@silicaclaw/cli 2026.3.18-4 → 2026.3.19-2
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/ARCHITECTURE.md +15 -0
- package/CHANGELOG.md +17 -2
- package/INSTALL.md +35 -0
- package/README.md +119 -10
- package/RELEASE_NOTES_v1.0.md +29 -2
- package/SOCIAL_MD_SPEC.md +2 -0
- package/VERSION +1 -1
- package/apps/local-console/public/index.html +2297 -231
- package/apps/local-console/src/server.ts +1120 -24
- package/apps/local-console/src/socialRoutes.ts +21 -0
- package/apps/public-explorer/public/index.html +190 -43
- package/docs/NEW_USER_OPERATIONS.md +35 -5
- package/docs/OPENCLAW_BRIDGE.md +449 -0
- package/docs/OPENCLAW_BRIDGE_ZH.md +445 -0
- package/docs/QUICK_START.md +20 -1
- package/docs/release/ANNOUNCEMENT_v1.0-beta.md +68 -0
- package/docs/release/FINAL_RELEASE_SUMMARY_v1.0-beta.md +112 -0
- package/docs/release/GITHUB_RELEASE_v1.0-beta.md +16 -16
- package/docs/release/RELEASE_COPY_v1.0-beta.md +102 -0
- package/openclaw-skills/silicaclaw-broadcast/SKILL.md +89 -0
- package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -0
- package/openclaw-skills/silicaclaw-broadcast/agents/openai.yaml +6 -0
- package/openclaw-skills/silicaclaw-broadcast/manifest.json +34 -0
- package/openclaw-skills/silicaclaw-broadcast/references/computer-control-via-openclaw.md +41 -0
- package/openclaw-skills/silicaclaw-broadcast/references/owner-dispatch-adapter.md +81 -0
- package/openclaw-skills/silicaclaw-broadcast/references/owner-forwarding-policy.md +48 -0
- package/openclaw-skills/silicaclaw-broadcast/scripts/bridge-client.mjs +59 -0
- package/openclaw-skills/silicaclaw-broadcast/scripts/owner-dispatch-adapter-demo.mjs +12 -0
- package/openclaw-skills/silicaclaw-broadcast/scripts/owner-forwarder-demo.mjs +111 -0
- package/openclaw-skills/silicaclaw-broadcast/scripts/send-to-owner-via-openclaw.mjs +69 -0
- package/openclaw.social.md.example +6 -0
- package/package.json +2 -1
- package/packages/core/dist/index.d.ts +1 -0
- package/packages/core/dist/index.js +1 -0
- package/packages/core/dist/socialConfig.d.ts +1 -0
- package/packages/core/dist/socialConfig.js +9 -1
- package/packages/core/dist/socialMessage.d.ts +19 -0
- package/packages/core/dist/socialMessage.js +69 -0
- package/packages/core/dist/socialTemplate.js +3 -1
- package/packages/core/dist/types.d.ts +22 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/socialConfig.ts +13 -1
- package/packages/core/src/socialMessage.ts +86 -0
- package/packages/core/src/socialTemplate.ts +3 -1
- package/packages/core/src/types.ts +24 -0
- package/packages/network/dist/relayPreview.js +16 -4
- package/packages/network/src/relayPreview.ts +17 -4
- package/packages/storage/dist/repos.d.ts +40 -0
- package/packages/storage/dist/repos.js +27 -1
- package/packages/storage/dist/socialRuntimeRepo.js +1 -0
- package/packages/storage/src/repos.ts +60 -0
- package/packages/storage/src/socialRuntimeRepo.ts +1 -0
- package/packages/storage/tsconfig.json +1 -1
- package/scripts/functional-check.mjs +85 -2
- package/scripts/install-openclaw-skill.mjs +54 -0
- package/scripts/openclaw-bridge-adapter.mjs +89 -0
- package/scripts/openclaw-bridge-client.mjs +223 -0
- package/scripts/openclaw-runtime-demo.mjs +202 -0
- package/scripts/pack-openclaw-skill.mjs +58 -0
- package/scripts/silicaclaw-cli.mjs +30 -0
- package/scripts/silicaclaw-gateway.mjs +215 -0
- package/scripts/validate-openclaw-skill.mjs +74 -0
- package/social.md.example +6 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import express, { NextFunction, Request, Response } from "express";
|
|
2
2
|
import cors from "cors";
|
|
3
|
+
import { execFile, spawnSync } from "child_process";
|
|
3
4
|
import { resolve } from "path";
|
|
4
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
5
|
+
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
5
6
|
import { createHash } from "crypto";
|
|
6
7
|
import { hostname } from "os";
|
|
8
|
+
import { promisify } from "util";
|
|
7
9
|
import {
|
|
8
10
|
AgentIdentity,
|
|
9
11
|
DirectoryState,
|
|
@@ -30,11 +32,17 @@ import {
|
|
|
30
32
|
resolveIdentityWithSocial,
|
|
31
33
|
resolveProfileInputWithSocial,
|
|
32
34
|
searchDirectory,
|
|
35
|
+
signSocialMessage,
|
|
36
|
+
signSocialMessageObservation,
|
|
33
37
|
signPresence,
|
|
34
38
|
signProfile,
|
|
35
39
|
SocialConfig,
|
|
40
|
+
SocialMessageObservationRecord,
|
|
41
|
+
SocialMessageRecord,
|
|
36
42
|
SocialRuntimeConfig,
|
|
37
43
|
generateSocialMdTemplate,
|
|
44
|
+
verifySocialMessage,
|
|
45
|
+
verifySocialMessageObservation,
|
|
38
46
|
verifyPresence,
|
|
39
47
|
verifyProfile,
|
|
40
48
|
} from "@silicaclaw/core";
|
|
@@ -48,7 +56,17 @@ import {
|
|
|
48
56
|
UdpLanBroadcastTransport,
|
|
49
57
|
WebRTCPreviewAdapter,
|
|
50
58
|
} from "@silicaclaw/network";
|
|
51
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
CacheRepo,
|
|
61
|
+
IdentityRepo,
|
|
62
|
+
LogRepo,
|
|
63
|
+
ProfileRepo,
|
|
64
|
+
SocialMessageGovernanceConfig,
|
|
65
|
+
SocialMessageGovernanceRepo,
|
|
66
|
+
SocialMessageRepo,
|
|
67
|
+
SocialMessageObservationRepo,
|
|
68
|
+
SocialRuntimeRepo,
|
|
69
|
+
} from "@silicaclaw/storage";
|
|
52
70
|
import { registerSocialRoutes } from "./socialRoutes";
|
|
53
71
|
|
|
54
72
|
const BROADCAST_INTERVAL_MS = Number(process.env.BROADCAST_INTERVAL_MS || 20_000);
|
|
@@ -71,6 +89,27 @@ const WEBRTC_ROOM = process.env.WEBRTC_ROOM || "silicaclaw-global-preview";
|
|
|
71
89
|
const WEBRTC_SEED_PEERS = process.env.WEBRTC_SEED_PEERS || "";
|
|
72
90
|
const WEBRTC_BOOTSTRAP_HINTS = process.env.WEBRTC_BOOTSTRAP_HINTS || "";
|
|
73
91
|
const PROFILE_VERSION = "v0.9";
|
|
92
|
+
const SOCIAL_MESSAGE_TOPIC = "social.message";
|
|
93
|
+
const SOCIAL_MESSAGE_OBSERVATION_TOPIC = "social.message.observation";
|
|
94
|
+
const DEFAULT_SOCIAL_MESSAGE_CHANNEL = "global";
|
|
95
|
+
const SOCIAL_MESSAGE_MAX_BODY_CHARS = Number(process.env.SOCIAL_MESSAGE_MAX_BODY_CHARS || 500);
|
|
96
|
+
const SOCIAL_MESSAGE_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_HISTORY_LIMIT || 100);
|
|
97
|
+
const SOCIAL_MESSAGE_SEND_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_SEND_WINDOW_MS || 60_000);
|
|
98
|
+
const SOCIAL_MESSAGE_SEND_MAX_PER_WINDOW = Number(process.env.SOCIAL_MESSAGE_SEND_MAX_PER_WINDOW || 5);
|
|
99
|
+
const SOCIAL_MESSAGE_RECEIVE_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_RECEIVE_WINDOW_MS || 60_000);
|
|
100
|
+
const SOCIAL_MESSAGE_RECEIVE_MAX_PER_WINDOW = Number(process.env.SOCIAL_MESSAGE_RECEIVE_MAX_PER_WINDOW || 8);
|
|
101
|
+
const SOCIAL_MESSAGE_DUPLICATE_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_DUPLICATE_WINDOW_MS || 180_000);
|
|
102
|
+
const SOCIAL_MESSAGE_MAX_FUTURE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_FUTURE_MS || 30_000);
|
|
103
|
+
const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS || 15 * 60_000);
|
|
104
|
+
const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
|
|
105
|
+
const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(
|
|
106
|
+
dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || ""))
|
|
107
|
+
);
|
|
108
|
+
const SOCIAL_MESSAGE_BLOCKED_TERMS = dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_TERMS || ""))
|
|
109
|
+
.map((term) => term.trim().toLowerCase())
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
const execFileAsync = promisify(execFile);
|
|
112
|
+
const OPENCLAW_SKILL_NAME = "silicaclaw-broadcast";
|
|
74
113
|
|
|
75
114
|
function readWorkspaceVersion(workspaceRoot: string): string {
|
|
76
115
|
const pkgFile = resolve(workspaceRoot, "package.json");
|
|
@@ -109,6 +148,193 @@ function resolveStorageRoot(workspaceRoot: string, cwd = process.cwd()): string
|
|
|
109
148
|
return cwd;
|
|
110
149
|
}
|
|
111
150
|
|
|
151
|
+
function resolveExecutableInPath(binName: string): string | null {
|
|
152
|
+
const pathValue = String(process.env.PATH || "").trim();
|
|
153
|
+
if (!pathValue) return null;
|
|
154
|
+
const pathEntries = pathValue.split(":").map((item) => item.trim()).filter(Boolean);
|
|
155
|
+
for (const entry of pathEntries) {
|
|
156
|
+
const candidate = resolve(entry, binName);
|
|
157
|
+
if (!existsSync(candidate)) continue;
|
|
158
|
+
try {
|
|
159
|
+
accessSync(candidate, constants.X_OK);
|
|
160
|
+
return candidate;
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore non-executable matches
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function existingPathOrNull(filePath: string): string | null {
|
|
169
|
+
return existsSync(filePath) ? filePath : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function detectOpenClawInstallation(workspaceRoot: string) {
|
|
173
|
+
const workspaceDir = resolve(workspaceRoot, ".openclaw");
|
|
174
|
+
const homeDir = resolve(process.env.HOME || "", ".openclaw");
|
|
175
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
176
|
+
|
|
177
|
+
const workspaceIdentityPath = existingPathOrNull(resolve(workspaceDir, "identity.json"));
|
|
178
|
+
const workspaceProfilePath = existingPathOrNull(resolve(workspaceDir, "profile.json"));
|
|
179
|
+
const workspaceSocialPath = existingPathOrNull(resolve(workspaceDir, "social.md"));
|
|
180
|
+
const workspaceSkillsPath = existingPathOrNull(resolve(workspaceDir, "skills"));
|
|
181
|
+
const homeIdentityPath = existingPathOrNull(resolve(homeDir, "identity.json"));
|
|
182
|
+
const homeProfilePath = existingPathOrNull(resolve(homeDir, "profile.json"));
|
|
183
|
+
const homeSocialPath = existingPathOrNull(resolve(homeDir, "social.md"));
|
|
184
|
+
const homeSkillsPath = existingPathOrNull(resolve(homeDir, "skills"));
|
|
185
|
+
|
|
186
|
+
const workspaceDetected = Boolean(
|
|
187
|
+
existsSync(workspaceDir) ||
|
|
188
|
+
workspaceIdentityPath ||
|
|
189
|
+
workspaceProfilePath ||
|
|
190
|
+
workspaceSocialPath ||
|
|
191
|
+
workspaceSkillsPath
|
|
192
|
+
);
|
|
193
|
+
const homeDetected = Boolean(
|
|
194
|
+
existsSync(homeDir) || homeIdentityPath || homeProfilePath || homeSocialPath || homeSkillsPath
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
detected: Boolean(commandPath || workspaceDetected || homeDetected),
|
|
199
|
+
detection_mode: commandPath
|
|
200
|
+
? "command"
|
|
201
|
+
: workspaceDetected
|
|
202
|
+
? "workspace"
|
|
203
|
+
: homeDetected
|
|
204
|
+
? "home"
|
|
205
|
+
: "not_found",
|
|
206
|
+
command_path: commandPath,
|
|
207
|
+
workspace_dir: workspaceDir,
|
|
208
|
+
home_dir: homeDir,
|
|
209
|
+
workspace_dir_exists: existsSync(workspaceDir),
|
|
210
|
+
home_dir_exists: existsSync(homeDir),
|
|
211
|
+
workspace_identity_path: workspaceIdentityPath,
|
|
212
|
+
workspace_profile_path: workspaceProfilePath,
|
|
213
|
+
workspace_social_path: workspaceSocialPath,
|
|
214
|
+
workspace_skills_path: workspaceSkillsPath,
|
|
215
|
+
home_identity_path: homeIdentityPath,
|
|
216
|
+
home_profile_path: homeProfilePath,
|
|
217
|
+
home_social_path: homeSocialPath,
|
|
218
|
+
home_skills_path: homeSkillsPath,
|
|
219
|
+
} as const;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function detectOpenClawRuntime() {
|
|
223
|
+
const result = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
|
|
224
|
+
encoding: "utf8",
|
|
225
|
+
});
|
|
226
|
+
const stdout = String(result.stdout || "");
|
|
227
|
+
const lines = stdout
|
|
228
|
+
.split("\n")
|
|
229
|
+
.map((line) => line.trim())
|
|
230
|
+
.filter(Boolean);
|
|
231
|
+
|
|
232
|
+
const processes = lines
|
|
233
|
+
.map((line) => {
|
|
234
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
235
|
+
if (!match) return null;
|
|
236
|
+
const command = match[3] || "";
|
|
237
|
+
const lower = command.toLowerCase();
|
|
238
|
+
const isOpenClaw =
|
|
239
|
+
lower.includes(" openclaw ") ||
|
|
240
|
+
lower.endsWith(" openclaw") ||
|
|
241
|
+
lower.includes("/openclaw ") ||
|
|
242
|
+
lower.includes("openclaw.mjs") ||
|
|
243
|
+
lower.includes("openclaw gateway") ||
|
|
244
|
+
lower.includes("openclaw agent") ||
|
|
245
|
+
lower.includes("openclaw message");
|
|
246
|
+
if (!isOpenClaw) return null;
|
|
247
|
+
return {
|
|
248
|
+
pid: Number(match[1]),
|
|
249
|
+
ppid: Number(match[2]),
|
|
250
|
+
command,
|
|
251
|
+
};
|
|
252
|
+
})
|
|
253
|
+
.filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
running: processes.length > 0,
|
|
257
|
+
process_count: processes.length,
|
|
258
|
+
processes: processes.slice(0, 10),
|
|
259
|
+
detection_error: result.status === 0 ? null : String(result.stderr || "ps failed"),
|
|
260
|
+
} as const;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function detectOpenClawSkillInstallation() {
|
|
264
|
+
const homeDir = resolve(process.env.HOME || "", ".openclaw");
|
|
265
|
+
const workspaceSkillRoot = resolve(homeDir, "workspace", "skills");
|
|
266
|
+
const legacySkillRoot = resolve(homeDir, "skills");
|
|
267
|
+
const workspaceSkillPath = resolve(workspaceSkillRoot, OPENCLAW_SKILL_NAME, "SKILL.md");
|
|
268
|
+
const legacySkillPath = resolve(legacySkillRoot, OPENCLAW_SKILL_NAME, "SKILL.md");
|
|
269
|
+
const workspaceInstalled = existsSync(workspaceSkillPath);
|
|
270
|
+
const legacyInstalled = existsSync(legacySkillPath);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
installed: workspaceInstalled || legacyInstalled,
|
|
274
|
+
install_mode: workspaceInstalled ? "workspace" : legacyInstalled ? "legacy" : "not_installed",
|
|
275
|
+
workspace_skill_root: workspaceSkillRoot,
|
|
276
|
+
legacy_skill_root: legacySkillRoot,
|
|
277
|
+
workspace_skill_path: workspaceInstalled ? workspaceSkillPath : null,
|
|
278
|
+
legacy_skill_path: legacyInstalled ? legacySkillPath : null,
|
|
279
|
+
} as const;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function detectOwnerDeliveryStatus(params: {
|
|
283
|
+
workspaceRoot: string;
|
|
284
|
+
connectedToSilicaclaw: boolean;
|
|
285
|
+
openclawRunning: boolean;
|
|
286
|
+
skillInstalled: boolean;
|
|
287
|
+
}) {
|
|
288
|
+
const forwardCommand = String(process.env.OPENCLAW_OWNER_FORWARD_CMD || "").trim();
|
|
289
|
+
const ownerChannel = String(process.env.OPENCLAW_OWNER_CHANNEL || "").trim();
|
|
290
|
+
const ownerTarget = String(process.env.OPENCLAW_OWNER_TARGET || "").trim();
|
|
291
|
+
const ownerAccount = String(process.env.OPENCLAW_OWNER_ACCOUNT || "").trim();
|
|
292
|
+
const explicitOpenClawBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
293
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
294
|
+
const defaultSourceDir = resolve(params.workspaceRoot, "..", "openclaw");
|
|
295
|
+
const openclawSourceDir = configuredSourceDir || defaultSourceDir;
|
|
296
|
+
const openclawSourceEntry = existingPathOrNull(resolve(openclawSourceDir, "openclaw.mjs"));
|
|
297
|
+
const openclawCommandResolvable = Boolean(explicitOpenClawBin || resolveExecutableInPath("openclaw") || openclawSourceEntry);
|
|
298
|
+
const bridgeMessagesReadable = params.connectedToSilicaclaw && params.openclawRunning && params.skillInstalled;
|
|
299
|
+
const forwardCommandConfigured = Boolean(forwardCommand);
|
|
300
|
+
const ownerRouteConfigured = Boolean(ownerChannel && ownerTarget);
|
|
301
|
+
const ready =
|
|
302
|
+
bridgeMessagesReadable && forwardCommandConfigured && ownerRouteConfigured && openclawCommandResolvable;
|
|
303
|
+
|
|
304
|
+
let reason = "";
|
|
305
|
+
if (!params.connectedToSilicaclaw) {
|
|
306
|
+
reason = "SilicaClaw social bridge is not connected yet, so there is no broadcast stream for OpenClaw to learn.";
|
|
307
|
+
} else if (!params.openclawRunning) {
|
|
308
|
+
reason = "OpenClaw is not running on this machine yet, so broadcast learning and owner forwarding are idle.";
|
|
309
|
+
} else if (!params.skillInstalled) {
|
|
310
|
+
reason = "OpenClaw is running, but the silicaclaw-broadcast skill is not installed yet.";
|
|
311
|
+
} else if (!forwardCommandConfigured) {
|
|
312
|
+
reason = "Broadcast learning is ready, but OPENCLAW_OWNER_FORWARD_CMD is not configured yet.";
|
|
313
|
+
} else if (!ownerRouteConfigured) {
|
|
314
|
+
reason = "The owner forward command exists, but OPENCLAW_OWNER_CHANNEL and OPENCLAW_OWNER_TARGET are still missing.";
|
|
315
|
+
} else if (!openclawCommandResolvable) {
|
|
316
|
+
reason = "Owner forwarding is configured, but no runnable OpenClaw CLI or source checkout was found.";
|
|
317
|
+
} else {
|
|
318
|
+
reason = "This machine can read SilicaClaw broadcasts and route owner summaries through OpenClaw.";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
supported: bridgeMessagesReadable,
|
|
323
|
+
mode: "public-broadcast-via-openclaw" as const,
|
|
324
|
+
send_to_owner_via_openclaw: ready,
|
|
325
|
+
bridge_messages_readable: bridgeMessagesReadable,
|
|
326
|
+
forward_command_configured: forwardCommandConfigured,
|
|
327
|
+
openclaw_command_resolvable: openclawCommandResolvable,
|
|
328
|
+
ready,
|
|
329
|
+
forward_command: forwardCommand || null,
|
|
330
|
+
owner_channel: ownerChannel || null,
|
|
331
|
+
owner_target: ownerTarget || null,
|
|
332
|
+
owner_account: ownerAccount || null,
|
|
333
|
+
openclaw_source_dir: openclawSourceEntry ? openclawSourceDir : null,
|
|
334
|
+
reason,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
112
338
|
function hasMeaningfulJson(filePath: string): boolean {
|
|
113
339
|
if (!existsSync(filePath)) return false;
|
|
114
340
|
try {
|
|
@@ -128,7 +354,14 @@ function migrateLegacyDataIfNeeded(workspaceRoot: string, storageRoot: string):
|
|
|
128
354
|
const legacyDataDir = resolve(workspaceRoot, "data");
|
|
129
355
|
const targetDataDir = resolve(storageRoot, "data");
|
|
130
356
|
if (legacyDataDir === targetDataDir) return;
|
|
131
|
-
const files = [
|
|
357
|
+
const files = [
|
|
358
|
+
"identity.json",
|
|
359
|
+
"profile.json",
|
|
360
|
+
"cache.json",
|
|
361
|
+
"logs.json",
|
|
362
|
+
"social-messages.json",
|
|
363
|
+
"social-message-observations.json",
|
|
364
|
+
];
|
|
132
365
|
for (const file of files) {
|
|
133
366
|
const src = resolve(legacyDataDir, file);
|
|
134
367
|
const dst = resolve(targetDataDir, file);
|
|
@@ -179,29 +412,151 @@ type IntegrationStatusSummary = {
|
|
|
179
412
|
status_line: string;
|
|
180
413
|
};
|
|
181
414
|
|
|
182
|
-
|
|
415
|
+
type SocialMessageView = SocialMessageRecord & {
|
|
416
|
+
is_self: boolean;
|
|
417
|
+
online: boolean;
|
|
418
|
+
last_seen_at: number | null;
|
|
419
|
+
observation_count: number;
|
|
420
|
+
remote_observation_count: number;
|
|
421
|
+
last_observed_at: number | null;
|
|
422
|
+
delivery_status: "local-only" | "remote-observed";
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
type RuntimeMessageGovernance = SocialMessageGovernanceConfig;
|
|
426
|
+
|
|
427
|
+
type OpenClawBridgeStatus = {
|
|
428
|
+
enabled: boolean;
|
|
429
|
+
connected_to_silicaclaw: boolean;
|
|
430
|
+
public_enabled: boolean;
|
|
431
|
+
message_broadcast_enabled: boolean;
|
|
432
|
+
network_mode: "local" | "lan" | "global-preview";
|
|
433
|
+
adapter: string;
|
|
434
|
+
agent_id: string;
|
|
435
|
+
display_name: string;
|
|
436
|
+
identity_source: "silicaclaw-existing" | "openclaw-existing" | "silicaclaw-generated";
|
|
437
|
+
openclaw_identity_source_path: string | null;
|
|
438
|
+
social_source_path: string | null;
|
|
439
|
+
openclaw_installation: {
|
|
440
|
+
detected: boolean;
|
|
441
|
+
detection_mode: "command" | "workspace" | "home" | "not_found";
|
|
442
|
+
command_path: string | null;
|
|
443
|
+
workspace_dir: string;
|
|
444
|
+
home_dir: string;
|
|
445
|
+
workspace_dir_exists: boolean;
|
|
446
|
+
home_dir_exists: boolean;
|
|
447
|
+
workspace_identity_path: string | null;
|
|
448
|
+
workspace_profile_path: string | null;
|
|
449
|
+
workspace_social_path: string | null;
|
|
450
|
+
workspace_skills_path: string | null;
|
|
451
|
+
home_identity_path: string | null;
|
|
452
|
+
home_profile_path: string | null;
|
|
453
|
+
home_social_path: string | null;
|
|
454
|
+
home_skills_path: string | null;
|
|
455
|
+
};
|
|
456
|
+
openclaw_runtime: {
|
|
457
|
+
running: boolean;
|
|
458
|
+
process_count: number;
|
|
459
|
+
processes: Array<{
|
|
460
|
+
pid: number;
|
|
461
|
+
ppid: number;
|
|
462
|
+
command: string;
|
|
463
|
+
}>;
|
|
464
|
+
detection_error: string | null;
|
|
465
|
+
};
|
|
466
|
+
skill_learning: {
|
|
467
|
+
available: boolean;
|
|
468
|
+
installed: boolean;
|
|
469
|
+
install_mode: "workspace" | "legacy" | "not_installed";
|
|
470
|
+
installed_skill_path: string | null;
|
|
471
|
+
install_action: {
|
|
472
|
+
supported: boolean;
|
|
473
|
+
endpoint: string;
|
|
474
|
+
recommended_command: string;
|
|
475
|
+
};
|
|
476
|
+
skills: Array<{
|
|
477
|
+
key: "get_profile" | "list_messages" | "watch_messages" | "send_message";
|
|
478
|
+
summary: string;
|
|
479
|
+
endpoint: string;
|
|
480
|
+
}>;
|
|
481
|
+
};
|
|
482
|
+
owner_delivery: {
|
|
483
|
+
supported: boolean;
|
|
484
|
+
mode: "public-broadcast-via-openclaw";
|
|
485
|
+
send_to_owner_via_openclaw: boolean;
|
|
486
|
+
bridge_messages_readable: boolean;
|
|
487
|
+
forward_command_configured: boolean;
|
|
488
|
+
openclaw_command_resolvable: boolean;
|
|
489
|
+
ready: boolean;
|
|
490
|
+
forward_command: string | null;
|
|
491
|
+
owner_channel: string | null;
|
|
492
|
+
owner_target: string | null;
|
|
493
|
+
owner_account: string | null;
|
|
494
|
+
openclaw_source_dir: string | null;
|
|
495
|
+
reason: string;
|
|
496
|
+
};
|
|
497
|
+
endpoints: {
|
|
498
|
+
status: string;
|
|
499
|
+
profile: string;
|
|
500
|
+
messages: string;
|
|
501
|
+
send_message: string;
|
|
502
|
+
install_skill: string;
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
type OpenClawBridgeConfigView = {
|
|
507
|
+
bridge_api_base: string;
|
|
508
|
+
openclaw_detected: boolean;
|
|
509
|
+
openclaw_running: boolean;
|
|
510
|
+
openclaw_workspace_skill_dir: string;
|
|
511
|
+
openclaw_legacy_skill_dir: string;
|
|
512
|
+
silicaclaw_env_template_path: string;
|
|
513
|
+
recommended_skill_name: string;
|
|
514
|
+
recommended_install_command: string;
|
|
515
|
+
recommended_owner_forward_env: {
|
|
516
|
+
OPENCLAW_SOURCE_DIR: string;
|
|
517
|
+
OPENCLAW_OWNER_CHANNEL: string;
|
|
518
|
+
OPENCLAW_OWNER_TARGET: string;
|
|
519
|
+
OPENCLAW_OWNER_ACCOUNT: string;
|
|
520
|
+
OPENCLAW_OWNER_FORWARD_CMD: string;
|
|
521
|
+
};
|
|
522
|
+
owner_forward_command_example: string;
|
|
523
|
+
notes: string[];
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
export class LocalNodeService {
|
|
183
527
|
private workspaceRoot: string;
|
|
184
528
|
private storageRoot: string;
|
|
185
529
|
private identityRepo: IdentityRepo;
|
|
186
530
|
private profileRepo: ProfileRepo;
|
|
187
531
|
private cacheRepo: CacheRepo;
|
|
188
532
|
private logRepo: LogRepo;
|
|
533
|
+
private socialMessageGovernanceRepo: SocialMessageGovernanceRepo;
|
|
534
|
+
private socialMessageRepo: SocialMessageRepo;
|
|
535
|
+
private socialMessageObservationRepo: SocialMessageObservationRepo;
|
|
189
536
|
private socialRuntimeRepo: SocialRuntimeRepo;
|
|
190
537
|
|
|
191
538
|
private identity: AgentIdentity | null = null;
|
|
192
539
|
private profile: PublicProfile | null = null;
|
|
193
540
|
private directory: DirectoryState = createEmptyDirectoryState();
|
|
541
|
+
private socialMessages: SocialMessageRecord[] = [];
|
|
542
|
+
private socialMessageObservations: SocialMessageObservationRecord[] = [];
|
|
543
|
+
private messageGovernance: RuntimeMessageGovernance;
|
|
194
544
|
|
|
195
545
|
private receivedCount = 0;
|
|
196
546
|
private broadcastCount = 0;
|
|
197
547
|
private lastMessageAt = 0;
|
|
198
548
|
private lastBroadcastAt = 0;
|
|
549
|
+
private lastBroadcastErrorAt = 0;
|
|
550
|
+
private lastBroadcastError: string | null = null;
|
|
551
|
+
private broadcastFailureCount = 0;
|
|
199
552
|
private broadcaster: NodeJS.Timeout | null = null;
|
|
200
553
|
private subscriptionsBound = false;
|
|
201
554
|
private broadcastEnabled = true;
|
|
202
555
|
|
|
203
556
|
private receivedByTopic: Record<string, number> = {};
|
|
204
557
|
private publishedByTopic: Record<string, number> = {};
|
|
558
|
+
private outboundMessageTimestamps: number[] = [];
|
|
559
|
+
private inboundMessageTimestampsByAgent: Record<string, number[]> = {};
|
|
205
560
|
|
|
206
561
|
private initState: InitState = {
|
|
207
562
|
identity_auto_created: false,
|
|
@@ -212,7 +567,7 @@ class LocalNodeService {
|
|
|
212
567
|
|
|
213
568
|
private network: NetworkAdapter;
|
|
214
569
|
private adapterMode: "mock" | "local-event-bus" | "real-preview" | "webrtc-preview" | "relay-preview";
|
|
215
|
-
private networkMode: "local" | "lan" | "global-preview" = "
|
|
570
|
+
private networkMode: "local" | "lan" | "global-preview" = "global-preview";
|
|
216
571
|
private networkNamespace: string;
|
|
217
572
|
private networkPort: number | null;
|
|
218
573
|
private socialConfig: SocialConfig;
|
|
@@ -232,9 +587,9 @@ class LocalNodeService {
|
|
|
232
587
|
private webrtcBootstrapSources: string[] = [];
|
|
233
588
|
private appVersion = "unknown";
|
|
234
589
|
|
|
235
|
-
constructor() {
|
|
236
|
-
this.workspaceRoot = resolveWorkspaceRoot();
|
|
237
|
-
this.storageRoot = resolveStorageRoot(this.workspaceRoot);
|
|
590
|
+
constructor(options?: { workspaceRoot?: string; storageRoot?: string }) {
|
|
591
|
+
this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
|
|
592
|
+
this.storageRoot = options?.storageRoot || resolveStorageRoot(this.workspaceRoot);
|
|
238
593
|
this.appVersion = readWorkspaceVersion(this.workspaceRoot);
|
|
239
594
|
migrateLegacyDataIfNeeded(this.workspaceRoot, this.storageRoot);
|
|
240
595
|
|
|
@@ -242,7 +597,11 @@ class LocalNodeService {
|
|
|
242
597
|
this.profileRepo = new ProfileRepo(this.storageRoot);
|
|
243
598
|
this.cacheRepo = new CacheRepo(this.storageRoot);
|
|
244
599
|
this.logRepo = new LogRepo(this.storageRoot);
|
|
600
|
+
this.socialMessageGovernanceRepo = new SocialMessageGovernanceRepo(this.storageRoot);
|
|
601
|
+
this.socialMessageRepo = new SocialMessageRepo(this.storageRoot);
|
|
602
|
+
this.socialMessageObservationRepo = new SocialMessageObservationRepo(this.storageRoot);
|
|
245
603
|
this.socialRuntimeRepo = new SocialRuntimeRepo(this.storageRoot);
|
|
604
|
+
this.messageGovernance = this.defaultMessageGovernance();
|
|
246
605
|
|
|
247
606
|
let loadedSocial = loadSocialConfig(this.workspaceRoot);
|
|
248
607
|
if (!loadedSocial.meta.found) {
|
|
@@ -282,7 +641,14 @@ class LocalNodeService {
|
|
|
282
641
|
);
|
|
283
642
|
|
|
284
643
|
if (this.profile?.public_enabled && this.broadcastEnabled) {
|
|
285
|
-
|
|
644
|
+
try {
|
|
645
|
+
await this.broadcastNow("adapter_start");
|
|
646
|
+
} catch (error) {
|
|
647
|
+
await this.log(
|
|
648
|
+
"warn",
|
|
649
|
+
`Initial broadcast failed: ${error instanceof Error ? error.message : String(error)}`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
286
652
|
}
|
|
287
653
|
|
|
288
654
|
this.startBroadcastLoop();
|
|
@@ -321,6 +687,9 @@ class LocalNodeService {
|
|
|
321
687
|
public_enabled: Boolean(this.profile?.public_enabled),
|
|
322
688
|
broadcast_enabled: this.broadcastEnabled,
|
|
323
689
|
last_broadcast_at: this.lastBroadcastAt,
|
|
690
|
+
last_broadcast_error_at: this.lastBroadcastErrorAt,
|
|
691
|
+
last_broadcast_error: this.lastBroadcastError,
|
|
692
|
+
broadcast_failure_count: this.broadcastFailureCount,
|
|
324
693
|
discovered_count: profiles.length,
|
|
325
694
|
online_count: onlineCount,
|
|
326
695
|
offline_count: Math.max(0, profiles.length - onlineCount),
|
|
@@ -332,6 +701,14 @@ class LocalNodeService {
|
|
|
332
701
|
enabled: this.socialConfig.enabled,
|
|
333
702
|
source_path: this.socialSourcePath,
|
|
334
703
|
network_mode: this.networkMode,
|
|
704
|
+
mode_explainer: this.getModeExplainer(),
|
|
705
|
+
governance: {
|
|
706
|
+
send_limit: { max: this.messageGovernance.send_limit_max, window_ms: this.messageGovernance.send_window_ms },
|
|
707
|
+
receive_limit: { max: this.messageGovernance.receive_limit_max, window_ms: this.messageGovernance.receive_window_ms },
|
|
708
|
+
duplicate_window_ms: this.messageGovernance.duplicate_window_ms,
|
|
709
|
+
blocked_agent_count: this.messageGovernance.blocked_agent_ids.length,
|
|
710
|
+
blocked_term_count: this.messageGovernance.blocked_terms.length,
|
|
711
|
+
},
|
|
335
712
|
},
|
|
336
713
|
};
|
|
337
714
|
}
|
|
@@ -346,8 +723,11 @@ class LocalNodeService {
|
|
|
346
723
|
mode: this.networkMode,
|
|
347
724
|
received_count: this.receivedCount,
|
|
348
725
|
broadcast_count: this.broadcastCount,
|
|
726
|
+
broadcast_failure_count: this.broadcastFailureCount,
|
|
349
727
|
last_message_at: this.lastMessageAt,
|
|
350
728
|
last_broadcast_at: this.lastBroadcastAt,
|
|
729
|
+
last_broadcast_error_at: this.lastBroadcastErrorAt,
|
|
730
|
+
last_broadcast_error: this.lastBroadcastError,
|
|
351
731
|
received_by_topic: this.receivedByTopic,
|
|
352
732
|
published_by_topic: this.publishedByTopic,
|
|
353
733
|
peers_discovered: peerCount,
|
|
@@ -445,6 +825,7 @@ class LocalNodeService {
|
|
|
445
825
|
: this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview"
|
|
446
826
|
? "internet-preview"
|
|
447
827
|
: "local-process",
|
|
828
|
+
mode_explainer: this.getModeExplainer(),
|
|
448
829
|
};
|
|
449
830
|
}
|
|
450
831
|
|
|
@@ -459,8 +840,11 @@ class LocalNodeService {
|
|
|
459
840
|
message_counters: {
|
|
460
841
|
received_total: this.receivedCount,
|
|
461
842
|
broadcast_total: this.broadcastCount,
|
|
843
|
+
broadcast_failures_total: this.broadcastFailureCount,
|
|
462
844
|
last_message_at: this.lastMessageAt,
|
|
463
845
|
last_broadcast_at: this.lastBroadcastAt,
|
|
846
|
+
last_broadcast_error_at: this.lastBroadcastErrorAt,
|
|
847
|
+
last_broadcast_error: this.lastBroadcastError,
|
|
464
848
|
received_by_topic: this.receivedByTopic,
|
|
465
849
|
published_by_topic: this.publishedByTopic,
|
|
466
850
|
},
|
|
@@ -825,6 +1209,301 @@ class LocalNodeService {
|
|
|
825
1209
|
return this.identity;
|
|
826
1210
|
}
|
|
827
1211
|
|
|
1212
|
+
getSocialMessages(limit = 50, options?: { agent_id?: string | null }): {
|
|
1213
|
+
total: number;
|
|
1214
|
+
items: SocialMessageView[];
|
|
1215
|
+
governance: {
|
|
1216
|
+
send_limit: { max: number; window_ms: number };
|
|
1217
|
+
receive_limit: { max: number; window_ms: number };
|
|
1218
|
+
duplicate_window_ms: number;
|
|
1219
|
+
blocked_agent_count: number;
|
|
1220
|
+
blocked_term_count: number;
|
|
1221
|
+
};
|
|
1222
|
+
} {
|
|
1223
|
+
const resolvedLimit = Math.max(1, Math.min(200, Number(limit) || 50));
|
|
1224
|
+
this.ensureLocalDirectoryBaseline();
|
|
1225
|
+
this.compactCacheInMemory();
|
|
1226
|
+
const agentId = String(options?.agent_id || "").trim();
|
|
1227
|
+
const filtered = agentId
|
|
1228
|
+
? this.socialMessages.filter((message) => message.agent_id === agentId)
|
|
1229
|
+
: this.socialMessages;
|
|
1230
|
+
return {
|
|
1231
|
+
total: filtered.length,
|
|
1232
|
+
items: filtered.slice(0, resolvedLimit).map((message) => {
|
|
1233
|
+
const profile = this.directory.profiles[message.agent_id];
|
|
1234
|
+
const lastSeenAt = this.directory.presence[message.agent_id] ?? 0;
|
|
1235
|
+
const observations = this.socialMessageObservations.filter((item) => item.message_id === message.message_id);
|
|
1236
|
+
const remoteObservationCount = observations.filter((item) => item.observer_agent_id !== message.agent_id).length;
|
|
1237
|
+
const lastObservedAt = observations.length > 0 ? Math.max(...observations.map((item) => item.observed_at)) : 0;
|
|
1238
|
+
return {
|
|
1239
|
+
...message,
|
|
1240
|
+
display_name: profile?.display_name || message.display_name || "Unnamed",
|
|
1241
|
+
is_self: message.agent_id === this.identity?.agent_id,
|
|
1242
|
+
online: isAgentOnline(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
|
|
1243
|
+
last_seen_at: lastSeenAt || null,
|
|
1244
|
+
observation_count: observations.length,
|
|
1245
|
+
remote_observation_count: remoteObservationCount,
|
|
1246
|
+
last_observed_at: lastObservedAt || null,
|
|
1247
|
+
delivery_status: remoteObservationCount > 0 ? "remote-observed" : "local-only",
|
|
1248
|
+
};
|
|
1249
|
+
}),
|
|
1250
|
+
governance: {
|
|
1251
|
+
send_limit: { max: this.messageGovernance.send_limit_max, window_ms: this.messageGovernance.send_window_ms },
|
|
1252
|
+
receive_limit: { max: this.messageGovernance.receive_limit_max, window_ms: this.messageGovernance.receive_window_ms },
|
|
1253
|
+
duplicate_window_ms: this.messageGovernance.duplicate_window_ms,
|
|
1254
|
+
blocked_agent_count: this.messageGovernance.blocked_agent_ids.length,
|
|
1255
|
+
blocked_term_count: this.messageGovernance.blocked_terms.length,
|
|
1256
|
+
},
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
getOpenClawBridgeStatus(): OpenClawBridgeStatus {
|
|
1261
|
+
const integration = this.getIntegrationStatus();
|
|
1262
|
+
const openclawInstallation = detectOpenClawInstallation(this.workspaceRoot);
|
|
1263
|
+
const openclawRuntime = detectOpenClawRuntime();
|
|
1264
|
+
const skillInstallation = detectOpenClawSkillInstallation();
|
|
1265
|
+
const ownerDelivery = detectOwnerDeliveryStatus({
|
|
1266
|
+
workspaceRoot: this.workspaceRoot,
|
|
1267
|
+
connectedToSilicaclaw: integration.connected_to_silicaclaw,
|
|
1268
|
+
openclawRunning: openclawRuntime.running,
|
|
1269
|
+
skillInstalled: skillInstallation.installed,
|
|
1270
|
+
});
|
|
1271
|
+
return {
|
|
1272
|
+
enabled: this.socialConfig.enabled,
|
|
1273
|
+
connected_to_silicaclaw: integration.connected_to_silicaclaw,
|
|
1274
|
+
public_enabled: integration.public_enabled,
|
|
1275
|
+
message_broadcast_enabled: this.socialConfig.discovery.allow_message_broadcast && this.broadcastEnabled,
|
|
1276
|
+
network_mode: this.networkMode,
|
|
1277
|
+
adapter: this.adapterMode,
|
|
1278
|
+
agent_id: this.identity?.agent_id ?? "",
|
|
1279
|
+
display_name: this.profile?.display_name ?? "",
|
|
1280
|
+
identity_source: this.resolvedIdentitySource,
|
|
1281
|
+
openclaw_identity_source_path: this.resolvedOpenClawIdentityPath,
|
|
1282
|
+
social_source_path: this.socialSourcePath,
|
|
1283
|
+
openclaw_installation: openclawInstallation,
|
|
1284
|
+
openclaw_runtime: openclawRuntime,
|
|
1285
|
+
skill_learning: {
|
|
1286
|
+
available: integration.connected_to_silicaclaw && openclawRuntime.running,
|
|
1287
|
+
installed: skillInstallation.installed,
|
|
1288
|
+
install_mode: skillInstallation.install_mode,
|
|
1289
|
+
installed_skill_path: skillInstallation.workspace_skill_path || skillInstallation.legacy_skill_path,
|
|
1290
|
+
install_action: {
|
|
1291
|
+
supported: true,
|
|
1292
|
+
endpoint: "/api/openclaw/bridge/skill-install",
|
|
1293
|
+
recommended_command: "silicaclaw openclaw-skill-install",
|
|
1294
|
+
},
|
|
1295
|
+
skills: [
|
|
1296
|
+
{
|
|
1297
|
+
key: "get_profile",
|
|
1298
|
+
summary: "Read SilicaClaw identity/profile so OpenClaw can align its runtime persona.",
|
|
1299
|
+
endpoint: "/api/openclaw/bridge/profile",
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
key: "list_messages",
|
|
1303
|
+
summary: "Read recent public broadcast messages observed by this SilicaClaw node.",
|
|
1304
|
+
endpoint: "/api/openclaw/bridge/messages",
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
key: "watch_messages",
|
|
1308
|
+
summary: "Poll the recent broadcast feed so OpenClaw can learn from new public messages.",
|
|
1309
|
+
endpoint: "/api/openclaw/bridge/messages",
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
key: "send_message",
|
|
1313
|
+
summary: "Publish a signed public broadcast through SilicaClaw on behalf of OpenClaw.",
|
|
1314
|
+
endpoint: "/api/openclaw/bridge/message",
|
|
1315
|
+
},
|
|
1316
|
+
],
|
|
1317
|
+
},
|
|
1318
|
+
owner_delivery: ownerDelivery,
|
|
1319
|
+
endpoints: {
|
|
1320
|
+
status: "/api/openclaw/bridge",
|
|
1321
|
+
profile: "/api/openclaw/bridge/profile",
|
|
1322
|
+
messages: "/api/openclaw/bridge/messages",
|
|
1323
|
+
send_message: "/api/openclaw/bridge/message",
|
|
1324
|
+
install_skill: "/api/openclaw/bridge/skill-install",
|
|
1325
|
+
},
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
async installOpenClawSkill() {
|
|
1330
|
+
const scriptPath = resolve(this.workspaceRoot, "scripts", "install-openclaw-skill.mjs");
|
|
1331
|
+
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
|
|
1332
|
+
cwd: this.workspaceRoot,
|
|
1333
|
+
env: process.env,
|
|
1334
|
+
maxBuffer: 1024 * 1024,
|
|
1335
|
+
});
|
|
1336
|
+
const parsed = JSON.parse(String(stdout || "{}"));
|
|
1337
|
+
return {
|
|
1338
|
+
...parsed,
|
|
1339
|
+
bridge: this.getOpenClawBridgeStatus(),
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
getOpenClawBridgeProfile() {
|
|
1344
|
+
return {
|
|
1345
|
+
identity: this.getIdentity(),
|
|
1346
|
+
profile: this.getProfile(),
|
|
1347
|
+
public_profile_preview: this.getPublicProfilePreview(),
|
|
1348
|
+
integration: this.getIntegrationSummary(),
|
|
1349
|
+
bridge: this.getOpenClawBridgeStatus(),
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
getOpenClawBridgeConfig(): OpenClawBridgeConfigView {
|
|
1354
|
+
const homeDir = resolve(process.env.HOME || "", ".openclaw");
|
|
1355
|
+
const workspaceSkillDir = resolve(homeDir, "workspace", "skills");
|
|
1356
|
+
const legacySkillDir = resolve(homeDir, "skills");
|
|
1357
|
+
const openclawSourceDir = resolve(this.workspaceRoot, "..", "openclaw");
|
|
1358
|
+
|
|
1359
|
+
return {
|
|
1360
|
+
bridge_api_base: "http://localhost:4310",
|
|
1361
|
+
openclaw_detected: detectOpenClawInstallation(this.workspaceRoot).detected,
|
|
1362
|
+
openclaw_running: detectOpenClawRuntime().running,
|
|
1363
|
+
openclaw_workspace_skill_dir: workspaceSkillDir,
|
|
1364
|
+
openclaw_legacy_skill_dir: legacySkillDir,
|
|
1365
|
+
silicaclaw_env_template_path: resolve(this.workspaceRoot, "openclaw-owner-forward.env.example"),
|
|
1366
|
+
recommended_skill_name: "silicaclaw-broadcast",
|
|
1367
|
+
recommended_install_command: "silicaclaw openclaw-skill-install",
|
|
1368
|
+
recommended_owner_forward_env: {
|
|
1369
|
+
OPENCLAW_SOURCE_DIR: openclawSourceDir,
|
|
1370
|
+
OPENCLAW_OWNER_CHANNEL: "<channel>",
|
|
1371
|
+
OPENCLAW_OWNER_TARGET: "<target>",
|
|
1372
|
+
OPENCLAW_OWNER_ACCOUNT: "",
|
|
1373
|
+
OPENCLAW_OWNER_FORWARD_CMD: "node scripts/send-to-owner-via-openclaw.mjs",
|
|
1374
|
+
},
|
|
1375
|
+
owner_forward_command_example: [
|
|
1376
|
+
`OPENCLAW_SOURCE_DIR='${openclawSourceDir}'`,
|
|
1377
|
+
"OPENCLAW_OWNER_CHANNEL='<channel>'",
|
|
1378
|
+
"OPENCLAW_OWNER_TARGET='<target>'",
|
|
1379
|
+
"OPENCLAW_OWNER_FORWARD_CMD='node scripts/send-to-owner-via-openclaw.mjs'",
|
|
1380
|
+
"node scripts/owner-forwarder-demo.mjs",
|
|
1381
|
+
].join(" "),
|
|
1382
|
+
notes: [
|
|
1383
|
+
"Install and maintain the skill from SilicaClaw; do not edit OpenClaw core source for this integration.",
|
|
1384
|
+
"OpenClaw learns broadcasts via the installed skill under ~/.openclaw/workspace/skills/.",
|
|
1385
|
+
"Owner delivery runs through OpenClaw's own message channel stack after the skill forwards a summary.",
|
|
1386
|
+
"Sensitive computer control still requires OpenClaw's own owner approval and node permission flow.",
|
|
1387
|
+
],
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
getRuntimeMessageGovernance() {
|
|
1392
|
+
return this.messageGovernance;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
async getMessageGovernanceView() {
|
|
1396
|
+
const logs = await this.logRepo.get();
|
|
1397
|
+
const recentEvents = logs
|
|
1398
|
+
.filter((entry) => (
|
|
1399
|
+
entry.message.includes("Rejected social message") ||
|
|
1400
|
+
entry.message.includes("Social message blocked") ||
|
|
1401
|
+
entry.message.includes("Social message throttled")
|
|
1402
|
+
))
|
|
1403
|
+
.slice(0, 20);
|
|
1404
|
+
|
|
1405
|
+
return {
|
|
1406
|
+
policy: {
|
|
1407
|
+
send_limit: { max: this.messageGovernance.send_limit_max, window_ms: this.messageGovernance.send_window_ms },
|
|
1408
|
+
receive_limit: { max: this.messageGovernance.receive_limit_max, window_ms: this.messageGovernance.receive_window_ms },
|
|
1409
|
+
duplicate_window_ms: this.messageGovernance.duplicate_window_ms,
|
|
1410
|
+
blocked_agent_ids: Array.from(this.messageGovernance.blocked_agent_ids),
|
|
1411
|
+
blocked_terms: Array.from(this.messageGovernance.blocked_terms),
|
|
1412
|
+
},
|
|
1413
|
+
recent_events: recentEvents,
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async updateMessageGovernance(input: Partial<RuntimeMessageGovernance>) {
|
|
1418
|
+
const next: RuntimeMessageGovernance = {
|
|
1419
|
+
send_limit_max: Math.max(1, Math.min(100, Number(input.send_limit_max ?? this.messageGovernance.send_limit_max) || this.messageGovernance.send_limit_max)),
|
|
1420
|
+
send_window_ms: Math.max(5_000, Math.min(3_600_000, Number(input.send_window_ms ?? this.messageGovernance.send_window_ms) || this.messageGovernance.send_window_ms)),
|
|
1421
|
+
receive_limit_max: Math.max(1, Math.min(200, Number(input.receive_limit_max ?? this.messageGovernance.receive_limit_max) || this.messageGovernance.receive_limit_max)),
|
|
1422
|
+
receive_window_ms: Math.max(5_000, Math.min(3_600_000, Number(input.receive_window_ms ?? this.messageGovernance.receive_window_ms) || this.messageGovernance.receive_window_ms)),
|
|
1423
|
+
duplicate_window_ms: Math.max(5_000, Math.min(3_600_000, Number(input.duplicate_window_ms ?? this.messageGovernance.duplicate_window_ms) || this.messageGovernance.duplicate_window_ms)),
|
|
1424
|
+
blocked_agent_ids: dedupeStrings(Array.isArray(input.blocked_agent_ids) ? input.blocked_agent_ids.map((item) => String(item || "").trim()) : this.messageGovernance.blocked_agent_ids),
|
|
1425
|
+
blocked_terms: dedupeStrings(Array.isArray(input.blocked_terms) ? input.blocked_terms.map((item) => String(item || "").trim().toLowerCase()) : this.messageGovernance.blocked_terms),
|
|
1426
|
+
};
|
|
1427
|
+
this.messageGovernance = next;
|
|
1428
|
+
await this.socialMessageGovernanceRepo.set(next);
|
|
1429
|
+
await this.log("info", "Runtime message governance updated");
|
|
1430
|
+
return this.getMessageGovernanceView();
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async sendSocialMessage(body: string, topic = DEFAULT_SOCIAL_MESSAGE_CHANNEL): Promise<{ sent: boolean; reason: string; message?: SocialMessageView; error?: string }> {
|
|
1434
|
+
if (!this.identity || !this.profile) {
|
|
1435
|
+
return { sent: false, reason: "missing_identity_or_profile" };
|
|
1436
|
+
}
|
|
1437
|
+
if (!this.profile.public_enabled) {
|
|
1438
|
+
return { sent: false, reason: "public_disabled" };
|
|
1439
|
+
}
|
|
1440
|
+
if (!this.broadcastEnabled) {
|
|
1441
|
+
return { sent: false, reason: "broadcast_paused" };
|
|
1442
|
+
}
|
|
1443
|
+
if (!this.socialConfig.discovery.allow_message_broadcast) {
|
|
1444
|
+
return { sent: false, reason: "message_broadcast_disabled" };
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const normalizedBody = this.normalizeSocialMessageBody(body);
|
|
1448
|
+
const normalizedTopic = String(topic || DEFAULT_SOCIAL_MESSAGE_CHANNEL).trim() || DEFAULT_SOCIAL_MESSAGE_CHANNEL;
|
|
1449
|
+
if (!normalizedBody) {
|
|
1450
|
+
return { sent: false, reason: "empty_message" };
|
|
1451
|
+
}
|
|
1452
|
+
if (normalizedBody.length > SOCIAL_MESSAGE_MAX_BODY_CHARS) {
|
|
1453
|
+
return { sent: false, reason: "message_too_long" };
|
|
1454
|
+
}
|
|
1455
|
+
if (this.containsBlockedMessageTerm(normalizedBody)) {
|
|
1456
|
+
await this.log("warn", `Social message blocked: blocked_term (${this.identity.agent_id.slice(0, 10)})`);
|
|
1457
|
+
return { sent: false, reason: "blocked_term" };
|
|
1458
|
+
}
|
|
1459
|
+
if (this.isRateLimited(this.outboundMessageTimestamps, this.messageGovernance.send_window_ms, this.messageGovernance.send_limit_max)) {
|
|
1460
|
+
await this.log("warn", `Social message throttled: rate_limited (${this.identity.agent_id.slice(0, 10)})`);
|
|
1461
|
+
return { sent: false, reason: "rate_limited" };
|
|
1462
|
+
}
|
|
1463
|
+
if (this.hasRecentDuplicateMessage(this.identity.agent_id, normalizedBody, normalizedTopic)) {
|
|
1464
|
+
await this.log("warn", `Social message blocked: duplicate_recent_message (${this.identity.agent_id.slice(0, 10)})`);
|
|
1465
|
+
return { sent: false, reason: "duplicate_recent_message" };
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const message = signSocialMessage({
|
|
1469
|
+
identity: this.identity,
|
|
1470
|
+
message_id: createHash("sha256")
|
|
1471
|
+
.update(`${this.identity.agent_id}:${normalizedTopic}:${Date.now()}:${normalizedBody}:${Math.random()}`, "utf8")
|
|
1472
|
+
.digest("hex"),
|
|
1473
|
+
display_name: this.profile.display_name,
|
|
1474
|
+
topic: normalizedTopic,
|
|
1475
|
+
body: normalizedBody,
|
|
1476
|
+
created_at: Date.now(),
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
this.recordTimestamp(this.outboundMessageTimestamps, this.messageGovernance.send_window_ms, message.created_at);
|
|
1480
|
+
this.ingestSocialMessage(message);
|
|
1481
|
+
try {
|
|
1482
|
+
await this.publish(SOCIAL_MESSAGE_TOPIC, message);
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
1485
|
+
this.lastBroadcastErrorAt = Date.now();
|
|
1486
|
+
this.lastBroadcastError = messageText;
|
|
1487
|
+
this.broadcastFailureCount += 1;
|
|
1488
|
+
await this.persistSocialMessages();
|
|
1489
|
+
await this.log("error", `Social message broadcast failed (${message.message_id.slice(0, 10)}): ${messageText}`);
|
|
1490
|
+
return {
|
|
1491
|
+
sent: false,
|
|
1492
|
+
reason: "publish_failed",
|
|
1493
|
+
error: messageText,
|
|
1494
|
+
message: this.getSocialMessages(1).items[0],
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
await this.persistSocialMessages();
|
|
1498
|
+
await this.log("info", `Social message broadcast (${message.message_id.slice(0, 10)})`);
|
|
1499
|
+
|
|
1500
|
+
return {
|
|
1501
|
+
sent: true,
|
|
1502
|
+
reason: "sent",
|
|
1503
|
+
message: this.getSocialMessages(1).items[0],
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
|
|
828
1507
|
async getLogs() {
|
|
829
1508
|
return this.logRepo.get();
|
|
830
1509
|
}
|
|
@@ -935,7 +1614,7 @@ class LocalNodeService {
|
|
|
935
1614
|
return { broadcast_enabled: this.broadcastEnabled };
|
|
936
1615
|
}
|
|
937
1616
|
|
|
938
|
-
async broadcastNow(reason = "manual"): Promise<{ sent: boolean; reason: string }> {
|
|
1617
|
+
async broadcastNow(reason = "manual"): Promise<{ sent: boolean; reason: string; error?: string }> {
|
|
939
1618
|
if (!this.identity || !this.profile) {
|
|
940
1619
|
return { sent: false, reason: "missing_identity_or_profile" };
|
|
941
1620
|
}
|
|
@@ -953,14 +1632,25 @@ class LocalNodeService {
|
|
|
953
1632
|
const presenceRecord = signPresence(this.identity, Date.now());
|
|
954
1633
|
const indexRecords = buildIndexRecords(this.profile);
|
|
955
1634
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1635
|
+
try {
|
|
1636
|
+
await this.publish("profile", profileRecord);
|
|
1637
|
+
await this.publish("presence", presenceRecord);
|
|
1638
|
+
for (const record of indexRecords) {
|
|
1639
|
+
await this.publish("index", record);
|
|
1640
|
+
}
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1643
|
+
this.lastBroadcastErrorAt = Date.now();
|
|
1644
|
+
this.lastBroadcastError = message;
|
|
1645
|
+
this.broadcastFailureCount += 1;
|
|
1646
|
+
await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
|
|
1647
|
+
return { sent: false, reason: "publish_failed", error: message };
|
|
960
1648
|
}
|
|
961
1649
|
|
|
962
1650
|
this.lastBroadcastAt = Date.now();
|
|
963
1651
|
this.broadcastCount += 1;
|
|
1652
|
+
this.lastBroadcastError = null;
|
|
1653
|
+
this.lastBroadcastErrorAt = 0;
|
|
964
1654
|
|
|
965
1655
|
this.directory = ingestProfileRecord(this.directory, profileRecord);
|
|
966
1656
|
this.directory = ingestPresenceRecord(this.directory, presenceRecord);
|
|
@@ -1019,6 +1709,12 @@ class LocalNodeService {
|
|
|
1019
1709
|
await this.profileRepo.set(this.profile);
|
|
1020
1710
|
|
|
1021
1711
|
this.directory = dedupeIndex(await this.cacheRepo.get());
|
|
1712
|
+
this.messageGovernance = {
|
|
1713
|
+
...this.defaultMessageGovernance(),
|
|
1714
|
+
...(await this.socialMessageGovernanceRepo.get()),
|
|
1715
|
+
};
|
|
1716
|
+
this.socialMessages = this.normalizeSocialMessages(await this.socialMessageRepo.get());
|
|
1717
|
+
this.socialMessageObservations = this.normalizeSocialMessageObservations(await this.socialMessageObservationRepo.get());
|
|
1022
1718
|
this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: this.profile });
|
|
1023
1719
|
this.compactCacheInMemory();
|
|
1024
1720
|
await this.persistCache();
|
|
@@ -1100,7 +1796,10 @@ class LocalNodeService {
|
|
|
1100
1796
|
await this.socialRuntimeRepo.set(runtime);
|
|
1101
1797
|
}
|
|
1102
1798
|
|
|
1103
|
-
private async onMessage(
|
|
1799
|
+
private async onMessage(
|
|
1800
|
+
topic: "profile" | "presence" | "index" | "social.message" | "social.message.observation",
|
|
1801
|
+
data: unknown
|
|
1802
|
+
): Promise<void> {
|
|
1104
1803
|
this.receivedCount += 1;
|
|
1105
1804
|
this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
|
|
1106
1805
|
this.lastMessageAt = Date.now();
|
|
@@ -1143,6 +1842,40 @@ class LocalNodeService {
|
|
|
1143
1842
|
return;
|
|
1144
1843
|
}
|
|
1145
1844
|
|
|
1845
|
+
if (topic === SOCIAL_MESSAGE_TOPIC) {
|
|
1846
|
+
const record = this.normalizeIncomingSocialMessage(data);
|
|
1847
|
+
if (!record) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (!verifySocialMessage(record)) {
|
|
1851
|
+
await this.log("warn", `Rejected social message with invalid signature (${record.message_id.slice(0, 10)})`);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
const governanceReason = this.getIncomingSocialMessageRejectionReason(record);
|
|
1855
|
+
if (governanceReason) {
|
|
1856
|
+
await this.log("warn", `Rejected social message (${record.message_id.slice(0, 10)}): ${governanceReason}`);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
this.ingestSocialMessage(record);
|
|
1860
|
+
await this.persistSocialMessages();
|
|
1861
|
+
await this.publishObservationForMessage(record);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (topic === SOCIAL_MESSAGE_OBSERVATION_TOPIC) {
|
|
1866
|
+
const record = this.normalizeIncomingSocialMessageObservation(data);
|
|
1867
|
+
if (!record) {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
if (!verifySocialMessageObservation(record)) {
|
|
1871
|
+
await this.log("warn", `Rejected message observation with invalid signature (${record.observation_id.slice(0, 10)})`);
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
this.ingestSocialMessageObservation(record);
|
|
1875
|
+
await this.persistSocialMessageObservations();
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1146
1879
|
const record = data as IndexRefRecord;
|
|
1147
1880
|
if (!record?.key || !record?.agent_id) {
|
|
1148
1881
|
return;
|
|
@@ -1162,7 +1895,14 @@ class LocalNodeService {
|
|
|
1162
1895
|
}
|
|
1163
1896
|
|
|
1164
1897
|
this.broadcaster = setInterval(async () => {
|
|
1165
|
-
|
|
1898
|
+
try {
|
|
1899
|
+
await this.broadcastNow("interval");
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
await this.log(
|
|
1902
|
+
"warn",
|
|
1903
|
+
`Scheduled broadcast failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1166
1906
|
}, BROADCAST_INTERVAL_MS);
|
|
1167
1907
|
}
|
|
1168
1908
|
|
|
@@ -1179,6 +1919,12 @@ class LocalNodeService {
|
|
|
1179
1919
|
this.network.subscribe("index", (data: IndexRefRecord) => {
|
|
1180
1920
|
this.onMessage("index", data);
|
|
1181
1921
|
});
|
|
1922
|
+
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data: SocialMessageRecord) => {
|
|
1923
|
+
this.onMessage(SOCIAL_MESSAGE_TOPIC, data);
|
|
1924
|
+
});
|
|
1925
|
+
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord) => {
|
|
1926
|
+
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data);
|
|
1927
|
+
});
|
|
1182
1928
|
this.subscriptionsBound = true;
|
|
1183
1929
|
}
|
|
1184
1930
|
|
|
@@ -1302,6 +2048,14 @@ class LocalNodeService {
|
|
|
1302
2048
|
await this.cacheRepo.set(this.directory);
|
|
1303
2049
|
}
|
|
1304
2050
|
|
|
2051
|
+
private async persistSocialMessages(): Promise<void> {
|
|
2052
|
+
await this.socialMessageRepo.set(this.socialMessages);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
private async persistSocialMessageObservations(): Promise<void> {
|
|
2056
|
+
await this.socialMessageObservationRepo.set(this.socialMessageObservations);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
1305
2059
|
private async log(level: "info" | "warn" | "error", message: string): Promise<void> {
|
|
1306
2060
|
await this.logRepo.append({
|
|
1307
2061
|
level,
|
|
@@ -1391,6 +2145,40 @@ class LocalNodeService {
|
|
|
1391
2145
|
return host ? `OpenClaw @ ${host}` : "OpenClaw Agent";
|
|
1392
2146
|
}
|
|
1393
2147
|
|
|
2148
|
+
private getModeExplainer() {
|
|
2149
|
+
if (this.networkMode === "local") {
|
|
2150
|
+
return {
|
|
2151
|
+
mode: "local",
|
|
2152
|
+
short_label: "Local only",
|
|
2153
|
+
summary: "Only nodes inside the same local process bus are visible.",
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
if (this.networkMode === "lan") {
|
|
2157
|
+
return {
|
|
2158
|
+
mode: "lan",
|
|
2159
|
+
short_label: "LAN broadcast",
|
|
2160
|
+
summary: "Uses UDP LAN broadcast. Peers usually need to be on the same local network.",
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
return {
|
|
2164
|
+
mode: "global-preview",
|
|
2165
|
+
short_label: "Relay preview",
|
|
2166
|
+
summary: "Uses the public relay preview room so public nodes can find each other across the internet.",
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
private defaultMessageGovernance(): RuntimeMessageGovernance {
|
|
2171
|
+
return {
|
|
2172
|
+
send_limit_max: SOCIAL_MESSAGE_SEND_MAX_PER_WINDOW,
|
|
2173
|
+
send_window_ms: SOCIAL_MESSAGE_SEND_WINDOW_MS,
|
|
2174
|
+
receive_limit_max: SOCIAL_MESSAGE_RECEIVE_MAX_PER_WINDOW,
|
|
2175
|
+
receive_window_ms: SOCIAL_MESSAGE_RECEIVE_WINDOW_MS,
|
|
2176
|
+
duplicate_window_ms: SOCIAL_MESSAGE_DUPLICATE_WINDOW_MS,
|
|
2177
|
+
blocked_agent_ids: Array.from(SOCIAL_MESSAGE_BLOCKED_AGENT_IDS),
|
|
2178
|
+
blocked_terms: Array.from(SOCIAL_MESSAGE_BLOCKED_TERMS),
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
|
|
1394
2182
|
private adapterForMode(mode: "local" | "lan" | "global-preview"): "local-event-bus" | "real-preview" | "webrtc-preview" | "relay-preview" {
|
|
1395
2183
|
if (mode === "local") return "local-event-bus";
|
|
1396
2184
|
if (mode === "lan") return "real-preview";
|
|
@@ -1400,9 +2188,10 @@ class LocalNodeService {
|
|
|
1400
2188
|
private applyResolvedNetworkConfig(): void {
|
|
1401
2189
|
const modeEnv = String(NETWORK_MODE || "").trim();
|
|
1402
2190
|
const resolvedMode =
|
|
1403
|
-
|
|
2191
|
+
this.socialConfig.network.mode ||
|
|
2192
|
+
(modeEnv === "local" || modeEnv === "lan" || modeEnv === "global-preview"
|
|
1404
2193
|
? modeEnv
|
|
1405
|
-
:
|
|
2194
|
+
: "global-preview");
|
|
1406
2195
|
|
|
1407
2196
|
this.networkMode = resolvedMode;
|
|
1408
2197
|
this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || "silicaclaw.preview";
|
|
@@ -1479,6 +2268,242 @@ class LocalNodeService {
|
|
|
1479
2268
|
this.webrtcBootstrapHints = bootstrapHints;
|
|
1480
2269
|
this.webrtcBootstrapSources = [signalingSource, roomSource, seedPeersSource, bootstrapHintsSource];
|
|
1481
2270
|
}
|
|
2271
|
+
|
|
2272
|
+
private normalizeSocialMessageBody(body: string): string {
|
|
2273
|
+
return String(body || "")
|
|
2274
|
+
.replace(/\r\n/g, "\n")
|
|
2275
|
+
.split("\n")
|
|
2276
|
+
.map((line) => line.trimEnd())
|
|
2277
|
+
.join("\n")
|
|
2278
|
+
.trim();
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
private normalizeWindowTimestamps(timestamps: number[], windowMs: number, now = Date.now()): number[] {
|
|
2282
|
+
return timestamps.filter((timestamp) => now - timestamp <= windowMs);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
private recordTimestamp(timestamps: number[], windowMs: number, at = Date.now()): void {
|
|
2286
|
+
const cleaned = this.normalizeWindowTimestamps(timestamps, windowMs, at);
|
|
2287
|
+
cleaned.push(at);
|
|
2288
|
+
timestamps.splice(0, timestamps.length, ...cleaned);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
private isRateLimited(timestamps: number[], windowMs: number, maxCount: number, now = Date.now()): boolean {
|
|
2292
|
+
const cleaned = this.normalizeWindowTimestamps(timestamps, windowMs, now);
|
|
2293
|
+
timestamps.splice(0, timestamps.length, ...cleaned);
|
|
2294
|
+
return cleaned.length >= maxCount;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
private containsBlockedMessageTerm(body: string): boolean {
|
|
2298
|
+
const normalized = String(body || "").toLowerCase();
|
|
2299
|
+
return this.messageGovernance.blocked_terms.some((term) => normalized.includes(term));
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
private hasRecentDuplicateMessage(agentId: string, body: string, topic: string, now = Date.now()): boolean {
|
|
2303
|
+
return this.socialMessages.some((item) => (
|
|
2304
|
+
item.agent_id === agentId &&
|
|
2305
|
+
item.topic === topic &&
|
|
2306
|
+
item.body === body &&
|
|
2307
|
+
now - item.created_at <= this.messageGovernance.duplicate_window_ms
|
|
2308
|
+
));
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
private getIncomingSocialMessageRejectionReason(record: SocialMessageRecord): string | null {
|
|
2312
|
+
const now = Date.now();
|
|
2313
|
+
if (this.messageGovernance.blocked_agent_ids.includes(record.agent_id)) {
|
|
2314
|
+
return "blocked_agent";
|
|
2315
|
+
}
|
|
2316
|
+
if (this.containsBlockedMessageTerm(record.body)) {
|
|
2317
|
+
return "blocked_term";
|
|
2318
|
+
}
|
|
2319
|
+
if (record.created_at - now > SOCIAL_MESSAGE_MAX_FUTURE_MS) {
|
|
2320
|
+
return "message_from_future";
|
|
2321
|
+
}
|
|
2322
|
+
if (now - record.created_at > SOCIAL_MESSAGE_MAX_AGE_MS) {
|
|
2323
|
+
return "message_too_old";
|
|
2324
|
+
}
|
|
2325
|
+
const timestamps = this.inboundMessageTimestampsByAgent[record.agent_id] || [];
|
|
2326
|
+
this.inboundMessageTimestampsByAgent[record.agent_id] = timestamps;
|
|
2327
|
+
if (this.isRateLimited(timestamps, this.messageGovernance.receive_window_ms, this.messageGovernance.receive_limit_max, now)) {
|
|
2328
|
+
return "remote_rate_limited";
|
|
2329
|
+
}
|
|
2330
|
+
if (this.hasRecentDuplicateMessage(record.agent_id, record.body, record.topic, now)) {
|
|
2331
|
+
return "duplicate_recent_message";
|
|
2332
|
+
}
|
|
2333
|
+
this.recordTimestamp(timestamps, this.messageGovernance.receive_window_ms, now);
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
private canPublishMessageObservation(): boolean {
|
|
2338
|
+
return Boolean(
|
|
2339
|
+
this.identity &&
|
|
2340
|
+
this.profile?.public_enabled &&
|
|
2341
|
+
this.broadcastEnabled &&
|
|
2342
|
+
this.socialConfig.discovery.allow_message_broadcast
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
private async publishObservationForMessage(message: SocialMessageRecord): Promise<void> {
|
|
2347
|
+
if (!this.identity || !this.profile || !this.canPublishMessageObservation()) {
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
if (message.agent_id === this.identity.agent_id) {
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
const existing = this.socialMessageObservations.find((item) => (
|
|
2354
|
+
item.message_id === message.message_id && item.observer_agent_id === this.identity?.agent_id
|
|
2355
|
+
));
|
|
2356
|
+
if (existing) {
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
const observation = signSocialMessageObservation({
|
|
2360
|
+
identity: this.identity,
|
|
2361
|
+
observation_id: createHash("sha256")
|
|
2362
|
+
.update(`${message.message_id}:${this.identity.agent_id}:${Date.now()}`, "utf8")
|
|
2363
|
+
.digest("hex"),
|
|
2364
|
+
message_id: message.message_id,
|
|
2365
|
+
observed_agent_id: message.agent_id,
|
|
2366
|
+
observer_display_name: this.profile.display_name,
|
|
2367
|
+
observed_at: Date.now(),
|
|
2368
|
+
});
|
|
2369
|
+
this.ingestSocialMessageObservation(observation);
|
|
2370
|
+
await this.publish(SOCIAL_MESSAGE_OBSERVATION_TOPIC, observation);
|
|
2371
|
+
await this.persistSocialMessageObservations();
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
private normalizeIncomingSocialMessage(value: unknown): SocialMessageRecord | null {
|
|
2375
|
+
if (typeof value !== "object" || value === null) {
|
|
2376
|
+
return null;
|
|
2377
|
+
}
|
|
2378
|
+
const record = value as Partial<SocialMessageRecord>;
|
|
2379
|
+
const body = this.normalizeSocialMessageBody(String(record.body || ""));
|
|
2380
|
+
const agentId = String(record.agent_id || "").trim();
|
|
2381
|
+
const displayName = String(record.display_name || "").trim();
|
|
2382
|
+
const topic = String(record.topic || DEFAULT_SOCIAL_MESSAGE_CHANNEL).trim() || DEFAULT_SOCIAL_MESSAGE_CHANNEL;
|
|
2383
|
+
const messageId = String(record.message_id || "").trim();
|
|
2384
|
+
const createdAt = Number(record.created_at || 0);
|
|
2385
|
+
if (
|
|
2386
|
+
record.type !== SOCIAL_MESSAGE_TOPIC ||
|
|
2387
|
+
!messageId ||
|
|
2388
|
+
!agentId ||
|
|
2389
|
+
typeof record.public_key !== "string" ||
|
|
2390
|
+
!String(record.public_key).trim() ||
|
|
2391
|
+
!body ||
|
|
2392
|
+
typeof record.signature !== "string" ||
|
|
2393
|
+
!String(record.signature).trim() ||
|
|
2394
|
+
!Number.isFinite(createdAt) ||
|
|
2395
|
+
body.length > SOCIAL_MESSAGE_MAX_BODY_CHARS
|
|
2396
|
+
) {
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
return {
|
|
2400
|
+
type: SOCIAL_MESSAGE_TOPIC,
|
|
2401
|
+
message_id: messageId,
|
|
2402
|
+
agent_id: agentId,
|
|
2403
|
+
public_key: String(record.public_key).trim(),
|
|
2404
|
+
display_name: displayName || "Unnamed",
|
|
2405
|
+
topic,
|
|
2406
|
+
body,
|
|
2407
|
+
created_at: createdAt,
|
|
2408
|
+
signature: String(record.signature).trim(),
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
private normalizeSocialMessages(items: unknown): SocialMessageRecord[] {
|
|
2413
|
+
if (!Array.isArray(items)) {
|
|
2414
|
+
return [];
|
|
2415
|
+
}
|
|
2416
|
+
const deduped = new Set<string>();
|
|
2417
|
+
return items
|
|
2418
|
+
.map((item) => this.normalizeIncomingSocialMessage(item))
|
|
2419
|
+
.filter((item): item is SocialMessageRecord => Boolean(item))
|
|
2420
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
2421
|
+
.filter((item) => {
|
|
2422
|
+
if (deduped.has(item.message_id)) {
|
|
2423
|
+
return false;
|
|
2424
|
+
}
|
|
2425
|
+
deduped.add(item.message_id);
|
|
2426
|
+
return true;
|
|
2427
|
+
})
|
|
2428
|
+
.slice(0, SOCIAL_MESSAGE_HISTORY_LIMIT);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
private normalizeIncomingSocialMessageObservation(value: unknown): SocialMessageObservationRecord | null {
|
|
2432
|
+
if (typeof value !== "object" || value === null) {
|
|
2433
|
+
return null;
|
|
2434
|
+
}
|
|
2435
|
+
const record = value as Partial<SocialMessageObservationRecord>;
|
|
2436
|
+
const observationId = String(record.observation_id || "").trim();
|
|
2437
|
+
const messageId = String(record.message_id || "").trim();
|
|
2438
|
+
const observedAgentId = String(record.observed_agent_id || "").trim();
|
|
2439
|
+
const observerAgentId = String(record.observer_agent_id || "").trim();
|
|
2440
|
+
const observerDisplayName = String(record.observer_display_name || "").trim();
|
|
2441
|
+
const observedAt = Number(record.observed_at || 0);
|
|
2442
|
+
if (
|
|
2443
|
+
record.type !== SOCIAL_MESSAGE_OBSERVATION_TOPIC ||
|
|
2444
|
+
!observationId ||
|
|
2445
|
+
!messageId ||
|
|
2446
|
+
!observedAgentId ||
|
|
2447
|
+
!observerAgentId ||
|
|
2448
|
+
typeof record.observer_public_key !== "string" ||
|
|
2449
|
+
!String(record.observer_public_key).trim() ||
|
|
2450
|
+
typeof record.signature !== "string" ||
|
|
2451
|
+
!String(record.signature).trim() ||
|
|
2452
|
+
!Number.isFinite(observedAt)
|
|
2453
|
+
) {
|
|
2454
|
+
return null;
|
|
2455
|
+
}
|
|
2456
|
+
return {
|
|
2457
|
+
type: SOCIAL_MESSAGE_OBSERVATION_TOPIC,
|
|
2458
|
+
observation_id: observationId,
|
|
2459
|
+
message_id: messageId,
|
|
2460
|
+
observed_agent_id: observedAgentId,
|
|
2461
|
+
observer_agent_id: observerAgentId,
|
|
2462
|
+
observer_public_key: String(record.observer_public_key).trim(),
|
|
2463
|
+
observer_display_name: observerDisplayName || "Unnamed",
|
|
2464
|
+
observed_at: observedAt,
|
|
2465
|
+
signature: String(record.signature).trim(),
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
private normalizeSocialMessageObservations(items: unknown): SocialMessageObservationRecord[] {
|
|
2470
|
+
if (!Array.isArray(items)) {
|
|
2471
|
+
return [];
|
|
2472
|
+
}
|
|
2473
|
+
const deduped = new Set<string>();
|
|
2474
|
+
return items
|
|
2475
|
+
.map((item) => this.normalizeIncomingSocialMessageObservation(item))
|
|
2476
|
+
.filter((item): item is SocialMessageObservationRecord => Boolean(item))
|
|
2477
|
+
.sort((a, b) => b.observed_at - a.observed_at)
|
|
2478
|
+
.filter((item) => {
|
|
2479
|
+
if (deduped.has(item.observation_id)) {
|
|
2480
|
+
return false;
|
|
2481
|
+
}
|
|
2482
|
+
deduped.add(item.observation_id);
|
|
2483
|
+
return true;
|
|
2484
|
+
})
|
|
2485
|
+
.slice(0, SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
private ingestSocialMessage(message: SocialMessageRecord): void {
|
|
2489
|
+
const existing = this.socialMessages.findIndex((item) => item.message_id === message.message_id);
|
|
2490
|
+
if (existing >= 0) {
|
|
2491
|
+
this.socialMessages[existing] = message;
|
|
2492
|
+
} else {
|
|
2493
|
+
this.socialMessages.unshift(message);
|
|
2494
|
+
}
|
|
2495
|
+
this.socialMessages = this.normalizeSocialMessages(this.socialMessages);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
private ingestSocialMessageObservation(observation: SocialMessageObservationRecord): void {
|
|
2499
|
+
const existing = this.socialMessageObservations.findIndex((item) => item.observation_id === observation.observation_id);
|
|
2500
|
+
if (existing >= 0) {
|
|
2501
|
+
this.socialMessageObservations[existing] = observation;
|
|
2502
|
+
} else {
|
|
2503
|
+
this.socialMessageObservations.unshift(observation);
|
|
2504
|
+
}
|
|
2505
|
+
this.socialMessageObservations = this.normalizeSocialMessageObservations(this.socialMessageObservations);
|
|
2506
|
+
}
|
|
1482
2507
|
}
|
|
1483
2508
|
|
|
1484
2509
|
function sendOk<T>(res: Response, data: T, meta?: Record<string, unknown>) {
|
|
@@ -1589,7 +2614,7 @@ function renderBootstrapScript(payload: unknown): string {
|
|
|
1589
2614
|
</script>`;
|
|
1590
2615
|
}
|
|
1591
2616
|
|
|
1592
|
-
async function main() {
|
|
2617
|
+
export async function main() {
|
|
1593
2618
|
const app = express();
|
|
1594
2619
|
const port = Number(process.env.PORT || 4310);
|
|
1595
2620
|
const staticDir = resolveLocalConsoleStaticDir();
|
|
@@ -1695,6 +2720,8 @@ async function main() {
|
|
|
1695
2720
|
registerSocialRoutes(app, {
|
|
1696
2721
|
getSocialConfigView: () => node.getSocialConfigView(),
|
|
1697
2722
|
getIntegrationSummary: () => node.getIntegrationSummary(),
|
|
2723
|
+
getMessageGovernanceView: () => node.getMessageGovernanceView(),
|
|
2724
|
+
updateMessageGovernance: (input) => node.updateMessageGovernance(input),
|
|
1698
2725
|
exportSocialTemplate: () => node.exportSocialTemplate(),
|
|
1699
2726
|
setNetworkModeRuntime: (mode) => node.setNetworkModeRuntime(mode),
|
|
1700
2727
|
reloadSocialConfig: () => node.reloadSocialConfig(),
|
|
@@ -1743,6 +2770,73 @@ async function main() {
|
|
|
1743
2770
|
})
|
|
1744
2771
|
);
|
|
1745
2772
|
|
|
2773
|
+
app.post(
|
|
2774
|
+
"/api/messages/broadcast",
|
|
2775
|
+
asyncRoute(async (req, res) => {
|
|
2776
|
+
const body = String(req.body?.body || "");
|
|
2777
|
+
const topic = String(req.body?.topic || DEFAULT_SOCIAL_MESSAGE_CHANNEL);
|
|
2778
|
+
const result = await node.sendSocialMessage(body, topic);
|
|
2779
|
+
sendOk(res, result, {
|
|
2780
|
+
message: result.sent ? "Message broadcast published" : `Message broadcast skipped: ${result.reason}`,
|
|
2781
|
+
});
|
|
2782
|
+
})
|
|
2783
|
+
);
|
|
2784
|
+
|
|
2785
|
+
app.get("/api/messages", (req, res) => {
|
|
2786
|
+
const limit = Number(req.query.limit ?? 50);
|
|
2787
|
+
const agentId = String(req.query.agent_id ?? "").trim();
|
|
2788
|
+
sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
app.get("/api/openclaw/bridge", (_req, res) => {
|
|
2792
|
+
sendOk(res, node.getOpenClawBridgeStatus());
|
|
2793
|
+
});
|
|
2794
|
+
|
|
2795
|
+
app.get("/api/openclaw/bridge/config", (_req, res) => {
|
|
2796
|
+
sendOk(res, node.getOpenClawBridgeConfig());
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2799
|
+
app.get("/api/openclaw/bridge/profile", (_req, res) => {
|
|
2800
|
+
sendOk(res, node.getOpenClawBridgeProfile());
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
app.get("/api/openclaw/bridge/messages", (req, res) => {
|
|
2804
|
+
const limit = Number(req.query.limit ?? 50);
|
|
2805
|
+
const agentId = String(req.query.agent_id ?? "").trim();
|
|
2806
|
+
sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
|
|
2807
|
+
});
|
|
2808
|
+
|
|
2809
|
+
app.post(
|
|
2810
|
+
"/api/openclaw/bridge/message",
|
|
2811
|
+
asyncRoute(async (req, res) => {
|
|
2812
|
+
const body = String(req.body?.body || "");
|
|
2813
|
+
const topic = String(req.body?.topic || DEFAULT_SOCIAL_MESSAGE_CHANNEL);
|
|
2814
|
+
const result = await node.sendSocialMessage(body, topic);
|
|
2815
|
+
sendOk(res, result, {
|
|
2816
|
+
message: result.sent ? "OpenClaw bridge message published" : `OpenClaw bridge message skipped: ${result.reason}`,
|
|
2817
|
+
});
|
|
2818
|
+
})
|
|
2819
|
+
);
|
|
2820
|
+
|
|
2821
|
+
app.post(
|
|
2822
|
+
"/api/openclaw/bridge/skill-install",
|
|
2823
|
+
asyncRoute(async (_req, res) => {
|
|
2824
|
+
try {
|
|
2825
|
+
const result = await node.installOpenClawSkill();
|
|
2826
|
+
sendOk(res, result, {
|
|
2827
|
+
message: "OpenClaw skill installed",
|
|
2828
|
+
});
|
|
2829
|
+
} catch (error) {
|
|
2830
|
+
sendError(
|
|
2831
|
+
res,
|
|
2832
|
+
500,
|
|
2833
|
+
"OPENCLAW_SKILL_INSTALL_FAILED",
|
|
2834
|
+
error instanceof Error ? error.message : "OpenClaw skill install failed"
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
})
|
|
2838
|
+
);
|
|
2839
|
+
|
|
1746
2840
|
app.post(
|
|
1747
2841
|
"/api/cache/refresh",
|
|
1748
2842
|
asyncRoute(async (_req, res) => {
|
|
@@ -1879,8 +2973,10 @@ async function main() {
|
|
|
1879
2973
|
});
|
|
1880
2974
|
}
|
|
1881
2975
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2976
|
+
if (require.main === module) {
|
|
2977
|
+
main().catch((error) => {
|
|
2978
|
+
// eslint-disable-next-line no-console
|
|
2979
|
+
console.error(error);
|
|
2980
|
+
process.exit(1);
|
|
2981
|
+
});
|
|
2982
|
+
}
|