@theowlops/channelhub 1.0.1 → 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 +1 -0
- package/dist/channels/zalo/index.js +8 -0
- 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 +186 -2
- package/dist/index.js +186 -2
- 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
|
|
@@ -30,4 +30,5 @@ export declare class ZaloChannelAdapter extends BaseChannel {
|
|
|
30
30
|
sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
|
|
31
31
|
sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
|
|
32
32
|
addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
|
|
33
|
+
sendTyping(chatId: string): Promise<void>;
|
|
33
34
|
}
|
|
@@ -37738,6 +37738,14 @@ class ZaloChannelAdapter extends BaseChannel {
|
|
|
37738
37738
|
await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
|
|
37739
37739
|
});
|
|
37740
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 {}
|
|
37748
|
+
}
|
|
37741
37749
|
}
|
|
37742
37750
|
// src/personal/client.ts
|
|
37743
37751
|
init_dist();
|
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;
|
package/dist/index.cjs
CHANGED
|
@@ -20548,9 +20548,9 @@ var require_websocket_server = __commonJS(function(exports2, module2) {
|
|
|
20548
20548
|
});
|
|
20549
20549
|
|
|
20550
20550
|
// node_modules/ws/wrapper.mjs
|
|
20551
|
-
var
|
|
20551
|
+
var import_stream2, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
|
|
20552
20552
|
var init_wrapper = __esm(() => {
|
|
20553
|
-
|
|
20553
|
+
import_stream2 = __toESM(require_stream(), 1);
|
|
20554
20554
|
import_extension = __toESM(require_extension(), 1);
|
|
20555
20555
|
import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
|
|
20556
20556
|
import_receiver = __toESM(require_receiver(), 1);
|
|
@@ -37520,6 +37520,7 @@ __export(exports_src, {
|
|
|
37520
37520
|
CommandRouter: () => CommandRouter,
|
|
37521
37521
|
DiscordChannelAdapter: () => DiscordChannelAdapter,
|
|
37522
37522
|
SlackChannelAdapter: () => SlackChannelAdapter,
|
|
37523
|
+
SmartStreamer: () => SmartStreamer,
|
|
37523
37524
|
TelegramChannelAdapter: () => TelegramChannelAdapter,
|
|
37524
37525
|
WebhookBridge: () => WebhookBridge,
|
|
37525
37526
|
Zalo: () => Zalo,
|
|
@@ -37563,6 +37564,126 @@ class ChannelEventBus extends import_node_events2.EventEmitter {
|
|
|
37563
37564
|
return this.emit("status", status);
|
|
37564
37565
|
}
|
|
37565
37566
|
}
|
|
37567
|
+
// src/core/stream.ts
|
|
37568
|
+
class SmartStreamer {
|
|
37569
|
+
adapter;
|
|
37570
|
+
options;
|
|
37571
|
+
constructor(adapter, options = {}) {
|
|
37572
|
+
this.adapter = adapter;
|
|
37573
|
+
this.options = {
|
|
37574
|
+
editDebounceMs: 1000,
|
|
37575
|
+
typingIntervalMs: 4000,
|
|
37576
|
+
chunkMode: "sentence",
|
|
37577
|
+
minSentenceLength: 60,
|
|
37578
|
+
initialPlaceholder: "...",
|
|
37579
|
+
...options
|
|
37580
|
+
};
|
|
37581
|
+
}
|
|
37582
|
+
async stream(chatId, tokenStream, sendOptions) {
|
|
37583
|
+
let typingActive = true;
|
|
37584
|
+
const triggerTyping = async () => {
|
|
37585
|
+
if (this.adapter.sendTyping) {
|
|
37586
|
+
try {
|
|
37587
|
+
await this.adapter.sendTyping(chatId);
|
|
37588
|
+
} catch {}
|
|
37589
|
+
}
|
|
37590
|
+
};
|
|
37591
|
+
await triggerTyping();
|
|
37592
|
+
const typingTimer = setInterval(() => {
|
|
37593
|
+
if (typingActive)
|
|
37594
|
+
triggerTyping();
|
|
37595
|
+
}, this.options.typingIntervalMs);
|
|
37596
|
+
try {
|
|
37597
|
+
if (typeof this.adapter.editText === "function") {
|
|
37598
|
+
return await this.streamWithEdit(chatId, tokenStream, sendOptions);
|
|
37599
|
+
} else {
|
|
37600
|
+
return await this.streamWithoutEdit(chatId, tokenStream, sendOptions);
|
|
37601
|
+
}
|
|
37602
|
+
} finally {
|
|
37603
|
+
typingActive = false;
|
|
37604
|
+
clearInterval(typingTimer);
|
|
37605
|
+
}
|
|
37606
|
+
}
|
|
37607
|
+
async streamWithEdit(chatId, tokenStream, sendOptions) {
|
|
37608
|
+
let accumulated = "";
|
|
37609
|
+
let sentMsg = null;
|
|
37610
|
+
let lastEditTime = 0;
|
|
37611
|
+
let pendingEditTimeout = null;
|
|
37612
|
+
const performEdit = async (text) => {
|
|
37613
|
+
if (sentMsg && this.adapter.editText) {
|
|
37614
|
+
await this.adapter.editText(chatId, sentMsg.messageId, text);
|
|
37615
|
+
lastEditTime = Date.now();
|
|
37616
|
+
}
|
|
37617
|
+
};
|
|
37618
|
+
for await (const chunk of tokenStream) {
|
|
37619
|
+
accumulated += chunk;
|
|
37620
|
+
if (!sentMsg) {
|
|
37621
|
+
sentMsg = await this.adapter.sendText(chatId, accumulated.trim() || this.options.initialPlaceholder, sendOptions);
|
|
37622
|
+
lastEditTime = Date.now();
|
|
37623
|
+
continue;
|
|
37624
|
+
}
|
|
37625
|
+
const now = Date.now();
|
|
37626
|
+
const elapsed = now - lastEditTime;
|
|
37627
|
+
if (elapsed >= this.options.editDebounceMs) {
|
|
37628
|
+
if (pendingEditTimeout) {
|
|
37629
|
+
clearTimeout(pendingEditTimeout);
|
|
37630
|
+
pendingEditTimeout = null;
|
|
37631
|
+
}
|
|
37632
|
+
await performEdit(accumulated);
|
|
37633
|
+
} else if (!pendingEditTimeout) {
|
|
37634
|
+
pendingEditTimeout = setTimeout(async () => {
|
|
37635
|
+
pendingEditTimeout = null;
|
|
37636
|
+
await performEdit(accumulated);
|
|
37637
|
+
}, this.options.editDebounceMs - elapsed);
|
|
37638
|
+
}
|
|
37639
|
+
}
|
|
37640
|
+
if (pendingEditTimeout) {
|
|
37641
|
+
clearTimeout(pendingEditTimeout);
|
|
37642
|
+
pendingEditTimeout = null;
|
|
37643
|
+
}
|
|
37644
|
+
if (sentMsg && accumulated) {
|
|
37645
|
+
await performEdit(accumulated);
|
|
37646
|
+
return [sentMsg];
|
|
37647
|
+
} else if (!sentMsg && accumulated) {
|
|
37648
|
+
const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
|
|
37649
|
+
return [res];
|
|
37650
|
+
}
|
|
37651
|
+
return sentMsg ? [sentMsg] : [];
|
|
37652
|
+
}
|
|
37653
|
+
async streamWithoutEdit(chatId, tokenStream, sendOptions) {
|
|
37654
|
+
const results = [];
|
|
37655
|
+
if (this.options.chunkMode === "accumulate") {
|
|
37656
|
+
let accumulated = "";
|
|
37657
|
+
for await (const chunk of tokenStream) {
|
|
37658
|
+
accumulated += chunk;
|
|
37659
|
+
}
|
|
37660
|
+
if (accumulated.trim()) {
|
|
37661
|
+
const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
|
|
37662
|
+
results.push(res);
|
|
37663
|
+
}
|
|
37664
|
+
return results;
|
|
37665
|
+
}
|
|
37666
|
+
let buffer = "";
|
|
37667
|
+
const sentenceEndRegex = /[.?!;\n]\s*$/;
|
|
37668
|
+
for await (const chunk of tokenStream) {
|
|
37669
|
+
buffer += chunk;
|
|
37670
|
+
if (buffer.length >= this.options.minSentenceLength && sentenceEndRegex.test(buffer.trimEnd())) {
|
|
37671
|
+
const textToSend = buffer.trim();
|
|
37672
|
+
if (textToSend) {
|
|
37673
|
+
const res = await this.adapter.sendText(chatId, textToSend, sendOptions);
|
|
37674
|
+
results.push(res);
|
|
37675
|
+
buffer = "";
|
|
37676
|
+
}
|
|
37677
|
+
}
|
|
37678
|
+
}
|
|
37679
|
+
if (buffer.trim()) {
|
|
37680
|
+
const res = await this.adapter.sendText(chatId, buffer.trim(), sendOptions);
|
|
37681
|
+
results.push(res);
|
|
37682
|
+
}
|
|
37683
|
+
return results;
|
|
37684
|
+
}
|
|
37685
|
+
}
|
|
37686
|
+
|
|
37566
37687
|
// src/core/context.ts
|
|
37567
37688
|
function createMessageContext(message, channel) {
|
|
37568
37689
|
return {
|
|
@@ -37580,6 +37701,17 @@ function createMessageContext(message, channel) {
|
|
|
37580
37701
|
if (channel.addReaction) {
|
|
37581
37702
|
await channel.addReaction(message.chat.id, message.id, emoji);
|
|
37582
37703
|
}
|
|
37704
|
+
},
|
|
37705
|
+
sendTyping: async () => {
|
|
37706
|
+
if (channel.sendTyping) {
|
|
37707
|
+
await channel.sendTyping(message.chat.id);
|
|
37708
|
+
}
|
|
37709
|
+
},
|
|
37710
|
+
stream: async (tokenStream, options) => {
|
|
37711
|
+
const streamer = new SmartStreamer(channel, options);
|
|
37712
|
+
return await streamer.stream(message.chat.id, tokenStream, {
|
|
37713
|
+
replyToId: message.id
|
|
37714
|
+
});
|
|
37583
37715
|
}
|
|
37584
37716
|
};
|
|
37585
37717
|
}
|
|
@@ -37877,6 +38009,14 @@ class ZaloChannelAdapter extends BaseChannel {
|
|
|
37877
38009
|
await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
|
|
37878
38010
|
});
|
|
37879
38011
|
}
|
|
38012
|
+
async sendTyping(chatId) {
|
|
38013
|
+
if (!this.api?.sendTypingEvent)
|
|
38014
|
+
return;
|
|
38015
|
+
const threadType = this.resolveThreadType(chatId);
|
|
38016
|
+
try {
|
|
38017
|
+
await this.api.sendTypingEvent(chatId, true, threadType);
|
|
38018
|
+
} catch {}
|
|
38019
|
+
}
|
|
37880
38020
|
}
|
|
37881
38021
|
// src/personal/client.ts
|
|
37882
38022
|
init_dist();
|
|
@@ -38753,6 +38893,24 @@ class TelegramChannelAdapter extends BaseChannel {
|
|
|
38753
38893
|
reaction: [{ type: "emoji", emoji }]
|
|
38754
38894
|
});
|
|
38755
38895
|
}
|
|
38896
|
+
async sendTyping(chatId) {
|
|
38897
|
+
await this.callApi("sendChatAction", {
|
|
38898
|
+
chat_id: chatId,
|
|
38899
|
+
action: "typing"
|
|
38900
|
+
});
|
|
38901
|
+
}
|
|
38902
|
+
async editText(chatId, messageId, text) {
|
|
38903
|
+
const res = await this.callApi("editMessageText", {
|
|
38904
|
+
chat_id: chatId,
|
|
38905
|
+
message_id: Number(messageId),
|
|
38906
|
+
text
|
|
38907
|
+
});
|
|
38908
|
+
return {
|
|
38909
|
+
messageId: String(res.message_id || messageId),
|
|
38910
|
+
chatId,
|
|
38911
|
+
timestamp: Date.now()
|
|
38912
|
+
};
|
|
38913
|
+
}
|
|
38756
38914
|
}
|
|
38757
38915
|
// src/channels/discord/adapter.ts
|
|
38758
38916
|
class DiscordChannelAdapter extends BaseChannel {
|
|
@@ -38863,6 +39021,19 @@ class DiscordChannelAdapter extends BaseChannel {
|
|
|
38863
39021
|
const encoded = encodeURIComponent(emoji);
|
|
38864
39022
|
await this.callApi("PUT", `/channels/${chatId}/messages/${messageId}/reactions/${encoded}/@me`);
|
|
38865
39023
|
}
|
|
39024
|
+
async sendTyping(chatId) {
|
|
39025
|
+
await this.callApi("POST", `/channels/${chatId}/typing`, {});
|
|
39026
|
+
}
|
|
39027
|
+
async editText(chatId, messageId, text) {
|
|
39028
|
+
const res = await this.callApi("PATCH", `/channels/${chatId}/messages/${messageId}`, {
|
|
39029
|
+
content: text
|
|
39030
|
+
});
|
|
39031
|
+
return {
|
|
39032
|
+
messageId: String(res.id || messageId),
|
|
39033
|
+
chatId,
|
|
39034
|
+
timestamp: Date.now()
|
|
39035
|
+
};
|
|
39036
|
+
}
|
|
38866
39037
|
}
|
|
38867
39038
|
// src/channels/slack/adapter.ts
|
|
38868
39039
|
class SlackChannelAdapter extends BaseChannel {
|
|
@@ -38965,6 +39136,19 @@ class SlackChannelAdapter extends BaseChannel {
|
|
|
38965
39136
|
name: cleanName
|
|
38966
39137
|
});
|
|
38967
39138
|
}
|
|
39139
|
+
async sendTyping(chatId) {}
|
|
39140
|
+
async editText(chatId, messageId, text) {
|
|
39141
|
+
const res = await this.callApi("chat.update", {
|
|
39142
|
+
channel: chatId,
|
|
39143
|
+
ts: messageId,
|
|
39144
|
+
text
|
|
39145
|
+
});
|
|
39146
|
+
return {
|
|
39147
|
+
messageId: String(res.ts || messageId),
|
|
39148
|
+
chatId,
|
|
39149
|
+
timestamp: Date.now()
|
|
39150
|
+
};
|
|
39151
|
+
}
|
|
38968
39152
|
}
|
|
38969
39153
|
// src/bridges/mcp/index.ts
|
|
38970
39154
|
function getChannelHubMcpTools() {
|
package/dist/index.js
CHANGED
|
@@ -20519,9 +20519,9 @@ var require_websocket_server = __commonJS(function(exports, module) {
|
|
|
20519
20519
|
});
|
|
20520
20520
|
|
|
20521
20521
|
// node_modules/ws/wrapper.mjs
|
|
20522
|
-
var
|
|
20522
|
+
var import_stream2, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
|
|
20523
20523
|
var init_wrapper = __esm(() => {
|
|
20524
|
-
|
|
20524
|
+
import_stream2 = __toESM(require_stream(), 1);
|
|
20525
20525
|
import_extension = __toESM(require_extension(), 1);
|
|
20526
20526
|
import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
|
|
20527
20527
|
import_receiver = __toESM(require_receiver(), 1);
|
|
@@ -37499,6 +37499,126 @@ class ChannelEventBus extends EventEmitter2 {
|
|
|
37499
37499
|
return this.emit("status", status);
|
|
37500
37500
|
}
|
|
37501
37501
|
}
|
|
37502
|
+
// src/core/stream.ts
|
|
37503
|
+
class SmartStreamer {
|
|
37504
|
+
adapter;
|
|
37505
|
+
options;
|
|
37506
|
+
constructor(adapter, options = {}) {
|
|
37507
|
+
this.adapter = adapter;
|
|
37508
|
+
this.options = {
|
|
37509
|
+
editDebounceMs: 1000,
|
|
37510
|
+
typingIntervalMs: 4000,
|
|
37511
|
+
chunkMode: "sentence",
|
|
37512
|
+
minSentenceLength: 60,
|
|
37513
|
+
initialPlaceholder: "...",
|
|
37514
|
+
...options
|
|
37515
|
+
};
|
|
37516
|
+
}
|
|
37517
|
+
async stream(chatId, tokenStream, sendOptions) {
|
|
37518
|
+
let typingActive = true;
|
|
37519
|
+
const triggerTyping = async () => {
|
|
37520
|
+
if (this.adapter.sendTyping) {
|
|
37521
|
+
try {
|
|
37522
|
+
await this.adapter.sendTyping(chatId);
|
|
37523
|
+
} catch {}
|
|
37524
|
+
}
|
|
37525
|
+
};
|
|
37526
|
+
await triggerTyping();
|
|
37527
|
+
const typingTimer = setInterval(() => {
|
|
37528
|
+
if (typingActive)
|
|
37529
|
+
triggerTyping();
|
|
37530
|
+
}, this.options.typingIntervalMs);
|
|
37531
|
+
try {
|
|
37532
|
+
if (typeof this.adapter.editText === "function") {
|
|
37533
|
+
return await this.streamWithEdit(chatId, tokenStream, sendOptions);
|
|
37534
|
+
} else {
|
|
37535
|
+
return await this.streamWithoutEdit(chatId, tokenStream, sendOptions);
|
|
37536
|
+
}
|
|
37537
|
+
} finally {
|
|
37538
|
+
typingActive = false;
|
|
37539
|
+
clearInterval(typingTimer);
|
|
37540
|
+
}
|
|
37541
|
+
}
|
|
37542
|
+
async streamWithEdit(chatId, tokenStream, sendOptions) {
|
|
37543
|
+
let accumulated = "";
|
|
37544
|
+
let sentMsg = null;
|
|
37545
|
+
let lastEditTime = 0;
|
|
37546
|
+
let pendingEditTimeout = null;
|
|
37547
|
+
const performEdit = async (text) => {
|
|
37548
|
+
if (sentMsg && this.adapter.editText) {
|
|
37549
|
+
await this.adapter.editText(chatId, sentMsg.messageId, text);
|
|
37550
|
+
lastEditTime = Date.now();
|
|
37551
|
+
}
|
|
37552
|
+
};
|
|
37553
|
+
for await (const chunk of tokenStream) {
|
|
37554
|
+
accumulated += chunk;
|
|
37555
|
+
if (!sentMsg) {
|
|
37556
|
+
sentMsg = await this.adapter.sendText(chatId, accumulated.trim() || this.options.initialPlaceholder, sendOptions);
|
|
37557
|
+
lastEditTime = Date.now();
|
|
37558
|
+
continue;
|
|
37559
|
+
}
|
|
37560
|
+
const now = Date.now();
|
|
37561
|
+
const elapsed = now - lastEditTime;
|
|
37562
|
+
if (elapsed >= this.options.editDebounceMs) {
|
|
37563
|
+
if (pendingEditTimeout) {
|
|
37564
|
+
clearTimeout(pendingEditTimeout);
|
|
37565
|
+
pendingEditTimeout = null;
|
|
37566
|
+
}
|
|
37567
|
+
await performEdit(accumulated);
|
|
37568
|
+
} else if (!pendingEditTimeout) {
|
|
37569
|
+
pendingEditTimeout = setTimeout(async () => {
|
|
37570
|
+
pendingEditTimeout = null;
|
|
37571
|
+
await performEdit(accumulated);
|
|
37572
|
+
}, this.options.editDebounceMs - elapsed);
|
|
37573
|
+
}
|
|
37574
|
+
}
|
|
37575
|
+
if (pendingEditTimeout) {
|
|
37576
|
+
clearTimeout(pendingEditTimeout);
|
|
37577
|
+
pendingEditTimeout = null;
|
|
37578
|
+
}
|
|
37579
|
+
if (sentMsg && accumulated) {
|
|
37580
|
+
await performEdit(accumulated);
|
|
37581
|
+
return [sentMsg];
|
|
37582
|
+
} else if (!sentMsg && accumulated) {
|
|
37583
|
+
const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
|
|
37584
|
+
return [res];
|
|
37585
|
+
}
|
|
37586
|
+
return sentMsg ? [sentMsg] : [];
|
|
37587
|
+
}
|
|
37588
|
+
async streamWithoutEdit(chatId, tokenStream, sendOptions) {
|
|
37589
|
+
const results = [];
|
|
37590
|
+
if (this.options.chunkMode === "accumulate") {
|
|
37591
|
+
let accumulated = "";
|
|
37592
|
+
for await (const chunk of tokenStream) {
|
|
37593
|
+
accumulated += chunk;
|
|
37594
|
+
}
|
|
37595
|
+
if (accumulated.trim()) {
|
|
37596
|
+
const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
|
|
37597
|
+
results.push(res);
|
|
37598
|
+
}
|
|
37599
|
+
return results;
|
|
37600
|
+
}
|
|
37601
|
+
let buffer = "";
|
|
37602
|
+
const sentenceEndRegex = /[.?!;\n]\s*$/;
|
|
37603
|
+
for await (const chunk of tokenStream) {
|
|
37604
|
+
buffer += chunk;
|
|
37605
|
+
if (buffer.length >= this.options.minSentenceLength && sentenceEndRegex.test(buffer.trimEnd())) {
|
|
37606
|
+
const textToSend = buffer.trim();
|
|
37607
|
+
if (textToSend) {
|
|
37608
|
+
const res = await this.adapter.sendText(chatId, textToSend, sendOptions);
|
|
37609
|
+
results.push(res);
|
|
37610
|
+
buffer = "";
|
|
37611
|
+
}
|
|
37612
|
+
}
|
|
37613
|
+
}
|
|
37614
|
+
if (buffer.trim()) {
|
|
37615
|
+
const res = await this.adapter.sendText(chatId, buffer.trim(), sendOptions);
|
|
37616
|
+
results.push(res);
|
|
37617
|
+
}
|
|
37618
|
+
return results;
|
|
37619
|
+
}
|
|
37620
|
+
}
|
|
37621
|
+
|
|
37502
37622
|
// src/core/context.ts
|
|
37503
37623
|
function createMessageContext(message, channel) {
|
|
37504
37624
|
return {
|
|
@@ -37516,6 +37636,17 @@ function createMessageContext(message, channel) {
|
|
|
37516
37636
|
if (channel.addReaction) {
|
|
37517
37637
|
await channel.addReaction(message.chat.id, message.id, emoji);
|
|
37518
37638
|
}
|
|
37639
|
+
},
|
|
37640
|
+
sendTyping: async () => {
|
|
37641
|
+
if (channel.sendTyping) {
|
|
37642
|
+
await channel.sendTyping(message.chat.id);
|
|
37643
|
+
}
|
|
37644
|
+
},
|
|
37645
|
+
stream: async (tokenStream, options) => {
|
|
37646
|
+
const streamer = new SmartStreamer(channel, options);
|
|
37647
|
+
return await streamer.stream(message.chat.id, tokenStream, {
|
|
37648
|
+
replyToId: message.id
|
|
37649
|
+
});
|
|
37519
37650
|
}
|
|
37520
37651
|
};
|
|
37521
37652
|
}
|
|
@@ -37813,6 +37944,14 @@ class ZaloChannelAdapter extends BaseChannel {
|
|
|
37813
37944
|
await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
|
|
37814
37945
|
});
|
|
37815
37946
|
}
|
|
37947
|
+
async sendTyping(chatId) {
|
|
37948
|
+
if (!this.api?.sendTypingEvent)
|
|
37949
|
+
return;
|
|
37950
|
+
const threadType = this.resolveThreadType(chatId);
|
|
37951
|
+
try {
|
|
37952
|
+
await this.api.sendTypingEvent(chatId, true, threadType);
|
|
37953
|
+
} catch {}
|
|
37954
|
+
}
|
|
37816
37955
|
}
|
|
37817
37956
|
// src/personal/client.ts
|
|
37818
37957
|
init_dist();
|
|
@@ -38689,6 +38828,24 @@ class TelegramChannelAdapter extends BaseChannel {
|
|
|
38689
38828
|
reaction: [{ type: "emoji", emoji }]
|
|
38690
38829
|
});
|
|
38691
38830
|
}
|
|
38831
|
+
async sendTyping(chatId) {
|
|
38832
|
+
await this.callApi("sendChatAction", {
|
|
38833
|
+
chat_id: chatId,
|
|
38834
|
+
action: "typing"
|
|
38835
|
+
});
|
|
38836
|
+
}
|
|
38837
|
+
async editText(chatId, messageId, text) {
|
|
38838
|
+
const res = await this.callApi("editMessageText", {
|
|
38839
|
+
chat_id: chatId,
|
|
38840
|
+
message_id: Number(messageId),
|
|
38841
|
+
text
|
|
38842
|
+
});
|
|
38843
|
+
return {
|
|
38844
|
+
messageId: String(res.message_id || messageId),
|
|
38845
|
+
chatId,
|
|
38846
|
+
timestamp: Date.now()
|
|
38847
|
+
};
|
|
38848
|
+
}
|
|
38692
38849
|
}
|
|
38693
38850
|
// src/channels/discord/adapter.ts
|
|
38694
38851
|
class DiscordChannelAdapter extends BaseChannel {
|
|
@@ -38799,6 +38956,19 @@ class DiscordChannelAdapter extends BaseChannel {
|
|
|
38799
38956
|
const encoded = encodeURIComponent(emoji);
|
|
38800
38957
|
await this.callApi("PUT", `/channels/${chatId}/messages/${messageId}/reactions/${encoded}/@me`);
|
|
38801
38958
|
}
|
|
38959
|
+
async sendTyping(chatId) {
|
|
38960
|
+
await this.callApi("POST", `/channels/${chatId}/typing`, {});
|
|
38961
|
+
}
|
|
38962
|
+
async editText(chatId, messageId, text) {
|
|
38963
|
+
const res = await this.callApi("PATCH", `/channels/${chatId}/messages/${messageId}`, {
|
|
38964
|
+
content: text
|
|
38965
|
+
});
|
|
38966
|
+
return {
|
|
38967
|
+
messageId: String(res.id || messageId),
|
|
38968
|
+
chatId,
|
|
38969
|
+
timestamp: Date.now()
|
|
38970
|
+
};
|
|
38971
|
+
}
|
|
38802
38972
|
}
|
|
38803
38973
|
// src/channels/slack/adapter.ts
|
|
38804
38974
|
class SlackChannelAdapter extends BaseChannel {
|
|
@@ -38901,6 +39071,19 @@ class SlackChannelAdapter extends BaseChannel {
|
|
|
38901
39071
|
name: cleanName
|
|
38902
39072
|
});
|
|
38903
39073
|
}
|
|
39074
|
+
async sendTyping(chatId) {}
|
|
39075
|
+
async editText(chatId, messageId, text) {
|
|
39076
|
+
const res = await this.callApi("chat.update", {
|
|
39077
|
+
channel: chatId,
|
|
39078
|
+
ts: messageId,
|
|
39079
|
+
text
|
|
39080
|
+
});
|
|
39081
|
+
return {
|
|
39082
|
+
messageId: String(res.ts || messageId),
|
|
39083
|
+
chatId,
|
|
39084
|
+
timestamp: Date.now()
|
|
39085
|
+
};
|
|
39086
|
+
}
|
|
38904
39087
|
}
|
|
38905
39088
|
// src/bridges/mcp/index.ts
|
|
38906
39089
|
function getChannelHubMcpTools() {
|
|
@@ -39305,6 +39488,7 @@ export {
|
|
|
39305
39488
|
CommandRouter,
|
|
39306
39489
|
DiscordChannelAdapter,
|
|
39307
39490
|
SlackChannelAdapter,
|
|
39491
|
+
SmartStreamer,
|
|
39308
39492
|
TelegramChannelAdapter,
|
|
39309
39493
|
WebhookBridge,
|
|
39310
39494
|
Zalo,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theowlops/channelhub",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "ChannelHub SDK - Unified multi-channel messaging toolkit (Zalo, Telegram, Discord, Slack) for AI agents, standalone bots, and enterprise automation.",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"main": "dist/index.cjs",
|