Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
17 KiB
17 KiB
⚙️ GitHub 自动化指令 · HoloLake CI/CD + Copilot Rules
本页为 guanghulab.com GitHub 仓库的自动化执行规则,涵盖 CI/CD 流水线、接口契约校验、Copilot 自定义指令 三部分。
💡 所有规则均以 HLI 接口协议为基准,确保「Notion 注册表 ↔ 代码仓库」双向一致。
一、CI/CD 流水线 · GitHub Actions
1.1 工作流文件结构
# .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 接口契约校验脚本
// 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 对齐检查
// 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 脚本注册
{
"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:
# 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 标准格式:
{
"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 | 🟡 建议 | AI 写代码时自动对齐 HLI 规范 |
| 手动 dispatch | 全量 route-align 覆盖率报告 | 🟢 仅报告 | 查看当前实现进度 |
七、广播分发自动化(铸渊 → 各开发者)
7.1 分发工作流 · GitHub Actions
# .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
// 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 开发者怎么收广播
cd guanghulab
git pull # 拉取最新
cat 你的模块目录/LATEST-BROADCAST.md # 查看最新广播
💡 开发者只需要
git pull,新广播就自动出现在自己的模块目录里。不需要去别的地方找。
7.6 完整数据流回顾
开发者 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 → 看到新广播 → 继续开发 🔄