@sunnoy/wecom 1.4.0 β 1.5.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/package.json +1 -1
- package/wecom/channel-plugin.js +51 -8
- package/wecom/inbound-processor.js +13 -0
- package/wecom/media.js +102 -29
- package/wecom/state.js +1 -0
- package/wecom/stream-utils.js +26 -1
package/package.json
CHANGED
package/wecom/channel-plugin.js
CHANGED
|
@@ -8,11 +8,18 @@ import { listAccountIds, resolveAccount, detectAccountConflicts } from "./accoun
|
|
|
8
8
|
import { DEFAULT_ACCOUNT_ID, THINKING_PLACEHOLDER } from "./constants.js";
|
|
9
9
|
import { parseResponseUrlResult } from "./response-url.js";
|
|
10
10
|
import { messageBuffers, resolveAgentConfig, resolveWebhookUrl, responseUrls, streamContext } from "./state.js";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveRecoverableStream, unregisterActiveStream } from "./stream-utils.js";
|
|
12
12
|
import { resolveWecomTarget } from "./target.js";
|
|
13
13
|
import { webhookSendImage, webhookSendText, webhookUploadFile, webhookSendFile } from "./webhook-bot.js";
|
|
14
14
|
import { registerWebhookTarget } from "./webhook-targets.js";
|
|
15
15
|
|
|
16
|
+
const AGENT_IMAGE_EXTS = new Set(["jpg", "jpeg", "png", "gif", "bmp"]);
|
|
17
|
+
|
|
18
|
+
export function resolveAgentMediaTypeFromFilename(filename) {
|
|
19
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
20
|
+
return AGENT_IMAGE_EXTS.has(ext) ? "image" : "file";
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
export const wecomChannelPlugin = {
|
|
17
24
|
id: "wecom",
|
|
18
25
|
meta: {
|
|
@@ -259,7 +266,7 @@ export const wecomChannelPlugin = {
|
|
|
259
266
|
|
|
260
267
|
// Prefer stream from async context (correct for concurrent processing).
|
|
261
268
|
const ctx = streamContext.getStore();
|
|
262
|
-
const streamId = ctx?.streamId ??
|
|
269
|
+
const streamId = ctx?.streamId ?? resolveRecoverableStream(userId);
|
|
263
270
|
|
|
264
271
|
// Layer 1: Active stream (normal path)
|
|
265
272
|
if (streamId && streamManager.hasStream(streamId) && !streamManager.getStream(streamId)?.finished) {
|
|
@@ -377,7 +384,7 @@ export const wecomChannelPlugin = {
|
|
|
377
384
|
|
|
378
385
|
// Prefer stream from async context (correct for concurrent processing).
|
|
379
386
|
const ctx = streamContext.getStore();
|
|
380
|
-
const streamId = ctx?.streamId ??
|
|
387
|
+
const streamId = ctx?.streamId ?? resolveRecoverableStream(userId);
|
|
381
388
|
|
|
382
389
|
if (streamId && streamManager.hasStream(streamId)) {
|
|
383
390
|
// Check if mediaUrl is a local path (sandbox: prefix or absolute path)
|
|
@@ -575,6 +582,7 @@ export const wecomChannelPlugin = {
|
|
|
575
582
|
if (agentConfig) {
|
|
576
583
|
try {
|
|
577
584
|
const agentTarget = (target && !target.webhook) ? target : resolveWecomTarget(to) || { toUser: userId };
|
|
585
|
+
let deliveredFilename = "file";
|
|
578
586
|
|
|
579
587
|
// Determine if mediaUrl is a local file path.
|
|
580
588
|
let absolutePath = mediaUrl;
|
|
@@ -587,9 +595,11 @@ export const wecomChannelPlugin = {
|
|
|
587
595
|
// Upload local file then send via Agent API.
|
|
588
596
|
const buffer = await readFile(absolutePath);
|
|
589
597
|
const filename = basename(absolutePath);
|
|
598
|
+
deliveredFilename = filename;
|
|
599
|
+
const uploadType = resolveAgentMediaTypeFromFilename(filename);
|
|
590
600
|
const mediaId = await agentUploadMedia({
|
|
591
601
|
agent: agentConfig,
|
|
592
|
-
type:
|
|
602
|
+
type: uploadType,
|
|
593
603
|
buffer,
|
|
594
604
|
filename,
|
|
595
605
|
});
|
|
@@ -597,16 +607,25 @@ export const wecomChannelPlugin = {
|
|
|
597
607
|
agent: agentConfig,
|
|
598
608
|
...agentTarget,
|
|
599
609
|
mediaId,
|
|
600
|
-
mediaType:
|
|
610
|
+
mediaType: uploadType,
|
|
601
611
|
});
|
|
602
612
|
} else {
|
|
603
613
|
// For external URLs, download first then upload.
|
|
604
614
|
const res = await fetch(mediaUrl);
|
|
615
|
+
if (!res.ok) {
|
|
616
|
+
throw new Error(`download media failed: ${res.status}`);
|
|
617
|
+
}
|
|
605
618
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
606
|
-
const filename = basename(new URL(mediaUrl).pathname) || "
|
|
619
|
+
const filename = basename(new URL(mediaUrl).pathname) || "file";
|
|
620
|
+
deliveredFilename = filename;
|
|
621
|
+
let uploadType = resolveAgentMediaTypeFromFilename(filename);
|
|
622
|
+
const contentType = res.headers.get("content-type") || "";
|
|
623
|
+
if (uploadType === "file" && contentType.toLowerCase().startsWith("image/")) {
|
|
624
|
+
uploadType = "image";
|
|
625
|
+
}
|
|
607
626
|
const mediaId = await agentUploadMedia({
|
|
608
627
|
agent: agentConfig,
|
|
609
|
-
type:
|
|
628
|
+
type: uploadType,
|
|
610
629
|
buffer,
|
|
611
630
|
filename,
|
|
612
631
|
});
|
|
@@ -614,7 +633,7 @@ export const wecomChannelPlugin = {
|
|
|
614
633
|
agent: agentConfig,
|
|
615
634
|
...agentTarget,
|
|
616
635
|
mediaId,
|
|
617
|
-
mediaType:
|
|
636
|
+
mediaType: uploadType,
|
|
618
637
|
});
|
|
619
638
|
}
|
|
620
639
|
|
|
@@ -623,6 +642,30 @@ export const wecomChannelPlugin = {
|
|
|
623
642
|
await agentSendText({ agent: agentConfig, ...agentTarget, text });
|
|
624
643
|
}
|
|
625
644
|
|
|
645
|
+
// Best-effort stream recovery: when async context is missing and the
|
|
646
|
+
// active stream mapping was already cleaned, still clear "thinking..."
|
|
647
|
+
// in the most recent stream for this user.
|
|
648
|
+
const recoverStreamId = resolveRecoverableStream(userId);
|
|
649
|
+
if (recoverStreamId && streamManager.hasStream(recoverStreamId)) {
|
|
650
|
+
const recoverStream = streamManager.getStream(recoverStreamId);
|
|
651
|
+
if (recoverStream && !recoverStream.finished) {
|
|
652
|
+
const deliveryHint = text
|
|
653
|
+
? `${text}\n\nπ ζδ»Άε·²ιθΏη§δΏ‘ειη»ζ¨οΌ${deliveredFilename}`
|
|
654
|
+
: `π ζδ»Άε·²ιθΏη§δΏ‘ειη»ζ¨οΌ${deliveredFilename}`;
|
|
655
|
+
streamManager.replaceIfPlaceholder(
|
|
656
|
+
recoverStreamId,
|
|
657
|
+
deliveryHint,
|
|
658
|
+
THINKING_PLACEHOLDER,
|
|
659
|
+
);
|
|
660
|
+
await streamManager.finishStream(recoverStreamId);
|
|
661
|
+
unregisterActiveStream(userId, recoverStreamId);
|
|
662
|
+
logger.info("WeCom: recovered and finished stream after media fallback", {
|
|
663
|
+
userId,
|
|
664
|
+
streamId: recoverStreamId,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
626
669
|
logger.info("WeCom: sent media via Agent API fallback (sendMedia)", {
|
|
627
670
|
userId,
|
|
628
671
|
to,
|
|
@@ -166,6 +166,19 @@ export async function processInboundMessage({
|
|
|
166
166
|
senderId,
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
+
// ββ Quoted message context ββββββββββββββββββββββββββββββββββββββββ
|
|
170
|
+
// When the user replies to (quotes) a previous message, prepend the
|
|
171
|
+
// quoted content so the LLM sees the full conversational context.
|
|
172
|
+
const quote = message.quote;
|
|
173
|
+
if (quote && quote.content) {
|
|
174
|
+
const quoteLabel = quote.msgType === "image" ? "[εΌη¨εΎη]" : `> ${quote.content}`;
|
|
175
|
+
rawBody = `${quoteLabel}\n\n${rawBody}`;
|
|
176
|
+
logger.debug("WeCom: prepended quoted message context", {
|
|
177
|
+
quoteType: quote.msgType,
|
|
178
|
+
quotePreview: quote.content.substring(0, 60),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
// Skip empty messages, but allow image/mixed/file messages.
|
|
170
183
|
if (!rawBody.trim() && !imageUrl && imageUrls.length === 0 && !fileUrl) {
|
|
171
184
|
logger.debug("WeCom: empty message, skipping", { msgType });
|
package/wecom/media.js
CHANGED
|
@@ -4,66 +4,134 @@ import { WecomCrypto } from "../crypto.js";
|
|
|
4
4
|
import { logger } from "../logger.js";
|
|
5
5
|
import { MEDIA_CACHE_DIR } from "./constants.js";
|
|
6
6
|
|
|
7
|
+
// ββ Magic-byte signatures for common file formats βββββββββββββββββββββββββββ
|
|
8
|
+
const MAGIC_SIGNATURES = [
|
|
9
|
+
{ magic: [0xff, 0xd8, 0xff], ext: "jpg" }, // JPEG
|
|
10
|
+
{ magic: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a], ext: "png" }, // PNG
|
|
11
|
+
{ magic: [0x47, 0x49, 0x46, 0x38], ext: "gif" }, // GIF
|
|
12
|
+
{ magic: [0x25, 0x50, 0x44, 0x46], ext: "pdf" }, // %PDF
|
|
13
|
+
{ magic: [0x50, 0x4b, 0x03, 0x04], ext: "zip" }, // PK (ZIP / DOCX / XLSX / PPTX)
|
|
14
|
+
{ magic: [0x50, 0x4b, 0x05, 0x06], ext: "zip" }, // PK (empty ZIP)
|
|
15
|
+
{ magic: [0xd0, 0xcf, 0x11, 0xe0], ext: "doc" }, // OLE2 (DOC / XLS / PPT)
|
|
16
|
+
{ magic: [0x52, 0x61, 0x72, 0x21], ext: "rar" }, // Rar!
|
|
17
|
+
{ magic: [0x1f, 0x8b], ext: "gz" }, // gzip
|
|
18
|
+
{ magic: [0x42, 0x4d], ext: "bmp" }, // BMP
|
|
19
|
+
{ magic: [0x49, 0x44, 0x33], ext: "mp3" }, // ID3 (MP3)
|
|
20
|
+
{ magic: [0x00, 0x00, 0x00], ext: "mp4" }, // ftyp box (loose)
|
|
21
|
+
{ magic: [0x52, 0x49, 0x46, 0x46], ext: "wav" }, // RIFF (WAV / AVI)
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check whether a buffer looks like a recognisable (plain) file by inspecting
|
|
26
|
+
* its leading magic bytes. Returns the matched extension or `null`.
|
|
27
|
+
*/
|
|
28
|
+
function detectMagic(buf) {
|
|
29
|
+
if (!buf || buf.length < 4) return null;
|
|
30
|
+
for (const sig of MAGIC_SIGNATURES) {
|
|
31
|
+
if (buf.length >= sig.magic.length && sig.magic.every((b, i) => buf[i] === b)) {
|
|
32
|
+
return sig.ext;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Conditionally decrypt a buffer.
|
|
40
|
+
*
|
|
41
|
+
* WeCom encrypts media with AES-256-CBC in Agent mode, but in some Bot (AI
|
|
42
|
+
* ζΊε¨δΊΊ) configurations the file URL returns an **unencrypted** payload.
|
|
43
|
+
* Blindly decrypting an already-plain file corrupts it (#44).
|
|
44
|
+
*
|
|
45
|
+
* Strategy:
|
|
46
|
+
* 1. If raw bytes already match a known file signature β skip decrypt.
|
|
47
|
+
* 2. Otherwise attempt AES-256-CBC decryption.
|
|
48
|
+
* 3. If decryption throws or the result has no recognisable signature,
|
|
49
|
+
* fall back to the original bytes (best-effort).
|
|
50
|
+
*/
|
|
51
|
+
function smartDecrypt(rawBuffer, encodingAesKey, token) {
|
|
52
|
+
const plainExt = detectMagic(rawBuffer);
|
|
53
|
+
if (plainExt) {
|
|
54
|
+
logger.info("Media is already plain (skip decrypt)", { magic: plainExt, size: rawBuffer.length });
|
|
55
|
+
return { buffer: rawBuffer, decrypted: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const crypto = new WecomCrypto(token, encodingAesKey);
|
|
60
|
+
const dec = crypto.decryptMedia(rawBuffer);
|
|
61
|
+
logger.info("Media decrypted", { inputSize: rawBuffer.length, outputSize: dec.length });
|
|
62
|
+
return { buffer: dec, decrypted: true };
|
|
63
|
+
} catch (e) {
|
|
64
|
+
logger.warn("Decrypt failed, using raw bytes", { error: e.message, size: rawBuffer.length });
|
|
65
|
+
return { buffer: rawBuffer, decrypted: false };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ββ Image helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
70
|
+
|
|
71
|
+
function detectImageExt(buf) {
|
|
72
|
+
if (buf[0] === 0x89 && buf[1] === 0x50) return "png";
|
|
73
|
+
if (buf[0] === 0x47 && buf[1] === 0x49) return "gif";
|
|
74
|
+
return "jpg";
|
|
75
|
+
}
|
|
76
|
+
|
|
7
77
|
/**
|
|
8
|
-
* Download and decrypt a WeCom
|
|
9
|
-
* @param {string} imageUrl -
|
|
78
|
+
* Download and (conditionally) decrypt a WeCom image.
|
|
79
|
+
* @param {string} imageUrl - Image URL from WeCom callback
|
|
10
80
|
* @param {string} encodingAesKey - AES key
|
|
11
81
|
* @param {string} token - Token
|
|
12
|
-
* @returns {Promise<{ localPath: string, mimeType: string }>}
|
|
82
|
+
* @returns {Promise<{ localPath: string, mimeType: string }>}
|
|
13
83
|
*/
|
|
14
84
|
export async function downloadAndDecryptImage(imageUrl, encodingAesKey, token) {
|
|
15
85
|
if (!existsSync(MEDIA_CACHE_DIR)) {
|
|
16
86
|
mkdirSync(MEDIA_CACHE_DIR, { recursive: true });
|
|
17
87
|
}
|
|
18
88
|
|
|
19
|
-
logger.info("Downloading
|
|
89
|
+
logger.info("Downloading image", { url: imageUrl.substring(0, 80) });
|
|
20
90
|
const response = await fetch(imageUrl);
|
|
21
91
|
if (!response.ok) {
|
|
22
92
|
throw new Error(`Failed to download image: ${response.status}`);
|
|
23
93
|
}
|
|
24
|
-
const
|
|
25
|
-
logger.debug("Downloaded
|
|
26
|
-
|
|
27
|
-
const wecomCrypto = new WecomCrypto(token, encodingAesKey);
|
|
28
|
-
const decryptedBuffer = wecomCrypto.decryptMedia(encryptedBuffer);
|
|
29
|
-
|
|
30
|
-
// Detect image type via magic bytes.
|
|
31
|
-
let ext = "jpg";
|
|
32
|
-
if (decryptedBuffer[0] === 0x89 && decryptedBuffer[1] === 0x50) {
|
|
33
|
-
ext = "png";
|
|
34
|
-
} else if (decryptedBuffer[0] === 0x47 && decryptedBuffer[1] === 0x49) {
|
|
35
|
-
ext = "gif";
|
|
36
|
-
}
|
|
94
|
+
const rawBuffer = Buffer.from(await response.arrayBuffer());
|
|
95
|
+
logger.debug("Downloaded image bytes", { size: rawBuffer.length });
|
|
37
96
|
|
|
97
|
+
const { buffer: finalBuffer } = smartDecrypt(rawBuffer, encodingAesKey, token);
|
|
98
|
+
|
|
99
|
+
const ext = detectImageExt(finalBuffer);
|
|
38
100
|
const filename = `wecom_${Date.now()}_${Math.random().toString(36).substring(2, 8)}.${ext}`;
|
|
39
101
|
const localPath = join(MEDIA_CACHE_DIR, filename);
|
|
40
|
-
writeFileSync(localPath,
|
|
102
|
+
writeFileSync(localPath, finalBuffer);
|
|
41
103
|
|
|
42
104
|
const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : "image/jpeg";
|
|
43
|
-
logger.info("Image
|
|
105
|
+
logger.info("Image saved", { path: localPath, size: finalBuffer.length, mimeType });
|
|
44
106
|
return { localPath, mimeType };
|
|
45
107
|
}
|
|
46
108
|
|
|
109
|
+
// ββ File helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
110
|
+
|
|
47
111
|
/**
|
|
48
|
-
* Download and decrypt a file from WeCom.
|
|
49
|
-
*
|
|
112
|
+
* Download and (conditionally) decrypt a file from WeCom.
|
|
113
|
+
*
|
|
114
|
+
* In Agent mode WeCom encrypts all media with AES-256-CBC; in Bot mode the
|
|
115
|
+
* file URL may return plain bytes. This function auto-detects the case and
|
|
116
|
+
* only decrypts when necessary, preventing file corruption (#44).
|
|
117
|
+
*
|
|
50
118
|
* @param {string} fileUrl - File download URL
|
|
51
119
|
* @param {string} fileName - Original file name
|
|
52
120
|
* @param {string} encodingAesKey - AES key for decryption
|
|
53
121
|
* @param {string} token - Token for decryption
|
|
54
|
-
* @returns {Promise<{ localPath: string, effectiveFileName: string }>}
|
|
122
|
+
* @returns {Promise<{ localPath: string, effectiveFileName: string }>}
|
|
55
123
|
*/
|
|
56
124
|
export async function downloadWecomFile(fileUrl, fileName, encodingAesKey, token) {
|
|
57
125
|
if (!existsSync(MEDIA_CACHE_DIR)) {
|
|
58
126
|
mkdirSync(MEDIA_CACHE_DIR, { recursive: true });
|
|
59
127
|
}
|
|
60
128
|
|
|
61
|
-
logger.info("Downloading
|
|
129
|
+
logger.info("Downloading file", { url: fileUrl.substring(0, 80), name: fileName });
|
|
62
130
|
const response = await fetch(fileUrl);
|
|
63
131
|
if (!response.ok) {
|
|
64
132
|
throw new Error(`Failed to download file: ${response.status}`);
|
|
65
133
|
}
|
|
66
|
-
const
|
|
134
|
+
const rawBuffer = Buffer.from(await response.arrayBuffer());
|
|
67
135
|
|
|
68
136
|
// Try to extract filename from Content-Disposition header if not provided
|
|
69
137
|
let effectiveFileName = fileName;
|
|
@@ -79,15 +147,20 @@ export async function downloadWecomFile(fileUrl, fileName, encodingAesKey, token
|
|
|
79
147
|
}
|
|
80
148
|
}
|
|
81
149
|
|
|
82
|
-
//
|
|
83
|
-
const
|
|
84
|
-
|
|
150
|
+
// Smart decrypt: only decrypt if the raw bytes are not already a valid file.
|
|
151
|
+
const { buffer: finalBuffer, decrypted } = smartDecrypt(rawBuffer, encodingAesKey, token);
|
|
152
|
+
logger.info("File processed", {
|
|
153
|
+
name: effectiveFileName,
|
|
154
|
+
rawSize: rawBuffer.length,
|
|
155
|
+
finalSize: finalBuffer.length,
|
|
156
|
+
wasDecrypted: decrypted,
|
|
157
|
+
});
|
|
85
158
|
|
|
86
159
|
const safeName = (effectiveFileName || `file_${Date.now()}`).replace(/[/\\:*?"<>|]/g, "_");
|
|
87
160
|
const localPath = join(MEDIA_CACHE_DIR, `${Date.now()}_${safeName}`);
|
|
88
|
-
writeFileSync(localPath,
|
|
161
|
+
writeFileSync(localPath, finalBuffer);
|
|
89
162
|
|
|
90
|
-
logger.info("File
|
|
163
|
+
logger.info("File saved", { path: localPath, size: finalBuffer.length });
|
|
91
164
|
return { localPath, effectiveFileName: effectiveFileName || fileName };
|
|
92
165
|
}
|
|
93
166
|
|
package/wecom/state.js
CHANGED
|
@@ -14,6 +14,7 @@ export const messageBuffers = new Map();
|
|
|
14
14
|
export const webhookTargets = new Map();
|
|
15
15
|
export const activeStreams = new Map();
|
|
16
16
|
export const activeStreamHistory = new Map();
|
|
17
|
+
export const lastStreamByKey = new Map();
|
|
17
18
|
export const streamMeta = new Map();
|
|
18
19
|
export const responseUrls = new Map();
|
|
19
20
|
export const streamContext = new AsyncLocalStorage();
|
package/wecom/stream-utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { logger } from "../logger.js";
|
|
2
2
|
import { streamManager } from "../stream-manager.js";
|
|
3
3
|
import { THINKING_PLACEHOLDER } from "./constants.js";
|
|
4
|
-
import { activeStreamHistory, activeStreams, messageBuffers } from "./state.js";
|
|
4
|
+
import { activeStreamHistory, activeStreams, lastStreamByKey, messageBuffers } from "./state.js";
|
|
5
5
|
|
|
6
6
|
export function getMessageStreamKey(message) {
|
|
7
7
|
if (!message || typeof message !== "object") {
|
|
@@ -25,6 +25,7 @@ export function registerActiveStream(streamKey, streamId) {
|
|
|
25
25
|
deduped.push(streamId);
|
|
26
26
|
activeStreamHistory.set(streamKey, deduped);
|
|
27
27
|
activeStreams.set(streamKey, streamId);
|
|
28
|
+
lastStreamByKey.set(streamKey, streamId);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function unregisterActiveStream(streamKey, streamId) {
|
|
@@ -72,9 +73,33 @@ export function resolveActiveStream(streamKey) {
|
|
|
72
73
|
activeStreamHistory.set(streamKey, remaining);
|
|
73
74
|
const latest = remaining[remaining.length - 1];
|
|
74
75
|
activeStreams.set(streamKey, latest);
|
|
76
|
+
lastStreamByKey.set(streamKey, latest);
|
|
75
77
|
return latest;
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a usable stream id for a sender/group.
|
|
82
|
+
* Prefer active history; if that is temporarily empty, fall back to the latest
|
|
83
|
+
* known stream id for the same key (when it still exists).
|
|
84
|
+
*/
|
|
85
|
+
export function resolveRecoverableStream(streamKey) {
|
|
86
|
+
const activeId = resolveActiveStream(streamKey);
|
|
87
|
+
if (activeId) {
|
|
88
|
+
return activeId;
|
|
89
|
+
}
|
|
90
|
+
if (!streamKey) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const recentId = lastStreamByKey.get(streamKey);
|
|
94
|
+
if (!recentId) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (!streamManager.hasStream(recentId)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return recentId;
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
export function clearBufferedMessagesForStream(streamKey, reason) {
|
|
79
104
|
const buffer = messageBuffers.get(streamKey);
|
|
80
105
|
if (!buffer) {
|