9 files modified
6 files added
1357 ■■■■■ changed files
.gitignore 6 ●●●● patch | view | raw | blame | history
agents/lifehelper/agent/models.json 2 ●●● patch | view | raw | blame | history
agents/main/agent/models.json 2 ●●● patch | view | raw | blame | history
cron/jobs.json 20 ●●●● patch | view | raw | blame | history
openclaw.json 30 ●●●●● patch | view | raw | blame | history
workspace/.learnings/LEARNINGS.md 51 ●●●●● patch | view | raw | blame | history
workspace/MEMORY.md 1 ●●●● patch | view | raw | blame | history
workspace/memory/journal/2026-03-12.md 121 ●●●●● patch | view | raw | blame | history
workspace/memory/journal/2026-03-13.md 201 ●●●●● patch | view | raw | blame | history
workspace/memory/journal/2026-03-14.md 107 ●●●●● patch | view | raw | blame | history
workspace/memory/journal/2026-03-15.md 251 ●●●●● patch | view | raw | blame | history
workspace/memory/milestones/2026-03-skills.md 52 ●●●●● patch | view | raw | blame | history
workspace/memory/milestones/2026-03-workflows.md 85 ●●●●● patch | view | raw | blame | history
workspace/skills/memory-management/scripts/daily_check.py 173 ●●●●● patch | view | raw | blame | history
workspace/skills/memory-management/scripts/weekly_maintenance.py 255 ●●●●● patch | view | raw | blame | history
.gitignore
@@ -28,11 +28,15 @@
devices/
subagents/
identity/
memory/
feishu/
cron/runs/
completions/
# 记忆管理
memory/
!*/memory/
*/memory/ontology/
# ============================================
# 敏感信息(绝不能提交)
# ============================================
agents/lifehelper/agent/models.json
@@ -2,7 +2,7 @@
  "providers": {
    "custom-api-siliconflow-cn": {
      "baseUrl": "https://api.siliconflow.cn",
      "apiKey": "sk-hjxtzyxeoagiqozjdifstbmzmtdmmpiupquzfvoicyfnfnmy",
      "apiKey": "${SILICONFLOW_API_KEY}",
      "api": "openai-completions",
      "models": [
        {
agents/main/agent/models.json
@@ -2,7 +2,7 @@
  "providers": {
    "custom-api-siliconflow-cn": {
      "baseUrl": "https://api.siliconflow.cn",
      "apiKey": "SILICONFLOW_CN_API_KEY",
      "apiKey": "${SILICONFLOW_API_KEY}",
      "api": "openai-completions",
      "models": [
        {
cron/jobs.json
@@ -7,7 +7,7 @@
      "description": "每天早上9点AI早报",
      "enabled": true,
      "createdAtMs": 1773390853562,
      "updatedAtMs": 1773536472466,
      "updatedAtMs": 1773624106557,
      "schedule": {
        "kind": "cron",
        "expr": "0 9 * * *",
@@ -25,11 +25,11 @@
        "to": "ou_53994d69bfaad1bfa5ca4c658de5b23f"
      },
      "state": {
        "nextRunAtMs": 1773622800000,
        "lastRunAtMs": 1773536400033,
        "nextRunAtMs": 1773709200000,
        "lastRunAtMs": 1773623818703,
        "lastRunStatus": "ok",
        "lastStatus": "ok",
        "lastDurationMs": 72433,
        "lastDurationMs": 287854,
        "lastDelivered": true,
        "lastDeliveryStatus": "delivered",
        "consecutiveErrors": 0
@@ -40,7 +40,7 @@
      "name": "memory-weekly-maintenance",
      "enabled": true,
      "createdAtMs": 1773409688576,
      "updatedAtMs": 1773409688576,
      "updatedAtMs": 1773624926494,
      "schedule": {
        "kind": "cron",
        "expr": "30 9 * * 1",
@@ -58,7 +58,15 @@
        "to": "ou_53994d69bfaad1bfa5ca4c658de5b23f"
      },
      "state": {
        "nextRunAtMs": 1773624600000
        "nextRunAtMs": 1774229400000,
        "lastRunAtMs": 1773624600007,
        "lastRunStatus": "error",
        "lastStatus": "error",
        "lastDurationMs": 326487,
        "lastError": "⚠️ 📝 Edit: in , failed",
        "lastDelivered": true,
        "lastDeliveryStatus": "delivered",
        "consecutiveErrors": 1
      }
    }
  ]
openclaw.json
@@ -152,8 +152,17 @@
          ]
        }
      },
      "domain": "feishu",
      "connectionMode": "websocket",
      "domain": "feishu"
      "streaming": true,
      "timeout": 2000,
      "idempotent": true,
      "sessionMode": "per-chat",
      "requireMention": true,
      "footer": {
        "elapsed": true,
        "status": true
      }
    }
  },
  "gateway": {
@@ -194,27 +203,14 @@
    ],
    "load": {
      "paths": [
        "/home/tevin/.nvm/versions/node/v24.14.0/lib/node_modules/@m1heng-clawd/feishu"
        "/home/tevin/.nvm/versions/node/v24.14.0/lib/node_modules/@openclaw/feishu"
      ]
    },
    "slots": {
    },
    "entries": {
      "feishu": {
        "enabled": true
      }
    },
    "installs": {
      "feishu": {
        "source": "npm",
        "spec": "@m1heng-clawd/feishu",
        "installPath": "/home/tevin/.nvm/versions/node/v24.14.0/lib/node_modules/@m1heng-clawd/feishu",
        "version": "0.1.17",
        "resolvedName": "@m1heng-clawd/feishu",
        "resolvedVersion": "0.1.17",
        "resolvedSpec": "@m1heng-clawd/feishu@0.1.17",
        "integrity": "sha512-6BMqvndOXvWvGzMJEQQdp3vX1jidaIXrwwlz6Q8F5gC+yzcuHmNqIaAxXsrVOj7jaEAtznFjGmPWZ97sGc2eRw==",
        "shasum": "4e33e4c0cef6593da0b9e40f96d9310adc5bf6ab",
        "resolvedAt": "2026-03-15T03:57:41.889Z",
        "installedAt": "2026-03-15T03:58:15.894Z"
      }
    }
  }
workspace/.learnings/LEARNINGS.md
@@ -3,3 +3,54 @@
记录学习、改进和最佳实践。
---
## [LRN-20260316-001] best_practice
**Logged**: 2026-03-16T07:20:00+08:00
**Priority**: high
**Status**: resolved
**Area**: infra
### Summary
飞书 Webhook 超时导致消息重复投递问题及解决方案
### Details
**问题现象:**
- 收到用户未主动发送的消息(05:40 消息在 06:23 再次出现)
- 消息内容完全一致但消息ID不同(`om_x100b545876a2ed34c3d91545d8feb5f` vs 新ID)
- 触发对话意外延续
**根因分析:**
飞书事件订阅规则要求 **3秒内必须返回HTTP 200**,否则触发重试:
- 重试间隔:15秒 → 5分钟 → 1小时 → 6小时(最多4次)
- 由于超时未响应,飞书服务器重复投递了消息
**解决方案:**
修改 `openclaw.json` 配置:
```json
"feishu": {
  "connectionMode": "websocket",  // 使用长连接替代HTTP webhook
  "timeout": 2000,                 // 响应超时<3000ms
  "idempotent": true,              // 开启幂等去重
  "sessionMode": "per-chat"        // 按会话隔离
}
```
### Suggested Action
1. 监控网关响应时间,确保<3000ms
2. 优先使用 websocket 连接模式
3. 开启幂等性配置自动过滤重复 event_id
4. 识别重复消息特征:相同内容、不同消息ID、间隔符合重试规则
### Metadata
- Source: user_feedback
- Related Files: ~/.openclaw/openclaw.json
- Tags: feishu, webhook, timeout, retry, websocket
- Pattern-Key: infra.feishu_webhook_timeout
### Resolution
- **Resolved**: 2026-03-16T07:20:00+08:00
- **Action**: 已更新 openclaw.json 配置,用户已重启网关
- **Notes**: 配置已添加 websocket 模式、2000ms超时、幂等去重、per-chat会话隔离
---
workspace/MEMORY.md
@@ -15,6 +15,7 @@
### 🗂️ L1 概览层 (milestones/)
- [2026-03 技能安装](./memory/milestones/2026-03-skills.md) - 本月技能扩展记录
- [2026-03 工作流与规范](./memory/milestones/2026-03-workflows.md) - AI早报、沟通原则、记录规范
### 📖 L2 详情层 (journal/)
- [2026-03-15](./memory/journal/2026-03-15.md) - 测试qmd wrapper脚本、飞书渠道检查、每日维护
workspace/memory/journal/2026-03-12.md
New file
@@ -0,0 +1,121 @@
# 2026-03-12 详细日志
## 上午
- 会话启动,读取 SOUL.md、USER.md、MEMORY.md
## 下午
### 17:48 - 会话开始
- 新会话启动
- 模型: custom-api-siliconflow-cn/Pro/moonshotai/Kimi-K2.5
### 18:00 - 技能安装
- 用户要求安装 find-skills
- 使用 clawhub 安装成功
- 读取 SKILL.md 确认功能
### 18:22 - 优化记忆管理
- 讨论记忆管理技能需求
- 搜索 memory/knowledge 相关技能
- 决定安装 super-search 和 memory-merger
### 18:28 - 安装记忆技能
- 安装 super-search ✓
- 安装 memory-merger ✓
- 读取两个技能的 SKILL.md 了解使用方法
### 18:35 - 实现三层记忆架构
- 用户分享三层记忆架构图
- 开始重构记忆系统:
  - 创建目录: memory/milestones/, memory/journal/, knowledge/
  - 创建劳作层: workspace/00-Pending/, 01-Projects/, 02-Areas/, 03-Resources/, 04-Archive/
  - 重构 MEMORY.md 为 L0 索引层 (4KB以内)
  - 创建 L1 概览层示例 (milestones/2026-03-skills.md)
  - 创建 L2 详情层示例 (journal/2026-03-12.md)
## 技术细节
### 新技能使用方法
**super-search**:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs" [--user|--repo|--both] "搜索内容"
```
**memory-merger**:
```
/memory-merger >domain-name [scope]
```
### 目录结构
```
~/openclaw/workspace/
├── AGENTS.md          # 行为规则与启动序列
├── SOUL.md            # 灵魂与人格
├── MEMORY.md          # L0: 索引层 (4KB)
├── memory/
│   ├── milestones/    # L1: 概览层
│   └── journal/       # L2: 详情层
├── knowledge/         # 客观知识
├── skills/            # 能力层
├── tools/             # 能力层
└── workspace/         # 劳作层 (PARA)
    ├── 00-Pending/
    ├── 01-Projects/
    ├── 02-Areas/
    ├── 03-Resources/
    └── 04-Archive/
```
### 19:24 - 调整技能配置
- 移除 super-search(需要外部 Supermemory 后端)
- 保留 memory-merger(对三层记忆体系重要)
- 解释 npx skills 与 clawhub 安装路径差异
### 19:35 - 三层记忆体系测试
- 测试启动序列自动读取机制
- 验证 L0/L1/L2 三层架构正常工作
- 讨论持久化保障机制(文件系统 + AGENTS.md 启动规则)
### 19:47 - 写入和维护机制讨论
- 明确写入和维护主要靠手动触发
- 创建 HEARTBEAT.md 定期维护任务清单
- 澄清三层记忆 vs 脑机分离的边界
### 20:00 - Ontology 知识图谱演示
- 初始化 ontology 存储 (memory/ontology/graph.jsonl)
- 创建 Person 实体 (Tevin)
- 创建 Project 实体 ("三层记忆架构")
- 建立 has_owner 关系
### 20:05 - 完整项目任务示例
- 创建3个 Task 实体(设计Schema、实现API、开发前端)
- 建立任务依赖关系(blocks 链)
- 演示查询功能(查询项目任务、查询阻塞状态)
- 展示完整图谱结构和存储格式
### 20:18 - 自动化程度坦诚讨论
- 说明写入和维护不能全自动运作
- 解释技术限制和设计选择
- 确定触发词机制("记住..."、"记录一下"等)
- 达成"半自动"模式共识
## 关键决策
1. **采用三层记忆架构** (L0索引→L1概览→L2详情)
2. **保留4个记忆管理技能**: find-skills, memory-merger, ontology, self-improving-agent
3. **创建 Ontology 知识图谱** 用于结构化项目/任务管理
4. **建立触发词机制** 作为半自动记忆写入方案
## 创建的文件
- `MEMORY.md` - L0 索引层(重构)
- `AGENTS.md` - 更新启动序列
- `HEARTBEAT.md` - 定期维护任务
- `memory/milestones/2026-03-skills.md` - L1 里程碑
- `memory/journal/2026-03-12.md` - L2 详情日志
- `memory/ontology/graph.jsonl` - 知识图谱数据
---
*此文件属于 L2 详情层 - 按需读取的非常驻上下文*
workspace/memory/journal/2026-03-13.md
New file
@@ -0,0 +1,201 @@
# 2026-03-13
## [10:37] 启动会话与早间问候
### 背景
新会话启动,问候用户Tevin。
### 详情
系统已就绪,问候并询问今日任务。
---
## [10:45] AI早报定时任务需求
### 背景
用户询问是否能设置定时任务,希望每天早上9点收到AI领域新闻的早安播报。
### 详情
讨论定时任务实现方案:
- 确认可以使用 `openclaw cron` 创建定时任务
- 确定先制作测试版早报,满意后再创建定时任务
- 使用subagent搜索昨天AI新闻并生成首份早报
### 决策
采用"测试→确认→定时"三步流程实现AI早报功能。
---
## [16:13] 确立沟通原则
### 背景
用户要求记住两条沟通原则。
### 决策
1. **即时回复优先** — 不沉默执行,先回应再行动
2. **耗时任务分离** — 启用subagent异步处理,主会话负责协调汇报
---
## [16:21] 探索定时任务功能
### 背景
用户询问 `openclaw cron` 命令的具体用法。
### 详情
查询确认:
- 存在完整的 `openclaw cron` 子命令(add/list/rm/run等)
- 支持 cron 表达式定时
- 可通过 `--message` 指定agent消息, `--channel` 和 `--to` 指定投递目标
---
## [16:28] AI早报测试版完成
### 背景
首份AI早报生成完成。
### 详情
Subagent搜索到5条昨日AI重要新闻:
1. OpenAI计划将Sora整合进ChatGPT
2. 英伟达20亿美元投资Nebius
3. Adobe CEO卸任,强调AI创意时代
4. 香港大学AI心血管疾病预测系统(提前15年预警)
5. Netflix 6亿美元收购Ben Affleck的AI初创公司
用户反馈:格式和内容满意,可以创建定时任务。
---
## [16:33] 创建"AI早报"定时任务
### 背景
测试版效果满意,正式创建定时任务。
### 详情
```bash
openclaw cron add \
  --name "AI早报" \
  --cron "0 9 * * *" \
  --message "搜索昨天AI领域的重要新闻,整理成早报发送给用户" \
  --channel feishu \
  --to "ou_53994d69bfaad1bfa5ca4c658de5b23f" \
  --tz "Asia/Shanghai"
```
**任务详情**:
- 每天上午9:00(Asia/Shanghai时区)自动执行
- 通过Feishu投递给用户
### 决策
从明天(2026-03-14)开始,每天早上9点自动接收AI早报。
---
## [17:00] 优化早报:增加AI编程分类
### 背景
用户要求早报增加AI编程领域关注。
### 详情
更新定时任务提示词,增加:
- **AI编程分类**:特别关注 Anthropic/ClaudeCode 和 OpenAI/Codex 的动态
- **优先级**:AI编程相关新闻优先入选
---
## [17:38] 安装Tavily Search技能
### 背景
用户要求安装Tavily Search技能,用于高质量搜索。
### 详情
创建技能 `~/.openclaw/workspace/skills/tavily-search/`,包含:
- **SKILL.md**:使用说明和API配置指南
- **scripts/tavily_search.py**:搜索脚本,支持从环境变量或OpenClaw配置读取API key
### 决策
使用Tavily AI搜索引擎获取更高质量的AI新闻。
---
## [17:42] 确立语音偏好
### 背景
用户询问为何发送语音消息,是哪个配置导致的。
### 详情
发现是误操作调用了tts工具。用户明确要求:
**偏好**:如非必要,正常用文字交流即可
### 决策
- **默认只用文字交流**
- 除非特别要求,否则不发语音
- 已更新到 USER.md 记忆
---
## [17:48] 配置Tavily API Key
### 背景
用户已在 `openclaw.json` 的 `agents.defaults.env` 中配置Tavily API Key。
### 详情
- 确认配置位置:`agents.defaults.env.TAVILY_API_KEY`
- 更新技能脚本,支持从OpenClaw配置读取key(优先级:环境变量 > OpenClaw配置 > 直接参数)
- 测试搜索功能成功
---
## [18:28] 检查自我成长技能
### 背景
用户询问是否安装了"自我成长"技能。
### 详情
确认已安装 `self-improving-agent` 技能(位于 `~/.openclaw/workspace/skills/self-improving-agent/`)。
---
## [22:00] 解答 OpenClaw 目录结构问题
### 背景
用户询问 `.openclaw` 目录下新增的 browser、canvas、devices 文件夹用途。
### 详情
通过调用各工具状态检查,确认:
- **browser/**: 浏览器控制数据,存储 CDP 连接配置、用户数据目录、截图缓存等。当前检测到 Chromium 可用。
- **canvas/**: 画布渲染数据,用于 UI 画布展示功能。需要配对节点才能使用。
- **devices/**: 配对设备管理,存储已配对设备信息。当前暂无配对设备。
### 结论
这些都是 OpenClaw 内部运行时数据目录,用户无需直接操作。
---
## [22:29] 中文化核心配置文件
### 背景
用户表示 SOUL.md 和 TOOLS.md 是英文的看不懂,要求改写成中文,后续记录优先中文。
### 详情
将两个核心配置文件从英文翻译为中文:
- **SOUL.md**: 624 bytes,保留核心信条、边界、气质等章节
- **TOOLS.md**: 409 bytes,保留本地备注用途说明
### 决策
后续所有记录默认使用中文优先。
### 关联
- L0 索引: [MEMORY.md](../MEMORY.md)
---
## 今日关键决策汇总
1. **AI早报定时任务**:每天早上9点自动执行,已启用
2. **沟通原则**:即时回复 + subagent异步处理
3. **Tavily Search技能**:已安装并配置,用于高质量搜索
4. **语音偏好**:默认只用文字,除非特别要求
5. **记录语言**:后续所有记录优先使用中文
workspace/memory/journal/2026-03-14.md
New file
@@ -0,0 +1,107 @@
# 2026-03-14
## [08:00] AI早报格式定型与定时任务创建
### 背景
用户要求创建每天早上9点的AI早报定时任务,需要先测试格式再正式启用。
### 详情
经过测试和讨论,确定了AI早报的格式和内容结构:
**行业分类(3个):**
1. **AI行业** - OpenAI、Google、Anthropic、Meta、NVIDIA等动态
2. **AI编程** - 重点关注 Anthropic/ClaudeCode 和 OpenAI/Codex
3. **国产大模型** - 字节、阿里、百度、DeepSeek等国内进展
**内容结构:**
- 每个行业1-2条重要新闻
- 每条包含:标题、一句话摘要、来源
- 最后附加"昨日总结"段落
- 大标题不使用图标,保持简洁文字
**定时任务配置:**
```bash
openclaw cron add \
  --name "AI早报" \
  --cron "0 9 * * *" \
  --tz "Asia/Shanghai" \
  --channel feishu
```
### 决策
- 早报优先关注AI编程领域动态(ClaudeCode/Codex)
- 使用简洁文字标题,不用emoji图标
- 每天早上9点自动发送
---
## [09:38-12:27] 安装并配置 Tavily Search 技能
### 背景
为AI早报提供高质量的AI新闻搜索能力,需要集成Tavily AI搜索引擎。
### 详情
**技能创建过程:**
1. **创建技能结构** - `~/.openclaw/workspace/skills/tavily-search/`
2. **编写 SKILL.md** - 定义技能描述和使用说明
3. **编写搜索脚本** - `scripts/tavily_search.py` 支持命令行和Python API
**API Key 配置迭代:**
- **第一次**:计划使用环境变量 `TAVILY_API_KEY`
- **第二次**:改为从OpenClaw配置文件 `~/.openclaw/openclaw.json` 读取
- **第三次**:更新脚本支持多层级key读取(环境变量 > OpenClaw配置 > 参数传入)
**配置位置:**
```json
{
  "agents": {
    "defaults": {
      "env": {
        "TAVILY_API_KEY": "tvly-dev-xxx"
      }
    }
  }
}
```
**功能测试:**
- 成功搜索"Claude Code最新功能"
- 返回3条相关结果,包含版本更新、CLI工具、桌面版功能
- 验证key读取和API调用正常工作
### 决策
- 保留Tavily Search技能用于AI早报和其他搜索需求
- 采用OpenClaw配置文件集中管理API key,便于维护
### 关联
- L0索引: [MEMORY.md](../MEMORY.md)
- 技能路径: `skills/tavily-search/`
- 定时任务ID: `f83b0227-20d1-405b-b4c0-9248dad6d959`
---
## [17:42] 沟通原则确认
### 背景
回顾工作方式约定。
### 详情
用户重申两条沟通原则:
1. **即时回复优先** - 只负责沟通,必须立即回复,不能先去干活不回复
2. **耗时任务分离** - 需要时间执行的任务启动专用subagent处理,主会话负责协调反馈
### 决策
- 严格执行上述两条原则
- 语音消息仅限用户特别要求时发送,默认只用文字
---
## 今日总结
3月14日主要完成两项工作:
1. **AI早报系统搭建** - 从格式设计到定时任务创建,已完成部署,明天开始自动发送
2. **Tavily Search技能** - 创建并配置完成,支持从OpenClaw配置读取API key,已测试可用
**待观察:**
- 明日早报执行效果
- Tavily搜索在早报中的实际表现
workspace/memory/journal/2026-03-15.md
New file
@@ -0,0 +1,251 @@
# 2026-03-15
## [12:20] 会话启动与问候
### 详情
新会话启动,问候用户Tevin。确认当前时间:周日下午。
---
## [12:27] 飞书渠道状态检查
### 详情
用户要求检测飞书渠道是否正常。
**结果**: 飞书渠道未配置(Feishu account "default" is not configured)
**说明**: 飞书相关功能(消息、文档、任务等)当前不可用,需要配置飞书应用凭证后才能使用。
---
## [12:29] 查询三层记忆首次提及时间
### 详情
用户询问第一次提到"三层记忆"是什么时候。
**答案**: 2026年3月12日
**背景**: 当天下午18:35左右,用户分享了三层记忆架构图,随后一起实现了该架构:
- L0 索引层 → MEMORY.md
- L1 概览层 → memory/milestones/
- L2 详情层 → memory/journal/
---
## [17:58] 测试 QMD Wrapper 脚本
### 详情
用户要求测试完整的 wrapper 脚本。
**测试对象**: `qmd-wrapper.sh`
**脚本内容**:
```bash
#!/bin/bash
# QMD Wrapper - 强制使用 CPU 后端
export GGML_USE_CPU=1
exec qmd "$@"
```
**测试结果**: ✅ 全部通过
- --help 输出正常
- 搜索功能正常(搜索"三层记忆"返回6条结果)
- 成功解决 Vulkan GPU 内存分配问题
**关键改进**: 通过 `GGML_USE_CPU=1` 强制使用 CPU 后端,避免之前 `ggml_vulkan: Device memory allocation failed` 错误。
---
## [22:00] 心跳每日维护
### 详情
定时心跳检查,执行每日维护任务。
**检查结果**:
- 今日 L2 记录: 不存在(已补充)
- Session 文件: 找到10个(包含重置归档)
- 用户消息: 33条
- L0 大小: 1.9KB / 4KB ✅ 正常
**执行动作**: 补充今日 L2 详细日志
---
## [18:07] Gateway 重启与 QMD 测试
### 详情
用户重启 Gateway 后,测试 QMD 是否正常工作。
**测试内容:**
- 测试 memory_search 工具
- 查询 "定时任务 AI早报"
**结果:**
- ✅ 纯文本搜索(BM25)工作正常
- ❌ 向量搜索(rerank)仍不可用
- 错误:`ggml_vulkan: Device memory allocation of size 633207232 failed`
**结论:** GPU 显存不足(需 ~600MB),rerank 模型无法加载。
---
## [18:13-18:59] QMD 深度调试与最终决策
### 详情
用户多次重启 Gateway,尝试解决 QMD 的 GPU 显存问题。
**尝试过程:**
1. **18:13** - 测试环境变量传递
   - 确认 TAVILY_API_KEY 等变量已正确传递
   - 但 rerank 模型仍尝试加载到 GPU
2. **18:43** - 修改超时设置为 30 秒
   - 测试 memory_search 响应时间
   - 纯文本搜索正常,rerank 仍失败
3. **18:48** - 再次重启 Gateway
   - 测试环境变量是否正确加载
   - 确认 `GGML_USE_CPU=1` 对 rerank 无效
4. **18:56** - 停止所有子代理
   - 确认无活动子代理
5. **18:59** - **最终决策:完全卸载 QMD**
   - 执行卸载命令
   - 删除 qmd 命令、wrapper 脚本、数据目录
   - 从 openclaw.json 移除 memory 配置
### 关键发现
- qmd 的 rerank 模型**必须使用 GPU**,无法通过环境变量强制使用 CPU
- 这是 node-llama-cpp 库的限制
- 当前设备显存不足以支持向量搜索
---
## [19:18] Git 提交 QMD 卸载更改
### 详情
将 QMD 卸载的更改提交到 Git。
**提交信息:**
```
[master 27adc32] 移除 QMD 配置:卸载 QMD 并清理相关配置和脚本
 1 file changed, 1 insertion(+), 25 deletions(-)
```
**已清理内容:**
- qmd 命令(npm 全局包)
- wrapper 脚本(~/.openclaw/workspace/qmd-wrapper.sh)
- qmd 数据目录(~/.openclaw/agents/main/qmd/)
- openclaw.json 中的 memory 配置
---
## [19:20] 查找 Session 压缩技能
### 详情
用户询问是否有自动压缩长聊天内容的技能。
**搜索结果:**
1. **session-compression**(推荐)- 204 次安装
   - bobmatnyc/claude-mpm-skills
   - 链接:https://skills.sh/bobmatnyc/claude-mpm-skills/session-compression
2. **context-compression** - 33 次安装
   - eyadsibai/ltk
3. **context-manager** - 27 次安装
   - aaaaaqwq/claude-code-skills
**OpenClaw 内置功能:**
- 当前配置:`"compaction": {"mode": "safeguard"}`
- 支持三种模式:off / safeguard / always
---
## [19:53] Self-Improvement 技能状态检查
### 详情
用户要求检查 self-improvement 技能是否生效。
**检查结果:**
- ✅ 技能已安装并生效(SKILL.md 已加载)
- ❌ **实际使用:尚未开始记录**
**记录文件状态:**
- `.learnings/LEARNINGS.md` - 空模板(62 字节)
- `.learnings/ERRORS.md` - 空模板(373 字节)
- `.learnings/FEATURE_REQUESTS.md` - 空模板(340 字节)
**应记录但未记录的事件:**
- QMD GPU 显存分配失败 → 应记录到 ERRORS.md
- QMD 卸载过程 → 应记录到 LEARNINGS.md
- Session 压缩技能需求 → 应记录到 FEATURE_REQUESTS.md
---
## [20:02] QMD 卸载状态最终确认
### 详情
用户再次确认 QMD 是否已完全卸载。
**验证结果:**
- ✅ qmd 命令已删除
- ✅ wrapper 脚本已删除
- ✅ qmd 数据目录已删除
- ✅ memory 配置已移除
- ✅ Git 提交已完成
---
## [22:06] 今日 Token 使用量查询
### 详情
用户查询今天的 Token 使用明细。
**统计结果:**
| 会话 | Tokens |
|------|--------|
| 主会话 (Feishu) | 68,729 |
| Heartbeat (Webchat) | 37,936 |
| AI早报定时任务 | 20,600 |
| **总计** | **~127k** |
**费用:** $0.0000(硅基流动,当前成本为 0)
---
## [22:17] L2 记录完整性检查
### 详情
用户指出今日 L2 记录不完整,缺少大量重要内容。
**发现的问题:**
1. 只记录了 12:00-17:58 的内容
2. 18:00-22:00 的重要事件全部缺失
3. Self-improvement 技能未实际生效(未记录任何学习)
**缺失内容:**
- QMD 调试完整过程
- GPU 显存问题排查
- wrapper 脚本修改
- QMD 卸载和 Git 提交
- Session 压缩技能查找
- Self-improvement 技能检查
- Token 使用量查询
**根本原因分析:**
1. Self-improvement 技能是被动触发,需要主动记录
2. 心跳脚本扫描可能未及时捕获所有 session 活动
3. 没有实时将重要事件写入 journal
**改进措施:**
- 立即补充完整今天的 L2 记录
- 后续加强实时记录意识
- 定期触发 self-improvement 技能进行记录
---
*此文件属于 L2 详情层 - 按需读取的非常驻上下文*
workspace/memory/milestones/2026-03-skills.md
New file
@@ -0,0 +1,52 @@
# 2026-03 技能扩展里程碑
## 记忆管理技能组
### 2026-03-12 安装
#### 1. find-skills
- **用途**: 发现和安装 Agent 技能
- **触发**: "如何安装 XX 技能"、"有没有做 XX 的技能"
- **位置**: `~/.openclaw/workspace/skills/find-skills/`
#### 2. super-search
- **用途**: 搜索过去的编码记忆和会话记录
- **触发**: "我昨天做了什么"、"我们如何实现 XX"
- **位置**: `~/.openclaw/workspace/.agents/skills/super-search/`
#### 3. memory-merger
- **用途**: 将临时记忆合并为结构化指令
- **语法**: `/memory-merger >domain [scope]`
- **位置**: `~/.openclaw/workspace/.agents/skills/memory-merger/`
#### 4. ontology
- **用途**: 知识图谱存储实体和关系
- **触发**: "记住..."、"链接 X 到 Y"、实体 CRUD
- **位置**: `~/.openclaw/workspace/skills/ontology/`
### 2026-03-14 安装
#### 5. tavily-search
- **用途**: Tavily AI 搜索引擎集成,用于高质量搜索
- **触发**: AI早报新闻搜索、知识查询
- **位置**: `~/.openclaw/workspace/skills/tavily-search/`
- **配置**: API Key 存储于 `openclaw.json` 的 `agents.defaults.env.TAVILY_API_KEY`
- **来源**: [L2 2026-03-14](./../journal/2026-03-14.md#安装并配置-tavily-search-技能)
### 2026-03-15 状态检查
#### 6. self-improving-agent
- **用途**: 记录学习、错误和功能请求,实现持续改进
- **触发**: 操作失败、用户纠正、发现更好方法
- **位置**: `~/.openclaw/workspace/skills/self-improving-agent/`
- **状态**: ⚠️ 已安装但尚未开始记录(.learnings/ 目录文件为空)
- **来源**: [L2 2026-03-15](./../journal/2026-03-15.md#self-improvement-技能状态检查)
## 架构决策
- 采用**三层记忆架构**: L0索引 → L1概览 → L2详情
- 脑机分离: 认知层(根目录) / 能力层(skills/) / 劳作层(workspace/)
---
*此文件属于 L1 概览层 - 按主题组织的里程碑总结*
workspace/memory/milestones/2026-03-workflows.md
New file
@@ -0,0 +1,85 @@
# 2026-03 工作流与规范里程碑
## AI早报定时任务
### 2026-03-13 需求提出与测试
- **背景**: 用户希望每天早上9点收到AI领域新闻早报
- **测试**: 首份测试版早报生成,内容涵盖5条昨日AI重要新闻
- **来源**: [L2 2026-03-13](./../journal/2026-03-13.md#1045-ai早报定时任务需求)
### 2026-03-13 正式创建
- **定时任务配置**:
  ```bash
  openclaw cron add \
    --name "AI早报" \
    --cron "0 9 * * *" \
    --tz "Asia/Shanghai" \
    --channel feishu
  ```
- **内容格式**: 三个分类(AI行业、AI编程、国产大模型),每个1-2条新闻
- **投递**: 每天早上9点通过飞书发送
- **来源**: [L2 2026-03-13](./../journal/2026-03-13.md#1633-创建ai早报定时任务)
### 2026-03-14 格式优化
- **优先级调整**: AI编程相关新闻优先入选
- **特别关注**: Anthropic/ClaudeCode 和 OpenAI/Codex 动态
- **格式定型**: 大标题不使用emoji,保持简洁文字
- **来源**: [L2 2026-03-14](./../journal/2026-03-14.md#0800-ai早报格式定型与定时任务创建)
---
## 沟通工作原则
### 2026-03-13 确立
**两条核心原则:**
1. **即时回复优先** — 只负责与用户沟通,必须立即回复,不能自己先去干活而不回复
2. **耗时任务分离** — 需要时间执行的任务/动作,启动专用subagent处理,主会话负责协调并反馈给用户
**来源**: [L2 2026-03-13](./../journal/2026-03-13.md#1613-确立沟通原则) | [L2 2026-03-14](./../journal/2026-03-14.md#1742-沟通原则确认)
---
## 记录语言规范
### 2026-03-13 中文优先决策
- **背景**: SOUL.md 和 TOOLS.md 原为英文,用户要求中文化
- **决策**: 后续所有记录优先使用中文
- **执行**: 已完成核心配置文件中文化
- **来源**: [L2 2026-03-13](./../journal/2026-03-13.md#2229-中文化核心配置文件)
---
## QMD 问题与卸载决策
### 2026-03-15 问题发现
- **症状**: `ggml_vulkan: Device memory allocation failed` 错误
- **原因**: rerank 模型需要 ~600MB GPU 显存,当前设备不足
- **尝试**: 环境变量 `GGML_USE_CPU=1`、wrapper 脚本均无法解决
- **来源**: [L2 2026-03-15](./../journal/2026-03-15.md#1807-gateway-重启与-qmd-测试)
### 2026-03-15 最终决策
- **决策**: 完全卸载 QMD
- **执行**:
  - 卸载 qmd npm 包
  - 删除 wrapper 脚本
  - 清理数据目录
  - 从 openclaw.json 移除 memory 配置
  - Git 提交更改
- **结论**: QMD rerank 必须使用 GPU,当前硬件不支持
- **来源**: [L2 2026-03-15](./../journal/2026-03-15.md#1813-1859-qmd-深度调试与最终决策)
---
## 三层记忆架构
### 2026-03-12 架构实现
- **架构**: L0索引 → L1概览 → L2详情
- **L0**: MEMORY.md(4KB以内,索引和摘要)
- **L1**: memory/milestones/(按主题组织的里程碑)
- **L2**: memory/journal/(每日详细日志)
- **来源**: [L2 2026-03-12](./../journal/2026-03-12.md#1835-实现三层记忆架构)
---
*此文件属于 L1 概览层 - 按主题组织的重要工作流和规范*
workspace/skills/memory-management/scripts/daily_check.py
@@ -128,10 +128,48 @@
    return None
def parse_timestamp(ts: any) -> Optional[datetime]:
    """
    解析各种格式的时间戳为 datetime 对象
    支持 ISO 8601 字符串和毫秒级 Unix 时间戳
    """
    if not ts:
        return None
    # 如果是数字(毫秒级 Unix 时间戳)
    if isinstance(ts, (int, float)):
        # 毫秒转秒
        ts_sec = ts / 1000 if ts > 1e10 else ts
        try:
            return datetime.fromtimestamp(ts_sec)
        except (ValueError, OSError):
            return None
    # 如果是字符串(ISO 8601 格式)
    if isinstance(ts, str):
        try:
            # 处理带 Z 的 UTC 时间
            ts = ts.replace('Z', '+00:00')
            # Python 3.7+ 支持 fromisoformat
            from datetime import timezone
            dt = datetime.fromisoformat(ts)
            # 转换为本地时间
            if dt.tzinfo is not None:
                dt = dt.replace(tzinfo=None)
            return dt
        except (ValueError, TypeError):
            return None
    return None
def extract_messages_from_session(file_info: Dict) -> List[Dict]:
    """
    从 session 文件中提取所有真实用户消息
    增强版:过滤系统消息,提取实际用户内容
    优化版:
    1. 正确解析消息时间戳(而非使用文件修改时间)
    2. 提取飞书消息中的真实发送时间
    3. 改进内容去重和过滤
    """
    messages = []
    file_path = file_info['path']
@@ -172,13 +210,55 @@
                        
                        # 提取真实用户内容(过滤系统消息)
                        user_content = extract_user_content(text)
                        if user_content:
                            messages.append({
                                'timestamp': record.get("timestamp", ""),
                                'content': user_content[:400],  # 限制长度
                                'session': session_name,
                                'session_time': file_info['mtime'].strftime('%H:%M:%S')
                            })
                        if not user_content:
                            break
                        # 解析时间戳 - 优先级:
                        # 1. record 级别的时间戳(ISO 8601)
                        # 2. message 内部的 timestamp(毫秒 Unix)
                        # 3. 从飞书消息文本中提取时间
                        # 4. 最后使用文件修改时间
                        msg_time = None
                        time_source = "unknown"
                        # 尝试从 record 获取时间戳
                        record_ts = record.get("timestamp")
                        if record_ts:
                            msg_time = parse_timestamp(record_ts)
                            if msg_time:
                                time_source = "record"
                        # 尝试从 message 内部获取时间戳(毫秒 Unix)
                        if not msg_time and "timestamp" in msg:
                            msg_time = parse_timestamp(msg.get("timestamp"))
                            if msg_time:
                                time_source = "message"
                        # 尝试从飞书消息文本中提取时间
                        if not msg_time:
                            feishu_time_match = re.search(r'\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})', text)
                            if feishu_time_match:
                                try:
                                    msg_time = datetime.strptime(feishu_time_match.group(1), "%Y-%m-%d %H:%M:%S")
                                    time_source = "feishu_text"
                                except ValueError:
                                    pass
                        # 最后使用文件修改时间
                        if not msg_time:
                            msg_time = file_info['mtime']
                            time_source = "file_mtime"
                        messages.append({
                            'timestamp': msg_time.isoformat() if msg_time else "",
                            'timestamp_dt': msg_time,
                            'content': user_content[:400],  # 限制长度
                            'session': session_name,
                            'time_str': msg_time.strftime('%H:%M:%S') if msg_time else 'unknown',
                            'time_source': time_source,
                            'content_hash': hash(user_content[:100])  # 用于去重
                        })
                        break
                        
            except json.JSONDecodeError:
@@ -190,19 +270,90 @@
    return messages
def deduplicate_messages(messages: List[Dict]) -> List[Dict]:
    """
    对跨 session 的消息进行去重
    基于内容哈希和时间窗口判断是否为重复消息
    """
    if not messages:
        return []
    # 先按时间排序
    messages.sort(key=lambda x: x.get('timestamp_dt') or datetime.min)
    deduped = []
    seen_hashes = {}  # hash -> (timestamp, content_preview)
    # 时间窗口:5分钟内相同内容的视为重复
    time_window = timedelta(minutes=5)
    for msg in messages:
        content_hash = msg.get('content_hash')
        msg_time = msg.get('timestamp_dt')
        if content_hash is None:
            deduped.append(msg)
            continue
        # 检查是否已有相似消息
        is_duplicate = False
        if content_hash in seen_hashes:
            last_time, last_preview = seen_hashes[content_hash]
            if msg_time and last_time:
                if abs((msg_time - last_time).total_seconds()) < time_window.total_seconds():
                    is_duplicate = True
                    # 保留更详细的消息(更长的内容)
                    if len(msg.get('content', '')) > len(last_preview):
                        # 替换之前的消息
                        for i, existing in enumerate(deduped):
                            if existing.get('content_hash') == content_hash:
                                deduped[i] = msg
                                seen_hashes[content_hash] = (msg_time, msg.get('content', '')[:100])
                                break
        if not is_duplicate:
            deduped.append(msg)
            if content_hash:
                seen_hashes[content_hash] = (msg_time, msg.get('content', '')[:100])
    return deduped
def aggregate_messages_across_sessions(session_files: List[Dict]) -> List[Dict]:
    """
    跨 session 聚合所有消息,按时间排序
    这是解决 session 分割问题的关键函数
    优化版:
    1. 正确解析每条消息的真实时间戳
    2. 跨 session 去重(处理 session 重置导致的重复消息)
    3. 重建完整时间线
    """
    all_messages = []
    
    print(f"\n  正在处理 {len(session_files)} 个 session 文件...")
    for file_info in session_files:
        messages = extract_messages_from_session(file_info)
        all_messages.extend(messages)
        if messages:
            all_messages.extend(messages)
            # 显示时间源统计
            time_sources = {}
            for m in messages:
                src = m.get('time_source', 'unknown')
                time_sources[src] = time_sources.get(src, 0) + 1
            print(f"    📄 {file_info['name'][:30]}...: {len(messages)} 条消息")
            for src, count in time_sources.items():
                print(f"       └─ {src}: {count}")
    if not all_messages:
        return []
    # 去重(处理 session 重置导致的重复)
    print(f"\n  🔄 原始消息数: {len(all_messages)}")
    all_messages = deduplicate_messages(all_messages)
    print(f"  ✅ 去重后消息数: {len(all_messages)}")
    
    # 按时间戳排序,重建完整时间线
    all_messages.sort(key=lambda x: x.get('timestamp', ''))
    all_messages.sort(key=lambda x: x.get('timestamp_dt') or datetime.min)
    
    return all_messages
workspace/skills/memory-management/scripts/weekly_maintenance.py
@@ -2,7 +2,7 @@
"""
每周维护脚本
周一早上9:30执行,负责:
1. 运行memory-merger整理L2→L1
1. 自动合并L2→L1(基于内容筛选策略)
2. 检查L0大小
3. 生成周报
4. 发送报告(可选)
@@ -11,6 +11,7 @@
import os
import sys
import subprocess
import re
from datetime import datetime, timedelta
from pathlib import Path
@@ -20,28 +21,242 @@
    return Path.home() / ".openclaw" / "workspace"
def run_memory_merger() -> tuple:
    """运行memory-merger技能。"""
    workspace = get_workspace_path()
    merger_path = workspace / ".agents" / "skills" / "memory-merger"
def should_merge_to_l1(content: str) -> tuple:
    """
    判断L2内容是否应该合并到L1
    返回: (是否应该合并, 原因/类别)
    """
    content_lower = content.lower()
    
    if not merger_path.exists():
        return False, "memory-merger技能未安装"
    # 策略1: 关键词匹配 - 重要决策类
    decision_keywords = [
        "决策", "决定", "结论", "方案", "选择",
        "采用", "确定", "最终", "resolved", "解决",
        "关键", "重要", "核心", "原则"
    ]
    
    # 运行memory-merger
    # 策略2: 技术方案类
    tech_keywords = [
        "架构", "设计", "实现", "配置", "优化",
        "部署", "迁移", "升级", "重构", "方案"
    ]
    # 策略3: 经验教训类
    lesson_keywords = [
        "教训", "经验", "学习", "注意", "避免",
        "问题", "bug", "错误", "失败原因"
    ]
    # 策略4: 流程规范类
    process_keywords = [
        "流程", "规范", "规则", "约定", "标准",
        "红线", "必须", "禁止", "要求"
    ]
    # 检查匹配
    matched_keywords = []
    category = None
    for kw in decision_keywords:
        if kw in content_lower:
            matched_keywords.append(kw)
            category = "决策记录"
            break
    if not category:
        for kw in tech_keywords:
            if kw in content_lower:
                matched_keywords.append(kw)
                category = "技术方案"
                break
    if not category:
        for kw in lesson_keywords:
            if kw in content_lower:
                matched_keywords.append(kw)
                category = "经验教训"
                break
    if not category:
        for kw in process_keywords:
            if kw in content_lower:
                matched_keywords.append(kw)
                category = "流程规范"
                break
    # 策略5: 内容长度检查(太短的内容不值得合并)
    if len(content) < 200:
        return False, "内容过短"
    # 策略6: 必须有结构化标记(有###标题的才算正式记录)
    has_structure = bool(re.search(r'###\s+', content))
    if not has_structure:
        return False, "缺乏结构化标记"
    if category:
        return True, category
    return False, "未匹配合并策略"
def is_duplicate_in_l1(content: str, l1_file: Path) -> bool:
    """检查内容是否已在L1中存在(简单去重)"""
    if not l1_file.exists():
        return False
    try:
        result = subprocess.run(
            ["python", str(merger_path / "scripts" / "merge.py"), "memory-management"],
            capture_output=True,
            text=True,
            timeout=60
        )
        if result.returncode == 0:
            return True, result.stdout
        else:
            return False, result.stderr
    except Exception as e:
        return False, str(e)
        l1_content = l1_file.read_text(encoding='utf-8')
        # 提取内容前100字作为指纹
        content_fingerprint = content[:100].strip()
        # 检查L1中是否已有相似内容
        return content_fingerprint in l1_content
    except Exception:
        return False
def auto_merge_l2_to_l1() -> dict:
    """
    自动合并本周L2到L1
    返回合并统计信息
    """
    workspace = get_workspace_path()
    journal_dir = workspace / "memory" / "journal"
    milestones_dir = workspace / "memory" / "milestones"
    if not journal_dir.exists():
        return {"status": "no_journal", "merged": 0, "skipped": 0, "details": []}
    # 确保milestones目录存在
    milestones_dir.mkdir(parents=True, exist_ok=True)
    # 获取本周日期范围
    today = datetime.now()
    start_of_week = today - timedelta(days=today.weekday())
    # 本周的L1文件
    current_month = today.strftime("%Y-%m")
    l1_file = milestones_dir / f"{current_month}-weekly.md"
    merged_count = 0
    skipped_count = 0
    details = []
    # 遍历本周L2文件
    for l2_file in sorted(journal_dir.glob("*.md")):
        try:
            file_date = datetime.strptime(l2_file.stem, "%Y-%m-%d")
            if not (start_of_week <= file_date <= today):
                continue
            # 读取L2内容
            l2_content = l2_file.read_text(encoding='utf-8')
            # 按事件分割(按##标题分割)
            events = re.split(r'\n##\s+\[', l2_content)
            for event in events[1:]:  # 第一个通常是文件头
                event = "## [" + event  # 恢复标题标记
                # 提取事件标题
                title_match = re.search(r'##\s*\[.*?\]\s*(.+?)\n', event)
                title = title_match.group(1).strip() if title_match else "未命名事件"
                # 判断是否应该合并
                should_merge, reason = should_merge_to_l1(event)
                if not should_merge:
                    skipped_count += 1
                    details.append({
                        "date": l2_file.stem,
                        "title": title,
                        "action": "跳过",
                        "reason": reason
                    })
                    continue
                # 检查是否重复
                if is_duplicate_in_l1(event, l1_file):
                    skipped_count += 1
                    details.append({
                        "date": l2_file.stem,
                        "title": title,
                        "action": "跳过",
                        "reason": "L1中已存在"
                    })
                    continue
                # 执行合并
                try:
                    # 准备L1格式内容
                    l1_entry = f"""
## [{l2_file.stem}] {title}
**类别**: {reason}
**来源**: [L2详情](./journal/{l2_file.name})
### 摘要
{event[:500]}...
---
"""
                    # 追加到L1文件
                    with open(l1_file, 'a', encoding='utf-8') as f:
                        f.write(l1_entry)
                    merged_count += 1
                    details.append({
                        "date": l2_file.stem,
                        "title": title,
                        "action": "已合并",
                        "category": reason
                    })
                except Exception as e:
                    details.append({
                        "date": l2_file.stem,
                        "title": title,
                        "action": "失败",
                        "reason": str(e)
                    })
        except ValueError:
            continue  # 文件名格式不正确
        except Exception as e:
            details.append({
                "date": l2_file.stem if 'l2_file' in locals() else "unknown",
                "title": "读取失败",
                "action": "错误",
                "reason": str(e)
            })
    return {
        "status": "success" if merged_count > 0 else "no_merge",
        "merged": merged_count,
        "skipped": skipped_count,
        "l1_file": str(l1_file) if merged_count > 0 else None,
        "details": details
    }
def run_memory_merger() -> tuple:
    """运行自动合并(替代原memory-merger调用)"""
    result = auto_merge_l2_to_l1()
    if result["status"] == "no_journal":
        return False, "未找到journal目录"
    if result["status"] == "success":
        summary = f"✅ 合并完成: {result['merged']} 条 → {result['l1_file']}\n"
        summary += f"⏭️  跳过: {result['skipped']} 条\n"
        summary += "\n详细记录:\n"
        for d in result['details'][-5:]:  # 只显示最后5条
            summary += f"  - [{d['date']}] {d['title']}: {d['action']}"
            if 'reason' in d:
                summary += f" ({d['reason']})"
            summary += "\n"
        return True, summary
    else:
        return False, f"未找到可合并内容(本周共扫描 {result['skipped']} 条,均不符合合并条件)"
def check_l0_size() -> dict: