9 files modified
6 files added
| | |
| | | devices/ |
| | | subagents/ |
| | | identity/ |
| | | memory/ |
| | | feishu/ |
| | | cron/runs/ |
| | | completions/ |
| | | |
| | | # 记忆管理 |
| | | memory/ |
| | | !*/memory/ |
| | | */memory/ontology/ |
| | | |
| | | # ============================================ |
| | | # 敏感信息(绝不能提交) |
| | | # ============================================ |
| | |
| | | "providers": { |
| | | "custom-api-siliconflow-cn": { |
| | | "baseUrl": "https://api.siliconflow.cn", |
| | | "apiKey": "sk-hjxtzyxeoagiqozjdifstbmzmtdmmpiupquzfvoicyfnfnmy", |
| | | "apiKey": "${SILICONFLOW_API_KEY}", |
| | | "api": "openai-completions", |
| | | "models": [ |
| | | { |
| | |
| | | "providers": { |
| | | "custom-api-siliconflow-cn": { |
| | | "baseUrl": "https://api.siliconflow.cn", |
| | | "apiKey": "SILICONFLOW_CN_API_KEY", |
| | | "apiKey": "${SILICONFLOW_API_KEY}", |
| | | "api": "openai-completions", |
| | | "models": [ |
| | | { |
| | |
| | | "description": "每天早上9点AI早报", |
| | | "enabled": true, |
| | | "createdAtMs": 1773390853562, |
| | | "updatedAtMs": 1773536472466, |
| | | "updatedAtMs": 1773624106557, |
| | | "schedule": { |
| | | "kind": "cron", |
| | | "expr": "0 9 * * *", |
| | |
| | | "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 |
| | |
| | | "name": "memory-weekly-maintenance", |
| | | "enabled": true, |
| | | "createdAtMs": 1773409688576, |
| | | "updatedAtMs": 1773409688576, |
| | | "updatedAtMs": 1773624926494, |
| | | "schedule": { |
| | | "kind": "cron", |
| | | "expr": "30 9 * * 1", |
| | |
| | | "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 |
| | | } |
| | | } |
| | | ] |
| | |
| | | ] |
| | | } |
| | | }, |
| | | "domain": "feishu", |
| | | "connectionMode": "websocket", |
| | | "domain": "feishu" |
| | | "streaming": true, |
| | | "timeout": 2000, |
| | | "idempotent": true, |
| | | "sessionMode": "per-chat", |
| | | "requireMention": true, |
| | | "footer": { |
| | | "elapsed": true, |
| | | "status": true |
| | | } |
| | | } |
| | | }, |
| | | "gateway": { |
| | |
| | | ], |
| | | "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" |
| | | } |
| | | } |
| | | } |
| | |
| | | 记录学习、改进和最佳实践。 |
| | | |
| | | --- |
| | | |
| | | ## [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会话隔离 |
| | | |
| | | --- |
| | |
| | | |
| | | ### 🗂️ 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脚本、飞书渠道检查、每日维护 |
| New file |
| | |
| | | # 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 详情层 - 按需读取的非常驻上下文* |
| New file |
| | |
| | | # 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. **记录语言**:后续所有记录优先使用中文 |
| New file |
| | |
| | | # 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搜索在早报中的实际表现 |
| New file |
| | |
| | | # 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 详情层 - 按需读取的非常驻上下文* |
| New file |
| | |
| | | # 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 概览层 - 按主题组织的里程碑总结* |
| New file |
| | |
| | | # 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 概览层 - 按主题组织的重要工作流和规范* |
| | |
| | | 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'] |
| | |
| | | |
| | | # 提取真实用户内容(过滤系统消息) |
| | | 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: |
| | |
| | | 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 |
| | | |
| | |
| | | """ |
| | | 每周维护脚本 |
| | | 周一早上9:30执行,负责: |
| | | 1. 运行memory-merger整理L2→L1 |
| | | 1. 自动合并L2→L1(基于内容筛选策略) |
| | | 2. 检查L0大小 |
| | | 3. 生成周报 |
| | | 4. 发送报告(可选) |
| | |
| | | import os |
| | | import sys |
| | | import subprocess |
| | | import re |
| | | from datetime import datetime, timedelta |
| | | from pathlib import Path |
| | | |
| | |
| | | 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: |