Add web professional reading gateway surface
This commit is contained in:
@@ -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
|
||||
@@ -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 仓库拉取本项目,推荐先走这一条稳定入口,而不是手动拼多个底层脚本:
|
||||
|
||||
+27
-1
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
${renderRuntimeHealthPanel(runtime)}
|
||||
${renderVedAstroUserScanPanel()}
|
||||
<div id="professional-reading-host">
|
||||
${renderProfessionalReadingPanel(professionalReadingState)}
|
||||
</div>
|
||||
${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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user