@nextclaw/channel-runtime 0.1.22 → 0.1.24
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 +21 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +234 -23
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NextClaw contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
CHANGED
|
@@ -175,14 +175,32 @@ declare class QQChannel extends BaseChannel<Config["channels"]["qq"]> {
|
|
|
175
175
|
private bot;
|
|
176
176
|
private processedIds;
|
|
177
177
|
private processedSet;
|
|
178
|
+
private reconnectTimer;
|
|
179
|
+
private connectTask;
|
|
180
|
+
private reconnectAttempt;
|
|
181
|
+
private readonly reconnectBaseMs;
|
|
182
|
+
private readonly reconnectMaxMs;
|
|
178
183
|
constructor(config: Config["channels"]["qq"], bus: MessageBus);
|
|
179
184
|
start(): Promise<void>;
|
|
180
185
|
stop(): Promise<void>;
|
|
181
186
|
send(msg: OutboundMessage): Promise<void>;
|
|
182
187
|
private handleIncoming;
|
|
188
|
+
private resolveSenderName;
|
|
189
|
+
private decorateSpeakerPrefix;
|
|
190
|
+
private sanitizeSpeakerToken;
|
|
183
191
|
private isDuplicate;
|
|
184
192
|
private sendWithTokenRetry;
|
|
185
193
|
private isTokenExpiredError;
|
|
194
|
+
private tryConnect;
|
|
195
|
+
private connect;
|
|
196
|
+
private createBot;
|
|
197
|
+
private handleSessionDead;
|
|
198
|
+
private scheduleReconnect;
|
|
199
|
+
private clearReconnectTimer;
|
|
200
|
+
private teardownBot;
|
|
201
|
+
private safeStopBot;
|
|
202
|
+
private getBackoffDelayMs;
|
|
203
|
+
private formatError;
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
declare class SlackChannel extends BaseChannel<Config["channels"]["slack"]> {
|
|
@@ -219,6 +237,7 @@ declare class TelegramChannel extends BaseChannel<Config["channels"]["telegram"]
|
|
|
219
237
|
private startTyping;
|
|
220
238
|
private stopTyping;
|
|
221
239
|
private resolveAccountId;
|
|
240
|
+
private maybeAddAckReaction;
|
|
222
241
|
private isAllowedByPolicy;
|
|
223
242
|
private resolveMentionState;
|
|
224
243
|
}
|
package/dist/index.js
CHANGED
|
@@ -2414,6 +2414,7 @@ function sleep3(ms) {
|
|
|
2414
2414
|
import {
|
|
2415
2415
|
Bot,
|
|
2416
2416
|
ReceiverMode,
|
|
2417
|
+
SessionEvents,
|
|
2417
2418
|
segment
|
|
2418
2419
|
} from "qq-official-bot";
|
|
2419
2420
|
var QQChannel = class extends BaseChannel {
|
|
@@ -2421,38 +2422,31 @@ var QQChannel = class extends BaseChannel {
|
|
|
2421
2422
|
bot = null;
|
|
2422
2423
|
processedIds = [];
|
|
2423
2424
|
processedSet = /* @__PURE__ */ new Set();
|
|
2425
|
+
reconnectTimer = null;
|
|
2426
|
+
connectTask = null;
|
|
2427
|
+
reconnectAttempt = 0;
|
|
2428
|
+
reconnectBaseMs = 1e3;
|
|
2429
|
+
reconnectMaxMs = 6e4;
|
|
2424
2430
|
constructor(config, bus) {
|
|
2425
2431
|
super(config, bus);
|
|
2426
2432
|
}
|
|
2427
2433
|
async start() {
|
|
2428
|
-
this.running = true;
|
|
2429
2434
|
if (!this.config.appId || !this.config.secret) {
|
|
2435
|
+
this.running = false;
|
|
2430
2436
|
throw new Error("QQ appId/appSecret not configured");
|
|
2431
2437
|
}
|
|
2432
|
-
this.
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
intents: ["C2C_MESSAGE_CREATE", "GROUP_AT_MESSAGE_CREATE"],
|
|
2437
|
-
removeAt: true,
|
|
2438
|
-
logLevel: "info"
|
|
2439
|
-
});
|
|
2440
|
-
this.bot.on("message.private", async (event) => {
|
|
2441
|
-
await this.handleIncoming(event);
|
|
2442
|
-
});
|
|
2443
|
-
this.bot.on("message.group", async (event) => {
|
|
2444
|
-
await this.handleIncoming(event);
|
|
2445
|
-
});
|
|
2446
|
-
await this.bot.start();
|
|
2447
|
-
console.log("QQ bot connected");
|
|
2438
|
+
this.running = true;
|
|
2439
|
+
this.reconnectAttempt = 0;
|
|
2440
|
+
this.clearReconnectTimer();
|
|
2441
|
+
this.tryConnect("startup");
|
|
2448
2442
|
}
|
|
2449
2443
|
async stop() {
|
|
2450
2444
|
this.running = false;
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
this.
|
|
2445
|
+
this.clearReconnectTimer();
|
|
2446
|
+
this.reconnectAttempt = 0;
|
|
2447
|
+
await this.teardownBot();
|
|
2448
|
+
if (this.connectTask) {
|
|
2449
|
+
await this.connectTask;
|
|
2456
2450
|
}
|
|
2457
2451
|
}
|
|
2458
2452
|
async send(msg) {
|
|
@@ -2497,7 +2491,8 @@ var QQChannel = class extends BaseChannel {
|
|
|
2497
2491
|
return;
|
|
2498
2492
|
}
|
|
2499
2493
|
const content = event.raw_message?.trim() ?? "";
|
|
2500
|
-
const
|
|
2494
|
+
const normalizedContent = content || "[empty message]";
|
|
2495
|
+
const senderName = this.resolveSenderName(rawEvent);
|
|
2501
2496
|
let chatId = senderId;
|
|
2502
2497
|
let messageType = "private";
|
|
2503
2498
|
const qqMeta = {};
|
|
@@ -2507,21 +2502,36 @@ var QQChannel = class extends BaseChannel {
|
|
|
2507
2502
|
chatId = groupId;
|
|
2508
2503
|
qqMeta.groupId = groupId;
|
|
2509
2504
|
qqMeta.userId = senderId;
|
|
2505
|
+
if (senderName) {
|
|
2506
|
+
qqMeta.userName = senderName;
|
|
2507
|
+
}
|
|
2510
2508
|
} else if (event.message_type === "guild") {
|
|
2511
2509
|
messageType = "guild";
|
|
2512
2510
|
chatId = event.channel_id ?? "";
|
|
2513
2511
|
qqMeta.guildId = event.guild_id;
|
|
2514
2512
|
qqMeta.channelId = event.channel_id;
|
|
2515
2513
|
qqMeta.userId = senderId;
|
|
2514
|
+
if (senderName) {
|
|
2515
|
+
qqMeta.userName = senderName;
|
|
2516
|
+
}
|
|
2516
2517
|
} else if (event.sub_type === "direct") {
|
|
2517
2518
|
messageType = "direct";
|
|
2518
2519
|
chatId = event.guild_id ?? "";
|
|
2519
2520
|
qqMeta.guildId = event.guild_id;
|
|
2520
2521
|
qqMeta.userId = senderId;
|
|
2522
|
+
if (senderName) {
|
|
2523
|
+
qqMeta.userName = senderName;
|
|
2524
|
+
}
|
|
2521
2525
|
} else {
|
|
2522
2526
|
qqMeta.userId = senderId;
|
|
2523
2527
|
}
|
|
2524
2528
|
qqMeta.messageType = messageType;
|
|
2529
|
+
const safeContent = this.decorateSpeakerPrefix({
|
|
2530
|
+
content: normalizedContent,
|
|
2531
|
+
messageType,
|
|
2532
|
+
senderId,
|
|
2533
|
+
senderName
|
|
2534
|
+
});
|
|
2525
2535
|
if (!chatId) {
|
|
2526
2536
|
return;
|
|
2527
2537
|
}
|
|
@@ -2539,6 +2549,42 @@ var QQChannel = class extends BaseChannel {
|
|
|
2539
2549
|
}
|
|
2540
2550
|
});
|
|
2541
2551
|
}
|
|
2552
|
+
resolveSenderName(rawEvent) {
|
|
2553
|
+
const candidates = [
|
|
2554
|
+
rawEvent.sender?.card,
|
|
2555
|
+
rawEvent.sender?.nickname,
|
|
2556
|
+
rawEvent.sender?.nick,
|
|
2557
|
+
rawEvent.sender?.username
|
|
2558
|
+
];
|
|
2559
|
+
for (const value of candidates) {
|
|
2560
|
+
if (typeof value !== "string") {
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
const normalized = value.trim();
|
|
2564
|
+
if (normalized) {
|
|
2565
|
+
return normalized;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
return null;
|
|
2569
|
+
}
|
|
2570
|
+
decorateSpeakerPrefix(params) {
|
|
2571
|
+
if (params.messageType !== "group" && params.messageType !== "guild") {
|
|
2572
|
+
return params.content;
|
|
2573
|
+
}
|
|
2574
|
+
const userId = this.sanitizeSpeakerToken(params.senderId);
|
|
2575
|
+
if (!userId) {
|
|
2576
|
+
return params.content;
|
|
2577
|
+
}
|
|
2578
|
+
const name = this.sanitizeSpeakerToken(params.senderName ?? "");
|
|
2579
|
+
const speakerFields = [`user_id=${userId}`];
|
|
2580
|
+
if (name) {
|
|
2581
|
+
speakerFields.push(`name=${name}`);
|
|
2582
|
+
}
|
|
2583
|
+
return `[speaker:${speakerFields.join(";")}] ${params.content}`;
|
|
2584
|
+
}
|
|
2585
|
+
sanitizeSpeakerToken(value) {
|
|
2586
|
+
return value.replace(/[\r\n;\]]/g, " ").trim();
|
|
2587
|
+
}
|
|
2542
2588
|
isDuplicate(messageId) {
|
|
2543
2589
|
if (this.processedSet.has(messageId)) {
|
|
2544
2590
|
return true;
|
|
@@ -2568,6 +2614,117 @@ var QQChannel = class extends BaseChannel {
|
|
|
2568
2614
|
const message = error instanceof Error ? error.message : String(error);
|
|
2569
2615
|
return message.includes("code(11244)") || message.toLowerCase().includes("token not exist or expire");
|
|
2570
2616
|
}
|
|
2617
|
+
tryConnect(trigger) {
|
|
2618
|
+
if (!this.running || this.bot || this.connectTask) {
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
this.connectTask = this.connect(trigger).finally(() => {
|
|
2622
|
+
this.connectTask = null;
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
async connect(trigger) {
|
|
2626
|
+
let candidate = null;
|
|
2627
|
+
try {
|
|
2628
|
+
candidate = this.createBot();
|
|
2629
|
+
await candidate.start();
|
|
2630
|
+
if (!this.running) {
|
|
2631
|
+
await this.safeStopBot(candidate);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
this.bot = candidate;
|
|
2635
|
+
this.reconnectAttempt = 0;
|
|
2636
|
+
console.log("QQ bot connected");
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
if (candidate) {
|
|
2639
|
+
await this.safeStopBot(candidate);
|
|
2640
|
+
}
|
|
2641
|
+
if (!this.running) {
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
this.reconnectAttempt += 1;
|
|
2645
|
+
const delayMs = this.getBackoffDelayMs(this.reconnectAttempt);
|
|
2646
|
+
console.error(
|
|
2647
|
+
`[qq] start failed (${trigger}, attempt ${this.reconnectAttempt}), retry in ${delayMs}ms: ${this.formatError(error)}`
|
|
2648
|
+
);
|
|
2649
|
+
this.scheduleReconnect(delayMs, `${trigger}-retry`);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
createBot() {
|
|
2653
|
+
const bot = new Bot({
|
|
2654
|
+
appid: this.config.appId,
|
|
2655
|
+
secret: this.config.secret,
|
|
2656
|
+
mode: ReceiverMode.WEBSOCKET,
|
|
2657
|
+
intents: ["C2C_MESSAGE_CREATE", "GROUP_AT_MESSAGE_CREATE"],
|
|
2658
|
+
removeAt: true,
|
|
2659
|
+
logLevel: "info"
|
|
2660
|
+
});
|
|
2661
|
+
bot.on("message.private", async (event) => {
|
|
2662
|
+
await this.handleIncoming(event);
|
|
2663
|
+
});
|
|
2664
|
+
bot.on("message.group", async (event) => {
|
|
2665
|
+
await this.handleIncoming(event);
|
|
2666
|
+
});
|
|
2667
|
+
bot.sessionManager.on(SessionEvents.DEAD, () => {
|
|
2668
|
+
void this.handleSessionDead(bot);
|
|
2669
|
+
});
|
|
2670
|
+
return bot;
|
|
2671
|
+
}
|
|
2672
|
+
async handleSessionDead(bot) {
|
|
2673
|
+
if (!this.running || this.bot !== bot) {
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
this.bot = null;
|
|
2677
|
+
await this.safeStopBot(bot);
|
|
2678
|
+
this.reconnectAttempt += 1;
|
|
2679
|
+
const delayMs = this.getBackoffDelayMs(this.reconnectAttempt);
|
|
2680
|
+
console.error(`[qq] session dead, reconnect in ${delayMs}ms`);
|
|
2681
|
+
this.scheduleReconnect(delayMs, "session-dead");
|
|
2682
|
+
}
|
|
2683
|
+
scheduleReconnect(delayMs, trigger) {
|
|
2684
|
+
if (!this.running) {
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
this.clearReconnectTimer();
|
|
2688
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2689
|
+
this.reconnectTimer = null;
|
|
2690
|
+
this.tryConnect(trigger);
|
|
2691
|
+
}, delayMs);
|
|
2692
|
+
}
|
|
2693
|
+
clearReconnectTimer() {
|
|
2694
|
+
if (!this.reconnectTimer) {
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
clearTimeout(this.reconnectTimer);
|
|
2698
|
+
this.reconnectTimer = null;
|
|
2699
|
+
}
|
|
2700
|
+
async teardownBot() {
|
|
2701
|
+
if (!this.bot) {
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
const bot = this.bot;
|
|
2705
|
+
this.bot = null;
|
|
2706
|
+
await this.safeStopBot(bot);
|
|
2707
|
+
}
|
|
2708
|
+
async safeStopBot(bot) {
|
|
2709
|
+
bot.removeAllListeners("message.private");
|
|
2710
|
+
bot.removeAllListeners("message.group");
|
|
2711
|
+
bot.sessionManager.removeAllListeners(SessionEvents.DEAD);
|
|
2712
|
+
try {
|
|
2713
|
+
await bot.stop();
|
|
2714
|
+
} catch {
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
getBackoffDelayMs(attempt) {
|
|
2718
|
+
const jitter = Math.floor(Math.random() * 500);
|
|
2719
|
+
const exp = Math.min(this.reconnectMaxMs, this.reconnectBaseMs * 2 ** Math.max(0, attempt - 1));
|
|
2720
|
+
return Math.min(this.reconnectMaxMs, exp + jitter);
|
|
2721
|
+
}
|
|
2722
|
+
formatError(error) {
|
|
2723
|
+
if (error instanceof Error) {
|
|
2724
|
+
return error.stack ?? error.message;
|
|
2725
|
+
}
|
|
2726
|
+
return String(error);
|
|
2727
|
+
}
|
|
2571
2728
|
};
|
|
2572
2729
|
|
|
2573
2730
|
// src/channels/slack.ts
|
|
@@ -2988,6 +3145,12 @@ Just send me a text message to chat!`;
|
|
|
2988
3145
|
contentParts.push(`[${mediaType}: ${finalPath}]`);
|
|
2989
3146
|
}
|
|
2990
3147
|
}
|
|
3148
|
+
await this.maybeAddAckReaction({
|
|
3149
|
+
message,
|
|
3150
|
+
chatId,
|
|
3151
|
+
isGroup,
|
|
3152
|
+
mentionState
|
|
3153
|
+
});
|
|
2991
3154
|
const content = contentParts.length ? contentParts.join("\n") : "[empty message]";
|
|
2992
3155
|
this.startTyping(chatId);
|
|
2993
3156
|
try {
|
|
@@ -3024,6 +3187,35 @@ Just send me a text message to chat!`;
|
|
|
3024
3187
|
const accountId = this.config.accountId?.trim();
|
|
3025
3188
|
return accountId || "default";
|
|
3026
3189
|
}
|
|
3190
|
+
async maybeAddAckReaction(params) {
|
|
3191
|
+
if (!this.bot) {
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
if (typeof params.message.message_id !== "number") {
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
const emoji = (this.config.ackReaction ?? "\u{1F440}").trim();
|
|
3198
|
+
if (!emoji) {
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
const shouldAck = shouldSendAckReaction({
|
|
3202
|
+
scope: this.config.ackReactionScope,
|
|
3203
|
+
isDirect: !params.isGroup,
|
|
3204
|
+
isGroup: params.isGroup,
|
|
3205
|
+
requireMention: params.mentionState.requireMention,
|
|
3206
|
+
wasMentioned: params.mentionState.wasMentioned
|
|
3207
|
+
});
|
|
3208
|
+
if (!shouldAck) {
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
const reaction = [{ type: "emoji", emoji }];
|
|
3212
|
+
try {
|
|
3213
|
+
await this.bot.setMessageReaction(Number(params.chatId), params.message.message_id, {
|
|
3214
|
+
reaction
|
|
3215
|
+
});
|
|
3216
|
+
} catch {
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3027
3219
|
isAllowedByPolicy(params) {
|
|
3028
3220
|
if (!params.isGroup) {
|
|
3029
3221
|
if (this.config.dmPolicy === "disabled") {
|
|
@@ -3150,6 +3342,25 @@ function inferMediaMimeType(mediaType) {
|
|
|
3150
3342
|
}
|
|
3151
3343
|
return void 0;
|
|
3152
3344
|
}
|
|
3345
|
+
function shouldSendAckReaction(params) {
|
|
3346
|
+
const scope = params.scope ?? "all";
|
|
3347
|
+
if (scope === "off") {
|
|
3348
|
+
return false;
|
|
3349
|
+
}
|
|
3350
|
+
if (scope === "all") {
|
|
3351
|
+
return true;
|
|
3352
|
+
}
|
|
3353
|
+
if (scope === "direct") {
|
|
3354
|
+
return params.isDirect;
|
|
3355
|
+
}
|
|
3356
|
+
if (scope === "group-all") {
|
|
3357
|
+
return params.isGroup;
|
|
3358
|
+
}
|
|
3359
|
+
if (scope === "group-mentions") {
|
|
3360
|
+
return params.isGroup && params.requireMention && params.wasMentioned;
|
|
3361
|
+
}
|
|
3362
|
+
return false;
|
|
3363
|
+
}
|
|
3153
3364
|
function markdownToTelegramHtml(text) {
|
|
3154
3365
|
if (!text) {
|
|
3155
3366
|
return "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/channel-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Runtime implementations for NextClaw builtin channel plugins.",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@larksuiteoapi/node-sdk": "^1.58.0",
|
|
18
|
-
"@nextclaw/core": "^0.6.
|
|
18
|
+
"@nextclaw/core": "^0.6.41",
|
|
19
19
|
"@slack/socket-mode": "^1.3.3",
|
|
20
20
|
"@slack/web-api": "^7.6.0",
|
|
21
21
|
"dingtalk-stream": "^2.1.4",
|