diff --git a/.dockerignore b/.dockerignore index 0c96096a..e48c1fa4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,7 +26,6 @@ data/ *.sqlite # Build -frontend/dist/ static/ # Docs diff --git a/Dockerfile b/Dockerfile index db655e8e..5f8280e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 运行环境 ===== @@ -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 diff --git a/src/agents/tradingagents/auto_trigger.py b/src/agents/tradingagents/auto_trigger.py index 677f9653..646eda79 100644 --- a/src/agents/tradingagents/auto_trigger.py +++ b/src/agents/tradingagents/auto_trigger.py @@ -30,7 +30,7 @@ def _read_auto_trigger_config(db: Session) -> dict | None: - """从 AgentConfig.raw_config 读 auto_trigger 配置。 + """从 AgentConfig.config 读 auto_trigger 配置。 Returns: { @@ -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 @@ -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: @@ -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 # 没设上限 = 不限制 diff --git a/src/core/ai_client.py b/src/core/ai_client.py index 470fd252..f6183205 100644 --- a/src/core/ai_client.py +++ b/src/core/ai_client.py @@ -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 客户端""" diff --git a/src/core/doctor.py b/src/core/doctor.py deleted file mode 100644 index a90cbc3c..00000000 --- a/src/core/doctor.py +++ /dev/null @@ -1,56 +0,0 @@ -"""命令行系统自检:`python -m src.core.doctor` 或 `make doctor`。 - -终端跑一遍 系统基础项(DB/磁盘/调度)+ 数据源/AI/通知,打印结果与中文修复建议。 -CLI 进程内无运行中的调度器 → 调度项会优雅跳过(显示说明,不误报)。 -退出码:有异常项返回 1,全通返回 0(便于 CI/脚本判断)。 -""" - -from __future__ import annotations - -import asyncio -import sys - -from src.core.selfcheck import run_selfcheck - -_ICON = {"ok": "✅", "slow": "⚠️", "fail": "❌"} -_CAT = {"system": "系统", "datasource": "数据源", "ai": "AI模型", "notify": "通知渠道"} -_ORDER = ["system", "datasource", "ai", "notify"] - - -def _print_report(res: dict) -> None: - s = res["summary"] - print("\n===== PanWatch 系统自检 =====") - print(f"共 {s['total']} · ✅通 {s['ok']} · ⚠️慢 {s['slow']} · ❌断 {s['fail']}\n") - items = res.get("items", []) - for cat in _ORDER: - cat_items = [i for i in items if i["category"] == cat] - if not cat_items: - continue - print(f"【{_CAT.get(cat, cat)}】") - for i in cat_items: - icon = _ICON.get(i["status"], "?") - grp = f"{i['group']} / " if i.get("group") else "" - lat = f" {i['latency_ms']}ms" if i.get("latency_ms") else "" - print(f" {icon} {grp}{i['name']}{lat}") - if i["status"] == "fail": - if i.get("error"): - print(f" 错误: {i['error']}") - if i.get("hint"): - print(f" 建议: {i['hint']}") - elif i.get("note"): - print(f" {i['note']}") - print() - if s["fail"]: - print(f"⚠️ 发现 {s['fail']} 项异常,见上方建议。") - else: - print("✅ 全部正常。") - - -def main() -> int: - res = asyncio.run(run_selfcheck()) - _print_report(res) - return 1 if res["summary"]["fail"] else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/src/core/paper_trading_scheduler.py b/src/core/paper_trading_scheduler.py index 1fe31d1f..7ec577c5 100644 --- a/src/core/paper_trading_scheduler.py +++ b/src/core/paper_trading_scheduler.py @@ -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( diff --git a/src/core/price_alert_scheduler.py b/src/core/price_alert_scheduler.py index 814358c6..b43cdb35 100644 --- a/src/core/price_alert_scheduler.py +++ b/src/core/price_alert_scheduler.py @@ -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