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)))}
+
+ ${requiredRows.map(row => `- ${escapeHtml(row)}
`).join('')}
+
+
+ `;
+}
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")