Merge branch 'main' of https://github.com/qinfendebingshuo/guanghulab
This commit is contained in:
commit
86e783c64b
|
|
@ -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/ 所有开发者个人工作区
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <module-id>
|
||||
```
|
||||
|
||||
### 3. 📋 管理(Manage)
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 模块注册 | 每个 `m**-*/` 目录自动识别为模块 |
|
||||
| 开发者映射 | `.github/brain/collaborators.json` 记录归属 |
|
||||
| 状态追踪 | `repo-snapshot.md` 自动更新模块状态 |
|
||||
| 依赖管理 | 模块间依赖通过 `routing-map.json` 管理 |
|
||||
|
||||
**管理命令**:
|
||||
```bash
|
||||
npm run module:protocol -- inspect # 全模块检查
|
||||
npm run module:protocol -- inspect <module-id> # 单模块检查
|
||||
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 代码守护人格体*
|
||||
|
|
@ -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"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 — 广播分发系统 + 唤醒协议全部激活
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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%
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"last_updated": "2026-03-07",
|
||||
"last_updated": "2026-03-08",
|
||||
"faq": [
|
||||
{
|
||||
"category": "SSH/服务器",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -3,8 +3,7 @@ name: 🌀 部署铸渊聊天室 (GitHub Pages)
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- copilot/** # Copilot Agent 功能分支可直接部署预览,PR 合并后自动失效
|
||||
- main # 仅 main 分支触发生产部署
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/brain/**'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
191
README.md
191
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/
|
||||
|
|
|
|||
|
|
@ -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 由后端环境变量管理,不暴露给前端
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
|||
PRIMARY_API_KEY=你的主密钥
|
||||
FALLBACK_API_KEY=你的备用密钥
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
# chat-bubble 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
# cloud-drive 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
# coldstart 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
# cost-control 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
# dashboard 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
guanghulab.com
|
||||
|
|
@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# docs 模块
|
||||
1260
docs/index.html
1260
docs/index.html
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit f8340b659c60daaa15f57f2541200a2ca14b2c84
|
||||
|
|
@ -0,0 +1 @@
|
|||
# frontend 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
# help-center 模块
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 4c5dc4e8be2bcba1acee37c1bf93e18372ae4795
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# M18 系统健康检查
|
||||
- 负责人:待分配
|
||||
- 状态:规划中
|
||||
- 技术栈:待定
|
||||
- 依赖模块:无
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
# multi-persona 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
# notification 模块
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# persona-selector 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
# portal 模块
|
||||
|
|
@ -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 += `<details>\n<summary>点击展开成功列表(${categories.success.length} 个)</summary>\n\n`;
|
||||
for (const run of categories.success) {
|
||||
report += `- ✅ **${run.name}** · ${branchDisplayName(run.head_branch)} · ${formatTime(run.created_at)}\n`;
|
||||
}
|
||||
report += `\n</details>\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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 🌀 铸渊人格协议 · 模块生命周期管理脚本 v1.0
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/zhuyuan-module-protocol.js inspect # 全模块检查
|
||||
* node scripts/zhuyuan-module-protocol.js inspect <module-id> # 单模块检查
|
||||
* node scripts/zhuyuan-module-protocol.js status # 模块状态汇总
|
||||
* node scripts/zhuyuan-module-protocol.js recover <module-id> # 模块回收检查
|
||||
* 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 <module-id> 单模块检查');
|
||||
console.log(' node scripts/zhuyuan-module-protocol.js status 模块状态汇总');
|
||||
console.log(' node scripts/zhuyuan-module-protocol.js recover <module-id> 模块回收检查');
|
||||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
# settings 模块
|
||||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# status-board 模块
|
||||
|
|
@ -177,13 +177,7 @@
|
|||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 环节2新增:三个核心脚本 -->
|
||||
<script src="api-config.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="render.js"></script>
|
||||
<!-- 原有的交互脚本 -->
|
||||
<script src="script.js"></script>
|
||||
<!-- 环节2新增:三个核心脚本 -->
|
||||
<!-- 核心脚本 -->
|
||||
<script src="api-config.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="render.js"></script>
|
||||
|
|
@ -191,23 +185,27 @@
|
|||
<!-- 原有的交互脚本 -->
|
||||
<script src="script.js"></script>
|
||||
|
||||
<!-- ===== 新加的初始化脚本 ===== -->
|
||||
<!-- ===== WebSocket 实时推送初始化 ===== -->
|
||||
<script>
|
||||
// ===== WebSocket 实时推送初始化 =====
|
||||
(function() {
|
||||
const wsUrl = 'ws://localhost:8080';
|
||||
const wsClient = new WebSocketClient(wsUrl, {
|
||||
onMessage: (data) => {
|
||||
// 自动检测 WebSocket 地址:生产环境走 Nginx 反向代理,本地开发直连 8080
|
||||
var wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
var wsUrl = (location.hostname === 'localhost' || location.hostname === '127.0.0.1')
|
||||
? 'ws://localhost:8080'
|
||||
: wsProtocol + '//' + location.host + '/ws';
|
||||
|
||||
var wsClient = new WebSocketClient(wsUrl, {
|
||||
onMessage: function(data) {
|
||||
if (data.type === 'dashboard_update' && data.data) {
|
||||
if (data.data.system) renderSystemStatus(data.data.system);
|
||||
if (data.data.developers) renderDevelopers(data.data.developers);
|
||||
if (data.data.broadcasts) renderBroadcasts(data.data.broadcasts);
|
||||
}
|
||||
},
|
||||
onStatusChange: (status) => {
|
||||
const el = document.getElementById('conn-status');
|
||||
onStatusChange: function(status) {
|
||||
var el = document.getElementById('conn-status');
|
||||
if (!el) return;
|
||||
let text = '', className = '';
|
||||
var text = '', className = '';
|
||||
switch(status) {
|
||||
case 'connected':
|
||||
text = '🔵 实时连接 (WebSocket)';
|
||||
|
|
@ -229,14 +227,12 @@
|
|||
if (typeof refreshDashboard === 'function') refreshDashboard();
|
||||
break;
|
||||
}
|
||||
el.innerHTML = `<span class="${className}"></span> ${text}`;
|
||||
el.innerHTML = '<span class="' + className + '"></span> ' + text;
|
||||
}
|
||||
});
|
||||
wsClient.connect();
|
||||
window.addEventListener('beforeunload', () => wsClient.disconnect());
|
||||
window.addEventListener('beforeunload', function() { wsClient.disconnect(); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
# style-system 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
# ticket-system 模块
|
||||
|
|
@ -0,0 +1 @@
|
|||
# user-center 模块
|
||||
Loading…
Reference in New Issue