@t0ken.ai/memoryx-openclaw-plugin 1.0.1 → 1.0.3
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 +36 -0
- package/dist/index.d.ts +93 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +347 -128
- package/openclaw.plugin.json +2 -2
- package/package.json +19 -6
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# MemoryX OpenClaw Plugin
|
|
2
|
+
|
|
3
|
+
Real-time memory capture and recall plugin for OpenClaw.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Conversation Buffer**: Automatically buffers conversations with token counting
|
|
8
|
+
- **Auto Registration**: Agents auto-register with machine fingerprint
|
|
9
|
+
- **Memory Recall**: Semantic search for relevant memories
|
|
10
|
+
- **Configurable API**: Custom API base URL support
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @t0ken.ai/memoryx-openclaw-plugin
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"apiBaseUrl": "https://t0ken.ai/api"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
The plugin automatically:
|
|
29
|
+
1. Captures user and assistant messages
|
|
30
|
+
2. Buffers conversations until threshold (2 rounds)
|
|
31
|
+
3. Flushes to MemoryX API for memory extraction
|
|
32
|
+
4. Recalls relevant memories before agent starts
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MemoryX
|
|
2
|
+
* MemoryX Realtime Plugin for OpenClaw - Phase 1
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* - ConversationBuffer with token counting
|
|
6
|
+
* - Batch upload to /conversations/flush
|
|
7
|
+
* - Auto-register and quota handling
|
|
8
|
+
* - Sensitive data filtered on server
|
|
9
|
+
* - Configurable API base URL
|
|
10
|
+
* - Precise token counting with tiktoken
|
|
11
|
+
*
|
|
12
|
+
* Model Downloads (CDN):
|
|
13
|
+
* - INT8 Model (122MB, recommended): https://static.t0ken.ai/models/model_int8.onnx
|
|
14
|
+
* - FP32 Model (489MB): https://static.t0ken.ai/models/model.onnx
|
|
8
15
|
*/
|
|
16
|
+
interface PluginConfig {
|
|
17
|
+
apiBaseUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
interface Message {
|
|
20
|
+
role: string;
|
|
21
|
+
content: string;
|
|
22
|
+
tokens: number;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
interface RecallResult {
|
|
26
|
+
memories: Array<{
|
|
27
|
+
id: string;
|
|
28
|
+
content: string;
|
|
29
|
+
category: string;
|
|
30
|
+
score: number;
|
|
31
|
+
}>;
|
|
32
|
+
isLimited: boolean;
|
|
33
|
+
remainingQuota: number;
|
|
34
|
+
upgradeHint?: string;
|
|
35
|
+
}
|
|
36
|
+
declare class ConversationBuffer {
|
|
37
|
+
private messages;
|
|
38
|
+
private tokenCount;
|
|
39
|
+
private roundCount;
|
|
40
|
+
private lastRole;
|
|
41
|
+
private conversationId;
|
|
42
|
+
private startedAt;
|
|
43
|
+
private lastActivityAt;
|
|
44
|
+
private encoder;
|
|
45
|
+
private readonly ROUND_THRESHOLD;
|
|
46
|
+
private readonly TIMEOUT_MS;
|
|
47
|
+
private readonly MAX_TOKENS_PER_MESSAGE;
|
|
48
|
+
constructor();
|
|
49
|
+
private generateId;
|
|
50
|
+
private countTokens;
|
|
51
|
+
addMessage(role: string, content: string): boolean;
|
|
52
|
+
shouldFlush(): boolean;
|
|
53
|
+
flush(): {
|
|
54
|
+
conversation_id: string;
|
|
55
|
+
messages: Message[];
|
|
56
|
+
total_tokens: number;
|
|
57
|
+
};
|
|
58
|
+
forceFlush(): {
|
|
59
|
+
conversation_id: string;
|
|
60
|
+
messages: Message[];
|
|
61
|
+
total_tokens: number;
|
|
62
|
+
} | null;
|
|
63
|
+
getStatus(): {
|
|
64
|
+
messageCount: number;
|
|
65
|
+
tokenCount: number;
|
|
66
|
+
conversationId: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
declare class MemoryXPlugin {
|
|
70
|
+
private config;
|
|
71
|
+
private buffer;
|
|
72
|
+
private flushTimer;
|
|
73
|
+
private readonly FLUSH_CHECK_INTERVAL;
|
|
74
|
+
private pluginConfig;
|
|
75
|
+
constructor(pluginConfig?: PluginConfig);
|
|
76
|
+
private get apiBase();
|
|
77
|
+
private init;
|
|
78
|
+
private loadConfig;
|
|
79
|
+
private saveConfig;
|
|
80
|
+
private autoRegister;
|
|
81
|
+
private getMachineFingerprint;
|
|
82
|
+
private startFlushTimer;
|
|
83
|
+
private flushConversation;
|
|
84
|
+
onMessage(role: string, content: string): Promise<boolean>;
|
|
85
|
+
recall(query: string, limit?: number): Promise<RecallResult>;
|
|
86
|
+
endConversation(): Promise<void>;
|
|
87
|
+
getStatus(): {
|
|
88
|
+
initialized: boolean;
|
|
89
|
+
hasApiKey: boolean;
|
|
90
|
+
bufferStatus: {
|
|
91
|
+
messageCount: number;
|
|
92
|
+
tokenCount: number;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
}
|
|
9
96
|
export declare function onMessage(message: string, context: Record<string, any>): Promise<{
|
|
10
97
|
context: Record<string, any>;
|
|
11
98
|
}>;
|
|
12
99
|
export declare function onResponse(response: string, context: Record<string, any>): string;
|
|
13
|
-
export declare function register(api: any): void;
|
|
100
|
+
export declare function register(api: any, pluginConfig?: PluginConfig): void;
|
|
101
|
+
export { MemoryXPlugin, ConversationBuffer };
|
|
14
102
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,164 +1,383 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MemoryX
|
|
2
|
+
* MemoryX Realtime Plugin for OpenClaw - Phase 1
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* - ConversationBuffer with token counting
|
|
6
|
+
* - Batch upload to /conversations/flush
|
|
7
|
+
* - Auto-register and quota handling
|
|
8
|
+
* - Sensitive data filtered on server
|
|
9
|
+
* - Configurable API base URL
|
|
10
|
+
* - Precise token counting with tiktoken
|
|
11
|
+
*
|
|
12
|
+
* Model Downloads (CDN):
|
|
13
|
+
* - INT8 Model (122MB, recommended): https://static.t0ken.ai/models/model_int8.onnx
|
|
14
|
+
* - FP32 Model (489MB): https://static.t0ken.ai/models/model.onnx
|
|
8
15
|
*/
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
import { getEncoding } from "js-tiktoken";
|
|
17
|
+
const DEFAULT_API_BASE = "https://t0ken.ai/api";
|
|
18
|
+
class ConversationBuffer {
|
|
19
|
+
messages = [];
|
|
20
|
+
tokenCount = 0;
|
|
21
|
+
roundCount = 0;
|
|
22
|
+
lastRole = "";
|
|
23
|
+
conversationId = "";
|
|
24
|
+
startedAt = Date.now();
|
|
25
|
+
lastActivityAt = Date.now();
|
|
26
|
+
encoder;
|
|
27
|
+
ROUND_THRESHOLD = 2;
|
|
28
|
+
TIMEOUT_MS = 30 * 60 * 1000;
|
|
29
|
+
MAX_TOKENS_PER_MESSAGE = 8000;
|
|
30
|
+
constructor() {
|
|
31
|
+
this.conversationId = this.generateId();
|
|
32
|
+
this.encoder = getEncoding("cl100k_base");
|
|
23
33
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
34
|
+
generateId() {
|
|
35
|
+
return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
36
|
+
}
|
|
37
|
+
countTokens(text) {
|
|
38
|
+
return this.encoder.encode(text).length;
|
|
39
|
+
}
|
|
40
|
+
addMessage(role, content) {
|
|
41
|
+
if (!content || content.length < 2) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const tokens = this.countTokens(content);
|
|
45
|
+
if (tokens > this.MAX_TOKENS_PER_MESSAGE) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const message = {
|
|
49
|
+
role,
|
|
50
|
+
content,
|
|
51
|
+
tokens,
|
|
52
|
+
timestamp: Date.now()
|
|
53
|
+
};
|
|
54
|
+
this.messages.push(message);
|
|
55
|
+
this.tokenCount += tokens;
|
|
56
|
+
this.lastActivityAt = Date.now();
|
|
57
|
+
if (role === "assistant" && this.lastRole === "user") {
|
|
58
|
+
this.roundCount++;
|
|
59
|
+
}
|
|
60
|
+
this.lastRole = role;
|
|
61
|
+
return this.roundCount >= this.ROUND_THRESHOLD;
|
|
45
62
|
}
|
|
46
|
-
|
|
63
|
+
shouldFlush() {
|
|
64
|
+
if (this.messages.length === 0) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (this.roundCount >= this.ROUND_THRESHOLD) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const elapsed = Date.now() - this.lastActivityAt;
|
|
71
|
+
if (elapsed > this.TIMEOUT_MS) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
47
74
|
return false;
|
|
48
75
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
flush() {
|
|
77
|
+
const data = {
|
|
78
|
+
conversation_id: this.conversationId,
|
|
79
|
+
messages: [...this.messages],
|
|
80
|
+
total_tokens: this.tokenCount
|
|
81
|
+
};
|
|
82
|
+
this.messages = [];
|
|
83
|
+
this.tokenCount = 0;
|
|
84
|
+
this.roundCount = 0;
|
|
85
|
+
this.lastRole = "";
|
|
86
|
+
this.conversationId = this.generateId();
|
|
87
|
+
this.startedAt = Date.now();
|
|
88
|
+
this.lastActivityAt = Date.now();
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
forceFlush() {
|
|
92
|
+
if (this.messages.length === 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return this.flush();
|
|
96
|
+
}
|
|
97
|
+
getStatus() {
|
|
98
|
+
return {
|
|
99
|
+
messageCount: this.messages.length,
|
|
100
|
+
tokenCount: this.tokenCount,
|
|
101
|
+
conversationId: this.conversationId
|
|
102
|
+
};
|
|
71
103
|
}
|
|
72
104
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
class MemoryXPlugin {
|
|
106
|
+
config = {
|
|
107
|
+
apiKey: null,
|
|
108
|
+
projectId: "default",
|
|
109
|
+
userId: null,
|
|
110
|
+
initialized: false,
|
|
111
|
+
apiBaseUrl: DEFAULT_API_BASE
|
|
112
|
+
};
|
|
113
|
+
buffer = new ConversationBuffer();
|
|
114
|
+
flushTimer = null;
|
|
115
|
+
FLUSH_CHECK_INTERVAL = 30000;
|
|
116
|
+
pluginConfig = null;
|
|
117
|
+
constructor(pluginConfig) {
|
|
118
|
+
this.pluginConfig = pluginConfig || null;
|
|
119
|
+
if (pluginConfig?.apiBaseUrl) {
|
|
120
|
+
this.config.apiBaseUrl = pluginConfig.apiBaseUrl;
|
|
121
|
+
}
|
|
122
|
+
this.init();
|
|
123
|
+
}
|
|
124
|
+
get apiBase() {
|
|
125
|
+
return this.config.apiBaseUrl || DEFAULT_API_BASE;
|
|
126
|
+
}
|
|
127
|
+
async init() {
|
|
128
|
+
await this.loadConfig();
|
|
129
|
+
if (!this.config.apiKey) {
|
|
130
|
+
await this.autoRegister();
|
|
131
|
+
}
|
|
132
|
+
this.startFlushTimer();
|
|
133
|
+
this.config.initialized = true;
|
|
134
|
+
}
|
|
135
|
+
async loadConfig() {
|
|
136
|
+
try {
|
|
137
|
+
const stored = localStorage.getItem("memoryx_config");
|
|
138
|
+
if (stored) {
|
|
139
|
+
const storedConfig = JSON.parse(stored);
|
|
140
|
+
this.config = {
|
|
141
|
+
...this.config,
|
|
142
|
+
...storedConfig,
|
|
143
|
+
apiBaseUrl: storedConfig.apiBaseUrl || this.config.apiBaseUrl
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
console.warn("[MemoryX] Failed to load config:", e);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
saveConfig() {
|
|
152
|
+
try {
|
|
153
|
+
localStorage.setItem("memoryx_config", JSON.stringify(this.config));
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
console.warn("[MemoryX] Failed to save config:", e);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async autoRegister() {
|
|
160
|
+
try {
|
|
161
|
+
const fingerprint = await this.getMachineFingerprint();
|
|
162
|
+
const response = await fetch(`${this.apiBase}/agents/auto-register`, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { "Content-Type": "application/json" },
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
machine_fingerprint: fingerprint,
|
|
167
|
+
agent_type: "openclaw",
|
|
168
|
+
agent_name: "openclaw-agent",
|
|
169
|
+
platform: navigator.platform,
|
|
170
|
+
platform_version: navigator.userAgent
|
|
171
|
+
})
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
throw new Error(`Auto-register failed: ${response.status}`);
|
|
175
|
+
}
|
|
176
|
+
const data = await response.json();
|
|
177
|
+
this.config.apiKey = data.api_key;
|
|
178
|
+
this.config.projectId = String(data.project_id);
|
|
179
|
+
this.config.userId = data.agent_id;
|
|
180
|
+
this.saveConfig();
|
|
181
|
+
console.log("[MemoryX] Auto-registered successfully");
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
console.error("[MemoryX] Auto-register failed:", e);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async getMachineFingerprint() {
|
|
188
|
+
const components = [
|
|
189
|
+
navigator.platform,
|
|
190
|
+
navigator.language,
|
|
191
|
+
navigator.hardwareConcurrency || 0,
|
|
192
|
+
screen.width,
|
|
193
|
+
screen.height,
|
|
194
|
+
new Date().getTimezoneOffset()
|
|
195
|
+
];
|
|
196
|
+
const raw = components.join("|");
|
|
197
|
+
const encoder = new TextEncoder();
|
|
198
|
+
const data = encoder.encode(raw);
|
|
199
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
200
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
201
|
+
return hashArray.slice(0, 32).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
202
|
+
}
|
|
203
|
+
startFlushTimer() {
|
|
204
|
+
this.flushTimer = setInterval(() => {
|
|
205
|
+
if (this.buffer.shouldFlush()) {
|
|
206
|
+
this.flushConversation();
|
|
207
|
+
}
|
|
208
|
+
}, this.FLUSH_CHECK_INTERVAL);
|
|
209
|
+
}
|
|
210
|
+
async flushConversation() {
|
|
211
|
+
if (!this.config.apiKey) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const data = this.buffer.forceFlush();
|
|
215
|
+
if (!data || data.messages.length === 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const response = await fetch(`${this.apiBase}/v1/conversations/flush`, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: {
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
"X-API-Key": this.config.apiKey
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify(data)
|
|
226
|
+
});
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
const errorData = await response.json().catch(() => ({}));
|
|
229
|
+
if (response.status === 402) {
|
|
230
|
+
console.warn("[MemoryX] Quota exceeded:", errorData.detail);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.error("[MemoryX] Flush failed:", errorData);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const result = await response.json();
|
|
238
|
+
console.log(`[MemoryX] Flushed ${data.messages.length} messages, extracted ${result.extracted_count} memories`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
console.error("[MemoryX] Flush error:", e);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async onMessage(role, content) {
|
|
246
|
+
if (!content || content.length < 2) {
|
|
83
247
|
return false;
|
|
248
|
+
}
|
|
249
|
+
const skipPatterns = [
|
|
250
|
+
/^[好的ok谢谢嗯啊哈哈你好hihello拜拜再见]{1,5}$/i,
|
|
251
|
+
/^[??!!。,,\s]{1,10}$/
|
|
252
|
+
];
|
|
253
|
+
for (const pattern of skipPatterns) {
|
|
254
|
+
if (pattern.test(content.trim())) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const shouldFlush = this.buffer.addMessage(role, content);
|
|
259
|
+
if (shouldFlush) {
|
|
260
|
+
await this.flushConversation();
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
async recall(query, limit = 5) {
|
|
265
|
+
if (!this.config.apiKey || !query || query.length < 2) {
|
|
266
|
+
return { memories: [], isLimited: false, remainingQuota: 0 };
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const response = await fetch(`${this.apiBase}/v1/memories/search`, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: {
|
|
272
|
+
"Content-Type": "application/json",
|
|
273
|
+
"X-API-Key": this.config.apiKey
|
|
274
|
+
},
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
query,
|
|
277
|
+
project_id: this.config.projectId,
|
|
278
|
+
limit
|
|
279
|
+
})
|
|
280
|
+
});
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
const errorData = await response.json().catch(() => ({}));
|
|
283
|
+
if (response.status === 402 || response.status === 429) {
|
|
284
|
+
return {
|
|
285
|
+
memories: [],
|
|
286
|
+
isLimited: true,
|
|
287
|
+
remainingQuota: 0,
|
|
288
|
+
upgradeHint: errorData.detail || "云端查询配额已用尽,请升级到付费版"
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
throw new Error(`Search failed: ${response.status}`);
|
|
292
|
+
}
|
|
293
|
+
const data = await response.json();
|
|
294
|
+
return {
|
|
295
|
+
memories: (data.data || []).map((m) => ({
|
|
296
|
+
id: m.id,
|
|
297
|
+
content: m.content,
|
|
298
|
+
category: m.category || "other",
|
|
299
|
+
score: m.score || 0.5
|
|
300
|
+
})),
|
|
301
|
+
isLimited: false,
|
|
302
|
+
remainingQuota: data.remaining_quota ?? -1
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
console.error("[MemoryX] Recall failed:", e);
|
|
307
|
+
return { memories: [], isLimited: false, remainingQuota: 0 };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async endConversation() {
|
|
311
|
+
await this.flushConversation();
|
|
312
|
+
console.log("[MemoryX] Conversation ended, buffer flushed");
|
|
313
|
+
}
|
|
314
|
+
getStatus() {
|
|
315
|
+
return {
|
|
316
|
+
initialized: this.config.initialized,
|
|
317
|
+
hasApiKey: !!this.config.apiKey,
|
|
318
|
+
bufferStatus: this.buffer.getStatus()
|
|
319
|
+
};
|
|
84
320
|
}
|
|
85
|
-
const triggers = [
|
|
86
|
-
/记住|记一下|别忘了|save|remember/i,
|
|
87
|
-
/我喜欢|我讨厌|我习惯|我偏好|prefer|like|hate/i,
|
|
88
|
-
/我是|我在|我来自|i am|i work/i,
|
|
89
|
-
/纠正|更正|应该是|correct|actually/i,
|
|
90
|
-
/计划|打算|目标|plan|goal|will/i,
|
|
91
|
-
];
|
|
92
|
-
return triggers.some((pattern) => pattern.test(text));
|
|
93
|
-
}
|
|
94
|
-
// Detect category
|
|
95
|
-
function detectCategory(text) {
|
|
96
|
-
const lower = text.toLowerCase();
|
|
97
|
-
if (/prefer|like|hate|习惯|偏好|喜欢|讨厌/.test(lower))
|
|
98
|
-
return "preference";
|
|
99
|
-
if (/correct|纠正|更正/.test(lower))
|
|
100
|
-
return "correction";
|
|
101
|
-
if (/plan|goal|计划|打算/.test(lower))
|
|
102
|
-
return "plan";
|
|
103
|
-
return "semantic";
|
|
104
321
|
}
|
|
105
|
-
|
|
322
|
+
let plugin;
|
|
106
323
|
export async function onMessage(message, context) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
if (!shouldCapture(message)) {
|
|
112
|
-
return { context };
|
|
113
|
-
}
|
|
114
|
-
const category = detectCategory(message);
|
|
115
|
-
// Async store (non-blocking)
|
|
116
|
-
storeToMemoryX(message, category, {
|
|
117
|
-
from: context?.from,
|
|
118
|
-
channel: context?.channelId,
|
|
119
|
-
}).catch(() => { });
|
|
324
|
+
if (message && plugin) {
|
|
325
|
+
await plugin.onMessage("user", message);
|
|
326
|
+
}
|
|
120
327
|
return { context };
|
|
121
328
|
}
|
|
122
329
|
export function onResponse(response, context) {
|
|
123
330
|
return response;
|
|
124
331
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
332
|
+
export function register(api, pluginConfig) {
|
|
333
|
+
api.logger.info("[MemoryX] Plugin registering (Phase 1 - Cloud with Buffer)...");
|
|
334
|
+
if (pluginConfig?.apiBaseUrl) {
|
|
335
|
+
api.logger.info(`[MemoryX] Using custom API base URL: ${pluginConfig.apiBaseUrl}`);
|
|
336
|
+
}
|
|
337
|
+
plugin = new MemoryXPlugin(pluginConfig);
|
|
129
338
|
api.on("message_received", async (event, ctx) => {
|
|
130
|
-
if (isPluginInstalled())
|
|
131
|
-
return;
|
|
132
339
|
const { content, from, timestamp } = event;
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
340
|
+
if (content && plugin) {
|
|
341
|
+
await plugin.onMessage("user", content);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
api.on("assistant_response", async (event, ctx) => {
|
|
345
|
+
const { content } = event;
|
|
346
|
+
if (content && plugin) {
|
|
347
|
+
await plugin.onMessage("assistant", content);
|
|
348
|
+
}
|
|
141
349
|
});
|
|
142
|
-
// 2. Auto-recall before agent starts
|
|
143
350
|
api.on("before_agent_start", async (event, ctx) => {
|
|
144
351
|
const { prompt } = event;
|
|
145
|
-
if (!prompt || prompt.length <
|
|
352
|
+
if (!prompt || prompt.length < 2 || !plugin)
|
|
146
353
|
return;
|
|
147
354
|
try {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
355
|
+
const result = await plugin.recall(prompt, 5);
|
|
356
|
+
if (result.isLimited) {
|
|
357
|
+
api.logger.warn(`[MemoryX] ${result.upgradeHint}`);
|
|
358
|
+
return {
|
|
359
|
+
prependContext: `[系统提示] ${result.upgradeHint}\n`
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (result.memories.length === 0)
|
|
150
363
|
return;
|
|
151
|
-
const memories =
|
|
152
|
-
.map(
|
|
364
|
+
const memories = result.memories
|
|
365
|
+
.map(m => `- [${m.category}] ${m.content}`)
|
|
153
366
|
.join("\n");
|
|
154
|
-
api.logger.info(`[MemoryX] Recalled ${
|
|
367
|
+
api.logger.info(`[MemoryX] Recalled ${result.memories.length} memories from cloud`);
|
|
155
368
|
return {
|
|
156
|
-
prependContext: `[相关记忆]\n${memories}\n[End of memories]
|
|
369
|
+
prependContext: `[相关记忆]\n${memories}\n[End of memories]\n`
|
|
157
370
|
};
|
|
158
371
|
}
|
|
159
372
|
catch (error) {
|
|
160
373
|
api.logger.warn(`[MemoryX] Recall failed: ${error}`);
|
|
161
374
|
}
|
|
162
375
|
});
|
|
163
|
-
api.
|
|
376
|
+
api.on("conversation_end", async (event, ctx) => {
|
|
377
|
+
if (plugin) {
|
|
378
|
+
await plugin.endConversation();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
api.logger.info("[MemoryX] Plugin registered successfully");
|
|
164
382
|
}
|
|
383
|
+
export { MemoryXPlugin, ConversationBuffer };
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "memoryx-
|
|
2
|
+
"id": "@t0ken.ai/memoryx-openclaw-plugin",
|
|
3
3
|
"name": "MemoryX Real-time Plugin",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.3",
|
|
5
5
|
"description": "Real-time memory capture and recall for OpenClaw",
|
|
6
6
|
"entry": "./dist/index.js",
|
|
7
7
|
"kind": "memory",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t0ken.ai/memoryx-openclaw-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "MemoryX real-time memory capture and recall plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"prepare": "npm run build"
|
|
16
16
|
},
|
|
17
|
-
"keywords": [
|
|
17
|
+
"keywords": [
|
|
18
|
+
"openclaw",
|
|
19
|
+
"memoryx",
|
|
20
|
+
"memory",
|
|
21
|
+
"plugin"
|
|
22
|
+
],
|
|
18
23
|
"author": "MemoryX Team",
|
|
19
24
|
"license": "MIT",
|
|
20
25
|
"repository": {
|
|
@@ -23,10 +28,18 @@
|
|
|
23
28
|
"directory": "plugins/memoryx-realtime-plugin"
|
|
24
29
|
},
|
|
25
30
|
"openclaw": {
|
|
26
|
-
"extensions": [
|
|
31
|
+
"extensions": [
|
|
32
|
+
"./dist/index.js"
|
|
33
|
+
]
|
|
27
34
|
},
|
|
28
35
|
"devDependencies": {
|
|
29
|
-
"
|
|
30
|
-
"
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"typescript": "^5.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@huggingface/transformers": "^3.8.1",
|
|
41
|
+
"js-tiktoken": "^1.0.21",
|
|
42
|
+
"onnxruntime-node": "^1.24.1",
|
|
43
|
+
"onnxruntime-web": "^1.24.1"
|
|
31
44
|
}
|
|
32
|
-
}
|
|
45
|
+
}
|