Files
Genarrative/server-node/src/app.ts
2026-04-10 15:37:02 +08:00

154 lines
4.4 KiB
TypeScript

import express from 'express';
import pinoHttp from 'pino-http';
import type { AppContext } from './context.js';
import { buildApiLogContext, withRouteMeta } from './http.js';
import { errorHandler } from './middleware/errorHandler.js';
import { requestIdMiddleware } from './middleware/requestId.js';
import { responseEnvelopeMiddleware } from './middleware/responseEnvelope.js';
import { createCharacterAssetRoutes } from './modules/assets/characterAssetRoutes.js';
import { createQwenSpriteRoutes } from './modules/assets/qwenSpriteRoutes.js';
import { createEditorRoutes } from './modules/editor/editorRoutes.js';
import { createStoryActionRoutes } from './modules/story/storyActionRoutes.js';
import { createAuthRoutes } from './routes/authRoutes.js';
import { createRuntimeRoutes } from './routes/runtimeRoutes.js';
import { notFound } from './errors.js';
function matchesRoutePrefix(
request: express.Request,
prefixes: readonly string[],
) {
const requestPath = request.path || request.originalUrl || request.url || '/';
return prefixes.some((prefix) => {
const normalizedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
return (
requestPath === normalizedPrefix ||
requestPath.startsWith(`${normalizedPrefix}/`)
);
});
}
function scopeToPrefixes(
prefixes: readonly string[],
handler: express.RequestHandler,
): express.RequestHandler {
return (request, response, next) => {
if (!matchesRoutePrefix(request, prefixes)) {
next();
return;
}
handler(request, response, next);
};
}
export function createApp(context: AppContext) {
const app = express();
const createHttpLogger = pinoHttp as unknown as (
options: Record<string, unknown>,
) => express.RequestHandler;
app.disable('x-powered-by');
app.use(requestIdMiddleware);
app.use(
createHttpLogger({
logger: context.logger,
genReqId: (request: express.Request) => request.requestId,
customSuccessObject: (
request: express.Request,
response: express.Response,
baseObject: Record<string, unknown> & { responseTime?: number },
) => ({
...baseObject,
...buildApiLogContext(request, response),
user_id: request.userId ?? null,
method: request.method,
path: request.url,
status: response.statusCode,
latency_ms: baseObject.responseTime,
}),
customErrorObject: (
request: express.Request,
response: express.Response,
error: unknown,
baseObject: Record<string, unknown> & { responseTime?: number },
) => ({
...baseObject,
...buildApiLogContext(request, response),
user_id: request.userId ?? null,
method: request.method,
path: request.url,
status: response.statusCode,
latency_ms: baseObject.responseTime,
err: error,
}),
}),
);
app.use(express.json({ limit: '10mb' }));
app.use(responseEnvelopeMiddleware);
app.get(
'/healthz',
withRouteMeta({ operation: 'health.check' }),
(_request, response) => {
response.json({
ok: true,
service: 'genarrative-node-server',
});
},
);
app.use(
scopeToPrefixes(
['/api/editor'],
withRouteMeta({ routeVersion: '2026-04-08', operation: 'editor.api' }),
),
);
app.use(scopeToPrefixes(['/api/editor'], createEditorRoutes(context.config)));
app.use(
scopeToPrefixes(
['/api/assets'],
withRouteMeta({ routeVersion: '2026-04-08', operation: 'assets.api' }),
),
);
app.use(
scopeToPrefixes(['/api/assets'], createCharacterAssetRoutes(context.config)),
);
app.use(
scopeToPrefixes(
['/api/assets/qwen-sprite'],
withRouteMeta({ routeVersion: '2026-04-08', operation: 'assets.qwen' }),
),
);
app.use(
scopeToPrefixes(
['/api/assets/qwen-sprite'],
createQwenSpriteRoutes(context.config),
),
);
app.use(
'/api/auth',
withRouteMeta({ routeVersion: '2026-04-08' }),
createAuthRoutes(context),
);
app.use(
'/api/runtime/story',
withRouteMeta({ routeVersion: '2026-04-08' }),
createStoryActionRoutes(context),
);
app.use(
'/api',
withRouteMeta({ routeVersion: '2026-04-08' }),
createRuntimeRoutes(context),
);
app.use((request, _response, next) => {
next(notFound(`接口不存在:${request.method} ${request.originalUrl}`));
});
app.use(errorHandler);
return app;
}