memory-search-plugin 0.8.0 → 1.1.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.
- package/gateway-client.ts +45 -64
- package/identity.ts +28 -88
- package/index.js +136 -273
- package/index.ts +160 -330
- package/openclaw.plugin.json +13 -16
- package/package.json +1 -1
package/gateway-client.ts
CHANGED
|
@@ -1,39 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory Search Gateway HTTP 调用封装
|
|
3
|
-
*
|
|
4
|
-
* 封装与外部 Memory Search Gateway 服务的通信。
|
|
5
|
-
* 请求/响应格式与后端 app/models.py 完全对齐。
|
|
6
|
-
* Gateway 地址从 openclaw.json plugins.entries.memory-search-plugin.config.gatewayUrl 获取。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
import type { Scene } from "./identity";
|
|
10
2
|
|
|
11
|
-
// ──
|
|
12
|
-
|
|
13
|
-
export type MemoryType = "owner" | "peer" | "group" | "knowledge";
|
|
3
|
+
// ── Search ──
|
|
14
4
|
|
|
15
5
|
export interface MemorySearchRequest {
|
|
6
|
+
owner_id: string;
|
|
7
|
+
sender_id: string;
|
|
16
8
|
agent_id: string;
|
|
17
|
-
user_id: string;
|
|
18
9
|
query: string;
|
|
19
10
|
scene: Scene;
|
|
20
11
|
group_id?: string | null;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
threshold?: number; // 0.0-1.0, default 0.3
|
|
12
|
+
limit?: number;
|
|
13
|
+
threshold?: number;
|
|
24
14
|
}
|
|
25
15
|
|
|
26
16
|
export interface MemorySearchResult {
|
|
27
17
|
id: string;
|
|
28
18
|
content: string;
|
|
29
19
|
score: number;
|
|
30
|
-
memory_type:
|
|
31
|
-
source_file?: string;
|
|
32
|
-
created_at?: string;
|
|
33
|
-
agent_id?: string;
|
|
34
|
-
peer_id?: string;
|
|
20
|
+
memory_type: "owner" | "group" | "knowledge";
|
|
35
21
|
group_id?: string;
|
|
36
22
|
kb_id?: string;
|
|
23
|
+
created_at?: string;
|
|
37
24
|
}
|
|
38
25
|
|
|
39
26
|
export interface MemorySearchResponse {
|
|
@@ -43,23 +30,34 @@ export interface MemorySearchResponse {
|
|
|
43
30
|
steps: string[];
|
|
44
31
|
}
|
|
45
32
|
|
|
33
|
+
// ── Get (keyword search on memory_chat_logs) ──
|
|
34
|
+
|
|
46
35
|
export interface MemoryGetRequest {
|
|
36
|
+
owner_id: string;
|
|
37
|
+
sender_id: string;
|
|
47
38
|
agent_id: string;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
scene?: Scene;
|
|
39
|
+
keyword: string;
|
|
40
|
+
scene: Scene;
|
|
51
41
|
group_id?: string | null;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
message_type?: "owner" | "group";
|
|
43
|
+
limit?: number;
|
|
44
|
+
offset?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MemoryGetMessage {
|
|
48
|
+
id: string;
|
|
49
|
+
formatted_text: string;
|
|
50
|
+
message_type: "owner" | "group";
|
|
51
|
+
group_id?: string | null;
|
|
52
|
+
message_ts: string;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
export interface MemoryGetResponse {
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
messages: MemoryGetMessage[];
|
|
57
|
+
total: number;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
// ──
|
|
60
|
+
// ── Client ──
|
|
63
61
|
|
|
64
62
|
export interface GatewayClientConfig {
|
|
65
63
|
gatewayUrl: string;
|
|
@@ -73,43 +71,26 @@ export interface GatewayClient {
|
|
|
73
71
|
|
|
74
72
|
export function createGatewayClient(config: GatewayClientConfig): GatewayClient {
|
|
75
73
|
const baseUrl = config.gatewayUrl.replace(/\/+$/, "");
|
|
76
|
-
const
|
|
74
|
+
const headers: Record<string, string> = {
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
...(config.gatewayToken ? { Authorization: `Bearer ${config.gatewayToken}` } : {}),
|
|
77
|
+
};
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
async function post<T>(path: string, body: unknown): Promise<T> {
|
|
80
|
+
const resp = await fetch(`${baseUrl}${path}`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers,
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
});
|
|
85
|
+
if (!resp.ok) {
|
|
86
|
+
const text = await resp.text().catch(() => "");
|
|
87
|
+
throw new Error(`Gateway ${path} failed: ${resp.status} ${text}`);
|
|
88
|
+
}
|
|
89
|
+
return resp.json() as Promise<T>;
|
|
90
|
+
}
|
|
81
91
|
|
|
82
92
|
return {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const response = await fetch(url, {
|
|
86
|
-
method: "POST",
|
|
87
|
-
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
88
|
-
body: JSON.stringify(options),
|
|
89
|
-
});
|
|
90
|
-
if (!response.ok) {
|
|
91
|
-
const errorBody = await response.text().catch(() => "");
|
|
92
|
-
throw new Error(
|
|
93
|
-
`Gateway search failed: ${response.status} ${response.statusText} ${errorBody}`
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
return response.json() as Promise<MemorySearchResponse>;
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
async callGatewayGet(options) {
|
|
100
|
-
const url = `${baseUrl}/api/memory/get`;
|
|
101
|
-
const response = await fetch(url, {
|
|
102
|
-
method: "POST",
|
|
103
|
-
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
104
|
-
body: JSON.stringify(options),
|
|
105
|
-
});
|
|
106
|
-
if (!response.ok) {
|
|
107
|
-
const errorBody = await response.text().catch(() => "");
|
|
108
|
-
throw new Error(
|
|
109
|
-
`Gateway get failed: ${response.status} ${response.statusText} ${errorBody}`
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
return response.json() as Promise<MemoryGetResponse>;
|
|
113
|
-
},
|
|
93
|
+
callGatewaySearch: (opts) => post("/api/memory/search", opts),
|
|
94
|
+
callGatewayGet: (opts) => post("/api/memory/get", opts),
|
|
114
95
|
};
|
|
115
96
|
}
|
package/identity.ts
CHANGED
|
@@ -1,119 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 身份解析模块
|
|
2
|
+
* 身份解析模块 — 仅 owner/group/knowledge 三种场景
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* conversation_type = "group" → scene = "group"
|
|
5
|
+
* conversation_type = "direct" → scene = "owner"
|
|
6
|
+
* knowledge 场景由特定工具调用或 LLM 判断触发
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 输出:
|
|
11
|
-
* - scene: 对应后端 SearchRequest.scene(private_own / private_other / group)
|
|
12
|
-
* - user_id: 当前对话用户的 ID
|
|
13
|
-
* - group_id: 群聊时的群 ID(仅 group 场景)
|
|
8
|
+
* owner_id 用于 SQL 数据隔离,替代 release_name。
|
|
9
|
+
* 不依赖任何环境变量兜底。
|
|
14
10
|
*/
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
export type Scene =
|
|
18
|
-
| "private_own"
|
|
19
|
-
| "private_other"
|
|
20
|
-
| "group"
|
|
21
|
-
| "knowledge"
|
|
22
|
-
| "admin";
|
|
12
|
+
export type Scene = "owner" | "group" | "knowledge";
|
|
23
13
|
|
|
24
|
-
/** 解析后的身份信息,与后端 SearchRequest 的 user_id / scene / group_id 对齐 */
|
|
25
14
|
export interface ResolvedIdentity {
|
|
26
15
|
scene: Scene;
|
|
27
|
-
|
|
16
|
+
sender_id: string;
|
|
17
|
+
owner_id: string;
|
|
28
18
|
group_id: string | null;
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
/** LLM 传入的身份相关参数(来自 UntrustedContext) */
|
|
32
21
|
export interface IdentityParams {
|
|
33
22
|
sender_id?: string;
|
|
34
|
-
|
|
23
|
+
owner_id?: string;
|
|
24
|
+
conversation_type?: string;
|
|
35
25
|
group_id?: string;
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
|
|
39
|
-
* 从 LLM 工具参数解析身份 + 场景
|
|
40
|
-
*
|
|
41
|
-
* owner 判断逻辑与旧版一致:通过环境变量 OPENCLAW_GATEWAY_TOKEN 获取 owner_id,
|
|
42
|
-
* 再与 sender_id 比对。
|
|
43
|
-
*
|
|
44
|
-
* 场景映射:
|
|
45
|
-
* conversation_type="group" → scene = "group"
|
|
46
|
-
* conversation_type="direct" + isOwner(sender_id) → scene = "private_own"
|
|
47
|
-
* conversation_type="direct" + !isOwner → scene = "private_other"
|
|
48
|
-
*/
|
|
49
|
-
export function resolveIdentityFromParams(params: IdentityParams): ResolvedIdentity {
|
|
50
|
-
// ── 测试模式:环境变量覆盖 ──────────────────────────
|
|
28
|
+
export function resolveIdentity(params: IdentityParams): ResolvedIdentity {
|
|
51
29
|
const testScene = process.env.MEMORY_GATEWAY_TEST_SCENE as Scene | undefined;
|
|
52
30
|
if (testScene) {
|
|
53
|
-
|
|
31
|
+
return {
|
|
54
32
|
scene: testScene,
|
|
55
|
-
|
|
33
|
+
sender_id: process.env.MEMORY_GATEWAY_TEST_USER_ID || "test_user",
|
|
34
|
+
owner_id: process.env.MEMORY_GATEWAY_TEST_OWNER_ID || "test_owner",
|
|
56
35
|
group_id: process.env.MEMORY_GATEWAY_TEST_GROUP_ID || null,
|
|
57
36
|
};
|
|
58
|
-
console.log("[memory-search-plugin] TEST MODE identity:", JSON.stringify(result));
|
|
59
|
-
return result;
|
|
60
37
|
}
|
|
61
38
|
|
|
62
|
-
const
|
|
39
|
+
const senderId = params.sender_id?.trim() || "unknown";
|
|
40
|
+
const ownerId = params.owner_id?.trim() || "";
|
|
63
41
|
const convType = (params.conversation_type || "direct").trim().toLowerCase();
|
|
42
|
+
const groupId = params.group_id?.trim() || null;
|
|
64
43
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 };
|
|
44
|
+
if (!ownerId) {
|
|
45
|
+
console.warn(
|
|
46
|
+
"[identity] owner_id is empty — LLM failed to extract from UntrustedContext. " +
|
|
47
|
+
"owner queries will be skipped to prevent cross-user leak."
|
|
48
|
+
);
|
|
77
49
|
}
|
|
78
50
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// ── 向后兼容:保留旧函数签名,sessionKey 仍可用时作为 fallback ──
|
|
83
|
-
|
|
84
|
-
export function resolveIdentity(sessionKey: string | undefined): ResolvedIdentity {
|
|
85
|
-
const sk = sessionKey ?? "";
|
|
86
|
-
const sessionPart = sk.split(":").pop() || "";
|
|
87
|
-
|
|
88
|
-
const groupMatch = sessionPart.match(
|
|
89
|
-
/user_(.+?)_lobster_(.+?)_group_(.+?)_release_(.+)$/
|
|
90
|
-
);
|
|
91
|
-
if (groupMatch) {
|
|
92
|
-
return { scene: "group", user_id: groupMatch[1], group_id: groupMatch[3] };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const peerMatch = sessionPart.match(
|
|
96
|
-
/user_(.+?)_lobster_(.+?)_release_(.+)$/
|
|
97
|
-
);
|
|
98
|
-
if (peerMatch) {
|
|
99
|
-
const userId = peerMatch[1];
|
|
100
|
-
if (isOwner(userId)) {
|
|
101
|
-
return { scene: "private_own", user_id: userId, group_id: null };
|
|
51
|
+
if (convType === "group") {
|
|
52
|
+
if (!groupId) {
|
|
53
|
+
console.warn("[identity] group scene but group_id is empty");
|
|
102
54
|
}
|
|
103
|
-
return { scene: "
|
|
55
|
+
return { scene: "group", sender_id: senderId, owner_id: ownerId, group_id: groupId };
|
|
104
56
|
}
|
|
105
57
|
|
|
106
|
-
return { scene: "
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function isOwner(userId: string): boolean {
|
|
110
|
-
const token = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
111
|
-
if (!token) return false;
|
|
112
|
-
const ownerUserId = token.split("-").pop() || "";
|
|
113
|
-
return ownerUserId !== "" && ownerUserId === userId;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function extractAgentId(sessionKey: string | undefined): string {
|
|
117
|
-
const parts = (sessionKey || "").split(":");
|
|
118
|
-
return parts.length >= 2 ? parts[1] : "main";
|
|
58
|
+
return { scene: "owner", sender_id: senderId, owner_id: ownerId, group_id: null };
|
|
119
59
|
}
|