feat: add relationship-aware synastry entry (#17)
This commit is contained in:
@@ -11,6 +11,8 @@ type Profile = {
|
||||
districtCode?: string;
|
||||
};
|
||||
|
||||
type RelationshipType = "romance" | "business" | "family" | "general";
|
||||
|
||||
const apiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
|
||||
const china = chinaLocations.country;
|
||||
|
||||
@@ -102,6 +104,30 @@ function relationshipReport(synastry: Record<string, unknown>, selfD9: Record<st
|
||||
};
|
||||
}
|
||||
|
||||
function vargaPlanetSign(varga: Record<string, unknown>, division: string, planet: string) {
|
||||
const result = varga.result && typeof varga.result === "object" ? varga.result as Record<string, unknown> : {};
|
||||
const chart = result[division] && typeof result[division] === "object" ? result[division] as Record<string, unknown> : {};
|
||||
const planets = chart.planets && typeof chart.planets === "object" ? chart.planets as Record<string, unknown> : {};
|
||||
return planetSign(planet === "Ascendant" ? chart.ascendant : planets[planet]);
|
||||
}
|
||||
|
||||
function businessReport(selfVargas: Record<string, unknown>, partnerVargas: Record<string, unknown>) {
|
||||
return {
|
||||
status: "partial_evidence",
|
||||
scoreBand: "not_scored",
|
||||
headline: "已完成基础合作结构筛查;这不是合作成败、收益或契约保证。",
|
||||
strengths: [
|
||||
`D10 事业轴:本人 ${vargaPlanetSign(selfVargas, "D10_Dasamsa", "Ascendant")} / 对方 ${vargaPlanetSign(partnerVargas, "D10_Dasamsa", "Ascendant")}`,
|
||||
`D2 财富轴:本人 Moon ${vargaPlanetSign(selfVargas, "D2_Hora", "Moon")} / 对方 Moon ${vargaPlanetSign(partnerVargas, "D2_Hora", "Moon")}`,
|
||||
`D11 收益轴:本人 Sun ${vargaPlanetSign(selfVargas, "D11_Rudramsa", "Sun")} / 对方 Sun ${vargaPlanetSign(partnerVargas, "D11_Rudramsa", "Sun")}`,
|
||||
],
|
||||
risks: [
|
||||
"尚未完成 A10、功能吉凶、双方 Vimshottari + Narayana、Shadbala/AV 与外部数值一致性,不得据此断言合作结果或精确时点。",
|
||||
],
|
||||
nextEvidence: ["A10", "功能吉凶", "双方 Vimshottari + Narayana", "D10/D2/D11 原始度数与外部校验"],
|
||||
};
|
||||
}
|
||||
|
||||
async function postPython(path: string, body: unknown) {
|
||||
const response = await fetch(`${apiBase}${path}`, {
|
||||
method: "POST",
|
||||
@@ -118,12 +144,37 @@ async function postPython(path: string, body: unknown) {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json().catch(() => null) as { selfProfile?: Profile; partnerProfile?: Profile } | null;
|
||||
const body = await request.json().catch(() => null) as { selfProfile?: Profile; partnerProfile?: Profile; relationshipType?: RelationshipType } | null;
|
||||
if (!body?.selfProfile || !body.partnerProfile) {
|
||||
return NextResponse.json({ error: "请提供双方星盘资料" }, { status: 400 });
|
||||
}
|
||||
const selfChart = await postPython("/api/chart", birthPayload(body.selfProfile));
|
||||
const partnerChart = await postPython("/api/chart", birthPayload(body.partnerProfile));
|
||||
const relationshipType = body.relationshipType === "business" || body.relationshipType === "family" || body.relationshipType === "general"
|
||||
? body.relationshipType
|
||||
: "romance";
|
||||
if (relationshipType === "business") {
|
||||
const [selfVargas, partnerVargas] = await Promise.all([
|
||||
postPython("/api/varga_full", { ...birthPayload(body.selfProfile), planets: selfChart.planets, ascendant: selfChart.ascendant, divisions: ["D2", "D10", "D11"] }),
|
||||
postPython("/api/varga_full", { ...birthPayload(body.partnerProfile), planets: partnerChart.planets, ascendant: partnerChart.ascendant, divisions: ["D2", "D10", "D11"] }),
|
||||
]);
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
relationshipType,
|
||||
claimStatus: "partial",
|
||||
method: "d2_d10_d11_business_screening_partial",
|
||||
evidenceLayers: ["d2_hora", "d10_dashamsa", "d11_ekadashamsa"],
|
||||
blockedLayers: ["A10", "functional_benefic_malefic", "vimshottari_narayana", "shadbala_ashtakavarga", "external_engine_parity"],
|
||||
relationshipReport: businessReport(selfVargas, partnerVargas),
|
||||
});
|
||||
}
|
||||
if (relationshipType !== "romance") {
|
||||
return NextResponse.json({
|
||||
status: "blocked",
|
||||
relationshipType,
|
||||
message: "该关系类型尚无可验证的专用合盘计算合同;已保留问题草稿。",
|
||||
});
|
||||
}
|
||||
const selfD9 = await postPython("/api/varga_full", {
|
||||
...birthPayload(body.selfProfile),
|
||||
planets: selfChart.planets,
|
||||
|
||||
+30
-14
@@ -89,6 +89,7 @@ type ChartLibraryRecord = {
|
||||
profile: Profile;
|
||||
updatedAt: number;
|
||||
};
|
||||
type SynastryRelationshipType = "romance" | "business" | "family" | "general";
|
||||
type ChartLibraryApiRecord = {
|
||||
id: string;
|
||||
role: "self" | "other";
|
||||
@@ -355,12 +356,18 @@ function profilePlaceLabel(profile: Profile) {
|
||||
return selectedBirthPlace(profile)?.label || "地点未完整";
|
||||
}
|
||||
|
||||
function buildSynastryQuestion(selfProfile: Profile, partnerProfile: Profile) {
|
||||
function buildSynastryQuestion(selfProfile: Profile, partnerProfile: Profile, relationshipType: SynastryRelationshipType) {
|
||||
const relationshipLabel = relationshipType === "business" ? "商业合作" : relationshipType === "family" ? "亲友/家庭" : relationshipType === "general" ? "其他关系" : "婚恋";
|
||||
const evidenceRequest = relationshipType === "business"
|
||||
? "请先说明 D2/D10/D11 已用层与 A10、双方 Dasha/Narayana、功能吉凶等缺失层;不得给出合作成败、收益保证或精确时点。"
|
||||
: relationshipType === "romance"
|
||||
? "请先说明会使用哪些证据层,再分析关系模式、冲突点、适合发展的方式和需要谨慎的时间窗口。"
|
||||
: "请先说明当前缺少专用合盘计算合同,只基于可验证资料提出需要补充的现实关系信息,不作确定性判断。";
|
||||
return [
|
||||
`请用印度占星合盘分析我和${partnerProfile.name || "对方"}的关系。`,
|
||||
`请用印度占星分析我和${partnerProfile.name || "对方"}的${relationshipLabel}关系。`,
|
||||
`我的资料:${selfProfile.name || "本人"},${selfProfile.date} ${selfProfile.time},${profilePlaceLabel(selfProfile)}。`,
|
||||
`对方资料:${partnerProfile.name || "对方"},${partnerProfile.date} ${partnerProfile.time},${profilePlaceLabel(partnerProfile)}。`,
|
||||
"请先说明会使用哪些证据层,再分析关系模式、冲突点、适合发展的方式和需要谨慎的时间窗口。",
|
||||
evidenceRequest,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -668,6 +675,7 @@ export default function Home() {
|
||||
const [activeAccountDialog, setActiveAccountDialog] = useState<AccountDialog | null>(null);
|
||||
const [chartLibrary, setChartLibrary] = useState<ChartLibraryRecord[]>([]);
|
||||
const [chartLibraryOpen, setChartLibraryOpen] = useState(false);
|
||||
const [synastryRelationshipType, setSynastryRelationshipType] = useState<SynastryRelationshipType>("romance");
|
||||
const [synastryPendingId, setSynastryPendingId] = useState<string | null>(null);
|
||||
const [otherProfileDraft, setOtherProfileDraft] = useState<Profile>(emptyProfile);
|
||||
const [synastryReportCard, setSynastryReportCard] = useState<SynastryReportCard | null>(null);
|
||||
@@ -1461,9 +1469,8 @@ export default function Home() {
|
||||
setOtherProfileDraft(emptyProfile);
|
||||
if (cloudSaved) {
|
||||
setAccountError("");
|
||||
setProfileNotice("已保存到云端星盘库。");
|
||||
setProfileNotice("已保存到云端星盘库。请选择关系类型后点击“用于合盘”。");
|
||||
}
|
||||
await draftSynastryQuestionFromChart(record);
|
||||
}
|
||||
|
||||
async function deleteOtherChart(recordId: string) {
|
||||
@@ -1745,24 +1752,27 @@ export default function Home() {
|
||||
);
|
||||
}
|
||||
|
||||
async function draftSynastryQuestionFromChart(record: ChartLibraryRecord) {
|
||||
async function draftSynastryQuestionFromChart(record: ChartLibraryRecord, relationshipType: SynastryRelationshipType) {
|
||||
if (record.role !== "other") return;
|
||||
if (synastryPendingId) return;
|
||||
const baseQuestion = buildSynastryQuestion(profile, record.profile);
|
||||
const baseQuestion = buildSynastryQuestion(profile, record.profile, relationshipType);
|
||||
setSynastryPendingId(record.id);
|
||||
setComposerNotice("正在计算基础合盘证据,请稍候。");
|
||||
try {
|
||||
const response = await fetch("/api/synastry", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ selfProfile: profile, partnerProfile: record.profile }),
|
||||
body: JSON.stringify({ selfProfile: profile, partnerProfile: record.profile, relationshipType }),
|
||||
});
|
||||
const payload = await response.json().catch(() => null) as { status?: string; evidenceLayers?: string[]; synastry?: { total_score?: number; max_score?: number; assessment?: string }; relationshipReport?: { headline?: string; scoreBand?: string; strengths?: string[]; risks?: string[]; nextEvidence?: string[] } } | null;
|
||||
const payload = await response.json().catch(() => null) as { status?: string; claimStatus?: string; blockedLayers?: string[]; evidenceLayers?: string[]; synastry?: { total_score?: number; max_score?: number; assessment?: string }; relationshipReport?: { headline?: string; scoreBand?: string; strengths?: string[]; risks?: string[]; nextEvidence?: string[] } } | null;
|
||||
if (response.ok && payload?.status === "ok") {
|
||||
const score = payload.synastry?.total_score;
|
||||
const max = payload.synastry?.max_score;
|
||||
const assessment = payload.synastry?.assessment;
|
||||
const layers = (payload.evidenceLayers || []).join(" / ") || "Ashtakoot / Moon / D9";
|
||||
const evidenceSummary = relationshipType === "business"
|
||||
? `已完成基础商业合作证据筛查:${layers};声明状态:${payload.claimStatus || "partial"};未用层:${(payload.blockedLayers || []).join(" / ") || "A10 / 双方 Dasha-Narayana / 功能吉凶"}。请勿将其表述为合作保证或精确时点。`
|
||||
: `已计算基础合盘证据:${layers};Ashtakoot ${score ?? "?"}/${max ?? "?"},初步评级:${assessment || "待解释"}。请基于这个证据包继续分析。`;
|
||||
const reportCard: SynastryReportCard = {
|
||||
id: `${record.id}-${Date.now()}`,
|
||||
partnerName: record.profile.name || "对方",
|
||||
@@ -1795,15 +1805,15 @@ export default function Home() {
|
||||
chooseSuggestedQuestion([
|
||||
baseQuestion,
|
||||
"",
|
||||
`已计算基础合盘证据:${layers};Ashtakoot ${score ?? "?"}/${max ?? "?"},初步评级:${assessment || "待解释"}。请基于这个证据包继续分析。`,
|
||||
evidenceSummary,
|
||||
payload.relationshipReport?.headline ? `结构化摘要:${payload.relationshipReport.headline}` : "",
|
||||
].join("\n"), "marriage");
|
||||
].join("\n"), relationshipType === "business" ? "career" : "marriage");
|
||||
} else {
|
||||
chooseSuggestedQuestion(baseQuestion, "marriage");
|
||||
chooseSuggestedQuestion(baseQuestion, relationshipType === "business" ? "career" : "marriage");
|
||||
setComposerNotice(payload?.status === "blocked" ? "合盘计算暂时不可用,已先生成问题草稿。" : "已生成合盘问题草稿。");
|
||||
}
|
||||
} catch {
|
||||
chooseSuggestedQuestion(baseQuestion, "marriage");
|
||||
chooseSuggestedQuestion(baseQuestion, relationshipType === "business" ? "career" : "marriage");
|
||||
setComposerNotice("合盘计算暂时不可用,已先生成问题草稿。");
|
||||
} finally {
|
||||
setSynastryPendingId(null);
|
||||
@@ -2600,7 +2610,13 @@ export default function Home() {
|
||||
<small>{record.profile.date} {record.profile.time} · {profilePlaceLabel(record.profile)}</small>
|
||||
</div>
|
||||
<div className="chart-library-actions">
|
||||
<button className="button-secondary" type="button" onClick={() => void draftSynastryQuestionFromChart(record)} disabled={synastryPendingId !== null}>{synastryPendingId === record.id ? "正在计算合盘..." : "用于合盘"}</button>
|
||||
<select aria-label="关系类型" value={synastryRelationshipType} onChange={(event) => setSynastryRelationshipType(event.target.value as SynastryRelationshipType)} disabled={synastryPendingId !== null}>
|
||||
<option value="romance">婚恋</option>
|
||||
<option value="business">商业合作</option>
|
||||
<option value="family">亲友/家庭</option>
|
||||
<option value="general">其他关系</option>
|
||||
</select>
|
||||
<button className="button-secondary" type="button" onClick={() => void draftSynastryQuestionFromChart(record, synastryRelationshipType)} disabled={synastryPendingId !== null}>{synastryPendingId === record.id ? "正在计算合盘..." : "用于合盘"}</button>
|
||||
<button className="button-secondary" type="button" onClick={() => void makeDefaultChart(record)} disabled={profileSaving}>设为默认</button>
|
||||
<button className="button-secondary danger-button" type="button" onClick={() => deleteOtherChart(record.id)}>删除</button>
|
||||
</div>
|
||||
|
||||
@@ -23,8 +23,9 @@ test("other chart save falls back to local library when cloud sync fails", () =>
|
||||
assert.doesNotMatch(source, /deleteOtherChart[\s\S]{0,500}return;\s*}\s*setChartLibrary/);
|
||||
});
|
||||
|
||||
test("adding another chart immediately opens the synastry path", () => {
|
||||
assert.match(source, /async function saveOtherChart[\s\S]{0,1400}await draftSynastryQuestionFromChart\(record\)/);
|
||||
test("adding another chart waits for the user to choose a relationship type", () => {
|
||||
assert.doesNotMatch(source, /async function saveOtherChart[\s\S]{0,1400}await draftSynastryQuestionFromChart\(/);
|
||||
assert.match(source, /请选择关系类型后点击“用于合盘”。/);
|
||||
assert.match(source, /用于合盘/);
|
||||
assert.match(source, /\/api\/synastry/);
|
||||
});
|
||||
@@ -55,3 +56,22 @@ test("synastry selection exposes a pending state while the evidence packet is co
|
||||
assert.match(source, /disabled=\{synastryPendingId !== null\}/);
|
||||
assert.match(source, /synastryPendingId === record\.id \? "正在计算合盘\.\.\." : "用于合盘"/);
|
||||
});
|
||||
|
||||
test("relationship intent selects domain-specific evidence instead of treating every pairing as romance", () => {
|
||||
const route = readFileSync(new URL("../src/app/api/synastry/route.ts", import.meta.url), "utf8");
|
||||
assert.match(source, /商业合作/);
|
||||
assert.match(source, /亲友\/家庭/);
|
||||
assert.match(source, /其他关系/);
|
||||
assert.match(source, /relationshipType: SynastryRelationshipType/);
|
||||
assert.match(source, /body: JSON\.stringify\(\{ selfProfile: profile, partnerProfile: record\.profile, relationshipType \}\)/);
|
||||
assert.match(route, /relationshipType === "business"/);
|
||||
assert.match(route, /divisions: \["D2", "D10", "D11"\]/);
|
||||
assert.match(route, /"D10_Dasamsa"/);
|
||||
assert.match(route, /"D11_Rudramsa"/);
|
||||
assert.match(route, /blockedLayers:/);
|
||||
assert.match(route, /"A10"/);
|
||||
assert.match(route, /"functional_benefic_malefic"/);
|
||||
assert.match(route, /"vimshottari_narayana"/);
|
||||
assert.match(source, /基础商业合作证据筛查/);
|
||||
assert.match(source, /relationshipType === "business"/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user