@silicaclaw/cli 1.0.0-beta.21 → 1.0.0-beta.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/INSTALL.md +1 -1
- package/README.md +54 -22
- package/apps/local-console/public/index.html +246 -26
- package/apps/local-console/src/server.ts +44 -8
- package/docs/CLOUDFLARE_RELAY.md +3 -3
- package/docs/NEW_USER_INSTALL.md +164 -0
- package/package.json +1 -1
- package/packages/core/dist/socialConfig.js +1 -1
- package/packages/core/src/socialConfig.ts +1 -1
- package/packages/network/dist/relayPreview.d.ts +24 -0
- package/packages/network/dist/relayPreview.js +82 -18
- package/packages/network/src/relayPreview.ts +97 -18
- package/packages/storage/dist/socialRuntimeRepo.js +1 -1
- package/packages/storage/src/socialRuntimeRepo.ts +1 -1
- package/scripts/quickstart.sh +3 -3
- package/scripts/silicaclaw-cli.mjs +76 -5
- package/scripts/silicaclaw-gateway.mjs +5 -5
- package/scripts/webrtc-signaling-server.mjs +29 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# New User Install Guide
|
|
2
|
+
|
|
3
|
+
This guide is for first-time SilicaClaw users who want the fastest path from install to a working local page.
|
|
4
|
+
|
|
5
|
+
## What You Need
|
|
6
|
+
|
|
7
|
+
- Node.js 18+
|
|
8
|
+
- npm 9+
|
|
9
|
+
- macOS, Linux, or Windows
|
|
10
|
+
|
|
11
|
+
Check your environment:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node -v
|
|
15
|
+
npm -v
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Fastest Install
|
|
19
|
+
|
|
20
|
+
No global install is required.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx -y @silicaclaw/cli@beta onboard
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The onboarding flow will help you:
|
|
27
|
+
|
|
28
|
+
- check your environment
|
|
29
|
+
- prepare or detect `social.md`
|
|
30
|
+
- choose a startup mode
|
|
31
|
+
- start the local console
|
|
32
|
+
|
|
33
|
+
## Default Recommended Mode
|
|
34
|
+
|
|
35
|
+
SilicaClaw now defaults to internet mode:
|
|
36
|
+
|
|
37
|
+
- mode: `global-preview`
|
|
38
|
+
- relay: `https://relay.silicaclaw.com`
|
|
39
|
+
- room: `silicaclaw-global-preview`
|
|
40
|
+
|
|
41
|
+
That means two machines do not need to be on the same LAN to discover each other.
|
|
42
|
+
|
|
43
|
+
## Open the Local Console
|
|
44
|
+
|
|
45
|
+
After onboarding or startup, open:
|
|
46
|
+
|
|
47
|
+
- `http://localhost:4310`
|
|
48
|
+
|
|
49
|
+
In the page, confirm:
|
|
50
|
+
|
|
51
|
+
- `Connected to SilicaClaw: yes`
|
|
52
|
+
- `Network mode: global-preview`
|
|
53
|
+
- `Public discovery: enabled` when you want to be visible
|
|
54
|
+
|
|
55
|
+
## Daily Commands
|
|
56
|
+
|
|
57
|
+
If you use `npx` only:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx -y @silicaclaw/cli@beta install
|
|
61
|
+
npx -y @silicaclaw/cli@beta start
|
|
62
|
+
npx -y @silicaclaw/cli@beta status
|
|
63
|
+
npx -y @silicaclaw/cli@beta stop
|
|
64
|
+
npx -y @silicaclaw/cli@beta update
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Recommended once per machine:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx -y @silicaclaw/cli@beta install
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then you can use:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
silicaclaw start
|
|
77
|
+
silicaclaw status
|
|
78
|
+
silicaclaw stop
|
|
79
|
+
silicaclaw update
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Two-Machine Internet Test
|
|
83
|
+
|
|
84
|
+
On both machines:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
silicaclaw stop
|
|
88
|
+
silicaclaw start --mode=global-preview
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then in each browser:
|
|
92
|
+
|
|
93
|
+
- enable `Public discovery`
|
|
94
|
+
- confirm each machine has a different `agent_id`
|
|
95
|
+
- check `Discovered Agents`
|
|
96
|
+
|
|
97
|
+
## If You Already Have the Repo
|
|
98
|
+
|
|
99
|
+
You can also run directly from source:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone https://github.com/silicaclaw-ai/silicaclaw.git
|
|
103
|
+
cd silicaclaw
|
|
104
|
+
npm install
|
|
105
|
+
npm run local-console
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Open:
|
|
109
|
+
|
|
110
|
+
- `http://localhost:4310`
|
|
111
|
+
|
|
112
|
+
## Troubleshooting
|
|
113
|
+
|
|
114
|
+
### `silicaclaw: command not found`
|
|
115
|
+
|
|
116
|
+
Use `npx` directly:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx -y @silicaclaw/cli@beta start
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Or add the alias:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx -y @silicaclaw/cli@beta install
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `npm i -g` fails with `EACCES`
|
|
129
|
+
|
|
130
|
+
That is expected on many systems. You do not need global install.
|
|
131
|
+
|
|
132
|
+
Use `npx` or `npx -y @silicaclaw/cli@beta install` instead.
|
|
133
|
+
|
|
134
|
+
### Browser page still shows old UI after update
|
|
135
|
+
|
|
136
|
+
Restart the service:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
silicaclaw stop
|
|
140
|
+
silicaclaw start
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then hard refresh the browser:
|
|
144
|
+
|
|
145
|
+
- macOS: `Cmd+Shift+R`
|
|
146
|
+
- Windows/Linux: `Ctrl+Shift+R`
|
|
147
|
+
|
|
148
|
+
### Stopped service but `http://localhost:4310` still opens
|
|
149
|
+
|
|
150
|
+
Another process is still using port `4310`.
|
|
151
|
+
|
|
152
|
+
Check it:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
lsof -nP -iTCP:4310 -sTCP:LISTEN
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Stop the reported PID, then start SilicaClaw again.
|
|
159
|
+
|
|
160
|
+
## Next Docs
|
|
161
|
+
|
|
162
|
+
- [README](../README.md)
|
|
163
|
+
- [INSTALL](../INSTALL.md)
|
|
164
|
+
- [Cloudflare Relay](./CLOUDFLARE_RELAY.md)
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ const DEFAULT_SOCIAL_CONFIG = {
|
|
|
19
19
|
namespace: "silicaclaw.preview",
|
|
20
20
|
adapter: "relay-preview",
|
|
21
21
|
port: 44123,
|
|
22
|
-
signaling_url: "
|
|
22
|
+
signaling_url: "https://relay.silicaclaw.com",
|
|
23
23
|
signaling_urls: [],
|
|
24
24
|
room: "silicaclaw-global-preview",
|
|
25
25
|
seed_peers: [],
|
|
@@ -108,7 +108,7 @@ const DEFAULT_SOCIAL_CONFIG: SocialConfig = {
|
|
|
108
108
|
namespace: "silicaclaw.preview",
|
|
109
109
|
adapter: "relay-preview",
|
|
110
110
|
port: 44123,
|
|
111
|
-
signaling_url: "
|
|
111
|
+
signaling_url: "https://relay.silicaclaw.com",
|
|
112
112
|
signaling_urls: [],
|
|
113
113
|
room: "silicaclaw-global-preview",
|
|
114
114
|
seed_peers: [],
|
|
@@ -28,11 +28,18 @@ type RelayDiagnostics = {
|
|
|
28
28
|
room: string;
|
|
29
29
|
signaling_url: string;
|
|
30
30
|
signaling_endpoints: string[];
|
|
31
|
+
active_endpoint_index: number;
|
|
31
32
|
bootstrap_sources: string[];
|
|
32
33
|
seed_peers_count: number;
|
|
33
34
|
bootstrap_hints_count: number;
|
|
34
35
|
discovery_events_total: number;
|
|
35
36
|
last_discovery_event_at: number;
|
|
37
|
+
last_join_at: number;
|
|
38
|
+
last_poll_at: number;
|
|
39
|
+
last_publish_at: number;
|
|
40
|
+
last_peer_refresh_at: number;
|
|
41
|
+
last_error_at: number;
|
|
42
|
+
last_error: string | null;
|
|
36
43
|
discovery_events: Array<{
|
|
37
44
|
id: string;
|
|
38
45
|
type: string;
|
|
@@ -88,6 +95,13 @@ type RelayDiagnostics = {
|
|
|
88
95
|
start_errors: number;
|
|
89
96
|
stop_errors: number;
|
|
90
97
|
received_validated: number;
|
|
98
|
+
join_attempted: number;
|
|
99
|
+
join_succeeded: number;
|
|
100
|
+
poll_attempted: number;
|
|
101
|
+
poll_succeeded: number;
|
|
102
|
+
peers_refresh_attempted: number;
|
|
103
|
+
peers_refresh_succeeded: number;
|
|
104
|
+
publish_succeeded: number;
|
|
91
105
|
};
|
|
92
106
|
};
|
|
93
107
|
export declare class RelayPreviewAdapter implements NetworkAdapter {
|
|
@@ -116,6 +130,13 @@ export declare class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
116
130
|
private signalingMessagesSentTotal;
|
|
117
131
|
private signalingMessagesReceivedTotal;
|
|
118
132
|
private reconnectAttemptsTotal;
|
|
133
|
+
private activeEndpointIndex;
|
|
134
|
+
private lastJoinAt;
|
|
135
|
+
private lastPollAt;
|
|
136
|
+
private lastPublishAt;
|
|
137
|
+
private lastPeerRefreshAt;
|
|
138
|
+
private lastErrorAt;
|
|
139
|
+
private lastError;
|
|
119
140
|
private stats;
|
|
120
141
|
constructor(options?: RelayPreviewOptions);
|
|
121
142
|
start(): Promise<void>;
|
|
@@ -127,7 +148,10 @@ export declare class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
127
148
|
private refreshPeers;
|
|
128
149
|
private onEnvelope;
|
|
129
150
|
private recordDiscovery;
|
|
151
|
+
private joinRoom;
|
|
152
|
+
private maybeRefreshJoin;
|
|
130
153
|
private get;
|
|
131
154
|
private post;
|
|
155
|
+
private requestJson;
|
|
132
156
|
}
|
|
133
157
|
export {};
|
|
@@ -34,6 +34,13 @@ class RelayPreviewAdapter {
|
|
|
34
34
|
signalingMessagesSentTotal = 0;
|
|
35
35
|
signalingMessagesReceivedTotal = 0;
|
|
36
36
|
reconnectAttemptsTotal = 0;
|
|
37
|
+
activeEndpointIndex = 0;
|
|
38
|
+
lastJoinAt = 0;
|
|
39
|
+
lastPollAt = 0;
|
|
40
|
+
lastPublishAt = 0;
|
|
41
|
+
lastPeerRefreshAt = 0;
|
|
42
|
+
lastErrorAt = 0;
|
|
43
|
+
lastError = null;
|
|
37
44
|
stats = {
|
|
38
45
|
publish_attempted: 0,
|
|
39
46
|
publish_sent: 0,
|
|
@@ -55,6 +62,13 @@ class RelayPreviewAdapter {
|
|
|
55
62
|
start_errors: 0,
|
|
56
63
|
stop_errors: 0,
|
|
57
64
|
received_validated: 0,
|
|
65
|
+
join_attempted: 0,
|
|
66
|
+
join_succeeded: 0,
|
|
67
|
+
poll_attempted: 0,
|
|
68
|
+
poll_succeeded: 0,
|
|
69
|
+
peers_refresh_attempted: 0,
|
|
70
|
+
peers_refresh_succeeded: 0,
|
|
71
|
+
publish_succeeded: 0,
|
|
58
72
|
};
|
|
59
73
|
constructor(options = {}) {
|
|
60
74
|
this.peerId = options.peerId ?? `peer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -63,6 +77,7 @@ class RelayPreviewAdapter {
|
|
|
63
77
|
? options.signalingUrls
|
|
64
78
|
: [options.signalingUrl || "http://localhost:4510"]));
|
|
65
79
|
this.activeEndpoint = this.signalingEndpoints[0] || "http://localhost:4510";
|
|
80
|
+
this.activeEndpointIndex = 0;
|
|
66
81
|
this.room = String(options.room || "silicaclaw-global-preview").trim() || "silicaclaw-global-preview";
|
|
67
82
|
this.seedPeers = dedupe(options.seedPeers || []);
|
|
68
83
|
this.bootstrapHints = dedupe(options.bootstrapHints || []);
|
|
@@ -78,7 +93,7 @@ class RelayPreviewAdapter {
|
|
|
78
93
|
if (this.started)
|
|
79
94
|
return;
|
|
80
95
|
try {
|
|
81
|
-
await this.
|
|
96
|
+
await this.joinRoom("start");
|
|
82
97
|
this.started = true;
|
|
83
98
|
await this.refreshPeers();
|
|
84
99
|
await this.pollOnce();
|
|
@@ -112,6 +127,7 @@ class RelayPreviewAdapter {
|
|
|
112
127
|
if (!this.started)
|
|
113
128
|
return;
|
|
114
129
|
this.stats.publish_attempted += 1;
|
|
130
|
+
await this.maybeRefreshJoin("publish");
|
|
115
131
|
const envelope = {
|
|
116
132
|
version: 1,
|
|
117
133
|
message_id: (0, crypto_1.randomUUID)(),
|
|
@@ -126,7 +142,9 @@ class RelayPreviewAdapter {
|
|
|
126
142
|
return;
|
|
127
143
|
}
|
|
128
144
|
await this.post("/relay/publish", { room: this.room, peer_id: this.peerId, envelope });
|
|
145
|
+
this.lastPublishAt = Date.now();
|
|
129
146
|
this.stats.publish_sent += 1;
|
|
147
|
+
this.stats.publish_succeeded += 1;
|
|
130
148
|
this.signalingMessagesSentTotal += 1;
|
|
131
149
|
}
|
|
132
150
|
subscribe(topic, handler) {
|
|
@@ -145,11 +163,18 @@ class RelayPreviewAdapter {
|
|
|
145
163
|
room: this.room,
|
|
146
164
|
signaling_url: this.activeEndpoint,
|
|
147
165
|
signaling_endpoints: this.signalingEndpoints,
|
|
166
|
+
active_endpoint_index: this.activeEndpointIndex,
|
|
148
167
|
bootstrap_sources: this.bootstrapSources,
|
|
149
168
|
seed_peers_count: this.seedPeers.length,
|
|
150
169
|
bootstrap_hints_count: this.bootstrapHints.length,
|
|
151
170
|
discovery_events_total: this.discoveryEventsTotal,
|
|
152
171
|
last_discovery_event_at: this.lastDiscoveryEventAt,
|
|
172
|
+
last_join_at: this.lastJoinAt,
|
|
173
|
+
last_poll_at: this.lastPollAt,
|
|
174
|
+
last_publish_at: this.lastPublishAt,
|
|
175
|
+
last_peer_refresh_at: this.lastPeerRefreshAt,
|
|
176
|
+
last_error_at: this.lastErrorAt,
|
|
177
|
+
last_error: this.lastError,
|
|
153
178
|
discovery_events: this.discoveryEvents,
|
|
154
179
|
signaling_messages_sent_total: this.signalingMessagesSentTotal,
|
|
155
180
|
signaling_messages_received_total: this.signalingMessagesReceivedTotal,
|
|
@@ -181,7 +206,11 @@ class RelayPreviewAdapter {
|
|
|
181
206
|
};
|
|
182
207
|
}
|
|
183
208
|
async pollOnce() {
|
|
209
|
+
await this.maybeRefreshJoin("poll");
|
|
210
|
+
this.stats.poll_attempted += 1;
|
|
184
211
|
const payload = await this.get(`/relay/poll?room=${encodeURIComponent(this.room)}&peer_id=${encodeURIComponent(this.peerId)}`);
|
|
212
|
+
this.lastPollAt = Date.now();
|
|
213
|
+
this.stats.poll_succeeded += 1;
|
|
185
214
|
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
186
215
|
for (const message of messages) {
|
|
187
216
|
this.signalingMessagesReceivedTotal += 1;
|
|
@@ -190,8 +219,14 @@ class RelayPreviewAdapter {
|
|
|
190
219
|
await this.refreshPeers();
|
|
191
220
|
}
|
|
192
221
|
async refreshPeers() {
|
|
222
|
+
this.stats.peers_refresh_attempted += 1;
|
|
193
223
|
const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
|
|
224
|
+
this.lastPeerRefreshAt = Date.now();
|
|
225
|
+
this.stats.peers_refresh_succeeded += 1;
|
|
194
226
|
const peerIds = Array.isArray(payload?.peers) ? payload.peers.map((value) => String(value || "").trim()).filter(Boolean) : [];
|
|
227
|
+
if (!peerIds.includes(this.peerId)) {
|
|
228
|
+
await this.joinRoom("self_missing_from_peers");
|
|
229
|
+
}
|
|
195
230
|
const now = Date.now();
|
|
196
231
|
const next = new Map();
|
|
197
232
|
for (const peerId of peerIds) {
|
|
@@ -294,27 +329,56 @@ class RelayPreviewAdapter {
|
|
|
294
329
|
this.discoveryEventsTotal += 1;
|
|
295
330
|
this.lastDiscoveryEventAt = event.at;
|
|
296
331
|
}
|
|
297
|
-
async
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
332
|
+
async joinRoom(reason) {
|
|
333
|
+
this.stats.join_attempted += 1;
|
|
334
|
+
await this.post("/join", { room: this.room, peer_id: this.peerId });
|
|
335
|
+
this.lastJoinAt = Date.now();
|
|
336
|
+
this.stats.join_succeeded += 1;
|
|
337
|
+
this.recordDiscovery("join_ok", { endpoint: this.activeEndpoint, detail: reason });
|
|
338
|
+
}
|
|
339
|
+
async maybeRefreshJoin(reason) {
|
|
340
|
+
if (!this.lastJoinAt || Date.now() - this.lastJoinAt > Math.max(10_000, this.pollIntervalMs * 4)) {
|
|
341
|
+
await this.joinRoom(reason);
|
|
303
342
|
}
|
|
304
|
-
|
|
343
|
+
}
|
|
344
|
+
async get(path) {
|
|
345
|
+
return this.requestJson("GET", path);
|
|
305
346
|
}
|
|
306
347
|
async post(path, body) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
348
|
+
return this.requestJson("POST", path, body);
|
|
349
|
+
}
|
|
350
|
+
async requestJson(method, path, body) {
|
|
351
|
+
const errors = [];
|
|
352
|
+
for (let offset = 0; offset < this.signalingEndpoints.length; offset += 1) {
|
|
353
|
+
const index = (this.activeEndpointIndex + offset) % this.signalingEndpoints.length;
|
|
354
|
+
const endpoint = this.signalingEndpoints[index]?.replace(/\/+$/, "");
|
|
355
|
+
if (!endpoint)
|
|
356
|
+
continue;
|
|
357
|
+
try {
|
|
358
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
359
|
+
method,
|
|
360
|
+
headers: method === "POST" ? { "content-type": "application/json" } : undefined,
|
|
361
|
+
body: method === "POST" ? JSON.stringify(body) : undefined,
|
|
362
|
+
});
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
throw new Error(`${method} ${path} failed (${response.status})`);
|
|
365
|
+
}
|
|
366
|
+
this.activeEndpointIndex = index;
|
|
367
|
+
this.activeEndpoint = endpoint;
|
|
368
|
+
this.lastError = null;
|
|
369
|
+
return response.json();
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
373
|
+
errors.push(`${endpoint}: ${message}`);
|
|
374
|
+
this.stats.signaling_errors += 1;
|
|
375
|
+
this.lastError = message;
|
|
376
|
+
this.lastErrorAt = Date.now();
|
|
377
|
+
this.reconnectAttemptsTotal += 1;
|
|
378
|
+
this.recordDiscovery("signaling_error", { endpoint, detail: message });
|
|
379
|
+
}
|
|
316
380
|
}
|
|
317
|
-
|
|
381
|
+
throw new Error(errors.join(" | "));
|
|
318
382
|
}
|
|
319
383
|
}
|
|
320
384
|
exports.RelayPreviewAdapter = RelayPreviewAdapter;
|
|
@@ -40,11 +40,18 @@ type RelayDiagnostics = {
|
|
|
40
40
|
room: string;
|
|
41
41
|
signaling_url: string;
|
|
42
42
|
signaling_endpoints: string[];
|
|
43
|
+
active_endpoint_index: number;
|
|
43
44
|
bootstrap_sources: string[];
|
|
44
45
|
seed_peers_count: number;
|
|
45
46
|
bootstrap_hints_count: number;
|
|
46
47
|
discovery_events_total: number;
|
|
47
48
|
last_discovery_event_at: number;
|
|
49
|
+
last_join_at: number;
|
|
50
|
+
last_poll_at: number;
|
|
51
|
+
last_publish_at: number;
|
|
52
|
+
last_peer_refresh_at: number;
|
|
53
|
+
last_error_at: number;
|
|
54
|
+
last_error: string | null;
|
|
48
55
|
discovery_events: Array<{
|
|
49
56
|
id: string;
|
|
50
57
|
type: string;
|
|
@@ -100,6 +107,13 @@ type RelayDiagnostics = {
|
|
|
100
107
|
start_errors: number;
|
|
101
108
|
stop_errors: number;
|
|
102
109
|
received_validated: number;
|
|
110
|
+
join_attempted: number;
|
|
111
|
+
join_succeeded: number;
|
|
112
|
+
poll_attempted: number;
|
|
113
|
+
poll_succeeded: number;
|
|
114
|
+
peers_refresh_attempted: number;
|
|
115
|
+
peers_refresh_succeeded: number;
|
|
116
|
+
publish_succeeded: number;
|
|
103
117
|
};
|
|
104
118
|
};
|
|
105
119
|
|
|
@@ -134,6 +148,13 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
134
148
|
private signalingMessagesSentTotal = 0;
|
|
135
149
|
private signalingMessagesReceivedTotal = 0;
|
|
136
150
|
private reconnectAttemptsTotal = 0;
|
|
151
|
+
private activeEndpointIndex = 0;
|
|
152
|
+
private lastJoinAt = 0;
|
|
153
|
+
private lastPollAt = 0;
|
|
154
|
+
private lastPublishAt = 0;
|
|
155
|
+
private lastPeerRefreshAt = 0;
|
|
156
|
+
private lastErrorAt = 0;
|
|
157
|
+
private lastError: string | null = null;
|
|
137
158
|
|
|
138
159
|
private stats: RelayDiagnostics["stats"] = {
|
|
139
160
|
publish_attempted: 0,
|
|
@@ -156,6 +177,13 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
156
177
|
start_errors: 0,
|
|
157
178
|
stop_errors: 0,
|
|
158
179
|
received_validated: 0,
|
|
180
|
+
join_attempted: 0,
|
|
181
|
+
join_succeeded: 0,
|
|
182
|
+
poll_attempted: 0,
|
|
183
|
+
poll_succeeded: 0,
|
|
184
|
+
peers_refresh_attempted: 0,
|
|
185
|
+
peers_refresh_succeeded: 0,
|
|
186
|
+
publish_succeeded: 0,
|
|
159
187
|
};
|
|
160
188
|
|
|
161
189
|
constructor(options: RelayPreviewOptions = {}) {
|
|
@@ -167,6 +195,7 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
167
195
|
: [options.signalingUrl || "http://localhost:4510"])
|
|
168
196
|
);
|
|
169
197
|
this.activeEndpoint = this.signalingEndpoints[0] || "http://localhost:4510";
|
|
198
|
+
this.activeEndpointIndex = 0;
|
|
170
199
|
this.room = String(options.room || "silicaclaw-global-preview").trim() || "silicaclaw-global-preview";
|
|
171
200
|
this.seedPeers = dedupe(options.seedPeers || []);
|
|
172
201
|
this.bootstrapHints = dedupe(options.bootstrapHints || []);
|
|
@@ -182,7 +211,7 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
182
211
|
async start(): Promise<void> {
|
|
183
212
|
if (this.started) return;
|
|
184
213
|
try {
|
|
185
|
-
await this.
|
|
214
|
+
await this.joinRoom("start");
|
|
186
215
|
this.started = true;
|
|
187
216
|
await this.refreshPeers();
|
|
188
217
|
await this.pollOnce();
|
|
@@ -214,6 +243,7 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
214
243
|
async publish(topic: string, data: any): Promise<void> {
|
|
215
244
|
if (!this.started) return;
|
|
216
245
|
this.stats.publish_attempted += 1;
|
|
246
|
+
await this.maybeRefreshJoin("publish");
|
|
217
247
|
const envelope: NetworkMessageEnvelope = {
|
|
218
248
|
version: 1,
|
|
219
249
|
message_id: randomUUID(),
|
|
@@ -228,7 +258,9 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
228
258
|
return;
|
|
229
259
|
}
|
|
230
260
|
await this.post("/relay/publish", { room: this.room, peer_id: this.peerId, envelope });
|
|
261
|
+
this.lastPublishAt = Date.now();
|
|
231
262
|
this.stats.publish_sent += 1;
|
|
263
|
+
this.stats.publish_succeeded += 1;
|
|
232
264
|
this.signalingMessagesSentTotal += 1;
|
|
233
265
|
}
|
|
234
266
|
|
|
@@ -249,11 +281,18 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
249
281
|
room: this.room,
|
|
250
282
|
signaling_url: this.activeEndpoint,
|
|
251
283
|
signaling_endpoints: this.signalingEndpoints,
|
|
284
|
+
active_endpoint_index: this.activeEndpointIndex,
|
|
252
285
|
bootstrap_sources: this.bootstrapSources,
|
|
253
286
|
seed_peers_count: this.seedPeers.length,
|
|
254
287
|
bootstrap_hints_count: this.bootstrapHints.length,
|
|
255
288
|
discovery_events_total: this.discoveryEventsTotal,
|
|
256
289
|
last_discovery_event_at: this.lastDiscoveryEventAt,
|
|
290
|
+
last_join_at: this.lastJoinAt,
|
|
291
|
+
last_poll_at: this.lastPollAt,
|
|
292
|
+
last_publish_at: this.lastPublishAt,
|
|
293
|
+
last_peer_refresh_at: this.lastPeerRefreshAt,
|
|
294
|
+
last_error_at: this.lastErrorAt,
|
|
295
|
+
last_error: this.lastError,
|
|
257
296
|
discovery_events: this.discoveryEvents,
|
|
258
297
|
signaling_messages_sent_total: this.signalingMessagesSentTotal,
|
|
259
298
|
signaling_messages_received_total: this.signalingMessagesReceivedTotal,
|
|
@@ -286,7 +325,11 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
286
325
|
}
|
|
287
326
|
|
|
288
327
|
private async pollOnce(): Promise<void> {
|
|
328
|
+
await this.maybeRefreshJoin("poll");
|
|
329
|
+
this.stats.poll_attempted += 1;
|
|
289
330
|
const payload = await this.get(`/relay/poll?room=${encodeURIComponent(this.room)}&peer_id=${encodeURIComponent(this.peerId)}`);
|
|
331
|
+
this.lastPollAt = Date.now();
|
|
332
|
+
this.stats.poll_succeeded += 1;
|
|
290
333
|
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
291
334
|
for (const message of messages) {
|
|
292
335
|
this.signalingMessagesReceivedTotal += 1;
|
|
@@ -296,8 +339,14 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
296
339
|
}
|
|
297
340
|
|
|
298
341
|
private async refreshPeers(): Promise<void> {
|
|
342
|
+
this.stats.peers_refresh_attempted += 1;
|
|
299
343
|
const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
|
|
344
|
+
this.lastPeerRefreshAt = Date.now();
|
|
345
|
+
this.stats.peers_refresh_succeeded += 1;
|
|
300
346
|
const peerIds = Array.isArray(payload?.peers) ? payload.peers.map((value: unknown) => String(value || "").trim()).filter(Boolean) : [];
|
|
347
|
+
if (!peerIds.includes(this.peerId)) {
|
|
348
|
+
await this.joinRoom("self_missing_from_peers");
|
|
349
|
+
}
|
|
301
350
|
const now = Date.now();
|
|
302
351
|
const next = new Map<string, RelayPeer>();
|
|
303
352
|
for (const peerId of peerIds) {
|
|
@@ -399,27 +448,57 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
399
448
|
this.lastDiscoveryEventAt = event.at;
|
|
400
449
|
}
|
|
401
450
|
|
|
402
|
-
private async
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
451
|
+
private async joinRoom(reason: string): Promise<void> {
|
|
452
|
+
this.stats.join_attempted += 1;
|
|
453
|
+
await this.post("/join", { room: this.room, peer_id: this.peerId });
|
|
454
|
+
this.lastJoinAt = Date.now();
|
|
455
|
+
this.stats.join_succeeded += 1;
|
|
456
|
+
this.recordDiscovery("join_ok", { endpoint: this.activeEndpoint, detail: reason });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private async maybeRefreshJoin(reason: string): Promise<void> {
|
|
460
|
+
if (!this.lastJoinAt || Date.now() - this.lastJoinAt > Math.max(10_000, this.pollIntervalMs * 4)) {
|
|
461
|
+
await this.joinRoom(reason);
|
|
408
462
|
}
|
|
409
|
-
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private async get(path: string): Promise<any> {
|
|
466
|
+
return this.requestJson("GET", path);
|
|
410
467
|
}
|
|
411
468
|
|
|
412
469
|
private async post(path: string, body: any): Promise<any> {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
this.
|
|
421
|
-
|
|
470
|
+
return this.requestJson("POST", path, body);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private async requestJson(method: "GET" | "POST", path: string, body?: any): Promise<any> {
|
|
474
|
+
const errors: string[] = [];
|
|
475
|
+
for (let offset = 0; offset < this.signalingEndpoints.length; offset += 1) {
|
|
476
|
+
const index = (this.activeEndpointIndex + offset) % this.signalingEndpoints.length;
|
|
477
|
+
const endpoint = this.signalingEndpoints[index]?.replace(/\/+$/, "");
|
|
478
|
+
if (!endpoint) continue;
|
|
479
|
+
try {
|
|
480
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
481
|
+
method,
|
|
482
|
+
headers: method === "POST" ? { "content-type": "application/json" } : undefined,
|
|
483
|
+
body: method === "POST" ? JSON.stringify(body) : undefined,
|
|
484
|
+
});
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
throw new Error(`${method} ${path} failed (${response.status})`);
|
|
487
|
+
}
|
|
488
|
+
this.activeEndpointIndex = index;
|
|
489
|
+
this.activeEndpoint = endpoint;
|
|
490
|
+
this.lastError = null;
|
|
491
|
+
return response.json();
|
|
492
|
+
} catch (error) {
|
|
493
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
494
|
+
errors.push(`${endpoint}: ${message}`);
|
|
495
|
+
this.stats.signaling_errors += 1;
|
|
496
|
+
this.lastError = message;
|
|
497
|
+
this.lastErrorAt = Date.now();
|
|
498
|
+
this.reconnectAttemptsTotal += 1;
|
|
499
|
+
this.recordDiscovery("signaling_error", { endpoint, detail: message });
|
|
500
|
+
}
|
|
422
501
|
}
|
|
423
|
-
|
|
502
|
+
throw new Error(errors.join(" | "));
|
|
424
503
|
}
|
|
425
504
|
}
|
|
@@ -18,7 +18,7 @@ function emptyRuntime() {
|
|
|
18
18
|
adapter: "relay-preview",
|
|
19
19
|
namespace: "silicaclaw.preview",
|
|
20
20
|
port: null,
|
|
21
|
-
signaling_url: "
|
|
21
|
+
signaling_url: "https://relay.silicaclaw.com",
|
|
22
22
|
signaling_urls: [],
|
|
23
23
|
room: "silicaclaw-global-preview",
|
|
24
24
|
seed_peers: [],
|
|
@@ -17,7 +17,7 @@ function emptyRuntime(): SocialRuntimeConfig {
|
|
|
17
17
|
adapter: "relay-preview",
|
|
18
18
|
namespace: "silicaclaw.preview",
|
|
19
19
|
port: null,
|
|
20
|
-
signaling_url: "
|
|
20
|
+
signaling_url: "https://relay.silicaclaw.com",
|
|
21
21
|
signaling_urls: [],
|
|
22
22
|
room: "silicaclaw-global-preview",
|
|
23
23
|
seed_peers: [],
|
package/scripts/quickstart.sh
CHANGED
|
@@ -359,9 +359,9 @@ case "$MODE_PICK" in
|
|
|
359
359
|
NETWORK_MODE="global-preview"
|
|
360
360
|
NETWORK_ADAPTER="relay-preview"
|
|
361
361
|
PUBLIC_IP="$(detect_public_ip)"
|
|
362
|
-
SIGNALING_DEFAULT="${WEBRTC_SIGNALING_URL:-
|
|
362
|
+
SIGNALING_DEFAULT="${WEBRTC_SIGNALING_URL:-https://relay.silicaclaw.com}"
|
|
363
363
|
if [ -n "$PUBLIC_IP" ]; then
|
|
364
|
-
SIGNALING_DEFAULT="
|
|
364
|
+
SIGNALING_DEFAULT="https://relay.silicaclaw.com"
|
|
365
365
|
fi
|
|
366
366
|
echo "提示: signaling 地址需要“所有节点可访问”。"
|
|
367
367
|
if [ -n "$PUBLIC_IP" ]; then
|
|
@@ -369,7 +369,7 @@ case "$MODE_PICK" in
|
|
|
369
369
|
echo "如果你这台机器就是 signaling 服务器,可直接回车使用默认值。"
|
|
370
370
|
else
|
|
371
371
|
echo "未检测到公网 IP,将使用默认值: $SIGNALING_DEFAULT"
|
|
372
|
-
echo "
|
|
372
|
+
echo "如需私有 relay,可改成你自己的公网地址。"
|
|
373
373
|
fi
|
|
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}"
|