|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { getUserProfileById, updateProfileCoreDocumentStatus, getDb, nowIso } from './database.js'; |
|
|
import { getCachedAnalysis, cacheAnalysis, computeBaziHash } from './cacheManager.js'; |
|
|
import { calculateLifeTimeline, generateFallbackKLine } from './baziCalculator.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const generateCoreDocument = async (profile, skipCache = false) => { |
|
|
try { |
|
|
console.log(`[CoreDocEngine] 开始生成核心文档 - Profile ID: ${profile.id}`); |
|
|
|
|
|
|
|
|
updateProfileCoreDocumentStatus(profile.id, 'generating'); |
|
|
|
|
|
|
|
|
const timelineData = calculateLifeTimeline({ |
|
|
birthYear: profile.birthYear, |
|
|
gender: profile.gender === 'male' ? 'Male' : 'Female', |
|
|
yearPillar: profile.yearPillar, |
|
|
monthPillar: profile.monthPillar, |
|
|
dayPillar: profile.dayPillar, |
|
|
hourPillar: profile.hourPillar, |
|
|
startAge: profile.startAge, |
|
|
firstDaYun: profile.firstDaYun |
|
|
}); |
|
|
|
|
|
console.log(`[CoreDocEngine] 时间线计算完成 - ${timelineData.timeline.length} 年`); |
|
|
|
|
|
|
|
|
const baziHash = computeBaziHash( |
|
|
profile.yearPillar, |
|
|
profile.monthPillar, |
|
|
profile.dayPillar, |
|
|
profile.hourPillar |
|
|
); |
|
|
|
|
|
|
|
|
let cachedAnalysis = null; |
|
|
if (!skipCache) { |
|
|
cachedAnalysis = getCachedAnalysis(baziHash, profile.gender); |
|
|
if (cachedAnalysis) { |
|
|
console.log(`[CoreDocEngine] 找到缓存分析 - Hash: ${baziHash}`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let coreDocument; |
|
|
if (cachedAnalysis) { |
|
|
|
|
|
coreDocument = { |
|
|
profileId: profile.id, |
|
|
baziHash, |
|
|
chartPoints: cachedAnalysis.klineData || [], |
|
|
personalityCore: cachedAnalysis.personalityCore, |
|
|
careerCore: cachedAnalysis.careerCore, |
|
|
wealthCore: cachedAnalysis.wealthCore, |
|
|
marriageCore: cachedAnalysis.marriageCore, |
|
|
healthCore: cachedAnalysis.healthCore, |
|
|
klineData: cachedAnalysis.klineData, |
|
|
peakYears: cachedAnalysis.peakYears, |
|
|
troughYears: cachedAnalysis.troughYears, |
|
|
cryptoCore: cachedAnalysis.cryptoCore, |
|
|
luckyElements: cachedAnalysis.luckyElements, |
|
|
physicalTraits: cachedAnalysis.physicalTraits, |
|
|
modelUsed: cachedAnalysis.modelUsed, |
|
|
generatedAt: nowIso(), |
|
|
fromCache: true |
|
|
}; |
|
|
} else { |
|
|
|
|
|
console.log(`[CoreDocEngine] 生成新的核心分析 - 使用降级算法`); |
|
|
|
|
|
const klineData = generateFallbackKLine(timelineData); |
|
|
|
|
|
|
|
|
const sortedByScore = [...klineData].sort((a, b) => b.score - a.score); |
|
|
const peakYears = sortedByScore.slice(0, 5).map(p => ({ |
|
|
year: p.year, |
|
|
age: p.age, |
|
|
score: p.score, |
|
|
reason: p.reason |
|
|
})); |
|
|
const troughYears = sortedByScore.slice(-5).reverse().map(p => ({ |
|
|
year: p.year, |
|
|
age: p.age, |
|
|
score: p.score, |
|
|
reason: p.reason |
|
|
})); |
|
|
|
|
|
|
|
|
coreDocument = { |
|
|
profileId: profile.id, |
|
|
baziHash, |
|
|
chartPoints: klineData, |
|
|
personalityCore: { |
|
|
content: '基于四柱八字的性格分析(降级版)', |
|
|
score: 5 |
|
|
}, |
|
|
careerCore: { |
|
|
content: '基于四柱八字的事业分析(降级版)', |
|
|
score: 5 |
|
|
}, |
|
|
wealthCore: { |
|
|
content: '基于四柱八字的财运分析(降级版)', |
|
|
score: 5 |
|
|
}, |
|
|
marriageCore: { |
|
|
content: '基于四柱八字的婚姻分析(降级版)', |
|
|
score: 5 |
|
|
}, |
|
|
healthCore: { |
|
|
content: '基于四柱八字的健康分析(降级版)', |
|
|
score: 5, |
|
|
bodyParts: [] |
|
|
}, |
|
|
klineData, |
|
|
peakYears, |
|
|
troughYears, |
|
|
cryptoCore: { |
|
|
content: '暂无币圈分析', |
|
|
score: 5 |
|
|
}, |
|
|
luckyElements: { |
|
|
colors: [], |
|
|
directions: [], |
|
|
zodiac: [], |
|
|
numbers: [] |
|
|
}, |
|
|
physicalTraits: { |
|
|
appearance: '', |
|
|
bodyType: '', |
|
|
skin: '', |
|
|
characterSummary: '' |
|
|
}, |
|
|
modelUsed: 'fallback_v1', |
|
|
generatedAt: nowIso(), |
|
|
fromCache: false |
|
|
}; |
|
|
|
|
|
|
|
|
cacheAnalysis({ |
|
|
baziHash, |
|
|
gender: profile.gender, |
|
|
structuralData: { |
|
|
bazi: [profile.yearPillar, profile.monthPillar, profile.dayPillar, profile.hourPillar], |
|
|
summaryScore: 5 |
|
|
}, |
|
|
personalityCore: coreDocument.personalityCore, |
|
|
careerCore: coreDocument.careerCore, |
|
|
wealthCore: coreDocument.wealthCore, |
|
|
marriageCore: coreDocument.marriageCore, |
|
|
healthCore: coreDocument.healthCore, |
|
|
klineData, |
|
|
peakYears, |
|
|
troughYears, |
|
|
cryptoCore: coreDocument.cryptoCore, |
|
|
luckyElements: coreDocument.luckyElements, |
|
|
physicalTraits: coreDocument.physicalTraits, |
|
|
modelUsed: 'fallback_v1', |
|
|
version: 1 |
|
|
}); |
|
|
|
|
|
console.log(`[CoreDocEngine] 核心分析已缓存 - Hash: ${baziHash}`); |
|
|
} |
|
|
|
|
|
|
|
|
updateProfileCoreDocumentStatus(profile.id, 'ready'); |
|
|
console.log(`[CoreDocEngine] 核心文档生成完成 - Profile ID: ${profile.id}`); |
|
|
|
|
|
return coreDocument; |
|
|
|
|
|
} catch (error) { |
|
|
console.error(`[CoreDocEngine] 生成核心文档失败:`, error); |
|
|
|
|
|
|
|
|
updateProfileCoreDocumentStatus(profile.id, 'failed'); |
|
|
|
|
|
throw new Error(`生成核心文档失败: ${error.message}`); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getCoreDocument = async (profileId) => { |
|
|
try { |
|
|
console.log(`[CoreDocEngine] 获取核心文档 - Profile ID: ${profileId}`); |
|
|
|
|
|
|
|
|
const profile = getUserProfileById(profileId); |
|
|
if (!profile) { |
|
|
throw new Error('档案不存在'); |
|
|
} |
|
|
|
|
|
|
|
|
const baziHash = computeBaziHash( |
|
|
profile.yearPillar, |
|
|
profile.monthPillar, |
|
|
profile.dayPillar, |
|
|
profile.hourPillar |
|
|
); |
|
|
|
|
|
|
|
|
const cachedAnalysis = getCachedAnalysis(baziHash, profile.gender); |
|
|
|
|
|
|
|
|
if (cachedAnalysis && cachedAnalysis.klineData && cachedAnalysis.klineData.length > 0) { |
|
|
const document = { |
|
|
profileId: profile.id, |
|
|
baziHash, |
|
|
chartPoints: cachedAnalysis.klineData, |
|
|
personalityCore: cachedAnalysis.personalityCore, |
|
|
careerCore: cachedAnalysis.careerCore, |
|
|
wealthCore: cachedAnalysis.wealthCore, |
|
|
marriageCore: cachedAnalysis.marriageCore, |
|
|
healthCore: cachedAnalysis.healthCore, |
|
|
klineData: cachedAnalysis.klineData, |
|
|
peakYears: cachedAnalysis.peakYears, |
|
|
troughYears: cachedAnalysis.troughYears, |
|
|
cryptoCore: cachedAnalysis.cryptoCore, |
|
|
luckyElements: cachedAnalysis.luckyElements, |
|
|
physicalTraits: cachedAnalysis.physicalTraits, |
|
|
modelUsed: cachedAnalysis.modelUsed, |
|
|
generatedAt: cachedAnalysis.createdAt, |
|
|
fromCache: true |
|
|
}; |
|
|
|
|
|
|
|
|
const validation = validateCoreDocument(document); |
|
|
|
|
|
console.log(`[CoreDocEngine] 核心文档已从缓存返回 - 验证分数: ${validation.score}`); |
|
|
|
|
|
return { |
|
|
document, |
|
|
validation, |
|
|
status: 'ready' |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
console.log(`[CoreDocEngine] 缓存未找到,生成新核心文档`); |
|
|
const document = await generateCoreDocument(profile); |
|
|
const validation = validateCoreDocument(document); |
|
|
|
|
|
return { |
|
|
document, |
|
|
validation, |
|
|
status: 'ready' |
|
|
}; |
|
|
|
|
|
} catch (error) { |
|
|
console.error(`[CoreDocEngine] 获取核心文档失败:`, error); |
|
|
throw new Error(`获取核心文档失败: ${error.message}`); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const validateCoreDocument = (doc) => { |
|
|
const missing = []; |
|
|
let score = 0; |
|
|
const maxScore = 100; |
|
|
|
|
|
|
|
|
if (!doc.chartPoints || !Array.isArray(doc.chartPoints)) { |
|
|
missing.push('chartPoints'); |
|
|
} else if (doc.chartPoints.length === 0) { |
|
|
missing.push('chartPoints (empty)'); |
|
|
} else if (doc.chartPoints.length < 90) { |
|
|
missing.push('chartPoints (不足100年)'); |
|
|
score += 10; |
|
|
} else { |
|
|
score += 30; |
|
|
} |
|
|
|
|
|
|
|
|
if (!doc.personalityCore || !doc.personalityCore.content) { |
|
|
missing.push('personality_core'); |
|
|
} else { |
|
|
score += 15; |
|
|
} |
|
|
|
|
|
|
|
|
if (!doc.careerCore || !doc.careerCore.content) { |
|
|
missing.push('career_core'); |
|
|
} else { |
|
|
score += 15; |
|
|
} |
|
|
|
|
|
|
|
|
if (!doc.klineData || !Array.isArray(doc.klineData)) { |
|
|
missing.push('kline_data'); |
|
|
} else if (doc.klineData.length === 0) { |
|
|
missing.push('kline_data (empty)'); |
|
|
} else { |
|
|
score += 20; |
|
|
} |
|
|
|
|
|
|
|
|
if (doc.wealthCore && doc.wealthCore.content) { |
|
|
score += 5; |
|
|
} |
|
|
|
|
|
|
|
|
if (doc.marriageCore && doc.marriageCore.content) { |
|
|
score += 5; |
|
|
} |
|
|
|
|
|
|
|
|
if (doc.healthCore && doc.healthCore.content) { |
|
|
score += 5; |
|
|
} |
|
|
|
|
|
|
|
|
if (doc.peakYears && Array.isArray(doc.peakYears) && doc.peakYears.length > 0) { |
|
|
score += 3; |
|
|
} |
|
|
if (doc.troughYears && Array.isArray(doc.troughYears) && doc.troughYears.length > 0) { |
|
|
score += 2; |
|
|
} |
|
|
|
|
|
|
|
|
if (doc.luckyElements && Object.keys(doc.luckyElements).length > 0) { |
|
|
score += 3; |
|
|
} |
|
|
|
|
|
|
|
|
if (doc.physicalTraits && Object.keys(doc.physicalTraits).length > 0) { |
|
|
score += 2; |
|
|
} |
|
|
|
|
|
const valid = missing.length === 0 && score >= 80; |
|
|
|
|
|
return { |
|
|
valid, |
|
|
missing, |
|
|
score, |
|
|
maxScore, |
|
|
message: valid |
|
|
? '核心文档完整' |
|
|
: `核心文档不完整,缺失字段: ${missing.join(', ')}` |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const regenerateCoreDocument = async (profileId, reason = '手动触发') => { |
|
|
try { |
|
|
console.log(`[CoreDocEngine] 重新生成核心文档 - Profile ID: ${profileId}, 原因: ${reason}`); |
|
|
|
|
|
|
|
|
const profile = getUserProfileById(profileId); |
|
|
if (!profile) { |
|
|
throw new Error('档案不存在'); |
|
|
} |
|
|
|
|
|
|
|
|
const baziHash = computeBaziHash( |
|
|
profile.yearPillar, |
|
|
profile.monthPillar, |
|
|
profile.dayPillar, |
|
|
profile.hourPillar |
|
|
); |
|
|
|
|
|
|
|
|
const db = getDb(); |
|
|
const deleteStmt = db.prepare(` |
|
|
DELETE FROM bazi_analysis_cache |
|
|
WHERE bazi_hash = ? AND gender = ? |
|
|
`); |
|
|
const result = deleteStmt.run(baziHash, profile.gender); |
|
|
|
|
|
if (result.changes > 0) { |
|
|
console.log(`[CoreDocEngine] 已删除旧缓存 - Hash: ${baziHash}, 删除条数: ${result.changes}`); |
|
|
} |
|
|
|
|
|
|
|
|
console.log(`[CoreDocEngine] 重新生成原因: ${reason}`); |
|
|
|
|
|
|
|
|
const newDocument = await generateCoreDocument(profile, true); |
|
|
|
|
|
console.log(`[CoreDocEngine] 核心文档重新生成完成 - Profile ID: ${profileId}`); |
|
|
|
|
|
return { |
|
|
document: newDocument, |
|
|
regenerated: true, |
|
|
reason, |
|
|
timestamp: nowIso() |
|
|
}; |
|
|
|
|
|
} catch (error) { |
|
|
console.error(`[CoreDocEngine] 重新生成核心文档失败:`, error); |
|
|
throw new Error(`重新生成核心文档失败: ${error.message}`); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const batchGenerateCoreDocuments = async (profileIds) => { |
|
|
console.log(`[CoreDocEngine] 批量生成核心文档 - 数量: ${profileIds.length}`); |
|
|
|
|
|
const results = { |
|
|
total: profileIds.length, |
|
|
success: 0, |
|
|
failed: 0, |
|
|
errors: [] |
|
|
}; |
|
|
|
|
|
for (const profileId of profileIds) { |
|
|
try { |
|
|
await generateCoreDocument({ id: profileId }); |
|
|
results.success++; |
|
|
} catch (error) { |
|
|
results.failed++; |
|
|
results.errors.push({ |
|
|
profileId, |
|
|
error: error.message |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(`[CoreDocEngine] 批量生成完成 - 成功: ${results.success}, 失败: ${results.failed}`); |
|
|
|
|
|
return results; |
|
|
}; |
|
|
|
|
|
export default { |
|
|
generateCoreDocument, |
|
|
getCoreDocument, |
|
|
validateCoreDocument, |
|
|
regenerateCoreDocument, |
|
|
batchGenerateCoreDocuments |
|
|
}; |
|
|
|