Claude Code 最终输出归档:剔除 Thinking 与工具过程,打造自动化个人 Markdown 知识库(进阶篇)
这篇博客是上一篇《Claude Code 提示词自动归档》的进阶篇。
在上一篇文章中,我们实现了通过 Claude Code 的 Hook 机制自动将输入的 Prompt 记录到本地。但在实际使用中,仅存 Prompt 相当于“只有问题没有答案”。
为了把 Claude Code 真正变成随手可查的编码知识库,我们需要连同 Claude 的回复一并归档。
然而,Claude Code 在干活时会产生大量中间过程——比如漫长的思考(`
` 块)、频繁的工具调用(如 `Write`、`Edit`、`Bash` 执行结果)。如果直接把完整的 Context 存下来,日志会极其臃肿。 本文将介绍如何利用 Claude Code 的原生 Hook 与 Node.js 脚本,**精确识别并提取 Claude 的“最终回答”,彻底剔除中间思考与工具执行过程,并自动按“项目/日期”生成 Markdown 知识库文件**。 ## 一、 核心判断逻辑:如何精准捕获“最终输出”? Claude Code 在运行过程中会频繁触发各种 Hook 事件。要实现“只存最终回答”,核心在于解决以下 3 个判断问题: ### 1. Hook 事件精准判定 – **`UserPromptSubmit`**:仅在用户刚按下回车发送提示词时触发,用于记录问题。 – **`Stop` / `SessionStop`**:在 Claude 完成一轮响应(或暂停)时触发。**必须依赖此事件来捕获输出**。 – **注意**:Claude Code 在触发 `Stop` 事件时传给 stdin 的 JSON 中,依然会携带上一轮的 `prompt` 字段。如果仅靠“是否存在 `prompt` 字段”来判定,会导致 `Stop` 事件被误判为用户输入而直接跳过。脚本必须优先读取 `data.hook_event_name` 进行严格判定。 ### 2. 过滤思考过程(Thinking Blocks) 在 Claude 开启 Extended Thinking 时,思考内容会被封装在 `type: ‘thinking’` 的 Block 中,或者包含在 ` ` 标签内。 – **判断方式**:解析内容 Block 数组时,过滤掉所有 `block.type === ‘thinking’` 的节点,仅提取 `block.type === ‘text’` 的内容。同时在字符串层级正则清理残余的 ` ` 标签。 ### 3. 过滤工具执行过程(Tool Use & Tool Results) 当 Claude 执行文件写入或终端命令时,会产生 `tool_use` 和 `tool_result` 节点。 – **判断方式**:直接跳过所有 Tool 节点。当一轮 Assistant 的回复处理完毕后,只拼接最终渲染给用户的文本内容(例如:“已在 hello.html 生成好啦…”)。 ### 4. 深度兜底(Transcript JSONL 倒序解析) 在部分 CLI 版本中,`Stop` 事件的 Payload 并不直接包含完整的消息历史,而是提供了一个指向本地转录日志的路径 `transcript_path`。脚本通过**倒序读取 JSONL 文件**,寻找最后一个 `role === ‘assistant’` 且包含文本的节点,可以做到 **100% 稳妥提取**。 ## 二、 核心 Node.js 脚本 (`log_prompt.js`) 这是一个零依赖(不需要 `npm install` 任何第三方包)的 Node.js 原生脚本。支持跨平台(Windows / Linux),自动识别当前工作区项目名并按日期生成 Markdown。 请将以下代码保存为 **`log_prompt.js`**: JavaScript “` const fs = require(‘fs’); const path = require(‘path’); const os = require(‘os’); let body = ”; process.stdin.on(‘data’, chunk => { body += chunk; }); process.stdin.on(‘end’, () => { try { if (!body.trim()) return; const data = JSON.parse(body); // 1. 获取工作目录与项目名称(提取最后一级目录名) const cwd = data.cwd || process.cwd(); const fullProjectPath = path.resolve(cwd); const projectName = path.basename(fullProjectPath) || ‘global’; // 2. 格式化当前时间与日期 const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, ‘0’); const day = String(now.getDate()).padStart(2, ‘0’); const dateStr = `${year}-${month}-${day}`; // YYYY-MM-DD const timeStr = now.toLocaleTimeString(‘zh-CN’, { hour12: false }); // HH:mm:ss // 3. 构建归档目录:~/.claude/history/项目名/YYYY-MM-DD.md const userHome = os.homedir() || process.env.USERPROFILE; const baseDir = path.join(userHome, ‘.claude’, ‘history’, projectName); if (!fs.existsSync(baseDir)) { fs.mkdirSync(baseDir, { recursive: true }); } const mdFilePath = path.join(baseDir, `${dateStr}.md`); // 如果是当天首条记录,先写入 Markdown 文档头 if (!fs.existsSync(mdFilePath)) { const header = `# ${projectName} – 对话记录 (${dateStr})\n\n> **项目路径**:\`${fullProjectPath}\` \n> **创建时间**:${dateStr}\n\n—\n\n`; fs.writeFileSync(mdFilePath, header, ‘utf8’); } // 4. 获取原生 Hook 事件名称 const eventName = data.hook_event_name || data.event || data.type || ”; // ————————————————————- // 情况 A:用户提交提示词 (UserPromptSubmit) // ————————————————————- if (eventName === ‘UserPromptSubmit’) { const prompt = data.prompt || data.content || (data.message && data.message.content); if (prompt) { const promptMarkdown = `## 💬 Prompt [${timeStr}]\n\n\`\`\`text\n${prompt}\n\`\`\`\n\n`; fs.appendFileSync(mdFilePath, promptMarkdown, ‘utf8’); } return; } // ————————————————————- // 情况 B:Claude 回复结束 (Stop / SessionStop) // ————————————————————- if (eventName === ‘Stop’ || eventName === ‘SessionStop’ || !eventName) { let finalAnswer = ”; // 文本提取逻辑:只保留 type === ‘text’ 的节点 const extractTextFromContent = (content) => { if (typeof content === ‘string’) return content; if (Array.isArray(content)) { return content .filter(block => block && block.type === ‘text’ && block.text) .map(block => block.text) .join(‘\n’); } return ”; }; // 策略 1:直接从 stdin JSON 的 messages 解析 if (Array.isArray(data.messages) && data.messages.length > 0) { const lastAssistantMsg = […data.messages].reverse().find(m => m.role === ‘assistant’); if (lastAssistantMsg) { finalAnswer = extractTextFromContent(lastAssistantMsg.content); } } // 策略 2:从 response / result / message 解析 if (!finalAnswer) { if (data.last_assistant_message) { finalAnswer = extractTextFromContent(data.last_assistant_message); } else if (typeof data.response === ‘string’) { finalAnswer = data.response; } else if (typeof data.result === ‘string’) { finalAnswer = data.result; } else if (data.result && data.result.text) { finalAnswer = data.result.text; } } // 策略 3:从本地转录文件 (transcript_path JSONL) 倒序解析 if (!finalAnswer && data.transcript_path && fs.existsSync(data.transcript_path)) { try { const lines = fs.readFileSync(data.transcript_path, ‘utf8’).trim().split(‘\n’); for (let i = lines.length – 1; i >= 0; i–) { if (!lines[i].trim()) continue; const entry = JSON.parse(lines[i]); const msg = entry.message || entry; if (msg && msg.role === ‘assistant’) { const text = extractTextFromContent(msg.content); if (text) { finalAnswer = text; break; } } } } catch (e) { // ignore error } } // 深度清洗:彻底清空 标签 if (finalAnswer) { finalAnswer = finalAnswer.replace(/ [\s\S]*?/gi, ”).trim(); } // 写入 Markdown if (finalAnswer) { const responseMarkdown = `### 🤖 Claude Output [${timeStr}]\n\n${finalAnswer}\n\n—\n\n`; fs.appendFileSync(mdFilePath, responseMarkdown, ‘utf8’); } } } catch (err) { // 异常静默,避免阻塞终端运行 } }); “` ## 三、 双平台部署指南 在两个平台下,核心思想都是:将 `log_prompt.js` 放到 Hook 目录下,并在 `settings.json` 中配置对 `UserPromptSubmit` 与 `Stop` 的监听。 ### 1. Windows 环境部署 #### 第一步:放置脚本 在 `C:\Users\\.claude\hooks\` 目录下保存上述脚本为 `log_prompt.js`。 – 脚本完整路径:`C:\Users\\.claude\hooks\log_prompt.js` #### 第二步:配置 `C:\Users\\.claude\settings.json` *(请将路径中的用户名替换为你真实的 Windows 用户名,例如 `subk`)* JSON “` { “env”: { “CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC”: “1” }, “includeCoAuthoredBy”: false, “model”: “opus”, “statusLine”: { “type”: “command”, “command”: “node C:/Users/subk/.claude/hooks/statusline.js” }, “hooks”: { “UserPromptSubmit”: [ { “matcher”: “”, “hooks”: [ { “type”: “command”, “command”: “node C:/Users/subk/.claude/hooks/log_prompt.js” } ] } ], “Stop”: [ { “matcher”: “”, “hooks”: [ { “type”: “command”, “command”: “node C:/Users/subk/.claude/hooks/log_prompt.js” } ] } ] } } “` ### 2. Ubuntu / Linux 环境部署 #### 第一步:放置脚本 在 `~/.claude/hooks/` 目录下保存上述脚本为 `log_prompt.js`。 Bash “` mkdir -p ~/.claude/hooks # 将脚本写入 ~/.claude/hooks/log_prompt.js “` #### 第二步:配置 `~/.claude/settings.json` JSON “` { “env”: { “CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC”: “1” }, “includeCoAuthoredBy”: false, “model”: “opus”, “statusLine”: { “type”: “command”, “command”: “~/.claude/statusline.sh” }, “hooks”: { “UserPromptSubmit”: [ { “matcher”: “”, “hooks”: [ { “type”: “command”, “command”: “node ~/.claude/hooks/log_prompt.js” } ] } ], “Stop”: [ { “matcher”: “”, “hooks”: [ { “type”: “command”, “command”: “node ~/.claude/hooks/log_prompt.js” } ] } ] } } “` 运行 ## 四、 成果展示与效果 配置完成后,打开 Terminal 正常使用 `claude` 即可。归档日志会自动保存在: “` ~/.claude/history//YYYY-MM-DD.md “` ### 目录结构预览: Plaintext “` ~/.claude/history/ ├── my-web-app/ │ ├── 2026-08-06.md │ └── 2026-08-07.md └── temp-script/ └── 2026-08-07.md “` ### 生成的 Markdown 文件样例 (`2026-08-07.md`): > # my-web-app – 对话记录 (2026-08-07) > > > **项目路径**:`/home/subk/projects/my-web-app` > > > > **创建时间**:2026-08-07 > > ## 💬 Prompt > > Plaintext > > “` > 帮我写个10行左右的HTML代码,标题是你好。其余随意填充。 > “` > > ### 🤖 Claude Output > > 已在 hello.html 生成好啦,标题是”你好”,正文简单填了两句欢迎语,一共10行。 可以看到,中间所有文件写入的过程(如 `Write(hello.html)` 过程)、命令渲染以及后台思考全被干净地剔除了,只留下了干净对齐的 **Prompt 与最终结论**。直接用 VS Code 或 Obsidian 打开就是一套排版精美的知识积累文档!