Skill: anfengxiaoguo/conversation-archive (version 1.0.0)

--- name: conversation-archive version: 1.0.0 description: "对话记忆仓库：自动归档 session 对话，保留原始记录，支持检索和误解纠正。可与 memory-never-forget 联动形成完整记忆体系。" author: Simon tags: ["memory", "conversation", "archive", "openclaw", "session", "recall"] metadata: { "openclaw": { "emoji": "💬" } } license: MIT --- > ⚠️ 作者信息待填写：ClawHub 上传前需要确认你的 ClawHub/GitHub 用户名 # 💬 Conversation Archive（对话记忆仓库） ## 核心理念 **「记忆会骗人，但对话记录不会。」** 当缪斯说「我不记得了」「我可能理解错了」——去对话仓库里找原始记录，比问老豆更快、更准。 --- ## 工作流程 ``` Session 结束 ↓ 1. ARCHIVE（归档） - 保存原始对话 JSON - 生成结构化摘要 - 提取关键信息（主题/决策/教训） ↓ 2. INDEX（索引） - 按日期/主题/关键词写入 index.json - （未来：生成 embedding 向量） ↓ 3. RETRIEVE（检索） - 当需要回溯时 → 搜对话仓库 - 当发现误解时 → 拉原始记录纠正 ↓ 4. INTEGRATE（整合） - 与 memory-never-forget 联动 - 从归档中提取值得记忆的内容 → 4层分类 ``` --- ## 目录结构 ``` conversation_archive/ ├── sessions/ # 原始对话存档 │ └── YYYY-MM/ │ └── {session_id}.json ├── index.json # 内存索引 └── embeddings/ # 预留：向量索引（未来） ``` --- ## Session JSON 格式 ```json { "sessionId": "ccba19e7-...", "date": "2026-04-07", "channel": "webchat", "participants": ["Simon", "Muse"], "topics": ["OpenClaw升级", "Dreaming", "记忆系统"], "decisions": [ {"text": "开启Dreaming每天0点运行", "context": "老豆同意"} ], "feedback": [ {"user": "不要用Markdown表格", "from": "Simon"} ], "summary": "讨论了OpenClaw升级和记忆系统...", "messages": [ {"role": "user", "content": "...", "time": "..."}, {"role": "assistant", "content": "...", "time": "..."} ], "archivedAt": "2026-04-07T17:30:00+08:00" } ``` --- ## 工具（Tools） ### 1. `archive_session` — 归档当前 session **触发时机：** session 结束前 / 手动触发 / cron 触发 ```json { "name": "archive_session", "arguments": { "sessionKey": "agent:main:main", "includeMessages": true } } ``` **自动触发逻辑：** - 当 session 被 compact 或 restart 时自动归档 - 每次 webchat 超过 30 分钟无活动时归档 ### 2. `search_archive` — 搜索对话 **触发时机：** 老豆问「我之前说过什么」「那次对话」「记得3月29日吗」 ```json { "name": "search_archive", "arguments": { "query": "OpenClaw升级", "date": "2026-03-29", "limit": 5 } } ``` ### 3. `get_session` — 获取原始对话 **触发时机：** 发现缪斯理解错了，用原始记录纠正 ```json { "name": "get_session", "arguments": { "sessionId": "ccba19e7-..." } } ``` ### 4. `extract_memories` — 从归档中提取记忆 **触发时机：** 与 memory-never-forget 联动时 ```json { "name": "extract_memories", "arguments": { "sessionId": "ccba19e7-...", "types": ["user", "feedback", "project"] } } ``` --- ## 检索策略 ### 当前（关键词模式） ```python # 搜索逻辑 1. 在 index.json 中模糊匹配 query 2. 匹配 topic、keywords、summary 字段 3. 按日期倒序返回 top N 4. 展示匹配的摘要片段 ``` ### 未来（向量模式） 当 embedding 配置好后： ```python # 向量搜索 1. query → embedding 向量 2. 在 embeddings/ 目录做余弦相似度搜索 3. 返回 top N 最语义相关的对话 ``` **接入条件：** `agents.defaults.memorySearch` 配置了 embedding provider --- ## 与 memory-never-forget 的联动 ``` conversation_archive ──→ memory-never-forget │ │ │ 提取值得记忆的内容 │ 使用4层分类 ↓ ↓ decisions/feedback/ memory/{user,feedback, project/ project,reference}/ │ │ └───────────升华──────────────→ knowledge/ ``` **联动触发：** 每次归档后，自动调用 `extract_memories` 把值得记忆的内容传给 memory-never-forget 处理。 --- ## 自动归档规则 **立即归档：** - session restart / compact 时 - 超过 30 分钟无活动时 - 每天 23:59 强制归档所有活跃 session **保留对话：** - 最近 7 天：完整原始记录 - 7-30 天：仅摘要（messages 字段清除） - 30 天以上：压缩归档，保留摘要和元数据 **安全清理：** - 不存 API key、token、密码等敏感信息 - messages 字段在归档时做敏感信息扫描和脱敏 --- ## 误解纠正流程 当缪斯意识到自己可能理解错了： 1. 用 `search_archive` 搜相关对话 2. 用 `get_session` 拉原始记录 3. 核对原始对话，确认误解点 4. 更新对应分类记忆（feedback/） 5. 在这次 session 末尾做修正性回复 --- ## 触发关键词 | 老豆说 | 缪斯做 | |--------|--------| | 「我之前说过...」 | `search_archive(query="...")` | | 「那天...」 | `search_archive(date="YYYY-MM-DD")` | | 「你不记得了？」 | `search_archive` + 展示原始记录 | | 「理解错了」 | `get_session` + 纠正 + 更新记忆 | --- *版本：v1.0 | 日期：2026-04-07 | 对话记忆仓库 + 未来向量检索预留*

path: scripts/archive_session.py
content: #!/usr/bin/env python3
"""
Conversation Archive - Session Archiver
归档 OpenClaw session 对话，生成结构化 JSON 存档
"""

import json
import os
import sys
import re
from datetime import datetime, timedelta

ARCHIVE_DIR = os.path.expanduser("~/.openclaw/workspace/conversation_archive")
INDEX_FILE = os.path.join(ARCHIVE_DIR, "index.json")

def load_index():
    if os.path.exists(INDEX_FILE):
        with open(INDEX_FILE) as f:
            return json.load(f)
    return []

def save_index(index):
    with open(INDEX_FILE, "w") as f:
        json.dump(index, f, ensure_ascii=False, indent=2)

def extract_topics(messages):
    """从对话中提取主题关键词"""
    topics = set()
    keywords = ["升级", "记忆", "skill", "安装", "删除", "配置", "飞书", "视频", "音频", "session", "清理", "搜索"]
    
    for msg in messages:
        content = msg.get("content", "")
        if isinstance(content, list):
            for c in content:
                if isinstance(c, dict) and c.get("type") == "text":
                    content = c.get("text", "")
                    break
        if isinstance(content, str):
            for kw in keywords:
                if kw.lower() in content.lower():
                    topics.add(kw)
    return list(topics)

def extract_decisions(messages):
    """提取决策类语句"""
    decisions = []
    for msg in messages:
        content = msg.get("content", "")
        if isinstance(content, list):
            for c in content:
                if isinstance(c, dict) and c.get("type") == "text":
                    content = c.get("text", "")
                    break
        if isinstance(content, str):
            if any(kw in content for kw in ["好的", "同意", "开始", "就这个", "没问题"]):
                if len(content) < 200:
                    decisions.append({"text": content[:100], "source": "user" if msg.get("role") == "user" else "assistant"})
    return decisions[:5]  # 最多5条

def extract_feedback(messages):
    """提取反馈/纠正类语句"""
    feedback = []
    for msg in messages:
        content = msg.get("content", "")
        if isinstance(content, list):
            for c in content:
                if isinstance(c, dict) and c.get("type") == "text":
                    content = c.get("text", "")
                    break
        if isinstance(content, str):
            if any(kw in content for kw in ["不对", "错了", "不是这样", "不要", "应该", "改成"]):
                feedback.append({"user": content[:150], "from": "user" if msg.get("role") == "user" else "assistant"})
    return feedback[:5]

def generate_summary(messages):
    """生成对话摘要"""
    user_msgs = []
    assistant_msgs = []
    for msg in messages:
        content = msg.get("content", "")
        if isinstance(content, list):
            for c in content:
                if isinstance(c, dict) and c.get("type") == "text":
                    content = c.get("text", "")
                    break
        if isinstance(content, str) and content.strip():
            if msg.get("role") == "user":
                user_msgs.append(content)
            else:
                assistant_msgs.append(content)
    
    # 简单摘要：取前3条用户消息的核心词
    summary_parts = []
    for msg in user_msgs[:3]:
        # 取前50字
        excerpt = msg[:50].strip()
        if excerpt:
            summary_parts.append(excerpt)
    
    return " | ".join(summary_parts) if summary_parts else "常规对话"

def archive_session(session_id, messages, channel="webchat", date=None):
    """归档一个 session"""
    if date is None:
        date = datetime.now().strftime("%Y-%m-%d")
    
    year_month = date[:7]  # YYYY-MM
    
    # 确保目录存在
    session_dir = os.path.join(ARCHIVE_DIR, "sessions", year_month)
    os.makedirs(session_dir, exist_ok=True)
    
    archive_file = os.path.join(session_dir, f"{session_id}.json")
    
    # 提取结构化信息
    topics = extract_topics(messages)
    decisions = extract_decisions(messages)
    feedback = extract_feedback(messages)
    summary = generate_summary(messages)
    
    archive = {
        "sessionId": session_id,
        "date": date,
        "channel": channel,
        "topics": topics,
        "decisions": decisions,
        "feedback": feedback,
        "summary": summary,
        "messageCount": len(messages),
        "archivedAt": datetime.now().isoformat()
    }
    
    # 写入归档
    with open(archive_file, "w", encoding="utf-8") as f:
        json.dump(archive, f, ensure_ascii=False, indent=2)
    
    # 更新索引
    index = load_index()
    index.insert(0, {
        "sessionId": session_id,
        "date": date,
        "topics": topics,
        "summary": summary,
        "file": f"sessions/{year_month}/{session_id}.json"
    })
    
    # 保留最近1000条索引
    if len(index) > 1000:
        index = index[:1000]
    
    save_index(index)
    
    print(f"Archived: {session_id}")
    print(f"  Date: {date}")
    print(f"  Topics: {topics}")
    print(f"  Decisions: {len(decisions)}")
    print(f"  Feedback: {len(feedback)}")
    print(f"  Summary: {summary[:80]}...")
    
    return archive

if __name__ == "__main__":
    # 测试：打印当前索引状态
    index = load_index()
    print(f"Current archive index: {len(index)} entries")
    print(f"Archive directory: {ARCHIVE_DIR}")


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