@openclaw/line 2026.7.2-beta.7 → 2026.7.34

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.
Files changed (117) hide show
  1. package/README.md +1 -1
  2. package/dist/.setup/accounts-DJVOv1JI.mjs +107 -0
  3. package/dist/.setup/api-D3h9w6oz.mjs +3 -0
  4. package/dist/.setup/api-DdlGHKoX.mjs +11 -0
  5. package/dist/.setup/basic-cards-C8Qwh2X1.mjs +307 -0
  6. package/dist/{card-command-ty11OJNt.js → .setup/card-command-CWrkUiGY.mjs} +51 -23
  7. package/dist/{channel-D4i_K4fQ.js → .setup/channel-Du0PrvOH.mjs} +76 -66
  8. package/dist/.setup/channel.runtime-b3suPB-W.mjs +4 -0
  9. package/dist/.setup/flex-templates-BP6VHjzK.mjs +814 -0
  10. package/dist/{markdown-to-line-DMDQTMhx.js → .setup/markdown-to-line-o89GT1Qn.mjs} +252 -254
  11. package/dist/{monitor-DXEodYFJ.js → .setup/monitor-DpxjN6Ks.mjs} +491 -658
  12. package/dist/.setup/monitor.runtime-BS-cGD5Q.mjs +2 -0
  13. package/dist/.setup/outbound.runtime-BeNDuEIF.mjs +2 -0
  14. package/dist/{probe-CM03wo6r.js → .setup/probe-BslD77tJ.mjs} +9 -7
  15. package/dist/.setup/probe.runtime-YwWmWIlk.mjs +2 -0
  16. package/dist/{reply-payload-transform-D5-dHB8b.js → .setup/reply-payload-transform-CYQIy_x8.mjs} +204 -80
  17. package/dist/{setup-surface-DRQ7n0En.js → .setup/setup-surface-M1ivmZ-G.mjs} +143 -158
  18. package/dist/api.js +2 -11
  19. package/dist/channel-plugin-api.js +1 -1
  20. package/dist/contract-api.js +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/runtime-api.js +56 -15
  23. package/dist/setup-api.js +1 -1
  24. package/dist/setup-entry.js +2 -1
  25. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
  26. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
  27. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
  28. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
  29. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
  30. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
  31. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
  32. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBubble.d.ts.map +1 -1
  33. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexIconSize.d.ts.map +1 -1
  34. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexSpanSize.d.ts.map +1 -1
  35. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
  36. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
  37. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/postbackAction.d.ts.map +1 -1
  38. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
  39. package/node_modules/@line/bot-sdk/dist/cjs/version.d.ts +1 -1
  40. package/node_modules/@line/bot-sdk/dist/cjs/version.js +1 -1
  41. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts +1 -2
  42. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts.map +1 -1
  43. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts +0 -1
  44. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts.map +1 -1
  45. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js +0 -1
  46. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js.map +1 -1
  47. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
  48. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
  49. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
  50. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
  51. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
  52. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
  53. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
  54. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBubble.d.ts.map +1 -1
  55. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexIconSize.d.ts.map +1 -1
  56. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexSpanSize.d.ts.map +1 -1
  57. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
  58. package/node_modules/@line/bot-sdk/dist/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
  59. package/node_modules/@line/bot-sdk/dist/messaging-api/model/postbackAction.d.ts.map +1 -1
  60. package/node_modules/@line/bot-sdk/dist/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
  61. package/node_modules/@line/bot-sdk/dist/version.d.ts +1 -1
  62. package/node_modules/@line/bot-sdk/dist/version.js +1 -1
  63. package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts +1 -2
  64. package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts.map +1 -1
  65. package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts +0 -1
  66. package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts.map +1 -1
  67. package/node_modules/@line/bot-sdk/dist/webhook/model/models.js +0 -1
  68. package/node_modules/@line/bot-sdk/dist/webhook/model/models.js.map +1 -1
  69. package/node_modules/@line/bot-sdk/lib/insight/model/getNumberOfFollowersResponse.ts +1 -1
  70. package/node_modules/@line/bot-sdk/lib/liff/model/liffScope.ts +1 -1
  71. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupCreateRoute.ts +4 -1
  72. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupFailedType.ts +2 -1
  73. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobFailedType.ts +2 -1
  74. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobStatus.ts +4 -1
  75. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupStatus.ts +6 -1
  76. package/node_modules/@line/bot-sdk/lib/messaging-api/model/botInfoResponse.ts +1 -1
  77. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxBorderWidth.ts +6 -1
  78. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxCornerRadius.ts +7 -1
  79. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBubble.ts +7 -1
  80. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexIconSize.ts +10 -1
  81. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexSpanSize.ts +10 -1
  82. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexTextFontSize.ts +10 -1
  83. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getAggregationUnitNameListResponse.ts +1 -1
  84. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getFollowersResponse.ts +1 -1
  85. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getJoinedMembershipUsersResponse.ts +1 -1
  86. package/node_modules/@line/bot-sdk/lib/messaging-api/model/issueLinkTokenResponse.ts +1 -1
  87. package/node_modules/@line/bot-sdk/lib/messaging-api/model/numberOfMessagesResponse.ts +5 -2
  88. package/node_modules/@line/bot-sdk/lib/messaging-api/model/postbackAction.ts +4 -1
  89. package/node_modules/@line/bot-sdk/lib/messaging-api/model/subscriptionPeriodDemographic.ts +5 -1
  90. package/node_modules/@line/bot-sdk/lib/version.ts +1 -1
  91. package/node_modules/@line/bot-sdk/lib/webhook/.openapi-generator/FILES +0 -1
  92. package/node_modules/@line/bot-sdk/lib/webhook/model/event.ts +0 -2
  93. package/node_modules/@line/bot-sdk/lib/webhook/model/models.ts +0 -1
  94. package/node_modules/@line/bot-sdk/lib/webhook/model/stickerMessageContent.ts +1 -1
  95. package/node_modules/@line/bot-sdk/node_modules/@types/node/README.md +1 -1
  96. package/node_modules/@line/bot-sdk/node_modules/@types/node/fs/promises.d.ts +5 -16
  97. package/node_modules/@line/bot-sdk/node_modules/@types/node/package.json +3 -3
  98. package/node_modules/@line/bot-sdk/package.json +3 -3
  99. package/openclaw.plugin.json +75 -57
  100. package/package.json +6 -71
  101. package/dist/accounts-BqEVsDoB.js +0 -103
  102. package/dist/channel.runtime-BdhK8Xw7.js +0 -4
  103. package/dist/flex-templates-iKJsyCq-.js +0 -1339
  104. package/dist/monitor.runtime-CZquR7jI.js +0 -2
  105. package/dist/outbound.runtime-BS0IhU_M.js +0 -2
  106. package/dist/probe.runtime-D13ZtXsd.js +0 -2
  107. package/dist/quick-reply-fallback-D86FwAfr.js +0 -56
  108. package/dist/send-receipt-CY2ey8XH.js +0 -107
  109. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts +0 -24
  110. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts.map +0 -1
  111. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js +0 -14
  112. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js.map +0 -1
  113. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts +0 -24
  114. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts.map +0 -1
  115. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js +0 -13
  116. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js.map +0 -1
  117. package/node_modules/@line/bot-sdk/lib/webhook/model/messageEditedEvent.ts +0 -32
@@ -1,36 +1,30 @@
1
- import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BqEVsDoB.js";
2
- import { a as resolveLineGroupConfigEntry, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-D86FwAfr.js";
3
- import { n as buildLineMediaMessage, r as hasLineSpecificMediaOptions } from "./send-receipt-CY2ey8XH.js";
4
- import { E as buildTemplateMessageFromPayload, S as replyMessageLine, i as processLineMessage, l as createLocationMessage, p as getUserDisplayName, s as createFlexMessage, u as createQuickReplyItems, v as pushMessageLine, w as showLoadingAnimation, y as pushMessagesLine } from "./markdown-to-line-DMDQTMhx.js";
1
+ import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-DJVOv1JI.mjs";
2
+ import { f as resolveLineGroupConfigEntry, i as buildLineQuickReplyFallbackText, s as getLineRuntime } from "./reply-payload-transform-CYQIy_x8.mjs";
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-o89GT1Qn.mjs";
5
4
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
6
- import { normalizeNullableString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
- import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
5
+ import { normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
6
+ import { createMessageReceiveContext } from "openclaw/plugin-sdk/channel-outbound";
8
7
  import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
9
- import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
10
8
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
11
- import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
12
9
  import { firstDefined } from "openclaw/plugin-sdk/allow-from";
13
- import { setTimeout as setTimeout$1 } from "node:timers/promises";
14
- import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
10
+ import { messagingApi } from "@line/bot-sdk";
15
11
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
16
- import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal, warn } from "openclaw/plugin-sdk/runtime-env";
17
- import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
18
- import { HTTPFetchError } from "@line/bot-sdk";
19
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
12
+ import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
20
13
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
21
- import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
14
+ import { buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
22
15
  import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
23
16
  import { isRequestBodyLimitError, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
24
- import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1, runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
17
+ import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1 } from "openclaw/plugin-sdk/webhook-request-guards";
25
18
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
26
19
  import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
27
20
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
28
- import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
21
+ import { shouldComputeCommandAuthorized } from "openclaw/plugin-sdk/command-auth-native";
29
22
  import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
23
+ import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
30
24
  import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
31
25
  import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
26
+ import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
32
27
  import crypto from "node:crypto";
33
- import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
34
28
  //#region extensions/line/src/bot-access.ts
35
29
  function normalizeLineAllowEntry(value) {
36
30
  const trimmed = String(value).trim();
@@ -48,92 +42,9 @@ const normalizeAllowFrom = (list) => {
48
42
  };
49
43
  //#endregion
50
44
  //#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
- var RetryableLineMediaFetchError = class extends MediaFetchError {
57
- constructor(message, options) {
58
- super("fetch_failed", message, options);
59
- this.name = "RetryableLineMediaFetchError";
60
- }
61
- };
62
- function contentBackoffDelayMs(attempt) {
63
- return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
64
- }
65
- function lineContentUrl(messageId) {
66
- return `${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`;
67
- }
68
- async function* lineResponseBodyChunks(response, messageId) {
69
- const body = response.body;
70
- if (!body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
71
- const reader = body.getReader();
72
- let completed = false;
73
- try {
74
- while (true) {
75
- let chunk;
76
- try {
77
- chunk = await reader.read();
78
- } catch (err) {
79
- throw new RetryableLineMediaFetchError(`LINE media response stream failed for message ${messageId}`, { cause: err });
80
- }
81
- if (chunk.done) {
82
- completed = true;
83
- return;
84
- }
85
- if (chunk.value.byteLength > 0) yield chunk.value;
86
- }
87
- } finally {
88
- if (!completed) await reader.cancel().catch(() => void 0);
89
- try {
90
- reader.releaseLock();
91
- } catch {}
92
- }
93
- }
94
- async function fetchLineContentWhenReady(messageId, channelAccessToken) {
95
- const controller = new AbortController();
96
- const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
97
- deadline.unref();
98
- try {
99
- for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
100
- const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
101
- headers: { Authorization: `Bearer ${channelAccessToken}` },
102
- redirect: "error",
103
- signal: controller.signal
104
- });
105
- if (response.status === 200) {
106
- if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
107
- return response;
108
- }
109
- await response.body?.cancel().catch(() => void 0);
110
- if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
111
- if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
112
- }
113
- } catch (err) {
114
- if (controller.signal.aborted) throw new RetryableLineMediaFetchError(`LINE media for message ${messageId} did not become ready within ${CONTENT_READY_TIMEOUT_MS / 1e3} seconds`, { cause: err });
115
- if (err instanceof MediaFetchError) throw err;
116
- throw new RetryableLineMediaFetchError(`LINE media download failed for message ${messageId}`, { cause: err });
117
- } finally {
118
- clearTimeout(deadline);
119
- }
120
- throw new MediaFetchError("http_error", `LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`, { status: 202 });
121
- }
122
- function isRetryableLineInboundMediaError(err) {
123
- if (err instanceof RetryableLineMediaFetchError) return true;
124
- if (!(err instanceof MediaFetchError)) return false;
125
- if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
126
- return false;
127
- }
128
- async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
129
- const response = await fetchLineContentWhenReady(messageId, channelAccessToken);
130
- let saved;
131
- try {
132
- saved = await saveMediaStream(lineResponseBodyChunks(response, messageId), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
133
- } catch (err) {
134
- await response.body?.cancel().catch(() => void 0);
135
- throw err;
136
- }
45
+ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10485760, options) {
46
+ const response = await new messagingApi.MessagingApiBlobClient({ channelAccessToken }).getMessageContent(messageId);
47
+ const saved = await saveMediaStream(response, void 0, "inbound", maxBytes, options?.originalFilename);
137
48
  logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
138
49
  return {
139
50
  path: saved.path,
@@ -143,23 +54,15 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
143
54
  }
144
55
  //#endregion
145
56
  //#region extensions/line/src/auto-reply-delivery.ts
146
- function toLineDeliveryError(error) {
147
- return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
148
- }
149
- function getLineHttpError(error) {
150
- if (error instanceof HTTPFetchError) return error;
151
- return error instanceof Error && error.cause instanceof HTTPFetchError ? error.cause : void 0;
152
- }
153
57
  function markLineVisibleDeliveryError(error) {
154
- const deliveryError = toLineDeliveryError(error);
155
- if (Object.isExtensible(deliveryError)) {
156
- Object.assign(deliveryError, {
58
+ if (error instanceof Error && Object.isExtensible(error)) {
59
+ Object.assign(error, {
157
60
  sentBeforeError: true,
158
61
  visibleReplySent: true
159
62
  });
160
- return deliveryError;
63
+ return error;
161
64
  }
162
- const visibleError = new Error("LINE message send failed", { cause: deliveryError });
65
+ const visibleError = new Error("LINE rich or media message send failed", { cause: error });
163
66
  Object.assign(visibleError, {
164
67
  sentBeforeError: true,
165
68
  visibleReplySent: true
@@ -169,37 +72,12 @@ function markLineVisibleDeliveryError(error) {
169
72
  async function deliverLineAutoReply(params) {
170
73
  const { payload, lineData, replyToken, accountId, to, textLimit, deps } = params;
171
74
  let replyTokenUsed = params.replyTokenUsed;
172
- let visibleReplySent = false;
173
- const sendVisible = async (send) => {
174
- try {
175
- const result = await send();
176
- visibleReplySent = true;
177
- return result;
178
- } catch (error) {
179
- if (visibleReplySent) throw markLineVisibleDeliveryError(error);
180
- throw error;
181
- }
182
- };
183
- const replyVisible = (...args) => sendVisible(() => deps.replyMessageLine(...args));
184
- const failedPushSegments = /* @__PURE__ */ new WeakMap();
185
- const pushLineMessages = async (messages, allowFailedBatchTextRecovery, externalTail = []) => {
75
+ const pushLineMessages = async (messages) => {
186
76
  if (messages.length === 0) return;
187
- for (let i = 0; i < messages.length; i += 5) {
188
- const batch = messages.slice(i, i + 5);
189
- try {
190
- await sendVisible(() => deps.pushMessagesLine(to, batch, {
191
- cfg: params.cfg,
192
- accountId
193
- }));
194
- } catch (error) {
195
- if (typeof error === "object" && error !== null) failedPushSegments.set(error, {
196
- allowFailedBatchTextRecovery,
197
- failedBatch: batch,
198
- unattemptedTail: [...messages.slice(i + batch.length), ...externalTail]
199
- });
200
- throw error;
201
- }
202
- }
77
+ for (let i = 0; i < messages.length; i += 5) await deps.pushMessagesLine(to, messages.slice(i, i + 5), {
78
+ cfg: params.cfg,
79
+ accountId
80
+ });
203
81
  };
204
82
  const sendLineMessages = async (messages, allowReplyToken) => {
205
83
  if (messages.length === 0) return;
@@ -207,19 +85,18 @@ async function deliverLineAutoReply(params) {
207
85
  if (allowReplyToken && replyToken && !replyTokenUsed) {
208
86
  const replyBatch = remaining.slice(0, 5);
209
87
  try {
210
- await replyVisible(replyToken, replyBatch, {
88
+ await deps.replyMessageLine(replyToken, replyBatch, {
211
89
  cfg: params.cfg,
212
90
  accountId
213
91
  });
214
92
  } catch (err) {
215
93
  deps.onReplyError?.(err);
216
- await pushLineMessages(replyBatch, getLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
217
- } finally {
218
- replyTokenUsed = true;
94
+ await pushLineMessages(replyBatch);
219
95
  }
96
+ replyTokenUsed = true;
220
97
  remaining = remaining.slice(replyBatch.length);
221
98
  }
222
- if (remaining.length > 0) await pushLineMessages(remaining, true);
99
+ if (remaining.length > 0) await pushLineMessages(remaining);
223
100
  };
224
101
  const richMessages = [];
225
102
  const hasQuickReplies = Boolean(lineData.quickReplies?.length);
@@ -229,80 +106,80 @@ async function deliverLineAutoReply(params) {
229
106
  if (templateMsg) richMessages.push(templateMsg);
230
107
  }
231
108
  if (lineData.location) richMessages.push(deps.createLocationMessage(lineData.location));
232
- const visibleText = payload.text ? sanitizeAssistantVisibleText(payload.text) : "";
233
- const processed = visibleText ? deps.processLineMessage(visibleText) : {
109
+ const processed = payload.text ? deps.processLineMessage(payload.text) : {
234
110
  text: "",
235
111
  flexMessages: []
236
112
  };
237
113
  for (const flexMsg of processed.flexMessages) richMessages.push(deps.createFlexMessage(truncateUtf16Safe(flexMsg.altText, 400), flexMsg.contents));
238
114
  const chunks = processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : [];
239
- const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
240
- const mediaOpts = {
241
- mediaKind: hasLineSpecificMediaOptions(lineData) ? lineData.mediaKind : "image",
242
- previewImageUrl: lineData.previewImageUrl,
243
- durationMs: lineData.durationMs,
244
- trackingId: lineData.trackingId
245
- };
246
- const mediaMessages = [];
247
- let deliveryError;
248
- for (const rawUrl of mediaUrls) {
249
- const url = rawUrl?.trim();
250
- if (!url) continue;
251
- try {
252
- mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
115
+ const mediaMessages = resolveSendableOutboundReplyParts(payload).mediaUrls.map((url) => url?.trim()).filter((url) => Boolean(url)).map((url) => deps.createImageMessage(url));
116
+ if (chunks.length > 0) {
117
+ const hasRichOrMedia = richMessages.length > 0 || mediaMessages.length > 0;
118
+ const sendRichBeforeText = hasQuickReplies && hasRichOrMedia;
119
+ let richMediaError;
120
+ if (sendRichBeforeText) try {
121
+ await sendLineMessages([...richMessages, ...mediaMessages], false);
253
122
  } catch (err) {
254
- deliveryError ??= err;
123
+ richMediaError = err;
255
124
  }
256
- }
257
- const textMessages = chunks.map((text) => ({
258
- type: "text",
259
- text
260
- }));
261
- const richMediaMessages = [...richMessages, ...mediaMessages];
262
- if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
263
- type: "text",
264
- text: buildLineQuickReplyFallbackText(lineData.quickReplies)
265
- });
266
- if (hasQuickReplies) {
267
- const targetMessages = textMessages.length > 0 ? textMessages : richMediaMessages;
268
- const lastIndex = targetMessages.length - 1;
269
- targetMessages[lastIndex] = {
270
- ...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
271
- quickReply: deps.createQuickReplyItems(lineData.quickReplies)
272
- };
273
- }
274
- const messages = hasQuickReplies ? [...richMediaMessages, ...textMessages] : [...textMessages, ...richMediaMessages];
275
- try {
276
- await sendLineMessages(messages, true);
277
- } catch (err) {
278
- deliveryError ??= err;
279
- const failedSegment = typeof err === "object" && err !== null ? failedPushSegments.get(err) : void 0;
280
- const httpError = getLineHttpError(err);
281
- const retryCandidates = failedSegment ? [...failedSegment.allowFailedBatchTextRecovery ? failedSegment.failedBatch : [], ...failedSegment.unattemptedTail] : [];
282
- const retryTextMessages = retryCandidates.filter((message) => message.type === "text");
283
- const quickRepliesNeedCarrier = hasQuickReplies && retryCandidates.some((message) => "quickReply" in message);
284
- const retryMessages = retryTextMessages.length > 0 ? retryTextMessages : quickRepliesNeedCarrier ? [{
285
- type: "text",
286
- text: buildLineQuickReplyFallbackText(lineData.quickReplies),
287
- quickReply: deps.createQuickReplyItems(lineData.quickReplies)
288
- }] : [];
289
- if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400) try {
290
- await sendLineMessages(retryMessages, false);
291
- } catch {}
292
- }
293
- if (deliveryError !== void 0) {
294
- if (!visibleReplySent) throw toLineDeliveryError(deliveryError);
295
- return {
125
+ const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
126
+ to,
127
+ chunks,
128
+ quickReplies: lineData.quickReplies,
129
+ replyToken,
130
+ replyTokenUsed,
131
+ cfg: params.cfg,
132
+ accountId,
133
+ replyMessageLine: deps.replyMessageLine,
134
+ pushMessageLine: deps.pushMessageLine,
135
+ pushTextMessageWithQuickReplies: deps.pushTextMessageWithQuickReplies,
136
+ createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies
137
+ });
138
+ replyTokenUsed = nextReplyTokenUsed;
139
+ if (!sendRichBeforeText) try {
140
+ await sendLineMessages(richMessages, false);
141
+ if (mediaMessages.length > 0) await sendLineMessages(mediaMessages, false);
142
+ } catch (err) {
143
+ richMediaError = err;
144
+ }
145
+ if (richMediaError !== void 0) return {
296
146
  status: "partial",
297
147
  replyTokenUsed,
298
- visibleReplySent: true,
299
- error: markLineVisibleDeliveryError(deliveryError)
148
+ error: markLineVisibleDeliveryError(richMediaError)
300
149
  };
150
+ } else {
151
+ const combined = [...richMessages, ...mediaMessages];
152
+ if (hasQuickReplies && combined.length === 0) {
153
+ const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
154
+ to,
155
+ chunks: [buildLineQuickReplyFallbackText(lineData.quickReplies)],
156
+ quickReplies: lineData.quickReplies,
157
+ replyToken,
158
+ replyTokenUsed,
159
+ cfg: params.cfg,
160
+ accountId,
161
+ replyMessageLine: deps.replyMessageLine,
162
+ pushMessageLine: deps.pushMessageLine,
163
+ pushTextMessageWithQuickReplies: deps.pushTextMessageWithQuickReplies,
164
+ createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
165
+ onReplyError: deps.onReplyError
166
+ });
167
+ replyTokenUsed = nextReplyTokenUsed;
168
+ } else {
169
+ if (hasQuickReplies && combined.length > 0) {
170
+ const quickReply = deps.createQuickReplyItems(lineData.quickReplies);
171
+ const targetIndex = replyToken && !replyTokenUsed ? Math.min(4, combined.length - 1) : combined.length - 1;
172
+ combined[targetIndex] = {
173
+ ...combined[targetIndex],
174
+ quickReply
175
+ };
176
+ }
177
+ await sendLineMessages(combined, true);
178
+ }
301
179
  }
302
180
  return {
303
181
  status: "delivered",
304
- replyTokenUsed,
305
- visibleReplySent
182
+ replyTokenUsed
306
183
  };
307
184
  }
308
185
  //#endregion
@@ -432,27 +309,48 @@ function extractMessageText(message) {
432
309
  }
433
310
  return "";
434
311
  }
435
- function extractNativeMediaKind(message) {
312
+ function extractMediaPlaceholder(message) {
436
313
  switch (message.type) {
437
- case "image": return "image";
438
- case "video": return "video";
439
- case "audio": return "audio";
440
- case "file": return "document";
441
- default: return;
314
+ case "image": return "<media:image>";
315
+ case "video": return "<media:video>";
316
+ case "audio": return "<media:audio>";
317
+ case "file": return "<media:document>";
318
+ default: return "";
442
319
  }
443
320
  }
321
+ function resolveLineConversationLabel(params) {
322
+ return params.isGroup ? params.groupId ? `group:${params.groupId}` : params.roomId ? `room:${params.roomId}` : "unknown-group" : params.senderLabel;
323
+ }
324
+ function resolveLineAddresses(params) {
325
+ const fromAddress = params.isGroup ? params.groupId ? `line:group:${params.groupId}` : params.roomId ? `line:room:${params.roomId}` : `line:${params.peerId}` : `line:${params.userId ?? params.peerId}`;
326
+ return {
327
+ fromAddress,
328
+ toAddress: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`,
329
+ originatingTo: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`
330
+ };
331
+ }
444
332
  async function finalizeLineInboundContext(params) {
333
+ const { fromAddress, toAddress, originatingTo } = resolveLineAddresses({
334
+ isGroup: params.source.isGroup,
335
+ groupId: params.source.groupId,
336
+ roomId: params.source.roomId,
337
+ userId: params.source.userId,
338
+ peerId: params.source.peerId
339
+ });
445
340
  const senderId = params.source.userId ?? "unknown";
446
341
  const senderLabel = params.source.userId ? `user:${params.source.userId}` : "unknown";
447
- const conversationLabel = params.source.isGroup ? params.source.groupId ? `group:${params.source.groupId}` : params.source.roomId ? `room:${params.source.roomId}` : "unknown-group" : senderLabel;
448
- const address = params.source.groupId ? `line:group:${params.source.groupId}` : params.source.roomId ? `line:room:${params.source.roomId}` : `line:${params.source.userId ?? params.source.peerId}`;
342
+ const conversationLabel = resolveLineConversationLabel({
343
+ isGroup: params.source.isGroup,
344
+ groupId: params.source.groupId,
345
+ roomId: params.source.roomId,
346
+ senderLabel
347
+ });
449
348
  const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
450
349
  cfg: params.cfg,
451
350
  agentId: params.route.agentId,
452
351
  sessionKey: params.route.sessionKey
453
352
  });
454
353
  const agentBody = params.agentBody ?? params.rawBody;
455
- const media = await toInboundMediaFactsWithMetadata(params.media);
456
354
  const body = formatInboundEnvelope({
457
355
  channel: "LINE",
458
356
  from: conversationLabel,
@@ -463,45 +361,38 @@ async function finalizeLineInboundContext(params) {
463
361
  previousTimestamp,
464
362
  envelope: envelopeOptions
465
363
  });
466
- const ctxPayload = buildChannelInboundEventContext({
467
- channel: "line",
468
- accountId: params.route.accountId,
469
- messageId: params.messageSid,
470
- timestamp: params.timestamp,
471
- from: address,
472
- sender: { id: senderId },
473
- conversation: {
474
- kind: params.source.isGroup ? "group" : "direct",
475
- id: params.source.peerId,
476
- label: conversationLabel
477
- },
478
- route: {
479
- agentId: params.route.agentId,
480
- dmScope: params.route.dmScope,
481
- accountId: params.route.accountId,
482
- routeSessionKey: params.route.sessionKey
483
- },
484
- reply: {
485
- to: address,
486
- originatingTo: address
487
- },
488
- message: {
489
- body,
490
- bodyForAgent: agentBody,
491
- rawBody: params.rawBody,
492
- commandBody: params.rawBody,
493
- inboundHistory: params.inboundHistory
494
- },
495
- access: { commands: { authorized: params.commandAuthorized } },
496
- media,
497
- extra: {
498
- ...params.locationContext,
499
- GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
500
- GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
501
- groupId: params.source.groupId,
502
- roomId: params.source.roomId
503
- })?.systemPrompt) : void 0
504
- }
364
+ const ctxPayload = finalizeInboundContext({
365
+ Body: body,
366
+ BodyForAgent: agentBody,
367
+ RawBody: params.rawBody,
368
+ CommandBody: params.rawBody,
369
+ From: fromAddress,
370
+ To: toAddress,
371
+ SessionKey: params.route.sessionKey,
372
+ AccountId: params.route.accountId,
373
+ ChatType: params.source.isGroup ? "group" : "direct",
374
+ ConversationLabel: conversationLabel,
375
+ GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
376
+ SenderId: senderId,
377
+ Provider: "line",
378
+ Surface: "line",
379
+ MessageSid: params.messageSid,
380
+ Timestamp: params.timestamp,
381
+ MediaPath: params.media.firstPath,
382
+ MediaType: params.media.firstContentType,
383
+ MediaUrl: params.media.firstPath,
384
+ MediaPaths: params.media.paths,
385
+ MediaUrls: params.media.paths,
386
+ MediaTypes: params.media.types,
387
+ ...params.locationContext,
388
+ CommandAuthorized: params.commandAuthorized,
389
+ OriginatingChannel: "line",
390
+ OriginatingTo: originatingTo,
391
+ GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
392
+ groupId: params.source.groupId,
393
+ roomId: params.source.roomId
394
+ })?.systemPrompt) : void 0,
395
+ InboundHistory: params.inboundHistory
505
396
  });
506
397
  const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
507
398
  dmScope: params.cfg.session?.dmScope,
@@ -515,7 +406,8 @@ async function finalizeLineInboundContext(params) {
515
406
  if (shouldLogVerbose()) {
516
407
  const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
517
408
  const mediaInfo = params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1 ? ` mediaCount=${params.verboseLog.mediaCount}` : "";
518
- logVerbose(`${params.verboseLog.kind === "inbound" ? "line inbound" : "line postback"}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
409
+ const label = params.verboseLog.kind === "inbound" ? "line inbound" : "line postback";
410
+ logVerbose(`${label}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
519
411
  }
520
412
  return {
521
413
  ctxPayload,
@@ -544,7 +436,7 @@ async function finalizeLineInboundContext(params) {
544
436
  };
545
437
  }
546
438
  async function buildLineMessageContext(params) {
547
- const { event, allMedia, mediaUnavailable, cfg, account, commandAuthorized, inboundHistory } = params;
439
+ const { event, allMedia, mediaUnavailable, cfg, account, commandAuthorized, groupHistories, historyLimit } = params;
548
440
  const source = event.source;
549
441
  const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
550
442
  source,
@@ -555,17 +447,15 @@ async function buildLineMessageContext(params) {
555
447
  const messageId = message.id;
556
448
  const timestamp = event.timestamp;
557
449
  const textContent = extractMessageText(message);
558
- const nativeMediaKind = extractNativeMediaKind(message);
559
- const mediaFacts = allMedia.length > 0 ? allMedia.map((media) => ({
560
- ...media,
561
- kind: nativeMediaKind
562
- })) : nativeMediaKind ? [{ kind: nativeMediaKind }] : [];
563
- const rawBody = textContent;
450
+ const placeholder = extractMediaPlaceholder(message);
451
+ let rawBody = textContent || placeholder;
452
+ if (!rawBody && allMedia.length > 0) rawBody = `<media:image>${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`;
564
453
  const agentBody = mediaUnavailable ? formatInboundMediaUnavailableText({
565
454
  body: rawBody,
455
+ mediaPlaceholder: placeholder,
566
456
  notice: "[line attachment unavailable]"
567
457
  }) : rawBody;
568
- if (!agentBody && mediaFacts.length === 0) return null;
458
+ if (!agentBody && allMedia.length === 0) return null;
569
459
  let locationContext;
570
460
  if (message.type === "location") {
571
461
  const loc = message;
@@ -576,6 +466,11 @@ async function buildLineMessageContext(params) {
576
466
  address: loc.address
577
467
  });
578
468
  }
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;
579
474
  const finalized = await finalizeLineInboundContext({
580
475
  cfg,
581
476
  account,
@@ -593,7 +488,12 @@ async function buildLineMessageContext(params) {
593
488
  timestamp,
594
489
  messageSid: messageId,
595
490
  commandAuthorized,
596
- media: mediaFacts,
491
+ media: {
492
+ firstPath: allMedia[0]?.path,
493
+ firstContentType: allMedia[0]?.contentType,
494
+ paths: allMedia.length > 0 ? allMedia.map((m) => m.path) : void 0,
495
+ types: allMedia.length > 0 ? allMedia.map((m) => m.contentType).filter(Boolean) : void 0
496
+ },
597
497
  locationContext,
598
498
  verboseLog: {
599
499
  kind: "inbound",
@@ -623,14 +523,18 @@ async function buildLinePostbackContext(params) {
623
523
  account
624
524
  });
625
525
  const timestamp = event.timestamp;
626
- const rawData = event.postback?.data?.trim() ?? "";
627
- if (!rawData) return null;
628
- let rawBody = rawData;
629
- if (rawData.includes("line.action=")) {
630
- const searchParams = new URLSearchParams(rawData);
526
+ const rawBody = event.postback?.data?.trim() ?? "";
527
+ if (!rawBody) return null;
528
+ let agentBody = rawBody;
529
+ if (rawBody.includes("line.action=")) {
530
+ const searchParams = new URLSearchParams(rawBody);
631
531
  const action = searchParams.get("line.action") ?? "";
632
532
  const device = searchParams.get("line.device");
633
- rawBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
533
+ agentBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
534
+ }
535
+ for (const [key, value] of Object.entries(event.postback.params ?? {}).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
536
+ const picked = normalizeOptionalString(value);
537
+ if (picked) agentBody += ` ${key}=${picked}`;
634
538
  }
635
539
  const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
636
540
  const finalized = await finalizeLineInboundContext({
@@ -646,10 +550,16 @@ async function buildLinePostbackContext(params) {
646
550
  peerId
647
551
  },
648
552
  rawBody,
553
+ agentBody,
649
554
  timestamp,
650
555
  messageSid,
651
556
  commandAuthorized,
652
- media: [],
557
+ media: {
558
+ firstPath: "",
559
+ firstContentType: void 0,
560
+ paths: void 0,
561
+ types: void 0
562
+ },
653
563
  verboseLog: { kind: "postback" }
654
564
  });
655
565
  return {
@@ -666,41 +576,6 @@ async function buildLinePostbackContext(params) {
666
576
  };
667
577
  }
668
578
  //#endregion
669
- //#region extensions/line/src/group-history.ts
670
- const reservedEntries = /* @__PURE__ */ new WeakSet();
671
- function reserveLineGroupHistory(historyMap, historyKey, limit) {
672
- if (!historyMap || !historyKey || limit <= 0) return {
673
- commit: () => {},
674
- release: () => {}
675
- };
676
- const consumedEntries = (historyMap.get(historyKey) ?? []).filter((entry) => !reservedEntries.has(entry));
677
- for (const entry of consumedEntries) reservedEntries.add(entry);
678
- const inboundHistory = createChannelHistoryWindow({ historyMap: /* @__PURE__ */ new Map([[historyKey, consumedEntries]]) }).buildInboundHistory({
679
- historyKey,
680
- limit
681
- });
682
- let settled = false;
683
- const settle = () => {
684
- if (settled) return false;
685
- settled = true;
686
- for (const entry of consumedEntries) reservedEntries.delete(entry);
687
- return true;
688
- };
689
- return {
690
- inboundHistory,
691
- commit: () => {
692
- if (!settle() || consumedEntries.length === 0) return;
693
- const consumed = new Set(consumedEntries);
694
- const kept = (historyMap.get(historyKey) ?? []).filter((entry) => !consumed.has(entry));
695
- if (kept.length > 0) historyMap.set(historyKey, kept);
696
- else historyMap.delete(historyKey);
697
- },
698
- release: () => {
699
- settle();
700
- }
701
- };
702
- }
703
- //#endregion
704
579
  //#region extensions/line/src/bot-handlers.ts
705
580
  const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
706
581
  "image",
@@ -711,9 +586,63 @@ const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
711
586
  function isDownloadableLineMessageType(messageType) {
712
587
  return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
713
588
  }
589
+ const LINE_WEBHOOK_REPLAY_WINDOW_MS = 6e5;
590
+ const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
714
591
  function normalizeLineIngressEntry(value) {
715
592
  return normalizeLineAllowEntry(value) || null;
716
593
  }
594
+ var LineRetryableWebhookError = class extends Error {
595
+ constructor(message, options) {
596
+ super(message, options);
597
+ this.name = "LineRetryableWebhookError";
598
+ }
599
+ };
600
+ function createLineWebhookReplayCache() {
601
+ return createClaimableDedupe({
602
+ ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
603
+ memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
604
+ });
605
+ }
606
+ function buildLineWebhookReplayKey(event, accountId) {
607
+ if (event.type === "message") {
608
+ const messageId = event.message?.id?.trim();
609
+ if (messageId) return {
610
+ key: `${accountId}|message:${messageId}`,
611
+ eventId: `message:${messageId}`
612
+ };
613
+ }
614
+ const eventId = event.webhookEventId?.trim();
615
+ if (!eventId) return null;
616
+ const source = event.source;
617
+ const sourceId = source?.type === "group" ? `group:${source.groupId ?? ""}` : source?.type === "room" ? `room:${source.roomId ?? ""}` : `user:${source?.userId ?? ""}`;
618
+ return {
619
+ key: `${accountId}|${event.type}|${sourceId}|${eventId}`,
620
+ eventId: `event:${eventId}`
621
+ };
622
+ }
623
+ function getLineReplayCandidate(event, context) {
624
+ const replay = buildLineWebhookReplayKey(event, context.account.accountId);
625
+ const cache = context.replayCache;
626
+ if (!replay || !cache) return null;
627
+ return {
628
+ key: replay.key,
629
+ eventId: replay.eventId,
630
+ cache
631
+ };
632
+ }
633
+ async function claimLineReplayEvent(candidate) {
634
+ const claim = await candidate.cache.claim(candidate.key);
635
+ if (claim.kind === "claimed") return { skip: false };
636
+ if (claim.kind === "inflight") {
637
+ logVerbose(`line: skipped in-flight replayed webhook event ${candidate.eventId}`);
638
+ return {
639
+ skip: true,
640
+ inFlightResult: claim.pending.then(() => void 0)
641
+ };
642
+ }
643
+ logVerbose(`line: skipped replayed webhook event ${candidate.eventId}`);
644
+ return { skip: true };
645
+ }
717
646
  function resolveLineGroupConfig(params) {
718
647
  return resolveLineGroupConfigEntry(params.config.groups, {
719
648
  groupId: params.groupId,
@@ -788,7 +717,7 @@ async function shouldProcessLineEvent(event, context) {
788
717
  defaultGroupPolicy: resolveDefaultGroupPolicy(cfg)
789
718
  });
790
719
  const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
791
- const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom));
720
+ const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom, account.config.allowFrom?.length ? account.config.allowFrom : void 0));
792
721
  const mentionFacts = (() => {
793
722
  if (!isGroup || event.type !== "message") return {
794
723
  canDetectMention: false,
@@ -853,7 +782,7 @@ async function shouldProcessLineEvent(event, context) {
853
782
  allowFrom: normalizeStringEntries(account.config.allowFrom),
854
783
  groupAllowFrom,
855
784
  command: {
856
- hasControlCommand: hasControlCommand(rawText, cfg),
785
+ hasControlCommand: shouldComputeCommandAuthorized(rawText, cfg),
857
786
  groupOwnerAllowFrom: "none"
858
787
  }
859
788
  });
@@ -951,42 +880,42 @@ async function handleMessageEvent(event, context) {
951
880
  });
952
881
  return;
953
882
  }
954
- const groupHistoryKey = isGroup ? groupId ?? roomId : void 0;
955
- const historyReservation = reserveLineGroupHistory(context.groupHistories, groupHistoryKey, context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT);
956
- try {
957
- const allMedia = [];
958
- let mediaUnavailable = false;
959
- if (isDownloadableLineMessageType(message.type)) try {
960
- const originalFilename = message.type === "file" ? normalizeOptionalString(message.fileName) : void 0;
961
- const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes, { originalFilename });
962
- allMedia.push({
963
- path: media.path,
964
- contentType: media.contentType
965
- });
966
- } catch (err) {
967
- if (isRetryableLineInboundMediaError(err)) throw err;
968
- mediaUnavailable = true;
969
- const errMsg = String(err);
970
- if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
971
- else runtime.error?.(danger(`line: failed to download media: ${errMsg}`));
972
- }
973
- const messageContext = await buildLineMessageContext({
974
- event,
975
- allMedia,
976
- mediaUnavailable,
977
- cfg,
978
- account,
979
- commandAuthorized: decision.commandAccess.authorized,
980
- inboundHistory: historyReservation.inboundHistory
883
+ const allMedia = [];
884
+ let mediaUnavailable = false;
885
+ if (isDownloadableLineMessageType(message.type)) try {
886
+ const originalFilename = message.type === "file" ? normalizeOptionalString(message.fileName) : void 0;
887
+ const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes, { originalFilename });
888
+ allMedia.push({
889
+ path: media.path,
890
+ contentType: media.contentType
891
+ });
892
+ } catch (err) {
893
+ mediaUnavailable = true;
894
+ const errMsg = String(err);
895
+ if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
896
+ else runtime.error?.(danger(`line: failed to download media: ${errMsg}`));
897
+ }
898
+ const messageContext = await buildLineMessageContext({
899
+ event,
900
+ allMedia,
901
+ mediaUnavailable,
902
+ cfg,
903
+ account,
904
+ commandAuthorized: decision.commandAccess.authorized,
905
+ groupHistories: context.groupHistories,
906
+ historyLimit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
907
+ });
908
+ if (!messageContext) {
909
+ logVerbose("line: skipping empty message");
910
+ return;
911
+ }
912
+ await processMessage(messageContext);
913
+ if (isGroup && context.groupHistories) {
914
+ const historyKey = groupId ?? roomId;
915
+ if (historyKey && context.groupHistories.has(historyKey)) createChannelHistoryWindow({ historyMap: context.groupHistories }).clear({
916
+ historyKey,
917
+ limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
981
918
  });
982
- if (!messageContext) {
983
- logVerbose("line: skipping empty message");
984
- return;
985
- }
986
- await processMessage(messageContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
987
- historyReservation.commit();
988
- } finally {
989
- historyReservation.release();
990
919
  }
991
920
  }
992
921
  async function handleFollowEvent(event, _context) {
@@ -1017,41 +946,56 @@ async function handlePostbackEvent(event, context) {
1017
946
  commandAuthorized: decision.commandAccess.authorized
1018
947
  });
1019
948
  if (!postbackContext) return;
1020
- await context.processMessage(postbackContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
949
+ await context.processMessage(postbackContext);
1021
950
  }
1022
951
  async function handleLineWebhookEvents(events, context) {
1023
952
  let firstError;
1024
- for (const event of events) try {
1025
- await handleLineWebhookEvent(event, context);
1026
- } catch (err) {
1027
- context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
1028
- firstError ??= err;
953
+ for (const event of events) {
954
+ const replayCandidate = getLineReplayCandidate(event, context);
955
+ const replaySkip = replayCandidate ? await claimLineReplayEvent(replayCandidate) : null;
956
+ if (replaySkip?.skip) {
957
+ if (replaySkip.inFlightResult) try {
958
+ await replaySkip.inFlightResult;
959
+ } catch (err) {
960
+ context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
961
+ firstError ??= err;
962
+ }
963
+ continue;
964
+ }
965
+ try {
966
+ switch (event.type) {
967
+ case "message":
968
+ await handleMessageEvent(event, context);
969
+ break;
970
+ case "follow":
971
+ await handleFollowEvent(event, context);
972
+ break;
973
+ case "unfollow":
974
+ await handleUnfollowEvent(event, context);
975
+ break;
976
+ case "join":
977
+ await handleJoinEvent(event, context);
978
+ break;
979
+ case "leave":
980
+ await handleLeaveEvent(event, context);
981
+ break;
982
+ case "postback":
983
+ await handlePostbackEvent(event, context);
984
+ break;
985
+ default: logVerbose(`line: unhandled event type: ${event.type}`);
986
+ }
987
+ if (replayCandidate) await replayCandidate.cache.commit(replayCandidate.key);
988
+ } catch (err) {
989
+ if (replayCandidate) {
990
+ if (err instanceof LineRetryableWebhookError) replayCandidate.cache.release(replayCandidate.key, { error: err });
991
+ else await replayCandidate.cache.commit(replayCandidate.key);
992
+ }
993
+ context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
994
+ firstError ??= err;
995
+ }
1029
996
  }
1030
997
  if (firstError) throw toLintErrorObject(firstError, "Non-Error thrown");
1031
998
  }
1032
- async function handleLineWebhookEvent(event, context) {
1033
- switch (event.type) {
1034
- case "message":
1035
- await handleMessageEvent(event, context);
1036
- break;
1037
- case "follow":
1038
- await handleFollowEvent(event, context);
1039
- break;
1040
- case "unfollow":
1041
- await handleUnfollowEvent(event, context);
1042
- break;
1043
- case "join":
1044
- await handleJoinEvent(event, context);
1045
- break;
1046
- case "leave":
1047
- await handleLeaveEvent(event, context);
1048
- break;
1049
- case "postback":
1050
- await handlePostbackEvent(event, context);
1051
- break;
1052
- default: logVerbose(`line: unhandled event type: ${event.type}`);
1053
- }
1054
- }
1055
999
  function toLintErrorObject(value, fallbackMessage) {
1056
1000
  if (value instanceof Error) return value;
1057
1001
  if (typeof value === "string") return new Error(value);
@@ -1060,224 +1004,6 @@ function toLintErrorObject(value, fallbackMessage) {
1060
1004
  return error;
1061
1005
  }
1062
1006
  //#endregion
1063
- //#region extensions/line/src/webhook-spool.ts
1064
- const LINE_WEBHOOK_SPOOL_VERSION = 1;
1065
- const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
1066
- const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
1067
- const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
1068
- const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
1069
- const LineWebhookPayloadError = createChannelIngressError("LineWebhookPayloadError");
1070
- var LineWebhookTerminalDeliveryError = class extends Error {
1071
- constructor(message, options) {
1072
- super(message, options);
1073
- this.reason = "delivery-side-effects-committed";
1074
- this.name = "LineWebhookTerminalDeliveryError";
1075
- }
1076
- };
1077
- /** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
1078
- function eventIdFor(event) {
1079
- if (!event || typeof event !== "object") throw new LineWebhookPayloadError("LINE webhook event must be an object.");
1080
- const candidate = event;
1081
- if (candidate.type === "message") {
1082
- const messageId = normalizeNullableString(candidate.message?.id);
1083
- if (messageId) return `message:${messageId}`;
1084
- }
1085
- const webhookEventId = normalizeNullableString(candidate.webhookEventId);
1086
- if (webhookEventId) return `event:${webhookEventId}`;
1087
- throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
1088
- }
1089
- function laneKeyFor(event, eventId) {
1090
- if (!event || typeof event !== "object") return eventId;
1091
- const source = event.source;
1092
- if (source?.type === "group") {
1093
- const groupId = normalizeNullableString(source.groupId);
1094
- if (groupId) return `group:${groupId}`;
1095
- }
1096
- if (source?.type === "room") {
1097
- const roomId = normalizeNullableString(source.roomId);
1098
- if (roomId) return `room:${roomId}`;
1099
- }
1100
- if (source?.type === "user") {
1101
- const userId = normalizeNullableString(source.userId);
1102
- if (userId) return `user:${userId}`;
1103
- }
1104
- return eventId;
1105
- }
1106
- function parseStoredEvent(rawEvent) {
1107
- let event;
1108
- try {
1109
- event = JSON.parse(rawEvent);
1110
- } catch (error) {
1111
- throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
1112
- }
1113
- return event;
1114
- }
1115
- function errorText(error) {
1116
- return error instanceof Error ? error.message : String(error);
1117
- }
1118
- function isLineAuthenticationFailure(error) {
1119
- if (!error || typeof error !== "object") return false;
1120
- const status = error.status;
1121
- return status === 401 || status === 403;
1122
- }
1123
- async function waitForActiveDeliveriesBeforeDispose(activeDeliveries) {
1124
- let timeout;
1125
- try {
1126
- return await Promise.race([Promise.allSettled(activeDeliveries).then(() => true), new Promise((resolve) => {
1127
- timeout = setTimeout(() => resolve(false), LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS);
1128
- timeout.unref?.();
1129
- })]);
1130
- } finally {
1131
- if (timeout) clearTimeout(timeout);
1132
- }
1133
- }
1134
- function createLineWebhookSpool(options) {
1135
- const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
1136
- const activeDeliveries = /* @__PURE__ */ new Set();
1137
- let acceptsDeferredClaims = true;
1138
- const monitor = createChannelIngressMonitor({
1139
- queue,
1140
- inspect: ({ event }) => {
1141
- const eventId = eventIdFor(event);
1142
- return {
1143
- eventId,
1144
- laneKey: laneKeyFor(event, eventId)
1145
- };
1146
- },
1147
- payload: {
1148
- version: LINE_WEBHOOK_SPOOL_VERSION,
1149
- serialize: ({ event, destination }) => ({
1150
- rawEvent: JSON.stringify(event),
1151
- destination
1152
- }),
1153
- deserialize: ({ rawEvent, destination }) => ({
1154
- event: parseStoredEvent(rawEvent),
1155
- destination
1156
- }),
1157
- encode: ({ version, body }) => ({
1158
- version,
1159
- rawEvent: body.rawEvent,
1160
- destination: body.destination
1161
- }),
1162
- decode: (payload) => {
1163
- if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
1164
- return {
1165
- version: payload.version,
1166
- body: {
1167
- rawEvent: payload.rawEvent,
1168
- destination: payload.destination
1169
- }
1170
- };
1171
- },
1172
- createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? "LINE webhook spool payload is invalid." : "LINE webhook event identity changed after durable admission.")
1173
- },
1174
- deliver: async ({ event, destination }, lifecycle) => {
1175
- const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
1176
- let handedOff = false;
1177
- const delivery = options.deliver(event, destination, { turnAdoptionLifecycle: {
1178
- ...boundLifecycle,
1179
- onAdopted: async () => {
1180
- handedOff = true;
1181
- await boundLifecycle.onAdopted();
1182
- },
1183
- onDeferred: () => {
1184
- handedOff = true;
1185
- if (!acceptsDeferredClaims) {
1186
- Promise.resolve().then(() => boundLifecycle.onAbandoned()).catch((error) => {
1187
- options.runtime.error?.(danger(`line: failed to abandon a late webhook delivery: ${errorText(error)}`));
1188
- });
1189
- return;
1190
- }
1191
- boundLifecycle.onDeferred();
1192
- },
1193
- onAbandoned: async () => {
1194
- handedOff = true;
1195
- await boundLifecycle.onAbandoned();
1196
- }
1197
- } });
1198
- activeDeliveries.add(delivery);
1199
- try {
1200
- await delivery;
1201
- } finally {
1202
- activeDeliveries.delete(delivery);
1203
- }
1204
- if (stopTask && !handedOff) return {
1205
- kind: "failed-retryable",
1206
- error: /* @__PURE__ */ new Error("LINE webhook spool stopped before delivery handoff.")
1207
- };
1208
- },
1209
- pollIntervalMs: LINE_WEBHOOK_DRAIN_INTERVAL_MS,
1210
- retention: {
1211
- pruneIntervalMs: 0,
1212
- completedMaxEntries: 4096,
1213
- failedMaxEntries: 4096
1214
- },
1215
- appendRetryDelaysMs: [0],
1216
- waitForDeliveryIdleBeforeRepump: false,
1217
- waitForDeliveryIdleOnStop: false,
1218
- deferredClaims: "manual",
1219
- runPumpTask: runDetachedWebhookWork,
1220
- admissionMode: "durable-after-stop",
1221
- drain: {
1222
- adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
1223
- orderBy: "received",
1224
- scanLimit: LINE_WEBHOOK_DRAIN_SCAN_LIMIT,
1225
- startLimit: LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
1226
- retryPolicy: {
1227
- maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
1228
- deadLetterMinAgeMs: 0
1229
- },
1230
- resolveNonRetryableFailure: (error) => {
1231
- if (error instanceof LineWebhookPayloadError) return {
1232
- reason: "invalid-event",
1233
- message: error.message
1234
- };
1235
- if (error instanceof LineWebhookTerminalDeliveryError) return {
1236
- reason: error.reason,
1237
- message: error.message
1238
- };
1239
- if (isLineAuthenticationFailure(error)) return {
1240
- reason: "authentication-failed",
1241
- message: errorText(error)
1242
- };
1243
- return null;
1244
- },
1245
- onLog: (message) => options.runtime.error?.(danger(`line: ${message}`))
1246
- },
1247
- createStoppedError: () => /* @__PURE__ */ new Error("LINE webhook spool is stopped."),
1248
- onError: (error) => options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`))
1249
- });
1250
- let stopTask;
1251
- return {
1252
- accept: async (body) => {
1253
- const events = body.events ?? [];
1254
- if (events.length === 0) return;
1255
- await monitor.admitBatch(events.map((event) => ({
1256
- event,
1257
- destination: body.destination ?? ""
1258
- })), { receivedAt: Date.now() });
1259
- },
1260
- start: () => {
1261
- if (!stopTask) monitor.start();
1262
- },
1263
- stop: () => {
1264
- stopTask ??= (async () => {
1265
- await monitor.pause();
1266
- try {
1267
- const deliveriesSettled = await waitForActiveDeliveriesBeforeDispose(activeDeliveries);
1268
- if (!deliveriesSettled) options.runtime.log(warn(`line: timed out after ${LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS}ms waiting for active webhook deliveries; releasing drain ownership`));
1269
- await monitor.waitForDeferredClaims();
1270
- acceptsDeferredClaims = false;
1271
- if (deliveriesSettled) await monitor.waitForIdle();
1272
- } finally {
1273
- await monitor.stop();
1274
- }
1275
- })();
1276
- return stopTask;
1277
- }
1278
- };
1279
- }
1280
- //#endregion
1281
1007
  //#region extensions/line/src/bot.ts
1282
1008
  function createLineBot(opts) {
1283
1009
  const runtime = opts.runtime ?? createNonExitingRuntime();
@@ -1290,26 +1016,24 @@ function createLineBot(opts) {
1290
1016
  const processMessage = opts.onMessage ?? (async () => {
1291
1017
  logVerbose("line: no message handler configured");
1292
1018
  });
1019
+ const replayCache = createLineWebhookReplayCache();
1293
1020
  const groupHistories = /* @__PURE__ */ new Map();
1294
- const spool = createLineWebhookSpool({
1295
- accountId: account.accountId,
1296
- runtime,
1297
- deliver: async (event, _destination, control) => await handleLineWebhookEvents([event], {
1021
+ const handleWebhook = async (body) => {
1022
+ if (!body.events || body.events.length === 0) return;
1023
+ await handleLineWebhookEvents(body.events, {
1298
1024
  cfg,
1299
1025
  account,
1300
1026
  runtime,
1301
1027
  mediaMaxBytes,
1302
1028
  processMessage,
1303
- ...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
1029
+ replayCache,
1304
1030
  groupHistories,
1305
1031
  historyLimit: cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
1306
- })
1307
- });
1308
- spool.start();
1032
+ });
1033
+ };
1309
1034
  return {
1310
- handleWebhook: spool.accept,
1311
- account,
1312
- stop: spool.stop
1035
+ handleWebhook,
1036
+ account
1313
1037
  };
1314
1038
  }
1315
1039
  //#endregion
@@ -1327,9 +1051,63 @@ function resolveLineDurableReplyOptions(params) {
1327
1051
  return { to: params.to };
1328
1052
  }
1329
1053
  //#endregion
1054
+ //#region extensions/line/src/reply-chunks.ts
1055
+ async function sendLineReplyChunks(params) {
1056
+ const hasQuickReplies = Boolean(params.quickReplies?.length);
1057
+ let replyTokenUsed = Boolean(params.replyTokenUsed);
1058
+ if (params.chunks.length === 0) return { replyTokenUsed };
1059
+ if (params.replyToken && !replyTokenUsed) try {
1060
+ const replyBatch = params.chunks.slice(0, 5);
1061
+ const remaining = params.chunks.slice(replyBatch.length);
1062
+ const replyMessages = replyBatch.map((chunk) => ({
1063
+ type: "text",
1064
+ text: chunk
1065
+ }));
1066
+ if (hasQuickReplies && remaining.length === 0 && replyMessages.length > 0) {
1067
+ const lastIndex = replyMessages.length - 1;
1068
+ replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(replyBatch[lastIndex], params.quickReplies);
1069
+ }
1070
+ await params.replyMessageLine(params.replyToken, replyMessages, {
1071
+ cfg: params.cfg,
1072
+ accountId: params.accountId
1073
+ });
1074
+ replyTokenUsed = true;
1075
+ for (let i = 0; i < remaining.length; i += 1) if (i === remaining.length - 1 && hasQuickReplies) await params.pushTextMessageWithQuickReplies(params.to, remaining[i], params.quickReplies, {
1076
+ cfg: params.cfg,
1077
+ accountId: params.accountId
1078
+ });
1079
+ else await params.pushMessageLine(params.to, remaining[i], {
1080
+ cfg: params.cfg,
1081
+ accountId: params.accountId
1082
+ });
1083
+ return { replyTokenUsed };
1084
+ } catch (err) {
1085
+ params.onReplyError?.(err);
1086
+ replyTokenUsed = true;
1087
+ }
1088
+ for (let i = 0; i < params.chunks.length; i += 1) if (i === params.chunks.length - 1 && hasQuickReplies) await params.pushTextMessageWithQuickReplies(params.to, params.chunks[i], params.quickReplies, {
1089
+ cfg: params.cfg,
1090
+ accountId: params.accountId
1091
+ });
1092
+ else await params.pushMessageLine(params.to, params.chunks[i], {
1093
+ cfg: params.cfg,
1094
+ accountId: params.accountId
1095
+ });
1096
+ return { replyTokenUsed };
1097
+ }
1098
+ //#endregion
1330
1099
  //#region extensions/line/src/signature.ts
1331
1100
  function validateLineSignature(body, signature, channelSecret) {
1332
- return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
1101
+ const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
1102
+ const hashBuffer = Buffer.from(hash);
1103
+ const signatureBuffer = Buffer.from(signature);
1104
+ const maxLen = Math.max(hashBuffer.length, signatureBuffer.length);
1105
+ const paddedHash = Buffer.alloc(maxLen);
1106
+ const paddedSig = Buffer.alloc(maxLen);
1107
+ hashBuffer.copy(paddedHash);
1108
+ signatureBuffer.copy(paddedSig);
1109
+ const timingResult = crypto.timingSafeEqual(paddedHash, paddedSig);
1110
+ return hashBuffer.length === signatureBuffer.length && timingResult;
1333
1111
  }
1334
1112
  //#endregion
1335
1113
  //#region extensions/line/src/webhook-utils.ts
@@ -1342,8 +1120,8 @@ function parseLineWebhookBody(rawBody) {
1342
1120
  }
1343
1121
  //#endregion
1344
1122
  //#region extensions/line/src/webhook-node.ts
1345
- const LINE_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
1346
- const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 64 * 1024;
1123
+ const LINE_WEBHOOK_MAX_BODY_BYTES = 1048576;
1124
+ const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 65536;
1347
1125
  const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1 = 5e3;
1348
1126
  async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1) {
1349
1127
  return await readRequestBodyWithLimit(req, {
@@ -1351,6 +1129,9 @@ async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_
1351
1129
  timeoutMs
1352
1130
  });
1353
1131
  }
1132
+ function logLineWebhookDispatchError(runtime, err) {
1133
+ runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
1134
+ }
1354
1135
  function createLineNodeWebhookHandler(params) {
1355
1136
  const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
1356
1137
  const readBody = params.readBody ?? readLineWebhookRequestBody;
@@ -1373,6 +1154,7 @@ function createLineNodeWebhookHandler(params) {
1373
1154
  res.end(JSON.stringify({ error: "Method Not Allowed" }));
1374
1155
  return;
1375
1156
  }
1157
+ let receiveContext;
1376
1158
  try {
1377
1159
  const signatureHeader = req.headers["x-line-signature"];
1378
1160
  const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
@@ -1399,14 +1181,24 @@ function createLineNodeWebhookHandler(params) {
1399
1181
  return;
1400
1182
  }
1401
1183
  params.onRequestAuthenticated?.();
1184
+ receiveContext = createMessageReceiveContext({
1185
+ id: `${Date.now()}:line:webhook`,
1186
+ channel: "line",
1187
+ message: body,
1188
+ ackPolicy: "after_receive_record",
1189
+ onAck: () => {
1190
+ res.statusCode = 200;
1191
+ res.setHeader("Content-Type", "application/json");
1192
+ res.end(JSON.stringify({ status: "ok" }));
1193
+ }
1194
+ });
1195
+ if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
1402
1196
  if (body.events && body.events.length > 0) {
1403
1197
  logVerbose(`line: received ${body.events.length} webhook events`);
1404
- await params.bot.handleWebhook(body);
1198
+ Promise.resolve().then(() => params.bot.handleWebhook(body)).catch((err) => logLineWebhookDispatchError(params.runtime, err));
1405
1199
  }
1406
- res.statusCode = 200;
1407
- res.setHeader("Content-Type", "application/json");
1408
- res.end(JSON.stringify({ status: "ok" }));
1409
1200
  } catch (err) {
1201
+ await receiveContext?.nack(err);
1410
1202
  if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
1411
1203
  res.statusCode = 413;
1412
1204
  res.setHeader("Content-Type", "application/json");
@@ -1419,7 +1211,7 @@ function createLineNodeWebhookHandler(params) {
1419
1211
  res.end(JSON.stringify({ error: requestBodyErrorToText$1("REQUEST_BODY_TIMEOUT") }));
1420
1212
  return;
1421
1213
  }
1422
- params.runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
1214
+ params.runtime.error?.(danger(`line webhook error: ${String(err)}`));
1423
1215
  if (!res.headersSent) {
1424
1216
  res.statusCode = 500;
1425
1217
  res.setHeader("Content-Type", "application/json");
@@ -1430,10 +1222,24 @@ function createLineNodeWebhookHandler(params) {
1430
1222
  }
1431
1223
  //#endregion
1432
1224
  //#region extensions/line/src/monitor.ts
1225
+ const runtimeState = /* @__PURE__ */ new Map();
1433
1226
  const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
1434
- const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 64 * 1024;
1227
+ const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
1435
1228
  const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
1436
1229
  const lineWebhookTargets = /* @__PURE__ */ new Map();
1230
+ function recordChannelRuntimeState(params) {
1231
+ const key = `${params.channel}:${params.accountId}`;
1232
+ const existing = runtimeState.get(key) ?? {
1233
+ running: false,
1234
+ lastStartAt: null,
1235
+ lastStopAt: null,
1236
+ lastError: null
1237
+ };
1238
+ runtimeState.set(key, {
1239
+ ...existing,
1240
+ ...params.state
1241
+ });
1242
+ }
1437
1243
  function startLineLoadingKeepalive(params) {
1438
1244
  const intervalMs = params.intervalMs ?? 18e3;
1439
1245
  const loadingSeconds = params.loadingSeconds ?? 20;
@@ -1467,9 +1273,14 @@ async function monitorLineProvider(opts) {
1467
1273
  accountId,
1468
1274
  runtime,
1469
1275
  config,
1470
- onMessage: async (ctx, deliveryControl) => {
1276
+ onMessage: async (ctx) => {
1471
1277
  if (!ctx) return;
1472
1278
  const { ctxPayload, replyToken, route } = ctx;
1279
+ recordChannelRuntimeState({
1280
+ channel: "line",
1281
+ accountId: resolvedAccountId,
1282
+ state: { lastInboundAt: Date.now() }
1283
+ });
1473
1284
  const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
1474
1285
  const displayNamePromise = ctx.userId ? getUserDisplayName(ctx.userId, {
1475
1286
  cfg: config,
@@ -1480,24 +1291,16 @@ async function monitorLineProvider(opts) {
1480
1291
  userId: ctx.userId,
1481
1292
  accountId: ctx.accountId
1482
1293
  }) : null;
1483
- logVerbose(`line: received message from ${await displayNamePromise} (${ctxPayload.From})`);
1484
- let replyTokenUsed = false;
1485
- let turnAdopted = false;
1486
- const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
1294
+ const displayName = await displayNamePromise;
1295
+ logVerbose(`line: received message from ${displayName} (${ctxPayload.From})`);
1487
1296
  try {
1488
1297
  const textLimit = 5e3;
1489
- const turnResult = await getLineRuntime().channel.inbound.run({
1298
+ let replyTokenUsed = false;
1299
+ const core = getLineRuntime();
1300
+ const turnResult = await core.channel.inbound.run({
1490
1301
  channel: "line",
1491
1302
  accountId: route.accountId,
1492
1303
  raw: ctx,
1493
- turnAdoptionLifecycle: {
1494
- ...ingressLifecycle,
1495
- admission: "exclusive",
1496
- onAdopted: async () => {
1497
- await ingressLifecycle?.onAdopted();
1498
- turnAdopted = true;
1499
- }
1500
- },
1501
1304
  adapter: {
1502
1305
  ingest: () => ({
1503
1306
  id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
@@ -1507,14 +1310,14 @@ async function monitorLineProvider(opts) {
1507
1310
  cfg: config,
1508
1311
  channel: "line",
1509
1312
  accountId: route.accountId,
1510
- route: {
1511
- agentId: route.agentId,
1512
- sessionKey: route.sessionKey
1513
- },
1313
+ agentId: route.agentId,
1314
+ routeSessionKey: route.sessionKey,
1315
+ storePath: ctx.turn.storePath,
1514
1316
  ctxPayload,
1317
+ recordInboundSession: core.channel.session.recordInboundSession,
1318
+ dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
1515
1319
  record: ctx.turn.record,
1516
1320
  replyPipeline: {},
1517
- ...ingressLifecycle?.abortSignal ? { replyOptions: { abortSignal: ingressLifecycle.abortSignal } } : {},
1518
1321
  delivery: {
1519
1322
  durable: (payload, info) => resolveLineDurableReplyOptions({
1520
1323
  payload,
@@ -1542,11 +1345,15 @@ async function monitorLineProvider(opts) {
1542
1345
  buildTemplateMessageFromPayload,
1543
1346
  processLineMessage,
1544
1347
  chunkMarkdownText,
1348
+ sendLineReplyChunks,
1545
1349
  replyMessageLine,
1350
+ pushMessageLine,
1351
+ pushTextMessageWithQuickReplies,
1546
1352
  createQuickReplyItems,
1353
+ createTextMessageWithQuickReplies,
1547
1354
  pushMessagesLine,
1548
1355
  createFlexMessage,
1549
- buildMediaMessage: buildLineMediaMessage,
1356
+ createImageMessage,
1550
1357
  createLocationMessage,
1551
1358
  onReplyError: (replyErr) => {
1552
1359
  logVerbose(`line: reply token failed, falling back to push: ${String(replyErr)}`);
@@ -1555,7 +1362,11 @@ async function monitorLineProvider(opts) {
1555
1362
  });
1556
1363
  replyTokenUsed = deliveryResult.replyTokenUsed;
1557
1364
  if (deliveryResult.status === "partial") throw deliveryResult.error;
1558
- return { visibleReplySent: deliveryResult.visibleReplySent };
1365
+ recordChannelRuntimeState({
1366
+ channel: "line",
1367
+ accountId: resolvedAccountId,
1368
+ state: { lastOutboundAt: Date.now() }
1369
+ });
1559
1370
  },
1560
1371
  onError: (err, info) => {
1561
1372
  runtime.error?.(danger(`line ${info.kind} reply failed: ${String(err)}`));
@@ -1564,11 +1375,21 @@ async function monitorLineProvider(opts) {
1564
1375
  })
1565
1376
  }
1566
1377
  });
1567
- if (!hasFinalInboundReplyDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1378
+ const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
1379
+ if (!hasFinalInboundReplyDispatch(dispatchResult)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1568
1380
  } catch (err) {
1569
1381
  runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
1570
- if (turnAdopted || replyTokenUsed) throw new LineWebhookTerminalDeliveryError("LINE delivery failed after consuming the event reply token.", { cause: err });
1571
- throw err;
1382
+ if (replyToken) try {
1383
+ await replyMessageLine(replyToken, [{
1384
+ type: "text",
1385
+ text: "Sorry, I encountered an error processing your message."
1386
+ }], {
1387
+ cfg: config,
1388
+ accountId: ctx.accountId
1389
+ });
1390
+ } catch (replyErr) {
1391
+ runtime.error?.(danger(`line: error reply failed: ${String(replyErr)}`));
1392
+ }
1572
1393
  } finally {
1573
1394
  stopLoading?.();
1574
1395
  }
@@ -1646,14 +1467,16 @@ async function monitorLineProvider(opts) {
1646
1467
  res.end(JSON.stringify({ error: "Invalid webhook payload" }));
1647
1468
  return;
1648
1469
  }
1649
- if (body.events && body.events.length > 0) {
1650
- logVerbose(`line: received ${body.events.length} webhook events`);
1651
- await match.target.bot.handleWebhook(body);
1652
- res.setHeader("x-openclaw-delivery-accepted", "durable");
1653
- }
1470
+ requestLifecycle.release();
1654
1471
  res.statusCode = 200;
1655
1472
  res.setHeader("Content-Type", "application/json");
1656
1473
  res.end(JSON.stringify({ status: "ok" }));
1474
+ if (body.events && body.events.length > 0) {
1475
+ logVerbose(`line: received ${body.events.length} webhook events`);
1476
+ Promise.resolve().then(() => match.target.bot.handleWebhook(body)).catch((err) => {
1477
+ match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
1478
+ });
1479
+ }
1657
1480
  } catch (err) {
1658
1481
  if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1659
1482
  res.statusCode = 413;
@@ -1667,7 +1490,7 @@ async function monitorLineProvider(opts) {
1667
1490
  res.end(JSON.stringify({ error: requestBodyErrorToText("REQUEST_BODY_TIMEOUT") }));
1668
1491
  return;
1669
1492
  }
1670
- runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
1493
+ runtime.error?.(danger(`line webhook error: ${String(err)}`));
1671
1494
  if (!res.headersSent) {
1672
1495
  res.statusCode = 500;
1673
1496
  res.setHeader("Content-Type", "application/json");
@@ -1679,31 +1502,41 @@ async function monitorLineProvider(opts) {
1679
1502
  }
1680
1503
  }
1681
1504
  });
1505
+ recordChannelRuntimeState({
1506
+ channel: "line",
1507
+ accountId: resolvedAccountId,
1508
+ state: {
1509
+ running: true,
1510
+ lastStartAt: Date.now()
1511
+ }
1512
+ });
1682
1513
  logVerbose(`line: registered webhook handler at ${normalizedPath}`);
1683
1514
  let stopped = false;
1684
- let stopPromise;
1685
1515
  const stopHandler = () => {
1686
- if (stopPromise) return stopPromise;
1687
- if (stopped) return Promise.resolve();
1516
+ if (stopped) return;
1688
1517
  stopped = true;
1689
1518
  logVerbose(`line: stopping provider for account ${resolvedAccountId}`);
1690
1519
  unregisterHttp();
1691
- stopPromise = bot.stop();
1692
- return stopPromise;
1520
+ recordChannelRuntimeState({
1521
+ channel: "line",
1522
+ accountId: resolvedAccountId,
1523
+ state: {
1524
+ running: false,
1525
+ lastStopAt: Date.now()
1526
+ }
1527
+ });
1693
1528
  };
1694
- const stopOnAbort = () => void stopHandler();
1695
- if (abortSignal?.aborted) await stopHandler();
1529
+ if (abortSignal?.aborted) stopHandler();
1696
1530
  else if (abortSignal) {
1697
- abortSignal.addEventListener("abort", stopOnAbort, { once: true });
1531
+ abortSignal.addEventListener("abort", stopHandler, { once: true });
1698
1532
  await waitForAbortSignal(abortSignal);
1699
- await stopHandler();
1700
1533
  }
1701
1534
  return {
1702
1535
  account: bot.account,
1703
1536
  handleWebhook: bot.handleWebhook,
1704
- stop: async () => {
1705
- await stopHandler();
1706
- abortSignal?.removeEventListener("abort", stopOnAbort);
1537
+ stop: () => {
1538
+ stopHandler();
1539
+ abortSignal?.removeEventListener("abort", stopHandler);
1707
1540
  }
1708
1541
  };
1709
1542
  }