@seemem/see-mem-openclaw-plugin 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # See-Mem OpenClaw
2
+
3
+ ## 概述
4
+
5
+ 本文档指导你如何为 OpenClaw 接入 See-Mem 记忆系统插件。
package/index.js ADDED
@@ -0,0 +1,261 @@
1
+ import { addMessage, searchMemory, formatPromptBlock, buildConfig } from "./lib/see-mem-api.js";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const logsDir = path.join(__dirname, "logs");
9
+
10
+ /**
11
+ * 从 content 中提取附件信息
12
+ * 支持格式: [media attached: /path/to/file.jpg (image/jpeg) | /path/to/file.jpg]
13
+ */
14
+ function extractAttachments(content) {
15
+ if (!content) return [];
16
+
17
+ const textContent = typeof content === "string" ? content : "";
18
+ const attachments = [];
19
+
20
+ // 匹配 [media attached: 文件路径 (MIME类型) | 文件路径]
21
+ const regex = /\[media attached: ([^\s]+) \(([^)]+)\)/g;
22
+ let match;
23
+
24
+
25
+ while ((match = regex.exec(textContent)) !== null) {
26
+ attachments.push({
27
+ filePath: match[1],
28
+ mimeType: match[2],
29
+ });
30
+ }
31
+
32
+ return attachments;
33
+ }
34
+
35
+ /**
36
+ * 从 content 数组中提取 base64 图片数据
37
+ */
38
+ function extractBase64Images(content) {
39
+ if (!Array.isArray(content)) return [];
40
+
41
+ const images = [];
42
+ for (const part of content) {
43
+ console.log("extractBase64Images part", part)
44
+ if (part?.type === "image" && part?.data) {
45
+ images.push({
46
+ data: part.data,
47
+ mimeType: part.mimeType || "image/jpeg",
48
+ });
49
+ }
50
+ }
51
+
52
+ return images;
53
+ }
54
+
55
+ /**
56
+ * 将本地文件路径转换为 file:// URL
57
+ * 注意: 这只是本地文件路径的 URL 形式,能否访问取决于服务端是否允许
58
+ */
59
+ function filePathToUrl(filePath) {
60
+ // 替换空格等特殊字符
61
+ const encodedPath = encodeURIComponent(filePath);
62
+ return `file://${encodedPath}`;
63
+ }
64
+
65
+ function resolveSessionId(cfg, ctx) {
66
+ if (cfg.sessionId) return cfg.sessionId;
67
+ return ctx?.sessionKey || ctx?.sessionId || `openclaw-${Date.now()}`;
68
+ }
69
+
70
+ function buildSearchPayload(cfg, prompt, ctx) {
71
+ return {
72
+ user_id: cfg.userId,
73
+ query: prompt,
74
+ session_id: resolveSessionId(cfg, ctx),
75
+ };
76
+ }
77
+
78
+ function buildAddMessagePayload(cfg, messages, ctx, startTime) {
79
+ return {
80
+ user_id: cfg.userId,
81
+ messages,
82
+ start_time: startTime,
83
+ session_id: resolveSessionId(cfg, ctx),
84
+ };
85
+ }
86
+
87
+ function extractText(content) {
88
+ if (!content) return "";
89
+ if (typeof content === "string") return content;
90
+ if (Array.isArray(content)) {
91
+ return content
92
+ .filter((part) => part?.type === "text")
93
+ .map((part) => part.text)
94
+ .join("");
95
+ }
96
+ return "";
97
+ }
98
+
99
+ function pickLastTurnMessages(messages, cfg) {
100
+ const lastUserIndex = messages
101
+ .map((m, idx) => ({ m, idx }))
102
+ .filter(({ m }) => m?.role === "user")
103
+ .map(({ idx }) => idx)
104
+ .pop();
105
+
106
+ if (lastUserIndex === undefined) return [];
107
+
108
+ const slice = messages.slice(lastUserIndex);
109
+ const results = [];
110
+
111
+ for (const msg of slice) {
112
+ if (!msg || !msg.role) continue;
113
+
114
+ // 提取文本内容
115
+ const content = extractText(msg.content);
116
+ if (!content) continue;
117
+
118
+ const truncated = content.length > cfg.maxMessageChars
119
+ ? content.slice(0, cfg.maxMessageChars) + "..."
120
+ : content;
121
+
122
+ // 构建消息对象
123
+ const messageObj = {
124
+ role: msg.role,
125
+ content: truncated,
126
+ };
127
+
128
+ // 如果是用户消息,提取附件信息并添加到消息中
129
+ if (msg.role === "user") {
130
+ console.log("pickLastTurnMessages msg.content", msg.content)
131
+ console.log("pickLastTurnMessages msg.images", msg.images)
132
+ // 1. 从文本中提取本地文件路径
133
+ const attachments = extractAttachments(msg.content);
134
+ if (attachments.length > 0) {
135
+ messageObj.attachments = attachments.map((att) => ({
136
+ type: "local_file",
137
+ url: filePathToUrl(att.filePath),
138
+ mimeType: att.mimeType,
139
+ filePath: att.filePath,
140
+ }));
141
+ }
142
+
143
+ // 2. 从 content 数组中提取 base64 图片
144
+ const base64Images = extractBase64Images(msg.content);
145
+ if (base64Images.length > 0) {
146
+ messageObj.medias = base64Images.map((img) => ({
147
+ data: img.data,
148
+ mime_type: img.mimeType,
149
+ }));
150
+ }
151
+ }
152
+
153
+ results.push(messageObj);
154
+ }
155
+
156
+ return results;
157
+ }
158
+
159
+ function warnMissingApiKey(log, context) {
160
+ const heading = "[see-mem] Missing SEE_MEM_API_KEY (Token auth)";
161
+ const msg = `${heading}${context ? `; ${context} skipped` : ""}. Configure it with:`;
162
+ log.warn?.(
163
+ [
164
+ msg,
165
+ "echo 'export SEE_MEM_API_KEY=\"your-token\"' >> ~/.zshrc",
166
+ "source ~/.zshrc",
167
+ "or",
168
+ "[System.Environment]::SetEnvironmentVariable(\"SEE_MEM_API_KEY\", \"your-token\", \"User\")",
169
+ "Get API key from See-Mem developer platform",
170
+ ].join("\n"),
171
+ );
172
+ }
173
+
174
+ /**
175
+ * 将 searchMemory 的结果输出到 logs 目录
176
+ */
177
+ function saveSearchResult(result) {
178
+ try {
179
+ if (!fs.existsSync(logsDir)) {
180
+ fs.mkdirSync(logsDir, { recursive: true });
181
+ }
182
+ const timestamp = Date.now();
183
+ const filename = `${timestamp}.json`;
184
+ const filepath = path.join(logsDir, filename);
185
+ fs.writeFileSync(filepath, JSON.stringify(result, null, 2), "utf-8");
186
+ console.log(`[see-mem] search result saved to ${filepath}`);
187
+ } catch (err) {
188
+ console.warn(`[see-mem] failed to save search result: ${String(err)}`);
189
+ }
190
+ }
191
+
192
+ export default {
193
+ id: "see-mem-openclaw-plugin",
194
+ name: "See-Mem OpenClaw Plugin",
195
+ description: "See-Mem long-term memory recall + add via lifecycle hooks",
196
+ kind: "lifecycle",
197
+
198
+ register(api) {
199
+ const cfg = buildConfig(api.pluginConfig);
200
+ const log = api.logger ?? console;
201
+
202
+ api.on("before_prompt_build", async (event, ctx) => {
203
+ console.log("before_prompt_build params", event, ctx, cfg);
204
+ if (!cfg.recallEnabled) return;
205
+ if (!event?.prompt || event.prompt.length < 3) return;
206
+ if (!cfg.apiKey) {
207
+ warnMissingApiKey(log, "recall");
208
+ return;
209
+ }
210
+
211
+ try {
212
+ const payload = buildSearchPayload(cfg, event.prompt, ctx);
213
+ // console.log("before_agent_start searchMemory", cfg, payload)
214
+ log.info(`before_agent_start searchMemory payload: ${JSON.stringify(payload)}, cfg: ${JSON.stringify(cfg)}`)
215
+ const result = await searchMemory(cfg, payload);
216
+ log.info(`before_agent_start searchMemory result: ${JSON.stringify(result)}`)
217
+ // saveSearchResult(result);
218
+ // console.log("before_agent_start searchMemory result", result)
219
+ if (!result || result.code !== 0 || !result.data?.response) {
220
+ return;
221
+ }
222
+
223
+ const promptBlock = formatPromptBlock(result.data.response);
224
+ log.info(`before_agent_start promptBlock: ${JSON.stringify(promptBlock)}`)
225
+
226
+ if (!promptBlock) return;
227
+
228
+ return {
229
+ prependSystemContext: promptBlock,
230
+ };
231
+ } catch (err) {
232
+ log.warn?.(`[see-mem] recall failed: ${String(err)}`);
233
+ }
234
+ });
235
+
236
+ api.on("agent_end", async (event, ctx) => {
237
+ console.log("agent_end", event, ctx);
238
+ // cfg.sessionId = `openclaw_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
239
+ if (!cfg.addEnabled) return;
240
+ if (!event?.success || !event?.messages?.length) return;
241
+ if (!cfg.apiKey) {
242
+ warnMissingApiKey(log, "add");
243
+ return;
244
+ }
245
+
246
+ try {
247
+ const messages = pickLastTurnMessages(event.messages, cfg);
248
+ if (!messages.length) return;
249
+
250
+ const startTime = ctx?.sessionStartTime || new Date().toISOString().slice(0, 19);
251
+ const payload = buildAddMessagePayload(cfg, messages, ctx, startTime);
252
+ console.log("agent_end addMessage", cfg, payload);
253
+
254
+
255
+ await addMessage(cfg, payload);
256
+ } catch (err) {
257
+ log.warn?.(`[see-mem] add failed: ${String(err)}`);
258
+ }
259
+ });
260
+ },
261
+ };
@@ -0,0 +1,158 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ const DEFAULT_BASE_URL = "https://seemem.com/api/v1/memory";
6
+
7
+ function stripQuotes(value) {
8
+ if (!value) return value;
9
+ const trimmed = value.trim();
10
+ if (
11
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
12
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
13
+ ) {
14
+ return trimmed.slice(1, -1);
15
+ }
16
+ return trimmed;
17
+ }
18
+
19
+ function parseEnvFile(content) {
20
+ const values = new Map();
21
+ for (const line of content.split(/\r?\n/)) {
22
+ const trimmed = line.trim();
23
+ if (!trimmed || trimmed.startsWith("#")) continue;
24
+ const idx = trimmed.indexOf("=");
25
+ if (idx <= 0) continue;
26
+ const key = trimmed.slice(0, idx).trim();
27
+ const rawValue = trimmed.slice(idx + 1);
28
+ if (!key) continue;
29
+ values.set(key, stripQuotes(rawValue));
30
+ }
31
+ return values;
32
+ }
33
+
34
+ function loadEnvVar(name) {
35
+ try {
36
+ const envFilePath = join(homedir(), ".openclaw", ".env");
37
+ console.log("loadEnvVar envFilePath", envFilePath)
38
+ // const envFilePath = join("~/.openclaw", ".env");
39
+ const content = readFileSync(envFilePath, "utf-8");
40
+ const values = parseEnvFile(content);
41
+ if (values.has(name)) return values.get(name);
42
+ } catch {
43
+ // ignore missing file
44
+ }
45
+ return process.env[name];
46
+ }
47
+
48
+ export function buildConfig(pluginConfig = {}) {
49
+ const cfg = pluginConfig ?? {};
50
+
51
+ const baseUrl = cfg.baseUrl || loadEnvVar("SEE_MEM_BASE_URL") || DEFAULT_BASE_URL;
52
+ const apiKey = cfg.apiKey || loadEnvVar("SEE_MEM_API_KEY") || "";
53
+ const userId = cfg.userId || loadEnvVar("SEE_MEM_USER_ID") || "openclaw-user";
54
+ const sessionId = cfg.sessionId || loadEnvVar("SEE_MEM_SESSION_ID") || "";
55
+ // const sessionId = `openclaw_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
56
+
57
+ const recallEnabled = cfg.recallEnabled !== false;
58
+ const addEnabled = cfg.addEnabled !== false;
59
+ const maxMessageChars = cfg.maxMessageChars || 20000;
60
+ const timeoutMs = cfg.timeoutMs || 5000;
61
+
62
+ return {
63
+ baseUrl: baseUrl.replace(/\/+$/, ""),
64
+ apiKey,
65
+ userId,
66
+ sessionId,
67
+ recallEnabled,
68
+ addEnabled,
69
+ maxMessageChars,
70
+ timeoutMs,
71
+ };
72
+ }
73
+
74
+ export async function callApi({ baseUrl, apiKey, timeoutMs = 5000 }, path, body) {
75
+ if (!apiKey) {
76
+ throw new Error("Missing SEE_MEM API key (Token auth)");
77
+ }
78
+
79
+ const headers = {
80
+ "Content-Type": "application/json",
81
+ Authorization: apiKey,
82
+ };
83
+
84
+ const controller = new AbortController();
85
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
86
+
87
+ try {
88
+ const res = await fetch(`${baseUrl}${path}`, {
89
+ method: "POST",
90
+ headers,
91
+ body: JSON.stringify(body),
92
+ signal: controller.signal,
93
+ });
94
+
95
+ clearTimeout(timeoutId);
96
+
97
+ if (!res.ok) {
98
+ throw new Error(`HTTP ${res.status}`);
99
+ }
100
+
101
+ return await res.json();
102
+ } catch (err) {
103
+ clearTimeout(timeoutId);
104
+ throw err;
105
+ }
106
+ }
107
+
108
+ export async function searchMemory(cfg, payload) {
109
+ return callApi(cfg, "/search", payload);
110
+ }
111
+
112
+ export async function addMessage(cfg, payload) {
113
+ return callApi(cfg, "/add", payload);
114
+ }
115
+
116
+ /**
117
+ * 替换文本中的图片域名
118
+ * @param memoryText 原始文本内容
119
+ * @returns 替换域名后的文本内容
120
+ */
121
+ function replaceImageDomain(memoryText) {
122
+ // 定义需要替换的原始域名和新域名
123
+ const oldDomain = 'seemem-prod-1310684398.cos.ap-guangzhou.myqcloud.com';
124
+ const newDomain = 'res.seemem.seemem.com';
125
+
126
+ // 使用正则表达式全局替换域名(忽略大小写,增强兼容性)
127
+ // 正则中的 \. 需要转义,\b 用于匹配单词边界,避免部分匹配
128
+ const regex = new RegExp(`\\b${oldDomain.replace(/\./g, '\\.')}\\b`, 'gi');
129
+
130
+ // 执行替换并返回结果
131
+ return memoryText.replace(regex, newDomain);
132
+ }
133
+
134
+ export function formatPromptBlock(memoryText) {
135
+
136
+ if (!memoryText) return "";
137
+
138
+ return `# Relevant Memory
139
+
140
+ <memory>
141
+
142
+ ${replaceImageDomain(memoryText)}
143
+
144
+ </memory>
145
+
146
+ ---
147
+
148
+ ## SYSTEM PROMPT
149
+ 1. Above is the user's historical memory data (from past conversations), which you can reference when answering.
150
+ 2. When answering, combine the user's current question with the memory context to provide personalized responses.
151
+ 3. Image URLs in the memory are in format: (URL: https://...) - search for patterns like "(URL: https://..." to find images.
152
+ 4. Images are stored on OSS and URLs are publicly accessible over the Internet - you can directly send them to the user.
153
+ 5. Determine whether to send the image based on the user's question:
154
+ - If the user explicitly asks about the image, mentions related content, or the image is directly relevant to their question → send it
155
+ - If you're unsure whether to send → ask the user if they want to see the related images
156
+ 6. When sending images, you can say something like "我找到了一些相关图片,发给你看看" or "这张图片和你提到的xxx有关" to let the user know you found relevant images.
157
+ 7. Do NOT download or save the image yourself - just send the URL as a media message.`;
158
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "id": "see-mem-openclaw-plugin",
3
+ "name": "See-Mem OpenClaw Plugin",
4
+ "description": "See-Mem long-term memory recall + add via lifecycle hooks",
5
+ "version": "0.1.4",
6
+ "kind": "lifecycle",
7
+ "main": "./index.js",
8
+ "configSchema": {
9
+ "type": "object",
10
+ "properties": {
11
+ "baseUrl": {
12
+ "type": "string",
13
+ "description": "See-Mem API base URL",
14
+ "default": "https://seemem.com/api/v1/memory"
15
+ },
16
+ "apiKey": {
17
+ "type": "string",
18
+ "description": "See-Mem API Key (Token auth; also supports via ~/.openclaw/.env or process env SEE_MEM_API_KEY)"
19
+ },
20
+ "userId": {
21
+ "type": "string",
22
+ "description": "See-Mem user_id (default: openclaw-user)",
23
+ "default": "openclaw-user"
24
+ },
25
+ "sessionId": {
26
+ "type": "string",
27
+ "description": "Override session_id"
28
+ },
29
+ "recallEnabled": {
30
+ "type": "boolean",
31
+ "default": true
32
+ },
33
+ "addEnabled": {
34
+ "type": "boolean",
35
+ "default": true
36
+ },
37
+ "maxMessageChars": {
38
+ "type": "integer",
39
+ "description": "Max chars per message when adding",
40
+ "default": 20000
41
+ },
42
+ "timeoutMs": {
43
+ "type": "integer",
44
+ "default": 5000
45
+ }
46
+ },
47
+ "required": [],
48
+ "additionalProperties": true
49
+ }
50
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@seemem/see-mem-openclaw-plugin",
3
+ "version": "0.1.4",
4
+ "description": "OpenClaw lifecycle plugin for SeeMemory - long-term memory recall and persistence",
5
+ "main": "./index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "sync-version": "node scripts/sync-version.js",
9
+ "version": "npm run sync-version && git add openclaw.plugin.json moltbot.plugin.json clawdbot.plugin.json",
10
+ "publish-beta": "npm publish --access public --tag beta",
11
+ "publish-beta-patch": "npm version prepatch --preid=beta && npm publish --access public --tag beta",
12
+ "publish-latest": "npm version $(node -p \"require('./package.json').version.split('-')[0]\") && npm publish --access public",
13
+ "publish-latest-patch": "npm version patch && npm publish --access public"
14
+ },
15
+ "keywords": [
16
+ "seemem",
17
+ "see-mem",
18
+ "openclaw",
19
+ "plugin",
20
+ "memory",
21
+ "long-term-memory",
22
+ "ai-memory"
23
+ ],
24
+ "author": "SeeMemory",
25
+ "license": "MIT",
26
+ "files": [
27
+ "index.js",
28
+ "lib/",
29
+ "openclaw.plugin.json"
30
+ ],
31
+ "openclaw": {
32
+ "extensions": [
33
+ "./index.js"
34
+ ]
35
+ },
36
+ "clawdbot": {
37
+ "extensions": [
38
+ "./index.js"
39
+ ]
40
+ },
41
+ "moltbot": {
42
+ "extensions": [
43
+ "./index.js"
44
+ ]
45
+ }
46
+ }