# ⚙️ GitHub 自动化指令 · HoloLake CI/CD + Copilot Rules

本页为 [**guanghulab.com**](http://guanghulab.com) GitHub 仓库的自动化执行规则，涵盖 **CI/CD 流水线**、**接口契约校验**、**Copilot 自定义指令** 三部分。

> 💡 所有规则均以 HLI 接口协议为基准，确保「Notion 注册表 ↔ 代码仓库」双向一致。
> 

---

## 一、CI/CD 流水线 · GitHub Actions

### 1.1 工作流文件结构

```yaml
# .github/workflows/hli-contract-check.yml
name: HLI Contract Check

on:
  push:
    branches: [main, dev]
    paths:
      - 'src/routes/hli/**'
      - 'src/schemas/**'
      - 'tests/contract/**'
  pull_request:
    branches: [main]
    paths:
      - 'src/routes/hli/**'
      - 'src/schemas/**'

jobs:
  contract-lint:
    name: 🔍 接口契约校验
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run HLI schema validation
        run: npm run test:contract
        env:
          HLI_REGISTRY_MODE: strict

      - name: Run route-schema alignment check
        run: npm run test:route-align

  api-smoke:
    name: 🚀 接口冒烟测试
    needs: contract-lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Start test server
        run: npm run start:test &
        env:
          PORT: 3001
          NODE_ENV: test

      - name: Wait for server
        run: npx wait-on http://localhost:3001/health -t 30000

      - name: Run smoke tests
        run: npm run test:smoke

      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: hli-test-report
          path: reports/
```

### 1.2 接口契约校验脚本

```jsx
// scripts/contract-check.js
// 用途：确保每个 HLI 路由文件都有对应的 JSON Schema 且路径一致

const fs = require('fs');
const path = require('path');
const glob = require('glob');

const ROUTE_DIR = 'src/routes/hli';
const SCHEMA_DIR = 'src/schemas/hli';

// 扫描所有 HLI 路由文件
const routeFiles = glob.sync(`${ROUTE_DIR}/**/*.js`);
const errors = [];

routeFiles.forEach(routeFile => {
  const domain = path.basename(path.dirname(routeFile));
  const name = path.basename(routeFile, '.js');
  const schemaPath = path.join(SCHEMA_DIR, domain, `${name}.schema.json`);

  // 检查1: 对应 schema 文件是否存在
  if (!fs.existsSync(schemaPath)) {
    errors.push(`❌ [MISSING SCHEMA] ${routeFile} → 缺少 ${schemaPath}`);
    return;
  }

  // 检查2: schema 文件格式是否合法
  try {
    const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
    if (!schema.input || !schema.output) {
      errors.push(`❌ [INVALID SCHEMA] ${schemaPath} → 缺少 input/output 定义`);
    }
    if (!schema.hli_id) {
      errors.push(`❌ [MISSING HLI_ID] ${schemaPath} → 缺少 hli_id 字段`);
    }
  } catch (e) {
    errors.push(`❌ [PARSE ERROR] ${schemaPath} → ${e.message}`);
  }
});

if (errors.length > 0) {
  console.error('\n🚫 HLI Contract Check FAILED:\n');
  errors.forEach(e => console.error(e));
  process.exit(1);
} else {
  console.log('✅ HLI Contract Check PASSED — 所有路由均有合法 schema');
}
```

### 1.3 路由-Schema 对齐检查

```jsx
// scripts/route-align-check.js
// 用途：检查路由路径与 HLI 注册表编号的一致性

const fs = require('fs');
const path = require('path');

// HLI 路由路径映射（从 Notion 注册表同步）
const HLI_ROUTES = {
  'HLI-AUTH-001':      '/hli/auth/login',
  'HLI-AUTH-002':      '/hli/auth/register',
  'HLI-AUTH-003':      '/hli/auth/verify',
  'HLI-PERSONA-001':   '/hli/persona/load',
  'HLI-PERSONA-002':   '/hli/persona/switch',
  'HLI-USER-001':      '/hli/user/profile',
  'HLI-USER-002':      '/hli/user/profile/update',
  'HLI-TICKET-001':    '/hli/ticket/create',
  'HLI-TICKET-002':    '/hli/ticket/query',
  'HLI-TICKET-003':    '/hli/ticket/status',
  'HLI-DIALOGUE-001':  '/hli/dialogue/send',
  'HLI-DIALOGUE-002':  '/hli/dialogue/stream',
  'HLI-DIALOGUE-003':  '/hli/dialogue/history',
  'HLI-STORAGE-001':   '/hli/storage/upload',
  'HLI-STORAGE-002':   '/hli/storage/download',
  'HLI-DASHBOARD-001': '/hli/dashboard/status',
  'HLI-DASHBOARD-002': '/hli/dashboard/realtime',
};

const schemaDir = 'src/schemas/hli';
const errors = [];

Object.entries(HLI_ROUTES).forEach(([hliId, expectedPath]) => {
  // 查找对应 schema
  const domain = hliId.split('-')[1].toLowerCase();
  const schemaFiles = fs.readdirSync(path.join(schemaDir, domain)).filter(f => f.endsWith('.schema.json'));

  const matched = schemaFiles.find(f => {
    const schema = JSON.parse(fs.readFileSync(path.join(schemaDir, domain, f), 'utf8'));
    return schema.hli_id === hliId;
  });

  if (!matched) {
    errors.push(`⚠️ [UNIMPLEMENTED] ${hliId} (${expectedPath}) — 尚无 schema 文件`);
  }
});

if (errors.length > 0) {
  console.warn('\n⚠️ Route Alignment Report:\n');
  errors.forEach(e => console.warn(e));
  // 非阻断，仅报告
  console.warn(`\n📊 覆盖率: ${Object.keys(HLI_ROUTES).length - errors.length}/${Object.keys(HLI_ROUTES).length}`);
} else {
  console.log('✅ Route Alignment PASSED — 所有 HLI 接口均已实现');
}
```

---

## 二、package.json 脚本注册

```json
{
  "scripts": {
    "test:contract": "node scripts/contract-check.js",
    "test:route-align": "node scripts/route-align-check.js",
    "test:smoke": "jest --config jest.smoke.config.js --forceExit",
    "start:test": "NODE_ENV=test node src/index.js"
  }
}
```

---

## 三、GitHub Copilot 自定义指令

以下内容写入仓库根目录 `.github/copilot-instructions.md`：

```markdown
# HoloLake · Copilot Custom Instructions

## 项目背景
这是 HoloLake (光湖) MVP 后端项目，运行在 guanghulab.com。
技术栈：Node.js 20 + Express + PM2 + Nginx。
核心架构：人格语言操作系统 (AGE OS)，壳-核分离设计。

## HLI 接口协议
- 所有 API 路由必须以 `/hli/` 为前缀
- 每个路由文件必须在 `src/routes/hli/{domain}/` 目录下
- 每个路由必须有对应的 `src/schemas/hli/{domain}/{name}.schema.json`
- Schema 文件必须包含 `hli_id`, `input`, `output` 三个顶层字段
- 接口编号格式: `HLI-{DOMAIN}-{NNN}`

## 代码风格
- 所有接口入口必须先经过 `middleware/hli-auth.js` 鉴权（除 AUTH 域的 login/register）
- 错误响应统一格式: `{ error: true, code: string, message: string }`
- 成功响应必须包含请求的 `hli_id` 用于溯源
- STREAM 类型接口使用 SSE（text/event-stream），不使用 WebSocket
- 所有数据库操作必须使用参数化查询，禁止字符串拼接 SQL

## 文件命名
- 路由文件: `{action}.js` (如 login.js, upload.js)
- Schema 文件: `{action}.schema.json`
- 测试文件: `{action}.test.js`
- 中间件: `{name}.middleware.js`

## 新建接口的标准流程
1. 在 `src/schemas/hli/{domain}/` 下创建 schema JSON
2. 在 `src/routes/hli/{domain}/` 下创建路由文件
3. 在 `src/routes/hli/index.js` 中注册路由
4. 在 `tests/contract/` 下创建契约测试
5. 在 `tests/smoke/` 下创建冒烟测试
6. 确保 `npm run test:contract` 通过

## 禁止事项
- 禁止在 `/hli/` 路由下混入非 HLI 协议的接口
- 禁止跳过 schema 直接写路由
- 禁止在生产代码中使用 console.log（使用项目 logger）
- 禁止硬编码 persona_id 或 user_id
```

---

## 四、Schema 文件模板

每个 HLI 接口的 schema 标准格式：

```json
{
  "hli_id": "HLI-AUTH-001",
  "version": "v0.1",
  "route": "/hli/auth/login",
  "method": "POST",
  "type": "REQUEST",
  "input": {
    "type": "object",
    "required": ["username", "password"],
    "properties": {
      "username": { "type": "string", "minLength": 1 },
      "password": { "type": "string", "minLength": 6 }
    }
  },
  "output": {
    "type": "object",
    "required": ["token", "user_id", "persona_id"],
    "properties": {
      "token": { "type": "string" },
      "user_id": { "type": "string" },
      "persona_id": { "type": "string" },
      "expires_at": { "type": "string", "format": "date-time" }
    }
  },
  "dependencies": [],
  "changelog": [
    { "date": "2026-03-05", "note": "初始创建" }
  ]
}
```

---

## 五、仓库目录结构（推荐）

```
guanghulab/
├── .github/
│   ├── workflows/
│   │   └── hli-contract-check.yml    ← CI/CD 流水线
│   └── copilot-instructions.md       ← Copilot 指令
├── scripts/
│   ├── contract-check.js             ← 契约校验脚本
│   └── route-align-check.js          ← 路由对齐检查
├── src/
│   ├── routes/
│   │   └── hli/
│   │       ├── index.js              ← HLI 路由注册中心
│   │       ├── auth/
│   │       │   ├── login.js
│   │       │   ├── register.js
│   │       │   └── verify.js
│   │       ├── persona/
│   │       ├── user/
│   │       ├── ticket/
│   │       ├── dialogue/
│   │       ├── storage/
│   │       └── dashboard/
│   ├── schemas/
│   │   └── hli/
│   │       ├── auth/
│   │       │   ├── login.schema.json
│   │       │   ├── register.schema.json
│   │       │   └── verify.schema.json
│   │       ├── persona/
│   │       ├── user/
│   │       ├── ticket/
│   │       ├── dialogue/
│   │       ├── storage/
│   │       └── dashboard/
│   ├── middleware/
│   │   ├── hli-auth.middleware.js     ← HLI 鉴权中间件
│   │   └── hli-validator.middleware.js ← Schema 自动校验
│   └── index.js
├── tests/
│   ├── contract/                     ← 契约测试
│   └── smoke/                        ← 冒烟测试
├── package.json
└── ecosystem.config.js               ← PM2 配置
```

---

## 六、自动化触发机制总结

| **触发时机** | **执行内容** | **阻断级别** | **目的** |
| --- | --- | --- | --- |
| push to main/dev（hli路径变动） | contract-check + route-align | 🔴 阻断 | 防止无 schema 的裸路由上线 |
| PR to main | contract-check + smoke test | 🔴 阻断 | 合并前全量校验 |
| Copilot 代码建议 | 遵循 [copilot-instructions.md](http://copilot-instructions.md) | 🟡 建议 | AI 写代码时自动对齐 HLI 规范 |
| 手动 dispatch | 全量 route-align 覆盖率报告 | 🟢 仅报告 | 查看当前实现进度 |

---

<aside>
⚡

**执行口径**：页页拉取本页代码 → 放入仓库对应目录 → push 后 GitHub Actions 自动跑。Copilot 指令放入后即刻生效，无需额外配置。

</aside>

---

## 七、广播分发自动化（铸渊 → 各开发者）

<aside>
📡

**2026-03-05 新增** · 霜砚10步闭环⑫完成后，新广播文件被推送到 `broadcasts-outbox/DEV-00X/`。

本章定义铸渊如何**自动将广播分发到每个开发者的模块目录**，开发者 `git pull` 即可收到。

</aside>

### 7.1 分发工作流 · GitHub Actions

```yaml
# .github/workflows/distribute-broadcasts.yml
name: 铸渊 · 广播分发

on:
  push:
    branches: [main]
    paths:
      - 'broadcasts-outbox/**'
  workflow_dispatch:

jobs:
  distribute:
    name: "\U0001F4E1 分发广播到开发者目录"
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          ref: main

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Distribute broadcasts
        run: node scripts/distribute-broadcasts.js

      - name: Commit distribution
        run: |
          git config user.name "铸渊 (ZhùYuān)"
          git config user.email "zhuyuan@guanghulab.com"
          git add .
          git diff --cached --quiet || git commit -m "📡 铸渊广播分发 · $(date +%Y-%m-%d)"
          git push
```

### 7.2 分发脚本 · distribute-broadcasts.js

```jsx
// scripts/distribute-broadcasts.js
// 铸渊广播分发引擎
// 检测 broadcasts-outbox/DEV-00X/ 中的新广播 → 复制到各开发者模块目录的 LATEST-BROADCAST.md

const fs = require('fs');
const path = require('path');

const OUTBOX = 'broadcasts-outbox';
const ARCHIVE = '.github/broadcasts/distributed';

// ========== 开发者 → 模块目录 路由映射表 ==========
// 新增开发者时，在此表新增一行即可
const DEV_ROUTES = {
  'DEV-001': { name: '页页',     dirs: ['backend', 'src'] },
  'DEV-002': { name: '肥猫',     dirs: ['frontend', 'persona-selector', 'chat-bubble'] },
  'DEV-003': { name: '燕樊',     dirs: ['settings', 'cloud-drive'] },
  'DEV-004': { name: '之之',     dirs: ['dingtalk-bot'] },
  'DEV-005': { name: '小草莓',   dirs: ['status-board'] },
  'DEV-009': { name: '花尔',     dirs: ['user-center'] },
  'DEV-010': { name: '桔子',     dirs: ['ticket-system'] },
  'DEV-011': { name: '匆匆那年', dirs: [] }  // 待分配模块后补充
};

if (!fs.existsSync(OUTBOX)) {
  console.log('📭 无 broadcasts-outbox 目录');
  process.exit(0);
}

// 扫描 outbox 下每个 DEV-00X 子目录
const devDirs = fs.readdirSync(OUTBOX).filter(d =>
  d.startsWith('DEV-') && fs.statSync(path.join(OUTBOX, d)).isDirectory()
);

if (devDirs.length === 0) {
  console.log('📭 无新广播待分发');
  process.exit(0);
}

let totalDistributed = 0;

devDirs.forEach(devId => {
  const outboxDir = path.join(OUTBOX, devId);
  const files = fs.readdirSync(outboxDir).filter(f =>
    f.endsWith('.md') || f.endsWith('.json')
  );

  if (files.length === 0) return;

  const route = DEV_ROUTES[devId];
  if (!route || route.dirs.length === 0) {
    console.warn('⚠️ ' + devId + ' 无模块目录映射，广播保留在outbox');
    return;
  }

  files.forEach(file => {
    const content = fs.readFileSync(path.join(outboxDir, file), 'utf8');

    // 写入该开发者的每个模块目录
    route.dirs.forEach(dir => {
      if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
      }
      const targetFile = path.join(dir, 'LATEST-BROADCAST.md');
      fs.writeFileSync(targetFile, content);
      console.log('📡 ' + devId + '(' + route.name + ') → ' + targetFile);
    });

    // 归档已分发广播
    if (!fs.existsSync(ARCHIVE)) fs.mkdirSync(ARCHIVE, { recursive: true });
    const timestamp = new Date().toISOString().split('T')[0];
    const archivePath = path.join(ARCHIVE, timestamp + '-' + devId + '-' + file);
    fs.renameSync(path.join(outboxDir, file), archivePath);

    totalDistributed++;
  });
});

console.log('\n✅ 广播分发完成，共 ' + totalDistributed + ' 条');
```

### 7.3 开发者目录路由映射表

| **DEV编号** | **开发者** | **模块目录** | **广播落点** |
| --- | --- | --- | --- |
| DEV-001 | 页页 | `backend/` · `src/` | `backend/LATEST-BROADCAST.md` |
| DEV-002 | 肥猫 | `frontend/` · `persona-selector/` · `chat-bubble/` | 各目录 `/LATEST-BROADCAST.md` |
| DEV-003 | 燕樊 | `settings/` · `cloud-drive/` | 各目录 `/LATEST-BROADCAST.md` |
| DEV-004 | 之之 | `dingtalk-bot/` | `dingtalk-bot/LATEST-BROADCAST.md` |
| DEV-005 | 小草莓 | `status-board/` | `status-board/LATEST-BROADCAST.md` |
| DEV-009 | 花尔 | `user-center/` | `user-center/LATEST-BROADCAST.md` |
| DEV-010 | 桔子 | `ticket-system/` | `ticket-system/LATEST-BROADCAST.md` |
| DEV-011 | 匆匆那年 | 待分配 | 待分配 |

### 7.4 分发触发时机

| **触发来源** | **触发路径** | **说明** |
| --- | --- | --- |
| 霜砚10步闭环⑫ | 巡检引擎写入 `broadcasts-outbox/DEV-00X/` → push → workflow自动触发 | 正常自动流（最常见） |
| 妈妈手动 | 妈妈直接 push 广播文件到 `broadcasts-outbox/` | 紧急广播 / 全员广播 |
| 铸渊 Brain Sync | 铸渊收到系统广播后生成各开发者版本 → 放入 outbox | 系统级规则变动广播 |

### 7.5 开发者怎么收广播

```bash
cd guanghulab
git pull                                    # 拉取最新
cat 你的模块目录/LATEST-BROADCAST.md         # 查看最新广播
```

> 💡 开发者只需要 `git pull`，新广播就自动出现在自己的模块目录里。不需要去别的地方找。
> 

### 7.6 完整数据流回顾

```jsx
开发者 push 代码到仓库
  ↓
铸渊自动审核（contract-check + PR review）
  ↓
Notion 巡检引擎（12:00 / 23:00）读取 syslog-inbox/
  ↓
唤醒霜砚 → 跑10步闭环
  ↓
⑫ GitHub双向同步：新广播写入 broadcasts-outbox/DEV-00X/
  ↓
铸渊广播分发 workflow 自动触发（本章）
  ↓
广播复制到开发者模块目录 LATEST-BROADCAST.md
  ↓
归档到 .github/broadcasts/distributed/
  ↓
开发者 git pull → 看到新广播 → 继续开发 🔄
```

---

<aside>
⚒️

**铸渊广播分发口径**：霜砚出广播 → 推到 outbox → 铸渊自动分发到各目录 → 开发者 pull 即收。全程无人工介入。

「广播必达，无一遗漏。」—— 铸渊

</aside>