fix(web): keep unrectified birth data usable without inventing a minute
Rectification stays optional. Reported minutes can consult and generate reports; date-plus-period uses a declared window instead of a midpoint or 00:00. Updates BUG-341. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
getGeneralJyotishAgent,
|
||||
getJyotishAgent,
|
||||
getLegacyJyotishAgent,
|
||||
getWindowJyotishAgent,
|
||||
runConsultationWorkflow,
|
||||
} from "@/mastra";
|
||||
import { blocksPromptExtraction } from "@/lib/consult-safety";
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
import {
|
||||
consultationEntrypointSchema,
|
||||
resolveConsultationQuestion,
|
||||
shouldLoadGeneralDailyPanchanga,
|
||||
} from "@/lib/consultation-entrypoint";
|
||||
import { CreditRpcError } from "@/lib/consultation-billing";
|
||||
import { reserveConsultationModel } from "@/lib/consultation-model-selection";
|
||||
@@ -39,6 +41,7 @@ import {
|
||||
AGENT_TIMEOUT_MS,
|
||||
consultationGenerationSettings,
|
||||
createConsultationAgentContext,
|
||||
createWindowConsultationAgentContext,
|
||||
consultationModelStepTelemetry,
|
||||
consultationStepBudgetReceipt,
|
||||
createConsultationRuntimeHooks,
|
||||
@@ -50,6 +53,7 @@ import {
|
||||
consultationBirthTimeModeSchema,
|
||||
createBirthTimeModeOutputGuard,
|
||||
shouldRunBirthChartWorkflow,
|
||||
shouldRunDeclaredWindowWorkflow,
|
||||
type ConsultationBirthTimeMode,
|
||||
} from "@/lib/consultation-birth-time-mode";
|
||||
import {
|
||||
@@ -89,7 +93,7 @@ const chatRequestMetadataSchema = z.object({
|
||||
|
||||
const chartChatRequestSchema = consultationInputSchema.extend({
|
||||
...chatRequestMetadataSchema.shape,
|
||||
consultationMode: consultationBirthTimeModeSchema.exclude(["general_no_birth_time"])
|
||||
consultationMode: consultationBirthTimeModeSchema.exclude(["general_no_birth_time", "declared_birth_window"])
|
||||
.optional()
|
||||
.default("verified_chart"),
|
||||
entrypoint: consultationEntrypointSchema.optional(),
|
||||
@@ -103,7 +107,19 @@ const generalChatRequestSchema = z.object({
|
||||
entrypoint: z.literal("daily_starlanguage").optional(),
|
||||
}).strict();
|
||||
|
||||
const chatRequestSchema = z.union([generalChatRequestSchema, chartChatRequestSchema]);
|
||||
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(),
|
||||
@@ -160,6 +176,24 @@ function generalDailyContextPrompt(context: GeneralDailyPanchangaContext | null)
|
||||
].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);
|
||||
}
|
||||
@@ -307,7 +341,7 @@ export async function POST(request: Request) {
|
||||
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_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")
|
||||
.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");
|
||||
@@ -638,11 +672,11 @@ export async function POST(request: Request) {
|
||||
content: [
|
||||
currentTimeContext(requestTime),
|
||||
name ? `用户称呼:${name}` : "",
|
||||
consultationMode === "general_no_birth_time"
|
||||
? generalDailyContext
|
||||
? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
|
||||
: "当前是用户明确选择的无出生分钟一般咨询。不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
|
||||
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。事业/财富/婚恋/家庭按 skill Level 2 模板写:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,然后才是现代生活措辞。不要复述内部 JSON 字段。",
|
||||
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"),
|
||||
@@ -656,16 +690,23 @@ export async function POST(request: Request) {
|
||||
hooks,
|
||||
...consultationGenerationSettings(selectedModel.model),
|
||||
};
|
||||
const workflowReceipt: WorkflowReceipt = consultationMode === "general_no_birth_time"
|
||||
const workflowReceipt: WorkflowReceipt = usesPublicDailyGeneralAgent(consultationMode, generalDailyContext)
|
||||
? {
|
||||
route: generalDailyContext ? "general-daily-panchanga" : "general-no-birth-time",
|
||||
status: "ready",
|
||||
preciseTiming: "blocked",
|
||||
missingLayers: ["birth-minute"],
|
||||
}
|
||||
: { route: "pending", status: "blocked", preciseTiming: "blocked", missingLayers: [] };
|
||||
: consultationMode === "declared_birth_window"
|
||||
? {
|
||||
route: "declared-birth-window",
|
||||
status: "blocked",
|
||||
preciseTiming: "blocked",
|
||||
missingLayers: ["birth-minute"],
|
||||
}
|
||||
: { route: "pending", status: "blocked", preciseTiming: "blocked", missingLayers: [] };
|
||||
|
||||
if (consultationMode === "general_no_birth_time") {
|
||||
if (usesPublicDailyGeneralAgent(consultationMode, generalDailyContext)) {
|
||||
state.workflowReceipt = workflowReceipt;
|
||||
const agent = getGeneralJyotishAgent(selectedModel);
|
||||
const result = await agent.stream(baseMessages, streamOptions);
|
||||
@@ -705,7 +746,10 @@ export async function POST(request: Request) {
|
||||
requireTool: false,
|
||||
retryForAnswer,
|
||||
continueAfterDisconnect: true,
|
||||
transformText: createBirthTimeModeOutputGuard(consultationMode, false),
|
||||
transformText: createBirthTimeModeOutputGuard(
|
||||
generalDailyContext ? "general_no_birth_time" : consultationMode,
|
||||
false,
|
||||
),
|
||||
toolStatus: () => "ready",
|
||||
receipt: executionReceipt,
|
||||
headers: { "x-jyotish-birth-time-mode": consultationMode },
|
||||
@@ -726,12 +770,97 @@ export async function POST(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
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) => settleRun(() => completeResponse(
|
||||
output,
|
||||
mergeUsage(usages),
|
||||
state.techniqueTruth ?? "declared-window",
|
||||
state.workflowReceipt ?? workflowReceipt,
|
||||
agentExecutionReceipt,
|
||||
), 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: consultationMode === "verified_chart" ? "verified_chart" : "unverified_birth_time",
|
||||
plan: prepared.preReserveResult,
|
||||
theme: consultationTheme,
|
||||
serverChart: prepared.serverChart,
|
||||
@@ -818,16 +947,19 @@ export async function POST(request: Request) {
|
||||
|
||||
try {
|
||||
const { history } = parsed.data;
|
||||
const name = prepared.serverChart?.name ?? parsed.data.name;
|
||||
const name = prepared.serverChart?.name ?? prepared.declaredWindow?.name ?? parsed.data.name;
|
||||
const consultationMode: ConsultationBirthTimeMode = prepared.consultationMode;
|
||||
const generalDailyContext = consultationMode === "general_no_birth_time"
|
||||
&& parsed.data.entrypoint === "daily_starlanguage"
|
||||
const generalDailyContext = shouldLoadGeneralDailyPanchanga({
|
||||
consultationMode,
|
||||
entrypoint: parsed.data.entrypoint,
|
||||
visibleQuestion: parsed.data.question,
|
||||
})
|
||||
? await loadGeneralDailyPanchangaContext({
|
||||
date: currentDate,
|
||||
reference: prepared.generalDailyReference,
|
||||
})
|
||||
: null;
|
||||
if (shouldUseAgenticRuntime(user)) {
|
||||
if (shouldUseAgenticRuntime(user) || shouldRunDeclaredWindowWorkflow(consultationMode)) {
|
||||
return await runAgenticConsultation(consultationMode, history, name, generalDailyContext);
|
||||
}
|
||||
if (!shouldRunBirthChartWorkflow(consultationMode)) {
|
||||
@@ -837,9 +969,7 @@ export async function POST(request: Request) {
|
||||
content: [
|
||||
currentTimeContext(requestTime),
|
||||
name ? `用户称呼:${name}` : "",
|
||||
generalDailyContext
|
||||
? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
|
||||
: "当前是用户明确选择的无出生分钟一般咨询。不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。",
|
||||
generalNoMinuteInstruction(Boolean(generalDailyContext)),
|
||||
generalDailyContextPrompt(generalDailyContext),
|
||||
resolvedQuestion.modelQuestion,
|
||||
].filter(Boolean).join("\n"),
|
||||
@@ -862,7 +992,10 @@ export async function POST(request: Request) {
|
||||
: cancel,
|
||||
);
|
||||
return streamTextResponse(result.textStream, {
|
||||
transformText: createBirthTimeModeOutputGuard(consultationMode, false),
|
||||
transformText: createBirthTimeModeOutputGuard(
|
||||
generalDailyContext ? "general_no_birth_time" : consultationMode,
|
||||
false,
|
||||
),
|
||||
mode: "mastra",
|
||||
requestId,
|
||||
continueAfterDisconnect: true,
|
||||
|
||||
@@ -124,7 +124,7 @@ export async function POST(request: Request) {
|
||||
if (userId) {
|
||||
const result = await supabase
|
||||
.from("profiles")
|
||||
.select("name,birth_date,active_birth_time,birth_time_status,latitude,longitude,timezone_offset,birth_place_label")
|
||||
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_status,latitude,longitude,timezone_offset,birth_place_label")
|
||||
.eq("id", userId)
|
||||
.maybeSingle();
|
||||
profile = result.data ?? null;
|
||||
|
||||
Reference in New Issue
Block a user