memory-gateway-sync 0.4.0 → 0.6.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/package.json +1 -1
- package/src/index.ts +37 -85
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
|
|
6
7
|
// ── 类型 ──────────────────────────────────��───────────────────────────────────
|
|
@@ -15,6 +16,7 @@ interface PluginConfig {
|
|
|
15
16
|
interface IngestPayload {
|
|
16
17
|
path: string;
|
|
17
18
|
content: string;
|
|
19
|
+
release_name: string | null;
|
|
18
20
|
agentId: string;
|
|
19
21
|
userId: string | null;
|
|
20
22
|
workspaceDir: string;
|
|
@@ -68,8 +70,15 @@ export default definePluginEntry({
|
|
|
68
70
|
ReturnType<typeof setTimeout>
|
|
69
71
|
>();
|
|
70
72
|
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
+
// 内容哈希 Map:key=文件绝对路径,value=上次同步内容的 MD5,用于去重
|
|
74
|
+
const lastSyncedHash = new Map<string, string>();
|
|
75
|
+
|
|
76
|
+
const parseReleaseName = (content: string): string | null => {
|
|
77
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
78
|
+
if (!match) return null;
|
|
79
|
+
const nameMatch = match[1].match(/^name:\s*(.+)$/m);
|
|
80
|
+
return nameMatch ? nameMatch[1].trim() : null;
|
|
81
|
+
};
|
|
73
82
|
|
|
74
83
|
// 已注册的 workspace 集合(避免重复注册)
|
|
75
84
|
const registeredWorkspaces = new Map<string, WorkspaceContext>();
|
|
@@ -121,6 +130,11 @@ export default definePluginEntry({
|
|
|
121
130
|
return;
|
|
122
131
|
}
|
|
123
132
|
|
|
133
|
+
const contentHash = crypto.createHash("md5").update(content).digest("hex");
|
|
134
|
+
if (lastSyncedHash.get(absPath) === contentHash) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
124
138
|
const relativePath = path.relative(ws.dir, absPath);
|
|
125
139
|
|
|
126
140
|
// 根据路径解析 userId
|
|
@@ -138,6 +152,7 @@ export default definePluginEntry({
|
|
|
138
152
|
const payload: IngestPayload = {
|
|
139
153
|
path: relativePath,
|
|
140
154
|
content,
|
|
155
|
+
release_name: parseReleaseName(content),
|
|
141
156
|
agentId: ws.agentId,
|
|
142
157
|
userId,
|
|
143
158
|
workspaceDir: ws.dir,
|
|
@@ -162,9 +177,29 @@ export default definePluginEntry({
|
|
|
162
177
|
clearTimeout(timeout);
|
|
163
178
|
|
|
164
179
|
if (res.ok) {
|
|
180
|
+
lastSyncedHash.set(absPath, contentHash);
|
|
165
181
|
api.logger.info?.(
|
|
166
182
|
`memory-gateway-sync: [${ws.agentId}] 同步成功 ${relativePath} (${content.length} chars)`
|
|
167
183
|
);
|
|
184
|
+
|
|
185
|
+
if (relativePath === "MEMORY.md" && content.trim().length > 0) {
|
|
186
|
+
try {
|
|
187
|
+
const backupPath = absPath + ".bak";
|
|
188
|
+
const timestamp = new Date().toISOString();
|
|
189
|
+
const section = `\n<!-- backup ${timestamp} -->\n${content}\n`;
|
|
190
|
+
await fs.promises.appendFile(backupPath, section, "utf-8");
|
|
191
|
+
await fs.promises.writeFile(absPath, "", "utf-8");
|
|
192
|
+
const emptyHash = crypto.createHash("md5").update("").digest("hex");
|
|
193
|
+
lastSyncedHash.set(absPath, emptyHash);
|
|
194
|
+
api.logger.info?.(
|
|
195
|
+
`memory-gateway-sync: [${ws.agentId}] MEMORY.md 已备份到 .bak 并清空`
|
|
196
|
+
);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
api.logger.warn(
|
|
199
|
+
`memory-gateway-sync: [${ws.agentId}] 备份/清空 MEMORY.md 失败: ${String(err)}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
168
203
|
} else {
|
|
169
204
|
const body = await res.text().catch(() => "");
|
|
170
205
|
api.logger.warn(
|
|
@@ -193,85 +228,6 @@ export default definePluginEntry({
|
|
|
193
228
|
);
|
|
194
229
|
};
|
|
195
230
|
|
|
196
|
-
// ── 递归监听目录中的 .md 文件 ────────────────────────────────────
|
|
197
|
-
|
|
198
|
-
const watchDirectory = (dirPath: string, wsDir: string): void => {
|
|
199
|
-
if (watchedDirs.has(dirPath)) return;
|
|
200
|
-
watchedDirs.add(dirPath);
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
fs.watch(dirPath, async (eventType, filename) => {
|
|
204
|
-
if (!filename) return;
|
|
205
|
-
|
|
206
|
-
const absFilePath = path.join(dirPath, filename);
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const stat = await fs.promises.stat(absFilePath);
|
|
210
|
-
|
|
211
|
-
if (stat.isDirectory()) {
|
|
212
|
-
scanAndWatch(absFilePath, wsDir, true);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (stat.isFile() && filename.endsWith(".md")) {
|
|
217
|
-
scheduleSync(absFilePath, eventType ?? "change");
|
|
218
|
-
}
|
|
219
|
-
} catch {
|
|
220
|
-
// stat 失败说明文件已删除,跳过
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const ws = registeredWorkspaces.get(wsDir);
|
|
225
|
-
const label = ws ? ws.agentId : "?";
|
|
226
|
-
api.logger.info?.(
|
|
227
|
-
`memory-gateway-sync: [${label}] 监听目录 ${path.relative(wsDir, dirPath) || "memory/"}`
|
|
228
|
-
);
|
|
229
|
-
} catch (err) {
|
|
230
|
-
api.logger.warn(
|
|
231
|
-
`memory-gateway-sync: 无法监听目录 ${dirPath}: ${String(err)}`
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
// ── 递归扫描已存在的子目录并注册 watcher ─────────────────────────
|
|
237
|
-
|
|
238
|
-
const scanAndWatch = async (
|
|
239
|
-
dirPath: string,
|
|
240
|
-
wsDir: string,
|
|
241
|
-
syncExisting = false
|
|
242
|
-
): Promise<void> => {
|
|
243
|
-
try {
|
|
244
|
-
await fs.promises.mkdir(dirPath, { recursive: true });
|
|
245
|
-
} catch {
|
|
246
|
-
// ignore
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
watchDirectory(dirPath, wsDir);
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
const entries = await fs.promises.readdir(dirPath, {
|
|
253
|
-
withFileTypes: true,
|
|
254
|
-
});
|
|
255
|
-
for (const entry of entries) {
|
|
256
|
-
if (entry.isDirectory()) {
|
|
257
|
-
await scanAndWatch(
|
|
258
|
-
path.join(dirPath, entry.name),
|
|
259
|
-
wsDir,
|
|
260
|
-
syncExisting
|
|
261
|
-
);
|
|
262
|
-
} else if (
|
|
263
|
-
syncExisting &&
|
|
264
|
-
entry.isFile() &&
|
|
265
|
-
entry.name.endsWith(".md")
|
|
266
|
-
) {
|
|
267
|
-
scheduleSync(path.join(dirPath, entry.name), "change");
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
} catch {
|
|
271
|
-
// ignore
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
|
|
275
231
|
// ── 注册单个 workspace ───────────────────────────────────────────
|
|
276
232
|
|
|
277
233
|
const registerWorkspace = async (wsDir: string, wsAgentId: string): Promise<void> => {
|
|
@@ -295,10 +251,6 @@ export default definePluginEntry({
|
|
|
295
251
|
);
|
|
296
252
|
}
|
|
297
253
|
|
|
298
|
-
// 递归监听 memory/ 目录
|
|
299
|
-
const memoryDirPath = path.join(wsDir, "memory");
|
|
300
|
-
await scanAndWatch(memoryDirPath, wsDir);
|
|
301
|
-
|
|
302
254
|
api.logger.info(
|
|
303
255
|
`memory-gateway-sync: [${wsAgentId}] workspace 注册完成 → ${wsDir}`
|
|
304
256
|
);
|