Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
572 lines
17 KiB
Markdown
572 lines
17 KiB
Markdown
# ⚙️ 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> |