@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silicaclaw/cli",
3
- "version": "1.0.0-beta.21",
3
+ "version": "1.0.0-beta.23",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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: "http://localhost:4510",
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: "http://localhost:4510",
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.post("/join", { room: this.room, peer_id: this.peerId });
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 get(path) {
298
- const endpoint = this.activeEndpoint.replace(/\/+$/, "");
299
- const response = await fetch(`${endpoint}${path}`);
300
- if (!response.ok) {
301
- this.stats.signaling_errors += 1;
302
- throw new Error(`Relay GET failed (${response.status})`);
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
- return response.json();
343
+ }
344
+ async get(path) {
345
+ return this.requestJson("GET", path);
305
346
  }
306
347
  async post(path, body) {
307
- const endpoint = this.activeEndpoint.replace(/\/+$/, "");
308
- const response = await fetch(`${endpoint}${path}`, {
309
- method: "POST",
310
- headers: { "content-type": "application/json" },
311
- body: JSON.stringify(body),
312
- });
313
- if (!response.ok) {
314
- this.stats.signaling_errors += 1;
315
- throw new Error(`Relay POST failed (${response.status})`);
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
- return response.json();
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.post("/join", { room: this.room, peer_id: this.peerId });
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 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})`);
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
- return response.json();
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
- 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})`);
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
- return response.json();
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: "http://localhost:4510",
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: "http://localhost:4510",
20
+ signaling_url: "https://relay.silicaclaw.com",
21
21
  signaling_urls: [],
22
22
  room: "silicaclaw-global-preview",
23
23
  seed_peers: [],
@@ -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:-http://localhost:4510}"
362
+ SIGNALING_DEFAULT="${WEBRTC_SIGNALING_URL:-https://relay.silicaclaw.com}"
363
363
  if [ -n "$PUBLIC_IP" ]; then
364
- SIGNALING_DEFAULT="http://$PUBLIC_IP:4510"
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 "如果 signaling 在其他机器,请输入该机器公网地址。"
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}"