memory-gateway-sync 0.2.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/README.md +113 -0
- package/openclaw.plugin.json +29 -0
- package/package.json +14 -0
- package/src/index.ts +361 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# OpenClaw 插件侧方案
|
|
2
|
+
|
|
3
|
+
## 插件信息
|
|
4
|
+
|
|
5
|
+
| 项目 | 值 |
|
|
6
|
+
|------|----|
|
|
7
|
+
| 插件 ID | `memory-gateway-sync` |
|
|
8
|
+
| kind | `generic`(不替换 memory slot) |
|
|
9
|
+
| 安装路径 | `/home/node/.openclaw/extensions/memory-gateway-sync/` |
|
|
10
|
+
| 作用 | 监听 MEMORY.md / memory/*.md 变化,POST 到 Gateway |
|
|
11
|
+
|
|
12
|
+
## 目录结构(部署到容器内)
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
memory-gateway-sync/
|
|
16
|
+
├── openclaw.plugin.json
|
|
17
|
+
├── package.json
|
|
18
|
+
└── src/
|
|
19
|
+
└── index.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## openclaw.plugin.json
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"id": "memory-gateway-sync",
|
|
27
|
+
"name": "Memory Gateway Sync",
|
|
28
|
+
"description": "fs.watch 监听 MEMORY 文件变化,双写到外部 Gateway",
|
|
29
|
+
"configSchema": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"properties": {
|
|
33
|
+
"gatewayUrl": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Gateway POST 地址"
|
|
36
|
+
},
|
|
37
|
+
"gatewayToken": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Bearer Token"
|
|
40
|
+
},
|
|
41
|
+
"debounceMs": {
|
|
42
|
+
"type": "number",
|
|
43
|
+
"default": 1500,
|
|
44
|
+
"description": "防抖延迟毫秒数"
|
|
45
|
+
},
|
|
46
|
+
"agentId": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "当前 agent 标识,写入 Gateway 时携带"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## package.json
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"name": "@openclaw/memory-gateway-sync",
|
|
60
|
+
"version": "0.1.0",
|
|
61
|
+
"type": "module",
|
|
62
|
+
"openclaw": {
|
|
63
|
+
"extensions": ["./src/index.ts"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## openclaw.json 配置(在容器内 /home/node/.openclaw/openclaw.json 中添加)
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
"plugins": {
|
|
72
|
+
"entries": {
|
|
73
|
+
"memory-gateway-sync": {
|
|
74
|
+
"enabled": true,
|
|
75
|
+
"config": {
|
|
76
|
+
"gatewayUrl": "http://172.22.39.89:8765/api/memory/ingest",
|
|
77
|
+
"gatewayToken": "your-secret-token",
|
|
78
|
+
"debounceMs": 1500,
|
|
79
|
+
"agentId": "main"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 核心逻辑说明(src/index.ts)
|
|
87
|
+
|
|
88
|
+
### 启动时
|
|
89
|
+
|
|
90
|
+
1. 从插件配置读取 `gatewayUrl`、`gatewayToken`、`debounceMs`
|
|
91
|
+
2. 计算 workspace 路径:`~/.openclaw/workspace/`
|
|
92
|
+
3. 确保 `MEMORY.md` 和 `memory/` 目录存在
|
|
93
|
+
4. 分别挂载 `fs.watch`
|
|
94
|
+
|
|
95
|
+
### 文件变化时
|
|
96
|
+
|
|
97
|
+
1. 记录文件路径,启动防抖计时(同一文件 debounceMs 内只处理一次)
|
|
98
|
+
2. 计时结束后:读取文件完整内容
|
|
99
|
+
3. 构造 payload:`{ path, content, agentId, syncedAt }`
|
|
100
|
+
4. `fetch POST` 到 Gateway
|
|
101
|
+
5. 成功:打 info 日志;失败:打 warn 日志,**不抛异常**
|
|
102
|
+
|
|
103
|
+
### 不处理的情况
|
|
104
|
+
|
|
105
|
+
- 非 `.md` 文件的变化
|
|
106
|
+
- `memory/` 二级子目录(暂不递归,可后续扩展)
|
|
107
|
+
- 文件删除事件(只关心写入)
|
|
108
|
+
|
|
109
|
+
## 关键约束
|
|
110
|
+
|
|
111
|
+
- Gateway 失败不能影响 OpenClaw 正常运行(所有 Gateway 调用都 try/catch)
|
|
112
|
+
- 不替换 memory-core,不干预读取链路
|
|
113
|
+
- 只监听两个位置:`MEMORY.md` 和 `memory/`,不监听其他文件
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "memory-gateway-sync",
|
|
3
|
+
"name": "Memory Gateway Sync",
|
|
4
|
+
"description": "监听 MEMORY.md / memory/*.md 文件变化,双写到外部 Gateway",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"gatewayUrl": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Gateway POST 地址,如 http://172.22.39.89:8765/api/memory/ingest"
|
|
12
|
+
},
|
|
13
|
+
"gatewayToken": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Bearer Token,与 Gateway 的 GATEWAY_TOKEN 环境变量一致"
|
|
16
|
+
},
|
|
17
|
+
"debounceMs": {
|
|
18
|
+
"type": "number",
|
|
19
|
+
"default": 1500,
|
|
20
|
+
"description": "防抖延迟毫秒数,同一文件在此时间内多次变化只触发一次同步"
|
|
21
|
+
},
|
|
22
|
+
"agentId": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "当前 agent 标识,写入 Gateway 时携带,用于后续多 agent 隔离"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["gatewayUrl", "gatewayToken"]
|
|
28
|
+
}
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memory-gateway-sync",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Memory 双写插件:fs.watch 感知变化后同步到外部 Gateway",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"openclaw": {
|
|
7
|
+
"extensions": [
|
|
8
|
+
"./src/index.ts"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"typescript": "^5.4.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
// ── 类型 ──────────────────────────────────��───────────────────────────────────
|
|
7
|
+
|
|
8
|
+
interface PluginConfig {
|
|
9
|
+
gatewayUrl: string;
|
|
10
|
+
gatewayToken: string;
|
|
11
|
+
debounceMs?: number;
|
|
12
|
+
agentId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IngestPayload {
|
|
16
|
+
path: string;
|
|
17
|
+
content: string;
|
|
18
|
+
agentId: string;
|
|
19
|
+
workspaceDir: string;
|
|
20
|
+
eventType: string;
|
|
21
|
+
syncedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface WorkspaceContext {
|
|
25
|
+
dir: string;
|
|
26
|
+
agentId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── 插件入口 ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export default definePluginEntry({
|
|
32
|
+
id: "memory-gateway-sync",
|
|
33
|
+
name: "Memory Gateway Sync",
|
|
34
|
+
description:
|
|
35
|
+
"fs.watch 监听多 workspace 的 MEMORY 文件变化,双写到外部 Gateway(支持 peers/groups/knowledge 子目录)",
|
|
36
|
+
kind: "generic",
|
|
37
|
+
|
|
38
|
+
register(api) {
|
|
39
|
+
const cfg = (api.pluginConfig ?? {}) as PluginConfig;
|
|
40
|
+
const gatewayUrl = cfg.gatewayUrl;
|
|
41
|
+
const gatewayToken = cfg.gatewayToken;
|
|
42
|
+
const debounceMs = cfg.debounceMs ?? 1500;
|
|
43
|
+
const defaultAgentId = cfg.agentId ?? "default";
|
|
44
|
+
|
|
45
|
+
if (!gatewayUrl || !gatewayToken) {
|
|
46
|
+
api.logger.warn(
|
|
47
|
+
"memory-gateway-sync: gatewayUrl 或 gatewayToken 未配置,插件不启动"
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
api.registerService({
|
|
53
|
+
id: "memory-gateway-sync",
|
|
54
|
+
|
|
55
|
+
start: async () => {
|
|
56
|
+
const openclawDir = path.join(os.homedir(), ".openclaw");
|
|
57
|
+
|
|
58
|
+
// 防抖 Map:key=文件绝对路径,value=setTimeout 句柄
|
|
59
|
+
const debounceTimers = new Map<
|
|
60
|
+
string,
|
|
61
|
+
ReturnType<typeof setTimeout>
|
|
62
|
+
>();
|
|
63
|
+
|
|
64
|
+
// 已注册 watcher 的目录集合(避免重复监听)
|
|
65
|
+
const watchedDirs = new Set<string>();
|
|
66
|
+
|
|
67
|
+
// 已注册的 workspace 集合(避免重复注册)
|
|
68
|
+
const registeredWorkspaces = new Map<string, WorkspaceContext>();
|
|
69
|
+
|
|
70
|
+
// ── 根据文件路径找到所属 workspace ────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const findWorkspace = (absPath: string): WorkspaceContext | null => {
|
|
73
|
+
for (const [wsDir, ctx] of registeredWorkspaces) {
|
|
74
|
+
if (absPath.startsWith(wsDir + path.sep) || absPath === wsDir) {
|
|
75
|
+
return ctx;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ── 从目录名解析 agentId ─────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const resolveAgentId = (dirName: string): string => {
|
|
84
|
+
if (dirName === "workspace") {
|
|
85
|
+
return defaultAgentId;
|
|
86
|
+
}
|
|
87
|
+
if (dirName.startsWith("workspace-")) {
|
|
88
|
+
return dirName.substring("workspace-".length);
|
|
89
|
+
}
|
|
90
|
+
return defaultAgentId;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ── 核心:同步单个文件到 Gateway ──────────────────────────────────
|
|
94
|
+
|
|
95
|
+
const syncFile = async (
|
|
96
|
+
absPath: string,
|
|
97
|
+
eventType: string
|
|
98
|
+
): Promise<void> => {
|
|
99
|
+
const ws = findWorkspace(absPath);
|
|
100
|
+
if (!ws) {
|
|
101
|
+
api.logger.warn(
|
|
102
|
+
`memory-gateway-sync: 找不到所属 workspace: ${absPath}`
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let content: string;
|
|
108
|
+
try {
|
|
109
|
+
content = await fs.promises.readFile(absPath, "utf-8");
|
|
110
|
+
} catch (err) {
|
|
111
|
+
api.logger.warn(
|
|
112
|
+
`memory-gateway-sync: 读取文件失败 ${absPath}: ${String(err)}`
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const relativePath = path.relative(ws.dir, absPath);
|
|
118
|
+
const payload: IngestPayload = {
|
|
119
|
+
path: relativePath,
|
|
120
|
+
content,
|
|
121
|
+
agentId: ws.agentId,
|
|
122
|
+
workspaceDir: ws.dir,
|
|
123
|
+
eventType,
|
|
124
|
+
syncedAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
130
|
+
|
|
131
|
+
const res = await fetch(gatewayUrl, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
Authorization: `Bearer ${gatewayToken}`,
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(payload),
|
|
138
|
+
signal: controller.signal,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
|
|
143
|
+
if (res.ok) {
|
|
144
|
+
api.logger.info?.(
|
|
145
|
+
`memory-gateway-sync: [${ws.agentId}] 同步成功 ${relativePath} (${content.length} chars)`
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
const body = await res.text().catch(() => "");
|
|
149
|
+
api.logger.warn(
|
|
150
|
+
`memory-gateway-sync: [${ws.agentId}] Gateway 返回 ${res.status} for ${relativePath}: ${body}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
api.logger.warn(
|
|
155
|
+
`memory-gateway-sync: [${ws.agentId}] POST 失败 ${relativePath}: ${String(err)}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ── 防抖调度 ─────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
const scheduleSync = (absPath: string, eventType: string): void => {
|
|
163
|
+
const existing = debounceTimers.get(absPath);
|
|
164
|
+
if (existing) clearTimeout(existing);
|
|
165
|
+
|
|
166
|
+
debounceTimers.set(
|
|
167
|
+
absPath,
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
debounceTimers.delete(absPath);
|
|
170
|
+
syncFile(absPath, eventType);
|
|
171
|
+
}, debounceMs)
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// ── 递归监听目录中的 .md 文件 ────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
const watchDirectory = (dirPath: string, wsDir: string): void => {
|
|
178
|
+
if (watchedDirs.has(dirPath)) return;
|
|
179
|
+
watchedDirs.add(dirPath);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
fs.watch(dirPath, async (eventType, filename) => {
|
|
183
|
+
if (!filename) return;
|
|
184
|
+
|
|
185
|
+
const absFilePath = path.join(dirPath, filename);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const stat = await fs.promises.stat(absFilePath);
|
|
189
|
+
|
|
190
|
+
if (stat.isDirectory()) {
|
|
191
|
+
scanAndWatch(absFilePath, wsDir, true);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (stat.isFile() && filename.endsWith(".md")) {
|
|
196
|
+
scheduleSync(absFilePath, eventType ?? "change");
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// stat 失败说明文件已删除,跳过
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const ws = registeredWorkspaces.get(wsDir);
|
|
204
|
+
const label = ws ? ws.agentId : "?";
|
|
205
|
+
api.logger.info?.(
|
|
206
|
+
`memory-gateway-sync: [${label}] 监听目录 ${path.relative(wsDir, dirPath) || "memory/"}`
|
|
207
|
+
);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
api.logger.warn(
|
|
210
|
+
`memory-gateway-sync: 无法监听目录 ${dirPath}: ${String(err)}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// ── 递归扫描已存在的子目录并注册 watcher ─────────────────────────
|
|
216
|
+
|
|
217
|
+
const scanAndWatch = async (
|
|
218
|
+
dirPath: string,
|
|
219
|
+
wsDir: string,
|
|
220
|
+
syncExisting = false
|
|
221
|
+
): Promise<void> => {
|
|
222
|
+
try {
|
|
223
|
+
await fs.promises.mkdir(dirPath, { recursive: true });
|
|
224
|
+
} catch {
|
|
225
|
+
// ignore
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
watchDirectory(dirPath, wsDir);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const entries = await fs.promises.readdir(dirPath, {
|
|
232
|
+
withFileTypes: true,
|
|
233
|
+
});
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
if (entry.isDirectory()) {
|
|
236
|
+
await scanAndWatch(
|
|
237
|
+
path.join(dirPath, entry.name),
|
|
238
|
+
wsDir,
|
|
239
|
+
syncExisting
|
|
240
|
+
);
|
|
241
|
+
} else if (
|
|
242
|
+
syncExisting &&
|
|
243
|
+
entry.isFile() &&
|
|
244
|
+
entry.name.endsWith(".md")
|
|
245
|
+
) {
|
|
246
|
+
scheduleSync(path.join(dirPath, entry.name), "change");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// ignore
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// ── 注册单个 workspace ───────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
const registerWorkspace = async (wsDir: string, wsAgentId: string): Promise<void> => {
|
|
257
|
+
if (registeredWorkspaces.has(wsDir)) return;
|
|
258
|
+
|
|
259
|
+
registeredWorkspaces.set(wsDir, { dir: wsDir, agentId: wsAgentId });
|
|
260
|
+
|
|
261
|
+
// 监听 MEMORY.md
|
|
262
|
+
const memoryMdPath = path.join(wsDir, "MEMORY.md");
|
|
263
|
+
try {
|
|
264
|
+
await fs.promises.writeFile(memoryMdPath, "", { flag: "a" });
|
|
265
|
+
fs.watch(memoryMdPath, (eventType) => {
|
|
266
|
+
scheduleSync(memoryMdPath, eventType ?? "change");
|
|
267
|
+
});
|
|
268
|
+
api.logger.info?.(
|
|
269
|
+
`memory-gateway-sync: [${wsAgentId}] 开始监听 MEMORY.md`
|
|
270
|
+
);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
api.logger.warn(
|
|
273
|
+
`memory-gateway-sync: [${wsAgentId}] 无法监听 MEMORY.md: ${String(err)}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 递归监听 memory/ 目录
|
|
278
|
+
const memoryDirPath = path.join(wsDir, "memory");
|
|
279
|
+
await scanAndWatch(memoryDirPath, wsDir);
|
|
280
|
+
|
|
281
|
+
api.logger.info(
|
|
282
|
+
`memory-gateway-sync: [${wsAgentId}] workspace 注册完成 → ${wsDir}`
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// ── 扫描所有 workspace 目录 ──────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
const scanWorkspaces = async (): Promise<void> => {
|
|
289
|
+
try {
|
|
290
|
+
const entries = await fs.promises.readdir(openclawDir, {
|
|
291
|
+
withFileTypes: true,
|
|
292
|
+
});
|
|
293
|
+
for (const entry of entries) {
|
|
294
|
+
if (!entry.isDirectory()) continue;
|
|
295
|
+
const name = entry.name;
|
|
296
|
+
|
|
297
|
+
if (name === "workspace" || name.startsWith("workspace-")) {
|
|
298
|
+
const wsDir = path.join(openclawDir, name);
|
|
299
|
+
const wsAgentId = resolveAgentId(name);
|
|
300
|
+
await registerWorkspace(wsDir, wsAgentId);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
api.logger.warn(
|
|
305
|
+
`memory-gateway-sync: 扫描 workspace 失败: ${String(err)}`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// ── 监听 .openclaw/ 目录,动态感知新 workspace ──────────────────
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
fs.watch(openclawDir, async (eventType, filename) => {
|
|
314
|
+
if (!filename) return;
|
|
315
|
+
if (
|
|
316
|
+
filename !== "workspace" &&
|
|
317
|
+
!filename.startsWith("workspace-")
|
|
318
|
+
) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const wsDir = path.join(openclawDir, filename);
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const stat = await fs.promises.stat(wsDir);
|
|
326
|
+
if (stat.isDirectory() && !registeredWorkspaces.has(wsDir)) {
|
|
327
|
+
const wsAgentId = resolveAgentId(filename);
|
|
328
|
+
api.logger.info(
|
|
329
|
+
`memory-gateway-sync: 发现新 workspace → ${filename} (agentId=${wsAgentId})`
|
|
330
|
+
);
|
|
331
|
+
await registerWorkspace(wsDir, wsAgentId);
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// 目录不存在或已删除,忽略
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
} catch (err) {
|
|
338
|
+
api.logger.warn(
|
|
339
|
+
`memory-gateway-sync: 无法监听 ${openclawDir}: ${String(err)}`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── 启动:扫描所有已有 workspace ─────────────────────────────────
|
|
344
|
+
|
|
345
|
+
await scanWorkspaces();
|
|
346
|
+
|
|
347
|
+
const wsCount = registeredWorkspaces.size;
|
|
348
|
+
const wsIds = Array.from(registeredWorkspaces.values())
|
|
349
|
+
.map((w) => w.agentId)
|
|
350
|
+
.join(", ");
|
|
351
|
+
api.logger.info(
|
|
352
|
+
`memory-gateway-sync: 启动完成 → ${gatewayUrl} (防抖 ${debounceMs}ms, ${wsCount} 个 workspace: ${wsIds})`
|
|
353
|
+
);
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
stop: () => {
|
|
357
|
+
api.logger.info("memory-gateway-sync: 停止");
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
});
|