mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
feat(plugins): 新增 fuyao 同花顺官方实时行情插件(默认 hidden)与插件 Key 界面配置
- fuyao 插件: 全市场实时快照分页拉取, 字段对齐数据契约(pct 百分制÷100), 兼容官方文档与实际返回两种信封/字段名; 优化完成前 plugin.yaml 标记 hidden: true, 加载器跳过注册不展示, 删除该行即可开放 - plugin.yaml 新增 api_key_env 约定: 设置页提供 Key 输入框, 先探后存 (POST/DELETE /api/settings/plugin-key), secrets.json 优先于 .env - secrets_store.get_env_backed_secret(field, env_name) 支持插件取 Key - loader: 插件状态透出 api_key_env, 新增 probe_plugin_key 委托探测 - 31 个插件测试; docs/plugin-development.md 补充约定说明
This commit is contained in:
@@ -401,6 +401,11 @@ class DataProvidersIn(BaseModel):
|
||||
financial_data_provider: str | None = None
|
||||
|
||||
|
||||
class PluginKeyIn(BaseModel):
|
||||
plugin: str
|
||||
api_key: str
|
||||
|
||||
|
||||
class DataSourceJobTimeoutPrefs(BaseModel):
|
||||
data_source_job_timeout_s: int = Field(ge=60)
|
||||
data_source_long_job_timeout_s: int = Field(ge=60)
|
||||
@@ -536,6 +541,53 @@ def list_data_sources() -> dict:
|
||||
}
|
||||
|
||||
|
||||
@router.post("/plugin-key")
|
||||
def save_plugin_key(req: PluginKeyIn) -> dict:
|
||||
"""保存插件 API Key(先探后存, 对齐 /tickflow-key 语义)。
|
||||
|
||||
流程: probe_plugin_key 用候选 Key 实探 → 有效才写 secrets.json
|
||||
({plugin}_api_key, 优先级高于 .env) → load_all 重扫, 插件即刻变为可切换。
|
||||
"""
|
||||
from app.data_providers import custom as custom_sources
|
||||
|
||||
name = req.plugin.strip().lower()
|
||||
key = req.api_key.strip()
|
||||
if not key:
|
||||
return {"ok": False, "error": "key empty"}
|
||||
ok, message = custom_sources.probe_plugin_key(name, key)
|
||||
if not ok:
|
||||
return {"ok": False, "reason": "invalid", "error": message}
|
||||
secrets_store.save({f"{name}_api_key": key})
|
||||
custom_sources.load_all()
|
||||
status = next((p for p in custom_sources.list_plugins() if p["name"] == name), None)
|
||||
return {
|
||||
"ok": True,
|
||||
"api_key_masked": secrets_store.mask(key),
|
||||
"plugin_available": bool(status and status.get("available")),
|
||||
"plugin": status,
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/plugin-key/{name}")
|
||||
def clear_plugin_key(name: str) -> dict:
|
||||
"""清除插件的界面配置 Key(secrets.json);.env 里的同名变量仍然生效。"""
|
||||
from app.data_providers import custom as custom_sources
|
||||
|
||||
manifest = custom_sources.plugin_manifest(name)
|
||||
if manifest is None or not custom_sources.is_builtin(name):
|
||||
raise HTTPException(status_code=404, detail=f"插件 '{name}' 不存在")
|
||||
if not manifest.get("api_key_env"):
|
||||
raise HTTPException(status_code=400, detail=f"插件 '{name}' 不支持在界面配置 Key")
|
||||
secrets_store.clear(f"{name.lower()}_api_key")
|
||||
custom_sources.load_all()
|
||||
status = next((p for p in custom_sources.list_plugins() if p["name"] == name), None)
|
||||
return {
|
||||
"ok": True,
|
||||
"plugin_available": bool(status and status.get("available")),
|
||||
"plugin": status,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/data-sources/reload")
|
||||
def reload_data_sources() -> dict:
|
||||
"""重新加载 data_sources/*.yaml。"""
|
||||
|
||||
Reference in New Issue
Block a user