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:
Jesse_Chen
2026-08-21 15:29:58 +08:00
parent e7f4030e3f
commit 9958e00abc
34 changed files with 1441 additions and 169 deletions
+154 -21
View File
@@ -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,
+1 -1
View File
@@ -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;