feat: implement scan terminal dashboard system and supporting services
This commit is contained in:
@@ -6,7 +6,6 @@ from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from web.services.scan_api import (
|
||||
get_scan_terminal_ai_payload,
|
||||
get_scan_terminal_overview_payload,
|
||||
get_scan_terminal_payload,
|
||||
)
|
||||
@@ -54,11 +53,6 @@ async def scan_terminal(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/scan/terminal/ai")
|
||||
async def scan_terminal_ai(request: Request):
|
||||
return await get_scan_terminal_ai_payload(request)
|
||||
|
||||
|
||||
@router.post("/api/scan/terminal/overview")
|
||||
async def scan_terminal_overview(request: Request):
|
||||
return await get_scan_terminal_overview_payload(request)
|
||||
|
||||
@@ -12,7 +12,7 @@ from loguru import logger
|
||||
|
||||
from web.analysis_service import _analyze
|
||||
from web.core import CITIES
|
||||
from web.services.scan_ai_config import (
|
||||
from web.services.scan_terminal_config import (
|
||||
SCAN_TERMINAL_BUILD_TIMEOUT_SEC,
|
||||
SCAN_TERMINAL_MAX_WORKERS,
|
||||
SCAN_TERMINAL_PAYLOAD_TTL_SEC,
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Iterator, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
||||
from loguru import logger
|
||||
@@ -59,49 +59,6 @@ router = APIRouter()
|
||||
_CACHE_DB = DBManager()
|
||||
|
||||
|
||||
def build_scan_terminal_ai_payload(
|
||||
raw_filters: Optional[Dict[str, Any]] = None,
|
||||
*,
|
||||
snapshot_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"available": False,
|
||||
"status": "disabled",
|
||||
"reason": "scan AI has been removed",
|
||||
"snapshot_id": snapshot_id,
|
||||
"rows": [],
|
||||
}
|
||||
|
||||
|
||||
def build_scan_city_ai_forecast_payload(
|
||||
city: str,
|
||||
*,
|
||||
force_refresh: bool = False,
|
||||
locale: str = "zh-CN",
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"available": False,
|
||||
"status": "disabled",
|
||||
"reason": "city AI has been removed",
|
||||
"city": city,
|
||||
"locale": locale,
|
||||
"force_refresh": force_refresh,
|
||||
}
|
||||
|
||||
|
||||
def stream_scan_city_ai_forecast_payload(
|
||||
city: str,
|
||||
*,
|
||||
force_refresh: bool = False,
|
||||
locale: str = "zh-CN",
|
||||
) -> Iterator[str]:
|
||||
payload = build_scan_city_ai_forecast_payload(
|
||||
city,
|
||||
force_refresh=force_refresh,
|
||||
locale=locale,
|
||||
)
|
||||
yield f"data: {payload}\n\n"
|
||||
|
||||
_DEB_RECENT_LOOKBACK = 7
|
||||
_DEB_RECENT_MIN_SAMPLES = 3
|
||||
_daily_record_repo = DailyRecordRepository()
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
"""Scan terminal and AI configuration constants.
|
||||
|
||||
Extracted from scan_terminal_service.py to keep the module leaner.
|
||||
Re-exported from the original module for backward compatibility.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import threading
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from src.utils.refresh_policy import SCAN_ROWS_REFRESH_SEC
|
||||
|
||||
|
||||
_SCAN_CITY_AI_CACHE_LOCK = threading.Lock()
|
||||
_SCAN_CITY_AI_CACHE: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
def _env_int(
|
||||
name: str,
|
||||
default: int,
|
||||
*,
|
||||
min_value: int,
|
||||
max_value: Optional[int] = None,
|
||||
) -> int:
|
||||
try:
|
||||
value = int(os.getenv(name, str(default)))
|
||||
except Exception:
|
||||
value = int(default)
|
||||
value = max(int(min_value), value)
|
||||
if max_value is not None:
|
||||
value = min(int(max_value), value)
|
||||
return value
|
||||
|
||||
|
||||
SCAN_TERMINAL_PAYLOAD_TTL_SEC = min(
|
||||
SCAN_ROWS_REFRESH_SEC,
|
||||
max(10, int(os.getenv("POLYWEATHER_SCAN_TERMINAL_PAYLOAD_TTL_SEC", str(SCAN_ROWS_REFRESH_SEC)))),
|
||||
)
|
||||
SCAN_TERMINAL_BUILD_TIMEOUT_SEC = max(
|
||||
8,
|
||||
int(os.getenv("POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC", "120")),
|
||||
)
|
||||
SCAN_TERMINAL_MAX_WORKERS = _env_int(
|
||||
"POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS",
|
||||
8,
|
||||
min_value=1,
|
||||
max_value=12,
|
||||
)
|
||||
DEFAULT_SCAN_AI_MODEL = "mimo-v2.5-pro"
|
||||
DEFAULT_SCAN_AI_BASE_URL = "https://token-plan-cn.xiaomimimo.com/v1"
|
||||
SCAN_AI_API_KEY_ENV_HINT = (
|
||||
"POLYWEATHER_SCAN_AI_API_KEY "
|
||||
"(or POLYWEATHER_MIMO_API_KEY / POLYWEATHER_DEEPSEEK_API_KEY)"
|
||||
)
|
||||
|
||||
|
||||
def _env_str(*names: str, default: str = "") -> str:
|
||||
for name in names:
|
||||
value = str(os.getenv(name) or "").strip()
|
||||
if value:
|
||||
return value
|
||||
return str(default).strip()
|
||||
|
||||
|
||||
def _scan_ai_api_key() -> str:
|
||||
return _env_str(
|
||||
"POLYWEATHER_SCAN_AI_API_KEY",
|
||||
"POLYWEATHER_MIMO_API_KEY",
|
||||
"POLYWEATHER_DEEPSEEK_API_KEY",
|
||||
)
|
||||
|
||||
|
||||
def _infer_scan_ai_provider(base_url: str, model: str) -> str:
|
||||
text = f"{base_url} {model}".lower()
|
||||
if "xiaomimimo" in text or "mimo" in text:
|
||||
return "mimo"
|
||||
if "deepseek" in text:
|
||||
return "deepseek"
|
||||
return "openai-compatible"
|
||||
|
||||
|
||||
def _scan_ai_provider_label(provider: str) -> str:
|
||||
normalized = provider.strip().lower()
|
||||
if normalized == "mimo":
|
||||
return "MiMo"
|
||||
if normalized == "deepseek":
|
||||
return "DeepSeek"
|
||||
return "AI provider"
|
||||
|
||||
|
||||
SCAN_AI_MODEL = _env_str("POLYWEATHER_SCAN_AI_MODEL", default=DEFAULT_SCAN_AI_MODEL)
|
||||
SCAN_CITY_AI_MODEL = _env_str(
|
||||
"POLYWEATHER_SCAN_CITY_AI_MODEL",
|
||||
"POLYWEATHER_SCAN_AI_MODEL",
|
||||
default=SCAN_AI_MODEL or DEFAULT_SCAN_AI_MODEL,
|
||||
)
|
||||
SCAN_AI_BASE_URL = _env_str(
|
||||
"POLYWEATHER_SCAN_AI_BASE_URL",
|
||||
"POLYWEATHER_MIMO_BASE_URL",
|
||||
"POLYWEATHER_DEEPSEEK_BASE_URL",
|
||||
default=DEFAULT_SCAN_AI_BASE_URL,
|
||||
).rstrip("/")
|
||||
SCAN_AI_PROVIDER = _env_str(
|
||||
"POLYWEATHER_SCAN_AI_PROVIDER",
|
||||
default=_infer_scan_ai_provider(SCAN_AI_BASE_URL, SCAN_CITY_AI_MODEL),
|
||||
)
|
||||
SCAN_AI_PROVIDER_LABEL = _env_str(
|
||||
"POLYWEATHER_SCAN_AI_PROVIDER_LABEL",
|
||||
default=_scan_ai_provider_label(SCAN_AI_PROVIDER),
|
||||
)
|
||||
SCAN_AI_ENABLED = str(
|
||||
os.getenv("POLYWEATHER_SCAN_AI_ENABLED") or "false"
|
||||
).strip().lower() in {"1", "true", "yes", "on"}
|
||||
SCAN_AI_TIMEOUT_SEC = _env_int(
|
||||
"POLYWEATHER_SCAN_AI_TIMEOUT_SEC",
|
||||
40,
|
||||
min_value=10,
|
||||
max_value=120,
|
||||
)
|
||||
SCAN_CITY_AI_TIMEOUT_SEC = _env_int(
|
||||
"POLYWEATHER_SCAN_CITY_AI_TIMEOUT_SEC",
|
||||
30,
|
||||
min_value=10,
|
||||
max_value=120,
|
||||
)
|
||||
SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR = str(
|
||||
os.getenv("POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR") or "false"
|
||||
).strip().lower() in {"1", "true", "yes", "on"}
|
||||
SCAN_AI_CACHE_TTL_SEC = max(
|
||||
30,
|
||||
int(os.getenv("POLYWEATHER_SCAN_AI_CACHE_TTL_SEC", "3600")),
|
||||
)
|
||||
SCAN_AI_MAX_ROWS = _env_int("POLYWEATHER_SCAN_AI_MAX_ROWS", 40, min_value=1)
|
||||
SCAN_AI_MAX_TOKENS = _env_int(
|
||||
"POLYWEATHER_SCAN_AI_MAX_TOKENS",
|
||||
3200,
|
||||
min_value=600,
|
||||
max_value=64000,
|
||||
)
|
||||
SCAN_CITY_AI_MAX_TOKENS = _env_int(
|
||||
"POLYWEATHER_SCAN_CITY_AI_MAX_TOKENS",
|
||||
800,
|
||||
min_value=400,
|
||||
max_value=64000,
|
||||
)
|
||||
SCAN_CITY_AI_STREAM_MAX_TOKENS = _env_int(
|
||||
"POLYWEATHER_SCAN_CITY_AI_STREAM_MAX_TOKENS",
|
||||
min(SCAN_CITY_AI_MAX_TOKENS, 800),
|
||||
min_value=400,
|
||||
max_value=64000,
|
||||
)
|
||||
@@ -4,33 +4,12 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi import Request
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
import web.routes as legacy_routes
|
||||
|
||||
|
||||
def _boolish(value: Any) -> bool:
|
||||
return str(value or "false").lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
async def _json_body_or_empty(request: Request) -> Dict[str, Any]:
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
if not isinstance(body, dict):
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON body")
|
||||
return body
|
||||
|
||||
|
||||
def _extract_required_city(body: Dict[str, Any]) -> str:
|
||||
city = str(body.get("city") or "").strip()
|
||||
if not city:
|
||||
raise HTTPException(status_code=400, detail="city is required")
|
||||
return city
|
||||
|
||||
|
||||
async def get_scan_terminal_payload(
|
||||
request: Request,
|
||||
*,
|
||||
@@ -70,17 +49,5 @@ async def get_scan_terminal_payload(
|
||||
)
|
||||
|
||||
|
||||
async def get_scan_terminal_ai_payload(request: Request) -> Dict[str, Any]:
|
||||
legacy_routes._assert_entitlement(request)
|
||||
body = await _json_body_or_empty(request)
|
||||
filters = body.get("filters") if isinstance(body.get("filters"), dict) else {}
|
||||
snapshot_id = str(body.get("snapshot_id") or "").strip() or None
|
||||
return await run_in_threadpool(
|
||||
legacy_routes.build_scan_terminal_ai_payload,
|
||||
filters,
|
||||
snapshot_id=snapshot_id,
|
||||
)
|
||||
|
||||
|
||||
async def get_scan_terminal_overview_payload(request: Request) -> Dict[str, Any]:
|
||||
return {"overview": [], "available": False}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Scan terminal configuration constants."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from src.utils.refresh_policy import SCAN_ROWS_REFRESH_SEC
|
||||
|
||||
|
||||
def _env_int(
|
||||
name: str,
|
||||
default: int,
|
||||
*,
|
||||
min_value: int,
|
||||
max_value: Optional[int] = None,
|
||||
) -> int:
|
||||
try:
|
||||
value = int(os.getenv(name, str(default)))
|
||||
except Exception:
|
||||
value = int(default)
|
||||
value = max(int(min_value), value)
|
||||
if max_value is not None:
|
||||
value = min(int(max_value), value)
|
||||
return value
|
||||
|
||||
|
||||
SCAN_TERMINAL_PAYLOAD_TTL_SEC = min(
|
||||
SCAN_ROWS_REFRESH_SEC,
|
||||
max(10, int(os.getenv("POLYWEATHER_SCAN_TERMINAL_PAYLOAD_TTL_SEC", str(SCAN_ROWS_REFRESH_SEC)))),
|
||||
)
|
||||
SCAN_TERMINAL_BUILD_TIMEOUT_SEC = max(
|
||||
8,
|
||||
int(os.getenv("POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC", "120")),
|
||||
)
|
||||
SCAN_TERMINAL_MAX_WORKERS = _env_int(
|
||||
"POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS",
|
||||
8,
|
||||
min_value=1,
|
||||
max_value=12,
|
||||
)
|
||||
Reference in New Issue
Block a user