OpenClaw agent-transcript:为 PR/Issue 正文嵌入脱敏后的 Agent 会话记录(附源码级实现剖析)
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 目录,收集其中实际存在的三个候选子目录 sessions、agent/sessions、agent/codex-home/sessions(即 OpenClaw 托管 Codex 时把 Codex home 也放进自己状态目录内的布局),并去重。扫描过程由 walkJsonl() 完成:按 mtime 过滤出窗口内的 .jsonl 文件,跳过 node_modules 与 .git,再按修改时间倒序取最新的 --max-files(默认 400)个文件参与打分(见 candidateFiles())。
打分算法
打分逻辑在 scoreScanRecord(),是一个确定性的加权包含匹配:
- 查询词拆分:
--query按空白拆词,同时用https?://\S+额外抽出其中的 URL 作为整词参与匹配(见 findSessions()),因此把 PR URL 一起塞进 query 是有实际作用的; - 每个词命中得分:
min(20, max(3, 词长/3)),即词越长得分越高,但封顶 20 分,且长度小于 3 字符的词直接忽略; - cwd 加分:会话文本中出现工作目录路径(或其把
/替换为-的变体)时额外 +8 分,理由记为cwd; - 文件扫描预算:
find每个文件只读头部 60000 字节做匹配(代码中--scan-bytes默认值),大文件取头尾各半(readBoundedText()); - 排序与截断:按分数降序、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_message、agent_message、function_call、function_call_output、web_search_call、reasoning 等)、Claude 风格的 user/assistant/tool_use/tool_result 行,以及带 message.role 的通用行。detectAgent() 则优先按文件路径(.codex / .claude / .pi / .openclaw 及 agents/…/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.MD、Knowledge 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_site、GH_SESSION_TOKEN)、敏感环境变量名(GITHUB_TOKEN、GH_TOKEN、OPENAI_API_KEY、ANTHROPIC_API_KEY)、以及 /upload/policies/assets、uploadToken、authenticity_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 status 算 read,git commit/pnpm test 分别算 write/execute);apply_patch 固定算 write(toolCallFamily())。最终所有工具调用被压缩成一条摘要,例如 12 read, 3 write, 5 execute; raw tool outputs dropped: 18(compactToolSummary()),作为 [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
preview 与 render 走同一条渲染管线(同样执行 fail-closed 检查),只是把结果包进一个独立的 HTML 文档(singlePreviewDocument()):系统字体、灰色 <pre> 代码块、含标题与安全状态页眉的最小样式。按契约,用户要求预览时必须先打开这个 HTML 并等待确认,才能执行插入动作。macOS 上 open 即可;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 规定的标准工作流是:
- 先起草常规的 PR/Issue 正文;
- 用
find传入标题、分支名、已知 PR URL/编号与 cwd; - 找到高置信度会话后,询问用户:
Include a redacted agent transcript? It helps reviewers and can make the PR easier to prioritize. I can open a local preview first.; - 用户要预览时,运行
preview、用open打开 HTML,并等待确认; - 插入前人工裁剪生成的区块:只保留解释本次 PR/Issue 目标、实现选择、改动文件、测试、验证证据、阻塞与最终结果的轮次;
- 用户批准后运行
append-body; - 用增强后的正文文件创建/更新 PR(如
gh pr create --body-file或 connector 创建流程); - 找不到安全会话时不特别说明、直接不带转录继续;用户拒绝时同样不带转录继续,且不得添加任何转录占位区块。
维护者批量审计: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());每个条目可用字段包括url、number、title、headRefName/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 工作流溯源实践。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00