@openclaw/line 2026.9.6 → 2026.9.8

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 (77) hide show
  1. package/assets/icon.png +0 -0
  2. package/dist/.setup/{accounts-BBEFMYbY.mjs → accounts-DOiM8-y3.mjs} +1 -4
  3. package/dist/.setup/api-4RgpQmHW.mjs +3 -0
  4. package/dist/.setup/{api-D_06cuHl.mjs → api-w4QdlCCB.mjs} +2 -2
  5. package/dist/.setup/{card-command-710u8FBH.mjs → card-command-DYf4gnmI.mjs} +25 -33
  6. package/dist/.setup/{channel-kc3VRglA.mjs → channel-DN6Za8Vm.mjs} +789 -26
  7. package/dist/.setup/channel.runtime-bd5vGeSo.mjs +4 -0
  8. package/dist/.setup/{monitor-morJPcPd.mjs → monitor-C01hAm3f.mjs} +262 -333
  9. package/dist/.setup/monitor.runtime-DwH2zK48.mjs +2 -0
  10. package/dist/.setup/outbound.runtime-D_Gs99Dm.mjs +3 -0
  11. package/dist/.setup/probe.runtime-Cj08-BZ1.mjs +2 -0
  12. package/dist/.setup/runtime-V7yh9OeC.mjs +8 -0
  13. package/dist/.setup/{send-BmsHY4WC.mjs → send-DOY1WTxv.mjs} +7 -26
  14. package/dist/.setup/{send-retry-DbfiHBPd.mjs → send-retry-DV6I0muq.mjs} +104 -208
  15. package/dist/.setup/{setup-surface-CcfKJKtr.mjs → setup-surface-CbfkmkIS.mjs} +15 -4
  16. package/dist/.setup/{markdown-to-line-CRfEHFs0.mjs → template-messages-CxCnYsWN.mjs} +168 -189
  17. package/dist/api.js +2 -2
  18. package/dist/channel-plugin-api.js +1 -1
  19. package/dist/contract-api.js +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/runtime-api.js +2 -260
  22. package/dist/setup-api.js +1 -1
  23. package/dist/setup-entry.js +1 -1
  24. package/node_modules/@line/bot-sdk/node_modules/@types/node/README.md +1 -1
  25. package/node_modules/@line/bot-sdk/node_modules/@types/node/child_process.d.ts +1 -1
  26. package/node_modules/@line/bot-sdk/node_modules/@types/node/package.json +2 -2
  27. package/node_modules/typebox/build/compile/code.mjs +1 -2
  28. package/node_modules/typebox/build/compile/validator.mjs +5 -1
  29. package/node_modules/typebox/build/format/iri.mjs +16 -9
  30. package/node_modules/typebox/build/guard/emit.mjs +2 -2
  31. package/node_modules/typebox/build/guard/guard.d.mts +7 -5
  32. package/node_modules/typebox/build/guard/guard.mjs +14 -9
  33. package/node_modules/typebox/build/guard/index.mjs +6 -0
  34. package/node_modules/typebox/build/guard/unicode/unicode.d.mts +6 -0
  35. package/node_modules/typebox/build/guard/unicode/unicode.mjs +35 -0
  36. package/node_modules/typebox/build/guard/{string.mjs → unicode/unicode_segment.mjs} +16 -19
  37. package/node_modules/typebox/build/schema/build.mjs +2 -3
  38. package/node_modules/typebox/build/schema/engine/_pathing.d.mts +1 -0
  39. package/node_modules/typebox/build/schema/engine/_pathing.mjs +11 -0
  40. package/node_modules/typebox/build/schema/engine/_refine.mjs +13 -5
  41. package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +2 -1
  42. package/node_modules/typebox/build/schema/engine/dependencies.mjs +2 -1
  43. package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +2 -1
  44. package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +1 -1
  45. package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +2 -2
  46. package/node_modules/typebox/build/schema/engine/format.mjs +9 -2
  47. package/node_modules/typebox/build/schema/engine/patternProperties.mjs +3 -2
  48. package/node_modules/typebox/build/schema/engine/properties.mjs +3 -2
  49. package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -1
  50. package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +1 -1
  51. package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +2 -2
  52. package/node_modules/typebox/build/schema/engine/ref.d.mts +1 -1
  53. package/node_modules/typebox/build/schema/engine/ref.mjs +2 -2
  54. package/node_modules/typebox/build/schema/engine/schema.mjs +24 -24
  55. package/node_modules/typebox/build/schema/parse.mjs +3 -0
  56. package/node_modules/typebox/build/schema/pointer/pointer.d.mts +1 -6
  57. package/node_modules/typebox/build/schema/pointer/pointer.mjs +47 -64
  58. package/node_modules/typebox/build/system/settings/internal.d.mts +2 -0
  59. package/node_modules/typebox/build/system/settings/internal.mjs +20 -0
  60. package/node_modules/typebox/build/system/settings/settings.d.mts +11 -0
  61. package/node_modules/typebox/build/system/settings/settings.mjs +14 -0
  62. package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
  63. package/node_modules/typebox/build/type/extends/inference.mjs +15 -2
  64. package/node_modules/typebox/build/type/extends/tuple.d.mts +2 -1
  65. package/node_modules/typebox/build/type/extends/tuple.mjs +21 -1
  66. package/node_modules/typebox/build/value/parse/parse.mjs +10 -4
  67. package/node_modules/typebox/package.json +1 -1
  68. package/node_modules/typebox/readme.md +3 -1
  69. package/package.json +5 -5
  70. package/dist/.setup/account-helpers-BSUyZwLm.mjs +0 -12
  71. package/dist/.setup/api-RdEuQgW-.mjs +0 -3
  72. package/dist/.setup/channel.runtime-BtkeDELb.mjs +0 -4
  73. package/dist/.setup/monitor.runtime-BGhxYDKG.mjs +0 -2
  74. package/dist/.setup/outbound.runtime-QGvZy8Em.mjs +0 -3
  75. package/dist/.setup/probe.runtime-CMf8b-jB.mjs +0 -2
  76. package/dist/.setup/rich-messages-d8CmBVJj.mjs +0 -872
  77. /package/node_modules/typebox/build/guard/{string.d.mts → unicode/unicode_segment.d.mts} +0 -0
@@ -1,12 +1,15 @@
1
- import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BBEFMYbY.mjs";
2
- import { b as normalizeAllowFrom, c as resolveLineQuestionPostback, f as buildLineQuickReplyFallbackText, g as resolveLineGroupConfigEntry, i as prepareLineReplyPayload, n as createLineQuickReply, p as getLineRuntime, s as parseLineQuestionPostbackData, x as normalizeLineAllowEntry, y as firstDefined } from "./rich-messages-d8CmBVJj.mjs";
3
- import { D as recordLineQuoteToken, E as readLineQuoteToken, O as reportLineQuoteCarrierMissing, T as canCarryLineQuoteToken, j as buildLineMediaMessage, k as resolveLineQuoteToken, n as findLineHttpError, r as resolveLineNonDispatchRetryable, t as explainLineRefusal, w as applyLineQuoteToken } from "./send-retry-DbfiHBPd.mjs";
1
+ import { n as resolveDefaultLineAccountId, r as resolveLineAccount } from "./accounts-DOiM8-y3.mjs";
2
+ import { a as resolveLineQuestionPostback, c as normalizeLineAllowEntry, i as parseLineQuestionPostbackData, n as createLineQuickReply, o as buildLineQuickReplyFallbackText, r as prepareLineReplyPayload, s as resolveLineGroupConfigEntry } from "./channel-DN6Za8Vm.mjs";
3
+ import { t as getLineRuntime } from "./runtime-V7yh9OeC.mjs";
4
+ import { D as recordLineQuoteToken, E as readLineQuoteToken, O as reportLineQuoteCarrierMissing, T as canCarryLineQuoteToken, j as buildLineMediaMessage, k as resolveLineQuoteToken, n as findLineHttpError, r as resolveLineNonDispatchRetryable, t as explainLineRefusal, w as applyLineQuoteToken } from "./send-retry-DV6I0muq.mjs";
4
5
  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.mjs";
5
- import { a as buildTemplateMessageFromPayload, r as processLineMessage } from "./markdown-to-line-CRfEHFs0.mjs";
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-BmsHY4WC.mjs";
6
+ import { a as getUserDisplayName, g as quotesLineBotMessage, h as showLoadingAnimation, i as getLineGroupName, l as pushMessageLine, n as createLocationMessage, o as getUserProfile, p as replyMessageLine, t as createFlexMessage, u as pushMessagesLine } from "./send-DOY1WTxv.mjs";
7
+ import { n as processLineMessage, t as buildTemplateMessageFromPayload } from "./template-messages-CxCnYsWN.mjs";
7
8
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
8
9
  import { resolveChannelGroupsConfigPath } from "openclaw/plugin-sdk/channel-policy";
9
10
  import { normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
11
+ import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
12
+ import { firstDefined } from "openclaw/plugin-sdk/allow-from";
10
13
  import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
11
14
  import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, implicitMentionKindWhen, isChannelPartialDeliveryError, logInboundDrop, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
12
15
  import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, readErrorName, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
@@ -16,133 +19,29 @@ import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForA
16
19
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
17
20
  import { classifyTransientNetworkErrorCode } from "openclaw/plugin-sdk/retry-runtime";
18
21
  import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
19
- import { setTimeout as setTimeout$1 } from "node:timers/promises";
20
- import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
21
- import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
22
+ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
22
23
  import crypto from "node:crypto";
23
24
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
24
25
  import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
25
26
  import { channelReadyPatch, channelStoppedPatch } from "openclaw/plugin-sdk/gateway-runtime";
26
27
  import { canonicalizeWebhookRouteKey, registerWebhookTargetWithPluginRoute, resolveSingleWebhookTarget, resolveWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
27
28
  import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, runDetachedWebhookWork, sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
28
- import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
29
29
  import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
30
30
  import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
31
31
  import { getRuntimeConfig, getRuntimeConfigSnapshot, getRuntimeConfigSourceSnapshot, selectApplicableRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
32
32
  import { fanInChannelIngressLifecycles, resolveChannelImplicitMentions } from "openclaw/plugin-sdk/channel-ingress-runtime";
33
33
  import { reportChannelRoomJoin } from "openclaw/plugin-sdk/channel-join-intro-runtime";
34
34
  import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
35
- import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
35
+ import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
36
36
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
37
- import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
37
+ import { parseAgentSessionKey, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
38
38
  import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
39
+ import { getSessionBindingService, inspectRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-binding-runtime";
40
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
41
+ import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
42
+ import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
39
43
  import { enqueueKeyedTask } from "openclaw/plugin-sdk/keyed-async-queue";
40
44
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
41
- //#region extensions/line/src/download.ts
42
- const CONTENT_READY_MAX_ATTEMPTS = 6;
43
- const CONTENT_READY_BASE_DELAY_MS = 500;
44
- const CONTENT_READY_MAX_DELAY_MS = 4e3;
45
- const CONTENT_DOWNLOAD_TIMEOUT_MS = 15e3;
46
- const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
47
- var RetryableLineMediaFetchError = class extends MediaFetchError {
48
- constructor(message, options) {
49
- super("fetch_failed", message, options);
50
- this.name = "RetryableLineMediaFetchError";
51
- }
52
- };
53
- function contentBackoffDelayMs(attempt) {
54
- return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
55
- }
56
- function lineContentUrl(messageId) {
57
- return `${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`;
58
- }
59
- async function* lineResponseBodyChunks(response, messageId, signal, onComplete) {
60
- const body = response.body;
61
- if (!body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
62
- const reader = body.getReader();
63
- let completed = false;
64
- try {
65
- while (true) {
66
- let chunk;
67
- try {
68
- chunk = await reader.read();
69
- } catch (err) {
70
- if (signal.aborted) throw signal.reason;
71
- throw new RetryableLineMediaFetchError(`LINE media response stream failed for message ${messageId}`, { cause: err });
72
- }
73
- if (chunk.done) {
74
- completed = true;
75
- onComplete();
76
- return;
77
- }
78
- if (chunk.value.byteLength > 0) yield chunk.value;
79
- }
80
- } finally {
81
- if (!completed) await reader.cancel(signal.aborted ? signal.reason : void 0).catch(() => void 0);
82
- try {
83
- reader.releaseLock();
84
- } catch {}
85
- }
86
- }
87
- async function fetchLineContentWhenReady(messageId, channelAccessToken, signal) {
88
- try {
89
- for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
90
- const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
91
- headers: { Authorization: `Bearer ${channelAccessToken}` },
92
- redirect: "error",
93
- signal
94
- });
95
- if (response.status === 200) {
96
- if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
97
- return response;
98
- }
99
- await response.body?.cancel().catch(() => void 0);
100
- if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
101
- if (attempt < 5) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal });
102
- }
103
- } catch (err) {
104
- if (signal.aborted) throw signal.reason;
105
- if (err instanceof MediaFetchError) throw err;
106
- throw new RetryableLineMediaFetchError(`LINE media download failed for message ${messageId}`, { cause: err });
107
- }
108
- throw new MediaFetchError("http_error", `LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`, { status: 202 });
109
- }
110
- function isRetryableLineInboundMediaError(err) {
111
- if (err instanceof RetryableLineMediaFetchError) return true;
112
- if (!(err instanceof MediaFetchError)) return false;
113
- if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
114
- return false;
115
- }
116
- async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10485760, options) {
117
- options?.signal?.throwIfAborted();
118
- const deadlineAbort = new AbortController();
119
- const signal = options?.signal ? AbortSignal.any([options.signal, deadlineAbort.signal]) : deadlineAbort.signal;
120
- const deadline = setTimeout(() => {
121
- deadlineAbort.abort(new RetryableLineMediaFetchError(`LINE media for message ${messageId} did not download within ${CONTENT_DOWNLOAD_TIMEOUT_MS / 1e3} seconds`));
122
- }, CONTENT_DOWNLOAD_TIMEOUT_MS);
123
- deadline.unref();
124
- try {
125
- const response = await fetchLineContentWhenReady(messageId, channelAccessToken, signal);
126
- let saved;
127
- try {
128
- saved = await saveMediaStream(lineResponseBodyChunks(response, messageId, signal, () => clearTimeout(deadline)), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
129
- } catch (err) {
130
- await response.body?.cancel(signal.aborted ? signal.reason : err).catch(() => void 0);
131
- if (signal.aborted) throw signal.reason;
132
- throw err;
133
- }
134
- options?.signal?.throwIfAborted();
135
- logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
136
- return {
137
- path: saved.path,
138
- contentType: saved.contentType,
139
- size: saved.size
140
- };
141
- } finally {
142
- clearTimeout(deadline);
143
- }
144
- }
145
- //#endregion
146
45
  //#region extensions/line/src/auto-reply-delivery.ts
147
46
  function toLineDeliveryError(error) {
148
47
  return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
@@ -410,46 +309,73 @@ function buildPeerId(source) {
410
309
  if (source.type === "user" && source.userId) return source.userId;
411
310
  return "unknown";
412
311
  }
413
- async function resolveLineInboundRoute(params) {
414
- recordChannelActivity({
415
- channel: "line",
416
- accountId: params.account.accountId,
417
- direction: "inbound"
418
- });
312
+ async function prepareLineInboundRoute(params) {
419
313
  const { userId, groupId, roomId, isGroup } = getLineSourceInfo(params.source);
420
314
  const peerId = buildPeerId(params.source);
421
- let route = resolveAgentRoute({
422
- cfg: params.cfg,
315
+ const routeInput = {
423
316
  channel: "line",
424
317
  accountId: params.account.accountId,
425
318
  peer: {
426
319
  kind: isGroup ? "group" : "direct",
427
320
  id: peerId
428
321
  }
322
+ };
323
+ const conversation = {
324
+ channel: "line",
325
+ accountId: params.account.accountId,
326
+ conversationId: peerId
327
+ };
328
+ const inspection = await getSessionBindingService().inspectByConversationAsync(conversation);
329
+ if (inspection.status === "unavailable") throw new Error("LINE conversation binding owner is temporarily unavailable; retry the message.");
330
+ const resolveScopeRoute = (agentId) => resolveAgentRoute({
331
+ ...routeInput,
332
+ cfg: { session: params.cfg.session },
333
+ defaultAgentId: agentId
429
334
  });
430
- const configuredRoute = resolveConfiguredBindingRoute({
335
+ const selection = inspectRuntimeConversationBindingRoute({
336
+ route: resolveScopeRoute(),
337
+ inspection
338
+ });
339
+ const metadataAgentId = selection.bindingRecord?.metadata?.agentId;
340
+ const hasBoundAgent = selection.boundSessionKey && (parseAgentSessionKey(selection.boundSessionKey) || typeof metadataAgentId === "string" && metadataAgentId.trim());
341
+ const baseRoute = hasBoundAgent ? resolveScopeRoute(selection.boundAgentId) : resolveAgentRoute({
342
+ ...routeInput,
343
+ cfg: params.cfg
344
+ });
345
+ const configuredRoute = hasBoundAgent ? {
346
+ route: baseRoute,
347
+ bindingResolution: null
348
+ } : resolveConfiguredBindingRoute({
431
349
  cfg: params.cfg,
432
- route,
433
- conversation: {
434
- channel: "line",
435
- accountId: params.account.accountId,
436
- conversationId: peerId
437
- }
350
+ route: baseRoute,
351
+ conversation
438
352
  });
439
- let configuredBinding = configuredRoute.bindingResolution;
440
- const configuredBindingSessionKey = configuredRoute.boundSessionKey ?? "";
441
- route = configuredRoute.route;
442
- const runtimeRoute = resolveRuntimeConversationBindingRoute({
443
- route,
444
- conversation: {
445
- channel: "line",
446
- accountId: params.account.accountId,
447
- conversationId: peerId
448
- }
353
+ const runtimeRoute = inspectRuntimeConversationBindingRoute({
354
+ route: configuredRoute.route,
355
+ inspection
356
+ });
357
+ return {
358
+ userId,
359
+ groupId,
360
+ roomId,
361
+ isGroup,
362
+ peerId,
363
+ route: runtimeRoute.route,
364
+ mentionAgentId: baseRoute.agentId,
365
+ runtimeRoute,
366
+ configuredBinding: runtimeRoute.bindingRecord ? null : configuredRoute.bindingResolution,
367
+ configuredBindingSessionKey: configuredRoute.boundSessionKey ?? ""
368
+ };
369
+ }
370
+ async function resolveLineInboundRoute(params) {
371
+ recordChannelActivity({
372
+ channel: "line",
373
+ accountId: params.account.accountId,
374
+ direction: "inbound"
449
375
  });
450
- route = runtimeRoute.route;
376
+ const { userId, groupId, roomId, isGroup, peerId, route, runtimeRoute, configuredBinding, configuredBindingSessionKey } = params.preparedRoute ?? await prepareLineInboundRoute(params);
451
377
  if (runtimeRoute.bindingRecord) {
452
- configuredBinding = null;
378
+ await getSessionBindingService().touchAsync(runtimeRoute.bindingRecord.bindingId, void 0, runtimeRoute.bindingRecord.conversation);
453
379
  logVerbose(runtimeRoute.boundSessionKey ? `line: routed via bound conversation ${peerId} -> ${runtimeRoute.boundSessionKey}` : `line: plugin-bound conversation ${peerId}`);
454
380
  }
455
381
  if (configuredBinding) {
@@ -539,7 +465,7 @@ async function finalizeLineInboundContext(params) {
539
465
  const body = formatInboundEnvelope({
540
466
  channel: "LINE",
541
467
  from: conversationLabel,
542
- timestamp: params.timestamp,
468
+ timestamp: params.event.timestamp,
543
469
  body: agentBody,
544
470
  chatType: params.source.isGroup ? "group" : "direct",
545
471
  sender: {
@@ -554,7 +480,7 @@ async function finalizeLineInboundContext(params) {
554
480
  channel: "line",
555
481
  accountId: params.route.accountId,
556
482
  messageId: params.messageSid,
557
- timestamp: params.timestamp,
483
+ timestamp: params.event.timestamp,
558
484
  from: address,
559
485
  sender: {
560
486
  id: senderId,
@@ -594,7 +520,7 @@ async function finalizeLineInboundContext(params) {
594
520
  const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
595
521
  dmScope: params.cfg.session?.dmScope,
596
522
  allowFrom: params.account.config.allowFrom,
597
- normalizeEntry: (entry) => normalizeAllowFrom([entry]).entries[0]
523
+ normalizeEntry: (entry) => normalizeLineAllowEntry(entry) || void 0
598
524
  }) : null;
599
525
  const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
600
526
  route: params.route,
@@ -608,7 +534,14 @@ async function finalizeLineInboundContext(params) {
608
534
  }
609
535
  return {
610
536
  ctxPayload,
537
+ event: params.event,
538
+ userId: params.source.userId,
539
+ groupId: params.source.groupId,
540
+ roomId: params.source.roomId,
541
+ isGroup: params.source.isGroup,
542
+ route: params.route,
611
543
  replyToken: params.event.replyToken,
544
+ accountId: params.account.accountId,
612
545
  skillFilter: groupConfig?.skills,
613
546
  turn: {
614
547
  storePath,
@@ -639,11 +572,11 @@ async function buildLineMessageContext(params) {
639
572
  const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
640
573
  source,
641
574
  cfg,
642
- account
575
+ account,
576
+ preparedRoute: params.preparedRoute
643
577
  });
644
578
  const message = event.message;
645
579
  const messageId = message.id;
646
- const timestamp = event.timestamp;
647
580
  const textContent = extractMessageText(message);
648
581
  const nativeMediaKind = extractNativeMediaKind(message);
649
582
  const mediaFacts = allMedia.length > 0 ? allMedia.map((media) => ({
@@ -677,7 +610,7 @@ async function buildLineMessageContext(params) {
677
610
  address: loc.address
678
611
  });
679
612
  }
680
- const finalized = await finalizeLineInboundContext({
613
+ return finalizeLineInboundContext({
681
614
  cfg,
682
615
  account,
683
616
  event,
@@ -693,7 +626,6 @@ async function buildLineMessageContext(params) {
693
626
  agentBody,
694
627
  commandBody: resolveLineMentionStrippedText(message) || rawBody,
695
628
  mentions: params.mentions,
696
- timestamp,
697
629
  messageSid: messageId,
698
630
  commandAuthorized,
699
631
  channelIngress: await params.resolveChannelIngress?.({
@@ -711,19 +643,6 @@ async function buildLineMessageContext(params) {
711
643
  },
712
644
  inboundHistory
713
645
  });
714
- return {
715
- ctxPayload: finalized.ctxPayload,
716
- turn: finalized.turn,
717
- skillFilter: finalized.skillFilter,
718
- event,
719
- userId,
720
- groupId,
721
- roomId,
722
- isGroup,
723
- route,
724
- replyToken: event.replyToken,
725
- accountId: account.accountId
726
- };
727
646
  }
728
647
  async function buildLinePostbackContext(params) {
729
648
  const { event, cfg, account, commandAuthorized } = params;
@@ -748,7 +667,7 @@ async function buildLinePostbackContext(params) {
748
667
  if (picked) agentBody += ` ${key}=${picked}`;
749
668
  }
750
669
  const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
751
- const finalized = await finalizeLineInboundContext({
670
+ return finalizeLineInboundContext({
752
671
  cfg,
753
672
  account,
754
673
  event,
@@ -762,7 +681,6 @@ async function buildLinePostbackContext(params) {
762
681
  },
763
682
  rawBody,
764
683
  agentBody,
765
- timestamp,
766
684
  messageSid,
767
685
  commandAuthorized,
768
686
  channelIngress: await params.resolveChannelIngress?.({
@@ -775,19 +693,111 @@ async function buildLinePostbackContext(params) {
775
693
  media: [],
776
694
  verboseLog: { kind: "postback" }
777
695
  });
778
- return {
779
- ctxPayload: finalized.ctxPayload,
780
- turn: finalized.turn,
781
- skillFilter: finalized.skillFilter,
782
- event,
783
- userId,
784
- groupId,
785
- roomId,
786
- isGroup,
787
- route,
788
- replyToken: event.replyToken,
789
- accountId: account.accountId
790
- };
696
+ }
697
+ //#endregion
698
+ //#region extensions/line/src/download.ts
699
+ const CONTENT_READY_MAX_ATTEMPTS = 6;
700
+ const CONTENT_READY_BASE_DELAY_MS = 500;
701
+ const CONTENT_READY_MAX_DELAY_MS = 4e3;
702
+ const CONTENT_DOWNLOAD_TIMEOUT_MS = 15e3;
703
+ const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
704
+ var RetryableLineMediaFetchError = class extends MediaFetchError {
705
+ constructor(message, options) {
706
+ super("fetch_failed", message, options);
707
+ this.name = "RetryableLineMediaFetchError";
708
+ }
709
+ };
710
+ function contentBackoffDelayMs(attempt) {
711
+ return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
712
+ }
713
+ function lineContentUrl(messageId) {
714
+ return `${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`;
715
+ }
716
+ async function* lineResponseBodyChunks(response, messageId, signal, onComplete) {
717
+ const body = response.body;
718
+ if (!body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
719
+ const reader = body.getReader();
720
+ let completed = false;
721
+ try {
722
+ while (true) {
723
+ let chunk;
724
+ try {
725
+ chunk = await reader.read();
726
+ } catch (err) {
727
+ if (signal.aborted) throw signal.reason;
728
+ throw new RetryableLineMediaFetchError(`LINE media response stream failed for message ${messageId}`, { cause: err });
729
+ }
730
+ if (chunk.done) {
731
+ completed = true;
732
+ onComplete();
733
+ return;
734
+ }
735
+ if (chunk.value.byteLength > 0) yield chunk.value;
736
+ }
737
+ } finally {
738
+ if (!completed) await reader.cancel(signal.aborted ? signal.reason : void 0).catch(() => void 0);
739
+ try {
740
+ reader.releaseLock();
741
+ } catch {}
742
+ }
743
+ }
744
+ async function fetchLineContentWhenReady(messageId, channelAccessToken, signal) {
745
+ try {
746
+ for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
747
+ const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
748
+ headers: { Authorization: `Bearer ${channelAccessToken}` },
749
+ redirect: "error",
750
+ signal
751
+ });
752
+ if (response.status === 200) {
753
+ if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
754
+ return response;
755
+ }
756
+ await response.body?.cancel().catch(() => void 0);
757
+ if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
758
+ if (attempt < 5) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal });
759
+ }
760
+ } catch (err) {
761
+ if (signal.aborted) throw signal.reason;
762
+ if (err instanceof MediaFetchError) throw err;
763
+ throw new RetryableLineMediaFetchError(`LINE media download failed for message ${messageId}`, { cause: err });
764
+ }
765
+ throw new MediaFetchError("http_error", `LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`, { status: 202 });
766
+ }
767
+ function isRetryableLineInboundMediaError(err) {
768
+ if (err instanceof RetryableLineMediaFetchError) return true;
769
+ if (!(err instanceof MediaFetchError)) return false;
770
+ if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
771
+ return false;
772
+ }
773
+ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10485760, options) {
774
+ options?.signal?.throwIfAborted();
775
+ const deadlineAbort = new AbortController();
776
+ const signal = options?.signal ? AbortSignal.any([options.signal, deadlineAbort.signal]) : deadlineAbort.signal;
777
+ const deadline = setTimeout(() => {
778
+ deadlineAbort.abort(new RetryableLineMediaFetchError(`LINE media for message ${messageId} did not download within ${CONTENT_DOWNLOAD_TIMEOUT_MS / 1e3} seconds`));
779
+ }, CONTENT_DOWNLOAD_TIMEOUT_MS);
780
+ deadline.unref();
781
+ try {
782
+ const response = await fetchLineContentWhenReady(messageId, channelAccessToken, signal);
783
+ let saved;
784
+ try {
785
+ saved = await saveMediaStream(lineResponseBodyChunks(response, messageId, signal, () => clearTimeout(deadline)), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
786
+ } catch (err) {
787
+ await response.body?.cancel(signal.aborted ? signal.reason : err).catch(() => void 0);
788
+ if (signal.aborted) throw signal.reason;
789
+ throw err;
790
+ }
791
+ options?.signal?.throwIfAborted();
792
+ logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
793
+ return {
794
+ path: saved.path,
795
+ contentType: saved.contentType,
796
+ size: saved.size
797
+ };
798
+ } finally {
799
+ clearTimeout(deadline);
800
+ }
791
801
  }
792
802
  //#endregion
793
803
  //#region extensions/line/src/group-history.ts
@@ -915,6 +925,10 @@ async function resolveLineEventAdmission(event, context) {
915
925
  groupId,
916
926
  roomId
917
927
  });
928
+ if (isGroup && groupConfig?.enabled === false) {
929
+ logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
930
+ return null;
931
+ }
918
932
  const rawText = resolveEventRawText(event);
919
933
  const requireMention = isGroup ? groupConfig?.requireMention !== false : false;
920
934
  const dmPolicy = account.config.dmPolicy ?? "pairing";
@@ -925,29 +939,8 @@ async function resolveLineEventAdmission(event, context) {
925
939
  });
926
940
  const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
927
941
  const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom));
928
- const mentionFacts = (() => {
929
- if (!isGroup || event.type !== "message") return;
930
- const peerId = groupId ?? roomId ?? userId ?? "unknown";
931
- const { agentId } = resolveAgentRoute({
932
- cfg,
933
- channel: "line",
934
- accountId: account.accountId,
935
- peer: {
936
- kind: "group",
937
- id: peerId
938
- }
939
- });
940
- const mentionRegexes = buildMentionRegexes(cfg, agentId);
941
- const wasMentionedByNative = isLineBotMentioned(event.message);
942
- const wasMentionedByPattern = event.message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
943
- return {
944
- canDetectMention: event.message.type === "text",
945
- wasMentioned: wasMentionedByNative || wasMentionedByPattern,
946
- explicitlyMentionedBot: wasMentionedByNative,
947
- hasAnyMention: hasAnyLineMention(event.message),
948
- implicitMentionKinds: implicitMentionKindWhen("quoted_bot", quotesLineBotMessage(account.accountId, resolveLineQuotedMessageId(event.message)))
949
- };
950
- })();
942
+ let preparedRoute;
943
+ let mentionFacts;
951
944
  const resolveAccess = async (contextBinding) => await getLineRuntime().channel.inbound.ingress.resolveStable({
952
945
  channelId: "line",
953
946
  accountId: account.accountId,
@@ -965,10 +958,6 @@ async function resolveLineEventAdmission(event, context) {
965
958
  id: (groupId ?? roomId ?? senderId) || "unknown"
966
959
  },
967
960
  ...contextBinding ? { contextBinding } : {},
968
- ...isGroup && groupConfig?.enabled === false ? { route: {
969
- id: "line:group-config",
970
- enabled: false
971
- } } : {},
972
961
  mentionFacts,
973
962
  event: { kind: event.type === "join" ? "system" : event.type },
974
963
  dmPolicy,
@@ -992,35 +981,53 @@ async function resolveLineEventAdmission(event, context) {
992
981
  groupOwnerAllowFrom: "none"
993
982
  }
994
983
  });
995
- const access = await resolveAccess();
984
+ let access = await resolveAccess();
985
+ if (isGroup && event.type === "message" && isLineEventAdmitted(access)) {
986
+ preparedRoute = await prepareLineInboundRoute({
987
+ source: event.source,
988
+ cfg,
989
+ account
990
+ });
991
+ const mentionRegexes = buildMentionRegexes(cfg, preparedRoute.mentionAgentId);
992
+ const wasMentionedByNative = isLineBotMentioned(event.message);
993
+ const wasMentionedByPattern = event.message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
994
+ mentionFacts = {
995
+ canDetectMention: event.message.type === "text",
996
+ wasMentioned: wasMentionedByNative || wasMentionedByPattern,
997
+ explicitlyMentionedBot: wasMentionedByNative,
998
+ hasAnyMention: hasAnyLineMention(event.message),
999
+ implicitMentionKinds: implicitMentionKindWhen("quoted_bot", quotesLineBotMessage(account.accountId, resolveLineQuotedMessageId(event.message)))
1000
+ };
1001
+ access = await resolveAccess();
1002
+ }
996
1003
  warnMissingProviderGroupPolicyFallbackOnce({
997
1004
  providerMissingFallbackApplied,
998
1005
  providerKey: "line",
999
1006
  accountId: account.accountId,
1000
1007
  log: (message) => logVerbose(message)
1001
1008
  });
1002
- if (event.type === "join") return groupConfig?.enabled !== false && groupPolicy !== "disabled" && (groupPolicy !== "allowlist" || access.state.allowlists.group.hasMatchableEntries) ? {
1009
+ if (event.type === "join") return groupPolicy !== "disabled" && (groupPolicy !== "allowlist" || access.state.allowlists.group.hasMatchableEntries) ? {
1003
1010
  access,
1004
1011
  resolveBoundAccess: resolveAccess
1005
1012
  } : null;
1006
- if (isLineEventAdmitted(access)) return {
1007
- access,
1008
- resolveBoundAccess: resolveAccess,
1009
- mentions: mentionFacts ? {
1013
+ if (isLineEventAdmitted(access)) {
1014
+ const mentions = mentionFacts ? {
1010
1015
  ...mentionFacts,
1011
1016
  wasMentioned: access.activationAccess.effectiveWasMentioned ?? mentionFacts.wasMentioned,
1012
1017
  requireMention
1013
- } : void 0
1014
- };
1018
+ } : void 0;
1019
+ return {
1020
+ access,
1021
+ resolveBoundAccess: resolveAccess,
1022
+ mentions,
1023
+ preparedRoute
1024
+ };
1025
+ }
1015
1026
  if (access.senderAccess.decision === "allow") {
1016
1027
  logVerbose(`Blocked line event (${access.ingress.reasonCode})`);
1017
1028
  return null;
1018
1029
  }
1019
1030
  if (isGroup) {
1020
- if (groupConfig?.enabled === false) {
1021
- logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
1022
- return null;
1023
- }
1024
1031
  if (groupConfig?.allowFrom !== void 0) {
1025
1032
  if (!senderId) {
1026
1033
  logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
@@ -1149,6 +1156,7 @@ async function handleMessageEvent(event, context, setParts) {
1149
1156
  ...context.missingParts === void 0 ? {} : { missingParts: context.missingParts },
1150
1157
  cfg,
1151
1158
  account,
1159
+ preparedRoute: decision.preparedRoute,
1152
1160
  commandAuthorized: decision.access.commandAccess.authorized,
1153
1161
  resolveChannelIngress: decision.resolveBoundAccess,
1154
1162
  inboundHistory: historyReservation.inboundHistory,
@@ -1167,14 +1175,6 @@ async function handleMessageEvent(event, context, setParts) {
1167
1175
  historyReservation.release();
1168
1176
  }
1169
1177
  }
1170
- async function handleFollowEvent(event, _context) {
1171
- const { userId } = getLineSourceInfo(event.source);
1172
- logVerbose(`line: user ${userId ?? "unknown"} followed`);
1173
- }
1174
- async function handleUnfollowEvent(event, _context) {
1175
- const { userId } = getLineSourceInfo(event.source);
1176
- logVerbose(`line: user ${userId ?? "unknown"} unfollowed`);
1177
- }
1178
1178
  async function handleJoinEvent(event, context) {
1179
1179
  const { groupId, roomId, isGroup } = getLineSourceInfo(event.source);
1180
1180
  const conversationId = groupId ?? roomId;
@@ -1212,10 +1212,6 @@ async function handleJoinEvent(event, context) {
1212
1212
  }
1213
1213
  });
1214
1214
  }
1215
- async function handleLeaveEvent(event, _context) {
1216
- const { groupId, roomId } = getLineSourceInfo(event.source);
1217
- logVerbose(`line: bot left ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
1218
- }
1219
1215
  /** What a tap that did not answer the question has to tell the person who tapped. */
1220
1216
  function lineQuestionOutcomeNotice(status) {
1221
1217
  if (status === "already-terminal") return "That question is no longer waiting for an answer.";
@@ -1290,17 +1286,19 @@ async function handleLineWebhookEvent(event, context, setParts = []) {
1290
1286
  await handleMessageEvent(event, context, setParts.filter((part) => part.type === "message"));
1291
1287
  break;
1292
1288
  case "follow":
1293
- await handleFollowEvent(event, context);
1294
- break;
1295
- case "unfollow":
1296
- await handleUnfollowEvent(event, context);
1289
+ case "unfollow": {
1290
+ const { userId } = getLineSourceInfo(event.source);
1291
+ logVerbose(`line: user ${userId ?? "unknown"} ${event.type}ed`);
1297
1292
  break;
1293
+ }
1298
1294
  case "join":
1299
1295
  await handleJoinEvent(event, context);
1300
1296
  break;
1301
- case "leave":
1302
- await handleLeaveEvent(event, context);
1297
+ case "leave": {
1298
+ const { groupId, roomId } = getLineSourceInfo(event.source);
1299
+ logVerbose(`line: bot left ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
1303
1300
  break;
1301
+ }
1304
1302
  case "postback":
1305
1303
  await handlePostbackEvent(event, context);
1306
1304
  break;
@@ -1310,40 +1308,17 @@ async function handleLineWebhookEvent(event, context, setParts = []) {
1310
1308
  //#endregion
1311
1309
  //#region extensions/line/src/inbound-image-set.ts
1312
1310
  const IMAGE_SET_FLUSH_DELAY_MS = 4e3;
1313
- /**
1314
- * Ordered by the index the sender picked, falling back to arrival.
1315
- *
1316
- * `index` is optional per part in LINE's contract, so a set can arrive partly
1317
- * indexed. Choosing the key per pair would make the comparator intransitive and
1318
- * the resulting order depend on insertion; ranking unindexed parts last keeps
1319
- * one total order for every mix.
1320
- */
1321
1311
  function orderedParts(pending) {
1322
1312
  return [...pending.parts.values()].toSorted((left, right) => (left.index ?? Number.MAX_SAFE_INTEGER) - (right.index ?? Number.MAX_SAFE_INTEGER) || left.arrivedAt - right.arrivedAt);
1323
1313
  }
1324
- /**
1325
- * Buffers the parts of a LINE image set until the set is whole.
1326
- *
1327
- * The first part becomes the set's holder: its call does not resolve until the
1328
- * set completes or its wait expires, and it is the one that delivers. Keeping
1329
- * that call open is what keeps the combined turn inside a live ingress
1330
- * adoption - a turn dispatched after every part had returned is refused by
1331
- * admission, so a set that never completes would never be delivered at all.
1332
- */
1333
1314
  function createLineImageSetIngressBuffer() {
1334
1315
  const pendingBySet = /* @__PURE__ */ new Map();
1335
1316
  const deliveredBySet = /* @__PURE__ */ new Map();
1336
1317
  const pendingKey = (laneKey, senderKey, setId) => `${laneKey}\u0000${senderKey}\u0000${setId}`;
1337
1318
  const laneChain = /* @__PURE__ */ new Map();
1338
1319
  const enterLane = async (laneKey) => {
1339
- let release = () => {};
1340
- const held = new Promise((resolve) => {
1341
- release = resolve;
1342
- });
1343
- let entered = () => {};
1344
- const turn = new Promise((resolve) => {
1345
- entered = resolve;
1346
- });
1320
+ const { promise: held, resolve: release } = createDeferred();
1321
+ const { promise: turn, resolve: entered } = createDeferred();
1347
1322
  enqueueKeyedTask({
1348
1323
  tails: laneChain,
1349
1324
  key: laneKey,
@@ -1378,10 +1353,7 @@ function createLineImageSetIngressBuffer() {
1378
1353
  }
1379
1354
  return null;
1380
1355
  }
1381
- let release = () => {};
1382
- const whole = new Promise((resolve) => {
1383
- release = resolve;
1384
- });
1356
+ const { promise: whole, resolve: release } = createDeferred();
1385
1357
  const pending = {
1386
1358
  parts: /* @__PURE__ */ new Map([[input.messageId, part]]),
1387
1359
  total: input.total,
@@ -1772,9 +1744,9 @@ function parseLineWebhookBody(rawBody) {
1772
1744
  //#endregion
1773
1745
  //#region extensions/line/src/webhook-node.ts
1774
1746
  const LINE_WEBHOOK_MAX_BODY_BYTES = 1048576;
1775
- const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 65536;
1776
- const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1 = 5e3;
1777
- async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1) {
1747
+ const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
1748
+ const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
1749
+ async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS) {
1778
1750
  return await readRequestBodyWithLimit(req, {
1779
1751
  maxBytes,
1780
1752
  timeoutMs,
@@ -1797,6 +1769,12 @@ function createLineNodeWebhookHandler(params) {
1797
1769
  const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
1798
1770
  const readBody = params.readBody ?? readLineWebhookRequestBody;
1799
1771
  return async (req, res) => {
1772
+ const targets = params.getTargets();
1773
+ if (req.method !== "POST" && targets.length === 0) {
1774
+ res.statusCode = 404;
1775
+ res.end("Not Found");
1776
+ return;
1777
+ }
1800
1778
  if (req.method === "GET" || req.method === "HEAD") {
1801
1779
  if (req.method === "HEAD") {
1802
1780
  res.statusCode = 204;
@@ -1825,14 +1803,22 @@ function createLineNodeWebhookHandler(params) {
1825
1803
  res.end(JSON.stringify({ error: "Missing X-Line-Signature header" }));
1826
1804
  return;
1827
1805
  }
1828
- const rawBody = await readBody(req, Math.min(maxBodyBytes, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1), LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1);
1829
- if (!validateLineSignature(rawBody, signature, params.channelSecret)) {
1806
+ const rawBody = await readBody(req, Math.min(maxBodyBytes, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES), LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS);
1807
+ const match = resolveSingleWebhookTarget(targets, (target) => validateLineSignature(rawBody, signature, target.channelSecret));
1808
+ if (match.kind === "none") {
1830
1809
  logVerbose("line: webhook signature validation failed");
1831
1810
  res.statusCode = 401;
1832
1811
  res.setHeader("Content-Type", "application/json");
1833
1812
  res.end(JSON.stringify({ error: "Invalid signature" }));
1834
1813
  return;
1835
1814
  }
1815
+ if (match.kind === "ambiguous") {
1816
+ logVerbose("line: webhook signature matched multiple accounts");
1817
+ res.statusCode = 401;
1818
+ res.setHeader("Content-Type", "application/json");
1819
+ res.end(JSON.stringify({ error: "Ambiguous webhook target" }));
1820
+ return;
1821
+ }
1836
1822
  const body = parseLineWebhookBody(rawBody);
1837
1823
  if (!body) {
1838
1824
  res.statusCode = 400;
@@ -1840,10 +1826,9 @@ function createLineNodeWebhookHandler(params) {
1840
1826
  res.end(JSON.stringify({ error: "Invalid webhook payload" }));
1841
1827
  return;
1842
1828
  }
1843
- params.onRequestAuthenticated?.();
1844
1829
  if (body.events && body.events.length > 0) {
1845
1830
  logVerbose(`line: received ${body.events.length} webhook events`);
1846
- await params.bot.handleWebhook(body);
1831
+ if (await match.target.bot.handleWebhook(body) === "durable") res.setHeader("x-openclaw-delivery-accepted", "durable");
1847
1832
  }
1848
1833
  res.statusCode = 200;
1849
1834
  res.setHeader("Content-Type", "application/json");
@@ -1862,8 +1847,6 @@ function createLineNodeWebhookHandler(params) {
1862
1847
  //#endregion
1863
1848
  //#region extensions/line/src/monitor.ts
1864
1849
  const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
1865
- const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
1866
- const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
1867
1850
  async function registerLineWebhookTarget(params, bot) {
1868
1851
  try {
1869
1852
  return registerWebhookTargetWithPluginRoute(params);
@@ -2016,10 +1999,9 @@ async function monitorLineProvider(opts) {
2016
1999
  });
2017
2000
  const normalizedPath = resolveLineWebhookPath(webhookPath);
2018
2001
  const webhookRouteKey = canonicalizeWebhookRouteKey(normalizedPath);
2019
- const createScopedLineWebhookHandler = (target) => createLineNodeWebhookHandler({
2020
- channelSecret: target.channelSecret,
2021
- bot: target.bot,
2022
- runtime: target.runtime
2002
+ const handleWebhook = createLineNodeWebhookHandler({
2003
+ getTargets: () => lineWebhookTargets.get(webhookRouteKey) ?? [],
2004
+ runtime
2023
2005
  });
2024
2006
  const { unregister: unregisterHttp } = await registerLineWebhookTarget({
2025
2007
  targetsByPath: lineWebhookTargets,
@@ -2038,15 +2020,8 @@ async function monitorLineProvider(opts) {
2038
2020
  log: (msg) => logVerbose(msg),
2039
2021
  throwOnFailure: true,
2040
2022
  handler: async (req, res) => {
2041
- const targets = lineWebhookTargets.get(webhookRouteKey) ?? [];
2042
- const firstTarget = targets[0];
2043
2023
  if (req.method !== "POST") {
2044
- if (!firstTarget) {
2045
- res.statusCode = 404;
2046
- res.end("Not Found");
2047
- return;
2048
- }
2049
- await createScopedLineWebhookHandler(firstTarget)(req, res);
2024
+ await handleWebhook(req, res);
2050
2025
  return;
2051
2026
  }
2052
2027
  const requestLifecycle = beginWebhookRequestPipelineOrReject({
@@ -2057,53 +2032,7 @@ async function monitorLineProvider(opts) {
2057
2032
  });
2058
2033
  if (!requestLifecycle.ok) return;
2059
2034
  try {
2060
- const signatureHeader = req.headers["x-line-signature"];
2061
- const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
2062
- if (!signature) {
2063
- logVerbose("line: webhook missing X-Line-Signature header");
2064
- res.statusCode = 400;
2065
- res.setHeader("Content-Type", "application/json");
2066
- res.end(JSON.stringify({ error: "Missing X-Line-Signature header" }));
2067
- return;
2068
- }
2069
- const rawBody = await readLineWebhookRequestBody(req, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES, LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS);
2070
- const match = resolveSingleWebhookTarget(targets, (target) => validateLineSignature(rawBody, signature, target.channelSecret));
2071
- if (match.kind === "none") {
2072
- logVerbose("line: webhook signature validation failed");
2073
- res.statusCode = 401;
2074
- res.setHeader("Content-Type", "application/json");
2075
- res.end(JSON.stringify({ error: "Invalid signature" }));
2076
- return;
2077
- }
2078
- if (match.kind === "ambiguous") {
2079
- logVerbose("line: webhook signature matched multiple accounts");
2080
- res.statusCode = 401;
2081
- res.setHeader("Content-Type", "application/json");
2082
- res.end(JSON.stringify({ error: "Ambiguous webhook target" }));
2083
- return;
2084
- }
2085
- const body = parseLineWebhookBody(rawBody);
2086
- if (!body) {
2087
- res.statusCode = 400;
2088
- res.setHeader("Content-Type", "application/json");
2089
- res.end(JSON.stringify({ error: "Invalid webhook payload" }));
2090
- return;
2091
- }
2092
- if (body.events && body.events.length > 0) {
2093
- logVerbose(`line: received ${body.events.length} webhook events`);
2094
- if (await match.target.bot.handleWebhook(body) === "durable") res.setHeader("x-openclaw-delivery-accepted", "durable");
2095
- }
2096
- res.statusCode = 200;
2097
- res.setHeader("Content-Type", "application/json");
2098
- res.end(JSON.stringify({ status: "ok" }));
2099
- } catch (err) {
2100
- if (await rejectLineWebhookRequest(req, res, err)) return;
2101
- runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
2102
- if (!res.headersSent) {
2103
- res.statusCode = 500;
2104
- res.setHeader("Content-Type", "application/json");
2105
- res.end(JSON.stringify({ error: "Internal server error" }));
2106
- }
2035
+ await handleWebhook(req, res);
2107
2036
  } finally {
2108
2037
  requestLifecycle.release();
2109
2038
  }
@@ -2142,4 +2071,4 @@ async function monitorLineProvider(opts) {
2142
2071
  };
2143
2072
  }
2144
2073
  //#endregion
2145
- export { resolveLineWebhookPath as a, parseLineWebhookBody as i, createLineNodeWebhookHandler as n, validateLineSignature as o, readLineWebhookRequestBody as r, downloadLineMedia as s, monitorLineProvider as t };
2074
+ export { monitorLineProvider as t };