memory-search-plugin 0.2.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 +113 -0
- package/identity.ts +106 -0
- package/index.ts +390 -0
- package/openclaw.plugin.json +22 -0
- package/package.json +20 -0
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
import type { Scene } from "./identity";
|
|
10
|
+
|
|
11
|
+
// ── 类型定义(与后端 app/models.py 对齐)──────────────
|
|
12
|
+
|
|
13
|
+
export type MemoryType = "owner" | "peer" | "group" | "knowledge";
|
|
14
|
+
|
|
15
|
+
export interface MemorySearchRequest {
|
|
16
|
+
agent_id: string;
|
|
17
|
+
user_id: string;
|
|
18
|
+
query: string;
|
|
19
|
+
scene: Scene;
|
|
20
|
+
group_id?: string | null;
|
|
21
|
+
limit?: number; // 1-100, default 20
|
|
22
|
+
threshold?: number; // 0.0-1.0, default 0.3
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MemorySearchResult {
|
|
26
|
+
id: string;
|
|
27
|
+
content: string;
|
|
28
|
+
score: number;
|
|
29
|
+
memory_type: MemoryType;
|
|
30
|
+
source_file?: string;
|
|
31
|
+
created_at?: string;
|
|
32
|
+
agent_id?: string;
|
|
33
|
+
peer_id?: string;
|
|
34
|
+
group_id?: string;
|
|
35
|
+
kb_id?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MemorySearchResponse {
|
|
39
|
+
results: MemorySearchResult[];
|
|
40
|
+
total: number;
|
|
41
|
+
scene: Scene;
|
|
42
|
+
steps: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MemoryGetRequest {
|
|
46
|
+
agent_id: string;
|
|
47
|
+
user_id: string;
|
|
48
|
+
path: string;
|
|
49
|
+
scene?: Scene;
|
|
50
|
+
group_id?: string | null;
|
|
51
|
+
from?: number;
|
|
52
|
+
lines?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MemoryGetResponse {
|
|
56
|
+
text: string;
|
|
57
|
+
path: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Gateway Client ─────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export interface GatewayClientConfig {
|
|
63
|
+
gatewayUrl: string;
|
|
64
|
+
gatewayToken?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface GatewayClient {
|
|
68
|
+
callGatewaySearch(options: MemorySearchRequest): Promise<MemorySearchResponse>;
|
|
69
|
+
callGatewayGet(options: MemoryGetRequest): Promise<MemoryGetResponse>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createGatewayClient(config: GatewayClientConfig): GatewayClient {
|
|
73
|
+
const baseUrl = config.gatewayUrl.replace(/\/+$/, "");
|
|
74
|
+
const token = config.gatewayToken || "";
|
|
75
|
+
|
|
76
|
+
const authHeaders = token
|
|
77
|
+
? { Authorization: `Bearer ${token}` }
|
|
78
|
+
: {};
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
async callGatewaySearch(options) {
|
|
82
|
+
const url = `${baseUrl}/api/memory/search`;
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
86
|
+
body: JSON.stringify(options),
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const errorBody = await response.text().catch(() => "");
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Gateway search failed: ${response.status} ${response.statusText} ${errorBody}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return response.json() as Promise<MemorySearchResponse>;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async callGatewayGet(options) {
|
|
98
|
+
const url = `${baseUrl}/api/memory/get`;
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
102
|
+
body: JSON.stringify(options),
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
const errorBody = await response.text().catch(() => "");
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Gateway get failed: ${response.status} ${response.statusText} ${errorBody}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return response.json() as Promise<MemoryGetResponse>;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
package/identity.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 身份解析模块
|
|
3
|
+
*
|
|
4
|
+
* 从 sessionKey 中解析出当前会话的身份类型和场景。
|
|
5
|
+
* sessionKey 格式参考 V1.4 文档和 memory-privacy/identity.ts
|
|
6
|
+
*
|
|
7
|
+
* 输出:
|
|
8
|
+
* - scene: 对应后端 SearchRequest.scene(private_own / private_other / group)
|
|
9
|
+
* - user_id: 当前对话用户的 ID
|
|
10
|
+
* - group_id: 群聊时的群 ID(仅 group 场景)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 后端 models.py 定义的五种场景 */
|
|
14
|
+
export type Scene =
|
|
15
|
+
| "private_own"
|
|
16
|
+
| "private_other"
|
|
17
|
+
| "group"
|
|
18
|
+
| "knowledge"
|
|
19
|
+
| "admin";
|
|
20
|
+
|
|
21
|
+
/** 解析后的身份信息,与后端 SearchRequest 的 user_id / scene / group_id 对齐 */
|
|
22
|
+
export interface ResolvedIdentity {
|
|
23
|
+
scene: Scene;
|
|
24
|
+
user_id: string;
|
|
25
|
+
group_id: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 从 sessionKey 解析身份 + 场景
|
|
30
|
+
*
|
|
31
|
+
* sessionKey 末段格式:
|
|
32
|
+
* owner/peer: user_{userId}_lobster_{lobsterId}_release_{version}
|
|
33
|
+
* group: user_{userId}_lobster_{lobsterId}_group_{groupId}_release_{version}
|
|
34
|
+
*
|
|
35
|
+
* 场景映射:
|
|
36
|
+
* owner(私聊自己的 agent)→ scene = "private_own"
|
|
37
|
+
* peer (私聊别人的 agent)→ scene = "private_other"
|
|
38
|
+
* group(群聊中 agent 回复)→ scene = "group"
|
|
39
|
+
*/
|
|
40
|
+
export function resolveIdentity(sessionKey: string | undefined): ResolvedIdentity {
|
|
41
|
+
// ── 测试模式:环境变量覆盖 ──────────────────────────
|
|
42
|
+
// 设置 MEMORY_GATEWAY_TEST_SCENE 即可跳过 sessionKey 解析,
|
|
43
|
+
// 方便在没有真实 IM 会话时测试插件。
|
|
44
|
+
const testScene = process.env.MEMORY_GATEWAY_TEST_SCENE as Scene | undefined;
|
|
45
|
+
if (testScene) {
|
|
46
|
+
const result: ResolvedIdentity = {
|
|
47
|
+
scene: testScene,
|
|
48
|
+
user_id: process.env.MEMORY_GATEWAY_TEST_USER_ID || "owner_A",
|
|
49
|
+
group_id: process.env.MEMORY_GATEWAY_TEST_GROUP_ID || null,
|
|
50
|
+
};
|
|
51
|
+
console.log("[memory-search-plugin] TEST MODE identity:", JSON.stringify(result));
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── 正常模式:从 sessionKey 解析 ──────────────────────
|
|
56
|
+
const sk = sessionKey ?? "";
|
|
57
|
+
const sessionPart = sk.split(":").pop() || "";
|
|
58
|
+
|
|
59
|
+
// 1. 群聊: 包含 _group_
|
|
60
|
+
const groupMatch = sessionPart.match(
|
|
61
|
+
/user_(.+?)_lobster_(.+?)_group_(.+?)_release_(.+)$/
|
|
62
|
+
);
|
|
63
|
+
if (groupMatch) {
|
|
64
|
+
return {
|
|
65
|
+
scene: "group",
|
|
66
|
+
user_id: groupMatch[1],
|
|
67
|
+
group_id: groupMatch[3],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. 私聊 (owner 或 peer)
|
|
72
|
+
const peerMatch = sessionPart.match(
|
|
73
|
+
/user_(.+?)_lobster_(.+?)_release_(.+)$/
|
|
74
|
+
);
|
|
75
|
+
if (peerMatch) {
|
|
76
|
+
const userId = peerMatch[1];
|
|
77
|
+
if (isOwner(userId)) {
|
|
78
|
+
return { scene: "private_own", user_id: userId, group_id: null };
|
|
79
|
+
}
|
|
80
|
+
return { scene: "private_other", user_id: userId, group_id: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. 兜底: 安全起见当作 peer(private_other)
|
|
84
|
+
return { scene: "private_other", user_id: "unknown", group_id: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 判断是否是 owner
|
|
89
|
+
* 通过环境变量 OPENCLAW_GATEWAY_TOKEN 判断
|
|
90
|
+
* token 格式: oc-user-{ownerUserId}
|
|
91
|
+
*/
|
|
92
|
+
function isOwner(userId: string): boolean {
|
|
93
|
+
const token = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
94
|
+
if (!token) return false;
|
|
95
|
+
const ownerUserId = token.split("-").pop() || "";
|
|
96
|
+
return ownerUserId !== "" && ownerUserId === userId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 从 sessionKey 提取 agent_id
|
|
101
|
+
* sessionKey 格式: agent:{agentId}:palz:...
|
|
102
|
+
*/
|
|
103
|
+
export function extractAgentId(sessionKey: string | undefined): string {
|
|
104
|
+
const parts = (sessionKey || "").split(":");
|
|
105
|
+
return parts.length >= 2 ? parts[1] : "main";
|
|
106
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-gateway 插件
|
|
3
|
+
*
|
|
4
|
+
* 替换 OpenClaw 默认的 memory-core 插件(通过 memory slot)
|
|
5
|
+
* - memory_search → 路由到外部 Memory Gateway Search API
|
|
6
|
+
* - memory_get → 路径分流:MEMORY 路径走 Gateway,其他路径走原始实现
|
|
7
|
+
*
|
|
8
|
+
* 基于:
|
|
9
|
+
* - OpenClaw_记忆隐私分阶段改造技术方案_V1.md(第 5.1 / 6.1 / 7.2 节)
|
|
10
|
+
* - 存储方案设计-V2-虾生态2.md(第 4 节可见性规则)
|
|
11
|
+
* - 三种场景-存储与查询方案.md(三场景可见性 + 存储 payload 格式)
|
|
12
|
+
*
|
|
13
|
+
* 场景可见性:
|
|
14
|
+
* private_own: owner + peer(全部) + group(并集) + knowledge(用户ACL)
|
|
15
|
+
* private_other: peer(仅自己) + group(交集) + knowledge(用户ACL)
|
|
16
|
+
* group: group(仅当前群) + knowledge(群ACL)
|
|
17
|
+
* knowledge: knowledge(用户ACL)
|
|
18
|
+
* admin: 全部
|
|
19
|
+
*
|
|
20
|
+
* 原理:
|
|
21
|
+
* memory-core 只注册了 memory_search 和 memory_get 两个工具。
|
|
22
|
+
* append / insert / replace 等文件编辑工具不属于 memory slot,替换后不受影响。
|
|
23
|
+
* 原始的 memory_get 实现可以通过 api.runtime.tools.createMemoryGetTool() 获取,
|
|
24
|
+
* 用于非 MEMORY 路径的 passthrough。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
28
|
+
import { createGatewayClient } from "./gateway-client";
|
|
29
|
+
import { resolveIdentity, extractAgentId } from "./identity";
|
|
30
|
+
import type { MemorySearchResult } from "./gateway-client";
|
|
31
|
+
|
|
32
|
+
// ── Prompt Section ──────────────────────────────────────
|
|
33
|
+
// 告诉 LLM 如何使用 memory 工具(和 memory-core 的逻辑一致)
|
|
34
|
+
|
|
35
|
+
const buildPromptSection = ({
|
|
36
|
+
availableTools,
|
|
37
|
+
citationsMode,
|
|
38
|
+
}: {
|
|
39
|
+
availableTools: Set<string>;
|
|
40
|
+
citationsMode: string;
|
|
41
|
+
}) => {
|
|
42
|
+
const hasMemorySearch = availableTools.has("memory_search");
|
|
43
|
+
const hasMemoryGet = availableTools.has("memory_get");
|
|
44
|
+
if (!hasMemorySearch && !hasMemoryGet) return [];
|
|
45
|
+
|
|
46
|
+
let toolGuidance: string;
|
|
47
|
+
if (hasMemorySearch && hasMemoryGet) {
|
|
48
|
+
toolGuidance =
|
|
49
|
+
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: " +
|
|
50
|
+
"run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. " +
|
|
51
|
+
"If low confidence after search, say you checked.";
|
|
52
|
+
} else if (hasMemorySearch) {
|
|
53
|
+
toolGuidance =
|
|
54
|
+
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: " +
|
|
55
|
+
"run memory_search on MEMORY.md + memory/*.md and answer from the matching results. " +
|
|
56
|
+
"If low confidence after search, say you checked.";
|
|
57
|
+
} else {
|
|
58
|
+
toolGuidance =
|
|
59
|
+
"Before answering anything about prior work, decisions, dates, people, preferences, or todos " +
|
|
60
|
+
"that already point to a specific memory file or note: " +
|
|
61
|
+
"run memory_get to pull only the needed lines. " +
|
|
62
|
+
"If low confidence after reading them, say you checked.";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lines = ["## Memory Recall", toolGuidance];
|
|
66
|
+
|
|
67
|
+
if (citationsMode === "off") {
|
|
68
|
+
lines.push(
|
|
69
|
+
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks."
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
lines.push(
|
|
73
|
+
"Citations: include Source: <path#line> when it helps the user verify memory snippets."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
lines.push("");
|
|
78
|
+
return lines;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ── 路径判断 ───────────────────────────────────────────���
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 判断是否是 MEMORY 路径
|
|
85
|
+
* V1 方案第一阶段接管的路径:MEMORY.md 和 memory/**\/*.md
|
|
86
|
+
*/
|
|
87
|
+
function isMemoryPath(path: string): boolean {
|
|
88
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
89
|
+
return (
|
|
90
|
+
normalized === "MEMORY.md" ||
|
|
91
|
+
normalized.startsWith("memory/") ||
|
|
92
|
+
normalized.endsWith("/MEMORY.md") ||
|
|
93
|
+
normalized.includes("/memory/")
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 结果格式化 ──────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 将 Gateway 搜索结果格式化为 LLM 友好的文本
|
|
101
|
+
* 包含 memory_type、来源文件和相似度信息,方便 LLM 做 citation
|
|
102
|
+
* 字段对应 V2 存储方案的 payload 结构
|
|
103
|
+
*
|
|
104
|
+
* 注意:后端返回的相似度字段是 score(不是 similarity)
|
|
105
|
+
*/
|
|
106
|
+
function formatSearchResults(results: MemorySearchResult[]): string {
|
|
107
|
+
if (results.length === 0) {
|
|
108
|
+
return "No relevant memories found.";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return results
|
|
112
|
+
.map((r, i) => {
|
|
113
|
+
// 构建来源描述:memory_type + 归属对象
|
|
114
|
+
let sourceDesc = r.memory_type;
|
|
115
|
+
if (r.memory_type === "peer" && r.peer_id) {
|
|
116
|
+
sourceDesc = `peer/${r.peer_id}`;
|
|
117
|
+
} else if (r.memory_type === "group" && r.group_id) {
|
|
118
|
+
sourceDesc = `group/${r.group_id}`;
|
|
119
|
+
} else if (r.memory_type === "knowledge" && r.kb_id) {
|
|
120
|
+
sourceDesc = `knowledge/${r.kb_id}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const filePart = r.source_file ? ` | ${r.source_file}` : "";
|
|
124
|
+
const header = `[${i + 1}] (${sourceDesc}, score: ${r.score.toFixed(2)}${filePart})`;
|
|
125
|
+
return `${header}\n${r.content}`;
|
|
126
|
+
})
|
|
127
|
+
.join("\n\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── 插件定义 ────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export default definePluginEntry({
|
|
133
|
+
id: "memory-search-plugin",
|
|
134
|
+
name: "Memory Search Plugin",
|
|
135
|
+
description: "Routes memory to external Memory Search Gateway with ACL",
|
|
136
|
+
kind: "memory" as const,
|
|
137
|
+
|
|
138
|
+
register(api: any) {
|
|
139
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
140
|
+
// 0. 从 openclaw.json pluginConfig 读取 Gateway 配置
|
|
141
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
142
|
+
const gatewayUrl = (api.pluginConfig?.gatewayUrl as string | undefined) || "http://localhost:18790";
|
|
143
|
+
const gatewayToken = (api.pluginConfig?.gatewayToken as string | undefined) || "";
|
|
144
|
+
console.log("[memory-search-plugin] gatewayUrl:", gatewayUrl);
|
|
145
|
+
|
|
146
|
+
const gateway = createGatewayClient({ gatewayUrl, gatewayToken });
|
|
147
|
+
|
|
148
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
149
|
+
// 1. 注册 prompt section(和 memory-core 一致)
|
|
150
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
151
|
+
api.registerMemoryPromptSection(buildPromptSection);
|
|
152
|
+
|
|
153
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
154
|
+
// 2. 注册 memory_search → 调 Gateway Search API
|
|
155
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
156
|
+
api.registerTool(
|
|
157
|
+
(ctx: any) => {
|
|
158
|
+
const identity = resolveIdentity(ctx.sessionKey);
|
|
159
|
+
const agentId = extractAgentId(ctx.sessionKey);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
name: "memory_search",
|
|
163
|
+
description:
|
|
164
|
+
"Semantically search MEMORY.md and memory/**/*.md for relevant memories.",
|
|
165
|
+
parameters: {
|
|
166
|
+
type: "object" as const,
|
|
167
|
+
properties: {
|
|
168
|
+
query: {
|
|
169
|
+
type: "string" as const,
|
|
170
|
+
description: "The search query",
|
|
171
|
+
},
|
|
172
|
+
maxResults: {
|
|
173
|
+
type: "number" as const,
|
|
174
|
+
description: "Maximum number of results to return",
|
|
175
|
+
},
|
|
176
|
+
minScore: {
|
|
177
|
+
type: "number" as const,
|
|
178
|
+
description: "Minimum similarity score threshold",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ["query"],
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async execute(
|
|
185
|
+
toolCallId: string,
|
|
186
|
+
params: { query?: string; maxResults?: number; minScore?: number }
|
|
187
|
+
) {
|
|
188
|
+
const query = params.query || "";
|
|
189
|
+
if (!query) {
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: "text" as const, text: "No query provided." }],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
// 按后端 SearchRequest 格式发送请求
|
|
197
|
+
const data = await gateway.callGatewaySearch({
|
|
198
|
+
agent_id: agentId,
|
|
199
|
+
user_id: identity.user_id,
|
|
200
|
+
query,
|
|
201
|
+
scene: identity.scene,
|
|
202
|
+
group_id: identity.group_id,
|
|
203
|
+
limit: params.maxResults || 20,
|
|
204
|
+
threshold: params.minScore || 0.3,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const text = formatSearchResults(data.results);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text" as const, text }],
|
|
211
|
+
details: {
|
|
212
|
+
results: data.results,
|
|
213
|
+
total: data.total,
|
|
214
|
+
scene: data.scene,
|
|
215
|
+
steps: data.steps,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
} catch (err: any) {
|
|
219
|
+
console.error("[memory-search-plugin] search failed:", err.message);
|
|
220
|
+
|
|
221
|
+
// ── 降级:尝试用原始的 memory_search ──
|
|
222
|
+
try {
|
|
223
|
+
const fallbackTool =
|
|
224
|
+
api.runtime.tools.createMemorySearchTool({
|
|
225
|
+
config: ctx.config,
|
|
226
|
+
agentSessionKey: ctx.sessionKey,
|
|
227
|
+
});
|
|
228
|
+
if (fallbackTool) {
|
|
229
|
+
console.warn(
|
|
230
|
+
"[memory-search-plugin] falling back to local memory_search"
|
|
231
|
+
);
|
|
232
|
+
return await fallbackTool.execute(toolCallId, params);
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// 降级也失败,返回错误信息
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: "text" as const,
|
|
242
|
+
text: "Memory search temporarily unavailable.",
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
{ names: ["memory_search"] }
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
254
|
+
// 3. 注册 memory_get → 路径分流
|
|
255
|
+
// MEMORY 路径 → Gateway
|
|
256
|
+
// 其他路径 → 原始实现(passthrough)
|
|
257
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
258
|
+
api.registerTool(
|
|
259
|
+
(ctx: any) => {
|
|
260
|
+
const identity = resolveIdentity(ctx.sessionKey);
|
|
261
|
+
const agentId = extractAgentId(ctx.sessionKey);
|
|
262
|
+
|
|
263
|
+
// 获取原始 memory_get 实现,用于非 MEMORY 路径的 passthrough
|
|
264
|
+
const originalGetTool = api.runtime.tools.createMemoryGetTool({
|
|
265
|
+
config: ctx.config,
|
|
266
|
+
agentSessionKey: ctx.sessionKey,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
name: "memory_get",
|
|
271
|
+
description:
|
|
272
|
+
"Read a memory file or workspace file. " +
|
|
273
|
+
"Supports reading MEMORY.md, memory/**/*.md, and other workspace files.",
|
|
274
|
+
parameters: {
|
|
275
|
+
type: "object" as const,
|
|
276
|
+
properties: {
|
|
277
|
+
path: {
|
|
278
|
+
type: "string" as const,
|
|
279
|
+
description:
|
|
280
|
+
"Relative path to the file (e.g. MEMORY.md, memory/notes.md, SOUL.md)",
|
|
281
|
+
},
|
|
282
|
+
from: {
|
|
283
|
+
type: "number" as const,
|
|
284
|
+
description: "Starting line number (1-indexed)",
|
|
285
|
+
},
|
|
286
|
+
lines: {
|
|
287
|
+
type: "number" as const,
|
|
288
|
+
description: "Number of lines to read",
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
required: ["path"],
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
async execute(
|
|
295
|
+
toolCallId: string,
|
|
296
|
+
params: { path?: string; from?: number; lines?: number }
|
|
297
|
+
) {
|
|
298
|
+
const path = params.path || "";
|
|
299
|
+
if (!path) {
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: "text" as const, text: "No path provided." }],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── 路径分流 ──
|
|
306
|
+
if (isMemoryPath(path)) {
|
|
307
|
+
// MEMORY 路径 → 调 Gateway(带 ACL)
|
|
308
|
+
try {
|
|
309
|
+
// 按后端 GetRequest 格式发送请求
|
|
310
|
+
const data = await gateway.callGatewayGet({
|
|
311
|
+
agent_id: agentId,
|
|
312
|
+
user_id: identity.user_id,
|
|
313
|
+
path,
|
|
314
|
+
scene: identity.scene,
|
|
315
|
+
group_id: identity.group_id,
|
|
316
|
+
from: params.from,
|
|
317
|
+
lines: params.lines,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
content: [{ type: "text" as const, text: data.text }],
|
|
322
|
+
details: { path: data.path, text: data.text },
|
|
323
|
+
};
|
|
324
|
+
} catch (err: any) {
|
|
325
|
+
console.error("[memory-search-plugin] get failed:", err.message);
|
|
326
|
+
|
|
327
|
+
// 403 明确拒绝 → 不降级,直接返回拒绝
|
|
328
|
+
if (err.message?.includes('403')) {
|
|
329
|
+
return {
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: "text" as const,
|
|
333
|
+
text: `Access denied: ${path}`,
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 其他错误(网络不通等)→ 降级到本地文件读取
|
|
340
|
+
if (originalGetTool) {
|
|
341
|
+
console.warn(
|
|
342
|
+
"[memory-search-plugin] falling back to local memory_get for:",
|
|
343
|
+
path
|
|
344
|
+
);
|
|
345
|
+
return await originalGetTool.execute(toolCallId, params);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: "text" as const,
|
|
352
|
+
text: `Failed to read ${path}: Gateway unavailable.`,
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
details: { path, text: "", error: err.message },
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
// 非 MEMORY 路径 → passthrough 给原始实现
|
|
360
|
+
// 这样 SOUL.md、rules.md 等文件的读取行为完全不变
|
|
361
|
+
if (originalGetTool) {
|
|
362
|
+
return await originalGetTool.execute(toolCallId, params);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
content: [
|
|
367
|
+
{
|
|
368
|
+
type: "text" as const,
|
|
369
|
+
text: `Cannot read ${path}: memory_get backend not available.`,
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
{ names: ["memory_get"] }
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
381
|
+
// 4. 保留 CLI 命令(和 memory-core 一致)
|
|
382
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
383
|
+
api.registerCli(
|
|
384
|
+
({ program }: any) => {
|
|
385
|
+
api.runtime.tools.registerMemoryCli(program);
|
|
386
|
+
},
|
|
387
|
+
{ commands: ["memory"] }
|
|
388
|
+
);
|
|
389
|
+
},
|
|
390
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "memory-search-plugin",
|
|
3
|
+
"name": "Memory Search Plugin",
|
|
4
|
+
"description": "Routes memory_search to external Memory Search Gateway with ACL, memory_get with path routing",
|
|
5
|
+
"kind": "memory",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {
|
|
11
|
+
"gatewayUrl": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "Memory Search Gateway API 地址,如 http://172.22.39.89:8888"
|
|
14
|
+
},
|
|
15
|
+
"gatewayToken": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Bearer Token,留空则不认证"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": ["gatewayUrl"]
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memory-search-plugin",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"*.ts",
|
|
8
|
+
"openclaw.plugin.json",
|
|
9
|
+
"!*.test.ts"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"registry": "https://registry.npmjs.org/"
|
|
14
|
+
},
|
|
15
|
+
"openclaw": {
|
|
16
|
+
"extensions": [
|
|
17
|
+
"./index.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|