memory-gateway-sync 0.7.0 → 0.9.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 +288 -0
- package/openclaw.plugin.json +0 -1
- package/package.json +3 -2
- package/src/index.ts +99 -36
package/index.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// memory-gateway-sync/src/index.ts
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
var index_default = definePluginEntry({
|
|
8
|
+
id: "memory-gateway-sync",
|
|
9
|
+
name: "Memory Gateway Sync",
|
|
10
|
+
description: "fs.watch \u76D1\u542C\u591A workspace \u7684 MEMORY \u6587\u4EF6\u53D8\u5316\uFF0C\u53CC\u5199\u5230\u5916\u90E8 Gateway\uFF08\u652F\u6301 peers/groups/knowledge \u5B50\u76EE\u5F55\uFF09",
|
|
11
|
+
kind: "generic",
|
|
12
|
+
register(api) {
|
|
13
|
+
const cfg = api.pluginConfig ?? {};
|
|
14
|
+
const gatewayUrl = cfg.gatewayUrl || process.env.MEMORY_SYNC_GATEWAY_URL || "";
|
|
15
|
+
const gatewayToken = cfg.gatewayToken || process.env.MEMORY_SYNC_GATEWAY_TOKEN || "";
|
|
16
|
+
const debounceMs = cfg.debounceMs ?? 1500;
|
|
17
|
+
const defaultAgentId = cfg.agentId ?? "default";
|
|
18
|
+
const ownerClawUserId = (() => {
|
|
19
|
+
const token = process.env.OPENCLAW_GATEWAY_TOKEN ?? "";
|
|
20
|
+
const parts = token.split("-");
|
|
21
|
+
return parts.length >= 3 ? parts[parts.length - 1] : null;
|
|
22
|
+
})();
|
|
23
|
+
const podReleaseName = process.env.botID ?? null;
|
|
24
|
+
if (!gatewayUrl || !gatewayToken) {
|
|
25
|
+
api.logger.warn(
|
|
26
|
+
"memory-gateway-sync: gatewayUrl \u6216 gatewayToken \u672A\u914D\u7F6E\uFF0C\u63D2\u4EF6\u4E0D\u542F\u52A8"
|
|
27
|
+
);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
api.registerService({
|
|
31
|
+
id: "memory-gateway-sync",
|
|
32
|
+
start: async () => {
|
|
33
|
+
const openclawDir = path.join(os.homedir(), ".openclaw");
|
|
34
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
35
|
+
const lastSyncedHash = /* @__PURE__ */ new Map();
|
|
36
|
+
const parseReleaseName = (content) => {
|
|
37
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
38
|
+
if (!match) return null;
|
|
39
|
+
const nameMatch = match[1].match(/^name:\s*(.+)$/m);
|
|
40
|
+
return nameMatch ? nameMatch[1].trim() : null;
|
|
41
|
+
};
|
|
42
|
+
const registeredWorkspaces = /* @__PURE__ */ new Map();
|
|
43
|
+
const registeredRealPaths = /* @__PURE__ */ new Set();
|
|
44
|
+
const discoveredDataDirs = /* @__PURE__ */ new Set();
|
|
45
|
+
const findWorkspace = (absPath) => {
|
|
46
|
+
for (const [wsDir, ctx] of registeredWorkspaces) {
|
|
47
|
+
if (absPath.startsWith(wsDir + path.sep) || absPath === wsDir) {
|
|
48
|
+
return ctx;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
const resolveAgentId = (dirName) => {
|
|
54
|
+
if (dirName === "workspace") {
|
|
55
|
+
return defaultAgentId;
|
|
56
|
+
}
|
|
57
|
+
if (dirName.startsWith("workspace-")) {
|
|
58
|
+
return dirName.substring("workspace-".length);
|
|
59
|
+
}
|
|
60
|
+
return defaultAgentId;
|
|
61
|
+
};
|
|
62
|
+
const syncFile = async (absPath, eventType) => {
|
|
63
|
+
const ws = findWorkspace(absPath);
|
|
64
|
+
if (!ws) {
|
|
65
|
+
api.logger.warn(
|
|
66
|
+
`memory-gateway-sync: \u627E\u4E0D\u5230\u6240\u5C5E workspace: ${absPath}`
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let content;
|
|
71
|
+
try {
|
|
72
|
+
content = await fs.promises.readFile(absPath, "utf-8");
|
|
73
|
+
} catch (err) {
|
|
74
|
+
api.logger.warn(
|
|
75
|
+
`memory-gateway-sync: \u8BFB\u53D6\u6587\u4EF6\u5931\u8D25 ${absPath}: ${String(err)}`
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const contentHash = crypto.createHash("md5").update(content).digest("hex");
|
|
80
|
+
if (lastSyncedHash.get(absPath) === contentHash) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const relativePath = path.relative(ws.dir, absPath);
|
|
84
|
+
let userId = null;
|
|
85
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
86
|
+
if (relativePath === "MEMORY.md" || parts[0] === "memory" && parts[1] === "owner") {
|
|
87
|
+
userId = ownerClawUserId;
|
|
88
|
+
} else if (parts[0] === "memory" && parts[1] === "peers" && parts.length >= 3) {
|
|
89
|
+
userId = parts[2];
|
|
90
|
+
}
|
|
91
|
+
const payload = {
|
|
92
|
+
path: relativePath,
|
|
93
|
+
content,
|
|
94
|
+
release_name: parseReleaseName(content) ?? podReleaseName,
|
|
95
|
+
agentId: ws.agentId,
|
|
96
|
+
userId,
|
|
97
|
+
workspaceDir: ws.dir,
|
|
98
|
+
eventType,
|
|
99
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
104
|
+
const res = await fetch(gatewayUrl, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
Authorization: `Bearer ${gatewayToken}`
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify(payload),
|
|
111
|
+
signal: controller.signal
|
|
112
|
+
});
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
if (res.ok) {
|
|
115
|
+
lastSyncedHash.set(absPath, contentHash);
|
|
116
|
+
api.logger.info?.(
|
|
117
|
+
`memory-gateway-sync: [${ws.agentId}] \u540C\u6B65\u6210\u529F ${relativePath} (${content.length} chars)`
|
|
118
|
+
);
|
|
119
|
+
if (relativePath === "MEMORY.md" && content.trim().length > 0) {
|
|
120
|
+
try {
|
|
121
|
+
const backupPath = absPath + ".bak";
|
|
122
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
123
|
+
const section = `
|
|
124
|
+
<!-- backup ${timestamp} -->
|
|
125
|
+
${content}
|
|
126
|
+
`;
|
|
127
|
+
await fs.promises.appendFile(backupPath, section, "utf-8");
|
|
128
|
+
await fs.promises.writeFile(absPath, "", "utf-8");
|
|
129
|
+
const emptyHash = crypto.createHash("md5").update("").digest("hex");
|
|
130
|
+
lastSyncedHash.set(absPath, emptyHash);
|
|
131
|
+
api.logger.info?.(
|
|
132
|
+
`memory-gateway-sync: [${ws.agentId}] MEMORY.md \u5DF2\u5907\u4EFD\u5230 .bak \u5E76\u6E05\u7A7A`
|
|
133
|
+
);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
api.logger.warn(
|
|
136
|
+
`memory-gateway-sync: [${ws.agentId}] \u5907\u4EFD/\u6E05\u7A7A MEMORY.md \u5931\u8D25: ${String(err)}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
const body = await res.text().catch(() => "");
|
|
142
|
+
api.logger.warn(
|
|
143
|
+
`memory-gateway-sync: [${ws.agentId}] Gateway \u8FD4\u56DE ${res.status} for ${relativePath}: ${body}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
api.logger.warn(
|
|
148
|
+
`memory-gateway-sync: [${ws.agentId}] POST \u5931\u8D25 ${relativePath}: ${String(err)}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const scheduleSync = (absPath, eventType) => {
|
|
153
|
+
const existing = debounceTimers.get(absPath);
|
|
154
|
+
if (existing) clearTimeout(existing);
|
|
155
|
+
debounceTimers.set(
|
|
156
|
+
absPath,
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
debounceTimers.delete(absPath);
|
|
159
|
+
syncFile(absPath, eventType);
|
|
160
|
+
}, debounceMs)
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
const registerWorkspace = async (wsDir, wsAgentId) => {
|
|
164
|
+
const realDir = await fs.promises.realpath(wsDir).catch(() => wsDir);
|
|
165
|
+
if (registeredRealPaths.has(realDir)) return;
|
|
166
|
+
registeredRealPaths.add(realDir);
|
|
167
|
+
registeredWorkspaces.set(realDir, { dir: realDir, agentId: wsAgentId });
|
|
168
|
+
const memoryMdPath = path.join(realDir, "MEMORY.md");
|
|
169
|
+
try {
|
|
170
|
+
await fs.promises.writeFile(memoryMdPath, "", { flag: "a" });
|
|
171
|
+
fs.watch(memoryMdPath, (eventType) => {
|
|
172
|
+
scheduleSync(memoryMdPath, eventType ?? "change");
|
|
173
|
+
});
|
|
174
|
+
api.logger.info?.(
|
|
175
|
+
`memory-gateway-sync: [${wsAgentId}] \u5F00\u59CB\u76D1\u542C MEMORY.md`
|
|
176
|
+
);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
api.logger.warn(
|
|
179
|
+
`memory-gateway-sync: [${wsAgentId}] \u65E0\u6CD5\u76D1\u542C MEMORY.md: ${String(err)}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
api.logger.info(
|
|
183
|
+
`memory-gateway-sync: [${wsAgentId}] workspace \u6CE8\u518C\u5B8C\u6210 \u2192 ${wsDir}`
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
const scanWorkspaces = async () => {
|
|
187
|
+
try {
|
|
188
|
+
const entries = await fs.promises.readdir(openclawDir, {
|
|
189
|
+
withFileTypes: true
|
|
190
|
+
});
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
193
|
+
const name = entry.name;
|
|
194
|
+
if (name !== "workspace" && !name.startsWith("workspace-")) continue;
|
|
195
|
+
const wsDir = path.join(openclawDir, name);
|
|
196
|
+
if (entry.isSymbolicLink()) {
|
|
197
|
+
try {
|
|
198
|
+
const stat = await fs.promises.stat(wsDir);
|
|
199
|
+
if (!stat.isDirectory()) continue;
|
|
200
|
+
const realTarget = await fs.promises.realpath(wsDir);
|
|
201
|
+
discoveredDataDirs.add(path.dirname(realTarget));
|
|
202
|
+
} catch {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const wsAgentId = resolveAgentId(name);
|
|
207
|
+
await registerWorkspace(wsDir, wsAgentId);
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
api.logger.warn(
|
|
211
|
+
`memory-gateway-sync: \u626B\u63CF workspace \u5931\u8D25: ${String(err)}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
for (const dataDir of discoveredDataDirs) {
|
|
215
|
+
if (dataDir === openclawDir) continue;
|
|
216
|
+
try {
|
|
217
|
+
const entries = await fs.promises.readdir(dataDir, {
|
|
218
|
+
withFileTypes: true
|
|
219
|
+
});
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
if (!entry.isDirectory()) continue;
|
|
222
|
+
const name = entry.name;
|
|
223
|
+
if (name !== "workspace" && !name.startsWith("workspace-")) continue;
|
|
224
|
+
const wsDir = path.join(dataDir, name);
|
|
225
|
+
const wsAgentId = resolveAgentId(name);
|
|
226
|
+
await registerWorkspace(wsDir, wsAgentId);
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
api.logger.warn(
|
|
230
|
+
`memory-gateway-sync: \u626B\u63CF\u6570\u636E\u76EE\u5F55 ${dataDir} \u5931\u8D25: ${String(err)}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const watchDirForWorkspaces = (dir) => {
|
|
236
|
+
try {
|
|
237
|
+
fs.watch(dir, async (eventType, filename) => {
|
|
238
|
+
if (!filename) return;
|
|
239
|
+
if (filename !== "workspace" && !filename.startsWith("workspace-")) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const wsDir = path.join(dir, filename);
|
|
243
|
+
try {
|
|
244
|
+
const stat = await fs.promises.stat(wsDir);
|
|
245
|
+
if (!stat.isDirectory()) return;
|
|
246
|
+
const realDir = await fs.promises.realpath(wsDir).catch(() => wsDir);
|
|
247
|
+
if (registeredRealPaths.has(realDir)) return;
|
|
248
|
+
const wsAgentId = resolveAgentId(filename);
|
|
249
|
+
api.logger.info(
|
|
250
|
+
`memory-gateway-sync: \u53D1\u73B0\u65B0 workspace \u2192 ${filename} (agentId=${wsAgentId}) in ${dir}`
|
|
251
|
+
);
|
|
252
|
+
await registerWorkspace(wsDir, wsAgentId);
|
|
253
|
+
const parentDir = path.dirname(realDir);
|
|
254
|
+
if (!discoveredDataDirs.has(parentDir) && parentDir !== openclawDir) {
|
|
255
|
+
discoveredDataDirs.add(parentDir);
|
|
256
|
+
watchDirForWorkspaces(parentDir);
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
} catch (err) {
|
|
262
|
+
api.logger.warn(
|
|
263
|
+
`memory-gateway-sync: \u65E0\u6CD5\u76D1\u542C ${dir}: ${String(err)}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
watchDirForWorkspaces(openclawDir);
|
|
268
|
+
await scanWorkspaces();
|
|
269
|
+
for (const dataDir of discoveredDataDirs) {
|
|
270
|
+
if (dataDir !== openclawDir) {
|
|
271
|
+
watchDirForWorkspaces(dataDir);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const wsCount = registeredWorkspaces.size;
|
|
275
|
+
const wsIds = Array.from(registeredWorkspaces.values()).map((w) => w.agentId).join(", ");
|
|
276
|
+
api.logger.info(
|
|
277
|
+
`memory-gateway-sync: \u542F\u52A8\u5B8C\u6210 \u2192 ${gatewayUrl} (\u9632\u6296 ${debounceMs}ms, ${wsCount} \u4E2A workspace: ${wsIds})`
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
stop: () => {
|
|
281
|
+
api.logger.info("memory-gateway-sync: \u505C\u6B62");
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
export {
|
|
287
|
+
index_default as default
|
|
288
|
+
};
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-gateway-sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Memory 双写插件:fs.watch 感知变化后同步到外部 Gateway",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
6
7
|
"openclaw": {
|
|
7
8
|
"extensions": [
|
|
8
|
-
"./
|
|
9
|
+
"./index.js"
|
|
9
10
|
]
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -39,9 +39,9 @@ export default definePluginEntry({
|
|
|
39
39
|
kind: "generic",
|
|
40
40
|
|
|
41
41
|
register(api) {
|
|
42
|
-
const cfg = (api.pluginConfig ?? {}) as PluginConfig
|
|
43
|
-
const gatewayUrl = cfg.gatewayUrl;
|
|
44
|
-
const gatewayToken = cfg.gatewayToken;
|
|
42
|
+
const cfg = (api.pluginConfig ?? {}) as Partial<PluginConfig>;
|
|
43
|
+
const gatewayUrl = cfg.gatewayUrl || process.env.MEMORY_SYNC_GATEWAY_URL || "";
|
|
44
|
+
const gatewayToken = cfg.gatewayToken || process.env.MEMORY_SYNC_GATEWAY_TOKEN || "";
|
|
45
45
|
const debounceMs = cfg.debounceMs ?? 1500;
|
|
46
46
|
const defaultAgentId = cfg.agentId ?? "default";
|
|
47
47
|
const ownerClawUserId = (() => {
|
|
@@ -83,6 +83,10 @@ export default definePluginEntry({
|
|
|
83
83
|
|
|
84
84
|
// 已注册的 workspace 集合(避免重复注册)
|
|
85
85
|
const registeredWorkspaces = new Map<string, WorkspaceContext>();
|
|
86
|
+
// 用 realpath 去重,防止同一目录通过 symlink 和真实路径重复注册
|
|
87
|
+
const registeredRealPaths = new Set<string>();
|
|
88
|
+
// 从 symlink 目标发现的数据目录(如 /data/),用于扫描未建立 symlink 的 workspace
|
|
89
|
+
const discoveredDataDirs = new Set<string>();
|
|
86
90
|
|
|
87
91
|
// ── 根据文件路径找到所属 workspace ────────────────────────────────
|
|
88
92
|
|
|
@@ -232,12 +236,14 @@ export default definePluginEntry({
|
|
|
232
236
|
// ── 注册单个 workspace ───────────────────────────────────────────
|
|
233
237
|
|
|
234
238
|
const registerWorkspace = async (wsDir: string, wsAgentId: string): Promise<void> => {
|
|
235
|
-
|
|
239
|
+
const realDir = await fs.promises.realpath(wsDir).catch(() => wsDir);
|
|
240
|
+
if (registeredRealPaths.has(realDir)) return;
|
|
241
|
+
registeredRealPaths.add(realDir);
|
|
236
242
|
|
|
237
|
-
registeredWorkspaces.set(
|
|
243
|
+
registeredWorkspaces.set(realDir, { dir: realDir, agentId: wsAgentId });
|
|
238
244
|
|
|
239
|
-
// 监听 MEMORY.md
|
|
240
|
-
const memoryMdPath = path.join(
|
|
245
|
+
// 监听 MEMORY.md(使用真实路径,确保 symlink 场景下 fs.watch 正常工作)
|
|
246
|
+
const memoryMdPath = path.join(realDir, "MEMORY.md");
|
|
241
247
|
try {
|
|
242
248
|
await fs.promises.writeFile(memoryMdPath, "", { flag: "a" });
|
|
243
249
|
fs.watch(memoryMdPath, (eventType) => {
|
|
@@ -260,64 +266,121 @@ export default definePluginEntry({
|
|
|
260
266
|
// ── 扫描所有 workspace 目录 ──────────────────────────────────────
|
|
261
267
|
|
|
262
268
|
const scanWorkspaces = async (): Promise<void> => {
|
|
269
|
+
// 1. 扫描 openclawDir(处理 symlink 和物理目录)
|
|
263
270
|
try {
|
|
264
271
|
const entries = await fs.promises.readdir(openclawDir, {
|
|
265
272
|
withFileTypes: true,
|
|
266
273
|
});
|
|
267
274
|
for (const entry of entries) {
|
|
268
|
-
if (!entry.isDirectory()) continue;
|
|
275
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
269
276
|
const name = entry.name;
|
|
277
|
+
if (name !== "workspace" && !name.startsWith("workspace-")) continue;
|
|
270
278
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
279
|
+
const wsDir = path.join(openclawDir, name);
|
|
280
|
+
|
|
281
|
+
if (entry.isSymbolicLink()) {
|
|
282
|
+
try {
|
|
283
|
+
const stat = await fs.promises.stat(wsDir);
|
|
284
|
+
if (!stat.isDirectory()) continue;
|
|
285
|
+
const realTarget = await fs.promises.realpath(wsDir);
|
|
286
|
+
discoveredDataDirs.add(path.dirname(realTarget));
|
|
287
|
+
} catch {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
275
290
|
}
|
|
291
|
+
|
|
292
|
+
const wsAgentId = resolveAgentId(name);
|
|
293
|
+
await registerWorkspace(wsDir, wsAgentId);
|
|
276
294
|
}
|
|
277
295
|
} catch (err) {
|
|
278
296
|
api.logger.warn(
|
|
279
297
|
`memory-gateway-sync: 扫描 workspace 失败: ${String(err)}`
|
|
280
298
|
);
|
|
281
299
|
}
|
|
300
|
+
|
|
301
|
+
// 2. 扫描从 symlink 目标发现的数据目录,找到未建立 symlink 的 workspace
|
|
302
|
+
for (const dataDir of discoveredDataDirs) {
|
|
303
|
+
if (dataDir === openclawDir) continue;
|
|
304
|
+
try {
|
|
305
|
+
const entries = await fs.promises.readdir(dataDir, {
|
|
306
|
+
withFileTypes: true,
|
|
307
|
+
});
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (!entry.isDirectory()) continue;
|
|
310
|
+
const name = entry.name;
|
|
311
|
+
if (name !== "workspace" && !name.startsWith("workspace-")) continue;
|
|
312
|
+
|
|
313
|
+
const wsDir = path.join(dataDir, name);
|
|
314
|
+
const wsAgentId = resolveAgentId(name);
|
|
315
|
+
await registerWorkspace(wsDir, wsAgentId);
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
api.logger.warn(
|
|
319
|
+
`memory-gateway-sync: 扫描数据目录 ${dataDir} 失败: ${String(err)}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
282
323
|
};
|
|
283
324
|
|
|
284
|
-
// ──
|
|
325
|
+
// ── 监听目录变化,动态感知新 workspace ─────────────────────────
|
|
285
326
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
327
|
+
const watchDirForWorkspaces = (dir: string): void => {
|
|
328
|
+
try {
|
|
329
|
+
fs.watch(dir, async (eventType, filename) => {
|
|
330
|
+
if (!filename) return;
|
|
331
|
+
if (
|
|
332
|
+
filename !== "workspace" &&
|
|
333
|
+
!filename.startsWith("workspace-")
|
|
334
|
+
) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
295
337
|
|
|
296
|
-
|
|
338
|
+
const wsDir = path.join(dir, filename);
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const stat = await fs.promises.stat(wsDir);
|
|
342
|
+
if (!stat.isDirectory()) return;
|
|
343
|
+
|
|
344
|
+
const realDir = await fs.promises.realpath(wsDir).catch(() => wsDir);
|
|
345
|
+
if (registeredRealPaths.has(realDir)) return;
|
|
297
346
|
|
|
298
|
-
try {
|
|
299
|
-
const stat = await fs.promises.stat(wsDir);
|
|
300
|
-
if (stat.isDirectory() && !registeredWorkspaces.has(wsDir)) {
|
|
301
347
|
const wsAgentId = resolveAgentId(filename);
|
|
302
348
|
api.logger.info(
|
|
303
|
-
`memory-gateway-sync: 发现新 workspace → ${filename} (agentId=${wsAgentId})`
|
|
349
|
+
`memory-gateway-sync: 发现新 workspace → ${filename} (agentId=${wsAgentId}) in ${dir}`
|
|
304
350
|
);
|
|
305
351
|
await registerWorkspace(wsDir, wsAgentId);
|
|
352
|
+
|
|
353
|
+
// 如果是 symlink,也发现其数据目录并监听
|
|
354
|
+
const parentDir = path.dirname(realDir);
|
|
355
|
+
if (!discoveredDataDirs.has(parentDir) && parentDir !== openclawDir) {
|
|
356
|
+
discoveredDataDirs.add(parentDir);
|
|
357
|
+
watchDirForWorkspaces(parentDir);
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
// 目录不存在或已删除,忽略
|
|
306
361
|
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
362
|
+
});
|
|
363
|
+
} catch (err) {
|
|
364
|
+
api.logger.warn(
|
|
365
|
+
`memory-gateway-sync: 无法监听 ${dir}: ${String(err)}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// 监听 openclawDir(在 scan 之前注册,不会遗漏 scan 期间创建的目录)
|
|
371
|
+
watchDirForWorkspaces(openclawDir);
|
|
316
372
|
|
|
317
373
|
// ── 启动:扫描所有已有 workspace ─────────────────────────────────
|
|
318
374
|
|
|
319
375
|
await scanWorkspaces();
|
|
320
376
|
|
|
377
|
+
// 监听从 symlink 发现的数据目录(scan 之后才有 discoveredDataDirs)
|
|
378
|
+
for (const dataDir of discoveredDataDirs) {
|
|
379
|
+
if (dataDir !== openclawDir) {
|
|
380
|
+
watchDirForWorkspaces(dataDir);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
321
384
|
const wsCount = registeredWorkspaces.size;
|
|
322
385
|
const wsIds = Array.from(registeredWorkspaces.values())
|
|
323
386
|
.map((w) => w.agentId)
|