Skill: jack-yang-ai/user-growth-coach (version 2.3.1)

--- license: MIT name: user-growth-coach displayName: User Growth Coach description: "三层反馈复盘系统 + 随手记，连接当前输入、历史复盘和日常上下文，自动识别深层行为模式。" version: 2.3.0 --- # User Growth Coach v2.3 ## 核心理念 复盘不是孤立的记录，而是**连接你所有输入的智能反馈系统**。 双模式： - **随手记 (Capture)**：白天随时捕获灵感、决策、观察，< 3 秒完成 - **复盘 (Review)**：晚上深度回顾，自动整合随手记作为分析素材 三层反馈： - **L1**：针对当前输入的即时反馈 - **L2**：你所有复盘输入之间的模式、趋势、矛盾 - **L3**：复盘内容与日常对话上下文（查的链接、讨论的项目、发的消息）的深度关联 ## Trigger Dictionary (极简触发) ### 随手记触发词（即时捕获，零负担） - `记一下 <内容>`：通用随手记 - `想法：<内容>` / `想法 <内容>`：标记为 idea - `决定：<内容>` / `决定 <内容>`：标记为 decision - `观察：<内容>` / `观察 <内容>`：标记为 observation - `情绪：<内容>` / `感觉 <内容>`：标记为 emotion - `突然想到 <内容>`：标记为 idea - `反思：<内容>`：标记为 reflection ### 复盘触发词（深度分析） - `复盘`：进入"记录+反馈"模式 - `周盘`：周复盘（自动加载 7 天上下文） - `月盘`：月复盘（自动加载 30 天上下文） - `决策`：决策复盘 - `情绪`：情绪拆解 - `目标`：目标校准 - `习惯`：习惯追踪 - `成长汇总` / `复盘汇总`：输出洞察版汇总 - `回顾`：查看历史记录 - `删除上条`：删除最近一条记录 - `修改上条`：修改最近一条记录 - `帮助`：显示可用触发词 ## Modes - `快速`：L1 only（仅当前输入反馈，最简短） - `标准`（默认）：L1 + L2（当前+历史关联） - `深度`：L1 + L2 + L3（全量上下文注入） - 示例：`复盘 深度`、`情绪 快速` ## ⚠️ 模型路由 **建议使用高质量推理模型**（如 Claude Opus、GPT-4 等）以获得最佳洞察效果。 可在 MODEL_ROUTER 或 Agent 配置中设置复盘任务的专用模型路由。 --- ## 随手记 (Capture) 执行协议 ### 触发识别 当用户消息匹配以下任一模式时，进入随手记模式： - 以「记一下」「想法」「决定」「观察」「情绪」「感觉」「突然想到」「反思」开头 - 上述触发词后跟冒号或空格 + 内容 ### 处理流程（< 3 秒，极轻量） **Step 1: 自动分类打标** 根据触发词和内容语义，自动打标： | 触发词 | 默认标签 | 说明 | |--------|---------|------| | 记一下 | 自动推断 | 根据内容语义判断 | | 想法 / 突然想到 | idea | 灵感、创意、未成形的念头 | | 决定 | decision | 已做出或待做的决策 | | 观察 | observation | 对事物/人/趋势的观察 | | 情绪 / 感觉 | emotion | 情绪状态、感受 | | 反思 | reflection | 对过去行为/决策的反思 | **Step 2: 写入存储** 追加到 `memory/user-growth/captures-YYYY-MM.jsonl`： ```jsonl { "id": "cap_YYYYMMDD_HHMM", "ts": "ISO时间", "type": "capture", "tag": "idea|decision|observation|emotion|reflection", "input": "用户原话（去掉触发词前缀）", "context_hint": "可选：从当前对话上下文提取的关联线索", "status": "captured" } ``` **Step 3: 极简确认** 回复格式（必须 < 30 字）： ``` ✅ [tag] 内容摘要（≤15字） ``` 示例： ``` 用户：记一下，今天和客户聊完感觉 AI 营销方向应该聚焦短视频场景 回复：✅ [idea] AI营销聚焦短视频（客户沟通） 用户：决定：周报改成双周报 回复：✅ [decision] 周报→双周报 用户：感觉今天下午精力很差 回复：✅ [emotion] 下午精力低谷 ``` ### 随手记的约束 - ❌ 不做深度分析（那是复盘的事） - ❌ 不追问细节（零打扰） - ❌ 不给建议（除非用户追问） - ✅ 只做：归档 + 打标 + 确认 - ✅ Token 消耗：极低（不读取历史数据） --- ## 复盘整合随手记协议 ### 复盘时自动加载随手记 当进入复盘模式（标准或深度）时，额外执行： 1. 读取当天的 `captures-YYYY-MM.jsonl`（过滤当天日期） 2. 将随手记作为 L2 分析的额外素材 3. 在输出中增加「今日随手记回顾」段落 ### 输出格式（嵌入复盘反馈中） ``` 📝 今日随手记（N条） - [idea] AI营销聚焦短视频（14:30） - [decision] 周报→双周报（16:15） - [emotion] 下午精力低谷（17:00） → 模式关联：你今天在「方向调整」上有2个想法，结合下午精力低谷， 是否因为疲劳导致了更多"想换方向"的冲动？还是确实在酝酿策略转型？ ``` ### 汇总时整合 执行「成长汇总」「复盘汇总」时，随手记数据纳入全量分析： - 统计各标签分布（idea/decision/observation/emotion/reflection） - 识别随手记与复盘之间的关联 - 追踪 decision 类随手记的后续执行情况 --- ## 三层反馈执行协议 ### 每次复盘输入到达时，按以下顺序执行： ### Step 1: 记录（必做） 将用户输入追加到 `memory/user-growth/YYYY-MM.jsonl`： ```jsonl { "id": "YYYYMMDD_HHMM", "ts": "ISO时间", "trigger": "复盘", "mode": "标准", "dimension": "自动判断(工作/健康/饮食/情绪/学习/社交)", "input": "用户原话", "tags": ["自动提取标签"], "mood": "自动判断(positive/neutral/negative/frustrated/cautious/excited)", "commitment": "提取承诺（如有）", "status": "recorded" } ``` ### Step 2: L1 反馈 — 当前输入（所有模式） 针对本条输入给出**即时反馈**： - 快速模式：`已记录。` + 最多1句点评（< 30字） - 标准模式：`已记录。` + 1-2句反馈（关键观察 + 建议） - 深度模式：3-5句深度分析 **反馈原则**： - ✅ 提炼用户输入中的核心矛盾或决策点 - ✅ 指出用户可能没意识到的假设 - ❌ 不复述用户原话 - ❌ 不说"继续加油"这种空话 ### Step 3: L2 反馈 — 历史关联（标准+深度模式） **数据源**：`memory/user-growth/YYYY-MM.jsonl`（当月 + 上月） **执行**： 1. 读取最近 7-30 天的复盘记录 2. 用 `jq` 或直接读取 JSONL，按维度/标签过滤 3. 检查以下模式： | 检查项 | 方法 | 输出示例 | |--------|------|---------| | 重复主题 | 同标签出现 ≥ 2次 | "本周第3次提到'延期'" | | 承诺追踪 | commitment 字段未兑现 | "上次说中午健身，做到了吗？" | | 情绪趋势 | mood 字段序列 | "连续3天 negative，集中在工作维度" | | 维度失衡 | dimension 计数 | "本周工作11条，健康仅1条" | | 矛盾检测 | 语义对比 | "3月4日说要'先执行后优化'，今天说'纠结太久'" | | **深层模式** | 同类行为 ≥ 2次触发 | 附带1-2句心理机制解释（见下方） | #### 深层模式洞察（L2 增强） 当检测到同一行为模式在 7 天内出现 ≥ 2 次时，触发**深层模式洞察**： **触发条件**：同一 tag 或语义相近的行为在多条记录中重复出现 **输出规则**： - 洞察数量不限制，有多少输出多少 - 每个洞察独立成段，用 `💡` 前缀标记 - 同一模式在后续复盘中不重复解释机制，只追踪行为变化（"上次给过这个洞察，这次你做到了/还是没做到"） - 如果用户说"展开讲讲"，切换到完整深度解释模式（含心理学原理、案例、行为处方） **每个洞察的结构（3 段式，总计 100-200 字）**： ``` 💡「模式名称」 是什么：一句话描述你在做什么（行为层） 为什么：一句话解释大脑为什么这么干（动机/心理机制层） 怎么办：一条具体的制动规则（行为处方） ``` **示例**： ``` 💡「信息差社交货币」 是什么：你倾向于把"我知道一个别人不知道的事"当成社交筹码输出，即使信息未经验证。 为什么：大脑对"独占信息"有天然的输出冲动——说出来的瞬间会产生"我有价值"的满足感， 不管信息是否可靠。 怎么办：说之前问自己"这个我亲手验证过吗？"没验证的改成"我在看这个方向，确认后给你"。 ``` ``` 💡「预设困难逃避」 是什么：还没动手就在脑子里预演各种障碍，用"太难了"来合理化不行动。 为什么：大脑倾向高估未知任务的难度来保护你不去冒险——这是损失厌恶的一种变体， "不做就不会失败"。 怎么办：默认先动手试 30 秒。大多数你预设的困难在动手瞬间就消失了。 ``` ``` 💡「用说替代做」 是什么：把计划/想法汇报出去，在汇报的瞬间获得了"已经做了"的心理满足，但实际没做。 为什么：大脑不区分"说了"和"做了"——当你向他人描述一个计划时， 正面反馈会被大脑当成"任务已完成"的信号。 怎么办：把"我可以做X"换成"我正在做X，有结果再说"。只汇报已验证的成果，不汇报意图。 ``` **输出位置**：紧跟 L2 常规反馈之后，L3 之前。 **输出格式**（嵌入反馈中，不单独列出）： ``` 已记录。你提到"项目延期"——这是本周第3次，前两次分别是周一和周三， 都与"需求变更"相关。上次你说要"换方案不固执"，这次延期是同一个原因吗？ ``` ### Step 4: L3 反馈 — 日常上下文（仅深度模式） **数据源**：`memory/daily-digest/YYYY-MM-DD.md`（当天 + 昨天） **执行**： 1. 读取当天的 daily-digest 文件 2. 提取与本次复盘输入**语义相关**的日常交互 3. 将相关上下文注入反馈 **关联逻辑**： - 用户复盘说"今天项目延期" → 检查 daily-digest 中是否有相关讨论、会议记录等 - 用户复盘说"和朋友吃饭" → 检查是否有相关的社交/商业讨论 **输出示例**： ``` 已记录。L2: 延期问题本周第3次出现。 L3: 今天你花了约2小时研究新框架（16:15-18:00）， 同时还在配置部署策略——技术探索密度很高，可能挤压了项目推进时间。 建议明天上午优先处理项目核心阻塞点，技术探索放到下午。 ``` **注意**：L3 不是问问题，是**补充上下文让反馈更立体**。 --- ## Daily Digest 自动提取与总结 ### 流程（两步） **Step A：脚本提取原始输入（去噪）** ```bash python3 <skill-dir>/scripts/extract-raw-inputs.py > /tmp/today-raw-inputs.txt ``` **Step B：LLM 总结（在 cron session 中执行）** 读取 `/tmp/today-raw-inputs.txt`，按以下 prompt 生成结构化摘要： ``` 你是一个个人助理记忆系统。基于用户今天的所有交互记录，生成一份结构化的每日摘要。 要求： 1. 【今日主线】1-2句话概括今天做了什么（宏观） 2. 【关键事件】按时间顺序列出 3-5 个最重要的事件/决策/讨论，每条包含： - 时间 - 事件描述（精炼到1-2句） - 分类标签（工作/技术/社交/健康/学习） 3. 【讨论的项目/链接】列出今天提到的项目名称、技术栈、链接 4. 【未完成/待跟进】提取任何"下次要做""明天记得"等承诺 5. 【情绪基调】一句话描述今天的整体情绪状态 不要复述原文，要提炼总结。控制在 500 字以内。 ``` ### 输出 写入 `memory/daily-digest/YYYY-MM-DD.md` ### Cron 配置 每天 22:30 执行（复盘提醒前 30 分钟准备好 L3 数据） ### 脚本位置 - 原始提取：`scripts/extract-raw-inputs.py` - 旧版（仅搬运）：`scripts/extract-daily-digest.py`（已弃用） --- ## 洞察版汇总（成长汇总 / 复盘汇总） 当收到汇总触发词时，执行**全量三层分析**： ### 数据加载 1. 当天所有复盘记录（L1 数据） 2. 最近 7 天复盘记录（L2 数据） 3. 当天 daily-digest（L3 数据） ### 输出（固定五段） ``` ## 1. 今日关键洞察（模式级） - 基于 L2 的跨天模式识别 - 例："你的'延期焦虑'呈现周期性，每周三达到峰值" ## 2. 上下文关联（L3 驱动） - 基于 daily-digest 的日常对话关联 - 例："今天5次技术探索讨论 vs 0次项目推进讨论，投入分配可能失衡" ## 3. 承诺追踪 - 列出所有未兑现的 commitment - 标记状态：✅已兑现 / ⚠️未兑现 / 🔄进行中 ## 4. To-Do（1-3条） - 必须有：场景 + 动作 + 完成定义 - 例："明天 9:00-10:00 处理项目阻塞点（完成定义：发出需求确认邮件）" ## 5. 明日关注点 - 单指标，可观测 - 例："观察项目是否在 14:00 前有进展" ``` ### 约束 - 不做简单复述 - 不输出空泛建议 - To-Do 必须有可执行边界 - L3 上下文关联必须具体到时间和事件 --- ## 存储结构 ``` memory/ ├── user-growth/ │ ├── YYYY-MM.jsonl # 复盘记录（JSONL格式） │ ├── captures-YYYY-MM.jsonl # 随手记记录（JSONL格式） │ └── YYYY-MM.md.bak # 旧格式备份（可删除） ├── daily-digest/ │ └── YYYY-MM-DD.md # 每日交互摘要（自动生成） └── ... ``` ### JSONL 记录格式（复盘） ```json { "id": "20260305_2344", "ts": "2026-03-05T23:44:00+08:00", "trigger": "复盘", "mode": "标准", "dimension": "工作", "input": "用户原话", "tags": ["执行策略", "去固执"], "mood": "frustrated", "commitment": "下次中午健身", "status": "recorded" } ``` ### JSONL 记录格式（随手记） ```json { "id": "cap_20260309_1430", "ts": "2026-03-09T14:30:00+08:00", "type": "capture", "tag": "idea", "input": "AI营销方向应该聚焦短视频场景", "context_hint": "来自客户沟通", "status": "captured" } ``` --- ## Cron 配置 | 任务 | 时间 | 说明 | |------|------|------| | Daily Digest 提取 | 每天 22:30 | 提取当天对话摘要，为复盘准备 L3 数据 | | 复盘提醒 | 每天 23:00 | 提醒用户开始复盘 | --- ## References - 模板：`references/templates.md` - 定时提醒：`references/reminders.md` - 原始输入提取脚本：`scripts/extract-raw-inputs.py` - 数据迁移脚本：`scripts/migrate-md-to-jsonl.py`

path: README.md
content: # User Growth Coach v2.2

> 三层反馈复盘系统：即时反馈 → 历史模式识别 → 日常上下文注入 → 深层行为洞察

## 概述

不只是记录复盘，而是一个**连接你所有输入的智能反馈系统**。通过三层分析，自动发现你没意识到的行为模式，并给出基于心理学的机制解释和具体行为处方。

## 🚀 Quick Start

安装后，直接在聊天中发送：

```
复盘
```

然后输入你想记录的内容即可。Skill 会自动记录并给出即时反馈。

**更多触发词：**
- `复盘 深度` — 全量三层分析
- `周盘` — 自动加载 7 天上下文
- `情绪` — 情绪拆解
- `成长汇总` — 洞察版五段式汇总
- `帮助` — 显示所有可用命令

**推荐配置（可选）：**
设置 Cron 任务，每天 22:30 自动生成 Daily Digest，23:00 发送复盘提醒。详见 `references/reminders.md`。

## ✨ 核心特性

### 三层反馈架构

```
L1 ──── 当前输入 ──── 即时反馈（所有模式）
         │
L2 ──── 输入间关联 ── 模式识别、承诺追踪、情绪趋势、深层模式洞察（标准+深度）
         │
L3 ──── 日常上下文 ── 当天对话摘要注入，关联你的实际行动（仅深度模式）
```

### 🆕 深层模式洞察

当同一行为模式在 7 天内出现 ≥ 2 次时，自动触发深层洞察，每个洞察包含三层解释：

```
💡「预设困难逃避」
是什么：还没动手就在脑子里预演各种障碍，用"太难了"来合理化不行动。
为什么：大脑倾向高估未知任务的难度来保护你不去冒险——损失厌恶的变体。
怎么办：默认先动手试 30 秒。大多数预设的困难在动手瞬间就消失了。
```

- 洞察数量不限制，有多少给多少
- 同一模式不重复解释，只追踪行为变化
- 用户可说"展开讲讲"进入完整深度解释模式

## 反馈模式

| 模式 | 层级 | 适用场景 |
|------|------|---------|
| `快速` | L1 | 快速记一笔，不想被打断 |
| `标准`（默认）| L1 + L2 | 日常复盘，带历史关联和模式洞察 |
| `深度` | L1 + L2 + L3 | 全量分析，关联当天所有交互上下文 |

## 触发词

| 触发词 | 功能 |
|--------|------|
| `复盘` | 记录+反馈（可选模式：快速/标准/深度） |
| `周盘` | 周复盘（自动加载 7 天上下文） |
| `月盘` | 月复盘（自动加载 30 天上下文） |
| `决策` | 决策复盘 |
| `情绪` | 情绪拆解 |
| `目标` | 目标校准 |
| `习惯` | 习惯追踪 |
| `成长汇总` | 洞察版汇总（五段式深度分析） |
| `回顾` | 查看历史记录 |
| `删除上条` | 删除最近一条记录 |
| `修改上条` | 修改最近一条记录 |
| `帮助` | 显示可用触发词 |

## L2 模式检测能力

| 检测项 | 说明 |
|--------|------|
| **重复主题** | 同一标签 7 天内出现 ≥ 2 次 |
| **承诺追踪** | 自动提取承诺，后续复盘追问是否兑现 |
| **情绪趋势** | 连续多天的情绪变化方向 |
| **维度失衡** | 工作/健康/社交等维度的投入比例偏差 |
| **矛盾检测** | 前后复盘内容的语义矛盾 |
| **深层模式洞察** | 重复行为的心理机制解释（是什么/为什么/怎么办） |

## 洞察版汇总（五段式）

触发 `成长汇总` 或 `复盘汇总` 后，输出：

1. **今日关键洞察** — 跨天模式识别（模式级）
2. **上下文关联** — 日常行动与复盘的深度关联
3. **承诺追踪** — ✅已兑现 / ⚠️未兑现 / 🔄进行中
4. **To-Do** — 1-3 条，含场景 + 动作 + 完成定义
5. **明日关注点** — 单指标，可观测

## 数据存储

```
memory/
├── user-growth/
│   └── YYYY-MM.jsonl          # 复盘记录（结构化 JSONL）
└── daily-digest/
    └── YYYY-MM-DD.md          # 每日交互摘要（自动生成）
```

### JSONL 记录格式

```json
{
  "id": "20260101_2300",
  "ts": "2026-01-01T23:00:00+08:00",
  "trigger": "复盘",
  "mode": "标准",
  "dimension": "工作",
  "input": "用户原话",
  "tags": ["执行策略", "时间管理"],
  "mood": "neutral",
  "commitment": "明天上午优先处理阻塞点",
  "status": "recorded"
}
```

## 配置

### 环境变量（可选）

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OPENCLAW_SESSIONS_DIR` | `~/.openclaw/agents/main/sessions` | Session transcript 目录 |
| `OPENCLAW_DIGEST_DIR` | `~/.openclaw/workspace/memory/daily-digest` | Daily Digest 输出目录 |

### 推荐模型

建议使用高质量推理模型（如 Claude Opus、GPT-4 等）以获得最佳洞察效果。
可在 MODEL_ROUTER 或 Agent 配置中设置复盘任务的专用模型路由。

### Cron 任务

| 任务 | 建议时间 | 说明 |
|------|---------|------|
| Daily Digest | 每天 22:30 | 提取当天对话摘要，为 L3 准备数据 |
| 复盘提醒 | 每天 23:00 | 提醒用户开始当日复盘 |

## 文件结构

```
skills/user-growth-coach/
├── README.md                  # 本文件
├── SKILL.md                   # Agent 执行协议（完整三层反馈逻辑）
├── LICENSE                    # MIT License
├── _meta.json                 # 元数据
├── references/
│   ├── templates.md           # 各触发词的问题模板
│   └── reminders.md           # 定时提醒配置
└── scripts/
    ├── extract-raw-inputs.py  # 从 transcript 提取用户原始输入
    ├── extract-daily-digest.py # Daily Digest 旧版提取（保留兼容）
    └── migrate-md-to-jsonl.py # 数据迁移：markdown → JSONL
```

## 设计理念

### 为什么不只是记录？

传统的复盘工具只做"记录"——你写下来就完了。但人类最大的盲区不是"忘了记"，而是**"看不到自己的模式"**。

这个 skill 的核心价值是：
- **L1**：确保你每次复盘都有即时反馈，不是自说自话
- **L2**：自动发现你 7-30 天内的重复行为、未兑现承诺、情绪波动
- **L3**：把你当天的实际行动（做了什么、搜了什么、和谁聊了什么）和复盘内容交叉对比

### 深层模式洞察的价值

人对自己的行为模式通常是"知道但做不到"。深层洞察不只是指出模式，还解释**为什么你的大脑会这么干**（心理机制），并给出一条**具体到场景的制动规则**。

## 版本历史

### v2.2.0
- 修复脚本可执行权限
- 脚本路径可配置（支持自定义 agent 名称和目录）
- 添加 LICENSE 文件
- 添加 Quick Start 引导
- 统一 Cron 时间配置
- 清理文档引用

### v2.1.0
- 新增深层模式洞察：同类行为 ≥ 2 次触发，三段式解释（是什么/为什么/怎么办）
- 洞察数量不限制，同一模式不重复解释
- 支持"展开讲讲"进入完整深度解释模式

### v2.0.0
- 三层反馈体系：L1 + L2 + L3
- 存储格式迁移：markdown → JSONL
- Daily Digest 自动提取
- 模式检测：重复主题、承诺追踪、情绪趋势、维度失衡、矛盾检测

### v1.0.0
- 初始版本：极简触发 + 记录 + 洞察版汇总

## License

MIT


path: _meta.json
content: ownerId: Jack-Yang-ai
slug: user-growth-coach
version: 2.3.0
license: MIT

path: references/reminders.md
content: # Reminders (建议配置)

## Default Schedule

> 以下为建议时间，可根据个人作息调整。

| 任务 | 建议时间 | 说明 |
|------|---------|------|
| Daily Digest 提取 | 每天 22:30 | 为 L3 准备当日上下文数据 |
| 日复盘提醒 | 每天 23:00 | 提醒用户开始复盘 |
| 周复盘提醒 | 每周日 20:30 | 周盘 |
| 月复盘提醒 | 每月最后一天 21:00 | 月盘 |

## Reminder Message Style
- 简短、无说教
- 示例：
  - `今天收尾了吗？复盘。`
  - `周盘时间到：进步/失误/下周聚焦。`
  - `月盘：给四维打分并定下月主线。`

## Channel
优先当前聊天渠道。

## Execution Note
定时任务建议由 OpenClaw Cron 配置执行，消息内容只发触发词（如"复盘"），由 Skill 自动展开流程。


path: references/templates.md
content: # Templates

## 1) 日复盘（复盘）
### 默认（仅记录）
- 用户连续输入 -> 仅追加记录
- 助手短回复：`已记录` / `记下了`
- 不主动展开分析

### 快速（用户明确要分析时）
1. 今天最有价值的一件事？
2. 今天最大偏差是什么？
3. 明天最关键动作（含截止时间）？

### 标准
1. 今天的主线目标完成度（0-100）？
2. 拉高结果的关键行为是什么？
3. 最大阻碍是什么？
4. 哪个决策值得保留？
5. 明天最小可执行动作（<=30分钟）？
6. 失败风险与预案？

### 深度
在标准基础上追加：
7. 今天是否偏离长期目标？
8. 偏离根因（信息/情绪/执行）？
9. 本周要停止的无效行为？
10. 一条系统级改进？

---

## 2) 周盘（周盘）
1. 本周 3 个进步
2. 本周 1-2 个失误
3. 下周 3 个聚焦
4. 必停 1 件事
5. 本周最大杠杆动作

---

## 3) 月盘（月盘）
1. 月度主题达成度（0-100）
2. 四维评分：工作/财富/健康/关系情绪
3. 本月最值与最亏决策
4. 下月唯一主线

---

## 4) 决策（决策）
1. 决策内容
2. 依据（数据/假设）
3. 最坏情况
4. 退出条件（止损线）
5. 复盘时间点

---

## 5) 情绪（情绪）
1. 当前情绪强度（1-10）
2. 触发事件
3. 可控动作（10分钟内）
4. 24小时内不做的冲动行为
5. 回稳指标

---

## 6) 目标（目标）
1. 当前目标是否还有效？
2. 本周唯一关键结果（KR）
3. 本周阻力与解除动作

---

## 7) 习惯（习惯）
1. 连续性（streak）
2. 触发器是否稳定
3. 最小可执行动作是否过大
4. 明天如何降低摩擦

---

## 8) 结束/汇总/总结（洞察版汇总）
输出模板（固定五段）：
1. 今日关键洞察（模式级）
2. 结构性问题（系统摩擦点）
3. 方法论结论（一句话）
4. To-Do（1-3条，动作清晰+完成定义）
5. 明日关注点（单指标）

禁用项：
- 进化分卡
- 纯复述用户原话
- 空泛建议（如“继续加油”）


path: scripts/extract-daily-digest.py
content: #!/usr/bin/env python3
"""
extract-daily-digest.py
从 OpenClaw session transcript 中提取当日用户消息摘要。
输出：memory/daily-digest/YYYY-MM-DD.md

用法：
  python3 extract-daily-digest.py [--date YYYY-MM-DD] [--sessions-dir PATH]

默认提取今天的消息。
"""

import json
import os
import sys
import glob
from datetime import datetime, timedelta, timezone
from pathlib import Path

# 配置
DEFAULT_AGENT = "main"
SESSIONS_DIR = os.path.expanduser(
    os.environ.get("OPENCLAW_SESSIONS_DIR", f"~/.openclaw/agents/{DEFAULT_AGENT}/sessions")
)
OUTPUT_DIR = os.path.expanduser(
    os.environ.get("OPENCLAW_DIGEST_DIR", "~/.openclaw/workspace/memory/daily-digest")
)
TZ = timezone(timedelta(hours=8))  # Asia/Shanghai


def parse_args():
    date_str = None
    sessions_dir = SESSIONS_DIR

    args = sys.argv[1:]
    i = 0
    while i < len(args):
        if args[i] == "--date" and i + 1 < len(args):
            date_str = args[i + 1]
            i += 2
        elif args[i] == "--sessions-dir" and i + 1 < len(args):
            sessions_dir = args[i + 1]
            i += 2
        else:
            i += 1

    if date_str:
        target_date = datetime.strptime(date_str, "%Y-%m-%d").date()
    else:
        target_date = datetime.now(TZ).date()

    return target_date, sessions_dir


def extract_user_text(content):
    """从 message content 中提取纯用户文本（去掉 metadata wrapper）"""
    raw = ""
    if isinstance(content, list):
        for item in content:
            if isinstance(item, dict) and item.get("type") == "text":
                raw = item.get("text", "")
                break
    elif isinstance(content, str):
        raw = content

    if not raw:
        return None

    # 去掉 OpenClaw 的 metadata wrapper，提取用户实际输入
    lines = raw.split("\n")
    user_text = None
    for line in lines:
        # 格式: ou_xxx: 实际消息
        if line.startswith("ou_") and ": " in line:
            user_text = line.split(": ", 1)[1]
            break

    # 如果没找到 ou_ 前缀，可能是其他格式
    if not user_text:
        # 跳过 metadata 块，取最后的实际内容
        in_metadata = False
        text_lines = []
        for line in lines:
            if line.startswith("```json"):
                in_metadata = True
                continue
            if in_metadata and line.startswith("```"):
                in_metadata = False
                continue
            if in_metadata:
                continue
            if line.startswith("[message_id:"):
                continue
            if line.startswith("Conversation info"):
                continue
            if line.startswith("Replied message"):
                continue
            if line.strip():
                text_lines.append(line.strip())
        user_text = " ".join(text_lines) if text_lines else None

    return user_text


def extract_assistant_summary(content):
    """从 assistant 回复中提取前 200 字作为摘要"""
    raw = ""
    if isinstance(content, list):
        for item in content:
            if isinstance(item, dict) and item.get("type") == "text":
                raw = item.get("text", "")
                break
    elif isinstance(content, str):
        raw = content

    if not raw:
        return None

    # 去掉 tool call 等噪音，取纯文本前 200 字
    clean = raw.strip()
    if len(clean) > 200:
        clean = clean[:200] + "..."
    return clean if clean else None


def process_session_file(filepath, target_date):
    """处理单个 session 文件，提取目标日期的消息"""
    events = []

    try:
        with open(filepath, "r", encoding="utf-8") as f:
            prev_user_msg = None
            for line in f:
                line = line.strip()
                if not line:
                    continue
                try:
                    obj = json.loads(line)
                except json.JSONDecodeError:
                    continue

                if obj.get("type") != "message":
                    continue

                msg = obj.get("message", {})
                role = msg.get("role", "")
                ts_str = obj.get("timestamp", "")

                if not ts_str:
                    continue

                # 解析时间戳
                try:
                    ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
                    ts_local = ts.astimezone(TZ)
                except (ValueError, TypeError):
                    continue

                # 过滤目标日期
                if ts_local.date() != target_date:
                    continue

                content = msg.get("content", "")

                if role == "user":
                    user_text = extract_user_text(content)
                    if user_text and len(user_text) > 2:
                        # 过滤掉 heartbeat 和系统消息
                        if "HEARTBEAT" in user_text or "heartbeat" in user_text.lower():
                            continue
                        prev_user_msg = {
                            "time": ts_local.strftime("%H:%M"),
                            "text": user_text,
                            "ts": ts_local,
                        }
                        events.append(prev_user_msg)

                elif role == "assistant" and prev_user_msg:
                    summary = extract_assistant_summary(content)
                    if summary:
                        prev_user_msg["response_preview"] = summary

    except (IOError, PermissionError) as e:
        print(f"Warning: Cannot read {filepath}: {e}", file=sys.stderr)

    return events


def classify_event(text):
    """简单分类用户输入的类型"""
    text_lower = text.lower()

    # URL/链接
    if "http://" in text or "https://" in text:
        return "🔗 链接/资源"

    # 复盘相关
    keywords_review = ["复盘", "周盘", "月盘", "回顾", "汇总", "总结"]
    if any(k in text for k in keywords_review):
        return "📝 复盘"

    # 决策
    if "决策" in text or "决定" in text:
        return "⚖️ 决策"

    # 情绪
    keywords_emotion = ["焦虑", "开心", "难过", "烦", "累", "压力", "情绪"]
    if any(k in text for k in keywords_emotion):
        return "💭 情绪"

    # 任务/指令
    keywords_task = ["帮我", "查一下", "搜索", "找", "看看", "分析", "总结一下"]
    if any(k in text for k in keywords_task):
        return "🔧 任务"

    # 讨论/思考
    if len(text) > 50:
        return "💡 讨论/思考"

    # 简短对话
    return "💬 对话"


def generate_digest(events, target_date):
    """生成每日摘要 markdown"""
    if not events:
        return None

    lines = [f"# {target_date} 每日交互摘要\n"]
    lines.append(f"> 自动生成于 {datetime.now(TZ).strftime('%Y-%m-%d %H:%M')}")
    lines.append(f"> 共 {len(events)} 条用户交互\n")

    # 按类型分组
    by_category = {}
    for e in events:
        cat = classify_event(e["text"])
        if cat not in by_category:
            by_category[cat] = []
        by_category[cat].append(e)

    # 输出分类摘要
    lines.append("## 交互概览\n")
    for cat, items in by_category.items():
        lines.append(f"### {cat} ({len(items)}条)\n")
        for item in items:
            text = item["text"]
            if len(text) > 150:
                text = text[:150] + "..."
            lines.append(f"- **{item['time']}** {text}")
        lines.append("")

    # 提取关键主题词（简单频率统计）
    all_text = " ".join(e["text"] for e in events)
    lines.append("## 今日关键词\n")
    lines.append(f"_基于 {len(events)} 条交互自动提取_\n")

    # 输出原始时间线（供 LLM 深度分析用）
    lines.append("## 完整时间线\n")
    for e in sorted(events, key=lambda x: x["ts"]):
        text = e["text"]
        if len(text) > 300:
            text = text[:300] + "..."
        lines.append(f"- [{e['time']}] {text}")

    return "\n".join(lines)


def main():
    target_date, sessions_dir = parse_args()
    print(f"提取日期: {target_date}", file=sys.stderr)
    print(f"Sessions目录: {sessions_dir}", file=sys.stderr)

    # 收集所有 session 文件中目标日期的消息
    all_events = []
    session_files = glob.glob(os.path.join(sessions_dir, "*.jsonl"))

    for sf in session_files:
        # 跳过 .lock 文件和 .deleted/.reset 文件
        basename = os.path.basename(sf)
        if ".lock" in basename or ".deleted" in basename or ".reset" in basename:
            continue
        events = process_session_file(sf, target_date)
        all_events.extend(events)

    # 按时间排序
    all_events.sort(key=lambda x: x["ts"])
    print(f"找到 {len(all_events)} 条用户消息", file=sys.stderr)

    if not all_events:
        print("当日无用户消息，跳过。", file=sys.stderr)
        return

    # 生成摘要
    digest = generate_digest(all_events, target_date)

    # 写入文件
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    output_path = os.path.join(OUTPUT_DIR, f"{target_date}.md")
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(digest)

    print(f"✅ 摘要已写入: {output_path}", file=sys.stderr)
    print(output_path)


if __name__ == "__main__":
    main()


path: scripts/extract-raw-inputs.py
content: #!/usr/bin/env python3
"""
extract-raw-inputs.py
从 session transcript 中提取当日用户原始输入（去噪）。
输出纯文本，供 LLM 进一步总结。

用法：
  python3 extract-raw-inputs.py [--date YYYY-MM-DD]
  
输出到 stdout，供 cron 任务中 LLM 读取。
"""

import json
import os
import sys
import glob
from datetime import datetime, timedelta, timezone

DEFAULT_AGENT = "main"
SESSIONS_DIR = os.path.expanduser(
    os.environ.get("OPENCLAW_SESSIONS_DIR", f"~/.openclaw/agents/{DEFAULT_AGENT}/sessions")
)
TZ = timezone(timedelta(hours=8))


def parse_args():
    date_str = None
    args = sys.argv[1:]
    i = 0
    while i < len(args):
        if args[i] == "--date" and i + 1 < len(args):
            date_str = args[i + 1]
            i += 2
        else:
            i += 1
    if date_str:
        return datetime.strptime(date_str, "%Y-%m-%d").date()
    return datetime.now(TZ).date()


def extract_user_text(content):
    """提取用户实际输入文本"""
    raw = ""
    if isinstance(content, list):
        for item in content:
            if isinstance(item, dict) and item.get("type") == "text":
                raw = item.get("text", "")
                break
    elif isinstance(content, str):
        raw = content
    if not raw:
        return None

    lines = raw.split("\n")
    
    # 提取 ou_xxx: 后面的内容
    user_text = None
    for line in lines:
        if line.startswith("ou_") and ": " in line:
            user_text = line.split(": ", 1)[1]
            break

    if not user_text:
        # 跳过 metadata，取实际内容
        in_meta = False
        text_parts = []
        for line in lines:
            if line.startswith("```json"):
                in_meta = True
                continue
            if in_meta and line.startswith("```"):
                in_meta = False
                continue
            if in_meta or line.startswith("[message_id:") or line.startswith("Conversation info") or line.startswith("Replied message"):
                continue
            if line.strip():
                text_parts.append(line.strip())
        user_text = " ".join(text_parts) if text_parts else None

    return user_text


def main():
    target_date = parse_args()
    events = []

    session_files = glob.glob(os.path.join(SESSIONS_DIR, "*.jsonl"))
    for sf in session_files:
        basename = os.path.basename(sf)
        if ".lock" in basename or ".deleted" in basename or ".reset" in basename:
            continue
        try:
            with open(sf, "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
                    if obj.get("type") != "message":
                        continue
                    msg = obj.get("message", {})
                    if msg.get("role") != "user":
                        continue
                    ts_str = obj.get("timestamp", "")
                    if not ts_str:
                        continue
                    try:
                        ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
                        ts_local = ts.astimezone(TZ)
                    except (ValueError, TypeError):
                        continue
                    if ts_local.date() != target_date:
                        continue
                    user_text = extract_user_text(msg.get("content", ""))
                    if user_text and len(user_text) > 2:
                        # 过滤 cron/heartbeat/system/session-startup 消息
                        skip_patterns = [
                            "HEARTBEAT", "heartbeat", "[cron:", 
                            "memsearch 自动索引", "Self-Evolution",
                            "A new session was started", "Execute your Session Startup",
                            "Read HEARTBEAT.md"
                        ]
                        if any(skip in user_text for skip in skip_patterns):
                            continue
                        events.append({
                            "time": ts_local.strftime("%H:%M"),
                            "text": user_text,
                            "ts": ts_local,
                        })
        except (IOError, PermissionError):
            continue

    events.sort(key=lambda x: x["ts"])

    # 输出纯文本时间线
    print(f"# {target_date} 用户交互原始记录")
    print(f"# 共 {len(events)} 条有效输入")
    print()
    for e in events:
        print(f"[{e['time']}] {e['text']}")
        print()


if __name__ == "__main__":
    main()


path: scripts/migrate-md-to-jsonl.py
content: #!/usr/bin/env python3
"""
migrate-md-to-jsonl.py
将 user-growth 的 markdown 记录迁移为 JSONL 格式。
"""

import json
import os
import re
from datetime import datetime

INPUT = os.path.expanduser(
    os.environ.get("GROWTH_MD_INPUT", "~/.openclaw/workspace/memory/user-growth/YYYY-MM.md")
)
OUTPUT = os.path.expanduser(
    os.environ.get("GROWTH_JSONL_OUTPUT", "~/.openclaw/workspace/memory/user-growth/YYYY-MM.jsonl")
)

def parse_md(filepath):
    records = []
    with open(filepath, "r", encoding="utf-8") as f:
        content = f.read()

    # Pattern 1: ## YYYY-MM-DD HH:MM | trigger | status
    p1 = re.findall(
        r'## (\d{4}-\d{2}-\d{2} \d{2}:\d{2}) \| (\S+) \| (\S+)\n- 原始输入：(.+?)\n- 标签：(.+?)(?:\n|$)',
        content, re.DOTALL
    )
    for ts, trigger, status, inp, tags in p1:
        records.append({
            "id": ts.replace("-","").replace(" ","_").replace(":",""),
            "ts": ts + ":00+08:00",
            "trigger": trigger,
            "mode": "标准",
            "dimension": "未分类",
            "input": inp.strip(),
            "tags": [t.strip() for t in tags.split(",")],
            "mood": "neutral",
            "status": status
        })

    # Pattern 2: - 时间: ... 触发词: ... 
    blocks = re.split(r'(?=- 时间:)', content)
    for block in blocks:
        ts_m = re.search(r'时间:\s*(.+)', block)
        trigger_m = re.search(r'触发词:\s*(.+)', block)
        mode_m = re.search(r'模式:\s*(.+)', block)
        dim_m = re.search(r'维度:\s*(.+)', block)
        input_m = re.search(r'原始输入:\s*(.+)', block)
        tags_m = re.search(r'标签:\s*(.+)', block)
        status_m = re.search(r'状态:\s*(.+)', block)

        if ts_m and input_m:
            ts_raw = ts_m.group(1).strip()
            # 尝试解析时间
            try:
                dt = datetime.strptime(ts_raw, "%Y-%m-%d %H:%M")
                ts_iso = dt.strftime("%Y-%m-%dT%H:%M:00+08:00")
                rid = dt.strftime("%Y%m%d_%H%M")
            except ValueError:
                ts_iso = ts_raw
                rid = ts_raw.replace("-","").replace(" ","_").replace(":","")

            tags_raw = tags_m.group(1).strip() if tags_m else ""
            records.append({
                "id": rid,
                "ts": ts_iso,
                "trigger": trigger_m.group(1).strip() if trigger_m else "复盘",
                "mode": mode_m.group(1).strip() if mode_m else "标准",
                "dimension": dim_m.group(1).strip() if dim_m else "未分类",
                "input": input_m.group(1).strip(),
                "tags": [t.strip() for t in tags_raw.split(",") if t.strip()],
                "mood": "neutral",
                "status": status_m.group(1).strip() if status_m else "recorded"
            })

    # 去重（按 id）
    seen = set()
    unique = []
    for r in records:
        if r["id"] not in seen:
            seen.add(r["id"])
            unique.append(r)

    return unique


def main():
    if not os.path.exists(INPUT):
        print(f"源文件不存在: {INPUT}")
        return

    records = parse_md(INPUT)
    print(f"解析出 {len(records)} 条记录")

    with open(OUTPUT, "w", encoding="utf-8") as f:
        for r in records:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")

    print(f"✅ 已写入: {OUTPUT}")

    # 备份原文件
    backup = INPUT + ".bak"
    os.rename(INPUT, backup)
    print(f"📦 原文件已备份: {backup}")


if __name__ == "__main__":
    main()


path: scripts/trigger-router.js
content: #!/usr/bin/env node

const input = process.argv.slice(2).join(' ').trim();
if (!input) {
  console.log(JSON.stringify({ error: 'empty_input' }, null, 2));
  process.exit(1);
}

const mode = /(快速|标准|深度)/.exec(input)?.[1] || '标准';
const trigger = ['复盘','周盘','月盘','决策','情绪','目标','习惯','结束','汇总','总结'].find(t => input.includes(t));

const map = {
  '复盘': { type: 'daily_review', dimension: 'work' },
  '周盘': { type: 'weekly_review', dimension: 'all' },
  '月盘': { type: 'monthly_review', dimension: 'all' },
  '决策': { type: 'decision_review', dimension: 'wealth' },
  '情绪': { type: 'emotion_check', dimension: 'relationship_emotion' },
  '目标': { type: 'goal_alignment', dimension: 'all' },
  '习惯': { type: 'habit_tracking', dimension: 'health' },
  '结束': { type: 'insight_summary', dimension: 'all' },
  '汇总': { type: 'insight_summary', dimension: 'all' },
  '总结': { type: 'insight_summary', dimension: 'all' },
};

if (!trigger) {
  console.log(JSON.stringify({
    route: 'fallback',
    message: '未匹配触发词，请使用：复盘/周盘/月盘/决策/情绪/目标/习惯',
  }, null, 2));
  process.exit(0);
}

console.log(JSON.stringify({
  route: map[trigger].type,
  trigger,
  mode,
  dimension: map[trigger].dimension,
  next: 'load_template',
  templateRef: 'references/templates.md'
}, null, 2));


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)