import "server-only"; import { resolveSessionLanguageModel } from "./model-catalog"; import { buildReplyQualitySnapshot } from "./reply-quality-snapshot"; /** The narrow part of the service client this module uses. */ export type ReplyQualityService = { from(table: string): { select(columns: string): { eq(column: string, value: unknown): { eq(column: string, value: unknown): { maybeSingle(): PromiseLike<{ data: unknown; error: unknown }> }; }; }; }; rpc(name: string, args: Record): PromiseLike<{ data: unknown; error: { message?: string } | null }>; }; function record(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : null; } export type ReplyQualityCaptureResult = "saved" | "skipped" | "failed"; /** * Keeps the 故障上下文快照 for a 👎 (compliance round 2026-09-30): reads the * stored session, rebuilds the turn and its context, adds the run facts the * usage ledger already has for that request (model, tokens, duration), and * saves through save_reply_quality_snapshot(), which refuses unless the reply * is currently rated 👎. */ export async function captureReplyQualitySnapshot( service: ReplyQualityService, input: Readonly<{ userId: string; sessionId: string; messageIndex: number; answerSha256: string }>, ): Promise { const { data: sessionRow, error: sessionError } = await service .from("chat_sessions") .select("id,model_id,model_config_version,session_type,messages,context_summary") .eq("id", input.sessionId) .eq("user_id", input.userId) .maybeSingle(); const session = record(sessionRow); if (sessionError || !session) return "skipped"; const sessionType = session.session_type === "birth_time_rectification" ? "birth_time_rectification" : "consultation"; const modelId = typeof session.model_id === "string" ? session.model_id : null; let contextWindow: number | null = null; if (modelId) { const model = await resolveSessionLanguageModel( modelId, typeof session.model_config_version === "number" ? session.model_config_version : null, ).catch(() => null); contextWindow = typeof model?.contextWindow === "number" ? model.contextWindow : null; } const built = buildReplyQualitySnapshot({ messages: session.messages, messageIndex: input.messageIndex, answerSha256: input.answerSha256, sessionType, contextSummary: session.context_summary, contextWindow, }); if (!built.ok) return "skipped"; let runFacts: Record = { contextWindow }; if (built.snapshot.requestId) { const { data: ledger } = await service .from("usage_ledger") .select("actual_model_id,requested_model_id,input_tokens,output_tokens,duration_ms,feature_key") .eq("user_id", input.userId) .eq("request_id", built.snapshot.requestId) .maybeSingle(); const row = record(ledger); if (row) { runFacts = { ...runFacts, actualModelId: row.actual_model_id ?? null, requestedModelId: row.requested_model_id ?? null, inputTokens: row.input_tokens ?? null, outputTokens: row.output_tokens ?? null, durationMs: row.duration_ms ?? null, featureKey: row.feature_key ?? null, }; } } const { error } = await service.rpc("save_reply_quality_snapshot", { p_user_id: input.userId, p_session_id: input.sessionId, p_message_index: input.messageIndex, p_session_type: sessionType, p_model_id: typeof runFacts.actualModelId === "string" ? runFacts.actualModelId : modelId, p_request_id: built.snapshot.requestId, p_question: built.snapshot.question, p_answer: built.snapshot.answer, p_context: built.snapshot.context, p_run_facts: runFacts, }); return error ? "failed" : "saved"; }