@memtensor/memos-local-openclaw-plugin 1.0.4-beta.7 → 1.0.4-beta.9
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 +61 -7
- 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 +171 -8
- 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/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +78 -1
- package/dist/recall/engine.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 +5 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +13 -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 +735 -285
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +16 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +349 -21
- 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 +62 -7
- package/src/config.ts +0 -2
- package/src/hub/server.ts +168 -8
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +73 -1
- package/src/shared/llm-call.ts +97 -9
- package/src/skill/evolver.ts +5 -0
- package/src/storage/sqlite.ts +19 -6
- package/src/telemetry.ts +152 -39
- package/src/types.ts +1 -2
- package/src/viewer/html.ts +735 -285
- package/src/viewer/server.ts +322 -21
package/dist/viewer/server.js
CHANGED
|
@@ -91,6 +91,11 @@ class ViewerServer {
|
|
|
91
91
|
ppAbort = false;
|
|
92
92
|
ppState = { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
|
|
93
93
|
ppSSEClients = [];
|
|
94
|
+
notifSSEClients = [];
|
|
95
|
+
notifPollTimer;
|
|
96
|
+
lastKnownNotifCount = 0;
|
|
97
|
+
hubHeartbeatTimer;
|
|
98
|
+
static HUB_HEARTBEAT_INTERVAL_MS = 45_000;
|
|
94
99
|
constructor(opts) {
|
|
95
100
|
this.store = opts.store;
|
|
96
101
|
this.embedder = opts.embedder;
|
|
@@ -109,16 +114,17 @@ class ViewerServer {
|
|
|
109
114
|
this.server.on("error", (err) => {
|
|
110
115
|
if (err.code === "EADDRINUSE") {
|
|
111
116
|
this.log.warn(`Viewer port ${this.port} in use, trying ${this.port + 1}`);
|
|
112
|
-
this.server.listen(this.port + 1, "
|
|
117
|
+
this.server.listen(this.port + 1, "0.0.0.0");
|
|
113
118
|
}
|
|
114
119
|
else {
|
|
115
120
|
reject(err);
|
|
116
121
|
}
|
|
117
122
|
});
|
|
118
|
-
this.server.listen(this.port, "
|
|
123
|
+
this.server.listen(this.port, "0.0.0.0", () => {
|
|
119
124
|
const addr = this.server.address();
|
|
120
125
|
const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
|
|
121
126
|
this.autoCleanupPolluted();
|
|
127
|
+
this.startHubHeartbeat();
|
|
122
128
|
resolve(`http://127.0.0.1:${actualPort}`);
|
|
123
129
|
});
|
|
124
130
|
});
|
|
@@ -141,6 +147,15 @@ class ViewerServer {
|
|
|
141
147
|
}
|
|
142
148
|
}
|
|
143
149
|
stop() {
|
|
150
|
+
this.stopHubHeartbeat();
|
|
151
|
+
this.stopNotifPoll();
|
|
152
|
+
for (const c of this.notifSSEClients) {
|
|
153
|
+
try {
|
|
154
|
+
c.end();
|
|
155
|
+
}
|
|
156
|
+
catch { }
|
|
157
|
+
}
|
|
158
|
+
this.notifSSEClients = [];
|
|
144
159
|
this.server?.close();
|
|
145
160
|
this.server = null;
|
|
146
161
|
}
|
|
@@ -338,12 +353,18 @@ class ViewerServer {
|
|
|
338
353
|
this.handleSharingNotificationsRead(req, res);
|
|
339
354
|
else if (p === "/api/sharing/notifications/clear" && req.method === "POST")
|
|
340
355
|
this.handleSharingNotificationsClear(req, res);
|
|
356
|
+
else if (p === "/api/notifications/stream" && req.method === "GET")
|
|
357
|
+
this.handleNotifSSE(req, res);
|
|
341
358
|
else if (p === "/api/admin/shared-tasks" && req.method === "GET")
|
|
342
359
|
this.serveAdminSharedTasks(res);
|
|
360
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+\/detail$/) && req.method === "GET")
|
|
361
|
+
this.serveHubTaskDetail(res, p);
|
|
343
362
|
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE")
|
|
344
363
|
this.handleAdminDeleteTask(res, p);
|
|
345
364
|
else if (p === "/api/admin/shared-skills" && req.method === "GET")
|
|
346
365
|
this.serveAdminSharedSkills(res);
|
|
366
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+\/detail$/) && req.method === "GET")
|
|
367
|
+
this.serveHubSkillDetail(res, p);
|
|
347
368
|
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE")
|
|
348
369
|
this.handleAdminDeleteSkill(res, p);
|
|
349
370
|
else if (p === "/api/admin/shared-memories" && req.method === "GET")
|
|
@@ -521,7 +542,12 @@ class ViewerServer {
|
|
|
521
542
|
conditions.push("role = ?");
|
|
522
543
|
params.push(role);
|
|
523
544
|
}
|
|
524
|
-
if (owner) {
|
|
545
|
+
if (owner && owner.startsWith("agent:")) {
|
|
546
|
+
const agentPrefix = owner + ":";
|
|
547
|
+
conditions.push("(owner = ? OR (owner = 'public' AND session_key LIKE ?))");
|
|
548
|
+
params.push(owner, agentPrefix + "%");
|
|
549
|
+
}
|
|
550
|
+
else if (owner) {
|
|
525
551
|
conditions.push("owner = ?");
|
|
526
552
|
params.push(owner);
|
|
527
553
|
}
|
|
@@ -601,9 +627,10 @@ class ViewerServer {
|
|
|
601
627
|
serveTasks(res, url) {
|
|
602
628
|
this.store.recordViewerEvent("tasks_list");
|
|
603
629
|
const status = url.searchParams.get("status") ?? undefined;
|
|
630
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
604
631
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
605
632
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
606
|
-
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
633
|
+
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
|
|
607
634
|
const db = this.store.db;
|
|
608
635
|
const items = tasks.map((t) => {
|
|
609
636
|
const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id);
|
|
@@ -698,12 +725,22 @@ class ViewerServer {
|
|
|
698
725
|
embCount = db.prepare("SELECT COUNT(*) as count FROM embeddings").get().count;
|
|
699
726
|
}
|
|
700
727
|
catch { /* table may not exist */ }
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
728
|
+
let sessionQuery;
|
|
729
|
+
let sessionParams;
|
|
730
|
+
if (ownerFilter && ownerFilter.startsWith("agent:")) {
|
|
731
|
+
const agentPrefix = ownerFilter + ":";
|
|
732
|
+
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";
|
|
733
|
+
sessionParams = [ownerFilter, agentPrefix + "%"];
|
|
734
|
+
}
|
|
735
|
+
else if (ownerFilter) {
|
|
736
|
+
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";
|
|
737
|
+
sessionParams = [ownerFilter];
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
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";
|
|
741
|
+
sessionParams = [];
|
|
742
|
+
}
|
|
743
|
+
const sessionList = db.prepare(sessionQuery).all(...sessionParams);
|
|
707
744
|
let skillCount = 0;
|
|
708
745
|
try {
|
|
709
746
|
skillCount = db.prepare("SELECT COUNT(*) as count FROM skills").get().count;
|
|
@@ -717,10 +754,17 @@ class ViewerServer {
|
|
|
717
754
|
catch { /* column may not exist yet */ }
|
|
718
755
|
let owners = [];
|
|
719
756
|
try {
|
|
720
|
-
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL ORDER BY owner").all();
|
|
757
|
+
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY owner").all();
|
|
721
758
|
owners = ownerRows.map((o) => o.owner);
|
|
722
759
|
}
|
|
723
760
|
catch { /* column may not exist yet */ }
|
|
761
|
+
let currentAgentOwner = "agent:main";
|
|
762
|
+
try {
|
|
763
|
+
const latest = db.prepare("SELECT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY created_at DESC LIMIT 1").get();
|
|
764
|
+
if (latest?.owner)
|
|
765
|
+
currentAgentOwner = latest.owner;
|
|
766
|
+
}
|
|
767
|
+
catch { /* best-effort */ }
|
|
724
768
|
this.jsonResponse(res, {
|
|
725
769
|
totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
|
|
726
770
|
totalSkills: skillCount,
|
|
@@ -729,6 +773,7 @@ class ViewerServer {
|
|
|
729
773
|
timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
|
|
730
774
|
sessions: sessionList,
|
|
731
775
|
owners,
|
|
776
|
+
currentAgentOwner,
|
|
732
777
|
});
|
|
733
778
|
}
|
|
734
779
|
catch (e) {
|
|
@@ -1597,7 +1642,8 @@ class ViewerServer {
|
|
|
1597
1642
|
// ─── Config API ───
|
|
1598
1643
|
getOpenClawConfigPath() {
|
|
1599
1644
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1600
|
-
|
|
1645
|
+
const ocHome = process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
1646
|
+
return node_path_1.default.join(ocHome, "openclaw.json");
|
|
1601
1647
|
}
|
|
1602
1648
|
getPluginEntryConfig(raw) {
|
|
1603
1649
|
const entries = raw?.plugins?.entries ?? {};
|
|
@@ -1846,12 +1892,22 @@ class ViewerServer {
|
|
|
1846
1892
|
}
|
|
1847
1893
|
try {
|
|
1848
1894
|
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
1895
|
+
const localIPs = this.getLocalIPs();
|
|
1896
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
1897
|
+
try {
|
|
1898
|
+
const u = new URL(hubUrl);
|
|
1899
|
+
if (localIPs.includes(u.hostname)) {
|
|
1900
|
+
return this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
catch { }
|
|
1849
1904
|
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
1850
|
-
const
|
|
1905
|
+
const nickname = sharing.client?.nickname;
|
|
1906
|
+
const username = nickname || os.userInfo().username || "user";
|
|
1851
1907
|
const hostname = os.hostname() || "unknown";
|
|
1852
1908
|
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
1853
1909
|
method: "POST",
|
|
1854
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
1910
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true }),
|
|
1855
1911
|
});
|
|
1856
1912
|
this.store.setClientHubConnection({
|
|
1857
1913
|
hubUrl,
|
|
@@ -1873,7 +1929,14 @@ class ViewerServer {
|
|
|
1873
1929
|
return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1874
1930
|
try {
|
|
1875
1931
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1876
|
-
const
|
|
1932
|
+
const hub = this.resolveHubConnection();
|
|
1933
|
+
let data;
|
|
1934
|
+
if (hub) {
|
|
1935
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/memories?limit=${limit}`);
|
|
1936
|
+
}
|
|
1937
|
+
else {
|
|
1938
|
+
data = await (0, hub_1.hubListMemories)(this.store, this.ctx, { limit });
|
|
1939
|
+
}
|
|
1877
1940
|
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1878
1941
|
}
|
|
1879
1942
|
catch (err) {
|
|
@@ -1885,7 +1948,14 @@ class ViewerServer {
|
|
|
1885
1948
|
return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1886
1949
|
try {
|
|
1887
1950
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1888
|
-
const
|
|
1951
|
+
const hub = this.resolveHubConnection();
|
|
1952
|
+
let data;
|
|
1953
|
+
if (hub) {
|
|
1954
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/tasks?limit=${limit}`);
|
|
1955
|
+
}
|
|
1956
|
+
else {
|
|
1957
|
+
data = await (0, hub_1.hubListTasks)(this.store, this.ctx, { limit });
|
|
1958
|
+
}
|
|
1889
1959
|
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1890
1960
|
}
|
|
1891
1961
|
catch (err) {
|
|
@@ -1897,7 +1967,14 @@ class ViewerServer {
|
|
|
1897
1967
|
return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1898
1968
|
try {
|
|
1899
1969
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1900
|
-
const
|
|
1970
|
+
const hub = this.resolveHubConnection();
|
|
1971
|
+
let data;
|
|
1972
|
+
if (hub) {
|
|
1973
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/skills/list?limit=${limit}`);
|
|
1974
|
+
}
|
|
1975
|
+
else {
|
|
1976
|
+
data = await (0, hub_1.hubListSkills)(this.store, this.ctx, { limit });
|
|
1977
|
+
}
|
|
1901
1978
|
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1902
1979
|
}
|
|
1903
1980
|
catch (err) {
|
|
@@ -1914,13 +1991,22 @@ class ViewerServer {
|
|
|
1914
1991
|
const query = String(parsed.query || "");
|
|
1915
1992
|
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1916
1993
|
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1917
|
-
const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
|
|
1994
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
|
|
1918
1995
|
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1919
1996
|
if (scope === "local") {
|
|
1920
1997
|
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1921
1998
|
}
|
|
1922
1999
|
try {
|
|
1923
|
-
const
|
|
2000
|
+
const conn = this.resolveHubConnection();
|
|
2001
|
+
let hub;
|
|
2002
|
+
if (conn) {
|
|
2003
|
+
hub = await (0, hub_1.hubRequestJson)(conn.hubUrl, conn.userToken, "/api/v1/hub/search", {
|
|
2004
|
+
method: "POST", body: JSON.stringify({ query, maxResults, scope }),
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
else {
|
|
2008
|
+
hub = await (0, hub_1.hubSearchMemories)(this.store, this.ctx, { query, maxResults, scope });
|
|
2009
|
+
}
|
|
1924
2010
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1925
2011
|
}
|
|
1926
2012
|
catch (err) {
|
|
@@ -2356,6 +2442,38 @@ class ViewerServer {
|
|
|
2356
2442
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2357
2443
|
}
|
|
2358
2444
|
}
|
|
2445
|
+
async serveHubTaskDetail(res, p) {
|
|
2446
|
+
const hub = this.resolveHubConnection();
|
|
2447
|
+
if (!hub)
|
|
2448
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2449
|
+
const m = p.match(/^\/api\/admin\/shared-tasks\/([^/]+)\/detail$/);
|
|
2450
|
+
if (!m)
|
|
2451
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2452
|
+
const taskId = decodeURIComponent(m[1]);
|
|
2453
|
+
try {
|
|
2454
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-tasks/${encodeURIComponent(taskId)}/detail`, { method: "GET" });
|
|
2455
|
+
this.jsonResponse(res, data);
|
|
2456
|
+
}
|
|
2457
|
+
catch (err) {
|
|
2458
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
async serveHubSkillDetail(res, p) {
|
|
2462
|
+
const hub = this.resolveHubConnection();
|
|
2463
|
+
if (!hub)
|
|
2464
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2465
|
+
const m = p.match(/^\/api\/admin\/shared-skills\/([^/]+)\/detail$/);
|
|
2466
|
+
if (!m)
|
|
2467
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2468
|
+
const skillId = decodeURIComponent(m[1]);
|
|
2469
|
+
try {
|
|
2470
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-skills/${encodeURIComponent(skillId)}/detail`, { method: "GET" });
|
|
2471
|
+
this.jsonResponse(res, data);
|
|
2472
|
+
}
|
|
2473
|
+
catch (err) {
|
|
2474
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2359
2477
|
async serveAdminSharedSkills(res) {
|
|
2360
2478
|
const hub = this.resolveHubConnection();
|
|
2361
2479
|
if (!hub)
|
|
@@ -2449,6 +2567,13 @@ class ViewerServer {
|
|
|
2449
2567
|
const body = JSON.parse(raw || "{}");
|
|
2450
2568
|
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2451
2569
|
this.jsonResponse(res, { ok: true });
|
|
2570
|
+
try {
|
|
2571
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2572
|
+
const count = data?.unreadCount ?? 0;
|
|
2573
|
+
this.lastKnownNotifCount = count;
|
|
2574
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2575
|
+
}
|
|
2576
|
+
catch { /* best effort */ }
|
|
2452
2577
|
}
|
|
2453
2578
|
catch (err) {
|
|
2454
2579
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
@@ -2463,12 +2588,92 @@ class ViewerServer {
|
|
|
2463
2588
|
try {
|
|
2464
2589
|
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2465
2590
|
this.jsonResponse(res, { ok: true });
|
|
2591
|
+
this.broadcastNotifSSE({ type: "cleared", unreadCount: 0 });
|
|
2466
2592
|
}
|
|
2467
2593
|
catch (err) {
|
|
2468
2594
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2469
2595
|
}
|
|
2470
2596
|
});
|
|
2471
2597
|
}
|
|
2598
|
+
handleNotifSSE(req, res) {
|
|
2599
|
+
res.writeHead(200, {
|
|
2600
|
+
"Content-Type": "text/event-stream",
|
|
2601
|
+
"Cache-Control": "no-cache",
|
|
2602
|
+
Connection: "keep-alive",
|
|
2603
|
+
"Access-Control-Allow-Origin": "*",
|
|
2604
|
+
});
|
|
2605
|
+
res.write("data: {\"type\":\"connected\"}\n\n");
|
|
2606
|
+
this.notifSSEClients.push(res);
|
|
2607
|
+
if (!this.notifPollTimer)
|
|
2608
|
+
this.startNotifPoll();
|
|
2609
|
+
req.on("close", () => {
|
|
2610
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2611
|
+
if (this.notifSSEClients.length === 0)
|
|
2612
|
+
this.stopNotifPoll();
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
broadcastNotifSSE(data) {
|
|
2616
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
2617
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => {
|
|
2618
|
+
try {
|
|
2619
|
+
c.write(msg);
|
|
2620
|
+
return true;
|
|
2621
|
+
}
|
|
2622
|
+
catch {
|
|
2623
|
+
return false;
|
|
2624
|
+
}
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
startNotifPoll() {
|
|
2628
|
+
this.stopNotifPoll();
|
|
2629
|
+
const tick = async () => {
|
|
2630
|
+
const hub = this.resolveHubConnection();
|
|
2631
|
+
if (!hub)
|
|
2632
|
+
return;
|
|
2633
|
+
try {
|
|
2634
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2635
|
+
const count = data?.unreadCount ?? 0;
|
|
2636
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2637
|
+
this.lastKnownNotifCount = count;
|
|
2638
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
catch { /* ignore */ }
|
|
2642
|
+
};
|
|
2643
|
+
tick();
|
|
2644
|
+
this.notifPollTimer = setInterval(tick, 3000);
|
|
2645
|
+
}
|
|
2646
|
+
stopNotifPoll() {
|
|
2647
|
+
if (this.notifPollTimer) {
|
|
2648
|
+
clearInterval(this.notifPollTimer);
|
|
2649
|
+
this.notifPollTimer = undefined;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
startHubHeartbeat() {
|
|
2653
|
+
this.stopHubHeartbeat();
|
|
2654
|
+
const sendHeartbeat = async () => {
|
|
2655
|
+
try {
|
|
2656
|
+
const hub = this.resolveHubConnection();
|
|
2657
|
+
if (!hub) {
|
|
2658
|
+
const persisted = this.store.getClientHubConnection();
|
|
2659
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2660
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2661
|
+
}
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2665
|
+
}
|
|
2666
|
+
catch { /* best-effort */ }
|
|
2667
|
+
};
|
|
2668
|
+
sendHeartbeat();
|
|
2669
|
+
this.hubHeartbeatTimer = setInterval(sendHeartbeat, ViewerServer.HUB_HEARTBEAT_INTERVAL_MS);
|
|
2670
|
+
}
|
|
2671
|
+
stopHubHeartbeat() {
|
|
2672
|
+
if (this.hubHeartbeatTimer) {
|
|
2673
|
+
clearInterval(this.hubHeartbeatTimer);
|
|
2674
|
+
this.hubHeartbeatTimer = undefined;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2472
2677
|
getLocalIPs() {
|
|
2473
2678
|
const nets = node_os_1.default.networkInterfaces();
|
|
2474
2679
|
const ips = [];
|
|
@@ -2520,7 +2725,7 @@ class ViewerServer {
|
|
|
2520
2725
|
}
|
|
2521
2726
|
}
|
|
2522
2727
|
handleSaveConfig(req, res) {
|
|
2523
|
-
this.readBody(req, (body) => {
|
|
2728
|
+
this.readBody(req, async (body) => {
|
|
2524
2729
|
try {
|
|
2525
2730
|
const newCfg = JSON.parse(body);
|
|
2526
2731
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -2545,6 +2750,10 @@ class ViewerServer {
|
|
|
2545
2750
|
if (!entry.config)
|
|
2546
2751
|
entry.config = {};
|
|
2547
2752
|
const config = entry.config;
|
|
2753
|
+
const oldSharing = config.sharing;
|
|
2754
|
+
const oldSharingRole = oldSharing?.role;
|
|
2755
|
+
const oldSharingEnabled = Boolean(oldSharing?.enabled);
|
|
2756
|
+
const oldClientHubAddress = String(oldSharing?.client?.hubAddress || "");
|
|
2548
2757
|
if (newCfg.embedding)
|
|
2549
2758
|
config.embedding = newCfg.embedding;
|
|
2550
2759
|
if (newCfg.summarizer)
|
|
@@ -2578,6 +2787,33 @@ class ViewerServer {
|
|
|
2578
2787
|
catch { }
|
|
2579
2788
|
}
|
|
2580
2789
|
}
|
|
2790
|
+
const newRole = merged.role;
|
|
2791
|
+
const newEnabled = Boolean(merged.enabled);
|
|
2792
|
+
// Detect disabling sharing or switching away from hub mode
|
|
2793
|
+
const wasHub = oldSharingEnabled && oldSharingRole === "hub";
|
|
2794
|
+
const isHub = newEnabled && newRole === "hub";
|
|
2795
|
+
if (wasHub && !isHub) {
|
|
2796
|
+
await this.notifyHubShutdown();
|
|
2797
|
+
this.stopHubHeartbeat();
|
|
2798
|
+
this.log.info("Hub shutting down: notified connected clients");
|
|
2799
|
+
}
|
|
2800
|
+
// Detect disabling sharing or switching away from client mode
|
|
2801
|
+
const wasClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2802
|
+
const isClient = newEnabled && newRole === "client";
|
|
2803
|
+
if (wasClient && !isClient) {
|
|
2804
|
+
this.notifyHubLeave();
|
|
2805
|
+
this.store.clearClientHubConnection();
|
|
2806
|
+
this.log.info("Cleared client hub connection (sharing disabled or role changed)");
|
|
2807
|
+
}
|
|
2808
|
+
// Detect switching to a different Hub while still in client mode
|
|
2809
|
+
if (wasClient && isClient) {
|
|
2810
|
+
const newClientAddr = String(merged.client?.hubAddress || "");
|
|
2811
|
+
if (newClientAddr && oldClientHubAddress && (0, hub_1.normalizeHubUrl)(newClientAddr) !== (0, hub_1.normalizeHubUrl)(oldClientHubAddress)) {
|
|
2812
|
+
this.notifyHubLeave();
|
|
2813
|
+
this.store.clearClientHubConnection();
|
|
2814
|
+
this.log.info("Cleared client hub connection (switched to different Hub)");
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2581
2817
|
if (merged.role === "hub") {
|
|
2582
2818
|
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2583
2819
|
}
|
|
@@ -2589,6 +2825,12 @@ class ViewerServer {
|
|
|
2589
2825
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
|
|
2590
2826
|
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2591
2827
|
this.log.info("Plugin config updated via Viewer");
|
|
2828
|
+
this.stopHubHeartbeat();
|
|
2829
|
+
// When switching to client mode, immediately send join request
|
|
2830
|
+
const finalSharing = config.sharing;
|
|
2831
|
+
if (finalSharing?.role === "client" && oldSharingRole !== "client") {
|
|
2832
|
+
this.autoJoinOnSave(finalSharing).catch(e => this.log.warn(`Auto-join on save failed: ${e}`));
|
|
2833
|
+
}
|
|
2592
2834
|
this.jsonResponse(res, { ok: true });
|
|
2593
2835
|
}
|
|
2594
2836
|
catch (e) {
|
|
@@ -2598,6 +2840,92 @@ class ViewerServer {
|
|
|
2598
2840
|
}
|
|
2599
2841
|
});
|
|
2600
2842
|
}
|
|
2843
|
+
async autoJoinOnSave(sharing) {
|
|
2844
|
+
const clientCfg = sharing.client;
|
|
2845
|
+
const hubAddress = String(clientCfg?.hubAddress || "");
|
|
2846
|
+
const teamToken = String(clientCfg?.teamToken || "");
|
|
2847
|
+
if (!hubAddress || !teamToken)
|
|
2848
|
+
return;
|
|
2849
|
+
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
2850
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
2851
|
+
const nickname = String(clientCfg?.nickname || "");
|
|
2852
|
+
const username = nickname || os.userInfo().username || "user";
|
|
2853
|
+
const hostname = os.hostname() || "unknown";
|
|
2854
|
+
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
2855
|
+
method: "POST",
|
|
2856
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
2857
|
+
});
|
|
2858
|
+
this.store.setClientHubConnection({
|
|
2859
|
+
hubUrl,
|
|
2860
|
+
userId: String(result.userId || ""),
|
|
2861
|
+
username,
|
|
2862
|
+
userToken: result.userToken || "",
|
|
2863
|
+
role: "member",
|
|
2864
|
+
connectedAt: Date.now(),
|
|
2865
|
+
});
|
|
2866
|
+
this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
|
|
2867
|
+
if (result.userToken) {
|
|
2868
|
+
this.startHubHeartbeat();
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
async notifyHubLeave() {
|
|
2872
|
+
try {
|
|
2873
|
+
const hub = this.resolveHubConnection();
|
|
2874
|
+
if (hub) {
|
|
2875
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2876
|
+
this.log.info("Notified Hub of voluntary leave");
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
const persisted = this.store.getClientHubConnection();
|
|
2880
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2881
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2882
|
+
this.log.info("Notified Hub of voluntary leave (persisted connection)");
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
catch (e) {
|
|
2886
|
+
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
async notifyHubShutdown() {
|
|
2890
|
+
try {
|
|
2891
|
+
const sharing = this.ctx?.config.sharing;
|
|
2892
|
+
if (!sharing || sharing.role !== "hub")
|
|
2893
|
+
return;
|
|
2894
|
+
const hubPort = sharing.hub?.port ?? 18800;
|
|
2895
|
+
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
2896
|
+
let adminToken;
|
|
2897
|
+
try {
|
|
2898
|
+
const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
2899
|
+
adminToken = authData?.bootstrapAdminToken;
|
|
2900
|
+
}
|
|
2901
|
+
catch {
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
if (!adminToken)
|
|
2905
|
+
return;
|
|
2906
|
+
const users = this.store.listHubUsers("active");
|
|
2907
|
+
const { v4: uuidv4 } = require("uuid");
|
|
2908
|
+
for (const u of users) {
|
|
2909
|
+
try {
|
|
2910
|
+
this.store.insertHubNotification({
|
|
2911
|
+
id: uuidv4(),
|
|
2912
|
+
userId: u.id,
|
|
2913
|
+
type: "hub_shutdown",
|
|
2914
|
+
resource: "hub",
|
|
2915
|
+
title: "Hub is shutting down",
|
|
2916
|
+
message: "The Hub server is shutting down. You may be disconnected.",
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
catch (e) {
|
|
2920
|
+
this.log.warn(`Failed to insert shutdown notification for user ${u.id}: ${e}`);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
this.log.info(`Hub shutdown: notified ${users.length} approved user(s)`);
|
|
2924
|
+
}
|
|
2925
|
+
catch (e) {
|
|
2926
|
+
this.log.warn(`notifyHubShutdown error: ${e}`);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2601
2929
|
handleUpdateUsername(req, res) {
|
|
2602
2930
|
this.readBody(req, async (body) => {
|
|
2603
2931
|
if (!this.ctx)
|
|
@@ -3072,7 +3400,7 @@ class ViewerServer {
|
|
|
3072
3400
|
// ─── Migration: scan OpenClaw built-in memory ───
|
|
3073
3401
|
getOpenClawHome() {
|
|
3074
3402
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3075
|
-
return node_path_1.default.join(home, ".openclaw");
|
|
3403
|
+
return process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
3076
3404
|
}
|
|
3077
3405
|
handleCleanupPolluted(res) {
|
|
3078
3406
|
try {
|