@aka_openclaw_plugin/mychat 0.1.8 → 0.1.10

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.
@@ -1,985 +0,0 @@
1
- import { i as setMychatRuntime, n as init_runtime, o as __toCommonJS, r as runtime_exports, t as getMychatRuntime } from "./runtime-7z_VfQ27.js";
2
- import { t as mychatConfigSchema } from "./config-schema-C42X7OSY.js";
3
- import { createChannelPluginBase, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
4
- import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
5
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/channel-plugin-common";
6
- //#region src/accounts.ts
7
- const DEFAULT_DM_POLICY = "open";
8
- const DEFAULT_GROUP_POLICY = "open";
9
- const DEFAULT_HISTORY_LIMIT = 50;
10
- const DEFAULT_MEDIA_MAX_MB = 10;
11
- function resolveWsUrl(baseUrl) {
12
- const url = new URL(baseUrl);
13
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
14
- url.pathname = url.pathname.replace(/\/+$/, "") + "/ws/bot";
15
- return url.toString();
16
- }
17
- function mergeAccountConfig(channel, account) {
18
- if (!account) return channel;
19
- return {
20
- ...channel,
21
- ...account,
22
- actions: {
23
- messages: account.actions?.messages ?? channel.actions?.messages ?? true,
24
- threads: account.actions?.threads ?? channel.actions?.threads ?? true,
25
- reactions: account.actions?.reactions ?? channel.actions?.reactions ?? true
26
- },
27
- groups: {
28
- ...channel.groups ?? {},
29
- ...account.groups ?? {}
30
- },
31
- reconnect: account.reconnect ?? channel.reconnect
32
- };
33
- }
34
- function resolveMychatAccounts(cfg) {
35
- const mychatConfig = cfg.channels?.mychat;
36
- if (!mychatConfig) return [];
37
- const accounts = mychatConfig.accounts ?? {};
38
- const accountIds = Object.keys(accounts);
39
- if (accountIds.length === 0) {
40
- const resolved = resolveSingleAccount({
41
- accountId: DEFAULT_ACCOUNT_ID,
42
- config: mychatConfig
43
- });
44
- return resolved ? [resolved] : [];
45
- }
46
- return accountIds.map((id) => {
47
- return resolveSingleAccount({
48
- accountId: id,
49
- config: mergeAccountConfig(mychatConfig, accounts[id])
50
- });
51
- }).filter(Boolean);
52
- }
53
- function resolveSingleAccount(params) {
54
- const { accountId, config } = params;
55
- const enabled = config.enabled ?? true;
56
- if (!enabled) return null;
57
- const token = resolveToken(config);
58
- if (!token) return null;
59
- const baseUrl = config.baseUrl.replace(/\/+$/, "");
60
- const wsUrl = config.wsUrl ?? resolveWsUrl(baseUrl);
61
- return {
62
- accountId: normalizeAccountId(accountId),
63
- enabled,
64
- name: config.name,
65
- baseUrl,
66
- wsUrl,
67
- token,
68
- defaultTo: config.defaultTo,
69
- dmPolicy: config.dmPolicy ?? DEFAULT_DM_POLICY,
70
- groupPolicy: config.groupPolicy ?? DEFAULT_GROUP_POLICY,
71
- allowFrom: config.allowFrom ?? [],
72
- groupAllowFrom: config.groupAllowFrom ?? [],
73
- requireMention: config.requireMention ?? false,
74
- historyLimit: config.historyLimit ?? DEFAULT_HISTORY_LIMIT,
75
- mediaMaxMb: config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB,
76
- actions: {
77
- messages: config.actions?.messages ?? true,
78
- threads: config.actions?.threads ?? true,
79
- reactions: config.actions?.reactions ?? true
80
- },
81
- groups: config.groups ?? {},
82
- reconnect: config.reconnect ?? {}
83
- };
84
- }
85
- function resolveToken(config) {
86
- if (config.token) return config.token;
87
- if (typeof process !== "undefined") {
88
- const envToken = process.env.MYCHAT_BOT_TOKEN;
89
- if (envToken) return envToken;
90
- }
91
- return null;
92
- }
93
- function resolveMychatAccount(params) {
94
- const accounts = resolveMychatAccounts(params.cfg);
95
- if (accounts.length === 0) return null;
96
- if (!params.accountId) return accounts[0];
97
- return accounts.find((a) => a.accountId === params.accountId) ?? accounts[0] ?? null;
98
- }
99
- //#endregion
100
- //#region src/client/http-client.ts
101
- function createMychatHttpClient(params) {
102
- const { baseUrl, token } = params;
103
- const base = baseUrl.replace(/\/+$/, "");
104
- async function request(method, path, body) {
105
- const url = `${base}${path}`;
106
- try {
107
- const headers = { authorization: `Bearer ${token}` };
108
- if (body !== void 0) headers["content-type"] = "application/json";
109
- const response = await fetch(url, {
110
- method,
111
- headers,
112
- body: body !== void 0 ? JSON.stringify(body) : void 0
113
- });
114
- if (!response.ok) {
115
- let errorBody = "";
116
- try {
117
- errorBody = await response.text();
118
- } catch {}
119
- console.error(`[mychat] HTTP ${method} ${url} failed: ${response.status} ${response.statusText}${errorBody ? ` body=${errorBody.slice(0, 200)}` : ""}`);
120
- return null;
121
- }
122
- return await response.json();
123
- } catch (err) {
124
- const errMsg = err instanceof Error ? `${err.message}` : String(err);
125
- console.error(`[mychat] HTTP ${method} ${url} error: ${errMsg}`);
126
- return null;
127
- }
128
- }
129
- return {
130
- async getSelf() {
131
- return request("GET", "/api/bot/self");
132
- },
133
- async getMe() {
134
- return request("GET", "/api/auth/me");
135
- },
136
- async sendMessage(body) {
137
- return request("POST", "/api/bot/messages", body);
138
- },
139
- async listMessages(params) {
140
- const searchParams = new URLSearchParams();
141
- if (params.conversationId) searchParams.set("conversationId", params.conversationId);
142
- if (params.limit) searchParams.set("limit", String(params.limit));
143
- if (params.after) searchParams.set("after", String(params.after));
144
- const qs = searchParams.toString();
145
- return (await request("GET", `/api/bot/messages${qs ? `?${qs}` : ""}`))?.messages ?? [];
146
- },
147
- async addReaction(messageId, body) {
148
- return request("POST", `/api/bot/messages/${messageId}/reactions`, body);
149
- },
150
- async removeReaction(messageId, emoji) {
151
- return request("POST", `/api/bot/messages/${messageId}/reactions`, {
152
- emoji,
153
- action: "remove"
154
- });
155
- },
156
- async uploadFile(params) {
157
- const url = `${base}/api/bot/uploads`;
158
- try {
159
- const formData = new FormData();
160
- let blob;
161
- if (params.file instanceof Blob) blob = params.file;
162
- else {
163
- const bytes = new Uint8Array(params.file.buffer, params.file.byteOffset, params.file.byteLength);
164
- blob = new Blob([bytes], { type: params.mimeType });
165
- }
166
- formData.append("file", blob, params.filename);
167
- if (params.scopeKind) formData.append("scopeKind", params.scopeKind);
168
- if (params.scopeId) formData.append("scopeId", params.scopeId);
169
- const response = await fetch(url, {
170
- method: "POST",
171
- headers: { authorization: `Bearer ${token}` },
172
- body: formData
173
- });
174
- if (!response.ok) {
175
- let errorBody = "";
176
- try {
177
- errorBody = await response.text();
178
- } catch {}
179
- console.error(`[mychat] HTTP POST ${url} failed: ${response.status} ${response.statusText}${errorBody ? ` body=${errorBody.slice(0, 200)}` : ""}`);
180
- return null;
181
- }
182
- return await response.json();
183
- } catch (err) {
184
- const errMsg = err instanceof Error ? `${err.message}` : String(err);
185
- console.error(`[mychat] HTTP POST ${url} error: ${errMsg}`);
186
- return null;
187
- }
188
- },
189
- async healthCheck() {
190
- const url = `${base}/health`;
191
- const start = Date.now();
192
- try {
193
- const response = await fetch(url);
194
- const latencyMs = Date.now() - start;
195
- if (!response.ok) console.error(`[mychat] Health check failed: ${url} status=${response.status} latencyMs=${latencyMs}`);
196
- return {
197
- ok: response.ok,
198
- latencyMs
199
- };
200
- } catch (err) {
201
- const errMsg = err instanceof Error ? `${err.message}` : String(err);
202
- console.error(`[mychat] Health check error: ${url} error=${errMsg} latencyMs=${Date.now() - start}`);
203
- return {
204
- ok: false,
205
- latencyMs: Date.now() - start
206
- };
207
- }
208
- }
209
- };
210
- }
211
- //#endregion
212
- //#region src/client/ws-client.ts
213
- init_runtime();
214
- const DEFAULT_INITIAL_DELAY_MS = 1e3;
215
- const DEFAULT_MAX_DELAY_MS = 3e4;
216
- const DEFAULT_BACKOFF_MULTIPLIER = 2;
217
- function createMychatWsClient(params) {
218
- const { wsUrl, token, botSelfId, reconnect } = params;
219
- const initialDelay = reconnect?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
220
- const maxDelay = reconnect?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
221
- const backoffMultiplier = reconnect?.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER;
222
- let ws = null;
223
- let connected = false;
224
- let reconnectTimer = null;
225
- let currentDelay = initialDelay;
226
- const handlers = /* @__PURE__ */ new Set();
227
- function buildUrl() {
228
- const url = new URL(wsUrl);
229
- if (url.pathname.endsWith("/ws/bot")) {} else if (url.pathname.endsWith("/ws/")) url.pathname += "bot";
230
- else if (url.pathname.endsWith("/ws")) url.pathname += "/bot";
231
- else url.pathname = url.pathname.replace(/\/+$/, "") + "/ws/bot";
232
- return url.toString();
233
- }
234
- function scheduleReconnect() {
235
- if (reconnectTimer) return;
236
- console.log(`[mychat] WS will reconnect in ${currentDelay}ms`);
237
- reconnectTimer = setTimeout(() => {
238
- reconnectTimer = null;
239
- currentDelay = Math.min(currentDelay * backoffMultiplier, maxDelay);
240
- connect();
241
- }, currentDelay);
242
- }
243
- function connect() {
244
- if (ws) return;
245
- try {
246
- const url = buildUrl();
247
- const urlWithToken = new URL(url);
248
- urlWithToken.searchParams.set("token", token);
249
- console.log(`[mychat] WS connecting to ${urlWithToken.origin}${urlWithToken.pathname}`);
250
- ws = new WebSocket(urlWithToken.toString());
251
- ws.onopen = () => {
252
- connected = true;
253
- currentDelay = initialDelay;
254
- console.log(`[mychat] WS connected botSelfId=${botSelfId ?? "none"}`);
255
- if (botSelfId) ws?.send(JSON.stringify({
256
- type: "subscribe",
257
- body: { subscribe: { conversationId: botSelfId } }
258
- }));
259
- };
260
- ws.onmessage = (event) => {
261
- try {
262
- const message = JSON.parse(event.data);
263
- for (const handler of handlers) handler(message);
264
- } catch {}
265
- };
266
- ws.onclose = (event) => {
267
- connected = false;
268
- ws = null;
269
- console.log(`[mychat] WS closed code=${event.code} reason=${event.reason || "none"}`);
270
- scheduleReconnect();
271
- };
272
- ws.onerror = (event) => {
273
- const errorMsg = event instanceof ErrorEvent ? event.message : "unknown error";
274
- console.error(`[mychat] WS error: ${errorMsg}`);
275
- ws?.close();
276
- };
277
- } catch (err) {
278
- const errMsg = err instanceof Error ? err.message : String(err);
279
- console.error(`[mychat] WS connect exception: ${errMsg}`);
280
- scheduleReconnect();
281
- }
282
- }
283
- function disconnect() {
284
- if (reconnectTimer) {
285
- clearTimeout(reconnectTimer);
286
- reconnectTimer = null;
287
- }
288
- connected = false;
289
- ws?.close();
290
- ws = null;
291
- }
292
- return {
293
- connect,
294
- disconnect,
295
- onMessage(handler) {
296
- handlers.add(handler);
297
- return () => {
298
- handlers.delete(handler);
299
- };
300
- },
301
- isConnected() {
302
- return connected;
303
- }
304
- };
305
- }
306
- //#endregion
307
- //#region src/logger.ts
308
- const PREFIX = "mychat";
309
- function mychatLogPrefix(feature) {
310
- return `[${PREFIX}:${feature}]`;
311
- }
312
- function getMychatLogger() {
313
- try {
314
- const logger = ((init_runtime(), __toCommonJS(runtime_exports)).tryGetMychatRuntime?.())?.logger;
315
- if (!logger) return null;
316
- return {
317
- debug: (m) => logger.debug?.(m),
318
- info: (m) => logger.info(m),
319
- warn: (m) => logger.warn(m),
320
- error: (m) => logger.error(m)
321
- };
322
- } catch {
323
- return null;
324
- }
325
- }
326
- //#endregion
327
- //#region src/monitor/listeners.ts
328
- function createMychatMessageListener(params) {
329
- const { handler } = params;
330
- return { handle(message) {
331
- handler.handleMessage(message).catch(() => {});
332
- } };
333
- }
334
- function createMychatReactionListener(params) {
335
- const logger = getMychatLogger();
336
- const prefix = mychatLogPrefix("reaction");
337
- return { handle(message) {
338
- const emoji = message.body?.reaction;
339
- const action = message.body?.action ?? "add";
340
- const target = message.body?.target;
341
- const senderId = message.from?.id;
342
- if (logger) logger.debug(`${prefix} ${action} ${emoji} on ${target} by ${senderId}`);
343
- } };
344
- }
345
- function createMychatTypingListener(params) {
346
- const logger = getMychatLogger();
347
- const prefix = mychatLogPrefix("typing");
348
- return { handle(message) {
349
- const senderId = message.from?.id;
350
- if (senderId === params.account.accountId) return;
351
- if (logger) logger.debug(`${prefix} from ${senderId}`);
352
- } };
353
- }
354
- //#endregion
355
- //#region src/monitor/provider.startup.ts
356
- function createMychatProviderClients(account, botSelfId) {
357
- return { wsClient: createMychatWsClient({
358
- wsUrl: account.wsUrl,
359
- token: account.token,
360
- botSelfId,
361
- reconnect: account.reconnect
362
- }) };
363
- }
364
- function registerMychatMonitorListeners(params) {
365
- const { account, handler, wsClient } = params;
366
- const messageListener = createMychatMessageListener({
367
- account,
368
- handler
369
- });
370
- const reactionListener = createMychatReactionListener({ account });
371
- const typingListener = createMychatTypingListener({ account });
372
- wsClient.onMessage((message) => {
373
- switch (message.type) {
374
- case "message":
375
- case "stream":
376
- messageListener.handle(message);
377
- break;
378
- case "reaction":
379
- reactionListener.handle(message);
380
- break;
381
- case "typing":
382
- typingListener.handle(message);
383
- break;
384
- default: break;
385
- }
386
- });
387
- }
388
- //#endregion
389
- //#region src/monitor/provider.lifecycle.ts
390
- const DEFAULT_READY_TIMEOUT_MS = 15e3;
391
- function createMychatProviderLifecycle(params) {
392
- const { httpClient, wsClient, readyTimeoutMs = DEFAULT_READY_TIMEOUT_MS } = params;
393
- const logger = getMychatLogger();
394
- const prefix = mychatLogPrefix("lifecycle");
395
- return {
396
- async waitForReady() {
397
- const start = Date.now();
398
- logger?.info(`${prefix} starting connection lifecycle timeoutMs=${readyTimeoutMs}`);
399
- wsClient.connect();
400
- let wsConnectedAt = null;
401
- let getSelfAttempts = 0;
402
- while (Date.now() - start < readyTimeoutMs) {
403
- if (wsClient.isConnected()) {
404
- if (!wsConnectedAt) {
405
- wsConnectedAt = Date.now();
406
- logger?.info(`${prefix} ws connected, fetching bot identity elapsedMs=${wsConnectedAt - start}`);
407
- }
408
- getSelfAttempts++;
409
- const botSelf = await httpClient.getSelf();
410
- if (botSelf) {
411
- logger?.info(`${prefix} bot connected botId=${botSelf.botId} wsConnectMs=${wsConnectedAt - start} getSelfAttempts=${getSelfAttempts}`);
412
- return botSelf;
413
- }
414
- logger?.warn(`${prefix} getSelf() failed attempt=${getSelfAttempts} elapsedMs=${Date.now() - start}`);
415
- }
416
- await new Promise((r) => setTimeout(r, 500));
417
- }
418
- const wsConnected = wsClient.isConnected();
419
- const elapsed = Date.now() - start;
420
- logger?.warn(`${prefix} ready timeout wsConnected=${wsConnected} wsConnectedAt=${wsConnectedAt ? wsConnectedAt - start : "never"} getSelfAttempts=${getSelfAttempts} elapsedMs=${elapsed} timeoutMs=${readyTimeoutMs}`);
421
- return null;
422
- },
423
- shutdown() {
424
- wsClient.disconnect();
425
- logger?.info(`${prefix} shutdown`);
426
- }
427
- };
428
- }
429
- //#endregion
430
- //#region src/monitor/inbound-dedupe.ts
431
- const MAX_CACHE_SIZE = 500;
432
- function createMychatInboundDedupe() {
433
- const seen = /* @__PURE__ */ new Set();
434
- return { isDuplicate(messageId) {
435
- if (seen.has(messageId)) return true;
436
- seen.add(messageId);
437
- if (seen.size > MAX_CACHE_SIZE) {
438
- const first = seen.values().next().value;
439
- if (first !== void 0) seen.delete(first);
440
- }
441
- return false;
442
- } };
443
- }
444
- //#endregion
445
- //#region src/monitor/message-text.ts
446
- /** Extract plain text from a WebSocket message body. */
447
- function extractMychatMessageText(message) {
448
- return message.body?.text ?? "";
449
- }
450
- /** Extract mention user IDs from a message. */
451
- function extractMentionUserIds(message) {
452
- return (message.body?.mentions ?? []).map((m) => m.userId).filter(Boolean);
453
- }
454
- /** Check if the bot is mentioned in a message. */
455
- function isBotMentioned(message, botUserId) {
456
- if (!botUserId) return false;
457
- if (extractMentionUserIds(message).includes(botUserId)) return true;
458
- return (message.body?.text ?? "").includes(`@${botUserId}`);
459
- }
460
- //#endregion
461
- //#region src/monitor/message-handler.preflight.ts
462
- /** Check if an inbound message should be accepted. */
463
- function mychatPreflight(message, account) {
464
- const senderId = message.from?.id;
465
- if (!senderId) return {
466
- allowed: false,
467
- reason: "no-sender"
468
- };
469
- if (senderId === account.accountId) return {
470
- allowed: false,
471
- reason: "self"
472
- };
473
- const toKind = message.to?.kind ?? "direct";
474
- if (toKind === "direct" || toKind === "channel") {
475
- if (account.dmPolicy === "disabled") return {
476
- allowed: false,
477
- reason: "dm-disabled"
478
- };
479
- if (account.dmPolicy === "allowlist" && account.allowFrom.length > 0) {
480
- if (!account.allowFrom.includes(senderId)) return {
481
- allowed: false,
482
- reason: "dm-not-allowed"
483
- };
484
- }
485
- }
486
- if (toKind === "group") {
487
- if (account.groupPolicy === "disabled") return {
488
- allowed: false,
489
- reason: "group-disabled"
490
- };
491
- if (account.groupPolicy === "allowlist" && account.groupAllowFrom.length > 0) {
492
- if (!account.groupAllowFrom.includes(senderId)) return {
493
- allowed: false,
494
- reason: "group-not-allowed"
495
- };
496
- }
497
- const groupId = message.to?.id;
498
- if (groupId && account.groups[groupId]) {
499
- const groupConfig = account.groups[groupId];
500
- if (groupConfig.enabled === false) return {
501
- allowed: false,
502
- reason: "group-disabled"
503
- };
504
- if (groupConfig.requireMention && !isBotMentioned(message, account.accountId)) return {
505
- allowed: false,
506
- reason: "mention-required"
507
- };
508
- }
509
- if (account.requireMention && !isBotMentioned(message, account.accountId)) return {
510
- allowed: false,
511
- reason: "mention-required"
512
- };
513
- }
514
- return { allowed: true };
515
- }
516
- //#endregion
517
- //#region src/monitor/inbound-context.ts
518
- function buildMychatInboundContext(message, account) {
519
- if (message.type !== "message" && message.type !== "stream") return null;
520
- const senderId = message.from?.id;
521
- if (!senderId) return null;
522
- const text = message.body?.text ?? "";
523
- const toKind = message.to?.kind ?? "direct";
524
- const chatType = toKind === "group" || toKind === "channel" ? "group" : "direct";
525
- return {
526
- messageId: message.id,
527
- text,
528
- senderId,
529
- senderName: message.from?.name,
530
- chatType,
531
- conversationId: message.to?.id ?? message.body?.conversationId,
532
- threadId: message.body?.parentId,
533
- replyTo: message.body?.parentId,
534
- mentions: message.body?.mentions ?? [],
535
- attachments: message.body?.attachments ?? [],
536
- timestamp: message.timestamp
537
- };
538
- }
539
- //#endregion
540
- //#region src/monitor/message-media.ts
541
- /** Resolve media attachments from an inbound message. */
542
- function resolveMychatMediaAttachments(message) {
543
- return (message.body?.attachments ?? []).filter((a) => a.url && (a.mimeType.startsWith("image/") || a.mimeType.startsWith("video/") || a.mimeType.startsWith("audio/") || a.mimeType === "application/pdf"));
544
- }
545
- //#endregion
546
- //#region src/monitor/message-handler.process.ts
547
- /** Process an inbound message into a structured result. */
548
- function processMychatInbound(message, account) {
549
- const context = buildMychatInboundContext(message, account);
550
- if (!context) return null;
551
- return {
552
- context,
553
- text: extractMychatMessageText(message),
554
- media: resolveMychatMediaAttachments(message)
555
- };
556
- }
557
- //#endregion
558
- //#region src/monitor/message-handler.ts
559
- function createMychatMessageHandler(params) {
560
- const { account, onInbound } = params;
561
- const dedupe = createMychatInboundDedupe();
562
- const logger = getMychatLogger();
563
- const prefix = mychatLogPrefix("handler");
564
- return { async handleMessage(message) {
565
- if (dedupe.isDuplicate(message.id)) return;
566
- const preflight = mychatPreflight(message, account);
567
- if (!preflight.allowed) {
568
- if (logger) logger.debug(`${prefix} message rejected reason=${preflight.reason} messageId=${message.id}`);
569
- return;
570
- }
571
- const result = processMychatInbound(message, account);
572
- if (!result) return;
573
- if (logger) logger.debug(`${prefix} message accepted messageId=${result.context.messageId} chatType=${result.context.chatType}`);
574
- await onInbound(result);
575
- } };
576
- }
577
- //#endregion
578
- //#region src/monitor/provider.ts
579
- async function monitorMychatProvider(ctx) {
580
- const { account, setStatus } = ctx;
581
- const logger = getMychatLogger();
582
- const prefix = mychatLogPrefix("provider");
583
- if (logger) logger.info(`${prefix} starting accountId=${account.accountId}`);
584
- setStatus({
585
- connected: false,
586
- mode: "websocket"
587
- });
588
- const httpClient = createMychatHttpClient({
589
- baseUrl: account.baseUrl,
590
- token: account.token
591
- });
592
- if (logger) logger.info(`${prefix} fetching bot identity baseUrl=${account.baseUrl}`);
593
- const botSelf = await httpClient.getSelf();
594
- if (!botSelf) {
595
- const error = `MyChat provider failed to get bot identity (baseUrl=${account.baseUrl}, token=${account.token ? account.token.slice(0, 8) + "..." : "empty"})`;
596
- if (logger) logger.error(`${prefix} ${error}`);
597
- setStatus({
598
- connected: false,
599
- lastError: error
600
- });
601
- throw new Error(error);
602
- }
603
- if (logger) logger.info(`${prefix} got bot identity botId=${botSelf.botId} name=${botSelf.name ?? "unknown"}`);
604
- const health = await httpClient.healthCheck();
605
- if (!health.ok) {
606
- if (logger) logger.warn(`${prefix} health check failed latencyMs=${health.latencyMs} baseUrl=${account.baseUrl}`);
607
- } else if (logger) logger.info(`${prefix} health check ok latencyMs=${health.latencyMs}`);
608
- const { wsClient } = createMychatProviderClients(account, botSelf.botId);
609
- const rt = ctx.runtime;
610
- rt.mychat = {
611
- accountId: account.accountId,
612
- httpClient,
613
- wsClient,
614
- botSelf
615
- };
616
- try {
617
- setMychatRuntime(ctx.runtime);
618
- } catch {}
619
- registerMychatMonitorListeners({
620
- account,
621
- handler: createMychatMessageHandler({
622
- account,
623
- onInbound: async (result) => {
624
- await dispatchMychatInbound(ctx, httpClient, result);
625
- }
626
- }),
627
- wsClient
628
- });
629
- const lifecycle = createMychatProviderLifecycle({
630
- httpClient,
631
- wsClient
632
- });
633
- const readyBotSelf = await lifecycle.waitForReady();
634
- if (!readyBotSelf) {
635
- const error = "MyChat provider failed to connect";
636
- if (logger) logger.error(`${prefix} ${error}`);
637
- setStatus({
638
- connected: false,
639
- lastError: error
640
- });
641
- lifecycle.shutdown();
642
- throw new Error(error);
643
- }
644
- setStatus({
645
- connected: true,
646
- lastConnectedAt: Date.now(),
647
- lastEventAt: Date.now(),
648
- mode: "websocket",
649
- lastError: null
650
- });
651
- if (logger) logger.info(`${prefix} bot connected botId=${readyBotSelf.botId}`);
652
- ctx.abortSignal?.addEventListener("abort", () => {
653
- if (logger) logger.info(`${prefix} abort signal received, shutting down`);
654
- lifecycle.shutdown();
655
- setStatus({ connected: false });
656
- });
657
- return { unsubscribe() {
658
- lifecycle.shutdown();
659
- setStatus({ connected: false });
660
- } };
661
- }
662
- /** Dispatch an inbound message through the OpenClaw turn runtime. */
663
- async function dispatchMychatInbound(ctx, httpClient, result) {
664
- const { runtime, cfg, account } = ctx;
665
- const logger = getMychatLogger();
666
- const prefix = mychatLogPrefix("dispatch");
667
- const rt = runtime;
668
- if (!rt.channel?.turn?.run) {
669
- if (logger) logger.warn(`${prefix} channel turn runtime not available, skipping inbound`);
670
- return;
671
- }
672
- const conversationId = result.context.conversationId ?? result.context.senderId;
673
- const senderId = result.context.senderId;
674
- const chatType = result.context.chatType;
675
- const messageId = result.context.messageId;
676
- await rt.channel.turn.run({
677
- channel: "mychat",
678
- accountId: account.accountId,
679
- raw: result,
680
- adapter: {
681
- ingest: () => ({
682
- id: messageId,
683
- timestamp: result.context.timestamp,
684
- rawText: result.text,
685
- textForAgent: result.text,
686
- textForCommands: result.text,
687
- raw: result
688
- }),
689
- resolveTurn: (input) => {
690
- const msgCtx = rt.channel.turn.buildContext({
691
- channel: "mychat",
692
- accountId: account.accountId,
693
- timestamp: input.timestamp,
694
- from: `mychat:user:${senderId}`,
695
- sender: {
696
- id: senderId,
697
- name: result.context.senderName
698
- },
699
- conversation: {
700
- kind: chatType,
701
- id: conversationId,
702
- label: result.context.senderName ?? senderId,
703
- routePeer: {
704
- kind: chatType,
705
- id: conversationId
706
- }
707
- },
708
- route: {
709
- agentId: void 0,
710
- accountId: account.accountId
711
- },
712
- reply: {
713
- to: `mychat:${conversationId}`,
714
- originatingTo: `mychat:${conversationId}`
715
- },
716
- message: {
717
- rawBody: input.rawText,
718
- commandBody: input.textForCommands,
719
- bodyForAgent: input.textForAgent,
720
- envelopeFrom: result.context.senderName
721
- }
722
- });
723
- return {
724
- cfg,
725
- channel: "mychat",
726
- accountId: account.accountId,
727
- ctxPayload: msgCtx,
728
- dispatchReplyWithBufferedBlockDispatcher: rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
729
- delivery: { deliver: async (payload) => {
730
- const text = payload.text?.trim();
731
- if (!text) return;
732
- const sendResult = await httpClient.sendMessage({
733
- to: conversationId,
734
- text,
735
- type: chatType === "group" ? "group" : "direct"
736
- });
737
- if (logger) logger.debug(`${prefix} reply sent to=${conversationId} ok=${Boolean(sendResult)}`);
738
- } }
739
- };
740
- }
741
- }
742
- });
743
- }
744
- //#endregion
745
- //#region src/shared.ts
746
- function createMychatPluginBase() {
747
- return {
748
- ...createChannelPluginBase({
749
- id: "mychat",
750
- setup: { applyAccountConfig(params) {
751
- return params.cfg;
752
- } },
753
- meta: {
754
- label: "MyChat",
755
- docsPath: "docs/plugins/mychat",
756
- blurb: "Connect to MyChat server via bot token",
757
- aliases: ["mychat"]
758
- },
759
- capabilities: {
760
- chatTypes: [
761
- "direct",
762
- "group",
763
- "thread"
764
- ],
765
- reactions: true,
766
- media: true,
767
- threads: true
768
- },
769
- reload: { configPrefixes: ["channels.mychat"] },
770
- configSchema: mychatConfigSchema,
771
- config: {
772
- listAccountIds(cfg) {
773
- return resolveMychatAccounts(cfg).map((a) => a.accountId);
774
- },
775
- resolveAccount(cfg, accountId) {
776
- return resolveMychatAccount({
777
- cfg,
778
- accountId
779
- });
780
- },
781
- async inspectAccount(account) {
782
- return {
783
- accountId: account.accountId,
784
- enabled: account.enabled,
785
- label: account.name ?? account.baseUrl
786
- };
787
- }
788
- }
789
- }),
790
- gateway: { async startAccount(ctx) {
791
- const setStatus = createAccountStatusSink({
792
- accountId: ctx.accountId,
793
- setStatus: ctx.setStatus
794
- });
795
- return { unsubscribe: (await monitorMychatProvider({
796
- cfg: ctx.cfg,
797
- runtime: ctx.runtime,
798
- account: ctx.account,
799
- setStatus,
800
- abortSignal: ctx.abortSignal,
801
- log: ctx.log
802
- })).unsubscribe };
803
- } }
804
- };
805
- }
806
- //#endregion
807
- //#region src/outbound/outbound-payload.ts
808
- async function sendMychatOutboundPayload(ctx, body) {
809
- const logger = getMychatLogger();
810
- const prefix = mychatLogPrefix("outbound");
811
- try {
812
- const result = await ctx.httpClient.sendMessage(body);
813
- if (result) {
814
- if (logger) logger.debug(`${prefix} message sent id=${result.id} status=${result.status}`);
815
- return {
816
- messageId: result.id,
817
- ok: true
818
- };
819
- }
820
- return { ok: false };
821
- } catch (error) {
822
- if (logger) logger.error(`${prefix} send failed error=${error instanceof Error ? error.message : String(error)}`);
823
- return { ok: false };
824
- }
825
- }
826
- //#endregion
827
- //#region src/outbound/outbound-send-context.ts
828
- function createMychatOutboundSendContext(params) {
829
- return {
830
- account: params.account,
831
- httpClient: params.httpClient
832
- };
833
- }
834
- //#endregion
835
- //#region src/outbound/outbound-adapter.ts
836
- init_runtime();
837
- const TEXT_CHUNK_LIMIT = 4e3;
838
- function chunkText(text, limit) {
839
- if (text.length <= limit) return [text];
840
- const chunks = [];
841
- let remaining = text;
842
- while (remaining.length > 0) {
843
- chunks.push(remaining.slice(0, limit));
844
- remaining = remaining.slice(limit);
845
- }
846
- return chunks;
847
- }
848
- const mychatOutbound = {
849
- deliveryMode: "direct",
850
- textChunkLimit: TEXT_CHUNK_LIMIT,
851
- async sendText(params) {
852
- const logger = getMychatLogger();
853
- const account = getMychatRuntime().mychat;
854
- if (!account) {
855
- if (logger) logger.error(`${mychatLogPrefix("outbound")} no runtime account`);
856
- return {
857
- channel: "mychat",
858
- messageId: "",
859
- ok: false
860
- };
861
- }
862
- const ctx = createMychatOutboundSendContext({
863
- account: params.account ?? {},
864
- httpClient: account.httpClient
865
- });
866
- const chunks = chunkText(params.text, TEXT_CHUNK_LIMIT);
867
- let lastOk = false;
868
- let lastMessageId = "";
869
- for (const chunk of chunks) {
870
- const result = await sendMychatOutboundPayload(ctx, {
871
- to: params.target ?? "",
872
- text: chunk,
873
- type: params.chatType,
874
- replyTo: params.replyTo
875
- });
876
- lastOk = result.ok;
877
- if (result.messageId) lastMessageId = result.messageId;
878
- }
879
- return {
880
- channel: "mychat",
881
- messageId: lastMessageId,
882
- ok: lastOk
883
- };
884
- },
885
- async sendMedia(params) {
886
- const logger = getMychatLogger();
887
- const account = getMychatRuntime().mychat;
888
- if (!account) {
889
- if (logger) logger.error(`${mychatLogPrefix("outbound")} no runtime account`);
890
- return {
891
- channel: "mychat",
892
- messageId: "",
893
- ok: false
894
- };
895
- }
896
- if (params.media) {
897
- const uploadResult = await account.httpClient.uploadFile({
898
- file: params.media,
899
- filename: params.filename ?? "upload",
900
- mimeType: params.mimeType ?? "application/octet-stream"
901
- });
902
- if (uploadResult) {
903
- const result = await sendMychatOutboundPayload(createMychatOutboundSendContext({
904
- account: params.account ?? {},
905
- httpClient: account.httpClient
906
- }), {
907
- to: params.target ?? "",
908
- text: params.text ?? "",
909
- type: params.chatType,
910
- attachments: [uploadResult.attachment]
911
- });
912
- return {
913
- channel: "mychat",
914
- messageId: result.messageId ?? "",
915
- ok: result.ok
916
- };
917
- }
918
- }
919
- return {
920
- channel: "mychat",
921
- messageId: "",
922
- ok: false
923
- };
924
- },
925
- async sendPayload(params) {
926
- const logger = getMychatLogger();
927
- const account = getMychatRuntime().mychat;
928
- if (!account) {
929
- if (logger) logger.error(`${mychatLogPrefix("outbound")} no runtime account`);
930
- return {
931
- channel: "mychat",
932
- messageId: "",
933
- ok: false
934
- };
935
- }
936
- const result = await sendMychatOutboundPayload(createMychatOutboundSendContext({
937
- account: params.account ?? {},
938
- httpClient: account.httpClient
939
- }), {
940
- to: params.target ?? "",
941
- text: params.text ?? "",
942
- type: params.chatType,
943
- replyTo: params.replyTo
944
- });
945
- return {
946
- channel: "mychat",
947
- messageId: result.messageId ?? "",
948
- ok: result.ok
949
- };
950
- }
951
- };
952
- //#endregion
953
- //#region src/channel.ts
954
- const mychatPlugin = createChatChannelPlugin({
955
- base: createMychatPluginBase(),
956
- security: { dm: {
957
- channelKey: "dmPolicy",
958
- resolvePolicy(account) {
959
- return account.dmPolicy ?? "open";
960
- },
961
- resolveAllowFrom(account) {
962
- return account.allowFrom;
963
- }
964
- } },
965
- pairing: { text: {
966
- idLabel: "MyChat Username",
967
- message: "To connect to MyChat, configure your bot token and server URL in the channel config.",
968
- notify() {}
969
- } },
970
- threading: {
971
- replyToMode: "always",
972
- resolveReplyTo(params) {
973
- return {
974
- threadId: params.inbound?.threadId ?? params.inbound?.conversationId,
975
- replyTo: params.inbound?.messageId
976
- };
977
- },
978
- sanitizeThreadName(name) {
979
- return name.slice(0, 100).replace(/[^\w\s-]/g, "");
980
- }
981
- },
982
- outbound: mychatOutbound
983
- });
984
- //#endregion
985
- export { mychatPlugin as t };