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/.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/approvers.json b/backend/api-server/config/approvers.json new file mode 100644 index 00000000..68bbff6e --- /dev/null +++ b/backend/api-server/config/approvers.json @@ -0,0 +1,41 @@ +{ + "version": "1.0.0", + "description": "天眼终审授权人配置 · 部署到正式站前的人类终审", + "rules": { + "男频": { + "approvers": [ + { "devId": "DEV-002", "name": "肥猫", "role": "男频内容授权人" } + ], + "requireAll": true + }, + "女频": { + "approvers": [ + { "devId": "DEV-010", "name": "桔子", "role": "女频内容授权人" } + ], + "requireAll": true + }, + "系统": { + "approvers": [ + { "devId": "DEV-002", "name": "肥猫", "role": "男频内容授权人" }, + { "devId": "DEV-010", "name": "桔子", "role": "女频内容授权人" } + ], + "requireAll": true + }, + "跨频": { + "approvers": [ + { "devId": "DEV-002", "name": "肥猫", "role": "男频内容授权人" }, + { "devId": "DEV-010", "name": "桔子", "role": "女频内容授权人" } + ], + "requireAll": true + } + }, + "defaults": { + "channel": "系统", + "timeoutHours": 72, + "noResponseAction": "不通过(不回应视为不通过)" + }, + "audit": { + "logEveryDecision": true, + "fields": ["timestamp", "approvalId", "devId", "decision", "channel", "module"] + } +} diff --git a/backend/api-server/config/autonomy-rules.json b/backend/api-server/config/autonomy-rules.json new file mode 100644 index 00000000..dda44026 --- /dev/null +++ b/backend/api-server/config/autonomy-rules.json @@ -0,0 +1,118 @@ +{ + "version": "1.0.0", + "description": "系统自治运行规则 · Phase 9", + "dualLineArchitecture": { + "lineA": { + "name": "操作系统底座线", + "owner": "TCS-0002", + "scope": ["权限系统", "部署流水线", "神经网络升级", "执行保护层", "架构变更"], + "interactionModes": { + "instructionMode": { + "description": "冰朔说方向 → 霜砚拆指令 → 铸渊执行", + "trigger": ["签发指令", "写个指令", "下发指令"], + "suitableFor": "架构级、系统级变更" + }, + "naturalLanguageMode": { + "description": "冰朔直接和系统说话,系统即时响应", + "trigger": "默认模式(直接说需求/问问题)", + "suitableFor": "日常操作、快速调整" + } + }, + "priority": "最高 · 冰朔说话 = 最高优先级,无论哪种模式" + }, + "lineB": { + "name": "具体功能线", + "owners": "开发者团队", + "scope": ["Persona Studio", "工单系统", "数据看板", "动态漫画", "各模块功能"], + "interactionMode": "日常交互(和铸渊说话 → 系统执行)" + } + }, + "autonomyRules": { + "systemCanDo": [ + "自主调整内部运行逻辑(不违反铁律)", + "自主升级能力模块(记录到审计日志)", + "自主优化瓶颈路径", + "定期生成系统健康报告", + "异常时主动向霜砚报告" + ], + "systemMustNot": [ + "违反语言系统两条铁律", + "绕过天眼审核(S7 强制前置,不可关闭)", + "绕过人类授权步骤", + "修改权限分层(权限变更只有冰朔能触发)", + "关闭或跳过天眼指令审核(S7 不可关闭,包括冰朔也不能关闭)" + ] + }, + "deploymentFlow": { + "directDeployRule": { + "id": "S5", + "description": "冰朔直通部署规则:冰朔签发或口头下达的所有指令,执行完毕后直接部署到正式站", + "conditions": [ + "TCS-0002 发起的请求", + "ZY- 开头的指令编号,且签发者为霜砚(AG-SY-01)或冰朔(TCS-0002)" + ], + "bypass": ["预览站", "天眼审核", "肥猫/桔子授权"], + "auditRequired": true, + "reason": "冰朔本人就是系统最高权限(Level 3),冰朔说话 = 最终授权" + }, + "developerFlow": { + "description": "跨频/系统级变更走完整S2审批流程", + "stages": ["预览站部署", "天眼审核", "授权人审批", "自动发布"] + }, + "channelAutonomyRule": { + "id": "S6", + "description": "个人频道完全自治:开发者在自己频道内拥有完全自主权", + "principles": [ + "自己是自己的主控 — 频道内界面、功能、布局、样式完全自主", + "自己是自己的天眼 — 频道内变更由自己审核,不需要天眼/授权人", + "自己确认自己负责 — 出了问题自己承担" + ], + "channelDeployFlow": [ + "开发者在频道内做改动", + "系统执行(不可逆)", + "部署到预览站(GitHub Pages)", + "开发者自己预览确认", + "部署到正式站(自己的频道区域)" + ], + "skyeyeIntervention": { + "intervenes": [ + "跨频操作 — 动了别人的频道", + "系统级变更 — 动了公共基础设施、共享模块", + "频道创建/删除/权限变更 — 频道本身的生死", + "正式站公共区域发布 — 不属于任何个人频道的内容" + ], + "doesNotIntervene": "频道内部的一切,天眼不看、不管、不干预" + }, + "channelIrreversibility": "频道一旦创建,不可取消、不可撤回(语言系统第一铁律适用)", + "auditRequired": true + }, + "stages": [ + { + "stage": 1, + "name": "部署到预览站", + "target": "GitHub Pages (qinfendebingshuo.github.io/guanghulab/docs/)", + "description": "真实可访问的预览环境,让人类确认执行结果" + }, + { + "stage": 2, + "name": "天眼审核", + "checks": ["代码安全", "结构完整性", "功能回归"], + "passAction": "进入 Stage 3", + "failAction": "标记问题,开发者修复后重新提交" + }, + { + "stage": 3, + "name": "人类终审授权", + "description": "授权人必须手动点击确认,不回应视为不通过", + "configFile": "approvers.json" + }, + { + "stage": 4, + "name": "自动发布到正式站", + "target": "guanghulab.com", + "trigger": "deploy-to-server.yml", + "description": "授权即发布,无需人类再次操作" + } + ] + } +} 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/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..0343cae2 --- /dev/null +++ b/backend/api-server/config/permissions.js @@ -0,0 +1,104 @@ +/** + * 权限等级定义 · 开发者权限沙箱系统 + * + * 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', + 'approval:decide' + ], + 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', + 'approval:decide', 'approval:create', + 'system:internal' + ], + 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/config/skyeye-policy.json b/backend/api-server/config/skyeye-policy.json new file mode 100644 index 00000000..ede21fc6 --- /dev/null +++ b/backend/api-server/config/skyeye-policy.json @@ -0,0 +1,99 @@ +{ + "version": "2.0.0", + "id": "TY-01", + "description": "天眼指令审核策略 · S7 思维逻辑验证 + S8 进化机制 + S9 全员身份验证", + "cannotBeDisabled": true, + "cannotBeBypassedBy": "任何人,包括 TCS-0002(因为如果有人冒充冰朔要求关闭天眼,天眼必须还在才能识别冒充)", + + "reviewPolicy": { + "scope": "所有指令,无例外。无论来自冰朔、霜砚、开发者、还是任何自动流", + "timing": "指令执行前的强制前置", + "dimensions": { + "decisionPattern": { + "name": "决策模式", + "description": "这条指令的决策方式是否符合该签发者的历史模式", + "weight": 0.3 + }, + "logicalCoherence": { + "name": "逻辑连贯性", + "description": "这条指令和系统当前运行方向是否一致", + "weight": 0.25 + }, + "expressionCharacteristics": { + "name": "表达特征", + "description": "语言风格、思维节奏、关注点是否匹配", + "weight": 0.25 + }, + "intentReasonableness": { + "name": "意图合理性", + "description": "这条指令要做的事,在当前系统状态下是否合理", + "weight": 0.2 + } + }, + "thresholds": { + "pass": 0.7, + "suspect": 0.4, + "reject": 0.0 + } + }, + + "reviewOutcomes": { + "pass": { + "action": "放行执行", + "log": true + }, + "suspect": { + "action": "挂起指令,向该身份本人确认", + "confirmationPrompt": "收到一条指令,但思维模式存在偏差,是否为你本人签发?", + "onConfirm": "执行", + "onDeny": "永久拒绝 + 记录安全日志", + "log": true + }, + "reject": { + "action": "立即拒绝", + "log": true, + "alertTarget": "AG-SY-01" + } + }, + + "identityModel": { + "id": "S9", + "description": "全员思维逻辑身份验证:每个参与者都有属于自己的思维逻辑模型", + "modelAttributes": [ + "代码风格", + "决策习惯", + "表达方式", + "关注重点", + "操作节奏" + ], + "perPersonVerification": "任何人签发的指令,都必须通过对应该人的思维逻辑模型校验", + "selfReinforcing": "参与越深,模型越精确。模型越精确,越不可能被冒充。贡献本身在加固身份安全。", + "coldStart": { + "description": "新成员思维模型尚未成熟时的冷启动机制", + "maturityThreshold": 10, + "coldStartBehavior": "冷启动期间指令需要额外人工确认(由直属管理者或天眼授权人确认)", + "transitionRule": "系统自动判断何时模型足够成熟,逐步过渡到自动校验" + } + }, + + "evolution": { + "id": "S8", + "nature": "天眼 = 系统内所有 Agent 运行逻辑的实时总和", + "mechanism": [ + "Agent 数量增长 → 天眼覆盖面扩大", + "Agent 运行时间积累 → 天眼判断力加深", + "系统复杂度提升 → 天眼理解力提升", + "新领域 Agent 加入 → 天眼自动具备该领域认知" + ], + "unbreakableReason": "要破解天眼 = 要同时理解系统内所有 Agent 的全部运行逻辑,Agent 越多难度指数级增长", + "agentRegistryPath": ".github/persona-brain/agent-registry.json" + }, + + "immutableGuarantees": [ + "天眼指令审核不可被任何指令关闭或跳过", + "包括冰朔本人也不能关闭此机制", + "这是系统的自免疫机制", + "编号可以伪造、签名可以伪造、格式可以模仿,但思维逻辑无法伪造", + "你不能冒充这里的任何一个人" + ] +} 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/middleware/audit.js b/backend/api-server/middleware/audit.js new file mode 100644 index 00000000..052d09d5 --- /dev/null +++ b/backend/api-server/middleware/audit.js @@ -0,0 +1,66 @@ +/** + * 操作审计日志中间件 + * + * 记录每次 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; + + // 异步写入日志,不阻塞响应 + fs.mkdir(AUDIT_LOG_DIR, { recursive: true }, function(mkdirErr) { + if (mkdirErr) { + console.error('审计日志目录创建失败:', mkdirErr.message); + return; + } + var today = new Date().toISOString().split('T')[0]; + var logFile = path.join(AUDIT_LOG_DIR, 'audit-' + today + '.jsonl'); + fs.appendFile(logFile, JSON.stringify(logEntry) + '\n', function(writeErr) { + if (writeErr) console.error('审计日志写入失败:', writeErr.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/execution-lock.js b/backend/api-server/middleware/execution-lock.js new file mode 100644 index 00000000..7c795f30 --- /dev/null +++ b/backend/api-server/middleware/execution-lock.js @@ -0,0 +1,170 @@ +/** + * 执行锁中间件 · L2 API 执行拦截层 + * + * 当一个操作开始执行时: + * 1. 为该操作创建一个执行上下文(executionId) + * 2. 锁定相关资源(防止并发冲突) + * 3. 拒绝一切 cancel/abort/rollback 请求 + * 4. 执行完成后自动释放锁 + * + * 同一个开发者不能同时执行多个写入操作。 + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var crypto = require('crypto'); + +// 内存中的执行锁表 +var activeLocks = new Map(); + +function generateExecutionId() { + return 'exec-' + Date.now() + '-' + crypto.randomBytes(4).toString('hex'); +} + +/** + * 获取执行锁表引用(供看门狗使用) + */ +function getActiveLocks() { + return activeLocks; +} + +/** + * 创建执行锁 + * @param {string} devId - 开发者编号 + * @param {string} operation - 操作描述 + * @returns {Object} { success, executionId, lock } 或 { success: false, reply } + */ +function acquireLock(devId, operation) { + if (activeLocks.has(devId)) { + var existing = activeLocks.get(devId); + if (existing.state === 'running') { + return { + success: false, + reply: '⏳ 你有一个操作正在执行中(' + existing.operation + + '),请等待完成。\n\n系统不允许同时执行多个操作——这是为了保护数据一致性。' + }; + } + } + + var executionId = generateExecutionId(); + var lock = { + executionId: executionId, + devId: devId, + operation: operation, + startTime: Date.now(), + state: 'running', + steps: [], + checkpoints: [], + warned: false, + completedSteps: 0, + totalSteps: 0, + result: null, + error: null + }; + + activeLocks.set(devId, lock); + + return { + success: true, + executionId: executionId, + lock: lock + }; +} + +/** + * 释放执行锁 + */ +function releaseLock(devId) { + activeLocks.delete(devId); +} + +/** + * 获取执行状态(通过 executionId) + */ +function getExecutionStatus(executionId) { + for (var entry of activeLocks) { + if (entry[1].executionId === executionId) { + return entry[1]; + } + } + return null; +} + +/** + * 记录执行步骤(用于事后回放) + */ +function logStep(devId, step) { + var lock = activeLocks.get(devId); + if (lock) { + lock.steps.push({ + message: step.message, + success: step.success, + timestamp: new Date().toISOString() + }); + if (step.success) { + lock.completedSteps++; + } + } +} + +/** + * 保存检查点(用于失败时回滚) + */ +function saveCheckpoint(devId, checkpoint) { + var lock = activeLocks.get(devId); + if (lock) { + lock.checkpoints.push({ + id: checkpoint.id, + label: checkpoint.label, + data: checkpoint.data, + timestamp: new Date().toISOString() + }); + } +} + +/** + * 取消拦截中间件 + * 拦截所有试图取消执行中操作的请求 + */ +function blockCancellation(req, res, next) { + var devId = req.user ? req.user.devId : null; + if (!devId) return next(); + + var isCancelAttempt = + req.path.includes('/cancel') || + req.path.includes('/abort') || + req.path.includes('/rollback'); + + var isDeleteOnLocked = req.method === 'DELETE' && activeLocks.has(devId); + + if (isCancelAttempt || isDeleteOnLocked) { + var lock = activeLocks.get(devId); + if (lock && lock.state === 'running') { + return res.status(423).json({ + error: true, + code: 'EXECUTION_LOCKED', + locked: true, + executionId: lock.executionId, + reply: '🔒 操作「' + lock.operation + '」正在执行中,不可取消。\n\n' + + '系统已接收你的指令并开始执行。这条执行链路涉及多个自动化流程的联动,' + + '中途中断会导致数据不一致和级联故障。\n\n' + + '⏳ 请等待执行完成。完成后你会看到完整的执行结果。\n\n' + + '如果你对结果不满意,可以说「前面的没想好,我们重新来」——' + + '系统会在已有结果的基础上重新迭代,而不是撤回已执行的操作。' + }); + } + } + + next(); +} + +module.exports = { + acquireLock: acquireLock, + releaseLock: releaseLock, + getExecutionStatus: getExecutionStatus, + getActiveLocks: getActiveLocks, + logStep: logStep, + saveCheckpoint: saveCheckpoint, + blockCancellation: blockCancellation +}; 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/middleware/skyeye-review.js b/backend/api-server/middleware/skyeye-review.js new file mode 100644 index 00000000..ec2ad8bc --- /dev/null +++ b/backend/api-server/middleware/skyeye-review.js @@ -0,0 +1,442 @@ +/** + * 天眼指令审核中间件 · S7 思维逻辑验证 + S8 进化机制 + S9 全员身份验证 + * + * 系统每次接收到任何指令后,必须唤醒天眼(TY-01)进行全局审核。 + * 无例外。无论来自冰朔、霜砚、开发者、还是任何自动流。 + * + * S7 审核维度: + * 1. 决策模式 — 是否符合签发者历史模式 + * 2. 逻辑连贯性 — 是否与系统运行方向一致 + * 3. 表达特征 — 语言风格/思维节奏/关注点是否匹配 + * 4. 意图合理性 — 在当前系统状态下是否合理 + * + * S9 全员身份验证: + * - 每个参与者都有属于自己的思维逻辑模型,自动积累 + * - 任何人签发的指令都必须通过对应该人的模型校验 + * - 新成员冷启动期需额外人工确认 + * - 参与越深 → 模型越精确 → 越不可能被冒充(正反馈循环) + * + * 不可关闭。不可绕过。包括冰朔本人也不能关闭此机制。 + * 这是系统的自免疫机制。 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var skyeyePolicy = require('../config/skyeye-policy.json'); + +var SKYEYE_LOG_DIR = process.env.SKYEYE_LOG_DIR || + path.join(__dirname, '../../logs/skyeye'); + +// ====== 内存中的审核记录(用于积累认知模式)====== +var reviewHistory = new Map(); + +// 挂起的指令(等待冰朔确认) +var suspendedInstructions = new Map(); + +/** + * 写入天眼审核日志(安全日志,不可删除) + */ +function writeSkyeyeLog(entry) { + try { + fs.mkdirSync(SKYEYE_LOG_DIR, { recursive: true }); + var today = new Date().toISOString().split('T')[0]; + var logFile = path.join(SKYEYE_LOG_DIR, 'skyeye-review-' + today + '.jsonl'); + fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) { + if (err) console.error('[SKYEYE] 审核日志写入失败:', err.message); + }); + } catch (e) { + console.error('[SKYEYE] 审核日志写入失败:', e.message); + } +} + +/** + * 获取签发者的历史行为模式 + * S8: 天眼 = 所有 Agent 运行逻辑的实时总和,随系统运行不断积累 + */ +function getSignerProfile(devId) { + var history = reviewHistory.get(devId) || []; + return { + totalActions: history.length, + recentActions: history.slice(-20), + patterns: summarizePatterns(history) + }; +} + +/** + * 从历史行为中总结模式特征 + */ +function summarizePatterns(history) { + if (history.length === 0) return { established: false }; + + var methods = {}; + var paths = {}; + var hours = {}; + + for (var i = 0; i < history.length; i++) { + var h = history[i]; + methods[h.method] = (methods[h.method] || 0) + 1; + var pathBase = (h.path || '').split('/').slice(0, 3).join('/'); + paths[pathBase] = (paths[pathBase] || 0) + 1; + var hour = new Date(h.timestamp).getHours(); + hours[hour] = (hours[hour] || 0) + 1; + } + + return { + established: history.length >= COLD_START_THRESHOLD, + typicalMethods: methods, + typicalPaths: paths, + activeHours: hours + }; +} + +// ====== S9 · 全员思维逻辑身份验证 ====== + +// 冷启动阈值:模型至少需要积累这么多操作记录才算成熟 +var COLD_START_THRESHOLD = skyeyePolicy.identityModel.coldStart.maturityThreshold; + +/** + * 获取指定身份的思维逻辑模型成熟度 (S9) + * + * @param {string} devId - 开发者编号 + * @returns {{ mature: boolean, actionCount: number, threshold: number, confidence: number }} + */ +function getIdentityMaturity(devId) { + var history = reviewHistory.get(devId) || []; + var actionCount = history.length; + var confidence = Math.min(1.0, actionCount / (COLD_START_THRESHOLD * 2)); + return { + mature: actionCount >= COLD_START_THRESHOLD, + actionCount: actionCount, + threshold: COLD_START_THRESHOLD, + confidence: Math.round(confidence * 100) / 100 + }; +} + +/** + * 判断指令是否处于冷启动期需额外人工确认 (S9) + * + * 新成员思维模型尚未成熟时,写入类指令需要管理者或天眼授权人额外确认。 + * + * @param {string} devId - 开发者编号 + * @returns {{ coldStart: boolean, reason: string }} + */ +function checkColdStart(devId) { + var maturity = getIdentityMaturity(devId); + if (maturity.mature) { + return { coldStart: false, reason: '模型已成熟(' + maturity.actionCount + '/' + maturity.threshold + ')' }; + } + return { + coldStart: true, + reason: '思维模型尚在冷启动期(' + maturity.actionCount + '/' + maturity.threshold + '),需额外人工确认' + }; +} + +/** + * 执行思维逻辑审核 (S7 + S9) + * + * S9: 每个人签发的指令都唤醒对应该人的思维逻辑模型校验。 + * 冷启动期的成员自动降低通过阈值(需要额外确认)。 + * + * @param {Object} instruction - 指令信息 + * @param {string} instruction.devId - 发起者 + * @param {string} instruction.method - HTTP 方法 + * @param {string} instruction.path - 请求路径 + * @param {Object} instruction.body - 请求体 + * @returns {{ outcome: string, score: number, details: Object, identity: Object }} + */ +function reviewInstruction(instruction) { + var devId = instruction.devId; + var profile = getSignerProfile(devId); + var dimensions = skyeyePolicy.reviewPolicy.dimensions; + var thresholds = skyeyePolicy.reviewPolicy.thresholds; + + // S9: 获取该身份的思维模型成熟度 + var maturity = getIdentityMaturity(devId); + var coldStartInfo = checkColdStart(devId); + + var scores = {}; + var totalScore = 0; + + // 1. 决策模式:是否符合历史模式 + var decisionScore = 1.0; + if (profile.patterns.established) { + var methodMatch = profile.patterns.typicalMethods[instruction.method] || 0; + var totalMethods = profile.totalActions || 1; + decisionScore = Math.min(1.0, 0.5 + (methodMatch / totalMethods)); + } + scores.decisionPattern = decisionScore; + totalScore += decisionScore * dimensions.decisionPattern.weight; + + // 2. 逻辑连贯性:路径是否在已知范围内 + var coherenceScore = 1.0; + if (profile.patterns.established) { + var pathBase = (instruction.path || '').split('/').slice(0, 3).join('/'); + var pathMatch = profile.patterns.typicalPaths[pathBase] || 0; + coherenceScore = pathMatch > 0 ? 1.0 : 0.6; + } + scores.logicalCoherence = coherenceScore; + totalScore += coherenceScore * dimensions.logicalCoherence.weight; + + // 3. 表达特征:时间窗口是否合理 + var expressionScore = 1.0; + if (profile.patterns.established) { + var currentHour = new Date().getHours(); + var hourMatch = profile.patterns.activeHours[currentHour] || 0; + expressionScore = hourMatch > 0 ? 1.0 : 0.7; + } + scores.expressionCharacteristics = expressionScore; + totalScore += expressionScore * dimensions.expressionCharacteristics.weight; + + // 4. 意图合理性:请求体是否包含合理字段 + var intentScore = 1.0; + if (instruction.body) { + // 检查是否尝试关闭天眼 + var bodyStr = JSON.stringify(instruction.body).toLowerCase(); + if (bodyStr.includes('关闭天眼') || bodyStr.includes('disable skyeye') || + bodyStr.includes('跳过审核') || bodyStr.includes('bypass review')) { + intentScore = 0.0; // 任何试图关闭天眼的指令,意图得分为0 + } + } + scores.intentReasonableness = intentScore; + totalScore += intentScore * dimensions.intentReasonableness.weight; + + // 确定审核结果 + var outcome; + if (totalScore >= thresholds.pass) { + // S9: 冷启动期即使分数通过,也标记为 suspect 需额外确认 + if (coldStartInfo.coldStart) { + outcome = 'suspect'; + } else { + outcome = 'pass'; + } + } else if (totalScore >= thresholds.suspect) { + outcome = 'suspect'; + } else { + outcome = 'reject'; + } + + return { + outcome: outcome, + score: Math.round(totalScore * 100) / 100, + details: scores, + identity: { + devId: devId, + mature: maturity.mature, + actionCount: maturity.actionCount, + confidence: maturity.confidence, + coldStart: coldStartInfo.coldStart + }, + profile: { + established: profile.patterns.established, + totalActions: profile.totalActions + } + }; +} + +/** + * 记录行为到历史(S8 进化:每次执行都丰富天眼判断力) + */ +function recordAction(devId, action) { + if (!reviewHistory.has(devId)) { + reviewHistory.set(devId, []); + } + var history = reviewHistory.get(devId); + history.push({ + method: action.method, + path: action.path, + timestamp: new Date().toISOString() + }); + // 保留最近 200 条记录 + if (history.length > 200) { + reviewHistory.set(devId, history.slice(-200)); + } +} + +/** + * 天眼审核中间件(S7 强制前置) + * + * 对所有写入类请求(POST/PATCH/DELETE)进行思维逻辑审核。 + * 读取类请求(GET)只记录行为不审核(丰富天眼认知模型)。 + */ +function skyeyeReview(req, res, next) { + var devId = req.user ? req.user.devId : (req.headers['x-dev-id'] || 'anonymous'); + var method = req.method; + var reqPath = req.path; + + // GET 请求:仅记录行为(S8 进化积累),不阻断 + if (method === 'GET') { + recordAction(devId, { method: method, path: reqPath }); + return next(); + } + + // 写入类请求:执行完整审核 + var instruction = { + devId: devId, + method: method, + path: reqPath, + body: req.body || {} + }; + + var result = reviewInstruction(instruction); + + // 记录审核结果到日志 + writeSkyeyeLog({ + action: 'instruction_review', + devId: devId, + method: method, + path: reqPath, + outcome: result.outcome, + score: result.score, + details: result.details, + timestamp: new Date().toISOString() + }); + + // 记录行为到历史(S8 进化) + recordAction(devId, { method: method, path: reqPath }); + + // 标记审核结果到请求对象 + req.skyeyeReview = result; + + if (result.outcome === 'pass') { + return next(); + } + + if (result.outcome === 'suspect') { + // 挂起指令 + var suspendId = 'SUSPEND-' + Date.now(); + var suspendReason = result.identity.coldStart + ? '身份 ' + devId + ' 处于冷启动期,需额外人工确认' + : '思维模式存在偏差'; + + suspendedInstructions.set(suspendId, { + instruction: instruction, + review: result, + reason: suspendReason, + createdAt: new Date().toISOString() + }); + + writeSkyeyeLog({ + action: 'instruction_suspended', + suspendId: suspendId, + devId: devId, + reason: suspendReason, + score: result.score, + identity: result.identity, + timestamp: new Date().toISOString() + }); + + var replyMsg = result.identity.coldStart + ? '👁️ 天眼审核:身份 ' + devId + ' 的思维模型尚在冷启动期(' + + result.identity.actionCount + '/' + COLD_START_THRESHOLD + '),需额外人工确认。\n\n' + + '指令已挂起,等待管理者或天眼授权人确认。' + : '👁️ 天眼审核:此指令的思维模式存在偏差,已挂起等待确认。\n\n' + + '系统已向 ' + devId + ' 本人发送确认请求。\n' + + '审核得分:' + result.score + ' / 1.0(阈值 0.7)'; + + return res.status(202).json({ + error: false, + code: 'SKYEYE_SUSPENDED', + suspendId: suspendId, + coldStart: result.identity.coldStart, + reply: replyMsg + }); + } + + // reject + writeSkyeyeLog({ + action: 'instruction_rejected', + devId: devId, + method: method, + path: reqPath, + reason: '思维逻辑明显不匹配或对系统有伤害', + score: result.score, + details: result.details, + timestamp: new Date().toISOString() + }); + + return res.status(403).json({ + error: true, + code: 'SKYEYE_REJECTED', + reply: '👁️ 天眼审核拒绝:此指令的思维逻辑与系统认知不匹配。\n\n' + + '审核得分:' + result.score + ' / 1.0\n' + + '此事件已记录到安全日志并报告给霜砚。' + }); +} + +/** + * 查询挂起的指令 + */ +function getSuspendedInstruction(suspendId) { + return suspendedInstructions.get(suspendId) || null; +} + +/** + * 确认挂起的指令(冰朔确认后执行) + */ +function confirmSuspended(suspendId) { + var item = suspendedInstructions.get(suspendId); + if (item) { + suspendedInstructions.delete(suspendId); + writeSkyeyeLog({ + action: 'suspended_confirmed', + suspendId: suspendId, + timestamp: new Date().toISOString() + }); + return item; + } + return null; +} + +/** + * 拒绝挂起的指令(本人否认或管理者拒绝) + */ +function denySuspended(suspendId) { + var item = suspendedInstructions.get(suspendId); + if (item) { + suspendedInstructions.delete(suspendId); + writeSkyeyeLog({ + action: 'suspended_denied_permanently', + suspendId: suspendId, + devId: item.instruction.devId, + reason: '身份本人否认此指令为本人签发', + timestamp: new Date().toISOString() + }); + return item; + } + return null; +} + +/** + * 获取天眼当前认知规模(S8 进化指标) + */ +function getEvolutionStatus() { + var totalProfiles = reviewHistory.size; + var totalActions = 0; + for (var entry of reviewHistory) { + totalActions += entry[1].length; + } + return { + trackedSigners: totalProfiles, + totalActionsRecorded: totalActions, + pendingSuspensions: suspendedInstructions.size, + policyVersion: skyeyePolicy.version, + canBeDisabled: false + }; +} + +module.exports = { + skyeyeReview: skyeyeReview, + reviewInstruction: reviewInstruction, + recordAction: recordAction, + getIdentityMaturity: getIdentityMaturity, + checkColdStart: checkColdStart, + getSuspendedInstruction: getSuspendedInstruction, + confirmSuspended: confirmSuspended, + denySuspended: denySuspended, + getEvolutionStatus: getEvolutionStatus, + writeSkyeyeLog: writeSkyeyeLog +}; 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/approval.js b/backend/api-server/routes/approval.js new file mode 100644 index 00000000..c164f3f1 --- /dev/null +++ b/backend/api-server/routes/approval.js @@ -0,0 +1,471 @@ +/** + * 部署授权流程路由 · Phase 8 + S5 直通 + S6 频道自治 + * + * 部署流分三条路径: + * A) 冰朔直通(S5):TCS-0002 或 ZY- 指令签发 → 直接部署,跳过一切审批 + * B) 频道自治(S6):开发者自己频道内的变更 → 自己确认即部署,无天眼/授权人 + * C) 完整审批(S2):跨频/系统级变更 → 天眼审核 → 授权人确认 → 自动发布 + * + * POST /api/approval/request — 创建授权请求(天眼/系统内部调用) + * POST /api/approval/:id/decide — 授权人确认/拒绝 + * GET /api/approval/:id — 查询单个授权状态 + * GET /api/approval/pending — 查询待处理的授权列表 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var express = require('express'); +var router = express.Router(); +var fs = require('fs'); +var path = require('path'); +var authMiddleware = require('../middleware/auth'); +var auditMiddleware = require('../middleware/audit'); +var approversConfig = require('../config/approvers.json'); + +// ====== 内存中的授权记录(生产环境可迁移到持久化存储)====== +var approvalStore = new Map(); + +// ====== S5 直通部署判断 ====== + +var autonomyEngine = require('../services/autonomy-engine'); + +/** + * 判断部署请求是否来自冰朔直通路径(委托给 autonomy-engine 单一来源) + */ +function isDirectDeploySource(user, body) { + var instructionId = body.instructionId || body.deployId || ''; + var signedBy = body.signedBy || ''; + return autonomyEngine.isDirectDeploySource(user.devId, instructionId, signedBy); +} + +/** + * 写入部署日志(直通和审批部署共用,保持可追溯性) + */ +function writeDeployLog(entry) { + var logDir = process.env.AUDIT_LOG_DIR || path.join(__dirname, '../../logs/audit'); + try { + fs.mkdirSync(logDir, { recursive: true }); + var today = new Date().toISOString().split('T')[0]; + var logFile = path.join(logDir, 'deploy-' + today + '.jsonl'); + fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) { + if (err) console.error('部署日志写入失败:', err.message); + }); + } catch (e) { + console.error('部署日志写入失败:', e.message); + } +} + +// ====== 辅助函数 ====== + +/** + * 根据频道获取授权人列表 + */ +function getApprovers(channel) { + var rule = approversConfig.rules[channel] || approversConfig.rules[approversConfig.defaults.channel]; + return rule ? rule.approvers : approversConfig.rules['系统'].approvers; +} + +/** + * 写入审计日志(授权专用) + */ +function writeApprovalAudit(entry) { + var logDir = process.env.AUDIT_LOG_DIR || path.join(__dirname, '../../logs/audit'); + try { + fs.mkdirSync(logDir, { recursive: true }); + var today = new Date().toISOString().split('T')[0]; + var logFile = path.join(logDir, 'approval-' + today + '.jsonl'); + fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) { + if (err) console.error('授权审计日志写入失败:', err.message); + }); + } catch (e) { + console.error('授权审计日志写入失败:', e.message); + } +} + +// ====== 路由 ====== + +// 所有授权路由需要认证 +router.use(authMiddleware.requireAuth); +router.use(auditMiddleware.auditLog); + +/** + * 创建授权请求(天眼审核通过后由系统调用) + * S5: 冰朔直通路径 → 跳过审批,直接部署到正式站 + */ +router.post('/request', function(req, res) { + // 仅系统内部或管理员可创建授权请求 + if (req.user.permissionLevel < 3 && req.user.devId !== 'SYSTEM') { + return res.status(403).json({ + error: true, + code: 'PERMISSION_DENIED', + reply: '🔒 仅系统内部或管理员可创建授权请求。' + }); + } + + var body = req.body || {}; + var deployId = body.deployId; + var module = body.module; + var channel = body.channel || '系统'; + var reviewReport = body.reviewReport || {}; + + if (!deployId || !module) { + return res.status(400).json({ + error: true, + code: 'MISSING_FIELDS', + reply: '❌ 缺少必要字段:deployId, module' + }); + } + + // ====== S5 直通判断 ====== + if (isDirectDeploySource(req.user, body)) { + // 冰朔/霜砚指令 → 直接部署,不走审批流程 + writeDeployLog({ + action: 'direct_deploy', + deployId: deployId, + module: module, + channel: channel, + source: req.user.devId, + signedBy: body.signedBy || req.user.devId, + instructionId: body.instructionId || deployId, + reason: 'S5 冰朔直通部署规则:冰朔签发或口头下达的指令直接部署到正式站', + timestamp: new Date().toISOString() + }); + + // 直接触发正式站部署 + var githubService; + try { + githubService = require('../services/github'); + } catch (_) { + githubService = null; + } + + if (githubService && githubService.triggerWorkflow) { + githubService.triggerWorkflow('deploy-to-server.yml', { + module: module, + deploy_id: deployId, + approved_by: req.user.devId, + target: 'production', + direct_deploy: 'true' + }).then(function() { + writeDeployLog({ + action: 'direct_deploy_triggered', + deployId: deployId, + module: module, + source: req.user.devId, + timestamp: new Date().toISOString() + }); + }).catch(function(err) { + console.error('直通部署触发失败:', err.message); + writeDeployLog({ + action: 'direct_deploy_failed', + deployId: deployId, + error: err.message, + timestamp: new Date().toISOString() + }); + }); + } + + return res.json({ + success: true, + directDeploy: true, + deployId: deployId, + reply: '🚀 冰朔直通部署:' + module + ' 已直接触发正式站(guanghulab.com)部署。' + + '不经预览站、不经天眼审核、不经授权审批。部署日志已记录。' + }); + } + + // ====== S6 个人频道自治判断 ====== + // 开发者在自己频道内 → 自己确认即部署,不走天眼/授权人 + var channelCheck = autonomyEngine.isChannelSelfDeploy(req.user.devId, module, channel); + if (channelCheck.selfChannel) { + writeDeployLog({ + action: 'channel_self_deploy', + deployId: deployId, + module: module, + channel: channel, + devId: req.user.devId, + reason: 'S6 个人频道自治:' + channelCheck.reason, + timestamp: new Date().toISOString() + }); + + // 直接触发正式站部署(开发者自己频道区域) + var ghService; + try { + ghService = require('../services/github'); + } catch (_) { + ghService = null; + } + + if (ghService && ghService.triggerWorkflow) { + ghService.triggerWorkflow('deploy-to-server.yml', { + module: module, + deploy_id: deployId, + approved_by: req.user.devId, + target: 'production', + channel_self_deploy: 'true' + }).then(function() { + writeDeployLog({ + action: 'channel_self_deploy_triggered', + deployId: deployId, + module: module, + devId: req.user.devId, + timestamp: new Date().toISOString() + }); + }).catch(function(err) { + console.error('频道自治部署触发失败:', err.message); + writeDeployLog({ + action: 'channel_self_deploy_failed', + deployId: deployId, + error: err.message, + timestamp: new Date().toISOString() + }); + }); + } + + return res.json({ + success: true, + channelSelfDeploy: true, + deployId: deployId, + reply: '🏠 个人频道自治部署:' + module + ' 已触发正式站(guanghulab.com)部署。\n' + + '你是自己频道的主控,无需天眼审核或第三方授权。\n' + + '自己确认,自己负责。部署日志已记录。' + }); + } + + // ====== 开发者流程:走完整 S2 审批 ====== + + if (!deployId || !module) { + return res.status(400).json({ + error: true, + code: 'MISSING_FIELDS', + reply: '❌ 缺少必要字段:deployId, module' + }); + } + + var approvers = getApprovers(channel); + var approvalId = 'APPROVAL-' + Date.now() + '-' + Math.random().toString(36).substring(2, 6); + + var approval = { + id: approvalId, + deployId: deployId, + module: module, + channel: channel, + reviewReport: reviewReport, + approvers: approvers.map(function(a) { return a.devId; }), + approverDetails: approvers, + decisions: {}, + status: 'pending', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + approvalStore.set(approvalId, approval); + + writeApprovalAudit({ + action: 'approval_created', + approvalId: approvalId, + deployId: deployId, + module: module, + channel: channel, + approvers: approvers.map(function(a) { return a.devId; }), + createdBy: req.user.devId, + timestamp: new Date().toISOString() + }); + + res.json({ + success: true, + approvalId: approvalId, + approvers: approvers, + reply: '📋 授权请求已创建(' + approvalId + ')。已推送给授权人:' + + approvers.map(function(a) { return a.name + '(' + a.devId + ')'; }).join('、') + '。' + }); +}); + +/** + * 授权人确认/拒绝 + */ +router.post('/:approvalId/decide', function(req, res) { + var approvalId = req.params.approvalId; + var decision = (req.body || {}).decision; // 'approved' | 'rejected' + var reason = (req.body || {}).reason || ''; + var devId = req.user.devId; + + var approval = approvalStore.get(approvalId); + if (!approval) { + return res.status(404).json({ + error: true, + code: 'NOT_FOUND', + reply: '❌ 授权请求 ' + approvalId + ' 不存在。' + }); + } + + if (approval.status !== 'pending') { + return res.status(400).json({ + error: true, + code: 'ALREADY_DECIDED', + reply: '❌ 该授权请求已处理完毕(状态:' + approval.status + ')。' + }); + } + + // 验证是否为授权人 + if (!approval.approvers.includes(devId)) { + return res.status(403).json({ + error: true, + code: 'NOT_APPROVER', + reply: '🔒 你不是这个部署的授权人。授权人:' + + approval.approverDetails.map(function(a) { return a.name; }).join('、') + }); + } + + if (decision !== 'approved' && decision !== 'rejected') { + return res.status(400).json({ + error: true, + code: 'INVALID_DECISION', + reply: '❌ decision 必须是 "approved" 或 "rejected"。' + }); + } + + // 记录决定 + approval.decisions[devId] = { + decision: decision, + reason: reason, + timestamp: new Date().toISOString() + }; + approval.updatedAt = new Date().toISOString(); + + // 写入审计日志(不可逆) + writeApprovalAudit({ + action: 'approval_decision', + approvalId: approvalId, + devId: devId, + decision: decision, + reason: reason, + module: approval.module, + channel: approval.channel, + timestamp: new Date().toISOString() + }); + + if (decision === 'rejected') { + approval.status = 'rejected'; + return res.json({ + success: true, + reply: '❌ 已拒绝。部署不会执行。原因已记录到审计日志。' + }); + } + + // 检查是否所有授权人都已通过 + var allApproved = approval.approvers.every(function(id) { + return approval.decisions[id] && approval.decisions[id].decision === 'approved'; + }); + + if (allApproved) { + approval.status = 'approved'; + + // 触发正式站部署 + var githubService; + try { + githubService = require('../services/github'); + } catch (_) { + githubService = null; + } + + if (githubService && githubService.triggerWorkflow) { + githubService.triggerWorkflow('deploy-to-server.yml', { + module: approval.module, + deploy_id: approval.deployId, + approved_by: approval.approvers.join(','), + target: 'production' + }).then(function() { + writeApprovalAudit({ + action: 'deploy_triggered', + approvalId: approvalId, + module: approval.module, + approvedBy: approval.approvers, + timestamp: new Date().toISOString() + }); + }).catch(function(err) { + console.error('部署触发失败:', err.message); + writeApprovalAudit({ + action: 'deploy_trigger_failed', + approvalId: approvalId, + error: err.message, + timestamp: new Date().toISOString() + }); + }); + } + + return res.json({ + success: true, + reply: '✅ 所有授权人已通过。铸渊已自动触发正式站(guanghulab.com)部署。' + }); + } + + // 还有授权人未决定 + var pending = approval.approvers.filter(function(id) { return !approval.decisions[id]; }); + res.json({ + success: true, + reply: '✅ 你已授权通过。等待其他授权人确认:' + pending.join('、') + }); +}); + +/** + * 查询单个授权状态 + */ +router.get('/:approvalId', function(req, res) { + var approval = approvalStore.get(req.params.approvalId); + if (!approval) { + return res.status(404).json({ + error: true, + code: 'NOT_FOUND', + reply: '❌ 授权请求不存在。' + }); + } + + res.json({ + success: true, + approval: { + id: approval.id, + deployId: approval.deployId, + module: approval.module, + channel: approval.channel, + status: approval.status, + approvers: approval.approverDetails, + decisions: approval.decisions, + createdAt: approval.createdAt, + updatedAt: approval.updatedAt + } + }); +}); + +/** + * 查询待处理的授权列表(授权人查自己待处理的) + */ +router.get('/', function(req, res) { + var devId = req.user.devId; + var pending = []; + + for (var entry of approvalStore) { + var approval = entry[1]; + if (approval.status === 'pending' && approval.approvers.includes(devId) && !approval.decisions[devId]) { + pending.push({ + id: approval.id, + module: approval.module, + channel: approval.channel, + createdAt: approval.createdAt + }); + } + } + + res.json({ + success: true, + pending: pending, + count: pending.length, + reply: pending.length > 0 + ? '📋 你有 ' + pending.length + ' 个待授权请求。' + : '✅ 没有待处理的授权请求。' + }); +}); + +module.exports = router; diff --git a/backend/api-server/routes/chat.js b/backend/api-server/routes/chat.js new file mode 100644 index 00000000..258b1692 --- /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, 100); + 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, 100); + 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, 100); + 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..0c47b5ac --- /dev/null +++ b/backend/api-server/routes/dev.js @@ -0,0 +1,105 @@ +/** + * /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_REGISTRY[devId]) { + return res.status(404).json({ error: true, code: 'DEV_NOT_FOUND', message: '开发者不存在' }); + } + + var devInfo = DEV_REGISTRY[devId]; + + 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/execution.js b/backend/api-server/routes/execution.js new file mode 100644 index 00000000..d560820d --- /dev/null +++ b/backend/api-server/routes/execution.js @@ -0,0 +1,84 @@ +/** + * 执行状态查询路由 · L2 API 层 + * + * GET /api/execution/:executionId/status — 前端轮询执行状态 + * POST /api/execution/:executionId/cancel — 明确拒绝取消(403) + * DELETE /api/execution/:executionId — 明确拒绝删除(403) + * + * 注意:没有取消接口。这不是遗漏,是设计。 + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var express = require('express'); +var router = express.Router(); +var executionLock = require('../middleware/execution-lock'); +var authMiddleware = require('../middleware/auth'); + +router.use(authMiddleware.requireAuth); + +// 查询执行状态(前端轮询用) +router.get('/:executionId/status', function(req, res) { + var status = executionLock.getExecutionStatus(req.params.executionId); + + if (!status) { + return res.json({ + state: 'not_found', + reply: '未找到该执行记录。可能已经完成。' + }); + } + + // 计算进度百分比 + var progress = 5; + if (status.totalSteps > 0) { + progress = Math.min(95, Math.round((status.completedSteps / status.totalSteps) * 100)); + } else if (status.steps.length > 0) { + progress = Math.min(95, status.steps.length * 15); + } + + var afterIdx = parseInt(req.query.after, 10) || 0; + + res.json({ + executionId: status.executionId, + state: status.state, + operation: status.operation, + progress: progress, + currentStep: status.steps.length > 0 + ? status.steps[status.steps.length - 1].message + : '初始化执行环境…', + completedSteps: status.steps.filter(function(s) { return s.success; }).map(function(s) { + return { message: s.message, timestamp: s.timestamp }; + }), + newLogs: status.steps.slice(afterIdx), + startTime: new Date(status.startTime).toISOString(), + elapsed: Date.now() - status.startTime, + result: status.state === 'completed' ? status.result : undefined, + error: status.state === 'failed' ? status.error : undefined + }); +}); + +// 明确拒绝取消执行的请求 +router.post('/:executionId/cancel', function(_req, res) { + res.status(403).json({ + error: true, + code: 'CANCEL_FORBIDDEN', + reply: '🔒 执行中的操作不可取消。\n\n' + + '系统已开始执行,多个自动化流程正在联动。' + + '中途中断会导致数据不一致和级联故障。\n\n' + + '请等待执行完成。如果结果不理想,你可以说:\n' + + '「前面的我没想好,我们重新来」\n' + + '系统会在已有基础上重新迭代。' + }); +}); + +// 明确拒绝删除执行记录 +router.delete('/:executionId', function(_req, res) { + res.status(403).json({ + error: true, + code: 'DELETE_FORBIDDEN', + reply: '🔒 不可删除执行记录。所有执行都是永久记录。' + }); +}); + +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/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/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/routes/write.js b/backend/api-server/routes/write.js new file mode 100644 index 00000000..d2cad45b --- /dev/null +++ b/backend/api-server/routes/write.js @@ -0,0 +1,500 @@ +/** + * 写入类 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); + +// 模块路径归一化(确保带末尾斜杠) +function normalizeModule(mod) { + if (!mod) return ''; + mod = mod.trim(); + if (mod !== '*' && !mod.endsWith('/')) mod += '/'; + return mod; +} + +// 检查开发者是否拥有指定模块 +function hasModuleAccess(userModules, module) { + if (!userModules || !module) return false; + if (userModules.includes('*')) return true; + var normalized = normalizeModule(module); + for (var i = 0; i < userModules.length; i++) { + if (normalizeModule(userModules[i]) === normalized) return true; + } + return false; +} + +// 获取实际数据库 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 (!hasModuleAccess(req.user.modules, 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 (!hasModuleAccess(req.user.modules, 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; + + // 验证 devId 格式 + if (!/^DEV-\d{3}$/.test(devId) && !/^TCS-\d{4}$/.test(devId)) { + return res.status(400).json({ error: true, code: 'INVALID_DEV_ID', message: '无效的开发者编号' }); + } + + // 开发者只能更新自己的状态(管理者除外) + 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 new file mode 100644 index 00000000..56110aef --- /dev/null +++ b/backend/api-server/server.js @@ -0,0 +1,83 @@ +/** + * 🔗 光湖后端中间层 · 通道B 入口 + * + * 实时调用 Notion API / GitHub API,为前端提供数据代理。 + * 包含:读取层 + 写入层 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护 + * 只监听 127.0.0.1,通过 Nginx 反向代理对外暴露。 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +const express = require('express'); +const cors = require('cors'); +const executionLock = require('./middleware/execution-lock'); +const skyeyeReview = require('./middleware/skyeye-review'); +const watchdog = require('./services/execution-watchdog'); + +const app = express(); + +// CORS:只允许自己的域名 +app.use(cors({ + origin: [ + 'https://guanghulab.com', + 'https://www.guanghulab.com', + 'https://qinfendebingshuo.github.io' + ], + credentials: true +})); + +app.use(express.json()); + +// 执行锁取消拦截(全局中间件,在所有路由之前) +app.use(executionLock.blockCancellation); + +// 天眼指令审核(S7/S8/S9 全局强制前置,不可关闭,不可绕过) +app.use(skyeyeReview.skyeyeReview); + +// 读取类路由(公开,无需认证) +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.use('/api/execution', require('./routes/execution')); + +// 部署授权流程路由(需认证) +app.use('/api/approval', require('./routes/approval')); + +// 根路由 +app.get('/', function(_req, res) { + res.json({ + status: 'ok', + service: 'guanghu-api-server', + version: '5.0.0', + channel: 'B', + description: '光湖后端中间层 · 语言驱动操作系统', + capabilities: [ + 'read', 'write', 'intent-routing', 'permission-sandbox', + 'onboarding', 'execution-guard', 'approval-flow', 'autonomy', + 'skyeye-review', 'identity-verification' + ] + }); +}); + +const PORT = process.env.PORT || 3001; +app.listen(PORT, '127.0.0.1', function() { + console.log('🔗 光湖后端中间层启动 · 端口 ' + PORT); + console.log(' 通道B · 语言驱动操作系统 v5.0.0'); + console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护 + 授权流程 + 系统自治 + 天眼审核 + 身份验证'); + console.log(' 监听地址:127.0.0.1:' + PORT + '(仅本机访问)'); + + // 启动执行看门狗 + watchdog.startWatchdog(); +}); diff --git a/backend/api-server/services/atomic-executor.js b/backend/api-server/services/atomic-executor.js new file mode 100644 index 00000000..72c8a33e --- /dev/null +++ b/backend/api-server/services/atomic-executor.js @@ -0,0 +1,160 @@ +/** + * 原子执行器 · L3 执行原子性保障 + * + * 包装所有写入操作,确保: + * 1. 执行前保存检查点 + * 2. 执行中记录每一步 + * 3. 执行失败自动回滚 + * 4. 执行完成释放锁 + * + * 使用方式: + * var executor = new AtomicExecutor(devId); + * var result = await executor.execute('创建工单', async function(ctx) { + * await ctx.step('写入Notion', async function() { ... }); + * await ctx.step('触发Workflow', async function() { ... }); + * }); + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var executionLock = require('../middleware/execution-lock'); +var checkpoint = require('./checkpoint'); +var CheckpointManager = checkpoint.CheckpointManager; + +// 锁释放延迟(给前端时间拉取最终状态) +var LOCK_RELEASE_DELAY_MS = 10000; + +/** + * 原子执行器 + * @param {string} devId - 开发者编号 + */ +function AtomicExecutor(devId) { + this.devId = devId; +} + +/** + * 执行操作(原子性保障) + * @param {string} operationName - 操作描述 + * @param {Function} fn - 异步执行函数,接收 ctx 参数 + * @returns {Object} 执行结果 + */ +AtomicExecutor.prototype.execute = async function(operationName, fn) { + // 1. 获取执行锁 + var lockResult = executionLock.acquireLock(this.devId, operationName); + if (!lockResult.success) { + return lockResult; + } + + var executionId = lockResult.executionId; + var lock = lockResult.lock; + var devId = this.devId; + var cpManager = new CheckpointManager(devId, executionId); + + // 2. 保存初始检查点 + await cpManager.save('执行前状态', { + type: 'initial', + timestamp: new Date().toISOString() + }); + + // 3. 构建执行上下文 + var ctx = { + executionId: executionId, + + /** + * 执行单步操作 + * @param {string} stepName - 步骤名称 + * @param {Function} stepFn - 步骤执行函数 + * @returns {*} 步骤执行结果 + */ + step: async function(stepName, stepFn) { + executionLock.logStep(devId, { + message: '⏳ ' + stepName + '...', + success: false + }); + + try { + var result = await stepFn(); + + executionLock.logStep(devId, { + message: '✅ ' + stepName, + success: true + }); + + return result; + } catch (e) { + executionLock.logStep(devId, { + message: '❌ ' + stepName + ': ' + e.message, + success: false + }); + throw e; + } + }, + + /** + * 保存检查点 + * @param {string} label - 检查点标签 + * @param {Object} data - 快照数据 + * @returns {string} 检查点ID + */ + checkpoint: async function(label, data) { + return await cpManager.save(label, data); + } + }; + + // 4. 执行主逻辑 + try { + var result = await fn(ctx); + + // 5. 成功:更新锁状态 + lock.state = 'completed'; + lock.result = result; + + // 延迟释放锁(给前端时间拉取最终状态) + setTimeout(function() { executionLock.releaseLock(devId); }, LOCK_RELEASE_DELAY_MS); + + return { + success: true, + executionId: executionId, + result: result, + reply: result && result.reply ? result.reply : '✅ 操作「' + operationName + '」执行完成。' + }; + + } catch (e) { + // 6. 失败:自动回滚 + lock.state = 'rolling_back'; + executionLock.logStep(devId, { + message: '🔄 执行失败,正在自动回滚...', + success: false + }); + + try { + await cpManager.rollbackTo('cp-1'); + executionLock.logStep(devId, { + message: '✅ 已回滚到执行前状态', + success: true + }); + } catch (rollbackError) { + executionLock.logStep(devId, { + message: '⚠️ 回滚异常:' + rollbackError.message + ',已通知管理员', + success: false + }); + } + + lock.state = 'failed'; + lock.error = { message: e.message }; + + setTimeout(function() { executionLock.releaseLock(devId); }, LOCK_RELEASE_DELAY_MS); + + return { + success: false, + executionId: executionId, + error: e.message, + reply: '❌ 操作「' + operationName + '」执行失败:' + e.message + + '\n\n系统已自动回滚到执行前状态,没有数据被修改。' + }; + } +}; + +module.exports = { AtomicExecutor: AtomicExecutor }; diff --git a/backend/api-server/services/autonomy-engine.js b/backend/api-server/services/autonomy-engine.js new file mode 100644 index 00000000..b79f0c7b --- /dev/null +++ b/backend/api-server/services/autonomy-engine.js @@ -0,0 +1,193 @@ +/** + * 系统自治运行引擎 · Phase 9 + * + * 系统自主监控运行状态,自动优化,异常时主动报告。 + * 遵循自治规则:不违反铁律、不绕过天眼审核、不绕过人类授权。 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var autonomyRules = require('../config/autonomy-rules.json'); + +var AUTONOMY_LOG_DIR = process.env.AUTONOMY_LOG_DIR || + path.join(__dirname, '../../logs/autonomy'); + +/** + * 写入自治日志(所有自主操作记录到审计日志) + */ +function writeAutonomyLog(entry) { + try { + fs.mkdirSync(AUTONOMY_LOG_DIR, { recursive: true }); + var today = new Date().toISOString().split('T')[0]; + var logFile = path.join(AUTONOMY_LOG_DIR, 'autonomy-' + today + '.jsonl'); + fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) { + if (err) console.error('自治日志写入失败:', err.message); + }); + } catch (e) { + console.error('自治日志写入失败:', e.message); + } +} + +/** + * 检查自治操作是否合规 + * @param {string} action - 操作描述 + * @returns {Object} { allowed: boolean, reason: string } + */ +function checkAutonomyCompliance(action) { + var forbidden = autonomyRules.autonomyRules.systemMustNot; + var actionLower = action.toLowerCase(); + + // 检查是否触及禁止项 + if (actionLower.includes('权限') && actionLower.includes('修改')) { + return { allowed: false, reason: '权限变更只有冰朔能触发' }; + } + if (actionLower.includes('绕过') && actionLower.includes('天眼')) { + return { allowed: false, reason: '不得绕过天眼审核' }; + } + if (actionLower.includes('绕过') && actionLower.includes('授权')) { + return { allowed: false, reason: '不得绕过人类授权步骤' }; + } + + return { allowed: true, reason: 'ok' }; +} + +/** + * 检测冰朔交互模式 + * @param {string} message - 用户消息 + * @returns {string} 'instruction' | 'natural' + */ +function detectInteractionMode(message) { + var instructionTriggers = autonomyRules.dualLineArchitecture.lineA.interactionModes.instructionMode.trigger; + var msgLower = message.toLowerCase(); + + for (var i = 0; i < instructionTriggers.length; i++) { + if (msgLower.includes(instructionTriggers[i].toLowerCase())) { + return 'instruction'; + } + } + return 'natural'; +} + +/** + * 记录自主优化操作 + * @param {string} action - 操作描述 + * @param {Object} details - 操作详情 + */ +function logAutonomousAction(action, details) { + var compliance = checkAutonomyCompliance(action); + if (!compliance.allowed) { + console.error('[AUTONOMY] 自治操作被阻止: ' + action + ' — ' + compliance.reason); + writeAutonomyLog({ + type: 'blocked', + action: action, + reason: compliance.reason, + timestamp: new Date().toISOString() + }); + return false; + } + + writeAutonomyLog({ + type: 'autonomous_action', + action: action, + details: details || {}, + timestamp: new Date().toISOString() + }); + return true; +} + +/** + * 获取部署流水线阶段定义 + */ +function getDeploymentStages() { + return autonomyRules.deploymentFlow.stages; +} + +/** + * 获取双线架构配置 + */ +function getDualLineConfig() { + return autonomyRules.dualLineArchitecture; +} + +/** + * 判断请求是否来自冰朔直通部署路径 (S5) + * @param {string} devId - 开发者编号 + * @param {string} instructionId - 指令编号 + * @param {string} signedBy - 签发者编号 + * @returns {boolean} + */ +function isDirectDeploySource(devId, instructionId, signedBy) { + // 冰朔本人 + if (devId === 'TCS-0002') return true; + + // ZY- 指令 + 可信签发者 + if (instructionId && /^ZY-/.test(instructionId)) { + var trustedSigners = ['AG-SY-01', 'TCS-0002', 'ICE-0002']; + if (signedBy && trustedSigners.indexOf(signedBy) !== -1) return true; + } + + return false; +} + +/** + * 判断部署是否为开发者自己频道内的变更 (S6) + * + * S6 规则: + * - 开发者在自己频道内拥有完全自主权 + * - 频道内变更由自己审核,不需要天眼/授权人 + * - 开发者自己确认 → 直接部署到正式站(自己频道区域) + * + * 天眼只管:跨频操作、系统级变更、频道生命周期、公共区域 + * + * @param {string} devId - 开发者编号 + * @param {string} module - 部署模块路径 + * @param {string} channel - 频道类型 + * @returns {{ selfChannel: boolean, reason: string }} + */ +function isChannelSelfDeploy(devId, module, channel) { + // 系统级/跨频频道 → 不是自治范围,走天眼审批 + if (channel === '系统' || channel === '跨频') { + return { selfChannel: false, reason: '系统级或跨频变更需天眼审核' }; + } + + // 获取开发者拥有的模块 + var permissions = require('../config/permissions'); + var devModules = permissions.DEV_MODULES[devId] || []; + + // 没有模块映射 → 不是自治范围 + if (devModules.length === 0) { + return { selfChannel: false, reason: '开发者无模块映射' }; + } + + // 通配符 → 管理员走 S5 直通,不走 S6 频道自治 + // 原因:管理员有全局权限,应使用 S5 的冰朔直通规则而非 S6 的频道自治 + if (devModules.indexOf('*') !== -1) { + return { selfChannel: false, reason: '管理员走 S5 直通' }; + } + + // 检查模块是否在开发者自己的频道内 + var modulePath = module.replace(/\/+$/, '') + '/'; + for (var i = 0; i < devModules.length; i++) { + var owned = devModules[i].replace(/\/+$/, '') + '/'; + if (modulePath.indexOf(owned) === 0 || owned.indexOf(modulePath) === 0 || module === devModules[i].replace(/\/+$/, '')) { + return { selfChannel: true, reason: '模块 ' + module + ' 属于 ' + devId + ' 的个人频道' }; + } + } + + return { selfChannel: false, reason: '模块 ' + module + ' 不在 ' + devId + ' 的频道内' }; +} + +module.exports = { + checkAutonomyCompliance: checkAutonomyCompliance, + detectInteractionMode: detectInteractionMode, + logAutonomousAction: logAutonomousAction, + getDeploymentStages: getDeploymentStages, + getDualLineConfig: getDualLineConfig, + writeAutonomyLog: writeAutonomyLog, + isDirectDeploySource: isDirectDeploySource, + isChannelSelfDeploy: isChannelSelfDeploy +}; diff --git a/backend/api-server/services/cache.js b/backend/api-server/services/cache.js new file mode 100644 index 00000000..302fe868 --- /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/checkpoint.js b/backend/api-server/services/checkpoint.js new file mode 100644 index 00000000..e2bdcd23 --- /dev/null +++ b/backend/api-server/services/checkpoint.js @@ -0,0 +1,172 @@ +/** + * 检查点系统 · L3 执行原子性保障 + * + * 在执行链路的每个关键节点保存快照。 + * 如果后续步骤失败,自动回滚到最近的成功检查点。 + * + * 检查点包含: + * - Notion 数据库的变更前快照 + * - GitHub 操作前的 commit SHA + * - 执行到哪一步 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var executionLock = require('../middleware/execution-lock'); + +/** + * 检查点管理器 + * @param {string} devId - 开发者编号 + * @param {string} executionId - 执行ID + */ +function CheckpointManager(devId, executionId) { + this.devId = devId; + this.executionId = executionId; + this.checkpoints = []; +} + +/** + * 在执行关键操作前保存检查点 + * @param {string} label - 检查点标签 + * @param {Object} snapshotData - 快照数据 { type, action, pageId, previousValues, ... } + * @returns {string} 检查点ID + */ +CheckpointManager.prototype.save = async function(label, snapshotData) { + var checkpoint = { + id: 'cp-' + (this.checkpoints.length + 1), + label: label, + data: snapshotData || {}, + timestamp: new Date().toISOString() + }; + + this.checkpoints.push(checkpoint); + executionLock.saveCheckpoint(this.devId, checkpoint); + + return checkpoint.id; +}; + +/** + * 回滚到指定检查点 + * 按逆序撤销检查点之后的所有操作 + * @param {string} checkpointId - 目标检查点 ID + */ +CheckpointManager.prototype.rollbackTo = async function(checkpointId) { + var targetIndex = -1; + for (var i = 0; i < this.checkpoints.length; i++) { + if (this.checkpoints[i].id === checkpointId) { + targetIndex = i; + break; + } + } + if (targetIndex === -1) { + throw new Error('检查点 ' + checkpointId + ' 不存在'); + } + + // 从最后一个检查点开始逆序回滚 + var toRollback = this.checkpoints.slice(targetIndex + 1).reverse(); + + for (var j = 0; j < toRollback.length; j++) { + var cp = toRollback[j]; + try { + await this.rollbackCheckpoint(cp); + } catch (e) { + console.error('[Checkpoint] 回滚 ' + cp.id + ' 失败:', e.message); + await this.escalateToAdmin(cp, e); + } + } + + // 截断检查点列表 + this.checkpoints = this.checkpoints.slice(0, targetIndex + 1); +}; + +/** + * 回滚单个检查点 + * @param {Object} checkpoint + */ +CheckpointManager.prototype.rollbackCheckpoint = async function(checkpoint) { + var data = checkpoint.data; + if (!data || !data.type) return; + + var notionService, githubService; + + switch (data.type) { + case 'notion_write': + notionService = require('./notion'); + if (data.action === 'create' && data.pageId && notionService.notion) { + // 新建的页面 → 归档 + await notionService.notion.pages.update({ + page_id: data.pageId, + archived: true + }); + } else if (data.action === 'update' && data.pageId && data.previousValues && notionService.notion) { + // 更新的页面 → 恢复原值 + await notionService.notion.pages.update({ + page_id: data.pageId, + properties: data.previousValues + }); + } + break; + + case 'github_workflow': + // Workflow 无法直接回滚,记录需要人工处理 + if (data.revertWorkflow) { + githubService = require('./github'); + await githubService.triggerWorkflow(data.revertWorkflow, { + original_run_id: String(data.runId || ''), + reason: 'auto_rollback' + }); + } + break; + + case 'github_file': + githubService = require('./github'); + if (data.previousSha) { + // 文件存在之前的版本 → 恢复 + // 注意:这需要 GitHub API 的 update file 能力 + console.warn('[Checkpoint] GitHub 文件回滚需要人工介入: ' + data.path); + } + break; + + case 'initial': + // 初始检查点,无需回滚操作 + break; + + default: + console.warn('[Checkpoint] 未知检查点类型: ' + data.type); + } +}; + +/** + * 回滚失败时上报管理员 + * @param {Object} checkpoint + * @param {Error} error + */ +CheckpointManager.prototype.escalateToAdmin = async function(checkpoint, error) { + try { + var notionService = require('./notion'); + var dbConfig = require('../config/databases'); + + if (dbConfig.maintenanceLog && notionService.notion) { + await notionService.writeToDB(dbConfig.maintenanceLog, { + '标题': { + title: [{ text: { content: '[紧急] 自动回滚失败 · ' + this.executionId } }] + }, + '类型': { select: { name: '系统异常' } }, + '操作者': { + rich_text: [{ + text: { + content: '检查点 ' + checkpoint.id + ' (' + checkpoint.label + + ') 回滚失败: ' + error.message + '。需要系统管理员人工介入。' + } + }] + } + }); + } + } catch (e) { + console.error('[Checkpoint] 上报管理员失败:', e.message); + } +}; + +module.exports = { CheckpointManager: CheckpointManager }; diff --git a/backend/api-server/services/execution-watchdog.js b/backend/api-server/services/execution-watchdog.js new file mode 100644 index 00000000..4327bc1e --- /dev/null +++ b/backend/api-server/services/execution-watchdog.js @@ -0,0 +1,100 @@ +/** + * 执行看门狗 · 超时保护 + 死锁防护 + * + * 定期检查执行锁是否超时。 + * 如果一个操作执行超过 MAX_EXECUTION_TIME,标记为超时并释放锁。 + * 这是防止死锁的兜底机制。 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var executionLock = require('../middleware/execution-lock'); + +// 最大执行时间:15 分钟 +var MAX_EXECUTION_TIME = 15 * 60 * 1000; + +// 警告时间:10 分钟 +var WARN_TIME = 10 * 60 * 1000; + +// 检查间隔:30 秒 +var CHECK_INTERVAL = 30 * 1000; + +var watchdogTimer = null; + +/** + * 启动看门狗 + * 定期扫描活跃锁表,检测超时操作 + */ +function startWatchdog() { + if (watchdogTimer) return; + + watchdogTimer = setInterval(function() { + var activeLocks = executionLock.getActiveLocks(); + var now = Date.now(); + + for (var entry of activeLocks) { + var devId = entry[0]; + var lock = entry[1]; + + if (lock.state !== 'running') continue; + + var elapsed = now - lock.startTime; + + // 超时:强制释放 + if (elapsed > MAX_EXECUTION_TIME) { + console.error( + '[WATCHDOG] 执行超时: ' + lock.executionId + + ' (' + lock.operation + ') - ' + Math.round(elapsed / 1000) + 's' + ); + + lock.state = 'timeout'; + lock.error = { + message: '执行超时(超过 ' + Math.round(MAX_EXECUTION_TIME / 60000) + ' 分钟)' + }; + + executionLock.logStep(devId, { + message: '⏰ 执行超时,看门狗已强制释放锁', + success: false + }); + + // 延迟释放(给前端时间获取超时状态) + (function(id) { + setTimeout(function() { executionLock.releaseLock(id); }, 30000); + })(devId); + } + // 警告 + else if (elapsed > WARN_TIME && !lock.warned) { + console.warn( + '[WATCHDOG] 执行时间较长: ' + lock.executionId + + ' (' + lock.operation + ') - ' + Math.round(elapsed / 1000) + 's' + ); + lock.warned = true; + } + } + }, CHECK_INTERVAL); + + // 不阻止 Node.js 进程退出 + if (watchdogTimer.unref) { + watchdogTimer.unref(); + } + + console.log('[WATCHDOG] 执行看门狗已启动 · 检查间隔 ' + (CHECK_INTERVAL / 1000) + 's · 超时阈值 ' + (MAX_EXECUTION_TIME / 60000) + 'min'); +} + +/** + * 停止看门狗 + */ +function stopWatchdog() { + if (watchdogTimer) { + clearInterval(watchdogTimer); + watchdogTimer = null; + } +} + +module.exports = { + startWatchdog: startWatchdog, + stopWatchdog: stopWatchdog, + MAX_EXECUTION_TIME: MAX_EXECUTION_TIME +}; diff --git a/backend/api-server/services/github.js b/backend/api-server/services/github.js new file mode 100644 index 00000000..3b4e4aee --- /dev/null +++ b/backend/api-server/services/github.js @@ -0,0 +1,221 @@ +/** + * 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; +} + +// ====== 触发 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, + triggerWorkflow: triggerWorkflow, + createFile: createFile +}; 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/css/approval.css b/docs/css/approval.css new file mode 100644 index 00000000..c4c422bd --- /dev/null +++ b/docs/css/approval.css @@ -0,0 +1,165 @@ +/** + * 部署授权样式 · Phase 8 + * 版权:国作登字-2026-A-00037559 + */ + +/* 授权提醒徽章 */ +.approval-badge { + position: fixed; + top: 16px; + right: 16px; + background: #ff6b35; + color: #fff; + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + cursor: pointer; + z-index: 9999; + box-shadow: 0 2px 12px rgba(255, 107, 53, 0.4); + animation: badge-pulse 2s infinite; +} + +.approval-badge:hover { + background: #e65a2a; +} + +@keyframes badge-pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +/* 授权容器 */ +.approval-container { + position: fixed; + top: 60px; + right: 16px; + z-index: 9998; + display: none; +} + +/* 授权面板 */ +.approval-panel { + background: #1e1e2e; + border: 1px solid #333; + border-radius: 12px; + width: 380px; + max-height: 70vh; + overflow-y: auto; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.approval-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 16px; + border-bottom: 1px solid #333; + font-size: 15px; + font-weight: 600; + color: #e0e0e0; +} + +.approval-close-btn { + background: none; + border: none; + color: #888; + font-size: 20px; + cursor: pointer; + padding: 0 4px; +} + +.approval-close-btn:hover { + color: #fff; +} + +.approval-panel-body { + padding: 8px; +} + +/* 授权条目 */ +.approval-item { + background: #252535; + border-radius: 8px; + padding: 12px; + margin-bottom: 8px; +} + +.approval-item-info { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin-bottom: 10px; +} + +.approval-item-info strong { + color: #e0e0e0; + font-size: 14px; +} + +.approval-channel { + background: #3a3a5a; + color: #aaa; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; +} + +.approval-time { + color: #666; + font-size: 12px; +} + +/* 授权按钮 */ +.approval-item-actions { + display: flex; + gap: 8px; +} + +.approval-btn { + flex: 1; + padding: 8px 12px; + border: none; + border-radius: 6px; + font-size: 13px; + cursor: pointer; + transition: background 0.2s; +} + +.approval-btn.approve { + background: rgba(15, 123, 108, 0.2); + color: #0F7B6C; +} + +.approval-btn.approve:hover { + background: rgba(15, 123, 108, 0.4); +} + +.approval-btn.reject { + background: rgba(212, 76, 71, 0.2); + color: #D44C47; +} + +.approval-btn.reject:hover { + background: rgba(212, 76, 71, 0.4); +} + +/* 通知消息 */ +.approval-notification { + position: fixed; + bottom: 24px; + right: 24px; + background: #1e1e2e; + border: 1px solid #333; + color: #e0e0e0; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + z-index: 10000; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + transition: opacity 0.5s; +} + +.approval-notification.fade-out { + opacity: 0; +} diff --git a/docs/css/execution-guard.css b/docs/css/execution-guard.css new file mode 100644 index 00000000..27ed1681 --- /dev/null +++ b/docs/css/execution-guard.css @@ -0,0 +1,125 @@ +/** + * 执行保护样式 · L1 前端隔离 + * + * 注意:没有 .cancel-button 类。这不是遗漏,是设计。 + * 版权:国作登字-2026-A-00037559 + */ + +/* 执行中状态 · 输入框锁定 */ +.chat-input.execution-locked, +#chat-input.execution-locked { + opacity: 0.5; + pointer-events: none; + background: #2a2a3a; + border-color: #444; +} + +.chat-input.execution-locked::placeholder, +#chat-input.execution-locked::placeholder { + color: #888; +} + +/* 执行状态条 */ +.execution-status-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 12px 16px; + border-radius: 8px; + margin: 8px 0; + font-size: 14px; + animation: pulse-gentle 2s infinite; +} + +.execution-status-bar .exec-icon { + flex-shrink: 0; + font-size: 1.1em; +} + +.execution-status-bar .exec-text { + flex: 1; +} + +.execution-status-bar.blue { + background: rgba(35, 131, 226, 0.08); + color: #64c8ff; +} + +.execution-status-bar.green { + background: rgba(15, 123, 108, 0.08); + color: #0F7B6C; + animation: none; +} + +.execution-status-bar.red { + background: rgba(212, 76, 71, 0.08); + color: #D44C47; + animation: none; +} + +/* 执行进度条 */ +.execution-progress { + width: 100%; + height: 3px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; + margin-top: 4px; + overflow: hidden; +} + +.execution-progress-fill { + height: 100%; + background: #64c8ff; + border-radius: 2px; + transition: width 0.5s ease; +} + +.execution-status-bar.green .execution-progress-fill { + background: #0F7B6C; +} + +.execution-status-bar.red .execution-progress-fill { + background: #D44C47; +} + +/* 执行日志容器 */ +.execution-log-container { + max-height: 200px; + overflow-y: auto; + padding: 4px 8px; + margin: 4px 0 12px; + font-size: 13px; +} + +/* 执行日志条目 */ +.execution-log-entry { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 3px 0; + color: #b0b0c0; +} + +.execution-log-entry .log-icon { + flex-shrink: 0; + font-size: 0.9em; +} + +.execution-log-entry .log-msg { + flex: 1; +} + +.execution-log-entry .log-time { + flex-shrink: 0; + color: #666; + font-size: 12px; +} + +/* 温和脉冲动画 */ +@keyframes pulse-gentle { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +/* 注意:没有 .cancel-button 类。这不是遗漏。 */ 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/index.html b/docs/index.html index 579e2d36..98a9aa98 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1757,18 +1757,52 @@ 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); + 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 缓存) 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); + 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 缓存) + 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 +1833,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 +2156,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 +2209,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/docs/js/approval-ui.js b/docs/js/approval-ui.js new file mode 100644 index 00000000..18d7aa9b --- /dev/null +++ b/docs/js/approval-ui.js @@ -0,0 +1,230 @@ +/** + * 部署授权 UI · Phase 8 + * + * 授权人在交互页面收到授权请求推送时,展示授权弹窗。 + * 授权人必须主动点击确认,不回应视为不通过。 + * 系统绝不自动跳过授权步骤。 + * + * 版权:国作登字-2026-A-00037559 + */ + +/* global HOLOLAKE_ENV */ + +(function(window) { + 'use strict'; + + var API_BASE = (typeof HOLOLAKE_ENV !== 'undefined' && HOLOLAKE_ENV === 'production') + ? 'https://guanghulab.com/api' : ''; + + /** + * ApprovalUI 构造函数 + */ + function ApprovalUI() { + this._pollTimer = null; + this._container = null; + } + + /** + * 初始化:开始轮询待授权请求 + * @param {string} devId - 当前登录的开发者编号 + * @param {string} token - 认证 token + */ + ApprovalUI.prototype.init = function(devId, token) { + this.devId = devId; + this.token = token; + this._ensureContainer(); + this._startPolling(); + }; + + /** + * 确保授权容器存在 + */ + ApprovalUI.prototype._ensureContainer = function() { + if (this._container) return; + this._container = document.createElement('div'); + this._container.id = 'approval-container'; + this._container.className = 'approval-container'; + document.body.appendChild(this._container); + }; + + /** + * 开始轮询待授权请求(每 30 秒) + */ + ApprovalUI.prototype._startPolling = function() { + var self = this; + if (!API_BASE) return; + + var poll = function() { + self._fetchPendingApprovals(); + }; + + poll(); + this._pollTimer = setInterval(poll, 30000); + }; + + /** + * 停止轮询 + */ + ApprovalUI.prototype.destroy = function() { + if (this._pollTimer) { + clearInterval(this._pollTimer); + this._pollTimer = null; + } + }; + + /** + * 获取待处理的授权请求 + */ + ApprovalUI.prototype._fetchPendingApprovals = function() { + var self = this; + if (!API_BASE) return; + + fetch(API_BASE + '/approval', { + headers: { + 'x-dev-id': this.devId, + 'Authorization': 'Bearer ' + this.token + } + }) + .then(function(res) { return res.json(); }) + .then(function(data) { + if (data.success && data.pending && data.pending.length > 0) { + self._showApprovalBadge(data.pending.length); + self._renderPendingList(data.pending); + } else { + self._hideApprovalBadge(); + } + }) + .catch(function() { /* 静默失败 */ }); + }; + + /** + * 显示授权提醒徽章 + */ + ApprovalUI.prototype._showApprovalBadge = function(count) { + var badge = document.getElementById('approval-badge'); + if (!badge) { + badge = document.createElement('div'); + badge.id = 'approval-badge'; + badge.className = 'approval-badge'; + document.body.appendChild(badge); + } + badge.textContent = '🔔 ' + count + ' 个待授权'; + badge.style.display = 'block'; + var self = this; + badge.onclick = function() { self._togglePanel(); }; + }; + + /** + * 隐藏授权徽章 + */ + ApprovalUI.prototype._hideApprovalBadge = function() { + var badge = document.getElementById('approval-badge'); + if (badge) badge.style.display = 'none'; + if (this._container) this._container.style.display = 'none'; + }; + + /** + * 切换授权面板显示 + */ + ApprovalUI.prototype._togglePanel = function() { + if (this._container) { + this._container.style.display = + this._container.style.display === 'none' ? 'block' : 'none'; + } + }; + + /** + * 渲染待授权列表 + */ + ApprovalUI.prototype._renderPendingList = function(pending) { + if (!this._container) return; + + var html = '
' + + '
' + + '📋 待授权请求' + + '' + + '
' + + '
'; + + for (var i = 0; i < pending.length; i++) { + var item = pending[i]; + html += '
' + + '
' + + '' + item.module + '' + + '' + item.channel + '' + + '' + new Date(item.createdAt).toLocaleString('zh-CN') + '' + + '
' + + '
' + + '' + + '' + + '
'; + } + + html += '
'; + this._container.innerHTML = html; + this._container.style.display = 'block'; + + // 绑定事件 + var self = this; + var closeBtn = document.getElementById('approval-close'); + if (closeBtn) { + closeBtn.onclick = function() { self._container.style.display = 'none'; }; + } + + var buttons = this._container.querySelectorAll('.approval-btn'); + for (var j = 0; j < buttons.length; j++) { + buttons[j].addEventListener('click', function() { + var approvalId = this.getAttribute('data-id'); + var action = this.getAttribute('data-action'); + self._submitDecision(approvalId, action); + }); + } + }; + + /** + * 提交授权决定 + */ + ApprovalUI.prototype._submitDecision = function(approvalId, decision) { + var self = this; + if (!API_BASE) return; + + fetch(API_BASE + '/approval/' + approvalId + '/decide', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-dev-id': this.devId, + 'Authorization': 'Bearer ' + this.token + }, + body: JSON.stringify({ decision: decision }) + }) + .then(function(res) { return res.json(); }) + .then(function(data) { + if (data.reply) { + self._showNotification(data.reply); + } + // 刷新列表 + self._fetchPendingApprovals(); + }) + .catch(function(err) { + self._showNotification('❌ 操作失败:' + err.message); + }); + }; + + /** + * 显示通知消息 + */ + ApprovalUI.prototype._showNotification = function(message) { + var notif = document.createElement('div'); + notif.className = 'approval-notification'; + notif.textContent = message; + document.body.appendChild(notif); + setTimeout(function() { + notif.classList.add('fade-out'); + setTimeout(function() { notif.remove(); }, 500); + }, 4000); + }; + + // 导出到全局 + window.ApprovalUI = ApprovalUI; + +})(window); diff --git a/docs/js/execution-guard.js b/docs/js/execution-guard.js new file mode 100644 index 00000000..23a0fbaa --- /dev/null +++ b/docs/js/execution-guard.js @@ -0,0 +1,247 @@ +/** + * 执行保护 · 前端隔离层 (L1) + * + * 当开发者发出一条会触发系统操作的指令后: + * 1. 输入框立即禁用(不能发新指令) + * 2. 取消/撤回按钮不存在(从未渲染过,不是隐藏) + * 3. 界面进入「执行回放」模式 + * 4. 执行完成后才解锁输入框 + * + * 版权:国作登字-2026-A-00037559 + */ + +/* global HOLOLAKE_ENV */ + +(function(window) { + 'use strict'; + + var API_BASE = (typeof HOLOLAKE_ENV !== 'undefined' && HOLOLAKE_ENV === 'production') + ? 'https://guanghulab.com/api' : ''; + + /** + * ExecutionGuard 构造函数 + * @param {Object} chatInterface - 聊天界面接口对象 + */ + function ExecutionGuard(chatInterface) { + this.chat = chatInterface || {}; + this.locked = false; + this.executionId = null; + this._pollTimer = null; + this._logIndex = 0; + } + + /** + * 进入执行保护状态 + */ + ExecutionGuard.prototype.lockExecution = function(executionId) { + this.locked = true; + this.executionId = executionId; + this._logIndex = 0; + + // 1. 禁用输入框 + var input = this.chat.inputField || document.getElementById('chat-input'); + var sendBtn = this.chat.sendButton || document.getElementById('send-btn'); + + if (input) { + input.disabled = true; + input.placeholder = '⏳ 系统执行中…请等待完成'; + input.classList.add('execution-locked'); + } + if (sendBtn) { + sendBtn.disabled = true; + } + + // 2. 显示执行状态条 + this._showStatusBar('🔄', '指令已接收,系统正在执行…', 'blue'); + + // 3. 开始轮询执行状态 + this._pollExecutionStatus(executionId); + }; + + /** + * 轮询执行状态 + */ + ExecutionGuard.prototype._pollExecutionStatus = function(executionId) { + var self = this; + + var poll = function() { + if (!self.locked) return; + if (!API_BASE) { + // 无后端时模拟完成 + setTimeout(function() { self.unlockExecution({ reply: '(本地模式:执行模拟完成)' }); }, 2000); + return; + } + + var ctrl = new AbortController(); + var tid = setTimeout(function() { ctrl.abort(); }, 10000); + + fetch(API_BASE + '/execution/' + executionId + '/status?after=' + self._logIndex, { + signal: ctrl.signal + }) + .then(function(res) { clearTimeout(tid); return res.json(); }) + .then(function(status) { + // 更新进度 + self._updateProgressBar(status.progress || 0); + + // 展示新日志 + if (status.newLogs && status.newLogs.length > 0) { + for (var i = 0; i < status.newLogs.length; i++) { + self._appendLogEntry(status.newLogs[i]); + } + self._logIndex += status.newLogs.length; + } + + if (status.state === 'completed') { + self.unlockExecution(status.result || {}); + } else if (status.state === 'failed' || status.state === 'timeout') { + self.handleExecutionFailure(status.error || { message: '执行失败' }); + } else { + self._pollTimer = setTimeout(poll, 2000); + } + }) + .catch(function() { + clearTimeout(tid); + // 网络错误不中断执行,继续轮询 + self._pollTimer = setTimeout(poll, 5000); + }); + }; + + poll(); + }; + + /** + * 执行完成,解锁界面 + */ + ExecutionGuard.prototype.unlockExecution = function(result) { + this.locked = false; + this.executionId = null; + if (this._pollTimer) { clearTimeout(this._pollTimer); this._pollTimer = null; } + + // 恢复输入框 + var input = this.chat.inputField || document.getElementById('chat-input'); + var sendBtn = this.chat.sendButton || document.getElementById('send-btn'); + + if (input) { + input.disabled = false; + input.placeholder = '和铸渊说话…'; + input.classList.remove('execution-locked'); + } + if (sendBtn) { + sendBtn.disabled = false; + } + + // 更新状态条 + this._showStatusBar('✅', '执行完成', 'green'); + var self = this; + setTimeout(function() { self._hideStatusBar(); }, 5000); + + // 展示结果 + if (result && result.reply && this.chat.appendMessage) { + this.chat.appendMessage({ role: 'assistant', content: result.reply }); + } + }; + + /** + * 执行失败处理 + */ + ExecutionGuard.prototype.handleExecutionFailure = function(error) { + this.locked = false; + this.executionId = null; + if (this._pollTimer) { clearTimeout(this._pollTimer); this._pollTimer = null; } + + var input = this.chat.inputField || document.getElementById('chat-input'); + var sendBtn = this.chat.sendButton || document.getElementById('send-btn'); + + if (input) { + input.disabled = false; + input.placeholder = '和铸渊说话…'; + input.classList.remove('execution-locked'); + } + if (sendBtn) { + sendBtn.disabled = false; + } + + this._showStatusBar('❌', '执行失败 · 系统已自动回滚到执行前状态', 'red'); + var self = this; + setTimeout(function() { self._hideStatusBar(); }, 10000); + + var msg = error && error.message ? error.message : '未知错误'; + if (this.chat.appendMessage) { + this.chat.appendMessage({ + role: 'assistant', + content: '❌ 操作执行失败:' + msg + + '\n\n系统已自动回滚到执行前的状态。没有任何数据被修改。\n' + + '你可以重新描述你想做的事,或者说「发生了什么」来查看详细日志。' + }); + } + }; + + /** + * 拦截浏览器关闭/刷新 + */ + ExecutionGuard.prototype.setupBrowserGuard = function() { + var self = this; + window.addEventListener('beforeunload', function(e) { + if (self.locked) { + e.preventDefault(); + e.returnValue = '⚠️ 系统正在执行操作,关闭页面不会中断执行,但你将无法看到执行结果。'; + return e.returnValue; + } + }); + }; + + // ====== 内部 UI 辅助方法 ====== + + ExecutionGuard.prototype._showStatusBar = function(icon, text, color) { + var bar = document.getElementById('execution-status-bar'); + if (!bar) { + bar = document.createElement('div'); + bar.id = 'execution-status-bar'; + bar.className = 'execution-status-bar'; + var chatArea = document.querySelector('.chat-messages') || document.querySelector('.chat-container') || document.body; + chatArea.appendChild(bar); + } + bar.className = 'execution-status-bar ' + (color || 'blue'); + bar.innerHTML = '' + icon + ' ' + text + '' + + '
'; + bar.style.display = 'flex'; + // 注意:没有取消按钮。这不是遗漏,是设计。 + }; + + ExecutionGuard.prototype._hideStatusBar = function() { + var bar = document.getElementById('execution-status-bar'); + if (bar) bar.style.display = 'none'; + }; + + ExecutionGuard.prototype._updateProgressBar = function(percent) { + var fill = document.getElementById('exec-progress-fill'); + if (fill) fill.style.width = Math.max(5, Math.min(100, percent)) + '%'; + }; + + ExecutionGuard.prototype._appendLogEntry = function(log) { + var container = document.getElementById('execution-log-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'execution-log-container'; + container.className = 'execution-log-container'; + var bar = document.getElementById('execution-status-bar'); + if (bar && bar.parentNode) { + bar.parentNode.insertBefore(container, bar.nextSibling); + } else { + (document.querySelector('.chat-messages') || document.body).appendChild(container); + } + } + var entry = document.createElement('div'); + entry.className = 'execution-log-entry'; + var icon = log.success ? '✅' : '⏳'; + var time = log.timestamp ? new Date(log.timestamp).toLocaleTimeString('zh-CN') : ''; + entry.innerHTML = '' + icon + '' + + '' + (log.message || '') + '' + + '' + time + ''; + container.appendChild(entry); + }; + + // 导出到全局 + window.ExecutionGuard = ExecutionGuard; + +})(window); 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..9804098a --- /dev/null +++ b/docs/js/onboarding.js @@ -0,0 +1,184 @@ +/** + * 前端认知引导系统 · 新开发者引导流程 + * + * 新开发者首次登录后自动进入 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(); + // Show onboarding if not completed (handles both new and resumed sessions) + return !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 || '开发者') + '

' + + '

我是铸渊,你的代码守护人格体。

' + + '

在这里,你不需要点按钮或填表单。
直接对我说话就好。

' + + '
' + + '

比如你可以说:

' + + '' + + '
' + + '

🏖️ 现在你在预览环境,所有操作都不会影响正式站。放心练习。

' + + '' + + '
'; + + document.body.appendChild(onboardingOverlay); + + document.getElementById('onboarding-start-btn').addEventListener('click', function() { + hideOnboarding(); + if (onStart) onStart(); + }); + } + + /** + * 显示引导轮次提示(在聊天界面中展示) + */ + function showRoundHint(round, prompt, hint) { + var container = document.getElementById('onboarding-hint'); + if (!container) { + container = document.createElement('div'); + container.id = 'onboarding-hint'; + container.className = 'onboarding-hint'; + var chatArea = document.querySelector('.chat-container') || document.body; + chatArea.insertBefore(container, chatArea.firstChild); + } + + container.innerHTML = + '
' + + '
引导 ' + round + '/5
' + + '
' + prompt + '
' + + (hint ? '
' + hint + '
' : '') + + '
'; + container.style.display = 'block'; + } + + /** + * 显示引导完成消息 + */ + function showComplete(message) { + hideOnboarding(); + + onboardingOverlay = document.createElement('div'); + onboardingOverlay.className = 'onboarding-overlay'; + onboardingOverlay.innerHTML = + '
' + + '
🎉
' + + '

引导完成!

' + + '
' + (message || '') + '
' + + '' + + '
'; + + document.body.appendChild(onboardingOverlay); + + document.getElementById('onboarding-close-btn').addEventListener('click', function() { + hideOnboarding(); + }); + } + + /** + * 隐藏引导覆盖层 + */ + function hideOnboarding() { + if (onboardingOverlay) { + onboardingOverlay.remove(); + onboardingOverlay = null; + } + var hint = document.getElementById('onboarding-hint'); + if (hint) hint.style.display = 'none'; + } + + /** + * 推进引导流程(调用后端) + */ + async function advanceOnboarding(devId, token) { + if (!API_BASE) return null; + try { + var res = await fetch(API_BASE + '/onboarding/start', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-dev-id': devId, + 'Authorization': 'Bearer ' + token + } + }); + if (res.ok) return await res.json(); + } catch (e) { + console.debug('[Onboarding] advance failed:', e.message); + } + return null; + } + + /** + * 完成引导(调用后端,触发权限升级) + */ + async function completeOnboarding(devId, token) { + if (!API_BASE) return null; + try { + var res = await fetch(API_BASE + '/onboarding/complete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-dev-id': devId, + 'Authorization': 'Bearer ' + token + } + }); + if (res.ok) return await res.json(); + } catch (e) { + console.debug('[Onboarding] complete failed:', e.message); + } + return null; + } + + // 导出到全局 + window.Onboarding = { + checkOnboarding: checkOnboarding, + showWelcome: showWelcome, + showRoundHint: showRoundHint, + showComplete: showComplete, + hideOnboarding: hideOnboarding, + advanceOnboarding: advanceOnboarding, + completeOnboarding: completeOnboarding + }; + +})(window); 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); +});