mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
fix: 完善自定义分钟数据源配置与异常处理
- 补齐资产类型和周期参数在 API、YAML 与前端编辑器中的保存回读 - 权限判断和分时监控统一复用安全的数据源解析边界 - 增加配置往返及解析异常回归测试
This commit is contained in:
@@ -25,10 +25,10 @@ def _minute_allowed(capset) -> bool:
|
||||
return True
|
||||
from app.services import preferences
|
||||
provider = preferences.get_minute_data_provider()
|
||||
if provider == "tickflow":
|
||||
return False
|
||||
from app.data_providers import custom as custom_sources
|
||||
return custom_sources.provider_has_dataset(provider, "minute")
|
||||
_, fallback, error = kline_sync._resolve_minute_provider(provider)
|
||||
if error is not None:
|
||||
logger.warning("minute provider resolution failed while checking access: %s", error)
|
||||
return not fallback
|
||||
|
||||
|
||||
@router.get("/instruments/search")
|
||||
|
||||
@@ -371,6 +371,8 @@ class DatasetConfigIn(BaseModel):
|
||||
symbols_param: str = "symbols"
|
||||
start_param: str = "start_time"
|
||||
end_param: str = "end_time"
|
||||
asset_type_param: str | None = None
|
||||
freq_param: str | None = None
|
||||
|
||||
|
||||
class AuthConfigIn(BaseModel):
|
||||
@@ -1429,4 +1431,3 @@ def update_review_push(req: ReviewPushIn) -> dict:
|
||||
from app.services import preferences
|
||||
saved = preferences.set_review_push_channels(req.channels)
|
||||
return {"review_push_channels": saved}
|
||||
|
||||
|
||||
@@ -296,6 +296,8 @@ def _config_to_dict(config: CustomSourceConfig) -> dict:
|
||||
"symbols_param": ds.symbols_param,
|
||||
"start_param": ds.start_param,
|
||||
"end_param": ds.end_param,
|
||||
**({"asset_type_param": ds.asset_type_param} if ds.asset_type_param else {}),
|
||||
**({"freq_param": ds.freq_param} if ds.freq_param else {}),
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -401,6 +403,10 @@ def _sanitize_dataset(ds_cfg: dict) -> dict:
|
||||
out["start_param"] = str(ds_cfg["start_param"])
|
||||
if ds_cfg.get("end_param"):
|
||||
out["end_param"] = str(ds_cfg["end_param"])
|
||||
if ds_cfg.get("asset_type_param"):
|
||||
out["asset_type_param"] = str(ds_cfg["asset_type_param"])
|
||||
if ds_cfg.get("freq_param"):
|
||||
out["freq_param"] = str(ds_cfg["freq_param"])
|
||||
return out
|
||||
|
||||
|
||||
@@ -496,4 +502,3 @@ def _load_entry(entry_ref: str):
|
||||
|
||||
# 模块导入时即扫描一次, 保证 names()/_allowed_data_providers() 在 startup 前可用。
|
||||
_load_builtin_plugins()
|
||||
|
||||
|
||||
@@ -733,13 +733,14 @@ def sync_minute_batch(
|
||||
def intraday_monitor_support(capset: CapabilitySet | None) -> dict[str, object]:
|
||||
"""返回分时信号监控可用的数据能力和单轮标的上限。"""
|
||||
provider_name = preferences.get_minute_data_provider()
|
||||
if provider_name != "tickflow":
|
||||
from app.data_providers import custom as custom_sources
|
||||
if custom_sources.provider_has_dataset(provider_name, "minute"):
|
||||
return {
|
||||
"available": True, "source": "custom_minute", "max_symbols": 100,
|
||||
"reason": "使用已配置的分钟数据插件",
|
||||
}
|
||||
_, fallback, error = _resolve_minute_provider(provider_name)
|
||||
if not fallback:
|
||||
return {
|
||||
"available": True, "source": "custom_minute", "max_symbols": 100,
|
||||
"reason": "使用已配置的分钟数据插件",
|
||||
}
|
||||
if error is not None:
|
||||
logger.warning("minute provider resolution failed while checking monitor support: %s", error)
|
||||
if capset is None:
|
||||
return {
|
||||
"available": False, "source": None, "max_symbols": 0,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from app.api.settings import DatasetConfigIn
|
||||
from app.data_providers.custom.config import CustomSourceConfig, _dataset_from_dict
|
||||
from app.data_providers.custom.loader import _config_to_dict, _sanitize_for_yaml
|
||||
|
||||
|
||||
def test_minute_request_parameter_names_survive_config_round_trip():
|
||||
dataset = DatasetConfigIn(
|
||||
url="https://example.test/minute",
|
||||
method="GET",
|
||||
asset_type_param="asset",
|
||||
freq_param="period",
|
||||
).model_dump()
|
||||
|
||||
cleaned = _sanitize_for_yaml({
|
||||
"name": "test_source",
|
||||
"display_name": "Test Source",
|
||||
"datasets": {"minute": dataset},
|
||||
})
|
||||
parsed = _dataset_from_dict(cleaned["datasets"]["minute"])
|
||||
exposed = _config_to_dict(CustomSourceConfig(
|
||||
name="test_source",
|
||||
display_name="Test Source",
|
||||
datasets={"minute": parsed},
|
||||
))
|
||||
|
||||
assert parsed.asset_type_param == "asset"
|
||||
assert parsed.freq_param == "period"
|
||||
assert exposed["datasets"]["minute"]["asset_type_param"] == "asset"
|
||||
assert exposed["datasets"]["minute"]["freq_param"] == "period"
|
||||
@@ -639,3 +639,44 @@ def test_resolve_minute_provider_success_returns_provider(monkeypatch):
|
||||
assert provider is mock_provider
|
||||
assert fallback is False
|
||||
assert err is None
|
||||
|
||||
|
||||
def test_minute_allowed_resolver_exception_returns_false(monkeypatch):
|
||||
"""权限入口复用安全 resolver, 插件注册异常不再穿透为 500。"""
|
||||
from app.api import kline as kline_api
|
||||
from app.tickflow.capabilities import CapabilitySet
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.preferences.get_minute_data_provider",
|
||||
lambda: "broken",
|
||||
)
|
||||
|
||||
def _raising(name, dataset):
|
||||
raise RuntimeError("registry corrupted")
|
||||
|
||||
monkeypatch.setattr("app.data_providers.custom.provider_has_dataset", _raising)
|
||||
|
||||
assert kline_api._minute_allowed(CapabilitySet()) is False
|
||||
|
||||
|
||||
def test_intraday_monitor_support_resolver_exception_falls_back(monkeypatch):
|
||||
"""监控入口解析自定义源失败后继续按 TickFlow 能力判断。"""
|
||||
from app.tickflow.capabilities import Cap, CapabilitySet
|
||||
|
||||
monkeypatch.setattr(
|
||||
kline_sync.preferences,
|
||||
"get_minute_data_provider",
|
||||
lambda: "broken",
|
||||
)
|
||||
|
||||
def _raising(name, dataset):
|
||||
raise RuntimeError("registry corrupted")
|
||||
|
||||
monkeypatch.setattr("app.data_providers.custom.provider_has_dataset", _raising)
|
||||
capset = CapabilitySet()
|
||||
capset.grant(Cap.KLINE_MINUTE_BATCH)
|
||||
|
||||
support = kline_sync.intraday_monitor_support(capset)
|
||||
|
||||
assert support["available"] is True
|
||||
assert support["source"] == "minute_batch"
|
||||
|
||||
@@ -140,6 +140,15 @@ start_param: start_time
|
||||
end_param: end_time
|
||||
```
|
||||
|
||||
分钟数据源如果需要区分资产类型或周期,可继续配置:
|
||||
|
||||
```yaml
|
||||
asset_type_param: asset_type
|
||||
freq_param: period
|
||||
```
|
||||
|
||||
配置后,分钟请求会分别传入 `stock` / `etf` / `index` 和 `1m`;留空时不向上游发送这两个参数,以兼容已有数据源。
|
||||
|
||||
## 鉴权
|
||||
|
||||
支持三种简单鉴权:
|
||||
@@ -295,4 +304,3 @@ datasets:
|
||||
```
|
||||
|
||||
把这段 YAML 保存为 `data/data_sources/my_source.yaml`,然后在设置页重新加载即可。
|
||||
|
||||
|
||||
@@ -821,6 +821,8 @@ export interface DatasetConfig {
|
||||
symbols_param?: string
|
||||
start_param?: string
|
||||
end_param?: string
|
||||
asset_type_param?: string | null
|
||||
freq_param?: string | null
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
|
||||
@@ -395,6 +395,27 @@ function DatasetDetail({
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{datasetKey === 'minute' && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<Field label="资产类型参数">
|
||||
<input
|
||||
value={cfg.asset_type_param ?? ''}
|
||||
onChange={e => onUpdate({ asset_type_param: e.target.value || null })}
|
||||
placeholder="asset_type"
|
||||
className={`${INPUT_CLS} w-full`}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="周期参数">
|
||||
<input
|
||||
value={cfg.freq_param ?? ''}
|
||||
onChange={e => onUpdate({ freq_param: e.target.value || null })}
|
||||
placeholder="period"
|
||||
className={`${INPUT_CLS} w-full`}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-[10px] uppercase tracking-widest text-muted">字段映射</div>
|
||||
|
||||
Reference in New Issue
Block a user