feat: 数据源插件化架构 + stock-sdk 首个插件

将可选数据源改为插件化架构: 插件代码放 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 零错误
This commit is contained in:
shy3130
2026-07-06 17:00:56 +08:00
parent cc697e8ea4
commit 966d94ab03
16 changed files with 3374 additions and 1860 deletions
+49 -2
View File
@@ -413,10 +413,11 @@ def get_preferences() -> dict:
@router.get("/data-sources")
def list_data_sources() -> dict:
"""列出已加载的自定义数据源。"""
"""列出已加载的数据源 (内置 / 插件 / 用户自定义)"""
from app.data_providers import custom as custom_sources
return {
"builtin": [{"name": "tickflow", "display_name": "TickFlow", "datasets": ["daily", "adj_factor", "realtime"]}],
"builtin": [{"name": "tickflow", "display_name": "TickFlow", "datasets": ["daily", "adj_factor", "realtime", "minute"]}],
"plugins": custom_sources.list_plugins(),
"custom": custom_sources.list_sources(),
"errors": custom_sources.errors(),
"config_dir": str(custom_sources.data_sources_dir()),
@@ -431,6 +432,52 @@ def reload_data_sources() -> dict:
return list_data_sources()
@router.post("/plugins/{name}/install")
def install_plugin(name: str) -> dict:
"""安装指定插件的依赖 (npm install / pip install), 完成后重新扫描。
根据 plugin.yaml 的 runtime 字段决定安装方式。安装可能耗时较长 (网络下载),
客户端需设较长超时。
"""
from app.data_providers import custom as custom_sources
if not custom_sources.is_builtin(name):
raise HTTPException(status_code=404, detail=f"插件 '{name}' 不存在")
ok, message = custom_sources.install_plugin(name)
# 无论成功失败都重新扫描, 刷新插件状态 (安装可能部分成功)
custom_sources.load_all()
result = list_data_sources()
result["install_ok"] = ok
result["install_message"] = message
return result
@router.delete("/plugins/{name}/install")
def uninstall_plugin(name: str) -> dict:
"""卸载指定插件的依赖 (删除 node_modules / pip uninstall), 完成后重新扫描。
如果该插件当前正被使用, 自动回退到 tickflow。
"""
from app.data_providers import custom as custom_sources
from app.services import preferences
if not custom_sources.is_builtin(name):
raise HTTPException(status_code=404, detail=f"插件 '{name}' 不存在")
ok, message = custom_sources.uninstall_plugin(name)
# 卸载后若该插件正被使用, 回退 tickflow
for getter, key, default in [
(preferences.get_daily_data_provider, "daily_data_provider", "tickflow"),
(preferences.get_minute_data_provider, "minute_data_provider", "tickflow"),
(preferences.get_realtime_data_provider, "realtime_data_provider", "tickflow"),
(preferences.get_financial_provider, "financial_data_provider", "tickflow"),
]:
if getter() == name:
preferences.save({key: default})
custom_sources.load_all()
result = list_data_sources()
result["uninstall_ok"] = ok
result["uninstall_message"] = message
return result
@router.get("/data-sources/{name}")
def get_data_source(name: str) -> dict:
"""读取一个自定义数据源的完整配置(用于前端编辑回填)。"""