154 lines
4.4 KiB
TypeScript
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;
|
|
}
|