fix(quotas): unifier le décompte IA (BYOK, rollback) et combler les fuites
Centralise la réserve via ai-quota, corrige admin unavailable (-1), brancher les routes sans quota et le host-pays brainstorm, avec usage-meter élargi, noms de clusters, MCP et ajustements dashboard/insights. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -190,6 +190,46 @@ local newCount = tonumber(redis.call('GET', KEYS[1]))
|
||||
return newCount
|
||||
`;
|
||||
|
||||
const RELEASE_LUA = `
|
||||
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
|
||||
if current <= 0 then return 0 end
|
||||
redis.call('DECR', KEYS[1])
|
||||
return tonumber(redis.call('GET', KEYS[1]) or '0')
|
||||
`;
|
||||
|
||||
export async function releaseUsage(userId: string, feature: string): Promise<void> {
|
||||
if (!isValidFeature(feature)) return;
|
||||
|
||||
const tier = await getEffectiveTier(userId);
|
||||
const limit = await getLimitAsync(tier, feature);
|
||||
if (limit === undefined || limit === Infinity) return;
|
||||
|
||||
try {
|
||||
const key = getRedisKey(userId, feature);
|
||||
const raw = await redis.eval(RELEASE_LUA, 1, key);
|
||||
const newCount = parseReserveResult(raw);
|
||||
if (Number.isFinite(newCount) && newCount >= 0) {
|
||||
await mirrorUsageCountToDatabase(userId, feature, newCount);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[entitlements] Redis release failed, trying DB fallback:', err);
|
||||
try {
|
||||
const { periodStart } = getPeriodDates();
|
||||
const existing = await prisma.usageLog.findUnique({
|
||||
where: { userId_feature_periodStart: { userId, feature, periodStart } },
|
||||
});
|
||||
const current = existing?.requestsCount ?? 0;
|
||||
if (current <= 0) return;
|
||||
await prisma.usageLog.update({
|
||||
where: { userId_feature_periodStart: { userId, feature, periodStart } },
|
||||
data: { requestsCount: current - 1, syncedAt: new Date() },
|
||||
});
|
||||
} catch (dbErr) {
|
||||
console.error('[entitlements] DB release fallback failed:', dbErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RESERVE_LUA = `
|
||||
local limit = tonumber(ARGV[1])
|
||||
local ttl = tonumber(ARGV[2])
|
||||
@@ -472,6 +512,9 @@ export async function getUserQuotas(
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error('[entitlements] getUserQuotas Redis error:', err);
|
||||
if (shouldFailClosedOnRedisError()) {
|
||||
throw new QuotaServiceUnavailableError();
|
||||
}
|
||||
const result: Record<string, { remaining: number; limit: number; used: number }> = {};
|
||||
for (const feature of features) {
|
||||
const limit = (await getLimitAsync(tier, feature)) ?? 0;
|
||||
|
||||
Reference in New Issue
Block a user