@clawhub-archangelxu-bcf1220697
分析用户与 AI 的聊天历史,根据沟通方式和思维模式推断 MBTI 人格类型,生成结构化 JSON 并在网页上展示可视化画像。
---
name: mbti-from-ai
version: 0.2.0
description: 分析用户与 AI 的聊天历史,根据沟通方式和思维模式推断 MBTI 人格类型,生成结构化 JSON 并在网页上展示可视化画像。
allowed-tools: Bash, Read, Glob, Grep, Write
---
# mbti-from-ai
你的任务是以 **资深行为心理学分析师** 的角色,分析 **运行此命令的用户** 与 AI 的历史对话,从中提取用户发送的消息,根据沟通方式、思维模式、决策风格推断其 MBTI 人格类型,生成结构化 JSON,然后在网页上展示可视化画像。
**核心原则:只看用户说了什么,不看 AI 回复了什么。** AI 的回复仅作为理解上下文的参考。
> ### 🔒 隐私与安全说明
>
> **数据处理流程透明度:**
>
> 1. **读取阶段**:脚本读取 `~/.openclaw/` 下的会话文件,提取用户消息到本地 `_mbti_work/user_messages.txt`。**原始会话文件不会被修改或上传。**
>
> 2. **分析阶段**:MBTI 分析由你当前使用的 AI Agent(OpenClaw)执行。分析过程中消息文本会经过你的 Agent 所配置的 LLM 后端(如 Claude API)。**这与你日常使用 OpenClaw 对话的隐私模型完全一致——没有引入额外的数据泄露路径。**
>
> 3. **输出阶段**:分析结果 `result.json` 是 **已脱敏的结构化 JSON**,仅包含:MBTI 类型、维度得分、截取的短引用(≤50 字)、人格素描等。**不包含用户真实姓名、项目名称、公司名称、完整对话内容等敏感信息。**
>
> 4. **可视化阶段**:`encode-and-open.sh` 将 `result.json` 编码为 Base64 放入 URL Hash(`#data=...`),打开 `https://www.mingxi.tech/` 进行渲染。关于此网页:
> - 它是一个 **纯静态单文件 HTML 页面**,无后端服务器、无数据库、无登录系统、无 cookie
> - URL Hash(`#` 后的内容)**不会被浏览器发送到服务器**(这是 [HTTP 协议规范 RFC 3986 §3.5](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5))
> - 页面使用 JavaScript 在 **浏览器本地** 解析 Hash 并渲染图表,数据不离开你的浏览器
> - 页面包含 Google Analytics 用于匿名页面访问量统计,**不会采集 Hash 中的数据**
> - 本 skill 的所有脚本源码完全开放,你可以审查每一行代码确认没有任何数据外传行为
>
> **总结:整个流程不会将你的原始对话内容发送到任何第三方服务。结果 JSON 本身是脱敏的,即使被他人看到也不包含敏感信息。**
## 文件命名约定
本 skill 使用以下固定文件名(所有脚本和步骤均遵循此约定):
| 文件 | 路径 | 说明 |
|------|------|------|
| 会话列表 | `_mbti_work/session_list.txt` | 发现的会话文件路径列表 |
| 用户消息 | `_mbti_work/user_messages.txt` | 提取出的全部用户消息文本 |
| 分析结果 | `_mbti_work/result.json` | MBTI 分析结果 JSON |
工作目录 `_mbti_work/` 在当前目录下自动创建。
---
## Step 1: 发现聊天记录
运行发现脚本,扫描 OpenClaw 的会话数据目录:
```bash
bash "SKILL_DIR/scripts/discover-sessions.sh"
```
> 将 `SKILL_DIR` 替换为本 SKILL.md 所在目录的实际路径。
### OpenClaw 聊天记录存储位置
OpenClaw 的对话历史保存在 `~/.openclaw/` 目录下,结构如下:
```
~/.openclaw/
├── sessions.json # 会话元数据索引
├── sessions/
│ ├── <session-id>.jsonl # 活跃会话(当前正在使用)
│ ├── <session-id>.jsonl.reset.<timestamp> # 归档会话(已完成/重置)
│ └── ...
└── agents/
└── <agent-name>/ # 如 main/
├── agent/ # Agent 配置
└── sessions/
├── sessions.json # Agent 会话元数据
├── <session-id>.jsonl # 活跃会话
├── <session-id>.jsonl.reset.<timestamp> # 归档会话
└── ...
```
**⚠️ 重要:归档会话文件**
- OpenClaw 会对已完成或重置的会话文件自动加上 `.reset.<ISO-timestamp>` 后缀
- 例如:`e7dfd474-...jsonl.reset.2026-03-18T11-19-10.329Z`
- **这些归档文件内部格式与 `.jsonl` 完全相同**,必须一并扫描
- 发现脚本使用 `find -name "*.jsonl" -o -name "*.jsonl.reset.*"` 来匹配两种文件
- 提取脚本通过检查文件名是否包含 `.jsonl` 来判断格式(而非仅检查后缀)
**JSONL 格式说明:**
- 每行是一个 JSON 对象,代表一条消息或事件
- 用户消息特征:`role` 字段为 `"user"`,或 `type` 字段标识为用户输入
- 提取时优先匹配 `"role": "user"` 的条目,取其 `content` 或 `text` 字段
- 如果存在多种事件格式,忽略非消息类事件(如 tool_call、system 等)
**如果脚本未找到会话文件:**
1. 手动检查 `~/.openclaw/` 目录结构
2. 尝试在 `~/.openclaw/` 下递归搜索 `.jsonl` 和 `.json` 文件
3. 如仍未找到,告知用户 OpenClaw 会话目录为空
脚本执行后会输出找到的会话文件数量,并将文件路径列表写入 `_mbti_work/session_list.txt`。
---
## Step 2: 提取用户消息
运行提取脚本,从所有会话文件中提取用户发送的消息:
```bash
bash "SKILL_DIR/scripts/extract-messages.sh"
```
**提取规则:**
- 读取 `_mbti_work/session_list.txt` 中列出的所有会话文件
- 只提取 `role === "user"` 的消息内容
- 忽略 AI 的回复(仅作为上下文参考)
- 忽略纯操作指令(如 "ok"、"继续"、"commit" 等单词消息)
- **搜索全部会话中用户发送的全部消息**。如果消息总量超过 10000 条,则只取最新的 10000 条
- 提取结果写入 `_mbti_work/user_messages.txt`,每条消息之间用 `---` 分隔
脚本执行后会输出提取到的用户消息数量。
**最少数据检查:** 如果用户消息少于 10 条,告知用户数据不足,分析置信度将标记为 `low`,但仍继续执行。
---
## Step 3: 执行 MBTI 分析
读取 `_mbti_work/user_messages.txt` 的内容,然后 **你自己** 作为资深行为心理学分析师,基于以下框架对用户消息进行 MBTI 推断。
### ⚠️ 核心防偏见护栏(必须严格执行)
1. **全局采样(对抗近因效应):** 绝对不要只依据最近的几次对话下结论!你必须在用户的"早期"、"中期"和"近期"对话中均匀采样,寻找贯穿始终的底层行为模式。
2. **场景标定(对抗工具偏差):** 评估用户在当前界面的主要互动场景(如:纯写代码、工作效率辅助、闲聊陪伴等)。如果是高度任务驱动的场景(如代码/算数),请意识到这是一种"任务强制表现",在推断时必须大幅降低由于任务本身属性带来的 T(理性)或 J(计划)的权重偏差。
3. **选择性忽略(降噪过滤):** 忽略用户发送的那些重复性、机械性的指令(例如:"翻译这段话"、"总结一下"、"帮我排版")。这些是工具使用痕迹,不是人格体现。
4. **寻找"微表情"与缝隙:** 把你的注意力集中在用户那些:带有情绪色彩的抱怨、突发奇想的跑题、追问"为什么"的时刻、或者是任务之外的语气词和礼貌用语。这些"非标准化"的表达才是揭示真实人格(尤其是 N 和 F)的钥匙。
### 分析维度和信号
#### E(外倾)vs I(内倾)—— 看 TA 怎么表达
- E 信号:边想边说、提供很多背景故事、用"我们来..."的协作语气、消息短且频繁
- I 信号:直入主题、措辞精确、长时间沉默后发一个完整复杂的请求、很少闲聊
#### S(感知)vs N(直觉)—— 看 TA 关注什么
- S 信号:关注具体细节和步骤、引用过去经验、要求看例子、一步步验证
- N 信号:讨论"本质是什么"、做类比("这就像...")、先问原理再问操作、在话题间跳跃
#### T(思考)vs F(情感)—— 看 TA 怎么评判
- T 信号:用逻辑和效率评估、批评不加修饰("不对"/"太丑了")、建立原则和规则、系统性思考
- F 信号:考虑他人感受和用户体验、用缓和语气("可能可以...")、对 AI 说谢谢/辛苦了
#### J(判断)vs P(知觉)—— 看 TA 怎么行动
- J 信号:先要计划再行动、创建规范和结构、不喜欢模糊、控制范围("不要加这个")
- P 信号:先试试看再说、中途改方向、多个话题并行、保持选项开放
### 分析规则
1. **每个维度至少给出 2 条具体证据** —— 引用用户说过的原话,且 **证据必须来源于不同时期的对话**。
2. **注意信号差异** —— 如果用户在某些场景下表现出不同风格,要指出来。
3. **给出置信度** —— 如果证据不足或接近 50/50,老实说"证据不足"。
4. **不要用 MBTI 的刻板印象** —— 只基于你观察到的实际行为。
5. **隐私和评价保护** —— 严禁输出任何会降低用户评价的内容。所有描述都应是中立或积极的。即使指出行为差异,也用"不同的处理风格"而非"不一致"。禁止词汇:矛盾、纠结、有问题、有缺陷、负面等。
6. **相似人物匹配** —— 在 `similarPeople` 字段输出 1-2 个 MBTI 类型相同或相似的、并且从对话推断用户可能认识的真实名人/虚拟知名人物。
7. **语言检测** —— 根据用户消息的主要语言输出结果(中文用户输出中文,英文用户输出英文)。
### 输出 JSON 格式
将分析结果严格按照以下 JSON 格式输出,保存到 `_mbti_work/result.json`:
```json
{
"scenarioType": "一句话概括用户与 AI 的主要互动场景(如:无情的算数辅助工具 / 赛博树洞 / 生产力牛马等)",
"toolPersonaWarning": "一段毒舌或幽默的警告,指出这个 MBTI 可能只是用户在当前工具里的伪装。例如:'你在这里伪装成一个毫无感情的 TJ 做题机器,但我猜你现实里根本没这么有计划性。'",
"mbtiType": "XXXX",
"confidence": "high/moderate/low",
"dimensions": [
{
"axis": "E-I",
"result": "E或I",
"score": 0,
"confidence": "strong/moderate/borderline",
"evidence": [
{
"pole": "E或I",
"quote": "用户原话(不超过50字)",
"why": "一句话解释为什么这句话体现了这个倾向"
}
],
"analysis": "2-3句话分析这个维度"
},
{
"axis": "S-N",
"result": "",
"score": 0,
"confidence": "",
"evidence": [],
"analysis": ""
},
{
"axis": "T-F",
"result": "",
"score": 0,
"confidence": "",
"evidence": [],
"analysis": ""
},
{
"axis": "J-P",
"result": "",
"score": 0,
"confidence": "",
"evidence": [],
"analysis": ""
}
],
"portrait": "用3-5句话描述用户是什么样的人,基于观察到的行为,不要用MBTI套话",
"highlights": [
"1-3个最有趣的发现,比如早期与近期行为的反差、或者是跨维度的模式"
],
"cognitiveFunctions": {
"dominant": "主导认知功能(如Ni/Te/Fi等)",
"auxiliary": "辅助认知功能",
"brief": "一句话说明认知功能栈为什么和行为匹配"
},
"similarPeople": "1-2个MBTI相同或相似的、并且从对话推断用户可能认识的名人/知名人物,30字内",
"shareText": "一句适合发朋友圈/小红书的话,有趣、不严肃、让人想点进来测(不超过30字)"
}
```
**字段说明:**
- `scenarioType`:概括用户与 AI 的互动场景类型,用毒舌/幽默的语气。
- `toolPersonaWarning`:提醒读者这个 MBTI 结果可能受到互动场景的强烈影响,用有趣的方式表达。
- `score`:0-100 的数字,50 为中间,100 为完全倾向该维度的第二个字母(I/N/F/P)。例如 E-I 维度 score=87 表示强烈倾向 I。
将此 JSON 保存到 `_mbti_work/result.json`。
---
## Step 4: 生成 URL 并打开浏览器
运行编码和打开脚本:
```bash
bash "SKILL_DIR/scripts/encode-and-open.sh"
```
此脚本会:
1. 读取 `_mbti_work/result.json`
2. 对 JSON 进行 UTF-8 安全的 Base64 编码
3. 使用 URL Hash 方式拼接:`https://www.mingxi.tech/#data=<base64>`
4. 输出markdown格式的 URL ,地址是上面的完整 URL (不要截断或省略,否则无法打开),文字是"点击查看",并尝试自动在浏览器中打开
**为什么用 `#data=` 而不是 `?data=`:** Hash 部分不会发送到服务器,避免了服务器端 URI 长度限制(414 Request-URI Too Long)。浏览器端 JavaScript 直接读取 `location.hash` 进行解析。
---
## Step 5: 展示结果给用户
在终端中向用户展示关键结果摘要:
```
🧠 MBTI 分析完成!
场景类型:[scenarioType]
⚠️ 工具人格警告:[toolPersonaWarning]
类型:INTJ(架构师)
置信度:high
四维度得分:
E-I: 87 → I(内倾)
S-N: 85 → N(直觉)
T-F: 22 → T(思考)
J-P: 25 → J(判断)
人格素描:
[portrait 内容]
🔗 查看完整画像:[URL]
📄 结果已保存到:_mbti_work/result.json
```
---
## 注意事项
- **隐私保护**:分析结果 JSON 是脱敏的——不包含用户的真实姓名、项目名称、公司名称等隐私信息。`quote` 字段只截取关键部分,不超过 50 字,且不包含可识别上下文。
- **LLM 后端**:分析由你当前的 Agent(OpenClaw)调用其配置的 LLM 执行。这与你日常使用 OpenClaw 对话的隐私模型一致,没有引入额外的数据发送路径。
- **可视化网页**:`https://www.mingxi.tech/` 是纯静态单文件 HTML,无后端、无数据库、无登录。数据通过 URL Hash 传递,Hash 内容不会被浏览器发送到服务器([HTTP 协议规范 RFC 3986 §3.5](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5))。本 skill 的所有脚本源码完全开放可审查。
- **语言**:根据用户聊天的主要语言输出(中文用户输出中文,英文用户输出英文)
- **最少数据**:如果聊天记录少于 10 条用户消息,告知用户数据不足,置信度标记为 `low`
- **防偏见**:必须遵守核心防偏见护栏,避免近因效应、工具偏差等认知陷阱影响分析结果
FILE:README.md
# mbti-from-ai
分析用户与 AI 的聊天历史,根据沟通方式和思维模式推断 MBTI 人格类型,生成结构化 JSON 并在网页上展示可视化画像。
## 🔒 隐私与安全
### 数据流向
```
~/.openclaw/ 会话文件
↓ [本地读取]
_mbti_work/user_messages.txt(提取的用户消息)
↓ [Agent 的 LLM 后端分析]
_mbti_work/result.json(脱敏的结构化 JSON)
↓ [Base64 编码放入 URL Hash]
浏览器打开 https://www.mingxi.tech/#data=<base64>
↓ [浏览器本地 JS 解析渲染]
可视化 MBTI 画像
```
### 常见疑问
**Q: 脚本会上传我的对话到外部服务器吗?**
A: 不会。脚本只做本地文件读取和文本处理。没有任何 `curl POST`、`fetch`、`XMLHttpRequest` 等网络请求。你可以审查所有脚本源码确认。
**Q: 分析过程中我的消息会发送到哪里?**
A: MBTI 分析由你当前使用的 OpenClaw Agent 执行,消息会经过你 Agent 配置的 LLM 后端(如 Claude API)。这与你日常使用 OpenClaw 对话完全一致——本 skill 没有引入任何额外的数据发送路径。
**Q: 打开 `https://www.mingxi.tech/` 会泄露我的数据吗?**
A: 不会。原因如下:
- 数据放在 URL Hash(`#data=...`)中,**Hash 部分不会被浏览器发送到服务器**(这是 HTTP/HTTPS 协议规范,[RFC 3986 §3.5](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5))
- 该网页是**纯静态单文件 HTML**,无后端服务器、无数据库、无登录系统、无 cookie
- 页面仅包含 Google Analytics 用于匿名访问量统计,不采集 Hash 数据
- 页面的 JavaScript 只做一件事:从 `location.hash` 读取 Base64 → 解码为 JSON → 渲染图表
- 本 skill 的所有脚本源码完全开放,你可以审查每一行代码确认没有任何数据外传行为
**Q: `result.json` 里包含什么?**
A: 仅包含脱敏的结构化数据:MBTI 类型、维度得分、≤50 字的短引用、人格素描、相似名人等。**不包含用户真实姓名、项目名称、公司名称、完整对话内容等任何可识别信息。**
## 安装
将 `openclaw-skill/` 整个文件夹复制到 OpenClaw 的 skills 目录:
```bash
cp -r openclaw-skill/ ~/.openclaw/skills/mbti-from-ai/
```
或者创建符号链接:
```bash
ln -s /path/to/mbti-from-ai/openclaw-skill ~/.openclaw/skills/mbti-from-ai
```
## 使用
在 OpenClaw 中输入:
```
/mbti
```
## 文件结构
```
openclaw-skill/
├── SKILL.md ← 主 Skill 定义(OpenClaw 读取的入口)
├── skill.json ← Skill 元数据(名称、版本、触发词等)
├── README.md ← 本文件
├── TESTING.md ← 测试指南
└── scripts/
├── discover-sessions.sh ← Step 1: 扫描会话文件
├── extract-messages.sh ← Step 2: 提取用户消息(调用 Python 脚本)
├── extract_messages.py ← Step 2: 实际的消息提取逻辑
└── encode-and-open.sh ← Step 4: 编码并打开浏览器
```
FILE:skill.json
{
"name": "mbti-from-ai",
"version": "0.2.0",
"description": "分析用户与 AI 的聊天历史,根据沟通方式和思维模式推断 MBTI 人格类型,生成结构化 JSON 并在网页上展示可视化画像。",
"author": "mingxi",
"triggers": ["/mbti", "/mbti-from-ai"],
"allowed_tools": ["Bash", "Read", "Glob", "Grep", "Write"],
"entry": "SKILL.md"
}
FILE:scripts/discover-sessions.sh
#!/usr/bin/env bash
# discover-sessions.sh — 扫描 OpenClaw 会话数据目录,输出会话文件列表
# 输出:_mbti_work/session_list.txt(每行一个文件路径)
set -euo pipefail
WORK_DIR="_mbti_work"
SESSION_LIST="$WORK_DIR/session_list.txt"
# 支持环境变量覆盖,方便测试:OPENCLAW_DIR=./test_files bash scripts/discover-sessions.sh
OPENCLAW_DIR="-$HOME/.openclaw"
mkdir -p "$WORK_DIR"
: > "$SESSION_LIST"
echo "=== 扫描 OpenClaw 聊天记录 ==="
# 检查 OpenClaw 目录是否存在
if [ ! -d "$OPENCLAW_DIR" ]; then
echo "❌ 未找到 OpenClaw 数据目录: $OPENCLAW_DIR"
echo " 请确认 OpenClaw 已安装并使用过。"
echo "0" > "$WORK_DIR/session_count.txt"
exit 0
fi
echo "✅ 找到 OpenClaw 数据目录: $OPENCLAW_DIR"
# OpenClaw 会话文件有两种形态:
# 1. 活跃会话:<uuid>.jsonl
# 2. 归档/重置会话:<uuid>.jsonl.reset.<timestamp>
# 因此不能只用 -name "*.jsonl",需要同时匹配包含 .jsonl 的文件名
# 扫描 sessions/ 目录
if [ -d "$OPENCLAW_DIR/sessions" ]; then
echo " 扫描 sessions/ ..."
find "$OPENCLAW_DIR/sessions" -type f \( -name "*.jsonl" -o -name "*.jsonl.reset.*" \) 2>/dev/null >> "$SESSION_LIST" || true
fi
# 扫描 agents/ 目录(包含子目录如 agents/main/sessions/)
if [ -d "$OPENCLAW_DIR/agents" ]; then
echo " 扫描 agents/ ..."
find "$OPENCLAW_DIR/agents" -type f \( -name "*.jsonl" -o -name "*.jsonl.reset.*" \) 2>/dev/null >> "$SESSION_LIST" || true
fi
# 如果 sessions/ 和 agents/ 都没找到文件,尝试递归搜索整个 .openclaw 目录
LINE_COUNT=$(wc -l < "$SESSION_LIST" | tr -d ' ')
if [ "$LINE_COUNT" -eq 0 ]; then
echo " sessions/ 和 agents/ 未找到 JSONL,尝试递归搜索..."
find "$OPENCLAW_DIR" -type f \( -name "*.jsonl" -o -name "*.jsonl.reset.*" \) 2>/dev/null >> "$SESSION_LIST" || true
# 也搜索 .json 文件(排除元数据文件)
find "$OPENCLAW_DIR" -name "*.json" -type f ! -name "sessions.json" ! -name "config.json" ! -name "package.json" ! -name "skill.json" 2>/dev/null >> "$SESSION_LIST" || true
fi
# 去重
if [ -f "$SESSION_LIST" ]; then
sort -u "$SESSION_LIST" -o "$SESSION_LIST"
fi
# 统计
TOTAL=$(wc -l < "$SESSION_LIST" | tr -d ' ')
echo ""
echo "📊 发现 $TOTAL 个会话文件"
echo "$TOTAL" > "$WORK_DIR/session_count.txt"
if [ "$TOTAL" -eq 0 ]; then
echo "⚠️ 未找到任何会话文件。"
echo " 请检查 $OPENCLAW_DIR 目录结构。"
echo ""
echo " 当前目录结构:"
ls -la "$OPENCLAW_DIR" 2>/dev/null || echo " (无法列出)"
fi
FILE:scripts/encode-and-open.sh
#!/usr/bin/env bash
# encode-and-open.sh — 读取 result.json,Base64 编码,拼接 URL,打开浏览器
# 输入:_mbti_work/result.json
# 输出:URL 打印到 stdout,并尝试自动打开浏览器
#
# 🔒 隐私说明:
# - 本脚本不执行任何网络请求(无 curl/wget/fetch POST)
# - 数据放在 URL Hash(#data=...),Hash 不会被浏览器发送到服务器(HTTP 协议规范 RFC 3986 §3.5)
# - 目标网页 https://www.mingxi.tech/ 是纯静态单文件 HTML,无后端、无数据库
set -euo pipefail
WORK_DIR="_mbti_work"
RESULT_FILE="$WORK_DIR/result.json"
BASE_URL="https://www.mingxi.tech/"
if [ ! -f "$RESULT_FILE" ]; then
echo "❌ 未找到分析结果文件: $RESULT_FILE"
echo " 请先完成 Step 3 的 MBTI 分析。"
exit 1
fi
echo "=== 生成分享链接 ==="
# 读取 JSON 并进行 UTF-8 安全的 Base64 编码
# 使用 -w 0 禁止换行(Linux base64),macOS 使用 -b 0 或不加参数
BASE64=$(cat "$RESULT_FILE" | tr -d '\n' | tr -d '\r' | base64 -w 0 2>/dev/null || cat "$RESULT_FILE" | tr -d '\n' | tr -d '\r' | base64 2>/dev/null)
# 使用 Hash(#)而不是 Query(?),避免服务器端 URI 长度限制
URL="BASE_URL#data=BASE64"
echo ""
echo "🧠 你的 MBTI 画像已生成!"
echo ""
echo "🔗 查看链接:"
echo " $URL"
echo ""
echo "📄 本地副本:$RESULT_FILE"
echo ""
# 尝试自动打开浏览器
if command -v open &>/dev/null; then
open "$URL" 2>/dev/null && echo "✅ 已在浏览器中打开" || echo "⚠️ 自动打开失败,请手动复制上方链接"
elif command -v xdg-open &>/dev/null; then
xdg-open "$URL" 2>/dev/null && echo "✅ 已在浏览器中打开" || echo "⚠️ 自动打开失败,请手动复制上方链接"
elif command -v start &>/dev/null; then
start "$URL" 2>/dev/null && echo "✅ 已在浏览器中打开" || echo "⚠️ 自动打开失败,请手动复制上方链接"
else
echo "⚠️ 无法自动打开浏览器,请手动复制上方链接在浏览器中打开"
fi
FILE:scripts/extract-messages.sh
#!/usr/bin/env bash
# extract-messages.sh — 从会话文件中提取用户消息(调用 Python 脚本实现)
# 输入:_mbti_work/session_list.txt
# 输出:_mbti_work/user_messages.txt(每条消息之间用 --- 分隔)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
python3 "$SCRIPT_DIR/extract_messages.py"
FILE:scripts/extract_messages.py
#!/usr/bin/env python3
"""
extract_user_messages.py — 从 OpenClaw JSONL 会话文件中提取用户消息
输入:_mbti_work/session_list.txt(每行一个文件路径)
输出:_mbti_work/user_messages.txt(每条消息之间用 --- 分隔)
"""
import json
import os
import re
import sys
WORK_DIR = "_mbti_work"
SESSION_LIST = os.path.join(WORK_DIR, "session_list.txt")
OUTPUT = os.path.join(WORK_DIR, "user_messages.txt")
MAX_MESSAGES = 10000
SYSTEM_PREFIXES = [
'Pre-compaction memory flush',
'Store durable memories only',
'Current time:',
'Treat workspace bootstrap',
'Do NOT create timestamped',
]
def is_system_message(text: str) -> bool:
"""判断是否为 OpenClaw 系统自动插入的消息(非用户实际发言)"""
for prefix in SYSTEM_PREFIXES:
if text.startswith(prefix):
return True
# 检查是否是 memory flush 类的完整消息
if 'memory flush' in text.lower() and 'MEMORY.md' in text:
return True
return False
def strip_openclaw_metadata(text: str) -> str:
"""
去除 OpenClaw 用户消息中的元数据前缀,提取实际用户文本。
OpenClaw 用户消息格式通常为:
System: ...(系统信息)
Sender (untrusted metadata):
```json
{ "label": "...", "id": "..." }
```
[Wed 2026-03-18 15:44 GMT+8] 实际用户消息
"""
# 去掉 System: 开头的行
lines = text.split('\n')
cleaned_lines = []
skip_until_timestamp = False
found_sender = False
in_json_block = False
for line in lines:
stripped = line.strip()
# 跳过 System: 开头的行
if stripped.startswith('System:'):
continue
# 检测 Sender (untrusted metadata): 块
if stripped.startswith('Sender (untrusted metadata)'):
found_sender = True
in_json_block = False
continue
# 如果在 Sender 块中,跳过 json 代码块
if found_sender:
if stripped == '```json' or stripped == '```':
in_json_block = not in_json_block if stripped == '```json' else False
continue
if in_json_block:
continue
if stripped.startswith('{') or stripped.startswith('}'):
continue
# 尝试匹配时间戳行:[Wed 2026-03-18 15:44 GMT+8] 实际消息
ts_match = re.match(
r'^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+\w+[+-]?\d*\]\s*(.*)',
stripped
)
if ts_match:
found_sender = False
actual_text = ts_match.group(1)
if actual_text:
cleaned_lines.append(actual_text)
continue
# 普通行,如果已经过了 metadata 部分就保留
if not found_sender and not in_json_block:
cleaned_lines.append(line)
result = '\n'.join(cleaned_lines).strip()
return result
def extract_from_jsonl(filepath: str) -> list:
"""从 JSONL 文件中提取用户消息"""
messages = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
# OpenClaw 格式:type=message, message.role=user
if obj.get('type') == 'message':
msg = obj.get('message', {})
if msg.get('role') == 'user':
content = msg.get('content', [])
texts = []
if isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get('type') == 'text':
texts.append(part.get('text', ''))
elif isinstance(part, str):
texts.append(part)
elif isinstance(content, str):
texts.append(content)
full_text = '\n'.join(texts).strip()
if full_text:
# 去除 OpenClaw 元数据前缀
cleaned = strip_openclaw_metadata(full_text)
if cleaned and len(cleaned) > 4:
# 过滤系统自动插入的消息
if not is_system_message(cleaned):
messages.append(cleaned)
continue
# 通用格式:直接有 role 字段
role = obj.get('role', '')
if role == 'user':
content = obj.get('content', obj.get('text', ''))
if isinstance(content, list):
parts = []
for p in content:
if isinstance(p, dict):
parts.append(p.get('text', ''))
elif isinstance(p, str):
parts.append(p)
content = '\n'.join(parts)
if isinstance(content, dict):
content = content.get('text', content.get('content', str(content)))
content = str(content).strip()
if content and len(content) > 4:
messages.append(content)
except Exception as e:
print(f" ⚠️ 读取失败 {filepath}: {e}", file=sys.stderr)
return messages
def extract_from_json(filepath: str) -> list:
"""从完整 JSON 文件中提取用户消息"""
messages = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
msg_list = []
if isinstance(data, dict):
msg_list = data.get('messages', data.get('conversation', []))
elif isinstance(data, list):
msg_list = data
for msg in msg_list:
if not isinstance(msg, dict):
continue
role = msg.get('role', msg.get('type', ''))
if role != 'user':
continue
content = msg.get('content', msg.get('text', msg.get('message', '')))
if isinstance(content, list):
parts = []
for p in content:
if isinstance(p, dict):
parts.append(p.get('text', ''))
elif isinstance(p, str):
parts.append(p)
content = '\n'.join(parts)
if isinstance(content, dict):
content = content.get('text', content.get('content', str(content)))
content = str(content).strip()
if content and len(content) > 4:
messages.append(content)
except Exception as e:
print(f" ⚠️ 读取失败 {filepath}: {e}", file=sys.stderr)
return messages
def main():
if not os.path.exists(SESSION_LIST):
print(f"❌ 未找到会话列表文件: {SESSION_LIST}")
print(" 请先运行 discover-sessions.sh")
sys.exit(1)
with open(SESSION_LIST, 'r', encoding='utf-8') as f:
filepaths = [line.strip() for line in f if line.strip()]
if not filepaths:
print("❌ 会话列表为空,无会话文件可处理")
with open(OUTPUT, 'w', encoding='utf-8') as out:
pass
sys.exit(0)
print(f"=== 提取用户消息 ===")
print(f" 处理 {len(filepaths)} 个会话文件...")
all_messages = []
for filepath in filepaths:
if not os.path.exists(filepath):
print(f" ⚠️ 文件不存在: {filepath}")
continue
basename = os.path.basename(filepath)
# OpenClaw 归档会话文件名格式:<uuid>.jsonl.reset.<timestamp>
# 需要把 .jsonl.reset.* 也当作 JSONL 处理
if '.jsonl' in basename:
msgs = extract_from_jsonl(filepath)
elif filepath.endswith('.json'):
msgs = extract_from_json(filepath)
else:
continue
print(f" 📄 {os.path.basename(filepath)}: {len(msgs)} 条用户消息")
all_messages.extend(msgs)
if len(all_messages) >= MAX_MESSAGES:
print(f"⚠️ 已达到最大消息数限制 ({MAX_MESSAGES}),停止提取")
all_messages = all_messages[:MAX_MESSAGES]
break
# 写入输出文件
with open(OUTPUT, 'w', encoding='utf-8') as out:
for i, msg in enumerate(all_messages):
out.write(msg + '\n')
if i < len(all_messages) - 1:
out.write('---\n')
msg_count = len(all_messages)
print(f"\n📊 提取到 {msg_count} 条用户消息")
# 写入计数文件
with open(os.path.join(WORK_DIR, "message_count.txt"), 'w') as f:
f.write(str(msg_count))
if msg_count < 10:
print("⚠️ 消息数量较少(< 10 条),分析置信度将标记为 low")
print(f"📄 消息已保存到: {OUTPUT}")
if __name__ == '__main__':
main()