配套
architecture-logs-as-bridge.md。本篇讲"怎么做",上一篇讲"为什么"。
from cq2_sdk.debug_log import DebugLogger,每个跑流程的入口包一层D:\传奇\debug_logs\session_*.jsonl代码已经落地(单测 24/24 通过):
src/cq2_sdk/debug_log.py # 核心 logger,~170 行
tests/unit/test_debug_log.py # 24 个单测
scripts/demo_debug_log.py # 4 种典型用法演示
docs/architecture-logs-as-bridge.md # 架构哲学
docs/debug-session-workflow.md # 本文档
不需要装新依赖(DebugLogger 只用 stdlib:json / threading / pathlib / uuid)。
from cq2_sdk.debug_log import DebugLogger
with DebugLogger() as log:
log.step("login_attempt", account="longwu988")
# ... 你的代码 ...
跑完自动在 D:\传奇\debug_logs\ 留下一个 JSONL 文件。
with DebugLogger() as log:
req_id = log.http_request("POST", "https://api.jiyuhz.cn/auth/v3/login/safe/byPwd",
headers={...}, body="<rsa>")
try:
resp = httpx.post(...)
log.http_response(req_id, resp.status_code, body=resp.text,
duration_ms=elapsed)
except Exception as e:
log.http_error(req_id, e)
req_id 把请求和响应配对,OpenCode 读日志时能直接看到完整往返。
with DebugLogger() as log:
try:
do_risky_thing()
except Exception as e:
log.error(e, step="do_risky_thing", extra_ctx=...)
或者不抓——上下文管理器会在 __exit__ 时自动把异常落 error 事件。
# 看最新一次 session 的所有事件
Get-Content "D:\传奇\debug_logs\session_*.jsonl" | Select-Object -Last 1
# (PowerShell 不太好用 jq,参考下面的"用 Python 读")
# 如果装了 jq(推荐装一个)
jq -r '"\(.ts) \(.event) \(.msg // .name // .status // "")"' \
"D:/传奇/debug_logs/session_20260916_103345_a1b2c3.jsonl"
# 用 Python 读(最稳)
import json
with open(r"D:\传奇\debug_logs\session_xxx.jsonl", encoding="utf-8") as f:
for line in f:
rec = json.loads(line)
print(rec["ts"], rec["event"], rec.get("msg", rec.get("name", "")))
直接说人话就行:
"看
D:\传奇\debug_logs\session_20260916_180812_789abc.jsonl,找出box_996.py里 sign 算法应该用哪一种""对比这两个 session 的 http_request 事件,看今天新加的 retry 逻辑有没有生效:
session_xxx.jsonl和session_yyy.jsonl""session 末尾有 3 个 error 事件,定位是哪一步出的问题,然后帮我修"
OpenCode 会自己用 grep / read 工具处理 JSONL,不需要你手动解析。
每条事件都至少有:
{
"ts": "2026-09-16T10:23:45.123+00:00", // UTC ISO 8601,毫秒精度
"session": "s_20260916_102345_a1b2c3", // session id
"script": "smoke.py", // 哪个脚本写的
"event": "http_request", // 事件类型
// ... 事件特定字段
}
事件类型清单:
| event | 触发 | 必有字段 |
|---|---|---|
session_start |
DebugLogger 构造时 | start_ts, pid, extra 透传 |
session_end |
close() 或 context manager 退出 |
(无) |
step |
log.step(name, ...) |
name |
info / warn |
log.info/warn(msg, ...) |
level, msg |
error |
log.error(exc_or_str, ...) |
level, msg, exc_type(可空), step(可空) |
http_request |
log.http_request(...) |
req_id, method, url, headers, body_preview, step |
http_response |
log.http_response(...) |
req_id, status, headers, body_preview, duration_ms, step |
http_error |
log.http_error(...) |
req_id, msg, exc_type, level, step |
| 任意自定义 | log.log("my_event", ...) |
你的字段 |
所有 body_preview 自动截断到 800 字符,超出部分标记 ...<truncated N chars>。
完整 body 想要的话自己加字段(比如 body_full)。
# scripts/probe_new_endpoint.py
from cq2_sdk.debug_log import DebugLogger
with DebugLogger(extra={"purpose": "probe_new_endpoint", "issue": "#42"}) as log:
log.step("probe", endpoint="/new/api/v1/whatever")
# 试不同参数,看哪个返回 200
for params in variants:
req_id = log.http_request("POST", url, body=params)
resp = httpx.post(url, json=params)
log.http_response(req_id, resp.status_code, body=resp.text)
跑完让 OpenCode 看 http_response.status,挑出 200 的那个 body 当模板。
# scripts/debug_login.py
from cq2_sdk.debug_log import DebugLogger
with DebugLogger(extra={"purpose": "debug_login_101000"}) as log:
log.step("login_attempt", account="longwu988")
rid = log.http_request("POST", "https://api.jiyuhz.cn/auth/v3/login/safe/byPwd",
body=rsa_login_info)
resp = httpx.post(...)
log.http_response(rid, resp.status_code, body=resp.text,
duration_ms=elapsed)
if resp.json()["code"] == 101000:
log.warn("server demanded geetest, checking geetest solver",
challenge=resp.json()["data"]["gt"])
把 session 文件丢给 OpenCode,它能直接定位到 101000 的那一行, 告诉你 geetest 字段缺什么。
# 拿两个 session diff
diff <(jq -S . "D:/传奇/debug_logs/session_昨天.jsonl") \
<(jq -S . "D:/传奇/debug_logs/session_今天.jsonl")
或者让 OpenCode 帮你 diff。
| ❌ 不要 | ✅ 应该 |
|---|---|
在一次 with 块里写太多脚本逻辑 |
一个脚本一次 with DebugLogger() as log |
把日志写到 D:\传奇\debug_logs\ 之外 |
用 log_dir 参数明确指定 |
| 一个 session 跑多轮 | 一轮 = 一个 session 文件,方便 diff |
写密码、token 明文到 body_preview |
自己 mask:body_preview=mask_token(body) |
| 让 OpenCode 改日志文件 | OpenCode 只读,脚本只写 |
| 共享一个 session 文件给多个进程 | 各自开自己的,append 模式不保证原子 |
cd D:\传奇\cq2_sdk
uv run python scripts/demo_debug_log.py
会输出 4 个 demo 模式 + 最后 5 行 JSONL。把生成的 session 文件路径丢给 OpenCode 试读。
| 现象 | 原因 | 修法 |
|---|---|---|
Permission denied: D:\传奇\debug_logs\ |
目录被其他进程锁 | 关掉 Fiddler/编辑器;或换 log_dir 到别的位置 |
JSONL 里中文是 \uXXXX 转义 |
ensure_ascii=False 失效(极少见) |
重装 Python 3.12+ |
| 文件没出现 | 异常抛出但没进 with 块 |
用 try/finally 显式 log.close() |
| 多线程下某些行缺失 | 锁问题(理论上不会) | 升级到 1.0+ 报 bug;先 log.info 单线程验证 |
| OpenCode 读不动大文件 | 单个 session 太大(>10MB) | 按 step 分文件,或加 body_preview_limit=200 |