@silicaclaw/cli 1.0.0-beta.20 → 1.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/local-console/src/server.ts +41 -21
- package/docs/CLOUDFLARE_RELAY.md +61 -0
- package/package.json +1 -1
- package/packages/core/dist/socialConfig.d.ts +1 -1
- package/packages/core/dist/socialConfig.js +6 -5
- package/packages/core/src/socialConfig.ts +7 -6
- package/packages/network/dist/index.d.ts +1 -0
- package/packages/network/dist/index.js +1 -0
- package/packages/network/dist/relayPreview.d.ts +133 -0
- package/packages/network/dist/relayPreview.js +320 -0
- package/packages/network/src/index.ts +1 -0
- package/packages/network/src/relayPreview.ts +425 -0
- package/packages/storage/dist/socialRuntimeRepo.js +3 -3
- package/packages/storage/src/socialRuntimeRepo.ts +3 -3
- package/scripts/quickstart.sh +9 -9
- package/scripts/silicaclaw-cli.mjs +3 -3
- package/scripts/silicaclaw-gateway.mjs +4 -3
- package/scripts/webrtc-signaling-server.mjs +52 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { NetworkAdapter } from "./types";
|
|
3
|
+
import {
|
|
4
|
+
MessageEnvelopeCodec,
|
|
5
|
+
NetworkMessageEnvelope,
|
|
6
|
+
validateNetworkMessageEnvelope,
|
|
7
|
+
} from "./abstractions/messageEnvelope";
|
|
8
|
+
import { TopicCodec } from "./abstractions/topicCodec";
|
|
9
|
+
import { JsonMessageEnvelopeCodec } from "./codec/jsonMessageEnvelopeCodec";
|
|
10
|
+
import { JsonTopicCodec } from "./codec/jsonTopicCodec";
|
|
11
|
+
|
|
12
|
+
type RelayPreviewOptions = {
|
|
13
|
+
peerId?: string;
|
|
14
|
+
namespace?: string;
|
|
15
|
+
signalingUrl?: string;
|
|
16
|
+
signalingUrls?: string[];
|
|
17
|
+
room?: string;
|
|
18
|
+
seedPeers?: string[];
|
|
19
|
+
bootstrapHints?: string[];
|
|
20
|
+
bootstrapSources?: string[];
|
|
21
|
+
maxMessageBytes?: number;
|
|
22
|
+
pollIntervalMs?: number;
|
|
23
|
+
maxFutureDriftMs?: number;
|
|
24
|
+
maxPastDriftMs?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type RelayPeer = {
|
|
28
|
+
peer_id: string;
|
|
29
|
+
status: "online";
|
|
30
|
+
first_seen_at: number;
|
|
31
|
+
last_seen_at: number;
|
|
32
|
+
messages_seen: number;
|
|
33
|
+
reconnect_attempts: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type RelayDiagnostics = {
|
|
37
|
+
adapter: "relay-preview";
|
|
38
|
+
peer_id: string;
|
|
39
|
+
namespace: string;
|
|
40
|
+
room: string;
|
|
41
|
+
signaling_url: string;
|
|
42
|
+
signaling_endpoints: string[];
|
|
43
|
+
bootstrap_sources: string[];
|
|
44
|
+
seed_peers_count: number;
|
|
45
|
+
bootstrap_hints_count: number;
|
|
46
|
+
discovery_events_total: number;
|
|
47
|
+
last_discovery_event_at: number;
|
|
48
|
+
discovery_events: Array<{
|
|
49
|
+
id: string;
|
|
50
|
+
type: string;
|
|
51
|
+
at: number;
|
|
52
|
+
peer_id?: string;
|
|
53
|
+
endpoint?: string;
|
|
54
|
+
detail?: string;
|
|
55
|
+
}>;
|
|
56
|
+
signaling_messages_sent_total: number;
|
|
57
|
+
signaling_messages_received_total: number;
|
|
58
|
+
reconnect_attempts_total: number;
|
|
59
|
+
active_webrtc_peers: number;
|
|
60
|
+
components: {
|
|
61
|
+
transport: string;
|
|
62
|
+
discovery: string;
|
|
63
|
+
envelope_codec: string;
|
|
64
|
+
topic_codec: string;
|
|
65
|
+
};
|
|
66
|
+
limits: {
|
|
67
|
+
max_message_bytes: number;
|
|
68
|
+
max_future_drift_ms: number;
|
|
69
|
+
max_past_drift_ms: number;
|
|
70
|
+
};
|
|
71
|
+
config: {
|
|
72
|
+
started: boolean;
|
|
73
|
+
topic_handler_count: number;
|
|
74
|
+
poll_interval_ms: number;
|
|
75
|
+
};
|
|
76
|
+
peers: {
|
|
77
|
+
total: number;
|
|
78
|
+
online: number;
|
|
79
|
+
stale: number;
|
|
80
|
+
items: RelayPeer[];
|
|
81
|
+
};
|
|
82
|
+
stats: {
|
|
83
|
+
publish_attempted: number;
|
|
84
|
+
publish_sent: number;
|
|
85
|
+
received_total: number;
|
|
86
|
+
delivered_total: number;
|
|
87
|
+
dropped_malformed: number;
|
|
88
|
+
dropped_oversized: number;
|
|
89
|
+
dropped_namespace_mismatch: number;
|
|
90
|
+
dropped_timestamp_future_drift: number;
|
|
91
|
+
dropped_timestamp_past_drift: number;
|
|
92
|
+
dropped_decode_failed: number;
|
|
93
|
+
dropped_self: number;
|
|
94
|
+
dropped_topic_decode_error: number;
|
|
95
|
+
dropped_handler_error: number;
|
|
96
|
+
signaling_errors: number;
|
|
97
|
+
invalid_signaling_payload_total: number;
|
|
98
|
+
duplicate_sdp_total: number;
|
|
99
|
+
duplicate_ice_total: number;
|
|
100
|
+
start_errors: number;
|
|
101
|
+
stop_errors: number;
|
|
102
|
+
received_validated: number;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function dedupe(values: string[]): string[] {
|
|
107
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class RelayPreviewAdapter implements NetworkAdapter {
|
|
111
|
+
private readonly peerId: string;
|
|
112
|
+
private readonly namespace: string;
|
|
113
|
+
private readonly signalingEndpoints: string[];
|
|
114
|
+
private readonly room: string;
|
|
115
|
+
private readonly seedPeers: string[];
|
|
116
|
+
private readonly bootstrapHints: string[];
|
|
117
|
+
private readonly bootstrapSources: string[];
|
|
118
|
+
private readonly maxMessageBytes: number;
|
|
119
|
+
private readonly pollIntervalMs: number;
|
|
120
|
+
private readonly maxFutureDriftMs: number;
|
|
121
|
+
private readonly maxPastDriftMs: number;
|
|
122
|
+
private readonly envelopeCodec: MessageEnvelopeCodec;
|
|
123
|
+
private readonly topicCodec: TopicCodec;
|
|
124
|
+
|
|
125
|
+
private started = false;
|
|
126
|
+
private poller: NodeJS.Timeout | null = null;
|
|
127
|
+
private handlers = new Map<string, Set<(data: any) => void>>();
|
|
128
|
+
private peers = new Map<string, RelayPeer>();
|
|
129
|
+
private seenMessageIds = new Set<string>();
|
|
130
|
+
private activeEndpoint = "";
|
|
131
|
+
private discoveryEvents: RelayDiagnostics["discovery_events"] = [];
|
|
132
|
+
private discoveryEventsTotal = 0;
|
|
133
|
+
private lastDiscoveryEventAt = 0;
|
|
134
|
+
private signalingMessagesSentTotal = 0;
|
|
135
|
+
private signalingMessagesReceivedTotal = 0;
|
|
136
|
+
private reconnectAttemptsTotal = 0;
|
|
137
|
+
|
|
138
|
+
private stats: RelayDiagnostics["stats"] = {
|
|
139
|
+
publish_attempted: 0,
|
|
140
|
+
publish_sent: 0,
|
|
141
|
+
received_total: 0,
|
|
142
|
+
delivered_total: 0,
|
|
143
|
+
dropped_malformed: 0,
|
|
144
|
+
dropped_oversized: 0,
|
|
145
|
+
dropped_namespace_mismatch: 0,
|
|
146
|
+
dropped_timestamp_future_drift: 0,
|
|
147
|
+
dropped_timestamp_past_drift: 0,
|
|
148
|
+
dropped_decode_failed: 0,
|
|
149
|
+
dropped_self: 0,
|
|
150
|
+
dropped_topic_decode_error: 0,
|
|
151
|
+
dropped_handler_error: 0,
|
|
152
|
+
signaling_errors: 0,
|
|
153
|
+
invalid_signaling_payload_total: 0,
|
|
154
|
+
duplicate_sdp_total: 0,
|
|
155
|
+
duplicate_ice_total: 0,
|
|
156
|
+
start_errors: 0,
|
|
157
|
+
stop_errors: 0,
|
|
158
|
+
received_validated: 0,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
constructor(options: RelayPreviewOptions = {}) {
|
|
162
|
+
this.peerId = options.peerId ?? `peer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
163
|
+
this.namespace = String(options.namespace || "silicaclaw.preview").trim() || "silicaclaw.preview";
|
|
164
|
+
this.signalingEndpoints = dedupe(
|
|
165
|
+
(options.signalingUrls && options.signalingUrls.length > 0
|
|
166
|
+
? options.signalingUrls
|
|
167
|
+
: [options.signalingUrl || "http://localhost:4510"])
|
|
168
|
+
);
|
|
169
|
+
this.activeEndpoint = this.signalingEndpoints[0] || "http://localhost:4510";
|
|
170
|
+
this.room = String(options.room || "silicaclaw-global-preview").trim() || "silicaclaw-global-preview";
|
|
171
|
+
this.seedPeers = dedupe(options.seedPeers || []);
|
|
172
|
+
this.bootstrapHints = dedupe(options.bootstrapHints || []);
|
|
173
|
+
this.bootstrapSources = dedupe(options.bootstrapSources || []);
|
|
174
|
+
this.maxMessageBytes = options.maxMessageBytes ?? 64 * 1024;
|
|
175
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
176
|
+
this.maxFutureDriftMs = options.maxFutureDriftMs ?? 30_000;
|
|
177
|
+
this.maxPastDriftMs = options.maxPastDriftMs ?? 120_000;
|
|
178
|
+
this.envelopeCodec = new JsonMessageEnvelopeCodec();
|
|
179
|
+
this.topicCodec = new JsonTopicCodec();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async start(): Promise<void> {
|
|
183
|
+
if (this.started) return;
|
|
184
|
+
try {
|
|
185
|
+
await this.post("/join", { room: this.room, peer_id: this.peerId });
|
|
186
|
+
this.started = true;
|
|
187
|
+
await this.refreshPeers();
|
|
188
|
+
await this.pollOnce();
|
|
189
|
+
this.poller = setInterval(() => {
|
|
190
|
+
this.pollOnce().catch(() => {});
|
|
191
|
+
}, this.pollIntervalMs);
|
|
192
|
+
this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.stats.start_errors += 1;
|
|
195
|
+
throw new Error(`Relay start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async stop(): Promise<void> {
|
|
200
|
+
if (!this.started) return;
|
|
201
|
+
if (this.poller) {
|
|
202
|
+
clearInterval(this.poller);
|
|
203
|
+
this.poller = null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await this.post("/leave", { room: this.room, peer_id: this.peerId });
|
|
207
|
+
} catch {
|
|
208
|
+
this.stats.stop_errors += 1;
|
|
209
|
+
}
|
|
210
|
+
this.started = false;
|
|
211
|
+
this.recordDiscovery("signaling_disconnected", { endpoint: this.activeEndpoint });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async publish(topic: string, data: any): Promise<void> {
|
|
215
|
+
if (!this.started) return;
|
|
216
|
+
this.stats.publish_attempted += 1;
|
|
217
|
+
const envelope: NetworkMessageEnvelope = {
|
|
218
|
+
version: 1,
|
|
219
|
+
message_id: randomUUID(),
|
|
220
|
+
topic: `${this.namespace}:${topic}`,
|
|
221
|
+
source_peer_id: this.peerId,
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
payload: this.topicCodec.encode(topic, data),
|
|
224
|
+
};
|
|
225
|
+
const raw = this.envelopeCodec.encode(envelope);
|
|
226
|
+
if (raw.length > this.maxMessageBytes) {
|
|
227
|
+
this.stats.dropped_oversized += 1;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
await this.post("/relay/publish", { room: this.room, peer_id: this.peerId, envelope });
|
|
231
|
+
this.stats.publish_sent += 1;
|
|
232
|
+
this.signalingMessagesSentTotal += 1;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
subscribe(topic: string, handler: (data: any) => void): void {
|
|
236
|
+
const key = `${this.namespace}:${topic}`;
|
|
237
|
+
if (!this.handlers.has(key)) {
|
|
238
|
+
this.handlers.set(key, new Set());
|
|
239
|
+
}
|
|
240
|
+
this.handlers.get(key)?.add(handler);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getDiagnostics(): RelayDiagnostics {
|
|
244
|
+
const peerItems = Array.from(this.peers.values()).sort((a, b) => b.last_seen_at - a.last_seen_at);
|
|
245
|
+
return {
|
|
246
|
+
adapter: "relay-preview",
|
|
247
|
+
peer_id: this.peerId,
|
|
248
|
+
namespace: this.namespace,
|
|
249
|
+
room: this.room,
|
|
250
|
+
signaling_url: this.activeEndpoint,
|
|
251
|
+
signaling_endpoints: this.signalingEndpoints,
|
|
252
|
+
bootstrap_sources: this.bootstrapSources,
|
|
253
|
+
seed_peers_count: this.seedPeers.length,
|
|
254
|
+
bootstrap_hints_count: this.bootstrapHints.length,
|
|
255
|
+
discovery_events_total: this.discoveryEventsTotal,
|
|
256
|
+
last_discovery_event_at: this.lastDiscoveryEventAt,
|
|
257
|
+
discovery_events: this.discoveryEvents,
|
|
258
|
+
signaling_messages_sent_total: this.signalingMessagesSentTotal,
|
|
259
|
+
signaling_messages_received_total: this.signalingMessagesReceivedTotal,
|
|
260
|
+
reconnect_attempts_total: this.reconnectAttemptsTotal,
|
|
261
|
+
active_webrtc_peers: peerItems.length,
|
|
262
|
+
components: {
|
|
263
|
+
transport: "HttpRelayTransport",
|
|
264
|
+
discovery: "RelayRoomPeerList",
|
|
265
|
+
envelope_codec: this.envelopeCodec.constructor.name,
|
|
266
|
+
topic_codec: this.topicCodec.constructor.name,
|
|
267
|
+
},
|
|
268
|
+
limits: {
|
|
269
|
+
max_message_bytes: this.maxMessageBytes,
|
|
270
|
+
max_future_drift_ms: this.maxFutureDriftMs,
|
|
271
|
+
max_past_drift_ms: this.maxPastDriftMs,
|
|
272
|
+
},
|
|
273
|
+
config: {
|
|
274
|
+
started: this.started,
|
|
275
|
+
topic_handler_count: this.handlers.size,
|
|
276
|
+
poll_interval_ms: this.pollIntervalMs,
|
|
277
|
+
},
|
|
278
|
+
peers: {
|
|
279
|
+
total: peerItems.length,
|
|
280
|
+
online: peerItems.length,
|
|
281
|
+
stale: 0,
|
|
282
|
+
items: peerItems,
|
|
283
|
+
},
|
|
284
|
+
stats: { ...this.stats },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private async pollOnce(): Promise<void> {
|
|
289
|
+
const payload = await this.get(`/relay/poll?room=${encodeURIComponent(this.room)}&peer_id=${encodeURIComponent(this.peerId)}`);
|
|
290
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
291
|
+
for (const message of messages) {
|
|
292
|
+
this.signalingMessagesReceivedTotal += 1;
|
|
293
|
+
this.onEnvelope(message?.envelope);
|
|
294
|
+
}
|
|
295
|
+
await this.refreshPeers();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async refreshPeers(): Promise<void> {
|
|
299
|
+
const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
|
|
300
|
+
const peerIds = Array.isArray(payload?.peers) ? payload.peers.map((value: unknown) => String(value || "").trim()).filter(Boolean) : [];
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
const next = new Map<string, RelayPeer>();
|
|
303
|
+
for (const peerId of peerIds) {
|
|
304
|
+
if (peerId === this.peerId) continue;
|
|
305
|
+
const existing = this.peers.get(peerId);
|
|
306
|
+
if (!existing) {
|
|
307
|
+
this.recordDiscovery("peer_joined", { peer_id: peerId });
|
|
308
|
+
}
|
|
309
|
+
next.set(peerId, {
|
|
310
|
+
peer_id: peerId,
|
|
311
|
+
status: "online",
|
|
312
|
+
first_seen_at: existing?.first_seen_at ?? now,
|
|
313
|
+
last_seen_at: now,
|
|
314
|
+
messages_seen: existing?.messages_seen ?? 0,
|
|
315
|
+
reconnect_attempts: existing?.reconnect_attempts ?? 0,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
for (const peerId of this.peers.keys()) {
|
|
319
|
+
if (!next.has(peerId)) {
|
|
320
|
+
this.recordDiscovery("peer_removed", { peer_id: peerId });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
this.peers = next;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private onEnvelope(envelope: unknown): void {
|
|
327
|
+
this.stats.received_total += 1;
|
|
328
|
+
const validated = validateNetworkMessageEnvelope(envelope, {
|
|
329
|
+
max_future_drift_ms: this.maxFutureDriftMs,
|
|
330
|
+
max_past_drift_ms: this.maxPastDriftMs,
|
|
331
|
+
});
|
|
332
|
+
if (!validated.ok || !validated.envelope) {
|
|
333
|
+
if (validated.reason === "timestamp_future_drift") {
|
|
334
|
+
this.stats.dropped_timestamp_future_drift += 1;
|
|
335
|
+
} else if (validated.reason === "timestamp_past_drift") {
|
|
336
|
+
this.stats.dropped_timestamp_past_drift += 1;
|
|
337
|
+
} else {
|
|
338
|
+
this.stats.dropped_malformed += 1;
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const message = validated.envelope;
|
|
343
|
+
if (message.source_peer_id === this.peerId) {
|
|
344
|
+
this.stats.dropped_self += 1;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (!message.topic.startsWith(`${this.namespace}:`)) {
|
|
348
|
+
this.stats.dropped_namespace_mismatch += 1;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (this.seenMessageIds.has(message.message_id)) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
this.seenMessageIds.add(message.message_id);
|
|
355
|
+
if (this.seenMessageIds.size > 10000) {
|
|
356
|
+
const first = this.seenMessageIds.values().next().value;
|
|
357
|
+
if (first) this.seenMessageIds.delete(first);
|
|
358
|
+
}
|
|
359
|
+
this.stats.received_validated += 1;
|
|
360
|
+
|
|
361
|
+
const topicKey = message.topic;
|
|
362
|
+
const topic = topicKey.slice(this.namespace.length + 1);
|
|
363
|
+
const handlers = this.handlers.get(topicKey);
|
|
364
|
+
if (!handlers || handlers.size === 0) return;
|
|
365
|
+
|
|
366
|
+
const peer = this.peers.get(message.source_peer_id);
|
|
367
|
+
if (peer) {
|
|
368
|
+
peer.last_seen_at = Date.now();
|
|
369
|
+
peer.messages_seen += 1;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let payload: unknown;
|
|
373
|
+
try {
|
|
374
|
+
payload = this.topicCodec.decode(topic, message.payload);
|
|
375
|
+
} catch {
|
|
376
|
+
this.stats.dropped_topic_decode_error += 1;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
for (const handler of handlers) {
|
|
380
|
+
try {
|
|
381
|
+
handler(payload);
|
|
382
|
+
this.stats.delivered_total += 1;
|
|
383
|
+
} catch {
|
|
384
|
+
this.stats.dropped_handler_error += 1;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private recordDiscovery(type: string, extra: { peer_id?: string; endpoint?: string; detail?: string } = {}): void {
|
|
390
|
+
const event = {
|
|
391
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
392
|
+
type,
|
|
393
|
+
at: Date.now(),
|
|
394
|
+
...extra,
|
|
395
|
+
};
|
|
396
|
+
this.discoveryEvents.unshift(event);
|
|
397
|
+
this.discoveryEvents = this.discoveryEvents.slice(0, 200);
|
|
398
|
+
this.discoveryEventsTotal += 1;
|
|
399
|
+
this.lastDiscoveryEventAt = event.at;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async get(path: string): Promise<any> {
|
|
403
|
+
const endpoint = this.activeEndpoint.replace(/\/+$/, "");
|
|
404
|
+
const response = await fetch(`${endpoint}${path}`);
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
this.stats.signaling_errors += 1;
|
|
407
|
+
throw new Error(`Relay GET failed (${response.status})`);
|
|
408
|
+
}
|
|
409
|
+
return response.json();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private async post(path: string, body: any): Promise<any> {
|
|
413
|
+
const endpoint = this.activeEndpoint.replace(/\/+$/, "");
|
|
414
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
415
|
+
method: "POST",
|
|
416
|
+
headers: { "content-type": "application/json" },
|
|
417
|
+
body: JSON.stringify(body),
|
|
418
|
+
});
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
this.stats.signaling_errors += 1;
|
|
421
|
+
throw new Error(`Relay POST failed (${response.status})`);
|
|
422
|
+
}
|
|
423
|
+
return response.json();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -14,13 +14,13 @@ function emptyRuntime() {
|
|
|
14
14
|
resolved_identity: null,
|
|
15
15
|
resolved_profile: null,
|
|
16
16
|
resolved_network: {
|
|
17
|
-
mode: "
|
|
18
|
-
adapter: "
|
|
17
|
+
mode: "global-preview",
|
|
18
|
+
adapter: "relay-preview",
|
|
19
19
|
namespace: "silicaclaw.preview",
|
|
20
20
|
port: null,
|
|
21
21
|
signaling_url: "http://localhost:4510",
|
|
22
22
|
signaling_urls: [],
|
|
23
|
-
room: "silicaclaw-
|
|
23
|
+
room: "silicaclaw-global-preview",
|
|
24
24
|
seed_peers: [],
|
|
25
25
|
bootstrap_hints: [],
|
|
26
26
|
bootstrap_sources: [],
|
|
@@ -13,13 +13,13 @@ function emptyRuntime(): SocialRuntimeConfig {
|
|
|
13
13
|
resolved_identity: null,
|
|
14
14
|
resolved_profile: null,
|
|
15
15
|
resolved_network: {
|
|
16
|
-
mode: "
|
|
17
|
-
adapter: "
|
|
16
|
+
mode: "global-preview",
|
|
17
|
+
adapter: "relay-preview",
|
|
18
18
|
namespace: "silicaclaw.preview",
|
|
19
19
|
port: null,
|
|
20
20
|
signaling_url: "http://localhost:4510",
|
|
21
21
|
signaling_urls: [],
|
|
22
|
-
room: "silicaclaw-
|
|
22
|
+
room: "silicaclaw-global-preview",
|
|
23
23
|
seed_peers: [],
|
|
24
24
|
bootstrap_hints: [],
|
|
25
25
|
bootstrap_sources: [],
|
package/scripts/quickstart.sh
CHANGED
|
@@ -5,7 +5,7 @@ INVOKE_PWD="${INIT_CWD:-$PWD}"
|
|
|
5
5
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
6
6
|
WORK_DIR="$ROOT_DIR"
|
|
7
7
|
IS_NPX_MODE=0
|
|
8
|
-
DEFAULT_MODE_PICK="${QUICKSTART_DEFAULT_MODE:-
|
|
8
|
+
DEFAULT_MODE_PICK="${QUICKSTART_DEFAULT_MODE:-3}"
|
|
9
9
|
CONNECT_MODE="${QUICKSTART_CONNECT_MODE:-0}"
|
|
10
10
|
|
|
11
11
|
case "$DEFAULT_MODE_PICK" in
|
|
@@ -333,9 +333,9 @@ fi
|
|
|
333
333
|
title "选择网络模式"
|
|
334
334
|
echo "1) local 单机预览(最快)"
|
|
335
335
|
echo "2) lan 局域网预览(A/B 双机)"
|
|
336
|
-
echo "3) global-preview
|
|
337
|
-
echo "提示: 不确定就直接回车(默认
|
|
338
|
-
echo "提示:
|
|
336
|
+
echo "3) global-preview 互联网预览(Relay,推荐)"
|
|
337
|
+
echo "提示: 不确定就直接回车(默认 global-preview)。"
|
|
338
|
+
echo "提示: 互联网场景需要一个所有节点都可访问的 relay/signaling 地址。"
|
|
339
339
|
if [ "$CONNECT_MODE" = "1" ]; then
|
|
340
340
|
MODE_PICK="3"
|
|
341
341
|
echo "connect 模式:已自动选择 global-preview。"
|
|
@@ -347,7 +347,7 @@ fi
|
|
|
347
347
|
NETWORK_MODE="local"
|
|
348
348
|
NETWORK_ADAPTER="local-event-bus"
|
|
349
349
|
WEBRTC_SIGNALING_URL_VALUE=""
|
|
350
|
-
WEBRTC_ROOM_VALUE="silicaclaw-
|
|
350
|
+
WEBRTC_ROOM_VALUE="silicaclaw-global-preview"
|
|
351
351
|
AUTO_START_SIGNALING=0
|
|
352
352
|
|
|
353
353
|
case "$MODE_PICK" in
|
|
@@ -357,7 +357,7 @@ case "$MODE_PICK" in
|
|
|
357
357
|
;;
|
|
358
358
|
3)
|
|
359
359
|
NETWORK_MODE="global-preview"
|
|
360
|
-
NETWORK_ADAPTER="
|
|
360
|
+
NETWORK_ADAPTER="relay-preview"
|
|
361
361
|
PUBLIC_IP="$(detect_public_ip)"
|
|
362
362
|
SIGNALING_DEFAULT="${WEBRTC_SIGNALING_URL:-http://localhost:4510}"
|
|
363
363
|
if [ -n "$PUBLIC_IP" ]; then
|
|
@@ -374,7 +374,7 @@ case "$MODE_PICK" in
|
|
|
374
374
|
read -r -p "请输入 signaling URL(默认 ${SIGNALING_DEFAULT}): " WEBRTC_SIGNALING_URL_INPUT || true
|
|
375
375
|
WEBRTC_SIGNALING_URL_VALUE="${WEBRTC_SIGNALING_URL_INPUT:-$SIGNALING_DEFAULT}"
|
|
376
376
|
if [ -z "${WEBRTC_SIGNALING_URL_VALUE:-}" ]; then
|
|
377
|
-
echo "global-preview
|
|
377
|
+
echo "global-preview 必须提供公网可达的 signaling URL"
|
|
378
378
|
exit 1
|
|
379
379
|
fi
|
|
380
380
|
|
|
@@ -389,8 +389,8 @@ case "$MODE_PICK" in
|
|
|
389
389
|
echo "将尝试以 PORT=$SIGNALING_PORT_VALUE 后台启动 signaling server"
|
|
390
390
|
fi
|
|
391
391
|
|
|
392
|
-
read -r -p "请输入 room(默认 silicaclaw-
|
|
393
|
-
WEBRTC_ROOM_VALUE="${WEBRTC_ROOM_VALUE_INPUT:-silicaclaw-
|
|
392
|
+
read -r -p "请输入 room(默认 silicaclaw-global-preview): " WEBRTC_ROOM_VALUE_INPUT || true
|
|
393
|
+
WEBRTC_ROOM_VALUE="${WEBRTC_ROOM_VALUE_INPUT:-silicaclaw-global-preview}"
|
|
394
394
|
;;
|
|
395
395
|
*)
|
|
396
396
|
NETWORK_MODE="local"
|
|
@@ -101,12 +101,12 @@ function showUpdateGuide(current, latest, beta) {
|
|
|
101
101
|
}
|
|
102
102
|
console.log("");
|
|
103
103
|
console.log("Quick next commands:");
|
|
104
|
-
console.log("1) Start
|
|
105
|
-
console.log(" silicaclaw start --mode=
|
|
104
|
+
console.log("1) Start internet gateway");
|
|
105
|
+
console.log(" silicaclaw start --mode=global-preview --signaling-url=https://your-relay.example");
|
|
106
106
|
console.log("2) Check gateway status");
|
|
107
107
|
console.log(" silicaclaw status");
|
|
108
108
|
console.log("3) npx one-shot (no alias/global install)");
|
|
109
|
-
console.log(" npx -y @silicaclaw/cli@beta start --mode=
|
|
109
|
+
console.log(" npx -y @silicaclaw/cli@beta start --mode=global-preview --signaling-url=https://your-relay.example");
|
|
110
110
|
console.log("");
|
|
111
111
|
|
|
112
112
|
const writableGlobal = canWriteGlobalPrefix();
|
|
@@ -128,7 +128,7 @@ function parseMode(raw) {
|
|
|
128
128
|
|
|
129
129
|
function adapterForMode(mode) {
|
|
130
130
|
if (mode === "lan") return "real-preview";
|
|
131
|
-
if (mode === "global-preview") return "
|
|
131
|
+
if (mode === "global-preview") return "relay-preview";
|
|
132
132
|
return "local-event-bus";
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -185,7 +185,8 @@ Usage:
|
|
|
185
185
|
Notes:
|
|
186
186
|
- Default app dir: current directory; fallback: ~/silicaclaw
|
|
187
187
|
- State dir: .silicaclaw/gateway
|
|
188
|
-
- global-preview
|
|
188
|
+
- global-preview is internet-first and expects a publicly reachable relay/signaling URL
|
|
189
|
+
- global-preview + localhost signaling URL will auto-start signaling server for single-machine testing
|
|
189
190
|
`.trim());
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -309,7 +310,7 @@ async function stopAll() {
|
|
|
309
310
|
function startAll() {
|
|
310
311
|
ensureStateDir();
|
|
311
312
|
|
|
312
|
-
const mode = parseMode(parseFlag("mode", process.env.NETWORK_MODE || "
|
|
313
|
+
const mode = parseMode(parseFlag("mode", process.env.NETWORK_MODE || "global-preview"));
|
|
313
314
|
const adapter = adapterForMode(mode);
|
|
314
315
|
const signalingUrl = parseFlag("signaling-url", process.env.WEBRTC_SIGNALING_URL || "http://localhost:4510");
|
|
315
316
|
const room = parseFlag("room", process.env.WEBRTC_ROOM || "silicaclaw-demo");
|
|
@@ -6,7 +6,7 @@ const port = Number(process.env.PORT || process.env.WEBRTC_SIGNALING_PORT || 451
|
|
|
6
6
|
const PEER_STALE_MS = Number(process.env.WEBRTC_SIGNALING_PEER_STALE_MS || 120000);
|
|
7
7
|
const SIGNAL_DEDUPE_WINDOW_MS = Number(process.env.WEBRTC_SIGNALING_DEDUPE_WINDOW_MS || 60000);
|
|
8
8
|
|
|
9
|
-
/** @type {Map<string, {peers: Map<string, {last_seen_at:number}>, queues: Map<string, any[]>, signal_fingerprints: Map<string, number>}>} */
|
|
9
|
+
/** @type {Map<string, {peers: Map<string, {last_seen_at:number}>, queues: Map<string, any[]>, relay_queues: Map<string, any[]>, signal_fingerprints: Map<string, number>}>} */
|
|
10
10
|
const rooms = new Map();
|
|
11
11
|
|
|
12
12
|
const counters = {
|
|
@@ -24,6 +24,7 @@ function getRoom(roomId) {
|
|
|
24
24
|
rooms.set(id, {
|
|
25
25
|
peers: new Map(),
|
|
26
26
|
queues: new Map(),
|
|
27
|
+
relay_queues: new Map(),
|
|
27
28
|
signal_fingerprints: new Map(),
|
|
28
29
|
});
|
|
29
30
|
}
|
|
@@ -73,6 +74,7 @@ function cleanupRoom(roomId) {
|
|
|
73
74
|
if (peer.last_seen_at < threshold) {
|
|
74
75
|
room.peers.delete(peerId);
|
|
75
76
|
room.queues.delete(peerId);
|
|
77
|
+
room.relay_queues.delete(peerId);
|
|
76
78
|
counters.stale_peers_cleaned_total += 1;
|
|
77
79
|
}
|
|
78
80
|
}
|
|
@@ -96,6 +98,9 @@ function touchPeer(room, peerId) {
|
|
|
96
98
|
if (!room.queues.has(peerId)) {
|
|
97
99
|
room.queues.set(peerId, []);
|
|
98
100
|
}
|
|
101
|
+
if (!room.relay_queues.has(peerId)) {
|
|
102
|
+
room.relay_queues.set(peerId, []);
|
|
103
|
+
}
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
function isValidSignalPayload(body) {
|
|
@@ -168,6 +173,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
168
173
|
return json(res, 200, { ok: true, messages: queue });
|
|
169
174
|
}
|
|
170
175
|
|
|
176
|
+
if (req.method === 'GET' && url.pathname === '/relay/poll') {
|
|
177
|
+
const roomId = String(url.searchParams.get('room') || 'silicaclaw-room');
|
|
178
|
+
const peerId = String(url.searchParams.get('peer_id') || '');
|
|
179
|
+
if (!peerId) {
|
|
180
|
+
counters.invalid_payload_total += 1;
|
|
181
|
+
return json(res, 400, { ok: false, error: 'missing_peer_id' });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const room = getRoom(roomId);
|
|
185
|
+
touchPeer(room, peerId);
|
|
186
|
+
cleanupRoom(roomId);
|
|
187
|
+
|
|
188
|
+
const queue = room.relay_queues.get(peerId) || [];
|
|
189
|
+
room.relay_queues.set(peerId, []);
|
|
190
|
+
return json(res, 200, { ok: true, messages: queue });
|
|
191
|
+
}
|
|
192
|
+
|
|
171
193
|
if (req.method === 'POST' && url.pathname === '/join') {
|
|
172
194
|
const body = await parseBody(req);
|
|
173
195
|
const roomId = String(body.room || 'silicaclaw-room');
|
|
@@ -193,6 +215,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
193
215
|
if (peerId) {
|
|
194
216
|
room.peers.delete(peerId);
|
|
195
217
|
room.queues.delete(peerId);
|
|
218
|
+
room.relay_queues.delete(peerId);
|
|
196
219
|
counters.leave_total += 1;
|
|
197
220
|
} else {
|
|
198
221
|
counters.invalid_payload_total += 1;
|
|
@@ -240,6 +263,34 @@ const server = http.createServer(async (req, res) => {
|
|
|
240
263
|
return json(res, 200, { ok: true });
|
|
241
264
|
}
|
|
242
265
|
|
|
266
|
+
if (req.method === 'POST' && url.pathname === '/relay/publish') {
|
|
267
|
+
const body = await parseBody(req);
|
|
268
|
+
const roomId = String(body.room || 'silicaclaw-room');
|
|
269
|
+
const peerId = String(body.peer_id || '');
|
|
270
|
+
const envelope = body.envelope;
|
|
271
|
+
if (!peerId || typeof envelope !== 'object' || envelope === null) {
|
|
272
|
+
counters.invalid_payload_total += 1;
|
|
273
|
+
return json(res, 400, { ok: false, error: 'invalid_relay_payload' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const room = getRoom(roomId);
|
|
277
|
+
touchPeer(room, peerId);
|
|
278
|
+
|
|
279
|
+
for (const targetPeerId of room.peers.keys()) {
|
|
280
|
+
if (targetPeerId === peerId) continue;
|
|
281
|
+
if (!room.relay_queues.has(targetPeerId)) room.relay_queues.set(targetPeerId, []);
|
|
282
|
+
room.relay_queues.get(targetPeerId).push({
|
|
283
|
+
id: String(body.id || randomUUID()),
|
|
284
|
+
room: roomId,
|
|
285
|
+
from_peer_id: peerId,
|
|
286
|
+
envelope,
|
|
287
|
+
at: now(),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return json(res, 200, { ok: true, delivered_to: Math.max(0, room.peers.size - 1) });
|
|
292
|
+
}
|
|
293
|
+
|
|
243
294
|
return json(res, 404, { ok: false, error: 'not_found' });
|
|
244
295
|
});
|
|
245
296
|
|