mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 23:44:16 +08:00
将可选数据源改为插件化架构: 插件代码放 backend/app/plugins/<name>/, 用户在设置页点击安装依赖, 主仓库不背运行时依赖(nodejs 等)。 架构: - loader.py 新增 _load_builtin_plugins() 扫描 plugins/ 目录, 通过 plugin.yaml 清单动态发现并注册插件(委托自检 + 优雅降级) - runtime 字段支持 node/python, 安装/卸载分别用 npm/pip - 现有 tickflow + YAML 自定义源逻辑 100% 保留, 插件是叠加层 stock-sdk 插件 (plugins/stocksdk/): - 原始实现来自 @forrany 的 PR #57, 迁移到插件化架构, 署名保留 - Node 桥接 (bridge.py/mjs) + Python provider, 覆盖日K/除权/分钟/实时 - 设置页主列表直接显示: 未装灰显+安装按钮, 装完可切换/卸载 配套: - instrument_sync: 通用增强, 任何 provider 都能提供标的维表 - install/uninstall: uv 优先回退 pip, 容错坏 uv.toml + 国内镜像 - 10 例单测全过, tsc 零错误
91 lines
3.3 KiB
Python
91 lines
3.3 KiB
Python
"""Python ↔ Node 桥接: 通过 subprocess 调用 bridge.mjs 使用真实 stock-sdk 抓数据。
|
|
|
|
Original implementation by @forrany (PR #57), migrated to plugin architecture.
|
|
|
|
后端是 Python, stock-sdk 是 Node/JS 包, 这里用 subprocess 把二者接起来:
|
|
每次调用 spawn 一个 `node bridge.mjs`, 从 stdin 喂 JSON job, 从 stdout 读 JSON 结果。
|
|
批内并发由 bridge.mjs 内部承担, 一次进程调用摊薄 node 启动开销。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_HERE = Path(__file__).resolve().parent
|
|
_BRIDGE_MJS = _HERE / "bridge.mjs"
|
|
|
|
# 默认超时(秒)。全市场(realtime/instruments)与大批量 daily 可能较久, provider 侧按 op 调大。
|
|
DEFAULT_TIMEOUT = 120
|
|
|
|
|
|
class StockSDKBridgeError(RuntimeError):
|
|
"""桥接调用失败(node 缺失 / stock-sdk 未安装 / 子进程异常 / 结果非法)。"""
|
|
|
|
|
|
def _node_bin() -> str | None:
|
|
"""定位 node 可执行文件: 优先环境变量 STOCK_SDK_NODE, 否则 PATH 中的 node。"""
|
|
env = os.getenv("STOCK_SDK_NODE")
|
|
if env:
|
|
return env if (Path(env).exists() or shutil.which(env)) else None
|
|
return shutil.which("node")
|
|
|
|
|
|
def run_job(job: dict, timeout: int = DEFAULT_TIMEOUT) -> dict:
|
|
"""执行一次桥接 job, 返回解析后的 dict(含 `rows`)。失败抛 StockSDKBridgeError。"""
|
|
node = _node_bin()
|
|
if not node:
|
|
raise StockSDKBridgeError(
|
|
"未找到 node 可执行文件。请安装 Node.js(>=18)或设置 STOCK_SDK_NODE 指向 node。"
|
|
)
|
|
if not _BRIDGE_MJS.exists():
|
|
raise StockSDKBridgeError(f"桥接脚本缺失: {_BRIDGE_MJS}")
|
|
|
|
payload = json.dumps(job, ensure_ascii=False)
|
|
try:
|
|
proc = subprocess.run(
|
|
[node, str(_BRIDGE_MJS)],
|
|
input=payload,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
cwd=str(_HERE),
|
|
)
|
|
except subprocess.TimeoutExpired as e:
|
|
raise StockSDKBridgeError(f"stock-sdk 桥接超时(op={job.get('op')}, {timeout}s)") from e
|
|
except OSError as e:
|
|
raise StockSDKBridgeError(f"启动 node 失败: {e}") from e
|
|
|
|
if proc.returncode != 0:
|
|
tail = (proc.stderr or proc.stdout or "").strip()[-800:]
|
|
raise StockSDKBridgeError(f"stock-sdk 桥接非零退出({proc.returncode}): {tail}")
|
|
|
|
out = (proc.stdout or "").strip()
|
|
if not out:
|
|
raise StockSDKBridgeError(f"stock-sdk 桥接无输出。stderr: {(proc.stderr or '').strip()[-500:]}")
|
|
try:
|
|
result = json.loads(out)
|
|
except json.JSONDecodeError as e:
|
|
raise StockSDKBridgeError(f"stock-sdk 桥接输出非法 JSON: {out[:500]}") from e
|
|
|
|
if not result.get("ok"):
|
|
raise StockSDKBridgeError(f"stock-sdk 桥接返回错误: {result.get('error')}")
|
|
return result
|
|
|
|
|
|
def availability() -> tuple[bool, str]:
|
|
"""探活: 返回 (是否可用, 原因)。用于 UI 与日志。不抛异常。"""
|
|
node = _node_bin()
|
|
if not node:
|
|
return False, "未找到 node(需 Node.js>=18 或设置 STOCK_SDK_NODE)"
|
|
try:
|
|
result = run_job({"op": "ping"}, timeout=20)
|
|
return True, f"ok (stock-sdk {result.get('version', '?')})"
|
|
except StockSDKBridgeError as e:
|
|
return False, str(e)
|