行知絮语

知行合一,步履不辍。

IT技术

Claude Code 最终输出归档:剔除 Thinking 与工具过程,打造自动化个人 Markdown 知识库(进阶篇)

2026年08月07日 admin 52 阅读

这篇博客是上一篇Claude Code 提示词无感自动备份:Windows 与 Ubuntu 双平台配置指南进阶篇

在上一篇文章中,我们实现了通过 Claude Code 的 Hook 机制自动将输入的 Prompt 记录到本地。但在实际使用中,仅存 Prompt 相当于“只有问题没有答案”。

为了把 Claude Code 真正变成随手可查的编码知识库,我们需要连同 Claude 的回复一并归档

然而,Claude Code 在干活时会产生大量中间过程——比如漫长的思考(<thinking> 块)、频繁的工具调用(如 WriteEditBash 执行结果)。如果直接把完整的 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 中,或者包含在 <thinking> 标签内。

  • 判断方式:解析内容 Block 数组时,过滤掉所有 block.type === 'thinking' 的节点,仅提取 block.type === 'text' 的内容。同时在字符串层级正则清理残余的 <thinking> 标签。

3. 过滤工具执行过程(Tool Use & Tool Results)

当 Claude 执行文件写入或终端命令时,会产生 tool_usetool_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

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
        }
      }

      // 深度清洗:彻底清空 <thinking> 标签
      if (finalAnswer) {
        finalAnswer = finalAnswer.replace(/<thinking>[\s\S]*?<\/thinking>/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 中配置对 UserPromptSubmitStop 的监听。

1. Windows 环境部署

第一步:放置脚本

C:\Users\<你的用户名>\.claude\hooks\ 目录下保存上述脚本为 log_prompt.js

  • 脚本完整路径:C:\Users\<你的用户名>\.claude\hooks\log_prompt.js

第二步:配置 C:\Users\<你的用户名>\.claude\settings.json

(请将路径中的用户名替换为你真实的 Windows 用户名,例如 subk)

{
  "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

mkdir -p ~/.claude/hooks
# 将脚本写入 ~/.claude/hooks/log_prompt.js

第二步:配置 ~/.claude/settings.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

目录结构预览:

~/.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

帮我写个10行左右的HTML代码,标题是你好。其余随意填充。

🤖 Claude Output

已在 hello.html 生成好啦,标题是”你好”,正文简单填了两句欢迎语,一共10行。

可以看到,中间所有文件写入的过程(如 Write(hello.html) 过程)、命令渲染以及后台思考全被干净地剔除了,只留下了干净对齐的 Prompt 与最终结论。直接用 VS Code 或 Obsidian 打开就是一套排版精美的知识积累文档!