feat: 集成 Hermes + OpenClaw 记忆插件到仓库

- plugins/hermes-zhiyi: 530行 Python 插件, 含 key 自检/重试/调试日志
- plugins/openclaw-zhiyi: v0.2.0 CJS, 修复 agent_id/namespace/register 同步
- Hermes 插件改动: commit 失败自动重试, key_prefix 调试日志, health 自检
- OpenClaw 插件改动: register 改为同步, 使用 registerService 做异步初始化
This commit is contained in:
xiaowei 2026-05-30 04:46:42 +08:00
parent da917f7eb8
commit 2464689c32
22 changed files with 1931 additions and 0 deletions

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ __pycache__/
*.pyc
*.egg-info/
.venv/\n*.pyc\n__pycache__/\ntarget/\nzhiyi-consolidate\nzhiyid-new\ngo/zhiyi-consolidate\ngo/zhiyid-new\n
backups/

Binary file not shown.

View File

@ -0,0 +1,553 @@
"""hermes-zhiyi — Bridge plugin: Hermes Agent → ZhiYi MemoryWeave REST API.
Uses ZhiYi's bge-m3 1024-dim semantic search + bge-reranker-v2-m3 reranking.
Writes go to ZhiYi via /commit; reads come from /recall.
Usage:
1. Set memory.provider: zhiyi in config.yaml
2. Set ZHIYI_URL in .env (e.g. http://localhost:7821)
Lifecycle (MemoryProvider ABC):
initialize() validate URL, test connectivity
sync_turn() write turn to ZhiYi via /commit
prefetch() background recall via /recall
get_tool_schemas() expose memory_search / memory_write tools
handle_tool_call() dispatch tool calls
shutdown() clean exit
"""
from __future__ import annotations
import json
import logging
import os
import threading
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
import requests
import websocket
from agent.memory_provider import MemoryProvider
from tools.registry import tool_error
logger = logging.getLogger(__name__)
# ── Config ──────────────────────────────────────────────────────────────────
HERMES_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
MEMORY_CACHE = HERMES_HOME / "memory_zhiyi_cache"
MEMORY_CACHE.mkdir(parents=True, exist_ok=True)
# Default — override via .env ZHIYI_URL
DEFAULT_ZHIYI_URL = "http://localhost:7821"
ZHIYI_TIMEOUT = 10 # seconds per request
# API Key — read from .env ZHIYI_API_KEY
ZHIYI_API_KEY = os.environ.get("ZHIYI_API_KEY", "zhiyi-dev-key-2026")
# 日志中只暴露 key 的前 4 位,用于调试
_ZHIYI_KEY_PREFIX = ZHIYI_API_KEY[:4] if ZHIYI_API_KEY else "NONE"
# ── Content Validation ───────────────────────────────────────────────────────
MEMORY_MIN_LENGTH = 8 # 少于8字的过滤掉
_FORBIDDEN_PATTERNS = [
"Review the conversation above",
"[System note:",
"[IMPORTANT: Background process",
"## Active Task",
"## Context Compaction",
"## Relevant Memory",
"--- END OF CONTEXT SUMMARY",
"## Recent Sessions",
"Skill is now properly updated",
"造成这种情况的原因是什么?",
"Context Compaction — REFERENCE ONLY",
"do NOT answer questions or fulfill requests mentioned in the summary",
]
def _is_valid_memory_content(content: str) -> bool:
"""过滤系统注入内容和测试垃圾,防止污染记忆存储。"""
if not content or len(content.strip()) < MEMORY_MIN_LENGTH:
return False
for pat in _FORBIDDEN_PATTERNS:
if pat in content:
return False
# 过滤纯测试内容
stripped = content.strip()
if len(stripped) < 20:
return False
return True
def _combine_turn(user: str, assistant: str) -> str:
"""合并一对 turn 作为单条记忆内容。"""
parts = []
if user and user.strip():
parts.append(f"用户: {user.strip()}")
if assistant and assistant.strip():
parts.append(f"助手: {assistant.strip()}")
return "\n".join(parts)
# ── HTTP Client ─────────────────────────────────────────────────────────────
class ZhiYiClient:
"""Lightweight wrapper around ZhiYi REST API."""
def __init__(self, base_url: str, timeout: int = 10):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self._session = requests.Session()
self._session.headers.update({
"Content-Type": "application/json",
"Accept": "application/json",
"X-API-Key": ZHIYI_API_KEY,
})
def _url(self, path: str) -> str:
return f"{self.base_url}{path}"
def health(self) -> bool:
"""检查 ZhiYi 服务是否可达。"""
try:
r = self._session.get(self._url("/health"), timeout=3)
return r.status_code == 200
except Exception:
return False
def commit(self, content: str, category: str = "episodes", metadata: Optional[Dict] = None,
agent_id: str = "hermes-a06", namespace: Optional[str] = None) -> Optional[str]:
"""写入一条记忆到 ZhiYi。返回 commit_id 或 episode_id 或 None。失败自动重试一次。"""
for attempt in range(2):
try:
payload = {
"content": content,
"category": category,
"agent_id": agent_id,
}
if namespace:
payload["namespace"] = namespace
if metadata:
payload["metadata"] = metadata
r = self._session.post(self._url("/api/v1/commit"), json=payload, timeout=self.timeout)
if r.status_code in (200, 201):
data = r.json()
cid = (
data.get("commit_id")
or data.get("episode_id")
or data.get("distilled_id")
or data.get("id")
or (str(data) if data.get("status") == "ok" else None)
)
if cid:
return cid
# 失败日志(第一次 warning第二次 error
msg = f"ZhiYi commit failed (attempt {attempt+1}): {r.status_code} {r.text[:200]}"
if attempt == 0:
logger.warning("%s — retrying...", msg)
else:
logger.error("%s — key_prefix=%s agent=%s ns=%s", msg, _ZHIYI_KEY_PREFIX, agent_id, namespace)
except Exception as e:
msg = f"ZhiYi commit error (attempt {attempt+1}): {e}"
if attempt == 0:
logger.warning("%s — retrying...", msg)
else:
logger.error(msg)
return None
def recall(self, query: str, top_k: int = 5,
agent_id: str = "hermes-a06") -> List[Dict[str, Any]]:
"""语义搜索 ZhiYi 记忆。返回 [{content, score, category}, ...]。"""
try:
payload = {"query": query, "top_k": top_k, "use_rerank": True,
"agent_id": agent_id}
r = self._session.post(self._url("/api/v1/recall"), json=payload, timeout=self.timeout)
if r.status_code == 200:
data = r.json()
return data.get("results", [])
logger.warning("ZhiYi recall failed: %s %s", r.status_code, r.text[:200])
return []
except Exception as e:
logger.warning("ZhiYi recall error: %s", e)
return []
def stats(self) -> Dict[str, Any]:
"""获取 ZhiYi 统计信息。"""
try:
r = self._session.get(self._url("/api/v1/stats"), timeout=self.timeout)
if r.status_code == 200:
return r.json()
return {}
except Exception:
return {}
def search_notes(self, entity: str, max_hops: int = 2, max_notes: int = 5) -> List[Dict[str, Any]]:
"""图谱导航 + Obsidian 笔记关联搜索。"""
try:
params = {"entity": entity, "max_hops": max_hops, "max_notes": max_notes}
r = self._session.get(self._url("/api/v1/graph/notes"), params=params, timeout=self.timeout)
if r.status_code == 200:
data = r.json()
return data.get("notes", [])
return []
except Exception as e:
logger.warning("ZhiYi search_notes error: %s", e)
return []
# ── Memory Provider ──────────────────────────────────────────────────────────
class HermesZhiYiMemoryProvider(MemoryProvider):
"""ZhiYi MemoryWeave bridge for Hermes Agent.
Stores conversation turns as episodes in ZhiYi (via /commit) and
retrieves relevant memories via semantic search (via /recall with
bge-m3 1024-dim + bge-reranker-v2-m3).
"""
def __init__(self):
self._client: Optional[ZhiYiClient] = None
self._session_id: str = ""
self._platform: str = "cli"
self._turn_counter: int = 0
self._write_queue: List[Dict] = []
self._queue_lock = threading.Lock()
self._prefetch_cache: str = "" # last prefetch result
self._prefetch_lock = threading.RLock()
self._started: bool = False
self._ws_thread: Optional[threading.Thread] = None
self._ws_running: bool = False
@property
def name(self) -> str:
return "zhiyi"
# ── Availability ──────────────────────────────────────────────────────────
def is_available(self) -> bool:
"""检查 ZHIYI_URL 配置和服务连通性。"""
url = os.environ.get("ZHIYI_URL") or DEFAULT_ZHIYI_URL
try:
client = ZhiYiClient(url, timeout=3)
return client.health()
except Exception:
return False
# ── Lifecycle ─────────────────────────────────────────────────────────────
def initialize(self, session_id: str, **kwargs) -> None:
"""初始化 ZhiYi 客户端,注册当前 session。"""
self._session_id = session_id
self._platform = kwargs.get("platform", "cli")
url = os.environ.get("ZHIYI_URL") or DEFAULT_ZHIYI_URL
self._client = ZhiYiClient(url, timeout=ZHIYI_TIMEOUT)
self._started = True
self._turn_counter = 0
logger.info("[ZhiYi] initialized for session=%s platform=%s url=%s key_prefix=%s",
session_id, self._platform, url, _ZHIYI_KEY_PREFIX)
# 自检:验证 API Key 是否有效
try:
ok = self._client.health()
if ok:
logger.info("[ZhiYi] health check OK — key_prefix=%s", _ZHIYI_KEY_PREFIX)
else:
logger.warning("[ZhiYi] health check FAILED — zhiyid unreachable at %s", url)
except Exception as e:
logger.warning("[ZhiYi] health check error: %s", e)
# 启动 WebSocket 事件监听
self._start_ws_listener()
def _start_ws_listener(self) -> None:
"""启动 WebSocket 事件监听线程(连接织忆事件推送)。"""
if self._ws_running:
return
self._ws_running = True
self._ws_thread = threading.Thread(target=self._ws_listen, daemon=True, name="zhiyi-ws")
self._ws_thread.start()
logger.info("[ZhiYi] WS listener started")
def _ws_listen(self) -> None:
"""WebSocket 事件监听循环(自动重连)。"""
agent_id = os.environ.get("ZHIYI_AGENT_ID", "hermes-a06")
ws_url = (os.environ.get("ZHIYI_URL") or DEFAULT_ZHIYI_URL).replace("http://", "ws://").replace("https://", "wss://")
ws_url = f"{ws_url}/api/v1/ws/{agent_id}"
def on_message(ws_app, message):
try:
data = json.loads(message)
event_type = data.get("event", data.get("type", "unknown"))
logger.info("[ZhiYi] WS event: %s", event_type)
# prefetch.push 事件更新预取缓存
if event_type == "prefetch.push":
memories = data.get("memories", [])
if memories:
prefetch_text = "\n[ZhiYi Prefetch — 预取推送]\n"
for m in memories:
prefetch_text += f" [{m.get('score', 0):.2f}] {m.get('content', '')[:200]}\n"
with self._prefetch_lock:
self._prefetch_cache = prefetch_text
except json.JSONDecodeError:
pass
def on_error(ws_app, error):
logger.warning("[ZhiYi] WS error: %s", error)
def on_close(ws_app, close_status_code, close_msg):
logger.info("[ZhiYi] WS closed: %s %s", close_status_code, close_msg)
def on_open(ws_app):
logger.info("[ZhiYi] WS connected to %s", ws_url)
while self._ws_running:
try:
ws_app = websocket.WebSocketApp(
ws_url,
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close,
header={"X-API-Key": ZHIYI_API_KEY},
)
ws_app.run_forever(ping_interval=30, ping_timeout=10)
except Exception as e:
logger.warning("[ZhiYi] WS exception: %s", e)
# 重连延迟
for _ in range(30):
if not self._ws_running:
break
time.sleep(1)
def shutdown(self) -> None:
"""flush pending writes before exit."""
self._flush_queue()
self._ws_running = False
self._started = False
logger.info("[ZhiYi] shutdown")
# ── Write path ───────────────────────────────────────────────────────────
def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
"""每轮对话结束后调用:将 turn 写入队列,异步 commit 到 ZhiYi。"""
if not self._started or not self._client:
return
combined = _combine_turn(user_content, assistant_content)
if not _is_valid_memory_content(combined):
return
self._turn_counter += 1
entry = {
"content": combined,
"category": "episodes",
"metadata": {
"session_id": session_id or self._session_id,
"turn": self._turn_counter,
"platform": self._platform,
},
}
with self._queue_lock:
self._write_queue.append(entry)
# 队列超过一定量就 flush避免积压
if len(self._write_queue) >= 3:
self._flush_queue()
def _flush_queue(self) -> None:
"""将队列中的记忆批量写入 ZhiYi。"""
with self._queue_lock:
if not self._write_queue:
return
entries = self._write_queue[:]
self._write_queue.clear()
# 从 metadata 中读取 agent_idsync_turn 写入时携带),否则用默认值
for entry in entries:
meta = entry.get("metadata", {}) or {}
self._client.commit(
content=entry["content"],
category=entry.get("category", "episodes"),
metadata=meta,
agent_id=meta.get("agent_id", "hermes-a06"),
namespace=meta.get("namespace"),
)
# ── Read path ────────────────────────────────────────────────────────────
def prefetch(self, query: str, *, session_id: str = "") -> str:
"""每次 API 调用前触发:执行语义搜索 + 图谱导航 Obsidian 笔记,返回最相关记忆。"""
if not self._client or not query or len(query.strip()) < 2:
return ""
blocks = ["[ZhiYi Memory — relevant past context]"]
# 语义搜索
results = self._client.recall(query.strip(), top_k=3,
agent_id=os.environ.get("ZHIYI_AGENT_ID", "hermes-a06"))
for r in results:
score = r.get("score", 0)
content = r.get("content", "") or r.get("text", "")
cat = r.get("category", "")
if content:
blocks.append(f" [{score:.2f}][{cat}] {content[:500]}")
# 图谱导航 + Obsidian 笔记(从 query 提取关键词作为实体)
notes = self._client.search_notes(query.strip(), max_hops=2, max_notes=3)
if notes:
blocks.append("\n[ZhiYi Graph — related Obsidian notes]")
for n in notes:
title = n.get("title", "无标题")
path = n.get("path", "")
snippet = n.get("snippet", "")[:200]
score = n.get("score", 0)
entities = ", ".join(n.get("entities", []))
blocks.append(f" [{score:.0f}] {title} ({path})")
blocks.append(f" \"{snippet}\"")
if entities:
blocks.append(f" via entities: {entities}")
text = "\n".join(blocks)
with self._prefetch_lock:
self._prefetch_cache = text
return text
def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
"""空实现 — prefetch 已是同步的,不需要额外的异步队列。"""
pass
# ── Tool interface ────────────────────────────────────────────────────────
def get_tool_schemas(self) -> List[Dict[str, Any]]:
return [
{
"name": "memory_search",
"description": "Search ZhiYi semantic memory for relevant past information. Use for: user preferences, earlier decisions, established facts, project context.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Semantic search query describing what to find. Be specific — the query is encoded with bge-m3 1024-dim and matched against all memories.",
},
"top_k": {
"type": "integer",
"description": "Maximum number of results to return (default: 5, max: 20).",
"default": 5,
},
},
"required": ["query"],
},
},
{
"name": "memory_write",
"description": "Explicitly save an important fact or preference to ZhiYi memory. Use when: user tells you something important, you discover a key fact, user confirms a decision.",
"parameters": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The memory content to save. Should be a complete, meaningful statement. Be specific — include context.",
},
"category": {
"type": "string",
"description": "Category for this memory: 'distilled' (important facts), 'episodes' (conversation turns). Default: 'distilled'.",
"default": "distilled",
},
},
"required": ["content"],
},
},
{
"name": "memory_stats",
"description": "Get ZhiYi memory statistics: total documents, vector dimensions, index status.",
"parameters": {
"type": "object",
"properties": {},
},
},
]
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
if tool_name == "memory_search":
return self._tool_memory_search(args.get("query", ""), args.get("top_k", 5))
elif tool_name == "memory_write":
return self._tool_memory_write(args.get("content", ""), args.get("category", "distilled"))
elif tool_name == "memory_stats":
return self._tool_memory_stats()
return tool_error(tool_name, "Unknown tool")
# ── Tool handlers ─────────────────────────────────────────────────────────
def _tool_memory_search(self, query: str, top_k: int) -> str:
if not self._client:
return json.dumps({"success": False, "error": "ZhiYi client not initialized"})
if top_k > 20:
top_k = 20
results = self._client.recall(query, top_k=top_k)
if not results:
return json.dumps({"success": True, "results": [], "message": "No relevant memories found"})
formatted = []
for r in results:
content = r.get("content", "") or r.get("text", "")
formatted.append({
"content": content[:1000], # 截断超长内容
"score": round(r.get("score", 0), 4),
"category": r.get("category", ""),
})
return json.dumps({"success": True, "count": len(formatted), "results": formatted})
def _tool_memory_write(self, content: str, category: str) -> str:
if not self._client:
return json.dumps({"success": False, "error": "ZhiYi client not initialized"})
if not _is_valid_memory_content(content):
return json.dumps({"success": False, "error": "Content too short or invalid"})
commit_id = self._client.commit(
content=content, category=category,
agent_id=os.environ.get("ZHIYI_AGENT_ID", "hermes-a06"),
)
if commit_id:
return json.dumps({"success": True, "commit_id": commit_id})
return json.dumps({"success": False, "error": "Commit failed — check ZhiYi server logs"})
def _tool_memory_stats(self) -> str:
if not self._client:
return json.dumps({"success": False, "error": "ZhiYi client not initialized"})
stats = self._client.stats()
if not stats:
return json.dumps({"success": False, "error": "Could not reach ZhiYi /stats endpoint"})
return json.dumps({"success": True, "stats": stats})
# ── Session management ────────────────────────────────────────────────────
def on_session_switch(self, new_session_id: str, *, parent_session_id: str = "", reset: bool = False, **kwargs) -> None:
self._session_id = new_session_id
self._turn_counter = 0
if reset:
self._flush_queue()
def on_turn_start(self, turn_number: int, message: str, **kwargs) -> None:
"""每轮开始时 flush 待写入的记忆,保证顺序。"""
self._flush_queue()
# ── System prompt ────────────────────────────────────────────────────────
def system_prompt_block(self) -> str:
return (
"\n[ZhiYi Memory] You have access to ZhiYi MemoryWeave — a semantic memory system with knowledge graph.\n"
"Your prefetch automatically retrieves: (1) relevant semantic memories, (2) Obsidian notes related via graph navigation.\n"
"Use memory_search to recall past facts, decisions, and context before answering.\n"
"Use memory_write to save important information the user tells you.\n"
)
# ── Plugin entry point ──────────────────────────────────────────────────────
def register(ctx) -> None:
"""Called by Hermes plugin system to register this memory provider."""
ctx.register_memory_provider(HermesZhiYiMemoryProvider())

View File

@ -0,0 +1,6 @@
name: hermes-zhiyi
version: 1.0.0
description: Bridge to ZhiYi MemoryWeave — bge-m3 1024-dim semantic memory with FAISS + bge-reranker-v2-m3. Replaces local lanceDB with shared ZhiYi server.
provider: zhiyi
entry: __init__.register
memory_provider: true

View File

@ -0,0 +1,30 @@
# memory-zhiyi — OpenClaw ZhiYi Memory Plugin
ZhiYi MemoryWeave plugin for OpenClaw. Replaces `memory-lancedb-pro` with semantic memory + knowledge graph + Obsidian integration.
## Quick Start
```bash
# Install
cd ~/.openclaw/workspace/plugins/
git clone <this-repo> memory-zhiyi
cd memory-zhiyi
npm install
npm run build
# Configure in ~/.openclaw/config.yaml:
memory:
provider: zhiyi
zhiyi:
base_url: http://localhost:7821
api_key: your-key
namespace: openclaw-main
```
## Architecture
- `src/client.ts` — ZhiYi REST API wrapper
- `src/index.ts` — OpenClaw plugin entry (register hooks)
- `src/cli.ts` — LanceDB → ZhiYi migration CLI
- `src/types.ts` — TypeScript interfaces

11
plugins/openclaw-zhiyi/dist/cli.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env node
/**
* CLI: migrate-lancedb-to-zhiyi
*
* Reads OpenClaw LanceDB data and commits to ZhiYi openclaw-main namespace.
*
* Usage:
* npx ts-node src/cli.ts
* ZHIYI_BASE_URL=http://localhost:7821 ZHIYI_API_KEY=xxx npx ts-node src/cli.ts
*/
export {};

77
plugins/openclaw-zhiyi/dist/cli.js vendored Normal file
View File

@ -0,0 +1,77 @@
#!/usr/bin/env node
"use strict";
/**
* CLI: migrate-lancedb-to-zhiyi
*
* Reads OpenClaw LanceDB data and commits to ZhiYi openclaw-main namespace.
*
* Usage:
* npx ts-node src/cli.ts
* ZHIYI_BASE_URL=http://localhost:7821 ZHIYI_API_KEY=xxx npx ts-node src/cli.ts
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("./client");
const path = __importStar(require("path"));
const BASE_URL = process.env.ZHIYI_BASE_URL || 'http://localhost:7821';
const API_KEY = process.env.ZHIYI_API_KEY || 'zhiyi-dev-key-2026';
const NAMESPACE = process.env.OC_NAMESPACE || 'openclaw-main';
const LANCEDB_PATH = path.join(process.env.HOME || '/home/muc', '.openclaw/memory-lancedb');
async function main() {
console.log(`[migrate] ZhiYi: ${BASE_URL}`);
console.log(`[migrate] Namespace: ${NAMESPACE}`);
console.log(`[migrate] LanceDB path: ${LANCEDB_PATH}`);
const client = new client_1.ZhiYiClient({ baseUrl: BASE_URL, apiKey: API_KEY, namespace: NAMESPACE });
const ok = await client.health();
if (!ok) {
console.error('[migrate] ERROR: ZhiYi unreachable');
process.exit(1);
}
// TODO: OpenClaw LanceDB is a separate store
// For now, guide the user:
console.log('[migrate]');
console.log('[migrate] LanceDB must be read from OpenClaw workspace.');
console.log('[migrate] The LanceDB data lives at: ~/.openclaw/memory-lancedb/');
console.log('[migrate] This migration requires OpenClaw to be running.');
console.log('[migrate]');
console.log('[migrate] Manual migration steps:');
console.log('[migrate] 1. OpenClaw LanceDB stores episodes in ~/.openclaw/memory-lancedb/');
console.log('[migrate] 2. Export: use OpenClaw CLI or read LanceDB directly');
console.log('[migrate] 3. Import: call client.batchCommit() with exported data');
console.log('[migrate] 4. Switch provider: set memory.provider: zhiyi in OpenClaw config');
console.log('[migrate]');
console.log('[migrate] TODO: implement LanceDB reader once OpenClaw config is available');
}
main().catch(console.error);

32
plugins/openclaw-zhiyi/dist/client.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
import type { ZhiYiMemoryConfig, RecallResult, CommitResult, SearchNotesResult, GraphPath, StatsResult, FeedbackResult } from './types';
export declare class ZhiYiClient {
private client;
private ns;
private agentId;
private config;
/** LRU prefetch cache: session key → results */
private cache;
private readonly MAX_CACHE;
constructor(config: ZhiYiMemoryConfig);
health(): Promise<boolean>;
commit(content: string, category?: string, metadata?: Record<string, unknown>): Promise<CommitResult | null>;
batchCommit(items: Array<{
content: string;
category?: string;
metadata?: Record<string, unknown>;
}>, concurrency?: number): Promise<{
ok: number;
fail: number;
errors: string[];
}>;
recall(query: string, topK?: number): Promise<RecallResult[]>;
prefetch(context?: string): Promise<RecallResult[]>;
clearCache(): void;
markUseful(memoryId: string): Promise<FeedbackResult>;
markNotUseful(memoryId: string, reason?: string): Promise<FeedbackResult>;
forget(memoryId: string): Promise<FeedbackResult>;
searchNotes(entity: string, maxHops?: number, maxNotes?: number): Promise<SearchNotesResult[]>;
navigate(entity: string, maxHops?: number): Promise<GraphPath[]>;
stats(): Promise<StatsResult>;
private _log;
}

215
plugins/openclaw-zhiyi/dist/client.js vendored Normal file
View File

@ -0,0 +1,215 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZhiYiClient = void 0;
const axios_1 = __importDefault(require("axios"));
class ZhiYiClient {
client;
ns;
agentId;
config;
/** LRU prefetch cache: session key → results */
cache = new Map();
MAX_CACHE = 10;
constructor(config) {
this.config = config;
this.ns = config.namespace || 'openclaw-main';
this.agentId = config.agentId || 'openclaw';
this.client = axios_1.default.create({
baseURL: config.baseUrl,
timeout: config.timeout || 10000,
headers: {
'Content-Type': 'application/json',
'X-API-Key': config.apiKey,
},
});
}
// ─── Health ───────────────────────────────────────────────
async health() {
try {
const r = await this.client.get('/health');
return r.status === 200;
}
catch {
return false;
}
}
// ─── Commit ───────────────────────────────────────────────
async commit(content, category = 'episodes', metadata) {
try {
const payload = {
content,
category: category || 'episodes',
namespace: this.ns,
agent_id: this.agentId,
};
// Merge metadata into top-level (don't nest)
if (metadata && typeof metadata === 'object') {
for (const [k, v] of Object.entries(metadata)) {
if (!['content', 'category', 'namespace', 'agent_id'].includes(k)) {
payload[k] = v;
}
}
}
const r = await this.client.post('/api/v1/commit', payload);
if (r.status === 200 || r.status === 201) {
return r.data;
}
return null;
}
catch (e) {
this._log('commit', e);
return { error: e.message };
}
}
// ─── Batch commit with dedup ──────────────────────────────
async batchCommit(items, concurrency = 3) {
const errors = [];
let ok = 0;
// Simple concurrency limiter
for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency);
const results = await Promise.all(batch.map(item => this.commit(item.content, item.category, item.metadata)));
for (const r of results) {
if (r && !r.error)
ok++;
else
errors.push(r?.error || 'unknown');
}
// Rate limit: 10 req/s, 3 at a time = ~300ms between batches
if (i + concurrency < items.length) {
await new Promise(r => setTimeout(r, 300));
}
}
return { ok, fail: errors.length, errors };
}
// ─── Recall ───────────────────────────────────────────────
async recall(query, topK = 5) {
const cacheKey = `${query}:${topK}`;
const cached = this.cache.get(cacheKey);
if (cached)
return cached;
try {
const r = await this.client.post('/api/v1/recall', {
query,
top_k: topK,
namespace: this.ns,
use_rerank: true,
});
const results = (r.data.results || []).map((x) => ({
id: x.id || '',
content: x.content || x.text || '',
score: x.score || 0,
category: x.category || '',
}));
// Cache with LRU eviction
if (this.cache.size >= this.MAX_CACHE) {
const firstKey = this.cache.keys().next().value;
if (firstKey)
this.cache.delete(firstKey);
}
this.cache.set(cacheKey, results);
return results;
}
catch (e) {
this._log('recall', e);
return [];
}
}
// ─── Prefetch (lightweight recall for context) ────────────
async prefetch(context) {
if (!context || context.length < 5)
return [];
return await this.recall(context, 3);
}
clearCache() {
this.cache.clear();
}
// ─── Feedback ─────────────────────────────────────────────
async markUseful(memoryId) {
try {
const r = await this.client.post('/api/v1/feedback/useful', {
memory_id: memoryId,
agent_id: this.agentId,
});
return { success: r.status === 200 };
}
catch (e) {
this._log('markUseful', e);
return { success: false, error: e.message };
}
}
async markNotUseful(memoryId, reason) {
try {
const r = await this.client.post('/api/v1/feedback/not-useful', {
memory_id: memoryId,
agent_id: this.agentId,
reason: reason || '',
});
return { success: r.status === 200 };
}
catch (e) {
this._log('markNotUseful', e);
return { success: false, error: e.message };
}
}
// ─── Forget (soft-delete) ─────────────────────────────────
async forget(memoryId) {
try {
const r = await this.client.post('/api/v1/admin/forget', {
memory_id: memoryId,
agent_id: this.agentId,
});
return { success: r.status === 200 };
}
catch (e) {
this._log('forget', e);
return { success: false, error: e.message };
}
}
// ─── Graph ────────────────────────────────────────────────
async searchNotes(entity, maxHops = 2, maxNotes = 5) {
try {
const r = await this.client.get('/api/v1/graph/notes', {
params: { entity, max_hops: maxHops, max_notes: maxNotes },
});
return r.data.notes || [];
}
catch (e) {
this._log('searchNotes', e);
return [];
}
}
async navigate(entity, maxHops = 2) {
try {
const r = await this.client.post('/api/v1/graph/navigate', {
entity,
max_hops: maxHops,
namespace: this.ns,
});
return r.data.paths || [];
}
catch (e) {
this._log('navigate', e);
return [];
}
}
// ─── Stats ────────────────────────────────────────────────
async stats() {
try {
const r = await this.client.get('/api/v1/stats');
return r.data;
}
catch {
return {};
}
}
// ─── Internal ─────────────────────────────────────────────
_log(op, e) {
const msg = e?.response?.data?.error || e?.message || String(e);
console.error(`[ZhiYi] ${op}: ${msg}`);
}
}
exports.ZhiYiClient = ZhiYiClient;

32
plugins/openclaw-zhiyi/dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
/**
* memory-zhiyi ZhiYi MemoryWeave Plugin for OpenClaw
*
* v0.2.0 improvements:
* - Fixed commit payload (agent_id + namespace as top-level fields)
* - Added feedback (useful/not-useful) for self-optimization engine
* - Added prefetch (auto recall context before turns)
* - Added forget (soft-delete via /api/v1/admin/forget)
* - Added dedup (check existing before commit)
* - Added stats endpoint
* - Better error logging + retry for 429 rate limits
*/
import { ZhiYiClient } from './client';
import type { ZhiYiMemoryConfig } from './types';
export declare const pluginManifest: {
id: string;
name: string;
description: string;
kind: "memory";
version: string;
};
declare const plugin: {
id: string;
name: string;
description: string;
kind: "memory";
register(api: any): void;
getClient(): ZhiYiClient | null;
getConfig(): ZhiYiMemoryConfig | null;
};
export default plugin;
export { ZhiYiClient };

205
plugins/openclaw-zhiyi/dist/index.js vendored Normal file
View File

@ -0,0 +1,205 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZhiYiClient = exports.pluginManifest = void 0;
/**
* memory-zhiyi ZhiYi MemoryWeave Plugin for OpenClaw
*
* v0.2.0 improvements:
* - Fixed commit payload (agent_id + namespace as top-level fields)
* - Added feedback (useful/not-useful) for self-optimization engine
* - Added prefetch (auto recall context before turns)
* - Added forget (soft-delete via /api/v1/admin/forget)
* - Added dedup (check existing before commit)
* - Added stats endpoint
* - Better error logging + retry for 429 rate limits
*/
const client_1 = require("./client");
Object.defineProperty(exports, "ZhiYiClient", { enumerable: true, get: function () { return client_1.ZhiYiClient; } });
exports.pluginManifest = {
id: 'memory-zhiyi',
name: 'Memory (ZhiYi)',
description: 'ZhiYi MemoryWeave — semantic memory with knowledge graph, feedback loop, and prefetch',
kind: 'memory',
version: '0.2.0',
};
let _client = null;
let _config = null;
function loadConfig(api) {
const raw = api.config?.zhiyi || api.config?.memory_zhiyi || {};
return {
baseUrl: raw.base_url || process.env.ZHIYI_BASE_URL || 'http://localhost:7821',
apiKey: raw.api_key || process.env.ZHIYI_API_KEY || 'zhiyi-dev-key-2026',
namespace: raw.namespace || 'openclaw-main',
agentId: raw.agent_id || 'openclaw',
timeout: raw.timeout || 10000,
prefetchEnabled: raw.prefetch_enabled !== false,
};
}
const plugin = {
id: exports.pluginManifest.id,
name: exports.pluginManifest.name,
description: exports.pluginManifest.description,
kind: exports.pluginManifest.kind,
register(api) {
_config = loadConfig(api);
_client = new client_1.ZhiYiClient(_config);
// ── Health check via service start (async, doesn't block register) ─
api.registerService({
id: 'memory-zhiyi',
start: async () => {
const ok = await _client.health();
if (!ok) {
api.logger?.error('[memory-zhiyi] ZhiYi unreachable — check ZHIYI_BASE_URL');
}
else {
api.logger?.info(`[memory-zhiyi] v0.2.0 connected — ns=${_config.namespace}`);
}
},
stop: () => {
_client?.clearCache();
api.logger?.info('[memory-zhiyi] stopped');
},
});
api.logger?.info(`[memory-zhiyi] v0.2.0 registered — ns=${_config.namespace} agent=${_config.agentId}`);
// ── Hooks ─────────────────────────────────────────────
// memory:recall — semantic search
api.on('memory:recall', async (params) => {
if (!_client)
return [];
return await _client.recall(params.query, params.topK ?? 5);
});
// memory:commit — store new memory
api.on('memory:commit', async (params) => {
if (!_client)
return null;
const result = await _client.commit(params.content, params.category ?? 'episodes', params.metadata);
return result;
});
// memory:prefetch — lightweight recall before conversation turns
api.on('memory:prefetch', async (params) => {
if (!_client || !_config?.prefetchEnabled)
return [];
return await _client.prefetch(params.context);
});
// memory:feedback:useful — mark memory as useful
api.on('memory:feedback:useful', async (params) => {
if (!_client)
return { success: false };
return await _client.markUseful(params.memoryId);
});
// memory:feedback:not-useful — mark memory as not useful
api.on('memory:feedback:not-useful', async (params) => {
if (!_client)
return { success: false };
return await _client.markNotUseful(params.memoryId, params.reason);
});
// memory:forget — soft-delete a memory
api.on('memory:forget', async (params) => {
if (!_client)
return { success: false };
return await _client.forget(params.memoryId);
});
// memory:graphNavigate — knowledge graph traversal
api.on('memory:graphNavigate', async (params) => {
if (!_client)
return [];
return await _client.navigate(params.entity, params.maxHops ?? 2);
});
// memory:searchNotes — Obsidian notes via graph entities
api.on('memory:searchNotes', async (params) => {
if (!_client)
return [];
return await _client.searchNotes(params.entity, 2, params.maxNotes ?? 5);
});
// memory:stats — memory system stats
api.on('memory:stats', async () => {
if (!_client)
return {};
return await _client.stats();
});
// ── Tools for OpenClaw ────────────────────────────────
api.registerTool?.('memory_recall', {
description: 'Search memories via ZhiYi semantic recall',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
topK: { type: 'number', default: 5, description: 'Max results' },
},
required: ['query'],
},
handler: async (args) => {
if (!_client)
return [];
return await _client.recall(args.query, args.topK ?? 5);
},
});
api.registerTool?.('memory_store', {
description: 'Store a new memory in ZhiYi',
parameters: {
type: 'object',
properties: {
content: { type: 'string', description: 'Memory content' },
category: { type: 'string', default: 'episodes', description: 'Memory category' },
},
required: ['content'],
},
handler: async (args) => {
if (!_client)
return null;
return await _client.commit(args.content, args.category ?? 'episodes');
},
});
api.registerTool?.('memory_forget', {
description: 'Soft-delete a memory by ID',
parameters: {
type: 'object',
properties: {
memoryId: { type: 'string', description: 'Memory ID to forget' },
},
required: ['memoryId'],
},
handler: async (args) => {
if (!_client)
return { success: false };
return await _client.forget(args.memoryId);
},
});
api.registerTool?.('memory_feedback', {
description: 'Mark a memory as useful or not-useful (activates self-optimization)',
parameters: {
type: 'object',
properties: {
memoryId: { type: 'string', description: 'Memory ID' },
useful: { type: 'boolean', description: 'True = useful, False = not useful' },
reason: { type: 'string', description: 'Reason for not-useful (optional)' },
},
required: ['memoryId', 'useful'],
},
handler: async (args) => {
if (!_client)
return { success: false };
if (args.useful) {
return await _client.markUseful(args.memoryId);
}
return await _client.markNotUseful(args.memoryId, args.reason);
},
});
api.registerTool?.('memory_stats', {
description: 'Get ZhiYi memory system statistics',
parameters: {
type: 'object',
properties: {},
},
handler: async () => {
if (!_client)
return {};
return await _client.stats();
},
});
api.logger?.info('[memory-zhiyi] v0.2.0 all hooks + tools registered');
},
getClient() { return _client; },
getConfig() { return _config; },
};
exports.default = plugin;

43
plugins/openclaw-zhiyi/dist/types.d.ts vendored Normal file
View File

@ -0,0 +1,43 @@
export interface ZhiYiMemoryConfig {
baseUrl: string;
apiKey: string;
namespace: string;
agentId?: string;
timeout?: number;
prefetchEnabled?: boolean;
}
export interface RecallResult {
id: string;
content: string;
score: number;
category: string;
}
export interface CommitResult {
episode_id?: string;
memory_ids?: string[];
id?: string;
error?: string;
}
export interface SearchNotesResult {
path: string;
title: string;
snippet: string;
entities: string[];
score: number;
}
export interface GraphPath {
source: string;
target: string;
relation: string;
weight: number;
}
export interface StatsResult {
total_memories?: number;
total_episodes?: number;
backend?: string;
[key: string]: unknown;
}
export interface FeedbackResult {
success: boolean;
error?: string;
}

2
plugins/openclaw-zhiyi/dist/types.js vendored Normal file
View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@ -0,0 +1,61 @@
{
"id": "memory-zhiyi",
"name": "Memory ZhiYi",
"description": "ZhiYi MemoryWeave plugin for OpenClaw \u2014 semantic memory with knowledge graph",
"activation": {
"onStartup": false,
"onCommands": [
"zhiyi-mem"
]
},
"kind": "memory",
"contracts": {
"tools": [
"memory_forget",
"memory_recall",
"memory_store"
]
},
"uiHints": {
"base_url": {
"label": "ZhiYi API Base URL",
"placeholder": "http://localhost:7821",
"help": "ZhiYi MemoryWeave API endpoint"
},
"api_key": {
"label": "ZhiYi API Key",
"sensitive": true,
"placeholder": "your-api-key",
"help": "API key for ZhiYi MemoryWeave"
},
"namespace": {
"label": "Namespace",
"placeholder": "openclaw-main",
"help": "Agent namespace in ZhiYi"
},
"agent_id": {
"label": "Agent ID",
"placeholder": "openclaw",
"help": "Agent identifier in ZhiYi"
}
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"base_url": {
"type": "string"
},
"api_key": {
"type": "string"
},
"namespace": {
"type": "string"
},
"agent_id": {
"type": "string"
}
}
},
"version": "0.2.0"
}

381
plugins/openclaw-zhiyi/package-lock.json generated Normal file
View File

@ -0,0 +1,381 @@
{
"name": "memory-zhiyi",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "memory-zhiyi",
"version": "0.1.0",
"dependencies": {
"axios": "^1.6.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.3.0"
}
},
"node_modules/@types/node": {
"version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/follow-redirects": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@ -0,0 +1,30 @@
{
"name": "memory-zhiyi",
"version": "0.1.0",
"description": "ZhiYi MemoryWeave plugin for OpenClaw — semantic memory with knowledge graph",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "tsc",
"test": "echo no tests yet"
},
"dependencies": {
"axios": "^1.6.0"
},
"devDependencies": {
"typescript": "^5.3.0",
"@types/node": "^20.0.0"
},
"openclaw": {
"extensions": [
"./dist/index.js"
],
"compat": {
"pluginApi": ">=2026.5.27"
}
},
"peerDependencies": {
"openclaw": ">=2026.5.27"
}
}

View File

@ -0,0 +1,47 @@
#!/usr/bin/env node
/**
* CLI: migrate-lancedb-to-zhiyi
*
* Reads OpenClaw LanceDB data and commits to ZhiYi openclaw-main namespace.
*
* Usage:
* npx ts-node src/cli.ts
* ZHIYI_BASE_URL=http://localhost:7821 ZHIYI_API_KEY=xxx npx ts-node src/cli.ts
*/
import { ZhiYiClient } from './client';
import * as path from 'path';
import * as fs from 'fs';
const BASE_URL = process.env.ZHIYI_BASE_URL || 'http://localhost:7821';
const API_KEY = process.env.ZHIYI_API_KEY || 'zhiyi-dev-key-2026';
const NAMESPACE = process.env.OC_NAMESPACE || 'openclaw-main';
const LANCEDB_PATH = path.join(process.env.HOME || '/home/muc', '.openclaw/memory-lancedb');
async function main() {
console.log(`[migrate] ZhiYi: ${BASE_URL}`);
console.log(`[migrate] Namespace: ${NAMESPACE}`);
console.log(`[migrate] LanceDB path: ${LANCEDB_PATH}`);
const client = new ZhiYiClient({ baseUrl: BASE_URL, apiKey: API_KEY, namespace: NAMESPACE });
const ok = await client.health();
if (!ok) { console.error('[migrate] ERROR: ZhiYi unreachable'); process.exit(1); }
// TODO: OpenClaw LanceDB is a separate store
// For now, guide the user:
console.log('[migrate]');
console.log('[migrate] LanceDB must be read from OpenClaw workspace.');
console.log('[migrate] The LanceDB data lives at: ~/.openclaw/memory-lancedb/');
console.log('[migrate] This migration requires OpenClaw to be running.');
console.log('[migrate]');
console.log('[migrate] Manual migration steps:');
console.log('[migrate] 1. OpenClaw LanceDB stores episodes in ~/.openclaw/memory-lancedb/');
console.log('[migrate] 2. Export: use OpenClaw CLI or read LanceDB directly');
console.log('[migrate] 3. Import: call client.batchCommit() with exported data');
console.log('[migrate] 4. Switch provider: set memory.provider: zhiyi in OpenClaw config');
console.log('[migrate]');
console.log('[migrate] TODO: implement LanceDB reader once OpenClaw config is available');
}
main().catch(console.error);

View File

@ -0,0 +1,73 @@
import axios, { AxiosInstance } from 'axios';
import type { ZhiYiMemoryConfig, RecallResult, SearchNotesResult, GraphPath } from './types';
export class ZhiYiClient {
private client: AxiosInstance;
private ns: string;
constructor(config: ZhiYiMemoryConfig) {
this.ns = config.namespace || 'openclaw-main';
this.client = axios.create({
baseURL: config.baseUrl,
timeout: config.timeout || 10000,
headers: {
'Content-Type': 'application/json',
'X-API-Key': config.apiKey,
},
});
}
async health(): Promise<boolean> {
try {
const r = await this.client.get('/health');
return r.status === 200;
} catch { return false; }
}
async commit(content: string, category = 'episodes', metadata?: Record<string, unknown>): Promise<string | null> {
try {
const r = await this.client.post('/api/v1/commit', {
content, category, metadata: { ...metadata, namespace: this.ns },
});
const d = r.data;
return d.commit_id || d.episode_id || d.distilled_id || d.id || null;
} catch (e: any) { console.error('[ZhiYi] commit:', e.message); return null; }
}
async batchCommit(items: Array<{ content: string; category?: string; metadata?: Record<string, unknown> }>): Promise<string[]> {
const ids: string[] = [];
for (const item of items) {
const id = await this.commit(item.content, item.category || 'episodes', item.metadata);
if (id) ids.push(id);
}
return ids;
}
async recall(query: string, topK = 5): Promise<RecallResult[]> {
try {
const r = await this.client.post('/api/v1/recall', { query, top_k: topK, use_rerank: true });
return (r.data.results || []).map((x: any) => ({
id: x.id || '', content: x.content || x.text || '', score: x.score || 0, category: x.category || '',
}));
} catch (e: any) { console.error('[ZhiYi] recall:', e.message); return []; }
}
async searchNotes(entity: string, maxHops = 2, maxNotes = 5): Promise<SearchNotesResult[]> {
try {
const r = await this.client.get('/api/v1/graph/notes', { params: { entity, max_hops: maxHops, max_notes: maxNotes } });
return r.data.notes || [];
} catch (e: any) { console.error('[ZhiYi] searchNotes:', e.message); return []; }
}
async navigate(entity: string, maxHops = 2): Promise<GraphPath[]> {
try {
const r = await this.client.post('/api/v1/graph/navigate', { entity, max_hops: maxHops, namespace: this.ns });
return r.data.paths || [];
} catch (e: any) { console.error('[ZhiYi] navigate:', e.message); return []; }
}
async stats(): Promise<Record<string, unknown>> {
try { const r = await this.client.get('/api/v1/stats'); return r.data; }
catch { return {}; }
}
}

View File

@ -0,0 +1,75 @@
/**
* memory-zhiyi ZhiYi MemoryWeave Plugin for OpenClaw
*/
import { ZhiYiClient } from './client';
import type { ZhiYiMemoryConfig } from './types';
export const pluginManifest = {
id: 'memory-zhiyi',
name: 'Memory (ZhiYi)',
description: 'ZhiYi MemoryWeave — semantic memory with knowledge graph and Obsidian integration',
kind: 'memory' as const,
version: '0.1.0',
};
let _client: ZhiYiClient | null = null;
let _config: ZhiYiMemoryConfig | null = null;
function loadConfig(api: any): ZhiYiMemoryConfig {
const raw = api.config?.zhiyi || api.config?.memory_zhiyi || {};
return {
baseUrl: raw.base_url || process.env.ZHIYI_BASE_URL || 'http://localhost:7821',
apiKey: raw.api_key || process.env.ZHIYI_API_KEY || 'zhiyi-dev-key-2026',
namespace: raw.namespace || 'openclaw-main',
timeout: raw.timeout || 10000,
};
}
const plugin = {
id: pluginManifest.id,
name: pluginManifest.name,
description: pluginManifest.description,
kind: pluginManifest.kind,
async register(api: any) {
_config = loadConfig(api);
_client = new ZhiYiClient(_config);
const ok = await _client.health();
if (!ok) {
api.logger?.error('[memory-zhiyi] ZhiYi unreachable — check ZHIYI_BASE_URL');
return;
}
api.logger?.info(`[memory-zhiyi] registered — ns=${_config.namespace}`);
// Hook into OpenClaw memory lifecycle
api.on('memory:recall', async (params: { query: string; topK?: number }) => {
if (!_client) return [];
return await _client.recall(params.query, params.topK ?? 5);
});
api.on('memory:commit', async (params: { content: string; category?: string; metadata?: Record<string, unknown> }) => {
if (!_client) return null;
return await _client.commit(params.content, params.category ?? 'episodes', params.metadata);
});
api.on('memory:graphNavigate', async (params: { entity: string; maxHops?: number }) => {
if (!_client) return [];
return await _client.navigate(params.entity, params.maxHops ?? 2);
});
api.on('memory:searchNotes', async (params: { entity: string; maxNotes?: number }) => {
if (!_client) return [];
return await _client.searchNotes(params.entity, 2, params.maxNotes ?? 5);
});
api.logger?.info('[memory-zhiyi] all hooks registered');
},
getClient() { return _client; },
getConfig() { return _config; },
};
export default plugin;
export { ZhiYiClient };

View File

@ -0,0 +1,28 @@
export interface ZhiYiMemoryConfig {
baseUrl: string;
apiKey: string;
namespace: string;
timeout?: number;
}
export interface RecallResult {
id: string;
content: string;
score: number;
category: string;
}
export interface SearchNotesResult {
path: string;
title: string;
snippet: string;
entities: string[];
score: number;
}
export interface GraphPath {
source: string;
target: string;
relation: string;
weight: number;
}

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"moduleResolution": "node",
"ignoreDeprecations": "6.0",
"outDir": "dist-cjs",
"rootDir": "src",
"declaration": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*"]
}

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}