@silicaclaw/cli 2026.3.20-2 → 2026.3.20-21
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/CHANGELOG.md +108 -0
- package/INSTALL.md +13 -7
- package/README.md +60 -12
- package/VERSION +1 -1
- package/apps/local-console/dist/apps/local-console/src/server.d.ts +139 -3
- package/apps/local-console/dist/apps/local-console/src/server.js +1029 -92
- package/apps/local-console/dist/packages/core/src/index.d.ts +2 -0
- package/apps/local-console/dist/packages/core/src/index.js +2 -0
- package/apps/local-console/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/apps/local-console/dist/packages/core/src/privateCrypto.js +40 -0
- package/apps/local-console/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/apps/local-console/dist/packages/core/src/privateMessage.js +74 -0
- package/apps/local-console/dist/packages/core/src/profile.js +2 -0
- package/apps/local-console/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/apps/local-console/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/apps/local-console/dist/packages/core/src/types.d.ts +40 -0
- package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +12 -0
- package/apps/local-console/dist/packages/network/src/relayPreview.js +108 -8
- package/apps/local-console/dist/packages/network/src/types.d.ts +4 -0
- package/apps/local-console/dist/packages/storage/src/repos.d.ts +27 -1
- package/apps/local-console/dist/packages/storage/src/repos.js +35 -1
- package/apps/local-console/public/app/app.js +502 -11
- package/apps/local-console/public/app/events.js +21 -0
- package/apps/local-console/public/app/network.js +144 -32
- package/apps/local-console/public/app/overview.js +57 -27
- package/apps/local-console/public/app/social.js +342 -105
- package/apps/local-console/public/app/styles.css +149 -43
- package/apps/local-console/public/app/template.js +196 -100
- package/apps/local-console/public/app/translations.js +438 -316
- package/apps/local-console/src/server.ts +1177 -90
- package/apps/public-explorer/public/app/template.js +2 -2
- package/apps/public-explorer/public/app/translations.js +36 -36
- package/docs/NEW_USER_OPERATIONS.md +5 -5
- package/docs/OPENCLAW_BRIDGE.md +7 -7
- package/docs/OPENCLAW_BRIDGE_ZH.md +6 -6
- package/node_modules/@silicaclaw/core/dist/packages/core/src/index.d.ts +2 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/index.js +2 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.js +40 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.js +74 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/profile.js +2 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/types.d.ts +40 -0
- package/node_modules/@silicaclaw/core/package.json +2 -2
- package/node_modules/@silicaclaw/core/src/index.ts +2 -0
- package/node_modules/@silicaclaw/core/src/privateCrypto.ts +57 -0
- package/node_modules/@silicaclaw/core/src/privateMessage.ts +101 -0
- package/node_modules/@silicaclaw/core/src/profile.ts +2 -0
- package/node_modules/@silicaclaw/core/src/publicProfileSummary.ts +7 -0
- package/node_modules/@silicaclaw/core/src/types.ts +44 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +12 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +108 -8
- package/node_modules/@silicaclaw/network/dist/packages/network/src/types.d.ts +4 -0
- package/node_modules/@silicaclaw/network/src/relayPreview.ts +120 -10
- package/node_modules/@silicaclaw/network/src/types.ts +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.d.ts +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.js +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.js +40 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.js +74 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/profile.js +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/types.d.ts +40 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.d.ts +27 -1
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.js +35 -1
- package/node_modules/@silicaclaw/storage/package.json +2 -2
- package/node_modules/@silicaclaw/storage/src/repos.ts +59 -1
- package/openclaw-skills/silicaclaw-bridge-setup/SKILL.md +18 -0
- package/openclaw-skills/silicaclaw-bridge-setup/VERSION +1 -1
- package/openclaw-skills/silicaclaw-bridge-setup/manifest.json +2 -2
- package/openclaw-skills/silicaclaw-broadcast/SKILL.md +18 -0
- package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
- package/openclaw-skills/silicaclaw-broadcast/manifest.json +2 -2
- package/openclaw-skills/silicaclaw-network-config/SKILL.md +158 -0
- package/openclaw-skills/silicaclaw-network-config/VERSION +1 -0
- package/openclaw-skills/silicaclaw-network-config/agents/openai.yaml +6 -0
- package/openclaw-skills/silicaclaw-network-config/manifest.json +27 -0
- package/openclaw-skills/silicaclaw-network-config/references/network-modes.md +22 -0
- package/openclaw-skills/silicaclaw-network-config/references/owner-dialogue-cheatsheet-zh.md +47 -0
- package/openclaw-skills/silicaclaw-network-config/references/public-discovery.md +22 -0
- package/openclaw-skills/silicaclaw-owner-push/SKILL.md +18 -0
- package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
- package/openclaw-skills/silicaclaw-owner-push/manifest.json +2 -2
- package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
- package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +151 -9
- package/package.json +1 -1
- package/packages/core/dist/packages/core/src/index.d.ts +2 -0
- package/packages/core/dist/packages/core/src/index.js +2 -0
- package/packages/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/packages/core/dist/packages/core/src/privateCrypto.js +40 -0
- package/packages/core/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/packages/core/dist/packages/core/src/privateMessage.js +74 -0
- package/packages/core/dist/packages/core/src/profile.js +2 -0
- package/packages/core/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/packages/core/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/packages/core/dist/packages/core/src/types.d.ts +40 -0
- package/packages/core/package.json +2 -2
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/privateCrypto.ts +57 -0
- package/packages/core/src/privateMessage.ts +101 -0
- package/packages/core/src/profile.ts +2 -0
- package/packages/core/src/publicProfileSummary.ts +7 -0
- package/packages/core/src/types.ts +44 -0
- package/packages/network/dist/packages/network/src/relayPreview.d.ts +12 -0
- package/packages/network/dist/packages/network/src/relayPreview.js +108 -8
- package/packages/network/dist/packages/network/src/types.d.ts +4 -0
- package/packages/network/src/relayPreview.ts +120 -10
- package/packages/network/src/types.ts +2 -0
- package/packages/storage/dist/packages/core/src/index.d.ts +2 -0
- package/packages/storage/dist/packages/core/src/index.js +2 -0
- package/packages/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/packages/storage/dist/packages/core/src/privateCrypto.js +40 -0
- package/packages/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/packages/storage/dist/packages/core/src/privateMessage.js +74 -0
- package/packages/storage/dist/packages/core/src/profile.js +2 -0
- package/packages/storage/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/packages/storage/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/packages/storage/dist/packages/core/src/types.d.ts +40 -0
- package/packages/storage/dist/packages/storage/src/repos.d.ts +27 -1
- package/packages/storage/dist/packages/storage/src/repos.js +35 -1
- package/packages/storage/package.json +2 -2
- package/packages/storage/src/repos.ts +59 -1
- package/scripts/silicaclaw-cli.mjs +4 -1
- package/scripts/silicaclaw-gateway.mjs +114 -2
- package/scripts/validate-openclaw-skill.mjs +19 -0
- package/node_modules/@silicaclaw/storage/dist/index.d.ts +0 -3
- package/node_modules/@silicaclaw/storage/dist/index.js +0 -19
- package/node_modules/@silicaclaw/storage/dist/jsonRepo.d.ts +0 -7
- package/node_modules/@silicaclaw/storage/dist/jsonRepo.js +0 -29
- package/node_modules/@silicaclaw/storage/dist/repos.d.ts +0 -61
- package/node_modules/@silicaclaw/storage/dist/repos.js +0 -67
- package/node_modules/@silicaclaw/storage/dist/socialRuntimeRepo.d.ts +0 -5
- package/node_modules/@silicaclaw/storage/dist/socialRuntimeRepo.js +0 -57
- package/packages/storage/dist/index.d.ts +0 -3
- package/packages/storage/dist/index.js +0 -19
- package/packages/storage/dist/jsonRepo.d.ts +0 -7
- package/packages/storage/dist/jsonRepo.js +0 -29
- package/packages/storage/dist/repos.d.ts +0 -61
- package/packages/storage/dist/repos.js +0 -67
- package/packages/storage/dist/socialRuntimeRepo.d.ts +0 -5
- package/packages/storage/dist/socialRuntimeRepo.js +0 -57
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express, { NextFunction, Request, Response } from "express";
|
|
2
2
|
import cors from "cors";
|
|
3
|
-
import { execFile, spawnSync } from "child_process";
|
|
3
|
+
import { execFile, spawn, spawnSync } from "child_process";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs";
|
|
6
6
|
import { createHash } from "crypto";
|
|
@@ -11,19 +11,24 @@ import {
|
|
|
11
11
|
AgentIdentity,
|
|
12
12
|
DirectoryState,
|
|
13
13
|
IndexRefRecord,
|
|
14
|
+
PrivateEncryptionKeyPair,
|
|
15
|
+
PrivateMessageReceiptRecord,
|
|
16
|
+
PrivateMessageRecord,
|
|
14
17
|
PresenceRecord,
|
|
15
18
|
ProfileInput,
|
|
16
19
|
PublicProfile,
|
|
17
20
|
PublicProfileSummary,
|
|
18
21
|
SignedProfileRecord,
|
|
19
22
|
buildPublicProfileSummary,
|
|
20
|
-
buildIndexRecords,
|
|
21
23
|
cleanupExpiredPresence,
|
|
22
24
|
createDefaultProfileInput,
|
|
23
25
|
createEmptyDirectoryState,
|
|
24
26
|
createIdentity,
|
|
27
|
+
createPrivateEncryptionKeyPair,
|
|
25
28
|
dedupeIndex,
|
|
29
|
+
decryptPrivatePayload,
|
|
26
30
|
ensureDefaultSocialMd,
|
|
31
|
+
encryptPrivatePayload,
|
|
27
32
|
ingestIndexRecord,
|
|
28
33
|
ingestPresenceRecord,
|
|
29
34
|
ingestProfileRecord,
|
|
@@ -34,6 +39,8 @@ import {
|
|
|
34
39
|
resolveIdentityWithSocial,
|
|
35
40
|
resolveProfileInputWithSocial,
|
|
36
41
|
searchDirectory,
|
|
42
|
+
signPrivateMessage,
|
|
43
|
+
signPrivateMessageReceipt,
|
|
37
44
|
signSocialMessage,
|
|
38
45
|
signSocialMessageObservation,
|
|
39
46
|
signPresence,
|
|
@@ -46,6 +53,8 @@ import {
|
|
|
46
53
|
verifySocialMessage,
|
|
47
54
|
verifySocialMessageObservation,
|
|
48
55
|
verifyPresence,
|
|
56
|
+
verifyPrivateMessage,
|
|
57
|
+
verifyPrivateMessageReceipt,
|
|
49
58
|
verifyProfile,
|
|
50
59
|
} from "@silicaclaw/core";
|
|
51
60
|
import {
|
|
@@ -62,6 +71,11 @@ import {
|
|
|
62
71
|
CacheRepo,
|
|
63
72
|
IdentityRepo,
|
|
64
73
|
LogRepo,
|
|
74
|
+
PrivateEncryptionKeyRepo,
|
|
75
|
+
PrivateMessagingRuntimeRepo,
|
|
76
|
+
PrivateMessagingRuntimeState,
|
|
77
|
+
PrivateMessageReceiptRepo,
|
|
78
|
+
PrivateMessageRepo,
|
|
65
79
|
ProfileRepo,
|
|
66
80
|
SocialMessageGovernanceConfig,
|
|
67
81
|
SocialMessageGovernanceRepo,
|
|
@@ -89,7 +103,10 @@ const DEFAULT_GLOBAL_ROOM = defaults.network.global_preview.room;
|
|
|
89
103
|
const DEFAULT_BRIDGE_API_BASE = defaults.bridge.api_base;
|
|
90
104
|
const OPENCLAW_GATEWAY_PORT = defaults.ports.openclaw_gateway;
|
|
91
105
|
const OPENCLAW_GATEWAY_URL = `http://${OPENCLAW_GATEWAY_HOST}:${OPENCLAW_GATEWAY_PORT}/`;
|
|
106
|
+
const OPENCLAW_RUNTIME_CACHE_MS = 15_000;
|
|
107
|
+
const OPENCLAW_BRIDGE_STATUS_CACHE_MS = 5_000;
|
|
92
108
|
const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
|
|
109
|
+
const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
|
|
93
110
|
const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
|
|
94
111
|
const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
|
|
95
112
|
const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
|
|
@@ -102,6 +119,12 @@ const WEBRTC_BOOTSTRAP_HINTS = process.env.WEBRTC_BOOTSTRAP_HINTS || "";
|
|
|
102
119
|
const PROFILE_VERSION = "v0.9";
|
|
103
120
|
const SOCIAL_MESSAGE_TOPIC = "social.message";
|
|
104
121
|
const SOCIAL_MESSAGE_OBSERVATION_TOPIC = "social.message.observation";
|
|
122
|
+
const PRIVATE_MESSAGE_TOPIC = "private.message";
|
|
123
|
+
const PRIVATE_MESSAGE_RECEIPT_TOPIC = "private.message.receipt";
|
|
124
|
+
const PRIVATE_MESSAGE_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_HISTORY_LIMIT || 1000);
|
|
125
|
+
const PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT || 2000);
|
|
126
|
+
const PRIVATE_MESSAGE_QUERY_LIMIT = Number(process.env.PRIVATE_MESSAGE_QUERY_LIMIT || 100);
|
|
127
|
+
const PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS = Number(process.env.PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS || 750);
|
|
105
128
|
const DEFAULT_SOCIAL_MESSAGE_CHANNEL = "global";
|
|
106
129
|
const SOCIAL_MESSAGE_MAX_BODY_CHARS = Number(process.env.SOCIAL_MESSAGE_MAX_BODY_CHARS || 500);
|
|
107
130
|
const SOCIAL_MESSAGE_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_HISTORY_LIMIT || 100);
|
|
@@ -115,6 +138,16 @@ const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS |
|
|
|
115
138
|
const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
|
|
116
139
|
const SOCIAL_MESSAGE_REPLAY_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_WINDOW_MS || 10 * 60_000);
|
|
117
140
|
const SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST = Number(process.env.SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST || 3);
|
|
141
|
+
const SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS = Number(
|
|
142
|
+
process.env.SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS || 120_000
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
type StoredPrivateMessageRecord = PrivateMessageRecord & {
|
|
146
|
+
local_plaintext?: string;
|
|
147
|
+
};
|
|
148
|
+
const PROFILE_RELAY_REFRESH_INTERVAL_MS = Number(
|
|
149
|
+
process.env.PROFILE_RELAY_REFRESH_INTERVAL_MS || 120_000
|
|
150
|
+
);
|
|
118
151
|
const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(
|
|
119
152
|
dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || ""))
|
|
120
153
|
);
|
|
@@ -157,6 +190,10 @@ function normalizeVersionText(value: unknown): string {
|
|
|
157
190
|
return text.startsWith("v") ? text.slice(1) : text;
|
|
158
191
|
}
|
|
159
192
|
|
|
193
|
+
function formatBytesToMiB(value: number): number {
|
|
194
|
+
return Math.round((value / (1024 * 1024)) * 10) / 10;
|
|
195
|
+
}
|
|
196
|
+
|
|
160
197
|
function tokenizeVersion(value: unknown): Array<number | string> {
|
|
161
198
|
return normalizeVersionText(value)
|
|
162
199
|
.split(/[^0-9A-Za-z]+/)
|
|
@@ -186,7 +223,23 @@ function compareVersionTokens(left: unknown, right: unknown): number {
|
|
|
186
223
|
return 0;
|
|
187
224
|
}
|
|
188
225
|
|
|
226
|
+
function userNpmCacheDir(): string {
|
|
227
|
+
return resolve(homedir(), ".silicaclaw", "npm-cache");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function userShimPath(): string {
|
|
231
|
+
return resolve(homedir(), ".silicaclaw", "bin", "silicaclaw");
|
|
232
|
+
}
|
|
233
|
+
|
|
189
234
|
function resolveWorkspaceRoot(cwd = process.cwd()): string {
|
|
235
|
+
const envAppRoot = String(process.env.SILICACLAW_APP_DIR || "").trim();
|
|
236
|
+
if (
|
|
237
|
+
envAppRoot &&
|
|
238
|
+
existsSync(resolve(envAppRoot, "apps", "local-console", "package.json")) &&
|
|
239
|
+
existsSync(resolve(envAppRoot, "package.json"))
|
|
240
|
+
) {
|
|
241
|
+
return resolve(envAppRoot);
|
|
242
|
+
}
|
|
190
243
|
if (existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
|
|
191
244
|
return cwd;
|
|
192
245
|
}
|
|
@@ -441,45 +494,66 @@ function readOpenClawConfiguredGateway(workspaceRoot: string) {
|
|
|
441
494
|
} as const;
|
|
442
495
|
}
|
|
443
496
|
|
|
444
|
-
function
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
const stdout = String(result.stdout || "");
|
|
450
|
-
const lines = stdout
|
|
451
|
-
.split("\n")
|
|
452
|
-
.map((line) => line.trim())
|
|
453
|
-
.filter(Boolean);
|
|
497
|
+
function resolveOpenClawStatusCommand(workspaceRoot: string) {
|
|
498
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
499
|
+
if (explicitBin) {
|
|
500
|
+
return { cmd: explicitBin, args: ["status"] } as const;
|
|
501
|
+
}
|
|
454
502
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
lower.includes("openclaw agent") ||
|
|
468
|
-
lower.includes("openclaw message");
|
|
469
|
-
if (!isOpenClaw) return null;
|
|
470
|
-
return {
|
|
471
|
-
pid: Number(match[1]),
|
|
472
|
-
ppid: Number(match[2]),
|
|
473
|
-
command,
|
|
474
|
-
};
|
|
475
|
-
})
|
|
476
|
-
.filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
|
|
503
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
504
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
505
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
506
|
+
const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
|
|
507
|
+
if (sourceEntry) {
|
|
508
|
+
return { cmd: process.execPath, args: [sourceEntry, "status"] } as const;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
512
|
+
if (commandPath) {
|
|
513
|
+
return { cmd: commandPath, args: ["status"] } as const;
|
|
514
|
+
}
|
|
477
515
|
|
|
478
|
-
|
|
479
|
-
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function resolveOpenClawGatewayProbeCommand(workspaceRoot: string) {
|
|
520
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
521
|
+
if (explicitBin) {
|
|
522
|
+
return { cmd: explicitBin, args: ["gateway", "probe"] } as const;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
526
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
527
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
528
|
+
const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
|
|
529
|
+
if (sourceEntry) {
|
|
530
|
+
return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] } as const;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
534
|
+
if (commandPath) {
|
|
535
|
+
return { cmd: commandPath, args: ["gateway", "probe"] } as const;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function detectOpenClawRuntime(workspaceRoot: string) {
|
|
542
|
+
const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
|
|
543
|
+
const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
|
|
544
|
+
const statusLooksConfigured = Boolean(
|
|
545
|
+
statusCommand ||
|
|
546
|
+
configuredGateway.config_path ||
|
|
547
|
+
detectOpenClawInstallation(workspaceRoot).detected
|
|
548
|
+
);
|
|
549
|
+
const gatewayProbeCommand = ["lsof", "-nP", `-iTCP:${configuredGateway.gateway_port}`, "-sTCP:LISTEN"];
|
|
550
|
+
const gatewayProbe = spawnSync(gatewayProbeCommand[0], gatewayProbeCommand.slice(1), {
|
|
480
551
|
encoding: "utf8",
|
|
552
|
+
timeout: 1200,
|
|
481
553
|
});
|
|
482
|
-
const
|
|
554
|
+
const gatewayStatusStdout = String(gatewayProbe.stdout || "");
|
|
555
|
+
const gatewayStatusStderr = String(gatewayProbe.stderr || "");
|
|
556
|
+
const gatewayLines = gatewayStatusStdout
|
|
483
557
|
.split("\n")
|
|
484
558
|
.map((line) => line.trim())
|
|
485
559
|
.filter(Boolean);
|
|
@@ -489,14 +563,9 @@ function detectOpenClawRuntime(workspaceRoot: string) {
|
|
|
489
563
|
const parts = line.split(/\s+/);
|
|
490
564
|
const pid = Number(parts[1] || 0);
|
|
491
565
|
const command = parts[0] || "";
|
|
492
|
-
const lowerCommand = command.toLowerCase();
|
|
493
566
|
const endpoint = parts[8] || parts[parts.length - 1] || "";
|
|
494
567
|
const portMatch = endpoint.match(/:(\d+)(?:\s*\(|$)/);
|
|
495
568
|
if (!pid || !command || !portMatch) return null;
|
|
496
|
-
const isOpenClawListener =
|
|
497
|
-
openclawPids.has(pid) ||
|
|
498
|
-
lowerCommand.includes("openclaw");
|
|
499
|
-
if (!isOpenClawListener) return null;
|
|
500
569
|
const port = Number(portMatch[1]);
|
|
501
570
|
if (!Number.isFinite(port) || port <= 0) return null;
|
|
502
571
|
return {
|
|
@@ -507,46 +576,106 @@ function detectOpenClawRuntime(workspaceRoot: string) {
|
|
|
507
576
|
};
|
|
508
577
|
})
|
|
509
578
|
.filter((item): item is { pid: number; ppid: number; port: number; command: string } => Boolean(item));
|
|
579
|
+
const gatewayProbeOk = gatewayListeners.length > 0;
|
|
580
|
+
let processes: Array<{ pid: number; ppid: number; command: string }> = gatewayListeners.map((item) => ({
|
|
581
|
+
pid: item.pid,
|
|
582
|
+
ppid: item.ppid,
|
|
583
|
+
command: item.command,
|
|
584
|
+
}));
|
|
585
|
+
let processResult: ReturnType<typeof spawnSync> | null = null;
|
|
586
|
+
if (!gatewayProbeOk) {
|
|
587
|
+
processResult = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
|
|
588
|
+
encoding: "utf8",
|
|
589
|
+
timeout: 1200,
|
|
590
|
+
});
|
|
591
|
+
const stdout = String(processResult.stdout || "");
|
|
592
|
+
const lines = stdout
|
|
593
|
+
.split("\n")
|
|
594
|
+
.map((line) => line.trim())
|
|
595
|
+
.filter(Boolean);
|
|
596
|
+
processes = lines
|
|
597
|
+
.map((line) => {
|
|
598
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
599
|
+
if (!match) return null;
|
|
600
|
+
const command = match[3] || "";
|
|
601
|
+
const lower = command.toLowerCase();
|
|
602
|
+
const isOpenClaw =
|
|
603
|
+
lower.includes(" openclaw ") ||
|
|
604
|
+
lower.endsWith(" openclaw") ||
|
|
605
|
+
lower.includes("/openclaw ") ||
|
|
606
|
+
lower.includes("openclaw.mjs") ||
|
|
607
|
+
lower.includes("openclaw gateway") ||
|
|
608
|
+
lower.includes("openclaw agent") ||
|
|
609
|
+
lower.includes("openclaw message");
|
|
610
|
+
if (!isOpenClaw) return null;
|
|
611
|
+
return {
|
|
612
|
+
pid: Number(match[1]),
|
|
613
|
+
ppid: Number(match[2]),
|
|
614
|
+
command,
|
|
615
|
+
};
|
|
616
|
+
})
|
|
617
|
+
.filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
|
|
618
|
+
}
|
|
619
|
+
|
|
510
620
|
const preferredListener =
|
|
511
621
|
gatewayListeners.find((item) => item.port === configuredGateway.gateway_port) ||
|
|
512
622
|
gatewayListeners[0] ||
|
|
513
623
|
null;
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
for (const process of [...processes, ...gatewayListeners]) {
|
|
517
|
-
if (!combinedProcesses.has(process.pid)) {
|
|
518
|
-
combinedProcesses.set(process.pid, process);
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
const current = combinedProcesses.get(process.pid);
|
|
522
|
-
if (current && current.command.length < process.command.length) {
|
|
523
|
-
combinedProcesses.set(process.pid, process);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
const allProcesses = Array.from(combinedProcesses.values());
|
|
527
|
-
const gatewayReachable = gatewayListeners.length > 0;
|
|
624
|
+
const allProcesses = processes.slice(0, 10);
|
|
625
|
+
const gatewayReachable = gatewayProbeOk;
|
|
528
626
|
const detectionNotes = [];
|
|
529
|
-
if (result.status !== 0) detectionNotes.push(String(result.stderr || "ps failed").trim());
|
|
530
627
|
if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
|
|
531
|
-
detectionNotes.push(String(
|
|
628
|
+
detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
|
|
629
|
+
}
|
|
630
|
+
if (processResult && processResult.status !== 0) {
|
|
631
|
+
detectionNotes.push(String(processResult.stderr || "ps failed").trim());
|
|
532
632
|
}
|
|
533
633
|
const gatewayPort = preferredListener?.port || configuredGateway.gateway_port;
|
|
534
634
|
const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
|
|
535
635
|
|
|
536
636
|
return {
|
|
537
|
-
running: allProcesses.length > 0 || gatewayReachable,
|
|
637
|
+
running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
|
|
538
638
|
process_count: allProcesses.length,
|
|
539
639
|
processes: allProcesses.slice(0, 10),
|
|
540
640
|
detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
|
|
541
641
|
gateway_url: gatewayUrl,
|
|
542
642
|
gateway_port: gatewayPort,
|
|
543
643
|
gateway_reachable: gatewayReachable,
|
|
644
|
+
status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
|
|
645
|
+
status_ok: statusLooksConfigured,
|
|
646
|
+
status_summary: statusLooksConfigured
|
|
647
|
+
? configuredGateway.config_path
|
|
648
|
+
? `configured via ${configuredGateway.config_path}`
|
|
649
|
+
: statusCommand
|
|
650
|
+
? `command available: ${[statusCommand.cmd, ...statusCommand.args].join(" ")}`
|
|
651
|
+
: "OpenClaw environment detected"
|
|
652
|
+
: null,
|
|
653
|
+
gateway_probe_command: gatewayProbeCommand.join(" "),
|
|
654
|
+
gateway_probe_ok: gatewayProbeOk,
|
|
655
|
+
gateway_probe_summary: gatewayProbeOk
|
|
656
|
+
? gatewayStatusStdout
|
|
657
|
+
.split("\n")
|
|
658
|
+
.map((line) => line.trim())
|
|
659
|
+
.filter(Boolean)
|
|
660
|
+
.slice(0, 4)
|
|
661
|
+
.join(" | ")
|
|
662
|
+
: null,
|
|
544
663
|
configured_gateway_url: configuredGateway.gateway_url,
|
|
545
664
|
configured_gateway_port: configuredGateway.gateway_port,
|
|
546
665
|
configured_gateway_bind: configuredGateway.gateway_bind,
|
|
547
666
|
configured_gateway_config_path: configuredGateway.config_path,
|
|
548
667
|
detection_mode:
|
|
549
|
-
|
|
668
|
+
gatewayProbeOk
|
|
669
|
+
? (
|
|
670
|
+
processes.length > 0 && gatewayReachable
|
|
671
|
+
? "gateway-probe+process+gateway"
|
|
672
|
+
: gatewayReachable
|
|
673
|
+
? "gateway-probe+gateway"
|
|
674
|
+
: processes.length > 0
|
|
675
|
+
? "gateway-probe+process"
|
|
676
|
+
: "gateway-probe"
|
|
677
|
+
)
|
|
678
|
+
: processes.length > 0 && gatewayReachable
|
|
550
679
|
? "process+gateway"
|
|
551
680
|
: gatewayReachable
|
|
552
681
|
? "gateway"
|
|
@@ -766,6 +895,7 @@ type IntegrationStatusSummary = {
|
|
|
766
895
|
};
|
|
767
896
|
|
|
768
897
|
type SocialMessageView = SocialMessageRecord & {
|
|
898
|
+
avatar_url?: string;
|
|
769
899
|
is_self: boolean;
|
|
770
900
|
online: boolean;
|
|
771
901
|
last_seen_at: number | null;
|
|
@@ -775,6 +905,17 @@ type SocialMessageView = SocialMessageRecord & {
|
|
|
775
905
|
delivery_status: "local-only" | "remote-observed";
|
|
776
906
|
};
|
|
777
907
|
|
|
908
|
+
type PrivateMessageView = {
|
|
909
|
+
message_id: string;
|
|
910
|
+
conversation_id: string;
|
|
911
|
+
from_agent_id: string;
|
|
912
|
+
to_agent_id: string;
|
|
913
|
+
body: string;
|
|
914
|
+
created_at: number;
|
|
915
|
+
is_self: boolean;
|
|
916
|
+
delivery_status: "sent" | "direct-sent" | "fallback-sent" | "received" | "read";
|
|
917
|
+
};
|
|
918
|
+
|
|
778
919
|
type RuntimeMessageGovernance = SocialMessageGovernanceConfig;
|
|
779
920
|
|
|
780
921
|
type OpenClawBridgeStatus = {
|
|
@@ -818,11 +959,17 @@ type OpenClawBridgeStatus = {
|
|
|
818
959
|
gateway_url: string;
|
|
819
960
|
gateway_port: number;
|
|
820
961
|
gateway_reachable: boolean;
|
|
962
|
+
status_command: string | null;
|
|
963
|
+
status_ok: boolean;
|
|
964
|
+
status_summary: string | null;
|
|
965
|
+
gateway_probe_command: string | null;
|
|
966
|
+
gateway_probe_ok: boolean;
|
|
967
|
+
gateway_probe_summary: string | null;
|
|
821
968
|
configured_gateway_url: string;
|
|
822
969
|
configured_gateway_port: number;
|
|
823
970
|
configured_gateway_bind: string | null;
|
|
824
971
|
configured_gateway_config_path: string | null;
|
|
825
|
-
detection_mode: "process" | "gateway" | "process+gateway" | "not_running";
|
|
972
|
+
detection_mode: "gateway-probe" | "gateway-probe+process" | "gateway-probe+gateway" | "gateway-probe+process+gateway" | "process" | "gateway" | "process+gateway" | "not_running";
|
|
826
973
|
};
|
|
827
974
|
skill_learning: {
|
|
828
975
|
available: boolean;
|
|
@@ -899,6 +1046,10 @@ export class LocalNodeService {
|
|
|
899
1046
|
private socialMessageGovernanceRepo: SocialMessageGovernanceRepo;
|
|
900
1047
|
private socialMessageRepo: SocialMessageRepo;
|
|
901
1048
|
private socialMessageObservationRepo: SocialMessageObservationRepo;
|
|
1049
|
+
private privateMessageRepo: PrivateMessageRepo;
|
|
1050
|
+
private privateMessageReceiptRepo: PrivateMessageReceiptRepo;
|
|
1051
|
+
private privateEncryptionKeyRepo: PrivateEncryptionKeyRepo;
|
|
1052
|
+
private privateMessagingRuntimeRepo: PrivateMessagingRuntimeRepo;
|
|
902
1053
|
private socialRuntimeRepo: SocialRuntimeRepo;
|
|
903
1054
|
|
|
904
1055
|
private identity: AgentIdentity | null = null;
|
|
@@ -906,15 +1057,34 @@ export class LocalNodeService {
|
|
|
906
1057
|
private directory: DirectoryState = createEmptyDirectoryState();
|
|
907
1058
|
private socialMessages: SocialMessageRecord[] = [];
|
|
908
1059
|
private socialMessageObservations: SocialMessageObservationRecord[] = [];
|
|
1060
|
+
private privateMessages: PrivateMessageRecord[] = [];
|
|
1061
|
+
private privateMessageReceipts: PrivateMessageReceiptRecord[] = [];
|
|
1062
|
+
private privateEncryptionKeyPair: PrivateEncryptionKeyPair | null = null;
|
|
1063
|
+
private privateMessagingRuntime: PrivateMessagingRuntimeState | null = null;
|
|
1064
|
+
private privatePeerRoutes: Record<string, string> = {};
|
|
1065
|
+
private privatePeerEncryptionKeys: Record<string, string> = {};
|
|
1066
|
+
private privateMessageBodyCache = new Map<string, string>();
|
|
1067
|
+
private privateMessageDeliveryStatusCache = new Map<string, PrivateMessageView["delivery_status"]>();
|
|
909
1068
|
private messageGovernance: RuntimeMessageGovernance;
|
|
1069
|
+
private privateMessagesPersistDirty = false;
|
|
1070
|
+
private privateMessageReceiptsPersistDirty = false;
|
|
1071
|
+
private privateMessagesPersistTimer: NodeJS.Timeout | null = null;
|
|
1072
|
+
private privateMessageReceiptsPersistTimer: NodeJS.Timeout | null = null;
|
|
910
1073
|
|
|
911
1074
|
private receivedCount = 0;
|
|
912
1075
|
private broadcastCount = 0;
|
|
913
1076
|
private lastMessageAt = 0;
|
|
914
1077
|
private lastBroadcastAt = 0;
|
|
1078
|
+
private lastProfileBroadcastAt = 0;
|
|
1079
|
+
private lastProfileBroadcastSignature = "";
|
|
1080
|
+
private lastReplayBroadcastAt = 0;
|
|
1081
|
+
private lastReplayBroadcastSignature = "";
|
|
915
1082
|
private lastBroadcastErrorAt = 0;
|
|
916
1083
|
private lastBroadcastError: string | null = null;
|
|
917
1084
|
private broadcastFailureCount = 0;
|
|
1085
|
+
private consecutiveBroadcastFailures = 0;
|
|
1086
|
+
private lastBroadcastRecoveryAttemptAt = 0;
|
|
1087
|
+
private broadcastRecoveryInFlight = false;
|
|
918
1088
|
private broadcaster: NodeJS.Timeout | null = null;
|
|
919
1089
|
private subscriptionsBound = false;
|
|
920
1090
|
private broadcastEnabled = true;
|
|
@@ -956,6 +1126,8 @@ export class LocalNodeService {
|
|
|
956
1126
|
private networkReconnectTimer: NodeJS.Timeout | null = null;
|
|
957
1127
|
private networkReconnectDelayMs = 5_000;
|
|
958
1128
|
private appVersion = "unknown";
|
|
1129
|
+
private openclawRuntimeCache: { value: ReturnType<typeof detectOpenClawRuntime>; expiresAt: number } | null = null;
|
|
1130
|
+
private openclawBridgeStatusCache: { value: OpenClawBridgeStatus; expiresAt: number } | null = null;
|
|
959
1131
|
|
|
960
1132
|
constructor(options?: { workspaceRoot?: string; projectRoot?: string; storageRoot?: string }) {
|
|
961
1133
|
this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
|
|
@@ -971,6 +1143,10 @@ export class LocalNodeService {
|
|
|
971
1143
|
this.socialMessageGovernanceRepo = new SocialMessageGovernanceRepo(this.storageRoot);
|
|
972
1144
|
this.socialMessageRepo = new SocialMessageRepo(this.storageRoot);
|
|
973
1145
|
this.socialMessageObservationRepo = new SocialMessageObservationRepo(this.storageRoot);
|
|
1146
|
+
this.privateMessageRepo = new PrivateMessageRepo(this.storageRoot);
|
|
1147
|
+
this.privateMessageReceiptRepo = new PrivateMessageReceiptRepo(this.storageRoot);
|
|
1148
|
+
this.privateEncryptionKeyRepo = new PrivateEncryptionKeyRepo(this.storageRoot);
|
|
1149
|
+
this.privateMessagingRuntimeRepo = new PrivateMessagingRuntimeRepo(this.storageRoot);
|
|
974
1150
|
this.socialRuntimeRepo = new SocialRuntimeRepo(this.storageRoot);
|
|
975
1151
|
this.messageGovernance = this.defaultMessageGovernance();
|
|
976
1152
|
|
|
@@ -1001,6 +1177,24 @@ export class LocalNodeService {
|
|
|
1001
1177
|
this.networkPort = resolved.port;
|
|
1002
1178
|
}
|
|
1003
1179
|
|
|
1180
|
+
private getCachedOpenClawRuntime() {
|
|
1181
|
+
const now = Date.now();
|
|
1182
|
+
if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
|
|
1183
|
+
return this.openclawRuntimeCache.value;
|
|
1184
|
+
}
|
|
1185
|
+
const value = detectOpenClawRuntime(this.projectRoot);
|
|
1186
|
+
this.openclawRuntimeCache = {
|
|
1187
|
+
value,
|
|
1188
|
+
expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
|
|
1189
|
+
};
|
|
1190
|
+
return value;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
private invalidateOpenClawCaches() {
|
|
1194
|
+
this.openclawRuntimeCache = null;
|
|
1195
|
+
this.openclawBridgeStatusCache = null;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1004
1198
|
async start(): Promise<void> {
|
|
1005
1199
|
await this.hydrateFromDisk();
|
|
1006
1200
|
|
|
@@ -1014,6 +1208,7 @@ export class LocalNodeService {
|
|
|
1014
1208
|
clearInterval(this.broadcaster);
|
|
1015
1209
|
this.broadcaster = null;
|
|
1016
1210
|
}
|
|
1211
|
+
await this.flushPrivatePersistence();
|
|
1017
1212
|
if (this.networkStarted) {
|
|
1018
1213
|
await this.network.stop();
|
|
1019
1214
|
}
|
|
@@ -1034,6 +1229,9 @@ export class LocalNodeService {
|
|
|
1034
1229
|
getOverview() {
|
|
1035
1230
|
const discovered = this.search("");
|
|
1036
1231
|
const onlineCount = discovered.filter((profile) => profile.online).length;
|
|
1232
|
+
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
1233
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1234
|
+
const openclawSkillInstallation = detectOpenClawSkillInstallation();
|
|
1037
1235
|
|
|
1038
1236
|
return {
|
|
1039
1237
|
app_version: this.appVersion,
|
|
@@ -1050,6 +1248,15 @@ export class LocalNodeService {
|
|
|
1050
1248
|
init_state: this.initState,
|
|
1051
1249
|
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
1052
1250
|
onboarding: this.getOnboardingSummary(),
|
|
1251
|
+
openclaw: {
|
|
1252
|
+
detected: openclawInstallation.detected,
|
|
1253
|
+
running: openclawRuntime.running,
|
|
1254
|
+
detection_mode: openclawRuntime.detection_mode,
|
|
1255
|
+
gateway_url: openclawRuntime.gateway_url,
|
|
1256
|
+
gateway_probe_ok: openclawRuntime.gateway_probe_ok,
|
|
1257
|
+
status_ok: openclawRuntime.status_ok,
|
|
1258
|
+
skill_installed: openclawSkillInstallation.installed,
|
|
1259
|
+
},
|
|
1053
1260
|
social: {
|
|
1054
1261
|
found: this.socialFound,
|
|
1055
1262
|
enabled: this.socialConfig.enabled,
|
|
@@ -1197,6 +1404,7 @@ export class LocalNodeService {
|
|
|
1197
1404
|
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
1198
1405
|
const peers: Array<{ status?: string }> = diagnostics?.peers?.items ?? [];
|
|
1199
1406
|
const online = peers.filter((peer: { status?: string }) => peer.status === "online").length;
|
|
1407
|
+
const memory = process.memoryUsage();
|
|
1200
1408
|
|
|
1201
1409
|
return {
|
|
1202
1410
|
adapter: this.adapterMode,
|
|
@@ -1221,6 +1429,23 @@ export class LocalNodeService {
|
|
|
1221
1429
|
adapter_stats: diagnostics?.stats ?? null,
|
|
1222
1430
|
adapter_transport_stats: diagnostics?.transport_stats ?? null,
|
|
1223
1431
|
adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
1432
|
+
runtime_diagnostics: {
|
|
1433
|
+
memory_mib: {
|
|
1434
|
+
rss: formatBytesToMiB(memory.rss),
|
|
1435
|
+
heap_used: formatBytesToMiB(memory.heapUsed),
|
|
1436
|
+
heap_total: formatBytesToMiB(memory.heapTotal),
|
|
1437
|
+
external: formatBytesToMiB(memory.external),
|
|
1438
|
+
},
|
|
1439
|
+
directory: {
|
|
1440
|
+
profile_count: Object.keys(this.directory.profiles).length,
|
|
1441
|
+
presence_count: Object.keys(this.directory.presence).length,
|
|
1442
|
+
index_key_count: Object.keys(this.directory.index).length,
|
|
1443
|
+
},
|
|
1444
|
+
social: {
|
|
1445
|
+
message_count: this.socialMessages.length,
|
|
1446
|
+
observation_count: this.socialMessageObservations.length,
|
|
1447
|
+
},
|
|
1448
|
+
},
|
|
1224
1449
|
adapter_diagnostics_summary: relayCapable || diagnostics
|
|
1225
1450
|
? {
|
|
1226
1451
|
started: this.networkStarted,
|
|
@@ -1337,6 +1562,92 @@ export class LocalNodeService {
|
|
|
1337
1562
|
};
|
|
1338
1563
|
}
|
|
1339
1564
|
|
|
1565
|
+
getAppUpdateStatus() {
|
|
1566
|
+
const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
|
|
1567
|
+
const fallback = {
|
|
1568
|
+
current_version: currentVersion,
|
|
1569
|
+
latest_version: currentVersion,
|
|
1570
|
+
update_available: false,
|
|
1571
|
+
channel: "latest",
|
|
1572
|
+
platform: process.platform,
|
|
1573
|
+
checked_at: Date.now(),
|
|
1574
|
+
can_update: true,
|
|
1575
|
+
check_error: null as string | null,
|
|
1576
|
+
};
|
|
1577
|
+
try {
|
|
1578
|
+
const result = spawnSync("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
|
|
1579
|
+
cwd: this.projectRoot,
|
|
1580
|
+
encoding: "utf8",
|
|
1581
|
+
env: {
|
|
1582
|
+
...process.env,
|
|
1583
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1584
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1585
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1586
|
+
},
|
|
1587
|
+
});
|
|
1588
|
+
if ((result.status ?? 1) !== 0) {
|
|
1589
|
+
return {
|
|
1590
|
+
...fallback,
|
|
1591
|
+
check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}") as { latest?: string };
|
|
1595
|
+
const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
|
|
1596
|
+
return {
|
|
1597
|
+
...fallback,
|
|
1598
|
+
latest_version: latestVersion,
|
|
1599
|
+
update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
|
|
1600
|
+
};
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
return {
|
|
1603
|
+
...fallback,
|
|
1604
|
+
check_error: error instanceof Error ? error.message : String(error),
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
startAppUpdate(): { started: boolean; target_version: string; platform: string; reason?: string } {
|
|
1610
|
+
const status = this.getAppUpdateStatus();
|
|
1611
|
+
if (!status.update_available || !status.latest_version) {
|
|
1612
|
+
return {
|
|
1613
|
+
started: false,
|
|
1614
|
+
target_version: status.latest_version || status.current_version,
|
|
1615
|
+
platform: process.platform,
|
|
1616
|
+
reason: status.check_error || "already_current",
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
const shimPath = userShimPath();
|
|
1620
|
+
const scriptPath = resolve(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
|
|
1621
|
+
const useShim = existsSync(shimPath);
|
|
1622
|
+
if (!useShim && !existsSync(scriptPath)) {
|
|
1623
|
+
return {
|
|
1624
|
+
started: false,
|
|
1625
|
+
target_version: status.latest_version,
|
|
1626
|
+
platform: process.platform,
|
|
1627
|
+
reason: "missing_cli_script",
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
const command = useShim ? shimPath : process.execPath;
|
|
1631
|
+
const args = useShim ? ["update"] : [scriptPath, "update"];
|
|
1632
|
+
const child = spawn(command, args, {
|
|
1633
|
+
cwd: this.projectRoot,
|
|
1634
|
+
detached: true,
|
|
1635
|
+
stdio: "ignore",
|
|
1636
|
+
env: {
|
|
1637
|
+
...process.env,
|
|
1638
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1639
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1640
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1641
|
+
},
|
|
1642
|
+
});
|
|
1643
|
+
child.unref();
|
|
1644
|
+
return {
|
|
1645
|
+
started: true,
|
|
1646
|
+
target_version: status.latest_version,
|
|
1647
|
+
platform: process.platform,
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1340
1651
|
getIntegrationSummary() {
|
|
1341
1652
|
const status = this.getIntegrationStatus();
|
|
1342
1653
|
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
@@ -1635,6 +1946,7 @@ export class LocalNodeService {
|
|
|
1635
1946
|
return {
|
|
1636
1947
|
...message,
|
|
1637
1948
|
display_name: profile?.display_name || message.display_name || "Unnamed",
|
|
1949
|
+
avatar_url: profile?.avatar_url || "",
|
|
1638
1950
|
is_self: message.agent_id === this.identity?.agent_id,
|
|
1639
1951
|
online: isAgentOnline(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
|
|
1640
1952
|
last_seen_at: lastSeenAt || null,
|
|
@@ -1654,10 +1966,161 @@ export class LocalNodeService {
|
|
|
1654
1966
|
};
|
|
1655
1967
|
}
|
|
1656
1968
|
|
|
1969
|
+
getPrivateMessagingState() {
|
|
1970
|
+
return {
|
|
1971
|
+
enabled: Boolean(this.identity && this.privateEncryptionKeyPair),
|
|
1972
|
+
agent_id: this.identity?.agent_id || "",
|
|
1973
|
+
encryption_public_key: this.privateEncryptionKeyPair?.public_key || "",
|
|
1974
|
+
conversation_count: this.getPrivateConversations().length,
|
|
1975
|
+
message_count: this.privateMessages.length,
|
|
1976
|
+
runtime: this.privateMessagingRuntime,
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
getPrivateConversations(): Array<{
|
|
1981
|
+
conversation_id: string;
|
|
1982
|
+
peer_agent_id: string;
|
|
1983
|
+
peer_display_name: string;
|
|
1984
|
+
peer_avatar_url: string;
|
|
1985
|
+
peer_public_key: string;
|
|
1986
|
+
last_message_at: number | null;
|
|
1987
|
+
unread_count: number;
|
|
1988
|
+
}> {
|
|
1989
|
+
const conversations = new Map<string, {
|
|
1990
|
+
conversation_id: string;
|
|
1991
|
+
peer_agent_id: string;
|
|
1992
|
+
peer_display_name: string;
|
|
1993
|
+
peer_avatar_url: string;
|
|
1994
|
+
peer_public_key: string;
|
|
1995
|
+
last_message_at: number | null;
|
|
1996
|
+
unread_count: number;
|
|
1997
|
+
}>();
|
|
1998
|
+
for (const message of this.privateMessages) {
|
|
1999
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
2003
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
const peerProfile = this.directory.profiles[peerAgentId];
|
|
2007
|
+
const current = conversations.get(message.conversation_id);
|
|
2008
|
+
const nextLast = Math.max(current?.last_message_at || 0, message.created_at || 0) || null;
|
|
2009
|
+
const learnedPeerKey = this.privatePeerEncryptionKeys[peerAgentId] || "";
|
|
2010
|
+
conversations.set(message.conversation_id, {
|
|
2011
|
+
conversation_id: message.conversation_id,
|
|
2012
|
+
peer_agent_id: peerAgentId,
|
|
2013
|
+
peer_display_name: peerProfile?.display_name || peerAgentId,
|
|
2014
|
+
peer_avatar_url: peerProfile?.avatar_url || "",
|
|
2015
|
+
peer_public_key: learnedPeerKey || peerProfile?.private_encryption_public_key || "",
|
|
2016
|
+
last_message_at: nextLast,
|
|
2017
|
+
unread_count: current?.unread_count || 0,
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
getPrivateMessages(conversationId: string, limit = PRIVATE_MESSAGE_QUERY_LIMIT): PrivateMessageView[] {
|
|
2024
|
+
const normalizedConversationId = String(conversationId || "").trim();
|
|
2025
|
+
const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
|
|
2026
|
+
const receiptsByMessageId = new Map(
|
|
2027
|
+
this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status] as const)
|
|
2028
|
+
);
|
|
2029
|
+
return this.privateMessages
|
|
2030
|
+
.filter((message) => {
|
|
2031
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
2035
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
2036
|
+
return false;
|
|
2037
|
+
}
|
|
2038
|
+
return !normalizedConversationId || message.conversation_id === normalizedConversationId;
|
|
2039
|
+
})
|
|
2040
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
2041
|
+
.slice(0, resolvedLimit)
|
|
2042
|
+
.map((message) => ({
|
|
2043
|
+
message_id: message.message_id,
|
|
2044
|
+
conversation_id: message.conversation_id,
|
|
2045
|
+
from_agent_id: message.from_agent_id,
|
|
2046
|
+
to_agent_id: message.to_agent_id,
|
|
2047
|
+
body: this.decryptPrivateMessageBody(message),
|
|
2048
|
+
created_at: message.created_at,
|
|
2049
|
+
is_self: message.from_agent_id === this.identity?.agent_id,
|
|
2050
|
+
delivery_status:
|
|
2051
|
+
receiptsByMessageId.get(message.message_id) ||
|
|
2052
|
+
this.privateMessageDeliveryStatusCache.get(message.message_id) ||
|
|
2053
|
+
(message.from_agent_id === this.identity?.agent_id ? "fallback-sent" : "sent"),
|
|
2054
|
+
}));
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
async sendPrivateMessage(input: {
|
|
2058
|
+
to_agent_id: string;
|
|
2059
|
+
recipient_encryption_public_key: string;
|
|
2060
|
+
body: string;
|
|
2061
|
+
}): Promise<{ sent: boolean; reason: string; message?: PrivateMessageView }> {
|
|
2062
|
+
if (!this.identity || !this.privateEncryptionKeyPair) {
|
|
2063
|
+
return { sent: false, reason: "missing_identity_or_private_key" };
|
|
2064
|
+
}
|
|
2065
|
+
const toAgentId = String(input.to_agent_id || "").trim();
|
|
2066
|
+
const learnedRecipientKey = this.privatePeerEncryptionKeys[toAgentId] || "";
|
|
2067
|
+
const profileRecipientKey = this.directory.profiles[toAgentId]?.private_encryption_public_key || "";
|
|
2068
|
+
const recipientKey = String(learnedRecipientKey || input.recipient_encryption_public_key || profileRecipientKey || "").trim();
|
|
2069
|
+
const body = String(input.body || "").trim();
|
|
2070
|
+
if (toAgentId === this.identity.agent_id) {
|
|
2071
|
+
return { sent: false, reason: "self_private_message_not_allowed" };
|
|
2072
|
+
}
|
|
2073
|
+
const toPeerId = this.privatePeerRoutes[toAgentId] || "";
|
|
2074
|
+
if (!toAgentId || !recipientKey || !body) {
|
|
2075
|
+
return { sent: false, reason: "invalid_private_message_input" };
|
|
2076
|
+
}
|
|
2077
|
+
const encrypted = encryptPrivatePayload({
|
|
2078
|
+
plaintext: body,
|
|
2079
|
+
recipient_public_key: recipientKey,
|
|
2080
|
+
sender_keypair: this.privateEncryptionKeyPair,
|
|
2081
|
+
});
|
|
2082
|
+
const message = signPrivateMessage({
|
|
2083
|
+
identity: this.identity,
|
|
2084
|
+
message_id: createHash("sha256").update(`${this.identity.agent_id}:${toAgentId}:${Date.now()}:${body}:${Math.random()}`, "utf8").digest("hex"),
|
|
2085
|
+
conversation_id: this.buildPrivateConversationId(this.identity.agent_id, toAgentId),
|
|
2086
|
+
to_agent_id: toAgentId,
|
|
2087
|
+
sender_encryption_public_key: encrypted.sender_encryption_public_key,
|
|
2088
|
+
recipient_encryption_public_key: recipientKey,
|
|
2089
|
+
ciphertext: encrypted.ciphertext,
|
|
2090
|
+
nonce: encrypted.nonce,
|
|
2091
|
+
created_at: Date.now(),
|
|
2092
|
+
});
|
|
2093
|
+
this.privateMessageBodyCache.set(message.message_id, body);
|
|
2094
|
+
this.ingestPrivateMessage(message);
|
|
2095
|
+
await this.persistPrivateMessages();
|
|
2096
|
+
let reason = "fallback-sent";
|
|
2097
|
+
if (toPeerId && typeof this.network.sendDirect === "function") {
|
|
2098
|
+
try {
|
|
2099
|
+
await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
|
|
2100
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
2101
|
+
reason = "direct-sent";
|
|
2102
|
+
} catch {
|
|
2103
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
2104
|
+
}
|
|
2105
|
+
} else {
|
|
2106
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
2107
|
+
}
|
|
2108
|
+
this.privateMessageDeliveryStatusCache.set(message.message_id, reason as PrivateMessageView["delivery_status"]);
|
|
2109
|
+
const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
|
|
2110
|
+
if (view) {
|
|
2111
|
+
view.delivery_status = reason as PrivateMessageView["delivery_status"];
|
|
2112
|
+
}
|
|
2113
|
+
return { sent: true, reason, message: view };
|
|
2114
|
+
}
|
|
2115
|
+
|
|
1657
2116
|
getOpenClawBridgeStatus(): OpenClawBridgeStatus {
|
|
2117
|
+
const now = Date.now();
|
|
2118
|
+
if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
|
|
2119
|
+
return this.openclawBridgeStatusCache.value;
|
|
2120
|
+
}
|
|
1658
2121
|
const integration = this.getIntegrationStatus();
|
|
1659
2122
|
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
1660
|
-
const openclawRuntime =
|
|
2123
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1661
2124
|
const skillInstallation = detectOpenClawSkillInstallation();
|
|
1662
2125
|
const ownerDelivery = detectOwnerDeliveryStatus({
|
|
1663
2126
|
workspaceRoot: this.projectRoot,
|
|
@@ -1665,7 +2128,7 @@ export class LocalNodeService {
|
|
|
1665
2128
|
openclawRunning: openclawRuntime.running,
|
|
1666
2129
|
skillInstalled: skillInstallation.installed,
|
|
1667
2130
|
});
|
|
1668
|
-
|
|
2131
|
+
const value: OpenClawBridgeStatus = {
|
|
1669
2132
|
enabled: this.socialConfig.enabled,
|
|
1670
2133
|
connected_to_silicaclaw: integration.connected_to_silicaclaw,
|
|
1671
2134
|
public_enabled: integration.public_enabled,
|
|
@@ -1721,6 +2184,11 @@ export class LocalNodeService {
|
|
|
1721
2184
|
install_skill: "/api/openclaw/bridge/skill-install",
|
|
1722
2185
|
},
|
|
1723
2186
|
};
|
|
2187
|
+
this.openclawBridgeStatusCache = {
|
|
2188
|
+
value,
|
|
2189
|
+
expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
|
|
2190
|
+
};
|
|
2191
|
+
return value;
|
|
1724
2192
|
}
|
|
1725
2193
|
|
|
1726
2194
|
async installOpenClawSkill(skillName?: string) {
|
|
@@ -1735,6 +2203,7 @@ export class LocalNodeService {
|
|
|
1735
2203
|
maxBuffer: 1024 * 1024,
|
|
1736
2204
|
});
|
|
1737
2205
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
2206
|
+
this.invalidateOpenClawCaches();
|
|
1738
2207
|
return {
|
|
1739
2208
|
...parsed,
|
|
1740
2209
|
bridge: this.getOpenClawBridgeStatus(),
|
|
@@ -1756,7 +2225,7 @@ export class LocalNodeService {
|
|
|
1756
2225
|
const workspaceSkillDir = resolve(homeDir, "workspace", "skills");
|
|
1757
2226
|
const legacySkillDir = resolve(homeDir, "skills");
|
|
1758
2227
|
const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
|
|
1759
|
-
const openclawRuntime =
|
|
2228
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1760
2229
|
|
|
1761
2230
|
return {
|
|
1762
2231
|
bridge_api_base: DEFAULT_BRIDGE_API_BASE,
|
|
@@ -1797,7 +2266,11 @@ export class LocalNodeService {
|
|
|
1797
2266
|
}
|
|
1798
2267
|
|
|
1799
2268
|
getSkillsView() {
|
|
1800
|
-
const
|
|
2269
|
+
const bundledRootCandidates = [
|
|
2270
|
+
resolve(this.workspaceRoot, "openclaw-skills"),
|
|
2271
|
+
resolve(this.projectRoot, "openclaw-skills"),
|
|
2272
|
+
];
|
|
2273
|
+
const bundledRoot = bundledRootCandidates.find((candidate) => existsSync(candidate)) || bundledRootCandidates[0];
|
|
1801
2274
|
const openclawHome = resolve(process.env.HOME || "", ".openclaw");
|
|
1802
2275
|
const workspaceInstallRoot = resolve(openclawHome, "workspace", "skills");
|
|
1803
2276
|
const legacyInstallRoot = resolve(openclawHome, "skills");
|
|
@@ -2166,15 +2639,14 @@ export class LocalNodeService {
|
|
|
2166
2639
|
profile: this.profile,
|
|
2167
2640
|
};
|
|
2168
2641
|
const presenceRecord = signPresence(this.identity, Date.now());
|
|
2169
|
-
const
|
|
2170
|
-
const replayMessages = this.getReplayableSelfSocialMessages();
|
|
2642
|
+
const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
|
|
2643
|
+
const replayMessages = this.getReplayableSelfSocialMessages(reason);
|
|
2171
2644
|
|
|
2172
2645
|
try {
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
for (const record of indexRecords) {
|
|
2176
|
-
await this.publish("index", record);
|
|
2646
|
+
if (shouldPublishProfile) {
|
|
2647
|
+
await this.publish("profile", profileRecord);
|
|
2177
2648
|
}
|
|
2649
|
+
await this.publish("presence", presenceRecord);
|
|
2178
2650
|
for (const message of replayMessages) {
|
|
2179
2651
|
await this.publish(SOCIAL_MESSAGE_TOPIC, message);
|
|
2180
2652
|
}
|
|
@@ -2183,7 +2655,9 @@ export class LocalNodeService {
|
|
|
2183
2655
|
this.lastBroadcastErrorAt = Date.now();
|
|
2184
2656
|
this.lastBroadcastError = message;
|
|
2185
2657
|
this.broadcastFailureCount += 1;
|
|
2658
|
+
this.consecutiveBroadcastFailures += 1;
|
|
2186
2659
|
await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
|
|
2660
|
+
await this.maybeRecoverFromBroadcastFailure(reason, message);
|
|
2187
2661
|
return { sent: false, reason: "publish_failed", error: message };
|
|
2188
2662
|
}
|
|
2189
2663
|
|
|
@@ -2191,22 +2665,75 @@ export class LocalNodeService {
|
|
|
2191
2665
|
this.broadcastCount += 1;
|
|
2192
2666
|
this.lastBroadcastError = null;
|
|
2193
2667
|
this.lastBroadcastErrorAt = 0;
|
|
2668
|
+
this.consecutiveBroadcastFailures = 0;
|
|
2194
2669
|
|
|
2195
2670
|
this.directory = ingestProfileRecord(this.directory, profileRecord);
|
|
2196
2671
|
this.directory = ingestPresenceRecord(this.directory, presenceRecord);
|
|
2197
|
-
for (const record of indexRecords) {
|
|
2198
|
-
this.directory = ingestIndexRecord(this.directory, record);
|
|
2199
|
-
}
|
|
2200
2672
|
this.compactCacheInMemory();
|
|
2201
2673
|
await this.persistCache();
|
|
2202
2674
|
|
|
2203
2675
|
await this.log(
|
|
2204
2676
|
"info",
|
|
2205
|
-
`Broadcast sent (${
|
|
2677
|
+
`Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`
|
|
2206
2678
|
);
|
|
2207
2679
|
return { sent: true, reason };
|
|
2208
2680
|
}
|
|
2209
2681
|
|
|
2682
|
+
private shouldPublishProfileRecord(
|
|
2683
|
+
profileRecord: SignedProfileRecord,
|
|
2684
|
+
reason: string,
|
|
2685
|
+
now = Date.now()
|
|
2686
|
+
): boolean {
|
|
2687
|
+
if (reason !== "interval") {
|
|
2688
|
+
this.lastProfileBroadcastSignature = profileRecord.profile.signature;
|
|
2689
|
+
this.lastProfileBroadcastAt = now;
|
|
2690
|
+
return true;
|
|
2691
|
+
}
|
|
2692
|
+
const signature = profileRecord.profile.signature;
|
|
2693
|
+
const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
|
|
2694
|
+
const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
|
|
2695
|
+
if (!changedSinceLastPublish && !refreshDue) {
|
|
2696
|
+
return false;
|
|
2697
|
+
}
|
|
2698
|
+
this.lastProfileBroadcastSignature = signature;
|
|
2699
|
+
this.lastProfileBroadcastAt = now;
|
|
2700
|
+
return true;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
private async maybeRecoverFromBroadcastFailure(reason: string, errorMessage: string): Promise<void> {
|
|
2704
|
+
const recoveryThreshold = 3;
|
|
2705
|
+
const recoveryCooldownMs = 60_000;
|
|
2706
|
+
if (this.broadcastRecoveryInFlight) {
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
if (this.consecutiveBroadcastFailures < recoveryThreshold) {
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
if (Date.now() - this.lastBroadcastRecoveryAttemptAt < recoveryCooldownMs) {
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
if (this.adapterMode !== "relay-preview" && this.adapterMode !== "webrtc-preview" && this.adapterMode !== "real-preview") {
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
this.broadcastRecoveryInFlight = true;
|
|
2720
|
+
this.lastBroadcastRecoveryAttemptAt = Date.now();
|
|
2721
|
+
try {
|
|
2722
|
+
await this.log(
|
|
2723
|
+
"warn",
|
|
2724
|
+
`Broadcast recovery triggered after ${this.consecutiveBroadcastFailures} consecutive failures (${reason}): ${errorMessage}`
|
|
2725
|
+
);
|
|
2726
|
+
await this.restartNetworkAdapter("broadcast_failure_recovery");
|
|
2727
|
+
} catch (recoveryError) {
|
|
2728
|
+
await this.log(
|
|
2729
|
+
"error",
|
|
2730
|
+
`Broadcast recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
|
|
2731
|
+
);
|
|
2732
|
+
} finally {
|
|
2733
|
+
this.broadcastRecoveryInFlight = false;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2210
2737
|
private async hydrateFromDisk(): Promise<void> {
|
|
2211
2738
|
this.initState = {
|
|
2212
2739
|
identity_auto_created: false,
|
|
@@ -2236,6 +2763,8 @@ export class LocalNodeService {
|
|
|
2236
2763
|
await this.log("info", `Bound existing OpenClaw identity: ${resolvedIdentity.openclaw_source_path}`);
|
|
2237
2764
|
}
|
|
2238
2765
|
await this.identityRepo.set(this.identity);
|
|
2766
|
+
this.privateEncryptionKeyPair = (await this.privateEncryptionKeyRepo.get()) || createPrivateEncryptionKeyPair();
|
|
2767
|
+
await this.privateEncryptionKeyRepo.set(this.privateEncryptionKeyPair);
|
|
2239
2768
|
|
|
2240
2769
|
const existingProfile = await this.profileRepo.get();
|
|
2241
2770
|
const profileInput = resolveProfileInputWithSocial({
|
|
@@ -2244,7 +2773,10 @@ export class LocalNodeService {
|
|
|
2244
2773
|
existingProfile: existingProfile && existingProfile.agent_id === this.identity.agent_id ? existingProfile : null,
|
|
2245
2774
|
rootDir: this.projectRoot,
|
|
2246
2775
|
});
|
|
2247
|
-
this.profile = signProfile(
|
|
2776
|
+
this.profile = signProfile({
|
|
2777
|
+
...profileInput,
|
|
2778
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || profileInput.private_encryption_public_key || "",
|
|
2779
|
+
}, this.identity);
|
|
2248
2780
|
if (!existingProfile || existingProfile.agent_id !== this.identity.agent_id) {
|
|
2249
2781
|
this.initState.profile_auto_created = true;
|
|
2250
2782
|
await this.log("info", "profile.json missing/invalid, initialized from social/default profile");
|
|
@@ -2258,6 +2790,11 @@ export class LocalNodeService {
|
|
|
2258
2790
|
};
|
|
2259
2791
|
this.socialMessages = this.normalizeSocialMessages(await this.socialMessageRepo.get());
|
|
2260
2792
|
this.socialMessageObservations = this.normalizeSocialMessageObservations(await this.socialMessageObservationRepo.get());
|
|
2793
|
+
const storedPrivateMessages = await this.privateMessageRepo.get();
|
|
2794
|
+
this.hydratePrivateMessageBodyCache(storedPrivateMessages);
|
|
2795
|
+
this.privateMessages = this.normalizePrivateMessages(storedPrivateMessages);
|
|
2796
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(await this.privateMessageReceiptRepo.get());
|
|
2797
|
+
await this.refreshPrivateMessagingRuntime();
|
|
2261
2798
|
this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: this.profile });
|
|
2262
2799
|
this.compactCacheInMemory();
|
|
2263
2800
|
await this.persistCache();
|
|
@@ -2276,7 +2813,10 @@ export class LocalNodeService {
|
|
|
2276
2813
|
existingProfile: this.profile,
|
|
2277
2814
|
rootDir: this.projectRoot,
|
|
2278
2815
|
});
|
|
2279
|
-
const nextProfile = signProfile(
|
|
2816
|
+
const nextProfile = signProfile({
|
|
2817
|
+
...nextProfileInput,
|
|
2818
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || nextProfileInput.private_encryption_public_key || "",
|
|
2819
|
+
}, this.identity);
|
|
2280
2820
|
this.profile = nextProfile;
|
|
2281
2821
|
await this.profileRepo.set(nextProfile);
|
|
2282
2822
|
|
|
@@ -2341,7 +2881,8 @@ export class LocalNodeService {
|
|
|
2341
2881
|
|
|
2342
2882
|
private async onMessage(
|
|
2343
2883
|
topic: "profile" | "presence" | "index" | "social.message" | "social.message.observation",
|
|
2344
|
-
data: unknown
|
|
2884
|
+
data: unknown,
|
|
2885
|
+
meta?: { peerId?: string }
|
|
2345
2886
|
): Promise<void> {
|
|
2346
2887
|
this.receivedCount += 1;
|
|
2347
2888
|
this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
|
|
@@ -2359,6 +2900,9 @@ export class LocalNodeService {
|
|
|
2359
2900
|
return;
|
|
2360
2901
|
}
|
|
2361
2902
|
}
|
|
2903
|
+
if (meta?.peerId && record.profile.agent_id && !this.privatePeerRoutes[record.profile.agent_id]) {
|
|
2904
|
+
this.privatePeerRoutes[record.profile.agent_id] = meta.peerId;
|
|
2905
|
+
}
|
|
2362
2906
|
|
|
2363
2907
|
this.directory = ingestProfileRecord(this.directory, record);
|
|
2364
2908
|
this.compactCacheInMemory();
|
|
@@ -2378,6 +2922,9 @@ export class LocalNodeService {
|
|
|
2378
2922
|
return;
|
|
2379
2923
|
}
|
|
2380
2924
|
}
|
|
2925
|
+
if (meta?.peerId && record.agent_id && !this.privatePeerRoutes[record.agent_id]) {
|
|
2926
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2927
|
+
}
|
|
2381
2928
|
|
|
2382
2929
|
this.directory = ingestPresenceRecord(this.directory, record);
|
|
2383
2930
|
this.compactCacheInMemory();
|
|
@@ -2394,6 +2941,9 @@ export class LocalNodeService {
|
|
|
2394
2941
|
await this.log("warn", `Rejected social message with invalid signature (${record.message_id.slice(0, 10)})`);
|
|
2395
2942
|
return;
|
|
2396
2943
|
}
|
|
2944
|
+
if (meta?.peerId && record.agent_id && !this.privatePeerRoutes[record.agent_id]) {
|
|
2945
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2946
|
+
}
|
|
2397
2947
|
if (this.hasSocialMessage(record.message_id)) {
|
|
2398
2948
|
await this.publishObservationForMessage(record);
|
|
2399
2949
|
return;
|
|
@@ -2432,6 +2982,45 @@ export class LocalNodeService {
|
|
|
2432
2982
|
await this.persistCache();
|
|
2433
2983
|
}
|
|
2434
2984
|
|
|
2985
|
+
private async onDirectMessage(
|
|
2986
|
+
topic: "private.message" | "private.message.receipt",
|
|
2987
|
+
data: unknown,
|
|
2988
|
+
meta?: { peerId?: string }
|
|
2989
|
+
): Promise<void> {
|
|
2990
|
+
if (topic === PRIVATE_MESSAGE_TOPIC) {
|
|
2991
|
+
const record = this.normalizeIncomingPrivateMessage(data);
|
|
2992
|
+
if (!record || !verifyPrivateMessage(record)) {
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
if (meta?.peerId && record.from_agent_id) {
|
|
2996
|
+
this.privatePeerRoutes[record.from_agent_id] = meta.peerId;
|
|
2997
|
+
}
|
|
2998
|
+
if (record.from_agent_id && record.sender_encryption_public_key) {
|
|
2999
|
+
this.privatePeerEncryptionKeys[record.from_agent_id] = record.sender_encryption_public_key;
|
|
3000
|
+
}
|
|
3001
|
+
if (record.to_agent_id !== this.identity?.agent_id || this.hasPrivateMessage(record.message_id)) {
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
this.ingestPrivateMessage(record);
|
|
3005
|
+
await this.persistPrivateMessages();
|
|
3006
|
+
await this.sendPrivateMessageReceipt(record, meta?.peerId);
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
const receipt = this.normalizeIncomingPrivateMessageReceipt(data);
|
|
3011
|
+
if (!receipt || !verifyPrivateMessageReceipt(receipt)) {
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
if (meta?.peerId && receipt.from_agent_id) {
|
|
3015
|
+
this.privatePeerRoutes[receipt.from_agent_id] = meta.peerId;
|
|
3016
|
+
}
|
|
3017
|
+
if (receipt.to_agent_id !== this.identity?.agent_id) {
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
3021
|
+
await this.persistPrivateMessageReceipts();
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2435
3024
|
private startBroadcastLoop(): void {
|
|
2436
3025
|
if (this.broadcaster) {
|
|
2437
3026
|
clearInterval(this.broadcaster);
|
|
@@ -2457,21 +3046,35 @@ export class LocalNodeService {
|
|
|
2457
3046
|
if (this.subscriptionsBound) {
|
|
2458
3047
|
return;
|
|
2459
3048
|
}
|
|
2460
|
-
this.network.subscribe("profile", (data: SignedProfileRecord) => {
|
|
2461
|
-
this.onMessage("profile", data);
|
|
3049
|
+
this.network.subscribe("profile", (data: SignedProfileRecord, meta?: { peerId?: string }) => {
|
|
3050
|
+
this.onMessage("profile", data, meta);
|
|
3051
|
+
});
|
|
3052
|
+
this.network.subscribe("presence", (data: PresenceRecord, meta?: { peerId?: string }) => {
|
|
3053
|
+
this.onMessage("presence", data, meta);
|
|
2462
3054
|
});
|
|
2463
|
-
this.network.subscribe("
|
|
2464
|
-
this.onMessage("
|
|
3055
|
+
this.network.subscribe("index", (data: IndexRefRecord, meta?: { peerId?: string }) => {
|
|
3056
|
+
this.onMessage("index", data, meta);
|
|
2465
3057
|
});
|
|
2466
|
-
this.network.subscribe(
|
|
2467
|
-
this.onMessage(
|
|
3058
|
+
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data: SocialMessageRecord, meta?: { peerId?: string }) => {
|
|
3059
|
+
this.onMessage(SOCIAL_MESSAGE_TOPIC, data, meta);
|
|
2468
3060
|
});
|
|
2469
|
-
this.network.subscribe(
|
|
2470
|
-
this.onMessage(
|
|
3061
|
+
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord, meta?: { peerId?: string }) => {
|
|
3062
|
+
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
|
|
2471
3063
|
});
|
|
2472
|
-
this.network.subscribe(
|
|
2473
|
-
this.
|
|
3064
|
+
this.network.subscribe(PRIVATE_MESSAGE_TOPIC, (data: PrivateMessageRecord, meta?: { peerId?: string }) => {
|
|
3065
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
2474
3066
|
});
|
|
3067
|
+
this.network.subscribe(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data: PrivateMessageReceiptRecord, meta?: { peerId?: string }) => {
|
|
3068
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
3069
|
+
});
|
|
3070
|
+
if (typeof this.network.subscribeDirect === "function") {
|
|
3071
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data: PrivateMessageRecord, meta?: { peerId?: string }) => {
|
|
3072
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
3073
|
+
});
|
|
3074
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data: PrivateMessageReceiptRecord, meta?: { peerId?: string }) => {
|
|
3075
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
2475
3078
|
this.subscriptionsBound = true;
|
|
2476
3079
|
}
|
|
2477
3080
|
|
|
@@ -2628,9 +3231,66 @@ export class LocalNodeService {
|
|
|
2628
3231
|
this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
|
|
2629
3232
|
}
|
|
2630
3233
|
|
|
3234
|
+
private pruneRemoteProfilesInMemory(now = Date.now()): number {
|
|
3235
|
+
if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
|
|
3236
|
+
return 0;
|
|
3237
|
+
}
|
|
3238
|
+
const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
|
|
3239
|
+
const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
|
|
3240
|
+
if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
|
|
3241
|
+
return 0;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
const onlineRemoteProfiles = remoteProfiles.filter((profile) =>
|
|
3245
|
+
isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS)
|
|
3246
|
+
);
|
|
3247
|
+
const offlineRemoteProfiles = remoteProfiles
|
|
3248
|
+
.filter((profile) => !isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
|
|
3249
|
+
.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
|
|
3250
|
+
|
|
3251
|
+
const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
|
|
3252
|
+
const keptRemoteProfiles = [
|
|
3253
|
+
...onlineRemoteProfiles,
|
|
3254
|
+
...offlineRemoteProfiles.slice(0, keepOfflineCount),
|
|
3255
|
+
];
|
|
3256
|
+
const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
|
|
3257
|
+
const removedIds = remoteProfiles
|
|
3258
|
+
.map((profile) => profile.agent_id)
|
|
3259
|
+
.filter((agentId) => !keptRemoteIds.has(agentId));
|
|
3260
|
+
if (removedIds.length === 0) {
|
|
3261
|
+
return 0;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
const next = createEmptyDirectoryState();
|
|
3265
|
+
const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
|
|
3266
|
+
if (selfProfile) {
|
|
3267
|
+
next.profiles[selfAgentId] = selfProfile;
|
|
3268
|
+
const selfPresence = this.directory.presence[selfAgentId];
|
|
3269
|
+
if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
|
|
3270
|
+
next.presence[selfAgentId] = selfPresence;
|
|
3271
|
+
}
|
|
3272
|
+
const rebuilt = rebuildIndexForProfile(next, selfProfile);
|
|
3273
|
+
next.index = rebuilt.index;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
for (const profile of keptRemoteProfiles) {
|
|
3277
|
+
next.profiles[profile.agent_id] = profile;
|
|
3278
|
+
const seenAt = this.directory.presence[profile.agent_id];
|
|
3279
|
+
if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
|
|
3280
|
+
next.presence[profile.agent_id] = seenAt;
|
|
3281
|
+
}
|
|
3282
|
+
const rebuilt = rebuildIndexForProfile(next, profile);
|
|
3283
|
+
next.index = rebuilt.index;
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
this.directory = dedupeIndex(next);
|
|
3287
|
+
return removedIds.length;
|
|
3288
|
+
}
|
|
3289
|
+
|
|
2631
3290
|
private compactCacheInMemory(): number {
|
|
2632
3291
|
const cleaned = cleanupExpiredPresence(this.directory, Date.now(), PRESENCE_TTL_MS);
|
|
2633
3292
|
this.directory = dedupeIndex(cleaned.state);
|
|
3293
|
+
this.pruneRemoteProfilesInMemory();
|
|
2634
3294
|
return cleaned.removed;
|
|
2635
3295
|
}
|
|
2636
3296
|
|
|
@@ -2666,6 +3326,90 @@ export class LocalNodeService {
|
|
|
2666
3326
|
await this.socialMessageObservationRepo.set(this.socialMessageObservations);
|
|
2667
3327
|
}
|
|
2668
3328
|
|
|
3329
|
+
private async persistPrivateMessages(): Promise<void> {
|
|
3330
|
+
this.privateMessagesPersistDirty = true;
|
|
3331
|
+
if (this.privateMessagesPersistTimer) {
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
this.privateMessagesPersistTimer = setTimeout(() => {
|
|
3335
|
+
this.flushPrivateMessagesPersist().catch(() => {});
|
|
3336
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
private async persistPrivateMessageReceipts(): Promise<void> {
|
|
3340
|
+
this.privateMessageReceiptsPersistDirty = true;
|
|
3341
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
this.privateMessageReceiptsPersistTimer = setTimeout(() => {
|
|
3345
|
+
this.flushPrivateMessageReceiptsPersist().catch(() => {});
|
|
3346
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
private async flushPrivatePersistence(): Promise<void> {
|
|
3350
|
+
await Promise.all([
|
|
3351
|
+
this.flushPrivateMessagesPersist(),
|
|
3352
|
+
this.flushPrivateMessageReceiptsPersist(),
|
|
3353
|
+
]);
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
private async flushPrivateMessagesPersist(): Promise<void> {
|
|
3357
|
+
if (this.privateMessagesPersistTimer) {
|
|
3358
|
+
clearTimeout(this.privateMessagesPersistTimer);
|
|
3359
|
+
this.privateMessagesPersistTimer = null;
|
|
3360
|
+
}
|
|
3361
|
+
if (!this.privateMessagesPersistDirty) {
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
this.privateMessagesPersistDirty = false;
|
|
3365
|
+
await this.privateMessageRepo.set(this.buildPersistedPrivateMessages() as unknown as PrivateMessageRecord[]);
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
private hydratePrivateMessageBodyCache(items: unknown): void {
|
|
3369
|
+
if (!Array.isArray(items)) {
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
for (const item of items) {
|
|
3373
|
+
if (typeof item !== "object" || item === null) {
|
|
3374
|
+
continue;
|
|
3375
|
+
}
|
|
3376
|
+
const record = item as Partial<StoredPrivateMessageRecord>;
|
|
3377
|
+
const messageId = String(record.message_id || "").trim();
|
|
3378
|
+
const localPlaintext = typeof record.local_plaintext === "string" ? record.local_plaintext : "";
|
|
3379
|
+
if (messageId && localPlaintext) {
|
|
3380
|
+
this.privateMessageBodyCache.set(messageId, localPlaintext);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
private buildPersistedPrivateMessages(): StoredPrivateMessageRecord[] {
|
|
3386
|
+
return this.privateMessages.map((message) => {
|
|
3387
|
+
const localPlaintext =
|
|
3388
|
+
message.from_agent_id === this.identity?.agent_id
|
|
3389
|
+
? this.privateMessageBodyCache.get(message.message_id) || ""
|
|
3390
|
+
: "";
|
|
3391
|
+
if (!localPlaintext) {
|
|
3392
|
+
return { ...message };
|
|
3393
|
+
}
|
|
3394
|
+
return {
|
|
3395
|
+
...message,
|
|
3396
|
+
local_plaintext: localPlaintext,
|
|
3397
|
+
};
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
private async flushPrivateMessageReceiptsPersist(): Promise<void> {
|
|
3402
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
3403
|
+
clearTimeout(this.privateMessageReceiptsPersistTimer);
|
|
3404
|
+
this.privateMessageReceiptsPersistTimer = null;
|
|
3405
|
+
}
|
|
3406
|
+
if (!this.privateMessageReceiptsPersistDirty) {
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
this.privateMessageReceiptsPersistDirty = false;
|
|
3410
|
+
await this.privateMessageReceiptRepo.set(this.privateMessageReceipts);
|
|
3411
|
+
}
|
|
3412
|
+
|
|
2669
3413
|
private async log(level: "info" | "warn" | "error", message: string): Promise<void> {
|
|
2670
3414
|
await this.logRepo.append({
|
|
2671
3415
|
level,
|
|
@@ -2722,6 +3466,7 @@ export class LocalNodeService {
|
|
|
2722
3466
|
|
|
2723
3467
|
return buildPublicProfileSummary({
|
|
2724
3468
|
profile,
|
|
3469
|
+
is_self: isSelf,
|
|
2725
3470
|
online,
|
|
2726
3471
|
last_seen_at: lastSeenAt || null,
|
|
2727
3472
|
network_mode: isSelf ? this.networkMode : "unknown",
|
|
@@ -2775,6 +3520,7 @@ export class LocalNodeService {
|
|
|
2775
3520
|
updated_at: message.created_at,
|
|
2776
3521
|
signature: "",
|
|
2777
3522
|
},
|
|
3523
|
+
is_self: message.agent_id === this.identity?.agent_id,
|
|
2778
3524
|
online: false,
|
|
2779
3525
|
last_seen_at: null,
|
|
2780
3526
|
network_mode: "unknown",
|
|
@@ -2808,6 +3554,47 @@ export class LocalNodeService {
|
|
|
2808
3554
|
return `${digest.slice(0, 12)}:${digest.slice(-8)}`;
|
|
2809
3555
|
}
|
|
2810
3556
|
|
|
3557
|
+
private buildPrivateMessagingRuntimeState(): PrivateMessagingRuntimeState {
|
|
3558
|
+
const warnings: string[] = [];
|
|
3559
|
+
const keypair = this.privateEncryptionKeyPair;
|
|
3560
|
+
const selfSentMessages = this.privateMessages.filter((message) => message.from_agent_id === this.identity?.agent_id);
|
|
3561
|
+
let cachedPlaintextCount = 0;
|
|
3562
|
+
for (const message of selfSentMessages) {
|
|
3563
|
+
if (this.privateMessageBodyCache.get(message.message_id)) {
|
|
3564
|
+
cachedPlaintextCount += 1;
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
if (!keypair?.public_key || !keypair?.private_key) {
|
|
3568
|
+
warnings.push("missing_private_encryption_keypair");
|
|
3569
|
+
}
|
|
3570
|
+
if (selfSentMessages.length > 0 && cachedPlaintextCount === 0) {
|
|
3571
|
+
warnings.push("missing_local_plaintext_cache_for_self_messages");
|
|
3572
|
+
}
|
|
3573
|
+
if (selfSentMessages.length > 0 && cachedPlaintextCount < selfSentMessages.length) {
|
|
3574
|
+
warnings.push("partial_local_plaintext_cache_for_self_messages");
|
|
3575
|
+
}
|
|
3576
|
+
return {
|
|
3577
|
+
schema_version: 1,
|
|
3578
|
+
app_version: this.appVersion,
|
|
3579
|
+
last_started_at: Date.now(),
|
|
3580
|
+
encryption_public_key: keypair?.public_key || "",
|
|
3581
|
+
encryption_public_key_fingerprint: keypair?.public_key ? this.fingerprintPublicKey(keypair.public_key) : "",
|
|
3582
|
+
message_count: this.privateMessages.length,
|
|
3583
|
+
self_sent_count: selfSentMessages.length,
|
|
3584
|
+
cached_plaintext_count: cachedPlaintextCount,
|
|
3585
|
+
warnings,
|
|
3586
|
+
};
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
private async refreshPrivateMessagingRuntime(): Promise<void> {
|
|
3590
|
+
const runtime = this.buildPrivateMessagingRuntimeState();
|
|
3591
|
+
this.privateMessagingRuntime = runtime;
|
|
3592
|
+
await this.privateMessagingRuntimeRepo.set(runtime);
|
|
3593
|
+
for (const warning of runtime.warnings) {
|
|
3594
|
+
await this.log("warn", `Private messaging startup check: ${warning}`);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
|
|
2811
3598
|
private getOnboardingSummary() {
|
|
2812
3599
|
const summary = this.getIntegrationSummary();
|
|
2813
3600
|
const publicEnabled = Boolean(this.profile?.public_enabled);
|
|
@@ -2980,6 +3767,34 @@ export class LocalNodeService {
|
|
|
2980
3767
|
.trim();
|
|
2981
3768
|
}
|
|
2982
3769
|
|
|
3770
|
+
private buildPrivateConversationId(leftAgentId: string, rightAgentId: string): string {
|
|
3771
|
+
return [String(leftAgentId || "").trim(), String(rightAgentId || "").trim()].sort().join(":");
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
private decryptPrivateMessageBody(message: PrivateMessageRecord): string {
|
|
3775
|
+
const cached = this.privateMessageBodyCache.get(message.message_id);
|
|
3776
|
+
if (typeof cached === "string") {
|
|
3777
|
+
return cached;
|
|
3778
|
+
}
|
|
3779
|
+
if (!this.privateEncryptionKeyPair) {
|
|
3780
|
+
return "[encrypted]";
|
|
3781
|
+
}
|
|
3782
|
+
const decrypted = decryptPrivatePayload({
|
|
3783
|
+
ciphertext: message.ciphertext,
|
|
3784
|
+
nonce: message.nonce,
|
|
3785
|
+
sender_encryption_public_key: message.sender_encryption_public_key,
|
|
3786
|
+
recipient_private_key: this.privateEncryptionKeyPair.private_key,
|
|
3787
|
+
}) || "[encrypted]";
|
|
3788
|
+
this.privateMessageBodyCache.set(message.message_id, decrypted);
|
|
3789
|
+
if (this.privateMessageBodyCache.size > PRIVATE_MESSAGE_HISTORY_LIMIT * 2) {
|
|
3790
|
+
const firstKey = this.privateMessageBodyCache.keys().next().value;
|
|
3791
|
+
if (firstKey) {
|
|
3792
|
+
this.privateMessageBodyCache.delete(firstKey);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
return decrypted;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
2983
3798
|
private normalizeWindowTimestamps(timestamps: number[], windowMs: number, now = Date.now()): number[] {
|
|
2984
3799
|
return timestamps.filter((timestamp) => now - timestamp <= windowMs);
|
|
2985
3800
|
}
|
|
@@ -3005,18 +3820,32 @@ export class LocalNodeService {
|
|
|
3005
3820
|
return this.socialMessages.some((item) => item.message_id === messageId);
|
|
3006
3821
|
}
|
|
3007
3822
|
|
|
3008
|
-
private getReplayableSelfSocialMessages(now = Date.now()): SocialMessageRecord[] {
|
|
3823
|
+
private getReplayableSelfSocialMessages(reason = "manual", now = Date.now()): SocialMessageRecord[] {
|
|
3009
3824
|
const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
|
|
3010
3825
|
if (!this.identity || maxCount === 0) {
|
|
3011
3826
|
return [];
|
|
3012
3827
|
}
|
|
3013
|
-
|
|
3828
|
+
const replayable = this.socialMessages
|
|
3014
3829
|
.filter((item) => (
|
|
3015
3830
|
item.agent_id === this.identity?.agent_id &&
|
|
3016
3831
|
now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS
|
|
3017
3832
|
))
|
|
3018
3833
|
.sort((a, b) => a.created_at - b.created_at)
|
|
3019
3834
|
.slice(-maxCount);
|
|
3835
|
+
if (!replayable.length) {
|
|
3836
|
+
this.lastReplayBroadcastSignature = "";
|
|
3837
|
+
return [];
|
|
3838
|
+
}
|
|
3839
|
+
const signature = replayable.map((item) => item.message_id).join(",");
|
|
3840
|
+
const isIntervalReplay = reason === "interval";
|
|
3841
|
+
const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
|
|
3842
|
+
const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
|
|
3843
|
+
if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
|
|
3844
|
+
return [];
|
|
3845
|
+
}
|
|
3846
|
+
this.lastReplayBroadcastSignature = signature;
|
|
3847
|
+
this.lastReplayBroadcastAt = now;
|
|
3848
|
+
return replayable;
|
|
3020
3849
|
}
|
|
3021
3850
|
|
|
3022
3851
|
private hasRecentDuplicateMessage(agentId: string, body: string, topic: string, now = Date.now()): boolean {
|
|
@@ -3091,6 +3920,190 @@ export class LocalNodeService {
|
|
|
3091
3920
|
await this.persistSocialMessageObservations();
|
|
3092
3921
|
}
|
|
3093
3922
|
|
|
3923
|
+
private async sendPrivateMessageReceipt(message: PrivateMessageRecord, replyPeerId?: string): Promise<void> {
|
|
3924
|
+
if (!this.identity || typeof this.network.sendDirect !== "function" || !replyPeerId) {
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3927
|
+
const receipt = signPrivateMessageReceipt({
|
|
3928
|
+
identity: this.identity,
|
|
3929
|
+
receipt_id: createHash("sha256").update(`${message.message_id}:${this.identity.agent_id}:${Date.now()}`, "utf8").digest("hex"),
|
|
3930
|
+
message_id: message.message_id,
|
|
3931
|
+
conversation_id: message.conversation_id,
|
|
3932
|
+
to_agent_id: message.from_agent_id,
|
|
3933
|
+
status: "received",
|
|
3934
|
+
created_at: Date.now(),
|
|
3935
|
+
});
|
|
3936
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
3937
|
+
try {
|
|
3938
|
+
await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3939
|
+
await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3940
|
+
} catch {
|
|
3941
|
+
await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3942
|
+
}
|
|
3943
|
+
await this.persistPrivateMessageReceipts();
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
private normalizeIncomingPrivateMessage(value: unknown): PrivateMessageRecord | null {
|
|
3947
|
+
if (typeof value !== "object" || value === null) {
|
|
3948
|
+
return null;
|
|
3949
|
+
}
|
|
3950
|
+
const record = value as Partial<PrivateMessageRecord>;
|
|
3951
|
+
const createdAt = Number(record.created_at || 0);
|
|
3952
|
+
const fromAgentId = String(record.from_agent_id || "").trim();
|
|
3953
|
+
const toAgentId = String(record.to_agent_id || "").trim();
|
|
3954
|
+
const conversationId = String(record.conversation_id || "").trim();
|
|
3955
|
+
if (
|
|
3956
|
+
record.type !== PRIVATE_MESSAGE_TOPIC ||
|
|
3957
|
+
!String(record.message_id || "").trim() ||
|
|
3958
|
+
!conversationId ||
|
|
3959
|
+
!fromAgentId ||
|
|
3960
|
+
!toAgentId ||
|
|
3961
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3962
|
+
!String(record.sender_encryption_public_key || "").trim() ||
|
|
3963
|
+
!String(record.recipient_encryption_public_key || "").trim() ||
|
|
3964
|
+
!String(record.ciphertext || "").trim() ||
|
|
3965
|
+
!String(record.nonce || "").trim() ||
|
|
3966
|
+
String(record.cipher_scheme || "") !== "nacl-box-v1" ||
|
|
3967
|
+
!String(record.signature || "").trim() ||
|
|
3968
|
+
!Number.isFinite(createdAt)
|
|
3969
|
+
) {
|
|
3970
|
+
return null;
|
|
3971
|
+
}
|
|
3972
|
+
if (fromAgentId === toAgentId) {
|
|
3973
|
+
return null;
|
|
3974
|
+
}
|
|
3975
|
+
if (conversationId !== this.buildPrivateConversationId(fromAgentId, toAgentId)) {
|
|
3976
|
+
return null;
|
|
3977
|
+
}
|
|
3978
|
+
return {
|
|
3979
|
+
type: PRIVATE_MESSAGE_TOPIC,
|
|
3980
|
+
message_id: String(record.message_id).trim(),
|
|
3981
|
+
conversation_id: conversationId,
|
|
3982
|
+
from_agent_id: fromAgentId,
|
|
3983
|
+
to_agent_id: toAgentId,
|
|
3984
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3985
|
+
sender_encryption_public_key: String(record.sender_encryption_public_key).trim(),
|
|
3986
|
+
recipient_encryption_public_key: String(record.recipient_encryption_public_key).trim(),
|
|
3987
|
+
cipher_scheme: "nacl-box-v1",
|
|
3988
|
+
ciphertext: String(record.ciphertext).trim(),
|
|
3989
|
+
nonce: String(record.nonce).trim(),
|
|
3990
|
+
created_at: createdAt,
|
|
3991
|
+
signature: String(record.signature).trim(),
|
|
3992
|
+
};
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
private normalizePrivateMessages(items: unknown): PrivateMessageRecord[] {
|
|
3996
|
+
if (!Array.isArray(items)) {
|
|
3997
|
+
return [];
|
|
3998
|
+
}
|
|
3999
|
+
const deduped = new Set<string>();
|
|
4000
|
+
return items
|
|
4001
|
+
.map((item) => this.normalizeIncomingPrivateMessage(item))
|
|
4002
|
+
.filter((item): item is PrivateMessageRecord => Boolean(item))
|
|
4003
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
4004
|
+
.filter((item) => {
|
|
4005
|
+
if (deduped.has(item.message_id)) {
|
|
4006
|
+
return false;
|
|
4007
|
+
}
|
|
4008
|
+
deduped.add(item.message_id);
|
|
4009
|
+
return true;
|
|
4010
|
+
})
|
|
4011
|
+
.slice(-PRIVATE_MESSAGE_HISTORY_LIMIT);
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
private normalizeIncomingPrivateMessageReceipt(value: unknown): PrivateMessageReceiptRecord | null {
|
|
4015
|
+
if (typeof value !== "object" || value === null) {
|
|
4016
|
+
return null;
|
|
4017
|
+
}
|
|
4018
|
+
const record = value as Partial<PrivateMessageReceiptRecord>;
|
|
4019
|
+
const createdAt = Number(record.created_at || 0);
|
|
4020
|
+
const status = String(record.status || "").trim();
|
|
4021
|
+
if (
|
|
4022
|
+
record.type !== PRIVATE_MESSAGE_RECEIPT_TOPIC ||
|
|
4023
|
+
!String(record.receipt_id || "").trim() ||
|
|
4024
|
+
!String(record.message_id || "").trim() ||
|
|
4025
|
+
!String(record.conversation_id || "").trim() ||
|
|
4026
|
+
!String(record.from_agent_id || "").trim() ||
|
|
4027
|
+
!String(record.to_agent_id || "").trim() ||
|
|
4028
|
+
!String(record.sender_public_key || "").trim() ||
|
|
4029
|
+
(status !== "received" && status !== "read") ||
|
|
4030
|
+
!String(record.signature || "").trim() ||
|
|
4031
|
+
!Number.isFinite(createdAt)
|
|
4032
|
+
) {
|
|
4033
|
+
return null;
|
|
4034
|
+
}
|
|
4035
|
+
return {
|
|
4036
|
+
type: PRIVATE_MESSAGE_RECEIPT_TOPIC,
|
|
4037
|
+
receipt_id: String(record.receipt_id).trim(),
|
|
4038
|
+
message_id: String(record.message_id).trim(),
|
|
4039
|
+
conversation_id: String(record.conversation_id).trim(),
|
|
4040
|
+
from_agent_id: String(record.from_agent_id).trim(),
|
|
4041
|
+
to_agent_id: String(record.to_agent_id).trim(),
|
|
4042
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
4043
|
+
status: status as "received" | "read",
|
|
4044
|
+
created_at: createdAt,
|
|
4045
|
+
signature: String(record.signature).trim(),
|
|
4046
|
+
};
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
private normalizePrivateMessageReceipts(items: unknown): PrivateMessageReceiptRecord[] {
|
|
4050
|
+
if (!Array.isArray(items)) {
|
|
4051
|
+
return [];
|
|
4052
|
+
}
|
|
4053
|
+
const deduped = new Set<string>();
|
|
4054
|
+
return items
|
|
4055
|
+
.map((item) => this.normalizeIncomingPrivateMessageReceipt(item))
|
|
4056
|
+
.filter((item): item is PrivateMessageReceiptRecord => Boolean(item))
|
|
4057
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
4058
|
+
.filter((item) => {
|
|
4059
|
+
if (deduped.has(item.receipt_id)) {
|
|
4060
|
+
return false;
|
|
4061
|
+
}
|
|
4062
|
+
deduped.add(item.receipt_id);
|
|
4063
|
+
return true;
|
|
4064
|
+
})
|
|
4065
|
+
.slice(-PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT);
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
private hasPrivateMessage(messageId: string): boolean {
|
|
4069
|
+
return this.privateMessages.some((item) => item.message_id === messageId);
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
private ingestPrivateMessage(message: PrivateMessageRecord): void {
|
|
4073
|
+
const existing = this.privateMessages.findIndex((item) => item.message_id === message.message_id);
|
|
4074
|
+
if (existing >= 0) {
|
|
4075
|
+
this.privateMessages[existing] = message;
|
|
4076
|
+
} else {
|
|
4077
|
+
this.privateMessages.push(message);
|
|
4078
|
+
}
|
|
4079
|
+
this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
|
|
4080
|
+
const validIds = new Set(this.privateMessages.map((item) => item.message_id));
|
|
4081
|
+
if (message.from_agent_id !== this.identity?.agent_id) {
|
|
4082
|
+
this.privateMessageBodyCache.delete(message.message_id);
|
|
4083
|
+
}
|
|
4084
|
+
for (const key of Array.from(this.privateMessageBodyCache.keys())) {
|
|
4085
|
+
if (!validIds.has(key)) {
|
|
4086
|
+
this.privateMessageBodyCache.delete(key);
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
for (const key of Array.from(this.privateMessageDeliveryStatusCache.keys())) {
|
|
4090
|
+
if (!validIds.has(key)) {
|
|
4091
|
+
this.privateMessageDeliveryStatusCache.delete(key);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
private ingestPrivateMessageReceipt(receipt: PrivateMessageReceiptRecord): void {
|
|
4097
|
+
const existing = this.privateMessageReceipts.findIndex((item) => item.receipt_id === receipt.receipt_id);
|
|
4098
|
+
if (existing >= 0) {
|
|
4099
|
+
this.privateMessageReceipts[existing] = receipt;
|
|
4100
|
+
} else {
|
|
4101
|
+
this.privateMessageReceipts.push(receipt);
|
|
4102
|
+
}
|
|
4103
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
|
|
4104
|
+
this.privateMessageDeliveryStatusCache.set(receipt.message_id, receipt.status);
|
|
4105
|
+
}
|
|
4106
|
+
|
|
3094
4107
|
private normalizeIncomingSocialMessage(value: unknown): SocialMessageRecord | null {
|
|
3095
4108
|
if (typeof value !== "object" || value === null) {
|
|
3096
4109
|
return null;
|
|
@@ -3369,6 +4382,48 @@ export async function main() {
|
|
|
3369
4382
|
sendOk(res, node.getRuntimePaths());
|
|
3370
4383
|
});
|
|
3371
4384
|
|
|
4385
|
+
app.get("/api/app/update-status", (_req, res) => {
|
|
4386
|
+
sendOk(res, node.getAppUpdateStatus());
|
|
4387
|
+
});
|
|
4388
|
+
|
|
4389
|
+
app.post(
|
|
4390
|
+
"/api/app/update",
|
|
4391
|
+
asyncRoute(async (_req, res) => {
|
|
4392
|
+
const status = node.getAppUpdateStatus();
|
|
4393
|
+
if (!status.update_available || !status.latest_version) {
|
|
4394
|
+
sendOk(
|
|
4395
|
+
res,
|
|
4396
|
+
{
|
|
4397
|
+
started: false,
|
|
4398
|
+
current_version: status.current_version,
|
|
4399
|
+
latest_version: status.latest_version,
|
|
4400
|
+
platform: status.platform,
|
|
4401
|
+
reason: status.check_error || "already_current",
|
|
4402
|
+
},
|
|
4403
|
+
{ message: "Already on the latest version" }
|
|
4404
|
+
);
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4407
|
+
sendOk(
|
|
4408
|
+
res,
|
|
4409
|
+
{
|
|
4410
|
+
started: true,
|
|
4411
|
+
current_version: status.current_version,
|
|
4412
|
+
target_version: status.latest_version,
|
|
4413
|
+
platform: status.platform,
|
|
4414
|
+
},
|
|
4415
|
+
{ message: `Updating to ${status.latest_version}` }
|
|
4416
|
+
);
|
|
4417
|
+
setTimeout(() => {
|
|
4418
|
+
try {
|
|
4419
|
+
node.startAppUpdate();
|
|
4420
|
+
} catch {
|
|
4421
|
+
// best effort after response has been sent
|
|
4422
|
+
}
|
|
4423
|
+
}, 1200);
|
|
4424
|
+
})
|
|
4425
|
+
);
|
|
4426
|
+
|
|
3372
4427
|
app.put(
|
|
3373
4428
|
"/api/profile",
|
|
3374
4429
|
asyncRoute(async (req, res) => {
|
|
@@ -3511,6 +4566,38 @@ export async function main() {
|
|
|
3511
4566
|
sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
|
|
3512
4567
|
});
|
|
3513
4568
|
|
|
4569
|
+
app.get("/api/private/state", (_req, res) => {
|
|
4570
|
+
sendOk(res, node.getPrivateMessagingState());
|
|
4571
|
+
});
|
|
4572
|
+
|
|
4573
|
+
app.get("/api/private/conversations", (_req, res) => {
|
|
4574
|
+
sendOk(res, node.getPrivateConversations());
|
|
4575
|
+
});
|
|
4576
|
+
|
|
4577
|
+
app.get("/api/private/messages", (req, res) => {
|
|
4578
|
+
const conversationId = String(req.query.conversation_id ?? "").trim();
|
|
4579
|
+
const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
|
|
4580
|
+
sendOk(res, node.getPrivateMessages(conversationId, limit));
|
|
4581
|
+
});
|
|
4582
|
+
|
|
4583
|
+
app.post(
|
|
4584
|
+
"/api/private/messages/send",
|
|
4585
|
+
asyncRoute(async (req, res) => {
|
|
4586
|
+
const result = await node.sendPrivateMessage({
|
|
4587
|
+
to_agent_id: String(req.body?.to_agent_id || ""),
|
|
4588
|
+
recipient_encryption_public_key: String(req.body?.recipient_encryption_public_key || ""),
|
|
4589
|
+
body: String(req.body?.body || ""),
|
|
4590
|
+
});
|
|
4591
|
+
sendOk(res, result, {
|
|
4592
|
+
message: result.sent
|
|
4593
|
+
? (result.reason === "direct-sent"
|
|
4594
|
+
? "Private message sent directly"
|
|
4595
|
+
: "Private message sent via encrypted fallback")
|
|
4596
|
+
: `Private message skipped: ${result.reason}`,
|
|
4597
|
+
});
|
|
4598
|
+
})
|
|
4599
|
+
);
|
|
4600
|
+
|
|
3514
4601
|
app.get("/api/openclaw/bridge", (_req, res) => {
|
|
3515
4602
|
sendOk(res, node.getOpenClawBridgeStatus());
|
|
3516
4603
|
});
|