From 96b6b219cd1cd3e909d6bc46e35b7d13190e4a83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:58:05 +0000 Subject: [PATCH 01/11] Initial plan 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 02/11] 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); +}); From 726b05bc0b1f77fcccca33ce5bfb2f9a8797cd7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:15:48 +0000 Subject: [PATCH 03/11] fix: address code review feedback - timer leaks, CORS, validation, cache TTL - Fix timer leak in fetchNotionProfile/loadDatabaseCache with finally block - Remove HTTP origin from CORS whitelist (HTTPS-only) - Validate devId against DEV_REGISTRY instead of regex - Fix cache TTL check to use >= instead of > - Fix chat.js count queries to use page_size 100 instead of 1 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/a260ce18-279a-4490-af90-a90a7b1f46cd --- backend/api-server/routes/chat.js | 6 +++--- backend/api-server/routes/dev.js | 9 +++------ backend/api-server/server.js | 1 - backend/api-server/services/cache.js | 2 +- docs/index.html | 14 ++++++++------ 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/backend/api-server/routes/chat.js b/backend/api-server/routes/chat.js index 88ea70c0..258b1692 100644 --- a/backend/api-server/routes/chat.js +++ b/backend/api-server/routes/chat.js @@ -55,7 +55,7 @@ router.post('/chat/context', async function(req, res) { // 获取 Agent 注册表计数 try { if (dbConfig.agentRegistry) { - var agents = await notionService.queryDB(dbConfig.agentRegistry, null, null, 1); + var agents = await notionService.queryDB(dbConfig.agentRegistry, null, null, 100); context.agents_count = agents.results ? agents.results.length : 0; } } catch (_) {} @@ -66,7 +66,7 @@ router.post('/chat/context', async function(req, res) { var syslogs = await notionService.queryDB(dbConfig.syslogInbox, { property: 'DEV编号', rich_text: { equals: devId } - }, null, 1); + }, null, 100); context.syslogs_count = syslogs.results ? syslogs.results.length : 0; } } catch (_) {} @@ -75,7 +75,7 @@ router.post('/chat/context', async function(req, res) { try { if (dbConfig.ticketBook) { var tickets = await notionService.queryDB(dbConfig.ticketBook, - { property: '状态', select: { does_not_equal: '已完成' } }, null, 1); + { property: '状态', select: { does_not_equal: '已完成' } }, null, 100); context.tickets_count = tickets.results ? tickets.results.length : 0; } } catch (_) {} diff --git a/backend/api-server/routes/dev.js b/backend/api-server/routes/dev.js index 291c1262..0c47b5ac 100644 --- a/backend/api-server/routes/dev.js +++ b/backend/api-server/routes/dev.js @@ -28,15 +28,12 @@ var DEV_REGISTRY = { 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: '无效的开发者编号格式' }); + // 验证 devId + if (!DEV_REGISTRY[devId]) { + return res.status(404).json({ error: true, code: 'DEV_NOT_FOUND', 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, diff --git a/backend/api-server/server.js b/backend/api-server/server.js index 5e454e22..e4426c22 100644 --- a/backend/api-server/server.js +++ b/backend/api-server/server.js @@ -18,7 +18,6 @@ const app = express(); app.use(cors({ origin: [ 'https://guanghulab.com', - 'http://guanghulab.com', 'https://www.guanghulab.com', 'https://qinfendebingshuo.github.io' ], diff --git a/backend/api-server/services/cache.js b/backend/api-server/services/cache.js index ca33699f..302fe868 100644 --- a/backend/api-server/services/cache.js +++ b/backend/api-server/services/cache.js @@ -14,7 +14,7 @@ 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)) { + if (Date.now() - item.time >= (item.ttl || DEFAULT_TTL)) { store.delete(key); return null; } diff --git a/docs/index.html b/docs/index.html index 3b042598..98a9aa98 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1771,9 +1771,10 @@ async function fetchNotionProfile(devId) { 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; } + try { + const res = await fetch(API_BASE + '/dev/' + encodeURIComponent(devId), { signal: ctrl.signal }); + if (res.ok) { const d = await res.json(); d._source = 'realtime'; return d; } + } finally { clearTimeout(tid); } } catch (e) { console.debug('[通道B] profile fetch failed, 降级到通道A:', e.message); } } // 通道A(GitHub 缓存) @@ -1791,9 +1792,10 @@ async function loadDatabaseCache(name) { 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(); + try { + const res = await fetch(API_BASE + '/databases/' + encodeURIComponent(name), { signal: ctrl.signal }); + if (res.ok) return await res.json(); + } finally { clearTimeout(tid); } } catch (e) { console.debug('[通道B] database ' + name + ' fetch failed:', e.message); } } // 通道A(GitHub 缓存) From 38d8afef55365ba0eaddea57368c384f97a2b6d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:22:38 +0000 Subject: [PATCH 04/11] feat: implement language-driven OS capabilities (ZY-LANGDRIVE) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 - Write Capability Layer: - Add backend/api-server/routes/write.js with 10 write endpoints - Extend services/github.js with triggerWorkflow + createFile - Add middleware/audit.js for operation audit logging - Rate limiting: 10 writes/min per developer Phase 4 - Intent Router Engine: - Add config/function-tools.json with 10 operation definitions - Add middleware/intent-router.js for NL → API routing - Add docs/js/intent-tools.js for frontend function calling Phase 5 - Permission Sandbox System: - Add config/permissions.js with 4-level model (L0-L3) - Add middleware/auth.js (identity + permission check) - Add middleware/sandbox.js (preview environment guard) Phase 6 - Cognitive Guidance System: - Add config/onboarding-script.json (5-round guided dialogue) - Add routes/onboarding.js (onboarding API + tools endpoint) - Add docs/js/onboarding.js + docs/css/onboarding.css (UI) Update server.js to v2.0.0 with all new routes and middleware Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/a260ce18-279a-4490-af90-a90a7b1f46cd --- .gitignore | 1 + backend/api-server/config/function-tools.json | 97 ++++ .../api-server/config/onboarding-script.json | 39 ++ backend/api-server/config/permissions.js | 101 ++++ backend/api-server/middleware/audit.js | 63 +++ backend/api-server/middleware/auth.js | 88 ++++ .../api-server/middleware/intent-router.js | 110 ++++ backend/api-server/middleware/sandbox.js | 46 ++ backend/api-server/routes/onboarding.js | 136 +++++ backend/api-server/routes/write.js | 476 ++++++++++++++++++ backend/api-server/server.js | 17 +- backend/api-server/services/github.js | 91 +++- docs/css/onboarding.css | 159 ++++++ docs/js/intent-tools.js | 121 +++++ docs/js/onboarding.js | 183 +++++++ 15 files changed, 1723 insertions(+), 5 deletions(-) create mode 100644 backend/api-server/config/function-tools.json create mode 100644 backend/api-server/config/onboarding-script.json create mode 100644 backend/api-server/config/permissions.js create mode 100644 backend/api-server/middleware/audit.js create mode 100644 backend/api-server/middleware/auth.js create mode 100644 backend/api-server/middleware/intent-router.js create mode 100644 backend/api-server/middleware/sandbox.js create mode 100644 backend/api-server/routes/onboarding.js create mode 100644 backend/api-server/routes/write.js create mode 100644 docs/css/onboarding.css create mode 100644 docs/js/intent-tools.js create mode 100644 docs/js/onboarding.js diff --git a/.gitignore b/.gitignore index 9d438cfe..46a65b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ *.bak *.log +backend/api-server/logs/ diff --git a/backend/api-server/config/function-tools.json b/backend/api-server/config/function-tools.json new file mode 100644 index 00000000..f9f8963b --- /dev/null +++ b/backend/api-server/config/function-tools.json @@ -0,0 +1,97 @@ +{ + "tools": [ + { + "name": "query_tickets", + "description": "查询开发者的工单列表。开发者说'我的工单'、'还有几个工单没关'、'查一下Bug'时触发。", + "parameters": { + "status": { "type": "string", "enum": ["全部", "待处理", "进行中", "已完成"], "default": "全部" } + }, + "api": "GET /api/tickets?devId={devId}&status={status}", + "permission": "ticket:read" + }, + { + "name": "create_ticket", + "description": "创建新工单。开发者说'建个工单'、'提一个Bug'、'报告问题'时触发。", + "parameters": { + "title": { "type": "string", "required": true }, + "description": { "type": "string" }, + "priority": { "type": "string", "enum": ["P0", "P1", "P2", "P3"], "default": "P2" }, + "module": { "type": "string" } + }, + "api": "POST /api/tickets/create", + "permission": "ticket:create" + }, + { + "name": "submit_syslog", + "description": "提交系统日志。开发者说'提交日志'、'写SYSLOG'、'记录一下'时触发。", + "parameters": { + "content": { "type": "string", "required": true }, + "type": { "type": "string", "enum": ["日常", "Bug修复", "功能完成", "部署", "紧急"], "default": "日常" } + }, + "api": "POST /api/syslog/submit", + "permission": "syslog:submit" + }, + { + "name": "query_my_status", + "description": "查询开发者自己的状态。开发者说'我的进度'、'我现在什么状态'、'我还有什么没做'时触发。", + "parameters": {}, + "api": "GET /api/dev/{devId}", + "permission": "dev:read_self" + }, + { + "name": "deploy_to_preview", + "description": "部署模块到预览站。开发者说'部署一下'、'上预览'、'推到测试站'时触发。", + "parameters": { + "module": { "type": "string", "required": true } + }, + "api": "POST /api/deploy/preview", + "permission": "deploy:preview" + }, + { + "name": "deploy_to_production", + "description": "部署模块到正式站。开发者说'上线'、'部署到正式站'、'发布到guanghulab.com'时触发。需要二次确认。", + "parameters": { + "module": { "type": "string", "required": true }, + "confirmToken": { "type": "string" } + }, + "api": "POST /api/deploy/production", + "permission": "deploy:production", + "dangerous": true + }, + { + "name": "query_agents", + "description": "查询Agent注册表。开发者说'有哪些Agent'、'查一下Agent'、'系统里有谁'时触发。", + "parameters": {}, + "api": "GET /api/agents", + "permission": "agent:read" + }, + { + "name": "query_recent_syslogs", + "description": "查最近的系统日志。开发者说'最近有什么日志'、'系统最近怎么样'时触发。", + "parameters": { + "limit": { "type": "number", "default": 10 } + }, + "api": "GET /api/syslogs?limit={limit}", + "permission": "syslog:read" + }, + { + "name": "create_broadcast", + "description": "创建广播。开发者说'发个广播'、'通知一下'时触发。仅限有广播权限的开发者。", + "parameters": { + "title": { "type": "string", "required": true }, + "content": { "type": "string", "required": true }, + "target": { "type": "string", "description": "目标开发者编号,如 DEV-001,或 ALL" } + }, + "api": "POST /api/broadcasts/create", + "permission": "broadcast:create", + "dangerous": true + }, + { + "name": "query_repo_status", + "description": "查询仓库状态。开发者说'仓库怎么样'、'最近有什么PR'、'CI状态'时触发。", + "parameters": {}, + "api": "GET /api/repo/status", + "permission": "repo:read" + } + ] +} diff --git a/backend/api-server/config/onboarding-script.json b/backend/api-server/config/onboarding-script.json new file mode 100644 index 00000000..b0fee7af --- /dev/null +++ b/backend/api-server/config/onboarding-script.json @@ -0,0 +1,39 @@ +{ + "onboarding": [ + { + "round": 1, + "prompt": "👋 首先,试着问我你的状态。比如说:「我现在什么情况?」", + "expected_intent": "query_my_status", + "success_message": "✅ 很好!你看到了吗?我直接从 Notion 数据库里查了你的真实信息回来。这就是语言驱动——你说话,系统就动了。", + "hint": "💡 你可以说「我的状态」、「我的进度」、「我现在怎么样了」,怎么说都行,我能听懂。" + }, + { + "round": 2, + "prompt": "📝 接下来,试着建一个工单。比如:「帮我建一个工单,标题是测试工单」", + "expected_intent": "create_ticket", + "success_message": "✅ 工单已经建好了!注意,这是在预览环境里建的,不会影响正式系统。当你升级到正式环境后,建的工单就是真的了。", + "hint": "💡 你可以说「建个工单」、「提个Bug」、「报告一个问题」,都会触发建工单。" + }, + { + "round": 3, + "prompt": "📊 再试试查一下系统日志。比如:「最近有什么日志?」", + "expected_intent": "query_recent_syslogs", + "success_message": "✅ 这些是从 Notion SYSLOG数据库里实时查的。每个开发者提交的日志你都能看到。", + "hint": "💡 你可以说「最近的日志」、「系统日志」、「看看SYSLOG」。" + }, + { + "round": 4, + "prompt": "🚀 现在试试部署你的模块到预览站。比如:「部署 status-board 到预览站」", + "expected_intent": "deploy_to_preview", + "success_message": "✅ 部署已触发!在预览站你可以随便试。等你熟悉了,冰朔会给你开正式站的部署权限。", + "hint": "💡 你只能部署自己负责的模块。如果说了别人的模块,系统会友好提示你。" + }, + { + "round": 5, + "prompt": "🎉 最后一轮。试试随便和我聊点什么。什么都行——问我问题、讲笑话、或者告诉我你想做什么。", + "expected_intent": null, + "success_message": "✅ 引导完成!\n\n你现在已经知道怎么和系统交互了。记住:\n\n· **直接说人话**,不需要背命令\n· **想做什么就说什么**,我会帮你翻译成操作\n· **在预览站多练习**,正式站操作都是真的\n· **说错了也没关系**,我会帮你确认\n\n你的权限已经从「观察者」升级到「学习者」了 📖\n现在可以在预览站自由操作了。\n\n有什么问题随时找我。", + "hint": null + } + ] +} diff --git a/backend/api-server/config/permissions.js b/backend/api-server/config/permissions.js new file mode 100644 index 00000000..58c5d6fa --- /dev/null +++ b/backend/api-server/config/permissions.js @@ -0,0 +1,101 @@ +/** + * 权限等级定义 · 开发者权限沙箱系统 + * + * 4 级权限模型:观察者 → 学习者 → 执行者 → 管理者 + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +// 权限等级定义 +var PERMISSION_LEVELS = { + 0: { + name: '观察者', + label: '👀 观察者', + permissions: [ + 'dev:read_self', + 'ticket:read', + 'syslog:read', + 'agent:read', + 'repo:read' + ], + description: '只读权限,熟悉系统' + }, + 1: { + name: '学习者', + label: '📖 学习者', + permissions: [ + 'dev:read_self', + 'ticket:read', 'ticket:create', + 'syslog:read', 'syslog:submit', + 'agent:read', + 'repo:read', + 'deploy:preview' + ], + description: '预览站可写,正式站只读' + }, + 2: { + name: '执行者', + label: '⚡ 执行者', + permissions: [ + 'dev:read_self', 'dev:update_self', + 'ticket:read', 'ticket:create', 'ticket:update', + 'syslog:read', 'syslog:submit', + 'agent:read', + 'repo:read', + 'deploy:preview', 'deploy:production', + 'maintenance:log' + ], + description: '可操作自己的模块,含正式站部署' + }, + 3: { + name: '管理者', + label: '👑 管理者', + permissions: [ + 'dev:read_self', 'dev:read_all', 'dev:update_self', 'dev:update_all', + 'ticket:read', 'ticket:create', 'ticket:update', 'ticket:delete', + 'syslog:read', 'syslog:submit', + 'agent:read', 'agent:update', + 'repo:read', + 'deploy:preview', 'deploy:production', + 'broadcast:create', + 'maintenance:log', + 'permission:manage' + ], + description: '系统管理员,全部权限' + } +}; + +// 开发者默认权限配置 +var DEV_PERMISSIONS = { + 'TCS-0002': { level: 3, environment: 'production' }, + 'DEV-001': { level: 0, environment: 'preview' }, + 'DEV-002': { level: 0, environment: 'preview' }, + 'DEV-003': { level: 0, environment: 'preview' }, + 'DEV-004': { level: 0, environment: 'preview' }, + 'DEV-005': { level: 0, environment: 'preview' }, + 'DEV-009': { level: 0, environment: 'preview' }, + 'DEV-010': { level: 0, environment: 'preview' }, + 'DEV-011': { level: 0, environment: 'preview' }, + 'DEV-012': { level: 0, environment: 'preview' } +}; + +// 开发者 → 模块映射 +var DEV_MODULES = { + 'DEV-001': ['backend/', 'src/'], + 'DEV-002': ['frontend/', 'persona-selector/', 'chat-bubble/'], + 'DEV-003': ['settings/', 'cloud-drive/'], + 'DEV-004': ['dingtalk-bot/'], + 'DEV-005': ['status-board/'], + 'DEV-009': ['user-center/'], + 'DEV-010': ['ticket-system/', 'data-stats/', 'dynamic-comic/'], + 'DEV-011': ['writing-workspace/'], + 'DEV-012': ['notification-center/'], + 'TCS-0002': ['*'] +}; + +module.exports = { + PERMISSION_LEVELS: PERMISSION_LEVELS, + DEV_PERMISSIONS: DEV_PERMISSIONS, + DEV_MODULES: DEV_MODULES +}; diff --git a/backend/api-server/middleware/audit.js b/backend/api-server/middleware/audit.js new file mode 100644 index 00000000..5c92e9ac --- /dev/null +++ b/backend/api-server/middleware/audit.js @@ -0,0 +1,63 @@ +/** + * 操作审计日志中间件 + * + * 记录每次 API 操作到日志文件(谁、什么时间、做了什么) + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +// 优先使用配置的目录,本地开发时回退到项目内目录 +var AUDIT_LOG_DIR = process.env.AUDIT_LOG_DIR || + path.join(__dirname, '../../logs/audit'); + +/** + * 审计日志中间件 + * 拦截请求和响应,记录操作信息 + */ +function auditLog(req, res, next) { + var startTime = Date.now(); + + var logEntry = { + timestamp: new Date().toISOString(), + devId: (req.user && req.user.devId) || req.headers['x-dev-id'] || 'anonymous', + method: req.method, + path: req.path, + ip: req.ip + }; + + // 对 POST/PATCH 请求记录请求体(排除敏感字段) + if ((req.method === 'POST' || req.method === 'PATCH') && req.body) { + var sanitizedBody = Object.assign({}, req.body); + delete sanitizedBody.password; + delete sanitizedBody.token; + delete sanitizedBody.confirmToken; + logEntry.body = sanitizedBody; + } + + // 拦截响应完成 + var originalSend = res.send; + res.send = function(body) { + logEntry.responseTime = Date.now() - startTime; + logEntry.statusCode = res.statusCode; + + // 异步写入日志,不阻塞响应 + try { + fs.mkdirSync(AUDIT_LOG_DIR, { recursive: true }); + var today = new Date().toISOString().split('T')[0]; + var logFile = path.join(AUDIT_LOG_DIR, 'audit-' + today + '.jsonl'); + fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n'); + } catch (e) { + console.error('审计日志写入失败:', e.message); + } + + return originalSend.call(this, body); + }; + + next(); +} + +module.exports = { auditLog: auditLog }; diff --git a/backend/api-server/middleware/auth.js b/backend/api-server/middleware/auth.js new file mode 100644 index 00000000..64328a6e --- /dev/null +++ b/backend/api-server/middleware/auth.js @@ -0,0 +1,88 @@ +/** + * 身份验证 + 权限检查中间件 + * + * 人格体缓冲层铁律:人类不直接操作系统,人格体是唯一中间翻译层。 + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var permissions = require('../config/permissions'); +var PERMISSION_LEVELS = permissions.PERMISSION_LEVELS; +var DEV_PERMISSIONS = permissions.DEV_PERMISSIONS; +var DEV_MODULES = permissions.DEV_MODULES; + +/** + * 开发者身份验证中间件 + * 要求请求头携带 x-dev-id 和 authorization + */ +function requireAuth(req, res, next) { + var devId = req.headers['x-dev-id']; + var token = req.headers['authorization']; + + if (!devId || !token) { + return res.status(401).json({ + error: true, + code: 'AUTH_REQUIRED', + message: '请先登录。需要携带 x-dev-id 和 authorization 请求头。', + reply: '🔒 请先登录。你需要使用你的开发者编号登录系统。' + }); + } + + var devConfig = DEV_PERMISSIONS[devId]; + if (!devConfig) { + return res.status(401).json({ + error: true, + code: 'UNKNOWN_DEV', + message: '未知的开发者编号: ' + devId, + reply: '🔒 未知的开发者编号:' + devId + '。请联系冰朔注册。' + }); + } + + var level = PERMISSION_LEVELS[devConfig.level]; + + req.user = { + devId: devId, + permissionLevel: devConfig.level, + permissionLabel: level.label, + permissions: level.permissions, + environment: devConfig.environment, + modules: DEV_MODULES[devId] || [] + }; + + next(); +} + +/** + * 权限检查中间件工厂 + * @param {string} requiredPermission - 需要的权限标识 + */ +function checkPermission(requiredPermission) { + return function(req, res, next) { + if (!req.user) { + return res.status(401).json({ + error: true, + code: 'AUTH_REQUIRED', + message: '身份验证缺失' + }); + } + + if (!req.user.permissions.includes(requiredPermission)) { + return res.status(403).json({ + error: true, + code: 'PERMISSION_DENIED', + message: '权限不足,需要: ' + requiredPermission, + reply: '🔒 权限不足。你的等级是 ' + req.user.permissionLabel + + ',需要「' + requiredPermission + '」权限。\n\n' + + '💡 在预览站多练习,熟悉系统后冰朔会给你升级权限。' + }); + } + + next(); + }; +} + +module.exports = { + requireAuth: requireAuth, + checkPermission: checkPermission +}; diff --git a/backend/api-server/middleware/intent-router.js b/backend/api-server/middleware/intent-router.js new file mode 100644 index 00000000..1f3d3da1 --- /dev/null +++ b/backend/api-server/middleware/intent-router.js @@ -0,0 +1,110 @@ +/** + * 意图路由中间件 · 自然语言 → 操作映射 + * + * 核心逻辑: + * 1. 接收模型输出的 function_call + * 2. 验证权限 + * 3. 调用对应的后端 API + * 4. 返回结果 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var tools = require('../config/function-tools.json'); + +/** + * 意图路由核心 + * + * @param {Object} functionCall - { name, parameters } + * @param {Object} user - 当前用户信息(含 permissions, environment, modules) + * @param {Function} apiClient - API 调用函数 (method, path, body) => result + * @returns {Object} 执行结果 + */ +async function routeIntent(functionCall, user, apiClient) { + var tool = null; + for (var i = 0; i < tools.tools.length; i++) { + if (tools.tools[i].name === functionCall.name) { + tool = tools.tools[i]; + break; + } + } + + if (!tool) { + return { + success: false, + reply: '❓ 我不认识这个操作:' + functionCall.name + '。你可以问我能做什么。' + }; + } + + // 权限检查 + if (!user.permissions.includes(tool.permission)) { + return { + success: false, + reply: '🔒 你没有执行「' + tool.description + '」的权限。当前权限等级:' + user.permissionLabel + }; + } + + // 危险操作二次确认 + if (tool.dangerous && !functionCall.parameters.confirmToken) { + return { + success: false, + requireConfirmation: true, + reply: '⚠️ 这是一个**危险操作**。请说「确认执行」来确认。' + }; + } + + // 预览环境检查 + if (user.environment === 'preview' && tool.permission.includes('production')) { + return { + success: false, + reply: '🔒 你目前在**预览环境**,不能执行正式站操作。在预览站充分练习后,由冰朔开放正式站权限。' + }; + } + + // 解析 API 路径 + var apiDef = tool.api.split(' '); + var method = apiDef[0]; + var path = apiDef[1]; + + // 替换路径中的变量 + path = path.replace('{devId}', user.devId); + var params = functionCall.parameters || {}; + for (var key in params) { + path = path.replace('{' + key + '}', encodeURIComponent(params[key])); + } + + // 调用 API + try { + var result = await apiClient(method, path, Object.assign({}, params, { devId: user.devId })); + return result; + } catch (e) { + return { + success: false, + reply: '❌ 操作失败:' + e.message + }; + } +} + +/** + * 获取用户可用的工具列表(已根据权限过滤) + */ +function getAvailableTools(user) { + return tools.tools.filter(function(tool) { + return user.permissions.includes(tool.permission); + }); +} + +/** + * 获取全部工具定义 + */ +function getAllTools() { + return tools.tools; +} + +module.exports = { + routeIntent: routeIntent, + getAvailableTools: getAvailableTools, + getAllTools: getAllTools +}; diff --git a/backend/api-server/middleware/sandbox.js b/backend/api-server/middleware/sandbox.js new file mode 100644 index 00000000..0f856a75 --- /dev/null +++ b/backend/api-server/middleware/sandbox.js @@ -0,0 +1,46 @@ +/** + * 预览站沙箱守卫中间件 + * + * 预览环境中的写入操作在沙箱中执行,不影响真实系统。 + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +// 沙箱数据库 ID(通过环境变量配置,Notion 中的独立数据库) +var SANDBOX_DB_IDS = { + ticketBook: process.env.SANDBOX_TICKET_DB_ID || '', + syslogInbox: process.env.SANDBOX_SYSLOG_DB_ID || '', + maintenanceLog: process.env.SANDBOX_MAINTENANCE_DB_ID || '' +}; + +/** + * 沙箱守卫中间件 + * + * 如果开发者在预览环境中执行写入操作: + * - Notion 写入 → 重定向到沙箱数据库 + * - GitHub 操作 → 重定向到 preview 分支 + * - 所有操作标记 [SANDBOX] + */ +function sandboxGuard(req, res, next) { + if (req.user && req.user.environment === 'preview') { + req.sandbox = true; + req.sandboxDbIds = SANDBOX_DB_IDS; + + // 预览环境中禁止正式站部署 + if (req.path.includes('/deploy/production')) { + return res.status(403).json({ + error: true, + code: 'SANDBOX_BLOCKED', + message: '预览环境不允许正式站部署', + reply: '🏖️ 你目前在预览环境。正式站部署需要升级到 Level 2(执行者)权限。\n\n继续在预览站练习吧,你做得很好!' + }); + } + } + next(); +} + +module.exports = { + sandboxGuard: sandboxGuard, + SANDBOX_DB_IDS: SANDBOX_DB_IDS +}; diff --git a/backend/api-server/routes/onboarding.js b/backend/api-server/routes/onboarding.js new file mode 100644 index 00000000..15c9fdbe --- /dev/null +++ b/backend/api-server/routes/onboarding.js @@ -0,0 +1,136 @@ +/** + * 认知引导流程 API 路由 + * + * GET /api/onboarding/status — 获取引导状态 + * POST /api/onboarding/start — 开始引导 + * POST /api/onboarding/complete — 完成引导(自动升级权限) + * GET /api/tools — 获取可用工具列表(根据权限过滤) + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var express = require('express'); +var router = express.Router(); +var authMiddleware = require('../middleware/auth'); +var intentRouter = require('../middleware/intent-router'); +var onboardingScript = require('../config/onboarding-script.json'); +var permissions = require('../config/permissions'); + +// 引导状态存储(内存,生产环境应持久化) +var onboardingState = new Map(); + +// ====== 获取引导状态 ====== +router.get('/onboarding/status', + authMiddleware.requireAuth, + function(req, res) { + var devId = req.user.devId; + var state = onboardingState.get(devId); + + if (!state) { + // 未开始引导 + res.json({ + started: false, + completed: false, + currentRound: 0, + totalRounds: onboardingScript.onboarding.length, + permissionLevel: req.user.permissionLevel + }); + } else { + res.json({ + started: true, + completed: state.completed, + currentRound: state.currentRound, + totalRounds: onboardingScript.onboarding.length, + permissionLevel: req.user.permissionLevel, + script: state.completed ? null : onboardingScript.onboarding[state.currentRound - 1] + }); + } + } +); + +// ====== 开始/推进引导 ====== +router.post('/onboarding/start', + authMiddleware.requireAuth, + function(req, res) { + var devId = req.user.devId; + var state = onboardingState.get(devId); + + if (!state) { + // 开始新引导 + state = { currentRound: 1, completed: false, startedAt: new Date().toISOString() }; + onboardingState.set(devId, state); + } else if (!state.completed) { + // 推进到下一轮 + if (state.currentRound < onboardingScript.onboarding.length) { + state.currentRound++; + } else { + state.completed = true; + state.completedAt = new Date().toISOString(); + } + } + + var script = state.completed ? null : onboardingScript.onboarding[state.currentRound - 1]; + + res.json({ + currentRound: state.currentRound, + totalRounds: onboardingScript.onboarding.length, + completed: state.completed, + script: script + }); + } +); + +// ====== 完成引导(自动升级权限)====== +router.post('/onboarding/complete', + authMiddleware.requireAuth, + function(req, res) { + var devId = req.user.devId; + var state = onboardingState.get(devId); + + if (!state) { + return res.status(400).json({ + error: true, + code: 'NOT_STARTED', + message: '引导尚未开始' + }); + } + + // 标记完成 + state.completed = true; + state.completedAt = new Date().toISOString(); + + // 自动升级权限:Level 0 → Level 1 + var devConfig = permissions.DEV_PERMISSIONS[devId]; + if (devConfig && devConfig.level === 0) { + devConfig.level = 1; + } + + res.json({ + success: true, + previousLevel: 0, + newLevel: devConfig ? devConfig.level : 0, + reply: '🎉 恭喜!你已完成认知引导。\n\n你的权限已从「👀 观察者」升级为「📖 学习者」。\n现在可以在预览站创建工单、提交日志、触发部署了。\n\n有什么问题随时找我。' + }); + } +); + +// ====== 获取可用工具列表 ====== +router.get('/tools', + authMiddleware.requireAuth, + function(req, res) { + var available = intentRouter.getAvailableTools(req.user); + + res.json({ + dev_id: req.user.devId, + permission_level: req.user.permissionLevel, + permission_label: req.user.permissionLabel, + environment: req.user.environment, + tools_count: available.length, + tools: available + }); + } +); + +module.exports = router; diff --git a/backend/api-server/routes/write.js b/backend/api-server/routes/write.js new file mode 100644 index 00000000..ba1efc63 --- /dev/null +++ b/backend/api-server/routes/write.js @@ -0,0 +1,476 @@ +/** + * 写入类 API 路由 · 语言驱动操作系统写入能力层 + * + * POST /api/tickets/create — 创建工单 + * PATCH /api/tickets/:ticketId — 更新工单状态 + * POST /api/syslog/submit — 提交 SYSLOG + * POST /api/broadcasts/create — 创建广播 + * POST /api/deploy/preview — 触发预览站部署 + * POST /api/deploy/production — 触发正式站部署(需二次确认) + * PATCH /api/agents/:agentId — 更新 Agent 状态 + * POST /api/maintenance/log — 写入维护日志 + * POST /api/receipts/submit — 提交指令回执 + * PATCH /api/dev/:devId/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'); +var authMiddleware = require('../middleware/auth'); +var sandboxMiddleware = require('../middleware/sandbox'); +var auditMiddleware = require('../middleware/audit'); + +// 所有写入接口需要身份验证 + 审计 + 沙箱检查 +router.use(authMiddleware.requireAuth); +router.use(auditMiddleware.auditLog); +router.use(sandboxMiddleware.sandboxGuard); + +// 频率限制:每个开发者每分钟最多10次写入 +var rateLimitMap = new Map(); +var RATE_LIMIT = 10; +var RATE_WINDOW = 60 * 1000; + +function rateLimit(req, res, next) { + var key = req.user.devId; + var now = Date.now(); + var entry = rateLimitMap.get(key); + + if (!entry || now - entry.start >= RATE_WINDOW) { + rateLimitMap.set(key, { start: now, count: 1 }); + return next(); + } + + if (entry.count >= RATE_LIMIT) { + return res.status(429).json({ + error: true, + code: 'RATE_LIMITED', + message: '写入频率超限(每分钟最多 ' + RATE_LIMIT + ' 次)', + reply: '⏳ 操作太频繁了,请稍等一分钟再试。' + }); + } + + entry.count++; + next(); +} + +router.use(rateLimit); + +// 获取实际数据库 ID(沙箱环境使用沙箱 DB) +function getDbId(req, configKey) { + if (req.sandbox && req.sandboxDbIds && req.sandboxDbIds[configKey]) { + return req.sandboxDbIds[configKey]; + } + return dbConfig[configKey]; +} + +// ====== 创建工单 ====== +router.post('/tickets/create', + authMiddleware.checkPermission('ticket:create'), + async function(req, res) { + try { + var title = req.body.title; + var description = req.body.description || ''; + var priority = req.body.priority || 'P2'; + var module = req.body.module || ''; + var devId = req.user.devId; + + if (!title) { + return res.status(400).json({ + error: true, + code: 'MISSING_TITLE', + message: '工单标题不能为空' + }); + } + + var ticketDbId = getDbId(req, 'ticketBook'); + if (!ticketDbId) { + return res.status(503).json({ + error: true, + code: 'DB_NOT_CONFIGURED', + message: '工单数据库未配置' + }); + } + + var properties = { + '工单标题': { title: [{ text: { content: title } }] }, + '描述': { rich_text: [{ text: { content: description } }] }, + '优先级': { select: { name: priority } }, + '提交人': { rich_text: [{ text: { content: devId } }] }, + '状态': { select: { name: '待处理' } } + }; + if (module) { + properties['模块'] = { select: { name: module } }; + } + + var result = await notionService.writeToDB(ticketDbId, properties); + var prefix = req.sandbox ? '[沙箱] ' : ''; + + res.json({ + success: true, + message: prefix + '工单已创建', + ticketUrl: result.url || '', + reply: '✅ ' + prefix + '工单「' + title + '」已创建,优先级 ' + priority + ',状态:待处理。' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'CREATE_FAILED', message: e.message }); + } + } +); + +// ====== 更新工单状态 ====== +router.patch('/tickets/:ticketId', + authMiddleware.checkPermission('ticket:update'), + async function(req, res) { + try { + var ticketId = req.params.ticketId; + var status = req.body.status; + + if (!status) { + return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' }); + } + + // 使用 Notion page update API + if (!notionService.notion) { + return res.status(503).json({ error: true, code: 'NOTION_UNAVAILABLE', message: 'Notion 未连接' }); + } + + await notionService.notion.pages.update({ + page_id: ticketId, + properties: { + '状态': { select: { name: status } } + } + }); + + res.json({ + success: true, + reply: '✅ 工单状态已更新为:' + status + }); + } catch (e) { + res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message }); + } + } +); + +// ====== 提交 SYSLOG ====== +router.post('/syslog/submit', + authMiddleware.checkPermission('syslog:submit'), + async function(req, res) { + try { + var content = req.body.content; + var type = req.body.type || '日常'; + var devId = req.user.devId; + + if (!content) { + return res.status(400).json({ error: true, code: 'MISSING_CONTENT', message: 'SYSLOG 内容不能为空' }); + } + + var syslogDbId = getDbId(req, 'syslogInbox'); + if (!syslogDbId) { + return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: 'SYSLOG 数据库未配置' }); + } + + await notionService.writeToDB(syslogDbId, { + 'SYSLOG内容': { title: [{ text: { content: content } }] }, + '类型': { select: { name: type } }, + '开发者编号': { rich_text: [{ text: { content: devId } }] } + }); + + var prefix = req.sandbox ? '[沙箱] ' : ''; + var summary = content.length > 50 ? content.substring(0, 50) + '...' : content; + res.json({ + success: true, + reply: '✅ ' + prefix + 'SYSLOG 已提交:「' + summary + '」' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'SUBMIT_FAILED', message: e.message }); + } + } +); + +// ====== 创建广播 ====== +router.post('/broadcasts/create', + authMiddleware.checkPermission('broadcast:create'), + async function(req, res) { + try { + var title = req.body.title; + var content = req.body.content; + var target = req.body.target || 'ALL'; + var devId = req.user.devId; + + if (!title || !content) { + return res.status(400).json({ error: true, code: 'MISSING_FIELDS', message: '广播标题和内容不能为空' }); + } + + var broadcastId = 'BC-' + Date.now(); + var broadcastData = { + broadcast_id: broadcastId, + title: title, + content: content, + target: target, + created_by: devId, + created_at: new Date().toISOString(), + status: 'active' + }; + + // 写入 GitHub 广播文件 + var filePath = '.github/broadcasts/' + broadcastId + '.json'; + await githubService.createFile( + filePath, + JSON.stringify(broadcastData, null, 2), + '📢 新广播: ' + title + ' · by ' + devId + ); + + res.json({ + success: true, + broadcast_id: broadcastId, + reply: '📢 广播「' + title + '」已创建并发送,编号:' + broadcastId + }); + } catch (e) { + res.status(500).json({ error: true, code: 'CREATE_FAILED', message: e.message }); + } + } +); + +// ====== 触发预览站部署 ====== +router.post('/deploy/preview', + authMiddleware.checkPermission('deploy:preview'), + async function(req, res) { + try { + var module = req.body.module; + var devId = req.user.devId; + + if (!module) { + return res.status(400).json({ error: true, code: 'MISSING_MODULE', message: '需要指定部署模块' }); + } + + // 检查开发者是否拥有该模块 + if (!req.user.modules.includes('*') && !req.user.modules.includes(module)) { + return res.status(403).json({ + error: true, + code: 'MODULE_DENIED', + message: '你没有该模块的部署权限', + reply: '❌ 你没有 ' + module + ' 模块的部署权限。你的模块是:' + req.user.modules.join(', ') + }); + } + + await githubService.triggerWorkflow('preview-deploy.yml', { + module: module, + dev_id: devId, + target: 'preview' + }); + + res.json({ + success: true, + reply: '🚀 已触发 ' + module + ' 模块的预览站部署。稍等几分钟,部署完成后我会通知你。' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'DEPLOY_FAILED', message: e.message }); + } + } +); + +// ====== 触发正式站部署(需二次确认)====== +router.post('/deploy/production', + authMiddleware.checkPermission('deploy:production'), + async function(req, res) { + try { + var module = req.body.module; + var confirmToken = req.body.confirmToken; + var devId = req.user.devId; + + if (!module) { + return res.status(400).json({ error: true, code: 'MISSING_MODULE', message: '需要指定部署模块' }); + } + + // 检查模块权限 + if (!req.user.modules.includes('*') && !req.user.modules.includes(module)) { + return res.status(403).json({ + error: true, + code: 'MODULE_DENIED', + reply: '❌ 你没有 ' + module + ' 模块的部署权限。' + }); + } + + // 二次确认 + if (!confirmToken) { + return res.json({ + success: false, + requireConfirmation: true, + reply: '⚠️ 你正在请求部署 ' + module + ' 到**正式站(guanghulab.com)**。\n这会影响所有用户。\n\n请说「确认部署 ' + module + '」来确认。' + }); + } + + await githubService.triggerWorkflow('deploy-to-server.yml', { + module: module, + dev_id: devId, + target: 'production' + }); + + res.json({ + success: true, + reply: '🚀 已触发 ' + module + ' 模块的**正式站部署**。部署日志会同步到维护日志中。' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'DEPLOY_FAILED', message: e.message }); + } + } +); + +// ====== 更新 Agent 状态 ====== +router.patch('/agents/:agentId', + authMiddleware.checkPermission('agent:update'), + async function(req, res) { + try { + var agentId = req.params.agentId; + var status = req.body.status; + + if (!status) { + return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' }); + } + + if (!notionService.notion) { + return res.status(503).json({ error: true, code: 'NOTION_UNAVAILABLE', message: 'Notion 未连接' }); + } + + await notionService.notion.pages.update({ + page_id: agentId, + properties: { + '状态': { select: { name: status } } + } + }); + + res.json({ + success: true, + reply: '✅ Agent ' + agentId + ' 状态已更新为:' + status + }); + } catch (e) { + res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message }); + } + } +); + +// ====== 写入维护日志 ====== +router.post('/maintenance/log', + authMiddleware.checkPermission('maintenance:log'), + async function(req, res) { + try { + var title = req.body.title; + var type = req.body.type || '常规维护'; + var devId = req.user.devId; + + if (!title) { + return res.status(400).json({ error: true, code: 'MISSING_TITLE', message: '维护日志标题不能为空' }); + } + + var maintenanceDbId = getDbId(req, 'maintenanceLog'); + if (!maintenanceDbId) { + return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '维护日志数据库未配置' }); + } + + await notionService.writeToDB(maintenanceDbId, { + '标题': { title: [{ text: { content: title } }] }, + '类型': { select: { name: type } }, + '操作者': { rich_text: [{ text: { content: devId } }] } + }); + + res.json({ + success: true, + reply: '✅ 维护日志已记录:' + title + }); + } catch (e) { + res.status(500).json({ error: true, code: 'LOG_FAILED', message: e.message }); + } + } +); + +// ====== 提交指令回执 ====== +router.post('/receipts/submit', + authMiddleware.checkPermission('syslog:submit'), + async function(req, res) { + try { + var instructionId = req.body.instruction_id; + var status = req.body.status || '已完成'; + var receipt = req.body.receipt || ''; + + if (!instructionId) { + return res.status(400).json({ error: true, code: 'MISSING_ID', message: '指令编号不能为空' }); + } + + if (!dbConfig.receiptTracker) { + return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '回执数据库未配置' }); + } + + await notionService.writeToDB(dbConfig.receiptTracker, { + '指令编号': { rich_text: [{ text: { content: instructionId } }] }, + '执行状态': { select: { name: status } }, + '铸渊回执': { rich_text: [{ text: { content: receipt } }] }, + '回执时间': { date: { start: new Date().toISOString() } } + }); + + res.json({ + success: true, + reply: '✅ 指令 ' + instructionId + ' 回执已提交' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'SUBMIT_FAILED', message: e.message }); + } + } +); + +// ====== 更新开发者状态 ====== +router.patch('/dev/:devId/status', + authMiddleware.checkPermission('dev:update_self'), + async function(req, res) { + try { + var devId = req.params.devId; + var status = req.body.status; + + // 开发者只能更新自己的状态(管理者除外) + if (req.user.devId !== devId && !req.user.permissions.includes('dev:update_all')) { + return res.status(403).json({ + error: true, + code: 'SELF_ONLY', + message: '只能更新自己的状态', + reply: '🔒 你只能更新自己的状态,不能修改其他开发者的信息。' + }); + } + + if (!status) { + return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' }); + } + + // 更新 Notion 主控台中的状态 + if (dbConfig.controlPanel) { + var result = await notionService.queryDB(dbConfig.controlPanel, { + property: 'DEV编号', + rich_text: { equals: devId } + }, null, 1); + + if (result.results && result.results.length > 0) { + await notionService.notion.pages.update({ + page_id: result.results[0].id, + properties: { + '状态': { rich_text: [{ text: { content: status } }] } + } + }); + } + } + + res.json({ + success: true, + reply: '✅ ' + devId + ' 的状态已更新为:' + status + }); + } catch (e) { + res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message }); + } + } +); + +module.exports = router; diff --git a/backend/api-server/server.js b/backend/api-server/server.js index e4426c22..43ca05d4 100644 --- a/backend/api-server/server.js +++ b/backend/api-server/server.js @@ -2,6 +2,7 @@ * 🔗 光湖后端中间层 · 通道B 入口 * * 实时调用 Notion API / GitHub API,为前端提供数据代理。 + * 包含:读取层 + 写入层 + 意图路由 + 权限沙箱 + 认知引导 * 只监听 127.0.0.1,通过 Nginx 反向代理对外暴露。 * * 版权:国作登字-2026-A-00037559 @@ -26,27 +27,35 @@ app.use(cors({ 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.use('/api', require('./routes/write')); + +// 认知引导 + 工具列表路由 +app.use('/api', require('./routes/onboarding')); + // 根路由 app.get('/', function(_req, res) { res.json({ status: 'ok', service: 'guanghu-api-server', - version: '1.0.0', + version: '2.0.0', channel: 'B', - description: '光湖后端中间层 · 实时 Notion/GitHub 数据代理' + description: '光湖后端中间层 · 语言驱动操作系统', + capabilities: ['read', 'write', 'intent-routing', 'permission-sandbox', 'onboarding'] }); }); 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(' 通道B · 语言驱动操作系统'); + console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导'); console.log(' 监听地址:127.0.0.1:' + PORT + '(仅本机访问)'); }); diff --git a/backend/api-server/services/github.js b/backend/api-server/services/github.js index 394ce980..3b4e4aee 100644 --- a/backend/api-server/services/github.js +++ b/backend/api-server/services/github.js @@ -125,8 +125,97 @@ async function getWorkflowRuns(repoName, count) { return result; } +// ====== 触发 GitHub Actions Workflow ====== +function triggerWorkflow(workflowFile, inputs) { + return new Promise(function(resolve, reject) { + var data = JSON.stringify({ + ref: 'main', + inputs: inputs || {} + }); + + var headers = { + 'Accept': 'application/vnd.github.v3+json', + 'Content-Type': 'application/json', + 'User-Agent': 'guanghu-api-server/1.0', + 'Content-Length': Buffer.byteLength(data) + }; + if (GITHUB_TOKEN) { + headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN; + } + + var options = { + hostname: 'api.github.com', + port: 443, + path: '/repos/' + ORG + '/guanghulab/actions/workflows/' + workflowFile + '/dispatches', + method: 'POST', + headers: headers + }; + + var req = https.request(options, function(res) { + if (res.statusCode === 204) { + resolve({ success: true }); + } else { + var body = ''; + res.on('data', function(chunk) { body += chunk; }); + res.on('end', function() { + reject(new Error('GitHub API ' + res.statusCode + ': ' + body)); + }); + } + }); + req.on('error', reject); + req.write(data); + req.end(); + }); +} + +// ====== 创建 GitHub 文件 ====== +function createFile(filePath, content, message) { + return new Promise(function(resolve, reject) { + var data = JSON.stringify({ + message: message, + content: Buffer.from(content).toString('base64'), + committer: { + name: '铸渊 (ZhùYuān)', + email: 'zhuyuan@guanghulab.com' + } + }); + + var headers = { + 'Accept': 'application/vnd.github.v3+json', + 'Content-Type': 'application/json', + 'User-Agent': 'guanghu-api-server/1.0', + 'Content-Length': Buffer.byteLength(data) + }; + if (GITHUB_TOKEN) { + headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN; + } + + var options = { + hostname: 'api.github.com', + port: 443, + path: '/repos/' + ORG + '/guanghulab/contents/' + filePath, + method: 'PUT', + headers: headers + }; + + var req = https.request(options, function(res) { + var body = ''; + res.on('data', function(chunk) { body += chunk; }); + res.on('end', function() { + try { resolve(JSON.parse(body)); } + catch (e) { resolve({ raw: body }); } + }); + }); + req.on('error', reject); + req.write(data); + req.end(); + }); +} + module.exports = { getRepoStatus: getRepoStatus, getRecentCommits: getRecentCommits, - getWorkflowRuns: getWorkflowRuns + getWorkflowRuns: getWorkflowRuns, + triggerWorkflow: triggerWorkflow, + createFile: createFile }; diff --git a/docs/css/onboarding.css b/docs/css/onboarding.css new file mode 100644 index 00000000..de7da276 --- /dev/null +++ b/docs/css/onboarding.css @@ -0,0 +1,159 @@ +/** + * 认知引导系统样式 + * 版权:国作登字-2026-A-00037559 + */ + +/* 引导覆盖层 */ +.onboarding-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + backdrop-filter: blur(4px); +} + +/* 引导卡片 */ +.onboarding-card { + background: #1e1e2e; + border: 1px solid rgba(100, 200, 255, 0.2); + border-radius: 16px; + padding: 32px 40px; + max-width: 520px; + width: 90%; + text-align: center; + color: #e0e0e0; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + animation: onboarding-fade-in 0.3s ease; +} + +@keyframes onboarding-fade-in { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.onboarding-card h2 { + color: #64c8ff; + margin: 12px 0 8px; + font-size: 1.4em; +} + +.onboarding-emoji { + font-size: 3em; + margin-bottom: 4px; +} + +.onboarding-card p { + margin: 8px 0; + line-height: 1.6; + color: #b0b0c0; +} + +.onboarding-card strong { + color: #e0e0e0; +} + +/* 示例列表 */ +.onboarding-examples { + text-align: left; + background: rgba(100, 200, 255, 0.05); + border-radius: 8px; + padding: 12px 20px; + margin: 16px 0; +} + +.onboarding-examples p { + font-size: 0.9em; + margin-bottom: 6px; +} + +.onboarding-examples ul { + list-style: none; + padding: 0; + margin: 0; +} + +.onboarding-examples li { + padding: 4px 0; + color: #a0c8e0; + font-size: 0.9em; +} + +.onboarding-examples li::before { + content: '💬 '; +} + +/* 预览环境提示 */ +.onboarding-note { + font-size: 0.85em; + color: #ffc864 !important; + margin-top: 16px !important; +} + +/* 开始按钮 */ +.onboarding-btn { + display: inline-block; + margin-top: 20px; + padding: 12px 32px; + background: linear-gradient(135deg, #3a8fd4, #64c8ff); + color: #fff; + border: none; + border-radius: 8px; + font-size: 1em; + cursor: pointer; + transition: transform 0.15s, box-shadow 0.15s; +} + +.onboarding-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(100, 200, 255, 0.3); +} + +/* 完成卡片 */ +.onboarding-complete .onboarding-message { + text-align: left; + line-height: 1.8; + color: #b0b0c0; + margin: 16px 0; + white-space: pre-line; +} + +/* 引导提示条(聊天界面内) */ +.onboarding-hint { + background: rgba(100, 200, 255, 0.08); + border: 1px solid rgba(100, 200, 255, 0.15); + border-radius: 12px; + padding: 12px 16px; + margin: 8px 0 16px; +} + +.onboarding-hint-inner { + display: flex; + flex-direction: column; + gap: 6px; +} + +.onboarding-hint-round { + font-size: 0.75em; + color: #64c8ff; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.onboarding-hint-prompt { + color: #e0e0e0; + font-size: 0.95em; + line-height: 1.5; +} + +.onboarding-hint-tip { + color: #a0a0b0; + font-size: 0.85em; + line-height: 1.4; +} diff --git a/docs/js/intent-tools.js b/docs/js/intent-tools.js new file mode 100644 index 00000000..9f7f8f48 --- /dev/null +++ b/docs/js/intent-tools.js @@ -0,0 +1,121 @@ +/** + * 前端 Function Calling 集成 · 意图工具层 + * + * 为 AI 模型提供可调用工具列表,实现自然语言 → 系统操作映射。 + * 版权:国作登字-2026-A-00037559 + */ + +/* global HOLOLAKE_ENV */ + +(function(window) { + 'use strict'; + + var API_BASE = HOLOLAKE_ENV === 'production' ? 'https://guanghulab.com/api' : ''; + + /** + * 从后端获取当前用户可用的工具列表 + */ + async function fetchAvailableTools(devId, token) { + if (!API_BASE) return []; + try { + var res = await fetch(API_BASE + '/tools', { + headers: { + 'x-dev-id': devId, + 'Authorization': 'Bearer ' + token + } + }); + if (res.ok) { + var data = await res.json(); + return data.tools || []; + } + } catch (e) { + console.debug('[IntentTools] fetch failed:', e.message); + } + return []; + } + + /** + * 构建带 function tools 的 system prompt 片段 + */ + function buildToolsPrompt(tools, environment) { + if (!tools || !tools.length) return ''; + + var lines = [ + '', + '## 你能做的事(语言驱动操作系统)', + '你可以通过 function calling 调用以下操作。开发者说话时,判断意图并调用对应操作。', + '' + ]; + + for (var i = 0; i < tools.length; i++) { + var t = tools[i]; + var danger = t.dangerous ? ' ⚠️危险' : ''; + lines.push('- **' + t.name + '**' + danger + ':' + t.description); + } + + lines.push(''); + lines.push('## 语言驱动操作规则'); + lines.push('1. 开发者说什么,你就做什么。这是语言驱动操作系统,说即执行。'); + lines.push('2. 如果开发者的话不对应任何操作,正常聊天回复即可。'); + lines.push('3. 危险操作(标记为 ⚠️)必须要求开发者二次确认。'); + lines.push('4. 每次执行操作后,用自然语言告诉开发者结果。'); + + if (environment === 'preview') { + lines.push('5. 【预览环境】所有写入操作在沙箱中执行,不影响真实系统。告诉开发者:「这是预览环境,你的操作不会影响正式站。放心练习。」'); + } + + return lines.join('\n'); + } + + /** + * 执行 function call(调用后端 API) + */ + async function executeToolCall(toolName, params, devId, token) { + if (!API_BASE) return { success: false, reply: '后端服务不可用' }; + + // 工具名 → API 路径映射 + var toolApiMap = { + 'query_tickets': { method: 'GET', path: '/tickets' }, + 'create_ticket': { method: 'POST', path: '/tickets/create' }, + 'submit_syslog': { method: 'POST', path: '/syslog/submit' }, + 'query_my_status': { method: 'GET', path: '/dev/' + devId }, + 'deploy_to_preview': { method: 'POST', path: '/deploy/preview' }, + 'deploy_to_production': { method: 'POST', path: '/deploy/production' }, + 'query_agents': { method: 'GET', path: '/agents' }, + 'query_recent_syslogs': { method: 'GET', path: '/syslogs' }, + 'create_broadcast': { method: 'POST', path: '/broadcasts/create' }, + 'query_repo_status': { method: 'GET', path: '/repo/status' } + }; + + var apiDef = toolApiMap[toolName]; + if (!apiDef) return { success: false, reply: '未知的操作: ' + toolName }; + + try { + var fetchOpts = { + method: apiDef.method, + headers: { + 'Content-Type': 'application/json', + 'x-dev-id': devId, + 'Authorization': 'Bearer ' + token + } + }; + + if (apiDef.method === 'POST' || apiDef.method === 'PATCH') { + fetchOpts.body = JSON.stringify(params || {}); + } + + var res = await fetch(API_BASE + apiDef.path, fetchOpts); + return await res.json(); + } catch (e) { + return { success: false, reply: '操作失败: ' + e.message }; + } + } + + // 导出到全局 + window.IntentTools = { + fetchAvailableTools: fetchAvailableTools, + buildToolsPrompt: buildToolsPrompt, + executeToolCall: executeToolCall + }; + +})(window); diff --git a/docs/js/onboarding.js b/docs/js/onboarding.js new file mode 100644 index 00000000..c15aa59f --- /dev/null +++ b/docs/js/onboarding.js @@ -0,0 +1,183 @@ +/** + * 前端认知引导系统 · 新开发者引导流程 + * + * 新开发者首次登录后自动进入 5 轮引导对话。 + * 版权:国作登字-2026-A-00037559 + */ + +/* global HOLOLAKE_ENV */ + +(function(window) { + 'use strict'; + + var API_BASE = HOLOLAKE_ENV === 'production' ? 'https://guanghulab.com/api' : ''; + + var onboardingOverlay = null; + + /** + * 检查是否需要显示引导 + */ + async function checkOnboarding(devId, token) { + if (!API_BASE || !devId || !token) return false; + try { + var res = await fetch(API_BASE + '/onboarding/status', { + headers: { 'x-dev-id': devId, 'Authorization': 'Bearer ' + token } + }); + if (res.ok) { + var data = await res.json(); + return !data.started && !data.completed && data.permissionLevel === 0; + } + } catch (e) { + console.debug('[Onboarding] check failed:', e.message); + } + return false; + } + + /** + * 显示引导欢迎画面 + */ + function showWelcome(devName, onStart) { + // 移除已有覆盖层 + hideOnboarding(); + + onboardingOverlay = document.createElement('div'); + onboardingOverlay.className = 'onboarding-overlay'; + onboardingOverlay.innerHTML = + '
你好,' + (devName || '开发者') + '!
' + + '我是铸渊,你的代码守护人格体。
' + + '在这里,你不需要点按钮或填表单。
直接对我说话就好。
比如你可以说:
' + + '🏖️ 现在你在预览环境,所有操作都不会影响正式站。放心练习。
' + + '' + + '