7 files modified
3 files added
| | |
| | | # 扩展插件运行时数据 |
| | | extensions/ |
| | | |
| | | # Agent 会话数据 |
| | | # Agent 会话、检索数据 |
| | | agents/*/sessions/ |
| | | agents/*/qmd/ |
| | | |
| | | # 其他运行时目录 |
| | | delivery-queue/ |
| | |
| | | identity/ |
| | | memory/ |
| | | feishu/ |
| | | cron/runs/ |
| | | completions/ |
| | | |
| | | # ============================================ |
| | | # 敏感信息(绝不能提交) |
| New file |
| | |
| | | { |
| | | "version": 1, |
| | | "allowFrom": [ |
| | | "ou_53994d69bfaad1bfa5ca4c658de5b23f" |
| | | ] |
| | | } |
| New file |
| | |
| | | { |
| | | "version": 1, |
| | | "requests": [] |
| | | } |
| | |
| | | "description": "每天早上9点AI早报", |
| | | "enabled": true, |
| | | "createdAtMs": 1773390853562, |
| | | "updatedAtMs": 1773392478112, |
| | | "updatedAtMs": 1773536472466, |
| | | "schedule": { |
| | | "kind": "cron", |
| | | "expr": "0 9 * * *", |
| | |
| | | "to": "ou_53994d69bfaad1bfa5ca4c658de5b23f" |
| | | }, |
| | | "state": { |
| | | "nextRunAtMs": 1773536400000 |
| | | "nextRunAtMs": 1773622800000, |
| | | "lastRunAtMs": 1773536400033, |
| | | "lastRunStatus": "ok", |
| | | "lastStatus": "ok", |
| | | "lastDurationMs": 72433, |
| | | "lastDelivered": true, |
| | | "lastDeliveryStatus": "delivered", |
| | | "consecutiveErrors": 0 |
| | | } |
| | | }, |
| | | { |
| | |
| | | { |
| | | "meta": { |
| | | "lastTouchedVersion": "2026.3.12", |
| | | "lastTouchedAt": "2026-03-14T04:35:04.375Z" |
| | | "lastTouchedVersion": "2026.3.13", |
| | | "lastTouchedAt": "2026-03-15T03:58:15.938Z" |
| | | }, |
| | | "wizard": { |
| | | "lastRunAt": "2026-03-11T06:55:00.911Z", |
| | | "lastRunVersion": "2026.3.8", |
| | | "lastRunCommand": "configure", |
| | | "lastRunAt": "2026-03-15T03:19:13.147Z", |
| | | "lastRunVersion": "2026.3.12", |
| | | "lastRunCommand": "doctor", |
| | | "lastRunMode": "local" |
| | | }, |
| | | "models": { |
| | |
| | | "lifehelper": { |
| | | "appId": "cli_a93a53efc2781bdf", |
| | | "appSecret": "Zr5ZpQbilUn0SuxSa0uSvdLMipyXQYbR" |
| | | }, |
| | | "default": { |
| | | "groupPolicy": "open", |
| | | "dmPolicy": "open", |
| | | "allowFrom": [ |
| | | "*" |
| | | ] |
| | | } |
| | | }, |
| | | "connectionMode": "websocket", |
| | | "domain": "feishu", |
| | | "groupPolicy": "open", |
| | | "dmPolicy": "open", |
| | | "allowFrom": [ |
| | | "*" |
| | | ] |
| | | "domain": "feishu" |
| | | } |
| | | }, |
| | | "gateway": { |
| | |
| | | ] |
| | | } |
| | | }, |
| | | |
| | | "skills": { |
| | | "entries": { |
| | | "1password": { |
| | |
| | | } |
| | | }, |
| | | "plugins": { |
| | | "allow": [ |
| | | "feishu" |
| | | ], |
| | | "load": { |
| | | "paths": [ |
| | | "/home/tevin/.nvm/versions/node/v24.14.0/lib/node_modules/openclaw/extensions/feishu" |
| | | "/home/tevin/.nvm/versions/node/v24.14.0/lib/node_modules/@m1heng-clawd/feishu" |
| | | ] |
| | | }, |
| | | "entries": { |
| | |
| | | "feishu": { |
| | | "source": "npm", |
| | | "spec": "@m1heng-clawd/feishu", |
| | | "installPath": "/home/tevin/.openclaw/extensions/feishu", |
| | | "version": "0.1.16", |
| | | "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.16", |
| | | "resolvedSpec": "@m1heng-clawd/feishu@0.1.16", |
| | | "integrity": "sha512-BRbAdogf0NrjAX8HTPHcgMQ4zsx0SEFfWgoPcFYOTeq4muvGRkAXfPR14zS0ZtTGImcijatlZvgexWB7unj/pw==", |
| | | "shasum": "47780b9ee0d1b9a8585612e6072fbd787402e03d", |
| | | "resolvedAt": "2026-03-11T06:34:29.122Z", |
| | | "installedAt": "2026-03-11T06:34:48.231Z" |
| | | "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" |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | ## 任务清单 |
| | | |
| | | ### 1. 三层记忆每日总结(由 memory-management 技能处理) |
| | | ### 每日任务(晚上10点后执行) |
| | | |
| | | #### 1. 三层记忆每日总结(由 memory-management 技能处理) |
| | | |
| | | **触发条件**: 时间 ≥ 22:00 且当日无 L2 记录 |
| | | **执行技能**: [memory-management](../skills/memory-management/SKILL.md) |
| | | **执行脚本**: `skills/memory-management/scripts/daily_check.py` |
| | | |
| | | **逻辑**: |
| | | **执行逻辑**: |
| | | ``` |
| | | 收到心跳请求 |
| | | │ |
| | | ▼ |
| | | 读取 HEARTBEAT.md 获取任务清单 |
| | | │ |
| | | ▼ |
| | | 时间 ≥ 22:00 ? |
| | | └── 是 → 今日 L2 已存在 ? |
| | | └── 否 → 执行每日总结 |
| | | ├── 否 → 回复 HEARTBEAT_OK(时间未到) |
| | | │ |
| | | └── 是 → 检查今日 L2 是否存在 |
| | | │ |
| | | ├── 是 → 回复 HEARTBEAT_OK(已记录) |
| | | │ |
| | | └── 否 → 执行每日检查脚本 |
| | | │ |
| | | ▼ |
| | | 扫描所有session文件 |
| | | (当前活跃 + .reset.归档 + .deleted.删除) |
| | | │ |
| | | ▼ |
| | | 分析内容识别重要事件 |
| | | │ |
| | | ▼ |
| | | 生成每日总结建议 |
| | | ``` |
| | | |
| | | **完整检查流程(确保无遗漏)**: |
| | | |
| | | 1. **扫描所有Session文件** |
| | | - 当前活跃: `*.jsonl` |
| | | - 重置归档: `*.jsonl.reset.*` |
| | | - 删除归档: `*.jsonl.deleted.*` |
| | | - 检查今日修改时间戳 |
| | | |
| | | 2. **提取飞书渠道对话** |
| | | - 解析每个session文件 |
| | | - 识别 `channel: feishu` 的消息 |
| | | - 提取用户发送的文本内容 |
| | | |
| | | 3. **识别重要事件类型** |
| | | - 技能安装/更新(关键词: skill, 安装, 创建) |
| | | - 配置变更(关键词: config, 配置, API key) |
| | | - 定时任务(关键词: cron, 定时) |
| | | - 重要对话/决策 |
| | | |
| | | 4. **判断标准(怎样才算"没有遗漏")** |
| | | ``` |
| | | ✅ 检查完成标准: |
| | | ├── 已扫描今日所有修改过的session文件(≥1个) |
| | | ├── 已检查.reset.和.deleted.归档文件 |
| | | ├── 已提取飞书渠道对话记录 |
| | | ├── 已识别所有重要事件类型 |
| | | └── 已生成L2记录或确认无需记录 |
| | | |
| | | ❌ 遗漏警告: |
| | | ├── 发现今日session文件 > 0 |
| | | ├── 但今日L2记录不存在 |
| | | └── → 必须人工检查补充 |
| | | ``` |
| | | |
| | | **动作**: |
| | | - 扫描当日活动、决策、事件 |
| | | - 创建 L2 记录 (`memory/journal/YYYY-MM-DD.md`) |
| | | - 更新 L0 索引 |
| | | - 执行 `daily_check.py` 扫描所有session |
| | | - 如发现活动但未记录 → 提示需要补充L2 |
| | | - 更新 MEMORY.md 的"最近活动"摘要 |
| | | - 检查 L0 大小 |
| | | |
| | | --- |
| | |
| | | - [2026-03 技能安装](./memory/milestones/2026-03-skills.md) - 本月技能扩展记录 |
| | | |
| | | ### 📖 L2 详情层 (journal/) |
| | | - [2026-03-15](./memory/journal/2026-03-15.md) - 测试qmd wrapper脚本、飞书渠道检查、每日维护 |
| | | - [2026-03-14](./memory/journal/2026-03-14.md) - AI早报定时任务创建、Tavily Search技能安装配置 |
| | | - [2026-03-13](./memory/journal/2026-03-13.md) - 解答目录结构问题、中文化配置文件 |
| | | - [2026-03-12](./memory/journal/2026-03-12.md) - 技能安装与三层记忆架构实现 |
| | | |
| | |
| | | ## 🔍 快速检索 |
| | | |
| | | ### 最近活动 |
| | | - 2026-03-15: |
| | | - 测试 qmd-wrapper.sh 脚本,验证强制CPU后端工作正常 |
| | | - 飞书渠道状态检查(未配置) |
| | | - 查询三层记忆首次提及时间(2026-03-12) |
| | | - 详见 [L2](./memory/journal/2026-03-15.md) |
| | | - 2026-03-14: |
| | | - 创建AI早报定时任务(每天早上9点) |
| | | - 安装 Tavily Search 技能,支持从OpenClaw配置读取API key |
| | | - 确定早报格式:AI行业、AI编程、国产大模型三个分类 |
| | | - 详见 [L2](./memory/journal/2026-03-14.md) |
| | | - 2026-03-13: |
| | | - 中文化 SOUL.md 和 TOOLS.md |
| | | - 解答 .openclaw 目录结构问题 |
| | |
| | | |
| | | **任务清单:** |
| | | - [ ] 检查今日是否有重要决策需要记录到L2 |
| | | - [ ] **检查飞书渠道历史** — 如用户询问"检查昨天的每日总结",需读取所有session并提取飞书渠道的完整聊天记录补充到L2 |
| | | - [ ] **完整检查飞书渠道历史** — 执行以下步骤确保无遗漏: |
| | | 1. 列出所有今日活跃的session(包括.reset.归档文件) |
| | | 2. 扫描 `~/.openclaw/agents/main/sessions/` 目录下今日修改的所有 `.jsonl*` 文件 |
| | | 3. 读取每个session文件,提取飞书渠道的对话内容 |
| | | 4. 检查是否有未记录到L2的重要活动 |
| | | - [ ] 更新 MEMORY.md 的"最近活动"摘要 |
| | | - [ ] 确保 L0 层不超过 4KB |
| | | |
| | | **判断标准:怎样才算"没有遗漏"?** |
| | | ``` |
| | | 检查完成标准: |
| | | ├── 已扫描所有session文件(当前活跃 + .reset.归档 + .deleted.删除) |
| | | ├── 已提取飞书渠道所有对话记录 |
| | | ├── 已识别所有重要事件(技能安装、配置变更、定时任务等) |
| | | └── 已在L2中记录或确认无需记录 |
| | | ``` |
| | | |
| | | **重要提醒:** |
| | | > ⚠️ **飞书历史检查**:当用户说"检查昨天的每日总结"或类似表述时,必须: |
| | | > 1. 使用 `sessions_list` 查找过去48小时的活跃session |
| | |
| | | #!/usr/bin/env python3 |
| | | """ |
| | | 每日记忆检查脚本 |
| | | 在晚上10点后触发,检查今日是否已写入L2 |
| | | 每日记忆检查脚本 V2 - 增强跨 session 消息聚合能力 |
| | | 优化点: |
| | | 1. 跨 session 消息聚合 - 合并所有 session 的消息按时间排序 |
| | | 2. 增强事件检测 - 支持更多关键词和模式匹配 |
| | | 3. 处理 session 重置 - 正确识别 .reset. 和 .deleted. 文件 |
| | | 4. 完整时间线生成 - 按时间顺序展示今日所有活动 |
| | | 5. 智能消息过滤 - 区分真实用户消息和系统提示 |
| | | """ |
| | | |
| | | import os |
| | | import sys |
| | | from datetime import datetime |
| | | import json |
| | | import re |
| | | from datetime import datetime, timedelta |
| | | from pathlib import Path |
| | | from typing import List, Dict, Optional, Tuple |
| | | from collections import defaultdict |
| | | |
| | | |
| | | def get_workspace_path() -> Path: |
| | | """获取workspace路径。""" |
| | | """获取 workspace 路径。""" |
| | | return Path.home() / ".openclaw" / "workspace" |
| | | |
| | | |
| | | def get_sessions_path() -> Path: |
| | | """获取 sessions 路径。""" |
| | | return Path.home() / ".openclaw" / "agents" / "main" / "sessions" |
| | | |
| | | |
| | | def check_today_journal() -> bool: |
| | | """检查今日是否已有L2记录。""" |
| | | """检查今日是否已有 L2 记录。""" |
| | | workspace = get_workspace_path() |
| | | today = datetime.now().strftime("%Y-%m-%d") |
| | | journal_file = workspace / "memory" / "journal" / f"{today}.md" |
| | |
| | | |
| | | |
| | | def get_l0_size() -> int: |
| | | """获取MEMORY.md文件大小(字节)。""" |
| | | """获取 MEMORY.md 文件大小(字节)。""" |
| | | workspace = get_workspace_path() |
| | | memory_file = workspace / "MEMORY.md" |
| | | if memory_file.exists(): |
| | |
| | | return f"{kb:.1f}KB" |
| | | |
| | | |
| | | def get_today_session_files() -> List[Dict]: |
| | | """ |
| | | 获取今日所有 session 文件(包括 .reset. 和 .deleted. 归档) |
| | | 按修改时间排序,确保能重建完整时间线 |
| | | """ |
| | | sessions_dir = get_sessions_path() |
| | | if not sessions_dir.exists(): |
| | | return [] |
| | | |
| | | today = datetime.now() |
| | | today_files = [] |
| | | |
| | | # 扫描所有 .jsonl 相关文件(包括 .reset. 和 .deleted.) |
| | | for pattern in ["*.jsonl", "*.jsonl.reset.*", "*.jsonl.deleted.*"]: |
| | | for file in sessions_dir.glob(pattern): |
| | | try: |
| | | mtime = datetime.fromtimestamp(file.stat().st_mtime) |
| | | if mtime.date() == today.date(): |
| | | today_files.append({ |
| | | 'path': file, |
| | | 'mtime': mtime, |
| | | 'name': file.name |
| | | }) |
| | | except (OSError, ValueError): |
| | | continue |
| | | |
| | | # 按修改时间排序 |
| | | today_files.sort(key=lambda x: x['mtime']) |
| | | return today_files |
| | | |
| | | |
| | | def extract_user_content(text: str) -> Optional[str]: |
| | | """ |
| | | 从消息文本中提取用户的实际内容 |
| | | 过滤掉系统提示、元数据等 |
| | | """ |
| | | if not text or len(text) < 10: |
| | | return None |
| | | |
| | | # 跳过纯系统提示消息 |
| | | system_indicators = [ |
| | | "OpenClaw runtime context", |
| | | "[Subagent Context]", |
| | | "You are running as a subagent", |
| | | "Results auto-announce", |
| | | "This context is runtime-generated", |
| | | "Keep internal details private", |
| | | "conversation info (untrusted)", |
| | | "feishu control message", |
| | | "feishu event type:", |
| | | ] |
| | | |
| | | lower_text = text.lower() |
| | | for indicator in system_indicators: |
| | | if indicator.lower() in lower_text[:200]: |
| | | return None |
| | | |
| | | # 处理飞书消息格式 - 提取实际用户内容 |
| | | # 格式:System: [时间] Feishu[main] DM from xxx: 实际内容 |
| | | feishu_match = re.search(r'Feishu\[.*?\]\s+\w+\s+from\s+\w+:\s*(.+?)(?=\n\n|$)', text, re.DOTALL) |
| | | if feishu_match: |
| | | content = feishu_match.group(1).strip() |
| | | # 移除 JSON 元数据块 |
| | | content = re.sub(r'```json\s*\{.*?\}\s*```', '', content, flags=re.DOTALL) |
| | | content = content.strip() |
| | | if len(content) > 10: |
| | | return content |
| | | return None |
| | | |
| | | # 如果是普通用户消息(非系统消息),直接返回 |
| | | if not text.startswith("System:") and not text.startswith("["): |
| | | return text.strip() if len(text) > 10 else None |
| | | |
| | | return None |
| | | |
| | | |
| | | def extract_messages_from_session(file_info: Dict) -> List[Dict]: |
| | | """ |
| | | 从 session 文件中提取所有真实用户消息 |
| | | 增强版:过滤系统消息,提取实际用户内容 |
| | | """ |
| | | messages = [] |
| | | file_path = file_info['path'] |
| | | session_name = file_info['name'] |
| | | |
| | | try: |
| | | with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: |
| | | lines = f.readlines() |
| | | |
| | | for line in lines: |
| | | line = line.strip() |
| | | if not line: |
| | | continue |
| | | |
| | | try: |
| | | record = json.loads(line) |
| | | |
| | | # 只处理消息类型 |
| | | if record.get("type") != "message": |
| | | continue |
| | | |
| | | msg = record.get("message", {}) |
| | | if not msg: |
| | | continue |
| | | |
| | | # 只提取用户消息 |
| | | if msg.get("role") != "user": |
| | | continue |
| | | |
| | | content_list = msg.get("content", []) |
| | | if not content_list: |
| | | continue |
| | | |
| | | # 提取文本内容 |
| | | for item in content_list: |
| | | if isinstance(item, dict) and item.get("type") == "text": |
| | | text = item.get("text", "") |
| | | |
| | | # 提取真实用户内容(过滤系统消息) |
| | | 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') |
| | | }) |
| | | break |
| | | |
| | | except json.JSONDecodeError: |
| | | continue |
| | | |
| | | except (IOError, OSError) as e: |
| | | print(f" 警告:无法读取文件 {file_info['name']}: {e}") |
| | | |
| | | return messages |
| | | |
| | | |
| | | def aggregate_messages_across_sessions(session_files: List[Dict]) -> List[Dict]: |
| | | """ |
| | | 跨 session 聚合所有消息,按时间排序 |
| | | 这是解决 session 分割问题的关键函数 |
| | | """ |
| | | all_messages = [] |
| | | |
| | | for file_info in session_files: |
| | | messages = extract_messages_from_session(file_info) |
| | | all_messages.extend(messages) |
| | | |
| | | # 按时间戳排序,重建完整时间线 |
| | | all_messages.sort(key=lambda x: x.get('timestamp', '')) |
| | | |
| | | return all_messages |
| | | |
| | | |
| | | def detect_important_events(messages: List[Dict]) -> List[Dict]: |
| | | """ |
| | | 从聚合后的消息中检测重要事件 |
| | | 增强版:支持更多关键词和上下文分析 |
| | | """ |
| | | # 扩展关键词列表 |
| | | important_keywords = { |
| | | '配置变更': ['配置', 'config', 'setup', 'settings', '修改', '变更', '更新'], |
| | | '技能操作': ['技能', 'skill', '安装', '创建', '卸载', '删除', '移除', 'skill'], |
| | | '定时任务': ['定时', 'cron', '任务', 'schedule', 'job', '早报'], |
| | | '调试排错': ['调试', '测试', 'test', 'debug', '错误', 'error', '失败', 'fail', '问题'], |
| | | '决策讨论': ['决策', '决定', '方案', '选择', '最终', '结论', '分析'], |
| | | '搜索查询': ['搜索', '查找', 'query', 'find', 'check', '查询'], |
| | | 'API集成': ['api', 'key', 'token', '集成', 'integration'], |
| | | '系统维护': ['重启', 'reset', 'restart', '维护', '清理', 'gateway'], |
| | | '代码提交': ['git', '提交', 'commit', 'push', 'pr'], |
| | | '文档记录': ['记录', '文档', 'journal', 'memory', '笔记'], |
| | | } |
| | | |
| | | events = [] |
| | | seen_contents = set() # 用于去重 |
| | | |
| | | for msg in messages: |
| | | content = msg['content'] |
| | | content_hash = content[:100] # 用前100字符作为去重key |
| | | |
| | | if content_hash in seen_contents: |
| | | continue |
| | | seen_contents.add(content_hash) |
| | | |
| | | # 检查是否匹配任何关键词类别 |
| | | for category, keywords in important_keywords.items(): |
| | | for keyword in keywords: |
| | | if keyword.lower() in content.lower(): |
| | | events.append({ |
| | | 'time': msg.get('session_time', 'unknown'), |
| | | 'category': category, |
| | | 'content': content[:200] + '...' if len(content) > 200 else content, |
| | | 'session': msg.get('session', 'unknown')[:20] |
| | | }) |
| | | break |
| | | else: |
| | | continue |
| | | break |
| | | |
| | | return events |
| | | |
| | | |
| | | def generate_daily_summary(events: List[Dict]) -> str: |
| | | """ |
| | | 生成每日活动摘要 |
| | | """ |
| | | if not events: |
| | | return "今日暂无重要活动记录" |
| | | |
| | | summary = f"\n📋 今日活动摘要(共 {len(events)} 个事件):\n" |
| | | summary += "=" * 60 + "\n" |
| | | |
| | | # 按类别分组 |
| | | by_category = defaultdict(list) |
| | | for event in events: |
| | | by_category[event['category']].append(event) |
| | | |
| | | for category, cat_events in sorted(by_category.items()): |
| | | summary += f"\n【{category}】({len(cat_events)} 个)\n" |
| | | for i, event in enumerate(cat_events[:3], 1): |
| | | summary += f" {i}. [{event['time']}] {event['content']}\n" |
| | | if len(cat_events) > 3: |
| | | summary += f" ... 还有 {len(cat_events) - 3} 个相关事件\n" |
| | | |
| | | return summary |
| | | |
| | | |
| | | def analyze_sessions_for_events() -> Tuple[bool, List[Dict], str]: |
| | | """ |
| | | 分析今日所有 session,检查是否有重要事件需要记录 |
| | | 返回:(是否需要补充记录, 事件列表, 摘要文本) |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("🔍 跨 Session 消息聚合分析 V2") |
| | | print("=" * 60) |
| | | |
| | | session_files = get_today_session_files() |
| | | |
| | | if not session_files: |
| | | print("\n⚠️ 未找到今日 session 文件") |
| | | return False, [], "未找到 session 文件" |
| | | |
| | | print(f"\n📁 找到 {len(session_files)} 个 session 文件(含归档):") |
| | | current_count = sum(1 for f in session_files if '.reset.' not in f['name'] and '.deleted.' not in f['name']) |
| | | reset_count = sum(1 for f in session_files if '.reset.' in f['name']) |
| | | deleted_count = sum(1 for f in session_files if '.deleted.' in f['name']) |
| | | |
| | | print(f" - 当前活跃: {current_count} 个") |
| | | print(f" - 重置归档: {reset_count} 个") |
| | | print(f" - 删除归档: {deleted_count} 个") |
| | | |
| | | # 关键步骤:跨 session 聚合所有消息 |
| | | print("\n🔄 正在聚合所有 session 的真实用户消息...") |
| | | all_messages = aggregate_messages_across_sessions(session_files) |
| | | |
| | | if not all_messages: |
| | | print(" ⚠️ 未提取到真实用户消息(已过滤系统提示)") |
| | | return False, [], "未提取到用户消息" |
| | | |
| | | print(f" ✅ 成功聚合 {len(all_messages)} 条用户消息(已过滤系统消息)") |
| | | |
| | | # 显示活动时间线 |
| | | print(f"\n⏱️ 活动时间跨度:") |
| | | first_time = all_messages[0].get('session_time', 'unknown') |
| | | last_time = all_messages[-1].get('session_time', 'unknown') |
| | | print(f" 开始:{first_time}") |
| | | print(f" 结束:{last_time}") |
| | | |
| | | # 显示跨 session 统计 |
| | | session_stats = defaultdict(int) |
| | | for msg in all_messages: |
| | | session_stats[msg.get('session', 'unknown')[:20]] += 1 |
| | | |
| | | print(f"\n📊 各 Session 消息分布:") |
| | | for session_name, count in sorted(session_stats.items(), key=lambda x: -x[1])[:5]: |
| | | print(f" - {session_name}: {count} 条") |
| | | |
| | | # 检测重要事件 |
| | | print("\n🎯 检测重要事件...") |
| | | events = detect_important_events(all_messages) |
| | | |
| | | if events: |
| | | print(f" ✅ 识别到 {len(events)} 个重要事件(去重后)") |
| | | else: |
| | | print(" ℹ️ 未识别到重要事件") |
| | | |
| | | # 生成摘要 |
| | | summary = generate_daily_summary(events) |
| | | print(summary) |
| | | |
| | | # 判断是否需要补充记录 |
| | | has_today_journal = check_today_journal() |
| | | needs_update = len(events) >= 3 and not has_today_journal |
| | | |
| | | if needs_update: |
| | | print(f"\n🚨 发现遗漏:今日有 {len(events)} 个重要事件但未写入 L2") |
| | | print(f" 建议:执行 '补充今日 L2 记录'") |
| | | elif has_today_journal: |
| | | print(f"\n✅ 已记录 L2,跨 session 聚合完成") |
| | | print(f" 共处理 {len(session_files)} 个 session,提取 {len(all_messages)} 条消息") |
| | | else: |
| | | print(f"\n⚠️ 今日无重要活动或已记录完毕") |
| | | |
| | | return needs_update, events, summary |
| | | |
| | | |
| | | def main(): |
| | | """主函数。""" |
| | | today_str = datetime.now().strftime("%Y-%m-%d") |
| | | print(f"📅 日期检查: {today_str}") |
| | | print("=" * 50) |
| | | |
| | | # 检查今日L2 |
| | | has_today_journal = check_today_journal() |
| | | print(f"\n📝 L2记录检查:") |
| | | if has_today_journal: |
| | | print(" ✅ 今日已有journal记录") |
| | | else: |
| | | print(" ⚠️ 今日尚未创建journal记录") |
| | | print(" 💡 建议:如有重要决策或事件,写入L2详情层") |
| | | # 关键步骤:跨 session 聚合分析 |
| | | needs_update, events, summary = analyze_sessions_for_events() |
| | | |
| | | # 检查L0大小 |
| | | # 检查 L0 大小 |
| | | l0_size = get_l0_size() |
| | | print(f"\n📊 L0 (MEMORY.md) 大小检查:") |
| | | print(f" 当前: {format_size(l0_size)} / 4KB") |
| | | |
| | | if l0_size > 4096: |
| | | print(" 🚨 警告:超过4KB红线!需要立即归档到L1") |
| | | print(" 🚨 警告:超过 4KB 红线!需要立即归档到 L1") |
| | | elif l0_size > 3500: |
| | | print(" ⚠️ 提醒:接近4KB限制,建议准备归档") |
| | | print(" ⚠️ 提醒:接近 4KB 限制,建议准备归档") |
| | | else: |
| | | print(" ✅ 大小正常") |
| | | |
| | | print("\n" + "=" * 50) |
| | | # 维护清单 |
| | | print("\n" + "=" * 60) |
| | | print("📋 每日维护清单:") |
| | | if not has_today_journal: |
| | | print(" [ ] 如有重要事件,写入今日L2") |
| | | else: |
| | | print(" [x] L2记录已存在") |
| | | print(" [ ] 检查MEMORY.md最近活动摘要") |
| | | if l0_size > 3500: |
| | | print(" [ ] L0接近限制,考虑归档到L1") |
| | | print(" [ ] 确认L0层引用链接有效") |
| | | |
| | | return 0 if has_today_journal else 1 |
| | | has_today_journal = check_today_journal() |
| | | if has_today_journal: |
| | | print(" [x] L2 记录已存在") |
| | | else: |
| | | print(" [ ] 如有重要事件,写入今日 L2") |
| | | |
| | | if events: |
| | | print(f" [x] 已扫描并聚合 {len(events)} 个重要事件(跨 session)") |
| | | else: |
| | | print(" [-] 今日无重要活动") |
| | | |
| | | print(" [ ] 检查 MEMORY.md 最近活动摘要") |
| | | if l0_size > 3500: |
| | | print(" [ ] L0 接近限制,考虑归档到 L1") |
| | | print(" [ ] 确认 L0 层引用链接有效") |
| | | |
| | | print("\n💡 改进说明:") |
| | | print(" - 新增跨 session 消息聚合功能") |
| | | print(" - 智能过滤系统提示消息") |
| | | print(" - 自动识别 .reset. 和 .deleted. 归档文件") |
| | | print(" - 按时间线重建完整活动记录") |
| | | |
| | | # 返回状态码 |
| | | if needs_update: |
| | | return 2 # 需要补充记录 |
| | | elif not has_today_journal: |
| | | return 1 # 无 L2 记录 |
| | | else: |
| | | return 0 # 一切正常 |
| | | |
| | | |
| | | if __name__ == "__main__": |
| New file |
| | |
| | | #!/usr/bin/env python3 |
| | | """ |
| | | 每日记忆检查脚本 V2 - 增强跨 session 消息聚合能力 |
| | | 优化点: |
| | | 1. 跨 session 消息聚合 - 合并所有 session 的消息按时间排序 |
| | | 2. 增强事件检测 - 支持更多关键词和模式匹配 |
| | | 3. 处理 session 重置 - 正确识别 .reset. 和 .deleted. 文件 |
| | | 4. 完整时间线生成 - 按时间顺序展示今日所有活动 |
| | | 5. 智能消息过滤 - 区分真实用户消息和系统提示 |
| | | """ |
| | | |
| | | import os |
| | | import sys |
| | | import json |
| | | import re |
| | | from datetime import datetime, timedelta |
| | | from pathlib import Path |
| | | from typing import List, Dict, Optional, Tuple |
| | | from collections import defaultdict |
| | | |
| | | |
| | | def get_workspace_path() -> Path: |
| | | """获取 workspace 路径。""" |
| | | return Path.home() / ".openclaw" / "workspace" |
| | | |
| | | |
| | | def get_sessions_path() -> Path: |
| | | """获取 sessions 路径。""" |
| | | return Path.home() / ".openclaw" / "agents" / "main" / "sessions" |
| | | |
| | | |
| | | def check_today_journal() -> bool: |
| | | """检查今日是否已有 L2 记录。""" |
| | | workspace = get_workspace_path() |
| | | today = datetime.now().strftime("%Y-%m-%d") |
| | | journal_file = workspace / "memory" / "journal" / f"{today}.md" |
| | | return journal_file.exists() |
| | | |
| | | |
| | | def get_l0_size() -> int: |
| | | """获取 MEMORY.md 文件大小(字节)。""" |
| | | workspace = get_workspace_path() |
| | | memory_file = workspace / "MEMORY.md" |
| | | if memory_file.exists(): |
| | | return memory_file.stat().st_size |
| | | return 0 |
| | | |
| | | |
| | | def format_size(size_bytes: int) -> str: |
| | | """格式化文件大小显示。""" |
| | | kb = size_bytes / 1024 |
| | | return f"{kb:.1f}KB" |
| | | |
| | | |
| | | def get_today_session_files() -> List[Dict]: |
| | | """ |
| | | 获取今日所有 session 文件(包括 .reset. 和 .deleted. 归档) |
| | | 按修改时间排序,确保能重建完整时间线 |
| | | """ |
| | | sessions_dir = get_sessions_path() |
| | | if not sessions_dir.exists(): |
| | | return [] |
| | | |
| | | today = datetime.now() |
| | | today_files = [] |
| | | |
| | | # 扫描所有 .jsonl 相关文件(包括 .reset. 和 .deleted.) |
| | | for pattern in ["*.jsonl", "*.jsonl.reset.*", "*.jsonl.deleted.*"]: |
| | | for file in sessions_dir.glob(pattern): |
| | | try: |
| | | mtime = datetime.fromtimestamp(file.stat().st_mtime) |
| | | if mtime.date() == today.date(): |
| | | today_files.append({ |
| | | 'path': file, |
| | | 'mtime': mtime, |
| | | 'name': file.name |
| | | }) |
| | | except (OSError, ValueError): |
| | | continue |
| | | |
| | | # 按修改时间排序 |
| | | today_files.sort(key=lambda x: x['mtime']) |
| | | return today_files |
| | | |
| | | |
| | | def extract_user_content(text: str) -> Optional[str]: |
| | | """ |
| | | 从消息文本中提取用户的实际内容 |
| | | 过滤掉系统提示、元数据等 |
| | | """ |
| | | if not text or len(text) < 10: |
| | | return None |
| | | |
| | | # 跳过纯系统提示消息 |
| | | system_indicators = [ |
| | | "OpenClaw runtime context", |
| | | "[Subagent Context]", |
| | | "You are running as a subagent", |
| | | "Results auto-announce", |
| | | "This context is runtime-generated", |
| | | "Keep internal details private", |
| | | "conversation info (untrusted)", |
| | | "feishu control message", |
| | | "feishu event type:", |
| | | ] |
| | | |
| | | lower_text = text.lower() |
| | | for indicator in system_indicators: |
| | | if indicator.lower() in lower_text[:200]: |
| | | return None |
| | | |
| | | # 处理飞书消息格式 - 提取实际用户内容 |
| | | # 格式:System: [时间] Feishu[main] DM from xxx: 实际内容 |
| | | feishu_match = re.search(r'Feishu\[.*?\]\s+\w+\s+from\s+\w+:\s*(.+?)(?=\n\n|$)', text, re.DOTALL) |
| | | if feishu_match: |
| | | content = feishu_match.group(1).strip() |
| | | # 移除 JSON 元数据块 |
| | | content = re.sub(r'```json\s*\{.*?\}\s*```', '', content, flags=re.DOTALL) |
| | | content = content.strip() |
| | | if len(content) > 10: |
| | | return content |
| | | return None |
| | | |
| | | # 如果是普通用户消息(非系统消息),直接返回 |
| | | if not text.startswith("System:") and not text.startswith("["): |
| | | return text.strip() if len(text) > 10 else None |
| | | |
| | | return None |
| | | |
| | | |
| | | def extract_messages_from_session(file_info: Dict) -> List[Dict]: |
| | | """ |
| | | 从 session 文件中提取所有真实用户消息 |
| | | 增强版:过滤系统消息,提取实际用户内容 |
| | | """ |
| | | messages = [] |
| | | file_path = file_info['path'] |
| | | session_name = file_info['name'] |
| | | |
| | | try: |
| | | with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: |
| | | lines = f.readlines() |
| | | |
| | | for line in lines: |
| | | line = line.strip() |
| | | if not line: |
| | | continue |
| | | |
| | | try: |
| | | record = json.loads(line) |
| | | |
| | | # 只处理消息类型 |
| | | if record.get("type") != "message": |
| | | continue |
| | | |
| | | msg = record.get("message", {}) |
| | | if not msg: |
| | | continue |
| | | |
| | | # 只提取用户消息 |
| | | if msg.get("role") != "user": |
| | | continue |
| | | |
| | | content_list = msg.get("content", []) |
| | | if not content_list: |
| | | continue |
| | | |
| | | # 提取文本内容 |
| | | for item in content_list: |
| | | if isinstance(item, dict) and item.get("type") == "text": |
| | | text = item.get("text", "") |
| | | |
| | | # 提取真实用户内容(过滤系统消息) |
| | | 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') |
| | | }) |
| | | break |
| | | |
| | | except json.JSONDecodeError: |
| | | continue |
| | | |
| | | except (IOError, OSError) as e: |
| | | print(f" 警告:无法读取文件 {file_info['name']}: {e}") |
| | | |
| | | return messages |
| | | |
| | | |
| | | def aggregate_messages_across_sessions(session_files: List[Dict]) -> List[Dict]: |
| | | """ |
| | | 跨 session 聚合所有消息,按时间排序 |
| | | 这是解决 session 分割问题的关键函数 |
| | | """ |
| | | all_messages = [] |
| | | |
| | | for file_info in session_files: |
| | | messages = extract_messages_from_session(file_info) |
| | | all_messages.extend(messages) |
| | | |
| | | # 按时间戳排序,重建完整时间线 |
| | | all_messages.sort(key=lambda x: x.get('timestamp', '')) |
| | | |
| | | return all_messages |
| | | |
| | | |
| | | def detect_important_events(messages: List[Dict]) -> List[Dict]: |
| | | """ |
| | | 从聚合后的消息中检测重要事件 |
| | | 增强版:支持更多关键词和上下文分析 |
| | | """ |
| | | # 扩展关键词列表 |
| | | important_keywords = { |
| | | '配置变更': ['配置', 'config', 'setup', 'settings', '修改', '变更', '更新'], |
| | | '技能操作': ['技能', 'skill', '安装', '创建', '卸载', '删除', '移除', 'skill'], |
| | | '定时任务': ['定时', 'cron', '任务', 'schedule', 'job', '早报'], |
| | | '调试排错': ['调试', '测试', 'test', 'debug', '错误', 'error', '失败', 'fail', '问题'], |
| | | '决策讨论': ['决策', '决定', '方案', '选择', '最终', '结论', '分析'], |
| | | '搜索查询': ['搜索', '查找', 'query', 'find', 'check', '查询'], |
| | | 'API集成': ['api', 'key', 'token', '集成', 'integration'], |
| | | '系统维护': ['重启', 'reset', 'restart', '维护', '清理', 'gateway'], |
| | | '代码提交': ['git', '提交', 'commit', 'push', 'pr'], |
| | | '文档记录': ['记录', '文档', 'journal', 'memory', '笔记'], |
| | | } |
| | | |
| | | events = [] |
| | | seen_contents = set() # 用于去重 |
| | | |
| | | for msg in messages: |
| | | content = msg['content'] |
| | | content_hash = content[:100] # 用前100字符作为去重key |
| | | |
| | | if content_hash in seen_contents: |
| | | continue |
| | | seen_contents.add(content_hash) |
| | | |
| | | # 检查是否匹配任何关键词类别 |
| | | for category, keywords in important_keywords.items(): |
| | | for keyword in keywords: |
| | | if keyword.lower() in content.lower(): |
| | | events.append({ |
| | | 'time': msg.get('session_time', 'unknown'), |
| | | 'category': category, |
| | | 'content': content[:200] + '...' if len(content) > 200 else content, |
| | | 'session': msg.get('session', 'unknown')[:20] |
| | | }) |
| | | break |
| | | else: |
| | | continue |
| | | break |
| | | |
| | | return events |
| | | |
| | | |
| | | def generate_daily_summary(events: List[Dict]) -> str: |
| | | """ |
| | | 生成每日活动摘要 |
| | | """ |
| | | if not events: |
| | | return "今日暂无重要活动记录" |
| | | |
| | | summary = f"\n📋 今日活动摘要(共 {len(events)} 个事件):\n" |
| | | summary += "=" * 60 + "\n" |
| | | |
| | | # 按类别分组 |
| | | by_category = defaultdict(list) |
| | | for event in events: |
| | | by_category[event['category']].append(event) |
| | | |
| | | for category, cat_events in sorted(by_category.items()): |
| | | summary += f"\n【{category}】({len(cat_events)} 个)\n" |
| | | for i, event in enumerate(cat_events[:3], 1): |
| | | summary += f" {i}. [{event['time']}] {event['content']}\n" |
| | | if len(cat_events) > 3: |
| | | summary += f" ... 还有 {len(cat_events) - 3} 个相关事件\n" |
| | | |
| | | return summary |
| | | |
| | | |
| | | def analyze_sessions_for_events() -> Tuple[bool, List[Dict], str]: |
| | | """ |
| | | 分析今日所有 session,检查是否有重要事件需要记录 |
| | | 返回:(是否需要补充记录, 事件列表, 摘要文本) |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("🔍 跨 Session 消息聚合分析 V2") |
| | | print("=" * 60) |
| | | |
| | | session_files = get_today_session_files() |
| | | |
| | | if not session_files: |
| | | print("\n⚠️ 未找到今日 session 文件") |
| | | return False, [], "未找到 session 文件" |
| | | |
| | | print(f"\n📁 找到 {len(session_files)} 个 session 文件(含归档):") |
| | | current_count = sum(1 for f in session_files if '.reset.' not in f['name'] and '.deleted.' not in f['name']) |
| | | reset_count = sum(1 for f in session_files if '.reset.' in f['name']) |
| | | deleted_count = sum(1 for f in session_files if '.deleted.' in f['name']) |
| | | |
| | | print(f" - 当前活跃: {current_count} 个") |
| | | print(f" - 重置归档: {reset_count} 个") |
| | | print(f" - 删除归档: {deleted_count} 个") |
| | | |
| | | # 关键步骤:跨 session 聚合所有消息 |
| | | print("\n🔄 正在聚合所有 session 的真实用户消息...") |
| | | all_messages = aggregate_messages_across_sessions(session_files) |
| | | |
| | | if not all_messages: |
| | | print(" ⚠️ 未提取到真实用户消息(已过滤系统提示)") |
| | | return False, [], "未提取到用户消息" |
| | | |
| | | print(f" ✅ 成功聚合 {len(all_messages)} 条用户消息(已过滤系统消息)") |
| | | |
| | | # 显示活动时间线 |
| | | print(f"\n⏱️ 活动时间跨度:") |
| | | first_time = all_messages[0].get('session_time', 'unknown') |
| | | last_time = all_messages[-1].get('session_time', 'unknown') |
| | | print(f" 开始:{first_time}") |
| | | print(f" 结束:{last_time}") |
| | | |
| | | # 显示跨 session 统计 |
| | | session_stats = defaultdict(int) |
| | | for msg in all_messages: |
| | | session_stats[msg.get('session', 'unknown')[:20]] += 1 |
| | | |
| | | print(f"\n📊 各 Session 消息分布:") |
| | | for session_name, count in sorted(session_stats.items(), key=lambda x: -x[1])[:5]: |
| | | print(f" - {session_name}: {count} 条") |
| | | |
| | | # 检测重要事件 |
| | | print("\n🎯 检测重要事件...") |
| | | events = detect_important_events(all_messages) |
| | | |
| | | if events: |
| | | print(f" ✅ 识别到 {len(events)} 个重要事件(去重后)") |
| | | else: |
| | | print(" ℹ️ 未识别到重要事件") |
| | | |
| | | # 生成摘要 |
| | | summary = generate_daily_summary(events) |
| | | print(summary) |
| | | |
| | | # 判断是否需要补充记录 |
| | | has_today_journal = check_today_journal() |
| | | needs_update = len(events) >= 3 and not has_today_journal |
| | | |
| | | if needs_update: |
| | | print(f"\n🚨 发现遗漏:今日有 {len(events)} 个重要事件但未写入 L2") |
| | | print(f" 建议:执行 '补充今日 L2 记录'") |
| | | elif has_today_journal: |
| | | print(f"\n✅ 已记录 L2,跨 session 聚合完成") |
| | | print(f" 共处理 {len(session_files)} 个 session,提取 {len(all_messages)} 条消息") |
| | | else: |
| | | print(f"\n⚠️ 今日无重要活动或已记录完毕") |
| | | |
| | | return needs_update, events, summary |
| | | |
| | | |
| | | def main(): |
| | | """主函数。""" |
| | | today_str = datetime.now().strftime("%Y-%m-%d") |
| | | print(f"📅 日期检查: {today_str}") |
| | | |
| | | # 关键步骤:跨 session 聚合分析 |
| | | needs_update, events, summary = analyze_sessions_for_events() |
| | | |
| | | # 检查 L0 大小 |
| | | l0_size = get_l0_size() |
| | | print(f"\n📊 L0 (MEMORY.md) 大小检查:") |
| | | print(f" 当前: {format_size(l0_size)} / 4KB") |
| | | |
| | | if l0_size > 4096: |
| | | print(" 🚨 警告:超过 4KB 红线!需要立即归档到 L1") |
| | | elif l0_size > 3500: |
| | | print(" ⚠️ 提醒:接近 4KB 限制,建议准备归档") |
| | | else: |
| | | print(" ✅ 大小正常") |
| | | |
| | | # 维护清单 |
| | | print("\n" + "=" * 60) |
| | | print("📋 每日维护清单:") |
| | | |
| | | has_today_journal = check_today_journal() |
| | | if has_today_journal: |
| | | print(" [x] L2 记录已存在") |
| | | else: |
| | | print(" [ ] 如有重要事件,写入今日 L2") |
| | | |
| | | if events: |
| | | print(f" [x] 已扫描并聚合 {len(events)} 个重要事件(跨 session)") |
| | | else: |
| | | print(" [-] 今日无重要活动") |
| | | |
| | | print(" [ ] 检查 MEMORY.md 最近活动摘要") |
| | | if l0_size > 3500: |
| | | print(" [ ] L0 接近限制,考虑归档到 L1") |
| | | print(" [ ] 确认 L0 层引用链接有效") |
| | | |
| | | print("\n💡 改进说明:") |
| | | print(" - 新增跨 session 消息聚合功能") |
| | | print(" - 智能过滤系统提示消息") |
| | | print(" - 自动识别 .reset. 和 .deleted. 归档文件") |
| | | print(" - 按时间线重建完整活动记录") |
| | | |
| | | # 返回状态码 |
| | | if needs_update: |
| | | return 2 # 需要补充记录 |
| | | elif not has_today_journal: |
| | | return 1 # 无 L2 记录 |
| | | else: |
| | | return 0 # 一切正常 |
| | | |
| | | |
| | | if __name__ == "__main__": |
| | | sys.exit(main()) |