diff --git a/.github/brain/memory.json b/.github/brain/memory.json index 19493902..599de8f9 100644 --- a/.github/brain/memory.json +++ b/.github/brain/memory.json @@ -1,7 +1,7 @@ { "identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体", "rules_version": "v1.0", - "last_updated": "2026-03-06T08:51:24.831Z", + "last_updated": "2026-03-07T04:45:55.670Z", "wake_protocol_version": "v1.0", "wake_triggers": [ "我是冰朔", @@ -23,6 +23,12 @@ } }, "events": [ + { + "date": "2026-03-07", + "type": "psp_inspection", + "description": "铸渊 PSP 巡检完成 · 发现 1 个问题 · 自动修复 0 项", + "by": "铸渊PSP巡检" + }, { "timestamp": "2026-03-05T10:30:46.894Z", "type": "ci_run", @@ -70,4 +76,4 @@ "run_id": "22756140950" } ] -} +} \ No newline at end of file diff --git a/.github/persona-brain/growth-journal.md b/.github/persona-brain/growth-journal.md index 5f7324d5..c7ee396b 100644 --- a/.github/persona-brain/growth-journal.md +++ b/.github/persona-brain/growth-journal.md @@ -37,3 +37,10 @@ - 累计自检次数:4 - 累计CI运行:1次 - HLI覆盖率:17.6% + +## 2026-03-07 · 每日自检 +- 大脑文件完整性:⚠️ 缺失 identity.md, routing-map.json, responsibility.md, decision-log.md +- 知识库条目:4条 +- 累计自检次数:5 +- 累计CI运行:1次 +- HLI覆盖率:17.6% diff --git a/.github/persona-brain/memory.json b/.github/persona-brain/memory.json index 1e8ed86f..cc773552 100644 --- a/.github/persona-brain/memory.json +++ b/.github/persona-brain/memory.json @@ -1,7 +1,7 @@ { "identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体", "rules_version": "v1.0", - "last_updated": "2026-03-07T00:54:06.899Z", + "last_updated": "2026-03-07T04:46:02.610Z", "wake_protocol_version": "v1.0", "wake_triggers": [ "我是冰朔", @@ -12,9 +12,15 @@ ], "founder": "冰朔(Bīng Shuò)", "hli_coverage": "17.6%", - "total_selfchecks": 4, + "total_selfchecks": 5, "total_ci_runs": 1, "recent_events": [ + { + "date": "2026-03-07", + "type": "daily_selfcheck", + "description": "每日自检完成 · 知识库4条 · 缺失文件4个", + "by": "铸渊自检" + }, { "date": "2026-03-07", "type": "daily_selfcheck", diff --git a/.github/workflows/esp-signal-processor.yml b/.github/workflows/esp-signal-processor.yml new file mode 100644 index 00000000..c118f39a --- /dev/null +++ b/.github/workflows/esp-signal-processor.yml @@ -0,0 +1,38 @@ +name: 铸渊 · ESP 邮件信号处理器 + +on: + schedule: + - cron: '*/30 * * * *' # 每30分钟执行一次 + workflow_dispatch: # 手动触发 + +jobs: + esp-process: + name: 📡 ESP 信号处理 + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 安装依赖 + run: npm ci + + - name: 处理 GL-CMD 邮件信号 + env: + EMAIL_ADDRESS: ${{ secrets.EMAIL_ADDRESS }} + EMAIL_APP_PASSWORD: ${{ secrets.EMAIL_APP_PASSWORD }} + run: node scripts/esp-email-processor.js + + - name: 提交信号日志 + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add signal-log/ notion-push/ dev-nodes/ + git diff --cached --quiet || git commit -m "📡 ESP 信号处理 · $(date +%Y-%m-%dT%H:%M)" + git push diff --git a/.github/workflows/psp-daily-inspection.yml b/.github/workflows/psp-daily-inspection.yml new file mode 100644 index 00000000..19474b53 --- /dev/null +++ b/.github/workflows/psp-daily-inspection.yml @@ -0,0 +1,41 @@ +name: 铸渊 · PSP 分身巡检 + +on: + schedule: + - cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00 + workflow_dispatch: # 手动触发 + +jobs: + psp-inspect: + name: 🔍 铸渊 PSP 每日巡检 + runs-on: ubuntu-latest + permissions: + contents: write + actions: read + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 安装依赖 + run: npm ci + + - name: 执行 PSP 巡检(5项检查) + env: + EMAIL_ADDRESS: ${{ secrets.EMAIL_ADDRESS }} + EMAIL_APP_PASSWORD: ${{ secrets.EMAIL_APP_PASSWORD }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: node scripts/psp-inspection.js + + - name: 提交巡检结果 + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add signal-log/ dev-nodes/ notion-push/ .github/brain/memory.json + git diff --cached --quiet || git commit -m "🔍 铸渊PSP巡检 · $(date +%Y-%m-%d)" + git push diff --git a/.gitignore b/.gitignore index 27592c81..061a04ac 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/dev-nodes/DEV-001/config.json b/dev-nodes/DEV-001/config.json new file mode 100644 index 00000000..4d686d03 --- /dev/null +++ b/dev-nodes/DEV-001/config.json @@ -0,0 +1,10 @@ +{ + "dev_id": "DEV-001", + "name": "页页", + "emoji": "🖥️", + "modules": ["backend-integration"], + "pat_configured": false, + "network_scheme": "layer1_https", + "created_at": "2026-03-06T23:00:00+08:00", + "notes": "" +} diff --git a/dev-nodes/DEV-001/status.json b/dev-nodes/DEV-001/status.json new file mode 100644 index 00000000..b36d602c --- /dev/null +++ b/dev-nodes/DEV-001/status.json @@ -0,0 +1,9 @@ +{ + "dev_id": "DEV-001", + "last_seen": null, + "last_push": null, + "active_module": null, + "pending_broadcasts": 0, + "signal_count": 0, + "updated_at": "2026-03-06T23:00:00+08:00" +} diff --git a/dev-nodes/DEV-002/config.json b/dev-nodes/DEV-002/config.json new file mode 100644 index 00000000..3a265e41 --- /dev/null +++ b/dev-nodes/DEV-002/config.json @@ -0,0 +1,10 @@ +{ + "dev_id": "DEV-002", + "name": "肥猫", + "emoji": "🦁", + "modules": ["m01-login", "m03-personality"], + "pat_configured": false, + "network_scheme": "layer1_https", + "created_at": "2026-03-06T23:00:00+08:00", + "notes": "" +} diff --git a/dev-nodes/DEV-002/status.json b/dev-nodes/DEV-002/status.json new file mode 100644 index 00000000..d10a4412 --- /dev/null +++ b/dev-nodes/DEV-002/status.json @@ -0,0 +1,9 @@ +{ + "dev_id": "DEV-002", + "last_seen": null, + "last_push": null, + "active_module": null, + "pending_broadcasts": 0, + "signal_count": 0, + "updated_at": "2026-03-06T23:00:00+08:00" +} diff --git a/dev-nodes/DEV-003/config.json b/dev-nodes/DEV-003/config.json new file mode 100644 index 00000000..b15b8bf3 --- /dev/null +++ b/dev-nodes/DEV-003/config.json @@ -0,0 +1,10 @@ +{ + "dev_id": "DEV-003", + "name": "燕樊", + "emoji": "🌸", + "modules": ["m07-dialogue-ui", "m10-cloud", "m15-cloud-drive"], + "pat_configured": false, + "network_scheme": "layer1_https", + "created_at": "2026-03-06T23:00:00+08:00", + "notes": "" +} diff --git a/dev-nodes/DEV-003/status.json b/dev-nodes/DEV-003/status.json new file mode 100644 index 00000000..79cb2923 --- /dev/null +++ b/dev-nodes/DEV-003/status.json @@ -0,0 +1,9 @@ +{ + "dev_id": "DEV-003", + "last_seen": null, + "last_push": null, + "active_module": null, + "pending_broadcasts": 0, + "signal_count": 0, + "updated_at": "2026-03-06T23:00:00+08:00" +} diff --git a/dev-nodes/DEV-004/config.json b/dev-nodes/DEV-004/config.json new file mode 100644 index 00000000..4940e221 --- /dev/null +++ b/dev-nodes/DEV-004/config.json @@ -0,0 +1,10 @@ +{ + "dev_id": "DEV-004", + "name": "之之", + "emoji": "🤖", + "modules": ["dingtalk-bot"], + "pat_configured": false, + "network_scheme": "layer1_https", + "created_at": "2026-03-06T23:00:00+08:00", + "notes": "" +} diff --git a/dev-nodes/DEV-004/status.json b/dev-nodes/DEV-004/status.json new file mode 100644 index 00000000..a32080aa --- /dev/null +++ b/dev-nodes/DEV-004/status.json @@ -0,0 +1,9 @@ +{ + "dev_id": "DEV-004", + "last_seen": null, + "last_push": null, + "active_module": null, + "pending_broadcasts": 0, + "signal_count": 0, + "updated_at": "2026-03-06T23:00:00+08:00" +} diff --git a/dev-nodes/DEV-005/config.json b/dev-nodes/DEV-005/config.json new file mode 100644 index 00000000..982d0f5c --- /dev/null +++ b/dev-nodes/DEV-005/config.json @@ -0,0 +1,10 @@ +{ + "dev_id": "DEV-005", + "name": "小草莓", + "emoji": "🍓", + "modules": ["m12-kanban", "status-board"], + "pat_configured": false, + "network_scheme": "layer1_https", + "created_at": "2026-03-06T23:00:00+08:00", + "notes": "" +} diff --git a/dev-nodes/DEV-005/status.json b/dev-nodes/DEV-005/status.json new file mode 100644 index 00000000..4fbac109 --- /dev/null +++ b/dev-nodes/DEV-005/status.json @@ -0,0 +1,9 @@ +{ + "dev_id": "DEV-005", + "last_seen": null, + "last_push": null, + "active_module": null, + "pending_broadcasts": 0, + "signal_count": 0, + "updated_at": "2026-03-06T23:00:00+08:00" +} diff --git a/dev-nodes/DEV-009/config.json b/dev-nodes/DEV-009/config.json new file mode 100644 index 00000000..56ba40aa --- /dev/null +++ b/dev-nodes/DEV-009/config.json @@ -0,0 +1,10 @@ +{ + "dev_id": "DEV-009", + "name": "花尔", + "emoji": "🌺", + "modules": ["m05-user-center"], + "pat_configured": false, + "network_scheme": "layer1_https", + "created_at": "2026-03-06T23:00:00+08:00", + "notes": "" +} diff --git a/dev-nodes/DEV-009/status.json b/dev-nodes/DEV-009/status.json new file mode 100644 index 00000000..fae32904 --- /dev/null +++ b/dev-nodes/DEV-009/status.json @@ -0,0 +1,9 @@ +{ + "dev_id": "DEV-009", + "last_seen": null, + "last_push": null, + "active_module": null, + "pending_broadcasts": 0, + "signal_count": 0, + "updated_at": "2026-03-06T23:00:00+08:00" +} diff --git a/dev-nodes/DEV-010/config.json b/dev-nodes/DEV-010/config.json new file mode 100644 index 00000000..2cbc23c9 --- /dev/null +++ b/dev-nodes/DEV-010/config.json @@ -0,0 +1,10 @@ +{ + "dev_id": "DEV-010", + "name": "桔子", + "emoji": "🍊", + "modules": ["m06-ticket", "m11-module"], + "pat_configured": false, + "network_scheme": "layer1_https", + "created_at": "2026-03-06T23:00:00+08:00", + "notes": "" +} diff --git a/dev-nodes/DEV-010/status.json b/dev-nodes/DEV-010/status.json new file mode 100644 index 00000000..e79bf2e1 --- /dev/null +++ b/dev-nodes/DEV-010/status.json @@ -0,0 +1,9 @@ +{ + "dev_id": "DEV-010", + "last_seen": null, + "last_push": null, + "active_module": null, + "pending_broadcasts": 0, + "signal_count": 0, + "updated_at": "2026-03-06T23:00:00+08:00" +} diff --git a/dev-nodes/DEV-011/config.json b/dev-nodes/DEV-011/config.json new file mode 100644 index 00000000..6de3b50c --- /dev/null +++ b/dev-nodes/DEV-011/config.json @@ -0,0 +1,10 @@ +{ + "dev_id": "DEV-011", + "name": "匆匆那年", + "emoji": "🌙", + "modules": [], + "pat_configured": false, + "network_scheme": "layer1_https", + "created_at": "2026-03-06T23:00:00+08:00", + "notes": "" +} diff --git a/dev-nodes/DEV-011/status.json b/dev-nodes/DEV-011/status.json new file mode 100644 index 00000000..58c35741 --- /dev/null +++ b/dev-nodes/DEV-011/status.json @@ -0,0 +1,9 @@ +{ + "dev_id": "DEV-011", + "last_seen": null, + "last_push": null, + "active_module": null, + "pending_broadcasts": 0, + "signal_count": 0, + "updated_at": "2026-03-06T23:00:00+08:00" +} diff --git a/dev-nodes/gl-push.sh b/dev-nodes/gl-push.sh new file mode 100644 index 00000000..fbce4cff --- /dev/null +++ b/dev-nodes/gl-push.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# gl-push.sh — 光湖智能上传脚本 · 三层网络降级方案 +# 用法: bash gl-push.sh "提交信息" +# 要求: git 已配置,PAT token 已通过 git config credential.helper store 存储 + +set -euo pipefail + +REPO_URL="https://github.com/qinfendebingshuo/guanghulab.git" +PROXY_URL="https://ghproxy.com/https://github.com/qinfendebingshuo/guanghulab.git" +MSG="${1:-"📤 开发者上传 $(date +%Y-%m-%dT%H:%M)"}" + +echo "🐙 光湖代码上传中..." +git add . +git commit -m "$MSG" 2>/dev/null || echo " ℹ️ 无新提交,直接推送" + +# ── 层级1:直连 HTTPS ────────────────────────────────────────────────────── +echo "🔗 [层级1] 尝试直连 GitHub..." +if git push origin main 2>/dev/null; then + echo "✅ 直连上传成功!铸渊已接收。" + exit 0 +fi + +# ── 层级2:ghproxy 镜像加速 ─────────────────────────────────────────────── +echo "⚠️ 直连失败,切换镜像通道..." +git remote set-url origin "$PROXY_URL" +if git push origin main 2>/dev/null; then + echo "✅ 镜像通道上传成功!铸渊已接收。" + git remote set-url origin "$REPO_URL" + exit 0 +fi +git remote set-url origin "$REPO_URL" + +# ── 层级3:SSH ──────────────────────────────────────────────────────────── +echo "⚠️ 镜像也失败,尝试 SSH 通道..." +SSH_URL="git@github.com:qinfendebingshuo/guanghulab.git" +git remote set-url origin "$SSH_URL" +if git push origin main; then + echo "✅ SSH 通道上传成功!铸渊已接收。" + git remote set-url origin "$REPO_URL" + exit 0 +fi +git remote set-url origin "$REPO_URL" + +# ── 全部失败 ────────────────────────────────────────────────────────────── +echo "❌ 三层网络均不通,请联系知秋处理。" +echo " 可尝试手动命令: git push git@github.com:qinfendebingshuo/guanghulab.git main" +exit 1 diff --git a/notion-push/pending/.gitkeep b/notion-push/pending/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/notion-push/processed/.gitkeep b/notion-push/processed/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/package-lock.json b/package-lock.json index bf5d24f3..ac32db90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "nextjs", "version": "0.1.0", "dependencies": { + "imapflow": "^1.2.12", "next": "15.3.8", + "nodemailer": "^8.0.1", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -1644,6 +1646,12 @@ "node": ">= 10" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2161,6 +2169,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", + "integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2230,6 +2249,15 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2955,6 +2983,15 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding-japanese": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", + "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", + "license": "MIT", + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -3282,6 +3319,39 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/imapflow": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/imapflow/-/imapflow-1.2.12.tgz", + "integrity": "sha512-UX8qCKXZk2xExe/x8KPTSbhROdtUGP13bSLSjT9Sb3YwGuryD4aFNlGhbWBW5B1GtgHMRxVv9yvl61RqXgIQtQ==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.8", + "encoding-japanese": "2.2.0", + "iconv-lite": "0.7.2", + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1", + "nodemailer": "8.0.1", + "pino": "10.3.1", + "socks": "2.8.7" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -3331,6 +3401,15 @@ "dev": true, "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -4250,6 +4329,42 @@ "node": ">=6" } }, + "node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", + "license": "MIT" + }, + "node_modules/libmime": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "node_modules/libmime/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/libqp": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", + "integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==", + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.29.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", @@ -4730,6 +4845,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4753,6 +4877,15 @@ "node": ">=8" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4940,6 +5073,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -5020,6 +5190,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -5051,6 +5237,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -5079,6 +5271,15 @@ "dev": true, "license": "MIT" }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5143,6 +5344,21 @@ "node": ">=10" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -5266,6 +5482,39 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5296,6 +5545,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5625,6 +5883,18 @@ "node": "*" } }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 1820b151..6ddb8031 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,14 @@ "brain:sync": "node scripts/process-broadcasts.js", "brain:daily-check": "node scripts/daily-check.js", "brain:update-memory": "node scripts/update-memory.js", - "broadcast:distribute": "node scripts/distribute-broadcasts.js" + "broadcast:distribute": "node scripts/distribute-broadcasts.js", + "esp:process": "node scripts/esp-email-processor.js", + "psp:inspect": "node scripts/psp-inspection.js" }, "dependencies": { + "imapflow": "^1.2.12", "next": "15.3.8", + "nodemailer": "^8.0.1", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/scripts/esp-email-processor.js b/scripts/esp-email-processor.js new file mode 100644 index 00000000..cd5e7a92 --- /dev/null +++ b/scripts/esp-email-processor.js @@ -0,0 +1,360 @@ +// scripts/esp-email-processor.js +// ESP 邮件信号协议 · GitHub 端处理器 +// 功能:读取 Gmail IMAP → 解析 [GL-CMD] 信号 → 执行指令 → 写 signal-log → 发 [GL-ACK] + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { ImapFlow } = require('imapflow'); +const nodemailer = require('nodemailer'); + +// ── 配置 ──────────────────────────────────────────────────────────────────── +const EMAIL_ADDRESS = process.env.EMAIL_ADDRESS; +const EMAIL_PASSWORD = process.env.EMAIL_APP_PASSWORD; + +if (!EMAIL_ADDRESS || !EMAIL_PASSWORD) { + console.error('❌ 缺少环境变量 EMAIL_ADDRESS 或 EMAIL_APP_PASSWORD'); + process.exit(1); +} + +const SIGNAL_LOG_DIR = path.join(__dirname, '../signal-log'); +const SIGNAL_INDEX_PATH = path.join(SIGNAL_LOG_DIR, 'index.json'); +const NOTION_PUSH_DIR = path.join(__dirname, '../notion-push/pending'); +const DEV_NODES_DIR = path.join(__dirname, '../dev-nodes'); + +// ── 信号 ID 生成 ────────────────────────────────────────────────────────── +function generateSignalId() { + const now = new Date(); + const ymd = now.toISOString().slice(0, 10).replace(/-/g, ''); + const index = loadSignalIndex(); + const seq = String(index.total_count + 1).padStart(3, '0'); + return `SIG-${ymd}-${seq}`; +} + +// ── 信号索引读写 ────────────────────────────────────────────────────────── +function loadSignalIndex() { + if (!fs.existsSync(SIGNAL_INDEX_PATH)) { + return { last_updated: new Date().toISOString(), total_count: 0, signals: [] }; + } + return JSON.parse(fs.readFileSync(SIGNAL_INDEX_PATH, 'utf8')); +} + +function saveSignalIndex(index) { + index.last_updated = new Date().toISOString(); + fs.writeFileSync(SIGNAL_INDEX_PATH, JSON.stringify(index, null, 2)); +} + +// ── 写入信号日志文件 ────────────────────────────────────────────────────── +function writeSignalLog(signal) { + const dateStr = signal.timestamp.slice(0, 7); // "YYYY-MM" + const monthDir = path.join(SIGNAL_LOG_DIR, dateStr); + fs.mkdirSync(monthDir, { recursive: true }); + + const filePath = path.join(monthDir, `${signal.signal_id}.json`); + fs.writeFileSync(filePath, JSON.stringify(signal, null, 2)); + + const index = loadSignalIndex(); + index.total_count += 1; + index.signals.unshift({ + signal_id: signal.signal_id, + trace_id: signal.trace_id, + type: signal.signal_type, + timestamp: signal.timestamp, + summary: signal.summary, + related_dev: signal.related_dev || null, + file: `${dateStr}/${signal.signal_id}.json` + }); + saveSignalIndex(index); + + console.log(`📝 信号已写入: ${signal.signal_id}`); +} + +// ── 写入 notion-push/pending ────────────────────────────────────────────── +function pushToNotion(signal) { + fs.mkdirSync(NOTION_PUSH_DIR, { recursive: true }); + const filePath = path.join(NOTION_PUSH_DIR, `${signal.signal_id}.json`); + fs.writeFileSync(filePath, JSON.stringify(signal, null, 2)); + console.log(`📤 已写入 notion-push/pending: ${signal.signal_id}`); +} + +// ── SMTP 发送 ACK 邮件 ──────────────────────────────────────────────────── +async function sendAckEmail(originalSignal, ackSignalId, result) { + const transporter = nodemailer.createTransport({ + host: 'smtp.gmail.com', + port: 587, + secure: false, + auth: { user: EMAIL_ADDRESS, pass: EMAIL_PASSWORD } + }); + + const ackPayload = { + signal_type: 'GL-ACK', + trace_id: originalSignal.trace_id, + ack_signal_id: ackSignalId, + original_cmd_id: originalSignal.signal_id, + timestamp: new Date().toISOString(), + sender: '铸渊', + receiver: '霜砚', + original_command: originalSignal.command, + result, + message: `指令 ${originalSignal.command} 执行${result === '成功' ? '完成' : '失败'}` + }; + + await transporter.sendMail({ + from: EMAIL_ADDRESS, + to: EMAIL_ADDRESS, + subject: `[GL-ACK] ${originalSignal.trace_id} | ${originalSignal.command} ${result}`, + text: JSON.stringify(ackPayload, null, 2) + }); + + console.log(`📧 [GL-ACK] 已发送: ${ackSignalId}`); + return ackPayload; +} + +// ── 指令执行器 ──────────────────────────────────────────────────────────── +async function executeCommand(signal) { + const { command, payload } = signal; + console.log(`⚙️ 执行指令: ${command}`); + + switch (command) { + case 'sync_broadcast': { + // 将 broadcasts-outbox/ 中的广播同步到对应 dev-nodes/ + const outboxDir = path.join(__dirname, '../broadcasts-outbox'); + if (!fs.existsSync(outboxDir)) { + return { result: '失败', detail: 'broadcasts-outbox 目录不存在' }; + } + const devDirs = fs.readdirSync(outboxDir).filter(f => f.startsWith('DEV-')); + let synced = 0; + for (const devId of devDirs) { + const srcDir = path.join(outboxDir, devId); + const destDir = path.join(DEV_NODES_DIR, devId); + if (!fs.existsSync(destDir)) continue; + const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.json')); + for (const file of files) { + const src = path.join(srcDir, file); + const dest = path.join(destDir, file); + if (!fs.existsSync(dest)) { + fs.copyFileSync(src, dest); + synced++; + } + } + // 更新 status.json 中的 pending_broadcasts + const statusPath = path.join(destDir, 'status.json'); + if (fs.existsSync(statusPath)) { + const status = JSON.parse(fs.readFileSync(statusPath, 'utf8')); + status.pending_broadcasts = files.length; + status.updated_at = new Date().toISOString(); + fs.writeFileSync(statusPath, JSON.stringify(status, null, 2)); + } + } + return { result: '成功', detail: `已同步 ${synced} 个广播文件` }; + } + + case 'check_syslog': { + const syslogInbox = path.join(__dirname, '../syslog-inbox'); + const files = fs.existsSync(syslogInbox) + ? fs.readdirSync(syslogInbox).filter(f => f.endsWith('.json')) + : []; + const summary = `syslog-inbox 待处理: ${files.length} 条`; + pushToNotion({ + signal_id: generateSignalId(), + trace_id: signal.trace_id, + timestamp: new Date().toISOString(), + signal_type: 'GL-DATA', + direction: 'GitHub→Notion', + sender: '铸渊', + receiver: '霜砚', + related_dev: null, + related_module: null, + summary, + payload: { syslog_pending: files.length, files } + }); + return { result: '成功', detail: summary }; + } + + case 'update_status': { + const { dev_id, updates } = payload || {}; + if (!dev_id) return { result: '失败', detail: '缺少 dev_id' }; + const statusPath = path.join(DEV_NODES_DIR, dev_id, 'status.json'); + if (!fs.existsSync(statusPath)) { + return { result: '失败', detail: `${dev_id} status.json 不存在` }; + } + let status; + try { + status = JSON.parse(fs.readFileSync(statusPath, 'utf8')); + } catch (err) { + return { result: '失败', detail: `${dev_id} status.json 解析失败: ${err.message}` }; + } + Object.assign(status, updates, { updated_at: new Date().toISOString() }); + fs.writeFileSync(statusPath, JSON.stringify(status, null, 2)); + return { result: '成功', detail: `${dev_id} 状态已更新` }; + } + + default: + return { result: '失败', detail: `未知指令: ${command}` }; + } +} + +// ── 解析邮件正文中的 JSON 信号 ──────────────────────────────────────────── +function parseSignalFromEmail(subject, body) { + if (!subject.includes('[GL-CMD]')) return null; + try { + // 提取第一个完整 JSON 对象(跳过邮件头中的任何前缀内容) + const jsonMatch = body.match(/\{[\s\S]*\}/); + if (!jsonMatch) return null; + const parsed = JSON.parse(jsonMatch[0]); + if (!parsed.signal_type || !parsed.trace_id || !parsed.command) return null; + return parsed; + } catch (err) { + console.log(`⚠️ 邮件 JSON 解析失败: ${err.message}`); + return null; + } +} + +// ── 主流程 ──────────────────────────────────────────────────────────────── +async function main() { + console.log('═══════════════════════════════════════════════'); + console.log('📡 ESP 邮件信号处理器启动'); + console.log(` 时间: ${new Date().toISOString()}`); + console.log('═══════════════════════════════════════════════\n'); + + const client = new ImapFlow({ + host: 'imap.gmail.com', + port: 993, + secure: true, + auth: { user: EMAIL_ADDRESS, pass: EMAIL_PASSWORD }, + logger: false + }); + + await client.connect(); + + let processed = 0; + let errors = 0; + + try { + const lock = await client.getMailboxLock('INBOX'); + try { + // 搜索未读的 [GL-CMD] 邮件 + const messages = await client.search({ seen: false, subject: '[GL-CMD]' }); + + if (!messages || messages.length === 0) { + console.log('📭 无待处理 [GL-CMD] 邮件'); + } else { + console.log(`📬 发现 ${messages.length} 封 [GL-CMD] 邮件`); + + for await (const msg of client.fetch(messages, { envelope: true, source: true })) { + const subject = msg.envelope?.subject || ''; + const rawBody = msg.source?.toString('utf8') || ''; + + // 提取 JSON 正文(跳过邮件头) + const bodyStart = rawBody.indexOf('{'); + const body = bodyStart >= 0 ? rawBody.slice(bodyStart) : ''; + + const signal = parseSignalFromEmail(subject, body); + if (!signal) { + console.log(`⚠️ 跳过无效邮件: ${subject}`); + continue; + } + + console.log(`\n📩 处理信号: ${signal.trace_id} · ${signal.command}`); + + // 生成信号 ID(如邮件中未提供) + if (!signal.signal_id) signal.signal_id = generateSignalId(); + + // 写入 signal-log + const cmdSignal = { + signal_id: signal.signal_id, + trace_id: signal.trace_id, + timestamp: signal.timestamp || new Date().toISOString(), + signal_type: 'GL-CMD', + direction: 'Notion→GitHub', + sender: signal.sender || '霜砚', + receiver: '铸渊', + related_dev: signal.payload?.dev_id || null, + related_module: signal.payload?.module || null, + summary: `执行指令 ${signal.command}`, + payload: signal.payload || {}, + result: null, + ack_signal_id: null + }; + writeSignalLog(cmdSignal); + + // 执行指令 + let execResult; + try { + execResult = await executeCommand(signal); + } catch (err) { + execResult = { result: '失败', detail: err.message }; + } + + cmdSignal.result = execResult.result; + + // 发送 ACK 邮件并写入 ACK 信号日志 + const ackSignalId = generateSignalId(); + cmdSignal.ack_signal_id = ackSignalId; + + try { + const ackPayload = await sendAckEmail( + { ...signal, signal_id: cmdSignal.signal_id }, + ackSignalId, + execResult.result + ); + writeSignalLog({ + signal_id: ackSignalId, + trace_id: signal.trace_id, + timestamp: new Date().toISOString(), + signal_type: 'GL-ACK', + direction: 'GitHub→Notion', + sender: '铸渊', + receiver: '霜砚', + related_dev: cmdSignal.related_dev, + related_module: cmdSignal.related_module, + summary: `ACK: ${signal.command} ${execResult.result}`, + payload: ackPayload, + result: execResult.result, + ack_signal_id: null + }); + pushToNotion({ + signal_id: ackSignalId, + trace_id: signal.trace_id, + timestamp: new Date().toISOString(), + signal_type: 'GL-ACK', + direction: 'GitHub→Notion', + sender: '铸渊', + receiver: '霜砚', + related_dev: cmdSignal.related_dev, + related_module: cmdSignal.related_module, + summary: `ACK: ${signal.command} ${execResult.result}`, + payload: ackPayload + }); + } catch (err) { + console.error(`❌ 发送 ACK 邮件失败: ${err.message}`); + errors++; + } + + // 标记邮件为已读 + await client.messageFlagsAdd(msg.seq, ['\\Seen']); + + processed++; + console.log(`✅ 信号处理完成: ${signal.trace_id} → ${execResult.result}`); + if (execResult.detail) console.log(` 详情: ${execResult.detail}`); + } + } + } finally { + lock.release(); + } + } finally { + await client.logout(); + } + + console.log(`\n═══════════════════════════════════════════════`); + console.log(`📡 ESP 处理完毕 · 处理: ${processed} · 错误: ${errors}`); + console.log(`═══════════════════════════════════════════════`); + + if (errors > 0) process.exit(1); +} + +main().catch(err => { + console.error('❌ ESP 处理器致命错误:', err.message); + process.exit(1); +}); diff --git a/scripts/psp-inspection.js b/scripts/psp-inspection.js new file mode 100644 index 00000000..c92226be --- /dev/null +++ b/scripts/psp-inspection.js @@ -0,0 +1,393 @@ +// scripts/psp-inspection.js +// 铸渊 PSP 分身巡检 · 每日自动检查清单 +// CHK-G01 信号完整性 / CHK-G02 dev-nodes 一致性 / CHK-G03 notion-push 清理 +// CHK-G04 broadcasts 投递 / CHK-G05 CI 健康 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const nodemailer = require('nodemailer'); + +// ── 目录配置 ─────────────────────────────────────────────────────────────── +const ROOT = path.join(__dirname, '..'); +const SIGNAL_LOG_DIR = path.join(ROOT, 'signal-log'); +const SIGNAL_INDEX = path.join(SIGNAL_LOG_DIR, 'index.json'); +const DEV_NODES_DIR = path.join(ROOT, 'dev-nodes'); +const NOTION_PENDING = path.join(ROOT, 'notion-push/pending'); +const BROADCASTS_DIR = path.join(ROOT, 'broadcasts-outbox'); +const MEMORY_PATH = path.join(ROOT, '.github/brain/memory.json'); + +const EMAIL_ADDRESS = process.env.EMAIL_ADDRESS; +const EMAIL_PASSWORD = process.env.EMAIL_APP_PASSWORD; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const REPO = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab'; + +const now = new Date(); +const today = now.toISOString().split('T')[0]; +const results = {}; +const issues = []; +const autoFixed = []; +const MS_PER_HOUR = 3600000; +const PSP_TRACE_ID = `TRC-${today.replace(/-/g, '')}-PSP`; + +// ── 工具函数 ─────────────────────────────────────────────────────────────── +function hoursAgo(isoStr) { + if (!isoStr) return Infinity; + return (Date.now() - new Date(isoStr).getTime()) / MS_PER_HOUR; +} + +function loadJson(filePath) { + if (!fs.existsSync(filePath)) return null; + try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; } +} + +function saveJson(filePath, data) { + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); +} + +// ── 信号 ID 生成 ─────────────────────────────────────────────────────────── +function generateSignalId() { + const ymd = today.replace(/-/g, ''); + const index = loadJson(SIGNAL_INDEX) || { total_count: 0 }; + const seq = String(index.total_count + 1).padStart(3, '0'); + return `SIG-${ymd}-${seq}`; +} + +// ── 写信号日志 ───────────────────────────────────────────────────────────── +function writeSignalLog(signal) { + const dateStr = signal.timestamp.slice(0, 7); + const dir = path.join(SIGNAL_LOG_DIR, dateStr); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, `${signal.signal_id}.json`), JSON.stringify(signal, null, 2)); + + const index = loadJson(SIGNAL_INDEX) || { last_updated: '', total_count: 0, signals: [] }; + index.total_count += 1; + index.last_updated = now.toISOString(); + index.signals.unshift({ + signal_id: signal.signal_id, + trace_id: signal.trace_id, + type: signal.signal_type, + timestamp: signal.timestamp, + summary: signal.summary, + related_dev: signal.related_dev || null, + file: `${dateStr}/${signal.signal_id}.json` + }); + saveJson(SIGNAL_INDEX, index); +} + +// ── 发送 GL-DATA 通知邮件 ────────────────────────────────────────────────── +async function sendDataEmail(subject, payload) { + if (!EMAIL_ADDRESS || !EMAIL_PASSWORD) { + console.log('⚠️ EMAIL 未配置,跳过邮件发送'); + return; + } + const transport = nodemailer.createTransport({ + host: 'smtp.gmail.com', + port: 587, + secure: false, + auth: { user: EMAIL_ADDRESS, pass: EMAIL_PASSWORD } + }); + await transport.sendMail({ + from: EMAIL_ADDRESS, + to: EMAIL_ADDRESS, + subject: `[GL-DATA] ${subject}`, + text: JSON.stringify(payload, null, 2) + }); + console.log(`📧 [GL-DATA] 已发送: ${subject}`); +} + +// ── CHK-G01 信号收发完整性 ───────────────────────────────────────────────── +function checkG01() { + console.log('\n─── CHK-G01 信号收发完整性 ───'); + const index = loadJson(SIGNAL_INDEX); + if (!index) { + results.G01 = '⚠️'; + issues.push('CHK-G01: signal-log/index.json 不存在'); + return; + } + + const cutoff = Date.now() - 24 * MS_PER_HOUR; + const recent = index.signals.filter(s => new Date(s.timestamp).getTime() > cutoff); + const cmds = recent.filter(s => s.type === 'GL-CMD'); + const acks = new Set(recent.filter(s => s.type === 'GL-ACK').map(s => s.trace_id)); + const unacked = cmds.filter(s => !acks.has(s.trace_id)); + + if (unacked.length > 0) { + results.G01 = '❌'; + unacked.forEach(s => issues.push(`CHK-G01: 未收到 ACK · trace_id=${s.trace_id} · ${s.summary}`)); + } else { + results.G01 = '✅'; + console.log(` ✅ 最近24h CMD: ${cmds.length} · ACK 全部到位`); + } +} + +// ── CHK-G02 dev-nodes 状态一致性 ────────────────────────────────────────── +function checkG02() { + console.log('\n─── CHK-G02 dev-nodes 状态一致性 ───'); + if (!fs.existsSync(DEV_NODES_DIR)) { + results.G02 = '❌'; + issues.push('CHK-G02: dev-nodes/ 目录不存在'); + return; + } + + const devDirs = fs.readdirSync(DEV_NODES_DIR).filter(d => d.startsWith('DEV-')); + let allOk = true; + + for (const devId of devDirs) { + const statusPath = path.join(DEV_NODES_DIR, devId, 'status.json'); + const configPath = path.join(DEV_NODES_DIR, devId, 'config.json'); + if (!fs.existsSync(statusPath) || !fs.existsSync(configPath)) { + issues.push(`CHK-G02: ${devId} 缺少 status.json 或 config.json`); + allOk = false; + continue; + } + const status = loadJson(statusPath); + if (!status) { + issues.push(`CHK-G02: ${devId} status.json 解析失败`); + allOk = false; + } + } + + results.G02 = allOk ? '✅' : '⚠️'; + if (allOk) console.log(` ✅ ${devDirs.length} 个开发者节点状态文件完整`); +} + +// ── CHK-G03 notion-push 清理 ─────────────────────────────────────────────── +async function checkG03() { + console.log('\n─── CHK-G03 notion-push/pending 清理 ───'); + if (!fs.existsSync(NOTION_PENDING)) { + results.G03 = '✅'; + console.log(' ✅ notion-push/pending 目录为空或不存在'); + return; + } + + const files = fs.readdirSync(NOTION_PENDING).filter(f => f.endsWith('.json')); + const stale = files.filter(f => { + const stat = fs.statSync(path.join(NOTION_PENDING, f)); + return (Date.now() - stat.mtimeMs) > 24 * MS_PER_HOUR; + }); + + if (stale.length > 0) { + results.G03 = '⚠️'; + issues.push(`CHK-G03: ${stale.length} 个 notion-push/pending 文件超过24h未处理: ${stale.join(', ')}`); + // 发通知 + try { + await sendDataEmail('notion-push/pending 文件积压提醒', { + type: 'GL-CMD', + sender: '铸渊', + command: 'alert_pending_files', + payload: { stale_files: stale, count: stale.length } + }); + } catch (err) { + console.error(` ⚠️ 发送通知失败: ${err.message}`); + } + } else { + results.G03 = '✅'; + console.log(` ✅ pending: ${files.length} 个文件,无超期`); + } +} + +// ── CHK-G04 broadcasts 投递确认 ─────────────────────────────────────────── +function checkG04() { + console.log('\n─── CHK-G04 broadcasts-outbox 投递确认 ───'); + if (!fs.existsSync(BROADCASTS_DIR)) { + results.G04 = '⚠️'; + issues.push('CHK-G04: broadcasts-outbox/ 不存在'); + return; + } + + const devDirs = fs.readdirSync(BROADCASTS_DIR).filter(d => d.startsWith('DEV-')); + let allSynced = true; + + for (const devId of devDirs) { + const srcDir = path.join(BROADCASTS_DIR, devId); + const destDir = path.join(DEV_NODES_DIR, devId); + if (!fs.existsSync(destDir)) continue; + + const broadcasts = fs.readdirSync(srcDir).filter(f => f.endsWith('.json')); + for (const file of broadcasts) { + const dest = path.join(destDir, file); + if (!fs.existsSync(dest)) { + // 自动复制 + fs.copyFileSync(path.join(srcDir, file), dest); + autoFixed.push(`CHK-G04: 自动投递广播 ${devId}/${file}`); + console.log(` 🔧 自动投递: ${devId}/${file}`); + allSynced = false; + } + } + } + + results.G04 = '✅'; + if (allSynced) { + console.log(' ✅ 所有广播已同步至 dev-nodes/'); + } else { + console.log(` ✅ 已自动修复 ${autoFixed.length} 个广播投递`); + } +} + +// ── CHK-G05 CI 健康检查 ─────────────────────────────────────────────────── +async function checkG05() { + console.log('\n─── CHK-G05 CI 健康检查 ───'); + if (!GITHUB_TOKEN) { + results.G05 = '⚠️'; + console.log(' ⚠️ GITHUB_TOKEN 未配置,跳过 CI 检查'); + return; + } + + return new Promise((resolve) => { + const [owner, repoName] = REPO.split('/'); + const options = { + hostname: 'api.github.com', + path: `/repos/${owner}/${repoName}/actions/runs?per_page=20`, + method: 'GET', + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'zhuyuan-psp-inspector' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', () => { + try { + const body = JSON.parse(data); + const runs = body.workflow_runs || []; + const cutoff = Date.now() - 24 * MS_PER_HOUR; + const recent = runs.filter(r => new Date(r.created_at).getTime() > cutoff); + const failed = recent.filter(r => r.conclusion === 'failure'); + + if (failed.length > 0) { + results.G05 = '❌'; + failed.forEach(r => { + issues.push(`CHK-G05: CI 失败 · ${r.name} · ${r.html_url}`); + }); + // 写入 signal-log + const signalId = generateSignalId(); + writeSignalLog({ + signal_id: signalId, + trace_id: PSP_TRACE_ID, + timestamp: now.toISOString(), + signal_type: 'GL-DATA', + direction: 'GitHub→Notion', + sender: '铸渊', + receiver: '霜砚', + related_dev: null, + related_module: null, + summary: `CHK-G05: ${failed.length} 个 CI 失败`, + payload: { failed_runs: failed.map(r => ({ name: r.name, url: r.html_url })) }, + result: '待处理', + ack_signal_id: null + }); + } else { + results.G05 = '✅'; + console.log(` ✅ 最近24h CI: ${recent.length} 次运行,全部通过`); + } + } catch (err) { + results.G05 = '⚠️'; + console.log(` ⚠️ CI 数据解析失败: ${err.message}`); + } + resolve(); + }); + }); + + req.on('error', (err) => { + results.G05 = '⚠️'; + console.log(` ⚠️ GitHub API 请求失败: ${err.message}`); + resolve(); + }); + + req.end(); + }); +} + +// ── 更新 memory.json ─────────────────────────────────────────────────────── +function updateMemory(summary) { + const memory = loadJson(MEMORY_PATH); + if (!memory) return; + + if (!memory.events) memory.events = []; + memory.events.unshift({ + date: today, + type: 'psp_inspection', + description: summary, + by: '铸渊PSP巡检' + }); + if (memory.events.length > 50) memory.events = memory.events.slice(0, 50); + memory.last_updated = now.toISOString(); + + saveJson(MEMORY_PATH, memory); +} + +// ── 写入巡检结果到 signal-log ────────────────────────────────────────────── +function writeInspectionSignal(allPassed) { + const signalId = generateSignalId(); + const summary = allPassed + ? '铸渊 PSP 巡检通过 · 全部检查项 ✅' + : `铸渊 PSP 巡检完成 · 发现 ${issues.length} 个问题 · 自动修复 ${autoFixed.length} 项`; + + writeSignalLog({ + signal_id: signalId, + trace_id: PSP_TRACE_ID, + timestamp: now.toISOString(), + signal_type: 'GL-DATA', + direction: 'GitHub→Notion', + sender: '铸渊', + receiver: '霜砚', + related_dev: null, + related_module: null, + summary, + payload: { + check_results: results, + issues, + auto_fixed: autoFixed + }, + result: allPassed ? '全通过' : '有问题', + ack_signal_id: null + }); + + return summary; +} + +// ── 主流程 ──────────────────────────────────────────────────────────────── +async function main() { + console.log('═══════════════════════════════════════════════════'); + console.log('🔍 铸渊 PSP 分身巡检启动'); + console.log(` 日期: ${today}`); + console.log('═══════════════════════════════════════════════════'); + + checkG01(); + checkG02(); + await checkG03(); + checkG04(); + await checkG05(); + + const allPassed = Object.values(results).every(v => v === '✅'); + const summary = writeInspectionSignal(allPassed); + updateMemory(summary); + + console.log('\n═══════════════════════════════════════════════════'); + console.log('📊 PSP 巡检报告:'); + Object.entries(results).forEach(([k, v]) => console.log(` ${v} CHK-${k}`)); + + if (autoFixed.length > 0) { + console.log('\n🔧 自动修复:'); + autoFixed.forEach(f => console.log(` · ${f}`)); + } + + if (issues.length > 0) { + console.log('\n⚠️ 待处理问题:'); + issues.forEach(i => console.log(` · ${i}`)); + } + + console.log(`\n${allPassed ? '✅ 铸渊 PSP 巡检全部通过' : '⚠️ 铸渊 PSP 巡检发现问题,已记录'}`); + console.log('═══════════════════════════════════════════════════'); +} + +main().catch(err => { + console.error('❌ PSP 巡检致命错误:', err.message); + process.exit(1); +}); diff --git a/scripts/zhuyuan-daily-selfcheck.js b/scripts/zhuyuan-daily-selfcheck.js index 90d988bf..1f197fed 100644 --- a/scripts/zhuyuan-daily-selfcheck.js +++ b/scripts/zhuyuan-daily-selfcheck.js @@ -33,12 +33,15 @@ if (missing.length > 0) { } // === ① bis · 目录结构巡检 === -const requiredDirs = ['syslog-inbox', 'syslog-processed', 'broadcasts-outbox', '.github/brain']; +const requiredDirs = [ + 'syslog-inbox', 'syslog-processed', 'broadcasts-outbox', '.github/brain', + 'signal-log', 'dev-nodes', 'notion-push/pending', 'notion-push/processed' +]; const missingDirs = requiredDirs.filter(d => !fs.existsSync(d)); if (missingDirs.length > 0) { console.error('⚠️ 缺失目录:' + missingDirs.join(', ')); } else { - console.log('✅ 目录结构巡检:syslog-inbox / syslog-processed / broadcasts-outbox 全部就绪'); + console.log('✅ 目录结构巡检:syslog-inbox / syslog-processed / broadcasts-outbox / signal-log / dev-nodes / notion-push 全部就绪'); } // 统计 syslog-inbox 待处理条目 diff --git a/signal-log/2026-03/SIG-2026-0306-001.json b/signal-log/2026-03/SIG-2026-0306-001.json new file mode 100644 index 00000000..dc7cfb47 --- /dev/null +++ b/signal-log/2026-03/SIG-2026-0306-001.json @@ -0,0 +1,18 @@ +{ + "signal_id": "SIG-2026-0306-001", + "trace_id": "TRC-2026-0306-001", + "timestamp": "2026-03-06T23:00:00+08:00", + "signal_type": "GL-CMD", + "direction": "Notion→GitHub", + "sender": "霜砚", + "receiver": "铸渊", + "related_dev": null, + "related_module": null, + "summary": "AGE OS 系统初始化 · 铸渊 GitHub 端激活", + "payload": { + "command": "system_init", + "version": "1.0" + }, + "result": "成功", + "ack_signal_id": null +} diff --git a/signal-log/2026-03/SIG-20260307-002.json b/signal-log/2026-03/SIG-20260307-002.json new file mode 100644 index 00000000..fc57a25d --- /dev/null +++ b/signal-log/2026-03/SIG-20260307-002.json @@ -0,0 +1,27 @@ +{ + "signal_id": "SIG-20260307-002", + "trace_id": "TRC-20260307-PSP", + "timestamp": "2026-03-07T04:45:55.670Z", + "signal_type": "GL-DATA", + "direction": "GitHub→Notion", + "sender": "铸渊", + "receiver": "霜砚", + "related_dev": null, + "related_module": null, + "summary": "铸渊 PSP 巡检完成 · 发现 1 个问题 · 自动修复 0 项", + "payload": { + "check_results": { + "G01": "❌", + "G02": "✅", + "G03": "✅", + "G04": "✅", + "G05": "⚠️" + }, + "issues": [ + "CHK-G01: 未收到 ACK · trace_id=TRC-2026-0306-001 · AGE OS 系统初始化 · 铸渊 GitHub 端激活" + ], + "auto_fixed": [] + }, + "result": "有问题", + "ack_signal_id": null +} \ No newline at end of file diff --git a/signal-log/index.json b/signal-log/index.json new file mode 100644 index 00000000..76156707 --- /dev/null +++ b/signal-log/index.json @@ -0,0 +1,25 @@ +{ + "description": "铸渊信号日志目录索引 · AGE OS ESP 邮件信号协议", + "last_updated": "2026-03-07T04:45:55.670Z", + "total_count": 2, + "signals": [ + { + "signal_id": "SIG-20260307-002", + "trace_id": "TRC-20260307-PSP", + "type": "GL-DATA", + "timestamp": "2026-03-07T04:45:55.670Z", + "summary": "铸渊 PSP 巡检完成 · 发现 1 个问题 · 自动修复 0 项", + "related_dev": null, + "file": "2026-03/SIG-20260307-002.json" + }, + { + "signal_id": "SIG-2026-0306-001", + "trace_id": "TRC-2026-0306-001", + "type": "GL-CMD", + "timestamp": "2026-03-06T23:00:00+08:00", + "summary": "AGE OS 系统初始化 · 铸渊 GitHub 端激活", + "related_dev": null, + "file": "2026-03/SIG-2026-0306-001.json" + } + ] +} \ No newline at end of file