diff --git a/frontend/src/app/api/synastry/route.ts b/frontend/src/app/api/synastry/route.ts index 39c4a1ee..9396a731 100644 --- a/frontend/src/app/api/synastry/route.ts +++ b/frontend/src/app/api/synastry/route.ts @@ -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, selfD9: Record, division: string, planet: string) { + const result = varga.result && typeof varga.result === "object" ? varga.result as Record : {}; + const chart = result[division] && typeof result[division] === "object" ? result[division] as Record : {}; + const planets = chart.planets && typeof chart.planets === "object" ? chart.planets as Record : {}; + return planetSign(planet === "Ascendant" ? chart.ascendant : planets[planet]); +} + +function businessReport(selfVargas: Record, partnerVargas: Record) { + 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, diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index f5b81a8d..81b80a66 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -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(null); const [chartLibrary, setChartLibrary] = useState([]); const [chartLibraryOpen, setChartLibraryOpen] = useState(false); + const [synastryRelationshipType, setSynastryRelationshipType] = useState("romance"); const [synastryPendingId, setSynastryPendingId] = useState(null); const [otherProfileDraft, setOtherProfileDraft] = useState(emptyProfile); const [synastryReportCard, setSynastryReportCard] = useState(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() { {record.profile.date} {record.profile.time} · {profilePlaceLabel(record.profile)}
- + +
diff --git a/frontend/tests/chart-library-other-profile.test.ts b/frontend/tests/chart-library-other-profile.test.ts index 7ed8d5fc..1bfb59a4 100644 --- a/frontend/tests/chart-library-other-profile.test.ts +++ b/frontend/tests/chart-library-other-profile.test.ts @@ -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"/); +});