From df21bae39501879c1e7661554d4d9edfe8e31d1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:10:58 +0000 Subject: [PATCH] feat: implement dual-channel architecture (Channel A cache + Channel B backend) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .github/notion-cache/broadcasts/active.json | 25 ++ .github/notion-cache/databases/.gitkeep | 0 .../notion-cache/dev-profiles/DEV-001.json | 11 +- .../notion-cache/dev-profiles/DEV-002.json | 14 +- .../notion-cache/dev-profiles/DEV-003.json | 11 +- .../notion-cache/dev-profiles/DEV-004.json | 8 +- .../notion-cache/dev-profiles/DEV-005.json | 8 +- .../notion-cache/dev-profiles/DEV-009.json | 28 ++ .../notion-cache/dev-profiles/DEV-010.json | 14 +- .../notion-cache/dev-profiles/DEV-011.json | 28 ++ .../notion-cache/dev-profiles/DEV-012.json | 8 +- .github/notion-cache/dev-profiles/index.json | 56 +++ .github/workflows/deploy-backend.yml | 71 +++ .github/workflows/sync-notion-cache.yml | 106 +++++ backend/api-server/config/databases.js | 31 ++ backend/api-server/ecosystem.config.js | 22 + backend/api-server/package.json | 18 + backend/api-server/routes/chat.js | 86 ++++ backend/api-server/routes/databases.js | 148 ++++++ backend/api-server/routes/dev.js | 108 +++++ backend/api-server/routes/health.js | 24 + backend/api-server/routes/receipt.js | 72 +++ backend/api-server/server.js | 53 +++ backend/api-server/services/cache.js | 53 +++ backend/api-server/services/github.js | 132 ++++++ backend/api-server/services/notion.js | 125 ++++++ docs/index.html | 59 ++- scripts/cache/sync-notion-cache.js | 424 ++++++++++++++++++ 28 files changed, 1723 insertions(+), 20 deletions(-) create mode 100644 .github/notion-cache/broadcasts/active.json create mode 100644 .github/notion-cache/databases/.gitkeep create mode 100644 .github/notion-cache/dev-profiles/DEV-009.json create mode 100644 .github/notion-cache/dev-profiles/DEV-011.json create mode 100644 .github/notion-cache/dev-profiles/index.json create mode 100644 .github/workflows/deploy-backend.yml create mode 100644 .github/workflows/sync-notion-cache.yml create mode 100644 backend/api-server/config/databases.js create mode 100644 backend/api-server/ecosystem.config.js create mode 100644 backend/api-server/package.json create mode 100644 backend/api-server/routes/chat.js create mode 100644 backend/api-server/routes/databases.js create mode 100644 backend/api-server/routes/dev.js create mode 100644 backend/api-server/routes/health.js create mode 100644 backend/api-server/routes/receipt.js create mode 100644 backend/api-server/server.js create mode 100644 backend/api-server/services/cache.js create mode 100644 backend/api-server/services/github.js create mode 100644 backend/api-server/services/notion.js create mode 100644 scripts/cache/sync-notion-cache.js diff --git a/.github/notion-cache/broadcasts/active.json b/.github/notion-cache/broadcasts/active.json new file mode 100644 index 00000000..95e9c25b --- /dev/null +++ b/.github/notion-cache/broadcasts/active.json @@ -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" +} \ No newline at end of file diff --git a/.github/notion-cache/databases/.gitkeep b/.github/notion-cache/databases/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/notion-cache/dev-profiles/DEV-001.json b/.github/notion-cache/dev-profiles/DEV-001.json index bb3f175b..8ea5ce9d 100644 --- a/.github/notion-cache/dev-profiles/DEV-001.json +++ b/.github/notion-cache/dev-profiles/DEV-001.json @@ -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" } \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-002.json b/.github/notion-cache/dev-profiles/DEV-002.json index 6168a5e1..81abe1c8 100644 --- a/.github/notion-cache/dev-profiles/DEV-002.json +++ b/.github/notion-cache/dev-profiles/DEV-002.json @@ -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" } \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-003.json b/.github/notion-cache/dev-profiles/DEV-003.json index 91a85447..9ac2adf0 100644 --- a/.github/notion-cache/dev-profiles/DEV-003.json +++ b/.github/notion-cache/dev-profiles/DEV-003.json @@ -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" } \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-004.json b/.github/notion-cache/dev-profiles/DEV-004.json index 85cb62d9..b84aa8ff 100644 --- a/.github/notion-cache/dev-profiles/DEV-004.json +++ b/.github/notion-cache/dev-profiles/DEV-004.json @@ -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" } \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-005.json b/.github/notion-cache/dev-profiles/DEV-005.json index 3135ccdd..1f97e640 100644 --- a/.github/notion-cache/dev-profiles/DEV-005.json +++ b/.github/notion-cache/dev-profiles/DEV-005.json @@ -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" } \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-009.json b/.github/notion-cache/dev-profiles/DEV-009.json new file mode 100644 index 00000000..14b8d9cb --- /dev/null +++ b/.github/notion-cache/dev-profiles/DEV-009.json @@ -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" +} \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-010.json b/.github/notion-cache/dev-profiles/DEV-010.json index 5244b7f7..695cba24 100644 --- a/.github/notion-cache/dev-profiles/DEV-010.json +++ b/.github/notion-cache/dev-profiles/DEV-010.json @@ -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" } \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-011.json b/.github/notion-cache/dev-profiles/DEV-011.json new file mode 100644 index 00000000..9e33fa6f --- /dev/null +++ b/.github/notion-cache/dev-profiles/DEV-011.json @@ -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" +} \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/DEV-012.json b/.github/notion-cache/dev-profiles/DEV-012.json index 0319c566..360720b8 100644 --- a/.github/notion-cache/dev-profiles/DEV-012.json +++ b/.github/notion-cache/dev-profiles/DEV-012.json @@ -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" } \ No newline at end of file diff --git a/.github/notion-cache/dev-profiles/index.json b/.github/notion-cache/dev-profiles/index.json new file mode 100644 index 00000000..16ea168a --- /dev/null +++ b/.github/notion-cache/dev-profiles/index.json @@ -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" + } +] \ No newline at end of file diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml new file mode 100644 index 00000000..80341b06 --- /dev/null +++ b/.github/workflows/deploy-backend.yml @@ -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 "📋 自报告已生成" diff --git a/.github/workflows/sync-notion-cache.yml b/.github/workflows/sync-notion-cache.yml new file mode 100644 index 00000000..ed5c2fcb --- /dev/null +++ b/.github/workflows/sync-notion-cache.yml @@ -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" diff --git a/backend/api-server/config/databases.js b/backend/api-server/config/databases.js new file mode 100644 index 00000000..765503fc --- /dev/null +++ b/backend/api-server/config/databases.js @@ -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 || '', +}; diff --git a/backend/api-server/ecosystem.config.js b/backend/api-server/ecosystem.config.js new file mode 100644 index 00000000..f018dc41 --- /dev/null +++ b/backend/api-server/ecosystem.config.js @@ -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' + }] +}; diff --git a/backend/api-server/package.json b/backend/api-server/package.json new file mode 100644 index 00000000..60011b66 --- /dev/null +++ b/backend/api-server/package.json @@ -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" + } +} diff --git a/backend/api-server/routes/chat.js b/backend/api-server/routes/chat.js new file mode 100644 index 00000000..88ea70c0 --- /dev/null +++ b/backend/api-server/routes/chat.js @@ -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; diff --git a/backend/api-server/routes/databases.js b/backend/api-server/routes/databases.js new file mode 100644 index 00000000..58803c80 --- /dev/null +++ b/backend/api-server/routes/databases.js @@ -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; diff --git a/backend/api-server/routes/dev.js b/backend/api-server/routes/dev.js new file mode 100644 index 00000000..291c1262 --- /dev/null +++ b/backend/api-server/routes/dev.js @@ -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; diff --git a/backend/api-server/routes/health.js b/backend/api-server/routes/health.js new file mode 100644 index 00000000..bc214c53 --- /dev/null +++ b/backend/api-server/routes/health.js @@ -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; diff --git a/backend/api-server/routes/receipt.js b/backend/api-server/routes/receipt.js new file mode 100644 index 00000000..59da7eb6 --- /dev/null +++ b/backend/api-server/routes/receipt.js @@ -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; diff --git a/backend/api-server/server.js b/backend/api-server/server.js new file mode 100644 index 00000000..5e454e22 --- /dev/null +++ b/backend/api-server/server.js @@ -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 + '(仅本机访问)'); +}); diff --git a/backend/api-server/services/cache.js b/backend/api-server/services/cache.js new file mode 100644 index 00000000..ca33699f --- /dev/null +++ b/backend/api-server/services/cache.js @@ -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 +}; diff --git a/backend/api-server/services/github.js b/backend/api-server/services/github.js new file mode 100644 index 00000000..394ce980 --- /dev/null +++ b/backend/api-server/services/github.js @@ -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 +}; diff --git a/backend/api-server/services/notion.js b/backend/api-server/services/notion.js new file mode 100644 index 00000000..28a7a0b4 --- /dev/null +++ b/backend/api-server/services/notion.js @@ -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 +}; diff --git a/docs/index.html b/docs/index.html index 579e2d36..3b042598 100644 --- a/docs/index.html +++ b/docs/index.html @@ -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('[通道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('[Notion] profile fetch failed:', e.message); } + } 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(滑动窗口自动管理)'; } // 尝试从后端获取提示词,失败时降级到本地组装 diff --git a/scripts/cache/sync-notion-cache.js b/scripts/cache/sync-notion-cache.js new file mode 100644 index 00000000..3af4ee05 --- /dev/null +++ b/scripts/cache/sync-notion-cache.js @@ -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); +});