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:
parent
7bcc2d6e0a
commit
818e7c5d89
|
|
@ -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 类型接口使用 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
|
||||
|
|
@ -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/
|
||||
|
|
@ -39,3 +39,8 @@ yarn-error.log*
|
|||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# HLI 运行时产物
|
||||
/logs
|
||||
/reports/*
|
||||
!/reports/.gitkeep
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// jest.smoke.config.js
|
||||
// 冒烟测试 Jest 配置
|
||||
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/tests/smoke/**/*.test.js'],
|
||||
testTimeout: 30000,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# 测试报告目录(CI 产物,不纳入版本控制)
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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 接口均已实现');
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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": "初始创建" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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": "初始创建" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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": "初始创建" }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# 契约测试目录
|
||||
# 每新增 HLI 接口时,在此创建对应的 {action}.test.js
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# 冒烟测试目录
|
||||
# 每新增 HLI 接口时,在此创建对应的冒烟测试
|
||||
Loading…
Reference in New Issue