Skip to content

feat. 注入上下文完整记录到日志 - #1

Open
ColumbinaXn wants to merge 6 commits into
Michaelxwb:mainfrom
ColumbinaXn:feature_53309
Open

feat. 注入上下文完整记录到日志#1
ColumbinaXn wants to merge 6 commits into
Michaelxwb:mainfrom
ColumbinaXn:feature_53309

Conversation

@ColumbinaXn

Copy link
Copy Markdown

No description provided.

@Michaelxwb

Copy link
Copy Markdown
Owner

PR: feat. 注入上下文完整记录到日志 by @ColumbinaXn
结论: 🔴 Request Changes — 意图合理(增加可观测性),但实现有多处严重 bug 和规范违背,不建议合入。


🔴 Blocking(必须修复)

  1. cf_log.py:16-19 — get_log_path 逻辑错误,prefix=None 必崩

if not prefix or not isinstance(prefix, str):
log_path = os.path.join(LOG_ROOT, ...) # 分支 A 设置
log_path = os.path.join(LOG_ROOT, prefix, ...) # 缺 else,总是被覆盖
缺 else,分支 A 的赋值被下一行无条件覆盖。当 prefix 为 None 时,os.path.join(..., None, ...) 抛 TypeError。此函数任何无 prefix 调用都会炸。单测一写就挂——PR 也确实没加测试。

  1. cf_inject_hook.py:36-39 — 早退路径泄漏空日志文件

reset_stdout() 在 main() 开头就打开文件,但后续有 7+ 个 return 早退点(tool_name 不匹配、file_path 空、config 缺失、非代码文件、无 domains、无 matched、无 selected)。这些路径都不会执行
cleanup_none_logfile()。结果:每次对非 Edit/Write 文件的普通操作都会在 logs/ 下堆积空文件。

正确做法:atexit.register(cleanup_none_logfile) 或 try/finally。

  1. 违反项目编码规范(scripts/code-standards.md 自动注入)
  • Rules: "所有函数必须有 type hints" — cf_log.py 5 个函数零 type hint
  • Patterns: "测试 → tests/ 目录" — 无新增测试,规范明文要求
  • Patterns: "新增工具函数 → 放在 cf_core.py" — 本应合入 cf_core.py,而非新建模块
  1. 模块导入即副作用 — cf_log.py:10

os.makedirs(LOG_ROOT, exist_ok=True) # 模块顶层执行
任何 import cf_log(包括 pytest 收集、lint 工具静态分析)都会在用户项目根创建 logs/ 目录。违反最小惊讶原则。且 logs/ 未加入 .gitignore,会污染用户仓库。

  1. 性能回归 — 每次 Hook 调用都落盘

PreToolUse 对每次 Edit/Write/MultiEdit 触发。原实现纯内存处理;现在每次都 open(w)、write、可能 cleanup。在密集编辑场景下,是显著的 disk IO 回归。且 LogTee.close() 从未被调用,文件句柄泄漏。


🟡 Major(强烈建议修复)

  1. cf_log.py:45 — 时间戳秒级精度,同秒双调用互相覆盖

%Y%m%d_%H%M%S 无毫秒/微秒。同一 session 同秒两次 Hook → 同名文件 → 后者 w 模式覆盖前者日志。应加 %f 或序列号。

  1. cf_log.py:57-59 — TextIOWrapper(sys.stdout.buffer) 析构时关 stdout

Python 标准警告:包装 sys.stdout.buffer 后 wrapper 被 GC 时会尝试关闭底层 buffer,导致后续写 stdout 失败。正确做法是 sys.stdout.reconfigure(encoding="utf-8")(Py3.7+)。

  1. cf_log.py:23-25 — try/except: raise e 反模式

except Exception as e:
raise e # 丢失 traceback chain
要么删除 try 块,要么 raise(不带 e)。

  1. cf_inject_hook.py:149 — ensure_ascii=False 依赖 reset_stdout 重编码

若未来有人下掉日志逻辑但保留 ensure_ascii=False,在非 UTF-8 控制台(Windows cp1252)下会 UnicodeEncodeError 破坏 Hook 协议。建议在 assemble_context 或 cf_core 统一处理编码,而不是隐式耦合。

  1. 日志内容包含完整 additionalContext

日志落盘的是完整 JSON payload,其中含所有注入的 spec 文件内容。用户 spec 若含内部架构/凭据指引,将持久化到磁盘。至少应:(a) 添加到 .gitignore;(b) README 声明;(c) 提供关闭开关(如 CF_LOG=1
才启用)。


🟢 Minor

  • cf_log.py:2 — # -- encoding=utf-8 -- 非标准,PEP 263 用 coding:
  • cf_log.py:41 — LogTee.write() 返回 None,标准流返回字符数,可能破坏依赖返回值的调用方
  • cf_log.py:65 — open(_log_path, "r") 与仍持有的写句柄并发读,Windows 上 os.remove 会失败(文件占用)
  • 两处目录(.code-flow/scripts/ 与 src/core/code-flow/scripts/)同步修改 — 本项目 dogfood 需要,但应确认 CLI 安装器行为一致

建议重构方向(KISS)

70 行自造 LogTee 完全可用 stdlib 替代:

import logging, logging.handlers
def setup_inject_logger(sid: str) -> logging.Logger | None:
if os.environ.get("CF_LOG") != "1": # 默认关闭
return None
logger = logging.getLogger(f"cf_inject.{sid}")
handler = logging.handlers.RotatingFileHandler(
f"{LOG_ROOT}/hook_inject_{sid}.log",
maxBytes=1_000_000, backupCount=3, encoding="utf-8"
)
logger.addHandler(handler)
return logger

  • 默认关闭(环境变量开启)→ 解决性能与磁盘污染
  • RotatingFileHandler → 解决无界增长
  • 不替换 sys.stdout → 彻底规避 stdout 污染风险(这是 scripts/code-standards.md 明令禁止的"Hook stdout 输出非 JSON")
  • 日志写业务数据而非 payload 副本 → 降低泄漏风险

修复 Checklist(给 PR 作者)

  • 修复 get_log_path 的 if/else bug
  • 用 atexit 或 try/finally 保证清理
  • 改用 logging stdlib,默认关闭(CF_LOG=1 开启)
  • 移除模块导入副作用
  • 补 type hints(规范强制)
  • 补 tests/test_cf_log.py,覆盖 prefix=None/空/正常 + 早退清理
  • 将 logs/ 加入 .gitignore
  • 秒级时间戳 → 加微秒或序列号

…eature_53309

# Conflicts:
#	.code-flow/config.yml
#	src/core/code-flow/config.yml
@Michaelxwb

Copy link
Copy Markdown
Owner

⏺ PR 代码审查:feat. 注入上下文完整记录到日志

PR: #1
作者: ColumbinaXn
审查者: Claude


🔴 Blocking Issues

  1. cf_log.py:16-19 — 逻辑错误:当 prefix=None 时 TypeError

def init_log_path(self, prefix: str | None) -> str:
# ...
if not prefix:
log_path = os.path.join(LOG_ROOT, log_file_name) # prefix=None → 走此分支
else:
log_path = os.path.join(LOG_ROOT, prefix, log_file_name) # 永远执行不到

  os.makedirs(os.path.dirname(log_path), exist_ok=True)  # dirname(None) → TypeError!

问题:LOG_ROOT 是文件路径而非目录,os.path.dirname(LOG_ROOT) 得到上一级目录路径是对的。但 if not prefix 分支直接用 log_file_name 而不是 prefix,与注释逻辑相反。当 prefix=None 时走 if 分支,结果是
os.path.join(LOG_ROOT, log_file_name);当 prefix="hook_inject_xxx" 时走 else 分支。这逻辑是对的,但问题在于 当 prefix=None 时 os.makedirs(os.path.dirname(log_path)) 的 dirname 实际上是 LOG_ROOT
的上一级,这不对。

实际上,代码写的是 os.makedirs(os.path.dirname(log_path), exist_ok=True),而 log_path 是文件路径,所以 dirname(log_path) 是目录,这没问题。真正的问题是:当 prefix=None 时,日志文件直接放在
LOG_ROOT 下,而不是创建子目录。

但 LOG_ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(file)), "../logs")),这实际上是个目录而非文件路径。所以 os.path.dirname(LOG_ROOT) 会得到 ../,这有问题。

正确的实现应该是:
os.makedirs(LOG_ROOT, exist_ok=True) # 直接在 LOG_ROOT 下创建文件

而不是 os.makedirs(os.path.dirname(log_path), exist_ok=True)。

  1. cf_inject_hook.py:36-39 — 早期退出路径会留下空日志文件

Enable logging if configured (default off)

if inject_config.get("log") is True:
reset_stdout("hook_inject_" + sid)

... 后面多个 early return ...

if not isinstance(file_path, str) or not file_path:
return # ← 如果在此之前 reset_stdout 被调用,这里直接 return 会留下空 .log 文件

if not config:
return # ← 同样问题

StdoutLogTee 在 atexit 注册了 close(),但如果进程在 close() 被调用前就通过 early return 退出,日志文件会被留下(虽然 close() 会删除空文件,但前提是文件被写入过)。

修复:在 early return 前检查是否需要 cleanup:
if not isinstance(file_path, str) or not file_path:
if 'tee' in locals():
tee.close() # 确保清理
return

或者更好的方案是:使用 Python 标准库 logging + RotatingFileHandler,完全避免 sys.stdout 替换。


🟡 Design Issues

  1. 使用 sys.stdout 替换有风险

用 sys.stdout = tee 替换全局 stdout 会影响所有后续输出,任何未捕获的异常或提前退出都会导致日志不完整。

建议:使用 logging 模块 + RotatingFileHandler,通过环境变量 CF_LOG=1 启用,这样更符合 Python 惯例:
import logging
import os

if os.getenv("CF_LOG") == "1":
handler = RotatingFileHandler(..., maxBytes=1010241024, backupCount=5)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)

  1. 性能问题:每次 Hook 调用都写磁盘

当前实现为每个 session 创建新的日志文件,每次注入都打开/写入/flush。若高频调用会有 I/O 开销。

建议:除非 CF_LOG=1,否则不记录日志。默认只记录错误到 stderr。

  1. cf_log.py 应该放在核心 scripts 目录还是独立模块?

当前创建在 .code-flow/scripts/cf_log.py 和 src/core/code-flow/scripts/cf_log.py。如果需要跨项目复用,考虑放到独立模块;否则放在项目内合理。


🟢 Minor Issues

  1. 缺少类型提示

cf_log.py 的 write 方法返回 int,但 StdoutLogTee.write 返回 int,而标准 sys.stdout.write 返回 None。这会导致类型不匹配。

  1. 缺少测试

新模块 cf_log.py 需要测试用例覆盖:

  • prefix=None 的路径
  • prefix="something" 的路径
  • 空日志文件删除逻辑
  • write() 和 flush() 方法
  1. ensure_ascii=False 在 json.dumps 是好的改进 ✅

📋 Summary

┌────────┬────────────────────────────────────┬─────────────┐
│ 类别 │ 问题 │ 严重度 │
├────────┼────────────────────────────────────┼─────────────┤
│ Bug │ LOG_ROOT 作为目录被 dirname() 误用 │ 🔴 Blocking │
├────────┼────────────────────────────────────┼─────────────┤
│ Bug │ Early return 泄漏空日志 │ 🔴 Blocking │
├────────┼────────────────────────────────────┼─────────────┤
│ Design │ 使用 sys.stdout 替换不健壮 │ 🟡 Design │
├────────┼────────────────────────────────────┼─────────────┤
│ Design │ 每次调用写磁盘性能差 │ 🟡 Design │
├────────┼────────────────────────────────────┼─────────────┤
│ Style │ 缺少类型提示 │ 🟢 Minor │
├────────┼────────────────────────────────────┼─────────────┤
│ Style │ 缺少测试 │ 🟢 Minor │
└────────┴────────────────────────────────────┴─────────────┘

建议:采用 Python logging 标准库替代当前实现,修复 LOG_ROOT 路径问题,并添加单元测试。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants