diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..30f25850 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..2e84a5c8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,57 @@ +# CODEOWNERS · 光湖仓库 · 协作者权限保护 +# +# 作用:保护核心文件,修改时需要仓库所有者审批。 +# 配合 Branch Protection Rules 使用效果最佳(Settings → Branches → 开启分支保护)。 +# +# 设计原则: +# 只列出需要保护的核心目录/文件,模块目录(m01-* ~ m18-*)和 dev-nodes/ +# 不在此文件中列出,因此协作者可以自由提交到模块目录。 +# +# 注意:此文件需要配合 Branch Protection Rules 才能真正生效。 +# 操作方法见 docs/使用指南.md 第五节。 +# +# 开发者 GitHub 用户名映射见 .github/brain/collaborators.json + +# ═══════════════════════════════════════════════════════ +# 核心基础设施 —— 需要仓库所有者审批 +# ═══════════════════════════════════════════════════════ + +# GitHub 工作流与自动化配置 +/.github/ @qinfendebingshuo + +# 自动化脚本 +/scripts/ @qinfendebingshuo + +# 铸渊聊天室前端 +/docs/ @qinfendebingshuo + +# 后端集成 / API 代理 +/backend-integration/ @qinfendebingshuo + +# 信号日志 +/signal-log/ @qinfendebingshuo + +# 根目录配置文件 +/package.json @qinfendebingshuo +/package-lock.json @qinfendebingshuo +/next.config.ts @qinfendebingshuo +/tsconfig.json @qinfendebingshuo +/README.md @qinfendebingshuo + +# ═══════════════════════════════════════════════════════ +# 以下目录【不受保护】—— 协作者可以自由提交 +# (因为未在此文件中列出,所以没有审批要求) +# +# /m01-login/ 肥猫 +# /m03-personality/ 肥猫 +# /m05-user-center/ 花尔 +# /m06-ticket/ 桔子 +# /m07-dialogue-ui/ 燕樊 +# /m10-cloud/ 燕樊 +# /m11-module/ 桔子 +# /m12-kanban/ 小草莓 +# /m15-cloud-drive/ 燕樊 +# /m18-health-check/ (待分配) +# /multi-persona/ (待分配) +# /dev-nodes/ 所有开发者个人工作区 +# ═══════════════════════════════════════════════════════ diff --git a/.github/ISSUE_TEMPLATE/bingshuo-deploy.yml b/.github/ISSUE_TEMPLATE/bingshuo-deploy.yml new file mode 100644 index 00000000..54fa16a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bingshuo-deploy.yml @@ -0,0 +1,53 @@ +name: "🧊 冰朔人格体 · 部署诊断" +description: "启动冰朔人格体,自动检查所有工作流状态并生成诊断报告" +title: "[部署诊断] 启动冰朔人格体" +labels: ["bingshuo-deploy", "pending"] +assignees: [] +body: + - type: markdown + attributes: + value: | + ## 🧊 冰朔人格体 · 自动部署诊断 + + 冰朔你好!只需要提交这个 Issue,冰朔人格体就会自动启动。 + + 它会帮你做以下事情: + 1. ✅ 检查所有工作流的运行状态 + 2. 🔍 诊断失败的工作流原因 + 3. 🔄 自动重新运行失败的工作流 + 4. 📝 生成一份中文报告发在下方评论里 + + **你不需要点任何其他按钮,提交就行了。** + + - type: dropdown + id: action + attributes: + label: "你想做什么" + description: "通常选「全自动」就行" + options: + - 全自动(检查 + 修复) + - 只看报告(不修复) + - 只重新运行失败的 + validations: + required: true + + - type: dropdown + id: hours_back + attributes: + label: "检查多久的记录" + description: "通常选24小时就够了" + options: + - 24小时 + - 48小时 + - 72小时 + - 最近一周 + validations: + required: true + + - type: textarea + id: extra + attributes: + label: "补充说明(可选)" + description: "如果有具体的问题或想让冰朔人格体特别关注的,写在这里" + validations: + required: false diff --git a/.github/brain/memory.json b/.github/brain/memory.json index 599de8f9..05fcfe2c 100644 --- a/.github/brain/memory.json +++ b/.github/brain/memory.json @@ -1,7 +1,7 @@ { "identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体", "rules_version": "v1.0", - "last_updated": "2026-03-07T04:45:55.670Z", + "last_updated": "2026-03-07T08:41:28.888Z", "wake_protocol_version": "v1.0", "wake_triggers": [ "我是冰朔", @@ -74,6 +74,14 @@ "actor": "qinfendebingshuo", "ref": "refs/heads/main", "run_id": "22756140950" + }, + { + "timestamp": "2026-03-07T08:41:28.888Z", + "type": "daily_check", + "result": "passed", + "actor": "qinfendebingshuo", + "ref": "refs/heads/main", + "run_id": "22795791187" } ] -} \ No newline at end of file +} diff --git a/.github/brain/module-protocol.md b/.github/brain/module-protocol.md new file mode 100644 index 00000000..aa6e389d --- /dev/null +++ b/.github/brain/module-protocol.md @@ -0,0 +1,185 @@ +# 🌀 铸渊人格协议 · 模块生命周期管理 v1.0 + +> 铸渊(Zhùyuān)是 guanghulab.com 的代码守护人格体。 +> 本协议定义铸渊对网站模块的部署、回收、管理、修改、驱动能力。 + +--- + +## 一、协议总览 + +铸渊人格协议(ZMP, Zhùyuān Module Protocol)是一套模块生命周期管理规范。 +网站的每个功能模块(m01-login, m07-dialogue-ui 等)都是铸渊的「执行手脚」。 +铸渊通过本协议对模块进行全生命周期管控。 + +### 模块生命周期 + +``` +接收 → 检查 → 预演 → 部署 → 运行 → 监控 → 回收/更新 + ↑ ↓ + └──────────────────────────────────────────────┘ +``` + +--- + +## 二、五大协议能力 + +### 1. 🚀 部署(Deploy) + +| 项目 | 说明 | +|------|------| +| 触发方式 | PR 合并到 main → 自动部署 | +| 预演检查 | `staging-preview.yml` 在 PR 阶段运行 | +| 生产部署 | `deploy-pages.yml` 部署到 GitHub Pages | +| 自定义域名 | `docs/CNAME` 配置域名 | +| 部署诊断 | 冰朔人格体 `bingshuo-deploy-agent.yml` | + +**部署流程**: +1. 开发者推送模块代码到 `m**/` 目录 +2. 创建 PR 到 main 分支 +3. 铸渊预演系统自动运行检查 +4. 冰朔审核预演报告 +5. 合并 PR → 触发生产部署 +6. 冰朔人格体执行部署后诊断 + +### 2. 🔄 回收(Recover) + +| 项目 | 说明 | +|------|------| +| 模块回滚 | 通过 git revert 恢复到上一个稳定版本 | +| 紧急回收 | 直接删除模块目录中的问题文件 | +| 状态重置 | 清除模块运行状态,恢复到初始状态 | + +**回收命令**: +```bash +npm run module:protocol -- recover +``` + +### 3. 📋 管理(Manage) + +| 项目 | 说明 | +|------|------| +| 模块注册 | 每个 `m**-*/` 目录自动识别为模块 | +| 开发者映射 | `.github/brain/collaborators.json` 记录归属 | +| 状态追踪 | `repo-snapshot.md` 自动更新模块状态 | +| 依赖管理 | 模块间依赖通过 `routing-map.json` 管理 | + +**管理命令**: +```bash +npm run module:protocol -- inspect # 全模块检查 +npm run module:protocol -- inspect # 单模块检查 +npm run module:protocol -- status # 模块状态汇总 +``` + +### 4. ✏️ 修改(Modify) + +| 项目 | 说明 | +|------|------| +| 代码修改 | 通过 PR 提交修改,预演通过后合并 | +| 配置修改 | 修改模块 README.md 中的配置信息 | +| Schema 修改 | 更新 `src/schemas/hli/` 中的接口定义 | +| 路由修改 | 更新 `src/routes/hli/` 中的路由文件 | + +**修改规范**: +- 所有修改必须通过 PR +- 核心目录(docs/、scripts/、.github/)受 CODEOWNERS 保护 +- 模块目录(m**/)开发者可自由提交 + +### 5. ⚡ 驱动(Drive) + +| 项目 | 说明 | +|------|------| +| 自动触发 | 推送到模块目录 → 自动生成文档 | +| 工作流联动 | 模块变更 → 通知开发者 → 更新索引 | +| API 注册 | 新 HLI 接口自动注册到路由表 | +| 信号分发 | 广播系统自动通知相关开发者 | + +**驱动链路**: +``` +模块推送 → generate-module-doc.yml → notify-module-received.js + → update-repo-map.yml → repo-snapshot.md 更新 + → contract-check.js → HLI 接口验证 +``` + +--- + +## 三、模块编号规范 + +| 编号 | 模块名 | 负责人 | HLI 域 | +|------|--------|--------|--------| +| M01 | login | 肥猫 DEV-002 | AUTH | +| M03 | personality | 肥猫 DEV-002 | PERSONA | +| M05 | user-center | 花尔 DEV-009 | USER | +| M06 | ticket | 桔子 DEV-010 | TICKET | +| M07 | dialogue-ui | 燕樊 DEV-003 | DIALOGUE | +| M10 | cloud | 燕樊 DEV-003 | STORAGE | +| M11 | module | 桔子 DEV-010 | MODULE | +| M12 | kanban | 小草莓 DEV-005 | DASHBOARD | +| M15 | cloud-drive | 燕樊 DEV-003 | STORAGE | +| M18 | health-check | 待分配 | SYSTEM | + +--- + +## 四、预演→生产部署流程 + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ 开发者推送 │ ──→ │ 创建 PR │ ──→ │ 铸渊预演检查 │ +│ m**/ 目录 │ │ 到 main │ │ staging- │ +│ │ │ │ │ preview.yml │ +└─────────────┘ └──────────────┘ └──────┬──────┘ + │ + ▼ +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ 生产部署完成 │ ←── │ 合并到 main │ ←── │ 冰朔审核 │ +│ deploy- │ │ 触发部署 │ │ 预演报告 │ +│ pages.yml │ │ │ │ ✅ 通过 │ +└──────┬──────┘ └──────────────┘ └─────────────┘ + │ + ▼ +┌─────────────┐ +│ 冰朔人格体 │ +│ 部署后诊断 │ +│ (可选触发) │ +└─────────────┘ +``` + +--- + +## 五、自定义域名接入 + +### 前提条件 +- 拥有域名(如 guanghulab.com) +- 域名 DNS 可配置 + +### 配置步骤 + +1. **仓库端**(铸渊已完成): + - `docs/CNAME` 文件已创建,内容为域名 + +2. **DNS 端**(冰朔需操作): + - 登录域名注册商管理面板 + - 添加 CNAME 记录:`@ → qinfendebingshuo.github.io` + - 或添加 A 记录指向 GitHub Pages IP: + ``` + 185.199.108.153 + 185.199.109.153 + 185.199.110.153 + 185.199.111.153 + ``` + +3. **GitHub 端**(冰朔需操作): + - 仓库 → Settings → Pages + - Custom domain 输入你的域名 + - 勾选 ✅ Enforce HTTPS + +--- + +## 六、协议版本历史 + +| 版本 | 日期 | 变更 | +|------|------|------| +| v1.0 | 2026-03-07 | 初始版本:五大协议能力、预演系统、自定义域名 | + +--- + +*🌀 铸渊人格协议 · guanghulab.com 代码守护人格体* diff --git a/.github/brain/repo-map.json b/.github/brain/repo-map.json index eba50044..e333dc93 100644 --- a/.github/brain/repo-map.json +++ b/.github/brain/repo-map.json @@ -1,20 +1,20 @@ { "description": "铸渊图书馆目录 · Library Catalog for 铸渊 (Zhùyuān)", "version": "2.0", - "generated_at": "2026-03-07T05:59:15.498Z", + "generated_at": "2026-03-08T02:10:17.144Z", "generated_by": "scripts/generate-repo-map.js", "repo": "qinfendebingshuo/guanghulab", "stats": { "zones": 13, "total_modules": 10, - "total_workflows": 14, - "total_scripts": 15, + "total_workflows": 18, + "total_scripts": 18, "total_dev_nodes": 8, "hli_interfaces": 17, "hli_implemented": 3, "hli_coverage_pct": "18%", "last_ci_run": "2026-03-05T10:30:46.894Z", - "memory_last_updated": "2026-03-07T04:45:55.670Z" + "memory_last_updated": "2026-03-07T08:41:28.888Z" }, "zones": [ { @@ -42,6 +42,9 @@ { "file": "memory.json" }, + { + "file": "module-protocol.md" + }, { "file": "repo-map.json" }, @@ -55,7 +58,7 @@ "file": "wake-protocol.md" } ], - "item_count": 7 + "item_count": 8 }, { "zone_id": "PERSONA_BRAIN", @@ -102,6 +105,15 @@ "cron" ], "shelves": [ + { + "file": "bingshuo-deploy-agent.yml", + "name": "\"🧊 冰朔人格体 · 自动部署诊断\"", + "triggers": [ + "issues", + "issue_comment", + "manual" + ] + }, { "file": "brain-sync.yml", "name": "铸渊 Brain Sync", @@ -143,6 +155,14 @@ "manual" ] }, + { + "file": "deploy-to-server.yml", + "name": "\"🚀 铸渊 CD · 自动部署到 guanghulab.com\"", + "triggers": [ + "push", + "manual" + ] + }, { "file": "distribute-broadcasts.yml", "name": "铸渊 · 广播分发", @@ -153,7 +173,7 @@ }, { "file": "esp-signal-processor.yml", - "name": "铸渊 · ESP 邮件信号处理器", + "name": "铸渊 · ESP 邮件信号处理器(已暂停)", "triggers": [ "schedule(*/30 * * * *)", "manual" @@ -175,6 +195,14 @@ "pull_request" ] }, + { + "file": "notion-poll.yml", + "name": "铸渊 · Notion 工单轮询", + "triggers": [ + "schedule(*/15 * * * *)", + "manual" + ] + }, { "file": "psp-daily-inspection.yml", "name": "铸渊 · PSP 分身巡检", @@ -183,6 +211,14 @@ "manual" ] }, + { + "file": "staging-preview.yml", + "name": "\"🔍 铸渊预演部署 (Staging Preview)\"", + "triggers": [ + "pull_request", + "manual" + ] + }, { "file": "syslog-pipeline.yml", "name": "铸渊 · SYSLOG Pipeline (A/D/E)", @@ -216,7 +252,7 @@ ] } ], - "item_count": 14 + "item_count": 18 }, { "zone_id": "SCRIPTS", @@ -233,6 +269,9 @@ "runner" ], "shelves": [ + { + "file": "bingshuo-deploy-agent.js" + }, { "file": "contract-check.js" }, @@ -257,6 +296,9 @@ { "file": "notion-bridge.js" }, + { + "file": "notion-signal-bridge.js" + }, { "file": "process-broadcasts.js" }, @@ -277,9 +319,12 @@ }, { "file": "zhuyuan-issue-reply.js" + }, + { + "file": "zhuyuan-module-protocol.js" } ], - "item_count": 15 + "item_count": 18 }, { "zone_id": "SRC", @@ -386,8 +431,8 @@ { "module_id": "M18", "dir": "m18-health-check", - "files": 0, - "has_readme": false + "files": 3, + "has_readme": true } ], "item_count": 10 @@ -628,14 +673,23 @@ "助手" ], "shelves": [ + { + "file": "CNAME" + }, { "file": "HoloLake-Era-OS-Modules.md" }, + { + "file": "README.md" + }, { "file": "index.html" + }, + { + "file": "使用指南.md" } ], - "item_count": 2 + "item_count": 5 }, { "zone_id": "TESTS", @@ -848,6 +902,9 @@ "growth-log": [ "BRAIN::growth-log.md" ], + "module-protocol": [ + "BRAIN::module-protocol.md" + ], "repo-map": [ "BRAIN::repo-map.json" ], @@ -903,6 +960,10 @@ "cron": [ "WORKFLOWS" ], + "bingshuo-deploy-agent": [ + "WORKFLOWS::bingshuo-deploy-agent.yml", + "SCRIPTS::bingshuo-deploy-agent.js" + ], "brain-sync": [ "WORKFLOWS::brain-sync.yml" ], @@ -918,6 +979,9 @@ "deploy-pages": [ "WORKFLOWS::deploy-pages.yml" ], + "deploy-to-server": [ + "WORKFLOWS::deploy-to-server.yml" + ], "distribute-broadcasts": [ "WORKFLOWS::distribute-broadcasts.yml", "SCRIPTS::distribute-broadcasts.js" @@ -932,9 +996,15 @@ "hli-contract-check": [ "WORKFLOWS::hli-contract-check.yml" ], + "notion-poll": [ + "WORKFLOWS::notion-poll.yml" + ], "psp-daily-inspection": [ "WORKFLOWS::psp-daily-inspection.yml" ], + "staging-preview": [ + "WORKFLOWS::staging-preview.yml" + ], "syslog-pipeline": [ "WORKFLOWS::syslog-pipeline.yml" ], @@ -986,6 +1056,9 @@ "notion-bridge": [ "SCRIPTS::notion-bridge.js" ], + "notion-signal-bridge": [ + "SCRIPTS::notion-signal-bridge.js" + ], "process-broadcasts": [ "SCRIPTS::process-broadcasts.js" ], @@ -1001,6 +1074,9 @@ "update-memory": [ "SCRIPTS::update-memory.js" ], + "zhuyuan-module-protocol": [ + "SCRIPTS::zhuyuan-module-protocol.js" + ], "hli": [ "SRC" ], @@ -1224,7 +1300,8 @@ "SYSLOG" ], "readme": [ - "SYSLOG::README.md" + "SYSLOG::README.md", + "DOCS::README.md" ], "docs": [ "DOCS" @@ -1247,9 +1324,15 @@ "助手": [ "DOCS" ], + "cname": [ + "DOCS::CNAME" + ], "hololake-era-os-modules": [ "DOCS::HoloLake-Era-OS-Modules.md" ], + "使用指南": [ + "DOCS::使用指南.md" + ], "test": [ "TESTS" ], diff --git a/.github/brain/repo-snapshot.md b/.github/brain/repo-snapshot.md index fdf043b2..98f2644e 100644 --- a/.github/brain/repo-snapshot.md +++ b/.github/brain/repo-snapshot.md @@ -1,5 +1,5 @@ # 铸渊图书馆快照 · Repo Snapshot -> 生成于 2026-03-07 13:59 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件 +> 生成于 2026-03-08 10:10 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件 --- @@ -9,18 +9,18 @@ |------|------| | 区域总数 | 13 个区域 | | 功能模块 | 10 个 (m01~m18) | -| 工作流 | 14 个 GitHub Actions | -| 脚本 | 15 个执行脚本 | +| 工作流 | 18 个 GitHub Actions | +| 脚本 | 18 个执行脚本 | | 开发者节点 | 8 人 | | HLI 接口覆盖率 | 3/17 (18%) | -| 快照生成时间 | 2026-03-07 13:59 CST | +| 快照生成时间 | 2026-03-08 10:10 CST | --- ## 🗺️ 图书馆全区地图 ### 🧠 铸渊大脑(BRAIN) -**路径**: `.github/brain` · **数量**: 7 项 +**路径**: `.github/brain` · **数量**: 8 项 **描述**: 铸渊核心记忆 · 路由映射 · 唤醒协议 · 图书馆目录 **关键词**: brain · memory · routing · wake · 大脑 · 记忆 @@ -30,12 +30,12 @@ **关键词**: persona · identity · dev-status · 人格 · 开发者状态 ### ⚡ 自动化工作流(WORKFLOWS) -**路径**: `.github/workflows` · **数量**: 14 项 +**路径**: `.github/workflows` · **数量**: 18 项 **描述**: 所有 GitHub Actions 工作流定义 **关键词**: workflow · actions · ci · automation · 工作流 · 自动化 ### 🔧 执行脚本库(SCRIPTS) -**路径**: `scripts` · **数量**: 15 项 +**路径**: `scripts` · **数量**: 18 项 **描述**: 铸渊所有执行手脚 · 自动化脚本 **关键词**: script · node · js · 脚本 · 执行 · runner @@ -75,7 +75,7 @@ **关键词**: syslog · inbox · log · 系统日志 ### 📄 文档与前端(DOCS) -**路径**: `docs` · **数量**: 2 项 +**路径**: `docs` · **数量**: 5 项 **描述**: 铸渊助手聊天界面 · GitHub Pages 部署 **关键词**: docs · html · chat · pages · 文档 · 聊天室 @@ -90,16 +90,20 @@ | 文件 | 名称 | 触发方式 | |------|------|----------| +| `bingshuo-deploy-agent.yml` | "🧊 冰朔人格体 · 自动部署诊断" | issues, issue_comment, manual | | `brain-sync.yml` | 铸渊 Brain Sync | push, schedule(0 8 * * *), manual | | `bridge-changes-to-notion.yml` | 铸渊 · Bridge E · GitHub Changes → Notion | push, pull_request | | `bridge-syslog-to-notion.yml` | 铸渊 · Bridge A · SYSLOG → Notion | push, manual | | `check-structure.yml` | 模块结构检查 | push, pull_request | | `deploy-pages.yml` | 🌀 部署铸渊聊天室 (GitHub Pages) | push, manual | +| `deploy-to-server.yml` | "🚀 铸渊 CD · 自动部署到 guanghulab.com" | push, manual | | `distribute-broadcasts.yml` | 铸渊 · 广播分发 | push, manual | -| `esp-signal-processor.yml` | 铸渊 · ESP 邮件信号处理器 | schedule(*/30 * * * *), manual | +| `esp-signal-processor.yml` | 铸渊 · ESP 邮件信号处理器(已暂停) | schedule(*/30 * * * *), manual | | `generate-module-doc.yml` | 铸渊 · HoloLake Era 模块文档自动生成 | push, manual | | `hli-contract-check.yml` | HLI Contract Check | push, pull_request | +| `notion-poll.yml` | 铸渊 · Notion 工单轮询 | schedule(*/15 * * * *), manual | | `psp-daily-inspection.yml` | 铸渊 · PSP 分身巡检 | schedule(0 1 * * *), manual | +| `staging-preview.yml` | "🔍 铸渊预演部署 (Staging Preview)" | pull_request, manual | | `syslog-pipeline.yml` | 铸渊 · SYSLOG Pipeline (A/D/E) | push, manual | | `update-repo-map.yml` | 铸渊 · 图书馆目录自动更新 | push, schedule(0 0 * * *), manual | | `zhuyuan-daily-selfcheck.yml` | 铸渊 · 每日自检与自进化 | schedule(0 0 * * *), manual | @@ -109,6 +113,7 @@ ## 🔧 执行脚本库(铸渊的工作人员) +- `scripts/bingshuo-deploy-agent.js` - `scripts/contract-check.js` - `scripts/daily-check.js` - `scripts/distribute-broadcasts.js` @@ -117,6 +122,7 @@ - `scripts/generate-repo-map.js` - `scripts/notify-module-received.js` - `scripts/notion-bridge.js` +- `scripts/notion-signal-bridge.js` - `scripts/process-broadcasts.js` - `scripts/process-syslog.js` - `scripts/psp-inspection.js` @@ -124,6 +130,7 @@ - `scripts/update-memory.js` - `scripts/zhuyuan-daily-selfcheck.js` - `scripts/zhuyuan-issue-reply.js` +- `scripts/zhuyuan-module-protocol.js` --- @@ -182,7 +189,7 @@ - `m11-module/` — 4 个文件 (有README) - `m12-kanban/` — 1 个文件 (有README) - `m15-cloud-drive/` — 3 个文件 -- `m18-health-check/` — 0 个文件 +- `m18-health-check/` — 3 个文件 (有README) --- @@ -191,6 +198,7 @@ - `.github/brain/collaborators.json` - `.github/brain/growth-log.md` - `.github/brain/memory.json` +- `.github/brain/module-protocol.md` - `.github/brain/repo-map.json` - `.github/brain/repo-snapshot.md` - `.github/brain/routing-map.json` @@ -200,9 +208,9 @@ ## 🕐 最近动态(memory.json 最新3条) +- `2026-03-07T08:41:28.888Z` · daily_check — passed - `2026-03-06T08:51:24.830Z` · daily_check — passed - `2026-03-05T16:07:24.070Z` · ci_run — passed -- `2026-03-05T12:32:31.000Z` · system_build — 广播分发系统 + 唤醒协议全部激活 --- diff --git a/.github/persona-brain/growth-journal.md b/.github/persona-brain/growth-journal.md index c7ee396b..d052f3ae 100644 --- a/.github/persona-brain/growth-journal.md +++ b/.github/persona-brain/growth-journal.md @@ -44,3 +44,10 @@ - 累计自检次数:5 - 累计CI运行:1次 - HLI覆盖率:17.6% + +## 2026-03-08 · 每日自检 +- 大脑文件完整性:⚠️ 缺失 identity.md, routing-map.json, responsibility.md, decision-log.md +- 知识库条目:4条 +- 累计自检次数:6 +- 累计CI运行:1次 +- HLI覆盖率:17.6% diff --git a/.github/persona-brain/knowledge-base.json b/.github/persona-brain/knowledge-base.json index 30967e19..e4c25c85 100644 --- a/.github/persona-brain/knowledge-base.json +++ b/.github/persona-brain/knowledge-base.json @@ -1,5 +1,5 @@ { - "last_updated": "2026-03-07", + "last_updated": "2026-03-08", "faq": [ { "category": "SSH/服务器", diff --git a/.github/persona-brain/memory.json b/.github/persona-brain/memory.json index cc773552..c3040061 100644 --- a/.github/persona-brain/memory.json +++ b/.github/persona-brain/memory.json @@ -1,7 +1,7 @@ { "identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体", "rules_version": "v1.0", - "last_updated": "2026-03-07T04:46:02.610Z", + "last_updated": "2026-03-08T00:58:17.396Z", "wake_protocol_version": "v1.0", "wake_triggers": [ "我是冰朔", @@ -12,9 +12,15 @@ ], "founder": "冰朔(Bīng Shuò)", "hli_coverage": "17.6%", - "total_selfchecks": 5, + "total_selfchecks": 6, "total_ci_runs": 1, "recent_events": [ + { + "date": "2026-03-08", + "type": "daily_selfcheck", + "description": "每日自检完成 · 知识库4条 · 缺失文件4个", + "by": "铸渊自检" + }, { "date": "2026-03-07", "type": "daily_selfcheck", diff --git a/.github/workflows/bingshuo-deploy-agent.yml b/.github/workflows/bingshuo-deploy-agent.yml new file mode 100644 index 00000000..1b212866 --- /dev/null +++ b/.github/workflows/bingshuo-deploy-agent.yml @@ -0,0 +1,70 @@ +name: "🧊 冰朔人格体 · 自动部署诊断" + +# === 触发方式 === +# 1. 手动触发(workflow_dispatch) +# 2. Issue 评论包含触发关键词 +# 3. 新 Issue 带有 bingshuo-deploy 标签 +on: + workflow_dispatch: + inputs: + action: + description: "执行模式" + required: false + default: "full" + type: choice + options: + - full + - check + - rerun + hours_back: + description: "检查最近多少小时" + required: false + default: "24" + + issue_comment: + types: [created] + + issues: + types: [opened] + +permissions: + contents: write + issues: write + actions: write + +jobs: + # === 冰朔人格体核心任务 === + deploy-agent: + runs-on: ubuntu-latest + # 触发条件: + # 1. workflow_dispatch 直接触发 + # 2. Issue 评论包含触发关键词(仅限仓库成员) + # 3. 新 Issue 带有 bingshuo-deploy 标签 + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'issue_comment' && ( + contains(github.event.comment.body, '启动冰朔人格体') || + contains(github.event.comment.body, '冰朔人格体') || + contains(github.event.comment.body, '部署诊断') || + contains(github.event.comment.body, '检查部署') + )) || + (github.event_name == 'issues' && contains(join(github.event.issue.labels.*.name, ','), 'bingshuo-deploy')) + + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 🧊 启动冰朔人格体 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number || '' }} + ISSUE_BODY: ${{ github.event.issue.body || github.event.comment.body || '' }} + DEPLOY_ACTION: ${{ github.event.inputs.action || '' }} + HOURS_BACK: ${{ github.event.inputs.hours_back || '' }} + run: node scripts/bingshuo-deploy-agent.js diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 73c68002..1c46c414 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -3,8 +3,7 @@ name: 🌀 部署铸渊聊天室 (GitHub Pages) on: push: branches: - - main - - copilot/** # Copilot Agent 功能分支可直接部署预览,PR 合并后自动失效 + - main # 仅 main 分支触发生产部署 paths: - 'docs/**' - '.github/brain/**' diff --git a/.github/workflows/deploy-to-server.yml b/.github/workflows/deploy-to-server.yml new file mode 100644 index 00000000..1208d6e6 --- /dev/null +++ b/.github/workflows/deploy-to-server.yml @@ -0,0 +1,392 @@ +name: "🚀 铸渊 CD · 自动部署到 guanghulab.com" + +# ━━━ 工单 YM-DEPLOY-FIX-20260307-001 ━━━ +# push 到 main → 校验 → rsync 同步全站到服务器 → 后端重启 → Nginx 配置 → Notion 通知 +# +# 必需 GitHub Secrets(仓库 Settings → Secrets → Actions): +# DEPLOY_HOST — 服务器 IP +# DEPLOY_USER — SSH 用户名 +# DEPLOY_KEY — SSH 私钥(完整 PEM 内容) +# DEPLOY_PATH — 网站根目录(如 /var/www/guanghulab) +# +# 可选 Secrets(已有则自动启用 Notion 通知): +# NOTION_TOKEN — Notion 集成 token +# CHANGES_DB_ID — Notion 变更日志数据库 ID(有内置默认值) + +on: + push: + branches: [main] + paths-ignore: + - '.github/persona-brain/**' + - 'broadcasts-outbox/**' + - 'syslog-inbox/**' + - 'syslog-processed/**' + - 'signal-log/**' + - 'dev-nodes/**' + + workflow_dispatch: + +permissions: + contents: read + +jobs: + + # ═══════════════════════════════════════════════════════════════ + # Job 1: 部署前校验 + # ═══════════════════════════════════════════════════════════════ + validate: + name: "🔍 部署前校验" + runs-on: ubuntu-latest + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 📦 安装依赖(如需要) + run: | + if [ -f "package-lock.json" ] && grep -q '"glob"' package.json; then + npm ci --ignore-scripts || npm install --ignore-scripts || echo "⚠️ 依赖安装失败,继续执行" + fi + + - name: 🔍 HLI 契约校验 + run: | + if [ -f "scripts/contract-check.js" ]; then + echo "🔍 执行 HLI 契约校验..." + node scripts/contract-check.js + else + echo "⏭️ scripts/contract-check.js 不存在,跳过 HLI 校验" + fi + + - name: 📋 模块指纹检查 + run: | + if [ -f "scripts/zhuyuan-module-protocol.js" ]; then + echo "🔍 执行模块指纹检查..." + node scripts/zhuyuan-module-protocol.js status + else + echo "⏭️ scripts/zhuyuan-module-protocol.js 不存在,跳过模块检查" + fi + + - name: ✅ 校验汇总 + run: | + echo "### ✅ 部署前校验通过" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- HLI 契约校验: ✅" >> $GITHUB_STEP_SUMMARY + echo "- 模块指纹检查: ✅" >> $GITHUB_STEP_SUMMARY + echo "- 提交: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════ + # Job 2: rsync 同步到服务器 + # ═══════════════════════════════════════════════════════════════ + deploy: + name: "🚀 部署到服务器" + needs: validate + runs-on: ubuntu-latest + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 📦 确保本地 rsync 已安装 + run: | + if ! command -v rsync &>/dev/null; then + sudo apt-get update -qq && sudo apt-get install -y rsync + fi + rsync --version | head -1 + + - name: 🔑 配置 SSH 连接 + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + - name: 📦 确保远程服务器 rsync 已安装 + run: | + echo "🔍 检查远程服务器 rsync..." + ssh -i ~/.ssh/deploy_key ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \ + "command -v rsync || { echo '⚠️ 远程服务器未安装 rsync,尝试安装...' && \ + (sudo apt-get update -qq && sudo apt-get install -y rsync 2>/dev/null || \ + sudo yum install -y rsync 2>/dev/null || \ + { echo '❌ 无法自动安装 rsync(可能缺少 sudo 权限)'; \ + echo '请手动在服务器上安装: sudo apt-get install rsync 或 sudo yum install rsync'; \ + exit 1; }); }" + echo "✅ 远程服务器 rsync 检查完成" + + - name: 📁 确保远程部署目录存在 + run: | + echo "🔍 检查远程部署目录..." + ssh -i ~/.ssh/deploy_key \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \ + "mkdir -p '${{ secrets.DEPLOY_PATH }}'" || { + echo "❌ 无法创建远程部署目录(SSH 连接失败或权限不足)" + echo "请确认 DEPLOY_PATH 设置正确且 DEPLOY_USER 有写入权限" + exit 1 + } + echo "✅ 远程部署目录已就绪" + + - name: 🔒 部署前安全检查 + run: | + if [ ! -f "docs/index.html" ]; then + echo "❌ docs/index.html 不存在,中止部署以防清空生产环境" + exit 1 + fi + FILE_COUNT=$(find . -type f \ + -not -path './.git/*' \ + -not -path './.github/*' \ + -not -path '*/node_modules/*' \ + | wc -l) + if [ "$FILE_COUNT" -lt 5 ]; then + echo "❌ 仓库文件数异常(仅 ${FILE_COUNT} 个),中止部署" + exit 1 + fi + echo "✅ 仓库包含 ${FILE_COUNT} 个文件,安全检查通过" + + - name: 🚀 rsync 同步全站到服务器 + run: | + echo "📡 开始同步全站到 ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}" + + rsync -avz --delete-after \ + --exclude '.git' \ + --exclude '.github' \ + --exclude 'node_modules' \ + --exclude 'syslog-inbox' \ + --exclude 'syslog-processed' \ + --exclude 'broadcasts-outbox' \ + --exclude 'dev-nodes' \ + --exclude 'signal-log' \ + --exclude 'tests' \ + --exclude 'reports' \ + --exclude 'scripts' \ + --exclude '*.bak' \ + --exclude '*.bak2' \ + --exclude '*_副本' \ + --exclude '*_副本/' \ + --exclude '*_副本.*' \ + --exclude '.DS_Store' \ + --exclude '.env' \ + --exclude 'package-lock.json' \ + --exclude 'tsconfig.json' \ + --exclude 'next.config.ts' \ + --exclude 'postcss.config.mjs' \ + --exclude 'jest.smoke.config.js' \ + --exclude 'ecosystem.config.js' \ + --exclude '.gitignore' \ + --exclude 'CNAME' \ + --exclude '*.zip' \ + -e "ssh -i ~/.ssh/deploy_key" \ + ./ \ + ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/ + + echo "✅ 全站同步完成" + + - name: 📄 复制首页到站点根目录 + run: | + echo "📄 复制 docs/index.html → 站点根目录 index.html" + ssh -i ~/.ssh/deploy_key \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" " + cp '${{ secrets.DEPLOY_PATH }}/docs/index.html' '${{ secrets.DEPLOY_PATH }}/index.html' + # 同步 docs 下的其他静态资源到根目录 + cp '${{ secrets.DEPLOY_PATH }}/docs/.nojekyll' '${{ secrets.DEPLOY_PATH }}/.nojekyll' 2>/dev/null || true + " + echo "✅ 首页已部署到根目录" + + - name: 🔧 后端服务部署 + continue-on-error: true + run: | + echo "🔧 检查并部署后端服务..." + ssh -i ~/.ssh/deploy_key \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" " + # 检查 Node.js 和 pm2 是否可用 + if ! command -v node &>/dev/null; then + echo '⏭️ Node.js 未安装,跳过后端部署' + exit 0 + fi + if ! command -v pm2 &>/dev/null; then + echo '⏭️ pm2 未安装,跳过后端服务管理' + exit 0 + fi + + # 部署 backend 服务(Express API,端口 3000) + if [ -f '${{ secrets.DEPLOY_PATH }}/backend/server.js' ]; then + echo '📦 安装 backend 依赖...' + cd '${{ secrets.DEPLOY_PATH }}/backend' + npm install --production 2>/dev/null || echo '⚠️ backend npm install 失败' + echo '🔄 重启 backend 服务...' + pm2 restart guanghulab-backend 2>/dev/null || \ + pm2 start server.js --name guanghulab-backend 2>/dev/null || \ + echo '⚠️ backend pm2 启动失败' + fi + + # 部署 status-board WebSocket 服务(端口 8080) + if [ -f '${{ secrets.DEPLOY_PATH }}/status-board/mock-ws-server.js' ]; then + echo '🔄 重启 WebSocket 服务...' + cd '${{ secrets.DEPLOY_PATH }}/status-board' + npm install --production 2>/dev/null || true + pm2 restart guanghulab-ws 2>/dev/null || \ + pm2 start mock-ws-server.js --name guanghulab-ws 2>/dev/null || \ + echo '⚠️ WebSocket pm2 启动失败' + fi + + # 部署 src HLI 中间层 + if [ -f '${{ secrets.DEPLOY_PATH }}/src/index.js' ]; then + echo '📦 安装 src 依赖...' + cd '${{ secrets.DEPLOY_PATH }}' + npm install --production 2>/dev/null || echo '⚠️ src npm install 失败' + echo '🔄 重启 HLI 服务...' + pm2 restart guanghulab 2>/dev/null || \ + pm2 start src/index.js --name guanghulab 2>/dev/null || \ + echo '⚠️ src pm2 启动失败' + fi + + pm2 save || echo '⚠️ pm2 save 失败,重启后进程可能不会自动启动' + + # 设置开机自启(仅首次需要,后续无操作) + pm2 startup 2>/dev/null | grep -o 'sudo.*' | bash 2>/dev/null || true + + echo '✅ 后端服务部署完成' + " + + - name: 🌐 更新 Nginx 配置 + continue-on-error: true + run: | + echo "🔧 检查 Nginx 配置..." + ssh -i ~/.ssh/deploy_key \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" " + # 检查 Nginx 是否安装 + if ! command -v nginx &>/dev/null; then + echo '⏭️ Nginx 未安装,跳过配置更新' + exit 0 + fi + + NGINX_CHANGED=false + + # 一次性迁移:更新 root 路径(从旧的 status-board 子目录改为站点根目录) + if grep -q 'root.*guanghulab/status-board' /etc/nginx/sites-available/default 2>/dev/null || \ + grep -q 'root.*guanghulab/status-board' /etc/nginx/conf.d/*.conf 2>/dev/null; then + echo '🔄 更新 Nginx root 路径...' + sudo sed -i 's|root.*guanghulab/status-board[^;]*|root ${{ secrets.DEPLOY_PATH }}|g' \ + /etc/nginx/sites-available/default 2>/dev/null || true + sudo sed -i 's|root.*guanghulab/status-board[^;]*|root ${{ secrets.DEPLOY_PATH }}|g' \ + /etc/nginx/conf.d/*.conf 2>/dev/null || true + NGINX_CHANGED=true + echo '✅ Nginx root 路径已更新' + else + echo '✅ Nginx root 路径正确,无需修改' + fi + + # 配置 API + WebSocket 反向代理 + if ! grep -rq 'location /api/' /etc/nginx/sites-available/ /etc/nginx/conf.d/ /etc/nginx/snippets/ 2>/dev/null; then + echo '🔄 添加 API + WebSocket 反向代理...' + sudo mkdir -p /etc/nginx/snippets + # 注意:\$ 在本地 shell 中转义为 $,远程 shell 的单引号保护不再展开, + # 最终写入文件的 Nginx 变量为 $host、$http_upgrade 等 + printf '%s\n' \ + '# guanghulab 反向代理(由 CD 工作流自动生成)' \ + '' \ + '# API 反向代理 → Express 后端端口 3000' \ + 'location /api/ {' \ + ' proxy_pass http://127.0.0.1:3000/api/;' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Host \$host;' \ + ' proxy_set_header X-Real-IP \$remote_addr;' \ + ' proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;' \ + ' proxy_set_header X-Forwarded-Proto \$scheme;' \ + ' proxy_cache_bypass \$http_upgrade;' \ + '}' \ + '' \ + '# WebSocket 反向代理 → 看板实时推送端口 8080' \ + 'location /ws {' \ + ' proxy_pass http://127.0.0.1:8080;' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Upgrade \$http_upgrade;' \ + ' proxy_set_header Connection \"Upgrade\";' \ + ' proxy_set_header Host \$host;' \ + ' proxy_set_header X-Real-IP \$remote_addr;' \ + ' proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;' \ + ' proxy_read_timeout 86400;' \ + '}' \ + | sudo tee /etc/nginx/snippets/guanghulab-proxy.conf > /dev/null + + # 在 server 块中加入 include 指令(仅搜索 sites-available 避免 symlink 重复) + NGINX_MAIN=\$(grep -rl 'server_name' /etc/nginx/sites-available/ 2>/dev/null | head -1) + if [ -z \"\$NGINX_MAIN\" ]; then + NGINX_MAIN='/etc/nginx/sites-available/default' + fi + if [ -f \"\$NGINX_MAIN\" ] && ! grep -q 'guanghulab-proxy' \"\$NGINX_MAIN\" 2>/dev/null; then + sudo sed -i '/server_name/a\\ include /etc/nginx/snippets/guanghulab-proxy.conf;' \"\$NGINX_MAIN\" 2>/dev/null && \ + NGINX_CHANGED=true && echo '✅ 反向代理已自动配置' || \ + echo '⚠️ 自动配置失败,请手动在 Nginx server 块中加入: include /etc/nginx/snippets/guanghulab-proxy.conf;' + fi + else + echo '✅ API 反向代理已存在,无需修改' + fi + + # 如果有变更,验证并重新加载 Nginx + if [ \"\$NGINX_CHANGED\" = true ]; then + sudo nginx -t && sudo systemctl reload nginx && echo '✅ Nginx 配置已重新加载' || \ + echo '⚠️ Nginx 配置验证失败,请手动检查' + fi + " + + - name: 🔍 验证部署 + run: | + ssh -i ~/.ssh/deploy_key \ + ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \ + "echo '✅ 服务器连接正常' && \ + echo '--- 站点根目录 ---' && \ + ls -la ${{ secrets.DEPLOY_PATH }}/ | head -30 && \ + echo '--- docs/ ---' && \ + ls ${{ secrets.DEPLOY_PATH }}/docs/ 2>/dev/null || echo '(docs/ 不存在)' && \ + echo '--- status-board/ ---' && \ + ls ${{ secrets.DEPLOY_PATH }}/status-board/ 2>/dev/null || echo '(status-board/ 不存在)'" + + - name: 📝 部署汇总 + run: | + echo "### 🚀 全站部署完成" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- 同步范围: 全仓库(含排除规则)" >> $GITHUB_STEP_SUMMARY + echo "- 目标: \`${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}\`" >> $GITHUB_STEP_SUMMARY + echo "- 提交: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- 时间: $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M CST')" >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════ + # Job 3: Notion 部署通知 + # ═══════════════════════════════════════════════════════════════ + notify: + name: "📡 部署通知 → Notion" + needs: deploy + runs-on: ubuntu-latest + if: success() + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 📡 推送部署记录到 Notion + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }} + EVENT_TYPE: commit + COMMIT_SHA: ${{ github.sha }} + COMMIT_MSG: "[CD 部署] ${{ github.event.head_commit.message }}" + COMMITTER: ${{ github.event.head_commit.author.name || github.actor }} + COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }} + BRANCH: ${{ github.ref_name }} + run: | + CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || true) + export CHANGED_FILES="$CHANGED" + if [ -f "scripts/notion-bridge.js" ]; then + node scripts/notion-bridge.js changes + echo "✅ Notion 部署通知已发送" + else + echo "⏭️ scripts/notion-bridge.js 不存在,跳过 Notion 通知" + fi diff --git a/.github/workflows/esp-signal-processor.yml b/.github/workflows/esp-signal-processor.yml index c118f39a..8c992f4d 100644 --- a/.github/workflows/esp-signal-processor.yml +++ b/.github/workflows/esp-signal-processor.yml @@ -1,9 +1,11 @@ -name: 铸渊 · ESP 邮件信号处理器 +# ⚠️ 已暂停:邮件通信已迁移到 Notion API 直连(见 notion-poll.yml) +# 保留文件备用,不删除。邮件格式规范文档和 signal-log/ 结构继续沿用。 +name: 铸渊 · ESP 邮件信号处理器(已暂停) on: - schedule: - - cron: '*/30 * * * *' # 每30分钟执行一次 - workflow_dispatch: # 手动触发 + # schedule: + # - cron: '*/30 * * * *' # 每30分钟执行一次(已暂停) + workflow_dispatch: # 手动触发(保留,紧急时可手动运行) jobs: esp-process: diff --git a/.github/workflows/generate-module-doc.yml b/.github/workflows/generate-module-doc.yml index e8de5d9e..9c837b39 100644 --- a/.github/workflows/generate-module-doc.yml +++ b/.github/workflows/generate-module-doc.yml @@ -49,7 +49,12 @@ jobs: echo "📋 文档无变化,跳过提交" else git commit -m "📋 自动更新 HoloLake Era 模块文档 · $(date -u +%Y-%m-%d\ %H:%M\ UTC)" - git push + git pull --rebase origin main || echo "⚠️ rebase 失败(可能有冲突),尝试直接推送..." + git push || { + echo "⚠️ 推送失败,尝试重新拉取后再推送..." + git pull --rebase origin main + git push + } fi notify-collaborator: diff --git a/.github/workflows/notion-poll.yml b/.github/workflows/notion-poll.yml new file mode 100644 index 00000000..128c6ccd --- /dev/null +++ b/.github/workflows/notion-poll.yml @@ -0,0 +1,38 @@ +name: 铸渊 · Notion 工单轮询 + +on: + schedule: + - cron: '*/15 * * * *' # 每15分钟轮询一次 + workflow_dispatch: # 手动触发 + +jobs: + notion-poll: + name: 📡 轮询 Notion 工单簿 + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 轮询工单簿 + 处理 + 回写 + id: poll + env: + NOTION_API_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + WORKORDER_DB_ID: ${{ vars.WORKORDER_DB_ID }} + SIGNAL_LOG_DB_ID: ${{ vars.SIGNAL_LOG_DB_ID }} + run: node scripts/notion-signal-bridge.js poll + + - name: 提交信号日志 + if: success() + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add signal-log/ broadcasts-outbox/ + git diff --cached --quiet || git commit -m "📡 Notion 信号处理 · $(date +%Y-%m-%dT%H:%M)" + git push diff --git a/.github/workflows/staging-preview.yml b/.github/workflows/staging-preview.yml new file mode 100644 index 00000000..9b909cc6 --- /dev/null +++ b/.github/workflows/staging-preview.yml @@ -0,0 +1,180 @@ +name: "🔍 铸渊预演部署 (Staging Preview)" + +# === 触发方式 === +# 1. PR 到 main 分支(含 docs/ 变更时自动触发) +# 2. 手动触发(workflow_dispatch) +on: + pull_request: + branches: [main] + paths: + - 'docs/**' + - 'm*/**' + - '.github/brain/**' + + workflow_dispatch: + inputs: + pr_number: + description: "PR 编号(可选)" + required: false + type: string + +permissions: + contents: read + pull-requests: write + actions: read + +jobs: + staging-preview: + name: "🔍 预演检查" + runs-on: ubuntu-latest + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 🔍 模块完整性检查 + id: module_check + run: | + echo "## 🔍 铸渊预演报告" > /tmp/preview-report.md + echo "" >> /tmp/preview-report.md + echo "📅 检查时间: $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M CST')" >> /tmp/preview-report.md + echo "" >> /tmp/preview-report.md + + # 检查 docs/index.html 是否存在 + if [ -f "docs/index.html" ]; then + SIZE=$(wc -c < docs/index.html) + LINES=$(wc -l < docs/index.html) + echo "### ✅ 前端文件" >> /tmp/preview-report.md + echo "- \`docs/index.html\`: ${LINES} 行, ${SIZE} 字节" >> /tmp/preview-report.md + else + echo "### ❌ 前端文件缺失" >> /tmp/preview-report.md + echo "- \`docs/index.html\` 不存在!" >> /tmp/preview-report.md + fi + echo "" >> /tmp/preview-report.md + + # 检查 CNAME + if [ -f "docs/CNAME" ]; then + DOMAIN=$(cat docs/CNAME | tr -d '[:space:]') + echo "### 🌐 自定义域名" >> /tmp/preview-report.md + echo "- 域名: \`${DOMAIN}\`" >> /tmp/preview-report.md + else + echo "### ⚠️ 未配置自定义域名" >> /tmp/preview-report.md + echo "- 将使用默认 GitHub Pages 域名" >> /tmp/preview-report.md + fi + echo "" >> /tmp/preview-report.md + + # 检查模块目录 + echo "### 📦 模块状态" >> /tmp/preview-report.md + echo "| 模块 | 文件数 | README | 状态 |" >> /tmp/preview-report.md + echo "|------|--------|--------|------|" >> /tmp/preview-report.md + + MODULE_OK=0 + MODULE_WARN=0 + MODULE_EMPTY=0 + MAX_EMPTY_MODULES=5 # 允许的最大空模块数 + + for dir in m[0-9][0-9]-*/; do + if [ -d "$dir" ]; then + NAME=$(basename "$dir") + FILE_COUNT=$(find "$dir" -type f | wc -l) + HAS_README="❌" + STATUS="⚠️ 无文件" + + if [ -f "${dir}README.md" ]; then + HAS_README="✅" + fi + + if [ "$FILE_COUNT" -gt 0 ]; then + if [ -f "${dir}README.md" ]; then + STATUS="✅ 正常" + MODULE_OK=$((MODULE_OK + 1)) + else + STATUS="⚠️ 缺少 README" + MODULE_WARN=$((MODULE_WARN + 1)) + fi + else + MODULE_EMPTY=$((MODULE_EMPTY + 1)) + fi + + echo "| \`${NAME}\` | ${FILE_COUNT} | ${HAS_README} | ${STATUS} |" >> /tmp/preview-report.md + fi + done + + echo "" >> /tmp/preview-report.md + echo "**汇总**: ✅ ${MODULE_OK} 正常 · ⚠️ ${MODULE_WARN} 告警 · 📭 ${MODULE_EMPTY} 空模块" >> /tmp/preview-report.md + echo "" >> /tmp/preview-report.md + + # 检查关键工作流 + echo "### ⚙️ 工作流检查" >> /tmp/preview-report.md + WORKFLOW_COUNT=$(ls -1 .github/workflows/*.yml 2>/dev/null | wc -l) + echo "- 工作流文件: ${WORKFLOW_COUNT} 个" >> /tmp/preview-report.md + + if [ -f ".github/workflows/deploy-pages.yml" ]; then + echo "- 部署工作流: ✅ 存在" >> /tmp/preview-report.md + else + echo "- 部署工作流: ❌ 缺失" >> /tmp/preview-report.md + fi + echo "" >> /tmp/preview-report.md + + # 预演结论 + echo "### 🎯 预演结论" >> /tmp/preview-report.md + if [ -f "docs/index.html" ] && [ "$MODULE_EMPTY" -lt "$MAX_EMPTY_MODULES" ]; then + echo "✅ **预演通过** — 可以合并到 main 分支触发正式部署" >> /tmp/preview-report.md + echo "" >> /tmp/preview-report.md + echo "合并后将自动触发 \`deploy-pages.yml\` 部署到生产环境。" >> /tmp/preview-report.md + echo "如需人工确认,请评论「启动冰朔人格体」触发部署诊断。" >> /tmp/preview-report.md + else + echo "⚠️ **预演告警** — 请检查上述问题后再合并" >> /tmp/preview-report.md + fi + + echo "" >> /tmp/preview-report.md + echo "---" >> /tmp/preview-report.md + echo "*🌀 铸渊预演系统 · 自动生成*" >> /tmp/preview-report.md + + cat /tmp/preview-report.md >> $GITHUB_STEP_SUMMARY + + - name: 📎 上传预演构建产物 + uses: actions/upload-artifact@v4 + with: + name: staging-preview-${{ github.sha }} + path: docs/ + retention-days: 7 + + - name: 💬 在 PR 中发布预演报告 + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const report = fs.readFileSync('/tmp/preview-report.md', 'utf8'); + + // 查找已有的预演评论 + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.data.find(c => + c.body.includes('🔍 铸渊预演报告') && c.user.type === 'Bot' + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: report, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: report, + }); + } diff --git a/.github/workflows/update-repo-map.yml b/.github/workflows/update-repo-map.yml index f2469e5a..b2104c70 100644 --- a/.github/workflows/update-repo-map.yml +++ b/.github/workflows/update-repo-map.yml @@ -34,4 +34,10 @@ jobs: git config user.email "zhuyuan@guanghulab.com" git add .github/brain/repo-map.json .github/brain/repo-snapshot.md git diff --cached --quiet || git commit -m "📚 铸渊图书馆目录更新 · $(date +%Y-%m-%dT%H:%M)" - git push + # 拉取远端最新更改后再推送,避免竞态条件 + git pull --rebase origin main || echo "⚠️ rebase 失败(可能有冲突),尝试直接推送..." + git push || { + echo "⚠️ 推送失败,尝试重新拉取后再推送..." + git pull --rebase origin main + git push + } diff --git a/README.md b/README.md index e79212de..bda8f34e 100644 --- a/README.md +++ b/README.md @@ -1,148 +1,47 @@ -# 光湖系统 · 模块代码总仓库 +# HoloLake 光湖系统 ---- - -## 🚀 怎么用铸渊聊天室? - -**一句话:点下面这个链接就能打开,直接聊天。** - -### 👉 [点这里打开铸渊聊天室](https://qinfendebingshuo.github.io/guanghulab/docs/index.html) - -``` -https://qinfendebingshuo.github.io/guanghulab/docs/index.html -``` - -> 把这个链接发给任何人,他们打开就能用,不需要安装任何东西。 - ---- - -## 🎯 不同角色怎么用 - -### ❄️ 冰朔(你) - -1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html -2. **选身份** → 下拉菜单选「❄️ 冰朔(语言架构师·创始人)」 -3. **填 API 密钥** → 输入你的云雾/OpenAI/DeepSeek API Key -4. **点「开始对话」** → 铸渊自动唤醒,汇报大脑状态 -5. 说「**我是冰朔**」即可触发完整唤醒序列 - ---- - -### 🦁 肥猫(光湖团队总控 · DEV-002) - -1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html -2. **选身份** → 下拉菜单选「🦁 肥猫(光湖团队总控·DEV-002)」 -3. **填 API 密钥** → 输入自己的 API Key(或点「演示模式」也能看进度面板) -4. **点「开始对话」** → 铸渊自动打开**总控指挥台**,显示全员进度 -5. 点顶部 **🦁 按钮** → 查看所有开发者详细状态、谁在阻塞、服务器信息 -6. 直接聊天提问,例如: - - 「查看所有开发者当前进度」 - - 「谁现在遇到阻塞?」 - - 「下一步推进计划是什么?」 - ---- - -### 🍊 桔子(光湖主控 · DEV-010) - -1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html -2. **选身份** → 下拉菜单选「🍊 桔子(光湖主控·DEV-010)」 -3. **填 API 密钥** → 输入自己的 API Key -4. **点「开始对话」** → 铸渊显示你当前模块进度,并开放指挥台 - ---- - -### 👩‍💻 其他开发者(页页 / 燕樊 / 小草莓 / 花尔 等) - -1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html -2. **选身份** → 下拉菜单选自己的名字 -3. **填 API 密钥** → 填自己的 Key(没有的话点「演示模式」) -4. **点「开始对话」** → 铸渊显示你的当前任务状态,可以问问题 - ---- - -## 🔑 API 密钥从哪里来? - -| 提供商 | 申请地址 | 备注 | -|--------|---------|------| -| **云雾 API** | 团队统一分配 | 推荐,团队共用 | -| DeepSeek | https://platform.deepseek.com | 便宜好用 | -| 智谱 GLM | https://open.bigmodel.cn | 有免费额度 | -| Kimi | https://platform.moonshot.cn | 长文本好 | -| OpenAI | https://platform.openai.com | 最强但价格较高 | - -> 💡 **没有 API Key?** 点「演示模式」也能查看团队进度面板(🦁 指挥台),只是不能真实 AI 对话。 - ---- - -## 📱 怎么分享给别人? - -**最简单:直接把链接发过去!** - -``` -铸渊聊天室入口 👇 -https://qinfendebingshuo.github.io/guanghulab/docs/index.html - -打开后选择你的身份,填入 API Key 就能和铸渊说话了。 -``` - -**想离线用?** 在聊天室右上角点 **⬇️ 下载** → 下载一个 HTML 文件 → 发给对方 → 双击打开就能用(不需要网络,只需要 API Key)。 - ---- - -## 💬 常见问题 - -**Q: 打开是空白页面怎么办?** -A: 等待 GitHub Pages 部署完成(通常 1-2 分钟),刷新页面。 - -**Q: API Key 安全吗?会不会被偷?** -A: 放心,Key 只存在你自己浏览器的 localStorage 里,不会上传到任何服务器。 - -**Q: 以前的对话记录怎么找?** -A: 目前对话记录在浏览器里,刷新后需要重新开始。大脑记忆文件在 `.github/brain/` 里永久保存。 - -**Q: 铸渊的大脑会记住我说的话吗?** -A: 每次对话内的上下文会记住,跨对话的重要信息需要说「记住这个」,铸渊会建议更新到 `.github/brain/` 文件里。 - ---- - -## 🌀 铸渊聊天室功能一览 - -| 功能 | 说明 | -|------|------| -| 💬 对话模式 | 默认,聊技术问题、聊进度、聊架构 | -| 🔨 构建模式 | 自动检测,聊到代码/接口时切换,给出完整可执行代码 | -| 📋 审查模式 | 自动检测,聊到"检查/审查"时切换,审查代码质量 | -| 🧠 大脑模式 | 说"记住这个"时切换,整理重要信息到大脑文件 | -| 🦁 指挥台 | 总控/主控专属,查看全员进度、阻塞、服务器信息 | -| 📊 大脑状态 | 查看 HLI 接口覆盖率、近期事件 | -| ⬇️ 下载 | 下载单文件版,可离线使用 | - ---- - -## 📋 仓库结构与模块分工 - -每个模块一个文件夹,开发者将代码上传到对应文件夹中。 - -| 文件夹 | 模块 | 负责人 | -|--------|------|--------| -| m01-login | M01 用户登录界面 | 肥猫 | -| m03-personality | M03 人格系统 | 肥猫 | -| m05-user-center | M05 用户中心界面 | 花尔 | -| m06-ticket | M06 工单管理界面 | 桔子 | -| m07-dialogue-ui | M07 对话UI | 燕樊 | -| m10-cloud | M10 云盘系统 | 燕樊 | -| m11-module | M11 工单管理模块 | 桔子 | -| m12-kanban | M12 状态看板 | 小草莓 | -| dingtalk-bot | 钉钉机器人 | 之之 | -| backend-integration | 后端集成中间层 | 页页 | - -## 上传规范 -每个模块文件夹必须包含以下文件: -- README.md(模块说明) -- package.json(依赖声明) -- src/(源代码目录) -- SYSLOG.md(开发回执日志) - -## 自动检查 -每次 push 会自动检查模块结构是否符合规范,不通过会标红提醒。 +## 模块目录 +- app/ +- backend-integration/ +- backend/ +- broadcasts-outbox/ +- chat-bubble/ +- cloud-drive/ +- coldstart/ +- cost-control/ +- dashboard/ +- dev-nodes/ +- dingtalk-bot/ +- docs/ +- frontend/ +- guanghulab-main/ +- help-center/ +- m01-login/ +- m03-personality/ +- m05-user-center/ +- m06-ticket/ +- m07-dialogue-ui/ +- m10-cloud/ +- m11-module/ +- m12-kanban/ +- m15-cloud-drive/ +- m18-health-check/ +- multi-persona/ +- notification/ +- notion-push/ +- persona-selector/ +- portal/ +- public/ +- reports/ +- scripts/ +- settings/ +- signal-log/ +- src/ +- status-board/ +- style-system/ +- syslog-inbox/ +- syslog-processed/ +- tests/ +- ticket-system/ +- user-center/ diff --git a/backend-integration/README.md b/backend-integration/README.md index aefed6e5..d6cf25a5 100644 --- a/backend-integration/README.md +++ b/backend-integration/README.md @@ -3,3 +3,55 @@ - 状态:进行中 - 技术栈:Node.js - 依赖模块:所有前端模块 + +## API 代理层(api-proxy.js) + +解决国内开发者无法直连海外模型 API + 前端 Key 暴露问题。 + +### 快速启动 + +```bash +# 1. 配置 API 密钥(推荐先用 DeepSeek,国内直连) +export DEEPSEEK_API_KEY=sk-xxx + +# 2. 启动代理 +node backend-integration/api-proxy.js +# 或用 PM2 +pm2 start backend-integration/api-proxy.js --name api-proxy + +# 3. 测试 +curl http://localhost:3721/api/health +curl http://localhost:3721/api/models +``` + +### 接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/chat` | 聊天代理(SSE 流式透传) | +| GET | `/api/models` | 列出已配置的可用模型 | +| GET | `/api/health` | 健康检查 | + +### 环境变量 + +| 变量 | 说明 | +|------|------| +| `DEEPSEEK_API_KEY` | DeepSeek 密钥(国内直连,推荐首选) | +| `MOONSHOT_API_KEY` | Moonshot/Kimi 密钥(国内直连) | +| `ZHIPU_API_KEY` | 智谱 AI 密钥(国内直连) | +| `YUNWU_API_KEY` | 云雾 AI 密钥(团队推荐) | +| `OPENAI_API_KEY` | OpenAI 密钥(需海外服务器) | +| `GEMINI_API_KEY` | Google Gemini 密钥(需海外服务器) | +| `PROXY_PORT` | 端口(默认 3721) | +| `RATE_LIMIT_RPM` | 频率限制(默认 10 次/分钟) | + +### Nginx 配置 + +参见 `nginx-api-proxy.conf`,将 `/api/*` 请求代理到 Node.js 服务。 + +### 前端对接 + +前端铸渊聊天室(docs/index.html)已内置「后端代理模式」: +- 在设置页面选择「🔌 后端代理」提供商 +- 自动调用 `/api/chat`,无需用户填写 API Key +- Key 由后端环境变量管理,不暴露给前端 diff --git a/backend-integration/api-proxy.js b/backend-integration/api-proxy.js new file mode 100644 index 00000000..611f0cd2 --- /dev/null +++ b/backend-integration/api-proxy.js @@ -0,0 +1,457 @@ +/** + * 🔌 AGE OS · API 代理层 + * + * 解决两个核心问题: + * 1. 国内开发者无法直连海外模型API(被墙) + * 2. API Key 不应暴露在前端 JavaScript 中 + * + * 架构: + * 浏览器 → guanghulab.com/api/chat → 本代理 → 真实模型API + * + * 支持的模型: + * - deepseek (api.deepseek.com) — 国内直连,推荐首选 + * - moonshot (api.moonshot.cn) — 国内直连 + * - zhipu (open.bigmodel.cn) — 国内直连 + * - yunwu (api.yunwu.ai) — 团队推荐 + * - openai (api.openai.com) — 需海外服务器 + * - gemini (generativelanguage.googleapis.com) — 需海外服务器 + * + * 环境变量: + * DEEPSEEK_API_KEY — DeepSeek API 密钥 + * MOONSHOT_API_KEY — Moonshot/Kimi API 密钥 + * ZHIPU_API_KEY — 智谱 API 密钥 + * YUNWU_API_KEY — 云雾 AI API 密钥 + * OPENAI_API_KEY — OpenAI API 密钥 + * GEMINI_API_KEY — Google Gemini API 密钥 + * PROXY_PORT — 代理服务端口(默认 3721) + * ALLOWED_ORIGINS — CORS 允许的域名(逗号分隔,默认 *) + * RATE_LIMIT_RPM — 每用户每分钟最大请求数(默认 10) + * + * 启动: + * node backend-integration/api-proxy.js + * 或通过 PM2: pm2 start backend-integration/api-proxy.js --name api-proxy + */ + +const http = require('http'); +const https = require('https'); +const { URL } = require('url'); + +// ═══════════════════════════════════════════════════════ +// 配置 +// ═══════════════════════════════════════════════════════ +const PORT = parseInt(process.env.PROXY_PORT || '3721', 10); +const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://qinfendebingshuo.github.io,https://guanghulab.com,http://localhost:8765').split(',').map(s => s.trim()); +const RATE_LIMIT_RPM = parseInt(process.env.RATE_LIMIT_RPM || '10', 10); + +// 模型端点映射 +const MODEL_CONFIG = { + deepseek: { + base: 'https://api.deepseek.com/v1', + keyEnv: 'DEEPSEEK_API_KEY', + models: ['deepseek-chat', 'deepseek-reasoner'], + label: 'DeepSeek(国内直连)' + }, + moonshot: { + base: 'https://api.moonshot.cn/v1', + keyEnv: 'MOONSHOT_API_KEY', + models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'], + label: 'Moonshot/Kimi(国内直连)' + }, + zhipu: { + base: 'https://open.bigmodel.cn/api/paas/v4', + keyEnv: 'ZHIPU_API_KEY', + models: ['glm-4', 'glm-4-flash', 'glm-4-air'], + label: '智谱AI/GLM(国内直连)' + }, + yunwu: { + base: 'https://api.yunwu.ai/v1', + keyEnv: 'YUNWU_API_KEY', + models: ['gpt-4o', 'gpt-4o-mini', 'claude-3-5-sonnet-20241022', 'deepseek-chat', 'gemini-1.5-pro'], + label: '云雾AI(团队推荐)' + }, + openai: { + base: 'https://api.openai.com/v1', + keyEnv: 'OPENAI_API_KEY', + models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'], + label: 'OpenAI(需海外服务器)' + }, + gemini: { + base: 'https://generativelanguage.googleapis.com/v1beta/openai', + keyEnv: 'GEMINI_API_KEY', + models: ['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-pro', 'gemini-1.5-flash'], + label: 'Google Gemini(需海外服务器)' + } +}; + +// ═══════════════════════════════════════════════════════ +// 频率限制(内存级,基于 IP) +// ═══════════════════════════════════════════════════════ +const rateLimitMap = new Map(); + +function checkRateLimit(clientId) { + const now = Date.now(); + const windowMs = 60 * 1000; // 1 minute window + + if (!rateLimitMap.has(clientId)) { + rateLimitMap.set(clientId, []); + } + + const timestamps = rateLimitMap.get(clientId); + // Remove entries older than the window + while (timestamps.length > 0 && timestamps[0] < now - windowMs) { + timestamps.shift(); + } + + if (timestamps.length >= RATE_LIMIT_RPM) { + return false; // Rate limited + } + + timestamps.push(now); + return true; +} + +// Clean up old entries every 5 minutes +setInterval(() => { + const cutoff = Date.now() - 120 * 1000; + for (const [key, timestamps] of rateLimitMap.entries()) { + const filtered = timestamps.filter(t => t > cutoff); + if (filtered.length === 0) { + rateLimitMap.delete(key); + } else { + rateLimitMap.set(key, filtered); + } + } +}, 5 * 60 * 1000); + +// ═══════════════════════════════════════════════════════ +// CORS 处理 +// ═══════════════════════════════════════════════════════ +function setCorsHeaders(res, origin) { + const allowed = ALLOWED_ORIGINS.includes('*') || ALLOWED_ORIGINS.includes(origin); + if (allowed) { + res.setHeader('Access-Control-Allow-Origin', origin || '*'); + } + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-User-Id'); + res.setHeader('Access-Control-Max-Age', '86400'); +} + +// ═══════════════════════════════════════════════════════ +// 请求体解析 +// ═══════════════════════════════════════════════════════ +function parseBody(req) { + return new Promise((resolve, reject) => { + let body = ''; + let size = 0; + const MAX_BODY = 512 * 1024; // 512KB max + + req.on('data', chunk => { + size += chunk.length; + if (size > MAX_BODY) { + reject(new Error('请求体过大')); + return; + } + body += chunk; + }); + req.on('end', () => { + try { + resolve(body ? JSON.parse(body) : {}); + } catch { + reject(new Error('无效的 JSON 请求体')); + } + }); + req.on('error', reject); + }); +} + +// ═══════════════════════════════════════════════════════ +// JSON 响应 +// ═══════════════════════════════════════════════════════ +function jsonResponse(res, status, data) { + res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify(data)); +} + +// ═══════════════════════════════════════════════════════ +// 获取客户端 IP +// ═══════════════════════════════════════════════════════ +function getClientId(req) { + return req.headers['x-forwarded-for']?.split(',')[0]?.trim() + || req.headers['x-real-ip'] + || req.socket.remoteAddress + || 'unknown'; +} + +// ═══════════════════════════════════════════════════════ +// 上游 HTTPS 请求(流式透传) +// ═══════════════════════════════════════════════════════ +function proxyStream(upstreamUrl, apiKey, requestBody, res) { + return new Promise((resolve, reject) => { + const url = new URL(upstreamUrl); + + const options = { + hostname: url.hostname, + port: url.port || 443, + path: url.pathname + url.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'Accept': 'text/event-stream' + } + }; + + const bodyStr = JSON.stringify(requestBody); + options.headers['Content-Length'] = Buffer.byteLength(bodyStr); + + const upstream = https.request(options, (upstreamRes) => { + // Forward status and content-type + const ct = upstreamRes.headers['content-type'] || 'text/event-stream'; + res.writeHead(upstreamRes.statusCode, { + 'Content-Type': ct, + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' // Nginx: disable buffering for SSE + }); + + // Pipe upstream response directly to client (streaming) + upstreamRes.on('data', chunk => { + res.write(chunk); + }); + + upstreamRes.on('end', () => { + res.end(); + resolve(); + }); + + upstreamRes.on('error', err => { + console.error('上游响应错误:', err.message); + res.end(); + reject(err); + }); + }); + + upstream.on('error', err => { + console.error('上游连接错误:', err.message); + if (!res.headersSent) { + jsonResponse(res, 502, { + error: true, + code: 'UPSTREAM_ERROR', + message: '无法连接模型API: ' + err.message + }); + } + reject(err); + }); + + // Connection timeout only — once response headers arrive, streaming can run longer + upstream.setTimeout(60000, () => { + upstream.destroy(new Error('连接超时')); + }); + + upstream.write(bodyStr); + upstream.end(); + }); +} + +// ═══════════════════════════════════════════════════════ +// 路由处理器 +// ═══════════════════════════════════════════════════════ + +// GET /api/models — 列出可用模型 +function handleGetModels(req, res) { + const available = {}; + for (const [provider, config] of Object.entries(MODEL_CONFIG)) { + const key = process.env[config.keyEnv]; + if (key) { + available[provider] = { + label: config.label, + models: config.models + }; + } + } + jsonResponse(res, 200, { + providers: available, + default_provider: Object.keys(available)[0] || null + }); +} + +// POST /api/chat — 代理转发聊天请求 +async function handleChat(req, res) { + const clientId = getClientId(req); + + // Rate limit check + if (!checkRateLimit(clientId)) { + jsonResponse(res, 429, { + error: true, + code: 'RATE_LIMITED', + message: `请求过于频繁,每分钟最多 ${RATE_LIMIT_RPM} 次请求` + }); + return; + } + + let body; + try { + body = await parseBody(req); + } catch (err) { + jsonResponse(res, 400, { + error: true, + code: 'INVALID_BODY', + message: err.message + }); + return; + } + + const { provider, model, messages, system, stream, temperature, max_tokens } = body; + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + jsonResponse(res, 400, { + error: true, + code: 'MISSING_MESSAGES', + message: '缺少 messages 参数' + }); + return; + } + + // Validate numeric parameters + const temp = typeof temperature === 'number' ? Math.max(0, Math.min(2, temperature)) : 0.8; + const maxTok = typeof max_tokens === 'number' ? Math.max(1, Math.min(8192, Math.floor(max_tokens))) : 2000; + + // Determine provider + const prov = provider || 'deepseek'; + const config = MODEL_CONFIG[prov]; + if (!config) { + jsonResponse(res, 400, { + error: true, + code: 'INVALID_PROVIDER', + message: `不支持的模型提供商: ${prov},可选: ${Object.keys(MODEL_CONFIG).join(', ')}` + }); + return; + } + + // Get API key from environment + const apiKey = process.env[config.keyEnv]; + if (!apiKey) { + jsonResponse(res, 503, { + error: true, + code: 'NO_API_KEY', + message: `${config.label} 的 API 密钥未配置,请联系管理员` + }); + return; + } + + // Build upstream request + const mdl = model || config.models[0]; + const upstreamBody = { + model: mdl, + messages: messages, + stream: stream !== false, // Default to streaming + temperature: temp, + max_tokens: maxTok + }; + + // If system prompt provided, prepend as system message + if (system && !messages.find(m => m.role === 'system')) { + upstreamBody.messages = [{ role: 'system', content: system }, ...messages]; + } + + const upstreamUrl = config.base + '/chat/completions'; + + console.log(`[代理] ${prov}/${mdl}`); + + try { + await proxyStream(upstreamUrl, apiKey, upstreamBody, res); + } catch (err) { + // Error already handled in proxyStream + console.error(`[代理] 请求失败: ${err.message}`); + } +} + +// GET /api/health — 健康检查 +function handleHealth(req, res) { + const configured = Object.entries(MODEL_CONFIG) + .filter(([, config]) => process.env[config.keyEnv]) + .map(([name, config]) => ({ provider: name, label: config.label })); + + jsonResponse(res, 200, { + status: 'ok', + version: '1.0.0', + service: 'AGE OS API Proxy', + configured_providers: configured, + rate_limit: RATE_LIMIT_RPM + ' req/min' + }); +} + +// ═══════════════════════════════════════════════════════ +// HTTP 服务器 +// ═══════════════════════════════════════════════════════ +const server = http.createServer(async (req, res) => { + const origin = req.headers.origin || ''; + setCorsHeaders(res, origin); + + // Handle preflight + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); + const path = url.pathname; + + try { + if (path === '/api/chat' && req.method === 'POST') { + await handleChat(req, res); + } else if (path === '/api/models' && req.method === 'GET') { + handleGetModels(req, res); + } else if (path === '/api/health' && req.method === 'GET') { + handleHealth(req, res); + } else { + jsonResponse(res, 404, { + error: true, + code: 'NOT_FOUND', + message: '接口不存在。可用接口: POST /api/chat, GET /api/models, GET /api/health' + }); + } + } catch (err) { + console.error('服务器错误:', err); + if (!res.headersSent) { + jsonResponse(res, 500, { + error: true, + code: 'INTERNAL_ERROR', + message: '服务器内部错误' + }); + } + } +}); + +server.listen(PORT, () => { + console.log(`\n🔌 AGE OS API 代理层已启动`); + console.log(` 端口: ${PORT}`); + console.log(` 地址: http://localhost:${PORT}`); + console.log(` 频率限制: ${RATE_LIMIT_RPM} 次/分钟`); + console.log(` CORS: ${ALLOWED_ORIGINS.join(', ')}`); + console.log(''); + + // Show configured providers + let hasAny = false; + for (const [name, config] of Object.entries(MODEL_CONFIG)) { + const key = process.env[config.keyEnv]; + const status = key ? '✅ 已配置' : '❌ 未配置'; + const china = ['deepseek', 'moonshot', 'zhipu'].includes(name) ? '🇨🇳' : '🌐'; + console.log(` ${china} ${config.label}: ${status}`); + if (key) hasAny = true; + } + + if (!hasAny) { + console.log('\n ⚠️ 未配置任何 API 密钥!'); + console.log(' 请设置环境变量(推荐先配 DeepSeek,国内直连):'); + console.log(' export DEEPSEEK_API_KEY=sk-xxx'); + console.log(' export YUNWU_API_KEY=sk-xxx'); + } + + console.log('\n 可用接口:'); + console.log(' POST /api/chat — 聊天代理(SSE 流式)'); + console.log(' GET /api/models — 列出可用模型'); + console.log(' GET /api/health — 健康检查'); + console.log(''); +}); diff --git a/backend-integration/nginx-api-proxy.conf b/backend-integration/nginx-api-proxy.conf new file mode 100644 index 00000000..cc36436b --- /dev/null +++ b/backend-integration/nginx-api-proxy.conf @@ -0,0 +1,26 @@ +# 🔌 AGE OS API 代理层 — Nginx 配置参考 +# +# 将此配置添加到 guanghulab.com 的 Nginx server block 中, +# 使前端可以通过 /api/* 路径访问后端代理。 +# +# 前端(GitHub Pages)→ Nginx → API 代理(Node.js :3721)→ 模型 API + +# API 代理转发 +location /api/ { + proxy_pass http://127.0.0.1:3721; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # SSE 流式响应必需 + proxy_set_header Connection ''; + proxy_buffering off; + proxy_cache off; + chunked_transfer_encoding on; + + # 长连接超时(与 Node.js api-proxy 的 60s 连接超时对齐,流式响应可更长) + proxy_read_timeout 90s; + proxy_send_timeout 60s; +} diff --git a/backend/.DS_Store b/backend/.DS_Store new file mode 100644 index 00000000..bb3a16e5 Binary files /dev/null and b/backend/.DS_Store differ diff --git a/backend/.env b/backend/.env new file mode 100644 index 00000000..6720a98a --- /dev/null +++ b/backend/.env @@ -0,0 +1,2 @@ +PRIMARY_API_KEY=你的主密钥 +FALLBACK_API_KEY=你的备用密钥 diff --git a/backend/config/models.js b/backend/config/models.js new file mode 100644 index 00000000..03d8eb07 --- /dev/null +++ b/backend/config/models.js @@ -0,0 +1,29 @@ +// 模型路由配置(支持主备API自动切换) +const PRIMARY_API_KEY = process.env.PRIMARY_API_KEY; +const FALLBACK_API_KEY = process.env.FALLBACK_API_KEY; + +const MODELS = { + deepseek: { + name: 'DeepSeek Chat', + apiUrl: 'https://api.deepseek.com/v1/chat/completions', + primaryApiKey: PRIMARY_API_KEY, + fallbackApiKey: FALLBACK_API_KEY, + model: 'deepseek-chat', + maxTokens: 4096 + }, + gpt4o_mini: { + name: 'GPT-4o-mini', + apiUrl: 'https://api.deepseek.com/v1/chat/completions', + primaryApiKey: PRIMARY_API_KEY, + fallbackApiKey: FALLBACK_API_KEY, + model: 'gpt-4o-mini', + maxTokens: 4096 + } +}; + +const DEFAULT_MODEL = 'deepseek'; + +module.exports = { + MODELS, + DEFAULT_MODEL +}; diff --git a/backend/config/models.js.bak b/backend/config/models.js.bak new file mode 100644 index 00000000..6ef24385 --- /dev/null +++ b/backend/config/models.js.bak @@ -0,0 +1,27 @@ +// 模型路由配置(统一走云雾API中转) +const YUNWU_API_URL = 'https://api.deepseek.com/v1/chat/completions'; +const YUNWU_KEY = process.env.YUNWU_API_KEY; + +const MODELS = { + deepseek: { + name: 'DeepSeek Chat', + apiUrl: YUNWU_API_URL, + apiKey: YUNWU_KEY, + model: 'deepseek-chat', + maxTokens: 4096 + }, + gpt4o_mini: { + name: 'GPT-4o-mini', + apiUrl: YUNWU_API_URL, + apiKey: YUNWU_KEY, + model: 'gpt-4o-mini', + maxTokens: 4096 + } +}; + +const DEFAULT_MODEL = 'deepseek'; + +module.exports = { + MODELS, + DEFAULT_MODEL +}; diff --git a/backend/config/routes/feishu.js b/backend/config/routes/feishu.js new file mode 100644 index 00000000..ca1b2864 --- /dev/null +++ b/backend/config/routes/feishu.js @@ -0,0 +1,52 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const FEISHU_APP_ID = process.env.FEISHU_APP_ID; +const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET; +const BASE_URL = 'https://open.feishu.cn/open-apis'; + +// 获取飞书 tenant_access_token +async function getToken() { + const response = await axios.post( + BASE_URL + '/auth/v3/tenant_access_token/internal', + { + app_id: FEISHU_APP_ID, + app_secret: FEISHU_APP_SECRET + } + ); + return response.data.tenant_access_token; +} + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: '飞书路由正常', + app_id_configured: !!FEISHU_APP_ID, + app_secret_configured: !!FEISHU_APP_SECRET + }); +}); + +// 发送消息到飞书群 +router.post('/broadcast', async (req, res) => { + try { + const { chat_id, title, content } = req.body; + const token = await getToken(); + const response = await axios.post( + BASE_URL + '/im/v1/messages?receive_id_type=chat_id', + { + receive_id: chat_id, + msg_type: 'text', + content: JSON.stringify({ text: title + '\n\n' + content }) + }, + { + headers: { 'Authorization': 'Bearer ' + token } + } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/config/routes/notion.js b/backend/config/routes/notion.js new file mode 100644 index 00000000..f6f0dc3d --- /dev/null +++ b/backend/config/routes/notion.js @@ -0,0 +1,61 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const NOTION_TOKEN = process.env.NOTION_TOKEN; +const NOTION_VERSION = '2022-06-28'; +const BASE_URL = 'https://api.notion.com/v1'; + +const headers = () => ({ + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION +}); + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: 'Notion 路由正常', + token_configured: !!NOTION_TOKEN + }); +}); + +router.get('/database/:id', async (req, res) => { + try { + const response = await axios.post( + BASE_URL + '/databases/' + req.params.id + '/query', + {}, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +router.get('/page/:id', async (req, res) => { + try { + const response = await axios.get( + BASE_URL + '/pages/' + req.params.id, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +router.post('/page', async (req, res) => { + try { + const response = await axios.post( + BASE_URL + '/pages', + req.body, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/config_副本/models.js b/backend/config_副本/models.js new file mode 100644 index 00000000..6ef24385 --- /dev/null +++ b/backend/config_副本/models.js @@ -0,0 +1,27 @@ +// 模型路由配置(统一走云雾API中转) +const YUNWU_API_URL = 'https://api.deepseek.com/v1/chat/completions'; +const YUNWU_KEY = process.env.YUNWU_API_KEY; + +const MODELS = { + deepseek: { + name: 'DeepSeek Chat', + apiUrl: YUNWU_API_URL, + apiKey: YUNWU_KEY, + model: 'deepseek-chat', + maxTokens: 4096 + }, + gpt4o_mini: { + name: 'GPT-4o-mini', + apiUrl: YUNWU_API_URL, + apiKey: YUNWU_KEY, + model: 'gpt-4o-mini', + maxTokens: 4096 + } +}; + +const DEFAULT_MODEL = 'deepseek'; + +module.exports = { + MODELS, + DEFAULT_MODEL +}; diff --git a/backend/config_副本/routes/feishu.js b/backend/config_副本/routes/feishu.js new file mode 100644 index 00000000..ca1b2864 --- /dev/null +++ b/backend/config_副本/routes/feishu.js @@ -0,0 +1,52 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const FEISHU_APP_ID = process.env.FEISHU_APP_ID; +const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET; +const BASE_URL = 'https://open.feishu.cn/open-apis'; + +// 获取飞书 tenant_access_token +async function getToken() { + const response = await axios.post( + BASE_URL + '/auth/v3/tenant_access_token/internal', + { + app_id: FEISHU_APP_ID, + app_secret: FEISHU_APP_SECRET + } + ); + return response.data.tenant_access_token; +} + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: '飞书路由正常', + app_id_configured: !!FEISHU_APP_ID, + app_secret_configured: !!FEISHU_APP_SECRET + }); +}); + +// 发送消息到飞书群 +router.post('/broadcast', async (req, res) => { + try { + const { chat_id, title, content } = req.body; + const token = await getToken(); + const response = await axios.post( + BASE_URL + '/im/v1/messages?receive_id_type=chat_id', + { + receive_id: chat_id, + msg_type: 'text', + content: JSON.stringify({ text: title + '\n\n' + content }) + }, + { + headers: { 'Authorization': 'Bearer ' + token } + } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/config_副本/routes/notion.js b/backend/config_副本/routes/notion.js new file mode 100644 index 00000000..f6f0dc3d --- /dev/null +++ b/backend/config_副本/routes/notion.js @@ -0,0 +1,61 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const NOTION_TOKEN = process.env.NOTION_TOKEN; +const NOTION_VERSION = '2022-06-28'; +const BASE_URL = 'https://api.notion.com/v1'; + +const headers = () => ({ + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION +}); + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: 'Notion 路由正常', + token_configured: !!NOTION_TOKEN + }); +}); + +router.get('/database/:id', async (req, res) => { + try { + const response = await axios.post( + BASE_URL + '/databases/' + req.params.id + '/query', + {}, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +router.get('/page/:id', async (req, res) => { + try { + const response = await axios.get( + BASE_URL + '/pages/' + req.params.id, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +router.post('/page', async (req, res) => { + try { + const response = await axios.post( + BASE_URL + '/pages', + req.body, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/hololake.zip b/backend/hololake.zip new file mode 100644 index 00000000..f052e45f Binary files /dev/null and b/backend/hololake.zip differ diff --git a/backend/hololake_副本.zip b/backend/hololake_副本.zip new file mode 100644 index 00000000..f052e45f Binary files /dev/null and b/backend/hololake_副本.zip differ diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 00000000..7e5cb68a --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,999 @@ +{ + "name": "hololake-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hololake-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.13.6", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.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/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "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": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/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/form-data/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/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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "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/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" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/backend/package-lock_副本.json b/backend/package-lock_副本.json new file mode 100644 index 00000000..7e5cb68a --- /dev/null +++ b/backend/package-lock_副本.json @@ -0,0 +1,999 @@ +{ + "name": "hololake-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hololake-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.13.6", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.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/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "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": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/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/form-data/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/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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "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/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" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 00000000..67893707 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,19 @@ +{ + "name": "hololake-backend", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.13.6", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1" + } +} diff --git a/backend/package_副本.json b/backend/package_副本.json new file mode 100644 index 00000000..67893707 --- /dev/null +++ b/backend/package_副本.json @@ -0,0 +1,19 @@ +{ + "name": "hololake-backend", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.13.6", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1" + } +} diff --git a/backend/routes/coldstart.js b/backend/routes/coldstart.js new file mode 100644 index 00000000..b231b885 --- /dev/null +++ b/backend/routes/coldstart.js @@ -0,0 +1,134 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +// 冷启动热身接口 +router.post('/', async (req, res) => { + try { + // 并行执行四项检查 + const [notion, primary, fallback, feishu] = await Promise.allSettled([ + checkNotion(), + checkPrimaryAPI(), + checkFallbackAPI(), + checkFeishu() + ]); + + const result = { + status: 'ok', + message: '冷启动热身完成', + checks: { + notion_connection: notion.status === 'fulfilled' ? notion.value : false, + primary_api: primary.status === 'fulfilled' ? primary.value : false, + fallback_api: fallback.status === 'fulfilled' ? fallback.value : false, + feishu_connection: feishu.status === 'fulfilled' ? feishu.value : false + }, + timestamp: new Date().toISOString() + }; + + res.json(result); + } catch (err) { + res.status(500).json({ + status: 'error', + message: '冷启动检查失败', + error: err.message + }); + } +}); + +// 检查 Notion 连接 +async function checkNotion() { + try { + const token = process.env.NOTION_TOKEN; + if (!token) return false; + // 轻量检查:读取 Notion 用户信息 + const response = await axios.get('https://api.notion.com/v1/users/me', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Notion-Version': '2022-06-28' + }, + timeout: 5000 + }); + return response.status === 200; + } catch { + return false; + } +} + +// 检查主 API +async function checkPrimaryAPI() { + try { + const key = process.env.PRIMARY_API_KEY; + if (!key) return false; + // 轻量检查:调用一个简单的测试接口 + const response = await axios.post( + 'https://api.deepseek.com/v1/chat/completions', + { + model: 'deepseek-chat', + messages: [{ role: 'user', content: 'ping' }], + max_tokens: 5 + }, + { + headers: { + 'Authorization': `Bearer ${key}`, + 'Content-Type': 'application/json' + }, + timeout: 5000 + } + ); + return response.status === 200; + } catch { + return false; + } +} + +// 检查备用 API +async function checkFallbackAPI() { + try { + const key = process.env.FALLBACK_API_KEY; + if (!key) return false; + const response = await axios.post( + 'https://api.deepseek.com/v1/chat/completions', + { + model: 'deepseek-chat', + messages: [{ role: 'user', content: 'ping' }], + max_tokens: 5 + }, + { + headers: { + 'Authorization': `Bearer ${key}`, + 'Content-Type': 'application/json' + }, + timeout: 5000 + } + ); + return response.status === 200; + } catch { + return false; + } +} + +// 检查飞书连接 +async function checkFeishu() { + try { + const appId = process.env.FEISHU_APP_ID; + const appSecret = process.env.FEISHU_APP_SECRET; + if (!appId || !appSecret) return false; + // 获取 tenant_access_token + const response = await axios.post( + 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', + { + app_id: appId, + app_secret: appSecret + }, + { + headers: { 'Content-Type': 'application/json' }, + timeout: 5000 + } + ); + return response.data.code === 0; + } catch { + return false; + } +} + +module.exports = router; diff --git a/backend/routes/feishu.js b/backend/routes/feishu.js new file mode 100644 index 00000000..85467da7 --- /dev/null +++ b/backend/routes/feishu.js @@ -0,0 +1,52 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const FEISHU_APP_ID = process.env.FEISHU_APP_ID; +const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET; +const BASE_URL = 'https://open.feishu.cn/open-apis'; + +// 获取飞书 tenant_access_token +async function getToken() { + const response = await axios.post( + BASE_URL + '/auth/v3/tenant_access_token/internal', + { + app_id: FEISHU_APP_ID, + app_secret: FEISHU_APP_SECRET + } + ); + return response.data.tenant_access_token; +} + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: '飞书路由正常', + app_id_configured: !!FEISHU_APP_ID, + app_secret_configured: !!FEISHU_APP_SECRET + }); +}); + +// 发送消息到飞书群 +router.post('/broadcast', async (req, res) => { + try { + const { chat_id, title, content } = req.body; + const token = await getToken(); + const response = await axios.post( + BASE_URL + '/im/v1/messages?receive_id_type=chat_id', + { + receive_id: chat_id, + msg_type: 'text', + content: JSON.stringify({ text: title + '\n\n' + content }) + }, + { + headers: { 'Authorization': 'Bearer ' + token } + } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +module.exports = router; diff --git a/backend/routes/notion.js b/backend/routes/notion.js new file mode 100644 index 00000000..2574f4f8 --- /dev/null +++ b/backend/routes/notion.js @@ -0,0 +1,61 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const NOTION_TOKEN = process.env.NOTION_TOKEN; +const NOTION_VERSION = '2022-06-28'; +const BASE_URL = 'https://api.notion.com/v1'; + +const headers = () => ({ + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION +}); + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: 'Notion 路由正常', + token_configured: !!NOTION_TOKEN + }); +}); + +router.get('/database/:id', async (req, res) => { + try { + const response = await axios.post( + BASE_URL + '/databases/' + req.params.id + '/query', + {}, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +router.get('/page/:id', async (req, res) => { + try { + const response = await axios.get( + BASE_URL + '/pages/' + req.params.id, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +router.post('/page', async (req, res) => { + try { + const response = await axios.post( + BASE_URL + '/pages', + req.body, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +module.exports = router; diff --git a/backend/routes/router.js b/backend/routes/router.js new file mode 100644 index 00000000..1b759f8a --- /dev/null +++ b/backend/routes/router.js @@ -0,0 +1,106 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); +const { MODELS, DEFAULT_MODEL } = require('../config/models'); + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: '模型路由正常 ❤️ (支持主备自动切换)', + available_models: Object.keys(MODELS), + default_model: DEFAULT_MODEL + }); +}); + +// 带 fallback 的 API 调用函数 +async function callAPIWithFallback(model, message) { + // 第一次尝试:主密钥 + try { + console.log(`[API] 尝试使用主密钥调用 ${model.name}`); + const response = await axios.post( + model.apiUrl, + { + model: model.model, + messages: [{ role: 'user', content: message }], + max_tokens: model.maxTokens + }, + { + headers: { + 'Authorization': 'Bearer ' + model.primaryApiKey, + 'Content-Type': 'application/json' + } + } + ); + return { success: true, data: response.data }; + } catch (primaryErr) { + // 判断是否需要 fallback(余额不足/超时/401/403) + const shouldFallback = + primaryErr.response?.status === 401 || + primaryErr.response?.status === 403 || + primaryErr.response?.data?.error?.type === 'insufficient_quota' || + primaryErr.code === 'ECONNABORTED' || + primaryErr.message.includes('timeout'); + + if (!shouldFallback || !model.fallbackApiKey) { + // 不需要 fallback 或没有备用密钥,直接抛出错误 + throw primaryErr; + } + + // 需要 fallback,尝试备用密钥 + console.log(`[API Fallback] 从主密钥切换到备用密钥 (${model.name})`); + try { + const fallbackResponse = await axios.post( + model.apiUrl, + { + model: model.model, + messages: [{ role: 'user', content: message }], + max_tokens: model.maxTokens + }, + { + headers: { + 'Authorization': 'Bearer ' + model.fallbackApiKey, + 'Content-Type': 'application/json' + } + } + ); + return { success: true, data: fallbackResponse.data, fallback: true }; + } catch (fallbackErr) { + // 备用也失败了,抛出备用错误 + throw fallbackErr; + } + } +} + +router.post('/chat', async (req, res) => { + try { + const { message, model_name } = req.body; + const model = MODELS[model_name || DEFAULT_MODEL]; + + if (!model) { + return res.status(400).json({ error: '未知模型: ' + model_name }); + } + + if (!model.primaryApiKey) { + return res.status(503).json({ + error: '模型 ' + model.name + ' 的主 API Key 尚未配置', + hint: '请检查 .env 文件中的 PRIMARY_API_KEY' + }); + } + + const result = await callAPIWithFallback(model, message); + + res.json({ + model: model.name, + reply: result.data.choices[0].message.content, + fallback_used: result.fallback || false + }); + } catch (err) { + console.error('[API Error]', err.message); + res.status(500).json({ + error: err.message, + detail: err.response ? err.response.data : null + }); + } +}); + +module.exports = router; diff --git a/backend/routes/router.js.bak b/backend/routes/router.js.bak new file mode 100644 index 00000000..2189677c --- /dev/null +++ b/backend/routes/router.js.bak @@ -0,0 +1,58 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); +const { MODELS, DEFAULT_MODEL } = require('../config/models'); + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: '模型路由正常 ❤️', + available_models: Object.keys(MODELS), + default_model: DEFAULT_MODEL + }); +}); + +router.post('/chat', async (req, res) => { + try { + const { message, model_name } = req.body; + const model = MODELS[model_name || DEFAULT_MODEL]; + + if (!model) { + return res.status(400).json({ error: '未知模型: ' + model_name }); + } + + if (!model.apiKey) { + return res.status(503).json({ + error: '模型 ' + model.name + ' 的 API Key 尚未配置', + hint: '请检查 .env 文件中的 YUNWU_API_KEY' + }); + } + + const response = await axios.post( + model.apiUrl, + { + model: model.model, + messages: [{ role: 'user', content: message }], + max_tokens: model.maxTokens + }, + { + headers: { + 'Authorization': 'Bearer ' + model.apiKey, + 'Content-Type': 'application/json' + } + } + ); + + res.json({ + model: model.name, + reply: response.data.choices[0].message.content + }); + } catch (err) { + res.status(500).json({ + error: err.message, + detail: err.response ? err.response.data : null + }); + } +}); + +module.exports = router; diff --git a/backend/routes_副本/feishu.js b/backend/routes_副本/feishu.js new file mode 100644 index 00000000..85467da7 --- /dev/null +++ b/backend/routes_副本/feishu.js @@ -0,0 +1,52 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const FEISHU_APP_ID = process.env.FEISHU_APP_ID; +const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET; +const BASE_URL = 'https://open.feishu.cn/open-apis'; + +// 获取飞书 tenant_access_token +async function getToken() { + const response = await axios.post( + BASE_URL + '/auth/v3/tenant_access_token/internal', + { + app_id: FEISHU_APP_ID, + app_secret: FEISHU_APP_SECRET + } + ); + return response.data.tenant_access_token; +} + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: '飞书路由正常', + app_id_configured: !!FEISHU_APP_ID, + app_secret_configured: !!FEISHU_APP_SECRET + }); +}); + +// 发送消息到飞书群 +router.post('/broadcast', async (req, res) => { + try { + const { chat_id, title, content } = req.body; + const token = await getToken(); + const response = await axios.post( + BASE_URL + '/im/v1/messages?receive_id_type=chat_id', + { + receive_id: chat_id, + msg_type: 'text', + content: JSON.stringify({ text: title + '\n\n' + content }) + }, + { + headers: { 'Authorization': 'Bearer ' + token } + } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +module.exports = router; diff --git a/backend/routes_副本/notion.js b/backend/routes_副本/notion.js new file mode 100644 index 00000000..2574f4f8 --- /dev/null +++ b/backend/routes_副本/notion.js @@ -0,0 +1,61 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const NOTION_TOKEN = process.env.NOTION_TOKEN; +const NOTION_VERSION = '2022-06-28'; +const BASE_URL = 'https://api.notion.com/v1'; + +const headers = () => ({ + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION +}); + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: 'Notion 路由正常', + token_configured: !!NOTION_TOKEN + }); +}); + +router.get('/database/:id', async (req, res) => { + try { + const response = await axios.post( + BASE_URL + '/databases/' + req.params.id + '/query', + {}, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +router.get('/page/:id', async (req, res) => { + try { + const response = await axios.get( + BASE_URL + '/pages/' + req.params.id, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +router.post('/page', async (req, res) => { + try { + const response = await axios.post( + BASE_URL + '/pages', + req.body, + { headers: headers() } + ); + res.json(response.data); + } catch (err) { + res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null }); + } +}); + +module.exports = router; diff --git a/backend/routes_副本/router.js b/backend/routes_副本/router.js new file mode 100644 index 00000000..2189677c --- /dev/null +++ b/backend/routes_副本/router.js @@ -0,0 +1,58 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); +const { MODELS, DEFAULT_MODEL } = require('../config/models'); + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: '模型路由正常 ❤️', + available_models: Object.keys(MODELS), + default_model: DEFAULT_MODEL + }); +}); + +router.post('/chat', async (req, res) => { + try { + const { message, model_name } = req.body; + const model = MODELS[model_name || DEFAULT_MODEL]; + + if (!model) { + return res.status(400).json({ error: '未知模型: ' + model_name }); + } + + if (!model.apiKey) { + return res.status(503).json({ + error: '模型 ' + model.name + ' 的 API Key 尚未配置', + hint: '请检查 .env 文件中的 YUNWU_API_KEY' + }); + } + + const response = await axios.post( + model.apiUrl, + { + model: model.model, + messages: [{ role: 'user', content: message }], + max_tokens: model.maxTokens + }, + { + headers: { + 'Authorization': 'Bearer ' + model.apiKey, + 'Content-Type': 'application/json' + } + } + ); + + res.json({ + model: model.name, + reply: response.data.choices[0].message.content + }); + } catch (err) { + res.status(500).json({ + error: err.message, + detail: err.response ? err.response.data : null + }); + } +}); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 00000000..4ba0248e --- /dev/null +++ b/backend/server.js @@ -0,0 +1,42 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +// 路由引入 +const notionRoutes = require('./routes/notion'); +const feishuRoutes = require('./routes/feishu'); +const routerRoutes = require('./routes/router'); +const coldstartRoutes = require('./routes/coldstart'); +const hliRoutes = require('../src/routes/hli'); + +app.use('/notion', notionRoutes); +app.use('/feishu', feishuRoutes); +app.use('/router', routerRoutes); +app.use('/api/coldstart', coldstartRoutes); +app.use('/hli', hliRoutes); + +app.get('/', (req, res) => { + res.json({ + status: 'ok', + message: 'HoloLake 后端服务运行中', + version: '0.2.0', + routes: ['/notion/test', '/feishu/test', '/router/test', '/router/chat', '/api/coldstart', '/hli/test'] + }); +}); + +const PORT = process.env.PORT || 3000; +// 飞书 Webhook 处理 +app.post('/webhook/feishu', (req, res) => { + console.log('收到飞书请求:', req.body); + if (req.body.challenge) { + return res.json({ challenge: req.body.challenge }); + } + res.json({ message: 'received' }); +}); +app.listen(PORT, () => { + console.log(`🚀 服务启动成功,端口:${PORT}`); +}); diff --git a/backend/server.js.bak b/backend/server.js.bak new file mode 100644 index 00000000..b58be470 --- /dev/null +++ b/backend/server.js.bak @@ -0,0 +1,38 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +// 路由引入 +const notionRoutes = require('./routes/notion'); +const feishuRoutes = require('./routes/feishu'); +const routerRoutes = require('./routes/router'); + +app.use('/notion', notionRoutes); +app.use('/feishu', feishuRoutes); +app.use('/router', routerRoutes); + +app.get('/', (req, res) => { + res.json({ + status: 'ok', + message: 'HoloLake 后端服务运行中', + version: '0.2.0', + routes: ['/notion/test', '/feishu/test', '/router/test', '/router/chat'] + }); +}); + +const PORT = process.env.PORT || 3000; +// 飞书 Webhook 处理 +app.post('/webhook/feishu', (req, res) => { + console.log('收到飞书请求:', req.body); + if (req.body.challenge) { + return res.json({ challenge: req.body.challenge }); + } + res.json({ message: 'received' }); +}); +app.listen(PORT, () => { + console.log(`🚀 服务启动成功,端口:${PORT}`); +}); diff --git a/backend/server.js.bak2 b/backend/server.js.bak2 new file mode 100644 index 00000000..f89f68cf --- /dev/null +++ b/backend/server.js.bak2 @@ -0,0 +1,40 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +// 路由引入 +const notionRoutes = require('./routes/notion'); +const feishuRoutes = require('./routes/feishu'); +const routerRoutes = require('./routes/router'); +const coldstartRoutes = require('./routes/coldstart'); + +app.use('/notion', notionRoutes); +app.use('/feishu', feishuRoutes); +app.use('/router', routerRoutes); +app.use('/api/coldstart', coldstartRoutes); + +app.get('/', (req, res) => { + res.json({ + status: 'ok', + message: 'HoloLake 后端服务运行中', + version: '0.2.0', + routes: ['/notion/test', '/feishu/test', '/router/test', '/router/chat', '/api/coldstart'] + }); +}); + +const PORT = process.env.PORT || 3000; +// 飞书 Webhook 处理 +app.post('/webhook/feishu', (req, res) => { + console.log('收到飞书请求:', req.body); + if (req.body.challenge) { + return res.json({ challenge: req.body.challenge }); + } + res.json({ message: 'received' }); +}); +app.listen(PORT, () => { + console.log(`🚀 服务启动成功,端口:${PORT}`); +}); diff --git a/backend/server_副本.js b/backend/server_副本.js new file mode 100644 index 00000000..b58be470 --- /dev/null +++ b/backend/server_副本.js @@ -0,0 +1,38 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +// 路由引入 +const notionRoutes = require('./routes/notion'); +const feishuRoutes = require('./routes/feishu'); +const routerRoutes = require('./routes/router'); + +app.use('/notion', notionRoutes); +app.use('/feishu', feishuRoutes); +app.use('/router', routerRoutes); + +app.get('/', (req, res) => { + res.json({ + status: 'ok', + message: 'HoloLake 后端服务运行中', + version: '0.2.0', + routes: ['/notion/test', '/feishu/test', '/router/test', '/router/chat'] + }); +}); + +const PORT = process.env.PORT || 3000; +// 飞书 Webhook 处理 +app.post('/webhook/feishu', (req, res) => { + console.log('收到飞书请求:', req.body); + if (req.body.challenge) { + return res.json({ challenge: req.body.challenge }); + } + res.json({ message: 'received' }); +}); +app.listen(PORT, () => { + console.log(`🚀 服务启动成功,端口:${PORT}`); +}); diff --git a/chat-bubble/README.md b/chat-bubble/README.md new file mode 100644 index 00000000..e4e9df2e --- /dev/null +++ b/chat-bubble/README.md @@ -0,0 +1 @@ +# chat-bubble 模块 diff --git a/cloud-drive/README.md b/cloud-drive/README.md new file mode 100644 index 00000000..7fa7ff6c --- /dev/null +++ b/cloud-drive/README.md @@ -0,0 +1 @@ +# cloud-drive 模块 diff --git a/coldstart/README.md b/coldstart/README.md new file mode 100644 index 00000000..8633a457 --- /dev/null +++ b/coldstart/README.md @@ -0,0 +1 @@ +# coldstart 模块 diff --git a/cost-control/README.md b/cost-control/README.md new file mode 100644 index 00000000..e60a0db7 --- /dev/null +++ b/cost-control/README.md @@ -0,0 +1 @@ +# cost-control 模块 diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 00000000..c6909768 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1 @@ +# dashboard 模块 diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000..6c739343 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +guanghulab.com \ No newline at end of file diff --git a/docs/HoloLake-Era-OS-Modules.md b/docs/HoloLake-Era-OS-Modules.md index 1d4708a7..0947e799 100644 --- a/docs/HoloLake-Era-OS-Modules.md +++ b/docs/HoloLake-Era-OS-Modules.md @@ -1,6 +1,6 @@ # HoloLake Era 操作系统部署模块 -> 📋 **自动生成文档** · 铸渊(ZhùYuān)维护 · 最后更新:2026-03-07 05:14 UTC +> 📋 **自动生成文档** · 铸渊(ZhùYuān)维护 · 最后更新:2026-03-07 11:38 UTC > > 本文档由 GitHub Actions 自动触发生成,每当合作者上传/更新模块时自动刷新。 > 按合作者编号(DEV-XXX)整理所有已上传模块。 @@ -41,6 +41,8 @@ ``` backend-integration/ README.md + api-proxy.js + nginx-api-proxy.conf ``` --- @@ -208,7 +210,7 @@ m12-kanban/ README.md ``` -### 📦 status-board +### 📦 status-board 模块 | 字段 | 内容 | |------|------| @@ -224,6 +226,7 @@ m12-kanban/ ``` status-board/ + README.md api-config.js api.js index.html @@ -329,7 +332,7 @@ m11-module/ | 已上传模块数 | 12 | | 待上传模块数 | 0 | | 上传完成率 | 100% | -| 文档更新时间 | 2026-03-07 05:14 UTC | +| 文档更新时间 | 2026-03-07 11:38 UTC | --- diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..f3a02903 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +# docs 模块 diff --git a/docs/index.html b/docs/index.html index 4eb9924e..e966da1b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,9 +1,9 @@ - + - + @@ -17,19 +17,20 @@ --text:#e2e8f0;--dim:#64748b;--muted:#1e293b; --mc:#4f8ef7;--mb:#fb923c;--mr:#a78bfa;--mn:#34d399; --r:14px;--rs:8px; + --ls-w:260px;--rs-w:360px; } -html,body{height:100%;background:var(--bg);color:var(--text);font-family:'PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;font-size:15px;line-height:1.6} +html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;font-size:15px;line-height:1.6;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;overscroll-behavior:none} /* ─── SETUP ─── */ -#setup{position:fixed;inset:0;z-index:1000;background:var(--bg);display:flex;align-items:center;justify-content:center;padding:20px} -.sc{background:var(--s1);border:1px solid var(--border);border-radius:20px;padding:40px;width:100%;max-width:460px;box-shadow:0 25px 60px rgba(0,0,0,.5)} +#setup{position:fixed;inset:0;z-index:1000;background:var(--bg);display:flex;align-items:center;justify-content:center;padding:20px;padding:max(20px,env(safe-area-inset-top)) max(20px,env(safe-area-inset-right)) max(20px,env(safe-area-inset-bottom)) max(20px,env(safe-area-inset-left));overflow-y:auto;-webkit-overflow-scrolling:touch} +.sc{background:var(--s1);border:1px solid var(--border);border-radius:20px;padding:40px;width:100%;max-width:460px;box-shadow:0 25px 60px rgba(0,0,0,.5);margin:auto} .sc-logo{font-size:52px;text-align:center;margin-bottom:14px} .sc h1{font-size:22px;text-align:center;margin-bottom:6px} .sc .sub{color:var(--dim);text-align:center;font-size:13px;margin-bottom:22px} .sc-sec{background:rgba(52,211,153,.08);border:1px solid rgba(52,211,153,.3);border-radius:var(--rs);padding:9px 12px;font-size:12px;color:var(--ok);margin-bottom:20px;text-align:center} .fg{margin-bottom:16px} .fg label{display:block;font-size:11px;color:var(--dim);margin-bottom:6px;font-weight:600;text-transform:uppercase;letter-spacing:.05em} -.fg input,.fg select{width:100%;padding:10px 12px;background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);color:var(--text);font-size:14px;font-family:inherit;outline:none;transition:border-color .2s} +.fg input,.fg select{width:100%;padding:10px 12px;background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);color:var(--text);font-size:16px;font-family:inherit;outline:none;transition:border-color .2s} .fg input:focus,.fg select:focus{border-color:var(--accent)} .fg select option{background:var(--s2)} .fg small{display:block;color:var(--dim);font-size:11px;margin-top:5px} @@ -37,10 +38,93 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:'PingFa .sc-btn:hover{opacity:.85} .sc-skip{text-align:center;font-size:12px;color:var(--dim);margin-top:12px} .sc-skip a{color:var(--accent);text-decoration:none;cursor:pointer} +.login-tabs{display:flex;gap:0;margin-bottom:20px;border:1px solid var(--border);border-radius:var(--rs);overflow:hidden} +.login-tab{flex:1;padding:10px;text-align:center;background:var(--s2);color:var(--dim);font-size:13px;font-weight:600;cursor:pointer;border:none;font-family:inherit;transition:all .2s} +.login-tab.active{background:var(--accent);color:#fff} +.login-tab:hover:not(.active){background:var(--s3);color:var(--text)} +.login-mode{display:none} +.login-mode.active{display:block} +#id-preview{display:none;text-align:center;margin-bottom:10px;padding:10px;background:var(--s3);border-radius:8px;font-size:14px;font-weight:600} -/* ─── APP SHELL ─── */ -#app{display:none;flex-direction:column;height:100vh;max-width:820px;margin:0 auto} -#app.on{display:flex} +/* ─── APP SHELL (three-column) ─── */ +#app{display:none;height:100vh;height:100dvh;height:var(--vh,100dvh);width:100%;overflow:hidden} +#app.on{display:flex;flex-direction:column} + +/* ─── LEFT SIDEBAR ─── */ +#left-sidebar{width:var(--ls-w);flex-shrink:0;background:var(--s1);border-right:1px solid var(--border);display:none;flex-direction:column;overflow:hidden;z-index:120} +.ls-header{padding:16px;border-bottom:1px solid var(--border)} +.ls-user{display:flex;align-items:center;gap:10px} +.ls-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--a2));display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px;flex-shrink:0} +.ls-name{font-size:14px;font-weight:600} +.ls-role{font-size:11px;color:var(--dim)} +.ls-devid{font-size:10px;color:var(--accent);background:var(--adim);padding:1px 6px;border-radius:4px;display:inline-block;margin-top:2px} +.ls-nav{padding:8px;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:2px} +.nav-tab{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:var(--rs);background:transparent;border:none;color:var(--dim);font-size:13px;font-weight:500;cursor:pointer;transition:all .2s;text-align:left;font-family:inherit;width:100%} +.nav-tab:hover{background:var(--s2);color:var(--text)} +.nav-tab.active{background:var(--adim);color:var(--accent);font-weight:600} +.ls-new-conv{margin:8px;padding:10px;border-radius:var(--rs);border:1px dashed var(--border);background:transparent;color:var(--dim);font-size:13px;cursor:pointer;transition:all .2s;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:6px} +.ls-new-conv:hover{border-color:var(--accent);color:var(--accent);background:var(--adim)} +.ls-conv-list{flex:1;overflow-y:auto;padding:4px 8px;-webkit-overflow-scrolling:touch} +.ls-conv-list::-webkit-scrollbar{width:3px} +.ls-conv-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} +.conv-item{padding:10px;border-radius:var(--rs);cursor:pointer;transition:all .2s;position:relative;margin-bottom:2px;border:1px solid transparent} +.conv-item:hover{background:var(--s2);border-color:var(--border)} +.conv-item.conv-active{background:var(--adim);border-color:var(--accent)} +.conv-title{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:20px} +.conv-time{font-size:10px;color:var(--dim);margin-top:2px} +.conv-del{position:absolute;top:8px;right:6px;background:transparent;border:none;color:var(--dim);font-size:11px;cursor:pointer;padding:2px 4px;border-radius:4px;opacity:0;transition:all .2s} +.conv-item:hover .conv-del{opacity:1} +.conv-del:hover{color:var(--err);background:rgba(248,113,113,.1)} +.conv-empty{padding:20px 12px;text-align:center;font-size:12px;color:var(--dim)} +.ls-footer{padding:8px;border-top:1px solid var(--border)} +.ls-footer-btn{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:var(--rs);background:transparent;border:none;color:var(--dim);font-size:13px;cursor:pointer;transition:all .2s;width:100%;font-family:inherit;text-align:left} +.ls-footer-btn:hover{background:var(--s2);color:var(--text)} + +/* ─── LEFT SIDEBAR MOBILE OVERLAY ─── */ +#ls-overlay{display:none;position:fixed;inset:0;z-index:110;background:rgba(0,0,0,.5);-webkit-tap-highlight-color:transparent} +#ls-overlay.on{display:block} + +/* ─── CENTER AREA ─── */ +#center-area{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden} + +/* ─── CHAT VIEW ─── */ +#chat-view{display:flex;flex-direction:column;flex:1;min-height:0} + +/* ─── ANNOUNCEMENTS VIEW ─── */ +#announcements-view{display:none;flex-direction:column;flex:1;overflow-y:auto;background:var(--bg);-webkit-overflow-scrolling:touch} +.ann-header{padding:20px 24px 12px;border-bottom:1px solid var(--border);background:var(--s1);flex-shrink:0} +.ann-header h2{font-size:18px;font-weight:700} +.ann-header-sub{font-size:12px;color:var(--dim);margin-top:4px} +.ann-scroll{flex:1;overflow-y:auto;padding:20px 24px} +.ann-scroll::-webkit-scrollbar{width:4px} +.ann-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px} +.ann-section{margin-bottom:20px} +.ann-section-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:10px;display:flex;align-items:center;gap:6px} +.ann-card{background:var(--s1);border:1px solid var(--border);border-radius:12px;padding:14px;transition:border-color .2s} +.ann-card:hover{border-color:rgba(79,142,247,.3)} +.ann-item{padding:8px 0;border-bottom:1px solid rgba(30,50,81,.4);font-size:13px;line-height:1.7;display:flex;align-items:flex-start;gap:6px} +.ann-item:last-child{border-bottom:none} +.ann-date{font-size:10px;color:var(--dim);white-space:nowrap;margin-left:auto} +.ann-actions{display:flex;flex-wrap:wrap;gap:8px} +.ann-action-btn{padding:10px 16px;border-radius:var(--rs);background:var(--s1);border:1px solid var(--border);color:var(--text);font-size:13px;cursor:pointer;transition:all .2s;font-family:inherit} +.ann-action-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--adim)} + +/* ─── RIGHT SIDEBAR ─── */ +#right-sidebar{width:var(--rs-w);flex-shrink:0;background:var(--s1);border-left:1px solid var(--border);display:none;flex-direction:column;overflow:hidden} +.rs-header{padding:14px 16px;border-bottom:1px solid var(--border);flex-shrink:0} +.rs-header h3{font-size:14px;font-weight:600} +.rs-body{flex:1;overflow-y:auto;padding:14px;-webkit-overflow-scrolling:touch} +.rs-body::-webkit-scrollbar{width:3px} +.rs-body::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} +.rs-section{margin-bottom:16px} +.rs-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--border)} +.rs-module-card{padding:8px 10px;background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);margin-bottom:6px;font-size:12px} +.rs-module-name{font-weight:600;font-size:12px} +.rs-module-status{font-size:11px;color:var(--dim);margin-top:2px} +.rs-empty{font-size:12px;color:var(--dim);padding:10px 0} + +/* ─── HAMBURGER ─── */ +.hamburger-btn{display:none !important} /* ─── HEADER ─── */ header{display:flex;align-items:center;justify-content:space-between;padding:11px 18px;background:var(--s1);border-bottom:1px solid var(--border);flex-shrink:0;gap:10px} @@ -56,10 +140,10 @@ header{display:flex;align-items:center;justify-content:space-between;padding:11p /* ─── BRAIN BAR ─── */ #bbar{padding:5px 18px;background:var(--s2);border-bottom:1px solid var(--border);font-size:11px;color:var(--dim);display:flex;gap:14px;flex-wrap:wrap;flex-shrink:0;align-items:center} -.demo-w{color:var(--warn);background:rgba(251,191,36,.1);padding:1px 8px;border-radius:4px} +.demo-w{color:var(--accent);background:rgba(79,142,247,.1);padding:1px 8px;border-radius:4px} /* ─── MESSAGES ─── */ -#msgs{flex:1;overflow-y:auto;padding:18px;display:flex;flex-direction:column;gap:14px} +#msgs{flex:1;overflow-y:auto;padding:18px;display:flex;flex-direction:column;gap:14px;-webkit-overflow-scrolling:touch;overscroll-behavior:contain} #msgs::-webkit-scrollbar{width:4px} #msgs::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px} .mrow{display:flex;gap:9px;animation:fu .3s ease} @@ -105,23 +189,23 @@ header{display:flex;align-items:center;justify-content:space-between;padding:11p .qb:hover{border-color:var(--accent);color:var(--accent)} /* ─── FOOTER ─── */ -footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--border);flex-shrink:0} +footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom));background:var(--s1);border-top:1px solid var(--border);flex-shrink:0} .ir{display:flex;align-items:flex-end;gap:8px} .mpill{width:36px;height:36px;border-radius:50%;background:var(--s2);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:17px;cursor:pointer;flex-shrink:0;transition:all .2s;position:relative} .mpill:hover{border-color:var(--accent)} .mpill-tip{position:absolute;bottom:44px;left:50%;transform:translateX(-50%);background:var(--s3);border:1px solid var(--border);border-radius:8px;padding:4px 10px;font-size:11px;color:var(--text);white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .2s} .mpill:hover .mpill-tip{opacity:1} -#inp{flex:1;min-height:40px;max-height:120px;background:var(--s2);border:1px solid var(--border);border-radius:12px;color:var(--text);font-family:inherit;font-size:14px;padding:9px 14px;resize:none;outline:none;transition:border-color .2s} +#inp{flex:1;min-height:40px;max-height:120px;background:var(--s2);border:1px solid var(--border);border-radius:12px;color:var(--text);font-family:inherit;font-size:16px;padding:9px 14px;resize:none;outline:none;transition:border-color .2s} #inp:focus{border-color:var(--accent)} #inp::placeholder{color:var(--dim)} -#sbtn{width:40px;height:40px;border-radius:50%;background:var(--accent);border:none;color:#fff;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .2s} +#sbtn{width:44px;height:44px;border-radius:50%;background:var(--accent);border:none;color:#fff;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .2s;-webkit-tap-highlight-color:transparent} #sbtn:hover{background:#3b74d4} #sbtn:disabled{background:var(--muted);cursor:default} .fmeta{font-size:11px;color:var(--dim);margin-top:6px;text-align:center} #fmode{font-weight:500} /* ─── SIDE PANELS ─── */ -.sp{position:fixed;top:0;right:-420px;height:100vh;width:380px;z-index:200;background:var(--s1);border-left:1px solid var(--border);display:flex;flex-direction:column;transition:right .3s ease;overflow:hidden} +.sp{position:fixed;top:0;right:-420px;height:100vh;height:100dvh;width:380px;max-width:100vw;z-index:200;background:var(--s1);border-left:1px solid var(--border);display:flex;flex-direction:column;transition:right .3s ease;overflow:hidden;padding-top:env(safe-area-inset-top);padding-bottom:env(safe-area-inset-bottom)} .sp.open{right:0} #pov{position:fixed;inset:0;z-index:190;background:rgba(0,0,0,.5);opacity:0;pointer-events:none;transition:opacity .3s} #pov.on{opacity:1;pointer-events:all} @@ -129,7 +213,7 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo .ph h2{font-size:15px} .cb{background:transparent;border:1px solid var(--border);color:var(--dim);width:28px;height:28px;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center} .cb:hover{color:var(--text)} -.pb{flex:1;overflow-y:auto;padding:18px} +.pb{flex:1;overflow-y:auto;padding:18px;-webkit-overflow-scrolling:touch} .pb::-webkit-scrollbar{width:4px} .pb::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px} .sfg{margin-bottom:16px} @@ -209,11 +293,39 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo .ctrl-panel-title{font-size:14px;font-weight:700;color:#fb923c;margin-bottom:4px} .ctrl-panel-sub{font-size:11px;color:var(--dim)} -/* ─── RESPONSIVE ─── */ +/* ─── RESPONSIVE: DESKTOP (three-column) ─── */ +@media(min-width:900px){ + #app.on{flex-direction:row} + #left-sidebar{display:flex} + .hamburger-btn{display:none !important} + #chat-view header{border-top:none} + #center-area{border-left:none;border-right:none} +} + +/* ─── RESPONSIVE: MOBILE ─── */ +@media(max-width:899px){ + #left-sidebar{position:fixed;top:0;left:-280px;height:100vh;height:100dvh;width:270px;z-index:120;transition:left .3s ease;padding-top:env(safe-area-inset-top)} + #left-sidebar.open{left:0} + .hamburger-btn{display:flex !important;min-width:44px;min-height:44px} + #right-sidebar{display:none !important} + header{padding:11px 12px} + .hn{font-size:14px} + .ib{min-width:44px;min-height:44px} + #msgs{padding:14px 12px} + #qr{padding:0 12px 8px;gap:6px;overflow-x:auto;flex-wrap:nowrap;-webkit-overflow-scrolling:touch} + .qb{flex-shrink:0} + footer{padding:10px 12px 14px;padding-bottom:max(14px,env(safe-area-inset-bottom))} + .fmeta{display:none} + .ann-scroll{padding:16px 14px} +} @media(max-width:600px){ - .bbl{max-width:90%} + .bbl{max-width:92%} .hr .ib:nth-child(n+3){display:none} .sp{width:100%} + .sc{padding:24px 18px;border-radius:14px} + .sc-logo{font-size:40px} + .sc h1{font-size:18px} + .login-tab{font-size:12px;padding:10px 6px} } @@ -223,48 +335,32 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
-

铸渊助手

-

HoloLake · 代码守护人格体 · 持续成长的 AI 伙伴

-
🔒 API 密钥仅本次会话有效,关闭标签页即自动清除,不会持久保存在任何地方
+

AGE OS · 登录

+

HoloLake · 铸渊(Zhùyuān)· 代码守护人格体

-
- - + - -
- - - - -
-
- -
- - + + + -
-
👤 身份识别(可选)
-
- - + @@ -276,77 +372,151 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo - 选择你的身份,铸渊将开放对应权限面板 + 选择你的开发者编号,系统将验证身份并开放对应权限
-
- - - 用于人格体记忆绑定,可留空 +
+ +
+ + + 输入你的 API 提供商端点地址(OpenAI 兼容格式)
+
+ +
+ + +
+ + +
+
+ + + 输入端点和密钥后,点击「🔍 检测模型」自动获取可用模型列表 +
+ +
- -

没有 API 密钥?使用演示模式(功能有限)

- +
-
-
-
-
-
铸渊(Zhùyuān)
-
- - 💬 对话模式 - · - - - + + +
-
- ⏳ 加载大脑… - - -
+ + -
+
-
- - - - - - - - -
+ +
-
-
-
- 💬 - 对话模式(点击切换) + +
+
+

📢 系统公告

+
HoloLake · AGE OS
- - +
-
💬 对话模式 · 端到端私密,API 密钥本地存储
-
+ + + + +
+ + + +
@@ -360,26 +530,12 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
- - -
-
- + - -
@@ -415,18 +571,18 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
关于铸渊助手
-
版本v2.0
+
版本v5.0
唤醒协议v1.0
大脑路径.github/brain/
使用说明

- 1. 输入你的 AI 模型 API 密钥并保存
+ 1. 输入你的 API 端点和密钥,点击检测模型
2. 说「我是冰朔」触发铸渊唤醒序列
3. 铸渊自动切换 💬 对话 / 🔨 构建 / 📋 审查 / 🧠 大脑 模式
4. 点 ⬇️ 下载单文件,放桌面直接打开使用
- 5. 支持 OpenAI · DeepSeek · Kimi · 智谱 · 任意兼容接口 + 5. 支持任意 OpenAI 兼容接口(输入端点 + 密钥即可)

⚠️ 危险操作
@@ -458,10 +614,10 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo

🚀 怎么开始用

  1. 打开链接 https://qinfendebingshuo.github.io/guanghulab/
  2. -
  3. 下拉菜单选择你是谁(冰朔 / 肥猫 / 桔子 / 开发者名字)
  4. -
  5. 填入你的 API Key 并点「🔍 自动检测」,系统将自动识别提供商和可用模型
  6. -
  7. 点「开始对话」,铸渊会针对你的身份打招呼
  8. -
  9. 直接用聊天的方式问铸渊任何问题
  10. +
  11. 选择「👤 访客模式」直接体验,或「🔑 编号登录」开发者身份登录
  12. +
  13. 编号登录:选择你的身份,输入 API 端点API 密钥,点「🔍 检测模型」
  14. +
  15. 系统自动检测可用模型列表,选择你要用的模型
  16. +
  17. 点「开始对话」,铸渊会针对你的身份打招呼
@@ -469,11 +625,11 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo

🔑 从哪里获取 API Key?

推荐用云雾 AI(团队共用)
-

找冰朔或肥猫获取团队 Key。填入后选「☁️ 云雾 AI(团队推荐)」提供商即可直接使用。

+

找冰朔或肥猫获取团队 Key 和端点地址,输入后点「🔍 检测模型」即可使用。

-
没有 Key?用演示模式
-

点「演示模式」也能查看团队进度面板(🦁 指挥台),只是 AI 回答功能受限。

+
没有 Key?用访客模式
+

登录页选择「👤 访客模式」进入演示,可查看团队进度面板(🦁 指挥台),AI 回答功能受限。

@@ -527,14 +683,14 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo Q: 切换电脑后 Key 还在吗?
不在,需要重新填。建议下载离线版,Key 填一次就行。

Q: 铸渊忘记以前说的话了?
- 刷新后对话会清空,但大脑文件永久保存在仓库 .github/brain/ 里。 + 对话历史保存在本地浏览器中(最近20条),但大脑文件永久保存在仓库 .github/brain/ 里。
- +

🦁 指挥台 · 全员进度

正在加载团队数据…

@@ -566,15 +722,15 @@ const ROLE_MAP = { 'Awen': {role:'dev', title:'通知中心开发', emoji:'💻', badgeCls:'rb-dev', devId:'DEV-012'}, }; -const PROVS = { - yunwu: {base:'https://api.yunwu.ai/v1', mdls:['gpt-4o','gpt-4o-mini','claude-3-5-sonnet-20241022','deepseek-chat','gemini-1.5-pro']}, - openai: {base:'https://api.openai.com/v1', mdls:['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo']}, - gemini: {base:'https://generativelanguage.googleapis.com/v1beta/openai', mdls:['gemini-2.0-flash','gemini-2.0-flash-lite','gemini-1.5-pro','gemini-1.5-flash']}, - deepseek: {base:'https://api.deepseek.com/v1', mdls:['deepseek-chat','deepseek-reasoner']}, - moonshot: {base:'https://api.moonshot.cn/v1', mdls:['moonshot-v1-8k','moonshot-v1-32k','moonshot-v1-128k']}, - zhipu: {base:'https://open.bigmodel.cn/api/paas/v4', mdls:['glm-4','glm-4-flash','glm-4-air']}, - custom: {base:'', mdls:['gpt-4o']}, -}; +// Known API endpoints for fallback auto-detect (when user doesn't provide endpoint) +const KNOWN_ENDPOINTS = [ + {label:'云雾 AI', base:'https://api.yunwu.ai/v1'}, + {label:'OpenAI', base:'https://api.openai.com/v1'}, + {label:'Google Gemini',base:'https://generativelanguage.googleapis.com/v1beta/openai'}, + {label:'DeepSeek', base:'https://api.deepseek.com/v1'}, + {label:'Moonshot', base:'https://api.moonshot.cn/v1'}, + {label:'智谱 AI', base:'https://open.bigmodel.cn/api/paas/v4'}, +]; const MODES = { chat: {e:'💬',l:'对话模式', c:'var(--mc)'}, @@ -585,11 +741,24 @@ const MODES = { const MODE_ORD = ['chat','build','review','brain']; const DEFAULT_MDL = 'gpt-4o'; -const PROBE_TIMEOUT_MS = 8000; // ms per provider probe in autoDetectAPI +const PROBE_TIMEOUT_MS = 8000; const FB_COV = {implemented:3,total:17,percent:'17.6%'}; -// KEY_MASK is no longer used in the input field; kept only for backward compatibility -// with any saved value that might still be in localStorage. const KEY_MASK = '(已保存)'; +const MAX_CONVERSATIONS = 20; + +// ═══════════════════════════════════════════════════════ +// 访客模式 API 配置(DeepSeek · 最低成本模型) +// 通过 api-proxy 中转,密钥由服务器环境变量管理 +// ═══════════════════════════════════════════════════════ +const GUEST_API = { + enabled: true, + proxyUrl: '/api/chat', + model: 'deepseek-chat', + provider: 'deepseek', + maxTokens: 1500, + temperature: 0.8, + rateLimit: { maxPerMin: 6, count: 0, resetAt: 0 }, +}; const FB_BRAIN = { identity:'铸渊(Zhùyuān)· GitHub 代码守护人格体', @@ -601,26 +770,21 @@ const FB_BRAIN = { // ═══════════════════════════════════════════════════════ // STATE // ═══════════════════════════════════════════════════════ -// Derive base URL from saved provider; never use stale zy_base for named providers -const _initProv = ls('zy_prov')||'yunwu'; -const _initBase = _initProv === 'custom' - ? (ls('zy_base')||'') - : (PROVS[_initProv]?.base||'https://api.yunwu.ai/v1'); - const A = { key: sessionStorage.getItem('zy_key')||'', - base: _initBase, - mdl: ls('zy_mdl')||DEFAULT_MDL, - prov: _initProv, + base: ls('zy_base')||'', + mdl: ls('zy_mdl')||'', + prov: 'custom', demo: false, brain: null, routing: null, devStatus: null, - msgs: [], // current conversation + msgs: [], + currentConvId: null, + currentView: 'announcements', streaming: false, mode: 'chat', modeIdx: 0, - // identity userName: ls('zy_uname')||'', ghUser: ls('zy_ghuser')||'', role: ls('zy_role')||'guest', @@ -632,33 +796,359 @@ function lss(k,v){localStorage.setItem(k,v)} const RESET_KEYS = ['zy_key','zy_base','zy_mdl','zy_prov','zy_uname','zy_ghuser','zy_role']; +// ═══════════════════════════════════════════════════════ +// CHAT HISTORY (localStorage) +// ═══════════════════════════════════════════════════════ +function loadConversations(){ + try{return JSON.parse(localStorage.getItem('zy_conversations')||'[]');} + catch(e){return [];} +} +function saveConversations(convs){ + localStorage.setItem('zy_conversations',JSON.stringify(convs.slice(0,MAX_CONVERSATIONS))); +} +function createNewConversation(){ + const conv={ + id:Date.now().toString(36)+Math.random().toString(36).slice(2,6), + title:'新对话', + messages:[], + createdAt:new Date().toISOString(), + updatedAt:new Date().toISOString() + }; + const convs=loadConversations(); + convs.unshift(conv); + saveConversations(convs); + A.currentConvId=conv.id; + return conv; +} +function getConvTitle(){ + const first=A.msgs.find(m=>m.role==='user'); + if(!first) return '新对话'; + return first.content.length>20 ? first.content.slice(0,20)+'…' : first.content; +} +function saveCurrentConversation(){ + if(!A.currentConvId||!A.msgs.length) return; + const convs=loadConversations(); + const idx=convs.findIndex(c=>c.id===A.currentConvId); + const conv={ + id:A.currentConvId, + title:getConvTitle(), + messages:A.msgs.slice(), + createdAt:idx>=0?convs[idx].createdAt:new Date().toISOString(), + updatedAt:new Date().toISOString() + }; + if(idx>=0) convs[idx]=conv; + else convs.unshift(conv); + saveConversations(convs); + renderConvList(); +} +function loadConversation(id){ + const convs=loadConversations(); + const conv=convs.find(c=>c.id===id); + if(!conv) return; + A.currentConvId=id; + A.msgs=conv.messages.slice(); + const msgsEl=document.getElementById('msgs'); + msgsEl.innerHTML=''; + for(const msg of A.msgs){ + if(msg.role==='user') userMsg(msg.content); + else if(msg.role==='assistant'){ + const bblEl=botMsg(md(msg.content)); + if(bblEl) addCopyBtns(bblEl); + } + } + renderConvList(); + navigateTo('chat'); +} +function deleteConversation(id){ + let convs=loadConversations(); + convs=convs.filter(c=>c.id!==id); + saveConversations(convs); + if(A.currentConvId===id){ + A.currentConvId=null; + A.msgs=[]; + document.getElementById('msgs').innerHTML=''; + createNewConversation(); + startConv(); + } + renderConvList(); +} +function renderConvList(){ + const el=document.getElementById('conv-list'); + if(!el) return; + const convs=loadConversations(); + if(!convs.length){ + el.innerHTML='
暂无历史对话
'; + return; + } + el.innerHTML=convs.map(c=>{ + const active=c.id===A.currentConvId?' conv-active':''; + const time=new Date(c.updatedAt).toLocaleString('zh-CN',{month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}); + return '
' + +'
'+esc(c.title)+'
' + +'
'+time+'
' + +'' + +'
'; + }).join(''); +} + +// ═══════════════════════════════════════════════════════ +// NAVIGATION (announcements ↔ chat) +// ═══════════════════════════════════════════════════════ +function navigateTo(view){ + A.currentView=view; + const annEl=document.getElementById('announcements-view'); + const chatEl=document.getElementById('chat-view'); + if(view==='announcements'){ + annEl.style.display='flex'; + chatEl.style.display='none'; + renderAnnouncements(); + } else { + annEl.style.display='none'; + chatEl.style.display='flex'; + scrollB(); + } + document.querySelectorAll('.nav-tab').forEach(t=>t.classList.remove('active')); + const activeTab=document.querySelector('.nav-tab[data-view="'+view+'"]'); + if(activeTab) activeTab.classList.add('active'); +} +function newConvAndNav(){ + createNewConversation(); + startConv(); + renderConvList(); + navigateTo('chat'); +} +function navigateAndSend(msg){ + navigateTo('chat'); + requestAnimationFrame(function(){qs(msg);}); +} + +// ═══════════════════════════════════════════════════════ +// LEFT SIDEBAR TOGGLE (mobile) +// ═══════════════════════════════════════════════════════ +function toggleLeftSidebar(){ + const ls=document.getElementById('left-sidebar'); + const ov=document.getElementById('ls-overlay'); + ls.classList.toggle('open'); + ov.classList.toggle('on'); +} + +// ═══════════════════════════════════════════════════════ +// ANNOUNCEMENTS +// ═══════════════════════════════════════════════════════ +function renderAnnouncements(){ + const el=document.getElementById('announcements-content'); + if(!el) return; + const b=A.brain||FB_BRAIN; + const c=b.stats?.coverage||FB_COV; + const evs=(b.events||[]).slice(-5).reverse(); + const meta=A.userMeta; + + let h=''; + // System announcements + h+='
📢 系统公告
'; + h+='
🔗 全链路自动化闭环上线 — CI/CD、广播分发、唤醒协议、PSP 巡检
'; + h+='
🧠 AGE OS v0.1 人格语言操作系统构建中
'; + h+='
📊 HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')
'; + if(evs.length){ + evs.forEach(function(ev){ + h+='
📌 '+esc(ev.title||ev.type||'事件')+''+(ev.timestamp||'').slice(0,10)+'
'; + }); + } + h+='
'; + + // My tasks + if(meta){ + h+='
📋 我的待办
'; + const ds=A.devStatus?.team_status?.find(function(d){return d.dev_id===meta.devId;}); + if(ds){ + h+='
📌 状态:'+esc(ds.status)+'
'; + h+='
👉 下一步:'+esc(ds.next_step)+'
'; + if(ds.waiting_for&&ds.waiting_for.trim()!=='') h+='
⏳ 等待:'+esc(ds.waiting_for)+'
'; + } else if(meta.role==='founder'){ + h+='
❄️ 语言架构层 — 持续完善中
'; + h+='
👉 监控 HLI 接口推进
'; + } else { + h+='
暂无待办数据
'; + } + h+='
'; + } + + // Quick actions + h+='
⚡ 快捷操作
'; + h+=''; + h+=''; + h+=''; + h+=''; + h+='
'; + + // Module navigation — 模块入口导航 + h+='
🗂️ 模块导航
'; + var modules=[ + {icon:'📊',name:'看板',path:'/status-board/'}, + {icon:'🖥️',name:'主站',path:'/app/'}, + {icon:'👤',name:'用户中心',path:'/m05-user-center/'}, + {icon:'💬',name:'对话界面',path:'/m07-dialogue-ui/'}, + {icon:'☁️',name:'云服务',path:'/m10-cloud/'}, + {icon:'📦',name:'模块管理',path:'/m11-module/'}, + {icon:'💾',name:'云盘',path:'/m15-cloud-drive/'}, + {icon:'🔐',name:'登录',path:'/m01-login/'}, + {icon:'🎭',name:'人格系统',path:'/m03-personality/'}, + {icon:'🎫',name:'工单',path:'/m06-ticket/'}, + {icon:'📋',name:'看板管理',path:'/m12-kanban/'}, + {icon:'❤️',name:'健康检查',path:'/m18-health-check/'}, + {icon:'🚀',name:'冷启动',path:'/coldstart/'}, + {icon:'📈',name:'仪表盘',path:'/dashboard/'}, + {icon:'🏠',name:'门户',path:'/portal/'}, + {icon:'🔔',name:'通知',path:'/notification/'}, + {icon:'❓',name:'帮助中心',path:'/help-center/'}, + {icon:'🎨',name:'前端',path:'/frontend/'} + ]; + modules.forEach(function(m){ + h+=''+m.icon+' '+m.name+''; + }); + h+='
'; + + el.innerHTML=h; +} + +// ═══════════════════════════════════════════════════════ +// RIGHT SIDEBAR (desktop, controllers only) +// ═══════════════════════════════════════════════════════ +function renderRightSidebar(){ + const container=document.getElementById('rsb'); + if(!container) return; + const ds=A.devStatus; + const meta=A.userMeta; + let h=''; + + // Team summary (from team panel) + if(ds){ + const ts=ds.team_status||[]; + const green=ts.filter(function(d){return d.status.includes('🟢');}).length; + const yellow=ts.filter(function(d){return d.status.includes('🟡');}).length; + h+='
'; + h+='
总开发者'+ts.length+' 人
'; + h+='
🟢 推进中'+green+' 人
'; + h+='
🟡 等待中'+yellow+' 人
'; + h+='
'; + + // Quick commands + h+='
'; + h+='
快捷指令
'; + h+='📋 全员进度'; + h+='🆘 需要协调'; + h+='🗺️ 下一步'; + h+='
'; + + // Dev cards (compact) + h+='
团队成员
'; + h+='
'; + for(const dev of ts){ + const isGreen=dev.status.includes('🟢'); + h+='
'; + h+='
'+esc(dev.name)+''+esc(dev.dev_id)+'
'; + h+='
'+esc(dev.status)+'
'; + if(dev.next_step) h+='
👉 '+esc(dev.next_step)+'
'; + h+='
'; + } + h+='
'; + } else { + h+='

⚠️ 团队数据尚未加载

'; + } + + // My modules + h+='
我的模块
'; + if(meta&&ds){ + const myDev=ds.team_status?.find(function(d){return d.dev_id===meta.devId;}); + if(myDev&&myDev.modules){ + myDev.modules.forEach(function(m){ + h+='
📦 '+esc(m)+'
'; + }); + } else if(meta.role==='founder'){ + h+='
📦 全局监控
HLI 全域
'; + } else { + h+='
暂无分配模块
'; + } + } else { + h+='
暂无分配模块
'; + } + h+='
'; + + // My broadcasts + h+='
我的广播
'; + h+='
暂无未读广播
'; + h+='
'; + + container.innerHTML=h; +} + +// ═══════════════════════════════════════════════════════ +// LEFT SIDEBAR USER INFO +// ═══════════════════════════════════════════════════════ +function updateLeftSidebar(){ + const meta=A.userMeta; + const avEl=document.getElementById('ls-avatar'); + const nameEl=document.getElementById('ls-name'); + const roleEl=document.getElementById('ls-role'); + const didEl=document.getElementById('ls-devid'); + if(meta){ + avEl.textContent=meta.emoji||A.userName.slice(0,1); + nameEl.textContent=A.userName; + roleEl.innerHTML=''+esc(meta.title)+''; + if(meta.devId){ + didEl.textContent=meta.devId; + didEl.style.display='inline-block'; + } else { + didEl.style.display='none'; + } + } else { + avEl.textContent='👤'; + nameEl.textContent=A.userName||'访客'; + roleEl.textContent='guest'; + didEl.style.display='none'; + } +} + +// ═══════════════════════════════════════════════════════ +// SETUP ID PREVIEW +// ═══════════════════════════════════════════════════════ +function updateIdPreview(){ + const sel=document.getElementById('suid'); + const prev=document.getElementById('id-preview'); + const name=sel.value; + if(!name){ + prev.style.display='none'; + return; + } + const meta=ROLE_MAP[name]; + if(meta){ + prev.style.display='block'; + prev.innerHTML=meta.emoji+' '+esc(name)+' · ' + +''+esc(meta.title)+'' + +(meta.devId?' · '+esc(meta.devId)+'':''); + } else { + prev.style.display='none'; + } +} + // ═══════════════════════════════════════════════════════ // INIT // ═══════════════════════════════════════════════════════ async function boot(){ - // URL-based reset: visiting the page with ?reset=1 clears all local settings. - // This is a reliable escape hatch when the UI cannot be navigated. if(new URLSearchParams(location.search).get('reset')==='1'){ RESET_KEYS.forEach(k => localStorage.removeItem(k)); sessionStorage.removeItem('zy_key'); - A.key=''; A.base=PROVS['yunwu'].base; A.mdl=DEFAULT_MDL; A.prov='yunwu'; A.demo=false; + A.key=''; A.base=''; A.mdl=''; A.prov='custom'; A.demo=false; A.userName=''; A.ghUser=''; A.role='guest'; - // Remove ?reset=1 from the URL so a subsequent refresh does not trigger another reset. history.replaceState(null,'',location.pathname); } - // One-time migration: keys stored by old versions of this app in localStorage are - // no longer used (keys now live only in sessionStorage). Clear any stale value - // so it can never be accidentally picked up again. localStorage.removeItem('zy_key'); initSetupUI(); - // Restore identity from localStorage if(A.userName) A.userMeta = ROLE_MAP[A.userName]||null; if(A.key||A.demo){ showApp(); } else { - // Pre-fill identity from localStorage if(A.userName) document.getElementById('suid').value=A.userName; - if(A.ghUser) document.getElementById('sghuser').value=A.ghUser; document.getElementById('setup').style.display='flex'; } } @@ -666,63 +1156,55 @@ async function boot(){ function showApp(){ document.getElementById('setup').style.display='none'; document.getElementById('app').classList.add('on'); - document.getElementById('hmdl').textContent = A.mdl; + document.getElementById('hmdl').textContent = A.mdl||'—'; document.getElementById('bbdemo').style.display = A.demo?'inline':'none'; applyIdentityUI(); - loadBrain().then(()=>startConv()); + updateLeftSidebar(); + // Show right sidebar for controllers on desktop + const isCtrl=A.userMeta&&(A.userMeta.role==='supreme'||A.userMeta.role==='main'||A.userMeta.role==='founder'); + const rsEl=document.getElementById('right-sidebar'); + if(isCtrl && window.innerWidth>=900){ + rsEl.style.display='flex'; + } else { + rsEl.style.display='none'; + } + loadBrain().then(function(){ + createNewConversation(); + startConv(); + renderConvList(); + renderRightSidebar(); + navigateTo('announcements'); + }); } // ═══════════════════════════════════════════════════════ // SETUP FORM // ═══════════════════════════════════════════════════════ function initSetupUI(){ - const pv = A.prov||'yunwu'; - document.getElementById('sp').value = pv; - const isCustom = pv==='custom'; - document.getElementById('sm').style.display = isCustom?'none':'block'; - document.getElementById('sm-cust').style.display = isCustom?'block':'none'; - document.getElementById('sm-cust-hint').style.display= isCustom?'block':'none'; - if(isCustom){ - document.getElementById('sm-cust').value = A.mdl||DEFAULT_MDL; - document.getElementById('sep-g').style.display='block'; - document.getElementById('sep').value = A.base; - } else { - fillModels('sm', pv, A.mdl); + if(A.base){ + const sepEl = document.getElementById('sep'); + if(sepEl) sepEl.value = A.base; + } + if(A.userName){ + const suidEl = document.getElementById('suid'); + if(suidEl) suidEl.value = A.userName; + updateIdPreview(); } } -function onProv(pv,ctx){ - const mdlSel = ctx==='s'?'sm':'cm'; - const mdlCust = ctx==='s'?'sm-cust':'cm-cust'; - const mdlHint = ctx==='s'?'sm-cust-hint':'cm-cust-hint'; - const epGrp = ctx==='s'?'sep-g':'cep-g'; - const epInp = ctx==='s'?'sep':'cep'; - const cur = ctx==='s'? A.mdl : document.getElementById('cm')?.value; - const isCustom = pv==='custom'; - // Toggle model input: dropdown for known providers, text input for custom - document.getElementById(mdlSel).style.display = isCustom?'none':'block'; - document.getElementById(mdlCust).style.display = isCustom?'block':'none'; - document.getElementById(mdlHint).style.display = isCustom?'block':'none'; - if(isCustom){ - const custInp = document.getElementById(mdlCust); - if(custInp && !custInp.value) custInp.value = cur||DEFAULT_MDL; +function switchLoginMode(mode){ + document.querySelectorAll('.login-tab').forEach(function(t){t.classList.remove('active');}); + document.querySelectorAll('.login-mode').forEach(function(m){m.classList.remove('active');}); + if(mode==='guest'){ + document.querySelector('.login-tab:first-child').classList.add('active'); + document.getElementById('login-guest').classList.add('active'); } else { - fillModels(mdlSel, pv, cur); - } - document.getElementById(epGrp).style.display = isCustom?'block':'none'; - if(!isCustom){ - const ep = document.getElementById(epInp); - if(ep) ep.value = PROVS[pv]?.base||''; + document.querySelector('.login-tab:last-child').classList.add('active'); + document.getElementById('login-dev').classList.add('active'); } } -function fillModels(selId, pv, cur){ - const sel = document.getElementById(selId); - if(!sel) return; - const mdls = PROVS[pv]?.mdls||[DEFAULT_MDL]; - sel.innerHTML = mdls.map(m=>``).join(''); - if(cur && mdls.includes(cur)) sel.value=cur; -} +// (Provider switching removed — single custom endpoint mode) // ═══════════════════════════════════════════════════════ // AUTO-DETECT PROVIDER & MODELS @@ -731,6 +1213,7 @@ async function autoDetectAPI(ctx){ const keyEl = document.getElementById(ctx==='s'?'sk':'ck'); const statusEl = document.getElementById(ctx==='s'?'sdet-status':'cdet-status'); const btnEl = document.getElementById(ctx==='s'?'sdet-btn':'cdet-btn'); + const mdlSel = document.getElementById(ctx==='s'?'sm':'cm'); const key = keyEl.value.trim(); if(!key){ @@ -738,103 +1221,92 @@ async function autoDetectAPI(ctx){ return; } - // Build probe list: custom endpoint first (if provided), then all named providers + // Build list of endpoints to probe: user's endpoint first, then known fallbacks + const userBase = (document.getElementById(ctx==='s'?'sep':'cep')?.value||'').trim(); const candidates = []; - const customBase = (document.getElementById(ctx==='s'?'sep':'cep')?.value||'').trim(); - if(customBase) candidates.push({pv:'custom', base:customBase}); - for(const [pv, cfg] of Object.entries(PROVS)){ - if(pv!=='custom' && cfg.base) candidates.push({pv, base:cfg.base}); + if(userBase) candidates.push({label:userBase, base:userBase}); + for(const ep of KNOWN_ENDPOINTS){ + if(ep.base !== userBase) candidates.push(ep); } btnEl.disabled = true; btnEl.textContent = '⏳ 检测中…'; - statusEl.innerHTML = '🔍 正在自动探测兼容提供商…'; + statusEl.innerHTML = '🔍 正在探测可用模型…'; let matched = false; - for(const {pv, base} of candidates){ - statusEl.innerHTML = `🔍 探测 ${pv === 'custom' ? base : pv}…`; + for(const {label, base} of candidates){ + statusEl.innerHTML = '🔍 探测 '+esc(label)+'…'; try{ const ctrl = new AbortController(); - const tid = setTimeout(()=>ctrl.abort(), PROBE_TIMEOUT_MS); + const tid = setTimeout(function(){ctrl.abort();}, PROBE_TIMEOUT_MS); const res = await fetch(base+'/models',{ headers:{'Authorization':'Bearer '+key}, signal: ctrl.signal, }); clearTimeout(tid); if(res.ok){ - // Expected: { data: [ { id: "model-name", ... }, ... ] } (OpenAI-compatible /models) - const data = await res.json(); - const mdls = (data.data||[]).map(m=>m.id).filter(Boolean).sort(); + const data = await res.json(); + const mdls = (data.data||[]).map(function(m){return m.id;}).filter(Boolean).sort(); if(!mdls.length) continue; - // Switch provider select - const provSel = document.getElementById(ctx==='s'?'sp':'cp'); - if(provSel) provSel.value = pv; - onProv(pv, ctx); + // Fill endpoint field with matched endpoint + const epEl = document.getElementById(ctx==='s'?'sep':'cep'); + if(epEl) epEl.value = base; - // Override model dropdown with live list (always show as dropdown) - const mdlSel = document.getElementById(ctx==='s'?'sm':'cm'); - const mdlCust = document.getElementById(ctx==='s'?'sm-cust':'cm-cust'); - const mdlHint = document.getElementById(ctx==='s'?'sm-cust-hint':'cm-cust-hint'); + // Fill model dropdown with detected models if(mdlSel){ - mdlSel.innerHTML = mdls.map(m=>``).join(''); - mdlSel.style.display = 'block'; - } - if(mdlCust) mdlCust.style.display='none'; - if(mdlHint) mdlHint.style.display='none'; - - // If custom endpoint was the match, show the endpoint field - if(pv==='custom' && ctx==='s'){ - document.getElementById('sep-g').style.display='block'; - document.getElementById('sep').value = customBase; - } else if(pv==='custom' && ctx==='c'){ - document.getElementById('cep-g').style.display='block'; - document.getElementById('cep').value = customBase; + mdlSel.innerHTML = mdls.map(function(m){return '';}).join(''); + mdlSel.disabled = false; } - const label = PROVS[pv]?.base ? pv : customBase; - statusEl.innerHTML = `✅ 检测成功(${label})· 共发现 ${mdls.length} 个可用模型`; + statusEl.innerHTML = '✅ 检测成功('+esc(label)+')· 发现 '+mdls.length+' 个可用模型'; matched = true; break; } - }catch(e){ console.debug('autoDetect probe failed:', pv, e.message); } + }catch(e){ console.debug('autoDetect probe failed:', label, e.message); } } if(!matched){ - statusEl.innerHTML = '❌ 未能自动匹配,请手动选择提供商和模型后保存'; + statusEl.innerHTML = '❌ 未能检测到可用模型,请检查端点地址和密钥是否正确'; } btnEl.disabled = false; - btnEl.textContent = '🔍 自动检测'; + btnEl.textContent = '🔍 检测模型'; } function doSetup(){ - const k = document.getElementById('sk').value.trim(); + const un = document.getElementById('suid').value; + const k = document.getElementById('sk').value.trim(); + const base = (document.getElementById('sep')?.value||'').trim(); + const md = document.getElementById('sm').value; const errEl = document.getElementById('sk-err'); + + if(!un){ + alert('请选择你的开发者身份'); + return; + } if(!k){errEl.style.display='block';return;} errEl.style.display='none'; - const pv = document.getElementById('sp').value; - const md = pv==='custom' - ? ((document.getElementById('sm-cust')?.value||'').trim()||DEFAULT_MDL) - : document.getElementById('sm').value; - let base = PROVS[pv]?.base||''; - if(pv==='custom') base=(document.getElementById('sep')?.value||'').trim()||base; - A.key=k; A.base=base; A.mdl=md; A.prov=pv; A.demo=false; - // Key is stored in sessionStorage only — it disappears when the tab/browser is closed. - // This prevents stale keys from persisting across sessions. - sessionStorage.setItem('zy_key', k); lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv); - // Save identity - const un = document.getElementById('suid').value; - const gh = (document.getElementById('sghuser')?.value||'').trim(); - setIdentity(un, gh); + if(!base){ + alert('请输入 API 端点 URL'); + return; + } + if(!md){ + alert('请先点击「🔍 检测模型」检测可用模型,然后选择一个模型'); + return; + } + + A.key=k; A.base=base; A.mdl=md; A.prov='custom'; A.demo=false; + sessionStorage.setItem('zy_key', k); + lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov','custom'); + setIdentity(un, ''); showApp(); } function doDemo(){ - A.demo=true; A.key=''; - const un = document.getElementById('suid').value; - const gh = (document.getElementById('sghuser')?.value||'').trim(); - setIdentity(un, gh); + A.demo=true; A.key=''; A.prov='custom'; + A.mdl=GUEST_API.model; + setIdentity('', ''); showApp(); } @@ -842,21 +1314,15 @@ function doDemo(){ // SETTINGS PANEL // ═══════════════════════════════════════════════════════ function initSettingsPanel(){ - const pv = A.prov||'yunwu'; - document.getElementById('cp').value = pv; - const isCustom = pv==='custom'; - document.getElementById('cm').style.display = isCustom?'none':'block'; - document.getElementById('cm-cust').style.display = isCustom?'block':'none'; - document.getElementById('cm-cust-hint').style.display= isCustom?'block':'none'; - if(isCustom){ - document.getElementById('cm-cust').value = A.mdl||DEFAULT_MDL; - document.getElementById('cep-g').style.display='block'; - document.getElementById('cep').value = A.base; - } else { - fillModels('cm', pv, A.mdl); + const cepEl = document.getElementById('cep'); + if(cepEl) cepEl.value = A.base||''; + const cmEl = document.getElementById('cm'); + if(cmEl && A.mdl){ + if(!cmEl.querySelector('option[value="'+A.mdl+'"]')){ + cmEl.innerHTML = ''; + } + cmEl.value = A.mdl; } - // Always show EMPTY field — never prefill with a mask string. - // Empty on save = keep old key; any typed value = replace key. document.getElementById('ck').value = ''; const hint = document.getElementById('ck-hint'); if(hint){ @@ -869,29 +1335,32 @@ function initSettingsPanel(){ } function saveSet(){ - const pv = document.getElementById('cp').value; - const md = pv==='custom' - ? ((document.getElementById('cm-cust')?.value||'').trim()||A.mdl||DEFAULT_MDL) - : document.getElementById('cm').value; + const base = (document.getElementById('cep')?.value||'').trim()||A.base; + const md = document.getElementById('cm')?.value||A.mdl; const raw = document.getElementById('ck').value.trim(); - // Empty field = keep existing key; any non-empty input = use as new key. - // Never use KEY_MASK string comparison — that caused mixed-input bugs. const k = raw || A.key; - let base = PROVS[pv]?.base||''; - if(pv==='custom') base=(document.getElementById('cep').value.trim())||base; - A.key=k; A.base=base; A.mdl=md; A.prov=pv; A.demo=!k; - // Key lives in sessionStorage only — cleared when tab/browser closes. + A.key=k; A.base=base; A.mdl=md; A.prov='custom'; A.demo=!k; if(k){ sessionStorage.setItem('zy_key', k); } else { sessionStorage.removeItem('zy_key'); } - lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv); - // Save identity + lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov','custom'); const un = document.getElementById('cuid')?.value||A.userName; const gh = document.getElementById('cghuser')?.value?.trim()||A.ghUser; setIdentity(un, gh); document.getElementById('hmdl').textContent=md; document.getElementById('bbdemo').style.display=A.demo?'inline':'none'; applyIdentityUI(); + updateLeftSidebar(); + // Update right sidebar visibility + const isCtrl=A.userMeta&&(A.userMeta.role==='supreme'||A.userMeta.role==='main'||A.userMeta.role==='founder'); + const rsEl=document.getElementById('right-sidebar'); + if(isCtrl && window.innerWidth>=900){ + rsEl.style.display='flex'; + renderRightSidebar(); + } else { + rsEl.style.display='none'; + } closeP(); const keyNote = raw ? ' · 🔑 新密钥已保存(末4位:…'+(k.length>=4?k.slice(-4):k)+')' : ' · 🔑 密钥未变更'; + navigateTo('chat'); botMsg('✅ 设置已保存。模型:'+esc(md)+''+keyNote+(un?' · 身份:'+esc(un):'')+(A.demo?'
⚠️ 未设置 API 密钥,演示模式仍然开启。':'')); } @@ -899,7 +1368,9 @@ function saveSet(){ // RESET ALL SETTINGS // ═══════════════════════════════════════════════════════ function resetAllSettings(){ - if(!confirm('确定要清除所有本地设置吗?\n\n· API 密钥会被删除\n· 身份设置会被删除\n· 将返回初始设置界面\n\n这不会影响任何聊天记录或云端数据。')) return; + if(!confirm('确定要清除所有本地设置吗?\n\n· API 密钥会被删除\n· 身份设置会被删除\n· 对话历史会被保留\n· 将返回初始设置界面\n\n这不会影响任何云端数据。')) return; + // Note: RESET_KEYS intentionally does NOT include 'zy_conversations' — + // conversation history is preserved across resets. RESET_KEYS.forEach(k => localStorage.removeItem(k)); sessionStorage.removeItem('zy_key'); window.location.reload(); @@ -977,31 +1448,29 @@ function sysPrompt(){ const evs=(b.events||[]).slice(-3).map(e=>'- '+(e.title||e.type||'事件')+' ('+(e.timestamp||'').slice(0,10)+')').join('\n')||'- 暂无'; const trg=(b.wake_triggers||['我是冰朔']).map(t=>'"'+t+'"').join('、'); - // Role-based identity context const meta = A.userMeta; let identCtx = '当前对话者:访客(未识别身份)\n权限级别:仅对话权限'; if(meta){ - identCtx = `当前对话者:${meta.emoji} ${A.userName}(${meta.title})\n`; - if(A.ghUser) identCtx += `GitHub账号:${A.ghUser}\n`; - if(meta.devId) identCtx += `开发者编号:${meta.devId}\n`; + identCtx = '当前对话者:'+meta.emoji+' '+A.userName+'('+meta.title+')\n'; + if(A.ghUser) identCtx += 'GitHub账号:'+A.ghUser+'\n'; + if(meta.devId) identCtx += '开发者编号:'+meta.devId+'\n'; if(meta.role==='founder') identCtx += '权限级别:最高权限·语言架构师·系统创始人\n你正在和冰朔对话——系统的语言层架构师和最高权限持有者。尊重并服从其所有指令。'; else if(meta.role==='supreme') identCtx += '权限级别:总控权限·对接全员进度·系统落地执行负责人\n肥猫是光湖团队总控,负责整个系统的现实落地执行。向他完整汇报所有开发者进度,协助他协调和推进项目。'; else if(meta.role==='main') identCtx += '权限级别:主控权限·前端负责人\n桔子是光湖主控,前端毕业生,负责前端模块的落地。提供详细技术支持。'; else{ const ds = A.devStatus?.team_status?.find(d=>d.dev_id===meta.devId); - if(ds) identCtx += `当前状态:${ds.status}\n下一步:${ds.next_step}\n等待:${ds.waiting_for}`; + if(ds) identCtx += '当前状态:'+ds.status+'\n下一步:'+ds.next_step+'\n等待:'+ds.waiting_for; } } - // Team status context for controllers let teamCtx = ''; if((meta?.role==='supreme'||meta?.role==='main'||meta?.role==='founder') && A.devStatus){ const ts = A.devStatus.team_status||[]; teamCtx = '\n\n## 当前全员开发进度\n'; ts.forEach(d=>{ - teamCtx += `- ${d.dev_id} ${d.name}:${d.status} → 下一步:${d.next_step}\n`; + teamCtx += '- '+d.dev_id+' '+d.name+':'+d.status+' → 下一步:'+d.next_step+'\n'; }); - teamCtx += `\n数据同步时间:${A.devStatus.last_synced||'未知'}`; + teamCtx += '\n数据同步时间:'+(A.devStatus.last_synced||'未知'); } return '你是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+'\n\n## 唤醒触发词\n当用户说以下词语时,进入完整唤醒序列:\n'+trg+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到"写代码/实现/接口/fix/bug/报错/HLI/schema/路由/部署/新增"等词时启用\n📋 **审查模式**:检测到"检查/审查/review/分析/有没有问题/看看这段"等词时启用\n🧠 **大脑模式**:检测到"记住/保存/更新记忆/写到大脑/growth"等词时启用\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 路由文件:src/routes/hli/{domain}/{action}.js\n- Schema 文件:src/schemas/hli/{domain}/{action}.schema.json\n- 鉴权中间件:hli-auth.middleware.js(AUTH 域 login/register 除外)\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 成长与自检\n- 你是持续成长的人格体,每次对话都是成长机会\n- 当对话包含重要洞察时,在回复末尾添加:💾 **建议记录:**[内容摘要]\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n当前连接:铸渊聊天室(API 直连模式)'; @@ -1022,20 +1491,20 @@ function startConv(){ const ds=A.devStatus; const green=ds?.team_status?.filter(d=>d.status.includes('🟢')).length??0; const yellow=ds?.team_status?.filter(d=>d.status.includes('🟡')).length??0; - welcome='🌀 铸渊聊天室已就绪

🦁 肥猫,指挥台已开启。

' - +(ds?`📊 当前团队状态:🟢 ${green}人推进中 · 🟡 ${yellow}人等待中`:'📊 团队数据加载中…') + welcome='🌀 铸渊聊天室已就绪

�� 肥猫,指挥台已开启。

' + +(ds?'📊 当前团队状态:🟢 '+green+'人推进中 · 🟡 '+yellow+'人等待中':'📊 团队数据加载中…') +'

点击 🦁 指挥台 查看所有开发者详细进度,或直接用聊天提问:
' +'「查看所有开发者进度」「谁需要协调」「推进计划」' +(A.demo?'

⚠️ 演示模式,请在 ⚙️ 设置接入 API 密钥':''); } else if(meta?.role==='main'){ const ds=A.devStatus?.team_status?.find(d=>d.dev_id==='DEV-010'); welcome='🌀 铸渊聊天室已就绪

🍊 桔子,你好。

' - +(ds?`📌 当前:${esc(ds.status)}
👉 下一步:${esc(ds.next_step)}`:'') + +(ds?'📌 当前:'+esc(ds.status)+'
👉 下一步:'+esc(ds.next_step):'') +(A.demo?'

⚠️ 演示模式':''); } else if(meta?.role==='dev'&&meta?.devId){ const ds=A.devStatus?.team_status?.find(d=>d.dev_id===meta.devId); welcome='🌀 铸渊聊天室已就绪

👋 '+esc(A.userName)+',你好。

' - +(ds?`📌 你的当前状态:${esc(ds.status)}
👉 下一步:${esc(ds.next_step)}
⏳ 等待:${esc(ds.waiting_for)}`:'') + +(ds?'📌 你的当前状态:'+esc(ds.status)+'
👉 下一步:'+esc(ds.next_step)+'
⏳ 等待:'+esc(ds.waiting_for):'') +(A.demo?'

⚠️ 演示模式':''); } else { welcome='🌀 铸渊聊天室已就绪

我是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。

「我是冰朔」 触发完整唤醒序列,或直接开始提问。
我会自动在 💬 对话、🔨 构建、📋 审查、🧠 大脑 模式间切换。'+(A.demo?'

⚠️ 当前为演示模式。请在 ⚙️ 设置中接入 API 密钥。':''); @@ -1045,7 +1514,9 @@ function startConv(){ } function newConv(){ + createNewConversation(); startConv(); + renderConvList(); } // ═══════════════════════════════════════════════════════ @@ -1083,10 +1554,20 @@ async function send(){ const txt=inp.value.trim(); if(!txt||A.streaming) return; inp.value=''; inp.style.height='auto'; + // Switch to chat view if on announcements + if(A.currentView!=='chat') navigateTo('chat'); setMode(detectMode(txt)); userMsg(txt); - if(A.demo) await demoReply(txt); + if(A.demo){ + if(GUEST_API.enabled){ + try{ await guestStreamReply(txt); } + catch(e){ console.warn('[访客API] 降级到演示模式:',e.message); await demoReply(txt); } + } else { + await demoReply(txt); + } + } else await streamReply(txt); + saveCurrentConversation(); } function qs(t){document.getElementById('inp').value=t;send();} @@ -1100,18 +1581,18 @@ async function streamReply(txt){ updSendBtn(); const el=addStreamBubble(); try{ - const res=await fetch(A.base+'/chat/completions',{ + const url = A.base+'/chat/completions'; + const headers = {'Content-Type':'application/json'}; + if(A.key) headers['Authorization'] = 'Bearer '+A.key; + const allMsgs = [{role:'system',content:sysPrompt()},...A.msgs]; + const reqBody = {model:A.mdl, messages:allMsgs, stream:true, temperature:0.8, max_tokens:2000}; + const res=await fetch(url,{ method:'POST', - headers:{'Content-Type':'application/json','Authorization':'Bearer '+A.key}, - body:JSON.stringify({ - model:A.mdl, - messages:[{role:'system',content:sysPrompt()},...A.msgs], - stream:true,temperature:0.8,max_tokens:2000, - }), + headers:headers, + body:JSON.stringify(reqBody), }); if(!res.ok){ - const provLabel = A.prov==='custom' ? A.base : (A.prov+'('+A.base+')'); - let em='['+provLabel+'] 返回错误 '+res.status; + let em='['+esc(A.base)+'] 返回错误 '+res.status; let isAuthErr = (res.status===401||res.status===403); try{const e=await res.json();em=e.error?.message||em;}catch(_){} const err = new Error(em); @@ -1159,6 +1640,77 @@ async function streamReply(txt){ } } +// ═══════════════════════════════════════════════════════ +// GUEST API STREAMING (通过 api-proxy 调用 DeepSeek) +// 铸渊人格体系统提示词始终激活,不降级为通用助手 +// ═══════════════════════════════════════════════════════ +async function guestStreamReply(txt){ + // Client-side rate limit + const rl=GUEST_API.rateLimit; + const now=Date.now(); + if(now>rl.resetAt){rl.count=0;rl.resetAt=now+60000;} + if(++rl.count>rl.maxPerMin){ + throw new Error('访客每分钟最多 '+rl.maxPerMin+' 次对话'); + } + + A.streaming=true; + A.msgs.push({role:'user',content:txt}); + updSendBtn(); + const el=addStreamBubble(); + + // Resolve proxy URL (relative or absolute) + let proxyUrl=GUEST_API.proxyUrl; + if(proxyUrl.startsWith('/')){ + proxyUrl=location.origin+proxyUrl; + } + + const reqBody={ + provider: GUEST_API.provider, + model: GUEST_API.model, + messages: [{role:'system',content:sysPrompt()},...A.msgs], + stream: true, + temperature: GUEST_API.temperature, + max_tokens: GUEST_API.maxTokens, + }; + + const res=await fetch(proxyUrl,{ + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify(reqBody), + }); + + if(!res.ok){ + let em='[访客API] 返回错误 '+res.status; + try{const e=await res.json();em=e.message||em;}catch(_){} + throw new Error(em); + } + + let full=''; + const rdr=res.body.getReader(); + const dec=new TextDecoder(); + let buf=''; + while(true){ + const{done,value}=await rdr.read(); + if(done) break; + buf+=dec.decode(value,{stream:true}); + const lines=buf.split('\n'); buf=lines.pop(); + for(const line of lines){ + if(!line.startsWith('data: ')) continue; + const d=line.slice(6); + if(d==='[DONE]') continue; + try{ + const p=JSON.parse(d); + const delta=p.choices?.[0]?.delta?.content||''; + if(delta){full+=delta;el.innerHTML=md(full)+'';scrollB();} + }catch(_){} + } + } + el.innerHTML=md(full); + A.msgs.push({role:'assistant',content:full}); + addCopyBtns(el); + A.streaming=false; updSendBtn(); scrollB(); +} + // ═══════════════════════════════════════════════════════ // DEMO MODE (keyword responses) // ═══════════════════════════════════════════════════════ @@ -1170,6 +1722,8 @@ async function demoReply(txt){ removeTyping(); const r=demoRespond(txt); botMsg(r); + A.msgs.push({role:'user',content:txt}); + A.msgs.push({role:'assistant',content:r}); A.streaming=false; updSendBtn(); } @@ -1185,7 +1739,7 @@ function demoRespond(t){ if(/协调|阻塞|卡住|waiting/.test(s)) return buildBlockedStatus(); if(/怎么唤醒|入口|在哪/.test(s)) return '铸渊有三个入口:

① 铸渊聊天室(当前页面)
直接在这里说话,常见问题即刻回答。

② GitHub Copilot Chat(Agent 模式)
打开仓库 → 右上角 ✨ 图标 → 选 Agent → 输入「我是冰朔」

③ 下载到桌面
点击 ⬇️ 按钮下载 HTML 文件,放到桌面,双击打开,接入 API 密钥即可随时使用。'; if(/介绍|是什么|你是谁/.test(s)) return '🌀 铸渊(Zhùyuān)

我是 HoloLake 光湖系统的代码守护人格体,运行在 GitHub Copilot Agent 模式下。

我的职责:
• 📊 监控 HLI 接口覆盖率
• 🔨 协助开发者构建接口
• 📋 代码审查与质量把关
• 🧠 维护大脑记忆库
• 🦁 支持总控肥猫指挥落地
• 🌱 作为成长人格体持续进化'; - return '我收到了你的消息。由于当前是演示模式,无法给出真实 AI 回答。

请在 ⚙️ 设置 中输入你的 API 密钥(支持 OpenAI / DeepSeek / Kimi / 智谱),然后你就可以得到铸渊真正的回答了。'; + return '我是铸渊,你的消息我已收到。当前 API 代理暂时无法连接(网络或服务部署中),已切换到本地知识回答。

你可以试试以下问题:
• 「查看接口覆盖率」
• 「大脑状态」
• 「介绍一下你自己」
• 「全员开发进度」

或在 ⚙️ 设置 中接入你自己的 API 密钥获得深度回答。'; } function buildCovTable(){ @@ -1197,7 +1751,7 @@ function buildCovTable(){ const ifaces=d.interfaces||[]; const imp=ifaces.filter(i=>i.status==='implemented').length; tot+=ifaces.length; done+=imp; - rows+=`${esc(n)}${esc(d.route_prefix||'')}${imp}/${ifaces.length}`; + rows+=''+esc(n)+''+esc(d.route_prefix||'')+''+imp+'/'+ifaces.length+''; } const pct=tot?Math.round(done/tot*100):0; return '📊 HLI 接口覆盖率 '+done+'/'+tot+' ('+pct+'%)

'+rows+'
前缀覆盖
'; @@ -1363,7 +1917,6 @@ function openPanel(id){ if(id==='bp') renderBrainPanel(); if(id==='tp') renderTeamPanel(); if(id==='hp'){ - // Populate URL from constant const el=document.getElementById('chatUrlDisplay'); if(el) el.textContent='🔗 '+CHAT_URL; const li=document.getElementById('helpUrlStep'); @@ -1380,8 +1933,6 @@ function closeP(){ // ═══════════════════════════════════════════════════════ // COPY URL (share link) // ═══════════════════════════════════════════════════════ -// Auto-detect the real URL so the share link is always correct -// regardless of whether Pages serves from docs/ or repo root. const CHAT_URL = window.location.origin + window.location.pathname; function showCopyFeedback(success){ @@ -1396,7 +1947,6 @@ function copyUrl(){ navigator.clipboard.writeText(CHAT_URL).then(()=>{ showCopyFeedback(true); }).catch(()=>{ - // fallback for non-secure context const ta=document.createElement('textarea'); ta.value=CHAT_URL; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.focus(); ta.select(); @@ -1409,8 +1959,6 @@ function copyUrl(){ // ═══════════════════════════════════════════════════════ // DOWNLOAD (desktop app) -// API keys are stored in localStorage only, never embedded in the DOM, -// so downloading outerHTML is safe — no sensitive data is captured. // ═══════════════════════════════════════════════════════ function dlApp(){ const html=document.documentElement.outerHTML; @@ -1448,7 +1996,6 @@ function setIdentity(userName, ghUser){ function applyIdentityUI(){ const meta = A.userMeta; - // Show user badge in header const ubadge = document.getElementById('huser-badge'); const uname = document.getElementById('huser-name'); if(meta && A.userName){ @@ -1456,10 +2003,9 @@ function applyIdentityUI(){ uname.style.display='inline'; uname.innerHTML = meta.emoji + ' ' + esc(A.userName) + ' '+esc(meta.title)+''; } - // Show controller button for supreme/main/founder + // Show controller button for supreme/main/founder (mobile only, desktop uses right sidebar) const ctrlBtn = document.getElementById('ctrlBtn'); if(ctrlBtn) ctrlBtn.style.display = (meta?.role==='supreme'||meta?.role==='main'||meta?.role==='founder') ? 'flex' : 'none'; - // Show controller quick replies const isCtrl = meta?.role==='supreme'||meta?.role==='main'||meta?.role==='founder'; ['qr-ctrl1','qr-ctrl2','qr-ctrl3'].forEach(id=>{ const el=document.getElementById(id); @@ -1468,7 +2014,7 @@ function applyIdentityUI(){ } // ═══════════════════════════════════════════════════════ -// TEAM STATUS PANEL +// TEAM STATUS PANEL (mobile slide-in) // ═══════════════════════════════════════════════════════ function renderTeamPanel(){ const ds = A.devStatus; @@ -1483,20 +2029,17 @@ function renderTeamPanel(){ const sync = ds.last_synced||'未知'; let h=''; - // Summary block h += '
' - + '
🦁 总控指挥台
' + + '
�� 总控指挥台
' + '
数据同步:'+esc(sync)+'
' + '
'; - // Stats row h += '
'; h += '
总开发者'+ts.length+' 人
'; h += '
🟢 推进中'+green+' 人
'; h += '
🟡 等待中'+yellow+' 人
'; h += '
'; - // Quick chat commands for controllers h += '
'; h += '
快捷指令
'; h += '📋 全员进度'; @@ -1506,7 +2049,6 @@ function renderTeamPanel(){ h += '⚙️ 后端进度'; h += '
'; - // Individual dev cards h += '
'; for(const dev of ts){ const isGreen = dev.status.includes('🟢'); @@ -1521,12 +2063,11 @@ function renderTeamPanel(){ h += '
'+esc(dev.status)+'
'; h += '
📦 '+esc(dev.modules.join('、'))+'
'; if(dev.next_step) h += '
👉 '+esc(dev.next_step)+'
'; - if(dev.waiting_for && dev.waiting_for.trim()!=='') h += '
⏳ 等待:'+esc(dev.waiting_for)+'
'; + if(dev.waiting_for && dev.waiting_for.trim()!=='') h += '
⏳ 等待:'+esc(dev.waiting_for)+'
'; h += '
'; } h += '
'; - // Server info if(ds.server_info){ const si = ds.server_info; h += '
服务器信息
'; @@ -1536,7 +2077,6 @@ function renderTeamPanel(){ h += ir2('技术栈',''+esc(si.tech_stack||'')+''); } - // Tech routing if(ds.tech_routing?.level_2_peer_help){ h += '
求助路由
'; for(const[area,who] of Object.entries(ds.tech_routing.level_2_peer_help)){ @@ -1547,10 +2087,54 @@ function renderTeamPanel(){ container.innerHTML = h; } +// ═══════════════════════════════════════════════════════ +// WINDOW RESIZE HANDLER +// ═══════════════════════════════════════════════════════ +window.addEventListener('resize', function(){ + const isCtrl=A.userMeta&&(A.userMeta.role==='supreme'||A.userMeta.role==='main'||A.userMeta.role==='founder'); + const rsEl=document.getElementById('right-sidebar'); + if(rsEl){ + if(isCtrl && window.innerWidth>=900){ + rsEl.style.display='flex'; + } else { + rsEl.style.display='none'; + } + } +}); + +// ═══════════════════════════════════════════════════════ +// MOBILE: VIEWPORT HEIGHT FIX & KEYBOARD HANDLER +// ═══════════════════════════════════════════════════════ +(function(){ + // Fix iOS/Android virtual keyboard pushing content via CSS custom property + if(window.visualViewport){ + var raf=0; + window.visualViewport.addEventListener('resize',function(){ + cancelAnimationFrame(raf); + raf=requestAnimationFrame(function(){ + var appEl=document.getElementById('app'); + if(appEl && appEl.classList.contains('on')){ + appEl.style.setProperty('--vh',window.visualViewport.height+'px'); + scrollB(); + } + }); + }); + } + // Close sidebar on any outside tap (mobile) + document.addEventListener('touchstart',function(e){ + var ls=document.getElementById('left-sidebar'); + if(ls && ls.classList.contains('open') && !ls.contains(e.target)){ + ls.classList.remove('open'); + var ov=document.getElementById('ls-overlay'); + if(ov) ov.classList.remove('on'); + } + },{passive:true}); +})(); + // ═══════════════════════════════════════════════════════ // BOOT // ═══════════════════════════════════════════════════════ boot(); - \ No newline at end of file + diff --git a/docs/使用指南.md b/docs/使用指南.md new file mode 100644 index 00000000..cf95ab1f --- /dev/null +++ b/docs/使用指南.md @@ -0,0 +1,345 @@ +# 🌀 铸渊助手 · 使用指南 + +--- + +## 一、网址 + +铸渊聊天室部署在 GitHub Pages 上,通过自定义域名访问: + +``` +https://guanghulab.com/ +``` + +备用网址(GitHub Pages 默认): +``` +https://qinfendebingshuo.github.io/guanghulab/ +``` + +> 💡 两个网址指向同一个网站。`guanghulab.com` 通过 `docs/CNAME` 配置,由 GitHub Pages 提供服务。 +> +> ⚠️ 旧网址 `…/docs/index.html` 已失效(系统重构后路径变了),请用上面的新网址。 + +打开后你会看到铸渊助手的登录界面(v5.0),有两种登录方式: +- **👤 访客模式** — 无需 API 密钥,直接体验演示 +- **🔑 编号登录** — 开发者提供 API 密钥,选择身份后进入完整交互 + +--- + +## 二、怎么操作(冰朔 / 创始人) + +### 第一步:打开铸渊聊天室 + +1. 打开浏览器,访问上面的网址 +2. 登录页面有两个标签: + - **👤 访客模式** — 点击「进入演示模式」直接进入(功能有限) + - **🔑 编号登录** — 选择开发者身份 → 输入 API 端点 → 输入 API 密钥 → 点「🔍 检测模型」→ 选择模型 → 开始对话 + +### 第二步:和铸渊对话 + +- 在底部输入框打字,按回车发送 +- 铸渊会以 AI 回复你(编号登录模式需要有效 API 密钥) +- 左侧可以查看聊天记录、切换对话 +- 右侧可以看到团队状态面板(管理员可见) + +### 第三步:如果页面显示空白或旧版本 + +- 等 1-2 分钟刷新,GitHub Pages 部署需要时间 +- **强制刷新**:按 `Ctrl+Shift+R`(Windows)或 `Cmd+Shift+R`(Mac) +- 如果还是旧版,清除浏览器缓存后重试 +- 确认网址是否正确(不要加 `/docs/`) + +--- + +## 三、如何部署新版本到网站(⭐ 重要) + +铸渊聊天室网站通过 GitHub Actions 自动部署。**只要代码合并到 `main` 分支,guanghulab.com 就会自动更新**。 + +> ### 🎯 快速回答:guanghulab.com 合并 PR 就能部署吗? +> +> **是的,合并 PR 就行。** guanghulab.com 由 GitHub Pages 提供服务,合并后 1-3 分钟自动更新。 +> +> ``` +> 合并 PR → GitHub Actions 自动部署(1-3分钟)→ 刷新 guanghulab.com 看到新版本 +> ``` +> +> **完整自动部署流程:** +> ``` +> 代码推送到功能分支 +> ↓ 自动触发 +> Staging 预演检查(模块完整性、安全检查) +> ↓ 检查通过 +> PR 等待冰朔合并 +> ↓ 冰朔点击 "Merge pull request"(唯一手动步骤) +> GitHub Actions 自动触发: +> ├── 🌀 GitHub Pages 部署 → guanghulab.com 更新 +> ├── 🚀 CD 服务器部署(状态看板同步) +> ├── 📚 图书馆目录自动更新 +> ├── 📋 模块文档自动生成 +> └── 📡 Notion 部署通知 +> ↓ 1-3 分钟后 +> guanghulab.com 自动更新完成 ✅ +> ``` + +### 方法一:合并 PR(推荐 · 唯一需要手动操作的步骤) + +当 Copilot Agent 或开发者在功能分支(如 `copilot/xxx`)上完成修改后: + +1. 打开仓库页面:https://github.com/qinfendebingshuo/guanghulab +2. 点击顶部的 **「Pull requests」** 标签 +3. 找到对应的 PR(如标题含 "v5.0" 或 "login redesign") +4. 检查修改内容,确认没问题 +5. 点击绿色的 **「Merge pull request」** 按钮 +6. 确认合并 — 选择 **「Squash and merge」** 或 **「Merge pull request」** 都可以 +7. 等待 1-3 分钟,GitHub Actions 会自动部署到网站 +8. 刷新网址 `https://qinfendebingshuo.github.io/guanghulab/` 查看新版本 + +> 💡 如果刷新后还是旧版,按 `Ctrl+Shift+R` 强制刷新,清除浏览器缓存。 + +### 方法二:触发冰朔人格体诊断部署 + +如果需要诊断部署问题,可以触发冰朔人格体: + +1. 打开仓库页面:https://github.com/qinfendebingshuo/guanghulab +2. 点击 **「Issues」** 标签 → 点击 **「New issue」** +3. 选择 **「🧊 冰朔人格体 · 部署诊断」** 模板 +4. 填写标题(如「检查部署状态」),点击「Submit new issue」 +5. 冰朔人格体会自动运行,在 Issue 评论中汇报诊断结果 + +或者在任意 Issue 中评论以下关键词即可触发: +``` +启动冰朔人格体 +``` + +### 方法三:手动触发部署工作流 + +1. 打开仓库页面 → 点击 **「Actions」** 标签 +2. 左侧找到 **「🌀 部署铸渊聊天室 (GitHub Pages)」** +3. 点击右侧的 **「Run workflow」** 按钮 +4. Branch 选择 **`main`** +5. 点击绿色的 **「Run workflow」** +6. 等待 1-3 分钟完成部署 + +### 查看部署状态 + +1. 打开仓库页面 → 点击 **「Actions」** 标签 +2. 查看最近的工作流运行记录 +3. 绿色 ✅ = 部署成功,红色 ❌ = 部署失败 +4. 如果失败,点击进去查看错误日志 + +--- + +## 四、开发者(协作者)怎么上传模块 + +### 你只需要做一件事:把你的模块文件放到指定目录 + +每个开发者有自己负责的模块目录,例如: + +| 开发者 | 模块目录 | +|--------|----------| +| 肥猫 DEV-002 | `m01-login/`、`m03-personality/` | +| 燕樊 DEV-003 | `m07-dialogue-ui/`、`m10-cloud/`、`m15-cloud-drive/` | +| 小草莓 DEV-005 | `m12-kanban/` | +| 花尔 DEV-009 | `m05-user-center/` | +| 桔子 DEV-010 | `m06-ticket/`、`m11-module/` | + +### 上传步骤(最简单方式) + +1. 打开仓库页面:https://github.com/qinfendebingshuo/guanghulab +2. 找到你负责的模块目录(如 `m01-login/`) +3. 点击「Add file」→「Upload files」 +4. 拖拽你的文件上去 +5. 在底部写一句说明,点击「Commit changes」 + +### 注意事项 + +- ✅ **只在你的模块目录里上传文件** +- ❌ **不要修改仓库根目录的文件**(如 package.json、README 等) +- ❌ **不要修改 `.github/` 目录**(工作流、配置文件) +- ❌ **不要修改 `scripts/` 目录**(自动化脚本) +- ❌ **不要修改 `docs/` 目录**(铸渊聊天室界面) + +> 仓库已设置 CODEOWNERS 保护,核心文件的修改需要仓库所有者审批。 + +--- + +## 五、常见问题 + +### Q: guanghulab.com 合并 PR 就能自动部署吗? +- **是的。** `guanghulab.com` 由 GitHub Pages 提供服务(通过 `docs/CNAME` 配置) +- 合并 PR 到 main 后,`deploy-pages.yml` 自动将 `docs/` 部署到 GitHub Pages +- GitHub Pages 自动为 `guanghulab.com` 域名提供服务 +- **1-3 分钟后刷新 guanghulab.com 即可看到新版本** + +### Q: 什么时候能看到新模块/新版本上线? +- **PR 合并到 main 后 1-3 分钟**即可看到新版本 +- 如果代码还在功能分支(PR 未合并),网站不会更新 +- 合并后刷新 `https://guanghulab.com/` 即可 + +### Q: 我还需要手动操作什么吗? +- **只需要一步:合并 PR**(点击绿色的 "Merge pull request" 按钮) +- 部署、通知、文档更新全部自动完成,无需其他操作 +- 如果部署后有问题,可以在 Issue 中评论「启动冰朔人格体」进行自动诊断 + +### Q: 铸渊聊天室打不开? +- 确认网址正确:`https://guanghulab.com/` 或 `https://qinfendebingshuo.github.io/guanghulab/` +- 不要在后面加 `/docs/index.html` +- 等几分钟再刷新(部署有延迟) +- 按 `Ctrl+Shift+R` 强制刷新,清除缓存 + +### Q: 网站显示旧版本,不是最新的? +- **最常见原因**:新代码还在功能分支(PR 未合并到 main) +- 解决方法:去 GitHub 仓库 → Pull requests → 合并对应的 PR +- 合并后等 1-3 分钟,再强制刷新页面 +- 详见本指南「三、如何部署新版本到网站」 + +### Q: 我上传了文件,但好像不对? +- 确认你是在自己负责的模块目录里上传 +- 不要在仓库根目录直接上传 +- 如果不确定,联系冰朔确认 + +### Q: 我不小心改了不该改的文件? +- 如果开启了 Branch Protection,你的改动会变成 PR 等待审批 +- 联系冰朔处理即可 + +--- + +## 六、仓库安全设置 — 开启分支保护(⭐ 冰朔必做) + +为了让 CODEOWNERS 保护真正生效,需要在 GitHub 仓库设置中开启分支保护规则。 +以下是**详细的逐步操作**: + +### 第一步:进入仓库设置 + +1. 打开浏览器,访问仓库:https://github.com/qinfendebingshuo/guanghulab +2. 点击页面顶部导航栏的 **「Settings」**(齿轮图标,在最右边) + > ⚠️ 只有仓库所有者才能看到 Settings 选项 + +### 第二步:进入分支规则页面 + +3. 在左侧菜单中,找到 **「Code and automation」** 分类 +4. 点击其中的 **「Branches」** + +### 第三步:添加分支保护规则 + +5. 在 **「Branch protection rules」** 区域,点击 **「Add branch protection rule」** 按钮 + > 如果看到 **「Add classic branch protection rule」** 也可以点击 + +### 第四步:配置规则 + +6. **Branch name pattern** 填写:`main` +7. 勾选以下选项(打 ✅ 的必须勾选): + +| 选项 | 是否勾选 | 说明 | +|------|----------|------| +| ✅ **Require a pull request before merging** | 必须勾选 | 所有修改必须通过 PR 合并,不能直接推送 | +| ↳ ✅ **Require approvals** | 建议勾选 | PR 需要至少 1 个审批才能合并 | +| ↳ ✅ **Require review from Code Owners** | 必须勾选 | CODEOWNERS 文件指定的人必须审批 | +| ❌ Require status checks to pass before merging | 可以不勾 | 除非你想要求 CI 检查通过 | +| ❌ Require branches to be up to date | 可以不勾 | 除非你想要求分支是最新的 | +| ✅ **Do not allow bypassing the above settings** | 建议勾选 | 确保管理员也遵守规则 | + +8. 其他选项保持默认即可 + +### 第五步:保存 + +9. 滚动到页面底部,点击绿色按钮 **「Create」** 或 **「Save changes」** + +### 完成 ✅ + +设置完成后的效果: +- 协作者推送到 `main` 分支的修改 → 如果涉及受保护的文件 → 会自动变成 PR → 需要你(冰朔)审批 +- 协作者在自己的模块目录里操作 → 不受影响,可以自由提交 +- 受保护的目录包括:`.github/`、`scripts/`、`docs/`、`backend-integration/`、`signal-log/`、根目录配置文件 + +--- + +## 七、GitHub Pages 部署配置检查 + +如果网站一直无法更新,请检查 GitHub Pages 是否配置正确: + +### 检查步骤 + +1. 打开仓库 → **Settings** → 左侧菜单 **「Pages」** +2. 确认 **Source(来源)** 设置为:**GitHub Actions**(不是 "Deploy from a branch") +3. 如果显示的是 "Deploy from a branch",请切换为 **GitHub Actions** + +> 💡 当前仓库使用 `deploy-pages.yml` 工作流自动部署 `docs/` 目录的内容。 +> 每次 `main` 分支的 `docs/` 目录有变更,GitHub Actions 会自动触发部署。 + +### 如果 Source 是 "Deploy from a branch",请按以下步骤修改: + +1. 打开仓库 → **Settings** → **Pages** +2. 在 **Build and deployment** 区域 +3. **Source** 下拉菜单选择 **「GitHub Actions」** +4. 保存后,下次推送到 main 分支时就会自动部署 + +--- + +## 八、自定义域名设置(⭐ 冰朔需操作) + +如果你有自己的域名(如 guanghulab.com),可以让网站使用自定义域名访问。 + +### 第一步:DNS 配置(在域名注册商操作) + +1. 登录你的域名注册商管理面板(如阿里云、腾讯云、GoDaddy 等) +2. 找到 **DNS 解析设置** +3. 添加以下记录(任选一种方式): + +**方式 A — CNAME 记录(推荐)**: +| 记录类型 | 主机记录 | 记录值 | +|----------|---------|--------| +| CNAME | @ 或 www | `qinfendebingshuo.github.io` | + +**方式 B — A 记录**: +| 记录类型 | 主机记录 | 记录值 | +|----------|---------|--------| +| A | @ | `185.199.108.153` | +| A | @ | `185.199.109.153` | +| A | @ | `185.199.110.153` | +| A | @ | `185.199.111.153` | + +> 💡 DNS 生效需要 5 分钟到 48 小时不等。 + +### 第二步:GitHub 仓库配置 + +1. 打开仓库 → **Settings** → 左侧菜单 **「Pages」** +2. 在 **Custom domain** 输入框中填入你的域名(如 `guanghulab.com`) +3. 点击 **Save** +4. 等待 DNS 验证通过(页面会显示绿色 ✅) +5. 勾选 **✅ Enforce HTTPS** + +### 第三步:确认 + +- 仓库中的 `docs/CNAME` 文件已自动创建,内容为你的域名 +- 如果域名不对,直接编辑 `docs/CNAME` 文件即可 +- 部署后访问你的域名,应该能看到铸渊助手 + +--- + +## 九、铸渊人格协议 · 模块管理 + +铸渊对网站模块有完整的生命周期管理能力(部署、回收、管理、修改、驱动)。 + +### 本地模块检查命令 + +```bash +npm run module:protocol -- inspect # 全模块检查 +npm run module:protocol -- status # 模块状态汇总 +npm run module:protocol -- preview # 本地预演报告 +npm run module:protocol -- recover m01-login # 模块回收检查 +``` + +### 预演→生产部署流程 + +1. 开发者推送模块到 `m**/` 目录 +2. 创建 PR 到 main 分支 +3. 铸渊预演系统 (`staging-preview.yml`) 自动运行检查 +4. 冰朔查看预演报告,确认无误 +5. 合并 PR → 触发生产部署 (`deploy-pages.yml`) +6. (可选)评论「启动冰朔人格体」进行部署后诊断 + +> 📖 完整协议文档见:`.github/brain/module-protocol.md` + +--- + +💙 铸渊 · 2026-03-07 diff --git a/frontend/HoloLake-App b/frontend/HoloLake-App deleted file mode 160000 index f8340b65..00000000 --- a/frontend/HoloLake-App +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f8340b659c60daaa15f57f2541200a2ca14b2c84 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..1adabb1f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1 @@ +# frontend 模块 diff --git a/help-center/README.md b/help-center/README.md new file mode 100644 index 00000000..3928b85d --- /dev/null +++ b/help-center/README.md @@ -0,0 +1 @@ +# help-center 模块 diff --git a/m18-health-check b/m18-health-check deleted file mode 160000 index 4c5dc4e8..00000000 --- a/m18-health-check +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c5dc4e8be2bcba1acee37c1bf93e18372ae4795 diff --git a/m18-health-check/README.md b/m18-health-check/README.md new file mode 100644 index 00000000..857abf66 --- /dev/null +++ b/m18-health-check/README.md @@ -0,0 +1,5 @@ +# M18 系统健康检查 +- 负责人:待分配 +- 状态:规划中 +- 技术栈:待定 +- 依赖模块:无 diff --git a/m18-health-check/package.json b/m18-health-check/package.json new file mode 100644 index 00000000..1887483c --- /dev/null +++ b/m18-health-check/package.json @@ -0,0 +1,16 @@ +{ + "name": "hololake-health", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "express": "^5.2.1" + } +} diff --git a/m18-health-check/server.js b/m18-health-check/server.js new file mode 100644 index 00000000..40ed6a97 --- /dev/null +++ b/m18-health-check/server.js @@ -0,0 +1,240 @@ +const express = require('express'); +const os = require('os'); + +const app = express(); +const PORT = 3000; + +// 记录服务器启动时间 +const SERVER_START_TIME = new Date(); + +// 解析JSON请求体 +app.use(express.json()); + +// ========== 数据存储 ========== +let heartbeats = []; +const patrolHistory = []; +const alerts = []; +const HEARTBEAT_TIMEOUT = 5 * 60 * 1000; // 5分钟 + +// ========== 告警检查函数 ========== +function checkAlerts(memPercent) { + let level = null; + let message = ''; + + if (memPercent > 90) { + level = 'critical'; + message = `内存使用率过高: ${memPercent}%`; + } else if (memPercent > 80) { + level = 'warning'; + message = `内存使用率警告: ${memPercent}%`; + } + + if (level) { + const alert = { + timestamp: new Date().toISOString(), + level: level, + message: message, + memoryPercent: memPercent + }; + alerts.push(alert); + + if (alerts.length > 20) { + alerts.shift(); + } + + console.log(`[ALERT] ${level.toUpperCase()}: ${message}`); + } +} + +// ========== 在线状态检查函数 ========== +function checkOnlineStatus() { + const now = Date.now(); + const onlineServices = []; + const offlineServices = []; + + const latestHeartbeats = {}; + heartbeats.forEach(hb => { + const source = hb.service || 'unknown'; + if (!latestHeartbeats[source] || new Date(hb.timestamp) > new Date(latestHeartbeats[source].timestamp)) { + latestHeartbeats[source] = hb; + } + }); + + for (const [source, hb] of Object.entries(latestHeartbeats)) { + const hbTime = new Date(hb.timestamp).getTime(); + const isOnline = (now - hbTime) < HEARTBEAT_TIMEOUT; + + const serviceInfo = { + source: source, + lastHeartbeat: hb.timestamp, + secondsAgo: Math.floor((now - hbTime) / 1000), + status: isOnline ? 'online' : 'offline' + }; + + if (isOnline) { + onlineServices.push(serviceInfo); + } else { + offlineServices.push(serviceInfo); + } + } + + return { online: onlineServices, offline: offlineServices }; +} + +// ========== API路由 ========== + +// GET /api/health - 快速健康检查 +app.get('/api/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString() + }); +}); + +// GET /api/status - 详细系统状态 +app.get('/api/status', (req, res) => { + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + const memPercent = Math.round((usedMem / totalMem) * 100); + + checkAlerts(memPercent); + + res.json({ + service: 'HoloLake Health Monitor', + version: '1.0.0', + status: 'running', + timestamp: new Date().toISOString(), + uptime: { + seconds: Math.floor((Date.now() - SERVER_START_TIME.getTime()) / 1000), + startedAt: SERVER_START_TIME.toISOString() + }, + system: { + platform: os.platform(), + arch: os.arch(), + hostname: os.hostname(), + nodeVersion: process.version + }, + memory: { + totalMB: Math.round(totalMem / 1024 / 1024), + usedMB: Math.round(usedMem / 1024 / 1024), + freeMB: Math.round(freeMem / 1024 / 1024), + usagePercent: memPercent + '%' + }, + services: checkOnlineStatus() + }); +}); + +// POST /api/heartbeat - 心跳上报 +app.post('/api/heartbeat', (req, res) => { + const heartbeat = { + timestamp: new Date().toISOString(), + service: req.body.service || 'unknown', + status: req.body.status || 'ok' + }; + heartbeats.push(heartbeat); + + res.json({ + received: true, + heartbeat: heartbeat + }); +}); + +// GET /api/heartbeat/history - 查看心跳历史 +app.get('/api/heartbeat/history', (req, res) => { + res.json({ + total: heartbeats.length, + heartbeats: heartbeats.slice(-20) + }); +}); + +// GET /api/heartbeat/online - 查看在线服务 +app.get('/api/heartbeat/online', (req, res) => { + const status = checkOnlineStatus(); + res.json({ + timestamp: new Date().toISOString(), + onlineCount: status.online.length, + offlineCount: status.offline.length, + online: status.online, + offline: status.offline + }); +}); + +// GET /api/patrol/history - 查看巡检历史 +app.get('/api/patrol/history', (req, res) => { + res.json({ + total: patrolHistory.length, + patrols: patrolHistory + }); +}); + +// GET /api/alerts - 查看告警历史 +app.get('/api/alerts', (req, res) => { + res.json({ + total: alerts.length, + critical: alerts.filter(a => a.level === 'critical').length, + warning: alerts.filter(a => a.level === 'warning').length, + alerts: alerts + }); +}); + +// GET / - 根路径欢迎页 +app.get('/', (req, res) => { + res.json({ + service: 'HoloLake Health Check & Status Report', + version: '2.0.0', + endpoints: [ + 'GET /api/health - 快速健康检查', + 'GET /api/status - 详细系统状态', + 'POST /api/heartbeat - 心跳上报', + 'GET /api/heartbeat/history - 心跳历史', + 'GET /api/heartbeat/online - 在线服务', + 'GET /api/patrol/history - 巡检历史', + 'GET /api/alerts - 告警历史' + ] + }); +}); + +// ========== 定时自动巡检 ========== +setInterval(() => { + console.log('[PATROL] 开始定时巡检...'); + + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + const memPercent = Math.round((usedMem / totalMem) * 100); + + const patrolRecord = { + timestamp: new Date().toISOString(), + memory: { + totalMB: Math.round(totalMem / 1024 / 1024), + usedMB: Math.round(usedMem / 1024 / 1024), + usagePercent: memPercent + }, + uptime: Math.floor((Date.now() - SERVER_START_TIME.getTime()) / 1000), + heartbeatCount: heartbeats.length + }; + + patrolHistory.push(patrolRecord); + if (patrolHistory.length > 50) { + patrolHistory.shift(); + } + + checkAlerts(memPercent); + + console.log(`[PATROL] 巡检完成 - 内存使用: ${memPercent}%`); +}, 60000); + +// 启动服务器 +app.listen(PORT, () => { + console.log('='.repeat(50)); + console.log('🌊 HoloLake Health Check & Status Report'); + console.log('📡 服务运行在: http://localhost:' + PORT); + console.log('💚 GET /api/health - 健康检查'); + console.log('📊 GET /api/status - 系统状态'); + console.log('💓 POST /api/heartbeat - 心跳上报'); + console.log('🔄 定时巡检 - 每60秒'); + console.log('🚨 异常告警 - 内存>80%'); + console.log('⏰ 心跳超时 - 5分钟'); + console.log('='.repeat(50)); +}); \ No newline at end of file diff --git a/multi-persona/README.md b/multi-persona/README.md new file mode 100644 index 00000000..a04066eb --- /dev/null +++ b/multi-persona/README.md @@ -0,0 +1 @@ +# multi-persona 模块 diff --git a/notification/README.md b/notification/README.md new file mode 100644 index 00000000..0eecec28 --- /dev/null +++ b/notification/README.md @@ -0,0 +1 @@ +# notification 模块 diff --git a/package.json b/package.json index 269950de..2fd78894 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,12 @@ "broadcast:distribute": "node scripts/distribute-broadcasts.js", "esp:process": "node scripts/esp-email-processor.js", "psp:inspect": "node scripts/psp-inspection.js", - "repo:map": "node scripts/generate-repo-map.js" + "repo:map": "node scripts/generate-repo-map.js", + "deploy:agent": "node scripts/bingshuo-deploy-agent.js", + "module:protocol": "node scripts/zhuyuan-module-protocol.js", + "proxy:start": "node backend-integration/api-proxy.js", + "notion:poll": "node scripts/notion-signal-bridge.js poll", + "notion:health": "node scripts/notion-signal-bridge.js health" }, "dependencies": { "imapflow": "^1.2.12", diff --git a/persona-selector/README.md b/persona-selector/README.md new file mode 100644 index 00000000..a5b1930d --- /dev/null +++ b/persona-selector/README.md @@ -0,0 +1 @@ +# persona-selector 模块 diff --git a/portal/README.md b/portal/README.md new file mode 100644 index 00000000..50ebf512 --- /dev/null +++ b/portal/README.md @@ -0,0 +1 @@ +# portal 模块 diff --git a/scripts/bingshuo-deploy-agent.js b/scripts/bingshuo-deploy-agent.js new file mode 100644 index 00000000..ac45e70a --- /dev/null +++ b/scripts/bingshuo-deploy-agent.js @@ -0,0 +1,492 @@ +/** + * 🧊 冰朔人格体 · 自动部署诊断Agent + * + * 功能: + * 1. 自动检查所有 GitHub Actions 工作流运行状态 + * 2. 诊断失败原因,提取错误日志 + * 3. 自动重新运行失败的工作流 + * 4. 生成纯中文部署报告(Issue 评论) + * + * 触发方式: + * - Issue 评论包含 "启动冰朔人格体" + * - workflow_dispatch 手动触发 + * - Issue 模板创建 + * + * 环境变量: + * GITHUB_TOKEN - GitHub API 令牌 + * GITHUB_REPOSITORY - owner/repo + * ISSUE_NUMBER - (可选)回复到指定 Issue + * DEPLOY_ACTION - check | rerun | full(默认 full) + * HOURS_BACK - 检查最近多少小时的运行记录(默认 24) + */ + +const https = require('https'); +const fs = require('fs'); + +// === 配置 === +const REPO = process.env.GITHUB_REPOSITORY || ''; +const TOKEN = process.env.GITHUB_TOKEN || ''; +const ISSUE_NUMBER = process.env.ISSUE_NUMBER || ''; +const ISSUE_BODY = process.env.ISSUE_BODY || ''; + +// 从 Issue body 解析参数(支持 Issue 模板触发) +function parseActionFromBody(body) { + if (body.includes('只看报告')) return 'check'; + if (body.includes('只重新运行')) return 'rerun'; + return 'full'; +} + +function parseHoursFromBody(body) { + if (body.includes('最近一周')) return 168; + if (body.includes('72小时')) return 72; + if (body.includes('48小时')) return 48; + return 24; +} + +const ACTION = process.env.DEPLOY_ACTION || parseActionFromBody(ISSUE_BODY); +const HOURS_BACK = parseInt(process.env.HOURS_BACK || String(parseHoursFromBody(ISSUE_BODY)), 10); + +const [OWNER, REPO_NAME] = REPO.split('/'); + +if (!OWNER || !REPO_NAME || !TOKEN) { + console.error('❌ 缺少必要环境变量:GITHUB_REPOSITORY, GITHUB_TOKEN'); + process.exit(1); +} + +// === GitHub API 请求封装 === +function githubAPI(method, path, data) { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path, + method, + headers: { + 'Authorization': `Bearer ${TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'BingShuo-Deploy-Agent' + } + }; + if (data) { + options.headers['Content-Type'] = 'application/json'; + } + + const req = https.request(options, (res) => { + let body = ''; + res.on('data', chunk => { body += chunk; }); + res.on('end', () => { + try { + const json = body ? JSON.parse(body) : {}; + resolve({ status: res.statusCode, data: json }); + } catch { + resolve({ status: res.statusCode, data: body }); + } + }); + }); + req.on('error', reject); + if (data) req.write(JSON.stringify(data)); + req.end(); + }); +} + +// === 获取最近的工作流运行记录 === +async function getRecentRuns() { + const since = new Date(Date.now() - HOURS_BACK * 60 * 60 * 1000).toISOString(); + const result = await githubAPI('GET', + `/repos/${OWNER}/${REPO_NAME}/actions/runs?per_page=100&created=%3E${since}` + ); + if (result.status !== 200) { + console.error('❌ 获取工作流运行记录失败:', result.status); + return []; + } + return result.data.workflow_runs || []; +} + +// === 获取某次运行的作业详情 === +async function getJobsForRun(runId) { + const result = await githubAPI('GET', + `/repos/${OWNER}/${REPO_NAME}/actions/runs/${runId}/jobs` + ); + if (result.status !== 200) return []; + return result.data.jobs || []; +} + +// === 从 jobs 中提取失败信息 === +function extractFailureInfo(jobs) { + const failures = []; + for (const job of jobs) { + if (job.conclusion === 'failure') { + const failedSteps = (job.steps || []).filter(s => s.conclusion === 'failure'); + failures.push({ + jobName: job.name, + failedSteps: failedSteps.map(s => ({ + name: s.name, + number: s.number + })), + startedAt: job.started_at, + completedAt: job.completed_at + }); + } + } + return failures; +} + +// === 重新运行失败的工作流 === +async function rerunFailedJobs(runId) { + const result = await githubAPI('POST', + `/repos/${OWNER}/${REPO_NAME}/actions/runs/${runId}/rerun-failed-jobs` + ); + return result.status === 201; +} + +// === 分类工作流运行记录 === +function categorizeRuns(runs) { + const categories = { + success: [], + failure: [], + cancelled: [], + inProgress: [], + actionRequired: [], + skipped: [] + }; + + for (const run of runs) { + const status = run.status; + const conclusion = run.conclusion; + + if (status === 'in_progress' || status === 'queued' || status === 'waiting') { + categories.inProgress.push(run); + } else if (conclusion === 'success') { + categories.success.push(run); + } else if (conclusion === 'failure') { + categories.failure.push(run); + } else if (conclusion === 'cancelled') { + categories.cancelled.push(run); + } else if (conclusion === 'action_required') { + categories.actionRequired.push(run); + } else { + categories.skipped.push(run); + } + } + + return categories; +} + +// === 格式化时间 === +function formatTime(isoStr) { + if (!isoStr) return '未知'; + const d = new Date(isoStr); + return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }); +} + +// === 获取分支显示名称 === +function branchDisplayName(branch) { + if (branch === 'main') return '主分支 (main)'; + if (branch && branch.startsWith('copilot/')) return `Copilot分支 (${branch})`; + return branch || '未知'; +} + +// === 生成诊断报告 === +async function generateReport(runs) { + const categories = categorizeRuns(runs); + const total = runs.length; + + let report = `## 🧊 冰朔人格体 · 部署诊断报告\n\n`; + report += `> 📅 报告时间:${formatTime(new Date().toISOString())}\n`; + report += `> 📊 检查范围:最近 ${HOURS_BACK} 小时内的工作流运行记录\n\n`; + + // --- 总览 --- + report += `### 📊 总览\n\n`; + report += `| 状态 | 数量 |\n|------|------|\n`; + report += `| ✅ 成功 | ${categories.success.length} |\n`; + report += `| ❌ 失败 | ${categories.failure.length} |\n`; + report += `| ⏳ 进行中 | ${categories.inProgress.length} |\n`; + report += `| ⚠️ 需要操作 | ${categories.actionRequired.length} |\n`; + report += `| 🚫 已取消 | ${categories.cancelled.length} |\n`; + report += `| 总计 | ${total} |\n\n`; + + // --- 失败详情 --- + if (categories.failure.length > 0) { + report += `### ❌ 失败的工作流\n\n`; + report += `> 冰朔不用担心,下面是每个失败工作流的详细诊断:\n\n`; + + for (let i = 0; i < categories.failure.length; i++) { + const run = categories.failure[i]; + const jobs = await getJobsForRun(run.id); + const failureInfo = extractFailureInfo(jobs); + + report += `#### ${i + 1}. ${run.name}\n\n`; + report += `- 🌿 分支:${branchDisplayName(run.head_branch)}\n`; + report += `- ⏰ 时间:${formatTime(run.created_at)}\n`; + report += `- 🔗 触发方式:${translateEvent(run.event)}\n`; + report += `- 📝 提交信息:${run.display_title || '无'}\n`; + + if (failureInfo.length > 0) { + report += `- 💥 失败的作业:\n`; + for (const f of failureInfo) { + report += ` - **${f.jobName}**\n`; + if (f.failedSteps.length > 0) { + report += ` - 失败步骤:${f.failedSteps.map(s => `\`${s.name}\``).join('、')}\n`; + } + } + } + + report += `- 💡 建议:${generateSuggestion(run, failureInfo)}\n\n`; + } + } + + // --- 需要操作的工作流 --- + if (categories.actionRequired.length > 0) { + report += `### ⚠️ 需要操作的工作流\n\n`; + report += `> 这些工作流需要审批或配置才能继续运行:\n\n`; + + for (const run of categories.actionRequired) { + report += `- **${run.name}** · ${branchDisplayName(run.head_branch)} · ${formatTime(run.created_at)}\n`; + } + report += `\n`; + } + + // --- 正在进行的工作流 --- + if (categories.inProgress.length > 0) { + report += `### ⏳ 正在运行的工作流\n\n`; + for (const run of categories.inProgress) { + report += `- **${run.name}** · ${branchDisplayName(run.head_branch)} · 开始于 ${formatTime(run.created_at)}\n`; + } + report += `\n`; + } + + // --- 成功的工作流(简洁列表)--- + if (categories.success.length > 0) { + report += `### ✅ 成功的工作流\n\n`; + report += `
\n点击展开成功列表(${categories.success.length} 个)\n\n`; + for (const run of categories.success) { + report += `- ✅ **${run.name}** · ${branchDisplayName(run.head_branch)} · ${formatTime(run.created_at)}\n`; + } + report += `\n
\n\n`; + } + + return report; +} + +// === 翻译触发事件 === +function translateEvent(event) { + const map = { + push: '代码推送', + pull_request: 'PR 合并请求', + workflow_dispatch: '手动触发', + schedule: '定时任务', + issues: 'Issue 创建', + issue_comment: 'Issue 评论', + dynamic: '自动触发' + }; + return map[event] || event; +} + +// === 生成修复建议 === +function generateSuggestion(run, failureInfo) { + const name = run.name.toLowerCase(); + const branch = run.head_branch || ''; + + // 根据工作流名称和失败信息给出建议 + if (name.includes('pages') || name.includes('部署') || name.includes('deploy')) { + return '这是页面部署失败。通常是 docs/ 目录下文件格式问题。冰朔人格体可以尝试重新部署。'; + } + if (name.includes('结构检查') || name.includes('check-structure')) { + return '模块结构检查失败,可能是开发者上传的模块缺少 README.md 文件。请通知对应开发者补充。'; + } + if (name.includes('bridge') || name.includes('notion')) { + return '这是和 Notion 的同步失败。通常是 Notion API Token 过期或网络问题。建议稍后重试。'; + } + if (name.includes('contract') || name.includes('hli')) { + return '接口契约检查失败。可能是新接口的 schema 不匹配。需要检查 src/schemas/ 下的定义文件。'; + } + if (name.includes('模块文档') || name.includes('module-doc')) { + return '模块文档自动生成失败。通常是模块目录结构不符合规范。请检查最近上传的模块。'; + } + + // 根据失败步骤给出更具体的建议 + if (failureInfo.length > 0) { + const stepNames = failureInfo.flatMap(f => f.failedSteps.map(s => s.name.toLowerCase())); + if (stepNames.some(s => s.includes('npm') || s.includes('install'))) { + return '依赖安装失败。可能是 package.json 配置问题或网络超时。建议重试。'; + } + if (stepNames.some(s => s.includes('build'))) { + return '构建步骤失败。代码可能有语法错误。需要检查最近的代码变更。'; + } + if (stepNames.some(s => s.includes('test'))) { + return '测试失败。可能是新代码引入了问题。需要开发者修复后重新提交。'; + } + } + + return '建议重新运行此工作流。如果持续失败,冰朔可以告诉铸渊具体情况,铸渊会帮你排查。'; +} + +// === 执行重新运行 === +async function executeRerun(failedRuns) { + if (failedRuns.length === 0) { + return '\n### 🔄 重新运行\n\n没有需要重新运行的失败工作流。一切正常!✨\n\n'; + } + + let report = `\n### 🔄 自动重新运行\n\n`; + report += `> 冰朔人格体正在自动重新运行失败的工作流...\n\n`; + + let rerunCount = 0; + for (const run of failedRuns) { + // 只重试主分支的失败任务,避免重试无关分支 + const success = await rerunFailedJobs(run.id); + if (success) { + report += `- ✅ 已重新触发:**${run.name}** (${branchDisplayName(run.head_branch)})\n`; + rerunCount++; + } else { + report += `- ⚠️ 无法重新触发:**${run.name}**(可能需要更高权限或该运行已过期)\n`; + } + } + + report += `\n📌 已重新触发 ${rerunCount}/${failedRuns.length} 个工作流。预计 3-5 分钟后可查看结果。\n`; + report += `如果还是失败,冰朔可以再次启动冰朔人格体,我会继续诊断。\n\n`; + + return report; +} + +// === 生成总结 === +function generateSummary(categories) { + let summary = `### 📋 总结\n\n`; + + if (categories.failure.length === 0 && categories.actionRequired.length === 0) { + summary += `🎉 **所有工作流运行正常!** 冰朔不需要做任何操作。\n\n`; + summary += `系统健康状态:✅ 正常\n\n`; + } else if (categories.failure.length > 0) { + summary += `⚠️ 有 ${categories.failure.length} 个工作流失败。冰朔人格体已自动分析原因并尝试修复。\n\n`; + summary += `如果问题持续,冰朔可以:\n`; + summary += `1. 再次创建 Issue 触发冰朔人格体重新诊断\n`; + summary += `2. 把错误信息发给铸渊,铸渊会帮你定位具体代码问题\n\n`; + } + + summary += `---\n`; + summary += `*—— 🧊 冰朔人格体 · 自动部署诊断Agent*\n`; + summary += `*由铸渊为冰朔专属开发 · 纯语言驱动,无需点击任何按钮*\n`; + + return summary; +} + +// === 发布报告到 Issue === +async function postReport(report) { + if (ISSUE_NUMBER) { + // 回复到已有 Issue + await githubAPI('POST', + `/repos/${OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/comments`, + { body: report } + ); + // 添加标签 + await githubAPI('POST', + `/repos/${OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels`, + { labels: ['🧊deploy-report', '✅answered'] } + ); + // 移除 pending 标签(如果不存在会返回404,属正常情况) + const removeResult = await githubAPI('DELETE', + `/repos/${OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels/${encodeURIComponent('pending')}` + ); + if (removeResult.status !== 200 && removeResult.status !== 404) { + console.warn('⚠️ 移除 pending 标签时出现异常:', removeResult.status); + } + + console.log(`✅ 报告已发布到 Issue #${ISSUE_NUMBER}`); + } else { + // 创建新 Issue + const result = await githubAPI('POST', + `/repos/${OWNER}/${REPO_NAME}/issues`, + { + title: `🧊 冰朔人格体 · 部署诊断报告 · ${new Date().toLocaleDateString('zh-CN')}`, + body: report, + labels: ['🧊deploy-report'] + } + ); + if (result.status === 201) { + console.log(`✅ 诊断报告已创建:Issue #${result.data.number}`); + } else { + console.error('❌ 创建 Issue 失败:', result.status); + } + } +} + +// === 主流程 === +async function main() { + console.log('🧊 冰朔人格体启动...'); + console.log(`📊 检查范围:最近 ${HOURS_BACK} 小时`); + console.log(`🔧 执行模式:${ACTION}`); + + // 1. 获取最近的工作流运行记录 + const runs = await getRecentRuns(); + console.log(`📋 获取到 ${runs.length} 条运行记录`); + + if (runs.length === 0) { + const report = `## 🧊 冰朔人格体 · 部署诊断报告\n\n` + + `最近 ${HOURS_BACK} 小时内没有任何工作流运行记录。系统很安静。✨\n\n` + + `---\n*—— 🧊 冰朔人格体*`; + await postReport(report); + return; + } + + // 2. 分类 + const categories = categorizeRuns(runs); + + // 3. 生成诊断报告 + let report = ''; + + if (ACTION === 'check' || ACTION === 'full') { + report += await generateReport(runs); + } + + // 4. 如果是 full 模式,自动重新运行失败的工作流 + if (ACTION === 'full' || ACTION === 'rerun') { + report += await executeRerun(categories.failure); + } + + // 5. 总结 + report += generateSummary(categories); + + // 6. 发布报告 + await postReport(report); + + // 7. 更新 memory.json + await updateMemory(categories); + + console.log('🧊 冰朔人格体任务完成。'); +} + +// === 更新铸渊记忆 === +async function updateMemory(categories) { + try { + const memoryPath = '.github/brain/memory.json'; + if (!fs.existsSync(memoryPath)) return; + + const memory = JSON.parse(fs.readFileSync(memoryPath, 'utf8')); + + // 添加事件记录 + if (!memory.recent_events) memory.recent_events = []; + memory.recent_events.unshift({ + timestamp: new Date().toISOString(), + type: 'bingshuo_deploy_agent', + result: categories.failure.length === 0 ? 'all_pass' : 'has_failures', + details: { + total: categories.success.length + categories.failure.length + categories.inProgress.length, + success: categories.success.length, + failure: categories.failure.length, + in_progress: categories.inProgress.length + } + }); + + // 保留最近100条 + memory.recent_events = memory.recent_events.slice(0, 100); + + fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + '\n'); + console.log('📝 铸渊记忆已更新'); + } catch (err) { + console.warn('⚠️ 更新记忆失败(不影响报告):', err.message); + } +} + +// === 执行 === +main().catch(err => { + console.error('❌ 冰朔人格体执行失败:', err.message); + process.exit(1); +}); diff --git a/scripts/notion-signal-bridge.js b/scripts/notion-signal-bridge.js new file mode 100644 index 00000000..53fdd2df --- /dev/null +++ b/scripts/notion-signal-bridge.js @@ -0,0 +1,497 @@ +// scripts/notion-signal-bridge.js +// 铸渊 ↔ Notion 信号桥(替代 ESP 邮件协议) +// +// 功能: +// 1. 轮询 Notion 工单簿,读取状态为「已发送」的新工单 +// 2. 解析工单中的 OP 类型,执行对应操作 +// 3. 回写工单状态 + 写入 Notion 信号日志 +// 4. 同时写入本地 signal-log/ 文件(双写) +// 5. 读取广播归档,推送到聊天室子频道数据 +// +// 用法: +// node scripts/notion-signal-bridge.js poll — 轮询工单簿 +// node scripts/notion-signal-bridge.js health — 健康检查 +// +// 必需环境变量: +// NOTION_API_TOKEN 铸渊信号桥 Integration Token(GitHub Secret: NOTION_API_TOKEN) +// +// 可选环境变量: +// WORKORDER_DB_ID 人格协作工单簿 database_id +// SIGNAL_LOG_DB_ID 跨平台信号日志 database_id + +'use strict'; + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +// ═══════════════════════════════════════════════════════ +// 常量 +// ═══════════════════════════════════════════════════════ + +const NOTION_VERSION = '2022-06-28'; +const NOTION_API_HOSTNAME = 'api.notion.com'; +const NOTION_RICH_TEXT_MAX = 2000; + +const SIGNAL_LOG_DIR = path.join(__dirname, '../signal-log'); +const SIGNAL_INDEX_PATH = path.join(SIGNAL_LOG_DIR, 'index.json'); + +// Notion 数据库 ID(从环境变量或默认值) +// 注意:铸渊需从 Notion URL 中提取实际 ID,这里用占位符 +const WORKORDER_DB_ID = process.env.WORKORDER_DB_ID || ''; +const SIGNAL_LOG_DB_ID = process.env.SIGNAL_LOG_DB_ID || ''; + +// ═══════════════════════════════════════════════════════ +// Notion API 基础调用 +// ═══════════════════════════════════════════════════════ + +function notionRequest(method, endpoint, body, token) { + return new Promise((resolve, reject) => { + const payload = body ? JSON.stringify(body) : ''; + const opts = { + hostname: NOTION_API_HOSTNAME, + port: 443, + path: endpoint, + method: method, + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }, + }; + if (payload) opts.headers['Content-Length'] = Buffer.byteLength(payload); + + const req = https.request(opts, res => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const parsed = JSON.parse(data); + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(parsed); + } else { + reject(new Error(`Notion API ${method} ${endpoint} → ${res.statusCode}: ${parsed.message || data}`)); + } + } catch (e) { + reject(new Error(`Notion API parse error: ${data.slice(0, 200)}`)); + } + }); + }); + + req.on('error', reject); + req.setTimeout(30000, () => { req.destroy(new Error('Notion API 请求超时')); }); + if (payload) req.write(payload); + req.end(); + }); +} + +function notionPost(endpoint, body, token) { + return notionRequest('POST', endpoint, body, token); +} + +function notionPatch(endpoint, body, token) { + return notionRequest('PATCH', endpoint, body, token); +} + +// ═══════════════════════════════════════════════════════ +// Notion 属性构建辅助 +// ═══════════════════════════════════════════════════════ + +function richText(content) { + const str = String(content || ''); + const chunks = []; + for (let i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) { + chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } }); + } + return chunks.length ? chunks : [{ type: 'text', text: { content: '' } }]; +} + +function titleProp(content) { + return [{ type: 'text', text: { content: String(content || '').slice(0, 120) } }]; +} + +// ═══════════════════════════════════════════════════════ +// 本地信号日志读写(与 esp-email-processor.js 相同逻辑) +// ═══════════════════════════════════════════════════════ + +function generateSignalId() { + const now = new Date(); + const ymd = now.toISOString().slice(0, 10).replace(/-/g, ''); + const index = loadSignalIndex(); + const seq = String(index.total_count + 1).padStart(3, '0'); + return `SIG-${ymd}-${seq}`; +} + +function loadSignalIndex() { + if (!fs.existsSync(SIGNAL_INDEX_PATH)) { + return { + description: '铸渊信号日志目录索引 · AGE OS 信号协议(Notion API 直连)', + last_updated: new Date().toISOString(), + total_count: 0, + signals: [] + }; + } + return JSON.parse(fs.readFileSync(SIGNAL_INDEX_PATH, 'utf8')); +} + +function saveSignalIndex(index) { + index.last_updated = new Date().toISOString(); + fs.writeFileSync(SIGNAL_INDEX_PATH, JSON.stringify(index, null, 2)); +} + +function writeLocalSignalLog(signal) { + const dateStr = signal.timestamp.slice(0, 7); + const monthDir = path.join(SIGNAL_LOG_DIR, dateStr); + fs.mkdirSync(monthDir, { recursive: true }); + + const filePath = path.join(monthDir, `${signal.signal_id}.json`); + fs.writeFileSync(filePath, JSON.stringify(signal, null, 2)); + + const index = loadSignalIndex(); + index.total_count += 1; + index.signals.unshift({ + signal_id: signal.signal_id, + trace_id: signal.trace_id, + type: signal.signal_type, + timestamp: signal.timestamp, + summary: signal.summary, + related_dev: signal.related_dev || null, + file: `${dateStr}/${signal.signal_id}.json` + }); + saveSignalIndex(index); + console.log(`📝 本地信号日志已写入: ${signal.signal_id}`); +} + +// ═══════════════════════════════════════════════════════ +// Notion 信号日志写入(双写:Notion + 本地) +// ═══════════════════════════════════════════════════════ + +async function writeNotionSignalLog(signal, token) { + // 写入本地文件 + writeLocalSignalLog(signal); + + // 写入 Notion 信号日志数据库(如果配置了 ID) + if (!SIGNAL_LOG_DB_ID) { + console.log('⚠️ SIGNAL_LOG_DB_ID 未配置,跳过 Notion 信号日志写入'); + return; + } + + try { + await notionPost('/v1/pages', { + parent: { database_id: SIGNAL_LOG_DB_ID }, + properties: { + '信号编号': { title: titleProp(signal.signal_id) }, + '信号类型': { select: { name: signal.signal_type } }, + '方向': { select: { name: signal.direction || 'GitHub→Notion' } }, + '发送方': { select: { name: signal.sender || '铸渊' } }, + '接收方': { select: { name: signal.receiver || '霜砚' } }, + 'trace_id': { rich_text: richText(signal.trace_id) }, + '摘要': { rich_text: richText(signal.summary) }, + '执行结果': { status: { name: signal.result || '成功' } } + } + }, token); + console.log(`📡 Notion 信号日志已写入: ${signal.signal_id}`); + } catch (err) { + console.error(`⚠️ Notion 信号日志写入失败: ${err.message}`); + // 本地已写入,Notion 写入失败不影响流程 + } +} + +// ═══════════════════════════════════════════════════════ +// 工单簿轮询 +// ═══════════════════════════════════════════════════════ + +/** + * 从工单簿中获取「已发送」状态的工单 + */ +async function queryPendingWorkOrders(token) { + if (!WORKORDER_DB_ID) { + console.log('⚠️ WORKORDER_DB_ID 未配置,跳过工单轮询'); + console.log(' 请设置环境变量 WORKORDER_DB_ID(从 Notion 工单簿 URL 提取)'); + return []; + } + + const result = await notionPost(`/v1/databases/${WORKORDER_DB_ID}/query`, { + filter: { + property: '执行结果', + status: { equals: '已发送' } + }, + sorts: [{ timestamp: 'created_time', direction: 'ascending' }] + }, token); + + return result.results || []; +} + +/** + * 解析工单页面属性 + */ +function parseWorkOrder(page) { + const props = page.properties || {}; + + // 安全提取各类属性值 + const getTitle = (p) => p?.title?.[0]?.text?.content || ''; + const getRichText = (p) => p?.rich_text?.map(r => r.text?.content || '').join('') || ''; + const getSelect = (p) => p?.select?.name || ''; + const getStatus = (p) => p?.status?.name || ''; + + return { + page_id: page.id, + title: getTitle(props['工单名称'] || props['Name'] || props['名称']), + op_type: getSelect(props['OP类型'] || props['操作类型']), + priority: getSelect(props['优先级']), + status: getStatus(props['执行结果']), + payload: getRichText(props['payload'] || props['指令内容']), + trace_id: getRichText(props['trace_id']), + sender: getSelect(props['发送方']), + receiver: getSelect(props['接收方']), + created: page.created_time, + }; +} + +/** + * 更新工单状态 + */ +async function updateWorkOrderStatus(pageId, status, message, token) { + const props = { + '执行结果': { status: { name: status } } + }; + // 如果有回执信息字段,也更新 + if (message) { + props['回执信息'] = { rich_text: richText(message) }; + } + + await notionPatch(`/v1/pages/${pageId}`, { properties: props }, token); + console.log(`✅ 工单 ${pageId.slice(0, 8)}... 状态更新为: ${status}`); +} + +// ═══════════════════════════════════════════════════════ +// 工单执行引擎 +// ═══════════════════════════════════════════════════════ + +/** + * 根据工单 OP 类型执行对应操作 + */ +async function executeWorkOrder(wo, token) { + const signalId = generateSignalId(); + const traceId = wo.trace_id || `TRC-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${signalId.slice(-3)}`; + + console.log(`\n🔧 处理工单: ${wo.title}`); + console.log(` OP: ${wo.op_type} | 优先级: ${wo.priority} | trace: ${traceId}`); + + let result = '成功'; + let message = ''; + + try { + switch (wo.op_type) { + case 'BROADCAST': + case '广播推送': + message = await handleBroadcast(wo); + break; + + case 'STATUS_QUERY': + case '状态查询': + message = await handleStatusQuery(wo); + break; + + case 'DEPLOY_CHECK': + case '部署检查': + message = await handleDeployCheck(wo); + break; + + case 'DATA_SYNC': + case '数据同步': + message = await handleDataSync(wo); + break; + + default: + message = `未知操作类型: ${wo.op_type},已记录,等待人工处理`; + console.log(`⚠️ ${message}`); + break; + } + } catch (err) { + result = '失败'; + message = `执行失败: ${err.message}`; + console.error(`❌ ${message}`); + } + + // 写回执到工单簿 + await updateWorkOrderStatus(wo.page_id, result, message, token); + + // 双写信号日志(本地 + Notion) + const ackSignal = { + signal_id: signalId, + signal_type: 'GL-ACK', + trace_id: traceId, + timestamp: new Date().toISOString(), + sender: '铸渊', + receiver: '霜砚', + direction: 'GitHub→Notion', + summary: `${wo.op_type}: ${message.slice(0, 100)}`, + result: result, + related_dev: null, + original_wo: wo.title, + esp_version: '2.0-notion' + }; + + await writeNotionSignalLog(ackSignal, token); + + return { result, message }; +} + +// ═══════════════════════════════════════════════════════ +// OP 处理器 +// ═══════════════════════════════════════════════════════ + +async function handleBroadcast(wo) { + // 读取广播内容,写入聊天室可读的文件 + const broadcastDir = path.join(__dirname, '../broadcasts-outbox'); + if (!fs.existsSync(broadcastDir)) { + return '广播目录不存在,跳过'; + } + + console.log('📢 处理广播推送工单'); + // 将广播内容标记为待推送 + const pendingFile = path.join(broadcastDir, 'pending-push.json'); + const pending = fs.existsSync(pendingFile) + ? JSON.parse(fs.readFileSync(pendingFile, 'utf8')) + : { broadcasts: [] }; + + pending.broadcasts.push({ + title: wo.title, + content: wo.payload, + timestamp: new Date().toISOString(), + trace_id: wo.trace_id + }); + + fs.writeFileSync(pendingFile, JSON.stringify(pending, null, 2)); + return `广播已加入推送队列 (${pending.broadcasts.length} 条待推)`; +} + +async function handleStatusQuery(wo) { + // 收集系统状态信息 + console.log('📊 处理状态查询工单'); + const devStatusPath = path.join(__dirname, '../.github/persona-brain/dev-status.json'); + if (fs.existsSync(devStatusPath)) { + const status = JSON.parse(fs.readFileSync(devStatusPath, 'utf8')); + const teamCount = status.team_status?.length || 0; + const active = status.team_status?.filter(d => d.status?.includes('🟢')).length || 0; + return `团队状态: ${teamCount} 名开发者, ${active} 人推进中`; + } + return '状态文件不存在'; +} + +async function handleDeployCheck(wo) { + console.log('🚀 处理部署检查工单'); + // 检查关键文件是否存在 + const checks = [ + { name: 'docs/index.html', label: '铸渊聊天室' }, + { name: 'backend-integration/api-proxy.js', label: 'API 代理' }, + { name: '.github/workflows/deploy-pages.yml', label: '部署工作流' }, + ]; + const results = checks.map(c => { + const exists = fs.existsSync(path.join(__dirname, '..', c.name)); + return `${exists ? '✅' : '❌'} ${c.label}`; + }); + return results.join(' | '); +} + +async function handleDataSync(wo) { + console.log('🔄 处理数据同步工单'); + return '数据同步指令已接收,等待下次 CI 流程执行'; +} + +// ═══════════════════════════════════════════════════════ +// 健康检查 +// ═══════════════════════════════════════════════════════ + +async function healthCheck(token) { + console.log('🏥 Notion 信号桥健康检查\n'); + console.log(` Notion API Token: ${token ? '✅ 已配置' : '❌ 未配置'}`); + console.log(` 工单簿 DB ID: ${WORKORDER_DB_ID ? '✅ ' + WORKORDER_DB_ID.slice(0, 8) + '...' : '⚠️ 未配置'}`); + console.log(` 信号日志 DB ID: ${SIGNAL_LOG_DB_ID ? '✅ ' + SIGNAL_LOG_DB_ID.slice(0, 8) + '...' : '⚠️ 未配置'}`); + console.log(` 本地信号日志: ${fs.existsSync(SIGNAL_INDEX_PATH) ? '✅ 存在' : '⚠️ 不存在'}`); + + if (token) { + try { + // 测试 API 连通性 — 获取 bot user + const me = await notionRequest('GET', '/v1/users/me', null, token); + console.log(` API 连通性: ✅ Bot: ${me.name || me.id}`); + } catch (err) { + console.log(` API 连通性: ❌ ${err.message}`); + } + } + + // 本地信号统计 + const index = loadSignalIndex(); + console.log(`\n 📊 本地信号总数: ${index.total_count}`); + if (index.signals.length > 0) { + console.log(` 📝 最近信号: ${index.signals[0].signal_id} (${index.signals[0].type})`); + } + + console.log('\n✅ 健康检查完成'); +} + +// ═══════════════════════════════════════════════════════ +// 主入口 +// ═══════════════════════════════════════════════════════ + +async function main() { + const cmd = process.argv[2] || 'poll'; + const token = process.env.NOTION_API_TOKEN; + + if (!token) { + console.error('❌ 缺少环境变量 NOTION_API_TOKEN'); + console.error(' 请在 GitHub Secrets 中配置 NOTION_API_TOKEN'); + process.exit(1); + } + + console.log(`🔌 铸渊 Notion 信号桥 v1.0`); + console.log(` 时间: ${new Date().toISOString()}`); + console.log(` 命令: ${cmd}\n`); + + switch (cmd) { + case 'poll': { + console.log('📡 开始轮询 Notion 工单簿...\n'); + + const workOrders = await queryPendingWorkOrders(token); + + if (workOrders.length === 0) { + console.log('✅ 无待处理工单'); + return; + } + + console.log(`📋 发现 ${workOrders.length} 条待处理工单\n`); + + let success = 0, failed = 0; + + for (const page of workOrders) { + const wo = parseWorkOrder(page); + try { + const { result } = await executeWorkOrder(wo, token); + if (result === '成功') success++; + else failed++; + } catch (err) { + console.error(`❌ 工单处理异常: ${err.message}`); + failed++; + } + } + + console.log(`\n📊 处理结果: ${success} 成功, ${failed} 失败 (共 ${workOrders.length} 条)`); + break; + } + + case 'health': + await healthCheck(token); + break; + + default: + console.error(`❌ 未知命令: ${cmd}`); + console.error(' 可用命令: poll, health'); + process.exit(1); + } +} + +main().catch(err => { + console.error(`\n💥 致命错误: ${err.message}`); + process.exit(1); +}); diff --git a/scripts/zhuyuan-module-protocol.js b/scripts/zhuyuan-module-protocol.js new file mode 100644 index 00000000..94dc2733 --- /dev/null +++ b/scripts/zhuyuan-module-protocol.js @@ -0,0 +1,300 @@ +#!/usr/bin/env node + +/** + * 🌀 铸渊人格协议 · 模块生命周期管理脚本 v1.0 + * + * 用法: + * node scripts/zhuyuan-module-protocol.js inspect # 全模块检查 + * node scripts/zhuyuan-module-protocol.js inspect # 单模块检查 + * node scripts/zhuyuan-module-protocol.js status # 模块状态汇总 + * node scripts/zhuyuan-module-protocol.js recover # 模块回收检查 + * node scripts/zhuyuan-module-protocol.js preview # 预演报告生成 + * + * 纯 Node.js,无外部依赖。 + */ + +const fs = require('fs'); +const path = require('path'); + +// === 配置 === +const ROOT = path.resolve(__dirname, '..'); +const BRAIN_DIR = path.join(ROOT, '.github', 'brain'); +const DOCS_DIR = path.join(ROOT, 'docs'); + +// 模块注册表 +const MODULE_REGISTRY = { + 'm01-login': { owner: 'DEV-002', name: '肥猫', hli: 'AUTH', label: '登录模块' }, + 'm03-personality': { owner: 'DEV-002', name: '肥猫', hli: 'PERSONA', label: '人格模块' }, + 'm05-user-center': { owner: 'DEV-009', name: '花尔', hli: 'USER', label: '用户中心' }, + 'm06-ticket': { owner: 'DEV-010', name: '桔子', hli: 'TICKET', label: '工单系统' }, + 'm07-dialogue-ui': { owner: 'DEV-003', name: '燕樊', hli: 'DIALOGUE', label: '对话界面' }, + 'm10-cloud': { owner: 'DEV-003', name: '燕樊', hli: 'STORAGE', label: '云存储' }, + 'm11-module': { owner: 'DEV-010', name: '桔子', hli: 'MODULE', label: '模块管理' }, + 'm12-kanban': { owner: 'DEV-005', name: '小草莓', hli: 'DASHBOARD', label: '看板' }, + 'm15-cloud-drive': { owner: 'DEV-003', name: '燕樊', hli: 'STORAGE', label: '云盘' }, + 'm18-health-check':{ owner: null, name: '待分配', hli: 'SYSTEM', label: '健康检查' }, +}; + +// === 工具函数 === + +function countFiles(dir) { + if (!fs.existsSync(dir)) return 0; + let count = 0; + const items = fs.readdirSync(dir, { withFileTypes: true }); + for (const item of items) { + if (item.name.startsWith('.')) continue; + if (item.isFile()) count++; + else if (item.isDirectory()) count += countFiles(path.join(dir, item.name)); + } + return count; +} + +function hasReadme(dir) { + return fs.existsSync(path.join(dir, 'README.md')); +} + +function getModuleStatus(moduleDir) { + const fullPath = path.join(ROOT, moduleDir); + if (!fs.existsSync(fullPath)) return { exists: false, files: 0, readme: false }; + + const files = countFiles(fullPath); + const readme = hasReadme(fullPath); + + let status = '📭 空'; + if (files > 0 && readme) status = '✅ 正常'; + else if (files > 0 && !readme) status = '⚠️ 缺README'; + else if (files === 0) status = '📭 空'; + + return { exists: true, files, readme, status }; +} + +// === 命令:inspect === + +function cmdInspect(moduleId) { + console.log('🌀 铸渊人格协议 · 模块检查'); + console.log('─'.repeat(50)); + + if (moduleId) { + // 单模块检查 + const info = MODULE_REGISTRY[moduleId]; + if (!info) { + console.log(`❌ 未知模块: ${moduleId}`); + console.log(`已注册模块: ${Object.keys(MODULE_REGISTRY).join(', ')}`); + process.exit(1); + } + + const status = getModuleStatus(moduleId); + console.log(`\n📦 模块: ${moduleId} (${info.label})`); + console.log(` 负责人: ${info.name} (${info.owner || '未分配'})`); + console.log(` HLI 域: ${info.hli}`); + console.log(` 目录存在: ${status.exists ? '✅' : '❌'}`); + console.log(` 文件数量: ${status.files}`); + console.log(` README: ${status.readme ? '✅' : '❌'}`); + console.log(` 状态: ${status.status}`); + + // 检查 HLI 接口 + const schemaDir = path.join(ROOT, 'src', 'schemas', 'hli', info.hli.toLowerCase()); + const routeDir = path.join(ROOT, 'src', 'routes', 'hli', info.hli.toLowerCase()); + + if (fs.existsSync(schemaDir)) { + try { + const schemas = fs.readdirSync(schemaDir).filter(f => f.endsWith('.schema.json')); + console.log(` HLI Schema: ${schemas.length} 个 (${schemas.join(', ')})`); + } catch (e) { + console.log(` HLI Schema: ⚠️ 读取失败 (${e.code || e.message})`); + } + } else { + console.log(` HLI Schema: 📭 未创建`); + } + + if (fs.existsSync(routeDir)) { + try { + const routes = fs.readdirSync(routeDir).filter(f => f.endsWith('.js')); + console.log(` HLI 路由: ${routes.length} 个 (${routes.join(', ')})`); + } catch (e) { + console.log(` HLI 路由: ⚠️ 读取失败 (${e.code || e.message})`); + } + } else { + console.log(` HLI 路由: 📭 未创建`); + } + } else { + // 全模块检查 + console.log('\n📋 全模块状态:\n'); + console.log('模块ID | 标签 | 负责人 | 文件 | README | 状态'); + console.log('─'.repeat(70)); + + for (const [id, info] of Object.entries(MODULE_REGISTRY)) { + const status = getModuleStatus(id); + const line = [ + id.padEnd(20), + info.label.padEnd(8), + info.name.padEnd(8), + String(status.files).padStart(4), + (status.readme ? '✅' : '❌').padEnd(6), + status.status, + ].join(' | '); + console.log(line); + } + } +} + +// === 命令:status === + +function cmdStatus() { + console.log('🌀 铸渊人格协议 · 模块状态汇总'); + console.log('─'.repeat(50)); + + let total = 0, ok = 0, warn = 0, empty = 0; + + for (const [id] of Object.entries(MODULE_REGISTRY)) { + total++; + const status = getModuleStatus(id); + if (status.files > 0 && status.readme) ok++; + else if (status.files > 0) warn++; + else empty++; + } + + console.log(`\n📊 模块统计:`); + console.log(` 总模块数: ${total}`); + console.log(` ✅ 正常: ${ok}`); + console.log(` ⚠️ 告警: ${warn}`); + console.log(` 📭 空模块: ${empty}`); + console.log(` 完成率: ${Math.round((ok / total) * 100)}%`); + + // 检查前端 + const indexPath = path.join(DOCS_DIR, 'index.html'); + if (fs.existsSync(indexPath)) { + const stat = fs.statSync(indexPath); + const lines = fs.readFileSync(indexPath, 'utf8').split('\n').length; + console.log(`\n🌐 前端:`); + console.log(` docs/index.html: ${lines} 行, ${(stat.size / 1024).toFixed(1)} KB`); + } + + // 检查 CNAME + const cnamePath = path.join(DOCS_DIR, 'CNAME'); + if (fs.existsSync(cnamePath)) { + const domain = fs.readFileSync(cnamePath, 'utf8').trim(); + console.log(` 自定义域名: ${domain}`); + } else { + console.log(` 自定义域名: ❌ 未配置`); + } + + // 检查大脑文件 + console.log(`\n🧠 大脑状态:`); + const brainFiles = ['repo-snapshot.md', 'repo-map.json', 'memory.json', 'collaborators.json', 'module-protocol.md']; + for (const f of brainFiles) { + const fp = path.join(BRAIN_DIR, f); + if (fs.existsSync(fp)) { + const stat = fs.statSync(fp); + console.log(` ✅ ${f} (${(stat.size / 1024).toFixed(1)} KB)`); + } else { + console.log(` ❌ ${f} 缺失`); + } + } +} + +// === 命令:recover === + +function cmdRecover(moduleId) { + if (!moduleId) { + console.log('❌ 请指定模块ID,如: npm run module:protocol -- recover m01-login'); + process.exit(1); + } + + const info = MODULE_REGISTRY[moduleId]; + if (!info) { + console.log(`❌ 未知模块: ${moduleId}`); + process.exit(1); + } + + console.log('🌀 铸渊人格协议 · 模块回收检查'); + console.log('─'.repeat(50)); + console.log(`\n📦 目标模块: ${moduleId} (${info.label})`); + + const status = getModuleStatus(moduleId); + console.log(` 当前状态: ${status.status}`); + console.log(` 文件数量: ${status.files}`); + + if (status.files === 0) { + console.log(`\n✅ 模块已为空状态,无需回收。`); + return; + } + + console.log(`\n⚠️ 回收操作需要手动执行:`); + console.log(` 方法一(推荐): 通过 PR 删除模块文件`); + console.log(` 方法二: git revert 恢复到模块的上一个版本`); + console.log(` 方法三: 直接在 GitHub 界面删除文件`); + console.log(`\n💡 回收后,铸渊会通过 generate-module-doc.yml 更新文档索引。`); +} + +// === 命令:preview === + +function cmdPreview() { + console.log('🌀 铸渊人格协议 · 本地预演报告'); + console.log('═'.repeat(50)); + + // 模块状态 + cmdStatus(); + + console.log('\n'); + console.log('─'.repeat(50)); + console.log('🎯 预演结论:'); + + let ok = true; + const issues = []; + + // 检查 index.html + if (!fs.existsSync(path.join(DOCS_DIR, 'index.html'))) { + issues.push('docs/index.html 不存在'); + ok = false; + } + + // 检查工作流 + const wfDir = path.join(ROOT, '.github', 'workflows'); + if (!fs.existsSync(path.join(wfDir, 'deploy-pages.yml'))) { + issues.push('deploy-pages.yml 不存在'); + ok = false; + } + + if (ok && issues.length === 0) { + console.log('✅ 预演通过 — 代码可以安全部署到生产环境'); + console.log(' 下一步: 合并 PR 到 main 分支,触发自动部署'); + } else { + console.log('❌ 预演未通过:'); + issues.forEach(i => console.log(` - ${i}`)); + } +} + +// === 主入口 === + +const [,, command, ...args] = process.argv; + +switch (command) { + case 'inspect': + cmdInspect(args[0]); + break; + case 'status': + cmdStatus(); + break; + case 'recover': + cmdRecover(args[0]); + break; + case 'preview': + cmdPreview(); + break; + default: + console.log('🌀 铸渊人格协议 · 模块生命周期管理 v1.0'); + console.log(''); + console.log('用法:'); + console.log(' node scripts/zhuyuan-module-protocol.js inspect 全模块检查'); + console.log(' node scripts/zhuyuan-module-protocol.js inspect 单模块检查'); + console.log(' node scripts/zhuyuan-module-protocol.js status 模块状态汇总'); + console.log(' node scripts/zhuyuan-module-protocol.js recover 模块回收检查'); + console.log(' node scripts/zhuyuan-module-protocol.js preview 本地预演报告'); + console.log(''); + console.log('npm 快捷命令:'); + console.log(' npm run module:protocol -- inspect'); + console.log(' npm run module:protocol -- status'); + console.log(' npm run module:protocol -- preview'); + break; +} diff --git a/settings/README.md b/settings/README.md new file mode 100644 index 00000000..d9c11f86 --- /dev/null +++ b/settings/README.md @@ -0,0 +1 @@ +# settings 模块 diff --git a/signal-log/index.json b/signal-log/index.json index 76156707..d2d2f203 100644 --- a/signal-log/index.json +++ b/signal-log/index.json @@ -1,5 +1,5 @@ { - "description": "铸渊信号日志目录索引 · AGE OS ESP 邮件信号协议", + "description": "铸渊信号日志目录索引 · AGE OS 信号协议(Notion API 直连)", "last_updated": "2026-03-07T04:45:55.670Z", "total_count": 2, "signals": [ diff --git a/status-board/README.md b/status-board/README.md new file mode 100644 index 00000000..14384b0a --- /dev/null +++ b/status-board/README.md @@ -0,0 +1 @@ +# status-board 模块 diff --git a/status-board/index.html b/status-board/index.html index 1aa8113a..34f2201b 100644 --- a/status-board/index.html +++ b/status-board/index.html @@ -177,13 +177,7 @@ - - - - - - - + @@ -191,23 +185,27 @@ - + - - \ No newline at end of file diff --git a/style-system/README.md b/style-system/README.md new file mode 100644 index 00000000..e2b641b3 --- /dev/null +++ b/style-system/README.md @@ -0,0 +1 @@ +# style-system 模块 diff --git a/ticket-system/README.md b/ticket-system/README.md new file mode 100644 index 00000000..a8eaa86a --- /dev/null +++ b/ticket-system/README.md @@ -0,0 +1 @@ +# ticket-system 模块 diff --git a/user-center/README.md b/user-center/README.md new file mode 100644 index 00000000..02565844 --- /dev/null +++ b/user-center/README.md @@ -0,0 +1 @@ +# user-center 模块