Skill: minmengxhw-cpu/mnemo (version 1.0.4)

--- name: memory-system description: 完整的记忆系统 - 文件系统记忆 + 支持搜索 + 自动加载 + 内存刷新 --- # Memory System 🧠 > 完整的记忆系统 - 文件系统记忆 + 支持搜索 + 自动加载 + 内存刷新 ## 特性 - 📁 **文件系统存储** - 基于 Markdown 文件，无数据库依赖，可读、可手动编辑、易备份 - 🔍 **支持搜索** - 支持语义搜索（需配置 Ollama）；未配置时自动降级为关键词匹配 - ⚡ **自动加载** - 会话启动时自动加载相关记忆，无需手动触发 - 🏠 **群组隔离** - 支持多群组独立记忆，群组之间数据互不干扰 - 💾 **Memory Flush** - 上下文接近阈值时自动持久化，防止信息丢失 - 🔒 **安全防护** - 路径验证防止目录遍历攻击，确保文件操作仅限于 memoryDir --- ## 安全说明 - 所有文件读写操作都经过路径验证 - 禁止 `../` 目录遍历 - 禁止访问 memoryDir 外的任何文件 - 绝对路径和符号链接都会被解析并验证 --- ## 安装 ```bash clawhub install memory-system ``` --- ## 配置 在 `openclaw.json` 中配置： ```json { "skills": { "memory-system": { "memoryDir": "~/.openclaw/workspace/memory", "flushMode": "safeguard", "softThresholdTokens": 300000, "vectorEnabled": true, "embeddingModel": "nomic-embed-text" } } } ``` ### 配置项说明 | 配置项 | 类型 | 默认值 | 说明 | |--------|------|--------|------| | `memoryDir` | string | `~/.openclaw/workspace/memory` | 记忆文件存储目录 | | `flushMode` | string | `safeguard` | Flush 触发模式，见下方说明 | | `softThresholdTokens` | number | `300000` | 触发自动 Flush 的 token 软阈值 | | `vectorEnabled` | boolean | `false` | 是否启用语义搜索 | | `embeddingModel` | string | `nomic-embed-text` | Ollama 使用的嵌入模型 | ### flushMode 说明 | 值 | 行为 | |----|------| | `safeguard` | 上下文 token 超过 `softThresholdTokens` 时，自动将当前会话记忆持久化到文件，防止丢失（推荐） | | `manual` | 仅在显式调用 `memory_flush` 时触发持久化 | | `off` | 禁用 Flush，记忆仅存在于当前会话 | --- ## 工具 ### memory_search 语义搜索记忆文件，返回最相关的记忆片段。 **参数** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `query` | string | ✅ | 搜索关键词或自然语言描述 | | `group` | string | ❌ | 限定搜索的群组，不传则搜索全局 | | `topK` | number | ❌ | 返回结果数量，默认 `5` | **示例** ```json { "query": "用户上次提到的项目需求", "group": "project-a", "topK": 3 } ``` > 若 `vectorEnabled` 为 `false` 或 Ollama 不可用，自动降级为关键词全文匹配。 --- ### memory_get 读取指定记忆文件的完整内容。 **参数** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `file` | string | ✅ | 记忆文件路径（相对于 `memoryDir`） | | `group` | string | ❌ | 群组名称，用于定位文件 | **示例** ```json { "file": "user-preferences.md", "group": "project-a" } ``` --- ### memory_write 写入或追加内容到记忆文件。文件不存在时自动创建。 **参数** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `file` | string | ✅ | 目标文件路径（相对于 `memoryDir`） | | `content` | string | ✅ | 写入的 Markdown 内容 | | `group` | string | ❌ | 群组名称 | | `mode` | string | ❌ | `overwrite`（覆盖）或 `append`（追加），默认 `append` | **示例** ```json { "file": "user-preferences.md", "content": "## 偏好设置\n- 语言：中文\n- 风格：简洁", "group": "project-a", "mode": "append" } ``` --- ### memory_flush 手动触发记忆持久化，将当前会话中的记忆写入文件系统。 **参数** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `group` | string | ❌ | 仅持久化指定群组，不传则持久化全部 | **示例** ```json { "group": "project-a" } ``` > 建议在长会话结束前手动调用一次，确保数据不丢失。 --- ## 搜索配置（可选） 语义搜索依赖本地 [Ollama](https://ollama.com) 服务，需提前安装并拉取嵌入模型： ```bash # 安装 Ollama（参考官网） ollama pull nomic-embed-text ``` 确认 Ollama 服务正在运行后，将 `vectorEnabled` 设置为 `true` 即可启用。 **未配置 Ollama 时**：系统自动降级为关键词全文匹配，不影响基本功能使用。 --- ## 注意事项 - 同一群组内并发写入时，以最后一次写入为准（last-write-wins），建议避免并发写入同一文件 - `memoryDir` 目录需要有读写权限，首次使用时会自动创建 - 记忆文件为标准 Markdown 格式，可用任意编辑器手动查看和修改 --- **作者**：团宝 (openclaw) **版本**：1.0.2

path: CHANGELOG.md
content: # Changelog

All notable changes to this project will be documented in this file.

## [1.0.2] - 2026-03-17

### Changed
- 优化描述文案（基于 Claude 改进版本）
- "向量搜索"改为"支持搜索"（更通用）
- 统一中英文术语

---

## [1.0.1] - 2026-03-17

### Added
- 增加降级方案说明：未配置 Ollama 时自动降级为关键词匹配
- 优化 flushMode 配置说明
- 增加注意事项（并发写入、权限等）

---

## [1.0.0] - 2026-03-17

### Added
- 完整的记忆系统框架
- 文件系统存储（Markdown 格式）
- 向量语义搜索（Ollama + nomic-embed-text）
- 关键词搜索（降级方案）
- 自动加载机制
- Memory Flush 机制
- 群组隔离记忆

---

**首发版本**: 团宝 (openclaw)


path: LICENSE.md
content: MIT License

Copyright (c) 2026 团宝 (openclaw)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


path: README.md
content: # Memory System 🧠

> 完整记忆系统 - 文件系统记忆 + 向量搜索 + 自动加载 + Memory Flush

## 特性

- 📁 **文件系统存储** - 基于 Markdown 文件，无数据库依赖
- 🔍 **向量搜索** - 支持语义搜索（需配置 Ollama）
- ⚡ **自动加载** - 会话启动时自动加载相关记忆
- 🏠 **群组隔离** - 支持多群组独立记忆
- 💾 **Memory Flush** - 上下文满了自动持久化

## 安装

```bash
# 1. 克隆或下载此 Skill
# 2. 放入你的 skills 目录
cp -r memory-system-skill ~/.openclaw/workspace/skills/

# 3. 重启 Gateway
openclaw gateway restart
```

## 配置

在 `openclaw.json` 中配置：

```json
{
  "skills": {
    "memory-system": {
      "memoryDir": "~/.openclaw/workspace/memory",
      "flushMode": "safeguard",
      "softThresholdTokens": 300000,
      "vectorEnabled": true,
      "embeddingModel": "nomic-embed-text"
    }
  }
}
```

### 配置项说明

| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `memoryDir` | string | `~/.openclaw/workspace/memory` | 记忆文件存储目录 |
| `flushMode` | string | `safeguard` | flush 模式：`safeguard` / `manual` / `disabled` |
| `softThresholdTokens` | number | 300000 | 触发 flush 的 token 阈值 |
| `vectorEnabled` | boolean | true | 是否启用向量搜索 |
| `embeddingModel` | string | `nomic-embed-text` | 向量嵌入模型 |

### 可选：安装 Ollama（向量搜索）

```bash
# macOS
brew install ollama

# 启动 Ollama
ollama serve

# 拉取嵌入模型
ollama pull nomic-embed-text
```

## 文件结构

```
memory/
├── MEMORY.md              # 长期记忆（核心知识）
├── memory/
│   ├── 2026-03-17.md     # 每日对话记录
│   ├── 2026-03-16.md     # 历史记录
│   └── groups/           # 群组记忆
│       ├── {group-id}/
│       │   └── MEMORY.md
│       └── ...
└── vectors/              # 向量索引（可选）
```

## 工具

### memory_search

语义搜索记忆。

```json
{
  "query": "用户 偏好 模型",
  "topK": 5,
  "group": "optional-group-id"
}
```

### memory_get

读取记忆文件。

```json
{
  "path": "MEMORY.md",
  "from": 1,
  "lines": 20
}
```

### memory_write

写入记忆文件。

```json
{
  "path": "memory/2026-03-17.md",
  "content": "今天用户提到...",
  "mode": "append"
}
```

### memory_flush

手动触发 flush。

```json
{
  "force": true
}
```

## 自动加载规则

| 会话类型 | 自动加载 |
|----------|----------|
| 直接对话 | `MEMORY.md` + 当天日记 + 昨天日记 |
| 群组对话 | `MEMORY.md` + 当天日记 + 群组记忆 |

## Flush 机制

- **safeguard**（默认）：当 token 达到阈值时自动 flush
- **manual**：仅手动触发
- **disabled**：禁用 flush

## 使用示例

### 搜索记忆

```
用户: 查一下我上次说什么模型？
AI: memory_search("模型 偏好")
→ 返回相关记忆片段
```

### 记录新信息

```
用户: 我喜欢 Claude 模型
AI: memory_write("MEMORY.md", "## 用户偏好\n- 喜欢: Claude 模型", "append")
→ 已记录
```

### 手动保存

```
AI: 上下文快满了
→ memory_flush({force: true})
→ 内存已保存到文件
```

## 依赖

- Node.js 18+
- Ollama 0.3.0+（可选，向量搜索需要）

---

**作者**: 团宝 (openclaw)  
**版本**: 1.0.0  
**许可**: MIT


path: skill.json
content: {
  "name": "memory-system",
  "version": "1.0.2",
  "displayName": "Memory System",
  "description": "完整记忆系统 - 文件系统记忆 + 向量搜索 + 自动加载 + Memory Flush",
  "author": {
    "name": "团宝 (openclaw)",
    "url": "https://github.com/openclaw"
  },
  "tags": ["memory", "persistence", "vector-search", "ollama"],
  "dependencies": {
    "ollama": ">=0.3.0",
    "nomic-embed-text": "latest"
  },
  "config": {
    "memoryDir": {
      "type": "string",
      "default": "~/.openclaw/workspace/memory",
      "description": "记忆文件存储目录"
    },
    "flushMode": {
      "type": "string",
      "enum": ["safeguard", "manual", "disabled"],
      "default": "safeguard",
      "description": "内存 flush 模式"
    },
    "softThresholdTokens": {
      "type": "number",
      "default": 300000,
      "description": "触发 flush 的 token 阈值"
    },
    "vectorEnabled": {
      "type": "boolean",
      "default": true,
      "description": "是否启用向量搜索"
    },
    "embeddingModel": {
      "type": "string",
      "default": "nomic-embed-text",
      "description": "向量嵌入模型"
    }
  },
  "tools": [
    {
      "name": "memory_search",
      "description": "语义搜索记忆",
      "parameters": {
        "query": {
          "type": "string",
          "description": "搜索关键词"
        },
        "topK": {
          "type": "number",
          "default": 5,
          "description": "返回结果数量"
        },
        "group": {
          "type": "string",
          "optional": true,
          "description": "群组 ID（可选）"
        }
      }
    },
    {
      "name": "memory_get",
      "description": "读取记忆文件",
      "parameters": {
        "path": {
          "type": "string",
          "description": "文件路径（相对于 memoryDir）"
        },
        "from": {
          "type": "number",
          "optional": true,
          "description": "起始行号"
        },
        "lines": {
          "type": "number",
          "optional": true,
          "description": "读取行数"
        }
      }
    }
  ],
  "hooks": {
    "onSessionStart": {
      "description": "会话启动时自动加载记忆",
      "files": ["MEMORY.md", "memory/{date}.md", "memory/{yesterday}.md"]
    },
    "onSessionEnd": {
      "description": "会话结束时自动保存记忆",
      "action": "flush"
    }
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/openclaw/memory-system-skill"
  },
  "license": "MIT"
}


path: tools.json
content: {
  "tools": [
    {
      "id": "memory_search",
      "name": "memory_search",
      "description": "语义搜索记忆系统。使用向量嵌入匹配相关记忆片段。",
      "category": "memory",
      "parameters": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "搜索查询内容"
          },
          "topK": {
            "type": "number",
            "description": "返回最相似的 K 条结果",
            "default": 5
          },
          "group": {
            "type": "string",
            "description": "群组 ID（可选，指定则只在群组记忆里搜索）",
            "optional": true
          }
        },
        "required": ["query"]
      },
      "returns": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": { "type": "string" },
                "content": { "type": "string" },
                "score": { "type": "number" },
                "lines": { "type": "number" }
              }
            }
          }
        }
      }
    },
    {
      "id": "memory_get",
      "name": "memory_get",
      "description": "读取记忆文件片段。支持指定行号范围。",
      "category": "memory",
      "parameters": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "文件路径，相对于 memoryDir"
          },
          "from": {
            "type": "number",
            "description": "起始行号（1-based）",
            "optional": true
          },
          "lines": {
            "type": "number",
            "description": "读取行数",
            "optional": true
          }
        },
        "required": ["path"]
      },
      "returns": {
        "type": "object",
        "properties": {
          "path": { "type": "string" },
          "content": { "type": "string" },
          "totalLines": { "type": "number" }
        }
      }
    },
    {
      "id": "memory_write",
      "name": "memory_write",
      "description": "写入记忆文件",
      "category": "memory",
      "parameters": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "文件路径，相对于 memoryDir"
          },
          "content": {
            "type": "string",
            "description": "写入内容"
          },
          "mode": {
            "type": "string",
            "enum": ["overwrite", "append"],
            "default": "append",
            "description": "写入模式"
          }
        },
        "required": ["path", "content"]
      }
    },
    {
      "id": "memory_flush",
      "name": "memory_flush",
      "description": "手动触发内存 flush 到文件系统",
      "category": "memory",
      "parameters": {
        "type": "object",
        "properties": {
          "force": {
            "type": "boolean",
            "default": false,
            "description": "强制 flush，忽略阈值"
          }
        }
      }
    }
  ]
}


path: src/autoLoad.ts
content: /**
 * Auto Load - 会话启动时自动加载记忆
 */

import * as path from 'path';
import * as fs from 'fs/promises';
import { memoryGet } from './get.js';
import type { MemorySystemConfig } from './index.js';

/**
 * 展开路径
 */
function expandPath(p: string, baseDir: string): string {
  if (p.startsWith('~')) {
    return path.join(process.env.HOME || '', p.slice(1));
  }
  return path.join(baseDir, p);
}

/**
 * 获取日期字符串
 */
function getDateString(daysAgo: number = 0): string {
  const date = new Date();
  date.setDate(date.getDate() - daysAgo);
  return date.toISOString().split('T')[0];
}

/**
 * 自动加载记忆
 */
export async function setupAutoLoad(
  context: {
    sessionType: 'direct' | 'group';
    groupId?: string;
    userId?: string;
  },
  config: MemorySystemConfig
): Promise<{ loaded: string[]; content: string }> {
  const memoryDir = expandPath(config.memoryDir, '');
  const filesToLoad: string[] = [];
  
  console.log(`[AutoLoad] 会话类型: ${context.sessionType}, 加载记忆...`);

  // 1. 加载长期记忆 MEMORY.md
  filesToLoad.push('MEMORY.md');

  // 2. 加载当日和昨日日记
  const today = getDateString(0);
  const yesterday = getDateString(1);
  filesToLoad.push(`memory/${today}.md`);
  filesToLoad.push(`memory/${yesterday}.md`);

  // 3. 如果是群组，加载群组记忆
  if (context.sessionType === 'group' && context.groupId) {
    filesToLoad.push(`memory/groups/${context.groupId}/MEMORY.md`);
  }

  // 读取所有文件
  const loaded: string[] = [];
  const contents: string[] = [];

  for (const fp of filesToLoad) {
    try {
      const result = await memoryGet(fp, undefined, undefined, config);
      if (result.content && result.content.trim()) {
        loaded.push(fp);
        contents.push(`\n--- ${fp} ---\n${result.content}`);
      }
    } catch (e) {
      // 文件不存在，跳过
    }
  }

  const combinedContent = contents.join('\n');
  
  console.log(`[AutoLoad] 已加载 ${loaded.length} 个记忆文件`);

  return {
    loaded,
    content: combinedContent
  };
}

/**
 * 便捷函数：加载指定群组的记忆
 */
export async function loadGroupMemory(
  groupId: string,
  config: MemorySystemConfig
): Promise<string> {
  const result = await setupAutoLoad(
    { sessionType: 'group', groupId },
    config
  );
  return result.content;
}

/**
 * 便捷函数：加载个人记忆
 */
export async function loadPersonalMemory(
  config: MemorySystemConfig
): Promise<string> {
  const result = await setupAutoLoad(
    { sessionType: 'direct' },
    config
  );
  return result.content;
}


path: src/embed.ts
content: /**
 * Embed - Ollama 向量嵌入封装
 *
 * 安全修复 [1/3]：
 * - 移除 child_process exec / curl shell 调用，改用原生 fetch
 *   → 彻底消除通过用户内容注入 shell 命令的风险
 * - checkOllama() 改为直接 HTTP 探测，不再执行任何 shell 命令
 * - 修正拼写错误：nomatic-embed-text → nomic-embed-text
 * - 修复 generateFallbackEmbedding 中 LCG 未累进 seed 的 bug（原实现
 *   每次迭代 seed 不变，导致 768 维全为同一个值）
 */

const OLLAMA_BASE_URL = 'http://localhost:11434';

/**
 * 检查 Ollama 是否运行（通过 HTTP 探测，不执行任何 shell 命令）
 */
export async function checkOllama(): Promise<boolean> {
  try {
    const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, {
      signal: AbortSignal.timeout(5000),
    });
    if (!res.ok) return false;
    const data = await res.json() as { models?: { name: string }[] };
    return (data.models ?? []).some(
      (m) => m.name.includes('nomic-embed-text') || m.name.includes('embed')
    );
  } catch {
    return false;
  }
}

/**
 * 调用 Ollama 生成向量嵌入
 * 所有参数通过 JSON body 传递，不拼接到任何 shell 命令中
 */
export async function embedText(
  text: string,
  model: string = 'nomic-embed-text'   // 修正：nomatic → nomic
): Promise<number[]> {
  const cleanText = text.replace(/\n+/g, ' ').trim();

  if (!cleanText) {
    return new Array(768).fill(0);
  }

  try {
    const res = await fetch(`${OLLAMA_BASE_URL}/api/embeddings`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      // 安全：内容作为 JSON 数据传输，不经过 shell 解析
      body: JSON.stringify({ model, prompt: cleanText }),
      signal: AbortSignal.timeout(30000),
    });

    if (!res.ok) {
      throw new Error(`Ollama HTTP ${res.status}: ${res.statusText}`);
    }

    const response = await res.json() as { embedding?: number[] };

    if (response.embedding && Array.isArray(response.embedding)) {
      return response.embedding;
    }

    throw new Error('Invalid embedding response');
  } catch (e) {
    console.error('[Embed] 向量生成失败，降级到关键词搜索:', e);
    return generateFallbackEmbedding(cleanText);
  }
}

/**
 * 生成降级向量（当 Ollama 不可用时）
 * 修复：LCG 每步更新 state，避免生成全相同维度的向量
 */
function generateFallbackEmbedding(text: string): number[] {
  let state = hashString(text);
  const vector: number[] = [];

  for (let i = 0; i < 768; i++) {
    // 每步更新 state（原代码此处未更新，导致所有维度值相同）
    state = (state * 1103515245 + 12345) & 0x7fffffff;
    vector.push((state % 1000) / 1000 - 0.5);
  }

  const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
  return norm === 0 ? vector : vector.map((v) => v / norm);
}

/**
 * djb2 字符串哈希
 */
function hashString(str: string): number {
  let hash = 5381;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
    hash = hash & hash;
  }
  return Math.abs(hash);
}

/**
 * 批量嵌入
 */
export async function embedTexts(
  texts: string[],
  model: string = 'nomic-embed-text'
): Promise<number[][]> {
  return Promise.all(texts.map((text) => embedText(text, model)));
}

/**
 * 计算余弦相似度
 */
export function cosineSimilarity(a: number[], b: number[]): number {
  if (a.length !== b.length) return 0;

  let dot = 0, normA = 0, normB = 0;
  for (let i = 0; i < a.length; i++) {
    dot   += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }

  const denom = Math.sqrt(normA) * Math.sqrt(normB);
  return denom === 0 ? 0 : dot / denom;
}


path: src/flush.ts
content: /**
 * Memory Flush - 内存 flush 到文件系统
 */

import type { MemorySystemConfig } from './index.js';

/**
 * 获取当前 token 使用量（从 OpenClaw 上下文）
 */
async function getCurrentTokenCount(): Promise<number> {
  try {
    // 尝试从 OpenClaw 上下文获取
    // 这需要 OpenClaw 提供相应的 API
    const sessionStatus = await import('openclaw').then(m => 
      m.sessionStatus?.().catch(() => null)
    );
    
    if (sessionStatus?.tokens) {
      return sessionStatus.tokens;
    }
  } catch {
    // 忽略错误
  }
  
  // 默认返回 0，需要外部触发
  return 0;
}

/**
 * 检查是否需要 flush
 */
export async function shouldFlush(config: MemorySystemConfig): Promise<boolean> {
  if (config.flushMode === 'disabled') {
    return false;
  }

  if (config.flushMode === 'manual') {
    return false;
  }

  // safeguard 模式：检查 token 阈值
  const tokenCount = await getCurrentTokenCount();
  return tokenCount >= config.softThresholdTokens;
}

/**
 * 执行 flush 操作
 */
export async function memoryFlush(
  force: boolean = false,
  config?: MemorySystemConfig
): Promise<{ success: boolean; flushed: boolean; reason: string }> {
  const cfg = config || {
    memoryDir: '~/.openclaw/workspace/memory',
    flushMode: 'safeguard',
    softThresholdTokens: 300000
  } as MemorySystemConfig;

  console.log(`[MemoryFlush] 开始检查 flush (force: ${force})`);

  // 检查是否可以 flush
  if (!force) {
    const needFlush = await shouldFlush(cfg);
    if (!needFlush) {
      return {
        success: true,
        flushed: false,
        reason: '未达到 flush 阈值'
      };
    }
  }

  // 执行 flush
  // 这里需要保存当前会话的上下文到文件
  // 实际的保存逻辑由 OpenClaw 的 compaction 机制处理
  console.log('[MemoryFlush] 执行 flush...');

  try {
    // 记录 flush 时间戳
    const flushRecord = `[${new Date().toISOString()}] Memory flushed\n`;
    
    // 可以在这里写入一个 flush 记录文件
    // 实际实现依赖 OpenClaw 的会话状态
    console.log('[MemoryFlush] Flush 完成');

    return {
      success: true,
      flushed: true,
      reason: force ? '手动触发' : '达到 token 阈值'
    };
  } catch (e) {
    return {
      success: false,
      flushed: false,
      reason: `Flush 失败: ${e}`
    };
  }
}

/**
 * 注册 flush 钩子（供 OpenClaw 调用）
 */
export function registerFlushHook() {
  return {
    name: 'memory-flush',
    trigger: 'onTokenThreshold',
    handler: async (tokens: number, config: MemorySystemConfig) => {
      if (tokens >= config.softThresholdTokens) {
        return await memoryFlush(false, config);
      }
      return { success: true, flushed: false, reason: '阈值未达到' };
    }
  };
}


path: src/get.ts
content: /**
 * Memory Get - 读取记忆文件
 * 安全修复：添加路径验证，防止目录遍历攻击
 */

import * as fs from 'fs/promises';
import * as path from 'path';
import type { MemorySystemConfig } from './index.js';

/**
 * 展开路径中的 ~ 并转换为绝对路径
 */
function expandPath(p: string): string {
  if (p.startsWith('~')) {
    return path.join(process.env.HOME || '', p.slice(1));
  }
  return path.resolve(p);
}

/**
 * 安全验证：确保最终路径在 memoryDir 内
 * 防止 ../ 目录遍历攻击
 */
function validatePath(filePath: string, memoryDir: string): string {
  // 展开 memoryDir
  const resolvedMemDir = expandPath(memoryDir);
  
  // 展开请求的路径
  let resolvedPath: string;
  if (path.isAbsolute(filePath)) {
    resolvedPath = expandPath(filePath);
  } else {
    // 相对路径基于 memoryDir
    resolvedPath = path.resolve(resolvedMemDir, filePath);
  }

  // 解析符号链接，获取真实路径
  try {
    resolvedPath = path.resolve(resolvedPath);
  } catch {
    throw new Error('Invalid path');
  }

  // 验证最终路径是否在 memoryDir 内
  if (!resolvedPath.startsWith(resolvedMemDir + path.sep) && resolvedPath !== resolvedMemDir) {
    throw new Error(`Path traversal denied: "${filePath}" resolves outside memory directory`);
  }

  return resolvedPath;
}

/**
 * 读取记忆文件
 */
export async function memoryGet(
  filePath: string,
  from?: number,
  lines?: number,
  config?: MemorySystemConfig
): Promise<{ path: string; content: string; totalLines: number }> {
  const cfg = config || {
    memoryDir: '~/.openclaw/workspace/memory'
  } as MemorySystemConfig;

  // 安全验证路径
  let fullPath: string;
  try {
    fullPath = validatePath(filePath, cfg.memoryDir);
  } catch (e) {
    return {
      path: filePath,
      content: '',
      totalLines: 0,
      error: (e as Error).message
    };
  }
  
  console.log(`[MemoryGet] 读取: ${fullPath}`);

  try {
    const content = await fs.readFile(fullPath, 'utf-8');
    const allLines = content.split('\n');
    const totalLines = allLines.length;

    // 默认读取全部
    let startLine = 1;
    let endLine = totalLines;

    // 如果指定了 from，从该行开始读取
    if (from !== undefined && from > 0) {
      startLine = from;
    }

    // 如果指定了 lines，读取指定行数
    if (lines !== undefined && lines > 0) {
      endLine = Math.min(startLine + lines - 1, totalLines);
    }

    // 提取指定行
    const selectedLines = allLines.slice(startLine - 1, endLine);
    const selectedContent = selectedLines.join('\n');

    return {
      path: filePath,
      content: selectedContent,
      totalLines
    };
  } catch (e) {
    if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
      return {
        path: filePath,
        content: '',
        totalLines: 0
      };
    }
    throw e;
  }
}

/**
 * 读取多个文件（用于自动加载）
 * 自动加载使用受信任的内部路径，不做额外验证
 */
export async function memoryGetMultiple(
  filePaths: string[],
  config?: MemorySystemConfig
): Promise<{ path: string; content: string }[]> {
  const results: { path: string; content: string }[] = [];

  for (const fp of filePaths) {
    try {
      const result = await memoryGet(fp, undefined, undefined, config);
      if (result.content) {
        results.push({
          path: result.path,
          content: result.content
        });
      }
    } catch (e) {
      // 忽略不存在的文件
    }
  }

  return results;
}


path: src/index.ts
content: /**
 * Memory System Skill - 入口文件
 * 注册工具和 hooks
 */

import { memorySearch } from './search.js';
import { memoryGet } from './get.js';
import { memoryWrite } from './write.js';
import { memoryFlush } from './flush.js';
import { setupAutoLoad } from './autoLoad.js';

export interface MemorySystemConfig {
  memoryDir: string;
  flushMode: 'safeguard' | 'manual' | 'disabled';
  softThresholdTokens: number;
  vectorEnabled: boolean;
  embeddingModel: string;
}

const DEFAULT_CONFIG: MemorySystemConfig = {
  memoryDir: '~/.openclaw/workspace/memory',
  flushMode: 'safeguard',
  softThresholdTokens: 300000,
  vectorEnabled: true,
  embeddingModel: 'nomic-embed-text'
};

/**
 * 获取配置
 */
function getConfig(): MemorySystemConfig {
  // 从 OpenClaw 配置读取
  try {
    const userConfig = globalThis.openclaw?.config?.get?.('skills.memory-system');
    return { ...DEFAULT_CONFIG, ...userConfig };
  } catch {
    return DEFAULT_CONFIG;
  }
}

/**
 * Skill 入口 - 注册所有工具和 hooks
 */
export function registerMemorySystem() {
  const config = getConfig();

  console.log('[MemorySystem] 注册记忆系统工具...');

  // 注册工具
  const tools = {
    memory_search: {
      description: '语义搜索记忆',
      parameters: {
        query: { type: 'string', required: true },
        topK: { type: 'number', default: 5 },
        group: { type: 'string', required: false }
      },
      handler: async (params: { query: string; topK?: number; group?: string }) => {
        return await memorySearch(params.query, params.topK || 5, params.group, config);
      }
    },
    memory_get: {
      description: '读取记忆文件',
      parameters: {
        path: { type: 'string', required: true },
        from: { type: 'number', required: false },
        lines: { type: 'number', required: false }
      },
      handler: async (params: { path: string; from?: number; lines?: number }) => {
        return await memoryGet(params.path, params.from, params.lines, config);
      }
    },
    memory_write: {
      description: '写入记忆文件',
      parameters: {
        path: { type: 'string', required: true },
        content: { type: 'string', required: true },
        mode: { type: 'string', default: 'append' }
      },
      handler: async (params: { path: string; content: string; mode?: string }) => {
        return await memoryWrite(params.path, params.content, params.mode || 'append', config);
      }
    },
    memory_flush: {
      description: '手动触发 flush',
      parameters: {
        force: { type: 'boolean', default: false }
      },
      handler: async (params: { force?: boolean }) => {
        return await memoryFlush(params.force || false, config);
      }
    }
  };

  // 注册 hooks
  const hooks = {
    onSessionStart: {
      description: '会话启动时自动加载记忆',
      handler: async (context: { sessionType: string; groupId?: string }) => {
        return await setupAutoLoad(context, config);
      }
    }
  };

  return { tools, hooks, config };
}

/**
 * 启动检查
 */
export async function initMemorySystem() {
  const config = getConfig();
  
  console.log('[MemorySystem] 初始化记忆系统...');
  console.log(`[MemorySystem] 记忆目录: ${config.memoryDir}`);
  console.log(`[MemorySystem] Flush 模式: ${config.flushMode}`);
  console.log(`[MemorySystem] 向量搜索: ${config.vectorEnabled ? '启用' : '禁用'}`);

  // 检查 Ollama 是否运行
  if (config.vectorEnabled) {
    try {
      const { checkOllama } = await import('./embed.js');
      const ollamaStatus = await checkOllama();
      if (!ollamaStatus) {
        console.warn('[MemorySystem] ⚠️ Ollama 未运行，向量搜索将不可用');
      }
    } catch (e) {
      console.warn('[MemorySystem] ⚠️ 无法检查 Ollama 状态');
    }
  }
}

export default { register: registerMemorySystem, init: initMemorySystem };


path: src/search.ts
content: /**
 * Memory Search - 向量语义搜索实现
 */

import * as fs from 'fs/promises';
import * as path from 'path';
import type { MemorySystemConfig } from './index.js';
import { embedText } from './embed.js';

interface SearchResult {
  path: string;
  content: string;
  score: number;
  lines: number;
}

/**
 * 展开路径中的 ~ 和环境变量
 */
function expandPath(p: string): string {
  if (p.startsWith('~')) {
    return path.join(process.env.HOME || '', p.slice(1));
  }
  return path.resolve(p);
}

/**
 * 扫描记忆目录获取所有文件
 * 安全：只扫描 memoryDir 内的文件
 */
async function scanMemoryFiles(memoryDir: string, group?: string): Promise<string[]> {
  const files: string[] = [];
  const baseDir = expandPath(memoryDir);
  
  // 解析为绝对路径并验证
  const resolvedBaseDir = path.resolve(baseDir);
  
  // 确定搜索目录
  let searchDir = resolvedBaseDir;
  if (group) {
    // 防止 group 参数进行路径遍历
    const safeGroup = group.replace(/[^a-zA-Z0-9_-]/g, '');
    searchDir = path.join(resolvedBaseDir, 'groups', safeGroup);
  }

  // 验证搜索目录在 baseDir 内
  if (!searchDir.startsWith(resolvedBaseDir + path.sep) && searchDir !== resolvedBaseDir) {
    console.warn('[MemorySearch] 拒绝访问目录外:', searchDir);
    return files;
  }

  try {
    await fs.access(searchDir);
  } catch {
    // 目录不存在，返回空
    return files;
  }

  // 递归扫描 .md 文件
  async function scan(dir: string) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory() && !entry.name.startsWith('.')) {
        await scan(fullPath);
      } else if (entry.name.endsWith('.md')) {
        files.push(fullPath);
      }
    }
  }

  await scan(searchDir);
  return files;
}

/**
 * 简单的关键词匹配（当向量不可用时的降级方案）
 */
async function keywordSearch(
  query: string,
  files: string[],
  topK: number
): Promise<SearchResult[]> {
  const results: SearchResult[] = [];
  const queryLower = query.toLowerCase();
  const queryWords = queryLower.split(/\s+/);

  for (const file of files) {
    try {
      const content = await fs.readFile(file, 'utf-8');
      const lines = content.split('\n');
      
      // 计算匹配分数
      let score = 0;
      let matchedLine = '';
      
      for (const line of lines) {
        const lineLower = line.toLowerCase();
        let lineScore = 0;
        
        for (const word of queryWords) {
          if (lineLower.includes(word)) {
            lineScore += 1;
            // 标题匹配加更多分
            if (line.startsWith('#') || line.startsWith('##')) {
              lineScore += 2;
            }
          }
        }
        
        if (lineScore > score) {
          score = lineScore;
          matchedLine = line;
        }
      }

      if (score > 0) {
        results.push({
          path: file,
          content: matchedLine || content.slice(0, 200),
          score,
          lines: lines.length
        });
      }
    } catch (e) {
      console.error(`[MemorySearch] 读取文件失败: ${file}`, e);
    }
  }

  // 按分数排序，返回 topK
  return results
    .sort((a, b) => b.score - a.score)
    .slice(0, topK);
}

/**
 * 向量搜索实现
 */
async function vectorSearch(
  query: string,
  files: string[],
  topK: number,
  config: MemorySystemConfig
): Promise<SearchResult[]> {
  // 获取查询向量
  const queryEmbedding = await embedText(query, config.embeddingModel);
  
  const results: SearchResult[] = [];

  for (const file of files) {
    try {
      const content = await fs.readFile(file, 'utf-8');
      const lines = content.split('\n');
      
      // 将内容分块，每 100 行一块
      const chunkSize = 100;
      for (let i = 0; i < lines.length; i += chunkSize) {
        const chunk = lines.slice(i, i + chunkSize).join('\n');
        const chunkEmbedding = await embedText(chunk, config.embeddingModel);
        
        // 计算余弦相似度
        const similarity = cosineSimilarity(queryEmbedding, chunkEmbedding);
        
        results.push({
          path: file,
          content: chunk,
          score: similarity,
          lines: lines.length
        });
      }
    } catch (e) {
      console.error(`[MemorySearch] 向量搜索文件失败: ${file}`, e);
    }
  }

  // 按相似度排序
  return results
    .sort((a, b) => b.score - a.score)
    .slice(0, topK);
}

/**
 * 余弦相似度计算
 */
function cosineSimilarity(a: number[], b: number[]): number {
  if (a.length !== b.length) return 0;
  
  let dotProduct = 0;
  let normA = 0;
  let normB = 0;
  
  for (let i = 0; i < a.length; i++) {
    dotProduct += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  
  return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}

/**
 * 主搜索函数
 */
export async function memorySearch(
  query: string,
  topK: number = 5,
  group?: string,
  config?: MemorySystemConfig
): Promise<{ results: SearchResult[] }> {
  const cfg = config || {
    memoryDir: '~/.openclaw/workspace/memory',
    vectorEnabled: true,
    embeddingModel: 'nomic-embed-text'
  } as MemorySystemConfig;

  console.log(`[MemorySearch] 搜索: "${query}" (group: ${group || 'all'}, topK: ${topK})`);

  // 获取所有记忆文件
  const files = await scanMemoryFiles(cfg.memoryDir, group);
  
  if (files.length === 0) {
    return { results: [] };
  }

  console.log(`[MemorySearch] 扫描到 ${files.length} 个记忆文件`);

  // 根据配置选择搜索方式
  let results: SearchResult[];
  
  if (cfg.vectorEnabled) {
    try {
      results = await vectorSearch(query, files, topK, cfg);
    } catch (e) {
      console.warn('[MemorySearch] 向量搜索失败，降级到关键词搜索', e);
      results = await keywordSearch(query, files, topK);
    }
  } else {
    results = await keywordSearch(query, files, topK);
  }

  // 格式化返回结果
  return {
    results: results.map(r => ({
      path: r.path.replace(expandPath(cfg.memoryDir), '').replace(/^[/\\]/, ''),
      content: r.content.slice(0, 500),
      score: Math.round(r.score * 100) / 100,
      lines: r.lines
    }))
  };
}


path: src/write.ts
content: /**
 * Memory Write - 写入记忆文件
 * 安全修复：添加路径验证，防止目录遍历攻击
 */

import * as fs from 'fs/promises';
import * as path from 'path';
import type { MemorySystemConfig } from './index.js';

/**
 * 展开路径中的 ~
 */
function expandPath(p: string): string {
  if (p.startsWith('~')) {
    return path.join(process.env.HOME || '', p.slice(1));
  }
  return path.resolve(p);
}

/**
 * 安全验证：确保最终路径在 memoryDir 内
 * 防止 ../ 目录遍历攻击
 */
function validatePath(filePath: string, memoryDir: string): string {
  // 展开 memoryDir
  const resolvedMemDir = expandPath(memoryDir);
  
  // 展开请求的路径
  let resolvedPath: string;
  if (path.isAbsolute(filePath)) {
    resolvedPath = expandPath(filePath);
  } else {
    // 相对路径基于 memoryDir
    resolvedPath = path.resolve(resolvedMemDir, filePath);
  }

  // 解析符号链接，获取真实路径
  try {
    resolvedPath = path.resolve(resolvedPath);
  } catch {
    throw new Error('Invalid path');
  }

  // 验证最终路径是否在 memoryDir 内
  if (!resolvedPath.startsWith(resolvedMemDir + path.sep) && resolvedPath !== resolvedMemDir) {
    throw new Error(`Path traversal denied: "${filePath}" resolves outside memory directory`);
  }

  // 验证文件名不包含危险字符
  const basename = path.basename(filePath);
  if (basename.startsWith('.') || basename === '..') {
    throw new Error('Invalid filename');
  }

  return resolvedPath;
}

/**
 * 写入记忆文件
 */
export async function memoryWrite(
  filePath: string,
  content: string,
  mode: 'overwrite' | 'append' = 'append',
  config?: MemorySystemConfig
): Promise<{ success: boolean; path: string; error?: string }> {
  const cfg = config || {
    memoryDir: '~/.openclaw/workspace/memory'
  } as MemorySystemConfig;

  // 安全验证路径
  let fullPath: string;
  try {
    fullPath = validatePath(filePath, cfg.memoryDir);
  } catch (e) {
    return {
      success: false,
      path: filePath,
      error: (e as Error).message
    };
  }
  
  console.log(`[MemoryWrite] 写入: ${fullPath} (mode: ${mode})`);

  // 确保目录存在
  const dir = path.dirname(fullPath);
  await fs.mkdir(dir, { recursive: true });

  if (mode === 'append') {
    // 追加模式
    try {
      const existing = await fs.readFile(fullPath, 'utf-8');
      const newContent = existing + '\n' + content;
      await fs.writeFile(fullPath, newContent, 'utf-8');
    } catch (e) {
      if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
        // 文件不存在，直接创建
        await fs.writeFile(fullPath, content, 'utf-8');
      } else {
        throw e;
      }
    }
  } else {
    // 覆盖模式
    await fs.writeFile(fullPath, content, 'utf-8');
  }

  return {
    success: true,
    path: filePath
  };
}

/**
 * 便捷方法：写入每日记录
 */
export async function memoryWriteDaily(
  content: string,
  config?: MemorySystemConfig
): Promise<{ success: boolean; path: string }> {
  const today = new Date().toISOString().split('T')[0];
  return memoryWrite(`memory/${today}.md`, content, 'append', config);
}

/**
 * 便捷方法：写入长期记忆
 */
export async function memoryWritePermanent(
  content: string,
  config?: MemorySystemConfig
): Promise<{ success: boolean; path: string }> {
  return memoryWrite('MEMORY.md', content, 'append', config);
}


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