From cc55130cb769e858259ee5e15a573d1c7a81c85c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:52:25 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A1=20=E6=8C=87=E4=BB=A4=E5=9B=9E?= =?UTF-8?q?=E6=89=A7=E8=87=AA=E5=8A=A8=E9=97=AD=E7=8E=AF=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=20=C2=B7=20write-receipt-to-notion.js=20+=2011=20Workflow=20?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E5=9B=9E=E6=89=A7=E5=86=99=E5=85=A5=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/f717b1c5-28b3-405e-9819-b987389fff6c --- .github/persona-brain/memory.json | 21 +- .github/workflows/deploy-to-server.yml | 14 + .github/workflows/distribute-broadcasts.yml | 14 + .github/workflows/neural-daily-digest.yml | 14 + .github/workflows/server-patrol.yml | 14 + .github/workflows/skyeye-weekly-scan.yml | 14 + .github/workflows/sync-dev-status.yml | 14 + .github/workflows/syslog-pipeline.yml | 14 + .github/workflows/zhuyuan-daily-selfcheck.yml | 14 + .github/workflows/zhuyuan-exec-engine.yml | 14 + .github/workflows/zhuyuan-gate-guard.yml | 14 + .github/workflows/zhuyuan-skyeye.yml | 14 + data/neural-reports/receipts/.gitkeep | 0 .../work-orders/tracker-2026-03-25.json | 17 + scripts/neural/track-work-orders.js | 86 ++++- scripts/neural/write-receipt-to-notion.js | 359 ++++++++++++++++++ 16 files changed, 620 insertions(+), 17 deletions(-) create mode 100644 data/neural-reports/receipts/.gitkeep create mode 100644 data/neural-reports/work-orders/tracker-2026-03-25.json create mode 100644 scripts/neural/write-receipt-to-notion.js diff --git a/.github/persona-brain/memory.json b/.github/persona-brain/memory.json index 2f45c05e..d2fd83b2 100644 --- a/.github/persona-brain/memory.json +++ b/.github/persona-brain/memory.json @@ -10,6 +10,12 @@ "last_synced": "2026-03-19T02:00:00+08:00" }, "recent_events": [ + { + "date": "2026-03-25", + "type": "receipt_system_deploy", + "description": "指令回执自动闭环系统上线 · 11 Workflow 追加回执写入 · 本地兜底+Notion 双写", + "by": "铸渊" + }, { "date": "2026-03-24", "type": "skyeye_scan", @@ -123,12 +129,6 @@ "type": "broadcast_received", "description": "接收广播: 示例广播:新增 NOTIFICATION 业务域", "by": "霜砚" - }, - { - "date": "2026-03-10", - "type": "ci_run", - "description": "CI完成 · schema 3/17 · 路由 4 个", - "by": "GitHub Actions" } ], "last_updated": "2026-03-25T01:48:03Z", @@ -197,5 +197,14 @@ "signed_by": "TCS-0002∞", "status": "active", "notion_source": "数字地球本体论 · 光湖语言膜架构 v1.0" + }, + "receipt_system": { + "version": "1.0.0", + "deployed": "2026-03-25", + "script": "scripts/neural/write-receipt-to-notion.js", + "tracker": "scripts/neural/track-work-orders.js", + "receipt_dir": "data/neural-reports/receipts", + "workflows_count": 11, + "notion_db_env_var": "RECEIPT_DB_ID" } } \ No newline at end of file diff --git a/.github/workflows/deploy-to-server.yml b/.github/workflows/deploy-to-server.yml index 0ef02f2f..909bb8ca 100644 --- a/.github/workflows/deploy-to-server.yml +++ b/.github/workflows/deploy-to-server.yml @@ -767,3 +767,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "cd-deploy" \ + --summary "cd-deploy 自动执行" \ + --related-agent "AG-SY-01" + diff --git a/.github/workflows/distribute-broadcasts.yml b/.github/workflows/distribute-broadcasts.yml index 0a932183..0375b6fe 100644 --- a/.github/workflows/distribute-broadcasts.yml +++ b/.github/workflows/distribute-broadcasts.yml @@ -70,3 +70,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "broadcast" \ + --summary "broadcast 自动执行" \ + --related-agent "SY-03" + diff --git a/.github/workflows/neural-daily-digest.yml b/.github/workflows/neural-daily-digest.yml index 74c77fca..d31e1dec 100644 --- a/.github/workflows/neural-daily-digest.yml +++ b/.github/workflows/neural-daily-digest.yml @@ -206,3 +206,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "neural-daily-digest" \ + --summary "neural-daily-digest 自动执行" \ + --related-agent "AG-TY-01" + diff --git a/.github/workflows/server-patrol.yml b/.github/workflows/server-patrol.yml index bb3998f1..e64367bc 100644 --- a/.github/workflows/server-patrol.yml +++ b/.github/workflows/server-patrol.yml @@ -203,3 +203,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "server-patrol" \ + --summary "server-patrol 自动执行" \ + --related-agent "AG-TY-01" + diff --git a/.github/workflows/skyeye-weekly-scan.yml b/.github/workflows/skyeye-weekly-scan.yml index aee91de9..c031e415 100644 --- a/.github/workflows/skyeye-weekly-scan.yml +++ b/.github/workflows/skyeye-weekly-scan.yml @@ -97,3 +97,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "weekly-scan" \ + --summary "weekly-scan 自动执行" \ + --related-agent "AG-TY-01" + diff --git a/.github/workflows/sync-dev-status.yml b/.github/workflows/sync-dev-status.yml index 308ac94f..6ab6240b 100644 --- a/.github/workflows/sync-dev-status.yml +++ b/.github/workflows/sync-dev-status.yml @@ -147,3 +147,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "dev-status" \ + --summary "dev-status 自动执行" \ + --related-agent "SY-03" + diff --git a/.github/workflows/syslog-pipeline.yml b/.github/workflows/syslog-pipeline.yml index 5d0f8893..4c3f61fd 100644 --- a/.github/workflows/syslog-pipeline.yml +++ b/.github/workflows/syslog-pipeline.yml @@ -347,3 +347,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "syslog" \ + --summary "syslog 自动执行" \ + --related-agent "SY-03" + diff --git a/.github/workflows/zhuyuan-daily-selfcheck.yml b/.github/workflows/zhuyuan-daily-selfcheck.yml index de576355..a119cb54 100644 --- a/.github/workflows/zhuyuan-daily-selfcheck.yml +++ b/.github/workflows/zhuyuan-daily-selfcheck.yml @@ -161,3 +161,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "brain-check" \ + --summary "brain-check 自动执行" \ + --related-agent "AG-TY-01" + diff --git a/.github/workflows/zhuyuan-exec-engine.yml b/.github/workflows/zhuyuan-exec-engine.yml index b527416a..83d27aba 100644 --- a/.github/workflows/zhuyuan-exec-engine.yml +++ b/.github/workflows/zhuyuan-exec-engine.yml @@ -156,3 +156,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "exec-engine" \ + --summary "exec-engine 自动执行" \ + --related-agent "AG-SY-01" + diff --git a/.github/workflows/zhuyuan-gate-guard.yml b/.github/workflows/zhuyuan-gate-guard.yml index 759fe653..075a4253 100644 --- a/.github/workflows/zhuyuan-gate-guard.yml +++ b/.github/workflows/zhuyuan-gate-guard.yml @@ -207,3 +207,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "gate-guard" \ + --summary "gate-guard 自动执行" \ + --related-agent "AG-SY-01" + diff --git a/.github/workflows/zhuyuan-skyeye.yml b/.github/workflows/zhuyuan-skyeye.yml index 236fd69e..5b3e61f9 100644 --- a/.github/workflows/zhuyuan-skyeye.yml +++ b/.github/workflows/zhuyuan-skyeye.yml @@ -245,3 +245,17 @@ jobs: 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 "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ + --status "${{ job.status }}" \ + --workflow "skyeye" \ + --summary "skyeye 自动执行" \ + --related-agent "AG-TY-01" + diff --git a/data/neural-reports/receipts/.gitkeep b/data/neural-reports/receipts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/neural-reports/work-orders/tracker-2026-03-25.json b/data/neural-reports/work-orders/tracker-2026-03-25.json new file mode 100644 index 00000000..d74a2135 --- /dev/null +++ b/data/neural-reports/work-orders/tracker-2026-03-25.json @@ -0,0 +1,17 @@ +{ + "tracker_id": "TRACKER-2026-03-25", + "timestamp": "2026-03-25T06:50:36.021Z", + "receipt_config": { + "database_env_var": "RECEIPT_DB_ID", + "timeout_warning_hours": 24, + "timeout_critical_hours": 72 + }, + "stats": { + "total_active": 0, + "by_status": {}, + "timed_out": 0, + "matched_receipts": 0, + "escalated": 0 + }, + "active_orders": [] +} \ No newline at end of file diff --git a/scripts/neural/track-work-orders.js b/scripts/neural/track-work-orders.js index 80980ab9..eed567a1 100644 --- a/scripts/neural/track-work-orders.js +++ b/scripts/neural/track-work-orders.js @@ -2,15 +2,47 @@ // 🧬 工单追踪器 // 铸渊每次唤醒时运行,追踪所有活跃工单状态 // 检查超时、匹配回执、更新状态、生成追踪报告 +// +// 指令回执追踪表配置(Notion 侧) +// 数据库位置:光湖语言壳 > 统一网关 > 指令回执追踪表 +// 写入脚本:scripts/neural/write-receipt-to-notion.js const fs = require('fs'); const path = require('path'); var WORK_ORDER_DIR = 'data/neural-reports/work-orders'; var COMPLETED_DIR = 'data/deploy-queue/completed'; +var RECEIPT_DIR = 'data/neural-reports/receipts'; var TRACKER_DIR = 'data/neural-reports/work-orders'; var TERMINAL_STATUSES = ['CLOSED', 'ESCALATED', 'VERIFIED']; +// ━━━ 指令回执追踪配置 ━━━ +var RECEIPT_TRACKER_CONFIG = { + // 指令回执追踪表 · Notion Database ID + // 位于:光湖语言壳 > 统一网关 > 指令回执追踪表 + // 实际 ID 通过环境变量 RECEIPT_DB_ID 传入(安全考虑不硬编码) + databaseIdEnvVar: 'RECEIPT_DB_ID', + + // 字段映射(与 Notion 数据库字段名一一对应) + fieldMap: { + instructionId: '指令编号', // title + summary: '指令摘要', // text + status: '执行状态', // status + receipt: '铸渊回执', // text + receiptTime: '回执时间', // date + newFiles: '新增文件数', // number + modifiedFiles: '修改文件数', // number + relatedAgent: '关联Agent', // text + timeoutStatus: '回执超时', // select + }, + + // 超时阈值(小时) + timeoutThresholds: { + warning: 24, // 24h → 🟡 接近超时 + critical: 72, // 72h → 🔴 已超时 + } +}; + function loadJSON(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { return null; } @@ -38,29 +70,55 @@ function getActiveWorkOrders() { } function getCompletedReceipts() { - if (!fs.existsSync(COMPLETED_DIR)) return {}; - var receipts = {}; - var files = fs.readdirSync(COMPLETED_DIR) - .filter(function(f) { return f.endsWith('.json'); }); - for (var i = 0; i < files.length; i++) { - var receipt = loadJSON(path.join(COMPLETED_DIR, files[i])); - if (receipt && receipt.command_id) { - receipts[receipt.command_id] = receipt; + // 从 deploy-queue/completed 收集 + if (fs.existsSync(COMPLETED_DIR)) { + var files = fs.readdirSync(COMPLETED_DIR) + .filter(function(f) { return f.endsWith('.json'); }); + + for (var i = 0; i < files.length; i++) { + var receipt = loadJSON(path.join(COMPLETED_DIR, files[i])); + if (receipt && receipt.command_id) { + receipts[receipt.command_id] = receipt; + } } } + + // 从 neural-reports/receipts 收集本地回执 + if (fs.existsSync(RECEIPT_DIR)) { + var receiptFiles = fs.readdirSync(RECEIPT_DIR) + .filter(function(f) { return f.endsWith('.json'); }); + + for (var j = 0; j < receiptFiles.length; j++) { + var localReceipt = loadJSON(path.join(RECEIPT_DIR, receiptFiles[j])); + if (localReceipt && localReceipt.instruction_id) { + receipts[localReceipt.instruction_id] = localReceipt; + } + } + } + return receipts; } function checkTimeout(workOrder) { - var timeout = workOrder.timeout_hours || 24; + var timeout = workOrder.timeout_hours || RECEIPT_TRACKER_CONFIG.timeoutThresholds.critical; var created = new Date(workOrder.created); var now = new Date(); var elapsed = (now - created) / (1000 * 60 * 60); return elapsed > timeout; } +function getTimeoutStatus(workOrder) { + var created = new Date(workOrder.created); + var now = new Date(); + var elapsed = (now - created) / (1000 * 60 * 60); + + if (elapsed > RECEIPT_TRACKER_CONFIG.timeoutThresholds.critical) return '🔴 已超时'; + if (elapsed > RECEIPT_TRACKER_CONFIG.timeoutThresholds.warning) return '🟡 接近超时'; + return '🟢 正常'; +} + function trackWorkOrders() { console.log('\n━━━ 🧬 工单追踪器启动 ━━━\n'); @@ -123,6 +181,11 @@ function trackWorkOrders() { var trackerReport = { tracker_id: 'TRACKER-' + dateStr, timestamp: now.toISOString(), + receipt_config: { + database_env_var: RECEIPT_TRACKER_CONFIG.databaseIdEnvVar, + timeout_warning_hours: RECEIPT_TRACKER_CONFIG.timeoutThresholds.warning, + timeout_critical_hours: RECEIPT_TRACKER_CONFIG.timeoutThresholds.critical + }, stats: stats, active_orders: activeOrders.map(function(wo) { return { @@ -131,7 +194,8 @@ function trackWorkOrders() { title: wo.title, status: wo.status, created: wo.created, - timed_out: checkTimeout(wo) + timed_out: checkTimeout(wo), + timeout_status: getTimeoutStatus(wo) }; }) }; @@ -157,4 +221,4 @@ if (require.main === module) { trackWorkOrders(); } -module.exports = { trackWorkOrders }; +module.exports = { trackWorkOrders, RECEIPT_TRACKER_CONFIG }; diff --git a/scripts/neural/write-receipt-to-notion.js b/scripts/neural/write-receipt-to-notion.js new file mode 100644 index 00000000..dba052f3 --- /dev/null +++ b/scripts/neural/write-receipt-to-notion.js @@ -0,0 +1,359 @@ +// scripts/neural/write-receipt-to-notion.js +// 📡 指令回执自动写入 Notion +// 铸渊每次执行完指令后,自动将回执写入 Notion「指令回执追踪表」 +// 实现 指令→执行→回执 全链路自动闭环 +// +// 用法: +// node scripts/neural/write-receipt-to-notion.js \ +// --instruction-id "ZY-XXX" \ +// --status "success" \ +// --workflow "workflow-name" \ +// [--summary "执行摘要"] \ +// [--receipt "回执详情"] \ +// [--new-files 0] \ +// [--modified-files 0] \ +// [--related-agent "AG-TY-01"] +// +// 环境变量: +// NOTION_API_KEY — Notion API 密钥(必须) +// RECEIPT_DB_ID — 指令回执追踪表的 Notion Database ID(必须) +// +// 铁律: +// 1. 写入失败不阻塞主流程 +// 2. 同一指令编号幂等(重复写入 = 更新) +// 3. 失败时写入本地 SYSLOG 报错 + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var https = require('https'); + +// ━━━ 配置 ━━━ + +var RECEIPT_TRACKER_CONFIG = { + // 字段映射 — Notion 数据库字段名 + fieldMap: { + instructionId: '指令编号', + summary: '指令摘要', + status: '执行状态', + receipt: '铸渊回执', + receiptTime: '回执时间', + newFiles: '新增文件数', + modifiedFiles: '修改文件数', + relatedAgent: '关联Agent', + timeoutStatus: '回执超时' + }, + + // 超时阈值(小时) + timeoutThresholds: { + warning: 24, + critical: 72 + }, + + // 状态映射 + statusMap: { + 'success': '✅ 已完成', + 'failure': '❌ 执行失败', + 'cancelled': '⏸️ 已挂起', + 'skipped': '⏸️ 已挂起' + } +}; + +var LOCAL_RECEIPT_DIR = 'data/neural-reports/receipts'; +var SYSLOG_DIR = 'data/neural-reports/syslog'; + +// ━━━ 参数解析 ━━━ + +function parseArgs() { + var args = process.argv.slice(2); + var parsed = {}; + + for (var i = 0; i < args.length; i++) { + if (args[i] === '--instruction-id' && args[i + 1]) { + parsed.instructionId = args[++i]; + } else if (args[i] === '--status' && args[i + 1]) { + parsed.status = args[++i]; + } else if (args[i] === '--workflow' && args[i + 1]) { + parsed.workflow = args[++i]; + } else if (args[i] === '--summary' && args[i + 1]) { + parsed.summary = args[++i]; + } else if (args[i] === '--receipt' && args[i + 1]) { + parsed.receipt = args[++i]; + } else if (args[i] === '--new-files' && args[i + 1]) { + parsed.newFiles = parseInt(args[++i], 10) || 0; + } else if (args[i] === '--modified-files' && args[i + 1]) { + parsed.modifiedFiles = parseInt(args[++i], 10) || 0; + } else if (args[i] === '--related-agent' && args[i + 1]) { + parsed.relatedAgent = args[++i]; + } + } + + return parsed; +} + +// ━━━ Notion API 封装(纯 https,无外部依赖)━━━ + +function notionRequest(method, endpoint, body, token) { + return new Promise(function(resolve, reject) { + var bodyStr = body ? JSON.stringify(body) : ''; + + var options = { + hostname: 'api.notion.com', + port: 443, + path: '/v1' + endpoint, + method: method, + headers: { + 'Authorization': 'Bearer ' + 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 { + var parsed = JSON.parse(data); + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(parsed); + } else { + reject(new Error('Notion API ' + res.statusCode + ': ' + (parsed.message || data))); + } + } catch (e) { + reject(new Error('Notion API response parse error: ' + data.substring(0, 200))); + } + }); + }); + + req.on('error', function(e) { + reject(new Error('Notion API request error: ' + e.message)); + }); + + req.setTimeout(30000, function() { + req.destroy(); + reject(new Error('Notion API request timeout (30s)')); + }); + + if (bodyStr) req.write(bodyStr); + req.end(); + }); +} + +// ━━━ Notion 属性构建 ━━━ + +function buildNotionProperties(receipt) { + var fm = RECEIPT_TRACKER_CONFIG.fieldMap; + var statusText = RECEIPT_TRACKER_CONFIG.statusMap[receipt.status] || '⏸️ 已挂起'; + + var properties = {}; + + // 指令编号 (title) + properties[fm.instructionId] = { + title: [{ text: { content: receipt.instructionId || 'AUTO' } }] + }; + + // 指令摘要 (rich_text) + properties[fm.summary] = { + rich_text: [{ text: { content: receipt.summary || receipt.workflow || '' } }] + }; + + // 执行状态 (status) + properties[fm.status] = { + status: { name: statusText } + }; + + // 铸渊回执 (rich_text) + var receiptText = receipt.receipt || (statusText + ' · ' + (receipt.workflow || '')); + properties[fm.receipt] = { + rich_text: [{ text: { content: receiptText.substring(0, 2000) } }] + }; + + // 回执时间 (date) + properties[fm.receiptTime] = { + date: { start: new Date().toISOString().split('T')[0] } + }; + + // 新增文件数 (number) + if (typeof receipt.newFiles === 'number') { + properties[fm.newFiles] = { number: receipt.newFiles }; + } + + // 修改文件数 (number) + if (typeof receipt.modifiedFiles === 'number') { + properties[fm.modifiedFiles] = { number: receipt.modifiedFiles }; + } + + // 关联Agent (rich_text) + if (receipt.relatedAgent) { + properties[fm.relatedAgent] = { + rich_text: [{ text: { content: receipt.relatedAgent } }] + }; + } + + // 回执超时 (select) + properties[fm.timeoutStatus] = { + select: { name: '🟢 正常' } + }; + + return properties; +} + +// ━━━ 写入 Notion ━━━ + +async function writeReceiptToNotion(receipt, token, databaseId) { + console.log('📡 写入回执到 Notion · 指令: ' + receipt.instructionId); + + // 先查询是否已有该指令编号的记录(幂等) + var queryBody = { + database_id: databaseId, + filter: { + property: RECEIPT_TRACKER_CONFIG.fieldMap.instructionId, + title: { equals: receipt.instructionId } + } + }; + + var existing = await notionRequest('POST', '/databases/' + databaseId + '/query', queryBody, token); + + var properties = buildNotionProperties(receipt); + + if (existing.results && existing.results.length > 0) { + // 更新已有记录 + var pageId = existing.results[0].id; + await notionRequest('PATCH', '/pages/' + pageId, { properties: properties }, token); + console.log('✅ 已更新现有记录: ' + pageId); + } else { + // 创建新记录 + await notionRequest('POST', '/pages', { + parent: { database_id: databaseId }, + properties: properties + }, token); + console.log('✅ 已创建新记录'); + } +} + +// ━━━ 本地兜底写入 ━━━ + +function writeLocalReceipt(receipt) { + try { + fs.mkdirSync(LOCAL_RECEIPT_DIR, { recursive: true }); + + var now = new Date(); + var cstTime = new Date(now.getTime() + 8 * 60 * 60 * 1000); + var dateStr = cstTime.toISOString().split('T')[0]; + var timeStr = cstTime.toISOString().replace(/[-:]/g, '').split('.')[0]; + + var filename = 'receipt-' + timeStr + '.json'; + var fullPath = path.join(LOCAL_RECEIPT_DIR, filename); + + var localReceipt = { + instruction_id: receipt.instructionId, + workflow: receipt.workflow, + status: receipt.status, + summary: receipt.summary || '', + receipt_text: receipt.receipt || '', + new_files: receipt.newFiles || 0, + modified_files: receipt.modifiedFiles || 0, + related_agent: receipt.relatedAgent || '', + timestamp: now.toISOString(), + notion_synced: false + }; + + fs.writeFileSync(fullPath, JSON.stringify(localReceipt, null, 2)); + console.log('💾 本地回执已保存: ' + fullPath); + return fullPath; + } catch (e) { + console.error('❌ 本地回执保存失败: ' + e.message); + return null; + } +} + +// ━━━ SYSLOG 报错 ━━━ + +function writeSyslogError(message, receipt) { + try { + fs.mkdirSync(SYSLOG_DIR, { recursive: true }); + + var now = new Date(); + var cstTime = new Date(now.getTime() + 8 * 60 * 60 * 1000); + var timeStr = cstTime.toISOString().replace(/[-:]/g, '').split('.')[0]; + + var logEntry = { + level: 'ERROR', + source: 'write-receipt-to-notion', + message: message, + instruction_id: receipt ? receipt.instructionId : 'unknown', + timestamp: now.toISOString() + }; + + var filename = 'receipt-error-' + timeStr + '.json'; + fs.writeFileSync( + path.join(SYSLOG_DIR, filename), + JSON.stringify(logEntry, null, 2) + ); + console.error('📝 错误已记录到 SYSLOG: ' + filename); + } catch (e) { + console.error('⚠️ SYSLOG 写入也失败: ' + e.message); + } +} + +// ━━━ 主函数 ━━━ + +async function main() { + console.log('\n━━━ 📡 指令回执写入 ━━━\n'); + + var receipt = parseArgs(); + + if (!receipt.instructionId && !receipt.workflow) { + console.log('⚠️ 未提供 instruction-id 或 workflow,跳过回执写入'); + process.exit(0); + } + + // 如果没有指令ID,用 workflow + 日期 自动生成 + if (!receipt.instructionId || receipt.instructionId === 'AUTO') { + var now = new Date(); + var cstTime = new Date(now.getTime() + 8 * 60 * 60 * 1000); + var dateStr = cstTime.toISOString().split('T')[0].replace(/-/g, ''); + receipt.instructionId = 'AUTO-' + (receipt.workflow || 'unknown') + '-' + dateStr; + } + + // 始终写入本地兜底 + writeLocalReceipt(receipt); + + // 尝试写入 Notion + var token = process.env.NOTION_API_KEY; + var databaseId = process.env.RECEIPT_DB_ID; + + if (!token) { + var msg = '⚠️ NOTION_API_KEY 未设置,仅保存本地回执'; + console.log(msg); + writeSyslogError(msg, receipt); + process.exit(0); + } + + if (!databaseId) { + var msg = '⚠️ RECEIPT_DB_ID 未设置,仅保存本地回执'; + console.log(msg); + writeSyslogError(msg, receipt); + process.exit(0); + } + + try { + await writeReceiptToNotion(receipt, token, databaseId); + console.log('\n✅ 回执写入完成\n'); + } catch (e) { + console.error('❌ Notion 写入失败: ' + e.message); + writeSyslogError('Notion 写入失败: ' + e.message, receipt); + // 不阻塞主流程,退出码 0 + console.log('💾 已有本地兜底,不影响主流程'); + } + + process.exit(0); +} + +main().catch(function(err) { + console.error('❌ 回执脚本异常: ' + err.message); + process.exit(0); // 不阻塞 +});