mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 21:34:21 +08:00
feat: 涨停生态 vipdoc 路径可配置 — 应用级 KV 设置 + 页面配置行
- 新增 AppSettingsStore(~/.easy_tdx/settings.db,KV 持久化,重启不丢) - GET/PUT /settings/vipdoc:读取/保存本地 vipdoc 目录(保存即校验目录 存在,并清空涨停生态/历史缓存,下次扫描立用新路径;空串=恢复自动检测) - vipdoc 解析优先级:显式参数 > 已存设置 > 自动检测(TDX_HOME/常见路径) - /limitup-ecology 响应携带 vipdoc_path 供页面展示生效目录 - 涨停生态页头部新增路径配置行(输入框+保存+恢复自动检测), 未检测到数据的空态文案引导填写
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""应用级轻量设置(KV,SQLite 持久化,重启不丢)。
|
||||
|
||||
与 watchlist/llm_history 同款存储约定:单文件 SQLite 落统一配置目录
|
||||
(``~/.easy_tdx/settings.db``,随 ``EASY_TDX_CONFIG_DIR`` 走),短连接 +
|
||||
写锁串行。存放跨重启的服务端偏好,如本地 vipdoc 路径(涨停生态扫描用)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ["AppSettingsStore", "get_app_settings_store"]
|
||||
|
||||
_write_lock = threading.Lock()
|
||||
|
||||
|
||||
def _config_dir() -> Path:
|
||||
return Path(os.environ.get("EASY_TDX_CONFIG_DIR", str(Path.home() / ".easy_tdx")))
|
||||
|
||||
|
||||
class AppSettingsStore:
|
||||
"""KV 设置存储(值为任意 JSON 可序列化对象,实际以字符串存取)。"""
|
||||
|
||||
def __init__(self, db_path: str | Path | None = None):
|
||||
self._path = Path(db_path) if db_path else _config_dir() / "settings.db"
|
||||
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with _write_lock:
|
||||
conn = self._connect()
|
||||
try:
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS settings ("
|
||||
"key TEXT PRIMARY KEY, value TEXT NOT NULL)"
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(self._path, timeout=10)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def get(self, key: str, default: str | None = None) -> str | None:
|
||||
conn = self._connect()
|
||||
try:
|
||||
row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
|
||||
return str(row["value"]) if row is not None else default
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def set(self, key: str, value: str) -> None:
|
||||
with _write_lock:
|
||||
conn = self._connect()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
|
||||
(key, value),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def delete(self, key: str) -> None:
|
||||
with _write_lock:
|
||||
conn = self._connect()
|
||||
try:
|
||||
conn.execute("DELETE FROM settings WHERE key = ?", (key,))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
_store: AppSettingsStore | None = None
|
||||
_store_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_app_settings_store() -> AppSettingsStore:
|
||||
"""进程级单例(测试可通过 ``app_settings_store._store = None`` 重置)。"""
|
||||
global _store
|
||||
with _store_lock:
|
||||
if _store is None:
|
||||
_store = AppSettingsStore()
|
||||
return _store
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
@@ -98,6 +99,61 @@ async def market_session() -> dict[str, Any]:
|
||||
return session_info()
|
||||
|
||||
|
||||
# 本地 vipdoc 路径设置(应用级 KV,重启不丢;涨停生态/涨停历史共用)
|
||||
_VIPDOC_KEY = "vipdoc"
|
||||
|
||||
|
||||
def _effective_vipdoc(explicit: str | None) -> str | None:
|
||||
"""vipdoc 解析优先级:显式参数 > 已存设置 > 自动检测(None)。"""
|
||||
if explicit:
|
||||
return explicit
|
||||
from easy_tdx.web.app_settings_store import get_app_settings_store
|
||||
|
||||
return get_app_settings_store().get(_VIPDOC_KEY)
|
||||
|
||||
|
||||
@router.get("/settings/vipdoc")
|
||||
async def get_vipdoc_setting() -> dict[str, Any]:
|
||||
"""读取本地 vipdoc 路径设置(含自动检测的当前生效值,供涨停生态页配置)。"""
|
||||
from easy_tdx.web.app_settings_store import get_app_settings_store
|
||||
|
||||
stored = get_app_settings_store().get(_VIPDOC_KEY)
|
||||
effective = _effective_vipdoc(None)
|
||||
try:
|
||||
from easy_tdx.offline.paths import resolve_vipdoc
|
||||
|
||||
resolved = str(resolve_vipdoc(effective))
|
||||
except Exception: # noqa: BLE001 — 未检测到/路径无效
|
||||
resolved = None
|
||||
return {"stored": stored, "resolved": resolved}
|
||||
|
||||
|
||||
@router.put("/settings/vipdoc")
|
||||
async def set_vipdoc_setting(req: dict[str, Any]) -> dict[str, Any]:
|
||||
"""保存/清除本地 vipdoc 路径设置(空串 = 恢复自动检测)。
|
||||
|
||||
保存即校验目录存在;成功后清空涨停生态/历史缓存,下次扫描立用新路径。
|
||||
"""
|
||||
from easy_tdx.offline.paths import resolve_vipdoc
|
||||
from easy_tdx.web.app_settings_store import get_app_settings_store
|
||||
|
||||
path = str(req.get("path") or "").strip()
|
||||
if path:
|
||||
p = Path(path)
|
||||
if not p.is_dir():
|
||||
raise ValueError(f"路径不存在或不是目录: {p}")
|
||||
get_app_settings_store().set(_VIPDOC_KEY, str(p))
|
||||
resolved = str(resolve_vipdoc(str(p)))
|
||||
else:
|
||||
get_app_settings_store().delete(_VIPDOC_KEY)
|
||||
resolved = None
|
||||
# 路径变更后旧扫描结果作废
|
||||
global _limitup_cache, _limitup_history_cache
|
||||
_limitup_cache = None
|
||||
_limitup_history_cache.clear()
|
||||
return {"stored": path, "resolved": resolved}
|
||||
|
||||
|
||||
@router.get("/limitup-ecology", response_model=DictResponse)
|
||||
async def limitup_ecology(
|
||||
vipdoc: str | None = Query(None, description="离线数据目录(默认自动检测)"),
|
||||
@@ -114,13 +170,23 @@ async def limitup_ecology(
|
||||
if _limitup_cache is not None and now - _limitup_cache[0] < _LIMITUP_TTL:
|
||||
return DictResponse.from_dict(_limitup_cache[1])
|
||||
|
||||
effective = _effective_vipdoc(vipdoc)
|
||||
|
||||
def _scan() -> dict[str, Any]:
|
||||
from easy_tdx.screen.limitup import compute_limitup_ecology
|
||||
|
||||
eco = compute_limitup_ecology(vipdoc)
|
||||
eco = compute_limitup_ecology(effective)
|
||||
vipdoc_path = None
|
||||
try:
|
||||
from easy_tdx.offline.paths import resolve_vipdoc
|
||||
|
||||
vipdoc_path = str(resolve_vipdoc(effective))
|
||||
except Exception: # noqa: BLE001 — 未检测到时前端展示配置入口
|
||||
pass
|
||||
return {
|
||||
"data_date": eco.data_date,
|
||||
"total": eco.total,
|
||||
"vipdoc_path": vipdoc_path,
|
||||
"summary": eco.summary(),
|
||||
"limit_up": [asdict(e) for e in eco.limit_up],
|
||||
"limit_down": [asdict(e) for e in eco.limit_down],
|
||||
@@ -187,7 +253,7 @@ async def limitup_history(
|
||||
def _scan() -> dict[str, Any]:
|
||||
from easy_tdx.screen.limitup import compute_limitup_history
|
||||
|
||||
rows = compute_limitup_history(vipdoc, days=days)
|
||||
rows = compute_limitup_history(_effective_vipdoc(vipdoc), days=days)
|
||||
return {"count": len(rows), "days": rows}
|
||||
|
||||
payload = await asyncio.to_thread(_scan)
|
||||
|
||||
Reference in New Issue
Block a user