Files
Jyotisha/jyotish-app/ai-chat.js
T
2026-07-02 23:54:05 +08:00

573 lines
30 KiB
JavaScript
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.
/**
* AI Chat Module — 星盘咨询 · AI 对话
* 浮动按钮 + 星盘库(LocalStorage) + AI 对话面板
*/
import { SIGNS_CN, PLANET_CN, SIGNS } from './jyotish-engine.js';
import { t, getLang, signName, planetName } from './i18n.js';
const STORAGE_KEY = 'jyotish_chart_library';
const CHAT_CTX_KEY = 'jyotish_chat_context';
const DASHA_SHADBALA_AI_CALIBRATION_BOUNDARY = [
'【Dasha/Shadbala Calibration Status】',
'ready_for_calibration: 0',
'external_oracle_evidence_validation: valid_packets: 0',
'D1/D9/SAV 高可信;大运起点和 Shadbala 绝对值仍在外部 evidence validator 校准中。',
'不得把大运起点或 Shadbala 绝对值说成已完成外部校准;涉及具体日期/绝对力量值时必须说明当前只可作参考标定。',
].join('\n');
let _currentChartData = null;
let _selectedChartId = null;
let _panelEl = null;
let _fabEl = null;
let _authToken = null;
let _authUser = null;
let _guidedTopicContext = null;
// ============================================================================
// 初始化
// ============================================================================
export function initAIChat() {
createFAB();
createPanel();
}
// ============================================================================
// 设置当前 chartData(排盘后调用)
// ============================================================================
export function aiChatSetChartData(cd) {
_currentChartData = cd;
if (cd) {
_selectedChartId = buildChartId(cd);
refreshChartSelect();
}
}
export function openAIChatWithPrompt(prompt, guidedTopicContext = null) {
const text = String(prompt || '').trim();
if (!text) return;
_guidedTopicContext = guidedTopicContext && typeof guidedTopicContext === 'object'
? guidedTopicContext
: null;
if (_panelEl && !_panelEl.classList.contains('open')) {
_panelEl.classList.add('open');
}
const input = _panelEl?.querySelector('#ai-input');
if (!input) return;
input.value = text;
input.focus();
}
// ============================================================================
// 认证状态同步(由 auth.js 调用)
// ============================================================================
export function aiChatSetAuth(token, user) {
_authToken = token;
_authUser = user;
}
// ============================================================================
// 浮动按钮
// ============================================================================
function createFAB() {
_fabEl = document.createElement('button');
_fabEl.className = 'ai-fab';
_fabEl.innerHTML = `<span class="ai-fab-icon">✦</span><span class="ai-fab-text">${t('ai.fab.text')}</span>`;
_fabEl.title = t('ai.fab.title');
_fabEl.addEventListener('click', togglePanel);
document.body.appendChild(_fabEl);
// 首次使用:脉冲动画 + 引导气泡
if (!localStorage.getItem('jyotish_fab_seen')) {
_fabEl.classList.add('ai-fab-pulse');
const tip = document.createElement('div');
tip.className = 'ai-fab-tip';
tip.textContent = t('ai.fab.tip');
document.body.appendChild(tip);
// 点击后消失
const dismiss = () => {
localStorage.setItem('jyotish_fab_seen', '1');
_fabEl.classList.remove('ai-fab-pulse');
tip.style.opacity = '0';
setTimeout(() => tip.remove(), 300);
};
_fabEl.addEventListener('click', dismiss, {once: true});
// 8秒后自动消失
setTimeout(dismiss, 8000);
}
}
// ============================================================================
// 聊天面板
// ============================================================================
function createPanel() {
_panelEl = document.createElement('div');
_panelEl.className = 'ai-panel';
_panelEl.innerHTML = `
<div class="ai-panel-header">
<div class="ai-panel-title">${t('ai.panel.title')}</div>
<button class="ai-panel-close">&times;</button>
</div>
<div class="ai-chart-select">
<label>${t('ai.select.chart')}</label>
<select id="ai-chart-selector"></select>
<div class="ai-chart-actions">
<button class="ai-chart-btn" id="ai-save-chart">${t('ai.save.chart')}</button>
<button class="ai-chart-btn" id="ai-del-chart">${t('ai.delete')}</button>
</div>
</div>
<div class="ai-messages" id="ai-messages">
<div class="ai-msg system">${t('ai.welcome')}</div>
</div>
<div class="ai-input-area">
<input class="ai-input" id="ai-input" placeholder="${t('ai.placeholder')}" />
<button class="ai-send" id="ai-send">${t('ai.send')}</button>
</div>
`;
document.body.appendChild(_panelEl);
// 事件
_panelEl.querySelector('.ai-panel-close').addEventListener('click', closePanel);
_panelEl.querySelector('#ai-save-chart').addEventListener('click', saveCurrentChart);
_panelEl.querySelector('#ai-del-chart').addEventListener('click', deleteSelectedChart);
_panelEl.querySelector('#ai-send').addEventListener('click', sendMessage);
_panelEl.querySelector('#ai-input').addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
_panelEl.querySelector('#ai-chart-selector').addEventListener('change', e => {
_selectedChartId = e.target.value;
});
refreshChartSelect();
}
function togglePanel() {
_panelEl.classList.toggle('open');
}
function closePanel() {
_panelEl.classList.remove('open');
}
// ============================================================================
// 星盘库管理(LocalStorage
// ============================================================================
function getLibrary() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }
catch { return []; }
}
function saveLibrary(lib) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(lib));
}
function buildChartId(cd) {
if (!cd?.birth_info) return 'unknown';
const bi = cd.birth_info;
return `${bi.date}_${bi.lat}_${bi.lon}_${bi.tz}`;
}
function chartLabel(cd) {
if (!cd?.birth_info) return t('ai.unknown.chart');
const bi = cd.birth_info;
const asc = cd.ascendant;
return `${bi.date} ${asc ? signName(asc.sign) : ''} ↑ (${bi.lat}°,${bi.lon}°)`;
}
function refreshChartSelect() {
const sel = _panelEl?.querySelector('#ai-chart-selector');
if (!sel) return;
const lib = getLibrary();
sel.innerHTML = `<option value="">-- ${t('ai.select.saved')} --</option>`;
for (const entry of lib) {
const opt = document.createElement('option');
opt.value = entry.id;
opt.textContent = entry.label;
if (entry.id === _selectedChartId) opt.selected = true;
sel.appendChild(opt);
}
// 如果有当前星盘但未保存,添加临时选项
if (_currentChartData && !lib.find(e => e.id === _selectedChartId)) {
const opt = document.createElement('option');
opt.value = _selectedChartId || 'current';
opt.textContent = chartLabel(_currentChartData) + ` ${t('ai.current')}`;
opt.selected = true;
sel.appendChild(opt);
}
}
function saveCurrentChart() {
if (!_currentChartData) { addSystemMsg(t('ai.no.chart.gen')); return; }
const id = buildChartId(_currentChartData);
const lib = getLibrary();
if (lib.find(e => e.id === id)) { addSystemMsg(t('ai.chart.exists')); return; }
lib.push({
id,
label: chartLabel(_currentChartData),
data: _currentChartData,
savedAt: new Date().toISOString(),
});
saveLibrary(lib);
refreshChartSelect();
addSystemMsg(t('ai.chart.saved'));
}
function deleteSelectedChart() {
const sel = _panelEl?.querySelector('#ai-chart-selector');
if (!sel || !sel.value) { addSystemMsg(t('ai.select.first')); return; }
let lib = getLibrary();
lib = lib.filter(e => e.id !== sel.value);
saveLibrary(lib);
refreshChartSelect();
addSystemMsg(t('ai.chart.deleted'));
}
function getSelectedChartData() {
const sel = _panelEl?.querySelector('#ai-chart-selector');
if (!sel || !sel.value) return _currentChartData;
// 如果选的是当前星盘
if (_currentChartData && buildChartId(_currentChartData) === sel.value) return _currentChartData;
// 从库中查找
const lib = getLibrary();
const entry = lib.find(e => e.id === sel.value);
return entry?.data || _currentChartData;
}
// ============================================================================
// 对话系统
// ============================================================================
function addSystemMsg(text) {
const box = _panelEl?.querySelector('#ai-messages');
if (!box) return;
const div = document.createElement('div');
div.className = 'ai-msg system';
div.textContent = text;
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
function addUserMsg(text) {
const box = _panelEl?.querySelector('#ai-messages');
if (!box) return;
const div = document.createElement('div');
div.className = 'ai-msg user';
div.textContent = text;
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
function addAssistantMsg(text) {
const box = _panelEl?.querySelector('#ai-messages');
if (!box) return;
const div = document.createElement('div');
div.className = 'ai-msg assistant';
div.innerHTML = formatMsg(text);
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
function formatMsg(text) {
// 简单 markdown: **bold**, \n -> <br>
return text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>');
}
async function sendMessage() {
const input = _panelEl?.querySelector('#ai-input');
const sendBtn = _panelEl?.querySelector('#ai-send');
if (!input || !sendBtn) return;
const text = input.value.trim();
if (!text) return;
addUserMsg(text);
input.value = '';
sendBtn.disabled = true;
const cd = getSelectedChartData();
if (!cd) {
addAssistantMsg(t('ai.no.chart.or.saved'));
sendBtn.disabled = false;
return;
}
// 构建星盘摘要上下文
const context = buildChartContext(cd, _guidedTopicContext);
// 调用 AI 对话
try {
const reply = await callAI(text, context);
addAssistantMsg(reply);
} catch (err) {
addAssistantMsg(t('ai.error.prefix') + err.message);
}
sendBtn.disabled = false;
}
function buildChartContext(cd, guidedTopicContext = null) {
if (!cd?.planets || !cd?.ascendant) return t('ai.no.data');
const professionalReadingContext = buildProfessionalReadingContext(cd);
if (cd.ai_prompt_pack?.prompt_zh && cd.ai_prompt_pack?.evidence_snapshot) {
const workflow = cd._consultationWorkflow || {};
const runtimePlanner = workflow.runtime_planner || {};
const officialSnapshot = cd.ai_prompt_pack.evidence_snapshot.vedastro_official_full_snapshot || {};
const vedastroOverview = cd.ai_prompt_pack.evidence_snapshot.vedastro_overview || {};
const strictContracts = officialSnapshot.strict_workflow_contracts || {};
const primaryRoute = officialSnapshot.strict_workflow_primary_route || Object.keys(strictContracts)[0] || '';
const topReaderContract = primaryRoute ? (strictContracts[primaryRoute] || {}) : {};
const adjudicationStages = topReaderContract.adjudication_stages || {};
const predictionBoundaryContract = topReaderContract.prediction_boundary_contract || cd.ai_prompt_pack.evidence_snapshot.prediction_boundary_contract || {};
const multiReferenceSummary = topReaderContract.multi_reference_reading_summary || {};
const techniqueAuditSummary = topReaderContract.technique_audit_summary || {};
const runtimePlannerBoundary = runtimePlanner && typeof runtimePlanner === 'object' && Object.keys(runtimePlanner).length
? [
'【Runtime Planner】',
`planner=${runtimePlanner.planner_name || 'UnifiedConsultationRuntimePlanner'} · entry=${runtimePlanner.entry_mode || workflow.entry_mode || '-'} · route=${runtimePlanner.route?.question_type || workflow.routing?.question_type || '-'}`,
`sync_steps=${(runtimePlanner.sync_steps || []).join(' -> ') || '-'}`,
`async_candidates=${(runtimePlanner.async_candidates || []).join(' / ') || '-'}`,
].join('\n')
: '';
const officialBoundary = officialSnapshot && typeof officialSnapshot === 'object'
? [
'【VedAstro Official Snapshot Boundary】',
`status=${officialSnapshot.status || 'blocked'} · source=${officialSnapshot.primary_source || 'vedastro_official'} · official_python_path=${officialSnapshot.official_python_path || '-'}`,
`bundle=${officialSnapshot.official_bundle_status || 'blocked'} · chart_available=${officialSnapshot.official_chart_available ? 'yes' : 'no'}`,
`full_catalog=${officialSnapshot.official_full_capability_catalog_status || 'blocked'} · executed=${officialSnapshot.official_full_capability_catalog_summary?.executed_method_count || 0}/${officialSnapshot.official_full_capability_catalog_summary?.catalog_method_count || 0} · sample_limit=${officialSnapshot.official_full_capability_catalog_summary?.sample_limit || 0}`,
].join('\n')
: '';
const topReaderBoundary = topReaderContract && typeof topReaderContract === 'object' && Object.keys(topReaderContract).length
? [
'【Top Reader Contract】',
`route=${primaryRoute || '-'} · verdict=${topReaderContract.verdict || '-'} · dominant_label=${topReaderContract.dominant_label || '-'}`,
`adjudication_stages.promise=${adjudicationStages.promise?.status || 'missing'} · activation=${adjudicationStages.activation?.status || 'missing'} · manifestation=${adjudicationStages.manifestation?.status || 'missing'} · label=${adjudicationStages.label?.value || topReaderContract.dominant_label || '-'}`,
`technique_audit_summary.functional=${techniqueAuditSummary.functional_benefic_malefic?.gate || 'none'}/${techniqueAuditSummary.functional_benefic_malefic?.used ? 'used' : 'blocked'} · vargas=${(techniqueAuditSummary.relevant_vargas?.present_keys || []).join('/') || 'none'} · dual_dasha=${techniqueAuditSummary.vimshottari_narayana_crosscheck?.used ? 'used' : 'blocked'}`,
`multi_reference_reading_summary.root_frame=${Object.keys(multiReferenceSummary.root_frame || {}).join('/') || 'none'} · modifier_frame=${Object.keys(multiReferenceSummary.modifier_frame || {}).join('/') || 'none'}`,
`main_conflicts=${(topReaderContract.main_conflicts || []).map(item => item?.type).filter(Boolean).join('/') || 'none'}`,
].join('\n')
: '';
const predictionBoundary = predictionBoundaryContract && typeof predictionBoundaryContract === 'object' && Object.keys(predictionBoundaryContract).length
? [
'【Prediction Boundary Contract】',
`source_refs=${(predictionBoundaryContract.source_refs || []).join(' / ') || '-'}`,
`promise=${predictionBoundaryContract.promise?.status || adjudicationStages.promise?.status || 'missing'} · activation=${predictionBoundaryContract.activation?.status || adjudicationStages.activation?.status || 'missing'} · manifestation=${predictionBoundaryContract.manifestation?.status || adjudicationStages.manifestation?.status || 'missing'} · label=${predictionBoundaryContract.label?.value || topReaderContract.dominant_label || '-'}`,
`mevg=${predictionBoundaryContract.confidence_boundary?.mevg_status || 'blocked'} · real_case=${predictionBoundaryContract.confidence_boundary?.real_case_calibration_status || 'blocked'} · policy=${predictionBoundaryContract.confidence_boundary?.unverified_claim_policy || 'downgrade_or_block'}`,
].join('\n')
: '';
const vedastroBoundary = vedastroOverview && typeof vedastroOverview === 'object'
? [
'【VedAstro Overview Boundary】',
`status=${vedastroOverview.status || 'blocked'} · scope=${vedastroOverview.search_scope || 'single_day_overview'} · source=${vedastroOverview.source || 'vedastro_service_adapter_candidate'}`,
'overview only,不替代长周期精扫。',
].join('\n')
: '';
const guidedTopicBoundary = guidedTopicContext && typeof guidedTopicContext === 'object'
? [
'【Guided Topic Context】',
`topic_id=${guidedTopicContext.id || '-'} · title=${guidedTopicContext.title || '-'} · confidence=${guidedTopicContext.confidence || '-'}`,
`guided_topic_context.functional=${(guidedTopicContext.strict_adjudication_bundle?.strict_audit_gate || guidedTopicContext.strict_audit_gate)?.functional_benefic_malefic?.gate || 'none'}/${(guidedTopicContext.strict_adjudication_bundle?.strict_audit_gate || guidedTopicContext.strict_audit_gate)?.functional_benefic_malefic?.used ? 'used' : 'blocked'} · vargas=${((guidedTopicContext.strict_adjudication_bundle?.strict_audit_gate || guidedTopicContext.strict_audit_gate)?.relevant_vargas?.present_keys || []).join('/') || 'none'} · dual_dasha=${(guidedTopicContext.strict_adjudication_bundle?.strict_audit_gate || guidedTopicContext.strict_audit_gate)?.vimshottari_narayana_crosscheck?.used ? 'used' : 'blocked'}`,
`monthly_adjudication_summary=${(guidedTopicContext.strict_adjudication_bundle?.monthly_adjudication_summary || guidedTopicContext.monthly_adjudication_summary)?.primary_state?.value ? `${(guidedTopicContext.strict_adjudication_bundle?.monthly_adjudication_summary || guidedTopicContext.monthly_adjudication_summary).primary_state.value} / ${(guidedTopicContext.strict_adjudication_bundle?.monthly_adjudication_summary || guidedTopicContext.monthly_adjudication_summary).manifestation_mode?.value || '-'} / ${(guidedTopicContext.strict_adjudication_bundle?.monthly_adjudication_summary || guidedTopicContext.monthly_adjudication_summary).friction_source?.value || '-'} / ${(guidedTopicContext.strict_adjudication_bundle?.monthly_adjudication_summary || guidedTopicContext.monthly_adjudication_summary).time_confidence?.value || '-'}` : 'none'}`,
`official_day_signal_summary=${(guidedTopicContext.strict_adjudication_bundle?.official_day_signal_summary || guidedTopicContext.official_day_signal_summary)?.top_day ? `${(guidedTopicContext.strict_adjudication_bundle?.official_day_signal_summary || guidedTopicContext.official_day_signal_summary).top_day.date || '-'} / ${(guidedTopicContext.strict_adjudication_bundle?.official_day_signal_summary || guidedTopicContext.official_day_signal_summary).top_day.summary || '-'} / ${(guidedTopicContext.strict_adjudication_bundle?.official_day_signal_summary || guidedTopicContext.official_day_signal_summary).top_day.confidence || '-'}` : 'none'}`,
`why=${guidedTopicContext.why_worth_exploring || '-'}`,
].join('\n')
: '';
return [
professionalReadingContext,
'',
'【AI Prompt Pack】',
cd.ai_prompt_pack.prompt_zh,
'',
DASHA_SHADBALA_AI_CALIBRATION_BOUNDARY,
'',
runtimePlannerBoundary,
'',
officialBoundary,
'',
topReaderBoundary,
'',
predictionBoundary,
'',
guidedTopicBoundary,
'',
vedastroBoundary,
'',
'【evidence_snapshot】',
JSON.stringify(cd.ai_prompt_pack.evidence_snapshot, null, 2),
'',
'【retrieval_plan】',
JSON.stringify(cd.ai_prompt_pack.retrieval_plan || {}, null, 2),
].join('\n');
}
const asc = cd.ascendant;
const bi = cd.birth_info;
let ctx = `${t('ai.no.data').replace(t('ai.no.data'), 'Chart Info')}\n${t('label.date')}: ${bi?.date || '?'}\nAscendant: ${signName(asc.sign)} (${asc.sign}) ${asc.degree?.toFixed(2) || ''}°\n\n[Planets]\n`;
const order = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu'];
for (const pn of order) {
const p = cd.planets[pn];
if (!p || p.error) continue;
ctx += `${planetName(pn)}: ${signName(p.sign)} ${p.degree_in_sign?.toFixed(2) || ''}° H${p.house} ${p.status || ''} ${p.retrograde ? 'R' : ''} ${p.nakshatra || ''}\n`;
}
return `${professionalReadingContext}\n\n${ctx}\n${DASHA_SHADBALA_AI_CALIBRATION_BOUNDARY}`;
}
function buildProfessionalReadingContext(cd) {
const packet = cd?.professional_reading || window.__jyotishProfessionalReading?.professional_reading || null;
if (!packet || typeof packet !== 'object') {
return [
'【Professional Reading Packet】',
'status=not_loaded',
'professional_reading: absent',
'VedAstro Gateway Boundary: not_loaded',
'user_led_calibration_controls: absent',
].join('\n');
}
const gateway = packet.vedastro_gateway || {};
const controls = packet.user_led_calibration_controls || {};
const requiredRows = packet.technique_audit_table_required_rows || [];
return [
'【Professional Reading Packet】',
`status=${gateway.status || 'unknown'} · professional_reading=loaded`,
`VedAstro Gateway Boundary: ${gateway.user_visibility?.boundary || gateway.gateway_status?.boundary || 'blocked_or_not_reported'}`,
`user_led_calibration_controls: blind_mode=${Boolean(controls.blind_mode)} · disable_life_event_feedback=${Boolean(controls.disable_life_event_feedback)}`,
`required_audit_rows=${requiredRows.join(' / ') || 'Functional Benefic/Malefic / MEVG / Real Case Calibration / VedAstro Gateway Boundary'}`,
].join('\n');
}
function buildAISetupGuidance() {
const lang = getLang();
if (lang === 'en') {
return `💡 **${t('ai.setup.title')}**\n${t('ai.setup.server')}\n${t('ai.setup.secret')}\n${t('ai.setup.trust')}`;
}
return `💡 **${t('ai.setup.title')}**\n${t('ai.setup.server')}\n${t('ai.setup.secret')}\n${t('ai.setup.trust')}`;
}
// ============================================================================
// AI API 调用(对接后端 Jyotish Server
// ============================================================================
function getApiBase() {
if (window.JYOTISH_API_BASE) return window.JYOTISH_API_BASE;
if (import.meta.env?.VITE_JYOTISH_API_BASE) return import.meta.env.VITE_JYOTISH_API_BASE;
if (window.Capacitor?.isNativePlatform?.()) return localStorage.getItem('jyotish_api_base') || '';
return ''; // 同域部署
}
async function callAI(userMessage, chartContext) {
const apiBase = getApiBase();
// 优先使用后端 API(需要登录)
if (_authToken && apiBase !== undefined) {
try {
const cd = getSelectedChartData();
const resp = await fetch(`${apiBase}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${_authToken}`,
},
body: JSON.stringify({
message: userMessage,
chart_context: chartContext,
chart_data: cd,
guided_topic_context: _guidedTopicContext,
}),
});
const data = await parseAIResponse(resp);
if (!resp.ok) {
// 401 = token 过期
if (resp.status === 401) {
return '**登录已过期,请重新登录后继续对话。**\n\n点击页面右上角头像,退出后重新登录即可。';
}
// 429 = 超出限额
if (resp.status === 429) {
const todayUsage = data.todayUsage ?? '?';
const limit = data.limit ?? 3;
return `**今日对话次数已用完 (${todayUsage}/${limit})**\n\n免费用户每日 3 次 AI 对话。升级高级会员可享无限对话。\n\n点击右上角头像 → 升级高级会员。`;
}
throw new Error(data.error || data.message || `服务器错误 (${resp.status})`);
}
// 更新用量显示
if (data.todayUsage !== undefined && _authUser) {
_authUser.subscription = _authUser.subscription || {};
_authUser.subscription.todayUsage = data.todayUsage;
_authUser.subscription.limit = data.limit;
// 通知 auth 刷新 UI
localStorage.setItem('jyotish_auth_user', JSON.stringify(_authUser));
document.querySelectorAll('[data-auth-header]').forEach(el => {
// 触发 auth UI 更新(通过自定义事件)
el.dispatchEvent(new CustomEvent('jyotish:usage-update'));
});
}
return data.reply || data.message || data.content || t('ai.no.reply');
} catch (err) {
if (err.message.includes('登录已过期') || err.message.includes('次数已用完') || err.message.includes('expired') || err.message.includes('limit')) {
return err.message;
}
console.warn('[AI Chat] Backend API failed, falling back:', err);
return `${buildAIRecoveryMessage(err)}\n\n${generateLocalReply(userMessage, chartContext)}`;
}
}
// 未登录提示
if (!_authToken) {
return `**请先登录以使用 AI 占星师对话**\n\n登录后每日可免费对话 3 次。\n\n点击页面右上角「登录」按钮即可注册/登录。\n\n${buildAISetupGuidance()}`;
}
// 兜底:本地回复
return generateLocalReply(userMessage, chartContext);
}
async function parseAIResponse(resp) {
const raw = await resp.text();
try {
return raw ? JSON.parse(raw) : {};
} catch {
return { message: raw?.slice(0, 180) || '' };
}
}
function buildAIRecoveryMessage(error) {
const message = error?.message || error || '本地 API 未连接';
return `**服务端 AI 对话暂不可用**\n\n${message}\n\n请到 Trust Center 运行健康检查;如本地 API 未连接,请按 README 的普通用户启动路径启动网页服务和本地 API 服务后重试。\n\n${buildAISetupGuidance()}`;
}
function generateLocalReply(message, ctx) {
const cd = getSelectedChartData();
const asc = cd?.ascendant;
const promptPack = cd?.ai_prompt_pack;
const packAyanamsa = promptPack?.evidence_snapshot?.ayanamsa;
const parameterBoundary = packAyanamsa
? `\n\n参数:${packAyanamsa.display || packAyanamsa.name || 'Lahiri'} Ayanamsa;节点口径 ${packAyanamsa.node_mode || 'mean'}。AI Prompt Pack 已作为上下文入口;完整生成式解读需要服务端 /api/chat。`
: '';
const calibrationBoundary = `\n\n${DASHA_SHADBALA_AI_CALIBRATION_BOUNDARY}`;
const contextBoundary = `${parameterBoundary}${calibrationBoundary}`;
const lower = message.toLowerCase();
const lang = getLang();
if (lower.includes('事业') || lower.includes('工作') || lower.includes('职业') || lower.includes('career') || lower.includes('job')) {
const h10 = cd?.planets ? Object.entries(cd.planets).filter(([,p]) => p.house === 10).map(([pn]) => planetName(pn)) : [];
return lang === 'en'
? `**Career Analysis (D1 Rasi)**\n\nAscendant ${signName(asc?.sign)} — 10th House:\n${h10.length > 0 ? '- Planets in H10: ' + h10.join(', ') : '- No planets in H10'}\n\n⚠️ Full career analysis requires D10, Dasha cycles, and Transit.\n\n💡 Configure AI backend for deeper insights.`
: `**事业分析(基于 D1 本命盘)**\n\n上升 ${signName(asc?.sign)} 的第10宫:\n${h10.length > 0 ? '- 10宫内行星: ' + h10.join('、') : '- 10宫无行星落入'}\n\n⚠️ 完整职业分析需 D10、Dasha 大运、Transit 等。${contextBoundary}\n\n💡 建议配置 AI 后端获取更深入的分析。`;
}
if (lower.includes('婚姻') || lower.includes('感情') || lower.includes('配偶') || lower.includes('恋爱') || lower.includes('marriage') || lower.includes('love') || lower.includes('spouse')) {
const h7 = cd?.planets ? Object.entries(cd.planets).filter(([,p]) => p.house === 7).map(([pn]) => planetName(pn)) : [];
return lang === 'en'
? `**Marriage & Relationship (D1 Rasi)**\n\nAscendant ${signName(asc?.sign)} — 7th House:\n${h7.length > 0 ? '- Planets in H7: ' + h7.join(', ') : '- No planets in H7'}\n\n⚠️ Full analysis needs DK, D9, Vimshottari Venus periods.\n\n💡 Configure AI backend for complete reading.`
: `**婚姻感情分析(基于 D1 本命盘)**\n\n上升 ${signName(asc?.sign)} 的第7宫:\n${h7.length > 0 ? '- 7宫内行星: ' + h7.join('、') : '- 7宫无行星落入'}\n\n⚠️ 完整婚姻分析需 DK、D9、Dasha 等。${contextBoundary}\n\n💡 配置 AI 后端获取完整解读。`;
}
if (lower.includes('财运') || lower.includes('财富') || lower.includes('收入') || lower.includes('wealth') || lower.includes('money') || lower.includes('finance')) {
return lang === 'en'
? `**Wealth Analysis (D1 Rasi)**\n\nAscendant ${signName(asc?.sign)}:\n- H2 (earned income) and H11 (gains) are key houses\n- Jupiter and Venus status directly affects wealth potential\n\n⚠️ Full analysis needs D2, Dasha, and Transit.\n\n💡 Configure AI backend for deeper wealth reading.`
: `**财运分析(基于 D1 本命盘)**\n\n上升 ${signName(asc?.sign)}:\n- 第2宫(正财)和第11宫(收入)是关键宫位\n- Jupiter 和 Venus 的状态直接影响财富潜力\n\n⚠️ 完整分析需 D2、Dasha 和 Transit。${contextBoundary}\n\n💡 配置 AI 后端获取深度财运解读。`;
}
if (lower.includes('健康') || lower.includes('身体') || lower.includes('health')) {
return lang === 'en'
? `**Health Analysis (D1 Rasi)**\n\nAscendant ${signName(asc?.sign)}:\n- H1 represents body and vitality\n- H6 represents disease\n- H8 represents chronic health issues\n\n⚠️ Full analysis requires D6 (Shashtamsa).\n\n💡 Configure AI backend for deeper health reading.`
: `**健康分析(基于 D1 本命盘)**\n\n上升 ${signName(asc?.sign)}:\n- 第1宫代表身体和生命力\n- 第6宫代表疾病\n- 第8宫代表慢性健康问题\n\n⚠️ 完整分析需 D6。${contextBoundary}\n\n💡 配置 AI 后端获取深度健康解读。`;
}
return lang === 'en'
? `**Chart Overview**\n\nAscendant: ${signName(asc?.sign)} ${asc?.degree?.toFixed(2) || ''}°\n\nYou can ask about:\n- Career\n- Marriage & relationships\n- Wealth\n- Health\n- Dasha analysis\n- Transit impacts\n\n${buildAISetupGuidance()}`
: `**星盘概览**\n\n上升: ${signName(asc?.sign)} ${asc?.degree?.toFixed(2) || ''}°${contextBoundary}\n\n你可以询问以下话题:\n- 事业运 / 工作方向\n- 婚姻感情\n- 财运分析\n- 健康运势\n- Dasha 大运分析\n- Transit 过境影响\n\n${buildAISetupGuidance()}`;
}