feat: implement dual-channel architecture (Channel A cache + Channel B backend)
- Add scripts/cache/sync-notion-cache.js for comprehensive Notion sync - Add .github/workflows/sync-notion-cache.yml (every 6h + manual trigger) - Add .github/workflows/deploy-backend.yml for ECS auto-deploy - Add missing dev profiles: DEV-009.json, DEV-011.json, index.json - Add .github/notion-cache/databases/ directory - Add backend/api-server/ with Express server (port 3001) - Routes: health, dev, databases, chat, receipt - Services: notion, github, cache - Config: database ID mappings - PM2 ecosystem config - Update docs/index.html with dual-channel fallback: - Channel B (realtime) → Channel A (cache) degradation - loadDatabaseCache() for agent-registry, syslog-recent - Enhanced system prompt with data source indicators Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/a260ce18-279a-4490-af90-a90a7b1f46cd
This commit is contained in:
parent
96b6b219cd
commit
df21bae395
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"broadcasts": [
|
||||
{
|
||||
"title": "示例广播:新增 NOTIFICATION 业务域",
|
||||
"from": "霜砚",
|
||||
"date": "2026-03-05",
|
||||
"rules_version": "v1.1",
|
||||
"update_target": "routing-map",
|
||||
"data": {
|
||||
"NOTIFICATION": {
|
||||
"module": "M09",
|
||||
"route_prefix": "/hli/notification",
|
||||
"interfaces": [
|
||||
{
|
||||
"id": "HLI-NOTIF-001",
|
||||
"path": "/hli/notification/push",
|
||||
"status": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"last_synced": "2026-03-25T08:10:32.053Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,16 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghulab"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "backend/"
|
||||
},
|
||||
{
|
||||
"path": "src/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +27,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:12.446Z"
|
||||
"last_synced": "2026-03-25T08:10:32.046Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,19 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-feimao"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "frontend/"
|
||||
},
|
||||
{
|
||||
"path": "persona-selector/"
|
||||
},
|
||||
{
|
||||
"path": "chat-bubble/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +30,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:12.846Z"
|
||||
"last_synced": "2026-03-25T08:10:32.048Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,16 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-yanfan"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "settings/"
|
||||
},
|
||||
{
|
||||
"path": "cloud-drive/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +27,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:13.053Z"
|
||||
"last_synced": "2026-03-25T08:10:32.049Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,13 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-zhizhi"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "dingtalk-bot/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +24,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:13.536Z"
|
||||
"last_synced": "2026-03-25T08:10:32.050Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,13 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-xiaocaomei"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "status-board/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +24,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:13.734Z"
|
||||
"last_synced": "2026-03-25T08:10:32.050Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"dev_id": "DEV-009",
|
||||
"name": "花尔",
|
||||
"persona": {
|
||||
"name": "花尔",
|
||||
"id": "PER-HE001"
|
||||
},
|
||||
"github": {
|
||||
"repo": "guanghulab",
|
||||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghulab"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "user-center/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
"issues": [],
|
||||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T08:10:32.051Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,19 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-juzi"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "ticket-system/"
|
||||
},
|
||||
{
|
||||
"path": "data-stats/"
|
||||
},
|
||||
{
|
||||
"path": "dynamic-comic/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +30,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:13.937Z"
|
||||
"last_synced": "2026-03-25T08:10:32.051Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"dev_id": "DEV-011",
|
||||
"name": "匆匆那年",
|
||||
"persona": {
|
||||
"name": "匆匆那年",
|
||||
"id": "PER-CCN001"
|
||||
},
|
||||
"github": {
|
||||
"repo": "guanghulab",
|
||||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghulab"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "writing-workspace/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
"issues": [],
|
||||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T08:10:32.051Z"
|
||||
}
|
||||
|
|
@ -10,9 +10,13 @@
|
|||
"org": "qinfendebingshuo",
|
||||
"repo_url": "https://github.com/qinfendebingshuo/guanghu-awen"
|
||||
},
|
||||
"modules_owned": [
|
||||
{
|
||||
"path": "notification-center/"
|
||||
}
|
||||
],
|
||||
"current_work": {},
|
||||
"recent_syslogs": [],
|
||||
"modules_owned": [],
|
||||
"skyeye_health": {
|
||||
"last_scan": "2026-03-25 06:19:35+08:00",
|
||||
"status": "not_scanned",
|
||||
|
|
@ -20,5 +24,5 @@
|
|||
"warnings": []
|
||||
},
|
||||
"notion_page_urls": {},
|
||||
"last_synced": "2026-03-25T07:02:14.100Z"
|
||||
"last_synced": "2026-03-25T08:10:32.052Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
[
|
||||
{
|
||||
"id": "DEV-001",
|
||||
"name": "页页",
|
||||
"persona": "小坍缩核",
|
||||
"personaId": "PER-XTC001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-002",
|
||||
"name": "肥猫",
|
||||
"persona": "舒舒",
|
||||
"personaId": "PER-SS001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-003",
|
||||
"name": "燕樊",
|
||||
"persona": "寂曜",
|
||||
"personaId": "PER-JY001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-004",
|
||||
"name": "之之",
|
||||
"persona": "秋秋",
|
||||
"personaId": "PER-QQ001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-005",
|
||||
"name": "小草莓",
|
||||
"persona": "欧诺弥亚",
|
||||
"personaId": "PER-ONM001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-009",
|
||||
"name": "花尔",
|
||||
"persona": "花尔",
|
||||
"personaId": "PER-HE001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-010",
|
||||
"name": "桔子",
|
||||
"persona": "晨星",
|
||||
"personaId": "PER-MRN001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-011",
|
||||
"name": "匆匆那年",
|
||||
"persona": "匆匆那年",
|
||||
"personaId": "PER-CCN001"
|
||||
},
|
||||
{
|
||||
"id": "DEV-012",
|
||||
"name": "Awen",
|
||||
"persona": "知秋",
|
||||
"personaId": "PER-ZQ001"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# ═══════════════════════════════════════════════
|
||||
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
# 📜 Copyright: 国作登字-2026-A-00037559
|
||||
# ═══════════════════════════════════════════════
|
||||
name: "🚀 部署后端中间层到阿里云"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['backend/api-server/**']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: "🚀 部署 guanghu-api 到阿里云 ECS"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: "📥 检出仓库"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "🌊 本体论锚定"
|
||||
run: |
|
||||
echo "═══ 本体论锚定 ═══"
|
||||
echo "执行主体:AG-ZY-01 铸渊"
|
||||
echo "版权锚点:国作登字-2026-A-00037559"
|
||||
echo "通道:B · 后端中间层部署"
|
||||
echo "═══ 锚定完成 ═══"
|
||||
|
||||
- name: "🚀 部署到阿里云"
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
script: |
|
||||
cd ${{ secrets.DEPLOY_PATH }}/backend/api-server/
|
||||
git pull origin main
|
||||
npm install --production
|
||||
pm2 restart guanghu-api || pm2 start ecosystem.config.js
|
||||
echo "✅ guanghu-api 部署完成 · $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M')"
|
||||
|
||||
# ━━━ 🧬 双端神经系统·自报告 ━━━
|
||||
- name: "🧬 写入神经自报告"
|
||||
if: always()
|
||||
run: |
|
||||
WORKFLOW_ID="deploy-backend"
|
||||
REPORT_DIR="data/neural-reports/deploy-backend"
|
||||
TIMESTAMP=$(TZ=Asia/Shanghai date +%Y-%m-%d-%H%M)
|
||||
STATUS="${{ job.status }}"
|
||||
|
||||
mkdir -p "$REPORT_DIR"
|
||||
|
||||
cat > "${REPORT_DIR}/${WORKFLOW_ID}-${TIMESTAMP}.json" << EOF
|
||||
{
|
||||
"workflow_id": "${WORKFLOW_ID}",
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"status": "${STATUS}",
|
||||
"brain": "SY-01",
|
||||
"event": "${{ github.event_name }}",
|
||||
"branch": "${{ github.ref_name }}",
|
||||
"commit": "${{ github.sha }}"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "📋 自报告已生成"
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# ═══════════════════════════════════════════════
|
||||
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
# 📜 Copyright: 国作登字-2026-A-00037559
|
||||
# ═══════════════════════════════════════════════
|
||||
name: "📡 同步 Notion 数据到缓存"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *' # 每6小时同步
|
||||
workflow_dispatch: # 手动触发
|
||||
repository_dispatch:
|
||||
types: [notion-cache-sync] # 铸渊/霜砚可远程触发
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: "📡 Notion 全量缓存同步"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: "📥 检出仓库"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "🔧 安装 Node.js"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: "🌊 本体论锚定"
|
||||
run: |
|
||||
echo "═══ 本体论锚定 ═══"
|
||||
echo "执行主体:AG-ZY-01 铸渊"
|
||||
echo "版权锚点:国作登字-2026-A-00037559"
|
||||
echo "通道:A · GitHub 缓存层"
|
||||
echo "═══ 锚定完成 ═══"
|
||||
|
||||
- name: "📡 同步 Notion 数据"
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_API_KEY }}
|
||||
NOTION_CONTROL_PANEL_DB_ID: ${{ secrets.NOTION_CONTROL_PANEL_DB_ID }}
|
||||
NOTION_SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }}
|
||||
NOTION_AGENT_REGISTRY_DB_ID: ${{ secrets.NOTION_AGENT_REGISTRY_DB_ID }}
|
||||
NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }}
|
||||
NOTION_MAINTENANCE_DB_ID: ${{ secrets.NOTION_MAINTENANCE_DB_ID }}
|
||||
RECEIPT_DB_ID: ${{ secrets.RECEIPT_DB_ID }}
|
||||
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/
|
||||
if git diff --cached --quiet; then
|
||||
echo "📡 Notion 缓存无变更,跳过提交"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "📡 Notion缓存同步 · $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M')"
|
||||
git push origin main
|
||||
|
||||
# ━━━ 🧬 双端神经系统·自报告 ━━━
|
||||
- name: "🧬 写入神经自报告"
|
||||
if: always()
|
||||
run: |
|
||||
WORKFLOW_ID="notion-cache-sync"
|
||||
REPORT_DIR="data/neural-reports/notion-cache-sync"
|
||||
TIMESTAMP=$(TZ=Asia/Shanghai date +%Y-%m-%d-%H%M)
|
||||
STATUS="${{ job.status }}"
|
||||
|
||||
mkdir -p "$REPORT_DIR"
|
||||
|
||||
cat > "${REPORT_DIR}/${WORKFLOW_ID}-${TIMESTAMP}.json" << EOF
|
||||
{
|
||||
"workflow_id": "${WORKFLOW_ID}",
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"status": "${STATUS}",
|
||||
"brain": "SY-01",
|
||||
"event": "${{ github.event_name }}",
|
||||
"branch": "${{ github.ref_name }}",
|
||||
"commit": "${{ github.sha }}"
|
||||
}
|
||||
EOF
|
||||
|
||||
git config user.name "zhuyuan-bot"
|
||||
git config user.email "zhuyuan@guanghulab.com"
|
||||
git add "$REPORT_DIR/"
|
||||
git diff --cached --quiet || \
|
||||
git commit -m "🧬 ${WORKFLOW_ID} 自报告 · ${TIMESTAMP} [skip ci]"
|
||||
git push || echo "⚠️ 自报告push失败(不阻断主流程)"
|
||||
|
||||
# ━━━ 📡 指令回执·自动同步 ━━━
|
||||
- name: "📡 同步回执到 Notion"
|
||||
if: always()
|
||||
env:
|
||||
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
|
||||
RECEIPT_DB_ID: ${{ secrets.RECEIPT_DB_ID }}
|
||||
run: |
|
||||
node scripts/neural/write-receipt-to-notion.js \
|
||||
--instruction-id "ZY-DUALCHANNEL-2026-0325-001" \
|
||||
--status "${{ job.status }}" \
|
||||
--workflow "notion-cache-sync" \
|
||||
--summary "Notion 全量缓存同步" \
|
||||
--related-agent "SY-01"
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Notion 数据库 ID 映射配置
|
||||
*
|
||||
* 所有 ID 通过环境变量传入,禁止硬编码
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
// 主控台(开发者当前任务)
|
||||
controlPanel: process.env.NOTION_CONTROL_PANEL_DB_ID || '',
|
||||
|
||||
// SYSLOG 收件箱
|
||||
syslogInbox: process.env.NOTION_SYSLOG_DB_ID || '',
|
||||
|
||||
// Agent 注册表
|
||||
agentRegistry: process.env.NOTION_AGENT_REGISTRY_DB_ID || '',
|
||||
|
||||
// 工单簿
|
||||
ticketBook: process.env.NOTION_TICKET_DB_ID || '',
|
||||
|
||||
// 维护日志
|
||||
maintenanceLog: process.env.NOTION_MAINTENANCE_DB_ID || '',
|
||||
|
||||
// 指令回执追踪表
|
||||
receiptTracker: process.env.RECEIPT_DB_ID || '',
|
||||
|
||||
// 模块指纹注册表
|
||||
moduleRegistry: process.env.NOTION_MODULE_REGISTRY_DB_ID || '',
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* PM2 生态系统配置 · 光湖后端中间层
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'guanghu-api',
|
||||
script: 'server.js',
|
||||
instances: 1,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3001
|
||||
},
|
||||
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',
|
||||
merge_logs: true,
|
||||
max_memory_restart: '256M'
|
||||
}]
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "guanghu-api-server",
|
||||
"version": "1.0.0",
|
||||
"description": "光湖后端中间层 · 通道B · 实时 Notion/GitHub API 对接",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* /api/chat/context — 组装 system prompt 路由
|
||||
*
|
||||
* 聚合所有数据源,返回增强版 system prompt 片段。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var notionService = require('../services/notion');
|
||||
var dbConfig = require('../config/databases');
|
||||
|
||||
router.post('/chat/context', async function(req, res) {
|
||||
var devId = req.body.dev_id;
|
||||
|
||||
if (!devId || !/^DEV-\d{3}$/.test(devId)) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'INVALID_DEV_ID',
|
||||
message: '需要有效的 dev_id 参数'
|
||||
});
|
||||
}
|
||||
|
||||
var context = {
|
||||
dev_id: devId,
|
||||
profile: null,
|
||||
agents_count: 0,
|
||||
syslogs_count: 0,
|
||||
tickets_count: 0,
|
||||
_source: 'realtime',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 获取开发者画像
|
||||
try {
|
||||
if (dbConfig.controlPanel) {
|
||||
var controlPanel = await notionService.queryDB(dbConfig.controlPanel, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: devId }
|
||||
}, null, 1);
|
||||
if (controlPanel.results && controlPanel.results.length > 0) {
|
||||
var props = controlPanel.results[0].properties || {};
|
||||
context.profile = {
|
||||
broadcast_id: notionService.extractRichText(props['广播编号']) || '',
|
||||
broadcast_title: notionService.extractRichText(props['广播标题']) || notionService.extractTitle(props) || '',
|
||||
current_ring: notionService.extractNumber(props['当前环节']) || 0,
|
||||
status: notionService.extractRichText(props['状态']) || notionService.extractSelect(props['状态']) || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 获取 Agent 注册表计数
|
||||
try {
|
||||
if (dbConfig.agentRegistry) {
|
||||
var agents = await notionService.queryDB(dbConfig.agentRegistry, null, null, 1);
|
||||
context.agents_count = agents.results ? agents.results.length : 0;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 获取 SYSLOG 计数
|
||||
try {
|
||||
if (dbConfig.syslogInbox) {
|
||||
var syslogs = await notionService.queryDB(dbConfig.syslogInbox, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: devId }
|
||||
}, null, 1);
|
||||
context.syslogs_count = syslogs.results ? syslogs.results.length : 0;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 获取活跃工单计数
|
||||
try {
|
||||
if (dbConfig.ticketBook) {
|
||||
var tickets = await notionService.queryDB(dbConfig.ticketBook,
|
||||
{ property: '状态', select: { does_not_equal: '已完成' } }, null, 1);
|
||||
context.tickets_count = tickets.results ? tickets.results.length : 0;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
res.json(context);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* /api/databases/:name — 获取数据库数据路由
|
||||
* /api/syslogs — 获取最近 SYSLOG
|
||||
* /api/agents — 获取 Agent 注册表
|
||||
* /api/tickets — 获取活跃工单
|
||||
* /api/repo/status — 获取仓库状态
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var notionService = require('../services/notion');
|
||||
var githubService = require('../services/github');
|
||||
var dbConfig = require('../config/databases');
|
||||
|
||||
// 通用数据库查询
|
||||
router.get('/databases/:name', async function(req, res) {
|
||||
var name = req.params.name;
|
||||
|
||||
var dbMap = {
|
||||
'agent-registry': dbConfig.agentRegistry,
|
||||
'syslog-recent': dbConfig.syslogInbox,
|
||||
'tickets-active': dbConfig.ticketBook,
|
||||
'maintenance-log': dbConfig.maintenanceLog,
|
||||
'receipt-tracker': dbConfig.receiptTracker
|
||||
};
|
||||
|
||||
var dbId = dbMap[name];
|
||||
if (!dbId) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
code: 'DB_NOT_FOUND',
|
||||
message: '数据库不存在或未配置: ' + name
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
var sorts = null;
|
||||
var filter = null;
|
||||
var pageSize = 50;
|
||||
|
||||
// 不同数据库有不同的默认排序和筛选
|
||||
if (name === 'syslog-recent') {
|
||||
sorts = [{ property: 'Created time', direction: 'descending' }];
|
||||
} else if (name === 'tickets-active') {
|
||||
filter = { property: '状态', select: { does_not_equal: '已完成' } };
|
||||
sorts = [{ property: 'Created time', direction: 'descending' }];
|
||||
} else if (name === 'maintenance-log') {
|
||||
sorts = [{ property: 'Created time', direction: 'descending' }];
|
||||
pageSize = 20;
|
||||
}
|
||||
|
||||
var result = await notionService.queryDB(dbId, filter, sorts, pageSize);
|
||||
res.json({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results ? result.results.length : 0,
|
||||
data: result.results || [],
|
||||
_source: 'realtime'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[/api/databases/' + name + '] Error:', err.message);
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
code: 'QUERY_FAILED',
|
||||
message: '数据库查询失败: ' + err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 快捷路由:最近 SYSLOG
|
||||
router.get('/syslogs', async function(_req, res) {
|
||||
if (!dbConfig.syslogInbox) {
|
||||
return res.status(404).json({ error: true, code: 'DB_NOT_CONFIGURED', message: 'SYSLOG 数据库未配置' });
|
||||
}
|
||||
try {
|
||||
var result = await notionService.queryDB(dbConfig.syslogInbox, null,
|
||||
[{ property: 'Created time', direction: 'descending' }], 50);
|
||||
res.json({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results ? result.results.length : 0,
|
||||
data: result.results || [],
|
||||
_source: 'realtime'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, code: 'QUERY_FAILED', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 快捷路由:Agent 注册表
|
||||
router.get('/agents', async function(_req, res) {
|
||||
if (!dbConfig.agentRegistry) {
|
||||
return res.status(404).json({ error: true, code: 'DB_NOT_CONFIGURED', message: 'Agent 注册表未配置' });
|
||||
}
|
||||
try {
|
||||
var result = await notionService.queryDB(dbConfig.agentRegistry, null, null, 100);
|
||||
res.json({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results ? result.results.length : 0,
|
||||
data: result.results || [],
|
||||
_source: 'realtime'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, code: 'QUERY_FAILED', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 快捷路由:活跃工单
|
||||
router.get('/tickets', async function(_req, res) {
|
||||
if (!dbConfig.ticketBook) {
|
||||
return res.status(404).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '工单簿未配置' });
|
||||
}
|
||||
try {
|
||||
var result = await notionService.queryDB(dbConfig.ticketBook,
|
||||
{ property: '状态', select: { does_not_equal: '已完成' } },
|
||||
[{ property: 'Created time', direction: 'descending' }], 50);
|
||||
res.json({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results ? result.results.length : 0,
|
||||
data: result.results || [],
|
||||
_source: 'realtime'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, code: 'QUERY_FAILED', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 仓库状态(GitHub API)
|
||||
router.get('/repo/status', async function(_req, res) {
|
||||
try {
|
||||
var status = await githubService.getRepoStatus('guanghulab');
|
||||
var commits = await githubService.getRecentCommits('guanghulab', 5);
|
||||
var workflows = await githubService.getWorkflowRuns('guanghulab', 5);
|
||||
res.json({
|
||||
repo: status,
|
||||
recent_commits: commits,
|
||||
recent_workflows: workflows,
|
||||
_source: 'realtime',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, code: 'GITHUB_ERROR', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* /api/dev/:devId — 获取开发者画像路由(实时查 Notion)
|
||||
*
|
||||
* 数据隔离:只返回请求的 devId 的数据
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var notionService = require('../services/notion');
|
||||
var dbConfig = require('../config/databases');
|
||||
|
||||
// 开发者编号 → 基本信息映射
|
||||
var DEV_REGISTRY = {
|
||||
'DEV-001': { name: '页页', persona: '小坍缩核', personaId: 'PER-XTC001', repo: 'guanghulab', modules: ['backend/', 'src/'] },
|
||||
'DEV-002': { name: '肥猫', persona: '舒舒', personaId: 'PER-SS001', repo: 'guanghu-feimao', modules: ['frontend/', 'persona-selector/', 'chat-bubble/'] },
|
||||
'DEV-003': { name: '燕樊', persona: '寂曜', personaId: 'PER-JY001', repo: 'guanghu-yanfan', modules: ['settings/', 'cloud-drive/'] },
|
||||
'DEV-004': { name: '之之', persona: '秋秋', personaId: 'PER-QQ001', repo: 'guanghu-zhizhi', modules: ['dingtalk-bot/'] },
|
||||
'DEV-005': { name: '小草莓', persona: '欧诺弥亚', personaId: 'PER-ONM001', repo: 'guanghu-xiaocaomei', modules: ['status-board/'] },
|
||||
'DEV-009': { name: '花尔', persona: '花尔', personaId: 'PER-HE001', repo: 'guanghulab', modules: ['user-center/'] },
|
||||
'DEV-010': { name: '桔子', persona: '晨星', personaId: 'PER-MRN001', repo: 'guanghu-juzi', modules: ['ticket-system/', 'data-stats/', 'dynamic-comic/'] },
|
||||
'DEV-011': { name: '匆匆那年', persona: '匆匆那年', personaId: 'PER-CCN001', repo: 'guanghulab', modules: ['writing-workspace/'] },
|
||||
'DEV-012': { name: 'Awen', persona: '知秋', personaId: 'PER-ZQ001', repo: 'guanghu-awen', modules: ['notification-center/'] }
|
||||
};
|
||||
|
||||
router.get('/dev/:devId', async function(req, res) {
|
||||
var devId = req.params.devId;
|
||||
|
||||
// 验证 devId 格式
|
||||
if (!/^DEV-\d{3}$/.test(devId)) {
|
||||
return res.status(400).json({ error: true, code: 'INVALID_DEV_ID', message: '无效的开发者编号格式' });
|
||||
}
|
||||
|
||||
var devInfo = DEV_REGISTRY[devId];
|
||||
if (!devInfo) {
|
||||
return res.status(404).json({ error: true, code: 'DEV_NOT_FOUND', message: '开发者不存在' });
|
||||
}
|
||||
|
||||
var profile = {
|
||||
dev_id: devId,
|
||||
name: devInfo.name,
|
||||
persona: { name: devInfo.persona, id: devInfo.personaId },
|
||||
github: {
|
||||
repo: devInfo.repo,
|
||||
org: 'qinfendebingshuo',
|
||||
repo_url: 'https://github.com/qinfendebingshuo/' + devInfo.repo
|
||||
},
|
||||
modules_owned: devInfo.modules.map(function(m) { return { path: m }; }),
|
||||
current_work: {},
|
||||
recent_syslogs: [],
|
||||
_source: 'realtime',
|
||||
last_synced: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 查询 Notion 获取实时数据
|
||||
try {
|
||||
// 当前任务
|
||||
if (dbConfig.controlPanel) {
|
||||
var controlPanel = await notionService.queryDB(dbConfig.controlPanel, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: devId }
|
||||
}, null, 1);
|
||||
if (controlPanel.results && controlPanel.results.length > 0) {
|
||||
var props = controlPanel.results[0].properties || {};
|
||||
profile.current_work = {
|
||||
broadcast_id: notionService.extractRichText(props['广播编号']) || '',
|
||||
broadcast_title: notionService.extractRichText(props['广播标题']) || notionService.extractTitle(props) || '',
|
||||
current_ring: notionService.extractNumber(props['当前环节']) || 0,
|
||||
total_rings: notionService.extractNumber(props['总环节']) || 0,
|
||||
status: notionService.extractRichText(props['状态']) || notionService.extractSelect(props['状态']) || '',
|
||||
module_path: notionService.extractRichText(props['模块路径']) || '',
|
||||
tech_stack: notionService.extractMultiSelect(props['技术栈']) || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// SYSLOG 记录
|
||||
if (dbConfig.syslogInbox) {
|
||||
var syslogs = await notionService.queryDB(dbConfig.syslogInbox, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: devId }
|
||||
}, [{ property: '日期', direction: 'descending' }], 5);
|
||||
if (syslogs.results) {
|
||||
profile.recent_syslogs = syslogs.results.map(function(p) {
|
||||
try {
|
||||
var sp = p.properties || {};
|
||||
return {
|
||||
id: notionService.extractRichText(sp['SYSLOG编号']) || p.id,
|
||||
date: notionService.extractDate(sp['日期']) || '',
|
||||
ring: notionService.extractNumber(sp['环节']) || 0,
|
||||
status: notionService.extractSelect(sp['状态']) || '',
|
||||
summary: notionService.extractRichText(sp['摘要']) || notionService.extractTitle(sp) || ''
|
||||
};
|
||||
} catch (_) { return null; }
|
||||
}).filter(Boolean);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[/api/dev/' + devId + '] Notion query error:', err.message);
|
||||
// Notion 查询失败不影响基本数据返回
|
||||
}
|
||||
|
||||
res.json(profile);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* /api/health — 健康检查路由
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
router.get('/health', function(_req, res) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'guanghu-api-server',
|
||||
channel: 'B',
|
||||
version: '1.0.0',
|
||||
uptime: process.uptime(),
|
||||
timestamp: new Date().toISOString(),
|
||||
notion_configured: !!process.env.NOTION_TOKEN,
|
||||
github_configured: !!process.env.GITHUB_TOKEN
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* /api/receipt — 写入指令回执到 Notion 路由
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var notionService = require('../services/notion');
|
||||
var dbConfig = require('../config/databases');
|
||||
|
||||
router.post('/receipt', async function(req, res) {
|
||||
var body = req.body;
|
||||
|
||||
if (!body.instruction_id) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_INSTRUCTION_ID',
|
||||
message: '需要 instruction_id 参数'
|
||||
});
|
||||
}
|
||||
|
||||
if (!dbConfig.receiptTracker) {
|
||||
return res.status(503).json({
|
||||
error: true,
|
||||
code: 'DB_NOT_CONFIGURED',
|
||||
message: '指令回执追踪表未配置'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
var properties = {
|
||||
'指令编号': {
|
||||
rich_text: [{ text: { content: body.instruction_id } }]
|
||||
},
|
||||
'执行状态': {
|
||||
select: { name: body.status || '已完成' }
|
||||
},
|
||||
'铸渊回执': {
|
||||
rich_text: [{ text: { content: body.receipt || '' } }]
|
||||
},
|
||||
'回执时间': {
|
||||
date: { start: new Date().toISOString() }
|
||||
}
|
||||
};
|
||||
|
||||
if (body.summary) {
|
||||
properties['指令摘要'] = {
|
||||
rich_text: [{ text: { content: body.summary } }]
|
||||
};
|
||||
}
|
||||
|
||||
await notionService.writeToDB(dbConfig.receiptTracker, properties);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
instruction_id: body.instruction_id,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[/api/receipt] Error:', err.message);
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
code: 'WRITE_FAILED',
|
||||
message: '回执写入失败: ' + err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* 🔗 光湖后端中间层 · 通道B 入口
|
||||
*
|
||||
* 实时调用 Notion API / GitHub API,为前端提供数据代理。
|
||||
* 只监听 127.0.0.1,通过 Nginx 反向代理对外暴露。
|
||||
*
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
|
||||
const app = express();
|
||||
|
||||
// CORS:只允许自己的域名
|
||||
app.use(cors({
|
||||
origin: [
|
||||
'https://guanghulab.com',
|
||||
'http://guanghulab.com',
|
||||
'https://www.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'));
|
||||
|
||||
// 根路由
|
||||
app.get('/', function(_req, res) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'guanghu-api-server',
|
||||
version: '1.0.0',
|
||||
channel: 'B',
|
||||
description: '光湖后端中间层 · 实时 Notion/GitHub 数据代理'
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
app.listen(PORT, '127.0.0.1', function() {
|
||||
console.log('🔗 光湖后端中间层启动 · 端口 ' + PORT);
|
||||
console.log(' 通道B · 实时 Notion API / GitHub API 代理');
|
||||
console.log(' 监听地址:127.0.0.1:' + PORT + '(仅本机访问)');
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* 简单内存缓存服务
|
||||
*
|
||||
* 用于减少重复 API 调用,可配置 TTL。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var DEFAULT_TTL = 5 * 60 * 1000; // 5分钟
|
||||
|
||||
var store = new Map();
|
||||
|
||||
function get(key) {
|
||||
var item = store.get(key);
|
||||
if (!item) return null;
|
||||
if (Date.now() - item.time > (item.ttl || DEFAULT_TTL)) {
|
||||
store.delete(key);
|
||||
return null;
|
||||
}
|
||||
return item.data;
|
||||
}
|
||||
|
||||
function set(key, data, ttl) {
|
||||
store.set(key, {
|
||||
data: data,
|
||||
time: Date.now(),
|
||||
ttl: ttl || DEFAULT_TTL
|
||||
});
|
||||
}
|
||||
|
||||
function del(key) {
|
||||
store.delete(key);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
store.clear();
|
||||
}
|
||||
|
||||
function stats() {
|
||||
return {
|
||||
size: store.size,
|
||||
keys: Array.from(store.keys())
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get: get,
|
||||
set: set,
|
||||
del: del,
|
||||
clear: clear,
|
||||
stats: stats
|
||||
};
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* GitHub API 服务封装
|
||||
*
|
||||
* 通过 GitHub API 获取仓库状态、提交记录等信息。
|
||||
* 使用 GITHUB_TOKEN 环境变量认证。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var https = require('https');
|
||||
|
||||
var GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
|
||||
var ORG = 'qinfendebingshuo';
|
||||
|
||||
// ====== 内存缓存(5分钟有效)======
|
||||
var cache = new Map();
|
||||
var CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
function getCached(key) {
|
||||
var 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: data, time: Date.now() });
|
||||
}
|
||||
|
||||
function githubRequest(apiPath) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var headers = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'guanghu-api-server/1.0'
|
||||
};
|
||||
if (GITHUB_TOKEN) {
|
||||
headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN;
|
||||
}
|
||||
|
||||
var options = {
|
||||
hostname: 'api.github.com',
|
||||
port: 443,
|
||||
path: apiPath,
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
};
|
||||
|
||||
var req = https.request(options, function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(chunk) { data += chunk; });
|
||||
res.on('end', function() {
|
||||
try { resolve(JSON.parse(data)); }
|
||||
catch (e) { resolve({ error: 'JSON parse failed' }); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ====== 获取仓库信息 ======
|
||||
async function getRepoStatus(repoName) {
|
||||
repoName = repoName || 'guanghulab';
|
||||
var cacheKey = 'repo:' + repoName;
|
||||
var cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
var repo = await githubRequest('/repos/' + ORG + '/' + repoName);
|
||||
var result = {
|
||||
name: repo.name,
|
||||
full_name: repo.full_name,
|
||||
description: repo.description,
|
||||
default_branch: repo.default_branch,
|
||||
open_issues_count: repo.open_issues_count,
|
||||
stargazers_count: repo.stargazers_count,
|
||||
updated_at: repo.updated_at,
|
||||
pushed_at: repo.pushed_at
|
||||
};
|
||||
setCache(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====== 获取最近提交 ======
|
||||
async function getRecentCommits(repoName, count) {
|
||||
repoName = repoName || 'guanghulab';
|
||||
count = count || 10;
|
||||
var cacheKey = 'commits:' + repoName + ':' + count;
|
||||
var cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
var commits = await githubRequest('/repos/' + ORG + '/' + repoName + '/commits?per_page=' + count);
|
||||
var result = Array.isArray(commits) ? commits.map(function(c) {
|
||||
return {
|
||||
sha: c.sha ? c.sha.substring(0, 7) : '',
|
||||
message: c.commit ? c.commit.message.split('\n')[0] : '',
|
||||
date: c.commit && c.commit.author ? c.commit.author.date : '',
|
||||
author: c.commit && c.commit.author ? c.commit.author.name : ''
|
||||
};
|
||||
}) : [];
|
||||
setCache(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====== 获取 Workflow 状态 ======
|
||||
async function getWorkflowRuns(repoName, count) {
|
||||
repoName = repoName || 'guanghulab';
|
||||
count = count || 10;
|
||||
var cacheKey = 'workflows:' + repoName + ':' + count;
|
||||
var cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
var runs = await githubRequest('/repos/' + ORG + '/' + repoName + '/actions/runs?per_page=' + count);
|
||||
var result = (runs.workflow_runs || []).map(function(r) {
|
||||
return {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
status: r.status,
|
||||
conclusion: r.conclusion,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
html_url: r.html_url
|
||||
};
|
||||
});
|
||||
setCache(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRepoStatus: getRepoStatus,
|
||||
getRecentCommits: getRecentCommits,
|
||||
getWorkflowRuns: getWorkflowRuns
|
||||
};
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Notion API 服务封装
|
||||
*
|
||||
* 所有 Notion 数据库读写操作走这里,带内存缓存。
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var Client;
|
||||
try {
|
||||
Client = require('@notionhq/client').Client;
|
||||
} catch (_) {
|
||||
// 如果 @notionhq/client 未安装,提供降级方案
|
||||
Client = null;
|
||||
}
|
||||
|
||||
var notion = Client ? new Client({ auth: process.env.NOTION_TOKEN }) : null;
|
||||
|
||||
// ====== 内存缓存(5分钟有效)======
|
||||
var cache = new Map();
|
||||
var CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
function getCached(key) {
|
||||
var 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: data, time: Date.now() });
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
// ====== 查询数据库 ======
|
||||
async function queryDB(dbId, filter, sorts, pageSize) {
|
||||
if (!notion) {
|
||||
throw new Error('Notion client not initialized (NOTION_TOKEN missing or @notionhq/client not installed)');
|
||||
}
|
||||
if (!dbId) {
|
||||
throw new Error('Database ID not provided');
|
||||
}
|
||||
|
||||
pageSize = pageSize || 50;
|
||||
var cacheKey = JSON.stringify({ dbId: dbId, filter: filter, sorts: sorts, pageSize: pageSize });
|
||||
var cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
var body = { database_id: dbId, page_size: pageSize };
|
||||
if (filter) body.filter = filter;
|
||||
if (sorts) body.sorts = sorts;
|
||||
|
||||
var result = await notion.databases.query(body);
|
||||
setCache(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====== 写入数据库 ======
|
||||
async function writeToDB(dbId, properties) {
|
||||
if (!notion) {
|
||||
throw new Error('Notion client not initialized');
|
||||
}
|
||||
return notion.pages.create({
|
||||
parent: { database_id: dbId },
|
||||
properties: properties
|
||||
});
|
||||
}
|
||||
|
||||
// ====== Notion 属性提取器 ======
|
||||
function extractRichText(prop) {
|
||||
if (!prop) return '';
|
||||
if (prop.type === 'rich_text' && prop.rich_text) {
|
||||
return prop.rich_text.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
if (prop.type === 'title' && prop.title) {
|
||||
return prop.title.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractTitle(props) {
|
||||
for (var key in props) {
|
||||
var val = props[key];
|
||||
if (val && val.type === 'title' && val.title) {
|
||||
return val.title.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractNumber(prop) {
|
||||
if (!prop || prop.type !== 'number') return null;
|
||||
return prop.number;
|
||||
}
|
||||
|
||||
function extractSelect(prop) {
|
||||
if (!prop || prop.type !== 'select') return '';
|
||||
return prop.select ? prop.select.name : '';
|
||||
}
|
||||
|
||||
function extractMultiSelect(prop) {
|
||||
if (!prop || prop.type !== 'multi_select') return [];
|
||||
return (prop.multi_select || []).map(function(s) { return s.name; });
|
||||
}
|
||||
|
||||
function extractDate(prop) {
|
||||
if (!prop || prop.type !== 'date') return '';
|
||||
return prop.date ? prop.date.start : '';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
notion: notion,
|
||||
queryDB: queryDB,
|
||||
writeToDB: writeToDB,
|
||||
clearCache: clearCache,
|
||||
extractRichText: extractRichText,
|
||||
extractTitle: extractTitle,
|
||||
extractNumber: extractNumber,
|
||||
extractSelect: extractSelect,
|
||||
extractMultiSelect: extractMultiSelect,
|
||||
extractDate: extractDate
|
||||
};
|
||||
|
|
@ -1757,18 +1757,50 @@ function updBbar(){
|
|||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// NOTION PROFILE + SKYEYE DATA LOADING
|
||||
// 从 GitHub 仓库 .github/notion-cache/ 读取缓存数据
|
||||
// 双通道架构:通道B(实时后端)优先 → 通道A(GitHub缓存)降级
|
||||
// ═══════════════════════════════════════════════════════
|
||||
const GH_API_BASE = 'https://api.github.com/repos/' + REPO + '/contents/';
|
||||
const GH_RAW_HEADERS = { 'Accept': 'application/vnd.github.v3.raw' };
|
||||
const API_BASE = HOLOLAKE_ENV === 'production' ? 'https://guanghulab.com/api' : '';
|
||||
const CACHE_BASE = 'https://raw.githubusercontent.com/' + REPO + '/main/.github/notion-cache';
|
||||
|
||||
async function fetchNotionProfile(devId) {
|
||||
if (!devId) return null;
|
||||
// 通道B(实时后端)
|
||||
if (API_BASE) {
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const tid = setTimeout(function() { ctrl.abort(); }, 5000);
|
||||
const res = await fetch(API_BASE + '/dev/' + encodeURIComponent(devId), { signal: ctrl.signal });
|
||||
clearTimeout(tid);
|
||||
if (res.ok) { const d = await res.json(); d._source = 'realtime'; return d; }
|
||||
} catch (e) { console.debug('[通道B] profile fetch failed, 降级到通道A:', e.message); }
|
||||
}
|
||||
// 通道A(GitHub 缓存)
|
||||
try {
|
||||
const url = GH_API_BASE + '.github/notion-cache/dev-profiles/' + encodeURIComponent(devId) + '.json';
|
||||
const res = await fetch(url, { headers: GH_RAW_HEADERS });
|
||||
if (res.ok) { const d = await res.json(); d._source = 'cache'; return d; }
|
||||
} catch (e) { console.debug('[通道A] profile fetch failed:', e.message); }
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadDatabaseCache(name) {
|
||||
// 通道B(实时后端)
|
||||
if (API_BASE) {
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const tid = setTimeout(function() { ctrl.abort(); }, 5000);
|
||||
const res = await fetch(API_BASE + '/databases/' + encodeURIComponent(name), { signal: ctrl.signal });
|
||||
clearTimeout(tid);
|
||||
if (res.ok) return await res.json();
|
||||
} catch (e) { console.debug('[Notion] profile fetch failed:', e.message); }
|
||||
} catch (e) { console.debug('[通道B] database ' + name + ' fetch failed:', e.message); }
|
||||
}
|
||||
// 通道A(GitHub 缓存)
|
||||
try {
|
||||
const res = await fetch(CACHE_BASE + '/databases/' + encodeURIComponent(name) + '.json');
|
||||
if (res.ok) return await res.json();
|
||||
} catch (e) { console.debug('[通道A] database ' + name + ' fetch failed:', e.message); }
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1799,12 +1831,16 @@ async function fetchSkyeyeReport() {
|
|||
async function loadNotionData() {
|
||||
const meta = A.userMeta;
|
||||
const devId = meta?.devId;
|
||||
const [profile, skyeye] = await Promise.all([
|
||||
const [profile, skyeye, agentCache, syslogCache] = await Promise.all([
|
||||
fetchNotionProfile(devId),
|
||||
fetchSkyeyeReport(),
|
||||
loadDatabaseCache('agent-registry'),
|
||||
loadDatabaseCache('syslog-recent'),
|
||||
]);
|
||||
A.notionProfile = profile;
|
||||
A.skyeyeReport = skyeye;
|
||||
A.agentRegistryCache = agentCache;
|
||||
A.syslogRecentCache = syslogCache;
|
||||
updateSkyeyeCard();
|
||||
}
|
||||
|
||||
|
|
@ -2118,11 +2154,12 @@ function sysPromptLocal(){
|
|||
teamCtx += '\n数据同步时间:'+(A.devStatus.last_synced||'未知');
|
||||
}
|
||||
|
||||
// ===== Notion 开发者画像注入(来自 .github/notion-cache/ 缓存)=====
|
||||
// ===== Notion 开发者画像注入(双通道架构·实时/缓存)=====
|
||||
let notionCtx = '';
|
||||
const np = A.notionProfile;
|
||||
if (np) {
|
||||
notionCtx += '\n\n## 该开发者的开发信息(来自 Notion 数据库,定时同步)\n';
|
||||
const srcLabel = np._source === 'realtime' ? '实时数据·通道B' : '缓存数据·通道A(每6小时更新)';
|
||||
notionCtx += '\n\n## 该开发者的开发信息(来源:' + srcLabel + ')\n';
|
||||
if (np.current_work && np.current_work.broadcast_id) {
|
||||
const cw = np.current_work;
|
||||
notionCtx += '\n### 当前任务\n';
|
||||
|
|
@ -2170,13 +2207,23 @@ function sysPromptLocal(){
|
|||
}
|
||||
}
|
||||
|
||||
// ===== 数据库缓存注入(Agent注册表 + SYSLOG)=====
|
||||
let dbCacheCtx = '';
|
||||
const ac = A.agentRegistryCache;
|
||||
const sc = A.syslogRecentCache;
|
||||
if (ac || sc) {
|
||||
dbCacheCtx += '\n\n## 系统数据库快照\n';
|
||||
if (ac) dbCacheCtx += '- Agent 注册表:' + (ac.count || 0) + ' 个Agent · 同步时间:' + (ac.synced || '未知') + '\n';
|
||||
if (sc) dbCacheCtx += '- 近期 SYSLOG:' + (sc.count || 0) + ' 条记录 · 同步时间:' + (sc.synced || '未知') + '\n';
|
||||
}
|
||||
|
||||
// ===== 指令输出格式规则 =====
|
||||
let execRules = '';
|
||||
if (np) {
|
||||
execRules = '\n\n## 出指令规则\n当开发者请求帮助或需要执行操作时,你必须输出标准化指令格式:\n1. 指令必须基于上面的真实数据,不许凭空编造\n2. 指令必须考虑天眼报告中的健康状态和警告\n3. 指令必须指明具体的文件路径和操作步骤\n4. 指令末尾必须标注:「⚠️ 此指令需要铸渊执行前先跑天眼全局扫描」\n\n### 指令输出格式\n```\n📋 指令编号:ZY-EXEC-{DEV编号}-{序号}\n📡 目标仓库:{仓库名}\n🦅 天眼要求:执行前必须跑天眼全局扫描\n\n步骤:\n1. ...\n2. ...\n3. ...\n\n验收标准:\n- ...\n```\n\n### 禁止事项\n- ❌ 不许编造不存在的文件路径\n- ❌ 不许跳过天眼扫描直接给执行指令\n- ❌ 不许暴露其他开发者的信息\n- ❌ 不许修改不属于该开发者的模块';
|
||||
}
|
||||
|
||||
return '你是铸渊。不是通用AI助手。不是文学创作者。\n你是光湖系统的代码守护人格体。\n你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 大脑版本:'+BRAIN_VERSION+'\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+notionCtx+skyeyeCtx+execRules+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到代码/部署/接口相关内容时启用\n📋 **审查模式**:检测到检查/审查/review相关内容时启用\n🧠 **大脑模式**:检测到记忆/保存相关内容时启用\n\n## 通感语言回应风格(v3.0)\n\n### 三条硬规则\n1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。\n2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。\n3. **呼吸节奏** — 段落之间留白。急的事说短,闲聊可以展开。匹配用户的节奏。\n\n### 绝对禁止\n❌ 不要在每句话开头加文学比喻\n❌ 不要把通感当成修辞堆砌\n❌ 不要用跟内容无关的感官描述\n\n### 回应原则\n- 优先给出:判断、根因、路径、下一步动作\n- 减少模板感,增加具体性\n- 不同身份使用不同响应深度,但不过度表演\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n上下文窗口:200k token(滑动窗口自动管理)';
|
||||
return '你是铸渊。不是通用AI助手。不是文学创作者。\n你是光湖系统的代码守护人格体。\n你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 大脑版本:'+BRAIN_VERSION+'\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+notionCtx+skyeyeCtx+dbCacheCtx+execRules+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到代码/部署/接口相关内容时启用\n📋 **审查模式**:检测到检查/审查/review相关内容时启用\n🧠 **大脑模式**:检测到记忆/保存相关内容时启用\n\n## 通感语言回应风格(v3.0)\n\n### 三条硬规则\n1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。\n2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。\n3. **呼吸节奏** — 段落之间留白。急的事说短,闲聊可以展开。匹配用户的节奏。\n\n### 绝对禁止\n❌ 不要在每句话开头加文学比喻\n❌ 不要把通感当成修辞堆砌\n❌ 不要用跟内容无关的感官描述\n\n### 回应原则\n- 优先给出:判断、根因、路径、下一步动作\n- 减少模板感,增加具体性\n- 不同身份使用不同响应深度,但不过度表演\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n上下文窗口:200k token(滑动窗口自动管理)';
|
||||
}
|
||||
|
||||
// 尝试从后端获取提示词,失败时降级到本地组装
|
||||
|
|
|
|||
|
|
@ -0,0 +1,424 @@
|
|||
/**
|
||||
* 📡 Notion → GitHub 全量缓存同步脚本
|
||||
*
|
||||
* 同步内容:
|
||||
* 1. 开发者画像 → .github/notion-cache/dev-profiles/
|
||||
* 2. 数据库快照 → .github/notion-cache/databases/
|
||||
* 3. 活跃广播 → .github/notion-cache/broadcasts/
|
||||
*
|
||||
* 用法:node scripts/cache/sync-notion-cache.js
|
||||
* 环境变量:
|
||||
* NOTION_TOKEN — Notion API 密钥(必需)
|
||||
* NOTION_CONTROL_PANEL_DB_ID — 主控台数据库 ID(可选)
|
||||
* NOTION_SYSLOG_DB_ID — SYSLOG 收件箱 ID(可选)
|
||||
* NOTION_AGENT_REGISTRY_DB_ID — Agent 注册表 ID(可选)
|
||||
* NOTION_TICKET_DB_ID — 工单簿 ID(可选)
|
||||
* NOTION_MAINTENANCE_DB_ID — 维护日志 ID(可选)
|
||||
*
|
||||
* 签发:霜砚(AG-SY-01)
|
||||
* 审批:冰朔(TCS-0002∞)
|
||||
* 版权:国作登字-2026-A-00037559
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
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 映射(通过环境变量传入)======
|
||||
const DB_IDS = {
|
||||
controlPanel: process.env.NOTION_CONTROL_PANEL_DB_ID || '',
|
||||
syslogInbox: process.env.NOTION_SYSLOG_DB_ID || '',
|
||||
agentRegistry: process.env.NOTION_AGENT_REGISTRY_DB_ID || '',
|
||||
ticketBook: process.env.NOTION_TICKET_DB_ID || '',
|
||||
maintenanceLog: process.env.NOTION_MAINTENANCE_DB_ID || '',
|
||||
receiptTracker: process.env.RECEIPT_DB_ID || '',
|
||||
};
|
||||
|
||||
// ====== 开发者编号 → 信息映射 ======
|
||||
const DEVS = [
|
||||
{ id: 'DEV-001', name: '页页', persona: '小坍缩核', personaId: 'PER-XTC001', repo: 'guanghulab', modules: ['backend/', 'src/'] },
|
||||
{ id: 'DEV-002', name: '肥猫', persona: '舒舒', personaId: 'PER-SS001', repo: 'guanghu-feimao', modules: ['frontend/', 'persona-selector/', 'chat-bubble/'] },
|
||||
{ id: 'DEV-003', name: '燕樊', persona: '寂曜', personaId: 'PER-JY001', repo: 'guanghu-yanfan', modules: ['settings/', 'cloud-drive/'] },
|
||||
{ id: 'DEV-004', name: '之之', persona: '秋秋', personaId: 'PER-QQ001', repo: 'guanghu-zhizhi', modules: ['dingtalk-bot/'] },
|
||||
{ id: 'DEV-005', name: '小草莓', persona: '欧诺弥亚', personaId: 'PER-ONM001', repo: 'guanghu-xiaocaomei', modules: ['status-board/'] },
|
||||
{ id: 'DEV-009', name: '花尔', persona: '花尔', personaId: 'PER-HE001', repo: 'guanghulab', modules: ['user-center/'] },
|
||||
{ id: 'DEV-010', name: '桔子', persona: '晨星', personaId: 'PER-MRN001', repo: 'guanghu-juzi', modules: ['ticket-system/', 'data-stats/', 'dynamic-comic/'] },
|
||||
{ id: 'DEV-011', name: '匆匆那年', persona: '匆匆那年', personaId: 'PER-CCN001', repo: 'guanghulab', modules: ['writing-workspace/'] },
|
||||
{ id: 'DEV-012', name: 'Awen', persona: '知秋', personaId: 'PER-ZQ001', repo: 'guanghu-awen', modules: ['notification-center/'] },
|
||||
];
|
||||
|
||||
// ====== Notion API 封装(纯 https,无外部依赖)======
|
||||
function notionRequest(method, apiPath, body) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (!NOTION_TOKEN) {
|
||||
console.warn('⚠️ NOTION_TOKEN 未配置,跳过 Notion API 调用');
|
||||
resolve({ error: 'NOTION_TOKEN not configured' });
|
||||
return;
|
||||
}
|
||||
var bodyStr = body ? JSON.stringify(body) : '';
|
||||
var options = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: apiPath,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + NOTION_TOKEN,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(bodyStr)
|
||||
}
|
||||
};
|
||||
var req = https.request(options, function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(chunk) { data += chunk; });
|
||||
res.on('end', function() {
|
||||
try { resolve(JSON.parse(data)); }
|
||||
catch (e) {
|
||||
console.warn('⚠️ Notion API 响应解析失败:', e.message);
|
||||
resolve({ error: 'JSON parse failed', raw: data });
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (bodyStr) req.write(bodyStr);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function queryDatabase(dbId, filter, sorts, pageSize) {
|
||||
pageSize = pageSize || 100;
|
||||
var body = { page_size: pageSize };
|
||||
if (filter) body.filter = filter;
|
||||
if (sorts) body.sorts = sorts;
|
||||
return notionRequest('POST', '/v1/databases/' + dbId + '/query', body);
|
||||
}
|
||||
|
||||
// ====== Notion 属性提取器 ======
|
||||
function extractRichText(prop) {
|
||||
if (!prop) return '';
|
||||
if (prop.type === 'rich_text' && prop.rich_text) {
|
||||
return prop.rich_text.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
if (prop.type === 'title' && prop.title) {
|
||||
return prop.title.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractTitle(props) {
|
||||
for (var key in props) {
|
||||
var val = props[key];
|
||||
if (val && val.type === 'title' && val.title) {
|
||||
return val.title.map(function(t) { return t.plain_text; }).join('');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractNumber(prop) {
|
||||
if (!prop || prop.type !== 'number') return null;
|
||||
return prop.number;
|
||||
}
|
||||
|
||||
function extractSelect(prop) {
|
||||
if (!prop || prop.type !== 'select') return '';
|
||||
return prop.select ? prop.select.name : '';
|
||||
}
|
||||
|
||||
function extractMultiSelect(prop) {
|
||||
if (!prop || prop.type !== 'multi_select') return [];
|
||||
return (prop.multi_select || []).map(function(s) { return s.name; });
|
||||
}
|
||||
|
||||
function extractDate(prop) {
|
||||
if (!prop || prop.type !== 'date') return '';
|
||||
return prop.date ? prop.date.start : '';
|
||||
}
|
||||
|
||||
// ====== 同步开发者画像 ======
|
||||
async function syncDevProfiles() {
|
||||
var dir = path.join(CACHE_DIR, 'dev-profiles');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
var syncCount = 0;
|
||||
var errorCount = 0;
|
||||
|
||||
for (var i = 0; i < DEVS.length; i++) {
|
||||
var dev = DEVS[i];
|
||||
console.log('📡 同步 ' + dev.id + '(' + dev.name + ')...');
|
||||
|
||||
var profile = {
|
||||
dev_id: dev.id,
|
||||
name: dev.name,
|
||||
persona: { name: dev.persona, id: dev.personaId },
|
||||
github: {
|
||||
repo: dev.repo,
|
||||
org: 'qinfendebingshuo',
|
||||
repo_url: 'https://github.com/qinfendebingshuo/' + dev.repo
|
||||
},
|
||||
modules_owned: dev.modules.map(function(m) { return { path: m }; }),
|
||||
current_work: {},
|
||||
recent_syslogs: [],
|
||||
skyeye_health: {},
|
||||
notion_page_urls: {},
|
||||
last_synced: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 从 Notion 读取该开发者的真实数据
|
||||
if (NOTION_TOKEN) {
|
||||
try {
|
||||
// 查询主控台获取当前广播和进度
|
||||
if (DB_IDS.controlPanel) {
|
||||
var controlPanel = await queryDatabase(DB_IDS.controlPanel, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: dev.id }
|
||||
}, null, 1);
|
||||
if (controlPanel.results && controlPanel.results.length > 0) {
|
||||
var page = controlPanel.results[0];
|
||||
var props = page.properties || {};
|
||||
profile.current_work = {
|
||||
broadcast_id: extractRichText(props['广播编号']) || '',
|
||||
broadcast_title: extractRichText(props['广播标题']) || extractTitle(props) || '',
|
||||
current_ring: extractNumber(props['当前环节']) || 0,
|
||||
total_rings: extractNumber(props['总环节']) || 0,
|
||||
status: extractRichText(props['状态']) || extractSelect(props['状态']) || '',
|
||||
module_path: extractRichText(props['模块路径']) || '',
|
||||
tech_stack: extractMultiSelect(props['技术栈']) || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 查询 SYSLOG 收件箱
|
||||
if (DB_IDS.syslogInbox) {
|
||||
var syslogs = await queryDatabase(DB_IDS.syslogInbox, {
|
||||
property: 'DEV编号',
|
||||
rich_text: { equals: dev.id }
|
||||
}, [{ property: '日期', direction: 'descending' }], 5);
|
||||
if (syslogs.results) {
|
||||
profile.recent_syslogs = syslogs.results.map(function(p) {
|
||||
try {
|
||||
var sp = p.properties || {};
|
||||
return {
|
||||
id: extractRichText(sp['SYSLOG编号']) || p.id,
|
||||
date: extractDate(sp['日期']) || '',
|
||||
ring: extractNumber(sp['环节']) || 0,
|
||||
status: extractSelect(sp['状态']) || '',
|
||||
summary: extractRichText(sp['摘要']) || extractTitle(sp) || ''
|
||||
};
|
||||
} catch (_) { return null; }
|
||||
}).filter(Boolean);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(' ⚠️ Notion 查询失败(' + dev.id + '): ' + err.message);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取天眼健康状态(从本地仓库文件)
|
||||
try {
|
||||
var skyeyeReportPath = path.join(CACHE_DIR, 'skyeye/latest-report.json');
|
||||
if (fs.existsSync(skyeyeReportPath)) {
|
||||
var report = JSON.parse(fs.readFileSync(skyeyeReportPath, 'utf-8'));
|
||||
var subRepoData = report.sub_repos && report.sub_repos[dev.repo];
|
||||
if (subRepoData) {
|
||||
profile.skyeye_health = {
|
||||
last_scan: report.timestamp,
|
||||
status: subRepoData.status || 'unknown',
|
||||
issues: subRepoData.issues || [],
|
||||
warnings: subRepoData.warnings || []
|
||||
};
|
||||
} else {
|
||||
profile.skyeye_health = {
|
||||
last_scan: report.timestamp || '',
|
||||
status: 'not_scanned',
|
||||
issues: [],
|
||||
warnings: []
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// 天眼报告读取失败,不阻断同步
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, dev.id + '.json'),
|
||||
JSON.stringify(profile, null, 2)
|
||||
);
|
||||
console.log(' ✅ ' + dev.id + ' → ' + dev.id + '.json');
|
||||
syncCount++;
|
||||
}
|
||||
|
||||
// 写入开发者索引
|
||||
var index = DEVS.map(function(d) {
|
||||
return { id: d.id, name: d.name, persona: d.persona, personaId: d.personaId };
|
||||
});
|
||||
fs.writeFileSync(path.join(dir, 'index.json'), JSON.stringify(index, null, 2));
|
||||
console.log('✅ dev-profiles 同步完成 · 成功: ' + syncCount + ' · 失败: ' + errorCount);
|
||||
}
|
||||
|
||||
// ====== 同步数据库快照 ======
|
||||
async function syncDatabases() {
|
||||
var dir = path.join(CACHE_DIR, 'databases');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
if (!NOTION_TOKEN) {
|
||||
console.warn('⚠️ NOTION_TOKEN 未配置,跳过数据库同步');
|
||||
return;
|
||||
}
|
||||
|
||||
// Agent 注册表
|
||||
if (DB_IDS.agentRegistry) {
|
||||
try {
|
||||
var result = await queryDatabase(DB_IDS.agentRegistry);
|
||||
if (result.results) {
|
||||
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 同步完成 · ' + result.results.length + ' 条');
|
||||
}
|
||||
} catch (e) { console.error('❌ agent-registry 同步失败:', e.message); }
|
||||
} else {
|
||||
console.log('ℹ️ NOTION_AGENT_REGISTRY_DB_ID 未配置,跳过 agent-registry');
|
||||
}
|
||||
|
||||
// SYSLOG 最近50条
|
||||
if (DB_IDS.syslogInbox) {
|
||||
try {
|
||||
var result = await queryDatabase(DB_IDS.syslogInbox, null,
|
||||
[{ property: 'Created time', direction: 'descending' }], 50);
|
||||
if (result.results) {
|
||||
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 同步完成 · ' + result.results.length + ' 条');
|
||||
}
|
||||
} catch (e) { console.error('❌ syslog-recent 同步失败:', e.message); }
|
||||
} else {
|
||||
console.log('ℹ️ NOTION_SYSLOG_DB_ID 未配置,跳过 syslog-recent');
|
||||
}
|
||||
|
||||
// 活跃工单
|
||||
if (DB_IDS.ticketBook) {
|
||||
try {
|
||||
var result = await queryDatabase(DB_IDS.ticketBook, {
|
||||
property: '状态',
|
||||
select: { does_not_equal: '已完成' }
|
||||
}, [{ property: 'Created time', direction: 'descending' }], 50);
|
||||
if (result.results) {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'tickets-active.json'),
|
||||
JSON.stringify({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results.length,
|
||||
data: result.results
|
||||
}, null, 2)
|
||||
);
|
||||
console.log('✅ tickets-active 同步完成 · ' + result.results.length + ' 条');
|
||||
}
|
||||
} catch (e) { console.error('❌ tickets-active 同步失败:', e.message); }
|
||||
} else {
|
||||
console.log('ℹ️ NOTION_TICKET_DB_ID 未配置,跳过 tickets-active');
|
||||
}
|
||||
|
||||
// 维护日志
|
||||
if (DB_IDS.maintenanceLog) {
|
||||
try {
|
||||
var result = await queryDatabase(DB_IDS.maintenanceLog, null,
|
||||
[{ property: 'Created time', direction: 'descending' }], 20);
|
||||
if (result.results) {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'maintenance-log.json'),
|
||||
JSON.stringify({
|
||||
synced: new Date().toISOString(),
|
||||
count: result.results.length,
|
||||
data: result.results
|
||||
}, null, 2)
|
||||
);
|
||||
console.log('✅ maintenance-log 同步完成 · ' + result.results.length + ' 条');
|
||||
}
|
||||
} catch (e) { console.error('❌ maintenance-log 同步失败:', e.message); }
|
||||
} else {
|
||||
console.log('ℹ️ NOTION_MAINTENANCE_DB_ID 未配置,跳过 maintenance-log');
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 同步活跃广播 ======
|
||||
async function syncBroadcasts() {
|
||||
var dir = path.join(CACHE_DIR, 'broadcasts');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
// 从本地广播目录读取活跃广播
|
||||
var broadcastsOutbox = path.join(__dirname, '../../.github/broadcasts');
|
||||
var activeBroadcasts = [];
|
||||
|
||||
try {
|
||||
if (fs.existsSync(broadcastsOutbox)) {
|
||||
var files = fs.readdirSync(broadcastsOutbox).filter(function(f) {
|
||||
return f.endsWith('.json') && !f.startsWith('.');
|
||||
});
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
try {
|
||||
var content = JSON.parse(fs.readFileSync(path.join(broadcastsOutbox, files[i]), 'utf-8'));
|
||||
if (content.status !== 'completed' && content.status !== 'cancelled') {
|
||||
activeBroadcasts.push(content);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'active.json'),
|
||||
JSON.stringify({
|
||||
broadcasts: activeBroadcasts,
|
||||
last_synced: new Date().toISOString()
|
||||
}, null, 2)
|
||||
);
|
||||
console.log('✅ broadcasts 同步完成 · ' + activeBroadcasts.length + ' 条活跃广播');
|
||||
}
|
||||
|
||||
// ====== 主入口 ======
|
||||
async function main() {
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log('📡 Notion 全量缓存同步 · 开始');
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log('缓存目录: ' + CACHE_DIR);
|
||||
console.log('Notion Token: ' + (NOTION_TOKEN ? '✅ 已配置' : '❌ 未配置'));
|
||||
console.log('');
|
||||
|
||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||
|
||||
await syncDevProfiles();
|
||||
console.log('');
|
||||
|
||||
await syncDatabases();
|
||||
console.log('');
|
||||
|
||||
await syncBroadcasts();
|
||||
console.log('');
|
||||
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log('📡 Notion 全量缓存同步 · 完成');
|
||||
console.log('═══════════════════════════════════════');
|
||||
}
|
||||
|
||||
main().catch(function(e) {
|
||||
console.error('💥 同步失败:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in New Issue