Files
Jyotisha/frontend/src/lib/timing-output-guard.ts
T
jesse-ux e32ce6247e fix(consult): BUG-945~949 领域截断、思考分片、Pass 4 按模式分流
schema 上限与执行上限解耦;校正思考改分片门;日期观察不删字,保证句与无分钟个人盘退回重写。
2026-09-18 13:17:35 +08:00

230 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const exactTimingPatterns = [
/\b(?:19|20)\d{2}[-/.年]\s?\d{1,2}(?:[-/.月]\s?\d{1,2}(?:日|号)?)?\b/g,
/\b(?:19|20)\d{2}\s+(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)(?:\s+\d{1,2})?\b/gi,
/\b\d{1,2}\/\d{1,2}\/(?:\d{2}|\d{4})\b/g,
/(?:今年|明年|后年|(?:19|20)\d{2}年)?\s*\d{1,2}月(?:\s*\d{1,2}[日号])?/g,
/\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:,\s*(?:19|20)\d{2})?\b/gi,
];
const guaranteeConclusionPatterns = [
/(?:^|[。!?.!?\n])[^。!?.!?\n]*(?:一定|必然|保证|肯定|必定|注定|绝对)(?:会|能|将|发生|成功|结婚|复合|怀孕|发财|升职|得到|实现|出现)[^。!?.!?\n]*/g,
/(?:^|[.?!\n])[^.?!\n]*\b(?:will definitely|guaranteed? to|certain to|without doubt)\b[^.?!\n]*/gi,
];
// The boundary is structural: an educational question frame is removed first,
// then a personal subject/context, chart object, and chart predicate must agree.
// This avoids treating every sentence that merely contains `你` and `宫` as a
// personal chart claim.
const chineseZodiacSignSource = String.raw`(?:白羊|金牛|双子|巨蟹|狮子|处女|天秤|天蝎|射手|摩羯|水瓶|双鱼)`;
const chineseNamedHouseSource = String.raw`(?:命|财帛|兄弟|田宅|子女|夫妻|婚姻|疾厄|迁移|事业|官禄|交友|仆役|福德|父母)宫`;
const chineseChartObjectSource = String.raw`(?:盘面?|星盘|命盘|出生盘|本命盘|D\s*\d+|上升(?:星座)?|星座|${chineseZodiacSignSource}(?:座|上升)|(?:第\s*)?[一二三四五六七八九十百0-9]+\s*宫|${chineseNamedHouseSource}|宫位|行星|星体|太阳|月亮|火星|水星|木星|金星|土星|罗喉|凯图|Rahu|Ketu)`;
const chineseChartObjectPattern = new RegExp(chineseChartObjectSource, "iu");
const chineseChartPredicatePattern = /(?:落(?:在|入|座)?|位于|进入|为|是|有|显示|表明|说明|意味着|主宰|很?强|很?弱|旺|受克|有力|无力|[::])/iu;
const chineseOwnedChartSubjectPattern = new RegExp(
String.raw`(?:你|您)\s*(?:的|个人(?:的)?)?\s*${chineseChartObjectSource}`,
"iu",
);
const chineseUserHasChartPattern = new RegExp(
String.raw`(?:你|您)\s*(?:为|是|有|拥有)\s*(?:一(?:个|颗)\s*)?${chineseChartObjectSource}`,
"iu",
);
const chinesePersonalChartContextPattern = /(?:对(?:你|您)而言|基于(?:你|您)(?:的)?(?:盘面?|星盘|命盘|出生盘|本命盘)|在(?:你|您)(?:的)?(?:盘面?|星盘|命盘|出生盘|本命盘)(?:中|里)?)/iu;
const chineseChartAddressesUserPattern = new RegExp(
String.raw`${chineseChartObjectSource}\s*(?:显示|表明|说明|意味着)[^。!?.!?\n]{0,24}(?:你|您)`,
"iu",
);
const englishZodiacSignSource = String.raw`(?:aries|taurus|gemini|cancer|leo|virgo|libra|scorpio|sagittarius|capricorn|aquarius|pisces)`;
const englishChartObjectSource = String.raw`(?:natal\s+chart|birth\s+chart|chart|ascendant|rising(?:\s+sign)?|D\s*\d+(?:\s+chart)?|placement|(?:\d+(?:st|nd|rd|th)|[a-z]+)\s+house|house|planet|sun|moon|mars|mercury|jupiter|venus|saturn|rahu|ketu)`;
const englishChartObjectPattern = new RegExp(String.raw`\b${englishChartObjectSource}\b`, "iu");
const englishChartPredicatePattern = /(?:\b(?:has|have|is|are|occup(?:y|ies)|falls?|lands?|sits?|placed?|located?|shows?|indicates?|means?|rules?|strong|weak)\b|[::])/iu;
const englishOwnedChartSubjectPattern = new RegExp(
String.raw`\b(?:your|the\s+user(?:'s)?)\s+(?:personal\s+)?${englishChartObjectSource}\b`,
"iu",
);
const englishUserIsOrHasChartPattern = new RegExp(
String.raw`\byou\s+(?:are|have)\s+(?:an?\s+)?(?:${englishZodiacSignSource}\s+(?:rising|ascendant)|${englishChartObjectSource})\b`,
"iu",
);
const englishPersonalChartContextPattern = /\b(?:for\s+you|(?:in|from|based\s+on|according\s+to)\s+your\s+(?:natal\s+|birth\s+)?chart)\b/iu;
const englishChartAddressesUserPattern = new RegExp(
String.raw`\b${englishChartObjectSource}\s+(?:shows?|indicates?|means?)\s+(?:that\s+)?you\b`,
"iu",
);
const chineseEducationalFramePattern = /^(?:(?:你|您)(?:的)?问题(?:是|为)|(?:你|您)(?:所)?问(?:的)?(?:是)?)[\s,,::]*/iu;
const chineseAboutGeneralMeaningPattern = /^关于\s*(?:你|您)(?:的)?\s*(.+?(?:一般含义|一般意义))$/iu;
const chineseChartMetaReferencePattern = new RegExp(
String.raw`${chineseChartObjectSource}\s*(?:(?:的\s*)?一般问题|方面的问题|问题(?:的\s*)?提问者|(?:的\s*)?提问者)`,
"giu",
);
const englishEducationalFramePattern = /^(?:your\s+question\s+is|you\s+asked\s+about)\s*/iu;
function normalizeClaimClause(clause: string) {
let normalized = clause
.normalize("NFKC")
.replace(/[*_`~]+/gu, "")
.replace(/\s+/gu, " ")
.trim();
normalized = normalized
.replace(chineseEducationalFramePattern, "")
.replace(englishEducationalFramePattern, "")
.replace(chineseChartMetaReferencePattern, "咨询问题");
const aboutGeneralMeaning = chineseAboutGeneralMeaningPattern.exec(normalized);
return aboutGeneralMeaning?.[1] ?? normalized;
}
function isPersonalChartConclusion(clause: string) {
const normalized = normalizeClaimClause(clause);
const chineseStructure = (chineseOwnedChartSubjectPattern.test(normalized)
|| chineseUserHasChartPattern.test(normalized)
|| chinesePersonalChartContextPattern.test(normalized)
|| chineseChartAddressesUserPattern.test(normalized))
&& chineseChartObjectPattern.test(normalized)
&& chineseChartPredicatePattern.test(normalized);
const englishStructure = (englishOwnedChartSubjectPattern.test(normalized)
|| englishUserIsOrHasChartPattern.test(normalized)
|| englishPersonalChartContextPattern.test(normalized)
|| englishChartAddressesUserPattern.test(normalized))
&& englishChartObjectPattern.test(normalized)
&& englishChartPredicatePattern.test(normalized);
return chineseStructure || englishStructure;
}
export const GENERAL_NO_BIRTH_TIME_REFUSAL =
"这部分需要具体出生分钟才能判断,我不会补造时间";
export type TimingGuardKind = "exact-timing" | "guarantee" | "personal-chart" | "methodology";
export type TimingGuardViolation = Readonly<{
kind: TimingGuardKind;
excerpt: string;
}>;
function collectMatches(text: string, pattern: RegExp): string[] {
return [...text.matchAll(new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`))]
.map((match) => match[0]?.trim() ?? "")
.filter(Boolean);
}
export function detectPreciseTimingViolations(text: string): TimingGuardViolation[] {
const hits: TimingGuardViolation[] = [];
for (const pattern of exactTimingPatterns) {
for (const excerpt of collectMatches(text, pattern)) {
hits.push({ kind: "exact-timing", excerpt: excerpt.slice(0, 80) });
}
}
for (const pattern of guaranteeConclusionPatterns) {
for (const excerpt of collectMatches(text, pattern)) {
hits.push({ kind: "guarantee", excerpt: excerpt.slice(0, 80) });
}
}
return hits;
}
export function detectGeneralNoBirthTimeViolations(text: string): TimingGuardViolation[] {
const hits = detectPreciseTimingViolations(text);
for (const part of text.split(/[。!?.!?\n]+/u)) {
if (isPersonalChartConclusion(part)) {
hits.push({ kind: "personal-chart", excerpt: part.trim().slice(0, 80) });
}
}
return hits;
}
export function detectMethodologyBookkeeping(text: string): TimingGuardViolation[] {
const hits: TimingGuardViolation[] = [];
if (/##\s*统一参数/.test(text) || text.includes("统一参数与原始结构")) {
hits.push({ kind: "methodology", excerpt: "统一参数与原始结构" });
}
if (text.includes("技法审计表")) {
hits.push({ kind: "methodology", excerpt: "技法审计表" });
}
return hits;
}
export type Pass4Mode =
| "verified_chart"
| "unverified_birth_time"
| "declared_birth_window"
| "general_no_birth_time";
export type Pass4Step = Readonly<{
action: "observe" | "reject";
kind: TimingGuardKind;
excerpt: string;
}>;
export type Pass4Result = Readonly<{
text: string;
steps: readonly Pass4Step[];
retry: boolean;
retryHint?: string;
}>;
function dropGuaranteeClauses(text: string): string {
const parts = text.split(/([。!?.!?\n]+)/u);
let output = "";
for (let index = 0; index < parts.length; index += 2) {
const clause = parts[index] ?? "";
const punct = parts[index + 1] ?? "";
if (detectPreciseTimingViolations(clause).some((item) => item.kind === "guarantee")) continue;
output += clause + punct;
}
return output;
}
export function classifyPass4(
text: string,
mode: Pass4Mode,
): Pass4Step[] {
const steps: Pass4Step[] = [];
for (const hit of detectPreciseTimingViolations(text)) {
if (hit.kind === "exact-timing") {
if (mode !== "general_no_birth_time") {
steps.push({ action: "observe", kind: hit.kind, excerpt: hit.excerpt });
}
continue;
}
if (hit.kind === "guarantee") {
steps.push({ action: "reject", kind: hit.kind, excerpt: hit.excerpt });
}
}
if (mode === "general_no_birth_time") {
for (const hit of detectGeneralNoBirthTimeViolations(text)) {
if (hit.kind === "personal-chart") {
steps.push({ action: "reject", kind: hit.kind, excerpt: hit.excerpt });
}
}
}
for (const hit of detectMethodologyBookkeeping(text)) {
steps.push({ action: "reject", kind: hit.kind, excerpt: hit.excerpt });
}
return steps;
}
export function applyPass4Policy(
text: string,
mode: Pass4Mode,
options?: { secondPass?: boolean },
): Pass4Result {
const steps = classifyPass4(text, mode);
const rejects = steps.filter((step) => step.action === "reject");
if (!options?.secondPass && rejects.some((step) => step.kind === "guarantee" || step.kind === "personal-chart")) {
return {
text,
steps,
retry: true,
retryHint: "不要写保证性结论(一定、保证、注定、will definitely)。无出生分钟时不要对用户作个人星盘断言。",
};
}
let next = text;
if (options?.secondPass && rejects.some((step) => step.kind === "guarantee")) {
next = dropGuaranteeClauses(next);
}
if (options?.secondPass && mode === "general_no_birth_time" && rejects.some((step) => step.kind === "personal-chart")) {
next = GENERAL_NO_BIRTH_TIME_REFUSAL;
}
return { text: next, steps, retry: false };
}