# ⚙️ 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 覆盖率报告 | 🟢 仅报告 | 查看当前实现进度 | --- --- ## 七、广播分发自动化(铸渊 → 各开发者) ### 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 → 看到新广播 → 继续开发 🔄 ``` ---