From c7eeb3f8e5ddfc86b0e4e09a56d5d71a237bcf6b Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Thu, 16 Jul 2026 16:31:34 +0800 Subject: [PATCH] Add active rectification API workflow --- jyotish-app/api-bridge.js | 10 +++ jyotish-app/public/api-bridge.js | 28 +++++++++ scripts/jyotish_api_server.py | 52 ++++++++++++++++ tests/test_active_rectification_api.py | 85 ++++++++++++++++++++++++++ tests/test_frontend_productization.py | 2 + 5 files changed, 177 insertions(+) create mode 100644 tests/test_active_rectification_api.py diff --git a/jyotish-app/api-bridge.js b/jyotish-app/api-bridge.js index e48e5e39..28804130 100644 --- a/jyotish-app/api-bridge.js +++ b/jyotish-app/api-bridge.js @@ -344,6 +344,14 @@ async function computeRectificationGate(payload) { return postJson('/api/rectification_gate', payload); } +async function computeActiveRectificationQuestions(payload) { + return postJson('/api/active_rectification_questions', payload); +} + +async function computeActiveRectificationScore(payload) { + return postJson('/api/active_rectification_score', payload); +} + async function computeCaseValidation(payload) { return postJson('/api/case_validation', payload); } @@ -512,6 +520,8 @@ window.JyotishAPI = { computeYogas, computeAspects, computeRectificationGate, + computeActiveRectificationQuestions, + computeActiveRectificationScore, computeCaseValidation, getRealCaseRevalidation, computeDivisionalYoga, diff --git a/jyotish-app/public/api-bridge.js b/jyotish-app/public/api-bridge.js index 66286f2c..28804130 100644 --- a/jyotish-app/public/api-bridge.js +++ b/jyotish-app/public/api-bridge.js @@ -47,6 +47,7 @@ async function postJson(path, payload, { requireModernChart = false } = {}) { continue; } activeApiBase = base; + if (data?.mode === 'async_submitted') return pollAsyncJob(data, { base }); return data; } catch (error) { lastAttempt = `${base}${path}`; @@ -60,6 +61,22 @@ async function postJson(path, payload, { requireModernChart = false } = {}) { throw lastError || new Error(buildAPIRecoveryMessage(path, '本地 API 未连接', lastAttempt)); } +async function pollAsyncJob(job, { base = activeApiBase, timeoutMs = 120000, intervalMs = 500 } = {}) { + if (!job?.poll_path || !job?.access_token) throw new Error('Async job response missing poll capability'); + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const resp = await fetch(`${base}${job.poll_path}`, { + headers: { Authorization: `Bearer ${job.access_token}` }, + }); + const data = await parseApiResponse(resp); + if (!resp.ok) throw new Error(buildAPIRecoveryMessage(job.poll_path, data?.error || `Job poll failed (${resp.status})`)); + if (data.status === 'completed') return data.result || data; + if (data.status === 'failed') throw new Error(data.error || 'Async job failed'); + await new Promise(resolve => setTimeout(resolve, intervalMs)); + } + throw new Error(buildAPIRecoveryMessage(job.poll_path, 'Async job timed out')); +} + async function fetchJson(path) { let lastError = null; let lastAttempt = null; @@ -327,6 +344,14 @@ async function computeRectificationGate(payload) { return postJson('/api/rectification_gate', payload); } +async function computeActiveRectificationQuestions(payload) { + return postJson('/api/active_rectification_questions', payload); +} + +async function computeActiveRectificationScore(payload) { + return postJson('/api/active_rectification_score', payload); +} + async function computeCaseValidation(payload) { return postJson('/api/case_validation', payload); } @@ -495,12 +520,15 @@ window.JyotishAPI = { computeYogas, computeAspects, computeRectificationGate, + computeActiveRectificationQuestions, + computeActiveRectificationScore, computeCaseValidation, getRealCaseRevalidation, computeDivisionalYoga, computeKakshya, computeBhavaBala, computeTransitTriggers, + pollAsyncJob, // AI 解读 aiReading, aiFullReading, diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index bf964646..5812b58b 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -1035,6 +1035,8 @@ API_COMMAND_MAP = { 'yoga': '/api/yogas', 'aspects': '/api/aspects', 'rectification': '/api/rectification_gate', + 'active-rectification-questions': '/api/active_rectification_questions', + 'active-rectification-score': '/api/active_rectification_score', 'case-validation': '/api/case_validation', 'divisional-yoga': '/api/divisional_yoga', 'deep-varga-avastha': '/api/deep_varga_avastha', @@ -1066,6 +1068,8 @@ TECHNIQUE_EXAMPLE_ENDPOINTS = { '/api/pancha_mahapurusha', '/api/prashna', '/api/rectification_gate', + '/api/active_rectification_questions', + '/api/active_rectification_score', '/api/relationship', '/api/remedies', '/api/sade_sati', @@ -1478,6 +1482,12 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): elif path == '/api/rectification_gate': result = self._compute_rectification_gate(body) self._json(result) + elif path == '/api/active_rectification_questions': + result = self._compute_active_rectification_questions(body) + self._json(result) + elif path == '/api/active_rectification_score': + result = self._compute_active_rectification_score(body) + self._json(result) elif path == '/api/case_validation': result = self._compute_case_validation(body) self._json(result) @@ -6327,6 +6337,46 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): }, } + def _compute_active_rectification_questions(self, body): + birth_time = body.get('birth_time') + if not isinstance(birth_time, str) or not birth_time.strip(): + raise BadRequest('birth_time must be a string') + uncertainty_minutes = self._get_int(body, 'uncertainty_minutes', 30) + if not 1 <= uncertainty_minutes <= 180: + raise BadRequest('uncertainty_minutes must be between 1 and 180') + step_minutes = self._get_int(body, 'step_minutes', 1) + if not 1 <= step_minutes <= 30: + raise BadRequest('step_minutes must be between 1 and 30') + try: + module = _load_local_module('active_rectification_questions') + result = module.build_questionnaire( + birth_time.strip(), + uncertainty_minutes=uncertainty_minutes, + step_minutes=step_minutes, + ) + except ValueError as e: + raise BadRequest('birth_time must be YYYY-MM-DD HH:MM') from e + return { + 'success': True, + 'endpoint': 'active_rectification_questions', + **result, + } + + def _compute_active_rectification_score(self, body): + questionnaire = body.get('questionnaire') + if not isinstance(questionnaire, dict): + raise BadRequest('questionnaire must be an object') + answers = body.get('answers') + if not isinstance(answers, dict): + raise BadRequest('answers must be an object') + module = _load_local_module('active_rectification_questions') + result = module.score_answers(questionnaire, answers) + return { + 'success': True, + 'endpoint': 'active_rectification_score', + **result, + } + def _compute_case_validation(self, body): planets, _, _ = self._normalized_planets_from_body(body) current_md = body.get('current_md', body.get('dasha_lord', '')) @@ -7087,6 +7137,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): '/api/pancha_mahapurusha': self._compute_pmc, '/api/prashna': self._compute_prashna, '/api/rectification_gate': self._compute_rectification_gate, + '/api/active_rectification_questions': self._compute_active_rectification_questions, + '/api/active_rectification_score': self._compute_active_rectification_score, '/api/relationship': self._compute_relationship, '/api/remedies': self._compute_remedies, '/api/sade_sati': self._compute_sade_sati, diff --git a/tests/test_active_rectification_api.py b/tests/test_active_rectification_api.py new file mode 100644 index 00000000..6790e0e3 --- /dev/null +++ b/tests/test_active_rectification_api.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +from jyotish_api_server import BadRequest, JyotishAPIHandler # noqa: E402 + + +def _handler() -> JyotishAPIHandler: + return JyotishAPIHandler.__new__(JyotishAPIHandler) + + +def test_active_rectification_questions_api_builds_choice_workflow() -> None: + result = _handler()._compute_active_rectification_questions( + { + "birth_time": "1993-04-17 14:49", + "uncertainty_minutes": 30, + "step_minutes": 1, + } + ) + + assert result["success"] is True + assert result["endpoint"] == "active_rectification_questions" + assert result["scope"] == "active_birth_time_rectification_questionnaire" + assert result["candidate_scan"]["start"] == "1993-04-17 14:19" + assert result["candidate_scan"]["end"] == "1993-04-17 15:19" + assert result["candidate_scan"]["candidate_count"] == 61 + assert result["questions"] + assert {option["key"] for option in result["questions"][0]["options"]} == {"A", "B", "C", "D"} + assert "dynamic_candidate_cluster_scoring" in result["workflow"] + + +def test_active_rectification_score_api_returns_rankings_and_next_questions() -> None: + questionnaire = _handler()._compute_active_rectification_questions( + {"birth_time": "1993-04-17 14:49", "uncertainty_minutes": 30} + ) + scored = _handler()._compute_active_rectification_score( + { + "questionnaire": questionnaire, + "answers": { + "education_environment_shift": "A", + "residence_relocation_shift": "B", + "relationship_or_partner_entry": "D", + "career_responsibility_pressure": "A", + "research_tool_expression_shift": "C", + }, + } + ) + + assert scored["success"] is True + assert scored["endpoint"] == "active_rectification_score" + assert scored["scope"] == "active_birth_time_rectification_scoring" + assert scored["answered_count"] == 5 + assert scored["candidate_cluster_rankings"] + assert scored["next_round_questions"] + assert scored["candidate_cluster_rankings"][0]["score"] >= scored["candidate_cluster_rankings"][-1]["score"] + + +def test_active_rectification_questions_api_validates_request() -> None: + with pytest.raises(BadRequest, match="birth_time must be a string"): + _handler()._compute_active_rectification_questions({}) + + with pytest.raises(BadRequest, match="uncertainty_minutes must be between 1 and 180"): + _handler()._compute_active_rectification_questions( + {"birth_time": "1993-04-17 14:49", "uncertainty_minutes": 0} + ) + + with pytest.raises(BadRequest, match="step_minutes must be between 1 and 30"): + _handler()._compute_active_rectification_questions( + {"birth_time": "1993-04-17 14:49", "step_minutes": 31} + ) + + +def test_active_rectification_score_api_validates_payload() -> None: + with pytest.raises(BadRequest, match="questionnaire must be an object"): + _handler()._compute_active_rectification_score({"answers": {}}) + + with pytest.raises(BadRequest, match="answers must be an object"): + _handler()._compute_active_rectification_score({"questionnaire": {}}) diff --git a/tests/test_frontend_productization.py b/tests/test_frontend_productization.py index e76b996f..c8cf3fc1 100644 --- a/tests/test_frontend_productization.py +++ b/tests/test_frontend_productization.py @@ -1397,6 +1397,8 @@ def test_api_bridge_exports_productized_backend_actions() -> None: "computeYogas", "computeAspects", "computeRectificationGate", + "computeActiveRectificationQuestions", + "computeActiveRectificationScore", "computeCaseValidation", "computeDivisionalYoga", "computeKakshya",