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