首页
/ OpenClaw agent-transcript:为 PR/Issue 正文嵌入脱敏后的 Agent 会话记录(附源码级实现剖析)

OpenClaw agent-transcript:为 PR/Issue 正文嵌入脱敏后的 Agent 会话记录(附源码级实现剖析)

2026-09-06 09:12:29作者:毕习沙Eudora

OpenClaw 仓库中的 agent-transcript 是一个本地技能(skill):当由 Agent 代劳创建 GitHub PR/Issue 时,它从本地各 Agent 会话日志(Codex、Claude、Pi、OpenClaw)中找回与本次改动最相关的会话,将其脱敏渲染为一段 ## Agent Transcript 折叠区块,追加进 PR/Issue 正文,让审阅者能看到"这个 Agent 为什么这么改"的决策轨迹。读完全文,你将掌握 find / render / preview / append-body / html 五个子命令的完整用法、脱敏与 fail-closed 机制的底层实现,以及一套可复制的 Agent 工作流溯源方案。

场景定位:本地优先、尽力而为的溯源能力

该技能定义在 SKILL.md,配套一个约 683 行的零依赖 Node.js 脚本 agent-transcript。它解决的核心问题是:Agent 自动提交的 PR 往往只有"结果",审阅者看不到 Agent 在会话中做出的关键决策、踩过的坑和验证证据。直接把原始会话日志贴进 PR 又不可行——日志里混着密钥、Cookie、本地路径。因此技能契约(Contract)明确了一组硬性约束:

  • 绝不使用网络:会话发现只读本地 Agent 日志;
  • 绝不上传原始日志:必须先渲染为脱敏后的 Markdown;
  • 插入前必须征求用户同意,并向用户说明脱敏后的会话日志有助于审阅、便于排定 PR 优先级;
  • 先提供本地 HTML 预览:用户要求预览时,先打开 HTML 并等待确认,再插入正文;
  • fail closed(失败即拒绝):若脱敏后仍残留密钥、私钥、浏览器/会话/Cookie 细节或 auth URL,则整体放弃该段转录;
  • 内容取舍:丢弃系统/开发者提示词、原始工具输出、推理内容、环境变量、Cookie、Token 和宽泛的本地路径;保留用户提示、助手可见的决策、简化的工具摘要、测试与验证结果;
  • 范围裁剪:以 PR/Issue 标题、分支名、改动文件与声明目标为范围,删除同一会话中与此 PR 无关的轮次;
  • 尽力而为:找不到安全转录时,PR/Issue 创建流程照常继续;
  • 不留占位:只有在真正插入转录时才添加 ## Agent Transcript 区块,严禁添加"A sanitized local transcript preview was generated but not included."之类的占位文本;
  • 幂等更新:转录区块使用折叠的 <details> 形式,重复插入时更新既有 marker 区块而不是复制出第二个。

会话发现:find 子命令

find 的任务是从本地一堆 JSONL 会话日志里,定位与某个 PR 最可能相关的会话:

.agents/skills/agent-transcript/scripts/agent-transcript find \
  --query "$PR_TITLE $BRANCH_OR_PR_URL" \
  --cwd "$PWD" \
  --since-days 14

输出为 JSON 候选列表。默认参数含义(脚本 usage 见 main() 使用信息):

  • --query:查询文本,通常是 PR 标题加分支名/PR URL;
  • --cwd:当前工作目录,用于加分匹配;
  • --since-days N:只看最近 N 天修改过的日志(默认 14 天);
  • --max-files N:候选日志数量上限(默认 400,可加大以放宽本地搜索);
  • --root PATH...:自定义扫描根目录(可多次传入)。

默认扫描哪些日志根目录

从源码 defaultRoots() 看,默认扫描四类 Agent 的本地会话目录:

Agent 默认根目录
Codex ~/.codex/sessions
Claude ~/.claude/projects
Pi ~/.pi/agent/sessions
OpenClaw ~/.openclaw/agents/<agent>/sessions 等(见下)

OpenClaw 的根目录解析在 openClawSessionRoots() 中:状态目录取环境变量 OPENCLAW_STATE_DIR,未设置时回退到 ~/.openclaw;随后枚举 agents/ 下的每个 agent 目录,收集其中实际存在的三个候选子目录 sessionsagent/sessionsagent/codex-home/sessions(即 OpenClaw 托管 Codex 时把 Codex home 也放进自己状态目录内的布局),并去重。扫描过程由 walkJsonl() 完成:按 mtime 过滤出窗口内的 .jsonl 文件,跳过 node_modules.git,再按修改时间倒序取最新的 --max-files(默认 400)个文件参与打分(见 candidateFiles())。

打分算法

打分逻辑在 scoreScanRecord(),是一个确定性的加权包含匹配:

  1. 查询词拆分--query 按空白拆词,同时用 https?://\S+ 额外抽出其中的 URL 作为整词参与匹配(见 findSessions()),因此把 PR URL 一起塞进 query 是有实际作用的;
  2. 每个词命中得分min(20, max(3, 词长/3)),即词越长得分越高,但封顶 20 分,且长度小于 3 字符的词直接忽略;
  3. cwd 加分:会话文本中出现工作目录路径(或其把 / 替换为 - 的变体)时额外 +8 分,理由记为 cwd
  4. 文件扫描预算find 每个文件只读头部 60000 字节做匹配(代码中 --scan-bytes 默认值),大文件取头尾各半(readBoundedText());
  5. 排序与截断:按分数降序、mtime 降序,默认最多返回 10 条候选(limit 默认 10)。

拿到候选后,人工(或 Agent 工作流)从中挑选高置信度的会话,进入渲染阶段。

脱敏渲染管线:render 子命令

.agents/skills/agent-transcript/scripts/agent-transcript render \
  --session "$SESSION_JSONL" \
  --out /tmp/agent-transcript.md

--session 指向 find 选出的 JSONL 日志;可选参数还有 --max-chars(整段转录字符上限)、--entry-max-chars(单条上限)、--title--url(用于区块标题行)。核心渲染逻辑集中在 renderSession(),它依次完成五件事:角色归一、内容取舍、脱敏、工具调用压缩、截断与结构化输出。

角色归一:不同 Agent 的 JSONL 结构被映射到统一角色

会话日志按行解析(readJsonl() 限制最多 12000 行,解析失败的行为标记为 unparsed)。eventRole() 把各 Agent 的异质行结构统一映射为 user / assistant / tool / tool_output / web 五类角色,覆盖 Codex 的 event_msg/response_item 载荷(user_messageagent_messagefunction_callfunction_call_outputweb_search_callreasoning 等)、Claude 风格的 user/assistant/tool_use/tool_result 行,以及带 message.role 的通用行。detectAgent() 则优先按文件路径(.codex / .claude / .pi / .openclawagents/…/sessions 特征)判断 Agent 身份,路径无法判断时再按行内特征(session_meta/response_item 判 Codex,sessionId+userType 判 Claude)兜底,识别结果会写进输出标题。

内容取舍规则与 SKILL.md 契约一一对应:

  • tool_output(原始工具输出)一律丢弃,只计数(stats.toolOutputsDropped);
  • reasoning(推理内容)、token_count/task_started/task_complete 等事件直接过滤;
  • web_search_call 类事件只计数(stats.web),正文不入转录;
  • 系统/开发者提示词通过 hasSetupBlob() 识别(<INSTRUCTIONS># AGENTS.MDKnowledge cutoff:You are Codex、"your instructions" 等特征),整条替换为 [instructions recap omitted; policy/config text, not task dialogue]

脱敏规则表

文本级脱敏在 redact() 中实现,共 12 组正则替换规则:

目标 替换为
PKCS 私钥块(-----BEGIN … PRIVATE KEY----- 整块) [REDACTED_PRIVATE_KEY]
OpenAI 风格密钥 sk- 后跟 ≥20 位字符 [REDACTED_OPENAI_KEY]
GitHub token(ghp_/gho_/ghu_/ghs_/ghr_ 后跟 ≥20 位) [REDACTED_GITHUB_TOKEN]
AWS 密钥 AKIA + 16 位 [REDACTED_AWS_KEY]
JWT(eyJ… 三段式) [REDACTED_JWT]
Bearer/Basic 授权头 [REDACTED_AUTH_HEADER]
邮箱地址 [REDACTED_EMAIL]
电话号码形态字符串 [REDACTED_PHONE]
macOS 用户路径 /Users/... [LOCAL_PATH]
家目录相对路径 ~/... [HOME_PATH]
URL 查询参数 token/key/secret/signature/sig/access_token/auth= 的取值 保留参数名,值替换为 [REDACTED]

每次命中都会累加 stats.redactions,最终统计随转录一起输出,审阅者能直观看到做了多少处脱敏。

fail-closed:脱敏后仍不安全就整体拒绝

脱敏是替换,但有些内容"替换了也没意义"。unsafe() 定义了一组兜底模式:私钥块、Bearer/Basic 头、GitHub 会话 Cookie 名(user_session_gh_sess__Host-user_session_same_siteGH_SESSION_TOKEN)、敏感环境变量名(GITHUB_TOKENGH_TOKENOPENAI_API_KEYANTHROPIC_API_KEY)、以及 /upload/policies/assetsuploadTokenauthenticity_token 等上传鉴权内部细节。

  • 单条内容命中这些模式时,该条在 normalizeEntry() 中被整体替换为 [omitted: browser/session/auth internals; not useful for public PR transcript]
  • 渲染完成后对全文再扫一遍(renderSession() 末尾),若仍有残留,render/preview/append-body 三个子命令都会直接抛错 unsafe transcript after redaction: ... 并退出非零(见 main()),即 SKILL.md 所说"fail closed"。此时正确做法是放弃该会话转录,而不是尝试修补。

工具调用压缩:从原始输出到家族计数

工具调用不会逐条展开,而是按"家族"计数。toolFamily() 按名称关键词把工具归入 read(read/fetch/search/grep/diff…)、write(write/edit/patch/apply/click/type…)、execute(exec/shell/test/build/pnpm/git…)、network(web/http/browser/ssh…)或 other 五类。对 Codex 的 exec_command 工具会进一步解析参数中的 cmd,用 shellFamily() 按首命令细化归类(如 rg/grep/cat/git statusreadgit commit/pnpm test 分别算 write/execute);apply_patch 固定算 writetoolCallFamily())。最终所有工具调用被压缩成一条摘要,例如 12 read, 3 write, 5 execute; raw tool outputs dropped: 18compactToolSummary()),作为 [tool summary] 条目追加在转录末尾——这正是契约中"保留 terse tool summaries"的落点。

去重、合并与截断,以及最终输出结构

条目处理链还包括:空白归一后的整条去重(renderSession())与相邻同角色条目合并(coalesceEntries(),互相包含时保留更完整的一条)。截断有两个层级,默认值定义在 脚本头部常量

  • 单条超过 --entry-max-chars(默认 6000):截断并追加 ...[truncated N chars]
  • 全文超过 --max-chars(默认 50000):截断并追加 ...[transcript truncated to 50000 chars]

最终产物是一个带 HTML 注释 marker 的自包含区块(输出模板):

<!-- agent-transcript:start -->
## Agent Transcript

<details>
<summary>Redacted openclaw session transcript: <title | url></summary>

```text
source: [LOCAL_SESSION]
redaction: local paths, emails, phone-shaped strings, token-shaped strings, auth headers, auth query params
omitted: raw tool outputs, system/developer prompts, local paths, secrets, browser/session/auth details
stats: {"agent":"openclaw","entries":...,"user":...,"assistant":...,"toolCalls":...,"toolOutputsDropped":...,"web":...,"redactions":...,"omittedUnsafe":...}

[user]
...
[assistant]
...
[tool summary]
12 read, 3 write, 5 execute; raw tool outputs dropped: 18
```

</details>
<!-- agent-transcript:end -->

注意 source 行写的是 [LOCAL_SESSION] 而非具体路径——原始日志文件路径本身不会进入 PR 正文。

本地预览:preview 子命令

.agents/skills/agent-transcript/scripts/agent-transcript preview \
  --session "$SESSION_JSONL" \
  --out /tmp/agent-transcript-preview.html
open /tmp/agent-transcript-preview.html

previewrender 走同一条渲染管线(同样执行 fail-closed 检查),只是把结果包进一个独立的 HTML 文档(singlePreviewDocument()):系统字体、灰色 <pre> 代码块、含标题与安全状态页眉的最小样式。按契约,用户要求预览时必须先打开这个 HTML 并等待确认,才能执行插入动作。macOSopen 即可;Linux 上可换用系统默认浏览器命令打开同一文件。

写入正文:append-body 子命令

.agents/skills/agent-transcript/scripts/agent-transcript append-body \
  --body /tmp/pr-body.md \
  --session "$SESSION_JSONL" \
  --out /tmp/pr-body.with-transcript.md

--body 是已起草好的普通 PR/Issue 正文,--session 是选定的会话日志,产物写入 --out(未指定则打印到 stdout)。幂等性由 replaceSection() 保证:若正文中已存在 <!-- agent-transcript:start --><!-- agent-transcript:end --> 之间区块,则就地替换为最新转录;否则追加到文末。因此重复运行(比如改了会话后重新生成)不会产生重复的 ## Agent Transcript 标题,对应契约中"update existing markers instead of duplicating sections"。

完整 PR/Issue 工作流(SKILL.md 八步)

把上述命令串起来,SKILL.md 规定的标准工作流是:

  1. 先起草常规的 PR/Issue 正文;
  2. find 传入标题、分支名、已知 PR URL/编号与 cwd;
  3. 找到高置信度会话后,询问用户:Include a redacted agent transcript? It helps reviewers and can make the PR easier to prioritize. I can open a local preview first.
  4. 用户要预览时,运行 preview、用 open 打开 HTML,并等待确认;
  5. 插入前人工裁剪生成的区块:只保留解释本次 PR/Issue 目标、实现选择、改动文件、测试、验证证据、阻塞与最终结果的轮次;
  6. 用户批准后运行 append-body
  7. 用增强后的正文文件创建/更新 PR(如 gh pr create --body-file 或 connector 创建流程);
  8. 找不到安全会话时不特别说明、直接不带转录继续;用户拒绝时同样不带转录继续,且不得添加任何转录占位区块。

维护者批量审计:html 子命令

SKILL.md 另设一节"Review Artifacts",面向需要跨多个 PR/会话候选做人工审计的维护者(明确不属于 PR/Issue 工作流本身):

.agents/skills/agent-transcript/scripts/agent-transcript html \
  --prs /tmp/recent-prs.json \
  --out /tmp/agent-transcript-preview.html
  • 输入格式--prs 指向一个 JSON 文件,可以是数组,也可以是 {items: [...]}{prs: [...]} 包裹的对象(readPrs());每个条目可用字段包括 urlnumbertitleheadRefName/branch
  • 匹配与阈值:对每个 PR 用 url/编号/标题/分支拼出查询词,在本地会话上打分,默认 --min-score 50 以下视为无匹配,对应 PR 会渲染为 No local session match found. 的错误占位(html 分支);此路径的文件扫描预算为 90000 字节;
  • 输出:一份 HTML 页面,每个 PR 一个 <section>,含 PR 标题链接、匹配会话(同样只显示 [LOCAL_SESSION])、score 与 safe 状态、以及渲染出的转录 <pre> 文本;渲染失败(通常是 fail-closed 抛错)时记录错误信息而不是中断整批。

边界与失败行为小结

理解这个技能的能力边界,来自两条互相制衡的设计:

  • fail closed(单点):只要脱敏后仍可能泄漏密钥、会话凭证或认证内部细节,render/preview/append-body 立即报错拒绝产出——宁可没有转录,不带病上线;
  • best effort(整体):单个会话不安全或完全找不到会话,不阻塞 PR/Issue 创建流程本身,也不允许在正文中留下占位区块。

此外还有几个值得注意的默认值与限制:find 默认只看 14 天内、最新 400 个、每文件 60000 字节的候选(均可用 --since-days/--max-files/--scan-bytes 放宽);render 单日志最多读 12000 行;转录总量默认上限 50000 字符。全部处理都在本机完成,脚本无网络调用(usage 末行 Local-only. No network calls. 与源码中仅 node:fs/os/path 的 import 一致),这意味着它只适用于会话日志仍保留在本地机器的场景;若工作发生在 CI 容器等临时环境中,本地日志不可见,find 自然无果,此时按契约静默跳过即可。

小结

agent-transcript 把"Agent 为什么这么改"这一溯源问题,压缩成了一条本地、离线、幂等的工具链:find 按标题/分支/URL 在多 Agent 会话日志中打分定位,render 管线统一角色归一、12 组脱敏规则、工具家族压缩与 fail-closed 兜底,preview 提供插入前的人工确认,append-body 以 marker 区块幂等地并入 PR 正文,html 则给维护者提供批量审计视图。五个子命令全部由 单个零依赖脚本 承载,契约与行为均可在 SKILL.md 与脚本源码中逐条对照验证,是一套可直接借鉴到自有仓库的 Agent 工作流溯源实践。

登录后查看全文
热门项目推荐
相关项目推荐