@cored-im/openclaw-plugin 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/LICENSE +201 -0
- package/README.md +112 -0
- package/README.zh.md +112 -0
- package/dist/index.cjs +650 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +627 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/src/channel.ts +61 -0
- package/src/config.test.ts +251 -0
- package/src/config.ts +162 -0
- package/src/core/cored-client.test.ts +140 -0
- package/src/core/cored-client.ts +319 -0
- package/src/index.test.ts +164 -0
- package/src/index.ts +112 -0
- package/src/messaging/inbound.test.ts +560 -0
- package/src/messaging/inbound.ts +396 -0
- package/src/messaging/outbound.test.ts +172 -0
- package/src/messaging/outbound.ts +176 -0
- package/src/targets.test.ts +91 -0
- package/src/targets.ts +41 -0
- package/src/types.test.ts +10 -0
- package/src/types.ts +115 -0
- package/src/typings/cored-sdk.d.ts +23 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
default: () => register
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/config.ts
|
|
28
|
+
var DEFAULTS = {
|
|
29
|
+
enableEncryption: true,
|
|
30
|
+
requestTimeout: 3e4,
|
|
31
|
+
requireMention: true,
|
|
32
|
+
inboundWhitelist: []
|
|
33
|
+
};
|
|
34
|
+
var ENV_PREFIX = "CORED_";
|
|
35
|
+
function getChannelConfig(cfg) {
|
|
36
|
+
const root = cfg;
|
|
37
|
+
return root?.channels ? root.channels.cored : void 0;
|
|
38
|
+
}
|
|
39
|
+
function readEnvConfig() {
|
|
40
|
+
const env = process.env;
|
|
41
|
+
const result = {};
|
|
42
|
+
if (env[`${ENV_PREFIX}APP_ID`]) result.appId = env[`${ENV_PREFIX}APP_ID`];
|
|
43
|
+
if (env[`${ENV_PREFIX}APP_SECRET`])
|
|
44
|
+
result.appSecret = env[`${ENV_PREFIX}APP_SECRET`];
|
|
45
|
+
if (env[`${ENV_PREFIX}BACKEND_URL`])
|
|
46
|
+
result.backendUrl = env[`${ENV_PREFIX}BACKEND_URL`];
|
|
47
|
+
if (env[`${ENV_PREFIX}ENABLE_ENCRYPTION`] !== void 0)
|
|
48
|
+
result.enableEncryption = env[`${ENV_PREFIX}ENABLE_ENCRYPTION`] !== "false";
|
|
49
|
+
if (env[`${ENV_PREFIX}REQUEST_TIMEOUT`])
|
|
50
|
+
result.requestTimeout = Number(env[`${ENV_PREFIX}REQUEST_TIMEOUT`]);
|
|
51
|
+
if (env[`${ENV_PREFIX}REQUIRE_MENTION`] !== void 0)
|
|
52
|
+
result.requireMention = env[`${ENV_PREFIX}REQUIRE_MENTION`] !== "false";
|
|
53
|
+
if (env[`${ENV_PREFIX}BOT_USER_ID`])
|
|
54
|
+
result.botUserId = env[`${ENV_PREFIX}BOT_USER_ID`];
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
function listAccountIds(cfg) {
|
|
58
|
+
const ch = getChannelConfig(cfg);
|
|
59
|
+
if (!ch) {
|
|
60
|
+
if (process.env[`${ENV_PREFIX}APP_ID`]) return ["default"];
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
if (ch.accounts) return Object.keys(ch.accounts);
|
|
64
|
+
if (ch.appId) return ["default"];
|
|
65
|
+
if (process.env[`${ENV_PREFIX}APP_ID`]) return ["default"];
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
function resolveAccountConfig(cfg, accountId) {
|
|
69
|
+
const ch = getChannelConfig(cfg);
|
|
70
|
+
const id = accountId ?? "default";
|
|
71
|
+
const envConfig = readEnvConfig();
|
|
72
|
+
const raw = ch?.accounts?.[id] ?? ch;
|
|
73
|
+
return {
|
|
74
|
+
accountId: id,
|
|
75
|
+
enabled: raw?.enabled ?? true,
|
|
76
|
+
appId: raw?.appId ?? envConfig.appId ?? "",
|
|
77
|
+
appSecret: raw?.appSecret ?? envConfig.appSecret ?? "",
|
|
78
|
+
backendUrl: raw?.backendUrl ?? envConfig.backendUrl ?? "",
|
|
79
|
+
enableEncryption: raw?.enableEncryption ?? envConfig.enableEncryption ?? DEFAULTS.enableEncryption,
|
|
80
|
+
requestTimeout: raw?.requestTimeout ?? envConfig.requestTimeout ?? DEFAULTS.requestTimeout,
|
|
81
|
+
requireMention: raw?.requireMention ?? envConfig.requireMention ?? DEFAULTS.requireMention,
|
|
82
|
+
botUserId: raw?.botUserId ?? envConfig.botUserId,
|
|
83
|
+
inboundWhitelist: raw?.inboundWhitelist ?? [...DEFAULTS.inboundWhitelist]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function validateAccountConfig(config) {
|
|
87
|
+
const errors = [];
|
|
88
|
+
if (!config.appId) {
|
|
89
|
+
errors.push({
|
|
90
|
+
field: "appId",
|
|
91
|
+
message: `Account "${config.accountId}": appId is required. Set it in channels.cored.appId or CORED_APP_ID env var.`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (!config.appSecret) {
|
|
95
|
+
errors.push({
|
|
96
|
+
field: "appSecret",
|
|
97
|
+
message: `Account "${config.accountId}": appSecret is required. Set it in channels.cored.appSecret or CORED_APP_SECRET env var.`
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (!config.backendUrl) {
|
|
101
|
+
errors.push({
|
|
102
|
+
field: "backendUrl",
|
|
103
|
+
message: `Account "${config.accountId}": backendUrl is required. Set it in channels.cored.backendUrl or CORED_BACKEND_URL env var.`
|
|
104
|
+
});
|
|
105
|
+
} else if (!config.backendUrl.startsWith("http://") && !config.backendUrl.startsWith("https://")) {
|
|
106
|
+
errors.push({
|
|
107
|
+
field: "backendUrl",
|
|
108
|
+
message: `Account "${config.accountId}": backendUrl must start with http:// or https:// (got "${config.backendUrl}").`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (typeof config.requestTimeout !== "number" || !Number.isFinite(config.requestTimeout) || config.requestTimeout <= 0) {
|
|
112
|
+
errors.push({
|
|
113
|
+
field: "requestTimeout",
|
|
114
|
+
message: `Account "${config.accountId}": requestTimeout must be a positive number in milliseconds (got ${config.requestTimeout}).`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return errors;
|
|
118
|
+
}
|
|
119
|
+
function listEnabledAccountConfigs(cfg) {
|
|
120
|
+
const ids = listAccountIds(cfg);
|
|
121
|
+
return ids.map((id) => resolveAccountConfig(cfg, id)).filter((account) => account.enabled);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/core/cored-client.ts
|
|
125
|
+
var import_sdk = require("@cored-im/sdk");
|
|
126
|
+
var AUTH_ERROR_CODE = 40000006;
|
|
127
|
+
function isAuthError(err) {
|
|
128
|
+
return err instanceof import_sdk.ApiError && err.code === AUTH_ERROR_CODE;
|
|
129
|
+
}
|
|
130
|
+
var MessageType_TEXT = "text";
|
|
131
|
+
var clients = /* @__PURE__ */ new Map();
|
|
132
|
+
function makeLoggerAdapter(log) {
|
|
133
|
+
const emit = (level) => (msg, ...args) => {
|
|
134
|
+
log?.(`[${level}] ${msg}${args.length ? " " + JSON.stringify(args) : ""}`);
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
debug: emit("debug"),
|
|
138
|
+
info: emit("info"),
|
|
139
|
+
warn: emit("warn"),
|
|
140
|
+
error: emit("error")
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function createClient(opts) {
|
|
144
|
+
const { config, onMessage, log } = opts;
|
|
145
|
+
const accountId = config.accountId;
|
|
146
|
+
if (clients.has(accountId)) {
|
|
147
|
+
await destroyClient(accountId);
|
|
148
|
+
}
|
|
149
|
+
const sdkClient = await import_sdk.CoredClient.create(
|
|
150
|
+
config.backendUrl,
|
|
151
|
+
config.appId,
|
|
152
|
+
config.appSecret,
|
|
153
|
+
{
|
|
154
|
+
enableEncryption: config.enableEncryption,
|
|
155
|
+
requestTimeout: config.requestTimeout,
|
|
156
|
+
logger: log ? makeLoggerAdapter(log) : void 0,
|
|
157
|
+
logLevel: log ? import_sdk.LoggerLevel.Debug : import_sdk.LoggerLevel.Info
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
await sdkClient.preheat();
|
|
161
|
+
const managed = { client: sdkClient, config, connectionState: "connected" };
|
|
162
|
+
if (onMessage) {
|
|
163
|
+
const handler = (sdkEvent) => {
|
|
164
|
+
const normalized = normalizeSdkEvent(sdkEvent);
|
|
165
|
+
if (normalized) {
|
|
166
|
+
onMessage(normalized, config);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
sdkClient.Im.v1.Message.Event.onMessageReceive(
|
|
170
|
+
handler
|
|
171
|
+
);
|
|
172
|
+
managed.eventHandler = handler;
|
|
173
|
+
}
|
|
174
|
+
clients.set(accountId, managed);
|
|
175
|
+
return managed;
|
|
176
|
+
}
|
|
177
|
+
async function destroyClient(accountId) {
|
|
178
|
+
const managed = clients.get(accountId);
|
|
179
|
+
if (!managed) return;
|
|
180
|
+
managed.connectionState = "disconnecting";
|
|
181
|
+
if (managed.eventHandler) {
|
|
182
|
+
managed.client.Im.v1.Message.Event.offMessageReceive(
|
|
183
|
+
managed.eventHandler
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
await managed.client.close();
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
clients.delete(accountId);
|
|
191
|
+
}
|
|
192
|
+
async function destroyAllClients() {
|
|
193
|
+
const ids = [...clients.keys()];
|
|
194
|
+
await Promise.allSettled(ids.map((id) => destroyClient(id)));
|
|
195
|
+
}
|
|
196
|
+
function getClient(accountId) {
|
|
197
|
+
if (accountId && clients.has(accountId)) return clients.get(accountId);
|
|
198
|
+
if (clients.size > 0) return clients.values().next().value;
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
function clientCount() {
|
|
202
|
+
return clients.size;
|
|
203
|
+
}
|
|
204
|
+
function normalizeSdkEvent(sdk) {
|
|
205
|
+
const msg = sdk.body?.message;
|
|
206
|
+
if (!msg) return null;
|
|
207
|
+
const senderId = msg.sender_id;
|
|
208
|
+
let userId = "";
|
|
209
|
+
if (typeof senderId === "string") {
|
|
210
|
+
userId = senderId;
|
|
211
|
+
} else if (senderId && typeof senderId === "object") {
|
|
212
|
+
userId = senderId.user_id ?? senderId.open_user_id ?? senderId.union_user_id ?? "";
|
|
213
|
+
}
|
|
214
|
+
const mentionUsers = (msg.mention_user_list ?? []).map((u) => ({
|
|
215
|
+
userId: u.user_id?.user_id ?? u.user_id?.open_user_id ?? u.user_id?.union_user_id ?? ""
|
|
216
|
+
}));
|
|
217
|
+
const createdAt = typeof msg.message_created_at === "string" ? parseInt(msg.message_created_at, 10) || Date.now() : msg.message_created_at ?? Date.now();
|
|
218
|
+
return {
|
|
219
|
+
message: {
|
|
220
|
+
messageId: msg.message_id ?? "",
|
|
221
|
+
messageType: msg.message_type ?? "",
|
|
222
|
+
messageContent: msg.message_content,
|
|
223
|
+
chatId: msg.chat_id ?? "",
|
|
224
|
+
chatType: msg.chat_type ?? "direct",
|
|
225
|
+
sender: {
|
|
226
|
+
userId
|
|
227
|
+
},
|
|
228
|
+
createdAt,
|
|
229
|
+
mentionUserList: mentionUsers
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/messaging/outbound.ts
|
|
235
|
+
async function sendText(chatId, text, accountId, replyMessageId, logWarn) {
|
|
236
|
+
const managed = getClient(accountId);
|
|
237
|
+
if (!managed) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: new Error(
|
|
241
|
+
`[cored] no connected client for account=${accountId ?? "default"}`
|
|
242
|
+
)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const req = {
|
|
246
|
+
chat_id: chatId,
|
|
247
|
+
message_type: MessageType_TEXT,
|
|
248
|
+
message_content: {
|
|
249
|
+
text: { content: text }
|
|
250
|
+
},
|
|
251
|
+
reply_message_id: replyMessageId
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
const resp = await managed.client.Im.v1.Message.sendMessage(req);
|
|
255
|
+
return { ok: true, messageId: resp.message_id, provider: "cored" };
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (!isAuthError(err)) {
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
error: new Error(
|
|
261
|
+
`[cored] send failed for chat=${chatId}: ${err instanceof Error ? err.message : String(err)}`
|
|
262
|
+
)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
logWarn?.(
|
|
266
|
+
`[cored] auth error on send (chat=${chatId}, account=${accountId ?? "default"}) \u2014 refreshing token and retrying`
|
|
267
|
+
);
|
|
268
|
+
try {
|
|
269
|
+
await managed.client.preheat();
|
|
270
|
+
const resp = await managed.client.Im.v1.Message.sendMessage(req);
|
|
271
|
+
logWarn?.(
|
|
272
|
+
`[cored] auth retry succeeded (chat=${chatId}, account=${accountId ?? "default"})`
|
|
273
|
+
);
|
|
274
|
+
return { ok: true, messageId: resp.message_id, provider: "cored" };
|
|
275
|
+
} catch (retryErr) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
error: new Error(
|
|
279
|
+
`[cored] send failed after auth retry for chat=${chatId}: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`
|
|
280
|
+
)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function setTyping(chatId, accountId) {
|
|
286
|
+
const managed = getClient(accountId);
|
|
287
|
+
if (!managed) return;
|
|
288
|
+
try {
|
|
289
|
+
await managed.client.Im.v1.Chat.createTyping({ chat_id: chatId });
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function clearTyping(chatId, accountId) {
|
|
294
|
+
const managed = getClient(accountId);
|
|
295
|
+
if (!managed) return;
|
|
296
|
+
try {
|
|
297
|
+
await managed.client.Im.v1.Chat.deleteTyping({ chat_id: chatId });
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function readMessage(messageId, accountId) {
|
|
302
|
+
const managed = getClient(accountId);
|
|
303
|
+
if (!managed) return;
|
|
304
|
+
try {
|
|
305
|
+
await managed.client.Im.v1.Message.readMessage({ message_id: messageId });
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function makeDeliver(accountId, logWarn) {
|
|
310
|
+
return async (chatId, text) => {
|
|
311
|
+
const result = await sendText(chatId, text, accountId, void 0, logWarn);
|
|
312
|
+
if (!result.ok) {
|
|
313
|
+
throw result.error ?? new Error("[cored] send failed");
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/targets.ts
|
|
319
|
+
function parseTarget(to) {
|
|
320
|
+
const raw = String(to ?? "").trim();
|
|
321
|
+
if (!raw) return null;
|
|
322
|
+
const stripped = raw.replace(/^cored:/i, "");
|
|
323
|
+
if (stripped.startsWith("user:")) {
|
|
324
|
+
const id = stripped.slice("user:".length).trim();
|
|
325
|
+
return id ? { kind: "user", id } : null;
|
|
326
|
+
}
|
|
327
|
+
if (stripped.startsWith("chat:")) {
|
|
328
|
+
const id = stripped.slice("chat:".length).trim();
|
|
329
|
+
return id ? { kind: "chat", id } : null;
|
|
330
|
+
}
|
|
331
|
+
return stripped ? { kind: "chat", id: stripped } : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/channel.ts
|
|
335
|
+
var coredPlugin = {
|
|
336
|
+
id: "cored",
|
|
337
|
+
meta: {
|
|
338
|
+
id: "cored",
|
|
339
|
+
label: "Cored",
|
|
340
|
+
selectionLabel: "Cored",
|
|
341
|
+
docsPath: "/channels/cored",
|
|
342
|
+
blurb: "Cored enterprise IM channel",
|
|
343
|
+
aliases: ["cored", "cd"]
|
|
344
|
+
},
|
|
345
|
+
capabilities: {
|
|
346
|
+
chatTypes: ["direct", "group"]
|
|
347
|
+
},
|
|
348
|
+
config: {
|
|
349
|
+
listAccountIds: (cfg) => listAccountIds(cfg),
|
|
350
|
+
resolveAccount: (cfg, accountId) => resolveAccountConfig(cfg, accountId)
|
|
351
|
+
},
|
|
352
|
+
outbound: {
|
|
353
|
+
deliveryMode: "direct",
|
|
354
|
+
resolveTarget: ({ to }) => {
|
|
355
|
+
const target = parseTarget(to);
|
|
356
|
+
if (!target) {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
error: new Error(
|
|
360
|
+
`Cored requires --to <user:ID|chat:ID>, got: ${JSON.stringify(to)}`
|
|
361
|
+
)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return { ok: true, to: `${target.kind}:${target.id}` };
|
|
365
|
+
},
|
|
366
|
+
sendText: async ({
|
|
367
|
+
to,
|
|
368
|
+
text,
|
|
369
|
+
accountId
|
|
370
|
+
}) => {
|
|
371
|
+
const target = parseTarget(to);
|
|
372
|
+
if (!target) {
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
error: new Error(`[cored] invalid send target: ${to}`)
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return sendText(target.id, text, accountId);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/messaging/inbound.ts
|
|
384
|
+
function parseMessageEvent(event) {
|
|
385
|
+
const msg = event?.message;
|
|
386
|
+
if (!msg || !msg.messageId || !msg.chatId) return null;
|
|
387
|
+
const body = extractTextBody(msg);
|
|
388
|
+
if (body === null) return null;
|
|
389
|
+
const chatType = msg.chatType === "group" ? "group" : "direct";
|
|
390
|
+
const senderId = msg.sender?.userId || msg.sender?.openUserId || msg.sender?.unionUserId;
|
|
391
|
+
if (!senderId) return null;
|
|
392
|
+
const mentionUserIds = (msg.mentionUserList ?? []).map((u) => u.userId || u.openUserId || u.unionUserId || "").filter(Boolean);
|
|
393
|
+
return {
|
|
394
|
+
messageId: msg.messageId,
|
|
395
|
+
chatId: msg.chatId,
|
|
396
|
+
chatType,
|
|
397
|
+
senderId,
|
|
398
|
+
body,
|
|
399
|
+
timestamp: msg.createdAt ?? Date.now(),
|
|
400
|
+
mentionUserIds
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function extractTextBody(msg) {
|
|
404
|
+
if (msg.messageType !== "text") return null;
|
|
405
|
+
const content = msg.messageContent;
|
|
406
|
+
if (typeof content === "string") {
|
|
407
|
+
try {
|
|
408
|
+
const parsed = JSON.parse(content);
|
|
409
|
+
if (parsed?.text && typeof parsed.text === "object" && typeof parsed.text.content === "string")
|
|
410
|
+
return parsed.text.content.trim() || null;
|
|
411
|
+
if (typeof parsed?.text === "string") return parsed.text.trim() || null;
|
|
412
|
+
if (typeof parsed?.content === "string")
|
|
413
|
+
return parsed.content.trim() || null;
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
return content.trim() || null;
|
|
417
|
+
}
|
|
418
|
+
if (content && typeof content === "object") {
|
|
419
|
+
const obj = content;
|
|
420
|
+
if (obj.text && typeof obj.text === "object") {
|
|
421
|
+
const textObj = obj.text;
|
|
422
|
+
if (typeof textObj.content === "string") return textObj.content.trim() || null;
|
|
423
|
+
}
|
|
424
|
+
if (typeof obj.text === "string") return obj.text.trim() || null;
|
|
425
|
+
if (typeof obj.content === "string") return obj.content.trim() || null;
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
function checkMessageGate(msg, account) {
|
|
430
|
+
if (account.botUserId && msg.senderId === account.botUserId) {
|
|
431
|
+
return { pass: false, reason: "self-message" };
|
|
432
|
+
}
|
|
433
|
+
if (account.inboundWhitelist.length > 0) {
|
|
434
|
+
if (!account.inboundWhitelist.includes(msg.senderId)) {
|
|
435
|
+
return { pass: false, reason: "sender-not-in-whitelist" };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (msg.chatType === "group" && account.requireMention && !isBotMentioned(msg, account)) {
|
|
439
|
+
return { pass: false, reason: "group-no-mention" };
|
|
440
|
+
}
|
|
441
|
+
return { pass: true };
|
|
442
|
+
}
|
|
443
|
+
function isBotMentioned(msg, account) {
|
|
444
|
+
if (!account.botUserId) return false;
|
|
445
|
+
return msg.mentionUserIds.includes(account.botUserId);
|
|
446
|
+
}
|
|
447
|
+
var DEDUP_TTL_MS = 5 * 60 * 1e3;
|
|
448
|
+
var DEDUP_CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
449
|
+
var DEDUP_MAX_SIZE = 1e4;
|
|
450
|
+
var processedMessages = /* @__PURE__ */ new Map();
|
|
451
|
+
var lastCleanup = Date.now();
|
|
452
|
+
function isDuplicate(messageId) {
|
|
453
|
+
cleanupIfNeeded();
|
|
454
|
+
if (processedMessages.has(messageId)) return true;
|
|
455
|
+
processedMessages.set(messageId, Date.now());
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
function cleanupIfNeeded() {
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
if (now - lastCleanup < DEDUP_CLEANUP_INTERVAL_MS) return;
|
|
461
|
+
lastCleanup = now;
|
|
462
|
+
const expiry = now - DEDUP_TTL_MS;
|
|
463
|
+
for (const [id, ts] of processedMessages) {
|
|
464
|
+
if (ts < expiry) processedMessages.delete(id);
|
|
465
|
+
}
|
|
466
|
+
if (processedMessages.size > DEDUP_MAX_SIZE) {
|
|
467
|
+
const excess = processedMessages.size - DEDUP_MAX_SIZE;
|
|
468
|
+
const iter = processedMessages.keys();
|
|
469
|
+
for (let i = 0; i < excess; i++) {
|
|
470
|
+
const key = iter.next().value;
|
|
471
|
+
if (key !== void 0) processedMessages.delete(key);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function buildContext(msg, account) {
|
|
476
|
+
const isGroup = msg.chatType === "group";
|
|
477
|
+
const sessionKey = isGroup ? `cored:chat:${msg.chatId}` : `cored:user:${msg.senderId}`;
|
|
478
|
+
return {
|
|
479
|
+
Body: msg.body,
|
|
480
|
+
From: isGroup ? `cored:chat:${msg.chatId}` : `cored:user:${msg.senderId}`,
|
|
481
|
+
To: `cored:bot:${account.botUserId ?? account.appId}`,
|
|
482
|
+
SessionKey: sessionKey,
|
|
483
|
+
AccountId: account.accountId,
|
|
484
|
+
ChatType: isGroup ? "group" : "direct",
|
|
485
|
+
Provider: "cored",
|
|
486
|
+
Surface: "cored",
|
|
487
|
+
MessageSid: msg.messageId,
|
|
488
|
+
Timestamp: msg.timestamp,
|
|
489
|
+
CommandAuthorized: true,
|
|
490
|
+
_cored: {
|
|
491
|
+
accountId: account.accountId,
|
|
492
|
+
isGroup,
|
|
493
|
+
senderId: msg.senderId,
|
|
494
|
+
chatId: msg.chatId
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
async function processInboundMessage(api, account, event, opts) {
|
|
499
|
+
const logger = api.logger;
|
|
500
|
+
const parsed = parseMessageEvent(event);
|
|
501
|
+
if (!parsed) {
|
|
502
|
+
logger?.debug(
|
|
503
|
+
`[cored] ignoring unparseable event (messageId=${event?.message?.messageId ?? "unknown"} messageType=${event?.message?.messageType ?? "undefined"})`
|
|
504
|
+
);
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
const gate = checkMessageGate(parsed, account);
|
|
508
|
+
if (!gate.pass) {
|
|
509
|
+
logger?.debug(
|
|
510
|
+
`[cored] gated message=${parsed.messageId} reason=${gate.reason} chat=${parsed.chatId}`
|
|
511
|
+
);
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
if (isDuplicate(parsed.messageId)) {
|
|
515
|
+
logger?.debug(
|
|
516
|
+
`[cored] duplicate message=${parsed.messageId} chat=${parsed.chatId}`
|
|
517
|
+
);
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
const ctx = buildContext(parsed, account);
|
|
521
|
+
logger?.info(
|
|
522
|
+
`[cored] dispatching message=${parsed.messageId} chat=${parsed.chatId} sender=${parsed.senderId} type=${parsed.chatType}`
|
|
523
|
+
);
|
|
524
|
+
const runtime = api.runtime;
|
|
525
|
+
if (!runtime?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher) {
|
|
526
|
+
logger?.warn("[cored] runtime.channel.reply not available \u2014 cannot dispatch");
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
const cfgSession = api.config?.session;
|
|
530
|
+
const storePath = runtime.channel.session?.resolveStorePath?.(
|
|
531
|
+
cfgSession?.store,
|
|
532
|
+
{ agentId: "main" }
|
|
533
|
+
) ?? "";
|
|
534
|
+
await runtime.channel.session?.recordInboundSession?.({
|
|
535
|
+
storePath,
|
|
536
|
+
sessionKey: ctx.SessionKey,
|
|
537
|
+
ctx,
|
|
538
|
+
updateLastRoute: ctx.ChatType === "direct" ? {
|
|
539
|
+
sessionKey: ctx.SessionKey,
|
|
540
|
+
channel: "cored",
|
|
541
|
+
to: parsed.chatId,
|
|
542
|
+
accountId: account.accountId
|
|
543
|
+
} : void 0
|
|
544
|
+
});
|
|
545
|
+
logger?.debug(
|
|
546
|
+
`[cored] dispatch starting for message=${parsed.messageId} session=${ctx.SessionKey}`
|
|
547
|
+
);
|
|
548
|
+
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
549
|
+
ctx,
|
|
550
|
+
cfg: api.config,
|
|
551
|
+
dispatcherOptions: {
|
|
552
|
+
deliver: async (payload) => {
|
|
553
|
+
logger?.info(
|
|
554
|
+
`[cored] deliver callback called for message=${parsed.messageId} hasText=${!!payload.text} textLen=${payload.text?.length ?? 0}`
|
|
555
|
+
);
|
|
556
|
+
if (payload.text) {
|
|
557
|
+
await opts.deliver(parsed.chatId, payload.text);
|
|
558
|
+
logger?.info(
|
|
559
|
+
`[cored] deliver completed for message=${parsed.messageId} chat=${parsed.chatId}`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
onError: (err, info) => {
|
|
564
|
+
logger?.error(
|
|
565
|
+
`[cored] ${info?.kind ?? "reply"} error for message=${parsed.messageId}: ${err}`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
logger?.info(
|
|
571
|
+
`[cored] dispatch finished for message=${parsed.messageId}`
|
|
572
|
+
);
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/index.ts
|
|
577
|
+
function register(api) {
|
|
578
|
+
api.registerChannel({ plugin: coredPlugin });
|
|
579
|
+
api.registerService({
|
|
580
|
+
id: "cored-sdk",
|
|
581
|
+
start: async () => {
|
|
582
|
+
if (clientCount() > 0) return;
|
|
583
|
+
const accounts = listEnabledAccountConfigs(api.config);
|
|
584
|
+
if (accounts.length === 0) {
|
|
585
|
+
api.logger?.warn("[cored] no enabled account config found \u2014 service idle");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
for (const account of accounts) {
|
|
589
|
+
const errors = validateAccountConfig(account);
|
|
590
|
+
if (errors.length > 0) {
|
|
591
|
+
api.logger?.warn(
|
|
592
|
+
`[cored] skipping account=${account.accountId}: ${errors.map((e) => e.message).join("; ")}`
|
|
593
|
+
);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
await startAccount(api, account);
|
|
598
|
+
api.logger?.info(
|
|
599
|
+
`[cored] account=${account.accountId} connected (appId=${account.appId})`
|
|
600
|
+
);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
api.logger?.error(
|
|
603
|
+
`[cored] account=${account.accountId} failed to start: ${err instanceof Error ? err.message : String(err)}`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
api.logger?.info(`[cored] service started with ${clientCount()} account(s)`);
|
|
608
|
+
},
|
|
609
|
+
stop: async () => {
|
|
610
|
+
await destroyAllClients();
|
|
611
|
+
api.logger?.info("[cored] service stopped \u2014 all clients disconnected");
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
api.logger?.info("[cored] plugin registered");
|
|
615
|
+
}
|
|
616
|
+
async function startAccount(api, account) {
|
|
617
|
+
const deliver = makeDeliver(account.accountId, (msg) => api.logger?.warn(msg));
|
|
618
|
+
await createClient({
|
|
619
|
+
config: account,
|
|
620
|
+
log: (msg) => api.logger?.debug(msg),
|
|
621
|
+
onMessage: (event, accountConfig) => {
|
|
622
|
+
handleInbound(api, accountConfig, event, deliver).catch((err) => {
|
|
623
|
+
api.logger?.error(
|
|
624
|
+
`[cored] unhandled inbound error for account=${accountConfig.accountId}: ${err}`
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
async function handleInbound(api, account, event, deliver) {
|
|
631
|
+
const chatId = event.message?.chatId;
|
|
632
|
+
const messageId = event.message?.messageId;
|
|
633
|
+
if (chatId) {
|
|
634
|
+
setTyping(chatId, account.accountId).catch(() => {
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (messageId) {
|
|
638
|
+
readMessage(messageId, account.accountId).catch(() => {
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
await processInboundMessage(api, account, event, { deliver });
|
|
643
|
+
} finally {
|
|
644
|
+
if (chatId) {
|
|
645
|
+
clearTyping(chatId, account.accountId).catch(() => {
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
//# sourceMappingURL=index.cjs.map
|