memory-gateway-sync 0.12.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/index.ts +107 -17
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/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { resolveIdentity, type ResolvedIdentity } from "./identity";
|
|
|
10
10
|
interface PluginConfig {
|
|
11
11
|
gatewayUrl: string;
|
|
12
12
|
gatewayToken: string;
|
|
13
|
+
tenantsDir?: string;
|
|
13
14
|
debounceMs?: number;
|
|
14
15
|
agentId?: string;
|
|
15
16
|
}
|
|
@@ -30,6 +31,7 @@ interface IngestPayload {
|
|
|
30
31
|
interface WorkspaceContext {
|
|
31
32
|
dir: string;
|
|
32
33
|
agentId: string;
|
|
34
|
+
userId: string | null;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// ── 防重复加载 ───────────────────────────────────────────────────────────────
|
|
@@ -53,6 +55,7 @@ export default definePluginEntry({
|
|
|
53
55
|
const cfg = (api.pluginConfig ?? {}) as Partial<PluginConfig>;
|
|
54
56
|
const gatewayUrl = cfg.gatewayUrl || process.env.MEMORY_SYNC_GATEWAY_URL || "";
|
|
55
57
|
const gatewayToken = cfg.gatewayToken || process.env.MEMORY_SYNC_GATEWAY_TOKEN || "";
|
|
58
|
+
const tenantsDir = cfg.tenantsDir || process.env.MEMORY_SYNC_TENANTS_DIR || "";
|
|
56
59
|
const debounceMs = cfg.debounceMs ?? 1500;
|
|
57
60
|
const defaultAgentId = cfg.agentId ?? "default";
|
|
58
61
|
const ownerClawUserId = (() => {
|
|
@@ -166,12 +169,10 @@ export default definePluginEntry({
|
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
171
|
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// memory/
|
|
172
|
-
|
|
173
|
-
// 其他 → owner_id = ownerClawUserId
|
|
174
|
-
let owner_id: string | undefined = ownerClawUserId ?? undefined;
|
|
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;
|
|
175
176
|
let group_id: string | undefined;
|
|
176
177
|
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
177
178
|
if (parts[0] === "memory" && parts[1] === "peers" && parts.length >= 3) {
|
|
@@ -264,12 +265,12 @@ export default definePluginEntry({
|
|
|
264
265
|
|
|
265
266
|
// ── 注册单个 workspace ───────────────────────────────────────────
|
|
266
267
|
|
|
267
|
-
const registerWorkspace = async (wsDir: string, wsAgentId: string): Promise<void> => {
|
|
268
|
+
const registerWorkspace = async (wsDir: string, wsAgentId: string, wsUserId: string | null): Promise<void> => {
|
|
268
269
|
const realDir = await fs.promises.realpath(wsDir).catch(() => wsDir);
|
|
269
270
|
if (registeredRealPaths.has(realDir)) return;
|
|
270
271
|
registeredRealPaths.add(realDir);
|
|
271
272
|
|
|
272
|
-
registeredWorkspaces.set(realDir, { dir: realDir, agentId: wsAgentId });
|
|
273
|
+
registeredWorkspaces.set(realDir, { dir: realDir, agentId: wsAgentId, userId: wsUserId });
|
|
273
274
|
|
|
274
275
|
// 监听 MEMORY.md(使用真实路径,确保 symlink 场景下 fs.watch 正常工作)
|
|
275
276
|
const memoryMdPath = path.join(realDir, "MEMORY.md");
|
|
@@ -321,7 +322,7 @@ export default definePluginEntry({
|
|
|
321
322
|
}
|
|
322
323
|
|
|
323
324
|
const wsAgentId = resolveAgentId(name);
|
|
324
|
-
await registerWorkspace(wsDir, wsAgentId);
|
|
325
|
+
await registerWorkspace(wsDir, wsAgentId, ownerClawUserId);
|
|
325
326
|
}
|
|
326
327
|
} catch (err) {
|
|
327
328
|
api.logger.warn(
|
|
@@ -343,7 +344,7 @@ export default definePluginEntry({
|
|
|
343
344
|
|
|
344
345
|
const wsDir = path.join(dataDir, name);
|
|
345
346
|
const wsAgentId = resolveAgentId(name);
|
|
346
|
-
await registerWorkspace(wsDir, wsAgentId);
|
|
347
|
+
await registerWorkspace(wsDir, wsAgentId, ownerClawUserId);
|
|
347
348
|
}
|
|
348
349
|
} catch (err) {
|
|
349
350
|
api.logger.warn(
|
|
@@ -379,7 +380,7 @@ export default definePluginEntry({
|
|
|
379
380
|
api.logger.info(
|
|
380
381
|
`memory-gateway-sync: 发现新 workspace → ${filename} (agentId=${wsAgentId}) in ${dir}`
|
|
381
382
|
);
|
|
382
|
-
await registerWorkspace(wsDir, wsAgentId);
|
|
383
|
+
await registerWorkspace(wsDir, wsAgentId, ownerClawUserId);
|
|
383
384
|
|
|
384
385
|
// 如果是 symlink,也发现其数据目录并监听
|
|
385
386
|
const parentDir = path.dirname(realDir);
|
|
@@ -398,23 +399,112 @@ export default definePluginEntry({
|
|
|
398
399
|
}
|
|
399
400
|
};
|
|
400
401
|
|
|
401
|
-
//
|
|
402
|
-
watchDirForWorkspaces(openclawDir);
|
|
402
|
+
// ── Tenants 模式:扫描 {tenantsDir}/{userId}/workspaces/{agentId}/ ──
|
|
403
403
|
|
|
404
|
-
|
|
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
|
+
};
|
|
405
418
|
|
|
406
|
-
|
|
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
|
+
};
|
|
442
|
+
|
|
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
|
+
// ── 启动 ─────────────────────────────────────────────────────────
|
|
407
486
|
|
|
408
|
-
//
|
|
487
|
+
// 旧模式:~/.openclaw/workspace*
|
|
488
|
+
watchDirForWorkspaces(openclawDir);
|
|
489
|
+
await scanWorkspaces();
|
|
409
490
|
for (const dataDir of discoveredDataDirs) {
|
|
410
491
|
if (dataDir !== openclawDir) {
|
|
411
492
|
watchDirForWorkspaces(dataDir);
|
|
412
493
|
}
|
|
413
494
|
}
|
|
414
495
|
|
|
496
|
+
// Tenants 模式
|
|
497
|
+
if (tenantsDir) {
|
|
498
|
+
watchTenantsDir();
|
|
499
|
+
await scanTenants();
|
|
500
|
+
api.logger.info(
|
|
501
|
+
`memory-gateway-sync: [tenants] 扫描完成 → ${tenantsDir}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
415
505
|
const wsCount = registeredWorkspaces.size;
|
|
416
506
|
const wsIds = Array.from(registeredWorkspaces.values())
|
|
417
|
-
.map((w) => w.agentId)
|
|
507
|
+
.map((w) => `${w.agentId}(${w.userId ?? "?"})`)
|
|
418
508
|
.join(", ");
|
|
419
509
|
api.logger.info(
|
|
420
510
|
`memory-gateway-sync: 启动完成 → ${gatewayUrl} (防抖 ${debounceMs}ms, ${wsCount} 个 workspace: ${wsIds})`
|