fix: close birth-time consultation boundaries

This commit is contained in:
Jesse_Chen
2026-07-21 07:54:04 +08:00
parent 8be39d5ad6
commit bccfb60e41
17 changed files with 827 additions and 121 deletions
+18 -8
View File
@@ -5,6 +5,7 @@ import {
} from "@/lib/birth-time-consultation-consent";
import {
accountProfilePatchSchema,
applyAccountProfileConcurrencyGuards,
resolveAccountBirthTimeApplicationPatch,
} from "@/lib/account-profile-patch";
import { createAdminSupabaseClient, isAdminEmail } from "@/lib/supabase/admin";
@@ -121,11 +122,25 @@ export async function PATCH(request: Request) {
const payload = parsedPayload.data;
const admin = createAdminSupabaseClient();
const { data: currentProfile, error: currentProfileError } = await admin
let { data: currentProfile, error: currentProfileError } = await admin
.from("profiles")
.select("birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,rectification_case_id,country_code,province_code,city_code,district_code")
.select("birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,rectification_case_id,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset")
.eq("id", userId)
.maybeSingle();
if (currentProfileError && isMissingProfileColumn(currentProfileError)) {
const fallback = await admin
.from("profiles")
.select("birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,rectification_case_id,country_code,province_code,city_code,district_code")
.eq("id", userId)
.maybeSingle();
currentProfile = fallback.data ? {
...fallback.data,
latitude: undefined,
longitude: undefined,
timezone_offset: undefined,
} : null;
currentProfileError = fallback.error;
}
if (currentProfileError) {
return NextResponse.json({ error: "暂时无法核对现有出生资料" }, { status: 500 });
}
@@ -179,12 +194,7 @@ export async function PATCH(request: Request) {
}
let query = admin.from("profiles").update(values).eq("id", userId);
if (invalidatesUnconfirmedApplication) {
query = currentProfile.birth_time_status === null
? query.is("birth_time_status", null)
: query.eq("birth_time_status", currentProfile.birth_time_status);
query = currentProfile.active_birth_time === null
? query.is("active_birth_time", null)
: query.eq("active_birth_time", currentProfile.active_birth_time);
query = applyAccountProfileConcurrencyGuards(query, currentProfile);
}
return query.select("id").maybeSingle();
}
+45 -45
View File
@@ -24,10 +24,13 @@ import {
applyBirthTimeModeToWorkflowContext,
consultationBirthTimeModeSchema,
createBirthTimeModeOutputGuard,
serverProfileAllowsBirthTimeMode,
shouldRunBirthChartWorkflow,
type ConsultationBirthTimeMode,
} from "@/lib/consultation-birth-time-mode";
import {
ConsultationProfileTruthError,
prepareConsultationRoute,
} from "@/lib/consultation-route-service";
import { z } from "zod";
export const runtime = "nodejs";
@@ -171,41 +174,6 @@ export async function POST(request: Request) {
);
}
const requestedConsultationMode = parsed.data.consultationMode;
if (shouldRunBirthChartWorkflow(requestedConsultationMode)) {
if (!("hour" in parsed.data) || !("minute" in parsed.data)) {
return NextResponse.json(
{ error: "出生时间请求格式不正确" },
{ status: 400 },
);
}
const requestedTime = `${String(parsed.data.hour).padStart(2, "0")}:${String(parsed.data.minute).padStart(2, "0")}`;
const { data: birthTimeProfile, error: birthTimeProfileError } = await supabase
.from("profiles")
.select("active_birth_time,reported_birth_time,birth_time_source,birth_time_status")
.eq("id", user.id)
.single();
if (birthTimeProfileError || !birthTimeProfile) {
return NextResponse.json(
{ error: "暂时无法核对出生时间状态", message: "请稍后重试,本次不会扣点。" },
{ status: 503 },
);
}
if (!serverProfileAllowsBirthTimeMode(
birthTimeProfile,
requestedConsultationMode,
requestedTime,
)) {
return NextResponse.json(
{
error: "出生时间状态已经变化",
message: "请刷新后重新选择使用填报时间、一般咨询或先完成校正,本次不会扣点。",
},
{ status: 409 },
);
}
}
const requestTime = new Date();
const resolvedQuestion = resolveConsultationQuestion({
visibleQuestion: parsed.data.question,
@@ -215,20 +183,47 @@ export async function POST(request: Request) {
const userId = user.id;
const requestId = parsed.data.requestId;
let modelSelection;
let prepared;
try {
modelSelection = await reserveConsultationModel(
parsed.data.modelId,
resolveLanguageModel,
() =>
runCreditRpc(
prepared = await prepareConsultationRoute({
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_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset")
.eq("id", profileUserId)
.single();
if (error || !data) throw new ConsultationProfileTruthError("profile_unavailable");
return data;
},
reserve: () => reserveConsultationModel(
parsed.data.modelId,
resolveLanguageModel,
() => runCreditRpc(
accounting,
"begin_consultation_credit",
userId,
requestId,
),
);
),
});
} catch (error) {
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}`,
@@ -239,6 +234,8 @@ export async function POST(request: Request) {
);
}
const modelSelection = prepared.reservation;
if (modelSelection.status === "unavailable") {
return NextResponse.json(
{
@@ -299,7 +296,8 @@ export async function POST(request: Request) {
}
try {
const { history, name } = parsed.data;
const { history } = parsed.data;
const name = prepared.serverChart?.name ?? parsed.data.name;
const consultationMode: ConsultationBirthTimeMode = parsed.data.consultationMode;
if (!shouldRunBirthChartWorkflow(consultationMode)) {
const result = await getGeneralJyotishAgent(selectedModel).stream([
@@ -343,12 +341,14 @@ export async function POST(request: Request) {
});
}
if (!prepared.serverChart) throw new Error("server_chart_truth_missing");
const toolInput = consultationInputSchema.parse({
...parsed.data,
...prepared.serverChart.toolInput,
// Unverified use is still a normal chart calculation with a hard answer
// boundary. It must never reactivate the retired rectification questionnaire.
entryMode: "direct_chart",
question: resolvedQuestion.modelQuestion,
theme: parsed.data.theme,
});
const workflowContext = applyBirthTimeModeToWorkflowContext(
await runConsultationWorkflow(toolInput),
+1 -1
View File
@@ -2638,7 +2638,7 @@ export default function Home() {
{birthTimeDisplay ? (
<>
<div><dt>{birthTimeDisplay.kind === "candidate" ? "待验证候选时间" : "当前排盘时间"}</dt><dd>{birthTimeDisplay.activeTime}</dd></div>
<div><dt></dt><dd>{birthTimeDisplay.kind === "candidate" ? "未确认,可临时选择使用" : "已确认"}</dd></div>
<div><dt></dt><dd>{birthTimeDisplay.kind === "candidate" ? "未确认;咨询时仅可临时使用原始填报时间" : "已确认"}</dd></div>
<div><dt></dt><dd>{birthTimeDisplay.reportedLabel}</dd></div>
</>
) : (
@@ -78,7 +78,7 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
</div>
</dl>
{displayState.kind === "candidate" && (
<p>使</p>
<p>使</p>
)}
</section>
)}
+36 -1
View File
@@ -148,6 +148,9 @@ type AccountBirthTimeState = Readonly<{
province_code?: string | null;
city_code?: string | null;
district_code?: string | null;
latitude?: number | null;
longitude?: number | null;
timezone_offset?: number | null;
}>;
const declarationFields = [
@@ -162,8 +165,39 @@ const declarationFields = [
"province_code",
"city_code",
"district_code",
"latitude",
"longitude",
"timezone_offset",
] as const;
const concurrencyFields = [
...declarationFields,
"active_birth_time",
"birth_time",
"birth_time_status",
"rectification_case_id",
] as const;
type ConditionalProfileQuery<Query> = Readonly<{
eq: (column: string, value: string | number) => Query;
is: (column: string, value: null) => Query;
}>;
/** Keeps an ordinary profile edit from overwriting a concurrent edit or confirmation. */
export function applyAccountProfileConcurrencyGuards<
Query extends ConditionalProfileQuery<Query>,
>(query: Query, current: AccountBirthTimeState): Query {
let guarded = query;
for (const field of concurrencyFields) {
const value = current[field];
if (value === undefined) continue;
guarded = value === null
? guarded.is(field, null)
: guarded.eq(field, value);
}
return guarded;
}
export type AccountBirthTimeApplicationPatch = Readonly<{
active_birth_time?: null;
birth_time?: null;
@@ -185,7 +219,8 @@ export function resolveAccountBirthTimeApplicationPatch(
if (confirmed) return {};
if (!current.active_birth_time
&& !current.birth_time
&& current.birth_time_status !== "candidate") return {};
&& current.birth_time_status !== "candidate"
&& !current.rectification_case_id) return {};
return {
active_birth_time: null,
birth_time: null,
@@ -1,5 +1,8 @@
import { z } from "zod";
import { guardPreciseTimingOutput } from "./timing-output-guard.ts";
import {
guardGeneralNoBirthTimeOutput,
guardPreciseTimingOutput,
} from "./timing-output-guard.ts";
export const consultationBirthTimeModeSchema = z.enum([
"verified_chart",
@@ -15,35 +18,6 @@ export function shouldRunBirthChartWorkflow(mode: ConsultationBirthTimeMode): bo
return mode !== "general_no_birth_time";
}
type ServerBirthTimeProfile = Readonly<{
active_birth_time: string | null;
reported_birth_time: string | null;
birth_time_source: string | null;
birth_time_status: string | null;
}>;
const concreteReportedSources = new Set([
"hospital_record",
"family_exact",
"approximate",
]);
export function serverProfileAllowsBirthTimeMode(
profile: ServerBirthTimeProfile,
mode: ConsultationBirthTimeMode,
requestedTime: string | null,
): boolean {
if (mode === "general_no_birth_time") return requestedTime === null;
if (!requestedTime) return false;
if (mode === "verified_chart") {
return profile.birth_time_status === "confirmed"
&& profile.active_birth_time?.slice(0, 5) === requestedTime;
}
return profile.birth_time_status !== "confirmed"
&& concreteReportedSources.has(profile.birth_time_source ?? "")
&& profile.reported_birth_time?.slice(0, 5) === requestedTime;
}
export function applyBirthTimeModeToWorkflowContext<
T extends {
consumer_context: {
@@ -80,7 +54,9 @@ export function createBirthTimeModeOutputGuard(
): (text: string) => string {
let noticeWritten = false;
return (text) => {
const guarded = canAnswerPreciseTiming ? text : guardPreciseTimingOutput(text);
const guarded = mode === "general_no_birth_time"
? guardGeneralNoBirthTimeOutput(text)
: canAnswerPreciseTiming ? text : guardPreciseTimingOutput(text);
if (mode !== "unverified_birth_time" || noticeWritten || !guarded.trim()) return guarded;
noticeWritten = true;
return `> ${UNVERIFIED_BIRTH_TIME_NOTICE}\n\n${guarded}`;
@@ -0,0 +1,245 @@
import { chinaLocations } from "../data/china-locations.ts";
import { isBirthClockTime, parseBirthDate } from "./birth-time-intake-model.ts";
import type { ConsultationBirthTimeMode } from "./consultation-birth-time-mode.ts";
export type ConsultationProfileTruthErrorCode =
| "profile_unavailable"
| "profile_incomplete"
| "profile_inconsistent"
| "mode_changed";
export class ConsultationProfileTruthError extends Error {
readonly code: ConsultationProfileTruthErrorCode;
constructor(code: ConsultationProfileTruthErrorCode) {
super(`Consultation profile truth rejected: ${code}`);
this.name = "ConsultationProfileTruthError";
this.code = code;
}
}
type ServerChartToolInput = Readonly<{
year: number;
month: number;
day: number;
hour: number;
minute: number;
city: string;
lat: number;
lon: number;
tz: number;
}>;
export type ServerChartConsultation = Readonly<{
name: string;
toolInput: ServerChartToolInput;
truth: Readonly<{
birthDate: string;
reportedBirthTime: string | null;
activeBirthTime: string | null;
selectedTimeKind: "reported" | "active";
birthTimeSource: string;
birthTimeStatus: string;
placeLabel: string;
placeCodes: Readonly<{
countryCode: string;
provinceCode: string;
cityCode: string;
districtCode: string | null;
}>;
latitude: number;
longitude: number;
timezoneOffset: number;
}>;
}>;
type PrepareConsultationRouteInput<Reservation> = Readonly<{
userId: string;
mode: ConsultationBirthTimeMode;
loadProfile: (userId: string) => Promise<unknown>;
reserve: () => Promise<Reservation>;
}>;
type RecordValue = Record<string, unknown>;
const allowedBirthTimeSources = new Set([
"hospital_record", "family_exact", "approximate", "period_only", "unknown", "legacy_import",
]);
const allowedBirthTimeStatuses = new Set([
"reported", "assessing", "rectifying", "candidate", "confirmed",
]);
const concreteReportedSources = new Set([
"hospital_record", "family_exact", "approximate",
]);
function record(value: unknown): RecordValue | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? value as RecordValue
: null;
}
function requiredText(profile: RecordValue, key: string): string {
const value = profile[key];
if (typeof value !== "string" || !value.trim()) {
throw new ConsultationProfileTruthError("profile_incomplete");
}
return value.trim();
}
function nullableClock(profile: RecordValue, key: string): string | null {
const value = profile[key];
if (value === null || value === undefined) return null;
if (typeof value !== "string") {
throw new ConsultationProfileTruthError("profile_inconsistent");
}
const clock = value.slice(0, 5);
if (!isBirthClockTime(clock)) {
throw new ConsultationProfileTruthError("profile_inconsistent");
}
return clock;
}
function requiredFiniteNumber(profile: RecordValue, key: string, minimum: number, maximum: number) {
const value = profile[key];
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new ConsultationProfileTruthError("profile_incomplete");
}
if (value < minimum || value > maximum) {
throw new ConsultationProfileTruthError("profile_inconsistent");
}
return value;
}
function sameCoordinate(left: number, right: number) {
return Math.abs(left - right) <= 0.000001;
}
function serverChartFromProfile(
value: unknown,
mode: Exclude<ConsultationBirthTimeMode, "general_no_birth_time">,
): ServerChartConsultation {
const profile = record(value);
if (!profile) throw new ConsultationProfileTruthError("profile_incomplete");
const name = requiredText(profile, "name");
if (name.length > 80) throw new ConsultationProfileTruthError("profile_inconsistent");
const birthDate = requiredText(profile, "birth_date");
if (!parseBirthDate(birthDate)) {
throw new ConsultationProfileTruthError("profile_inconsistent");
}
const [year, month, day] = birthDate.split("-").map(Number);
const reportedBirthTime = nullableClock(profile, "reported_birth_time");
const activeBirthTime = nullableClock(profile, "active_birth_time");
const birthTimeSource = requiredText(profile, "birth_time_source");
const birthTimeStatus = requiredText(profile, "birth_time_status");
if (!allowedBirthTimeSources.has(birthTimeSource)
|| !allowedBirthTimeStatuses.has(birthTimeStatus)) {
throw new ConsultationProfileTruthError("profile_inconsistent");
}
const countryCode = requiredText(profile, "country_code");
const provinceCode = requiredText(profile, "province_code");
const cityCode = requiredText(profile, "city_code");
const districtValue = profile.district_code;
const districtCode = typeof districtValue === "string" && districtValue.trim()
? districtValue.trim()
: null;
const latitude = requiredFiniteNumber(profile, "latitude", -90, 90);
const longitude = requiredFiniteNumber(profile, "longitude", -180, 180);
const timezoneOffset = requiredFiniteNumber(profile, "timezone_offset", -12, 14);
const country = chinaLocations.country;
const province = country.provinces.find((candidate) => candidate.code === provinceCode);
const city = province?.cities.find((candidate) => candidate.code === cityCode);
const district = districtCode
? city?.districts.find((candidate) => candidate.code === districtCode)
: undefined;
if (countryCode !== country.code || !province || !city
|| (city.districts.length > 0 && !district)
|| (districtCode !== null && !district)) {
throw new ConsultationProfileTruthError("profile_inconsistent");
}
const location = district ?? city;
if (!sameCoordinate(latitude, location.center[1])
|| !sameCoordinate(longitude, location.center[0])
|| !sameCoordinate(timezoneOffset, country.timezone)) {
throw new ConsultationProfileTruthError("profile_inconsistent");
}
const placeLabel = [country.name, province.name, city.name, district?.name]
.filter((label, index, labels) => Boolean(label) && labels.indexOf(label) === index)
.join(" · ");
let selectedTime: string;
let selectedTimeKind: "reported" | "active";
if (mode === "verified_chart") {
if (birthTimeStatus !== "confirmed") {
throw new ConsultationProfileTruthError("mode_changed");
}
if (!activeBirthTime) throw new ConsultationProfileTruthError("profile_incomplete");
selectedTime = activeBirthTime;
selectedTimeKind = "active";
} else {
if (birthTimeStatus === "confirmed" || !concreteReportedSources.has(birthTimeSource)) {
throw new ConsultationProfileTruthError("mode_changed");
}
if (!reportedBirthTime) throw new ConsultationProfileTruthError("profile_incomplete");
selectedTime = reportedBirthTime;
selectedTimeKind = "reported";
}
const [hour, minute] = selectedTime.split(":").map(Number);
return Object.freeze({
name,
toolInput: Object.freeze({
year,
month,
day,
hour,
minute,
city: placeLabel,
lat: latitude,
lon: longitude,
tz: timezoneOffset,
}),
truth: Object.freeze({
birthDate,
reportedBirthTime,
activeBirthTime,
selectedTimeKind,
birthTimeSource,
birthTimeStatus,
placeLabel,
placeCodes: Object.freeze({
countryCode,
provinceCode,
cityCode,
districtCode,
}),
latitude,
longitude,
timezoneOffset,
}),
});
}
/**
* The route's pre-billing service boundary. Chart modes must load and resolve
* account truth successfully before the reservation callback can run.
*/
export async function prepareConsultationRoute<Reservation>(
input: PrepareConsultationRouteInput<Reservation>,
) {
let serverChart: ServerChartConsultation | null = null;
if (input.mode !== "general_no_birth_time") {
let profile: unknown;
try {
profile = await input.loadProfile(input.userId);
} catch (error) {
if (error instanceof ConsultationProfileTruthError) throw error;
throw new ConsultationProfileTruthError("profile_unavailable");
}
serverChart = serverChartFromProfile(profile, input.mode);
}
const reservation = await input.reserve();
return Object.freeze({ serverChart, reservation });
}
+86 -8
View File
@@ -11,6 +11,78 @@ type StreamTextResponseOptions = StreamHooks & {
readonly transformText?: (text: string) => string;
};
const hiddenBlockOpeners = [
"<!--AYANAM_SUGGESTIONS:",
"<!--AYANAM_TITLE:",
] as const;
function longestOpenerPrefixSuffix(value: string) {
const maximum = Math.min(
value.length,
Math.max(...hiddenBlockOpeners.map((opener) => opener.length - 1)),
);
for (let length = maximum; length > 0; length -= 1) {
const suffix = value.slice(-length);
if (hiddenBlockOpeners.some((opener) => opener.startsWith(suffix))) return length;
}
return 0;
}
/** Sends only visible prose through the output guard and preserves metadata bytes. */
function createVisibleTextTransformer(transform: (text: string) => string) {
let buffered = "";
let hidden = false;
function consume(value: string, final: boolean) {
buffered += value;
let output = "";
while (buffered) {
if (hidden) {
const closeIndex = buffered.indexOf("-->");
if (closeIndex < 0) {
if (final) {
output += buffered;
buffered = "";
}
break;
}
output += buffered.slice(0, closeIndex + 3);
buffered = buffered.slice(closeIndex + 3);
hidden = false;
continue;
}
const openerIndex = hiddenBlockOpeners.reduce<number>((earliest, opener) => {
const index = buffered.indexOf(opener);
return index >= 0 && (earliest < 0 || index < earliest) ? index : earliest;
}, -1);
if (openerIndex >= 0) {
if (openerIndex > 0) output += transform(buffered.slice(0, openerIndex));
buffered = buffered.slice(openerIndex);
hidden = true;
continue;
}
if (final) {
output += transform(buffered);
buffered = "";
break;
}
const retainedLength = longestOpenerPrefixSuffix(buffered);
const visibleLength = buffered.length - retainedLength;
if (visibleLength > 0) output += transform(buffered.slice(0, visibleLength));
buffered = buffered.slice(visibleLength);
break;
}
return output;
}
return Object.freeze({
push: (value: string) => consume(value, false),
finish: (value: string) => consume(value, true),
});
}
export function streamTextResponse(
stream: AsyncIterable<string>,
options: StreamTextResponseOptions,
@@ -20,6 +92,9 @@ export function streamTextResponse(
// Keep a full natural-language clause unflushed so a later stream chunk cannot
// turn an allowed prefix into a disallowed timing or guaranteed conclusion.
const guardTailLength = options.transformText ? 1024 : 0;
const visibleTransformer = options.transformText
? createVisibleTextTransformer(options.transformText)
: null;
let pending = "";
let settled = false;
let emitted = false;
@@ -30,10 +105,10 @@ export function streamTextResponse(
while (true) {
const { done, value } = await iterator.next();
if (done) {
if (pending)
controller.enqueue(
encoder.encode(options.transformText?.(pending) ?? pending),
);
const finalText = visibleTransformer
? visibleTransformer.finish(pending)
: pending;
if (finalText) controller.enqueue(encoder.encode(finalText));
settled = true;
if (!emitted) {
const error = new Error("empty_stream");
@@ -52,10 +127,13 @@ export function streamTextResponse(
const stableLength = pending.length - guardTailLength;
const stable = pending.slice(0, stableLength);
pending = pending.slice(stableLength);
controller.enqueue(
encoder.encode(options.transformText?.(stable) ?? stable),
);
return;
const transformed = visibleTransformer
? visibleTransformer.push(stable)
: stable;
if (transformed) {
controller.enqueue(encoder.encode(transformed));
return;
}
}
} catch (error) {
if (!settled) {
+28
View File
@@ -11,6 +11,23 @@ const guaranteeConclusionPatterns = [
/(?:^|[.?!\n])[^.?!\n]*\b(?:will definitely|guaranteed? to|certain to|without doubt)\b[^.?!\n]*/gi,
];
const personalChartClaimMarkers = [
String.raw`(?:基于|根据|从|结合)\s*(?:你|您)\s*(?:的\s*)?(?:个人\s*)?(?:星盘|命盘|出生盘|本命盘|盘)`,
String.raw`(?:你|您)\s*的\s*(?:(?:D\s*\d+)(?:\s*上升)?|上升(?:星座)?|月亮星座|太阳星座|太阳|月亮|火星|水星|木星|金星|土星|罗喉|凯图|Rahu|Ketu|第\s*[一二三四五六七八九十百0-9]+\s*宫|星盘|命盘|出生盘|本命盘|盘)`,
String.raw`(?:你|您)\s*(?:的\s*)?(?:(?:D\s*\d+)(?:\s*上升)?|上升(?:星座)?|月亮星座|太阳星座|太阳|月亮|火星|水星|木星|金星|土星|罗喉|凯图|Rahu|Ketu|第\s*[一二三四五六七八九十百0-9]+\s*宫|星盘|命盘|本命盘|盘)\s*(?:(?:一定|必然|肯定|必定|绝对)\s*)?(?:是|在|落(?:在|入)?|位于|显示|表明|说明|意味着|主宰)`,
String.raw`(?:你|您)\s*(?:的\s*)?(?:星盘|命盘|出生盘|本命盘|盘)\s*(?:中|里|内)`,
String.raw`(?:D\s*\d+|上升(?:星座)?)\s*(?:显示|表明|说明|意味着)\s*(?:你|您)`,
String.raw`(?:your|the user's)\s+(?:natal\s+|birth\s+)?(?:chart|ascendant|D\s*\d+|\d+(?:st|nd|rd|th)\s+house)`,
];
const personalChartClaimPatterns = personalChartClaimMarkers.map((marker) => new RegExp(
String.raw`(^|[。!?.!?\n])[^。!?.!?\n]*${marker}[^。!?.!?\n]*`,
"giu",
));
export const GENERAL_NO_BIRTH_TIME_REFUSAL =
"当前一般咨询模式不能生成个人星盘结论;你可以改问一般知识,或先完成生时校正";
/** Removes claims the evidence contract does not permit the model to make. */
export function guardPreciseTimingOutput(text: string) {
let guarded = text;
@@ -24,3 +41,14 @@ export function guardPreciseTimingOutput(text: string) {
}
return guarded;
}
/** A deterministic post-model boundary for the zero-chart general mode. */
export function guardGeneralNoBirthTimeOutput(text: string) {
let guarded = guardPreciseTimingOutput(text);
for (const pattern of personalChartClaimPatterns) {
guarded = guarded.replace(pattern, (_sentence, prefix: string) => (
`${prefix}${GENERAL_NO_BIRTH_TIME_REFUSAL}`
));
}
return guarded;
}
-2
View File
@@ -255,8 +255,6 @@ export function getGeneralJyotishAgent(model: ResolvedLanguageModel) {
name: "Jyotisha General Guide",
model: model.model,
instructions: generalJyotishInstructions,
skills: [jyotishSkillPath],
tools: {},
});
generalJyotishAgents.set(model.id, agent);
return agent;