fix(billing): 封住发布前计费竞态
ci / verify (pull_request) Failing after 8s
ci / verify (pull_request) Failing after 8s
阻止上游提交状态不明的任务被租约接管后重复执行,并为人工复核保留可操作的结算记录。 将生产提交绑定到当前估价签名,统一复用预处理快照,并补强规则形状、定点溢出与历史规则兼容校验。 已通过 PostgreSQL 16 集成测试、Go 全量测试与静态检查、前端测试与构建、OpenAPI、依赖审计、镜像、迁移、流水线和 SemVer 门禁。
This commit is contained in:
@@ -66,6 +66,7 @@ type MediaEstimateState = {
|
||||
error?: string;
|
||||
pricingVersion?: string;
|
||||
resolver?: string;
|
||||
signature?: string;
|
||||
status: 'idle' | 'loading' | 'ready' | 'free' | 'unavailable' | 'error';
|
||||
};
|
||||
|
||||
@@ -192,18 +193,19 @@ export function PlaygroundPage(props: {
|
||||
const sequence = mediaEstimateSequenceRef.current + 1;
|
||||
mediaEstimateSequenceRef.current = sequence;
|
||||
const controller = new AbortController();
|
||||
setMediaEstimate((current) => ({ ...current, error: '', status: 'loading' }));
|
||||
setMediaEstimate((current) => ({ ...current, error: '', signature: mediaEstimateSignature, status: 'loading' }));
|
||||
const timer = window.setTimeout(() => {
|
||||
estimatePricing(credential, mediaEstimatePayload, controller.signal)
|
||||
.then((estimate) => {
|
||||
if (controller.signal.aborted || sequence !== mediaEstimateSequenceRef.current) return;
|
||||
setMediaEstimate(mediaEstimateFromResponse(estimate));
|
||||
setMediaEstimate({ ...mediaEstimateFromResponse(estimate), signature: mediaEstimateSignature });
|
||||
})
|
||||
.catch((err) => {
|
||||
if (controller.signal.aborted || sequence !== mediaEstimateSequenceRef.current) return;
|
||||
const unavailable = err instanceof GatewayApiError && err.details.code === 'pricing_unavailable';
|
||||
setMediaEstimate({
|
||||
error: err instanceof Error ? err.message : unavailable ? '价格不可用' : '预计扣费计算失败',
|
||||
signature: mediaEstimateSignature,
|
||||
status: unavailable ? 'unavailable' : 'error',
|
||||
});
|
||||
});
|
||||
@@ -326,17 +328,34 @@ export function PlaygroundPage(props: {
|
||||
setMediaMessage(runMode === 'video' ? '请输入视频提示词。' : '请输入图片提示词。');
|
||||
return;
|
||||
}
|
||||
if (!overrides && !mediaEstimateAllowsProduction(mediaEstimate)) {
|
||||
const runUploads = sanitizeMediaRunUploads(overrides?.uploads ?? mediaUploads);
|
||||
const runVideoMode = overrides?.videoMode ?? videoMode;
|
||||
const runModelOption = modelOptions.find((item) => item.value === runModel);
|
||||
const runEstimatePayload = buildMediaEstimatePayload(runMode, runModel, trimmedPrompt, runSettings, runUploads, runVideoMode, {
|
||||
supportsQualityControl: runModelOption
|
||||
? deriveMediaModelCapabilities(runModelOption.models, runMode, runVideoMode, runSettings.resolution).supportsQualityControl
|
||||
: mediaCapabilities?.supportsQualityControl,
|
||||
});
|
||||
const runEstimateSignature = billingEstimateSignature(runEstimatePayload);
|
||||
if (!overrides && !mediaEstimateAllowsProduction(mediaEstimate, runEstimateSignature)) {
|
||||
setMediaMessage(mediaEstimate.status === 'unavailable'
|
||||
? '当前参数没有可用价格,已阻止生产生成。'
|
||||
: '请等待当前参数的预计费用计算完成后再生成。');
|
||||
return;
|
||||
}
|
||||
if (overrides) {
|
||||
try {
|
||||
await estimatePricing(credential, runEstimatePayload);
|
||||
} catch (error) {
|
||||
const unavailable = error instanceof GatewayApiError && error.details.code === 'pricing_unavailable';
|
||||
setMediaMessage(unavailable
|
||||
? '当前参数没有可用价格,已阻止生产生成。'
|
||||
: `预计扣费计算失败,未提交生产任务:${error instanceof Error ? error.message : '未知错误'}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const localId = newLocalId();
|
||||
const runUploads = sanitizeMediaRunUploads(overrides?.uploads ?? mediaUploads);
|
||||
const runVideoMode = overrides?.videoMode ?? videoMode;
|
||||
const runModelOption = modelOptions.find((item) => item.value === runModel);
|
||||
const modelLabel = runModelOption?.label ?? runModel;
|
||||
const run: MediaGenerationRun = {
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -475,7 +494,7 @@ export function PlaygroundPage(props: {
|
||||
imageHasReference={effectiveImageHasReference}
|
||||
mediaSettings={mediaSettings}
|
||||
mediaEstimate={mediaEstimate}
|
||||
submitDisabled={!mediaEstimateAllowsProduction(mediaEstimate)}
|
||||
submitDisabled={!mediaEstimateAllowsProduction(mediaEstimate, mediaEstimateSignature)}
|
||||
mediaCapabilities={mediaCapabilities}
|
||||
uploadAccept={mediaUploadAcceptValue}
|
||||
uploadMessage={mediaUploadMessage}
|
||||
@@ -901,8 +920,8 @@ function mediaEstimateAriaLabel(estimate: MediaEstimateState) {
|
||||
return `预计扣费 ${formatEstimateAmount(estimate.amount)} ${estimate.currency || 'resource'}`;
|
||||
}
|
||||
|
||||
function mediaEstimateAllowsProduction(estimate: MediaEstimateState) {
|
||||
return estimate.status === 'ready' || estimate.status === 'free';
|
||||
export function mediaEstimateAllowsProduction(estimate: MediaEstimateState, signature: string) {
|
||||
return estimate.signature === signature && (estimate.status === 'ready' || estimate.status === 'free');
|
||||
}
|
||||
|
||||
export function billingEstimateSignature(payload: Record<string, unknown> | null) {
|
||||
|
||||
Reference in New Issue
Block a user