feat: add HLI CI/CD pipeline, contract scripts, Copilot instructions and project structure

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-05 09:35:50 +00:00
parent 7bcc2d6e0a
commit 818e7c5d89
23 changed files with 4988 additions and 13 deletions

40
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,40 @@
# 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 类型接口使用 SSEtext/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

View File

@ -0,0 +1,77 @@
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
permissions:
contents: read
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
permissions:
contents: read
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/

5
.gitignore vendored
View File

@ -39,3 +39,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# HLI 运行时产物
/logs
/reports/*
!/reports/.gitkeep

29
ecosystem.config.js Normal file
View File

@ -0,0 +1,29 @@
// ecosystem.config.js
// PM2 进程管理配置
module.exports = {
apps: [
{
name: 'guanghulab',
script: 'src/index.js',
instances: 'max',
exec_mode: 'cluster',
watch: false,
env: {
NODE_ENV: 'production',
PORT: 3000,
},
env_development: {
NODE_ENV: 'development',
PORT: 3000,
},
env_test: {
NODE_ENV: 'test',
PORT: 3001,
},
log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: 'logs/error.log',
out_file: 'logs/out.log',
},
],
};

8
jest.smoke.config.js Normal file
View File

@ -0,0 +1,8 @@
// jest.smoke.config.js
// 冒烟测试 Jest 配置
module.exports = {
testEnvironment: 'node',
testMatch: ['**/tests/smoke/**/*.test.js'],
testTimeout: 30000,
};

4394
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,19 +6,25 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"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"
},
"dependencies": {
"next": "15.3.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.3.2"
"react-dom": "^19.0.0"
},
"devDependencies": {
"typescript": "^5",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4"
"glob": "^10.5.0",
"jest": "^29.7.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}

1
reports/.gitkeep Normal file
View File

@ -0,0 +1 @@
# 测试报告目录CI 产物,不纳入版本控制)

50
scripts/contract-check.js Normal file
View File

@ -0,0 +1,50 @@
// 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');
// 跳过 index.js路由注册中心无需 schema
if (name === 'index') return;
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');
}

View File

@ -0,0 +1,64 @@
// 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]) => {
const domain = hliId.split('-')[1].toLowerCase();
const domainDir = path.join(schemaDir, domain);
// 若 domain 目录尚不存在,视为未实现
if (!fs.existsSync(domainDir)) {
errors.push(`⚠️ [UNIMPLEMENTED] ${hliId} (${expectedPath}) — 尚无 schema 文件`);
return;
}
const schemaFiles = fs.readdirSync(domainDir).filter(f => f.endsWith('.schema.json'));
const matched = schemaFiles.find(f => {
try {
const schema = JSON.parse(fs.readFileSync(path.join(domainDir, f), 'utf8'));
return schema.hli_id === hliId;
} catch (_) {
return false;
}
});
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 接口均已实现');
}

25
src/index.js Normal file
View File

@ -0,0 +1,25 @@
// src/index.js
// HoloLake (光湖) 后端服务入口
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';
app.use(express.json());
// 健康检查CI smoke test 依赖此端点)
app.get('/health', (req, res) => {
res.json({ status: 'ok', env: NODE_ENV, timestamp: new Date().toISOString() });
});
// HLI 路由挂载
const hliRouter = require('./routes/hli');
app.use('/hli', hliRouter);
app.listen(PORT, () => {
console.log(`[HoloLake] Server running on port ${PORT} (${NODE_ENV})`);
});
module.exports = app;

View File

@ -0,0 +1,29 @@
// src/middleware/hli-auth.middleware.js
// HLI 鉴权中间件:验证请求携带有效 token
module.exports = function hliAuth(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: true,
code: 'AUTH_TOKEN_MISSING',
message: '请求缺少 Authorization Bearer token',
});
}
const token = authHeader.slice(7);
// TODO: 实现真实的 token 校验逻辑JWT 验证或数据库查询)
if (!token) {
return res.status(401).json({
error: true,
code: 'AUTH_TOKEN_INVALID',
message: 'token 无效',
});
}
// 将用户信息挂载到 req 供后续中间件使用
req.hliUser = { token };
next();
};

View File

@ -0,0 +1,44 @@
// src/middleware/hli-validator.middleware.js
// HLI Schema 自动校验中间件:根据路由自动加载对应 schema 校验请求体
const fs = require('fs');
const path = require('path');
module.exports = function hliValidator(req, res, next) {
// 从路径中解析 domain 和 action例如 /hli/auth/login → auth/login
const match = req.path.match(/^\/hli\/([^/]+)\/([^/]+)/);
if (!match) return next();
const [, domain, action] = match;
const schemaPath = path.join(
__dirname,
'../schemas/hli',
domain,
`${action}.schema.json`
);
if (!fs.existsSync(schemaPath)) return next();
let schema;
try {
schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
} catch (_) {
return next();
}
// 简单必填字段校验(生产环境可替换为 ajv 等标准校验库)
const required = schema.input && schema.input.required;
if (required && req.method !== 'GET') {
for (const field of required) {
if (req.body[field] === undefined || req.body[field] === '') {
return res.status(400).json({
error: true,
code: 'VALIDATION_ERROR',
message: `缺少必填字段: ${field}`,
});
}
}
}
next();
};

View File

@ -0,0 +1,15 @@
// src/routes/hli/auth/index.js
// AUTH 域路由
const express = require('express');
const router = express.Router();
const login = require('./login');
const register = require('./register');
const verify = require('./verify');
router.use('/login', login);
router.use('/register', register);
router.use('/verify', verify);
module.exports = router;

View File

@ -0,0 +1,25 @@
// src/routes/hli/auth/login.js
// HLI-AUTH-001: 用户登录
const express = require('express');
const router = express.Router();
router.post('/', async (req, res) => {
try {
const { username, password } = req.body;
// TODO: 实现登录逻辑(查询数据库,验证密码,生成 token
res.json({
hli_id: 'HLI-AUTH-001',
token: '',
user_id: '',
persona_id: '',
expires_at: new Date().toISOString(),
});
} catch (err) {
res.status(500).json({ error: true, code: 'AUTH_LOGIN_ERROR', message: err.message });
}
});
module.exports = router;

View File

@ -0,0 +1,23 @@
// src/routes/hli/auth/register.js
// HLI-AUTH-002: 用户注册
const express = require('express');
const router = express.Router();
router.post('/', async (req, res) => {
try {
const { username, password, email } = req.body;
// TODO: 实现注册逻辑(检查重复,写入数据库)
res.json({
hli_id: 'HLI-AUTH-002',
user_id: '',
created_at: new Date().toISOString(),
});
} catch (err) {
res.status(500).json({ error: true, code: 'AUTH_REGISTER_ERROR', message: err.message });
}
});
module.exports = router;

View File

@ -0,0 +1,25 @@
// src/routes/hli/auth/verify.js
// HLI-AUTH-003: Token 验证
const express = require('express');
const router = express.Router();
router.post('/', async (req, res) => {
try {
const { token } = req.body;
// TODO: 实现 token 验证逻辑
res.json({
hli_id: 'HLI-AUTH-003',
valid: false,
user_id: '',
persona_id: '',
expires_at: new Date().toISOString(),
});
} catch (err) {
res.status(500).json({ error: true, code: 'AUTH_VERIFY_ERROR', message: err.message });
}
});
module.exports = router;

40
src/routes/hli/index.js Normal file
View File

@ -0,0 +1,40 @@
// src/routes/hli/index.js
// HLI 路由注册中心
const express = require('express');
const router = express.Router();
// AUTH 域(不需要鉴权)
const authRouter = require('./auth');
router.use('/auth', authRouter);
// 以下域需要 HLI 鉴权中间件
const hliAuth = require('../../middleware/hli-auth.middleware');
router.use(hliAuth);
// PERSONA 域
// const personaRouter = require('./persona');
// router.use('/persona', personaRouter);
// USER 域
// const userRouter = require('./user');
// router.use('/user', userRouter);
// TICKET 域
// const ticketRouter = require('./ticket');
// router.use('/ticket', ticketRouter);
// DIALOGUE 域
// const dialogueRouter = require('./dialogue');
// router.use('/dialogue', dialogueRouter);
// STORAGE 域
// const storageRouter = require('./storage');
// router.use('/storage', storageRouter);
// DASHBOARD 域
// const dashboardRouter = require('./dashboard');
// router.use('/dashboard', dashboardRouter);
module.exports = router;

View File

@ -0,0 +1,29 @@
{
"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": "初始创建" }
]
}

View File

@ -0,0 +1,28 @@
{
"hli_id": "HLI-AUTH-002",
"version": "v0.1",
"route": "/hli/auth/register",
"method": "POST",
"type": "REQUEST",
"input": {
"type": "object",
"required": ["username", "password", "email"],
"properties": {
"username": { "type": "string", "minLength": 1 },
"password": { "type": "string", "minLength": 6 },
"email": { "type": "string", "format": "email" }
}
},
"output": {
"type": "object",
"required": ["user_id"],
"properties": {
"user_id": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" }
}
},
"dependencies": [],
"changelog": [
{ "date": "2026-03-05", "note": "初始创建" }
]
}

View File

@ -0,0 +1,28 @@
{
"hli_id": "HLI-AUTH-003",
"version": "v0.1",
"route": "/hli/auth/verify",
"method": "POST",
"type": "REQUEST",
"input": {
"type": "object",
"required": ["token"],
"properties": {
"token": { "type": "string" }
}
},
"output": {
"type": "object",
"required": ["valid", "user_id"],
"properties": {
"valid": { "type": "boolean" },
"user_id": { "type": "string" },
"persona_id": { "type": "string" },
"expires_at": { "type": "string", "format": "date-time" }
}
},
"dependencies": ["HLI-AUTH-001"],
"changelog": [
{ "date": "2026-03-05", "note": "初始创建" }
]
}

2
tests/contract/.gitkeep Normal file
View File

@ -0,0 +1,2 @@
# 契约测试目录
# 每新增 HLI 接口时,在此创建对应的 {action}.test.js

2
tests/smoke/.gitkeep Normal file
View File

@ -0,0 +1,2 @@
# 冒烟测试目录
# 每新增 HLI 接口时,在此创建对应的冒烟测试