Skill: thomasliu/kb-mini (version 1.3.0)

# Knowledge Base Skill **Version**: v1.2.0 **Repository**: https://github.com/ThomasLiu/knowledge-base-skill --- ## 触发条件 当用户提到以下关键词时触发： - "加入知识库" / "存到 KB" / "存入知识库" - "knowledge base" / "知识库" / "KB" - "记得这个" / "保存上下文" / "记忆" - "检索知识库" / "查一下 KB" --- ## 功能 ### 1. Collect - 存储 ```bash kb store --title "标题" --content "内容" --source "manual" ``` ### 2. Retrieve - 检索 ```bash kb search --query "关键词" kb retrieve --topic-key "entry-key" ``` ### 3. Recall - 对话前自动检索 在 `before_agent_start` hook 中自动调用，检索与当前对话相关的知识。 ### 4. Capture - 对话后自动存储 在 `after_turn` hook 中自动调用，判断并存储重要内容。 --- ## 使用方式 ### 存储信息 ``` 用户: 把这个配置加入知识库 Agent: 使用 kb store 命令存储 ``` ### 检索信息 ``` 用户: 查一下知识库里关于 OpenClaw 的内容 Agent: 使用 kb search 查询并返回结果 ``` --- ## 脚本列表 | 脚本 | 功能 | |------|------| | `scripts/storage.sh` | 核心存储 API | | `scripts/retriever.sh` | 检索 + recall/capture | | `scripts/hooks.sh` | OpenClaw Hooks 集成 | | `scripts/lifecycle.sh` | 生命周期管理 | ## 依赖 - bash - sqlite3 - python3 --- ## 配置 ### 默认模式（安装即用） Skill 安装后默认使用**自身目录**存储，无需任何配置： ``` ~/.openclaw/workspace/skills/kb-mini/data/knowledge.db ``` ### 共享 KB 模式（多 Agent 共用） 多个 Agent 共用同一个知识库： ```bash # 在调用 skill 前设置 export KNOWLEDGE_SHARED_NAME="coding-kb" # 实际路径: ~/.openclaw/shared/knowledge-bases/coding-kb/knowledge.db ``` ### 显式路径模式 指定任意路径作为 KB： ```bash export KNOWLEDGE_DB="/path/to/your/knowledge.db" ``` ### 环境变量优先级 | 优先级 | 变量 | 说明 | |--------|------|------| | 1 | `KNOWLEDGE_DB` | 显式指定路径 | | 2 | `KNOWLEDGE_SHARED_NAME` | 共享 KB 名称 | | 3 | Skill 内部目录 | 默认，安装即用 | --- ## 目录结构 ``` kb-mini/ ├── data/ │ └── knowledge.db # 默认 KB 路径 ├── scripts/ │ ├── storage.sh # 存储 API │ ├── retriever.sh # 检索 API │ ├── hooks.sh # Hooks 集成 │ └── lifecycle.sh # 生命周期 └── SKILL.md ```

path: PLAN.md
content: # Knowledge Base Skill 设计方案

**版本**: v0.1.0
**日期**: 2026-03-22
**状态**: 调研中

---

## 1. 概述

### 1.1 目标
创建一个通用的 **Knowledge Base Skill**，为 OpenClaw Agent 提供知识存储和检索能力。

### 1.2 核心定位
- **只负责怎么存、怎么取**
- **不内置采集逻辑**（collector 由外部 Agent 决定）
- **支持私有 KB 和共享 KB**

### 1.3 三大核心功能

| 功能 | 触发时机 | 说明 |
|------|----------|------|
| **Collect** | Agent 主动调用 | 存入数据到 KB |
| **Recall** | 每次对话前 (before_agent_start) | 自动检索相关知识 |
| **Capture** | 每次回复后 (after_turn) | 自动记忆重要内容 |

---

## 2. 目录结构

```
knowledge-base/
├── SKILL.md                    # Skill 定义（触发条件、接口说明）
├── scripts/
│   ├── storage.sh              # 存储核心 API
│   │   ├── store              # 存入单条
│   │   ├── search             # 检索
│   │   ├── retrieve           # 按 topic_key 取
│   │   └── batch_store        # 批量存入
│   ├── hooks.sh                # Hooks 集成
│   │   ├── before_agent_start  # 对话前：recall
│   │   └── after_turn         # 对话后：capture
│   ├── retriever.sh            # 检索排序
│   │   ├── fuse_rank           # RRF 融合
│   │   └── time_decay          # 时间衰减
│   └── config.sh               # 路径配置
├── references/
│   ├── db-schema.md            # 数据库 Schema
│   ├── api-spec.md             # API 接口规范
│   └── retrieval-pipeline.md  # 检索流程详解
└── README.md                   # 使用说明
```

---

## 3. 数据库设计

### 3.1 Schema

```sql
CREATE VIRTUAL TABLE knowledge_entries USING fts5(
  id,
  topic_key,
  title,
  content,
  source,
  tags,
  access_level,
  created_at,
  updated_at,
  metadata
);

CREATE TABLE entries (
  id TEXT PRIMARY KEY,
  topic_key TEXT UNIQUE NOT NULL,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  source TEXT NOT NULL,
  tags TEXT,
  access_level INTEGER DEFAULT 2,
  created_at TEXT,
  updated_at TEXT,
  metadata TEXT
);
```

### 3.2 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 唯一标识 |
| topic_key | String | 主题 key，同一主题 upsert |
| title | String | 标题 |
| content | Text | 完整内容 |
| source | Enum | rss, github, huggingface, manual |
| tags | JSON | 标签数组 |
| access_level | Int | 0:L0摘要, 1:L1概览, 2:L2详情 |
| created_at | DateTime | 创建时间 |
| updated_at | DateTime | 更新时间 |
| metadata | JSON | 扩展字段 |

---

## 4. 存储接口 (storage.sh)

### 4.1 store - 存入单条

```bash
kb store --title "..." --content "..." --tags "tag1,tag2" --source "manual"
```

**参数**：
- `--title`: 标题
- `--content`: 完整内容
- `--topic-key`: 可选，默认自动生成
- `--tags`: 逗号分隔的标签
- `--source`: 来源 (rss/github/huggingface/manual)
- `--level`: 加载层级 (0/1/2)

**输出**：
```json
{"status": "ok", "id": "uuid", "topic_key": "..."}
```

### 4.2 search - 检索

```bash
kb search --query "openclaw hooks" --limit 5 --level "L1"
```

**参数**：
- `--query`: 搜索关键词
- `--limit`: 返回数量
- `--level`: 加载层级
- `--time-decay`: 时间衰减因子

**输出**：
```json
{
  "results": [...],
  "total_hits": 12,
  "query_time_ms": 45
}
```

### 4.3 retrieve - 按 topic_key 取

```bash
kb retrieve --topic-key "openclaw-hooks-001"
```

### 4.4 batch_store - 批量存入

```bash
kb batch_store --file "/path/to/entries.jsonl"
```

---

## 5. 检索机制 (retriever.sh)

### 5.1 Pipeline

```
Query → 解析 → FTS5 搜索 → RRF 融合 → 时间衰减 → 分层加载 → Top-K
```

### 5.2 RRF (Reciprocal Rank Fusion)

多策略结果融合：

| 策略 | 说明 |
|------|------|
| 关键词匹配 | FTS5 直接匹配 |
| 标签匹配 | 命中的 tags |
| 来源偏好 | 同一来源加权 |
| 时间衰减 | 最近的内容排名更高 |

**公式**：
```
score = Σ (1 / (k + rank_i)) for each strategy i
```

### 5.3 时间衰减

```
decay = e^(-λ * days_ago)
```

- λ 通常取 0.1
- 7 天前的内容权重 ≈ 0.5

### 5.4 分层加载 (L0/L1/L2)

| 层级 | 内容 | Token 限制 |
|------|------|-----------|
| L0 | 标题 + 摘要 | < 200 tokens |
| L1 | 概览 + 要点 | < 1000 tokens |
| L2 | 完整内容 | 无限制 |

---

## 6. Hooks 集成 (hooks.sh)

### 6.1 before_agent_start - 对话前 Recall

```
用户发送消息
       ↓
Hook 拦截 → 调用 knowledge-base skill
       ↓
分析用户消息，提取关键词
       ↓
search() 检索相关知识
       ↓
注入 L0/L1 上下文到 Agent
```

### 6.2 after_turn - 对话后 Capture

```
Agent 生成回复
       ↓
Hook 拦截 → 调用 knowledge-base skill
       ↓
分析本次对话，提取：
  - 新决策/结论
  - 用户偏好/配置
  - 重要上下文
       ↓
store() 存入知识库
```

---

## 7. 配置 (config.sh)

### 7.1 路径配置

```bash
# 模式：private 或 shared
export KNOWLEDGE_KB_MODE="${KNOWLEDGE_KB_MODE:-private}"

# 私有 KB 路径
export KNOWLEDGE_DB_PRIVATE="$AGENT_DIR/knowledge.db"

# 共享 KB 路径
export KNOWLEDGE_DB_SHARED="$HOME/.openclaw/shared/knowledge-bases/<kb-name>/knowledge.db"

# 最终路径
if [ "$KNOWLEDGE_KB_MODE" = "shared" ]; then
  export KNOWLEDGE_DB="$KNOWLEDGE_DB_SHARED"
else
  export KNOWLEDGE_DB="$KNOWLEDGE_DB_PRIVATE"
fi
```

### 7.2 环境变量

| 变量 | 说明 | 默认值 |
|------|------|--------|
| KNOWLEDGE_KB_MODE | private/shared | private |
| KNOWLEDGE_DB | 数据库路径 | AGENT_DIR/knowledge.db |
| KNOWLEDGE_FTS_WEIGHT_TITLE | 标题权重 | 2.0 |
| KNOWLEDGE_FTS_WEIGHT_CONTENT | 内容权重 | 1.0 |

---

## 8. 知识库共享模式

### 8.1 私有 KB

- 路径：`~/.openclaw/agents/<agent-id>/knowledge.db`
- 用途：仅该 Agent 使用
- 特点：数据完全隔离

### 8.2 共享 KB

- 路径：`~/.openclaw/shared/knowledge-bases/<kb-name>/knowledge.db`
- 用途：多 Agent 共享同一知识库
- 特点：数据集中存储，组内所有 Agent 可读写

### 8.3 目录结构

```
~/.openclaw/
├── shared/
│   └── knowledge-bases/
│       ├── coding-kb/         # AI Coding 共享知识库
│       │   └── knowledge.db
│       └── research-kb/       # 研究类共享知识库
│           └── knowledge.db
├── agents/
│   ├── agent-a/
│   │   └── knowledge.db  ──────────→ shared/coding-kb/
│   ├── agent-b/
│   │   └── knowledge.db  ──────────→ shared/coding-kb/
```

---

## 9. Skill 触发条件

### 9.1 触发关键词

- "加入知识库" / "存到 KB"
- "knowledge base" / "知识库"
- "记得这个" / "保存上下文"
- "检索知识库" / "查一下 KB"

### 9.2 自动触发场景

1. **定时任务创建时**：询问是否需要采集信息到 KB
2. **before_agent_start**：自动 recall 相关知识
3. **after_turn**：自动 capture 新内容

---

## 10. 与 Agent 的集成方式

### 10.1 Cron Job 集成

```
Agent 创建定时任务时：
1. 询问是否需要存入知识库
2. 如果是，选择存储模式（私有/共享）
3. Cron job 包含调用 kb store 的逻辑
```

### 10.2 Hook 集成

```
1. 在 OpenClaw 配置 hooks.sh
2. before_agent_start → kb recall
3. after_turn → kb capture
```

---

## 11. 下一步

- [ ] 步骤 2：补充详细参考文档
- [ ] 步骤 3：调研 GitHub 和 Skill 市场
- [ ] 步骤 4：分析参考方案
- [ ] 步骤 5：优化并发布到 GitHub

---

## 附录

### A. 输出格式示例

**search 输出**：
```json
{
  "results": [
    {
      "topic_key": "openclaw-hooks-001",
      "title": "OpenClaw Hooks 配置指南",
      "level": "L1",
      "summary": "OpenClaw 支持 before_agent_start...",
      "url": "optional-link",
      "relevance_score": 0.85,
      "source": "manual",
      "updated_at": "2026-03-22"
    }
  ],
  "total_hits": 12,
  "query_time_ms": 45
}
```

### B. 参考项目

- [engram](https://github.com/khulnasoft/engram) - SQLite FTS5、TopicKey Upsert
- [MemOS](../Projects/MemOS) - RRF+MMR 检索、SkillEvolver
- [OpenViking](../Projects/OpenViking) - 分层加载、Context Engine
- [MemMachine](../Projects/MemMachine) - 四级隔离、Auto Hooks


path: RESEARCH.md
content: # 调研报告 - 参考项目分析

**日期**: 2026-03-22

---

## 一、GitHub/Skill 市场调研结果

### 1.1 ClawHub (Skill 市场)

**网址**: https://clawhub.com / https://clawhub.ai

**现状**:
- 刚上线，skill 数量较少
- 目前没有 knowledge-base 相关 skill
- 是未来发布的主要渠道

### 1.2 awesome-openclaw-skills

**Stars**: 40.6k
**链接**: https://github.com/VoltAgent/awesome-openclaw-skills

**现状**:
- 收录 339+ OpenClaw skills
- 按场景分类整理
- 包含中文版本

### 1.3 obsidian-agent-memory-skills

**Stars**: 27
**链接**: https://github.com/AdamTylerLynch/obsidian-agent-memory-skills

**核心设计**:
```
┌─────────────────────────────────────────────────┐
│ Session Start                                    │
│   Agent reads: TODOs → Project Overview          │
│   (2 files, ~100 lines — minimal token cost)     │
├─────────────────────────────────────────────────┤
│ During Work                                      │
│   Project Overview ──link──→ Component Note      │
│        │                         │               │
│        └──link──→ Pattern   ──link──→ Domain     │
│                    Note             Knowledge     │
│   Agent follows links ON DEMAND                  │
├─────────────────────────────────────────────────┤
│ Session End                                      │
│   Agent writes: Session summary, updates TODOs,  │
│   creates/updates component and pattern notes     │
└─────────────────────────────────────────────────┘
```

**优点**:
- **Proactive behaviors**: 自动 session 方向、结束时的 summary
- **Token 优化**: frontmatter-first scanning, CLI over file reads
- **Bidirectional relationships**: `relate` command with BFS tree walking
- **Graph traversal**: wikilinks 按需导航
- **多 agent 兼容**: Claude Code, Cursor, Cline, Windsurf, Copilot

**缺点**:
- 需要 Obsidian vault（额外依赖）
- 不支持 SQLite FTS5
- 无 RRF 融合检索

### 1.4 openclaw-superpowers

**Stars**: 25
**链接**: https://github.com/ArchieIndian/openclaw-superpowers

**核心设计**:
- **53 个 skills**: 15 核心 + 37 OpenClaw-native + 1 community
- **Self-modifying**: agent 可以自己写新 skill
- **DAG-based memory compaction**: 层级摘要 DAGs
- **SQLite session persistence + FTS5**: 和我们的方案类似！
- **Security guardrails**: 6 层安全防护
- **Runtime verification**: cron registration, state freshness

**关键特性**:
```
persistent-memory-hygiene    - 每日清理
memory-dag-compactor         - 层级 DAG 摘要
memory-integrity-checker     - DAG 完整性验证
session-persistence          - SQLite + FTS5 存储
dag-recall                   - DAG recall
context-assembly-scorer      - 上下文评分
```

**优点**:
- SQLite + FTS5 实现（和我们一致）
- DAG 层级摘要（可借鉴）
- 安全防护机制完善
- 20 个 cron scheduled skills

**缺点**:
- 偏向内存/会话管理，非专门知识库
- 无 RRF 融合检索
- 无明确的 topic_key upsert

### 1.5 para-memory

**Stars**: 4
**链接**: https://github.com/theSamPadilla/para-memory

**核心设计**:
| Folder | What goes here | Example |
|--------|-----------------|---------|
| `projects/` | Active work with goal/end | `migrate-to-v2.md` |
| `areas/` | Ongoing responsibilities | `infrastructure.md` |
| `resources/` | Reference material | `postgres-tuning.md` |
| `archives/` | Completed/inactive | `old-auth-system.md` |
| `memory/` | Daily logs (`YYYY-MM-DD.md`) | `2025-01-15.md` |

**优点**:
- PARA 方法论成熟
- 生命周期管理（active → archived）
- 每日 memory 日志

**缺点**:
- 无全文检索
- 无 SQLite 存储
- 偏向文件组织，非检索系统

---

## 二、可借鉴的设计

### 2.1 obsidian-agent-memory-skills 的借鉴点

1. **Proactive session orientation**
   - Session 开始时自动读取 TODOs + 项目概览
   - 最小化 token 消耗

2. **End-of-session summary**
   - Session 结束时自动写 summary
   - 更新 TODOs

3. **Bidirectional relationships**
   - `depends-on` / `depended-on-by`
   - `extends` / `extended-by`
   - BFS 树遍历

4. **Token 优化策略**
   - frontmatter-first scanning
   - CLI over file reads
   - scoped navigation

### 2.2 openclaw-superpowers 的借鉴点

1. **DAG-based memory compaction**
   - d0 leaf → d3+ durable 层级
   - depth-aware prompts

2. **Session persistence with FTS5**
   - SQLite + FTS5 实现
   - 可直接参考

3. **Integrity checker**
   - DAG orphans 检测
   - circular refs 检测
   - token inflation 检测

4. **Runtime verification**
   - cron registration 检查
   - state freshness 检查

### 2.3 para-memory 的借鉴点

1. **Lifecycle management**
   - active → archived 状态转换
   - 定期归档旧内容

2. **Daily memory log**
   - YYYY-MM-DD.md 日志
   - 便于时间追溯

---

## 三、我们的方案优化建议

### 3.1 借鉴 Proactive Session Orientation

**现状**: 只有 before_agent_start hook 触发 recall

**优化**: 借鉴 obsidian 的设计，session 开始时只读关键文件：
- 只读 L0 摘要层（< 200 tokens）
- 最多读 2 个文件
- 减少 token 消耗

### 3.2 借鉴 End-of-Session Summary

**现状**: after_turn hook 只做 capture

**优化**: 添加 session 结束检测：
- 检测 "done" / "wrapping up" 等关键词
- 自动写 session summary
- 更新相关 topic_key

### 3.3 借鉴 DAG-based Memory Compaction

**现状**: 只有简单的 access_level 分层

**优化**: 引入 DAG 结构：
- 叶子节点 = 原始内容
- 中间节点 = 摘要
- 根节点 = 高层次洞察
- 支持 expand/collapse

### 3.4 借鉴 Lifecycle Management

**现状**: 无生命周期管理

**优化**: 添加状态转换：
- active → stale → archived
- 自动归档 365 天前的 L0 记录
- 定期清理

### 3.5 借鉴 Token Optimization

**现状**: 加载层级粗粒度

**优化**: 借鉴 frontmatter-first scanning：
- 只加载 frontmatter 获取摘要
- 按需加载完整内容
- 使用 CLI 替代直接文件读取

---

## 四、结论

### 4.1 需要避免的

- **不要过度复杂化**: obsidian 的 bidirectional relationships 可能过度设计
- **不要引入过多依赖**: obsidian 需要额外安装
- **不要偏离核心**: 保持"怎么存、怎么取"的定位

### 4.2 应该采纳的

1. **SQLite + FTS5**: openclaw-superpowers 验证可行
2. **Proactive recall**: session 开始时自动检索
3. **DAG 摘要**: 层级化的记忆压缩
4. **Lifecycle management**: 定期归档和清理
5. **Token 优化**: frontmatter-first + 按需加载

### 4.3 保持我们的优势

- **RRF 融合检索**: 多个策略的结果融合
- **Topic_key upsert**: 同一主题的内容合并
- **共享 KB 模式**: 多 agent 共享知识库
- **Hook 自动触发**: before_agent_start + after_turn

---

## 五、下一步

- [ ] 详细分析每个借鉴点的实现细节
- [ ] 制定优化方案
- [ ] 更新 SPEC.md


path: SPEC.md
content: # Knowledge Base Skill - 设计规格文档

**版本**: v1.0.0
**日期**: 2026-03-22
**状态**: `draft`
**仓库**: https://github.com/ThomasLiu/knowledge-base-skill

---

## 1. 概述

### 1.1 目标

创建一个通用的 **Knowledge Base Skill**，为 OpenClaw Agent 提供知识存储和检索能力。

### 1.2 核心定位

- **只负责怎么存、怎么取**
- **不内置采集逻辑**（collector 由外部 Agent 决定）
- **支持私有 KB 和共享 KB**
- **轻量级、零外部依赖**

### 1.3 三大核心功能

| 功能 | 触发时机 | 说明 |
|------|----------|------|
| **Collect** | Agent 主动调用 | 存入数据到 KB |
| **Recall** | 每次对话前 (before_agent_start) | 自动检索相关知识 |
| **Capture** | 每次回复后 (after_turn) | 自动记忆重要内容 |

### 1.4 借鉴的设计

基于 GitHub/Skill 市场调研，借鉴以下项目：

| 项目 | 借鉴点 |
|------|--------|
| **obsidian-agent-memory-skills** | Proactive session orientation, Token 优化, Bidirectional relationships |
| **openclaw-superpowers** | SQLite + FTS5, DAG-based memory compaction, Integrity checker |
| **para-memory** | Lifecycle management, Daily memory log |

---

## 2. 目录结构

```
knowledge-base/
├── SKILL.md                    # Skill 定义（触发条件、接口说明）
├── scripts/
│   ├── storage.sh              # 存储核心 API
│   │   ├── store               # 存入单条
│   │   ├── search              # 检索
│   │   ├── retrieve            # 按 topic_key 取
│   │   └── batch_store         # 批量存入
│   ├── hooks.sh                # Hooks 集成
│   │   ├── before_agent_start  # 对话前：recall（借鉴 proactive orientation）
│   │   └── after_turn         # 对话后：capture
│   ├── retriever.sh            # 检索排序
│   │   ├── fuse_rank           # RRF 融合
│   │   ├── time_decay          # 时间衰减
│   │   └── dag_compactor       # DAG 层级摘要（借鉴 openclaw-superpowers）
│   ├── lifecycle.sh            # 生命周期管理（借鉴 para-memory）
│   │   ├── archive             # 归档旧记录
│   │   └── cleanup             # 清理过期数据
│   └── config.sh               # 路径配置
├── references/
│   ├── db-schema.md            # 数据库 Schema
│   ├── api-spec.md             # API 接口规范
│   └── retrieval-pipeline.md  # 检索流程详解
├── tests/
│   └── *_test.sh               # TDD 测试
└── README.md                   # 使用说明
```

---

## 3. 数据库设计

### 3.1 Schema

```sql
CREATE VIRTUAL TABLE knowledge_entries USING fts5(
  id,
  topic_key,
  title,
  content,
  source,
  tags,
  access_level,
  created_at,
  updated_at,
  metadata
);

CREATE TABLE entries (
  id TEXT PRIMARY KEY,
  topic_key TEXT UNIQUE NOT NULL,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  source TEXT NOT NULL,
  tags TEXT,
  access_level INTEGER DEFAULT 2,
  status TEXT DEFAULT 'active',  -- active/stale/archived
  created_at TEXT,
  updated_at TEXT,
  archived_at TEXT,
  metadata TEXT
);

CREATE INDEX idx_entries_status ON entries(status);
CREATE INDEX idx_entries_created_at ON entries(created_at);
CREATE INDEX idx_entries_topic_key ON entries(topic_key);
```

### 3.2 生命周期状态

| 状态 | 说明 | 转换条件 |
|------|------|----------|
| `active` | 活跃记录 | 默认状态 |
| `stale` | 陈旧但未归档 | 90 天无更新 |
| `archived` | 已归档 | 365 天前创建 + stale |

---

## 4. 检索机制

### 4.1 Pipeline

```
Query → 解析 → FTS5 搜索 → RRF 融合 → 时间衰减 → DAG 摘要 → 分层加载 → Top-K
```

### 4.2 RRF (Reciprocal Rank Fusion)

多策略结果融合：

| 策略 | 说明 |
|------|------|
| 关键词匹配 | FTS5 直接匹配 |
| 标签匹配 | 命中的 tags |
| 来源偏好 | 同一来源加权 |
| 时间衰减 | 最近的内容排名更高 |

### 4.3 DAG 层级摘要（借鉴 openclaw-superpowers）

```
d3 (Root)     - 高层次洞察
   ↓
d2 (Summary)  - 摘要层
   ↓
d1 (Overview) - 概览层
   ↓
d0 (Leaf)     - 原始内容
```

- **d0**: 完整内容（L2）
- **d1**: 概览 + 要点（L1, < 1000 tokens）
- **d2**: 简短摘要（L0, < 200 tokens）
- **d3**: 高层次洞察（可选）

### 4.4 时间衰减

```
decay = e^(-λ * days_ago)
```

- λ = 0.1
- 7 天前权重 ≈ 0.5

---

## 5. Hooks 集成

### 5.1 before_agent_start（借鉴 obsidian proactive orientation）

```
用户发送消息
       ↓
Hook 拦截 → 调用 kb recall
       ↓
解析用户消息，提取关键词
       ↓
检索相关知识（只取 L0/L1，限制 token）
       ↓
注入上下文到 Agent
```

**优化点**：
- 只读 L0/L1 层（< 200 tokens 总计）
- 最多读 3 个相关条目
- frontmatter-first scanning

### 5.2 after_turn

```
Agent 生成回复
       ↓
Hook 拦截 → 调用 kb capture
       ↓
分析本次对话，提取：
  - 新决策/结论
  - 用户偏好/配置
  - 重要上下文
       ↓
判断是否值得记忆（阈值 0.7）
       ↓
store() 存入知识库
```

---

## 6. 配置

### 6.1 路径配置

```bash
# 模式
export KNOWLEDGE_KB_MODE="${KNOWLEDGE_KB_MODE:-private}"

# 私有 KB
export KNOWLEDGE_DB_PRIVATE="$AGENT_DIR/knowledge.db"

# 共享 KB
export KNOWLEDGE_DB_SHARED="$HOME/.openclaw/shared/knowledge-bases/<kb-name>/knowledge.db"
```

### 6.2 环境变量

| 变量 | 说明 | 默认值 |
|------|------|--------|
| KNOWLEDGE_KB_MODE | private/shared | private |
| KNOWLEDGE_DB | 数据库路径 | AGENT_DIR/knowledge.db |
| KNOWLEDGE_RECALL_LIMIT | recall 最大条目数 | 3 |
| KNOWLEDGE_RECALL_MAX_TOKENS | recall 最大 token | 500 |
| KNOWLEDGE_ARCHIVE_DAYS | 归档阈值（天） | 365 |
| KNOWLEDGE_STALE_DAYS | 陈旧阈值（天） | 90 |

---

## 7. API 接口

### 7.1 store

```bash
kb store --title "..." --content "..." --tags "tag1,tag2" --source "manual"
```

### 7.2 search

```bash
kb search --query "openclaw hooks" --limit 5 --level 1
```

### 7.3 recall（Hook 调用）

```bash
kb recall --context "用户问如何配置 hooks"
```

**输出**：
```json
{
  "injected_context": "相关知识：\n1. OpenClaw Hooks 配置指南\n...",
  "entries_used": 3,
  "tokens_used": 450
}
```

### 7.4 capture（Hook 调用）

```bash
kb capture --turn "用户问... Agent 回复..."
```

**输出**：
```json
{
  "captured": true,
  "topic_key": "openclaw-hooks-001",
  "decision": "值得记忆：新决策"
}
```

### 7.5 lifecycle

```bash
# 归档旧记录
kb lifecycle archive

# 清理过期数据
kb lifecycle cleanup
```

---

## 8. 与 Agent 的集成

### 8.1 Cron Job 集成

```
Agent 创建定时任务时：
1. 询问是否需要存入知识库
2. 如果是，选择存储模式（私有/共享）
3. Cron job 包含调用 kb store 的逻辑
```

### 8.2 Hook 集成

```bash
# 在 OpenClaw 配置 hooks.sh
before_agent_start → kb recall
after_turn → kb capture
```

---

## 9. 知识库共享模式

### 9.1 私有 KB

- 路径：`~/.openclaw/agents/<agent-id>/knowledge.db`
- 用途：仅该 Agent 使用

### 9.2 共享 KB

- 路径：`~/.openclaw/shared/knowledge-bases/<kb-name>/knowledge.db`
- 用途：多 Agent 共享

---

## 10. TDD 开发流程

| 阶段 | 测试 | 实现 |
|------|------|------|
| RED | 写测试用例 | 验证失败 |
| GREEN | 最小实现 | 测试通过 |
| REFACTOR | 重构代码 | 保持测试通过 |

---

## 11. 验收标准

### Phase 1: 核心存储
- [ ] storage.sh store/search/retrieve 实现
- [ ] SQLite FTS5 索引建立
- [ ] TopicKey upsert 逻辑

### Phase 2: 检索增强
- [ ] RRF 融合
- [ ] 时间衰减
- [ ] 分层加载

### Phase 3: Hooks 集成
- [ ] before_agent_start recall
- [ ] after_turn capture
- [ ] Token 优化

### Phase 4: 生命周期
- [ ] DAG 摘要生成
- [ ] 归档策略
- [ ] 清理策略

---

## 12. 参考项目

| 项目 | Stars | 链接 | 借鉴点 |
|------|-------|------|--------|
| obsidian-agent-memory-skills | 27 | github.com/AdamTylerLynch/... | Proactive orientation, Token 优化 |
| openclaw-superpowers | 25 | github.com/ArchieIndian/... | SQLite + FTS5, DAG compaction |
| para-memory | 4 | github.com/theSamPadilla/... | Lifecycle management |
| awesome-openclaw-skills | 40.6k | github.com/VoltAgent/... | Skill 收录集合 |

---

## 13. 变更记录

| 日期 | 版本 | 变更内容 |
|------|------|----------|
| 2026-03-22 | 1.0.0 | 初始版本，基于调研结果优化设计 |


path: _meta.json
content: ownerId: kn7a9fwd3c5rpvp2d61wmvxdps82hf5v
slug: kb-mini
version: 1.0.0
publishedAt: 1774169594238

path: references/api-spec.md
content: # API 接口规范

## 1. 概述

所有接口通过 `kb` 命令行工具调用：

```bash
kb <command> [options]
```

## 2. store - 存入单条

### 2.1 用法

```bash
kb store [options]
```

### 2.2 参数

| 参数 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `--title`, `-t` | 是 | 标题 | `"OpenClaw Hooks"` |
| `--content`, `-c` | 是 | 内容 | `"配置说明..."` |
| `--topic-key` | 否 | 主题 key，默认自动生成 | `openclaw-hooks` |
| `--tags` | 否 | 标签，逗号分隔 | `openclaw,hooks` |
| `--source` | 否 | 来源，默认 manual | `rss/github/huggingface/manual` |
| `--level` | 否 | 层级，默认 2 | `0/1/2` |
| `--metadata` | 否 | 扩展 JSON | `'{"url":"..."}'` |

### 2.3 示例

```bash
kb store \
  --title "OpenClaw Hooks 配置" \
  --content "OpenClaw 支持 before_agent_start 和 after_turn..." \
  --tags "openclaw,hooks" \
  --source "manual" \
  --level 2
```

### 2.4 输出

**成功**：
```json
{
  "status": "ok",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "topic_key": "openclaw-hooks",
  "action": "insert"  // insert 或 update
}
```

**失败**：
```json
{
  "status": "error",
  "error": "Missing required argument: --title"
}
```

## 3. search - 检索

### 3.1 用法

```bash
kb search [options]
```

### 3.2 参数

| 参数 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `--query`, `-q` | 是 | 搜索关键词 | `openclaw hooks` |
| `--limit` | 否 | 返回数量，默认 5 | `10` |
| `--level` | 否 | 加载层级 | `0/1/2` |
| `--source` | 否 | 按来源过滤 | `rss/github/huggingface` |
| `--since` | 否 | 过滤时间 | `7d/30d/90d` |
| `--time-decay` | 否 | 时间衰减因子 | `0.1` |
| `--format` | 否 | 输出格式，默认 json | `json/text/compact` |

### 3.3 示例

```bash
# 基础搜索
kb search --query "openclaw hooks"

# 带过滤
kb search --query "openclaw" --source github --since 7d --limit 10

# 简洁输出
kb search --query "openclaw" --format compact
```

### 3.4 输出

**JSON 格式（默认）**：
```json
{
  "results": [
    {
      "topic_key": "openclaw-hooks",
      "title": "OpenClaw Hooks 配置指南",
      "level": "L1",
      "summary": "OpenClaw 支持 before_agent_start...",
      "relevance_score": 0.85,
      "source": "manual",
      "tags": ["openclaw", "hooks"],
      "updated_at": "2026-03-22T10:00:00Z"
    }
  ],
  "total_hits": 12,
  "query_time_ms": 45
}
```

**Text 格式**：
```
1. OpenClaw Hooks 配置指南 (0.85)
   摘要: OpenClaw 支持 before_agent_start...
   来源: manual | 更新: 2026-03-22
   
2. OpenClaw before_agent_start Hook (0.72)
   摘要: 在 Agent 启动前执行...
   来源: github | 更新: 2026-03-21
```

**Compact 格式**：
```
[0.85] OpenClaw Hooks 配置指南 - manual - 2026-03-22
[0.72] OpenClaw before_agent_start Hook - github - 2026-03-21
```

## 4. retrieve - 按 topic_key 取

### 4.1 用法

```bash
kb retrieve [options]
```

### 4.2 参数

| 参数 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `--topic-key`, `-k` | 是 | 主题 key | `openclaw-hooks` |
| `--level` | 否 | 加载层级，默认 2 | `0/1/2` |

### 4.3 示例

```bash
kb retrieve --topic-key "openclaw-hooks"
```

### 4.4 输出

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "topic_key": "openclaw-hooks",
  "title": "OpenClaw Hooks 配置指南",
  "content": "完整内容...",
  "source": "manual",
  "tags": ["openclaw", "hooks"],
  "level": "L2",
  "created_at": "2026-03-20T10:00:00Z",
  "updated_at": "2026-03-22T10:00:00Z"
}
```

## 5. batch_store - 批量存入

### 5.1 用法

```bash
kb batch_store [options]
```

### 5.2 参数

| 参数 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `--file`, `-f` | 是 | JSONL 文件路径 | `/path/to/entries.jsonl` |
| `--source` | 否 | 默认来源 | `rss` |

### 5.3 文件格式

JSONL（每行一个 JSON）：
```jsonl
{"title": "Title 1", "content": "Content 1", "tags": "tag1,tag2"}
{"title": "Title 2", "content": "Content 2", "tags": "tag3"}
```

### 5.4 示例

```bash
kb batch_store --file "/tmp/entries.jsonl" --source "rss"
```

### 5.5 输出

```json
{
  "status": "ok",
  "processed": 100,
  "inserted": 95,
  "updated": 5,
  "errors": 0
}
```

## 6. config - 配置管理

### 6.1 用法

```bash
kb config [subcommand]
```

### 6.2 子命令

| 子命令 | 说明 |
|--------|------|
| `kb config show` | 显示当前配置 |
| `kb config set <key> <value>` | 设置配置 |
| `kb config mode [private/shared]` | 切换 KB 模式 |

### 6.3 示例

```bash
# 显示配置
kb config show

# 切换到共享 KB
kb config mode shared

# 设置默认加载层级
kb config set default_level 1
```

### 6.4 输出

```json
{
  "kb_mode": "private",
  "db_path": "/Users/thomas/.openclaw/agents/coding-research-partner/knowledge.db",
  "fts_weight_title": 2.0,
  "fts_weight_content": 1.0,
  "default_level": 2,
  "time_decay_lambda": 0.1
}
```

## 7. recall - Hook 自动检索（内部接口）

### 7.1 用法

```bash
kb recall --context "<对话上下文>"
```

### 7.2 参数

| 参数 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `--context` | 是 | 对话上下文 | `"用户问如何配置 hooks"` |
| `--limit` | 否 | 返回数量，默认 3 | `5` |

### 7.3 用途

供 `before_agent_start` hook 调用，无需用户显式触发。

### 7.4 输出

```json
{
  "injected_context": "相关知识：\n1. OpenClaw Hooks 配置指南\n2. before_agent_start 用法\n...",
  "entries_used": 3
}
```

## 8. capture - Hook 自动捕获（内部接口）

### 8.1 用法

```bash
kb capture --turn "<对话内容>"
```

### 8.2 参数

| 参数 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `--turn` | 是 | 对话内容 | `"用户问... Agent 回复..."` |
| `--threshold` | 否 | 捕获阈值，默认 0.7 | `0.8` |

### 8.3 用途

供 `after_turn` hook 调用，自动判断是否值得记忆。

### 8.4 输出

```json
{
  "captured": true,
  "topic_key": "openclaw-hooks-001",
  "decision": "值得记忆：新决策"
}
```

## 9. 错误码

| 错误码 | 说明 |
|--------|------|
| `E001` | 缺少必填参数 |
| `E002` | 数据库初始化失败 |
| `E003` | FTS 查询失败 |
| `E004` | 文件读取失败 |
| `E005` | JSON 解析失败 |
| `E006` | 配置错误 |

## 10. 退出码

| 退出码 | 说明 |
|--------|------|
| 0 | 成功 |
| 1 | 一般错误 |
| 2 | 使用错误（参数错误）|
| 3 | 系统错误（数据库等）|


path: references/db-schema.md
content: # 数据库 Schema 详解

## 1. 主表结构

### 1.1 entries 表

```sql
CREATE TABLE entries (
  id TEXT PRIMARY KEY,
  topic_key TEXT UNIQUE NOT NULL,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  source TEXT NOT NULL,
  tags TEXT,
  access_level INTEGER DEFAULT 2,
  created_at TEXT,
  updated_at TEXT,
  metadata TEXT
);
```

### 1.2 FTS5 虚拟表

```sql
CREATE VIRTUAL TABLE knowledge_entries USING fts5(
  id,
  topic_key,
  title,
  content,
  source,
  tags,
  access_level,
  created_at,
  updated_at,
  content='entries',
  content_rowid='rowid'
);
```

## 2. 字段详解

| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| id | UUID | 唯一标识 | `550e8400-e29b-41d4-a716-446655440000` |
| topic_key | String | 主题 key，用于 upsert | `openclaw-hooks-config` |
| title | String | 标题 | `OpenClaw Hooks 配置指南` |
| content | Text | 完整内容 | 详细配置说明... |
| source | Enum | 来源 | `rss/github/huggingface/manual` |
| tags | JSON | 标签数组 | `["openclaw", "hooks"]` |
| access_level | Int | 加载层级 | `0=L0, 1=L1, 2=L2` |
| created_at | ISO8601 | 创建时间 | `2026-03-22T10:00:00Z` |
| updated_at | ISO8601 | 更新时间 | `2026-03-22T10:00:00Z` |
| metadata | JSON | 扩展字段 | `{"url": "...", "author": "..."}` |

## 3. TopicKey 生成规则

```
topic_key = slugify(title)
          = lowercase
          = replace spaces with hyphens
          = remove special characters
          = truncate to 50 chars
```

**示例**：
- `OpenClaw Hooks 配置指南` → `openclaw-hooks`

**冲突处理**：
- 同一 topic_key 的内容 → 追加到现有记录，更新 updated_at
- 不同 topic_key → 新建记录

## 4. Access Level 分层

| Level | 名称 | 内容 | Token 限制 |
|-------|------|------|-----------|
| 0 | L0 摘要 | 标题 + 简短摘要 | < 200 tokens |
| 1 | L1 概览 | 标题 + 摘要 + 要点 | < 1000 tokens |
| 2 | L2 详情 | 完整内容 | 无限制 |

## 5. Source 枚举值

| 值 | 说明 |
|-----|------|
| `rss` | RSS 订阅源抓取 |
| `github` | GitHub 项目监测 |
| `huggingface` | HuggingFace 模型追踪 |
| `manual` | 用户手动输入 |
| `capture` | Hook 自动捕获 |
| `import` | 外部导入 |

## 6. 索引

```sql
-- 提升常见查询性能
CREATE INDEX idx_entries_source ON entries(source);
CREATE INDEX idx_entries_created_at ON entries(created_at);
CREATE INDEX idx_entries_topic_key ON entries(topic_key);
```

## 7. 数据保留策略

```sql
-- 删除超过 365 天且 access_level = 0 的 L0 摘要
DELETE FROM entries 
WHERE access_level = 0 
AND created_at < datetime('now', '-365 days');
```

## 8. Upsert 逻辑

```sql
INSERT INTO entries (id, topic_key, title, content, source, tags, access_level, created_at, updated_at, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(topic_key) DO UPDATE SET
  content = content || chr(10) || excluded.content,
  updated_at = excluded.updated_at,
  metadata = excluded.metadata;
```

**注意**：Upsert 时内容是**追加**而非覆盖，保留历史版本。

## 9. JSON 字段处理

### 9.1 tags 存储

```sql
-- 存入
INSERT INTO entries (tags) VALUES (json.dumps(["tag1", "tag2"]));

-- 查询 tag 包含某值
SELECT * FROM entries WHERE json_each(tags) = 'tag1';
```

### 9.2 metadata 存储

```sql
-- 存入
INSERT INTO entries (metadata) VALUES (json.dumps({"url": "...", "stars": 100}));

-- 查询特定字段
SELECT * FROM entries WHERE json_extract(metadata, '$.stars') > 50;
```

## 10. 迁移脚本模板

```sql
-- v0.1.0 -> v0.2.0 迁移
ALTER TABLE entries ADD COLUMN language TEXT DEFAULT 'zh';

-- 重建 FTS 索引
INSERT INTO knowledge_entries(knowledge_entries) VALUES('rebuild');
```


path: references/retrieval-pipeline.md
content: # 检索 Pipeline 详解

## 1. 整体流程

```
用户查询
    ↓
┌─────────────────────────────────────────────┐
│            1. Query 解析                     │
│  - 分词                                      │
│  - 实体提取                                  │
│  - 同义词扩展（可选）                          │
└─────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────┐
│         2. 多策略并行搜索                     │
│  - FTS5 关键词搜索                            │
│  - 标签匹配搜索                              │
│  - 来源偏好搜索                              │
└─────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────┐
│         3. RRF 融合                          │
│  - 多策略结果排名融合                         │
│  - 计算综合得分                              │
└─────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────┐
│         4. 时间衰减加权                        │
│  - 越新的内容权重越高                          │
└─────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────┐
│         5. 分层加载                          │
│  - L0/L1/L2 按需加载                          │
└─────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────┐
│         6. 输出 Top-K                        │
└─────────────────────────────────────────────┘
```

## 2. Query 解析

### 2.1 分词

```bash
# 使用 FTS5 的分词器
query = "openclaw hooks 配置"
tokens = fts5_tokenize(query)
# → ["openclaw", "hooks", "配置"]
```

### 2.2 布尔搜索支持

```sql
-- AND: openclaw AND hooks
-- OR:  openclaw OR hooks  
-- NOT: openclaw NOT hooks
```

### 2.3 短语搜索

```sql
-- 匹配完整短语 "openclaw hooks"
SELECT * FROM knowledge_entries WHERE content MATCH '"openclaw hooks"';
```

## 3. 多策略搜索

### 3.1 FTS5 关键词搜索

```sql
-- 基础搜索，带权重
SELECT id, topic_key, title, content,
       bm25(knowledge_entries, 10.0, 1.0) as score
FROM knowledge_entries
WHERE knowledge_entries MATCH ?
ORDER BY score
LIMIT 100;
```

**权重配置**：
- `bm25(knowledge_entries, 10.0, 1.0)` = 标题权重 10.0，内容权重 1.0

### 3.2 标签匹配搜索

```sql
-- 查询 tags 包含关键词的记录
SELECT * FROM entries 
WHERE json_each(tags) IN (SELECT value FROM json_each(?));
```

### 3.3 来源偏好搜索

```sql
-- 优先返回同一来源的记录
SELECT *, 
       CASE WHEN source = ? THEN 1.2 ELSE 1.0 END as source_boost
FROM entries
WHERE content MATCH ?
ORDER BY source_boost DESC;
```

## 4. RRF 融合

### 4.1 公式

```
RRF_score(d) = Σ 1 / (k + rank_i(d))

其中：
- d 是文档
- k 是常数（通常 60）
- rank_i(d) 是策略 i 中文档 d 的排名
```

### 4.2 实现

```python
def rrf_fuse(results_by_strategy, k=60):
    scores = defaultdict(float)
    
    for strategy, results in results_by_strategy.items():
        for rank, doc in enumerate(results):
            doc_id = doc['id']
            scores[doc_id] += 1.0 / (k + rank)
    
    # 排序返回
    sorted_docs = sorted(scores.items(), key=lambda x: -x[1])
    return sorted_docs
```

### 4.3 k 值影响

| k 值 | 效果 |
|------|------|
| k=0 | 只看排名，不平滑 |
| k=60 | 标准值，平衡好 |
| k=120 | 重视多样性 |

## 5. 时间衰减

### 5.1 指数衰减

```
decay(d) = e^(-λ * days_ago)

其中：
- λ 是衰减因子（通常 0.1）
- days_ago 是距离今天的天数
```

### 5.2 衰减曲线

| days_ago | λ=0.1 时的权重 |
|----------|----------------|
| 0 | 1.00 |
| 7 | 0.50 |
| 14 | 0.25 |
| 30 | 0.05 |
| 90 | 0.0001 |

### 5.3 实现

```python
import math

def time_decay(created_at, lambda_param=0.1):
    days_ago = (datetime.now() - created_at).days
    return math.exp(-lambda_param * days_ago)

# 最终得分
final_score = rrf_score * time_decay(created_at)
```

## 6. 分层加载

### 6.1 L0 - 摘要层

```sql
SELECT 
  id,
  topic_key,
  title,
  substr(content, 1, 200) as summary,
  source,
  updated_at
FROM entries
WHERE ...
-- < 200 tokens
```

### 6.2 L1 - 概览层

```sql
SELECT 
  id,
  topic_key,
  title,
  content as overview,
  json_extract(metadata, '$.key_points') as key_points,
  source,
  updated_at
FROM entries
WHERE ...
-- < 1000 tokens
```

### 6.3 L2 - 详情层

```sql
SELECT 
  *,
  content
FROM entries
WHERE ...
-- 完整内容
```

## 7. MMR 多样性重排（可选）

### 7.1 公式

```
MMR = argmax [ λ * Similarity(doc, query) - (1-λ) * max Similarity(doc, already_selected) ]
```

### 7.2 用途

避免返回结果过于相似，提高多样性。

## 8. 完整流程实现

```python
def retrieve(query, limit=5, level=1, time_decay_lambda=0.1):
    # 1. Query 解析
    tokens = tokenize(query)
    
    # 2. 多策略搜索
    fts_results = fts5_search(tokens, limit=100)
    tag_results = tag_search(tokens, limit=100)
    source_results = source_search(tokens, limit=100)
    
    # 3. RRF 融合
    fused = rrf_fuse({
        'fts': fts_results,
        'tag': tag_results,
        'source': source_results
    })
    
    # 4. 时间衰减
    for doc in fused:
        doc['final_score'] = doc['score'] * time_decay(doc['created_at'])
    
    # 5. 分层加载
    loaded = load_levels(fused, level=level)
    
    # 6. Top-K
    return loaded[:limit]
```

## 9. 性能优化

### 9.1 索引优化

```sql
-- FTS 索引
CREATE VIRTUAL TABLE knowledge_entries USING fts5(...);

-- 普通索引
CREATE INDEX idx_entries_source ON entries(source);
CREATE INDEX idx_entries_created_at ON entries(created_at);
```

### 9.2 缓存策略

- 热数据（FTS 索引）常驻内存
- 查询结果缓存 5 分钟

### 9.3 限制

```sql
-- 最大返回 100 条
LIMIT 100

-- 超时 1 秒
PRAGMA query_only = 1;
PRAGMA busy_timeout = 1000;
```

## 10. 评估指标

| 指标 | 说明 |
|------|------|
| MRR@K | 平均倒数排名 |
| NDCG@K | 归一化折扣收益 |
| Recall@K | 召回率 |
| Query Time | 查询耗时 |


path: scripts/hooks.sh
content: #!/bin/bash
# Knowledge Base Skill - Hooks
# OpenClaw Hooks 集成

set -e

# ============================================
# 配置
# ============================================

# KB 路径
export KNOWLEDGE_DB="${KNOWLEDGE_DB:-$HOME/.openclaw/agents/current/knowledge.db}"

# Hook 脚本路径
KB_SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STORAGE_SCRIPT="$KB_SCRIPTS_DIR/storage.sh"
RETRIEVER_SCRIPT="$KB_SCRIPTS_DIR/retriever.sh"

# ============================================
# before_agent_start hook
# ============================================

do_before_agent_start() {
    local context
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --context) context="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    if [[ -z "$context" ]]; then
        echo '{"status": "error", "error": "Missing required argument: --context"}'
        return 1
    fi
    
    # 调用 recall
    local recall_result
    recall_result=$("$RETRIEVER_SCRIPT" recall --context "$context" --limit 3 --max-tokens 500 2>/dev/null)
    
    # 解析 recall 结果
    local injected_context entries_used
    injected_context=$(echo "$recall_result" | python3 -c "
import sys, json, re
try:
    raw = sys.stdin.read()
    raw = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', raw)
    data = json.loads(raw)
    print(data.get('injected_context', ''))
except:
    print('')
" 2>/dev/null)
    
    entries_used=$(echo "$recall_result" | python3 -c "
import sys, json, re
try:
    raw = sys.stdin.read()
    raw = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', raw)
    data = json.loads(raw)
    print(data.get('entries_used', 0))
except:
    print(0)
" 2>/dev/null)
    
    # 输出结果
    if [[ -n "$injected_context" ]]; then
        echo "{\"status\": \"ok\", \"action\": \"recall\", \"entries_used\": $entries_used, \"injected\": true}"
    else
        echo "{\"status\": \"ok\", \"action\": \"recall\", \"entries_used\": 0, \"injected\": false}"
    fi
}

# ============================================
# after_turn hook
# ============================================

do_after_turn() {
    local user_message agent_message threshold
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --user) user_message="$2"; shift 2 ;;
            --agent) agent_message="$2"; shift 2 ;;
            --threshold) threshold="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    threshold="${threshold:-0.7}"
    
    if [[ -z "$user_message" ]]; then
        echo '{"status": "error", "error": "Missing required argument: --user"}'
        return 1
    fi
    
    # 组合对话
    local turn="$user_message"
    if [[ -n "$agent_message" ]]; then
        turn="$turn\n$agent_message"
    fi
    
    # 调用 capture 判断是否值得存储
    local capture_result
    capture_result=$("$RETRIEVER_SCRIPT" capture --turn "$turn" --threshold "$threshold" 2>/dev/null)
    
    # 解析 capture 结果
    local captured score decision
    captured=$(echo "$capture_result" | python3 -c "
import sys, json, re
try:
    raw = sys.stdin.read()
    raw = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', raw)
    data = json.loads(raw)
    print('true' if data.get('captured') else 'false')
except:
    print('false')
" 2>/dev/null)
    
    score=$(echo "$capture_result" | python3 -c "
import sys, json, re
try:
    raw = sys.stdin.read()
    raw = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', raw)
    data = json.loads(raw)
    print(data.get('score', 0))
except:
    print(0)
" 2>/dev/null)
    
    decision=$(echo "$capture_result" | python3 -c "
import sys, json, re
try:
    raw = sys.stdin.read()
    raw = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', raw)
    data = json.loads(raw)
    print(data.get('decision', ''))
except:
    print('')
" 2>/dev/null)
    
    # 如果值得存储，存入 KB
    if [[ "$captured" == "true" ]]; then
        # 提取关键信息作为 title 和 content
        local title="User note: ${user_message:0:50}"
        local content="$turn"
        local topic_key
        topic_key=$(echo "$user_message" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' | cut -c1-50)
        
        # 存入
        "$STORAGE_SCRIPT" store \
            --title "$title" \
            --content "$content" \
            --source "capture" \
            --topic-key "capture-$topic_key" \
            --tags "auto-captured" \
            > /dev/null 2>&1
        
        echo "{\"status\": \"ok\", \"action\": \"capture\", \"captured\": true, \"score\": $score, \"decision\": \"$decision\"}"
    else
        echo "{\"status\": \"ok\", \"action\": \"capture\", \"captured\": false, \"score\": $score, \"decision\": \"$decision\"}"
    fi
}

# ============================================
# generate - 生成 hook 脚本
# ============================================

do_generate() {
    local type="${1:-before_agent_start}"
    local kb_path="${KNOWLEDGE_DB}"
    
    case "$type" in
        before_agent_start)
            cat <<EOF
#!/bin/bash
# OpenClaw before_agent_start hook
# Generated by knowledge-base skill

export KNOWLEDGE_DB="$kb_path"

SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
source "\$SCRIPT_DIR/../scripts/hooks.sh"

# 获取用户消息（从环境变量或参数）
CONTEXT="\${1:-}"
if [[ -z "\$CONTEXT" && -n "\$OPENCLAW_USER_MESSAGE" ]]; then
    CONTEXT="\$OPENCLAW_USER_MESSAGE"
fi

if [[ -n "\$CONTEXT" ]]; then
    hooks before_agent_start --context "\$CONTEXT"
fi
EOF
            ;;
        after_turn)
            cat <<EOF
#!/bin/bash
# OpenClaw after_turn hook
# Generated by knowledge-base skill

export KNOWLEDGE_DB="$kb_path"

SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
source "\$SCRIPT_DIR/../scripts/hooks.sh"

# 获取对话内容
USER_MSG="\${1:-}"
AGENT_MSG="\${2:-}"

if [[ -n "\$USER_MSG" ]]; then
    hooks after_turn --user "\$USER_MSG" --agent "\$AGENT_MSG"
fi
EOF
            ;;
        *)
            echo "Unknown hook type: $type"
            return 1
            ;;
    esac
}

# ============================================
# config - 配置
# ============================================

do_config() {
    local key value
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --kb-path) 
                KNOWLEDGE_DB="$2"
                export KNOWLEDGE_DB
                # 确保目录存在
                mkdir -p "$(dirname "$KNOWLEDGE_DB")"
                shift 2
                ;;
            --recall-limit)
                export KNOWLEDGE_RECALL_LIMIT="$2"
                shift 2
                ;;
            --capture-threshold)
                export KNOWLEDGE_CAPTURE_THRESHOLD="$2"
                shift 2
                ;;
            *)
                shift
                ;;
        esac
    done
    
    echo "{\"status\": \"ok\", \"kb_path\": \"$KNOWLEDGE_DB\"}"
}

# ============================================
# 主入口
# ============================================

main() {
    local command="${1:-}"
    shift
    
    case "$command" in
        before_agent_start)
            do_before_agent_start "$@"
            ;;
        after_turn)
            do_after_turn "$@"
            ;;
        generate)
            do_generate "$@"
            ;;
        config)
            do_config "$@"
            ;;
        *)
            cat <<EOF
Knowledge Base Hooks
====================

Usage:
  hooks <command> [options]

Commands:
  before_agent_start    对话前 hook - recall 相关知识
  after_turn            对话后 hook - capture 新内容
  generate              生成 hook 脚本
  config                配置

Examples:
  hooks before_agent_start --context "用户消息"
  hooks after_turn --user "用户说..." --agent "Agent 回复..."
  hooks generate before_agent_start > ~/.openclaw/hooks/before_agent_start
  hooks config --kb-path /path/to/kb.db

EOF
            ;;
    esac
}

# 如果直接执行
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi


path: scripts/lifecycle.sh
content: #!/bin/bash
# Knowledge Base Skill - Lifecycle Management
# 生命周期管理 + DAG 摘要

set -e

# ============================================
# 配置
# ============================================

export KNOWLEDGE_DB="${KNOWLEDGE_DB:-$HOME/.openclaw/agents/current/knowledge.db}"

# 生命周期阈值
STALE_DAYS="${KNOWLEDGE_STALE_DAYS:-90}"
ARCHIVE_DAYS="${KNOWLEDGE_ARCHIVE_DAYS:-365}"
CLEANUP_DAYS="${KNOWLEDGE_CLEANUP_DAYS:-30}"

SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STORAGE="$SCRIPTS_DIR/storage.sh"

# ============================================
# mark-stale - 标记陈旧记录
# ============================================

do_mark_stale() {
    local threshold="${1:-$STALE_DAYS}"
    
    local count
    count=$(sqlite3 "$KNOWLEDGE_DB" "
        UPDATE entries 
        SET status = 'stale' 
        WHERE status = 'active' 
        AND datetime(updated_at) < datetime('now', '-$threshold days');
    " 2>/dev/null)
    
    # 获取实际更新的行数
    count=$(sqlite3 "$KNOWLEDGE_DB" "
        SELECT COUNT(*) FROM entries 
        WHERE status = 'stale' 
        AND datetime(updated_at) < datetime('now', '-$threshold days');
    " 2>/dev/null || echo "0")
    
    echo "{\"status\": \"ok\", \"action\": \"mark-stale\", \"marked\": $count}"
}

# ============================================
# archive - 归档过期记录
# ============================================

do_archive() {
    local threshold="${1:-$ARCHIVE_DAYS}"
    
    local count
    count=$(sqlite3 "$KNOWLEDGE_DB" "
        UPDATE entries 
        SET status = 'archived', archived_at = datetime('now')
        WHERE status = 'stale' 
        AND datetime(updated_at) < datetime('now', '-$threshold days');
    " 2>/dev/null)
    
    count=$(sqlite3 "$KNOWLEDGE_DB" "
        SELECT COUNT(*) FROM entries 
        WHERE status = 'archived';
    " 2>/dev/null || echo "0")
    
    echo "{\"status\": \"ok\", \"action\": \"archive\", \"archived\": $count}"
}

# ============================================
# cleanup - 清理已归档记录
# ============================================

do_cleanup() {
    local threshold="${1:-$CLEANUP_DAYS}"
    
    # 先获取数量
    local count_before
    count_before=$(sqlite3 "$KNOWLEDGE_DB" "
        SELECT COUNT(*) FROM entries 
        WHERE status = 'archived' 
        AND datetime(archived_at) < datetime('now', '-$threshold days');
    " 2>/dev/null || echo "0")
    
    # 删除
    sqlite3 "$KNOWLEDGE_DB" "
        DELETE FROM entries 
        WHERE status = 'archived' 
        AND datetime(archived_at) < datetime('now', '-$threshold days');
    " 2>/dev/null
    
    echo "{\"status\": \"ok\", \"action\": \"cleanup\", \"deleted\": $count_before}"
}

# ============================================
# summarize - 生成摘要（DAG d2 层）
# ============================================

do_summarize() {
    local topic_key limit
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --topic-key) topic_key="$2"; shift 2 ;;
            --limit) limit="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    limit="${limit:-200}"
    
    if [[ -z "$topic_key" ]]; then
        echo '{"status": "error", "error": "Missing required argument: --topic-key"}'
        return 1
    fi
    
    # 获取完整内容
    local content
    content=$(sqlite3 "$KNOWLEDGE_DB" "
        SELECT content FROM entries 
        WHERE topic_key = '$topic_key' 
        AND status != 'archived'
        LIMIT 1;
    " 2>/dev/null)
    
    if [[ -z "$content" ]]; then
        echo '{"status": "error", "error": "Entry not found"}'
        return 1
    fi
    
    # 生成摘要（简单实现：取前 N 个字符）
    local summary
    summary=$(echo "$content" | head -c "$limit")
    
    if [[ ${#content} -gt $limit ]]; then
        summary="$summary..."
    fi
    
    # 提取关键点（简单实现：找句子）
    local key_points
    key_points=$(echo "$content" | grep -oE '[^.!?]+[.!?]' | head -3 | tr '\n' ' ' || echo "")
    
    # 构建 DAG 摘要（d2 层）
    local dag_summary
    dag_summary=$(python3 -c "
import sys, json

content = '''$content'''
summary = '''$summary'''
key_points = '''$key_points'''

# 简单的 DAG 结构
dag = {
    'd0': content,  # 原始内容
    'd2': {  # 摘要层
        'summary': summary,
        'key_points': key_points,
        'word_count': len(content.split()),
        'char_count': len(content)
    }
}

print(json.dumps(dag, ensure_ascii=False, indent=2))
" 2>/dev/null)
    
    echo "{\"status\": \"ok\", \"topic_key\": \"$topic_key\", \"dag\": $dag_summary}"
}

# ============================================
# compact - DAG 压缩（多级摘要）
# ============================================

do_compact() {
    local max_entries="${1:-100}"
    
    # 获取需要压缩的条目（按 updated_at 排序）
    local entries
    entries=$(sqlite3 -json "$KNOWLEDGE_DB" "
        SELECT id, topic_key, title, content, source, updated_at
        FROM entries
        WHERE status = 'active'
        ORDER BY updated_at DESC
        LIMIT $max_entries
    " 2>/dev/null || echo "[]")
    
    local count
    count=$(echo "$entries" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
    
    echo "{\"status\": \"ok\", \"action\": \"compact\", \"processed\": $count}"
}

# ============================================
# stats - 状态统计
# ============================================

do_stats() {
    local stats
    stats=$(sqlite3 "$KNOWLEDGE_DB" "
        SELECT 
            status,
            COUNT(*) as count
        FROM entries
        GROUP BY status;
    " 2>/dev/null)
    
    # 转换为 JSON
    echo "$stats" | python3 -c "
import sys, json

stats = {}
for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    parts = line.split('|')
    if len(parts) == 2:
        status, count = parts
        stats[status] = int(count)

# 添加总计
stats['total'] = sum(stats.values())

# 添加时间范围
import subprocess
result = subprocess.run(['sqlite3', '$KNOWLEDGE_DB', \"SELECT MIN(created_at), MAX(created_at) FROM entries;\"], capture_output=True, text=True)
dates = result.stdout.strip().split('|')
if len(dates) == 2:
    stats['oldest'] = dates[0] if dates[0] else None
    stats['newest'] = dates[1] if dates[1] else None

print(json.dumps(stats, ensure_ascii=False, indent=2))
" 2>/dev/null || echo "{\"total\": 0, \"active\": 0}"
}

# ============================================
# integrity - DAG 完整性检查
# ============================================

do_integrity() {
    local issues=()
    
    # 检查孤立记录（没有引用的摘要）
    local orphans
    orphans=$(sqlite3 "$KNOWLEDGE_DB" "
        SELECT COUNT(*) FROM entries e
        WHERE status = 'archived'
        AND NOT EXISTS (
            SELECT 1 FROM entries r 
            WHERE r.topic_key = e.topic_key 
            AND r.status = 'active'
        );
    " 2>/dev/null || echo "0")
    
    if [[ "$orphans" -gt 0 ]]; then
        issues+=("{\"type\": \"orphans\", \"count\": $orphans}")
    fi
    
    # 检查循环引用（自己引用自己）
    local self_refs
    self_refs=$(sqlite3 "$KNOWLEDGE_DB" "
        SELECT COUNT(*) FROM entries 
        WHERE metadata LIKE '%\"parent\":\"%' 
        AND topic_key = substr(metadata, instr(metadata, '\"parent\":\"') + 9, 50);
    " 2>/dev/null || echo "0")
    
    if [[ "$self_refs" -gt 0 ]]; then
        issues+=("{\"type\": \"self_references\", \"count\": $self_refs}")
    fi
    
    # 检查 token 膨胀（摘要比原文还长）
    local inflated
    inflated=$(sqlite3 "$KNOWLEDGE_DB" "
        SELECT COUNT(*) FROM entries 
        WHERE LENGTH(content) > 5000 
        AND LENGTH(content) < 100;
    " 2>/dev/null || echo "0")
    
    if [[ ${#issues[@]} -eq 0 ]]; then
        echo "{\"status\": \"ok\", \"action\": \"integrity\", \"issues\": [], \"healthy\": true}"
    else
        echo "{\"status\": \"ok\", \"action\": \"integrity\", \"issues\": [$(IFS=,; echo "${issues[*]}")], \"healthy\": false}"
    fi
}

# ============================================
# repair - 修复问题
# ============================================

do_repair() {
    local fixed=0
    
    # 修复孤立记录：删除孤立的 archived 记录
    local orphans
    orphans=$(sqlite3 "$KNOWLEDGE_DB" "
        DELETE FROM entries 
        WHERE status = 'archived'
        AND NOT EXISTS (
            SELECT 1 FROM entries r 
            WHERE r.topic_key = entries.topic_key 
            AND r.status = 'active'
        );
    " 2>/dev/null)
    
    # 重新计算被影响的数量
    fixed=$(sqlite3 "$KNOWLEDGE_DB" "SELECT COUNT(*) FROM entries WHERE status = 'archived';" 2>/dev/null || echo "0")
    
    echo "{\"status\": \"ok\", \"action\": \"repair\", \"fixed\": $fixed}"
}

# ============================================
# 主入口
# ============================================

main() {
    local command="${1:-}"
    shift
    
    case "$command" in
        mark-stale)
            do_mark_stale "$@"
            ;;
        archive)
            do_archive "$@"
            ;;
        cleanup)
            do_cleanup "$@"
            ;;
        summarize)
            do_summarize "$@"
            ;;
        compact)
            do_compact "$@"
            ;;
        stats)
            do_stats "$@"
            ;;
        integrity)
            do_integrity "$@"
            ;;
        repair)
            do_repair "$@"
            ;;
        run)
            # 运行完整生命周期：mark-stale → archive → cleanup
            do_mark_stale
            do_archive
            do_cleanup
            ;;
        *)
            cat <<EOF
Knowledge Base Lifecycle
=========================

Usage:
  lifecycle <command> [options]

Commands:
  mark-stale    标记陈旧记录（默认 90 天）
  archive       归档过期记录（默认 365 天）
  cleanup       清理已归档记录（默认 30 天）
  summarize     生成摘要（DAG d2 层）
  compact      DAG 压缩
  stats        状态统计
  integrity    DAG 完整性检查
  repair       修复问题
  run          运行完整生命周期

Examples:
  lifecycle mark-stale
  lifecycle archive
  lifecycle cleanup
  lifecycle summarize --topic-key "entry-key"
  lifecycle stats
  lifecycle integrity

EOF
            ;;
    esac
}

# 如果直接执行
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi


path: scripts/retriever.sh
content: #!/bin/bash
# Knowledge Base Skill - Retriever
# RRF 融合 + 时间衰减 + 分层加载

set -e

# ============================================
# 配置
# ============================================

export KNOWLEDGE_DB="${KNOWLEDGE_DB:-$HOME/.openclaw/agents/current/knowledge.db}"

# RRF 参数
RRF_K="${KNOWLEDGE_RRF_K:-60}"

# 时间衰减参数
TIME_DECAY_LAMBDA="${KNOWLEDGE_TIME_DECAY_LAMBDA:-0.1}"

# ============================================
# 主检索函数
# ============================================

do_retrieve() {
    local query limit level match_mode boost_source format
    local where_conditions="status != 'archived'"
    
    # 解析参数
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --query) query="$2"; shift 2 ;;
            --limit) limit="$2"; shift 2 ;;
            --level) level="$2"; shift 2 ;;
            --match-mode) match_mode="$2"; shift 2 ;;
            --boost-source) boost_source="$2"; shift 2 ;;
            --format) format="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    limit="${limit:-5}"
    level="${level:-2}"
    format="${format:-json}"
    
    if [[ -z "$query" ]]; then
        echo '{"status": "error", "error": "Missing required argument: --query"}'
        return 1
    fi
    
    # 构建搜索词
    local search_term="%$query%"
    
    # 根据不同模式构建 SQL
    local sql
    local order_by="ORDER BY updated_at DESC"
    local source_boost=""
    
    if [[ -n "$boost_source" ]]; then
        source_boost="CASE WHEN source = '$boost_source' THEN 1 ELSE 0 END as source_boost,"
        order_by="ORDER BY source_boost DESC, updated_at DESC"
    fi
    
    # SQL 查询
    local content_limit=500
    if [[ "$level" == "0" ]]; then
        content_limit=200
    elif [[ "$level" == "1" ]]; then
        content_limit=500
    fi
    
    sql="SELECT 
        id,
        topic_key,
        title,
        substr(content, 1, $content_limit) as summary,
        source,
        tags,
        access_level,
        created_at,
        updated_at,
        $source_boost
        1.0 as relevance_score
    FROM entries
    WHERE $where_conditions
    AND (title LIKE '$search_term' OR content LIKE '$search_term' OR tags LIKE '$search_term')
    $order_by
    LIMIT $limit"
    
    # 执行查询
    local results
    results=$(sqlite3 -json "${KNOWLEDGE_DB}" "$sql" 2>/dev/null || echo "[]")
    
    # 时间衰减计算
    local decay_results
    decay_results=$(echo "$results" | python3 -c "
import sys, json, math

try:
    data = json.load(sys.stdin)
    if isinstance(data, list):
        for item in data:
            # 计算时间衰减
            created_at = item.get('created_at', '')
            if created_at:
                try:
                    # 简单计算：假设格式为 ISO
                    from datetime import datetime
                    created = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
                    now = datetime.now(created.tzinfo) if created.tzinfo else datetime.now()
                    days = (now - created.replace(tzinfo=None)).days
                    decay = math.exp(-$TIME_DECAY_LAMBDA * days)
                except:
                    decay = 1.0
            else:
                decay = 1.0
            item['time_decay'] = decay
            item['final_score'] = item.get('relevance_score', 1.0) * decay
        # 按 final_score 排序
        data.sort(key=lambda x: -x.get('final_score', 0))
    print(json.dumps(data))
except Exception as e:
    print('[]')
" 2>/dev/null || echo "$results")
    
    # 计算命中数
    local total_hits
    total_hits=$(echo "$decay_results" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
    
    # 输出
    if [[ "$format" == "json" ]]; then
        echo "{\"results\": $decay_results, \"total_hits\": $total_hits, \"query\": \"$query\", \"level\": \"$level\"}"
    else
        echo "$decay_results" | python3 -c "
import sys, json
try:
    data = json.load(sys.stdin)
    if isinstance(data, list):
        for i, item in enumerate(data, 1):
            print(f\"{i}. {item.get('title', 'N/A')} (score: {item.get('final_score', 0):.3f})\")
            print(f\"   {item.get('summary', '')[:100]}...\")
except:
    pass
" 2>/dev/null
    fi
}

# ============================================
# recall - Hook 调用（自动检索）
# ============================================

do_recall() {
    local context limit max_tokens
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --context) context="$2"; shift 2 ;;
            --limit) limit="$2"; shift 2 ;;
            --max-tokens) max_tokens="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    limit="${limit:-3}"
    max_tokens="${max_tokens:-500}"
    
    if [[ -z "$context" ]]; then
        echo '{"status": "error", "error": "Missing required argument: --context"}'
        return 1
    fi
    
    # 使用 context 作为查询词
    local query="$context"
    
    # L0 级别检索
    local result
    result=$(do_retrieve --query "$query" --limit "$limit" --level 0 --format json)
    
    # 构建注入上下文
    local injected
    injected=$(echo "$result" | python3 -c "
import sys, json, re

try:
    # 处理换行符问题
    raw = sys.stdin.read()
    # 替换控制字符
    raw = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', raw)
    data = json.loads(raw)
    entries = data.get('results', [])
    
    if not entries:
        print('')
    else:
        lines = ['相关知识：']
        for i, entry in enumerate(entries, 1):
            title = entry.get('title', 'N/A')
            summary = entry.get('summary', '')[:150].replace('\\n', ' ')
            source = entry.get('source', 'N/A')
            updated = entry.get('updated_at', '')[:10]
            lines.append(f'{i}. {title} ({source}, {updated})')
            lines.append(f'   {summary}...')
        print('\\n'.join(lines))
except Exception as e:
    print('')
" 2>/dev/null)
    
    local entries_count
    entries_count=$(echo "$result" | python3 -c "
import sys, json, re
try:
    raw = sys.stdin.read()
    raw = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', raw)
    data = json.loads(raw)
    print(len(data.get('results',[])))
except:
    print(0)
" 2>/dev/null || echo "0")
    
    echo "{\"injected_context\": \"$injected\", \"entries_used\": $entries_count}"
}

# ============================================
# capture - Hook 调用（自动捕获）
# ============================================

do_capture() {
    local turn threshold
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --turn) turn="$2"; shift 2 ;;
            --threshold) threshold="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    threshold="${threshold:-0.7}"
    
    if [[ -z "$turn" ]]; then
        echo '{"status": "error", "error": "Missing required argument: --turn"}'
        return 1
    fi
    
    # 简单实现：检测关键词判断是否值得记忆
    local capture_score=0
    local decision=""
    
    # 检测关键词
    if echo "$turn" | grep -qi "记住\|save\|存储\|知识库\|knowledge"; then
        capture_score=0.9
        decision="显式请求存储"
    elif echo "$turn" | grep -qi "配置\|config\|设置\|setup"; then
        capture_score=0.8
        decision="配置信息"
    elif echo "$turn" | grep -qi "决策\|决定\|decision"; then
        capture_score=0.85
        decision="重要决策"
    elif echo "$turn" | grep -qi "api\|key\|token\|密码"; then
        capture_score=0.95
        decision="敏感信息"
    else
        capture_score=0.3
        decision="低优先级"
    fi
    
    # 比较分数
    local captured
    if python3 -c "exit(0 if $capture_score >= $threshold else 1)" 2>/dev/null; then
        captured="true"
    else
        captured="false"
    fi
    
    echo "{\"captured\": $captured, \"score\": $capture_score, \"decision\": \"$decision\"}"
}

# ============================================
# 主入口
# ============================================

main() {
    local command="${1:-}"
    shift
    
    case "$command" in
        retrieve|search)
            do_retrieve "$@"
            ;;
        recall)
            do_recall "$@"
            ;;
        capture)
            do_capture "$@"
            ;;
        *)
            cat <<EOF
Knowledge Base Retriever
========================

Usage:
  kb retriever <command> [options]

Commands:
  retrieve    检索（支持时间衰减 + 分层）
  recall      Hook 调用：自动检索相关知识
  capture     Hook 调用：判断是否值得记忆

Examples:
  kb retriever retrieve --query "OpenClaw" --limit 5 --level 1
  kb retriever recall --context "用户问如何配置 hooks"
  kb retriever capture --turn "用户说记得这个配置..."

EOF
            ;;
    esac
}

# 如果直接执行
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi


path: scripts/storage.sh
content: #!/bin/bash
# Knowledge Base Skill - Storage Core
# 提供 store, search, retrieve 等核心 API

set -e

# ============================================
# 配置
# ============================================

# 数据库路径
# 1. 显式环境变量（最高优先）
# 2. 共享 KB 模式（通过 KNOWLEDGE_SHARED_NAME 指定）
# 3. Skill 内部目录（默认，安装后即用）
SKILL_DIR="${KNOWLEDGE_SKILL_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
if [[ -n "$KNOWLEDGE_SHARED_NAME" ]]; then
    export KNOWLEDGE_DB="${KNOWLEDGE_DB:-$HOME/.openclaw/shared/knowledge-bases/$KNOWLEDGE_SHARED_NAME/knowledge.db}"
else
    export KNOWLEDGE_DB="${KNOWLEDGE_DB:-$SKILL_DIR/data/knowledge.db}"
fi

# FTS5 权重配置
FTS_WEIGHT_TITLE="${KNOWLEDGE_FTS_WEIGHT_TITLE:-2.0}"
FTS_WEIGHT_CONTENT="${KNOWLEDGE_FTS_WEIGHT_CONTENT:-1.0}"

# ============================================
# 工具函数
# ============================================

# 生成 UUID
gen_uuid() {
    if command -v uuidgen &> /dev/null; then
        uuidgen | tr '[:upper:]' '[:lower:]'
    else
        # macOS fallback
        printf '%s' "$(date +%s)-$$-$(head -c 16 /dev/urandom | xxd -p)"
    fi
}

# 生成 topic_key
gen_topic_key() {
    local title="$1"
    echo "$title" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' | cut -c1-50
}

# 格式化 JSON（简单实现）
json_escape() {
    echo "$1" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null | tr -d '"' || echo "$1"
}

# ============================================
# init - 初始化数据库
# ============================================

do_init() {
    local db="${KNOWLEDGE_DB}"
    
    # 确保目录存在
    mkdir -p "$(dirname "$db")"
    
    # 创建表
    sqlite3 "$db" "
        CREATE TABLE IF NOT EXISTS entries (
            id TEXT PRIMARY KEY,
            topic_key TEXT UNIQUE NOT NULL,
            title TEXT NOT NULL,
            content TEXT NOT NULL,
            source TEXT NOT NULL,
            tags TEXT,
            access_level INTEGER DEFAULT 2,
            status TEXT DEFAULT 'active',
            created_at TEXT,
            updated_at TEXT,
            archived_at TEXT,
            metadata TEXT
        );
        
        CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_entries USING fts5(
            id,
            topic_key,
            title,
            content,
            source,
            tags,
            access_level,
            created_at,
            updated_at,
            content='entries',
            content_rowid='rowid'
        );
        
        CREATE INDEX IF NOT EXISTS idx_entries_status ON entries(status);
        CREATE INDEX IF NOT EXISTS idx_entries_created_at ON entries(created_at);
        CREATE INDEX IF NOT EXISTS idx_entries_topic_key ON entries(topic_key);
    "
    
    :
}

# ============================================
# store - 存入数据
# ============================================

do_store() {
    local db="${KNOWLEDGE_DB}"
    local id topic_key title content source tags access_level metadata
    local created_at updated_at
    
    # 解析参数
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --id) id="$2"; shift 2 ;;
            --topic-key) topic_key="$2"; shift 2 ;;
            --title) title="$2"; shift 2 ;;
            --content) content="$2"; shift 2 ;;
            --source) source="$2"; shift 2 ;;
            --tags) tags="$2"; shift 2 ;;
            --level) access_level="$2"; shift 2 ;;
            --metadata) metadata="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    # 必填检查
    if [[ -z "$title" || -z "$content" ]]; then
        echo '{"status": "error", "error": "Missing required arguments: --title and --content"}'
        return 1
    fi
    
    # 默认值
    id="${id:-$(gen_uuid)}"
    topic_key="${topic_key:-$(gen_topic_key "$title")}"
    source="${source:-manual}"
    access_level="${access_level:-2}"
    created_at="${created_at:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
    updated_at="${updated_at:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
    tags="${tags:-[]}"
    metadata="${metadata:-{}}"
    
    # 检查是否存在
    local existing
    existing=$(sqlite3 "$db" "SELECT id FROM entries WHERE topic_key = '$topic_key' LIMIT 1" 2>/dev/null || true)
    
    local action
    if [[ -n "$existing" ]]; then
        # Update - 追加内容
        action="update"
        sqlite3 "$db" "
            UPDATE entries 
            SET content = content || char(10) || '$content',
                updated_at = '$updated_at',
                metadata = '$metadata'
            WHERE topic_key = '$topic_key'
        "
    else
        # Insert
        action="insert"
        sqlite3 "$db" "
            INSERT INTO entries (id, topic_key, title, content, source, tags, access_level, created_at, updated_at, metadata)
            VALUES ('$id', '$topic_key', '$title', '$content', '$source', '$tags', $access_level, '$created_at', '$updated_at', '$metadata')
        "
        
        # 更新 FTS 索引
        sqlite3 "$db" "
            INSERT INTO knowledge_entries (id, topic_key, title, content, source, tags, access_level, created_at, updated_at)
            VALUES ('$id', '$topic_key', '$title', '$content', '$source', '$tags', $access_level, '$created_at', '$updated_at')
        " 2>/dev/null || true
    fi
    
    echo "{\"status\": \"ok\", \"id\": \"$id\", \"topic_key\": \"$topic_key\", \"action\": \"$action\"}"
}

# ============================================
# search - 检索
# ============================================

do_search() {
    local db="${KNOWLEDGE_DB}"
    local query limit source level time_decay format
    local where_conditions="status != 'archived'"
    
    # 解析参数
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --query) query="$2"; shift 2 ;;
            --limit) limit="$2"; shift 2 ;;
            --source) source="$2"; shift 2 ;;
            --level) level="$2"; shift 2 ;;
            --since) since="$2"; shift 2 ;;
            --time-decay) time_decay="$2"; shift 2 ;;
            --format) format="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    limit="${limit:-5}"
    level="${level:-2}"
    format="${format:-json}"
    
    if [[ -z "$query" ]]; then
        echo '{"status": "error", "error": "Missing required argument: --query"}'
        return 1
    fi
    
    # 构建 WHERE 条件
    if [[ -n "$source" ]]; then
        where_conditions="$where_conditions AND source = '$source'"
    fi
    
    # 简单 FTS5 搜索
    # 注意：这里使用 LIKE 简化，实际生产应该用 FTS5 MATCH
    local search_term="%$query%"
    
    local results
    results=$(sqlite3 -json "$db" "
        SELECT 
            id,
            topic_key,
            title,
            CASE 
                WHEN access_level <= 1 THEN substr(content, 1, 200)
                ELSE substr(content, 1, 500)
            END as summary,
            source,
            tags,
            updated_at,
            1.0 as relevance_score
        FROM entries
        WHERE ($where_conditions)
        AND (title LIKE '$search_term' OR content LIKE '$search_term' OR tags LIKE '$search_term')
        ORDER BY updated_at DESC
        LIMIT $limit
    " 2>/dev/null || echo "[]")
    
    local total_hits
    total_hits=$(echo "$results" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
    
    if [[ "$format" == "json" ]]; then
        echo "{\"results\": $results, \"total_hits\": $total_hits}"
    elif [[ "$format" == "compact" ]]; then
        echo "$results" | python3 -c "
import sys, json
data = json.load(sys.stdin)
for item in data:
    print(f\"[{item['relevance_score']}] {item['title']} - {item['source']} - {item['updated_at']}\")
" 2>/dev/null || echo "$results"
    else
        echo "$results"
    fi
}

# ============================================
# retrieve - 按 topic_key 获取
# ============================================

do_retrieve() {
    local db="${KNOWLEDGE_DB}"
    local topic_key level
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --topic-key) topic_key="$2"; shift 2 ;;
            --level) level="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    level="${level:-2}"
    
    if [[ -z "$topic_key" ]]; then
        echo '{"status": "error", "error": "Missing required argument: --topic-key"}'
        return 1
    fi
    
    local result
    result=$(sqlite3 -json "$db" "
        SELECT 
            id,
            topic_key,
            title,
            content,
            source,
            tags,
            access_level as level,
            created_at,
            updated_at,
            metadata
        FROM entries
        WHERE topic_key = '$topic_key' AND status != 'archived'
        LIMIT 1
    " 2>/dev/null || echo "[]")
    
    # 提取单个结果
    echo "$result" | python3 -c "
import sys, json
data = json.load(sys.stdin)
if data:
    item = data[0]
    # 返回非 JSON 格式，方便使用
    print(f\"Title: {item['title']}\")
    print(f\"Content: {item['content']}\")
    print(f\"Source: {item['source']}\")
    print(f\"Updated: {item['updated_at']}\")
else:
    print('Not found')
" 2>/dev/null || echo "$result"
}

# ============================================
# batch_store - 批量存入
# ============================================

do_batch_store() {
    local db="${KNOWLED_DB}"
    local file source
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --file) file="$2"; shift 2 ;;
            --source) source="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    
    if [[ -z "$file" ]]; then
        echo '{"status": "error", "error": "Missing required argument: --file"}'
        return 1
    fi
    
    source="${source:-manual}"
    
    local processed=0
    local errors=0
    
    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        
        # 解析 JSONL
        title=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('title',''))" 2>/dev/null || true)
        content=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('content',''))" 2>/dev/null || true)
        tags=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tags',''))" 2>/dev/null || true)
        
        if [[ -n "$title" && -n "$content" ]]; then
            do_store --title "$title" --content "$content" --tags "$tags" --source "$source"
            ((processed++)) || true
        else
            ((errors++)) || true
        fi
    done < "$file"
    
    echo "{\"status\": \"ok\", \"processed\": $processed, \"errors\": $errors}"
}

# ============================================
# config - 配置管理
# ============================================

do_config() {
    local subcommand="$1"
    shift
    
    case "$subcommand" in
        show)
            cat <<EOF
{
  "kb_mode": "${KNOWLEDGE_KB_MODE:-private}",
  "db_path": "${KNOWLEDGE_DB}",
  "fts_weight_title": ${FTS_WEIGHT_TITLE},
  "fts_weight_content": ${FTS_WEIGHT_CONTENT}
}
EOF
            ;;
        mode)
            local mode="$1"
            if [[ -z "$mode" ]]; then
                echo "${KNOWLEDGE_KB_MODE:-private}"
            else
                export KNOWLEDGE_KB_MODE="$mode"
                echo "Mode set to: $mode"
            fi
            ;;
        *)
            echo "Unknown config command: $subcommand"
            return 1
            ;;
    esac
}

# ============================================
# 主入口
# ============================================

main() {
    local command="${1:-}"
    shift
    
    case "$command" in
        init)
            do_init "$@"
            ;;
        store)
            do_store "$@"
            ;;
        search)
            do_search "$@"
            ;;
        retrieve)
            do_retrieve "$@"
            ;;
        batch_store)
            do_batch_store "$@"
            ;;
        config)
            do_config "$@"
            ;;
        *)
            cat <<EOF
Knowledge Base Storage API
===========================

Usage:
  kb <command> [options]

Commands:
  init           初始化数据库
  store          存入数据
  search         检索
  retrieve       按 topic_key 获取
  batch_store    批量存入
  config         配置管理

Examples:
  kb init
  kb store --title "..." --content "..."
  kb search --query "..."
  kb retrieve --topic-key "..."
  kb config show

EOF
            ;;
    esac
}

# 如果直接执行此脚本
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

# 如果被 source（用于测试）
if [[ "$0" != "${BASH_SOURCE[0]}" ]] || [[ "$KB_SOURCED" == "1" ]]; then
    # 只导出函数，不执行
    :
else
    main "$@"
fi


path: tests/hooks_test.sh
content: #!/bin/bash
# hooks.sh TDD 测试
# Phase 3: OpenClaw Hooks 集成

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOOKS="$SCRIPT_DIR/../scripts/hooks.sh"
STORAGE="$SCRIPT_DIR/../scripts/storage.sh"
RETRIEVER="$SCRIPT_DIR/../scripts/retriever.sh"

echo "=== Phase 3: hooks.sh 测试 ==="

# ============================================
# TEST 1: before_agent_start hook - recall
# ============================================
echo ""
echo "TEST 1: before_agent_start hook"

TEST_DB="/tmp/kb_hooks_t1_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "OpenClaw Config" --content "Configure OpenClaw hooks in openclaw.json" --source "manual" --topic-key "openclaw-config"

# Simulate before_agent_start hook - use context that matches the content word order
result=$("$HOOKS" before_agent_start --context "Configure OpenClaw hooks" 2>&1)
echo "Result: $result"

if echo "$result" | grep -q '"entries_used": 1' && echo "$result" | grep -q '"injected": true'; then
    echo "✅ TEST 1 PASSED"
else
    echo "❌ TEST 1 FAILED"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 2: after_turn hook - capture
# ============================================
echo ""
echo "TEST 2: after_turn hook - 显式存储"

TEST_DB="/tmp/kb_hooks_t2_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init

# 模拟用户说 "请记住这个配置"
result=$("$HOOKS" after_turn --user "请记住这个配置：api_key=12345" --agent "好的，已记住" 2>&1)
echo "Result: $result"

if echo "$result" | grep -q '"captured": true'; then
    echo "✅ TEST 2 PASSED"
else
    echo "❌ TEST 2 FAILED"
fi

# 验证是否存入数据库
count=$(sqlite3 "$TEST_DB" "SELECT COUNT(*) FROM entries WHERE topic_key LIKE '%capture-%';" 2>/dev/null || echo "0")
if [[ "$count" -gt 0 ]]; then
    echo "✅ Verified: Entry captured to database"
else
    echo "❌ Failed: Entry not captured"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 3: after_turn hook - 低优先级不存储
# ============================================
echo ""
echo "TEST 3: after_turn hook - 低优先级"

TEST_DB="/tmp/kb_hooks_t3_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init

result=$("$HOOKS" after_turn --user "你好" --agent "你好，有什么可以帮助你的？" 2>&1)
echo "Result: $result"

if echo "$result" | grep -q '"captured": false'; then
    echo "✅ TEST 3 PASSED"
else
    echo "❌ TEST 3 FAILED"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 4: 生成的 hook 脚本格式正确
# ============================================
echo ""
echo "TEST 4: 生成 hook 脚本格式"

TEST_DB="/tmp/kb_hooks_t4_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init

# 生成 hook 脚本
hook_output=$("$HOOKS" generate before_agent_start 2>&1)
echo "Generated hook: $hook_output"

if echo "$hook_output" | grep -q "hooks before_agent_start"; then
    echo "✅ TEST 4 PASSED"
else
    echo "❌ TEST 4 FAILED"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 5: config 设置
# ============================================
echo ""
echo "TEST 5: hooks config"

TEST_DB="/tmp/kb_hooks_t5_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init

# 设置 KB 路径
"$HOOKS" config --kb-path "$TEST_DB" 2>&1

# 验证
if [[ -f "$TEST_DB" ]]; then
    echo "✅ TEST 5 PASSED: KB path configured"
else
    echo "❌ TEST 5 FAILED"
fi

rm -f "$TEST_DB"

echo ""
echo "=== Phase 3 测试完成 ==="


path: tests/lifecycle_test.sh
content: #!/bin/bash
# lifecycle.sh TDD 测试
# Phase 4: 生命周期管理 + DAG 摘要

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIFECYCLE="$SCRIPT_DIR/../scripts/lifecycle.sh"
STORAGE="$SCRIPT_DIR/../scripts/storage.sh"

echo "=== Phase 4: lifecycle.sh 测试 ==="

# ============================================
# TEST 1: 标记 stale 状态
# ============================================
echo ""
echo "TEST 1: 标记 stale 状态"

TEST_DB="/tmp/kb_lifecycle_t1_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "Old Entry" --content "Old content" --source "manual" --topic-key "old-entry"

# 修改为 91 天前（确保严格小于 90 天阈值）
sqlite3 "$TEST_DB" "UPDATE entries SET updated_at = datetime('now', '-91 days') WHERE topic_key = 'old-entry';"

# 运行 lifecycle mark-stale
result=$("$LIFECYCLE" mark-stale 2>&1)
echo "Result: $result"

# 检查状态
status=$(sqlite3 "$TEST_DB" "SELECT status FROM entries WHERE topic_key = 'old-entry';")
if [[ "$status" == "stale" ]]; then
    echo "✅ TEST 1 PASSED"
else
    echo "❌ TEST 1 FAILED: status=$status"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 2: 归档过期记录
# ============================================
echo ""
echo "TEST 2: 归档过期记录"

TEST_DB="/tmp/kb_lifecycle_t2_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "Archived Entry" --content "To be archived" --source "manual" --topic-key "archived-entry"

# 设置为 stale + 365 天前
sqlite3 "$TEST_DB" "UPDATE entries SET updated_at = datetime('now', '-400 days'), status = 'stale' WHERE topic_key = 'archived-entry';"

# 运行 archive
result=$("$LIFECYCLE" archive 2>&1)
echo "Result: $result"

# 检查状态
status=$(sqlite3 "$TEST_DB" "SELECT status FROM entries WHERE topic_key = 'archived-entry';")
if [[ "$status" == "archived" ]]; then
    echo "✅ TEST 2 PASSED"
else
    echo "❌ TEST 2 FAILED: status=$status"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 3: 清理已归档记录
# ============================================
echo ""
echo "TEST 3: 清理已归档记录"

TEST_DB="/tmp/kb_lifecycle_t3_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "To Delete" --content "To be deleted" --source "manual" --topic-key "delete-entry"

# 设置为 archived，archived_at 设置为 400 天前
sqlite3 "$TEST_DB" "
    UPDATE entries 
    SET status = 'archived', 
        archived_at = datetime('now', '-400 days'),
        updated_at = datetime('now', '-400 days')
    WHERE topic_key = 'delete-entry';
"

# 验证设置
echo "Before cleanup:"
sqlite3 "$TEST_DB" "SELECT status, archived_at FROM entries WHERE topic_key = 'delete-entry';"

# 运行 cleanup（默认 30 天阈值）
result=$("$LIFECYCLE" cleanup 2>&1)
echo "Result: $result"

# 检查是否删除
count=$(sqlite3 "$TEST_DB" "SELECT COUNT(*) FROM entries WHERE topic_key = 'delete-entry';" 2>/dev/null || echo "0")
if [[ "$count" == "0" ]]; then
    echo "✅ TEST 3 PASSED"
else
    echo "❌ TEST 3 FAILED: count=$count"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 4: 生成摘要 (DAG d2 层)
# ============================================
echo ""
echo "TEST 4: 生成摘要"

TEST_DB="/tmp/kb_lifecycle_t4_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "Long Content Entry" --content "This is a very long content that should be summarized. " && for i in {1..20}; do echo -n "Repeated information $i. "; done && echo "" > /dev/null

"$STORAGE" store --title "Long Content Entry" --content " More content to make it longer. Additional details. More text." --source "manual" --topic-key "long-content-entry"

# 运行 summarize
result=$("$LIFECYCLE" summarize --topic-key "long-content-entry" 2>&1)
echo "Result: $result"

if echo "$result" | grep -q "summary" || echo "$result" | grep -q "Summary"; then
    echo "✅ TEST 4 PASSED"
else
    echo "❌ TEST 4 FAILED"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 5: 状态统计
# ============================================
echo ""
echo "TEST 5: 状态统计"

TEST_DB="/tmp/kb_lifecycle_t5_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "Active 1" --content "Active" --source "manual" --topic-key "active-1"
"$STORAGE" store --title "Active 2" --content "Active" --source "manual" --topic-key "active-2"

# 标记一个 stale
sqlite3 "$TEST_DB" "UPDATE entries SET status = 'stale' WHERE topic_key = 'active-1';"

# 运行 stats
result=$("$LIFECYCLE" stats 2>&1)
echo "Result: $result"

if echo "$result" | grep -q "active" && echo "$result" | grep -q "stale"; then
    echo "✅ TEST 5 PASSED"
else
    echo "❌ TEST 5 FAILED"
fi

rm -f "$TEST_DB"

echo ""
echo "=== Phase 4 测试完成 ==="


path: tests/retriever_test.sh
content: #!/bin/bash
# retriever.sh TDD 测试
# Phase 2: 时间衰减 + 分层加载

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RETRIEVER="$SCRIPT_DIR/../scripts/retriever.sh"
STORAGE="$SCRIPT_DIR/../scripts/storage.sh"

echo "=== Phase 2: retriever.sh 测试 ==="

# ============================================
# TEST 1: 时间衰减
# ============================================
echo ""
echo "TEST 1: 时间衰减"

TEST_DB="/tmp/kb_retriever_t1_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "Recent Entry" --content "Content from today" --source "manual" --topic-key "recent-entry"
"$STORAGE" store --title "Old Entry" --content "Content from old day" --source "manual" --topic-key "old-entry"
sqlite3 "$TEST_DB" "UPDATE entries SET created_at = datetime('now', '-30 days') WHERE topic_key = 'old-entry';"

result=$("$RETRIEVER" retrieve --query "Content" --limit 2 2>&1)
echo "Result: $result"

if echo "$result" | grep -q "recent-entry" && echo "$result" | grep -q "old-entry"; then
    echo "✅ TEST 1 PASSED"
else
    echo "❌ TEST 1 FAILED"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 2: 分层加载 L0
# ============================================
echo ""
echo "TEST 2: 分层加载 L0"

TEST_DB="/tmp/kb_retriever_t2_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
LONG_CONTENT="This is a very long content. " && for i in {1..50}; do LONG_CONTENT="$LONG_CONTENT Repeated content $i. "; done
"$STORAGE" store --title "Long Entry" --content "$LONG_CONTENT" --source "manual" --topic-key "long-entry"

result=$("$RETRIEVER" retrieve --query "Repeated" --level 0 2>&1)
echo "Result length: $(echo "$result" | wc -c)"

if echo "$result" | grep -q "long-entry"; then
    echo "✅ TEST 2 PASSED"
else
    echo "❌ TEST 2 FAILED"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 3: 分层加载 L2
# ============================================
echo ""
echo "TEST 3: 分层加载 L2"

TEST_DB="/tmp/kb_retriever_t3_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "Test L2" --content "Full content here" --source "manual" --topic-key "l2-entry"

result=$("$RETRIEVER" retrieve --query "Full content" --level 2 2>&1)
echo "Result: $result"

if echo "$result" | grep -q "Full content here"; then
    echo "✅ TEST 3 PASSED"
else
    echo "❌ TEST 3 FAILED"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 4: 来源过滤
# ============================================
echo ""
echo "TEST 4: 来源过滤"

TEST_DB="/tmp/kb_retriever_t4_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "GitHub Item" --content "From github source" --source "github" --topic-key "github-item"
"$STORAGE" store --title "HF Item" --content "From huggingface source" --source "huggingface" --topic-key "hf-item"

result=$("$RETRIEVER" retrieve --query "source" --limit 1 --boost-source "github" 2>&1)
echo "Result: $result"

if echo "$result" | grep -q "github-item"; then
    echo "✅ TEST 4 PASSED"
else
    echo "❌ TEST 4 FAILED"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 5: recall
# ============================================
echo ""
echo "TEST 5: recall"

TEST_DB="/tmp/kb_retriever_t5_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

"$STORAGE" init
"$STORAGE" store --title "OpenClaw Hooks" --content "Configure hooks for agent" --source "manual" --topic-key "hooks-config"
"$STORAGE" store --title "SQLite FTS5" --content "Full text search" --source "manual" --topic-key "sqlite-fts5"

# recall uses context as search query, so use a term that matches the content
result=$("$RETRIEVER" recall --context "Configure hooks for agent" --limit 2 2>&1)
echo "Result: $result"

if echo "$result" | grep -q "OpenClaw Hooks"; then
    echo "✅ TEST 5 PASSED"
else
    echo "❌ TEST 5 FAILED"
fi

rm -f "$TEST_DB"

# ============================================
# TEST 6: capture
# ============================================
echo ""
echo "TEST 6: capture"

TEST_DB="/tmp/kb_retriever_t6_$$.db"
export KNOWLEDGE_DB="$TEST_DB"
rm -f "$TEST_DB"

result=$("$RETRIEVER" capture --turn "请记住这个配置：api_key=12345" 2>&1)
echo "Result: $result"

if echo "$result" | grep -q '"captured": true'; then
    echo "✅ TEST 6 PASSED"
else
    echo "❌ TEST 6 FAILED"
fi

echo ""
echo "=== Phase 2 测试完成 ==="


path: tests/storage_test.sh
content: #!/bin/bash
# storage.sh TDD 测试
# RED: 写测试 → GREEN: 最小实现 → REFACTOR: 重构

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
KB_SCRIPT="$SCRIPT_DIR/../scripts/storage.sh"

# 测试数据库路径（测试用）
TEST_DB="/tmp/kb_test_$$.db"
export KNOWLEDGE_DB="$TEST_DB"

echo "Testing with KB_SCRIPT=$KB_SCRIPT"
echo "Test DB=$TEST_DB"

# 清理函数
cleanup() {
    rm -f "$TEST_DB"
    rm -f "${TEST_DB}-journal"
}

trap cleanup EXIT

# 初始化数据库
init_db() {
    rm -f "$TEST_DB" "${TEST_DB}-journal"
}

echo "=== Phase 1: storage.sh 核心功能测试 ==="

# ============================================
# TEST 1: store - 基本存储
# ============================================
echo ""
echo "TEST 1: store - 基本存储"

init_db
"$KB_SCRIPT" init

result=$("$KB_SCRIPT" store --title "Test Entry" --content "Test content" --source "manual" 2>&1)
echo "Result: $result"

if echo "$result" | grep -q '"status".*"ok"'; then
    echo "✅ TEST 1 PASSED: store 基本存储成功"
else
    echo "❌ TEST 1 FAILED: store 返回错误"
    exit 1
fi

# ============================================
# TEST 2: store - 相同 topic_key 追加
# ============================================
echo ""
echo "TEST 2: store - 相同 topic_key 追加（upsert）"

init_db
"$KB_SCRIPT" init

# 第一次存入
"$KB_SCRIPT" store --title "Test" --content "Content A" --source "manual" --topic-key "test-entry" 2>&1

# 第二次存入相同 topic_key
result=$("$KB_SCRIPT" store --title "Test" --content "Content B" --source "manual" --topic-key "test-entry" 2>&1)

if echo "$result" | grep -q '"action".*"update"'; then
    echo "✅ TEST 2 PASSED: upsert 更新成功"
else
    echo "❌ TEST 2 FAILED: upsert 未更新"
    echo "Result was: $result"
    exit 1
fi

# ============================================
# TEST 3: search - 关键词检索
# ============================================
echo ""
echo "TEST 3: search - 关键词检索"

init_db
"$KB_SCRIPT" init

"$KB_SCRIPT" store --title "OpenClaw Hooks" --content "Configure before_agent_start and after_turn hooks" --source "manual" 2>&1
"$KB_SCRIPT" store --title "SQLite FTS5" --content "Full-text search with SQLite FTS5" --source "manual" 2>&1

result=$("$KB_SCRIPT" search --query "OpenClaw" 2>&1)
echo "Search result: $result"

if echo "$result" | grep -q "OpenClaw Hooks"; then
    echo "✅ TEST 3 PASSED: 关键词检索成功"
else
    echo "❌ TEST 3 FAILED: 检索未返回预期结果"
    exit 1
fi

# ============================================
# TEST 4: retrieve - 按 topic_key 获取
# ============================================
echo ""
echo "TEST 4: retrieve - 按 topic_key 获取"

init_db
"$KB_SCRIPT" init

"$KB_SCRIPT" store --title "Test Retrieve" --content "Full content here" --source "manual" --topic-key "retrieve-test" 2>&1

result=$("$KB_SCRIPT" retrieve --topic-key "retrieve-test" 2>&1)
echo "Retrieve result: $result"

if echo "$result" | grep -q "Full content here"; then
    echo "✅ TEST 4 PASSED: retrieve 成功"
else
    echo "❌ TEST 4 FAILED: retrieve 未返回内容"
    exit 1
fi

# ============================================
# TEST 5: store - 带 tags
# ============================================
echo ""
echo "TEST 5: store - 带 tags"

init_db
"$KB_SCRIPT" init

result=$("$KB_SCRIPT" store --title "Tagged Entry" --content "Content" --source "manual" --tags "tag1,tag2" 2>&1)

if echo "$result" | grep -q '"status".*"ok"'; then
    echo "✅ TEST 5 PASSED: tags 存储成功"
else
    echo "❌ TEST 5 FAILED: tags 存储失败"
    exit 1
fi

# ============================================
# TEST 6: search - 按 source 过滤
# ============================================
echo ""
echo "TEST 6: search - 按 source 过滤"

init_db
"$KB_SCRIPT" init

"$KB_SCRIPT" store --title "GitHub Entry" --content "From github" --source "github" 2>&1
"$KB_SCRIPT" store --title "HF Entry" --content "From huggingface" --source "huggingface" 2>&1

result=$("$KB_SCRIPT" search --query "entry" --source "github" 2>&1)
echo "Source filter result: $result"

if echo "$result" | grep -q "GitHub Entry" && ! echo "$result" | grep -q "HF Entry"; then
    echo "✅ TEST 6 PASSED: source 过滤成功"
else
    echo "❌ TEST 6 FAILED: source 过滤失败"
    exit 1
fi

echo ""
echo "=== Phase 1 测试完成 ==="
echo "所有核心存储测试通过！"


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