perf: optimize system concurrency, I/O stability and fix WebSocket disconnects
This commit is contained in:
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import UTC, datetime
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Set
|
||||||
@@ -78,7 +78,7 @@ class ApprovalRecord:
|
|||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
self.status = ApprovalStatus.PENDING
|
self.status = ApprovalStatus.PENDING
|
||||||
self.findings = findings or []
|
self.findings = findings or []
|
||||||
self.created_at = datetime.now(UTC)
|
self.created_at = datetime.now(timezone.utc)
|
||||||
self.resolved_at: Optional[datetime] = None
|
self.resolved_at: Optional[datetime] = None
|
||||||
self.resolved_by: Optional[str] = None
|
self.resolved_by: Optional[str] = None
|
||||||
self.metadata: Dict[str, Any] = {}
|
self.metadata: Dict[str, Any] = {}
|
||||||
@@ -163,7 +163,7 @@ class ToolGuardStore:
|
|||||||
return record
|
return record
|
||||||
|
|
||||||
record.status = status
|
record.status = status
|
||||||
record.resolved_at = datetime.now(UTC)
|
record.resolved_at = datetime.now(timezone.utc)
|
||||||
record.resolved_by = resolved_by
|
record.resolved_by = resolved_by
|
||||||
if notify_request and record.pending_request:
|
if notify_request and record.pending_request:
|
||||||
if status == ApprovalStatus.APPROVED:
|
if status == ApprovalStatus.APPROVED:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Provides REST API endpoints for tool guard operations.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from datetime import UTC, datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -146,7 +146,7 @@ async def check_tool_call(
|
|||||||
|
|
||||||
if request.tool_name in SAFE_TOOLS:
|
if request.tool_name in SAFE_TOOLS:
|
||||||
record.status = ApprovalStatus.APPROVED
|
record.status = ApprovalStatus.APPROVED
|
||||||
record.resolved_at = datetime.now(UTC)
|
record.resolved_at = datetime.now(timezone.utc)
|
||||||
record.resolved_by = "system"
|
record.resolved_by = "system"
|
||||||
STORE.set_status(
|
STORE.set_status(
|
||||||
record.approval_id,
|
record.approval_id,
|
||||||
|
|||||||
@@ -81,7 +81,12 @@ async def proxy_ws(ws: WebSocket):
|
|||||||
await ws.accept()
|
await ws.accept()
|
||||||
upstream = None
|
upstream = None
|
||||||
try:
|
try:
|
||||||
upstream = await websockets.asyncio.client.connect(gateway_url)
|
upstream = await websockets.asyncio.client.connect(
|
||||||
|
gateway_url,
|
||||||
|
ping_interval=20,
|
||||||
|
ping_timeout=120,
|
||||||
|
max_size=10 * 1024 * 1024, # 10MB
|
||||||
|
)
|
||||||
|
|
||||||
async def client_to_upstream():
|
async def client_to_upstream():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ def create_app() -> FastAPI:
|
|||||||
add_cors_middleware(app)
|
add_cors_middleware(app)
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check() -> dict[str, str]:
|
def health_check() -> dict[str, str]:
|
||||||
return {"status": "healthy", "service": "news-service"}
|
return {"status": "healthy", "service": "news-service"}
|
||||||
|
|
||||||
@app.get("/api/enriched-news")
|
@app.get("/api/enriched-news")
|
||||||
async def api_get_enriched_news(
|
def api_get_enriched_news(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
start_date: str | None = Query(None),
|
start_date: str | None = Query(None),
|
||||||
end_date: str | None = Query(None),
|
end_date: str | None = Query(None),
|
||||||
@@ -49,7 +49,7 @@ def create_app() -> FastAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/api/news-for-date")
|
@app.get("/api/news-for-date")
|
||||||
async def api_get_news_for_date(
|
def api_get_news_for_date(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
date: str = Query(...),
|
date: str = Query(...),
|
||||||
limit: int = Query(20, ge=1, le=100),
|
limit: int = Query(20, ge=1, le=100),
|
||||||
@@ -64,7 +64,7 @@ def create_app() -> FastAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/api/news-timeline")
|
@app.get("/api/news-timeline")
|
||||||
async def api_get_news_timeline(
|
def api_get_news_timeline(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
start_date: str = Query(...),
|
start_date: str = Query(...),
|
||||||
end_date: str = Query(...),
|
end_date: str = Query(...),
|
||||||
@@ -79,7 +79,7 @@ def create_app() -> FastAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/api/categories")
|
@app.get("/api/categories")
|
||||||
async def api_get_categories(
|
def api_get_categories(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
start_date: str | None = Query(None),
|
start_date: str | None = Query(None),
|
||||||
end_date: str | None = Query(None),
|
end_date: str | None = Query(None),
|
||||||
@@ -96,7 +96,7 @@ def create_app() -> FastAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/api/similar-days")
|
@app.get("/api/similar-days")
|
||||||
async def api_get_similar_days(
|
def api_get_similar_days(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
date: str = Query(...),
|
date: str = Query(...),
|
||||||
n_similar: int = Query(5, ge=1, le=20),
|
n_similar: int = Query(5, ge=1, le=20),
|
||||||
@@ -111,7 +111,7 @@ def create_app() -> FastAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/api/stories/{ticker}")
|
@app.get("/api/stories/{ticker}")
|
||||||
async def api_get_story(
|
def api_get_story(
|
||||||
ticker: str,
|
ticker: str,
|
||||||
as_of_date: str = Query(...),
|
as_of_date: str = Query(...),
|
||||||
store: MarketStore = Depends(get_market_store),
|
store: MarketStore = Depends(get_market_store),
|
||||||
@@ -124,7 +124,7 @@ def create_app() -> FastAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/api/range-explain")
|
@app.get("/api/range-explain")
|
||||||
async def api_get_range_explain(
|
def api_get_range_explain(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
start_date: str = Query(...),
|
start_date: str = Query(...),
|
||||||
end_date: str = Query(...),
|
end_date: str = Query(...),
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ def create_app() -> FastAPI:
|
|||||||
add_cors_middleware(app)
|
add_cors_middleware(app)
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check() -> dict[str, str]:
|
def health_check() -> dict[str, str]:
|
||||||
"""Health check endpoint."""
|
"""Health check endpoint."""
|
||||||
return {"status": "healthy", "service": "trading-service"}
|
return {"status": "healthy", "service": "trading-service"}
|
||||||
|
|
||||||
@app.get("/api/prices", response_model=PriceResponse)
|
@app.get("/api/prices", response_model=PriceResponse)
|
||||||
async def api_get_prices(
|
def api_get_prices(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
start_date: str = Query(...),
|
start_date: str = Query(...),
|
||||||
end_date: str = Query(...),
|
end_date: str = Query(...),
|
||||||
@@ -47,7 +47,7 @@ def create_app() -> FastAPI:
|
|||||||
return PriceResponse(ticker=payload["ticker"], prices=payload["prices"])
|
return PriceResponse(ticker=payload["ticker"], prices=payload["prices"])
|
||||||
|
|
||||||
@app.get("/api/financials", response_model=FinancialMetricsResponse)
|
@app.get("/api/financials", response_model=FinancialMetricsResponse)
|
||||||
async def api_get_financials(
|
def api_get_financials(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
end_date: str = Query(...),
|
end_date: str = Query(...),
|
||||||
period: str = Query("ttm"),
|
period: str = Query("ttm"),
|
||||||
@@ -62,7 +62,7 @@ def create_app() -> FastAPI:
|
|||||||
return FinancialMetricsResponse(financial_metrics=payload["financial_metrics"])
|
return FinancialMetricsResponse(financial_metrics=payload["financial_metrics"])
|
||||||
|
|
||||||
@app.get("/api/news", response_model=CompanyNewsResponse)
|
@app.get("/api/news", response_model=CompanyNewsResponse)
|
||||||
async def api_get_news(
|
def api_get_news(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
end_date: str = Query(...),
|
end_date: str = Query(...),
|
||||||
start_date: str | None = Query(None),
|
start_date: str | None = Query(None),
|
||||||
@@ -77,7 +77,7 @@ def create_app() -> FastAPI:
|
|||||||
return CompanyNewsResponse(news=payload["news"])
|
return CompanyNewsResponse(news=payload["news"])
|
||||||
|
|
||||||
@app.get("/api/insider-trades", response_model=InsiderTradeResponse)
|
@app.get("/api/insider-trades", response_model=InsiderTradeResponse)
|
||||||
async def api_get_insider_trades(
|
def api_get_insider_trades(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
end_date: str = Query(...),
|
end_date: str = Query(...),
|
||||||
start_date: str | None = Query(None),
|
start_date: str | None = Query(None),
|
||||||
@@ -92,12 +92,12 @@ def create_app() -> FastAPI:
|
|||||||
return InsiderTradeResponse(insider_trades=payload["insider_trades"])
|
return InsiderTradeResponse(insider_trades=payload["insider_trades"])
|
||||||
|
|
||||||
@app.get("/api/market/status")
|
@app.get("/api/market/status")
|
||||||
async def api_get_market_status() -> dict[str, Any]:
|
def api_get_market_status() -> dict[str, Any]:
|
||||||
"""Return current market status using the existing market service logic."""
|
"""Return current market status using the existing market service logic."""
|
||||||
return trading_domain.get_market_status_payload()
|
return trading_domain.get_market_status_payload()
|
||||||
|
|
||||||
@app.get("/api/market-cap")
|
@app.get("/api/market-cap")
|
||||||
async def api_get_market_cap(
|
def api_get_market_cap(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
end_date: str = Query(...),
|
end_date: str = Query(...),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -108,7 +108,7 @@ def create_app() -> FastAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/api/line-items", response_model=LineItemResponse)
|
@app.get("/api/line-items", response_model=LineItemResponse)
|
||||||
async def api_get_line_items(
|
def api_get_line_items(
|
||||||
ticker: str = Query(..., min_length=1),
|
ticker: str = Query(..., min_length=1),
|
||||||
line_items: list[str] = Query(...),
|
line_items: list[str] = Query(...),
|
||||||
end_date: str = Query(...),
|
end_date: str = Query(...),
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class TradingPipeline:
|
|||||||
self._team_controller = DynamicTeamController(
|
self._team_controller = DynamicTeamController(
|
||||||
create_callback=self._create_runtime_analyst,
|
create_callback=self._create_runtime_analyst,
|
||||||
remove_callback=self._remove_runtime_analyst,
|
remove_callback=self._remove_runtime_analyst,
|
||||||
get_analysts_callback=self._all_analysts,
|
get_analysts_callback=lambda: self._all_analysts() + [self.risk_manager, self.pm],
|
||||||
)
|
)
|
||||||
set_controller(self._team_controller)
|
set_controller(self._team_controller)
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ class StateSync:
|
|||||||
# Persist to feed_history
|
# Persist to feed_history
|
||||||
if persist:
|
if persist:
|
||||||
self.storage.add_feed_message(self._state, event)
|
self.storage.add_feed_message(self._state, event)
|
||||||
|
# Make persistence non-blocking to keep event loop snappy
|
||||||
|
if asyncio.get_event_loop().is_running():
|
||||||
|
asyncio.create_task(asyncio.to_thread(self.save_state))
|
||||||
|
else:
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
# Broadcast to frontend
|
# Broadcast to frontend
|
||||||
|
|||||||
@@ -190,8 +190,9 @@ class MarketStore:
|
|||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
sector: str | None = None,
|
sector: str | None = None,
|
||||||
is_active: bool = True,
|
is_active: bool = True,
|
||||||
) -> None:
|
) -> int:
|
||||||
timestamp = _utc_timestamp()
|
timestamp = _utc_timestamp()
|
||||||
|
count = 0
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -206,6 +207,8 @@ class MarketStore:
|
|||||||
""",
|
""",
|
||||||
(symbol, name, sector, 1 if is_active else 0, timestamp, timestamp),
|
(symbol, name, sector, 1 if is_active else 0, timestamp, timestamp),
|
||||||
)
|
)
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
def update_fetch_watermark(
|
def update_fetch_watermark(
|
||||||
self,
|
self,
|
||||||
@@ -213,8 +216,9 @@ class MarketStore:
|
|||||||
symbol: str,
|
symbol: str,
|
||||||
price_date: str | None = None,
|
price_date: str | None = None,
|
||||||
news_date: str | None = None,
|
news_date: str | None = None,
|
||||||
) -> None:
|
) -> int:
|
||||||
timestamp = _utc_timestamp()
|
timestamp = _utc_timestamp()
|
||||||
|
count = 0
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -227,6 +231,8 @@ class MarketStore:
|
|||||||
""",
|
""",
|
||||||
(symbol, timestamp, timestamp, price_date, news_date),
|
(symbol, timestamp, timestamp, price_date, news_date),
|
||||||
)
|
)
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
def get_ticker_watermarks(self, symbol: str) -> dict[str, Any]:
|
def get_ticker_watermarks(self, symbol: str) -> dict[str, Any]:
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
@@ -263,6 +269,8 @@ class MarketStore:
|
|||||||
count = 0
|
count = 0
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
for row in rows:
|
for row in rows:
|
||||||
|
if not row.get("date"):
|
||||||
|
continue
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO ohlc
|
INSERT INTO ohlc
|
||||||
@@ -341,6 +349,7 @@ class MarketStore:
|
|||||||
timestamp,
|
timestamp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
count += 1
|
||||||
for ticker in tickers:
|
for ticker in tickers:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -349,7 +358,6 @@ class MarketStore:
|
|||||||
""",
|
""",
|
||||||
(news_id, str(ticker).strip().upper()),
|
(news_id, str(ticker).strip().upper()),
|
||||||
)
|
)
|
||||||
count += 1
|
|
||||||
return count
|
return count
|
||||||
|
|
||||||
def get_news_without_trade_date(self, symbol: str | None = None, *, limit: int = 5000) -> list[dict[str, Any]]:
|
def get_news_without_trade_date(self, symbol: str | None = None, *, limit: int = 5000) -> list[dict[str, Any]]:
|
||||||
@@ -928,8 +936,9 @@ class MarketStore:
|
|||||||
as_of_date: str,
|
as_of_date: str,
|
||||||
content: str,
|
content: str,
|
||||||
source: str = "local",
|
source: str = "local",
|
||||||
) -> None:
|
) -> int:
|
||||||
timestamp = _utc_timestamp()
|
timestamp = _utc_timestamp()
|
||||||
|
count = 0
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -943,6 +952,8 @@ class MarketStore:
|
|||||||
""",
|
""",
|
||||||
(symbol, as_of_date, content, source, timestamp, timestamp),
|
(symbol, as_of_date, content, source, timestamp, timestamp),
|
||||||
)
|
)
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
def delete_story_cache(
|
def delete_story_cache(
|
||||||
self,
|
self,
|
||||||
@@ -1002,8 +1013,9 @@ class MarketStore:
|
|||||||
target_date: str,
|
target_date: str,
|
||||||
payload: dict[str, Any],
|
payload: dict[str, Any],
|
||||||
source: str = "local",
|
source: str = "local",
|
||||||
) -> None:
|
) -> int:
|
||||||
timestamp = _utc_timestamp()
|
timestamp = _utc_timestamp()
|
||||||
|
count = 0
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -1017,6 +1029,8 @@ class MarketStore:
|
|||||||
""",
|
""",
|
||||||
(symbol, target_date, _json_dumps(payload), source, timestamp, timestamp),
|
(symbol, target_date, _json_dumps(payload), source, timestamp, timestamp),
|
||||||
)
|
)
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
def delete_similar_day_cache(
|
def delete_similar_day_cache(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -444,6 +444,16 @@ def create_model(
|
|||||||
"""
|
"""
|
||||||
provider = canonicalize_model_provider(provider)
|
provider = canonicalize_model_provider(provider)
|
||||||
|
|
||||||
|
# If provider is default OPENAI but model name looks like deepseek,
|
||||||
|
# check if we should switch to DASHSCOPE.
|
||||||
|
if provider == "OPENAI" and "deepseek" in model_name.lower() and os.getenv("DASHSCOPE_API_KEY"):
|
||||||
|
provider = "DASHSCOPE"
|
||||||
|
|
||||||
|
# Intelligent routing: if it's a DeepSeek model and we have DashScope credentials,
|
||||||
|
# prefer using DashScopeChatModel over OpenAIChatModel.
|
||||||
|
if provider == "DEEPSEEK" and os.getenv("DASHSCOPE_API_KEY"):
|
||||||
|
provider = "DASHSCOPE"
|
||||||
|
|
||||||
model_class = PROVIDER_MODEL_MAP.get(provider)
|
model_class = PROVIDER_MODEL_MAP.get(provider)
|
||||||
if model_class is None:
|
if model_class is None:
|
||||||
raise ValueError(f"Unsupported provider: {provider}")
|
raise ValueError(f"Unsupported provider: {provider}")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
@@ -11,12 +11,12 @@ class AgentRuntimeState:
|
|||||||
display_name: str | None = None
|
display_name: str | None = None
|
||||||
status: str = "idle"
|
status: str = "idle"
|
||||||
last_session: str | None = None
|
last_session: str | None = None
|
||||||
last_updated: datetime = field(default_factory=lambda: datetime.now(UTC))
|
last_updated: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
def update(self, status: str, session_key: str | None = None) -> None:
|
def update(self, status: str, session_key: str | None = None) -> None:
|
||||||
self.status = status
|
self.status = status
|
||||||
self.last_session = session_key
|
self.last_session = session_key
|
||||||
self.last_updated = datetime.now(UTC)
|
self.last_updated = datetime.now(timezone.utc)
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ class TradingRuntimeManager:
|
|||||||
|
|
||||||
def log_event(self, event: str, details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
def log_event(self, event: str, details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
entry = {
|
entry = {
|
||||||
"timestamp": datetime.now(UTC).isoformat(),
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
"event": event,
|
"event": event,
|
||||||
"details": details or {},
|
"details": details or {},
|
||||||
"session": self.current_session_key,
|
"session": self.current_session_key,
|
||||||
@@ -120,7 +120,7 @@ class TradingRuntimeManager:
|
|||||||
|
|
||||||
def register_pending_approval(self, approval_id: str, payload: Dict[str, Any]) -> None:
|
def register_pending_approval(self, approval_id: str, payload: Dict[str, Any]) -> None:
|
||||||
payload.setdefault("status", "pending")
|
payload.setdefault("status", "pending")
|
||||||
payload.setdefault("created_at", datetime.now(UTC).isoformat())
|
payload.setdefault("created_at", datetime.now(timezone.utc).isoformat())
|
||||||
self.pending_approvals[approval_id] = payload
|
self.pending_approvals[approval_id] = payload
|
||||||
self._persist_snapshot()
|
self._persist_snapshot()
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ class TradingRuntimeManager:
|
|||||||
if not entry:
|
if not entry:
|
||||||
return
|
return
|
||||||
entry["status"] = status
|
entry["status"] = status
|
||||||
entry["resolved_at"] = datetime.now(UTC).isoformat()
|
entry["resolved_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
entry["resolved_by"] = resolved_by
|
entry["resolved_by"] = resolved_by
|
||||||
self._persist_snapshot()
|
self._persist_snapshot()
|
||||||
|
|
||||||
|
|||||||
@@ -148,8 +148,9 @@ class Gateway:
|
|||||||
self.handle_client,
|
self.handle_client,
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
ping_interval=30,
|
ping_interval=20,
|
||||||
ping_timeout=60,
|
ping_timeout=120,
|
||||||
|
max_size=10 * 1024 * 1024, # 10MB
|
||||||
)
|
)
|
||||||
logger.info(f"WebSocket server ready: ws://{host}:{port}")
|
logger.info(f"WebSocket server ready: ws://{host}:{port}")
|
||||||
|
|
||||||
@@ -833,12 +834,18 @@ class Gateway:
|
|||||||
if not self.connected_clients:
|
if not self.connected_clients:
|
||||||
return
|
return
|
||||||
|
|
||||||
message_json = json.dumps(message, ensure_ascii=False, default=str)
|
# Offload potentially heavy JSON serialization to thread
|
||||||
|
message_json = await asyncio.to_thread(
|
||||||
|
json.dumps, message, ensure_ascii=False, default=str
|
||||||
|
)
|
||||||
|
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
|
# Filter only active clients to minimize unnecessary send attempts
|
||||||
|
# In websockets v13+, we must check state.name == 'OPEN'
|
||||||
|
active_clients = [c for c in self.connected_clients if c.state.name == 'OPEN']
|
||||||
tasks = [
|
tasks = [
|
||||||
self._send_to_client(client, message_json)
|
self._send_to_client(client, message_json)
|
||||||
for client in self.connected_clients.copy()
|
for client in active_clients
|
||||||
]
|
]
|
||||||
|
|
||||||
if tasks:
|
if tasks:
|
||||||
@@ -849,9 +856,14 @@ class Gateway:
|
|||||||
client: ServerConnection,
|
client: ServerConnection,
|
||||||
message: str,
|
message: str,
|
||||||
):
|
):
|
||||||
|
if client.state.name != 'OPEN':
|
||||||
|
async with self.lock:
|
||||||
|
self.connected_clients.discard(client)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await client.send(message)
|
await client.send(message)
|
||||||
except websockets.ConnectionClosed:
|
except (websockets.ConnectionClosed, Exception):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
self.connected_clients.discard(client)
|
self.connected_clients.discard(client)
|
||||||
|
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ async def finalize_cycle(gateway: Any, date: str) -> None:
|
|||||||
|
|
||||||
async def get_market_caps(gateway: Any, tickers: list[str], date: str) -> dict[str, float]:
|
async def get_market_caps(gateway: Any, tickers: list[str], date: str) -> dict[str, float]:
|
||||||
market_caps: dict[str, float] = {}
|
market_caps: dict[str, float] = {}
|
||||||
for ticker in tickers:
|
|
||||||
|
async def _get_one(ticker: str):
|
||||||
try:
|
try:
|
||||||
market_cap = None
|
market_cap = None
|
||||||
response = await gateway._call_trading_service(
|
response = await gateway._call_trading_service(
|
||||||
@@ -263,12 +264,21 @@ async def get_market_caps(gateway: Any, tickers: list[str], date: str) -> dict[s
|
|||||||
if response is not None:
|
if response is not None:
|
||||||
market_cap = response.get("market_cap")
|
market_cap = response.get("market_cap")
|
||||||
if market_cap is None:
|
if market_cap is None:
|
||||||
payload = trading_domain.get_market_cap_payload(ticker=ticker, end_date=date)
|
payload = await asyncio.to_thread(
|
||||||
|
trading_domain.get_market_cap_payload,
|
||||||
|
ticker=ticker,
|
||||||
|
end_date=date,
|
||||||
|
)
|
||||||
market_cap = payload.get("market_cap")
|
market_cap = payload.get("market_cap")
|
||||||
market_caps[ticker] = market_cap if market_cap else 1e9
|
return ticker, (market_cap if market_cap else 1e9)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to get market cap for %s, using default 1e9: %s", ticker, exc)
|
logger.warning("Failed to get market cap for %s, using default 1e9: %s", ticker, exc)
|
||||||
market_caps[ticker] = 1e9
|
return ticker, 1e9
|
||||||
|
|
||||||
|
tasks = [_get_one(ticker) for ticker in tickers]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
for ticker, mc in results:
|
||||||
|
market_caps[ticker] = mc
|
||||||
return market_caps
|
return market_caps
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -529,7 +529,8 @@ async def handle_get_stock_technical_indicators(gateway: Any, websocket: Any, da
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
end_date = datetime.now()
|
end_date = datetime.now()
|
||||||
start_date = end_date - timedelta(days=250)
|
# Reduced from 250 to 150 days to lower CPU/memory pressure while still supporting MA200 (approx 140 trading days)
|
||||||
|
start_date = end_date - timedelta(days=150)
|
||||||
|
|
||||||
prices = None
|
prices = None
|
||||||
response = await gateway._call_trading_service(
|
response = await gateway._call_trading_service(
|
||||||
@@ -544,7 +545,9 @@ async def handle_get_stock_technical_indicators(gateway: Any, websocket: Any, da
|
|||||||
prices = response.prices
|
prices = response.prices
|
||||||
|
|
||||||
if prices is None:
|
if prices is None:
|
||||||
payload = trading_domain.get_prices_payload(
|
# Offload domain logic to thread
|
||||||
|
payload = await asyncio.to_thread(
|
||||||
|
trading_domain.get_prices_payload,
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
start_date=start_date.strftime("%Y-%m-%d"),
|
start_date=start_date.strftime("%Y-%m-%d"),
|
||||||
end_date=end_date.strftime("%Y-%m-%d"),
|
end_date=end_date.strftime("%Y-%m-%d"),
|
||||||
@@ -560,20 +563,21 @@ async def handle_get_stock_technical_indicators(gateway: Any, websocket: Any, da
|
|||||||
}, ensure_ascii=False))
|
}, ensure_ascii=False))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _calc():
|
||||||
df = prices_to_df(prices)
|
df = prices_to_df(prices)
|
||||||
signal = gateway._technical_analyzer.analyze(ticker, df)
|
signal = gateway._technical_analyzer.analyze(ticker, df)
|
||||||
|
|
||||||
df_sorted = df.sort_values("time").reset_index(drop=True)
|
df_sorted = df.sort_values("time").reset_index(drop=True)
|
||||||
df_sorted["returns"] = df_sorted["close"].pct_change()
|
df_sorted["returns"] = df_sorted["close"].pct_change()
|
||||||
vol_10 = float(df_sorted["returns"].tail(10).std() * (252**0.5) * 100) if len(df_sorted) >= 10 else None
|
v10 = float(df_sorted["returns"].tail(10).std() * (252**0.5) * 100) if len(df_sorted) >= 10 else None
|
||||||
vol_20 = float(df_sorted["returns"].tail(20).std() * (252**0.5) * 100) if len(df_sorted) >= 20 else None
|
v20 = float(df_sorted["returns"].tail(20).std() * (252**0.5) * 100) if len(df_sorted) >= 20 else None
|
||||||
vol_60 = float(df_sorted["returns"].tail(60).std() * (252**0.5) * 100) if len(df_sorted) >= 60 else None
|
v60 = float(df_sorted["returns"].tail(60).std() * (252**0.5) * 100) if len(df_sorted) >= 60 else None
|
||||||
ma_distance = {}
|
|
||||||
for ma_key in ["ma5", "ma10", "ma20", "ma50", "ma200"]:
|
|
||||||
ma_value = getattr(signal, ma_key, None)
|
|
||||||
ma_distance[ma_key] = ((signal.current_price - ma_value) / ma_value) * 100 if ma_value and ma_value > 0 else None
|
|
||||||
|
|
||||||
indicators = {
|
ma_dist = {}
|
||||||
|
for ma_key in ["ma5", "ma10", "ma20", "ma50", "ma200"]:
|
||||||
|
ma_val = getattr(signal, ma_key, None)
|
||||||
|
ma_dist[ma_key] = ((signal.current_price - ma_val) / ma_val) * 100 if ma_val and ma_val > 0 else None
|
||||||
|
|
||||||
|
return {
|
||||||
"ticker": ticker,
|
"ticker": ticker,
|
||||||
"current_price": signal.current_price,
|
"current_price": signal.current_price,
|
||||||
"ma": {
|
"ma": {
|
||||||
@@ -582,7 +586,7 @@ async def handle_get_stock_technical_indicators(gateway: Any, websocket: Any, da
|
|||||||
"ma20": signal.ma20,
|
"ma20": signal.ma20,
|
||||||
"ma50": signal.ma50,
|
"ma50": signal.ma50,
|
||||||
"ma200": signal.ma200,
|
"ma200": signal.ma200,
|
||||||
"distance": ma_distance,
|
"distance": ma_dist,
|
||||||
},
|
},
|
||||||
"rsi": {
|
"rsi": {
|
||||||
"rsi14": signal.rsi14,
|
"rsi14": signal.rsi14,
|
||||||
@@ -599,9 +603,9 @@ async def handle_get_stock_technical_indicators(gateway: Any, websocket: Any, da
|
|||||||
"lower": signal.bollinger_lower,
|
"lower": signal.bollinger_lower,
|
||||||
},
|
},
|
||||||
"volatility": {
|
"volatility": {
|
||||||
"vol_10d": vol_10,
|
"vol_10d": v10,
|
||||||
"vol_20d": vol_20,
|
"vol_20d": v20,
|
||||||
"vol_60d": vol_60,
|
"vol_60d": v60,
|
||||||
"annualized": signal.annualized_volatility_pct,
|
"annualized": signal.annualized_volatility_pct,
|
||||||
"risk_level": signal.risk_level,
|
"risk_level": signal.risk_level,
|
||||||
},
|
},
|
||||||
@@ -609,11 +613,25 @@ async def handle_get_stock_technical_indicators(gateway: Any, websocket: Any, da
|
|||||||
"mean_reversion": signal.mean_reversion_signal,
|
"mean_reversion": signal.mean_reversion_signal,
|
||||||
}
|
}
|
||||||
|
|
||||||
await websocket.send(json.dumps({
|
# Use a semaphore to prevent too many concurrent CPU-intensive calculations
|
||||||
|
# which can block the event loop heartbeats.
|
||||||
|
if not hasattr(gateway, "_calc_sem"):
|
||||||
|
gateway._calc_sem = asyncio.Semaphore(3)
|
||||||
|
|
||||||
|
async with gateway._calc_sem:
|
||||||
|
indicators = await asyncio.to_thread(_calc)
|
||||||
|
|
||||||
|
# Also offload JSON serialization to thread to avoid blocking main loop
|
||||||
|
msg = await asyncio.to_thread(json.dumps, {
|
||||||
"type": "stock_technical_indicators_loaded",
|
"type": "stock_technical_indicators_loaded",
|
||||||
"ticker": ticker,
|
"ticker": ticker,
|
||||||
"indicators": indicators,
|
"indicators": indicators,
|
||||||
}, ensure_ascii=False, default=str))
|
}, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
if websocket.state.name == 'OPEN':
|
||||||
|
await websocket.send(msg)
|
||||||
|
else:
|
||||||
|
logger.warning("Websocket closed for %s, skipping indicator send", ticker)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("Error getting technical indicators for %s", ticker)
|
logger.exception("Error getting technical indicators for %s", ticker)
|
||||||
await websocket.send(json.dumps({
|
await websocket.send(json.dumps({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Handles reading/writing dashboard JSON files and portfolio state
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
@@ -950,11 +951,14 @@ class StorageService:
|
|||||||
|
|
||||||
def save_server_state(self, state: Dict[str, Any]):
|
def save_server_state(self, state: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
Save server state to file
|
Save server state to file with rate-limiting to avoid I/O storms.
|
||||||
|
|
||||||
Args:
|
|
||||||
state: Server state dictionary
|
|
||||||
"""
|
"""
|
||||||
|
now = time.time()
|
||||||
|
# Ensure at least 2 seconds between physical disk writes
|
||||||
|
if hasattr(self, "_last_save_time") and (now - self._last_save_time) < 2.0:
|
||||||
|
return
|
||||||
|
self._last_save_time = now
|
||||||
|
|
||||||
state_to_save = {
|
state_to_save = {
|
||||||
**state,
|
**state,
|
||||||
"last_saved": datetime.now().isoformat(),
|
"last_saved": datetime.now().isoformat(),
|
||||||
@@ -970,14 +974,17 @@ class StorageService:
|
|||||||
if "trades" in state_to_save:
|
if "trades" in state_to_save:
|
||||||
state_to_save["trades"] = state_to_save["trades"][:100]
|
state_to_save["trades"] = state_to_save["trades"][:100]
|
||||||
|
|
||||||
|
try:
|
||||||
with open(self.server_state_file, "w", encoding="utf-8") as f:
|
with open(self.server_state_file, "w", encoding="utf-8") as f:
|
||||||
|
# Removed indent=2 to minimize file size and serialization overhead
|
||||||
json.dump(
|
json.dump(
|
||||||
state_to_save,
|
state_to_save,
|
||||||
f,
|
f,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
indent=2,
|
|
||||||
default=str,
|
default=str,
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save server state: {e}")
|
||||||
|
|
||||||
logger.debug(f"Server state saved to: {self.server_state_file}")
|
logger.debug(f"Server state saved to: {self.server_state_file}")
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ Before using the production scripts, ensure the runtime environment has:
|
|||||||
- a usable Python environment
|
- a usable Python environment
|
||||||
- backend dependencies installed from the checked-in Python package metadata in `pyproject.toml`
|
- backend dependencies installed from the checked-in Python package metadata in `pyproject.toml`
|
||||||
- the package installed with `pip install -e .` or `uv pip install -e .`
|
- the package installed with `pip install -e .` or `uv pip install -e .`
|
||||||
- frontend dependencies installed with `npm ci`
|
- frontend dependencies installed with `npm install`
|
||||||
- repo dependencies installed
|
- repo dependencies installed
|
||||||
- required market/model API keys
|
- required market/model API keys
|
||||||
- any desired `TICKERS` override
|
- any desired `TICKERS` override
|
||||||
@@ -94,7 +94,7 @@ Recommended production install sequence:
|
|||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
pip install -e .
|
pip install -e .
|
||||||
cd frontend && npm ci && npm run build && cd ..
|
cd frontend && npm install && npm run build && cd ..
|
||||||
```
|
```
|
||||||
|
|
||||||
## Skill Sandbox Configuration
|
## Skill Sandbox Configuration
|
||||||
|
|||||||
76
deploy/install-production.sh
Normal file → Executable file
76
deploy/install-production.sh
Normal file → Executable file
@@ -166,7 +166,7 @@ KillMode=mixed
|
|||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
ProtectSystem=full
|
ProtectSystem=full
|
||||||
ProtectHome=true
|
ProtectHome=false
|
||||||
LimitNOFILE=65535
|
LimitNOFILE=65535
|
||||||
TasksMax=4096
|
TasksMax=4096
|
||||||
MemoryMax=${memory_max}
|
MemoryMax=${memory_max}
|
||||||
@@ -477,7 +477,17 @@ main() {
|
|||||||
|
|
||||||
SERVICE_GROUP="${SERVICE_GROUP:-$(ask_required 'systemd 运行用户组' "$(id -gn)")}"
|
SERVICE_GROUP="${SERVICE_GROUP:-$(ask_required 'systemd 运行用户组' "$(id -gn)")}"
|
||||||
|
|
||||||
DOMAIN="${DOMAIN:-$(ask_required '部署域名(可填写 IP 或 localhost)' 'localhost')}"
|
# 自动尝试获取公网 IP 作为默认域名值
|
||||||
|
local detected_ip=""
|
||||||
|
if [[ -z "${DOMAIN:-}" ]]; then
|
||||||
|
log "正在尝试自动获取公网 IP..."
|
||||||
|
detected_ip=$(curl -s --connect-timeout 5 https://ifconfig.me || curl -s --connect-timeout 5 https://api.ipify.org || echo "")
|
||||||
|
if [[ -n "${detected_ip}" ]]; then
|
||||||
|
log "自动检测到公网 IP: ${detected_ip}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOMAIN="${DOMAIN:-$(ask_required '部署域名(可填写 IP 或 localhost)' "${detected_ip:-localhost}")}"
|
||||||
validate_domain_like "${DOMAIN}" || warn "域名/IP 形态看起来不标准,请再次确认: ${DOMAIN}"
|
validate_domain_like "${DOMAIN}" || warn "域名/IP 形态看起来不标准,请再次确认: ${DOMAIN}"
|
||||||
|
|
||||||
ENV_FILE="${ENV_FILE:-$(ask_required '环境变量文件路径' '/etc/bigtime/bigtime.env')}"
|
ENV_FILE="${ENV_FILE:-$(ask_required '环境变量文件路径' '/etc/bigtime/bigtime.env')}"
|
||||||
@@ -486,6 +496,15 @@ main() {
|
|||||||
PYTHON_BIN="${PYTHON_BIN:-$(ask 'Python 可执行文件路径' "${APP_DIR}/.venv/bin/python")}"
|
PYTHON_BIN="${PYTHON_BIN:-$(ask 'Python 可执行文件路径' "${APP_DIR}/.venv/bin/python")}"
|
||||||
[[ -n "${PYTHON_BIN}" ]] || fail "Python 路径不能为空"
|
[[ -n "${PYTHON_BIN}" ]] || fail "Python 路径不能为空"
|
||||||
|
|
||||||
|
local SKIP_ENV_CONFIG=false
|
||||||
|
if [[ -f "${ENV_FILE}" ]]; then
|
||||||
|
echo ""
|
||||||
|
if confirm "检测到环境变量文件 ${ENV_FILE} 已存在,是否跳过详细参数配置并保留现有文件?" "Y"; then
|
||||||
|
SKIP_ENV_CONFIG=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ${SKIP_ENV_CONFIG}; then
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}运行参数${NC}"
|
echo -e "${CYAN}运行参数${NC}"
|
||||||
TICKERS="${TICKERS:-$(ask '默认股票池(逗号分隔)' 'AAPL,MSFT,GOOGL,AMZN,NVDA,META,TSLA,AMD,NFLX,AVGO,PLTR,COIN')}"
|
TICKERS="${TICKERS:-$(ask '默认股票池(逗号分隔)' 'AAPL,MSFT,GOOGL,AMZN,NVDA,META,TSLA,AMD,NFLX,AVGO,PLTR,COIN')}"
|
||||||
@@ -526,14 +545,17 @@ main() {
|
|||||||
echo " 域名: ${DOMAIN}"
|
echo " 域名: ${DOMAIN}"
|
||||||
echo " 环境文件: ${ENV_FILE}"
|
echo " 环境文件: ${ENV_FILE}"
|
||||||
echo " Python: ${PYTHON_BIN}"
|
echo " Python: ${PYTHON_BIN}"
|
||||||
echo " 数据源: ${FIN_DATA_SOURCE}"
|
echo " 数据源: ${FIN_DATA_SOURCE:-}"
|
||||||
echo " 模型: ${MODEL_NAME}"
|
echo " 模型: ${MODEL_NAME:-}"
|
||||||
echo " 沙盒模式: ${SKILL_SANDBOX_MODE}"
|
echo " 沙盒模式: ${SKILL_SANDBOX_MODE:-none}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if ! confirm "确认以上配置并继续写入系统文件?" "Y"; then
|
if ! confirm "确认以上配置并继续写入系统文件?" "Y"; then
|
||||||
fail "用户取消部署。"
|
fail "用户取消部署。"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}将使用现有的环境文件,跳过详细参数配置。${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ! -x "${PYTHON_BIN}" ]]; then
|
if [[ ! -x "${PYTHON_BIN}" ]]; then
|
||||||
warn "未找到 ${PYTHON_BIN},准备创建虚拟环境。"
|
warn "未找到 ${PYTHON_BIN},准备创建虚拟环境。"
|
||||||
@@ -546,10 +568,12 @@ main() {
|
|||||||
"${PYTHON_BIN}" -m pip install -e "${APP_DIR}"
|
"${PYTHON_BIN}" -m pip install -e "${APP_DIR}"
|
||||||
|
|
||||||
log "构建前端"
|
log "构建前端"
|
||||||
(cd "${APP_DIR}/frontend" && npm ci && npm run build)
|
(cd "${APP_DIR}/frontend" && npm install && npm run build)
|
||||||
|
|
||||||
|
if ! ${SKIP_ENV_CONFIG}; then
|
||||||
log "写入环境变量文件 ${ENV_FILE}"
|
log "写入环境变量文件 ${ENV_FILE}"
|
||||||
write_env_file
|
write_env_file
|
||||||
|
fi
|
||||||
|
|
||||||
if confirm "生成并安装 systemd unit?" "Y" "${AUTO_INSTALL_SYSTEMD}"; then
|
if confirm "生成并安装 systemd unit?" "Y" "${AUTO_INSTALL_SYSTEMD}"; then
|
||||||
render_systemd_unit "Agent Service" "backend.apps.agent_service:app" "8000" "1" "1024M" "/etc/systemd/system/bigtime-agent.service"
|
render_systemd_unit "Agent Service" "backend.apps.agent_service:app" "8000" "1" "1024M" "/etc/systemd/system/bigtime-agent.service"
|
||||||
@@ -568,20 +592,50 @@ main() {
|
|||||||
if confirm "生成并安装 nginx 配置?" "Y" "${AUTO_INSTALL_NGINX}"; then
|
if confirm "生成并安装 nginx 配置?" "Y" "${AUTO_INSTALL_NGINX}"; then
|
||||||
local use_tls="no"
|
local use_tls="no"
|
||||||
if confirm "使用 HTTPS/Let's Encrypt 证书路径?" "N" "${AUTO_USE_TLS}"; then
|
if confirm "使用 HTTPS/Let's Encrypt 证书路径?" "N" "${AUTO_USE_TLS}"; then
|
||||||
use_tls="yes"
|
|
||||||
SSL_CERT_PATH="${SSL_CERT_PATH:-$(ask_required 'SSL 证书 fullchain.pem 路径' "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem")}"
|
SSL_CERT_PATH="${SSL_CERT_PATH:-$(ask_required 'SSL 证书 fullchain.pem 路径' "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem")}"
|
||||||
SSL_KEY_PATH="${SSL_KEY_PATH:-$(ask_required 'SSL 私钥 privkey.pem 路径' "/etc/letsencrypt/live/${DOMAIN}/privkey.pem")}"
|
SSL_KEY_PATH="${SSL_KEY_PATH:-$(ask_required 'SSL 私钥 privkey.pem 路径' "/etc/letsencrypt/live/${DOMAIN}/privkey.pem")}"
|
||||||
[[ -f "${SSL_CERT_PATH}" ]] || warn "证书文件当前不存在: ${SSL_CERT_PATH}"
|
|
||||||
[[ -f "${SSL_KEY_PATH}" ]] || warn "私钥文件当前不存在: ${SSL_KEY_PATH}"
|
local ssl_err=0
|
||||||
|
[[ -f "${SSL_CERT_PATH}" ]] || { warn "SSL 证书文件不存在: ${SSL_CERT_PATH}"; ssl_err=1; }
|
||||||
|
[[ -f "${SSL_KEY_PATH}" ]] || { warn "SSL 私钥文件不存在: ${SSL_KEY_PATH}"; ssl_err=1; }
|
||||||
|
[[ -f "/etc/letsencrypt/options-ssl-nginx.conf" ]] || { warn "缺失 /etc/letsencrypt/options-ssl-nginx.conf,请检查 certbot 配置"; ssl_err=1; }
|
||||||
|
[[ -f "/etc/letsencrypt/ssl-dhparams.pem" ]] || { warn "缺失 /etc/letsencrypt/ssl-dhparams.pem,请检查 certbot 配置"; ssl_err=1; }
|
||||||
|
|
||||||
|
if [[ ${ssl_err} -eq 0 ]]; then
|
||||||
|
use_tls="yes"
|
||||||
|
else
|
||||||
|
warn "由于 SSL 关键文件缺失,将回退至 HTTP 模式,以确保 Nginx 能通过配置检查。"
|
||||||
|
use_tls="no"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
SSL_CERT_PATH=""
|
SSL_CERT_PATH=""
|
||||||
SSL_KEY_PATH=""
|
SSL_KEY_PATH=""
|
||||||
fi
|
fi
|
||||||
NGINX_TARGET="/etc/nginx/conf.d/bigtime.conf"
|
NGINX_TARGET="/etc/nginx/conf.d/bigtime.conf"
|
||||||
render_nginx_conf "${NGINX_TARGET}" "${use_tls}"
|
render_nginx_conf "${NGINX_TARGET}" "${use_tls}"
|
||||||
if confirm "立即执行 nginx -t 并 reload?" "Y" "${AUTO_RELOAD_NGINX}"; then
|
if confirm "立即执行 nginx -t 并生效配置?" "Y" "${AUTO_RELOAD_NGINX}"; then
|
||||||
sudo nginx -t
|
log "正在验证 Nginx 配置..."
|
||||||
|
if ! sudo nginx -t; then
|
||||||
|
fail "Nginx 配置检查失败!请根据上方报错信息调整。常见的错误包括:80/443 端口被占用,或 server_name 冲突。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if systemctl is-active --quiet nginx; then
|
||||||
|
log "Nginx 正在运行,执行 reload..."
|
||||||
sudo systemctl reload nginx
|
sudo systemctl reload nginx
|
||||||
|
else
|
||||||
|
log "Nginx 未运行,尝试启动..."
|
||||||
|
sudo systemctl enable --now nginx
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 关键修复:确保 nginx 用户对 /root 路径有 x 权限
|
||||||
|
if [[ "${APP_DIR}" == /root/* ]]; then
|
||||||
|
log "检测到应用部署在 /root 下,正在修复父目录访问权限..."
|
||||||
|
sudo chmod o+x /root 2>/dev/null || true
|
||||||
|
sudo chmod o+x "$(dirname "${APP_DIR}")" 2>/dev/null || true
|
||||||
|
sudo chmod -R o+rX "${APP_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Nginx 配置已生效。"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Recommended frontend mode:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm ci
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ sudo systemctl enable --now bigtime-runtime.service
|
|||||||
|
|
||||||
Recommended production frontend mode:
|
Recommended production frontend mode:
|
||||||
|
|
||||||
- build with `cd frontend && npm ci && npm run build`
|
- build with `cd frontend && npm install && npm run build`
|
||||||
- let `nginx` serve `frontend/dist` directly
|
- let `nginx` serve `frontend/dist` directly
|
||||||
|
|
||||||
The repository also contains `backend.apps.frontend_service`, but for
|
The repository also contains `backend.apps.frontend_service`, but for
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ class NewsServiceClient:
|
|||||||
self._client: httpx.AsyncClient | None = None
|
self._client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
async def __aenter__(self) -> "NewsServiceClient":
|
async def __aenter__(self) -> "NewsServiceClient":
|
||||||
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=self.base_url,
|
||||||
|
timeout=90.0,
|
||||||
|
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20)
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ class TradingServiceClient:
|
|||||||
self._client: httpx.AsyncClient | None = None
|
self._client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
async def __aenter__(self) -> "TradingServiceClient":
|
async def __aenter__(self) -> "TradingServiceClient":
|
||||||
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=self.base_url,
|
||||||
|
timeout=60.0,
|
||||||
|
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20)
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user