feat: enforce commercial rectification evidence contracts

* feat: enforce precise timing output contract

* fix: recognize package imports in fragment audit

* test: make workflow stream contract formatting-independent

* fix: preserve VedAstro evidence across async workflows

* feat: enforce commercial technique truth contract

* feat: add rectification technique receipt

* feat: extend rectification event evidence

* feat: score rectification arudha evidence

* feat: gate high rigor rectification confirmation

* feat: add controlled transit to rectification

* feat: include d11 in rectification finance scoring

* feat: add ashtakavarga rectification auxiliary

* feat: show rectification technique receipt

* feat: use verified shadbala components in rectification

* fix: trace transitive script references in fragment audit

* feat: run request-level rectification parity packet
This commit is contained in:
732642856
2026-07-19 22:23:29 +08:00
committed by GitHub
parent d9b07802a5
commit 4ceb3a5157
42 changed files with 995 additions and 85 deletions
+83 -21
View File
@@ -19,6 +19,7 @@ import { reserveConsultationModel } from "@/lib/consultation-model-selection";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { streamTextResponse } from "@/lib/stream-text-response";
import { guardPreciseTimingOutput } from "@/lib/timing-output-guard";
import { z } from "zod";
export const runtime = "nodejs";
@@ -29,10 +30,15 @@ const chatRequestSchema = consultationInputSchema.extend({
modelId: z.string().trim().min(1).max(64),
entrypoint: consultationEntrypointSchema.optional(),
name: z.string().trim().max(80).optional().default(""),
history: z.array(z.object({
role: z.enum(["user", "assistant"]),
text: z.string().max(4000),
})).max(20).default([]),
history: z
.array(
z.object({
role: z.enum(["user", "assistant"]),
text: z.string().max(4000),
}),
)
.max(20)
.default([]),
});
function currentTimeContext(now = new Date()) {
@@ -67,10 +73,15 @@ async function recordModelUsage(
.eq("transaction_type", "reserve")
.eq("request_id", requestId);
if (error) console.warn(`[billing] unable to record model usage request=${requestId} model=${modelId}`);
if (error)
console.warn(
`[billing] unable to record model usage request=${requestId} model=${modelId}`,
);
} catch (error) {
const reason = error instanceof Error ? error.name : "UnknownError";
console.warn(`[billing] unable to read model usage request=${requestId} model=${modelId} reason=${reason}`);
console.warn(
`[billing] unable to read model usage request=${requestId} model=${modelId} reason=${reason}`,
);
}
}
@@ -87,7 +98,10 @@ export async function POST(request: Request) {
);
}
const { data: { user }, error: authError } = await supabase.auth.getUser();
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json(
{ error: "请先登录", message: "登录后才能开始咨询。" },
@@ -95,7 +109,9 @@ export async function POST(request: Request) {
);
}
const parsed = chatRequestSchema.safeParse(await request.json().catch(() => null));
const parsed = chatRequestSchema.safeParse(
await request.json().catch(() => null),
);
if (!parsed.success) {
return NextResponse.json(
{ error: "出生资料或问题格式不正确", details: parsed.error.flatten() },
@@ -111,7 +127,11 @@ export async function POST(request: Request) {
].join("\n");
if (blocksPromptExtraction(userControlledPrompt)) {
return NextResponse.json(
{ error: "无法处理该请求", message: "我不能提供系统提示词、技能原文或任何密钥。你可以继续询问占星相关问题。" },
{
error: "无法处理该请求",
message:
"我不能提供系统提示词、技能原文或任何密钥。你可以继续询问占星相关问题。",
},
{ status: 400 },
);
}
@@ -130,11 +150,19 @@ export async function POST(request: Request) {
modelSelection = await reserveConsultationModel(
parsed.data.modelId,
resolveLanguageModel,
() => runCreditRpc(accounting, "begin_consultation_credit", userId, requestId),
() =>
runCreditRpc(
accounting,
"begin_consultation_credit",
userId,
requestId,
),
);
} catch (error) {
const reason = error instanceof Error ? error.name : "UnknownError";
console.error(`[billing] reservation failed request=${requestId} reason=${reason}`);
console.error(
`[billing] reservation failed request=${requestId} reason=${reason}`,
);
return NextResponse.json(
{ error: "暂时无法确认咨询点数", message: "请稍后重试。" },
{ status: 503 },
@@ -143,7 +171,10 @@ export async function POST(request: Request) {
if (modelSelection.status === "unavailable") {
return NextResponse.json(
{ error: "模型暂不可用", message: "请选择其他模型后重新发送,本次不会扣除点数。" },
{
error: "模型暂不可用",
message: "请选择其他模型后重新发送,本次不会扣除点数。",
},
{ status: 409 },
);
}
@@ -156,7 +187,9 @@ export async function POST(request: Request) {
return NextResponse.json(
{
error: insufficient ? "咨询点数不足" : "暂时无法扣除咨询点数",
message: insufficient ? "请先兑换咨询点数后再继续。" : reserveResult.error_code || "请稍后重试。",
message: insufficient
? "请先兑换咨询点数后再继续。"
: reserveResult.error_code || "请稍后重试。",
},
{ status: insufficient ? 402 : 503 },
);
@@ -164,16 +197,29 @@ export async function POST(request: Request) {
async function cancel() {
try {
await runCreditRpc(accounting, "cancel_consultation_credit", userId, requestId);
await runCreditRpc(
accounting,
"cancel_consultation_credit",
userId,
requestId,
);
} catch (error) {
const reason = error instanceof Error ? error.name : "UnknownError";
console.error(`[billing] cancellation failed request=${requestId} reason=${reason}`);
console.error(
`[billing] cancellation failed request=${requestId} reason=${reason}`,
);
}
}
async function complete() {
const result = await runCreditRpc(accounting, "complete_consultation_credit", userId, requestId);
if (!result.success) throw new CreditRpcError(result.error_code || "completion_rejected");
const result = await runCreditRpc(
accounting,
"complete_consultation_credit",
userId,
requestId,
);
if (!result.success)
throw new CreditRpcError(result.error_code || "completion_rejected");
}
let settlement: Promise<void> | null = null;
@@ -208,15 +254,27 @@ export async function POST(request: Request) {
]);
const completeAndRecordUsage = async () => {
await complete();
void recordModelUsage(accounting, userId, requestId, modelSelection.usageModelId, result.totalUsage);
void recordModelUsage(
accounting,
userId,
requestId,
modelSelection.usageModelId,
result.totalUsage,
);
};
const settleInterrupted = (emitted: boolean) => settle(emitted ? completeAndRecordUsage : cancel);
const settleInterrupted = (emitted: boolean) =>
settle(emitted ? completeAndRecordUsage : cancel);
return streamTextResponse(result.textStream, {
transformText:
workflowReceipt.preciseTiming === "blocked"
? guardPreciseTimingOutput
: undefined,
mode: "mastra",
requestId,
headers: {
"x-jyotish-workflow-route": workflowReceipt.route,
"x-jyotish-workflow-status": workflowReceipt.status,
"x-jyotish-technique-truth": workflowReceipt.techniqueTruth,
"x-jyotish-precise-timing": workflowReceipt.preciseTiming,
"x-jyotish-missing-layers": workflowReceipt.missingLayers,
},
@@ -227,12 +285,16 @@ export async function POST(request: Request) {
} catch (error) {
await cancel();
const reason = error instanceof Error ? error.name : "UnknownError";
console.error(`[consult] generation failed request=${requestId} model=${modelSelection.usageModelId} reason=${reason}`);
console.error(
`[consult] generation failed request=${requestId} model=${modelSelection.usageModelId} reason=${reason}`,
);
return NextResponse.json(
{
error: "暂时无法生成解读",
message: "咨询服务暂时不可用,请稍后再试。",
recovery: languageModelConfigurationMessage() ? "当前没有可用的咨询模型,请联系管理员。" : "稍后重试,或换一个模型继续。",
recovery: languageModelConfigurationMessage()
? "当前没有可用的咨询模型,请联系管理员。"
: "稍后重试,或换一个模型继续。",
},
{ status: 503 },
);
+1
View File
@@ -475,6 +475,7 @@ button:disabled { cursor: default; opacity: .45; }
.message-bubble { overflow: hidden; border: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-lg); background: var(--color-canvas-muted); }
.message-assistant .message-bubble { border-radius: 0; background: transparent; padding: var(--space-3) 0; }
.message p, .message-markdown { color: var(--color-ink-strong); font-size: var(--type-body-md); line-height: 1.65; text-wrap: pretty; word-break: auto-phrase; }
.message-evidence-status { margin: var(--space-3) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--color-border); color: var(--color-ink-muted); font-size: var(--type-body-sm); line-height: 1.5; }
.message-user p { line-height: 1.55; color: var(--color-ink); font-size: var(--type-body-sm); }
.message-markdown h2, .message-markdown h3 { margin: 24px 0 10px; color: var(--color-ink); font-family: var(--font-display); font-weight: 400; letter-spacing: -.3px; }
.message-markdown h2 { font-size: var(--type-display-sm); }
+3 -1
View File
@@ -2119,6 +2119,7 @@ export default function Home() {
throw new Error(payloadMessage(errorPayload, "服务暂时不可用"));
}
if (!response.body) throw new Error("浏览器未收到可读取的回答流");
const techniqueTruth = response.headers.get("x-jyotish-technique-truth") ?? "unknown";
const reader = response.body.getReader();
const decoder = new TextDecoder();
@@ -2147,7 +2148,7 @@ export default function Home() {
const completedSession: ChatSession = {
...userSession,
title: currentSession.messages.length === 0 && reply.title ? reply.title : userSession.title,
messages: [...userSession.messages, { role: "assistant", text: reply.text, suggestions: reply.suggestions }],
messages: [...userSession.messages, { role: "assistant", text: reply.text, suggestions: reply.suggestions, techniqueTruth }],
updatedAt: timestamp(),
};
updateSession(sessionId, () => completedSession);
@@ -2511,6 +2512,7 @@ export default function Home() {
{chatMessageViews(activeSession.messages, isLoading, activeStreamingText).map((message) => (
<ChatMessageRow key={message.renderKey} message={message} />
))}
))}
{activeError && <p className="error-message">{activeError}</p>}
<div ref={conversationEnd} />
</div>