拆分 scan_terminal_service:AI 配置常量移至 scan_ai_config.py
提取 ~140 行配置+env helpers 到新模块。scan_terminal_service 1534→1474 行。
This commit is contained in:
-49
@@ -1,49 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
const projectRoot = process.cwd();
|
||||
const workspacePath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"scan-terminal",
|
||||
"use-ai-pinned-city-workspace.ts",
|
||||
);
|
||||
const storePath = path.join(projectRoot, "hooks", "useDashboardStore.tsx");
|
||||
const cardPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"scan-terminal",
|
||||
"AiPinnedCityCard.tsx",
|
||||
);
|
||||
const source = fs.readFileSync(workspacePath, "utf8");
|
||||
const storeSource = fs.readFileSync(storePath, "utf8");
|
||||
const cardSource = fs.readFileSync(cardPath, "utf8");
|
||||
|
||||
assert(
|
||||
!source.includes("waitForDeepAnalysisQueue"),
|
||||
"decision-card deep analysis hydration must not add an artificial queue delay",
|
||||
);
|
||||
assert(
|
||||
/store\.ensureCityDetail\(\s*nextCity,\s*false,\s*"full",?\s*\)/.test(source),
|
||||
"automatic deep analysis hydration should use cache-friendly full detail requests",
|
||||
);
|
||||
assert(
|
||||
storeSource.includes("row.model_cluster_sources") &&
|
||||
storeSource.includes("deb_prediction") &&
|
||||
storeSource.includes("multi_model: multiModel"),
|
||||
"decision-card preload must hydrate model cluster and DEB data from the scan row",
|
||||
);
|
||||
assert(
|
||||
cardSource.includes("getRowModelEntries") &&
|
||||
cardSource.includes("detailModelEntries.length ? detailModelEntries : getRowModelEntries(row)") &&
|
||||
cardSource.includes('aiForecast.status === "ready"'),
|
||||
"decision card should render model support from the row; AI predicted max must wait for AI ready status",
|
||||
);
|
||||
}
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
const source = fs.readFileSync(
|
||||
path.join(process.cwd(), "hooks", "useDashboardStore.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert(
|
||||
source.includes("function pickRicherHourly"),
|
||||
"city detail merge should have an explicit hourly-series preservation helper",
|
||||
);
|
||||
assert(
|
||||
/hourly:\s*pickRicherHourly\(\s*current\.hourly,\s*incoming\.hourly\s*\)/.test(
|
||||
source,
|
||||
),
|
||||
"deep-analysis refresh must not let sparse incoming hourly data overwrite chart-capable cached hourly data",
|
||||
);
|
||||
assert(
|
||||
source.includes("function pickRicherObservationSeries"),
|
||||
"city detail merge should preserve chart observation series when refresh payload is sparse",
|
||||
);
|
||||
assert(
|
||||
/metar_today_obs:\s*pickRicherObservationSeries\(\s*current\.metar_today_obs,\s*incoming\.metar_today_obs,?\s*\)/.test(
|
||||
source,
|
||||
),
|
||||
"deep-analysis refresh must not wipe METAR observation points used by the intraday path chart",
|
||||
);
|
||||
assert(
|
||||
/settlement_today_obs:\s*pickRicherObservationSeries\(\s*current\.settlement_today_obs,\s*incoming\.settlement_today_obs,?\s*\)/.test(
|
||||
source,
|
||||
),
|
||||
"deep-analysis refresh must not wipe settlement observation points used by the intraday path chart",
|
||||
);
|
||||
assert(
|
||||
/trend:\s*mergeTrendInfo\(\s*current\.trend,\s*incoming\.trend\s*\)/.test(source),
|
||||
"deep-analysis refresh must merge trend.recent instead of replacing it with sparse trend payloads",
|
||||
);
|
||||
}
|
||||
-6
@@ -20,7 +20,6 @@ export function runTests() {
|
||||
"dashboard",
|
||||
"ScanTerminalDashboard.tsx",
|
||||
);
|
||||
const dashboardClientPath = path.join(projectRoot, "lib", "dashboard-client.ts");
|
||||
const airportEvidencePath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
@@ -31,7 +30,6 @@ export function runTests() {
|
||||
|
||||
const querySource = fs.readFileSync(queryPath, "utf8");
|
||||
const dashboardSource = fs.readFileSync(dashboardPath, "utf8");
|
||||
const dashboardClientSource = fs.readFileSync(dashboardClientPath, "utf8");
|
||||
const airportEvidenceSource = fs.readFileSync(airportEvidencePath, "utf8");
|
||||
|
||||
assert(
|
||||
@@ -52,8 +50,4 @@ export function runTests() {
|
||||
!airportEvidenceSource.includes("busan:"),
|
||||
"settlement runway mapping must cover all active settlement cities without mixing in non-settlement airports",
|
||||
);
|
||||
assert(
|
||||
dashboardClientSource.includes('CACHE_KEY = "polyWeather_v2_chart_full_day"'),
|
||||
"city detail cache key must be bumped so old partial chart detail caches are not reused",
|
||||
);
|
||||
}
|
||||
|
||||
+23
-142
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
@@ -16,6 +15,29 @@ from loguru import logger
|
||||
|
||||
from web.analysis_service import _analyze
|
||||
from web.core import CITIES
|
||||
from web.services.scan_ai_config import (
|
||||
_SCAN_CITY_AI_CACHE,
|
||||
_SCAN_CITY_AI_CACHE_LOCK,
|
||||
_scan_ai_api_key,
|
||||
SCAN_AI_API_KEY_ENV_HINT,
|
||||
SCAN_AI_BASE_URL,
|
||||
SCAN_AI_CACHE_TTL_SEC,
|
||||
SCAN_AI_ENABLED,
|
||||
SCAN_AI_MAX_ROWS,
|
||||
SCAN_AI_MAX_TOKENS,
|
||||
SCAN_AI_MODEL,
|
||||
SCAN_AI_PROVIDER,
|
||||
SCAN_AI_PROVIDER_LABEL,
|
||||
SCAN_AI_TIMEOUT_SEC,
|
||||
SCAN_CITY_AI_MAX_TOKENS,
|
||||
SCAN_CITY_AI_MODEL,
|
||||
SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR,
|
||||
SCAN_CITY_AI_STREAM_MAX_TOKENS,
|
||||
SCAN_CITY_AI_TIMEOUT_SEC,
|
||||
SCAN_TERMINAL_BUILD_TIMEOUT_SEC,
|
||||
SCAN_TERMINAL_MAX_WORKERS,
|
||||
SCAN_TERMINAL_PAYLOAD_TTL_SEC,
|
||||
)
|
||||
from src.data_collection.city_registry import ALIASES
|
||||
from web.scan_city_ai_fallback import (
|
||||
_build_city_ai_fallback,
|
||||
@@ -63,147 +85,6 @@ from web.scan_terminal_payloads import (
|
||||
build_stale_scan_terminal_payload,
|
||||
)
|
||||
from web.scan_terminal_ranker import build_ranked_scan_terminal_result
|
||||
|
||||
_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 = max(
|
||||
10,
|
||||
int(os.getenv("POLYWEATHER_SCAN_TERMINAL_PAYLOAD_TTL_SEC", "120")),
|
||||
)
|
||||
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",
|
||||
6,
|
||||
min_value=1,
|
||||
max_value=8,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def _normalize_locale(value: Any) -> str:
|
||||
text = str(value or "").strip().lower()
|
||||
return "en-US" if text.startswith("en") else "zh-CN"
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""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
|
||||
|
||||
|
||||
_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 = max(
|
||||
10,
|
||||
int(os.getenv("POLYWEATHER_SCAN_TERMINAL_PAYLOAD_TTL_SEC", "120")),
|
||||
)
|
||||
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",
|
||||
6,
|
||||
min_value=1,
|
||||
max_value=8,
|
||||
)
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user