chat 4.20.2 → 4.22.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/index.d.ts +479 -332
- package/dist/index.js +389 -69
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-C2ATKxHQ.d.ts → jsx-runtime-DraWieqP.d.ts} +1 -1
- package/dist/jsx-runtime.d.ts +1 -1
- package/docs/api/chat.mdx +2 -0
- package/docs/api/index.mdx +1 -1
- package/docs/api/message.mdx +1 -1
- package/docs/api/postable-message.mdx +1 -1
- package/docs/api/to-ai-messages.mdx +190 -0
- package/docs/concurrency.mdx +223 -0
- package/docs/contributing/building.mdx +15 -1
- package/docs/guides/code-review-hono.mdx +4 -5
- package/docs/handling-events.mdx +2 -0
- package/docs/meta.json +1 -0
- package/docs/posting-messages.mdx +2 -0
- package/docs/streaming.mdx +3 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -131,7 +131,7 @@ async function toAiMessages(messages, options) {
|
|
|
131
131
|
filtered.map(async (msg) => {
|
|
132
132
|
const role = msg.author.isMe ? "assistant" : "user";
|
|
133
133
|
let textContent = includeNames && role === "user" ? `[${msg.author.userName}]: ${msg.text}` : msg.text;
|
|
134
|
-
if (msg.links.length > 0) {
|
|
134
|
+
if (msg.links && msg.links.length > 0) {
|
|
135
135
|
const linkParts = msg.links.map((link2) => {
|
|
136
136
|
const parts = link2.fetchMessage ? [`[Embedded message: ${link2.url}]`] : [link2.url];
|
|
137
137
|
if (link2.title) {
|
|
@@ -153,7 +153,7 @@ ${linkParts}`;
|
|
|
153
153
|
let aiMessage;
|
|
154
154
|
if (role === "user") {
|
|
155
155
|
const attachmentParts = [];
|
|
156
|
-
for (const att of msg.attachments) {
|
|
156
|
+
for (const att of msg.attachments ?? []) {
|
|
157
157
|
const part = await attachmentToPart(att);
|
|
158
158
|
if (part) {
|
|
159
159
|
attachmentParts.push(part);
|
|
@@ -236,7 +236,7 @@ async function* fromFullStream(stream) {
|
|
|
236
236
|
needsSeparator = false;
|
|
237
237
|
hasEmittedText = true;
|
|
238
238
|
yield textContent;
|
|
239
|
-
} else if (typed.type === "step
|
|
239
|
+
} else if (typed.type === "finish-step") {
|
|
240
240
|
needsSeparator = true;
|
|
241
241
|
}
|
|
242
242
|
}
|
|
@@ -1766,6 +1766,9 @@ function extractMessageContent2(message) {
|
|
|
1766
1766
|
|
|
1767
1767
|
// src/chat.ts
|
|
1768
1768
|
var DEFAULT_LOCK_TTL_MS = 3e4;
|
|
1769
|
+
function sleep(ms) {
|
|
1770
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1771
|
+
}
|
|
1769
1772
|
var SLACK_USER_ID_REGEX = /^U[A-Z0-9]+$/i;
|
|
1770
1773
|
var DISCORD_SNOWFLAKE_REGEX = /^\d{17,19}$/;
|
|
1771
1774
|
var DEDUPE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -1810,6 +1813,9 @@ var Chat = class {
|
|
|
1810
1813
|
_dedupeTtlMs;
|
|
1811
1814
|
_onLockConflict;
|
|
1812
1815
|
_messageHistory;
|
|
1816
|
+
_concurrencyStrategy;
|
|
1817
|
+
_concurrencyConfig;
|
|
1818
|
+
_lockScope;
|
|
1813
1819
|
mentionHandlers = [];
|
|
1814
1820
|
directMessageHandlers = [];
|
|
1815
1821
|
messagePatterns = [];
|
|
@@ -1840,6 +1846,38 @@ var Chat = class {
|
|
|
1840
1846
|
this._fallbackStreamingPlaceholderText = config.fallbackStreamingPlaceholderText !== void 0 ? config.fallbackStreamingPlaceholderText : "...";
|
|
1841
1847
|
this._dedupeTtlMs = config.dedupeTtlMs ?? DEDUPE_TTL_MS;
|
|
1842
1848
|
this._onLockConflict = config.onLockConflict;
|
|
1849
|
+
this._lockScope = config.lockScope;
|
|
1850
|
+
const concurrency = config.concurrency;
|
|
1851
|
+
if (concurrency) {
|
|
1852
|
+
if (typeof concurrency === "string") {
|
|
1853
|
+
this._concurrencyStrategy = concurrency;
|
|
1854
|
+
this._concurrencyConfig = {
|
|
1855
|
+
debounceMs: 1500,
|
|
1856
|
+
maxConcurrent: Number.POSITIVE_INFINITY,
|
|
1857
|
+
maxQueueSize: 10,
|
|
1858
|
+
onQueueFull: "drop-oldest",
|
|
1859
|
+
queueEntryTtlMs: 9e4
|
|
1860
|
+
};
|
|
1861
|
+
} else {
|
|
1862
|
+
this._concurrencyStrategy = concurrency.strategy;
|
|
1863
|
+
this._concurrencyConfig = {
|
|
1864
|
+
debounceMs: concurrency.debounceMs ?? 1500,
|
|
1865
|
+
maxConcurrent: concurrency.maxConcurrent ?? Number.POSITIVE_INFINITY,
|
|
1866
|
+
maxQueueSize: concurrency.maxQueueSize ?? 10,
|
|
1867
|
+
onQueueFull: concurrency.onQueueFull ?? "drop-oldest",
|
|
1868
|
+
queueEntryTtlMs: concurrency.queueEntryTtlMs ?? 9e4
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
} else {
|
|
1872
|
+
this._concurrencyStrategy = "drop";
|
|
1873
|
+
this._concurrencyConfig = {
|
|
1874
|
+
debounceMs: 1500,
|
|
1875
|
+
maxConcurrent: Number.POSITIVE_INFINITY,
|
|
1876
|
+
maxQueueSize: 10,
|
|
1877
|
+
onQueueFull: "drop-oldest",
|
|
1878
|
+
queueEntryTtlMs: 9e4
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1843
1881
|
this._messageHistory = new MessageHistoryCache(
|
|
1844
1882
|
this._stateAdapter,
|
|
1845
1883
|
config.messageHistory
|
|
@@ -1907,6 +1945,22 @@ var Chat = class {
|
|
|
1907
1945
|
*/
|
|
1908
1946
|
async shutdown() {
|
|
1909
1947
|
this.logger.info("Shutting down chat instance...");
|
|
1948
|
+
const shutdownPromises = Array.from(this.adapters.values()).map(
|
|
1949
|
+
async (adapter) => {
|
|
1950
|
+
if (!adapter.disconnect) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
this.logger.debug("Disconnecting adapter", adapter.name);
|
|
1954
|
+
await adapter.disconnect();
|
|
1955
|
+
this.logger.debug("Adapter disconnected", adapter.name);
|
|
1956
|
+
}
|
|
1957
|
+
);
|
|
1958
|
+
const results = await Promise.allSettled(shutdownPromises);
|
|
1959
|
+
for (const result of results) {
|
|
1960
|
+
if (result.status === "rejected") {
|
|
1961
|
+
this.logger.error("Adapter disconnect failed", result.reason);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1910
1964
|
await this._stateAdapter.disconnect();
|
|
1911
1965
|
this.initialized = false;
|
|
1912
1966
|
this.initPromise = null;
|
|
@@ -2755,6 +2809,27 @@ var Chat = class {
|
|
|
2755
2809
|
"UNKNOWN_USER_ID_FORMAT"
|
|
2756
2810
|
);
|
|
2757
2811
|
}
|
|
2812
|
+
/**
|
|
2813
|
+
* Resolve the lock key for a message based on lock scope.
|
|
2814
|
+
* With 'thread' scope, returns threadId. With 'channel' scope,
|
|
2815
|
+
* returns channelId (derived via adapter.channelIdFromThreadId).
|
|
2816
|
+
*/
|
|
2817
|
+
async getLockKey(adapter, threadId) {
|
|
2818
|
+
const channelId = adapter.channelIdFromThreadId(threadId);
|
|
2819
|
+
let scope;
|
|
2820
|
+
if (typeof this._lockScope === "function") {
|
|
2821
|
+
const isDM = adapter.isDM?.(threadId) ?? false;
|
|
2822
|
+
scope = await this._lockScope({
|
|
2823
|
+
adapter,
|
|
2824
|
+
channelId,
|
|
2825
|
+
isDM,
|
|
2826
|
+
threadId
|
|
2827
|
+
});
|
|
2828
|
+
} else {
|
|
2829
|
+
scope = this._lockScope ?? adapter.lockScope ?? "thread";
|
|
2830
|
+
}
|
|
2831
|
+
return scope === "channel" ? channelId : threadId;
|
|
2832
|
+
}
|
|
2758
2833
|
/**
|
|
2759
2834
|
* Handle an incoming message from an adapter.
|
|
2760
2835
|
* This is called by adapters when they receive a webhook.
|
|
@@ -2763,7 +2838,7 @@ var Chat = class {
|
|
|
2763
2838
|
* - Deduplication: Same message may arrive multiple times (e.g., Slack sends
|
|
2764
2839
|
* both `message` and `app_mention` events, GChat sends direct webhook + Pub/Sub)
|
|
2765
2840
|
* - Bot filtering: Messages from the bot itself are skipped
|
|
2766
|
-
* -
|
|
2841
|
+
* - Concurrency: Controlled by `concurrency` config (drop, queue, debounce, concurrent)
|
|
2767
2842
|
*/
|
|
2768
2843
|
async handleIncomingMessage(adapter, threadId, message) {
|
|
2769
2844
|
this.logger.debug("Incoming message", {
|
|
@@ -2805,103 +2880,312 @@ var Chat = class {
|
|
|
2805
2880
|
}
|
|
2806
2881
|
await Promise.all(appends);
|
|
2807
2882
|
}
|
|
2883
|
+
const lockKey = await this.getLockKey(adapter, threadId);
|
|
2884
|
+
const strategy = this._concurrencyStrategy;
|
|
2885
|
+
if (strategy === "concurrent") {
|
|
2886
|
+
await this.handleConcurrent(adapter, threadId, message);
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
if (strategy === "queue" || strategy === "debounce") {
|
|
2890
|
+
await this.handleQueueOrDebounce(
|
|
2891
|
+
adapter,
|
|
2892
|
+
threadId,
|
|
2893
|
+
lockKey,
|
|
2894
|
+
message,
|
|
2895
|
+
strategy
|
|
2896
|
+
);
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
await this.handleDrop(adapter, threadId, lockKey, message);
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Drop strategy: acquire lock or fail. Original behavior.
|
|
2903
|
+
*/
|
|
2904
|
+
async handleDrop(adapter, threadId, lockKey, message) {
|
|
2808
2905
|
let lock = await this._stateAdapter.acquireLock(
|
|
2809
|
-
|
|
2906
|
+
lockKey,
|
|
2810
2907
|
DEFAULT_LOCK_TTL_MS
|
|
2811
2908
|
);
|
|
2812
2909
|
if (!lock) {
|
|
2813
2910
|
const resolution = typeof this._onLockConflict === "function" ? await this._onLockConflict(threadId, message) : this._onLockConflict ?? "drop";
|
|
2814
2911
|
if (resolution === "force") {
|
|
2815
|
-
this.logger.info("Force-releasing lock on thread", {
|
|
2816
|
-
await this._stateAdapter.forceReleaseLock(threadId);
|
|
2817
|
-
lock = await this._stateAdapter.acquireLock(
|
|
2912
|
+
this.logger.info("Force-releasing lock on thread", {
|
|
2818
2913
|
threadId,
|
|
2914
|
+
lockKey
|
|
2915
|
+
});
|
|
2916
|
+
await this._stateAdapter.forceReleaseLock(lockKey);
|
|
2917
|
+
lock = await this._stateAdapter.acquireLock(
|
|
2918
|
+
lockKey,
|
|
2819
2919
|
DEFAULT_LOCK_TTL_MS
|
|
2820
2920
|
);
|
|
2821
2921
|
}
|
|
2822
2922
|
if (!lock) {
|
|
2823
|
-
this.logger.warn("Could not acquire lock on thread", {
|
|
2923
|
+
this.logger.warn("Could not acquire lock on thread", {
|
|
2924
|
+
threadId,
|
|
2925
|
+
lockKey
|
|
2926
|
+
});
|
|
2824
2927
|
throw new LockError(
|
|
2825
2928
|
`Could not acquire lock on thread ${threadId}. Another instance may be processing.`
|
|
2826
2929
|
);
|
|
2827
2930
|
}
|
|
2828
2931
|
}
|
|
2829
|
-
this.logger.debug("Lock acquired", {
|
|
2932
|
+
this.logger.debug("Lock acquired", {
|
|
2933
|
+
threadId,
|
|
2934
|
+
lockKey,
|
|
2935
|
+
token: lock.token
|
|
2936
|
+
});
|
|
2830
2937
|
try {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
this.
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2938
|
+
await this.dispatchToHandlers(adapter, threadId, message);
|
|
2939
|
+
} finally {
|
|
2940
|
+
await this._stateAdapter.releaseLock(lock);
|
|
2941
|
+
this.logger.debug("Lock released", { threadId, lockKey });
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Queue/Debounce strategy: enqueue if lock is busy, drain after processing.
|
|
2946
|
+
*/
|
|
2947
|
+
async handleQueueOrDebounce(adapter, threadId, lockKey, message, strategy) {
|
|
2948
|
+
const { maxQueueSize, queueEntryTtlMs, onQueueFull, debounceMs } = this._concurrencyConfig;
|
|
2949
|
+
const lock = await this._stateAdapter.acquireLock(
|
|
2950
|
+
lockKey,
|
|
2951
|
+
DEFAULT_LOCK_TTL_MS
|
|
2952
|
+
);
|
|
2953
|
+
if (!lock) {
|
|
2954
|
+
const effectiveMaxSize = strategy === "debounce" ? 1 : maxQueueSize;
|
|
2955
|
+
const depth = await this._stateAdapter.queueDepth(lockKey);
|
|
2956
|
+
if (depth >= effectiveMaxSize && strategy !== "debounce" && onQueueFull === "drop-newest") {
|
|
2957
|
+
this.logger.info("message-dropped", {
|
|
2847
2958
|
threadId,
|
|
2848
|
-
|
|
2959
|
+
lockKey,
|
|
2960
|
+
messageId: message.id,
|
|
2961
|
+
reason: "queue-full"
|
|
2849
2962
|
});
|
|
2850
|
-
const channel = thread.channel;
|
|
2851
|
-
for (const handler of this.directMessageHandlers) {
|
|
2852
|
-
await handler(thread, message, channel);
|
|
2853
|
-
}
|
|
2854
2963
|
return;
|
|
2855
2964
|
}
|
|
2856
|
-
|
|
2857
|
-
|
|
2965
|
+
await this._stateAdapter.enqueue(
|
|
2966
|
+
lockKey,
|
|
2967
|
+
{
|
|
2968
|
+
message,
|
|
2969
|
+
enqueuedAt: Date.now(),
|
|
2970
|
+
expiresAt: Date.now() + queueEntryTtlMs
|
|
2971
|
+
},
|
|
2972
|
+
effectiveMaxSize
|
|
2973
|
+
);
|
|
2974
|
+
this.logger.info(
|
|
2975
|
+
strategy === "debounce" ? "message-debounce-reset" : "message-queued",
|
|
2976
|
+
{
|
|
2977
|
+
threadId,
|
|
2978
|
+
lockKey,
|
|
2979
|
+
messageId: message.id,
|
|
2980
|
+
queueDepth: Math.min(depth + 1, effectiveMaxSize)
|
|
2981
|
+
}
|
|
2982
|
+
);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
this.logger.debug("Lock acquired", {
|
|
2986
|
+
threadId,
|
|
2987
|
+
lockKey,
|
|
2988
|
+
token: lock.token
|
|
2989
|
+
});
|
|
2990
|
+
try {
|
|
2991
|
+
if (strategy === "debounce") {
|
|
2992
|
+
await this._stateAdapter.enqueue(
|
|
2993
|
+
lockKey,
|
|
2994
|
+
{
|
|
2995
|
+
message,
|
|
2996
|
+
enqueuedAt: Date.now(),
|
|
2997
|
+
expiresAt: Date.now() + queueEntryTtlMs
|
|
2998
|
+
},
|
|
2999
|
+
1
|
|
3000
|
+
);
|
|
3001
|
+
this.logger.info("message-debouncing", {
|
|
3002
|
+
threadId,
|
|
3003
|
+
lockKey,
|
|
3004
|
+
messageId: message.id,
|
|
3005
|
+
debounceMs
|
|
3006
|
+
});
|
|
3007
|
+
await this.debounceLoop(lock, adapter, threadId, lockKey);
|
|
3008
|
+
} else {
|
|
3009
|
+
await this.dispatchToHandlers(adapter, threadId, message);
|
|
3010
|
+
await this.drainQueue(lock, adapter, threadId, lockKey);
|
|
2858
3011
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
3012
|
+
} finally {
|
|
3013
|
+
await this._stateAdapter.releaseLock(lock);
|
|
3014
|
+
this.logger.debug("Lock released", { threadId, lockKey });
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
/**
|
|
3018
|
+
* Debounce loop: wait for debounceMs, check if newer message arrived,
|
|
3019
|
+
* repeat until no new messages, then process the final message.
|
|
3020
|
+
*/
|
|
3021
|
+
async debounceLoop(lock, adapter, threadId, lockKey) {
|
|
3022
|
+
const { debounceMs } = this._concurrencyConfig;
|
|
3023
|
+
while (true) {
|
|
3024
|
+
await sleep(debounceMs);
|
|
3025
|
+
await this._stateAdapter.extendLock(lock, DEFAULT_LOCK_TTL_MS);
|
|
3026
|
+
const entry = await this._stateAdapter.dequeue(lockKey);
|
|
3027
|
+
if (!entry) {
|
|
3028
|
+
break;
|
|
3029
|
+
}
|
|
3030
|
+
const msg = this.rehydrateMessage(entry.message);
|
|
3031
|
+
if (Date.now() > entry.expiresAt) {
|
|
3032
|
+
this.logger.info("message-expired", {
|
|
2861
3033
|
threadId,
|
|
2862
|
-
|
|
3034
|
+
lockKey,
|
|
3035
|
+
messageId: msg.id
|
|
2863
3036
|
});
|
|
2864
|
-
|
|
2865
|
-
return;
|
|
3037
|
+
continue;
|
|
2866
3038
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
3039
|
+
const depth = await this._stateAdapter.queueDepth(lockKey);
|
|
3040
|
+
if (depth > 0) {
|
|
3041
|
+
this.logger.info("message-superseded", {
|
|
2869
3042
|
threadId,
|
|
2870
|
-
|
|
3043
|
+
lockKey,
|
|
3044
|
+
droppedId: msg.id
|
|
2871
3045
|
});
|
|
2872
|
-
|
|
2873
|
-
return;
|
|
3046
|
+
continue;
|
|
2874
3047
|
}
|
|
2875
|
-
this.logger.
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
3048
|
+
this.logger.info("message-dequeued", {
|
|
3049
|
+
threadId,
|
|
3050
|
+
lockKey,
|
|
3051
|
+
messageId: msg.id
|
|
2879
3052
|
});
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
3053
|
+
await this.dispatchToHandlers(adapter, threadId, msg);
|
|
3054
|
+
break;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Drain queue: collect all pending messages, dispatch the latest with
|
|
3059
|
+
* skipped context, then check for more.
|
|
3060
|
+
*/
|
|
3061
|
+
async drainQueue(lock, adapter, threadId, lockKey) {
|
|
3062
|
+
while (true) {
|
|
3063
|
+
const pending = [];
|
|
3064
|
+
while (true) {
|
|
3065
|
+
const entry = await this._stateAdapter.dequeue(lockKey);
|
|
3066
|
+
if (!entry) {
|
|
3067
|
+
break;
|
|
3068
|
+
}
|
|
3069
|
+
const msg = this.rehydrateMessage(entry.message);
|
|
3070
|
+
if (Date.now() <= entry.expiresAt) {
|
|
3071
|
+
pending.push({ message: msg, expiresAt: entry.expiresAt });
|
|
3072
|
+
} else {
|
|
3073
|
+
this.logger.info("message-expired", {
|
|
3074
|
+
threadId,
|
|
3075
|
+
lockKey,
|
|
3076
|
+
messageId: msg.id
|
|
2891
3077
|
});
|
|
2892
|
-
matchedPattern = true;
|
|
2893
|
-
await handler(thread, message);
|
|
2894
3078
|
}
|
|
2895
3079
|
}
|
|
2896
|
-
if (
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3080
|
+
if (pending.length === 0) {
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
await this._stateAdapter.extendLock(lock, DEFAULT_LOCK_TTL_MS);
|
|
3084
|
+
const latest = pending.at(-1);
|
|
3085
|
+
if (!latest) {
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
const skipped = pending.slice(0, -1).map((e) => e.message);
|
|
3089
|
+
this.logger.info("message-dequeued", {
|
|
3090
|
+
threadId,
|
|
3091
|
+
lockKey,
|
|
3092
|
+
messageId: latest.message.id,
|
|
3093
|
+
skippedCount: skipped.length,
|
|
3094
|
+
totalSinceLastHandler: pending.length
|
|
3095
|
+
});
|
|
3096
|
+
const context = {
|
|
3097
|
+
skipped,
|
|
3098
|
+
totalSinceLastHandler: pending.length
|
|
3099
|
+
};
|
|
3100
|
+
await this.dispatchToHandlers(adapter, threadId, latest.message, context);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* Concurrent strategy: no locking, process immediately.
|
|
3105
|
+
*/
|
|
3106
|
+
async handleConcurrent(adapter, threadId, message) {
|
|
3107
|
+
await this.dispatchToHandlers(adapter, threadId, message);
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Dispatch a message to the appropriate handler chain based on
|
|
3111
|
+
* subscription status, mention detection, and pattern matching.
|
|
3112
|
+
*/
|
|
3113
|
+
async dispatchToHandlers(adapter, threadId, message, context) {
|
|
3114
|
+
message.isMention = message.isMention || this.detectMention(adapter, message);
|
|
3115
|
+
const isSubscribed = await this._stateAdapter.isSubscribed(threadId);
|
|
3116
|
+
this.logger.debug("Subscription check", {
|
|
3117
|
+
threadId,
|
|
3118
|
+
isSubscribed,
|
|
3119
|
+
subscribedHandlerCount: this.subscribedMessageHandlers.length
|
|
3120
|
+
});
|
|
3121
|
+
const thread = await this.createThread(
|
|
3122
|
+
adapter,
|
|
3123
|
+
threadId,
|
|
3124
|
+
message,
|
|
3125
|
+
isSubscribed
|
|
3126
|
+
);
|
|
3127
|
+
const isDM = adapter.isDM?.(threadId) ?? false;
|
|
3128
|
+
if (isDM && this.directMessageHandlers.length > 0) {
|
|
3129
|
+
this.logger.debug("Direct message received - calling handlers", {
|
|
3130
|
+
threadId,
|
|
3131
|
+
handlerCount: this.directMessageHandlers.length
|
|
3132
|
+
});
|
|
3133
|
+
const channel = thread.channel;
|
|
3134
|
+
for (const handler of this.directMessageHandlers) {
|
|
3135
|
+
await handler(thread, message, channel, context);
|
|
3136
|
+
}
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
if (isDM) {
|
|
3140
|
+
message.isMention = true;
|
|
3141
|
+
}
|
|
3142
|
+
if (isSubscribed) {
|
|
3143
|
+
this.logger.debug("Message in subscribed thread - calling handlers", {
|
|
3144
|
+
threadId,
|
|
3145
|
+
handlerCount: this.subscribedMessageHandlers.length
|
|
3146
|
+
});
|
|
3147
|
+
await this.runHandlers(
|
|
3148
|
+
this.subscribedMessageHandlers,
|
|
3149
|
+
thread,
|
|
3150
|
+
message,
|
|
3151
|
+
context
|
|
3152
|
+
);
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
if (message.isMention) {
|
|
3156
|
+
this.logger.debug("Bot mentioned", {
|
|
3157
|
+
threadId,
|
|
3158
|
+
text: message.text.slice(0, 100)
|
|
3159
|
+
});
|
|
3160
|
+
await this.runHandlers(this.mentionHandlers, thread, message, context);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
this.logger.debug("Checking message patterns", {
|
|
3164
|
+
patternCount: this.messagePatterns.length,
|
|
3165
|
+
patterns: this.messagePatterns.map((p) => p.pattern.toString()),
|
|
3166
|
+
messageText: message.text
|
|
3167
|
+
});
|
|
3168
|
+
let matchedPattern = false;
|
|
3169
|
+
for (const { pattern, handler } of this.messagePatterns) {
|
|
3170
|
+
const matches = pattern.test(message.text);
|
|
3171
|
+
this.logger.debug("Pattern test", {
|
|
3172
|
+
pattern: pattern.toString(),
|
|
3173
|
+
text: message.text,
|
|
3174
|
+
matches
|
|
3175
|
+
});
|
|
3176
|
+
if (matches) {
|
|
3177
|
+
this.logger.debug("Message matched pattern - calling handler", {
|
|
3178
|
+
pattern: pattern.toString()
|
|
2900
3179
|
});
|
|
3180
|
+
matchedPattern = true;
|
|
3181
|
+
await handler(thread, message, context);
|
|
2901
3182
|
}
|
|
2902
|
-
}
|
|
2903
|
-
|
|
2904
|
-
this.logger.debug("
|
|
3183
|
+
}
|
|
3184
|
+
if (!matchedPattern) {
|
|
3185
|
+
this.logger.debug("No handlers matched message", {
|
|
3186
|
+
threadId,
|
|
3187
|
+
text: message.text.slice(0, 100)
|
|
3188
|
+
});
|
|
2905
3189
|
}
|
|
2906
3190
|
}
|
|
2907
3191
|
createThread(adapter, threadId, initialMessage, isSubscribedContext = false) {
|
|
@@ -2958,9 +3242,45 @@ var Chat = class {
|
|
|
2958
3242
|
escapeRegex(str) {
|
|
2959
3243
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2960
3244
|
}
|
|
2961
|
-
|
|
3245
|
+
/**
|
|
3246
|
+
* Reconstruct a proper Message instance from a dequeued entry.
|
|
3247
|
+
* After JSON roundtrip through the state adapter, the message is a plain
|
|
3248
|
+
* object (not a Message instance). This restores class invariants like
|
|
3249
|
+
* `links` defaulting to `[]` and `metadata.dateSent` being a Date.
|
|
3250
|
+
*/
|
|
3251
|
+
rehydrateMessage(raw) {
|
|
3252
|
+
if (raw instanceof Message) {
|
|
3253
|
+
return raw;
|
|
3254
|
+
}
|
|
3255
|
+
const obj = raw;
|
|
3256
|
+
if (obj._type === "chat:Message") {
|
|
3257
|
+
return Message.fromJSON(obj);
|
|
3258
|
+
}
|
|
3259
|
+
const metadata = obj.metadata;
|
|
3260
|
+
const dateSent = metadata.dateSent;
|
|
3261
|
+
const editedAt = metadata.editedAt;
|
|
3262
|
+
return new Message({
|
|
3263
|
+
id: obj.id,
|
|
3264
|
+
threadId: obj.threadId,
|
|
3265
|
+
text: obj.text,
|
|
3266
|
+
formatted: obj.formatted,
|
|
3267
|
+
raw: obj.raw,
|
|
3268
|
+
author: obj.author,
|
|
3269
|
+
metadata: {
|
|
3270
|
+
dateSent: dateSent instanceof Date ? dateSent : new Date(dateSent),
|
|
3271
|
+
edited: metadata.edited,
|
|
3272
|
+
editedAt: editedAt ? new Date(
|
|
3273
|
+
editedAt instanceof Date ? editedAt.toISOString() : editedAt
|
|
3274
|
+
) : void 0
|
|
3275
|
+
},
|
|
3276
|
+
attachments: obj.attachments ?? [],
|
|
3277
|
+
isMention: obj.isMention,
|
|
3278
|
+
links: obj.links ?? []
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
async runHandlers(handlers, thread, message, context) {
|
|
2962
3282
|
for (const handler of handlers) {
|
|
2963
|
-
await handler(thread, message);
|
|
3283
|
+
await handler(thread, message, context);
|
|
2964
3284
|
}
|
|
2965
3285
|
}
|
|
2966
3286
|
};
|