Merge pull request #8 from qinfendebingshuo/copilot/add-interactive-ai-status-page

feat: add CD workflow for auto-deploy to server via SSH/rsync
This commit is contained in:
冰朔 2026-03-07 17:44:36 +08:00 committed by GitHub
commit 4ad04ababc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 3664 additions and 333 deletions

57
.github/CODEOWNERS vendored Normal file
View File

@ -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/ 所有开发者个人工作区
# ═══════════════════════════════════════════════════════

View File

@ -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

185
.github/brain/module-protocol.md vendored Normal file
View File

@ -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 代码守护人格体*

View File

@ -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

View File

@ -3,8 +3,7 @@ name: 🌀 部署铸渊聊天室 (GitHub Pages)
on:
push:
branches:
- main
- copilot/** # Copilot Agent 功能分支可直接部署预览PR 合并后自动失效
- main # 仅 main 分支触发生产部署
paths:
- 'docs/**'
- '.github/brain/**'

200
.github/workflows/deploy-to-server.yml vendored Normal file
View File

@ -0,0 +1,200 @@
name: "🚀 铸渊 CD · 自动部署到 guanghulab.com"
# ━━━ 工单 YM-CD-20260307-001 ━━━
# push 到 main → 校验 → rsync 同步到服务器 → Notion 部署通知
#
# 必需 GitHub Secrets仓库 Settings → Secrets → Actions:
# DEPLOY_HOST — 服务器 IP
# DEPLOY_USER — SSH 用户名
# DEPLOY_KEY — SSH 私钥(完整 PEM 内容)
# DEPLOY_PATH — Nginx 网站根目录(如 /usr/share/nginx/html
#
# 可选 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: 🔑 配置 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: 🔒 部署前安全检查
run: |
if [ ! -f "docs/index.html" ]; then
echo "❌ docs/index.html 不存在,中止部署以防清空生产环境"
exit 1
fi
FILE_COUNT=$(find docs/ -type f | wc -l)
if [ "$FILE_COUNT" -lt 2 ]; then
echo "❌ docs/ 目录文件数异常(仅 ${FILE_COUNT} 个),中止部署"
exit 1
fi
echo "✅ docs/ 目录包含 ${FILE_COUNT} 个文件,安全检查通过"
- name: 🚀 rsync 同步到服务器
run: |
echo "📡 开始同步到 ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}"
rsync -avz --delete-after \
--exclude '.git' \
--exclude '.github' \
--exclude 'node_modules' \
--exclude 'scripts' \
--exclude 'syslog-inbox' \
--exclude 'syslog-processed' \
--exclude 'signal-log' \
--exclude 'dev-nodes' \
--exclude 'broadcasts-outbox' \
--exclude 'backend-integration' \
--exclude 'tests' \
--exclude 'src' \
--exclude 'cost-control' \
--exclude 'notion-push' \
--exclude 'multi-persona' \
--exclude 'status-board' \
--exclude 'reports' \
--exclude 'guanghulab-main' \
--exclude 'package*.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' \
-e "ssh -i ~/.ssh/deploy_key" \
docs/ \
${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/
echo "✅ 同步完成"
- name: 🔍 验证部署
run: |
ssh -i ~/.ssh/deploy_key \
${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \
"echo '✅ 服务器连接正常' && ls -la ${{ secrets.DEPLOY_PATH }}/ | head -20"
- name: 📝 部署汇总
run: |
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

View File

@ -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:

38
.github/workflows/notion-poll.yml vendored Normal file
View File

@ -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

180
.github/workflows/staging-preview.yml vendored Normal file
View File

@ -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,
});
}

View File

@ -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 由后端环境变量管理,不暴露给前端

View File

@ -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('');
});

View File

@ -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;
}

0
docs/.nojekyll Normal file
View File

1
docs/CNAME Normal file
View File

@ -0,0 +1 @@
guanghulab.com

File diff suppressed because it is too large Load Diff

296
docs/使用指南.md Normal file
View File

@ -0,0 +1,296 @@
# 🌀 铸渊助手 · 使用指南
---
## 一、网址
铸渊聊天室部署在 GitHub Pages 上,**正确网址**
```
https://qinfendebingshuo.github.io/guanghulab/
```
> ⚠️ 旧网址 `…/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` 分支,网站就会自动更新**。
### 方法一:合并 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: 铸渊聊天室打不开?
- 确认网址正确:`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

View File

@ -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",

View File

@ -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);
});

View File

@ -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 TokenGitHub 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);
});

View File

@ -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;
}

View File

@ -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": [