Skip to content
Closed
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
23569bd
fix(issue-877): [bug]-修复-info-日志默认输出-llm-prompt/response
ZhuLinsen Mar 27, 2026
fb553af
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
6c59f4f
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
9b74df8
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
fecdd2d
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
fab94c2
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
976148d
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
7495cb6
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
f1ca855
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
2d38d6d
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
1613061
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
742fc87
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
dd02994
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
b159196
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
a36ae29
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
489687a
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
ac79a85
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
8a94bcd
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
4ef258c
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
8d13b5a
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
1b6e383
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
b363ab3
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
9cbae26
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
96e07a2
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
f2cdacb
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
4d54b50
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
e0f93af
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
35cb9c9
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
d4bed2f
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
cdb28bc
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
122ba4f
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 28, 2026
be82139
fix: remove transient pr-body artifact from pr-897
Mar 29, 2026
2cd3a83
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 29, 2026
6913c53
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 29, 2026
99454c8
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 29, 2026
bccbc0a
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 29, 2026
a9b8925
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 29, 2026
285814e
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 30, 2026
4f1617d
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 30, 2026
008b8f8
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 30, 2026
b98028c
fix(review-feedback-897): address latest review comments
ZhuLinsen Mar 31, 2026
16c9bab
fix: normalize CHANGELOG [Unreleased] to flat format per AGENTS.md
ZhuLinsen Mar 31, 2026
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
47 changes: 46 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 或模型回复正文;仅在显式调试模式下输出单行、脱敏且截断后的受控预览,降低提示词、回复内容及常见凭据片段落入日志采集与共享链路的暴露风险,并补充对应回归测试。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep Unreleased changelog entries in flat bullet format

This change violates the repository rule in /workspace/daily_stock_analysis/AGENTS.md that [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 👍 / 👎.


## [3.11.0] - 2026-03-27

Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove duplicated 3.11.0 heading in changelog

This commit adds a second ## [3.11.0] - 2026-03-27 section, duplicating that release block. Our desktop release workflow extracts notes with awk from the first matching version header to the next ## [...] header (.github/workflows/desktop-release.yml line 180), so duplicate headings for the same version produce inconsistent/truncated release body content. Keep a single header per version and append the #877 note once.

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` 等正文内容直接导致整条通知失败。
Expand All @@ -62,7 +108,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
### 文档

- 🧾 **README 捐赠入口更新为小红书二维码** — README 及中英文说明中的赞助入口更新为小红书二维码素材,保持展示口径一致。

## [3.10.1] - 2026-03-24

### 新功能
Expand Down
153 changes: 143 additions & 10 deletions src/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact provider-prefixed API key names in previews

The sensitive-field matcher only covers exact names like api_key/x-api-key, so common credential fields such as OPENAI_API_KEY, GEMINI_API_KEY, and anthropic_api_key bypass sanitization and are logged in cleartext when debug preview logging is enabled. This is reproducible via _sanitize_llm_log_preview("OPENAI_API_KEY=sk-live-123"), which currently returns the raw secret, so the preview path still leaks real keys in a common format that this fix is supposed to protect.

Useful? React with 👍 / 👎.

Comment on lines +72 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact camelCase token/secret keys in log previews

The sensitive-key matcher only recognizes token/secret when they are standalone or prefixed with _/-, so common camelCase fields like accessToken and clientSecret are left unredacted in debug previews. In this commit’s new privacy path, _sanitize_llm_log_preview('{"accessToken":"abc123"}') and _sanitize_llm_log_preview('{"clientSecret":"abc123"}') both return raw secrets, which leaks credentials in the exact mode intended to emit only sanitized previews.

Useful? React with 👍 / 👎.

Comment on lines +72 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*P2 Badge Redact lowercase key field names in preview sanitizer

The sensitive-name regex only treats generic *key suffixes as sensitive when they are _key/-key or camelCase Key ((?-i:Keys?)), so fully lowercase variants like privatekey/clientsecretkey bypass masking. In debug-preview mode this leaves raw credentials in logs (e.g. _sanitize_llm_log_preview("privatekey=abc123") returns the secret), which undermines the new “sanitized preview” guarantee whenever payloads use lowercase key naming.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact full Authorization value when scheme is absent

_redact_authorization_preview_value always preserves the first whitespace-separated token whenever two tokens are present, assuming the first token is a safe auth scheme. For inputs like authorization=raw-secret-token session_id=abc123 (or quoted variants), this leaves raw-secret-token in the preview and only redacts the tail, so sensitive credentials still get logged in debug preview mode. This is a privacy regression in the sanitizer path that is intended to prevent secret leakage.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact quoted Proxy-Authorization header values

The quoted-header sanitizer only matches the exact key authorization, so serialized proxy auth headers like {"proxy_authorization":"Bearer raw-secret"} or {"proxy-authorization":"Bearer raw-secret"} bypass redaction and are emitted in clear text when debug previews are enabled. In this commit, _sanitize_llm_log_preview is the privacy guard for prompt/response previews, so this gap still leaks credentials in the new “sanitized preview” path whenever headers are logged as JSON/dict text.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact full unquoted Authorization header values

The unquoted authorization sanitizer currently reuses _LLM_SENSITIVE_ASSIGNMENT_VALUE_PATTERN, which stops at the first key=value token after the scheme, so parameterized headers leak credentials in debug previews. For example, _sanitize_llm_log_preview('Authorization: AWS4-HMAC-SHA256 Credential=AKIA..., Signature=deadbeef') leaves Credential=... and Signature=... visible, and the same happens with Digest-style headers (username=..., response=...). Since this path is intended to emit safe redacted previews, it should mask the entire header value, not just the first token.

Useful? React with 👍 / 👎.

Comment on lines +112 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact proxy_authorization assignments in preview sanitizer

The unquoted authorization matcher only recognizes a standalone authorization token (\b(authorization)), so underscore-normalized forms like proxy_authorization=Basic ... are not sanitized and can still leak proxy credentials in debug preview logs. This matters when headers are serialized with underscore keys (common in framework-normalized payloads or env-style dumps), because _sanitize_llm_log_preview is intended to be the privacy guard before emitting prompt/response previews.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact single-quoted credential fields in debug previews

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 _sanitize_llm_log_preview("{'api_key':'sk-live-123','password':'x'}") still returns raw secrets. In debug mode this leaks credentials into the new preview logs, undermining the intended “redacted preview” safety guarantee for common prompt/response payload formats.

Useful? React with 👍 / 👎.

r"\1\2\1:\3[REDACTED]\3",
Comment on lines +126 to +129

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact unquoted JSON values for sensitive quoted keys

The new sanitizer only redacts quoted-key fields when the value is also quoted (see the "key": "..." regex here), so payloads like {"session_id":12345}, {"token":false}, or {"password":null} are left unchanged in debug previews. The later unquoted-value pattern does not catch this because it expects an unquoted key (key=value/key:...), so quoted JSON keys with non-string values currently bypass redaction and can leak sensitive identifiers in the exact preview path this patch is intended to secure.

Useful? React with 👍 / 👎.

),
(
re.compile(
rf'(?i)(["\'])({_LLM_SENSITIVE_FIELD_NAME_PATTERN})\1\s*:\s*(?!["\'])(-?[\w.+\-]+)'
),
Comment on lines +132 to +134

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact object/array values for sensitive JSON keys

The quoted-key sanitizer only handles string values and primitive tokens (-?[\w.+\-]+), so sensitive keys whose values are JSON objects/arrays are left unchanged. In explicit debug preview mode this leaks raw data despite the new “sanitized preview” guarantee (for example, _sanitize_llm_log_preview('{"token":{"raw":"abc123"}}') keeps abc123). Please mask sensitive quoted keys regardless of whether their value is a string, primitive, array, or object.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact quoted JSON credential fields in debug previews

The new redaction patterns only match unquoted assignments (for example api_key=...), so common JSON forms such as "api_key":"sk-live-..." or "password":"..." are not sanitized before logging; _sanitize_llm_log_preview('{"api_key":"sk-live-123456"}') currently returns the raw secret. In explicit debug mode this still leaks credentials into log previews whenever prompt/response text includes JSON payloads, which defeats the intended “redacted preview” safety guarantee.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute preview gate from current debug settings

_should_log_llm_content_preview short-circuits to True whenever the process-global flag is set, so once setup_logging(..., debug=True) enables it, later config changes that disable debug mode no longer take effect. This is observable in the runtime-reload path (Config.reset_instance() in SystemConfigService.update) because logging setup is not rerun there: after switching DEBUG/LOG_LEVEL back to non-debug, analyzer calls still log prompt/response previews. That keeps sensitive previews enabled beyond the explicit debug window the fix is trying to enforce.

Useful? React with 👍 / 👎.

Comment on lines +158 to +159

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute preview gate from live config state

The early return on is_sensitive_log_preview_enabled() makes preview logging sticky for the process lifetime once debug is enabled at startup. Fresh evidence from this commit: SystemConfigService.update() reloads config with Config.reset_instance() (src/services/system_config_service.py) but does not re-run setup_logging, so after switching DEBUG/LOG_LEVEL back to non-debug, _should_log_llm_content_preview() still returns True and keeps emitting prompt/response previews. This breaks the expectation that disabling debug should immediately stop sensitive previews until debug is explicitly re-enabled.

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]]:
"""
Expand Down Expand Up @@ -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 = {
Expand All @@ -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

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LLM返回] 这一行使用的是 model_name = config.litellm_model,但实际调用 _call_litellm() 可能会因 fallback 返回不同的 model_used。这会导致日志误报实际使用的模型,排查问题时容易产生误导。建议在响应成功日志中改用 model_used(或同时输出 configured model 与实际 model_used)。

Copilot uses AI. Check for mistakes.
if allow_content_preview:
logger.debug(_build_llm_log_preview("LLM返回 调试预览", response_text))

# 解析响应
result = self._parse_response(response_text, code, name)
Expand Down
14 changes: 14 additions & 0 deletions src/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@

LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(pathname)s:%(lineno)d | %(message)s"
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
_sensitive_log_preview_enabled = False


def set_sensitive_log_preview_enabled(enabled: bool) -> None:
"""Persist an explicit per-process override for sensitive LLM previews."""
global _sensitive_log_preview_enabled
_sensitive_log_preview_enabled = bool(enabled)


def is_sensitive_log_preview_enabled() -> bool:
"""Return whether an explicit per-process preview override is enabled."""
return _sensitive_log_preview_enabled


class RelativePathFormatter(logging.Formatter):
Expand Down Expand Up @@ -76,6 +88,8 @@ def setup_logging(
level = console_level
else:
level = logging.DEBUG if debug else logging.INFO
# Only an explicit process-level debug override should bypass runtime config checks.
set_sensitive_log_preview_enabled(debug)

# 创建日志目录
log_path = Path(log_dir)
Expand Down
Loading
Loading