memory-search-plugin 0.2.0 → 0.3.0

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.
Files changed (3) hide show
  1. package/identity.ts +43 -30
  2. package/index.ts +103 -21
  3. package/package.json +1 -1
package/identity.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * 身份解析模块
3
3
  *
4
- * sessionKey 中解析出当前会话的身份类型和场景。
5
- * sessionKey 格式参考 V1.4 文档和 memory-privacy/identity.ts
4
+ * 方案 4:从 LLM 传入的工具参数解析身份信息。
5
+ * LLM system prompt 中的 UntrustedContext 提取字段并填入工具参数。
6
+ *
7
+ * owner 判断:通过环境变量 OPENCLAW_GATEWAY_TOKEN 获取 owner_id,
8
+ * 与 sender_id 比对(与旧版逻辑一致)。
6
9
  *
7
10
  * 输出:
8
11
  * - scene: 对应后端 SearchRequest.scene(private_own / private_other / group)
@@ -25,22 +28,26 @@ export interface ResolvedIdentity {
25
28
  group_id: string | null;
26
29
  }
27
30
 
31
+ /** LLM 传入的身份相关参数(来自 UntrustedContext) */
32
+ export interface IdentityParams {
33
+ sender_id?: string;
34
+ conversation_type?: string; // "direct" | "group"
35
+ group_id?: string;
36
+ }
37
+
28
38
  /**
29
- * 从 sessionKey 解析身份 + 场景
39
+ * 从 LLM 工具参数解析身份 + 场景
30
40
  *
31
- * sessionKey 末段格式:
32
- * owner/peer: user_{userId}_lobster_{lobsterId}_release_{version}
33
- * group: user_{userId}_lobster_{lobsterId}_group_{groupId}_release_{version}
41
+ * owner 判断逻辑与旧版一致:通过环境变量 OPENCLAW_GATEWAY_TOKEN 获取 owner_id,
42
+ * 再与 sender_id 比对。
34
43
  *
35
44
  * 场景映射:
36
- * owner(私聊自己的 agent)→ scene = "private_own"
37
- * peer (私聊别人的 agent)→ scene = "private_other"
38
- * group(群聊中 agent 回复)→ scene = "group"
45
+ * conversation_type="group" → scene = "group"
46
+ * conversation_type="direct" + isOwner(sender_id) scene = "private_own"
47
+ * conversation_type="direct" + !isOwner → scene = "private_other"
39
48
  */
40
- export function resolveIdentity(sessionKey: string | undefined): ResolvedIdentity {
49
+ export function resolveIdentityFromParams(params: IdentityParams): ResolvedIdentity {
41
50
  // ── 测试模式:环境变量覆盖 ──────────────────────────
42
- // 设置 MEMORY_GATEWAY_TEST_SCENE 即可跳过 sessionKey 解析,
43
- // 方便在没有真实 IM 会话时测试插件。
44
51
  const testScene = process.env.MEMORY_GATEWAY_TEST_SCENE as Scene | undefined;
45
52
  if (testScene) {
46
53
  const result: ResolvedIdentity = {
@@ -52,23 +59,39 @@ export function resolveIdentity(sessionKey: string | undefined): ResolvedIdentit
52
59
  return result;
53
60
  }
54
61
 
55
- // ── 正常模式:从 sessionKey 解析 ──────────────────────
62
+ const userId = params.sender_id?.trim() || "unknown";
63
+ const convType = (params.conversation_type || "direct").trim().toLowerCase();
64
+
65
+ // 1. 群聊
66
+ if (convType === "group") {
67
+ return {
68
+ scene: "group",
69
+ user_id: userId,
70
+ group_id: params.group_id?.trim() || null,
71
+ };
72
+ }
73
+
74
+ // 2. 私聊:通过环境变量比对 owner_id 判断 owner/peer
75
+ if (isOwner(userId)) {
76
+ return { scene: "private_own", user_id: userId, group_id: null };
77
+ }
78
+
79
+ return { scene: "private_other", user_id: userId, group_id: null };
80
+ }
81
+
82
+ // ── 向后兼容:保留旧函数签名,sessionKey 仍可用时作为 fallback ──
83
+
84
+ export function resolveIdentity(sessionKey: string | undefined): ResolvedIdentity {
56
85
  const sk = sessionKey ?? "";
57
86
  const sessionPart = sk.split(":").pop() || "";
58
87
 
59
- // 1. 群聊: 包含 _group_
60
88
  const groupMatch = sessionPart.match(
61
89
  /user_(.+?)_lobster_(.+?)_group_(.+?)_release_(.+)$/
62
90
  );
63
91
  if (groupMatch) {
64
- return {
65
- scene: "group",
66
- user_id: groupMatch[1],
67
- group_id: groupMatch[3],
68
- };
92
+ return { scene: "group", user_id: groupMatch[1], group_id: groupMatch[3] };
69
93
  }
70
94
 
71
- // 2. 私聊 (owner 或 peer)
72
95
  const peerMatch = sessionPart.match(
73
96
  /user_(.+?)_lobster_(.+?)_release_(.+)$/
74
97
  );
@@ -80,15 +103,9 @@ export function resolveIdentity(sessionKey: string | undefined): ResolvedIdentit
80
103
  return { scene: "private_other", user_id: userId, group_id: null };
81
104
  }
82
105
 
83
- // 3. 兜底: 安全起见当作 peer(private_other)
84
106
  return { scene: "private_other", user_id: "unknown", group_id: null };
85
107
  }
86
108
 
87
- /**
88
- * 判断是否是 owner
89
- * 通过环境变量 OPENCLAW_GATEWAY_TOKEN 判断
90
- * token 格式: oc-user-{ownerUserId}
91
- */
92
109
  function isOwner(userId: string): boolean {
93
110
  const token = process.env.OPENCLAW_GATEWAY_TOKEN || "";
94
111
  if (!token) return false;
@@ -96,10 +113,6 @@ function isOwner(userId: string): boolean {
96
113
  return ownerUserId !== "" && ownerUserId === userId;
97
114
  }
98
115
 
99
- /**
100
- * 从 sessionKey 提取 agent_id
101
- * sessionKey 格式: agent:{agentId}:palz:...
102
- */
103
116
  export function extractAgentId(sessionKey: string | undefined): string {
104
117
  const parts = (sessionKey || "").split(":");
105
118
  return parts.length >= 2 ? parts[1] : "main";
package/index.ts CHANGED
@@ -26,7 +26,8 @@
26
26
 
27
27
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
28
28
  import { createGatewayClient } from "./gateway-client";
29
- import { resolveIdentity, extractAgentId } from "./identity";
29
+ import { resolveIdentityFromParams, resolveIdentity, extractAgentId } from "./identity";
30
+ import type { IdentityParams } from "./identity";
30
31
  import type { MemorySearchResult } from "./gateway-client";
31
32
 
32
33
  // ── Prompt Section ──────────────────────────────────────
@@ -64,6 +65,14 @@ const buildPromptSection = ({
64
65
 
65
66
  const lines = ["## Memory Recall", toolGuidance];
66
67
 
68
+ lines.push(
69
+ "**Context extraction:** When calling memory_search or memory_get, " +
70
+ "you MUST fill in the agent_id, sender_id, conversation_type, and group_id parameters " +
71
+ "by extracting them from the UntrustedContext section in this conversation. " +
72
+ "Look for lines like `agent_id: xxx`, `sender_id: xxx`, `conversation_type: xxx`, `group_id: xxx`. " +
73
+ "If a field is not present, omit it."
74
+ );
75
+
67
76
  if (citationsMode === "off") {
68
77
  lines.push(
69
78
  "Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks."
@@ -155,13 +164,15 @@ export default definePluginEntry({
155
164
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
156
165
  api.registerTool(
157
166
  (ctx: any) => {
158
- const identity = resolveIdentity(ctx.sessionKey);
159
- const agentId = extractAgentId(ctx.sessionKey);
167
+ // fallback: 如果 sessionKey 仍可用,保留兼容
168
+ const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
169
+ const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
160
170
 
161
171
  return {
162
172
  name: "memory_search",
163
173
  description:
164
- "Semantically search MEMORY.md and memory/**/*.md for relevant memories.",
174
+ "Semantically search MEMORY.md and memory/**/*.md for relevant memories. " +
175
+ "You MUST extract agent_id, sender_id, conversation_type, group_id from the UntrustedContext in this conversation and pass them as parameters.",
165
176
  parameters: {
166
177
  type: "object" as const,
167
178
  properties: {
@@ -169,6 +180,22 @@ export default definePluginEntry({
169
180
  type: "string" as const,
170
181
  description: "The search query",
171
182
  },
183
+ agent_id: {
184
+ type: "string" as const,
185
+ description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')",
186
+ },
187
+ sender_id: {
188
+ type: "string" as const,
189
+ description: "The sender_id from UntrustedContext",
190
+ },
191
+ conversation_type: {
192
+ type: "string" as const,
193
+ description: "The conversation_type from UntrustedContext: 'direct' or 'group'",
194
+ },
195
+ group_id: {
196
+ type: "string" as const,
197
+ description: "The group_id from UntrustedContext (only present in group chats)",
198
+ },
172
199
  maxResults: {
173
200
  type: "number" as const,
174
201
  description: "Maximum number of results to return",
@@ -183,7 +210,15 @@ export default definePluginEntry({
183
210
 
184
211
  async execute(
185
212
  toolCallId: string,
186
- params: { query?: string; maxResults?: number; minScore?: number }
213
+ params: {
214
+ query?: string;
215
+ agent_id?: string;
216
+ sender_id?: string;
217
+ conversation_type?: string;
218
+ group_id?: string;
219
+ maxResults?: number;
220
+ minScore?: number;
221
+ }
187
222
  ) {
188
223
  const query = params.query || "";
189
224
  if (!query) {
@@ -192,8 +227,23 @@ export default definePluginEntry({
192
227
  };
193
228
  }
194
229
 
230
+ // 优先用 LLM 传参,fallback 到 sessionKey 解析
231
+ const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
232
+ const identity = (params.sender_id || params.conversation_type || params.group_id)
233
+ ? resolveIdentityFromParams({
234
+ sender_id: params.sender_id,
235
+ conversation_type: params.conversation_type,
236
+ group_id: params.group_id,
237
+ })
238
+ : (fallbackIdentity || resolveIdentityFromParams({}));
239
+
240
+ console.log(
241
+ `[memory-search-plugin] search: agent_id=${agentId} scene=${identity.scene} ` +
242
+ `user_id=${identity.user_id} group_id=${identity.group_id} ` +
243
+ `(source=${params.sender_id ? "llm_params" : "fallback"})`
244
+ );
245
+
195
246
  try {
196
- // 按后端 SearchRequest 格式发送请求
197
247
  const data = await gateway.callGatewaySearch({
198
248
  agent_id: agentId,
199
249
  user_id: identity.user_id,
@@ -218,7 +268,6 @@ export default definePluginEntry({
218
268
  } catch (err: any) {
219
269
  console.error("[memory-search-plugin] search failed:", err.message);
220
270
 
221
- // ── 降级:尝试用原始的 memory_search ──
222
271
  try {
223
272
  const fallbackTool =
224
273
  api.runtime.tools.createMemorySearchTool({
@@ -229,10 +278,10 @@ export default definePluginEntry({
229
278
  console.warn(
230
279
  "[memory-search-plugin] falling back to local memory_search"
231
280
  );
232
- return await fallbackTool.execute(toolCallId, params);
281
+ return await fallbackTool.execute(toolCallId, { query, maxResults: params.maxResults, minScore: params.minScore });
233
282
  }
234
283
  } catch {
235
- // 降级也失败,返回错误信息
284
+ // 降级也失败
236
285
  }
237
286
 
238
287
  return {
@@ -257,8 +306,8 @@ export default definePluginEntry({
257
306
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
258
307
  api.registerTool(
259
308
  (ctx: any) => {
260
- const identity = resolveIdentity(ctx.sessionKey);
261
- const agentId = extractAgentId(ctx.sessionKey);
309
+ const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
310
+ const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
262
311
 
263
312
  // 获取原始 memory_get 实现,用于非 MEMORY 路径的 passthrough
264
313
  const originalGetTool = api.runtime.tools.createMemoryGetTool({
@@ -270,7 +319,8 @@ export default definePluginEntry({
270
319
  name: "memory_get",
271
320
  description:
272
321
  "Read a memory file or workspace file. " +
273
- "Supports reading MEMORY.md, memory/**/*.md, and other workspace files.",
322
+ "Supports reading MEMORY.md, memory/**/*.md, and other workspace files. " +
323
+ "You MUST extract agent_id, sender_id, conversation_type, group_id from the UntrustedContext in this conversation and pass them as parameters.",
274
324
  parameters: {
275
325
  type: "object" as const,
276
326
  properties: {
@@ -279,6 +329,22 @@ export default definePluginEntry({
279
329
  description:
280
330
  "Relative path to the file (e.g. MEMORY.md, memory/notes.md, SOUL.md)",
281
331
  },
332
+ agent_id: {
333
+ type: "string" as const,
334
+ description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')",
335
+ },
336
+ sender_id: {
337
+ type: "string" as const,
338
+ description: "The sender_id from UntrustedContext",
339
+ },
340
+ conversation_type: {
341
+ type: "string" as const,
342
+ description: "The conversation_type from UntrustedContext: 'direct' or 'group'",
343
+ },
344
+ group_id: {
345
+ type: "string" as const,
346
+ description: "The group_id from UntrustedContext (only present in group chats)",
347
+ },
282
348
  from: {
283
349
  type: "number" as const,
284
350
  description: "Starting line number (1-indexed)",
@@ -293,7 +359,15 @@ export default definePluginEntry({
293
359
 
294
360
  async execute(
295
361
  toolCallId: string,
296
- params: { path?: string; from?: number; lines?: number }
362
+ params: {
363
+ path?: string;
364
+ agent_id?: string;
365
+ sender_id?: string;
366
+ conversation_type?: string;
367
+ group_id?: string;
368
+ from?: number;
369
+ lines?: number;
370
+ }
297
371
  ) {
298
372
  const path = params.path || "";
299
373
  if (!path) {
@@ -304,9 +378,21 @@ export default definePluginEntry({
304
378
 
305
379
  // ── 路径分流 ──
306
380
  if (isMemoryPath(path)) {
307
- // MEMORY 路径 Gateway(带 ACL)
381
+ const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
382
+ const identity = (params.sender_id || params.conversation_type || params.group_id)
383
+ ? resolveIdentityFromParams({
384
+ sender_id: params.sender_id,
385
+ conversation_type: params.conversation_type,
386
+ group_id: params.group_id,
387
+ })
388
+ : (fallbackIdentity || resolveIdentityFromParams({}));
389
+
390
+ console.log(
391
+ `[memory-search-plugin] get: path=${path} agent_id=${agentId} scene=${identity.scene} ` +
392
+ `user_id=${identity.user_id} (source=${params.sender_id ? "llm_params" : "fallback"})`
393
+ );
394
+
308
395
  try {
309
- // 按后端 GetRequest 格式发送请求
310
396
  const data = await gateway.callGatewayGet({
311
397
  agent_id: agentId,
312
398
  user_id: identity.user_id,
@@ -324,7 +410,6 @@ export default definePluginEntry({
324
410
  } catch (err: any) {
325
411
  console.error("[memory-search-plugin] get failed:", err.message);
326
412
 
327
- // 403 明确拒绝 → 不降级,直接返回拒绝
328
413
  if (err.message?.includes('403')) {
329
414
  return {
330
415
  content: [
@@ -336,13 +421,12 @@ export default definePluginEntry({
336
421
  };
337
422
  }
338
423
 
339
- // 其他错误(网络不通等)→ 降级到本地文件读取
340
424
  if (originalGetTool) {
341
425
  console.warn(
342
426
  "[memory-search-plugin] falling back to local memory_get for:",
343
427
  path
344
428
  );
345
- return await originalGetTool.execute(toolCallId, params);
429
+ return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
346
430
  }
347
431
 
348
432
  return {
@@ -356,10 +440,8 @@ export default definePluginEntry({
356
440
  };
357
441
  }
358
442
  } else {
359
- // 非 MEMORY 路径 → passthrough 给原始实现
360
- // 这样 SOUL.md、rules.md 等文件的读取行为完全不变
361
443
  if (originalGetTool) {
362
- return await originalGetTool.execute(toolCallId, params);
444
+ return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
363
445
  }
364
446
 
365
447
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-search-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "files": [