@openclaw/line 2026.7.2-beta.1 → 2026.7.2-beta.2
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/api.js +2 -2
- package/dist/{channel-CnPxxRbJ.js → channel-BHR-GoJ0.js} +54 -73
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel.runtime-DFgZ_Xwn.js +4 -0
- package/dist/{markdown-to-line-CixmAKiG.js → markdown-to-line-XceqeKRP.js} +3 -3
- package/dist/{monitor-q3UE2J1g.js → monitor-Db2rZsSX.js} +275 -166
- package/dist/monitor.runtime-CZuQgsEE.js +2 -0
- package/dist/{outbound.runtime-CI-t7Z8P.js → outbound.runtime-DnpU0Cms.js} +1 -1
- package/dist/{reply-payload-transform-BsbwCJGH.js → reply-payload-transform-r9sa-TYT.js} +61 -50
- package/dist/runtime-api.js +6 -5
- package/dist/setup-api.js +1 -1
- package/dist/{setup-surface-Bc36YSS3.js → setup-surface-B7W6pwn3.js} +36 -44
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +57 -57
- package/package.json +4 -4
- package/dist/channel.runtime-FPcRtjcT.js +0 -4
- package/dist/monitor.runtime-BiNrVz5G.js +0 -2
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-DJVOv1JI.js";
|
|
2
|
-
import {
|
|
3
|
-
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage,
|
|
2
|
+
import { a as buildLineMediaMessage, i as buildLineQuickReplyFallbackText, l as getLineRuntime, m as resolveLineGroupConfigEntry, o as hasLineSpecificMediaOptions } from "./reply-payload-transform-r9sa-TYT.js";
|
|
3
|
+
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-XceqeKRP.js";
|
|
4
4
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
5
5
|
import { normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
6
|
import { createMessageReceiveContext } from "openclaw/plugin-sdk/channel-outbound";
|
|
7
7
|
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
8
|
+
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
8
9
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
9
10
|
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
10
11
|
import { firstDefined } from "openclaw/plugin-sdk/allow-from";
|
|
11
|
-
import {
|
|
12
|
+
import { Readable } from "node:stream";
|
|
13
|
+
import { finished } from "node:stream/promises";
|
|
14
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
12
15
|
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
13
16
|
import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
|
|
17
|
+
import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
|
|
14
18
|
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
|
15
19
|
import { buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
16
20
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
17
21
|
import { isRequestBodyLimitError, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
|
|
18
|
-
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1 } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
22
|
+
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1, runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
19
23
|
import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
|
|
20
24
|
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
21
25
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
22
|
-
import {
|
|
26
|
+
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
|
|
23
27
|
import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
24
|
-
import {
|
|
28
|
+
import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
25
29
|
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
26
30
|
import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
27
31
|
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
|
@@ -44,8 +48,51 @@ const normalizeAllowFrom = (list) => {
|
|
|
44
48
|
};
|
|
45
49
|
//#endregion
|
|
46
50
|
//#region extensions/line/src/download.ts
|
|
51
|
+
const CONTENT_READY_MAX_ATTEMPTS = 6;
|
|
52
|
+
const CONTENT_READY_BASE_DELAY_MS = 500;
|
|
53
|
+
const CONTENT_READY_MAX_DELAY_MS = 4e3;
|
|
54
|
+
const CONTENT_READY_TIMEOUT_MS = 15e3;
|
|
55
|
+
const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
|
|
56
|
+
function contentBackoffDelayMs(attempt) {
|
|
57
|
+
return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
|
|
58
|
+
}
|
|
59
|
+
async function fetchLineContentWhenReady(messageId, channelAccessToken) {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
|
|
62
|
+
deadline.unref();
|
|
63
|
+
try {
|
|
64
|
+
for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
|
|
65
|
+
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(`${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`, {
|
|
66
|
+
headers: { Authorization: `Bearer ${channelAccessToken}` },
|
|
67
|
+
redirect: "error",
|
|
68
|
+
signal: controller.signal
|
|
69
|
+
});
|
|
70
|
+
if (response.status === 200) {
|
|
71
|
+
if (!response.body) throw new Error(`LINE media response for message ${messageId} had no body`);
|
|
72
|
+
return Readable.fromWeb(response.body);
|
|
73
|
+
}
|
|
74
|
+
await response.body?.cancel();
|
|
75
|
+
if (response.status !== 202) throw new Error(`LINE media download failed for message ${messageId} (HTTP ${response.status})`);
|
|
76
|
+
if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (controller.signal.aborted) throw new Error(`LINE media for message ${messageId} did not become ready within ${CONTENT_READY_TIMEOUT_MS / 1e3} seconds`, { cause: err });
|
|
80
|
+
throw err;
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(deadline);
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`);
|
|
85
|
+
}
|
|
47
86
|
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
|
|
48
|
-
const
|
|
87
|
+
const content = await fetchLineContentWhenReady(messageId, channelAccessToken);
|
|
88
|
+
let saved;
|
|
89
|
+
try {
|
|
90
|
+
saved = await saveMediaStream(content, void 0, "inbound", maxBytes, options?.originalFilename);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
content.destroy();
|
|
93
|
+
await finished(content).catch(() => void 0);
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
49
96
|
logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
|
|
50
97
|
return {
|
|
51
98
|
path: saved.path,
|
|
@@ -55,15 +102,19 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
|
|
|
55
102
|
}
|
|
56
103
|
//#endregion
|
|
57
104
|
//#region extensions/line/src/auto-reply-delivery.ts
|
|
105
|
+
function toLineDeliveryError(error) {
|
|
106
|
+
return error instanceof Error ? error : new Error("LINE rich or media message send failed", { cause: error });
|
|
107
|
+
}
|
|
58
108
|
function markLineVisibleDeliveryError(error) {
|
|
59
|
-
|
|
60
|
-
|
|
109
|
+
const deliveryError = toLineDeliveryError(error);
|
|
110
|
+
if (Object.isExtensible(deliveryError)) {
|
|
111
|
+
Object.assign(deliveryError, {
|
|
61
112
|
sentBeforeError: true,
|
|
62
113
|
visibleReplySent: true
|
|
63
114
|
});
|
|
64
|
-
return
|
|
115
|
+
return deliveryError;
|
|
65
116
|
}
|
|
66
|
-
const visibleError = new Error("LINE rich or media message send failed", { cause:
|
|
117
|
+
const visibleError = new Error("LINE rich or media message send failed", { cause: deliveryError });
|
|
67
118
|
Object.assign(visibleError, {
|
|
68
119
|
sentBeforeError: true,
|
|
69
120
|
visibleReplySent: true
|
|
@@ -73,12 +124,26 @@ function markLineVisibleDeliveryError(error) {
|
|
|
73
124
|
async function deliverLineAutoReply(params) {
|
|
74
125
|
const { payload, lineData, replyToken, accountId, to, textLimit, deps } = params;
|
|
75
126
|
let replyTokenUsed = params.replyTokenUsed;
|
|
127
|
+
let visibleReplySent = false;
|
|
128
|
+
const sendVisible = async (send) => {
|
|
129
|
+
try {
|
|
130
|
+
const result = await send();
|
|
131
|
+
visibleReplySent = true;
|
|
132
|
+
return result;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (visibleReplySent) throw markLineVisibleDeliveryError(error);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const replyVisible = (...args) => sendVisible(() => deps.replyMessageLine(...args));
|
|
139
|
+
const pushTextVisible = (...args) => sendVisible(() => deps.pushMessageLine(...args));
|
|
140
|
+
const pushQuickRepliesVisible = (...args) => sendVisible(() => deps.pushTextMessageWithQuickReplies(...args));
|
|
76
141
|
const pushLineMessages = async (messages) => {
|
|
77
142
|
if (messages.length === 0) return;
|
|
78
|
-
for (let i = 0; i < messages.length; i += 5) await deps.pushMessagesLine(to, messages.slice(i, i + 5), {
|
|
143
|
+
for (let i = 0; i < messages.length; i += 5) await sendVisible(() => deps.pushMessagesLine(to, messages.slice(i, i + 5), {
|
|
79
144
|
cfg: params.cfg,
|
|
80
145
|
accountId
|
|
81
|
-
});
|
|
146
|
+
}));
|
|
82
147
|
};
|
|
83
148
|
const sendLineMessages = async (messages, allowReplyToken) => {
|
|
84
149
|
if (messages.length === 0) return;
|
|
@@ -86,7 +151,7 @@ async function deliverLineAutoReply(params) {
|
|
|
86
151
|
if (allowReplyToken && replyToken && !replyTokenUsed) {
|
|
87
152
|
const replyBatch = remaining.slice(0, 5);
|
|
88
153
|
try {
|
|
89
|
-
await
|
|
154
|
+
await replyVisible(replyToken, replyBatch, {
|
|
90
155
|
cfg: params.cfg,
|
|
91
156
|
accountId
|
|
92
157
|
});
|
|
@@ -107,21 +172,38 @@ async function deliverLineAutoReply(params) {
|
|
|
107
172
|
if (templateMsg) richMessages.push(templateMsg);
|
|
108
173
|
}
|
|
109
174
|
if (lineData.location) richMessages.push(deps.createLocationMessage(lineData.location));
|
|
110
|
-
const
|
|
175
|
+
const visibleText = payload.text ? sanitizeAssistantVisibleText(payload.text) : "";
|
|
176
|
+
const processed = visibleText ? deps.processLineMessage(visibleText) : {
|
|
111
177
|
text: "",
|
|
112
178
|
flexMessages: []
|
|
113
179
|
};
|
|
114
180
|
for (const flexMsg of processed.flexMessages) richMessages.push(deps.createFlexMessage(truncateUtf16Safe(flexMsg.altText, 400), flexMsg.contents));
|
|
115
181
|
const chunks = processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : [];
|
|
116
|
-
const
|
|
182
|
+
const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
|
|
183
|
+
const mediaOpts = {
|
|
184
|
+
mediaKind: hasLineSpecificMediaOptions(lineData) ? lineData.mediaKind : "image",
|
|
185
|
+
previewImageUrl: lineData.previewImageUrl,
|
|
186
|
+
durationMs: lineData.durationMs,
|
|
187
|
+
trackingId: lineData.trackingId
|
|
188
|
+
};
|
|
189
|
+
const mediaMessages = [];
|
|
190
|
+
let richMediaError;
|
|
191
|
+
for (const rawUrl of mediaUrls) {
|
|
192
|
+
const url = rawUrl?.trim();
|
|
193
|
+
if (!url) continue;
|
|
194
|
+
try {
|
|
195
|
+
mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
|
|
196
|
+
} catch (err) {
|
|
197
|
+
richMediaError ??= err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
117
200
|
if (chunks.length > 0) {
|
|
118
201
|
const hasRichOrMedia = richMessages.length > 0 || mediaMessages.length > 0;
|
|
119
202
|
const sendRichBeforeText = hasQuickReplies && hasRichOrMedia;
|
|
120
|
-
let richMediaError;
|
|
121
203
|
if (sendRichBeforeText) try {
|
|
122
204
|
await sendLineMessages([...richMessages, ...mediaMessages], false);
|
|
123
205
|
} catch (err) {
|
|
124
|
-
richMediaError
|
|
206
|
+
richMediaError ??= err;
|
|
125
207
|
}
|
|
126
208
|
const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
|
|
127
209
|
to,
|
|
@@ -131,23 +213,19 @@ async function deliverLineAutoReply(params) {
|
|
|
131
213
|
replyTokenUsed,
|
|
132
214
|
cfg: params.cfg,
|
|
133
215
|
accountId,
|
|
134
|
-
replyMessageLine:
|
|
135
|
-
pushMessageLine:
|
|
136
|
-
pushTextMessageWithQuickReplies:
|
|
137
|
-
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies
|
|
216
|
+
replyMessageLine: replyVisible,
|
|
217
|
+
pushMessageLine: pushTextVisible,
|
|
218
|
+
pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
|
|
219
|
+
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
|
|
220
|
+
onReplyError: deps.onReplyError
|
|
138
221
|
});
|
|
139
222
|
replyTokenUsed = nextReplyTokenUsed;
|
|
140
223
|
if (!sendRichBeforeText) try {
|
|
141
224
|
await sendLineMessages(richMessages, false);
|
|
142
225
|
if (mediaMessages.length > 0) await sendLineMessages(mediaMessages, false);
|
|
143
226
|
} catch (err) {
|
|
144
|
-
richMediaError
|
|
227
|
+
richMediaError ??= err;
|
|
145
228
|
}
|
|
146
|
-
if (richMediaError !== void 0) return {
|
|
147
|
-
status: "partial",
|
|
148
|
-
replyTokenUsed,
|
|
149
|
-
error: markLineVisibleDeliveryError(richMediaError)
|
|
150
|
-
};
|
|
151
229
|
} else {
|
|
152
230
|
const combined = [...richMessages, ...mediaMessages];
|
|
153
231
|
if (hasQuickReplies && combined.length === 0) {
|
|
@@ -159,9 +237,9 @@ async function deliverLineAutoReply(params) {
|
|
|
159
237
|
replyTokenUsed,
|
|
160
238
|
cfg: params.cfg,
|
|
161
239
|
accountId,
|
|
162
|
-
replyMessageLine:
|
|
163
|
-
pushMessageLine:
|
|
164
|
-
pushTextMessageWithQuickReplies:
|
|
240
|
+
replyMessageLine: replyVisible,
|
|
241
|
+
pushMessageLine: pushTextVisible,
|
|
242
|
+
pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
|
|
165
243
|
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
|
|
166
244
|
onReplyError: deps.onReplyError
|
|
167
245
|
});
|
|
@@ -178,9 +256,19 @@ async function deliverLineAutoReply(params) {
|
|
|
178
256
|
await sendLineMessages(combined, true);
|
|
179
257
|
}
|
|
180
258
|
}
|
|
259
|
+
if (richMediaError !== void 0) {
|
|
260
|
+
if (!visibleReplySent) throw toLineDeliveryError(richMediaError);
|
|
261
|
+
return {
|
|
262
|
+
status: "partial",
|
|
263
|
+
replyTokenUsed,
|
|
264
|
+
visibleReplySent: true,
|
|
265
|
+
error: markLineVisibleDeliveryError(richMediaError)
|
|
266
|
+
};
|
|
267
|
+
}
|
|
181
268
|
return {
|
|
182
269
|
status: "delivered",
|
|
183
|
-
replyTokenUsed
|
|
270
|
+
replyTokenUsed,
|
|
271
|
+
visibleReplySent
|
|
184
272
|
};
|
|
185
273
|
}
|
|
186
274
|
//#endregion
|
|
@@ -436,7 +524,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
436
524
|
};
|
|
437
525
|
}
|
|
438
526
|
async function buildLineMessageContext(params) {
|
|
439
|
-
const { event, allMedia, mediaUnavailable, cfg, account, commandAuthorized,
|
|
527
|
+
const { event, allMedia, mediaUnavailable, cfg, account, commandAuthorized, inboundHistory } = params;
|
|
440
528
|
const source = event.source;
|
|
441
529
|
const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
|
|
442
530
|
source,
|
|
@@ -466,11 +554,6 @@ async function buildLineMessageContext(params) {
|
|
|
466
554
|
address: loc.address
|
|
467
555
|
});
|
|
468
556
|
}
|
|
469
|
-
const historyKey = isGroup ? peerId : void 0;
|
|
470
|
-
const inboundHistory = historyKey && groupHistories && (historyLimit ?? 0) > 0 ? createChannelHistoryWindow({ historyMap: groupHistories }).buildInboundHistory({
|
|
471
|
-
historyKey,
|
|
472
|
-
limit: historyLimit ?? 0
|
|
473
|
-
}) : void 0;
|
|
474
557
|
const finalized = await finalizeLineInboundContext({
|
|
475
558
|
cfg,
|
|
476
559
|
account,
|
|
@@ -571,6 +654,41 @@ async function buildLinePostbackContext(params) {
|
|
|
571
654
|
};
|
|
572
655
|
}
|
|
573
656
|
//#endregion
|
|
657
|
+
//#region extensions/line/src/group-history.ts
|
|
658
|
+
const reservedEntries = /* @__PURE__ */ new WeakSet();
|
|
659
|
+
function reserveLineGroupHistory(historyMap, historyKey, limit) {
|
|
660
|
+
if (!historyMap || !historyKey || limit <= 0) return {
|
|
661
|
+
commit: () => {},
|
|
662
|
+
release: () => {}
|
|
663
|
+
};
|
|
664
|
+
const consumedEntries = (historyMap.get(historyKey) ?? []).filter((entry) => !reservedEntries.has(entry));
|
|
665
|
+
for (const entry of consumedEntries) reservedEntries.add(entry);
|
|
666
|
+
const inboundHistory = createChannelHistoryWindow({ historyMap: /* @__PURE__ */ new Map([[historyKey, consumedEntries]]) }).buildInboundHistory({
|
|
667
|
+
historyKey,
|
|
668
|
+
limit
|
|
669
|
+
});
|
|
670
|
+
let settled = false;
|
|
671
|
+
const settle = () => {
|
|
672
|
+
if (settled) return false;
|
|
673
|
+
settled = true;
|
|
674
|
+
for (const entry of consumedEntries) reservedEntries.delete(entry);
|
|
675
|
+
return true;
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
inboundHistory,
|
|
679
|
+
commit: () => {
|
|
680
|
+
if (!settle() || consumedEntries.length === 0) return;
|
|
681
|
+
const consumed = new Set(consumedEntries);
|
|
682
|
+
const kept = (historyMap.get(historyKey) ?? []).filter((entry) => !consumed.has(entry));
|
|
683
|
+
if (kept.length > 0) historyMap.set(historyKey, kept);
|
|
684
|
+
else historyMap.delete(historyKey);
|
|
685
|
+
},
|
|
686
|
+
release: () => {
|
|
687
|
+
settle();
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
574
692
|
//#region extensions/line/src/bot-handlers.ts
|
|
575
693
|
const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
576
694
|
"image",
|
|
@@ -587,9 +705,12 @@ function normalizeLineIngressEntry(value) {
|
|
|
587
705
|
return normalizeLineAllowEntry(value) || null;
|
|
588
706
|
}
|
|
589
707
|
function createLineWebhookReplayCache() {
|
|
590
|
-
return
|
|
591
|
-
|
|
592
|
-
|
|
708
|
+
return createChannelReplayGuard({
|
|
709
|
+
dedupe: {
|
|
710
|
+
ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
|
|
711
|
+
memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
|
|
712
|
+
},
|
|
713
|
+
buildReplayKey: ({ event, accountId }) => buildLineWebhookReplayKey(event, accountId)?.key
|
|
593
714
|
});
|
|
594
715
|
}
|
|
595
716
|
function buildLineWebhookReplayKey(event, accountId) {
|
|
@@ -609,29 +730,6 @@ function buildLineWebhookReplayKey(event, accountId) {
|
|
|
609
730
|
eventId: `event:${eventId}`
|
|
610
731
|
};
|
|
611
732
|
}
|
|
612
|
-
function getLineReplayCandidate(event, context) {
|
|
613
|
-
const replay = buildLineWebhookReplayKey(event, context.account.accountId);
|
|
614
|
-
const cache = context.replayCache;
|
|
615
|
-
if (!replay || !cache) return null;
|
|
616
|
-
return {
|
|
617
|
-
key: replay.key,
|
|
618
|
-
eventId: replay.eventId,
|
|
619
|
-
cache
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
async function claimLineReplayEvent(candidate) {
|
|
623
|
-
const claim = await candidate.cache.claim(candidate.key);
|
|
624
|
-
if (claim.kind === "claimed") return { skip: false };
|
|
625
|
-
if (claim.kind === "inflight") {
|
|
626
|
-
logVerbose(`line: skipped in-flight replayed webhook event ${candidate.eventId}`);
|
|
627
|
-
return {
|
|
628
|
-
skip: true,
|
|
629
|
-
inFlightResult: claim.pending.then(() => void 0)
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
logVerbose(`line: skipped replayed webhook event ${candidate.eventId}`);
|
|
633
|
-
return { skip: true };
|
|
634
|
-
}
|
|
635
733
|
function resolveLineGroupConfig(params) {
|
|
636
734
|
return resolveLineGroupConfigEntry(params.config.groups, {
|
|
637
735
|
groupId: params.groupId,
|
|
@@ -771,7 +869,7 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
771
869
|
allowFrom: normalizeStringEntries(account.config.allowFrom),
|
|
772
870
|
groupAllowFrom,
|
|
773
871
|
command: {
|
|
774
|
-
hasControlCommand:
|
|
872
|
+
hasControlCommand: hasControlCommand(rawText, cfg),
|
|
775
873
|
groupOwnerAllowFrom: "none"
|
|
776
874
|
}
|
|
777
875
|
});
|
|
@@ -869,42 +967,41 @@ async function handleMessageEvent(event, context) {
|
|
|
869
967
|
});
|
|
870
968
|
return;
|
|
871
969
|
}
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
logVerbose("line: skipping empty message");
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
await processMessage(messageContext);
|
|
902
|
-
if (isGroup && context.groupHistories) {
|
|
903
|
-
const historyKey = groupId ?? roomId;
|
|
904
|
-
if (historyKey && context.groupHistories.has(historyKey)) createChannelHistoryWindow({ historyMap: context.groupHistories }).clear({
|
|
905
|
-
historyKey,
|
|
906
|
-
limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
970
|
+
const groupHistoryKey = isGroup ? groupId ?? roomId : void 0;
|
|
971
|
+
const historyReservation = reserveLineGroupHistory(context.groupHistories, groupHistoryKey, context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT);
|
|
972
|
+
try {
|
|
973
|
+
const allMedia = [];
|
|
974
|
+
let mediaUnavailable = false;
|
|
975
|
+
if (isDownloadableLineMessageType(message.type)) try {
|
|
976
|
+
const originalFilename = message.type === "file" ? normalizeOptionalString(message.fileName) : void 0;
|
|
977
|
+
const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes, { originalFilename });
|
|
978
|
+
allMedia.push({
|
|
979
|
+
path: media.path,
|
|
980
|
+
contentType: media.contentType
|
|
981
|
+
});
|
|
982
|
+
} catch (err) {
|
|
983
|
+
mediaUnavailable = true;
|
|
984
|
+
const errMsg = String(err);
|
|
985
|
+
if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
|
|
986
|
+
else runtime.error?.(danger(`line: failed to download media: ${errMsg}`));
|
|
987
|
+
}
|
|
988
|
+
const messageContext = await buildLineMessageContext({
|
|
989
|
+
event,
|
|
990
|
+
allMedia,
|
|
991
|
+
mediaUnavailable,
|
|
992
|
+
cfg,
|
|
993
|
+
account,
|
|
994
|
+
commandAuthorized: decision.commandAccess.authorized,
|
|
995
|
+
inboundHistory: historyReservation.inboundHistory
|
|
907
996
|
});
|
|
997
|
+
if (!messageContext) {
|
|
998
|
+
logVerbose("line: skipping empty message");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
await processMessage(messageContext);
|
|
1002
|
+
historyReservation.commit();
|
|
1003
|
+
} finally {
|
|
1004
|
+
historyReservation.release();
|
|
908
1005
|
}
|
|
909
1006
|
}
|
|
910
1007
|
async function handleFollowEvent(event, _context) {
|
|
@@ -939,49 +1036,55 @@ async function handlePostbackEvent(event, context) {
|
|
|
939
1036
|
}
|
|
940
1037
|
async function handleLineWebhookEvents(events, context) {
|
|
941
1038
|
let firstError;
|
|
942
|
-
for (const event of events) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1039
|
+
for (const event of events) try {
|
|
1040
|
+
if (!context.replayCache) {
|
|
1041
|
+
await handleLineWebhookEvent(event, context);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
const replayEvent = {
|
|
1045
|
+
event,
|
|
1046
|
+
accountId: context.account.accountId
|
|
1047
|
+
};
|
|
1048
|
+
const result = await context.replayCache.processGuarded(replayEvent, async () => await handleLineWebhookEvent(event, context), { onError: "commit" });
|
|
1049
|
+
const replayId = buildLineWebhookReplayKey(event, context.account.accountId)?.eventId;
|
|
1050
|
+
if (result.kind === "inflight") {
|
|
1051
|
+
logVerbose(`line: skipped in-flight replayed webhook event ${replayId ?? "unknown"}`);
|
|
1052
|
+
try {
|
|
1053
|
+
await result.pending;
|
|
948
1054
|
} catch (err) {
|
|
949
1055
|
context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
|
|
950
1056
|
firstError ??= err;
|
|
951
1057
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
case "message":
|
|
957
|
-
await handleMessageEvent(event, context);
|
|
958
|
-
break;
|
|
959
|
-
case "follow":
|
|
960
|
-
await handleFollowEvent(event, context);
|
|
961
|
-
break;
|
|
962
|
-
case "unfollow":
|
|
963
|
-
await handleUnfollowEvent(event, context);
|
|
964
|
-
break;
|
|
965
|
-
case "join":
|
|
966
|
-
await handleJoinEvent(event, context);
|
|
967
|
-
break;
|
|
968
|
-
case "leave":
|
|
969
|
-
await handleLeaveEvent(event, context);
|
|
970
|
-
break;
|
|
971
|
-
case "postback":
|
|
972
|
-
await handlePostbackEvent(event, context);
|
|
973
|
-
break;
|
|
974
|
-
default: logVerbose(`line: unhandled event type: ${event.type}`);
|
|
975
|
-
}
|
|
976
|
-
if (replayCandidate) await replayCandidate.cache.commit(replayCandidate.key);
|
|
977
|
-
} catch (err) {
|
|
978
|
-
if (replayCandidate) await replayCandidate.cache.commit(replayCandidate.key);
|
|
979
|
-
context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
|
|
980
|
-
firstError ??= err;
|
|
981
|
-
}
|
|
1058
|
+
} else if (result.kind === "duplicate") logVerbose(`line: skipped replayed webhook event ${replayId ?? "unknown"}`);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
|
|
1061
|
+
firstError ??= err;
|
|
982
1062
|
}
|
|
983
1063
|
if (firstError) throw toLintErrorObject(firstError, "Non-Error thrown");
|
|
984
1064
|
}
|
|
1065
|
+
async function handleLineWebhookEvent(event, context) {
|
|
1066
|
+
switch (event.type) {
|
|
1067
|
+
case "message":
|
|
1068
|
+
await handleMessageEvent(event, context);
|
|
1069
|
+
break;
|
|
1070
|
+
case "follow":
|
|
1071
|
+
await handleFollowEvent(event, context);
|
|
1072
|
+
break;
|
|
1073
|
+
case "unfollow":
|
|
1074
|
+
await handleUnfollowEvent(event, context);
|
|
1075
|
+
break;
|
|
1076
|
+
case "join":
|
|
1077
|
+
await handleJoinEvent(event, context);
|
|
1078
|
+
break;
|
|
1079
|
+
case "leave":
|
|
1080
|
+
await handleLeaveEvent(event, context);
|
|
1081
|
+
break;
|
|
1082
|
+
case "postback":
|
|
1083
|
+
await handlePostbackEvent(event, context);
|
|
1084
|
+
break;
|
|
1085
|
+
default: logVerbose(`line: unhandled event type: ${event.type}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
985
1088
|
function toLintErrorObject(value, fallbackMessage) {
|
|
986
1089
|
if (value instanceof Error) return value;
|
|
987
1090
|
if (typeof value === "string") return new Error(value);
|
|
@@ -1042,34 +1145,39 @@ async function sendLineReplyChunks(params) {
|
|
|
1042
1145
|
const quickReplies = params.quickReplies?.length ? params.quickReplies : void 0;
|
|
1043
1146
|
let replyTokenUsed = Boolean(params.replyTokenUsed);
|
|
1044
1147
|
if (params.chunks.length === 0) return { replyTokenUsed };
|
|
1045
|
-
if (params.replyToken && !replyTokenUsed)
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1148
|
+
if (params.replyToken && !replyTokenUsed) {
|
|
1149
|
+
let replySucceeded = false;
|
|
1150
|
+
try {
|
|
1151
|
+
const replyBatch = params.chunks.slice(0, 5);
|
|
1152
|
+
const remaining = params.chunks.slice(replyBatch.length);
|
|
1153
|
+
const replyMessages = replyBatch.map((chunk) => ({
|
|
1154
|
+
type: "text",
|
|
1155
|
+
text: chunk
|
|
1156
|
+
}));
|
|
1157
|
+
if (quickReplies && remaining.length === 0 && replyMessages.length > 0) {
|
|
1158
|
+
const lastIndex = replyMessages.length - 1;
|
|
1159
|
+
replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"), quickReplies);
|
|
1160
|
+
}
|
|
1161
|
+
await params.replyMessageLine(params.replyToken, replyMessages, {
|
|
1162
|
+
cfg: params.cfg,
|
|
1163
|
+
accountId: params.accountId
|
|
1164
|
+
});
|
|
1165
|
+
replyTokenUsed = true;
|
|
1166
|
+
replySucceeded = true;
|
|
1167
|
+
for (const [i, chunk] of remaining.entries()) if (i === remaining.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
|
1168
|
+
cfg: params.cfg,
|
|
1169
|
+
accountId: params.accountId
|
|
1170
|
+
});
|
|
1171
|
+
else await params.pushMessageLine(params.to, chunk, {
|
|
1172
|
+
cfg: params.cfg,
|
|
1173
|
+
accountId: params.accountId
|
|
1174
|
+
});
|
|
1175
|
+
return { replyTokenUsed };
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
if (replySucceeded) throw err;
|
|
1178
|
+
params.onReplyError?.(err);
|
|
1179
|
+
replyTokenUsed = true;
|
|
1055
1180
|
}
|
|
1056
|
-
await params.replyMessageLine(params.replyToken, replyMessages, {
|
|
1057
|
-
cfg: params.cfg,
|
|
1058
|
-
accountId: params.accountId
|
|
1059
|
-
});
|
|
1060
|
-
replyTokenUsed = true;
|
|
1061
|
-
for (const [i, chunk] of remaining.entries()) if (i === remaining.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
|
1062
|
-
cfg: params.cfg,
|
|
1063
|
-
accountId: params.accountId
|
|
1064
|
-
});
|
|
1065
|
-
else await params.pushMessageLine(params.to, chunk, {
|
|
1066
|
-
cfg: params.cfg,
|
|
1067
|
-
accountId: params.accountId
|
|
1068
|
-
});
|
|
1069
|
-
return { replyTokenUsed };
|
|
1070
|
-
} catch (err) {
|
|
1071
|
-
params.onReplyError?.(err);
|
|
1072
|
-
replyTokenUsed = true;
|
|
1073
1181
|
}
|
|
1074
1182
|
for (const [i, chunk] of params.chunks.entries()) if (i === params.chunks.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
|
1075
1183
|
cfg: params.cfg,
|
|
@@ -1172,7 +1280,7 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1172
1280
|
if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
|
|
1173
1281
|
if (body.events && body.events.length > 0) {
|
|
1174
1282
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1175
|
-
|
|
1283
|
+
runDetachedWebhookWork(() => params.bot.handleWebhook(body)).catch((err) => logLineWebhookDispatchError(params.runtime, err));
|
|
1176
1284
|
}
|
|
1177
1285
|
} catch (err) {
|
|
1178
1286
|
await receiveContext?.nack(err);
|
|
@@ -1310,7 +1418,7 @@ async function monitorLineProvider(opts) {
|
|
|
1310
1418
|
createTextMessageWithQuickReplies,
|
|
1311
1419
|
pushMessagesLine,
|
|
1312
1420
|
createFlexMessage,
|
|
1313
|
-
|
|
1421
|
+
buildMediaMessage: buildLineMediaMessage,
|
|
1314
1422
|
createLocationMessage,
|
|
1315
1423
|
onReplyError: (replyErr) => {
|
|
1316
1424
|
logVerbose(`line: reply token failed, falling back to push: ${String(replyErr)}`);
|
|
@@ -1319,6 +1427,7 @@ async function monitorLineProvider(opts) {
|
|
|
1319
1427
|
});
|
|
1320
1428
|
replyTokenUsed = deliveryResult.replyTokenUsed;
|
|
1321
1429
|
if (deliveryResult.status === "partial") throw deliveryResult.error;
|
|
1430
|
+
return { visibleReplySent: deliveryResult.visibleReplySent };
|
|
1322
1431
|
},
|
|
1323
1432
|
onError: (err, info) => {
|
|
1324
1433
|
runtime.error?.(danger(`line ${info.kind} reply failed: ${String(err)}`));
|
|
@@ -1424,7 +1533,7 @@ async function monitorLineProvider(opts) {
|
|
|
1424
1533
|
res.end(JSON.stringify({ status: "ok" }));
|
|
1425
1534
|
if (body.events && body.events.length > 0) {
|
|
1426
1535
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1427
|
-
|
|
1536
|
+
runDetachedWebhookWork(() => match.target.bot.handleWebhook(body)).catch((err) => {
|
|
1428
1537
|
match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
|
|
1429
1538
|
});
|
|
1430
1539
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, D as sendMessageLine, S as pushMessageLine, T as pushTextMessageWithQuickReplies, c as processLineMessage, m as createQuickReplyItems, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage } from "./markdown-to-line-
|
|
1
|
+
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, D as sendMessageLine, S as pushMessageLine, T as pushTextMessageWithQuickReplies, c as processLineMessage, m as createQuickReplyItems, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage } from "./markdown-to-line-XceqeKRP.js";
|
|
2
2
|
export { buildTemplateMessageFromPayload, createQuickReplyItems, processLineMessage, pushFlexMessage, pushLocationMessage, pushMessageLine, pushMessagesLine, pushTemplateMessage, pushTextMessageWithQuickReplies, sendMessageLine };
|