Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ data/
*.sqlite

# Build
frontend/dist/
static/

# Docs
Expand Down
24 changes: 5 additions & 19 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
# PanWatch Dockerfile
# 多阶段构建,减小最终镜像大小

# ===== Stage 1: 前端构建 =====
FROM node:20-alpine AS frontend-builder

WORKDIR /app/frontend

# 安装 pnpm
RUN npm install -g pnpm

# 复制依赖文件
COPY frontend/package.json frontend/pnpm-lock.yaml ./

# 安装依赖
RUN pnpm install --frozen-lockfile

# 复制源码并构建
COPY frontend/ ./
RUN pnpm build
# ===== Stage 1: 前端静态文件 =====
# 前端已在本机构建(frontend/dist/),直接复制到运行阶段
# 如需在 Docker 内重建,可回退至:使用 node:20-alpine 执行 pnpm install && pnpm build


# ===== Stage 2: Python 运行环境 =====
Expand Down Expand Up @@ -93,8 +79,8 @@ COPY prompts/ ./prompts/
# 写入版本号
RUN echo "${VERSION}" > VERSION

# 从前端构建阶段复制静态文件
COPY --from=frontend-builder /app/frontend/dist ./static/
# 复制本地已构建的前端静态文件
COPY frontend/dist ./static/

# 创建数据目录
RUN mkdir -p /app/data
Expand Down
8 changes: 4 additions & 4 deletions src/agents/tradingagents/auto_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@


def _read_auto_trigger_config(db: Session) -> dict | None:
"""从 AgentConfig.raw_config 读 auto_trigger 配置。
"""从 AgentConfig.config 读 auto_trigger 配置。

Returns:
{
Expand All @@ -42,7 +42,7 @@ def _read_auto_trigger_config(db: Session) -> dict | None:
agent = db.query(AgentConfig).filter(AgentConfig.name == "tradingagents").first()
if not agent:
return None
raw = agent.raw_config or {}
raw = agent.config or {}
auto = raw.get("auto_trigger") or {}
if not auto.get("enabled"):
return None
Expand All @@ -69,7 +69,7 @@ def _within_cooldown(db: Session, stock_symbol: str, cooldown_hours: int) -> boo


def _budget_allows(db: Session) -> bool:
"""检查月度预算是否还有余量。预算从 tradingagents 的 raw_config.monthly_budget_usd 读。"""
"""检查月度预算是否还有余量。预算从 tradingagents 的 config.monthly_budget_usd 读。"""
try:
from src.agents.tradingagents.cost_tracker import check_budget
except ImportError:
Expand All @@ -78,7 +78,7 @@ def _budget_allows(db: Session) -> bool:
agent = db.query(AgentConfig).filter(AgentConfig.name == "tradingagents").first()
if not agent:
return True
raw = agent.raw_config or {}
raw = agent.config or {}
budget = float(raw.get("monthly_budget_usd") or 0.0)
if budget <= 0:
return True # 没设上限 = 不限制
Expand Down
55 changes: 55 additions & 0 deletions src/core/ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,61 @@
logger = logging.getLogger(__name__)


class TokenLimitError(Exception):
"""Token 超出模型限制时抛出的错误"""

def __init__(self, prompt_tokens: int, max_tokens: int, context_limit: int, message: str = ""):
self.prompt_tokens = prompt_tokens
self.max_tokens = max_tokens
self.context_limit = context_limit
base_msg = (
f"输入内容过长(约 {prompt_tokens} tokens)+ 请求输出 {max_tokens} tokens "
f"超过模型限制 {context_limit} tokens"
)
if message:
base_msg += f",{message}"
super().__init__(base_msg)


def _estimate_tokens(text: str) -> int:
"""保守估算文本的 token 数量(中文/混合文本)"""
if not text:
return 0
# 中文/混合文本:约 1.2 个字符/tokens
# 纯英文:约 4 个字符/tokens
# 保守起见统一按 1.2 估算
return max(1, int(len(text) * 1.2))


def _clamp_max_tokens(
configured_max_tokens: int | None,
prompt_tokens: int,
context_limit: int,
safety_margin: int = 512,
minimum_output: int = 64,
) -> int:
"""根据 prompt 长度和上下文限制计算安全的 max_tokens 值

Args:
configured_max_tokens: 配置的输出 token 上限(None 表示不限制)
prompt_tokens: prompt 的估算 token 数
context_limit: 模型的上下文窗口大小
safety_margin: 安全余量,避免 tokenizer 差异导致的溢出
minimum_output: 最小保证的输出 token 数

Returns:
安全的 max_tokens 值
"""
if prompt_tokens >= context_limit - minimum_output - safety_margin:
raise TokenLimitError(prompt_tokens, 0, context_limit,
"请减少股票数量、新闻数量或历史K线范围后重试")

available = context_limit - prompt_tokens - safety_margin
if configured_max_tokens is not None:
return max(minimum_output, min(configured_max_tokens, available))
return available


class AIClient:
"""OpenAI 协议兼容的 AI 客户端"""

Expand Down
56 changes: 0 additions & 56 deletions src/core/doctor.py

This file was deleted.

1 change: 1 addition & 0 deletions src/core/paper_trading_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def start(self):
replace_existing=True,
coalesce=True,
max_instances=1,
misfire_grace_time=60,
)
# 盘前计划 - 每天 09:00
self.scheduler.add_job(
Expand Down
1 change: 1 addition & 0 deletions src/core/price_alert_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def start(self):
replace_existing=True,
coalesce=True,
max_instances=1,
misfire_grace_time=60,
)
self.scheduler.start()
from src.core.scheduler_registry import register
Expand Down