multiclaws 0.3.0

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,708 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MulticlawsService = void 0;
7
+ const node_events_1 = require("node:events");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_http_1 = __importDefault(require("node:http"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const promises_1 = __importDefault(require("node:fs/promises"));
12
+ const tailscale_1 = require("../infra/tailscale");
13
+ const json_store_1 = require("../infra/json-store");
14
+ const express_1 = __importDefault(require("express"));
15
+ const server_1 = require("@a2a-js/sdk/server");
16
+ const express_2 = require("@a2a-js/sdk/server/express");
17
+ const client_1 = require("@a2a-js/sdk/client");
18
+ const a2a_adapter_1 = require("./a2a-adapter");
19
+ const agent_registry_1 = require("./agent-registry");
20
+ const agent_profile_1 = require("./agent-profile");
21
+ const team_store_1 = require("../team/team-store");
22
+ const tracker_1 = require("../task/tracker");
23
+ const zod_1 = require("zod");
24
+ const gateway_client_1 = require("../infra/gateway-client");
25
+ const rate_limiter_1 = require("../infra/rate-limiter");
26
+ /* ------------------------------------------------------------------ */
27
+ /* Service */
28
+ /* ------------------------------------------------------------------ */
29
+ class MulticlawsService extends node_events_1.EventEmitter {
30
+ options;
31
+ started = false;
32
+ httpServer = null;
33
+ agentRegistry;
34
+ teamStore;
35
+ profileStore;
36
+ taskTracker;
37
+ agentExecutor = null;
38
+ a2aRequestHandler = null;
39
+ agentCard = null;
40
+ clientFactory = new client_1.ClientFactory();
41
+ httpRateLimiter = new rate_limiter_1.RateLimiter({ windowMs: 60_000, maxRequests: 60 });
42
+ selfUrl;
43
+ profileDescription = "OpenClaw agent";
44
+ constructor(options) {
45
+ super();
46
+ this.options = options;
47
+ const multiclawsStateDir = node_path_1.default.join(options.stateDir, "multiclaws");
48
+ this.agentRegistry = new agent_registry_1.AgentRegistry(node_path_1.default.join(multiclawsStateDir, "agents.json"));
49
+ this.teamStore = new team_store_1.TeamStore(node_path_1.default.join(multiclawsStateDir, "teams.json"));
50
+ this.profileStore = new agent_profile_1.ProfileStore(node_path_1.default.join(multiclawsStateDir, "profile.json"));
51
+ this.taskTracker = new tracker_1.TaskTracker({
52
+ filePath: node_path_1.default.join(multiclawsStateDir, "tasks.json"),
53
+ });
54
+ const port = options.port ?? 3100;
55
+ // selfUrl resolved later in start() after Tailscale detection; use placeholder for now
56
+ this.selfUrl = options.selfUrl ?? `http://${getLocalIp()}:${port}`;
57
+ }
58
+ async start() {
59
+ if (this.started)
60
+ return;
61
+ // Auto-detect Tailscale if selfUrl not explicitly configured
62
+ if (!this.options.selfUrl) {
63
+ const port = this.options.port ?? 3100;
64
+ // Fast path: Tailscale already active — just read from network interfaces, no subprocess
65
+ const tsIp = (0, tailscale_1.getTailscaleIpFromInterfaces)();
66
+ if (tsIp) {
67
+ this.selfUrl = `http://${tsIp}:${port}`;
68
+ this.log("info", `Tailscale IP detected: ${tsIp}`);
69
+ }
70
+ else {
71
+ // Slow path: Tailscale not active — run full detection and notify user
72
+ const tailscale = await (0, tailscale_1.detectTailscale)();
73
+ if (tailscale.status === "ready") {
74
+ this.selfUrl = `http://${tailscale.ip}:${port}`;
75
+ this.log("info", `Tailscale IP detected: ${tailscale.ip}`);
76
+ }
77
+ else {
78
+ void this.notifyTailscaleSetup(tailscale);
79
+ }
80
+ }
81
+ }
82
+ // Load profile for AgentCard description
83
+ let profile = await this.profileStore.load();
84
+ if (!profile.ownerName?.trim()) {
85
+ profile.ownerName = this.options.displayName ?? node_os_1.default.hostname();
86
+ await this.profileStore.save(profile);
87
+ await this.setPendingProfileReview();
88
+ }
89
+ this.profileDescription = (0, agent_profile_1.renderProfileDescription)(profile);
90
+ const logger = this.options.logger ?? { info: () => { }, warn: () => { }, error: () => { } };
91
+ this.agentExecutor = new a2a_adapter_1.OpenClawAgentExecutor({
92
+ gatewayConfig: this.options.gatewayConfig ?? null,
93
+ taskTracker: this.taskTracker,
94
+ logger,
95
+ });
96
+ this.agentCard = {
97
+ name: this.options.displayName ?? (profile.ownerName || "OpenClaw Agent"),
98
+ description: this.profileDescription,
99
+ url: this.selfUrl,
100
+ version: "0.3.0",
101
+ protocolVersion: "0.2.2",
102
+ defaultInputModes: ["text/plain"],
103
+ defaultOutputModes: ["text/plain"],
104
+ capabilities: { streaming: false, pushNotifications: false },
105
+ skills: [
106
+ {
107
+ id: "general",
108
+ name: "General Task",
109
+ description: "Execute any delegated task via OpenClaw",
110
+ tags: ["task", "delegation", "general"],
111
+ },
112
+ ],
113
+ };
114
+ const taskStore = new server_1.InMemoryTaskStore();
115
+ this.a2aRequestHandler = new server_1.DefaultRequestHandler(this.agentCard, taskStore, this.agentExecutor);
116
+ const app = (0, express_1.default)();
117
+ app.use(express_1.default.json({ limit: "1mb" }));
118
+ // Rate limiting
119
+ app.use((req, res, next) => {
120
+ const clientIp = req.ip ?? req.socket.remoteAddress ?? "unknown";
121
+ if (!this.httpRateLimiter.allow(clientIp)) {
122
+ res.status(429).json({ error: "rate limited" });
123
+ return;
124
+ }
125
+ next();
126
+ });
127
+ // Team + profile REST endpoints
128
+ this.mountTeamRoutes(app);
129
+ // A2A endpoints
130
+ app.use("/.well-known/agent-card.json", (0, express_2.agentCardHandler)({
131
+ agentCardProvider: this.a2aRequestHandler,
132
+ }));
133
+ app.use("/", (0, express_2.jsonRpcHandler)({
134
+ requestHandler: this.a2aRequestHandler,
135
+ userBuilder: express_2.UserBuilder.noAuthentication,
136
+ }));
137
+ const listenPort = this.options.port ?? 3100;
138
+ this.httpServer = node_http_1.default.createServer(app);
139
+ await new Promise((resolve) => this.httpServer.listen(listenPort, "0.0.0.0", resolve));
140
+ this.started = true;
141
+ this.log("info", `multiclaws A2A service listening on :${listenPort}`);
142
+ }
143
+ async stop() {
144
+ if (!this.started)
145
+ return;
146
+ this.started = false;
147
+ this.taskTracker.destroy();
148
+ this.httpRateLimiter.destroy();
149
+ await new Promise((resolve) => {
150
+ if (!this.httpServer) {
151
+ resolve();
152
+ return;
153
+ }
154
+ this.httpServer.close(() => resolve());
155
+ });
156
+ this.httpServer = null;
157
+ }
158
+ updateGatewayConfig(config) {
159
+ this.agentExecutor?.updateGatewayConfig(config);
160
+ }
161
+ /* ---------------------------------------------------------------- */
162
+ /* Agent management */
163
+ /* ---------------------------------------------------------------- */
164
+ async listAgents() {
165
+ return await this.agentRegistry.list();
166
+ }
167
+ async addAgent(params) {
168
+ const normalizedUrl = params.url.replace(/\/+$/, "");
169
+ try {
170
+ const client = await this.clientFactory.createFromUrl(normalizedUrl);
171
+ const card = await client.getAgentCard();
172
+ return await this.agentRegistry.add({
173
+ url: normalizedUrl,
174
+ name: card.name ?? normalizedUrl,
175
+ description: card.description ?? "",
176
+ skills: card.skills?.map((s) => s.name ?? s.id) ?? [],
177
+ apiKey: params.apiKey,
178
+ });
179
+ }
180
+ catch {
181
+ return await this.agentRegistry.add({
182
+ url: normalizedUrl,
183
+ name: normalizedUrl,
184
+ apiKey: params.apiKey,
185
+ });
186
+ }
187
+ }
188
+ async removeAgent(url) {
189
+ return await this.agentRegistry.remove(url);
190
+ }
191
+ /* ---------------------------------------------------------------- */
192
+ /* Task delegation */
193
+ /* ---------------------------------------------------------------- */
194
+ async delegateTask(params) {
195
+ const agentRecord = await this.agentRegistry.get(params.agentUrl);
196
+ if (!agentRecord) {
197
+ return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
198
+ }
199
+ const track = this.taskTracker.create({
200
+ fromPeerId: "local",
201
+ toPeerId: params.agentUrl,
202
+ task: params.task,
203
+ });
204
+ this.taskTracker.update(track.taskId, { status: "running" });
205
+ try {
206
+ const client = await this.createA2AClient(agentRecord);
207
+ const result = await client.sendMessage({
208
+ message: {
209
+ kind: "message",
210
+ role: "user",
211
+ parts: [{ kind: "text", text: params.task }],
212
+ messageId: track.taskId,
213
+ },
214
+ });
215
+ return this.processTaskResult(track.taskId, result);
216
+ }
217
+ catch (err) {
218
+ const errorMsg = err instanceof Error ? err.message : String(err);
219
+ this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
220
+ return { taskId: track.taskId, status: "failed", error: errorMsg };
221
+ }
222
+ }
223
+ getTaskStatus(taskId) {
224
+ return this.taskTracker.get(taskId);
225
+ }
226
+ /* ---------------------------------------------------------------- */
227
+ /* Profile */
228
+ /* ---------------------------------------------------------------- */
229
+ async getProfile() {
230
+ return await this.profileStore.load();
231
+ }
232
+ async setProfile(patch) {
233
+ const profile = await this.profileStore.update(patch);
234
+ this.updateProfileDescription(profile);
235
+ await this.broadcastProfileToTeams();
236
+ return profile;
237
+ }
238
+ updateProfileDescription(profile) {
239
+ this.profileDescription = (0, agent_profile_1.renderProfileDescription)(profile);
240
+ if (this.agentCard) {
241
+ this.agentCard.description = this.profileDescription;
242
+ }
243
+ }
244
+ /* ---------------------------------------------------------------- */
245
+ /* Pending profile review (install / first-run) */
246
+ /* ---------------------------------------------------------------- */
247
+ getPendingReviewPath() {
248
+ return node_path_1.default.join(this.options.stateDir, "multiclaws", "pending-profile-review.json");
249
+ }
250
+ async getPendingProfileReview() {
251
+ const p = this.getPendingReviewPath();
252
+ const data = await (0, json_store_1.readJsonWithFallback)(p, {});
253
+ if (data.pending !== true) {
254
+ return { pending: false };
255
+ }
256
+ const profile = await this.profileStore.load();
257
+ return {
258
+ pending: true,
259
+ profile,
260
+ message: "这是您当前的 MultiClaws 档案,是否需要修改名字、角色、数据源或能力?",
261
+ };
262
+ }
263
+ async setPendingProfileReview() {
264
+ const p = this.getPendingReviewPath();
265
+ await (0, json_store_1.writeJsonAtomically)(p, { pending: true });
266
+ }
267
+ async clearPendingProfileReview() {
268
+ const p = this.getPendingReviewPath();
269
+ try {
270
+ await promises_1.default.unlink(p);
271
+ }
272
+ catch {
273
+ // ignore if missing
274
+ }
275
+ }
276
+ /* ---------------------------------------------------------------- */
277
+ /* Team management */
278
+ /* ---------------------------------------------------------------- */
279
+ async createTeam(name) {
280
+ const team = await this.teamStore.createTeam({
281
+ teamName: name,
282
+ selfUrl: this.selfUrl,
283
+ selfName: this.options.displayName ?? node_os_1.default.hostname(),
284
+ selfDescription: this.profileDescription,
285
+ });
286
+ this.log("info", `team created: ${team.teamId} (${team.teamName})`);
287
+ return team;
288
+ }
289
+ async createInvite(teamId) {
290
+ const team = teamId
291
+ ? await this.teamStore.getTeam(teamId)
292
+ : await this.teamStore.getFirstTeam();
293
+ if (!team)
294
+ throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
295
+ return (0, team_store_1.encodeInvite)(team.teamId, this.selfUrl);
296
+ }
297
+ async joinTeam(inviteCode) {
298
+ const invite = (0, team_store_1.decodeInvite)(inviteCode);
299
+ const seedUrl = invite.u.replace(/\/+$/, "");
300
+ // 1. Fetch member list from seed
301
+ let membersRes;
302
+ try {
303
+ membersRes = await fetch(`${seedUrl}/team/${invite.t}/members`);
304
+ }
305
+ catch (err) {
306
+ throw new Error(`Unable to reach team seed node at ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
307
+ }
308
+ if (!membersRes.ok) {
309
+ throw new Error(`failed to fetch team members from ${seedUrl}: HTTP ${membersRes.status}`);
310
+ }
311
+ const { team: remoteTeam } = (await membersRes.json());
312
+ // 2. Announce self to seed (seed broadcasts to others)
313
+ const selfMember = {
314
+ url: this.selfUrl,
315
+ name: this.options.displayName ?? node_os_1.default.hostname(),
316
+ description: this.profileDescription,
317
+ joinedAtMs: Date.now(),
318
+ };
319
+ let announceRes;
320
+ try {
321
+ announceRes = await fetch(`${seedUrl}/team/${invite.t}/announce`, {
322
+ method: "POST",
323
+ headers: { "Content-Type": "application/json" },
324
+ body: JSON.stringify(selfMember),
325
+ });
326
+ }
327
+ catch (err) {
328
+ throw new Error(`Failed to announce self to seed ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
329
+ }
330
+ if (!announceRes.ok) {
331
+ throw new Error(`failed to announce to seed ${seedUrl}: HTTP ${announceRes.status}`);
332
+ }
333
+ // 3. Store team locally
334
+ const allMembers = [...remoteTeam.members];
335
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
336
+ if (!allMembers.some((m) => m.url.replace(/\/+$/, "") === selfNormalized)) {
337
+ allMembers.push(selfMember);
338
+ }
339
+ const team = {
340
+ teamId: invite.t,
341
+ teamName: remoteTeam.teamName,
342
+ selfUrl: this.selfUrl,
343
+ members: allMembers,
344
+ createdAtMs: Date.now(),
345
+ };
346
+ await this.teamStore.saveTeam(team);
347
+ // 4. Fetch Agent Cards for members without descriptions, then sync to registry
348
+ await this.fetchMemberDescriptions(team);
349
+ await this.syncTeamToRegistry(team);
350
+ this.log("info", `joined team ${team.teamId} (${team.teamName}) with ${allMembers.length} members`);
351
+ return team;
352
+ }
353
+ async leaveTeam(teamId) {
354
+ const team = teamId
355
+ ? await this.teamStore.getTeam(teamId)
356
+ : await this.teamStore.getFirstTeam();
357
+ if (!team)
358
+ throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
359
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
360
+ const selfMember = {
361
+ url: this.selfUrl,
362
+ name: this.options.displayName ?? node_os_1.default.hostname(),
363
+ joinedAtMs: 0,
364
+ };
365
+ const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
366
+ await Promise.allSettled(others.map(async (m) => {
367
+ try {
368
+ await fetch(`${m.url}/team/${team.teamId}/leave`, {
369
+ method: "POST",
370
+ headers: { "Content-Type": "application/json" },
371
+ body: JSON.stringify(selfMember),
372
+ });
373
+ }
374
+ catch {
375
+ this.log("warn", `failed to notify ${m.url} about leaving`);
376
+ }
377
+ }));
378
+ for (const m of others) {
379
+ await this.agentRegistry.remove(m.url);
380
+ }
381
+ await this.teamStore.deleteTeam(team.teamId);
382
+ this.log("info", `left team ${team.teamId}`);
383
+ }
384
+ async listTeamMembers(teamId) {
385
+ const team = teamId
386
+ ? await this.teamStore.getTeam(teamId)
387
+ : await this.teamStore.getFirstTeam();
388
+ if (!team)
389
+ return null;
390
+ return { team, members: team.members };
391
+ }
392
+ /* ---------------------------------------------------------------- */
393
+ /* Team REST routes */
394
+ /* ---------------------------------------------------------------- */
395
+ mountTeamRoutes(app) {
396
+ const announceBodySchema = zod_1.z.object({
397
+ url: zod_1.z.string().trim().min(1),
398
+ name: zod_1.z.string().trim().min(1),
399
+ description: zod_1.z.string().trim().optional(),
400
+ joinedAtMs: zod_1.z.number().optional(),
401
+ });
402
+ const leaveBodySchema = zod_1.z.object({
403
+ url: zod_1.z.string().trim().min(1),
404
+ });
405
+ const profileUpdateBodySchema = zod_1.z.object({
406
+ url: zod_1.z.string().trim().min(1),
407
+ name: zod_1.z.string().trim().optional(),
408
+ description: zod_1.z.string().optional(),
409
+ });
410
+ app.get("/team/:id/members", async (req, res) => {
411
+ try {
412
+ const team = await this.teamStore.getTeam(req.params.id);
413
+ if (!team) {
414
+ res.status(404).json({ error: "team not found" });
415
+ return;
416
+ }
417
+ res.json({ team: { teamName: team.teamName, members: team.members } });
418
+ }
419
+ catch (err) {
420
+ res.status(500).json({ error: String(err) });
421
+ }
422
+ });
423
+ app.post("/team/:id/announce", async (req, res) => {
424
+ try {
425
+ const team = await this.teamStore.getTeam(req.params.id);
426
+ if (!team) {
427
+ res.status(404).json({ error: "team not found" });
428
+ return;
429
+ }
430
+ const parsed = announceBodySchema.safeParse(req.body);
431
+ if (!parsed.success) {
432
+ res.status(400).json({ error: parsed.error.message });
433
+ return;
434
+ }
435
+ const member = parsed.data;
436
+ const normalizedUrl = member.url.replace(/\/+$/, "");
437
+ const alreadyKnown = team.members.some((m) => m.url.replace(/\/+$/, "") === normalizedUrl);
438
+ await this.teamStore.addMember(team.teamId, {
439
+ url: normalizedUrl,
440
+ name: member.name,
441
+ description: member.description,
442
+ joinedAtMs: member.joinedAtMs ?? Date.now(),
443
+ });
444
+ await this.agentRegistry.add({
445
+ url: normalizedUrl,
446
+ name: member.name,
447
+ description: member.description,
448
+ });
449
+ // Broadcast to other members if new
450
+ if (!alreadyKnown) {
451
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
452
+ const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== normalizedUrl &&
453
+ m.url.replace(/\/+$/, "") !== selfNormalized);
454
+ for (const other of others) {
455
+ void this.fetchWithRetry(`${other.url}/team/${team.teamId}/announce`, {
456
+ method: "POST",
457
+ headers: { "Content-Type": "application/json" },
458
+ body: JSON.stringify({
459
+ url: normalizedUrl,
460
+ name: member.name,
461
+ description: member.description,
462
+ joinedAtMs: member.joinedAtMs ?? Date.now(),
463
+ }),
464
+ }).catch(() => {
465
+ this.log("warn", `broadcast to ${other.url} failed`);
466
+ });
467
+ }
468
+ }
469
+ res.json({ ok: true });
470
+ }
471
+ catch (err) {
472
+ res.status(500).json({ error: String(err) });
473
+ }
474
+ });
475
+ app.post("/team/:id/leave", async (req, res) => {
476
+ try {
477
+ const team = await this.teamStore.getTeam(req.params.id);
478
+ if (!team) {
479
+ res.status(404).json({ error: "team not found" });
480
+ return;
481
+ }
482
+ const parsed = leaveBodySchema.safeParse(req.body);
483
+ if (!parsed.success) {
484
+ res.status(400).json({ error: parsed.error.message });
485
+ return;
486
+ }
487
+ const normalizedUrl = parsed.data.url.replace(/\/+$/, "");
488
+ await this.teamStore.removeMember(team.teamId, normalizedUrl);
489
+ await this.agentRegistry.remove(normalizedUrl);
490
+ res.json({ ok: true });
491
+ }
492
+ catch (err) {
493
+ res.status(500).json({ error: String(err) });
494
+ }
495
+ });
496
+ // Profile update broadcast receiver
497
+ app.post("/team/:id/profile-update", async (req, res) => {
498
+ try {
499
+ const team = await this.teamStore.getTeam(req.params.id);
500
+ if (!team) {
501
+ res.status(404).json({ error: "team not found" });
502
+ return;
503
+ }
504
+ const parsed = profileUpdateBodySchema.safeParse(req.body);
505
+ if (!parsed.success) {
506
+ res.status(400).json({ error: parsed.error.message });
507
+ return;
508
+ }
509
+ const { url, name, description } = parsed.data;
510
+ const normalizedUrl = url.replace(/\/+$/, "");
511
+ // Update team member description
512
+ const existing = team.members.find((m) => m.url.replace(/\/+$/, "") === normalizedUrl);
513
+ if (existing) {
514
+ if (name)
515
+ existing.name = name;
516
+ if (description !== undefined)
517
+ existing.description = description;
518
+ await this.teamStore.saveTeam(team);
519
+ }
520
+ // Update agent registry description
521
+ if (description !== undefined) {
522
+ await this.agentRegistry.updateDescription(normalizedUrl, description);
523
+ }
524
+ res.json({ ok: true });
525
+ }
526
+ catch (err) {
527
+ res.status(500).json({ error: String(err) });
528
+ }
529
+ });
530
+ }
531
+ /* ---------------------------------------------------------------- */
532
+ /* Private helpers */
533
+ /* ---------------------------------------------------------------- */
534
+ async broadcastProfileToTeams() {
535
+ const teams = await this.teamStore.listTeams();
536
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
537
+ const displayName = this.options.displayName ?? node_os_1.default.hostname();
538
+ for (const team of teams) {
539
+ // Update self in team store
540
+ await this.teamStore.addMember(team.teamId, {
541
+ url: this.selfUrl,
542
+ name: displayName,
543
+ description: this.profileDescription,
544
+ joinedAtMs: Date.now(),
545
+ });
546
+ // Broadcast to other members
547
+ const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
548
+ for (const member of others) {
549
+ void this.fetchWithRetry(`${member.url}/team/${team.teamId}/profile-update`, {
550
+ method: "POST",
551
+ headers: { "Content-Type": "application/json" },
552
+ body: JSON.stringify({
553
+ url: this.selfUrl,
554
+ name: displayName,
555
+ description: this.profileDescription,
556
+ }),
557
+ }).catch(() => {
558
+ this.log("warn", `profile broadcast to ${member.url} failed`);
559
+ });
560
+ }
561
+ }
562
+ }
563
+ async fetchMemberDescriptions(team) {
564
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
565
+ await Promise.allSettled(team.members
566
+ .filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized && !m.description)
567
+ .map(async (m) => {
568
+ try {
569
+ const client = await this.clientFactory.createFromUrl(m.url);
570
+ const card = await client.getAgentCard();
571
+ if (card.description) {
572
+ m.description = card.description;
573
+ }
574
+ }
575
+ catch {
576
+ this.log("warn", `failed to fetch Agent Card from ${m.url}`);
577
+ }
578
+ }));
579
+ await this.teamStore.saveTeam(team);
580
+ }
581
+ async syncTeamToRegistry(team) {
582
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
583
+ for (const member of team.members) {
584
+ if (member.url.replace(/\/+$/, "") === selfNormalized)
585
+ continue;
586
+ await this.agentRegistry.add({
587
+ url: member.url,
588
+ name: member.name,
589
+ description: member.description,
590
+ });
591
+ }
592
+ }
593
+ async createA2AClient(agent) {
594
+ return await this.clientFactory.createFromUrl(agent.url);
595
+ }
596
+ processTaskResult(trackId, result) {
597
+ if ("status" in result && result.status) {
598
+ const task = result;
599
+ const state = task.status?.state ?? "unknown";
600
+ const output = this.extractArtifactText(task);
601
+ if (state === "completed") {
602
+ this.taskTracker.update(trackId, { status: "completed", result: output });
603
+ }
604
+ else if (state === "failed") {
605
+ this.taskTracker.update(trackId, { status: "failed", error: output || "remote task failed" });
606
+ }
607
+ return { taskId: task.id, output, status: state };
608
+ }
609
+ const msg = result;
610
+ const text = msg.parts
611
+ ?.filter((p) => p.kind === "text")
612
+ .map((p) => p.text)
613
+ .join("\n") ?? "";
614
+ this.taskTracker.update(trackId, { status: "completed", result: text });
615
+ return { taskId: trackId, output: text, status: "completed" };
616
+ }
617
+ extractArtifactText(task) {
618
+ if (!task.artifacts?.length)
619
+ return "";
620
+ return task.artifacts
621
+ .flatMap((a) => a.parts ?? [])
622
+ .filter((p) => p.kind === "text")
623
+ .map((p) => p.text)
624
+ .join("\n");
625
+ }
626
+ async notifyTailscaleSetup(tailscale) {
627
+ let message;
628
+ if (tailscale.status === "needs_auth") {
629
+ message = [
630
+ "🔗 **MultiClaws: Tailscale 登录**",
631
+ "",
632
+ "Tailscale 已安装但未登录,跨网络协作需要完成登录。",
633
+ "",
634
+ `👉 **请在浏览器打开:** ${tailscale.authUrl}`,
635
+ "",
636
+ "登录完成后重启 OpenClaw 即可。",
637
+ "_(局域网内协作无需此步骤,现在即可使用)_",
638
+ ].join("\n");
639
+ }
640
+ else {
641
+ // not_installed or unavailable
642
+ message = [
643
+ "🌐 **MultiClaws: 跨网络协作提示**",
644
+ "",
645
+ "**局域网内已可直接协作,无需任何配置。**",
646
+ "",
647
+ "如需跨网络(不同局域网间)协作,请安装 Tailscale:",
648
+ "https://tailscale.com/download",
649
+ "",
650
+ "安装并登录后重启 OpenClaw,将自动配置跨网络连接。",
651
+ ].join("\n");
652
+ }
653
+ // Send to user via gateway (best-effort, don't throw)
654
+ if (this.options.gatewayConfig) {
655
+ try {
656
+ await (0, gateway_client_1.invokeGatewayTool)({
657
+ gateway: this.options.gatewayConfig,
658
+ tool: "message",
659
+ args: { action: "send", message },
660
+ timeoutMs: 5_000,
661
+ });
662
+ }
663
+ catch {
664
+ // Fallback to log
665
+ this.log("warn", message.replace(/\*\*/g, "").replace(/```[^`]*```/gs, ""));
666
+ }
667
+ }
668
+ }
669
+ /** Fetch with up to 2 retries and exponential backoff. */
670
+ async fetchWithRetry(url, init, retries = 2) {
671
+ let lastError = null;
672
+ for (let attempt = 0; attempt <= retries; attempt++) {
673
+ try {
674
+ const res = await fetch(url, init);
675
+ if (res.ok || attempt === retries)
676
+ return res;
677
+ lastError = new Error(`HTTP ${res.status}`);
678
+ }
679
+ catch (err) {
680
+ lastError = err instanceof Error ? err : new Error(String(err));
681
+ if (attempt === retries)
682
+ break;
683
+ }
684
+ await new Promise((r) => setTimeout(r, 200 * 2 ** attempt));
685
+ }
686
+ throw lastError;
687
+ }
688
+ log(level, message) {
689
+ this.options.logger?.[level]?.(`[multiclaws] ${message}`);
690
+ }
691
+ }
692
+ exports.MulticlawsService = MulticlawsService;
693
+ function getLocalIp() {
694
+ // Prefer Tailscale IP if available
695
+ const tsIp = (0, tailscale_1.getTailscaleIpFromInterfaces)();
696
+ if (tsIp)
697
+ return tsIp;
698
+ const interfaces = node_os_1.default.networkInterfaces();
699
+ for (const addrs of Object.values(interfaces)) {
700
+ if (!addrs)
701
+ continue;
702
+ for (const addr of addrs) {
703
+ if (addr.family === "IPv4" && !addr.internal)
704
+ return addr.address;
705
+ }
706
+ }
707
+ return node_os_1.default.hostname();
708
+ }