From 13f28452010d6e4c9eaa4ad81a12c30254c9acf8 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 27 Apr 2026 14:51:18 +0800 Subject: [PATCH] 1 --- backend/app/api/auth.py | 45 +++++- backend/app/api/paper_trading.py | 9 +- backend/app/api/system.py | 5 +- backend/app/config.py | 2 + backend/app/crypto_agent/crypto_agent.py | 17 +- backend/app/middleware/console_auth.py | 39 +++++ frontend/console.html | 195 ++++++++++++++++++++++- 7 files changed, 292 insertions(+), 20 deletions(-) create mode 100644 backend/app/middleware/console_auth.py diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 2319bab..8a11508 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -1,7 +1,7 @@ """ 认证API """ -from fastapi import APIRouter, HTTPException, Depends, Request +from fastapi import APIRouter, HTTPException, Depends, Request, Response from app.models.auth import ( SendCodeRequest, SendCodeResponse, LoginRequest, LoginResponse, @@ -12,11 +12,54 @@ from app.services.auth_service import auth_service from app.services.jwt_service import jwt_service from app.services.db_service import db_service from app.middleware.auth_middleware import get_current_user, get_client_ip +from app.middleware.console_auth import ( + CONSOLE_AUTH_COOKIE, + create_console_access_token, + require_console_access, +) +from app.config import get_settings from app.utils.logger import logger router = APIRouter(prefix="/api/auth", tags=["认证"]) +@router.post("/console-login") +async def console_login(request: Request, response: Response): + try: + body = await request.json() + except Exception: + body = {} + + password = str(body.get("password") or "") + settings = get_settings() + if not password or password != settings.console_access_password: + raise HTTPException(status_code=403, detail="访问密码错误") + + token = create_console_access_token() + max_age = max(1, int(settings.console_access_expire_days or 30)) * 24 * 60 * 60 + response.set_cookie( + key=CONSOLE_AUTH_COOKIE, + value=token, + max_age=max_age, + httponly=True, + samesite="lax", + secure=False, + path="/", + ) + return {"success": True, "message": "总控台登录成功"} + + +@router.post("/console-logout") +async def console_logout(response: Response): + response.delete_cookie(CONSOLE_AUTH_COOKIE, path="/") + return {"success": True, "message": "已退出总控台"} + + +@router.get("/console-session") +async def get_console_session(_: dict = Depends(require_console_access)): + return {"success": True, "authenticated": True} + + @router.post("/send-code", response_model=SendCodeResponse) async def send_verification_code( request_data: SendCodeRequest, diff --git a/backend/app/api/paper_trading.py b/backend/app/api/paper_trading.py index 9213567..f36f379 100644 --- a/backend/app/api/paper_trading.py +++ b/backend/app/api/paper_trading.py @@ -1,7 +1,7 @@ """ 交易 API """ -from fastapi import APIRouter, HTTPException, Query +from fastapi import APIRouter, HTTPException, Query, Depends from typing import Optional from datetime import datetime from pydantic import BaseModel @@ -13,6 +13,7 @@ from app.services.db_service import db_service from app.services.runtime_status_service import get_runtime_status from app.utils.logger import logger from app.crypto_agent.crypto_agent import get_crypto_agent +from app.middleware.console_auth import require_console_access router = APIRouter(prefix="/api/trading", tags=["交易"]) @@ -249,7 +250,7 @@ async def get_platform_halts(): @router.get("/execution-controls") -async def get_execution_controls(): +async def get_execution_controls(_: dict = Depends(require_console_access)): """获取目标级自动交易开关状态""" try: agent = get_crypto_agent() @@ -263,7 +264,7 @@ async def get_execution_controls(): @router.post("/execution-controls") -async def set_execution_controls(request: ExecutionControlRequest): +async def set_execution_controls(request: ExecutionControlRequest, _: dict = Depends(require_console_access)): """设置目标级自动交易开关""" try: agent = get_crypto_agent() @@ -286,7 +287,7 @@ async def set_execution_controls(request: ExecutionControlRequest): @router.post("/platform-halts/resume") -async def resume_platform(request: ResumePlatformRequest): +async def resume_platform(request: ResumePlatformRequest, _: dict = Depends(require_console_access)): """手动恢复指定平台执行""" try: agent = get_crypto_agent() diff --git a/backend/app/api/system.py b/backend/app/api/system.py index 744b0a0..ddd92f2 100644 --- a/backend/app/api/system.py +++ b/backend/app/api/system.py @@ -2,10 +2,11 @@ 系统状态 API """ from datetime import datetime, timedelta -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Depends from typing import Dict, Any import numpy as np from app.config import get_settings +from app.middleware.console_auth import require_console_access from app.utils.logger import logger from app.utils.system_status import get_system_monitor from app.crypto_agent.crypto_agent import get_crypto_agent @@ -339,7 +340,7 @@ async def get_agent_status(agent_id: str): @router.get("/console", response_model=Dict[str, Any]) -async def get_console_snapshot(): +async def get_console_snapshot(_: dict = Depends(require_console_access)): """ 获取总控台快照 diff --git a/backend/app/config.py b/backend/app/config.py index 6ed9903..34fb249 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -64,6 +64,8 @@ class Settings(BaseSettings): # 安全配置 secret_key: str = "change-this-secret-key-in-production" rate_limit: str = "100/minute" + console_access_password: str = "223388" # 总控台访问密码 + console_access_expire_days: int = 30 # 总控台访问会话有效期 # JWT配置 jwt_algorithm: str = "HS256" diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 140319b..1b49d90 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -2447,20 +2447,24 @@ class CryptoAgent: if setup_type in {'trend_continuation_pullback', 'deep_pullback_continuation', 'breakout_pullback'}: if entry_type != 'limit': return False, f"{setup_type} 应使用 limit 等待回踩/反抽" + if location_tag == 'far_from_trade_zone': + return False, f"{setup_type} 当前远离有效回踩交易区" if pullback_quality != 'healthy_pullback': - return False, f"{setup_type} 缺少健康缩量回调证据" - if location_tag in {'far_from_trade_zone', 'middle_of_range'}: - return False, f"{setup_type} 当前不在有效回踩交易区" + return True, f"{setup_type} 缺少健康缩量回调证据,降级执行" + if location_tag == 'middle_of_range': + return True, f"{setup_type} 当前位于区间中部,降级执行" if setup_type == 'range_reversal': + if location_tag == 'far_from_trade_zone': + return False, "区间反转 setup 远离关键交易区" if location_tag not in {'near_range_support', 'near_range_resistance'}: - return False, "区间反转 setup 不在区间边界" + return True, "区间反转 setup 不在区间边界,降级执行" if rejection_signal not in {'bullish_rejection', 'bearish_rejection'} and entry_type == 'market': return False, "区间反转现价执行缺少明确拒绝信号" if setup_type == 'trend_reversal': if rejection_signal not in {'bullish_rejection', 'bearish_rejection'}: - return False, "趋势反转 setup 缺少拒绝/结构切换证据" + return True, "趋势反转 setup 缺少明确拒绝信号,降级执行" if exhaustion_risk in {'upside_climax', 'downside_climax'} and setup_type != 'trend_reversal': return False, "当前量价处于高潮风险,非反转 setup 不执行" @@ -3995,6 +3999,9 @@ class CryptoAgent: setup_passed, setup_reason = self._check_setup_execution_constraints(signal) if not setup_passed: return False, f"setup 过滤: {setup_reason}" + if setup_reason != "setup 约束通过": + signal["_setup_execution_warning"] = setup_reason + logger.info(f"[{platform_name}] ⚠️ setup 降级执行: {setup_reason}") current_leverage = account.get('current_total_leverage', 0) max_leverage = account.get('max_total_leverage', 10) diff --git a/backend/app/middleware/console_auth.py b/backend/app/middleware/console_auth.py new file mode 100644 index 0000000..4a0d58b --- /dev/null +++ b/backend/app/middleware/console_auth.py @@ -0,0 +1,39 @@ +from datetime import datetime, timedelta +from typing import Dict + +from fastapi import Cookie, HTTPException +from jose import JWTError, jwt + +from app.config import get_settings + + +CONSOLE_AUTH_COOKIE = "console_access_token" + + +def create_console_access_token() -> str: + settings = get_settings() + expire = datetime.utcnow() + timedelta(days=max(1, int(settings.console_access_expire_days or 30))) + payload = { + "scope": "console_access", + "exp": expire, + "iat": datetime.utcnow(), + } + return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm) + + +def verify_console_access_token(token: str) -> Dict: + settings = get_settings() + try: + payload = jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm]) + except JWTError as exc: + raise HTTPException(status_code=401, detail="总控台访问已失效,请重新登录") from exc + + if payload.get("scope") != "console_access": + raise HTTPException(status_code=401, detail="总控台访问凭证无效") + return payload + + +def require_console_access(console_access_token: str | None = Cookie(default=None, alias=CONSOLE_AUTH_COOKIE)) -> Dict: + if not console_access_token: + raise HTTPException(status_code=401, detail="请先登录总控台") + return verify_console_access_token(console_access_token) diff --git a/frontend/console.html b/frontend/console.html index 62a2d62..2103c80 100644 --- a/frontend/console.html +++ b/frontend/console.html @@ -52,6 +52,70 @@ mask-image: linear-gradient(180deg, rgba(0,0,0,0.45), rgba(0,0,0,0.9)); } + .auth-overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(3, 8, 14, 0.82); + backdrop-filter: blur(18px); + } + + .auth-overlay.hidden { + display: none; + } + + .auth-card { + width: min(420px, 100%); + padding: 28px; + border-radius: 24px; + background: rgba(9, 18, 28, 0.96); + border: 1px solid rgba(128, 169, 202, 0.24); + box-shadow: var(--shadow); + } + + .auth-card h2 { + margin: 0 0 10px; + font-size: 28px; + } + + .auth-card p { + margin: 0 0 18px; + color: var(--muted); + line-height: 1.7; + } + + .auth-field { + width: 100%; + padding: 14px 16px; + border-radius: 14px; + border: 1px solid rgba(255,255,255,0.08); + background: rgba(255,255,255,0.04); + color: var(--text); + font-size: 15px; + outline: none; + } + + .auth-field:focus { + border-color: rgba(126, 200, 255, 0.36); + } + + .auth-actions { + display: flex; + gap: 12px; + margin-top: 14px; + } + + .auth-error { + min-height: 20px; + margin-top: 12px; + color: var(--danger); + font-size: 13px; + } + .console-shell { width: min(1600px, calc(100vw - 32px)); margin: 0 auto; @@ -2466,6 +2530,19 @@ +
+
+
Console Access
+

输入访问密码

+

总控台包含账户权益、持仓、挂单和执行控制,需要先完成访问验证。

+ +
+ +
+
+
+
+
@@ -2691,6 +2768,7 @@ let cachedExecutionEvents = []; let cachedConsoleData = null; let revealSensitiveData = false; + let consoleAuthenticated = false; const SENSITIVE_VISIBILITY_KEY = 'console_sensitive_visible_v2'; const HUB_PREFERENCE_KEY = 'console_hub_active_v1'; @@ -2712,10 +2790,24 @@ return Number.isFinite(num) ? `$${formatNumber(num, 2)}` : '-'; } + function parseConsoleDate(value) { + if (!value) return null; + if (value instanceof Date) return value; + const text = String(value).trim(); + if (!text) return null; + + const normalized = /([zZ]|[+-]\d{2}:\d{2})$/.test(text) ? text : `${text}Z`; + const date = new Date(normalized); + if (Number.isNaN(date.getTime())) return null; + return date; + } + function formatTime(value) { if (!value) return '-'; try { - return new Date(value).toLocaleString('zh-CN', { hour12: false }); + const date = parseConsoleDate(value); + if (!date) return String(value); + return date.toLocaleString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' }); } catch { return String(value); } @@ -2723,7 +2815,8 @@ function relativeTime(value) { if (!value) return '-'; - const target = new Date(value); + const target = parseConsoleDate(value); + if (!target) return String(value); if (Number.isNaN(target.getTime())) return String(value); const diff = Date.now() - target.getTime(); const sec = Math.max(0, Math.floor(diff / 1000)); @@ -2772,6 +2865,64 @@ `; } + function showConsoleAuthOverlay(message = '') { + document.getElementById('consoleAuthOverlay').classList.remove('hidden'); + document.getElementById('consoleAuthError').textContent = message; + } + + function hideConsoleAuthOverlay() { + document.getElementById('consoleAuthOverlay').classList.add('hidden'); + document.getElementById('consoleAuthError').textContent = ''; + } + + async function ensureConsoleSession() { + const response = await fetch('/api/auth/console-session'); + if (!response.ok) { + consoleAuthenticated = false; + showConsoleAuthOverlay(response.status === 401 ? '请输入访问密码后继续。' : '总控台认证失败'); + return false; + } + + consoleAuthenticated = true; + hideConsoleAuthOverlay(); + return true; + } + + async function loginConsole() { + const input = document.getElementById('consolePasswordInput'); + const password = input.value.trim(); + if (!password) { + showConsoleAuthOverlay('请输入访问密码'); + return; + } + + const button = document.getElementById('consoleLoginBtn'); + button.disabled = true; + button.textContent = '验证中...'; + + try { + const response = await fetch('/api/auth/console-login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }) + }); + const result = await response.json(); + if (!response.ok || !result.success) { + throw new Error(result.detail || result.message || '访问密码错误'); + } + + input.value = ''; + consoleAuthenticated = true; + hideConsoleAuthOverlay(); + loadConsole(); + } catch (error) { + showConsoleAuthOverlay(error.message || '登录失败'); + } finally { + button.disabled = false; + button.textContent = '进入总控台'; + } + } + function normalizeSeverity(severity) { const value = String(severity || '').toLowerCase(); if (['danger', 'error', 'critical'].includes(value)) return 'danger'; @@ -2783,7 +2934,8 @@ const cutoff = Date.now() - hours * 3600 * 1000; return (events || []).filter((event) => { const rawTime = event?.timestamp || event?.created_at || event?.opened_at; - const time = rawTime ? new Date(rawTime).getTime() : 0; + const parsed = parseConsoleDate(rawTime); + const time = parsed ? parsed.getTime() : 0; if (!time || Number.isNaN(time) || time < cutoff) return false; return typeof matcher === 'function' ? matcher(event) : true; }).length; @@ -2804,7 +2956,7 @@ const protectionCoverage = positions.length > 0 ? (completeProtectionCount / positions.length) * 100 : 100; const conversionRate = recentSignal24h > 0 ? (success24h / recentSignal24h) * 100 : 0; const staleEntryOrders = entryOrders.filter((order) => { - const created = order.created_at ? new Date(order.created_at).getTime() : 0; + const created = order.created_at ? (parseConsoleDate(order.created_at)?.getTime() || 0) : 0; return created && !Number.isNaN(created) && (Date.now() - created) > (30 * 60 * 1000); }).length; @@ -2937,17 +3089,21 @@ } function findMatchingEvent(signal, laneKeyword, events, platform) { - const signalTime = signal?.created_at ? new Date(signal.created_at).getTime() : 0; + const signalTime = signal?.created_at ? (parseConsoleDate(signal.created_at)?.getTime() || 0) : 0; const symbol = signal?.symbol; const candidates = (events || []).filter((event) => { if (event.symbol !== symbol) return false; if (!eventPlatformMatches(event, platform)) return false; if (laneKeyword && event.signal_timeframe_text && !String(event.signal_timeframe_text).includes(laneKeyword)) return false; - const eventTime = event?.timestamp ? new Date(event.timestamp).getTime() : 0; + const eventTime = event?.timestamp ? (parseConsoleDate(event.timestamp)?.getTime() || 0) : 0; if (!signalTime || !eventTime || Number.isNaN(signalTime) || Number.isNaN(eventTime)) return true; return Math.abs(eventTime - signalTime) <= 12 * 3600 * 1000; }); - return candidates.sort((a, b) => new Date(b.timestamp || 0) - new Date(a.timestamp || 0))[0] || null; + return candidates.sort((a, b) => { + const bt = parseConsoleDate(b.timestamp)?.getTime() || 0; + const at = parseConsoleDate(a.timestamp)?.getTime() || 0; + return bt - at; + })[0] || null; } function findMatchingPosition(signal, platform, positions) { @@ -3948,6 +4104,10 @@ const result = await response.json(); if (!response.ok || !result.success) { + if (response.status === 401) { + consoleAuthenticated = false; + showConsoleAuthOverlay('访问会话已失效,请重新输入密码'); + } throw new Error(result.detail || result.message || '恢复失败'); } @@ -3978,6 +4138,10 @@ const result = await response.json(); if (!response.ok || !result.success) { + if (response.status === 401) { + consoleAuthenticated = false; + showConsoleAuthOverlay('访问会话已失效,请重新输入密码'); + } throw new Error(result.detail || result.message || '更新失败'); } @@ -3992,8 +4156,17 @@ async function loadConsole() { try { + if (!consoleAuthenticated) { + const ok = await ensureConsoleSession(); + if (!ok) return; + } setFeedback(''); const response = await fetch('/api/system/console'); + if (response.status === 401) { + consoleAuthenticated = false; + showConsoleAuthOverlay('访问会话已失效,请重新输入密码'); + return; + } const result = await response.json(); if (result.status !== 'success') { @@ -4045,12 +4218,18 @@ applyAutoRefreshState(); }); document.getElementById('toggleSensitiveBtn').addEventListener('click', toggleSensitiveData); + document.getElementById('consoleLoginBtn').addEventListener('click', loginConsole); + document.getElementById('consolePasswordInput').addEventListener('keydown', (event) => { + if (event.key === 'Enter') loginConsole(); + }); loadSensitivePreference(); initTabs(); initHubNavigation(); applyAutoRefreshState(); - loadConsole(); + ensureConsoleSession().then((ok) => { + if (ok) loadConsole(); + });