2026-06-03 11:37:55 +08:00

101 lines
3.2 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import {
PREMIUM_MODULE_IDS,
updatePremiumModuleRuntimeStatus,
type PremiumModuleRuntimeStatus,
type PremiumModuleRuntimeStatusValue,
} from '@/lib/license/premium-module-service';
const validModuleIds = new Set<string>(Object.values(PREMIUM_MODULE_IDS));
const validStatuses = new Set<PremiumModuleRuntimeStatusValue>([
'missing',
'pulling',
'running',
'unhealthy',
'expired',
'revoked',
]);
function getRuntimeStatusToken(): string {
return (
process.env.MODULE_RUNTIME_STATUS_TOKEN ||
process.env.FLNXI_MODULE_STATUS_CALLBACK_TOKEN ||
''
).trim();
}
function readBearerToken(request: NextRequest): string {
const authorization = request.headers.get('authorization') || '';
if (authorization.toLowerCase().startsWith('bearer ')) {
return authorization.slice('bearer '.length).trim();
}
return (request.headers.get('x-module-status-token') || '').trim();
}
function normalizeRuntimeStatus(input: unknown): PremiumModuleRuntimeStatus | null {
if (!input || typeof input !== 'object') return null;
const raw = input as Record<string, unknown>;
const moduleId = typeof raw.moduleId === 'string' ? raw.moduleId.trim() : '';
const image = typeof raw.image === 'string' ? raw.image.trim() : '';
const digest = typeof raw.digest === 'string' && raw.digest.trim() ? raw.digest.trim() : null;
const status = typeof raw.status === 'string' ? raw.status.trim() : '';
const lastSyncAt = typeof raw.lastSyncAt === 'string' ? raw.lastSyncAt.trim() : '';
const lastError = typeof raw.lastError === 'string' && raw.lastError.trim()
? raw.lastError.trim()
: undefined;
if (!validModuleIds.has(moduleId)) return null;
if (!image) return null;
if (!validStatuses.has(status as PremiumModuleRuntimeStatusValue)) return null;
const lastSyncAtTime = Date.parse(lastSyncAt);
if (!lastSyncAt || Number.isNaN(lastSyncAtTime)) return null;
return {
moduleId,
image,
digest,
status: status as PremiumModuleRuntimeStatusValue,
lastSyncAt: new Date(lastSyncAtTime).toISOString(),
lastError,
};
}
export async function POST(request: NextRequest) {
const configuredToken = getRuntimeStatusToken();
if (!configuredToken || configuredToken.length < 32) {
return NextResponse.json(
{ success: false, error: '模块运行状态回写 token 未配置' },
{ status: 503 }
);
}
if (readBearerToken(request) !== configuredToken) {
return NextResponse.json(
{ success: false, error: '模块运行状态回写未授权' },
{ status: 401 }
);
}
const body = await request.json().catch(() => null);
const rawStatuses: unknown[] = Array.isArray(body?.statuses) ? body.statuses : [body];
const statuses = rawStatuses.map(normalizeRuntimeStatus);
if (statuses.length === 0 || statuses.some((status: PremiumModuleRuntimeStatus | null) => !status)) {
return NextResponse.json(
{ success: false, error: '模块运行状态参数无效' },
{ status: 400 }
);
}
for (const status of statuses as PremiumModuleRuntimeStatus[]) {
await updatePremiumModuleRuntimeStatus(status);
}
return NextResponse.json({
success: true,
updated: statuses.length,
});
}