Add web professional reading gateway surface

This commit is contained in:
732642856
2026-07-02 23:54:05 +08:00
parent 329523fa7c
commit b33c2f9cd2
8 changed files with 306 additions and 1 deletions
+21
View File
@@ -0,0 +1,21 @@
# Mainland China VedAstro Gateway mode.
# Copy to .env.local on the backend host. Do not expose secrets in the browser.
VEDASTRO_GATEWAY_MODE=cn_gateway
VEDASTRO_SELF_HOST_ENDPOINT=https://your-domain.example.com/vedastro
# Optional official upstream. Keep it backend-only.
# VEDASTRO_API_ENDPOINT=https://api.vedastro.org/api
# VEDASTRO_API_KEY=
# VEDASTRO_ENABLE_NETWORK=0
# Cache / TTL / free-tier queue. These keep ordinary users from calling upstream directly.
VEDASTRO_CACHE_TTL_SECONDS=604800
VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS=604800
VEDASTRO_GATEWAY_QUEUE_ENABLED=1
VEDASTRO_FREE_TIER_QUEUE=1
VEDASTRO_FAIL_OPEN_LOCAL=1
# Runtime guardrails.
VEDASTRO_TIMEOUT_SECONDS=20
VEDASTRO_FULL_CATALOG_SAMPLE_LIMIT=0
+27
View File
@@ -77,6 +77,33 @@ VEDASTRO_ENABLE_NETWORK=1
默认运行是快速模式:VedAstro official 证据层如果没有在前台预算内闭环,会诚实标记 `official_snapshot_budget_exhausted` 并退回本地 Swiss Ephemeris。要跑 official extended 模式,复制 `.env.official.example``.env.local` 并填好 endpoint/network/key;运行 `python3 scripts/diagnose_vedastro_mode.py` 可先确认当前是 `fast_local_fallback` 还是 `official_extended` 默认运行是快速模式:VedAstro official 证据层如果没有在前台预算内闭环,会诚实标记 `official_snapshot_budget_exhausted` 并退回本地 Swiss Ephemeris。要跑 official extended 模式,复制 `.env.official.example``.env.local` 并填好 endpoint/network/key;运行 `python3 scripts/diagnose_vedastro_mode.py` 可先确认当前是 `fast_local_fallback` 还是 `official_extended`
### 中国大陆用户:VedAstro Gateway 模式
普通中国大陆用户不需要、也不应该让浏览器直连 VedAstro。推荐部署方式是:网页只访问你自己的本地或云端后端;后端通过 `VedAstro Gateway` 统一管理 self-host、official upstream、TTL/cache、free-tier queue 和 local fallback。
最短配置:
```bash
cp .env.cn.example .env.local
python3 scripts/jyotish_api_server.py --host 127.0.0.1 --port 5200
cd jyotish-app && npm run dev -- --host 127.0.0.1 --port 5173
```
网页侧使用:
- `Trust Center -> Web Professional Reading v1`
- `/api/vedastro_gateway/status` 查看当前后端策略
- `/api/vedastro_gateway/run` 生成 VedAstro-compatible evidence packet
- `/api/professional_reading` 生成网页专业解盘包
关键边界:
- 不要让浏览器直连 VedAstro,也不要把 `VEDASTRO_API_KEY` 放进前端。
- `VEDASTRO_CACHE_TTL_SECONDS``VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS` 用于缓存官方或自建服务结果。
- `VEDASTRO_GATEWAY_QUEUE_ENABLED=1` / `VEDASTRO_FREE_TIER_QUEUE=1` 用于把昂贵或被限流的外部请求排队。
- 如果 VedAstro 官方或自建服务不可达,`VEDASTRO_FAIL_OPEN_LOCAL=1` 会保持本地 Jyotish 引擎继续输出,并在 Technique Audit Table 里降级标注。
- Gateway 不会默认声称跑完 641 项;它只把 capability catalog、dynamic selection、cache/queue/fallback 状态作为证据边界交给 strict workflow。
### Codex 用户级 VedAstro + strict workflow 入口 ### Codex 用户级 VedAstro + strict workflow 入口
如果用户在 Codex 窗口从云端 Git 仓库拉取本项目,推荐先走这一条稳定入口,而不是手动拼多个底层脚本: 如果用户在 Codex 窗口从云端 Git 仓库拉取本项目,推荐先走这一条稳定入口,而不是手动拼多个底层脚本:
+27 -1
View File
@@ -304,6 +304,7 @@ async function sendMessage() {
function buildChartContext(cd, guidedTopicContext = null) { function buildChartContext(cd, guidedTopicContext = null) {
if (!cd?.planets || !cd?.ascendant) return t('ai.no.data'); 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) { if (cd.ai_prompt_pack?.prompt_zh && cd.ai_prompt_pack?.evidence_snapshot) {
const workflow = cd._consultationWorkflow || {}; const workflow = cd._consultationWorkflow || {};
const runtimePlanner = workflow.runtime_planner || {}; const runtimePlanner = workflow.runtime_planner || {};
@@ -368,6 +369,8 @@ function buildChartContext(cd, guidedTopicContext = null) {
].join('\n') ].join('\n')
: ''; : '';
return [ return [
professionalReadingContext,
'',
'【AI Prompt Pack】', '【AI Prompt Pack】',
cd.ai_prompt_pack.prompt_zh, cd.ai_prompt_pack.prompt_zh,
'', '',
@@ -401,7 +404,30 @@ function buildChartContext(cd, guidedTopicContext = null) {
if (!p || p.error) continue; 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`; ctx += `${planetName(pn)}: ${signName(p.sign)} ${p.degree_in_sign?.toFixed(2) || ''}° H${p.house} ${p.status || ''} ${p.retrograde ? 'R' : ''} ${p.nakshatra || ''}\n`;
} }
return `${ctx}\n${DASHA_SHADBALA_AI_CALIBRATION_BOUNDARY}`; 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() { function buildAISetupGuidance() {
+15
View File
@@ -240,6 +240,18 @@ async function runVedAstroRangeScan(payload) {
return postJson('/api/vedastro/range_scan', payload); return postJson('/api/vedastro/range_scan', payload);
} }
async function getVedAstroGatewayStatus() {
return fetchJson('/api/vedastro_gateway/status');
}
async function runVedAstroGateway(payload) {
return postJson('/api/vedastro_gateway/run', payload);
}
async function runProfessionalReading(payload) {
return postJson('/api/professional_reading', payload);
}
async function getTechniqueCatalog() { async function getTechniqueCatalog() {
let lastError = null; let lastError = null;
for (const base of getApiBases(true)) { for (const base of getApiBases(true)) {
@@ -465,6 +477,9 @@ window.JyotishAPI = {
getCapabilityAudit, getCapabilityAudit,
getVedAstroStatus, getVedAstroStatus,
runVedAstroRangeScan, runVedAstroRangeScan,
getVedAstroGatewayStatus,
runVedAstroGateway,
runProfessionalReading,
getTechniqueCatalog, getTechniqueCatalog,
runTechniqueExample, runTechniqueExample,
computeAnnual, computeAnnual,
+60
View File
@@ -66,6 +66,10 @@ import { escapeHtml, escapeAttr, safeNumber } from './security.js';
import { renderSkillCoverage } from './skill-map.js'; import { renderSkillCoverage } from './skill-map.js';
import { initChartImport } from './import-chart.js'; import { initChartImport } from './import-chart.js';
import { buildMEVGAudit, renderMEVGAudit } from './mevg-audit.js'; import { buildMEVGAudit, renderMEVGAudit } from './mevg-audit.js';
import {
buildProfessionalReadingPayload,
renderProfessionalReadingPanel,
} from './professional-reading.js';
// 合并所有 Yoga 定义 // 合并所有 Yoga 定义
const ALL_YOGA_DEFS = [...YOGA_DEFINITIONS, ...YOGA_EXTENDED_A, ...YOGA_EXTENDED_B]; const ALL_YOGA_DEFS = [...YOGA_DEFINITIONS, ...YOGA_EXTENDED_A, ...YOGA_EXTENDED_B];
@@ -93,6 +97,7 @@ const PANCHANGA_CONDITIONS = [
['avoid_new_start', '不宜新开始'], ['avoid_new_start', '不宜新开始'],
['good_choghadiya', '有吉利 Choghadiya'], ['good_choghadiya', '有吉利 Choghadiya'],
]; ];
let professionalReadingState = {};
const PANCHANGA_CONDITION_GUIDE = { const PANCHANGA_CONDITION_GUIDE = {
all: '显示范围内所有日期。', all: '显示范围内所有日期。',
has_vrata: '当天存在 vrata、lunar observance 或节日候选标签。', has_vrata: '当天存在 vrata、lunar observance 或节日候选标签。',
@@ -2361,6 +2366,9 @@ function renderTrustCenterPanel() {
</div> </div>
${renderRuntimeHealthPanel(runtime)} ${renderRuntimeHealthPanel(runtime)}
${renderVedAstroUserScanPanel()} ${renderVedAstroUserScanPanel()}
<div id="professional-reading-host">
${renderProfessionalReadingPanel(professionalReadingState)}
</div>
${renderValidationTransparencyPanel()} ${renderValidationTransparencyPanel()}
${renderDashaShadbalaCalibrationPanel()} ${renderDashaShadbalaCalibrationPanel()}
${renderOracleEvidenceIntakePanel()} ${renderOracleEvidenceIntakePanel()}
@@ -3244,6 +3252,52 @@ async function runVedAstroRangeScanFromPanel(panel) {
renderAll(); renderAll();
} }
async function refreshProfessionalReadingGatewayStatus() {
const status = $('professional-reading-status') || $('trust-center-status');
professionalReadingState = { ...professionalReadingState, message: '正在刷新 VedAstro Gateway...' };
if (status) status.textContent = professionalReadingState.message;
try {
const gatewayStatus = await window.JyotishAPI.getVedAstroGatewayStatus();
professionalReadingState = {
...professionalReadingState,
gatewayStatus,
message: 'VedAstro Gateway 状态已刷新。',
};
} catch (error) {
professionalReadingState = {
...professionalReadingState,
message: `VedAstro Gateway 状态未完成:${error?.message || '本地 API 未连接'}`,
};
}
renderAll();
}
async function runProfessionalReadingFromPanel() {
const status = $('professional-reading-status') || $('trust-center-status');
professionalReadingState = { ...professionalReadingState, message: '正在运行专业解盘代理...' };
if (status) status.textContent = professionalReadingState.message;
try {
const payload = buildProfessionalReadingPayload(chartData || {});
const result = await window.JyotishAPI.runProfessionalReading(payload);
professionalReadingState = {
...professionalReadingState,
result,
gatewayStatus: result?.professional_reading?.vedastro_gateway?.gateway_status || professionalReadingState.gatewayStatus || {},
message: '专业解盘包已生成。',
};
if (chartData) {
chartData.professional_reading = result?.professional_reading || result;
window.__jyotishProfessionalReading = result;
}
} catch (error) {
professionalReadingState = {
...professionalReadingState,
message: `专业解盘未完成:${error?.message || '本地 API 未连接'}`,
};
}
renderAll();
}
async function promptPWAInstall() { async function promptPWAInstall() {
const status = $('trust-center-status'); const status = $('trust-center-status');
const prompt = window.__jyotishDeferredInstallPrompt; const prompt = window.__jyotishDeferredInstallPrompt;
@@ -3797,6 +3851,12 @@ function bindProvenanceActions() {
if (btn.dataset.action === 'vedastro-run-range-scan') { if (btn.dataset.action === 'vedastro-run-range-scan') {
runVedAstroRangeScanFromPanel(panel); runVedAstroRangeScanFromPanel(panel);
} }
if (btn.dataset.action === 'professional-reading-gateway') {
refreshProfessionalReadingGatewayStatus();
}
if (btn.dataset.action === 'professional-reading-run') {
runProfessionalReadingFromPanel();
}
if (btn.dataset.action === 'pwa-install') { if (btn.dataset.action === 'pwa-install') {
promptPWAInstall(); promptPWAInstall();
} }
+91
View File
@@ -0,0 +1,91 @@
import { escapeHtml } from './security.js';
const DEFAULT_QUESTION = '请按高严谨流程做盲推解盘,先列 Technique Audit Table,再给结论。';
const DEFAULT_THEMES = ['career', 'relationship', 'finance', 'health', 'timing'];
export function renderProfessionalReadingPanel(state = {}) {
const status = state.gatewayStatus || {};
const result = state.result?.professional_reading || null;
return `
<section class="provenance-card professional-reading-panel">
<div class="provenance-head">
<span>Web Professional Reading v1</span>
<strong>${escapeHtml(status.active_backend || 'gateway-ready')}</strong>
</div>
<div class="trust-status-grid">
${renderMetric('Technique Audit Table', 'required', '必须显示 Functional Benefic/Malefic、MEVG、Real Case Calibration。')}
${renderMetric('MEVG / Global Web Evidence', 'queued-or-blocked', '外部资料采集必须进入队列或说明 blocked。')}
${renderMetric('Real Case Calibration', 'required', '找不到相似案例时必须降级,不得静默跳过。')}
${renderMetric('VedAstro Gateway Boundary', status.mode || 'local_first', '普通中国大陆用户不需要浏览器直连 VedAstro。')}
</div>
<div class="professional-reading-actions">
<button type="button" class="provenance-action" data-action="professional-reading-run">运行专业解盘</button>
<button type="button" class="provenance-action" data-action="professional-reading-gateway">刷新网关状态</button>
</div>
<div id="professional-reading-status" class="workspace-import-status" aria-live="polite">
${escapeHtml(state.message || '等待运行。')}
</div>
${result ? renderProfessionalReadingResult(result) : ''}
</section>
`;
}
export function buildProfessionalReadingPayload(chartData = {}, overrides = {}) {
const birth = window.__jyotishBirth || chartData.birth_info || chartData.birth || {};
return {
year: Number(birth.year || birth.date?.slice?.(0, 4) || overrides.year || REDACTED_YEAR),
month: Number(birth.month || birth.date?.slice?.(5, 7) || overrides.month || 1),
day: Number(birth.day || birth.date?.slice?.(8, 10) || overrides.day || 1),
hour: Number(birth.hour ?? overrides.hour ?? 12),
minute: Number(birth.minute ?? overrides.minute ?? 0),
second: Number(birth.second ?? overrides.second ?? 0),
lat: Number(birth.lat ?? overrides.lat ?? 0),
lon: Number(birth.lon ?? overrides.lon ?? 0),
tz: Number(birth.tz ?? overrides.tz ?? 8),
question: overrides.question || DEFAULT_QUESTION,
themes: overrides.themes || DEFAULT_THEMES,
reference_date: overrides.reference_date || new Date().toISOString().slice(0, 10),
blind_mode: overrides.blind_mode ?? true,
disable_life_event_feedback: overrides.disable_life_event_feedback ?? true,
};
}
export function bindProfessionalReadingPanel(container, chartData = null, setState = () => {}) {
container?.querySelector('[data-action="professional-reading-gateway"]')?.addEventListener('click', async () => {
setState({ message: '正在刷新 VedAstro Gateway...' });
const gatewayStatus = await window.JyotishAPI.getVedAstroGatewayStatus();
setState({ gatewayStatus, message: 'VedAstro Gateway 状态已刷新。' });
});
container?.querySelector('[data-action="professional-reading-run"]')?.addEventListener('click', async () => {
setState({ message: '正在运行专业解盘代理...' });
const payload = buildProfessionalReadingPayload(chartData);
const result = await window.JyotishAPI.runProfessionalReading(payload);
setState({ result, gatewayStatus: result?.professional_reading?.vedastro_gateway?.gateway_status || {}, message: '专业解盘包已生成。' });
});
}
function renderMetric(label, value, note) {
return `
<div class="trust-status-card">
<span>${escapeHtml(label)}</span>
<strong>${escapeHtml(value)}</strong>
<p>${escapeHtml(note)}</p>
</div>
`;
}
function renderProfessionalReadingResult(result) {
const gateway = result.vedastro_gateway || {};
const controls = result.user_led_calibration_controls || {};
const requiredRows = result.technique_audit_table_required_rows || [];
return `
<div class="professional-reading-result">
<h3>Professional Reading Packet</h3>
<p>status: ${escapeHtml(gateway.status || 'unknown')}</p>
<p>blind_mode: ${escapeHtml(String(Boolean(controls.blind_mode)))}</p>
<ul>
${requiredRows.map(row => `<li>${escapeHtml(row)}</li>`).join('')}
</ul>
</div>
`;
}
+15
View File
@@ -240,6 +240,18 @@ async function runVedAstroRangeScan(payload) {
return postJson('/api/vedastro/range_scan', payload); return postJson('/api/vedastro/range_scan', payload);
} }
async function getVedAstroGatewayStatus() {
return fetchJson('/api/vedastro_gateway/status');
}
async function runVedAstroGateway(payload) {
return postJson('/api/vedastro_gateway/run', payload);
}
async function runProfessionalReading(payload) {
return postJson('/api/professional_reading', payload);
}
async function getTechniqueCatalog() { async function getTechniqueCatalog() {
let lastError = null; let lastError = null;
for (const base of getApiBases(true)) { for (const base of getApiBases(true)) {
@@ -465,6 +477,9 @@ window.JyotishAPI = {
getCapabilityAudit, getCapabilityAudit,
getVedAstroStatus, getVedAstroStatus,
runVedAstroRangeScan, runVedAstroRangeScan,
getVedAstroGatewayStatus,
runVedAstroGateway,
runProfessionalReading,
getTechniqueCatalog, getTechniqueCatalog,
runTechniqueExample, runTechniqueExample,
computeAnnual, computeAnnual,
+50
View File
@@ -1170,6 +1170,56 @@ def test_api_bridge_failures_have_recovery_guidance() -> None:
assert "const data = await resp.json();" not in public_bridge assert "const data = await resp.json();" not in public_bridge
def test_professional_reading_web_surface_uses_gateway_and_backend_agent() -> None:
bridge = read("api-bridge.js")
public_bridge = read("public/api-bridge.js")
main = read("main.js")
professional = read("professional-reading.js")
assert bridge == public_bridge
assert "getVedAstroGatewayStatus" in bridge
assert "postJson('/api/vedastro_gateway/run'" in bridge
assert "postJson('/api/professional_reading'" in bridge
assert "renderProfessionalReadingPanel" in professional
assert "Technique Audit Table" in professional
assert "MEVG / Global Web Evidence" in professional
assert "Real Case Calibration" in professional
assert "VedAstro Gateway Boundary" in professional
assert "renderProfessionalReadingPanel" in main
def test_ai_chat_consumes_professional_reading_packet() -> None:
ai_chat = read("ai-chat.js")
assert "【Professional Reading Packet】" in ai_chat
assert "professional_reading" in ai_chat
assert "user_led_calibration_controls" in ai_chat
assert "VedAstro Gateway Boundary" in ai_chat
def test_cn_gateway_docs_and_env_example_exist() -> None:
env = (ROOT / ".env.cn.example").read_text(encoding="utf-8")
readme = (ROOT / "README.md").read_text(encoding="utf-8")
for token in [
"VEDASTRO_GATEWAY_MODE=cn_gateway",
"VEDASTRO_SELF_HOST_ENDPOINT",
"VEDASTRO_CACHE_TTL_SECONDS",
"VEDASTRO_GATEWAY_QUEUE_ENABLED=1",
"VEDASTRO_FAIL_OPEN_LOCAL=1",
]:
assert token in env
for token in [
"中国大陆用户",
"VedAstro Gateway",
"不要让浏览器直连 VedAstro",
"/api/vedastro_gateway/status",
"/api/professional_reading",
"TTL/cache",
"free-tier queue",
]:
assert token in readme
def test_chart_compute_failures_have_visible_recovery_guidance() -> None: def test_chart_compute_failures_have_visible_recovery_guidance() -> None:
html = read("index.html") html = read("index.html")
main = read("main.js") main = read("main.js")