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