@openclaw/line 2026.9.1-beta.1 → 2026.9.1

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 (34) 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-pwvE48Wb.js +252 -0
  5. package/dist/{channel-CsQLcrsA.js → channel-CF2AeULJ.js} +111 -380
  6. package/dist/channel-plugin-api.js +1 -1
  7. package/dist/channel.runtime-BF4Wk2dM.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-BmZ4hhfC.js → markdown-to-line-B-sj-7II.js} +22 -23
  12. package/dist/{monitor-Bd9XDV1i.js → monitor-D0fHFXCi.js} +323 -234
  13. package/dist/monitor.runtime-Mgecav_D.js +2 -0
  14. package/dist/outbound.runtime-CGqyJ-Ug.js +3 -0
  15. package/dist/probe.runtime-CbtMgU4s.js +2 -0
  16. package/dist/rich-messages-DY1AGKAJ.js +742 -0
  17. package/dist/runtime-api.js +14 -15
  18. package/dist/{send-Dce0m539.js → send-CQQxpxPp.js} +142 -112
  19. package/dist/{send-receipt-CCPTalf9.js → send-retry-ChpfJ-LM.js} +326 -86
  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/email.mjs +1 -1
  24. package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
  25. package/node_modules/typebox/package.json +1 -1
  26. package/openclaw.plugin.json +16 -2
  27. package/package.json +5 -5
  28. package/dist/card-command-CmU47gD9.js +0 -230
  29. package/dist/channel.runtime-B83x7k_i.js +0 -4
  30. package/dist/media-control-cards-CksNB8Wk.js +0 -474
  31. package/dist/monitor.runtime-BCCa6qTX.js +0 -2
  32. package/dist/outbound.runtime-D4w0-Gkc.js +0 -3
  33. package/dist/probe-CM03wo6r.js +0 -28
  34. package/dist/probe.runtime-D13ZtXsd.js +0 -2
@@ -1,34 +1,36 @@
1
- import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BqEVsDoB.js";
2
- import { i as buildLineQuickReplyFallbackText, l as getLineRuntime, o as resolveLineGroupConfigEntry } from "./media-control-cards-CksNB8Wk.js";
3
- import { _ as hasLineSpecificMediaOptions, g as buildLineMediaMessage } from "./send-receipt-CCPTalf9.js";
4
- import { i as processLineMessage, s as buildTemplateMessageFromPayload } from "./markdown-to-line-BmZ4hhfC.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-Dce0m539.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-DY1AGKAJ.js";
3
+ import { n as findLineHttpError, r as resolveLineNonDispatchRetryable, t as explainLineRefusal, w as buildLineMediaMessage } from "./send-retry-ChpfJ-LM.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, o as buildTemplateMessageFromPayload } from "./markdown-to-line-B-sj-7II.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-CQQxpxPp.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";
16
+ import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
13
17
  import { firstDefined } from "openclaw/plugin-sdk/allow-from";
14
18
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
15
19
  import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
16
20
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
17
21
  import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal, warn } from "openclaw/plugin-sdk/runtime-env";
18
- 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
- import { canonicalizeWebhookRouteKey, isRequestBodyLimitError, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
27
- import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1, runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
27
+ import { canonicalizeWebhookRouteKey, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
28
+ import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, runDetachedWebhookWork, sendHttpRequestRejection } 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
@@ -292,14 +299,14 @@ async function deliverLineAutoReply(params) {
292
299
  ];
293
300
  if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
294
301
  type: "text",
295
- text: buildLineQuickReplyFallbackText(lineData.quickReplies)
302
+ text: buildLineQuickReplyFallbackText(quickReplyLabels)
296
303
  });
297
304
  if (hasQuickReplies) {
298
305
  const targetMessages = orderedDeliveryMessages?.length ? orderedDeliveryMessages : textMessages.length > 0 ? textMessages : richMediaMessages;
299
306
  const lastIndex = targetMessages.length - 1;
300
307
  targetMessages[lastIndex] = {
301
308
  ...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
302
- quickReply: deps.createQuickReplyItems(lineData.quickReplies)
309
+ quickReply: createLineQuickReply(quickReplyItems)
303
310
  };
304
311
  }
305
312
  const messages = hasQuickReplies ? [...richMediaMessages, ...orderedDeliveryMessages ?? textMessages] : [...orderedDeliveryMessages ?? textMessages, ...richMediaMessages];
@@ -308,25 +315,33 @@ async function deliverLineAutoReply(params) {
308
315
  } catch (err) {
309
316
  deliveryError ??= err;
310
317
  const failedSegment = typeof err === "object" && err !== null ? failedPushSegments.get(err) : void 0;
311
- const httpError = getLineHttpError(err);
318
+ const httpError = findLineHttpError(err);
312
319
  const retryCandidates = failedSegment ? [...failedSegment.allowFailedBatchTextRecovery ? failedSegment.failedBatch : [], ...failedSegment.unattemptedTail] : [];
313
320
  const retryTextMessages = retryCandidates.filter((message) => message.type === "text");
314
321
  const quickRepliesNeedCarrier = hasQuickReplies && retryCandidates.some((message) => "quickReply" in message);
315
322
  const retryMessages = retryTextMessages.length > 0 ? retryTextMessages : quickRepliesNeedCarrier ? [{
316
323
  type: "text",
317
- text: buildLineQuickReplyFallbackText(lineData.quickReplies),
318
- quickReply: deps.createQuickReplyItems(lineData.quickReplies)
324
+ text: buildLineQuickReplyFallbackText(quickReplyLabels),
325
+ quickReply: createLineQuickReply(quickReplyItems)
319
326
  }] : [];
320
- if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400) {
327
+ if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400 && resolveLineNonDispatchRetryable(err) !== void 0) {
321
328
  const lastRetryMessage = retryMessages.at(-1);
322
- if (quickRepliesNeedCarrier && lastRetryMessage && !lastRetryMessage.quickReply) lastRetryMessage.quickReply = deps.createQuickReplyItems(lineData.quickReplies);
329
+ if (quickRepliesNeedCarrier && lastRetryMessage && !lastRetryMessage.quickReply) lastRetryMessage.quickReply = createLineQuickReply(quickReplyItems);
323
330
  try {
324
331
  await sendLineMessages(retryMessages, false);
325
332
  } catch {}
326
333
  }
327
334
  }
328
335
  if (deliveryError !== void 0) {
329
- if (!visibleReplySent) throw toLineDeliveryError(deliveryError);
336
+ if (!visibleReplySent) {
337
+ const named = toLineDeliveryError(deliveryError);
338
+ const refusal = await explainLineRefusal({
339
+ error: named,
340
+ cfg: params.cfg,
341
+ accountId
342
+ });
343
+ throw refusal.reason !== named.message ? new Error(refusal.reason, { cause: deliveryError }) : named;
344
+ }
330
345
  return {
331
346
  status: "partial",
332
347
  replyTokenUsed,
@@ -341,6 +356,44 @@ async function deliverLineAutoReply(params) {
341
356
  };
342
357
  }
343
358
  //#endregion
359
+ //#region extensions/line/src/mentions.ts
360
+ function getLineMentionees(message) {
361
+ return message.type === "text" ? message.mention?.mentionees ?? [] : [];
362
+ }
363
+ function addressesLineBot(mentionee) {
364
+ return mentionee.type === "all" || mentionee.isSelf === true;
365
+ }
366
+ function isLineBotMentioned(message) {
367
+ return getLineMentionees(message).some(addressesLineBot);
368
+ }
369
+ function hasAnyLineMention(message) {
370
+ return getLineMentionees(message).length > 0;
371
+ }
372
+ /**
373
+ * Text to interpret as a command, with the mention that addressed the bot removed.
374
+ *
375
+ * LINE writes a mention as the plain channel display name, so no pattern can
376
+ * tell it apart from a member's name; `mentionees[].index`/`length` (UTF-16
377
+ * code units, the unit LINE counts text in) is the only authoritative marker.
378
+ * Leaving it in place makes a group `@<bot> /status` parse as prose, and group
379
+ * chats require the mention to reach the bot at all.
380
+ */
381
+ function resolveLineMentionStrippedText(message) {
382
+ const text = message.type === "text" ? message.text : "";
383
+ const spans = getLineMentionees(message).filter(addressesLineBot).map((mentionee) => ({
384
+ start: mentionee.index,
385
+ end: mentionee.index + mentionee.length
386
+ })).toSorted((left, right) => left.start - right.start);
387
+ let stripped = "";
388
+ let cursor = 0;
389
+ for (const span of spans) {
390
+ stripped += text.slice(cursor, span.start);
391
+ if (/\S$/u.test(stripped) && /^\S/u.test(text.slice(span.end))) stripped += " ";
392
+ cursor = Math.max(cursor, span.end);
393
+ }
394
+ return `${stripped}${text.slice(cursor)}`.trim();
395
+ }
396
+ //#endregion
344
397
  //#region extensions/line/src/bot-message-context.ts
345
398
  function getLineSourceInfo(source) {
346
399
  if (!source) return {
@@ -425,30 +478,23 @@ async function resolveLineInboundRoute(params) {
425
478
  route
426
479
  };
427
480
  }
428
- const STICKER_PACKAGES = {
429
- "1": "Moon & James",
430
- "2": "Cony & Brown",
431
- "3": "Brown & Friends",
432
- "4": "Moon Special",
433
- "789": "LINE Characters",
434
- "6136": "Cony's Happy Life",
435
- "6325": "Brown's Life",
436
- "6359": "Choco",
437
- "6362": "Sally",
438
- "6370": "Edward",
439
- "11537": "Cony",
440
- "11538": "Brown",
441
- "11539": "Moon"
442
- };
443
- function describeStickerKeywords(sticker) {
444
- const keywords = sticker.keywords;
445
- if (keywords && keywords.length > 0) return keywords.slice(0, 3).join(", ");
446
- const stickerText = sticker.text;
447
- if (stickerText) return stickerText;
448
- return "";
481
+ /**
482
+ * Describe a sticker from what its webhook actually carries: LINE sends up to
483
+ * 15 keywords for the sticker, and a message sticker also carries the sender's
484
+ * own text. The package name is not among those facts and cannot be derived
485
+ * from the package id, so it is not part of the description.
486
+ */
487
+ function describeLineSticker(sticker) {
488
+ const description = readNonEmptyStringPreservingWhitespace(sticker.text) ?? normalizeStringEntries(sticker.keywords ?? []).slice(0, 3).join(", ");
489
+ return description ? `[Sent a sticker: ${description}]` : "[Sent a sticker]";
490
+ }
491
+ function readLineTextMessageBody(message) {
492
+ let text = message.text;
493
+ 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)}`;
494
+ return text;
449
495
  }
450
496
  function extractMessageText(message) {
451
- if (message.type === "text") return message.text;
497
+ if (message.type === "text") return readLineTextMessageBody(message);
452
498
  if (message.type === "location") {
453
499
  const loc = message;
454
500
  return formatLocationText({
@@ -458,13 +504,7 @@ function extractMessageText(message) {
458
504
  address: loc.address
459
505
  }) ?? "";
460
506
  }
461
- if (message.type === "sticker") {
462
- const sticker = message;
463
- const packageName = STICKER_PACKAGES[sticker.packageId] ?? "sticker";
464
- const keywords = describeStickerKeywords(sticker);
465
- if (keywords) return `[Sent a ${packageName} sticker: ${keywords}]`;
466
- return `[Sent a ${packageName} sticker]`;
467
- }
507
+ if (message.type === "sticker") return describeLineSticker(message);
468
508
  return "";
469
509
  }
470
510
  function extractNativeMediaKind(message) {
@@ -478,9 +518,23 @@ function extractNativeMediaKind(message) {
478
518
  }
479
519
  async function finalizeLineInboundContext(params) {
480
520
  const senderId = params.source.userId ?? "unknown";
481
- const senderLabel = params.source.userId ? `user:${params.source.userId}` : "unknown";
482
- const conversationLabel = params.source.isGroup ? params.source.groupId ? `group:${params.source.groupId}` : params.source.roomId ? `room:${params.source.roomId}` : "unknown-group" : senderLabel;
521
+ const clientOpts = {
522
+ cfg: params.cfg,
523
+ accountId: params.account.accountId,
524
+ channelAccessToken: params.account.channelAccessToken
525
+ };
526
+ const [senderName, groupName] = await Promise.all([params.source.userId ? getUserProfile(params.source.userId, {
527
+ ...clientOpts,
528
+ groupId: params.source.groupId,
529
+ roomId: params.source.roomId
530
+ }).then((profile) => profile?.displayName) : void 0, params.source.groupId ? getLineGroupName(params.source.groupId, clientOpts) : void 0]);
531
+ const senderLabel = senderName ?? (params.source.userId ? `user:${params.source.userId}` : "unknown");
532
+ const conversationLabel = params.source.isGroup ? groupName ?? (params.source.groupId ? `group:${params.source.groupId}` : params.source.roomId ? `room:${params.source.roomId}` : "unknown-group") : senderLabel;
483
533
  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}`;
534
+ const groupConfig = params.source.isGroup ? resolveLineGroupConfigEntry(params.account.config.groups, {
535
+ groupId: params.source.groupId,
536
+ roomId: params.source.roomId
537
+ }) : void 0;
484
538
  const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
485
539
  cfg: params.cfg,
486
540
  agentId: params.route.agentId,
@@ -494,7 +548,10 @@ async function finalizeLineInboundContext(params) {
494
548
  timestamp: params.timestamp,
495
549
  body: agentBody,
496
550
  chatType: params.source.isGroup ? "group" : "direct",
497
- sender: { id: senderId },
551
+ sender: {
552
+ id: senderId,
553
+ name: senderName
554
+ },
498
555
  previousTimestamp,
499
556
  envelope: envelopeOptions
500
557
  });
@@ -505,7 +562,10 @@ async function finalizeLineInboundContext(params) {
505
562
  messageId: params.messageSid,
506
563
  timestamp: params.timestamp,
507
564
  from: address,
508
- sender: { id: senderId },
565
+ sender: {
566
+ id: senderId,
567
+ name: senderName
568
+ },
509
569
  conversation: {
510
570
  kind: params.source.isGroup ? "group" : "direct",
511
571
  id: params.source.peerId,
@@ -525,18 +585,18 @@ async function finalizeLineInboundContext(params) {
525
585
  body,
526
586
  bodyForAgent: agentBody,
527
587
  rawBody: params.rawBody,
528
- commandBody: params.rawBody,
588
+ commandBody: params.commandBody ?? params.rawBody,
529
589
  inboundHistory: params.inboundHistory
530
590
  },
531
- access: { commands: { authorized: params.commandAuthorized } },
591
+ access: {
592
+ commands: { authorized: params.commandAuthorized },
593
+ mentions: params.mentions
594
+ },
532
595
  media,
533
596
  extra: {
534
597
  ...params.locationContext,
535
- GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
536
- GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
537
- groupId: params.source.groupId,
538
- roomId: params.source.roomId
539
- })?.systemPrompt) : void 0
598
+ GroupSubject: params.source.isGroup ? groupName ?? params.source.groupId ?? params.source.roomId : void 0,
599
+ GroupSystemPrompt: normalizeOptionalString(groupConfig?.systemPrompt)
540
600
  }
541
601
  });
542
602
  const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
@@ -551,11 +611,13 @@ async function finalizeLineInboundContext(params) {
551
611
  if (shouldLogVerbose()) {
552
612
  const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
553
613
  const mediaInfo = params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1 ? ` mediaCount=${params.verboseLog.mediaCount}` : "";
554
- logVerbose(`${params.verboseLog.kind === "inbound" ? "line inbound" : "line postback"}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
614
+ const label = params.verboseLog.kind === "inbound" ? "line inbound" : "line postback";
615
+ logVerbose(`${label}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
555
616
  }
556
617
  return {
557
618
  ctxPayload,
558
619
  replyToken: params.event.replyToken,
620
+ skillFilter: groupConfig?.skills,
559
621
  turn: {
560
622
  storePath,
561
623
  record: {
@@ -626,15 +688,17 @@ async function buildLineMessageContext(params) {
626
688
  },
627
689
  rawBody,
628
690
  agentBody,
691
+ commandBody: resolveLineMentionStrippedText(message) || rawBody,
692
+ mentions: params.mentions,
629
693
  timestamp,
630
694
  messageSid: messageId,
631
695
  commandAuthorized,
632
- channelIngress: params.resolveChannelIngress ? await params.resolveChannelIngress({
696
+ channelIngress: await params.resolveChannelIngress?.({
633
697
  agentId: route.agentId,
634
698
  sessionKey: route.sessionKey,
635
699
  messageId,
636
700
  inboundEventKind: "user_request"
637
- }) : params.channelIngress,
701
+ }),
638
702
  buildContext: params.buildContext,
639
703
  media: mediaFacts,
640
704
  locationContext,
@@ -647,6 +711,7 @@ async function buildLineMessageContext(params) {
647
711
  return {
648
712
  ctxPayload: finalized.ctxPayload,
649
713
  turn: finalized.turn,
714
+ skillFilter: finalized.skillFilter,
650
715
  event,
651
716
  userId,
652
717
  groupId,
@@ -666,14 +731,18 @@ async function buildLinePostbackContext(params) {
666
731
  account
667
732
  });
668
733
  const timestamp = event.timestamp;
669
- const rawData = event.postback?.data?.trim() ?? "";
670
- if (!rawData) return null;
671
- let rawBody = rawData;
672
- if (rawData.includes("line.action=")) {
673
- const searchParams = new URLSearchParams(rawData);
734
+ const rawBody = event.postback?.data?.trim() ?? "";
735
+ if (!rawBody) return null;
736
+ let agentBody = rawBody;
737
+ if (rawBody.includes("line.action=")) {
738
+ const searchParams = new URLSearchParams(rawBody);
674
739
  const action = searchParams.get("line.action") ?? "";
675
740
  const device = searchParams.get("line.device");
676
- rawBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
741
+ agentBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
742
+ }
743
+ for (const [key, value] of Object.entries(event.postback.params ?? {}).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
744
+ const picked = normalizeOptionalString(value);
745
+ if (picked) agentBody += ` ${key}=${picked}`;
677
746
  }
678
747
  const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
679
748
  const finalized = await finalizeLineInboundContext({
@@ -689,15 +758,16 @@ async function buildLinePostbackContext(params) {
689
758
  peerId
690
759
  },
691
760
  rawBody,
761
+ agentBody,
692
762
  timestamp,
693
763
  messageSid,
694
764
  commandAuthorized,
695
- channelIngress: params.resolveChannelIngress ? await params.resolveChannelIngress({
765
+ channelIngress: await params.resolveChannelIngress?.({
696
766
  agentId: route.agentId,
697
767
  sessionKey: route.sessionKey,
698
768
  messageId: messageSid,
699
769
  inboundEventKind: "user_request"
700
- }) : params.channelIngress,
770
+ }),
701
771
  buildContext: params.buildContext,
702
772
  media: [],
703
773
  verboseLog: { kind: "postback" }
@@ -705,6 +775,7 @@ async function buildLinePostbackContext(params) {
705
775
  return {
706
776
  ctxPayload: finalized.ctxPayload,
707
777
  turn: finalized.turn,
778
+ skillFilter: finalized.skillFilter,
708
779
  event,
709
780
  userId,
710
781
  groupId,
@@ -764,12 +835,6 @@ function isDownloadableLineMessageType(messageType) {
764
835
  function normalizeLineIngressEntry(value) {
765
836
  return normalizeLineAllowEntry(value) || null;
766
837
  }
767
- function resolveLineGroupConfig(params) {
768
- return resolveLineGroupConfigEntry(params.config.groups, {
769
- groupId: params.groupId,
770
- roomId: params.roomId
771
- });
772
- }
773
838
  async function sendLinePairingReply(params) {
774
839
  const { senderId, replyToken, context } = params;
775
840
  const idLabel = (() => {
@@ -821,12 +886,11 @@ async function sendLinePairingReply(params) {
821
886
  }
822
887
  });
823
888
  }
824
- async function shouldProcessLineEvent(event, context) {
889
+ async function resolveLineEventAdmission(event, context) {
825
890
  const { cfg, account } = context;
826
891
  const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
827
892
  const senderId = userId ?? "";
828
- const groupConfig = resolveLineGroupConfig({
829
- config: account.config,
893
+ const groupConfig = resolveLineGroupConfigEntry(account.config.groups, {
830
894
  groupId,
831
895
  roomId
832
896
  });
@@ -841,11 +905,7 @@ async function shouldProcessLineEvent(event, context) {
841
905
  const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
842
906
  const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom));
843
907
  const mentionFacts = (() => {
844
- if (!isGroup || event.type !== "message") return {
845
- canDetectMention: false,
846
- wasMentioned: false,
847
- hasAnyMention: false
848
- };
908
+ if (!isGroup || event.type !== "message") return;
849
909
  const peerId = groupId ?? roomId ?? userId ?? "unknown";
850
910
  const { agentId } = resolveAgentRoute({
851
911
  cfg,
@@ -862,7 +922,9 @@ async function shouldProcessLineEvent(event, context) {
862
922
  return {
863
923
  canDetectMention: event.message.type === "text",
864
924
  wasMentioned: wasMentionedByNative || wasMentionedByPattern,
865
- hasAnyMention: hasAnyLineMention(event.message)
925
+ explicitlyMentionedBot: wasMentionedByNative,
926
+ hasAnyMention: hasAnyLineMention(event.message),
927
+ implicitMentionKinds: implicitMentionKindWhen("quoted_bot", quotesLineBotMessage(account.accountId, resolveLineQuotedMessageId(event.message)))
866
928
  };
867
929
  })();
868
930
  const resolveAccess = async (contextBinding) => await resolveStableChannelMessageIngress({
@@ -876,7 +938,7 @@ async function shouldProcessLineEvent(event, context) {
876
938
  },
877
939
  cfg,
878
940
  readStoreAllowFrom: async () => await readChannelAllowFromStore("line", void 0, account.accountId),
879
- subject: { stableId: senderId },
941
+ subject: event.type === "join" ? {} : { stableId: senderId },
880
942
  conversation: {
881
943
  kind: isGroup ? "group" : "direct",
882
944
  id: (groupId ?? roomId ?? senderId) || "unknown"
@@ -886,20 +948,20 @@ async function shouldProcessLineEvent(event, context) {
886
948
  id: "line:group-config",
887
949
  enabled: false
888
950
  } } : {},
889
- mentionFacts: isGroup && event.type === "message" ? {
890
- canDetectMention: mentionFacts.canDetectMention,
891
- wasMentioned: mentionFacts.wasMentioned,
892
- hasAnyMention: mentionFacts.hasAnyMention,
893
- implicitMentionKinds: []
894
- } : void 0,
895
- event: { kind: event.type === "postback" ? "postback" : "message" },
951
+ mentionFacts,
952
+ event: { kind: event.type === "join" ? "system" : event.type },
896
953
  dmPolicy,
897
954
  groupPolicy,
898
955
  policy: {
899
956
  groupAllowFromFallbackToAllowFrom: false,
900
957
  activation: {
901
958
  requireMention: isGroup && event.type === "message" && requireMention,
902
- allowTextCommands: true
959
+ allowTextCommands: true,
960
+ implicitMentions: resolveChannelImplicitMentions({
961
+ cfg,
962
+ channel: "line",
963
+ accountId: account.accountId
964
+ })
903
965
  }
904
966
  },
905
967
  allowFrom: normalizeStringEntries(account.config.allowFrom),
@@ -916,9 +978,18 @@ async function shouldProcessLineEvent(event, context) {
916
978
  accountId: account.accountId,
917
979
  log: (message) => logVerbose(message)
918
980
  });
919
- if (access.senderAccess.decision === "allow" && (access.ingress.admission === "dispatch" || access.ingress.admission === "observe" || access.ingress.admission === "skip")) return {
981
+ if (event.type === "join") return groupConfig?.enabled !== false && groupPolicy !== "disabled" && (groupPolicy !== "allowlist" || access.state.allowlists.group.hasMatchableEntries) ? {
920
982
  access,
921
983
  resolveBoundAccess: resolveAccess
984
+ } : null;
985
+ if (access.senderAccess.decision === "allow" && (access.ingress.admission === "dispatch" || access.ingress.admission === "observe" || access.ingress.admission === "skip")) return {
986
+ access,
987
+ resolveBoundAccess: resolveAccess,
988
+ mentions: mentionFacts ? {
989
+ ...mentionFacts,
990
+ wasMentioned: access.activationAccess.effectiveWasMentioned ?? mentionFacts.wasMentioned,
991
+ requireMention
992
+ } : void 0
922
993
  };
923
994
  if (access.senderAccess.decision === "allow") {
924
995
  logVerbose(`Blocked line event (${access.ingress.reasonCode})`);
@@ -964,21 +1035,13 @@ async function shouldProcessLineEvent(event, context) {
964
1035
  logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${account.config.dmPolicy ?? "pairing"})`);
965
1036
  return null;
966
1037
  }
967
- function getLineMentionees(message) {
968
- if (message.type !== "text") return [];
969
- const mentionees = message.mention?.mentionees;
970
- return Array.isArray(mentionees) ? mentionees : [];
971
- }
972
- function isLineBotMentioned(message) {
973
- return getLineMentionees(message).some((m) => m.isSelf === true || m.type === "all");
974
- }
975
- function hasAnyLineMention(message) {
976
- return getLineMentionees(message).length > 0;
1038
+ function resolveLineQuotedMessageId(message) {
1039
+ return message.type === "text" || message.type === "sticker" ? message.quotedMessageId : void 0;
977
1040
  }
978
1041
  function resolveEventRawText(event) {
979
1042
  if (event.type === "message") {
980
1043
  const msg = event.message;
981
- if (msg.type === "text") return msg.text;
1044
+ if (msg.type === "text") return readLineTextMessageBody(msg);
982
1045
  return "";
983
1046
  }
984
1047
  if (event.type === "postback") return event.postback?.data?.trim() ?? "";
@@ -987,24 +1050,34 @@ function resolveEventRawText(event) {
987
1050
  async function handleMessageEvent(event, context) {
988
1051
  const { cfg, account, runtime, mediaMaxBytes, processMessage } = context;
989
1052
  const message = event.message;
990
- const decision = await shouldProcessLineEvent(event, context);
1053
+ const decision = await resolveLineEventAdmission(event, context);
991
1054
  if (!decision) return;
992
1055
  const { isGroup, groupId, roomId } = getLineSourceInfo(event.source);
993
1056
  if (isGroup && decision.access.activationAccess.shouldSkip) {
994
- const rawText = message.type === "text" ? message.text : "";
1057
+ const rawText = message.type === "text" ? readLineTextMessageBody(message) : "";
995
1058
  const sourceInfo = getLineSourceInfo(event.source);
996
1059
  logVerbose(`line: skipping group message (requireMention, not mentioned)`);
997
1060
  const historyKey = groupId ?? roomId;
998
1061
  const senderId = sourceInfo.userId ?? "unknown";
999
- if (historyKey && context.groupHistories) createChannelHistoryWindow({ historyMap: context.groupHistories }).record({
1000
- historyKey,
1001
- limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
1002
- entry: {
1003
- sender: `user:${senderId}`,
1004
- body: rawText || `<${message.type}>`,
1005
- timestamp: event.timestamp
1006
- }
1007
- });
1062
+ if (historyKey && context.groupHistories) {
1063
+ const displayName = sourceInfo.userId ? await getUserDisplayName(sourceInfo.userId, {
1064
+ cfg,
1065
+ accountId: account.accountId,
1066
+ channelAccessToken: account.channelAccessToken,
1067
+ groupId,
1068
+ roomId
1069
+ }) : senderId;
1070
+ const sender = displayName === senderId ? senderId : `${displayName} (${senderId})`;
1071
+ createChannelHistoryWindow({ historyMap: context.groupHistories }).record({
1072
+ historyKey,
1073
+ limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
1074
+ entry: {
1075
+ sender,
1076
+ body: rawText || `<${message.type}>`,
1077
+ timestamp: event.timestamp
1078
+ }
1079
+ });
1080
+ }
1008
1081
  return;
1009
1082
  }
1010
1083
  const groupHistoryKey = isGroup ? groupId ?? roomId : void 0;
@@ -1023,7 +1096,8 @@ async function handleMessageEvent(event, context) {
1023
1096
  abortSignal?.throwIfAborted();
1024
1097
  allMedia.push({
1025
1098
  path: media.path,
1026
- contentType: media.contentType
1099
+ contentType: media.contentType,
1100
+ ...originalFilename ? { fileName: originalFilename } : {}
1027
1101
  });
1028
1102
  } catch (err) {
1029
1103
  if (abortSignal?.aborted) throw abortSignal.reason;
@@ -1043,13 +1117,17 @@ async function handleMessageEvent(event, context) {
1043
1117
  commandAuthorized: decision.access.commandAccess.authorized,
1044
1118
  resolveChannelIngress: decision.resolveBoundAccess,
1045
1119
  inboundHistory: historyReservation.inboundHistory,
1120
+ mentions: decision.mentions,
1046
1121
  buildContext: context.buildContext
1047
1122
  });
1048
1123
  if (!messageContext) {
1049
1124
  logVerbose("line: skipping empty message");
1050
1125
  return;
1051
1126
  }
1052
- await processMessage(messageContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
1127
+ await processMessage(messageContext, {
1128
+ cfg,
1129
+ ...context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {}
1130
+ });
1053
1131
  historyReservation.commit();
1054
1132
  } finally {
1055
1133
  historyReservation.release();
@@ -1063,9 +1141,42 @@ async function handleUnfollowEvent(event, _context) {
1063
1141
  const { userId } = getLineSourceInfo(event.source);
1064
1142
  logVerbose(`line: user ${userId ?? "unknown"} unfollowed`);
1065
1143
  }
1066
- async function handleJoinEvent(event, _context) {
1067
- const { groupId, roomId } = getLineSourceInfo(event.source);
1144
+ async function handleJoinEvent(event, context) {
1145
+ const { groupId, roomId, isGroup } = getLineSourceInfo(event.source);
1146
+ const conversationId = groupId ?? roomId;
1147
+ if (!isGroup || !conversationId) return;
1068
1148
  logVerbose(`line: bot joined ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
1149
+ const { cfg, account } = context;
1150
+ const roomAllowed = Boolean(await resolveLineEventAdmission(event, context));
1151
+ await reportChannelRoomJoin({
1152
+ cfg,
1153
+ channel: "line",
1154
+ accountId: account.accountId,
1155
+ conversationId,
1156
+ deliverTo: conversationId,
1157
+ route: resolveAgentRoute({
1158
+ cfg,
1159
+ channel: "line",
1160
+ accountId: account.accountId,
1161
+ peer: {
1162
+ kind: "group",
1163
+ id: conversationId
1164
+ }
1165
+ }),
1166
+ roomAllowed,
1167
+ resolveRoomContext: async () => {
1168
+ const roomContext = { historyUnavailable: true };
1169
+ const title = groupId ? await getLineGroupName(groupId, {
1170
+ cfg,
1171
+ accountId: account.accountId,
1172
+ channelAccessToken: account.channelAccessToken
1173
+ }) : void 0;
1174
+ return title ? {
1175
+ ...roomContext,
1176
+ title
1177
+ } : roomContext;
1178
+ }
1179
+ });
1069
1180
  }
1070
1181
  async function handleLeaveEvent(event, _context) {
1071
1182
  const { groupId, roomId } = getLineSourceInfo(event.source);
@@ -1074,7 +1185,7 @@ async function handleLeaveEvent(event, _context) {
1074
1185
  async function handlePostbackEvent(event, context) {
1075
1186
  const data = event.postback.data;
1076
1187
  logVerbose(`line: received postback: ${data}`);
1077
- const decision = await shouldProcessLineEvent(event, context);
1188
+ const decision = await resolveLineEventAdmission(event, context);
1078
1189
  if (!decision) return;
1079
1190
  const postbackContext = await buildLinePostbackContext({
1080
1191
  event,
@@ -1085,7 +1196,10 @@ async function handlePostbackEvent(event, context) {
1085
1196
  buildContext: context.buildContext
1086
1197
  });
1087
1198
  if (!postbackContext) return;
1088
- await context.processMessage(postbackContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
1199
+ await context.processMessage(postbackContext, {
1200
+ cfg: context.cfg,
1201
+ ...context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {}
1202
+ });
1089
1203
  }
1090
1204
  async function handleLineWebhookEvents(events, context) {
1091
1205
  let firstError;
@@ -1122,12 +1236,10 @@ async function handleLineWebhookEvent(event, context) {
1122
1236
  }
1123
1237
  //#endregion
1124
1238
  //#region extensions/line/src/webhook-spool.ts
1125
- const LINE_WEBHOOK_SPOOL_VERSION = 1;
1126
1239
  const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
1127
1240
  const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
1128
1241
  const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
1129
1242
  const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
1130
- const LineWebhookPayloadError = createChannelIngressError("LineWebhookPayloadError");
1131
1243
  var LineWebhookTerminalDeliveryError = class extends Error {
1132
1244
  constructor(message, options) {
1133
1245
  super(message, options);
@@ -1135,35 +1247,6 @@ var LineWebhookTerminalDeliveryError = class extends Error {
1135
1247
  this.name = "LineWebhookTerminalDeliveryError";
1136
1248
  }
1137
1249
  };
1138
- /** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
1139
- function eventIdFor(event) {
1140
- if (!event || typeof event !== "object") throw new LineWebhookPayloadError("LINE webhook event must be an object.");
1141
- const candidate = event;
1142
- if (candidate.type === "message") {
1143
- const messageId = normalizeNullableString(candidate.message?.id);
1144
- if (messageId) return `message:${messageId}`;
1145
- }
1146
- const webhookEventId = normalizeNullableString(candidate.webhookEventId);
1147
- if (webhookEventId) return `event:${webhookEventId}`;
1148
- throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
1149
- }
1150
- function laneKeyFor(event, eventId) {
1151
- if (!event || typeof event !== "object") return eventId;
1152
- const source = event.source;
1153
- if (source?.type === "group") {
1154
- const groupId = normalizeNullableString(source.groupId);
1155
- if (groupId) return `group:${groupId}`;
1156
- }
1157
- if (source?.type === "room") {
1158
- const roomId = normalizeNullableString(source.roomId);
1159
- if (roomId) return `room:${roomId}`;
1160
- }
1161
- if (source?.type === "user") {
1162
- const userId = normalizeNullableString(source.userId);
1163
- if (userId) return `user:${userId}`;
1164
- }
1165
- return eventId;
1166
- }
1167
1250
  function parseStoredEvent(rawEvent) {
1168
1251
  let event;
1169
1252
  try {
@@ -1203,7 +1286,7 @@ function createLineWebhookSpool(options) {
1203
1286
  };
1204
1287
  },
1205
1288
  payload: {
1206
- version: LINE_WEBHOOK_SPOOL_VERSION,
1289
+ version: 1,
1207
1290
  serialize: ({ event, destination }) => ({
1208
1291
  rawEvent: JSON.stringify(event),
1209
1292
  destination
@@ -1218,7 +1301,7 @@ function createLineWebhookSpool(options) {
1218
1301
  destination: body.destination
1219
1302
  }),
1220
1303
  decode: (payload) => {
1221
- if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
1304
+ if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError(LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE);
1222
1305
  return {
1223
1306
  version: payload.version,
1224
1307
  body: {
@@ -1227,7 +1310,7 @@ function createLineWebhookSpool(options) {
1227
1310
  }
1228
1311
  };
1229
1312
  },
1230
- createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? "LINE webhook spool payload is invalid." : "LINE webhook event identity changed after durable admission.")
1313
+ createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE : "LINE webhook event identity changed after durable admission.")
1231
1314
  },
1232
1315
  deliver: async ({ event, destination }, lifecycle) => {
1233
1316
  const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
@@ -1287,7 +1370,7 @@ function createLineWebhookSpool(options) {
1287
1370
  },
1288
1371
  resolveNonRetryableFailure: (error) => {
1289
1372
  if (error instanceof LineWebhookPayloadError) return {
1290
- reason: "invalid-event",
1373
+ reason: LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON,
1291
1374
  message: error.message
1292
1375
  };
1293
1376
  if (error instanceof LineWebhookTerminalDeliveryError) return {
@@ -1308,12 +1391,12 @@ function createLineWebhookSpool(options) {
1308
1391
  let stopTask;
1309
1392
  return {
1310
1393
  accept: async (body) => {
1311
- const events = body.events ?? [];
1312
- if (events.length === 0) return;
1313
- await monitor.admitBatch(events.map((event) => ({
1394
+ const events = (body.events ?? []).filter((event) => event.mode !== "standby");
1395
+ if (events.length === 0) return "ignored";
1396
+ return (await monitor.admitBatch(events.map((event) => ({
1314
1397
  event,
1315
1398
  destination: body.destination ?? ""
1316
- })), { receivedAt: Date.now() });
1399
+ })), { receivedAt: Date.now() })).some((admission) => admission.kind === "durable") ? "durable" : "ignored";
1317
1400
  },
1318
1401
  start: () => {
1319
1402
  if (!stopTask) monitor.start();
@@ -1340,9 +1423,17 @@ function createLineWebhookSpool(options) {
1340
1423
  const DEFAULT_MEDIA_MAX_MB = 10;
1341
1424
  function createLineBot(opts) {
1342
1425
  const runtime = opts.runtime ?? createNonExitingRuntime();
1343
- const cfg = opts.config ?? getRuntimeConfig();
1426
+ const startupConfig = opts.config ?? getRuntimeConfig();
1427
+ const startupRuntimeConfig = getRuntimeConfigSnapshot();
1428
+ const startupRuntimeSourceConfig = getRuntimeConfigSourceSnapshot();
1429
+ const followsRuntimeConfig = opts.config === void 0 || startupRuntimeConfig === startupConfig || startupRuntimeSourceConfig !== null && selectApplicableRuntimeConfig({
1430
+ inputConfig: startupConfig,
1431
+ runtimeConfig: startupRuntimeConfig,
1432
+ runtimeSourceConfig: startupRuntimeSourceConfig
1433
+ }) === startupRuntimeConfig;
1434
+ const resolveTurnConfig = () => (followsRuntimeConfig ? getRuntimeConfigSnapshot() : void 0) ?? startupConfig;
1344
1435
  const account = resolveLineAccount({
1345
- cfg,
1436
+ cfg: startupConfig,
1346
1437
  accountId: opts.accountId
1347
1438
  });
1348
1439
  const mediaMaxBytes = ([opts.mediaMaxMb, account.config.mediaMaxMb].find((value) => typeof value === "number" && value > 0) ?? DEFAULT_MEDIA_MAX_MB) * 1024 * 1024;
@@ -1353,17 +1444,20 @@ function createLineBot(opts) {
1353
1444
  const spool = createLineWebhookSpool({
1354
1445
  accountId: account.accountId,
1355
1446
  runtime,
1356
- deliver: async (event, _destination, control) => await handleLineWebhookEvents([event], {
1357
- cfg,
1358
- account,
1359
- runtime,
1360
- buildContext: opts.buildContext,
1361
- mediaMaxBytes,
1362
- processMessage,
1363
- ...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
1364
- groupHistories,
1365
- historyLimit: account.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
1366
- })
1447
+ deliver: async (event, _destination, control) => {
1448
+ const cfg = resolveTurnConfig();
1449
+ await handleLineWebhookEvents([event], {
1450
+ cfg,
1451
+ account,
1452
+ runtime,
1453
+ buildContext: opts.buildContext,
1454
+ mediaMaxBytes,
1455
+ processMessage,
1456
+ ...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
1457
+ groupHistories,
1458
+ historyLimit: account.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
1459
+ });
1460
+ }
1367
1461
  });
1368
1462
  spool.start();
1369
1463
  return {
@@ -1389,7 +1483,8 @@ function resolveLineDurableReplyOptions(params) {
1389
1483
  //#endregion
1390
1484
  //#region extensions/line/src/signature.ts
1391
1485
  function validateLineSignature(body, signature, channelSecret) {
1392
- return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
1486
+ const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
1487
+ return safeEqualSecret(signature, hash);
1393
1488
  }
1394
1489
  //#endregion
1395
1490
  //#region extensions/line/src/webhook-utils.ts
@@ -1402,15 +1497,28 @@ function parseLineWebhookBody(rawBody) {
1402
1497
  }
1403
1498
  //#endregion
1404
1499
  //#region extensions/line/src/webhook-node.ts
1405
- const LINE_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
1406
- const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 64 * 1024;
1500
+ const LINE_WEBHOOK_MAX_BODY_BYTES = 1048576;
1501
+ const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 65536;
1407
1502
  const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1 = 5e3;
1408
1503
  async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1) {
1409
1504
  return await readRequestBodyWithLimit(req, {
1410
1505
  maxBytes,
1411
- timeoutMs
1506
+ timeoutMs,
1507
+ destroyOnLimit: false
1412
1508
  });
1413
1509
  }
1510
+ /**
1511
+ * Answer a body-limit failure through the connection owner.
1512
+ *
1513
+ * The reader defers destruction for these two codes, so the connection is already fenced
1514
+ * and only the owner can still write: responding directly would race the teardown and LINE
1515
+ * would see a reset instead of the status.
1516
+ */
1517
+ async function rejectLineWebhookRequest(req, res, error) {
1518
+ if (!isRequestBodyLimitError(error, "PAYLOAD_TOO_LARGE") && !isRequestBodyLimitError(error, "REQUEST_BODY_TIMEOUT")) return false;
1519
+ await sendHttpRequestRejection(req, res, error.statusCode, JSON.stringify({ error: requestBodyErrorToText(error.code) }), "application/json");
1520
+ return true;
1521
+ }
1414
1522
  function createLineNodeWebhookHandler(params) {
1415
1523
  const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
1416
1524
  const readBody = params.readBody ?? readLineWebhookRequestBody;
@@ -1467,18 +1575,7 @@ function createLineNodeWebhookHandler(params) {
1467
1575
  res.setHeader("Content-Type", "application/json");
1468
1576
  res.end(JSON.stringify({ status: "ok" }));
1469
1577
  } catch (err) {
1470
- if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
1471
- res.statusCode = 413;
1472
- res.setHeader("Content-Type", "application/json");
1473
- res.end(JSON.stringify({ error: "Payload too large" }));
1474
- return;
1475
- }
1476
- if (isRequestBodyLimitError$1(err, "REQUEST_BODY_TIMEOUT")) {
1477
- res.statusCode = 408;
1478
- res.setHeader("Content-Type", "application/json");
1479
- res.end(JSON.stringify({ error: requestBodyErrorToText$1("REQUEST_BODY_TIMEOUT") }));
1480
- return;
1481
- }
1578
+ if (await rejectLineWebhookRequest(req, res, err)) return;
1482
1579
  params.runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
1483
1580
  if (!res.headersSent) {
1484
1581
  res.statusCode = 500;
@@ -1491,7 +1588,7 @@ function createLineNodeWebhookHandler(params) {
1491
1588
  //#endregion
1492
1589
  //#region extensions/line/src/monitor.ts
1493
1590
  const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
1494
- const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 64 * 1024;
1591
+ const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
1495
1592
  const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
1496
1593
  async function registerLineWebhookTarget(params, bot) {
1497
1594
  try {
@@ -1539,20 +1636,22 @@ async function monitorLineProvider(opts) {
1539
1636
  onMessage: async (ctx, deliveryControl) => {
1540
1637
  if (!ctx) return;
1541
1638
  const { ctxPayload, replyToken, route } = ctx;
1542
- const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
1543
- const displayNamePromise = ctx.userId ? getUserDisplayName(ctx.userId, {
1544
- cfg: config,
1545
- accountId: ctx.accountId
1546
- }) : Promise.resolve(ctxPayload.From);
1547
- const stopLoading = shouldShowLoading ? startLineLoadingKeepalive({
1548
- cfg: config,
1639
+ const turnConfig = deliveryControl.cfg;
1640
+ const stopLoading = Boolean(ctx.userId && !ctx.isGroup) ? startLineLoadingKeepalive({
1641
+ cfg: turnConfig,
1549
1642
  userId: ctx.userId,
1550
1643
  accountId: ctx.accountId
1551
1644
  }) : null;
1552
- logVerbose(`line: received message from ${await displayNamePromise} (${ctxPayload.From})`);
1645
+ logVerbose(`line: received message from ${ctxPayload.SenderName ?? ctx.userId ?? ctxPayload.From} (${ctxPayload.From})`);
1553
1646
  let replyTokenUsed = false;
1554
1647
  let turnAdopted = false;
1555
1648
  const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
1649
+ const turnAbortSignal = ingressLifecycle?.abortSignal;
1650
+ const skillFilter = ctx.skillFilter;
1651
+ const replyOptions = turnAbortSignal || skillFilter ? {
1652
+ ...turnAbortSignal ? { abortSignal: turnAbortSignal } : {},
1653
+ ...skillFilter ? { skillFilter } : {}
1654
+ } : void 0;
1556
1655
  try {
1557
1656
  const textLimit = 5e3;
1558
1657
  const turnResult = await getLineRuntime().channel.inbound.run({
@@ -1573,7 +1672,7 @@ async function monitorLineProvider(opts) {
1573
1672
  rawText: ctxPayload.RawBody ?? ctxPayload.BodyForAgent ?? ""
1574
1673
  }),
1575
1674
  resolveTurn: () => ({
1576
- cfg: config,
1675
+ cfg: turnConfig,
1577
1676
  channel: "line",
1578
1677
  accountId: route.accountId,
1579
1678
  route: {
@@ -1583,8 +1682,10 @@ async function monitorLineProvider(opts) {
1583
1682
  ctxPayload,
1584
1683
  record: ctx.turn.record,
1585
1684
  replyPipeline: {},
1586
- ...ingressLifecycle?.abortSignal ? { replyOptions: { abortSignal: ingressLifecycle.abortSignal } } : {},
1685
+ dispatcherOptions: { humanDelay: resolveHumanDelayConfig(turnConfig, route.agentId) },
1686
+ ...replyOptions ? { replyOptions } : {},
1587
1687
  delivery: {
1688
+ preparePayload: prepareLineReplyPayload,
1588
1689
  durable: (payload, info) => resolveLineDurableReplyOptions({
1589
1690
  payload,
1590
1691
  infoKind: info.kind,
@@ -1595,7 +1696,7 @@ async function monitorLineProvider(opts) {
1595
1696
  deliver: async (payload) => {
1596
1697
  const lineData = payload.channelData?.line ?? {};
1597
1698
  if (ctx.userId && !ctx.isGroup) showLoadingAnimation(ctx.userId, {
1598
- cfg: config,
1699
+ cfg: turnConfig,
1599
1700
  accountId: ctx.accountId
1600
1701
  }).catch(() => {});
1601
1702
  const deliveryResult = await deliverLineAutoReply({
@@ -1605,14 +1706,13 @@ async function monitorLineProvider(opts) {
1605
1706
  replyToken,
1606
1707
  replyTokenUsed,
1607
1708
  accountId: ctx.accountId,
1608
- cfg: config,
1709
+ cfg: turnConfig,
1609
1710
  textLimit,
1610
1711
  deps: {
1611
1712
  buildTemplateMessageFromPayload,
1612
1713
  processLineMessage,
1613
1714
  chunkMarkdownText,
1614
1715
  replyMessageLine,
1615
- createQuickReplyItems,
1616
1716
  pushMessagesLine,
1617
1717
  createFlexMessage,
1618
1718
  buildMediaMessage: buildLineMediaMessage,
@@ -1633,7 +1733,8 @@ async function monitorLineProvider(opts) {
1633
1733
  })
1634
1734
  }
1635
1735
  });
1636
- if (!hasFinalInboundReplyDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1736
+ const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
1737
+ if (!hasFinalInboundReplyDispatch(dispatchResult)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1637
1738
  } catch (err) {
1638
1739
  runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
1639
1740
  if (turnAdopted || replyTokenUsed) throw new LineWebhookTerminalDeliveryError("LINE delivery failed after consuming the event reply token.", { cause: err });
@@ -1720,25 +1821,13 @@ async function monitorLineProvider(opts) {
1720
1821
  }
1721
1822
  if (body.events && body.events.length > 0) {
1722
1823
  logVerbose(`line: received ${body.events.length} webhook events`);
1723
- await match.target.bot.handleWebhook(body);
1724
- res.setHeader("x-openclaw-delivery-accepted", "durable");
1824
+ if (await match.target.bot.handleWebhook(body) === "durable") res.setHeader("x-openclaw-delivery-accepted", "durable");
1725
1825
  }
1726
1826
  res.statusCode = 200;
1727
1827
  res.setHeader("Content-Type", "application/json");
1728
1828
  res.end(JSON.stringify({ status: "ok" }));
1729
1829
  } catch (err) {
1730
- if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1731
- res.statusCode = 413;
1732
- res.setHeader("Content-Type", "application/json");
1733
- res.end(JSON.stringify({ error: "Payload too large" }));
1734
- return;
1735
- }
1736
- if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
1737
- res.statusCode = 408;
1738
- res.setHeader("Content-Type", "application/json");
1739
- res.end(JSON.stringify({ error: requestBodyErrorToText("REQUEST_BODY_TIMEOUT") }));
1740
- return;
1741
- }
1830
+ if (await rejectLineWebhookRequest(req, res, err)) return;
1742
1831
  runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
1743
1832
  if (!res.headersSent) {
1744
1833
  res.statusCode = 500;