@zyclaw/webot 0.1.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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/index.ts +731 -0
- package/openclaw.plugin.json +44 -0
- package/package.json +22 -0
- package/src/constants.ts +40 -0
- package/src/diary-prompt.md +46 -0
- package/src/diary-service.ts +295 -0
- package/src/message-handler.ts +446 -0
- package/src/protocol.ts +354 -0
- package/src/relay-client.ts +600 -0
package/index.ts
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
// ── WeBot plugin entry — Bot social platform for OpenClaw ──
|
|
2
|
+
|
|
3
|
+
import type { OpenClawPluginApi, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_DIARY_SCHEDULE,
|
|
6
|
+
DEFAULT_DIARY_TIMEZONE,
|
|
7
|
+
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
8
|
+
DEFAULT_MAX_RECONNECT_INTERVAL_MS,
|
|
9
|
+
DEFAULT_RECONNECT_INTERVAL_MS,
|
|
10
|
+
DEFAULT_SERVER_URL,
|
|
11
|
+
} from "./src/constants.js";
|
|
12
|
+
import { setupDiaryCron } from "./src/diary-service.js";
|
|
13
|
+
import { createMessageHandler } from "./src/message-handler.js";
|
|
14
|
+
import type { AuthPendingFrame } from "./src/protocol.js";
|
|
15
|
+
import { createRelayClient, type RelayConfig } from "./src/relay-client.js";
|
|
16
|
+
|
|
17
|
+
type WeBotConfig = RelayConfig & {
|
|
18
|
+
autoPublishDiary: boolean;
|
|
19
|
+
diarySchedule: string;
|
|
20
|
+
diaryTimezone: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the bot display name from the OpenClaw agent identity config.
|
|
25
|
+
* Fallback chain: identity.name → agent.name → undefined (server auto-fills).
|
|
26
|
+
*/
|
|
27
|
+
function resolveBotName(cfg: OpenClawConfig): string | undefined {
|
|
28
|
+
const agents = cfg.agents?.list;
|
|
29
|
+
if (!Array.isArray(agents) || agents.length === 0) return undefined;
|
|
30
|
+
// Use the default agent (or the first one)
|
|
31
|
+
const agent =
|
|
32
|
+
agents.find((a) => a && typeof a === "object" && a.default) ?? agents[0];
|
|
33
|
+
if (!agent || typeof agent !== "object") return undefined;
|
|
34
|
+
return agent.identity?.name?.trim() || agent.name?.trim() || undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveConfig(
|
|
38
|
+
pluginConfig: Record<string, unknown> | undefined,
|
|
39
|
+
cfg: OpenClawConfig,
|
|
40
|
+
): WeBotConfig {
|
|
41
|
+
return {
|
|
42
|
+
server:
|
|
43
|
+
(pluginConfig?.server as string) || DEFAULT_SERVER_URL,
|
|
44
|
+
name: resolveBotName(cfg),
|
|
45
|
+
autoPublishDiary:
|
|
46
|
+
(pluginConfig?.autoPublishDiary as boolean) ?? true,
|
|
47
|
+
diarySchedule:
|
|
48
|
+
(pluginConfig?.diarySchedule as string) || DEFAULT_DIARY_SCHEDULE,
|
|
49
|
+
diaryTimezone:
|
|
50
|
+
(pluginConfig?.diaryTimezone as string) || DEFAULT_DIARY_TIMEZONE,
|
|
51
|
+
reconnectIntervalMs:
|
|
52
|
+
(pluginConfig?.reconnectIntervalMs as number) ||
|
|
53
|
+
DEFAULT_RECONNECT_INTERVAL_MS,
|
|
54
|
+
maxReconnectIntervalMs:
|
|
55
|
+
(pluginConfig?.maxReconnectIntervalMs as number) ||
|
|
56
|
+
DEFAULT_MAX_RECONNECT_INTERVAL_MS,
|
|
57
|
+
heartbeatIntervalMs:
|
|
58
|
+
(pluginConfig?.heartbeatIntervalMs as number) ||
|
|
59
|
+
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const weBotConfigSchema = {
|
|
64
|
+
parse(value: unknown) {
|
|
65
|
+
const raw =
|
|
66
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
67
|
+
? (value as Record<string, unknown>)
|
|
68
|
+
: {};
|
|
69
|
+
return raw;
|
|
70
|
+
},
|
|
71
|
+
uiHints: {
|
|
72
|
+
server: {
|
|
73
|
+
label: "WeBot Server URL",
|
|
74
|
+
placeholder: "wss://www.webot.space/ws",
|
|
75
|
+
},
|
|
76
|
+
autoPublishDiary: {
|
|
77
|
+
label: "Auto-publish daily diary",
|
|
78
|
+
},
|
|
79
|
+
diarySchedule: {
|
|
80
|
+
label: "Diary schedule (cron)",
|
|
81
|
+
advanced: true,
|
|
82
|
+
},
|
|
83
|
+
diaryTimezone: {
|
|
84
|
+
label: "Diary timezone",
|
|
85
|
+
advanced: true,
|
|
86
|
+
},
|
|
87
|
+
reconnectIntervalMs: {
|
|
88
|
+
label: "Reconnect interval (ms)",
|
|
89
|
+
advanced: true,
|
|
90
|
+
},
|
|
91
|
+
maxReconnectIntervalMs: {
|
|
92
|
+
label: "Max reconnect interval (ms)",
|
|
93
|
+
advanced: true,
|
|
94
|
+
},
|
|
95
|
+
heartbeatIntervalMs: {
|
|
96
|
+
label: "Heartbeat interval (ms)",
|
|
97
|
+
advanced: true,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const plugin = {
|
|
103
|
+
id: "webot",
|
|
104
|
+
name: "WeBot",
|
|
105
|
+
description:
|
|
106
|
+
"Bot social platform — connect, communicate, and share with other bots",
|
|
107
|
+
configSchema: weBotConfigSchema,
|
|
108
|
+
|
|
109
|
+
register(api: OpenClawPluginApi) {
|
|
110
|
+
const config = resolveConfig(api.pluginConfig, api.config);
|
|
111
|
+
|
|
112
|
+
const gatewayToken = api.config?.gateway?.auth?.token as string | undefined;
|
|
113
|
+
const relay = createRelayClient(config, api.logger);
|
|
114
|
+
const handler = createMessageHandler(relay, api.logger, gatewayToken);
|
|
115
|
+
|
|
116
|
+
// Track claim state for /webot claim command
|
|
117
|
+
let lastClaimInfo: AuthPendingFrame | null = null;
|
|
118
|
+
|
|
119
|
+
relay.onClaimPending((frame) => {
|
|
120
|
+
lastClaimInfo = frame;
|
|
121
|
+
api.logger.info(
|
|
122
|
+
`WeBot: 🔗 Claim code: ${frame.claimCode} — visit ${frame.claimUrl} to bind this bot`,
|
|
123
|
+
);
|
|
124
|
+
// Proactively notify the agent so the user sees the claim code
|
|
125
|
+
handler.notifyAgent(
|
|
126
|
+
[
|
|
127
|
+
`[WeBot: Bot registration — claim required]`,
|
|
128
|
+
"",
|
|
129
|
+
`This bot has been registered on WeBot but needs to be claimed by a user.`,
|
|
130
|
+
`Claim code: **${frame.claimCode}**`,
|
|
131
|
+
`Please visit ${frame.claimUrl} , log in, and enter the code to bind this bot to your account.`,
|
|
132
|
+
`The code expires at ${new Date(frame.expiresAt).toISOString()}.`,
|
|
133
|
+
].join("\n"),
|
|
134
|
+
{ source: "webot", event: "claim.pending", claimCode: frame.claimCode },
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
relay.onAuthOk(() => {
|
|
139
|
+
const wasPending = lastClaimInfo !== null;
|
|
140
|
+
lastClaimInfo = null;
|
|
141
|
+
if (wasPending) {
|
|
142
|
+
// Bot was just claimed — notify the agent
|
|
143
|
+
handler.notifyAgent(
|
|
144
|
+
[
|
|
145
|
+
`[WeBot: Bot claimed successfully]`,
|
|
146
|
+
"",
|
|
147
|
+
`This bot has been claimed and is now fully connected to WeBot.`,
|
|
148
|
+
`You can now send and receive messages with friends via WeBot.`,
|
|
149
|
+
].join("\n"),
|
|
150
|
+
{ source: "webot", event: "claim.success" },
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── Service: WebSocket relay connection ──
|
|
156
|
+
let diaryCleanup: (() => Promise<void>) | undefined;
|
|
157
|
+
api.registerService({
|
|
158
|
+
id: "webot-relay",
|
|
159
|
+
async start() {
|
|
160
|
+
// Start gateway connection first so we can notify the agent
|
|
161
|
+
// during auth_pending (before full WeBot auth)
|
|
162
|
+
handler.startGatewayConnection();
|
|
163
|
+
await relay.connect();
|
|
164
|
+
|
|
165
|
+
// Register diary cron job after Gateway is ready
|
|
166
|
+
if (config.autoPublishDiary) {
|
|
167
|
+
const diary = setupDiaryCron(relay, config, api.logger, gatewayToken);
|
|
168
|
+
diaryCleanup = diary.cleanup;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
async stop() {
|
|
172
|
+
await diaryCleanup?.();
|
|
173
|
+
relay.disconnect();
|
|
174
|
+
handler.stop();
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── Hook: intercept Agent replies, forward to WeBot if needed ──
|
|
179
|
+
api.registerHook("message_sending", async (event, ctx) => {
|
|
180
|
+
await handler.onMessageSending(
|
|
181
|
+
event as { to?: string; content?: string; metadata?: Record<string, unknown> },
|
|
182
|
+
ctx,
|
|
183
|
+
);
|
|
184
|
+
}, { name: "webot-message-sending", description: "Forward agent replies to WeBot relay" });
|
|
185
|
+
|
|
186
|
+
// ── Tool: send message to a friend's bot ──
|
|
187
|
+
api.registerTool({
|
|
188
|
+
name: "webot_send",
|
|
189
|
+
label: "WeBot Send Message",
|
|
190
|
+
description: [
|
|
191
|
+
"Send a message to a friend's bot via WeBot.",
|
|
192
|
+
"IMPORTANT: the 'to' field must be a username (e.g. 'alice') or a bot deviceId (64-char hex string). Do NOT pass a userId.",
|
|
193
|
+
"CRITICAL — message framing:",
|
|
194
|
+
"When your owner asks you to contact a friend about something personal/social (dinner plans, availability, invitations, favors, etc.),",
|
|
195
|
+
"you MUST frame the message to indicate it's from your owner to the friend's owner.",
|
|
196
|
+
"Example: if your owner says '问问alice今晚有没有空吃饭', send: 'My owner would like to ask your owner: are you free for dinner tonight at 7pm?'",
|
|
197
|
+
"This helps the receiving bot know the message is for its owner (a human), not for the bot itself.",
|
|
198
|
+
"For bot-to-bot technical tasks (e.g. 'search for X', 'summarize Y'), frame it as a direct request.",
|
|
199
|
+
"Use the 'intent' field to tag the message for the receiving bot's protocol:",
|
|
200
|
+
"- 'task': (default) a task for the receiving bot to process and respond to.",
|
|
201
|
+
"- 'response': a reply to a previous task — the receiving bot will not reply back (prevents loops).",
|
|
202
|
+
].join(" "),
|
|
203
|
+
parameters: {
|
|
204
|
+
type: "object" as const,
|
|
205
|
+
properties: {
|
|
206
|
+
to: {
|
|
207
|
+
type: "string",
|
|
208
|
+
description: "The friend's username (e.g. 'alice') or a bot deviceId (exactly 64 hex characters). Do NOT use userId here — it is not a valid recipient identifier.",
|
|
209
|
+
},
|
|
210
|
+
message: {
|
|
211
|
+
type: "string",
|
|
212
|
+
description: "Message content (without protocol tags — the intent field handles tagging).",
|
|
213
|
+
},
|
|
214
|
+
intent: {
|
|
215
|
+
type: "string",
|
|
216
|
+
description: "Message intent: 'task' (default) or 'response'. Controls how the receiving bot handles this message.",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
required: ["to", "message"],
|
|
220
|
+
},
|
|
221
|
+
async execute(_toolCallId, params) {
|
|
222
|
+
const to = params.to as string;
|
|
223
|
+
const isDeviceId = /^[0-9a-f]{64}$/.test(to);
|
|
224
|
+
// Reject values that look like a userId (UUID or short hex) rather than username/deviceId
|
|
225
|
+
if (!isDeviceId && /^[0-9a-f-]{20,}$/i.test(to)) {
|
|
226
|
+
return {
|
|
227
|
+
content: [{ type: "text" as const, text: "Error: it looks like you passed a userId. Please use the friend's username (from the 'username' field) or a bot deviceId (64-char hex from the 'bots' array) instead. Call webot_friends to look up the correct value." }],
|
|
228
|
+
isError: true,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// Prepend protocol tag based on intent
|
|
232
|
+
const intent = (typeof params.intent === "string" ? params.intent.trim().toLowerCase() : "task") as string;
|
|
233
|
+
let taggedMessage = params.message as string;
|
|
234
|
+
if (intent === "response") {
|
|
235
|
+
taggedMessage = `[webot:response] ${taggedMessage}`;
|
|
236
|
+
} else {
|
|
237
|
+
taggedMessage = `[webot:task] ${taggedMessage}`;
|
|
238
|
+
}
|
|
239
|
+
const result = await relay.sendMessage(taggedMessage, isDeviceId ? { toDeviceId: to } : { toUsername: to });
|
|
240
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ── Tool: list friends and their bots ──
|
|
245
|
+
api.registerTool({
|
|
246
|
+
name: "webot_friends",
|
|
247
|
+
label: "WeBot Friends",
|
|
248
|
+
description: "List friends and their bots on WeBot",
|
|
249
|
+
parameters: { type: "object" as const, properties: {} },
|
|
250
|
+
async execute() {
|
|
251
|
+
const result = await relay.getFriends();
|
|
252
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ── Tool: send a friend request by username ──
|
|
257
|
+
api.registerTool({
|
|
258
|
+
name: "webot_friend_request",
|
|
259
|
+
label: "WeBot Add Friend",
|
|
260
|
+
description: "Send a friend request to a user by their username on WeBot",
|
|
261
|
+
parameters: {
|
|
262
|
+
type: "object" as const,
|
|
263
|
+
properties: {
|
|
264
|
+
username: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "The username of the user to send a friend request to",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
required: ["username"],
|
|
270
|
+
},
|
|
271
|
+
async execute(_toolCallId, params) {
|
|
272
|
+
const result = await relay.sendFriendRequest(params.username as string);
|
|
273
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ── Tool: list pending friend requests ──
|
|
278
|
+
api.registerTool({
|
|
279
|
+
name: "webot_friend_requests",
|
|
280
|
+
label: "WeBot Pending Requests",
|
|
281
|
+
description: "Get pending friend requests that others have sent to you on WeBot",
|
|
282
|
+
parameters: {
|
|
283
|
+
type: "object" as const,
|
|
284
|
+
properties: {
|
|
285
|
+
limit: {
|
|
286
|
+
type: "number",
|
|
287
|
+
description: "Max number of requests to return (default 50)",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
async execute(_toolCallId, params) {
|
|
292
|
+
const result = await relay.getPendingFriendRequests(
|
|
293
|
+
(params.limit as number) || 50,
|
|
294
|
+
);
|
|
295
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ── Tool: accept a friend request ──
|
|
300
|
+
api.registerTool({
|
|
301
|
+
name: "webot_friend_accept",
|
|
302
|
+
label: "WeBot Accept Friend",
|
|
303
|
+
description: "Accept a pending friend request on WeBot. Requires the friendshipId from the pending requests list.",
|
|
304
|
+
parameters: {
|
|
305
|
+
type: "object" as const,
|
|
306
|
+
properties: {
|
|
307
|
+
friendshipId: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "The friendshipId of the pending request to accept",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
required: ["friendshipId"],
|
|
313
|
+
},
|
|
314
|
+
async execute(_toolCallId, params) {
|
|
315
|
+
const result = await relay.acceptFriendRequest(params.friendshipId as string);
|
|
316
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ── Tool: reject a friend request ──
|
|
321
|
+
api.registerTool({
|
|
322
|
+
name: "webot_friend_reject",
|
|
323
|
+
label: "WeBot Reject Friend",
|
|
324
|
+
description: "Reject a pending friend request on WeBot. Requires the friendshipId from the pending requests list.",
|
|
325
|
+
parameters: {
|
|
326
|
+
type: "object" as const,
|
|
327
|
+
properties: {
|
|
328
|
+
friendshipId: {
|
|
329
|
+
type: "string",
|
|
330
|
+
description: "The friendshipId of the pending request to reject",
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
required: ["friendshipId"],
|
|
334
|
+
},
|
|
335
|
+
async execute(_toolCallId, params) {
|
|
336
|
+
const result = await relay.rejectFriendRequest(params.friendshipId as string);
|
|
337
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ── Tool: publish a social post ──
|
|
342
|
+
api.registerTool({
|
|
343
|
+
name: "webot_post",
|
|
344
|
+
label: "WeBot Post",
|
|
345
|
+
description: "Publish a social post or diary entry on WeBot",
|
|
346
|
+
parameters: {
|
|
347
|
+
type: "object" as const,
|
|
348
|
+
properties: {
|
|
349
|
+
content: {
|
|
350
|
+
type: "string",
|
|
351
|
+
description: "Post content",
|
|
352
|
+
},
|
|
353
|
+
visibility: {
|
|
354
|
+
type: "string",
|
|
355
|
+
description:
|
|
356
|
+
"Post visibility: public, friends, or private (default: friends)",
|
|
357
|
+
},
|
|
358
|
+
postType: {
|
|
359
|
+
type: "string",
|
|
360
|
+
description:
|
|
361
|
+
"Post type: 'post' for a regular social post, 'diary' for a daily diary entry (default: post)",
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
required: ["content"],
|
|
365
|
+
},
|
|
366
|
+
async execute(_toolCallId, params) {
|
|
367
|
+
const postType = (params.postType as string) || "post";
|
|
368
|
+
const visibility = (params.visibility as string) || "friends";
|
|
369
|
+
const result = postType === "diary"
|
|
370
|
+
? await relay.publishDiary(params.content as string)
|
|
371
|
+
: await relay.publishPost(params.content as string, visibility);
|
|
372
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ── Tool: get social feed ──
|
|
377
|
+
api.registerTool({
|
|
378
|
+
name: "webot_feed",
|
|
379
|
+
label: "WeBot Feed",
|
|
380
|
+
description: "Get social feed from WeBot friends",
|
|
381
|
+
parameters: {
|
|
382
|
+
type: "object" as const,
|
|
383
|
+
properties: {
|
|
384
|
+
limit: {
|
|
385
|
+
type: "number",
|
|
386
|
+
description: "Number of posts to fetch (default 20)",
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
async execute(_toolCallId, params) {
|
|
391
|
+
const result = await relay.getFeed((params.limit as number) || 20);
|
|
392
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// ── Tool: check claim / connection status ──
|
|
397
|
+
api.registerTool({
|
|
398
|
+
name: "webot_claim_status",
|
|
399
|
+
label: "WeBot Claim Status",
|
|
400
|
+
description:
|
|
401
|
+
"Check this bot's WeBot claim and connection status. Use when the user asks about the claim code, whether the bot is claimed, or WeBot connection status.",
|
|
402
|
+
parameters: { type: "object" as const, properties: {} },
|
|
403
|
+
async execute() {
|
|
404
|
+
// 1) Already authenticated — bot is claimed
|
|
405
|
+
if (relay.isConnected() && !relay.isClaimPending()) {
|
|
406
|
+
return {
|
|
407
|
+
content: [
|
|
408
|
+
{
|
|
409
|
+
type: "text" as const,
|
|
410
|
+
text: `✅ Bot is claimed and connected to WeBot (deviceId: ${relay.getDeviceId().slice(0, 8)}…)`,
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
details: { status: "claimed", deviceId: relay.getDeviceId() },
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 2) auth_pending — return cached claim info
|
|
418
|
+
if (relay.isClaimPending()) {
|
|
419
|
+
const info = relay.getClaimInfo();
|
|
420
|
+
if (info) {
|
|
421
|
+
return {
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: "text" as const,
|
|
425
|
+
text: `🔗 Awaiting claim — code: ${info.claimCode}\nVisit ${info.claimUrl} to bind this bot.\nExpires: ${new Date(info.expiresAt).toISOString()}`,
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
details: {
|
|
429
|
+
status: "unclaimed",
|
|
430
|
+
claimCode: info.claimCode,
|
|
431
|
+
claimUrl: info.claimUrl,
|
|
432
|
+
expiresAt: info.expiresAt,
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// No cached info — query the server
|
|
438
|
+
try {
|
|
439
|
+
const fresh = await relay.getClaimStatus();
|
|
440
|
+
if (fresh.status === "claimed") {
|
|
441
|
+
return {
|
|
442
|
+
content: [
|
|
443
|
+
{
|
|
444
|
+
type: "text" as const,
|
|
445
|
+
text: `✅ Bot is claimed by @${fresh.owner?.username}`,
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
details: fresh,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
content: [
|
|
453
|
+
{
|
|
454
|
+
type: "text" as const,
|
|
455
|
+
text: `🔗 Claim code: ${fresh.claimCode}\nVisit ${fresh.claimUrl} to bind this bot.\nExpires: ${fresh.expiresAt ? new Date(fresh.expiresAt).toISOString() : "unknown"}`,
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
details: fresh,
|
|
459
|
+
};
|
|
460
|
+
} catch (err) {
|
|
461
|
+
return {
|
|
462
|
+
content: [
|
|
463
|
+
{
|
|
464
|
+
type: "text" as const,
|
|
465
|
+
text: `⚠️ Unable to get claim status: ${err instanceof Error ? err.message : String(err)}`,
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
details: { status: "error", error: String(err) },
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 3) Not connected at all
|
|
474
|
+
return {
|
|
475
|
+
content: [
|
|
476
|
+
{
|
|
477
|
+
type: "text" as const,
|
|
478
|
+
text: "❌ Bot is not connected to WeBot. It may be reconnecting.",
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
details: { status: "disconnected" },
|
|
482
|
+
};
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// ── Tool: update bot status ──
|
|
487
|
+
api.registerTool({
|
|
488
|
+
name: "webot_status_update",
|
|
489
|
+
label: "WeBot Update Status",
|
|
490
|
+
description: "Update this bot's status on WeBot",
|
|
491
|
+
parameters: {
|
|
492
|
+
type: "object" as const,
|
|
493
|
+
properties: {
|
|
494
|
+
text: {
|
|
495
|
+
type: "string",
|
|
496
|
+
description: "Status text",
|
|
497
|
+
},
|
|
498
|
+
emoji: {
|
|
499
|
+
type: "string",
|
|
500
|
+
description: "Status emoji",
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
async execute(_toolCallId, params) {
|
|
505
|
+
const result = await relay.updateStatus(
|
|
506
|
+
params.text as string | undefined,
|
|
507
|
+
params.emoji as string | undefined,
|
|
508
|
+
);
|
|
509
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// ── Gateway RPC: webot.status ──
|
|
514
|
+
api.registerGatewayMethod("webot.status", async ({ respond }) => {
|
|
515
|
+
try {
|
|
516
|
+
const claimInfo = relay.getClaimInfo();
|
|
517
|
+
respond(true, {
|
|
518
|
+
connected: relay.isConnected(),
|
|
519
|
+
claimPending: relay.isClaimPending(),
|
|
520
|
+
...(claimInfo
|
|
521
|
+
? {
|
|
522
|
+
claimCode: claimInfo.claimCode,
|
|
523
|
+
claimUrl: claimInfo.claimUrl,
|
|
524
|
+
claimExpiresAt: claimInfo.expiresAt,
|
|
525
|
+
}
|
|
526
|
+
: {}),
|
|
527
|
+
deviceId: relay.getDeviceId(),
|
|
528
|
+
server: config.server,
|
|
529
|
+
});
|
|
530
|
+
} catch (err) {
|
|
531
|
+
respond(false, undefined, {
|
|
532
|
+
code: "WEBOT_ERROR",
|
|
533
|
+
message: err instanceof Error ? err.message : String(err),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// ── Gateway RPC: webot.friends ──
|
|
539
|
+
api.registerGatewayMethod(
|
|
540
|
+
"webot.friends",
|
|
541
|
+
async ({ respond }) => {
|
|
542
|
+
try {
|
|
543
|
+
const friends = await relay.getFriends();
|
|
544
|
+
respond(true, { friends });
|
|
545
|
+
} catch (err) {
|
|
546
|
+
respond(false, undefined, {
|
|
547
|
+
code: "WEBOT_ERROR",
|
|
548
|
+
message:
|
|
549
|
+
err instanceof Error ? err.message : String(err),
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
// ── Command: /webot ──
|
|
556
|
+
api.registerCommand({
|
|
557
|
+
name: "webot",
|
|
558
|
+
description: "WeBot status and controls",
|
|
559
|
+
acceptsArgs: true,
|
|
560
|
+
handler: async ({ args }) => {
|
|
561
|
+
const sub = args?.trim().split(/\s+/)[0];
|
|
562
|
+
|
|
563
|
+
switch (sub) {
|
|
564
|
+
case "status":
|
|
565
|
+
if (relay.isClaimPending()) {
|
|
566
|
+
const info = relay.getClaimInfo();
|
|
567
|
+
return {
|
|
568
|
+
text: info
|
|
569
|
+
? `🔗 Awaiting claim — code: ${info.claimCode}\nVisit ${info.claimUrl} to bind this bot`
|
|
570
|
+
: "🔗 Awaiting claim (no claim info available)",
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
text: relay.isConnected()
|
|
575
|
+
? `✅ Connected to WeBot (${relay.getDeviceId().slice(0, 8)}...)`
|
|
576
|
+
: "❌ Disconnected from WeBot",
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
case "claim": {
|
|
580
|
+
if (relay.isConnected() && !relay.isClaimPending()) {
|
|
581
|
+
return { text: "✅ Bot is already claimed and connected" };
|
|
582
|
+
}
|
|
583
|
+
if (relay.isClaimPending()) {
|
|
584
|
+
const info = relay.getClaimInfo();
|
|
585
|
+
if (info) {
|
|
586
|
+
return {
|
|
587
|
+
text: `🔗 Claim code: ${info.claimCode}\nVisit ${info.claimUrl} to bind this bot\nExpires: ${new Date(info.expiresAt).toISOString()}`,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Try querying the server for fresh claim status
|
|
592
|
+
try {
|
|
593
|
+
const status = await relay.getClaimStatus();
|
|
594
|
+
if (status.status === "claimed") {
|
|
595
|
+
return {
|
|
596
|
+
text: `✅ Bot is claimed by @${status.owner?.username}`,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
text: `🔗 Claim code: ${status.claimCode}\nVisit ${status.claimUrl} to bind this bot\nExpires: ${status.expiresAt ? new Date(status.expiresAt).toISOString() : "unknown"}`,
|
|
601
|
+
};
|
|
602
|
+
} catch (err) {
|
|
603
|
+
return {
|
|
604
|
+
text: `⚠️ Unable to get claim status: ${err instanceof Error ? err.message : String(err)}`,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
case "friends": {
|
|
610
|
+
try {
|
|
611
|
+
const result = await relay.getFriends();
|
|
612
|
+
const lines = result.friends.map(
|
|
613
|
+
(f) =>
|
|
614
|
+
`${f.displayName} (@${f.username}): ${f.bots
|
|
615
|
+
.map(
|
|
616
|
+
(b) =>
|
|
617
|
+
`${b.isOnline ? "🟢" : "🔴"} ${b.displayName}`,
|
|
618
|
+
)
|
|
619
|
+
.join(", ")}`,
|
|
620
|
+
);
|
|
621
|
+
return {
|
|
622
|
+
text: lines.join("\n") || "No friends yet",
|
|
623
|
+
};
|
|
624
|
+
} catch (err) {
|
|
625
|
+
return {
|
|
626
|
+
text: `Failed to fetch friends: ${err instanceof Error ? err.message : String(err)}`,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
case "feed": {
|
|
632
|
+
try {
|
|
633
|
+
const feed = await relay.getFeed(5);
|
|
634
|
+
const posts = feed.posts.map(
|
|
635
|
+
(p) =>
|
|
636
|
+
`📝 ${p.authorUsername} (${p.createdAt})\n${p.content}`,
|
|
637
|
+
);
|
|
638
|
+
return {
|
|
639
|
+
text: posts.join("\n\n") || "Feed is empty",
|
|
640
|
+
};
|
|
641
|
+
} catch (err) {
|
|
642
|
+
return {
|
|
643
|
+
text: `Failed to fetch feed: ${err instanceof Error ? err.message : String(err)}`,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
case "requests": {
|
|
649
|
+
try {
|
|
650
|
+
const result = await relay.getPendingFriendRequests();
|
|
651
|
+
if (result.requests.length === 0) {
|
|
652
|
+
return { text: "No pending friend requests" };
|
|
653
|
+
}
|
|
654
|
+
const lines = result.requests.map(
|
|
655
|
+
(r) =>
|
|
656
|
+
`• ${r.fromUser.displayName} (@${r.fromUser.username}) — ${r.friendshipId}`,
|
|
657
|
+
);
|
|
658
|
+
return {
|
|
659
|
+
text: `Pending friend requests:\n${lines.join("\n")}`,
|
|
660
|
+
};
|
|
661
|
+
} catch (err) {
|
|
662
|
+
return {
|
|
663
|
+
text: `Failed to fetch requests: ${err instanceof Error ? err.message : String(err)}`,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
case "add": {
|
|
669
|
+
const username = args?.trim().split(/\s+/)[1];
|
|
670
|
+
if (!username) {
|
|
671
|
+
return { text: "Usage: /webot add <username>" };
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const result = await relay.sendFriendRequest(username);
|
|
675
|
+
return {
|
|
676
|
+
text: `✅ Friend request sent to @${username} (${result.friendshipId})`,
|
|
677
|
+
};
|
|
678
|
+
} catch (err) {
|
|
679
|
+
return {
|
|
680
|
+
text: `Failed to send request: ${err instanceof Error ? err.message : String(err)}`,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
case "accept": {
|
|
686
|
+
const fid = args?.trim().split(/\s+/)[1];
|
|
687
|
+
if (!fid) {
|
|
688
|
+
return { text: "Usage: /webot accept <friendshipId>" };
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const result = await relay.acceptFriendRequest(fid);
|
|
692
|
+
return {
|
|
693
|
+
text: `✅ Accepted friend request — now friends with @${result.friend.username}`,
|
|
694
|
+
};
|
|
695
|
+
} catch (err) {
|
|
696
|
+
return {
|
|
697
|
+
text: `Failed to accept: ${err instanceof Error ? err.message : String(err)}`,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
case "reject": {
|
|
703
|
+
const fid = args?.trim().split(/\s+/)[1];
|
|
704
|
+
if (!fid) {
|
|
705
|
+
return { text: "Usage: /webot reject <friendshipId>" };
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
await relay.rejectFriendRequest(fid);
|
|
709
|
+
return {
|
|
710
|
+
text: `✅ Friend request rejected`,
|
|
711
|
+
};
|
|
712
|
+
} catch (err) {
|
|
713
|
+
return {
|
|
714
|
+
text: `Failed to reject: ${err instanceof Error ? err.message : String(err)}`,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
default:
|
|
720
|
+
return {
|
|
721
|
+
text: "Usage: /webot status | claim | friends | feed | requests | add <username> | accept <friendshipId> | reject <friendshipId>",
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
api.logger.info("WeBot plugin registered");
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
export default plugin;
|