memory-gateway-sync 0.11.0 → 0.13.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/index.js +149 -31
- package/package.json +1 -1
- package/src/identity.ts +48 -0
- package/src/index.ts +121 -22
package/index.js
CHANGED
|
@@ -4,21 +4,45 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
import os from "node:os";
|
|
7
|
+
|
|
8
|
+
function resolveIdentity(params) {
|
|
9
|
+
const testScene = process.env.MEMORY_GATEWAY_TEST_SCENE;
|
|
10
|
+
if (testScene) {
|
|
11
|
+
return {
|
|
12
|
+
scene: testScene,
|
|
13
|
+
owner_id: process.env.MEMORY_GATEWAY_TEST_OWNER_ID || "test_owner",
|
|
14
|
+
group_id: process.env.MEMORY_GATEWAY_TEST_GROUP_ID || null,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const ownerId = params.owner_id?.trim() || "";
|
|
18
|
+
const groupId = params.group_id?.trim() || null;
|
|
19
|
+
if (!ownerId) {
|
|
20
|
+
console.warn(
|
|
21
|
+
"[identity] owner_id is empty — owner queries will be skipped to prevent cross-user leak."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (groupId) {
|
|
25
|
+
return { scene: "group", owner_id: ownerId, group_id: groupId };
|
|
26
|
+
}
|
|
27
|
+
return { scene: "owner", owner_id: ownerId, group_id: null };
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
var registered = false;
|
|
8
31
|
var index_default = definePluginEntry({
|
|
9
32
|
id: "memory-gateway-sync",
|
|
10
33
|
name: "Memory Gateway Sync",
|
|
11
|
-
description: "fs.watch
|
|
34
|
+
description: "fs.watch 监听多 workspace 的 MEMORY 文件变化,双写到外部 Gateway(支持 peers/groups/knowledge 子目录)",
|
|
12
35
|
kind: "generic",
|
|
13
36
|
register(api) {
|
|
14
37
|
if (registered) {
|
|
15
|
-
api.logger.info("memory-gateway-sync:
|
|
38
|
+
api.logger.info("memory-gateway-sync: 已注册,跳过重复加载");
|
|
16
39
|
return;
|
|
17
40
|
}
|
|
18
41
|
registered = true;
|
|
19
42
|
const cfg = api.pluginConfig ?? {};
|
|
20
43
|
const gatewayUrl = cfg.gatewayUrl || process.env.MEMORY_SYNC_GATEWAY_URL || "";
|
|
21
44
|
const gatewayToken = cfg.gatewayToken || process.env.MEMORY_SYNC_GATEWAY_TOKEN || "";
|
|
45
|
+
const tenantsDir = cfg.tenantsDir || process.env.MEMORY_SYNC_TENANTS_DIR || "";
|
|
22
46
|
const debounceMs = cfg.debounceMs ?? 1500;
|
|
23
47
|
const defaultAgentId = cfg.agentId ?? "default";
|
|
24
48
|
const ownerClawUserId = (() => {
|
|
@@ -29,7 +53,7 @@ var index_default = definePluginEntry({
|
|
|
29
53
|
const podReleaseName = process.env.botID ?? null;
|
|
30
54
|
if (!gatewayUrl || !gatewayToken) {
|
|
31
55
|
api.logger.warn(
|
|
32
|
-
"memory-gateway-sync: gatewayUrl
|
|
56
|
+
"memory-gateway-sync: gatewayUrl 或 gatewayToken 未配置,插件不启动"
|
|
33
57
|
);
|
|
34
58
|
return;
|
|
35
59
|
}
|
|
@@ -69,7 +93,7 @@ var index_default = definePluginEntry({
|
|
|
69
93
|
const ws = findWorkspace(absPath);
|
|
70
94
|
if (!ws) {
|
|
71
95
|
api.logger.warn(
|
|
72
|
-
`memory-gateway-sync:
|
|
96
|
+
`memory-gateway-sync: 找不到所属 workspace: ${absPath}`
|
|
73
97
|
);
|
|
74
98
|
return;
|
|
75
99
|
}
|
|
@@ -78,7 +102,7 @@ var index_default = definePluginEntry({
|
|
|
78
102
|
content = await fs.promises.readFile(absPath, "utf-8");
|
|
79
103
|
} catch (err) {
|
|
80
104
|
api.logger.warn(
|
|
81
|
-
`memory-gateway-sync:
|
|
105
|
+
`memory-gateway-sync: 读取文件失败 ${absPath}: ${String(err)}`
|
|
82
106
|
);
|
|
83
107
|
return;
|
|
84
108
|
}
|
|
@@ -98,19 +122,23 @@ var index_default = definePluginEntry({
|
|
|
98
122
|
);
|
|
99
123
|
}
|
|
100
124
|
}
|
|
101
|
-
let
|
|
125
|
+
let owner_id = ws.userId ?? ownerClawUserId ?? undefined;
|
|
126
|
+
let group_id;
|
|
102
127
|
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
} else if (parts[0] === "memory" && parts[1] === "
|
|
106
|
-
|
|
128
|
+
if (parts[0] === "memory" && parts[1] === "peers" && parts.length >= 3) {
|
|
129
|
+
owner_id = parts[2];
|
|
130
|
+
} else if (parts[0] === "memory" && parts[1] === "groups" && parts.length >= 3) {
|
|
131
|
+
group_id = parts[2];
|
|
107
132
|
}
|
|
133
|
+
const identity = resolveIdentity({ owner_id, group_id });
|
|
108
134
|
const payload = {
|
|
109
135
|
path: relativePath,
|
|
110
136
|
content,
|
|
111
137
|
release_name: parseReleaseName(content) ?? podReleaseName,
|
|
112
138
|
agentId: ws.agentId,
|
|
113
|
-
userId,
|
|
139
|
+
userId: identity.owner_id,
|
|
140
|
+
scene: identity.scene,
|
|
141
|
+
group_id: identity.group_id,
|
|
114
142
|
workspaceDir: ws.dir,
|
|
115
143
|
eventType,
|
|
116
144
|
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -133,17 +161,17 @@ var index_default = definePluginEntry({
|
|
|
133
161
|
syncOk = true;
|
|
134
162
|
lastSyncedHash.set(absPath, contentHash);
|
|
135
163
|
api.logger.info?.(
|
|
136
|
-
`memory-gateway-sync: [${ws.agentId}]
|
|
164
|
+
`memory-gateway-sync: [${ws.agentId}] 同步成功 ${relativePath} (${content.length} chars)`
|
|
137
165
|
);
|
|
138
166
|
} else {
|
|
139
167
|
const body = await res.text().catch(() => "");
|
|
140
168
|
api.logger.warn(
|
|
141
|
-
`memory-gateway-sync: [${ws.agentId}] Gateway
|
|
169
|
+
`memory-gateway-sync: [${ws.agentId}] Gateway 返回 ${res.status} for ${relativePath}: ${body}`
|
|
142
170
|
);
|
|
143
171
|
}
|
|
144
172
|
} catch (err) {
|
|
145
173
|
api.logger.warn(
|
|
146
|
-
`memory-gateway-sync: [${ws.agentId}] POST
|
|
174
|
+
`memory-gateway-sync: [${ws.agentId}] POST 失败 ${relativePath}: ${String(err)}`
|
|
147
175
|
);
|
|
148
176
|
}
|
|
149
177
|
if (relativePath === "MEMORY.md" && content.trim().length > 0) {
|
|
@@ -153,11 +181,11 @@ var index_default = definePluginEntry({
|
|
|
153
181
|
const backupPath = syncOk ? absPath + ".bak" : absPath + ".failed";
|
|
154
182
|
await fs.promises.appendFile(backupPath, section, "utf-8");
|
|
155
183
|
api.logger.info?.(
|
|
156
|
-
`memory-gateway-sync: [${ws.agentId}] MEMORY.md
|
|
184
|
+
`memory-gateway-sync: [${ws.agentId}] MEMORY.md 已备份到 ${syncOk ? ".bak" : ".failed"}`
|
|
157
185
|
);
|
|
158
186
|
} catch (err) {
|
|
159
187
|
api.logger.warn(
|
|
160
|
-
`memory-gateway-sync: [${ws.agentId}]
|
|
188
|
+
`memory-gateway-sync: [${ws.agentId}] 备份 MEMORY.md 失败: ${String(err)}`
|
|
161
189
|
);
|
|
162
190
|
}
|
|
163
191
|
}
|
|
@@ -173,11 +201,11 @@ var index_default = definePluginEntry({
|
|
|
173
201
|
}, debounceMs)
|
|
174
202
|
);
|
|
175
203
|
};
|
|
176
|
-
const registerWorkspace = async (wsDir, wsAgentId) => {
|
|
204
|
+
const registerWorkspace = async (wsDir, wsAgentId, wsUserId) => {
|
|
177
205
|
const realDir = await fs.promises.realpath(wsDir).catch(() => wsDir);
|
|
178
206
|
if (registeredRealPaths.has(realDir)) return;
|
|
179
207
|
registeredRealPaths.add(realDir);
|
|
180
|
-
registeredWorkspaces.set(realDir, { dir: realDir, agentId: wsAgentId });
|
|
208
|
+
registeredWorkspaces.set(realDir, { dir: realDir, agentId: wsAgentId, userId: wsUserId });
|
|
181
209
|
const memoryMdPath = path.join(realDir, "MEMORY.md");
|
|
182
210
|
try {
|
|
183
211
|
await fs.promises.writeFile(memoryMdPath, "", { flag: "a" });
|
|
@@ -185,16 +213,16 @@ var index_default = definePluginEntry({
|
|
|
185
213
|
scheduleSync(memoryMdPath, eventType ?? "change");
|
|
186
214
|
});
|
|
187
215
|
api.logger.info?.(
|
|
188
|
-
`memory-gateway-sync: [${wsAgentId}]
|
|
216
|
+
`memory-gateway-sync: [${wsAgentId}] 开始监听 MEMORY.md`
|
|
189
217
|
);
|
|
190
218
|
scheduleSync(memoryMdPath, "startup");
|
|
191
219
|
} catch (err) {
|
|
192
220
|
api.logger.warn(
|
|
193
|
-
`memory-gateway-sync: [${wsAgentId}]
|
|
221
|
+
`memory-gateway-sync: [${wsAgentId}] 无法监听 MEMORY.md: ${String(err)}`
|
|
194
222
|
);
|
|
195
223
|
}
|
|
196
224
|
api.logger.info(
|
|
197
|
-
`memory-gateway-sync: [${wsAgentId}] workspace
|
|
225
|
+
`memory-gateway-sync: [${wsAgentId}] workspace 注册完成 → ${wsDir}`
|
|
198
226
|
);
|
|
199
227
|
};
|
|
200
228
|
const scanWorkspaces = async () => {
|
|
@@ -218,11 +246,11 @@ var index_default = definePluginEntry({
|
|
|
218
246
|
}
|
|
219
247
|
}
|
|
220
248
|
const wsAgentId = resolveAgentId(name);
|
|
221
|
-
await registerWorkspace(wsDir, wsAgentId);
|
|
249
|
+
await registerWorkspace(wsDir, wsAgentId, ownerClawUserId);
|
|
222
250
|
}
|
|
223
251
|
} catch (err) {
|
|
224
252
|
api.logger.warn(
|
|
225
|
-
`memory-gateway-sync:
|
|
253
|
+
`memory-gateway-sync: 扫描 workspace 失败: ${String(err)}`
|
|
226
254
|
);
|
|
227
255
|
}
|
|
228
256
|
for (const dataDir of discoveredDataDirs) {
|
|
@@ -237,11 +265,11 @@ var index_default = definePluginEntry({
|
|
|
237
265
|
if (name !== "workspace" && !name.startsWith("workspace-")) continue;
|
|
238
266
|
const wsDir = path.join(dataDir, name);
|
|
239
267
|
const wsAgentId = resolveAgentId(name);
|
|
240
|
-
await registerWorkspace(wsDir, wsAgentId);
|
|
268
|
+
await registerWorkspace(wsDir, wsAgentId, ownerClawUserId);
|
|
241
269
|
}
|
|
242
270
|
} catch (err) {
|
|
243
271
|
api.logger.warn(
|
|
244
|
-
`memory-gateway-sync:
|
|
272
|
+
`memory-gateway-sync: 扫描数据目录 ${dataDir} 失败: ${String(err)}`
|
|
245
273
|
);
|
|
246
274
|
}
|
|
247
275
|
}
|
|
@@ -261,9 +289,9 @@ var index_default = definePluginEntry({
|
|
|
261
289
|
if (registeredRealPaths.has(realDir)) return;
|
|
262
290
|
const wsAgentId = resolveAgentId(filename);
|
|
263
291
|
api.logger.info(
|
|
264
|
-
`memory-gateway-sync:
|
|
292
|
+
`memory-gateway-sync: 发现新 workspace → ${filename} (agentId=${wsAgentId}) in ${dir}`
|
|
265
293
|
);
|
|
266
|
-
await registerWorkspace(wsDir, wsAgentId);
|
|
294
|
+
await registerWorkspace(wsDir, wsAgentId, ownerClawUserId);
|
|
267
295
|
const parentDir = path.dirname(realDir);
|
|
268
296
|
if (!discoveredDataDirs.has(parentDir) && parentDir !== openclawDir) {
|
|
269
297
|
discoveredDataDirs.add(parentDir);
|
|
@@ -274,10 +302,90 @@ var index_default = definePluginEntry({
|
|
|
274
302
|
});
|
|
275
303
|
} catch (err) {
|
|
276
304
|
api.logger.warn(
|
|
277
|
-
`memory-gateway-sync:
|
|
305
|
+
`memory-gateway-sync: 无法监听 ${dir}: ${String(err)}`
|
|
278
306
|
);
|
|
279
307
|
}
|
|
280
308
|
};
|
|
309
|
+
|
|
310
|
+
// ── Tenants 模式 ──
|
|
311
|
+
|
|
312
|
+
const scanTenantWorkspaces = async (tenantDir, userId) => {
|
|
313
|
+
const workspacesDir = path.join(tenantDir, "workspaces");
|
|
314
|
+
try {
|
|
315
|
+
const entries = await fs.promises.readdir(workspacesDir, { withFileTypes: true });
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
if (!entry.isDirectory()) continue;
|
|
318
|
+
const agentId = entry.name;
|
|
319
|
+
const wsDir = path.join(workspacesDir, agentId);
|
|
320
|
+
await registerWorkspace(wsDir, agentId, userId);
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const watchTenantWorkspacesDir = (tenantDir, userId) => {
|
|
326
|
+
const workspacesDir = path.join(tenantDir, "workspaces");
|
|
327
|
+
try {
|
|
328
|
+
fs.watch(workspacesDir, async (_eventType, filename) => {
|
|
329
|
+
if (!filename) return;
|
|
330
|
+
const wsDir = path.join(workspacesDir, filename);
|
|
331
|
+
try {
|
|
332
|
+
const stat = await fs.promises.stat(wsDir);
|
|
333
|
+
if (!stat.isDirectory()) return;
|
|
334
|
+
const realDir = await fs.promises.realpath(wsDir).catch(() => wsDir);
|
|
335
|
+
if (registeredRealPaths.has(realDir)) return;
|
|
336
|
+
api.logger.info(
|
|
337
|
+
`memory-gateway-sync: [tenants] 发现新 workspace → ${userId}/${filename}`
|
|
338
|
+
);
|
|
339
|
+
await registerWorkspace(wsDir, filename, userId);
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const scanTenants = async () => {
|
|
347
|
+
try {
|
|
348
|
+
const entries = await fs.promises.readdir(tenantsDir, { withFileTypes: true });
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
if (!entry.isDirectory()) continue;
|
|
351
|
+
const userId = entry.name;
|
|
352
|
+
const tenantDir = path.join(tenantsDir, userId);
|
|
353
|
+
await scanTenantWorkspaces(tenantDir, userId);
|
|
354
|
+
watchTenantWorkspacesDir(tenantDir, userId);
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
api.logger.warn(
|
|
358
|
+
`memory-gateway-sync: [tenants] 扫描 ${tenantsDir} 失败: ${String(err)}`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const watchTenantsDir = () => {
|
|
363
|
+
try {
|
|
364
|
+
fs.watch(tenantsDir, async (_eventType, filename) => {
|
|
365
|
+
if (!filename) return;
|
|
366
|
+
const tenantDir = path.join(tenantsDir, filename);
|
|
367
|
+
try {
|
|
368
|
+
const stat = await fs.promises.stat(tenantDir);
|
|
369
|
+
if (!stat.isDirectory()) return;
|
|
370
|
+
const userId = filename;
|
|
371
|
+
api.logger.info(
|
|
372
|
+
`memory-gateway-sync: [tenants] 发现新 tenant → ${userId}`
|
|
373
|
+
);
|
|
374
|
+
await scanTenantWorkspaces(tenantDir, userId);
|
|
375
|
+
watchTenantWorkspacesDir(tenantDir, userId);
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
} catch (err) {
|
|
380
|
+
api.logger.warn(
|
|
381
|
+
`memory-gateway-sync: [tenants] 无法监听 ${tenantsDir}: ${String(err)}`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// ── 启动 ──
|
|
387
|
+
|
|
388
|
+
// 旧模式
|
|
281
389
|
watchDirForWorkspaces(openclawDir);
|
|
282
390
|
await scanWorkspaces();
|
|
283
391
|
for (const dataDir of discoveredDataDirs) {
|
|
@@ -285,14 +393,24 @@ var index_default = definePluginEntry({
|
|
|
285
393
|
watchDirForWorkspaces(dataDir);
|
|
286
394
|
}
|
|
287
395
|
}
|
|
396
|
+
|
|
397
|
+
// Tenants 模式
|
|
398
|
+
if (tenantsDir) {
|
|
399
|
+
watchTenantsDir();
|
|
400
|
+
await scanTenants();
|
|
401
|
+
api.logger.info(
|
|
402
|
+
`memory-gateway-sync: [tenants] 扫描完成 → ${tenantsDir}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
288
406
|
const wsCount = registeredWorkspaces.size;
|
|
289
|
-
const wsIds = Array.from(registeredWorkspaces.values()).map((w) => w.agentId).join(", ");
|
|
407
|
+
const wsIds = Array.from(registeredWorkspaces.values()).map((w) => `${w.agentId}(${w.userId ?? "?"})`).join(", ");
|
|
290
408
|
api.logger.info(
|
|
291
|
-
`memory-gateway-sync:
|
|
409
|
+
`memory-gateway-sync: 启动完成 → ${gatewayUrl} (防抖 ${debounceMs}ms, ${wsCount} 个 workspace: ${wsIds})`
|
|
292
410
|
);
|
|
293
411
|
},
|
|
294
412
|
stop: () => {
|
|
295
|
-
api.logger.info("memory-gateway-sync:
|
|
413
|
+
api.logger.info("memory-gateway-sync: 停止");
|
|
296
414
|
}
|
|
297
415
|
});
|
|
298
416
|
}
|
package/package.json
CHANGED
package/src/identity.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 身份解析模块
|
|
3
|
+
*
|
|
4
|
+
* group_id 有值 → scene = "group"
|
|
5
|
+
* group_id 无值 → scene = "owner"
|
|
6
|
+
*
|
|
7
|
+
* owner_id 用于 SQL 数据隔离。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type Scene = "owner" | "group" | "knowledge";
|
|
11
|
+
|
|
12
|
+
export interface ResolvedIdentity {
|
|
13
|
+
scene: Scene;
|
|
14
|
+
owner_id: string;
|
|
15
|
+
group_id: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IdentityParams {
|
|
19
|
+
owner_id?: string;
|
|
20
|
+
group_id?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveIdentity(params: IdentityParams): ResolvedIdentity {
|
|
24
|
+
const testScene = process.env.MEMORY_GATEWAY_TEST_SCENE as Scene | undefined;
|
|
25
|
+
if (testScene) {
|
|
26
|
+
return {
|
|
27
|
+
scene: testScene,
|
|
28
|
+
owner_id: process.env.MEMORY_GATEWAY_TEST_OWNER_ID || "test_owner",
|
|
29
|
+
group_id: process.env.MEMORY_GATEWAY_TEST_GROUP_ID || null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ownerId = params.owner_id?.trim() || "";
|
|
34
|
+
const groupId = params.group_id?.trim() || null;
|
|
35
|
+
|
|
36
|
+
if (!ownerId) {
|
|
37
|
+
console.warn(
|
|
38
|
+
"[identity] owner_id is empty — " +
|
|
39
|
+
"owner queries will be skipped to prevent cross-user leak."
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (groupId) {
|
|
44
|
+
return { scene: "group", owner_id: ownerId, group_id: groupId };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { scene: "owner", owner_id: ownerId, group_id: null };
|
|
48
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,12 +3,14 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import os from "node:os";
|
|
6
|
+
import { resolveIdentity, type ResolvedIdentity } from "./identity";
|
|
6
7
|
|
|
7
8
|
// ── 类型 ──────────────────────────────────��───────────────────────────────────
|
|
8
9
|
|
|
9
10
|
interface PluginConfig {
|
|
10
11
|
gatewayUrl: string;
|
|
11
12
|
gatewayToken: string;
|
|
13
|
+
tenantsDir?: string;
|
|
12
14
|
debounceMs?: number;
|
|
13
15
|
agentId?: string;
|
|
14
16
|
}
|
|
@@ -18,7 +20,9 @@ interface IngestPayload {
|
|
|
18
20
|
content: string;
|
|
19
21
|
release_name: string | null;
|
|
20
22
|
agentId: string;
|
|
21
|
-
userId: string
|
|
23
|
+
userId: string;
|
|
24
|
+
scene: string;
|
|
25
|
+
group_id: string | null;
|
|
22
26
|
workspaceDir: string;
|
|
23
27
|
eventType: string;
|
|
24
28
|
syncedAt: string;
|
|
@@ -27,6 +31,7 @@ interface IngestPayload {
|
|
|
27
31
|
interface WorkspaceContext {
|
|
28
32
|
dir: string;
|
|
29
33
|
agentId: string;
|
|
34
|
+
userId: string | null;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
// ── 防重复加载 ───────────────────────────────────────────────────────────────
|
|
@@ -50,6 +55,7 @@ export default definePluginEntry({
|
|
|
50
55
|
const cfg = (api.pluginConfig ?? {}) as Partial<PluginConfig>;
|
|
51
56
|
const gatewayUrl = cfg.gatewayUrl || process.env.MEMORY_SYNC_GATEWAY_URL || "";
|
|
52
57
|
const gatewayToken = cfg.gatewayToken || process.env.MEMORY_SYNC_GATEWAY_TOKEN || "";
|
|
58
|
+
const tenantsDir = cfg.tenantsDir || process.env.MEMORY_SYNC_TENANTS_DIR || "";
|
|
53
59
|
const debounceMs = cfg.debounceMs ?? 1500;
|
|
54
60
|
const defaultAgentId = cfg.agentId ?? "default";
|
|
55
61
|
const ownerClawUserId = (() => {
|
|
@@ -163,24 +169,28 @@ export default definePluginEntry({
|
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// memory/
|
|
169
|
-
|
|
170
|
-
let
|
|
172
|
+
// 身份解析:优先使用 workspace 注册时的 userId(tenants 模式从路径获取,旧模式为 ownerClawUserId)
|
|
173
|
+
// memory/peers/{peerId}/* → 覆盖 owner_id
|
|
174
|
+
// memory/groups/{groupId}/* → 设置 group_id
|
|
175
|
+
let owner_id: string | undefined = ws.userId ?? ownerClawUserId ?? undefined;
|
|
176
|
+
let group_id: string | undefined;
|
|
171
177
|
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
} else if (parts[0] === "memory" && parts[1] === "
|
|
175
|
-
|
|
178
|
+
if (parts[0] === "memory" && parts[1] === "peers" && parts.length >= 3) {
|
|
179
|
+
owner_id = parts[2];
|
|
180
|
+
} else if (parts[0] === "memory" && parts[1] === "groups" && parts.length >= 3) {
|
|
181
|
+
group_id = parts[2];
|
|
176
182
|
}
|
|
177
183
|
|
|
184
|
+
const identity = resolveIdentity({ owner_id, group_id });
|
|
185
|
+
|
|
178
186
|
const payload: IngestPayload = {
|
|
179
187
|
path: relativePath,
|
|
180
188
|
content,
|
|
181
189
|
release_name: parseReleaseName(content) ?? podReleaseName,
|
|
182
190
|
agentId: ws.agentId,
|
|
183
|
-
userId,
|
|
191
|
+
userId: identity.owner_id,
|
|
192
|
+
scene: identity.scene,
|
|
193
|
+
group_id: identity.group_id,
|
|
184
194
|
workspaceDir: ws.dir,
|
|
185
195
|
eventType,
|
|
186
196
|
syncedAt: new Date().toISOString(),
|
|
@@ -255,12 +265,12 @@ export default definePluginEntry({
|
|
|
255
265
|
|
|
256
266
|
// ── 注册单个 workspace ───────────────────────────────────────────
|
|
257
267
|
|
|
258
|
-
const registerWorkspace = async (wsDir: string, wsAgentId: string): Promise<void> => {
|
|
268
|
+
const registerWorkspace = async (wsDir: string, wsAgentId: string, wsUserId: string | null): Promise<void> => {
|
|
259
269
|
const realDir = await fs.promises.realpath(wsDir).catch(() => wsDir);
|
|
260
270
|
if (registeredRealPaths.has(realDir)) return;
|
|
261
271
|
registeredRealPaths.add(realDir);
|
|
262
272
|
|
|
263
|
-
registeredWorkspaces.set(realDir, { dir: realDir, agentId: wsAgentId });
|
|
273
|
+
registeredWorkspaces.set(realDir, { dir: realDir, agentId: wsAgentId, userId: wsUserId });
|
|
264
274
|
|
|
265
275
|
// 监听 MEMORY.md(使用真实路径,确保 symlink 场景下 fs.watch 正常工作)
|
|
266
276
|
const memoryMdPath = path.join(realDir, "MEMORY.md");
|
|
@@ -312,7 +322,7 @@ export default definePluginEntry({
|
|
|
312
322
|
}
|
|
313
323
|
|
|
314
324
|
const wsAgentId = resolveAgentId(name);
|
|
315
|
-
await registerWorkspace(wsDir, wsAgentId);
|
|
325
|
+
await registerWorkspace(wsDir, wsAgentId, ownerClawUserId);
|
|
316
326
|
}
|
|
317
327
|
} catch (err) {
|
|
318
328
|
api.logger.warn(
|
|
@@ -334,7 +344,7 @@ export default definePluginEntry({
|
|
|
334
344
|
|
|
335
345
|
const wsDir = path.join(dataDir, name);
|
|
336
346
|
const wsAgentId = resolveAgentId(name);
|
|
337
|
-
await registerWorkspace(wsDir, wsAgentId);
|
|
347
|
+
await registerWorkspace(wsDir, wsAgentId, ownerClawUserId);
|
|
338
348
|
}
|
|
339
349
|
} catch (err) {
|
|
340
350
|
api.logger.warn(
|
|
@@ -370,7 +380,7 @@ export default definePluginEntry({
|
|
|
370
380
|
api.logger.info(
|
|
371
381
|
`memory-gateway-sync: 发现新 workspace → ${filename} (agentId=${wsAgentId}) in ${dir}`
|
|
372
382
|
);
|
|
373
|
-
await registerWorkspace(wsDir, wsAgentId);
|
|
383
|
+
await registerWorkspace(wsDir, wsAgentId, ownerClawUserId);
|
|
374
384
|
|
|
375
385
|
// 如果是 symlink,也发现其数据目录并监听
|
|
376
386
|
const parentDir = path.dirname(realDir);
|
|
@@ -389,23 +399,112 @@ export default definePluginEntry({
|
|
|
389
399
|
}
|
|
390
400
|
};
|
|
391
401
|
|
|
392
|
-
//
|
|
393
|
-
watchDirForWorkspaces(openclawDir);
|
|
402
|
+
// ── Tenants 模式:扫描 {tenantsDir}/{userId}/workspaces/{agentId}/ ──
|
|
394
403
|
|
|
395
|
-
|
|
404
|
+
const scanTenantWorkspaces = async (tenantDir: string, userId: string): Promise<void> => {
|
|
405
|
+
const workspacesDir = path.join(tenantDir, "workspaces");
|
|
406
|
+
try {
|
|
407
|
+
const entries = await fs.promises.readdir(workspacesDir, { withFileTypes: true });
|
|
408
|
+
for (const entry of entries) {
|
|
409
|
+
if (!entry.isDirectory()) continue;
|
|
410
|
+
const agentId = entry.name;
|
|
411
|
+
const wsDir = path.join(workspacesDir, agentId);
|
|
412
|
+
await registerWorkspace(wsDir, agentId, userId);
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
// workspaces 目录不存在,跳过
|
|
416
|
+
}
|
|
417
|
+
};
|
|
396
418
|
|
|
397
|
-
|
|
419
|
+
const watchTenantWorkspacesDir = (tenantDir: string, userId: string): void => {
|
|
420
|
+
const workspacesDir = path.join(tenantDir, "workspaces");
|
|
421
|
+
try {
|
|
422
|
+
fs.watch(workspacesDir, async (_eventType, filename) => {
|
|
423
|
+
if (!filename) return;
|
|
424
|
+
const wsDir = path.join(workspacesDir, filename);
|
|
425
|
+
try {
|
|
426
|
+
const stat = await fs.promises.stat(wsDir);
|
|
427
|
+
if (!stat.isDirectory()) return;
|
|
428
|
+
const realDir = await fs.promises.realpath(wsDir).catch(() => wsDir);
|
|
429
|
+
if (registeredRealPaths.has(realDir)) return;
|
|
430
|
+
api.logger.info(
|
|
431
|
+
`memory-gateway-sync: [tenants] 发现新 workspace → ${userId}/${filename}`
|
|
432
|
+
);
|
|
433
|
+
await registerWorkspace(wsDir, filename, userId);
|
|
434
|
+
} catch {
|
|
435
|
+
// 目录不存在或已删除
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
} catch {
|
|
439
|
+
// workspaces 目录不存在,无法监听
|
|
440
|
+
}
|
|
441
|
+
};
|
|
398
442
|
|
|
399
|
-
|
|
443
|
+
const scanTenants = async (): Promise<void> => {
|
|
444
|
+
try {
|
|
445
|
+
const entries = await fs.promises.readdir(tenantsDir, { withFileTypes: true });
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
if (!entry.isDirectory()) continue;
|
|
448
|
+
const userId = entry.name;
|
|
449
|
+
const tenantDir = path.join(tenantsDir, userId);
|
|
450
|
+
await scanTenantWorkspaces(tenantDir, userId);
|
|
451
|
+
watchTenantWorkspacesDir(tenantDir, userId);
|
|
452
|
+
}
|
|
453
|
+
} catch (err) {
|
|
454
|
+
api.logger.warn(
|
|
455
|
+
`memory-gateway-sync: [tenants] 扫描 ${tenantsDir} 失败: ${String(err)}`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const watchTenantsDir = (): void => {
|
|
461
|
+
try {
|
|
462
|
+
fs.watch(tenantsDir, async (_eventType, filename) => {
|
|
463
|
+
if (!filename) return;
|
|
464
|
+
const tenantDir = path.join(tenantsDir, filename);
|
|
465
|
+
try {
|
|
466
|
+
const stat = await fs.promises.stat(tenantDir);
|
|
467
|
+
if (!stat.isDirectory()) return;
|
|
468
|
+
const userId = filename;
|
|
469
|
+
api.logger.info(
|
|
470
|
+
`memory-gateway-sync: [tenants] 发现新 tenant → ${userId}`
|
|
471
|
+
);
|
|
472
|
+
await scanTenantWorkspaces(tenantDir, userId);
|
|
473
|
+
watchTenantWorkspacesDir(tenantDir, userId);
|
|
474
|
+
} catch {
|
|
475
|
+
// 目录不存在或已删除
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
} catch (err) {
|
|
479
|
+
api.logger.warn(
|
|
480
|
+
`memory-gateway-sync: [tenants] 无法监听 ${tenantsDir}: ${String(err)}`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// ── 启动 ─────────────────────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
// 旧模式:~/.openclaw/workspace*
|
|
488
|
+
watchDirForWorkspaces(openclawDir);
|
|
489
|
+
await scanWorkspaces();
|
|
400
490
|
for (const dataDir of discoveredDataDirs) {
|
|
401
491
|
if (dataDir !== openclawDir) {
|
|
402
492
|
watchDirForWorkspaces(dataDir);
|
|
403
493
|
}
|
|
404
494
|
}
|
|
405
495
|
|
|
496
|
+
// Tenants 模式
|
|
497
|
+
if (tenantsDir) {
|
|
498
|
+
watchTenantsDir();
|
|
499
|
+
await scanTenants();
|
|
500
|
+
api.logger.info(
|
|
501
|
+
`memory-gateway-sync: [tenants] 扫描完成 → ${tenantsDir}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
406
505
|
const wsCount = registeredWorkspaces.size;
|
|
407
506
|
const wsIds = Array.from(registeredWorkspaces.values())
|
|
408
|
-
.map((w) => w.agentId)
|
|
507
|
+
.map((w) => `${w.agentId}(${w.userId ?? "?"})`)
|
|
409
508
|
.join(", ");
|
|
410
509
|
api.logger.info(
|
|
411
510
|
`memory-gateway-sync: 启动完成 → ${gatewayUrl} (防抖 ${debounceMs}ms, ${wsCount} 个 workspace: ${wsIds})`
|