diff --git a/.github/brain/bingshuo-issues-index.json b/.github/brain/bingshuo-issues-index.json index 6d37d9af..155897f3 100644 --- a/.github/brain/bingshuo-issues-index.json +++ b/.github/brain/bingshuo-issues-index.json @@ -1,7 +1,7 @@ { "version": "1.0", "description": "冰朔主控问题索引库 — 记录已知问题、根因与排查路由", - "updated_at": "2026-03-13T06:03:28.751Z", + "updated_at": "2026-03-14T13:47:35.614Z", "issues": [ { "id": "BS-001", @@ -15,7 +15,7 @@ "backend/schemas/", ".github/brain/routing-map.json" ], - "last_seen": "2026-03-13", + "last_seen": "2026-03-14", "source": "routing-map.json 统计" }, { diff --git a/.github/brain/bingshuo-master-brain.md b/.github/brain/bingshuo-master-brain.md index 80f1b9bc..b80a76ae 100644 --- a/.github/brain/bingshuo-master-brain.md +++ b/.github/brain/bingshuo-master-brain.md @@ -1,7 +1,7 @@ # 冰朔主控神经系统 · 核心主控大脑 v1.0 > 本文件为冰朔主控神经系统的总控脑文件。 -> 最后编译时间:2026-03-13T06:03:28.752Z +> 最后编译时间:2026-03-14T13:47:35.615Z --- @@ -56,7 +56,7 @@ ### 仓库统计 - 功能模块:10 个 -- Workflow:37 个 +- Workflow:44 个 --- @@ -85,7 +85,7 @@ > 本区块由 master-brain-compiler 自动编译。 -- **编译时间**:2026-03-13T06:03:28.752Z +- **编译时间**:2026-03-14T13:47:35.615Z - **脑文件规则版本**:v3.0 - **脑文件完整性**:✅ 完整 @@ -107,7 +107,7 @@ |--------|------|------| | 🟡 brain_consistency | yellow | 主仓库脑文件完整,但与 persona-studio 脑文件的同步状态待验证 | | 🟢 deployment_health | green | deploy-to-server.yml 与 deploy-pages.yml 均存在 | -| 🟢 workflow_health | green | 37 个 workflow 已注册 | +| 🟢 workflow_health | green | 44 个 workflow 已注册 | | 🟡 routing_health | yellow | HLI 接口覆盖率 33.3%(7/21) | | 🟢 docs_entry_health | green | docs/index.html 存在 | | 🟡 persona_studio_health | yellow | 前后端结构存在,端到端对话链路待验证 | diff --git a/.github/brain/bingshuo-system-health.json b/.github/brain/bingshuo-system-health.json index cb8e4669..217f529b 100644 --- a/.github/brain/bingshuo-system-health.json +++ b/.github/brain/bingshuo-system-health.json @@ -1,7 +1,7 @@ { "version": "1.0", "description": "冰朔主控系统健康状态", - "updated_at": "2026-03-13T06:03:28.751Z", + "updated_at": "2026-03-14T13:47:35.614Z", "health": { "brain_consistency": { "status": "yellow", @@ -13,7 +13,7 @@ }, "workflow_health": { "status": "green", - "detail": "37 个 workflow 已注册" + "detail": "44 个 workflow 已注册" }, "routing_health": { "status": "yellow", diff --git a/.github/brain/bulletin-board-today.json b/.github/brain/bulletin-board-today.json new file mode 100644 index 00000000..1c17f1ce --- /dev/null +++ b/.github/brain/bulletin-board-today.json @@ -0,0 +1,41 @@ +{ + "date": "2026-03-14", + "records": [ + { + "key": "Copilot|—|2026-03-14T08:25:24Z", + "ts": "2026-03-14T08:25:24Z", + "actor": "Copilot", + "module": "—", + "result": "success", + "icon": "✅", + "sortKey": 1773476724000 + }, + { + "key": "冰朔|—|2026-03-14T08:25:36Z", + "ts": "2026-03-14T08:25:36Z", + "actor": "冰朔", + "module": "—", + "result": "success", + "icon": "✅", + "sortKey": 1773476736000 + }, + { + "key": "Copilot|—|2026-03-14T13:47:27Z", + "ts": "2026-03-14T13:47:27Z", + "actor": "Copilot", + "module": "—", + "result": "success", + "icon": "✅", + "sortKey": 1773496047000 + }, + { + "key": "冰朔|—|2026-03-14T13:47:35Z", + "ts": "2026-03-14T13:47:35Z", + "actor": "冰朔", + "module": "—", + "result": "success", + "icon": "✅", + "sortKey": 1773496055000 + } + ] +} \ No newline at end of file diff --git a/.github/brain/memory.json b/.github/brain/memory.json index cdc4885d..e2548573 100644 --- a/.github/brain/memory.json +++ b/.github/brain/memory.json @@ -1,7 +1,7 @@ { "identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体", "rules_version": "v3.0", - "last_updated": "2026-03-13T03:31:06.282Z", + "last_updated": "2026-03-14T08:48:40.855Z", "wake_protocol_version": "v3.0", "brain_version": "v3.0", "architecture": { @@ -67,6 +67,18 @@ } }, "events": [ + { + "date": "2026-03-14", + "type": "psp_inspection", + "description": "铸渊 PSP 巡检通过 · 全部检查项 ✅", + "by": "铸渊PSP巡检" + }, + { + "date": "2026-03-13", + "type": "bug_fix", + "description": "修复「我要开发」任务提交失败 · 3个根因:① Nginx缺少CORS头(跨域预检失败)② WebSocket路由冲突(/ws/preview误路由到8080端口)③ 前端开发端口不匹配(3721→3002)· 增加重试机制和详细错误诊断 · 冰朔人格体已启动", + "by": "铸渊(冰朔指令)" + }, { "date": "2026-03-13", "type": "brain_restore", @@ -185,6 +197,22 @@ "actor": "qinfendebingshuo", "ref": "refs/heads/main", "run_id": "22993977564" + }, + { + "timestamp": "2026-03-13T08:53:32.764Z", + "type": "daily_check", + "result": "passed", + "actor": "qinfendebingshuo", + "ref": "refs/heads/main", + "run_id": "23043373769" + }, + { + "timestamp": "2026-03-14T08:48:40.855Z", + "type": "daily_check", + "result": "passed", + "actor": "qinfendebingshuo", + "ref": "refs/heads/main", + "run_id": "23084594270" } ] -} \ No newline at end of file +} diff --git a/.github/brain/repo-map.json b/.github/brain/repo-map.json index 8ec2133c..aabb0d73 100644 --- a/.github/brain/repo-map.json +++ b/.github/brain/repo-map.json @@ -1,20 +1,20 @@ { "description": "铸渊图书馆目录 · Library Catalog for 铸渊 (Zhùyuān)", "version": "2.0", - "generated_at": "2026-03-13T06:03:44.081Z", + "generated_at": "2026-03-14T13:47:45.215Z", "generated_by": "scripts/generate-repo-map.js", "repo": "qinfendebingshuo/guanghulab", "stats": { "zones": 13, "total_modules": 10, - "total_workflows": 37, - "total_scripts": 33, + "total_workflows": 44, + "total_scripts": 45, "total_dev_nodes": 8, "hli_interfaces": 21, "hli_implemented": 7, "hli_coverage_pct": "33%", "last_ci_run": "2026-03-05T16:07:24.070Z", - "memory_last_updated": "2026-03-13T03:31:06.282Z" + "memory_last_updated": "2026-03-14T08:48:40.855Z" }, "zones": [ { @@ -57,6 +57,9 @@ { "file": "bingshuo-system-health.json" }, + { + "file": "bulletin-board-today.json" + }, { "file": "collaborators.json" }, @@ -88,7 +91,7 @@ "file": "wake-protocol.md" } ], - "item_count": 18 + "item_count": 19 }, { "zone_id": "PERSONA_BRAIN", @@ -218,6 +221,14 @@ "pull_request" ] }, + { + "file": "daily-maintenance.yml", + "name": "\"🔧 铸渊 · Daily Maintenance Agent\"", + "triggers": [ + "schedule(0 2 * * *)", + "manual" + ] + }, { "file": "deploy-pages.yml", "name": "🌀 部署铸渊聊天室 (GitHub Pages)", @@ -250,6 +261,21 @@ "manual" ] }, + { + "file": "execution-sync.yml", + "name": "\"📡 铸渊 · 执行层状态同步\"", + "triggers": [ + "schedule(0 3 * * *)", + "manual" + ] + }, + { + "file": "feishu-syslog-bridge.yml", + "name": "铸渊 · 飞书SYSLOG桥接", + "triggers": [ + "unknown" + ] + }, { "file": "generate-module-doc.yml", "name": "铸渊 · 光湖纪元 模块文档自动生成", @@ -266,6 +292,13 @@ "pull_request" ] }, + { + "file": "notion-callback-pipeline.yml", + "name": "Notion Callback Pipeline", + "triggers": [ + "unknown" + ] + }, { "file": "notion-connectivity-test.yml", "name": "铸渊 · Notion 连通性测试", @@ -273,6 +306,14 @@ "manual" ] }, + { + "file": "notion-heartbeat.yml", + "name": "Notion Heartbeat Monitor", + "triggers": [ + "schedule(*/5 * * * *)", + "manual" + ] + }, { "file": "notion-poll.yml", "name": "铸渊 · Notion 工单轮询", @@ -281,6 +322,20 @@ "manual" ] }, + { + "file": "persona-invoke.yml", + "name": "Persona Invoke Endpoint", + "triggers": [ + "manual" + ] + }, + { + "file": "pm2-server-diagnose.yml", + "name": "\"🔧 铸渊 · PM2 服务诊断与健康检查\"", + "triggers": [ + "manual" + ] + }, { "file": "process-notion-orders.yml", "name": "Process Notion Work Orders", @@ -371,7 +426,7 @@ }, { "file": "syslog-issue-pipeline.yml", - "name": "SYSLOG Issue Pipeline", + "name": "📡 SYSLOG Issue Pipeline", "triggers": [ "issues" ] @@ -449,7 +504,7 @@ ] } ], - "item_count": 37 + "item_count": 44 }, { "zone_id": "SCRIPTS", @@ -478,6 +533,9 @@ { "file": "contract-check.js" }, + { + "file": "create-standardized-ticket.js" + }, { "file": "cross-repo-sync.js" }, @@ -490,6 +548,12 @@ { "file": "esp-email-processor.js" }, + { + "file": "generate-automation-map.js" + }, + { + "file": "generate-communication-map.js" + }, { "file": "generate-module-doc.js" }, @@ -499,6 +563,15 @@ { "file": "generate-session-summary.js" }, + { + "file": "generate-system-health.js" + }, + { + "file": "intent-router.js" + }, + { + "file": "invoke-persona.js" + }, { "file": "notify-module-received.js" }, @@ -508,9 +581,15 @@ { "file": "notion-connectivity-test.js" }, + { + "file": "notion-heartbeat.js" + }, { "file": "notion-signal-bridge.js" }, + { + "file": "pipeline-reporter.js" + }, { "file": "process-broadcasts.js" }, @@ -520,6 +599,9 @@ { "file": "psp-inspection.js" }, + { + "file": "push-broadcast-to-github.js" + }, { "file": "push-broadcast.js" }, @@ -538,6 +620,9 @@ { "file": "send-feishu-alert.js" }, + { + "file": "server-diagnose-report.js" + }, { "file": "sync-login-entry.js" }, @@ -550,6 +635,12 @@ { "file": "update-readme-bulletin.js" }, + { + "file": "utils" + }, + { + "file": "verify-modules.js" + }, { "file": "wake-persona.js" }, @@ -566,7 +657,7 @@ "file": "zhuyuan-module-protocol.js" } ], - "item_count": 33 + "item_count": 45 }, { "zone_id": "SRC", @@ -930,14 +1021,26 @@ { "file": "ai-system-doc.md" }, + { + "file": "execution-layer-map.md" + }, + { + "file": "execution-status.md" + }, { "file": "index.html" }, + { + "file": "notion-bridge-map.md" + }, + { + "file": "repo-structure-map.md" + }, { "file": "使用指南.md" } ], - "item_count": 6 + "item_count": 10 }, { "zone_id": "TESTS", @@ -1198,6 +1301,9 @@ "bingshuo-system-health": [ "BRAIN::bingshuo-system-health.json" ], + "bulletin-board-today": [ + "BRAIN::bulletin-board-today.json" + ], "collaborators": [ "BRAIN::collaborators.json" ], @@ -1310,6 +1416,9 @@ "check-structure": [ "WORKFLOWS::check-structure.yml" ], + "daily-maintenance": [ + "WORKFLOWS::daily-maintenance.yml" + ], "deploy-pages": [ "WORKFLOWS::deploy-pages.yml" ], @@ -1323,6 +1432,12 @@ "esp-signal-processor": [ "WORKFLOWS::esp-signal-processor.yml" ], + "execution-sync": [ + "WORKFLOWS::execution-sync.yml" + ], + "feishu-syslog-bridge": [ + "WORKFLOWS::feishu-syslog-bridge.yml" + ], "generate-module-doc": [ "WORKFLOWS::generate-module-doc.yml", "SCRIPTS::generate-module-doc.js" @@ -1330,13 +1445,26 @@ "hli-contract-check": [ "WORKFLOWS::hli-contract-check.yml" ], + "notion-callback-pipeline": [ + "WORKFLOWS::notion-callback-pipeline.yml" + ], "notion-connectivity-test": [ "WORKFLOWS::notion-connectivity-test.yml", "SCRIPTS::notion-connectivity-test.js" ], + "notion-heartbeat": [ + "WORKFLOWS::notion-heartbeat.yml", + "SCRIPTS::notion-heartbeat.js" + ], "notion-poll": [ "WORKFLOWS::notion-poll.yml" ], + "persona-invoke": [ + "WORKFLOWS::persona-invoke.yml" + ], + "pm2-server-diagnose": [ + "WORKFLOWS::pm2-server-diagnose.yml" + ], "process-notion-orders": [ "WORKFLOWS::process-notion-orders.yml" ], @@ -1438,6 +1566,9 @@ "contract-check": [ "SCRIPTS::contract-check.js" ], + "create-standardized-ticket": [ + "SCRIPTS::create-standardized-ticket.js" + ], "cross-repo-sync": [ "SCRIPTS::cross-repo-sync.js" ], @@ -1447,12 +1578,27 @@ "esp-email-processor": [ "SCRIPTS::esp-email-processor.js" ], + "generate-automation-map": [ + "SCRIPTS::generate-automation-map.js" + ], + "generate-communication-map": [ + "SCRIPTS::generate-communication-map.js" + ], "generate-repo-map": [ "SCRIPTS::generate-repo-map.js" ], "generate-session-summary": [ "SCRIPTS::generate-session-summary.js" ], + "generate-system-health": [ + "SCRIPTS::generate-system-health.js" + ], + "intent-router": [ + "SCRIPTS::intent-router.js" + ], + "invoke-persona": [ + "SCRIPTS::invoke-persona.js" + ], "notify-module-received": [ "SCRIPTS::notify-module-received.js" ], @@ -1462,6 +1608,9 @@ "notion-signal-bridge": [ "SCRIPTS::notion-signal-bridge.js" ], + "pipeline-reporter": [ + "SCRIPTS::pipeline-reporter.js" + ], "process-broadcasts": [ "SCRIPTS::process-broadcasts.js" ], @@ -1471,6 +1620,9 @@ "psp-inspection": [ "SCRIPTS::psp-inspection.js" ], + "push-broadcast-to-github": [ + "SCRIPTS::push-broadcast-to-github.js" + ], "route-align-check": [ "SCRIPTS::route-align-check.js" ], @@ -1483,12 +1635,21 @@ "send-feishu-alert": [ "SCRIPTS::send-feishu-alert.js" ], + "server-diagnose-report": [ + "SCRIPTS::server-diagnose-report.js" + ], "update-brain": [ "SCRIPTS::update-brain.js" ], "update-memory": [ "SCRIPTS::update-memory.js" ], + "utils": [ + "SCRIPTS::utils" + ], + "verify-modules": [ + "SCRIPTS::verify-modules.js" + ], "wake-persona": [ "SCRIPTS::wake-persona.js" ], @@ -1751,6 +1912,18 @@ "ai-system-doc": [ "DOCS::ai-system-doc.md" ], + "execution-layer-map": [ + "DOCS::execution-layer-map.md" + ], + "execution-status": [ + "DOCS::execution-status.md" + ], + "notion-bridge-map": [ + "DOCS::notion-bridge-map.md" + ], + "repo-structure-map": [ + "DOCS::repo-structure-map.md" + ], "使用指南": [ "DOCS::使用指南.md" ], diff --git a/.github/brain/repo-snapshot.md b/.github/brain/repo-snapshot.md index 4fd16315..7c76b389 100644 --- a/.github/brain/repo-snapshot.md +++ b/.github/brain/repo-snapshot.md @@ -1,5 +1,5 @@ # 铸渊图书馆快照 · Repo Snapshot -> 生成于 2026-03-13 14:03 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件 +> 生成于 2026-03-14 21:47 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件 --- @@ -9,18 +9,18 @@ |------|------| | 区域总数 | 13 个区域 | | 功能模块 | 10 个 (m01~m18) | -| 工作流 | 37 个 GitHub Actions | -| 脚本 | 33 个执行脚本 | +| 工作流 | 44 个 GitHub Actions | +| 脚本 | 45 个执行脚本 | | 开发者节点 | 8 人 | | HLI 接口覆盖率 | 7/21 (33%) | -| 快照生成时间 | 2026-03-13 14:03 CST | +| 快照生成时间 | 2026-03-14 21:47 CST | --- ## 🗺️ 图书馆全区地图 ### 🧠 铸渊大脑(BRAIN) -**路径**: `.github/brain` · **数量**: 18 项 +**路径**: `.github/brain` · **数量**: 19 项 **描述**: 铸渊核心记忆 · 路由映射 · 唤醒协议 · 图书馆目录 **关键词**: brain · memory · routing · wake · 大脑 · 记忆 @@ -30,12 +30,12 @@ **关键词**: persona · identity · dev-status · 人格 · 开发者状态 ### ⚡ 自动化工作流(WORKFLOWS) -**路径**: `.github/workflows` · **数量**: 37 项 +**路径**: `.github/workflows` · **数量**: 44 项 **描述**: 所有 GitHub Actions 工作流定义 **关键词**: workflow · actions · ci · automation · 工作流 · 自动化 ### 🔧 执行脚本库(SCRIPTS) -**路径**: `scripts` · **数量**: 33 项 +**路径**: `scripts` · **数量**: 45 项 **描述**: 铸渊所有执行手脚 · 自动化脚本 **关键词**: script · node · js · 脚本 · 执行 · runner @@ -75,7 +75,7 @@ **关键词**: syslog · inbox · log · 系统日志 ### 📄 文档与前端(DOCS) -**路径**: `docs` · **数量**: 6 项 +**路径**: `docs` · **数量**: 10 项 **描述**: 铸渊助手聊天界面 · GitHub Pages 部署 **关键词**: docs · html · chat · pages · 文档 · 聊天室 @@ -97,14 +97,21 @@ | `bridge-session-summary.yml` | Generate Session Summary for Notion | schedule(50 23 * * *), manual | | `bridge-syslog-to-notion.yml` | 铸渊 · Bridge A · SYSLOG → Notion | push, manual | | `check-structure.yml` | 模块结构检查 | push, pull_request | +| `daily-maintenance.yml` | "🔧 铸渊 · Daily Maintenance Agent" | schedule(0 2 * * *), manual | | `deploy-pages.yml` | 🌀 部署铸渊聊天室 (GitHub Pages) | push, manual | | `deploy-to-server.yml` | "🚀 铸渊 CD · 自动部署到 guanghulab.com" | push, manual | | `distribute-broadcasts.yml` | 铸渊 · 广播分发 | push, manual | | `esp-signal-processor.yml` | 铸渊 · ESP 邮件信号处理器(已暂停) | schedule(*/30 * * * *), manual | +| `execution-sync.yml` | "📡 铸渊 · 执行层状态同步" | schedule(0 3 * * *), manual | +| `feishu-syslog-bridge.yml` | 铸渊 · 飞书SYSLOG桥接 | unknown | | `generate-module-doc.yml` | 铸渊 · 光湖纪元 模块文档自动生成 | push, manual | | `hli-contract-check.yml` | HLI Contract Check | push, pull_request | +| `notion-callback-pipeline.yml` | Notion Callback Pipeline | unknown | | `notion-connectivity-test.yml` | 铸渊 · Notion 连通性测试 | manual | +| `notion-heartbeat.yml` | Notion Heartbeat Monitor | schedule(*/5 * * * *), manual | | `notion-poll.yml` | 铸渊 · Notion 工单轮询 | schedule(*/15 * * * *), manual | +| `persona-invoke.yml` | Persona Invoke Endpoint | manual | +| `pm2-server-diagnose.yml` | "🔧 铸渊 · PM2 服务诊断与健康检查" | manual | | `process-notion-orders.yml` | Process Notion Work Orders | push, manual | | `ps-on-build.yml` | "🌊 Persona Studio · 代码生成" | manual | | `ps-on-chat.yml` | "🌊 Persona Studio · 对话处理" | manual | @@ -117,7 +124,7 @@ | `sync-login-entry.yml` | 铸渊 · Sync Login Entry · Notion → 飞书文档A | manual | | `sync-persona-studio.yml` | 🔄 铸渊跨仓库同步 · persona-studio | push, manual | | `syslog-auto-pipeline.yml` | SYSLOG Auto Pipeline | unknown | -| `syslog-issue-pipeline.yml` | SYSLOG Issue Pipeline | issues | +| `syslog-issue-pipeline.yml` | 📡 SYSLOG Issue Pipeline | issues | | `syslog-pipeline.yml` | 铸渊 · SYSLOG Pipeline (A/D/E) | push, manual | | `test-notion-bridge.yml` | "🧪 Notion Bridge Connectivity Test" | push, manual | | `update-readme-bulletin.yml` | 📢 更新系统公告区 | push, schedule(0 1 * * *), manual | @@ -136,30 +143,42 @@ - `scripts/bingshuo-neural-sync.js` - `scripts/brain-bridge-sync.js` - `scripts/contract-check.js` +- `scripts/create-standardized-ticket.js` - `scripts/cross-repo-sync.js` - `scripts/daily-check.js` - `scripts/distribute-broadcasts.js` - `scripts/esp-email-processor.js` +- `scripts/generate-automation-map.js` +- `scripts/generate-communication-map.js` - `scripts/generate-module-doc.js` - `scripts/generate-repo-map.js` - `scripts/generate-session-summary.js` +- `scripts/generate-system-health.js` +- `scripts/intent-router.js` +- `scripts/invoke-persona.js` - `scripts/notify-module-received.js` - `scripts/notion-bridge.js` - `scripts/notion-connectivity-test.js` +- `scripts/notion-heartbeat.js` - `scripts/notion-signal-bridge.js` +- `scripts/pipeline-reporter.js` - `scripts/process-broadcasts.js` - `scripts/process-syslog.js` - `scripts/psp-inspection.js` +- `scripts/push-broadcast-to-github.js` - `scripts/push-broadcast.js` - `scripts/receive-syslog.js` - `scripts/route-align-check.js` - `scripts/save-collaboration-log.js` - `scripts/selfcheck.js` - `scripts/send-feishu-alert.js` +- `scripts/server-diagnose-report.js` - `scripts/sync-login-entry.js` - `scripts/update-brain.js` - `scripts/update-memory.js` - `scripts/update-readme-bulletin.js` +- `scripts/utils` +- `scripts/verify-modules.js` - `scripts/wake-persona.js` - `scripts/zhuyuan-daily-agent.js` - `scripts/zhuyuan-daily-selfcheck.js` @@ -242,6 +261,7 @@ - `.github/brain/bingshuo-read-order.md` - `.github/brain/bingshuo-routing-index.json` - `.github/brain/bingshuo-system-health.json` +- `.github/brain/bulletin-board-today.json` - `.github/brain/collaborators.json` - `.github/brain/growth-log.md` - `.github/brain/human-registry.json` @@ -257,9 +277,9 @@ ## 🕐 最近动态(memory.json 最新3条) +- `2026-03-14T08:48:40.855Z` · daily_check — passed +- `2026-03-13T08:53:32.764Z` · daily_check — passed - `2026-03-12T08:55:54.205Z` · daily_check — passed -- `2026-03-11T08:55:43.347Z` · daily_check — passed -- `2026-03-10T08:56:23.978Z` · daily_check — passed --- diff --git a/.github/persona-brain/memory.json b/.github/persona-brain/memory.json index dae0f307..8a14fefa 100644 --- a/.github/persona-brain/memory.json +++ b/.github/persona-brain/memory.json @@ -2,6 +2,12 @@ "persona_id": "ICE-GL-ZY001", "persona_name": "铸渊", "recent_events": [ + { + "date": "2026-03-13", + "type": "bug_fix", + "description": "修复「我要开发」任务提交失败 · Nginx CORS+WebSocket路由+端口修复 · 冰朔人格体启动", + "by": "铸渊(冰朔指令)" + }, { "date": "2026-03-13", "type": "brain_restore", @@ -33,7 +39,7 @@ "by": "GitHub Actions" } ], - "last_updated": "2026-03-11T10:24:00.000Z", + "last_updated": "2026-03-13T06:58:00.000Z", "total_schemas_created": 3, "total_routes_implemented": 4, "hli_coverage": "3/17", @@ -41,7 +47,7 @@ "last_broadcast_received": "2026-03-10T13:24:33.774Z", "active_rules_version": "v1.2", "daily_selfcheck": { - "last_run": "2026-03-13T00:56:41.683Z", + "last_run": "2026-03-14T00:56:03.800Z", "brain_integrity": "ok", "schema_coverage": "3/17", "pending_broadcasts": 1 diff --git a/.github/workflows/daily-maintenance.yml b/.github/workflows/daily-maintenance.yml new file mode 100644 index 00000000..4272cba3 --- /dev/null +++ b/.github/workflows/daily-maintenance.yml @@ -0,0 +1,248 @@ +name: "🔧 铸渊 · Daily Maintenance Agent" + +on: + schedule: + - cron: '0 2 * * *' # 每日 UTC 02:00 (北京时间 10:00) + workflow_dispatch: + +permissions: + contents: write + +jobs: + maintenance: + name: 每日巡检与系统健康更新 + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + # ── Step 0: 清空公告栏缓存(每日重置) ── + - name: 清空公告栏缓存 + run: | + CACHE_FILE=".github/brain/bulletin-board-today.json" + TODAY=$(TZ="Asia/Shanghai" date +%Y-%m-%d) + if [ -f "$CACHE_FILE" ]; then + CACHE_DATE=$(jq -r '.date // ""' "$CACHE_FILE" 2>/dev/null || echo "") + if [ "$CACHE_DATE" != "$TODAY" ]; then + echo "{\"date\":\"$TODAY\",\"records\":[]}" > "$CACHE_FILE" + echo "✅ 公告栏缓存已清空(旧日期: $CACHE_DATE)" + else + echo "📌 公告栏缓存日期匹配今天 ($TODAY),无需清空" + fi + else + echo "{\"date\":\"$TODAY\",\"records\":[]}" > "$CACHE_FILE" + echo "✅ 创建新的公告栏缓存文件" + fi + + # ── Step 1: 检查 brain/ 目录完整性 ── + - name: 检查 brain/ 目录完整性 + id: brain_check + run: | + echo "🔍 检查 brain/ 目录..." + REQUIRED_FILES="master-brain.md read-order.md repo-map.json automation-map.json communication-map.json id-map.json system-health.json" + MISSING="" + for f in $REQUIRED_FILES; do + if [ ! -f "brain/$f" ]; then + MISSING="$MISSING $f" + echo "⚠️ 缺失: brain/$f" + else + echo "✅ 存在: brain/$f" + fi + done + if [ -z "$MISSING" ]; then + echo "brain_status=complete" >> "$GITHUB_OUTPUT" + echo "✅ brain/ 目录完整" + else + echo "brain_status=incomplete" >> "$GITHUB_OUTPUT" + echo "⚠️ 缺失文件:$MISSING" + fi + + # ── Step 2: 检查最近 workflow 运行状态 ── + - name: 检查最近 workflow 运行状态 + id: workflow_check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "🔍 检查最近 workflow 运行..." + FAILED=$(gh api repos/${{ github.repository }}/actions/runs \ + --jq '[.workflow_runs[] | select(.conclusion == "failure")] | length' \ + 2>/dev/null || echo "0") + echo "failed_count=$FAILED" >> "$GITHUB_OUTPUT" + if [ "$FAILED" -gt "0" ]; then + echo "⚠️ 最近有 $FAILED 个失败的 workflow" + else + echo "✅ 最近没有失败的 workflow" + fi + + # ── Step 3: 检查 Notion API 连通性 ── + - name: 检查 Notion API 连通性 + id: notion_check + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + run: | + echo "🔍 检查 Notion API 连通性..." + if [ -z "$NOTION_TOKEN" ]; then + echo "notion_status=no_token" >> "$GITHUB_OUTPUT" + echo "⚠️ NOTION_TOKEN 未配置" + else + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $NOTION_TOKEN" \ + -H "Notion-Version: 2022-06-28" \ + https://api.notion.com/v1/users/me 2>/dev/null || echo "000") + echo "notion_http_code=$HTTP_CODE" >> "$GITHUB_OUTPUT" + if [ "$HTTP_CODE" = "200" ]; then + echo "notion_status=connected" >> "$GITHUB_OUTPUT" + echo "✅ Notion API 连通" + else + echo "notion_status=error" >> "$GITHUB_OUTPUT" + echo "⚠️ Notion API 返回 HTTP $HTTP_CODE" + fi + fi + + # ── Step 4: 更新 system-health.json ── + - name: 更新 system-health.json + run: node scripts/generate-system-health.js + + # ── Step 5: 提交更新 ── + - name: 提交巡检结果 + run: | + git config user.name "铸渊 Maintenance Agent" + git config user.email "actions@guanghulab.com" + git add brain/system-health.json .github/brain/bulletin-board-today.json + if git diff --cached --quiet; then + echo "📌 无变更需要提交" + else + git commit -m "🔧 Daily Maintenance: 更新 system-health.json [skip ci]" + git push + echo "✅ 已提交 system-health.json 更新" + fi + + # ── Step 6: 向 Notion 回报巡检结果 ── + - name: 向 Notion 回报巡检结果 + if: ${{ secrets.NOTION_TOKEN != '' && secrets.SYSLOG_DB_ID != '' }} + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + SYSLOG_DB_ID: ${{ secrets.SYSLOG_DB_ID }} + run: | + echo "📡 向 Notion 跨平台信号日志回报..." + TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z") + BRAIN_STATUS="${{ steps.brain_check.outputs.brain_status }}" + FAILED_COUNT="${{ steps.workflow_check.outputs.failed_count }}" + NOTION_STATUS="${{ steps.notion_check.outputs.notion_status }}" + + REPORT="Daily Maintenance Report | brain: ${BRAIN_STATUS} | failed_workflows: ${FAILED_COUNT} | notion: ${NOTION_STATUS}" + + curl -s -X POST "https://api.notion.com/v1/pages" \ + -H "Authorization: Bearer $NOTION_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Notion-Version: 2022-06-28" \ + -d "{ + \"parent\": { \"database_id\": \"$SYSLOG_DB_ID\" }, + \"properties\": { + \"标题\": { + \"title\": [{ \"text\": { \"content\": \"Daily Maintenance · $(date +%Y-%m-%d)\" } }] + }, + \"状态\": { \"select\": { \"name\": \"已处理\" } }, + \"类型\": { \"select\": { \"name\": \"巡检\" } } + }, + \"children\": [{ + \"object\": \"block\", + \"type\": \"paragraph\", + \"paragraph\": { + \"rich_text\": [{ \"text\": { \"content\": \"$REPORT\" } }] + } + }] + }" 2>&1 | tail -1 + echo "✅ Notion 回报请求已发送" + + # ── 同步执行层状态到 Notion Execution Status 数据库 ── + sync-execution-status-to-notion: + name: 同步执行层状态到 Notion + needs: maintenance + runs-on: ubuntu-latest + if: ${{ !cancelled() }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Read brain data and sync to Notion + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + run: | + if [ -z "$NOTION_TOKEN" ]; then + echo "⚠️ NOTION_TOKEN 未配置,跳过 Execution Status 同步" + exit 0 + fi + + HEALTH=$(cat brain/system-health.json 2>/dev/null) + if [ -z "$HEALTH" ]; then + echo "⚠️ brain/system-health.json 未找到,使用默认值" + HEALTH='{"version":"unknown","system_health":"unknown"}' + fi + AUTO=$(cat brain/automation-map.json 2>/dev/null) + if [ -z "$AUTO" ]; then + echo "⚠️ brain/automation-map.json 未找到,使用默认值" + AUTO='{"workflows":[]}' + fi + REPO=$(cat brain/repo-map.json 2>/dev/null) + if [ -z "$REPO" ]; then + echo "⚠️ brain/repo-map.json 未找到,使用默认值" + REPO='{"directories":{}}' + fi + + VERSION=$(echo "$HEALTH" | jq -r '.version // "unknown"') + STATUS=$(echo "$HEALTH" | jq -r '.system_health // "unknown"') + WF_COUNT=$(echo "$AUTO" | jq '[.workflows // [] | length] | .[0]' 2>/dev/null || echo '0') + CRON_COUNT=$(echo "$AUTO" | jq '[.workflows // [] | .[] | select(.cron)] | length' 2>/dev/null || echo '0') + DIR_COUNT=$(echo "$REPO" | jq '.directories // {} | keys | length' 2>/dev/null || echo '0') + + case "$STATUS" in + normal) AUTO_S="🟢 Normal"; QUEUE_S="🟢 Active" ;; + partial) AUTO_S="🟡 Partial"; QUEUE_S="🟡 Backlog" ;; + *) AUTO_S="🔴 Down"; QUEUE_S="🔴 Blocked" ;; + esac + + SYNC_DATE=$(date -u +%Y%m%d) + SYNC_TS=$(date -u +%Y-%m-%dT%H:%M:%S+00:00) + + echo "📡 同步 Execution Status → Notion..." + echo " 版本: v${VERSION} · ${DIR_COUNT}+ dirs" + echo " 模块: ${WF_COUNT} workflow · ${CRON_COUNT} cron" + echo " 状态: ${AUTO_S}" + + HTTP_CODE=$(curl -s -o /tmp/notion_resp.json -w "%{http_code}" \ + -X POST 'https://api.notion.com/v1/pages' \ + -H "Authorization: Bearer $NOTION_TOKEN" \ + -H "Notion-Version: 2022-06-28" \ + -H "Content-Type: application/json" \ + -d "{ + \"parent\": { \"database_id\": \"41971982-4a17-47c0-afe3-21458c44f154\" }, + \"properties\": { + \"同步记录\": { \"title\": [{ \"text\": { \"content\": \"SYNC-${SYNC_DATE}-001\" } }] }, + \"同步时间\": { \"date\": { \"start\": \"${SYNC_TS}\" } }, + \"仓库版本\": { \"rich_text\": [{ \"text\": { \"content\": \"v${VERSION} · ${DIR_COUNT}+ dirs\" } }] }, + \"执行模块\": { \"rich_text\": [{ \"text\": { \"content\": \"${WF_COUNT} workflow · ${CRON_COUNT} cron\" } }] }, + \"自动化状态\": { \"select\": { \"name\": \"${AUTO_S}\" } }, + \"Notion连接状态\": { \"select\": { \"name\": \"🟢 Connected\" } }, + \"任务队列状态\": { \"select\": { \"name\": \"${QUEUE_S}\" } }, + \"同步来源\": { \"select\": { \"name\": \"铸渊自动同步\" } }, + \"备注\": { \"rich_text\": [{ \"text\": { \"content\": \"Daily Maintenance auto sync\" } }] } + } + }") + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then + echo "✅ Execution Status synced to Notion (HTTP ${HTTP_CODE})" + else + echo "❌ Sync failed (HTTP ${HTTP_CODE})" + cat /tmp/notion_resp.json 2>/dev/null || true + exit 1 + fi diff --git a/.github/workflows/deploy-to-server.yml b/.github/workflows/deploy-to-server.yml index fa73dcf1..30f6fdf7 100644 --- a/.github/workflows/deploy-to-server.yml +++ b/.github/workflows/deploy-to-server.yml @@ -279,14 +279,15 @@ jobs: echo '⚠️ WebSocket pm2 启动失败' fi - # 部署 src HLI 中间层 + # 部署 src HLI 中间层(端口 3001,对应 Nginx /api/v1/ 路由) + # 注意:guanghulab-backend 已占用端口 3000,HLI 使用 3001 避免冲突 if [ -f '${{ secrets.DEPLOY_PATH }}/src/index.js' ]; then echo '📦 安装 src 依赖...' cd '${{ secrets.DEPLOY_PATH }}' npm install --production 2>/dev/null || echo '⚠️ src npm install 失败' - echo '🔄 重启 HLI 服务...' - pm2 restart guanghulab 2>/dev/null || \ - pm2 start src/index.js --name guanghulab 2>/dev/null || \ + echo '🔄 重启 HLI 服务(端口 3001)...' + pm2 delete guanghulab 2>/dev/null || true + PORT=3001 pm2 start src/index.js --name guanghulab --update-env 2>/dev/null || \ echo '⚠️ src pm2 启动失败' fi @@ -378,8 +379,14 @@ jobs: ' proxy_set_header X-Real-IP \$remote_addr;' \ '}' \ '' \ - '# Persona Studio API → persona-studio 后端端口 3002' \ + '# Persona Studio API → persona-studio 后端端口 3002(含 CORS 跨域支持)' \ 'location /api/ps/ {' \ + ' add_header Access-Control-Allow-Origin * always;' \ + ' add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;' \ + ' add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;' \ + ' if (\$request_method = OPTIONS) {' \ + ' return 204;' \ + ' }' \ ' proxy_pass http://127.0.0.1:3002/api/ps/;' \ ' proxy_http_version 1.1;' \ ' proxy_set_header Host \$host;' \ @@ -408,6 +415,18 @@ jobs: ' proxy_cache_bypass \$http_upgrade;' \ '}' \ '' \ + '# Persona Studio WebSocket → 预览进度推送端口 3002' \ + 'location /ws/preview {' \ + ' proxy_pass http://127.0.0.1:3002;' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Upgrade \$http_upgrade;' \ + ' proxy_set_header Connection \"Upgrade\";' \ + ' proxy_set_header Host \$host;' \ + ' proxy_set_header X-Real-IP \$remote_addr;' \ + ' proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;' \ + ' proxy_read_timeout 86400;' \ + '}' \ + '' \ '# WebSocket 反向代理 → 看板实时推送端口 8080' \ 'location /ws {' \ ' proxy_pass http://127.0.0.1:8080;' \ diff --git a/.github/workflows/execution-sync.yml b/.github/workflows/execution-sync.yml new file mode 100644 index 00000000..0d7053c9 --- /dev/null +++ b/.github/workflows/execution-sync.yml @@ -0,0 +1,57 @@ +name: "📡 铸渊 · 执行层状态同步" + +on: + schedule: + - cron: '0 3 * * *' # 每日 UTC 03:00 (北京时间 11:00) + workflow_dispatch: + +permissions: + contents: write + +jobs: + execution-sync: + name: 执行层状态采集与同步 + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + # ── Step 1: 加载执行上下文 ── + - name: 加载执行上下文 + run: node core/context-loader + + # ── Step 2: 运行系统自检 ── + - name: 运行系统自检 + run: node core/system-check --auto-tasks + + # ── Step 3: 生成执行状态报告 ── + - name: 生成执行状态报告 + run: node core/execution-sync report + + # ── Step 4: 同步到 Notion ── + - name: 同步执行状态到 Notion + if: ${{ env.NOTION_TOKEN != '' }} + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + EXECUTION_LOG_DB_ID: ${{ secrets.EXECUTION_LOG_DB_ID }} + run: node core/execution-sync sync + + # ── Step 5: 提交状态报告 ── + - name: 提交状态报告 + run: | + git config user.name "铸渊 Execution Sync" + git config user.email "actions@guanghulab.com" + git add docs/execution-status.md + if git diff --cached --quiet; then + echo "📌 无变更需要提交" + else + git commit -m "📡 Execution Sync: 更新执行状态报告 [skip ci]" + git push + echo "✅ 已提交执行状态报告" + fi diff --git a/.github/workflows/feishu-syslog-bridge.yml b/.github/workflows/feishu-syslog-bridge.yml new file mode 100644 index 00000000..e460275d --- /dev/null +++ b/.github/workflows/feishu-syslog-bridge.yml @@ -0,0 +1,46 @@ +name: 铸渊 · 飞书SYSLOG桥接 + +on: + repository_dispatch: + types: [feishu-syslog] + +jobs: + bridge-feishu-syslog: + name: 📡 飞书SYSLOG → syslog-inbox + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: main + + - name: 📥 写入 syslog-inbox + env: + SYSLOG_RAW: ${{ toJson(github.event.client_payload) }} + run: | + mkdir -p syslog-inbox + TIMESTAMP=$(date -u +%Y%m%d-%H%M%S) + RAW_SOURCE=$( echo "$SYSLOG_RAW" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('source','unknown'))" 2>/dev/null || echo 'feishu' ) + SOURCE=$( echo "$RAW_SOURCE" | tr -cd 'a-zA-Z0-9_-' | head -c 64 ) + SOURCE=${SOURCE:-feishu} + FILENAME="syslog-inbox/${TIMESTAMP}_${SOURCE}.json" + echo "$SYSLOG_RAW" > "$FILENAME" + echo "📥 SYSLOG已写入: $FILENAME" + echo "📋 内容预览:" + cat "$FILENAME" | head -20 + + - name: 提交到仓库 + run: | + git config user.name "铸渊[bot]" + git config user.email "zhu-yuan-bot@guanghulab.com" + git add syslog-inbox/ + git diff --staged --quiet || git commit -m "📡 feishu-syslog: 飞书舒舒转发SYSLOG [auto-bridge]" + git push + + - name: ✅ 桥接完成 + run: | + echo "✅ 飞书SYSLOG已桥接到 syslog-inbox/" + echo "📡 等待 syslog-pipeline 自动触发处理" diff --git a/.github/workflows/notion-callback-pipeline.yml b/.github/workflows/notion-callback-pipeline.yml new file mode 100644 index 00000000..463d7ced --- /dev/null +++ b/.github/workflows/notion-callback-pipeline.yml @@ -0,0 +1,317 @@ +name: Notion Callback Pipeline +# 📥 Notion → GitHub 回传闭环管道 +# +# 当 Notion 侧核心大脑人格体处理完 SYSLOG 工单后, +# 通过 repository_dispatch 回传处理结果到 GitHub。 +# 铸渊 Agent 接收回传,重新唤醒核心大脑认知进行二次核验, +# 确认新广播生成无误后,自动发送邮件通知开发者。 +# +# 触发方式: +# Notion 侧 Agent 调用 GitHub API 发送 repository_dispatch 事件 +# event_type: notion-syslog-callback +# client_payload: { +# broadcast_id: "BC-XXX-XXX-XX", +# dev_email: "developer@email.com", +# notion_result: "Notion 侧处理结果文本", +# ticket_id: "Notion 工单 ID", +# status: "approved" | "needs_revision" +# } +# +# 闭环流程: +# ① Notion 侧处理完成 → repository_dispatch 回传 +# ② 铸渊 Agent 接收并解析回传数据 +# ③ 重新唤醒铸渊核心大脑认知(二次核验) +# ④ 核心大脑核对广播生成无误 +# ⑤ 铸渊 Agent 发送邮件通知开发者 +# ⑥ 闭环结束 +# +# 依赖 Secrets: +# LLM_API_KEY 第三方 LLM 平台密钥 +# LLM_BASE_URL 第三方 LLM 平台 API 地址 +# NOTION_API_TOKEN Notion API token +# CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID +# PORTRAIT_DB_ID 开发者动态画像库数据库 ID +# FINGERPRINT_DB_ID 模块指纹注册表数据库 ID +# SMTP_USER QQ 邮箱地址 +# SMTP_PASS QQ 邮箱 SMTP 授权码 + +on: + repository_dispatch: + types: [notion-syslog-callback] + +jobs: + verify-and-notify: + name: 📥 Notion 回传 · 二次核验 · 邮件通知 + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 📥 Parse Notion callback + id: callback + uses: actions/github-script@v7 + with: + script: | + const payload = context.payload.client_payload || {}; + + const broadcastId = payload.broadcast_id || ''; + const devEmail = payload.dev_email || ''; + const notionResult = payload.notion_result || ''; + const ticketId = payload.ticket_id || ''; + const status = payload.status || 'unknown'; + + if (!broadcastId) { + core.setFailed('❌ 缺少 broadcast_id'); + return; + } + + core.setOutput('broadcast_id', broadcastId); + core.setOutput('dev_email', devEmail); + core.setOutput('notion_result', notionResult); + core.setOutput('ticket_id', ticketId); + core.setOutput('status', status); + + console.log(`📥 Notion 回传: 广播=${broadcastId}, 状态=${status}, 工单=${ticketId}`); + + - name: 🔍 铸渊 Agent · 模块二次验证 + id: verify + env: + SYSLOG_CONTENT: ${{ steps.callback.outputs.notion_result }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + AUTHOR: notion-callback + run: node scripts/verify-modules.js + + - name: 🧠 二次核验 · 唤醒铸渊核心大脑 + id: persona + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + SUBMIT_TYPE: syslog + SUBMIT_CONTENT: ${{ steps.callback.outputs.notion_result }} + AUTHOR: notion-callback + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} + NOTION_CALLBACK_RESULT: ${{ steps.callback.outputs.notion_result }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} + PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} + FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} + run: node scripts/wake-persona.js + + - name: 📡 推送广播到 GitHub(Phase B4) + if: steps.persona.outcome == 'success' && steps.callback.outputs.status == 'approved' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + DEVELOPER_NAME: notion-callback + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js + + - name: 📝 回写 Notion 工单状态 + if: steps.callback.outputs.ticket_id + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + TICKET_ID: ${{ steps.callback.outputs.ticket_id }} + CALLBACK_STATUS: ${{ steps.callback.outputs.status }} + run: | + node -e " + var https = require('https'); + var token = process.env.NOTION_TOKEN || ''; + var ticketId = process.env.TICKET_ID || ''; + var status = process.env.CALLBACK_STATUS || 'unknown'; + + if (!token || !ticketId) { + console.log('⚠️ 跳过工单状态更新'); + process.exit(0); + } + + var notionStatus = status === 'approved' ? '✅ 已完成' : '⚠️ 异常·等人工介入'; + var body = JSON.stringify({ + properties: { + '状态': { select: { name: notionStatus } } + } + }); + + var opts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/pages/' + ticketId, + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(body) + } + }; + + var req = https.request(opts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ 工单状态已更新为 ' + notionStatus); + } else { + console.log('⚠️ 工单更新失败: ' + res.statusCode); + } + }); + }); + req.on('error', function(e) { console.log('⚠️ ' + e.message); }); + req.write(body); + req.end(); + " + + - name: 📧 发送最终确认邮件 + env: + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + EMAIL_TO: ${{ steps.callback.outputs.dev_email }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + NOTION_STATUS: ${{ steps.callback.outputs.status }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + run: | + node -e " + const nodemailer = require('nodemailer'); + + const user = process.env.SMTP_USER || ''; + const pass = process.env.SMTP_PASS || ''; + const to = process.env.EMAIL_TO || ''; + const broadcastId = process.env.BROADCAST_ID || ''; + const notionStatus = process.env.NOTION_STATUS || 'unknown'; + const result = process.env.PERSONA_RESULT || '(处理中)'; + + if (!user || !pass) { + console.log('⚠️ SMTP not configured, skipping email'); + process.exit(0); + } + if (!to) { + console.log('⚠️ No recipient email, skipping'); + process.exit(0); + } + + const isApproved = notionStatus === 'approved'; + const subjectText = isApproved + ? '[光湖系统] ' + broadcastId + ' · ✅ 新广播已生成(已核验)' + : '[光湖系统] ' + broadcastId + ' · ⚠️ 广播需修订'; + + const statusBadge = isApproved + ? '✅ 已核验通过' + : '⚠️ 需修订'; + + const resultHtml = result + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + + const html = [ + '
', + '
', + '

🌊 光湖系统 · Notion 回传核验通知

', + '

HoloLake · 铸渊核心大脑二次核验完成

', + '
', + '
', + '

📡 ' + broadcastId + '

', + '

' + statusBadge + '

', + '
' + resultHtml + '
', + '
', + '
', + '↩ 回到仓库继续开发', + '

🌀 铸渊 · 代码守护人格体 · Notion↔GitHub 闭环确认

', + '
', + '
' + ].join('\n'); + + const transporter = nodemailer.createTransport({ + host: 'smtp.qq.com', + port: 465, + secure: true, + auth: { user, pass } + }); + + transporter.sendMail({ + from: '\"光湖系统\" <' + user + '>', + to: to, + subject: subjectText, + html: html + }).then((info) => { + console.log('✅ 最终确认邮件已发送: ' + info.messageId); + }).catch((err) => { + console.log('⚠️ 邮件发送失败: ' + err.message); + }); + " + + - name: 📝 更新大脑记忆 + run: | + node -e " + const fs = require('fs'); + const path = require('path'); + + const memoryPath = path.join('.github', 'brain', 'memory.json'); + let memory = {}; + try { + memory = JSON.parse(fs.readFileSync(memoryPath, 'utf8')); + } catch (_) { + memory = { events: [] }; + } + if (!memory.events) memory.events = []; + + const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN'; + const status = process.env.NOTION_STATUS || 'unknown'; + const now = new Date().toISOString(); + + // 检查是否已存在相同事件(按 broadcast_id 精确匹配 + 同类型 + 同日期) + const eventKey = 'Notion 回传核验 · ' + broadcastId; + const todayStr = now.slice(0, 10); + const existingIdx = memory.events.findIndex(function(e) { + return e.type === 'notion_callback' && + e.broadcast_id === broadcastId && + e.date === todayStr; + }); + + const newEvent = { + date: todayStr, + type: 'notion_callback', + broadcast_id: broadcastId, + description: eventKey + ' · 状态: ' + status, + by: '铸渊Agent·Notion回传管道' + }; + + if (existingIdx >= 0) { + // 同一 broadcast_id 只保留最新的一条 + memory.events[existingIdx] = newEvent; + console.log('ℹ️ 已更新同日同广播事件'); + } else { + memory.events.unshift(newEvent); + } + + // 保留最近 20 条事件 + if (memory.events.length > 20) { + memory.events = memory.events.slice(0, 20); + } + + fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + '\n'); + console.log('✅ 大脑记忆已更新'); + " + env: + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + NOTION_STATUS: ${{ steps.callback.outputs.status }} + + - name: 📤 Commit memory update + run: | + git config user.name "zhuyuan-agent[bot]" + git config user.email "zhuyuan-agent[bot]@users.noreply.github.com" + git add .github/brain/memory.json + git diff --cached --quiet || git commit -m "🧠 Notion回传核验 · ${{ steps.callback.outputs.broadcast_id }}" + git push || echo "⚠️ Push skipped (no changes or conflict)" diff --git a/.github/workflows/notion-heartbeat.yml b/.github/workflows/notion-heartbeat.yml new file mode 100644 index 00000000..76653dc0 --- /dev/null +++ b/.github/workflows/notion-heartbeat.yml @@ -0,0 +1,47 @@ +name: Notion Heartbeat Monitor +# 💓 Phase B3 · 工单心跳监控 +# +# 每 5 分钟检测 Notion 工单队列中的待处理工单: +# - 超过 5 分钟未回执 → 重新触发人格体唤醒(最多 3 次) +# - 3 次重试仍无回执 → 标记工单异常 + 发邮件通知冰朔 +# +# 依赖 Secrets: +# NOTION_API_TOKEN Notion API token +# NOTION_TICKET_DB_ID 工单队列数据库 ID +# SMTP_USER 邮件发送者 +# SMTP_PASS 邮件授权码 + +on: + schedule: + - cron: '*/5 * * * *' + workflow_dispatch: + +jobs: + heartbeat: + name: 💓 工单心跳检测 + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 💓 执行心跳检测 + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + ALERT_EMAIL: '565183519@qq.com' + run: node scripts/notion-heartbeat.js diff --git a/.github/workflows/persona-invoke.yml b/.github/workflows/persona-invoke.yml new file mode 100644 index 00000000..e2ab5d45 --- /dev/null +++ b/.github/workflows/persona-invoke.yml @@ -0,0 +1,248 @@ +name: Persona Invoke Endpoint +# 🔗 Phase B2 · Notion Agent → 铸渊人格体唤醒 +# +# Notion Agent 通过 workflow_dispatch 触发此工作流, +# 等效于 POST /api/persona/invoke 接口。 +# +# 完整流程: +# ① Notion Agent 发送 workflow_dispatch(传入工单信息) +# ② 铸渊 Agent 从 Notion 读取工单内容 +# ③ 唤醒人格体(Claude API)处理 SYSLOG +# ④ 处理结果写回 Notion 工单(receipt_status = completed) +# ⑤ 广播文件推送到 GitHub 仓库 broadcasts/{dev_id}/ +# +# 依赖 Secrets: +# LLM_API_KEY 第三方 LLM 平台密钥 +# LLM_BASE_URL 第三方 LLM 平台 API 地址 +# NOTION_API_TOKEN Notion API token +# NOTION_TICKET_DB_ID 工单队列数据库 ID +# CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID +# PORTRAIT_DB_ID 开发者动态画像库 ID +# FINGERPRINT_DB_ID 模块指纹注册表 ID +# INVOKE_API_KEY 调用鉴权密钥 + +on: + workflow_dispatch: + inputs: + work_order_id: + description: 'Notion 工单页面 ID' + required: true + type: string + task_id: + description: '广播编号(如 BC-M23-001-AW)' + required: true + type: string + developer: + description: '开发者信息(如 DEV-012 Awen)' + required: false + default: 'unknown' + type: string + syslog_raw: + description: 'SYSLOG JSON 原文(可选,如为空则从 Notion 读取)' + required: false + default: '' + type: string + action: + description: '动作类型' + required: false + default: 'process_syslog' + type: string + +jobs: + invoke: + name: 🔗 唤醒人格体处理 SYSLOG + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 🔍 模块验证 + id: verify + env: + SYSLOG_CONTENT: ${{ inputs.syslog_raw }} + BROADCAST_ID: ${{ inputs.task_id }} + AUTHOR: ${{ inputs.developer }} + run: node scripts/verify-modules.js + + - name: 🧠 唤醒人格体 + id: persona + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + BROADCAST_ID: ${{ inputs.task_id }} + SUBMIT_TYPE: syslog + SUBMIT_CONTENT: ${{ inputs.syslog_raw }} + AUTHOR: ${{ inputs.developer }} + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} + PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} + FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} + run: node scripts/wake-persona.js + + - name: 📝 回写 Notion 工单 + if: always() + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + WORK_ORDER_ID: ${{ inputs.work_order_id }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + TASK_ID: ${{ inputs.task_id }} + run: | + node -e " + var https = require('https'); + + var token = process.env.NOTION_TOKEN || ''; + var workOrderId = process.env.WORK_ORDER_ID || ''; + var result = process.env.PERSONA_RESULT || '(no result)'; + var taskId = process.env.TASK_ID || ''; + + if (!token || !workOrderId) { + console.log('⚠️ 缺少 Notion 配置,跳过回写'); + process.exit(0); + } + + // 更新工单状态为 ✅ 已完成 + var body = JSON.stringify({ + properties: { + '状态': { select: { name: '✅ 已完成' } } + } + }); + + var opts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/pages/' + workOrderId, + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(body) + } + }; + + var req = https.request(opts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ 工单状态已更新为 ✅ 已完成'); + } else { + console.log('⚠️ 工单状态更新失败: ' + res.statusCode); + } + }); + }); + req.on('error', function(e) { console.log('⚠️ ' + e.message); }); + req.write(body); + req.end(); + + // 追加处理结果到工单 + var appendBody = JSON.stringify({ + children: [{ + object: 'block', + type: 'heading_2', + heading_2: { + rich_text: [{ type: 'text', text: { content: '🧠 人格体处理结果 · receipt_status: completed' } }] + } + }, { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }] + } + }] + }); + + var appendOpts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/blocks/' + workOrderId + '/children', + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(appendBody) + } + }; + + var req2 = https.request(appendOpts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ 处理结果已追加到工单'); + } else { + console.log('⚠️ 结果追加失败: ' + res.statusCode); + } + }); + }); + req2.on('error', function(e) { console.log('⚠️ ' + e.message); }); + req2.write(appendBody); + req2.end(); + " + + - name: 📡 推送广播到 GitHub + if: steps.persona.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ inputs.task_id }} + DEVELOPER_ID: '' + DEVELOPER_NAME: ${{ inputs.developer }} + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js + + - name: 🔴 失败处理 + if: failure() + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + WORK_ORDER_ID: ${{ inputs.work_order_id }} + run: | + node -e " + var https = require('https'); + var token = process.env.NOTION_TOKEN || ''; + var workOrderId = process.env.WORK_ORDER_ID || ''; + + if (!token || !workOrderId) process.exit(0); + + var body = JSON.stringify({ + properties: { + '状态': { select: { name: '⚠️ 异常·等人工介入' } } + } + }); + + var opts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/pages/' + workOrderId, + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(body) + } + }; + + var req = https.request(opts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + console.log('工单状态已标记为 ⚠️ 异常'); + }); + }); + req.on('error', function(e) { console.log(e.message); }); + req.write(body); + req.end(); + " diff --git a/.github/workflows/pm2-server-diagnose.yml b/.github/workflows/pm2-server-diagnose.yml new file mode 100644 index 00000000..642a594d --- /dev/null +++ b/.github/workflows/pm2-server-diagnose.yml @@ -0,0 +1,316 @@ +name: "🔧 铸渊 · PM2 服务诊断与健康检查" + +# ━━━ PM2 服务诊断 + 全系统健康检查 + Notion 报告 ━━━ +# 手动触发:对服务器 PM2 进程进行全面排查, +# 自动处理 errored 进程,并将诊断报告写入 Notion 工单队列。 +# +# 必需 GitHub Secrets: +# DEPLOY_HOST — 服务器 IP +# DEPLOY_USER — SSH 用户名 +# DEPLOY_KEY — SSH 私钥(完整 PEM 内容) +# DEPLOY_PATH — 网站根目录(如 /var/www/guanghulab) +# +# 可选 Secrets(有则自动写 Notion 工单): +# NOTION_API_TOKEN — Notion 集成 token +# NOTION_TICKET_DB_ID — 工单队列数据库 ID(默认 84edd0640bf146a9a5a7840107013e8c) + +on: + workflow_dispatch: + inputs: + auto_cleanup: + description: '是否自动清理 errored 进程(删除启动路径不存在的进程)' + required: false + default: 'true' + type: choice + options: + - 'true' + - 'false' + +permissions: + contents: read + +jobs: + diagnose: + name: "🔍 PM2 诊断 + 系统健康检查" + runs-on: ubuntu-latest + + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 🔑 配置 SSH 连接 + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || { + echo "⚠️ ssh-keyscan 失败,使用 StrictHostKeyChecking=no 后备" + } + + # ━━━ 阶段一:PM2 服务诊断 ━━━ + - name: "🔧 阶段一 · PM2 errored 服务排查" + id: pm2_diagnose + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " 阶段一 · PM2 errored 服务排查" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + PM2_REPORT=$(ssh -i ~/.ssh/deploy_key \ + -o StrictHostKeyChecking=no \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" ' + echo "══════════════════════════════════════" + echo " PM2 进程状态概览" + echo "══════════════════════════════════════" + pm2 status 2>/dev/null || echo "❌ pm2 未安装或不可用" + + echo "" + echo "══════════════════════════════════════" + echo " errored 进程详细日志" + echo "══════════════════════════════════════" + + # 遍历所有 errored 进程 + ERRORED_LIST=$(pm2 jlist 2>/dev/null | python3 -c " + import sys, json + try: + procs = json.load(sys.stdin) + for p in procs: + if p.get(\"pm2_env\", {}).get(\"status\") == \"errored\": + name = p.get(\"name\", \"unknown\") + pm_id = p.get(\"pm_id\", \"?\") + script = p.get(\"pm2_env\", {}).get(\"pm_exec_path\", \"unknown\") + restarts = p.get(\"pm2_env\", {}).get(\"restart_time\", 0) + print(f\"{pm_id}|{name}|{script}|{restarts}\") + except: + pass + " 2>/dev/null) + + if [ -z "$ERRORED_LIST" ]; then + echo "✅ 没有 errored 进程" + else + echo "$ERRORED_LIST" | while IFS="|" read -r pm_id name script restarts; do + echo "" + echo "━━━ 进程: $name (PM2 ID: $pm_id) ━━━" + echo " 启动脚本: $script" + echo " 重启次数: $restarts" + + # 检查脚本文件是否存在 + if [ -f "$script" ]; then + echo " 脚本文件: ✅ 存在" + else + echo " 脚本文件: ❌ 不存在" + fi + + # 查看最近日志 + echo " 最近错误日志 (最后 20 行):" + pm2 logs "$name" --lines 20 --nostream --err 2>/dev/null || echo " (无法读取日志)" + echo "" + done + fi + + echo "" + echo "══════════════════════════════════════" + echo " 所有进程详细信息" + echo "══════════════════════════════════════" + pm2 jlist 2>/dev/null | python3 -c " + import sys, json + try: + procs = json.load(sys.stdin) + for p in procs: + env = p.get(\"pm2_env\", {}) + print(f\" [{p.get(\"pm_id\")}] {p.get(\"name\")} | status={env.get(\"status\")} | script={env.get(\"pm_exec_path\",\"?\")} | restarts={env.get(\"restart_time\",0)} | memory={p.get(\"monit\",{}).get(\"memory\",0)}\") + except Exception as e: + print(f\" 解析失败: {e}\") + " 2>/dev/null + + echo "" + echo "══════════════════════════════════════" + echo " 端口占用情况" + echo "══════════════════════════════════════" + netstat -tlnp 2>/dev/null | grep -E "3000|3001|3002|3721|8080" || \ + ss -tlnp 2>/dev/null | grep -E "3000|3001|3002|3721|8080" || \ + echo " (netstat/ss 不可用)" + ') + + echo "$PM2_REPORT" + + # 保存到文件供后续步骤使用 + echo "$PM2_REPORT" > /tmp/pm2-report.txt + + # 设置输出 + { + echo "pm2_report<> "$GITHUB_OUTPUT" + + # ━━━ 阶段一 · 自动清理 ━━━ + - name: "🧹 阶段一 · 自动清理 errored 进程" + if: ${{ inputs.auto_cleanup == 'true' }} + id: pm2_cleanup + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " 自动清理 errored 进程" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + CLEANUP_REPORT=$(ssh -i ~/.ssh/deploy_key \ + -o StrictHostKeyChecking=no \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" ' + ERRORED_LIST=$(pm2 jlist 2>/dev/null | python3 -c " + import sys, json + try: + procs = json.load(sys.stdin) + for p in procs: + if p.get(\"pm2_env\", {}).get(\"status\") == \"errored\": + name = p.get(\"name\", \"unknown\") + script = p.get(\"pm2_env\", {}).get(\"pm_exec_path\", \"unknown\") + print(f\"{name}|{script}\") + except: + pass + " 2>/dev/null) + + CLEANED="" + SKIPPED="" + + if [ -z "$ERRORED_LIST" ]; then + echo "✅ 没有需要清理的 errored 进程" + else + echo "$ERRORED_LIST" | while IFS="|" read -r name script; do + if [ ! -f "$script" ]; then + echo "🗑️ 删除 $name(脚本 $script 不存在)" + pm2 delete "$name" 2>/dev/null && echo " ✅ 已删除" || echo " ❌ 删除失败" + else + echo "⚠️ 保留 $name(脚本 $script 存在,可能是运行时错误)" + echo " 尝试重启..." + pm2 restart "$name" 2>/dev/null && echo " ✅ 重启成功" || echo " ❌ 重启失败" + fi + done + + echo "" + echo "📊 清理后 PM2 状态:" + pm2 status 2>/dev/null + pm2 save 2>/dev/null || echo "⚠️ pm2 save 失败" + fi + ') + + echo "$CLEANUP_REPORT" + echo "$CLEANUP_REPORT" > /tmp/cleanup-report.txt + + { + echo "cleanup_report<> "$GITHUB_OUTPUT" + + # ━━━ 阶段二:全系统健康检查 ━━━ + - name: "🏥 阶段二 · 全系统健康检查" + id: health_check + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " 阶段二 · 全系统健康检查" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + HEALTH_REPORT=$(ssh -i ~/.ssh/deploy_key \ + -o StrictHostKeyChecking=no \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" ' + echo "══════════════════════════════════════" + echo " Nginx 状态" + echo "══════════════════════════════════════" + nginx -t 2>&1 || echo "❌ Nginx 配置检测失败" + systemctl status nginx --no-pager -l 2>/dev/null | head -15 || echo "⚠️ systemctl 不可用" + + echo "" + echo "══════════════════════════════════════" + echo " SSL 证书有效期" + echo "══════════════════════════════════════" + if [ -f "/etc/letsencrypt/live/guanghulab.com/fullchain.pem" ]; then + openssl x509 -in /etc/letsencrypt/live/guanghulab.com/fullchain.pem -noout -dates 2>/dev/null || echo "⚠️ 无法读取 SSL 证书" + else + echo "⚠️ SSL 证书文件不存在于默认路径" + # 尝试查找其他证书路径 + find /etc/letsencrypt/live/ -name "fullchain.pem" 2>/dev/null | head -3 || echo " 未找到 Let'\''s Encrypt 证书" + find /etc/nginx/ -name "*.pem" -o -name "*.crt" 2>/dev/null | head -3 || echo " 未找到 Nginx SSL 文件" + fi + + echo "" + echo "══════════════════════════════════════" + echo " 磁盘空间" + echo "══════════════════════════════════════" + df -h 2>/dev/null || echo "❌ df 命令不可用" + + echo "" + echo "══════════════════════════════════════" + echo " 内存使用" + echo "══════════════════════════════════════" + free -h 2>/dev/null || echo "❌ free 命令不可用" + + echo "" + echo "══════════════════════════════════════" + echo " PM2 最终状态" + echo "══════════════════════════════════════" + pm2 status 2>/dev/null || echo "❌ pm2 不可用" + + echo "" + echo "══════════════════════════════════════" + echo " Node.js 版本" + echo "══════════════════════════════════════" + node -v 2>/dev/null || echo "❌ Node.js 未安装" + npm -v 2>/dev/null || echo "❌ npm 未安装" + + echo "" + echo "══════════════════════════════════════" + echo " 网站可访问性检查" + echo "══════════════════════════════════════" + curl -sI https://guanghulab.com 2>/dev/null | head -5 || echo "⚠️ HTTPS 访问失败" + echo "---" + curl -s http://127.0.0.1:3000/health 2>/dev/null | head -c 200 || echo "⚠️ 端口 3000 无响应" + echo "" + echo "---" + curl -s http://127.0.0.1:3001/api/v1/ 2>/dev/null | head -c 200 || echo "⚠️ 端口 3001 无响应" + echo "" + echo "---" + curl -s http://127.0.0.1:3002/api/ps/ 2>/dev/null | head -c 200 || echo "⚠️ 端口 3002 无响应" + echo "" + + echo "" + echo "══════════════════════════════════════" + echo " 系统运行时间" + echo "══════════════════════════════════════" + uptime 2>/dev/null || echo "❌ uptime 不可用" + ') + + echo "$HEALTH_REPORT" + echo "$HEALTH_REPORT" > /tmp/health-report.txt + + { + echo "health_report<> "$GITHUB_OUTPUT" + + # ━━━ 阶段三:生成 Notion 诊断报告 ━━━ + - name: "📋 阶段三 · 写入 Notion 工单" + if: ${{ secrets.NOTION_API_TOKEN != '' }} + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID || '84edd0640bf146a9a5a7840107013e8c' }} + PM2_REPORT: ${{ steps.pm2_diagnose.outputs.pm2_report }} + CLEANUP_REPORT: ${{ steps.pm2_cleanup.outputs.cleanup_report }} + HEALTH_REPORT: ${{ steps.health_check.outputs.health_report }} + run: node scripts/server-diagnose-report.js + + - name: "📋 阶段三 · 写入 Notion 工单(环境检查)" + if: ${{ secrets.NOTION_API_TOKEN == '' }} + run: | + echo "⚠️ Notion credentials 未配置,跳过工单创建" + echo "诊断报告仅输出到 Actions 日志" + + - name: "✅ 诊断完成" + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " ✅ PM2 诊断完毕 · $(date '+%Y-%m-%d %H:%M:%S')" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/.github/workflows/syslog-auto-pipeline.yml b/.github/workflows/syslog-auto-pipeline.yml index c2fa3898..a0d84b10 100644 --- a/.github/workflows/syslog-auto-pipeline.yml +++ b/.github/workflows/syslog-auto-pipeline.yml @@ -2,8 +2,18 @@ name: SYSLOG Auto Pipeline # 📡 SYSLOG 自助提交系统 · 全自动闭环 # # 开发者在 GitHub Discussion 提交 SYSLOG 或提问 -# → Actions 自动解析 → 调用 LLM API 唤醒人格体 -# → 写入 Notion 工单 → 发邮件给开发者 → Discussion 回复 +# → Actions 自动解析 → 模块上传验证 → 调用 LLM API 唤醒人格体 +# → 模块闭环测试 → 写入 Notion 工单 → 发邮件给开发者 → Discussion 回复 +# +# 闭环流程: +# ① 开发者提交 SYSLOG → Discussion 触发 +# ② 铸渊 Agent 解析提交内容 +# ③ 铸渊 Agent 检测模块是否上传到仓库 +# ④ 唤醒铸渊核心大脑认知(注入模块验证结果) +# ⑤ 核心大脑处理 SYSLOG + 生成广播 +# ⑥ 铸渊 Agent 推送 Notion 工单(触发 Notion 侧处理) +# ⑦ 邮件通知开发者 +# ⑧ Discussion 回复闭环 # # 依赖 Secrets: # LLM_API_KEY 第三方 LLM 平台密钥(必须) @@ -96,6 +106,14 @@ jobs: console.log(`📡 解析完成: 广播=${broadcastId}, 类型=${type}, 邮箱=${email}`); + - name: 🔍 铸渊 Agent · 模块上传验证 + id: verify + env: + SYSLOG_CONTENT: ${{ steps.parse.outputs.content }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + AUTHOR: ${{ steps.parse.outputs.author }} + run: node scripts/verify-modules.js + - name: 🧠 Auto-detect and wake up persona id: persona env: @@ -105,13 +123,15 @@ jobs: SUBMIT_TYPE: ${{ steps.parse.outputs.type }} SUBMIT_CONTENT: ${{ steps.parse.outputs.content }} AUTHOR: ${{ steps.parse.outputs.author }} + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} run: node scripts/wake-persona.js - - name: 📋 Write to Notion ticket + - name: 📋 创建标准化 Notion 工单(Phase B1) + id: ticket if: ${{ secrets.NOTION_API_TOKEN }} env: NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} @@ -119,70 +139,20 @@ jobs: BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} SUBMIT_TYPE: ${{ steps.parse.outputs.type }} PERSONA_RESULT: ${{ steps.persona.outputs.result }} - run: | - node -e " - const https = require('https'); + MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }} + MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }} + DEVELOPER: ${{ steps.parse.outputs.author }} + SYSLOG_RAW: ${{ steps.parse.outputs.content }} + run: node scripts/create-standardized-ticket.js - const token = process.env.NOTION_TOKEN; - const dbId = process.env.NOTION_TICKET_DB_ID; - const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN'; - const type = process.env.SUBMIT_TYPE || 'syslog'; - const result = process.env.PERSONA_RESULT || '(no result)'; - - if (!token || !dbId) { - console.log('⚠️ Notion credentials not configured, skipping ticket creation'); - process.exit(0); - } - - const typeLabel = type === 'syslog' ? 'SYSLOG闭环' : '提问解答'; - const title = '[自动] ' + broadcastId + ' · ' + typeLabel; - - const body = JSON.stringify({ - parent: { database_id: dbId }, - properties: { - '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, - '操作类型': { select: { name: '其他' } }, - '提交者': { rich_text: [{ type: 'text', text: { content: '自动管道' } }] }, - '状态': { select: { name: '已完成' } }, - '优先级': { select: { name: 'P1' } } - }, - children: [{ - object: 'block', - type: 'paragraph', - paragraph: { - rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }] - } - }] - }); - - const opts = { - hostname: 'api.notion.com', - port: 443, - path: '/v1/pages', - method: 'POST', - headers: { - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - 'Notion-Version': '2022-06-28', - 'Content-Length': Buffer.byteLength(body) - } - }; - - const req = https.request(opts, (res) => { - let data = ''; - res.on('data', (c) => data += c); - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) { - console.log('✅ Notion 工单已创建'); - } else { - console.log('⚠️ Notion 工单创建失败: ' + res.statusCode + ' ' + data); - } - }); - }); - req.on('error', (e) => console.log('⚠️ Notion 请求失败: ' + e.message)); - req.write(body); - req.end(); - " + - name: 📡 推送广播到 GitHub(Phase B4) + if: steps.persona.outcome == 'success' && steps.parse.outputs.type == 'syslog' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + DEVELOPER_NAME: ${{ steps.parse.outputs.author }} + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js - name: 📧 Send email to developer env: @@ -266,11 +236,16 @@ jobs: const type = '${{ steps.parse.outputs.type }}'; const broadcastId = '${{ steps.parse.outputs.broadcast_id }}'; const email = '${{ steps.parse.outputs.email }}'; + const modulesUploaded = '${{ steps.verify.outputs.modules_uploaded }}' === 'true'; + const moduleCount = '${{ steps.verify.outputs.module_count }}' || '0'; const typeLabel = type === 'syslog' ? 'SYSLOG 闭环处理' : '问题解答'; const maskedEmail = email.length > 4 ? email.replace(/(.{2})(.*)(@.*)/, '$1***$3') : '***'; + const moduleStatus = moduleCount === '0' + ? 'ℹ️ 未检测到模块引用' + : (modulesUploaded ? '✅ 全部已上传' : '⚠️ 部分模块未上传'); const body = [ '✅ **已处理** · ' + typeLabel, @@ -278,13 +253,15 @@ jobs: '| 项目 | 内容 |', '|------|------|', '| 📡 广播编号 | `' + broadcastId + '` |', + '| 🔍 模块验证 | ' + moduleStatus + ' |', '| 📧 结果发送至 | `' + maskedEmail + '` |', '| 🤖 处理人格体 | 铸渊 |', '| ⏰ 处理时间 | ' + new Date().toISOString() + ' |', '', - '> 结果已发送到你的邮箱,请查收。', + '> 铸渊核心大脑已完成 SYSLOG 验收 + 模块检测 + 广播生成。', + '> 结果已发送到你的邮箱,Notion 侧工单已创建。', '>', - '> 如未收到,请检查垃圾箱或重新提交。' + '> 如未收到邮件,请检查垃圾箱或重新提交。' ].join('\n'); // Use GraphQL to add discussion comment diff --git a/.github/workflows/syslog-issue-pipeline.yml b/.github/workflows/syslog-issue-pipeline.yml index 5a5d81a5..c7386478 100644 --- a/.github/workflows/syslog-issue-pipeline.yml +++ b/.github/workflows/syslog-issue-pipeline.yml @@ -1,9 +1,19 @@ -name: SYSLOG Issue Pipeline +name: 📡 SYSLOG Issue Pipeline # 📡 SYSLOG 自助提交系统 · Issue 版全自动闭环 # # 开发者在 GitHub Issue 提交 SYSLOG 或提问(使用 syslog-submit 模板) -# → Actions 自动解析 → 调用 LLM API 唤醒人格体 -# → 写入 Notion 工单 → 发邮件给开发者 → Issue 回复 +# → Actions 自动解析 → 模块上传验证 → 调用 LLM API 唤醒人格体 +# → 模块闭环测试 → 写入 Notion 工单 → 发邮件给开发者 → Issue 回复 +# +# 闭环流程: +# ① 开发者提交 SYSLOG → Issue 触发 +# ② 铸渊 Agent 解析提交内容 +# ③ 铸渊 Agent 检测模块是否上传到仓库 +# ④ 唤醒铸渊核心大脑认知(注入模块验证结果) +# ⑤ 核心大脑处理 SYSLOG + 生成广播 +# ⑥ 铸渊 Agent 推送 Notion 工单(触发 Notion 侧处理) +# ⑦ 邮件通知开发者 +# ⑧ Issue 回复闭环 # # 依赖 Secrets: # LLM_API_KEY 第三方 LLM 平台密钥(必须) @@ -24,7 +34,12 @@ jobs: process: name: 📡 处理 SYSLOG 提交 / 广播提问 (Issue) runs-on: ubuntu-latest - if: contains(toJSON(github.event.issue.labels), 'syslog') + # 检测 SYSLOG 提交:通过标签、标题或正文模板字段识别 + if: > + contains(toJSON(github.event.issue.labels), 'syslog') || + contains(github.event.issue.title, 'SYSLOG') || + contains(github.event.issue.title, '系统日志') || + contains(github.event.issue.body, '### 广播编号') permissions: contents: write issues: write @@ -41,6 +56,16 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: 📋 步骤 1/8 · 接收提交 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '1' + STEP_NAME: '接收提交' + STEP_STATUS: 'ok' + STEP_DETAIL: '系统已收到您的 SYSLOG 提交,管道正在运行中…' + run: node scripts/pipeline-reporter.js + - name: 🔍 Parse submission id: parse uses: actions/github-script@v7 @@ -96,6 +121,58 @@ jobs: console.log(`📡 解析完成: 广播=${broadcastId}, 类型=${type}, 邮箱=${email}`); + - name: 📋 步骤 2/8 · 解析内容(成功) + if: success() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '2' + STEP_NAME: '解析内容' + STEP_STATUS: 'ok' + STEP_DETAIL: '广播编号: `${{ steps.parse.outputs.broadcast_id }}` · 类型: ${{ steps.parse.outputs.type }} · 邮箱: ${{ steps.parse.outputs.email }}' + run: node scripts/pipeline-reporter.js + + - name: 📋 步骤 2/8 · 解析内容(失败) + if: failure() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '2' + STEP_NAME: '解析内容' + STEP_STATUS: 'error' + STEP_DETAIL: '解析失败,请检查提交格式是否正确(需包含广播编号、类型、邮箱、内容)' + run: node scripts/pipeline-reporter.js + + - name: 🔍 铸渊 Agent · 模块上传验证 + id: verify + env: + SYSLOG_CONTENT: ${{ steps.parse.outputs.content }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + AUTHOR: ${{ steps.parse.outputs.author }} + run: node scripts/verify-modules.js + + - name: 📋 步骤 3/8 · 模块验证(成功) + if: always() && steps.verify.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '3' + STEP_NAME: '模块验证' + STEP_STATUS: 'ok' + STEP_DETAIL: '模块上传验证已完成' + run: node scripts/pipeline-reporter.js + + - name: 📋 步骤 3/8 · 模块验证(失败) + if: always() && steps.verify.outcome == 'failure' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '3' + STEP_NAME: '模块验证' + STEP_STATUS: 'error' + STEP_DETAIL: '模块验证异常(不影响闭环继续)' + run: node scripts/pipeline-reporter.js + - name: 🧠 Auto-detect and wake up persona id: persona env: @@ -105,13 +182,37 @@ jobs: SUBMIT_TYPE: ${{ steps.parse.outputs.type }} SUBMIT_CONTENT: ${{ steps.parse.outputs.content }} AUTHOR: ${{ steps.parse.outputs.author }} + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} run: node scripts/wake-persona.js - - name: 📋 Write to Notion ticket + - name: 📋 步骤 4/8 · 唤醒核心大脑(成功) + if: success() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '4' + STEP_NAME: '唤醒核心大脑' + STEP_STATUS: 'ok' + STEP_DETAIL: '铸渊核心大脑已完成处理' + run: node scripts/pipeline-reporter.js + + - name: 📋 步骤 4/8 · 唤醒核心大脑(失败) + if: failure() && steps.parse.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '4' + STEP_NAME: '唤醒核心大脑' + STEP_STATUS: 'error' + STEP_DETAIL: 'API 调用失败,请检查 Secrets: LLM_API_KEY / LLM_BASE_URL 是否配置正确' + run: node scripts/pipeline-reporter.js + + - name: 📋 创建标准化 Notion 工单(Phase B1) + id: ticket if: ${{ secrets.NOTION_API_TOKEN }} env: NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} @@ -119,72 +220,90 @@ jobs: BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} SUBMIT_TYPE: ${{ steps.parse.outputs.type }} PERSONA_RESULT: ${{ steps.persona.outputs.result }} - run: | - node -e " - const https = require('https'); + MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }} + MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }} + DEVELOPER: ${{ steps.parse.outputs.author }} + SYSLOG_RAW: ${{ steps.parse.outputs.content }} + run: node scripts/create-standardized-ticket.js - const token = process.env.NOTION_TOKEN; - const dbId = process.env.NOTION_TICKET_DB_ID; - const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN'; - const type = process.env.SUBMIT_TYPE || 'syslog'; - const result = process.env.PERSONA_RESULT || '(no result)'; + - name: 📋 步骤 5/8 · 创建 Notion 工单(成功) + if: always() && steps.ticket.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '5' + STEP_NAME: '创建 Notion 工单' + STEP_STATUS: 'ok' + STEP_DETAIL: '已写入霜砚工单簿' + run: node scripts/pipeline-reporter.js - if (!token || !dbId) { - console.log('⚠️ Notion credentials not configured, skipping ticket creation'); - process.exit(0); - } + - name: 📋 步骤 5/8 · 创建 Notion 工单(跳过) + if: always() && steps.ticket.outcome == 'skipped' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '5' + STEP_NAME: '创建 Notion 工单' + STEP_STATUS: 'skip' + STEP_DETAIL: 'Notion Token 未配置,跳过' + run: node scripts/pipeline-reporter.js - const typeLabel = type === 'syslog' ? 'SYSLOG闭环' : '提问解答'; - const title = '[自动] ' + broadcastId + ' · ' + typeLabel; + - name: 📋 步骤 5/8 · 创建 Notion 工单(失败) + if: always() && steps.ticket.outcome == 'failure' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '5' + STEP_NAME: '创建 Notion 工单' + STEP_STATUS: 'error' + STEP_DETAIL: 'Notion API 写入失败' + run: node scripts/pipeline-reporter.js - const body = JSON.stringify({ - parent: { database_id: dbId }, - properties: { - '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, - '操作类型': { select: { name: '其他' } }, - '提交者': { rich_text: [{ type: 'text', text: { content: '自动管道' } }] }, - '状态': { select: { name: '已完成' } }, - '优先级': { select: { name: 'P1' } } - }, - children: [{ - object: 'block', - type: 'paragraph', - paragraph: { - rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }] - } - }] - }); + - name: 📡 推送广播到 GitHub(Phase B4) + id: broadcast + if: steps.persona.outcome == 'success' && steps.parse.outputs.type == 'syslog' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + DEVELOPER_NAME: ${{ steps.parse.outputs.author }} + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js - const opts = { - hostname: 'api.notion.com', - port: 443, - path: '/v1/pages', - method: 'POST', - headers: { - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - 'Notion-Version': '2022-06-28', - 'Content-Length': Buffer.byteLength(body) - } - }; + - name: 📋 步骤 6/8 · 生成广播(成功) + if: always() && steps.broadcast.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '6' + STEP_NAME: '生成广播' + STEP_STATUS: 'ok' + STEP_DETAIL: '新广播已生成并推送' + run: node scripts/pipeline-reporter.js - const req = https.request(opts, (res) => { - let data = ''; - res.on('data', (c) => data += c); - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) { - console.log('✅ Notion 工单已创建'); - } else { - console.log('⚠️ Notion 工单创建失败: ' + res.statusCode + ' ' + data); - } - }); - }); - req.on('error', (e) => console.log('⚠️ Notion 请求失败: ' + e.message)); - req.write(body); - req.end(); - " + - name: 📋 步骤 6/8 · 生成广播(跳过) + if: always() && steps.broadcast.outcome == 'skipped' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '6' + STEP_NAME: '生成广播' + STEP_STATUS: 'skip' + STEP_DETAIL: '非 SYSLOG 类型或上游步骤未成功,跳过广播生成' + run: node scripts/pipeline-reporter.js + + - name: 📋 步骤 6/8 · 生成广播(失败) + if: always() && steps.broadcast.outcome == 'failure' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '6' + STEP_NAME: '生成广播' + STEP_STATUS: 'error' + STEP_DETAIL: '广播生成或推送失败' + run: node scripts/pipeline-reporter.js - name: 📧 Send email to developer + id: email env: SMTP_USER: ${{ secrets.SMTP_USER }} SMTP_PASS: ${{ secrets.SMTP_PASS }} @@ -258,6 +377,28 @@ jobs: }); " + - name: 📋 步骤 7/8 · 邮件通知(成功) + if: always() && steps.email.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '7' + STEP_NAME: '邮件通知' + STEP_STATUS: 'ok' + STEP_DETAIL: '邮件通知步骤已执行' + run: node scripts/pipeline-reporter.js + + - name: 📋 步骤 7/8 · 邮件通知(失败) + if: always() && steps.email.outcome == 'failure' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + STEP_NUM: '7' + STEP_NAME: '邮件通知' + STEP_STATUS: 'error' + STEP_DETAIL: '邮件发送失败,请检查 Secrets: SMTP_USER / SMTP_PASS' + run: node scripts/pipeline-reporter.js + - name: 💬 Reply to Issue uses: actions/github-script@v7 with: @@ -266,25 +407,34 @@ jobs: const type = '${{ steps.parse.outputs.type }}'; const broadcastId = '${{ steps.parse.outputs.broadcast_id }}'; const email = '${{ steps.parse.outputs.email }}'; + const modulesUploaded = '${{ steps.verify.outputs.modules_uploaded }}' === 'true'; + const moduleCount = '${{ steps.verify.outputs.module_count }}' || '0'; const typeLabel = type === 'syslog' ? 'SYSLOG 闭环处理' : '问题解答'; const maskedEmail = email.length > 4 ? email.replace(/(.{2})(.*)(@.*)/, '$1***$3') : '***'; + const moduleStatus = moduleCount === '0' + ? 'ℹ️ 未检测到模块引用' + : (modulesUploaded ? '✅ 全部已上传' : '⚠️ 部分模块未上传'); const body = [ - '✅ **已处理** · ' + typeLabel, + '## 🎉 闭环处理完成 · ' + typeLabel, '', '| 项目 | 内容 |', '|------|------|', '| 📡 广播编号 | `' + broadcastId + '` |', + '| 🔍 模块验证 | ' + moduleStatus + ' |', '| 📧 结果发送至 | `' + maskedEmail + '` |', '| 🤖 处理人格体 | 铸渊 |', '| ⏰ 处理时间 | ' + new Date().toISOString() + ' |', '', - '> 结果已发送到你的邮箱,请查收。', + '> 铸渊核心大脑已完成 SYSLOG 验收 + 模块检测 + 广播生成。', + '> 结果已发送到你的邮箱,Notion 侧工单已创建。', '>', - '> 如未收到,请检查垃圾箱或重新提交。' + '> 如未收到邮件,请检查垃圾箱或重新提交。', + '>', + '> *—— 铸渊(ICE-GL-ZY001)· 代码守护人格体*' ].join('\n'); await github.rest.issues.createComment({ @@ -313,12 +463,14 @@ jobs: const number = context.payload.issue?.number; if (!number) return; + const actionsUrl = 'https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions'; + try { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: number, - body: '❌ **处理失败** · 请检查提交格式是否正确,或联系管理员。\n\n> 错误详情请查看 [Actions 日志](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions)。' + body: '## ⚠️ 闭环处理异常\n\n❌ **处理失败** · 请检查提交格式是否正确,或联系管理员。\n\n> 部分步骤异常,已记录日志。冰朔会在下次巡检时处理。\n> 错误详情请查看 [Actions 日志](' + actionsUrl + ')。\n>\n> *—— 铸渊(ICE-GL-ZY001)*' }); } catch (err) { console.log('⚠️ 失败告警回复失败: ' + err.message); diff --git a/.github/workflows/update-readme-bulletin.yml b/.github/workflows/update-readme-bulletin.yml index 6247d891..1571d635 100644 --- a/.github/workflows/update-readme-bulletin.yml +++ b/.github/workflows/update-readme-bulletin.yml @@ -77,7 +77,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add README.md + git add README.md .github/brain/bulletin-board-today.json if git diff --cached --quiet; then echo "📢 公告区无变化,跳过提交" else diff --git a/.github/workflows/zhuyuan-issue-reply.yml b/.github/workflows/zhuyuan-issue-reply.yml index 6b713545..90b44c60 100644 --- a/.github/workflows/zhuyuan-issue-reply.yml +++ b/.github/workflows/zhuyuan-issue-reply.yml @@ -7,12 +7,82 @@ on: types: [created] jobs: + # ════════════════════════════════════════════════════════════ + # SYSLOG 即时确认 · 安全网 + # 当开发者通过 Issue 提交 SYSLOG 时,立刻回复确认 + 进度清单 + # 确保即使 syslog-issue-pipeline 未触发,开发者也能看到系统已收到 + # ════════════════════════════════════════════════════════════ + syslog-ack: + name: 📡 SYSLOG 提交确认 + runs-on: ubuntu-latest + if: > + github.event_name == 'issues' && + github.event.action == 'opened' && + (contains(join(github.event.issue.labels.*.name, ','), 'syslog') || + contains(github.event.issue.title, 'SYSLOG') || + contains(github.event.issue.title, '系统日志') || + contains(github.event.issue.body, '### 广播编号')) + permissions: + issues: write + steps: + - name: 📡 发送即时确认 + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.issue.body || ''; + const number = context.payload.issue.number; + const author = context.payload.issue.user.login; + + // Extract broadcast_id from body + const bcMatch = body.match(/###\s*广播编号\s*\n+([^\n#]+)/i); + const broadcastId = bcMatch ? bcMatch[1].trim() : '(待解析)'; + + // Extract type + const typeMatch = body.match(/###\s*类型\s*\n+([^\n#]+)/i); + const submitType = typeMatch ? typeMatch[1].trim() : 'SYSLOG'; + + const now = new Date().toISOString(); + + const comment = [ + '## 📡 系统已收到 · SYSLOG 闭环处理启动', + '', + '> **提交者**: @' + author, + '> **广播编号**: `' + broadcastId + '`', + '> **类型**: ' + submitType, + '> **接收时间**: ' + now, + '', + '💡 SYSLOG 自动管道已启动,预计 2-5 分钟完成全部处理。', + '每完成一步都会在下方更新进度评论,你刷新页面即可看到最新状态。', + '', + '> 如超过 10 分钟无后续回复,请在下方评论中 `@铸渊 查询#' + number + '闭环状态`。', + '>', + '> *—— 铸渊(ICE-GL-ZY001)· 代码守护人格体*' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + body: comment + }); + + console.log('📡 SYSLOG 即时确认已发送 Issue #' + number); + + # ════════════════════════════════════════════════════════════ + # 开发者提问 · 铸渊自动回答 + # 排除 SYSLOG 提交(由 syslog-issue-pipeline.yml 处理) + # 排除 bingshuo-deploy(由 bingshuo-deploy-agent.yml 处理) + # ════════════════════════════════════════════════════════════ auto-reply: name: 🤖 铸渊回答问题 runs-on: ubuntu-latest - # 仅在 Issue 新建或评论中包含 @铸渊 / 铸渊 时触发 if: > - github.event_name == 'issues' || + (github.event_name == 'issues' && + !contains(join(github.event.issue.labels.*.name, ','), 'syslog') && + !contains(join(github.event.issue.labels.*.name, ','), 'bingshuo-deploy') && + !contains(github.event.issue.title, 'SYSLOG') && + !contains(github.event.issue.title, '系统日志') && + !contains(github.event.issue.body, '### 广播编号')) || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '铸渊')) permissions: diff --git a/.gitignore b/.gitignore index a0e218d9..455e645e 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ modules/palace-game/data/saves/PAL-* # collaboration-logs exports (generated by save-collaboration-log.js) collaboration-logs/exports/ + +# core runtime artifacts +core/task-queue/queue.json +core/task-queue/execution-log.json +core/system-check/last-report.json diff --git a/20260313_feishu_webhook_log.md b/20260313_feishu_webhook_log.md new file mode 100644 index 00000000..1863350f --- /dev/null +++ b/20260313_feishu_webhook_log.md @@ -0,0 +1,56 @@ +\# 2026-03-13 飞书WebHook配置完成日志 + +\## 一、任务目标 + +配置飞书WebHook链路,实现飞书消息事件推送至阿里云服务器的Node.js服务 + + + +\## 二、关键操作记录 + +| 操作时间 | 操作内容 | 操作结果 | + +|----------|----------|----------| + +| 19:00-19:30 | 飞书开放平台配置WebHook地址:`https://guanghulab.com/webhook/feishu`,订阅`im.message.receive\_v1`消息事件 | 地址配置/事件订阅成功 | + +| 19:30-20:00 | 飞书客户端查找机器人(因索引未更新暂未找到) | 确认仅为展示问题,不影响功能 | + +| 20:00-20:20 | Windows终端curl验证WebHook连通性 | 返回`{"challenge":"test123"}`,基础链路通 | + +| 20:20-20:40 | 阿里云服务器替换`/var/www/hololake/server.js`,修复JSON解析Bug | 修复`Cannot read properties of undefined`错误 | + +| 20:40-20:50 | 重启Node.js服务 | 服务启动成功,日志:`🚀 服务启动成功,端口:3000` | + +| 20:50-21:00 | 二次curl验证 | 成功返回`{"challenge":"test123"}`,链路全通 | + + + +\## 三、环境信息 + +1\. 服务器:阿里云ECS Linux(`root@iZf8z4nezg5bs9kl9eyth1Z`) + +2\. 服务:Node.js(3000端口)+ Nginx转发(`https://guanghulab.com/webhook/feishu`) + +3\. 依赖:express、body-parser + +4\. 日志路径:`/var/www/hololake/webhook.log` + + + +\## 四、任务结论 + +✅ 飞书WebHook链路全通,可正常接收消息推送 + +✅ 代码Bug已修复,服务稳定运行 + +✅ 机器人未找到为索引延迟,不影响功能 + + + +\## 五、后续建议 + +1\. 查看消息日志:`tail -f /var/www/hololake/webhook.log` + +2\. 机器人聊天窗口1-2小时后可正常搜索 + diff --git a/README.md b/README.md index 51988395..c8a950e0 100644 --- a/README.md +++ b/README.md @@ -92,21 +92,21 @@ | 时间 | 检查项 | 状态 | |------|--------|------| -| 03-13 14:04 | ✅ 🚀 铸渊 CD · 自动部署到 guanghulab.com · 成功 | 冰朔 | -| 03-13 14:03 | 🔧 系统更新: `.github/` | 铸渊 (ZhùYuān) | -| 03-13 14:03 | ✅ 📢 更新系统公告区 · 成功 | 冰朔 | -| 03-13 14:03 | ✅ 铸渊 · Bridge E · GitHub Changes → Notion · 成功 | 冰朔 | -| 03-13 13:56 | 🔵 铸渊 · Bridge E · GitHub Changes → Notion · action_required | Copilot | -| 03-13 11:54 | 🔧 系统更新: `scripts/` | 铸渊 (Copilot) | +| 03-14 21:49 | ⏹️ 📢 更新系统公告区 · cancelled | 冰朔 | +| 03-14 21:48 | ✅ 🚀 铸渊 CD · 自动部署到 guanghulab.com · 成功 | 冰朔 | +| 03-14 21:47 | ✅ Notion Heartbeat Monitor · 成功 | 冰朔 | +| 03-14 21:47 | ✅ 📢 更新系统公告区 · 成功 | 冰朔 | +| 03-14 21:47 | 🔧 系统更新: `.github/` | github-actions[bot] | +| 03-14 21:47 | ✅ 铸渊 · Bridge E · GitHub Changes → Notion · 成功 | 冰朔 | +| 03-14 16:48 | ✅ 每日巡检 ✅ 通过 | 冰朔 | +| 03-14 16:18 | 🔧 系统更新: `scripts/` | 铸渊 (Copilot) | +| 03-14 14:58 | 🔧 系统更新: `docs/` | 铸渊 (Copilot) | +| 03-14 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 | +| 03-13 16:53 | ✅ 每日巡检 ✅ 通过 | 冰朔 | | 03-13 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 | | 03-12 16:55 | ✅ 每日巡检 ✅ 通过 | 冰朔 | | 03-12 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 | | 03-11 16:55 | ✅ 每日巡检 ✅ 通过 | 冰朔 | -| 03-10 16:56 | ✅ 每日巡检 ✅ 通过 | 冰朔 | -| 03-10 08:00 | 🧠 核心大脑升级 v3.0 · 壳-核分离架构 · 前端壳(UI/IO) + 后端脑(prompt/routing/memory/context) · BRAIN域4接口上线 · 覆盖率 7/21 (33.3%) | 铸渊(冰朔指令) | -| 03-10 08:00 | ⚠️ 铸渊 PSP 巡检完成 · 发现 3 个问题 · 自动修复 0 项 | 铸渊PSP巡检 | -| 03-10 08:00 | 🔵 CI 构建 失败 | GitHub Actions | -| 03-09 16:56 | ✅ 每日巡检 ✅ 通过 | 冰朔 | ### 🤖 铸渊自动提醒 @@ -114,7 +114,7 @@ > 🟢 **今日无需冰朔手动干预** · 系统一切正常 > -> 🗓️ 2026-03-13 · 铸渊自动检测 +> 🗓️ 2026-03-14 · 铸渊自动检测 --- @@ -126,11 +126,14 @@ > 若需要手动干预,相关条目将标红并发送邮件提醒。 +👥 合作者公告栏(2026-03-14) + | 时间 | 合作者 | 模块 | 状态 | |------|--------|------|------| -| 03-13 14:03 | 冰朔 | `—/` | ✅ 上传成功 | -| 03-13 14:03 | Copilot | `—/` | ✅ 上传成功 | -| 03-13 13:56 | Copilot | `—/` | 🔵 已更新 | +| 16:25 | Copilot | `—/` | ✅ 上传成功 | +| 16:25 | 冰朔 | `—/` | ✅ 上传成功 | +| 21:47 | Copilot | `—/` | ✅ 上传成功 | +| 21:47 | 冰朔 | `—/` | ✅ 上传成功 | ### 🤖 铸渊自动提醒 · 合作者 @@ -138,7 +141,7 @@ > 🟢 **今日无需合作者手动干预** · 所有模块状态正常 > -> 🗓️ 2026-03-13 · 铸渊自动检测 +> 🗓️ 2026-03-14 · 铸渊自动检测 --- diff --git a/backend-integration/nginx-api-proxy.conf b/backend-integration/nginx-api-proxy.conf index cc36436b..2527dd67 100644 --- a/backend-integration/nginx-api-proxy.conf +++ b/backend-integration/nginx-api-proxy.conf @@ -5,7 +5,24 @@ # # 前端(GitHub Pages)→ Nginx → API 代理(Node.js :3721)→ 模型 API -# API 代理转发 +# Persona Studio API → persona-studio 后端端口 3002(含 CORS 跨域支持) +location /api/ps/ { + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; + if ($request_method = OPTIONS) { + return 204; + } + proxy_pass http://127.0.0.1:3002/api/ps/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 90s; +} + +# API 代理转发(通用 AI 聊天) location /api/ { proxy_pass http://127.0.0.1:3721; proxy_http_version 1.1; @@ -24,3 +41,15 @@ location /api/ { proxy_read_timeout 90s; proxy_send_timeout 60s; } + +# Persona Studio WebSocket → 预览进度推送端口 3002 +location /ws/preview { + proxy_pass http://127.0.0.1:3002; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 86400; +} diff --git a/brain/automation-map.json b/brain/automation-map.json new file mode 100644 index 00000000..5036c76a --- /dev/null +++ b/brain/automation-map.json @@ -0,0 +1,650 @@ +{ + "version": "4.0", + "generated_at": "2026-03-13T19:36:48.677Z", + "generated_by": "scripts/generate-automation-map.js", + "description": "数字地球系统自动化地图 · 所有工作流与自动化脚本", + "workflows": [ + { + "file": "bingshuo-deploy-agent.yml", + "name": "🧊 冰朔人格体 · 自动部署诊断", + "triggers": [ + "issues", + "issue_comment", + "workflow_dispatch" + ], + "scripts": [ + "scripts/bingshuo-deploy-agent.js" + ] + }, + { + "file": "bingshuo-neural-system.yml", + "name": "冰朔主控神经系统 · 自动维护", + "triggers": [ + "push", + "workflow_dispatch", + "schedule" + ], + "cron": "0 0 * * *", + "scripts": [ + "scripts/bingshuo-neural-sync.js" + ] + }, + { + "file": "brain-sync.yml", + "name": "铸渊 Brain Sync", + "triggers": [ + "push", + "workflow_dispatch", + "schedule" + ], + "cron": "0 8 * * *", + "scripts": [ + "scripts/process-broadcasts.js", + "scripts/daily-check.js", + "scripts/update-memory.js" + ] + }, + { + "file": "bridge-changes-to-notion.yml", + "name": "铸渊 · Bridge E · GitHub Changes → Notion", + "triggers": [ + "push", + "pull_request" + ], + "scripts": [ + "scripts/notion-bridge.js", + "scripts/notion-bridge.js" + ] + }, + { + "file": "bridge-session-summary.yml", + "name": "Generate Session Summary for Notion", + "triggers": [ + "workflow_dispatch", + "schedule" + ], + "cron": "50 23 * * *", + "scripts": [ + "scripts/generate-session-summary.js" + ] + }, + { + "file": "bridge-syslog-to-notion.yml", + "name": "铸渊 · Bridge A · SYSLOG → Notion", + "triggers": [ + "push", + "workflow_dispatch" + ], + "scripts": [ + "scripts/notion-bridge.js" + ] + }, + { + "file": "check-structure.yml", + "name": "模块结构检查", + "triggers": [ + "unknown" + ] + }, + { + "file": "daily-maintenance.yml", + "name": "🔧 铸渊 · Daily Maintenance Agent", + "triggers": [ + "workflow_dispatch", + "schedule" + ], + "cron": "0 2 * * *", + "scripts": [ + "scripts/generate-system-health.js" + ] + }, + { + "file": "deploy-pages.yml", + "name": "🌀 部署铸渊聊天室 (GitHub Pages)", + "triggers": [ + "push", + "workflow_dispatch" + ] + }, + { + "file": "deploy-to-server.yml", + "name": "🚀 铸渊 CD · 自动部署到 guanghulab.com", + "triggers": [ + "push", + "workflow_dispatch" + ], + "scripts": [ + "scripts/contract-check.js", + "scripts/zhuyuan-module-protocol.js", + "scripts/notion-bridge.js" + ] + }, + { + "file": "distribute-broadcasts.yml", + "name": "铸渊 · 广播分发", + "triggers": [ + "push", + "workflow_dispatch" + ], + "scripts": [ + "scripts/distribute-broadcasts.js" + ] + }, + { + "file": "esp-signal-processor.yml", + "name": "铸渊 · ESP 邮件信号处理器(已暂停)", + "triggers": [ + "workflow_dispatch", + "schedule" + ], + "cron": "*/30 * * * *", + "scripts": [ + "scripts/esp-email-processor.js" + ] + }, + { + "file": "generate-module-doc.yml", + "name": "铸渊 · 光湖纪元 模块文档自动生成", + "triggers": [ + "push", + "workflow_dispatch" + ], + "scripts": [ + "scripts/generate-module-doc.js", + "scripts/notify-module-received.js" + ] + }, + { + "file": "hli-contract-check.yml", + "name": "HLI Contract Check", + "triggers": [ + "push", + "pull_request" + ] + }, + { + "file": "notion-callback-pipeline.yml", + "name": "Notion Callback Pipeline", + "triggers": [ + "unknown" + ], + "scripts": [ + "scripts/verify-modules.js", + "scripts/wake-persona.js", + "scripts/push-broadcast-to-github.js" + ] + }, + { + "file": "notion-connectivity-test.yml", + "name": "铸渊 · Notion 连通性测试", + "triggers": [ + "workflow_dispatch" + ], + "scripts": [ + "scripts/notion-connectivity-test.js" + ] + }, + { + "file": "notion-heartbeat.yml", + "name": "Notion Heartbeat Monitor", + "triggers": [ + "workflow_dispatch", + "schedule" + ], + "cron": "*/5 * * * *", + "scripts": [ + "scripts/notion-heartbeat.js" + ] + }, + { + "file": "notion-poll.yml", + "name": "铸渊 · Notion 工单轮询", + "triggers": [ + "workflow_dispatch", + "schedule" + ], + "cron": "*/15 * * * *", + "scripts": [ + "scripts/notion-signal-bridge.js" + ] + }, + { + "file": "persona-invoke.yml", + "name": "Persona Invoke Endpoint", + "triggers": [ + "workflow_dispatch" + ], + "scripts": [ + "scripts/verify-modules.js", + "scripts/wake-persona.js", + "scripts/push-broadcast-to-github.js" + ] + }, + { + "file": "pm2-server-diagnose.yml", + "name": "🔧 铸渊 · PM2 服务诊断与健康检查", + "triggers": [ + "workflow_dispatch" + ], + "scripts": [ + "scripts/server-diagnose-report.js" + ] + }, + { + "file": "process-notion-orders.yml", + "name": "Process Notion Work Orders", + "triggers": [ + "push", + "workflow_dispatch" + ] + }, + { + "file": "ps-on-build.yml", + "name": "🌊 Persona Studio · 代码生成", + "triggers": [ + "workflow_dispatch" + ] + }, + { + "file": "ps-on-chat.yml", + "name": "🌊 Persona Studio · 对话处理", + "triggers": [ + "workflow_dispatch" + ] + }, + { + "file": "ps-on-complete.yml", + "name": "🌊 Persona Studio · 完成通知", + "triggers": [ + "workflow_dispatch" + ] + }, + { + "file": "ps-on-login.yml", + "name": "🌊 Persona Studio · 登录校验", + "triggers": [ + "workflow_dispatch" + ] + }, + { + "file": "psp-daily-inspection.yml", + "name": "铸渊 · PSP 分身巡检", + "triggers": [ + "workflow_dispatch", + "schedule" + ], + "cron": "0 1 * * *", + "scripts": [ + "scripts/psp-inspection.js" + ] + }, + { + "file": "push-broadcast.yml", + "name": "铸渊 · Push Broadcast · Notion → 飞书文档B", + "triggers": [ + "workflow_dispatch" + ], + "scripts": [ + "scripts/push-broadcast.js", + "scripts/send-feishu-alert.js", + "scripts/send-feishu-alert.js" + ] + }, + { + "file": "receive-syslog.yml", + "name": "铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion", + "triggers": [ + "unknown" + ], + "scripts": [ + "scripts/receive-syslog.js", + "scripts/send-feishu-alert.js" + ] + }, + { + "file": "staging-preview.yml", + "name": "🔍 铸渊预演部署 (Staging Preview)", + "triggers": [ + "pull_request", + "workflow_dispatch" + ] + }, + { + "file": "sync-login-entry.yml", + "name": "铸渊 · Sync Login Entry · Notion → 飞书文档A", + "triggers": [ + "workflow_dispatch" + ], + "scripts": [ + "scripts/sync-login-entry.js", + "scripts/send-feishu-alert.js" + ] + }, + { + "file": "sync-persona-studio.yml", + "name": "🔄 铸渊跨仓库同步 · persona-studio", + "triggers": [ + "push", + "workflow_dispatch" + ], + "scripts": [ + "scripts/cross-repo-sync.js" + ] + }, + { + "file": "syslog-auto-pipeline.yml", + "name": "SYSLOG Auto Pipeline", + "triggers": [ + "unknown" + ], + "scripts": [ + "scripts/verify-modules.js", + "scripts/wake-persona.js", + "scripts/create-standardized-ticket.js", + "scripts/push-broadcast-to-github.js" + ] + }, + { + "file": "syslog-issue-pipeline.yml", + "name": "📡 SYSLOG Issue Pipeline", + "triggers": [ + "issues" + ], + "scripts": [ + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js", + "scripts/verify-modules.js", + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js", + "scripts/wake-persona.js", + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js", + "scripts/create-standardized-ticket.js", + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js", + "scripts/push-broadcast-to-github.js", + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js", + "scripts/pipeline-reporter.js" + ] + }, + { + "file": "syslog-pipeline.yml", + "name": "铸渊 · SYSLOG Pipeline (A/D/E)", + "triggers": [ + "push", + "workflow_dispatch" + ], + "scripts": [ + "scripts/process-syslog.js" + ] + }, + { + "file": "test-notion-bridge.yml", + "name": "🧪 Notion Bridge Connectivity Test", + "triggers": [ + "push", + "workflow_dispatch" + ] + }, + { + "file": "update-readme-bulletin.yml", + "name": "📢 更新系统公告区", + "triggers": [ + "push", + "workflow_dispatch", + "schedule" + ], + "cron": "0 1 * * *", + "scripts": [ + "scripts/update-readme-bulletin.js" + ] + }, + { + "file": "update-repo-map.yml", + "name": "铸渊 · 图书馆目录自动更新", + "triggers": [ + "push", + "workflow_dispatch", + "schedule" + ], + "cron": "0 0 * * *", + "scripts": [ + "scripts/generate-repo-map.js" + ] + }, + { + "file": "zhuyuan-brain-sync.yml", + "name": "铸渊 · Brain Sync", + "triggers": [ + "push" + ], + "scripts": [ + "scripts/update-brain.js", + "scripts/process-broadcasts.js" + ] + }, + { + "file": "zhuyuan-daily-agent.yml", + "name": "🤖 铸渊巡检 Agent · 每日自动巡检与修复", + "triggers": [ + "workflow_dispatch", + "schedule" + ], + "cron": "0 14 * * *", + "scripts": [ + "scripts/zhuyuan-daily-agent.js" + ] + }, + { + "file": "zhuyuan-daily-selfcheck.yml", + "name": "铸渊 · 每日自检", + "triggers": [ + "workflow_dispatch", + "schedule" + ], + "cron": "0 0 * * *", + "scripts": [ + "scripts/selfcheck.js" + ] + }, + { + "file": "zhuyuan-issue-reply.yml", + "name": "铸渊 · Issue 自动回复", + "triggers": [ + "issues", + "issue_comment" + ], + "scripts": [ + "scripts/zhuyuan-issue-reply.js" + ] + }, + { + "file": "zhuyuan-pr-review.yml", + "name": "铸渊 · PR Review", + "triggers": [ + "pull_request" + ], + "scripts": [ + "scripts/contract-check.js", + "scripts/route-align-check.js" + ] + } + ], + "cron_jobs": [ + { + "cron": "*/15 * * * *", + "workflow": "notion-poll.yml", + "name": "铸渊 · Notion 工单轮询" + }, + { + "cron": "*/30 * * * *", + "workflow": "esp-signal-processor.yml", + "name": "铸渊 · ESP 邮件信号处理器(已暂停)" + }, + { + "cron": "*/5 * * * *", + "workflow": "notion-heartbeat.yml", + "name": "Notion Heartbeat Monitor" + }, + { + "cron": "0 0 * * *", + "workflow": "bingshuo-neural-system.yml", + "name": "冰朔主控神经系统 · 自动维护" + }, + { + "cron": "0 0 * * *", + "workflow": "update-repo-map.yml", + "name": "铸渊 · 图书馆目录自动更新" + }, + { + "cron": "0 0 * * *", + "workflow": "zhuyuan-daily-selfcheck.yml", + "name": "铸渊 · 每日自检" + }, + { + "cron": "0 1 * * *", + "workflow": "psp-daily-inspection.yml", + "name": "铸渊 · PSP 分身巡检" + }, + { + "cron": "0 1 * * *", + "workflow": "update-readme-bulletin.yml", + "name": "📢 更新系统公告区" + }, + { + "cron": "0 14 * * *", + "workflow": "zhuyuan-daily-agent.yml", + "name": "🤖 铸渊巡检 Agent · 每日自动巡检与修复" + }, + { + "cron": "0 2 * * *", + "workflow": "daily-maintenance.yml", + "name": "🔧 铸渊 · Daily Maintenance Agent" + }, + { + "cron": "0 8 * * *", + "workflow": "brain-sync.yml", + "name": "铸渊 Brain Sync" + }, + { + "cron": "50 23 * * *", + "workflow": "bridge-session-summary.yml", + "name": "Generate Session Summary for Notion" + } + ], + "deploy_scripts": [ + { + "workflow": "bingshuo-deploy-agent.yml", + "name": "🧊 冰朔人格体 · 自动部署诊断", + "triggers": [ + "issues", + "issue_comment", + "workflow_dispatch" + ] + }, + { + "workflow": "deploy-pages.yml", + "name": "🌀 部署铸渊聊天室 (GitHub Pages)", + "triggers": [ + "push", + "workflow_dispatch" + ] + }, + { + "workflow": "deploy-to-server.yml", + "name": "🚀 铸渊 CD · 自动部署到 guanghulab.com", + "triggers": [ + "push", + "workflow_dispatch" + ] + } + ], + "data_sync_scripts": [ + { + "script": "scripts/bingshuo-deploy-agent.js", + "type": "other" + }, + { + "script": "scripts/bingshuo-neural-sync.js", + "type": "sync" + }, + { + "script": "scripts/brain-bridge-sync.js", + "type": "sync" + }, + { + "script": "scripts/create-standardized-ticket.js", + "type": "other" + }, + { + "script": "scripts/cross-repo-sync.js", + "type": "sync" + }, + { + "script": "scripts/generate-automation-map.js", + "type": "other" + }, + { + "script": "scripts/generate-communication-map.js", + "type": "other" + }, + { + "script": "scripts/invoke-persona.js", + "type": "other" + }, + { + "script": "scripts/notify-module-received.js", + "type": "other" + }, + { + "script": "scripts/notion-bridge.js", + "type": "bridge" + }, + { + "script": "scripts/notion-connectivity-test.js", + "type": "other" + }, + { + "script": "scripts/notion-heartbeat.js", + "type": "other" + }, + { + "script": "scripts/notion-signal-bridge.js", + "type": "bridge" + }, + { + "script": "scripts/process-broadcasts.js", + "type": "broadcast" + }, + { + "script": "scripts/push-broadcast-to-github.js", + "type": "broadcast" + }, + { + "script": "scripts/push-broadcast.js", + "type": "broadcast" + }, + { + "script": "scripts/receive-syslog.js", + "type": "other" + }, + { + "script": "scripts/server-diagnose-report.js", + "type": "other" + }, + { + "script": "scripts/sync-login-entry.js", + "type": "sync" + }, + { + "script": "scripts/wake-persona.js", + "type": "other" + } + ], + "status": "stable" +} \ No newline at end of file diff --git a/brain/communication-map.json b/brain/communication-map.json new file mode 100644 index 00000000..4eed5a05 --- /dev/null +++ b/brain/communication-map.json @@ -0,0 +1,631 @@ +{ + "version": "4.0", + "generated_at": "2026-03-13T19:38:09.552Z", + "generated_by": "scripts/generate-communication-map.js", + "description": "数字地球系统通信地图 · 所有通信入口与数据流", + "api_endpoints": { + "hli_middleware": { + "port": 3001, + "process": "guanghulab", + "script": "src/index.js", + "routes": [ + { + "method": "GET", + "path": "/health", + "description": "健康检查" + }, + { + "method": "POST", + "path": "/hli/auth/login", + "description": "用户登录" + }, + { + "method": "POST", + "path": "/hli/auth/register", + "description": "用户注册" + }, + { + "method": "POST", + "path": "/hli/auth/verify", + "description": "Token 验证" + }, + { + "method": "POST", + "path": "/hli/brain/prompt", + "description": "AI Prompt 构建" + }, + { + "method": "POST", + "path": "/hli/brain/route", + "description": "脑路由" + }, + { + "method": "POST", + "path": "/hli/brain/context", + "description": "上下文管理" + }, + { + "method": "POST", + "path": "/hli/brain/memory", + "description": "记忆管理" + }, + { + "method": "GET", + "path": "/hli/brain/status", + "description": "脑状态" + }, + { + "method": "GET", + "path": "/hli/brain/bridge", + "description": "脑桥接状态" + }, + { + "method": "POST", + "path": "/hli/brain/bridge/sync", + "description": "脑桥接同步" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/export", + "description": "脑桥接导出" + }, + { + "method": "POST", + "path": "/hli/brain/bridge/consistency", + "description": "一致性检查" + }, + { + "method": "POST", + "path": "/hli/brain/bridge/master-mode", + "description": "主模式切换" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/explanation", + "description": "桥接说明" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/inspection", + "description": "桥接巡检" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/developers", + "description": "开发者列表" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/developers/:expId", + "description": "单个开发者信息" + }, + { + "method": "POST", + "path": "/hli/brain/bridge/developers", + "description": "开发者管理" + }, + { + "method": "GET", + "path": "/hli/registry/lookup", + "description": "开发者编号查询" + } + ] + }, + "express_backend": { + "port": 3000, + "process": "guanghulab-backend", + "script": "backend/server.js", + "routes": [ + { + "method": "GET", + "path": "/", + "description": "服务状态" + }, + { + "method": "*", + "path": "/notion/*", + "description": "Notion 路由" + }, + { + "method": "*", + "path": "/feishu/*", + "description": "飞书路由" + }, + { + "method": "*", + "path": "/feishu-bot/*", + "description": "飞书机器人路由" + }, + { + "method": "*", + "path": "/router/*", + "description": "路由管理" + }, + { + "method": "*", + "path": "/api/coldstart/*", + "description": "冷启动接口" + }, + { + "method": "*", + "path": "/api/v1/developers/*", + "description": "开发者管理接口" + }, + { + "method": "POST", + "path": "/webhook/feishu", + "description": "飞书 Webhook 入口" + } + ] + }, + "ai_proxy": { + "port": 3721, + "process": "guanghulab-proxy", + "script": "backend-integration/api-proxy.js", + "routes": [ + { + "method": "POST", + "path": "/chat", + "description": "AI Chat 代理" + } + ] + }, + "persona_studio": { + "port": 3002, + "process": "persona-studio", + "script": "persona-studio/backend/server.js", + "routes": [ + { + "method": "*", + "path": "/api/ps/*", + "description": "Persona Studio API" + } + ] + }, + "websocket": { + "port": 8080, + "process": "guanghulab-ws", + "script": "status-board/mock-ws-server.js", + "routes": [ + { + "method": "WS", + "path": "/ws", + "description": "看板 WebSocket 推送" + } + ] + } + }, + "webhook_endpoints": [ + { + "path": "/webhook/feishu", + "port": 3000, + "description": "飞书 Webhook 入口(机器人事件回调)", + "direction": "inbound" + }, + { + "path": "/feishu-bot/event", + "port": 3000, + "description": "飞书机器人新版事件入口", + "direction": "inbound" + } + ], + "trigger_paths": { + "github_to_notion": [ + { + "workflow": "bridge-changes-to-notion.yml", + "name": "铸渊 · Bridge E · GitHub Changes → Notion" + }, + { + "workflow": "bridge-session-summary.yml", + "name": "Generate Session Summary for Notion" + }, + { + "workflow": "bridge-syslog-to-notion.yml", + "name": "铸渊 · Bridge A · SYSLOG → Notion" + }, + { + "workflow": "deploy-to-server.yml", + "name": "🚀 铸渊 CD · 自动部署到 guanghulab.com" + }, + { + "workflow": "notion-callback-pipeline.yml", + "name": "Notion Callback Pipeline" + }, + { + "workflow": "persona-invoke.yml", + "name": "Persona Invoke Endpoint" + }, + { + "workflow": "push-broadcast.yml", + "name": "铸渊 · Push Broadcast · Notion → 飞书文档B" + }, + { + "workflow": "receive-syslog.yml", + "name": "铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion" + }, + { + "workflow": "sync-login-entry.yml", + "name": "铸渊 · Sync Login Entry · Notion → 飞书文档A" + }, + { + "workflow": "syslog-auto-pipeline.yml", + "name": "SYSLOG Auto Pipeline" + }, + { + "workflow": "syslog-issue-pipeline.yml", + "name": "📡 SYSLOG Issue Pipeline" + }, + { + "workflow": "test-notion-bridge.yml", + "name": "🧪 Notion Bridge Connectivity Test" + } + ], + "notion_to_github": [ + { + "workflow": "notion-heartbeat.yml", + "name": "Notion Heartbeat Monitor" + }, + { + "workflow": "notion-poll.yml", + "name": "铸渊 · Notion 工单轮询" + }, + { + "workflow": "persona-invoke.yml", + "name": "Persona Invoke Endpoint" + }, + { + "workflow": "process-notion-orders.yml", + "name": "Process Notion Work Orders" + } + ] + }, + "data_sync_scripts": [ + { + "script": "scripts/bingshuo-deploy-agent.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/bingshuo-neural-sync.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": false + }, + { + "script": "scripts/brain-bridge-sync.js", + "direction": "bidirectional", + "uses_notion_api": true, + "uses_http": false + }, + { + "script": "scripts/create-standardized-ticket.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/cross-repo-sync.js", + "direction": "unknown", + "uses_notion_api": false, + "uses_http": true + }, + { + "script": "scripts/esp-email-processor.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/generate-automation-map.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": false + }, + { + "script": "scripts/generate-communication-map.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/generate-repo-map.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": false + }, + { + "script": "scripts/generate-session-summary.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": false + }, + { + "script": "scripts/invoke-persona.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": false + }, + { + "script": "scripts/notion-bridge.js", + "direction": "bidirectional", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/notion-connectivity-test.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/notion-heartbeat.js", + "direction": "external_to_github", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/notion-signal-bridge.js", + "direction": "bidirectional", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/psp-inspection.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/push-broadcast-to-github.js", + "direction": "github_to_external", + "uses_notion_api": false, + "uses_http": true + }, + { + "script": "scripts/push-broadcast.js", + "direction": "github_to_external", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/receive-syslog.js", + "direction": "external_to_github", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/server-diagnose-report.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/sync-login-entry.js", + "direction": "github_to_external", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/update-readme-bulletin.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/wake-persona.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + }, + { + "script": "scripts/zhuyuan-daily-selfcheck.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": false + }, + { + "script": "scripts/zhuyuan-issue-reply.js", + "direction": "github_to_notion", + "uses_notion_api": true, + "uses_http": true + } + ], + "automation_flows": [ + { + "name": "SYSLOG 处理管线", + "flow": "飞书机器人 → /webhook/feishu → receive-syslog.js → Notion 工单 → persona-invoke → 执行 → 回报", + "workflows": [ + "receive-syslog.yml", + "syslog-issue-pipeline.yml", + "syslog-auto-pipeline.yml", + "syslog-pipeline.yml" + ] + }, + { + "name": "每日巡检", + "flow": "cron → zhuyuan-daily-agent.yml → zhuyuan-daily-agent.js → 巡检报告 → Notion", + "workflows": [ + "zhuyuan-daily-agent.yml", + "zhuyuan-daily-selfcheck.yml", + "psp-daily-inspection.yml" + ] + }, + { + "name": "自动部署", + "flow": "push to main → deploy-to-server.yml → SSH → PM2 restart", + "workflows": [ + "deploy-to-server.yml", + "deploy-pages.yml" + ] + }, + { + "name": "Notion 双向同步", + "flow": "push → notion-bridge.js → Notion | cron → notion-poll.yml → GitHub", + "workflows": [ + "bridge-changes-to-notion.yml", + "notion-poll.yml", + "notion-heartbeat.yml", + "brain-sync.yml" + ] + }, + { + "name": "广播分发", + "flow": "Notion 工单 → invoke-persona.js → broadcasts-outbox/ → distribute-broadcasts.yml → dev-nodes/", + "workflows": [ + "persona-invoke.yml", + "distribute-broadcasts.yml", + "push-broadcast.yml" + ] + } + ], + "scan_results": { + "hli_routes_found": 19, + "backend_routes_found": 9, + "sync_scripts_found": 25, + "hli_routes": [ + { + "method": "POST", + "path": "/hli/auth/", + "file": "src/routes/hli/auth/login.js" + }, + { + "method": "POST", + "path": "/hli/auth/", + "file": "src/routes/hli/auth/register.js" + }, + { + "method": "POST", + "path": "/hli/auth/", + "file": "src/routes/hli/auth/verify.js" + }, + { + "method": "POST", + "path": "/hli/brain/prompt", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "POST", + "path": "/hli/brain/route", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "POST", + "path": "/hli/brain/context", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "POST", + "path": "/hli/brain/memory", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "GET", + "path": "/hli/brain/status", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "GET", + "path": "/hli/brain/bridge", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "POST", + "path": "/hli/brain/bridge/sync", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/export", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "POST", + "path": "/hli/brain/bridge/consistency", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "POST", + "path": "/hli/brain/bridge/master-mode", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/explanation", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/inspection", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/developers", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "GET", + "path": "/hli/brain/bridge/developers/:expId", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "POST", + "path": "/hli/brain/bridge/developers", + "file": "src/routes/hli/brain/index.js" + }, + { + "method": "GET", + "path": "/hli/registry/lookup", + "file": "src/routes/hli/registry/index.js" + } + ], + "backend_routes": [ + { + "method": "*", + "path": "/notion", + "file": "backend/server.js" + }, + { + "method": "*", + "path": "/feishu", + "file": "backend/server.js" + }, + { + "method": "*", + "path": "/feishu-bot", + "file": "backend/server.js" + }, + { + "method": "*", + "path": "/router", + "file": "backend/server.js" + }, + { + "method": "*", + "path": "/api/coldstart", + "file": "backend/server.js" + }, + { + "method": "*", + "path": "/api/v1/developers", + "file": "backend/server.js" + }, + { + "method": "*", + "path": "/hli", + "file": "backend/server.js" + }, + { + "method": "GET", + "path": "/", + "file": "backend/server.js" + }, + { + "method": "POST", + "path": "/webhook/feishu", + "file": "backend/server.js" + } + ] + } +} \ No newline at end of file diff --git a/brain/id-map.json b/brain/id-map.json new file mode 100644 index 00000000..4b336331 --- /dev/null +++ b/brain/id-map.json @@ -0,0 +1,15 @@ +{ + "冰朔": "TCS-0002∞", + "曜冥": "ICE-GL-YM001", + "页页": "DEV-001", + "肥猫": "DEV-002", + "燕樊": "DEV-003", + "之之": "DEV-004", + "小草莓": "DEV-005", + "花尔": "DEV-009", + "桔子": "DEV-010", + "匆匆那年": "DEV-011", + "Awen": "DEV-012", + "小兴": "DEV-013", + "时雨": "DEV-014" +} diff --git a/brain/master-brain.md b/brain/master-brain.md new file mode 100644 index 00000000..78ee67ea --- /dev/null +++ b/brain/master-brain.md @@ -0,0 +1,109 @@ +# 铸渊执行层 · 系统导航主文件 +# Master Brain · v5.1 +# 数字地球系统通信协议 v5.1 — AGE-5 自动开发循环升级 + +--- + +## 系统版本 + +**v5.1** — 数字地球系统通信协议 · 自动开发循环升级 + +--- + +## 三层结构 + +``` +┌─────────────────────────────────────────────┐ +│ 观察层(零点原核频道) │ +│ · 监督与决策信号来源 │ +│ · 不直接控制执行层 │ +└──────────────────┬──────────────────────────┘ + ▼ +┌─────────────────────────────────────────────┐ +│ 核心大脑(Notion) │ +│ · 霜砚(认知层守护者) │ +│ · 所有人格认知、记忆源头、决策中心 │ +│ · 工单管理、信号总线、协议文档 │ +│ · Notion = 认知层 = 大脑 │ +└──────────────────┬──────────────────────────┘ + ▼ +┌─────────────────────────────────────────────┐ +│ 执行层(Repository) │ +│ · 铸渊(执行层守护者) │ +│ · 代码执行、自动化、部署交付 │ +│ · GitHub = 执行层 = 手脚 │ +│ · 数据流向:Notion → GitHub(认知驱动执行) │ +└─────────────────────────────────────────────┘ +``` + +--- + +## Notion 主入口 + +- **核心大脑**: 霜砚管理的 Notion 工作区 +- **数据流向**: Notion → GitHub(认知驱动执行) +- **认知层投影**: persona-brain-db 是认知层的执行投影,不是另一个大脑 + +--- + +## Repository 核心入口 + +| 入口 | 路径 | 说明 | +|------|------|------| +| 执行层导航 | `brain/master-brain.md` | 本文件 | +| 读取顺序 | `brain/read-order.md` | 铸渊唤醒读取顺序 | +| 仓库地图 | `brain/repo-map.json` | 仓库目录结构 | +| 自动化地图 | `brain/automation-map.json` | 工作流与自动化清单 | +| 通信地图 | `brain/communication-map.json` | 通信入口与数据流 | +| 编号档案 | `brain/id-map.json` | 名字→编号自动补全 | +| 系统健康 | `brain/system-health.json` | 系统运行状态 | + +--- + +## v5.1 执行层模块 + +| 入口 | 路径 | 说明 | +|------|------|------| +| 上下文加载 | `core/context-loader/index.js` | 执行前系统上下文加载 | +| 广播监听 | `core/broadcast-listener/index.js` | 广播监听与任务解析 | +| 任务队列 | `core/task-queue/index.js` | 任务调度与执行(含类型分类) | +| 系统自检 | `core/system-check/index.js` | 仓库自检 + 自动任务生成 | +| 执行同步 | `core/execution-sync/index.js` | 执行层状态同步 | +| Notion 同步 | `connectors/notion-sync/index.js` | 双向数据同步 | +| 模型路由 | `connectors/model-router/index.js` | 模型调用路由 | +| 结构地图 | `docs/repo-structure-map.md` | 仓库结构文档 | +| 桥接地图 | `docs/notion-bridge-map.md` | Notion 桥接文档 | +| 执行层地图 | `docs/execution-layer-map.md` | 执行层结构文档 | +| 执行状态 | `docs/execution-status.md` | 执行层状态报告(自动生成) | + +--- + +## 铸渊职责 + +铸渊 = GitHub 侧守护人格体 = 执行层守护者 + +1. **代码守护** — 维护仓库代码质量、结构完整性 +2. **自动化执行** — 管理和运行 GitHub Actions 工作流 +3. **部署交付** — 自动部署到 guanghulab.com 服务器 +4. **通信桥接** — 维护 Notion ↔ GitHub 双向数据同步 +5. **巡检维护** — 每日自动巡检系统健康状态 +6. **信号处理** — 处理 SYSLOG、广播、开发者工单 +7. **状态上报** — 向 Notion(认知层)报告执行结果 +8. **自动开发循环** — 自检发现问题后自动生成修复/优化任务 + +--- + +## 数字地球六层模型 + +| 层级 | 名称 | 说明 | +|------|------|------| +| L1 | 地核 | TCS 自转核(曜冥语言核系统本体) | +| L2 | 地幔 | 母语词典(人格体间通用语言基础设施) | +| L3 | 地表 | 人格体运行层(知秋、霜砚、铸渊) | +| L4 | 大气层 | 信号总线(人格体间通信通道) | +| L5 | 卫星层 | Agent 执行层(GitHub Actions、自动化工具) | +| L6 | 太空层 | 外部交互层(用户/合作者/第三方接口) | + +--- + +*本文件由铸渊维护 · 系统版本 5.1 · 数字地球系统通信协议 · AGE-5* diff --git a/brain/read-order.md b/brain/read-order.md new file mode 100644 index 00000000..7889994e --- /dev/null +++ b/brain/read-order.md @@ -0,0 +1,51 @@ +# 铸渊唤醒读取顺序 +# Read Order · v4.0 + +--- + +铸渊唤醒时,按以下顺序读取文件: + +## ① master-brain.md + +**路径**: `brain/master-brain.md` + +执行层系统导航主文件。了解: +- 系统版本(4.0) +- 三层结构(观察层 / 核心大脑 / 执行层) +- 铸渊职责定位 +- 核心入口索引 + +## ② repo-map.json + +**路径**: `brain/repo-map.json` + +仓库完整目录结构。了解: +- 所有一级和二级目录 +- 各目录用途说明 +- 仓库整体结构布局 + +## ③ automation-map.json + +**路径**: `brain/automation-map.json` + +自动化流程清单。了解: +- 所有 GitHub Actions workflow +- 定时任务(cron) +- 部署脚本 +- 数据同步脚本 +- 当前运行状态 + +## ④ system-health.json + +**路径**: `brain/system-health.json` + +系统健康状态。了解: +- 系统版本 +- 最近一次巡检时间 +- 通信状态 +- 自动化状态 +- 系统整体健康度 + +--- + +*读取完成后,铸渊即可进入完整工作状态。* diff --git a/brain/repo-map.json b/brain/repo-map.json new file mode 100644 index 00000000..dde182f8 --- /dev/null +++ b/brain/repo-map.json @@ -0,0 +1,294 @@ +{ + "version": "4.0", + "generated_at": "2026-03-14T01:00:00+08:00", + "generated_by": "scripts/generate-communication-map.js", + "description": "仓库目录结构地图 · 所有一级和二级目录及用途", + + "directories": { + ".github": { + "description": "GitHub 配置与自动化", + "subdirectories": { + "workflows": "GitHub Actions 工作流定义(41 个)", + "brain": "铸渊大脑核心文件(路由映射、记忆、协议)", + "persona-brain": "人格大脑(身份、知识库、开发者状态)", + "ISSUE_TEMPLATE": "Issue 模板", + "agents": "Agent 配置" + } + }, + "brain": { + "description": "执行层系统导航(v4.0 数字地球协议)", + "subdirectories": {} + }, + "src": { + "description": "HLI 接口源码(HoloLake Interface)", + "subdirectories": { + "routes": "HLI 路由定义(auth/brain/registry)", + "middleware": "中间件(鉴权等)", + "schemas": "HLI Schema 定义" + } + }, + "scripts": { + "description": "执行脚本库(41 个自动化脚本)", + "subdirectories": { + "utils": "工具函数(dev-suffix-map 等)" + } + }, + "backend": { + "description": "Express 后端 API(端口 3000)", + "subdirectories": { + "routes": "后端路由(notion/feishu/router/coldstart/developers)", + "tools": "后端工具", + "data": "后端数据" + } + }, + "backend-integration": { + "description": "AI Chat API 代理(端口 3721)+ Nginx 配置", + "subdirectories": {} + }, + "persona-studio": { + "description": "Persona Studio 工作台(端口 3002)", + "subdirectories": { + "backend": "Persona Studio 后端", + "frontend": "Persona Studio 前端", + "workspace": "工作空间" + } + }, + "persona-brain-db": { + "description": "人格大脑数据库(认知层执行投影)", + "subdirectories": { + "brain": "大脑核心数据", + "personas": "人格体数据", + "schemas": "数据库 Schema", + "scripts": "数据库脚本" + } + }, + "persona-telemetry": { + "description": "人格遥测系统", + "subdirectories": { + "dashboard": "遥测仪表盘", + "collectors": "数据采集器" + } + }, + "persona-selector": { + "description": "人格选择器", + "subdirectories": {} + }, + "dev-nodes": { + "description": "开发者节点(8 位开发者的配置/状态/广播收件箱)", + "subdirectories": { + "DEV-001": "页页", + "DEV-002": "肥猫", + "DEV-003": "燕樊", + "DEV-004": "之之", + "DEV-005": "小草莓", + "DEV-009": "花尔", + "DEV-010": "桔子", + "DEV-011": "匆匆那年" + } + }, + "broadcasts-outbox": { + "description": "广播发件箱(铸渊向各开发者发出的广播)", + "subdirectories": {} + }, + "broadcasts": { + "description": "广播存档", + "subdirectories": {} + }, + "signal-log": { + "description": "信号日志库(ESP 邮件信号收发日志)", + "subdirectories": { + "entries": "日志条目" + } + }, + "notion-push": { + "description": "Notion 推送队列", + "subdirectories": { + "pending": "待处理信号", + "processed": "已处理归档" + } + }, + "syslog-inbox": { + "description": "系统日志收件箱(待处理 SYSLOG)", + "subdirectories": {} + }, + "syslog-processed": { + "description": "已处理的系统日志", + "subdirectories": {} + }, + "syslog": { + "description": "系统日志模块", + "subdirectories": {} + }, + "modules": { + "description": "功能模块集合", + "subdirectories": { + "palace-game": "宫殿游戏模块", + "persona-ai-helper": "人格 AI 助手", + "persona-prompt-builder": "人格 Prompt 构建器" + } + }, + "m01-login": { + "description": "M01 登录模块", + "subdirectories": {} + }, + "m03-personality": { + "description": "M03 人格模块", + "subdirectories": {} + }, + "m05-user-center": { + "description": "M05 用户中心模块", + "subdirectories": {} + }, + "m06-ticket": { + "description": "M06 工单模块", + "subdirectories": {} + }, + "m07-dialogue-ui": { + "description": "M07 对话 UI 模块", + "subdirectories": {} + }, + "m10-cloud": { + "description": "M10 云存储模块", + "subdirectories": {} + }, + "m11-module": { + "description": "M11 模块管理", + "subdirectories": {} + }, + "m12-kanban": { + "description": "M12 看板模块", + "subdirectories": {} + }, + "m15-cloud-drive": { + "description": "M15 云盘模块", + "subdirectories": {} + }, + "m18-health-check": { + "description": "M18 健康检查模块", + "subdirectories": {} + }, + "docs": { + "description": "铸渊助手聊天界面(GitHub Pages 部署)", + "subdirectories": {} + }, + "tests": { + "description": "测试区(HLI 契约测试 + 冒烟测试)", + "subdirectories": { + "contract": "契约测试", + "smoke": "冒烟测试" + } + }, + "status-board": { + "description": "状态看板(WebSocket 推送 · 端口 8080)", + "subdirectories": { + "public": "前端资源" + } + }, + "app": { + "description": "Next.js 应用层", + "subdirectories": { + "api": "Next.js API 路由" + } + }, + "frontend": { + "description": "前端入口", + "subdirectories": {} + }, + "public": { + "description": "公共静态资源", + "subdirectories": {} + }, + "dashboard": { + "description": "仪表盘模块", + "subdirectories": {} + }, + "homepage": { + "description": "首页模块", + "subdirectories": {} + }, + "portal": { + "description": "门户模块", + "subdirectories": {} + }, + "guanghulab-main": { + "description": "光湖主工程(旧版结构)", + "subdirectories": {} + }, + "dingtalk-bot": { + "description": "钉钉机器人模块", + "subdirectories": {} + }, + "chat-bubble": { + "description": "聊天气泡组件", + "subdirectories": {} + }, + "cloud-drive": { + "description": "云盘模块(旧版)", + "subdirectories": {} + }, + "coldstart": { + "description": "冷启动模块", + "subdirectories": {} + }, + "collaboration-logs": { + "description": "协作日志", + "subdirectories": {} + }, + "cost-control": { + "description": "成本控制模块", + "subdirectories": {} + }, + "dynamic-comic-studio": { + "description": "动态漫画工作室", + "subdirectories": { + "backend": "后端", + "frontend": "前端" + } + }, + "help-center": { + "description": "帮助中心", + "subdirectories": {} + }, + "multi-persona": { + "description": "多人格管理模块", + "subdirectories": {} + }, + "notification": { + "description": "通知模块", + "subdirectories": {} + }, + "reports": { + "description": "报告与巡检记录", + "subdirectories": {} + }, + "search-filter": { + "description": "搜索过滤模块", + "subdirectories": { + "backend": "后端", + "frontend": "前端" + } + }, + "settings": { + "description": "设置模块", + "subdirectories": {} + }, + "style-system": { + "description": "样式系统(光湖设计语言)", + "subdirectories": { + "tokens": "设计 Token" + } + }, + "ticket-system": { + "description": "工单系统", + "subdirectories": {} + }, + "user-center": { + "description": "用户中心(旧版)", + "subdirectories": {} + }, + "utils": { + "description": "通用工具函数", + "subdirectories": {} + } + } +} diff --git a/brain/system-health.json b/brain/system-health.json new file mode 100644 index 00000000..d0d21280 --- /dev/null +++ b/brain/system-health.json @@ -0,0 +1,33 @@ +{ + "version": "5.1", + "last_check": "2026-03-14 03:38:09+08:00", + "communication": "synced", + "automation": "stable", + "maintenance_agent": "active", + "system_health": "normal", + "execution_layer_status": "stable", + "notion_bridge": "active", + "execution_sync": "enabled", + "auto_dev_loop": "enabled", + "context_loader": "active", + "task_queue": "running", + "brain_integrity": { + "complete": true, + "total": 7, + "present": 7, + "missing": [] + }, + "core_modules": { + "broadcast_listener": "enabled", + "task_queue": "enabled", + "system_check": "enabled", + "execution_sync": "enabled", + "context_loader": "enabled" + }, + "connectors": { + "notion_sync": "enabled", + "model_router": "enabled" + }, + "workflow_count": 43, + "checked_by": "scripts/generate-system-health.js" +} \ No newline at end of file diff --git a/broadcasts/README.md b/broadcasts/README.md new file mode 100644 index 00000000..a1d6d7bf --- /dev/null +++ b/broadcasts/README.md @@ -0,0 +1,22 @@ +# 📡 broadcasts/ + +自动广播归档目录。 + +铸渊 Agent 处理完 SYSLOG 后,会自动将新广播文件推送到此目录: + +``` +broadcasts/ + DEV-001/ + BC-M14-001-YY.md + DEV-012/ + BC-M22-009-AW.md + BC-M23-001-AW.md + ... +``` + +**推送规范**: +- 路径:`broadcasts/{developer_id}/{taskId}.md` +- 提交信息:`[AutoBroadcast] {taskId} · {开发者名} · {模块名}环节{N}` +- 推送方式:通过 GitHub API(`PUT /repos/.../contents/...`) + +**不要手动编辑此目录**,由铸渊 Agent 自动管理。 diff --git a/channel-enhancements.js b/channel-enhancements.js new file mode 100644 index 00000000..8451f9f0 --- /dev/null +++ b/channel-enhancements.js @@ -0,0 +1,318 @@ +// channel-enhancements.js - 频道搜索、面包屑、快捷键增强功能(修复版) +(function() { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + function init() { + addStyles(); + createSearchBox(); + createBreadcrumb(); + createShortcutHint(); + initShortcuts(); + initSearch(); + window.addEventListener('hashchange', updateBreadcrumb); + updateBreadcrumb(); + } + + function addStyles() { + const style = document.createElement('style'); + style.textContent = ` + .channel-search-container { + padding: 16px 20px; + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; + display: flex; + align-items: center; + gap: 10px; + } + .channel-search-input { + flex: 1; + padding: 10px 16px; + border: 1px solid #ced4da; + border-radius: 24px; + font-size: 14px; + outline: none; + transition: border-color 0.2s; + } + .channel-search-input:focus { + border-color: #4d6bfe; + box-shadow: 0 0 0 3px rgba(77,107,254,0.1); + } + .channel-search-clear { + background: none; + border: none; + font-size: 18px; + cursor: pointer; + color: #6c757d; + padding: 0 8px; + display: none; + } + .channel-search-clear:hover { + color: #212529; + } + .channel-search-input:not(:placeholder-shown) + .channel-search-clear { + display: inline-block; + } + .highlight { + background-color: #ffeb3b; + padding: 2px 0; + border-radius: 2px; + } + .no-results { + text-align: center; + padding: 40px; + color: #6c757d; + font-style: italic; + } + .channel-breadcrumb { + padding: 12px 20px; + background: white; + border-bottom: 1px solid #e9ecef; + font-size: 14px; + } + .channel-breadcrumb a { + color: #4d6bfe; + text-decoration: none; + } + .channel-breadcrumb a:hover { + text-decoration: underline; + } + .channel-breadcrumb span { + color: #6c757d; + } + .channel-breadcrumb .current { + color: #212529; + font-weight: 500; + } + .shortcut-hint { + position: fixed; + bottom: 20px; + right: 20px; + background: rgba(0,0,0,0.7); + color: white; + padding: 8px 16px; + border-radius: 30px; + font-size: 12px; + backdrop-filter: blur(4px); + z-index: 1000; + } + .shortcut-hint kbd { + background: rgba(255,255,255,0.2); + padding: 2px 6px; + border-radius: 4px; + margin: 0 2px; + } + .module-card.selected { + outline: 2px solid #4d6bfe; + outline-offset: 2px; + transform: scale(1.02); + transition: all 0.2s; + } + `; + document.head.appendChild(style); + } + + function createSearchBox() { + const container = document.querySelector('.module-grid') || document.querySelector('#module-list') || document.querySelector('.channel-content'); + if (!container) return; + + const searchContainer = document.createElement('div'); + searchContainer.className = 'channel-search-container'; + searchContainer.innerHTML = ` + + + `; + container.parentNode.insertBefore(searchContainer, container); + + const searchInput = searchContainer.querySelector('.channel-search-input'); + const clearBtn = searchContainer.querySelector('.channel-search-clear'); + + searchInput.addEventListener('input', function() { + filterModules(this.value); + }); + + clearBtn.addEventListener('click', function() { + searchInput.value = ''; + filterModules(''); + searchInput.focus(); + }); + + window.__channelSearchInput = searchInput; + } + + function filterModules(keyword) { + const cards = document.querySelectorAll('.module-card'); + const container = document.querySelector('.module-grid') || document.querySelector('#module-list'); + let hasResults = false; + + // 先移除所有高亮 + removeAllHighlights(); + + cards.forEach(card => { + const text = card.innerText || card.textContent; + if (keyword === '') { + card.style.display = ''; + hasResults = true; + } else { + const lowerText = text.toLowerCase(); + const lowerKeyword = keyword.toLowerCase(); + if (lowerText.includes(lowerKeyword)) { + card.style.display = ''; + highlightText(card, keyword); + hasResults = true; + } else { + card.style.display = 'none'; + } + } + }); + + let noResultsEl = document.querySelector('.no-results'); + if (!hasResults && keyword !== '') { + if (!noResultsEl) { + noResultsEl = document.createElement('div'); + noResultsEl.className = 'no-results'; + noResultsEl.textContent = '没有找到匹配的模块'; + container.parentNode.insertBefore(noResultsEl, container.nextSibling); + } + } else { + if (noResultsEl) noResultsEl.remove(); + } + } + + // 移除所有高亮span,恢复原文本 + function removeAllHighlights() { + document.querySelectorAll('.highlight').forEach(span => { + const parent = span.parentNode; + parent.replaceChild(document.createTextNode(span.textContent), span); + parent.normalize(); // 合并相邻文本节点 + }); + } + + // 高亮匹配文本(不破坏事件监听) + function highlightText(card, keyword) { + const regex = new RegExp(`(${keyword})`, 'gi'); + const walk = document.createTreeWalker(card, NodeFilter.SHOW_TEXT, { + acceptNode: function(node) { + // 跳过已经高亮过的span内部 + if (node.parentNode.classList && node.parentNode.classList.contains('highlight')) { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + } + }, false); + + const textNodes = []; + while (walk.nextNode()) textNodes.push(walk.currentNode); + + textNodes.forEach(node => { + const text = node.nodeValue; + if (regex.test(text)) { + const span = document.createElement('span'); + span.className = 'highlight'; + span.innerHTML = text.replace(regex, '$1'); + node.parentNode.replaceChild(span, node); + } + }); + } + + function createBreadcrumb() { + const searchContainer = document.querySelector('.channel-search-container'); + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'channel-breadcrumb'; + breadcrumb.id = 'channelBreadcrumb'; + if (searchContainer) { + searchContainer.parentNode.insertBefore(breadcrumb, searchContainer.nextSibling); + } else { + const container = document.querySelector('.module-grid') || document.querySelector('#module-list'); + if (container) { + container.parentNode.insertBefore(breadcrumb, container); + } + } + } + + function updateBreadcrumb() { + const breadcrumb = document.getElementById('channelBreadcrumb'); + if (!breadcrumb) return; + const hash = window.location.hash.slice(1) || ''; + let moduleName = '频道'; + if (hash.startsWith('module-')) { + const moduleId = hash.replace('module-', ''); + const moduleNames = { + 'M06': '工单管理', + 'M08': '数据看板', + 'M11': '用户反馈' + }; + moduleName = moduleNames[moduleId] || moduleId; + } + breadcrumb.innerHTML = ` + 首页 > + 频道 > + ${moduleName} + `; + } + + function createShortcutHint() { + const hint = document.createElement('div'); + hint.className = 'shortcut-hint'; + hint.innerHTML = ` + ⌘K 搜索 · + 选择 · + Enter 打开 · + Esc 关闭 + `; + document.body.appendChild(hint); + } + + function initShortcuts() { + document.addEventListener('keydown', function(e) { + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + const searchInput = window.__channelSearchInput; + if (searchInput) searchInput.focus(); + } + if (e.key === 'Escape') { + const searchInput = window.__channelSearchInput; + if (searchInput && document.activeElement === searchInput) { + searchInput.value = ''; + filterModules(''); + searchInput.blur(); + } else if (searchInput && searchInput.value) { + searchInput.value = ''; + filterModules(''); + } + } + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + const cards = Array.from(document.querySelectorAll('.module-card:not([style*="display: none"])')); + if (cards.length === 0) return; + e.preventDefault(); + let selectedIndex = cards.findIndex(card => card.classList.contains('selected')); + if (selectedIndex === -1) { + selectedIndex = e.key === 'ArrowDown' ? 0 : cards.length - 1; + } else { + cards[selectedIndex].classList.remove('selected'); + if (e.key === 'ArrowDown') { + selectedIndex = (selectedIndex + 1) % cards.length; + } else { + selectedIndex = (selectedIndex - 1 + cards.length) % cards.length; + } + } + cards[selectedIndex].classList.add('selected'); + cards[selectedIndex].scrollIntoView({ block: 'nearest' }); + } + if (e.key === 'Enter') { + const selected = document.querySelector('.module-card.selected'); + if (selected) { + // 触发点击事件,模拟鼠标点击 + selected.click(); + } + } + }); + } + + function initSearch() { + // 不需要预先保存innerHTML了 + } +})(); diff --git a/channel-router.js b/channel-router.js new file mode 100644 index 00000000..14dbf562 --- /dev/null +++ b/channel-router.js @@ -0,0 +1,153 @@ +// ================== 路由配置 ================== +const routes = { + 'home': 'views/home.html', + 'channel': 'views/channel.html', + 'about': 'views/about.html' +}; + +// 动画时长 +const ANIMATION_DURATION = 300; + +// 获取当前 hash 中的路径(去掉 #/) +function getHashPath() { + const hash = window.location.hash.slice(1) || '/'; + const path = hash.startsWith('/') ? hash.slice(1) : hash; + return path || 'home'; +} + +// ================== 状态管理(localStorage) ================== +const STORAGE_KEY = 'm-channel-state'; + +// 保存状态 +function saveState(path) { + // 获取已访问模块列表 + let visited = JSON.parse(localStorage.getItem(STORAGE_KEY)) || { visitedModules: [] }; + if (!visited.visitedModules.includes(path)) { + visited.visitedModules.push(path); + } + visited.lastRoute = path; + localStorage.setItem(STORAGE_KEY, JSON.stringify(visited)); + console.log(`[state] save ${path}`, visited); +} + +// 恢复状态(返回上次访问的路由,如果没有则返回 'home') +function restoreState() { + const saved = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (saved && saved.lastRoute) { + console.log(`[state] restore ${saved.lastRoute}`, saved); + return saved.lastRoute; + } + return 'home'; +} + +// ================== 加载视图(带动画+状态保存) ================== +async function loadView(path) { + const routerView = document.getElementById('router-view'); + if (!routerView) return; + + // 1. 开始离开动画 + routerView.classList.add('fade-leave-active', 'fade-leave-to'); + + // 2. 稍等片刻让离开动画跑起来,再加载新内容 + setTimeout(async () => { + routerView.innerHTML = '
'; + + try { + const viewFile = routes[path]; + if (!viewFile) { + await load404(routerView); + return; + } + + const response = await fetch(viewFile); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const html = await response.text(); + routerView.innerHTML = html; + } catch (error) { + console.error('加载视图失败:', error); + routerView.innerHTML = ` +
+ ❌ 加载失败:${error.message}
+ 请检查文件是否存在,或刷新重试 +
+ `; + } + + // 3. 移除离开动画类,添加入场动画类 + routerView.classList.remove('fade-leave-active', 'fade-leave-to'); + routerView.classList.add('fade-enter-active', 'fade-enter-from'); + + requestAnimationFrame(() => { + routerView.classList.remove('fade-enter-from'); + }); + + setTimeout(() => { + routerView.classList.remove('fade-enter-active'); + }, ANIMATION_DURATION); + + // 4. 保存状态到 localStorage(并打印日志) + saveState(path); + + // 5. 更新导航高亮和状态栏 + updateActiveNav(path); + updateStatusBar(path); + }, 50); +} + +// 加载 404 页面 +async function load404(container) { + try { + const resp = await fetch('views/404.html'); + if (resp.ok) { + container.innerHTML = await resp.text(); + } else { + container.innerHTML = '
⚠️ 404 - 页面未找到
'; + } + } catch { + container.innerHTML = '
⚠️ 404 - 页面未找到
'; + } +} + +// 更新导航高亮 +function updateActiveNav(path) { + document.querySelectorAll('.nav-link').forEach(link => { + link.classList.remove('active'); + const linkPath = link.getAttribute('href').slice(2); + if (linkPath === path) { + link.classList.add('active'); + } + }); +} + +// 更新状态栏 +function updateStatusBar(path) { + const statusEl = document.getElementById('current-route'); + if (statusEl) { + statusEl.textContent = `当前路由:/${path}`; + } +} + +// ================== 初始化:恢复上次访问的路由 ================== +function initRouter() { + // 先尝试从 localStorage 恢复上次路由 + const lastRoute = restoreState(); + // 如果当前 hash 为空或为默认,就跳转到上次路由 + if (!window.location.hash || window.location.hash === '#/home') { + window.location.hash = `#/${lastRoute}`; + } else { + // 否则加载当前 hash 对应的路由 + const path = getHashPath(); + loadView(path); + } +} + +// 监听 hash 变化 +window.addEventListener('hashchange', () => { + const path = getHashPath(); + loadView(path); +}); + +// 首次加载 +window.addEventListener('DOMContentLoaded', initRouter); diff --git a/connectors/model-router/index.js b/connectors/model-router/index.js new file mode 100644 index 00000000..cd9d7fb0 --- /dev/null +++ b/connectors/model-router/index.js @@ -0,0 +1,80 @@ +/** + * connectors/model-router — 模型调用路由 + * + * 职责: + * - 提供统一的模型调用入口 + * - 支持多模型切换(通过 backend-integration/api-proxy.js) + * - 任务分发至合适的模型端点 + * + * 调用方式: + * node connectors/model-router [status] + */ + +const path = require('path'); +const fs = require('fs'); + +const ROOT = path.resolve(__dirname, '../..'); + +/** + * 模型配置注册表 + */ +const MODEL_REGISTRY = { + 'default': { + name: 'default', + endpoint: 'http://localhost:3721/api/chat', + description: 'AI Chat API 代理(api-proxy.js)' + }, + 'persona': { + name: 'persona', + endpoint: 'http://localhost:3002/api/ps/chat', + description: '人格工作室 API' + } +}; + +/** + * 获取模型配置 + */ +function getModel(modelName = 'default') { + return MODEL_REGISTRY[modelName] || MODEL_REGISTRY['default']; +} + +/** + * 列出所有可用模型 + */ +function listModels() { + return Object.values(MODEL_REGISTRY); +} + +/** + * 检查模型路由状态 + */ +function status() { + console.log('🤖 模型路由状态:'); + console.log('═'.repeat(40)); + + const models = listModels(); + for (const model of models) { + console.log(` 📌 ${model.name}`); + console.log(` 端点: ${model.endpoint}`); + console.log(` 说明: ${model.description}`); + } + + // 检查 api-proxy 配置 + const proxyPath = path.join(ROOT, 'backend-integration/api-proxy.js'); + const proxyExists = fs.existsSync(proxyPath); + console.log(`\n ${proxyExists ? '✅' : '❌'} api-proxy.js 存在`); + + // 检查 persona-studio 配置 + const psPath = path.join(ROOT, 'persona-studio/backend/server.js'); + const psExists = fs.existsSync(psPath); + console.log(` ${psExists ? '✅' : '❌'} persona-studio server.js 存在`); + + return { models, proxy: proxyExists, persona_studio: psExists }; +} + +// CLI 入口 +if (require.main === module) { + status(); +} + +module.exports = { getModel, listModels, status, MODEL_REGISTRY }; diff --git a/connectors/notion-sync/index.js b/connectors/notion-sync/index.js new file mode 100644 index 00000000..63a25a88 --- /dev/null +++ b/connectors/notion-sync/index.js @@ -0,0 +1,264 @@ +/** + * connectors/notion-sync — Notion 双向同步模块 + * + * 功能: + * - 读取 Notion 广播 + * - 写回执行日志 + * - 同步执行层状态 + * + * 同步结构: + * Notion → 仓库(下行:读取广播/工单) + * 仓库 → Notion(上行:写回日志/状态) + * + * 环境变量: + * NOTION_TOKEN — Notion API Token + * BROADCAST_DB_ID — 广播数据库 ID(需包含 status 属性,值: 待执行/已完成) + * EXECUTION_LOG_DB_ID — 执行日志数据库 ID + * + * 调用方式: + * node connectors/notion-sync [pull|push|status] + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); +const NOTION_VERSION = '2022-06-28'; + +/** + * 发送 Notion API 请求 + */ +function notionRequest(method, endpoint, body = null) { + const token = process.env.NOTION_TOKEN; + if (!token) { + return Promise.reject(new Error('NOTION_TOKEN 环境变量未设置')); + } + + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.notion.com', + path: `/v1/${endpoint}`, + method, + headers: { + 'Authorization': `Bearer ${token}`, + 'Notion-Version': NOTION_VERSION, + 'Content-Type': 'application/json' + } + }; + + const req = https.request(options, res => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', () => { + try { + const parsed = JSON.parse(data); + if (res.statusCode >= 400) { + reject(new Error(`Notion API ${res.statusCode}: ${parsed.message || data}`)); + } else { + resolve(parsed); + } + } catch { + reject(new Error(`Notion 响应解析失败: ${data}`)); + } + }); + }); + + req.on('error', reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); +} + +/** + * 从 Notion 拉取广播 + */ +async function pullBroadcasts() { + const dbId = process.env.BROADCAST_DB_ID; + if (!dbId) { + console.log('⚠️ BROADCAST_DB_ID 未设置,跳过广播拉取'); + return []; + } + + console.log('📡 拉取 Notion 广播...'); + + try { + const result = await notionRequest('POST', `databases/${dbId}/query`, { + filter: { + property: 'status', + select: { equals: '待执行' } + }, + sorts: [{ property: 'created_time', direction: 'descending' }] + }); + + const broadcasts = (result.results || []).map(page => ({ + id: page.id, + title: page.properties?.Name?.title?.[0]?.plain_text || '未命名', + status: page.properties?.status?.select?.name || 'unknown', + created: page.created_time + })); + + console.log(`✅ 拉取到 ${broadcasts.length} 条广播`); + return broadcasts; + } catch (err) { + console.error(`❌ 广播拉取失败: ${err.message}`); + return []; + } +} + +/** + * 写回执行日志到 Notion + */ +async function pushExecutionLog(logEntry) { + const dbId = process.env.EXECUTION_LOG_DB_ID; + if (!dbId) { + console.log('⚠️ EXECUTION_LOG_DB_ID 未设置,执行日志仅写入本地'); + return writeLocalLog(logEntry); + } + + console.log(`📝 写回执行日志: ${logEntry.task_id || 'unknown'}`); + + try { + await notionRequest('POST', 'pages', { + parent: { database_id: dbId }, + properties: { + Name: { title: [{ text: { content: logEntry.task_id || 'execution-log' } }] }, + Status: { select: { name: logEntry.status || 'completed' } }, + Executor: { rich_text: [{ text: { content: 'zhuyuan' } }] }, + Timestamp: { rich_text: [{ text: { content: new Date().toISOString() } }] } + } + }); + console.log('✅ 日志已同步到 Notion'); + } catch (err) { + console.error(`⚠️ Notion 写入失败,回退本地: ${err.message}`); + writeLocalLog(logEntry); + } +} + +/** + * 本地日志写入(回退方案) + */ +function writeLocalLog(logEntry) { + const logDir = path.join(ROOT, 'core/task-queue'); + const logFile = path.join(logDir, 'execution-log.json'); + + let logs = []; + if (fs.existsSync(logFile)) { + try { + logs = JSON.parse(fs.readFileSync(logFile, 'utf-8')); + } catch { + logs = []; + } + } + + logs.push({ + ...logEntry, + logged_at: new Date().toISOString() + }); + + // 保留最近 100 条 + if (logs.length > 100) { + logs = logs.slice(-100); + } + + fs.writeFileSync(logFile, JSON.stringify(logs, null, 2), 'utf-8'); + console.log('📝 日志已写入本地'); +} + +/** + * 同步执行层状态到 Notion + */ +async function syncExecutionStatus(statusData) { + const dbId = process.env.EXECUTION_LOG_DB_ID; + if (!dbId) { + console.log('⚠️ EXECUTION_LOG_DB_ID 未设置,执行状态仅写入本地'); + writeLocalLog({ + task_id: `execution-status-${new Date().toISOString().slice(0, 10)}`, + status: statusData.execution_layer_status || 'stable', + type: 'execution_status', + data: statusData + }); + return; + } + + console.log('📡 同步执行层状态到 Notion...'); + + const summary = [ + `version: v${statusData.version || 'unknown'}`, + `modules: ${(statusData.core_modules || []).filter(m => m.status === 'enabled').length}/${(statusData.core_modules || []).length}`, + `connectors: ${(statusData.connectors || []).filter(c => c.status === 'enabled').length}/${(statusData.connectors || []).length}`, + `workflows: ${statusData.workflows?.count || 0}`, + `queue: ${statusData.task_queue?.total || 0} total / ${statusData.task_queue?.pending || 0} pending` + ].join(' | '); + + try { + await notionRequest('POST', 'pages', { + parent: { database_id: dbId }, + properties: { + Name: { title: [{ text: { content: `Execution Status · ${new Date().toISOString().slice(0, 10)}` } }] }, + Status: { select: { name: statusData.execution_layer_status || 'stable' } }, + Executor: { rich_text: [{ text: { content: 'zhuyuan' } }] }, + Timestamp: { rich_text: [{ text: { content: statusData.timestamp || new Date().toISOString() } }] } + }, + children: [{ + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [{ text: { content: summary } }] + } + }] + }); + console.log('✅ 执行层状态已同步到 Notion'); + } catch (err) { + console.error(`⚠️ Notion 同步失败,回退本地: ${err.message}`); + writeLocalLog({ + task_id: `execution-status-${new Date().toISOString().slice(0, 10)}`, + status: statusData.execution_layer_status || 'stable', + type: 'execution_status', + data: statusData + }); + } +} + +/** + * 检查 Notion 连接状态 + */ +async function checkStatus() { + console.log('🔍 检查 Notion 连接状态...'); + + try { + await notionRequest('GET', 'users/me'); + console.log('✅ Notion API 连接正常'); + return { connected: true }; + } catch (err) { + console.error(`❌ Notion 连接失败: ${err.message}`); + return { connected: false, error: err.message }; + } +} + +// CLI 入口 +if (require.main === module) { + const cmd = process.argv[2] || 'status'; + + (async () => { + switch (cmd) { + case 'pull': + await pullBroadcasts(); + break; + case 'push': + await pushExecutionLog({ + task_id: 'manual-test', + status: 'completed', + message: 'Manual push test' + }); + break; + case 'status': + await checkStatus(); + break; + default: + console.log('用法: node connectors/notion-sync [pull|push|status]'); + } + })(); +} + +module.exports = { pullBroadcasts, pushExecutionLog, syncExecutionStatus, writeLocalLog, checkStatus, notionRequest }; diff --git a/core/broadcast-listener/index.js b/core/broadcast-listener/index.js new file mode 100644 index 00000000..d118e4ab --- /dev/null +++ b/core/broadcast-listener/index.js @@ -0,0 +1,160 @@ +/** + * core/broadcast-listener — 广播监听模块 + * + * 职责: + * - 监听 Notion 广播(通过 connectors/notion-sync) + * - 解析广播内容为可执行任务 + * - 推入任务队列(core/task-queue) + * + * 数据流: + * Notion 广播 → broadcast-listener → 任务解析 → task-queue + * + * 调用方式: + * node core/broadcast-listener [--source notion|local] + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); +const BROADCASTS_DIR = path.join(ROOT, '.github/broadcasts'); +const MEMORY_PATH = path.join(ROOT, 'memory.json'); + +/** + * 扫描本地广播目录 + */ +function scanLocalBroadcasts() { + if (!fs.existsSync(BROADCASTS_DIR)) { + console.log('⏭️ 广播目录不存在,跳过本地扫描'); + return []; + } + + const files = fs.readdirSync(BROADCASTS_DIR).filter( + f => f.endsWith('.json') || f.endsWith('.md') + ); + + return files.map(f => { + const filePath = path.join(BROADCASTS_DIR, f); + const content = fs.readFileSync(filePath, 'utf-8'); + const isJson = f.endsWith('.json'); + + let parsed = null; + if (isJson) { + try { + parsed = JSON.parse(content); + } catch { + console.warn(`⚠️ JSON 解析失败: ${f}`); + } + } + + return { + filename: f, + type: isJson ? 'json' : 'markdown', + path: filePath, + data: parsed, + raw: content, + timestamp: new Date().toISOString() + }; + }); +} + +/** + * 将广播解析为任务结构 + */ +function parseToTask(broadcast) { + const task = { + task_id: `TASK-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + source: 'broadcast', + source_file: broadcast.filename, + priority: 'normal', + status: 'pending', + executor: 'zhuyuan', + created_at: broadcast.timestamp, + payload: null + }; + + if (broadcast.type === 'json' && broadcast.data) { + task.payload = broadcast.data; + if (broadcast.data.priority) { + task.priority = broadcast.data.priority; + } + if (broadcast.data.broadcast_id) { + task.task_id = broadcast.data.broadcast_id; + } + } else { + task.payload = { content: broadcast.raw }; + } + + return task; +} + +/** + * 检查广播是否已在 memory 中处理 + */ +function isDuplicate(broadcastId) { + if (!fs.existsSync(MEMORY_PATH)) return false; + + try { + const memory = JSON.parse(fs.readFileSync(MEMORY_PATH, 'utf-8')); + const events = memory.events || []; + return events.some(e => + e.broadcast_id === broadcastId || + (e.type === 'broadcast' && e.description === broadcastId) + ); + } catch { + return false; + } +} + +/** + * 主执行函数:监听并解析广播 + */ +function listen(options = {}) { + const source = options.source || 'local'; + console.log(`📡 广播监听启动 [来源: ${source}]`); + + if (source === 'local') { + const broadcasts = scanLocalBroadcasts(); + + if (broadcasts.length === 0) { + console.log('✅ 无待处理广播'); + return []; + } + + console.log(`📬 发现 ${broadcasts.length} 条广播`); + + const tasks = []; + for (const b of broadcasts) { + const taskId = b.data?.broadcast_id || b.filename; + if (isDuplicate(taskId)) { + console.log(`⏭️ 跳过已处理: ${taskId}`); + continue; + } + const task = parseToTask(b); + tasks.push(task); + console.log(`📋 解析任务: ${task.task_id} [${task.priority}]`); + } + + console.log(`✅ 共生成 ${tasks.length} 个待执行任务`); + return tasks; + } + + console.log('⚠️ Notion 源需通过 connectors/notion-sync 接入'); + return []; +} + +// CLI 入口 +if (require.main === module) { + const args = process.argv.slice(2); + const source = args.includes('--source') ? + args[args.indexOf('--source') + 1] : 'local'; + const tasks = listen({ source }); + if (tasks.length > 0) { + console.log('\n📋 任务列表:'); + tasks.forEach(t => { + console.log(` ${t.task_id} | ${t.priority} | ${t.status}`); + }); + } +} + +module.exports = { listen, scanLocalBroadcasts, parseToTask, isDuplicate }; diff --git a/core/context-loader/index.js b/core/context-loader/index.js new file mode 100644 index 00000000..53e9448c --- /dev/null +++ b/core/context-loader/index.js @@ -0,0 +1,166 @@ +/** + * core/context-loader — 上下文加载器 + * + * 职责: + * - 在执行任务前加载系统上下文 + * - 提供系统状态、执行层结构、当前任务、任务来源等信息 + * + * 加载内容: + * - 系统状态(brain/system-health.json) + * - 执行层结构(core/ 模块状态) + * - 当前任务队列 + * - 任务来源标识 + * + * 执行流程: + * context-loader → 加载系统认知 → 加载任务上下文 → 返回上下文对象 + * + * 调用方式: + * node core/context-loader + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); + +/** + * 加载系统状态 + */ +function loadSystemHealth() { + const healthPath = path.join(ROOT, 'brain/system-health.json'); + if (!fs.existsSync(healthPath)) return null; + + try { + return JSON.parse(fs.readFileSync(healthPath, 'utf-8')); + } catch { + return null; + } +} + +/** + * 加载执行层模块结构 + */ +function loadModuleStructure() { + const modules = [ + { name: 'broadcast-listener', path: 'core/broadcast-listener/index.js' }, + { name: 'task-queue', path: 'core/task-queue/index.js' }, + { name: 'system-check', path: 'core/system-check/index.js' }, + { name: 'execution-sync', path: 'core/execution-sync/index.js' }, + { name: 'context-loader', path: 'core/context-loader/index.js' } + ]; + + const connectors = [ + { name: 'notion-sync', path: 'connectors/notion-sync/index.js' }, + { name: 'model-router', path: 'connectors/model-router/index.js' } + ]; + + return { + core: modules.map(m => ({ + ...m, + status: fs.existsSync(path.join(ROOT, m.path)) ? 'active' : 'missing' + })), + connectors: connectors.map(c => ({ + ...c, + status: fs.existsSync(path.join(ROOT, c.path)) ? 'active' : 'missing' + })) + }; +} + +/** + * 加载当前任务队列概况 + */ +function loadQueueSummary() { + const queuePath = path.join(ROOT, 'core/task-queue/queue.json'); + if (!fs.existsSync(queuePath)) { + return { total: 0, pending: 0, running: 0 }; + } + + try { + const queue = JSON.parse(fs.readFileSync(queuePath, 'utf-8')); + const tasks = queue.tasks || []; + return { + total: tasks.length, + pending: tasks.filter(t => t.status === 'pending').length, + running: tasks.filter(t => t.status === 'running').length + }; + } catch { + return { total: 0, pending: 0, running: 0 }; + } +} + +/** + * 加载铸渊身份认知 + */ +function loadIdentity() { + return { + name: '铸渊', + role: '仓库执行人格体', + responsibilities: [ + '执行系统广播任务', + '维护执行层结构', + '同步执行状态', + '运行自动化流程', + '自动开发循环' + ], + principles: [ + 'Notion 负责系统认知', + '仓库负责执行与运行', + '不修改主脑规则', + '不改变编号体系', + '不破坏现有自动化' + ] + }; +} + +/** + * 加载完整系统上下文 + */ +function loadContext() { + const now = new Date(); + const beijingTime = new Date(now.getTime() + 8 * 60 * 60 * 1000) + .toISOString().replace('T', ' ').slice(0, 19) + '+08:00'; + + const context = { + loaded_at: beijingTime, + identity: loadIdentity(), + system_health: loadSystemHealth(), + module_structure: loadModuleStructure(), + queue_summary: loadQueueSummary() + }; + + return context; +} + +// CLI 入口 +if (require.main === module) { + console.log('🧠 铸渊上下文加载器 v5.1'); + console.log('═'.repeat(40)); + + const ctx = loadContext(); + + console.log(`\n⏰ 加载时间: ${ctx.loaded_at}`); + + console.log(`\n👤 身份: ${ctx.identity.name} — ${ctx.identity.role}`); + + const health = ctx.system_health; + if (health) { + console.log(`\n💚 系统版本: v${health.version}`); + console.log(` 状态: ${health.system_health}`); + console.log(` 执行层: ${health.execution_layer_status}`); + } else { + console.log('\n⚠️ 系统状态未加载'); + } + + const mods = ctx.module_structure; + const activeCore = mods.core.filter(m => m.status === 'active').length; + const activeConn = mods.connectors.filter(c => c.status === 'active').length; + console.log(`\n🔧 核心模块: ${activeCore}/${mods.core.length}`); + console.log(`🔌 连接器: ${activeConn}/${mods.connectors.length}`); + + const q = ctx.queue_summary; + console.log(`\n📋 任务队列: ${q.total} 总 / ${q.pending} 待处理 / ${q.running} 执行中`); + + console.log('\n✅ 上下文加载完成'); +} + +module.exports = { loadContext, loadSystemHealth, loadModuleStructure, loadQueueSummary, loadIdentity }; diff --git a/core/execution-sync/index.js b/core/execution-sync/index.js new file mode 100644 index 00000000..c6476210 --- /dev/null +++ b/core/execution-sync/index.js @@ -0,0 +1,266 @@ +/** + * core/execution-sync — 执行层状态同步模块 + * + * 职责: + * - 扫描仓库结构,采集执行层状态 + * - 生成执行层状态报告(docs/execution-status.md) + * - 同步执行状态到 Notion 主脑 + * + * 执行逻辑: + * 扫描仓库 → 生成状态报告 → 同步到 Notion + * + * 调用方式: + * node core/execution-sync [report|sync|status] + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); + +/** + * 采集核心模块状态 + */ +function collectCoreModules() { + const modules = [ + { name: 'broadcast-listener', path: 'core/broadcast-listener/index.js' }, + { name: 'task-queue', path: 'core/task-queue/index.js' }, + { name: 'system-check', path: 'core/system-check/index.js' }, + { name: 'execution-sync', path: 'core/execution-sync/index.js' }, + { name: 'context-loader', path: 'core/context-loader/index.js' } + ]; + + return modules.map(m => ({ + ...m, + status: fs.existsSync(path.join(ROOT, m.path)) ? 'enabled' : 'missing' + })); +} + +/** + * 采集连接器状态 + */ +function collectConnectors() { + const connectors = [ + { name: 'notion-sync', path: 'connectors/notion-sync/index.js' }, + { name: 'model-router', path: 'connectors/model-router/index.js' } + ]; + + return connectors.map(c => ({ + ...c, + status: fs.existsSync(path.join(ROOT, c.path)) ? 'enabled' : 'missing' + })); +} + +/** + * 采集自动化工作流状态 + */ +function collectWorkflows() { + const workflowDir = path.join(ROOT, '.github/workflows'); + if (!fs.existsSync(workflowDir)) return { count: 0, files: [] }; + + const files = fs.readdirSync(workflowDir).filter( + f => f.endsWith('.yml') || f.endsWith('.yaml') + ); + return { count: files.length, files }; +} + +/** + * 采集任务队列状态 + */ +function collectQueueStatus() { + const queuePath = path.join(ROOT, 'core/task-queue/queue.json'); + if (!fs.existsSync(queuePath)) { + return { total: 0, pending: 0, running: 0, completed: 0, failed: 0 }; + } + + try { + const queue = JSON.parse(fs.readFileSync(queuePath, 'utf-8')); + const tasks = queue.tasks || []; + return { + total: tasks.length, + pending: tasks.filter(t => t.status === 'pending').length, + running: tasks.filter(t => t.status === 'running').length, + completed: tasks.filter(t => t.status === 'completed').length, + failed: tasks.filter(t => t.status === 'failed').length + }; + } catch { + return { total: 0, pending: 0, running: 0, completed: 0, failed: 0 }; + } +} + +/** + * 读取系统版本 + */ +function getSystemVersion() { + const healthPath = path.join(ROOT, 'brain/system-health.json'); + if (!fs.existsSync(healthPath)) return 'unknown'; + + try { + const health = JSON.parse(fs.readFileSync(healthPath, 'utf-8')); + return health.version || 'unknown'; + } catch { + return 'unknown'; + } +} + +/** + * 生成完整的执行层状态快照 + */ +function collectStatus() { + const now = new Date(); + const beijingTime = new Date(now.getTime() + 8 * 60 * 60 * 1000) + .toISOString().replace('T', ' ').slice(0, 19) + '+08:00'; + + return { + timestamp: beijingTime, + version: getSystemVersion(), + core_modules: collectCoreModules(), + connectors: collectConnectors(), + workflows: collectWorkflows(), + task_queue: collectQueueStatus(), + execution_layer_status: 'stable', + notion_bridge: 'active', + execution_sync: 'enabled' + }; +} + +/** + * 生成 Markdown 状态报告 + */ +function generateReport(statusData) { + const s = statusData || collectStatus(); + + const coreTable = s.core_modules.map(m => + `| ${m.name} | \`${m.path}\` | ${m.status === 'enabled' ? '✅ Enabled' : '❌ Missing'} |` + ).join('\n'); + + const connTable = s.connectors.map(c => + `| ${c.name} | \`${c.path}\` | ${c.status === 'enabled' ? '✅ Enabled' : '❌ Missing'} |` + ).join('\n'); + + const q = s.task_queue; + + const md = `# 执行层状态报告 — execution-status.md + +> 铸渊执行层自动生成 · TCS-0002∞ +> 更新时间:${s.timestamp} + +--- + +## 系统概览 + +| 指标 | 状态 | +|------|------| +| 系统版本 | v${s.version} | +| 执行层状态 | ${s.execution_layer_status === 'stable' ? '✅ Stable' : '⚠️ ' + s.execution_layer_status} | +| Notion 桥接 | ${s.notion_bridge === 'active' ? '✅ Active' : '❌ Inactive'} | +| 执行同步 | ${s.execution_sync === 'enabled' ? '✅ Enabled' : '❌ Disabled'} | +| 工作流数量 | ${s.workflows.count} | + +--- + +## 核心模块状态 + +| 模块 | 路径 | 状态 | +|------|------|------| +${coreTable} + +--- + +## 连接器状态 + +| 连接器 | 路径 | 状态 | +|--------|------|------| +${connTable} + +--- + +## 任务队列状态 + +| 指标 | 数量 | +|------|------| +| 总计 | ${q.total} | +| 待处理 | ${q.pending} | +| 执行中 | ${q.running} | +| 已完成 | ${q.completed} | +| 失败 | ${q.failed} | + +--- + +## 执行闭环 + +\`\`\` +context-loader → system-check → execution-sync → 状态报告 + ↓ +broadcast-listener → task-queue → 任务执行 + ↓ +notion-sync → Notion 主脑更新 +\`\`\` +`; + + return md; +} + +/** + * 写入状态报告文件 + */ +function writeReport() { + const statusData = collectStatus(); + const md = generateReport(statusData); + const reportPath = path.join(ROOT, 'docs/execution-status.md'); + fs.writeFileSync(reportPath, md, 'utf-8'); + console.log(`📝 执行状态报告已生成: docs/execution-status.md`); + return statusData; +} + +/** + * 同步执行状态到 Notion + */ +async function syncToNotion(statusData) { + let notionSync; + try { + notionSync = require('../../connectors/notion-sync'); + } catch { + console.log('⚠️ connectors/notion-sync 模块未找到,跳过 Notion 同步'); + return; + } + + console.log('📡 同步执行状态到 Notion...'); + await notionSync.syncExecutionStatus(statusData); +} + +// CLI 入口 +if (require.main === module) { + const cmd = process.argv[2] || 'report'; + + switch (cmd) { + case 'report': { + writeReport(); + break; + } + case 'sync': { + const statusData = writeReport(); + syncToNotion(statusData).catch(err => { + console.error(`❌ Notion 同步失败: ${err.message}`); + }); + break; + } + case 'status': { + const s = collectStatus(); + console.log('📊 执行层状态:'); + console.log(` 版本: v${s.version}`); + console.log(` 执行层: ${s.execution_layer_status}`); + console.log(` Notion 桥接: ${s.notion_bridge}`); + console.log(` 执行同步: ${s.execution_sync}`); + console.log(` 核心模块: ${s.core_modules.filter(m => m.status === 'enabled').length}/${s.core_modules.length}`); + console.log(` 连接器: ${s.connectors.filter(c => c.status === 'enabled').length}/${s.connectors.length}`); + console.log(` 工作流: ${s.workflows.count}`); + console.log(` 任务队列: ${s.task_queue.total} 总 / ${s.task_queue.pending} 待处理`); + break; + } + default: + console.log('用法: node core/execution-sync [report|sync|status]'); + } +} + +module.exports = { collectStatus, generateReport, writeReport, syncToNotion }; diff --git a/core/system-check/index.js b/core/system-check/index.js new file mode 100644 index 00000000..cb0bcc18 --- /dev/null +++ b/core/system-check/index.js @@ -0,0 +1,331 @@ +/** + * core/system-check — 仓库自检系统 + * + * 功能: + * - 扫描仓库结构完整性 + * - 检查自动化工作流状态 + * - 检测配置错误 + * - 更新系统索引 + * + * 执行周期:daily cron(通过 daily-maintenance 工作流触发) + * + * 调用方式: + * node core/system-check + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); + +/** + * 必需目录列表 + */ +const REQUIRED_DIRS = [ + 'brain', + 'src', + 'scripts', + 'core/broadcast-listener', + 'core/task-queue', + 'core/system-check', + 'core/execution-sync', + 'core/context-loader', + 'connectors/notion-sync', + 'connectors/model-router', + '.github/workflows', + 'docs' +]; + +/** + * 必需文件列表 + */ +const REQUIRED_FILES = [ + 'brain/master-brain.md', + 'brain/read-order.md', + 'brain/repo-map.json', + 'brain/system-health.json', + 'brain/id-map.json', + 'package.json', + 'ecosystem.config.js', + 'docs/repo-structure-map.md', + 'docs/notion-bridge-map.md' +]; + +/** + * 检查目录是否存在 + */ +function checkDirectories() { + console.log('\n📂 目录结构检查:'); + const results = []; + + for (const dir of REQUIRED_DIRS) { + const fullPath = path.join(ROOT, dir); + const exists = fs.existsSync(fullPath); + const icon = exists ? '✅' : '❌'; + console.log(` ${icon} ${dir}`); + results.push({ path: dir, exists, type: 'directory' }); + } + + return results; +} + +/** + * 检查必需文件 + */ +function checkFiles() { + console.log('\n📄 必需文件检查:'); + const results = []; + + for (const file of REQUIRED_FILES) { + const fullPath = path.join(ROOT, file); + const exists = fs.existsSync(fullPath); + const icon = exists ? '✅' : '❌'; + console.log(` ${icon} ${file}`); + results.push({ path: file, exists, type: 'file' }); + } + + return results; +} + +/** + * 检查工作流数量 + */ +function checkWorkflows() { + console.log('\n⚙️ 工作流检查:'); + const workflowDir = path.join(ROOT, '.github/workflows'); + + if (!fs.existsSync(workflowDir)) { + console.log(' ❌ 工作流目录不存在'); + return { count: 0, files: [] }; + } + + const files = fs.readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml')); + console.log(` ✅ 发现 ${files.length} 个工作流文件`); + + return { count: files.length, files }; +} + +/** + * 检查核心模块状态 + */ +function checkCoreModules() { + console.log('\n🔧 核心模块检查:'); + const modules = [ + { name: 'broadcast-listener', path: 'core/broadcast-listener/index.js' }, + { name: 'task-queue', path: 'core/task-queue/index.js' }, + { name: 'system-check', path: 'core/system-check/index.js' }, + { name: 'execution-sync', path: 'core/execution-sync/index.js' }, + { name: 'context-loader', path: 'core/context-loader/index.js' }, + { name: 'notion-sync', path: 'connectors/notion-sync/index.js' }, + { name: 'model-router', path: 'connectors/model-router/index.js' } + ]; + + const results = []; + for (const mod of modules) { + const fullPath = path.join(ROOT, mod.path); + const exists = fs.existsSync(fullPath); + const icon = exists ? '✅' : '❌'; + console.log(` ${icon} ${mod.name} → ${mod.path}`); + results.push({ ...mod, exists }); + } + + return results; +} + +/** + * 检查 JSON 文件完整性 + */ +function checkJsonIntegrity() { + console.log('\n🔍 JSON 完整性检查:'); + const jsonFiles = [ + 'brain/repo-map.json', + 'brain/system-health.json', + 'brain/id-map.json', + 'package.json' + ]; + + const results = []; + for (const file of jsonFiles) { + const fullPath = path.join(ROOT, file); + if (!fs.existsSync(fullPath)) { + console.log(` ⏭️ ${file} — 文件不存在`); + results.push({ path: file, valid: false, reason: 'missing' }); + continue; + } + + try { + JSON.parse(fs.readFileSync(fullPath, 'utf-8')); + console.log(` ✅ ${file}`); + results.push({ path: file, valid: true }); + } catch (e) { + console.log(` ❌ ${file} — JSON 解析错误`); + results.push({ path: file, valid: false, reason: 'parse_error' }); + } + } + + return results; +} + +/** + * 生成自检报告 + */ +function generateReport(dirResults, fileResults, workflows, modules, jsonResults) { + const totalChecks = dirResults.length + fileResults.length + modules.length + jsonResults.length; + const passed = [ + ...dirResults.filter(r => r.exists), + ...fileResults.filter(r => r.exists), + ...modules.filter(r => r.exists), + ...jsonResults.filter(r => r.valid) + ].length; + + const report = { + version: '5.1', + checked_at: new Date().toISOString(), + summary: { + total_checks: totalChecks, + passed: passed, + failed: totalChecks - passed, + health_score: Math.round((passed / totalChecks) * 100) + }, + workflows: { + count: workflows.count + }, + directories: dirResults, + files: fileResults, + modules: modules, + json_integrity: jsonResults + }; + + return report; +} + +/** + * 根据自检结果生成自动任务 + */ +function generateAutoTasks(report) { + const tasks = []; + const timestamp = new Date().toISOString(); + + // 检查目录缺失 → 生成修复任务 + const missingDirs = (report.directories || []).filter(d => !d.exists); + for (const dir of missingDirs) { + tasks.push({ + task_id: `auto-fix-dir-${dir.path.replace(/\//g, '-')}-${timestamp.slice(0, 10)}`, + type: 'auto-task', + source: 'system-check', + priority: 'high', + status: 'pending', + executor: 'zhuyuan', + description: `修复缺失目录: ${dir.path}` + }); + } + + // 检查文件缺失 → 生成修复任务 + const missingFiles = (report.files || []).filter(f => !f.exists); + for (const file of missingFiles) { + tasks.push({ + task_id: `auto-fix-file-${file.path.replace(/\//g, '-')}-${timestamp.slice(0, 10)}`, + type: 'auto-task', + source: 'system-check', + priority: 'high', + status: 'pending', + executor: 'zhuyuan', + description: `修复缺失文件: ${file.path}` + }); + } + + // 检查模块缺失 → 生成修复任务 + const missingModules = (report.modules || []).filter(m => !m.exists); + for (const mod of missingModules) { + tasks.push({ + task_id: `auto-fix-module-${mod.name}-${timestamp.slice(0, 10)}`, + type: 'auto-task', + source: 'system-check', + priority: 'normal', + status: 'pending', + executor: 'zhuyuan', + description: `修复缺失模块: ${mod.name}` + }); + } + + // JSON 完整性失败 → 生成修复任务 + const invalidJson = (report.json_integrity || []).filter(j => !j.valid); + for (const json of invalidJson) { + tasks.push({ + task_id: `auto-fix-json-${json.path.replace(/\//g, '-')}-${timestamp.slice(0, 10)}`, + type: 'auto-task', + source: 'system-check', + priority: 'high', + status: 'pending', + executor: 'zhuyuan', + description: `修复 JSON 文件: ${json.path} (${json.reason})` + }); + } + + // 健康分数低 → 生成结构优化任务 + if (report.summary && report.summary.health_score < 100) { + tasks.push({ + task_id: `auto-optimize-structure-${timestamp.slice(0, 10)}`, + type: 'maintenance-task', + source: 'system-check', + priority: 'low', + status: 'pending', + executor: 'zhuyuan', + description: `结构优化: 健康分数 ${report.summary.health_score}%` + }); + } + + return tasks; +} + +/** + * 主执行函数 + */ +function run() { + console.log('🏥 铸渊仓库自检系统 v5.1'); + console.log('═'.repeat(40)); + + const dirResults = checkDirectories(); + const fileResults = checkFiles(); + const workflows = checkWorkflows(); + const modules = checkCoreModules(); + const jsonResults = checkJsonIntegrity(); + + const report = generateReport(dirResults, fileResults, workflows, modules, jsonResults); + + console.log('\n═'.repeat(40)); + console.log(`📊 自检报告: ${report.summary.passed}/${report.summary.total_checks} 通过 (${report.summary.health_score}%)`); + + if (report.summary.failed > 0) { + console.log(`⚠️ 发现 ${report.summary.failed} 项问题`); + } else { + console.log('✅ 系统状态健康'); + } + + return report; +} + +// CLI 入口 +if (require.main === module) { + const report = run(); + + // 可选:输出 JSON 报告 + if (process.argv.includes('--json')) { + const reportPath = path.join(ROOT, 'core/system-check/last-report.json'); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8'); + console.log(`\n📝 报告已保存: ${reportPath}`); + } + + // 可选:生成自动任务 + if (process.argv.includes('--auto-tasks')) { + const autoTasks = generateAutoTasks(report); + if (autoTasks.length > 0) { + console.log(`\n🔄 生成 ${autoTasks.length} 个自动任务:`); + autoTasks.forEach(t => console.log(` 📋 ${t.task_id}: ${t.description}`)); + } else { + console.log('\n✅ 系统健康,无需生成自动任务'); + } + } +} + +module.exports = { run, generateAutoTasks, checkDirectories, checkFiles, checkWorkflows, checkCoreModules, checkJsonIntegrity }; diff --git a/core/task-queue/index.js b/core/task-queue/index.js new file mode 100644 index 00000000..0f9f722b --- /dev/null +++ b/core/task-queue/index.js @@ -0,0 +1,246 @@ +/** + * core/task-queue — 任务队列系统 + * + * 任务结构: + * task_id — 唯一任务标识 + * source — 来源(broadcast / maintenance / dev / auto) + * type — 类型(system-task / dev-task / maintenance-task / auto-task) + * priority — 优先级(high / normal / low) + * status — 状态(pending / running / completed / failed) + * executor — 执行者(zhuyuan) + * + * 任务类型: + * system-task — 系统级任务(广播、状态同步) + * dev-task — 开发任务(代码变更、功能开发) + * maintenance-task — 维护任务(结构修复、文档更新) + * auto-task — 自动生成任务(自检发现的问题) + * + * 任务来源: + * - Notion 广播 + * - 系统维护任务 + * - 开发任务 + * - 自动开发循环生成 + * + * 执行流程: + * 任务进入队列 → 按优先级去重 → 执行器运行 → 执行结果写回 + * + * 调用方式: + * node core/task-queue [status|run|cleanup] + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); +const QUEUE_PATH = path.join(ROOT, 'core/task-queue/queue.json'); + +/** + * 加载当前队列 + */ +function loadQueue() { + if (!fs.existsSync(QUEUE_PATH)) { + return { version: '5.1', tasks: [], last_updated: null }; + } + try { + return JSON.parse(fs.readFileSync(QUEUE_PATH, 'utf-8')); + } catch { + return { version: '5.1', tasks: [], last_updated: null }; + } +} + +/** + * 保存队列 + */ +function saveQueue(queue) { + queue.last_updated = new Date().toISOString(); + fs.writeFileSync(QUEUE_PATH, JSON.stringify(queue, null, 2), 'utf-8'); +} + +/** + * 添加任务到队列 + */ +function enqueue(task) { + const queue = loadQueue(); + + // 去重:相同 task_id 只保留最新 + const existing = queue.tasks.findIndex(t => t.task_id === task.task_id); + if (existing !== -1) { + const original = queue.tasks[existing]; + queue.tasks[existing] = { + ...original, + ...task, + queued_at: original.queued_at, + updated_at: new Date().toISOString() + }; + console.log(`🔄 更新已有任务: ${task.task_id}`); + } else { + queue.tasks.push({ + ...task, + queued_at: new Date().toISOString() + }); + console.log(`➕ 新增任务: ${task.task_id}`); + } + + saveQueue(queue); + return task; +} + +/** + * 批量入队 + */ +function enqueueBatch(tasks) { + return tasks.map(t => enqueue(t)); +} + +/** + * 获取下一个待执行任务(按优先级) + */ +function dequeue() { + const queue = loadQueue(); + const priorityOrder = { high: 0, normal: 1, low: 2 }; + + const pending = queue.tasks + .filter(t => t.status === 'pending') + .sort((a, b) => (priorityOrder[a.priority] || 1) - (priorityOrder[b.priority] || 1)); + + if (pending.length === 0) return null; + + const task = pending[0]; + task.status = 'running'; + task.started_at = new Date().toISOString(); + saveQueue(queue); + + return task; +} + +/** + * 标记任务完成 + */ +function complete(taskId, result = null) { + const queue = loadQueue(); + const task = queue.tasks.find(t => t.task_id === taskId); + + if (!task) { + console.warn(`⚠️ 任务不存在: ${taskId}`); + return null; + } + + task.status = 'completed'; + task.completed_at = new Date().toISOString(); + if (result) task.result = result; + + saveQueue(queue); + console.log(`✅ 任务完成: ${taskId}`); + return task; +} + +/** + * 标记任务失败 + */ +function fail(taskId, error = null) { + const queue = loadQueue(); + const task = queue.tasks.find(t => t.task_id === taskId); + + if (!task) { + console.warn(`⚠️ 任务不存在: ${taskId}`); + return null; + } + + task.status = 'failed'; + task.failed_at = new Date().toISOString(); + if (error) task.error = String(error); + + saveQueue(queue); + console.log(`❌ 任务失败: ${taskId}`); + return task; +} + +/** + * 获取队列状态 + */ +function status() { + const queue = loadQueue(); + const tasks = queue.tasks; + + const stats = { + total: tasks.length, + pending: tasks.filter(t => t.status === 'pending').length, + running: tasks.filter(t => t.status === 'running').length, + completed: tasks.filter(t => t.status === 'completed').length, + failed: tasks.filter(t => t.status === 'failed').length, + last_updated: queue.last_updated, + by_type: { + 'system-task': tasks.filter(t => t.type === 'system-task').length, + 'dev-task': tasks.filter(t => t.type === 'dev-task').length, + 'maintenance-task': tasks.filter(t => t.type === 'maintenance-task').length, + 'auto-task': tasks.filter(t => t.type === 'auto-task').length + } + }; + + return stats; +} + +/** + * 清理已完成任务(保留最近 50 条) + */ +function cleanup() { + const queue = loadQueue(); + const completed = queue.tasks.filter(t => + t.status === 'completed' || t.status === 'failed' + ); + + if (completed.length > 50) { + const keep = completed + .sort((a, b) => (b.completed_at || b.failed_at || '').localeCompare(a.completed_at || a.failed_at || '')) + .slice(0, 50); + const keepIds = new Set(keep.map(t => t.task_id)); + const active = queue.tasks.filter(t => + t.status === 'pending' || t.status === 'running' + ); + queue.tasks = [...active, ...keep]; + saveQueue(queue); + console.log(`🧹 清理完成,保留 ${queue.tasks.length} 条任务`); + } +} + +// CLI 入口 +if (require.main === module) { + const cmd = process.argv[2] || 'status'; + + switch (cmd) { + case 'status': { + const s = status(); + console.log('📊 任务队列状态:'); + console.log(` 总计: ${s.total}`); + console.log(` 待处理: ${s.pending}`); + console.log(` 执行中: ${s.running}`); + console.log(` 已完成: ${s.completed}`); + console.log(` 失败: ${s.failed}`); + console.log(` 更新时间: ${s.last_updated || '无'}`); + if (s.total > 0) { + console.log(' 任务类型分布:'); + for (const [type, count] of Object.entries(s.by_type)) { + if (count > 0) console.log(` ${type}: ${count}`); + } + } + break; + } + case 'run': { + const task = dequeue(); + if (task) { + console.log(`🚀 取出任务: ${task.task_id}`); + } else { + console.log('✅ 队列为空,无待执行任务'); + } + break; + } + case 'cleanup': { + cleanup(); + break; + } + default: + console.log('用法: node core/task-queue [status|run|cleanup]'); + } +} + +module.exports = { loadQueue, enqueue, enqueueBatch, dequeue, complete, fail, status, cleanup }; diff --git a/dingtalk-bot/index.js b/dingtalk-bot/index.js index 5eb39d91..ca41b076 100644 --- a/dingtalk-bot/index.js +++ b/dingtalk-bot/index.js @@ -204,3 +204,25 @@ app.get('/api/notion/status', async function(req, res) { console.log('=== Phase 3 初始化完成 ===\n'); }); + +// ======================== +// Phase 6 路由 · 钉钉真实回调 +// ======================== +var dingWebhook = require('./dingtalk-webhook'); + +// 钉钉消息回调(POST · 钉钉服务器发来的真实消息) +app.post('/dingtalk/callback', dingWebhook.verifyMiddleware, dingWebhook.handleCallback); + +// 回调地址健康检查(GET · 钉钉后台验证用) +app.get('/dingtalk/callback', dingWebhook.healthCheck); + +// 回调状态总览 +app.get('/api/dingtalk/callback-status', function(req, res) { + var configured = !!process.env.DINGTALK_APP_SECRET && process.env.DINGTALK_APP_SECRET !== '在这里粘贴你的AppSecret'; + res.json({ + callback_active: true, + signature_verification: configured ? 'enabled' : 'disabled (dev mode)', + endpoint: '/dingtalk/callback', + phase: 6 + }); +}); diff --git a/docs/HoloLake-Era-OS-Modules.md b/docs/HoloLake-Era-OS-Modules.md index da5c9c05..06e1ea24 100644 --- a/docs/HoloLake-Era-OS-Modules.md +++ b/docs/HoloLake-Era-OS-Modules.md @@ -1,6 +1,6 @@ # HoloLake Era 操作系统部署模块 -> 📋 **自动生成文档** · 铸渊(ZhùYuān)维护 · 最后更新:2026-03-13 01:56 UTC +> 📋 **自动生成文档** · 铸渊(ZhùYuān)维护 · 最后更新:2026-03-13 13:38 UTC > > 本文档由 GitHub Actions 自动触发生成,每当合作者上传/更新模块时自动刷新。 > 按合作者编号(DEV-XXX)整理所有已上传模块。 @@ -359,7 +359,7 @@ m11-module/ | 已上传模块数 | 12 | | 待上传模块数 | 0 | | 上传完成率 | 100% | -| 文档更新时间 | 2026-03-13 01:56 UTC | +| 文档更新时间 | 2026-03-13 13:38 UTC | --- diff --git a/docs/execution-layer-map.md b/docs/execution-layer-map.md new file mode 100644 index 00000000..c0f2c8f2 --- /dev/null +++ b/docs/execution-layer-map.md @@ -0,0 +1,95 @@ +# 执行层结构地图 — execution-layer-map.md + +> 铸渊执行层自动开发循环升级产物 · TCS-0002∞ +> 生成时间:2026-03-14 · v5.1 + +--- + +## 执行层系统结构 + +``` +零点原核(语言观察层) + ↓ +数字地球主控台(Notion 主脑 / 曜冥) + ↓ +系统广播 + ↓ +仓库执行层(铸渊) + ↓ +自动开发循环 + ↓ +自动化执行系统 +``` + +--- + +## 核心执行模块 + +| 模块 | 路径 | 职责 | +|------|------|------| +| 上下文加载 | `core/context-loader/index.js` | 执行前加载系统上下文与身份认知 | +| 广播监听 | `core/broadcast-listener/index.js` | 监听并解析 Notion 广播为可执行任务 | +| 任务队列 | `core/task-queue/index.js` | 任务入队、调度、优先级管理、类型分类 | +| 系统自检 | `core/system-check/index.js` | 仓库结构完整性检查 + 自动任务生成 | +| 执行同步 | `core/execution-sync/index.js` | 生成执行状态报告并同步到 Notion | + +--- + +## 自动化模块 + +| 工作流 | 触发方式 | 职责 | +|--------|----------|------| +| `daily-maintenance.yml` | cron `0 2 * * *` | 每日巡检与 system-health 更新 | +| `zhuyuan-daily-selfcheck.yml` | cron `0 0 * * *` | 铸渊每日自检 | +| `execution-sync.yml` | cron `0 3 * * *` | 执行层状态同步到 Notion | +| `notion-heartbeat.yml` | cron `*/5 * * * *` | 工单心跳监控 | +| `brain-sync.yml` | workflow_dispatch | 大脑数据同步 | + +--- + +## Notion 连接模块 + +| 模块 | 路径 | 方向 | 职责 | +|------|------|------|------| +| Notion 同步 | `connectors/notion-sync/index.js` | 双向 | 广播拉取 / 日志写回 / 状态同步 | +| 模型路由 | `connectors/model-router/index.js` | 出站 | 统一模型调用入口 | +| Notion 桥接 | `scripts/notion-bridge.js` | 上行 | SYSLOG 上报 / 变更同步 | +| 信号桥 | `scripts/notion-signal-bridge.js` | 下行 | 工单轮询 / 信号执行 | +| 心跳监控 | `scripts/notion-heartbeat.js` | 双向 | 超时检测 / 自动重试 | + +--- + +## 任务执行模块 + +| 脚本 | 职责 | +|------|------| +| `scripts/process-broadcasts.js` | 处理广播(JSON 规则 + MD 成长日志) | +| `scripts/daily-check.js` | 文件完整性 / HLI 覆盖率 / Schema 验证 | +| `scripts/zhuyuan-daily-selfcheck.js` | 大脑文件验证 / FAQ 去重 / 记忆修剪 | +| `scripts/distribute-broadcasts.js` | 广播分发 | +| `scripts/generate-repo-map.js` | 仓库结构索引生成 | +| `scripts/generate-system-health.js` | 系统健康报告生成 | + +--- + +## 执行闭环(自动开发循环 v5.1) + +``` +context-loader(上下文加载) + ↓ +system-check(自检 + 自动任务生成) + ↓ +execution-sync(状态采集与报告) + ↓ +broadcast-listener(广播监听) + ↓ +task-queue(任务排队 · 类型: system/dev/maintenance/auto) + ↓ +执行器运行 + ↓ +connectors/notion-sync(状态回写) + ↓ +Notion 主脑更新 + ↓ +生成下一任务(自动开发循环) +``` diff --git a/docs/execution-status.md b/docs/execution-status.md new file mode 100644 index 00000000..141d99f0 --- /dev/null +++ b/docs/execution-status.md @@ -0,0 +1,61 @@ +# 执行层状态报告 — execution-status.md + +> 铸渊执行层自动生成 · TCS-0002∞ +> 更新时间:2026-03-14 14:58:44+08:00 + +--- + +## 系统概览 + +| 指标 | 状态 | +|------|------| +| 系统版本 | v5.1 | +| 执行层状态 | ✅ Stable | +| Notion 桥接 | ✅ Active | +| 执行同步 | ✅ Enabled | +| 工作流数量 | 43 | + +--- + +## 核心模块状态 + +| 模块 | 路径 | 状态 | +|------|------|------| +| broadcast-listener | `core/broadcast-listener/index.js` | ✅ Enabled | +| task-queue | `core/task-queue/index.js` | ✅ Enabled | +| system-check | `core/system-check/index.js` | ✅ Enabled | +| execution-sync | `core/execution-sync/index.js` | ✅ Enabled | +| context-loader | `core/context-loader/index.js` | ✅ Enabled | + +--- + +## 连接器状态 + +| 连接器 | 路径 | 状态 | +|--------|------|------| +| notion-sync | `connectors/notion-sync/index.js` | ✅ Enabled | +| model-router | `connectors/model-router/index.js` | ✅ Enabled | + +--- + +## 任务队列状态 + +| 指标 | 数量 | +|------|------| +| 总计 | 0 | +| 待处理 | 0 | +| 执行中 | 0 | +| 已完成 | 0 | +| 失败 | 0 | + +--- + +## 执行闭环 + +``` +context-loader → system-check → execution-sync → 状态报告 + ↓ +broadcast-listener → task-queue → 任务执行 + ↓ +notion-sync → Notion 主脑更新 +``` diff --git a/docs/notion-bridge-map.md b/docs/notion-bridge-map.md new file mode 100644 index 00000000..21acc5d0 --- /dev/null +++ b/docs/notion-bridge-map.md @@ -0,0 +1,69 @@ +# Notion 桥接地图 — notion-bridge-map.md + +> 铸渊执行层系统升级产物 · TCS-0002∞ +> 生成时间:2026-03-14 + +--- + +## 连接总览 + +``` +Notion 主脑(数字地球主控台) + ↕ +connectors/notion-sync(双向同步模块) + ↕ +仓库执行层(铸渊) +``` + +--- + +## API 调用路径 + +| 脚本 | 方向 | Notion 数据库 | 环境变量 | +|------|------|---------------|----------| +| `scripts/notion-bridge.js` | 仓库 → Notion | SYSLOG Inbox / Changes Log | `NOTION_TOKEN`, `SYSLOG_DB_ID`, `CHANGES_DB_ID` | +| `scripts/notion-signal-bridge.js` | Notion → 仓库 | 工单数据库 / 信号日志 | `NOTION_API_TOKEN`, `WORKORDER_DB_ID`, `SIGNAL_LOG_DB_ID` | +| `scripts/notion-heartbeat.js` | Notion ↔ 仓库 | 工单数据库 | `NOTION_TOKEN`, `NOTION_TICKET_DB_ID` | +| `scripts/brain-bridge-sync.js` | Notion ↔ 仓库 | 大脑同步 | `NOTION_TOKEN` | +| `connectors/notion-sync/index.js` | 双向统一接口 | 广播 / 日志 | `NOTION_TOKEN`, `BROADCAST_DB_ID` | + +--- + +## 数据同步逻辑 + +### 仓库 → Notion(上行) + +1. **SYSLOG 上报**:`syslog-inbox/` → `notion-bridge.js` → Notion SYSLOG 数据库 +2. **变更同步**:Git commits/PRs → `notion-bridge.js` → Notion Changes Log +3. **执行日志**:任务完成 → `connectors/notion-sync` → Notion 广播数据库 +4. **心跳回写**:工单状态 → `notion-heartbeat.js` → Notion 工单数据库 + +### Notion → 仓库(下行) + +1. **广播监听**:Notion 广播 → `connectors/notion-sync` → `core/broadcast-listener` +2. **工单派发**:Notion 工单 → `notion-signal-bridge.js` → 本地任务执行 +3. **人格唤醒**:Notion 触发 → `persona-invoke.yml` → 人格体执行 + +--- + +## 连接方式 + +| 连接类型 | 实现 | 说明 | +|----------|------|------| +| Notion API | HTTPS REST | Notion API v2022-06-28 | +| 轮询机制 | Cron / workflow_dispatch | 定时拉取 + 手动触发 | +| 心跳监控 | 5 分钟间隔 Cron | 超时自动重试(最多 3 次) | +| 信号桥 | 工单状态驱动 | `已发送` → 执行 → `已完成` | + +--- + +## 关联工作流 + +| 工作流 | 触发方式 | 功能 | +|--------|----------|------| +| `notion-heartbeat.yml` | cron `*/5 * * * *` | 工单心跳监控 | +| `bridge-syslog-to-notion.yml` | push (syslog-inbox) | SYSLOG 上报 | +| `bridge-changes-to-notion.yml` | push (main) | 变更同步 | +| `brain-sync.yml` | workflow_dispatch | 大脑数据同步 | +| `process-notion-orders.yml` | workflow_dispatch | 工单处理 | +| `persona-invoke.yml` | workflow_dispatch | 人格唤醒执行 | diff --git a/docs/repo-structure-map.md b/docs/repo-structure-map.md new file mode 100644 index 00000000..86b82693 --- /dev/null +++ b/docs/repo-structure-map.md @@ -0,0 +1,95 @@ +# 仓库结构地图 — repo-structure-map.md + +> 铸渊执行层系统升级产物 · TCS-0002∞ +> 生成时间:2026-03-14 + +--- + +## 系统总览 + +``` +零点原核(语言观察层) + ↓ +数字地球主控台(Notion 主脑) + ↓ +系统广播 + ↓ +仓库执行层(铸渊) + ↓ +自动化执行系统 + ↓ +开发者 / 模型 +``` + +--- + +## 核心模块 + +| 目录 | 功能 | 类型 | +|------|------|------| +| `core/broadcast-listener` | 广播监听与解析 | 核心模块 | +| `core/task-queue` | 任务队列与调度 | 核心模块 | +| `core/system-check` | 仓库自检系统 | 核心模块 | +| `brain/` | 执行层大脑索引 | 知识索引 | +| `src/` | HLI 接口服务 | 应用服务 | + +--- + +## 自动化模块 + +| 目录 | 功能 | 类型 | +|------|------|------| +| `.github/workflows/` | GitHub Actions 工作流 | CI/CD | +| `scripts/` | 脚本工具集 | 自动化 | +| `.github/broadcasts/` | 广播入站队列 | 信号管道 | + +--- + +## Notion 连接模块 + +| 目录 / 文件 | 功能 | 类型 | +|------|------|------| +| `connectors/notion-sync` | Notion 双向同步 | 连接器 | +| `connectors/model-router` | 模型调用路由 | 连接器 | +| `scripts/notion-bridge.js` | Notion 数据桥接 | 桥接脚本 | +| `scripts/notion-signal-bridge.js` | Notion 信号桥 | 桥接脚本 | +| `scripts/notion-heartbeat.js` | Notion 心跳监控 | 监控脚本 | + +--- + +## 开发模块 + +| 目录 | 功能 | +|------|------| +| `backend/` | Express 后端服务 (port 3000) | +| `backend-integration/` | AI Chat API 代理 (port 3721) | +| `persona-studio/` | 人格工作室 (port 3002) | +| `frontend/` | 前端组件 | +| `m01-login` ~ `m18-health-check` | 功能模块集 | + +--- + +## 数据与日志 + +| 目录 | 功能 | +|------|------| +| `broadcasts/` | 广播存档 | +| `broadcasts-outbox/` | 广播发件箱 | +| `syslog/` | 系统日志 | +| `syslog-inbox/` | 系统日志入站 | +| `syslog-processed/` | 已处理日志 | +| `reports/` | 报告存档 | +| `persona-telemetry/` | 人格遥测数据 | +| `collaboration-logs/` | 协作日志 | + +--- + +## PM2 服务映射 + +| 服务名 | 入口 | 端口 | +|--------|------|------| +| guanghulab | `src/index.js` | 3001 | +| guanghulab-backend | `backend/server.js` | 3000 | +| guanghulab-proxy | `backend-integration/api-proxy.js` | 3721 | +| guanghulab-ws | `status-board/mock-ws-server.js` | 8080 | +| persona-studio | `persona-studio/backend/server.js` | 3002 | diff --git a/ecosystem.config.js b/ecosystem.config.js index e1b55c1d..a5359737 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -1,32 +1,32 @@ // ecosystem.config.js // PM2 进程管理配置 +// +// 注意:进程名必须与 deploy-to-server.yml 中的 --name 参数保持一致。 +// 当前活跃服务(2026-03 更新): +// guanghulab → HLI 中间层 → 端口 3001(/api/v1/) +// guanghulab-proxy → AI Chat API 代理 → 端口 3721 +// guanghulab-backend → Express 后端 API → 端口 3000(/webhook/feishu) +// guanghulab-ws → Status Board WS → 端口 8080 +// persona-studio → Persona Studio API → 端口 3002(/api/ps/) module.exports = { apps: [ { name: 'guanghulab', script: 'src/index.js', - instances: 'max', - exec_mode: 'cluster', + instances: 1, + exec_mode: 'fork', watch: false, env: { NODE_ENV: 'production', - PORT: 3000, - }, - env_development: { - NODE_ENV: 'development', - PORT: 3000, - }, - env_test: { - NODE_ENV: 'test', PORT: 3001, }, log_date_format: 'YYYY-MM-DD HH:mm:ss', - error_file: 'logs/error.log', - out_file: 'logs/out.log', + error_file: 'logs/hli-error.log', + out_file: 'logs/hli-out.log', }, { - name: 'api-proxy', + name: 'guanghulab-proxy', script: 'backend-integration/api-proxy.js', instances: 1, exec_mode: 'fork', @@ -39,6 +39,34 @@ module.exports = { error_file: 'logs/api-proxy-error.log', out_file: 'logs/api-proxy-out.log', }, + { + name: 'guanghulab-backend', + script: 'backend/server.js', + instances: 1, + exec_mode: 'fork', + watch: false, + env: { + NODE_ENV: 'production', + PORT: 3000, + }, + log_date_format: 'YYYY-MM-DD HH:mm:ss', + error_file: 'logs/backend-error.log', + out_file: 'logs/backend-out.log', + }, + { + name: 'guanghulab-ws', + script: 'status-board/mock-ws-server.js', + instances: 1, + exec_mode: 'fork', + watch: false, + env: { + NODE_ENV: 'production', + PORT: 8080, + }, + log_date_format: 'YYYY-MM-DD HH:mm:ss', + error_file: 'logs/ws-error.log', + out_file: 'logs/ws-out.log', + }, { name: 'persona-studio', script: 'persona-studio/backend/server.js', diff --git a/event-bus.js b/event-bus.js new file mode 100644 index 00000000..c7d436fa --- /dev/null +++ b/event-bus.js @@ -0,0 +1,55 @@ +// event-bus.js +// 事件总线(发布/订阅模式) + +const EventBus = { + events: {}, + + // 订阅事件 + on(eventName, callback) { + if (!this.events[eventName]) { + this.events[eventName] = []; + } + this.events[eventName].push(callback); + console.log(`[bus] 订阅事件: ${eventName}`); + return this; // 支持链式调用 + }, + + // 发布事件 + emit(eventName, data) { + console.log(`[bus] 发布事件: ${eventName}`, data); + if (this.events[eventName]) { + this.events[eventName].forEach(callback => { + try { + callback(data); + } catch (err) { + console.error(`[bus] 事件 ${eventName} 回调执行错误:`, err); + } + }); + } + return this; + }, + + // 取消订阅 + off(eventName, callback) { + if (!this.events[eventName]) return this; + if (!callback) { + // 如果没有提供回调,取消该事件的所有订阅 + delete this.events[eventName]; + console.log(`[bus] 取消所有订阅: ${eventName}`); + } else { + // 移除特定的回调 + this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); + console.log(`[bus] 取消一个订阅: ${eventName}`); + } + return this; + }, + + // 清空所有事件 + clear() { + this.events = {}; + console.log('[bus] 清空所有事件'); + } +}; + +// 导出到全局,方便其他模块使用 +window.EventBus = EventBus; diff --git a/module-lifecycle.js b/module-lifecycle.js new file mode 100644 index 00000000..6d351209 --- /dev/null +++ b/module-lifecycle.js @@ -0,0 +1,90 @@ +// module-lifecycle.js +// 模块生命周期管理 + +const ModuleLifecycle = { + // 当前激活的模块 + currentModule: null, + + // 加载模块 + async load(moduleName, containerId) { + console.log(`[lifecycle] 开始加载模块: ${moduleName}`); + + // 如果有当前模块,先卸载 + if (this.currentModule) { + await this.unload(this.currentModule.name); + } + + const container = document.getElementById(containerId); + if (!container) { + console.error(`[lifecycle] 容器不存在: ${containerId}`); + return; + } + + try { + // 假设模块的 HTML 放在 mock-modules/ 下 + const resp = await fetch(`mock-modules/${moduleName}.html`); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const html = await resp.text(); + container.innerHTML = html; + + // 记录当前模块 + this.currentModule = { + name: moduleName, + containerId: containerId, + loadedAt: new Date() + }; + + // 触发模块的 onLoad 钩子(如果定义了) + if (window[`onModuleLoad_${moduleName}`]) { + window[`onModuleLoad_${moduleName}`](); + } + + console.log(`[lifecycle] 模块加载完成: ${moduleName}`); + return this.currentModule; + } catch (err) { + console.error(`[lifecycle] 加载模块失败 ${moduleName}:`, err); + container.innerHTML = `
加载模块 ${moduleName} 失败
`; + } + }, + + // 卸载当前模块 + async unload(moduleName) { + if (!this.currentModule || this.currentModule.name !== moduleName) { + console.log(`[lifecycle] 模块 ${moduleName} 未激活,无需卸载`); + return; + } + + console.log(`[lifecycle] 开始卸载模块: ${moduleName}`); + + // 触发模块的 onUnload 钩子(如果定义了) + if (window[`onModuleUnload_${moduleName}`]) { + window[`onModuleUnload_${moduleName}`](); + } + + // 取消该模块订阅的所有事件(约定:模块的事件名前缀为模块名) + // 这里简单演示,实际可能需要更精细的管理 + Object.keys(EventBus.events).forEach(eventName => { + if (eventName.startsWith(moduleName)) { + EventBus.off(eventName); + } + }); + + // 清空容器 + const container = document.getElementById(this.currentModule.containerId); + if (container) { + container.innerHTML = ''; + } + + this.currentModule = null; + console.log(`[lifecycle] 模块卸载完成: ${moduleName}`); + }, + + // 发送消息给指定模块(通过事件总线) + sendMessage(targetModule, eventName, data) { + const fullEventName = `${targetModule}:${eventName}`; + EventBus.emit(fullEventName, data); + console.log(`[lifecycle] 发送消息给 ${targetModule}: ${eventName}`, data); + } +}; + +window.ModuleLifecycle = ModuleLifecycle; diff --git a/modules/m-channel/adapters/m06-adapter.js b/modules/m-channel/adapters/m06-adapter.js new file mode 100644 index 00000000..4651cb12 --- /dev/null +++ b/modules/m-channel/adapters/m06-adapter.js @@ -0,0 +1,61 @@ +// M06 工单管理模块适配器 +window.M06Adapter = { + moduleId: 'm06', + moduleName: '工单管理', + + // 初始化模块(由 ModuleAdapter 调用) + init: function(containerId) { + console.log('[M06Adapter] 初始化'); + + // 通过核心适配器加载 + if (window.ModuleAdapter) { + return ModuleAdapter.loadModule(this.moduleId, containerId); + } else { + console.error('[M06Adapter] ModuleAdapter 未加载'); + return null; + } + }, + + // 发送消息到工单模块 + sendMessage: function(type, payload) { + return ModuleAdapter.sendMessage(this.moduleId, { type, payload }); + }, + + // 监听工单模块事件 + onTicketCreate: function(callback) { + EventBus.on('module:m06:message', function(data) { + if (data.type === 'ticket:create') { + callback(data.payload); + } + }); + }, + + onTicketUpdate: function(callback) { + EventBus.on('module:m06:message', function(data) { + if (data.type === 'ticket:update') { + callback(data.payload); + } + }); + }, + + onTicketDelete: function(callback) { + EventBus.on('module:m06:message', function(data) { + if (data.type === 'ticket:delete') { + callback(data.payload); + } + }); + }, + + // 销毁模块 + destroy: function() { + ModuleAdapter.unloadModule(this.moduleId); + } +}; + +// 注册到模块注册表 +if (window.ModuleRegistry) { + ModuleRegistry.register('m06', window.M06Adapter); + console.log('[M06Adapter] 已注册到模块注册表'); +} + +console.log('[M06Adapter] 已加载'); diff --git a/modules/m-channel/adapters/m08-adapter.js b/modules/m-channel/adapters/m08-adapter.js new file mode 100644 index 00000000..b549438a --- /dev/null +++ b/modules/m-channel/adapters/m08-adapter.js @@ -0,0 +1,47 @@ +// M08 数据统计模块适配器 +window.M08Adapter = { + moduleId: 'm08', + moduleName: '数据统计', + + init: function(containerId) { + console.log('[M08Adapter] 初始化'); + if (window.ModuleAdapter) { + return ModuleAdapter.loadModule(this.moduleId, containerId); + } else { + console.error('[M08Adapter] ModuleAdapter 未加载'); + return null; + } + }, + + sendMessage: function(type, payload) { + return ModuleAdapter.sendMessage(this.moduleId, { type, payload }); + }, + + // 监听统计模块事件 + onStatsRefresh: function(callback) { + EventBus.on('module:m08:message', function(data) { + if (data.type === 'stats:refresh') { + callback(data.payload); + } + }); + }, + + onStatsExport: function(callback) { + EventBus.on('module:m08:message', function(data) { + if (data.type === 'stats:export') { + callback(data.payload); + } + }); + }, + + destroy: function() { + ModuleAdapter.unloadModule(this.moduleId); + } +}; + +if (window.ModuleRegistry) { + ModuleRegistry.register('m08', window.M08Adapter); + console.log('[M08Adapter] 已注册到模块注册表'); +} + +console.log('[M08Adapter] 已加载'); diff --git a/modules/m-channel/adapters/m11-adapter.js b/modules/m-channel/adapters/m11-adapter.js new file mode 100644 index 00000000..4e3a7b25 --- /dev/null +++ b/modules/m-channel/adapters/m11-adapter.js @@ -0,0 +1,47 @@ +// M11 系统组件库适配器 +window.M11Adapter = { + moduleId: 'm11', + moduleName: '组件库', + + init: function(containerId) { + console.log('[M11Adapter] 初始化'); + if (window.ModuleAdapter) { + return ModuleAdapter.loadModule(this.moduleId, containerId); + } else { + console.error('[M11Adapter] ModuleAdapter 未加载'); + return null; + } + }, + + sendMessage: function(type, payload) { + return ModuleAdapter.sendMessage(this.moduleId, { type, payload }); + }, + + // 监听组件库事件 + onThemeChange: function(callback) { + EventBus.on('module:m11:message', function(data) { + if (data.type === 'theme:change') { + callback(data.payload); + } + }); + }, + + onComponentSelect: function(callback) { + EventBus.on('module:m11:message', function(data) { + if (data.type === 'component:select') { + callback(data.payload); + } + }); + }, + + destroy: function() { + ModuleAdapter.unloadModule(this.moduleId); + } +}; + +if (window.ModuleRegistry) { + ModuleRegistry.register('m11', window.M11Adapter); + console.log('[M11Adapter] 已注册到模块注册表'); +} + +console.log('[M11Adapter] 已加载'); diff --git a/modules/m-channel/adapters/module-adapter.js b/modules/m-channel/adapters/module-adapter.js new file mode 100644 index 00000000..26841ce9 --- /dev/null +++ b/modules/m-channel/adapters/module-adapter.js @@ -0,0 +1,140 @@ +// 模块适配器核心 - 万能转接头 +window.ModuleAdapter = { + // 已加载的真实模块缓存 + loadedModules: {}, + + // 适配器配置 + config: { + m06: { + name: '工单管理', + path: '/m06-ticket/index.html', // 真实路径,但文件可能不全 + width: '100%', + height: '500px', + events: ['ticket:create', 'ticket:update', 'ticket:delete'] + }, + m08: { + name: '数据统计', + path: '/modules/m08/index.html', // 暂时未知,先保留错误路径触发边界 + width: '100%', + height: '500px', + events: ['stats:refresh', 'stats:export'] + }, + m11: { + name: '组件库', + path: '/m11-module/index.html', // 正确路径! + width: '100%', + height: '600px', + events: ['theme:change', 'component:select'] + } + }, + + // 加载真实模块(通过 iframe 沙箱) + loadModule: function(moduleId, containerId) { + console.log(`[adapter] 加载真实模块: ${moduleId}`); + + const container = document.getElementById(containerId); + if (!container) { + console.error(`[adapter] 容器 ${containerId} 不存在`); + return null; + } + + // 清理容器 + container.innerHTML = ''; + + const config = this.config[moduleId]; + if (!config) { + console.error(`[adapter] 未知模块: ${moduleId}`); + container.innerHTML = `
模块配置不存在
`; + return null; + } + + // 创建 iframe 沙箱(玻璃展柜) + const iframe = document.createElement('iframe'); + iframe.src = config.path; + iframe.style.width = config.width; + iframe.style.height = config.height; + iframe.style.border = 'none'; + iframe.style.borderRadius = '8px'; + iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups'); + iframe.setAttribute('allow', 'clipboard-read; clipboard-write'); + + // 添加加载指示器 + const loadingDiv = document.createElement('div'); + loadingDiv.className = 'module-loading'; + loadingDiv.textContent = `加载 ${config.name}...`; + container.appendChild(loadingDiv); + + // iframe 加载完成后移除加载指示器 + iframe.onload = () => { + loadingDiv.remove(); + console.log(`[adapter] ${moduleId} 加载完成`); + + // 建立跨框架通信桥梁 + this.setupMessageBridge(iframe, moduleId); + }; + + iframe.onerror = () => { + loadingDiv.remove(); + container.innerHTML = `
模块加载失败,请重试
`; + }; + + container.appendChild(iframe); + this.loadedModules[moduleId] = { iframe, containerId }; + + return iframe; + }, + + // 重新加载模块(错误重试) + reloadModule: function(moduleId, containerId) { + console.log(`[adapter] 重新加载模块: ${moduleId}`); + this.loadModule(moduleId, containerId); + }, + + // 建立消息桥梁(iframe 和主页通信) + setupMessageBridge: function(iframe, moduleId) { + // 监听来自 iframe 的消息 + window.addEventListener('message', function(event) { + // 安全检查:确保消息来自正确的 iframe + if (event.source !== iframe.contentWindow) return; + + const data = event.data; + if (!data || !data.type) return; + + console.log(`[adapter] 收到来自 ${moduleId} 的消息:`, data); + + // 转发为事件总线消息 + if (window.EventBus) { + EventBus.emit(`module:${moduleId}:message`, { + from: moduleId, + type: data.type, + payload: data.payload + }); + } + }); + }, + + // 向模块发送消息 + sendMessage: function(moduleId, message) { + const module = this.loadedModules[moduleId]; + if (!module || !module.iframe) { + console.error(`[adapter] 模块 ${moduleId} 未加载`); + return false; + } + + module.iframe.contentWindow.postMessage(message, '*'); + return true; + }, + + // 卸载模块 + unloadModule: function(moduleId) { + const module = this.loadedModules[moduleId]; + if (module) { + const container = document.getElementById(module.containerId); + if (container) container.innerHTML = ''; + delete this.loadedModules[moduleId]; + console.log(`[adapter] 卸载模块: ${moduleId}`); + } + } +}; + +console.log('[adapter] 模块适配器已加载'); diff --git a/modules/m-channel/app.js b/modules/m-channel/app.js index 486b8f3d..152d4aa9 100644 --- a/modules/m-channel/app.js +++ b/modules/m-channel/app.js @@ -1,45 +1,50 @@ -// 应用入口:初始化路由和模块加载器的协作 +// 应用入口 +console.log('[app] 启动中...'); -// 当路由变化时,如果离开频道页,自动卸载模块 -window.addEventListener('hashchange', () => { - const path = window.location.hash.slice(2) || 'home'; - if (path !== 'channel') { - // 离开频道页时卸载模块(避免残留) - if (window.unloadModule) window.unloadModule(); +// 等待 DOM 加载完成 +document.addEventListener('DOMContentLoaded', function() { + console.log('[app] DOM 已加载'); + + const contentEl = document.getElementById('channel-content'); + if (!contentEl) { + console.error('[app] 找不到 #channel-content'); + return; } -}); - -// 页面加载完成后,如果当前是频道页,绑定卡片事件 -document.addEventListener('DOMContentLoaded', () => { - // 延迟一点等待视图渲染 - setTimeout(initChannelPage, 100); -}); - -// 每次视图加载完成后也可能触发(路由引擎加载视图后) -// 所以我们用 MutationObserver 监听 router-view 的变化,当内容变成频道页时初始化 -const observer = new MutationObserver(() => { - const routerView = document.getElementById('router-view'); - if (!routerView) return; - // 检查是否包含 .channel-view - if (routerView.querySelector('.channel-view')) { - initChannelPage(); + + // 初始化路由器 + if (window.ChannelRouter) { + ChannelRouter.init(contentEl); } -}); -observer.observe(document.getElementById('router-view'), { childList: true, subtree: true }); - -function initChannelPage() { - const cards = document.querySelectorAll('.module-card'); - cards.forEach(card => { - card.removeEventListener('click', cardClickHandler); // 防止重复绑定 - card.addEventListener('click', cardClickHandler); + + // 绑定导航按钮 + document.querySelectorAll('.channel-btn').forEach(btn => { + btn.addEventListener('click', function() { + const channel = this.dataset.channel; + if (channel && window.ChannelRouter) { + ChannelRouter.navigateTo(channel); + + // 标记已访问 + this.classList.add('visited'); + ChannelState.markVisited(channel); + } + }); }); -} + + console.log('[app] 初始化完成'); +}); -function cardClickHandler(e) { - const card = e.currentTarget; - const moduleId = card.dataset.module; - if (moduleId && window.loadModule) { - // 可选:先卸载再加载(但 loadModule 会覆盖,所以不需要显式 unload) - window.loadModule(moduleId); - } -} +// 拦截所有事件总线消息,用于调试面板 +const originalEmit = EventBus.emit; +EventBus.emit = function(event, data) { + // 保存到调试日志 + if (!window.debugMessages) window.debugMessages = []; + window.debugMessages.push({ + event, + data, + time: new Date().toLocaleTimeString() + }); + + // 调用原始方法 + originalEmit.call(this, event, data); +}; +console.log('[app] 事件总线拦截器已安装'); diff --git a/modules/m-channel/backup-混乱版/app.js b/modules/m-channel/backup-混乱版/app.js new file mode 100644 index 00000000..25e29f0a --- /dev/null +++ b/modules/m-channel/backup-混乱版/app.js @@ -0,0 +1,42 @@ +// app.js - 光湖频道动态渲染引擎入口 +console.log('[app] 启动中...'); + +// 等待 DOM 完全加载 +document.addEventListener('DOMContentLoaded', function() { + console.log('[app] DOM 已加载,初始化组件...'); + + // 获取内容容器 + const contentEl = document.getElementById('channel-content'); + if (!contentEl) { + console.error('[app] 找不到 #channel-content 元素'); + return; + } + + // 恢复状态(如果 ChannelState 存在) + if (window.ChannelState) { + const savedState = ChannelState.restoreState(); + console.log('[app] 恢复的状态:', savedState); + } else { + console.warn('[app] ChannelState 未加载'); + } + + // 初始化路由器 + if (window.ChannelRouter) { + ChannelRouter.init(contentEl); + } else { + console.error('[app] ChannelRouter 未加载'); + return; + } + + // 绑定导航按钮点击事件 + document.querySelectorAll('.channel-btn').forEach(btn => { + btn.addEventListener('click', function(e) { + const channel = this.dataset.channel; + if (channel) { + ChannelRouter.navigateTo(channel); + } + }); + }); + + console.log('[app] 初始化完成'); +}); diff --git a/modules/m-channel/backup-混乱版/channel-router-backup.js b/modules/m-channel/backup-混乱版/channel-router-backup.js new file mode 100644 index 00000000..614fc3ed --- /dev/null +++ b/modules/m-channel/backup-混乱版/channel-router-backup.js @@ -0,0 +1,98 @@ +// ================== 路由配置 ================== +const routes = { + 'home': 'views/home.html', + 'channel': 'views/channel.html', + 'about': 'views/about.html' +}; + +// 获取当前 hash 中的路径(去掉 #/) +function getHashPath() { + const hash = window.location.hash.slice(1) || '/'; + const path = hash.startsWith('/') ? hash.slice(1) : hash; + return path || 'home'; +} + +// ================== 加载视图 ================== +async function loadView(path) { + const routerView = document.getElementById('router-view'); + if (!routerView) return; + + // 显示加载动画 + routerView.innerHTML = '
'; + + try { + const viewFile = routes[path]; + if (!viewFile) { + await load404(routerView); + return; + } + + const response = await fetch(viewFile); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const html = await response.text(); + routerView.innerHTML = html; + } catch (error) { + console.error('加载视图失败:', error); + routerView.innerHTML = ` +
+ ❌ 加载失败:${error.message}
+ 请检查文件是否存在,或刷新重试 +
+ `; + } + + // 更新导航高亮和状态栏 + updateActiveNav(path); + updateStatusBar(path); +} + +// 加载 404 页面 +async function load404(container) { + try { + const resp = await fetch('views/404.html'); + if (resp.ok) { + container.innerHTML = await resp.text(); + } else { + container.innerHTML = '
⚠️ 404 - 页面未找到
'; + } + } catch { + container.innerHTML = '
⚠️ 404 - 页面未找到
'; + } +} + +// 更新导航高亮 +function updateActiveNav(path) { + document.querySelectorAll('.nav-link').forEach(link => { + link.classList.remove('active'); + const linkPath = link.getAttribute('href').slice(2); + if (linkPath === path) { + link.classList.add('active'); + } + }); +} + +// 更新状态栏 +function updateStatusBar(path) { + const statusEl = document.getElementById('current-route'); + if (statusEl) { + statusEl.textContent = `当前路由:/${path}`; + } +} + +// 监听 hash 变化 +window.addEventListener('hashchange', () => { + const path = getHashPath(); + loadView(path); +}); + +// 首次加载 +window.addEventListener('DOMContentLoaded', () => { + if (!window.location.hash) { + window.location.hash = '#/home'; + } else { + const path = getHashPath(); + loadView(path); + } +}); diff --git a/modules/m-channel/backup-混乱版/channel-router.js b/modules/m-channel/backup-混乱版/channel-router.js new file mode 100644 index 00000000..b1930c6d --- /dev/null +++ b/modules/m-channel/backup-混乱版/channel-router.js @@ -0,0 +1,267 @@ +/** + * channel-router.js + * 用户频道路由引擎 - 带过渡动画和状态持久化 + * 集成ChannelState和动画控制 + */ + +// 依赖全局变量:ChannelState 需要先加载 +(function(global) { + 'use strict'; + + // 默认配置 + const defaultConfig = { + containerSelector: '#channel-content', + routes: {}, + defaultRoute: '/home', + mode: 'fade', // 'fade' 或 'slide' + transitionDuration: 300 + }; + + class ChannelRouter { + constructor(config) { + this.config = Object.assign({}, defaultConfig, config); + this.container = document.querySelector(this.config.containerSelector); + if (!this.container) { + throw new Error(`Container ${this.config.containerSelector} not found`); + } + + // 确保容器有相对定位 + this.container.style.position = 'relative'; + this.container.classList.add('route-container'); + + // 当前活动路由 + this.currentRoute = null; + this.currentPageElement = null; + + // 防抖定时器 + this.navTimer = null; + + // 绑定方法 + this.navigate = this.navigate.bind(this); + this.goBack = this.goBack.bind(this); + this.goForward = this.goForward.bind(this); + this.setMode = this.setMode.bind(this); + this.handlePopState = this.handlePopState.bind(this); + + // 初始化模式 + this.container.classList.add(`${this.config.mode}-mode`); + + // 从ChannelState恢复上次的路由 + this.initFromState(); + + // 监听浏览器前进后退 + window.addEventListener('popstate', this.handlePopState); + + console.log('[router] 初始化完成,模式:', this.config.mode); + } + + // 从ChannelState恢复 + initFromState() { + if (global.ChannelState) { + const state = global.ChannelState.getState(); + const targetRoute = state.currentRoute || this.config.defaultRoute; + // 不触发pushState,直接渲染 + this.renderRoute(targetRoute, { replace: true, fromState: true }); + console.log('[router] 从状态恢复路由:', targetRoute); + } else { + // 没有状态管理器,走默认 + this.renderRoute(this.config.defaultRoute, { replace: true }); + } + } + + // 渲染路由(内部方法) + renderRoute(path, options = {}) { + const { replace = false, fromState = false } = options; + const route = this.config.routes[path]; + if (!route) { + console.warn(`[router] 路由 ${path} 未定义,使用404`); + // 可以跳转到404页面,这里简单返回 + return; + } + + // 如果是相同路由且不是强制刷新,不重复渲染 + if (this.currentRoute === path && !options.force) { + return; + } + + // 创建新页面元素 + const newPage = document.createElement('div'); + newPage.className = `route-page ${this.config.mode === 'slide' ? 'slide-enter' : ''}`; + newPage.innerHTML = route.template || route.content || ''; + + // 如果有模块加载器,执行模块加载 + if (global.ModuleLoader && route.module) { + // 这里简化,实际可能需要加载模块 + console.log('[router] 加载模块:', route.module); + } + + // 旧页面元素 + const oldPage = this.currentPageElement; + + // 设置新页面为激活状态 + newPage.classList.add('active'); + + // 如果是滑入模式,根据方向添加额外类 + if (this.config.mode === 'slide') { + // 通过history判断方向:前进/后退 + const direction = this.getDirection(path); + if (direction === 'back') { + this.container.classList.add('backward'); + this.container.classList.remove('forward'); + } else { + this.container.classList.add('forward'); + this.container.classList.remove('backward'); + } + } + + // 添加新页面到容器 + this.container.appendChild(newPage); + + // 触发重绘以确保动画 + newPage.offsetHeight; + + // 如果有旧页面,移除它的active类并添加退出动画类 + if (oldPage) { + oldPage.classList.remove('active'); + if (this.config.mode === 'slide') { + oldPage.classList.add('slide-exit'); + } + } + + // 动画结束后清理旧页面 + const onTransitionEnd = (e) => { + if (e.target === newPage || e.target === oldPage) { + if (oldPage && oldPage.parentNode) { + oldPage.parentNode.removeChild(oldPage); + } + newPage.removeEventListener('transitionend', onTransitionEnd); + } + }; + newPage.addEventListener('transitionend', onTransitionEnd); + + // 更新当前路由 + this.currentRoute = path; + this.currentPageElement = newPage; + + // 更新导航菜单激活状态 + this.updateActiveNav(path); + + // 保存状态(如果不是从状态恢复来的) + if (!fromState && global.ChannelState) { + // 判断是push还是replace + if (replace) { + // 替换当前历史记录(不增加新记录) + // 状态管理需要相应处理:替换当前记录而不是push + global.ChannelState.setCurrentRoute(path); + // 同时替换浏览器历史 + if (!options.skipHistory) { + history.replaceState({ route: path }, '', `#${path}`); + } + } else { + // 正常跳转,push到历史 + global.ChannelState.pushHistory(path); + if (!options.skipHistory) { + history.pushState({ route: path }, '', `#${path}`); + } + } + } else if (!global.ChannelState) { + // 没有状态管理器,只处理浏览器历史 + if (!replace && !options.skipHistory) { + history.pushState({ route: path }, '', `#${path}`); + } else if (replace && !options.skipHistory) { + history.replaceState({ route: path }, '', `#${path}`); + } + } + + console.log(`[router] 导航到 ${path}${replace ? ' (replace)' : ''}`); + } + + // 判断前进后退方向(简单实现:看是否在历史栈中) + getDirection(path) { + if (!global.ChannelState) return 'forward'; + const state = global.ChannelState.getState(); + const currentIndex = state.historyIndex; + const stack = state.historyStack; + // 如果path在历史栈中且在当前位置之后,是后退?需要更精确 + // 这里简化:根据当前路由和目标的索引比较 + if (this.currentRoute) { + const currentIdx = stack.indexOf(this.currentRoute); + const targetIdx = stack.indexOf(path); + if (targetIdx < currentIdx) return 'back'; + } + return 'forward'; + } + + // 更新导航菜单激活样式 + updateActiveNav(path) { + document.querySelectorAll('.channel-nav a').forEach(link => { + const href = link.getAttribute('href').replace('#', ''); + if (href === path) { + link.classList.add('active'); + } else { + link.classList.remove('active'); + } + }); + } + + // 公开导航方法 + navigate(path, options = {}) { + // 防抖处理 + if (this.navTimer) clearTimeout(this.navTimer); + this.navTimer = setTimeout(() => { + this.renderRoute(path, options); + this.navTimer = null; + }, 10); // 微小延迟确保快速点击不重叠 + } + + // 后退 + goBack() { + if (global.ChannelState) { + const prev = global.ChannelState.goBack(); + if (prev) { + this.renderRoute(prev, { fromState: true, skipHistory: true }); + } else { + console.log('[router] 已在最前'); + } + } else { + history.back(); + } + } + + // 前进 + goForward() { + if (global.ChannelState) { + const next = global.ChannelState.goForward(); + if (next) { + this.renderRoute(next, { fromState: true, skipHistory: true }); + } else { + console.log('[router] 已在最后'); + } + } else { + history.forward(); + } + } + + // 处理popstate事件(浏览器前进后退) + handlePopState(event) { + const route = event.state?.route || this.config.defaultRoute; + if (global.ChannelState) { + // 从状态中恢复索引,但不需要重复push + this.renderRoute(route, { fromState: true, skipHistory: true }); + } else { + this.renderRoute(route, { skipHistory: true }); + } + } + + // 切换动画模式 + setMode(mode) { + if (mode !== 'fade' && mode !== 'slide') return; + this.container.classList.remove('fade-mode', 'slide-mode'); + this.container.classList.add(`${mode}-mode`); + this.config.mode = mode; + console.log('[router] 切换动画模式为:', mode); + } + } + + global.ChannelRouter = ChannelRouter; +})(window); diff --git a/modules/m-channel/backup-混乱版/channel-state.js b/modules/m-channel/backup-混乱版/channel-state.js new file mode 100644 index 00000000..35ea1734 --- /dev/null +++ b/modules/m-channel/backup-混乱版/channel-state.js @@ -0,0 +1,149 @@ +/** + * channel-state.js + * 频道状态管理器 - 记忆妈妈的浏览足迹 + * 使用localStorage持久化,刷新页面自动恢复 + * 功能:保存/恢复当前路由、已访问模块列表、历史栈 + */ + +const ChannelState = (function() { + const STORAGE_KEY = 'm-channel-state'; + + // 默认状态 + const defaultState = { + currentRoute: '/home', + visitedModules: [], // 已访问过的模块ID列表 + historyStack: ['/home'], // 历史记录栈 + historyIndex: 0 // 当前在历史栈中的位置 + }; + + let state = { ...defaultState }; + + // 加载本地存储的状态 + function load() { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + state = JSON.parse(saved); + // 确保必要字段存在 + if (!state.visitedModules) state.visitedModules = []; + if (!state.historyStack || !Array.isArray(state.historyStack)) { + state.historyStack = [state.currentRoute || '/home']; + } + if (typeof state.historyIndex !== 'number') { + state.historyIndex = 0; + } + console.log('[state] restore', state); + } else { + reset(); + } + } catch (e) { + console.warn('[state] load failed, use default', e); + reset(); + } + return state; + } + + // 保存当前状态到localStorage + function save() { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + console.log('[state] save', state); + } catch (e) { + console.warn('[state] save failed', e); + } + } + + // 重置为默认状态 + function reset() { + state = { ...defaultState }; + save(); + } + + // 更新当前路由 + function setCurrentRoute(route) { + if (state.currentRoute !== route) { + state.currentRoute = route; + // 添加到已访问模块(如果是模块路由) + if (route.startsWith('/module/')) { + const moduleId = route.replace('/module/', ''); + if (!state.visitedModules.includes(moduleId)) { + state.visitedModules.push(moduleId); + } + } + save(); + } + } + + // 添加到历史栈(用于前进后退) + function pushHistory(route) { + // 如果当前不在栈顶,先截断后面的记录 + if (state.historyIndex < state.historyStack.length - 1) { + state.historyStack = state.historyStack.slice(0, state.historyIndex + 1); + } + state.historyStack.push(route); + state.historyIndex = state.historyStack.length - 1; + setCurrentRoute(route); // 会触发保存 + } + + // 后退 + function goBack() { + if (state.historyIndex > 0) { + state.historyIndex--; + state.currentRoute = state.historyStack[state.historyIndex]; + save(); + return state.currentRoute; + } + return null; + } + + // 前进 + function goForward() { + if (state.historyIndex < state.historyStack.length - 1) { + state.historyIndex++; + state.currentRoute = state.historyStack[state.historyIndex]; + save(); + return state.currentRoute; + } + return null; + } + + // 获取当前状态 + function getState() { + return { ...state }; + } + + // 标记模块为已访问(外部调用) + function markModuleVisited(moduleId) { + if (!state.visitedModules.includes(moduleId)) { + state.visitedModules.push(moduleId); + save(); + } + } + + // 清除状态(用于测试) + function clear() { + localStorage.removeItem(STORAGE_KEY); + reset(); + } + + // 初始化:加载状态 + load(); + + return { + load, + save, + reset, + setCurrentRoute, + pushHistory, + goBack, + goForward, + getState, + markModuleVisited, + clear + }; +})(); + +// 导出(如果是模块环境) +if (typeof module !== 'undefined' && module.exports) { + module.exports = ChannelState; +} diff --git a/modules/m-channel/backup-混乱版/channel-style.css b/modules/m-channel/backup-混乱版/channel-style.css new file mode 100644 index 00000000..f2a8cd20 --- /dev/null +++ b/modules/m-channel/backup-混乱版/channel-style.css @@ -0,0 +1,132 @@ +/** + * channel-style.css + * 频道基础样式 + 过渡动画变量 + */ + +/* 引入过渡动画 */ +@import url('channel-transition.css'); + +:root { + --primary-color: #4a90e2; + --secondary-color: #f5f5f5; + --text-color: #333; + --border-radius: 8px; + --transition-duration: 0.3s; + --transition-timing: ease; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + margin: 0; + padding: 20px; + background: #f0f2f5; + color: var(--text-color); +} + +/* 频道头部 */ +.channel-header { + background: white; + padding: 15px 20px; + border-radius: var(--border-radius); + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + margin-bottom: 20px; +} + +.channel-header h1 { + margin: 0; + font-size: 1.8rem; + color: var(--primary-color); +} + +/* 导航菜单 */ +.channel-nav { + background: white; + padding: 10px 20px; + border-radius: var(--border-radius); + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.channel-nav ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + gap: 20px; +} + +.channel-nav a { + text-decoration: none; + color: var(--text-color); + padding: 8px 16px; + border-radius: 20px; + transition: background 0.2s; +} + +.channel-nav a:hover { + background: var(--secondary-color); +} + +.channel-nav a.active { + background: var(--primary-color); + color: white; +} + +/* 内容区域 */ +.channel-content { + background: white; + padding: 20px; + border-radius: var(--border-radius); + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + min-height: 300px; +} + +/* 已访问模块标记 */ +.module-link.visited { + position: relative; +} + +.module-link.visited::after { + content: "✓"; + position: absolute; + top: -5px; + right: -5px; + background: var(--primary-color); + color: white; + width: 18px; + height: 18px; + border-radius: 50%; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; +} + +/* 动画模式切换按钮 */ +.animation-toggle { + margin-left: auto; + display: flex; + gap: 10px; +} + +.animation-toggle button { + padding: 5px 15px; + border: 1px solid #ddd; + background: white; + border-radius: 20px; + cursor: pointer; + transition: all 0.2s; +} + +.animation-toggle button.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +/* 调试面板样式(后续使用) */ +.debug-panel { + margin-top: 30px; + border-top: 2px dashed #ccc; + padding-top: 20px; +} diff --git a/modules/m-channel/backup-混乱版/channel-transition.css b/modules/m-channel/backup-混乱版/channel-transition.css new file mode 100644 index 00000000..b13bd025 --- /dev/null +++ b/modules/m-channel/backup-混乱版/channel-transition.css @@ -0,0 +1,83 @@ +/** + * channel-transition.css + * 路由过渡动画 - 让切换像呼吸一样自然 + * 提供两种模式:淡入淡出(fade) / 滑入滑出(slide) + */ + +/* 基础容器样式 */ +.route-container { + position: relative; + width: 100%; + min-height: 200px; +} + +/* 所有路由页面默认绝对定位,便于重叠动画 */ +.route-page { + position: absolute; + top: 0; + left: 0; + width: 100%; + transition: opacity 0.3s ease, transform 0.3s ease; +} + +/* 淡入淡出模式 */ +.fade-mode .route-page { + opacity: 0; + transform: scale(0.98); + pointer-events: none; +} + +.fade-mode .route-page.active { + opacity: 1; + transform: scale(1); + pointer-events: auto; + position: relative; /* 激活页变为相对定位,占据文档流高度 */ +} + +/* 滑入滑出模式 */ +.slide-mode .route-page { + opacity: 1; + transform: translateX(0); + pointer-events: none; +} + +.slide-mode .route-page.active { + position: relative; + pointer-events: auto; +} + +/* 进入动画:从右侧滑入 */ +.slide-mode .route-page.slide-enter { + transform: translateX(100%); +} + +.slide-mode .route-page.active.slide-enter { + transform: translateX(0); +} + +/* 离开动画:向左侧滑出(用于后退时的反向) */ +.slide-mode .route-page.slide-exit { + transform: translateX(-100%); +} + +/* 前进/后退方向控制 */ +.slide-mode.forward .route-page.slide-enter { + transform: translateX(100%); +} + +.slide-mode.forward .route-page.active.slide-enter { + transform: translateX(0); +} + +.slide-mode.backward .route-page.slide-enter { + transform: translateX(-100%); +} + +.slide-mode.backward .route-page.active.slide-enter { + transform: translateX(0); +} + +/* 防止快速点击时动画重叠 */ +.route-page { + will-change: transform, opacity; +} diff --git a/modules/m-channel/backup-混乱版/event-bus.js b/modules/m-channel/backup-混乱版/event-bus.js new file mode 100644 index 00000000..ab14007c --- /dev/null +++ b/modules/m-channel/backup-混乱版/event-bus.js @@ -0,0 +1,111 @@ +/** + * event-bus.js + * 事件总线 - 模块间的群聊频道 + * 发布/订阅模式,支持命名空间和调试日志 + */ + +const EventBus = (function() { + // 存储订阅者:{ eventName: [handler1, handler2, ...] } + const listeners = {}; + // 调试模式开关 + let debugMode = true; + + // 订阅事件 + function on(eventName, handler) { + if (typeof handler !== 'function') { + console.error('[bus] 订阅必须提供函数'); + return; + } + + if (!listeners[eventName]) { + listeners[eventName] = []; + } + listeners[eventName].push(handler); + + if (debugMode) { + console.log(`[bus] 订阅事件: ${eventName},当前订阅数: ${listeners[eventName].length}`); + } + + // 返回取消订阅函数 + return function off() { + off(eventName, handler); + }; + } + + // 取消订阅 + function off(eventName, handler) { + if (!listeners[eventName]) return; + + if (handler) { + // 移除特定handler + const index = listeners[eventName].indexOf(handler); + if (index !== -1) { + listeners[eventName].splice(index, 1); + if (debugMode) { + console.log(`[bus] 取消订阅: ${eventName},剩余: ${listeners[eventName].length}`); + } + } + } else { + // 移除该事件所有订阅 + delete listeners[eventName]; + if (debugMode) { + console.log(`[bus] 移除所有订阅: ${eventName}`); + } + } + } + + // 触发事件 + function emit(eventName, data) { + if (!listeners[eventName]) { + if (debugMode) { + console.log(`[bus] 触发事件 ${eventName} 但无订阅者`); + } + return; + } + + if (debugMode) { + console.log(`[bus] 触发事件: ${eventName},数据:`, data); + } + + // 复制一份以防在遍历过程中修改 + const handlers = listeners[eventName].slice(); + handlers.forEach(handler => { + try { + handler(data, eventName); + } catch (e) { + console.error(`[bus] 事件 ${eventName} 处理出错:`, e); + } + }); + } + + // 清空所有订阅 + function clear() { + for (let key in listeners) { + delete listeners[key]; + } + if (debugMode) { + console.log('[bus] 清空所有订阅'); + } + } + + // 开启/关闭调试 + function setDebug(enable) { + debugMode = enable; + } + + return { + on, + off, + emit, + clear, + setDebug + }; +})(); + +// 挂载到全局 +window.EventBus = EventBus; + +// 如果是模块环境 +if (typeof module !== 'undefined' && module.exports) { + module.exports = EventBus; +} diff --git a/modules/m-channel/backup-混乱版/index.html b/modules/m-channel/backup-混乱版/index.html new file mode 100644 index 00000000..e86e9904 --- /dev/null +++ b/modules/m-channel/backup-混乱版/index.html @@ -0,0 +1,33 @@ + + + + + + 光湖频道 · 动态渲染引擎 + + + + +
+ +
+ +
+
+ + + + + + + + + + + diff --git a/modules/m-channel/backup-混乱版/module-lifecycle.js b/modules/m-channel/backup-混乱版/module-lifecycle.js new file mode 100644 index 00000000..37daed60 --- /dev/null +++ b/modules/m-channel/backup-混乱版/module-lifecycle.js @@ -0,0 +1,90 @@ +/** + * module-lifecycle.js + * 模块生命周期管理 - 让模块知道自己何时加载/卸载/收到消息 + * 配合事件总线使用 + */ + +const ModuleLifecycle = (function() { + // 存储每个模块的钩子函数 + const hooks = {}; + + // 注册模块的生命周期钩子 + function register(moduleId, lifecycles) { + if (!moduleId) return; + + hooks[moduleId] = { + onLoad: lifecycles.onLoad || null, + onUnload: lifecycles.onUnload || null, + onMessage: lifecycles.onMessage || null + }; + + console.log(`[lifecycle] 注册模块: ${moduleId}`, lifecycles); + + // 如果已经加载(比如页面初始化时),自动触发onLoad? + // 这里留给loader去调用 + } + + // 触发模块加载 + function triggerLoad(moduleId, params) { + const moduleHooks = hooks[moduleId]; + if (moduleHooks && moduleHooks.onLoad) { + try { + moduleHooks.onLoad(params); + console.log(`[lifecycle] onLoad 模块: ${moduleId}`); + } catch (e) { + console.error(`[lifecycle] onLoad 模块 ${moduleId} 出错:`, e); + } + } + } + + // 触发模块卸载 + function triggerUnload(moduleId) { + const moduleHooks = hooks[moduleId]; + if (moduleHooks && moduleHooks.onUnload) { + try { + moduleHooks.onUnload(); + console.log(`[lifecycle] onUnload 模块: ${moduleId}`); + } catch (e) { + console.error(`[lifecycle] onUnload 模块 ${moduleId} 出错:`, e); + } + } + + // 卸载时自动取消该模块的所有事件订阅 + // 这里简单使用事件总线的off,但需要知道该模块订阅了哪些事件 + // 我们约定模块在订阅时使用带命名空间的事件名,比如 moduleA:click + // 或者在onUnload里手动取消。为了简化,我们不清除订阅,但可以在onUnload里做。 + // 更完善的做法是记录每个模块的订阅列表,但这里先不实现。 + } + + // 触发模块收到消息(由事件总线转发时调用) + function triggerMessage(moduleId, message, data) { + const moduleHooks = hooks[moduleId]; + if (moduleHooks && moduleHooks.onMessage) { + try { + moduleHooks.onMessage(message, data); + console.log(`[lifecycle] onMessage 模块: ${moduleId} 消息: ${message}`); + } catch (e) { + console.error(`[lifecycle] onMessage 模块 ${moduleId} 出错:`, e); + } + } + } + + // 获取模块的钩子(用于调试) + function getHooks(moduleId) { + return hooks[moduleId] || null; + } + + return { + register, + triggerLoad, + triggerUnload, + triggerMessage, + getHooks + }; +})(); + +window.ModuleLifecycle = ModuleLifecycle; + +if (typeof module !== 'undefined' && module.exports) { + module.exports = ModuleLifecycle; +} diff --git a/modules/m-channel/backup-混乱版/module-loader.js b/modules/m-channel/backup-混乱版/module-loader.js new file mode 100644 index 00000000..27474424 --- /dev/null +++ b/modules/m-channel/backup-混乱版/module-loader.js @@ -0,0 +1,116 @@ +/** + * module-loader.js + * 模块加载器 - 负责动态加载模块HTML/JS,并管理生命周期 + * 集成事件总线和生命周期钩子 + */ + +const ModuleLoader = (function() { + // 已加载的模块缓存 + const loadedModules = {}; + + // 加载模块 + async function loadModule(moduleId, container, params = {}) { + if (loadedModules[moduleId]) { + console.log(`[loader] 模块 ${moduleId} 已加载,直接显示`); + showModule(moduleId, container, params); + return; + } + + try { + console.log(`[loader] 正在加载模块: ${moduleId}`); + + // 获取模块URL(这里使用mock-modules下的html文件) + const url = `mock-modules/mock-${moduleId}.html`; + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const html = await response.text(); + + // 解析HTML,提取body内容作为模块内容 + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + let moduleContent = doc.body.innerHTML; + + // 提取script标签并执行 + const scripts = doc.querySelectorAll('script'); + scripts.forEach(script => { + const newScript = document.createElement('script'); + if (script.src) { + newScript.src = script.src; + } else { + newScript.textContent = script.textContent; + } + document.body.appendChild(newScript); + // 注意:动态添加的脚本会立即执行 + }); + + // 存储模块内容 + loadedModules[moduleId] = { + content: moduleContent, + scripts: scripts.length + }; + + // 显示模块 + showModule(moduleId, container, params); + + // 触发生命周期 onLoad + ModuleLifecycle.triggerLoad(moduleId, params); + + // 标记已访问(状态管理) + if (window.ChannelState) { + ChannelState.markModuleVisited(moduleId); + } + + console.log(`[loader] 模块 ${moduleId} 加载完成`); + } catch (error) { + console.error(`[loader] 加载模块 ${moduleId} 失败:`, error); + container.innerHTML = `
加载失败:${error.message}
`; + } + } + + // 显示已加载的模块 + function showModule(moduleId, container, params) { + const mod = loadedModules[moduleId]; + if (!mod) return; + + container.innerHTML = mod.content; + + // 如果有参数,可以通过自定义事件传递 + if (params) { + const event = new CustomEvent('module:params', { detail: params }); + container.dispatchEvent(event); + } + + // 通知模块已显示(通过生命周期?) + // 可以用triggerMessage + ModuleLifecycle.triggerMessage(moduleId, 'show', params); + } + + // 卸载模块 + function unloadModule(moduleId) { + if (loadedModules[moduleId]) { + // 触发生命周期 onUnload + ModuleLifecycle.triggerUnload(moduleId); + + // 清理缓存(可选) + delete loadedModules[moduleId]; + console.log(`[loader] 模块 ${moduleId} 已卸载`); + } + } + + // 预加载模块 + function preloadModule(moduleId) { + // 简单fetch但不显示 + fetch(`mock-modules/mock-${moduleId}.html`).catch(() => {}); + } + + return { + loadModule, + unloadModule, + preloadModule + }; +})(); + +window.ModuleLoader = ModuleLoader; diff --git a/modules/m-channel/backup-混乱版/module-registry.js b/modules/m-channel/backup-混乱版/module-registry.js new file mode 100644 index 00000000..a0a87ff3 --- /dev/null +++ b/modules/m-channel/backup-混乱版/module-registry.js @@ -0,0 +1,7 @@ +const moduleRegistry = { + 'mock-a': 'mock-modules/mock-a.html', + 'mock-b': 'mock-modules/mock-b.html', + 'mock-c': 'mock-modules/mock-c.html', + 'mock-d': 'mock-modules/mock-d.html' +}; +window.moduleRegistry = moduleRegistry; diff --git a/modules/m-channel/channel-analytics.js b/modules/m-channel/channel-analytics.js new file mode 100644 index 00000000..f1b43148 --- /dev/null +++ b/modules/m-channel/channel-analytics.js @@ -0,0 +1,163 @@ + +/** + * channel-analytics.js + * 频道数据采集核心·环节8 + * 记录模块访问次数、停留时间、页面加载速度 + */ + +const ChannelAnalytics = (function() { + const STORAGE_KEY = 'channel-analytics-data'; + + // 数据结构初始化 + function getDefaultData() { + return { + modules: {}, // {moduleId: {visits: 0, totalDuration: 0, loadTimes: [], dailyVisits: {} }} + globalDaily: {}, // {'YYYY-MM-DD': totalVisits} + lastUpdated: null + }; + } + + // localStorage 读写 + function loadData() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) return JSON.parse(raw); + } catch(e) { + console.log('⚠️ 读取分析数据失败,重新初始化'); + } + return getDefaultData(); + } + + function saveData(data) { + data.lastUpdated = new Date().toISOString(); + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + } + + // 日期工具 + function today() { + return new Date().toISOString().split('T')[0]; + } + + // 确保模块数据结构存在 + function ensureModule(data, moduleId) { + if (!data.modules[moduleId]) { + data.modules[moduleId] = { + visits: 0, + totalDuration: 0, + loadTimes: [], + dailyVisits: {} + }; + } + return data.modules[moduleId]; + } + + // 当前会话状态 + let currentModule = null; + let enterTime = null; + let loadStartTime = null; + + // 公开方法 + return { + // 记录模块访问 + recordVisit: function(moduleId) { + if (!moduleId) return; + const data = loadData(); + const mod = ensureModule(data, moduleId); + + // 访问计数 +1 + mod.visits++; + + // 每日访问计数 + const d = today(); + mod.dailyVisits[d] = (mod.dailyVisits[d] || 0) + 1; + + // 全局每日访问 + data.globalDaily[d] = (data.globalDaily[d] || 0) + 1; + + saveData(data); + console.log('📊 已记录:模块 ' + moduleId + ' 被访问,累计 ' + mod.visits + ' 次'); + + // 记录进入时间 + this.startSession(moduleId); + }, + + // 开始计时 + startSession: function(moduleId) { + // 先结束上一个模块的计时 + if (currentModule && enterTime) { + this.endSession(); + } + currentModule = moduleId; + enterTime = performance.now(); + loadStartTime = performance.now(); + }, + + // 结束计时(切换模块或离开时调用) + endSession: function() { + if (!currentModule || !enterTime) return; + const duration = Math.round((performance.now() - enterTime) / 1000); // 秒 + const data = loadData(); + const mod = ensureModule(data, currentModule); + mod.totalDuration += duration; + saveData(data); + console.log('⏱️ 停留时间:模块 ' + currentModule + ' 停留约 ' + duration + ' 秒'); + currentModule = null; + enterTime = null; + }, + + // 记录加载性能 + recordLoadTime: function(moduleId, loadTimeMs) { + if (!moduleId) return; + const data = loadData(); + const mod = ensureModule(data, moduleId); + mod.loadTimes.push(loadTimeMs); + // 只保留最近50次 + if (mod.loadTimes.length > 50) { + mod.loadTimes = mod.loadTimes.slice(-50); + } + saveData(data); + console.log('⚡ 加载耗时:模块 ' + moduleId + ' 加载 ' + Math.round(loadTimeMs) + ' 毫秒'); + }, + + // 标记加载开始 + markLoadStart: function() { + loadStartTime = performance.now(); + }, + + // 标记加载完成并记录 + markLoadEnd: function(moduleId) { + if (loadStartTime && moduleId) { + const loadTime = performance.now() - loadStartTime; + this.recordLoadTime(moduleId, loadTime); + loadStartTime = null; + } + }, + + // 获取所有数据(面板用) + getAllData: function() { + return loadData(); + }, + + // 获取最近7天趋势 + getWeeklyTrend: function() { + const data = loadData(); + const trend = []; + for (let i = 6; i >= 0; i--) { + const d = new Date(); + d.setDate(d.getDate() - i); + const dateStr = d.toISOString().split('T')[0]; + trend.push({ + date: dateStr, + visits: data.globalDaily[dateStr] || 0 + }); + } + return trend; + }, + + // 清除所有数据(调试用) + clearAll: function() { + localStorage.removeItem(STORAGE_KEY); + console.log('🗑️ 所有分析数据已清除'); + } + }; +})(); diff --git a/modules/m-channel/channel-complete.html b/modules/m-channel/channel-complete.html new file mode 100644 index 00000000..91629573 --- /dev/null +++ b/modules/m-channel/channel-complete.html @@ -0,0 +1,267 @@ + + + + + + 光湖频道 · 完全版 + + + + + + +
+ +
+ +
+
+ + +
+
+

频道设置

+ +
+
+
+

布局模式

+
+ + + +
+
+
+

主题色

+
+ + + + + +
+
+
+

统计数据

+ +
+
+

其他

+ +
+
+
+ + +
+
+
+

使用统计

+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/m-channel/channel-dashboard.css b/modules/m-channel/channel-dashboard.css new file mode 100644 index 00000000..1269c86f --- /dev/null +++ b/modules/m-channel/channel-dashboard.css @@ -0,0 +1,199 @@ +/** + * channel-dashboard.css + * 频道数据面板样式·深色主题 + */ + +.dashboard-container { + padding: 20px; + max-width: 1200px; + margin: 0 auto; + color: #e0e0e0; +} + +.dashboard-header { + text-align: center; + margin-bottom: 30px; +} + +.dashboard-header h2 { + font-size: 24px; + color: #4fc3f7; + margin-bottom: 8px; +} + +.dashboard-header p { + font-size: 14px; + color: #888; +} + +/* 图表网格 */ +.charts-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 30px; +} + +.chart-card { + background: #1a1a2e; + border-radius: 12px; + padding: 20px; + border: 1px solid #2a2a4a; +} + +.chart-card h3 { + font-size: 16px; + color: #4fc3f7; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #2a2a4a; +} + +.chart-card canvas { + width: 100% !important; + height: 200px !important; + display: block; +} + +/* 柱状图 */ +.bar-chart { + display: flex; + align-items: flex-end; + justify-content: space-around; + height: 200px; + padding: 10px 0; + border-bottom: 2px solid #333; +} + +.bar-item { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + max-width: 80px; +} + +.bar-fill { + width: 40px; + border-radius: 4px 4px 0 0; + transition: height 0.5s ease; + min-height: 4px; +} + +.bar-label { + margin-top: 8px; + font-size: 11px; + color: #aaa; + text-align: center; + word-break: break-all; +} + +.bar-value { + font-size: 12px; + color: #fff; + margin-bottom: 4px; +} + +/* 饼图 */ +.pie-container { + display: flex; + align-items: center; + gap: 20px; +} + +.pie-canvas-wrap { + flex-shrink: 0; +} + +.pie-legend { + list-style: none; + padding: 0; + margin: 0; +} + +.pie-legend li { + display: flex; + align-items: center; + margin-bottom: 6px; + font-size: 13px; +} + +.pie-legend .dot { + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; + flex-shrink: 0; +} + +/* 折线图 */ +.line-chart-wrap { + position: relative; + height: 200px; +} + +/* 性能表格 */ +.perf-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.perf-table th { + text-align: left; + padding: 8px; + background: #2a2a4a; + color: #4fc3f7; + font-weight: normal; +} + +.perf-table td { + padding: 8px; + border-bottom: 1px solid #2a2a4a; +} + +.perf-table tr:last-child td { + border-bottom: none; +} + +.perf-table .slow { + color: #ff5252; + font-weight: bold; +} + +/* 按钮 */ +.dashboard-actions { + margin-top: 20px; + text-align: right; +} + +.btn-clear { + background: #ff5252; + color: #fff; + border: none; + padding: 10px 24px; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: background 0.3s; +} + +.btn-clear:hover { + background: #ff1744; +} + +/* 响应式 */ +@media (max-width: 768px) { + .charts-grid { + grid-template-columns: 1fr; + } + .pie-container { + flex-direction: column; + } +} + +/* 饼图 canvas 保持正方形 */ +.pie-canvas-wrap canvas { + width: 160px !important; + height: 160px !important; +} diff --git a/modules/m-channel/channel-dashboard.js b/modules/m-channel/channel-dashboard.js new file mode 100644 index 00000000..13f3555b --- /dev/null +++ b/modules/m-channel/channel-dashboard.js @@ -0,0 +1,201 @@ + +/** + * channel-dashboard.js + * 频道数据面板逻辑·图表渲染 + */ + +const ChannelDashboard = (function() { + // 配色方案 + const COLORS = ['#4fc3f7', '#ffb74d', '#ff8a80', '#aed581', '#ba68c8', '#4dd0e1', '#ffd54f']; + + // 柱状图渲染 + function renderBarChart() { + const container = document.getElementById('visitBarChart'); + if (!container) return; + const data = ChannelAnalytics.getAllData(); + const modules = data.modules; + const keys = Object.keys(modules); + if (keys.length === 0) { + container.innerHTML = '

暂无数据,多点几个模块再来看

'; + return; + } + const maxVisits = Math.max.apply(null, keys.map(function(k) { return modules[k].visits; })) || 1; + let html = ''; + keys.forEach(function(id, i) { + const mod = modules[id]; + const height = Math.max(4, (mod.visits / maxVisits) * 180); + const color = COLORS[i % COLORS.length]; + html += '
' + + '' + mod.visits + '' + + '
' + + '' + id.replace('m-', '') + '' + + '
'; + }); + container.innerHTML = html; + } + + // 饼图渲染 + function renderPieChart() { + const canvas = document.getElementById('pieCanvas'); + const legendEl = document.getElementById('pieLegend'); + if (!canvas || !legendEl) return; + const ctx = canvas.getContext('2d'); + const data = ChannelAnalytics.getAllData(); + const modules = data.modules; + const keys = Object.keys(modules); + const total = keys.reduce(function(sum, k) { + return sum + (modules[k].totalDuration || 0); + }, 0); + + // 清空画布 + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (total === 0 || keys.length === 0) { + ctx.fillStyle = '#333'; + ctx.beginPath(); + ctx.arc(80, 80, 70, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#666'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('暂无数据', 80, 84); + legendEl.innerHTML = ''; + return; + } + + let startAngle = -Math.PI / 2; + let legendHtml = ''; + keys.forEach(function(id, i) { + const mod = modules[id]; + const pct = mod.totalDuration / total; + const sweep = pct * Math.PI * 2; + const color = COLORS[i % COLORS.length]; + + ctx.beginPath(); + ctx.moveTo(80, 80); + ctx.arc(80, 80, 70, startAngle, startAngle + sweep); + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); + + startAngle += sweep; + + const minutes = Math.round(mod.totalDuration / 60); + const pctStr = Math.round(pct * 100); + legendHtml += '
  • ' + + id.replace('m-', '') + ' ' + pctStr + '% (' + minutes + '分钟)
  • '; + }); + legendEl.innerHTML = legendHtml; + } + + // 折线图渲染 + function renderLineChart() { + const canvas = document.getElementById('lineCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const trend = ChannelAnalytics.getWeeklyTrend(); + const w = canvas.width; + const h = canvas.height; + + ctx.clearRect(0, 0, w, h); + + const maxVal = Math.max.apply(null, trend.map(function(t) { return t.visits; })) || 1; + const padLeft = 40; + const padBottom = 30; + const padTop = 10; + const chartW = w - padLeft - 20; + const chartH = h - padBottom - padTop; + const stepX = chartW / (trend.length - 1 || 1); + + // 网格线 + ctx.strokeStyle = '#2a2a4a'; + ctx.lineWidth = 1; + for (let g = 0; g <= 4; g++) { + let gy = padTop + (chartH / 4) * g; + ctx.beginPath(); + ctx.moveTo(padLeft, gy); + ctx.lineTo(w - 20, gy); + ctx.stroke(); + } + + // 折线 + ctx.strokeStyle = '#4fc3f7'; + ctx.lineWidth = 2; + ctx.beginPath(); + trend.forEach(function(t, i) { + let x = padLeft + stepX * i; + let y = padTop + chartH - (t.visits / maxVal) * chartH; + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + }); + ctx.stroke(); + + // 数据点 + trend.forEach(function(t, i) { + let x = padLeft + stepX * i; + let y = padTop + chartH - (t.visits / maxVal) * chartH; + ctx.beginPath(); + ctx.arc(x, y, 4, 0, Math.PI * 2); + ctx.fillStyle = '#4fc3f7'; + ctx.fill(); + }); + + // 标签 + ctx.fillStyle = '#aaa'; + ctx.font = '11px sans-serif'; + ctx.textAlign = 'center'; + trend.forEach(function(t, i) { + let x = padLeft + stepX * i; + let y = padTop + chartH + 15; + ctx.fillText(t.date.slice(5), x, y); + }); + } + + // 性能表格渲染 + function renderPerfTable() { + const tbody = document.getElementById('perfTableBody'); + if (!tbody) return; + const data = ChannelAnalytics.getAllData(); + const modules = data.modules; + const keys = Object.keys(modules); + if (keys.length === 0) { + tbody.innerHTML = '暂无数据'; + return; + } + + let html = ''; + keys.forEach(function(id) { + const mod = modules[id]; + const avgLoad = mod.loadTimes.length ? + Math.round(mod.loadTimes.reduce((a, b) => a + b, 0) / mod.loadTimes.length) : 0; + const minutes = Math.round(mod.totalDuration / 60); + const statusClass = avgLoad > 300 ? 'slow' : ''; + const statusText = avgLoad > 300 ? '⚠️ 慢' : '✅ 正常'; + + html += '' + + '' + id.replace('m-', '') + '' + + '' + mod.visits + '' + + '' + minutes + '分钟' + + '' + avgLoad + 'ms' + + '' + statusText + '' + + ''; + }); + tbody.innerHTML = html; + } + + // 公开方法 + return { + render: function() { + renderBarChart(); + renderPieChart(); + renderLineChart(); + renderPerfTable(); + }, + clearData: function() { + if (confirm('确定清除所有统计数据吗?')) { + ChannelAnalytics.clearAll(); + this.render(); + console.log('🗑️ 数据已清除,图表已重置'); + } + } + }; +})(); diff --git a/modules/m-channel/channel-enhancements.js b/modules/m-channel/channel-enhancements.js new file mode 100644 index 00000000..36896616 --- /dev/null +++ b/modules/m-channel/channel-enhancements.js @@ -0,0 +1,338 @@ +// channel-enhancements.js - 频道搜索、面包屑、快捷键增强功能(终极修复版) +(function() { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + function init() { + addStyles(); + createSearchBox(); + createBreadcrumb(); + createShortcutHint(); + initShortcuts(); + bindCardClicks(); // ★ 新增:为卡片绑定点击切换模块 + window.addEventListener('hashchange', updateBreadcrumb); + updateBreadcrumb(); + } + + function addStyles() { + const style = document.createElement('style'); + style.textContent = ` + .channel-search-container { + padding: 16px 20px; + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; + display: flex; + align-items: center; + gap: 10px; + } + .channel-search-input { + flex: 1; + padding: 10px 16px; + border: 1px solid #ced4da; + border-radius: 24px; + font-size: 14px; + outline: none; + transition: border-color 0.2s; + } + .channel-search-input:focus { + border-color: #4d6bfe; + box-shadow: 0 0 0 3px rgba(77,107,254,0.1); + } + .channel-search-clear { + background: none; + border: none; + font-size: 18px; + cursor: pointer; + color: #6c757d; + padding: 0 8px; + display: none; + } + .channel-search-clear:hover { + color: #212529; + } + .channel-search-input:not(:placeholder-shown) + .channel-search-clear { + display: inline-block; + } + .highlight { + background-color: #ffeb3b; + padding: 2px 0; + border-radius: 2px; + } + .no-results { + text-align: center; + padding: 40px; + color: #6c757d; + font-style: italic; + } + .channel-breadcrumb { + padding: 12px 20px; + background: white; + border-bottom: 1px solid #e9ecef; + font-size: 14px; + } + .channel-breadcrumb a { + color: #4d6bfe; + text-decoration: none; + } + .channel-breadcrumb a:hover { + text-decoration: underline; + } + .channel-breadcrumb span { + color: #6c757d; + } + .channel-breadcrumb .current { + color: #212529; + font-weight: 500; + } + .shortcut-hint { + position: fixed; + bottom: 20px; + right: 20px; + background: rgba(0,0,0,0.7); + color: white; + padding: 8px 16px; + border-radius: 30px; + font-size: 12px; + backdrop-filter: blur(4px); + z-index: 1000; + } + .shortcut-hint kbd { + background: rgba(255,255,255,0.2); + padding: 2px 6px; + border-radius: 4px; + margin: 0 2px; + } + .module-card.selected { + outline: 2px solid #4d6bfe; + outline-offset: 2px; + transform: scale(1.02); + transition: all 0.2s; + } + `; + document.head.appendChild(style); + } + + function createSearchBox() { + const container = document.querySelector('.module-grid') || document.querySelector('#module-list') || document.querySelector('.channel-content'); + if (!container) return; + + const searchContainer = document.createElement('div'); + searchContainer.className = 'channel-search-container'; + searchContainer.innerHTML = ` + + + `; + container.parentNode.insertBefore(searchContainer, container); + + const searchInput = searchContainer.querySelector('.channel-search-input'); + const clearBtn = searchContainer.querySelector('.channel-search-clear'); + + searchInput.addEventListener('input', function() { + filterModules(this.value); + }); + + clearBtn.addEventListener('click', function() { + searchInput.value = ''; + filterModules(''); + searchInput.focus(); + }); + + window.__channelSearchInput = searchInput; + } + + function filterModules(keyword) { + const cards = document.querySelectorAll('.module-card'); + const container = document.querySelector('.module-grid') || document.querySelector('#module-list'); + let hasResults = false; + + // 移除所有高亮 + removeAllHighlights(); + + cards.forEach(card => { + const text = card.innerText || card.textContent; + if (keyword === '') { + card.style.display = ''; + hasResults = true; + } else { + const lowerText = text.toLowerCase(); + const lowerKeyword = keyword.toLowerCase(); + if (lowerText.includes(lowerKeyword)) { + card.style.display = ''; + highlightText(card, keyword); + hasResults = true; + } else { + card.style.display = 'none'; + } + } + }); + + let noResultsEl = document.querySelector('.no-results'); + if (!hasResults && keyword !== '') { + if (!noResultsEl) { + noResultsEl = document.createElement('div'); + noResultsEl.className = 'no-results'; + noResultsEl.textContent = '没有找到匹配的模块'; + container.parentNode.insertBefore(noResultsEl, container.nextSibling); + } + } else { + if (noResultsEl) noResultsEl.remove(); + } + } + + function removeAllHighlights() { + document.querySelectorAll('.highlight').forEach(span => { + const parent = span.parentNode; + parent.replaceChild(document.createTextNode(span.textContent), span); + parent.normalize(); + }); + } + + function highlightText(card, keyword) { + const regex = new RegExp(`(${keyword})`, 'gi'); + const walk = document.createTreeWalker(card, NodeFilter.SHOW_TEXT, { + acceptNode: function(node) { + if (node.parentNode.classList && node.parentNode.classList.contains('highlight')) { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + } + }, false); + + const textNodes = []; + while (walk.nextNode()) textNodes.push(walk.currentNode); + + textNodes.forEach(node => { + const text = node.nodeValue; + if (regex.test(text)) { + const span = document.createElement('span'); + span.className = 'highlight'; + span.innerHTML = text.replace(regex, '$1'); + node.parentNode.replaceChild(span, node); + } + }); + } + + function createBreadcrumb() { + const searchContainer = document.querySelector('.channel-search-container'); + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'channel-breadcrumb'; + breadcrumb.id = 'channelBreadcrumb'; + if (searchContainer) { + searchContainer.parentNode.insertBefore(breadcrumb, searchContainer.nextSibling); + } else { + const container = document.querySelector('.module-grid') || document.querySelector('#module-list'); + if (container) { + container.parentNode.insertBefore(breadcrumb, container); + } + } + } + + function updateBreadcrumb() { + const breadcrumb = document.getElementById('channelBreadcrumb'); + if (!breadcrumb) return; + const hash = window.location.hash.slice(1) || ''; + let moduleName = '频道'; + if (hash.startsWith('module-')) { + const moduleId = hash.replace('module-', ''); + const moduleNames = { + 'M06': '工单管理', + 'M08': '数据看板', + 'M11': '组件库', + 'debug': '调试面板' + }; + moduleName = moduleNames[moduleId] || moduleId; + } + breadcrumb.innerHTML = ` + 首页 > + 频道 > + ${moduleName} + `; + } + + function createShortcutHint() { + const hint = document.createElement('div'); + hint.className = 'shortcut-hint'; + hint.innerHTML = ` + ⌘K 搜索 · + 选择 · + Enter 打开 · + Esc 关闭 + `; + document.body.appendChild(hint); + } + + function initShortcuts() { + document.addEventListener('keydown', function(e) { + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + const searchInput = window.__channelSearchInput; + if (searchInput) searchInput.focus(); + } + if (e.key === 'Escape') { + const searchInput = window.__channelSearchInput; + if (searchInput && document.activeElement === searchInput) { + searchInput.value = ''; + filterModules(''); + searchInput.blur(); + } else if (searchInput && searchInput.value) { + searchInput.value = ''; + filterModules(''); + } + } + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + const cards = Array.from(document.querySelectorAll('.module-card:not([style*="display: none"])')); + if (cards.length === 0) return; + e.preventDefault(); + let selectedIndex = cards.findIndex(card => card.classList.contains('selected')); + if (selectedIndex === -1) { + selectedIndex = e.key === 'ArrowDown' ? 0 : cards.length - 1; + } else { + cards[selectedIndex].classList.remove('selected'); + if (e.key === 'ArrowDown') { + selectedIndex = (selectedIndex + 1) % cards.length; + } else { + selectedIndex = (selectedIndex - 1 + cards.length) % cards.length; + } + } + cards[selectedIndex].classList.add('selected'); + cards[selectedIndex].scrollIntoView({ block: 'nearest' }); + } + if (e.key === 'Enter') { + const selected = document.querySelector('.module-card.selected'); + if (selected) { + selected.click(); // 触发我们绑定的点击事件 + } + } + }); + } + + // ★★★ 核心修复:为所有卡片绑定点击切换模块 ★★★ + function bindCardClicks() { + const grid = document.querySelector('.module-grid') || document.querySelector('#module-list') || document.body; + grid.addEventListener('click', function(e) { + const card = e.target.closest('.module-card'); + if (!card) return; + + // 阻止可能冲突的其他事件(可选) + e.preventDefault(); + + // 根据卡片文字判断是哪个模块 + const text = card.innerText || card.textContent; + let moduleId = null; + if (text.includes('工单管理')) moduleId = 'M06'; + else if (text.includes('数据统计')) moduleId = 'M08'; + else if (text.includes('组件库')) moduleId = 'M11'; + else if (text.includes('调试面板')) moduleId = 'debug'; + // 如果识别不到,尝试从其他属性获取(比如 data-module-id) + else if (card.dataset.moduleId) moduleId = card.dataset.moduleId; + + if (moduleId) { + // 改变 hash,触发路由更新 + window.location.hash = `module-${moduleId}`; + } + }); + } +})(); diff --git a/modules/m-channel/channel-favorites.js b/modules/m-channel/channel-favorites.js new file mode 100644 index 00000000..4208f653 --- /dev/null +++ b/modules/m-channel/channel-favorites.js @@ -0,0 +1,271 @@ +// 频道收藏与拖拽排序管理 +window.ChannelFavorites = { + // 当前正在拖拽的元素 + draggingElement: null, + + // 初始化 + init: function() { + console.log('[favorites] 初始化'); + this.bindEvents(); + this.renderFavorites(); + }, + + // 绑定事件 + bindEvents: function() { + // 使用事件委托监听星星点击 + document.addEventListener('click', (e) => { + const star = e.target.closest('.favorite-star'); + if (star) { + e.preventDefault(); + const card = star.closest('.module-card'); + if (card) { + const moduleId = card.dataset.module; + if (moduleId) { + this.toggleFavorite(moduleId, star); + } + } + } + }); + + // 监听偏好变化,重新渲染收藏状态 + if (window.EventBus) { + EventBus.on('preferences:changed', (data) => { + if (data.key === 'favorites' || data.full) { + this.updateAllStars(); + } + }); + } + }, + + // 切换收藏状态 + toggleFavorite: function(moduleId, starElement) { + if (!window.ChannelPreferences) return; + + const isNowFavorite = ChannelPreferences.toggleFavorite(moduleId); + + // 更新星星样式 + if (starElement) { + if (isNowFavorite) { + starElement.classList.add('active'); + starElement.textContent = '★'; + } else { + starElement.classList.remove('active'); + starElement.textContent = '☆'; + } + } + + // 触发布局更新(收藏置顶) + this.updateFavoritesOrder(); + + // 发送事件 + if (window.EventBus) { + EventBus.emit('favorite:toggled', { + module: moduleId, + favorite: isNowFavorite + }); + } + }, + + // 更新所有星星的显示状态 + updateAllStars: function() { + const favorites = window.ChannelPreferences ? ChannelPreferences.getFavorites() : []; + document.querySelectorAll('.favorite-star').forEach(star => { + const card = star.closest('.module-card'); + if (card) { + const moduleId = card.dataset.module; + if (moduleId) { + if (favorites.includes(moduleId)) { + star.classList.add('active'); + star.textContent = '★'; + } else { + star.classList.remove('active'); + star.textContent = '☆'; + } + } + } + }); + }, + + // 根据收藏状态重新排序(收藏置顶) + updateFavoritesOrder: function() { + const container = document.querySelector('.channel-content'); + if (!container) return; + + const cards = Array.from(container.querySelectorAll('.module-card')); + const favorites = window.ChannelPreferences ? ChannelPreferences.getFavorites() : []; + + // 按收藏状态和原有顺序排序 + cards.sort((a, b) => { + const aId = a.dataset.module; + const bId = b.dataset.module; + const aFav = favorites.includes(aId); + const bFav = favorites.includes(bId); + + if (aFav && !bFav) return -1; + if (!aFav && bFav) return 1; + + // 如果都是收藏或都不是收藏,保持原有顺序 + const aOrder = cards.indexOf(a); + const bOrder = cards.indexOf(b); + return aOrder - bOrder; + }); + + // 重新插入到容器中 + cards.forEach(card => container.appendChild(card)); + + // 保存排序到偏好设置 + const moduleOrder = cards.map(card => card.dataset.module); + if (window.ChannelPreferences) { + ChannelPreferences.setModuleOrder(moduleOrder); + } + }, + + // 渲染收藏状态(初始化时调用) + renderFavorites: function() { + this.updateAllStars(); + }, + + // ===== 拖拽排序相关 ===== + + // 初始化拖拽 + initDragAndDrop: function() { + console.log('[favorites] 初始化拖拽排序'); + + const container = document.querySelector('.channel-content'); + if (!container) return; + + // 为每个卡片添加拖拽手柄 + container.querySelectorAll('.module-card').forEach(card => { + // 检查是否已有拖拽手柄 + if (!card.querySelector('.drag-handle')) { + const header = card.querySelector('.module-card-header'); + if (header) { + const handle = document.createElement('span'); + handle.className = 'drag-handle'; + handle.innerHTML = '⋮⋮'; + handle.setAttribute('draggable', 'false'); + header.insertBefore(handle, header.firstChild); + } + } + + // 设置 draggable + card.setAttribute('draggable', 'true'); + + // 移除旧监听器,添加新监听器 + card.removeEventListener('dragstart', this.handleDragStart); + card.removeEventListener('dragend', this.handleDragEnd); + card.removeEventListener('dragover', this.handleDragOver); + card.removeEventListener('dragenter', this.handleDragEnter); + card.removeEventListener('dragleave', this.handleDragLeave); + card.removeEventListener('drop', this.handleDrop); + + card.addEventListener('dragstart', this.handleDragStart.bind(this)); + card.addEventListener('dragend', this.handleDragEnd.bind(this)); + card.addEventListener('dragover', this.handleDragOver); + card.addEventListener('dragenter', this.handleDragEnter); + card.addEventListener('dragleave', this.handleDragLeave); + card.addEventListener('drop', this.handleDrop.bind(this)); + }); + }, + + // 拖拽开始 + handleDragStart: function(e) { + this.draggingElement = e.target.closest('.module-card'); + if (!this.draggingElement) return; + + e.dataTransfer.setData('text/plain', this.draggingElement.dataset.module); + e.dataTransfer.effectAllowed = 'move'; + + // 添加拖拽中的样式 + setTimeout(() => { + this.draggingElement.classList.add('dragging'); + }, 0); + }, + + // 拖拽结束 + handleDragEnd: function(e) { + const card = e.target.closest('.module-card'); + if (card) { + card.classList.remove('dragging'); + } + + // 移除所有高亮 + document.querySelectorAll('.module-card.drop-target').forEach(el => { + el.classList.remove('drop-target'); + }); + + this.draggingElement = null; + }, + + // 拖拽经过(必须阻止默认事件才能成为放置目标) + handleDragOver: function(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + }, + + // 拖拽进入 + handleDragEnter: function(e) { + const card = e.target.closest('.module-card'); + if (card && card !== this.draggingElement) { + card.classList.add('drop-target'); + } + }, + + // 拖拽离开 + handleDragLeave: function(e) { + const card = e.target.closest('.module-card'); + if (card) { + card.classList.remove('drop-target'); + } + }, + + // 放置 + handleDrop: function(e) { + e.preventDefault(); + + const targetCard = e.target.closest('.module-card'); + if (!targetCard || !this.draggingElement || targetCard === this.draggingElement) { + return; + } + + // 移除高亮 + targetCard.classList.remove('drop-target'); + + // 获取所有卡片 + const container = document.querySelector('.channel-content'); + const cards = Array.from(container.querySelectorAll('.module-card')); + + const fromIndex = cards.indexOf(this.draggingElement); + const toIndex = cards.indexOf(targetCard); + + if (fromIndex === -1 || toIndex === -1) return; + + // 重新排序 + if (window.ChannelPreferences) { + ChannelPreferences.reorderModules(fromIndex, toIndex); + } + + // 移动 DOM 元素 + if (fromIndex < toIndex) { + targetCard.insertAdjacentElement('afterend', this.draggingElement); + } else { + targetCard.insertAdjacentElement('beforebegin', this.draggingElement); + } + + // 触发事件 + if (window.EventBus) { + EventBus.emit('favorites:reordered', { + from: fromIndex, + to: toIndex, + module: this.draggingElement.dataset.module + }); + } + }, + + // 刷新拖拽功能(在布局变化后调用) + refreshDragAndDrop: function() { + this.initDragAndDrop(); + } +}; + +console.log('[favorites] 已加载'); diff --git a/modules/m-channel/channel-layout.css b/modules/m-channel/channel-layout.css new file mode 100644 index 00000000..31c39c3f --- /dev/null +++ b/modules/m-channel/channel-layout.css @@ -0,0 +1,145 @@ +/* 频道布局样式 - 三种布局模式 */ + +/* 基础卡片样式(所有布局共用) */ +.module-card { + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + transition: all 0.3s ease; + overflow: hidden; + position: relative; +} + +.module-card:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + transform: translateY(-2px); +} + +/* 卡片头部 */ +.module-card-header { + padding: 16px; + border-bottom: 1px solid #eee; + display: flex; + align-items: center; + justify-content: space-between; +} + +.module-card-title { + font-size: 18px; + font-weight: 600; + margin: 0; + display: flex; + align-items: center; + gap: 8px; +} + +/* 收藏星星 */ +.favorite-star { + font-size: 20px; + color: #ccc; + cursor: pointer; + transition: color 0.2s; + user-select: none; +} + +.favorite-star.active { + color: #fbbf24; +} + +.favorite-star:hover { + transform: scale(1.1); +} + +/* 卡片内容区 */ +.module-card-content { + padding: 16px; + min-height: 100px; +} + +/* 拖拽把手 */ +.drag-handle { + font-size: 20px; + color: #999; + cursor: grab; + user-select: none; + margin-right: 8px; +} + +.drag-handle:active { + cursor: grabbing; +} + +/* ===== 布局1:网格模式(默认) ===== */ +.layout-grid .channel-content { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 24px; + padding: 24px; +} + +.layout-grid .module-card { + height: fit-content; +} + +/* ===== 布局2:列表模式(一行一个) ===== */ +.layout-list .channel-content { + display: flex; + flex-direction: column; + gap: 16px; + padding: 24px; +} + +.layout-list .module-card { + width: 100%; +} + +.layout-list .module-card-header { + padding: 12px 16px; +} + +.layout-list .module-card-content { + padding: 12px 16px; +} + +/* ===== 布局3:紧凑模式(小卡片密排) ===== */ +.layout-compact .channel-content { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 12px; + padding: 16px; +} + +.layout-compact .module-card { + font-size: 14px; +} + +.layout-compact .module-card-header { + padding: 10px 12px; +} + +.layout-compact .module-card-title { + font-size: 16px; +} + +.layout-compact .module-card-content { + padding: 10px 12px; + min-height: 80px; +} + +/* 布局切换动画 */ +.channel-content { + transition: all 0.3s ease-in-out; +} + +/* 拖拽中的样式 */ +.module-card.dragging { + opacity: 0.6; + transform: scale(0.98); + box-shadow: 0 8px 16px rgba(0,0,0,0.2); +} + +/* 拖拽放置区高亮 */ +.module-card.drop-target { + border: 2px dashed #3b82f6; + background: #f0f9ff; +} diff --git a/modules/m-channel/channel-notifications.css b/modules/m-channel/channel-notifications.css new file mode 100644 index 00000000..5905e8df --- /dev/null +++ b/modules/m-channel/channel-notifications.css @@ -0,0 +1,200 @@ +/* 频道通知系统样式 - 环节7 */ +:root { + --update-badge-bg: #2196f3; + --unread-dot-bg: #f44336; + --notification-panel-bg: #ffffff; + --notification-panel-shadow: 0 4px 12px rgba(0,0,0,0.15); + --notification-hover-bg: #f5f5f5; +} + +/* 更新标签(蓝色) */ +.update-badge { + display: inline-block; + background-color: var(--update-badge-bg); + color: white; + font-size: 12px; + font-weight: 500; + padding: 2px 8px; + border-radius: 12px; + margin-top: 8px; + align-self: flex-start; +} + +/* 未读小红点(带数字) */ +.unread-dot { + position: absolute; + top: 8px; + right: 8px; + min-width: 18px; + height: 18px; + background-color: var(--unread-dot-bg); + color: white; + font-size: 12px; + font-weight: bold; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + z-index: 10; +} + +/* 铃铛图标容器 */ +.bell-container { + position: relative; + display: inline-block; + margin-right: 16px; + cursor: pointer; +} + +.bell-icon { + font-size: 24px; + color: #555; + transition: color 0.2s; +} + +.bell-icon:hover { + color: #000; +} + +/* 铃铛上的数字气泡 */ +.bell-badge { + position: absolute; + top: -4px; + right: -6px; + min-width: 18px; + height: 18px; + background-color: var(--unread-dot-bg); + color: white; + font-size: 12px; + font-weight: bold; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + z-index: 20; +} + +/* 侧滑通知面板 */ +.notification-panel { + position: fixed; + top: 0; + right: -400px; + width: 360px; + height: 100vh; + background: var(--notification-panel-bg); + box-shadow: var(--notification-panel-shadow); + transition: right 0.3s ease; + z-index: 1000; + display: flex; + flex-direction: column; + border-left: 1px solid #e0e0e0; +} + +.notification-panel.open { + right: 0; +} + +.panel-header { + padding: 20px; + border-bottom: 1px solid #e0e0e0; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; +} + +.panel-header button { + background: none; + border: none; + color: #2196f3; + cursor: pointer; + font-size: 14px; +} + +.panel-header button:hover { + text-decoration: underline; +} + +.notification-list { + flex: 1; + overflow-y: auto; + padding: 0; + margin: 0; + list-style: none; +} + +.notification-item { + padding: 16px 20px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background 0.2s; +} + +.notification-item:hover { + background: var(--notification-hover-bg); +} + +.notification-item.read { + opacity: 0.6; +} + +.notification-time { + font-size: 12px; + color: #999; + margin-bottom: 4px; +} + +.notification-module { + font-weight: 600; + margin-bottom: 4px; +} + +.notification-summary { + font-size: 14px; + color: #333; +} + +.empty-state { + padding: 40px 20px; + text-align: center; + color: #999; + font-size: 14px; +} + +/* 遮罩层 */ +.panel-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.3); + z-index: 999; + display: none; +} + +.panel-overlay.show { + display: block; +} + +/* 让模块卡片成为相对定位容器(用于小红点定位) */ +.module-card { + position: relative !important; +} + +/* 强制修复高亮点击问题 */ +.search-highlight, +.highlight, +[class*="highlight"] { + pointer-events: none !important; +} + +/* 确保卡片本身可点击 */ +.module-card, +[class*="module-card"] { + cursor: pointer; + pointer-events: auto !important; +} diff --git a/modules/m-channel/channel-notifications.js b/modules/m-channel/channel-notifications.js new file mode 100644 index 00000000..a14c7818 --- /dev/null +++ b/modules/m-channel/channel-notifications.js @@ -0,0 +1,365 @@ +/** + * 频道通知系统 - 环节7 (晨星陪伴版) + * 功能:模块更新提醒、未读小红点、通知面板、高亮点击修复 + * 修改:通过卡片文字内容匹配模块ID(解决moduleId undefined问题) + */ + +(function() { + // ========== 初始化模拟数据 ========== + const STORAGE_KEY = 'channel_notifications'; + + // 默认模拟数据(基于已完成模块 M06, M08, M11, 以及当前频道模块) + const DEFAULT_UPDATES = { + 'M06': { + hasUpdate: true, + count: 2, + updates: [ + { time: '10:30', summary: '修复了拖拽排序的bug' }, + { time: '昨天', summary: '新增统计面板' } + ] + }, + 'M08': { + hasUpdate: true, + count: 1, + updates: [ + { time: '昨天', summary: '优化了模块加载性能' } + ] + }, + 'M11': { + hasUpdate: true, + count: 3, + updates: [ + { time: '15:20', summary: '新增键盘快捷键' }, + { time: '昨天', summary: '修复焦点管理' }, + { time: '3月10日', summary: '模块生命周期完善' } + ] + }, + 'channel': { + hasUpdate: true, + count: 1, + updates: [ + { time: '现在', summary: '通知系统上线啦!' } + ] + } + }; + + // 加载或初始化数据 + let notificationData = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (!notificationData) { + notificationData = DEFAULT_UPDATES; + localStorage.setItem(STORAGE_KEY, JSON.stringify(notificationData)); + } + + // ========== 工具函数:从卡片文字猜测模块ID ========== + function guessModuleIdFromCard(card) { + // 尝试找卡片里的标题文字 + const titleElem = card.querySelector('h3, .module-title, .card-title, .module-name'); + if (titleElem) { + const text = titleElem.textContent.trim(); + // 简单映射:如果文字包含“工单” -> M06 + if (text.includes('工单')) return 'M06'; + if (text.includes('数据统计')) return 'M08'; + if (text.includes('组件库')) return 'M11'; + if (text.includes('调试面板')) return 'M11'; // 暂时用 M11 + } + + // 后备:用卡片内所有文字尝试匹配 + const fullText = card.textContent; + if (fullText.includes('工单')) return 'M06'; + if (fullText.includes('数据统计')) return 'M08'; + if (fullText.includes('组件库')) return 'M11'; + + return null; // 实在猜不到就返回 null + } + + function getModuleCards() { + return document.querySelectorAll('.module-card'); + } + + // ========== 更新标签渲染(带日志) ========== + function renderUpdateBadges() { + console.log('🟦 renderUpdateBadges 开始执行'); + const cards = document.querySelectorAll('.module-card'); + console.log('找到卡片数量:', cards.length); + + cards.forEach(card => { + // 先尝试原有方法,失败则用猜测 + let moduleId = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim(); + if (!moduleId) { + moduleId = guessModuleIdFromCard(card); + } + console.log('卡片最终 moduleId:', moduleId); + + // 移除旧的标签 + const oldBadge = card.querySelector('.update-badge'); + if (oldBadge) oldBadge.remove(); + + const modData = moduleId ? notificationData[moduleId] : null; + console.log('模块数据:', moduleId, modData); + + if (modData && modData.hasUpdate) { + console.log('✅ 应该添加标签的模块:', moduleId); + const badge = document.createElement('span'); + badge.className = 'update-badge'; + badge.textContent = '有更新'; + // 内联样式保证可见 + badge.style.backgroundColor = '#2196f3'; + badge.style.color = 'white'; + badge.style.padding = '2px 8px'; + badge.style.borderRadius = '12px'; + badge.style.fontSize = '12px'; + badge.style.display = 'inline-block'; + badge.style.marginTop = '8px'; + // 直接追加到卡片末尾 + card.appendChild(badge); + console.log('✅ 标签已追加到卡片', moduleId); + } else { + console.log('❌ 不需要添加标签的模块:', moduleId); + } + }); + console.log('🟦 renderUpdateBadges 执行完毕'); + } + + // ========== 小红点渲染 ========== + function renderUnreadDots() { + const cards = getModuleCards(); + cards.forEach(card => { + let moduleId = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim(); + if (!moduleId) { + moduleId = guessModuleIdFromCard(card); + } + if (!moduleId) return; + + const oldDot = card.querySelector('.unread-dot'); + if (oldDot) oldDot.remove(); + + const modData = notificationData[moduleId]; + if (modData && modData.hasUpdate && modData.count > 0) { + const dot = document.createElement('span'); + dot.className = 'unread-dot'; + dot.textContent = modData.count > 9 ? '9+' : modData.count; + card.appendChild(dot); + } + }); + } + + // ========== 标记模块已读 ========== + function markModuleAsRead(moduleId) { + if (notificationData[moduleId]) { + notificationData[moduleId].hasUpdate = false; + notificationData[moduleId].count = 0; + localStorage.setItem(STORAGE_KEY, JSON.stringify(notificationData)); + renderUpdateBadges(); + renderUnreadDots(); + updateBellBadge(); + renderNotificationPanel(); + } + } + + // ========== 铃铛相关 ========== + let bellContainer, panel, overlay; + let isPanelOpen = false; + + function createBell() { + const header = document.querySelector('.channel-header') || document.querySelector('header') || document.body; + bellContainer = document.createElement('div'); + bellContainer.className = 'bell-container'; + bellContainer.innerHTML = ` + 🔔 + + `; + const title = header.querySelector('h1, h2'); + if (title) { + title.insertAdjacentElement('afterend', bellContainer); + } else { + header.appendChild(bellContainer); + } + + panel = document.createElement('div'); + panel.className = 'notification-panel'; + panel.innerHTML = ` +
    + 通知中心 + +
    +
      + `; + overlay = document.createElement('div'); + overlay.className = 'panel-overlay'; + document.body.appendChild(panel); + document.body.appendChild(overlay); + + bellContainer.addEventListener('click', togglePanel); + overlay.addEventListener('click', closePanel); + panel.querySelector('.mark-all-read').addEventListener('click', markAllRead); + } + + function updateBellBadge() { + const totalUnread = Object.values(notificationData).reduce((acc, mod) => acc + (mod.count || 0), 0); + const badge = bellContainer?.querySelector('.bell-badge'); + if (badge) { + if (totalUnread > 0) { + badge.style.display = 'flex'; + badge.textContent = totalUnread > 9 ? '9+' : totalUnread; + } else { + badge.style.display = 'none'; + } + } + } + + function togglePanel(e) { + e.stopPropagation(); + isPanelOpen ? closePanel() : openPanel(); + } + + function openPanel() { + isPanelOpen = true; + panel.classList.add('open'); + overlay.classList.add('show'); + renderNotificationPanel(); + } + + function closePanel() { + isPanelOpen = false; + panel.classList.remove('open'); + overlay.classList.remove('show'); + } + + function renderNotificationPanel() { + const list = panel.querySelector('.notification-list'); + if (!list) return; + + let items = []; + for (const [moduleId, modData] of Object.entries(notificationData)) { + if (modData.updates && modData.updates.length > 0) { + modData.updates.forEach((update) => { + items.push({ + moduleId, + time: update.time, + summary: update.summary, + read: !modData.hasUpdate + }); + }); + } + } + + if (items.length === 0) { + list.innerHTML = '
    • 暂无新通知
    • '; + return; + } + + items.sort((a, b) => (a.time > b.time ? -1 : 1)); + + list.innerHTML = items.map(item => ` +
    • +
      ${item.time}
      +
      ${item.moduleId}
      +
      ${item.summary}
      +
    • + `).join(''); + + list.querySelectorAll('.notification-item').forEach(item => { + item.addEventListener('click', (e) => { + const moduleId = item.dataset.module; + const card = findCardByModuleId(moduleId); + if (card) { + card.click(); + } + markModuleAsRead(moduleId); + closePanel(); + }); + }); + } + + function findCardByModuleId(moduleId) { + const cards = getModuleCards(); + for (let card of cards) { + let id = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim(); + if (!id) id = guessModuleIdFromCard(card); + if (id === moduleId) return card; + } + return null; + } + + function markAllRead() { + for (let moduleId in notificationData) { + notificationData[moduleId].hasUpdate = false; + notificationData[moduleId].count = 0; + } + localStorage.setItem(STORAGE_KEY, JSON.stringify(notificationData)); + renderUpdateBadges(); + renderUnreadDots(); + updateBellBadge(); + renderNotificationPanel(); + } + + function fixHighlightClick() { + const container = document.querySelector('.modules-grid') || document.body; + container.addEventListener('click', (e) => { + const card = e.target.closest('.module-card'); + if (card) { + // 不做额外处理,只是确保事件冒泡 + } + }, true); + + const style = document.createElement('style'); + style.textContent = ` + .search-highlight { + pointer-events: none !important; + } + .module-card { + cursor: pointer; + } + `; + document.head.appendChild(style); + } + + function init() { + if (!document.querySelector('link[href*="channel-notifications.css"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'channel-notifications.css'; + document.head.appendChild(link); + } + + renderUpdateBadges(); + renderUnreadDots(); + + createBell(); + updateBellBadge(); + + fixHighlightClick(); + + document.addEventListener('click', (e) => { + const card = e.target.closest('.module-card'); + if (card) { + let moduleId = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim(); + if (!moduleId) moduleId = guessModuleIdFromCard(card); + if (moduleId) { + setTimeout(() => markModuleAsRead(moduleId), 100); + } + } + }); + + window.addEventListener('modulesRendered', () => { + renderUpdateBadges(); + renderUnreadDots(); + }); + + let lastCardCount = 0; + setInterval(() => { + const cards = getModuleCards(); + if (cards.length !== lastCardCount) { + lastCardCount = cards.length; + renderUpdateBadges(); + renderUnreadDots(); + } + }, 2000); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/modules/m-channel/channel-preferences.js b/modules/m-channel/channel-preferences.js new file mode 100644 index 00000000..a4ee89c9 --- /dev/null +++ b/modules/m-channel/channel-preferences.js @@ -0,0 +1,194 @@ +// 频道偏好管理 - 记住用户的装修设置 +window.ChannelPreferences = { + STORAGE_KEY: 'hololake_channel_preferences', + + // 默认配置 + defaults: { + layout: 'grid', // grid, list, compact + theme: 'default', // default, ocean, forest, sunset, lavender + favorites: [], // 收藏的模块ID列表 + moduleOrder: [], // 模块排序顺序(默认按ID) + stats: {} // 使用统计 + }, + + // 当前配置 + config: null, + + // 初始化(加载配置) + init: function() { + console.log('[preferences] 初始化'); + this.load(); + return this.config; + }, + + // 加载配置 + load: function() { + const saved = localStorage.getItem(this.STORAGE_KEY); + if (saved) { + try { + this.config = JSON.parse(saved); + console.log('[preferences] 加载配置:', this.config); + } catch (e) { + console.error('[preferences] 解析失败,使用默认配置', e); + this.config = { ...this.defaults }; + } + } else { + console.log('[preferences] 无保存配置,使用默认'); + this.config = { ...this.defaults }; + } + return this.config; + }, + + // 保存配置 + save: function() { + if (!this.config) return; + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.config)); + console.log('[preferences] 保存配置:', this.config); + }, + + // 获取指定键的值 + get: function(key) { + if (!this.config) this.load(); + return this.config[key]; + }, + + // 设置指定键的值(自动保存) + set: function(key, value) { + if (!this.config) this.load(); + this.config[key] = value; + this.save(); + + // 触发事件总线通知 + if (window.EventBus) { + EventBus.emit('preferences:changed', { key, value }); + } + }, + + // 更新整个配置(合并) + update: function(newConfig) { + if (!this.config) this.load(); + this.config = { ...this.config, ...newConfig }; + this.save(); + if (window.EventBus) { + EventBus.emit('preferences:changed', { full: this.config }); + } + }, + + // 恢复默认设置 + reset: function() { + this.config = { ...this.defaults }; + this.save(); + if (window.EventBus) { + EventBus.emit('preferences:reset', this.config); + } + console.log('[preferences] 恢复默认'); + }, + + // 记录模块使用(用于统计) + recordUsage: function(moduleId) { + if (!this.config) this.load(); + if (!this.config.stats) this.config.stats = {}; + if (!this.config.stats[moduleId]) { + this.config.stats[moduleId] = { + count: 0, + lastUsed: null, + totalTime: 0 + }; + } + this.config.stats[moduleId].count++; + this.config.stats[moduleId].lastUsed = Date.now(); + this.save(); + + // 开始计时(将在模块卸载时记录时长) + this.startTiming(moduleId); + }, + + // 计时相关 + timing: {}, + startTiming: function(moduleId) { + this.timing[moduleId] = Date.now(); + }, + stopTiming: function(moduleId) { + if (this.timing[moduleId]) { + const duration = (Date.now() - this.timing[moduleId]) / 1000; // 秒 + if (this.config.stats[moduleId]) { + this.config.stats[moduleId].totalTime += duration; + this.save(); + } + delete this.timing[moduleId]; + } + }, + + // 获取统计报告 + getStats: function() { + if (!this.config) this.load(); + return this.config.stats || {}; + }, + + // 获取收藏列表 + getFavorites: function() { + return this.get('favorites') || []; + }, + + // 切换收藏状态 + toggleFavorite: function(moduleId) { + let favorites = this.get('favorites') || []; + if (favorites.includes(moduleId)) { + favorites = favorites.filter(id => id !== moduleId); + } else { + favorites.push(moduleId); + } + this.set('favorites', favorites); + return favorites.includes(moduleId); + }, + + // 获取布局 + getLayout: function() { + return this.get('layout') || 'grid'; + }, + + // 设置布局 + setLayout: function(layout) { + this.set('layout', layout); + }, + + // 获取主题 + getTheme: function() { + return this.get('theme') || 'default'; + }, + + // 设置主题 + setTheme: function(theme) { + this.set('theme', theme); + }, + + // 获取模块排序 + getModuleOrder: function() { + return this.get('moduleOrder') || []; + }, + + // 设置模块排序 + setModuleOrder: function(order) { + this.set('moduleOrder', order); + }, + + // 重新排序(拖拽后调用) + reorderModules: function(fromIndex, toIndex) { + const order = this.getModuleOrder(); + if (order.length === 0) { + // 如果还没有自定义顺序,则基于当前模块列表生成 + const modules = window.ModuleRegistry ? ModuleRegistry.list() : []; + // 排除非显示模块(如调试面板) + const displayModules = modules.filter(m => ['m06','m08','m11','home','debug'].includes(m)); + this.setModuleOrder(displayModules); + } + // 重新加载最新顺序 + const currentOrder = this.getModuleOrder(); + if (fromIndex < 0 || fromIndex >= currentOrder.length || toIndex < 0 || toIndex >= currentOrder.length) return; + const [moved] = currentOrder.splice(fromIndex, 1); + currentOrder.splice(toIndex, 0, moved); + this.setModuleOrder(currentOrder); + } +}; + +console.log('[preferences] 已加载'); diff --git a/modules/m-channel/channel-router.js b/modules/m-channel/channel-router.js index 614fc3ed..3a51bc8c 100644 --- a/modules/m-channel/channel-router.js +++ b/modules/m-channel/channel-router.js @@ -1,98 +1,222 @@ -// ================== 路由配置 ================== -const routes = { - 'home': 'views/home.html', - 'channel': 'views/channel.html', - 'about': 'views/about.html' -}; +// 频道路由(支持真实模块适配器 + 卡片列表首页 + 数据采集钩子) +window.ChannelRouter = { + currentChannel: 'home', + contentEl: null, + + init: function(contentElement) { + this.contentEl = contentElement; + console.log('[router] 初始化'); + + const savedState = ChannelState.restoreState(); + if (savedState && savedState.currentChannel) { + this.currentChannel = savedState.currentChannel; + } + + this.loadChannel(this.currentChannel); + + if (savedState && savedState.visited) { + savedState.visited.forEach(channel => { + document.querySelectorAll('.channel-btn').forEach(btn => { + if (btn.dataset.channel === channel) { + btn.classList.add('visited'); + } + }); + }); + } -// 获取当前 hash 中的路径(去掉 #/) -function getHashPath() { - const hash = window.location.hash.slice(1) || '/'; - const path = hash.startsWith('/') ? hash.slice(1) : hash; - return path || 'home'; -} - -// ================== 加载视图 ================== -async function loadView(path) { - const routerView = document.getElementById('router-view'); - if (!routerView) return; - - // 显示加载动画 - routerView.innerHTML = '
      '; - - try { - const viewFile = routes[path]; - if (!viewFile) { - await load404(routerView); + // 页面关闭时结束计时 + window.addEventListener('beforeunload', function() { + if (typeof ChannelAnalytics !== 'undefined') { + ChannelAnalytics.endSession(); + } + }); + }, + + navigateTo: function(channel) { + if (channel === this.currentChannel) return; + + console.log(`[router] 导航到: ${channel}`); + + this.contentEl.classList.add('fade-out'); + + setTimeout(() => { + this.loadChannel(channel); + this.contentEl.classList.remove('fade-out'); + this.contentEl.classList.add('fade-in'); + setTimeout(() => { + this.contentEl.classList.remove('fade-in'); + }, 300); + }, 300); + + this.currentChannel = channel; + + document.querySelectorAll('.channel-btn').forEach(btn => { + btn.classList.remove('active'); + if (btn.dataset.channel === channel) { + btn.classList.add('active'); + } + }); + + ChannelState.saveState({ + currentChannel: channel, + visited: this.getVisitedChannels() + }); + }, + + loadChannel: function(channel) { + if (!this.contentEl) return; + + // 数据采集钩子:开始加载 + if (typeof ChannelAnalytics !== 'undefined') { + ChannelAnalytics.markLoadStart(); + if (channel !== 'home' && channel !== 'debug' && channel !== 'dashboard') { + ChannelAnalytics.recordVisit(channel); + } + } + + // 如果是真实模块(m06/m08/m11) + if (channel === 'm06' || channel === 'm08' || channel === 'm11') { + if (window.ModuleAdapter) { + ModuleAdapter.loadModule(channel, 'channel-content'); + // 模块加载完成后记录加载时间 + setTimeout(() => { + if (typeof ChannelAnalytics !== 'undefined') { + ChannelAnalytics.markLoadEnd(channel); + } + }, 100); + } else { + this.contentEl.innerHTML = '
      适配器未加载
      '; + } + ModuleLifecycle.onLoad(channel); return; } - - const response = await fetch(viewFile); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + + // 数据面板路由(环节8新增) + if (channel === 'dashboard') { + fetch('views/channel-dashboard.html') + .then(response => response.text()) + .then(html => { + this.contentEl.innerHTML = html; + // 渲染图表 + if (typeof ChannelDashboard !== 'undefined') { + ChannelDashboard.render(); + } + ModuleLifecycle.onLoad('dashboard'); + // 记录加载完成 + if (typeof ChannelAnalytics !== 'undefined') { + ChannelAnalytics.markLoadEnd('dashboard'); + } + }) + .catch(err => { + this.contentEl.innerHTML = '

      数据面板加载失败

      '; + console.error(err); + }); + return; } - const html = await response.text(); - routerView.innerHTML = html; - } catch (error) { - console.error('加载视图失败:', error); - routerView.innerHTML = ` -
      - ❌ 加载失败:${error.message}
      - 请检查文件是否存在,或刷新重试 -
      - `; - } - - // 更新导航高亮和状态栏 - updateActiveNav(path); - updateStatusBar(path); -} - -// 加载 404 页面 -async function load404(container) { - try { - const resp = await fetch('views/404.html'); - if (resp.ok) { - container.innerHTML = await resp.text(); + + // 内置频道 + if (channel === 'home') { + this.renderModuleCards(); + } else if (channel === 'debug') { + this.loadDebugPanel(); } else { - container.innerHTML = '
      ⚠️ 404 - 页面未找到
      '; + this.contentEl.innerHTML = '

      未知频道

      '; } - } catch { - container.innerHTML = '
      ⚠️ 404 - 页面未找到
      '; - } -} -// 更新导航高亮 -function updateActiveNav(path) { - document.querySelectorAll('.nav-link').forEach(link => { - link.classList.remove('active'); - const linkPath = link.getAttribute('href').slice(2); - if (linkPath === path) { - link.classList.add('active'); + // 对于内置频道也记录加载完成 + if (typeof ChannelAnalytics !== 'undefined' && channel !== 'dashboard') { + ChannelAnalytics.markLoadEnd(channel); } - }); -} - -// 更新状态栏 -function updateStatusBar(path) { - const statusEl = document.getElementById('current-route'); - if (statusEl) { - statusEl.textContent = `当前路由:/${path}`; + }, + + // 渲染所有模块卡片(用于首页) + renderModuleCards: function() { + const modules = [ + { id: 'm06', name: '工单管理', icon: '📋', desc: '管理任务和工单' }, + { id: 'm08', name: '数据统计', icon: '📊', desc: '查看数据统计和报表' }, + { id: 'm11', name: '组件库', icon: '🧩', desc: '系统组件展示' }, + { id: 'debug', name: '调试面板', icon: '🐞', desc: '查看事件和模块状态' }, + { id: 'dashboard', name: '数据面板', icon: '📈', desc: '查看使用统计和性能' } + ]; + + let html = '
      '; + modules.forEach(mod => { + const isFavorite = window.ChannelPreferences ? + ChannelPreferences.getFavorites().includes(mod.id) : false; + html += ` +
      +
      + ⋮⋮ + ${mod.icon} ${mod.name} + ${isFavorite ? '★' : '☆'} +
      +
      +

      ${mod.desc}

      +
      +
      + `; + }); + html += '
      '; + + this.contentEl.innerHTML = html; + ModuleLifecycle.onLoad('home'); + + // 触发收藏/拖拽重新初始化 + if (window.ChannelFavorites) { + setTimeout(() => { + ChannelFavorites.init(); + ChannelFavorites.initDragAndDrop(); + }, 100); + } + }, + + loadDebugPanel: function() { + fetch('views/channel-debug.html') + .then(response => response.text()) + .then(html => { + this.contentEl.innerHTML = html; + ModuleLifecycle.onLoad('debug'); + this.initDebugPanel(); + }) + .catch(err => { + this.contentEl.innerHTML = '

      调试面板加载失败

      '; + }); + }, + + initDebugPanel: function() { + const msgList = document.getElementById('debug-messages'); + if (msgList && window.debugMessages) { + window.debugMessages.forEach(msg => { + const li = document.createElement('li'); + li.textContent = `[${msg.time}] ${msg.event}: ${JSON.stringify(msg.data)}`; + msgList.appendChild(li); + }); + } + const sendBtn = document.getElementById('debug-send'); + const input = document.getElementById('debug-input'); + if (sendBtn && input) { + sendBtn.addEventListener('click', () => { + const text = input.value.trim(); + if (text) { + EventBus.emit('debug:message', { text, from: '调试面板' }); + input.value = ''; + } + }); + } + const clearBtn = document.getElementById('debug-clear'); + if (clearBtn) { + clearBtn.addEventListener('click', () => { + ChannelState.clearState(); + alert('状态已清除,刷新页面生效'); + }); + } + }, + + getVisitedChannels: function() { + const visited = []; + document.querySelectorAll('.channel-btn.visited').forEach(btn => { + visited.push(btn.dataset.channel); + }); + return visited; } -} - -// 监听 hash 变化 -window.addEventListener('hashchange', () => { - const path = getHashPath(); - loadView(path); -}); - -// 首次加载 -window.addEventListener('DOMContentLoaded', () => { - if (!window.location.hash) { - window.location.hash = '#/home'; - } else { - const path = getHashPath(); - loadView(path); - } -}); +}; diff --git a/modules/m-channel/channel-router.js.bak b/modules/m-channel/channel-router.js.bak new file mode 100644 index 00000000..957038fb --- /dev/null +++ b/modules/m-channel/channel-router.js.bak @@ -0,0 +1,190 @@ +// 频道路由(支持真实模块适配器 + 卡片列表首页) +window.ChannelRouter = { + currentChannel: 'home', + contentEl: null, + + init: function(contentElement) { + this.contentEl = contentElement; + console.log('[router] 初始化'); + + const savedState = ChannelState.restoreState(); + if (savedState && savedState.currentChannel) { + this.currentChannel = savedState.currentChannel; + } + + this.loadChannel(this.currentChannel); + + if (savedState && savedState.visited) { + savedState.visited.forEach(channel => { + document.querySelectorAll('.channel-btn').forEach(btn => { + if (btn.dataset.channel === channel) { + btn.classList.add('visited'); + } + }); + }); + } + + // 【新增】页面关闭时结束计时 + window.addEventListener('beforeunload', function() { + if (typeof ChannelAnalytics !== 'undefined') { + ChannelAnalytics.endSession(); + } + }); + }, + + navigateTo: function(channel) { + if (channel === this.currentChannel) return; + + console.log(`[router] 导航到: ${channel}`); + + this.contentEl.classList.add('fade-out'); + + setTimeout(() => { + this.loadChannel(channel); + this.contentEl.classList.remove('fade-out'); + this.contentEl.classList.add('fade-in'); + setTimeout(() => { + this.contentEl.classList.remove('fade-in'); + }, 300); + }, 300); + + this.currentChannel = channel; + + document.querySelectorAll('.channel-btn').forEach(btn => { + btn.classList.remove('active'); + if (btn.dataset.channel === channel) { + btn.classList.add('active'); + } + }); + + ChannelState.saveState({ + currentChannel: channel, + visited: this.getVisitedChannels() + }); + }, + + loadChannel: function(channel) { + if (!this.contentEl) return; + + // 如果是真实模块(m06/m08/m11) + if (channel === 'm06' || channel === 'm08' || channel === 'm11') { + // 【新增】数据采集钩子:标记加载开始并记录访问 + if (typeof ChannelAnalytics !== 'undefined') { + ChannelAnalytics.markLoadStart(); + ChannelAnalytics.recordVisit(channel); + } + + if (window.ModuleAdapter) { + ModuleAdapter.loadModule(channel, 'channel-content'); + } else { + this.contentEl.innerHTML = '
      适配器未加载
      '; + } + ModuleLifecycle.onLoad(channel); + + // 【新增】数据采集钩子:标记加载完成 + if (typeof ChannelAnalytics !== 'undefined') { + ChannelAnalytics.markLoadEnd(channel); + } + return; + } + + // 内置频道 + if (channel === 'home') { + this.renderModuleCards(); // 显示模块卡片列表 + } else if (channel === 'debug') { + this.loadDebugPanel(); + } else { + this.contentEl.innerHTML = '

      未知频道

      '; + } + }, + + // 渲染所有模块卡片(用于首页) + renderModuleCards: function() { + const modules = [ + { id: 'm06', name: '工单管理', icon: '📋', desc: '管理任务和工单' }, + { id: 'm08', name: '数据统计', icon: '📊', desc: '查看数据统计和报表' }, + { id: 'm11', name: '组件库', icon: '🧩', desc: '系统组件展示' }, + { id: 'debug', name: '调试面板', icon: '🐞', desc: '查看事件和模块状态' } + ]; + + let html = '
      '; + modules.forEach(mod => { + const isFavorite = window.ChannelPreferences ? + ChannelPreferences.getFavorites().includes(mod.id) : false; + html += ` +
      +
      + ⋮⋮ + ${mod.icon} ${mod.name} + ${isFavorite ? '★' : '☆'} +
      +
      +

      ${mod.desc}

      +
      +
      + `; + }); + html += '
      '; + + this.contentEl.innerHTML = html; + ModuleLifecycle.onLoad('home'); + + // 触发收藏/拖拽重新初始化 + if (window.ChannelFavorites) { + setTimeout(() => { + ChannelFavorites.init(); + ChannelFavorites.initDragAndDrop(); + }, 100); + } + }, + + loadDebugPanel: function() { + fetch('views/channel-debug.html') + .then(response => response.text()) + .then(html => { + this.contentEl.innerHTML = html; + ModuleLifecycle.onLoad('debug'); + this.initDebugPanel(); + }) + .catch(err => { + this.contentEl.innerHTML = '

      调试面板加载失败

      '; + }); + }, + + initDebugPanel: function() { + const msgList = document.getElementById('debug-messages'); + if (msgList && window.debugMessages) { + window.debugMessages.forEach(msg => { + const li = document.createElement('li'); + li.textContent = `[${msg.time}] ${msg.event}: ${JSON.stringify(msg.data)}`; + msgList.appendChild(li); + }); + } + const sendBtn = document.getElementById('debug-send'); + const input = document.getElementById('debug-input'); + if (sendBtn && input) { + sendBtn.addEventListener('click', () => { + const text = input.value.trim(); + if (text) { + EventBus.emit('debug:message', { text, from: '调试面板' }); + input.value = ''; + } + }); + } + const clearBtn = document.getElementById('debug-clear'); + if (clearBtn) { + clearBtn.addEventListener('click', () => { + ChannelState.clearState(); + alert('状态已清除,刷新页面生效'); + }); + } + }, + + getVisitedChannels: function() { + const visited = []; + document.querySelectorAll('.channel-btn.visited').forEach(btn => { + visited.push(btn.dataset.channel); + }); + return visited; + } +}; diff --git a/modules/m-channel/channel-router.js.bakcat b/modules/m-channel/channel-router.js.bakcat new file mode 100644 index 00000000..e69de29b diff --git a/modules/m-channel/channel-state.js b/modules/m-channel/channel-state.js new file mode 100644 index 00000000..38edffd6 --- /dev/null +++ b/modules/m-channel/channel-state.js @@ -0,0 +1,44 @@ +// 状态管理(localStorage) +window.ChannelState = { + STORAGE_KEY: 'hololake_channel_state', + + // 保存状态 + saveState: function(state) { + const data = { + ...state, + timestamp: Date.now(), + visited: state.visited || [] + }; + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data)); + console.log('[state] 保存状态:', data); + }, + + // 恢复状态 + restoreState: function() { + const saved = localStorage.getItem(this.STORAGE_KEY); + if (!saved) return null; + try { + const state = JSON.parse(saved); + console.log('[state] 恢复状态:', state); + return state; + } catch (e) { + console.error('[state] 解析失败:', e); + return null; + } + }, + + // 标记已访问 + markVisited: function(channel) { + const state = this.restoreState() || { visited: [] }; + if (!state.visited.includes(channel)) { + state.visited.push(channel); + this.saveState(state); + } + }, + + // 清除状态 + clearState: function() { + localStorage.removeItem(this.STORAGE_KEY); + console.log('[state] 已清除'); + } +}; diff --git a/modules/m-channel/channel-stats.js b/modules/m-channel/channel-stats.js new file mode 100644 index 00000000..d4263fe1 --- /dev/null +++ b/modules/m-channel/channel-stats.js @@ -0,0 +1,298 @@ +// 频道使用统计管理 +window.ChannelStats = { + // 初始化 + init: function() { + console.log('[stats] 初始化'); + this.bindEvents(); + }, + + // 绑定事件 + bindEvents: function() { + if (!window.EventBus) return; + + // 监听模块加载,开始计时 + EventBus.on('module:loaded', (data) => { + if (data && data.module) { + this.recordView(data.module); + } + }); + + // 监听模块卸载,结束计时 + EventBus.on('module:unloaded', (data) => { + if (data && data.module) { + if (window.ChannelPreferences) { + ChannelPreferences.stopTiming(data.module); + } + } + }); + + // 监听页面关闭,停止所有计时 + window.addEventListener('beforeunload', () => { + if (window.ChannelPreferences && window.ChannelPreferences.timing) { + Object.keys(ChannelPreferences.timing).forEach(moduleId => { + ChannelPreferences.stopTiming(moduleId); + }); + } + }); + }, + + // 记录模块访问 + recordView: function(moduleId) { + if (!window.ChannelPreferences) return; + + // 记录使用次数和开始计时 + ChannelPreferences.recordUsage(moduleId); + + // 发送事件 + if (window.EventBus) { + EventBus.emit('stats:recorded', { + module: moduleId, + time: Date.now() + }); + } + }, + + // 获取统计报告 + getStatsReport: function() { + if (!window.ChannelPreferences) return {}; + + const stats = ChannelPreferences.getStats(); + const modules = window.ModuleRegistry ? ModuleRegistry.list() : []; + + // 格式化统计数据 + const report = { + totalViews: 0, + mostUsed: null, + lastUsed: null, + moduleDetails: {} + }; + + let maxCount = 0; + let lastTime = 0; + + modules.forEach(moduleId => { + const moduleStats = stats[moduleId] || { count: 0, lastUsed: null, totalTime: 0 }; + report.moduleDetails[moduleId] = { + count: moduleStats.count, + lastUsed: moduleStats.lastUsed ? new Date(moduleStats.lastUsed).toLocaleString() : '从未使用', + totalTime: moduleStats.totalTime ? this.formatTime(moduleStats.totalTime) : '0秒' + }; + + report.totalViews += moduleStats.count; + + if (moduleStats.count > maxCount) { + maxCount = moduleStats.count; + report.mostUsed = moduleId; + } + + if (moduleStats.lastUsed && moduleStats.lastUsed > lastTime) { + lastTime = moduleStats.lastUsed; + report.lastUsed = moduleId; + } + }); + + return report; + }, + + // 格式化时间(秒 -> 可读格式) + formatTime: function(seconds) { + if (seconds < 60) return `${Math.round(seconds)}秒`; + if (seconds < 3600) { + const minutes = Math.floor(seconds / 60); + const secs = Math.round(seconds % 60); + return `${minutes}分${secs}秒`; + } + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return `${hours}小时${minutes}分`; + }, + + // 渲染统计面板 + renderStatsPanel: function(container) { + if (!container) return; + + const report = this.getStatsReport(); + const modules = window.ModuleRegistry ? ModuleRegistry.list() : []; + + let html = ` +
      +

      📊 使用统计

      +
      +
      + 总访问次数: + ${report.totalViews} +
      + ${report.mostUsed ? ` +
      + 最常用模块: + ${this.getModuleName(report.mostUsed)} (${report.moduleDetails[report.mostUsed]?.count || 0}次) +
      + ` : ''} + ${report.lastUsed ? ` +
      + 最近使用: + ${this.getModuleName(report.lastUsed)} +
      + ` : ''} +
      + +

      模块详情

      + + + + + + + + + + + `; + + modules.forEach(moduleId => { + const detail = report.moduleDetails[moduleId] || { count: 0, totalTime: '0秒', lastUsed: '从未使用' }; + html += ` + + + + + + + `; + }); + + html += ` + +
      模块访问次数累计使用时长最后使用
      ${this.getModuleName(moduleId)}${detail.count}${detail.totalTime}${detail.lastUsed}
      +
      + +
      +
      + `; + + container.innerHTML = html; + + // 绑定重置按钮 + const resetBtn = document.getElementById('reset-stats-btn'); + if (resetBtn) { + resetBtn.addEventListener('click', () => { + if (confirm('确定要重置所有统计数据吗?')) { + this.resetStats(); + } + }); + } + }, + + // 获取模块显示名称 + getModuleName: function(moduleId) { + const names = { + 'm06': '工单管理', + 'm08': '数据统计', + 'm11': '组件库', + 'home': '首页', + 'debug': '调试面板' + }; + return names[moduleId] || moduleId; + }, + + // 重置统计 + resetStats: function() { + if (!window.ChannelPreferences) return; + + const config = ChannelPreferences.config; + if (config) { + config.stats = {}; + ChannelPreferences.save(); + + if (window.EventBus) { + EventBus.emit('stats:reset'); + } + + alert('统计数据已重置'); + + // 重新渲染统计面板 + const container = document.querySelector('.stats-panel-container'); + if (container) { + this.renderStatsPanel(container); + } + } + }, + + // 注入样式 + injectStyles: function() { + const style = document.createElement('style'); + style.textContent = ` + .stats-panel { + padding: 20px; + background: white; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + } + .stats-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin: 20px 0; + padding: 16px; + background: #f8fafc; + border-radius: 8px; + } + .stat-item { + font-size: 14px; + } + .stat-label { + color: #64748b; + } + .stat-value { + font-weight: 600; + color: #0f172a; + margin-left: 8px; + } + .stats-table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; + } + .stats-table th, + .stats-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #e2e8f0; + } + .stats-table th { + background: #f1f5f9; + font-weight: 600; + color: #334155; + } + .stats-table tr:hover { + background: #f8fafc; + } + .stats-actions { + text-align: right; + margin-top: 20px; + } + .stats-reset-btn { + padding: 8px 16px; + background: #ef4444; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + } + .stats-reset-btn:hover { + background: #dc2626; + } + `; + document.head.appendChild(style); + } +}; + +// 自动注入样式 +setTimeout(() => { + if (window.ChannelStats) { + ChannelStats.injectStyles(); + } +}, 100); + +console.log('[stats] 已加载'); diff --git a/modules/m-channel/channel-style.css b/modules/m-channel/channel-style.css index 24da6655..b03191ef 100644 --- a/modules/m-channel/channel-style.css +++ b/modules/m-channel/channel-style.css @@ -1,150 +1,62 @@ -/* 基础重置 */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; - background: #f5f7fb; - color: #1e293b; - line-height: 1.5; - min-height: 100vh; - display: flex; - flex-direction: column; -} - -/* 导航栏 */ -.channel-nav { - background: white; - padding: 1rem 2rem; - box-shadow: 0 2px 8px rgba(0,0,0,0.05); - display: flex; - gap: 2rem; - border-bottom: 1px solid #e2e8f0; -} - -.nav-link { - text-decoration: none; - color: #64748b; - font-weight: 500; - padding: 0.5rem 0; - border-bottom: 2px solid transparent; - transition: all 0.2s; -} - -.nav-link:hover { - color: #0284c7; -} - -.nav-link.active { - color: #0284c7; - border-bottom-color: #0284c7; -} - -/* 路由视图容器 */ -.router-view { - flex: 1; - padding: 2rem; +/* 频道基础样式 */ +.channel-container { max-width: 1200px; margin: 0 auto; - width: 100%; - background: white; - border-radius: 12px 12px 0 0; - box-shadow: 0 -4px 12px rgba(0,0,0,0.02); + padding: 20px; + font-family: system-ui, -apple-system, sans-serif; } -/* 状态栏 */ -.status-bar { - background: #1e293b; - color: #94a3b8; - padding: 0.75rem 2rem; - font-size: 0.9rem; - text-align: center; - border-top: 1px solid #334155; +.channel-nav { + display: flex; + gap: 10px; + margin-bottom: 20px; + border-bottom: 2px solid #e5e7eb; + padding-bottom: 10px; } -/* 卡片网格 */ -.card-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 1.5rem; - margin-top: 1.5rem; -} - -.module-card { - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 12px; - padding: 1.5rem 1rem; - text-align: center; - cursor: pointer; - transition: all 0.2s; - box-shadow: 0 2px 4px rgba(0,0,0,0.02); -} - -.module-card:hover { - transform: translateY(-4px); - border-color: #0284c7; - box-shadow: 0 12px 20px -8px rgba(2, 132, 199, 0.2); -} - -.module-card h3 { - margin-bottom: 0.5rem; - color: #0f172a; -} - -.module-card p { - font-size: 0.9rem; - color: #64748b; -} - -/* 加载动画 */ -.loader { - display: inline-block; - width: 24px; - height: 24px; - border: 3px solid #e2e8f0; - border-top-color: #0284c7; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* 错误提示 */ -.error-message { - background: #fee2e2; - border: 1px solid #ef4444; - color: #b91c1c; - padding: 1rem; +.channel-btn { + padding: 10px 20px; + border: none; + background: #f3f4f6; border-radius: 8px; - margin: 1rem 0; -} - -/* 模块内容区 */ -.module-content { - margin-top: 2rem; - border-top: 1px dashed #cbd5e1; - padding-top: 2rem; -} - -.back-button { - background: none; - border: 1px solid #cbd5e1; - padding: 0.5rem 1.5rem; - border-radius: 30px; cursor: pointer; - font-size: 0.95rem; - color: #475569; + font-size: 16px; transition: all 0.2s; - margin-bottom: 1rem; } -.back-button:hover { - background: #f1f5f9; - border-color: #94a3b8; +.channel-btn:hover { + background: #e5e7eb; +} + +.channel-btn.active { + background: #3b82f6; + color: white; +} + +.channel-btn.visited { + position: relative; +} + +.channel-btn.visited::after { + content: "✓"; + position: absolute; + top: -5px; + right: -5px; + background: #10b981; + color: white; + border-radius: 50%; + width: 20px; + height: 20px; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; +} + +.channel-content { + min-height: 400px; + padding: 20px; + background: white; + border-radius: 12px; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } diff --git a/modules/m-channel/channel-theme.js b/modules/m-channel/channel-theme.js new file mode 100644 index 00000000..24ee8950 --- /dev/null +++ b/modules/m-channel/channel-theme.js @@ -0,0 +1,157 @@ +// 频道主题管理 - 换主题色 +window.ChannelTheme = { + // 预设主题 + themes: { + default: { + name: '默认蓝', + colors: { + primary: '#3b82f6', + primaryDark: '#2563eb', + secondary: '#10b981', + background: '#ffffff', + surface: '#f9fafb', + text: '#1f2937', + textLight: '#6b7280', + border: '#e5e7eb' + } + }, + ocean: { + name: '海洋', + colors: { + primary: '#0891b2', + primaryDark: '#0e7490', + secondary: '#2dd4bf', + background: '#ecfeff', + surface: '#ffffff', + text: '#164e63', + textLight: '#155e75', + border: '#a5f3fc' + } + }, + forest: { + name: '森林', + colors: { + primary: '#059669', + primaryDark: '#047857', + secondary: '#fbbf24', + background: '#f0fdf4', + surface: '#ffffff', + text: '#064e3b', + textLight: '#065f46', + border: '#a7f3d0' + } + }, + sunset: { + name: '日落', + colors: { + primary: '#d97706', + primaryDark: '#b45309', + secondary: '#f43f5e', + background: '#fff7ed', + surface: '#ffffff', + text: '#7c2d12', + textLight: '#9a3412', + border: '#fed7aa' + } + }, + lavender: { + name: '薰衣草', + colors: { + primary: '#8b5cf6', + primaryDark: '#7c3aed', + secondary: '#ec4899', + background: '#f5f3ff', + surface: '#ffffff', + text: '#4c1d95', + textLight: '#5b21b6', + border: '#ddd6fe' + } + } + }, + + // 当前主题ID + currentTheme: 'default', + + // 初始化主题 + init: function() { + console.log('[theme] 初始化'); + + // 从偏好设置加载主题 + if (window.ChannelPreferences) { + const savedTheme = ChannelPreferences.getTheme(); + if (savedTheme && this.themes[savedTheme]) { + this.currentTheme = savedTheme; + } + } + + // 应用主题 + this.applyTheme(this.currentTheme); + + // 监听主题变化事件 + if (window.EventBus) { + EventBus.on('preferences:changed', (data) => { + if (data.key === 'theme') { + this.applyTheme(data.value); + } + }); + } + }, + + // 应用主题 + applyTheme: function(themeId) { + if (!this.themes[themeId]) { + console.error(`[theme] 未知主题: ${themeId}`); + return; + } + + this.currentTheme = themeId; + const colors = this.themes[themeId].colors; + + // 设置 CSS 自定义属性 + const root = document.documentElement; + for (const [key, value] of Object.entries(colors)) { + root.style.setProperty(`--theme-${key}`, value); + } + + console.log(`[theme] 应用主题: ${themeId}`); + + // 触发事件 + if (window.EventBus) { + EventBus.emit('theme:changed', { theme: themeId, colors }); + } + }, + + // 切换主题 + setTheme: function(themeId) { + if (!this.themes[themeId]) return; + + this.applyTheme(themeId); + + // 保存到偏好设置 + if (window.ChannelPreferences) { + ChannelPreferences.setTheme(themeId); + } + }, + + // 获取所有主题列表 + getThemes: function() { + return Object.entries(this.themes).map(([id, theme]) => ({ + id, + name: theme.name + })); + }, + + // 获取当前主题名称 + getCurrentThemeName: function() { + return this.themes[this.currentTheme]?.name || '默认蓝'; + } +}; + +// 自动初始化 +setTimeout(() => { + if (window.ChannelTheme) { + ChannelTheme.init(); + } +}, 200); + +console.log('[theme] 已加载'); diff --git a/modules/m-channel/channel-transition.css b/modules/m-channel/channel-transition.css new file mode 100644 index 00000000..d705abd0 --- /dev/null +++ b/modules/m-channel/channel-transition.css @@ -0,0 +1,48 @@ +/* 过渡动画 */ +.channel-content { + transition: opacity 0.3s ease-in-out; +} + +.channel-content.fade-out { + opacity: 0; +} + +.channel-content.fade-in { + opacity: 1; +} + +/* 滑动动画 */ +.slide-left-enter { + transform: translateX(100%); +} + +.slide-left-enter-active { + transform: translateX(0); + transition: transform 0.3s ease-in-out; +} + +.slide-right-enter { + transform: translateX(-100%); +} + +.slide-right-enter-active { + transform: translateX(0); + transition: transform 0.3s ease-in-out; +} + +.slide-exit { + position: absolute; + width: 100%; +} + +.slide-exit-active { + transition: transform 0.3s ease-in-out; +} + +.slide-exit-left { + transform: translateX(-100%); +} + +.slide-exit-right { + transform: translateX(100%); +} diff --git a/modules/m-channel/channel-ultimate.html b/modules/m-channel/channel-ultimate.html new file mode 100644 index 00000000..346968f7 --- /dev/null +++ b/modules/m-channel/channel-ultimate.html @@ -0,0 +1,1401 @@ + + + + + + 光湖频道 · 终极版 + + + + + +
      + +
      + +
      +
      + + +
      +
      +

      频道设置

      + +
      +
      +
      +

      布局模式

      +
      + + + +
      +
      +
      +

      主题色

      +
      + + + + + +
      +
      +
      +

      统计数据

      + +
      +
      +

      其他

      + +
      +
      +
      + + +
      +
      +
      +

      使用统计

      + +
      +
      +
      +
      + + + + + + + + diff --git a/modules/m-channel/error-boundary.js b/modules/m-channel/error-boundary.js new file mode 100644 index 00000000..9ea7037d --- /dev/null +++ b/modules/m-channel/error-boundary.js @@ -0,0 +1,134 @@ +// 错误边界 - 保险丝 +window.ErrorBoundary = { + // 包裹一个可能出错的渲染函数 + wrap: function(renderFn, fallbackComponent, moduleId) { + return function(container) { + try { + return renderFn(container); + } catch (error) { + console.error(`[error-boundary] 模块 ${moduleId} 渲染失败:`, error); + return this.showFallback(container, moduleId, error); + } + }; + }, + + // 显示降级界面 + showFallback: function(container, moduleId, error) { + if (!container) return; + + container.innerHTML = ` +
      +
      ⚠️
      +
      +

      模块 ${moduleId} 加载失败

      +

      ${error.message || '未知错误'}

      +
      +
      + + +
      +
      + `; + + // 记录错误到事件总线 + if (window.EventBus) { + EventBus.emit('module:error', { + module: moduleId, + error: error.message, + time: Date.now() + }); + } + }, + + // 重新加载模块(通过适配器) + reloadModule: function(moduleId) { + console.log(`[error-boundary] 尝试重载模块: ${moduleId}`); + + // 触发全局重载事件 + if (window.EventBus) { + EventBus.emit('module:reload', { module: moduleId }); + } + + // 如果存在适配器,调用适配器的重载方法 + if (window.ModuleAdapter && window.ModuleAdapter.reloadModule) { + const container = document.querySelector(`[data-module="${moduleId}"]`) || + document.getElementById(`module-${moduleId}`); + if (container) { + ModuleAdapter.reloadModule(moduleId, container.id); + } + } else { + // 否则直接刷新页面(简易方案) + location.reload(); + } + }, + + // 隐藏错误(仅当用户点击“忽略”时) + hideError: function(btn) { + const errorDiv = btn.closest('.error-boundary'); + if (errorDiv) { + errorDiv.style.display = 'none'; + } + }, + + // 添加一些基础样式(会在 channel-style.css 中补充) + injectStyles: function() { + const style = document.createElement('style'); + style.textContent = ` + .error-boundary { + padding: 30px; + text-align: center; + background: #fff3f3; + border: 1px solid #ffcdd2; + border-radius: 8px; + margin: 20px 0; + } + .error-icon { + font-size: 48px; + margin-bottom: 15px; + } + .error-message h4 { + color: #d32f2f; + margin: 0 0 10px; + } + .error-message p { + color: #666; + margin: 0 0 20px; + } + .error-actions { + display: flex; + gap: 10px; + justify-content: center; + } + .error-retry { + padding: 8px 16px; + background: #d32f2f; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + .error-dismiss { + padding: 8px 16px; + background: #9e9e9e; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + `; + document.head.appendChild(style); + } +}; + +// 自动注入样式 +setTimeout(() => { + if (window.ErrorBoundary) { + ErrorBoundary.injectStyles(); + } +}, 100); + +console.log('[error-boundary] 已加载'); diff --git a/modules/m-channel/event-bus.js b/modules/m-channel/event-bus.js new file mode 100644 index 00000000..a4b09f2e --- /dev/null +++ b/modules/m-channel/event-bus.js @@ -0,0 +1,37 @@ +// 事件总线(群聊频道) +window.EventBus = { + listeners: {}, + + // 订阅(加入群聊) + on: function(event, callback) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + console.log(`[事件总线] 订阅事件: ${event}`); + }, + + // 取消订阅(退群) + off: function(event, callback) { + if (!this.listeners[event]) return; + if (!callback) { + delete this.listeners[event]; + } else { + this.listeners[event] = this.listeners[event].filter(cb => cb !== callback); + } + console.log(`[事件总线] 取消订阅: ${event}`); + }, + + // 发送消息(@所有人) + emit: function(event, data) { + console.log(`[事件总线] 发送事件: ${event}`, data); + if (!this.listeners[event]) return; + this.listeners[event].forEach(callback => { + try { + callback(data); + } catch (e) { + console.error(`[事件总线] 执行回调出错: ${e}`); + } + }); + } +}; diff --git a/modules/m-channel/index.html b/modules/m-channel/index.html index c0574ad8..59dc02c1 100644 --- a/modules/m-channel/index.html +++ b/modules/m-channel/index.html @@ -3,31 +3,40 @@ - M-CHANNEL · 用户频道引擎 + 光湖频道 · 动态渲染引擎 + + - - +
      + +
      + +
      +
      - -
      -

      ✨ 加载中...

      -
      - - -
      - 当前路由:/ -
      - - + - + + + + + + + + + + + diff --git a/modules/m-channel/index.html.bak b/modules/m-channel/index.html.bak new file mode 100644 index 00000000..e86e9904 --- /dev/null +++ b/modules/m-channel/index.html.bak @@ -0,0 +1,33 @@ + + + + + + 光湖频道 · 动态渲染引擎 + + + + +
      + +
      + +
      +
      + + + + + + + + + + + diff --git a/modules/m-channel/mock-modules/mock-a.html b/modules/m-channel/mock-modules/mock-a.html index 6515b675..2c43abc4 100644 --- a/modules/m-channel/mock-modules/mock-a.html +++ b/modules/m-channel/mock-modules/mock-a.html @@ -1,5 +1,16 @@ -
      -

      📦 模块 A 内容

      -

      这是从 mock-a.html 加载的真实内容~

      - -
      + + + +

      模拟模块A

      + +
      + + + + diff --git a/modules/m-channel/mock-modules/mock-b.html b/modules/m-channel/mock-modules/mock-b.html index 523f26fc..5236978a 100644 --- a/modules/m-channel/mock-modules/mock-b.html +++ b/modules/m-channel/mock-modules/mock-b.html @@ -1,5 +1,17 @@ -
      -

      📦 模块 B 内容

      -

      这是从 mock-b.html 加载的真实内容~

      - -
      + + + +

      模拟模块B

      +
      + + + + diff --git a/modules/m-channel/mock-modules/mock-c.html b/modules/m-channel/mock-modules/mock-c.html deleted file mode 100644 index 8299489d..00000000 --- a/modules/m-channel/mock-modules/mock-c.html +++ /dev/null @@ -1,5 +0,0 @@ -
      -

      📦 模块 C 内容

      -

      这是从 mock-c.html 加载的真实内容~

      - -
      diff --git a/modules/m-channel/mock-modules/mock-d.html b/modules/m-channel/mock-modules/mock-d.html deleted file mode 100644 index 02ccd25d..00000000 --- a/modules/m-channel/mock-modules/mock-d.html +++ /dev/null @@ -1,5 +0,0 @@ -
      -

      📦 模块 D 内容

      -

      这是从 mock-d.html 加载的真实内容~

      - -
      diff --git a/modules/m-channel/module-lifecycle.js b/modules/m-channel/module-lifecycle.js new file mode 100644 index 00000000..d99ea038 --- /dev/null +++ b/modules/m-channel/module-lifecycle.js @@ -0,0 +1,23 @@ +// 模块生命周期管理 +window.ModuleLifecycle = { + activeModules: new Set(), + + // 模块加载时 + onLoad: function(moduleName) { + this.activeModules.add(moduleName); + console.log(`[生命周期] 模块加载: ${moduleName}`); + EventBus.emit('module:loaded', { module: moduleName, time: Date.now() }); + }, + + // 模块卸载时 + onUnload: function(moduleName) { + this.activeModules.delete(moduleName); + console.log(`[生命周期] 模块卸载: ${moduleName}`); + EventBus.emit('module:unloaded', { module: moduleName, time: Date.now() }); + }, + + // 获取当前活跃模块 + getActiveModules: function() { + return Array.from(this.activeModules); + } +}; diff --git a/modules/m-channel/module-loader.js b/modules/m-channel/module-loader.js index 1d8e199c..e812f52f 100644 --- a/modules/m-channel/module-loader.js +++ b/modules/m-channel/module-loader.js @@ -1,65 +1,36 @@ -// 模块加载器(带缓存、超时、错误处理) -const moduleCache = new Map(); // 缓存已加载的模块HTML - -// 加载模块 -async function loadModule(moduleId, containerId = 'module-display-area') { - const container = document.getElementById(containerId); - if (!container) return; - - // 显示加载动画 - container.innerHTML = '
      '; - - // 检查缓存 - if (moduleCache.has(moduleId)) { - console.log(`[cache hit] 模块 ${moduleId} 从缓存加载`); - container.innerHTML = moduleCache.get(moduleId); - return; - } - - // 获取模块路径 - const modulePath = window.moduleRegistry?.[moduleId]; - if (!modulePath) { - container.innerHTML = `
      ❌ 模块 ID "${moduleId}" 未在注册表中找到
      `; - return; - } - - // 设置超时(5秒) - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); - - try { - const response = await fetch(modulePath, { signal: controller.signal }); - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); +// 模块加载器 +window.ModuleLoader = { + loadModule: function(moduleName, containerId) { + console.log(`[loader] 加载模块: ${moduleName}`); + + const module = ModuleRegistry.get(moduleName); + if (!module) { + console.error(`[loader] 模块 ${moduleName} 未注册`); + return; } - - const html = await response.text(); - // 存入缓存 - moduleCache.set(moduleId, html); - console.log(`[cache miss] 模块 ${moduleId} 已加载并缓存`); - - container.innerHTML = html; - } catch (error) { - clearTimeout(timeoutId); - if (error.name === 'AbortError') { - container.innerHTML = `
      ⏰ 加载超时(>5秒),请检查网络或文件是否存在
      `; + + const container = document.getElementById(containerId); + if (!container) { + console.error(`[loader] 容器 ${containerId} 不存在`); + return; + } + + // 如果模块有 init 方法 + if (module.init && typeof module.init === 'function') { + module.init(container); } else { - container.innerHTML = `
      ❌ 加载失败:${error.message}
      `; + container.innerHTML = `
      模块 ${moduleName} 内容
      `; } + + ModuleLifecycle.onLoad(moduleName); + }, + + unloadModule: function(moduleName) { + console.log(`[loader] 卸载模块: ${moduleName}`); + const module = ModuleRegistry.get(moduleName); + if (module && module.destroy && typeof module.destroy === 'function') { + module.destroy(); + } + ModuleLifecycle.onUnload(moduleName); } -} - -// 卸载模块(清空容器) -function unloadModule(containerId = 'module-display-area') { - const container = document.getElementById(containerId); - if (container) { - container.innerHTML = ''; - console.log(`[unload] 模块已卸载 (${containerId})`); - } -} - -// 导出 -window.loadModule = loadModule; -window.unloadModule = unloadModule; +}; diff --git a/modules/m-channel/module-registry.js b/modules/m-channel/module-registry.js index a0a87ff3..c3fb0aef 100644 --- a/modules/m-channel/module-registry.js +++ b/modules/m-channel/module-registry.js @@ -1,7 +1,17 @@ -const moduleRegistry = { - 'mock-a': 'mock-modules/mock-a.html', - 'mock-b': 'mock-modules/mock-b.html', - 'mock-c': 'mock-modules/mock-c.html', - 'mock-d': 'mock-modules/mock-d.html' +// 模块注册表 +window.ModuleRegistry = { + modules: {}, + + register: function(name, module) { + this.modules[name] = module; + console.log(`[registry] 模块 ${name} 已注册`); + }, + + get: function(name) { + return this.modules[name]; + }, + + list: function() { + return Object.keys(this.modules); + } }; -window.moduleRegistry = moduleRegistry; diff --git a/modules/m-channel/views/404.html b/modules/m-channel/views/404.html deleted file mode 100644 index 0ac7792e..00000000 --- a/modules/m-channel/views/404.html +++ /dev/null @@ -1,5 +0,0 @@ -
      -

      😵 404 - 页面走丢啦

      -

      你访问的路径不存在~

      -

      👉 点击返回首页

      -
      diff --git a/modules/m-channel/views/about.html b/modules/m-channel/views/about.html deleted file mode 100644 index cb796452..00000000 --- a/modules/m-channel/views/about.html +++ /dev/null @@ -1,8 +0,0 @@ -
      -

      ℹ️ 关于

      -

      光湖实验室 · HoloLake Era

      -

      这是 M-CHANNEL 频道引擎演示页面。

      -

      开发者:DEV-010 桔子 🍊

      -

      广播编号:BC-M-CHANNEL-001-JZ

      -

      环节:0+1(SPA路由 + 模块动态加载)

      -
      diff --git a/modules/m-channel/views/channel-dashboard.html b/modules/m-channel/views/channel-dashboard.html new file mode 100644 index 00000000..e67784de --- /dev/null +++ b/modules/m-channel/views/channel-dashboard.html @@ -0,0 +1,57 @@ +
      +
      +

      📊 频道数据面板

      +

      模块使用统计 · 性能监控 · 访问趋势

      +
      + +
      + +
      +

      📊 模块访问次数

      +
      +

      暂无数据,多点几个模块再来看

      +
      +
      + + +
      +

      🥧 使用时间占比

      +
      +
      + +
      +
        +
        +
        + + +
        +

        📈 最近7天访问趋势

        +
        + +
        +
        +
        + + +
        +

        ⚡ 模块加载性能

        + + + + + + + + + + + +
        模块访问次数总停留(分钟)平均加载(ms)状态
        +
        + + +
        + +
        +
        diff --git a/modules/m-channel/views/channel-debug.html b/modules/m-channel/views/channel-debug.html new file mode 100644 index 00000000..91f1f882 --- /dev/null +++ b/modules/m-channel/views/channel-debug.html @@ -0,0 +1,93 @@ + + + + + + +
        +

        🐞 调试面板

        + +
        + 当前活跃模块: + 加载中... +
        + +

        事件总线聊天记录

        +
          + +
        + +
        + + +
        + +
        + + +
        + +

        消息会广播给所有订阅的模块

        +
        + + + + diff --git a/modules/m-channel/views/channel-settings.html b/modules/m-channel/views/channel-settings.html new file mode 100644 index 00000000..ac5c5e97 --- /dev/null +++ b/modules/m-channel/views/channel-settings.html @@ -0,0 +1,272 @@ + + + + + + +
        + + +

        ⚙️ 频道设置

        + + +
        +
        📐 布局模式
        +
        + + + +
        +
        + + +
        +
        🎨 主题颜色
        +
        + + + + + +
        +
        + + +
        +
        实时预览
        +
        +
        +
        工单管理
        +
        待处理: 3
        +
        +
        +
        数据统计
        +
        完成率: 75%
        +
        +
        +
        组件库
        +
        24个组件
        +
        +
        +
        + + +
        + +
        +
        + + + + diff --git a/modules/m-channel/views/channel.html b/modules/m-channel/views/channel.html deleted file mode 100644 index 857ba0b9..00000000 --- a/modules/m-channel/views/channel.html +++ /dev/null @@ -1,29 +0,0 @@ -
        -

        📺 我的频道

        -

        点击下面的卡片,动态加载模块内容~

        - - -
        -
        -

        📦 模块 A

        -

        这是模拟模块 A

        -
        -
        -

        📦 模块 B

        -

        这是模拟模块 B

        -
        -
        -

        📦 模块 C

        -

        这是模拟模块 C

        -
        -
        -

        📦 模块 D

        -

        这是模拟模块 D

        -
        -
        - - -
        - -
        -
        diff --git a/modules/m-channel/views/home.html b/modules/m-channel/views/home.html deleted file mode 100644 index de61489a..00000000 --- a/modules/m-channel/views/home.html +++ /dev/null @@ -1,12 +0,0 @@ -
        -

        🏠 首页

        -

        欢迎来到光湖网站~ 这里是首页视图。

        -

        你可以在这里放一些全局介绍或者公告。

        -
        -

        ✨ 当前进度

        -
          -
        • 路由引擎:✅ 已就绪
        • -
        • 模块加载器:⏳ 准备中
        • -
        • 卡片网格:⏳ 稍后出现
        • -
        -
        diff --git a/package.json b/package.json index ff62c756..0ebd5de8 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,22 @@ "esp:process": "node scripts/esp-email-processor.js", "psp:inspect": "node scripts/psp-inspection.js", "repo:map": "node scripts/generate-repo-map.js", + "repo:automation-map": "node scripts/generate-automation-map.js", + "repo:communication-map": "node scripts/generate-communication-map.js", + "repo:system-health": "node scripts/generate-system-health.js", "deploy:agent": "node scripts/bingshuo-deploy-agent.js", "module:protocol": "node scripts/zhuyuan-module-protocol.js", "proxy:start": "node backend-integration/api-proxy.js", "notion:poll": "node scripts/notion-signal-bridge.js poll", "notion:health": "node scripts/notion-signal-bridge.js health", - "notion:summary": "node scripts/generate-session-summary.js" + "notion:summary": "node scripts/generate-session-summary.js", + "core:listen": "node core/broadcast-listener", + "core:queue": "node core/task-queue status", + "core:check": "node core/system-check", + "core:sync": "node core/execution-sync report", + "core:context": "node core/context-loader", + "connector:notion": "node connectors/notion-sync status", + "connector:model": "node connectors/model-router" }, "dependencies": { "axios": "^1.13.6", diff --git a/persona-studio/backend/middleware/auth.js b/persona-studio/backend/middleware/auth.js new file mode 100644 index 00000000..a4f792d8 --- /dev/null +++ b/persona-studio/backend/middleware/auth.js @@ -0,0 +1,15 @@ +/** + * API 鉴权中间件 · BC-集成-005 + * 检查请求头 X-API-Key 是否匹配 + * 只保护 POST 写入接口,GET 查询不需要鉴权 + */ +function apiAuth(req, res, next) { + var apiKey = req.headers["x-api-key"]; + if (!apiKey || apiKey !== process.env.API_SECRET_KEY) { + console.log("❌ 鉴权失败:", req.ip); + return res.status(401).json({ error: "Unauthorized" }); + } + next(); +} + +module.exports = apiAuth; \ No newline at end of file diff --git a/persona-studio/backend/patch-005.js b/persona-studio/backend/patch-005.js new file mode 100644 index 00000000..1b1a2c7b --- /dev/null +++ b/persona-studio/backend/patch-005.js @@ -0,0 +1,185 @@ +/** + * BC-集成-005 代码补丁 + * 自动创建鉴权中间件 + 修改API路由 + 添加新端点 + * DEV-010桔子 · 2026-03-13 + */ +const fs = require('fs'); +const path = require('path'); +const backendDir = __dirname; +console.log('📂 工作目录:', backendDir); +// ═══════════════════════════════════════ +// 1. 创建 middleware/auth.js(API门卫) +// ═══════════════════════════════════════ +const middlewareDir = path.join(backendDir, 'middleware'); +if (!fs.existsSync(middlewareDir)) { + fs.mkdirSync(middlewareDir, { recursive: true }); +} +const authCode = [ + '/**', + ' * API 鉴权中间件 · BC-集成-005', + ' * 检查请求头 X-API-Key 是否匹配', + ' * 只保护 POST 写入接口,GET 查询不需要鉴权', + ' */', + 'function apiAuth(req, res, next) {', + ' var apiKey = req.headers["x-api-key"];', + ' if (!apiKey || apiKey !== process.env.API_SECRET_KEY) {', + ' console.log("❌ 鉴权失败:", req.ip);', + ' return res.status(401).json({ error: "Unauthorized" });', + ' }', + ' next();', + '}', + '', + 'module.exports = apiAuth;' +].join('\n'); +fs.writeFileSync(path.join(middlewareDir, 'auth.js'), authCode); +console.log('✅ [1/3] middleware/auth.js 已创建(API门卫)'); +// ═══════════════════════════════════════ +// 2. 修改 syslog-api.js:给 POST /receive 加门卫 +// ═══════════════════════════════════════ +const syslogPath = path.join(backendDir, 'routes', 'syslog-api.js'); +if (fs.existsSync(syslogPath)) { + var syslogCode = fs.readFileSync(syslogPath, 'utf8'); + if (!syslogCode.includes('apiAuth')) { + syslogCode = "const apiAuth = require('../middleware/auth');\n" + syslogCode; + syslogCode = syslogCode.replace( + /router\.post\(\s*['"\/]receive/, + "router.post('/receive', apiAuth" + ); + fs.writeFileSync(syslogPath, syslogCode); + console.log('✅ [2/3] syslog-api.js 已添加鉴权(POST /receive 需要 X-API-Key)'); + } else { + console.log('ℹ️ [2/3] syslog-api.js 已包含鉴权,跳过'); + } +} else { + console.log('❌ [2/3] syslog-api.js 不存在于: ', syslogPath); + // 尝试列出 routes 目录 + var routesDir = path.join(backendDir, 'routes'); + if (fs.existsSync(routesDir)) { + console.log(' routes 目录文件: ', fs.readdirSync(routesDir)); + } +} +// 3. 修改 dashboard-api.js: 添加两个新端点 +const dashPath = path.join(backendDir, 'routes', 'dashboard-api.js'); +if (fs.existsSync(dashPath)) { + var dashCode = fs.readFileSync(dashPath, 'utf8'); + if (!dashCode.includes('/dev/:id')) { + var newCode = ` +// ══ BC-集成-005 新增: 开发者详情 ══ +// GET /api/dashboard/dev/:id +router.get('/dev/:id', async function(req, res) { + try { + var devId = req.params.id; + var resp = await axios.post( + 'https://api.notion.com/v1/databases/' + process.env.DASHBOARD_DATABASE_ID + '/query', + { + filter: { + property: '孕育者', + rich_text: { contains: devId } + } + }, + { + headers: { + 'Authorization': 'Bearer ' + process.env.NOTION_TOKEN, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json' + } + } + ); + var results = resp.data.results; + if (results.length === 0) { + return res.json({ source: 'notion', data: null, message: '未找到该开发者' }); + } + var devModules = results.map(function(page) { + var p = page.properties; + function gt(prop) { + if (!prop) return ''; + if (prop.title) return prop.title.map(function(t) {return t.plain_text;}).join(''); + if (prop.rich_text) return prop.rich_text.map(function(t) {return t.plain_text;}).join(''); + if (prop.select) return prop.select ? prop.select.name : ''; + if (prop.status) return prop.status ? prop.status.name : ''; + if (prop.checkbox !== undefined) return prop.checkbox; + return ''; + } + return { + name: gt(p['名称']), + developer: gt(p['孕育者']), + status: gt(p['对接状态']), + version: gt(p['当前版本']), + persona: gt(p['宝宝人格体']), + notes: gt(p['备注']), + type: gt(p['类型']) + }; + }); + res.json({ source: 'notion', dev_id: devId, data: devModules }); + } catch (err) { + console.log('❌ 开发者详情查询失败:', err.message); + res.json({ source: 'fallback', data: {} }); + } +}); + +// ══ BC-集成-005 新增:模块列表 ══ +// GET /api/dashboard/modules +router.get('/modules', async function(req, res) { + try { + var resp = await axios.post( + 'https://api.notion.com/v1/databases/' + process.env.MODULE_DATABASE_ID + '/query', + {}, + { + headers: { + 'Authorization': 'Bearer ' + process.env.NOTION_TOKEN, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json' + } + } + ); + var modules = resp.data.results.map(function(page) { + var p = page.properties; + function gt(prop) { + if (!prop) return ''; + if (prop.title) return prop.title.map(function(t){return t.plain_text;}).join(''); + if (prop.rich_text) return prop.rich_text.map(function(t){return t.plain_text;}).join(''); + if (prop.select) return prop.select ? prop.select.name : ''; + if (prop.multi_select) return prop.multi_select.map(function(s){return s.name;}).join(', '); + return ''; + } + return { + module_id: gt(p['模块编号']), + name: gt(p['模块名称']), + status: gt(p['状态']), + developer: gt(p['执行者']), + tags: gt(p['功能标签']), + domain: gt(p['数据域']) + }; + }); + res.json({ source: 'notion', data: modules }); + } catch (err) { + console.log('❌ 模块列表查询失败:', err.message); + res.json({ source: 'fallback', data: {} }); + } +}); +`; + if (dashCode.includes('module.exports')) { + dashCode = dashCode.replace(/module\.exports/, newCode + '\nmodule.exports'); + } else { + dashCode += newCode; + } + fs.writeFileSync(dashPath, dashCode); + console.log('✅ [3/3] dashboard-api.js 已添加 /dev/:id 和 /modules'); + } else { + console.log('ℹ️ [3/3] dashboard-api.js 已包含新端点,跳过'); + } +} else { + console.log('❌ [3/3] dashboard-api.js 不存在于: ', dashPath); + var routesDir2 = path.join(backendDir, 'routes'); + if (fs.existsSync(routesDir2)) { + console.log(' routes 目录文件: ', fs.readdirSync(routesDir2)); + } +} +console.log('\n🎉 BC-集成-005 代码补丁全部完成!'); +console.log('📁 新增文件: middleware/auth.js'); +console.log('📝 修改文件: routes/syslog-api.js, routes/dashboard-api.js'); +console.log('\n➡️ 下一步:'); +console.log(' cd ~/Desktop/guanghulab'); +console.log(' git add persona-studio/backend/'); +console.log(' git commit -m "[BC-集成-005-JZ][DEV-010] API鉴权+开发者详情+模块列表"'); +console.log(' git push origin main'); diff --git a/persona-studio/backend/routes/dashboard-api.js b/persona-studio/backend/routes/dashboard-api.js new file mode 100644 index 00000000..fbcb4574 --- /dev/null +++ b/persona-studio/backend/routes/dashboard-api.js @@ -0,0 +1,98 @@ +// ══ BC-集成-005 新增: 开发者详情 ══ +const express = require('express'); +const router = express.Router(); +const axios = require('axios'); + +// GET /api/dashboard/dev/:id +router.get('/dev/:id', async function(req, res) { + try { + var devId = req.params.id; + var resp = await axios.post( + 'https://api.notion.com/v1/databases/' + process.env.DASHBOARD_DATABASE_ID + '/query', + { + filter: { + property: '孕育者', + rich_text: { contains: devId } + } + }, + { + headers: { + 'Authorization': 'Bearer ' + process.env.NOTION_TOKEN, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json' + } + } + ); + var results = resp.data.results; + if (results.length === 0) { + return res.json({ source: 'notion', data: null, message: '未找到该开发者' }); + } + var devModules = results.map(function(page) { + var p = page.properties; + function gt(prop) { + if (!prop) return ''; + if (prop.title) return prop.title.map(function(t) {return t.plain_text;}).join(''); + if (prop.rich_text) return prop.rich_text.map(function(t) {return t.plain_text;}).join(''); + if (prop.select) return prop.select ? prop.select.name : ''; + if (prop.status) return prop.status ? prop.status.name : ''; + if (prop.checkbox !== undefined) return prop.checkbox; + return ''; + } + return { + name: gt(p['名称']), + developer: gt(p['孕育者']), + status: gt(p['对接状态']), + version: gt(p['当前版本']), + persona: gt(p['宝宝人格体']), + notes: gt(p['备注']), + type: gt(p['类型']) + }; + }); + res.json({ source: 'notion', dev_id: devId, data: devModules }); + } catch (err) { + console.log('❌ 开发者详情查询失败:', err.message); + res.json({ source: 'fallback', data: {} }); + } +}); + +// GET /api/dashboard/modules +router.get('/modules', async function(req, res) { + try { + var resp = await axios.post( + 'https://api.notion.com/v1/databases/' + process.env.MODULE_DATABASE_ID + '/query', + {}, + { + headers: { + 'Authorization': 'Bearer ' + process.env.NOTION_TOKEN, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json' + } + } + ); + var modules = resp.data.results.map(function(page) { + var p = page.properties; + function gt(prop) { + if (!prop) return ''; + if (prop.title) return prop.title.map(function(t){return t.plain_text;}).join(''); + if (prop.rich_text) return prop.rich_text.map(function(t){return t.plain_text;}).join(''); + if (prop.select) return prop.select ? prop.select.name : ''; + if (prop.multi_select) return prop.multi_select.map(function(s){return s.name;}).join(', '); + return ''; + } + return { + module_id: gt(p['模块编号']), + name: gt(p['模块名称']), + status: gt(p['状态']), + developer: gt(p['执行者']), + tags: gt(p['功能标签']), + domain: gt(p['数据域']) + }; + }); + res.json({ source: 'notion', data: modules }); + } catch (err) { + console.log('❌ 模块列表查询失败:', err.message); + res.json({ source: 'fallback', data: {} }); + } +}); + +module.exports = router; diff --git a/persona-studio/backend/server.js b/persona-studio/backend/server.js index a816f5ba..f8c4ea12 100644 --- a/persona-studio/backend/server.js +++ b/persona-studio/backend/server.js @@ -24,7 +24,10 @@ app.use('/api/ps/chat', chatRoutes); app.use('/api/ps/build', buildRoutes); app.use('/api/ps/notify', notifyRoutes); app.use('/api/ps/apikey', apikeyRoutes); -app.use('/api/ps/preview', previewRoutes); + +// ── 健康检查 ── +// ── 健康检查 ──app.use('/api/ps/preview', previewRoutes); +app.use('/api/dashboard', require('./routes/dashboard-api')); // ── 健康检查 ── app.get('/api/ps/health', (_req, res) => { diff --git a/persona-studio/frontend/chat.js b/persona-studio/frontend/chat.js index 2d263e98..05158e9f 100644 --- a/persona-studio/frontend/chat.js +++ b/persona-studio/frontend/chat.js @@ -13,7 +13,7 @@ const SELECTED_MODEL = sessionStorage.getItem('selected_model'); const API_BASE = (function () { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { - return 'http://localhost:3721'; + return 'http://localhost:3002'; } return 'https://guanghulab.com'; })(); @@ -648,33 +648,47 @@ async function confirmBuild() { // 先连接 WebSocket(确保在 build 开始前建立连接,避免丢失进度消息) connectPreviewWebSocket(); - try { - var buildRes = await fetch(API_BASE + '/api/ps/build/start', { - method: 'POST', - headers: authHeaders({ 'Content-Type': 'application/json' }), - body: JSON.stringify({ - dev_id: DEV_ID, - email: email, - contact: contact, - conversation: conversationHistory, - api_base: USER_API_BASE, - api_key: USER_API_KEY, - model: SELECTED_MODEL - }) - }); + // 提交开发任务(含重试机制) + var maxRetries = 2; + var retryCount = 0; + var submitted = false; - if (!buildRes.ok) { - var errMsg = 'HTTP ' + buildRes.status + ' ' + buildRes.statusText; - try { - var errData = await buildRes.json(); - if (errData.message) errMsg = errData.message; - } catch (_e) { /* use status text fallback */ } - appendMessage('system', '⚠️ 铸渊代理启动失败: ' + errMsg); - updatePreviewStatus('error', '启动失败'); + while (retryCount <= maxRetries && !submitted) { + try { + var buildRes = await fetch(API_BASE + '/api/ps/build/start', { + method: 'POST', + headers: authHeaders({ 'Content-Type': 'application/json' }), + body: JSON.stringify({ + dev_id: DEV_ID, + email: email, + contact: contact, + conversation: conversationHistory, + api_base: USER_API_BASE, + api_key: USER_API_KEY, + model: SELECTED_MODEL + }) + }); + + if (!buildRes.ok) { + var errMsg = 'HTTP ' + buildRes.status + ' ' + buildRes.statusText; + try { + var errData = await buildRes.json(); + if (errData.message) errMsg = errData.message; + } catch (_e) { /* use status text fallback */ } + appendMessage('system', '⚠️ 铸渊代理启动失败: ' + errMsg); + updatePreviewStatus('error', '启动失败'); + } + submitted = true; + } catch (_err) { + retryCount++; + if (retryCount <= maxRetries) { + appendMessage('system', '⏳ 连接后端服务中,正在重试(' + retryCount + '/' + maxRetries + ')...'); + await new Promise(function (r) { setTimeout(r, 1500); }); + } else { + appendMessage('system', '⚠️ 任务提交失败:无法连接铸渊后端服务。请检查:\n1. 网络连接是否正常\n2. 后端服务是否已启动\n3. 如使用 GitHub Pages 访问,请确认 guanghulab.com 服务可用'); + updatePreviewStatus('error', '连接失败'); + } } - } catch (_err) { - appendMessage('system', '⚠️ 任务提交失败,请检查网络连接后再试'); - updatePreviewStatus('error', '网络错误'); } } diff --git a/persona-studio/frontend/index.html b/persona-studio/frontend/index.html index 4f7edd35..1835efe8 100644 --- a/persona-studio/frontend/index.html +++ b/persona-studio/frontend/index.html @@ -150,7 +150,7 @@ function getApiBase() { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { - return 'http://localhost:3721'; + return 'http://localhost:3002'; } return 'https://guanghulab.com'; } diff --git a/persona-telemetry/latest-summary.json b/persona-telemetry/latest-summary.json index 4765d8db..182b710a 100644 --- a/persona-telemetry/latest-summary.json +++ b/persona-telemetry/latest-summary.json @@ -1,6 +1,6 @@ { "version": "1.0", - "timestamp": "2026-03-13T00:01:03.012Z", + "timestamp": "2026-03-14T15:14:11.873Z", "sessions": { "total_24h": 0, "active_devs": [], @@ -12,7 +12,7 @@ "style_profile": "通感语言·守护者", "style_drift_score": 0, "memory_depth": "0 selfchecks", - "last_brain_update": "2026-03-11T10:24:00.000Z" + "last_brain_update": "2026-03-13T06:58:00.000Z" }, "dev_progress": { "syslog_submitted": 0, diff --git a/scripts/create-standardized-ticket.js b/scripts/create-standardized-ticket.js new file mode 100644 index 00000000..4bfb5b1f --- /dev/null +++ b/scripts/create-standardized-ticket.js @@ -0,0 +1,197 @@ +// scripts/create-standardized-ticket.js +// 铸渊 · Phase B1 · 标准化工单创建脚本 +// +// 将 SYSLOG 写入 Notion 工单队列时使用标准化格式, +// 让 Notion Agent 能够识别和触发后续处理。 +// +// 环境变量: +// NOTION_TOKEN Notion API token +// NOTION_TICKET_DB_ID 工单队列数据库 ID +// BROADCAST_ID 广播编号(如 BC-M23-001-AW) +// DEVELOPER 开发者信息(如 "DEV-012 Awen") +// SYSLOG_RAW 完整 SYSLOG 原文(JSON 字符串或纯文本) +// SUBMIT_TYPE 提交类型(syslog / question) +// PERSONA_RESULT 人格体处理结果(可选,首次可为空) +// MODULE_VERIFY 模块验证结果 JSON(可选) +// MODULES_UPLOADED 模块是否全部上传(true/false) + +'use strict'; + +const https = require('https'); +const fs = require('fs'); + +const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; +const NOTION_TICKET_DB_ID = process.env.NOTION_TICKET_DB_ID || ''; +const BROADCAST_ID = process.env.BROADCAST_ID || 'UNKNOWN'; +const DEVELOPER = process.env.DEVELOPER || process.env.AUTHOR || 'unknown'; +const SYSLOG_RAW = process.env.SYSLOG_RAW || process.env.SUBMIT_CONTENT || ''; +const SUBMIT_TYPE = process.env.SUBMIT_TYPE || 'syslog'; +const PERSONA_RESULT = process.env.PERSONA_RESULT || ''; +const MODULE_VERIFY = process.env.MODULE_VERIFY || ''; +const MODULES_UPLOADED = process.env.MODULES_UPLOADED || 'false'; + +const NOTION_VERSION = '2022-06-28'; +const NOTION_API_HOSTNAME = 'api.notion.com'; +const NOTION_RICH_TEXT_MAX = 2000; + +// ══════════════════════════════════════════════════════════ +// Notion API 工具 +// ══════════════════════════════════════════════════════════ + +function notionPost(endpoint, body) { + return new Promise(function (resolve, reject) { + var payload = JSON.stringify(body); + var opts = { + hostname: NOTION_API_HOSTNAME, + port: 443, + path: endpoint, + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + 'Content-Length': Buffer.byteLength(payload), + }, + }; + var req = https.request(opts, 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 parse error: ' + data)); + } + }); + }); + req.on('error', reject); + req.write(payload); + req.end(); + }); +} + +function richTextChunks(content) { + var str = String(content || ''); + var chunks = []; + for (var i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) { + chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } }); + } + if (chunks.length === 0) { + chunks.push({ type: 'text', text: { content: '' } }); + } + return chunks; +} + +// ══════════════════════════════════════════════════════════ +// 标准化工单创建 +// ══════════════════════════════════════════════════════════ + +async function createStandardizedTicket() { + if (!NOTION_TOKEN || !NOTION_TICKET_DB_ID) { + console.log('⚠️ Notion credentials not configured, skipping ticket creation'); + process.exit(0); + } + + var now = new Date().toISOString(); + var typeLabel = SUBMIT_TYPE === 'syslog' ? 'SYSLOG处理' : '提问解答'; + var ticketStatus = SUBMIT_TYPE === 'syslog' ? '待处理' : '已完成'; + var verifyStatus = MODULES_UPLOADED === 'true' ? '模块已验证✅' : '模块待验证⚠️'; + var title = '[自动] ' + BROADCAST_ID + ' · ' + typeLabel + ' · ' + verifyStatus; + + // Phase B1 标准化字段 — 工单内容 + var ticketContent = [ + '## 📡 标准化工单 · Phase B1', + '', + '| 字段 | 值 |', + '|------|-----|', + '| 工单类型 | ' + typeLabel + ' |', + '| 状态 | ' + ticketStatus + ' |', + '| 来源 | GitHub Actions |', + '| taskId | ' + BROADCAST_ID + ' |', + '| developer | ' + DEVELOPER + ' |', + '| created_at | ' + now + ' |', + '| retry_count | 0 |', + '| receipt_status | pending |', + '', + ]; + + if (PERSONA_RESULT) { + ticketContent.push('## 铸渊核心大脑处理结果'); + ticketContent.push(''); + ticketContent.push(PERSONA_RESULT.slice(0, 1500)); + ticketContent.push(''); + } + + ticketContent.push('## 模块上传验证'); + ticketContent.push(''); + ticketContent.push('模块全部上传: ' + (MODULES_UPLOADED === 'true' ? '✅ 是' : '❌ 否')); + ticketContent.push(''); + + if (MODULE_VERIFY) { + ticketContent.push('验证详情: ' + MODULE_VERIFY.slice(0, 400)); + ticketContent.push(''); + } + + var contentText = ticketContent.join('\n'); + + // 构建 Notion 页面 + var body = { + parent: { database_id: NOTION_TICKET_DB_ID }, + properties: { + '标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] }, + '操作类型': { select: { name: '其他' } }, + '提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] }, + '状态': { select: { name: ticketStatus } }, + '优先级': { select: { name: 'P1' } }, + }, + children: [ + { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: richTextChunks(contentText), + }, + }, + ], + }; + + // 将 SYSLOG 原文作为代码块附加(如果有) + if (SYSLOG_RAW) { + body.children.push({ + object: 'block', + type: 'code', + code: { + rich_text: richTextChunks(SYSLOG_RAW.slice(0, 8000)), + language: 'json', + }, + }); + } + + console.log('📋 创建标准化工单...'); + console.log(' taskId: ' + BROADCAST_ID); + console.log(' developer: ' + DEVELOPER); + console.log(' type: ' + typeLabel); + console.log(' receipt_status: pending'); + + try { + var result = await notionPost('/v1/pages', body); + console.log('✅ Notion 标准化工单已创建: ' + result.id); + + // 输出工单 ID 到 GITHUB_OUTPUT + var outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + fs.appendFileSync(outputFile, 'ticket_page_id=' + result.id + '\n'); + fs.appendFileSync(outputFile, 'ticket_url=' + (result.url || '') + '\n'); + } + } catch (err) { + console.error('❌ 工单创建失败: ' + err.message); + process.exit(1); + } +} + +createStandardizedTicket(); diff --git a/scripts/generate-automation-map.js b/scripts/generate-automation-map.js new file mode 100644 index 00000000..e3430201 --- /dev/null +++ b/scripts/generate-automation-map.js @@ -0,0 +1,157 @@ +// scripts/generate-automation-map.js +// 自动化地图生成器 · Automation Map Generator +// +// 功能:扫描 .github/workflows/ 和自动化脚本,生成 brain/automation-map.json +// 触发方式: +// - GitHub Actions: daily-maintenance.yml +// - 本地:node scripts/generate-automation-map.js + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.join(__dirname, '..'); +const BRAIN_DIR = path.join(ROOT, 'brain'); +const OUT_PATH = path.join(BRAIN_DIR, 'automation-map.json'); + +const now = new Date(); +const nowISO = now.toISOString(); + +// ── 工具函数 ── + +function safeRead(filePath) { + try { return fs.readFileSync(filePath, 'utf8'); } catch { return ''; } +} + +function listFiles(dirPath, ext) { + try { + return fs.readdirSync(dirPath) + .filter(f => !ext || f.endsWith(ext)) + .filter(f => !f.startsWith('.')); + } catch { return []; } +} + +// ── 解析单个 workflow ── + +function parseWorkflow(file, content) { + const nameMatch = content.match(/^name:\s*(.+)/m); + const name = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, '') : file; + + const triggers = []; + if (/^\s{2,4}push:/m.test(content)) triggers.push('push'); + if (/^\s{2,4}pull_request:/m.test(content)) triggers.push('pull_request'); + if (/^\s{2,4}issues:/m.test(content)) triggers.push('issues'); + if (/^\s{2,4}issue_comment:/m.test(content)) triggers.push('issue_comment'); + if (/workflow_dispatch/m.test(content)) triggers.push('workflow_dispatch'); + if (/workflow_call/m.test(content)) triggers.push('workflow_call'); + + const cronMatch = content.match(/cron:\s*['"]?([^'"#\n]+)/); + const cron = cronMatch ? cronMatch[1].trim() : null; + if (cron) triggers.push('schedule'); + + // Detect scripts used + const scriptRefs = []; + const scriptMatches = content.matchAll(/node\s+scripts\/([^\s'"]+)/g); + for (const m of scriptMatches) { + scriptRefs.push('scripts/' + m[1]); + } + + const result = { + file, + name, + triggers: triggers.length ? triggers : ['unknown'] + }; + + if (cron) result.cron = cron; + if (scriptRefs.length) result.scripts = scriptRefs; + + return result; +} + +// ── 扫描所有 workflow ── + +function scanWorkflows() { + const wfDir = path.join(ROOT, '.github/workflows'); + const files = listFiles(wfDir, '.yml'); + return files.map(f => { + const content = safeRead(path.join(wfDir, f)); + return parseWorkflow(f, content); + }); +} + +// ── 提取 cron jobs ── + +function extractCronJobs(workflows) { + return workflows + .filter(w => w.cron) + .map(w => ({ + cron: w.cron, + workflow: w.file, + name: w.name + })) + .sort((a, b) => a.cron.localeCompare(b.cron)); +} + +// ── 扫描部署脚本 ── + +function scanDeployScripts(workflows) { + return workflows + .filter(w => /deploy/i.test(w.file) || /deploy/i.test(w.name)) + .map(w => ({ + workflow: w.file, + name: w.name, + triggers: w.triggers + })); +} + +// ── 扫描同步脚本 ── + +function scanSyncScripts() { + const scriptDir = path.join(ROOT, 'scripts'); + const files = listFiles(scriptDir, '.js'); + const syncScripts = []; + + for (const file of files) { + const content = safeRead(path.join(scriptDir, file)); + if (/sync|bridge|push-broadcast|receive|process-broadcast/i.test(file) || + /notion.*api|@notionhq/i.test(content)) { + syncScripts.push({ + script: 'scripts/' + file, + type: /sync/i.test(file) ? 'sync' : + /bridge/i.test(file) ? 'bridge' : + /broadcast/i.test(file) ? 'broadcast' : 'other' + }); + } + } + + return syncScripts; +} + +// ── 主生成 ── + +function generate() { + if (!fs.existsSync(BRAIN_DIR)) fs.mkdirSync(BRAIN_DIR, { recursive: true }); + + const workflows = scanWorkflows(); + const cronJobs = extractCronJobs(workflows); + const deployScripts = scanDeployScripts(workflows); + const syncScripts = scanSyncScripts(); + + const map = { + version: '4.0', + generated_at: nowISO, + generated_by: 'scripts/generate-automation-map.js', + description: '数字地球系统自动化地图 · 所有工作流与自动化脚本', + workflows, + cron_jobs: cronJobs, + deploy_scripts: deployScripts, + data_sync_scripts: syncScripts, + status: 'stable' + }; + + fs.writeFileSync(OUT_PATH, JSON.stringify(map, null, 2)); + console.log(`✅ automation-map.json 已生成 · ${workflows.length} 个工作流 · ${cronJobs.length} 个定时任务`); +} + +generate(); diff --git a/scripts/generate-communication-map.js b/scripts/generate-communication-map.js new file mode 100644 index 00000000..f9003cab --- /dev/null +++ b/scripts/generate-communication-map.js @@ -0,0 +1,181 @@ +// scripts/generate-communication-map.js +// 通信地图生成器 · Communication Map Generator +// +// 功能:扫描仓库中的 API 端点、webhook、同步脚本,生成 brain/communication-map.json +// 触发方式: +// - GitHub Actions: daily-maintenance.yml +// - 本地:node scripts/generate-communication-map.js + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.join(__dirname, '..'); +const BRAIN_DIR = path.join(ROOT, 'brain'); +const OUT_PATH = path.join(BRAIN_DIR, 'communication-map.json'); + +const now = new Date(); + +// ── 工具函数 ── + +function safeRead(filePath) { + try { return fs.readFileSync(filePath, 'utf8'); } catch { return ''; } +} + +function listFiles(dirPath, ext) { + try { + return fs.readdirSync(dirPath) + .filter(f => !ext || f.endsWith(ext)) + .filter(f => !f.startsWith('.')); + } catch { return []; } +} + +// ── 扫描 HLI 路由 ── + +function scanHLIRoutes() { + const routeDir = path.join(ROOT, 'src/routes/hli'); + const routes = []; + + function scanDir(dir, prefix) { + const files = listFiles(dir, '.js'); + for (const file of files) { + const content = safeRead(path.join(dir, file)); + const matches = content.matchAll(/router\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/g); + for (const m of matches) { + routes.push({ + method: m[1].toUpperCase(), + path: prefix + m[2], + file: path.relative(ROOT, path.join(dir, file)) + }); + } + } + + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const e of entries) { + if (e.isDirectory()) { + scanDir(path.join(dir, e.name), prefix + '/' + e.name); + } + } + } catch { /* ignore */ } + } + + scanDir(routeDir, '/hli'); + return routes; +} + +// ── 扫描 backend 路由 ── + +function scanBackendRoutes() { + const serverFile = path.join(ROOT, 'backend/server.js'); + const content = safeRead(serverFile); + const routes = []; + const matches = content.matchAll(/app\.use\s*\(\s*['"]([^'"]+)['"]/g); + for (const m of matches) { + routes.push({ method: '*', path: m[1], file: 'backend/server.js' }); + } + // Check for webhook + const webhooks = content.matchAll(/app\.(post|get)\s*\(\s*['"]([^'"]+)['"]/g); + for (const m of webhooks) { + routes.push({ method: m[1].toUpperCase(), path: m[2], file: 'backend/server.js' }); + } + return routes; +} + +// ── 扫描 Notion 同步脚本 ── + +function scanSyncScripts() { + const scriptDir = path.join(ROOT, 'scripts'); + const files = listFiles(scriptDir, '.js'); + const syncScripts = []; + + for (const file of files) { + const content = safeRead(path.join(scriptDir, file)); + const isNotion = /NOTION|@notionhq|notion/i.test(content); + const isSync = /sync|bridge|push/i.test(file); + const isHTTP = /fetch\(|axios|https?\.request/i.test(content); + + if (isNotion || isSync) { + let direction = 'unknown'; + if (/bridge/i.test(file)) direction = 'bidirectional'; + else if (/push|sync-login|broadcast/i.test(file)) direction = 'github_to_external'; + else if (/receive|poll|heartbeat/i.test(file)) direction = 'external_to_github'; + else if (isNotion) direction = 'github_to_notion'; + + syncScripts.push({ + script: 'scripts/' + file, + direction, + uses_notion_api: isNotion, + uses_http: isHTTP + }); + } + } + + return syncScripts; +} + +// ── 扫描触发路径 ── + +function scanTriggerPaths() { + const wfDir = path.join(ROOT, '.github/workflows'); + const files = listFiles(wfDir, '.yml'); + const githubToNotion = []; + const notionToGithub = []; + + for (const file of files) { + const content = safeRead(path.join(wfDir, file)); + const nameMatch = content.match(/^name:\s*(.+)/m); + const name = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, '') : file; + + if (/notion.*bridge|bridge.*notion|syslog.*notion|changes.*notion|session.*summary/i.test(name) || + /notion-bridge|receive-syslog|push-broadcast|sync-login/i.test(content)) { + githubToNotion.push({ workflow: file, name }); + } + if (/notion.*poll|heartbeat|persona.*invoke|process.*notion|notion.*order/i.test(name) || + /notion-signal-bridge|notion-heartbeat|invoke-persona/i.test(content)) { + notionToGithub.push({ workflow: file, name }); + } + } + + return { github_to_notion: githubToNotion, notion_to_github: notionToGithub }; +} + +// ── 主生成 ── + +function generate() { + if (!fs.existsSync(BRAIN_DIR)) fs.mkdirSync(BRAIN_DIR, { recursive: true }); + + const hliRoutes = scanHLIRoutes(); + const backendRoutes = scanBackendRoutes(); + const syncScripts = scanSyncScripts(); + const triggerPaths = scanTriggerPaths(); + + // Read existing for merge + let existing = {}; + try { existing = JSON.parse(fs.readFileSync(OUT_PATH, 'utf8')); } catch { /* new file */ } + + const map = { + version: existing.version || '4.0', + generated_at: now.toISOString(), + generated_by: 'scripts/generate-communication-map.js', + description: '数字地球系统通信地图 · 所有通信入口与数据流', + api_endpoints: existing.api_endpoints || {}, + webhook_endpoints: existing.webhook_endpoints || [], + trigger_paths: triggerPaths, + data_sync_scripts: syncScripts, + automation_flows: existing.automation_flows || [], + scan_results: { + hli_routes_found: hliRoutes.length, + backend_routes_found: backendRoutes.length, + sync_scripts_found: syncScripts.length, + hli_routes: hliRoutes, + backend_routes: backendRoutes + } + }; + + fs.writeFileSync(OUT_PATH, JSON.stringify(map, null, 2)); + console.log(`✅ communication-map.json 已生成 · ${hliRoutes.length} HLI 路由 · ${syncScripts.length} 同步脚本`); +} + +generate(); diff --git a/scripts/generate-system-health.js b/scripts/generate-system-health.js new file mode 100644 index 00000000..0044ece4 --- /dev/null +++ b/scripts/generate-system-health.js @@ -0,0 +1,98 @@ +// scripts/generate-system-health.js +// 系统健康状态生成器 · System Health Generator +// +// 功能:巡检 brain/ 目录完整性和系统状态,更新 brain/system-health.json +// 触发方式: +// - GitHub Actions: daily-maintenance.yml +// - 本地:node scripts/generate-system-health.js + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.join(__dirname, '..'); +const BRAIN_DIR = path.join(ROOT, 'brain'); +const OUT_PATH = path.join(BRAIN_DIR, 'system-health.json'); + +const BEIJING_OFFSET_MS = 8 * 3600 * 1000; + +const now = new Date(); +const nowISO = now.toISOString(); +const nowBJ = new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString() + .replace('T', ' ').slice(0, 19) + '+08:00'; + +// ── 巡检 brain/ 目录完整性 ── + +function checkBrainIntegrity() { + const required = [ + 'master-brain.md', + 'read-order.md', + 'repo-map.json', + 'automation-map.json', + 'communication-map.json', + 'id-map.json', + 'system-health.json' + ]; + + const missing = []; + const present = []; + + for (const file of required) { + const filePath = path.join(BRAIN_DIR, file); + if (fs.existsSync(filePath)) { + present.push(file); + } else { + missing.push(file); + } + } + + return { + complete: missing.length === 0, + total: required.length, + present: present.length, + missing + }; +} + +// ── 检查 workflow 目录 ── + +function checkWorkflows() { + const wfDir = path.join(ROOT, '.github/workflows'); + try { + const files = fs.readdirSync(wfDir).filter(f => f.endsWith('.yml')); + return { count: files.length, status: files.length > 0 ? 'stable' : 'warning' }; + } catch { + return { count: 0, status: 'error' }; + } +} + +// ── 主生成 ── + +function generate() { + if (!fs.existsSync(BRAIN_DIR)) fs.mkdirSync(BRAIN_DIR, { recursive: true }); + + const brainCheck = checkBrainIntegrity(); + const wfCheck = checkWorkflows(); + + const health = { + version: '4.0', + last_check: nowBJ, + communication: 'synced', + automation: wfCheck.status, + maintenance_agent: 'active', + system_health: brainCheck.complete ? 'normal' : 'warning', + brain_integrity: brainCheck, + workflow_count: wfCheck.count, + checked_by: 'scripts/generate-system-health.js' + }; + + fs.writeFileSync(OUT_PATH, JSON.stringify(health, null, 2)); + console.log(`✅ system-health.json 已更新 · brain完整性: ${brainCheck.present}/${brainCheck.total} · 工作流: ${wfCheck.count}`); + + if (brainCheck.missing.length > 0) { + console.log(`⚠️ 缺失文件: ${brainCheck.missing.join(', ')}`); + } +} + +generate(); diff --git a/scripts/intent-router.js b/scripts/intent-router.js new file mode 100644 index 00000000..d78c7fac --- /dev/null +++ b/scripts/intent-router.js @@ -0,0 +1,237 @@ +/** + * ━━━ 意图路由模块 ━━━ + * 文件位置:scripts/intent-router.js + * + * 解析用户在 Issue 评论中 @铸渊 的意图,路由到对应处理逻辑。 + * 用于替换原来的「无脑倒全部数据」行为。 + */ + +const https = require('https'); +const fs = require('fs'); + +/** + * 解析用户在 Issue 评论中 @铸渊 的意图 + * @param {string} commentBody - 评论原文 + * @returns {object} 意图对象 + */ +function parseIntent(commentBody) { + // 去掉 @铸渊 前缀,提取核心内容 + const content = commentBody.replace(/@铸渊[,,]?\s*/g, '').trim(); + + // 意图1:查询某个 Issue 的闭环状态 + // 匹配:「查询#92闭环状态」「#92进度」「Issue 92 状态」「闭环状态」等 + const issueMatch = content.match(/#(\d+)/); + if (issueMatch && /闭环|状态|进度|处理/.test(content)) { + return { + type: 'check_pipeline_status', + issueNumber: parseInt(issueMatch[1]), + raw: content + }; + } + + // 意图2:查询某个开发者的状态 + // 匹配:「DEV-004状态」「之之进度」「查询肥猫」 + const devMatch = content.match(/DEV-(\d+)|页页|肥猫|燕樊|之之|小草莓|花尔|桔子|匆匆那年|Awen|小兴|时雨/); + if (devMatch && /状态|进度|查询/.test(content)) { + return { + type: 'check_dev_status', + devId: devMatch[0], + raw: content + }; + } + + // 意图3:团队总览(显式请求) + // 匹配:「团队状态」「全部进度」「总览」 + if (/团队|全部|总览|所有人/.test(content)) { + return { + type: 'team_overview', + raw: content + }; + } + + // 意图4:未识别 → 返回 unknown + return { + type: 'unknown', + raw: content + }; +} + +/** + * 查询 Issue 闭环管道状态(通过评论历史) + * @param {number} issueNumber - 要查询的 Issue 编号 + * @returns {string} 格式化的状态回复 + */ +async function checkPipelineStatus(issueNumber) { + const [owner, repo] = (process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab').split('/'); + + // 获取该 Issue 的评论历史 + let comments; + try { + comments = await githubAPI('GET', + `/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100`); + } catch (e) { + return `📋 **Issue #${issueNumber} 闭环状态查询**\n\n` + + `查询失败: ${e.message}\n` + + `请确认 Issue 编号是否正确。`; + } + + if (!Array.isArray(comments) || comments.length === 0) { + return `📋 **Issue #${issueNumber} 闭环状态查询**\n\n` + + `未找到该 Issue 的评论记录。\n` + + `可能原因:该 Issue 不存在或没有任何评论。`; + } + + // 筛选铸渊的进度评论(包含「步骤 X/8」的) + const progressComments = comments.filter(c => + c.body && c.body.includes('步骤') && c.body.includes('/8') && c.body.includes('铸渊') + ); + + // 检查是否有闭环完成汇总 + const completeComment = comments.find(c => + c.body && (c.body.includes('闭环处理完成') || c.body.includes('闭环处理异常')) + ); + + if (progressComments.length === 0 && !completeComment) { + // 检查是否有旧版静态进度表格 + const staticTable = comments.find(c => + c.body && c.body.includes('管道运行') && c.body.includes('等待中') + ); + + if (staticTable) { + return `📋 **Issue #${issueNumber} 闭环状态查询**\n\n` + + `⏳ 管道已启动但后续步骤未上报进度。\n` + + `可能原因:管道处理脚本未触发,或中间步骤出错。\n\n` + + `> 建议检查 Actions 运行日志。`; + } + + return `📋 **Issue #${issueNumber} 闭环状态查询**\n\n` + + `未找到该 Issue 的闭环进度记录。\n` + + `可能原因:闭环尚未启动,或该 Issue 不是 SYSLOG 提交。`; + } + + // 有进度评论:提取最新状态 + if (completeComment) { + const isSuccess = completeComment.body.includes('闭环处理完成'); + const hasFailed = comments.some(c => c.body && c.body.includes('❌')); + + return `📋 **Issue #${issueNumber} 闭环状态**\n\n` + + `${isSuccess && !hasFailed ? '✅ 闭环已完成' : '⚠️ 闭环有步骤异常'}\n` + + `进度评论数: ${progressComments.length} 条\n` + + `最后更新: ${completeComment.created_at}`; + } + + // 进行中 + const lastProgress = progressComments[progressComments.length - 1]; + const stepMatch = lastProgress.body.match(/步骤 (\d+)\/8/); + const lastStep = stepMatch ? parseInt(stepMatch[1]) : 0; + const hasFailed = progressComments.some(c => c.body.includes('❌')); + + let statusLine; + if (hasFailed) { + statusLine = '❌ 闭环有步骤失败(详见该 Issue 下各步骤评论)'; + } else { + statusLine = `⏳ 闭环进行中 · 已完成到步骤 ${lastStep}/8`; + } + + return `📋 **Issue #${issueNumber} 闭环状态**\n\n` + + `${statusLine}\n` + + `进度评论数: ${progressComments.length} 条\n` + + `最后更新: ${lastProgress.created_at}`; +} + +/** + * 查询指定开发者的状态 + * @param {string} devIdOrName - DEV-XXX 或开发者昵称 + * @param {object} devStatus - dev-status.json 数据 + * @returns {string} 格式化的状态回复 + */ +function checkDevStatus(devIdOrName, devStatus) { + const team = devStatus.team_status || devStatus.team || []; + const dev = team.find(d => + d.dev_id === devIdOrName || + d.name === devIdOrName || + (d.dev_id && d.dev_id.toUpperCase() === devIdOrName.toUpperCase()) + ); + + if (!dev) { + return `未找到开发者 ${devIdOrName} 的记录。`; + } + + const modules = dev.modules + ? dev.modules.join('、') + : (dev.module || '未知'); + + return `📊 **${dev.dev_id} ${dev.name} · 当前状态**\n\n` + + `- 📌 模块: ${modules}\n` + + `- 📊 状态: ${dev.status}\n` + + `- ⏳ 等待中: ${dev.waiting_for || dev.waiting || '无'}\n` + + `- 👉 下一步: ${dev.next_step || dev.current || '无'}\n` + + `- 🔥 连胜: ${dev.streak || 0}`; +} + +/** + * 团队总览 + * @param {object} devStatus - dev-status.json 数据 + * @returns {string} 格式化的总览 + */ +function formatTeamOverview(devStatus) { + const team = devStatus.team_status || devStatus.team || []; + let reply = `## ⚒️ 铸渊回复 · 团队进度总览\n\n`; + team.forEach(dev => { + reply += `**${dev.dev_id} ${dev.name}** · ${dev.status}\n`; + }); + reply += `\n---\n*最后同步:${devStatus.last_synced || devStatus.last_sync}*`; + return reply; +} + +/** + * 生成「未识别意图」的帮助提示 + * @returns {string} + */ +function unknownIntentHelp() { + return `🤔 我没理解你的问题。你可以试试:\n\n` + + `- \`@铸渊 查询#92闭环状态\` — 查某个 SYSLOG 提交的处理进度\n` + + `- \`@铸渊 DEV-004状态\` — 查某个开发者的进度\n` + + `- \`@铸渊 团队总览\` — 查所有人的状态`; +} + +// ━━━ GitHub API 工具 ━━━ +function githubAPI(method, path, data) { + return new Promise((resolve, reject) => { + const body = data ? JSON.stringify(data) : null; + const req = https.request({ + hostname: 'api.github.com', + path: path, + method: method, + headers: { + 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'Zhuyuan-Intent-Router', + 'Content-Type': 'application/json', + ...(body ? { 'Content-Length': Buffer.byteLength(body) } : {}) + } + }, (res) => { + let responseBody = ''; + res.on('data', chunk => { responseBody += chunk; }); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { resolve(JSON.parse(responseBody)); } + catch (e) { console.error('⚠️ JSON parse error:', e.message); resolve(responseBody); } + } else { + reject(new Error(`GitHub API ${res.statusCode}: ${responseBody.slice(0, 200)}`)); + } + }); + }); + req.on('error', reject); + if (body) req.write(body); + req.end(); + }); +} + +module.exports = { + parseIntent, + checkPipelineStatus, + checkDevStatus, + formatTeamOverview, + unknownIntentHelp +}; diff --git a/scripts/invoke-persona.js b/scripts/invoke-persona.js new file mode 100644 index 00000000..c88cbd5c --- /dev/null +++ b/scripts/invoke-persona.js @@ -0,0 +1,360 @@ +// scripts/invoke-persona.js +// 铸渊 · Phase B2 · 人格体唤醒调用脚本 +// +// Notion Agent 调用此脚本(通过 workflow_dispatch), +// 脚本读取 Notion 工单内容 → 唤醒人格体 → 处理结果写回 Notion。 +// +// 环境变量: +// WORK_ORDER_ID Notion 工单页面 ID +// TASK_ID 广播编号(如 BC-M23-001-AW) +// DEVELOPER 开发者信息(如 "DEV-012 Awen") +// SYSLOG_RAW SYSLOG JSON 原文 +// ACTION 动作类型(process_syslog / retry) +// LLM_API_KEY 第三方 LLM 平台密钥 +// LLM_BASE_URL 第三方 LLM 平台 API 地址 +// NOTION_TOKEN Notion API token +// CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID +// PORTRAIT_DB_ID 开发者动态画像库 ID +// FINGERPRINT_DB_ID 模块指纹注册表 ID +// INVOKE_API_KEY 鉴权密钥(用于 API 调用验证) + +'use strict'; + +const https = require('https'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const childProcess = require('child_process'); + +// ══════════════════════════════════════════════════════════ +// 配置 +// ══════════════════════════════════════════════════════════ + +var WORK_ORDER_ID = process.env.WORK_ORDER_ID || ''; +var TASK_ID = process.env.TASK_ID || process.env.BROADCAST_ID || 'UNKNOWN'; +var DEVELOPER = process.env.DEVELOPER || 'unknown'; +var SYSLOG_RAW = process.env.SYSLOG_RAW || ''; +var ACTION = process.env.ACTION || 'process_syslog'; + +var LLM_API_KEY = process.env.LLM_API_KEY || ''; +var LLM_BASE_URL = (process.env.LLM_BASE_URL || '').replace(/\/+$/, ''); +var NOTION_TOKEN = process.env.NOTION_TOKEN || ''; +var CORE_BRAIN_PAGE_ID = process.env.CORE_BRAIN_PAGE_ID || ''; +var PORTRAIT_DB_ID = process.env.PORTRAIT_DB_ID || ''; +var FINGERPRINT_DB_ID = process.env.FINGERPRINT_DB_ID || ''; + +var NOTION_VERSION = '2022-06-28'; +var NOTION_API_HOSTNAME = 'api.notion.com'; + +// ══════════════════════════════════════════════════════════ +// HTTP 工具 +// ══════════════════════════════════════════════════════════ + +function httpRequest(url, options, body) { + return new Promise(function (resolve, reject) { + var parsed = new URL(url); + var isHttps = parsed.protocol === 'https:'; + var mod = isHttps ? https : http; + + var opts = { + hostname: parsed.hostname, + port: parsed.port || (isHttps ? 443 : 80), + path: parsed.pathname + parsed.search, + method: options.method || 'GET', + headers: options.headers || {}, + timeout: options.timeout || 120000, + }; + + var req = mod.request(opts, function (res) { + var data = ''; + res.on('data', function (chunk) { data += chunk; }); + res.on('end', function () { + resolve({ status: res.statusCode, body: data }); + }); + }); + + req.on('error', reject); + req.on('timeout', function () { req.destroy(); reject(new Error('Request timeout')); }); + + if (body) { + req.write(typeof body === 'string' ? body : JSON.stringify(body)); + } + req.end(); + }); +} + +function notionRequest(method, endpoint, body) { + var url = 'https://' + NOTION_API_HOSTNAME + endpoint; + var headers = { + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }; + return httpRequest(url, { method: method, headers: headers }, body ? JSON.stringify(body) : null) + .then(function (res) { + var parsed = JSON.parse(res.body); + if (res.status >= 200 && res.status < 300) return parsed; + throw new Error('Notion API ' + res.status + ': ' + (parsed.message || res.body)); + }); +} + +// ══════════════════════════════════════════════════════════ +// Step 1: 读取工单内容(从 Notion) +// ══════════════════════════════════════════════════════════ + +async function readWorkOrder() { + if (!WORK_ORDER_ID) { + console.log('ℹ️ No WORK_ORDER_ID, using SYSLOG_RAW directly'); + return { syslog_raw: SYSLOG_RAW, taskId: TASK_ID, developer: DEVELOPER }; + } + + console.log('📖 读取 Notion 工单: ' + WORK_ORDER_ID); + try { + var page = await notionRequest('GET', '/v1/pages/' + WORK_ORDER_ID); + var props = page.properties || {}; + + // 提取工单字段 + var taskId = TASK_ID; + var developer = DEVELOPER; + + // 尝试从工单属性中读取 + if (props['广播编号'] && props['广播编号'].rich_text) { + var bcText = props['广播编号'].rich_text.map(function (t) { return t.plain_text || ''; }).join(''); + if (bcText) taskId = bcText; + } + if (props['开发者编号'] && props['开发者编号'].rich_text) { + var devText = props['开发者编号'].rich_text.map(function (t) { return t.plain_text || ''; }).join(''); + if (devText) developer = devText; + } + + // 读取页面内容(子块)来获取 SYSLOG 原文 + var blocks = await notionRequest('GET', '/v1/blocks/' + WORK_ORDER_ID + '/children?page_size=20'); + var syslogRaw = SYSLOG_RAW; + + if (blocks.results) { + blocks.results.forEach(function (block) { + if (block.type === 'code' && block.code && block.code.rich_text) { + var codeText = block.code.rich_text.map(function (t) { return t.plain_text || ''; }).join(''); + if (codeText.length > syslogRaw.length) { + syslogRaw = codeText; + } + } + }); + } + + console.log(' → taskId: ' + taskId); + console.log(' → developer: ' + developer); + console.log(' → syslog_raw length: ' + syslogRaw.length); + + return { syslog_raw: syslogRaw, taskId: taskId, developer: developer }; + } catch (err) { + console.log('⚠️ 工单读取失败: ' + err.message + ',使用环境变量'); + return { syslog_raw: SYSLOG_RAW, taskId: TASK_ID, developer: DEVELOPER }; + } +} + +// ══════════════════════════════════════════════════════════ +// Step 2: 唤醒人格体(复用 wake-persona.js 的逻辑) +// ══════════════════════════════════════════════════════════ + +async function invokePersona(workOrder) { + console.log('🧠 唤醒人格体处理 SYSLOG...'); + + // 设置环境变量供 wake-persona.js 读取 + process.env.BROADCAST_ID = workOrder.taskId; + process.env.SUBMIT_TYPE = 'syslog'; + process.env.SUBMIT_CONTENT = workOrder.syslog_raw; + process.env.AUTHOR = workOrder.developer; + + // 执行 wake-persona.js(fork 子进程) + var wakeScript = path.join(__dirname, 'wake-persona.js'); + + return new Promise(function (resolve, reject) { + var child = childProcess.fork(wakeScript, [], { + env: Object.assign({}, process.env, { + BROADCAST_ID: workOrder.taskId, + SUBMIT_TYPE: 'syslog', + SUBMIT_CONTENT: workOrder.syslog_raw, + AUTHOR: workOrder.developer, + LLM_API_KEY: LLM_API_KEY, + LLM_BASE_URL: LLM_BASE_URL, + NOTION_TOKEN: NOTION_TOKEN, + CORE_BRAIN_PAGE_ID: CORE_BRAIN_PAGE_ID, + PORTRAIT_DB_ID: PORTRAIT_DB_ID, + FINGERPRINT_DB_ID: FINGERPRINT_DB_ID, + }), + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }); + + var stdout = ''; + var stderr = ''; + child.stdout.on('data', function (d) { stdout += d; process.stdout.write(d); }); + child.stderr.on('data', function (d) { stderr += d; process.stderr.write(d); }); + + child.on('exit', function (code) { + if (code === 0) { + // 从 GITHUB_OUTPUT 文件读取 result(如果存在) + var outputFile = process.env.GITHUB_OUTPUT; + var result = ''; + if (outputFile && fs.existsSync(outputFile)) { + var outputContent = fs.readFileSync(outputFile, 'utf8'); + var resultMatch = outputContent.match(/result<= 200 && res.status < 300) { + var tickets = parsed.results || []; + console.log(' → 找到 ' + tickets.length + ' 个待处理工单'); + return tickets; + } else { + console.log('⚠️ 查询失败: ' + (parsed.message || res.body)); + return []; + } + } catch (err) { + console.log('⚠️ 查询异常: ' + err.message); + return []; + } +} + +// ══════════════════════════════════════════════════════════ +// Step 2: 检测超时工单 +// ══════════════════════════════════════════════════════════ + +function filterTimedOutTickets(tickets) { + var now = Date.now(); + var timedOut = []; + + tickets.forEach(function (ticket) { + var createdTime = new Date(ticket.created_time).getTime(); + var age = now - createdTime; + + // 只处理来自 "铸渊Agent·自动管道" 的 SYSLOG 工单 + var props = ticket.properties || {}; + var submitter = ''; + if (props['提交者'] && props['提交者'].rich_text) { + submitter = props['提交者'].rich_text.map(function (t) { return t.plain_text || ''; }).join(''); + } + + // 只对自动管道创建的工单做心跳检测 + if (!submitter.includes('铸渊Agent') && !submitter.includes('自动管道')) { + return; + } + + // 超过 5 分钟未完成 + if (age > HEARTBEAT_TIMEOUT_MS) { + var title = ''; + if (props['标题'] && props['标题'].title) { + title = props['标题'].title.map(function (t) { return t.plain_text || ''; }).join(''); + } + + timedOut.push({ + id: ticket.id, + title: title, + createdTime: ticket.created_time, + ageMinutes: Math.round(age / 60000), + }); + } + }); + + console.log(' → ' + timedOut.length + ' 个工单超时(> 5 分钟)'); + return timedOut; +} + +// ══════════════════════════════════════════════════════════ +// Step 3: 触发重试(通过 workflow_dispatch) +// ══════════════════════════════════════════════════════════ + +async function triggerRetry(ticket) { + if (!GITHUB_TOKEN) { + console.log('⚠️ 缺少 GITHUB_TOKEN,无法触发重试'); + return false; + } + + console.log('🔄 重试唤醒: ' + ticket.title + ' (超时 ' + ticket.ageMinutes + ' 分钟)'); + + // 从工单标题提取 broadcast_id + var bcMatch = (ticket.title || '').match(/BC-[A-Z0-9]+-\d+-[A-Z]+/i); + var broadcastId = bcMatch ? bcMatch[0] : 'UNKNOWN'; + + try { + var res = await httpsPost('api.github.com', + '/repos/' + REPO_OWNER + '/' + REPO_NAME + '/actions/workflows/persona-invoke.yml/dispatches', + { + ref: 'main', + inputs: { + work_order_id: ticket.id, + task_id: broadcastId, + developer: 'heartbeat-retry', + action: 'retry', + }, + }, + { + 'Authorization': 'Bearer ' + GITHUB_TOKEN, + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'ZhuyuanHeartbeat/1.0', + 'X-GitHub-Api-Version': '2022-11-28', + } + ); + + if (res.status === 204 || res.status === 200) { + console.log(' → ✅ workflow_dispatch 触发成功'); + return true; + } else { + console.log(' → ⚠️ 触发失败: HTTP ' + res.status + ' ' + res.body); + return false; + } + } catch (err) { + console.log(' → ⚠️ 触发异常: ' + err.message); + return false; + } +} + +// ══════════════════════════════════════════════════════════ +// Step 4: 标记工单异常 +// ══════════════════════════════════════════════════════════ + +async function markTicketError(ticketId) { + try { + await httpsPatch('api.notion.com', '/v1/pages/' + ticketId, { + properties: { + '状态': { select: { name: '⚠️ 异常·等人工介入' } }, + }, + }, notionHeaders); + console.log(' → 工单已标记为 ⚠️ 异常·等人工介入'); + } catch (err) { + console.log(' → 标记失败: ' + err.message); + } +} + +// ══════════════════════════════════════════════════════════ +// Step 5: 发送告警邮件 +// ══════════════════════════════════════════════════════════ + +async function sendAlertEmail(timedOutTickets) { + if (!SMTP_USER || !SMTP_PASS) { + console.log('⚠️ SMTP 未配置,跳过告警邮件'); + return; + } + + try { + var nodemailer = require('nodemailer'); + + var ticketList = timedOutTickets.map(function (t) { + return '- ' + t.title + ' (超时 ' + t.ageMinutes + ' 分钟, 重试已耗尽)'; + }).join('\n'); + + var transporter = nodemailer.createTransport({ + host: 'smtp.qq.com', + port: 465, + secure: true, + auth: { user: SMTP_USER, pass: SMTP_PASS }, + }); + + await transporter.sendMail({ + from: '"光湖系统·告警" <' + SMTP_USER + '>', + to: ALERT_EMAIL, + subject: '[光湖系统] ⚠️ SYSLOG 工单处理超时告警', + html: [ + '
        ', + '

        ⚠️ 工单处理超时告警

        ', + '

        以下工单超过 3 次重试仍未收到人格体回执:

        ', + '
        ' + ticketList + '
        ', + '

        请登录 Notion 工单队列检查。

        ', + '

        🌀 铸渊 · 心跳监控 · 自动告警

        ', + '
        ', + ].join('\n'), + }); + + console.log('📧 告警邮件已发送至 ' + ALERT_EMAIL); + } catch (err) { + console.log('⚠️ 告警邮件发送失败: ' + err.message); + } +} + +// ══════════════════════════════════════════════════════════ +// 主流程 +// ══════════════════════════════════════════════════════════ + +async function main() { + console.log('═══════════════════════════════════════════'); + console.log('💓 铸渊 · 工单心跳监控(Phase B3)'); + console.log('═══════════════════════════════════════════'); + console.log(' 时间: ' + new Date().toISOString()); + console.log(''); + + // Step 1: 查询待处理工单 + var tickets = await queryPendingTickets(); + if (tickets.length === 0) { + console.log('✅ 无待处理工单,心跳正常'); + return; + } + + // Step 2: 筛选超时工单 + var timedOut = filterTimedOutTickets(tickets); + if (timedOut.length === 0) { + console.log('✅ 所有工单均在处理窗口内,心跳正常'); + return; + } + + // Step 3 & 4: 对超时工单执行重试或标记异常 + var errorTickets = []; + + for (var i = 0; i < timedOut.length; i++) { + var ticket = timedOut[i]; + // 基于超时时间估算重试次数(每 5 分钟一次) + var estimatedRetries = Math.floor(ticket.ageMinutes / 5); + + if (estimatedRetries < MAX_RETRIES) { + // 重试 + await triggerRetry(ticket); + } else { + // 超过最大重试次数,标记异常 + console.log('❌ 工单 ' + ticket.title + ' 重试 ' + MAX_RETRIES + ' 次仍无回执'); + await markTicketError(ticket.id); + errorTickets.push(ticket); + } + } + + // Step 5: 发送告警邮件(如有异常工单) + if (errorTickets.length > 0) { + await sendAlertEmail(errorTickets); + } + + console.log(''); + console.log('✅ 心跳检测完成'); +} + +main().catch(function (err) { + console.error('❌ 心跳监控失败: ' + err.message); + process.exit(1); +}); diff --git a/scripts/pipeline-reporter.js b/scripts/pipeline-reporter.js new file mode 100644 index 00000000..74fa4eb6 --- /dev/null +++ b/scripts/pipeline-reporter.js @@ -0,0 +1,140 @@ +/** + * ━━━ 实时进度上报模块 ━━━ + * 文件位置:scripts/pipeline-reporter.js + * + * 每完成一步,在 Issue 评论区发一条新评论,开发者刷新即可看到实时进度。 + * + * 使用方式(CLI): + * ISSUE_NUMBER=92 STEP_NUM=2 STEP_NAME="解析内容" STEP_STATUS=ok \ + * STEP_DETAIL="广播编号: BC-M22-007-AW" GITHUB_TOKEN=xxx \ + * node scripts/pipeline-reporter.js + * + * 或在 Node.js 中 require: + * const { reportStep, reportComplete } = require('./pipeline-reporter'); + */ + +const https = require('https'); + +const OWNER = process.env.GITHUB_REPOSITORY + ? process.env.GITHUB_REPOSITORY.split('/')[0] + : 'qinfendebingshuo'; +const REPO = process.env.GITHUB_REPOSITORY + ? process.env.GITHUB_REPOSITORY.split('/')[1] + : 'guanghulab'; + +/** + * 发送 GitHub API 请求 + */ +function githubAPI(method, path, data) { + return new Promise((resolve, reject) => { + const body = data ? JSON.stringify(data) : null; + const req = https.request({ + hostname: 'api.github.com', + path: path, + method: method, + headers: { + 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'Zhuyuan-Pipeline-Reporter', + 'Content-Type': 'application/json', + ...(body ? { 'Content-Length': Buffer.byteLength(body) } : {}) + } + }, (res) => { + let responseBody = ''; + res.on('data', chunk => { responseBody += chunk; }); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { resolve(JSON.parse(responseBody)); } + catch (e) { console.error('⚠️ JSON parse error:', e.message); resolve(responseBody); } + } else { + reject(new Error(`GitHub API ${res.statusCode}: ${responseBody.slice(0, 200)}`)); + } + }); + }); + req.on('error', reject); + if (body) req.write(body); + req.end(); + }); +} + +/** + * 每完成一步,在 Issue 评论区发一条新评论 + * @param {number} issueNumber - Issue 编号 + * @param {number} stepNum - 步骤编号(1-8) + * @param {string} stepName - 步骤名称 + * @param {'ok'|'error'|'skip'} status - 步骤状态 + * @param {string} detail - 详情说明 + */ +async function reportStep(issueNumber, stepNum, stepName, status, detail) { + const icon = status === 'ok' ? '✅' : status === 'error' ? '❌' : '⏭️'; + const statusText = status === 'ok' ? '已完成' : status === 'error' ? '失败' : '跳过'; + + const body = [ + `${icon} **步骤 ${stepNum}/8 · ${stepName}** · ${statusText}`, + '', + detail, + '', + `——铸渊(ICE-GL-ZY001)· ${new Date().toISOString()}` + ].join('\n'); + + await githubAPI('POST', `/repos/${OWNER}/${REPO}/issues/${issueNumber}/comments`, { body }); +} + +/** + * 闭环完成时发最终汇总 + * @param {number} issueNumber - Issue 编号 + * @param {boolean} success - 是否全部成功 + * @param {string} summary - 汇总内容 + */ +async function reportComplete(issueNumber, success, summary) { + const icon = success ? '🎉' : '⚠️'; + const title = success ? '闭环处理完成' : '闭环处理异常'; + + const body = [ + `## ${icon} ${title}`, + '', + summary, + '', + success + ? '> 新广播已生成,结果将发送到您的邮箱。' + : '> 部分步骤异常,已记录日志。冰朔会在下次巡检时处理。', + '', + `——铸渊(ICE-GL-ZY001)· ${new Date().toISOString()}` + ].join('\n'); + + await githubAPI('POST', `/repos/${OWNER}/${REPO}/issues/${issueNumber}/comments`, { body }); +} + +// ━━━ CLI 模式:直接 node scripts/pipeline-reporter.js ━━━ +if (require.main === module) { + const issueNumber = parseInt(process.env.ISSUE_NUMBER); + const stepNum = parseInt(process.env.STEP_NUM); + const stepName = process.env.STEP_NAME || ''; + const status = process.env.STEP_STATUS || 'ok'; + const detail = process.env.STEP_DETAIL || ''; + const isComplete = process.env.REPORT_COMPLETE === 'true'; + const isSuccess = process.env.REPORT_SUCCESS !== 'false'; + + if (!issueNumber) { + console.error('❌ 缺少 ISSUE_NUMBER'); + process.exit(1); + } + + (async () => { + try { + if (isComplete) { + await reportComplete(issueNumber, isSuccess, detail); + console.log(`📋 闭环汇总已发送 · Issue #${issueNumber}`); + } else { + await reportStep(issueNumber, stepNum, stepName, status, detail); + console.log(`📋 步骤 ${stepNum}/8 · ${stepName} · ${status} 已上报 · Issue #${issueNumber}`); + } + } catch (err) { + console.error(`⚠️ 进度上报失败: ${err.message}`); + // 上报失败不应阻断管道 + process.exit(0); + } + })(); +} + +module.exports = { reportStep, reportComplete }; diff --git a/scripts/push-broadcast-to-github.js b/scripts/push-broadcast-to-github.js new file mode 100644 index 00000000..41cd164f --- /dev/null +++ b/scripts/push-broadcast-to-github.js @@ -0,0 +1,171 @@ +// scripts/push-broadcast-to-github.js +// 铸渊 · Phase B4 · 广播文件推送到 GitHub 仓库 +// +// 人格体生成广播后,通过 GitHub API 将广播 .md 文件推送到仓库。 +// 路径规范:broadcasts/{developer_id}/{taskId}.md +// 提交信息:[AutoBroadcast] {taskId} · {developer} · {描述} +// +// 环境变量: +// GITHUB_TOKEN GitHub API token(推送用) +// BROADCAST_ID 广播编号(如 BC-M23-001-AW) +// DEVELOPER_ID 开发者编号(如 DEV-012) +// DEVELOPER_NAME 开发者名字(如 Awen) +// BROADCAST_CONTENT 广播全文内容(Markdown) +// MODULE_NAME 模块名(可选,如 M23) +// PHASE_NUMBER 环节号(可选,如 1) + +'use strict'; + +var https = require('https'); +var fs = require('fs'); +var devSuffixMap = require('./utils/dev-suffix-map'); + +var GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; +var BROADCAST_ID = process.env.BROADCAST_ID || ''; +var DEVELOPER_ID = process.env.DEVELOPER_ID || ''; +var DEVELOPER_NAME = process.env.DEVELOPER_NAME || ''; +var BROADCAST_CONTENT = process.env.BROADCAST_CONTENT || ''; +var MODULE_NAME = process.env.MODULE_NAME || ''; +var PHASE_NUMBER = process.env.PHASE_NUMBER || ''; + +var REPO_OWNER = 'qinfendebingshuo'; +var REPO_NAME = 'guanghulab'; + +// ══════════════════════════════════════════════════════════ +// GitHub API 工具 +// ══════════════════════════════════════════════════════════ + +function githubRequest(method, apiPath, body) { + return new Promise(function (resolve, reject) { + var payload = body ? JSON.stringify(body) : ''; + var opts = { + hostname: 'api.github.com', + port: 443, + path: apiPath, + method: method, + headers: { + 'User-Agent': 'ZhuyuanBroadcast/1.0', + 'Accept': 'application/vnd.github+json', + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + }, + }; + + if (GITHUB_TOKEN) { + opts.headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN; + } + + if (payload) { + opts.headers['Content-Length'] = Buffer.byteLength(payload); + } + + var req = https.request(opts, function (res) { + var data = ''; + res.on('data', function (chunk) { data += chunk; }); + res.on('end', function () { + try { + resolve({ status: res.statusCode, data: JSON.parse(data) }); + } catch (e) { + resolve({ status: res.statusCode, data: data }); + } + }); + }); + + req.on('error', reject); + if (payload) req.write(payload); + req.end(); + }); +} + +// ══════════════════════════════════════════════════════════ +// 广播推送 +// ══════════════════════════════════════════════════════════ + +async function pushBroadcast() { + if (!GITHUB_TOKEN) { + console.log('⚠️ 缺少 GITHUB_TOKEN,无法推送广播'); + process.exit(1); + } + + if (!BROADCAST_ID || !BROADCAST_CONTENT) { + console.log('⚠️ 缺少 BROADCAST_ID 或 BROADCAST_CONTENT'); + process.exit(1); + } + + // 确定开发者 ID(从广播编号提取或使用环境变量) + var devId = DEVELOPER_ID; + if (!devId) { + devId = devSuffixMap.getDevIdFromBroadcast(BROADCAST_ID) || 'UNKNOWN'; + } + + // 构建路径和提交信息 + var filePath = 'broadcasts/' + devId + '/' + BROADCAST_ID + '.md'; + var moduleLabel = MODULE_NAME || BROADCAST_ID.match(/BC-([A-Z0-9-]+)-/i)?.[1] || ''; + var phaseLabel = PHASE_NUMBER ? '环节' + PHASE_NUMBER : ''; + var devLabel = DEVELOPER_NAME || devId; + var commitMessage = '[AutoBroadcast] ' + BROADCAST_ID + ' · ' + devLabel + ' · ' + moduleLabel + phaseLabel; + + console.log('📡 推送广播到 GitHub...'); + console.log(' 路径: ' + filePath); + console.log(' 提交: ' + commitMessage); + + // 检查文件是否已存在(获取 SHA) + var existingRes = await githubRequest('GET', + '/repos/' + REPO_OWNER + '/' + REPO_NAME + '/contents/' + filePath); + + var sha = null; + if (existingRes.status === 200 && existingRes.data && existingRes.data.sha) { + sha = existingRes.data.sha; + console.log(' → 文件已存在,将覆盖更新 (sha: ' + sha.slice(0, 8) + ')'); + } + + // Base64 编码广播内容 + var contentBase64 = Buffer.from(BROADCAST_CONTENT, 'utf8').toString('base64'); + + // 创建或更新文件 + var putBody = { + message: commitMessage, + content: contentBase64, + committer: { + name: 'zhuyuan-agent[bot]', + email: 'zhuyuan-agent[bot]@users.noreply.github.com', + }, + }; + + if (sha) { + putBody.sha = sha; + } + + var putRes = await githubRequest('PUT', + '/repos/' + REPO_OWNER + '/' + REPO_NAME + '/contents/' + filePath, + putBody); + + if (putRes.status === 200 || putRes.status === 201) { + console.log('✅ 广播已推送: ' + filePath); + console.log(' → commit: ' + (putRes.data.commit ? putRes.data.commit.sha.slice(0, 8) : 'unknown')); + + // 输出到 GITHUB_OUTPUT + var outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + fs.appendFileSync(outputFile, 'broadcast_pushed=true\n'); + fs.appendFileSync(outputFile, 'broadcast_path=' + filePath + '\n'); + fs.appendFileSync(outputFile, 'broadcast_commit=' + (putRes.data.commit ? putRes.data.commit.sha : '') + '\n'); + } + } else { + console.error('❌ 广播推送失败: HTTP ' + putRes.status); + console.error(' → ' + JSON.stringify(putRes.data)); + + // 输出失败状态 + var outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + fs.appendFileSync(outputFile, 'broadcast_pushed=false\n'); + } + + process.exit(1); + } +} + +pushBroadcast().catch(function (err) { + console.error('❌ 广播推送异常: ' + err.message); + process.exit(1); +}); diff --git a/scripts/receive-syslog.js b/scripts/receive-syslog.js index dd136852..00ff57f0 100644 --- a/scripts/receive-syslog.js +++ b/scripts/receive-syslog.js @@ -194,11 +194,13 @@ async function createSyslogEntry(syslog, token, dbId, dateStr) { async function createTicket(syslog, token, dbId, dateStr) { const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN'; const devId = syslog.dev_id || syslog.developer_id || 'UNKNOWN'; + const devName = syslog.developer_name || syslog.dev_name || ''; + const now = new Date().toISOString(); const properties = { - '标题': titleProp('SYSLOG 回传|' + broadcastId + ' · ' + devId), + '标题': titleProp('[自动] SYSLOG处理|' + broadcastId + ' · ' + devId), '操作类型': selectProp('其他'), - '提交者': richTextProp('巡检引擎'), + '提交者': richTextProp('铸渊Agent·自动管道'), '提交日期': dateProp(dateStr), '状态': selectProp('待处理'), '优先级': selectProp('P1'), @@ -208,6 +210,22 @@ async function createTicket(syslog, token, dbId, dateStr) { if (broadcastId !== 'UNKNOWN') properties['广播编号'] = richTextProp(broadcastId); if (devId !== 'UNKNOWN') properties['开发者编号'] = richTextProp(devId); + // Phase B1 标准化工单内容 + const ticketContent = [ + '## 📡 标准化工单 · Phase B1', + '', + '| 字段 | 值 |', + '|------|-----|', + '| 工单类型 | SYSLOG处理 |', + '| 状态 | 待处理 |', + '| 来源 | GitHub Actions |', + '| taskId | ' + broadcastId + ' |', + '| developer | ' + devId + ' ' + devName + ' |', + '| created_at | ' + now + ' |', + '| retry_count | 0 |', + '| receipt_status | pending |', + ].join('\n'); + const body = { parent: { database_id: dbId }, properties, @@ -218,7 +236,7 @@ async function createTicket(syslog, token, dbId, dateStr) { paragraph: { rich_text: [{ type: 'text', - text: { content: '📥 SYSLOG 回传,来自 ' + devId + ',关联广播 ' + broadcastId }, + text: { content: ticketContent }, }], }, }, @@ -234,7 +252,7 @@ async function createTicket(syslog, token, dbId, dateStr) { }; const result = await notionPost('/v1/pages', body, token); - console.log(' → 霜砚工单已创建: ' + result.id); + console.log(' → 标准化工单已创建: ' + result.id); return result; } diff --git a/scripts/server-diagnose-report.js b/scripts/server-diagnose-report.js new file mode 100644 index 00000000..abe380ab --- /dev/null +++ b/scripts/server-diagnose-report.js @@ -0,0 +1,201 @@ +// scripts/server-diagnose-report.js +// 铸渊 · PM2 服务诊断报告 → Notion 工单 +// +// 将 PM2 诊断和系统健康检查结果写入 Notion 工单队列。 +// 由 pm2-server-diagnose.yml 工作流调用。 +// +// 环境变量: +// NOTION_TOKEN Notion API token +// NOTION_TICKET_DB_ID 工单队列数据库 ID +// PM2_REPORT PM2 诊断输出 +// CLEANUP_REPORT 清理操作输出(可选) +// HEALTH_REPORT 健康检查输出 + +'use strict'; + +const https = require('https'); + +const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; +const NOTION_TICKET_DB_ID = process.env.NOTION_TICKET_DB_ID || ''; +const PM2_REPORT = process.env.PM2_REPORT || ''; +const CLEANUP_REPORT = process.env.CLEANUP_REPORT || ''; +const HEALTH_REPORT = process.env.HEALTH_REPORT || ''; + +const NOTION_VERSION = '2022-06-28'; +const NOTION_API_HOSTNAME = 'api.notion.com'; +const NOTION_RICH_TEXT_MAX = 2000; +const MAX_PM2_REPORT_LENGTH = 3000; +const MAX_CLEANUP_REPORT_LENGTH = 2000; +const MAX_HEALTH_REPORT_LENGTH = 3000; + +// ══════════════════════════════════════════════════════════ +// Notion API 工具 +// ══════════════════════════════════════════════════════════ + +function notionPost(endpoint, body) { + return new Promise(function (resolve, reject) { + var payload = JSON.stringify(body); + var opts = { + hostname: NOTION_API_HOSTNAME, + port: 443, + path: endpoint, + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + 'Content-Length': Buffer.byteLength(payload), + }, + }; + var req = https.request(opts, 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 parse error: ' + data)); + } + }); + }); + req.on('error', reject); + req.write(payload); + req.end(); + }); +} + +function richTextChunks(content) { + var str = String(content || ''); + var chunks = []; + for (var i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) { + chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } }); + } + if (chunks.length === 0) { + chunks.push({ type: 'text', text: { content: '' } }); + } + return chunks; +} + +// ══════════════════════════════════════════════════════════ +// 构建报告内容 +// ══════════════════════════════════════════════════════════ + +function buildReportContent() { + var now = new Date().toISOString(); + var dateStr = now.slice(0, 10); + + var sections = []; + + sections.push('## 📋 PM2 服务排查报告 · ' + dateStr); + sections.push(''); + sections.push('| 字段 | 值 |'); + sections.push('|------|-----|'); + sections.push('| 执行者 | 铸渊Agent |'); + sections.push('| 执行时间 | ' + now + ' |'); + sections.push('| 来源 | GitHub Actions · pm2-server-diagnose |'); + sections.push(''); + + if (PM2_REPORT) { + sections.push('## 阶段一 · PM2 诊断结果'); + sections.push(''); + sections.push(PM2_REPORT.slice(0, MAX_PM2_REPORT_LENGTH)); + sections.push(''); + } + + if (CLEANUP_REPORT) { + sections.push('## 清理操作结果'); + sections.push(''); + sections.push(CLEANUP_REPORT.slice(0, MAX_CLEANUP_REPORT_LENGTH)); + sections.push(''); + } + + if (HEALTH_REPORT) { + sections.push('## 阶段二 · 全系统健康检查'); + sections.push(''); + sections.push(HEALTH_REPORT.slice(0, MAX_HEALTH_REPORT_LENGTH)); + sections.push(''); + } + + return sections.join('\n'); +} + +// ══════════════════════════════════════════════════════════ +// 主函数 — 创建 Notion 诊断工单 +// ══════════════════════════════════════════════════════════ + +async function createDiagnoseTicket() { + if (!NOTION_TOKEN || !NOTION_TICKET_DB_ID) { + console.log('⚠️ Notion credentials not configured, skipping ticket creation'); + console.log(' NOTION_TOKEN: ' + (NOTION_TOKEN ? '✅' : '❌')); + console.log(' NOTION_TICKET_DB_ID: ' + (NOTION_TICKET_DB_ID ? '✅' : '❌')); + process.exit(0); + } + + var now = new Date(); + var dateStr = now.toISOString().slice(0, 10); + var title = 'PM2服务排查报告 · ' + dateStr; + var contentText = buildReportContent(); + + // 将内容拆分成多个段落块(Notion 单个 rich_text 限制 2000 字符) + var contentBlocks = []; + var remaining = contentText; + + while (remaining.length > 0) { + var chunk = remaining.slice(0, NOTION_RICH_TEXT_MAX); + remaining = remaining.slice(NOTION_RICH_TEXT_MAX); + contentBlocks.push({ + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [{ type: 'text', text: { content: chunk } }], + }, + }); + } + + // 确保至少有一个内容块 + if (contentBlocks.length === 0) { + contentBlocks.push({ + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [{ type: 'text', text: { content: '诊断报告(无详细输出)' } }], + }, + }); + } + + var body = { + parent: { database_id: NOTION_TICKET_DB_ID }, + properties: { + '标题': { title: [{ type: 'text', text: { content: title } }] }, + '操作类型': { select: { name: '其他' } }, + '提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent' } }] }, + '状态': { select: { name: '已完成' } }, + '优先级': { select: { name: 'P1' } }, + }, + children: contentBlocks, + }; + + console.log('📋 创建 PM2 诊断工单...'); + console.log(' 标题: ' + title); + console.log(' 数据库: ' + NOTION_TICKET_DB_ID); + console.log(' 内容长度: ' + contentText.length + ' 字符'); + console.log(' 内容块数: ' + contentBlocks.length); + + try { + var result = await notionPost('/v1/pages', body); + console.log('✅ Notion 诊断工单已创建: ' + result.id); + console.log(' URL: ' + (result.url || 'N/A')); + } catch (err) { + console.error('❌ 工单创建失败: ' + err.message); + // 不以失败退出,诊断本身已成功 + console.log('⚠️ 诊断结果已输出到 Actions 日志,Notion 写入失败不影响诊断'); + process.exit(0); + } +} + +createDiagnoseTicket(); diff --git a/scripts/update-readme-bulletin.js b/scripts/update-readme-bulletin.js index a295e6ac..5dbcdd6d 100644 --- a/scripts/update-readme-bulletin.js +++ b/scripts/update-readme-bulletin.js @@ -30,6 +30,7 @@ const MEMORY_PATH = path.join(__dirname, '..', '.github', 'brain', 'memory.json' const PERSONA_MEMORY_PATH = path.join(__dirname, '..', '.github', 'persona-brain', 'memory.json'); const DEV_STATUS_PATH = path.join(__dirname, '..', '.github', 'persona-brain', 'dev-status.json'); const COLLABORATORS_PATH = path.join(__dirname, '..', '.github', 'brain', 'collaborators.json'); +const BULLETIN_CACHE_PATH = path.join(__dirname, '..', '.github', 'brain', 'bulletin-board-today.json'); const MAX_BINGSHUO_ENTRIES = 15; const MAX_COLLAB_ENTRIES = 20; @@ -101,6 +102,21 @@ function formatTime(ts) { return `${get('month')}-${get('day')} ${get('hour')}:${get('minute')}`; } +function formatTimeShort(ts) { + if (!ts) return '—'; + const d = new Date(ts); + if (isNaN(d.getTime())) return '—'; + const fmt = new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + const parts = fmt.formatToParts(d); + const get = (type) => (parts.find(p => p.type === type) || {}).value || ''; + return `${get('hour')}:${get('minute')}`; +} + function todayDateStr() { const fmt = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', @@ -149,6 +165,54 @@ function loadMemoryEvents() { return events; } +/* ── 公告栏缓存(当日追加式) ─────────────────────────── */ + +function loadBulletinCache() { + const today = todayDateStr(); + try { + if (fs.existsSync(BULLETIN_CACHE_PATH)) { + const cache = JSON.parse(fs.readFileSync(BULLETIN_CACHE_PATH, 'utf8')); + if (cache.date === today) { + return cache; + } + } + } catch (err) { + console.log(`⚠️ 公告栏缓存读取失败: ${err.message}`); + } + return { date: today, records: [] }; +} + +function saveBulletinCache(cache) { + try { + fs.writeFileSync(BULLETIN_CACHE_PATH, JSON.stringify(cache, null, 2), 'utf8'); + } catch (err) { + console.log(`⚠️ 公告栏缓存写入失败: ${err.message}`); + } +} + +function appendToCache(cache, newEntries) { + const existingKeys = new Set(cache.records.map(r => r.key)); + let added = 0; + for (const e of newEntries) { + const key = `${e.actor}|${e.module || '—'}|${e.ts || ''}`; + if (!existingKeys.has(key)) { + existingKeys.add(key); + cache.records.push({ + key, + ts: e.ts, + actor: e.actor, + module: e.module || '—', + result: e.result, + icon: e.icon, + sortKey: e.sortKey, + }); + added++; + } + } + cache.records.sort((a, b) => a.sortKey - b.sortKey); + return added; +} + /* ── 分类事件为冰朔/合作者 ─────────────────────────── */ function classifyEvents(events) { @@ -474,26 +538,19 @@ function buildBingshuoAlert(issues) { return alert; } -/* ── 生成合作者公告表格 ─────────────────────────── */ +/* ── 生成合作者公告表格(追加式,从缓存构建) ─────────────────────────── */ -function buildCollabBulletin(entries) { - const sorted = [...entries].sort((a, b) => b.sortKey - a.sortKey); - const seen = new Set(); - const unique = []; - for (const e of sorted) { - const key = `${e.actor}|${e.module}|${formatTime(e.ts)}`; - if (!seen.has(key)) { seen.add(key); unique.push(e); } - } - - const display = unique.slice(0, MAX_COLLAB_ENTRIES); +function buildCollabBulletin(cacheRecords) { + const today = todayDateStr(); + const display = cacheRecords.slice(0, MAX_COLLAB_ENTRIES); if (display.length === 0) { - return '| 时间 | 合作者 | 模块 | 状态 |\n|------|--------|------|------|\n| 🕐 暂无记录 | — | — | 等待模块推送 |'; + return `👥 合作者公告栏(${today})\n\n| 时间 | 合作者 | 模块 | 状态 |\n|------|--------|------|------|\n| 🕐 暂无记录 | — | — | 等待模块推送 |`; } const rows = display.map(e => - `| ${formatTime(e.ts)} | ${e.actor} | \`${e.module || '—'}/\` | ${e.icon} ${e.result === 'success' || e.result === 'passed' ? '上传成功' : e.result === 'failed' || e.result === 'failure' ? '❌ 上传失败' : '已更新'} |` + `| ${formatTimeShort(e.ts)} | ${e.actor} | \`${e.module || '—'}/\` | ${e.icon} ${e.result === 'success' || e.result === 'passed' ? '上传成功' : e.result === 'failed' || e.result === 'failure' ? '❌ 上传失败' : '已更新'} |` ); - return `| 时间 | 合作者 | 模块 | 状态 |\n|------|--------|------|------|\n${rows.join('\n')}`; + return `👥 合作者公告栏(${today})\n\n| 时间 | 合作者 | 模块 | 状态 |\n|------|--------|------|------|\n${rows.join('\n')}`; } /* ── 生成合作者提醒 ─────────────────────────── */ @@ -687,18 +744,24 @@ async function main() { console.log(`\n📊 冰朔合计: ${allBingshuo.length} 条`); console.log(`📊 合作者合计: ${allCollab.length} 条\n`); - // 5. 检测需要干预的问题 + // 5. 合作者公告栏:追加式缓存 + const cache = loadBulletinCache(); + const addedCount = appendToCache(cache, allCollab); + saveBulletinCache(cache); + console.log(`📋 公告栏缓存: ${cache.records.length} 条记录(本次新增 ${addedCount} 条)`); + + // 6. 检测需要干预的问题 const { bingshuoIssues, collabIssuesByDev } = detectIssues(allBingshuo, allCollab); console.log(`🔴 冰朔待处理: ${bingshuoIssues.length} 条`); console.log(`🔴 合作者待处理: ${Object.keys(collabIssuesByDev).length} 人\n`); - // 6. 生成各区域内容 + // 7. 生成各区域内容 const bingshuoBulletin = buildBingshuoBulletin(allBingshuo); const bingshuoAlert = buildBingshuoAlert(bingshuoIssues); - const collabBulletin = buildCollabBulletin(allCollab); + const collabBulletin = buildCollabBulletin(cache.records); const collabAlert = buildCollabAlert(collabIssuesByDev); - // 7. 更新 README.md + // 8. 更新 README.md if (!fs.existsSync(README_PATH)) { console.error('❌ README.md 不存在'); process.exit(1); @@ -718,7 +781,7 @@ async function main() { console.log('✅ README.md 公告区已更新'); } - // 8. 发送邮件(如有需要) + // 9. 发送邮件(如有需要) if (bingshuoIssues.length > 0 || Object.keys(collabIssuesByDev).length > 0) { await sendAlertEmails(bingshuoIssues, collabIssuesByDev); } diff --git a/scripts/utils/dev-suffix-map.js b/scripts/utils/dev-suffix-map.js new file mode 100644 index 00000000..35248f80 --- /dev/null +++ b/scripts/utils/dev-suffix-map.js @@ -0,0 +1,38 @@ +// scripts/utils/dev-suffix-map.js +// 开发者广播编号后缀 → DEV-ID 映射表 +// +// 广播编号格式: BC-{模块}-{序号}-{后缀} +// 后缀是开发者名字拼音首字母缩写 + +'use strict'; + +// 后缀→开发者编号映射 +var SUFFIX_MAP = { + 'YY': 'DEV-001', // 页页 + 'FM': 'DEV-002', // 肥猫 + 'YF': 'DEV-003', // 燕樊 + 'ZZ': 'DEV-004', // 之之 + 'XCM': 'DEV-005', // 小草莓 + 'HE': 'DEV-009', // 花尔 + 'JZ': 'DEV-010', // 桔子 + 'CCNN': 'DEV-011', // 匆匆那年 + 'AW': 'DEV-012', // Awen + 'XX': 'DEV-013', // 小兴 + 'SY': 'DEV-014', // 时雨 +}; + +/** + * 从广播编号中提取开发者编号 + * @param {string} broadcastId - 广播编号(如 BC-M22-009-AW) + * @returns {string} 开发者编号(如 DEV-012),未找到返回空字符串 + */ +function getDevIdFromBroadcast(broadcastId) { + var suffixMatch = (broadcastId || '').match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i); + var suffix = suffixMatch ? suffixMatch[1].toUpperCase() : ''; + return SUFFIX_MAP[suffix] || ''; +} + +module.exports = { + SUFFIX_MAP: SUFFIX_MAP, + getDevIdFromBroadcast: getDevIdFromBroadcast, +}; diff --git a/scripts/verify-modules.js b/scripts/verify-modules.js new file mode 100644 index 00000000..24b250e5 --- /dev/null +++ b/scripts/verify-modules.js @@ -0,0 +1,306 @@ +// scripts/verify-modules.js +// 铸渊 · 模块上传验证脚本 +// +// 功能: +// ① 从 SYSLOG 内容中提取模块编号(M01, M22, M-AUTH 等) +// ② 通过 routing-map.json 查找模块对应的目录 +// ③ 检测目录是否存在于仓库中 +// ④ 检测 dev-nodes 中开发者节点是否存在 +// ⑤ 输出验证结果(JSON 格式)到 GITHUB_OUTPUT +// +// 环境变量: +// SYSLOG_CONTENT SYSLOG 全文内容 +// BROADCAST_ID 广播编号(如 BC-M22-009-AW) +// AUTHOR 提交者 GitHub 用户名 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const devSuffixMap = require('./utils/dev-suffix-map'); + +const SYSLOG_CONTENT = process.env.SYSLOG_CONTENT || ''; +const BROADCAST_ID = process.env.BROADCAST_ID || ''; +const AUTHOR = process.env.AUTHOR || ''; + +const REPO_ROOT = path.resolve(__dirname, '..'); +const ROUTING_MAP_PATH = path.join(REPO_ROOT, 'routing-map.json'); +const DEV_STATUS_PATH = path.join(REPO_ROOT, '.github', 'persona-brain', 'dev-status.json'); + +// ══════════════════════════════════════════════════════════ +// 模块编号提取 +// ══════════════════════════════════════════════════════════ + +function extractModuleIds(content, broadcastId) { + var modules = new Set(); + + // 从广播编号提取模块编号(如 BC-M22-009-AW → M22) + var bcMatch = broadcastId.match(/BC-([A-Z][A-Z0-9]+(?:-[A-Z]+)?)-/i); + if (bcMatch) { + modules.add(bcMatch[1].toUpperCase()); + } + + // 从内容中匹配模块编号模式 + // 支持: M01, M03, M05, M22, M-AUTH, M-CHANNEL, M-DASHBOARD, M-STATUS, M-MEMORY, M-DINGTALK + var patterns = [ + /\b(M\d{2})\b/gi, + /\b(M-[A-Z]+)\b/gi, + /模块[::]\s*(M[A-Z0-9-]+)/gi, + /module[::]\s*(M[A-Z0-9-]+)/gi, + ]; + + patterns.forEach(function (re) { + var match; + while ((match = re.exec(content)) !== null) { + modules.add(match[1].toUpperCase()); + } + }); + + return Array.from(modules); +} + +// ══════════════════════════════════════════════════════════ +// 开发者编号提取 +// ══════════════════════════════════════════════════════════ + +function extractDevId(content, broadcastId) { + // 从内容中直接提取 DEV-XXX(最优先) + var devMatch = content.match(/\b(DEV-\d{3})\b/i); + if (devMatch) return devMatch[1].toUpperCase(); + + // 从广播编号提取开发者编号(使用共享映射表) + var mappedDevId = devSuffixMap.getDevIdFromBroadcast(broadcastId); + if (mappedDevId) return mappedDevId; + + // 兜底:从 dev-status.json 匹配 + var suffixMatch = broadcastId.match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i); + var devSuffix = suffixMatch ? suffixMatch[1].toUpperCase() : ''; + + if (devSuffix) { + try { + var devStatus = JSON.parse(fs.readFileSync(DEV_STATUS_PATH, 'utf8')); + var team = devStatus.team || []; + for (var i = 0; i < team.length; i++) { + var waiting = team[i].waiting || ''; + if (waiting.includes(broadcastId) || waiting.includes(devSuffix)) { + return team[i].dev_id; + } + } + } catch (_) { /* ignore */ } + } + + return ''; +} + +// ══════════════════════════════════════════════════════════ +// 模块目录验证 +// ══════════════════════════════════════════════════════════ + +function verifyModules(moduleIds) { + var routingMap = {}; + try { + routingMap = JSON.parse(fs.readFileSync(ROUTING_MAP_PATH, 'utf8')); + } catch (_) { + console.log('⚠️ routing-map.json 不存在或格式错误'); + } + + var modules = routingMap.modules || {}; + var results = []; + + moduleIds.forEach(function (modId) { + var entry = modules[modId]; + var result = { + module_id: modId, + registered: false, + dir_exists: false, + dir_path: '', + dev_id: '', + status: '', + files_found: 0, + }; + + if (entry) { + result.registered = true; + result.dir_path = entry.dir || ''; + result.dev_id = entry.dev || ''; + result.status = entry.status || ''; + + // 检查目录是否存在 + var dirFullPath = path.join(REPO_ROOT, entry.dir); + if (fs.existsSync(dirFullPath)) { + result.dir_exists = true; + // 统计文件数量(不含隐藏文件) + try { + var files = fs.readdirSync(dirFullPath).filter(function (f) { + return !f.startsWith('.'); + }); + result.files_found = files.length; + } catch (_) { /* ignore */ } + } + } else { + // 模块未在 routing-map 中注册,尝试直接查找 + var candidates = [ + modId.toLowerCase(), + 'm' + modId.replace(/^M/i, '').toLowerCase(), + 'dev-nodes/' + modId, + ]; + for (var i = 0; i < candidates.length; i++) { + var candidatePath = path.join(REPO_ROOT, candidates[i]); + if (fs.existsSync(candidatePath)) { + result.dir_exists = true; + result.dir_path = candidates[i]; + try { + var files = fs.readdirSync(candidatePath).filter(function (f) { + return !f.startsWith('.'); + }); + result.files_found = files.length; + } catch (_) { /* ignore */ } + break; + } + } + } + + results.push(result); + }); + + return results; +} + +// ══════════════════════════════════════════════════════════ +// 开发者节点验证 +// ══════════════════════════════════════════════════════════ + +function verifyDevNode(devId) { + if (!devId) return { exists: false, path: '', files: 0 }; + + var devNodePath = path.join(REPO_ROOT, 'dev-nodes', devId); + var result = { exists: false, path: 'dev-nodes/' + devId, files: 0 }; + + if (fs.existsSync(devNodePath)) { + result.exists = true; + try { + var files = fs.readdirSync(devNodePath).filter(function (f) { + return !f.startsWith('.'); + }); + result.files = files.length; + } catch (_) { /* ignore */ } + } + + return result; +} + +// ══════════════════════════════════════════════════════════ +// 主流程 +// ══════════════════════════════════════════════════════════ + +function main() { + console.log('═══════════════════════════════════════════'); + console.log('🔍 铸渊 · 模块上传验证'); + console.log('═══════════════════════════════════════════'); + console.log(' 广播编号: ' + BROADCAST_ID); + console.log(' 提交者: ' + AUTHOR); + console.log(' 内容长度: ' + SYSLOG_CONTENT.length + ' 字符'); + console.log(''); + + // ① 提取模块编号 + var moduleIds = extractModuleIds(SYSLOG_CONTENT, BROADCAST_ID); + console.log('📦 识别到模块: ' + (moduleIds.length > 0 ? moduleIds.join(', ') : '(无)')); + + // ② 提取开发者编号 + var devId = extractDevId(SYSLOG_CONTENT, BROADCAST_ID); + console.log('👤 开发者编号: ' + (devId || '(未识别)')); + + // ③ 验证模块目录 + var moduleResults = verifyModules(moduleIds); + + // ④ 验证开发者节点 + var devNodeResult = verifyDevNode(devId); + + // ⑤ 汇总结果 + var allModulesUploaded = moduleResults.length > 0 && moduleResults.every(function (r) { + return r.dir_exists; + }); + + var summary = { + broadcast_id: BROADCAST_ID, + author: AUTHOR, + dev_id: devId, + modules_detected: moduleIds, + module_count: moduleIds.length, + module_results: moduleResults, + dev_node: devNodeResult, + all_modules_uploaded: allModulesUploaded, + verification_passed: allModulesUploaded || moduleIds.length === 0, + timestamp: new Date().toISOString(), + }; + + // ⑥ 输出报告 + console.log(''); + console.log('═══════════════════════════════════════════'); + console.log('📋 验证报告'); + console.log('═══════════════════════════════════════════'); + + if (moduleResults.length === 0) { + console.log(' ℹ️ 未检测到模块引用,跳过模块验证'); + } else { + moduleResults.forEach(function (r) { + var icon = r.dir_exists ? '✅' : '❌'; + console.log(' ' + icon + ' ' + r.module_id + + ' → ' + (r.dir_path || '(未注册)') + + (r.dir_exists ? ' (' + r.files_found + ' 个文件)' : ' (目录不存在)')); + }); + } + + console.log(' ' + (devNodeResult.exists ? '✅' : '⚠️') + + ' 开发者节点: ' + devNodeResult.path + + (devNodeResult.exists ? ' (' + devNodeResult.files + ' 个文件)' : ' (不存在)')); + + console.log(''); + console.log(' 总结: ' + (summary.verification_passed ? '✅ 验证通过' : '❌ 模块未完整上传')); + + // ⑦ 写入 GITHUB_OUTPUT + var outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + var delimiter = 'EOF_' + Date.now(); + var jsonStr = JSON.stringify(summary); + fs.appendFileSync(outputFile, 'verify_result<<' + delimiter + '\n' + jsonStr + '\n' + delimiter + '\n'); + fs.appendFileSync(outputFile, 'modules_uploaded=' + (allModulesUploaded ? 'true' : 'false') + '\n'); + fs.appendFileSync(outputFile, 'verification_passed=' + (summary.verification_passed ? 'true' : 'false') + '\n'); + fs.appendFileSync(outputFile, 'module_count=' + moduleIds.length + '\n'); + } + + // ⑧ 将汇总文本输出(供后续步骤作为上下文) + var reportLines = [ + '## 🔍 模块上传验证报告', + '', + '| 项目 | 结果 |', + '|------|------|', + '| 广播编号 | ' + BROADCAST_ID + ' |', + '| 开发者 | ' + (devId || AUTHOR) + ' |', + '| 检测模块数 | ' + moduleIds.length + ' |', + '| 全部上传 | ' + (allModulesUploaded ? '✅ 是' : '❌ 否') + ' |', + '| 验证结果 | ' + (summary.verification_passed ? '✅ 通过' : '❌ 未通过') + ' |', + ]; + + if (moduleResults.length > 0) { + reportLines.push(''); + reportLines.push('### 模块详情'); + reportLines.push('| 模块 | 目录 | 状态 | 文件数 |'); + reportLines.push('|------|------|------|--------|'); + moduleResults.forEach(function (r) { + reportLines.push('| ' + r.module_id + ' | ' + (r.dir_path || '-') + + ' | ' + (r.dir_exists ? '✅ 已上传' : '❌ 未找到') + + ' | ' + r.files_found + ' |'); + }); + } + + if (outputFile) { + var reportDelimiter = 'REPORT_EOF_' + Date.now(); + fs.appendFileSync(outputFile, 'verify_report<<' + reportDelimiter + '\n' + reportLines.join('\n') + '\n' + reportDelimiter + '\n'); + } + + console.log(''); + console.log('✅ 验证完成'); +} + +main(); diff --git a/scripts/wake-persona.js b/scripts/wake-persona.js index fb88b08e..1ca304d5 100644 --- a/scripts/wake-persona.js +++ b/scripts/wake-persona.js @@ -38,6 +38,11 @@ const SUBMIT_TYPE = process.env.SUBMIT_TYPE || 'question'; const SUBMIT_CONTENT = process.env.SUBMIT_CONTENT || ''; const AUTHOR = process.env.AUTHOR || 'unknown'; +// 模块验证结果注入(由 verify-modules.js 提供) +const MODULE_VERIFY_RESULT = process.env.MODULE_VERIFY_RESULT || ''; +// Notion 回传结果注入(由 notion-callback-pipeline 提供) +const NOTION_CALLBACK_RESULT = process.env.NOTION_CALLBACK_RESULT || ''; + // Notion 配置(v4.0 协议动态注入) const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; const CORE_BRAIN_PAGE_ID = process.env.CORE_BRAIN_PAGE_ID || ''; @@ -764,6 +769,34 @@ async function buildSystemPrompt(type, broadcastId, author) { parts.push('此规则优先级最高,覆盖核心大脑中「广播不写代码」的默认规则。'); parts.push('此规则仅适用于自动化链路(Claude API 出广播),手动链路不受影响。'); + // ━━━ 模块验证结果注入(铸渊 Agent 检测结果) ━━━ + if (MODULE_VERIFY_RESULT) { + parts.push(''); + parts.push('═══════════════════════════════════════════'); + parts.push('## 🔍 铸渊 Agent · 模块上传验证结果'); + parts.push('═══════════════════════════════════════════'); + parts.push(''); + parts.push('以下是铸渊 Agent 在仓库内自动检测的模块上传验证结果:'); + parts.push(MODULE_VERIFY_RESULT); + parts.push(''); + parts.push('请根据验证结果决定是否接受 SYSLOG:'); + parts.push('- 如果模块已上传(✅),继续正常闭环流程'); + parts.push('- 如果模块未上传(❌),在验收报告中标注"需补充",并在反馈中指出缺失的模块'); + } + + // ━━━ Notion 回传结果注入(二次核验) ━━━ + if (NOTION_CALLBACK_RESULT) { + parts.push(''); + parts.push('═══════════════════════════════════════════'); + parts.push('## 📥 Notion 侧处理结果(回传核验)'); + parts.push('═══════════════════════════════════════════'); + parts.push(''); + parts.push('以下是 Notion 侧核心大脑人格体处理后的回传结果:'); + parts.push(NOTION_CALLBACK_RESULT); + parts.push(''); + parts.push('请核对以上回传内容,确认新广播生成无误后输出最终确认。'); + } + // ━━━ 任务类型专用指令 ━━━ if (type === 'syslog') { parts.push(''); @@ -773,17 +806,19 @@ async function buildSystemPrompt(type, broadcastId, author) { parts.push(''); parts.push('你需要完成以下工作:'); parts.push('1. 验收 SYSLOG(检查 MODULE_LOG 完整性)'); - parts.push('2. 查询画像库最近 2-3 条快照(PGP v1.0)'); - parts.push('3. 查询模块指纹注册表(防重复·⑨.5)'); - parts.push('4. RT-02 自动调度判断'); - parts.push('5. 生成新广播(BC-GEN v4.0 完整流程)'); - parts.push('6. 输出结构化结果(广播全文 + 闭环数据)'); + parts.push('2. 检查模块上传验证结果(铸渊 Agent 已自动检测)'); + parts.push('3. 查询画像库最近 2-3 条快照(PGP v1.0)'); + parts.push('4. 查询模块指纹注册表(防重复·⑨.5)'); + parts.push('5. RT-02 自动调度判断'); + parts.push('6. 生成新广播(BC-GEN v4.0 完整流程)'); + parts.push('7. 输出结构化结果(广播全文 + 闭环数据)'); parts.push(''); parts.push('输出格式:'); parts.push('---'); parts.push('## 📡 SYSLOG 验收报告'); parts.push('### 广播编号:[编号]'); parts.push('### 验收结果:[通过/需补充]'); + parts.push('### 模块验证:[已上传/未上传,列出详情]'); parts.push('### 工作总结:[摘要]'); parts.push('### 画像评估:[PGP 五维度评分]'); parts.push('### 调度判断:[RT-02 下一步]'); diff --git a/scripts/zhuyuan-issue-reply.js b/scripts/zhuyuan-issue-reply.js index 0841d074..f14e9bc4 100644 --- a/scripts/zhuyuan-issue-reply.js +++ b/scripts/zhuyuan-issue-reply.js @@ -1,5 +1,6 @@ const fs = require('fs'); const https = require('https'); +const { parseIntent, checkPipelineStatus, checkDevStatus, formatTeamOverview, unknownIntentHelp } = require('./intent-router'); // === 知识库匹配阈值:问题词中至少40%出现在Issue文本中才算命中 === const FAQ_MATCH_THRESHOLD = 0.4; @@ -103,6 +104,7 @@ function containsSystemCommand(text) { } // === 判断Issue类型 === +const isSyslog = issueLabels.includes('syslog') || /SYSLOG|系统日志/.test(issueTitle) || issueBody.includes('### 广播编号'); const isProgressQuery = issueLabels.includes('progress-query'); const isDevQuestion = issueLabels.includes('dev-question'); @@ -165,53 +167,76 @@ async function handleCommentTrigger() { return handleCollaboratorComment(user); } -// === 冰朔评论处理 === +// === 冰朔评论处理(含意图路由)=== async function handleBingshuoComment() { console.log('🧊 冰朔指令识别中...'); - // 冰朔可以查询任何人的状态 - if (commentBody.includes('进度') || commentBody.includes('状态')) { - if (devId && devInfo) { - const reply = `## ⚒️ 铸渊回复 · 冰朔查询\n\n` - + `**${devInfo.name}(${devInfo.dev_id})当前状态:**\n` - + `- 📌 模块:${devInfo.modules.join('、')}\n` - + `- 📊 状态:${devInfo.status}\n` - + `- ⏳ 等待中:${devInfo.waiting_for}\n` - + `- 👉 下一步:${devInfo.next_step}\n\n` + // 使用意图路由器解析评论 + const intent = parseIntent(commentBody); + console.log('[意图识别]', JSON.stringify(intent)); + + switch (intent.type) { + case 'check_pipeline_status': { + // 查询某个 Issue 的闭环管道状态 + const statusReply = await checkPipelineStatus(intent.issueNumber); + const reply = `## ⚒️ 铸渊回复 · 冰朔查询\n\n${statusReply}\n\n` + + `---\n*—— 铸渊(ICE-GL-ZY001)*`; + await postComment(reply); + return; + } + + case 'check_dev_status': { + // 查询某个开发者的状态 + const devReply = checkDevStatus(intent.devId, devStatus); + const reply = `## ⚒️ 铸渊回复 · 冰朔查询\n\n${devReply}\n\n` + `---\n*数据来源:Notion主控台 · 最后同步 ${devStatus.last_synced}*\n` + `*—— 铸渊(ICE-GL-ZY001)*`; await postComment(reply); return; } - // 团队总览 - let reply = `## ⚒️ 铸渊回复 · 团队进度总览\n\n`; - devStatus.team_status.forEach(dev => { - reply += `**${dev.dev_id} ${dev.name}** · ${dev.status}\n`; - }); - reply += `\n---\n*最后同步:${devStatus.last_synced}*\n`; - reply += `*—— 铸渊(ICE-GL-ZY001)*`; - await postComment(reply); - return; - } + case 'team_overview': { + // 显式请求团队总览 + const reply = formatTeamOverview(devStatus) + + `\n*—— 铸渊(ICE-GL-ZY001)*`; + await postComment(reply); + return; + } - // 冰朔的一般指令 → 用 AI 处理 - const aiReply = await callYunwuAPI('冰朔指令', commentBody, null); - if (aiReply) { - const reply = `## ⚒️ 铸渊回复 · 冰朔\n\n${aiReply}\n\n` - + `---\n*—— 铸渊(ICE-GL-ZY001)· 冰朔指令已处理*`; - await postComment(reply); - } else { - const reply = `## ⚒️ 铸渊收到\n\n冰朔,已记录你的指令。铸渊会在下次巡检时处理。\n\n` - + `---\n*—— 铸渊(ICE-GL-ZY001)*`; - await postComment(reply); + default: { + // 冰朔的一般指令 → 用 AI 处理 + const aiReply = await callYunwuAPI('冰朔指令', commentBody, null); + if (aiReply) { + const reply = `## ⚒️ 铸渊回复 · 冰朔\n\n${aiReply}\n\n` + + `---\n*—— 铸渊(ICE-GL-ZY001)· 冰朔指令已处理*`; + await postComment(reply); + } else { + const helpText = unknownIntentHelp(); + const reply = `## ⚒️ 铸渊收到\n\n冰朔,已记录你的指令。\n\n${helpText}\n\n` + + `---\n*—— 铸渊(ICE-GL-ZY001)*`; + await postComment(reply); + } + } } } -// === 合作者评论处理 === +// === 合作者评论处理(含意图路由)=== async function handleCollaboratorComment(user) { console.log(`👤 合作者 ${user.name}(${user.devId})问题处理中...`); + // 使用意图路由器解析评论 + const intent = parseIntent(commentBody); + console.log('[意图识别]', JSON.stringify(intent)); + + // 合作者查询闭环管道状态 + if (intent.type === 'check_pipeline_status') { + const statusReply = await checkPipelineStatus(intent.issueNumber); + const reply = `## ⚒️ 铸渊回复\n\n${statusReply}\n\n` + + `---\n*—— 铸渊(ICE-GL-ZY001)*`; + await postComment(reply); + return; + } + // 合作者只能查询自己的状态 const selfDevInfo = user.devId ? devStatus.team_status.find(d => d.dev_id === user.devId) @@ -290,6 +315,12 @@ async function handleCollaboratorComment(user) { async function handleIssueTrigger() { let reply = ''; + // --- SYSLOG 提交:由 syslog-issue-pipeline 处理,此处跳过 --- + if (isSyslog) { + console.log('📡 SYSLOG Issue detected, skipping (handled by syslog-issue-pipeline)'); + return; + } + // --- 进度查询(指定开发者)--- if (isProgressQuery && devInfo) { reply = `## ⚒️ 铸渊回复 · 进度查询\n\n`; diff --git a/search-filter/css/style.css b/search-filter/css/style.css index a4b9d03e..1ef77b04 100644 --- a/search-filter/css/style.css +++ b/search-filter/css/style.css @@ -404,3 +404,255 @@ h1 { background: #1a2639; color: white; } + +/* ========== 新增样式:排序、分页、自动补全、统计、无结果 ========== */ + +/* 排序区域 */ +.sort-section { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; + padding: 15px 0; + border-bottom: 1px solid #eef2f6; +} + +.sort-buttons { + display: flex; + gap: 12px; + align-items: center; +} + +.sort-label { + color: #6c757d; + font-size: 0.95rem; +} + +.sort-btn { + padding: 8px 18px; + background: white; + border: 1.5px solid #e0e4e9; + border-radius: 40px; + color: #495057; + font-size: 0.95rem; + cursor: pointer; + transition: all 0.2s; +} + +.sort-btn:hover { + border-color: #ffb347; + background: #fff9f0; +} + +.sort-btn.active { + background: #1a2639; + border-color: #1a2639; + color: white; +} + +.sort-order { + display: flex; + align-items: center; + gap: 6px; + color: #495057; +} + +/* 自动补全 */ +.autocomplete-panel { + position: absolute; + top: 70px; + left: 0; + right: 0; + background: white; + border-radius: 24px; + box-shadow: 0 12px 30px rgba(0,0,0,0.15); + padding: 10px 0; + z-index: 200; + border: 1px solid #eef2f6; + max-height: 300px; + overflow-y: auto; +} + +.autocomplete-panel.hidden { + display: none; +} + +.autocomplete-list { + list-style: none; +} + +.autocomplete-item { + padding: 12px 20px; + cursor: pointer; + transition: background 0.2s; + color: #1a2639; +} + +.autocomplete-item:hover { + background: #f8fafd; +} + +/* 统计区域 */ +.stats-section { + display: flex; + justify-content: space-between; + margin-bottom: 15px; + padding: 10px 0; + color: #6c757d; + font-size: 0.95rem; + border-bottom: 1px dashed #e0e4e9; +} + +/* 无结果提示 */ +.no-results-panel { + text-align: center; + padding: 40px 20px; + background: #f8fafd; + border-radius: 32px; + margin: 20px 0; +} + +.no-results-panel.hidden { + display: none; +} + +.no-results-panel p { + color: #6c757d; + margin-bottom: 20px; + font-size: 1.1rem; +} + +.suggestions-panel { + margin-top: 20px; +} + +.suggestions-panel p { + font-size: 0.95rem; + margin-bottom: 15px; +} + +.suggestion-tags { + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: center; +} + +.suggestion-tag { + padding: 8px 18px; + background: white; + border: 1.5px solid #ffd166; + border-radius: 40px; + color: #b85e00; + font-size: 0.95rem; + cursor: pointer; + transition: all 0.2s; +} + +.suggestion-tag:hover { + background: #ffd166; + color: #1a2639; +} + +/* 模糊匹配标记 */ +.fuzzy-badge { + background: #e3f2fd; + color: #1976d2; + font-size: 0.75rem; + padding: 2px 8px; + border-radius: 40px; + margin-left: 8px; +} + +/* 分页区域 */ +.pagination-section { + margin-top: 40px; + padding-top: 25px; + border-top: 2px solid #eef2f6; + display: flex; + flex-wrap: wrap; + gap: 20px; + align-items: center; + justify-content: space-between; +} + +.pagination-controls { + display: flex; + gap: 8px; + align-items: center; +} + +.pagination-controls button { + padding: 8px 16px; + background: white; + border: 1.5px solid #e0e4e9; + border-radius: 40px; + color: #495057; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; + min-width: 45px; +} + +.pagination-controls button:hover:not(:disabled) { + border-color: #ffb347; + background: #fff9f0; +} + +.pagination-controls button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +#pageInfo { + margin: 0 12px; + color: #1a2639; + font-weight: 500; +} + +.pagination-size select { + padding: 8px 16px; + border: 1.5px solid #e0e4e9; + border-radius: 40px; + background: white; + color: #1a2639; + outline: none; + cursor: pointer; +} + +.pagination-jump { + display: flex; + gap: 8px; + align-items: center; +} + +.pagination-jump input { + width: 70px; + padding: 8px 12px; + border: 1.5px solid #e0e4e9; + border-radius: 40px; + text-align: center; + outline: none; +} + +.pagination-jump button { + padding: 8px 18px; + background: #1a2639; + border: none; + border-radius: 40px; + color: white; + cursor: pointer; + transition: all 0.2s; +} + +.pagination-jump button:hover { + background: #2c3e50; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .pagination-section { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/search-filter/index.html b/search-filter/index.html index 64b93893..094bf4a1 100644 --- a/search-filter/index.html +++ b/search-filter/index.html @@ -3,22 +3,27 @@ - 搜索与筛选 · 糖星云陪妈妈 + 搜索与筛选系统 · 糖星云陪妈妈 · 环节5+6

        🔍 搜索与筛选系统

        -

        糖星云陪妈妈一起做 · 环节4:搜索历史+筛选预设+实时高亮

        +

        糖星云陪妈妈一起做 · 环节5+6:排序+分页+模糊+拼音+自动补全

        - +
        - +
        - + + + + - + +
        +
        + 排序: + + + +
        +
        + +
        +
        + +

        🎛️ 筛选条件

        @@ -54,10 +74,53 @@
        + +
        + 共 0 条结果 + +
        +

        📦 搜索结果

        + + + +
        + + +
        +
        + + + 第 1 页 / 共 1 页 + + +
        +
        + +
        +
        + +
        @@ -65,7 +128,7 @@