feat(billing): enforce image generation quota with admin API and user display
Memind CI / Test, build, and release guards (push) Failing after 3s
Memind CI / Test, build, and release guards (push) Failing after 3s
Add period_images_bonus and ledger tracking, gate image_make on remaining quota, expose admin image-quota routes, show remaining image quota in the balance popover, and document that admin UI must live in memind_adm (5174) not ops. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { IMAGE_MAKE_PURPOSE_CATALOG } from './asset-gateway.mjs';
|
||||
|
||||
const PURPOSES = {
|
||||
inline_image: { presetId: 'memind_square_illustration', filename: 'inline-image.webp', envPrefix: 'INLINE_IMAGE' },
|
||||
@@ -58,9 +59,15 @@ export function createMindSpaceImageGenerationService({
|
||||
assetService,
|
||||
imageMakeClient,
|
||||
imageReviewService,
|
||||
subscriptionService = null,
|
||||
env = process.env,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
function shouldBillImageGeneration(purpose) {
|
||||
const purposeConfig = IMAGE_MAKE_PURPOSE_CATALOG.find((item) => item.id === purpose);
|
||||
return purposeConfig?.strategy !== 'derive';
|
||||
}
|
||||
|
||||
async function generate({
|
||||
userId,
|
||||
purpose,
|
||||
@@ -78,6 +85,18 @@ export function createMindSpaceImageGenerationService({
|
||||
}
|
||||
const gate = await configService.resolveImageGenerationPurpose(purpose);
|
||||
if (!gate.ok) return { ...gate, fallback: true };
|
||||
if (shouldBillImageGeneration(purpose) && subscriptionService?.checkImageQuota) {
|
||||
const quotaCheck = await subscriptionService.checkImageQuota(userId, 1);
|
||||
if (!quotaCheck.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
fallback: false,
|
||||
code: quotaCheck.code ?? 'image_quota_exceeded',
|
||||
message: quotaCheck.message ?? '图片生成额度不足',
|
||||
quota: quotaCheck.quota ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!imageMakeClient || !assetService?.createChatAsset) {
|
||||
return { ok: false, fallback: true, code: 'runtime_unavailable', message: '图片生成服务未配置' };
|
||||
}
|
||||
@@ -162,6 +181,18 @@ export function createMindSpaceImageGenerationService({
|
||||
await imageMakeClient.acknowledge(generated.jobId, asset.id).catch((error) => {
|
||||
logger.warn?.('[image_make] acknowledge failed; TTL cleanup will apply:', error?.message ?? error);
|
||||
});
|
||||
if (shouldBillImageGeneration(purpose) && subscriptionService?.consumeImageQuota) {
|
||||
const consumed = await subscriptionService.consumeImageQuota(userId, 1, null, {
|
||||
refId: generated.jobId,
|
||||
note: `image_make:${purpose}`,
|
||||
});
|
||||
if (!consumed?.fullyCovers) {
|
||||
logger.warn?.('[image_make] quota consume failed after successful generation', {
|
||||
userId,
|
||||
jobId: generated.jobId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
purpose,
|
||||
|
||||
Reference in New Issue
Block a user