-
Notifications
You must be signed in to change notification settings - Fork 54.3k
fix: 收口 GeminiAnalyzer 主分析链路的 LLM prompt/response 日志… (#877) #897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 41 commits
23569bd
fb553af
6c59f4f
9b74df8
fecdd2d
fab94c2
976148d
7495cb6
f1ca855
2d38d6d
1613061
742fc87
dd02994
b159196
a36ae29
489687a
ac79a85
8a94bcd
4ef258c
8d13b5a
1b6e383
b363ab3
9cbae26
96e07a2
f2cdacb
4d54b50
e0f93af
35cb9c9
d4bed2f
cdb28bc
122ba4f
be82139
2cd3a83
6913c53
99454c8
bccbc0a
a9b8925
285814e
4f1617d
008b8f8
b98028c
16c9bab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/). | |
| - [测试] 🧪 **补充前端变更验证命令** — 对应前端资源变更同步执行 `cd apps/dsa-web && npm ci && npm run lint && npm run build`,作为版本信息展示与 Docker 重建生效验证的最小验证闭环记录。 | ||
| - [修复] 内置定时调度器现在会在运行中感知 WebUI 保存后的 `SCHEDULE_TIME` 变化,并在下一轮检查时重绑 daily job,避免 `python main.py --serve --schedule` 仍固定按启动时的 `18:00` 触发;`.env.example` 也同步删除了重复的定时任务配置示例。 | ||
| - [修复] 🪟 **Windows Release 渠道编辑器保留 MiniMax 模型前缀** — 渠道模式下填写 `minimax/<模型名>` 时,后端归一化与 Web 设置页运行时模型列表都会保留该值原样,不再误改写成 `openai/minimax/<模型名>`,从而恢复 MiniMax 模型在 Win 客户端里的保存、选择与使用。 | ||
| ### 修复 | ||
|
|
||
| - 🔐 **LLM prompt/response 日志默认不再输出正文预览**(fixes #877)— `GeminiAnalyzer` 不再在常规 INFO 日志中输出 prompt 或模型回复正文;仅在显式调试模式下输出单行、脱敏且截断后的受控预览,降低提示词、回复内容及常见凭据片段落入日志采集与共享链路的暴露风险,并补充对应回归测试。 | ||
| - [修复] 🔐 **LLM prompt/response 日志默认不再输出正文预览**(fixes #877)— `GeminiAnalyzer` 不再在常规 INFO 日志中输出 prompt 或模型回复正文;仅在显式调试模式下输出单行、脱敏且截断后的受控预览,降低提示词、回复内容及常见凭据片段落入日志采集与共享链路的暴露风险,并补充对应回归测试。 | ||
|
|
||
| ## [3.11.0] - 2026-03-27 | ||
|
|
||
|
|
@@ -50,6 +54,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/). | |
| - 🌗 **Web 首屏默认主题预设为深色** — `apps/dsa-web/index.html` 现在会在 React 挂载前读取本地保存的主题偏好;若没有已保存值,则立即给 `<html>` 预设 `dark` 并同步 `color-scheme`,避免首页和登录页首屏先闪出浅色主题。 | ||
| - 🔐 **登录页独立主题层收口** — 登录页输入框、标签、切换按钮和按钮文案现在使用独立的 `--login-*` 视觉 token,不再继承全局浅/深主题文字色;即使浏览器缓存了浅色主题,登录页仍保持稳定的深色视觉与青色密码输入表现,避免密码圆点和文案落成黑色。 | ||
| - 🖥️ **首页港股代码输入修复** — Web 首页分析输入框现在可正确接受港股代码与自动完成选中的港股项,补齐 `00700.HK` / `HK00700` 等格式识别,避免提交时误报“请输入有效的股票代码或股票名称”。 | ||
| - 🔐 **LLM prompt/response 日志默认不再输出正文预览**(fixes #877)— `GeminiAnalyzer` 不再在常规 INFO 日志中输出 prompt 或模型回复正文;仅在显式调试模式下输出单行、脱敏且截断后的受控预览,降低提示词、回复内容及常见凭据片段落入日志采集与共享链路的暴露风险,并补充对应回归测试。 | ||
| - 🔒 **认证限流 X-Forwarded-For 取值修复(CWE-345)**(#841 / #842)— `get_client_ip()` 从取 `X-Forwarded-For` 最左值改为最右值,防止攻击者通过伪造首部旋转限流桶绕过暴力破解保护;仅影响 `TRUST_X_FORWARDED_FOR=true` 且单层可信反向代理的部署场景,多级代理环境需按部署文档评估配置。 | ||
| - 📦 **恢复 LiteLLM 官方 PyPI 安装并锁定安全上限** — `requirements.txt` 重新使用 `pip install litellm` 的官方 PyPI 安装路径,并在保留历史最低要求 `>=1.80.10` 的同时增加 `<1.82.7` 的安全上限,避免误装已被移除的 `1.82.7` / `1.82.8` 风险版本;Windows 桌面打包脚本也同步回退到标准 `pip install -r requirements.txt` 链路,减少特殊下载分支带来的维护成本。 | ||
| - 📨 **Telegram Markdown 解析失败回退纯文本**(fixes #850)— `src/notification_sender/telegram_sender.py` 现在会在 Telegram 返回 `HTTP 400` 且包含 `can't parse entities` / Markdown 解析错误时,自动去掉 `parse_mode` 后重试纯文本发送,避免 `*ST` 等正文内容直接导致整条通知失败。 | ||
| - 🔢 **A 股同码实时行情保留交易所提示**(fixes #852)— `DataFetcherManager` 与 `TushareFetcher` 现在会保留 `SZ000001` / `000001.SZ` 这类显式沪深提示,旧版 Tushare 实时行情降级分支不再把深市 `000001` 误判成 `sh000001` 上证指数。 | ||
| - 🎯 **多 Agent 次优买点不再盲目复制理想买点**(fixes #851)— 当多智能体结果缺少独立 `secondary_buy` 时,仪表盘现在优先展示 `N/A` 而不是把 fallback 值硬拷贝成与 `ideal_buy` 完全相同,减少误导性的双买点展示。 | ||
| - 🧩 **Tushare 初始化不再强依赖本地 SDK 包** — `TushareFetcher` 现在直接使用内置 HTTP client 访问 Tushare Pro,不再在启动阶段先 `import tushare` 才能初始化;修复了 Docker、桌面打包或环境重建后因缺少 `tushare` 包而提前报 `No module named 'tushare'` 的问题,并补充对应回归测试。 | ||
| - ⚙️ **`daily_analysis` 工作流补齐 `DEEPSEEK_API_KEY` 映射** — GitHub Actions 每日分析工作流现在会正确透传 `DEEPSEEK_API_KEY`,避免云端任务配置了密钥却在运行时拿不到对应环境变量。 | ||
| - 🖥️ **历史列表过长股票名称截断与悬停展示**(fixes #815)— 历史列表中过长的股票名称, 现在会按字符类型自动截断(英文15/中文8/混合10字符),默认显示截断结果,悬停时展示完整名称;解决 1920x1080 分辨率下股票名称与右侧状态标签文字重叠的问题。新增 `stockName.ts` 工具函数并补充对应测试。 | ||
|
|
||
| - [修复] 🔐 **LLM prompt/response 日志默认不再输出正文预览**(fixes #877)— `GeminiAnalyzer` 不再在常规 INFO 日志中输出 prompt 或模型回复正文;仅在显式调试模式下输出单行、脱敏且截断后的受控预览,降低提示词、回复内容及常见凭据片段落入日志采集与共享链路的暴露风险,并补充对应回归测试。 | ||
|
|
||
| ## [3.11.0] - 2026-03-27 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This commit adds a second Useful? React with 👍 / 👎. |
||
|
|
||
| ### 发布亮点 | ||
|
|
||
| - 🎨 **Web 工作台完成一轮 UI 统一与双主题升级** — 首页、问股、回测、持仓和设置页进一步收口到统一设计 token、输入表面和状态表达;新增完整浅色主题,并支持浅色 / 深色一键切换与持久化保存。 | ||
| - 🤖 **Bot / Agent 能力重新补回主分支** — 恢复 `/history`、`/strategies`、`/research` 等命令,`/ask` 继续支持多股对比与组合视角;Deep Research、事件监控与 schedule 轮询链路重新接回主线能力。 | ||
| - 🔒 **安全性与运行稳态同步补强** — 修复 `X-Forwarded-For` 限流绕过风险,恢复 LiteLLM 官方 PyPI 安装路径,Tushare 初始化不再依赖本地 SDK,降低 Docker、桌面打包和环境重建时的脆弱点。 | ||
| - 🖥️ **日常使用细节继续打磨** — 修复首页港股自动补全提交、登录页首屏主题闪烁、历史长股票名重叠,以及 Telegram Markdown 解析失败时整条通知发送中断等问题。 | ||
|
|
||
| ### 新功能 | ||
|
|
||
| - 🎨 **全新浅色主题与双主题切换上线** — Web 工作台新增完整浅色主题,并支持在侧边栏中一键切换浅色 / 深色模式;主题选择会持久化保存,刷新页面后仍保持当前偏好。此次升级不是局部配色微调,而是对卡片层级、边界对比、输入表面、状态提示和页面背景做了一整套 light theme 重绘。 | ||
| - 🤖 **补回主分支缺失的 Agent / Bot 能力** — `#648` / `#649` 已重新补回 `main`:Bot 恢复 `/history`、`/strategies`、`/research`,`/ask` 保留多股对比与组合视角;Deep Research 与 Event Monitor 的配置重新在 Web 设置页可见并可编辑,schedule 模式也重新接入事件告警轮询。 | ||
|
|
||
| ### 改进 | ||
|
|
||
| - 🖥️ **核心页面统一到同一套工作台视觉语言** — `Home / Chat / Backtest / Portfolio / Settings` 进一步收口到共享设计 token、`input-surface` 输入体系、空态/错误态表达和抽屉遮罩语义,减少页面之间的视觉割裂与局部私有样式漂移。 | ||
| - 💬 **问股交互可达性与反馈增强** — 问股页补强了会话导出、通知发送、消息复制、历史删除与追问上下文提示;AI 回复操作不再过度依赖 hover,触屏设备和小屏场景下也能直接触达关键按钮。 | ||
| - 📊 **回测与持仓页表面和状态表达继续标准化** — 回测页筛选控件、布尔状态、结果表格与汇总卡片统一到共享输入/状态原语;持仓页的导入反馈、汇率刷新提示、空态与警示信息进一步归口到共享组件,减少页面级重复实现。 | ||
| - 🧭 **导航与页面壳层协同优化** — 侧边栏主题切换、问股完成角标、移动端抽屉遮罩和主内容滚动契约进一步统一,首页、问股和回测在桌面端与移动端的切页体验更稳定。 | ||
|
|
||
| ### 测试 | ||
|
|
||
| - 🧪 **UI 治理与关键路径回归补强** — 补充 `SidebarNav`、`ChatPage`、`BacktestPage` 等组件测试,并新增 UI governance 守卫,持续防止交互元素重新引入原生 `title` 属性或旧 `input-terminal` 样式回流。同步更新 smoke / markdown drawer 相关验证,覆盖主题升级后的关键主链路。 | ||
|
|
||
| ### 修复 | ||
|
|
||
| - 🌗 **Web 首屏默认主题预设为深色** — `apps/dsa-web/index.html` 现在会在 React 挂载前读取本地保存的主题偏好;若没有已保存值,则立即给 `<html>` 预设 `dark` 并同步 `color-scheme`,避免首页和登录页首屏先闪出浅色主题。 | ||
| - 🔒 **登录页独立主题层收口** — 登录页输入框、标签、切换按钮和按钮文案现在使用独立的 `--login-*` 视觉 token,不再继承全局浅/深主题文字色;即使浏览器缓存了浅色主题,登录页仍保持稳定的深色视觉与青色密码输入表现,避免密码圆点和文案落成黑色。 | ||
| - 🖥️ **首页港股代码输入修复** — Web 首页分析输入框现在可正确接受港股代码与自动完成选中的港股项,补齐 `00700.HK` / `HK00700` 等格式识别,避免提交时误报“请输入有效的股票代码或股票名称”。 | ||
| - 🔒 **认证限流 X-Forwarded-For 取值修复(CWE-345)**(#841 / #842)— `get_client_ip()` 从取 `X-Forwarded-For` 最左值改为最右值,防止攻击者通过伪造首部旋转限流桶绕过暴力破解保护;仅影响 `TRUST_X_FORWARDED_FOR=true` 且单层可信反向代理的部署场景,多级代理环境需按部署文档评估配置。 | ||
| - 📦 **恢复 LiteLLM 官方 PyPI 安装并锁定安全上限** — `requirements.txt` 重新使用 `pip install litellm` 的官方 PyPI 安装路径,并在保留历史最低要求 `>=1.80.10` 的同时增加 `<1.82.7` 的安全上限,避免误装已被移除的 `1.82.7` / `1.82.8` 风险版本;Windows 桌面打包脚本也同步回退到标准 `pip install -r requirements.txt` 链路,减少特殊下载分支带来的维护成本。 | ||
| - 📨 **Telegram Markdown 解析失败回退纯文本**(fixes #850)— `src/notification_sender/telegram_sender.py` 现在会在 Telegram 返回 `HTTP 400` 且包含 `can't parse entities` / Markdown 解析错误时,自动去掉 `parse_mode` 后重试纯文本发送,避免 `*ST` 等正文内容直接导致整条通知失败。 | ||
|
|
@@ -62,7 +108,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/). | |
| ### 文档 | ||
|
|
||
| - 🧾 **README 捐赠入口更新为小红书二维码** — README 及中英文说明中的赞助入口更新为小红书二维码素材,保持展示口径一致。 | ||
|
|
||
| ## [3.10.1] - 2026-03-24 | ||
|
|
||
| ### 新功能 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |
| import json | ||
| import logging | ||
| import math | ||
| import re | ||
| import time | ||
| from dataclasses import dataclass | ||
| from typing import Optional, Dict, Any, List, Tuple | ||
|
|
@@ -45,9 +46,144 @@ | |
| ) | ||
| from src.schemas.report_schema import AnalysisReportSchema | ||
| from src.market_context import get_market_role, get_market_guidelines | ||
| from src.logging_config import is_sensitive_log_preview_enabled | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| _LLM_PREVIEW_MAX_CHARS = 240 | ||
| _LLM_AUTHORIZATION_SAFE_SCHEMES = { | ||
| "aws4-hmac-sha256", | ||
| "basic", | ||
| "bearer", | ||
| "digest", | ||
| "dpop", | ||
| "hoba", | ||
| "mutual", | ||
| "negotiate", | ||
| "ntlm", | ||
| "pop", | ||
| "signature", | ||
| "token", | ||
| "vapid", | ||
| } | ||
| _LLM_SENSITIVE_ASSIGNMENT_VALUE_PATTERN = ( | ||
| r"[^\s]+(?:\s+(?!(?:[\w.-]+|\"[^\"]+\"|'[^']+')\s*[:=])\S+)*" | ||
| ) | ||
| _LLM_SENSITIVE_FIELD_NAME_PATTERN = ( | ||
| r"(?:api[_-]?keys?|tokens?|secrets?|passwords?|passwd|passphrase|credentials?|session[_-]?id" | ||
| r"|[\w.-]+(?:api[_-]?keys?|tokens?|secrets?|passwords?|passwd|passphrase|credentials?|session[_-]?id|keys?))" | ||
| ) | ||
|
Comment on lines
+72
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The sensitive-field matcher only covers exact names like Useful? React with 👍 / 👎.
Comment on lines
+72
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The sensitive-key matcher only recognizes Useful? React with 👍 / 👎.
Comment on lines
+72
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. * The sensitive-name regex only treats generic Useful? React with 👍 / 👎. |
||
|
|
||
|
|
||
| def _redact_authorization_preview_value(value: str) -> str: | ||
| parts = str(value or "").strip().split(None, 1) | ||
| if len(parts) == 2 and parts[0] and parts[0].lower() in _LLM_AUTHORIZATION_SAFE_SCHEMES: | ||
| return f"{parts[0]} [REDACTED]" | ||
|
Comment on lines
+79
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| return "[REDACTED]" | ||
|
|
||
|
|
||
| def _replace_quoted_authorization_preview(match) -> str: | ||
| return ( | ||
| f"{match.group(1)}{match.group(2)}{match.group(1)}" | ||
| f"{match.group(3)}{match.group(4)}" | ||
| f"{_redact_authorization_preview_value(match.group('value'))}" | ||
| f"{match.group(4)}" | ||
| ) | ||
|
|
||
|
|
||
| def _replace_authorization_preview(match) -> str: | ||
| return f"{match.group(1)}={_redact_authorization_preview_value(match.group('value'))}" | ||
|
|
||
|
|
||
| _LLM_RAW_LINE_SENSITIVE_PATTERNS = ( | ||
| ( | ||
| re.compile(r"(?im)\b(authorization)\s*[:=]\s*(?P<value>[^\n\r]*)"), | ||
| _replace_authorization_preview, | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| _LLM_SENSITIVE_PATTERNS = ( | ||
| ( | ||
| re.compile(r'(?i)(["\'])(authorization)\1\s*([:=])\s*(["\'])(?P<value>(?:\\.|(?!\4).)*)\4'), | ||
| _replace_quoted_authorization_preview, | ||
|
Comment on lines
+108
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The quoted-header sanitizer only matches the exact key Useful? React with 👍 / 👎. |
||
| ), | ||
| ( | ||
| re.compile(rf"(?i)\b(authorization)\s*[:=]\s*(?P<value>{_LLM_SENSITIVE_ASSIGNMENT_VALUE_PATTERN})"), | ||
| _replace_authorization_preview, | ||
|
Comment on lines
+112
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The unquoted Useful? React with 👍 / 👎.
Comment on lines
+112
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The unquoted authorization matcher only recognizes a standalone Useful? React with 👍 / 👎. |
||
| ), | ||
| ( | ||
| re.compile(r'(?i)(["\'])(set-cookie|cookie)\1\s*([:=])\s*(["\'])(?:\\.|(?!\4).)*\4'), | ||
| r"\1\2\1\3\4[REDACTED]\4", | ||
| ), | ||
| ( | ||
| re.compile( | ||
| r"(?i)\b(set-cookie|cookie)\s*[:=]\s*[^;=\s]+(?:\s*=\s*[^;\s]+)?(?:\s*;\s*[^;\n\r]+(?:\s*=\s*[^;\n\r]+)?)*" | ||
| ), | ||
| r"\1=[REDACTED]", | ||
| ), | ||
| ( | ||
| re.compile( | ||
| rf'(?i)(["\'])({_LLM_SENSITIVE_FIELD_NAME_PATTERN})\1\s*:\s*(["\'])(?:\\.|(?!\3).)*\3' | ||
| ), | ||
|
Comment on lines
+126
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The sanitizer only has a quoted-key pattern for double-quoted JSON, so Python-style dict payloads with single quotes are not redacted; for example Useful? React with 👍 / 👎. |
||
| r"\1\2\1:\3[REDACTED]\3", | ||
|
Comment on lines
+126
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new sanitizer only redacts quoted-key fields when the value is also quoted (see the Useful? React with 👍 / 👎. |
||
| ), | ||
| ( | ||
| re.compile( | ||
| rf'(?i)(["\'])({_LLM_SENSITIVE_FIELD_NAME_PATTERN})\1\s*:\s*(?!["\'])(-?[\w.+\-]+)' | ||
| ), | ||
|
Comment on lines
+132
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The quoted-key sanitizer only handles string values and primitive tokens ( Useful? React with 👍 / 👎. |
||
| r"\1\2\1:[REDACTED]", | ||
| ), | ||
| ( | ||
| re.compile( | ||
| rf"(?i)\b({_LLM_SENSITIVE_FIELD_NAME_PATTERN})\b\s*[:=]\s*(['\"])(?:\\.|(?!\2).)*\2" | ||
| ), | ||
|
Comment on lines
+138
to
+140
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new redaction patterns only match unquoted assignments (for example Useful? React with 👍 / 👎. |
||
| r"\1=\2[REDACTED]\2", | ||
| ), | ||
| ( | ||
| re.compile( | ||
| rf"(?i)\b({_LLM_SENSITIVE_FIELD_NAME_PATTERN})\b\s*[:=]\s*(?!['\"])({_LLM_SENSITIVE_ASSIGNMENT_VALUE_PATTERN})" | ||
| ), | ||
| r"\1=[REDACTED]", | ||
| ), | ||
| ( | ||
| re.compile(r"(?i)\b[\w.+-]+@[\w.-]+\.[a-z]{2,}\b"), | ||
| "[REDACTED_EMAIL]", | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def _should_log_llm_content_preview(config: Optional[Config] = None) -> bool: | ||
| """Allow LLM content preview only under explicit debug switches.""" | ||
| if is_sensitive_log_preview_enabled(): | ||
| return True | ||
|
Comment on lines
+158
to
+159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎.
Comment on lines
+158
to
+159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The early return on Useful? React with 👍 / 👎. |
||
| runtime_config = config if config is not None else get_config() | ||
| return bool( | ||
| getattr(runtime_config, "debug", False) | ||
| or str(getattr(runtime_config, "log_level", "INFO") or "INFO").upper() == "DEBUG" | ||
| ) | ||
|
|
||
|
|
||
| def _sanitize_llm_log_preview(content: str, max_chars: int = _LLM_PREVIEW_MAX_CHARS) -> str: | ||
| """Normalize, redact, and truncate preview text for logs.""" | ||
| sanitized_raw = str(content or "") | ||
| for pattern, replacement in _LLM_RAW_LINE_SENSITIVE_PATTERNS: | ||
| sanitized_raw = pattern.sub(replacement, sanitized_raw) | ||
| normalized = re.sub(r"\s+", " ", sanitized_raw).strip() | ||
| if not normalized: | ||
| return "[empty]" | ||
| sanitized = normalized | ||
| for pattern, replacement in _LLM_SENSITIVE_PATTERNS: | ||
| sanitized = pattern.sub(replacement, sanitized) | ||
| if len(sanitized) <= max_chars: | ||
| return sanitized | ||
| return sanitized[:max_chars].rstrip() + "..." | ||
|
|
||
|
|
||
| def _build_llm_log_preview(label: str, content: str, max_chars: int = _LLM_PREVIEW_MAX_CHARS) -> str: | ||
| """Build a safe one-line preview entry for debug logs.""" | ||
| return f"[{label}] len={len(content or '')} preview={_sanitize_llm_log_preview(content, max_chars=max_chars)}" | ||
|
|
||
|
|
||
| def check_content_integrity(result: "AnalysisResult") -> Tuple[bool, List[str]]: | ||
| """ | ||
|
|
@@ -1175,16 +1311,15 @@ def analyze( | |
| prompt = self._format_prompt(context, name, news_context, report_language=report_language) | ||
|
|
||
| config = self._get_runtime_config() | ||
| allow_content_preview = _should_log_llm_content_preview(config) | ||
| model_name = config.litellm_model or "unknown" | ||
| logger.info(f"========== AI 分析 {name}({code}) ==========") | ||
| logger.info(f"[LLM配置] 模型: {model_name}") | ||
| logger.info(f"[LLM配置] Prompt 长度: {len(prompt)} 字符") | ||
| logger.info(f"[LLM配置] 是否包含新闻: {'是' if news_context else '否'}") | ||
|
|
||
| # 记录完整 prompt 到日志(INFO级别记录摘要,DEBUG记录完整) | ||
| prompt_preview = prompt[:500] + "..." if len(prompt) > 500 else prompt | ||
| logger.info(f"[LLM Prompt 预览]\n{prompt_preview}") | ||
| logger.debug(f"=== 完整 Prompt ({len(prompt)}字符) ===\n{prompt}\n=== End Prompt ===") | ||
| if allow_content_preview: | ||
| logger.debug(_build_llm_log_preview("LLM Prompt 调试预览", prompt)) | ||
|
|
||
| # 设置生成配置 | ||
| generation_config = { | ||
|
|
@@ -1209,14 +1344,12 @@ def analyze( | |
| elapsed = time.time() - start_time | ||
|
|
||
| # 记录响应信息 | ||
| actual_model_name = model_used or model_name | ||
| logger.info( | ||
| f"[LLM返回] {model_name} 响应成功, 耗时 {elapsed:.2f}s, 响应长度 {len(response_text)} 字符" | ||
| ) | ||
| response_preview = response_text[:300] + "..." if len(response_text) > 300 else response_text | ||
| logger.info(f"[LLM返回 预览]\n{response_preview}") | ||
| logger.debug( | ||
| f"=== {model_name} 完整响应 ({len(response_text)}字符) ===\n{response_text}\n=== End Response ===" | ||
| f"[LLM返回] {actual_model_name} 响应成功, 耗时 {elapsed:.2f}s, 响应长度 {len(response_text)} 字符" | ||
| ) | ||
|
Comment on lines
1348
to
1350
|
||
| if allow_content_preview: | ||
| logger.debug(_build_llm_log_preview("LLM返回 调试预览", response_text)) | ||
|
|
||
| # 解析响应 | ||
| result = self._parse_response(response_text, code, name) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change violates the repository rule in
/workspace/daily_stock_analysis/AGENTS.mdthat[Unreleased]must stay in flat- [类型] 描述lines and must not add###category headers. Adding### 修复here (and duplicating the same #877 entry in two formats) reintroduces the merge-conflict/document-drift pattern that this rule is explicitly meant to prevent.Useful? React with 👍 / 👎.