59559d4b24
Thinking disappeared on failure and never reached session storage. Keep the sanitized chain on disk and on errors, and regroup the sidebar around reports, charts, favorites, and dated history titles. Co-authored-by: Cursor <cursoragent@cursor.com>
1120 lines
44 KiB
TypeScript
1120 lines
44 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import {
|
|
consultationInputSchema,
|
|
consultationWorkflowReceipt,
|
|
getGeneralJyotishAgent,
|
|
getJyotishAgent,
|
|
getLegacyJyotishAgent,
|
|
getWindowJyotishAgent,
|
|
runConsultationWorkflow,
|
|
} from "@/mastra";
|
|
import { blocksPromptExtraction } from "@/lib/consult-safety";
|
|
import { consultationDomainSchema } from "@/lib/consultation-domain-registry";
|
|
import { parseAgentReply } from "@/lib/agent-reply";
|
|
import { createConsultationReplyMetadata } from "@/lib/consultation-reply-metadata";
|
|
import {
|
|
ConsultationPlanValidationError,
|
|
createConsultationPlan,
|
|
type ConsultationPlan,
|
|
} from "@/lib/consultation-plan";
|
|
import {
|
|
logAgentObservability,
|
|
settlementTelemetryOutcome,
|
|
toAgentObservabilityErrorCode,
|
|
type AgentSettlementResult,
|
|
} from "@/lib/agent-observability";
|
|
import {
|
|
consultationEntrypointSchema,
|
|
resolveConsultationQuestion,
|
|
shouldLoadGeneralDailyPanchanga,
|
|
} from "@/lib/consultation-entrypoint";
|
|
import { CreditRpcError } from "@/lib/consultation-billing";
|
|
import { reserveConsultationModel } from "@/lib/consultation-model-selection";
|
|
import { resolveSessionLanguageModel } from "@/lib/model-catalog";
|
|
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
|
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
|
import { streamTextResponse } from "@/lib/stream-text-response";
|
|
import { streamAgentResponse } from "@/lib/stream-agent-response";
|
|
import type { AgentExecutionReceipt, WorkflowReceipt } from "@/lib/consultation-agent-events";
|
|
import {
|
|
AGENT_MAX_STEPS,
|
|
AGENT_TIMEOUT_MS,
|
|
consultationGenerationSettings,
|
|
createConsultationAgentContext,
|
|
createWindowConsultationAgentContext,
|
|
consultationModelStepTelemetry,
|
|
consultationStepBudgetReceipt,
|
|
createConsultationRuntimeHooks,
|
|
createConsultationRuntimeState,
|
|
publicConsultationRuntimeSteps,
|
|
} from "@/mastra/consultation-tools";
|
|
import {
|
|
applyBirthTimeModeToWorkflowContext,
|
|
consultationBirthTimeModeSchema,
|
|
createBirthTimeModeOutputGuard,
|
|
shouldRunBirthChartWorkflow,
|
|
shouldRunDeclaredWindowWorkflow,
|
|
type ConsultationBirthTimeMode,
|
|
} from "@/lib/consultation-birth-time-mode";
|
|
import {
|
|
ConsultationProfileTruthError,
|
|
prepareConsultationRoute,
|
|
type PreparedConsultationRoute,
|
|
} from "@/lib/consultation-route-service";
|
|
import {
|
|
loadGeneralDailyPanchangaContext,
|
|
type GeneralDailyPanchangaContext,
|
|
} from "@/lib/general-daily-panchanga";
|
|
import { z } from "zod";
|
|
|
|
export const runtime = "nodejs";
|
|
export const maxDuration = 120;
|
|
|
|
// The step budget, the wall-clock budget and the domain cap all bound this same
|
|
// run, so they are declared as one group in @/mastra/consultation-tools with the
|
|
// reasoning that ties them together. maxDuration above is the ceiling they must
|
|
// stay under; raising it here without raising that is meaningless.
|
|
|
|
const chatRequestMetadataSchema = z.object({
|
|
requestId: z.string().uuid(),
|
|
sessionId: z.string().uuid(),
|
|
modelId: z.string().trim().min(1).max(64),
|
|
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([]),
|
|
});
|
|
|
|
const chartChatRequestSchema = consultationInputSchema.extend({
|
|
...chatRequestMetadataSchema.shape,
|
|
consultationMode: consultationBirthTimeModeSchema.exclude(["general_no_birth_time", "declared_birth_window"])
|
|
.optional()
|
|
.default("verified_chart"),
|
|
entrypoint: consultationEntrypointSchema.optional(),
|
|
}).strict();
|
|
|
|
const generalChatRequestSchema = z.object({
|
|
...chatRequestMetadataSchema.shape,
|
|
consultationMode: z.literal("general_no_birth_time"),
|
|
question: z.string().trim().min(1).max(500),
|
|
theme: consultationDomainSchema,
|
|
entrypoint: z.literal("daily_starlanguage").optional(),
|
|
}).strict();
|
|
|
|
const windowChatRequestSchema = z.object({
|
|
...chatRequestMetadataSchema.shape,
|
|
consultationMode: z.literal("declared_birth_window"),
|
|
question: z.string().trim().min(1).max(500),
|
|
theme: consultationDomainSchema,
|
|
entrypoint: z.literal("daily_starlanguage").optional(),
|
|
}).strict();
|
|
|
|
const chatRequestSchema = z.union([
|
|
generalChatRequestSchema,
|
|
windowChatRequestSchema,
|
|
chartChatRequestSchema,
|
|
]);
|
|
|
|
const consultationReservationSchema = z.object({
|
|
success: z.boolean(),
|
|
reservation_id: z.string().uuid().nullable(),
|
|
source: z.enum(["subscription", "credits"]).nullable(),
|
|
credits: z.number().int().nullable(),
|
|
subscription_id: z.string().uuid().nullable(),
|
|
reason: z.string().nullable(),
|
|
retry_after_seconds: z.number().int().nullable(),
|
|
});
|
|
|
|
const consultationCompletionSchema = z.object({
|
|
success: z.boolean(),
|
|
credits: z.number().int().nullable(),
|
|
error_code: z.string().nullable().optional(),
|
|
});
|
|
|
|
function first<T>(value: T | T[]): T {
|
|
return Array.isArray(value) ? value[0] : value;
|
|
}
|
|
|
|
const detachedSettlementAttempts = 3;
|
|
// ponytail: Staging MVP ceiling—without a queue/worker, detached settlement gets only three short retries.
|
|
async function retryDetachedSettlement<T>(action: () => Promise<T>): Promise<T> {
|
|
let lastError: unknown;
|
|
for (let attempt = 1; attempt <= detachedSettlementAttempts; attempt += 1) {
|
|
try {
|
|
return await action();
|
|
} catch (error) {
|
|
lastError = error;
|
|
if (attempt < detachedSettlementAttempts) {
|
|
await new Promise((resolve) => setTimeout(resolve, attempt * 150));
|
|
}
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
function currentTimeContext(now = new Date()) {
|
|
const chinaTime = new Date(now.getTime() + 8 * 60 * 60 * 1000)
|
|
.toISOString()
|
|
.replace("T", " ")
|
|
.slice(0, 19);
|
|
return `服务端当前时间(权威):${now.toISOString()};中国标准时间(UTC+8):${chinaTime}。涉及“现在、今天、今年、未来几个月”等相对时间时,以此为准。`;
|
|
}
|
|
|
|
function generalDailyContextPrompt(context: GeneralDailyPanchangaContext | null) {
|
|
if (!context) return "";
|
|
return [
|
|
"以下是服务器计算并校验结构后的公共日历证据。只能在其边界内解释,不得补充个人命盘结论。",
|
|
"<public-daily-panchanga>",
|
|
JSON.stringify(context),
|
|
"</public-daily-panchanga>",
|
|
].join("\n");
|
|
}
|
|
|
|
function generalNoMinuteInstruction(hasPublicDaily: boolean) {
|
|
return hasPublicDaily
|
|
? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
|
|
: "当前没有具体出生分钟。不得计算或推断个人星盘,也不得补 00:00、时段中点或任何候选分钟。请直接回答用户的问题:能答的部分照实说,需要出生分钟的部分明确标出限制。不要把整轮对话改成「一般知识或生时校正」二选一,也不要把服务端当前时间当成用户提供的出生时间。";
|
|
}
|
|
|
|
function declaredWindowInstruction() {
|
|
return "当前是声明出生窗口咨询,没有单一出生分钟。如需个人结构结论,必须调用 run-jyotish-window-consultation。探针时刻不是出生时间,不得把 00:00、时段中点、中午或任一探针写成出生分钟。只能根据稳定层作方向性回答;上升或宫位若在窗口内变化,列出可能星座,不得写成「你的上升是 X」。精确应期一律不可用。";
|
|
}
|
|
|
|
function usesPublicDailyGeneralAgent(
|
|
consultationMode: ConsultationBirthTimeMode,
|
|
generalDailyContext: GeneralDailyPanchangaContext | null,
|
|
) {
|
|
return consultationMode === "general_no_birth_time"
|
|
|| (consultationMode === "declared_birth_window" && Boolean(generalDailyContext));
|
|
}
|
|
|
|
function chinaCalendarDate(now: Date) {
|
|
return new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
}
|
|
|
|
type Usage = { inputTokens?: number; outputTokens?: number };
|
|
|
|
function mergeUsage(usages: Promise<Usage>[]): Promise<Usage> {
|
|
return Promise.all(usages).then((items) => items.reduce((total, item) => ({
|
|
inputTokens: (total.inputTokens ?? 0) + (item.inputTokens ?? 0),
|
|
outputTokens: (total.outputTokens ?? 0) + (item.outputTokens ?? 0),
|
|
}), {} as Usage));
|
|
}
|
|
|
|
function shouldUseAgenticRuntime(user: { id: string; app_metadata?: Record<string, unknown> }) {
|
|
const mode = process.env.CONSULTATION_AGENTIC_RUNTIME?.trim().toLowerCase() ?? "enabled";
|
|
if (mode === "legacy") return false;
|
|
if (mode === "enabled") return true;
|
|
if (mode !== "canary") return true;
|
|
const ids = new Set((process.env.CONSULTATION_AGENTIC_CANARY_USER_IDS ?? "")
|
|
.split(",").map((value) => value.trim()).filter(Boolean));
|
|
const roles = Array.isArray(user.app_metadata?.roles) ? user.app_metadata.roles : [];
|
|
return ids.has(user.id) || user.app_metadata?.role === "admin" || roles.includes("admin");
|
|
}
|
|
|
|
function workflowStatus(status: string | undefined): "ready" | "degraded" | "blocked" {
|
|
return status === "ready" || status === "degraded" ? status : "blocked";
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
let supabase: Awaited<ReturnType<typeof createServerSupabaseClient>>;
|
|
let accounting: ReturnType<typeof createAdminSupabaseClient>;
|
|
try {
|
|
supabase = await createServerSupabaseClient();
|
|
accounting = createAdminSupabaseClient();
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
|
|
const {
|
|
data: { user },
|
|
error: authError,
|
|
} = await supabase.auth.getUser();
|
|
if (authError || !user) {
|
|
return NextResponse.json(
|
|
{ error: "请先登录", message: "登录后才能开始咨询。" },
|
|
{ status: 401 },
|
|
);
|
|
}
|
|
|
|
const parsed = chatRequestSchema.safeParse(
|
|
await request.json().catch(() => null),
|
|
);
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{ error: "出生资料或问题格式不正确", details: parsed.error.flatten() },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const { data: chatSession, error: chatSessionError } = await supabase
|
|
.from("chat_sessions")
|
|
.select("id,model_id,model_config_version,session_type")
|
|
.eq("id", parsed.data.sessionId)
|
|
.eq("user_id", user.id)
|
|
.maybeSingle();
|
|
if (chatSessionError) {
|
|
return NextResponse.json(
|
|
{ error: "暂时无法读取咨询会话", message: "请稍后重试。" },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
if (!chatSession || chatSession.session_type !== "consultation") {
|
|
return NextResponse.json(
|
|
{ error: "咨询会话不存在", message: "请重新进入咨询。" },
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
if (!chatSession.model_id || chatSession.model_id !== parsed.data.modelId) {
|
|
return NextResponse.json(
|
|
{ error: "会话模型已经变化", message: "请刷新会话后重新发送,本次不会扣点。" },
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
const sessionModel = await resolveSessionLanguageModel(
|
|
chatSession.model_id,
|
|
chatSession.model_config_version,
|
|
);
|
|
|
|
if (!sessionModel) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "模型暂不可用",
|
|
message: "当前会话绑定的数据库模型配置不可用,请联系管理员,本次不会扣点。",
|
|
},
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
|
|
if (parsed.data.entrypoint === "birth_time_rectification") {
|
|
return NextResponse.json(
|
|
{
|
|
error: "旧版生时校正入口已停用",
|
|
message: "请从首页生时校正卡片开始或继续对话式校正,本次不会扣点。",
|
|
},
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
|
|
const requestTime = new Date();
|
|
const currentDate = chinaCalendarDate(requestTime);
|
|
|
|
const userId = user.id;
|
|
const requestId = parsed.data.requestId;
|
|
const sessionId = parsed.data.sessionId;
|
|
const consultationTheme = parsed.data.theme;
|
|
const visibleQuestion = parsed.data.question;
|
|
|
|
const userControlledPrompt = [
|
|
parsed.data.question,
|
|
...parsed.data.history
|
|
.filter((message) => message.role === "user")
|
|
.map((message) => message.text),
|
|
].join("\n");
|
|
if (blocksPromptExtraction(userControlledPrompt)) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "无法处理该请求",
|
|
message:
|
|
"我不能提供系统提示词、技能原文或任何密钥。你可以继续询问占星相关问题。",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
type SelectedModel = NonNullable<Awaited<ReturnType<typeof resolveSessionLanguageModel>>>;
|
|
type ReservationResult = { success: boolean; credits: number | null; error_code: string | null };
|
|
type ModelSelection = Awaited<ReturnType<typeof reserveConsultationModel<SelectedModel, ReservationResult>>>;
|
|
let prepared: PreparedConsultationRoute<ModelSelection, ConsultationPlan>;
|
|
try {
|
|
prepared = await prepareConsultationRoute<ModelSelection, ConsultationPlan>({
|
|
userId,
|
|
mode: parsed.data.consultationMode,
|
|
async loadProfile(profileUserId) {
|
|
const { data, error } = await supabase
|
|
.from("profiles")
|
|
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source")
|
|
.eq("id", profileUserId)
|
|
.single();
|
|
if (error || !data) throw new ConsultationProfileTruthError("profile_unavailable");
|
|
return data;
|
|
},
|
|
beforeReserve: ({ consultationMode }) => createConsultationPlan({
|
|
userIntent: parsed.data.question,
|
|
theme: consultationTheme,
|
|
consultationMode,
|
|
modelCreditCost: sessionModel.creditCost,
|
|
}),
|
|
reserve: () => reserveConsultationModel(
|
|
chatSession.model_id,
|
|
(modelId) => sessionModel?.id === modelId ? sessionModel : null,
|
|
async (model) => {
|
|
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,
|
|
});
|
|
const reservation = consultationReservationSchema.safeParse(first(data ?? []));
|
|
if (error || !reservation.success) {
|
|
throw new CreditRpcError(error?.message || "invalid_reservation_response");
|
|
}
|
|
return {
|
|
success: reservation.data.success,
|
|
credits: reservation.data.credits,
|
|
error_code: reservation.data.reason,
|
|
};
|
|
},
|
|
),
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ConsultationPlanValidationError) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "当前咨询计划不可用",
|
|
message: error.code === "plan_cost_ceiling_exceeded"
|
|
? "当前会话模型超出本次咨询的点数上限,请选择标准模型后重试,本次不会扣点。"
|
|
: "当前咨询模式与回答精度边界不一致,请刷新后重试,本次不会扣点。",
|
|
},
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
if (error instanceof ConsultationProfileTruthError) {
|
|
const modeChanged = error.code === "mode_changed";
|
|
return NextResponse.json(
|
|
modeChanged
|
|
? {
|
|
error: "出生时间状态已经变化",
|
|
message: "请刷新后重新选择使用填报时间、一般咨询或先完成校正,本次不会扣点。",
|
|
}
|
|
: {
|
|
error: "暂时无法核对完整出生资料",
|
|
message: "出生日期、时间来源或出生地点资料不完整或不一致,请重新保存后再试,本次不会扣点。",
|
|
},
|
|
{ status: modeChanged ? 409 : 503 },
|
|
);
|
|
}
|
|
const reason = error instanceof Error ? error.name : "UnknownError";
|
|
console.error(
|
|
`[billing] reservation failed request=${requestId} reason=${reason}`,
|
|
);
|
|
return NextResponse.json(
|
|
{ error: "暂时无法确认咨询点数", message: "请稍后重试。" },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
|
|
const resolvedQuestion = resolveConsultationQuestion({
|
|
visibleQuestion: parsed.data.question,
|
|
entrypoint: parsed.data.entrypoint,
|
|
currentDate,
|
|
consultationMode: prepared.consultationMode,
|
|
});
|
|
|
|
const modelSelection = prepared.reservation;
|
|
|
|
if (modelSelection.status === "unavailable") {
|
|
return NextResponse.json(
|
|
{
|
|
error: "模型暂不可用",
|
|
message: "请选择其他模型后重新发送,本次不会扣除点数。",
|
|
},
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
|
|
const selectedModel = modelSelection.model;
|
|
const reserveResult = modelSelection.reservation;
|
|
|
|
if (!reserveResult.success) {
|
|
const insufficient = reserveResult.error_code === "insufficient_credits";
|
|
return NextResponse.json(
|
|
{
|
|
error: insufficient ? "咨询点数不足" : "暂时无法扣除咨询点数",
|
|
message: insufficient
|
|
? "请先兑换咨询点数后再继续。"
|
|
: reserveResult.error_code || "请稍后重试。",
|
|
},
|
|
{ status: insufficient ? 402 : 503 },
|
|
);
|
|
}
|
|
|
|
|
|
async function cancel(): Promise<AgentSettlementResult> {
|
|
try {
|
|
const result = await retryDetachedSettlement(async () => {
|
|
const { data, error } = await accounting.rpc("cancel_consultation_credit", {
|
|
p_user_id: userId,
|
|
p_request_id: requestId,
|
|
});
|
|
const parsedCancellation = consultationCompletionSchema.safeParse(first(data ?? []));
|
|
if (error || !parsedCancellation.success) {
|
|
throw new CreditRpcError(error?.message || "invalid_cancellation_response");
|
|
}
|
|
return parsedCancellation.data;
|
|
});
|
|
if (!result.success && result.error_code !== "request_completed") {
|
|
throw new CreditRpcError(result.error_code || "cancellation_rejected");
|
|
}
|
|
return "cancelled";
|
|
} catch (error) {
|
|
const reason = error instanceof Error ? error.name : "UnknownError";
|
|
console.error(
|
|
`[billing] cancellation failed request=${requestId} reason=${reason}`,
|
|
);
|
|
return "failed";
|
|
}
|
|
}
|
|
|
|
const usageStartedAt = Date.now();
|
|
async function usagePayload(usage: Promise<{ inputTokens?: number; outputTokens?: number }>) {
|
|
const resolved = await usage;
|
|
const inputTokens = Math.max(0, Math.trunc(resolved.inputTokens ?? 0));
|
|
const outputTokens = Math.max(0, Math.trunc(resolved.outputTokens ?? 0));
|
|
return {
|
|
eventKey: requestId,
|
|
actualModelId: selectedModel.id,
|
|
modelConfigVersion: selectedModel.configVersion,
|
|
inputTokens,
|
|
outputTokens,
|
|
costMicrousd: Math.round((
|
|
inputTokens * (selectedModel.inputCostMicrousdPerMillion ?? 0)
|
|
+ outputTokens * (selectedModel.outputCostMicrousdPerMillion ?? 0)
|
|
) / 1_000_000),
|
|
durationMs: Date.now() - usageStartedAt,
|
|
};
|
|
}
|
|
|
|
async function completeResponse(
|
|
rawTransformedText: string,
|
|
usage: Promise<{ inputTokens?: number; outputTokens?: number }>,
|
|
techniqueTruth: string,
|
|
workflowReceipt: WorkflowReceipt,
|
|
agentExecutionReceipt?: AgentExecutionReceipt,
|
|
thinkingText?: string,
|
|
): Promise<AgentSettlementResult> {
|
|
try {
|
|
const reply = parseAgentReply(
|
|
rawTransformedText,
|
|
createConsultationReplyMetadata({ question: visibleQuestion }),
|
|
);
|
|
if (!reply.text) throw new Error("empty_agent_reply");
|
|
const persistedThinking = thinkingText?.trim().slice(0, 4_000);
|
|
const responseMessage = {
|
|
role: "assistant" as const,
|
|
text: reply.text,
|
|
...(persistedThinking ? { thinkingText: persistedThinking } : {}),
|
|
techniqueTruth,
|
|
workflowReceipt,
|
|
...(agentExecutionReceipt ? { agentExecutionReceipt } : {}),
|
|
};
|
|
const actualUsage = await usagePayload(usage);
|
|
const completion = await retryDetachedSettlement(async () => {
|
|
const { data, error } = await accounting.rpc("complete_consultation_response", {
|
|
p_user_id: userId,
|
|
p_request_id: requestId,
|
|
p_session_id: sessionId,
|
|
p_response_message: responseMessage,
|
|
p_actual_usage: actualUsage,
|
|
});
|
|
const parsedCompletion = consultationCompletionSchema.safeParse(first(data ?? []));
|
|
if (error || !parsedCompletion.success) {
|
|
throw new CreditRpcError(error?.message || "invalid_completion_response");
|
|
}
|
|
return parsedCompletion.data;
|
|
});
|
|
if (!completion.success && completion.error_code !== "request_cancelled") {
|
|
throw new CreditRpcError(completion.error_code || "completion_rejected");
|
|
}
|
|
return "completed";
|
|
} catch (error) {
|
|
await cancel();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
let settlement: Promise<AgentSettlementResult> | null = null;
|
|
function settleResult(action: () => Promise<AgentSettlementResult>) {
|
|
settlement ??= action();
|
|
return settlement;
|
|
}
|
|
async function settle(action: () => Promise<AgentSettlementResult>): Promise<void> {
|
|
await settleResult(action);
|
|
}
|
|
|
|
// A run that fails before streamAgentResponse exists never reaches its error
|
|
// path, so the closed observability event—the only place the tool failure
|
|
// code, per-step durations and the model finish reason are recorded—would be
|
|
// lost for exactly the runs that failed hardest. The agentic setup publishes
|
|
// its settle-and-log entry point here so the request-level catch below can
|
|
// still emit it.
|
|
const agenticFailure: { report?: (error: unknown) => Promise<void> } = {};
|
|
|
|
async function runAgenticConsultation(
|
|
consultationMode: ConsultationBirthTimeMode,
|
|
history: Array<{ role: "user" | "assistant"; text: string }>,
|
|
name: string,
|
|
generalDailyContext: GeneralDailyPanchangaContext | null,
|
|
) {
|
|
// The recorded step list has to be able to hold everything the model loop
|
|
// can produce, otherwise a run that exhausts its steps also truncates the
|
|
// evidence of having done so.
|
|
const state = createConsultationRuntimeState({ plannedSteps: AGENT_MAX_STEPS });
|
|
const hooks = createConsultationRuntimeHooks(state);
|
|
const usages: Promise<Usage>[] = [];
|
|
const agentStartedAt = Date.now();
|
|
let firstActivityMs = -1;
|
|
let firstTextMs = -1;
|
|
let logged = false;
|
|
const markFirstActivity = () => { if (firstActivityMs < 0) firstActivityMs = Date.now() - agentStartedAt; };
|
|
const markFirstText = () => { if (firstTextMs < 0) firstTextMs = Date.now() - agentStartedAt; };
|
|
const logRun = async (
|
|
errorCode: string | undefined,
|
|
settlementResult: AgentSettlementResult,
|
|
) => {
|
|
if (logged) return;
|
|
logged = true;
|
|
const resolvedUsage: Usage = await mergeUsage(usages).catch(() => ({}));
|
|
const inputTokens = Math.max(0, Math.trunc(resolvedUsage.inputTokens ?? 0));
|
|
const outputTokens = Math.max(0, Math.trunc(resolvedUsage.outputTokens ?? 0));
|
|
const skillStep = state.steps.find((step) => step.kind === "skill");
|
|
const runStatus = errorCode === undefined
|
|
? "completed"
|
|
: errorCode === "cancelled"
|
|
? "cancelled"
|
|
: "failed";
|
|
logAgentObservability({
|
|
runId: requestId,
|
|
requestId,
|
|
sessionId,
|
|
agentVersion: "consultation-agentic-v1",
|
|
modelVersion: String(selectedModel.configVersion),
|
|
policyVersion: "consultation-runtime-contract-v1",
|
|
toolCalls: state.steps
|
|
.filter((step) => step.kind === "tool")
|
|
.map((step) => ({
|
|
name: step.name,
|
|
durationMs: Math.max(0, Math.trunc(step.durationMs ?? 0)),
|
|
status: step.status,
|
|
...(step.failureCode ? { failureCode: step.failureCode } : {}),
|
|
})),
|
|
contractPhases: [
|
|
{
|
|
phase: "skill.load",
|
|
...(skillStep?.durationMs === undefined
|
|
? {}
|
|
: { durationMs: Math.max(0, Math.trunc(skillStep.durationMs)) }),
|
|
status: state.jyotishSkillBound ? "completed" : "failed",
|
|
},
|
|
{
|
|
phase: "answer.first_activity",
|
|
...(firstActivityMs < 0 ? {} : { durationMs: firstActivityMs }),
|
|
status: firstActivityMs < 0 ? "skipped" : "completed",
|
|
},
|
|
{
|
|
phase: "answer.first_output",
|
|
...(firstTextMs < 0 ? {} : { durationMs: firstTextMs }),
|
|
status: firstTextMs < 0 ? "skipped" : "completed",
|
|
},
|
|
{
|
|
phase: "run.total",
|
|
durationMs: Date.now() - agentStartedAt,
|
|
status: runStatus,
|
|
},
|
|
{
|
|
phase: "billing.settled",
|
|
status: settlementResult,
|
|
},
|
|
],
|
|
retryCount: Math.max(0, usages.length - 1),
|
|
...consultationModelStepTelemetry(state),
|
|
...(errorCode === undefined ? {} : { errorCode }),
|
|
inputTokens,
|
|
outputTokens,
|
|
themeCoverage: state.workflowReceipt?.domains ?? [consultationTheme],
|
|
billingSettlementResult: settlementResult,
|
|
});
|
|
};
|
|
const settleRun = async (
|
|
action: () => Promise<AgentSettlementResult>,
|
|
errorCode: string | undefined,
|
|
) => {
|
|
try {
|
|
const actualSettlementResult = await settleResult(action);
|
|
const outcome = settlementTelemetryOutcome(actualSettlementResult, errorCode);
|
|
await logRun(outcome.errorCode, outcome.billingSettlementResult);
|
|
} catch (error) {
|
|
const outcome = settlementTelemetryOutcome("failed", errorCode);
|
|
await logRun(outcome.errorCode, outcome.billingSettlementResult);
|
|
throw error;
|
|
}
|
|
};
|
|
agenticFailure.report = async (error) => {
|
|
try {
|
|
await settleRun(cancel, toAgentObservabilityErrorCode(error));
|
|
} catch {
|
|
// Settlement already reported itself through logRun; the request-level
|
|
// failure response must not depend on it succeeding.
|
|
}
|
|
};
|
|
const baseMessages = [
|
|
...history.map((message) => message.role === "user"
|
|
? { role: "user" as const, content: message.text }
|
|
: { role: "assistant" as const, content: message.text }),
|
|
{
|
|
role: "user" as const,
|
|
content: [
|
|
currentTimeContext(requestTime),
|
|
name ? `用户称呼:${name}` : "",
|
|
consultationMode === "general_no_birth_time" || (consultationMode === "declared_birth_window" && generalDailyContext)
|
|
? generalNoMinuteInstruction(Boolean(generalDailyContext))
|
|
: consultationMode === "declared_birth_window"
|
|
? declaredWindowInstruction()
|
|
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。事业/财富/婚恋/家庭按 skill Level 2 模板写:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,然后才是现代生活措辞。不要复述内部 JSON 字段。",
|
|
generalDailyContextPrompt(generalDailyContext),
|
|
resolvedQuestion.modelQuestion,
|
|
].filter(Boolean).join("\n"),
|
|
},
|
|
];
|
|
const agentAbortSignal = AbortSignal.timeout(AGENT_TIMEOUT_MS);
|
|
const streamOptions = {
|
|
runId: requestId,
|
|
maxSteps: AGENT_MAX_STEPS,
|
|
abortSignal: agentAbortSignal,
|
|
hooks,
|
|
...consultationGenerationSettings(selectedModel.model),
|
|
};
|
|
const workflowReceipt: WorkflowReceipt = usesPublicDailyGeneralAgent(consultationMode, generalDailyContext)
|
|
? {
|
|
route: generalDailyContext ? "general-daily-panchanga" : "general-no-birth-time",
|
|
status: "ready",
|
|
preciseTiming: "blocked",
|
|
missingLayers: ["birth-minute"],
|
|
}
|
|
: consultationMode === "declared_birth_window"
|
|
? {
|
|
route: "declared-birth-window",
|
|
status: "blocked",
|
|
preciseTiming: "blocked",
|
|
missingLayers: ["birth-minute"],
|
|
}
|
|
: { route: "pending", status: "blocked", preciseTiming: "blocked", missingLayers: [] };
|
|
|
|
if (usesPublicDailyGeneralAgent(consultationMode, generalDailyContext)) {
|
|
state.workflowReceipt = workflowReceipt;
|
|
const agent = getGeneralJyotishAgent(selectedModel);
|
|
const result = await agent.stream(baseMessages, streamOptions);
|
|
usages.push(result.totalUsage);
|
|
// This mode has no calculation to require and no chart method to bind,
|
|
// so there is no contract for a retry to repair.
|
|
const retryForAnswer = async () => {
|
|
const retried = await agent.stream([
|
|
...baseMessages,
|
|
{ role: "user" as const, content: "上一轮没有输出任何回答文本。请直接给出这个问题的回答,不要只说明过程。" },
|
|
], streamOptions);
|
|
usages.push(retried.totalUsage);
|
|
return retried.fullStream;
|
|
};
|
|
const executionReceipt = (): AgentExecutionReceipt => ({
|
|
runId: requestId,
|
|
runtime: "mastra-agentic",
|
|
skill: {
|
|
name: "jyotish-vedic-astrology",
|
|
loaded: state.jyotishSkillBound,
|
|
referenceReads: state.skillReferenceReadCount,
|
|
methodologySections: state.methodologySectionCount,
|
|
},
|
|
steps: publicConsultationRuntimeSteps(state),
|
|
stepBudget: consultationStepBudgetReceipt(state),
|
|
workflow: workflowReceipt,
|
|
techniqueTruth: generalDailyContext ? "public-panchanga-only" : "not-applicable",
|
|
...(state.techniqueAuditTable?.length
|
|
? { techniqueAuditTable: state.techniqueAuditTable }
|
|
: {}),
|
|
});
|
|
return streamAgentResponse({
|
|
runId: requestId,
|
|
requestId,
|
|
state,
|
|
stream: result.fullStream,
|
|
requireTool: false,
|
|
retryForAnswer,
|
|
continueAfterDisconnect: true,
|
|
transformText: createBirthTimeModeOutputGuard(
|
|
generalDailyContext ? "general_no_birth_time" : consultationMode,
|
|
false,
|
|
),
|
|
toolStatus: () => "ready",
|
|
receipt: executionReceipt,
|
|
headers: { "x-jyotish-birth-time-mode": consultationMode },
|
|
onFirstActivity: markFirstActivity,
|
|
onFirstOutput: markFirstText,
|
|
onComplete: (output, agentExecutionReceipt, thinkingText) => settleRun(() => completeResponse(
|
|
output,
|
|
mergeUsage(usages),
|
|
generalDailyContext ? "public-panchanga-only" : "not-applicable",
|
|
workflowReceipt,
|
|
agentExecutionReceipt,
|
|
thinkingText,
|
|
), undefined),
|
|
onError: (error) => settleRun(
|
|
cancel,
|
|
toAgentObservabilityErrorCode(error),
|
|
),
|
|
onCancel: () => settleRun(cancel, "cancelled"),
|
|
});
|
|
}
|
|
|
|
if (shouldRunDeclaredWindowWorkflow(consultationMode)) {
|
|
if (!prepared.declaredWindow) throw new Error("declared_window_truth_missing");
|
|
const agentContext = createWindowConsultationAgentContext({
|
|
userId,
|
|
sessionId,
|
|
requestId,
|
|
consultationMode: "declared_birth_window",
|
|
plan: prepared.preReserveResult,
|
|
theme: consultationTheme,
|
|
declaredWindow: prepared.declaredWindow,
|
|
abortSignal: agentAbortSignal,
|
|
state,
|
|
});
|
|
const agent = getWindowJyotishAgent(selectedModel, agentContext);
|
|
const result = await agent.stream(baseMessages, streamOptions);
|
|
usages.push(result.totalUsage);
|
|
const retry = async () => {
|
|
const retried = await agent.stream([
|
|
...baseMessages,
|
|
{
|
|
role: "user" as const,
|
|
content: "运行合同不完整:本次尚未取得声明窗口计算结果。请调用 run-jyotish-window-consultation 完成计算,再据此回答;不要在工具参数中添加出生分钟。",
|
|
},
|
|
], streamOptions);
|
|
usages.push(retried.totalUsage);
|
|
return retried.fullStream;
|
|
};
|
|
const retryForAnswer = async () => {
|
|
const retried = await agent.stream([
|
|
...baseMessages,
|
|
{
|
|
role: "user" as const,
|
|
content: "服务器窗口计算已经完成,但上一轮没有输出任何回答文本。请重新取回本次计算结果,然后直接给出回答;不要只描述过程或工具调用。",
|
|
},
|
|
], streamOptions);
|
|
usages.push(retried.totalUsage);
|
|
return retried.fullStream;
|
|
};
|
|
const executionReceipt = (): AgentExecutionReceipt => ({
|
|
runId: requestId,
|
|
runtime: "mastra-agentic",
|
|
skill: {
|
|
name: "jyotish-vedic-astrology",
|
|
loaded: state.jyotishSkillBound,
|
|
referenceReads: state.skillReferenceReadCount,
|
|
methodologySections: state.methodologySectionCount,
|
|
},
|
|
steps: publicConsultationRuntimeSteps(state),
|
|
stepBudget: consultationStepBudgetReceipt(state),
|
|
workflow: state.workflowReceipt ?? workflowReceipt,
|
|
techniqueTruth: state.techniqueTruth ?? "declared-window",
|
|
...(state.techniqueAuditTable?.length
|
|
? { techniqueAuditTable: state.techniqueAuditTable }
|
|
: {}),
|
|
});
|
|
return streamAgentResponse({
|
|
runId: requestId,
|
|
requestId,
|
|
state,
|
|
stream: result.fullStream,
|
|
requireTool: true,
|
|
retry,
|
|
retryForAnswer,
|
|
continueAfterDisconnect: true,
|
|
transformText: createBirthTimeModeOutputGuard(consultationMode, false),
|
|
toolStatus: () => workflowStatus(state.workflowReceipt?.status),
|
|
receipt: executionReceipt,
|
|
headers: { "x-jyotish-birth-time-mode": consultationMode },
|
|
onFirstActivity: markFirstActivity,
|
|
onFirstOutput: markFirstText,
|
|
onComplete: (output, agentExecutionReceipt, thinkingText) => settleRun(() => completeResponse(
|
|
output,
|
|
mergeUsage(usages),
|
|
state.techniqueTruth ?? "declared-window",
|
|
state.workflowReceipt ?? workflowReceipt,
|
|
agentExecutionReceipt,
|
|
thinkingText,
|
|
), undefined),
|
|
onError: (error) => settleRun(
|
|
cancel,
|
|
toAgentObservabilityErrorCode(error),
|
|
),
|
|
onCancel: () => settleRun(cancel, "cancelled"),
|
|
});
|
|
}
|
|
|
|
if (!prepared.serverChart) throw new Error("server_chart_truth_missing");
|
|
const agentContext = createConsultationAgentContext({
|
|
userId,
|
|
sessionId,
|
|
requestId,
|
|
consultationMode: consultationMode === "verified_chart" ? "verified_chart" : "unverified_birth_time",
|
|
plan: prepared.preReserveResult,
|
|
theme: consultationTheme,
|
|
serverChart: prepared.serverChart,
|
|
abortSignal: agentAbortSignal,
|
|
state,
|
|
});
|
|
const agent = getJyotishAgent(selectedModel, agentContext);
|
|
const result = await agent.stream(baseMessages, streamOptions);
|
|
usages.push(result.totalUsage);
|
|
const retry = async () => {
|
|
const retried = await agent.stream([
|
|
...baseMessages,
|
|
{
|
|
role: "user" as const,
|
|
content: "运行合同不完整:本次尚未取得服务器计算结果。请调用 run-jyotish-consultation 完成计算,再据此回答;不要在工具参数中添加出生资料。",
|
|
},
|
|
], streamOptions);
|
|
usages.push(retried.totalUsage);
|
|
return retried.fullStream;
|
|
};
|
|
// The tool caches this request's calculation, so this attempt gets the same
|
|
// evidence back without paying for it twice; keeping the tools available is
|
|
// what puts that evidence in front of the model at all.
|
|
const retryForAnswer = async () => {
|
|
const retried = await agent.stream([
|
|
...baseMessages,
|
|
{
|
|
role: "user" as const,
|
|
content: "服务器计算已经完成,但上一轮没有输出任何回答文本。请重新取回本次计算结果,然后直接给出回答;不要只描述过程或工具调用。",
|
|
},
|
|
], streamOptions);
|
|
usages.push(retried.totalUsage);
|
|
return retried.fullStream;
|
|
};
|
|
const executionReceipt = (): AgentExecutionReceipt => ({
|
|
runId: requestId,
|
|
runtime: "mastra-agentic",
|
|
skill: {
|
|
name: "jyotish-vedic-astrology",
|
|
loaded: state.jyotishSkillBound,
|
|
referenceReads: state.skillReferenceReadCount,
|
|
methodologySections: state.methodologySectionCount,
|
|
},
|
|
steps: publicConsultationRuntimeSteps(state),
|
|
stepBudget: consultationStepBudgetReceipt(state),
|
|
workflow: state.workflowReceipt ?? workflowReceipt,
|
|
techniqueTruth: state.techniqueTruth ?? "unknown",
|
|
...(state.techniqueAuditTable?.length
|
|
? { techniqueAuditTable: state.techniqueAuditTable }
|
|
: {}),
|
|
});
|
|
return streamAgentResponse({
|
|
runId: requestId,
|
|
requestId,
|
|
state,
|
|
stream: result.fullStream,
|
|
requireTool: true,
|
|
retry,
|
|
retryForAnswer,
|
|
continueAfterDisconnect: true,
|
|
transformText: (text) => createBirthTimeModeOutputGuard(
|
|
consultationMode,
|
|
state.workflowReceipt?.preciseTiming === "allowed",
|
|
)(text),
|
|
toolStatus: () => workflowStatus(state.workflowReceipt?.status),
|
|
receipt: executionReceipt,
|
|
headers: { "x-jyotish-birth-time-mode": consultationMode },
|
|
onFirstActivity: markFirstActivity,
|
|
onFirstOutput: markFirstText,
|
|
onComplete: (output, agentExecutionReceipt, thinkingText) => settleRun(() => completeResponse(
|
|
output,
|
|
mergeUsage(usages),
|
|
state.techniqueTruth ?? "unknown",
|
|
state.workflowReceipt ?? workflowReceipt,
|
|
agentExecutionReceipt,
|
|
thinkingText,
|
|
), undefined),
|
|
onError: (error) => settleRun(
|
|
cancel,
|
|
toAgentObservabilityErrorCode(error),
|
|
),
|
|
onCancel: () => settleRun(cancel, "cancelled"),
|
|
});
|
|
}
|
|
|
|
try {
|
|
const { history } = parsed.data;
|
|
const name = prepared.serverChart?.name ?? prepared.declaredWindow?.name ?? parsed.data.name;
|
|
const consultationMode: ConsultationBirthTimeMode = prepared.consultationMode;
|
|
const generalDailyContext = shouldLoadGeneralDailyPanchanga({
|
|
consultationMode,
|
|
entrypoint: parsed.data.entrypoint,
|
|
visibleQuestion: parsed.data.question,
|
|
})
|
|
? await loadGeneralDailyPanchangaContext({
|
|
date: currentDate,
|
|
reference: prepared.generalDailyReference,
|
|
})
|
|
: null;
|
|
if (shouldUseAgenticRuntime(user) || shouldRunDeclaredWindowWorkflow(consultationMode)) {
|
|
return await runAgenticConsultation(consultationMode, history, name, generalDailyContext);
|
|
}
|
|
if (!shouldRunBirthChartWorkflow(consultationMode)) {
|
|
const result = await getGeneralJyotishAgent(selectedModel).stream([
|
|
{
|
|
role: "user",
|
|
content: [
|
|
currentTimeContext(requestTime),
|
|
name ? `用户称呼:${name}` : "",
|
|
generalNoMinuteInstruction(Boolean(generalDailyContext)),
|
|
generalDailyContextPrompt(generalDailyContext),
|
|
resolvedQuestion.modelQuestion,
|
|
].filter(Boolean).join("\n"),
|
|
},
|
|
]);
|
|
const workflowReceipt: WorkflowReceipt = {
|
|
route: generalDailyContext ? "general-daily-panchanga" : "general-no-birth-time",
|
|
status: "ready",
|
|
preciseTiming: "blocked",
|
|
missingLayers: ["birth-minute"],
|
|
};
|
|
const settleErrored = (emitted: boolean, output: string) => settle(
|
|
emitted
|
|
? () => completeResponse(
|
|
output,
|
|
result.totalUsage,
|
|
generalDailyContext ? "public-panchanga-only" : "not-applicable",
|
|
workflowReceipt,
|
|
)
|
|
: cancel,
|
|
);
|
|
return streamTextResponse(result.textStream, {
|
|
transformText: createBirthTimeModeOutputGuard(
|
|
generalDailyContext ? "general_no_birth_time" : consultationMode,
|
|
false,
|
|
),
|
|
mode: "mastra",
|
|
requestId,
|
|
continueAfterDisconnect: true,
|
|
headers: {
|
|
"x-jyotish-workflow-route": workflowReceipt.route,
|
|
"x-jyotish-workflow-status": workflowReceipt.status,
|
|
"x-jyotish-technique-truth": generalDailyContext ? "public-panchanga-only" : "not-applicable",
|
|
"x-jyotish-precise-timing": workflowReceipt.preciseTiming,
|
|
"x-jyotish-missing-layers": workflowReceipt.missingLayers.join(","),
|
|
"x-jyotish-birth-time-mode": consultationMode,
|
|
},
|
|
onComplete: (rawTransformedText) => settle(() => completeResponse(
|
|
rawTransformedText,
|
|
result.totalUsage,
|
|
generalDailyContext ? "public-panchanga-only" : "not-applicable",
|
|
workflowReceipt,
|
|
)),
|
|
onError: (_error, emitted, output: string) => settleErrored(emitted, output),
|
|
onCancel: () => settle(cancel),
|
|
});
|
|
}
|
|
|
|
if (!prepared.serverChart) throw new Error("server_chart_truth_missing");
|
|
const toolInput = consultationInputSchema.parse({
|
|
...prepared.serverChart.toolInput,
|
|
// A user-reported concrete minute is a normal chart calculation. Keep its
|
|
// provenance, but let server evidence—not rectification purchase state—own
|
|
// precise-timing permission. Never reactivate the retired questionnaire.
|
|
entryMode: "direct_chart",
|
|
question: resolvedQuestion.modelQuestion,
|
|
theme: parsed.data.theme,
|
|
});
|
|
const workflowContext = applyBirthTimeModeToWorkflowContext(
|
|
await runConsultationWorkflow(toolInput, {
|
|
foreground: true,
|
|
plan: prepared.preReserveResult,
|
|
}),
|
|
consultationMode,
|
|
);
|
|
const workflowReceipt = consultationWorkflowReceipt(workflowContext);
|
|
|
|
const result = await getLegacyJyotishAgent(selectedModel, workflowContext).stream([
|
|
...history.map((message) => message.role === "user"
|
|
? { role: "user" as const, content: message.text }
|
|
: { role: "assistant" as const, content: message.text }),
|
|
{
|
|
role: "user",
|
|
content: [
|
|
currentTimeContext(requestTime),
|
|
name ? `用户称呼:${name}` : "",
|
|
"请按 skill Level 2 模板回答下面的问题:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,然后才是现代生活措辞。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。",
|
|
resolvedQuestion.modelQuestion,
|
|
].filter(Boolean).join("\n"),
|
|
},
|
|
]);
|
|
const responseWorkflowReceipt = {
|
|
route: workflowReceipt.route,
|
|
status: workflowReceipt.status,
|
|
preciseTiming: workflowReceipt.preciseTiming,
|
|
missingLayers: workflowReceipt.missingLayers === "none"
|
|
? []
|
|
: workflowReceipt.missingLayers.split(",").map((item) => item.trim()).filter(Boolean),
|
|
};
|
|
const settleErrored = (emitted: boolean, output: string) => settle(
|
|
emitted
|
|
? () => completeResponse(
|
|
output,
|
|
result.totalUsage,
|
|
workflowReceipt.techniqueTruth,
|
|
responseWorkflowReceipt,
|
|
)
|
|
: cancel,
|
|
);
|
|
return streamTextResponse(result.textStream, {
|
|
transformText: createBirthTimeModeOutputGuard(
|
|
consultationMode,
|
|
workflowReceipt.preciseTiming !== "blocked",
|
|
),
|
|
mode: "mastra",
|
|
requestId,
|
|
continueAfterDisconnect: true,
|
|
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,
|
|
"x-jyotish-birth-time-mode": consultationMode,
|
|
},
|
|
onComplete: (rawTransformedText) => settle(() => completeResponse(
|
|
rawTransformedText,
|
|
result.totalUsage,
|
|
workflowReceipt.techniqueTruth,
|
|
responseWorkflowReceipt,
|
|
)),
|
|
onError: (_error, emitted, output: string) => settleErrored(emitted, output),
|
|
onCancel: () => settle(cancel),
|
|
});
|
|
} catch (error) {
|
|
if (agenticFailure.report) await agenticFailure.report(error);
|
|
else await cancel();
|
|
const reason = error instanceof Error ? error.name : "UnknownError";
|
|
console.error(
|
|
`[consult] generation failed request=${requestId} model=${selectedModel.id} reason=${reason}`,
|
|
);
|
|
return NextResponse.json(
|
|
{
|
|
error: "暂时无法生成解读",
|
|
message: "咨询服务暂时不可用,请稍后再试。",
|
|
recovery: "稍后重试,或换一个模型继续。",
|
|
},
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
}
|