feat(billing): resolve chat and rectification pricing by feature

This commit is contained in:
Jesse_Chen
2026-08-31 04:29:45 +08:00
parent 8edae211ff
commit 6e13949d3b
3 changed files with 16 additions and 5 deletions
+3 -1
View File
@@ -29,6 +29,7 @@ import {
shouldLoadGeneralDailyPanchanga,
} from "@/lib/consultation-entrypoint";
import { CreditRpcError } from "@/lib/consultation-billing";
import { resolveFeaturePricing } from "@/lib/feature-pricing";
import { reserveConsultationModel } from "@/lib/consultation-model-selection";
import { resolveSessionLanguageModel } from "@/lib/model-catalog";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
@@ -361,12 +362,13 @@ export async function POST(request: Request) {
chatSession.model_id,
(modelId) => sessionModel?.id === modelId ? sessionModel : null,
async (model) => {
const pricing = await resolveFeaturePricing(accounting, "chat.standard", model.id);
const { data, error } = await accounting.rpc("reserve_consultation_usage", {
p_user_id: userId,
p_request_id: requestId,
p_session_id: sessionId,
p_requested_model_id: model.id,
p_credit_cost: model.creditCost,
p_credit_cost: pricing.credit_cost,
});
const reservation = consultationReservationSchema.safeParse(first(data ?? []));
if (error || !reservation.success) {
@@ -24,6 +24,7 @@ import { safePublicEvent } from "@/lib/rectification-agentic/v9/stream-mapping";
import { RECTIFICATION_SKILL_NAME, RECTIFICATION_SKILL_VERSION } from "@/lib/rectification-agentic/v9/case-status";
import { blocksPromptExtraction } from "@/lib/consult-safety";
import { authorizeUsage, completeUsage, releaseUsage } from "@/lib/consultation-billing";
import { resolveFeaturePricing } from "@/lib/feature-pricing";
import { loadRuntimeFeatureFlags } from "@/lib/feature-flags";
import { isProductEnabled } from "@/lib/product-access";
import { resolveSessionLanguageModel } from "@/lib/model-catalog";
@@ -564,12 +565,13 @@ export async function POST(request: Request) {
}
try {
const billingRequestId = await rectificationBillingRequestId(accounting, userId, caseId);
const pricing = await resolveFeaturePricing(accounting, "rectification", selectedModel.id);
const result = await authorizeUsage(accounting, {
userId,
requestId: billingRequestId,
featureKey: "rectification",
requestedModelId: selectedModel.id,
creditCost: selectedModel.creditCost,
creditCost: pricing.credit_cost,
});
if (result.success) return { success: true, status: 200 };
return {
@@ -719,8 +721,16 @@ export async function POST(request: Request) {
} catch (error) {
const code = error instanceof RectificationToolServiceError ? error.code : "run_failed";
console.error(`[rectification-v9] run failed case=${caseId} code=${code}`);
if (code === "billing_denied" || code.includes("insufficient")) {
send({ type: "error", code: "billing_denied", message: "咨询点数不足,请先兑换后再继续。" });
if (code === "insufficient_credits" || code.includes("insufficient")) {
send({ type: "error", code: "insufficient_credits", message: "校正点数不足,请先兑换后再继续。" });
} else if (code === "fair_use_minute") {
send({ type: "error", code, message: "请求频率达到安全上限,请稍后再试。" });
} else if (code === "fair_use_day") {
send({ type: "error", code, message: "今日请求频率达到安全上限,请明天再试。" });
} else if (code === "fair_use_billing_period") {
send({ type: "error", code, message: "当前账户触发安全熔断,请稍后再试或联系支持。" });
} else if (code === "billing_denied") {
send({ type: "error", code, message: "暂时无法确认校正点数,请稍后重试。" });
} else if (code.includes("legacy_skill_identity_unverifiable")) {
send({ type: "error", code: "skill_identity_unverifiable", message: "该校正绑定的是无法核验的历史 Skill,请先采用当前注册版本。" });
} else if (code.includes("skill_identity_missing")) {
@@ -34,7 +34,6 @@ export function FeaturePricingManagement() {
useEffect(() => {
let active = true;
setIsLoading(true);
adminRequestJson<{ data: Row[] }>("/api/admin/feature-pricing")
.then((payload) => { if (active) setRows(payload.data); })
.finally(() => { if (active) setIsLoading(false); });