chore: 服务骨架(Hono+TS+vitest+eslint,health 路由与探活注册点)
This commit is contained in:
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.log
|
||||
@@ -0,0 +1,8 @@
|
||||
# bpos-service-gateway
|
||||
|
||||
前端唯一入口:BFF 聚合、鉴权与到 task / data / agent 的路由(:8800,无自有库)。
|
||||
|
||||
```bash
|
||||
pnpm i && pnpm dev # 本地起服务
|
||||
pnpm gates # typecheck + lint + test
|
||||
```
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+1971
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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-/);
|
||||
});
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user