@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.
@@ -1,27 +1,31 @@
1
1
  import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-DJVOv1JI.js";
2
- import { f as resolveLineGroupConfigEntry, i as buildLineQuickReplyFallbackText, s as getLineRuntime } from "./reply-payload-transform-BsbwCJGH.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, f as createImageMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-CixmAKiG.js";
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 { messagingApi } from "@line/bot-sdk";
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 { shouldComputeCommandAuthorized } from "openclaw/plugin-sdk/command-auth-native";
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 { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
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 saved = await saveMediaStream(await new messagingApi.MessagingApiBlobClient({ channelAccessToken }).getMessageContent(messageId), void 0, "inbound", maxBytes, options?.originalFilename);
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
- if (error instanceof Error && Object.isExtensible(error)) {
60
- Object.assign(error, {
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 error;
115
+ return deliveryError;
65
116
  }
66
- const visibleError = new Error("LINE rich or media message send failed", { cause: error });
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 deps.replyMessageLine(replyToken, replyBatch, {
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 processed = payload.text ? deps.processLineMessage(payload.text) : {
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 mediaMessages = resolveSendableOutboundReplyParts(payload).mediaUrls.map((url) => url?.trim()).filter((url) => Boolean(url)).map((url) => deps.createImageMessage(url));
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 = err;
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: deps.replyMessageLine,
135
- pushMessageLine: deps.pushMessageLine,
136
- pushTextMessageWithQuickReplies: deps.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 = err;
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: deps.replyMessageLine,
163
- pushMessageLine: deps.pushMessageLine,
164
- pushTextMessageWithQuickReplies: deps.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, groupHistories, historyLimit } = params;
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 createClaimableDedupe({
591
- ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
592
- memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
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: shouldComputeCommandAuthorized(rawText, cfg),
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 allMedia = [];
873
- let mediaUnavailable = false;
874
- if (isDownloadableLineMessageType(message.type)) try {
875
- const originalFilename = message.type === "file" ? normalizeOptionalString(message.fileName) : void 0;
876
- const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes, { originalFilename });
877
- allMedia.push({
878
- path: media.path,
879
- contentType: media.contentType
880
- });
881
- } catch (err) {
882
- mediaUnavailable = true;
883
- const errMsg = String(err);
884
- if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
885
- else runtime.error?.(danger(`line: failed to download media: ${errMsg}`));
886
- }
887
- const messageContext = await buildLineMessageContext({
888
- event,
889
- allMedia,
890
- mediaUnavailable,
891
- cfg,
892
- account,
893
- commandAuthorized: decision.commandAccess.authorized,
894
- groupHistories: context.groupHistories,
895
- historyLimit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
896
- });
897
- if (!messageContext) {
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
- const replayCandidate = getLineReplayCandidate(event, context);
944
- const replaySkip = replayCandidate ? await claimLineReplayEvent(replayCandidate) : null;
945
- if (replaySkip?.skip) {
946
- if (replaySkip.inFlightResult) try {
947
- await replaySkip.inFlightResult;
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
- continue;
953
- }
954
- try {
955
- switch (event.type) {
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) try {
1046
- const replyBatch = params.chunks.slice(0, 5);
1047
- const remaining = params.chunks.slice(replyBatch.length);
1048
- const replyMessages = replyBatch.map((chunk) => ({
1049
- type: "text",
1050
- text: chunk
1051
- }));
1052
- if (quickReplies && remaining.length === 0 && replyMessages.length > 0) {
1053
- const lastIndex = replyMessages.length - 1;
1054
- replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"), quickReplies);
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
- Promise.resolve().then(() => params.bot.handleWebhook(body)).catch((err) => logLineWebhookDispatchError(params.runtime, err));
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
- createImageMessage,
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
- Promise.resolve().then(() => match.target.bot.handleWebhook(body)).catch((err) => {
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
  }
@@ -0,0 +1,2 @@
1
+ import { t as monitorLineProvider } from "./monitor-Db2rZsSX.js";
2
+ export { monitorLineProvider };
@@ -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-CixmAKiG.js";
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 };