@openclaw/line 2026.8.1-beta.3 → 2026.8.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.
Files changed (156) hide show
  1. package/dist/account-helpers-BSUyZwLm.js +12 -0
  2. package/dist/{accounts-BqEVsDoB.js → accounts-BBEFMYbY.js} +2 -1
  3. package/dist/api.js +2 -2
  4. package/dist/card-command-DS6A5V6o.js +252 -0
  5. package/dist/{channel-DmoKZXmG.js → channel-CLCy4jAa.js} +97 -357
  6. package/dist/channel-plugin-api.js +1 -1
  7. package/dist/channel.runtime-Ih7sq1GN.js +4 -0
  8. package/dist/contract-api.js +1 -1
  9. package/dist/doctor-contract-api.js +141 -0
  10. package/dist/index.js +5 -17
  11. package/dist/{markdown-to-line-rEaras_0.js → markdown-to-line--jytmFan.js} +36 -29
  12. package/dist/{monitor-BYXS_B6r.js → monitor-CSpwQCBI.js} +292 -194
  13. package/dist/monitor.runtime-CV8EjkIT.js +2 -0
  14. package/dist/outbound.runtime-DLqcz_BQ.js +3 -0
  15. package/dist/probe.runtime-DTxBWvKz.js +2 -0
  16. package/dist/rich-messages-CIP7xn5E.js +761 -0
  17. package/dist/runtime-api.js +12 -13
  18. package/dist/{send-receipt-DKZzgHMO.js → send-retry-CUvrarfE.js} +137 -413
  19. package/dist/{send-BJCeiiOB.js → send-wVUV00WS.js} +150 -109
  20. package/dist/setup-api.js +1 -1
  21. package/dist/{setup-surface-W0Dbgu58.js → setup-surface-CDlgkTKi.js} +47 -39
  22. package/dist/webhook-spool-contract-1rY8OJw8.js +59 -0
  23. package/node_modules/typebox/build/format/date.d.mts +1 -1
  24. package/node_modules/typebox/build/format/date.mjs +1 -1
  25. package/node_modules/typebox/build/format/date_time.d.mts +2 -2
  26. package/node_modules/typebox/build/format/date_time.mjs +3 -3
  27. package/node_modules/typebox/build/format/email.d.mts +1 -1
  28. package/node_modules/typebox/build/format/email.mjs +2 -2
  29. package/node_modules/typebox/build/format/hostname.mjs +2 -10
  30. package/node_modules/typebox/build/format/idn_email.d.mts +1 -1
  31. package/node_modules/typebox/build/format/idn_email.mjs +3 -3
  32. package/node_modules/typebox/build/format/idn_hostname.d.mts +1 -0
  33. package/node_modules/typebox/build/format/idn_hostname.mjs +3 -11
  34. package/node_modules/typebox/build/format/idna/format/bidi.d.mts +3 -0
  35. package/node_modules/typebox/build/format/idna/format/bidi.mjs +98 -0
  36. package/node_modules/typebox/build/format/idna/format/puny.d.mts +3 -0
  37. package/node_modules/typebox/build/format/idna/format/puny.mjs +144 -0
  38. package/node_modules/typebox/build/format/idna/hostname.d.mts +1 -0
  39. package/node_modules/typebox/build/format/idna/hostname.mjs +16 -0
  40. package/node_modules/typebox/build/format/idna/idn-hostname.d.mts +1 -0
  41. package/node_modules/typebox/build/format/idna/idn-hostname.mjs +41 -0
  42. package/node_modules/typebox/build/format/idna/index.d.mts +2 -0
  43. package/node_modules/typebox/build/format/idna/index.mjs +2 -0
  44. package/node_modules/typebox/build/format/idna/label/ascii.d.mts +1 -0
  45. package/node_modules/typebox/build/format/idna/label/ascii.mjs +9 -0
  46. package/node_modules/typebox/build/format/idna/label/puny.d.mts +1 -0
  47. package/node_modules/typebox/build/format/idna/label/puny.mjs +28 -0
  48. package/node_modules/typebox/build/format/idna/label/unicode.d.mts +1 -0
  49. package/node_modules/typebox/build/format/idna/label/unicode.mjs +106 -0
  50. package/node_modules/typebox/build/format/idna/pattern/pattern.d.mts +30 -0
  51. package/node_modules/typebox/build/format/idna/pattern/pattern.mjs +70 -0
  52. package/node_modules/typebox/build/format/ipv4.d.mts +0 -1
  53. package/node_modules/typebox/build/format/ipv4.mjs +2 -32
  54. package/node_modules/typebox/build/format/ipv6.mjs +2 -61
  55. package/node_modules/typebox/build/format/iri.d.mts +2 -2
  56. package/node_modules/typebox/build/format/iri.mjs +26 -8
  57. package/node_modules/typebox/build/format/iri_reference.d.mts +2 -2
  58. package/node_modules/typebox/build/format/iri_reference.mjs +9 -55
  59. package/node_modules/typebox/build/format/regex.d.mts +1 -2
  60. package/node_modules/typebox/build/format/regex.mjs +2 -6
  61. package/node_modules/typebox/build/format/time.d.mts +2 -2
  62. package/node_modules/typebox/build/format/time.mjs +19 -14
  63. package/node_modules/typebox/build/format/uri.d.mts +1 -1
  64. package/node_modules/typebox/build/format/uri.mjs +3 -130
  65. package/node_modules/typebox/build/format/uri_reference.d.mts +1 -1
  66. package/node_modules/typebox/build/format/uri_reference.mjs +2 -3
  67. package/node_modules/typebox/build/format/uri_template.d.mts +2 -3
  68. package/node_modules/typebox/build/format/uri_template.mjs +4 -5
  69. package/node_modules/typebox/build/format/url.d.mts +3 -3
  70. package/node_modules/typebox/build/format/url.mjs +4 -5
  71. package/node_modules/typebox/build/guard/emit.d.mts +3 -1
  72. package/node_modules/typebox/build/guard/emit.mjs +12 -5
  73. package/node_modules/typebox/build/guard/guard.d.mts +8 -2
  74. package/node_modules/typebox/build/guard/guard.mjs +25 -4
  75. package/node_modules/typebox/build/guard/string.d.mts +2 -2
  76. package/node_modules/typebox/build/guard/string.mjs +7 -7
  77. package/node_modules/typebox/build/schema/engine/_context.mjs +3 -3
  78. package/node_modules/typebox/build/schema/engine/_functions.mjs +1 -2
  79. package/node_modules/typebox/build/schema/engine/_regexp.d.mts +2 -0
  80. package/node_modules/typebox/build/schema/engine/_regexp.mjs +4 -0
  81. package/node_modules/typebox/build/schema/engine/_stack.mjs +12 -13
  82. package/node_modules/typebox/build/schema/engine/additionalItems.mjs +17 -7
  83. package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +4 -3
  84. package/node_modules/typebox/build/schema/engine/contains.mjs +18 -6
  85. package/node_modules/typebox/build/schema/engine/enum.mjs +1 -1
  86. package/node_modules/typebox/build/schema/engine/items.mjs +26 -6
  87. package/node_modules/typebox/build/schema/engine/maxContains.mjs +3 -3
  88. package/node_modules/typebox/build/schema/engine/minContains.mjs +14 -4
  89. package/node_modules/typebox/build/schema/engine/not.mjs +4 -2
  90. package/node_modules/typebox/build/schema/engine/oneOf.mjs +7 -6
  91. package/node_modules/typebox/build/schema/engine/pattern.mjs +3 -2
  92. package/node_modules/typebox/build/schema/engine/patternProperties.mjs +4 -3
  93. package/node_modules/typebox/build/schema/engine/ref.mjs +1 -7
  94. package/node_modules/typebox/build/schema/engine/schema.mjs +13 -4
  95. package/node_modules/typebox/build/schema/engine/type.mjs +1 -1
  96. package/node_modules/typebox/build/schema/resolve/resolve.d.mts +4 -1
  97. package/node_modules/typebox/build/schema/resolve/resolve.mjs +202 -1
  98. package/node_modules/typebox/build/schema/static/propertyNames.d.mts +5 -0
  99. package/node_modules/typebox/build/schema/static/propertyNames.mjs +3 -0
  100. package/node_modules/typebox/build/schema/static/schema.d.mts +3 -0
  101. package/node_modules/typebox/build/schema/types/index.d.mts +0 -2
  102. package/node_modules/typebox/build/schema/types/index.mjs +0 -2
  103. package/node_modules/typebox/build/system/memory/clone.mjs +14 -11
  104. package/node_modules/typebox/build/system/memory/discard.mjs +6 -5
  105. package/node_modules/typebox/build/system/settings/settings.d.mts +9 -0
  106. package/node_modules/typebox/build/system/settings/settings.mjs +2 -0
  107. package/node_modules/typebox/build/type/engine/call/distribute_arguments.d.mts +6 -1
  108. package/node_modules/typebox/build/type/engine/call/distribute_arguments.mjs +16 -3
  109. package/node_modules/typebox/build/type/engine/call/instantiate.mjs +34 -4
  110. package/node_modules/typebox/build/type/engine/evaluate/broaden.d.mts +5 -5
  111. package/node_modules/typebox/build/type/engine/evaluate/broaden.mjs +20 -26
  112. package/node_modules/typebox/build/type/engine/evaluate/compare.d.mts +6 -10
  113. package/node_modules/typebox/build/type/engine/evaluate/compare.mjs +9 -13
  114. package/node_modules/typebox/build/type/engine/evaluate/composite.d.mts +7 -4
  115. package/node_modules/typebox/build/type/engine/evaluate/composite.mjs +12 -19
  116. package/node_modules/typebox/build/type/engine/evaluate/distribute.d.mts +4 -20
  117. package/node_modules/typebox/build/type/engine/evaluate/distribute.mjs +8 -20
  118. package/node_modules/typebox/build/type/engine/evaluate/evaluate.d.mts +4 -4
  119. package/node_modules/typebox/build/type/engine/evaluate/evaluate.mjs +11 -8
  120. package/node_modules/typebox/build/type/engine/evaluate/flatten.d.mts +1 -1
  121. package/node_modules/typebox/build/type/engine/evaluate/flatten.mjs +3 -4
  122. package/node_modules/typebox/build/type/engine/evaluate/narrow.d.mts +17 -2
  123. package/node_modules/typebox/build/type/engine/evaluate/narrow.mjs +26 -6
  124. package/node_modules/typebox/build/type/engine/exclude/operation.mjs +3 -4
  125. package/node_modules/typebox/build/type/engine/extract/operation.d.mts +1 -1
  126. package/node_modules/typebox/build/type/engine/extract/operation.mjs +3 -4
  127. package/node_modules/typebox/build/type/engine/priority/priority.d.mts +2 -2
  128. package/node_modules/typebox/build/type/engine/priority/priority.mjs +3 -4
  129. package/node_modules/typebox/build/type/script/mapping.d.mts +32 -29
  130. package/node_modules/typebox/build/type/script/mapping.mjs +18 -25
  131. package/node_modules/typebox/build/type/script/parser.d.mts +19 -25
  132. package/node_modules/typebox/build/type/script/parser.mjs +27 -23
  133. package/node_modules/typebox/build/value/errors/errors.d.mts +2 -2
  134. package/node_modules/typebox/build/value/errors/errors.mjs +1 -1
  135. package/node_modules/typebox/package.json +27 -27
  136. package/node_modules/typebox/readme.md +79 -82
  137. package/openclaw.plugin.json +26 -2
  138. package/package.json +6 -6
  139. package/dist/card-command-CyDSm-OU.js +0 -224
  140. package/dist/channel.runtime-qrUV6wPE.js +0 -4
  141. package/dist/config-schema-Be3azjB_.js +0 -47
  142. package/dist/monitor.runtime-B2z_5ugU.js +0 -2
  143. package/dist/outbound.runtime-DDTeqaBW.js +0 -3
  144. package/dist/probe.runtime-D13ZtXsd.js +0 -2
  145. package/dist/quick-reply-fallback-D86FwAfr.js +0 -56
  146. package/node_modules/typebox/build/format/_idna.d.mts +0 -2
  147. package/node_modules/typebox/build/format/_idna.mjs +0 -246
  148. package/node_modules/typebox/build/format/_puny.d.mts +0 -1
  149. package/node_modules/typebox/build/format/_puny.mjs +0 -77
  150. package/node_modules/typebox/build/schema/resolve/ref.d.mts +0 -3
  151. package/node_modules/typebox/build/schema/resolve/ref.mjs +0 -146
  152. package/node_modules/typebox/build/schema/types/contentEncoding.d.mts +0 -9
  153. package/node_modules/typebox/build/schema/types/contentEncoding.mjs +0 -13
  154. package/node_modules/typebox/build/schema/types/contentMediaType.d.mts +0 -9
  155. package/node_modules/typebox/build/schema/types/contentMediaType.mjs +0 -13
  156. package/dist/{probe-CM03wo6r.js → probe-4gc_YQ2L.js} +1 -1
@@ -1,34 +1,36 @@
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 { v as buildLineMediaMessage, y as hasLineSpecificMediaOptions } from "./send-receipt-DKZzgHMO.js";
4
- import { i as processLineMessage, o as buildTemplateMessageFromPayload } from "./markdown-to-line-rEaras_0.js";
5
- import { _ as replyMessageLine, a as createQuickReplyItems, c as getUserDisplayName, i as createLocationMessage, m as pushMessagesLine, n as createFlexMessage, p as pushMessageLine, y as showLoadingAnimation } from "./send-BJCeiiOB.js";
1
+ import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BBEFMYbY.js";
2
+ import { f as resolveLineGroupConfigEntry, h as getLineRuntime, i as prepareLineReplyPayload, n as createLineQuickReply, u as buildLineQuickReplyFallbackText } from "./rich-messages-CIP7xn5E.js";
3
+ import { n as resolveLineNonDispatchRetryable, t as findLineHttpError, y as buildLineMediaMessage } from "./send-retry-CUvrarfE.js";
4
+ import { a as eventIdFor, n as LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE, o as laneKeyFor, r as LineWebhookPayloadError, t as LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON } from "./webhook-spool-contract-1rY8OJw8.js";
5
+ import { i as processLineMessage, s as buildTemplateMessageFromPayload } from "./markdown-to-line--jytmFan.js";
6
+ import { _ as showLoadingAnimation, a as getLineGroupName, d as pushMessageLine, f as pushMessagesLine, h as replyMessageLine, n as createLocationMessage, o as getUserDisplayName, s as getUserProfile, t as createFlexMessage, v as quotesLineBotMessage } from "./send-wVUV00WS.js";
6
7
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
7
- import { normalizeNullableString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
8
- import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
9
- import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, isChannelPartialDeliveryError, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
8
+ import { normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
9
+ import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
10
+ import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, implicitMentionKindWhen, isChannelPartialDeliveryError, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
11
+ import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, readErrorName, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
10
12
  import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
11
13
  import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
12
14
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
15
+ import { classifyTransientNetworkErrorCode } from "openclaw/plugin-sdk/retry-runtime";
13
16
  import { firstDefined } from "openclaw/plugin-sdk/allow-from";
14
17
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
15
18
  import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
16
19
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
17
20
  import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal, warn } from "openclaw/plugin-sdk/runtime-env";
18
21
  import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
19
- import { HTTPFetchError } from "@line/bot-sdk";
20
- import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, readErrorName, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
21
22
  import crypto from "node:crypto";
22
23
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
23
- import { classifyTransientNetworkErrorCode } from "openclaw/plugin-sdk/retry-runtime";
24
+ import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
24
25
  import { channelReadyPatch, channelStoppedPatch } from "openclaw/plugin-sdk/gateway-runtime";
25
26
  import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
26
27
  import { canonicalizeWebhookRouteKey, isRequestBodyLimitError, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
27
28
  import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1, runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
28
29
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
29
30
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
30
- import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
31
- import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
31
+ import { getRuntimeConfig, getRuntimeConfigSnapshot, getRuntimeConfigSourceSnapshot, selectApplicableRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
32
+ import { resolveChannelImplicitMentions, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
33
+ import { reportChannelRoomJoin } from "openclaw/plugin-sdk/channel-join-intro-runtime";
32
34
  import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
33
35
  import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
34
36
  import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
@@ -110,7 +112,7 @@ async function fetchLineContentWhenReady(messageId, channelAccessToken, signal)
110
112
  }
111
113
  await response.body?.cancel().catch(() => void 0);
112
114
  if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
113
- if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal });
115
+ if (attempt < 5) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal });
114
116
  }
115
117
  } catch (err) {
116
118
  if (signal.aborted) throw signal.reason;
@@ -125,7 +127,7 @@ function isRetryableLineInboundMediaError(err) {
125
127
  if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
126
128
  return false;
127
129
  }
128
- async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
130
+ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10485760, options) {
129
131
  options?.signal?.throwIfAborted();
130
132
  const deadlineAbort = new AbortController();
131
133
  const signal = options?.signal ? AbortSignal.any([options.signal, deadlineAbort.signal]) : deadlineAbort.signal;
@@ -159,11 +161,8 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
159
161
  function toLineDeliveryError(error) {
160
162
  return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
161
163
  }
162
- function getLineHttpError(error) {
163
- return collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]).find((candidate) => candidate instanceof HTTPFetchError);
164
- }
165
164
  function canFallbackAfterLineReplyFailure(error) {
166
- const httpError = getLineHttpError(error);
165
+ const httpError = findLineHttpError(error);
167
166
  if (httpError) return httpError.status >= 400 && httpError.status < 500 && httpError.status !== 408;
168
167
  const candidates = collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]);
169
168
  if (candidates.some((candidate) => readErrorName(candidate) === "AbortError" || classifyTransientNetworkErrorCode(extractErrorCode(candidate)) === "ambiguous")) return false;
@@ -235,7 +234,7 @@ async function deliverLineAutoReply(params) {
235
234
  } catch (err) {
236
235
  if (isChannelPartialDeliveryError(err) || !canFallbackAfterLineReplyFailure(err)) throw err;
237
236
  deps.onReplyError?.(err);
238
- await pushLineMessages(replyBatch, getLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
237
+ await pushLineMessages(replyBatch, findLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
239
238
  } finally {
240
239
  replyTokenUsed = true;
241
240
  }
@@ -244,7 +243,15 @@ async function deliverLineAutoReply(params) {
244
243
  if (remaining.length > 0) await pushLineMessages(remaining, true);
245
244
  };
246
245
  const richMessages = [];
247
- const hasQuickReplies = Boolean(lineData.quickReplies?.length);
246
+ const quickReplyItems = lineData.quickReplyItems?.length ? lineData.quickReplyItems : (lineData.quickReplies ?? []).map((label) => ({
247
+ label,
248
+ action: {
249
+ type: "command",
250
+ command: label
251
+ }
252
+ }));
253
+ const quickReplyLabels = quickReplyItems.map((item) => item.label);
254
+ const hasQuickReplies = quickReplyItems.length > 0;
248
255
  if (lineData.flexMessage) richMessages.push(deps.createFlexMessage(lineData.flexMessage.altText, lineData.flexMessage.contents));
249
256
  if (lineData.templateMessage) {
250
257
  const templateMsg = deps.buildTemplateMessageFromPayload(lineData.templateMessage);
@@ -264,7 +271,7 @@ async function deliverLineAutoReply(params) {
264
271
  const chunks = orderedMessages ? orderedMessages.flatMap((message) => message.type === "text" ? [message.text] : []) : processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : [];
265
272
  const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
266
273
  const mediaOpts = {
267
- mediaKind: hasLineSpecificMediaOptions(lineData) ? lineData.mediaKind : "image",
274
+ mediaKind: lineData.mediaKind,
268
275
  previewImageUrl: lineData.previewImageUrl,
269
276
  durationMs: lineData.durationMs,
270
277
  trackingId: lineData.trackingId
@@ -284,37 +291,46 @@ async function deliverLineAutoReply(params) {
284
291
  type: "text",
285
292
  text
286
293
  }));
287
- const richMediaMessages = [...richMessages, ...mediaMessages];
294
+ const orderedDeliveryMessages = hasQuickReplies && chunks.length === 0 ? void 0 : orderedMessages;
295
+ const richMediaMessages = [
296
+ ...richMessages,
297
+ ...orderedDeliveryMessages ? [] : orderedMessages ?? [],
298
+ ...mediaMessages
299
+ ];
288
300
  if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
289
301
  type: "text",
290
- text: buildLineQuickReplyFallbackText(lineData.quickReplies)
302
+ text: buildLineQuickReplyFallbackText(quickReplyLabels)
291
303
  });
292
304
  if (hasQuickReplies) {
293
- const targetMessages = orderedMessages?.length ? orderedMessages : textMessages.length > 0 ? textMessages : richMediaMessages;
305
+ const targetMessages = orderedDeliveryMessages?.length ? orderedDeliveryMessages : textMessages.length > 0 ? textMessages : richMediaMessages;
294
306
  const lastIndex = targetMessages.length - 1;
295
307
  targetMessages[lastIndex] = {
296
308
  ...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
297
- quickReply: deps.createQuickReplyItems(lineData.quickReplies)
309
+ quickReply: createLineQuickReply(quickReplyItems)
298
310
  };
299
311
  }
300
- const messages = hasQuickReplies ? [...richMediaMessages, ...orderedMessages ?? textMessages] : [...orderedMessages ?? textMessages, ...richMediaMessages];
312
+ const messages = hasQuickReplies ? [...richMediaMessages, ...orderedDeliveryMessages ?? textMessages] : [...orderedDeliveryMessages ?? textMessages, ...richMediaMessages];
301
313
  try {
302
314
  await sendLineMessages(messages, true);
303
315
  } catch (err) {
304
316
  deliveryError ??= err;
305
317
  const failedSegment = typeof err === "object" && err !== null ? failedPushSegments.get(err) : void 0;
306
- const httpError = getLineHttpError(err);
318
+ const httpError = findLineHttpError(err);
307
319
  const retryCandidates = failedSegment ? [...failedSegment.allowFailedBatchTextRecovery ? failedSegment.failedBatch : [], ...failedSegment.unattemptedTail] : [];
308
320
  const retryTextMessages = retryCandidates.filter((message) => message.type === "text");
309
321
  const quickRepliesNeedCarrier = hasQuickReplies && retryCandidates.some((message) => "quickReply" in message);
310
322
  const retryMessages = retryTextMessages.length > 0 ? retryTextMessages : quickRepliesNeedCarrier ? [{
311
323
  type: "text",
312
- text: buildLineQuickReplyFallbackText(lineData.quickReplies),
313
- quickReply: deps.createQuickReplyItems(lineData.quickReplies)
324
+ text: buildLineQuickReplyFallbackText(quickReplyLabels),
325
+ quickReply: createLineQuickReply(quickReplyItems)
314
326
  }] : [];
315
- if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400) try {
316
- await sendLineMessages(retryMessages, false);
317
- } catch {}
327
+ if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400 && resolveLineNonDispatchRetryable(err) !== void 0) {
328
+ const lastRetryMessage = retryMessages.at(-1);
329
+ if (quickRepliesNeedCarrier && lastRetryMessage && !lastRetryMessage.quickReply) lastRetryMessage.quickReply = createLineQuickReply(quickReplyItems);
330
+ try {
331
+ await sendLineMessages(retryMessages, false);
332
+ } catch {}
333
+ }
318
334
  }
319
335
  if (deliveryError !== void 0) {
320
336
  if (!visibleReplySent) throw toLineDeliveryError(deliveryError);
@@ -332,6 +348,44 @@ async function deliverLineAutoReply(params) {
332
348
  };
333
349
  }
334
350
  //#endregion
351
+ //#region extensions/line/src/mentions.ts
352
+ function getLineMentionees(message) {
353
+ return message.type === "text" ? message.mention?.mentionees ?? [] : [];
354
+ }
355
+ function addressesLineBot(mentionee) {
356
+ return mentionee.type === "all" || mentionee.isSelf === true;
357
+ }
358
+ function isLineBotMentioned(message) {
359
+ return getLineMentionees(message).some(addressesLineBot);
360
+ }
361
+ function hasAnyLineMention(message) {
362
+ return getLineMentionees(message).length > 0;
363
+ }
364
+ /**
365
+ * Text to interpret as a command, with the mention that addressed the bot removed.
366
+ *
367
+ * LINE writes a mention as the plain channel display name, so no pattern can
368
+ * tell it apart from a member's name; `mentionees[].index`/`length` (UTF-16
369
+ * code units, the unit LINE counts text in) is the only authoritative marker.
370
+ * Leaving it in place makes a group `@<bot> /status` parse as prose, and group
371
+ * chats require the mention to reach the bot at all.
372
+ */
373
+ function resolveLineMentionStrippedText(message) {
374
+ const text = message.type === "text" ? message.text : "";
375
+ const spans = getLineMentionees(message).filter(addressesLineBot).map((mentionee) => ({
376
+ start: mentionee.index,
377
+ end: mentionee.index + mentionee.length
378
+ })).toSorted((left, right) => left.start - right.start);
379
+ let stripped = "";
380
+ let cursor = 0;
381
+ for (const span of spans) {
382
+ stripped += text.slice(cursor, span.start);
383
+ if (/\S$/u.test(stripped) && /^\S/u.test(text.slice(span.end))) stripped += " ";
384
+ cursor = Math.max(cursor, span.end);
385
+ }
386
+ return `${stripped}${text.slice(cursor)}`.trim();
387
+ }
388
+ //#endregion
335
389
  //#region extensions/line/src/bot-message-context.ts
336
390
  function getLineSourceInfo(source) {
337
391
  if (!source) return {
@@ -416,30 +470,23 @@ async function resolveLineInboundRoute(params) {
416
470
  route
417
471
  };
418
472
  }
419
- const STICKER_PACKAGES = {
420
- "1": "Moon & James",
421
- "2": "Cony & Brown",
422
- "3": "Brown & Friends",
423
- "4": "Moon Special",
424
- "789": "LINE Characters",
425
- "6136": "Cony's Happy Life",
426
- "6325": "Brown's Life",
427
- "6359": "Choco",
428
- "6362": "Sally",
429
- "6370": "Edward",
430
- "11537": "Cony",
431
- "11538": "Brown",
432
- "11539": "Moon"
433
- };
434
- function describeStickerKeywords(sticker) {
435
- const keywords = sticker.keywords;
436
- if (keywords && keywords.length > 0) return keywords.slice(0, 3).join(", ");
437
- const stickerText = sticker.text;
438
- if (stickerText) return stickerText;
439
- return "";
473
+ /**
474
+ * Describe a sticker from what its webhook actually carries: LINE sends up to
475
+ * 15 keywords for the sticker, and a message sticker also carries the sender's
476
+ * own text. The package name is not among those facts and cannot be derived
477
+ * from the package id, so it is not part of the description.
478
+ */
479
+ function describeLineSticker(sticker) {
480
+ const description = readNonEmptyStringPreservingWhitespace(sticker.text) ?? normalizeStringEntries(sticker.keywords ?? []).slice(0, 3).join(", ");
481
+ return description ? `[Sent a sticker: ${description}]` : "[Sent a sticker]";
482
+ }
483
+ function readLineTextMessageBody(message) {
484
+ let text = message.text;
485
+ for (const { index, length } of (message.emojis ?? []).toSorted((a, b) => b.index - a.index)) if (index >= 0 && length === 2 && text.slice(index, index + length) === "()") text = `${text.slice(0, index)}[emoji]${text.slice(index + length)}`;
486
+ return text;
440
487
  }
441
488
  function extractMessageText(message) {
442
- if (message.type === "text") return message.text;
489
+ if (message.type === "text") return readLineTextMessageBody(message);
443
490
  if (message.type === "location") {
444
491
  const loc = message;
445
492
  return formatLocationText({
@@ -449,13 +496,7 @@ function extractMessageText(message) {
449
496
  address: loc.address
450
497
  }) ?? "";
451
498
  }
452
- if (message.type === "sticker") {
453
- const sticker = message;
454
- const packageName = STICKER_PACKAGES[sticker.packageId] ?? "sticker";
455
- const keywords = describeStickerKeywords(sticker);
456
- if (keywords) return `[Sent a ${packageName} sticker: ${keywords}]`;
457
- return `[Sent a ${packageName} sticker]`;
458
- }
499
+ if (message.type === "sticker") return describeLineSticker(message);
459
500
  return "";
460
501
  }
461
502
  function extractNativeMediaKind(message) {
@@ -469,9 +510,23 @@ function extractNativeMediaKind(message) {
469
510
  }
470
511
  async function finalizeLineInboundContext(params) {
471
512
  const senderId = params.source.userId ?? "unknown";
472
- const senderLabel = params.source.userId ? `user:${params.source.userId}` : "unknown";
473
- const conversationLabel = params.source.isGroup ? params.source.groupId ? `group:${params.source.groupId}` : params.source.roomId ? `room:${params.source.roomId}` : "unknown-group" : senderLabel;
513
+ const clientOpts = {
514
+ cfg: params.cfg,
515
+ accountId: params.account.accountId,
516
+ channelAccessToken: params.account.channelAccessToken
517
+ };
518
+ const [senderName, groupName] = await Promise.all([params.source.userId ? getUserProfile(params.source.userId, {
519
+ ...clientOpts,
520
+ groupId: params.source.groupId,
521
+ roomId: params.source.roomId
522
+ }).then((profile) => profile?.displayName) : void 0, params.source.groupId ? getLineGroupName(params.source.groupId, clientOpts) : void 0]);
523
+ const senderLabel = senderName ?? (params.source.userId ? `user:${params.source.userId}` : "unknown");
524
+ const conversationLabel = params.source.isGroup ? groupName ?? (params.source.groupId ? `group:${params.source.groupId}` : params.source.roomId ? `room:${params.source.roomId}` : "unknown-group") : senderLabel;
474
525
  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}`;
526
+ const groupConfig = params.source.isGroup ? resolveLineGroupConfigEntry(params.account.config.groups, {
527
+ groupId: params.source.groupId,
528
+ roomId: params.source.roomId
529
+ }) : void 0;
475
530
  const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
476
531
  cfg: params.cfg,
477
532
  agentId: params.route.agentId,
@@ -485,7 +540,10 @@ async function finalizeLineInboundContext(params) {
485
540
  timestamp: params.timestamp,
486
541
  body: agentBody,
487
542
  chatType: params.source.isGroup ? "group" : "direct",
488
- sender: { id: senderId },
543
+ sender: {
544
+ id: senderId,
545
+ name: senderName
546
+ },
489
547
  previousTimestamp,
490
548
  envelope: envelopeOptions
491
549
  });
@@ -496,7 +554,10 @@ async function finalizeLineInboundContext(params) {
496
554
  messageId: params.messageSid,
497
555
  timestamp: params.timestamp,
498
556
  from: address,
499
- sender: { id: senderId },
557
+ sender: {
558
+ id: senderId,
559
+ name: senderName
560
+ },
500
561
  conversation: {
501
562
  kind: params.source.isGroup ? "group" : "direct",
502
563
  id: params.source.peerId,
@@ -516,18 +577,15 @@ async function finalizeLineInboundContext(params) {
516
577
  body,
517
578
  bodyForAgent: agentBody,
518
579
  rawBody: params.rawBody,
519
- commandBody: params.rawBody,
580
+ commandBody: params.commandBody ?? params.rawBody,
520
581
  inboundHistory: params.inboundHistory
521
582
  },
522
583
  access: { commands: { authorized: params.commandAuthorized } },
523
584
  media,
524
585
  extra: {
525
586
  ...params.locationContext,
526
- GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
527
- GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
528
- groupId: params.source.groupId,
529
- roomId: params.source.roomId
530
- })?.systemPrompt) : void 0
587
+ GroupSubject: params.source.isGroup ? groupName ?? params.source.groupId ?? params.source.roomId : void 0,
588
+ GroupSystemPrompt: normalizeOptionalString(groupConfig?.systemPrompt)
531
589
  }
532
590
  });
533
591
  const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
@@ -542,11 +600,13 @@ async function finalizeLineInboundContext(params) {
542
600
  if (shouldLogVerbose()) {
543
601
  const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
544
602
  const mediaInfo = params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1 ? ` mediaCount=${params.verboseLog.mediaCount}` : "";
545
- logVerbose(`${params.verboseLog.kind === "inbound" ? "line inbound" : "line postback"}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
603
+ const label = params.verboseLog.kind === "inbound" ? "line inbound" : "line postback";
604
+ logVerbose(`${label}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
546
605
  }
547
606
  return {
548
607
  ctxPayload,
549
608
  replyToken: params.event.replyToken,
609
+ skillFilter: groupConfig?.skills,
550
610
  turn: {
551
611
  storePath,
552
612
  record: {
@@ -617,6 +677,7 @@ async function buildLineMessageContext(params) {
617
677
  },
618
678
  rawBody,
619
679
  agentBody,
680
+ commandBody: resolveLineMentionStrippedText(message) || rawBody,
620
681
  timestamp,
621
682
  messageSid: messageId,
622
683
  commandAuthorized,
@@ -638,6 +699,7 @@ async function buildLineMessageContext(params) {
638
699
  return {
639
700
  ctxPayload: finalized.ctxPayload,
640
701
  turn: finalized.turn,
702
+ skillFilter: finalized.skillFilter,
641
703
  event,
642
704
  userId,
643
705
  groupId,
@@ -657,14 +719,18 @@ async function buildLinePostbackContext(params) {
657
719
  account
658
720
  });
659
721
  const timestamp = event.timestamp;
660
- const rawData = event.postback?.data?.trim() ?? "";
661
- if (!rawData) return null;
662
- let rawBody = rawData;
663
- if (rawData.includes("line.action=")) {
664
- const searchParams = new URLSearchParams(rawData);
722
+ const rawBody = event.postback?.data?.trim() ?? "";
723
+ if (!rawBody) return null;
724
+ let agentBody = rawBody;
725
+ if (rawBody.includes("line.action=")) {
726
+ const searchParams = new URLSearchParams(rawBody);
665
727
  const action = searchParams.get("line.action") ?? "";
666
728
  const device = searchParams.get("line.device");
667
- rawBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
729
+ agentBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
730
+ }
731
+ for (const [key, value] of Object.entries(event.postback.params ?? {}).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
732
+ const picked = normalizeOptionalString(value);
733
+ if (picked) agentBody += ` ${key}=${picked}`;
668
734
  }
669
735
  const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
670
736
  const finalized = await finalizeLineInboundContext({
@@ -680,6 +746,7 @@ async function buildLinePostbackContext(params) {
680
746
  peerId
681
747
  },
682
748
  rawBody,
749
+ agentBody,
683
750
  timestamp,
684
751
  messageSid,
685
752
  commandAuthorized,
@@ -696,6 +763,7 @@ async function buildLinePostbackContext(params) {
696
763
  return {
697
764
  ctxPayload: finalized.ctxPayload,
698
765
  turn: finalized.turn,
766
+ skillFilter: finalized.skillFilter,
699
767
  event,
700
768
  userId,
701
769
  groupId,
@@ -755,12 +823,6 @@ function isDownloadableLineMessageType(messageType) {
755
823
  function normalizeLineIngressEntry(value) {
756
824
  return normalizeLineAllowEntry(value) || null;
757
825
  }
758
- function resolveLineGroupConfig(params) {
759
- return resolveLineGroupConfigEntry(params.config.groups, {
760
- groupId: params.groupId,
761
- roomId: params.roomId
762
- });
763
- }
764
826
  async function sendLinePairingReply(params) {
765
827
  const { senderId, replyToken, context } = params;
766
828
  const idLabel = (() => {
@@ -812,12 +874,11 @@ async function sendLinePairingReply(params) {
812
874
  }
813
875
  });
814
876
  }
815
- async function shouldProcessLineEvent(event, context) {
877
+ async function resolveLineEventAdmission(event, context) {
816
878
  const { cfg, account } = context;
817
879
  const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
818
880
  const senderId = userId ?? "";
819
- const groupConfig = resolveLineGroupConfig({
820
- config: account.config,
881
+ const groupConfig = resolveLineGroupConfigEntry(account.config.groups, {
821
882
  groupId,
822
883
  roomId
823
884
  });
@@ -835,7 +896,8 @@ async function shouldProcessLineEvent(event, context) {
835
896
  if (!isGroup || event.type !== "message") return {
836
897
  canDetectMention: false,
837
898
  wasMentioned: false,
838
- hasAnyMention: false
899
+ hasAnyMention: false,
900
+ implicitMentionKinds: []
839
901
  };
840
902
  const peerId = groupId ?? roomId ?? userId ?? "unknown";
841
903
  const { agentId } = resolveAgentRoute({
@@ -853,7 +915,8 @@ async function shouldProcessLineEvent(event, context) {
853
915
  return {
854
916
  canDetectMention: event.message.type === "text",
855
917
  wasMentioned: wasMentionedByNative || wasMentionedByPattern,
856
- hasAnyMention: hasAnyLineMention(event.message)
918
+ hasAnyMention: hasAnyLineMention(event.message),
919
+ implicitMentionKinds: implicitMentionKindWhen("quoted_bot", quotesLineBotMessage(account.accountId, resolveLineQuotedMessageId(event.message)))
857
920
  };
858
921
  })();
859
922
  const resolveAccess = async (contextBinding) => await resolveStableChannelMessageIngress({
@@ -867,7 +930,7 @@ async function shouldProcessLineEvent(event, context) {
867
930
  },
868
931
  cfg,
869
932
  readStoreAllowFrom: async () => await readChannelAllowFromStore("line", void 0, account.accountId),
870
- subject: { stableId: senderId },
933
+ subject: event.type === "join" ? {} : { stableId: senderId },
871
934
  conversation: {
872
935
  kind: isGroup ? "group" : "direct",
873
936
  id: (groupId ?? roomId ?? senderId) || "unknown"
@@ -881,16 +944,21 @@ async function shouldProcessLineEvent(event, context) {
881
944
  canDetectMention: mentionFacts.canDetectMention,
882
945
  wasMentioned: mentionFacts.wasMentioned,
883
946
  hasAnyMention: mentionFacts.hasAnyMention,
884
- implicitMentionKinds: []
947
+ implicitMentionKinds: mentionFacts.implicitMentionKinds
885
948
  } : void 0,
886
- event: { kind: event.type === "postback" ? "postback" : "message" },
949
+ event: { kind: event.type === "join" ? "system" : event.type },
887
950
  dmPolicy,
888
951
  groupPolicy,
889
952
  policy: {
890
953
  groupAllowFromFallbackToAllowFrom: false,
891
954
  activation: {
892
955
  requireMention: isGroup && event.type === "message" && requireMention,
893
- allowTextCommands: true
956
+ allowTextCommands: true,
957
+ implicitMentions: resolveChannelImplicitMentions({
958
+ cfg,
959
+ channel: "line",
960
+ accountId: account.accountId
961
+ })
894
962
  }
895
963
  },
896
964
  allowFrom: normalizeStringEntries(account.config.allowFrom),
@@ -907,6 +975,10 @@ async function shouldProcessLineEvent(event, context) {
907
975
  accountId: account.accountId,
908
976
  log: (message) => logVerbose(message)
909
977
  });
978
+ if (event.type === "join") return groupConfig?.enabled !== false && groupPolicy !== "disabled" && (groupPolicy !== "allowlist" || access.state.allowlists.group.hasMatchableEntries) ? {
979
+ access,
980
+ resolveBoundAccess: resolveAccess
981
+ } : null;
910
982
  if (access.senderAccess.decision === "allow" && (access.ingress.admission === "dispatch" || access.ingress.admission === "observe" || access.ingress.admission === "skip")) return {
911
983
  access,
912
984
  resolveBoundAccess: resolveAccess
@@ -955,21 +1027,13 @@ async function shouldProcessLineEvent(event, context) {
955
1027
  logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${account.config.dmPolicy ?? "pairing"})`);
956
1028
  return null;
957
1029
  }
958
- function getLineMentionees(message) {
959
- if (message.type !== "text") return [];
960
- const mentionees = message.mention?.mentionees;
961
- return Array.isArray(mentionees) ? mentionees : [];
962
- }
963
- function isLineBotMentioned(message) {
964
- return getLineMentionees(message).some((m) => m.isSelf === true || m.type === "all");
965
- }
966
- function hasAnyLineMention(message) {
967
- return getLineMentionees(message).length > 0;
1030
+ function resolveLineQuotedMessageId(message) {
1031
+ return message.type === "text" || message.type === "sticker" ? message.quotedMessageId : void 0;
968
1032
  }
969
1033
  function resolveEventRawText(event) {
970
1034
  if (event.type === "message") {
971
1035
  const msg = event.message;
972
- if (msg.type === "text") return msg.text;
1036
+ if (msg.type === "text") return readLineTextMessageBody(msg);
973
1037
  return "";
974
1038
  }
975
1039
  if (event.type === "postback") return event.postback?.data?.trim() ?? "";
@@ -978,24 +1042,34 @@ function resolveEventRawText(event) {
978
1042
  async function handleMessageEvent(event, context) {
979
1043
  const { cfg, account, runtime, mediaMaxBytes, processMessage } = context;
980
1044
  const message = event.message;
981
- const decision = await shouldProcessLineEvent(event, context);
1045
+ const decision = await resolveLineEventAdmission(event, context);
982
1046
  if (!decision) return;
983
1047
  const { isGroup, groupId, roomId } = getLineSourceInfo(event.source);
984
1048
  if (isGroup && decision.access.activationAccess.shouldSkip) {
985
- const rawText = message.type === "text" ? message.text : "";
1049
+ const rawText = message.type === "text" ? readLineTextMessageBody(message) : "";
986
1050
  const sourceInfo = getLineSourceInfo(event.source);
987
1051
  logVerbose(`line: skipping group message (requireMention, not mentioned)`);
988
1052
  const historyKey = groupId ?? roomId;
989
1053
  const senderId = sourceInfo.userId ?? "unknown";
990
- if (historyKey && context.groupHistories) createChannelHistoryWindow({ historyMap: context.groupHistories }).record({
991
- historyKey,
992
- limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
993
- entry: {
994
- sender: `user:${senderId}`,
995
- body: rawText || `<${message.type}>`,
996
- timestamp: event.timestamp
997
- }
998
- });
1054
+ if (historyKey && context.groupHistories) {
1055
+ const displayName = sourceInfo.userId ? await getUserDisplayName(sourceInfo.userId, {
1056
+ cfg,
1057
+ accountId: account.accountId,
1058
+ channelAccessToken: account.channelAccessToken,
1059
+ groupId,
1060
+ roomId
1061
+ }) : senderId;
1062
+ const sender = displayName === senderId ? senderId : `${displayName} (${senderId})`;
1063
+ createChannelHistoryWindow({ historyMap: context.groupHistories }).record({
1064
+ historyKey,
1065
+ limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
1066
+ entry: {
1067
+ sender,
1068
+ body: rawText || `<${message.type}>`,
1069
+ timestamp: event.timestamp
1070
+ }
1071
+ });
1072
+ }
999
1073
  return;
1000
1074
  }
1001
1075
  const groupHistoryKey = isGroup ? groupId ?? roomId : void 0;
@@ -1014,7 +1088,8 @@ async function handleMessageEvent(event, context) {
1014
1088
  abortSignal?.throwIfAborted();
1015
1089
  allMedia.push({
1016
1090
  path: media.path,
1017
- contentType: media.contentType
1091
+ contentType: media.contentType,
1092
+ ...originalFilename ? { fileName: originalFilename } : {}
1018
1093
  });
1019
1094
  } catch (err) {
1020
1095
  if (abortSignal?.aborted) throw abortSignal.reason;
@@ -1040,7 +1115,10 @@ async function handleMessageEvent(event, context) {
1040
1115
  logVerbose("line: skipping empty message");
1041
1116
  return;
1042
1117
  }
1043
- await processMessage(messageContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
1118
+ await processMessage(messageContext, {
1119
+ cfg,
1120
+ ...context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {}
1121
+ });
1044
1122
  historyReservation.commit();
1045
1123
  } finally {
1046
1124
  historyReservation.release();
@@ -1054,9 +1132,42 @@ async function handleUnfollowEvent(event, _context) {
1054
1132
  const { userId } = getLineSourceInfo(event.source);
1055
1133
  logVerbose(`line: user ${userId ?? "unknown"} unfollowed`);
1056
1134
  }
1057
- async function handleJoinEvent(event, _context) {
1058
- const { groupId, roomId } = getLineSourceInfo(event.source);
1135
+ async function handleJoinEvent(event, context) {
1136
+ const { groupId, roomId, isGroup } = getLineSourceInfo(event.source);
1137
+ const conversationId = groupId ?? roomId;
1138
+ if (!isGroup || !conversationId) return;
1059
1139
  logVerbose(`line: bot joined ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
1140
+ const { cfg, account } = context;
1141
+ const roomAllowed = Boolean(await resolveLineEventAdmission(event, context));
1142
+ await reportChannelRoomJoin({
1143
+ cfg,
1144
+ channel: "line",
1145
+ accountId: account.accountId,
1146
+ conversationId,
1147
+ deliverTo: conversationId,
1148
+ route: resolveAgentRoute({
1149
+ cfg,
1150
+ channel: "line",
1151
+ accountId: account.accountId,
1152
+ peer: {
1153
+ kind: "group",
1154
+ id: conversationId
1155
+ }
1156
+ }),
1157
+ roomAllowed,
1158
+ resolveRoomContext: async () => {
1159
+ const roomContext = { historyUnavailable: true };
1160
+ const title = groupId ? await getLineGroupName(groupId, {
1161
+ cfg,
1162
+ accountId: account.accountId,
1163
+ channelAccessToken: account.channelAccessToken
1164
+ }) : void 0;
1165
+ return title ? {
1166
+ ...roomContext,
1167
+ title
1168
+ } : roomContext;
1169
+ }
1170
+ });
1060
1171
  }
1061
1172
  async function handleLeaveEvent(event, _context) {
1062
1173
  const { groupId, roomId } = getLineSourceInfo(event.source);
@@ -1065,7 +1176,7 @@ async function handleLeaveEvent(event, _context) {
1065
1176
  async function handlePostbackEvent(event, context) {
1066
1177
  const data = event.postback.data;
1067
1178
  logVerbose(`line: received postback: ${data}`);
1068
- const decision = await shouldProcessLineEvent(event, context);
1179
+ const decision = await resolveLineEventAdmission(event, context);
1069
1180
  if (!decision) return;
1070
1181
  const postbackContext = await buildLinePostbackContext({
1071
1182
  event,
@@ -1076,7 +1187,10 @@ async function handlePostbackEvent(event, context) {
1076
1187
  buildContext: context.buildContext
1077
1188
  });
1078
1189
  if (!postbackContext) return;
1079
- await context.processMessage(postbackContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
1190
+ await context.processMessage(postbackContext, {
1191
+ cfg: context.cfg,
1192
+ ...context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {}
1193
+ });
1080
1194
  }
1081
1195
  async function handleLineWebhookEvents(events, context) {
1082
1196
  let firstError;
@@ -1113,12 +1227,10 @@ async function handleLineWebhookEvent(event, context) {
1113
1227
  }
1114
1228
  //#endregion
1115
1229
  //#region extensions/line/src/webhook-spool.ts
1116
- const LINE_WEBHOOK_SPOOL_VERSION = 1;
1117
1230
  const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
1118
1231
  const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
1119
1232
  const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
1120
1233
  const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
1121
- const LineWebhookPayloadError = createChannelIngressError("LineWebhookPayloadError");
1122
1234
  var LineWebhookTerminalDeliveryError = class extends Error {
1123
1235
  constructor(message, options) {
1124
1236
  super(message, options);
@@ -1126,35 +1238,6 @@ var LineWebhookTerminalDeliveryError = class extends Error {
1126
1238
  this.name = "LineWebhookTerminalDeliveryError";
1127
1239
  }
1128
1240
  };
1129
- /** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
1130
- function eventIdFor(event) {
1131
- if (!event || typeof event !== "object") throw new LineWebhookPayloadError("LINE webhook event must be an object.");
1132
- const candidate = event;
1133
- if (candidate.type === "message") {
1134
- const messageId = normalizeNullableString(candidate.message?.id);
1135
- if (messageId) return `message:${messageId}`;
1136
- }
1137
- const webhookEventId = normalizeNullableString(candidate.webhookEventId);
1138
- if (webhookEventId) return `event:${webhookEventId}`;
1139
- throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
1140
- }
1141
- function laneKeyFor(event, eventId) {
1142
- if (!event || typeof event !== "object") return eventId;
1143
- const source = event.source;
1144
- if (source?.type === "group") {
1145
- const groupId = normalizeNullableString(source.groupId);
1146
- if (groupId) return `group:${groupId}`;
1147
- }
1148
- if (source?.type === "room") {
1149
- const roomId = normalizeNullableString(source.roomId);
1150
- if (roomId) return `room:${roomId}`;
1151
- }
1152
- if (source?.type === "user") {
1153
- const userId = normalizeNullableString(source.userId);
1154
- if (userId) return `user:${userId}`;
1155
- }
1156
- return eventId;
1157
- }
1158
1241
  function parseStoredEvent(rawEvent) {
1159
1242
  let event;
1160
1243
  try {
@@ -1194,7 +1277,7 @@ function createLineWebhookSpool(options) {
1194
1277
  };
1195
1278
  },
1196
1279
  payload: {
1197
- version: LINE_WEBHOOK_SPOOL_VERSION,
1280
+ version: 1,
1198
1281
  serialize: ({ event, destination }) => ({
1199
1282
  rawEvent: JSON.stringify(event),
1200
1283
  destination
@@ -1209,7 +1292,7 @@ function createLineWebhookSpool(options) {
1209
1292
  destination: body.destination
1210
1293
  }),
1211
1294
  decode: (payload) => {
1212
- if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
1295
+ if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError(LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE);
1213
1296
  return {
1214
1297
  version: payload.version,
1215
1298
  body: {
@@ -1218,7 +1301,7 @@ function createLineWebhookSpool(options) {
1218
1301
  }
1219
1302
  };
1220
1303
  },
1221
- createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? "LINE webhook spool payload is invalid." : "LINE webhook event identity changed after durable admission.")
1304
+ createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE : "LINE webhook event identity changed after durable admission.")
1222
1305
  },
1223
1306
  deliver: async ({ event, destination }, lifecycle) => {
1224
1307
  const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
@@ -1278,7 +1361,7 @@ function createLineWebhookSpool(options) {
1278
1361
  },
1279
1362
  resolveNonRetryableFailure: (error) => {
1280
1363
  if (error instanceof LineWebhookPayloadError) return {
1281
- reason: "invalid-event",
1364
+ reason: LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON,
1282
1365
  message: error.message
1283
1366
  };
1284
1367
  if (error instanceof LineWebhookTerminalDeliveryError) return {
@@ -1299,12 +1382,12 @@ function createLineWebhookSpool(options) {
1299
1382
  let stopTask;
1300
1383
  return {
1301
1384
  accept: async (body) => {
1302
- const events = body.events ?? [];
1303
- if (events.length === 0) return;
1304
- await monitor.admitBatch(events.map((event) => ({
1385
+ const events = (body.events ?? []).filter((event) => event.mode !== "standby");
1386
+ if (events.length === 0) return "ignored";
1387
+ return (await monitor.admitBatch(events.map((event) => ({
1305
1388
  event,
1306
1389
  destination: body.destination ?? ""
1307
- })), { receivedAt: Date.now() });
1390
+ })), { receivedAt: Date.now() })).some((admission) => admission.kind === "durable") ? "durable" : "ignored";
1308
1391
  },
1309
1392
  start: () => {
1310
1393
  if (!stopTask) monitor.start();
@@ -1331,9 +1414,17 @@ function createLineWebhookSpool(options) {
1331
1414
  const DEFAULT_MEDIA_MAX_MB = 10;
1332
1415
  function createLineBot(opts) {
1333
1416
  const runtime = opts.runtime ?? createNonExitingRuntime();
1334
- const cfg = opts.config ?? getRuntimeConfig();
1417
+ const startupConfig = opts.config ?? getRuntimeConfig();
1418
+ const startupRuntimeConfig = getRuntimeConfigSnapshot();
1419
+ const startupRuntimeSourceConfig = getRuntimeConfigSourceSnapshot();
1420
+ const followsRuntimeConfig = opts.config === void 0 || startupRuntimeConfig === startupConfig || startupRuntimeSourceConfig !== null && selectApplicableRuntimeConfig({
1421
+ inputConfig: startupConfig,
1422
+ runtimeConfig: startupRuntimeConfig,
1423
+ runtimeSourceConfig: startupRuntimeSourceConfig
1424
+ }) === startupRuntimeConfig;
1425
+ const resolveTurnConfig = () => (followsRuntimeConfig ? getRuntimeConfigSnapshot() : void 0) ?? startupConfig;
1335
1426
  const account = resolveLineAccount({
1336
- cfg,
1427
+ cfg: startupConfig,
1337
1428
  accountId: opts.accountId
1338
1429
  });
1339
1430
  const mediaMaxBytes = ([opts.mediaMaxMb, account.config.mediaMaxMb].find((value) => typeof value === "number" && value > 0) ?? DEFAULT_MEDIA_MAX_MB) * 1024 * 1024;
@@ -1344,17 +1435,20 @@ function createLineBot(opts) {
1344
1435
  const spool = createLineWebhookSpool({
1345
1436
  accountId: account.accountId,
1346
1437
  runtime,
1347
- deliver: async (event, _destination, control) => await handleLineWebhookEvents([event], {
1348
- cfg,
1349
- account,
1350
- runtime,
1351
- buildContext: opts.buildContext,
1352
- mediaMaxBytes,
1353
- processMessage,
1354
- ...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
1355
- groupHistories,
1356
- historyLimit: cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
1357
- })
1438
+ deliver: async (event, _destination, control) => {
1439
+ const cfg = resolveTurnConfig();
1440
+ await handleLineWebhookEvents([event], {
1441
+ cfg,
1442
+ account,
1443
+ runtime,
1444
+ buildContext: opts.buildContext,
1445
+ mediaMaxBytes,
1446
+ processMessage,
1447
+ ...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
1448
+ groupHistories,
1449
+ historyLimit: account.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
1450
+ });
1451
+ }
1358
1452
  });
1359
1453
  spool.start();
1360
1454
  return {
@@ -1380,7 +1474,8 @@ function resolveLineDurableReplyOptions(params) {
1380
1474
  //#endregion
1381
1475
  //#region extensions/line/src/signature.ts
1382
1476
  function validateLineSignature(body, signature, channelSecret) {
1383
- return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
1477
+ const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
1478
+ return safeEqualSecret(signature, hash);
1384
1479
  }
1385
1480
  //#endregion
1386
1481
  //#region extensions/line/src/webhook-utils.ts
@@ -1393,8 +1488,8 @@ function parseLineWebhookBody(rawBody) {
1393
1488
  }
1394
1489
  //#endregion
1395
1490
  //#region extensions/line/src/webhook-node.ts
1396
- const LINE_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
1397
- const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 64 * 1024;
1491
+ const LINE_WEBHOOK_MAX_BODY_BYTES = 1048576;
1492
+ const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 65536;
1398
1493
  const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1 = 5e3;
1399
1494
  async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1) {
1400
1495
  return await readRequestBodyWithLimit(req, {
@@ -1482,7 +1577,7 @@ function createLineNodeWebhookHandler(params) {
1482
1577
  //#endregion
1483
1578
  //#region extensions/line/src/monitor.ts
1484
1579
  const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
1485
- const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 64 * 1024;
1580
+ const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
1486
1581
  const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
1487
1582
  async function registerLineWebhookTarget(params, bot) {
1488
1583
  try {
@@ -1530,20 +1625,22 @@ async function monitorLineProvider(opts) {
1530
1625
  onMessage: async (ctx, deliveryControl) => {
1531
1626
  if (!ctx) return;
1532
1627
  const { ctxPayload, replyToken, route } = ctx;
1533
- const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
1534
- const displayNamePromise = ctx.userId ? getUserDisplayName(ctx.userId, {
1535
- cfg: config,
1536
- accountId: ctx.accountId
1537
- }) : Promise.resolve(ctxPayload.From);
1538
- const stopLoading = shouldShowLoading ? startLineLoadingKeepalive({
1539
- cfg: config,
1628
+ const turnConfig = deliveryControl.cfg;
1629
+ const stopLoading = Boolean(ctx.userId && !ctx.isGroup) ? startLineLoadingKeepalive({
1630
+ cfg: turnConfig,
1540
1631
  userId: ctx.userId,
1541
1632
  accountId: ctx.accountId
1542
1633
  }) : null;
1543
- logVerbose(`line: received message from ${await displayNamePromise} (${ctxPayload.From})`);
1634
+ logVerbose(`line: received message from ${ctxPayload.SenderName ?? ctx.userId ?? ctxPayload.From} (${ctxPayload.From})`);
1544
1635
  let replyTokenUsed = false;
1545
1636
  let turnAdopted = false;
1546
1637
  const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
1638
+ const turnAbortSignal = ingressLifecycle?.abortSignal;
1639
+ const skillFilter = ctx.skillFilter;
1640
+ const replyOptions = turnAbortSignal || skillFilter ? {
1641
+ ...turnAbortSignal ? { abortSignal: turnAbortSignal } : {},
1642
+ ...skillFilter ? { skillFilter } : {}
1643
+ } : void 0;
1547
1644
  try {
1548
1645
  const textLimit = 5e3;
1549
1646
  const turnResult = await getLineRuntime().channel.inbound.run({
@@ -1564,7 +1661,7 @@ async function monitorLineProvider(opts) {
1564
1661
  rawText: ctxPayload.RawBody ?? ctxPayload.BodyForAgent ?? ""
1565
1662
  }),
1566
1663
  resolveTurn: () => ({
1567
- cfg: config,
1664
+ cfg: turnConfig,
1568
1665
  channel: "line",
1569
1666
  accountId: route.accountId,
1570
1667
  route: {
@@ -1574,8 +1671,10 @@ async function monitorLineProvider(opts) {
1574
1671
  ctxPayload,
1575
1672
  record: ctx.turn.record,
1576
1673
  replyPipeline: {},
1577
- ...ingressLifecycle?.abortSignal ? { replyOptions: { abortSignal: ingressLifecycle.abortSignal } } : {},
1674
+ dispatcherOptions: { humanDelay: resolveHumanDelayConfig(turnConfig, route.agentId) },
1675
+ ...replyOptions ? { replyOptions } : {},
1578
1676
  delivery: {
1677
+ preparePayload: prepareLineReplyPayload,
1579
1678
  durable: (payload, info) => resolveLineDurableReplyOptions({
1580
1679
  payload,
1581
1680
  infoKind: info.kind,
@@ -1586,7 +1685,7 @@ async function monitorLineProvider(opts) {
1586
1685
  deliver: async (payload) => {
1587
1686
  const lineData = payload.channelData?.line ?? {};
1588
1687
  if (ctx.userId && !ctx.isGroup) showLoadingAnimation(ctx.userId, {
1589
- cfg: config,
1688
+ cfg: turnConfig,
1590
1689
  accountId: ctx.accountId
1591
1690
  }).catch(() => {});
1592
1691
  const deliveryResult = await deliverLineAutoReply({
@@ -1596,14 +1695,13 @@ async function monitorLineProvider(opts) {
1596
1695
  replyToken,
1597
1696
  replyTokenUsed,
1598
1697
  accountId: ctx.accountId,
1599
- cfg: config,
1698
+ cfg: turnConfig,
1600
1699
  textLimit,
1601
1700
  deps: {
1602
1701
  buildTemplateMessageFromPayload,
1603
1702
  processLineMessage,
1604
1703
  chunkMarkdownText,
1605
1704
  replyMessageLine,
1606
- createQuickReplyItems,
1607
1705
  pushMessagesLine,
1608
1706
  createFlexMessage,
1609
1707
  buildMediaMessage: buildLineMediaMessage,
@@ -1624,7 +1722,8 @@ async function monitorLineProvider(opts) {
1624
1722
  })
1625
1723
  }
1626
1724
  });
1627
- if (!hasFinalInboundReplyDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1725
+ const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
1726
+ if (!hasFinalInboundReplyDispatch(dispatchResult)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1628
1727
  } catch (err) {
1629
1728
  runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
1630
1729
  if (turnAdopted || replyTokenUsed) throw new LineWebhookTerminalDeliveryError("LINE delivery failed after consuming the event reply token.", { cause: err });
@@ -1711,8 +1810,7 @@ async function monitorLineProvider(opts) {
1711
1810
  }
1712
1811
  if (body.events && body.events.length > 0) {
1713
1812
  logVerbose(`line: received ${body.events.length} webhook events`);
1714
- await match.target.bot.handleWebhook(body);
1715
- res.setHeader("x-openclaw-delivery-accepted", "durable");
1813
+ if (await match.target.bot.handleWebhook(body) === "durable") res.setHeader("x-openclaw-delivery-accepted", "durable");
1716
1814
  }
1717
1815
  res.statusCode = 200;
1718
1816
  res.setHeader("Content-Type", "application/json");