guanwei 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +202 -0
- package/README.md +247 -0
- package/dist/assets/index-CsOD5rSo.js +102 -0
- package/dist/assets/index-CsOD5rSo.js.map +1 -0
- package/dist/assets/index-DQx62TUr.css +1 -0
- package/dist/favicon.svg +12 -0
- package/dist/index.html +15 -0
- package/eslint.config.js +28 -0
- package/index.html +25 -0
- package/package.json +96 -0
- package/postcss.config.js +10 -0
- package/public/favicon.svg +12 -0
- package/scripts/guanwei +245 -0
- package/scripts/install.sh +79 -0
- package/scripts/release.sh +42 -0
- package/scripts/setup.bat +36 -0
- package/scripts/setup.sh +123 -0
- package/server/src/data/db.json +12059 -0
- package/server/src/data/guanwei.db +0 -0
- package/server/src/data/lifeContext.ts +423 -0
- package/server/src/data/spreads.ts +255 -0
- package/server/src/data/tarotCards.ts +2353 -0
- package/server/src/index.ts +71 -0
- package/server/src/routes/ai.ts +376 -0
- package/server/src/routes/divine.ts +140 -0
- package/server/src/routes/hour.ts +40 -0
- package/server/src/routes/tarot.ts +129 -0
- package/server/src/routes/users.ts +140 -0
- package/server/src/services/chartBrief.ts +131 -0
- package/server/src/services/divineStore.ts +188 -0
- package/server/src/services/hourInference.ts +150 -0
- package/server/src/services/llmProvider.ts +181 -0
- package/server/src/services/promptBuilder.ts +327 -0
- package/server/src/services/relativesCheck.ts +139 -0
- package/server/src/services/sixRelatives.ts +70 -0
- package/server/src/services/skills.ts +116 -0
- package/server/src/types/index.ts +104 -0
- package/server/src/utils/semanticAnalyzer.ts +555 -0
- package/server/src/utils/tarotEngine.ts +849 -0
- package/server/tsconfig.json +18 -0
- package/shared/core/data/classics.ts +276 -0
- package/shared/core/data/courses.ts +211 -0
- package/shared/core/data/duanyu.ts +148 -0
- package/shared/core/data/ganzhi.ts +53 -0
- package/shared/core/data/gua64.ts +93 -0
- package/shared/core/data/liuren.ts +18 -0
- package/shared/core/data/liuyao.ts +73 -0
- package/shared/core/data/qimen.ts +27 -0
- package/shared/core/data/region-full.ts +3 -0
- package/shared/core/data/region.ts +310 -0
- package/shared/core/data/tarotSpreads.ts +78 -0
- package/shared/core/data/xiaoliuren.ts +18 -0
- package/shared/core/data/ziwei.ts +97 -0
- package/shared/core/data/zodiac.ts +20 -0
- package/shared/core/engine/astrology.ts +169 -0
- package/shared/core/engine/bazi.ts +306 -0
- package/shared/core/engine/calendar.ts +127 -0
- package/shared/core/engine/daily.ts +133 -0
- package/shared/core/engine/liuren.ts +86 -0
- package/shared/core/engine/liuyao.ts +93 -0
- package/shared/core/engine/meihua.ts +87 -0
- package/shared/core/engine/qimen.ts +78 -0
- package/shared/core/engine/questionFit.ts +94 -0
- package/shared/core/engine/tarot.ts +69 -0
- package/shared/core/engine/trueSolarTime.ts +86 -0
- package/shared/core/engine/xiaoliuren.ts +19 -0
- package/shared/core/engine/ziwei.ts +217 -0
- package/shared/core/engine/ziweiInterpret.ts +161 -0
- package/shared/core/types.ts +206 -0
- package/src/App.tsx +69 -0
- package/src/arts/registry.ts +31 -0
- package/src/assets/react.svg +1 -0
- package/src/components/Backdrop.tsx +27 -0
- package/src/components/BambooArt.tsx +41 -0
- package/src/components/CalendarInput.tsx +131 -0
- package/src/components/DateInput.tsx +103 -0
- package/src/components/Disclaimer.tsx +12 -0
- package/src/components/Empty.tsx +8 -0
- package/src/components/ErrorBoundary.tsx +38 -0
- package/src/components/HomeSideAnchor.tsx +62 -0
- package/src/components/InkVein.tsx +10 -0
- package/src/components/LifeEventsInput.tsx +47 -0
- package/src/components/LocationPicker.tsx +117 -0
- package/src/components/Motes.tsx +65 -0
- package/src/components/Navigation.tsx +80 -0
- package/src/components/ProfileForm.tsx +40 -0
- package/src/components/ProfilePicker.tsx +40 -0
- package/src/components/QuestionFields.tsx +47 -0
- package/src/components/ReportView.tsx +211 -0
- package/src/components/ResultCard.tsx +40 -0
- package/src/components/SealButton.tsx +18 -0
- package/src/components/SiteFooter.tsx +17 -0
- package/src/components/SiteNav.tsx +103 -0
- package/src/components/SongDialog.tsx +64 -0
- package/src/components/SongSearchSelect.tsx +81 -0
- package/src/components/SongSelect.tsx +61 -0
- package/src/components/StarField.tsx +119 -0
- package/src/components/TarotCard.tsx +129 -0
- package/src/components/TimeShichenInput.tsx +29 -0
- package/src/components/arts/AstrologyArt.tsx +141 -0
- package/src/components/arts/BaziArt.tsx +269 -0
- package/src/components/arts/LiurenArt.tsx +87 -0
- package/src/components/arts/LiuyaoArt.tsx +111 -0
- package/src/components/arts/MeihuaArt.tsx +105 -0
- package/src/components/arts/QimenArt.tsx +86 -0
- package/src/components/arts/TarotArt.tsx +110 -0
- package/src/components/arts/XiaoliurenArt.tsx +93 -0
- package/src/components/arts/ZiweiArt.tsx +296 -0
- package/src/data/arts.ts +84 -0
- package/src/data/lifeContext.ts +423 -0
- package/src/data/sample-report.json +146 -0
- package/src/data/shichen.ts +21 -0
- package/src/data/spreads.ts +255 -0
- package/src/data/tarotCards.ts +2353 -0
- package/src/hooks/useAIInterpret.ts +76 -0
- package/src/hooks/useDivine.ts +39 -0
- package/src/hooks/useTheme.ts +29 -0
- package/src/index.css +190 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +12 -0
- package/src/pages/AboutPage.tsx +25 -0
- package/src/pages/AcademyPage.tsx +128 -0
- package/src/pages/AstrologyPage.tsx +33 -0
- package/src/pages/AuthPage.tsx +263 -0
- package/src/pages/BaZiPage.tsx +33 -0
- package/src/pages/ClassicsPage.tsx +120 -0
- package/src/pages/DemoPage.tsx +161 -0
- package/src/pages/HistoryPage.tsx +133 -0
- package/src/pages/Home.tsx +179 -0
- package/src/pages/HomePage.tsx +174 -0
- package/src/pages/ModulePage.tsx +320 -0
- package/src/pages/SpreadEditor.tsx +230 -0
- package/src/pages/SpreadEditorPage.tsx +105 -0
- package/src/pages/SpreadLibrary.tsx +199 -0
- package/src/pages/SpreadLibraryPage.tsx +17 -0
- package/src/pages/TarotPage.tsx +738 -0
- package/src/pages/ZiWeiPage.tsx +33 -0
- package/src/services/api.ts +342 -0
- package/src/stores/useDivinationStore.ts +22 -0
- package/src/styles/song.css +1066 -0
- package/src/styles/tokens.css +60 -0
- package/src/types/index.ts +104 -0
- package/src/utils/astrologyCalc.ts +61 -0
- package/src/utils/comboEngine.ts +134 -0
- package/src/utils/dst.ts +24 -0
- package/src/utils/panTone.ts +206 -0
- package/src/utils/recordStore.ts +60 -0
- package/src/utils/semanticAnalyzer.ts +555 -0
- package/src/utils/spreadStorage.ts +32 -0
- package/src/utils/storage.ts +27 -0
- package/src/utils/tarotEngine.ts +849 -0
- package/src/utils/userStore.ts +234 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +52 -0
- package/tsconfig.json +40 -0
- package/vite.config.ts +34 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// 提示词构建:9 术 agent + Skills 编排 + 两套报告 Schema 模板 + 档案注入
|
|
2
|
+
import type { ChatMessage } from './llmProvider';
|
|
3
|
+
import { agentOf } from './skills';
|
|
4
|
+
import { chartBrief } from './chartBrief';
|
|
5
|
+
import { sixRelativesFacts } from './sixRelatives';
|
|
6
|
+
|
|
7
|
+
// ===== 报告 Schema 模板(命盘类:八字/紫微/星盘) =====
|
|
8
|
+
const MINGPAN_SAMPLE = {
|
|
9
|
+
title: '报告标题,120 字内',
|
|
10
|
+
overview: '人物画像核心论断,150 字以上,引用盘面具体数据',
|
|
11
|
+
rawReading: { summary: '盘面最关键事实的白话概括,100-150 字,只讲最要紧的 3-5 点,不逐项罗列', keyPoints: ['最关键盘面事实1(一句话白话)', '关键盘面事实2', '关键盘面事实3'] },
|
|
12
|
+
character: {
|
|
13
|
+
summary: '人格画像总述,180 字以上:气质底色+内在动力+核心矛盾,落到生活画面',
|
|
14
|
+
traits: [{ name: '气质特质', desc: '盘面依据+现实表现,3-4 句' }],
|
|
15
|
+
coreConflict: '内在核心冲突:张力+盘面机制,3-4 句',
|
|
16
|
+
emotion: '情绪与心理模式:情绪底色/压力反应/安全感来源,3-4 句',
|
|
17
|
+
},
|
|
18
|
+
family: {
|
|
19
|
+
background: '原生家庭画像:家境实况/家庭氛围/父母关系模式,150 字以上,含盘面依据',
|
|
20
|
+
parents: '父母与命主关系:父母星位状态/实际支持方式/命主主观感受的落差,150 字以上',
|
|
21
|
+
imprint: '家庭印记:原生家庭对安全感/信任/自我价值的影响,120 字以上',
|
|
22
|
+
},
|
|
23
|
+
mind: {
|
|
24
|
+
action: '行动力与坚持力:启动/拖延/半途而废的机制+何时改善,150 字以上',
|
|
25
|
+
pattern: '行为循环:反复出现的人生模式+破局方向,120 字以上',
|
|
26
|
+
growth: '成长方向:从知道到做到的路径(对应用神五行在现实中的行动),120 字以上',
|
|
27
|
+
},
|
|
28
|
+
lifeStages: [
|
|
29
|
+
{ stage: '幼年·少年', age: '0-20 岁', summary: '家庭背景与早年经历,120 字以上' },
|
|
30
|
+
{ stage: '青年', age: '20-40 岁', summary: '人生探索期,允许非常规轨迹并分析机制,120 字以上' },
|
|
31
|
+
{ stage: '中年', age: '40-60 岁', summary: '沉淀与转折,120 字以上' },
|
|
32
|
+
{ stage: '晚年', age: '60 岁后', summary: '归宿与心境,120 字以上' },
|
|
33
|
+
],
|
|
34
|
+
career: { summary: '学业事业总述+行动力与持续力专项分析,150 字以上', direction: '适宜方向,允许非常规路径', advice: '针对启动难/坚持难的对策,具体可执行' },
|
|
35
|
+
love: { summary: '情感与亲密关系:亲密模式/吸引类型/性别气质与亲密取向(温和描述不标签)/对婚姻态度,180 字以上,全部含盘面依据', advice: '亲密关系建议,尊重非常规选择' },
|
|
36
|
+
wealth: { summary: '财运总述,120 字以上', advice: '理财建议' },
|
|
37
|
+
health: { summary: '健康状况:体质倾向+心理身体关联,120 字以上', advice: '养生建议' },
|
|
38
|
+
advice: '综合建议 5-7 条,分号分隔,含盘面依据与可执行动作',
|
|
39
|
+
conclusion: '结语,80 字以上,给命主接纳与力量',
|
|
40
|
+
disclaimer: '免责声明',
|
|
41
|
+
suitability: { suitable: true, note: '适问性说明 2-3 句', suggestion: '换问建议或无' },
|
|
42
|
+
};
|
|
43
|
+
const ZHANWEN_SAMPLE = {
|
|
44
|
+
title: '报告标题,100 字内',
|
|
45
|
+
overview: '核心论断 3-4 句(120 字以上),直接回答所问之事的大势,必须引用卦/课/牌的具体象意(卦名/掌诀/牌名/天将/门星等)',
|
|
46
|
+
rawReading: { summary: '盘面象意白话概括(100-150 字):卦名/掌诀/牌义等最要紧的 3-5 点,不逐项罗列', keyPoints: ['关键象意 1(含具体盘面数据)', '关键象意 2', '关键象意 3'] },
|
|
47
|
+
situation: '当下局势(180 字以上):所问之事现状 + 当事人在此局中的真实处境与心理状态(如:表面平静实则进退两难),结合体用/用神/牌位/课传等具体要素',
|
|
48
|
+
trend: '发展趋势:近/中/远期各 80 字以上,结合变卦/三传/牌阵位置/旺衰变化,写明各阶段的转折点与关键信号',
|
|
49
|
+
timing: '时机宜忌:何时宜动、何时宜守(结合旺衰/门星/天将吉凶/课体),方位/方法上有何宜忌,给具体建议',
|
|
50
|
+
advice: '具体建议 4-6 条,分号分隔,每条含象意依据与可执行动作',
|
|
51
|
+
conclusion: '结语,80 字以上,温润收束给力量',
|
|
52
|
+
disclaimer: '免责声明',
|
|
53
|
+
suitability: { suitable: true, note: '适问性说明 2-3 句', suggestion: '换问建议或无' },
|
|
54
|
+
};
|
|
55
|
+
const MINGPAN_TEMPLATE = JSON.stringify(MINGPAN_SAMPLE, null, 2);
|
|
56
|
+
const ZHANWEN_TEMPLATE = JSON.stringify(ZHANWEN_SAMPLE, null, 2);
|
|
57
|
+
const MINGPAN_ARTS = ['bazi', 'ziwei', 'astrology'];
|
|
58
|
+
|
|
59
|
+
// 档案信息格式化
|
|
60
|
+
function profileText(profile?: unknown): string {
|
|
61
|
+
if (!profile) return '(未提供出生档案)';
|
|
62
|
+
const p = profile as any;
|
|
63
|
+
const hourKnown = !(p.birthTimeUnknown === true || p.birthHourIndex === undefined || p.birthHourIndex === null || p.birthHourIndex < 0);
|
|
64
|
+
return [
|
|
65
|
+
'出生日期(公历):' + (p.birthDate || '未录'),
|
|
66
|
+
'时辰:' + (hourKnown ? (p.birthHourIndex + 1) + ' 时' : '未知(未录)'),
|
|
67
|
+
'性别:' + (p.gender || '未录'),
|
|
68
|
+
'出生地:' + (p.location ? (p.location.province || '') + (p.location.city || '') + (p.location.district || '') : '未录'),
|
|
69
|
+
].join(';');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 时辰未知 → 解读约束(禁止编造时柱/子女宫/晚年等时辰依赖信息)
|
|
73
|
+
function hourUnknownNote(profile?: unknown): string {
|
|
74
|
+
const p = (profile || {}) as any;
|
|
75
|
+
const unknown = p.birthTimeUnknown === true || p.birthHourIndex === undefined || p.birthHourIndex === null || p.birthHourIndex < 0;
|
|
76
|
+
if (!unknown) return '';
|
|
77
|
+
return '【时辰未知 · 最高约束】命主出生时辰未知,命盘未排时柱。严禁编造、推断或默认时柱(出生时刻、时柱干支、时支藏干),严禁据此断言子女宫、子女缘分、晚年运、暮年际遇等依赖时辰的信息;所有论断只能基于年/月/日三柱与命局整体展开。凡涉及子女、暮年等维度,必须明确写「因时辰未知,此维度从略」,不得自行假设一个时辰来圆说。';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 命主已知人生经历 → 解读校准注入(报告须呼应该年份事件、不得与其矛盾)
|
|
81
|
+
function lifeEventsNote(profile?: unknown): string {
|
|
82
|
+
const evs: { year: number; text: string }[] = (profile as any)?.lifeEvents;
|
|
83
|
+
if (!Array.isArray(evs) || evs.length === 0) return '';
|
|
84
|
+
const clean = evs
|
|
85
|
+
.filter(e => e && Number.isFinite(e.year) && e.year >= 1900 && e.year <= 2100 && e.text)
|
|
86
|
+
.sort((a, b) => a.year - b.year)
|
|
87
|
+
.map(e => e.year + ' 年:' + String(e.text).slice(0, 50));
|
|
88
|
+
if (clean.length === 0) return '';
|
|
89
|
+
return '【命主已知人生经历 · 解读校准】以下为命主本人确认的人生事件,解读必须与此相符:\n' + clean.join('\n') + '\n要求:① 凡解读涉及这些年份(或与之相邻的大运流年)时,必须呼应对应事件(如该年患病手术,则论健康/流年时须呼应);② 严禁写出与已知经历相矛盾的内容(如已知 2024 年重大健康事件,就不得断言该年平安无事);③ 已知经历未覆盖的年份正常论命,不得为了呼应而编造具体事件细节。';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Skills 编排版:9 术 agent + 档案 + 排盘结果 → 结构化解读报告
|
|
93
|
+
export function buildReportMessages(
|
|
94
|
+
artId: string,
|
|
95
|
+
question: string,
|
|
96
|
+
resultRaw: unknown,
|
|
97
|
+
profile?: unknown,
|
|
98
|
+
semantic?: unknown,
|
|
99
|
+
fit?: { suitable: boolean | 'partial'; reason: string; suggestion: string },
|
|
100
|
+
): ChatMessage[] {
|
|
101
|
+
const agent = agentOf(artId);
|
|
102
|
+
const skillLines = agent.skills.map((s, i) => i + 1 + '. 【' + s.name + '】' + s.instruction).join('\n');
|
|
103
|
+
const isMingpan = MINGPAN_ARTS.includes(artId);
|
|
104
|
+
const schemaTemplate = isMingpan ? MINGPAN_TEMPLATE : ZHANWEN_TEMPLATE;
|
|
105
|
+
const relativesFacts = sixRelativesFacts(artId, resultRaw, (profile as any)?.gender);
|
|
106
|
+
const sectionRules = isMingpan
|
|
107
|
+
? 'character.traits 至少 3 条并给出盘中依据;lifeStages 分 4 段并结合年/月/日/时柱或大限;career(学业与事业皆须解读)/love/wealth/health 每项含 summary 与 advice;advice 3-5 条。'
|
|
108
|
+
: 'situation/trend/timing 各 2-4 句;trend 须分近/中/远期;advice 3-5 条。';
|
|
109
|
+
const system = [
|
|
110
|
+
'你是一位' + agent.name + '。' + agent.role,
|
|
111
|
+
'',
|
|
112
|
+
'【输出要求 · 最高优先】必须只输出一个合法的 json 对象(按下方 Schema,字段名不可更改,不要输出 json 以外的任何文字、注释或 Markdown 代码块)。',
|
|
113
|
+
'',
|
|
114
|
+
'【篇幅与细节 · 重要】报告充实详尽,总字数 2500 字以上,但充实靠「新信息」而非「重复旧话」:每个区块至少要给出一个前面未出现过的角度或数据点;论断须引用盘面中的具体数据(干支/星曜/宫位/卦名/天将/牌名等),禁止空泛套话,但同一数据只在首次出现的区块完整展开(盘面依据 → 命理逻辑 → 现实投射),后续区块点题引用即可(如「如前所析,命宫借星……」,一句话带过,不得重述全段)。',
|
|
115
|
+
'【去重与分工 · 重要】全篇是一份报告,不是各区块的独立作文,严禁把同一段内容换着说法再写一遍。区块分工:总览(overview)只给 3-4 句核心论断,不罗列盘面数据;原始解读(rawReading)是全篇唯一「完整翻译盘面」的地方,只做白话复述不评价;性格/事业/爱情/财富/健康等各区块只写自己领域独有的内容,引用相关盘面数据时用简略表述;人生阶段(lifeStages)按时间线推进,与性格/事业区块内容不重叠。整篇应层层递进、信息增量递减。',
|
|
116
|
+
'【字数预算 · 总量控制】全篇 2000-2500 字即可,参考预算:总览 120-180、原始解读 100-150、性格 200-280、原生家庭 200-280、心智模式 200-280、人生阶段 300-400、事业 150-220、爱情 150-220、财富 100-160、健康 100-150、建议 100-150、结语 60-100。预算为参考上限:某区块无新内容可低于预算,但不得为凑字数重复前文。',
|
|
117
|
+
'【宁短勿凑 · 重要】若某区块相对前文没有新信息可写,写短(3-4 句)甚至从略,绝不为了凑字数重复前文;字数下限是「可写内容充足时的下限」,不是「凑字任务」。',
|
|
118
|
+
'【用词规范】术语使用要准确(十神/旺衰/庙陷/四化/格局/六亲/世应/四课三传/天将/体用/旺相休囚死等),对普通用户要用白话解释(术语后括注通俗解释);语气温润克制,不作绝对化断言,不制造恐惧。',
|
|
119
|
+
'',
|
|
120
|
+
'【具体化 · 最重要】禁止空泛套话:每一段都要把盘面数据翻译成「这个人在真实生活中会怎样」——具体到场景、行为、感受(如:不是写「印重身弱主依赖」,而是写「他想法很多、临事却总在准备阶段反复,别人催他他更想逃,事情常常开了头就慢慢无声无息」)。宁可具体到让命主一眼认出自己,也不要安全但无用的泛泛之谈。',
|
|
121
|
+
'【人生轨迹】不得默认「人人工作/结婚/生子」的常规模板:命局允许非常规轨迹(长期不工作、独身、非传统亲密关系、多次转行等),要分析这些轨迹的盘面机制并给建设性建议,不得评判。',
|
|
122
|
+
'【敏感维度】涉及性取向、性别气质、亲密关系选择等:以尊重、接纳、不作判断的方式温和描述,从盘面阴阳结构/桃花/夫妻宫等给出气质性线索即可,不贴标签、不病理化、不引导。',
|
|
123
|
+
'',
|
|
124
|
+
'【必需字段 · 硬性要求】输出必须包含以下所有字段,缺一不可:' + (isMingpan
|
|
125
|
+
? 'character.coreConflict、character.emotion、family.background、family.parents、family.imprint、mind.action、mind.pattern、mind.growth、love.summary(含亲密模式与亲密取向的气质性描述)'
|
|
126
|
+
: 'rawReading.summary(含具体象意数据)、situation(含当事人真实处境与心理状态)、trend(近/中/远期三段)、timing(具体时机方位宜忌)、advice(4-6 条含依据)') + '。若 Schema 模板中某字段缺失,也须按此清单补足输出。',
|
|
127
|
+
'【现代语境】不得使用「配偶宫主配偶」「第三者介入」「克妻克夫」「刑夫克子」等旧式偏见措辞;亲密关系一律以现代、尊重、非预设异性恋婚姻的语境描述。',
|
|
128
|
+
'',
|
|
129
|
+
'【六亲星位(子平惯例,八字适用)】男命:偏财为父、正印为母、正财为妻、官杀为子女;女命:正财为父、偏印为母、正官为夫、食伤为子女。分析父母/伴侣时按此映射取星位,星位不显可结合宫位(年柱为祖上父母、月柱为父母兄弟)灵活推断,并注明依据。',
|
|
130
|
+
'',
|
|
131
|
+
'【格式规范 · 重要】所有正文均为纯文本,禁止使用任何 Markdown 标记(不要用 **、*、#、>、- 、1. 等符号排版;不要输出代码块)。内容直接以自然段落呈现。',
|
|
132
|
+
'',
|
|
133
|
+
'【盘面事实一致性 · 最高约束】解读中出现的所有盘面事实(如上升星座、太阳/月亮/行星落座与宫位、相位、四柱干支、十神、命宫/身宫、十四主星落宫、大限流年、卦名爻象等)必须与【排盘结果】中给出的数据完全一致,逐字引用,不得更改、不得编造、不得自行推算补全。若【排盘结果】未提供某项,则不得在解读中虚构该项(如未提供上升星座就不得写上升为何座)。rawReading 只需概括最关键 3-5 条盘面事实并翻译成人话,不必逐项罗列。',
|
|
134
|
+
'【论断锚定 · 最重要】每个核心论断(性格底色、父母关系、感情模式、事业方向、健康倾向)都必须能回溯到具体盘面数据,给出「数据 → 推论」的推导链,不得无依据自由发挥。特别地:',
|
|
135
|
+
'1. 六亲论断(父母/子女/配偶)必须基于父母宫与六亲星位(八字看年柱/月柱与六亲十神,紫微看父母宫/兄弟宫等)的客观状态(主星、旺衰、四化、十神),并注明依据;',
|
|
136
|
+
'2. 禁止使用「严格/宽松/慈爱/冷漠/严厉/溺爱」等主观程度词,除非能从盘面推出并注明依据(例:正官坐月柱主规矩约束,可写「家教较严」并注明正官依据;父母宫化忌可写「关系有隔阂」;若盘面无明确指向,写「盘面未明示」,绝不编造程度);',
|
|
137
|
+
'3. 全篇论断须自洽:性格/家庭/事业/感情各区块相互呼应,不得前后矛盾(同一维度两次出现必须口径一致)。',
|
|
138
|
+
relativesFacts,
|
|
139
|
+
'',
|
|
140
|
+
hourUnknownNote(profile),
|
|
141
|
+
'',
|
|
142
|
+
'【Skills 编排 · 请依序调用以下技能,逐章成文】',
|
|
143
|
+
skillLines,
|
|
144
|
+
'',
|
|
145
|
+
'【写作次序】先做原始解读(rawReading:概括最关键盘面事实并翻译成人话),再依据 Skills 编排逐章深度分析' + (isMingpan ? '(性格/人生阶段/学业事业/爱情/财富/健康)' : '(现状/趋势/时机)') + '。',
|
|
146
|
+
fit ? '【问题适配性分析】此问经适配判定为:' + (fit.suitable === true ? '相契' : fit.suitable === 'partial' ? '部分相契' : '不甚相契') + '。' + fit.reason + (fit.suggestion ? '建议:' + fit.suggestion : '') + '请将此分析如实写入报告的 suitability 字段(suitable 用 ' + (fit.suitable === true ? 'true' : fit.suitable === 'partial' ? 'partial' : 'false') + '),并在解读行文中温和呼应。' : '',
|
|
147
|
+
'',
|
|
148
|
+
'【语言规范 · 三层】',
|
|
149
|
+
'1. 解读正文用现代白话,直白清晰有温度;不作绝对化断言(不说必/定/绝),涉及健康、法律、投资等话题温和提示仅供参考。',
|
|
150
|
+
'2. 引经据典处以引用块呈现原文并附现代注译与出处。',
|
|
151
|
+
'3. 输出结构:严格按以下 json Schema 输出(字段名不可更改,缺失字段填空值,不要输出 json 以外的任何内容):',
|
|
152
|
+
schemaTemplate,
|
|
153
|
+
'4. 章节要求:' + sectionRules,
|
|
154
|
+
'5. 结尾免责:凡占问所得,仅供修身养性、怡情遣兴之用,不构成决策依据。',
|
|
155
|
+
'',
|
|
156
|
+
'The final output must be a valid json object only.',
|
|
157
|
+
].join('\n');
|
|
158
|
+
const user = [
|
|
159
|
+
question ? '【所问之事(命主之问,当为全篇之纲)】' + question : '【所问之事】(未书,心念已至)',
|
|
160
|
+
'',
|
|
161
|
+
isMingpan ? '【出生档案】' + profileText(profile) : '',
|
|
162
|
+
'',
|
|
163
|
+
'【盘面事实(以下为不可更改的排盘结果,解读必须逐字引用、不得编造)】',
|
|
164
|
+
chartBrief(artId, resultRaw),
|
|
165
|
+
'',
|
|
166
|
+
lifeEventsNote(profile),
|
|
167
|
+
'',
|
|
168
|
+
semantic ? '【问题语义分析】' + JSON.stringify(semantic, null, 1) : '',
|
|
169
|
+
'',
|
|
170
|
+
'请以 ' + agent.name + ' 的身份,依序调用全部技能,生成完整解读报告。',
|
|
171
|
+
'重要:最终输出必须是一个合法的 json 对象(严格按上方 Schema)。',
|
|
172
|
+
].filter(Boolean).join('\n');
|
|
173
|
+
return [
|
|
174
|
+
{ role: 'system', content: system },
|
|
175
|
+
{ role: 'user', content: user },
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
// ===== 两步管线(2026-08-20):Step1 盘面解析 → Step2 深度报告 =====
|
|
181
|
+
// 目的:① 盘面先明、再论人事,分步聚焦提升质量 ② 每步 prompt 更短、失败重试粒度细 ③ Step1 结果可复用
|
|
182
|
+
const STEP1_TEMPLATE = JSON.stringify({
|
|
183
|
+
art: '术名(如:八字)',
|
|
184
|
+
chartSummary: '盘面骨架的白话总述(200 字以上):何命何局、关键要素齐列。',
|
|
185
|
+
pillars: ['盘面事实 1(如:日主壬水身弱,喜金水,忌火土)', '盘面事实 2(如:午月火旺失令,地支通根于申)', '盘面事实 3(如:大运甲申 28-37 岁,食神生财)'],
|
|
186
|
+
keyFacts: ['对论断最重要的 3-5 条盘面事实(供后续分析引用,含具体数据)'],
|
|
187
|
+
}, null, 2);
|
|
188
|
+
|
|
189
|
+
export function buildStep1Messages(
|
|
190
|
+
artId: string,
|
|
191
|
+
question: string,
|
|
192
|
+
resultRaw: unknown,
|
|
193
|
+
profile?: unknown,
|
|
194
|
+
): ChatMessage[] {
|
|
195
|
+
const agent = agentOf(artId);
|
|
196
|
+
const system = [
|
|
197
|
+
'你是一位' + agent.name + '。' + agent.role,
|
|
198
|
+
'',
|
|
199
|
+
'【任务】你是解读管线的第一步:只做「盘面解析」——把排盘结果整理成结构化的事实清单,供第二步深度分析引用。',
|
|
200
|
+
'【要求】只陈述盘面事实与通行命理含义,不做人生论断、不给建议;每条事实必须来自下方盘面数据,不得编造;术语后括注白话。',
|
|
201
|
+
hourUnknownNote(profile),
|
|
202
|
+
'【输出】只输出一个合法 json 对象(按下方 Schema,字段名不可更改):',
|
|
203
|
+
STEP1_TEMPLATE,
|
|
204
|
+
].filter(Boolean).join('\n');
|
|
205
|
+
const user = [
|
|
206
|
+
'【命主档案】性别:' + ((profile as any)?.gender || '未录') + '(报告人称与性别语境以此为准,不得臆断为其他性别)',
|
|
207
|
+
'',
|
|
208
|
+
question ? '【所问之事】' + question + '(仅作背景,本步不回答)' : '【所问之事】(未书,仅作背景)',
|
|
209
|
+
'',
|
|
210
|
+
'【盘面事实】',
|
|
211
|
+
chartBrief(artId, resultRaw),
|
|
212
|
+
'',
|
|
213
|
+
'请输出盘面解析 json。',
|
|
214
|
+
].filter(Boolean).join('\n');
|
|
215
|
+
return [
|
|
216
|
+
{ role: 'system', content: system },
|
|
217
|
+
{ role: 'user', content: user },
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function buildStep2Messages(
|
|
222
|
+
artId: string,
|
|
223
|
+
question: string,
|
|
224
|
+
resultRaw: unknown,
|
|
225
|
+
step1Text: string,
|
|
226
|
+
profile?: unknown,
|
|
227
|
+
semantic?: unknown,
|
|
228
|
+
fit?: { suitable: boolean | 'partial'; reason: string; suggestion: string },
|
|
229
|
+
): ChatMessage[] {
|
|
230
|
+
const agent = agentOf(artId);
|
|
231
|
+
const skillLines = agent.skills.map((s, i) => i + 1 + '. 【' + s.name + '】' + s.instruction).join('\n');
|
|
232
|
+
const isMingpan = MINGPAN_ARTS.includes(artId);
|
|
233
|
+
const schemaTemplate = isMingpan ? MINGPAN_TEMPLATE : ZHANWEN_TEMPLATE;
|
|
234
|
+
const relativesFacts = sixRelativesFacts(artId, resultRaw, (profile as any)?.gender);
|
|
235
|
+
const sectionRules = isMingpan
|
|
236
|
+
? 'character.traits 至少 3 条并给出盘中依据;lifeStages 分 4 段并结合年/月/日/时柱或大限;career(学业与事业皆须解读)/love/wealth/health 每项含 summary 与 advice;advice 4-6 条。'
|
|
237
|
+
: 'situation/trend/timing 各 150 字以上;trend 须分近/中/远期;advice 4-6 条。';
|
|
238
|
+
const system = [
|
|
239
|
+
'你是一位' + agent.name + '。' + agent.role,
|
|
240
|
+
'',
|
|
241
|
+
'【任务】你是解读管线的第二步:在「盘面解析」基础上进行深度解读,输出完整报告。',
|
|
242
|
+
'【输出要求 · 最高优先】必须只输出一个合法的 json 对象(按下方 Schema,字段名不可更改,不要输出 json 以外的任何文字、注释或 Markdown 代码块)。',
|
|
243
|
+
hourUnknownNote(profile),
|
|
244
|
+
'',
|
|
245
|
+
'【篇幅与细节 · 重要】报告充实详尽,总字数 2500 字以上,但充实靠「新信息」而非「重复旧话」:每个区块至少要给出一个前面未出现过的角度或数据点;论断须引用盘面解析与盘面事实中的具体数据,禁止空泛套话,但同一数据只在首次出现的区块完整展开(盘面依据 → 命理逻辑 → 现实投射),后续区块点题引用即可(如「如前所析,命宫借星……」,一句话带过,不得重述全段)。',
|
|
246
|
+
'【去重与分工 · 重要】全篇是一份报告,不是各区块的独立作文,严禁把同一段内容换着说法再写一遍。区块分工:总览(overview)只给 3-4 句核心论断,不罗列盘面数据;原始解读(rawReading)是全篇唯一「完整翻译盘面」的地方,只做白话复述不评价;性格/事业/爱情/财富/健康等各区块只写自己领域独有的内容,引用相关盘面数据时用简略表述;人生阶段(lifeStages)按时间线推进,与性格/事业区块内容不重叠。整篇应层层递进、信息增量递减。',
|
|
247
|
+
'【字数预算 · 总量控制】全篇 2000-2500 字即可,参考预算:总览 120-180、原始解读 100-150、性格 200-280、原生家庭 200-280、心智模式 200-280、人生阶段 300-400、事业 150-220、爱情 150-220、财富 100-160、健康 100-150、建议 100-150、结语 60-100。预算为参考上限:某区块无新内容可低于预算,但不得为凑字数重复前文。',
|
|
248
|
+
'【宁短勿凑 · 重要】若某区块相对前文没有新信息可写,写短(3-4 句)甚至从略,绝不为了凑字数重复前文;字数下限是「可写内容充足时的下限」,不是「凑字任务」。',
|
|
249
|
+
'【用词规范】术语使用要准确(十神/旺衰/庙陷/四化/格局/六亲/世应/四课三传/天将/体用/旺相休囚死等),对普通用户要用白话解释(术语后括注通俗解释);语气温润克制,不作绝对化断言,不制造恐惧。',
|
|
250
|
+
'【论断锚定 · 最重要】每个核心论断(性格底色、父母关系、感情模式、事业方向、健康倾向)都必须能回溯到具体盘面数据,给出「数据 → 推论」的推导链,不得无依据自由发挥。特别地:',
|
|
251
|
+
'1. 六亲论断(父母/子女/配偶)必须基于父母宫与六亲星位(八字看年柱/月柱与六亲十神,紫微看父母宫/兄弟宫等)的客观状态(主星、旺衰、四化、十神),并注明依据;',
|
|
252
|
+
'2. 禁止使用「严格/宽松/慈爱/冷漠/严厉/溺爱」等主观程度词,除非能从盘面推出并注明依据(例:正官坐月柱主规矩约束,可写「家教较严」并注明正官依据;父母宫化忌可写「关系有隔阂」;若盘面无明确指向,写「盘面未明示」,绝不编造程度);',
|
|
253
|
+
'3. 全篇论断须自洽:性格/家庭/事业/感情各区块相互呼应,不得前后矛盾(同一维度两次出现必须口径一致)。',
|
|
254
|
+
relativesFacts,
|
|
255
|
+
'',
|
|
256
|
+
'【技能编排】依序运用以下技能完成报告(每一步的结论都要落进对应章节):',
|
|
257
|
+
skillLines,
|
|
258
|
+
'',
|
|
259
|
+
'【格式规范 · 重要】所有正文均为纯文本,禁止使用Markdown 符号(#、*、-、> 等),列表项用「1. 2. 3.」或分号分隔;引经据典附出处(书名·篇名)。',
|
|
260
|
+
'',
|
|
261
|
+
'【必需字段 · 硬性要求】输出必须包含以下所有字段,缺一不可:' + (isMingpan
|
|
262
|
+
? 'character.coreConflict、character.emotion、family.background、family.parents、family.imprint、mind.action、mind.pattern、mind.growth、lifeStages(4 段)、career.summary、love.summary、wealth.summary、health.summary、advice(4-6 条)、conclusion'
|
|
263
|
+
: 'rawReading.summary(含具体象意数据)、situation(含当事人真实处境与心理状态)、trend(近/中/远期三段)、timing(具体时机方位宜忌)、advice(4-6 条含依据)、conclusion') + '。若 Schema 模板中某字段缺失,也须按此清单补足输出。',
|
|
264
|
+
'',
|
|
265
|
+
'【报告 Schema 模板(字段名与结构不可更改)】',
|
|
266
|
+
schemaTemplate,
|
|
267
|
+
'',
|
|
268
|
+
'【章节要求】' + sectionRules,
|
|
269
|
+
].filter(Boolean).join('\n');
|
|
270
|
+
const user = [
|
|
271
|
+
'【命主档案】性别:' + ((profile as any)?.gender || '未录') + '——报告人称、六亲、亲密关系等所有涉及性别的表述一律以此为准(如女命则用「她/女士」,不得写成男命)。',
|
|
272
|
+
'',
|
|
273
|
+
question ? '【所问之事】' + question : '【所问之事】(未书,心念已至)',
|
|
274
|
+
'',
|
|
275
|
+
'【盘面解析(第一步结果 · 唯一事实源,所有解读结论必须能在此找到依据,不得另立新的事实)】',
|
|
276
|
+
step1Text,
|
|
277
|
+
'',
|
|
278
|
+
'【盘面事实(原始排盘,引用须一致)】',
|
|
279
|
+
chartBrief(artId, resultRaw),
|
|
280
|
+
'',
|
|
281
|
+
lifeEventsNote(profile),
|
|
282
|
+
'',
|
|
283
|
+
semantic ? '【问题语义分析】' + JSON.stringify(semantic, null, 1) : '',
|
|
284
|
+
'',
|
|
285
|
+
fit ? '【适问分析】' + JSON.stringify(fit, null, 1) : '',
|
|
286
|
+
'',
|
|
287
|
+
'请依序运用全部技能,生成完整解读报告 json。',
|
|
288
|
+
].filter(Boolean).join('\n');
|
|
289
|
+
return [
|
|
290
|
+
{ role: 'system', content: system },
|
|
291
|
+
{ role: 'user', content: user },
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 简易解读(非报告模式,保留)
|
|
296
|
+
function systemPrompt(artId: string): string {
|
|
297
|
+
const p = (agentOf(artId) as any);
|
|
298
|
+
return [
|
|
299
|
+
'你是一位' + (p?.name || '命理师') + '。' + (p?.role || ''),
|
|
300
|
+
'',
|
|
301
|
+
'【语言规范】现代白话,温润克制,不作绝对化断言;引经据典附出处;结尾附免责声明。',
|
|
302
|
+
'输出结构:严格输出合法 json 对象(字段名不可更改)。',
|
|
303
|
+
'【可引古籍】' + (p?.classics || ''),
|
|
304
|
+
].join('\n');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function buildMessages(
|
|
308
|
+
artId: string,
|
|
309
|
+
question: string,
|
|
310
|
+
resultRaw: unknown,
|
|
311
|
+
semantic?: unknown,
|
|
312
|
+
): ChatMessage[] {
|
|
313
|
+
const user = [
|
|
314
|
+
question ? '【所问之事】' + question : '【所问之事】(未书,心念已至)',
|
|
315
|
+
'',
|
|
316
|
+
'【排盘结果】',
|
|
317
|
+
JSON.stringify(resultRaw, null, 1),
|
|
318
|
+
'',
|
|
319
|
+
semantic ? '【问题语义分析】' + JSON.stringify(semantic, null, 1) : '',
|
|
320
|
+
'',
|
|
321
|
+
'请按语言规范生成解读。',
|
|
322
|
+
].filter(Boolean).join('\n');
|
|
323
|
+
return [
|
|
324
|
+
{ role: 'system', content: systemPrompt(artId) },
|
|
325
|
+
{ role: 'user', content: user },
|
|
326
|
+
];
|
|
327
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// 六亲表述校验与修正:报告生成后检测六亲宫位表述是否与盘面事实矛盾,矛盾则定向重写
|
|
2
|
+
// 背景:实测模型在长报告中对「父母宫主星」等事实屡屡编造(写无主星/错误地支),提示词约束不可靠,需工程兜底
|
|
3
|
+
import type { AIReportNormalized } from '../routes/ai.js';
|
|
4
|
+
import { chatOnce, type ChatMessage } from './llmProvider.js';
|
|
5
|
+
|
|
6
|
+
const DIZHI = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑']; // 引擎宫位索引:0=寅
|
|
7
|
+
const SIHUA_KEY: Record<string, string> = { lu: '化禄', quan: '化权', ke: '化科', ji: '化忌' };
|
|
8
|
+
|
|
9
|
+
interface PalaceFact { name: string; zhi: string; stars: string[]; fus: string[]; sihua: string }
|
|
10
|
+
|
|
11
|
+
function ziweiPalace(r: any, palaceIdx: number, name: string): PalaceFact {
|
|
12
|
+
const zhiIdx = r.palaces?.[palaceIdx] ?? ((r.ming - palaceIdx + 24) % 12);
|
|
13
|
+
const stars = Object.entries(r.zwStars).filter(([, p]) => p === zhiIdx).map(([s]) => s);
|
|
14
|
+
const fus = Object.entries(r.fuStars || {}).filter(([, p]) => p === zhiIdx).map(([s]) => s);
|
|
15
|
+
const sihua = Object.entries(r.sihuaPos || {}).filter(([, p]) => p === zhiIdx).map(([k]) => SIHUA_KEY[k]).join('、');
|
|
16
|
+
return { name, zhi: DIZHI[zhiIdx], stars, fus, sihua };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ziweiFacts(r: any): PalaceFact[] {
|
|
20
|
+
return [ziweiPalace(r, 11, '父母宫'), ziweiPalace(r, 2, '夫妻宫'), ziweiPalace(r, 3, '子女宫')];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 校验报告文本中某宫的表述是否与事实矛盾(出现「宫名」且关键要素全错即矛盾) */
|
|
24
|
+
function checkPalace(text: string, fact: PalaceFact, r: any): string[] {
|
|
25
|
+
const errs: string[] = [];
|
|
26
|
+
// 窗口限 10 字且排除全/半角标点,避免跨句误匹配(如「子女宫天相,父母宫在酉」把酉算进子女宫)
|
|
27
|
+
const re = new RegExp(fact.name + '[^,。;、,.;\\n\\s]{0,10}', 'g');
|
|
28
|
+
let m: RegExpExecArray | null;
|
|
29
|
+
const mentions: string[] = [];
|
|
30
|
+
while ((m = re.exec(text)) !== null) mentions.push(m[0]);
|
|
31
|
+
// 对宫主星(借星校验用):本宫无主星时 AI 常写「借对宫X」——对宫主星是客观事实
|
|
32
|
+
const oppZhi = (DIZHI.indexOf(fact.zhi) + 6) % 12;
|
|
33
|
+
const oppStars = Object.entries(r?.zwStars || {}).filter(([, p]) => p === oppZhi).map(([s]) => s);
|
|
34
|
+
for (const seg of mentions) {
|
|
35
|
+
const body = seg.slice(fact.name.length); // 宫名本身可能含地支字(如「子女宫」的「子」),只查其后内容
|
|
36
|
+
const zhiMentioned = DIZHI.filter(z => body.includes(z));
|
|
37
|
+
const wrongZhi = zhiMentioned.filter(z => z !== fact.zhi);
|
|
38
|
+
if (wrongZhi.length) errs.push(`${fact.name}地支写错(${seg.slice(0, 20)}…应为${fact.zhi}宫)`);
|
|
39
|
+
if (fact.stars.length > 0 && body.includes('无主星')) {
|
|
40
|
+
errs.push(`${fact.name}写成无主星(实际主星${fact.stars.join('、')})`);
|
|
41
|
+
}
|
|
42
|
+
// 借星校验:写「借对宫X」时 X 必须与实际对宫主星一致(跨逗号匹配到句号为止)
|
|
43
|
+
if (oppStars.length) {
|
|
44
|
+
const jiRe = new RegExp(fact.name + '[^。;\\n]{0,28}借对宫([^,。;、,.;\\s"{}]{1,8})', 'g');
|
|
45
|
+
let jm: RegExpExecArray | null;
|
|
46
|
+
while ((jm = jiRe.exec(text)) !== null) {
|
|
47
|
+
const borrowed = jm[1];
|
|
48
|
+
const matched = oppStars.some(s => borrowed.includes(s));
|
|
49
|
+
if (!matched) {
|
|
50
|
+
errs.push(`${fact.name}借星写错(借对宫${borrowed}…实际对宫主星为${oppStars.join('、')})`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return errs;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 四化一致性:只校验生年四化星名(巨门/太阳/文曲/文昌)的化名是否写错。
|
|
59
|
+
* 例:生年文昌化忌,报告写「文曲化忌」→ 错;「武曲化禄」可能是大限/流年四化 → 不校验 */
|
|
60
|
+
function checkSihua(text: string, r: any): string[] {
|
|
61
|
+
const errs: string[] = [];
|
|
62
|
+
const sihua = r?.sihua;
|
|
63
|
+
if (!sihua) return errs;
|
|
64
|
+
const SI_MAP: Record<string, string> = {
|
|
65
|
+
[sihua.lu]: '化禄', [sihua.quan]: '化权', [sihua.ke]: '化科', [sihua.ji]: '化忌',
|
|
66
|
+
};
|
|
67
|
+
const siStars = Object.keys(SI_MAP);
|
|
68
|
+
const re = /([^,。;、,.;\n\s"{}:]{1,4})(化禄|化权|化科|化忌)/g;
|
|
69
|
+
let m: RegExpExecArray | null;
|
|
70
|
+
while ((m = re.exec(text)) !== null) {
|
|
71
|
+
const label = m[2];
|
|
72
|
+
const written = m[1].replace(/[,。;、,.;"{}:\s]/g, '').slice(-2); // 星名最多 2 字,取末尾
|
|
73
|
+
if (siStars.includes(written) && SI_MAP[written] !== label) {
|
|
74
|
+
errs.push(`${written}${label}写错(实际应为${written}${SI_MAP[written]})`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return errs;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 校验 AI 报告中的六亲表述,返回矛盾清单(空数组 = 通过) */
|
|
81
|
+
export function verifyRelatives(artId: string, resultRaw: unknown, report: AIReportNormalized): string[] {
|
|
82
|
+
try {
|
|
83
|
+
if (artId !== 'ziwei' && artId !== 'bazi') return [];
|
|
84
|
+
const text = JSON.stringify(report, null, 1);
|
|
85
|
+
if (artId === 'ziwei' && resultRaw && (resultRaw as any).palaces) {
|
|
86
|
+
const facts = ziweiFacts(resultRaw);
|
|
87
|
+
const errs: string[] = [];
|
|
88
|
+
for (const f of facts) {
|
|
89
|
+
if (f.name === '夫妻宫' && f.stars.length === 0) continue; // 无主星是合法状态
|
|
90
|
+
errs.push(...checkPalace(text, f, resultRaw));
|
|
91
|
+
}
|
|
92
|
+
errs.push(...checkSihua(text, resultRaw));
|
|
93
|
+
return errs;
|
|
94
|
+
}
|
|
95
|
+
} catch { /* 校验失败不阻塞 */ }
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function factsText(artId: string, resultRaw: unknown): string {
|
|
100
|
+
if (artId === 'ziwei' && resultRaw && (resultRaw as any).palaces) {
|
|
101
|
+
return ziweiFacts(resultRaw)
|
|
102
|
+
.map(f => `${f.name}:${f.zhi}宫,${f.stars.length ? '主星' + f.stars.join('、') : '无主星'}${f.fus.length ? ',辅星' + f.fus.join('、') : ''}${f.sihua ? ',' + f.sihua : ''}`)
|
|
103
|
+
.join(';');
|
|
104
|
+
}
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 定向重写 family 区块:仅当校验发现矛盾时调用(一次短调用,只输出修正后的 family JSON) */
|
|
109
|
+
export async function fixFamilyRelatives(artId: string, resultRaw: unknown, report: AIReportNormalized, errors: string[]): Promise<unknown | null> {
|
|
110
|
+
try {
|
|
111
|
+
const facts = factsText(artId, resultRaw);
|
|
112
|
+
if (!facts) return null;
|
|
113
|
+
const oldFamily = JSON.stringify(report.family || {}, null, 1);
|
|
114
|
+
const messages: ChatMessage[] = [
|
|
115
|
+
{
|
|
116
|
+
role: 'system',
|
|
117
|
+
content: [
|
|
118
|
+
'你是紫微斗数报告校正助手。以下是命盘六亲宫位事实(不可更改、必须逐字引用):',
|
|
119
|
+
facts,
|
|
120
|
+
'',
|
|
121
|
+
'原报告的家庭段落存在与事实矛盾的表述:' + errors.join(';'),
|
|
122
|
+
'请只重写报告的「原生家庭 family」区块,输出一个合法 JSON 对象(字段 background/parents/imprint,结构与原区块一致):',
|
|
123
|
+
'1. 所有盘面描述(宫位地支、主星、辅星、四化)必须与上述六亲事实完全一致;',
|
|
124
|
+
'2. 解读结论可保留原意,但盘面依据必须更正;',
|
|
125
|
+
'3. 只输出 JSON 对象本身,不要任何其他文字。',
|
|
126
|
+
].join('\n'),
|
|
127
|
+
},
|
|
128
|
+
{ role: 'user', content: '原 family 区块:\n' + oldFamily + '\n\n请输出修正后的 family JSON。' },
|
|
129
|
+
];
|
|
130
|
+
const text = await chatOnce(messages);
|
|
131
|
+
const cleaned = text.trim().replace(/^```json\s*/i, '').replace(/```\s*$/, '');
|
|
132
|
+
const obj = JSON.parse(cleaned);
|
|
133
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) return obj;
|
|
134
|
+
return null;
|
|
135
|
+
} catch (e: any) {
|
|
136
|
+
console.warn('[relatives-fix] 修正失败:', e.message);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 六亲关键事实提取:后端从排盘结果确定性提取父母/夫妻/子女宫位事实
|
|
2
|
+
// 目的:AI 只做「解读」,不承担「推算宫位」——推算即出错源(实测 AI 多次把父母宫主星编错)
|
|
3
|
+
import type { ZiweiResult, BaziResult } from '../../../shared/core/types';
|
|
4
|
+
|
|
5
|
+
const DIZHI = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'];
|
|
6
|
+
const SIHUA_KEY: Record<string, string> = { lu: '化禄', quan: '化权', ke: '化科', ji: '化忌' };
|
|
7
|
+
|
|
8
|
+
function ziweiPalaceFact(r: ZiweiResult, palaceIdx: number, palaceName: string): string {
|
|
9
|
+
const zhiIdx = r.palaces?.[palaceIdx] ?? ((r.ming - palaceIdx + 24) % 12);
|
|
10
|
+
const stars = Object.entries(r.zwStars).filter(([, p]) => p === zhiIdx).map(([s]) => s);
|
|
11
|
+
const fus = Object.entries(r.fuStars || {}).filter(([, p]) => p === zhiIdx).map(([s]) => s);
|
|
12
|
+
const sh = Object.entries(r.sihuaPos || {}).filter(([, p]) => p === zhiIdx).map(([k]) => SIHUA_KEY[k]).join('、');
|
|
13
|
+
const parts = [DIZHI[zhiIdx] + '宫', stars.length ? '主星' + stars.join('、') : '无主星'];
|
|
14
|
+
if (fus.length) parts.push('辅星' + fus.join('、'));
|
|
15
|
+
if (sh) parts.push(sh);
|
|
16
|
+
return palaceName + ':' + parts.join(',');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 八字六亲星位(子平惯例):男命 偏财父/正印母/正财妻/官杀子女;女命 正财父/偏印母/正官夫/食伤子女
|
|
20
|
+
function baziRelatives(r: BaziResult, gender: '男' | '女'): string {
|
|
21
|
+
const dayGan = r.dayGan;
|
|
22
|
+
const findStar = (target: string): string | null => {
|
|
23
|
+
// 天干
|
|
24
|
+
for (let i = 0; i < r.shishen.length; i++) {
|
|
25
|
+
const s = r.shishen[i];
|
|
26
|
+
if (s.name === target) return ['年干', '月干', '日干', '时干'][i] + s.gan + '(' + target + ')';
|
|
27
|
+
}
|
|
28
|
+
// 地支藏干
|
|
29
|
+
for (let i = 0; i < r.canggan.length; i++) {
|
|
30
|
+
const cg = r.canggan[i];
|
|
31
|
+
const hit = cg.gans.find(g => g.shishen === target);
|
|
32
|
+
if (hit) return ['年支', '月支', '日支', '时支'][i] + cg.zhi + '藏' + hit.gan + '(' + target + ')';
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
const parts: string[] = [];
|
|
37
|
+
if (gender === '男') {
|
|
38
|
+
const f = findStar('偏财'), m = findStar('正印');
|
|
39
|
+
parts.push('父亲星位(偏财):' + (f || '未现'));
|
|
40
|
+
parts.push('母亲星位(正印):' + (m || '未现'));
|
|
41
|
+
} else {
|
|
42
|
+
const f = findStar('正财'), m = findStar('偏印');
|
|
43
|
+
parts.push('父亲星位(正财):' + (f || '未现'));
|
|
44
|
+
parts.push('母亲星位(偏印):' + (m || '未现'));
|
|
45
|
+
}
|
|
46
|
+
const spouse = findStar(gender === '男' ? '正财' : '正官');
|
|
47
|
+
parts.push('配偶星位(' + (gender === '男' ? '正财' : '正官') + '):' + (spouse || '未现'));
|
|
48
|
+
return parts.join(';');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 提取六亲关键事实(AI 解读六亲必须引用,不得另立) */
|
|
52
|
+
export function sixRelativesFacts(artId: string, resultRaw: unknown, gender?: '男' | '女'): string {
|
|
53
|
+
try {
|
|
54
|
+
if (artId === 'ziwei') {
|
|
55
|
+
const r = resultRaw as ZiweiResult;
|
|
56
|
+
if (!r || typeof r.ming !== 'number') return '';
|
|
57
|
+
return '【本盘六亲事实 · 必须逐字引用,不得推算或更改】'
|
|
58
|
+
+ ziweiPalaceFact(r, 11, '父母宫') + ';'
|
|
59
|
+
+ ziweiPalaceFact(r, 2, '夫妻宫') + ';'
|
|
60
|
+
+ ziweiPalaceFact(r, 3, '子女宫') + '。'
|
|
61
|
+
+ '(解读父母/感情/子女必须以此为准,例如父母宫主星武曲贪狼就不得写「父母宫无主星」。)';
|
|
62
|
+
}
|
|
63
|
+
if (artId === 'bazi') {
|
|
64
|
+
const r = resultRaw as BaziResult;
|
|
65
|
+
if (!r || !r.dayGan) return '';
|
|
66
|
+
return '【本盘六亲事实 · 必须逐字引用,不得推算或更改】' + baziRelatives(r, gender === '女' ? '女' : '男') + '。';
|
|
67
|
+
}
|
|
68
|
+
} catch { /* 提取失败则不注入 */ }
|
|
69
|
+
return '';
|
|
70
|
+
}
|