From b33c2f9cd203339fc718adc21c6ffc04076fd66c Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Thu, 2 Jul 2026 23:54:05 +0800 Subject: [PATCH] Add web professional reading gateway surface --- .env.cn.example | 21 +++++++ README.md | 27 ++++++++ jyotish-app/ai-chat.js | 28 ++++++++- jyotish-app/api-bridge.js | 15 +++++ jyotish-app/main.js | 60 ++++++++++++++++++ jyotish-app/professional-reading.js | 91 +++++++++++++++++++++++++++ jyotish-app/public/api-bridge.js | 15 +++++ tests/test_frontend_productization.py | 50 +++++++++++++++ 8 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 .env.cn.example create mode 100644 jyotish-app/professional-reading.js diff --git a/.env.cn.example b/.env.cn.example new file mode 100644 index 00000000..68632a8c --- /dev/null +++ b/.env.cn.example @@ -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 diff --git a/README.md b/README.md index c5d64480..72585280 100644 --- a/README.md +++ b/README.md @@ -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 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 窗口从云端 Git 仓库拉取本项目,推荐先走这一条稳定入口,而不是手动拼多个底层脚本: diff --git a/jyotish-app/ai-chat.js b/jyotish-app/ai-chat.js index 895d5195..a15533e4 100644 --- a/jyotish-app/ai-chat.js +++ b/jyotish-app/ai-chat.js @@ -304,6 +304,7 @@ async function sendMessage() { 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 || {}; @@ -368,6 +369,8 @@ function buildChartContext(cd, guidedTopicContext = null) { ].join('\n') : ''; return [ + professionalReadingContext, + '', '【AI Prompt Pack】', cd.ai_prompt_pack.prompt_zh, '', @@ -401,7 +404,30 @@ function buildChartContext(cd, guidedTopicContext = null) { 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 `${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() { diff --git a/jyotish-app/api-bridge.js b/jyotish-app/api-bridge.js index fc2baaa7..66286f2c 100644 --- a/jyotish-app/api-bridge.js +++ b/jyotish-app/api-bridge.js @@ -240,6 +240,18 @@ async function runVedAstroRangeScan(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() { let lastError = null; for (const base of getApiBases(true)) { @@ -465,6 +477,9 @@ window.JyotishAPI = { getCapabilityAudit, getVedAstroStatus, runVedAstroRangeScan, + getVedAstroGatewayStatus, + runVedAstroGateway, + runProfessionalReading, getTechniqueCatalog, runTechniqueExample, computeAnnual, diff --git a/jyotish-app/main.js b/jyotish-app/main.js index fb6c774e..5a50a983 100644 --- a/jyotish-app/main.js +++ b/jyotish-app/main.js @@ -66,6 +66,10 @@ import { escapeHtml, escapeAttr, safeNumber } from './security.js'; import { renderSkillCoverage } from './skill-map.js'; import { initChartImport } from './import-chart.js'; import { buildMEVGAudit, renderMEVGAudit } from './mevg-audit.js'; +import { + buildProfessionalReadingPayload, + renderProfessionalReadingPanel, +} from './professional-reading.js'; // 合并所有 Yoga 定义 const ALL_YOGA_DEFS = [...YOGA_DEFINITIONS, ...YOGA_EXTENDED_A, ...YOGA_EXTENDED_B]; @@ -93,6 +97,7 @@ const PANCHANGA_CONDITIONS = [ ['avoid_new_start', '不宜新开始'], ['good_choghadiya', '有吉利 Choghadiya'], ]; +let professionalReadingState = {}; const PANCHANGA_CONDITION_GUIDE = { all: '显示范围内所有日期。', has_vrata: '当天存在 vrata、lunar observance 或节日候选标签。', @@ -2361,6 +2366,9 @@ function renderTrustCenterPanel() { ${renderRuntimeHealthPanel(runtime)} ${renderVedAstroUserScanPanel()} +
+ ${renderProfessionalReadingPanel(professionalReadingState)} +
${renderValidationTransparencyPanel()} ${renderDashaShadbalaCalibrationPanel()} ${renderOracleEvidenceIntakePanel()} @@ -3244,6 +3252,52 @@ async function runVedAstroRangeScanFromPanel(panel) { 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() { const status = $('trust-center-status'); const prompt = window.__jyotishDeferredInstallPrompt; @@ -3797,6 +3851,12 @@ function bindProvenanceActions() { if (btn.dataset.action === 'vedastro-run-range-scan') { runVedAstroRangeScanFromPanel(panel); } + if (btn.dataset.action === 'professional-reading-gateway') { + refreshProfessionalReadingGatewayStatus(); + } + if (btn.dataset.action === 'professional-reading-run') { + runProfessionalReadingFromPanel(); + } if (btn.dataset.action === 'pwa-install') { promptPWAInstall(); } diff --git a/jyotish-app/professional-reading.js b/jyotish-app/professional-reading.js new file mode 100644 index 00000000..0c434629 --- /dev/null +++ b/jyotish-app/professional-reading.js @@ -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 ` +
+
+ Web Professional Reading v1 + ${escapeHtml(status.active_backend || 'gateway-ready')} +
+
+ ${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。')} +
+
+ + +
+
+ ${escapeHtml(state.message || '等待运行。')} +
+ ${result ? renderProfessionalReadingResult(result) : ''} +
+ `; +} + +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 ` +
+ ${escapeHtml(label)} + ${escapeHtml(value)} +

${escapeHtml(note)}

+
+ `; +} + +function renderProfessionalReadingResult(result) { + const gateway = result.vedastro_gateway || {}; + const controls = result.user_led_calibration_controls || {}; + const requiredRows = result.technique_audit_table_required_rows || []; + return ` +
+

Professional Reading Packet

+

status: ${escapeHtml(gateway.status || 'unknown')}

+

blind_mode: ${escapeHtml(String(Boolean(controls.blind_mode)))}

+ +
+ `; +} diff --git a/jyotish-app/public/api-bridge.js b/jyotish-app/public/api-bridge.js index fc2baaa7..66286f2c 100644 --- a/jyotish-app/public/api-bridge.js +++ b/jyotish-app/public/api-bridge.js @@ -240,6 +240,18 @@ async function runVedAstroRangeScan(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() { let lastError = null; for (const base of getApiBases(true)) { @@ -465,6 +477,9 @@ window.JyotishAPI = { getCapabilityAudit, getVedAstroStatus, runVedAstroRangeScan, + getVedAstroGatewayStatus, + runVedAstroGateway, + runProfessionalReading, getTechniqueCatalog, runTechniqueExample, computeAnnual, diff --git a/tests/test_frontend_productization.py b/tests/test_frontend_productization.py index 9ab8139a..08b026ca 100644 --- a/tests/test_frontend_productization.py +++ b/tests/test_frontend_productization.py @@ -1170,6 +1170,56 @@ def test_api_bridge_failures_have_recovery_guidance() -> None: 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: html = read("index.html") main = read("main.js")