@memtensor/memos-local-openclaw-plugin 1.0.4-beta.7 → 1.0.4-beta.8
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 +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +6 -0
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +17 -4
- package/dist/client/connector.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -2
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +7 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +160 -5
- package/dist/hub/server.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +82 -8
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/skill/evolver.d.ts +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +3 -0
- package/dist/skill/evolver.js.map +1 -1
- package/dist/storage/sqlite.d.ts +4 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +8 -4
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +135 -38
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +473 -191
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +14 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +233 -20
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +26 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -2
- package/scripts/postinstall.cjs +1 -1
- package/src/capture/index.ts +8 -0
- package/src/client/connector.ts +17 -4
- package/src/config.ts +0 -2
- package/src/hub/server.ts +157 -5
- package/src/ingest/providers/index.ts +41 -7
- package/src/shared/llm-call.ts +97 -9
- package/src/skill/evolver.ts +5 -0
- package/src/storage/sqlite.ts +11 -6
- package/src/telemetry.ts +152 -39
- package/src/types.ts +1 -2
- package/src/viewer/html.ts +473 -191
- package/src/viewer/server.ts +208 -20
package/src/viewer/server.ts
CHANGED
|
@@ -84,6 +84,12 @@ export class ViewerServer {
|
|
|
84
84
|
{ running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
|
|
85
85
|
private ppSSEClients: http.ServerResponse[] = [];
|
|
86
86
|
|
|
87
|
+
private notifSSEClients: http.ServerResponse[] = [];
|
|
88
|
+
private notifPollTimer?: ReturnType<typeof setInterval>;
|
|
89
|
+
private lastKnownNotifCount = 0;
|
|
90
|
+
private hubHeartbeatTimer?: ReturnType<typeof setInterval>;
|
|
91
|
+
private static readonly HUB_HEARTBEAT_INTERVAL_MS = 45_000;
|
|
92
|
+
|
|
87
93
|
constructor(opts: ViewerServerOptions) {
|
|
88
94
|
this.store = opts.store;
|
|
89
95
|
this.embedder = opts.embedder;
|
|
@@ -103,15 +109,16 @@ export class ViewerServer {
|
|
|
103
109
|
this.server.on("error", (err: NodeJS.ErrnoException) => {
|
|
104
110
|
if (err.code === "EADDRINUSE") {
|
|
105
111
|
this.log.warn(`Viewer port ${this.port} in use, trying ${this.port + 1}`);
|
|
106
|
-
this.server!.listen(this.port + 1, "
|
|
112
|
+
this.server!.listen(this.port + 1, "0.0.0.0");
|
|
107
113
|
} else {
|
|
108
114
|
reject(err);
|
|
109
115
|
}
|
|
110
116
|
});
|
|
111
|
-
this.server.listen(this.port, "
|
|
117
|
+
this.server.listen(this.port, "0.0.0.0", () => {
|
|
112
118
|
const addr = this.server!.address();
|
|
113
119
|
const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
|
|
114
120
|
this.autoCleanupPolluted();
|
|
121
|
+
this.startHubHeartbeat();
|
|
115
122
|
resolve(`http://127.0.0.1:${actualPort}`);
|
|
116
123
|
});
|
|
117
124
|
});
|
|
@@ -134,6 +141,10 @@ export class ViewerServer {
|
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
stop(): void {
|
|
144
|
+
this.stopHubHeartbeat();
|
|
145
|
+
this.stopNotifPoll();
|
|
146
|
+
for (const c of this.notifSSEClients) { try { c.end(); } catch {} }
|
|
147
|
+
this.notifSSEClients = [];
|
|
137
148
|
this.server?.close();
|
|
138
149
|
this.server = null;
|
|
139
150
|
}
|
|
@@ -279,9 +290,12 @@ export class ViewerServer {
|
|
|
279
290
|
else if (p === "/api/sharing/notifications" && req.method === "GET") this.serveSharingNotifications(res, url);
|
|
280
291
|
else if (p === "/api/sharing/notifications/read" && req.method === "POST") this.handleSharingNotificationsRead(req, res);
|
|
281
292
|
else if (p === "/api/sharing/notifications/clear" && req.method === "POST") this.handleSharingNotificationsClear(req, res);
|
|
293
|
+
else if (p === "/api/notifications/stream" && req.method === "GET") this.handleNotifSSE(req, res);
|
|
282
294
|
else if (p === "/api/admin/shared-tasks" && req.method === "GET") this.serveAdminSharedTasks(res);
|
|
295
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+\/detail$/) && req.method === "GET") this.serveHubTaskDetail(res, p);
|
|
283
296
|
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteTask(res, p);
|
|
284
297
|
else if (p === "/api/admin/shared-skills" && req.method === "GET") this.serveAdminSharedSkills(res);
|
|
298
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+\/detail$/) && req.method === "GET") this.serveHubSkillDetail(res, p);
|
|
285
299
|
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteSkill(res, p);
|
|
286
300
|
else if (p === "/api/admin/shared-memories" && req.method === "GET") this.serveAdminSharedMemories(res);
|
|
287
301
|
else if (p.match(/^\/api\/admin\/shared-memories\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteMemory(res, p);
|
|
@@ -436,7 +450,13 @@ export class ViewerServer {
|
|
|
436
450
|
const params: any[] = [];
|
|
437
451
|
if (session) { conditions.push("session_key = ?"); params.push(session); }
|
|
438
452
|
if (role) { conditions.push("role = ?"); params.push(role); }
|
|
439
|
-
if (owner
|
|
453
|
+
if (owner && owner.startsWith("agent:")) {
|
|
454
|
+
const agentPrefix = owner + ":";
|
|
455
|
+
conditions.push("(owner = ? OR (owner = 'public' AND session_key LIKE ?))");
|
|
456
|
+
params.push(owner, agentPrefix + "%");
|
|
457
|
+
} else if (owner) {
|
|
458
|
+
conditions.push("owner = ?"); params.push(owner);
|
|
459
|
+
}
|
|
440
460
|
if (dateFrom) { conditions.push("created_at >= ?"); params.push(new Date(dateFrom).getTime()); }
|
|
441
461
|
if (dateTo) { conditions.push("created_at <= ?"); params.push(new Date(dateTo).getTime()); }
|
|
442
462
|
|
|
@@ -510,9 +530,10 @@ export class ViewerServer {
|
|
|
510
530
|
private serveTasks(res: http.ServerResponse, url: URL): void {
|
|
511
531
|
this.store.recordViewerEvent("tasks_list");
|
|
512
532
|
const status = url.searchParams.get("status") ?? undefined;
|
|
533
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
513
534
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
514
535
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
515
|
-
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
536
|
+
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
|
|
516
537
|
|
|
517
538
|
const db = (this.store as any).db;
|
|
518
539
|
const items = tasks.map((t) => {
|
|
@@ -616,12 +637,20 @@ export class ViewerServer {
|
|
|
616
637
|
}
|
|
617
638
|
let embCount = 0;
|
|
618
639
|
try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
640
|
+
let sessionQuery: string;
|
|
641
|
+
let sessionParams: any[];
|
|
642
|
+
if (ownerFilter && ownerFilter.startsWith("agent:")) {
|
|
643
|
+
const agentPrefix = ownerFilter + ":";
|
|
644
|
+
sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE (owner = ? OR (owner = 'public' AND session_key LIKE ?)) GROUP BY session_key ORDER BY latest DESC";
|
|
645
|
+
sessionParams = [ownerFilter, agentPrefix + "%"];
|
|
646
|
+
} else if (ownerFilter) {
|
|
647
|
+
sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE owner = ? GROUP BY session_key ORDER BY latest DESC";
|
|
648
|
+
sessionParams = [ownerFilter];
|
|
649
|
+
} else {
|
|
650
|
+
sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks GROUP BY session_key ORDER BY latest DESC";
|
|
651
|
+
sessionParams = [];
|
|
652
|
+
}
|
|
653
|
+
const sessionList = db.prepare(sessionQuery).all(...sessionParams) as any[];
|
|
625
654
|
|
|
626
655
|
let skillCount = 0;
|
|
627
656
|
try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
|
|
@@ -634,7 +663,7 @@ export class ViewerServer {
|
|
|
634
663
|
|
|
635
664
|
let owners: string[] = [];
|
|
636
665
|
try {
|
|
637
|
-
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL ORDER BY owner").all() as any[];
|
|
666
|
+
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY owner").all() as any[];
|
|
638
667
|
owners = ownerRows.map((o: any) => o.owner);
|
|
639
668
|
} catch { /* column may not exist yet */ }
|
|
640
669
|
|
|
@@ -1470,7 +1499,8 @@ export class ViewerServer {
|
|
|
1470
1499
|
|
|
1471
1500
|
private getOpenClawConfigPath(): string {
|
|
1472
1501
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1473
|
-
|
|
1502
|
+
const ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
|
|
1503
|
+
return path.join(ocHome, "openclaw.json");
|
|
1474
1504
|
}
|
|
1475
1505
|
|
|
1476
1506
|
private getPluginEntryConfig(raw: any): Record<string, unknown> {
|
|
@@ -1713,7 +1743,8 @@ export class ViewerServer {
|
|
|
1713
1743
|
try {
|
|
1714
1744
|
const hubUrl = normalizeHubUrl(hubAddress);
|
|
1715
1745
|
const os = await import("os");
|
|
1716
|
-
const
|
|
1746
|
+
const nickname = sharing.client?.nickname;
|
|
1747
|
+
const username = nickname || os.userInfo().username || "user";
|
|
1717
1748
|
const hostname = os.hostname() || "unknown";
|
|
1718
1749
|
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
1719
1750
|
method: "POST",
|
|
@@ -1738,7 +1769,13 @@ export class ViewerServer {
|
|
|
1738
1769
|
if (!this.ctx) return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1739
1770
|
try {
|
|
1740
1771
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1741
|
-
const
|
|
1772
|
+
const hub = this.resolveHubConnection();
|
|
1773
|
+
let data: any;
|
|
1774
|
+
if (hub) {
|
|
1775
|
+
data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/memories?limit=${limit}`);
|
|
1776
|
+
} else {
|
|
1777
|
+
data = await hubListMemories(this.store, this.ctx, { limit });
|
|
1778
|
+
}
|
|
1742
1779
|
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1743
1780
|
} catch (err) {
|
|
1744
1781
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
@@ -1749,7 +1786,13 @@ export class ViewerServer {
|
|
|
1749
1786
|
if (!this.ctx) return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1750
1787
|
try {
|
|
1751
1788
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1752
|
-
const
|
|
1789
|
+
const hub = this.resolveHubConnection();
|
|
1790
|
+
let data: any;
|
|
1791
|
+
if (hub) {
|
|
1792
|
+
data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/tasks?limit=${limit}`);
|
|
1793
|
+
} else {
|
|
1794
|
+
data = await hubListTasks(this.store, this.ctx, { limit });
|
|
1795
|
+
}
|
|
1753
1796
|
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1754
1797
|
} catch (err) {
|
|
1755
1798
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
@@ -1760,7 +1803,13 @@ export class ViewerServer {
|
|
|
1760
1803
|
if (!this.ctx) return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1761
1804
|
try {
|
|
1762
1805
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1763
|
-
const
|
|
1806
|
+
const hub = this.resolveHubConnection();
|
|
1807
|
+
let data: any;
|
|
1808
|
+
if (hub) {
|
|
1809
|
+
data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/skills/list?limit=${limit}`);
|
|
1810
|
+
} else {
|
|
1811
|
+
data = await hubListSkills(this.store, this.ctx, { limit });
|
|
1812
|
+
}
|
|
1764
1813
|
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1765
1814
|
} catch (err) {
|
|
1766
1815
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
@@ -1776,13 +1825,21 @@ export class ViewerServer {
|
|
|
1776
1825
|
const query = String(parsed.query || "");
|
|
1777
1826
|
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1778
1827
|
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1779
|
-
const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
|
|
1828
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
|
|
1780
1829
|
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1781
1830
|
if (scope === "local") {
|
|
1782
1831
|
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1783
1832
|
}
|
|
1784
1833
|
try {
|
|
1785
|
-
const
|
|
1834
|
+
const conn = this.resolveHubConnection();
|
|
1835
|
+
let hub: any;
|
|
1836
|
+
if (conn) {
|
|
1837
|
+
hub = await hubRequestJson(conn.hubUrl, conn.userToken, "/api/v1/hub/search", {
|
|
1838
|
+
method: "POST", body: JSON.stringify({ query, maxResults, scope }),
|
|
1839
|
+
});
|
|
1840
|
+
} else {
|
|
1841
|
+
hub = await hubSearchMemories(this.store, this.ctx!, { query, maxResults, scope });
|
|
1842
|
+
}
|
|
1786
1843
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1787
1844
|
} catch (err) {
|
|
1788
1845
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
|
|
@@ -2192,6 +2249,34 @@ export class ViewerServer {
|
|
|
2192
2249
|
}
|
|
2193
2250
|
}
|
|
2194
2251
|
|
|
2252
|
+
private async serveHubTaskDetail(res: http.ServerResponse, p: string): Promise<void> {
|
|
2253
|
+
const hub = this.resolveHubConnection();
|
|
2254
|
+
if (!hub) return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2255
|
+
const m = p.match(/^\/api\/admin\/shared-tasks\/([^/]+)\/detail$/);
|
|
2256
|
+
if (!m) return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2257
|
+
const taskId = decodeURIComponent(m[1]);
|
|
2258
|
+
try {
|
|
2259
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-tasks/${encodeURIComponent(taskId)}/detail`, { method: "GET" }) as any;
|
|
2260
|
+
this.jsonResponse(res, data);
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
private async serveHubSkillDetail(res: http.ServerResponse, p: string): Promise<void> {
|
|
2267
|
+
const hub = this.resolveHubConnection();
|
|
2268
|
+
if (!hub) return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2269
|
+
const m = p.match(/^\/api\/admin\/shared-skills\/([^/]+)\/detail$/);
|
|
2270
|
+
if (!m) return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2271
|
+
const skillId = decodeURIComponent(m[1]);
|
|
2272
|
+
try {
|
|
2273
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-skills/${encodeURIComponent(skillId)}/detail`, { method: "GET" }) as any;
|
|
2274
|
+
this.jsonResponse(res, data);
|
|
2275
|
+
} catch (err) {
|
|
2276
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2195
2280
|
private async serveAdminSharedSkills(res: http.ServerResponse): Promise<void> {
|
|
2196
2281
|
const hub = this.resolveHubConnection();
|
|
2197
2282
|
if (!hub) return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
@@ -2272,6 +2357,12 @@ export class ViewerServer {
|
|
|
2272
2357
|
const body = JSON.parse(raw || "{}");
|
|
2273
2358
|
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2274
2359
|
this.jsonResponse(res, { ok: true });
|
|
2360
|
+
try {
|
|
2361
|
+
const data = (await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")) as any;
|
|
2362
|
+
const count = data?.unreadCount ?? 0;
|
|
2363
|
+
this.lastKnownNotifCount = count;
|
|
2364
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2365
|
+
} catch { /* best effort */ }
|
|
2275
2366
|
} catch (err) {
|
|
2276
2367
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2277
2368
|
}
|
|
@@ -2285,12 +2376,81 @@ export class ViewerServer {
|
|
|
2285
2376
|
try {
|
|
2286
2377
|
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2287
2378
|
this.jsonResponse(res, { ok: true });
|
|
2379
|
+
this.broadcastNotifSSE({ type: "cleared", unreadCount: 0 });
|
|
2288
2380
|
} catch (err) {
|
|
2289
2381
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2290
2382
|
}
|
|
2291
2383
|
});
|
|
2292
2384
|
}
|
|
2293
2385
|
|
|
2386
|
+
private handleNotifSSE(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2387
|
+
res.writeHead(200, {
|
|
2388
|
+
"Content-Type": "text/event-stream",
|
|
2389
|
+
"Cache-Control": "no-cache",
|
|
2390
|
+
Connection: "keep-alive",
|
|
2391
|
+
"Access-Control-Allow-Origin": "*",
|
|
2392
|
+
});
|
|
2393
|
+
res.write("data: {\"type\":\"connected\"}\n\n");
|
|
2394
|
+
this.notifSSEClients.push(res);
|
|
2395
|
+
if (!this.notifPollTimer) this.startNotifPoll();
|
|
2396
|
+
req.on("close", () => {
|
|
2397
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2398
|
+
if (this.notifSSEClients.length === 0) this.stopNotifPoll();
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
private broadcastNotifSSE(data: Record<string, unknown>): void {
|
|
2403
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
2404
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => {
|
|
2405
|
+
try { c.write(msg); return true; } catch { return false; }
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
private startNotifPoll(): void {
|
|
2410
|
+
this.stopNotifPoll();
|
|
2411
|
+
const tick = async () => {
|
|
2412
|
+
const hub = this.resolveHubConnection();
|
|
2413
|
+
if (!hub) return;
|
|
2414
|
+
try {
|
|
2415
|
+
const data = (await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")) as any;
|
|
2416
|
+
const count = data?.unreadCount ?? 0;
|
|
2417
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2418
|
+
this.lastKnownNotifCount = count;
|
|
2419
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2420
|
+
}
|
|
2421
|
+
} catch { /* ignore */ }
|
|
2422
|
+
};
|
|
2423
|
+
tick();
|
|
2424
|
+
this.notifPollTimer = setInterval(tick, 3000);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
private stopNotifPoll(): void {
|
|
2428
|
+
if (this.notifPollTimer) { clearInterval(this.notifPollTimer); this.notifPollTimer = undefined; }
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
private startHubHeartbeat(): void {
|
|
2432
|
+
this.stopHubHeartbeat();
|
|
2433
|
+
const sendHeartbeat = async () => {
|
|
2434
|
+
try {
|
|
2435
|
+
const hub = this.resolveHubConnection();
|
|
2436
|
+
if (!hub) {
|
|
2437
|
+
const persisted = this.store.getClientHubConnection();
|
|
2438
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2439
|
+
await hubRequestJson(persisted.hubUrl, persisted.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2440
|
+
}
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2444
|
+
} catch { /* best-effort */ }
|
|
2445
|
+
};
|
|
2446
|
+
sendHeartbeat();
|
|
2447
|
+
this.hubHeartbeatTimer = setInterval(sendHeartbeat, ViewerServer.HUB_HEARTBEAT_INTERVAL_MS);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
private stopHubHeartbeat(): void {
|
|
2451
|
+
if (this.hubHeartbeatTimer) { clearInterval(this.hubHeartbeatTimer); this.hubHeartbeatTimer = undefined; }
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2294
2454
|
private getLocalIPs(): string[] {
|
|
2295
2455
|
const nets = os.networkInterfaces();
|
|
2296
2456
|
const ips: string[] = [];
|
|
@@ -2343,7 +2503,7 @@ export class ViewerServer {
|
|
|
2343
2503
|
}
|
|
2344
2504
|
|
|
2345
2505
|
private handleSaveConfig(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2346
|
-
this.readBody(req, (body) => {
|
|
2506
|
+
this.readBody(req, async (body) => {
|
|
2347
2507
|
try {
|
|
2348
2508
|
const newCfg = JSON.parse(body);
|
|
2349
2509
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -2366,6 +2526,8 @@ export class ViewerServer {
|
|
|
2366
2526
|
if (!entry.config) entry.config = {};
|
|
2367
2527
|
const config = entry.config as Record<string, unknown>;
|
|
2368
2528
|
|
|
2529
|
+
const oldSharingRole = (config.sharing as Record<string, unknown>)?.role as string | undefined;
|
|
2530
|
+
|
|
2369
2531
|
if (newCfg.embedding) config.embedding = newCfg.embedding;
|
|
2370
2532
|
if (newCfg.summarizer) config.summarizer = newCfg.summarizer;
|
|
2371
2533
|
if (newCfg.skillEvolution) config.skillEvolution = newCfg.skillEvolution;
|
|
@@ -2393,6 +2555,13 @@ export class ViewerServer {
|
|
|
2393
2555
|
} catch {}
|
|
2394
2556
|
}
|
|
2395
2557
|
}
|
|
2558
|
+
|
|
2559
|
+
// When switching away from client mode, notify Hub that we're leaving
|
|
2560
|
+
const newRole = merged.role as string | undefined;
|
|
2561
|
+
if (oldSharingRole === "client" && newRole !== "client") {
|
|
2562
|
+
this.notifyHubLeave();
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2396
2565
|
if (merged.role === "hub") {
|
|
2397
2566
|
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2398
2567
|
} else if (merged.role === "client") {
|
|
@@ -2404,6 +2573,7 @@ export class ViewerServer {
|
|
|
2404
2573
|
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
2405
2574
|
fs.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2406
2575
|
this.log.info("Plugin config updated via Viewer");
|
|
2576
|
+
this.stopHubHeartbeat();
|
|
2407
2577
|
this.jsonResponse(res, { ok: true });
|
|
2408
2578
|
} catch (e) {
|
|
2409
2579
|
this.log.warn(`handleSaveConfig error: ${e}`);
|
|
@@ -2413,6 +2583,24 @@ export class ViewerServer {
|
|
|
2413
2583
|
});
|
|
2414
2584
|
}
|
|
2415
2585
|
|
|
2586
|
+
private async notifyHubLeave(): Promise<void> {
|
|
2587
|
+
try {
|
|
2588
|
+
const hub = this.resolveHubConnection();
|
|
2589
|
+
if (hub) {
|
|
2590
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2591
|
+
this.log.info("Notified Hub of voluntary leave");
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
const persisted = this.store.getClientHubConnection();
|
|
2595
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2596
|
+
await hubRequestJson(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2597
|
+
this.log.info("Notified Hub of voluntary leave (persisted connection)");
|
|
2598
|
+
}
|
|
2599
|
+
} catch (e) {
|
|
2600
|
+
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2416
2604
|
private handleUpdateUsername(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2417
2605
|
this.readBody(req, async (body) => {
|
|
2418
2606
|
if (!this.ctx) return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
@@ -2866,7 +3054,7 @@ export class ViewerServer {
|
|
|
2866
3054
|
|
|
2867
3055
|
private getOpenClawHome(): string {
|
|
2868
3056
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2869
|
-
return path.join(home, ".openclaw");
|
|
3057
|
+
return process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
|
|
2870
3058
|
}
|
|
2871
3059
|
|
|
2872
3060
|
private handleCleanupPolluted(res: http.ServerResponse): void {
|