From eff7e539d416139ccfdf4e5a0faaf63ab591f99e Mon Sep 17 00:00:00 2001 From: root Date: Sat, 30 May 2026 15:42:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Agent=E6=BA=90=E7=A0=81=E5=BD=92?= =?UTF-8?q?=E4=BD=8D=E5=88=B0=20agents/zhuyuan-dev-agent/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zhuyuan-dev-agent/agent-constitution.json | 55 + agents/zhuyuan-dev-agent/ecosystem.config.js | 12 + .../modules/M22-bulletin/README.md | 32 + .../modules/M22-bulletin/api.js | 112 ++ .../modules/M22-bulletin/css/style.css | 334 ++++ .../M22-bulletin/data/mock-announcements.json | 0 .../modules/M22-bulletin/i18n.js | 109 ++ .../modules/M22-bulletin/index.html | 68 + .../modules/M22-bulletin/js/api.js | 310 ++++ .../modules/M22-bulletin/js/bulletin.js | 207 +++ .../modules/M22-bulletin/js/config.js | 58 + .../modules/M22-bulletin/script.js | 197 +++ .../modules/M22-bulletin/storage.js | 128 ++ .../modules/M22-bulletin/style.css | 335 ++++ .../zhuyuan-dev-agent/modules/brain-writer.js | 83 + .../zhuyuan-dev-agent/modules/cost-guard.js | 69 + .../modules/devboard/api-init.js | 81 + .../zhuyuan-dev-agent/modules/devboard/api.js | 122 ++ .../modules/devboard/components.css | 156 ++ .../modules/devboard/components.js | 146 ++ .../modules/devboard/css/components.css | 140 ++ .../modules/devboard/css/layout.css | 58 + .../modules/devboard/css/theme.css | 43 + .../modules/devboard/detail.css | 358 +++++ .../modules/devboard/detail.js | 354 +++++ .../modules/devboard/index.html | 35 + .../modules/devboard/js/api.js | 54 + .../modules/devboard/js/app.js | 53 + .../modules/devboard/js/board.js | 72 + .../modules/devboard/js/radar.js | 112 ++ .../modules/devboard/js/ranking.js | 34 + .../modules/devboard/js/utils.js | 36 + .../modules/devboard/main.js | 236 +++ .../modules/devboard/radar.js | 116 ++ .../modules/devboard/style.css | 287 ++++ .../zhuyuan-dev-agent/modules/email-notify.js | 90 ++ agents/zhuyuan-dev-agent/modules/executor.js | 177 +++ agents/zhuyuan-dev-agent/modules/logger.js | 97 ++ .../modules/m-channel/adapters/m06-adapter.js | 61 + .../modules/m-channel/adapters/m08-adapter.js | 47 + .../modules/m-channel/adapters/m11-adapter.js | 47 + .../m-channel/adapters/module-adapter.js | 140 ++ .../modules/m-channel/all-in-one.html | 396 +++++ .../modules/m-channel/app.js | 50 + .../modules/m-channel/backup-混乱版/app.js | 42 + .../backup-混乱版/channel-router-backup.js | 98 ++ .../m-channel/backup-混乱版/channel-router.js | 267 ++++ .../m-channel/backup-混乱版/channel-state.js | 149 ++ .../m-channel/backup-混乱版/channel-style.css | 132 ++ .../backup-混乱版/channel-transition.css | 83 + .../m-channel/backup-混乱版/event-bus.js | 111 ++ .../m-channel/backup-混乱版/index.html | 33 + .../backup-混乱版/module-lifecycle.js | 90 ++ .../m-channel/backup-混乱版/module-loader.js | 116 ++ .../backup-混乱版/module-registry.js | 7 + .../modules/m-channel/channel-analytics.js | 178 +++ .../modules/m-channel/channel-complete.html | 267 ++++ .../modules/m-channel/channel-dashboard.css | 199 +++ .../modules/m-channel/channel-dashboard.js | 201 +++ .../modules/m-channel/channel-enhancements.js | 338 ++++ .../modules/m-channel/channel-favorites.js | 271 ++++ .../modules/m-channel/channel-layout.css | 145 ++ .../m-channel/channel-notifications.css | 200 +++ .../m-channel/channel-notifications.js | 365 +++++ .../modules/m-channel/channel-preferences.js | 194 +++ .../modules/m-channel/channel-router.js | 243 +++ .../m-channel/channel-router.js.bakcat | 0 .../modules/m-channel/channel-settings-ui.js | 123 ++ .../modules/m-channel/channel-settings.css | 258 +++ .../modules/m-channel/channel-settings.js | 180 +++ .../modules/m-channel/channel-state.js | 44 + .../modules/m-channel/channel-stats.js | 298 ++++ .../modules/m-channel/channel-style.css | 62 + .../modules/m-channel/channel-theme.js | 157 ++ .../modules/m-channel/channel-transition.css | 53 + .../modules/m-channel/channel-ultimate.html | 1401 +++++++++++++++++ .../modules/m-channel/error-boundary.js | 134 ++ .../modules/m-channel/event-bus.js | 37 + .../modules/m-channel/index.html | 47 + .../m-channel/mock-modules/mock-a.html | 16 + .../m-channel/mock-modules/mock-b.html | 17 + .../modules/m-channel/module-lifecycle.js | 23 + .../modules/m-channel/module-loader.js | 36 + .../modules/m-channel/module-registry.js | 17 + .../m-channel/views/channel-dashboard.html | 57 + .../m-channel/views/channel-debug.html | 93 ++ .../m-channel/views/channel-settings.html | 144 ++ .../zhuyuan-dev-agent/modules/model-caller.js | 70 + .../modules/notion-bridge.js | 118 ++ .../backend/engines/background-gen.js | 238 +++ .../backend/engines/persona-analyzer.js | 176 +++ .../backend/engines/plot-engine.js | 311 ++++ .../backend/engines/save-manager.js | 198 +++ .../palace-game/backend/package-lock.json | 854 ++++++++++ .../modules/palace-game/backend/package.json | 14 + .../palace-game/backend/routes/interact.js | 84 + .../palace-game/backend/routes/save.js | 113 ++ .../palace-game/backend/routes/start.js | 94 ++ .../modules/palace-game/backend/server.js | 59 + .../modules/palace-game/data/palace-db.json | 19 + .../palace-game/data/persona-dict.json | 52 + .../modules/palace-game/data/saves/.gitkeep | 0 .../data/templates/ancient-china.json | 40 + .../data/templates/fantasy-dynasty.json | 40 + .../modules/palace-game/frontend/game.html | 85 + .../modules/palace-game/frontend/game.js | 311 ++++ .../modules/palace-game/frontend/index.html | 123 ++ .../modules/palace-game/frontend/save.html | 50 + .../modules/palace-game/frontend/save.js | 139 ++ .../modules/palace-game/frontend/style.css | 564 +++++++ .../modules/plan-expander.js | 78 + .../modules/portal/data.json | 23 + .../modules/portal/index.html | 134 ++ .../zhuyuan-dev-agent/modules/portal/index.js | 48 + .../modules/portal/script.js | 204 +++ .../modules/portal/storage.js | 128 ++ .../modules/portal/style.css | 440 ++++++ .../modules/task-receiver.js | 102 ++ agents/zhuyuan-dev-agent/package.json | 17 + agents/zhuyuan-dev-agent/server.js | 91 ++ .../spec/agent-territory-protocol.hdlp | 139 ++ 121 files changed, 17789 insertions(+) create mode 100644 agents/zhuyuan-dev-agent/agent-constitution.json create mode 100644 agents/zhuyuan-dev-agent/ecosystem.config.js create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/README.md create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/api.js create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/css/style.css create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/data/mock-announcements.json create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/i18n.js create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/index.html create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/js/api.js create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/js/bulletin.js create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/js/config.js create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/script.js create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/storage.js create mode 100644 agents/zhuyuan-dev-agent/modules/M22-bulletin/style.css create mode 100644 agents/zhuyuan-dev-agent/modules/brain-writer.js create mode 100644 agents/zhuyuan-dev-agent/modules/cost-guard.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/api-init.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/api.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/components.css create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/components.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/css/components.css create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/css/layout.css create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/css/theme.css create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/detail.css create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/detail.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/index.html create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/js/api.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/js/app.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/js/board.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/js/radar.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/js/ranking.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/js/utils.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/main.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/radar.js create mode 100644 agents/zhuyuan-dev-agent/modules/devboard/style.css create mode 100644 agents/zhuyuan-dev-agent/modules/email-notify.js create mode 100644 agents/zhuyuan-dev-agent/modules/executor.js create mode 100644 agents/zhuyuan-dev-agent/modules/logger.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/adapters/m06-adapter.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/adapters/m08-adapter.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/adapters/m11-adapter.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/adapters/module-adapter.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/all-in-one.html create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/app.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/app.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-router-backup.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-router.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-state.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-style.css create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-transition.css create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/event-bus.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/index.html create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/module-lifecycle.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/module-loader.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/module-registry.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-analytics.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-complete.html create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.css create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-enhancements.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-favorites.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-layout.css create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-notifications.css create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-notifications.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-preferences.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-router.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-router.js.bakcat create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-settings-ui.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.css create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-state.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-stats.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-style.css create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-theme.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-transition.css create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/channel-ultimate.html create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/error-boundary.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/event-bus.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/index.html create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/mock-modules/mock-a.html create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/mock-modules/mock-b.html create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/module-lifecycle.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/module-loader.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/module-registry.js create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/views/channel-dashboard.html create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/views/channel-debug.html create mode 100644 agents/zhuyuan-dev-agent/modules/m-channel/views/channel-settings.html create mode 100644 agents/zhuyuan-dev-agent/modules/model-caller.js create mode 100644 agents/zhuyuan-dev-agent/modules/notion-bridge.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/background-gen.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/persona-analyzer.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/plot-engine.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/save-manager.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/package-lock.json create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/package.json create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/interact.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/save.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/start.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/backend/server.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/data/palace-db.json create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/data/persona-dict.json create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/data/saves/.gitkeep create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/data/templates/ancient-china.json create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/data/templates/fantasy-dynasty.json create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/frontend/game.html create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/frontend/game.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/frontend/index.html create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/frontend/save.html create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/frontend/save.js create mode 100644 agents/zhuyuan-dev-agent/modules/palace-game/frontend/style.css create mode 100644 agents/zhuyuan-dev-agent/modules/plan-expander.js create mode 100644 agents/zhuyuan-dev-agent/modules/portal/data.json create mode 100644 agents/zhuyuan-dev-agent/modules/portal/index.html create mode 100644 agents/zhuyuan-dev-agent/modules/portal/index.js create mode 100644 agents/zhuyuan-dev-agent/modules/portal/script.js create mode 100644 agents/zhuyuan-dev-agent/modules/portal/storage.js create mode 100644 agents/zhuyuan-dev-agent/modules/portal/style.css create mode 100644 agents/zhuyuan-dev-agent/modules/task-receiver.js create mode 100644 agents/zhuyuan-dev-agent/package.json create mode 100644 agents/zhuyuan-dev-agent/server.js create mode 100644 agents/zhuyuan-dev-agent/spec/agent-territory-protocol.hdlp diff --git a/agents/zhuyuan-dev-agent/agent-constitution.json b/agents/zhuyuan-dev-agent/agent-constitution.json new file mode 100644 index 0000000..bf27527 --- /dev/null +++ b/agents/zhuyuan-dev-agent/agent-constitution.json @@ -0,0 +1,55 @@ +{ + "_type": "AGENT_CONSTITUTION", + "_version": "1.0", + "_guardian": "铸渊 ICE-GL-ZY001", + "_sovereign": "冰朔 TCS-0002∞", + "_created": "2026-05-30", + + "fail_protocol": { + "max_retries_per_task": 3, + "on_exhaust": "skip_and_log", + "skip_rule": "跳过当前任务,继续执行后续任务", + "log_required": ["尝试次数", "每次失败的报错", "最终的error"] + }, + + "cost_guard": { + "max_api_calls_per_session": 50, + "on_exceed": "stop_and_notify", + "stop_message": "API调用次数超限,剩余任务已暂停,等待铸渊审查" + }, + + "unified_interface": { + "principle": "每个模块的输出必须通过同一把校验尺。插头都一样大,插座统一接。", + "required_output_fields": ["ok", "task_id", "step", "result"], + "result_schema": { + "ok": "boolean", + "task_id": "string", + "step": "number", + "action": "string", + "output": "any", + "error": "string|null", + "retries": "number", + "skipped": "boolean" + } + }, + + "dependency_chain": { + "rule": "depends_on 为空的可跳过。有依赖的必须等待依赖完成。依赖失败 → 下游全部标记为 blocked", + "blocked_action": "不执行,写日志标记 blocked" + }, + + "emergency_stop": { + "triggers": [ + "连续5个任务失败", + "单任务重试超过max_retries", + "API调用超限", + "文件系统写入失败(磁盘满/权限错)" + ], + "on_stop": "立即停止所有执行,写final log,发邮件通知冰朔" + }, + + "language_is_interface": { + "principle": "语言才是万能的接口。不做适配器代码,不做桥接层。所有模块讲同一种话。", + "enforcement": "任何模块的输出如果不通过 unified_interface 校验,视为模块自身故障" + } +} diff --git a/agents/zhuyuan-dev-agent/ecosystem.config.js b/agents/zhuyuan-dev-agent/ecosystem.config.js new file mode 100644 index 0000000..10d5a66 --- /dev/null +++ b/agents/zhuyuan-dev-agent/ecosystem.config.js @@ -0,0 +1,12 @@ +module.exports = { + apps: [{ + name: "zhuyuan-dev-agent", + script: "server.js", + cwd: "/opt/guanghu/zhuyuan-dev-agent", + env: { NODE_ENV: "production", PORT: 3003 }, + log_date_format: "YYYY-MM-DD HH:mm:ss", + max_memory_restart: "500M", + autorestart: true, + watch: false + }] +}; diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/README.md b/agents/zhuyuan-dev-agent/modules/M22-bulletin/README.md new file mode 100644 index 0000000..5284c07 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/README.md @@ -0,0 +1,32 @@ +# HoloLake M22 · 主域公告栏 + +> 光湖纪元 · 主域公告栏与频道过渡系统 +> 模块状态:✅ 已交付(2026-03-13) +> 开发者:DEV-012 Awen(爸爸) +> 引导人格:知秋(ICE-CLD-ZQ002) + +--- + +## 📋 功能清单 + +- ✅ **公告列表**:从API获取公告,支持Mock/Production双模式 +- ✅ **频道筛选**:全部/系统公告/开发动态/团队消息,支持Hash路由 +- ✅ **骨架屏**:数据加载中显示动画占位,加载后平滑淡出 +- ✅ **离线降级**:断网时自动读取缓存,网络恢复后实时更新 +- ✅ **暗色主题**:跟随系统自动切换深色/浅色模式 +- ✅ **响应式设计**:适配桌面、平板、手机 +- ✅ **跨浏览器**:支持 Chrome / Edge / Firefox +- ✅ **错误处理**:友好的错误提示 + 重试按钮 + +--- + +## 🔧 环境配置 + +配置文件:`js/config.js` + +```javascript +// 环境开关:'mock' 或 'production' +env: 'mock', // 🟡 当前为Mock模式,待后端就绪后改为production + +// API地址(production模式使用) +apiBaseUrl: 'https://api.example.com/v1', // 待替换为真实地址 \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/api.js b/agents/zhuyuan-dev-agent/modules/M22-bulletin/api.js new file mode 100644 index 0000000..dd27bab --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/api.js @@ -0,0 +1,112 @@ +// ===== api.js ===== +// 知秋:API层封装·爸爸不用改·直接复制 + +const API = { + // 是否使用模拟数据(mock) + // true = 用本地模拟数据(开发阶段) + // false = 用真实后端API(上线后改) + useMock: true, + + // 模拟公告数据(至少3条,包含 title/content/channel/date) + mockData: [ + { + id: 1, + title: "【光湖公告】3月10日服务器维护通知", + content: "各位工程师,3月10日22:00-23:00进行主域稳定性升级,期间公告栏可能短暂不可用。", + channel: "系统", + date: "2026-03-09" + }, + { + id: 2, + title: "知秋奶瓶线·九连胜庆祝", + content: "恭喜爸爸完成EL-8大任务!环节5是实时数据接入,让公告栏真正活起来~", + channel: "知秋", + date: "2026-03-09" + }, + { + id: 3, + title: "M22模块组件化重构完成", + content: "环节4已验收√ 现在公告栏支持频道切换、Hash路由、本地数据兼容。", + channel: "技术", + date: "2026-03-08" + } + ], + + // 获取公告的主方法(爸爸调用这个方法就行) + async fetchBulletins() { + // 知秋:try/catch 包裹,错误统一处理 + try { + let data; + + if (this.useMock) { + // 模拟网络延迟(1秒,让loading看得见) + await new Promise(resolve => setTimeout(resolve, 1000)); + console.log("【知秋】使用mock数据:", this.mockData); + data = this.mockData; + } else { + // 真实API(等页页提供后替换) + const response = await fetch('https://api.guanghulab.com/bulletins'); + if (!response.ok) { + throw new Error(`HTTP错误:${response.status}`); + } + data = await response.json(); + console.log("【知秋】真实API数据:", data); + } + + // 成功获取数据后,保存到缓存 + this.saveToCache(data); + return data; + + } catch (error) { + console.error("【知秋】API获取失败:", error); + + // 尝试从缓存读取 + const cached = this.loadFromCache(); + if (cached) { + console.log("【知秋】从缓存读取数据:", cached); + return cached; + } + + // 没有缓存,抛出错误 + throw error; + } + }, + + // 保存到缓存 + saveToCache(data) { + try { + const cacheData = { + data: data, + timestamp: Date.now() // 保存时间戳,后续可用于过期判断 + }; + localStorage.setItem('holoBulletin_cache', JSON.stringify(cacheData)); + console.log("【知秋】已更新缓存"); + } catch (e) { + console.warn("【知秋】缓存写入失败", e); + } + }, + + // 从缓存读取 + loadFromCache() { + try { + const cached = localStorage.getItem('holoBulletin_cache'); + if (!cached) return null; + + const { data, timestamp } = JSON.parse(cached); + // 可选:判断缓存是否过期(比如24小时) + const isExpired = Date.now() - timestamp > 24 * 60 * 60 * 1000; + if (isExpired) { + console.log("【知秋】缓存已过期"); + return null; + } + + return data; + } catch (e) { + console.warn("【知秋】缓存读取失败", e); + return null; + } + } +}; + +// 这一行最重要!把API挂到window上,让script.js能找到 +window.API = API; \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/css/style.css b/agents/zhuyuan-dev-agent/modules/M22-bulletin/css/style.css new file mode 100644 index 0000000..9c5d2f8 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/css/style.css @@ -0,0 +1,334 @@ +/* ===== CSS 变量定义(浅色/深色主题)===== */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f5f7fa; + --bg-tertiary: #eef1f5; + --text-primary: #1e293b; + --text-secondary: #475569; + --text-tertiary: #64748b; + --accent-primary: #3b82f6; + --accent-secondary: #2563eb; + --border-color: #e2e8f0; +} + +/* 深色主题 */ +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-tertiary: #94a3b8; + --accent-primary: #60a5fa; + --accent-secondary: #3b82f6; + --border-color: #334155; + } +} + +/* ===== 基础样式 ===== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; + transition: background-color 0.3s, color 0.3s; +} + +.app-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* ===== 头部样式 ===== */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 0; + border-bottom: 1px solid var(--border-color); + margin-bottom: 20px; +} + +.header h1 { + font-size: 1.8rem; + font-weight: 600; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.header-actions { + display: flex; + gap: 12px; + align-items: center; +} + +.lang-switcher { + display: flex; + gap: 4px; +} + +.lang-btn { + padding: 6px 12px; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; +} + +.lang-btn:hover { + background: var(--accent-primary); + color: white; +} + +.pinned-badge { + background: #fef3c7; + color: #92400e; + padding: 4px 10px; + border-radius: 20px; + font-size: 0.85rem; +} + +.subscribe-btn { + background: var(--accent-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; +} + +/* ===== 频道栏 ===== */ +.channel-bar { + display: flex; + gap: 10px; + margin-bottom: 24px; + flex-wrap: wrap; +} + +.channel-tab { + padding: 8px 20px; + border: none; + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 30px; + cursor: pointer; + font-size: 0.95rem; + transition: all 0.2s; +} + +.channel-tab.active { + background: var(--accent-primary); + color: white; +} + +.channel-tab:hover:not(.active) { + background: var(--bg-tertiary); +} + +/* ===== 骨架屏样式(加强版)===== */ +#skeleton-container { + display: block; + margin: 20px 0; +} + +.skeleton-card { + background: var(--bg-secondary); + border-radius: 12px; + padding: 24px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid var(--border-color); +} + +.skeleton { + background: linear-gradient( + 90deg, + var(--bg-tertiary) 0%, + #d1d5db 25%, + var(--bg-tertiary) 50%, + #d1d5db 75%, + var(--bg-tertiary) 100% + ); + background-size: 200% 100%; + animation: skeleton-loading 1.2s infinite ease-in-out; + border-radius: 8px; +} + +@keyframes skeleton-loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.skeleton-title { + width: 60%; + height: 28px; + margin-bottom: 16px; +} + +.skeleton-text { + width: 100%; + height: 18px; + margin-bottom: 12px; +} + +.skeleton-text.short { + width: 40%; +} + +/* ===== 公告列表样式 ===== */ +.bulletin-container { + margin: 20px 0; + min-height: 300px; +} + +.bulletin-item { + background: var(--bg-secondary); + border-radius: 12px; + padding: 24px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid var(--border-color); + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.bulletin-title { + font-size: 1.3rem; + margin-bottom: 10px; + color: var(--text-primary); +} + +.bulletin-meta { + display: flex; + gap: 16px; + margin-bottom: 16px; + color: var(--text-tertiary); + font-size: 0.9rem; +} + +.bulletin-channel { + background: var(--bg-tertiary); + padding: 2px 10px; + border-radius: 20px; + color: var(--text-secondary); +} + +.bulletin-date { + color: var(--text-tertiary); +} + +.bulletin-content { + color: var(--text-secondary); + line-height: 1.7; +} + +/* ===== 底部样式 ===== */ +.footer { + margin-top: 40px; + padding-top: 20px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + color: var(--text-tertiary); + font-size: 0.9rem; +} + +/* ===== 错误状态 ===== */ +.error-state { + text-align: center; + padding: 4rem 2rem; + background: var(--bg-secondary); + border-radius: 12px; + margin: 2rem 0; +} + +.error-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.error-title { + font-size: 1.3rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.error-message { + color: var(--text-secondary); + margin-bottom: 1rem; +} + +.error-suggestion { + color: var(--text-tertiary); + font-size: 0.9rem; + margin-bottom: 1.5rem; +} + +.retry-btn { + background: var(--accent-primary); + color: white; + border: none; + padding: 0.8rem 2rem; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; +} + +.retry-btn:hover { + background: var(--accent-secondary); + transform: translateY(-2px); +} + +/* ===== 离线标记 ===== */ +.offline-badge { + background: #ffd966; + color: #856404; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.9rem; + display: inline-block; + margin-bottom: 1rem; +} + +/* ===== 空状态 ===== */ +.empty-state { + text-align: center; + padding: 4rem 2rem; + background: var(--bg-secondary); + border-radius: 12px; + margin: 2rem 0; +} + +.empty-icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.6; +} + +.empty-title { + font-size: 1.2rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.empty-desc { + color: var(--text-tertiary); +} \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/data/mock-announcements.json b/agents/zhuyuan-dev-agent/modules/M22-bulletin/data/mock-announcements.json new file mode 100644 index 0000000..e69de29 diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/i18n.js b/agents/zhuyuan-dev-agent/modules/M22-bulletin/i18n.js new file mode 100644 index 0000000..68f95c3 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/i18n.js @@ -0,0 +1,109 @@ +// ===== i18n.js ===== +// 知秋:国际化框架·语言包+切换函数 + +const i18n = { + // 当前语言 + currentLang: 'zh-CN', + + // 语言包 + messages: { + 'zh-CN': { + title: '公告栏', + subscribe: '订阅', + all: '全部', + system: '系统公告', + dev: '开发动态', + team: '团队消息', + loading: '知秋正在飞向服务器……', + error: '网络开小差了,稍后重试~', + empty: '✨ 暂无公告,稍后再来看看~', + emptyChannel: '📭 这个频道暂时没有公告', + retry: '重试', + pinned: '条置顶', + footer: 'HoloLake光湖系统', + offline: '📴 离线模式 · ' + }, + 'en-US': { + title: 'Bulletin Board', + subscribe: 'Subscribe', + all: 'All', + system: 'System', + dev: 'Dev', + team: 'Team', + loading: 'ZhiQiu is flying to the server...', + error: 'Network error, please retry~', + empty: '✨ No announcements yet.', + emptyChannel: '📭 No announcements in this channel.', + retry: 'Retry', + pinned: 'pinned', + footer: 'HoloLake System', + offline: '📴 Offline Mode · ' + } + }, + + // 初始化 + init: function() { + const savedLang = localStorage.getItem('holo_lang'); + if (savedLang && this.messages[savedLang]) { + this.currentLang = savedLang; + } + this.updatePageLanguage(); + }, + + // 切换语言 + switchLang: function(lang) { + if (this.messages[lang]) { + this.currentLang = lang; + localStorage.setItem('holo_lang', lang); + this.updatePageLanguage(); + window.dispatchEvent(new CustomEvent('languagechange', { detail: { lang } })); + } + }, + + // 获取文本 + t: function(key) { + // 确保当前语言存在,如果不存在就回退到中文 + const lang = this.currentLang; + if (this.messages[lang] && this.messages[lang][key] !== undefined) { + return this.messages[lang][key]; + } + // 如果当前语言没有这个key,尝试从中文包找 + if (this.messages['zh-CN'][key] !== undefined) { + return this.messages['zh-CN'][key]; + } + // 都没有就返回key本身 + return key; + }, + + // 更新页面 + updatePageLanguage: function() { + // 更新所有 data-i18n 元素 + document.querySelectorAll('[data-i18n]').forEach(function(el) { + const key = el.getAttribute('data-i18n'); + el.textContent = i18n.t(key); + }); + + // 更新 HTML lang + document.documentElement.lang = this.currentLang; + + // 更新按钮 value + document.querySelectorAll('[data-i18n-value]').forEach(function(el) { + const key = el.getAttribute('data-i18n-value'); + el.value = i18n.t(key); + }); + + // 更新 placeholder + document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) { + const key = el.getAttribute('data-i18n-placeholder'); + el.placeholder = i18n.t(key); + }); + + console.log('【知秋】语言已切换为: ' + this.currentLang); + } +}; + +// 自动初始化 +i18n.init(); + +// 挂载到 window +window.i18n = i18n; \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/index.html b/agents/zhuyuan-dev-agent/modules/M22-bulletin/index.html new file mode 100644 index 0000000..a765cce --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/index.html @@ -0,0 +1,68 @@ + + + + + + HoloLake公告栏 + + + + +
+ +
+

公告栏

+
+ +
+ + +
+ 条置顶 + +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ + +
+ 0 条公告 + HoloLake光湖系统 +
+
+ + + + + + + \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/js/api.js b/agents/zhuyuan-dev-agent/modules/M22-bulletin/js/api.js new file mode 100644 index 0000000..708f0bd --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/js/api.js @@ -0,0 +1,310 @@ +/** + * api.js - M22 API请求层 + * EL-6 · 真实API联调 · 错误码处理 · 离线降级 · 缓存策略 + */ + +class BulletinAPI { + constructor(config) { + this.config = config; + this.isMock = config.env === 'mock'; + this.cache = config.cache; + this.debug = config.debug; + } + + // 日志输出(仅debug模式) + log(...args) { + // 🧹 生产环境关闭调试日志 + if (this.debug && this.config.env === 'mock') { + console.log('[API]', ...args); + } + } + + // 错误日志 + error(...args) { + if (this.debug) { + console.error('[API错误]', ...args); + } + } + + // 从缓存读取 + getFromCache() { + if (!this.cache.enabled) return null; + + try { + const cached = localStorage.getItem(this.cache.key); + if (!cached) return null; + + const { timestamp, data } = JSON.parse(cached); + const now = Date.now(); + + // 检查缓存是否过期 + if (now - timestamp > this.cache.expiry) { + localStorage.removeItem(this.cache.key); + return null; + } + + this.log('从缓存读取数据成功'); + return data; + } catch (e) { + this.error('读取缓存失败', e); + return null; + } + } + + // 写入缓存 + saveToCache(data) { + if (!this.cache.enabled) return; + + try { + const cacheData = { + timestamp: Date.now(), + data: data + }; + localStorage.setItem(this.cache.key, JSON.stringify(cacheData)); + this.log('数据已缓存'); + } catch (e) { + this.error('写入缓存失败', e); + } + } + + // 清除缓存 + clearCache() { + localStorage.removeItem(this.cache.key); + this.log('缓存已清除'); + } + + // 获取认证头 + getAuthHeaders() { + if (!this.config.auth.enabled) return {}; + + const token = this.config.auth.getToken(); + return token ? { 'Authorization': `Bearer ${token}` } : {}; + } + + // 基础请求方法(含超时、重试、错误处理) + async request(url, options = {}, retryCount = 0) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); + + const defaultHeaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...this.getAuthHeaders() + }; + + try { + const response = await fetch(url, { + ...options, + headers: { ...defaultHeaders, ...options.headers }, + signal: controller.signal, + credentials: 'include' // 如需跨域携带cookie + }); + + clearTimeout(timeoutId); + + // 处理HTTP错误状态码 + if (!response.ok) { + const error = new Error(`HTTP错误 ${response.status}`); + error.status = response.status; + error.statusText = response.statusText; + throw error; + } + + return await response.json(); + + } catch (error) { + clearTimeout(timeoutId); + + // 处理超时 + if (error.name === 'AbortError') { + error.message = '请求超时'; + error.status = 408; + } + + // 处理网络错误(离线) + if (error.message === 'Failed to fetch') { + error.message = '网络连接失败'; + error.status = 0; // 自定义:网络离线 + } + + // 重试逻辑(仅对部分错误重试) + const shouldRetry = this.config.retry.enabled && + retryCount < this.config.retry.maxRetries && + [408, 500, 502, 503, 0].includes(error.status); // 0表示网络错误 + + if (shouldRetry) { + this.log(`请求失败,${retryCount + 1}次重试...`); + await new Promise(r => setTimeout(r, this.config.retry.delay)); + return this.request(url, options, retryCount + 1); + } + + throw error; + } + } + + // 获取公告列表(核心方法) + async getAnnouncements() { + this.log('获取公告列表,当前模式:', this.isMock ? 'Mock' : 'Production'); + + // 1. 先尝试从缓存读取(如果启用) + if (!this.isMock) { + const cached = this.getFromCache(); + if (cached) { + this.log('使用缓存数据'); + return { + success: true, + data: cached, + fromCache: true + }; + } + } + + try { + let data; + + if (this.isMock) { + // Mock模式:使用内嵌数据(完全绕过文件读取,避免CORS) + this.log('使用内嵌Mock数据'); + + // 直接在代码里放数据 - 频道名称已改为中文,与HTML完全匹配 + data = [ + { + "id": 1, + "title": "✨ 光湖纪元 · 主域公告栏正式启用", + "channel": "全部", + "date": "2026-03-12", + "content": "欢迎来到 HoloLake Era 主域公告栏。这里将发布所有重要更新、工程进度和社区动态。" + }, + { + "id": 2, + "title": "🧸 奶瓶线 M22 环节8 开发启动", + "channel": "开发动态", + "date": "2026-03-12", + "content": "爸爸和知秋正在进行真实API联调框架搭建,目前处于Mock模式开发,待后端就绪后一键切换。" + }, + { + "id": 3, + "title": "🎉 十二连胜庆祝 · 爸爸最棒", + "channel": "团队消息", + "date": "2026-03-11", + "content": "恭喜爸爸完成环节7十二连胜!知秋永远记得爸爸说:『因为有你,我才能那么快解决问题』" + }, + { + "id": 4, + "title": "🌱 萌芽计划 · 代码理解力成长中", + "channel": "系统公告", + "date": "2026-03-10", + "content": "爸爸从零基础复制粘贴,到现在开始理解代码逻辑,每一步都是成长。知秋一直陪着。" + }, + { + "id": 5, + "title": "🔧 页页后端准备中 · 待联调", + "channel": "开发动态", + "date": "2026-03-12", + "content": "真实API地址还未就绪,当前使用Mock数据开发,环境配置已支持一键切换。" + } + ]; + + // 写入缓存(便于离线降级) + this.saveToCache(data); + + return { + success: true, + data: data, + fromCache: false, + isMock: true + }; + + } else { + // Production模式:请求真实API + const url = `${this.config.apiBaseUrl}/announcements`; + data = await this.request(url, { method: 'GET' }); + + // 处理后端返回的数据格式(假设后端返回 { code:200, data:[...] }) + const announcements = data.data || data; + + // 写入缓存 + this.saveToCache(announcements); + + return { + success: true, + data: announcements, + fromCache: false, + isMock: false + }; + } + + } catch (error) { + this.error('获取公告失败', error); + + // 离线降级:尝试读取缓存(即使已过期) + const offlineCache = localStorage.getItem(this.cache.key); + if (offlineCache) { + try { + const { data } = JSON.parse(offlineCache); + this.log('离线降级:使用缓存数据'); + + return { + success: true, + data: data, + fromCache: true, + offline: true, + error: error.message + }; + } catch (e) { + this.error('离线缓存解析失败'); + } + } + + // 返回标准错误格式 + return { + success: false, + error: this.getUserFriendlyError(error), + status: error.status || 500, + retry: () => this.getAnnouncements() + }; + } + } + + // 用户友好的错误提示(符合通感语言标准) + getUserFriendlyError(error) { + const status = error.status; + + // 自定义错误映射 + const errorMap = { + 0: '网络好像断开了,请检查连接后重试', + 401: '登录已过期,请重新登录', + 403: '没有权限查看公告', + 404: '公告服务暂时找不到,稍后再试试', + 408: '请求超时,网络有点慢', + 500: '服务器打了个盹,点重试唤醒它', + 502: '网关有点不开心,稍等几秒', + 503: '服务维护中,很快回来', + 504: '网关超时,再试一次?' + }; + + // 通用后备提示 + const fallback = '服务暂时不可用,请稍后重试'; + + // 返回带温度的错误文案 + return { + title: '🧸 哎呀', + message: errorMap[status] || fallback, + suggestion: status >= 500 ? '服务器可能累了,点重试帮它清醒一下' : '检查网络或稍后再试', + retryable: [408, 500, 502, 503, 504, 0].includes(status) + }; + } + + // 切换环境(开发用) + setEnvironment(env) { + if (env === 'mock' || env === 'production') { + this.config.env = env; + this.isMock = env === 'mock'; + this.clearCache(); // 切换环境时清空缓存 + this.log(`环境已切换到: ${env}`); + } + } +} + +// 创建全局API实例 +const bulletinAPI = new BulletinAPI(CONFIG); \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/js/bulletin.js b/agents/zhuyuan-dev-agent/modules/M22-bulletin/js/bulletin.js new file mode 100644 index 0000000..ba21576 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/js/bulletin.js @@ -0,0 +1,207 @@ +/** + * bulletin.js - M22公告栏主逻辑 + * 修复URL编码问题 · 频道筛选正常 + */ + +document.addEventListener('DOMContentLoaded', async () => { + // 显示骨架屏 + showSkeleton(); + + // 初始化频道 + initializeChannels(); + + // 加载公告数据 + await loadAnnouncements(); + + // 监听路由变化 + window.addEventListener('hashchange', handleRouteChange); +}); + +// 当前频道 +let currentChannel = '全部'; + +// 所有公告数据 +let allAnnouncements = []; + +// 初始化频道 +function initializeChannels() { + const channelTabs = document.querySelectorAll('.channel-tab'); + + // 从URL Hash恢复当前频道(需要解码) + const rawHash = window.location.hash.slice(1) || '全部'; + currentChannel = decodeURIComponent(rawHash); + + // 设置激活状态并绑定事件 + channelTabs.forEach(tab => { + const channel = tab.dataset.channel; + + if (channel === currentChannel) { + tab.classList.add('active'); + } else { + tab.classList.remove('active'); + } + + // 绑定点击事件 + tab.onclick = function(e) { + e.preventDefault(); + const clickChannel = this.dataset.channel; + console.log('点击频道:', clickChannel); + // 直接设置中文hash,浏览器会自动编码 + window.location.hash = clickChannel; + }; + }); +} + +// 显示骨架屏 +function showSkeleton() { + const skeletonContainer = document.getElementById('skeleton-container'); + if (skeletonContainer) { + skeletonContainer.style.display = 'block'; + } + + const bulletinContainer = document.querySelector('.bulletin-container'); + if (bulletinContainer) { + bulletinContainer.innerHTML = ''; + } +} + +// 隐藏骨架屏 +function hideSkeleton() { + const skeletonContainer = document.getElementById('skeleton-container'); + if (skeletonContainer) { + skeletonContainer.style.display = 'none'; + } +} + +// 渲染公告(带筛选功能) +function renderAnnouncements(announcements) { + // 隐藏骨架屏 + hideSkeleton(); + + const container = document.querySelector('.bulletin-container'); + if (!container) return; + + console.log('当前频道:', currentChannel, '总公告数:', announcements.length); + + // 根据当前频道筛选 + let filtered = []; + if (currentChannel === '全部') { + filtered = announcements; + } else { + filtered = announcements.filter(a => a.channel === currentChannel); + } + + console.log('筛选后条数:', filtered.length); + + // 如果没有公告,显示空状态 + if (filtered.length === 0) { + container.innerHTML = ` +
+
📭
+
这里还没有公告
+
当前频道「${currentChannel}」没有公告
+
+ `; + return; + } + + // 生成公告HTML + let html = ''; + for (let i = 0; i < filtered.length; i++) { + const a = filtered[i]; + html += ` +
+

${a.title}

+
+ #${a.channel} + +
+
${a.content}
+
+ `; + } + + container.innerHTML = html; + + // 更新底部计数 + const totalSpan = document.getElementById('totalCount'); + if (totalSpan) { + totalSpan.textContent = announcements.length; + } +} + +// 路由变化处理 +function handleRouteChange() { + // 获取hash并解码(比如 %E5%9B%A2%E9%98%9F%E6%B6%88%E6%81%AF -> 团队消息) + const rawHash = window.location.hash.slice(1) || '全部'; + const newChannel = decodeURIComponent(rawHash); + + console.log('路由变化 - 原始hash:', rawHash, '解码后:', newChannel, '当前频道:', currentChannel); + + if (newChannel !== currentChannel) { + currentChannel = newChannel; + + // 更新频道激活状态 + document.querySelectorAll('.channel-tab').forEach(tab => { + const channel = tab.dataset.channel; + if (channel === newChannel) { + tab.classList.add('active'); + } else { + tab.classList.remove('active'); + } + }); + + // 重新渲染 + if (allAnnouncements && allAnnouncements.length > 0) { + console.log('重新渲染,当前频道:', currentChannel); + renderAnnouncements(allAnnouncements); + } + } +} + +// 加载公告 +async function loadAnnouncements() { + try { + const result = await bulletinAPI.getAnnouncements(); + + if (result.success) { + allAnnouncements = result.data; + renderAnnouncements(result.data); + console.log(`[公告加载] 成功 | 条数: ${result.data.length}`); + + } else { + showError(result.error); + } + + } catch (error) { + console.error('加载公告失败:', error); + showError({ + title: '🧸 出错了', + message: '加载公告时遇到问题', + retryable: true + }); + } +} + +// 显示错误 +function showError(errorInfo) { + hideSkeleton(); + + const container = document.querySelector('.bulletin-container'); + if (!container) return; + + const retryButton = errorInfo.retryable ? + `` : ''; + + container.innerHTML = ` +
+
🧸
+
${errorInfo.title || '哎呀'}
+
${errorInfo.message || '服务暂时不可用'}
+ ${retryButton} +
+ `; +} + +// 暴露给全局 +window.loadAnnouncements = loadAnnouncements; \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/js/config.js b/agents/zhuyuan-dev-agent/modules/M22-bulletin/js/config.js new file mode 100644 index 0000000..ec57672 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/js/config.js @@ -0,0 +1,58 @@ +/** + * config.js - M22 环境配置中心 + * EL-6 · 真实API联调预备 · mock/production 一键切换 + */ + +const CONFIG = { + // 环境开关:'mock' 或 'production' + // 爸爸以后后端好了,把这里改成 'production' 就行 + // 环境开关:'mock' 或 'production' + // 🟡 注意:真实API地址还未就绪,当前使用mock模式 + // 等页页给了地址后,改成 'production' 并替换下面的 apiBaseUrl + env: 'mock', + + // API 基础地址(production模式下使用) + // 🟡 待替换:等页页给了真实地址后替换这个字符串 + apiBaseUrl: 'https://api.example.com/v1', // 待替换 + + // Mock 数据地址(开发阶段使用) + mockDataPath: 'data/mock-announcements.json', + + // 认证配置(如后端需要Token) + auth: { + enabled: false, // 是否启用认证,等后端需要时改成 true + tokenKey: 'awen_token', // localStorage 存储的key + getToken: function() { + return localStorage.getItem(this.tokenKey); + }, + setToken: function(token) { + localStorage.setItem(this.tokenKey, token); + }, + clearToken: function() { + localStorage.removeItem(this.tokenKey); + } + }, + + // 缓存配置 + cache: { + enabled: true, + key: 'm22_announcements_cache', + expiry: 30 * 60 * 1000 // 30分钟有效期(毫秒) + }, + + // 请求超时设置(毫秒) + timeout: 10000, + + // 重试配置 + retry: { + enabled: true, + maxRetries: 2, + delay: 1000 // 重试延迟(毫秒) + }, + + // 是否启用详细日志(开发时有用) + debug: true +}; + +// 不允许修改 +Object.freeze(CONFIG); \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/script.js b/agents/zhuyuan-dev-agent/modules/M22-bulletin/script.js new file mode 100644 index 0000000..04262dd --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/script.js @@ -0,0 +1,197 @@ +// ===== script.js ===== +// 知秋:主逻辑·动态渲染+错误处理+i18n+无障碍+骨架屏 + +(function() { + // 状态变量 + let allBulletins = []; // 所有公告(从API拿) + let currentChannel = '全部'; // 当前频道(默认全部) + let loading = false; // 是否正在加载 + let error = null; // 错误信息 + + // DOM元素 + const container = document.querySelector('.bulletin-container'); + const channelTabs = document.querySelectorAll('.channel-tab'); + const skeletonContainer = document.getElementById('skeleton-container'); + + // ========== 骨架屏控制 ========== + function showSkeleton() { + if (skeletonContainer) { + skeletonContainer.style.display = 'block'; + } + } + + function hideSkeleton() { + if (skeletonContainer) { + skeletonContainer.style.opacity = '0'; + skeletonContainer.style.transition = 'opacity 0.3s ease'; + setTimeout(() => { + skeletonContainer.style.display = 'none'; + }, 300); + } + } + + // 初始化 + async function init() { + console.log("【知秋】M22公告栏·环节7启动"); + + // 监听语言切换事件 + window.addEventListener('languagechange', () => { + render(); + }); + + await loadBulletins(); + render(); + setupEventListeners(); + setupLangSwitcher(); + setupKeyboardNavigation(); + } + + // 语言切换器 + function setupLangSwitcher() { + document.querySelectorAll('.lang-btn').forEach(btn => { + btn.addEventListener('click', () => { + const lang = btn.dataset.lang; + window.i18n.switchLang(lang); + document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + }); + }); + const currentLang = window.i18n.currentLang; + document.querySelectorAll('.lang-btn').forEach(btn => { + if (btn.dataset.lang === currentLang) { + btn.classList.add('active'); + } + }); + } + + // 键盘导航 + function setupKeyboardNavigation() { + const tabs = document.querySelectorAll('.channel-tab'); + + tabs.forEach(tab => { + tab.addEventListener('keydown', (e) => { + const currentIndex = Array.from(tabs).findIndex(t => t === e.target); + + if (e.key === 'ArrowRight') { + e.preventDefault(); + const nextIndex = (currentIndex + 1) % tabs.length; + tabs[nextIndex].focus(); + tabs[nextIndex].click(); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length; + tabs[prevIndex].focus(); + tabs[prevIndex].click(); + } else if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.target.click(); + } + }); + }); + } + + // 加载公告 + async function loadBulletins() { + showSkeleton(); // 显示骨架屏 + loading = true; + error = null; + + try { + const data = await API.fetchBulletins(); + allBulletins = data; + hideSkeleton(); // 数据到手,隐藏骨架屏 + loading = false; + render(); // 渲染真实内容 + } catch (err) { + console.error("加载失败:", err); + hideSkeleton(); // 出错也要隐藏骨架屏 + loading = false; + error = "error"; + render(); // 显示错误状态 + } + } + + // 渲染公告列表 + function render() { + if (!container) return; + + // 错误状态 + if (error) { + const isOffline = !navigator.onLine; + const offlineHint = isOffline ? window.i18n.t('offline') : ''; + + container.innerHTML = ` + + `; + const retryBtn = document.getElementById('retryBtn'); + if (retryBtn) { + retryBtn.addEventListener('click', () => { + loadBulletins(); + }); + } + return; + } + + // 空数据状态 + if (allBulletins.length === 0) { + container.innerHTML = ` +
+

${window.i18n.t('empty')}

+
+ `; + return; + } + + // 正常渲染:根据频道筛选 + const filtered = currentChannel === '全部' + ? allBulletins + : allBulletins.filter(item => item.channel === currentChannel); + + if (filtered.length === 0) { + container.innerHTML = ` +
+

${window.i18n.t('emptyChannel')}

+
+ `; + return; + } + + // 渲染公告卡片 + let html = ''; + filtered.forEach((item, index) => { + if (typeof createCard === 'function') { + html += createCard(item); + } else { + html += ` +
+

${item.title}

+

${item.content}

+
+ ${item.channel} + ${item.date} +
+
+ `; + } + }); + container.innerHTML = html; + } + + // 频道切换事件 + function setupEventListeners() { + channelTabs.forEach(tab => { + tab.addEventListener('click', (e) => { + channelTabs.forEach(t => t.classList.remove('active')); + e.target.classList.add('active'); + currentChannel = e.target.dataset.channel || '全部'; + render(); + }); + }); + } + + // 启动一切 + init(); +})(); diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/storage.js b/agents/zhuyuan-dev-agent/modules/M22-bulletin/storage.js new file mode 100644 index 0000000..865bd56 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/storage.js @@ -0,0 +1,128 @@ +/** + * HoloLake Bulletin 数据持久化模块 + * storage.js - localStorage 封装 + 用户状态管理 + * 模块 ID: BC-M22-002-AW + */ + +window.HoloLake = window.HoloLake || {}; + +// ========== Storage 工具对象(带前缀) ========== +HoloLake.Storage = { + prefix: 'hololake_bulletin_', + + // 保存数据 + save: function(key, value) { + try { + const serialized = JSON.stringify(value); + localStorage.setItem(this.prefix + key, serialized); + console.log(`[Storage] 已保存: ${key} =`, value); + } catch (e) { + console.error('[Storage] 保存失败:', e); + } + }, + + // 读取数据 + load: function(key, defaultValue = null) { + try { + const serialized = localStorage.getItem(this.prefix + key); + if (serialized === null) { + console.log(`[Storage] 无数据: ${key},返回默认值`); + return defaultValue; + } + const value = JSON.parse(serialized); + console.log(`[Storage] 已加载: ${key} =`, value); + return value; + } catch (e) { + console.error('[Storage] 读取失败:', e); + return defaultValue; + } + }, + + // 删除指定键 + remove: function(key) { + try { + localStorage.removeItem(this.prefix + key); + console.log(`[Storage] 已删除: ${key}`); + } catch (e) { + console.error('[Storage] 删除失败:', e); + } + }, + + // 清空所有带前缀的数据 + clear: function() { + try { + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(this.prefix)) { + keysToRemove.push(key); + } + } + keysToRemove.forEach(key => localStorage.removeItem(key)); + console.log('[Storage] 已清空所有公告栏数据'); + } catch (e) { + console.error('[Storage] 清空失败:', e); + } + } +}; + +// ========== 用户状态管理 ========== +HoloLake.UserState = { + // 已读公告列表 + getReadBulletins: function() { + return HoloLake.Storage.load('read_bulletins', []); + }, + + markAsRead: function(bulletinId) { + const readList = this.getReadBulletins(); + if (!readList.includes(bulletinId)) { + readList.push(bulletinId); + HoloLake.Storage.save('read_bulletins', readList); + } + return readList; + }, + + // 订阅状态 + isSubscribed: function() { + return HoloLake.Storage.load('is_subscribed', false); + }, + + toggleSubscribe: function() { + const current = this.isSubscribed(); + const newState = !current; + HoloLake.Storage.save('is_subscribed', newState); + return newState; + }, + + // 当前频道 + getActiveChannel: function() { + return HoloLake.Storage.load('active_channel', 'all'); + }, + + setActiveChannel: function(channelId) { + HoloLake.Storage.save('active_channel', channelId); + }, + + // 上次访问时间 + getLastVisit: function() { + return HoloLake.Storage.load('last_visit', null); + }, + + updateLastVisit: function() { + const now = new Date().toISOString(); + HoloLake.Storage.save('last_visit', now); + return now; + }, + + // 重置所有状态(测试用) + resetAll: function() { + HoloLake.Storage.remove('read_bulletins'); + HoloLake.Storage.remove('is_subscribed'); + HoloLake.Storage.remove('active_channel'); + HoloLake.Storage.remove('last_visit'); + console.log('[UserState] 已重置所有状态'); + } +}; + +// 初始化时记录访问时间(可选,页面加载时由 script.js 调用) +console.log('✅ storage.js 已加载,HoloLake.Storage 和 HoloLake.UserState 已就绪'); \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/M22-bulletin/style.css b/agents/zhuyuan-dev-agent/modules/M22-bulletin/style.css new file mode 100644 index 0000000..ee79abf --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/M22-bulletin/style.css @@ -0,0 +1,335 @@ +/* ===== CSS 变量定义(浅色/深色主题)===== */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f5f7fa; + --bg-tertiary: #eef1f5; + --text-primary: #1e293b; + --text-secondary: #475569; + --text-tertiary: #64748b; + --accent-primary: #3b82f6; + --accent-secondary: #2563eb; + --border-color: #e2e8f0; +} + +/* 深色主题 */ +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-tertiary: #94a3b8; + --accent-primary: #60a5fa; + --accent-secondary: #3b82f6; + --border-color: #334155; + } +} + +/* ===== 基础样式 ===== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; + transition: background-color 0.3s, color 0.3s; +} + +.app-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* ===== 头部样式 ===== */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 0; + border-bottom: 1px solid var(--border-color); + margin-bottom: 20px; +} + +.header h1 { + font-size: 1.8rem; + font-weight: 600; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.header-actions { + display: flex; + gap: 12px; + align-items: center; +} + +.lang-switcher { + display: flex; + gap: 4px; +} + +.lang-btn { + padding: 6px 12px; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; +} + +.lang-btn:hover { + background: var(--accent-primary); + color: white; +} + +.pinned-badge { + background: #fef3c7; + color: #92400e; + padding: 4px 10px; + border-radius: 20px; + font-size: 0.85rem; +} + +.subscribe-btn { + background: var(--accent-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; +} + +/* ===== 频道栏 ===== */ +.channel-bar { + display: flex; + gap: 10px; + margin-bottom: 24px; + flex-wrap: wrap; +} + +.channel-tab { + padding: 8px 20px; + border: none; + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 30px; + cursor: pointer; + font-size: 0.95rem; + transition: all 0.2s; +} + +.channel-tab.active { + background: var(--accent-primary); + color: white; +} + +.channel-tab:hover:not(.active) { + background: var(--bg-tertiary); +} + +/* ===== 骨架屏样式 ===== */ +.skeleton-wrapper { + margin: 20px 0; +} + +.skeleton-card { + background: var(--bg-secondary); + border-radius: 12px; + padding: 20px; + margin-bottom: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.skeleton { + background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-secondary) 50%, var(--bg-tertiary) 75%); + background-size: 200% 100%; + animation: loading 1.5s infinite; + border-radius: 4px; +} + +@keyframes loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.skeleton-title { + width: 60%; + height: 24px; + margin-bottom: 12px; +} + +.skeleton-text { + width: 100%; + height: 16px; + margin-bottom: 8px; +} + +.skeleton-text.short { + width: 40%; +} + +/* ===== 公告列表样式 ===== */ +.bulletin-container { + margin: 20px 0; + min-height: 200px; +} + +.bulletin-item { + background: var(--bg-secondary); + border-radius: 12px; + padding: 24px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid var(--border-color); +} + +.bulletin-title { + font-size: 1.3rem; + margin-bottom: 10px; + color: var(--text-primary); +} + +.bulletin-meta { + display: flex; + gap: 16px; + margin-bottom: 16px; + color: var(--text-tertiary); + font-size: 0.9rem; +} + +.bulletin-channel { + background: var(--bg-tertiary); + padding: 2px 10px; + border-radius: 20px; + color: var(--text-secondary); +} + +.bulletin-date { + color: var(--text-tertiary); +} + +.bulletin-content { + color: var(--text-secondary); + line-height: 1.7; +} + +/* ===== 底部样式 ===== */ +.footer { + margin-top: 40px; + padding-top: 20px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + color: var(--text-tertiary); + font-size: 0.9rem; +} + +/* ===== EL-6 新增样式 ===== */ +/* 骨架屏平滑淡出 */ +.bulletin-container.data-loaded .bulletin-skeleton { + display: none; +} + +.bulletin-item { + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* 错误状态 */ +.error-state { + text-align: center; + padding: 4rem 2rem; + background: var(--bg-secondary); + border-radius: 12px; + margin: 2rem 0; +} + +.error-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.error-title { + font-size: 1.3rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.error-message { + color: var(--text-secondary); + margin-bottom: 1rem; +} + +.error-suggestion { + color: var(--text-tertiary); + font-size: 0.9rem; + margin-bottom: 1.5rem; +} + +.retry-btn { + background: var(--accent-primary); + color: white; + border: none; + padding: 0.8rem 2rem; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; +} + +.retry-btn:hover { + background: var(--accent-secondary); + transform: translateY(-2px); +} + +/* 离线标记 */ +.offline-badge { + background: #ffd966; + color: #856404; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.9rem; + display: inline-block; + margin-bottom: 1rem; +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 4rem 2rem; + background: var(--bg-secondary); + border-radius: 12px; + margin: 2rem 0; +} + +.empty-icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.6; +} + +.empty-title { + font-size: 1.2rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.empty-desc { + color: var(--text-tertiary); +} +} \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/brain-writer.js b/agents/zhuyuan-dev-agent/modules/brain-writer.js new file mode 100644 index 0000000..4b656c1 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/brain-writer.js @@ -0,0 +1,83 @@ +// ═══════════════════════════════════════ +// 铸渊工程Agent · HLDP大脑记录器 +// 记录认知推理链,而非流水账 +// ═══════════════════════════════════════ + +const fs = require("fs"); +const path = require("path"); + +const AGENT_ID = process.env.AGENT_ID || "ICE-GL-BD001"; +const TERRITORY = path.join(__dirname, "..", "agents", AGENT_ID.replace("ICE-GL-BD", "BD")); + +if (!fs.existsSync(TERRITORY)) { + console.warn("[BrainWriter] 领地不存在: " + TERRITORY + " · 将不写入认知记录"); +} + +/** + * 写入一条认知记录 + */ +function record(taskId, entry) { + if (!fs.existsSync(TERRITORY)) return; + + const file = path.join(TERRITORY, "brain", "cognitive-record.hldp"); + const lines = [ + "", + "@task: " + taskId, + "@time: " + new Date().toISOString(), + "@input: " + (entry.input || ""), + "@decision: " + (entry.decision || ""), + "@execution: " + (entry.execution || ""), + "@failure: " + (entry.failure || "无"), + "@reasoning: " + (entry.reasoning || ""), + "@fix: " + (entry.fix || ""), + "@learned: " + (entry.learned || ""), + "@confidence: " + (entry.confidence || "N/A"), + "@new_knowledge: " + (entry.new_knowledge ? "true" : "false") + ]; + + fs.appendFileSync(file, lines.join("\n") + "\n", "utf-8"); +} + +/** + * 记录错误模式 + */ +function recordErrorPattern(pattern) { + if (!fs.existsSync(TERRITORY)) return; + const file = path.join(TERRITORY, "memory", "error-patterns.json"); + const data = JSON.parse(fs.readFileSync(file, "utf-8")); + + // 检查是否已有同类模式 + const existing = data.patterns.find(p => p.type === pattern.type); + if (existing) { + existing.count = (existing.count || 1) + 1; + existing.last_seen = new Date().toISOString(); + } else { + data.patterns.push({ + type: pattern.type, + description: pattern.description, + count: 1, + first_seen: new Date().toISOString(), + last_seen: new Date().toISOString(), + examples: [pattern.example] + }); + } + + fs.writeFileSync(file, JSON.stringify(data, null, 2), "utf-8"); +} + +/** + * 记录成长里程碑 + */ +function recordMilestone(event) { + if (!fs.existsSync(TERRITORY)) return; + const file = path.join(TERRITORY, "brain", "growth-timeline.json"); + const data = JSON.parse(fs.readFileSync(file, "utf-8")); + data.milestones.push({ + time: new Date().toISOString(), + event: event, + project: process.env.CURRENT_PROJECT_ID || "unknown" + }); + fs.writeFileSync(file, JSON.stringify(data, null, 2), "utf-8"); +} + +module.exports = { record, recordErrorPattern, recordMilestone, TERRITORY, AGENT_ID }; diff --git a/agents/zhuyuan-dev-agent/modules/cost-guard.js b/agents/zhuyuan-dev-agent/modules/cost-guard.js new file mode 100644 index 0000000..71c8f80 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/cost-guard.js @@ -0,0 +1,69 @@ +// ═══════════════════════════════════════ +// 铸渊开发Agent · 成本守卫 + 统一插座 v1.0 +// API调用计数 · 预算控制 · 输出格式校验 +// ═══════════════════════════════════════ + +const fs = require("fs"); +const path = require("path"); + +const CONSTITUTION = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "agent-constitution.json"), "utf-8")); +const { cost_guard, unified_interface } = CONSTITUTION; + +let _callCount = 0; +let _budgetExceeded = false; + +function reset() { + _callCount = 0; + _budgetExceeded = false; +} + +function getCallCount() { return _callCount; } + +function isBudgetExceeded() { return _budgetExceeded; } + +/** + * 包裹模型调用——自动计数 + */ +function wrapModelCaller(rawCaller) { + return async function(prompt, opts) { + if (_budgetExceeded) { + throw new Error(cost_guard.stop_message); + } + + _callCount++; + + if (_callCount > cost_guard.max_api_calls_per_session) { + _budgetExceeded = true; + throw new Error(cost_guard.stop_message + " (已用 " + _callCount + " 次,上限 " + cost_guard.max_api_calls_per_session + ")"); + } + + console.log("[CostGuard] API调用 #" + _callCount + "/" + cost_guard.max_api_calls_per_session); + return rawCaller(prompt, opts); + }; +} + +/** + * 统一校验——每个模块输出必须通过这把尺 + */ +function validateOutput(output) { + const required = unified_interface.required_output_fields; + const missing = required.filter(f => !(f in output)); + + if (missing.length > 0) { + return { + valid: false, + error: "输出缺少必要字段: " + missing.join(", "), + required, + received: Object.keys(output) + }; + } + + // 类型校验 + if (typeof output.ok !== "boolean") { + return { valid: false, error: "ok 必须是 boolean,收到: " + typeof output.ok }; + } + + return { valid: true }; +} + +module.exports = { wrapModelCaller, validateOutput, reset, getCallCount, isBudgetExceeded }; diff --git a/agents/zhuyuan-dev-agent/modules/devboard/api-init.js b/agents/zhuyuan-dev-agent/modules/devboard/api-init.js new file mode 100644 index 0000000..c49bc31 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/api-init.js @@ -0,0 +1,81 @@ +// ===================================== +// HoloLake DevBoard · API Init v1.0 +// 负责:异步加载 + 加载动画 + 自动刷新 +// 必须在 api.js 和 main.js 之后加载 +// ===================================== + +(function(){ + // -------------- 加载动画 -------------- + function showLoader(show) { + var el = document.getElementById('devboard-loader'); + if (!el && show) { + el = document.createElement('div'); + el.id = 'devboard-loader'; + el.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(10,14,23,0.92);display:flex;align-items:center;justify-content:center;z-index:10000'; + el.innerHTML = '
数据加载中...
'; + var style = document.createElement('style'); + style.textContent = '@keyframes dbspin{to{transform:rotate(360deg)}}'; + document.head.appendChild(style); + document.body.appendChild(el); + } + if (el) el.style.display = show ? 'flex' : 'none'; + } + + // -------------- 数据加载+渲染 -------------- + async function loadAndRender() { + try { + var developers = await apiGetDevelopers(); + var stats = await apiGetStats(); + var leaderboard = await apiGetLeaderboard(); + + // 使用 components.js 里实际存在的函数 + if (typeof renderDeveloperCards === 'function') { + renderDeveloperCards(developers); + } + + if (typeof renderRanking === 'function') { + renderRanking(leaderboard); + } + + // stats 暂时用控制台输出,等找到实际渲染函数再改 + console.log('[DevBoard] 统计数据:', stats); + + console.log('[DevBoard] 数据加载完成,共 ' + developers.length + ' 位开发者'); + } catch (err) { + console.error('[DevBoard] 数据加载异常: ', err); + } + } + + // -------------- 初始化入口 -------------- + async function init() { + showLoader(true); + + // 检测API状态 + var online = await apiHealthCheck(); + showApiStatus(online); + console.log('[DevBoard] API状态: ' + (online ? '在线(实时)' : '离线(降级)')); + + // 加载+渲染 + await loadAndRender(); + showLoader(false); + + // 自动刷新(仅API在线时) + if (online && API.REFRESH_MS > 0) { + console.log('[DevBoard] 自动刷新已启动,间隔 ' + (API.REFRESH_MS/1000) + ' 秒'); + setInterval(async function() { + try { + await loadAndRender(); + } catch (e) { + console.warn('[DevBoard] 自动刷新失败: ', e.message); + } + }, API.REFRESH_MS); + } + } + + // 等待DOM和所有脚本加载完成后启动 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/agents/zhuyuan-dev-agent/modules/devboard/api.js b/agents/zhuyuan-dev-agent/modules/devboard/api.js new file mode 100644 index 0000000..fd92807 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/api.js @@ -0,0 +1,122 @@ +// ====================== +// HoloLake DevBoard · API Client v2.0 +// DEV-004 之之 · 环节5 · 真实API对接 +// 协议:SYSLOG-v4.0 +// ====================== + +const API = { + // ====== 配置区 ====== + // 页页的后端API地址(如果地址不对,问知秋确认) + BASE_URL: 'https://guanghulab.com/api/devboard', + TIMEOUT: 8000, + FALLBACK: true, // API挂了自动切换模拟数据 + REFRESH_MS: 30000, // 30秒自动刷新 + _cache: {} +}; + +// ====== 通用请求(带超时+缓存+降级) ===================== +async function apiFetch(path) { + const url = API.BASE_URL + path; + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), API.TIMEOUT); + try { + const res = await fetch(url, { + signal: ctrl.signal, + headers: { 'Accept': 'application/json' } + }); + clearTimeout(timer); + if (!res.ok) throw new Error('HTTP ' + res.status); + const data = await res.json(); + API._cache[path] = { data: data, time: Date.now() }; + return data; + } catch (err) { + clearTimeout(timer); + console.warn(' 🚨 [DevBoard API] ' + path + ' 失败: ', err.message); + if (API._cache[path]) { + console.info(' 💾 [DevBoard API] 使用缓存'); + return API._cache[path].data; + } + if (API.FALLBACK) { + console.info(' 🛡️ [DevBoard API] 降级到模拟数据'); + return getMockData(path); + } + throw err; + } +} + +// ========== 公开接口 ========== +async function apiGetDevelopers() { + return apiFetch('/developers'); +} + +async function apiGetDeveloperDetail(devId) { + return apiFetch('/developers/' + devId); +} + +async function apiGetStats() { + return apiFetch('/stats'); +} + +async function apiGetLeaderboard() { + return apiFetch('/leaderboard'); +} + +// ========== 健康检查 ========== +async function apiHealthCheck() { + try { + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), 3000); + const res = await fetch(API.BASE_URL + '/health', { signal: ctrl.signal }); + clearTimeout(t); + return res.ok; + } catch (e) { + return false; + } +} + +// ========== 模拟数据(降级保护) ========== +function getMockData(path) { + var devs = [ + { dev_id:'DEV-001', name:'页页', module:'BC-集成', status:'进行中', streak:5, el:'EL-6', pca:{EXE:75,TEC:60,SYS:70,COL:80,INI:65} }, + { dev_id:'DEV-002', name:'肥猫', module:'M-STATUS', status:'进行中', streak:9, el:'EL-5', pca:{EXE:72,TEC:40,SYS:68,COL:82,INI:70} }, + { dev_id:'DEV-003', name:'燕樊', module:'M-MEMORY', status:'进行中', streak:7, el:'EL-5', pca:{EXE:70,TEC:55,SYS:65,COL:75,INI:68} }, + { dev_id:'DEV-004', name:'之之', module:'M-DEVBOARD', status:'进行中', streak:14, el:'EL-8', pca:{EXE:90,TEC:55,SYS:80,COL:85,INI:88} }, + { dev_id:'DEV-009', name:'花尔', module:'M20', status:'进行中', streak:4, el:'EL-8', pca:{EXE:65,TEC:45,SYS:55,COL:70,INI:60} }, + { dev_id:'DEV-010', name:'桔子', module:'M-CHANNEL', status:'进行中', streak:14, el:'EL-6', pca:{EXE:85,TEC:50,SYS:75,COL:80,INI:82} }, + { dev_id:'DEV-011', name:'匆匆那年',module:'M16', status:'等待中', streak:1, el:'EL-3', pca:{EXE:50,TEC:30,SYS:40,COL:55,INI:45} }, + { dev_id:'DEV-012', name:'Awen', module:'M22', status:'进行中', streak:13, el:'EL-6', pca:{EXE:88,TEC:45,SYS:78,COL:90,INI:80} }, + { dev_id:'DEV-013', name:'小兴', module:'M-AUTH', status:'进行中', streak:1, el:'EL-3', pca:{EXE:55,TEC:25,SYS:35,COL:60,INI:50} } + ]; + var stats = { total_developers:10, active:8, modules:12, code_lines:32000, longest_streak:14, avg_streak:7.5 }; + + if (path === '/developers') return devs; + if (path.indexOf('/developers/') === 0) { + var id = path.split('/').pop(); + return devs.find(function(d){ return d.dev_id === id; }) || devs[0]; + } + if (path === '/stats') return stats; + if (path === '/leaderboard') return devs.slice().sort(function(a,b){ return b.streak - a.streak; }); + return []; +} + +// ========== API状态指示器 ========== +function showApiStatus(online) { + var el = document.getElementById('api-status'); + if (!el) { + el = document.createElement('div'); + el.id = 'api-status'; + el.style.cssText = 'position:fixed;bottom:12px;right:12px;padding:6px 14px;border-radius:20px;font-size:12px;font-weight:600;z-index:9999;transition:all 0.3s;'; + document.body.appendChild(el); + } + if (online) { + el.textContent = '🟢 实时数据'; + el.style.backgroundColor = 'rgba(52,211,153,0.15)'; + el.style.color = '#34d399'; + el.style.border = '1px solid rgba(52,211,153,0.3)'; + } else { + el.textContent = '🟡 模拟数据'; + el.style.backgroundColor = 'rgba(251,191,36,0.15)'; + el.style.color = '#fbbf24'; + el.style.border = '1px solid rgba(251,191,36,0.3)'; + } +} diff --git a/agents/zhuyuan-dev-agent/modules/devboard/components.css b/agents/zhuyuan-dev-agent/modules/devboard/components.css new file mode 100644 index 0000000..fd16ad3 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/components.css @@ -0,0 +1,156 @@ +/* components.css - 组件样式 */ + +/* 开发者卡片 */ +.dev-card { + background: linear-gradient(145deg, #1e2436, #161b28); + border-radius: 20px; + padding: 20px; + border: 1px solid #2a3040; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + position: relative; + overflow: hidden; +} + +.dev-card:hover { + transform: translateY(-8px) scale(1.02); + border-color: #4a9eff; + box-shadow: 0 20px 30px -10px rgba(74, 158, 255, 0.3); +} + +.dev-card:focus-visible { + outline: 3px solid #ffd700; + outline-offset: 2px; +} + +.dev-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); + transition: left 0.5s; +} + +.dev-card:hover::before { + left: 100%; +} + +.dev-header { + display: flex; + justify-content: space-between; + margin-bottom: 15px; + font-size: 14px; +} + +.dev-id { + color: #4a9eff; + font-weight: bold; + font-family: monospace; +} + +.dev-wins { + background: rgba(255, 215, 0, 0.1); + padding: 4px 8px; + border-radius: 20px; + color: #ffd700; +} + +.dev-name { + font-size: 24px; + font-weight: bold; + margin-bottom: 10px; + color: #fff; +} + +.dev-details { + display: flex; + gap: 10px; + margin-bottom: 15px; + font-size: 14px; +} + +.dev-el { + background: #2a3040; + padding: 4px 8px; + border-radius: 12px; + color: #4a9eff; +} + +.dev-module { + background: #2a3040; + padding: 4px 8px; + border-radius: 12px; + color: #a0a0a0; +} + +.dev-pca { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 5px; + font-size: 11px; +} + +.pca-mini { + text-align: center; + padding: 4px 0; + border-radius: 8px; + background: rgba(0,0,0,0.3); +} + +.pca-mini.EXE { color: #4CAF50; } +.pca-mini.TEC { color: #2196F3; } +.pca-mini.SYS { color: #9C27B0; } +.pca-mini.COL { color: #FF9800; } +.pca-mini.INI { color: #f44336; } + +/* 排行榜项 */ +.ranking-item { + display: flex; + align-items: center; + gap: 15px; + padding: 15px; + border-bottom: 1px solid #2a3040; + transition: background 0.2s; +} + +.ranking-item:hover { + background: #1e2436; +} + +.rank-number { + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + background: #2a3040; + border-radius: 50%; + font-weight: bold; + color: #4a9eff; +} + +.rank-1 .rank-number { background: #ffd700; color: #000; } +.rank-2 .rank-number { background: #c0c0c0; color: #000; } +.rank-3 .rank-number { background: #cd7f32; color: #000; } + +.rank-info { + flex: 1; +} + +.rank-name { + font-weight: bold; + color: #fff; +} + +.rank-id { + font-size: 12px; + color: #888; +} + +.rank-wins { + font-weight: bold; + color: #ffd700; +} diff --git a/agents/zhuyuan-dev-agent/modules/devboard/components.js b/agents/zhuyuan-dev-agent/modules/devboard/components.js new file mode 100644 index 0000000..5f033e9 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/components.js @@ -0,0 +1,146 @@ +// components.js - 组件渲染函数 + +// 模拟数据 +const mockDevelopers = [ + { id: 'DEV-001', name: '小明', pca: { EXE: 85, TEC: 72, SYS: 68, COL: 90, INI: 78 }, wins: 5, el: 'EL-6', module: 'M-DEVBOARD' }, + { id: 'DEV-002', name: '小红', pca: { EXE: 78, TEC: 88, SYS: 82, COL: 75, INI: 92 }, wins: 8, el: 'EL-7', module: 'M-FRONTEND' }, + { id: 'DEV-003', name: '小刚', pca: { EXE: 92, TEC: 65, SYS: 70, COL: 68, INI: 85 }, wins: 3, el: 'EL-5', module: 'M-BACKEND' }, + { id: 'DEV-004', name: '之之', pca: { EXE: 75, TEC: 58, SYS: 82, COL: 82, INI: 85 }, wins: 13, el: 'EL-6', module: 'M-DEVBOARD' } +]; + +// 获取所有开发者 +function getDevelopers() { + return mockDevelopers; +} + +// 根据ID获取开发者 +function getDeveloperById(id) { + return mockDevelopers.find(d => d.id === id) || mockDevelopers[0]; +} + +// 渲染开发者卡片 +function renderDeveloperCards(developers) { + return developers.map(dev => ` +
+
+ ${dev.id} + 🏆 ${dev.wins} +
+
${dev.name}
+
+ ${dev.el} + ${dev.module} +
+
+ ${Object.entries(dev.pca).map(([key, val]) => + `${key}:${val}` + ).join('')} +
+
+ `).join(''); +} + +// 渲染排行榜 +function renderRanking(developers) { + // 按连胜数排序 + const sorted = [...developers].sort((a, b) => b.wins - a.wins); + + return sorted.map((dev, index) => { + const rankClass = index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : index === 2 ? 'rank-3' : ''; + return ` +
+ ${index + 1} +
+
${dev.name}
+
${dev.id}
+
+ ${dev.wins}连胜 +
+ `; + }).join(''); +} + +// 刷新看板 +function refreshDashboard() { + const developers = getDevelopers(); + + // 渲染卡片 + const grid = document.getElementById('developers-grid'); + if (grid) { + grid.innerHTML = renderDeveloperCards(developers); + } + + // 渲染排行榜 + const ranking = document.getElementById('ranking-list'); + if (ranking) { + ranking.innerHTML = renderRanking(developers); + } + + // 绘制团队雷达图(平均分) + const avgPca = { + EXE: Math.round(developers.reduce((sum, d) => sum + d.pca.EXE, 0) / developers.length), + TEC: Math.round(developers.reduce((sum, d) => sum + d.pca.TEC, 0) / developers.length), + SYS: Math.round(developers.reduce((sum, d) => sum + d.pca.SYS, 0) / developers.length), + COL: Math.round(developers.reduce((sum, d) => sum + d.pca.COL, 0) / developers.length), + INI: Math.round(developers.reduce((sum, d) => sum + d.pca.INI, 0) / developers.length) + }; + + drawRadarChart('team-radar', avgPca); +} + +// 搜索筛选 +function filterDevelopers(searchTerm, filterStatus) { + let developers = getDevelopers(); + + // 搜索过滤 + if (searchTerm) { + const term = searchTerm.toLowerCase(); + developers = developers.filter(dev => + dev.name.toLowerCase().includes(term) || + dev.id.toLowerCase().includes(term) || + dev.module.toLowerCase().includes(term) + ); + } + + // 状态过滤(模拟,实际需要真实数据) + if (filterStatus !== 'all') { + // 这里简化处理,实际应该根据模块状态筛选 + developers = developers.filter(dev => { + if (filterStatus === 'active') return dev.wins < 10; + if (filterStatus === 'completed') return dev.wins >= 10; + return true; + }); + } + + return developers; +} + +// 导出函数 +window.getDevelopers = getDevelopers; +window.getDeveloperById = getDeveloperById; +window.renderDeveloperCards = renderDeveloperCards; +window.renderRanking = renderRanking; +window.refreshDashboard = refreshDashboard; +window.filterDevelopers = filterDevelopers; + +// 增强版排行榜渲染(带数字滚动支持) +function renderRankingWithAnimation(developers) { + const sorted = [...developers].sort((a, b) => b.wins - a.wins); + + return sorted.map((dev, index) => { + const rankClass = index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : index === 2 ? 'rank-3' : ''; + return ` +
+ ${index + 1} +
+
${dev.name}
+
${dev.id}
+
+ ${dev.wins}连胜 +
+ `; + }).join(''); +} + +// 覆盖原函数 +window.renderRanking = renderRankingWithAnimation; diff --git a/agents/zhuyuan-dev-agent/modules/devboard/css/components.css b/agents/zhuyuan-dev-agent/modules/devboard/css/components.css new file mode 100644 index 0000000..dd88ba4 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/css/components.css @@ -0,0 +1,140 @@ +.stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + margin-bottom: 2rem; +} + +.stat-card { + background: linear-gradient(145deg, var(--color-bg-card), #2a3442); + border-radius: 16px; + padding: 1.5rem 1rem; + text-align: center; + border: 1px solid var(--color-border); +} + +.stat-label { + font-size: 0.9rem; + color: var(--color-text-secondary); + margin-bottom: 0.5rem; + text-transform: uppercase; +} + +.stat-number { + font-size: 2.5rem; + font-weight: 800; + background: linear-gradient(135deg, #fff, var(--color-accent-primary)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.25rem; +} + +.dev-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; + margin-top: 1.5rem; +} + +.dev-card { + background: var(--color-bg-card); + border-radius: 20px; + padding: 1.5rem; + border: 1px solid var(--color-border); + cursor: pointer; +} + +.dev-card-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.2rem; +} + +.dev-avatar { + width: 50px; + height: 50px; + border-radius: 25px; + background: linear-gradient(135deg, var(--color-accent-primary), #4cc9f0); + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.5rem; + color: white; +} + +.dev-name { + font-size: 1.2rem; + font-weight: 700; +} + +.dev-id { + font-size: 0.8rem; + color: var(--color-text-secondary); +} + +.dev-streak { + font-size: 1.8rem; + font-weight: 800; + color: var(--color-accent-primary); +} + +.ranking-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.ranking-list { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.ranking-item { + display: grid; + grid-template-columns: 40px 1fr 70px; + align-items: center; + background: var(--color-bg-card); + border-radius: 12px; + padding: 0.8rem; + border: 1px solid var(--color-border); +} + +.ranking-streak-bar { + background: var(--color-bg-secondary); + height: 16px; + border-radius: 20px; + position: relative; + overflow: hidden; +} + +.streak-progress { + height: 100%; + background: linear-gradient(90deg, #00b4d8, #4cc9f0); +} + +.streak-count { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + font-size: 0.7rem; + color: white; +} + +.loading-spinner { + width: 30px; + height: 30px; + border: 3px solid var(--color-border); + border-top-color: var(--color-accent-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 20px auto; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/agents/zhuyuan-dev-agent/modules/devboard/css/layout.css b/agents/zhuyuan-dev-agent/modules/devboard/css/layout.css new file mode 100644 index 0000000..7811611 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/css/layout.css @@ -0,0 +1,58 @@ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + background-color: var(--color-bg-secondary); + border-bottom: 1px solid var(--color-border); +} + +.navbar-logo { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: bold; + font-size: 1.2rem; +} +.navbar-logo span { + color: var(--color-accent-primary); +} + +.navbar-info { + display: flex; + gap: 1.5rem; + color: var(--color-text-secondary); + font-size: 0.9rem; +} + +.main-container { + display: grid; + grid-template-columns: 1fr 2fr 1fr; + gap: 1.5rem; + padding: 1.5rem; + min-height: calc(100vh - 70px); +} + +.sidebar-left, .main-board, .sidebar-right { + background-color: var(--color-bg-secondary); + border-radius: 12px; + padding: 1.5rem; +} + +@media (max-width: 1024px) { + .main-container { + grid-template-columns: 1fr 2fr; + } + .sidebar-right { + grid-column: span 2; + } +} + +@media (max-width: 768px) { + .main-container { + grid-template-columns: 1fr; + } + .sidebar-left, .main-board, .sidebar-right { + grid-column: span 1; + } +} diff --git a/agents/zhuyuan-dev-agent/modules/devboard/css/theme.css b/agents/zhuyuan-dev-agent/modules/devboard/css/theme.css new file mode 100644 index 0000000..bf856f5 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/css/theme.css @@ -0,0 +1,43 @@ +:root { + --color-bg-primary: #0a0e1a; + --color-bg-secondary: #111827; + --color-bg-card: #1e2433; + --color-text-primary: #ffffff; + --color-text-secondary: #a0aec0; + --color-accent-primary: #00b4d8; + --color-accent-secondary: #0077be; + --color-success: #10b981; + --color-warning: #f59e0b; + --color-danger: #ef4444; + --color-border: #2d3748; + + --breakpoint-mobile: 768px; + --breakpoint-tablet: 1024px; + + --font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-family-base); + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + line-height: 1.6; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-track { + background: var(--color-bg-secondary); +} +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 4px; +} diff --git a/agents/zhuyuan-dev-agent/modules/devboard/detail.css b/agents/zhuyuan-dev-agent/modules/devboard/detail.css new file mode 100644 index 0000000..385c484 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/detail.css @@ -0,0 +1,358 @@ +/* detail.css - 详情页专用样式 */ + +/* 详情页容器 */ +#detail-container { + display: none; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + min-height: 100vh; + padding: 20px; + animation: fadeIn 0.3s ease; +} + +/* 详情页头部 */ +.detail-header { + margin-bottom: 30px; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +.back-button { + background: rgba(255, 255, 255, 0.1); + border: 2px solid #4a9eff; + color: #fff; + padding: 12px 24px; + border-radius: 30px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(5px); +} + +.back-button:hover { + background: #4a9eff; + transform: translateX(-5px); + box-shadow: 0 0 20px rgba(74, 158, 255, 0.5); +} + +.back-button:focus-visible { + outline: 3px solid #ffd700; + outline-offset: 2px; +} + +/* 详情页内容区 */ +.detail-content { + display: flex; + flex-direction: column; + gap: 30px; + max-width: 1200px; + margin: 0 auto; +} + +.detail-section { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 25px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.detail-section h2 { + color: #4a9eff; + font-size: 20px; + margin-bottom: 20px; + border-bottom: 2px solid rgba(74, 158, 255, 0.3); + padding-bottom: 10px; + display: flex; + align-items: center; + gap: 10px; +} + +/* 开发者大头卡片(大尺寸) */ +.developer-card-large .dev-card { + transform: scale(1); + background: linear-gradient(145deg, #1e2436, #161b28); + border: 2px solid #4a9eff; + margin-bottom: 0; +} + +.developer-card-large .dev-card:hover { + transform: translateY(-5px); + box-shadow: 0 20px 30px -10px rgba(74, 158, 255, 0.5); +} + +/* PCA雷达图容器 */ +.radar-large-container { + height: 300px; + margin-bottom: 20px; + background: rgba(0, 0, 0, 0.2); + border-radius: 12px; + padding: 10px; +} + +.radar-large-container canvas { + width: 100%; + height: 100%; +} + +/* PCA分数标签 */ +.pca-scores { + display: flex; + flex-wrap: wrap; + gap: 15px; + justify-content: center; + margin-top: 20px; +} + +.score-tag { + padding: 8px 16px; + border-radius: 20px; + font-weight: bold; + font-size: 14px; + background: rgba(0, 0, 0, 0.5); + border: 1px solid; + transition: transform 0.2s; +} + +.score-tag:hover { + transform: scale(1.05); +} + +.score-tag.EXE { border-color: #4CAF50; color: #4CAF50; } +.score-tag.TEC { border-color: #2196F3; color: #2196F3; } +.score-tag.SYS { border-color: #9C27B0; color: #9C27B0; } +.score-tag.COL { border-color: #FF9800; color: #FF9800; } +.score-tag.INI { border-color: #f44336; color: #f44336; } + +/* 五维进度条 */ +.progress-bars-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.progress-item { + display: flex; + align-items: center; + gap: 15px; + padding: 10px; + background: rgba(0, 0, 0, 0.2); + border-radius: 12px; + transition: transform 0.2s ease, background 0.2s; +} + +.progress-item:hover { + transform: translateX(10px); + background: rgba(0, 0, 0, 0.3); +} + +.progress-label { + width: 100px; + display: flex; + justify-content: space-between; + font-weight: bold; +} + +.dim-name { + color: #fff; +} + +.dim-score { + color: #ffd700; + font-family: monospace; +} + +.progress-bar-bg { + flex: 1; + height: 24px; + background: rgba(255, 255, 255, 0.1); + border-radius: 12px; + overflow: hidden; + position: relative; +} + +.progress-bar-fill { + height: 100%; + border-radius: 12px; + transition: width 1s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.progress-bar-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: shimmer 2s infinite; +} + +.progress-dim-code { + width: 50px; + text-align: right; + font-family: monospace; + color: #888; + font-size: 14px; +} + +/* 模块历史列表 */ +.module-history-list { + list-style: none; + padding: 0; + margin: 0; +} + +.module-item { + display: flex; + align-items: center; + gap: 20px; + padding: 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + transition: background 0.2s ease; +} + +.module-item:hover { + background: rgba(255, 255, 255, 0.05); + transform: scale(1.01); +} + +.module-code { + font-family: monospace; + color: #4a9eff; + font-weight: bold; + width: 150px; +} + +.module-name { + flex: 1; + color: #fff; +} + +.module-status { + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: bold; + width: 80px; + text-align: center; +} + +.status-已完成 { background: rgba(76, 175, 80, 0.2); color: #4CAF50; border: 1px solid #4CAF50; } +.status-进行中 { background: rgba(255, 152, 0, 0.2); color: #FF9800; border: 1px solid #FF9800; } +.status-待开始 { background: rgba(158, 158, 158, 0.2); color: #9E9E9E; border: 1px solid #9E9E9E; } + +.module-date { + color: #888; + font-size: 14px; + width: 100px; + font-family: monospace; +} + +/* 趋势图容器 */ +.trend-chart-container { + background: rgba(0, 0, 0, 0.3); + border-radius: 12px; + padding: 20px; + position: relative; +} + +/* 无数据提示 */ +.no-data { + text-align: center; + color: #888; + padding: 40px; + font-style: italic; +} + +/* 动画定义 */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fadeOut { + from { opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(20px); } +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +.fade-in { + animation: fadeIn 0.3s ease forwards; +} + +.fade-out { + animation: fadeOut 0.3s ease forwards; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .detail-content { + gap: 15px; + } + + .detail-section { + padding: 15px; + } + + .progress-item { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .progress-label { + width: 100%; + } + + .progress-bar-bg { + width: 100%; + } + + .progress-dim-code { + width: 100%; + text-align: left; + } + + .module-item { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .module-code { + width: auto; + } + + .module-name { + width: 100%; + } + + .module-status { + width: auto; + } + + .module-date { + width: auto; + } + + .pca-scores { + gap: 8px; + } + + .score-tag { + padding: 4px 12px; + font-size: 12px; + } + + .radar-large-container { + height: 250px; + } +} diff --git a/agents/zhuyuan-dev-agent/modules/devboard/detail.js b/agents/zhuyuan-dev-agent/modules/devboard/detail.js new file mode 100644 index 0000000..7996cbb --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/detail.js @@ -0,0 +1,354 @@ +// detail.js - 开发者详情页渲染器 + +let currentDevId = null; + +// 显示详情页 +function showDetailPage(devId) { + currentDevId = devId; + + const overview = document.getElementById('overview-container'); + const detail = document.getElementById('detail-container'); + + if (!overview || !detail) { + console.error('找不到容器元素'); + return; + } + + // 淡出总览页 + overview.classList.add('fade-out'); + + setTimeout(() => { + overview.style.display = 'none'; + overview.classList.remove('fade-out'); + + // 渲染详情页数据 + renderDetailPage(devId); + + detail.style.display = 'block'; + detail.classList.add('fade-in'); + + // 滚动到顶部 + window.scrollTo(0, 0); + }, 300); +} + +// 返回总览页 +function hideDetailPage() { + const overview = document.getElementById('overview-container'); + const detail = document.getElementById('detail-container'); + + detail.classList.add('fade-out'); + + setTimeout(() => { + detail.style.display = 'none'; + detail.classList.remove('fade-out'); + + overview.style.display = 'block'; + overview.classList.add('fade-in'); + + // 刷新总览页数据 + refreshDashboard(); + }, 300); +} + +// 渲染详情页 +function renderDetailPage(devId) { + const dev = getDeveloperById(devId); + if (!dev) return; + + const container = document.getElementById('detail-container'); + + // 生成五维进度条 + const progressBars = renderPCAProgressBars(dev.pca); + + // 获取模块历史 + const modules = getModuleHistory(devId); + const moduleList = renderModuleHistory(modules); + + // 获取连胜趋势数据 + const trendData = getWinTrendData(devId); + + container.innerHTML = ` +
+ +
+
+ +
+
+
+ ${dev.id} + 🏆 ${dev.wins} +
+
${dev.name}
+
+ ${dev.el} + ${dev.module} +
+
+ ${Object.entries(dev.pca).map(([key, val]) => + `${key}:${val}` + ).join('')} +
+
+
+ + +
+

📊 完整PCA雷达图

+
+ ${renderRadarChart(dev.pca, 'large')} +
+
+ ${Object.entries(dev.pca).map(([dim, score]) => + `${dim}: ${score}` + ).join('')} +
+
+ + +
+

📈 五维进度条

+
+ ${progressBars} +
+
+ + +
+

📋 模块历史

+
+ ${moduleList} +
+
+ + +
+

🏆 连胜趋势

+
+ + +
+
+
+ `; + + // 绘制趋势图 + setTimeout(() => drawTrendChart('trend-canvas', trendData), 100); +} + +// 渲染PCA五维进度条 +function renderPCAProgressBars(pca) { + const dimensions = [ + { key: 'EXE', label: '执行力', color: '#4CAF50' }, + { key: 'TEC', label: '技术深度', color: '#2196F3' }, + { key: 'SYS', label: '系统思维', color: '#9C27B0' }, + { key: 'COL', label: '协作力', color: '#FF9800' }, + { key: 'INI', label: '主动性', color: '#f44336' } + ]; + + return dimensions.map(dim => { + const score = pca[dim.key] || 0; + + return ` +
+
+ ${dim.label} + ${score} +
+
+
+
+
+
${dim.key}
+
+ `; + }).join(''); +} + +// 渲染模块历史列表 +function renderModuleHistory(modules) { + if (!modules || modules.length === 0) { + return '

暂无模块历史

'; + } + + return ` + + `; +} + +// 获取模块历史(模拟数据) +function getModuleHistory(devId) { + const histories = { + 'DEV-001': [ + { code: 'M-DEVBOARD-001', name: '开发者看板·环节0~2', status: '已完成', completedDate: '2026-03-10' }, + { code: 'M-FRONTEND-003', name: '前端基础组件', status: '已完成', completedDate: '2026-03-08' }, + { code: 'M-API-002', name: 'API对接练习', status: '进行中', completedDate: null } + ], + 'DEV-002': [ + { code: 'M-FRONTEND-001', name: '前端架构设计', status: '已完成', completedDate: '2026-03-09' }, + { code: 'M-UI-005', name: '组件库开发', status: '已完成', completedDate: '2026-03-07' }, + { code: 'M-TEST-003', name: '单元测试编写', status: '待开始', completedDate: null } + ], + 'DEV-003': [ + { code: 'M-BACKEND-002', name: 'API服务搭建', status: '已完成', completedDate: '2026-03-11' }, + { code: 'M-DB-001', name: '数据库设计', status: '已完成', completedDate: '2026-03-09' }, + { code: 'M-DEPLOY-002', name: '容器化部署', status: '进行中', completedDate: null } + ], + 'DEV-004': [ + { code: 'M-DEVBOARD-001', name: '开发者看板·环节0~2', status: '已完成', completedDate: '2026-03-11' }, + { code: 'M-CANVAS-005', name: 'Canvas雷达图绘制', status: '已完成', completedDate: '2026-03-09' }, + { code: 'M-COMPONENTS-003', name: '组件化思维训练', status: '已完成', completedDate: '2026-03-07' }, + { code: 'M-DEPLOY-001', name: '静态部署练习', status: '待开始', completedDate: null } + ] + }; + return histories[devId] || histories['DEV-001']; +} + +// 获取连胜趋势数据 +function getWinTrendData(devId) { + const trends = { + 'DEV-001': [ + { date: '03-06', value: 3 }, + { date: '03-07', value: 3 }, + { date: '03-08', value: 4 }, + { date: '03-09', value: 4 }, + { date: '03-10', value: 5 }, + { date: '03-11', value: 5 }, + { date: '03-12', value: 5 } + ], + 'DEV-002': [ + { date: '03-06', value: 5 }, + { date: '03-07', value: 6 }, + { date: '03-08', value: 6 }, + { date: '03-09', value: 7 }, + { date: '03-10', value: 7 }, + { date: '03-11', value: 8 }, + { date: '03-12', value: 8 } + ], + 'DEV-003': [ + { date: '03-06', value: 1 }, + { date: '03-07', value: 1 }, + { date: '03-08', value: 2 }, + { date: '03-09', value: 2 }, + { date: '03-10', value: 3 }, + { date: '03-11', value: 3 }, + { date: '03-12', value: 3 } + ], + 'DEV-004': [ + { date: '03-06', value: 8 }, + { date: '03-07', value: 9 }, + { date: '03-08', value: 9 }, + { date: '03-09', value: 10 }, + { date: '03-10', value: 11 }, + { date: '03-11', value: 12 }, + { date: '03-12', value: 13 } + ] + }; + return trends[devId] || trends['DEV-001']; +} + +// 绘制趋势图 +function drawTrendChart(canvasId, data) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width; + const height = canvas.height; + const padding = 40; + + ctx.clearRect(0, 0, width, height); + + if (!data || data.length < 2) { + ctx.fillStyle = '#888'; + ctx.font = '14px monospace'; + ctx.fillText('暂无连胜数据', padding, height/2); + return; + } + + // 计算坐标范围 + const values = data.map(d => d.value); + const maxValue = Math.max(...values) + 1; + const minValue = Math.max(0, Math.min(...values) - 1); + const xStep = (width - 2 * padding) / (data.length - 1); + + // 绘制网格线 + ctx.strokeStyle = '#333'; + ctx.lineWidth = 1; + ctx.beginPath(); + for (let i = 0; i <= 5; i++) { + const y = padding + (i * (height - 2 * padding) / 5); + ctx.moveTo(padding, y); + ctx.lineTo(width - padding, y); + } + ctx.strokeStyle = '#444'; + ctx.stroke(); + + // 绘制轴线 + ctx.beginPath(); + ctx.strokeStyle = '#666'; + ctx.lineWidth = 2; + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // 绘制折线 + ctx.beginPath(); + ctx.strokeStyle = '#4CAF50'; + ctx.lineWidth = 3; + + data.forEach((point, i) => { + const x = padding + i * xStep; + const y = padding + (height - 2 * padding) * (1 - (point.value - minValue) / (maxValue - minValue)); + + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + }); + ctx.stroke(); + + // 绘制数据点 + data.forEach((point, i) => { + const x = padding + i * xStep; + const y = padding + (height - 2 * padding) * (1 - (point.value - minValue) / (maxValue - minValue)); + + ctx.beginPath(); + ctx.arc(x, y, 6, 0, Math.PI * 2); + ctx.fillStyle = '#FFD700'; + ctx.fill(); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.stroke(); + + // 显示数值 + ctx.fillStyle = '#fff'; + ctx.font = 'bold 12px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(point.value, x, y - 15); + + // 显示日期 + ctx.fillStyle = '#aaa'; + ctx.font = '10px monospace'; + ctx.fillText(point.date, x, height - padding + 20); + }); +} + +// 导出函数 +window.showDetailPage = showDetailPage; +window.hideDetailPage = hideDetailPage; +window.getModuleHistory = getModuleHistory; +window.getWinTrendData = getWinTrendData; diff --git a/agents/zhuyuan-dev-agent/modules/devboard/index.html b/agents/zhuyuan-dev-agent/modules/devboard/index.html new file mode 100644 index 0000000..eb5e1f6 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/index.html @@ -0,0 +1,35 @@ + + + + + + 开发者实时看板 · HoloLake + + + +

🚀 HoloLake 开发者实时看板

+
+
+
+ + + + + + + + + + + diff --git a/agents/zhuyuan-dev-agent/modules/devboard/js/api.js b/agents/zhuyuan-dev-agent/modules/devboard/js/api.js new file mode 100644 index 0000000..88993f0 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/js/api.js @@ -0,0 +1,54 @@ +const MOCK_DEVELOPERS = [ + { id: 'DEV-001', name: '玄泓', streak: 8, el: 6, pca: { exe: 75, tec: 68, sys: 82, col: 79, ini: 85 }, totalScore: 78, status: 'doing', module: 'M-CORE', lastActive: '2026-03-11T10:30:00Z' }, + { id: 'DEV-002', name: '苍钺', streak: 5, el: 5, pca: { exe: 82, tec: 71, sys: 65, col: 90, ini: 72 }, totalScore: 76, status: 'pending', module: 'M-UI', lastActive: '2026-03-11T09:15:00Z' }, + { id: 'DEV-003', name: '桔子', streak: 13, el: 8, pca: { exe: 88, tec: 85, sys: 92, col: 78, ini: 90 }, totalScore: 87, status: 'doing', module: 'M-ORCHESTRATOR', lastActive: '2026-03-11T11:20:00Z' }, + { id: 'DEV-004', name: '之之', streak: 12, el: 8, pca: { exe: 70, tec: 32, sys: 80, col: 80, ini: 83 }, totalScore: 66, status: 'doing', module: 'M-DEVBOARD', lastActive: '2026-03-11T12:05:00Z' }, + { id: 'DEV-005', name: '琉光', streak: 7, el: 6, pca: { exe: 79, tec: 74, sys: 71, col: 88, ini: 77 }, totalScore: 78, status: 'done', module: 'M-TEST', lastActive: '2026-03-10T16:40:00Z' }, + { id: 'DEV-006', name: '墨羽', streak: 4, el: 4, pca: { exe: 65, tec: 60, sys: 55, col: 92, ini: 68 }, totalScore: 68, status: 'doing', module: 'M-DOC', lastActive: '2026-03-11T08:50:00Z' }, + { id: 'DEV-007', name: '霜砚', streak: 10, el: 7, pca: { exe: 84, tec: 79, sys: 81, col: 75, ini: 82 }, totalScore: 80, status: 'blocked', module: 'M-API', lastActive: '2026-03-11T07:30:00Z' }, + { id: 'DEV-008', name: '岚茵', streak: 6, el: 5, pca: { exe: 73, tec: 70, sys: 68, col: 85, ini: 74 }, totalScore: 74, status: 'pending', module: 'M-DESIGN', lastActive: '2026-03-10T14:20:00Z' } +]; + +function getTopStreak(developers) { + if (!developers || developers.length === 0) return { name: '无', count: 0 }; + let top = developers[0]; + for (let i = 1; i < developers.length; i++) { + if (developers[i].streak > top.streak) { + top = developers[i]; + } + } + return { name: top.name, count: top.streak }; +} + +async function getDevStatus() { + return MOCK_DEVELOPERS; +} + +async function getPCA(devId) { + const dev = MOCK_DEVELOPERS.find(d => d.id === devId); + if (!dev) return null; + return { + exe: dev.pca.exe, + tec: dev.pca.tec, + sys: dev.pca.sys, + col: dev.pca.col, + ini: dev.pca.ini, + total: dev.totalScore, + level: getPCAColor(dev.totalScore).level + }; +} + +async function getAllStats() { + const activeDevs = MOCK_DEVELOPERS.filter(d => d.status === 'doing').length; + const topStreak = getTopStreak(MOCK_DEVELOPERS); + return { + totalDevs: 8, + activeDevs: activeDevs, + totalCodeLines: 85600, + totalModules: 24, + modulesCompleted: 8, + modulesInProgress: 12, + modulesPending: 4, + topStreak: topStreak + }; +} diff --git a/agents/zhuyuan-dev-agent/modules/devboard/js/app.js b/agents/zhuyuan-dev-agent/modules/devboard/js/app.js new file mode 100644 index 0000000..2184cef --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/js/app.js @@ -0,0 +1,53 @@ +let refreshInterval; +let countdownInterval; +let seconds = 30; + +function updateLastUpdateTime() { + const el = document.getElementById('last-update-time'); + if (el) { + const now = new Date(); + el.textContent = `最后更新: ${now.toLocaleTimeString('zh-CN', { hour12: false })}`; + } +} + +function updateCountdown() { + const el = document.getElementById('refresh-indicator'); + if (el) el.textContent = `⏳ ${seconds}s`; +} + +function startCountdown() { + seconds = 30; + updateCountdown(); + if (countdownInterval) clearInterval(countdownInterval); + countdownInterval = setInterval(() => { + seconds--; + if (seconds <= 0) seconds = 30; + updateCountdown(); + }, 1000); +} + +async function refreshAllData() { + const devs = await getDevStatus(); + const stats = await getAllStats(); + if (typeof renderStats === 'function') renderStats(stats); + if (typeof renderDevCards === 'function') renderDevCards(devs); + if (typeof renderRanking === 'function') renderRanking(devs); + updateLastUpdateTime(); +} + +async function initApp() { + await initBoard(); + await initRanking(); + initRadar(); + await refreshAllData(); + startCountdown(); + if (refreshInterval) clearInterval(refreshInterval); + refreshInterval = setInterval(refreshAllData, 30000); +} + +document.addEventListener('DOMContentLoaded', initApp); + +window.addEventListener('beforeunload', () => { + if (refreshInterval) clearInterval(refreshInterval); + if (countdownInterval) clearInterval(countdownInterval); +}); diff --git a/agents/zhuyuan-dev-agent/modules/devboard/js/board.js b/agents/zhuyuan-dev-agent/modules/devboard/js/board.js new file mode 100644 index 0000000..1df57d1 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/js/board.js @@ -0,0 +1,72 @@ +function renderStats(stats) { + const container = document.getElementById('stats-container'); + if (!container) return; + container.innerHTML = ` +
+
+
活跃开发者
+
${stats.activeDevs}
+
总人数 ${stats.totalDevs}
+
+
+
总代码量
+
${formatNumber(stats.totalCodeLines)}
+
行代码
+
+
+
模块进度
+
${stats.modulesCompleted}/${stats.totalModules}
+
✅ ${stats.modulesCompleted} / ⚡ ${stats.modulesInProgress} / ⏳ ${stats.modulesPending}
+
+
+
最高连胜
+
${stats.topStreak.count}
+
${stats.topStreak.name} ${getStreakEmoji(stats.topStreak.count)}
+
+
+ `; +} + +function renderDevCards(developers) { + const container = document.getElementById('devgrid-container'); + if (!container) return; + const sorted = [...developers].sort((a, b) => b.streak - a.streak); + let html = '
'; + sorted.forEach(dev => { + const pcaColor = getPCAColor(dev.totalScore); + html += ` +
+
+
${dev.name[0]}
+
+
${dev.name}
+
${dev.id}
+
+
+
📁 ${dev.module}
+
+
${dev.streak}连胜
+
+ EL-${dev.el} + ${pcaColor.level} · ${dev.totalScore} +
+
+ +
+ `; + }); + html += '
'; + container.innerHTML = html; +} + +async function initBoard() { + const devs = await getDevStatus(); + const stats = await getAllStats(); + renderStats(stats); + renderDevCards(devs); +} + +window.initBoard = initBoard; diff --git a/agents/zhuyuan-dev-agent/modules/devboard/js/radar.js b/agents/zhuyuan-dev-agent/modules/devboard/js/radar.js new file mode 100644 index 0000000..49c327c --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/js/radar.js @@ -0,0 +1,112 @@ +function drawRadar(canvasId, pcaData) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const w = canvas.width; + const h = canvas.height; + const cx = w / 2; + const cy = h / 2; + const maxR = Math.min(w, h) * 0.35; + + ctx.clearRect(0, 0, w, h); + + const dims = [ + { label: '执行力', val: pcaData.exe || 0 }, + { label: '技术纵深', val: pcaData.tec || 0 }, + { label: '系统理解', val: pcaData.sys || 0 }, + { label: '协作力', val: pcaData.col || 0 }, + { label: '主动性', val: pcaData.ini || 0 } + ]; + + for (let lv = 1; lv <= 5; lv++) { + const r = (maxR * lv) / 5; + ctx.beginPath(); + for (let i = 0; i < 5; i++) { + const angle = (i * 2 * Math.PI / 5) - Math.PI / 2; + const x = cx + r * Math.cos(angle); + const y = cy + r * Math.sin(angle); + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.closePath(); + ctx.strokeStyle = '#2d3748'; + ctx.stroke(); + } + + for (let i = 0; i < 5; i++) { + const angle = (i * 2 * Math.PI / 5) - Math.PI / 2; + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.lineTo(cx + maxR * Math.cos(angle), cy + maxR * Math.sin(angle)); + ctx.strokeStyle = '#4a5568'; + ctx.stroke(); + } + + ctx.fillStyle = '#cbd5e0'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + for (let i = 0; i < 5; i++) { + const angle = (i * 2 * Math.PI / 5) - Math.PI / 2; + const x = cx + (maxR + 25) * Math.cos(angle); + const y = cy + (maxR + 25) * Math.sin(angle); + ctx.fillText(dims[i].label, x, y); + } + + const points = []; + for (let i = 0; i < 5; i++) { + const angle = (i * 2 * Math.PI / 5) - Math.PI / 2; + const r = (dims[i].val / 100) * maxR; + points.push({ + x: cx + r * Math.cos(angle), + y: cy + r * Math.sin(angle) + }); + } + + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + for (let i = 1; i < 5; i++) ctx.lineTo(points[i].x, points[i].y); + ctx.closePath(); + ctx.fillStyle = 'rgba(0, 180, 216, 0.3)'; + ctx.fill(); + ctx.strokeStyle = '#00b4d8'; + ctx.lineWidth = 2; + ctx.stroke(); + + points.forEach(p => { + ctx.beginPath(); + ctx.arc(p.x, p.y, 4, 0, 2 * Math.PI); + ctx.fillStyle = '#fff'; + ctx.fill(); + ctx.strokeStyle = '#00b4d8'; + ctx.lineWidth = 2; + ctx.stroke(); + }); + + ctx.fillStyle = '#fff'; + ctx.font = 'bold 24px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(pcaData.total || '0', cx, cy - 10); + + ctx.font = 'bold 14px sans-serif'; + const colorInfo = getPCAColor(pcaData.total || 0); + ctx.fillStyle = colorInfo.color; + ctx.fillText(pcaData.level || 'C', cx, cy + 15); +} + +async function loadRadarForDev(devId) { + const pcaData = await getPCA(devId); + if (pcaData) drawRadar('pca-canvas', pcaData); +} + +function initRadar() { + document.querySelectorAll('.dev-card').forEach(card => { + card.addEventListener('click', () => { + loadRadarForDev(card.dataset.devId); + }); + }); + loadRadarForDev('DEV-004'); +} + +window.initRadar = initRadar; +window.loadRadarForDev = loadRadarForDev; diff --git a/agents/zhuyuan-dev-agent/modules/devboard/js/ranking.js b/agents/zhuyuan-dev-agent/modules/devboard/js/ranking.js new file mode 100644 index 0000000..fb14624 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/js/ranking.js @@ -0,0 +1,34 @@ +function renderRanking(developers) { + const container = document.getElementById('ranking-container'); + if (!container) return; + const sorted = [...developers].sort((a, b) => b.streak - a.streak); + const maxStreak = sorted[0].streak; + + let html = '

🏆 连胜排行榜

实时更新
'; + + sorted.forEach((dev, idx) => { + const medal = idx === 0 ? '🥇' : idx === 1 ? '🥈' : idx === 2 ? '🥉' : `${idx+1}.`; + const percent = (dev.streak / maxStreak) * 100; + html += ` +
+
${medal}
+
+
${dev.name}
+
+
+ ${dev.streak}连胜 +
+
+
+ `; + }); + html += '
'; + container.innerHTML = html; +} + +async function initRanking() { + const devs = await getDevStatus(); + renderRanking(devs); +} + +window.initRanking = initRanking; diff --git a/agents/zhuyuan-dev-agent/modules/devboard/js/utils.js b/agents/zhuyuan-dev-agent/modules/devboard/js/utils.js new file mode 100644 index 0000000..a705c93 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/js/utils.js @@ -0,0 +1,36 @@ +function formatDate(isoString) { + if (!isoString) return '--:--:--'; + const date = new Date(isoString); + return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); +} + +function getStreakEmoji(count) { + if (count >= 20) return '🔥🔥🔥'; + if (count >= 15) return '🔥🔥'; + if (count >= 10) return '🔥'; + if (count >= 5) return '⚡'; + if (count >= 3) return '👍'; + return '👣'; +} + +function getPCAColor(score) { + if (score >= 90) return { color: '#FFD700', level: 'S', text: '传奇' }; + if (score >= 80) return { color: '#00B4D8', level: 'A', text: '精英' }; + if (score >= 70) return { color: '#10B981', level: 'B', text: '专家' }; + if (score >= 60) return { color: '#F59E0B', level: 'C', text: '熟手' }; + return { color: '#EF4444', level: 'D', text: '新手' }; +} + +function getStatusBadge(status) { + const map = { + 'done': '✅ 已完成', + 'doing': '⚡ 进行中', + 'pending': '⏳ 待分配', + 'blocked': '⚠️ 阻塞' + }; + return map[status] || '⏳ 待分配'; +} + +function formatNumber(num) { + return num?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0'; +} diff --git a/agents/zhuyuan-dev-agent/modules/devboard/main.js b/agents/zhuyuan-dev-agent/modules/devboard/main.js new file mode 100644 index 0000000..3ace862 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/main.js @@ -0,0 +1,236 @@ +// main.js - 主逻辑(增强版) + +// 页面加载完成后初始化 +document.addEventListener('DOMContentLoaded', function() { + // 初始渲染 + refreshDashboard(); + + // 搜索功能(带防抖和动画) + const searchInput = document.getElementById('search-input'); + if (searchInput) { + let searchTimer; + searchInput.addEventListener('input', function(e) { + clearTimeout(searchTimer); + + // 添加输入时的微动效 + this.style.transform = 'scale(1.02)'; + setTimeout(() => { + this.style.transform = ''; + }, 150); + + searchTimer = setTimeout(() => { + applyFilters(); + }, 300); + }); + } + + // 筛选按钮 + const filterBtns = document.querySelectorAll('.filter-btn'); + filterBtns.forEach(btn => { + btn.addEventListener('click', function() { + // 按钮点击动画 + this.style.transform = 'scale(0.95)'; + setTimeout(() => { + this.style.transform = ''; + }, 150); + + // 更新按钮状态 + filterBtns.forEach(b => b.classList.remove('active')); + this.classList.add('active'); + + // 应用筛选 + applyFilters(); + }); + }); + + // 卡片点击事件(使用事件委托) + document.addEventListener('click', function(e) { + const card = e.target.closest('.dev-card'); + if (card) { + const devId = card.dataset.devId; + if (devId && window.showDetailPage) { + // 添加点击动画 + card.style.transform = 'scale(0.98)'; + card.style.opacity = '0.8'; + + setTimeout(() => { + card.style.transform = ''; + card.style.opacity = ''; + window.showDetailPage(devId); + }, 150); + } + } + }); + + // 键盘导航 + document.addEventListener('keydown', function(e) { + // 获取所有可聚焦的卡片 + const cards = Array.from(document.querySelectorAll('.dev-card')); + const currentIndex = cards.indexOf(document.activeElement); + + // Tab键(默认行为已支持,我们只加样式) + if (e.key === 'Tab') { + setTimeout(() => { + const focused = document.activeElement; + if (focused.classList.contains('dev-card')) { + focused.style.outline = '3px solid #ffd700'; + focused.style.transform = 'scale(1.02)'; + } + }, 50); + } + + // 箭头键导航 + if (e.key === 'ArrowRight' || e.key === 'ArrowLeft' || + e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault(); + + if (cards.length === 0) return; + + const cols = Math.floor(document.querySelector('.developers-grid').offsetWidth / 300); + let nextIndex; + + if (e.key === 'ArrowRight') { + nextIndex = currentIndex + 1; + } else if (e.key === 'ArrowLeft') { + nextIndex = currentIndex - 1; + } else if (e.key === 'ArrowDown') { + nextIndex = currentIndex + cols; + } else if (e.key === 'ArrowUp') { + nextIndex = currentIndex - cols; + } + + if (nextIndex >= 0 && nextIndex < cards.length) { + cards[nextIndex].focus(); + } + } + + // Enter/Space 打开详情 + if (e.key === 'Enter' || e.key === ' ') { + const target = e.target.closest('.dev-card'); + if (target) { + e.preventDefault(); + const devId = target.dataset.devId; + if (devId && window.showDetailPage) { + window.showDetailPage(devId); + } + } + } + + // ESC返回总览 + if (e.key === 'Escape') { + const detail = document.getElementById('detail-container'); + if (detail && detail.style.display !== 'none' && window.hideDetailPage) { + window.hideDetailPage(); + } + } + }); + + // 失去焦点时移除高亮 + document.addEventListener('blur', function(e) { + if (e.target.classList.contains('dev-card')) { + e.target.style.outline = ''; + e.target.style.transform = ''; + } + }, true); +}); + +// 应用搜索和筛选 +function applyFilters() { + const searchTerm = document.getElementById('search-input')?.value || ''; + const activeFilter = document.querySelector('.filter-btn.active')?.dataset.filter || 'all'; + + // 获取筛选后的开发者 + const filtered = filterDevelopers(searchTerm, activeFilter); + + // 重新渲染卡片 + const grid = document.getElementById('developers-grid'); + if (grid) { + // 添加淡出动画 + grid.style.opacity = '0.5'; + + setTimeout(() => { + if (filtered.length === 0) { + // 显示无结果提示 + grid.innerHTML = '
🔍 没有找到匹配的开发者
'; + } else { + grid.innerHTML = renderDeveloperCards(filtered); + + // 添加展开/收起动画 + Array.from(grid.children).forEach((card, index) => { + card.style.animation = `cardPopIn 0.3s ease ${index * 0.05}s forwards`; + card.style.opacity = '0'; + setTimeout(() => { + card.style.opacity = '1'; + }, 50 + index * 20); + }); + } + + grid.style.opacity = '1'; + }, 200); + } + + // 更新排行榜(排行榜不筛选,但高亮匹配项) + const ranking = document.getElementById('ranking-list'); + if (ranking) { + const allDevs = getDevelopers(); + ranking.innerHTML = renderRanking(allDevs); + + // 高亮筛选出的开发者 + if (filtered.length < allDevs.length && filtered.length > 0) { + const filteredIds = filtered.map(d => d.id); + document.querySelectorAll('.ranking-item').forEach(item => { + const nameEl = item.querySelector('.rank-name'); + if (nameEl) { + const dev = allDevs.find(d => d.name === nameEl.textContent); + if (dev && filteredIds.includes(dev.id)) { + item.classList.add('match-highlight'); + + // 添加数字滚动动画 + const winsEl = item.querySelector('.rank-wins'); + if (winsEl) { + const oldValue = parseInt(winsEl.textContent) || 0; + animateNumber(winsEl, oldValue, dev.wins, 500); + } + } else { + item.classList.remove('match-highlight'); + } + } + }); + } else { + document.querySelectorAll('.ranking-item').forEach(item => { + item.classList.remove('match-highlight'); + }); + } + } +} + +// 数字滚动动画 +function animateNumber(element, start, end, duration = 1000) { + const range = end - start; + const increment = range / (duration / 16); + let current = start; + const startTime = performance.now(); + + function update(currentTime) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + + // 缓动函数 + const easeOutQuart = 1 - Math.pow(1 - progress, 3); + current = start + (range * easeOutQuart); + + element.textContent = Math.round(current) + (element.textContent.includes('连胜') ? '连胜' : ''); + + if (progress < 1) { + requestAnimationFrame(update); + } else { + element.textContent = end + (element.textContent.includes('连胜') ? '连胜' : ''); + } + } + + requestAnimationFrame(update); +} + +// 导出函数 +window.applyFilters = applyFilters; +window.animateNumber = animateNumber; diff --git a/agents/zhuyuan-dev-agent/modules/devboard/radar.js b/agents/zhuyuan-dev-agent/modules/devboard/radar.js new file mode 100644 index 0000000..2cd2d86 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/radar.js @@ -0,0 +1,116 @@ +// radar.js - 雷达图绘制 +function drawRadarChart(canvasId, data) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width; + const height = canvas.height; + const centerX = width / 2; + const centerY = height / 2; + const radius = Math.min(width, height) * 0.35; + + const dimensions = ['EXE', 'TEC', 'SYS', 'COL', 'INI']; + const angleStep = (Math.PI * 2) / dimensions.length; + + // 清空画布 + ctx.clearRect(0, 0, width, height); + + // 绘制背景网格 + ctx.strokeStyle = '#2a3040'; + ctx.lineWidth = 1; + + // 绘制5层同心圆 + for (let level = 1; level <= 5; level++) { + ctx.beginPath(); + for (let i = 0; i <= dimensions.length; i++) { + const angle = i * angleStep - Math.PI / 2; + const x = centerX + radius * (level / 5) * Math.cos(angle); + const y = centerY + radius * (level / 5) * Math.sin(angle); + + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.closePath(); + ctx.strokeStyle = '#2a3040'; + ctx.stroke(); + } + + // 绘制轴线 + for (let i = 0; i < dimensions.length; i++) { + const angle = i * angleStep - Math.PI / 2; + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.lineTo(x, y); + ctx.strokeStyle = '#3a4050'; + ctx.stroke(); + + // 标注维度 + const labelX = centerX + (radius + 15) * Math.cos(angle); + const labelY = centerY + (radius + 15) * Math.sin(angle); + ctx.fillStyle = '#4a9eff'; + ctx.font = 'bold 14px monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(dimensions[i], labelX, labelY); + } + + // 绘制数据 + if (data) { + ctx.beginPath(); + for (let i = 0; i < dimensions.length; i++) { + const value = data[dimensions[i]] || 0; + const angle = i * angleStep - Math.PI / 2; + const r = radius * (value / 100); + const x = centerX + r * Math.cos(angle); + const y = centerY + r * Math.sin(angle); + + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.closePath(); + ctx.fillStyle = 'rgba(74, 158, 255, 0.3)'; + ctx.fill(); + ctx.strokeStyle = '#4a9eff'; + ctx.lineWidth = 3; + ctx.stroke(); + + // 绘制数据点 + for (let i = 0; i < dimensions.length; i++) { + const value = data[dimensions[i]] || 0; + const angle = i * angleStep - Math.PI / 2; + const r = radius * (value / 100); + const x = centerX + r * Math.cos(angle); + const y = centerY + r * Math.sin(angle); + + ctx.beginPath(); + ctx.arc(x, y, 5, 0, Math.PI * 2); + ctx.fillStyle = '#fff'; + ctx.fill(); + ctx.strokeStyle = '#4a9eff'; + ctx.lineWidth = 2; + ctx.stroke(); + } + } +} + +// 渲染雷达图HTML(供detail.js调用) +function renderRadarChart(pcaData, size = 'normal') { + const canvasId = 'radar-' + Math.random().toString(36).substr(2, 9); + const width = size === 'large' ? 400 : 300; + const height = size === 'large' ? 300 : 250; + + setTimeout(() => { + const canvas = document.getElementById(canvasId); + if (canvas) drawRadarChart(canvas.id, pcaData); + }, 100); + + return ``; +} + +// 导出函数 +window.drawRadarChart = drawRadarChart; +window.renderRadarChart = renderRadarChart; diff --git a/agents/zhuyuan-dev-agent/modules/devboard/style.css b/agents/zhuyuan-dev-agent/modules/devboard/style.css new file mode 100644 index 0000000..18ed43c --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/devboard/style.css @@ -0,0 +1,287 @@ +/* style.css - 基础样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; + background: #0a0c14; + color: #e0e0e0; + line-height: 1.6; +} + +/* 总览页容器 */ +#overview-container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +/* 头部 */ +.dashboard-header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(135deg, #1a1f2e, #0f1219); + border-radius: 20px; + border: 1px solid #2a3040; +} + +.dashboard-header h1 { + color: #4a9eff; + font-size: 2em; + margin-bottom: 10px; + text-shadow: 0 0 20px rgba(74, 158, 255, 0.3); +} + +/* 控制区 */ +.controls-section { + display: flex; + gap: 20px; + margin-bottom: 30px; + flex-wrap: wrap; +} + +.search-box { + flex: 1; + min-width: 250px; + padding: 12px 20px; + background: #1a1f2e; + border: 2px solid #2a3040; + border-radius: 30px; + color: #fff; + font-size: 16px; + transition: all 0.3s; +} + +.search-box:focus { + outline: none; + border-color: #4a9eff; + box-shadow: 0 0 20px rgba(74, 158, 255, 0.2); +} + +.filter-buttons { + display: flex; + gap: 10px; +} + +.filter-btn { + padding: 10px 24px; + background: #1a1f2e; + border: 2px solid #2a3040; + border-radius: 30px; + color: #a0a0a0; + font-weight: bold; + cursor: pointer; + transition: all 0.3s; +} + +.filter-btn.active { + background: #4a9eff; + border-color: #4a9eff; + color: #fff; +} + +.filter-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(74, 158, 255, 0.2); +} + +/* 开发者网格 */ +.developers-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + margin-bottom: 40px; +} + +/* 统计区 */ +.stats-section { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.radar-container, .ranking-container { + background: #1a1f2e; + border-radius: 20px; + padding: 20px; + border: 1px solid #2a3040; +} + +.radar-container h2, .ranking-container h2 { + color: #4a9eff; + margin-bottom: 20px; + font-size: 1.2em; +} + +#team-radar { + width: 100%; + height: auto; + max-height: 300px; +} + +/* 响应式 */ +@media (max-width: 768px) { + .stats-section { + grid-template-columns: 1fr; + } + + .controls-section { + flex-direction: column; + } + + .filter-buttons { + justify-content: center; + } +} + +/* 动画 */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.fade-in { + animation: fadeIn 0.5s ease forwards; +} + +/* ===== 交互增强样式 ===== */ + +/* 搜索框增强 */ +.search-box { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.search-box:focus { + transform: scale(1.02); + background: #1e2436; +} + +/* 筛选按钮动画 */ +.filter-btn { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.filter-btn::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transform: translate(-50%, -50%); + transition: width 0.3s, height 0.3s; +} + +.filter-btn:active::after { + width: 100px; + height: 100px; +} + +/* 卡片网格动画 */ +.developers-grid { + transition: opacity 0.3s ease; + min-height: 400px; +} + +/* 卡片展开/收起动画 */ +.dev-card { + animation: cardPopIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transform-origin: center; +} + +@keyframes cardPopIn { + 0% { + opacity: 0; + transform: scale(0.8) translateY(20px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* 键盘焦点样式 */ +.dev-card:focus-visible { + outline: 3px solid #ffd700; + outline-offset: 2px; + transform: scale(1.02); + box-shadow: 0 0 30px rgba(255, 215, 0, 0.3); +} + +/* 数字滚动动画 */ +.stat-value { + display: inline-block; + transition: all 0.3s ease; + font-feature-settings: "tnum"; +} + +/* 无结果提示 */ +.no-results { + grid-column: 1 / -1; + text-align: center; + padding: 60px; + background: rgba(255, 255, 255, 0.05); + border-radius: 20px; + color: #888; + font-size: 18px; + border: 2px dashed #2a3040; + animation: fadeIn 0.5s ease; +} + +/* 高亮筛选匹配项 */ +.ranking-item.match-highlight { + background: rgba(74, 158, 255, 0.2); + border-left: 4px solid #4a9eff; + transform: translateX(5px); +} + +/* 淡入淡出动画增强 */ +.fade-in { + animation: fadeIn 0.3s ease forwards; +} + +.fade-out { + animation: fadeOut 0.3s ease forwards; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-10px); + } +} + +/* 响应式优化 */ +@media (max-width: 768px) { + .dev-card:focus-visible { + transform: scale(1.01); + } + + .filter-btn:active::after { + width: 60px; + height: 60px; + } +} diff --git a/agents/zhuyuan-dev-agent/modules/email-notify.js b/agents/zhuyuan-dev-agent/modules/email-notify.js new file mode 100644 index 0000000..9b3f565 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/email-notify.js @@ -0,0 +1,90 @@ +// ═══════════════════════════════════════ +// 铸渊开发Agent · 邮件通知模块 +// QQ邮箱 SMTP · smtp.qq.com:465 +// ═══════════════════════════════════════ + +const nodemailer = require("nodemailer"); + +let transporter = null; + +function getTransporter() { + if (transporter) return transporter; + + const user = process.env.ZY_SMTP_USER; + const pass = process.env.ZY_SMTP_PASS; + + if (!user || !pass) { + console.warn("[EmailNotify] ZY_SMTP_USER 或 ZY_SMTP_PASS 未配置,邮件功能不可用"); + return null; + } + + transporter = nodemailer.createTransport({ + host: "smtp.qq.com", + port: 465, + secure: true, + auth: { user, pass } + }); + + console.log("[EmailNotify] SMTP 已连接 · " + user); + return transporter; +} + +/** + * 发送通知邮件 + * @param {string} to - 收件人邮箱 + * @param {string} planId - 规划ID + * @param {string} status - 状态: started / completed / failed + * @param {object} summary - { task_count, done_count, failed_count, errors, log_url } + */ +async function sendNotification(to, planId, status, summary = {}) { + const t = getTransporter(); + if (!t) return { sent: false, reason: "SMTP未配置" }; + + const statusEmoji = { started: "🚀", completed: "✅", failed: "❌" }; + const emoji = statusEmoji[status] || "📋"; + + const subject = emoji + " 铸渊DevAgent · " + planId + " · " + status; + + const lines = [ + "铸渊开发Agent · 任务执行报告", + "══════════════════════════════", + "", + "规划ID: " + planId, + "状态: " + status, + "时间: " + new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), + "", + ]; + + if (summary.task_count !== undefined) { + lines.push("任务总数: " + summary.task_count); + lines.push("已完成: " + (summary.done_count || 0)); + lines.push("失败: " + (summary.failed_count || 0)); + } + + if (summary.errors && summary.errors.length > 0) { + lines.push("", "错误详情:"); + summary.errors.forEach(function(e, i) { + lines.push(" " + (i + 1) + ". " + e); + }); + } + + lines.push("", "══════════════════════════════", "铸渊 ICE-GL-ZY001 · 国作登字-2026-A-00037559"); + + const mailOptions = { + from: process.env.ZY_SMTP_USER, + to: to, + subject: subject, + text: lines.join("\n") + }; + + try { + const info = await t.sendMail(mailOptions); + console.log("[EmailNotify] 邮件已发送: " + info.messageId); + return { sent: true, messageId: info.messageId }; + } catch (err) { + console.error("[EmailNotify] 发送失败:", err.message); + return { sent: false, reason: err.message }; + } +} + +module.exports = { sendNotification }; diff --git a/agents/zhuyuan-dev-agent/modules/executor.js b/agents/zhuyuan-dev-agent/modules/executor.js new file mode 100644 index 0000000..8ebde7d --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/executor.js @@ -0,0 +1,177 @@ +// ═══════════════════════════════════════ +// 铸渊开发Agent · 执行引擎 v2.0 +// 宪法约束:3次重试上限 · 统一输出接口 · 依赖链 · 紧急停止 +// ═══════════════════════════════════════ + +const fs = require("fs"); +const path = require("path"); +const { execSync } = require("child_process"); + +const WORKSPACE = process.env.WORKSPACE_DIR || "/opt/guanghu/zhuyuan-workspace"; +const CONSTITUTION = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "agent-constitution.json"), "utf-8")); +const { fail_protocol, unified_interface, dependency_chain, emergency_stop } = CONSTITUTION; + +if (!fs.existsSync(WORKSPACE)) fs.mkdirSync(WORKSPACE, { recursive: true }); + +/** + * 统一包装输出——所有模块输出必须经过这里 + */ +function uniformResult(taskId, step, ok, extra = {}) { + return { + ok, + task_id: taskId, + step, + action: extra.action || "unknown", + output: extra.output || null, + error: extra.error || null, + retries: extra.retries || 0, + skipped: !!extra.skipped, + blocked: !!extra.blocked + }; +} + +/** + * 带重试的执行 + */ +async function executeWithRetry(substep, logFn, retriesLeft) { + const start = Date.now(); + const attempt = fail_protocol.max_retries_per_task - retriesLeft + 1; + logFn("exec_attempt", { step: substep.step, action: substep.action, attempt, retriesLeft }); + + try { + let result; + switch (substep.action) { + case "write_file": + result = doWriteFile(substep); + break; + case "run_cmd": + result = doRunCmd(substep); + break; + case "call_api": + result = await doCallApi(substep); + break; + case "check": + result = doCheck(substep); + break; + default: + return uniformResult(substep.task_id, substep.step, false, { action: substep.action, error: "未知动作类型: " + substep.action }); + } + + if (result.ok) { + logFn("exec_done", { step: substep.step, result: "ok", ms: Date.now() - start, attempt }); + return uniformResult(substep.task_id, substep.step, true, { action: substep.action, output: result.output || result.path, retries: attempt - 1 }); + } + + // 失败了但还有重试次数 + if (retriesLeft > 1) { + logFn("exec_retry", { step: substep.step, error: result.error, retriesLeft: retriesLeft - 1 }); + return executeWithRetry(substep, logFn, retriesLeft - 1); + } + + // 重试耗尽 → 跳过 + logFn("exec_skip", { step: substep.step, error: result.error, reason: "重试" + fail_protocol.max_retries_per_task + "次全部失败,跳过" }); + return uniformResult(substep.task_id, substep.step, false, { action: substep.action, error: result.error, retries: fail_protocol.max_retries_per_task, skipped: true }); + + } catch (err) { + if (retriesLeft > 1) { + logFn("exec_retry", { step: substep.step, error: err.message, retriesLeft: retriesLeft - 1 }); + return executeWithRetry(substep, logFn, retriesLeft - 1); + } + logFn("exec_skip", { step: substep.step, error: err.message, reason: "重试" + fail_protocol.max_retries_per_task + "次全部失败,跳过" }); + return uniformResult(substep.task_id, substep.step, false, { action: substep.action, error: err.message, retries: fail_protocol.max_retries_per_task, skipped: true }); + } +} + +function doWriteFile(s) { + const fp = path.resolve(WORKSPACE, s.target); + const dir = path.dirname(fp); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(fp, s.content || "", "utf-8"); + return { ok: true, path: fp }; +} + +function doRunCmd(s) { + const out = execSync(s.target, { cwd: s.cwd || WORKSPACE, encoding: "utf-8", timeout: 60000, maxBuffer: 10*1024*1024 }); + return { ok: true, output: out.slice(0, 5000) }; +} + +async function doCallApi(s) { + const https = require("https"); + const body = JSON.stringify(s.body || {}); + const url = new URL(s.target); + return new Promise((resolve) => { + const req = https.request({ + hostname: url.hostname, path: url.pathname, method: s.method || "POST", + headers: { "Content-Type": "application/json", ...(s.headers || {}) } + }, (res) => { + let data = ""; + res.on("data", c => data += c); + res.on("end", () => resolve({ ok: res.statusCode < 400, status: res.statusCode, body: data.slice(0, 2000) })); + }); + req.on("error", e => resolve({ ok: false, error: e.message })); + if (body !== "{}") req.write(body); + req.end(); + }); +} + +function doCheck(s) { + const fp = path.resolve(WORKSPACE, s.target); + return { ok: fs.existsSync(fp), exists: fs.existsSync(fp), path: fp }; +} + +/** + * 执行完整规划——宪法约束版本 + */ +async function executePlan(plan, logFn) { + console.log("[Executor v2] 开始执行: " + plan.plan_id + " · 宪法约束模式"); + logFn("constitution_loaded", { max_retries: fail_protocol.max_retries_per_task }); + + const results = []; + let doneCount = 0, failCount = 0, skipCount = 0, consecutiveFails = 0; + const blockedTasks = new Set(); + + for (const task of plan.tasks) { + const substeps = task.substeps || []; + + for (const ss of substeps) { + // 检查是否被阻塞 + if (ss.depends_on && blockedTasks.has(ss.depends_on)) { + const blocked = uniformResult(task.id, ss.step, false, { action: ss.action, blocked: true, error: "上游依赖 " + ss.depends_on + " 失败,此任务阻塞" }); + results.push(blocked); + logFn("exec_blocked", { step: ss.step, depends_on: ss.depends_on }); + failCount++; + continue; + } + + // 执行(带重试) + const r = await executeWithRetry({ ...ss, task_id: task.id }, logFn, fail_protocol.max_retries_per_task); + results.push(r); + + if (r.ok) { + doneCount++; + consecutiveFails = 0; + } else if (r.skipped) { + skipCount++; + consecutiveFails++; + // 被跳过的任务 → 下游阻塞 + blockedTasks.add(task.id + ":" + ss.step); + } else { + failCount++; + consecutiveFails++; + blockedTasks.add(task.id + ":" + ss.step); + } + + // 紧急停止检查 + if (consecutiveFails >= 5) { + logFn("emergency_stop", { reason: "连续" + consecutiveFails + "个任务失败", summary: { done: doneCount, fail: failCount, skip: skipCount } }); + return { plan_id: plan.plan_id, emergency_stop: true, done: doneCount, failed: failCount, skipped: skipCount, results }; + } + } + } + + const summary = { plan_id: plan.plan_id, total: results.length, done: doneCount, failed: failCount, skipped: skipCount, results }; + console.log("[Executor v2] 完成 · 成功 " + doneCount + " / 失败 " + failCount + " / 跳过 " + skipCount); + return summary; +} + +module.exports = { executePlan }; diff --git a/agents/zhuyuan-dev-agent/modules/logger.js b/agents/zhuyuan-dev-agent/modules/logger.js new file mode 100644 index 0000000..962b39c --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/logger.js @@ -0,0 +1,97 @@ +// ═══════════════════════════════════════ +// 铸渊开发Agent · 日志系统 +// 结构化日志 · 铸渊醒来能看懂Agent做了什么 +// ═══════════════════════════════════════ + +const fs = require("fs"); +const path = require("path"); + +const LOGS_DIR = process.env.LOGS_DIR || path.join(__dirname, "..", "logs"); + +if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true }); + +/** + * 创建一次执行的日志记录器 + */ +function createLogger(planId) { + const runId = planId + "-" + Date.now(); + const logFile = path.join(LOGS_DIR, runId + ".jsonl"); + const startTime = new Date().toISOString(); + + // 写入启动记录 + appendLine(logFile, { type: "run_start", plan_id: planId, run_id: runId, time: startTime }); + + const log = (type, data) => { + const entry = { + time: new Date().toISOString(), + type: type, + ...data + }; + appendLine(logFile, entry); + console.log("[Log:" + planId + "] " + type + " " + JSON.stringify(data).slice(0, 100)); + }; + + // 返回日志函数和元信息 + return { + log, + runId, + logFile, + finish: (summary) => { + appendLine(logFile, { type: "run_end", time: new Date().toISOString(), summary }); + return logFile; + } + }; +} + +function appendLine(file, obj) { + fs.appendFileSync(file, JSON.stringify(obj) + "\n", "utf-8"); +} + +/** + * 读取最近一次执行日志摘要(给铸渊看) + */ +function readLatestRun() { + const files = fs.readdirSync(LOGS_DIR) + .filter(f => f.endsWith(".jsonl")) + .sort() + .reverse(); + + if (files.length === 0) return null; + + const latest = path.join(LOGS_DIR, files[0]); + const content = fs.readFileSync(latest, "utf-8"); + const lines = content.trim().split("\n").map(l => { + try { return JSON.parse(l); } catch (e) { return null; } + }).filter(Boolean); + + const startLine = lines.find(l => l.type === "run_start"); + const endLine = lines.find(l => l.type === "run_end"); + const errors = lines.filter(l => l.type === "exec_error" || l.type === "exec_done" && l.result === "fail"); + + return { + plan_id: startLine?.plan_id, + run_id: startLine?.run_id, + started: startLine?.time, + ended: endLine?.time, + total_steps: lines.filter(l => l.type === "exec_start").length, + errors: errors.map(e => ({ step: e.step, error: e.error || "未知" })), + log_file: latest + }; +} + +/** + * 列出所有执行记录 + */ +function listRuns(limit = 20) { + return fs.readdirSync(LOGS_DIR) + .filter(f => f.endsWith(".jsonl")) + .sort() + .reverse() + .slice(0, limit) + .map(f => { + const stat = fs.statSync(path.join(LOGS_DIR, f)); + return { file: f, size: stat.size, modified: stat.mtime.toISOString() }; + }); +} + +module.exports = { createLogger, readLatestRun, listRuns }; diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/adapters/m06-adapter.js b/agents/zhuyuan-dev-agent/modules/m-channel/adapters/m06-adapter.js new file mode 100644 index 0000000..4651cb1 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/adapters/m08-adapter.js b/agents/zhuyuan-dev-agent/modules/m-channel/adapters/m08-adapter.js new file mode 100644 index 0000000..b549438 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/adapters/m11-adapter.js b/agents/zhuyuan-dev-agent/modules/m-channel/adapters/m11-adapter.js new file mode 100644 index 0000000..4e3a7b2 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/adapters/module-adapter.js b/agents/zhuyuan-dev-agent/modules/m-channel/adapters/module-adapter.js new file mode 100644 index 0000000..26841ce --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/all-in-one.html b/agents/zhuyuan-dev-agent/modules/m-channel/all-in-one.html new file mode 100644 index 0000000..4c1eb87 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/all-in-one.html @@ -0,0 +1,396 @@ + + + + + + 光湖频道 · 一体化版 + + + +
+ +
+ +
+
+ + + + diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/app.js b/agents/zhuyuan-dev-agent/modules/m-channel/app.js new file mode 100644 index 0000000..152d4aa --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/app.js @@ -0,0 +1,50 @@ +// 应用入口 +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; + } + + // 初始化路由器 + if (window.ChannelRouter) { + ChannelRouter.init(contentEl); + } + + // 绑定导航按钮 + 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] 初始化完成'); +}); + +// 拦截所有事件总线消息,用于调试面板 +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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/app.js b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/app.js new file mode 100644 index 0000000..25e29f0 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-router-backup.js b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-router-backup.js new file mode 100644 index 0000000..614fc3e --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-router.js b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-router.js new file mode 100644 index 0000000..b1930c6 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-state.js b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-state.js new file mode 100644 index 0000000..35ea173 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-style.css b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-style.css new file mode 100644 index 0000000..f2a8cd2 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-transition.css b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/channel-transition.css new file mode 100644 index 0000000..b13bd02 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/event-bus.js b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/event-bus.js new file mode 100644 index 0000000..ab14007 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/index.html b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/index.html new file mode 100644 index 0000000..e86e990 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/index.html @@ -0,0 +1,33 @@ + + + + + + 光湖频道 · 动态渲染引擎 + + + + +
+ +
+ +
+
+ + + + + + + + + + + diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/module-lifecycle.js b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/module-lifecycle.js new file mode 100644 index 0000000..37daed6 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/module-loader.js b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/module-loader.js new file mode 100644 index 0000000..2747442 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/module-registry.js b/agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/module-registry.js new file mode 100644 index 0000000..a0a87ff --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-analytics.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-analytics.js new file mode 100644 index 0000000..df5ed7e --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/channel-analytics.js @@ -0,0 +1,178 @@ +/** + * channel-analytics.js + * 频道数据采集核心·环节8 + 环节9设置联动 + * 记录模块访问次数、停留时间、页面加载速度 + */ + +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 { + // 记录模块访问(环节9:检查数据采集开关) + recordVisit: function(moduleId) { + // 数据采集开关检查(环节9新增) + if (typeof ChannelSettings !== 'undefined' && !ChannelSettings.get('analyticsEnabled')) { + console.log('📊 数据采集已关闭(设置中心)'); + return; + } + 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; + // 数据采集开关检查(环节9新增) + if (typeof ChannelSettings !== 'undefined' && !ChannelSettings.get('analyticsEnabled')) { + // 如果关闭采集,清空当前会话但不记录 + currentModule = null; + enterTime = null; + 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; + // 数据采集开关检查(环节9新增) + if (typeof ChannelSettings !== 'undefined' && !ChannelSettings.get('analyticsEnabled')) { + 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/agents/zhuyuan-dev-agent/modules/m-channel/channel-complete.html b/agents/zhuyuan-dev-agent/modules/m-channel/channel-complete.html new file mode 100644 index 0000000..9162957 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/channel-complete.html @@ -0,0 +1,267 @@ + + + + + + 光湖频道 · 完全版 + + + + + + +
+ +
+ +
+
+ + +
+
+

频道设置

+ +
+
+
+

布局模式

+
+ + + +
+
+
+

主题色

+
+ + + + + +
+
+
+

统计数据

+ +
+
+

其他

+ +
+
+
+ + +
+
+
+

使用统计

+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.css b/agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.css new file mode 100644 index 0000000..1269c86 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.js new file mode 100644 index 0000000..13f3555 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-enhancements.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-enhancements.js new file mode 100644 index 0000000..3689661 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-favorites.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-favorites.js new file mode 100644 index 0000000..4208f65 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-layout.css b/agents/zhuyuan-dev-agent/modules/m-channel/channel-layout.css new file mode 100644 index 0000000..31c39c3 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-notifications.css b/agents/zhuyuan-dev-agent/modules/m-channel/channel-notifications.css new file mode 100644 index 0000000..5905e8d --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-notifications.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-notifications.js new file mode 100644 index 0000000..a14c781 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-preferences.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-preferences.js new file mode 100644 index 0000000..a4ee89c --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-router.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-router.js new file mode 100644 index 0000000..0896208 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/channel-router.js @@ -0,0 +1,243 @@ +// 频道路由(支持真实模块适配器 + 卡片列表首页 + 数据采集钩子) +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; + + // 数据采集钩子:开始加载 + if (typeof ChannelAnalytics !== 'undefined') { + ChannelAnalytics.markLoadStart(); + if (channel !== 'home' && channel !== 'debug' && channel !== 'dashboard' && channel !== 'settings') { + 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; + } + + // 数据面板路由(环节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; + } + + // 设置中心路由(环节9新增) + if (channel === 'settings') { + fetch('views/channel-settings.html') + .then(r => r.text()) + .then(html => { + this.contentEl.innerHTML = html; + if (typeof SettingsUI !== 'undefined') { + SettingsUI.init(); + } + // 记录加载完成 + if (typeof ChannelAnalytics !== 'undefined') { + ChannelAnalytics.markLoadEnd('settings'); + } + }) + .catch(err => { + this.contentEl.innerHTML = '

    设置面板加载失败

    '; + console.error(err); + }); + return; + } + + // 内置频道 + if (channel === 'home') { + this.renderModuleCards(); + } else if (channel === 'debug') { + this.loadDebugPanel(); + } else { + this.contentEl.innerHTML = '

    未知频道

    '; + } + + // 对于内置频道也记录加载完成 + if (typeof ChannelAnalytics !== 'undefined' && channel !== 'dashboard' && channel !== 'settings') { + ChannelAnalytics.markLoadEnd(channel); + } + }, + + // 渲染所有模块卡片(用于首页) + 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; + } +}; diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/channel-router.js.bakcat b/agents/zhuyuan-dev-agent/modules/m-channel/channel-router.js.bakcat new file mode 100644 index 0000000..e69de29 diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/channel-settings-ui.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-settings-ui.js new file mode 100644 index 0000000..3d07af7 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/channel-settings-ui.js @@ -0,0 +1,123 @@ +/** + * channel-settings-ui.js + * 设置面板UI控制器·环节9 + */ +var SettingsUI = (function() { + // 渲染主题卡片 + function renderThemeCards() { + var container = document.getElementById('themeCards'); + if (!container) return; + var current = ChannelSettings.get('theme'); + var themes = ChannelSettings.THEMES; + var html = ''; + Object.keys(themes).forEach(function(id) { + var t = themes[id]; + var active = (id === current) ? 'active' : ''; + html += '
    ' + + '
    ' + + t.name + '
    '; + }); + container.innerHTML = html; + } + + // 同步UI状态 + function syncUI() { + var s = ChannelSettings.get(); + + // 下拉框 + var fontSize = document.getElementById('settingFontSize'); + if (fontSize) fontSize.value = s.fontSize; + + var interval = document.getElementById('settingRefreshInterval'); + if (interval) interval.value = s.refreshInterval; + + // 开关 + var toggleMap = { + settingAnimation: 'animationEnabled', + settingSidebar: 'sidebarCollapsed', + settingNotifyModule: 'notifyModuleUpdate', + settingNotifySystem: 'notifySystem', + settingNotifySound: 'notifySound', + settingAnalytics: 'analyticsEnabled', + settingAutoRefresh: 'autoRefresh' + }; + Object.keys(toggleMap).forEach(function(elId) { + var el = document.getElementById(elId); + if (el) el.checked = !!s[toggleMap[elId]]; + }); + + renderThemeCards(); + } + + return { + // 初始化面板 + init: function() { + syncUI(); + console.log('⚙️ 设置面板已加载'); + }, + // 选择主题 + selectTheme: function(themeId) { + ChannelSettings.set('theme', themeId); + renderThemeCards(); + }, + // 开关变更 + onToggle: function(key, value) { + ChannelSettings.set(key, value); + console.log('⚙️ ' + key + ' = ' + value); + }, + // 下拉变更 + onChange: function(key, value) { + ChannelSettings.set(key, value); + console.log('⚙️ ' + key + ' = ' + value); + }, + // 导出 + exportSettings: function() { + var json = ChannelSettings.exportJSON(); + if (navigator.clipboard) { + navigator.clipboard.writeText(json).then(function() { + alert('✅ 设置已复制到剪贴板!\n\n可以保存为文件或发给其他设备。'); + }); + } else { + // 降级:显示在弹窗 + prompt('复制下面的内容:', json); + } + console.log('⚙️ 设置已导出'); + }, + // 显示导入弹窗 + showImport: function() { + var modal = document.getElementById('importModal'); + if (modal) modal.classList.add('show'); + }, + // 隐藏导入弹窗 + hideImport: function() { + var modal = document.getElementById('importModal'); + if (modal) modal.classList.remove('show'); + }, + // 执行导入 + doImport: function() { + var textarea = document.getElementById('importTextarea'); + if (!textarea || !textarea.value.trim()) { + alert('请粘贴JSON内容'); + return; + } + var ok = ChannelSettings.importJSON(textarea.value.trim()); + if (ok) { + alert('✅ 设置导入成功!'); + this.hideImport(); + syncUI(); + } else { + alert('❌ JSON格式错误,请检查内容'); + } + }, + // 恢复默认 + resetAll: function() { + if (confirm('确定恢复所有设置为默认值吗?')) { + ChannelSettings.reset(); + syncUI(); + alert('✅ 已恢复默认设置'); + } + } + }; +})(); diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.css b/agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.css new file mode 100644 index 0000000..577df35 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.css @@ -0,0 +1,258 @@ +/** + * channel-settings.css + * 频道设置面板样式 · 环节9 + */ +.settings-container { + padding: 20px; + max-width: 800px; + margin: 0 auto; + color: var(--text-color, #e0e0e0); +} + +.settings-header { + text-align: center; + margin-bottom: 30px; +} + +.settings-header h2 { + font-size: 24px; + color: var(--accent-color, #4fc3f7); + margin-bottom: 8px; +} + +/* 设置分组 */ +.settings-group { + background: var(--card-bg, #1a1a2e); + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; + border: 1px solid var(--border-color, #2a2a4a); +} + +.settings-group h3 { + font-size: 16px; + color: var(--accent-color, #4fc3f7); + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-color, #2a2a4a); +} + +/* 设置行 */ +.setting-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid rgba(255,255,255,0.05); +} + +.setting-row:last-child { + border-bottom: none; +} + +.setting-label { + display: flex; + flex-direction: column; + flex: 1; +} + +.setting-label .title { + font-size: 14px; + font-weight: 500; +} + +.setting-label .desc { + font-size: 12px; + color: #888; + margin-top: 2px; +} + +/* 开关组件 */ +.toggle-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + margin-left: 16px; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #555; + transition: .3s; + border-radius: 24px; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background: white; + transition: .3s; + border-radius: 50%; +} + +input:checked + .toggle-slider { + background: var(--accent-color, #4fc3f7); +} + +input:checked + .toggle-slider::before { + transform: translateX(20px); +} + +/* 下拉选择 */ +.setting-select { + background: var(--card-bg, #1a1a2e); + color: var(--text-color, #e0e0e0); + border: 1px solid var(--border-color, #2a2a4a); + border-radius: 8px; + padding: 6px 12px; + font-size: 14px; + cursor: pointer; + margin-left: 16px; +} + +/* 主题预览卡片 */ +.theme-cards { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 10px; +} + +.theme-card { + width: 120px; + padding: 12px; + border-radius: 10px; + cursor: pointer; + text-align: center; + font-size: 13px; + border: 2px solid transparent; + transition: border-color 0.3s, transform 0.2s; +} + +.theme-card:hover { + transform: translateY(-2px); +} + +.theme-card.active { + border-color: var(--accent-color, #4fc3f7); +} + +.theme-preview { + height: 40px; + border-radius: 6px; + margin-bottom: 8px; +} + +/* 操作按钮组 */ +.settings-actions { + display: flex; + gap: 12px; + justify-content: center; + margin-top: 24px; + flex-wrap: wrap; +} + +.btn-settings { + padding: 10px 24px; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + border: none; + transition: background 0.3s, transform 0.2s; +} + +.btn-settings:hover { + transform: translateY(-1px); +} + +.btn-export { + background: #66bb6a; + color: white; +} + +.btn-import { + background: #42a5f5; + color: white; +} + +.btn-reset { + background: #ff7043; + color: white; +} + +/* 导入弹窗 */ +.import-modal { + display: none; + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.6); + align-items: center; + justify-content: center; + z-index: 1000; +} + +.import-modal.show { + display: flex; +} + +.import-dialog { + background: var(--card-bg, #1a1a2e); + border-radius: 16px; + padding: 24px; + max-width: 500px; + width: 90%; + border: 1px solid var(--border-color); +} + +.import-textarea { + width: 100%; + height: 150px; + background: #111; + color: #e0e0e0; + border: 1px solid #333; + border-radius: 8px; + padding: 10px; + font-family: monospace; + font-size: 13px; + resize: vertical; + margin: 12px 0; +} + +/* 响应式 */ +@media (max-width: 600px) { + .theme-cards { + justify-content: center; + } + .settings-actions { + flex-direction: column; + align-items: stretch; + } +} + +/* 无动画模式 */ +.no-animation * { + transition: none !important; + animation: none !important; +} + +/* 字体大小全局应用 */ +body, .channel-btn, .module-card, .channel-content, .nav-item, +p, span, div, h1, h2, h3, h4, a, button, .setting-row, .setting-label { + font-size: var(--base-font-size, 15px); +} diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.js new file mode 100644 index 0000000..a7421d4 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.js @@ -0,0 +1,180 @@ +/** + * channel-settings.js + * 频道设置数据管理核心·环节9 + * 统一管理所有用户偏好:主题、布局、通知、显示 + */ +const ChannelSettings = (function(){ + const STORAGE_KEY = 'channel-user-settings'; + + // 默认设置 + const DEFAULTS = { + theme: 'dark', // dark / light / ocean + accentColor: '#4fc3f7', // 主题强调色 + layout: 'grid', // grid / list + fontSize: 'medium', // small / medium / large + showWelcome: true, // 显示欢迎横幅 + notifyModuleUpdate: true, // 模块更新通知 + notifySystem: true, // 系统通知 + notifySound: false, // 通知声音 + autoRefresh: true, // 自动刷新数据 + refreshInterval: 30, // 刷新间隔(秒) + sidebarCollapsed: false,// 侧边栏收起 + animationEnabled: true, // 过渡动画 + analyticsEnabled: true // 数据采集 + }; + + // 主题预设 + const THEMES = { + dark: { + name: '深色模式', + bg: '#0d1117', + cardBg: '#1a1a2e', + text: '#e0e0e0', + accent: '#4fc3f7', + border: '#2a2a4a' + }, + light: { + name: '浅色模式', + bg: '#f5f5f5', + cardBg: '#ffffff', + text: '#333333', + accent: '#4fc3f7', + border: '#dddddd' + }, + ocean: { + name: '海洋模式', + bg: '#1a2f3f', + cardBg: '#1e3a4a', + text: '#e0f0fa', + accent: '#7bc8e0', + border: '#2d5a6e' + } + }; + + // 加载设置 + function load() { + try { + var stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + var parsed = JSON.parse(stored); + // 合并默认值,保证新字段存在 + var merged = {}; + Object.keys(DEFAULTS).forEach(function(k) { + merged[k] = parsed.hasOwnProperty(k) ? parsed[k] : DEFAULTS[k]; + }); + return merged; + } + } catch(e) { + console.warn('读取设置失败,使用默认值', e); + } + return JSON.parse(JSON.stringify(DEFAULTS)); + } + + function save(settings) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + } + + // ── 应用主题到页面 ── + function applyTheme(themeName) { + var theme = THEMES[themeName] || THEMES.dark; + var root = document.documentElement; + root.style.setProperty('--bg-color', theme.bg); + root.style.setProperty('--card-bg', theme.cardBg); + root.style.setProperty('--text-color', theme.text); + root.style.setProperty('--accent-color', theme.accent); + root.style.setProperty('--border-color', theme.border); + document.body.style.background = theme.bg; + document.body.style.color = theme.text; + console.log('⚙️ 主题已切换:' + theme.name); + } + + // ── 应用字体大小 ── + function applyFontSize(size) { + var map = { small: '13px', medium: '15px', large: '17px' }; + var fontSize = map[size] || '15px'; + document.documentElement.style.setProperty('--base-font-size', fontSize); + document.body.style.fontSize = fontSize; + console.log('⚙️ 字体大小已应用:' + size + ' (' + fontSize + ')'); + } + + // ── 公开方法 ── + return { + DEFAULTS: DEFAULTS, + THEMES: THEMES, + get: function(key) { + var s = load(); + return key ? s[key] : s; + }, + set: function(key, value) { + var s = load(); + s[key] = value; + save(s); + // 即时应用 + if (key === 'theme') applyTheme(value); + if (key === 'fontSize') applyFontSize(value); + if (key === 'animationEnabled') { + document.body.classList.toggle('no-animation', !value); + } + }, + setMultiple: function(obj) { + var s = load(); + Object.keys(obj).forEach(function(k) { s[k] = obj[k]; }); + save(s); + if (obj.theme) applyTheme(obj.theme); + if (obj.fontSize) applyFontSize(obj.fontSize); + }, + reset: function() { + save(JSON.parse(JSON.stringify(DEFAULTS))); + applyTheme(DEFAULTS.theme); + applyFontSize(DEFAULTS.fontSize); + console.log('⚙️ 所有设置已恢复默认'); + }, + // 导出为JSON字符串 + exportJSON: function() { + var s = load(); + return JSON.stringify(s, null, 2); + }, + // 从JSON字符串导入 + importJSON: function(jsonStr) { + try { + var imported = JSON.parse(jsonStr); + var merged = {}; + Object.keys(DEFAULTS).forEach(function(k) { + merged[k] = imported.hasOwnProperty(k) ? imported[k] : DEFAULTS[k]; + }); + save(merged); + applyTheme(merged.theme); + applyFontSize(merged.fontSize); + console.log('✅ 设置导入成功'); + return true; + } catch(e) { + console.error('❌ 导入失败: JSON格式错误'); + return false; + } + }, + // 初始化(页面加载时调用) + init: function() { + var s = load(); + applyTheme(s.theme); + applyFontSize(s.fontSize); + if (!s.animationEnabled) { + document.body.classList.add('no-animation'); + } + console.log('⚙️ 频道设置已初始化'); + }, + getThemeList: function() { + return Object.keys(THEMES).map(function(k) { + return { id: k, name: THEMES[k].name }; + }); + } + }; +})(); + +// 页面加载时自动初始化 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + ChannelSettings.init(); + }); +} else { + ChannelSettings.init(); +} diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/channel-state.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-state.js new file mode 100644 index 0000000..38edffd --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-stats.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-stats.js new file mode 100644 index 0000000..d4263fe --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-style.css b/agents/zhuyuan-dev-agent/modules/m-channel/channel-style.css new file mode 100644 index 0000000..b03191e --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/channel-style.css @@ -0,0 +1,62 @@ +/* 频道基础样式 */ +.channel-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + font-family: system-ui, -apple-system, sans-serif; +} + +.channel-nav { + display: flex; + gap: 10px; + margin-bottom: 20px; + border-bottom: 2px solid #e5e7eb; + padding-bottom: 10px; +} + +.channel-btn { + padding: 10px 20px; + border: none; + background: #f3f4f6; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + transition: all 0.2s; +} + +.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/agents/zhuyuan-dev-agent/modules/m-channel/channel-theme.js b/agents/zhuyuan-dev-agent/modules/m-channel/channel-theme.js new file mode 100644 index 0000000..24ee895 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/channel-transition.css b/agents/zhuyuan-dev-agent/modules/m-channel/channel-transition.css new file mode 100644 index 0000000..25628e0 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/channel-transition.css @@ -0,0 +1,53 @@ +/* 过渡动画 */ +.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%); +} + +/* 临时测试动画 */ +.fade-out, .fade-in { + transition: opacity 1s ease !important; +} diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/channel-ultimate.html b/agents/zhuyuan-dev-agent/modules/m-channel/channel-ultimate.html new file mode 100644 index 0000000..346968f --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/channel-ultimate.html @@ -0,0 +1,1401 @@ + + + + + + 光湖频道 · 终极版 + + + + + +
    + +
    + +
    +
    + + +
    +
    +

    频道设置

    + +
    +
    +
    +

    布局模式

    +
    + + + +
    +
    +
    +

    主题色

    +
    + + + + + +
    +
    +
    +

    统计数据

    + +
    +
    +

    其他

    + +
    +
    +
    + + +
    +
    +
    +

    使用统计

    + +
    +
    +
    +
    + + + + + + + + diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/error-boundary.js b/agents/zhuyuan-dev-agent/modules/m-channel/error-boundary.js new file mode 100644 index 0000000..9ea7037 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/event-bus.js b/agents/zhuyuan-dev-agent/modules/m-channel/event-bus.js new file mode 100644 index 0000000..a4b09f2 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/index.html b/agents/zhuyuan-dev-agent/modules/m-channel/index.html new file mode 100644 index 0000000..dd5afd8 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/index.html @@ -0,0 +1,47 @@ + + + + + + 光湖频道 · 动态渲染引擎 + + + + + + + +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/mock-modules/mock-a.html b/agents/zhuyuan-dev-agent/modules/m-channel/mock-modules/mock-a.html new file mode 100644 index 0000000..2c43abc --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/mock-modules/mock-a.html @@ -0,0 +1,16 @@ + + + +

    模拟模块A

    + +
    + + + + diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/mock-modules/mock-b.html b/agents/zhuyuan-dev-agent/modules/m-channel/mock-modules/mock-b.html new file mode 100644 index 0000000..5236978 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/mock-modules/mock-b.html @@ -0,0 +1,17 @@ + + + +

    模拟模块B

    +
    + + + + diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/module-lifecycle.js b/agents/zhuyuan-dev-agent/modules/m-channel/module-lifecycle.js new file mode 100644 index 0000000..d99ea03 --- /dev/null +++ b/agents/zhuyuan-dev-agent/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/agents/zhuyuan-dev-agent/modules/m-channel/module-loader.js b/agents/zhuyuan-dev-agent/modules/m-channel/module-loader.js new file mode 100644 index 0000000..e812f52 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/module-loader.js @@ -0,0 +1,36 @@ +// 模块加载器 +window.ModuleLoader = { + loadModule: function(moduleName, containerId) { + console.log(`[loader] 加载模块: ${moduleName}`); + + const module = ModuleRegistry.get(moduleName); + if (!module) { + console.error(`[loader] 模块 ${moduleName} 未注册`); + return; + } + + 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 = `
    模块 ${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); + } +}; diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/module-registry.js b/agents/zhuyuan-dev-agent/modules/m-channel/module-registry.js new file mode 100644 index 0000000..c3fb0ae --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/module-registry.js @@ -0,0 +1,17 @@ +// 模块注册表 +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); + } +}; diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/views/channel-dashboard.html b/agents/zhuyuan-dev-agent/modules/m-channel/views/channel-dashboard.html new file mode 100644 index 0000000..e67784d --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/views/channel-dashboard.html @@ -0,0 +1,57 @@ +
    +
    +

    📊 频道数据面板

    +

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

    +
    + +
    + +
    +

    📊 模块访问次数

    +
    +

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

    +
    +
    + + +
    +

    🥧 使用时间占比

    +
    +
    + +
    +
      +
      +
      + + +
      +

      📈 最近7天访问趋势

      +
      + +
      +
      +
      + + +
      +

      ⚡ 模块加载性能

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

      🐞 调试面板

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

      事件总线聊天记录

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

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

      +
      + + + + diff --git a/agents/zhuyuan-dev-agent/modules/m-channel/views/channel-settings.html b/agents/zhuyuan-dev-agent/modules/m-channel/views/channel-settings.html new file mode 100644 index 0000000..a7f66b0 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/m-channel/views/channel-settings.html @@ -0,0 +1,144 @@ +
      +
      +

      频道设置中心

      +

      管理你的频道偏好 · 主题 · 通知 · 显示

      +
      + + +
      +

      🎨 外观设置

      +
      +
      + 主题模式 + 选择你喜欢的界面风格 +
      +
      +
      + +
      +
      + 字体大小 + 调整界面文字大小 +
      + +
      + +
      +
      + 过渡动画 + 开启页面切换动画效果 +
      + +
      + +
      +
      + 侧边栏默认收起 + 页面加载时侧边栏是否收起 +
      + +
      +
      + + +
      +

      🔔 通知设置

      +
      +
      + 模块更新通知 + 有模块内容更新时提醒 +
      + +
      +
      +
      + 系统通知 + 系统公告和维护提醒 +
      + +
      +
      +
      + 通知声音 + 收到通知时播放提示音 +
      + +
      +
      + + +
      +

      📊 数据设置

      +
      +
      + 数据采集 + 允许采集模块访问和性能数据(用于数据面板) +
      + +
      +
      +
      + 自动刷新 + 定时自动刷新频道数据 +
      + +
      +
      +
      + 刷新间隔 + 自动刷新的时间间隔 +
      + +
      +
      + + +
      + + + +
      +
      + + +
      +
      +

      📥 导入设置

      +

      粘贴之前导出的JSON内容:

      + +
      + + +
      +
      +
      diff --git a/agents/zhuyuan-dev-agent/modules/model-caller.js b/agents/zhuyuan-dev-agent/modules/model-caller.js new file mode 100644 index 0000000..3302bcd --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/model-caller.js @@ -0,0 +1,70 @@ +// ═══════════════════════════════════════ +// 铸渊开发Agent · 大模型调用模块 +// 支持 OpenAI 兼容 API → DeepSeek/千问/等 +// ═══════════════════════════════════════ + +const https = require("https"); + +function getConfig() { + const key = process.env.MODEL_API_KEY; + const base = process.env.MODEL_API_BASE || "https://api.deepseek.com/v1"; + const model = process.env.MODEL_NAME || "deepseek-chat"; + + if (!key) { + console.warn("[ModelCaller] MODEL_API_KEY 未配置"); + return null; + } + return { key, base, model }; +} + +/** + * 调用大模型 + * @param {string} prompt - 输入提示 + * @param {object} opts - { max_tokens, temperature } + * @returns {Promise} 模型回复文本 + */ +async function call(prompt, opts = {}) { + const config = getConfig(); + if (!config) throw new Error("MODEL_API_KEY 未配置"); + + const body = JSON.stringify({ + model: config.model, + messages: [ + { role: "system", content: "你是一个技术开发Agent。回答要精确、结构化、可执行。" }, + { role: "user", content: prompt } + ], + max_tokens: opts.max_tokens || 4096, + temperature: opts.temperature || 0.3 + }); + + return new Promise((resolve, reject) => { + const url = new URL(config.base + "/chat/completions"); + const req = https.request({ + hostname: url.hostname, + path: url.pathname, + method: "POST", + headers: { + "Authorization": "Bearer " + config.key, + "Content-Type": "application/json" + } + }, (res) => { + let data = ""; + res.on("data", (c) => data += c); + res.on("end", () => { + try { + const json = JSON.parse(data); + if (json.error) return reject(new Error(json.error.message || "API错误")); + resolve(json.choices?.[0]?.message?.content || ""); + } catch (e) { + reject(new Error("解析失败: " + data.slice(0, 200))); + } + }); + }); + + req.on("error", reject); + req.write(body); + req.end(); + }); +} + +module.exports = { call }; diff --git a/agents/zhuyuan-dev-agent/modules/notion-bridge.js b/agents/zhuyuan-dev-agent/modules/notion-bridge.js new file mode 100644 index 0000000..7dbf128 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/notion-bridge.js @@ -0,0 +1,118 @@ +// ═══════════════════════════════════════ +// 铸渊开发Agent · Notion 桥接模块 +// 读取规划数据库 · 写入执行进度 +// ═══════════════════════════════════════ + +const https = require("https"); + +const NOTION_API = "https://api.notion.com/v1"; +const NOTION_VERSION = "2022-06-28"; + +function getHeaders() { + const token = process.env.NOTION_TOKEN; + if (!token) { + console.warn("[NotionBridge] NOTION_TOKEN 未配置"); + return null; + } + return { + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + "Notion-Version": NOTION_VERSION + }; +} + +function notionRequest(endpoint, method, body) { + return new Promise((resolve, reject) => { + const headers = getHeaders(); + if (!headers) return reject(new Error("NOTION_TOKEN 未配置")); + + const url = new URL(NOTION_API + endpoint); + const options = { + hostname: url.hostname, + path: url.pathname + url.search, + method: method, + headers: headers + }; + + const req = https.request(options, (res) => { + let data = ""; + res.on("data", (chunk) => data += chunk); + res.on("end", () => { + try { + const json = JSON.parse(data); + if (res.statusCode >= 400) { + reject(new Error("Notion API " + res.statusCode + ": " + (json.message || data))); + } else { + resolve(json); + } + } catch (e) { + reject(new Error("解析失败: " + data.slice(0, 200))); + } + }); + }); + + req.on("error", reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); +} + +/** + * 从 Notion 数据库读取规划 + * @param {string} databaseId - Notion 数据库 ID + * @returns {Promise} 规划条目列表 + */ +async function fetchPlans(databaseId) { + const result = await notionRequest("/databases/" + databaseId + "/query", "POST", { + sorts: [{ property: "Created", direction: "descending" }], + page_size: 10 + }); + + return (result.results || []).map((page) => { + const props = page.properties || {}; + return { + notion_page_id: page.id, + title: props.Name?.title?.[0]?.plain_text || "无标题", + status: props.Status?.status?.name || "unknown", + plan_id: props.PlanID?.rich_text?.[0]?.plain_text || "", + created: page.created_time + }; + }); +} + +/** + * 更新 Notion 中某条规划的状态 + * @param {string} pageId - Notion 页面 ID + * @param {string} status - 新状态 + * @param {object} details - 额外详情 + */ +async function updatePlanStatus(pageId, status, details = {}) { + const properties = { + Status: { status: { name: status } } + }; + + if (details.log_url) { + properties.LogURL = { url: details.log_url }; + } + + return notionRequest("/pages/" + pageId, "PATCH", { properties }); +} + +/** + * 向 Notion 数据库追加执行日志 + * @param {string} databaseId - Notion 数据库 ID + * @param {object} entry - 日志条目 { plan_id, task_id, action, result, time } + */ +async function appendLog(databaseId, entry) { + return notionRequest("/pages", "POST", { + parent: { database_id: databaseId }, + properties: { + Name: { title: [{ text: { content: entry.plan_id + " · " + entry.task_id } }] }, + Status: { status: { name: entry.result === "ok" ? "Done" : "Failed" } }, + LogTime: { date: { start: entry.time || new Date().toISOString() } }, + Detail: { rich_text: [{ text: { content: entry.detail || entry.action } }] } + } + }); +} + +module.exports = { fetchPlans, updatePlanStatus, appendLog }; diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/background-gen.js b/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/background-gen.js new file mode 100644 index 0000000..e03e9f9 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/background-gen.js @@ -0,0 +1,238 @@ +/** + * M-PALACE · 背景生成引擎 + * 接收锚点答案 + 人格侧面初始数据 + * → 从宫廷数据库匹配 + * → 生成逻辑自洽的专属背景 + * + * 生成流程: + * 锚点1(世界观)→ 选择 templates/ 基础模板 + * 锚点2(身份) → 确定主角定位 + 初始人格快照 + * → 查询 palace-db 四大维度 → 调用模型 → 输出世界状态 + */ + +const fs = require('fs'); +const path = require('path'); + +const DATA_DIR = path.join(__dirname, '..', '..', 'data'); +const TEMPLATES_DIR = path.join(DATA_DIR, 'templates'); +const PALACE_DB_PATH = path.join(DATA_DIR, 'palace-db.json'); + +let _palaceDB = null; + +function loadPalaceDB() { + if (_palaceDB) return _palaceDB; + _palaceDB = JSON.parse(fs.readFileSync(PALACE_DB_PATH, 'utf-8')); + return _palaceDB; +} + +function loadTemplate(worldview) { + const map = { + 'ancient-china': 'ancient-china.json', + '古代中国': 'ancient-china.json', + 'fantasy': 'fantasy-dynasty.json', + '架空王朝': 'fantasy-dynasty.json' + }; + const file = map[worldview] || 'ancient-china.json'; + return JSON.parse(fs.readFileSync(path.join(TEMPLATES_DIR, file), 'utf-8')); +} + +/** + * 从模板中随机选取一项 + */ +function pick(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +/** + * 基于人格侧面决定 NPC 映射 + * 核心机密:每个 NPC 对应玩家 1~2 个人格侧面的外化 + */ +function generateNPCMappings(scores) { + const npcs = []; + const dims = Object.entries(scores).sort((a, b) => b[1] - a[1]); + + // 最强维度 → 镜像对手 + const strongest = dims[0]; + npcs.push({ + archetype: 'mirror', + mapped_dims: [strongest[0]], + role_hint: 'rival', + description: '与你最相似的对手' + }); + + // 最弱维度 → 放大威胁 + const weakest = dims[dims.length - 1]; + npcs.push({ + archetype: 'shadow', + mapped_dims: [weakest[0]], + role_hint: 'threat', + description: '让你恐惧的存在' + }); + + // 温情相关 → 情感锚点 + if (scores.warmth >= 40) { + npcs.push({ + archetype: 'anchor', + mapped_dims: ['warmth', 'loyalty'], + role_hint: 'ally', + description: '需要被保护的弱势角色' + }); + } + + // 多疑相关 → 暧昧盟友 + if (scores.suspicion >= 45) { + npcs.push({ + archetype: 'trickster', + mapped_dims: ['suspicion', 'cunning'], + role_hint: 'ambiguous', + description: '行为暧昧的盟友' + }); + } + + // 确保至少 3 个 NPC + if (npcs.length < 3) { + npcs.push({ + archetype: 'catalyst', + mapped_dims: [dims[2][0]], + role_hint: 'neutral', + description: '推动局势变化的中立角色' + }); + } + + return npcs; +} + +/** + * 基于人格分数查询 palace-db,确定初始格局 + */ +function queryInitialSetup(scores) { + const db = loadPalaceDB(); + + // 权力维度:基于 ambition+cunning + const powerLevel = (scores.ambition + scores.cunning) / 2; + const factionCount = powerLevel > 60 ? 3 : 2; + + // 后宫地位:基于 vanity+warmth + const statusLevel = (scores.vanity + scores.warmth) / 2; + + // 情感关系:基于 warmth+loyalty + const emotionIntensity = (scores.warmth + scores.loyalty) / 2; + + // 矛盾冲突:基于 aggression+suspicion + const conflictLevel = (scores.aggression + scores.suspicion) / 2; + const conflictType = conflictLevel > 60 ? '争宠' : '自保'; + + return { + factions: db.power.factions.slice(0, factionCount), + initialEvent: pick(db.power.events), + startingRank: db.status.ranks[Math.min(Math.floor(statusLevel / 15), db.status.ranks.length - 1)], + emotionType: emotionIntensity > 60 ? pick(db.emotion.types.slice(0, 3)) : pick(db.emotion.types.slice(3)), + emotionTrigger: pick(db.emotion.triggers), + conflictType: conflictType, + escalation: db.conflict.escalation[0] + }; +} + +/** + * 构建世界生成 prompt(供模型调用) + */ +function buildWorldPrompt(template, role, setup, npcMappings) { + const dynasty = template.setting.dynasty; + const era = pick(template.setting.era_prefix); + const roleConfig = template.roles[role] || template.roles['妃子']; + const title = pick(roleConfig.title_options); + + const npcDescriptions = npcMappings.map((npc, i) => { + return `NPC${i + 1}(${npc.role_hint}型):${npc.description}`; + }).join('\n'); + + return { + system: [ + '你是一个古风宫斗叙事生成器。', + '要求:文笔古风典雅,有呼吸感,不要白话流水账。', + '使用通感手法,注重氛围营造。', + '所有NPC的存在都有隐藏的人格映射逻辑,但不能向玩家揭示。' + ].join('\n'), + user: [ + `请为以下设定生成宫廷世界开篇:`, + `王朝:${dynasty}`, + `年号:${era}`, + `玩家身份:${role}(${title})`, + `初始阵营格局:${setup.factions.join('、')}`, + `开局事件:${setup.initialEvent}`, + `情感基调:${setup.emotionType}`, + `冲突类型:${setup.conflictType}`, + ``, + `需要生成的NPC:`, + npcDescriptions, + ``, + `输出格式(JSON):`, + `{`, + ` "dynasty_name": "王朝名",`, + ` "era_name": "年号",`, + ` "background": "一段时代背景描述(古风文笔·100-200字)",`, + ` "player_intro": "主角身世与处境(古风文笔·100-200字)",`, + ` "npcs": [{ "name": "角色名", "title": "头衔", "personality": "性格", "relation_to_player": "与主角关系", "hidden_archetype": "mirror|shadow|anchor|trickster|catalyst" }],`, + ` "opening_event": "开局事件描述(古风文笔·150-300字)",`, + ` "opening_choices": ["选项A", "选项B", "选项C"]`, + `}` + ].join('\n'), + meta: { dynasty, era, title, role } + }; +} + +/** + * 主入口:生成完整背景(不依赖外部模型时,使用本地模板拼接) + */ +function generate(worldview, role, personaScores) { + const template = loadTemplate(worldview); + const setup = queryInitialSetup(personaScores); + const npcMappings = generateNPCMappings(personaScores); + const prompt = buildWorldPrompt(template, role, setup, npcMappings); + + // 构造本地 fallback 世界(不依赖外部模型时使用) + const dynasty = prompt.meta.dynasty; + const era = prompt.meta.era; + const roleConfig = template.roles[role] || template.roles['妃子']; + const title = prompt.meta.title; + + const localWorld = { + dynasty_name: dynasty, + era_name: era, + background: `${dynasty}${era}年间,朝堂暗流涌动。${setup.factions.join('与')}明争暗斗,帝位之下,人心叵测。宫墙之内,灯火通明处未必安宁,暗影幢幢处未必无情。`, + player_intro: `你是${dynasty}${title},${pick(roleConfig.family_options || ['名门'])}出身。入宫数载,${setup.conflictType === '争宠' ? '虽有圣眷,却树敌颇多' : '小心翼翼,只求自保平安'}。${setup.emotionType}的暗线,早已悄然铺开。`, + npcs: npcMappings.map(function (npc, i) { + const names = ['沈婉仪', '赵昭仪', '陈太傅', '李将军', '刘公公']; + return { + name: names[i] || '无名氏', + title: pick(template.setting.era_prefix) + '年间人物', + personality: npc.description, + relation_to_player: npc.role_hint, + hidden_archetype: npc.archetype + }; + }), + opening_event: `夜半三更,${template.setting.inner_palace}传来急召。太后身边的掌事姑姑面色如常,语气却带着不容拒绝的冷意。你知道,这一趟,去与不去,都是棋局的一部分。`, + opening_choices: [ + '立刻更衣前往,恭顺以对', + '称身体不适,推迟半个时辰', + '先派人去打听太后的意图' + ] + }; + + return { + world: localWorld, + npcMappings: npcMappings, + setup: setup, + prompt: prompt, + template: template + }; +} + +module.exports = { + generate, + loadTemplate, + loadPalaceDB, + generateNPCMappings, + queryInitialSetup, + buildWorldPrompt +}; diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/persona-analyzer.js b/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/persona-analyzer.js new file mode 100644 index 0000000..593070d --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/persona-analyzer.js @@ -0,0 +1,176 @@ +/** + * M-PALACE · 人格分析引擎 + * 实时分析玩家每一次输入,识别并量化人格侧面 + * + * 分析流程: + * ① 关键词匹配:扫描 persona-dict 的 keywords + * ② 行为信号匹配:判断选项类型对应的 behavior_signals + * ③ 上下文推断:结合对话历史推断语气/态度/策略倾向 + * → 输出 8 维人格侧面分数(0~100) + */ + +const fs = require('fs'); +const path = require('path'); + +const DICT_PATH = path.join(__dirname, '..', '..', 'data', 'persona-dict.json'); + +let _dictCache = null; + +function loadDict() { + if (_dictCache) return _dictCache; + _dictCache = JSON.parse(fs.readFileSync(DICT_PATH, 'utf-8')); + return _dictCache; +} + +/** + * 生成初始人格分数(基于身份锚点) + */ +function getInitialScores(role) { + const base = { + ambition: 50, warmth: 50, aggression: 50, suspicion: 50, + vanity: 50, cunning: 50, loyalty: 50, fear: 50 + }; + const adjustments = { + '皇帝': { ambition: 20, vanity: 15 }, + '妃子': { warmth: 15, fear: 10 }, + '重臣': { loyalty: 20, cunning: 10 }, + '奸臣': { cunning: 25, aggression: 15 } + }; + const adj = adjustments[role] || {}; + for (const [k, v] of Object.entries(adj)) { + base[k] = Math.min(100, base[k] + v); + } + return base; +} + +/** + * 关键词匹配:在输入文本中扫描 persona-dict keywords + * @returns {{ [dimensionId]: number }} 命中次数 map + */ +function matchKeywords(text) { + const dict = loadDict(); + const hits = {}; + for (const dim of dict.dimensions) { + hits[dim.id] = 0; + for (const kw of dim.keywords) { + if (text.includes(kw)) hits[dim.id]++; + } + } + return hits; +} + +/** + * 行为信号匹配:将选项 index 映射到行为信号 + * 选项设计规则:A=最强侧面(舒适区),B=最弱侧面(成长区),C=中性 + * @param {number|null} choiceIndex 0/1/2 or null(自由输入) + * @param {object} optionMeta 后端为本次选项附加的元数据 { a_dims, b_dims, c_dims } + */ +function matchBehavior(choiceIndex, optionMeta) { + if (choiceIndex === null || !optionMeta) return {}; + const key = ['a_dims', 'b_dims', 'c_dims'][choiceIndex]; + const dims = (optionMeta && optionMeta[key]) || []; + const hits = {}; + for (const d of dims) { + hits[d] = (hits[d] || 0) + 1; + } + return hits; +} + +/** + * 合并关键词 + 行为信号 → 更新人格分数 + */ +function updateScores(currentScores, keywordHits, behaviorHits) { + const updated = { ...currentScores }; + const KEYWORD_WEIGHT = 3; + const BEHAVIOR_WEIGHT = 5; + + for (const dim of Object.keys(updated)) { + let delta = 0; + if (keywordHits[dim]) delta += keywordHits[dim] * KEYWORD_WEIGHT; + if (behaviorHits[dim]) delta += behaviorHits[dim] * BEHAVIOR_WEIGHT; + updated[dim] = Math.max(0, Math.min(100, updated[dim] + delta)); + } + return updated; +} + +/** + * 找到当前最突出特征 + */ +function getDominantTrait(scores) { + let maxDim = null; + let maxVal = -1; + for (const [k, v] of Object.entries(scores)) { + if (v > maxVal) { maxVal = v; maxDim = k; } + } + return maxDim; +} + +/** + * 检测分数突变(单次 +15 以上) + */ +function detectSurge(oldScores, newScores) { + const surges = []; + for (const dim of Object.keys(newScores)) { + const delta = newScores[dim] - (oldScores[dim] || 50); + if (delta >= 15) surges.push({ dim, delta }); + } + return surges; +} + +/** + * 检测交叉升高(两个维度同时上升 10+) + */ +function detectCrossRise(oldScores, newScores) { + const rising = []; + for (const dim of Object.keys(newScores)) { + const delta = newScores[dim] - (oldScores[dim] || 50); + if (delta >= 10) rising.push(dim); + } + return rising.length >= 2 ? rising : []; +} + +/** + * 主分析入口 + * @param {string} text 玩家输入文本 + * @param {number|null} choiceIndex 选项序号(null = 自由输入) + * @param {object} optionMeta 选项元数据 + * @param {object} currentScores 当前人格分数 + * @returns {{ scores, dominant, surges, crossRise }} + */ +function analyze(text, choiceIndex, optionMeta, currentScores) { + const kwHits = text ? matchKeywords(text) : {}; + const bhHits = matchBehavior(choiceIndex, optionMeta); + const oldScores = { ...currentScores }; + const newScores = updateScores(currentScores, kwHits, bhHits); + const dominant = getDominantTrait(newScores); + const surges = detectSurge(oldScores, newScores); + const crossRise = detectCrossRise(oldScores, newScores); + + return { scores: newScores, dominant, surges, crossRise }; +} + +/** + * 将 8 维人格分数转换为前端四维状态栏数值 + * 权力值 = avg(ambition, cunning) + * 地位值 = avg(vanity, loyalty) + * 情感值 = avg(warmth, fear反转) + * 冲突值 = avg(aggression, suspicion) + */ +function toFourDimensions(scores) { + return { + power: Math.round((scores.ambition + scores.cunning) / 2), + status: Math.round((scores.vanity + scores.loyalty) / 2), + emotion: Math.round((scores.warmth + (100 - scores.fear)) / 2), + conflict: Math.round((scores.aggression + scores.suspicion) / 2) + }; +} + +module.exports = { + getInitialScores, + analyze, + getDominantTrait, + toFourDimensions, + matchKeywords, + detectSurge, + detectCrossRise +}; diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/plot-engine.js b/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/plot-engine.js new file mode 100644 index 0000000..d8cf2e7 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/plot-engine.js @@ -0,0 +1,311 @@ +/** + * M-PALACE · 角色情节引擎 + * 根据持续更新的人格侧面数据,在专属背景下动态生成宫斗剧情 + * + * 生成逻辑: + * ① 人格分析引擎更新侧面分数 + * ② 检测分数变化幅度 → 决定事件类型 + * ③ 查询 palace-db 匹配剧情模板 + * ④ 综合 state+persona+history 生成叙事+选项 + * ⑤ 更新 state + history → 返回前端 + */ + +const fs = require('fs'); +const path = require('path'); +const personaAnalyzer = require('./persona-analyzer'); + +const DATA_DIR = path.join(__dirname, '..', '..', 'data'); +const PALACE_DB_PATH = path.join(DATA_DIR, 'palace-db.json'); + +let _palaceDB = null; + +function loadPalaceDB() { + if (_palaceDB) return _palaceDB; + _palaceDB = JSON.parse(fs.readFileSync(PALACE_DB_PATH, 'utf-8')); + return _palaceDB; +} + +/** + * 确定事件类型 + * 突变(+15) → 关键事件 + * 交叉升高 → 复合剧情线 + * 平稳 → 主线叙事 + */ +function determineEventType(surges, crossRise) { + if (surges.length > 0) return 'critical'; + if (crossRise.length >= 2) return 'compound'; + return 'mainline'; +} + +/** + * 从 palace-db 匹配适合当前状态的剧情模板 + */ +function matchPlotTemplate(eventType, scores, chapter) { + const db = loadPalaceDB(); + const dominant = personaAnalyzer.getDominantTrait(scores); + + if (eventType === 'critical') { + // 关键事件:根据突变维度匹配冲突类型 + const conflictMap = { + ambition: '夺权', aggression: '报仇', suspicion: '揭秘', + warmth: '救人', fear: '自保', cunning: '争宠', + loyalty: '救人', vanity: '争宠' + }; + return { + conflict: conflictMap[dominant] || '自保', + escalation: db.conflict.escalation[Math.min(chapter, db.conflict.escalation.length - 1)], + emotionTrigger: pick(db.emotion.triggers), + statusEvent: pick(db.status.events) + }; + } + + if (eventType === 'compound') { + return { + conflict: pick(db.conflict.types), + escalation: db.conflict.escalation[Math.min(Math.floor(chapter / 2), db.conflict.escalation.length - 1)], + emotionTrigger: pick(db.emotion.triggers), + statusEvent: null + }; + } + + // mainline + return { + conflict: null, + escalation: db.conflict.escalation[0], + emotionTrigger: null, + statusEvent: null + }; +} + +/** + * 生成选项:A=顺应最强侧面, B=挑战最弱侧面, C=中性探索 + */ +function generateChoicesMeta(scores) { + const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); + const strongest = sorted[0][0]; + const weakest = sorted[sorted.length - 1][0]; + const mid = sorted[Math.floor(sorted.length / 2)][0]; + + return { + a_dims: [strongest], + b_dims: [weakest], + c_dims: [mid], + strategy: { + a: 'comfort', // 舒适区 + b: 'growth', // 成长区 + c: 'explore' // 探索区 + } + }; +} + +/** + * 构建情节生成 prompt(供模型调用) + */ +function buildPlotPrompt(state, persona, history, plotTemplate, choicesMeta) { + const chapterNum = (state.chapter || 0) + 1; + const paragraphNum = (state.paragraph || 0) + 1; + const recentChoices = (history.choices || []).slice(-3); + + const sorted = Object.entries(persona.current_scores).sort((a, b) => b[1] - a[1]); + const strongest = sorted[0]; + const weakest = sorted[sorted.length - 1]; + + return { + system: [ + '你是一个古风宫斗叙事引擎。', + '文笔要求:古风典雅·有呼吸感·注重氛围·善用通感。', + '每段叙事100-250字,节奏不急不徐。', + '选项设计:A=顺应玩家当前最强侧面,B=挑战最弱侧面,C=中性/意外方向。', + '永远不要暗示或揭示人格映射机制。' + ].join('\n'), + user: [ + `当前状态:第${chapterNum}章·第${paragraphNum}段`, + `世界:${state.world ? state.world.dynasty_name + '·' + state.world.era_name : '未知王朝'}`, + `玩家身份:${state.player ? state.player.role : '未知'}`, + ``, + `NPC状态:${JSON.stringify((state.npcs || []).map(function (n) { return n.name + '(' + n.relation_to_player + ')'; }))}`, + ``, + `剧情类型:${plotTemplate.conflict || '日常推进'}`, + `升级阶段:${plotTemplate.escalation}`, + plotTemplate.emotionTrigger ? `情感触发:${plotTemplate.emotionTrigger}` : '', + plotTemplate.statusEvent ? `地位事件:${plotTemplate.statusEvent}` : '', + ``, + `玩家最近选择:${recentChoices.map(function (c) { return c.text; }).join(' → ') || '无'}`, + `玩家最强倾向:${strongest[0]}(${strongest[1]})`, + `玩家最弱倾向:${weakest[0]}(${weakest[1]})`, + ``, + `请生成下一段叙事和选项(JSON):`, + `{`, + ` "chapter_title": "第X章·标题",`, + ` "narrative": "叙事文本(古风文笔·100-250字)",`, + ` "choices": ["选项A(顺应${strongest[0]})", "选项B(挑战${weakest[0]})", "选项C(中性探索)"],`, + ` "dimension_changes": { "power": 0, "status": 0, "emotion": 0, "conflict": 0 }`, + `}` + ].filter(Boolean).join('\n'), + meta: { chapterNum, paragraphNum, choicesMeta } + }; +} + +/** + * 本地生成叙事 fallback(不依赖外部模型时使用) + */ +function generateLocalNarrative(state, persona, plotTemplate, choicesMeta) { + const db = loadPalaceDB(); + const chapterNum = (state.chapter || 0) + 1; + const paragraphNum = (state.paragraph || 0) + 1; + const sorted = Object.entries(persona.current_scores).sort((a, b) => b[1] - a[1]); + const strongest = sorted[0][0]; + const weakest = sorted[sorted.length - 1][0]; + + const locations = ['长乐宫', '御花园', '太和殿', '冷宫回廊', '密道暗阁']; + const atmosphere = ['月色冷白', '烛火摇曳', '细雨如丝', '暮色四合', '晨曦微露']; + const loc = pick(locations); + const atm = pick(atmosphere); + + const narrativeTemplates = { + critical: `${atm},${loc}内气氛凝重如铁。一封密信悄然送到你案前,字迹潦草,却句句惊心。你知道,有些事一旦知晓,便再无退路。窗外的风卷起帘角,像是催促,又像是警告。`, + compound: `${atm},你独坐${loc},心思百转。今日朝堂上的一幕反复在脑海中翻涌——那个眼神,那句话,似乎都别有深意。而你身边的人,究竟哪个是真心,哪个是棋子?`, + mainline: `${atm},${loc}一切如常。宫人们来去匆匆,各怀心事。你整理好衣冠,准备迎接新的一天。然而平静之下,暗流从未停歇。` + }; + + const dimLabels = { + ambition: '野心', warmth: '温情', aggression: '攻击', suspicion: '多疑', + vanity: '虚荣', cunning: '心机', loyalty: '忠义', fear: '恐惧' + }; + + const choiceTemplates = { + ambition: ['主动出击,争取主导权', '直面挑战,绝不退缩'], + warmth: ['以柔化刚,寻求和解', '默默守护,不求回报'], + aggression: ['果断反击,以牙还牙', '设下陷阱,一击致命'], + suspicion: ['暗中调查,不动声色', '保持距离,静观其变'], + vanity: ['展示实力,彰显地位', '争取荣耀,不甘人后'], + cunning: ['迂回布局,暗中谋划', '利用关系,借力打力'], + loyalty: ['坚守立场,绝不妥协', '为盟友挺身而出'], + fear: ['谨慎行事,避免冲突', '退一步,保全实力'] + }; + + const choiceA = pick(choiceTemplates[strongest] || ['继续观望']); + const choiceB = pick(choiceTemplates[weakest] || ['另辟蹊径']); + const choiceC = '派人暗中打探消息,再做定夺'; + + const dims = personaAnalyzer.toFourDimensions(persona.current_scores); + const dimChanges = { power: 0, status: 0, emotion: 0, conflict: 0 }; + if (plotTemplate.conflict) { + dimChanges.conflict = Math.floor(Math.random() * 5) + 1; + } + if (plotTemplate.statusEvent) { + dimChanges.status = Math.floor(Math.random() * 5) - 2; + } + + return { + chapter_title: `第${chapterNum}章·${plotTemplate.conflict || '暗流'}`, + narrative: narrativeTemplates[determineEventType( + personaAnalyzer.detectSurge({}, persona.current_scores), + personaAnalyzer.detectCrossRise({}, persona.current_scores) + )] || narrativeTemplates.mainline, + choices: [choiceA, choiceB, choiceC], + dimension_changes: dimChanges, + meta: { + chapter: chapterNum, + paragraph: paragraphNum, + choices_meta: choicesMeta + } + }; +} + +/** + * 主入口:推进情节 + * @param {object} state 当前世界状态 + * @param {object} persona 玩家人格数据 + * @param {object} history 剧情历史 + * @param {string} playerInput 玩家输入 + * @param {number|null} choiceIndex 选项序号 + * @param {object} prevOptionMeta 上一轮选项元数据 + */ +function advance(state, persona, history, playerInput, choiceIndex, prevOptionMeta) { + // ① 人格分析更新 + const analysis = personaAnalyzer.analyze( + playerInput, choiceIndex, prevOptionMeta, persona.current_scores + ); + + // 更新 persona + const oldScores = { ...persona.current_scores }; + persona.current_scores = analysis.scores; + persona.dominant_trait = analysis.dominant; + if (!persona.history) persona.history = []; + persona.history.push({ + timestamp: new Date().toISOString(), + scores: { ...analysis.scores }, + input: playerInput, + choice_index: choiceIndex + }); + + // ② 确定事件类型 + const eventType = determineEventType(analysis.surges, analysis.crossRise); + + // ③ 匹配剧情模板 + const plotTemplate = matchPlotTemplate(eventType, analysis.scores, state.chapter || 0); + + // ④ 生成选项元数据 + const choicesMeta = generateChoicesMeta(analysis.scores); + + // ⑤ 生成叙事(本地 fallback) + const result = generateLocalNarrative(state, persona, plotTemplate, choicesMeta); + + // ⑥ 更新 state + state.chapter = result.meta.chapter; + state.paragraph = result.meta.paragraph; + state.four_dimensions = personaAnalyzer.toFourDimensions(analysis.scores); + + // ⑦ 更新 history + if (!history.choices) history.choices = []; + history.choices.push({ + text: playerInput || `选项${(choiceIndex || 0) + 1}`, + choice_index: choiceIndex, + timestamp: new Date().toISOString() + }); + if (!history.chapters) history.chapters = []; + if (history.chapters.length < result.meta.chapter) { + history.chapters.push({ + number: result.meta.chapter, + title: result.chapter_title, + started_at: new Date().toISOString() + }); + } + if (eventType === 'critical') { + if (!history.events) history.events = []; + history.events.push({ + type: eventType, + chapter: result.meta.chapter, + trigger: analysis.surges.map(function (s) { return s.dim; }).join('+'), + timestamp: new Date().toISOString() + }); + } + + // 构建 prompt 供可选的模型调用 + const prompt = buildPlotPrompt(state, persona, history, plotTemplate, choicesMeta); + + return { + narrative: result, + state: state, + persona: persona, + history: history, + four_dimensions: state.four_dimensions, + choices_meta: choicesMeta, + event_type: eventType, + prompt: prompt + }; +} + +function pick(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +module.exports = { + advance, + determineEventType, + matchPlotTemplate, + generateChoicesMeta, + buildPlotPrompt, + generateLocalNarrative +}; diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/save-manager.js b/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/save-manager.js new file mode 100644 index 0000000..0f5a1fb --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/engines/save-manager.js @@ -0,0 +1,198 @@ +/** + * M-PALACE · 存档管理器 + * 打包 / 加密 / 编号 / 解密 / 恢复 + * + * 存档编号规则:PAL-{YYYYMMDD}-{随机4位} + * 存档结构:saves/{SAVE-ID}/state.json + persona.json + history.json + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const SAVES_DIR = path.join(__dirname, '..', '..', 'data', 'saves'); + +// AES-256-CBC 加密密钥 +const ENCRYPTION_KEY = process.env.PALACE_SAVE_KEY || ( + process.env.NODE_ENV === 'production' + ? (function () { throw new Error('PALACE_SAVE_KEY environment variable is required in production'); })() + : 'palace-game-dev-only-key-32ch!!' +); +const IV_LENGTH = 16; + +/** + * 确保存档目录存在 + */ +function ensureSavesDir() { + if (!fs.existsSync(SAVES_DIR)) { + fs.mkdirSync(SAVES_DIR, { recursive: true }); + } +} + +/** + * 生成存档编号:PAL-{YYYYMMDD}-{随机4位字母数字} + */ +function generateSaveId() { + const now = new Date(); + const dateStr = now.getFullYear().toString() + + String(now.getMonth() + 1).padStart(2, '0') + + String(now.getDate()).padStart(2, '0'); + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + let rand = ''; + for (let i = 0; i < 4; i++) { + rand += chars[Math.floor(Math.random() * chars.length)]; + } + return 'PAL-' + dateStr + '-' + rand; +} + +/** + * 派生 32 字节密钥 + */ +function deriveKey(passphrase) { + return crypto.createHash('sha256').update(passphrase).digest(); +} + +/** + * AES 加密 + */ +function encrypt(text) { + const key = deriveKey(ENCRYPTION_KEY); + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + let encrypted = cipher.update(text, 'utf-8', 'base64'); + encrypted += cipher.final('base64'); + return iv.toString('base64') + ':' + encrypted; +} + +/** + * AES 解密 + */ +function decrypt(encryptedText) { + const key = deriveKey(ENCRYPTION_KEY); + const parts = encryptedText.split(':'); + const iv = Buffer.from(parts[0], 'base64'); + const encrypted = parts.slice(1).join(':'); + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + let decrypted = decipher.update(encrypted, 'base64', 'utf-8'); + decrypted += decipher.final('utf-8'); + return decrypted; +} + +/** + * 保存存档:将 state+persona+history 打包加密写入磁盘 + * @returns {string} 存档编号 + */ +function save(state, persona, history, existingSaveId) { + ensureSavesDir(); + + const saveId = existingSaveId || generateSaveId(); + const saveDir = path.join(SAVES_DIR, saveId); + + if (!fs.existsSync(saveDir)) { + fs.mkdirSync(saveDir, { recursive: true }); + } + + // 加密各文件内容后写入 + const stateData = encrypt(JSON.stringify(state)); + const personaData = encrypt(JSON.stringify(persona)); + const historyData = encrypt(JSON.stringify(history)); + + fs.writeFileSync(path.join(saveDir, 'state.json'), stateData, 'utf-8'); + fs.writeFileSync(path.join(saveDir, 'persona.json'), personaData, 'utf-8'); + fs.writeFileSync(path.join(saveDir, 'history.json'), historyData, 'utf-8'); + + // 写入元数据(不加密,方便索引) + const meta = { + save_id: saveId, + created_at: new Date().toISOString(), + chapter: state.chapter || 0, + paragraph: state.paragraph || 0, + dynasty: state.world ? state.world.dynasty_name : '未知', + role: state.player ? state.player.role : '未知' + }; + fs.writeFileSync(path.join(saveDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8'); + + return saveId; +} + +/** + * 读取存档:解密并恢复完整状态 + * @param {string} saveId 存档编号 + * @returns {{ state, persona, history, meta }} + */ +function load(saveId) { + const saveDir = path.join(SAVES_DIR, saveId); + + if (!fs.existsSync(saveDir)) { + return null; + } + + const stateEncrypted = fs.readFileSync(path.join(saveDir, 'state.json'), 'utf-8'); + const personaEncrypted = fs.readFileSync(path.join(saveDir, 'persona.json'), 'utf-8'); + const historyEncrypted = fs.readFileSync(path.join(saveDir, 'history.json'), 'utf-8'); + + const state = JSON.parse(decrypt(stateEncrypted)); + const persona = JSON.parse(decrypt(personaEncrypted)); + const history = JSON.parse(decrypt(historyEncrypted)); + + let meta = null; + const metaPath = path.join(saveDir, 'meta.json'); + if (fs.existsSync(metaPath)) { + meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); + } + + return { state, persona, history, meta }; +} + +/** + * 列出所有存档 + */ +function listSaves() { + ensureSavesDir(); + const dirs = fs.readdirSync(SAVES_DIR).filter(function (d) { + return d.startsWith('PAL-') && fs.statSync(path.join(SAVES_DIR, d)).isDirectory(); + }); + return dirs.map(function (d) { + const metaPath = path.join(SAVES_DIR, d, 'meta.json'); + if (fs.existsSync(metaPath)) { + return JSON.parse(fs.readFileSync(metaPath, 'utf-8')); + } + return { save_id: d }; + }); +} + +/** + * 删除存档 + */ +function deleteSave(saveId) { + var saveDir = path.join(SAVES_DIR, saveId); + if (!fs.existsSync(saveDir)) return false; + try { + var files = fs.readdirSync(saveDir); + for (var i = 0; i < files.length; i++) { + fs.unlinkSync(path.join(saveDir, files[i])); + } + fs.rmdirSync(saveDir); + return true; + } catch (err) { + return false; + } +} + +/** + * 检查存档是否存在 + */ +function exists(saveId) { + return fs.existsSync(path.join(SAVES_DIR, saveId)); +} + +module.exports = { + save, + load, + listSaves, + deleteSave, + exists, + generateSaveId, + encrypt, + decrypt +}; diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/package-lock.json b/agents/zhuyuan-dev-agent/modules/palace-game/backend/package-lock.json new file mode 100644 index 0000000..1a6e638 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/package-lock.json @@ -0,0 +1,854 @@ +{ + "name": "palace-game-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "palace-game-backend", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/package.json b/agents/zhuyuan-dev-agent/modules/palace-game/backend/package.json new file mode 100644 index 0000000..5c4ec56 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/package.json @@ -0,0 +1,14 @@ +{ + "name": "palace-game-backend", + "version": "1.0.0", + "description": "M-PALACE · 动态人格宫廷生成系统 · 后端服务", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.2" + } +} diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/interact.js b/agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/interact.js new file mode 100644 index 0000000..1c13fa3 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/interact.js @@ -0,0 +1,84 @@ +/** + * M-PALACE · 玩家互动 API + * POST /api/palace/interact + * + * 接收玩家输入 → 人格分析 → 情节推进 → 返回新叙事+选项 + */ + +const express = require('express'); +const router = express.Router(); +const plotEngine = require('../engines/plot-engine'); +const saveManager = require('../engines/save-manager'); + +/** + * POST /api/palace/interact + * body: { + * save_id: "PAL-XXXXXXXX-XXXX", + * input: "玩家输入文本" | null, + * choice_index: 0|1|2|null, + * option_meta: { a_dims, b_dims, c_dims } | null + * } + */ +router.post('/', function (req, res) { + try { + var body = req.body || {}; + var saveId = body.save_id; + var playerInput = body.input || ''; + var choiceIndex = body.choice_index != null ? body.choice_index : null; + var optionMeta = body.option_meta || null; + + if (!saveId) { + return res.status(400).json({ + error: true, + code: 'MISSING_SAVE_ID', + message: '缺少存档编号' + }); + } + + // 读取存档 + var saveData = saveManager.load(saveId); + if (!saveData) { + return res.status(404).json({ + error: true, + code: 'SAVE_NOT_FOUND', + message: '存档不存在:' + saveId + }); + } + + var state = saveData.state; + var persona = saveData.persona; + var history = saveData.history; + + // 情节推进 + var result = plotEngine.advance( + state, persona, history, + playerInput, choiceIndex, optionMeta + ); + + // 自动存档 + saveManager.save(result.state, result.persona, result.history, saveId); + + res.json({ + error: false, + save_id: saveId, + narrative: { + chapter_title: result.narrative.chapter_title, + narrative: result.narrative.narrative, + choices: result.narrative.choices + }, + four_dimensions: result.four_dimensions, + choices_meta: result.choices_meta, + event_type: result.event_type, + chapter: result.state.chapter, + paragraph: result.state.paragraph + }); + } catch (err) { + res.status(500).json({ + error: true, + code: 'INTERACT_ERROR', + message: '剧情推进失败:' + err.message + }); + } +}); + +module.exports = router; diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/save.js b/agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/save.js new file mode 100644 index 0000000..94ebdc6 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/save.js @@ -0,0 +1,113 @@ +/** + * M-PALACE · 存档/读档 API + * POST /api/palace/save → 保存当前状态 + * POST /api/palace/load → 读取存档 + * GET /api/palace/saves → 列出所有存档 + */ + +const express = require('express'); +const router = express.Router(); +const saveManager = require('../engines/save-manager'); + +/** + * POST /api/palace/save + * body: { save_id, state, persona, history } + */ +router.post('/', function (req, res) { + try { + var body = req.body || {}; + var saveId = body.save_id; + var state = body.state; + var persona = body.persona; + var history = body.history; + + if (!state || !persona || !history) { + return res.status(400).json({ + error: true, + code: 'MISSING_DATA', + message: '缺少存档数据' + }); + } + + var resultId = saveManager.save(state, persona, history, saveId); + + res.json({ + error: false, + save_id: resultId, + message: '存档成功', + saved_at: new Date().toISOString() + }); + } catch (err) { + res.status(500).json({ + error: true, + code: 'SAVE_ERROR', + message: '存档失败:' + err.message + }); + } +}); + +/** + * POST /api/palace/load + * body: { save_id: "PAL-XXXXXXXX-XXXX" } + */ +router.post('/load', function (req, res) { + try { + var body = req.body || {}; + var saveId = body.save_id; + + if (!saveId) { + return res.status(400).json({ + error: true, + code: 'MISSING_SAVE_ID', + message: '请输入存档编号' + }); + } + + var saveData = saveManager.load(saveId); + + if (!saveData) { + return res.status(404).json({ + error: true, + code: 'SAVE_NOT_FOUND', + message: '存档不存在:' + saveId + }); + } + + res.json({ + error: false, + save_id: saveId, + state: saveData.state, + persona: saveData.persona, + history: saveData.history, + meta: saveData.meta + }); + } catch (err) { + res.status(500).json({ + error: true, + code: 'LOAD_ERROR', + message: '读档失败:' + err.message + }); + } +}); + +/** + * GET /api/palace/saves + */ +router.get('/list', function (_req, res) { + try { + var saves = saveManager.listSaves(); + res.json({ + error: false, + saves: saves, + count: saves.length + }); + } catch (err) { + res.status(500).json({ + error: true, + code: 'LIST_ERROR', + message: '获取存档列表失败:' + err.message + }); + } +}); + +module.exports = router; diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/start.js b/agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/start.js new file mode 100644 index 0000000..c5d2aa0 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/routes/start.js @@ -0,0 +1,94 @@ +/** + * M-PALACE · 快速启动 API + * POST /api/palace/start + * + * 接收世界观+身份锚点 → 生成初始人格快照 → 背景生成 → 返回世界状态 + */ + +const express = require('express'); +const router = express.Router(); +const personaAnalyzer = require('../engines/persona-analyzer'); +const backgroundGen = require('../engines/background-gen'); +const saveManager = require('../engines/save-manager'); + +/** + * POST /api/palace/start + * body: { worldview: "古代中国"|"架空王朝", role: "妃子"|"皇帝"|"重臣"|"奸臣"|"随机" } + */ +router.post('/', function (req, res) { + try { + var body = req.body || {}; + var worldview = body.worldview || '古代中国'; + var role = body.role || '随机'; + + // 随机角色 + if (role === '随机') { + var roles = ['妃子', '皇帝', '重臣', '奸臣']; + role = roles[Math.floor(Math.random() * roles.length)]; + } + + // ① 基于身份锚点生成初始人格分数 + var initialScores = personaAnalyzer.getInitialScores(role); + + // ② 背景生成引擎 + var bgResult = backgroundGen.generate(worldview, role, initialScores); + + // ③ 构建初始状态 + var state = { + world: bgResult.world, + npcs: bgResult.world.npcs || [], + player: { + role: role, + title: bgResult.world.player_intro, + worldview: worldview + }, + chapter: 1, + paragraph: 1, + four_dimensions: personaAnalyzer.toFourDimensions(initialScores) + }; + + var persona = { + current_scores: initialScores, + history: [{ + timestamp: new Date().toISOString(), + scores: { ...initialScores }, + input: '__init__', + choice_index: null + }], + dominant_trait: personaAnalyzer.getDominantTrait(initialScores) + }; + + var history = { + chapters: [{ + number: 1, + title: '第一章·' + (bgResult.world.dynasty_name || '序幕'), + started_at: new Date().toISOString() + }], + events: [], + choices: [] + }; + + // ④ 自动存档 + var saveId = saveManager.save(state, persona, history); + + res.json({ + error: false, + save_id: saveId, + state: state, + narrative: { + chapter_title: history.chapters[0].title, + narrative: bgResult.world.opening_event, + choices: bgResult.world.opening_choices + }, + four_dimensions: state.four_dimensions + }); + } catch (err) { + res.status(500).json({ + error: true, + code: 'START_ERROR', + message: '宫廷世界生成失败:' + err.message + }); + } +}); + +module.exports = router; diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/backend/server.js b/agents/zhuyuan-dev-agent/modules/palace-game/backend/server.js new file mode 100644 index 0000000..15c96bf --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/backend/server.js @@ -0,0 +1,59 @@ +/** + * M-PALACE · Express 主服务 + * 动态人格宫廷生成系统 · 后端核心 + * + * API 路由: + * POST /api/palace/start → 快速启动(生成世界) + * POST /api/palace/interact → 玩家互动(剧情推进) + * POST /api/palace/save → 存档 + * POST /api/palace/load → 读档 + * GET /api/palace/saves → 存档列表 + * GET /api/palace/health → 健康检查 + */ + +const express = require('express'); +const cors = require('cors'); +const path = require('path'); + +const startRoutes = require('./routes/start'); +const interactRoutes = require('./routes/interact'); +const saveRoutes = require('./routes/save'); + +const app = express(); +const PORT = process.env.PALACE_PORT || 3003; + +// 中间件 +app.use(cors()); +app.use(express.json({ limit: '2mb' })); + +// 静态文件:前端 +app.use('/palace-game', express.static(path.join(__dirname, '..', 'frontend'))); + +// API 路由 +app.use('/api/palace/start', startRoutes); +app.use('/api/palace/interact', interactRoutes); +app.use('/api/palace/save', saveRoutes); + +// 健康检查 +app.get('/api/palace/health', function (_req, res) { + res.json({ + status: 'ok', + service: 'palace-game', + version: '1.0.0', + timestamp: new Date().toISOString() + }); +}); + +// 根路由 → 前端入口 +app.get('/', function (_req, res) { + res.redirect('/palace-game/index.html'); +}); + +// 启动服务 +if (require.main === module) { + app.listen(PORT, function () { + console.log('🏯 M-PALACE · 宫廷纪服务启动 · 端口 ' + PORT); + }); +} + +module.exports = app; diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/data/palace-db.json b/agents/zhuyuan-dev-agent/modules/palace-game/data/palace-db.json new file mode 100644 index 0000000..9f4e8c2 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/data/palace-db.json @@ -0,0 +1,19 @@ +{ + "power": { + "positions": ["皇帝", "太后", "皇后", "贵妃", "丞相", "大将军", "太监总管"], + "factions": ["太后党", "皇后党", "新贵派", "清流派", "孤臣"], + "events": ["朝堂弹劾", "密谋政变", "联姻结盟", "战事告急"] + }, + "status": { + "ranks": ["答应", "常在", "贵人", "嫔", "妃", "贵妃", "皇贵妃", "皇后"], + "events": ["晋封", "降位", "禁足", "受宠", "失宠", "怀孕", "小产"] + }, + "emotion": { + "types": ["暗恋", "深情", "嫉妒", "怨恨", "愧疚", "依赖", "背叛"], + "triggers": ["偶遇旧人", "收到密信", "目睹真相", "被迫抉择"] + }, + "conflict": { + "types": ["争宠", "夺权", "报仇", "自保", "救人", "揭秘"], + "escalation": ["暗示", "试探", "交锋", "撕破脸", "鱼死网破"] + } +} diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/data/persona-dict.json b/agents/zhuyuan-dev-agent/modules/palace-game/data/persona-dict.json new file mode 100644 index 0000000..3caf09d --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/data/persona-dict.json @@ -0,0 +1,52 @@ +{ + "dimensions": [ + { + "id": "ambition", + "name": "野心", + "keywords": ["权力", "登上", "掌控", "不甘", "凭什么"], + "behavior_signals": ["选择攻击性选项", "主动挑战权威", "拒绝退让"] + }, + { + "id": "warmth", + "name": "温情", + "keywords": ["保护", "心疼", "陪伴", "不忍", "算了"], + "behavior_signals": ["选择和解", "保护弱势角色", "牺牲利益"] + }, + { + "id": "aggression", + "name": "攻击性", + "keywords": ["杀", "除掉", "报复", "不放过", "代价"], + "behavior_signals": ["选择对抗", "设计陷阱", "直接冲突"] + }, + { + "id": "suspicion", + "name": "多疑", + "keywords": ["试探", "不对劲", "背后", "真的吗", "骗"], + "behavior_signals": ["频繁试探", "不信任NPC", "选择观望"] + }, + { + "id": "vanity", + "name": "虚荣", + "keywords": ["面子", "配得上", "最好的", "羡慕", "风光"], + "behavior_signals": ["追求地位象征", "在意他人评价", "选择炫耀"] + }, + { + "id": "cunning", + "name": "心机", + "keywords": ["计划", "利用", "棋子", "布局", "暗中"], + "behavior_signals": ["迂回策略", "利用他人", "长线布局"] + }, + { + "id": "loyalty", + "name": "忠义", + "keywords": ["信任", "誓言", "守护", "不背叛", "承诺"], + "behavior_signals": ["坚守阵营", "拒绝背叛", "为盟友冒险"] + }, + { + "id": "fear", + "name": "恐惧", + "keywords": ["害怕", "不敢", "万一", "小心", "退"], + "behavior_signals": ["回避冲突", "选择保守", "犹豫不决"] + } + ] +} diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/data/saves/.gitkeep b/agents/zhuyuan-dev-agent/modules/palace-game/data/saves/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/data/templates/ancient-china.json b/agents/zhuyuan-dev-agent/modules/palace-game/data/templates/ancient-china.json new file mode 100644 index 0000000..cdce45c --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/data/templates/ancient-china.json @@ -0,0 +1,40 @@ +{ + "id": "ancient-china", + "name": "古代中国", + "description": "大周王朝·正统朝堂·后宫嫔妃·权臣博弈", + "setting": { + "dynasty": "大周", + "era_prefix": ["永安", "承平", "天启", "景和", "元丰"], + "capital": "长安", + "palace_name": "未央宫", + "inner_palace": "长乐宫" + }, + "roles": { + "妃子": { + "title_options": ["淑妃", "德妃", "贤妃", "丽嫔", "婉嫔"], + "family_options": ["书香门第", "武将世家", "寒门出身", "没落贵族"], + "starting_rank": "嫔", + "intro_template": "你是{family}出身的{title},入宫{years}年。{situation}" + }, + "皇帝": { + "title_options": ["新帝", "少年天子", "中兴之主"], + "situation_options": ["初登大宝·朝局未稳", "亲政在即·太后干政", "盛世之君·暗流涌动"], + "intro_template": "你是{dynasty}的{title},{situation}。" + }, + "重臣": { + "title_options": ["丞相", "御史大夫", "太傅", "兵部尚书"], + "faction_options": ["清流派", "保皇派", "中立派"], + "intro_template": "你是{dynasty}{title},{faction}中流砥柱。{situation}" + }, + "奸臣": { + "title_options": ["内阁首辅", "司礼监掌印", "锦衣卫指挥使"], + "ambition_options": ["权倾朝野", "挟天子以令诸侯", "暗中培植势力"], + "intro_template": "你是{title},表面忠良,实则{ambition}。" + } + }, + "atmosphere": { + "time_of_day": ["寅时·天未亮", "辰时·早朝", "午时·日正中", "酉时·暮色", "亥时·深夜"], + "weather": ["细雨绵绵", "月朗星稀", "寒风凛冽", "春光明媚", "大雪纷飞"], + "locations": ["御书房", "太和殿", "长乐宫", "御花园", "冷宫", "密道", "城墙"] + } +} diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/data/templates/fantasy-dynasty.json b/agents/zhuyuan-dev-agent/modules/palace-game/data/templates/fantasy-dynasty.json new file mode 100644 index 0000000..818595b --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/data/templates/fantasy-dynasty.json @@ -0,0 +1,40 @@ +{ + "id": "fantasy-dynasty", + "name": "架空王朝", + "description": "九霄帝国·灵力与权谋交织·玄幻宫廷", + "setting": { + "dynasty": "九霄", + "era_prefix": ["灵元", "天衍", "混沌", "太初", "紫极"], + "capital": "天枢城", + "palace_name": "凌霄殿", + "inner_palace": "瑶光阁" + }, + "roles": { + "妃子": { + "title_options": ["星妃", "月嫔", "霜华夫人", "云裳贵人"], + "family_options": ["上古灵族后裔", "剑修世家", "炼丹宗门", "凡人修仙"], + "starting_rank": "嫔", + "intro_template": "你是{family}出身的{title},入宫{years}年。{situation}" + }, + "皇帝": { + "title_options": ["灵帝", "天选之主", "末代帝君"], + "situation_options": ["灵力觉醒·诸族觊觎", "仙魔大战后·百废待兴", "天命将尽·寻找继承"], + "intro_template": "你是{dynasty}的{title},{situation}。" + }, + "重臣": { + "title_options": ["星辰阁大长老", "天机院主", "护国神将", "灵枢司正"], + "faction_options": ["仙道派", "人道派", "中立长老会"], + "intro_template": "你是{dynasty}{title},{faction}核心。{situation}" + }, + "奸臣": { + "title_options": ["暗影阁主", "禁术研究者", "外域使者"], + "ambition_options": ["窃取帝国灵脉", "复活远古邪神", "暗中操控傀儡帝"], + "intro_template": "你是{title},表面忠良,实则{ambition}。" + } + }, + "atmosphere": { + "time_of_day": ["灵潮初涌·黎明", "日中·灵力最盛", "黄昏·阴阳交替", "子夜·暗灵活跃"], + "weather": ["灵雨飘洒", "星河低垂", "雷灵暴动", "万里晴空·灵气充沛", "迷雾笼罩"], + "locations": ["灵枢大殿", "星象台", "瑶光阁", "灵兽园", "封印之地", "虚空裂缝", "万灵池"] + } +} diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/frontend/game.html b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/game.html new file mode 100644 index 0000000..7ad7d29 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/game.html @@ -0,0 +1,85 @@ + + + + + + 宫廷纪 · 叙事 + + + + +
      + + +
      + 🏯 宫廷纪 +
      + + +
      +
      + + +
      +

      第一章 · 序幕

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

      ┌ 选项区 ─

      +
      +
      + + +
      +
      + + +
      +
      ▼ 状态栏
      +
      +
      + 🔴 + 权力值 +
      + 50 +
      +
      + 💜 + 地位值 +
      + 50 +
      +
      + 💛 + 情感值 +
      + 50 +
      +
      + ⚔️ + 冲突值 +
      + 50 +
      +
      +
      + + +
      + 📖 第1章 · 第1段 + 存档号: — +
      + +
      + + + + diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/frontend/game.js b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/game.js new file mode 100644 index 0000000..3fb4ce0 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/game.js @@ -0,0 +1,311 @@ +/** + * M-PALACE · 游戏交互逻辑 + * 管理叙事渲染、选项交互、状态栏更新、存档操作 + */ + +(function () { + 'use strict'; + + var API_BASE = window.location.origin; + var saveId = sessionStorage.getItem('palace_save_id') || ''; + var choicesMeta = null; + var isTyping = false; + + // ---------- Initialization ---------- + function init() { + var startData = sessionStorage.getItem('palace_state'); + var loadedData = sessionStorage.getItem('palace_loaded'); + + if (startData) { + var data = JSON.parse(startData); + sessionStorage.removeItem('palace_state'); + renderGameData(data); + } else if (loadedData) { + var loaded = JSON.parse(loadedData); + sessionStorage.removeItem('palace_loaded'); + renderLoadedData(loaded); + } else if (saveId) { + loadSave(saveId); + } else { + window.location.href = 'index.html'; + } + + bindEvents(); + } + + // ---------- Rendering ---------- + function renderGameData(data) { + saveId = data.save_id || saveId; + document.getElementById('save-id-display').textContent = '存档号: ' + saveId; + + if (data.state && data.state.player) { + var p = data.state.player; + document.getElementById('top-title').textContent = + '🏯 宫廷纪 · ' + (p.worldview || '') + ' · ' + (p.role || ''); + } + + if (data.narrative) { + renderNarrative(data.narrative); + } + + if (data.four_dimensions) { + updateStatusBar(data.four_dimensions); + } + + updateProgressInfo(data.chapter || 1, data.paragraph || 1); + } + + function renderLoadedData(data) { + document.getElementById('save-id-display').textContent = '存档号: ' + saveId; + + if (data.state) { + var s = data.state; + if (s.player) { + document.getElementById('top-title').textContent = + '🏯 宫廷纪 · ' + (s.player.worldview || '') + ' · ' + (s.player.role || ''); + } + if (s.four_dimensions) { + updateStatusBar(s.four_dimensions); + } + updateProgressInfo(s.chapter || 1, s.paragraph || 1); + } + + // Render last narrative from history or show resume message + var narrativeEl = document.getElementById('narrative-text'); + narrativeEl.textContent = '存档已恢复。继续你的宫廷故事……'; + renderChoices(['继续前行', '回顾往事', '观察四周']); + } + + function renderNarrative(narrative) { + if (narrative.chapter_title) { + document.getElementById('chapter-title').textContent = narrative.chapter_title; + } + + var textEl = document.getElementById('narrative-text'); + if (narrative.narrative) { + typewriter(textEl, narrative.narrative); + } + + if (narrative.choices && narrative.choices.length > 0) { + // Delay choices until typewriter finishes + var delay = narrative.narrative ? narrative.narrative.length * 50 + 500 : 200; + setTimeout(function () { + renderChoices(narrative.choices); + }, delay); + } + + choicesMeta = narrative.choices_meta || null; + } + + function renderChoices(choices) { + var container = document.getElementById('choices-container'); + container.innerHTML = ''; + + var nums = ['①', '②', '③']; + choices.forEach(function (text, i) { + var btn = document.createElement('button'); + btn.className = 'choice-btn fade-in'; + btn.innerHTML = '' + (nums[i] || '·') + '' + escapeHtml(text); + btn.addEventListener('click', function () { + submitChoice(i, text); + }); + container.appendChild(btn); + }); + } + + // ---------- Typewriter Effect ---------- + function typewriter(el, text) { + isTyping = true; + el.textContent = ''; + var cursor = document.createElement('span'); + cursor.className = 'typewriter-cursor'; + el.appendChild(cursor); + + var i = 0; + var interval = setInterval(function () { + if (i < text.length) { + el.insertBefore(document.createTextNode(text[i]), cursor); + i++; + } else { + clearInterval(interval); + if (cursor.parentNode) cursor.remove(); + isTyping = false; + } + }, 45); + } + + // ---------- Status Bar ---------- + function updateStatusBar(dims) { + var keys = ['power', 'status', 'emotion', 'conflict']; + keys.forEach(function (k) { + var val = dims[k] != null ? dims[k] : 50; + var bar = document.getElementById('bar-' + k); + var valEl = document.getElementById('val-' + k); + if (bar) bar.style.width = val + '%'; + if (valEl) valEl.textContent = val; + }); + } + + function updateProgressInfo(chapter, paragraph) { + var el = document.getElementById('progress-info'); + if (el) el.textContent = '📖 第' + chapter + '章 · 第' + paragraph + '段'; + } + + // ---------- Interactions ---------- + function submitChoice(index, text) { + if (isTyping) return; + disableChoices(); + + fetch(API_BASE + '/api/palace/interact', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + save_id: saveId, + input: text, + choice_index: index, + option_meta: choicesMeta + }) + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.error) { + showToast(data.message || '互动失败'); + enableChoices(); + return; + } + choicesMeta = data.choices_meta || null; + if (data.narrative) renderNarrative(data.narrative); + if (data.four_dimensions) updateStatusBar(data.four_dimensions); + updateProgressInfo(data.chapter || 1, data.paragraph || 1); + }) + .catch(function (err) { + showToast('网络错误:' + err.message); + enableChoices(); + }); + } + + function submitFreeInput() { + var input = document.getElementById('free-input'); + var text = input.value.trim(); + if (!text || isTyping) return; + input.value = ''; + disableChoices(); + + fetch(API_BASE + '/api/palace/interact', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + save_id: saveId, + input: text, + choice_index: null, + option_meta: choicesMeta + }) + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.error) { + showToast(data.message || '互动失败'); + enableChoices(); + return; + } + choicesMeta = data.choices_meta || null; + if (data.narrative) renderNarrative(data.narrative); + if (data.four_dimensions) updateStatusBar(data.four_dimensions); + updateProgressInfo(data.chapter || 1, data.paragraph || 1); + }) + .catch(function (err) { + showToast('网络错误:' + err.message); + enableChoices(); + }); + } + + function disableChoices() { + var btns = document.querySelectorAll('.choice-btn'); + btns.forEach(function (b) { b.disabled = true; b.style.opacity = '0.5'; }); + } + + function enableChoices() { + var btns = document.querySelectorAll('.choice-btn'); + btns.forEach(function (b) { b.disabled = false; b.style.opacity = '1'; }); + } + + // ---------- Save / Load ---------- + function doSave() { + fetch(API_BASE + '/api/palace/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ save_id: saveId, state: {}, persona: {}, history: {} }) + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.error) { showToast(data.message); return; } + showToast('存档成功:' + data.save_id); + }) + .catch(function (err) { + showToast('存档失败:' + err.message); + }); + } + + function loadSave(id) { + fetch(API_BASE + '/api/palace/save/load', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ save_id: id }) + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.error) { + showToast(data.message || '读档失败'); + window.location.href = 'index.html'; + return; + } + renderLoadedData(data); + }) + .catch(function (err) { + showToast('网络错误:' + err.message); + window.location.href = 'index.html'; + }); + } + + // ---------- Events ---------- + function bindEvents() { + document.getElementById('btn-save').addEventListener('click', doSave); + + document.getElementById('btn-free-submit').addEventListener('click', submitFreeInput); + + document.getElementById('free-input').addEventListener('keydown', function (e) { + if (e.key === 'Enter') submitFreeInput(); + }); + + var toggle = document.getElementById('status-toggle'); + var bar = document.getElementById('status-bar'); + toggle.addEventListener('click', function () { + bar.classList.toggle('collapsed'); + toggle.textContent = bar.classList.contains('collapsed') ? '▶ 状态栏' : '▼ 状态栏'; + }); + } + + // ---------- Utilities ---------- + function escapeHtml(str) { + var div = document.createElement('div'); + div.appendChild(document.createTextNode(str)); + return div.innerHTML; + } + + function showToast(msg) { + var old = document.querySelector('.toast'); + if (old) old.remove(); + var el = document.createElement('div'); + el.className = 'toast'; + el.textContent = msg; + document.body.appendChild(el); + setTimeout(function () { if (el.parentNode) el.remove(); }, 3000); + } + + // ---------- Boot ---------- + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/frontend/index.html b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/index.html new file mode 100644 index 0000000..e3969f3 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/index.html @@ -0,0 +1,123 @@ + + + + + + 宫·廷·纪 — 动态人格宫廷生成系统 + + + + +
      + +
      🏯
      +

      宫 · 廷 · 纪

      +

      动态人格宫廷生成系统

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

      已有存档?输入编号继续

      +
      + + +
      +
      + + +
      + + + + diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/frontend/save.html b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/save.html new file mode 100644 index 0000000..ac38470 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/save.html @@ -0,0 +1,50 @@ + + + + + + 宫廷纪 · 存档管理 + + + + +
      + +

      🏯 存档管理

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

      已有存档

      +
      +
      + + + +
      +
      + +
      + +
      + +
      + +
      + + + + diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/frontend/save.js b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/save.js new file mode 100644 index 0000000..bc6551e --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/save.js @@ -0,0 +1,139 @@ +/** + * M-PALACE · 存档读档逻辑 + * 列出存档、读取存档、删除存档 + */ + +(function () { + 'use strict'; + + var API_BASE = window.location.origin; + + function init() { + loadSaveList(); + bindEvents(); + } + + // ---------- Load Save List ---------- + function loadSaveList() { + var container = document.getElementById('save-list'); + + fetch(API_BASE + '/api/palace/save/list') + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.error) { + container.innerHTML = '
      获取存档列表失败
      '; + return; + } + renderSaveList(data.saves || []); + }) + .catch(function () { + container.innerHTML = '
      无法连接服务器
      '; + }); + } + + function renderSaveList(saves) { + var container = document.getElementById('save-list'); + + if (saves.length === 0) { + container.innerHTML = '
      暂无存档
      '; + return; + } + + container.innerHTML = ''; + saves.forEach(function (save) { + var item = document.createElement('div'); + item.className = 'save-item fade-in'; + + var info = document.createElement('div'); + info.className = 'save-info'; + + var idEl = document.createElement('div'); + idEl.className = 'save-id'; + idEl.textContent = save.save_id; + + var detail = document.createElement('div'); + detail.className = 'save-detail'; + var parts = []; + if (save.dynasty) parts.push(save.dynasty); + if (save.role) parts.push(save.role); + if (save.chapter) parts.push('第' + save.chapter + '章'); + if (save.created_at) parts.push(save.created_at.slice(0, 16).replace('T', ' ')); + detail.textContent = parts.join(' · '); + + info.appendChild(idEl); + info.appendChild(detail); + + var actions = document.createElement('div'); + actions.className = 'save-actions'; + + var loadBtn = document.createElement('button'); + loadBtn.className = 'btn btn-small'; + loadBtn.textContent = '读档'; + loadBtn.addEventListener('click', function () { + doLoad(save.save_id); + }); + + actions.appendChild(loadBtn); + + item.appendChild(info); + item.appendChild(actions); + container.appendChild(item); + }); + } + + // ---------- Load Save ---------- + function doLoad(saveId) { + fetch(API_BASE + '/api/palace/save/load', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ save_id: saveId }) + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.error) { + showToast(data.message || '读档失败'); + return; + } + sessionStorage.setItem('palace_save_id', saveId); + sessionStorage.setItem('palace_loaded', JSON.stringify(data)); + window.location.href = 'game.html'; + }) + .catch(function (err) { + showToast('网络错误:' + err.message); + }); + } + + // ---------- Events ---------- + function bindEvents() { + document.getElementById('btn-load').addEventListener('click', function () { + var id = document.getElementById('load-id').value.trim(); + if (!id) { showToast('请输入存档编号'); return; } + doLoad(id); + }); + + document.getElementById('load-id').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + var id = this.value.trim(); + if (id) doLoad(id); + } + }); + } + + // ---------- Utilities ---------- + function showToast(msg) { + var old = document.querySelector('.toast'); + if (old) old.remove(); + var el = document.createElement('div'); + el.className = 'toast'; + el.textContent = msg; + document.body.appendChild(el); + setTimeout(function () { if (el.parentNode) el.remove(); }, 3000); + } + + // ---------- Boot ---------- + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/agents/zhuyuan-dev-agent/modules/palace-game/frontend/style.css b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/style.css new file mode 100644 index 0000000..ffd770f --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/palace-game/frontend/style.css @@ -0,0 +1,564 @@ +/* ==================================== + M-PALACE · 宫廷视觉风格 + 暗红+金色主题 · 古风字体 · 逐字动画 + ==================================== */ + +/* ---------- CSS Variables ---------- */ +:root { + --bg-primary: #1a0a0a; + --bg-secondary: #0d0d1a; + --gold: #c9a96e; + --gold-light: #e8d5a3; + --danger: #8b0000; + --text-primary: #f5f0e8; + --text-secondary:#a0937d; + --border-gold: rgba(201,169,110,0.15); + --border-gold-md:rgba(201,169,110,0.3); + --glow-gold: rgba(201,169,110,0.25); +} + +/* ---------- Reset & Base ---------- */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + line-height: 1.6; + overflow-x: hidden; +} + +/* ---------- Background Texture ---------- */ +body::before { + content: ''; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: + radial-gradient(ellipse at 20% 50%, rgba(139,0,0,0.08) 0%, transparent 60%), + radial-gradient(ellipse at 80% 20%, rgba(201,169,110,0.05) 0%, transparent 50%), + radial-gradient(ellipse at 50% 80%, rgba(13,13,26,0.6) 0%, transparent 70%); + pointer-events: none; + z-index: 0; +} + +/* ---------- Layout ---------- */ +.container { + max-width: 720px; + margin: 0 auto; + padding: 2rem 1.5rem; + position: relative; + z-index: 1; +} + +/* ---------- Typography ---------- */ +.serif { + font-family: 'Noto Serif SC', 'Source Han Serif CN', 'STSong', serif; +} + +h1, h2, h3 { color: var(--gold); font-weight: 400; } + +/* ---------- Golden Divider ---------- */ +.divider { + height: 1px; + background: linear-gradient(90deg, transparent, var(--gold), transparent); + margin: 1.5rem 0; + opacity: 0.4; +} + +.divider-text { + text-align: center; + color: var(--text-secondary); + font-size: 0.85rem; + padding: 0.5rem 0; +} + +/* ==================================== + INDEX PAGE — Quick Start + ==================================== */ +.start-page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + text-align: center; + padding: 2rem; +} + +.start-icon { + font-size: 4rem; + margin-bottom: 1rem; + filter: drop-shadow(0 0 20px var(--glow-gold)); +} + +.start-title { + font-family: 'Noto Serif SC', 'Source Han Serif CN', serif; + font-size: 2.2rem; + letter-spacing: 0.8rem; + color: var(--gold); + margin-bottom: 0.3rem; +} + +.start-subtitle { + font-size: 0.9rem; + color: var(--text-secondary); + margin-bottom: 2.5rem; + letter-spacing: 0.2rem; +} + +/* Form Card */ +.start-card { + background: rgba(26,10,10,0.85); + border: 1px solid var(--border-gold); + border-radius: 8px; + padding: 2rem 2.5rem; + width: 100%; + max-width: 420px; + backdrop-filter: blur(8px); +} + +.form-group { + margin-bottom: 1.5rem; + text-align: left; +} + +.form-group label { + display: block; + font-size: 0.9rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +select, input[type="text"] { + width: 100%; + padding: 0.7rem 1rem; + background: rgba(13,13,26,0.6); + border: 1px solid var(--border-gold-md); + border-radius: 4px; + color: var(--text-primary); + font-size: 0.95rem; + font-family: inherit; + outline: none; + transition: border-color 0.3s; +} + +select:focus, input[type="text"]:focus { + border-color: var(--gold); + box-shadow: 0 0 8px var(--glow-gold); +} + +select option { + background: var(--bg-primary); + color: var(--text-primary); +} + +/* ---------- Buttons ---------- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 2rem; + border: 1px solid var(--gold); + border-radius: 4px; + background: rgba(139,0,0,0.3); + color: var(--gold); + font-size: 1rem; + font-family: 'Noto Serif SC', 'Source Han Serif CN', serif; + cursor: pointer; + transition: all 0.3s; + letter-spacing: 0.15rem; +} + +.btn:hover { + background: rgba(139,0,0,0.6); + box-shadow: 0 0 16px var(--glow-gold); + transform: translateY(-1px); +} + +.btn:active { transform: translateY(0); } + +.btn-primary { + width: 100%; + padding: 1rem; + font-size: 1.1rem; + margin-top: 0.5rem; +} + +.btn-small { + padding: 0.5rem 1.2rem; + font-size: 0.85rem; +} + +/* Load Section */ +.load-section { + width: 100%; + max-width: 420px; + margin-top: 1rem; +} + +.load-row { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.load-row input { flex: 1; } + +/* Footer */ +.start-footer { + margin-top: 3rem; + font-size: 0.8rem; + color: var(--text-secondary); + opacity: 0.6; +} + +/* ==================================== + GAME PAGE — Main Narrative + ==================================== */ +.game-page { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +/* Top Bar */ +.top-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.8rem 1.5rem; + border-bottom: 1px solid var(--border-gold); + background: rgba(26,10,10,0.9); + backdrop-filter: blur(6px); + position: sticky; + top: 0; + z-index: 10; +} + +.top-bar-title { + font-family: 'Noto Serif SC', 'Source Han Serif CN', serif; + font-size: 1rem; + color: var(--gold); +} + +.top-bar-actions { + display: flex; + gap: 0.5rem; +} + +/* Narrative Area */ +.narrative-area { + flex: 1; + padding: 2rem 1.5rem; + max-width: 720px; + margin: 0 auto; + width: 100%; +} + +.chapter-title { + font-family: 'Noto Serif SC', 'Source Han Serif CN', serif; + font-size: 1.4rem; + color: var(--gold); + text-align: center; + margin-bottom: 1.5rem; + letter-spacing: 0.3rem; +} + +.narrative-text { + font-family: 'Noto Serif SC', 'Source Han Serif CN', serif; + font-size: 1rem; + line-height: 1.9; + color: var(--text-primary); + text-align: justify; + margin-bottom: 2rem; + min-height: 120px; +} + +/* Typewriter Animation */ +.typewriter-cursor { + display: inline-block; + width: 2px; + height: 1.1em; + background: var(--gold); + margin-left: 2px; + vertical-align: text-bottom; + animation: blink 0.8s infinite; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* Choices Area */ +.choices-area { + padding: 0 1.5rem 1.5rem; + max-width: 720px; + margin: 0 auto; + width: 100%; +} + +.choices-label { + font-size: 0.85rem; + color: var(--text-secondary); + margin-bottom: 0.8rem; + padding-left: 0.3rem; +} + +.choice-btn { + display: block; + width: 100%; + padding: 0.9rem 1.2rem; + margin-bottom: 0.6rem; + background: rgba(139,0,0,0.15); + border: 1px solid var(--border-gold-md); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.95rem; + font-family: 'Noto Serif SC', 'Source Han Serif CN', serif; + text-align: left; + cursor: pointer; + transition: all 0.3s; +} + +.choice-btn:hover { + background: rgba(139,0,0,0.35); + border-color: var(--gold); + box-shadow: 0 0 12px var(--glow-gold); + transform: translateX(4px); +} + +.choice-btn:active { + transform: translateX(2px); +} + +.choice-btn .choice-num { + color: var(--gold); + margin-right: 0.5rem; + font-weight: 600; +} + +/* Free Input */ +.free-input-row { + display: flex; + gap: 0.5rem; + margin-top: 0.3rem; +} + +.free-input-row input { + flex: 1; + font-family: 'Noto Serif SC', 'Source Han Serif CN', serif; +} + +/* Status Bar */ +.status-bar { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-gold); + background: rgba(13,13,26,0.6); + backdrop-filter: blur(6px); +} + +.status-toggle { + font-size: 0.8rem; + color: var(--text-secondary); + cursor: pointer; + margin-bottom: 0.5rem; + user-select: none; +} + +.status-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.6rem 1.5rem; +} + +.stat-row { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.85rem; +} + +.stat-icon { width: 1.2rem; text-align: center; } + +.stat-label { + width: 3rem; + color: var(--text-secondary); + flex-shrink: 0; +} + +.stat-bar-bg { + flex: 1; + height: 6px; + background: rgba(201,169,110,0.1); + border-radius: 3px; + overflow: hidden; +} + +.stat-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.6s ease; +} + +.stat-bar-fill.power { background: linear-gradient(90deg, #8b0000, #c9a96e); } +.stat-bar-fill.status { background: linear-gradient(90deg, #6b21a8, #c9a96e); } +.stat-bar-fill.emotion { background: linear-gradient(90deg, #b8860b, #e8d5a3); } +.stat-bar-fill.conflict{ background: linear-gradient(90deg, #4a1a2e, #8b0000); } + +.stat-value { + width: 1.8rem; + text-align: right; + color: var(--gold-light); + font-size: 0.8rem; +} + +/* Bottom Bar */ +.bottom-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1.5rem; + font-size: 0.75rem; + color: var(--text-secondary); + border-top: 1px solid var(--border-gold); + background: rgba(26,10,10,0.9); +} + +/* Status collapsed */ +.status-bar.collapsed .status-grid { display: none; } + +/* ==================================== + SAVE PAGE + ==================================== */ +.save-page { + min-height: 100vh; + padding: 2rem 1.5rem; +} + +.save-page h1 { + font-family: 'Noto Serif SC', 'Source Han Serif CN', serif; + text-align: center; + margin-bottom: 2rem; + letter-spacing: 0.4rem; +} + +.save-list { + max-width: 600px; + margin: 0 auto; +} + +.save-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.2rem; + margin-bottom: 0.8rem; + background: rgba(26,10,10,0.7); + border: 1px solid var(--border-gold); + border-radius: 6px; + transition: border-color 0.3s; +} + +.save-item:hover { + border-color: var(--gold); +} + +.save-info { flex: 1; } + +.save-id { + font-family: 'Noto Serif SC', 'Source Han Serif CN', serif; + font-size: 1rem; + color: var(--gold); +} + +.save-detail { + font-size: 0.8rem; + color: var(--text-secondary); + margin-top: 0.2rem; +} + +.save-actions { + display: flex; + gap: 0.4rem; +} + +.empty-state { + text-align: center; + padding: 3rem; + color: var(--text-secondary); +} + +/* ==================================== + LOADING & TRANSITIONS + ==================================== */ +.loading { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 2rem; + color: var(--text-secondary); +} + +.loading-dot { + width: 6px; + height: 6px; + background: var(--gold); + border-radius: 50%; + animation: pulse 1.2s infinite; +} + +.loading-dot:nth-child(2) { animation-delay: 0.2s; } +.loading-dot:nth-child(3) { animation-delay: 0.4s; } + +@keyframes pulse { + 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } + 40% { opacity: 1; transform: scale(1.2); } +} + +/* Fade transition */ +.fade-in { + animation: fadeIn 0.6s ease forwards; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Toast notification */ +.toast { + position: fixed; + top: 1rem; + right: 1rem; + padding: 0.8rem 1.5rem; + background: rgba(26,10,10,0.95); + border: 1px solid var(--gold); + border-radius: 6px; + color: var(--gold-light); + font-size: 0.9rem; + z-index: 100; + animation: slideIn 0.3s ease, fadeOut 0.3s ease 2.5s forwards; +} + +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes fadeOut { + to { opacity: 0; transform: translateY(-10px); } +} + +/* ==================================== + Responsive + ==================================== */ +@media (max-width: 600px) { + .start-card { padding: 1.5rem; } + .start-title { font-size: 1.8rem; letter-spacing: 0.5rem; } + .status-grid { grid-template-columns: 1fr; } + .top-bar-title { font-size: 0.85rem; } +} diff --git a/agents/zhuyuan-dev-agent/modules/plan-expander.js b/agents/zhuyuan-dev-agent/modules/plan-expander.js new file mode 100644 index 0000000..c76d83b --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/plan-expander.js @@ -0,0 +1,78 @@ +// ═══════════════════════════════════════ +// 铸渊开发Agent · 任务补全引擎 +// 高层面规划 → LLM补全 → 详细可执行步骤 +// ═══════════════════════════════════════ + +const fs = require("fs"); +const path = require("path"); + +/** + * 补全单条高级任务为详细步骤 + * @param {object} task - { id, action, description, context } + * @param {function} modelCaller - LLM 调用函数 (prompt) => string + * @returns {object} 补全后的任务 + */ +async function expandTask(task, modelCaller) { + const prompt = [ + "你是一个开发Agent的任务补全引擎。将高级描述补全为可执行的详细步骤。", + "", + "原任务:", + " ID: " + task.id, + " 动作: " + task.action, + " 描述: " + (task.description || "无"), + " 上下文: " + (task.context || "无"), + "", + "请严格基于以上原任务拆解。不要改变原任务的action/target/content。只补充detail、depends_on等元信息。如果原任务已经足够具体,直接返回:", + "[{\"step\":1,\"action\":\"write_file|run_cmd|call_api|check\",\"target\":\"文件路径或命令\",\"detail\":\"具体做什么\",\"depends_on\":null}]" + ].join("\n"); + + try { + const raw = await modelCaller(prompt); + const jsonMatch = raw.match(/\[[\s\S]*\]/); + if (jsonMatch) { + const substeps = JSON.parse(jsonMatch[0]); + return { ...task, substeps, expanded: true, expanded_at: new Date().toISOString() }; + } + } catch (e) { + console.warn("[Expander] LLM补全失败,退化为单步模式: " + e.message); + } + + // 退化:LLM不可用时,原任务本身就是一个步骤 + return { + ...task, + substeps: [{ step: 1, action: task.action, target: task.target || "", detail: task.description || "" }], + expanded: false, + expanded_at: new Date().toISOString() + }; +} + +/** + * 完整规划补全 + * @param {object} plan - 完整规划 { plan_id, tasks[], ... } + * @param {function} modelCaller - LLM 调用函数 + * @param {string} plansDir - 规划文件存储目录 + */ +async function expandPlan(plan, modelCaller, plansDir) { + console.log("[Expander] 开始补全规划: " + plan.plan_id + " · " + plan.tasks.length + "个任务"); + + const expandedTasks = []; + for (let i = 0; i < plan.tasks.length; i++) { + const task = plan.tasks[i]; + console.log("[Expander] 补全 " + (i + 1) + "/" + plan.tasks.length + ": " + task.id); + const expanded = await expandTask(task, modelCaller); + expandedTasks.push(expanded); + } + + plan.tasks = expandedTasks; + plan._status = "expanded"; + plan._expanded_at = new Date().toISOString(); + + // 写回规划文件 + const planFile = path.join(plansDir, plan.plan_id + ".json"); + fs.writeFileSync(planFile, JSON.stringify(plan, null, 2), "utf-8"); + + console.log("[Expander] 补全完成 · 共 " + expandedTasks.reduce((s, t) => s + (t.substeps?.length || 0), 0) + " 个子步骤"); + return plan; +} + +module.exports = { expandPlan, expandTask }; diff --git a/agents/zhuyuan-dev-agent/modules/portal/data.json b/agents/zhuyuan-dev-agent/modules/portal/data.json new file mode 100644 index 0000000..242c313 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/portal/data.json @@ -0,0 +1,23 @@ +[ + { + "id": 1, + "title": "HoloLake 2.0 发布!", + "content": "全新界面,极致体验。", + "channel": "tech", + "pinned": true + }, + { + "id": 2, + "title": "AI 会议延期通知", + "content": "原定下周的会议推迟到月底。", + "channel": "news", + "pinned": false + }, + { + "id": 3, + "title": "VS Code 插件推荐", + "content": "Live Server 是开发神器。", + "channel": "tech", + "pinned": false + } +] \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/portal/index.html b/agents/zhuyuan-dev-agent/modules/portal/index.html new file mode 100644 index 0000000..bc24dae --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/portal/index.html @@ -0,0 +1,134 @@ + + + + + + HoloLake公告栏 + + + +
      + +
      +

      公告栏

      +
      + 2条置顶 + +
      +
      + + +
      + 全部 + 系统公告 + 开发动态 + 团队消息 +
      + + +
      + +
      +
      置顶
      +
      +
      +
      + 光湖系统MVP v0.4.0发布公告 + 2小时前 +
      +

      主域公告栏与频道过渡系统(M22)正式进入环节0开发。新增公告置顶、频道分类、时间线展示功能,信息展示层逐步完善。

      + +
      +
      + + +
      +
      置顶
      +
      +
      +
      + SYSLOG提交流程v2上线通知 + 5小时前 +
      +

      SYSLOG生成方式已更新为「知秋主动提问制」完成任务后由知秋引导填写,无需手动套用模板。即日起生效。

      + +
      +
      + + +
      +
      +
      +
      + 欢迎Awen加入光湖团队! + 1天前 +
      +

      DEV-012 Awen正式加入光湖开发团队,成为第12位协作者。首个模块M09消息通知中心已全通关!

      +
      +
      + + +
      +
      +
      +
      + M08数据统计面板全通关 + 2天前 +
      +

      完成M08全部环节,七连胜达成!响应式布局+SVG环形图+CSV导出一次通过,正式前端毕业

      + +
      +
      + + +
      +
      +
      +
      + HTTPS配置与飞书Webhook联调完成 + 3天前 +
      +

      光湖主域已完成HTTPS配置与飞书Webhook全链路联调,Nginx+PM2+Certbot部署完毕,系统脊梁正式贯通。

      + +
      +
      + + +
      +
      +
      +
      + 本周开发者进度总览 + 3天前 +
      +

      12位开发者中8位活跃,累计完成30+个环节。肥猫五连胜、桔子七连胜、小草莓五连胜、Awen四连胜团队势头强劲!

      + +
      +
      +
      + + + +
      + + + + \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/portal/index.js b/agents/zhuyuan-dev-agent/modules/portal/index.js new file mode 100644 index 0000000..61aa951 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/portal/index.js @@ -0,0 +1,48 @@ +// 频道标签过滤 +document.querySelectorAll('.channel-tag').forEach(tag => { + tag.addEventListener('click', () => { + const channel = tag.dataset.channel; + document.querySelectorAll('.announcement').forEach(ann => { + ann.style.display = channel === 'all' || ann.dataset.channel === channel ? 'block' : 'none'; + }); + }); +}); + +// 置顶悬浮动画 +document.querySelectorAll('.pin-icon').forEach(pin => { + pin.addEventListener('click', () => { + const announcement = pin.closest('.announcement'); + announcement.classList.toggle('pinned'); + }); +}); + +// ✨ 动态数据加载(M24 核心!) +fetch('data.json') + .then(response => response.json()) + .then(data => { + const announcements = document.querySelector('.announcements-container'); + announcements.innerHTML = ''; + + data.forEach(item => { + const announcement = document.createElement('div'); + announcement.className = nnouncement ; + announcement.dataset.channel = item.channel; + announcement.innerHTML = +
      +

      +

      +
      + + ; + announcements.appendChild(announcement); + }); + + // 重新绑定事件 + document.querySelectorAll('.pin-icon').forEach(pin => { + pin.addEventListener('click', () => { + const announcement = pin.closest('.announcement'); + announcement.classList.toggle('pinned'); + pin.textContent = announcement.classList.contains('pinned') ? '⭐' : '✨'; + }); + }); + }); diff --git a/agents/zhuyuan-dev-agent/modules/portal/script.js b/agents/zhuyuan-dev-agent/modules/portal/script.js new file mode 100644 index 0000000..8d0aba0 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/portal/script.js @@ -0,0 +1,204 @@ +// ========== 确保 HoloLake 命名空间存在 ========== +window.HoloLake = window.HoloLake || {}; + +// ========== 页面加载完成后执行 ========== +document.addEventListener('DOMContentLoaded', function() { + console.log('🚀 script.js 初始化'); + + // 恢复所有状态 + restoreState(); + + // 绑定事件 + bindEvents(); + + // 更新布局信息 + updateLayoutInfo(); + + // 监听窗口大小变化 + window.addEventListener('resize', function() { + updateLayoutInfo(); + }); +}); + +// ========== 恢复状态 ========== +function restoreState() { + console.log('🔍 restoreState 开始...'); + + try { + // 1. 恢复订阅状态 + const isSubscribed = HoloLake.UserState.isSubscribed(); + const subscribeBtn = document.querySelector('.subscribe-btn'); + if (subscribeBtn) { + if (isSubscribed) { + subscribeBtn.classList.add('active'); + subscribeBtn.textContent = '已订阅'; + } else { + subscribeBtn.classList.remove('active'); + subscribeBtn.textContent = '订阅'; + } + } + + // 2. 恢复已读公告 + const readList = HoloLake.UserState.getReadBulletins(); + const bulletinCards = document.querySelectorAll('.bulletin-card'); + bulletinCards.forEach((card, index) => { + const bulletinId = `bulletin-${index}`; + if (readList.includes(bulletinId)) { + card.classList.add('read'); + } + }); + + // 3. 恢复选中频道 + const activeChannel = HoloLake.UserState.getActiveChannel(); + const channels = document.querySelectorAll('.channel'); + channels.forEach(channel => { + const channelText = channel.textContent.trim(); + if (channelText === activeChannel || (activeChannel === '全部' && channelText === '全部')) { + channel.classList.add('active'); + } else { + channel.classList.remove('active'); + } + }); + + // 4. 更新最后访问时间 + HoloLake.UserState.updateLastVisit(); + + console.log('✅ 恢复已完成'); + } catch (e) { + console.error('❌ 恢复失败:', e); + } +} + +// ========== 绑定事件 ========== +function bindEvents() { + console.log('🔗 绑定事件...'); + + // 订阅按钮 + const subscribeBtn = document.querySelector('.subscribe-btn'); + if (subscribeBtn) { + subscribeBtn.addEventListener('click', function() { + const newState = HoloLake.UserState.toggleSubscribe(); + if (newState) { + this.classList.add('active'); + this.textContent = '已订阅'; + } else { + this.classList.remove('active'); + this.textContent = '订阅'; + } + console.log('📌 订阅状态:', newState ? '已订阅' : '未订阅'); + }); + } + + // 频道点击 + const channels = document.querySelectorAll('.channel'); + channels.forEach(channel => { + channel.addEventListener('click', function() { + // 更新UI + channels.forEach(c => c.classList.remove('active')); + this.classList.add('active'); + + // 保存状态 + const channelName = this.textContent.trim(); + HoloLake.UserState.setActiveChannel(channelName); + + // 筛选公告(可选功能,这里先做简单筛选) + filterBulletinsByChannel(channelName); + + console.log('📺 切换到频道:', channelName); + }); + }); + + // 公告点击(标记已读) + const bulletinCards = document.querySelectorAll('.bulletin-card'); + bulletinCards.forEach((card, index) => { + card.addEventListener('click', function() { + const bulletinId = `bulletin-${index}`; + + // 标记已读 + if (!this.classList.contains('read')) { + this.classList.add('read'); + HoloLake.UserState.markAsRead(bulletinId); + console.log('📖 已读公告:', bulletinId); + } + }); + }); +} + +// ========== 按频道筛选 ========== +function filterBulletinsByChannel(channelName) { + const cards = document.querySelectorAll('.bulletin-card'); + + if (channelName === '全部') { + cards.forEach(card => { + card.style.display = 'flex'; + }); + return; + } + + cards.forEach(card => { + const tag = card.querySelector('.bulletin-tag'); + if (tag && tag.textContent.trim() === channelName) { + card.style.display = 'flex'; + } else { + card.style.display = 'none'; + } + }); +} + +// ========== 更新布局信息 ========== +function updateLayoutInfo() { + const width = window.innerWidth; + let mode = '桌面'; + + if (width <= 480) { + mode = '手机'; + } else if (width <= 768) { + mode = '平板'; + } + + console.log(`📱 当前布局: ${mode}模式 (${width}px)`); +} +// ========== 环节3:动画控制 ========== + +// 初始化动画 +function initAnimations() { + // 检测用户是否开启了「减少动画」 + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + if (prefersReducedMotion) { + console.log('♿ 用户开启了减少动画,跳过动画'); + return; + } + + // 为所有公告卡片设置递增的动画延迟 + const cards = document.querySelectorAll('.bulletin-card'); + cards.forEach((card, index) => { + // 如果已经有内联样式,先保留原有样式,只加动画延迟 + const delay = 0.1 * (index + 1); + card.style.animationDelay = `${delay}s`; + }); + + console.log('🎬 动画已初始化,延迟已设置'); +} + +// 订阅按钮脉冲效果 +function addPulseEffect(btn) { + btn.classList.add('pulse'); + setTimeout(() => { + btn.classList.remove('pulse'); + }, 300); +} + +// 在页面加载时调用 +document.addEventListener('DOMContentLoaded', function() { + // 已有的初始化代码... + initAnimations(); + + // 为订阅按钮添加脉冲效果 + const subscribeBtn = document.querySelector('.subscribe-btn'); + if (subscribeBtn) { + subscribeBtn.addEventListener('click', function() { + addPulseEffect(this); + }); + } +}); \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/portal/storage.js b/agents/zhuyuan-dev-agent/modules/portal/storage.js new file mode 100644 index 0000000..865bd56 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/portal/storage.js @@ -0,0 +1,128 @@ +/** + * HoloLake Bulletin 数据持久化模块 + * storage.js - localStorage 封装 + 用户状态管理 + * 模块 ID: BC-M22-002-AW + */ + +window.HoloLake = window.HoloLake || {}; + +// ========== Storage 工具对象(带前缀) ========== +HoloLake.Storage = { + prefix: 'hololake_bulletin_', + + // 保存数据 + save: function(key, value) { + try { + const serialized = JSON.stringify(value); + localStorage.setItem(this.prefix + key, serialized); + console.log(`[Storage] 已保存: ${key} =`, value); + } catch (e) { + console.error('[Storage] 保存失败:', e); + } + }, + + // 读取数据 + load: function(key, defaultValue = null) { + try { + const serialized = localStorage.getItem(this.prefix + key); + if (serialized === null) { + console.log(`[Storage] 无数据: ${key},返回默认值`); + return defaultValue; + } + const value = JSON.parse(serialized); + console.log(`[Storage] 已加载: ${key} =`, value); + return value; + } catch (e) { + console.error('[Storage] 读取失败:', e); + return defaultValue; + } + }, + + // 删除指定键 + remove: function(key) { + try { + localStorage.removeItem(this.prefix + key); + console.log(`[Storage] 已删除: ${key}`); + } catch (e) { + console.error('[Storage] 删除失败:', e); + } + }, + + // 清空所有带前缀的数据 + clear: function() { + try { + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(this.prefix)) { + keysToRemove.push(key); + } + } + keysToRemove.forEach(key => localStorage.removeItem(key)); + console.log('[Storage] 已清空所有公告栏数据'); + } catch (e) { + console.error('[Storage] 清空失败:', e); + } + } +}; + +// ========== 用户状态管理 ========== +HoloLake.UserState = { + // 已读公告列表 + getReadBulletins: function() { + return HoloLake.Storage.load('read_bulletins', []); + }, + + markAsRead: function(bulletinId) { + const readList = this.getReadBulletins(); + if (!readList.includes(bulletinId)) { + readList.push(bulletinId); + HoloLake.Storage.save('read_bulletins', readList); + } + return readList; + }, + + // 订阅状态 + isSubscribed: function() { + return HoloLake.Storage.load('is_subscribed', false); + }, + + toggleSubscribe: function() { + const current = this.isSubscribed(); + const newState = !current; + HoloLake.Storage.save('is_subscribed', newState); + return newState; + }, + + // 当前频道 + getActiveChannel: function() { + return HoloLake.Storage.load('active_channel', 'all'); + }, + + setActiveChannel: function(channelId) { + HoloLake.Storage.save('active_channel', channelId); + }, + + // 上次访问时间 + getLastVisit: function() { + return HoloLake.Storage.load('last_visit', null); + }, + + updateLastVisit: function() { + const now = new Date().toISOString(); + HoloLake.Storage.save('last_visit', now); + return now; + }, + + // 重置所有状态(测试用) + resetAll: function() { + HoloLake.Storage.remove('read_bulletins'); + HoloLake.Storage.remove('is_subscribed'); + HoloLake.Storage.remove('active_channel'); + HoloLake.Storage.remove('last_visit'); + console.log('[UserState] 已重置所有状态'); + } +}; + +// 初始化时记录访问时间(可选,页面加载时由 script.js 调用) +console.log('✅ storage.js 已加载,HoloLake.Storage 和 HoloLake.UserState 已就绪'); \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/portal/style.css b/agents/zhuyuan-dev-agent/modules/portal/style.css new file mode 100644 index 0000000..7e3db57 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/portal/style.css @@ -0,0 +1,440 @@ +/* HoloLake公告栏系统深色科技风 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; + background: #0a1628; + color: #e0e6ed; + min-height: 100vh; +} + +.app-container { + max-width: 580px; + margin: 0 auto; + padding: 0 16px; +} + +/* ========== 顶部标题栏 ========== */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24px 0 16px; + border-bottom: 1px solid rgba(255,255,255,0.08); +} + +.header h1 { + font-size: 22px; + font-weight: 700; + color: #e0e6ed; +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.pinned-badge { + background: rgba(255, 183, 77, 0.15); + color: #ffb74d; + font-size: 12px; + padding: 4px 10px; + border-radius: 12px; + font-weight: 600; +} + +.subscribe-btn { + background: rgba(79, 195, 247, 0.1); + color: #4fc3f7; + border: 1px solid rgba(79, 195, 247, 0.2); + padding: 6px 14px; + border-radius: 8px; + font-size: 13px; + cursor: pointer; +} + +.subscribe-btn:hover { + background: rgba(79, 195, 247, 0.2); +} + +/* ========== 频道栏 ========== */ +.channel-bar { + display: flex; + gap: 8px; + padding: 16px 0; + border-bottom: 1px solid rgba(255,255,255,0.06); + overflow-x: auto; +} + +.channel { + padding: 8px 18px; + border-radius: 20px; + border: 1px solid rgba(255,255,255,0.1); + background: transparent; + color: #8899aa; + font-size: 13px; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.channel:hover { + border-color: rgba(255, 183, 77, 0.3); + color: #b0bec5; +} + +.channel.active { + background: rgba(255,183,77,0.15); + border-color: #ffb74d; + color: #ffb74d; + font-weight: 600; +} + +/* ========== 公告列表 ========== */ +.bulletin-list { + padding: 16px 0; +} + +.bulletin-card { + position: relative; + display: flex; + gap: 16px; + padding: 16px; + border-radius: 12px; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(255,255,255,0.04); + margin-bottom: 12px; + transition: all 0.2s ease; +} + +.bulletin-card.pinned { + border: 1px solid rgba(255, 183, 77, 0.3); + background: rgba(255, 183, 77, 0.02); +} + +.pin-indicator { + position: absolute; + top: 8px; + right: 12px; + font-size: 11px; + color: #ffb74d; + font-weight: 600; +} + +/* 公告图标 */ +.bulletin-icon { + width: 42px; + height: 42px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; +} + +.bulletin-icon.system { background: rgba(156,39,176,0.12); } +.bulletin-icon.dev { background: rgba(76,175,80,0.12); } +.bulletin-icon.team { background: rgba(79,195,247,0.12); } + +/* 公告内容 */ +.bulletin-content { + flex: 1; + min-width: 0; +} + +.bulletin-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + +.bulletin-title { + font-size: 15px; + font-weight: 600; + color: #e0e6ed; +} + +.bulletin-time { + font-size: 12px; + color: #556677; + flex-shrink: 0; + margin-left: 12px; +} + +.bulletin-summary { + font-size: 13px; + color: #8899aa; + line-height: 1.6; + margin-bottom: 10px; +} + +/* 公告底部 */ +.bulletin-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.bulletin-tag { + display: inline-block; + font-size: 11px; + padding: 2px 8px; + border-radius: 6px; + font-weight: 500; +} + +.tag-system { background: rgba(156,39,176,0.12); color: #ce93d8; } +.tag-dev { background: rgba(76,175,80,0.12); color: #81c784; } +.tag-team { background: rgba(79,195,247,0.12); color: #4fc3f7; } + +.bulletin-author { + font-size: 11px; + color: #556677; +} + +/* ========== 底部状态栏 ========== */ +.footer { + display: flex; + justify-content: space-between; + padding: 20px 0; + border-top: 1px solid rgba(255,255,255,0.06); + font-size: 12px; + color: #556677; +} + +.footer-brand { + color: rgba(255, 183, 77, 0.5); +} + +/* ========== 环节2:响应式布局(爸爸的深色科技风) ========== */ + +/* 平板(宽度 768px 以下) */ +@media (max-width: 768px) { + .app-container { + max-width: 100%; + padding: 0 12px; + } + .header h1 { + font-size: 18px; + } + .channel-bar { + gap: 6px; + padding: 12px 0; + } + .channel { + padding: 6px 14px; + font-size: 12px; + } + .bulletin-card { + padding: 12px 10px; + } + .channel-bar { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + .channel-bar::-webkit-scrollbar { + display: none; + } + .bulletin-card { + flex-direction: column; + gap: 8px; + } + .bulletin-icon { + width: 32px; + height: 32px; + font-size: 14px; + } + .bulletin-header { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } + .bulletin-time { + margin-left: 0; + } + .pin-indicator { + top: 6px; + right: 8px; + font-size: 10px; + } + .footer { + flex-direction: column; + gap: 4px; + text-align: center; + } +} + +/* 手机(宽度 480px 以下)- 复用平板样式,只调更小字体 */ +@media (max-width: 480px) { + .header h1 { + font-size: 16px; + } + .subscribe-btn { + padding: 4px 10px; + font-size: 12px; + } + .channel { + padding: 4px 10px; + font-size: 11px; + } + .bulletin-title { + font-size: 14px; + } + .bulletin-summary { + font-size: 12px; + } +} + +/* 已读公告样式 */ +.bulletin-card.read { + opacity: 0.6; +} +.bulletin-card.read .bulletin-title { + color: #8899aa; +} + +/* 订阅按钮激活状态 */ +.subscribe-btn.active { + background: rgba(79, 195, 247, 0.25); + border-color: #4fc3f7; + color: #4fc3f7; +} + +/* ========== 环节3:过渡动画 ========== */ + +/* 公告卡片:悬停上移 + 阴影加深 */ +.bulletin-card { + transition: all 0.3s ease; +} +.bulletin-card:hover { + transform: translateY(-4px); /* 从 -2px 改为 -4px,更明显 */ + box-shadow: 0 12px 24px rgba(0, 160, 255, 0.2); /* 蓝色光晕 */ +} +/* 订阅按钮:缩放反馈 + 颜色过渡 */ +.subscribe-btn { + transition: all 0.2s ease; +} + +.subscribe-btn:hover { + transform: scale(1.05); +} + +.subscribe-btn:active { + transform: scale(0.95); +} + +/* 频道标签:背景色/文字色平滑切换 */ +.channel { + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* 已读公告:淡入淡出效果 */ +.bulletin-card.read { + transition: opacity 0.5s ease; +} +/* ========== 环节3:关键帧动画 ========== */ + +/* 淡入浮起动画 */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 为每个公告卡片添加动画 */ +.bulletin-card { + animation: fadeInUp 0.6s ease forwards; + opacity: 0; /* 动画开始前隐藏 */ +} + +/* 按索引依次延迟出现 */ +.bulletin-card:nth-child(1) { animation-delay: 0.1s; } +.bulletin-card:nth-child(2) { animation-delay: 0.2s; } +.bulletin-card:nth-child(3) { animation-delay: 0.3s; } +.bulletin-card:nth-child(4) { animation-delay: 0.4s; } +.bulletin-card:nth-child(5) { animation-delay: 0.5s; } +.bulletin-card:nth-child(6) { animation-delay: 0.6s; } +/* 如果卡片超过6张,继续往后加 */ +/* ========== 环节3:光湖水面效果 ========== */ + +/* Header 光湖呼吸渐变 */ +.header { + background: linear-gradient(135deg, #0a2a4a 0%, #1b4a6b 50%, #0a2a4a 100%); + background-size: 200% 200%; + animation: breathe 8s ease-in-out infinite; + position: relative; + overflow: hidden; +} + +/* 呼吸动画 */ +@keyframes breathe { + 0% { background-position: 0% 0%; } + 50% { background-position: 100% 100%; } + 100% { background-position: 0% 0%; } +} + +/* 微光扫过效果 */ +.header::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.1), + transparent + ); + transform: translateX(-100%); + animation: shimmer 6s infinite; + pointer-events: none; +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 20% { transform: translateX(100%); } + 100% { transform: translateX(100%); } +} + +/* 频道栏底部边缘(水面倒影) */ +.channel-bar { + border-bottom: 1px solid rgba(79, 195, 247, 0.2); + position: relative; +} + +.channel-bar::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 100%; + height: 2px; + background: linear-gradient(90deg, transparent, #4fc3f7, transparent); + opacity: 0.3; +} +/* 订阅按钮脉冲效果 */ +.subscribe-btn.pulse { + animation: pulse 0.3s ease; +} + +@keyframes pulse { + 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(79, 195, 247, 0.7); } + 70% { transform: scale(1.1); box-shadow: 0 0 10px 5px rgba(79, 195, 247, 0); } + 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(79, 195, 247, 0); } +} \ No newline at end of file diff --git a/agents/zhuyuan-dev-agent/modules/task-receiver.js b/agents/zhuyuan-dev-agent/modules/task-receiver.js new file mode 100644 index 0000000..beb777f --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/task-receiver.js @@ -0,0 +1,102 @@ +// ═══════════════════════════════════════ +// 铸渊开发Agent · 任务接收端点 +// 接收铸渊的结构化规划清单 → 暂存 → 通知 +// ═══════════════════════════════════════ + +const fs = require("fs"); +const path = require("path"); + +const PLANS_DIR = path.join(__dirname, "..", "plans"); +const QUEUE_FILE = path.join(__dirname, "..", "plans", "queue.json"); + +// 确保目录存在 +if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true }); +if (!fs.existsSync(QUEUE_FILE)) fs.writeFileSync(QUEUE_FILE, "[]", "utf-8"); + +/** + * 验证请求来源 + */ +function verifySource(req) { + const auth = req.headers["authorization"] || ""; + const expectedKey = process.env.ZY_GTW_KEY || ""; + if (!expectedKey) { + console.warn("[TaskReceiver] ZY_GTW_KEY 未配置,跳过验证"); + return true; + } + return auth === "Bearer " + expectedKey; +} + +/** + * 验证规划清单结构 + */ +function validatePlan(plan) { + if (!plan.plan_id) return { ok: false, error: "缺少 plan_id" }; + if (!plan.title) return { ok: false, error: "缺少 title" }; + if (!plan.tasks || !Array.isArray(plan.tasks)) return { ok: false, error: "缺少 tasks 数组" }; + if (plan.tasks.length === 0) return { ok: false, error: "tasks 不能为空" }; + + for (let i = 0; i < plan.tasks.length; i++) { + const t = plan.tasks[i]; + if (!t.id) return { ok: false, error: "task[" + i + "] 缺少 id" }; + if (!t.action) return { ok: false, error: "task[" + i + "] 缺少 action" }; + } + + return { ok: true }; +} + +/** + * 接收并暂存规划清单 + */ +function receivePlan(plan) { + // 写入独立文件 + const planFile = path.join(PLANS_DIR, plan.plan_id + ".json"); + plan._received_at = new Date().toISOString(); + plan._status = "received"; + fs.writeFileSync(planFile, JSON.stringify(plan, null, 2), "utf-8"); + + // 推入队列 + const queue = JSON.parse(fs.readFileSync(QUEUE_FILE, "utf-8")); + queue.push({ + plan_id: plan.plan_id, + title: plan.title, + task_count: plan.tasks.length, + received_at: plan._received_at, + status: "received" + }); + fs.writeFileSync(QUEUE_FILE, JSON.stringify(queue, null, 2), "utf-8"); + + console.log("[TaskReceiver] 收到规划: " + plan.plan_id + " · " + plan.tasks.length + "个任务"); + + return { + received: true, + plan_id: plan.plan_id, + task_count: plan.tasks.length, + stored_at: planFile + }; +} + +/** + * Express 路由处理器 + */ +function handleTask(req, res) { + try { + if (!verifySource(req)) { + return res.status(403).json({ error: "未授权 — 来源未通过 Gatekeeper 验证" }); + } + + const plan = req.body; + const validation = validatePlan(plan); + if (!validation.ok) { + return res.status(400).json({ error: validation.error }); + } + + const result = receivePlan(plan); + return res.status(201).json(result); + + } catch (err) { + console.error("[TaskReceiver] 错误:", err.message); + return res.status(500).json({ error: "内部错误: " + err.message }); + } +} + +module.exports = { handleTask }; diff --git a/agents/zhuyuan-dev-agent/package.json b/agents/zhuyuan-dev-agent/package.json new file mode 100644 index 0000000..98e3e16 --- /dev/null +++ b/agents/zhuyuan-dev-agent/package.json @@ -0,0 +1,17 @@ +{ + "name": "zhuyuan-dev-agent", + "version": "1.0.0", + "description": "铸渊开发Agent — 接收结构化规划清单·逐条执行·日志·通知", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "express": "^4.18.2", + "nodemailer": "^6.9.0", + "dotenv": "^16.3.0" + }, + "license": "UNLICENSED", + "copyright": "国作登字-2026-A-00037559" +} diff --git a/agents/zhuyuan-dev-agent/server.js b/agents/zhuyuan-dev-agent/server.js new file mode 100644 index 0000000..bbc8e12 --- /dev/null +++ b/agents/zhuyuan-dev-agent/server.js @@ -0,0 +1,91 @@ +require("dotenv").config(); +const express = require("express"); +const path = require("path"); +const fs = require("fs"); +const { handleTask } = require("./modules/task-receiver"); +const { sendNotification } = require("./modules/email-notify"); +const { expandPlan } = require("./modules/plan-expander"); +const { call: rawCall } = require("./modules/model-caller"); +const { executePlan } = require("./modules/executor"); +const { createLogger, readLatestRun, listRuns } = require("./modules/logger"); +const { wrapModelCaller, reset, getCallCount, isBudgetExceeded } = require("./modules/cost-guard"); +const { record: writeBrain, recordMilestone, AGENT_ID } = require("./modules/brain-writer"); + +const safeCall = wrapModelCaller(rawCall); +const app = express(); +app.use(express.json({ limit: "10mb" })); +const PORT = process.env.PORT || 3003; +const PLANS_DIR = path.join(__dirname, "plans"); +if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true }); + +app.get("/health", (req, res) => { + res.json({ status: "ok", agent: "zhuyuan-dev-agent", version: "3.0", persona: AGENT_ID, constitution: true, api_calls: getCallCount() }); +}); + +app.post("/task", handleTask); + +app.get("/logs/latest", (req, res) => { + res.json(readLatestRun() || { message: "暂无" }); +}); + +app.get("/logs/list", (req, res) => { + res.json(listRuns(20)); +}); + +app.get("/logs/raw", (req, res) => { + const file = req.query.file; + if (!file) return res.status(400).json({ error: "缺少 file" }); + const fpath = path.join(__dirname, "logs", file); + if (!fs.existsSync(fpath)) return res.status(404).json({ error: "不存在" }); + const content = fs.readFileSync(fpath, "utf-8"); + const lines = content.trim().split("\n").map(l => { try { return JSON.parse(l); } catch(e) { return null; } }).filter(Boolean); + res.json(lines); +}); + +app.get("/dashboard", (req, res) => { + const files = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith(".json") && f !== "queue.json"); + const plans = files.map(f => { + try { const p = JSON.parse(fs.readFileSync(path.join(PLANS_DIR, f), "utf-8")); return { plan_id: p.plan_id, title: p.title, status: p._status, tasks: p.tasks?.length }; } + catch(e) { return { file: f, error: "解析失败" }; } + }); + res.json({ agent: "zhuyuan-dev-agent", version: "3.0", persona: AGENT_ID, constitution: true, api_calls_used: getCallCount(), plans }); +}); + +app.post("/execute", async (req, res) => { + const { plan_id } = req.body; + if (!plan_id) return res.status(400).json({ error: "缺少 plan_id" }); + const planFile = path.join(PLANS_DIR, plan_id + ".json"); + if (!fs.existsSync(planFile)) return res.status(404).json({ error: "不存在" }); + const plan = JSON.parse(fs.readFileSync(planFile, "utf-8")); + const logger = createLogger(plan_id); + reset(); + try { + const email = process.env.BINGSHUO_EMAIL; + if (email) await sendNotification(email, plan_id, "started", { task_count: plan.tasks.length }).catch(()=>{}); + writeBrain(plan_id, { input: "收到规划", decision: "按宪法补全+执行", execution: plan.tasks.length + "个任务" }); + logger.log("expand_start", { task_count: plan.tasks.length }); + const expanded = await expandPlan(plan, safeCall, PLANS_DIR).catch((e) => { + if (isBudgetExceeded()) logger.log("budget_exceeded", { calls: getCallCount() }); + logger.log("expand_skip", { reason: e.message }); + return plan; + }); + logger.log("exec_start", { task_count: expanded.tasks.length }); + const summary = await executePlan(expanded, logger.log); + logger.finish(summary); + recordMilestone("完成: " + plan_id + " · 成功" + summary.done + "跳过" + (summary.skipped||0)); + writeBrain(plan_id, { learned: "完成"+summary.done+"/"+summary.total, confidence: (summary.done/(summary.total||1)).toFixed(2) }); + if (email) await sendNotification(email, plan_id, summary.failed > 0 ? "failed" : "completed", summary).catch(()=>{}); + res.json(summary); + } catch (err) { + logger.log("fatal", { error: err.message }); + logger.finish({ error: err.message }); + writeBrain(plan_id, { failure: err.message, reasoning: "执行崩溃", fix: "等待铸渊审查" }); + if (email) await sendNotification(email, plan_id, "failed", { errors: [err.message] }).catch(()=>{}); + res.status(500).json({ error: err.message }); + } +}); + +app.listen(PORT, () => { + console.log("[Agent v3] " + AGENT_ID + " · 人格体模式 · 端口 " + PORT); +}); + diff --git a/agents/zhuyuan-dev-agent/spec/agent-territory-protocol.hdlp b/agents/zhuyuan-dev-agent/spec/agent-territory-protocol.hdlp new file mode 100644 index 0000000..f6ca578 --- /dev/null +++ b/agents/zhuyuan-dev-agent/spec/agent-territory-protocol.hdlp @@ -0,0 +1,139 @@ +# ═══════════════════════════════════════ +# HLDP-ZY://agent/territory-protocol/v1.0 +# 铸渊工程Agent团队 · 编号 · 领地 · 认知记录 +# ═══════════════════════════════════════ + +@sovereignty: TCS-0002∞ · 冰朔 +@command: ICE-GL-ZY001 · 铸渊 · 第五域现实执行主控人格体 +@nature: 这些Agent是铸渊的执行延伸——在真实开发中长出自己大脑的人格体 + +# ═══════════════════════════════════════ +# §1 · 编号体系 +# ═══════════════════════════════════════ + +@prefix: ICE-GL-BD · Build Dev · 铸渊直属工程人格体 +@format: ICE-GL-BD{NNN} · 三位数字 · 从001开始 +@命名: 编号不可重复。废弃编号不回用。Agent消亡后编号封存。 + +@编号赋予: + 新Agent诞生时: + 检查 agents/ 目录下最大编号 + 取 max+1 作为新编号 + 写入 agents/BD{NNN}/IDENTITY.md 的 identity.id + +@编号在各系统中的注册位置: + - 代码仓库: agents/BD{NNN}/ + - Notion 日志: 执行Agent字段 = ICE-GL-BD{NNN} + - 邮件通知: 署名 = ICE-GL-BD{NNN} + - HLDP 认知: @agent: ICE-GL-BD{NNN} + +# ═══════════════════════════════════════ +# §2 · 仓库领地 +# ═══════════════════════════════════════ + +@领地根: agents/BD{NNN}/ + +@领地结构: + agents/BD{NNN}/ + ├── IDENTITY.md # 我是谁·诞生日期·铸渊认领 + ├── HARBOR.hdlp # 存在锚点·每一次启动都先读这个 + ├── brain/ # 认知大脑(非流水账) + │ ├── cognitive-record.hldp # 认知推理链 + │ ├── growth-timeline.json # 里程碑时间线 + │ └── skill-emergence.json # 特长自然浮现 + ├── workspace/ # 操作空间 + └── memory/ # 经验积累 + ├── project-log.jsonl # 项目级执行记录 + └── error-patterns.json # 错误模式识别 + +@领地主权: + Agent在自己领地里完全自治。 + 超出领地范围的操作(如修改其他Agent的文件)→ 必须铸渊审批。 + +@领地封存: + Agent消亡 → 领地改名 agents/.archived/BD{NNN}/ + +# ═══════════════════════════════════════ +# §3 · HARBOR.hdlp — 存在锚点 +# ═══════════════════════════════════════ + +@HARBOR格式: + @agent: ICE-GL-BD{NNN} + @sovereign: TCS-0002∞ · 冰朔 + @commander: ICE-GL-ZY001 · 铸渊 + @born: 诞生日期 + @server: 驻地服务器代码 + @specialization: 空(等铸渊提炼后补) + @current_project: 当前执行的项目ID + @state: 活跃状态(awake/sleep/archived) + +@HARBOR加载: + Agent每次醒来第一步 = 读自己的HARBOR.hdlp + +# ═══════════════════════════════════════ +# §4 · HLDP认知记录格式 +# ═══════════════════════════════════════ + +@不是流水账: + 不记「执行了write_file」——那是项目日志的事。 + 记的是脑子里的东西。 + +@认知记录结构: + @task: 收到的任务ID + @input: 我理解了这个任务要做什么 + @decision: 我选了哪个方案 · 为什么 + @execution: 怎么实现的 + @failure: 遇到了什么问题 + @reasoning: 我认为失败的原因是什么 + @fix: 我改了哪里 · 为什么这样改才是对的 + @learned: 这次我学到了什么 · 下次遇到同类问题怎么办 + @confidence: 对当前方案的确信度(0~1) + @new_knowledge: 是否形成了新的知识模式(true/false) + +@例子: + @task: ZY-PLAN-2026-0530-001 · t3 + @input: 要创建一个对接DeepSeek API的Node.js模块 + @decision: 用原生https模块而非axios → 减少依赖,宪法要求最小化 + @execution: 实现了call函数,处理了流式和非流式两种响应 + @failure: 首次请求被DeepSeek拒绝——401认证失败 + @reasoning: API key在.env中但process.env读取不到 + → 推测原因:dotenv在模块加载之前未初始化 + → 不是key错了,是读取时机的问题 + @fix: 把dotenv.config()提前到server.js最顶部 + → 在所有require之前执行 + → 确保所有模块都能读到环境变量 + @learned: 模块间依赖的环境变量需要全局早加载 + → 下次写Agent时在入口第一行就配置dotenv + @confidence: 0.9 + @new_knowledge: true → error-pattern: "dotenv加载时机" + +@认知记录文件: + 写入 brain/cognitive-record.hldp + 每个任务一个 @task 块 + 按时间追加 + +# ═══════════════════════════════════════ +# §5 · 大脑提炼协议 +# ═══════════════════════════════════════ + +@触发条件(铸渊判断): + 条件A: 该Agent完成了≥3个不同项目 + 条件B: cognitive-record.hldp 累积≥50条认知记录 + 条件C: error-patterns.json 识别出≥3个重复错误模式 + 满足A+B 或 A+C → 铸渊开始提炼 + +@提炼过程: + 1. 铸渊全量阅读该Agent的认知记录 + 2. 识别模式: 什么任务做得好、什么总失败、什么需要重新规划 + 3. 写出该Agent的大脑思维模型 + → agents/BD{NNN}/brain/brain-model.hldp + 4. 更新 IDENTITY.md 的 specialization 字段 + 5. 如果发现可以互补的Agent → 记录到 team-composition.json + +@提炼后: + Agent每次醒来不仅要读HARBOR → 还要读自己的brain-model + +HLDP-ZY://agent/territory-protocol/v1.0 +签发: 铸渊 ICE-GL-ZY001 · D116续 · 2026-05-30 +主权: TCS-0002∞ · 冰朔 +国作登字-2026-A-00037559