主站行情管理并入数据中枢
- 数据中枢数据源配置页新增「主站行情任务」卡:交易时段后台刷新开关、
手动后台刷新、历史区间回补(任务提交 + 轮询主站 job 记录亮灯),
数据与调度仍归主站,控制台只经桥接代操作
- 新增桥接端点 /api/hub-admin/market/{refresh,backfill};回补改为
market.backfill 后台任务(jobs.config.json 注册,锁独立,超时 30 分钟),
桥接调用秒回,不再阻塞
- 主站桌面端删除顶栏「行情管理」按钮与整个行情管理对话框,清理随之
失效的 CSS;移动端删除 system/admin 页与入口,管理员专区只留数据中枢
- Tushare Token / iFinD 凭证编辑沿用数据源页既有凭证区,无功能缺失
模型池收起修复
- 拉出模型清单后按钮切换为「收起列表」,收起只留一行摘要;再次点击
重新拉取并展开;勾选添加完成后清单自动收起(原逻辑保留)
- 交互全部沿用 HEL-558 已确认样式的既有按钮与提示组件,未新增视觉
自测
- verify_baseline 通过;pytest 492 项通过;数据中枢 240 项通过
- verify_datahub_console 新增 [11b] 行情任务桥接端到端段;UI 自测新增
行情任务卡开关往返、模型清单展开/收起/再展开/添加自动收起,日夜主题
与 1030 窄屏复验通过
- Playwright e2e 102/103:唯一失败项在基线提交上同样失败(本机字体度量
导致的头部溢出,与本卡无关)
Co-authored-by: multica-agent <github@multica.ai>
195 lines
7.3 KiB
Python
195 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import date
|
|
from http import HTTPStatus
|
|
|
|
from backend.features.accounts.security import token_hash, verify_password
|
|
|
|
|
|
class HubAdminHttpMixin:
|
|
"""Service-to-service bridge used by the data hub console (port 8766).
|
|
|
|
Every handler here is reached only after `require_service_token`, so the
|
|
shared `HUB_ADMIN_TOKEN` is the single trust boundary and no browser
|
|
session or CSRF token is involved. The data hub still verifies the site
|
|
session of the operator through `hub_session_check` before it exposes any
|
|
of these results to a page.
|
|
"""
|
|
|
|
def _hub_body(self) -> dict:
|
|
return self.read_json_body(allow_empty=True)
|
|
|
|
def _hub_failure(self, exc: Exception) -> None:
|
|
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
|
|
|
def hub_session_check(self) -> None:
|
|
try:
|
|
body = self._hub_body()
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
return
|
|
raw_token = str(body.get("session_token") or "")
|
|
user = (
|
|
self.application_service.database.session_user(token_hash(raw_token))
|
|
if raw_token
|
|
else None
|
|
)
|
|
if not user:
|
|
self.send_json({"ok": True, "authenticated": False})
|
|
return
|
|
self.send_json(
|
|
{
|
|
"ok": True,
|
|
"authenticated": True,
|
|
"user": {
|
|
"id": int(user["id"]),
|
|
"username": str(user["username"]),
|
|
"role": str(user.get("role") or "user"),
|
|
"is_admin": str(user.get("role") or "user") == "admin",
|
|
},
|
|
}
|
|
)
|
|
|
|
def hub_session_logout(self) -> None:
|
|
try:
|
|
body = self._hub_body()
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
return
|
|
raw_token = str(body.get("session_token") or "")
|
|
if raw_token:
|
|
self.application_service.database.delete_session(token_hash(raw_token))
|
|
self.send_json({"ok": True})
|
|
|
|
def hub_password_check(self) -> None:
|
|
try:
|
|
body = self._hub_body()
|
|
user_id = int(body.get("user_id") or 0)
|
|
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
return
|
|
stored = self.application_service.database.user_password(user_id)
|
|
verified = bool(
|
|
stored
|
|
and verify_password(
|
|
str(body.get("password") or ""),
|
|
str(stored.get("password_salt") or ""),
|
|
str(stored.get("password_hash") or ""),
|
|
)
|
|
)
|
|
self.send_json({"ok": True, "verified": verified})
|
|
|
|
def hub_system_status(self) -> None:
|
|
service = self.application_service
|
|
self.send_json({"ok": True, **service.system_status(), "users": service.admin_users()})
|
|
|
|
def hub_save_settings(self) -> None:
|
|
try:
|
|
result = self.application_service.save_system_settings(self._hub_body())
|
|
self.send_json({"ok": True, **result})
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
|
|
def hub_test_model(self) -> None:
|
|
try:
|
|
body = self._hub_body()
|
|
result = self.application_service.test_system_llm_profile(
|
|
str(body.get("model_id") or ""), body.get("profile") or {}
|
|
)
|
|
self.send_json({"ok": True, "result": result})
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
|
|
def hub_fetch_models(self) -> None:
|
|
try:
|
|
body = self._hub_body()
|
|
models = self.application_service.fetch_llm_models(
|
|
str(body.get("base_url") or ""),
|
|
str(body.get("api_key") or ""),
|
|
)
|
|
self.send_json({"ok": True, "models": models})
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
|
|
def hub_market_refresh(self) -> None:
|
|
"""HEL-566: manual snapshot refresh, submitted as a tracked job."""
|
|
try:
|
|
body = self._hub_body()
|
|
trade_date = str(body.get("trade_date") or "") or date.today().isoformat()
|
|
refresh = self.application_service.request_background_sync(trade_date)
|
|
started = bool(refresh.get("started"))
|
|
self.send_json(
|
|
{
|
|
"ok": True,
|
|
"started": started,
|
|
"job_key": str(refresh.get("job_key") or ""),
|
|
"message": "后台刷新已开始" if started else "已有后台刷新任务正在运行",
|
|
}
|
|
)
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
|
|
def hub_market_backfill(self) -> None:
|
|
"""HEL-566: historical snapshot backfill, submitted as a tracked job."""
|
|
try:
|
|
body = self._hub_body()
|
|
result = self.application_service.request_market_backfill(
|
|
str(body.get("start_date") or ""),
|
|
str(body.get("end_date") or ""),
|
|
)
|
|
started = bool(result.get("started"))
|
|
self.send_json(
|
|
{
|
|
"ok": True,
|
|
"started": started,
|
|
"job_key": str(result.get("job_key") or ""),
|
|
"message": "历史回补任务已开始" if started else "已有回补任务正在运行",
|
|
}
|
|
)
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
|
|
def hub_members(self) -> None:
|
|
service = self.application_service
|
|
self.send_json(
|
|
{
|
|
"ok": True,
|
|
"users": service.admin_users(),
|
|
"membership": service.system_status()["membership"],
|
|
}
|
|
)
|
|
|
|
def hub_save_membership(self) -> None:
|
|
try:
|
|
service = self.application_service
|
|
service.update_membership(self._hub_body())
|
|
self.send_json({"ok": True, "users": service.admin_users()})
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
|
|
def hub_invites(self) -> None:
|
|
self.send_json({"ok": True, **self.application_service.accounts.invite_overview()})
|
|
|
|
def hub_create_invites(self) -> None:
|
|
try:
|
|
body = self._hub_body()
|
|
accounts = self.application_service.accounts
|
|
codes = accounts.generate_invite_codes(
|
|
body.get("count") or 1,
|
|
str(body.get("note") or ""),
|
|
int(body.get("created_by") or 0),
|
|
)
|
|
self.send_json({"ok": True, "created": codes, **accounts.invite_overview()})
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|
|
|
|
def hub_revoke_invite(self) -> None:
|
|
try:
|
|
body = self._hub_body()
|
|
accounts = self.application_service.accounts
|
|
accounts.revoke_invite_code(str(body.get("code_id") or body.get("code") or ""))
|
|
self.send_json({"ok": True, **accounts.invite_overview()})
|
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
self._hub_failure(exc)
|