@theowlops/channelhub 1.0.0 → 1.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/dist/channels/discord/adapter.d.ts +2 -0
- package/dist/channels/discord/index.js +13 -0
- package/dist/channels/slack/adapter.d.ts +2 -0
- package/dist/channels/slack/index.js +13 -0
- package/dist/channels/telegram/adapter.d.ts +2 -0
- package/dist/channels/telegram/index.js +18 -0
- package/dist/channels/zalo/adapter.d.ts +13 -1
- package/dist/channels/zalo/index.js +168 -31
- package/dist/core/context.d.ts +3 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +132 -0
- package/dist/core/stream.d.ts +16 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/index.cjs +345 -33
- package/dist/index.js +345 -33
- package/package.json +1 -1
|
@@ -21,4 +21,6 @@ export declare class DiscordChannelAdapter extends BaseChannel {
|
|
|
21
21
|
sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
|
|
22
22
|
sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
|
|
23
23
|
addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
|
|
24
|
+
sendTyping(chatId: string): Promise<void>;
|
|
25
|
+
editText(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
|
|
24
26
|
}
|
|
@@ -124,6 +124,19 @@ class DiscordChannelAdapter extends BaseChannel {
|
|
|
124
124
|
const encoded = encodeURIComponent(emoji);
|
|
125
125
|
await this.callApi("PUT", `/channels/${chatId}/messages/${messageId}/reactions/${encoded}/@me`);
|
|
126
126
|
}
|
|
127
|
+
async sendTyping(chatId) {
|
|
128
|
+
await this.callApi("POST", `/channels/${chatId}/typing`, {});
|
|
129
|
+
}
|
|
130
|
+
async editText(chatId, messageId, text) {
|
|
131
|
+
const res = await this.callApi("PATCH", `/channels/${chatId}/messages/${messageId}`, {
|
|
132
|
+
content: text
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
messageId: String(res.id || messageId),
|
|
136
|
+
chatId,
|
|
137
|
+
timestamp: Date.now()
|
|
138
|
+
};
|
|
139
|
+
}
|
|
127
140
|
}
|
|
128
141
|
export {
|
|
129
142
|
DiscordChannelAdapter
|
|
@@ -17,4 +17,6 @@ export declare class SlackChannelAdapter extends BaseChannel {
|
|
|
17
17
|
sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
|
|
18
18
|
sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
|
|
19
19
|
addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
|
|
20
|
+
sendTyping(chatId: string): Promise<void>;
|
|
21
|
+
editText(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
|
|
20
22
|
}
|
|
@@ -116,6 +116,19 @@ class SlackChannelAdapter extends BaseChannel {
|
|
|
116
116
|
name: cleanName
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
|
+
async sendTyping(chatId) {}
|
|
120
|
+
async editText(chatId, messageId, text) {
|
|
121
|
+
const res = await this.callApi("chat.update", {
|
|
122
|
+
channel: chatId,
|
|
123
|
+
ts: messageId,
|
|
124
|
+
text
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
messageId: String(res.ts || messageId),
|
|
128
|
+
chatId,
|
|
129
|
+
timestamp: Date.now()
|
|
130
|
+
};
|
|
131
|
+
}
|
|
119
132
|
}
|
|
120
133
|
export {
|
|
121
134
|
SlackChannelAdapter
|
|
@@ -18,4 +18,6 @@ export declare class TelegramChannelAdapter extends BaseChannel {
|
|
|
18
18
|
sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
|
|
19
19
|
sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
|
|
20
20
|
addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
|
|
21
|
+
sendTyping(chatId: string): Promise<void>;
|
|
22
|
+
editText(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
|
|
21
23
|
}
|
|
@@ -173,6 +173,24 @@ class TelegramChannelAdapter extends BaseChannel {
|
|
|
173
173
|
reaction: [{ type: "emoji", emoji }]
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
|
+
async sendTyping(chatId) {
|
|
177
|
+
await this.callApi("sendChatAction", {
|
|
178
|
+
chat_id: chatId,
|
|
179
|
+
action: "typing"
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async editText(chatId, messageId, text) {
|
|
183
|
+
const res = await this.callApi("editMessageText", {
|
|
184
|
+
chat_id: chatId,
|
|
185
|
+
message_id: Number(messageId),
|
|
186
|
+
text
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
messageId: String(res.message_id || messageId),
|
|
190
|
+
chatId,
|
|
191
|
+
timestamp: Date.now()
|
|
192
|
+
};
|
|
193
|
+
}
|
|
176
194
|
}
|
|
177
195
|
export {
|
|
178
196
|
TelegramChannelAdapter
|
|
@@ -5,18 +5,30 @@ export interface ZaloAdapterConfig {
|
|
|
5
5
|
credentialsPath?: string;
|
|
6
6
|
ownId?: string;
|
|
7
7
|
defaultIsGroup?: boolean;
|
|
8
|
+
minDelayMs?: number;
|
|
9
|
+
maxDelayMs?: number;
|
|
10
|
+
cacheLimit?: number;
|
|
8
11
|
}
|
|
12
|
+
export declare const EMOJI_TO_ZALO: Record<string, string>;
|
|
9
13
|
export declare class ZaloChannelAdapter extends BaseChannel {
|
|
10
14
|
readonly name: ChannelType;
|
|
11
15
|
private api;
|
|
12
16
|
private ownId?;
|
|
13
17
|
private config;
|
|
18
|
+
private threadTypeCache;
|
|
19
|
+
private messageCache;
|
|
20
|
+
private sendQueue;
|
|
14
21
|
constructor(config?: ZaloAdapterConfig);
|
|
15
22
|
connect(): Promise<void>;
|
|
16
23
|
disconnect(): Promise<void>;
|
|
17
24
|
private setupEventListener;
|
|
25
|
+
private recordInbound;
|
|
26
|
+
private resolveThreadType;
|
|
27
|
+
private resolveQuote;
|
|
28
|
+
private enqueueSend;
|
|
18
29
|
private normalizeMessage;
|
|
19
30
|
sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
|
|
20
|
-
sendMedia(chatId: string, media: MediaPayload,
|
|
31
|
+
sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
|
|
21
32
|
addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
|
|
33
|
+
sendTyping(chatId: string): Promise<void>;
|
|
22
34
|
}
|
|
@@ -37488,14 +37488,47 @@ class BaseChannel extends EventEmitter {
|
|
|
37488
37488
|
}
|
|
37489
37489
|
|
|
37490
37490
|
// src/channels/zalo/adapter.ts
|
|
37491
|
+
var EMOJI_TO_ZALO = {
|
|
37492
|
+
"❤️": "HEART",
|
|
37493
|
+
"\uD83D\uDC96": "HEART",
|
|
37494
|
+
"\uD83D\uDC4D": "LIKE",
|
|
37495
|
+
"\uD83D\uDE06": "HAHA",
|
|
37496
|
+
"\uD83D\uDE02": "TEARS_OF_JOY",
|
|
37497
|
+
"\uD83D\uDE2E": "WOW",
|
|
37498
|
+
"\uD83D\uDE2D": "CRY",
|
|
37499
|
+
"\uD83D\uDE21": "ANGRY",
|
|
37500
|
+
"\uD83D\uDE18": "KISS",
|
|
37501
|
+
"\uD83D\uDCA9": "SHIT",
|
|
37502
|
+
"\uD83C\uDF39": "ROSE",
|
|
37503
|
+
"\uD83D\uDC94": "BROKEN_HEART",
|
|
37504
|
+
"\uD83D\uDC4E": "DISLIKE",
|
|
37505
|
+
"\uD83D\uDE0D": "LOVE",
|
|
37506
|
+
"\uD83E\uDD14": "CONFUSED",
|
|
37507
|
+
"\uD83D\uDE09": "WINK",
|
|
37508
|
+
heart: "HEART",
|
|
37509
|
+
like: "LIKE",
|
|
37510
|
+
haha: "HAHA",
|
|
37511
|
+
wow: "WOW",
|
|
37512
|
+
cry: "CRY",
|
|
37513
|
+
angry: "ANGRY"
|
|
37514
|
+
};
|
|
37515
|
+
|
|
37491
37516
|
class ZaloChannelAdapter extends BaseChannel {
|
|
37492
37517
|
name = "zalo";
|
|
37493
37518
|
api;
|
|
37494
37519
|
ownId;
|
|
37495
37520
|
config;
|
|
37521
|
+
threadTypeCache = new Map;
|
|
37522
|
+
messageCache = new Map;
|
|
37523
|
+
sendQueue = Promise.resolve();
|
|
37496
37524
|
constructor(config = {}) {
|
|
37497
37525
|
super();
|
|
37498
|
-
this.config =
|
|
37526
|
+
this.config = {
|
|
37527
|
+
minDelayMs: 300,
|
|
37528
|
+
maxDelayMs: 800,
|
|
37529
|
+
cacheLimit: 1000,
|
|
37530
|
+
...config
|
|
37531
|
+
};
|
|
37499
37532
|
if (config.api) {
|
|
37500
37533
|
this.api = config.api;
|
|
37501
37534
|
}
|
|
@@ -37530,17 +37563,101 @@ class ZaloChannelAdapter extends BaseChannel {
|
|
|
37530
37563
|
if (!this.api?.listener?.on)
|
|
37531
37564
|
return;
|
|
37532
37565
|
this.api.listener.on("message", (raw) => {
|
|
37566
|
+
this.recordInbound(raw);
|
|
37533
37567
|
const unified = this.normalizeMessage(raw);
|
|
37534
37568
|
if (unified) {
|
|
37535
37569
|
this.emit("message", unified);
|
|
37536
37570
|
}
|
|
37537
37571
|
});
|
|
37572
|
+
const onSessionDrop = (err) => {
|
|
37573
|
+
this.emit("session:expired", {
|
|
37574
|
+
reason: "SESSION_EXPIRED_OR_DROPPED",
|
|
37575
|
+
raw: err,
|
|
37576
|
+
requiresQrScan: true
|
|
37577
|
+
});
|
|
37578
|
+
this.setConnected(false);
|
|
37579
|
+
};
|
|
37580
|
+
this.api.listener.on("closed", onSessionDrop);
|
|
37581
|
+
this.api.listener.on("error", (err) => {
|
|
37582
|
+
const msg = String(err?.message || err);
|
|
37583
|
+
if (msg.includes("1002") || msg.includes("session") || msg.includes("auth")) {
|
|
37584
|
+
onSessionDrop(err);
|
|
37585
|
+
}
|
|
37586
|
+
});
|
|
37538
37587
|
if (this.api.listener.start) {
|
|
37539
37588
|
try {
|
|
37540
37589
|
this.api.listener.start();
|
|
37541
37590
|
} catch {}
|
|
37542
37591
|
}
|
|
37543
37592
|
}
|
|
37593
|
+
recordInbound(raw) {
|
|
37594
|
+
if (!raw)
|
|
37595
|
+
return;
|
|
37596
|
+
const data = raw.data || raw;
|
|
37597
|
+
const isGroup = raw.type === 1 || raw.type === "group" || Boolean(raw.isGroup);
|
|
37598
|
+
const chatId = String(raw.threadId || data.idTo || data.threadId || "");
|
|
37599
|
+
const msgId = String(data.msgId || raw.msgId || "");
|
|
37600
|
+
const cliMsgId = data.cliMsgId ? String(data.cliMsgId) : undefined;
|
|
37601
|
+
const uidFrom = String(data.uidFrom || raw.senderId || "");
|
|
37602
|
+
const content = typeof data.content === "string" ? data.content : data.msg || "";
|
|
37603
|
+
if (chatId) {
|
|
37604
|
+
this.threadTypeCache.set(chatId, isGroup ? 1 : 0);
|
|
37605
|
+
}
|
|
37606
|
+
const cached = {
|
|
37607
|
+
msgId,
|
|
37608
|
+
cliMsgId,
|
|
37609
|
+
uidFrom,
|
|
37610
|
+
content,
|
|
37611
|
+
threadId: chatId,
|
|
37612
|
+
isGroup
|
|
37613
|
+
};
|
|
37614
|
+
const limit = this.config.cacheLimit || 1000;
|
|
37615
|
+
if (this.messageCache.size >= limit) {
|
|
37616
|
+
const firstKey = this.messageCache.keys().next().value;
|
|
37617
|
+
if (firstKey)
|
|
37618
|
+
this.messageCache.delete(firstKey);
|
|
37619
|
+
}
|
|
37620
|
+
if (msgId)
|
|
37621
|
+
this.messageCache.set(msgId, cached);
|
|
37622
|
+
if (cliMsgId)
|
|
37623
|
+
this.messageCache.set(cliMsgId, cached);
|
|
37624
|
+
}
|
|
37625
|
+
resolveThreadType(chatId) {
|
|
37626
|
+
if (this.threadTypeCache.has(chatId)) {
|
|
37627
|
+
return this.threadTypeCache.get(chatId);
|
|
37628
|
+
}
|
|
37629
|
+
return this.config.defaultIsGroup ?? true ? 1 : 0;
|
|
37630
|
+
}
|
|
37631
|
+
resolveQuote(replyToId) {
|
|
37632
|
+
if (!replyToId)
|
|
37633
|
+
return;
|
|
37634
|
+
const cached = this.messageCache.get(replyToId);
|
|
37635
|
+
if (cached) {
|
|
37636
|
+
return {
|
|
37637
|
+
msgId: cached.msgId,
|
|
37638
|
+
cliMsgId: cached.cliMsgId,
|
|
37639
|
+
uidFrom: cached.uidFrom,
|
|
37640
|
+
content: cached.content
|
|
37641
|
+
};
|
|
37642
|
+
}
|
|
37643
|
+
return replyToId;
|
|
37644
|
+
}
|
|
37645
|
+
async enqueueSend(operation) {
|
|
37646
|
+
const minDelay = this.config.minDelayMs ?? 300;
|
|
37647
|
+
const maxDelay = this.config.maxDelayMs ?? 800;
|
|
37648
|
+
const execute = async () => {
|
|
37649
|
+
if (maxDelay > 0) {
|
|
37650
|
+
const jitter = Math.floor(minDelay + Math.random() * Math.max(0, maxDelay - minDelay));
|
|
37651
|
+
if (jitter > 0) {
|
|
37652
|
+
await new Promise((r) => setTimeout(r, jitter));
|
|
37653
|
+
}
|
|
37654
|
+
}
|
|
37655
|
+
return await operation();
|
|
37656
|
+
};
|
|
37657
|
+
const next = this.sendQueue.then(execute, execute);
|
|
37658
|
+
this.sendQueue = next.catch(() => {});
|
|
37659
|
+
return next;
|
|
37660
|
+
}
|
|
37544
37661
|
normalizeMessage(raw) {
|
|
37545
37662
|
if (!raw)
|
|
37546
37663
|
return null;
|
|
@@ -37574,41 +37691,60 @@ class ZaloChannelAdapter extends BaseChannel {
|
|
|
37574
37691
|
async sendText(chatId, text, options) {
|
|
37575
37692
|
if (!this.api)
|
|
37576
37693
|
throw new Error("Zalo adapter is not connected.");
|
|
37577
|
-
const
|
|
37578
|
-
const
|
|
37579
|
-
const payload = { msg: text, quote
|
|
37580
|
-
|
|
37581
|
-
|
|
37582
|
-
|
|
37583
|
-
|
|
37584
|
-
|
|
37585
|
-
|
|
37586
|
-
|
|
37694
|
+
const threadType = this.resolveThreadType(chatId);
|
|
37695
|
+
const quote = this.resolveQuote(options?.replyToId);
|
|
37696
|
+
const payload = { msg: text, quote };
|
|
37697
|
+
return await this.enqueueSend(async () => {
|
|
37698
|
+
const res = await this.api.sendMessage(payload, chatId, threadType);
|
|
37699
|
+
const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
|
|
37700
|
+
return {
|
|
37701
|
+
messageId: String(resMsgId),
|
|
37702
|
+
chatId,
|
|
37703
|
+
timestamp: Date.now()
|
|
37704
|
+
};
|
|
37705
|
+
});
|
|
37587
37706
|
}
|
|
37588
|
-
async sendMedia(chatId, media,
|
|
37707
|
+
async sendMedia(chatId, media, options) {
|
|
37589
37708
|
if (!this.api)
|
|
37590
37709
|
throw new Error("Zalo adapter is not connected.");
|
|
37591
|
-
const
|
|
37592
|
-
const
|
|
37593
|
-
|
|
37594
|
-
|
|
37595
|
-
|
|
37596
|
-
|
|
37597
|
-
|
|
37598
|
-
|
|
37599
|
-
|
|
37600
|
-
|
|
37601
|
-
|
|
37602
|
-
|
|
37603
|
-
|
|
37604
|
-
|
|
37605
|
-
|
|
37606
|
-
|
|
37710
|
+
const threadType = this.resolveThreadType(chatId);
|
|
37711
|
+
const quote = this.resolveQuote(options?.replyToId);
|
|
37712
|
+
return await this.enqueueSend(async () => {
|
|
37713
|
+
let res;
|
|
37714
|
+
if (media.type === "image") {
|
|
37715
|
+
res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
|
|
37716
|
+
} else if (media.type === "video" && this.api.sendVideo) {
|
|
37717
|
+
res = await this.api.sendVideo({ video: media.source, msg: media.caption || "", quote }, chatId, threadType);
|
|
37718
|
+
} else {
|
|
37719
|
+
res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
|
|
37720
|
+
}
|
|
37721
|
+
const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
|
|
37722
|
+
return {
|
|
37723
|
+
messageId: String(resMsgId),
|
|
37724
|
+
chatId,
|
|
37725
|
+
timestamp: Date.now()
|
|
37726
|
+
};
|
|
37727
|
+
});
|
|
37607
37728
|
}
|
|
37608
37729
|
async addReaction(chatId, messageId, emoji) {
|
|
37609
|
-
if (this.api?.addReaction)
|
|
37610
|
-
|
|
37611
|
-
|
|
37730
|
+
if (!this.api?.addReaction)
|
|
37731
|
+
return;
|
|
37732
|
+
const threadType = this.resolveThreadType(chatId);
|
|
37733
|
+
const isGroup = threadType === 1;
|
|
37734
|
+
const reactionCode = EMOJI_TO_ZALO[emoji] || emoji;
|
|
37735
|
+
const cached = this.messageCache.get(messageId);
|
|
37736
|
+
const cliMsgId = cached?.cliMsgId || messageId;
|
|
37737
|
+
await this.enqueueSend(async () => {
|
|
37738
|
+
await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
|
|
37739
|
+
});
|
|
37740
|
+
}
|
|
37741
|
+
async sendTyping(chatId) {
|
|
37742
|
+
if (!this.api?.sendTypingEvent)
|
|
37743
|
+
return;
|
|
37744
|
+
const threadType = this.resolveThreadType(chatId);
|
|
37745
|
+
try {
|
|
37746
|
+
await this.api.sendTypingEvent(chatId, true, threadType);
|
|
37747
|
+
} catch {}
|
|
37612
37748
|
}
|
|
37613
37749
|
}
|
|
37614
37750
|
// src/personal/client.ts
|
|
@@ -38329,6 +38465,7 @@ function initOABot() {
|
|
|
38329
38465
|
return new ZaloOABot;
|
|
38330
38466
|
}
|
|
38331
38467
|
export {
|
|
38468
|
+
EMOJI_TO_ZALO,
|
|
38332
38469
|
ZaloChannelAdapter,
|
|
38333
38470
|
ZaloOABot,
|
|
38334
38471
|
ZaloPersonalBot,
|
package/dist/core/context.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { IChannelAdapter, MediaPayload, SendOptions, SentMessageResult, UnifiedMessage } from "./types";
|
|
2
|
+
import { type StreamOptions } from "./stream";
|
|
2
3
|
export interface MessageContext {
|
|
3
4
|
message: UnifiedMessage;
|
|
4
5
|
channel: IChannelAdapter;
|
|
5
6
|
reply: (text: string, options?: SendOptions) => Promise<SentMessageResult>;
|
|
6
7
|
replyMedia: (media: MediaPayload, options?: SendOptions) => Promise<SentMessageResult>;
|
|
7
8
|
react: (emoji: string) => Promise<void>;
|
|
9
|
+
sendTyping: () => Promise<void>;
|
|
10
|
+
stream: (tokenStream: AsyncIterable<string>, options?: StreamOptions) => Promise<SentMessageResult[]>;
|
|
8
11
|
}
|
|
9
12
|
export declare function createMessageContext(message: UnifiedMessage, channel: IChannelAdapter): MessageContext;
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -28,6 +28,126 @@ class ChannelEventBus extends EventEmitter2 {
|
|
|
28
28
|
return this.emit("status", status);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
// src/core/stream.ts
|
|
32
|
+
class SmartStreamer {
|
|
33
|
+
adapter;
|
|
34
|
+
options;
|
|
35
|
+
constructor(adapter, options = {}) {
|
|
36
|
+
this.adapter = adapter;
|
|
37
|
+
this.options = {
|
|
38
|
+
editDebounceMs: 1000,
|
|
39
|
+
typingIntervalMs: 4000,
|
|
40
|
+
chunkMode: "sentence",
|
|
41
|
+
minSentenceLength: 60,
|
|
42
|
+
initialPlaceholder: "...",
|
|
43
|
+
...options
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async stream(chatId, tokenStream, sendOptions) {
|
|
47
|
+
let typingActive = true;
|
|
48
|
+
const triggerTyping = async () => {
|
|
49
|
+
if (this.adapter.sendTyping) {
|
|
50
|
+
try {
|
|
51
|
+
await this.adapter.sendTyping(chatId);
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
await triggerTyping();
|
|
56
|
+
const typingTimer = setInterval(() => {
|
|
57
|
+
if (typingActive)
|
|
58
|
+
triggerTyping();
|
|
59
|
+
}, this.options.typingIntervalMs);
|
|
60
|
+
try {
|
|
61
|
+
if (typeof this.adapter.editText === "function") {
|
|
62
|
+
return await this.streamWithEdit(chatId, tokenStream, sendOptions);
|
|
63
|
+
} else {
|
|
64
|
+
return await this.streamWithoutEdit(chatId, tokenStream, sendOptions);
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
typingActive = false;
|
|
68
|
+
clearInterval(typingTimer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async streamWithEdit(chatId, tokenStream, sendOptions) {
|
|
72
|
+
let accumulated = "";
|
|
73
|
+
let sentMsg = null;
|
|
74
|
+
let lastEditTime = 0;
|
|
75
|
+
let pendingEditTimeout = null;
|
|
76
|
+
const performEdit = async (text) => {
|
|
77
|
+
if (sentMsg && this.adapter.editText) {
|
|
78
|
+
await this.adapter.editText(chatId, sentMsg.messageId, text);
|
|
79
|
+
lastEditTime = Date.now();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
for await (const chunk of tokenStream) {
|
|
83
|
+
accumulated += chunk;
|
|
84
|
+
if (!sentMsg) {
|
|
85
|
+
sentMsg = await this.adapter.sendText(chatId, accumulated.trim() || this.options.initialPlaceholder, sendOptions);
|
|
86
|
+
lastEditTime = Date.now();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const elapsed = now - lastEditTime;
|
|
91
|
+
if (elapsed >= this.options.editDebounceMs) {
|
|
92
|
+
if (pendingEditTimeout) {
|
|
93
|
+
clearTimeout(pendingEditTimeout);
|
|
94
|
+
pendingEditTimeout = null;
|
|
95
|
+
}
|
|
96
|
+
await performEdit(accumulated);
|
|
97
|
+
} else if (!pendingEditTimeout) {
|
|
98
|
+
pendingEditTimeout = setTimeout(async () => {
|
|
99
|
+
pendingEditTimeout = null;
|
|
100
|
+
await performEdit(accumulated);
|
|
101
|
+
}, this.options.editDebounceMs - elapsed);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (pendingEditTimeout) {
|
|
105
|
+
clearTimeout(pendingEditTimeout);
|
|
106
|
+
pendingEditTimeout = null;
|
|
107
|
+
}
|
|
108
|
+
if (sentMsg && accumulated) {
|
|
109
|
+
await performEdit(accumulated);
|
|
110
|
+
return [sentMsg];
|
|
111
|
+
} else if (!sentMsg && accumulated) {
|
|
112
|
+
const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
|
|
113
|
+
return [res];
|
|
114
|
+
}
|
|
115
|
+
return sentMsg ? [sentMsg] : [];
|
|
116
|
+
}
|
|
117
|
+
async streamWithoutEdit(chatId, tokenStream, sendOptions) {
|
|
118
|
+
const results = [];
|
|
119
|
+
if (this.options.chunkMode === "accumulate") {
|
|
120
|
+
let accumulated = "";
|
|
121
|
+
for await (const chunk of tokenStream) {
|
|
122
|
+
accumulated += chunk;
|
|
123
|
+
}
|
|
124
|
+
if (accumulated.trim()) {
|
|
125
|
+
const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
|
|
126
|
+
results.push(res);
|
|
127
|
+
}
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
let buffer = "";
|
|
131
|
+
const sentenceEndRegex = /[.?!;\n]\s*$/;
|
|
132
|
+
for await (const chunk of tokenStream) {
|
|
133
|
+
buffer += chunk;
|
|
134
|
+
if (buffer.length >= this.options.minSentenceLength && sentenceEndRegex.test(buffer.trimEnd())) {
|
|
135
|
+
const textToSend = buffer.trim();
|
|
136
|
+
if (textToSend) {
|
|
137
|
+
const res = await this.adapter.sendText(chatId, textToSend, sendOptions);
|
|
138
|
+
results.push(res);
|
|
139
|
+
buffer = "";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (buffer.trim()) {
|
|
144
|
+
const res = await this.adapter.sendText(chatId, buffer.trim(), sendOptions);
|
|
145
|
+
results.push(res);
|
|
146
|
+
}
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
31
151
|
// src/core/context.ts
|
|
32
152
|
function createMessageContext(message, channel) {
|
|
33
153
|
return {
|
|
@@ -45,6 +165,17 @@ function createMessageContext(message, channel) {
|
|
|
45
165
|
if (channel.addReaction) {
|
|
46
166
|
await channel.addReaction(message.chat.id, message.id, emoji);
|
|
47
167
|
}
|
|
168
|
+
},
|
|
169
|
+
sendTyping: async () => {
|
|
170
|
+
if (channel.sendTyping) {
|
|
171
|
+
await channel.sendTyping(message.chat.id);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
stream: async (tokenStream, options) => {
|
|
175
|
+
const streamer = new SmartStreamer(channel, options);
|
|
176
|
+
return await streamer.stream(message.chat.id, tokenStream, {
|
|
177
|
+
replyToId: message.id
|
|
178
|
+
});
|
|
48
179
|
}
|
|
49
180
|
};
|
|
50
181
|
}
|
|
@@ -95,5 +226,6 @@ export {
|
|
|
95
226
|
BaseChannel,
|
|
96
227
|
ChannelEventBus,
|
|
97
228
|
ChannelHub,
|
|
229
|
+
SmartStreamer,
|
|
98
230
|
createMessageContext
|
|
99
231
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IChannelAdapter, SendOptions, SentMessageResult } from "./types";
|
|
2
|
+
export interface StreamOptions {
|
|
3
|
+
editDebounceMs?: number;
|
|
4
|
+
typingIntervalMs?: number;
|
|
5
|
+
chunkMode?: "sentence" | "accumulate";
|
|
6
|
+
minSentenceLength?: number;
|
|
7
|
+
initialPlaceholder?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class SmartStreamer {
|
|
10
|
+
private adapter;
|
|
11
|
+
private options;
|
|
12
|
+
constructor(adapter: IChannelAdapter, options?: StreamOptions);
|
|
13
|
+
stream(chatId: string, tokenStream: AsyncIterable<string>, sendOptions?: SendOptions): Promise<SentMessageResult[]>;
|
|
14
|
+
private streamWithEdit;
|
|
15
|
+
private streamWithoutEdit;
|
|
16
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface IChannelAdapter {
|
|
|
57
57
|
sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
|
|
58
58
|
sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
|
|
59
59
|
addReaction?(chatId: string, messageId: string, emoji: string): Promise<void>;
|
|
60
|
+
sendTyping?(chatId: string): Promise<void>;
|
|
61
|
+
editText?(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
|
|
60
62
|
on(event: "message", handler: (msg: UnifiedMessage) => Promise<void> | void): this;
|
|
61
63
|
on(event: "error", handler: (err: Error) => void): this;
|
|
62
64
|
on(event: "status", handler: (status: ChannelStatus) => void): this;
|