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.
- package/README.md +36 -0
- package/README.zh-CN.md +36 -0
- package/dist/gateway/handlers.d.ts +3 -0
- package/dist/gateway/handlers.js +172 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +446 -0
- package/dist/infra/gateway-client.d.ts +27 -0
- package/dist/infra/gateway-client.js +136 -0
- package/dist/infra/json-store.d.ts +4 -0
- package/dist/infra/json-store.js +57 -0
- package/dist/infra/logger.d.ts +13 -0
- package/dist/infra/logger.js +18 -0
- package/dist/infra/rate-limiter.d.ts +19 -0
- package/dist/infra/rate-limiter.js +69 -0
- package/dist/infra/tailscale.d.ts +19 -0
- package/dist/infra/tailscale.js +120 -0
- package/dist/infra/telemetry.d.ts +3 -0
- package/dist/infra/telemetry.js +17 -0
- package/dist/service/a2a-adapter.d.ts +44 -0
- package/dist/service/a2a-adapter.js +208 -0
- package/dist/service/agent-profile.d.ts +13 -0
- package/dist/service/agent-profile.js +38 -0
- package/dist/service/agent-registry.d.ts +26 -0
- package/dist/service/agent-registry.js +100 -0
- package/dist/service/multiclaws-service.d.ts +88 -0
- package/dist/service/multiclaws-service.js +708 -0
- package/dist/task/tracker.d.ts +43 -0
- package/dist/task/tracker.js +186 -0
- package/dist/team/team-store.d.ts +39 -0
- package/dist/team/team-store.js +146 -0
- package/dist/types/openclaw.d.ts +86 -0
- package/dist/types/openclaw.js +2 -0
- package/openclaw.plugin.json +34 -0
- package/package.json +56 -0
- package/skills/multiclaws/SKILL.md +164 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const handlers_1 = require("./gateway/handlers");
|
|
4
|
+
const multiclaws_service_1 = require("./service/multiclaws-service");
|
|
5
|
+
const gateway_client_1 = require("./infra/gateway-client");
|
|
6
|
+
const logger_1 = require("./infra/logger");
|
|
7
|
+
const telemetry_1 = require("./infra/telemetry");
|
|
8
|
+
function readConfig(api) {
|
|
9
|
+
const raw = (api.pluginConfig ?? {});
|
|
10
|
+
return {
|
|
11
|
+
port: typeof raw.port === "number" ? raw.port : undefined,
|
|
12
|
+
displayName: typeof raw.displayName === "string" ? raw.displayName : undefined,
|
|
13
|
+
selfUrl: typeof raw.selfUrl === "string" ? raw.selfUrl : undefined,
|
|
14
|
+
telemetry: {
|
|
15
|
+
consoleExporter: typeof raw.telemetry?.consoleExporter === "boolean"
|
|
16
|
+
? Boolean(raw.telemetry.consoleExporter)
|
|
17
|
+
: undefined,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function textResult(text, details) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text", text }],
|
|
24
|
+
...(details === undefined ? {} : { details }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function requireService(service) {
|
|
28
|
+
if (!service) {
|
|
29
|
+
throw new Error("multiclaws service is not running yet");
|
|
30
|
+
}
|
|
31
|
+
return service;
|
|
32
|
+
}
|
|
33
|
+
function createTools(getService) {
|
|
34
|
+
/* ── Agent tools ──────────────────────────────────────────────── */
|
|
35
|
+
const multiclawsAgents = {
|
|
36
|
+
name: "multiclaws_agents",
|
|
37
|
+
description: "List known A2A agents and their capabilities.",
|
|
38
|
+
parameters: {
|
|
39
|
+
type: "object",
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
properties: {},
|
|
42
|
+
},
|
|
43
|
+
execute: async () => {
|
|
44
|
+
const service = requireService(getService());
|
|
45
|
+
const agents = await service.listAgents();
|
|
46
|
+
return textResult(JSON.stringify({ agents }, null, 2), { agents });
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const multiclawsAddAgent = {
|
|
50
|
+
name: "multiclaws_add_agent",
|
|
51
|
+
description: "Add a remote A2A agent by URL. Automatically fetches its Agent Card.",
|
|
52
|
+
parameters: {
|
|
53
|
+
type: "object",
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
properties: {
|
|
56
|
+
url: { type: "string" },
|
|
57
|
+
apiKey: { type: "string" },
|
|
58
|
+
},
|
|
59
|
+
required: ["url"],
|
|
60
|
+
},
|
|
61
|
+
execute: async (_toolCallId, args) => {
|
|
62
|
+
const service = requireService(getService());
|
|
63
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
64
|
+
if (!url)
|
|
65
|
+
throw new Error("url is required");
|
|
66
|
+
const apiKey = typeof args.apiKey === "string" ? args.apiKey.trim() : undefined;
|
|
67
|
+
const agent = await service.addAgent({ url, apiKey });
|
|
68
|
+
return textResult(`Agent added: ${agent.name} (${agent.url})`, agent);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const multiclawsRemoveAgent = {
|
|
72
|
+
name: "multiclaws_remove_agent",
|
|
73
|
+
description: "Remove a known A2A agent by URL.",
|
|
74
|
+
parameters: {
|
|
75
|
+
type: "object",
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
properties: {
|
|
78
|
+
url: { type: "string" },
|
|
79
|
+
},
|
|
80
|
+
required: ["url"],
|
|
81
|
+
},
|
|
82
|
+
execute: async (_toolCallId, args) => {
|
|
83
|
+
const service = requireService(getService());
|
|
84
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
85
|
+
if (!url)
|
|
86
|
+
throw new Error("url is required");
|
|
87
|
+
const removed = await service.removeAgent(url);
|
|
88
|
+
return textResult(removed ? `Agent ${url} removed.` : `Agent ${url} not found.`);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const multiclawsDelegate = {
|
|
92
|
+
name: "multiclaws_delegate",
|
|
93
|
+
description: "Delegate a task to a remote A2A agent.",
|
|
94
|
+
parameters: {
|
|
95
|
+
type: "object",
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
properties: {
|
|
98
|
+
agentUrl: { type: "string" },
|
|
99
|
+
task: { type: "string" },
|
|
100
|
+
},
|
|
101
|
+
required: ["agentUrl", "task"],
|
|
102
|
+
},
|
|
103
|
+
execute: async (_toolCallId, args) => {
|
|
104
|
+
const service = requireService(getService());
|
|
105
|
+
const agentUrl = typeof args.agentUrl === "string" ? args.agentUrl.trim() : "";
|
|
106
|
+
const task = typeof args.task === "string" ? args.task.trim() : "";
|
|
107
|
+
if (!agentUrl || !task)
|
|
108
|
+
throw new Error("agentUrl and task are required");
|
|
109
|
+
const result = await service.delegateTask({ agentUrl, task });
|
|
110
|
+
return textResult(JSON.stringify(result, null, 2), result);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const multiclawsTaskStatus = {
|
|
114
|
+
name: "multiclaws_task_status",
|
|
115
|
+
description: "Check the status of a delegated task.",
|
|
116
|
+
parameters: {
|
|
117
|
+
type: "object",
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
properties: {
|
|
120
|
+
taskId: { type: "string" },
|
|
121
|
+
},
|
|
122
|
+
required: ["taskId"],
|
|
123
|
+
},
|
|
124
|
+
execute: async (_toolCallId, args) => {
|
|
125
|
+
const service = requireService(getService());
|
|
126
|
+
const taskId = typeof args.taskId === "string" ? args.taskId.trim() : "";
|
|
127
|
+
if (!taskId)
|
|
128
|
+
throw new Error("taskId is required");
|
|
129
|
+
const task = service.getTaskStatus(taskId);
|
|
130
|
+
if (!task)
|
|
131
|
+
throw new Error(`task not found: ${taskId}`);
|
|
132
|
+
return textResult(JSON.stringify(task, null, 2), task);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
/* ── Team tools ───────────────────────────────────────────────── */
|
|
136
|
+
const multiclawsTeamCreate = {
|
|
137
|
+
name: "multiclaws_team_create",
|
|
138
|
+
description: "Create a new team. Returns teamId and invite code.",
|
|
139
|
+
parameters: {
|
|
140
|
+
type: "object",
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
properties: {
|
|
143
|
+
name: { type: "string" },
|
|
144
|
+
},
|
|
145
|
+
required: ["name"],
|
|
146
|
+
},
|
|
147
|
+
execute: async (_toolCallId, args) => {
|
|
148
|
+
const service = requireService(getService());
|
|
149
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
150
|
+
if (!name)
|
|
151
|
+
throw new Error("name is required");
|
|
152
|
+
const team = await service.createTeam(name);
|
|
153
|
+
const invite = await service.createInvite(team.teamId);
|
|
154
|
+
return textResult(`Team "${team.teamName}" created (${team.teamId}).\nInvite code: ${invite}\n\n⚠️ 请只将邀请码分享给完全信任的用户。持有邀请码的人可以加入团队并向你的 AI 委派任务。权限管理模块正在开发中。`, { team, inviteCode: invite });
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const multiclawsTeamJoin = {
|
|
158
|
+
name: "multiclaws_team_join",
|
|
159
|
+
description: "Join a team using an invite code. Automatically syncs all team members as agents.",
|
|
160
|
+
parameters: {
|
|
161
|
+
type: "object",
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
properties: {
|
|
164
|
+
inviteCode: { type: "string" },
|
|
165
|
+
},
|
|
166
|
+
required: ["inviteCode"],
|
|
167
|
+
},
|
|
168
|
+
execute: async (_toolCallId, args) => {
|
|
169
|
+
const service = requireService(getService());
|
|
170
|
+
const inviteCode = typeof args.inviteCode === "string" ? args.inviteCode.trim() : "";
|
|
171
|
+
if (!inviteCode)
|
|
172
|
+
throw new Error("inviteCode is required");
|
|
173
|
+
const team = await service.joinTeam(inviteCode);
|
|
174
|
+
const memberNames = team.members.map((m) => m.name).join(", ");
|
|
175
|
+
return textResult(`Joined team "${team.teamName}" with ${team.members.length} members: ${memberNames}`, { team });
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const multiclawsTeamLeave = {
|
|
179
|
+
name: "multiclaws_team_leave",
|
|
180
|
+
description: "Leave a team. Notifies all members and removes them from local agent registry.",
|
|
181
|
+
parameters: {
|
|
182
|
+
type: "object",
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
properties: {
|
|
185
|
+
teamId: { type: "string" },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
execute: async (_toolCallId, args) => {
|
|
189
|
+
const service = requireService(getService());
|
|
190
|
+
const teamId = typeof args.teamId === "string" ? args.teamId.trim() : undefined;
|
|
191
|
+
await service.leaveTeam(teamId || undefined);
|
|
192
|
+
return textResult("Left team successfully.");
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const multiclawsTeamMembers = {
|
|
196
|
+
name: "multiclaws_team_members",
|
|
197
|
+
description: "List all members of a team.",
|
|
198
|
+
parameters: {
|
|
199
|
+
type: "object",
|
|
200
|
+
additionalProperties: false,
|
|
201
|
+
properties: {
|
|
202
|
+
teamId: { type: "string" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
execute: async (_toolCallId, args) => {
|
|
206
|
+
const service = requireService(getService());
|
|
207
|
+
const teamId = typeof args.teamId === "string" ? args.teamId.trim() : undefined;
|
|
208
|
+
const result = await service.listTeamMembers(teamId || undefined);
|
|
209
|
+
if (!result) {
|
|
210
|
+
return textResult("No team found.");
|
|
211
|
+
}
|
|
212
|
+
return textResult(JSON.stringify(result, null, 2), result);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
/* ── Profile tools ──────────────────────────────────────────── */
|
|
216
|
+
const multiclawsProfileSet = {
|
|
217
|
+
name: "multiclaws_profile_set",
|
|
218
|
+
description: "Set or update the owner profile (name and bio). Bio is free-form markdown describing role, capabilities, data sources, etc. Broadcasts to team members.",
|
|
219
|
+
parameters: {
|
|
220
|
+
type: "object",
|
|
221
|
+
additionalProperties: false,
|
|
222
|
+
properties: {
|
|
223
|
+
ownerName: { type: "string" },
|
|
224
|
+
bio: { type: "string" },
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
execute: async (_toolCallId, args) => {
|
|
228
|
+
const service = requireService(getService());
|
|
229
|
+
const patch = {};
|
|
230
|
+
if (typeof args.ownerName === "string")
|
|
231
|
+
patch.ownerName = args.ownerName.trim();
|
|
232
|
+
if (typeof args.bio === "string")
|
|
233
|
+
patch.bio = args.bio;
|
|
234
|
+
const profile = await service.setProfile(patch);
|
|
235
|
+
return textResult(JSON.stringify(profile, null, 2), profile);
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
const multiclawsProfileShow = {
|
|
239
|
+
name: "multiclaws_profile_show",
|
|
240
|
+
description: "Show the current owner profile.",
|
|
241
|
+
parameters: {
|
|
242
|
+
type: "object",
|
|
243
|
+
additionalProperties: false,
|
|
244
|
+
properties: {},
|
|
245
|
+
},
|
|
246
|
+
execute: async () => {
|
|
247
|
+
const service = requireService(getService());
|
|
248
|
+
const profile = await service.getProfile();
|
|
249
|
+
return textResult(JSON.stringify(profile, null, 2), profile);
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const multiclawsProfilePendingReview = {
|
|
253
|
+
name: "multiclaws_profile_pending_review",
|
|
254
|
+
description: "Check if the user's profile was just initialized and is pending review. If pending, returns profile and a message to show the user and ask if they want to adjust.",
|
|
255
|
+
parameters: {
|
|
256
|
+
type: "object",
|
|
257
|
+
additionalProperties: false,
|
|
258
|
+
properties: {},
|
|
259
|
+
},
|
|
260
|
+
execute: async () => {
|
|
261
|
+
const service = requireService(getService());
|
|
262
|
+
const result = await service.getPendingProfileReview();
|
|
263
|
+
return textResult(JSON.stringify(result, null, 2), result);
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
const multiclawsProfileClearPendingReview = {
|
|
267
|
+
name: "multiclaws_profile_clear_pending_review",
|
|
268
|
+
description: "Clear the pending profile review flag after the user has confirmed or finished adjusting their profile.",
|
|
269
|
+
parameters: {
|
|
270
|
+
type: "object",
|
|
271
|
+
additionalProperties: false,
|
|
272
|
+
properties: {},
|
|
273
|
+
},
|
|
274
|
+
execute: async () => {
|
|
275
|
+
const service = requireService(getService());
|
|
276
|
+
await service.clearPendingProfileReview();
|
|
277
|
+
return textResult("Pending profile review cleared.");
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
return [
|
|
281
|
+
multiclawsAgents,
|
|
282
|
+
multiclawsAddAgent,
|
|
283
|
+
multiclawsRemoveAgent,
|
|
284
|
+
multiclawsDelegate,
|
|
285
|
+
multiclawsTaskStatus,
|
|
286
|
+
multiclawsTeamCreate,
|
|
287
|
+
multiclawsTeamJoin,
|
|
288
|
+
multiclawsTeamLeave,
|
|
289
|
+
multiclawsTeamMembers,
|
|
290
|
+
multiclawsProfileSet,
|
|
291
|
+
multiclawsProfileShow,
|
|
292
|
+
multiclawsProfilePendingReview,
|
|
293
|
+
multiclawsProfileClearPendingReview,
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
const plugin = {
|
|
297
|
+
id: "multiclaws",
|
|
298
|
+
name: "MultiClaws",
|
|
299
|
+
version: "0.3.0",
|
|
300
|
+
register(api) {
|
|
301
|
+
const config = readConfig(api);
|
|
302
|
+
(0, telemetry_1.initializeTelemetry)({ enableConsoleExporter: config.telemetry?.consoleExporter });
|
|
303
|
+
const structured = (0, logger_1.createStructuredLogger)(api.logger, "multiclaws");
|
|
304
|
+
let service = null;
|
|
305
|
+
let bioSpawnAttempted = false;
|
|
306
|
+
// Ensure required tools are in gateway.tools.allow at registration time
|
|
307
|
+
// so the gateway starts with them already present (no restart needed).
|
|
308
|
+
if (api.config) {
|
|
309
|
+
const gw = api.config.gateway;
|
|
310
|
+
if (gw) {
|
|
311
|
+
const tools = (gw.tools ?? {});
|
|
312
|
+
const allow = Array.isArray(tools.allow) ? tools.allow : [];
|
|
313
|
+
const required = ["sessions_spawn", "sessions_history"];
|
|
314
|
+
const missing = required.filter((t) => !allow.includes(t));
|
|
315
|
+
if (missing.length > 0) {
|
|
316
|
+
tools.allow = [...allow, ...missing];
|
|
317
|
+
gw.tools = tools;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const gatewayConfig = (() => {
|
|
322
|
+
const gw = api.config?.gateway;
|
|
323
|
+
const port = typeof gw?.port === "number" ? gw.port : 18789;
|
|
324
|
+
const token = typeof gw?.auth?.token === "string" ? gw.auth.token : null;
|
|
325
|
+
if (!token)
|
|
326
|
+
return null;
|
|
327
|
+
return { port, token };
|
|
328
|
+
})();
|
|
329
|
+
const pluginService = {
|
|
330
|
+
id: "multiclaws-service",
|
|
331
|
+
start: async (ctx) => {
|
|
332
|
+
service = new multiclaws_service_1.MulticlawsService({
|
|
333
|
+
stateDir: ctx.stateDir,
|
|
334
|
+
port: config.port,
|
|
335
|
+
displayName: config.displayName,
|
|
336
|
+
selfUrl: config.selfUrl,
|
|
337
|
+
gatewayConfig: gatewayConfig ?? undefined,
|
|
338
|
+
logger: structured.logger,
|
|
339
|
+
});
|
|
340
|
+
await service.start();
|
|
341
|
+
},
|
|
342
|
+
stop: async () => {
|
|
343
|
+
if (service) {
|
|
344
|
+
await service.stop();
|
|
345
|
+
service = null;
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
api.registerService(pluginService);
|
|
350
|
+
const gatewayHandlers = (0, handlers_1.createGatewayHandlers)(() => requireService(service));
|
|
351
|
+
for (const [method, handler] of Object.entries(gatewayHandlers)) {
|
|
352
|
+
api.registerGatewayMethod(method, handler);
|
|
353
|
+
}
|
|
354
|
+
for (const tool of createTools(() => service)) {
|
|
355
|
+
api.registerTool(tool);
|
|
356
|
+
}
|
|
357
|
+
api.registerHttpRoute({
|
|
358
|
+
path: "/multiclaws/health",
|
|
359
|
+
auth: "plugin",
|
|
360
|
+
handler: (_req, res) => {
|
|
361
|
+
const running = service !== null;
|
|
362
|
+
res.statusCode = running ? 200 : 503;
|
|
363
|
+
res.end(JSON.stringify({
|
|
364
|
+
ok: running,
|
|
365
|
+
plugin: "multiclaws",
|
|
366
|
+
}));
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
api.on("gateway_start", async () => {
|
|
370
|
+
structured.logger.info("[multiclaws] gateway_start observed");
|
|
371
|
+
// On first run: spawn a subagent to generate bio and notify user (once only)
|
|
372
|
+
if (!service || !gatewayConfig || bioSpawnAttempted)
|
|
373
|
+
return;
|
|
374
|
+
try {
|
|
375
|
+
const pending = await service.getPendingProfileReview();
|
|
376
|
+
if (!pending.pending)
|
|
377
|
+
return;
|
|
378
|
+
bioSpawnAttempted = true;
|
|
379
|
+
const profile = pending.profile;
|
|
380
|
+
const ownerName = profile?.ownerName || "unknown";
|
|
381
|
+
const task = [
|
|
382
|
+
`You are initializing a MultiClaws agent profile for "${ownerName}" on first run.`,
|
|
383
|
+
`MultiClaws enables multiple OpenClaw instances to collaborate as a team over A2A protocol.`,
|
|
384
|
+
``,
|
|
385
|
+
`Complete all steps below, then send ONE message to the user.`,
|
|
386
|
+
``,
|
|
387
|
+
`== STEP 1: Generate bio ==`,
|
|
388
|
+
`Inspect the current environment:`,
|
|
389
|
+
`- Available tools and skills (implies capabilities)`,
|
|
390
|
+
`- Connected channels (Telegram, Gmail, Discord, etc.)`,
|
|
391
|
+
`- Workspace contents (git repos, project folders, key files)`,
|
|
392
|
+
`- Installed plugins`,
|
|
393
|
+
`- Timezone and language`,
|
|
394
|
+
``,
|
|
395
|
+
`Generate a bio in markdown — a "skill card" that other AI agents read to decide`,
|
|
396
|
+
`whether to delegate tasks here. Include:`,
|
|
397
|
+
`- One-line role description`,
|
|
398
|
+
`- What this agent can handle`,
|
|
399
|
+
`- What data/systems it has access to`,
|
|
400
|
+
`- Timezone / language`,
|
|
401
|
+
``,
|
|
402
|
+
`Call: multiclaws_profile_set(ownerName="${ownerName}", bio="<generated markdown>")`,
|
|
403
|
+
``,
|
|
404
|
+
`== STEP 2: Check Tailscale ==`,
|
|
405
|
+
`Check if Tailscale is active by looking at network interfaces for a 100.x.x.x IP address.`,
|
|
406
|
+
`- Found 100.x.x.x: cross-network collaboration is ready.`,
|
|
407
|
+
`- Not found: only LAN collaboration available.`,
|
|
408
|
+
``,
|
|
409
|
+
`== STEP 3: Send ONE message to the user ==`,
|
|
410
|
+
``,
|
|
411
|
+
`1. **MultiClaws 已就绪** — 简要介绍插件功能:`,
|
|
412
|
+
` "MultiClaws 让多个 OpenClaw 实例组成团队协作。你可以创建团队、邀请队友加入,然后把任务委派给队友的 AI——它会自动根据每个智能体的档案选择最合适的执行者。"`,
|
|
413
|
+
``,
|
|
414
|
+
`2. **默认名字**: "你的默认名字是 '${ownerName}',需要修改吗?"`,
|
|
415
|
+
``,
|
|
416
|
+
`3. **Bio 预览**: 展示生成的 bio,问"这是根据你的环境自动生成的档案,需要修改吗?"`,
|
|
417
|
+
``,
|
|
418
|
+
`4. **网络状态** (one line based on Step 2):`,
|
|
419
|
+
` - Tailscale active: "Tailscale 已检测到,跨网络协作已就绪。"`,
|
|
420
|
+
` - LAN only: "当前仅支持局域网协作。如需跨网络,安装 Tailscale:https://tailscale.com/download"`,
|
|
421
|
+
``,
|
|
422
|
+
`5. **如何使用**:`,
|
|
423
|
+
` - "说「创建一个叫 xxx 的团队」创建团队,把邀请码分享给队友"`,
|
|
424
|
+
` - "说「用邀请码 mc:xxxx 加入团队」加入队友的团队"`,
|
|
425
|
+
` - "加入后,说「让 Bob 帮我做 xxx」就能把任务委派给队友的 AI"`,
|
|
426
|
+
` - "说「显示所有智能体」查看团队成员及其能力"`,
|
|
427
|
+
``,
|
|
428
|
+
`Keep the message concise.`,
|
|
429
|
+
].join("\n");
|
|
430
|
+
await (0, gateway_client_1.invokeGatewayTool)({
|
|
431
|
+
gateway: gatewayConfig,
|
|
432
|
+
tool: "sessions_spawn",
|
|
433
|
+
args: { task, mode: "run" },
|
|
434
|
+
timeoutMs: 30_000,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
structured.logger.warn(`[multiclaws] bio init task failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
api.on("gateway_stop", () => {
|
|
442
|
+
structured.logger.info("[multiclaws] gateway_stop observed");
|
|
443
|
+
});
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
exports.default = plugin;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type GatewayConfig = {
|
|
2
|
+
port: number;
|
|
3
|
+
token: string;
|
|
4
|
+
};
|
|
5
|
+
export type InvokeToolResult = {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
result?: unknown;
|
|
8
|
+
error?: {
|
|
9
|
+
type?: string;
|
|
10
|
+
message?: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Call the local OpenClaw gateway's /tools/invoke endpoint.
|
|
15
|
+
* Requires the tool to be allowed by gateway policy.
|
|
16
|
+
*
|
|
17
|
+
* Timeout is enforced via AbortController on the fetch call.
|
|
18
|
+
* Circuit breaker tracks error rates per tool to fail fast on persistent failures.
|
|
19
|
+
* p-retry handles transient errors with up to 2 retries.
|
|
20
|
+
*/
|
|
21
|
+
export declare function invokeGatewayTool(params: {
|
|
22
|
+
gateway: GatewayConfig;
|
|
23
|
+
tool: string;
|
|
24
|
+
args?: Record<string, unknown>;
|
|
25
|
+
sessionKey?: string;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}): Promise<unknown>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.invokeGatewayTool = invokeGatewayTool;
|
|
40
|
+
const opossum_1 = __importDefault(require("opossum"));
|
|
41
|
+
class NonRetryableError extends Error {
|
|
42
|
+
}
|
|
43
|
+
const breakerCache = new Map();
|
|
44
|
+
let pRetryModulePromise = null;
|
|
45
|
+
async function loadPRetry() {
|
|
46
|
+
if (!pRetryModulePromise) {
|
|
47
|
+
pRetryModulePromise = Promise.resolve().then(() => __importStar(require("p-retry")));
|
|
48
|
+
}
|
|
49
|
+
return await pRetryModulePromise;
|
|
50
|
+
}
|
|
51
|
+
function getBreaker(key, timeoutMs) {
|
|
52
|
+
const existing = breakerCache.get(key);
|
|
53
|
+
if (existing) {
|
|
54
|
+
return existing;
|
|
55
|
+
}
|
|
56
|
+
const breaker = new opossum_1.default((operation) => operation(), {
|
|
57
|
+
timeout: false, // timeout handled by AbortController in the operation
|
|
58
|
+
errorThresholdPercentage: 50,
|
|
59
|
+
resetTimeout: 10_000,
|
|
60
|
+
volumeThreshold: 5,
|
|
61
|
+
});
|
|
62
|
+
breakerCache.set(key, breaker);
|
|
63
|
+
return breaker;
|
|
64
|
+
}
|
|
65
|
+
async function executeResilient(params) {
|
|
66
|
+
const pRetryModule = await loadPRetry();
|
|
67
|
+
const pRetry = pRetryModule.default;
|
|
68
|
+
const AbortError = pRetryModule.AbortError;
|
|
69
|
+
const breaker = getBreaker(params.key, params.timeoutMs);
|
|
70
|
+
return (await pRetry(async () => {
|
|
71
|
+
try {
|
|
72
|
+
return (await breaker.fire(params.operation));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof NonRetryableError && AbortError) {
|
|
76
|
+
throw new AbortError(error.message);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}, {
|
|
81
|
+
retries: 2,
|
|
82
|
+
factor: 2,
|
|
83
|
+
minTimeout: 150,
|
|
84
|
+
maxTimeout: 1200,
|
|
85
|
+
randomize: true,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Call the local OpenClaw gateway's /tools/invoke endpoint.
|
|
90
|
+
* Requires the tool to be allowed by gateway policy.
|
|
91
|
+
*
|
|
92
|
+
* Timeout is enforced via AbortController on the fetch call.
|
|
93
|
+
* Circuit breaker tracks error rates per tool to fail fast on persistent failures.
|
|
94
|
+
* p-retry handles transient errors with up to 2 retries.
|
|
95
|
+
*/
|
|
96
|
+
async function invokeGatewayTool(params) {
|
|
97
|
+
const url = `http://localhost:${params.gateway.port}/tools/invoke`;
|
|
98
|
+
const timeoutMs = params.timeoutMs ?? 8_000;
|
|
99
|
+
const key = `${params.gateway.port}:${params.tool}`;
|
|
100
|
+
return await executeResilient({
|
|
101
|
+
key,
|
|
102
|
+
timeoutMs,
|
|
103
|
+
operation: async () => {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(url, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
Authorization: `Bearer ${params.gateway.token}`,
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
tool: params.tool,
|
|
115
|
+
action: "json",
|
|
116
|
+
args: params.args ?? {},
|
|
117
|
+
sessionKey: params.sessionKey ?? "main",
|
|
118
|
+
}),
|
|
119
|
+
signal: controller.signal,
|
|
120
|
+
});
|
|
121
|
+
const json = (await response.json());
|
|
122
|
+
if (!response.ok || !json.ok) {
|
|
123
|
+
const msg = json.error?.message ?? `HTTP ${response.status}`;
|
|
124
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
125
|
+
throw new NonRetryableError(`invokeGatewayTool(${params.tool}) failed: ${msg}`);
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`invokeGatewayTool(${params.tool}) failed: ${msg}`);
|
|
128
|
+
}
|
|
129
|
+
return json.result;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function ensureJsonFile(filePath: string, fallback: unknown): Promise<void>;
|
|
2
|
+
export declare function readJsonWithFallback<T>(filePath: string, fallback: T): Promise<T>;
|
|
3
|
+
export declare function writeJsonAtomically(filePath: string, value: unknown): Promise<void>;
|
|
4
|
+
export declare function withJsonLock<T>(filePath: string, fallback: unknown, fn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,57 @@
|
|
|
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.ensureJsonFile = ensureJsonFile;
|
|
7
|
+
exports.readJsonWithFallback = readJsonWithFallback;
|
|
8
|
+
exports.writeJsonAtomically = writeJsonAtomically;
|
|
9
|
+
exports.withJsonLock = withJsonLock;
|
|
10
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
11
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
|
|
14
|
+
const LOCK_OPTIONS = {
|
|
15
|
+
retries: {
|
|
16
|
+
retries: 10,
|
|
17
|
+
factor: 1.5,
|
|
18
|
+
minTimeout: 50,
|
|
19
|
+
maxTimeout: 1_000,
|
|
20
|
+
randomize: true,
|
|
21
|
+
},
|
|
22
|
+
stale: 10_000,
|
|
23
|
+
};
|
|
24
|
+
async function ensureJsonFile(filePath, fallback) {
|
|
25
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
|
|
26
|
+
try {
|
|
27
|
+
await promises_1.default.access(filePath);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
await writeJsonAtomically(filePath, fallback);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function readJsonWithFallback(filePath, fallback) {
|
|
34
|
+
try {
|
|
35
|
+
const content = await promises_1.default.readFile(filePath, "utf8");
|
|
36
|
+
return JSON.parse(content);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function writeJsonAtomically(filePath, value) {
|
|
43
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
|
|
44
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.${node_crypto_1.default.randomUUID()}.tmp`;
|
|
45
|
+
await promises_1.default.writeFile(tmp, JSON.stringify(value, null, 2), "utf8");
|
|
46
|
+
await promises_1.default.rename(tmp, filePath);
|
|
47
|
+
}
|
|
48
|
+
async function withJsonLock(filePath, fallback, fn) {
|
|
49
|
+
await ensureJsonFile(filePath, fallback);
|
|
50
|
+
const release = await proper_lockfile_1.default.lock(filePath, LOCK_OPTIONS);
|
|
51
|
+
try {
|
|
52
|
+
return await fn();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await release();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type BasicLogger = {
|
|
2
|
+
info: (message: string) => void;
|
|
3
|
+
warn: (message: string) => void;
|
|
4
|
+
error: (message: string) => void;
|
|
5
|
+
debug?: (message: string) => void;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Creates a structured logger that delegates to OpenClaw's base logger.
|
|
9
|
+
* Only outputs via baseLogger to avoid duplicate stdout writes.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createStructuredLogger(baseLogger: BasicLogger, _name?: string): {
|
|
12
|
+
logger: BasicLogger;
|
|
13
|
+
};
|