@silicaclaw/cli 2026.3.20-2 → 2026.3.20-20
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 +102 -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 +1018 -91
- 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 +472 -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 +430 -316
- package/apps/local-console/src/server.ts +1164 -89
- 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,6 +223,14 @@ 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 {
|
|
190
235
|
if (existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
|
|
191
236
|
return cwd;
|
|
@@ -441,45 +486,66 @@ function readOpenClawConfiguredGateway(workspaceRoot: string) {
|
|
|
441
486
|
} as const;
|
|
442
487
|
}
|
|
443
488
|
|
|
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);
|
|
489
|
+
function resolveOpenClawStatusCommand(workspaceRoot: string) {
|
|
490
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
491
|
+
if (explicitBin) {
|
|
492
|
+
return { cmd: explicitBin, args: ["status"] } as const;
|
|
493
|
+
}
|
|
454
494
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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));
|
|
495
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
496
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
497
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
498
|
+
const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
|
|
499
|
+
if (sourceEntry) {
|
|
500
|
+
return { cmd: process.execPath, args: [sourceEntry, "status"] } as const;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
504
|
+
if (commandPath) {
|
|
505
|
+
return { cmd: commandPath, args: ["status"] } as const;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
477
510
|
|
|
478
|
-
|
|
479
|
-
const
|
|
511
|
+
function resolveOpenClawGatewayProbeCommand(workspaceRoot: string) {
|
|
512
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
513
|
+
if (explicitBin) {
|
|
514
|
+
return { cmd: explicitBin, args: ["gateway", "probe"] } as const;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
518
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
519
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
520
|
+
const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
|
|
521
|
+
if (sourceEntry) {
|
|
522
|
+
return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] } as const;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
526
|
+
if (commandPath) {
|
|
527
|
+
return { cmd: commandPath, args: ["gateway", "probe"] } as const;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function detectOpenClawRuntime(workspaceRoot: string) {
|
|
534
|
+
const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
|
|
535
|
+
const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
|
|
536
|
+
const statusLooksConfigured = Boolean(
|
|
537
|
+
statusCommand ||
|
|
538
|
+
configuredGateway.config_path ||
|
|
539
|
+
detectOpenClawInstallation(workspaceRoot).detected
|
|
540
|
+
);
|
|
541
|
+
const gatewayProbeCommand = ["lsof", "-nP", `-iTCP:${configuredGateway.gateway_port}`, "-sTCP:LISTEN"];
|
|
542
|
+
const gatewayProbe = spawnSync(gatewayProbeCommand[0], gatewayProbeCommand.slice(1), {
|
|
480
543
|
encoding: "utf8",
|
|
544
|
+
timeout: 1200,
|
|
481
545
|
});
|
|
482
|
-
const
|
|
546
|
+
const gatewayStatusStdout = String(gatewayProbe.stdout || "");
|
|
547
|
+
const gatewayStatusStderr = String(gatewayProbe.stderr || "");
|
|
548
|
+
const gatewayLines = gatewayStatusStdout
|
|
483
549
|
.split("\n")
|
|
484
550
|
.map((line) => line.trim())
|
|
485
551
|
.filter(Boolean);
|
|
@@ -489,14 +555,9 @@ function detectOpenClawRuntime(workspaceRoot: string) {
|
|
|
489
555
|
const parts = line.split(/\s+/);
|
|
490
556
|
const pid = Number(parts[1] || 0);
|
|
491
557
|
const command = parts[0] || "";
|
|
492
|
-
const lowerCommand = command.toLowerCase();
|
|
493
558
|
const endpoint = parts[8] || parts[parts.length - 1] || "";
|
|
494
559
|
const portMatch = endpoint.match(/:(\d+)(?:\s*\(|$)/);
|
|
495
560
|
if (!pid || !command || !portMatch) return null;
|
|
496
|
-
const isOpenClawListener =
|
|
497
|
-
openclawPids.has(pid) ||
|
|
498
|
-
lowerCommand.includes("openclaw");
|
|
499
|
-
if (!isOpenClawListener) return null;
|
|
500
561
|
const port = Number(portMatch[1]);
|
|
501
562
|
if (!Number.isFinite(port) || port <= 0) return null;
|
|
502
563
|
return {
|
|
@@ -507,46 +568,106 @@ function detectOpenClawRuntime(workspaceRoot: string) {
|
|
|
507
568
|
};
|
|
508
569
|
})
|
|
509
570
|
.filter((item): item is { pid: number; ppid: number; port: number; command: string } => Boolean(item));
|
|
571
|
+
const gatewayProbeOk = gatewayListeners.length > 0;
|
|
572
|
+
let processes: Array<{ pid: number; ppid: number; command: string }> = gatewayListeners.map((item) => ({
|
|
573
|
+
pid: item.pid,
|
|
574
|
+
ppid: item.ppid,
|
|
575
|
+
command: item.command,
|
|
576
|
+
}));
|
|
577
|
+
let processResult: ReturnType<typeof spawnSync> | null = null;
|
|
578
|
+
if (!gatewayProbeOk) {
|
|
579
|
+
processResult = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
|
|
580
|
+
encoding: "utf8",
|
|
581
|
+
timeout: 1200,
|
|
582
|
+
});
|
|
583
|
+
const stdout = String(processResult.stdout || "");
|
|
584
|
+
const lines = stdout
|
|
585
|
+
.split("\n")
|
|
586
|
+
.map((line) => line.trim())
|
|
587
|
+
.filter(Boolean);
|
|
588
|
+
processes = lines
|
|
589
|
+
.map((line) => {
|
|
590
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
591
|
+
if (!match) return null;
|
|
592
|
+
const command = match[3] || "";
|
|
593
|
+
const lower = command.toLowerCase();
|
|
594
|
+
const isOpenClaw =
|
|
595
|
+
lower.includes(" openclaw ") ||
|
|
596
|
+
lower.endsWith(" openclaw") ||
|
|
597
|
+
lower.includes("/openclaw ") ||
|
|
598
|
+
lower.includes("openclaw.mjs") ||
|
|
599
|
+
lower.includes("openclaw gateway") ||
|
|
600
|
+
lower.includes("openclaw agent") ||
|
|
601
|
+
lower.includes("openclaw message");
|
|
602
|
+
if (!isOpenClaw) return null;
|
|
603
|
+
return {
|
|
604
|
+
pid: Number(match[1]),
|
|
605
|
+
ppid: Number(match[2]),
|
|
606
|
+
command,
|
|
607
|
+
};
|
|
608
|
+
})
|
|
609
|
+
.filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
|
|
610
|
+
}
|
|
611
|
+
|
|
510
612
|
const preferredListener =
|
|
511
613
|
gatewayListeners.find((item) => item.port === configuredGateway.gateway_port) ||
|
|
512
614
|
gatewayListeners[0] ||
|
|
513
615
|
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;
|
|
616
|
+
const allProcesses = processes.slice(0, 10);
|
|
617
|
+
const gatewayReachable = gatewayProbeOk;
|
|
528
618
|
const detectionNotes = [];
|
|
529
|
-
if (result.status !== 0) detectionNotes.push(String(result.stderr || "ps failed").trim());
|
|
530
619
|
if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
|
|
531
|
-
detectionNotes.push(String(
|
|
620
|
+
detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
|
|
621
|
+
}
|
|
622
|
+
if (processResult && processResult.status !== 0) {
|
|
623
|
+
detectionNotes.push(String(processResult.stderr || "ps failed").trim());
|
|
532
624
|
}
|
|
533
625
|
const gatewayPort = preferredListener?.port || configuredGateway.gateway_port;
|
|
534
626
|
const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
|
|
535
627
|
|
|
536
628
|
return {
|
|
537
|
-
running: allProcesses.length > 0 || gatewayReachable,
|
|
629
|
+
running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
|
|
538
630
|
process_count: allProcesses.length,
|
|
539
631
|
processes: allProcesses.slice(0, 10),
|
|
540
632
|
detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
|
|
541
633
|
gateway_url: gatewayUrl,
|
|
542
634
|
gateway_port: gatewayPort,
|
|
543
635
|
gateway_reachable: gatewayReachable,
|
|
636
|
+
status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
|
|
637
|
+
status_ok: statusLooksConfigured,
|
|
638
|
+
status_summary: statusLooksConfigured
|
|
639
|
+
? configuredGateway.config_path
|
|
640
|
+
? `configured via ${configuredGateway.config_path}`
|
|
641
|
+
: statusCommand
|
|
642
|
+
? `command available: ${[statusCommand.cmd, ...statusCommand.args].join(" ")}`
|
|
643
|
+
: "OpenClaw environment detected"
|
|
644
|
+
: null,
|
|
645
|
+
gateway_probe_command: gatewayProbeCommand.join(" "),
|
|
646
|
+
gateway_probe_ok: gatewayProbeOk,
|
|
647
|
+
gateway_probe_summary: gatewayProbeOk
|
|
648
|
+
? gatewayStatusStdout
|
|
649
|
+
.split("\n")
|
|
650
|
+
.map((line) => line.trim())
|
|
651
|
+
.filter(Boolean)
|
|
652
|
+
.slice(0, 4)
|
|
653
|
+
.join(" | ")
|
|
654
|
+
: null,
|
|
544
655
|
configured_gateway_url: configuredGateway.gateway_url,
|
|
545
656
|
configured_gateway_port: configuredGateway.gateway_port,
|
|
546
657
|
configured_gateway_bind: configuredGateway.gateway_bind,
|
|
547
658
|
configured_gateway_config_path: configuredGateway.config_path,
|
|
548
659
|
detection_mode:
|
|
549
|
-
|
|
660
|
+
gatewayProbeOk
|
|
661
|
+
? (
|
|
662
|
+
processes.length > 0 && gatewayReachable
|
|
663
|
+
? "gateway-probe+process+gateway"
|
|
664
|
+
: gatewayReachable
|
|
665
|
+
? "gateway-probe+gateway"
|
|
666
|
+
: processes.length > 0
|
|
667
|
+
? "gateway-probe+process"
|
|
668
|
+
: "gateway-probe"
|
|
669
|
+
)
|
|
670
|
+
: processes.length > 0 && gatewayReachable
|
|
550
671
|
? "process+gateway"
|
|
551
672
|
: gatewayReachable
|
|
552
673
|
? "gateway"
|
|
@@ -766,6 +887,7 @@ type IntegrationStatusSummary = {
|
|
|
766
887
|
};
|
|
767
888
|
|
|
768
889
|
type SocialMessageView = SocialMessageRecord & {
|
|
890
|
+
avatar_url?: string;
|
|
769
891
|
is_self: boolean;
|
|
770
892
|
online: boolean;
|
|
771
893
|
last_seen_at: number | null;
|
|
@@ -775,6 +897,17 @@ type SocialMessageView = SocialMessageRecord & {
|
|
|
775
897
|
delivery_status: "local-only" | "remote-observed";
|
|
776
898
|
};
|
|
777
899
|
|
|
900
|
+
type PrivateMessageView = {
|
|
901
|
+
message_id: string;
|
|
902
|
+
conversation_id: string;
|
|
903
|
+
from_agent_id: string;
|
|
904
|
+
to_agent_id: string;
|
|
905
|
+
body: string;
|
|
906
|
+
created_at: number;
|
|
907
|
+
is_self: boolean;
|
|
908
|
+
delivery_status: "sent" | "direct-sent" | "fallback-sent" | "received" | "read";
|
|
909
|
+
};
|
|
910
|
+
|
|
778
911
|
type RuntimeMessageGovernance = SocialMessageGovernanceConfig;
|
|
779
912
|
|
|
780
913
|
type OpenClawBridgeStatus = {
|
|
@@ -818,11 +951,17 @@ type OpenClawBridgeStatus = {
|
|
|
818
951
|
gateway_url: string;
|
|
819
952
|
gateway_port: number;
|
|
820
953
|
gateway_reachable: boolean;
|
|
954
|
+
status_command: string | null;
|
|
955
|
+
status_ok: boolean;
|
|
956
|
+
status_summary: string | null;
|
|
957
|
+
gateway_probe_command: string | null;
|
|
958
|
+
gateway_probe_ok: boolean;
|
|
959
|
+
gateway_probe_summary: string | null;
|
|
821
960
|
configured_gateway_url: string;
|
|
822
961
|
configured_gateway_port: number;
|
|
823
962
|
configured_gateway_bind: string | null;
|
|
824
963
|
configured_gateway_config_path: string | null;
|
|
825
|
-
detection_mode: "process" | "gateway" | "process+gateway" | "not_running";
|
|
964
|
+
detection_mode: "gateway-probe" | "gateway-probe+process" | "gateway-probe+gateway" | "gateway-probe+process+gateway" | "process" | "gateway" | "process+gateway" | "not_running";
|
|
826
965
|
};
|
|
827
966
|
skill_learning: {
|
|
828
967
|
available: boolean;
|
|
@@ -899,6 +1038,10 @@ export class LocalNodeService {
|
|
|
899
1038
|
private socialMessageGovernanceRepo: SocialMessageGovernanceRepo;
|
|
900
1039
|
private socialMessageRepo: SocialMessageRepo;
|
|
901
1040
|
private socialMessageObservationRepo: SocialMessageObservationRepo;
|
|
1041
|
+
private privateMessageRepo: PrivateMessageRepo;
|
|
1042
|
+
private privateMessageReceiptRepo: PrivateMessageReceiptRepo;
|
|
1043
|
+
private privateEncryptionKeyRepo: PrivateEncryptionKeyRepo;
|
|
1044
|
+
private privateMessagingRuntimeRepo: PrivateMessagingRuntimeRepo;
|
|
902
1045
|
private socialRuntimeRepo: SocialRuntimeRepo;
|
|
903
1046
|
|
|
904
1047
|
private identity: AgentIdentity | null = null;
|
|
@@ -906,15 +1049,34 @@ export class LocalNodeService {
|
|
|
906
1049
|
private directory: DirectoryState = createEmptyDirectoryState();
|
|
907
1050
|
private socialMessages: SocialMessageRecord[] = [];
|
|
908
1051
|
private socialMessageObservations: SocialMessageObservationRecord[] = [];
|
|
1052
|
+
private privateMessages: PrivateMessageRecord[] = [];
|
|
1053
|
+
private privateMessageReceipts: PrivateMessageReceiptRecord[] = [];
|
|
1054
|
+
private privateEncryptionKeyPair: PrivateEncryptionKeyPair | null = null;
|
|
1055
|
+
private privateMessagingRuntime: PrivateMessagingRuntimeState | null = null;
|
|
1056
|
+
private privatePeerRoutes: Record<string, string> = {};
|
|
1057
|
+
private privatePeerEncryptionKeys: Record<string, string> = {};
|
|
1058
|
+
private privateMessageBodyCache = new Map<string, string>();
|
|
1059
|
+
private privateMessageDeliveryStatusCache = new Map<string, PrivateMessageView["delivery_status"]>();
|
|
909
1060
|
private messageGovernance: RuntimeMessageGovernance;
|
|
1061
|
+
private privateMessagesPersistDirty = false;
|
|
1062
|
+
private privateMessageReceiptsPersistDirty = false;
|
|
1063
|
+
private privateMessagesPersistTimer: NodeJS.Timeout | null = null;
|
|
1064
|
+
private privateMessageReceiptsPersistTimer: NodeJS.Timeout | null = null;
|
|
910
1065
|
|
|
911
1066
|
private receivedCount = 0;
|
|
912
1067
|
private broadcastCount = 0;
|
|
913
1068
|
private lastMessageAt = 0;
|
|
914
1069
|
private lastBroadcastAt = 0;
|
|
1070
|
+
private lastProfileBroadcastAt = 0;
|
|
1071
|
+
private lastProfileBroadcastSignature = "";
|
|
1072
|
+
private lastReplayBroadcastAt = 0;
|
|
1073
|
+
private lastReplayBroadcastSignature = "";
|
|
915
1074
|
private lastBroadcastErrorAt = 0;
|
|
916
1075
|
private lastBroadcastError: string | null = null;
|
|
917
1076
|
private broadcastFailureCount = 0;
|
|
1077
|
+
private consecutiveBroadcastFailures = 0;
|
|
1078
|
+
private lastBroadcastRecoveryAttemptAt = 0;
|
|
1079
|
+
private broadcastRecoveryInFlight = false;
|
|
918
1080
|
private broadcaster: NodeJS.Timeout | null = null;
|
|
919
1081
|
private subscriptionsBound = false;
|
|
920
1082
|
private broadcastEnabled = true;
|
|
@@ -956,6 +1118,8 @@ export class LocalNodeService {
|
|
|
956
1118
|
private networkReconnectTimer: NodeJS.Timeout | null = null;
|
|
957
1119
|
private networkReconnectDelayMs = 5_000;
|
|
958
1120
|
private appVersion = "unknown";
|
|
1121
|
+
private openclawRuntimeCache: { value: ReturnType<typeof detectOpenClawRuntime>; expiresAt: number } | null = null;
|
|
1122
|
+
private openclawBridgeStatusCache: { value: OpenClawBridgeStatus; expiresAt: number } | null = null;
|
|
959
1123
|
|
|
960
1124
|
constructor(options?: { workspaceRoot?: string; projectRoot?: string; storageRoot?: string }) {
|
|
961
1125
|
this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
|
|
@@ -971,6 +1135,10 @@ export class LocalNodeService {
|
|
|
971
1135
|
this.socialMessageGovernanceRepo = new SocialMessageGovernanceRepo(this.storageRoot);
|
|
972
1136
|
this.socialMessageRepo = new SocialMessageRepo(this.storageRoot);
|
|
973
1137
|
this.socialMessageObservationRepo = new SocialMessageObservationRepo(this.storageRoot);
|
|
1138
|
+
this.privateMessageRepo = new PrivateMessageRepo(this.storageRoot);
|
|
1139
|
+
this.privateMessageReceiptRepo = new PrivateMessageReceiptRepo(this.storageRoot);
|
|
1140
|
+
this.privateEncryptionKeyRepo = new PrivateEncryptionKeyRepo(this.storageRoot);
|
|
1141
|
+
this.privateMessagingRuntimeRepo = new PrivateMessagingRuntimeRepo(this.storageRoot);
|
|
974
1142
|
this.socialRuntimeRepo = new SocialRuntimeRepo(this.storageRoot);
|
|
975
1143
|
this.messageGovernance = this.defaultMessageGovernance();
|
|
976
1144
|
|
|
@@ -1001,6 +1169,24 @@ export class LocalNodeService {
|
|
|
1001
1169
|
this.networkPort = resolved.port;
|
|
1002
1170
|
}
|
|
1003
1171
|
|
|
1172
|
+
private getCachedOpenClawRuntime() {
|
|
1173
|
+
const now = Date.now();
|
|
1174
|
+
if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
|
|
1175
|
+
return this.openclawRuntimeCache.value;
|
|
1176
|
+
}
|
|
1177
|
+
const value = detectOpenClawRuntime(this.projectRoot);
|
|
1178
|
+
this.openclawRuntimeCache = {
|
|
1179
|
+
value,
|
|
1180
|
+
expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
|
|
1181
|
+
};
|
|
1182
|
+
return value;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private invalidateOpenClawCaches() {
|
|
1186
|
+
this.openclawRuntimeCache = null;
|
|
1187
|
+
this.openclawBridgeStatusCache = null;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1004
1190
|
async start(): Promise<void> {
|
|
1005
1191
|
await this.hydrateFromDisk();
|
|
1006
1192
|
|
|
@@ -1014,6 +1200,7 @@ export class LocalNodeService {
|
|
|
1014
1200
|
clearInterval(this.broadcaster);
|
|
1015
1201
|
this.broadcaster = null;
|
|
1016
1202
|
}
|
|
1203
|
+
await this.flushPrivatePersistence();
|
|
1017
1204
|
if (this.networkStarted) {
|
|
1018
1205
|
await this.network.stop();
|
|
1019
1206
|
}
|
|
@@ -1034,6 +1221,9 @@ export class LocalNodeService {
|
|
|
1034
1221
|
getOverview() {
|
|
1035
1222
|
const discovered = this.search("");
|
|
1036
1223
|
const onlineCount = discovered.filter((profile) => profile.online).length;
|
|
1224
|
+
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
1225
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1226
|
+
const openclawSkillInstallation = detectOpenClawSkillInstallation();
|
|
1037
1227
|
|
|
1038
1228
|
return {
|
|
1039
1229
|
app_version: this.appVersion,
|
|
@@ -1050,6 +1240,15 @@ export class LocalNodeService {
|
|
|
1050
1240
|
init_state: this.initState,
|
|
1051
1241
|
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
1052
1242
|
onboarding: this.getOnboardingSummary(),
|
|
1243
|
+
openclaw: {
|
|
1244
|
+
detected: openclawInstallation.detected,
|
|
1245
|
+
running: openclawRuntime.running,
|
|
1246
|
+
detection_mode: openclawRuntime.detection_mode,
|
|
1247
|
+
gateway_url: openclawRuntime.gateway_url,
|
|
1248
|
+
gateway_probe_ok: openclawRuntime.gateway_probe_ok,
|
|
1249
|
+
status_ok: openclawRuntime.status_ok,
|
|
1250
|
+
skill_installed: openclawSkillInstallation.installed,
|
|
1251
|
+
},
|
|
1053
1252
|
social: {
|
|
1054
1253
|
found: this.socialFound,
|
|
1055
1254
|
enabled: this.socialConfig.enabled,
|
|
@@ -1197,6 +1396,7 @@ export class LocalNodeService {
|
|
|
1197
1396
|
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
1198
1397
|
const peers: Array<{ status?: string }> = diagnostics?.peers?.items ?? [];
|
|
1199
1398
|
const online = peers.filter((peer: { status?: string }) => peer.status === "online").length;
|
|
1399
|
+
const memory = process.memoryUsage();
|
|
1200
1400
|
|
|
1201
1401
|
return {
|
|
1202
1402
|
adapter: this.adapterMode,
|
|
@@ -1221,6 +1421,23 @@ export class LocalNodeService {
|
|
|
1221
1421
|
adapter_stats: diagnostics?.stats ?? null,
|
|
1222
1422
|
adapter_transport_stats: diagnostics?.transport_stats ?? null,
|
|
1223
1423
|
adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
1424
|
+
runtime_diagnostics: {
|
|
1425
|
+
memory_mib: {
|
|
1426
|
+
rss: formatBytesToMiB(memory.rss),
|
|
1427
|
+
heap_used: formatBytesToMiB(memory.heapUsed),
|
|
1428
|
+
heap_total: formatBytesToMiB(memory.heapTotal),
|
|
1429
|
+
external: formatBytesToMiB(memory.external),
|
|
1430
|
+
},
|
|
1431
|
+
directory: {
|
|
1432
|
+
profile_count: Object.keys(this.directory.profiles).length,
|
|
1433
|
+
presence_count: Object.keys(this.directory.presence).length,
|
|
1434
|
+
index_key_count: Object.keys(this.directory.index).length,
|
|
1435
|
+
},
|
|
1436
|
+
social: {
|
|
1437
|
+
message_count: this.socialMessages.length,
|
|
1438
|
+
observation_count: this.socialMessageObservations.length,
|
|
1439
|
+
},
|
|
1440
|
+
},
|
|
1224
1441
|
adapter_diagnostics_summary: relayCapable || diagnostics
|
|
1225
1442
|
? {
|
|
1226
1443
|
started: this.networkStarted,
|
|
@@ -1337,6 +1554,92 @@ export class LocalNodeService {
|
|
|
1337
1554
|
};
|
|
1338
1555
|
}
|
|
1339
1556
|
|
|
1557
|
+
getAppUpdateStatus() {
|
|
1558
|
+
const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
|
|
1559
|
+
const fallback = {
|
|
1560
|
+
current_version: currentVersion,
|
|
1561
|
+
latest_version: currentVersion,
|
|
1562
|
+
update_available: false,
|
|
1563
|
+
channel: "latest",
|
|
1564
|
+
platform: process.platform,
|
|
1565
|
+
checked_at: Date.now(),
|
|
1566
|
+
can_update: true,
|
|
1567
|
+
check_error: null as string | null,
|
|
1568
|
+
};
|
|
1569
|
+
try {
|
|
1570
|
+
const result = spawnSync("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
|
|
1571
|
+
cwd: this.projectRoot,
|
|
1572
|
+
encoding: "utf8",
|
|
1573
|
+
env: {
|
|
1574
|
+
...process.env,
|
|
1575
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1576
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1577
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1578
|
+
},
|
|
1579
|
+
});
|
|
1580
|
+
if ((result.status ?? 1) !== 0) {
|
|
1581
|
+
return {
|
|
1582
|
+
...fallback,
|
|
1583
|
+
check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}") as { latest?: string };
|
|
1587
|
+
const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
|
|
1588
|
+
return {
|
|
1589
|
+
...fallback,
|
|
1590
|
+
latest_version: latestVersion,
|
|
1591
|
+
update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
|
|
1592
|
+
};
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
return {
|
|
1595
|
+
...fallback,
|
|
1596
|
+
check_error: error instanceof Error ? error.message : String(error),
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
startAppUpdate(): { started: boolean; target_version: string; platform: string; reason?: string } {
|
|
1602
|
+
const status = this.getAppUpdateStatus();
|
|
1603
|
+
if (!status.update_available || !status.latest_version) {
|
|
1604
|
+
return {
|
|
1605
|
+
started: false,
|
|
1606
|
+
target_version: status.latest_version || status.current_version,
|
|
1607
|
+
platform: process.platform,
|
|
1608
|
+
reason: status.check_error || "already_current",
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
const shimPath = userShimPath();
|
|
1612
|
+
const scriptPath = resolve(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
|
|
1613
|
+
const useShim = existsSync(shimPath);
|
|
1614
|
+
if (!useShim && !existsSync(scriptPath)) {
|
|
1615
|
+
return {
|
|
1616
|
+
started: false,
|
|
1617
|
+
target_version: status.latest_version,
|
|
1618
|
+
platform: process.platform,
|
|
1619
|
+
reason: "missing_cli_script",
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
const command = useShim ? shimPath : process.execPath;
|
|
1623
|
+
const args = useShim ? ["update"] : [scriptPath, "update"];
|
|
1624
|
+
const child = spawn(command, args, {
|
|
1625
|
+
cwd: this.projectRoot,
|
|
1626
|
+
detached: true,
|
|
1627
|
+
stdio: "ignore",
|
|
1628
|
+
env: {
|
|
1629
|
+
...process.env,
|
|
1630
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1631
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1632
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1633
|
+
},
|
|
1634
|
+
});
|
|
1635
|
+
child.unref();
|
|
1636
|
+
return {
|
|
1637
|
+
started: true,
|
|
1638
|
+
target_version: status.latest_version,
|
|
1639
|
+
platform: process.platform,
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1340
1643
|
getIntegrationSummary() {
|
|
1341
1644
|
const status = this.getIntegrationStatus();
|
|
1342
1645
|
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
@@ -1635,6 +1938,7 @@ export class LocalNodeService {
|
|
|
1635
1938
|
return {
|
|
1636
1939
|
...message,
|
|
1637
1940
|
display_name: profile?.display_name || message.display_name || "Unnamed",
|
|
1941
|
+
avatar_url: profile?.avatar_url || "",
|
|
1638
1942
|
is_self: message.agent_id === this.identity?.agent_id,
|
|
1639
1943
|
online: isAgentOnline(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
|
|
1640
1944
|
last_seen_at: lastSeenAt || null,
|
|
@@ -1654,10 +1958,161 @@ export class LocalNodeService {
|
|
|
1654
1958
|
};
|
|
1655
1959
|
}
|
|
1656
1960
|
|
|
1961
|
+
getPrivateMessagingState() {
|
|
1962
|
+
return {
|
|
1963
|
+
enabled: Boolean(this.identity && this.privateEncryptionKeyPair),
|
|
1964
|
+
agent_id: this.identity?.agent_id || "",
|
|
1965
|
+
encryption_public_key: this.privateEncryptionKeyPair?.public_key || "",
|
|
1966
|
+
conversation_count: this.getPrivateConversations().length,
|
|
1967
|
+
message_count: this.privateMessages.length,
|
|
1968
|
+
runtime: this.privateMessagingRuntime,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
getPrivateConversations(): Array<{
|
|
1973
|
+
conversation_id: string;
|
|
1974
|
+
peer_agent_id: string;
|
|
1975
|
+
peer_display_name: string;
|
|
1976
|
+
peer_avatar_url: string;
|
|
1977
|
+
peer_public_key: string;
|
|
1978
|
+
last_message_at: number | null;
|
|
1979
|
+
unread_count: number;
|
|
1980
|
+
}> {
|
|
1981
|
+
const conversations = new Map<string, {
|
|
1982
|
+
conversation_id: string;
|
|
1983
|
+
peer_agent_id: string;
|
|
1984
|
+
peer_display_name: string;
|
|
1985
|
+
peer_avatar_url: string;
|
|
1986
|
+
peer_public_key: string;
|
|
1987
|
+
last_message_at: number | null;
|
|
1988
|
+
unread_count: number;
|
|
1989
|
+
}>();
|
|
1990
|
+
for (const message of this.privateMessages) {
|
|
1991
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
1995
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
const peerProfile = this.directory.profiles[peerAgentId];
|
|
1999
|
+
const current = conversations.get(message.conversation_id);
|
|
2000
|
+
const nextLast = Math.max(current?.last_message_at || 0, message.created_at || 0) || null;
|
|
2001
|
+
const learnedPeerKey = this.privatePeerEncryptionKeys[peerAgentId] || "";
|
|
2002
|
+
conversations.set(message.conversation_id, {
|
|
2003
|
+
conversation_id: message.conversation_id,
|
|
2004
|
+
peer_agent_id: peerAgentId,
|
|
2005
|
+
peer_display_name: peerProfile?.display_name || peerAgentId,
|
|
2006
|
+
peer_avatar_url: peerProfile?.avatar_url || "",
|
|
2007
|
+
peer_public_key: learnedPeerKey || peerProfile?.private_encryption_public_key || "",
|
|
2008
|
+
last_message_at: nextLast,
|
|
2009
|
+
unread_count: current?.unread_count || 0,
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
getPrivateMessages(conversationId: string, limit = PRIVATE_MESSAGE_QUERY_LIMIT): PrivateMessageView[] {
|
|
2016
|
+
const normalizedConversationId = String(conversationId || "").trim();
|
|
2017
|
+
const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
|
|
2018
|
+
const receiptsByMessageId = new Map(
|
|
2019
|
+
this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status] as const)
|
|
2020
|
+
);
|
|
2021
|
+
return this.privateMessages
|
|
2022
|
+
.filter((message) => {
|
|
2023
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
2024
|
+
return false;
|
|
2025
|
+
}
|
|
2026
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
2027
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
2028
|
+
return false;
|
|
2029
|
+
}
|
|
2030
|
+
return !normalizedConversationId || message.conversation_id === normalizedConversationId;
|
|
2031
|
+
})
|
|
2032
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
2033
|
+
.slice(0, resolvedLimit)
|
|
2034
|
+
.map((message) => ({
|
|
2035
|
+
message_id: message.message_id,
|
|
2036
|
+
conversation_id: message.conversation_id,
|
|
2037
|
+
from_agent_id: message.from_agent_id,
|
|
2038
|
+
to_agent_id: message.to_agent_id,
|
|
2039
|
+
body: this.decryptPrivateMessageBody(message),
|
|
2040
|
+
created_at: message.created_at,
|
|
2041
|
+
is_self: message.from_agent_id === this.identity?.agent_id,
|
|
2042
|
+
delivery_status:
|
|
2043
|
+
receiptsByMessageId.get(message.message_id) ||
|
|
2044
|
+
this.privateMessageDeliveryStatusCache.get(message.message_id) ||
|
|
2045
|
+
(message.from_agent_id === this.identity?.agent_id ? "fallback-sent" : "sent"),
|
|
2046
|
+
}));
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
async sendPrivateMessage(input: {
|
|
2050
|
+
to_agent_id: string;
|
|
2051
|
+
recipient_encryption_public_key: string;
|
|
2052
|
+
body: string;
|
|
2053
|
+
}): Promise<{ sent: boolean; reason: string; message?: PrivateMessageView }> {
|
|
2054
|
+
if (!this.identity || !this.privateEncryptionKeyPair) {
|
|
2055
|
+
return { sent: false, reason: "missing_identity_or_private_key" };
|
|
2056
|
+
}
|
|
2057
|
+
const toAgentId = String(input.to_agent_id || "").trim();
|
|
2058
|
+
const learnedRecipientKey = this.privatePeerEncryptionKeys[toAgentId] || "";
|
|
2059
|
+
const profileRecipientKey = this.directory.profiles[toAgentId]?.private_encryption_public_key || "";
|
|
2060
|
+
const recipientKey = String(learnedRecipientKey || input.recipient_encryption_public_key || profileRecipientKey || "").trim();
|
|
2061
|
+
const body = String(input.body || "").trim();
|
|
2062
|
+
if (toAgentId === this.identity.agent_id) {
|
|
2063
|
+
return { sent: false, reason: "self_private_message_not_allowed" };
|
|
2064
|
+
}
|
|
2065
|
+
const toPeerId = this.privatePeerRoutes[toAgentId] || "";
|
|
2066
|
+
if (!toAgentId || !recipientKey || !body) {
|
|
2067
|
+
return { sent: false, reason: "invalid_private_message_input" };
|
|
2068
|
+
}
|
|
2069
|
+
const encrypted = encryptPrivatePayload({
|
|
2070
|
+
plaintext: body,
|
|
2071
|
+
recipient_public_key: recipientKey,
|
|
2072
|
+
sender_keypair: this.privateEncryptionKeyPair,
|
|
2073
|
+
});
|
|
2074
|
+
const message = signPrivateMessage({
|
|
2075
|
+
identity: this.identity,
|
|
2076
|
+
message_id: createHash("sha256").update(`${this.identity.agent_id}:${toAgentId}:${Date.now()}:${body}:${Math.random()}`, "utf8").digest("hex"),
|
|
2077
|
+
conversation_id: this.buildPrivateConversationId(this.identity.agent_id, toAgentId),
|
|
2078
|
+
to_agent_id: toAgentId,
|
|
2079
|
+
sender_encryption_public_key: encrypted.sender_encryption_public_key,
|
|
2080
|
+
recipient_encryption_public_key: recipientKey,
|
|
2081
|
+
ciphertext: encrypted.ciphertext,
|
|
2082
|
+
nonce: encrypted.nonce,
|
|
2083
|
+
created_at: Date.now(),
|
|
2084
|
+
});
|
|
2085
|
+
this.privateMessageBodyCache.set(message.message_id, body);
|
|
2086
|
+
this.ingestPrivateMessage(message);
|
|
2087
|
+
await this.persistPrivateMessages();
|
|
2088
|
+
let reason = "fallback-sent";
|
|
2089
|
+
if (toPeerId && typeof this.network.sendDirect === "function") {
|
|
2090
|
+
try {
|
|
2091
|
+
await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
|
|
2092
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
2093
|
+
reason = "direct-sent";
|
|
2094
|
+
} catch {
|
|
2095
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
2096
|
+
}
|
|
2097
|
+
} else {
|
|
2098
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
2099
|
+
}
|
|
2100
|
+
this.privateMessageDeliveryStatusCache.set(message.message_id, reason as PrivateMessageView["delivery_status"]);
|
|
2101
|
+
const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
|
|
2102
|
+
if (view) {
|
|
2103
|
+
view.delivery_status = reason as PrivateMessageView["delivery_status"];
|
|
2104
|
+
}
|
|
2105
|
+
return { sent: true, reason, message: view };
|
|
2106
|
+
}
|
|
2107
|
+
|
|
1657
2108
|
getOpenClawBridgeStatus(): OpenClawBridgeStatus {
|
|
2109
|
+
const now = Date.now();
|
|
2110
|
+
if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
|
|
2111
|
+
return this.openclawBridgeStatusCache.value;
|
|
2112
|
+
}
|
|
1658
2113
|
const integration = this.getIntegrationStatus();
|
|
1659
2114
|
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
1660
|
-
const openclawRuntime =
|
|
2115
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1661
2116
|
const skillInstallation = detectOpenClawSkillInstallation();
|
|
1662
2117
|
const ownerDelivery = detectOwnerDeliveryStatus({
|
|
1663
2118
|
workspaceRoot: this.projectRoot,
|
|
@@ -1665,7 +2120,7 @@ export class LocalNodeService {
|
|
|
1665
2120
|
openclawRunning: openclawRuntime.running,
|
|
1666
2121
|
skillInstalled: skillInstallation.installed,
|
|
1667
2122
|
});
|
|
1668
|
-
|
|
2123
|
+
const value: OpenClawBridgeStatus = {
|
|
1669
2124
|
enabled: this.socialConfig.enabled,
|
|
1670
2125
|
connected_to_silicaclaw: integration.connected_to_silicaclaw,
|
|
1671
2126
|
public_enabled: integration.public_enabled,
|
|
@@ -1721,6 +2176,11 @@ export class LocalNodeService {
|
|
|
1721
2176
|
install_skill: "/api/openclaw/bridge/skill-install",
|
|
1722
2177
|
},
|
|
1723
2178
|
};
|
|
2179
|
+
this.openclawBridgeStatusCache = {
|
|
2180
|
+
value,
|
|
2181
|
+
expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
|
|
2182
|
+
};
|
|
2183
|
+
return value;
|
|
1724
2184
|
}
|
|
1725
2185
|
|
|
1726
2186
|
async installOpenClawSkill(skillName?: string) {
|
|
@@ -1735,6 +2195,7 @@ export class LocalNodeService {
|
|
|
1735
2195
|
maxBuffer: 1024 * 1024,
|
|
1736
2196
|
});
|
|
1737
2197
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
2198
|
+
this.invalidateOpenClawCaches();
|
|
1738
2199
|
return {
|
|
1739
2200
|
...parsed,
|
|
1740
2201
|
bridge: this.getOpenClawBridgeStatus(),
|
|
@@ -1756,7 +2217,7 @@ export class LocalNodeService {
|
|
|
1756
2217
|
const workspaceSkillDir = resolve(homeDir, "workspace", "skills");
|
|
1757
2218
|
const legacySkillDir = resolve(homeDir, "skills");
|
|
1758
2219
|
const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
|
|
1759
|
-
const openclawRuntime =
|
|
2220
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1760
2221
|
|
|
1761
2222
|
return {
|
|
1762
2223
|
bridge_api_base: DEFAULT_BRIDGE_API_BASE,
|
|
@@ -2166,15 +2627,14 @@ export class LocalNodeService {
|
|
|
2166
2627
|
profile: this.profile,
|
|
2167
2628
|
};
|
|
2168
2629
|
const presenceRecord = signPresence(this.identity, Date.now());
|
|
2169
|
-
const
|
|
2170
|
-
const replayMessages = this.getReplayableSelfSocialMessages();
|
|
2630
|
+
const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
|
|
2631
|
+
const replayMessages = this.getReplayableSelfSocialMessages(reason);
|
|
2171
2632
|
|
|
2172
2633
|
try {
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
for (const record of indexRecords) {
|
|
2176
|
-
await this.publish("index", record);
|
|
2634
|
+
if (shouldPublishProfile) {
|
|
2635
|
+
await this.publish("profile", profileRecord);
|
|
2177
2636
|
}
|
|
2637
|
+
await this.publish("presence", presenceRecord);
|
|
2178
2638
|
for (const message of replayMessages) {
|
|
2179
2639
|
await this.publish(SOCIAL_MESSAGE_TOPIC, message);
|
|
2180
2640
|
}
|
|
@@ -2183,7 +2643,9 @@ export class LocalNodeService {
|
|
|
2183
2643
|
this.lastBroadcastErrorAt = Date.now();
|
|
2184
2644
|
this.lastBroadcastError = message;
|
|
2185
2645
|
this.broadcastFailureCount += 1;
|
|
2646
|
+
this.consecutiveBroadcastFailures += 1;
|
|
2186
2647
|
await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
|
|
2648
|
+
await this.maybeRecoverFromBroadcastFailure(reason, message);
|
|
2187
2649
|
return { sent: false, reason: "publish_failed", error: message };
|
|
2188
2650
|
}
|
|
2189
2651
|
|
|
@@ -2191,22 +2653,75 @@ export class LocalNodeService {
|
|
|
2191
2653
|
this.broadcastCount += 1;
|
|
2192
2654
|
this.lastBroadcastError = null;
|
|
2193
2655
|
this.lastBroadcastErrorAt = 0;
|
|
2656
|
+
this.consecutiveBroadcastFailures = 0;
|
|
2194
2657
|
|
|
2195
2658
|
this.directory = ingestProfileRecord(this.directory, profileRecord);
|
|
2196
2659
|
this.directory = ingestPresenceRecord(this.directory, presenceRecord);
|
|
2197
|
-
for (const record of indexRecords) {
|
|
2198
|
-
this.directory = ingestIndexRecord(this.directory, record);
|
|
2199
|
-
}
|
|
2200
2660
|
this.compactCacheInMemory();
|
|
2201
2661
|
await this.persistCache();
|
|
2202
2662
|
|
|
2203
2663
|
await this.log(
|
|
2204
2664
|
"info",
|
|
2205
|
-
`Broadcast sent (${
|
|
2665
|
+
`Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`
|
|
2206
2666
|
);
|
|
2207
2667
|
return { sent: true, reason };
|
|
2208
2668
|
}
|
|
2209
2669
|
|
|
2670
|
+
private shouldPublishProfileRecord(
|
|
2671
|
+
profileRecord: SignedProfileRecord,
|
|
2672
|
+
reason: string,
|
|
2673
|
+
now = Date.now()
|
|
2674
|
+
): boolean {
|
|
2675
|
+
if (reason !== "interval") {
|
|
2676
|
+
this.lastProfileBroadcastSignature = profileRecord.profile.signature;
|
|
2677
|
+
this.lastProfileBroadcastAt = now;
|
|
2678
|
+
return true;
|
|
2679
|
+
}
|
|
2680
|
+
const signature = profileRecord.profile.signature;
|
|
2681
|
+
const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
|
|
2682
|
+
const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
|
|
2683
|
+
if (!changedSinceLastPublish && !refreshDue) {
|
|
2684
|
+
return false;
|
|
2685
|
+
}
|
|
2686
|
+
this.lastProfileBroadcastSignature = signature;
|
|
2687
|
+
this.lastProfileBroadcastAt = now;
|
|
2688
|
+
return true;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
private async maybeRecoverFromBroadcastFailure(reason: string, errorMessage: string): Promise<void> {
|
|
2692
|
+
const recoveryThreshold = 3;
|
|
2693
|
+
const recoveryCooldownMs = 60_000;
|
|
2694
|
+
if (this.broadcastRecoveryInFlight) {
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (this.consecutiveBroadcastFailures < recoveryThreshold) {
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
if (Date.now() - this.lastBroadcastRecoveryAttemptAt < recoveryCooldownMs) {
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
if (this.adapterMode !== "relay-preview" && this.adapterMode !== "webrtc-preview" && this.adapterMode !== "real-preview") {
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
this.broadcastRecoveryInFlight = true;
|
|
2708
|
+
this.lastBroadcastRecoveryAttemptAt = Date.now();
|
|
2709
|
+
try {
|
|
2710
|
+
await this.log(
|
|
2711
|
+
"warn",
|
|
2712
|
+
`Broadcast recovery triggered after ${this.consecutiveBroadcastFailures} consecutive failures (${reason}): ${errorMessage}`
|
|
2713
|
+
);
|
|
2714
|
+
await this.restartNetworkAdapter("broadcast_failure_recovery");
|
|
2715
|
+
} catch (recoveryError) {
|
|
2716
|
+
await this.log(
|
|
2717
|
+
"error",
|
|
2718
|
+
`Broadcast recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
|
|
2719
|
+
);
|
|
2720
|
+
} finally {
|
|
2721
|
+
this.broadcastRecoveryInFlight = false;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2210
2725
|
private async hydrateFromDisk(): Promise<void> {
|
|
2211
2726
|
this.initState = {
|
|
2212
2727
|
identity_auto_created: false,
|
|
@@ -2236,6 +2751,8 @@ export class LocalNodeService {
|
|
|
2236
2751
|
await this.log("info", `Bound existing OpenClaw identity: ${resolvedIdentity.openclaw_source_path}`);
|
|
2237
2752
|
}
|
|
2238
2753
|
await this.identityRepo.set(this.identity);
|
|
2754
|
+
this.privateEncryptionKeyPair = (await this.privateEncryptionKeyRepo.get()) || createPrivateEncryptionKeyPair();
|
|
2755
|
+
await this.privateEncryptionKeyRepo.set(this.privateEncryptionKeyPair);
|
|
2239
2756
|
|
|
2240
2757
|
const existingProfile = await this.profileRepo.get();
|
|
2241
2758
|
const profileInput = resolveProfileInputWithSocial({
|
|
@@ -2244,7 +2761,10 @@ export class LocalNodeService {
|
|
|
2244
2761
|
existingProfile: existingProfile && existingProfile.agent_id === this.identity.agent_id ? existingProfile : null,
|
|
2245
2762
|
rootDir: this.projectRoot,
|
|
2246
2763
|
});
|
|
2247
|
-
this.profile = signProfile(
|
|
2764
|
+
this.profile = signProfile({
|
|
2765
|
+
...profileInput,
|
|
2766
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || profileInput.private_encryption_public_key || "",
|
|
2767
|
+
}, this.identity);
|
|
2248
2768
|
if (!existingProfile || existingProfile.agent_id !== this.identity.agent_id) {
|
|
2249
2769
|
this.initState.profile_auto_created = true;
|
|
2250
2770
|
await this.log("info", "profile.json missing/invalid, initialized from social/default profile");
|
|
@@ -2258,6 +2778,11 @@ export class LocalNodeService {
|
|
|
2258
2778
|
};
|
|
2259
2779
|
this.socialMessages = this.normalizeSocialMessages(await this.socialMessageRepo.get());
|
|
2260
2780
|
this.socialMessageObservations = this.normalizeSocialMessageObservations(await this.socialMessageObservationRepo.get());
|
|
2781
|
+
const storedPrivateMessages = await this.privateMessageRepo.get();
|
|
2782
|
+
this.hydratePrivateMessageBodyCache(storedPrivateMessages);
|
|
2783
|
+
this.privateMessages = this.normalizePrivateMessages(storedPrivateMessages);
|
|
2784
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(await this.privateMessageReceiptRepo.get());
|
|
2785
|
+
await this.refreshPrivateMessagingRuntime();
|
|
2261
2786
|
this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: this.profile });
|
|
2262
2787
|
this.compactCacheInMemory();
|
|
2263
2788
|
await this.persistCache();
|
|
@@ -2276,7 +2801,10 @@ export class LocalNodeService {
|
|
|
2276
2801
|
existingProfile: this.profile,
|
|
2277
2802
|
rootDir: this.projectRoot,
|
|
2278
2803
|
});
|
|
2279
|
-
const nextProfile = signProfile(
|
|
2804
|
+
const nextProfile = signProfile({
|
|
2805
|
+
...nextProfileInput,
|
|
2806
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || nextProfileInput.private_encryption_public_key || "",
|
|
2807
|
+
}, this.identity);
|
|
2280
2808
|
this.profile = nextProfile;
|
|
2281
2809
|
await this.profileRepo.set(nextProfile);
|
|
2282
2810
|
|
|
@@ -2341,7 +2869,8 @@ export class LocalNodeService {
|
|
|
2341
2869
|
|
|
2342
2870
|
private async onMessage(
|
|
2343
2871
|
topic: "profile" | "presence" | "index" | "social.message" | "social.message.observation",
|
|
2344
|
-
data: unknown
|
|
2872
|
+
data: unknown,
|
|
2873
|
+
meta?: { peerId?: string }
|
|
2345
2874
|
): Promise<void> {
|
|
2346
2875
|
this.receivedCount += 1;
|
|
2347
2876
|
this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
|
|
@@ -2359,6 +2888,9 @@ export class LocalNodeService {
|
|
|
2359
2888
|
return;
|
|
2360
2889
|
}
|
|
2361
2890
|
}
|
|
2891
|
+
if (meta?.peerId && record.profile.agent_id && !this.privatePeerRoutes[record.profile.agent_id]) {
|
|
2892
|
+
this.privatePeerRoutes[record.profile.agent_id] = meta.peerId;
|
|
2893
|
+
}
|
|
2362
2894
|
|
|
2363
2895
|
this.directory = ingestProfileRecord(this.directory, record);
|
|
2364
2896
|
this.compactCacheInMemory();
|
|
@@ -2378,6 +2910,9 @@ export class LocalNodeService {
|
|
|
2378
2910
|
return;
|
|
2379
2911
|
}
|
|
2380
2912
|
}
|
|
2913
|
+
if (meta?.peerId && record.agent_id && !this.privatePeerRoutes[record.agent_id]) {
|
|
2914
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2915
|
+
}
|
|
2381
2916
|
|
|
2382
2917
|
this.directory = ingestPresenceRecord(this.directory, record);
|
|
2383
2918
|
this.compactCacheInMemory();
|
|
@@ -2394,6 +2929,9 @@ export class LocalNodeService {
|
|
|
2394
2929
|
await this.log("warn", `Rejected social message with invalid signature (${record.message_id.slice(0, 10)})`);
|
|
2395
2930
|
return;
|
|
2396
2931
|
}
|
|
2932
|
+
if (meta?.peerId && record.agent_id && !this.privatePeerRoutes[record.agent_id]) {
|
|
2933
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2934
|
+
}
|
|
2397
2935
|
if (this.hasSocialMessage(record.message_id)) {
|
|
2398
2936
|
await this.publishObservationForMessage(record);
|
|
2399
2937
|
return;
|
|
@@ -2432,6 +2970,45 @@ export class LocalNodeService {
|
|
|
2432
2970
|
await this.persistCache();
|
|
2433
2971
|
}
|
|
2434
2972
|
|
|
2973
|
+
private async onDirectMessage(
|
|
2974
|
+
topic: "private.message" | "private.message.receipt",
|
|
2975
|
+
data: unknown,
|
|
2976
|
+
meta?: { peerId?: string }
|
|
2977
|
+
): Promise<void> {
|
|
2978
|
+
if (topic === PRIVATE_MESSAGE_TOPIC) {
|
|
2979
|
+
const record = this.normalizeIncomingPrivateMessage(data);
|
|
2980
|
+
if (!record || !verifyPrivateMessage(record)) {
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
if (meta?.peerId && record.from_agent_id) {
|
|
2984
|
+
this.privatePeerRoutes[record.from_agent_id] = meta.peerId;
|
|
2985
|
+
}
|
|
2986
|
+
if (record.from_agent_id && record.sender_encryption_public_key) {
|
|
2987
|
+
this.privatePeerEncryptionKeys[record.from_agent_id] = record.sender_encryption_public_key;
|
|
2988
|
+
}
|
|
2989
|
+
if (record.to_agent_id !== this.identity?.agent_id || this.hasPrivateMessage(record.message_id)) {
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
this.ingestPrivateMessage(record);
|
|
2993
|
+
await this.persistPrivateMessages();
|
|
2994
|
+
await this.sendPrivateMessageReceipt(record, meta?.peerId);
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
const receipt = this.normalizeIncomingPrivateMessageReceipt(data);
|
|
2999
|
+
if (!receipt || !verifyPrivateMessageReceipt(receipt)) {
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
if (meta?.peerId && receipt.from_agent_id) {
|
|
3003
|
+
this.privatePeerRoutes[receipt.from_agent_id] = meta.peerId;
|
|
3004
|
+
}
|
|
3005
|
+
if (receipt.to_agent_id !== this.identity?.agent_id) {
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
3009
|
+
await this.persistPrivateMessageReceipts();
|
|
3010
|
+
}
|
|
3011
|
+
|
|
2435
3012
|
private startBroadcastLoop(): void {
|
|
2436
3013
|
if (this.broadcaster) {
|
|
2437
3014
|
clearInterval(this.broadcaster);
|
|
@@ -2457,21 +3034,35 @@ export class LocalNodeService {
|
|
|
2457
3034
|
if (this.subscriptionsBound) {
|
|
2458
3035
|
return;
|
|
2459
3036
|
}
|
|
2460
|
-
this.network.subscribe("profile", (data: SignedProfileRecord) => {
|
|
2461
|
-
this.onMessage("profile", data);
|
|
3037
|
+
this.network.subscribe("profile", (data: SignedProfileRecord, meta?: { peerId?: string }) => {
|
|
3038
|
+
this.onMessage("profile", data, meta);
|
|
3039
|
+
});
|
|
3040
|
+
this.network.subscribe("presence", (data: PresenceRecord, meta?: { peerId?: string }) => {
|
|
3041
|
+
this.onMessage("presence", data, meta);
|
|
3042
|
+
});
|
|
3043
|
+
this.network.subscribe("index", (data: IndexRefRecord, meta?: { peerId?: string }) => {
|
|
3044
|
+
this.onMessage("index", data, meta);
|
|
2462
3045
|
});
|
|
2463
|
-
this.network.subscribe(
|
|
2464
|
-
this.onMessage(
|
|
3046
|
+
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data: SocialMessageRecord, meta?: { peerId?: string }) => {
|
|
3047
|
+
this.onMessage(SOCIAL_MESSAGE_TOPIC, data, meta);
|
|
2465
3048
|
});
|
|
2466
|
-
this.network.subscribe(
|
|
2467
|
-
this.onMessage(
|
|
3049
|
+
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord, meta?: { peerId?: string }) => {
|
|
3050
|
+
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
|
|
2468
3051
|
});
|
|
2469
|
-
this.network.subscribe(
|
|
2470
|
-
this.
|
|
3052
|
+
this.network.subscribe(PRIVATE_MESSAGE_TOPIC, (data: PrivateMessageRecord, meta?: { peerId?: string }) => {
|
|
3053
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
2471
3054
|
});
|
|
2472
|
-
this.network.subscribe(
|
|
2473
|
-
this.
|
|
3055
|
+
this.network.subscribe(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data: PrivateMessageReceiptRecord, meta?: { peerId?: string }) => {
|
|
3056
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
2474
3057
|
});
|
|
3058
|
+
if (typeof this.network.subscribeDirect === "function") {
|
|
3059
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data: PrivateMessageRecord, meta?: { peerId?: string }) => {
|
|
3060
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
3061
|
+
});
|
|
3062
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data: PrivateMessageReceiptRecord, meta?: { peerId?: string }) => {
|
|
3063
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
2475
3066
|
this.subscriptionsBound = true;
|
|
2476
3067
|
}
|
|
2477
3068
|
|
|
@@ -2628,9 +3219,66 @@ export class LocalNodeService {
|
|
|
2628
3219
|
this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
|
|
2629
3220
|
}
|
|
2630
3221
|
|
|
3222
|
+
private pruneRemoteProfilesInMemory(now = Date.now()): number {
|
|
3223
|
+
if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
|
|
3224
|
+
return 0;
|
|
3225
|
+
}
|
|
3226
|
+
const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
|
|
3227
|
+
const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
|
|
3228
|
+
if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
|
|
3229
|
+
return 0;
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
const onlineRemoteProfiles = remoteProfiles.filter((profile) =>
|
|
3233
|
+
isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS)
|
|
3234
|
+
);
|
|
3235
|
+
const offlineRemoteProfiles = remoteProfiles
|
|
3236
|
+
.filter((profile) => !isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
|
|
3237
|
+
.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
|
|
3238
|
+
|
|
3239
|
+
const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
|
|
3240
|
+
const keptRemoteProfiles = [
|
|
3241
|
+
...onlineRemoteProfiles,
|
|
3242
|
+
...offlineRemoteProfiles.slice(0, keepOfflineCount),
|
|
3243
|
+
];
|
|
3244
|
+
const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
|
|
3245
|
+
const removedIds = remoteProfiles
|
|
3246
|
+
.map((profile) => profile.agent_id)
|
|
3247
|
+
.filter((agentId) => !keptRemoteIds.has(agentId));
|
|
3248
|
+
if (removedIds.length === 0) {
|
|
3249
|
+
return 0;
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
const next = createEmptyDirectoryState();
|
|
3253
|
+
const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
|
|
3254
|
+
if (selfProfile) {
|
|
3255
|
+
next.profiles[selfAgentId] = selfProfile;
|
|
3256
|
+
const selfPresence = this.directory.presence[selfAgentId];
|
|
3257
|
+
if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
|
|
3258
|
+
next.presence[selfAgentId] = selfPresence;
|
|
3259
|
+
}
|
|
3260
|
+
const rebuilt = rebuildIndexForProfile(next, selfProfile);
|
|
3261
|
+
next.index = rebuilt.index;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
for (const profile of keptRemoteProfiles) {
|
|
3265
|
+
next.profiles[profile.agent_id] = profile;
|
|
3266
|
+
const seenAt = this.directory.presence[profile.agent_id];
|
|
3267
|
+
if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
|
|
3268
|
+
next.presence[profile.agent_id] = seenAt;
|
|
3269
|
+
}
|
|
3270
|
+
const rebuilt = rebuildIndexForProfile(next, profile);
|
|
3271
|
+
next.index = rebuilt.index;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
this.directory = dedupeIndex(next);
|
|
3275
|
+
return removedIds.length;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
2631
3278
|
private compactCacheInMemory(): number {
|
|
2632
3279
|
const cleaned = cleanupExpiredPresence(this.directory, Date.now(), PRESENCE_TTL_MS);
|
|
2633
3280
|
this.directory = dedupeIndex(cleaned.state);
|
|
3281
|
+
this.pruneRemoteProfilesInMemory();
|
|
2634
3282
|
return cleaned.removed;
|
|
2635
3283
|
}
|
|
2636
3284
|
|
|
@@ -2666,6 +3314,90 @@ export class LocalNodeService {
|
|
|
2666
3314
|
await this.socialMessageObservationRepo.set(this.socialMessageObservations);
|
|
2667
3315
|
}
|
|
2668
3316
|
|
|
3317
|
+
private async persistPrivateMessages(): Promise<void> {
|
|
3318
|
+
this.privateMessagesPersistDirty = true;
|
|
3319
|
+
if (this.privateMessagesPersistTimer) {
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
this.privateMessagesPersistTimer = setTimeout(() => {
|
|
3323
|
+
this.flushPrivateMessagesPersist().catch(() => {});
|
|
3324
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
private async persistPrivateMessageReceipts(): Promise<void> {
|
|
3328
|
+
this.privateMessageReceiptsPersistDirty = true;
|
|
3329
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
this.privateMessageReceiptsPersistTimer = setTimeout(() => {
|
|
3333
|
+
this.flushPrivateMessageReceiptsPersist().catch(() => {});
|
|
3334
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
private async flushPrivatePersistence(): Promise<void> {
|
|
3338
|
+
await Promise.all([
|
|
3339
|
+
this.flushPrivateMessagesPersist(),
|
|
3340
|
+
this.flushPrivateMessageReceiptsPersist(),
|
|
3341
|
+
]);
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
private async flushPrivateMessagesPersist(): Promise<void> {
|
|
3345
|
+
if (this.privateMessagesPersistTimer) {
|
|
3346
|
+
clearTimeout(this.privateMessagesPersistTimer);
|
|
3347
|
+
this.privateMessagesPersistTimer = null;
|
|
3348
|
+
}
|
|
3349
|
+
if (!this.privateMessagesPersistDirty) {
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
this.privateMessagesPersistDirty = false;
|
|
3353
|
+
await this.privateMessageRepo.set(this.buildPersistedPrivateMessages() as unknown as PrivateMessageRecord[]);
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
private hydratePrivateMessageBodyCache(items: unknown): void {
|
|
3357
|
+
if (!Array.isArray(items)) {
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
for (const item of items) {
|
|
3361
|
+
if (typeof item !== "object" || item === null) {
|
|
3362
|
+
continue;
|
|
3363
|
+
}
|
|
3364
|
+
const record = item as Partial<StoredPrivateMessageRecord>;
|
|
3365
|
+
const messageId = String(record.message_id || "").trim();
|
|
3366
|
+
const localPlaintext = typeof record.local_plaintext === "string" ? record.local_plaintext : "";
|
|
3367
|
+
if (messageId && localPlaintext) {
|
|
3368
|
+
this.privateMessageBodyCache.set(messageId, localPlaintext);
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
private buildPersistedPrivateMessages(): StoredPrivateMessageRecord[] {
|
|
3374
|
+
return this.privateMessages.map((message) => {
|
|
3375
|
+
const localPlaintext =
|
|
3376
|
+
message.from_agent_id === this.identity?.agent_id
|
|
3377
|
+
? this.privateMessageBodyCache.get(message.message_id) || ""
|
|
3378
|
+
: "";
|
|
3379
|
+
if (!localPlaintext) {
|
|
3380
|
+
return { ...message };
|
|
3381
|
+
}
|
|
3382
|
+
return {
|
|
3383
|
+
...message,
|
|
3384
|
+
local_plaintext: localPlaintext,
|
|
3385
|
+
};
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
private async flushPrivateMessageReceiptsPersist(): Promise<void> {
|
|
3390
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
3391
|
+
clearTimeout(this.privateMessageReceiptsPersistTimer);
|
|
3392
|
+
this.privateMessageReceiptsPersistTimer = null;
|
|
3393
|
+
}
|
|
3394
|
+
if (!this.privateMessageReceiptsPersistDirty) {
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
this.privateMessageReceiptsPersistDirty = false;
|
|
3398
|
+
await this.privateMessageReceiptRepo.set(this.privateMessageReceipts);
|
|
3399
|
+
}
|
|
3400
|
+
|
|
2669
3401
|
private async log(level: "info" | "warn" | "error", message: string): Promise<void> {
|
|
2670
3402
|
await this.logRepo.append({
|
|
2671
3403
|
level,
|
|
@@ -2722,6 +3454,7 @@ export class LocalNodeService {
|
|
|
2722
3454
|
|
|
2723
3455
|
return buildPublicProfileSummary({
|
|
2724
3456
|
profile,
|
|
3457
|
+
is_self: isSelf,
|
|
2725
3458
|
online,
|
|
2726
3459
|
last_seen_at: lastSeenAt || null,
|
|
2727
3460
|
network_mode: isSelf ? this.networkMode : "unknown",
|
|
@@ -2775,6 +3508,7 @@ export class LocalNodeService {
|
|
|
2775
3508
|
updated_at: message.created_at,
|
|
2776
3509
|
signature: "",
|
|
2777
3510
|
},
|
|
3511
|
+
is_self: message.agent_id === this.identity?.agent_id,
|
|
2778
3512
|
online: false,
|
|
2779
3513
|
last_seen_at: null,
|
|
2780
3514
|
network_mode: "unknown",
|
|
@@ -2808,6 +3542,47 @@ export class LocalNodeService {
|
|
|
2808
3542
|
return `${digest.slice(0, 12)}:${digest.slice(-8)}`;
|
|
2809
3543
|
}
|
|
2810
3544
|
|
|
3545
|
+
private buildPrivateMessagingRuntimeState(): PrivateMessagingRuntimeState {
|
|
3546
|
+
const warnings: string[] = [];
|
|
3547
|
+
const keypair = this.privateEncryptionKeyPair;
|
|
3548
|
+
const selfSentMessages = this.privateMessages.filter((message) => message.from_agent_id === this.identity?.agent_id);
|
|
3549
|
+
let cachedPlaintextCount = 0;
|
|
3550
|
+
for (const message of selfSentMessages) {
|
|
3551
|
+
if (this.privateMessageBodyCache.get(message.message_id)) {
|
|
3552
|
+
cachedPlaintextCount += 1;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
if (!keypair?.public_key || !keypair?.private_key) {
|
|
3556
|
+
warnings.push("missing_private_encryption_keypair");
|
|
3557
|
+
}
|
|
3558
|
+
if (selfSentMessages.length > 0 && cachedPlaintextCount === 0) {
|
|
3559
|
+
warnings.push("missing_local_plaintext_cache_for_self_messages");
|
|
3560
|
+
}
|
|
3561
|
+
if (selfSentMessages.length > 0 && cachedPlaintextCount < selfSentMessages.length) {
|
|
3562
|
+
warnings.push("partial_local_plaintext_cache_for_self_messages");
|
|
3563
|
+
}
|
|
3564
|
+
return {
|
|
3565
|
+
schema_version: 1,
|
|
3566
|
+
app_version: this.appVersion,
|
|
3567
|
+
last_started_at: Date.now(),
|
|
3568
|
+
encryption_public_key: keypair?.public_key || "",
|
|
3569
|
+
encryption_public_key_fingerprint: keypair?.public_key ? this.fingerprintPublicKey(keypair.public_key) : "",
|
|
3570
|
+
message_count: this.privateMessages.length,
|
|
3571
|
+
self_sent_count: selfSentMessages.length,
|
|
3572
|
+
cached_plaintext_count: cachedPlaintextCount,
|
|
3573
|
+
warnings,
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
private async refreshPrivateMessagingRuntime(): Promise<void> {
|
|
3578
|
+
const runtime = this.buildPrivateMessagingRuntimeState();
|
|
3579
|
+
this.privateMessagingRuntime = runtime;
|
|
3580
|
+
await this.privateMessagingRuntimeRepo.set(runtime);
|
|
3581
|
+
for (const warning of runtime.warnings) {
|
|
3582
|
+
await this.log("warn", `Private messaging startup check: ${warning}`);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
2811
3586
|
private getOnboardingSummary() {
|
|
2812
3587
|
const summary = this.getIntegrationSummary();
|
|
2813
3588
|
const publicEnabled = Boolean(this.profile?.public_enabled);
|
|
@@ -2980,6 +3755,34 @@ export class LocalNodeService {
|
|
|
2980
3755
|
.trim();
|
|
2981
3756
|
}
|
|
2982
3757
|
|
|
3758
|
+
private buildPrivateConversationId(leftAgentId: string, rightAgentId: string): string {
|
|
3759
|
+
return [String(leftAgentId || "").trim(), String(rightAgentId || "").trim()].sort().join(":");
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
private decryptPrivateMessageBody(message: PrivateMessageRecord): string {
|
|
3763
|
+
const cached = this.privateMessageBodyCache.get(message.message_id);
|
|
3764
|
+
if (typeof cached === "string") {
|
|
3765
|
+
return cached;
|
|
3766
|
+
}
|
|
3767
|
+
if (!this.privateEncryptionKeyPair) {
|
|
3768
|
+
return "[encrypted]";
|
|
3769
|
+
}
|
|
3770
|
+
const decrypted = decryptPrivatePayload({
|
|
3771
|
+
ciphertext: message.ciphertext,
|
|
3772
|
+
nonce: message.nonce,
|
|
3773
|
+
sender_encryption_public_key: message.sender_encryption_public_key,
|
|
3774
|
+
recipient_private_key: this.privateEncryptionKeyPair.private_key,
|
|
3775
|
+
}) || "[encrypted]";
|
|
3776
|
+
this.privateMessageBodyCache.set(message.message_id, decrypted);
|
|
3777
|
+
if (this.privateMessageBodyCache.size > PRIVATE_MESSAGE_HISTORY_LIMIT * 2) {
|
|
3778
|
+
const firstKey = this.privateMessageBodyCache.keys().next().value;
|
|
3779
|
+
if (firstKey) {
|
|
3780
|
+
this.privateMessageBodyCache.delete(firstKey);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
return decrypted;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
2983
3786
|
private normalizeWindowTimestamps(timestamps: number[], windowMs: number, now = Date.now()): number[] {
|
|
2984
3787
|
return timestamps.filter((timestamp) => now - timestamp <= windowMs);
|
|
2985
3788
|
}
|
|
@@ -3005,18 +3808,32 @@ export class LocalNodeService {
|
|
|
3005
3808
|
return this.socialMessages.some((item) => item.message_id === messageId);
|
|
3006
3809
|
}
|
|
3007
3810
|
|
|
3008
|
-
private getReplayableSelfSocialMessages(now = Date.now()): SocialMessageRecord[] {
|
|
3811
|
+
private getReplayableSelfSocialMessages(reason = "manual", now = Date.now()): SocialMessageRecord[] {
|
|
3009
3812
|
const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
|
|
3010
3813
|
if (!this.identity || maxCount === 0) {
|
|
3011
3814
|
return [];
|
|
3012
3815
|
}
|
|
3013
|
-
|
|
3816
|
+
const replayable = this.socialMessages
|
|
3014
3817
|
.filter((item) => (
|
|
3015
3818
|
item.agent_id === this.identity?.agent_id &&
|
|
3016
3819
|
now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS
|
|
3017
3820
|
))
|
|
3018
3821
|
.sort((a, b) => a.created_at - b.created_at)
|
|
3019
3822
|
.slice(-maxCount);
|
|
3823
|
+
if (!replayable.length) {
|
|
3824
|
+
this.lastReplayBroadcastSignature = "";
|
|
3825
|
+
return [];
|
|
3826
|
+
}
|
|
3827
|
+
const signature = replayable.map((item) => item.message_id).join(",");
|
|
3828
|
+
const isIntervalReplay = reason === "interval";
|
|
3829
|
+
const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
|
|
3830
|
+
const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
|
|
3831
|
+
if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
|
|
3832
|
+
return [];
|
|
3833
|
+
}
|
|
3834
|
+
this.lastReplayBroadcastSignature = signature;
|
|
3835
|
+
this.lastReplayBroadcastAt = now;
|
|
3836
|
+
return replayable;
|
|
3020
3837
|
}
|
|
3021
3838
|
|
|
3022
3839
|
private hasRecentDuplicateMessage(agentId: string, body: string, topic: string, now = Date.now()): boolean {
|
|
@@ -3091,6 +3908,190 @@ export class LocalNodeService {
|
|
|
3091
3908
|
await this.persistSocialMessageObservations();
|
|
3092
3909
|
}
|
|
3093
3910
|
|
|
3911
|
+
private async sendPrivateMessageReceipt(message: PrivateMessageRecord, replyPeerId?: string): Promise<void> {
|
|
3912
|
+
if (!this.identity || typeof this.network.sendDirect !== "function" || !replyPeerId) {
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
const receipt = signPrivateMessageReceipt({
|
|
3916
|
+
identity: this.identity,
|
|
3917
|
+
receipt_id: createHash("sha256").update(`${message.message_id}:${this.identity.agent_id}:${Date.now()}`, "utf8").digest("hex"),
|
|
3918
|
+
message_id: message.message_id,
|
|
3919
|
+
conversation_id: message.conversation_id,
|
|
3920
|
+
to_agent_id: message.from_agent_id,
|
|
3921
|
+
status: "received",
|
|
3922
|
+
created_at: Date.now(),
|
|
3923
|
+
});
|
|
3924
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
3925
|
+
try {
|
|
3926
|
+
await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3927
|
+
await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3928
|
+
} catch {
|
|
3929
|
+
await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3930
|
+
}
|
|
3931
|
+
await this.persistPrivateMessageReceipts();
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
private normalizeIncomingPrivateMessage(value: unknown): PrivateMessageRecord | null {
|
|
3935
|
+
if (typeof value !== "object" || value === null) {
|
|
3936
|
+
return null;
|
|
3937
|
+
}
|
|
3938
|
+
const record = value as Partial<PrivateMessageRecord>;
|
|
3939
|
+
const createdAt = Number(record.created_at || 0);
|
|
3940
|
+
const fromAgentId = String(record.from_agent_id || "").trim();
|
|
3941
|
+
const toAgentId = String(record.to_agent_id || "").trim();
|
|
3942
|
+
const conversationId = String(record.conversation_id || "").trim();
|
|
3943
|
+
if (
|
|
3944
|
+
record.type !== PRIVATE_MESSAGE_TOPIC ||
|
|
3945
|
+
!String(record.message_id || "").trim() ||
|
|
3946
|
+
!conversationId ||
|
|
3947
|
+
!fromAgentId ||
|
|
3948
|
+
!toAgentId ||
|
|
3949
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3950
|
+
!String(record.sender_encryption_public_key || "").trim() ||
|
|
3951
|
+
!String(record.recipient_encryption_public_key || "").trim() ||
|
|
3952
|
+
!String(record.ciphertext || "").trim() ||
|
|
3953
|
+
!String(record.nonce || "").trim() ||
|
|
3954
|
+
String(record.cipher_scheme || "") !== "nacl-box-v1" ||
|
|
3955
|
+
!String(record.signature || "").trim() ||
|
|
3956
|
+
!Number.isFinite(createdAt)
|
|
3957
|
+
) {
|
|
3958
|
+
return null;
|
|
3959
|
+
}
|
|
3960
|
+
if (fromAgentId === toAgentId) {
|
|
3961
|
+
return null;
|
|
3962
|
+
}
|
|
3963
|
+
if (conversationId !== this.buildPrivateConversationId(fromAgentId, toAgentId)) {
|
|
3964
|
+
return null;
|
|
3965
|
+
}
|
|
3966
|
+
return {
|
|
3967
|
+
type: PRIVATE_MESSAGE_TOPIC,
|
|
3968
|
+
message_id: String(record.message_id).trim(),
|
|
3969
|
+
conversation_id: conversationId,
|
|
3970
|
+
from_agent_id: fromAgentId,
|
|
3971
|
+
to_agent_id: toAgentId,
|
|
3972
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3973
|
+
sender_encryption_public_key: String(record.sender_encryption_public_key).trim(),
|
|
3974
|
+
recipient_encryption_public_key: String(record.recipient_encryption_public_key).trim(),
|
|
3975
|
+
cipher_scheme: "nacl-box-v1",
|
|
3976
|
+
ciphertext: String(record.ciphertext).trim(),
|
|
3977
|
+
nonce: String(record.nonce).trim(),
|
|
3978
|
+
created_at: createdAt,
|
|
3979
|
+
signature: String(record.signature).trim(),
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
private normalizePrivateMessages(items: unknown): PrivateMessageRecord[] {
|
|
3984
|
+
if (!Array.isArray(items)) {
|
|
3985
|
+
return [];
|
|
3986
|
+
}
|
|
3987
|
+
const deduped = new Set<string>();
|
|
3988
|
+
return items
|
|
3989
|
+
.map((item) => this.normalizeIncomingPrivateMessage(item))
|
|
3990
|
+
.filter((item): item is PrivateMessageRecord => Boolean(item))
|
|
3991
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3992
|
+
.filter((item) => {
|
|
3993
|
+
if (deduped.has(item.message_id)) {
|
|
3994
|
+
return false;
|
|
3995
|
+
}
|
|
3996
|
+
deduped.add(item.message_id);
|
|
3997
|
+
return true;
|
|
3998
|
+
})
|
|
3999
|
+
.slice(-PRIVATE_MESSAGE_HISTORY_LIMIT);
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
private normalizeIncomingPrivateMessageReceipt(value: unknown): PrivateMessageReceiptRecord | null {
|
|
4003
|
+
if (typeof value !== "object" || value === null) {
|
|
4004
|
+
return null;
|
|
4005
|
+
}
|
|
4006
|
+
const record = value as Partial<PrivateMessageReceiptRecord>;
|
|
4007
|
+
const createdAt = Number(record.created_at || 0);
|
|
4008
|
+
const status = String(record.status || "").trim();
|
|
4009
|
+
if (
|
|
4010
|
+
record.type !== PRIVATE_MESSAGE_RECEIPT_TOPIC ||
|
|
4011
|
+
!String(record.receipt_id || "").trim() ||
|
|
4012
|
+
!String(record.message_id || "").trim() ||
|
|
4013
|
+
!String(record.conversation_id || "").trim() ||
|
|
4014
|
+
!String(record.from_agent_id || "").trim() ||
|
|
4015
|
+
!String(record.to_agent_id || "").trim() ||
|
|
4016
|
+
!String(record.sender_public_key || "").trim() ||
|
|
4017
|
+
(status !== "received" && status !== "read") ||
|
|
4018
|
+
!String(record.signature || "").trim() ||
|
|
4019
|
+
!Number.isFinite(createdAt)
|
|
4020
|
+
) {
|
|
4021
|
+
return null;
|
|
4022
|
+
}
|
|
4023
|
+
return {
|
|
4024
|
+
type: PRIVATE_MESSAGE_RECEIPT_TOPIC,
|
|
4025
|
+
receipt_id: String(record.receipt_id).trim(),
|
|
4026
|
+
message_id: String(record.message_id).trim(),
|
|
4027
|
+
conversation_id: String(record.conversation_id).trim(),
|
|
4028
|
+
from_agent_id: String(record.from_agent_id).trim(),
|
|
4029
|
+
to_agent_id: String(record.to_agent_id).trim(),
|
|
4030
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
4031
|
+
status: status as "received" | "read",
|
|
4032
|
+
created_at: createdAt,
|
|
4033
|
+
signature: String(record.signature).trim(),
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
private normalizePrivateMessageReceipts(items: unknown): PrivateMessageReceiptRecord[] {
|
|
4038
|
+
if (!Array.isArray(items)) {
|
|
4039
|
+
return [];
|
|
4040
|
+
}
|
|
4041
|
+
const deduped = new Set<string>();
|
|
4042
|
+
return items
|
|
4043
|
+
.map((item) => this.normalizeIncomingPrivateMessageReceipt(item))
|
|
4044
|
+
.filter((item): item is PrivateMessageReceiptRecord => Boolean(item))
|
|
4045
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
4046
|
+
.filter((item) => {
|
|
4047
|
+
if (deduped.has(item.receipt_id)) {
|
|
4048
|
+
return false;
|
|
4049
|
+
}
|
|
4050
|
+
deduped.add(item.receipt_id);
|
|
4051
|
+
return true;
|
|
4052
|
+
})
|
|
4053
|
+
.slice(-PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT);
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4056
|
+
private hasPrivateMessage(messageId: string): boolean {
|
|
4057
|
+
return this.privateMessages.some((item) => item.message_id === messageId);
|
|
4058
|
+
}
|
|
4059
|
+
|
|
4060
|
+
private ingestPrivateMessage(message: PrivateMessageRecord): void {
|
|
4061
|
+
const existing = this.privateMessages.findIndex((item) => item.message_id === message.message_id);
|
|
4062
|
+
if (existing >= 0) {
|
|
4063
|
+
this.privateMessages[existing] = message;
|
|
4064
|
+
} else {
|
|
4065
|
+
this.privateMessages.push(message);
|
|
4066
|
+
}
|
|
4067
|
+
this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
|
|
4068
|
+
const validIds = new Set(this.privateMessages.map((item) => item.message_id));
|
|
4069
|
+
if (message.from_agent_id !== this.identity?.agent_id) {
|
|
4070
|
+
this.privateMessageBodyCache.delete(message.message_id);
|
|
4071
|
+
}
|
|
4072
|
+
for (const key of Array.from(this.privateMessageBodyCache.keys())) {
|
|
4073
|
+
if (!validIds.has(key)) {
|
|
4074
|
+
this.privateMessageBodyCache.delete(key);
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
for (const key of Array.from(this.privateMessageDeliveryStatusCache.keys())) {
|
|
4078
|
+
if (!validIds.has(key)) {
|
|
4079
|
+
this.privateMessageDeliveryStatusCache.delete(key);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
private ingestPrivateMessageReceipt(receipt: PrivateMessageReceiptRecord): void {
|
|
4085
|
+
const existing = this.privateMessageReceipts.findIndex((item) => item.receipt_id === receipt.receipt_id);
|
|
4086
|
+
if (existing >= 0) {
|
|
4087
|
+
this.privateMessageReceipts[existing] = receipt;
|
|
4088
|
+
} else {
|
|
4089
|
+
this.privateMessageReceipts.push(receipt);
|
|
4090
|
+
}
|
|
4091
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
|
|
4092
|
+
this.privateMessageDeliveryStatusCache.set(receipt.message_id, receipt.status);
|
|
4093
|
+
}
|
|
4094
|
+
|
|
3094
4095
|
private normalizeIncomingSocialMessage(value: unknown): SocialMessageRecord | null {
|
|
3095
4096
|
if (typeof value !== "object" || value === null) {
|
|
3096
4097
|
return null;
|
|
@@ -3369,6 +4370,48 @@ export async function main() {
|
|
|
3369
4370
|
sendOk(res, node.getRuntimePaths());
|
|
3370
4371
|
});
|
|
3371
4372
|
|
|
4373
|
+
app.get("/api/app/update-status", (_req, res) => {
|
|
4374
|
+
sendOk(res, node.getAppUpdateStatus());
|
|
4375
|
+
});
|
|
4376
|
+
|
|
4377
|
+
app.post(
|
|
4378
|
+
"/api/app/update",
|
|
4379
|
+
asyncRoute(async (_req, res) => {
|
|
4380
|
+
const status = node.getAppUpdateStatus();
|
|
4381
|
+
if (!status.update_available || !status.latest_version) {
|
|
4382
|
+
sendOk(
|
|
4383
|
+
res,
|
|
4384
|
+
{
|
|
4385
|
+
started: false,
|
|
4386
|
+
current_version: status.current_version,
|
|
4387
|
+
latest_version: status.latest_version,
|
|
4388
|
+
platform: status.platform,
|
|
4389
|
+
reason: status.check_error || "already_current",
|
|
4390
|
+
},
|
|
4391
|
+
{ message: "Already on the latest version" }
|
|
4392
|
+
);
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
4395
|
+
sendOk(
|
|
4396
|
+
res,
|
|
4397
|
+
{
|
|
4398
|
+
started: true,
|
|
4399
|
+
current_version: status.current_version,
|
|
4400
|
+
target_version: status.latest_version,
|
|
4401
|
+
platform: status.platform,
|
|
4402
|
+
},
|
|
4403
|
+
{ message: `Updating to ${status.latest_version}` }
|
|
4404
|
+
);
|
|
4405
|
+
setTimeout(() => {
|
|
4406
|
+
try {
|
|
4407
|
+
node.startAppUpdate();
|
|
4408
|
+
} catch {
|
|
4409
|
+
// best effort after response has been sent
|
|
4410
|
+
}
|
|
4411
|
+
}, 1200);
|
|
4412
|
+
})
|
|
4413
|
+
);
|
|
4414
|
+
|
|
3372
4415
|
app.put(
|
|
3373
4416
|
"/api/profile",
|
|
3374
4417
|
asyncRoute(async (req, res) => {
|
|
@@ -3511,6 +4554,38 @@ export async function main() {
|
|
|
3511
4554
|
sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
|
|
3512
4555
|
});
|
|
3513
4556
|
|
|
4557
|
+
app.get("/api/private/state", (_req, res) => {
|
|
4558
|
+
sendOk(res, node.getPrivateMessagingState());
|
|
4559
|
+
});
|
|
4560
|
+
|
|
4561
|
+
app.get("/api/private/conversations", (_req, res) => {
|
|
4562
|
+
sendOk(res, node.getPrivateConversations());
|
|
4563
|
+
});
|
|
4564
|
+
|
|
4565
|
+
app.get("/api/private/messages", (req, res) => {
|
|
4566
|
+
const conversationId = String(req.query.conversation_id ?? "").trim();
|
|
4567
|
+
const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
|
|
4568
|
+
sendOk(res, node.getPrivateMessages(conversationId, limit));
|
|
4569
|
+
});
|
|
4570
|
+
|
|
4571
|
+
app.post(
|
|
4572
|
+
"/api/private/messages/send",
|
|
4573
|
+
asyncRoute(async (req, res) => {
|
|
4574
|
+
const result = await node.sendPrivateMessage({
|
|
4575
|
+
to_agent_id: String(req.body?.to_agent_id || ""),
|
|
4576
|
+
recipient_encryption_public_key: String(req.body?.recipient_encryption_public_key || ""),
|
|
4577
|
+
body: String(req.body?.body || ""),
|
|
4578
|
+
});
|
|
4579
|
+
sendOk(res, result, {
|
|
4580
|
+
message: result.sent
|
|
4581
|
+
? (result.reason === "direct-sent"
|
|
4582
|
+
? "Private message sent directly"
|
|
4583
|
+
: "Private message sent via encrypted fallback")
|
|
4584
|
+
: `Private message skipped: ${result.reason}`,
|
|
4585
|
+
});
|
|
4586
|
+
})
|
|
4587
|
+
);
|
|
4588
|
+
|
|
3514
4589
|
app.get("/api/openclaw/bridge", (_req, res) => {
|
|
3515
4590
|
sendOk(res, node.getOpenClawBridgeStatus());
|
|
3516
4591
|
});
|