chore: 服务骨架(Hono+TS+vitest+eslint,health 路由与探活注册点)

This commit is contained in:
lovefyf
2026-08-13 00:39:39 +08:00
commit 2a89abd638
15 changed files with 2125 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
PORT=<缺省 8800>
NATS_URL=<nats://主机:4222>
KEYCLOAK_URL=<Keycloak 站点地址>
KEYCLOAK_REALM=<realm 名,如 corp>
KEYCLOAK_CLIENT_ID=<本仓 client,如 api-gateway>
KEYCLOAK_CLIENT_SECRET=<从 Keycloak Credentials 标签抄>
AGENT_SERVICE_URL=<agent 服务地址,缺省 http://127.0.0.1:8901>
TASK_SERVICE_URL=<task 服务地址,缺省 http://127.0.0.1:8801>
DATA_SERVICE_URL=<data 服务地址,缺省 http://127.0.0.1:8802>
OBJECT_STORE_MODE=<fs | s3>
OBJECT_STORE_DIR=<本地对象目录,如 ./.objectstore>
+6
View File
@@ -0,0 +1,6 @@
node_modules/
dist/
.env
.env.*
!.env.example
*.log
+8
View File
@@ -0,0 +1,8 @@
# bpos-service-gateway
前端唯一入口:BFF 聚合、鉴权与到 task / data / agent 的路由(:8800,无自有库)。
```bash
pnpm i && pnpm dev # 本地起服务
pnpm gates # typecheck + lint + test
```
+8
View File
@@ -0,0 +1,8 @@
import tseslint from 'typescript-eslint';
export default tseslint.config(...tseslint.configs.recommended, {
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
},
});
+28
View File
@@ -0,0 +1,28 @@
{
"name": "bpos-service-gateway",
"private": true,
"type": "module",
"engines": { "node": ">=22" },
"scripts": {
"dev": "tsx watch --env-file=.env src/app/server.ts",
"start": "tsx --env-file=.env src/app/server.ts",
"typecheck": "tsc --noEmit",
"lint": "eslint src test",
"test": "vitest run",
"gates": "pnpm -s typecheck && pnpm -s lint && pnpm -s test"
},
"dependencies": {
"@hono/node-server": "^2.0.12",
"hono": "^4.12.0",
"jose": "^6.0.0",
"nats": "^2.29.0"
},
"devDependencies": {
"@types/node": "^22.10.0",
"eslint": "^9.20.0",
"tsx": "^4.19.0",
"typescript": "^5.9.0",
"typescript-eslint": "^8.20.0",
"vitest": "^3.0.0"
}
}
+1971
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: true
+8
View File
@@ -0,0 +1,8 @@
import { Hono } from 'hono';
import { healthRoute } from './health';
export function buildApp(): Hono {
const app = new Hono();
app.route('/health', healthRoute());
return app;
}
+23
View File
@@ -0,0 +1,23 @@
import { Hono } from 'hono';
import { config } from '../shared/config';
export type HealthCheck = () => Promise<'ok' | 'fail'>;
const checks = new Map<string, HealthCheck>();
/** 后续任务注册 db / nats 探活 */
export function registerHealthCheck(name: string, fn: HealthCheck): void {
checks.set(name, fn);
}
export function healthRoute(): Hono {
const r = new Hono();
r.get('/', async (c) => {
const entries = await Promise.all(
[...checks.entries()].map(async ([name, fn]) => [name, await fn()] as const),
);
const result = Object.fromEntries(entries);
const ok = entries.every(([, v]) => v === 'ok');
return c.json({ ok, service: config.serviceName, checks: result }, ok ? 200 : 503);
});
return r;
}
+7
View File
@@ -0,0 +1,7 @@
import { serve } from '@hono/node-server';
import { buildApp } from './app';
import { config } from '../shared/config';
serve({ fetch: buildApp().fetch, port: config.port }, (info) => {
console.log(`${config.serviceName} listening on :${info.port}`);
});
+14
View File
@@ -0,0 +1,14 @@
export const config = {
port: Number(process.env.PORT ?? 8800),
natsUrl: process.env.NATS_URL ?? 'nats://172.16.86.151:4222',
keycloak: {
url: process.env.KEYCLOAK_URL ?? 'https://keycloak.akhr-ai.com',
realm: process.env.KEYCLOAK_REALM ?? 'corp',
clientId: process.env.KEYCLOAK_CLIENT_ID ?? 'api-gateway',
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, // 只从环境读,无缺省;data-service 恒无
},
agentServiceUrl: process.env.AGENT_SERVICE_URL ?? 'http://127.0.0.1:8901',
taskServiceUrl: process.env.TASK_SERVICE_URL ?? 'http://127.0.0.1:8801',
dataServiceUrl: process.env.DATA_SERVICE_URL ?? 'http://127.0.0.1:8802',
serviceName: 'bpos-service-gateway',
} as const;
+4
View File
@@ -0,0 +1,4 @@
// 测试期环境缺省:先于任何被测模块的 import 生效
process.env.KEYCLOAK_URL ??= 'https://keycloak.akhr-ai.com';
process.env.KEYCLOAK_REALM ??= 'corp';
process.env.KEYCLOAK_CLIENT_ID ??= 'api-gateway';
+12
View File
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';
import { buildApp } from '../../src/app/app';
describe('health', () => {
it('无探活项时返回 200 ok', async () => {
const res = await buildApp().request('/health');
expect(res.status).toBe(200);
const body = (await res.json()) as { ok: boolean; service: string };
expect(body.ok).toBe(true);
expect(body.service).toMatch(/^bpos-service-/);
});
});
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src", "test", "*.config.ts", "drizzle.config.ts"]
}
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
setupFiles: ['test/support/setup.ts'],
testTimeout: 30_000,
hookTimeout: 30_000,
},
});