@openclaw/line 2026.5.7 → 2026.5.10-beta.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.
package/dist/api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-CnhV8Dzi.js";
1
+ import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-CQemYfAf.js";
2
2
  import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-DCH7CE-y.js";
3
3
  //#region extensions/line/src/channel.setup.ts
4
4
  const lineSetupPlugin = {
@@ -2,7 +2,7 @@ import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLin
2
2
  import { n as lineSetupAdapter, r as hasLineCredentials, t as lineSetupWizard } from "./setup-surface-DCH7CE-y.js";
3
3
  import { i as resolveExactLineGroupConfigKey, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-HskUUWsn.js";
4
4
  import { n as parseLineDirectives, o as LineChannelConfigSchema, t as hasLineDirectives } from "./reply-payload-transform-Bmf8GnSU.js";
5
- import { t as resolveLineOutboundMedia } from "./outbound-media-BfhBmWq5.js";
5
+ import { n as resolveLineOutboundMedia, t as createLineSendReceipt } from "./send-receipt-DSg7S42s.js";
6
6
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
7
7
  import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
8
8
  import { createRestrictSendersChannelSecurity, resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
@@ -12,6 +12,7 @@ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1 } from "openclaw/plugin-sdk/
12
12
  import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
13
13
  import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
14
14
  import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
15
+ import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
15
16
  import { createAttachedChannelResultAdapter, createEmptyChannelResult } from "openclaw/plugin-sdk/channel-send-result";
16
17
  import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
17
18
  import { buildTokenChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, createDependentCredentialStatusIssueCollector } from "openclaw/plugin-sdk/status-helpers";
@@ -107,7 +108,7 @@ const lineChannelPluginCommon = {
107
108
  //#endregion
108
109
  //#region extensions/line/src/gateway.ts
109
110
  const loadLineProbeRuntime$1 = createLazyRuntimeModule(() => import("./probe.runtime-N91OWW1J.js"));
110
- const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-DyxYlrM9.js"));
111
+ const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-D0Xj2uQz.js"));
111
112
  const lineGatewayAdapter = {
112
113
  startAccount: async (ctx) => {
113
114
  const account = ctx.account;
@@ -211,7 +212,7 @@ function resolveLineGroupRequireMention(params) {
211
212
  }
212
213
  //#endregion
213
214
  //#region extensions/line/src/outbound.ts
214
- const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-B-luYwEB.js"));
215
+ const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-zBxAf-it.js"));
215
216
  function isLineUserTarget(target) {
216
217
  const normalized = target.trim().replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
217
218
  return /^U/i.test(normalized);
@@ -265,17 +266,11 @@ const lineOutboundAdapter = {
265
266
  const quickReply = hasQuickReplies ? (lineRuntime?.createQuickReplyItems ?? outboundRuntime.createQuickReplyItems)(quickReplies) : void 0;
266
267
  const sendMessageBatch = async (messages) => {
267
268
  if (messages.length === 0) return;
268
- for (let i = 0; i < messages.length; i += 5) {
269
- const result = await sendBatch(to, messages.slice(i, i + 5), {
270
- verbose: false,
271
- cfg,
272
- accountId: accountId ?? void 0
273
- });
274
- lastResult = {
275
- messageId: result.messageId,
276
- chatId: result.chatId
277
- };
278
- }
269
+ for (let i = 0; i < messages.length; i += 5) lastResult = await sendBatch(to, messages.slice(i, i + 5), {
270
+ verbose: false,
271
+ cfg,
272
+ accountId: accountId ?? void 0
273
+ });
279
274
  };
280
275
  const processed = payload.text ? outboundRuntime.processLineMessage(payload.text) : {
281
276
  text: "",
@@ -437,7 +432,12 @@ const lineOutboundAdapter = {
437
432
  });
438
433
  else result = {
439
434
  messageId: "processed",
440
- chatId: to
435
+ chatId: to,
436
+ receipt: createLineSendReceipt({
437
+ messageId: "processed",
438
+ chatId: to,
439
+ kind: "card"
440
+ })
441
441
  };
442
442
  for (const flexMsg of processed.flexMessages) {
443
443
  const flexContents = flexMsg.contents;
@@ -457,6 +457,55 @@ const lineOutboundAdapter = {
457
457
  })
458
458
  })
459
459
  };
460
+ function toLineMessageSendResult(result, kind) {
461
+ const source = result;
462
+ const receipt = result.receipt ?? (result.messageId ? createLineSendReceipt({
463
+ messageId: result.messageId,
464
+ chatId: source.chatId ?? "",
465
+ kind
466
+ }) : void 0);
467
+ if (!receipt) throw new Error("LINE message adapter send did not return a receipt");
468
+ return {
469
+ messageId: result.messageId || receipt.primaryPlatformMessageId,
470
+ receipt
471
+ };
472
+ }
473
+ const lineMessageAdapter = defineChannelMessageAdapter({
474
+ id: "line",
475
+ durableFinal: { capabilities: {
476
+ text: true,
477
+ media: true,
478
+ messageSendingHooks: true
479
+ } },
480
+ send: {
481
+ text: async ({ cfg, to, text, accountId }) => {
482
+ return toLineMessageSendResult(await lineOutboundAdapter.sendPayload({
483
+ cfg,
484
+ to,
485
+ text,
486
+ accountId,
487
+ payload: { text }
488
+ }), "text");
489
+ },
490
+ media: async ({ cfg, to, text, mediaUrl, accountId }) => {
491
+ return toLineMessageSendResult(await lineOutboundAdapter.sendPayload({
492
+ cfg,
493
+ to,
494
+ text,
495
+ mediaUrl,
496
+ accountId,
497
+ payload: {
498
+ text,
499
+ mediaUrl
500
+ }
501
+ }), "media");
502
+ }
503
+ },
504
+ receive: {
505
+ defaultAckPolicy: "after_agent_dispatch",
506
+ supportedAckPolicies: ["after_receive_record", "after_agent_dispatch"]
507
+ }
508
+ });
460
509
  //#endregion
461
510
  //#region extensions/line/src/status.ts
462
511
  const loadLineProbeRuntime = createLazyRuntimeModule(() => import("./probe.runtime-N91OWW1J.js"));
@@ -484,7 +533,7 @@ const lineStatusAdapter = createComputedAccountStatusAdapter({
484
533
  });
485
534
  //#endregion
486
535
  //#region extensions/line/src/channel.ts
487
- const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-bXoj8_Sl.js"));
536
+ const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-7a5SEVBm.js"));
488
537
  const lineSecurityAdapter = createRestrictSendersChannelSecurity({
489
538
  channelKey: "line",
490
539
  resolveDmPolicy: (account) => account.config.dmPolicy,
@@ -530,6 +579,7 @@ const linePlugin = createChatChannelPlugin({
530
579
  setup: lineSetupAdapter,
531
580
  status: lineStatusAdapter,
532
581
  gateway: lineGatewayAdapter,
582
+ message: lineMessageAdapter,
533
583
  bindings: lineBindingsAdapter,
534
584
  conversationBindings: { defaultTopLevelPlacement: "current" },
535
585
  agentPrompt: { messageToolHints: () => [
@@ -1,2 +1,2 @@
1
- import { t as linePlugin } from "./channel-CnhV8Dzi.js";
1
+ import { t as linePlugin } from "./channel-CQemYfAf.js";
2
2
  export { linePlugin };
@@ -0,0 +1,4 @@
1
+ import { S as pushMessageLine } from "./markdown-to-line-DASNAjBq.js";
2
+ import { t as monitorLineProvider } from "./monitor-PVeb2tJQ.js";
3
+ import { t as probeLineBot } from "./probe-D9BPzb2n.js";
4
+ export { monitorLineProvider, probeLineBot, pushMessageLine };
@@ -1,5 +1,5 @@
1
1
  import { i as resolveLineAccount } from "./accounts-UXAvdBo_.js";
2
- import { n as validateLineMediaUrl } from "./outbound-media-BfhBmWq5.js";
2
+ import { r as validateLineMediaUrl, t as createLineSendReceipt } from "./send-receipt-DSg7S42s.js";
3
3
  import { r as createReceiptCard } from "./schedule-cards-BwQChTdM.js";
4
4
  import { stripMarkdown, stripMarkdown as stripMarkdown$1 } from "openclaw/plugin-sdk/text-runtime";
5
5
  import { messagingApi } from "@line/bot-sdk";
@@ -319,6 +319,14 @@ function recordLineOutboundActivity(accountId) {
319
319
  direction: "outbound"
320
320
  });
321
321
  }
322
+ function resolveLineReceiptKind(messages) {
323
+ const types = new Set(messages.map((message) => message.type));
324
+ if (types.has("audio")) return "voice";
325
+ if (types.has("image") || types.has("video")) return "media";
326
+ if (types.has("flex") || types.has("template") || types.has("location")) return "card";
327
+ if (types.has("text")) return "text";
328
+ return "unknown";
329
+ }
322
330
  async function pushLineMessages(to, messages, opts, behavior = {}) {
323
331
  if (messages.length === 0) throw new Error("Message must be non-empty for LINE sends");
324
332
  const { account, client, chatId } = createLinePushContext(to, opts);
@@ -335,7 +343,13 @@ async function pushLineMessages(to, messages, opts, behavior = {}) {
335
343
  if (opts.verbose) logVerbose(behavior.verboseMessage?.(chatId, messages.length) ?? `line: pushed ${messages.length} messages to ${chatId}`);
336
344
  return {
337
345
  messageId: "push",
338
- chatId
346
+ chatId,
347
+ receipt: createLineSendReceipt({
348
+ messageId: "push",
349
+ chatId,
350
+ kind: resolveLineReceiptKind(messages),
351
+ messageCount: messages.length
352
+ })
339
353
  };
340
354
  }
341
355
  async function replyLineMessages(replyToken, messages, opts, behavior = {}) {
@@ -380,7 +394,13 @@ async function sendMessageLine(to, text, opts) {
380
394
  await replyLineMessages(opts.replyToken, messages, opts, { verboseMessage: () => `line: replied to ${chatId}` });
381
395
  return {
382
396
  messageId: "reply",
383
- chatId
397
+ chatId,
398
+ receipt: createLineSendReceipt({
399
+ messageId: "reply",
400
+ chatId,
401
+ kind: resolveLineReceiptKind(messages),
402
+ messageCount: messages.length
403
+ })
384
404
  };
385
405
  }
386
406
  return pushLineMessages(chatId, messages, opts, { verboseMessage: (resolvedChatId) => `line: pushed message to ${resolvedChatId}` });
@@ -1,50 +1,44 @@
1
1
  import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-UXAvdBo_.js";
2
2
  import { a as resolveLineGroupConfigEntry, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-HskUUWsn.js";
3
- import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, f as createImageMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-CCWK6PJh.js";
3
+ import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, f as createImageMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-DASNAjBq.js";
4
4
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
5
+ import { createMessageReceiveContext, hasFinalChannelTurnDispatch } from "openclaw/plugin-sdk/channel-message";
5
6
  import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
6
- import { lowercasePreservingWhitespace, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
7
- import { firstDefined, isSenderIdAllowed, mergeDmAllowFromSources } from "openclaw/plugin-sdk/allow-from";
7
+ import { lowercasePreservingWhitespace, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/text-runtime";
8
+ import { firstDefined } from "openclaw/plugin-sdk/allow-from";
8
9
  import { messagingApi } from "@line/bot-sdk";
9
10
  import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
10
11
  import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
11
12
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
12
- import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
13
- import { hasFinalInboundReplyDispatch } from "openclaw/plugin-sdk/inbound-reply-dispatch";
14
13
  import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
15
14
  import { isRequestBodyLimitError, normalizePluginHttpPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
16
15
  import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1 } from "openclaw/plugin-sdk/webhook-request-guards";
17
16
  import { DEFAULT_GROUP_HISTORY_LIMIT, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
18
17
  import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
19
- import { buildMentionRegexes, formatInboundEnvelope, formatLocationText, matchesMentionPatterns, resolveInboundMentionDecision, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
20
- import { hasControlCommand, resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
18
+ import { buildMentionRegexes, formatInboundEnvelope, formatLocationText, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
19
+ import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
20
+ import { shouldComputeCommandAuthorized } from "openclaw/plugin-sdk/command-auth-native";
21
21
  import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
22
- import { evaluateMatchedGroupAccessForPolicy } from "openclaw/plugin-sdk/group-access";
23
22
  import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
24
23
  import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
25
24
  import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
26
25
  import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
27
26
  import crypto from "node:crypto";
28
27
  //#region extensions/line/src/bot-access.ts
29
- function normalizeAllowEntry(value) {
28
+ function normalizeLineAllowEntry(value) {
30
29
  const trimmed = String(value).trim();
31
30
  if (!trimmed) return "";
32
31
  if (trimmed === "*") return "*";
33
32
  return trimmed.replace(/^line:(?:user:)?/i, "");
34
33
  }
35
34
  const normalizeAllowFrom = (list) => {
36
- const entries = (list ?? []).map((value) => normalizeAllowEntry(value)).filter(Boolean);
35
+ const entries = (list ?? []).map((value) => normalizeLineAllowEntry(value)).filter(Boolean);
37
36
  return {
38
37
  entries,
39
38
  hasWildcard: entries.includes("*"),
40
39
  hasEntries: entries.length > 0
41
40
  };
42
41
  };
43
- const normalizeDmAllowFromWithStore = (params) => normalizeAllowFrom(mergeDmAllowFromSources(params));
44
- const isSenderAllowed = (params) => {
45
- const { allow, senderId } = params;
46
- return isSenderIdAllowed(allow, senderId, false);
47
- };
48
42
  //#endregion
49
43
  //#region extensions/line/src/download.ts
50
44
  const AUDIO_BRANDS = new Set([
@@ -581,6 +575,9 @@ function isDownloadableLineMessageType(messageType) {
581
575
  }
582
576
  const LINE_WEBHOOK_REPLAY_WINDOW_MS = 600 * 1e3;
583
577
  const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
578
+ function normalizeLineIngressEntry(value) {
579
+ return normalizeLineAllowEntry(value) || null;
580
+ }
584
581
  var LineRetryableWebhookError = class extends Error {
585
582
  constructor(message, options) {
586
583
  super(message, options);
@@ -689,33 +686,91 @@ async function sendLinePairingReply(params) {
689
686
  });
690
687
  }
691
688
  async function shouldProcessLineEvent(event, context) {
692
- const denied = {
693
- allowed: false,
694
- commandAuthorized: false
695
- };
696
689
  const { cfg, account } = context;
697
690
  const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
698
691
  const senderId = userId ?? "";
699
- const dmPolicy = account.config.dmPolicy ?? "pairing";
700
- const storeAllowFrom = await readChannelAllowFromStore("line", void 0, account.accountId).catch(() => []);
701
- const effectiveDmAllow = normalizeDmAllowFromWithStore({
702
- allowFrom: account.config.allowFrom,
703
- storeAllowFrom,
704
- dmPolicy
705
- });
706
692
  const groupConfig = resolveLineGroupConfig({
707
693
  config: account.config,
708
694
  groupId,
709
695
  roomId
710
696
  });
711
- const groupAllowOverride = groupConfig?.allowFrom;
712
- const fallbackGroupAllowFrom = account.config.allowFrom?.length ? account.config.allowFrom : void 0;
713
- const effectiveGroupAllow = normalizeAllowFrom(firstDefined(groupAllowOverride, account.config.groupAllowFrom, fallbackGroupAllowFrom));
714
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
715
- const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
697
+ const rawText = resolveEventRawText(event);
698
+ const requireMention = isGroup ? groupConfig?.requireMention !== false : false;
699
+ const dmPolicy = account.config.dmPolicy ?? "pairing";
700
+ const { groupPolicy: runtimeGroupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
716
701
  providerConfigPresent: cfg.channels?.line !== void 0,
717
702
  groupPolicy: account.config.groupPolicy,
718
- defaultGroupPolicy
703
+ defaultGroupPolicy: resolveDefaultGroupPolicy(cfg)
704
+ });
705
+ const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
706
+ const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom, account.config.allowFrom?.length ? account.config.allowFrom : void 0));
707
+ const mentionFacts = (() => {
708
+ if (!isGroup || event.type !== "message") return {
709
+ canDetectMention: false,
710
+ wasMentioned: false,
711
+ hasAnyMention: false
712
+ };
713
+ const peerId = groupId ?? roomId ?? userId ?? "unknown";
714
+ const { agentId } = resolveAgentRoute({
715
+ cfg,
716
+ channel: "line",
717
+ accountId: account.accountId,
718
+ peer: {
719
+ kind: "group",
720
+ id: peerId
721
+ }
722
+ });
723
+ const mentionRegexes = buildMentionRegexes(cfg, agentId);
724
+ const wasMentionedByNative = isLineBotMentioned(event.message);
725
+ const wasMentionedByPattern = event.message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
726
+ return {
727
+ canDetectMention: event.message.type === "text",
728
+ wasMentioned: wasMentionedByNative || wasMentionedByPattern,
729
+ hasAnyMention: hasAnyLineMention(event.message)
730
+ };
731
+ })();
732
+ const access = await resolveStableChannelMessageIngress({
733
+ channelId: "line",
734
+ accountId: account.accountId,
735
+ identity: {
736
+ key: "line-user-id",
737
+ normalize: normalizeLineIngressEntry,
738
+ sensitivity: "pii",
739
+ entryIdPrefix: "line-entry"
740
+ },
741
+ cfg,
742
+ readStoreAllowFrom: async () => await readChannelAllowFromStore("line", void 0, account.accountId),
743
+ subject: { stableId: senderId },
744
+ conversation: {
745
+ kind: isGroup ? "group" : "direct",
746
+ id: (groupId ?? roomId ?? senderId) || "unknown"
747
+ },
748
+ ...isGroup && groupConfig?.enabled === false ? { route: {
749
+ id: "line:group-config",
750
+ enabled: false
751
+ } } : {},
752
+ mentionFacts: isGroup && event.type === "message" ? {
753
+ canDetectMention: mentionFacts.canDetectMention,
754
+ wasMentioned: mentionFacts.wasMentioned,
755
+ hasAnyMention: mentionFacts.hasAnyMention,
756
+ implicitMentionKinds: []
757
+ } : void 0,
758
+ event: { kind: event.type === "postback" ? "postback" : "message" },
759
+ dmPolicy,
760
+ groupPolicy,
761
+ policy: {
762
+ groupAllowFromFallbackToAllowFrom: false,
763
+ activation: {
764
+ requireMention: isGroup && event.type === "message" && requireMention,
765
+ allowTextCommands: true
766
+ }
767
+ },
768
+ allowFrom: normalizeStringEntries(account.config.allowFrom),
769
+ groupAllowFrom,
770
+ command: {
771
+ hasControlCommand: shouldComputeCommandAuthorized(rawText, cfg),
772
+ groupOwnerAllowFrom: "none"
773
+ }
719
774
  });
720
775
  warnMissingProviderGroupPolicyFallbackOnce({
721
776
  providerMissingFallbackApplied,
@@ -723,90 +778,50 @@ async function shouldProcessLineEvent(event, context) {
723
778
  accountId: account.accountId,
724
779
  log: (message) => logVerbose(message)
725
780
  });
781
+ if (access.senderAccess.decision === "allow" && (access.ingress.admission === "dispatch" || access.ingress.admission === "observe" || access.ingress.admission === "skip")) return access;
782
+ if (access.senderAccess.decision === "allow") {
783
+ logVerbose(`Blocked line event (${access.ingress.reasonCode})`);
784
+ return null;
785
+ }
726
786
  if (isGroup) {
727
787
  if (groupConfig?.enabled === false) {
728
788
  logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
729
- return denied;
789
+ return null;
730
790
  }
731
- if (groupAllowOverride !== void 0) {
791
+ if (groupConfig?.allowFrom !== void 0) {
732
792
  if (!senderId) {
733
793
  logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
734
- return denied;
794
+ return null;
735
795
  }
736
- if (!isSenderAllowed({
737
- allow: effectiveGroupAllow,
738
- senderId
739
- })) {
796
+ if (access.senderAccess.reasonCode !== "group_policy_allowed") {
740
797
  logVerbose(`Blocked line group sender ${senderId} (group allowFrom override)`);
741
- return denied;
798
+ return null;
742
799
  }
743
800
  }
744
- const senderGroupAccess = evaluateMatchedGroupAccessForPolicy({
745
- groupPolicy,
746
- requireMatchInput: true,
747
- hasMatchInput: Boolean(senderId),
748
- allowlistConfigured: effectiveGroupAllow.entries.length > 0,
749
- allowlistMatched: Boolean(senderId) && isSenderAllowed({
750
- allow: effectiveGroupAllow,
751
- senderId
752
- })
753
- });
754
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
755
- logVerbose("Blocked line group message (groupPolicy: disabled)");
756
- return denied;
757
- }
758
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "missing_match_input") {
759
- logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
760
- return denied;
761
- }
762
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
763
- logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
764
- return denied;
765
- }
766
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "not_allowlisted") {
767
- logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
768
- return denied;
769
- }
770
- return {
771
- allowed: true,
772
- commandAuthorized: resolveLineCommandAuthorized({
773
- cfg,
774
- event,
775
- senderId,
776
- allow: effectiveGroupAllow
777
- })
778
- };
801
+ if (access.senderAccess.reasonCode === "group_policy_disabled") logVerbose("Blocked line group message (groupPolicy: disabled)");
802
+ else if (!senderId && groupPolicy === "allowlist") logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
803
+ else if (access.senderAccess.reasonCode === "group_policy_empty_allowlist") logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
804
+ else logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
805
+ return null;
779
806
  }
780
- if (dmPolicy === "disabled") {
807
+ if (access.senderAccess.reasonCode === "dm_policy_disabled") {
781
808
  logVerbose("Blocked line sender (dmPolicy: disabled)");
782
- return denied;
783
- }
784
- if (!isSenderAllowed({
785
- allow: effectiveDmAllow,
786
- senderId
787
- })) {
788
- if (dmPolicy === "pairing") {
789
- if (!senderId) {
790
- logVerbose("Blocked line sender (dmPolicy: pairing, no sender ID)");
791
- return denied;
792
- }
793
- await sendLinePairingReply({
794
- senderId,
795
- replyToken: "replyToken" in event ? event.replyToken : void 0,
796
- context
797
- });
798
- } else logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${dmPolicy})`);
799
- return denied;
809
+ return null;
800
810
  }
801
- return {
802
- allowed: true,
803
- commandAuthorized: resolveLineCommandAuthorized({
804
- cfg,
805
- event,
811
+ if (access.senderAccess.decision === "pairing") {
812
+ if (!senderId) {
813
+ logVerbose("Blocked line sender (dmPolicy: pairing, no sender ID)");
814
+ return null;
815
+ }
816
+ await sendLinePairingReply({
806
817
  senderId,
807
- allow: effectiveDmAllow
808
- })
809
- };
818
+ replyToken: "replyToken" in event ? event.replyToken : void 0,
819
+ context
820
+ });
821
+ return null;
822
+ }
823
+ logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${account.config.dmPolicy ?? "pairing"})`);
824
+ return null;
810
825
  }
811
826
  function getLineMentionees(message) {
812
827
  if (message.type !== "text") return [];
@@ -828,81 +843,29 @@ function resolveEventRawText(event) {
828
843
  if (event.type === "postback") return event.postback?.data?.trim() ?? "";
829
844
  return "";
830
845
  }
831
- function resolveLineCommandAuthorized(params) {
832
- const senderAllowedForCommands = isSenderAllowed({
833
- allow: params.allow,
834
- senderId: params.senderId
835
- });
836
- const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
837
- const rawText = resolveEventRawText(params.event);
838
- return resolveControlCommandGate({
839
- useAccessGroups,
840
- authorizers: [{
841
- configured: params.allow.hasEntries,
842
- allowed: senderAllowedForCommands
843
- }],
844
- allowTextCommands: true,
845
- hasControlCommand: hasControlCommand(rawText, params.cfg)
846
- }).commandAuthorized;
847
- }
848
846
  async function handleMessageEvent(event, context) {
849
847
  const { cfg, account, runtime, mediaMaxBytes, processMessage } = context;
850
848
  const message = event.message;
851
849
  const decision = await shouldProcessLineEvent(event, context);
852
- if (!decision.allowed) return;
850
+ if (!decision) return;
853
851
  const { isGroup, groupId, roomId } = getLineSourceInfo(event.source);
854
- if (isGroup) {
855
- const requireMention = resolveLineGroupConfig({
856
- config: account.config,
857
- groupId,
858
- roomId
859
- })?.requireMention !== false;
852
+ if (isGroup && decision.activationAccess.shouldSkip) {
860
853
  const rawText = message.type === "text" ? message.text : "";
861
854
  const sourceInfo = getLineSourceInfo(event.source);
862
- const peerId = groupId ?? roomId ?? sourceInfo.userId ?? "unknown";
863
- const { agentId } = resolveAgentRoute({
864
- cfg,
865
- channel: "line",
866
- accountId: account.accountId,
867
- peer: {
868
- kind: "group",
869
- id: peerId
855
+ logVerbose(`line: skipping group message (requireMention, not mentioned)`);
856
+ const historyKey = groupId ?? roomId;
857
+ const senderId = sourceInfo.userId ?? "unknown";
858
+ if (historyKey && context.groupHistories) recordPendingHistoryEntryIfEnabled({
859
+ historyMap: context.groupHistories,
860
+ historyKey,
861
+ limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
862
+ entry: {
863
+ sender: `user:${senderId}`,
864
+ body: rawText || `<${message.type}>`,
865
+ timestamp: event.timestamp
870
866
  }
871
867
  });
872
- const mentionRegexes = buildMentionRegexes(cfg, agentId);
873
- const wasMentionedByNative = isLineBotMentioned(message);
874
- const wasMentionedByPattern = message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
875
- const wasMentioned = wasMentionedByNative || wasMentionedByPattern;
876
- if (resolveInboundMentionDecision({
877
- facts: {
878
- canDetectMention: message.type === "text",
879
- wasMentioned,
880
- hasAnyMention: hasAnyLineMention(message),
881
- implicitMentionKinds: []
882
- },
883
- policy: {
884
- isGroup: true,
885
- requireMention,
886
- allowTextCommands: true,
887
- hasControlCommand: hasControlCommand(rawText, cfg),
888
- commandAuthorized: decision.commandAuthorized
889
- }
890
- }).shouldSkip) {
891
- logVerbose(`line: skipping group message (requireMention, not mentioned)`);
892
- const historyKey = groupId ?? roomId;
893
- const senderId = sourceInfo.userId ?? "unknown";
894
- if (historyKey && context.groupHistories) recordPendingHistoryEntryIfEnabled({
895
- historyMap: context.groupHistories,
896
- historyKey,
897
- limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
898
- entry: {
899
- sender: `user:${senderId}`,
900
- body: rawText || `<${message.type}>`,
901
- timestamp: event.timestamp
902
- }
903
- });
904
- return;
905
- }
868
+ return;
906
869
  }
907
870
  const allMedia = [];
908
871
  if (isDownloadableLineMessageType(message.type)) try {
@@ -921,7 +884,7 @@ async function handleMessageEvent(event, context) {
921
884
  allMedia,
922
885
  cfg,
923
886
  account,
924
- commandAuthorized: decision.commandAuthorized,
887
+ commandAuthorized: decision.commandAccess.authorized,
925
888
  groupHistories: context.groupHistories,
926
889
  historyLimit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
927
890
  });
@@ -959,12 +922,12 @@ async function handlePostbackEvent(event, context) {
959
922
  const data = event.postback.data;
960
923
  logVerbose(`line: received postback: ${data}`);
961
924
  const decision = await shouldProcessLineEvent(event, context);
962
- if (!decision.allowed) return;
925
+ if (!decision) return;
963
926
  const postbackContext = await buildLinePostbackContext({
964
927
  event,
965
928
  cfg: context.cfg,
966
929
  account: context.account,
967
- commandAuthorized: decision.commandAuthorized
930
+ commandAuthorized: decision.commandAccess.authorized
968
931
  });
969
932
  if (!postbackContext) return;
970
933
  await context.processMessage(postbackContext);
@@ -1049,6 +1012,20 @@ function createLineBot(opts) {
1049
1012
  };
1050
1013
  }
1051
1014
  //#endregion
1015
+ //#region extensions/line/src/monitor-durable.ts
1016
+ function hasLineChannelData(payload) {
1017
+ const lineData = payload.channelData?.line;
1018
+ return Boolean(lineData && Object.keys(lineData).length > 0);
1019
+ }
1020
+ function resolveLineDurableReplyOptions(params) {
1021
+ if (params.infoKind !== "final") return false;
1022
+ if (params.replyToken && !params.replyTokenUsed) return false;
1023
+ if (hasLineChannelData(params.payload)) return false;
1024
+ const reply = resolveSendableOutboundReplyParts(params.payload);
1025
+ if (reply.hasMedia || !reply.hasText) return false;
1026
+ return { to: params.to };
1027
+ }
1028
+ //#endregion
1052
1029
  //#region extensions/line/src/reply-chunks.ts
1053
1030
  async function sendLineReplyChunks(params) {
1054
1031
  const hasQuickReplies = Boolean(params.quickReplies?.length);
@@ -1149,6 +1126,7 @@ function createLineNodeWebhookHandler(params) {
1149
1126
  res.end(JSON.stringify({ error: "Method Not Allowed" }));
1150
1127
  return;
1151
1128
  }
1129
+ let receiveContext;
1152
1130
  try {
1153
1131
  const signatureHeader = req.headers["x-line-signature"];
1154
1132
  const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
@@ -1175,14 +1153,25 @@ function createLineNodeWebhookHandler(params) {
1175
1153
  return;
1176
1154
  }
1177
1155
  params.onRequestAuthenticated?.();
1156
+ receiveContext = createMessageReceiveContext({
1157
+ id: `${Date.now()}:line:webhook`,
1158
+ channel: "line",
1159
+ message: body,
1160
+ ackPolicy: body.events?.length ? "after_agent_dispatch" : "after_receive_record",
1161
+ onAck: () => {
1162
+ res.statusCode = 200;
1163
+ res.setHeader("Content-Type", "application/json");
1164
+ res.end(JSON.stringify({ status: "ok" }));
1165
+ }
1166
+ });
1178
1167
  if (body.events && body.events.length > 0) {
1179
1168
  logVerbose(`line: received ${body.events.length} webhook events`);
1180
1169
  await params.bot.handleWebhook(body);
1181
1170
  }
1182
- res.statusCode = 200;
1183
- res.setHeader("Content-Type", "application/json");
1184
- res.end(JSON.stringify({ status: "ok" }));
1171
+ const ackStage = body.events?.length ? "agent_dispatch" : "receive_record";
1172
+ if (receiveContext.shouldAckAfter(ackStage)) await receiveContext.ack();
1185
1173
  } catch (err) {
1174
+ await receiveContext?.nack(err);
1186
1175
  if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
1187
1176
  res.statusCode = 413;
1188
1177
  res.setHeader("Content-Type", "application/json");
@@ -1287,12 +1276,6 @@ async function monitorLineProvider(opts) {
1287
1276
  try {
1288
1277
  const textLimit = 5e3;
1289
1278
  let replyTokenUsed = false;
1290
- const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
1291
- cfg: config,
1292
- agentId: route.agentId,
1293
- channel: "line",
1294
- accountId: route.accountId
1295
- });
1296
1279
  const core = getLineRuntime();
1297
1280
  const turnResult = await core.channel.turn.run({
1298
1281
  channel: "line",
@@ -1314,9 +1297,15 @@ async function monitorLineProvider(opts) {
1314
1297
  recordInboundSession: core.channel.session.recordInboundSession,
1315
1298
  dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
1316
1299
  record: ctx.turn.record,
1317
- dispatcherOptions: { ...replyPipeline },
1318
- replyOptions: { onModelSelected },
1300
+ replyPipeline: {},
1319
1301
  delivery: {
1302
+ durable: (payload, info) => resolveLineDurableReplyOptions({
1303
+ payload,
1304
+ infoKind: info.kind,
1305
+ to: ctxPayload.From,
1306
+ replyToken,
1307
+ replyTokenUsed
1308
+ }),
1320
1309
  deliver: async (payload) => {
1321
1310
  const lineData = payload.channelData?.line ?? {};
1322
1311
  if (ctx.userId && !ctx.isGroup) showLoadingAnimation(ctx.userId, {
@@ -1365,7 +1354,7 @@ async function monitorLineProvider(opts) {
1365
1354
  })
1366
1355
  }
1367
1356
  });
1368
- if (!hasFinalInboundReplyDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1357
+ if (!hasFinalChannelTurnDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1369
1358
  } catch (err) {
1370
1359
  runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
1371
1360
  if (replyToken) try {
@@ -1520,4 +1509,4 @@ async function monitorLineProvider(opts) {
1520
1509
  };
1521
1510
  }
1522
1511
  //#endregion
1523
- export { validateLineSignature as a, isSenderAllowed as c, parseLineWebhookBody as i, normalizeAllowFrom as l, createLineNodeWebhookHandler as n, downloadLineMedia as o, readLineWebhookRequestBody as r, firstDefined as s, monitorLineProvider as t, normalizeDmAllowFromWithStore as u };
1512
+ export { validateLineSignature as a, normalizeAllowFrom as c, parseLineWebhookBody as i, createLineNodeWebhookHandler as n, downloadLineMedia as o, readLineWebhookRequestBody as r, firstDefined as s, monitorLineProvider as t };
@@ -0,0 +1,2 @@
1
+ import { t as monitorLineProvider } from "./monitor-PVeb2tJQ.js";
2
+ export { monitorLineProvider };
@@ -1,2 +1,2 @@
1
- import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, D as sendMessageLine, S as pushMessageLine, T as pushTextMessageWithQuickReplies, c as processLineMessage, m as createQuickReplyItems, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage } from "./markdown-to-line-CCWK6PJh.js";
1
+ import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, D as sendMessageLine, S as pushMessageLine, T as pushTextMessageWithQuickReplies, c as processLineMessage, m as createQuickReplyItems, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage } from "./markdown-to-line-DASNAjBq.js";
2
2
  export { buildTemplateMessageFromPayload, createQuickReplyItems, processLineMessage, pushFlexMessage, pushLocationMessage, pushMessageLine, pushMessagesLine, pushTemplateMessage, pushTextMessageWithQuickReplies, sendMessageLine };
@@ -3,17 +3,18 @@ import { a as resolveLineGroupConfigEntry, i as resolveExactLineGroupConfigKey,
3
3
  import { a as createMediaPlayerCard, i as createDeviceControlCard, n as parseLineDirectives, o as LineChannelConfigSchema, r as createAppleTvRemoteCard, s as LineConfigSchema, t as hasLineDirectives } from "./reply-payload-transform-Bmf8GnSU.js";
4
4
  import { n as createEventCard, r as createReceiptCard, t as createAgendaCard } from "./schedule-cards-BwQChTdM.js";
5
5
  import { a as createListCard, i as createInfoCard, n as createCarousel, o as createNotificationBubble, r as createImageCard, t as createActionCard } from "./basic-cards-7DTG6E-A.js";
6
- import { A as buildTemplateMessageFromPayload, B as createYesNoConfirm, C as pushMessagesLine, D as sendMessageLine, E as replyMessageLine, F as createImageCarousel, G as toFlexMessage, H as messageAction, I as createImageCarouselColumn, L as createLinkMenu, M as createButtonTemplate, N as createCarouselColumn, O as showLoadingAnimation, P as createConfirmTemplate, R as createProductCarousel, S as pushMessageLine, T as pushTextMessageWithQuickReplies, U as postbackAction, V as datetimePickerAction, W as uriAction, _ as getUserDisplayName, a as extractLinks, b as pushImageMessage, c as processLineMessage, d as createFlexMessage, f as createImageMessage, g as createVideoMessage, h as createTextMessageWithQuickReplies, i as extractCodeBlocks, j as createButtonMenu, k as resolveLineChannelAccessToken, l as stripMarkdown, m as createQuickReplyItems, n as convertLinksToFlexBubble, o as extractMarkdownTables, p as createLocationMessage, r as convertTableToFlexBubble, s as hasMarkdownToConvert, t as convertCodeBlockToFlexBubble, u as createAudioMessage, v as getUserProfile, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage, z as createTemplateCarousel } from "./markdown-to-line-CCWK6PJh.js";
7
- import { a as validateLineSignature, c as isSenderAllowed, i as parseLineWebhookBody, l as normalizeAllowFrom, n as createLineNodeWebhookHandler, o as downloadLineMedia, r as readLineWebhookRequestBody, s as firstDefined, t as monitorLineProvider, u as normalizeDmAllowFromWithStore } from "./monitor-B4_RxHc0.js";
6
+ import { A as buildTemplateMessageFromPayload, B as createYesNoConfirm, C as pushMessagesLine, D as sendMessageLine, E as replyMessageLine, F as createImageCarousel, G as toFlexMessage, H as messageAction, I as createImageCarouselColumn, L as createLinkMenu, M as createButtonTemplate, N as createCarouselColumn, O as showLoadingAnimation, P as createConfirmTemplate, R as createProductCarousel, S as pushMessageLine, T as pushTextMessageWithQuickReplies, U as postbackAction, V as datetimePickerAction, W as uriAction, _ as getUserDisplayName, a as extractLinks, b as pushImageMessage, c as processLineMessage, d as createFlexMessage, f as createImageMessage, g as createVideoMessage, h as createTextMessageWithQuickReplies, i as extractCodeBlocks, j as createButtonMenu, k as resolveLineChannelAccessToken, l as stripMarkdown, m as createQuickReplyItems, n as convertLinksToFlexBubble, o as extractMarkdownTables, p as createLocationMessage, r as convertTableToFlexBubble, s as hasMarkdownToConvert, t as convertCodeBlockToFlexBubble, u as createAudioMessage, v as getUserProfile, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage, z as createTemplateCarousel } from "./markdown-to-line-DASNAjBq.js";
7
+ import { a as validateLineSignature, c as normalizeAllowFrom, i as parseLineWebhookBody, n as createLineNodeWebhookHandler, o as downloadLineMedia, r as readLineWebhookRequestBody, s as firstDefined, t as monitorLineProvider } from "./monitor-PVeb2tJQ.js";
8
8
  import { t as probeLineBot } from "./probe-D9BPzb2n.js";
9
9
  import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
10
10
  import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
11
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
11
+ import { createMessageReceiveContext } from "openclaw/plugin-sdk/channel-message";
12
12
  import { DEFAULT_ACCOUNT_ID, formatDocsLink, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
13
13
  import { buildComputedAccountStatusSnapshot, buildTokenChannelStatusSummary } from "openclaw/plugin-sdk/status-helpers";
14
14
  import { messagingApi } from "@line/bot-sdk";
15
15
  import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
16
16
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/agent-media-payload";
17
+ import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
17
18
  import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
18
19
  //#region extensions/line/src/webhook.ts
19
20
  const LINE_WEBHOOK_MAX_RAW_BODY_BYTES = 64 * 1024;
@@ -29,6 +30,7 @@ function parseWebhookBody(rawBody) {
29
30
  function createLineWebhookMiddleware(options) {
30
31
  const { channelSecret, onEvents, runtime } = options;
31
32
  return async (req, res, _next) => {
33
+ let receiveContext;
32
34
  try {
33
35
  const signature = req.headers["x-line-signature"];
34
36
  if (!signature || typeof signature !== "string") {
@@ -54,12 +56,23 @@ function createLineWebhookMiddleware(options) {
54
56
  res.status(400).json({ error: "Invalid webhook payload" });
55
57
  return;
56
58
  }
59
+ receiveContext = createMessageReceiveContext({
60
+ id: `${Date.now()}:line:webhook`,
61
+ channel: "line",
62
+ message: body,
63
+ ackPolicy: body.events?.length ? "after_agent_dispatch" : "after_receive_record",
64
+ onAck: () => {
65
+ res.status(200).json({ status: "ok" });
66
+ }
67
+ });
57
68
  if (body.events && body.events.length > 0) {
58
69
  logVerbose(`line: received ${body.events.length} webhook events`);
59
70
  await onEvents(body);
60
71
  }
61
- res.status(200).json({ status: "ok" });
72
+ const ackStage = body.events?.length ? "agent_dispatch" : "receive_record";
73
+ if (receiveContext.shouldAckAfter(ackStage)) await receiveContext.ack();
62
74
  } catch (err) {
75
+ await receiveContext?.nack(err);
63
76
  runtime?.error?.(danger(`line webhook error: ${String(err)}`));
64
77
  if (!res.headersSent) res.status(500).json({ error: "Internal server error" });
65
78
  }
@@ -117,7 +130,7 @@ async function createRichMenu(menu, opts) {
117
130
  async function uploadRichMenuImage(richMenuId, imagePath, opts) {
118
131
  const blobClient = getBlobClient(opts);
119
132
  const media = await loadWebMediaRaw(imagePath, { localRoots: opts.mediaLocalRoots ?? getAgentScopedMediaLocalRoots(opts.cfg) });
120
- const contentType = media.contentType === "image/png" || media.contentType === "image/jpeg" ? media.contentType : normalizeLowercaseStringOrEmpty(imagePath).endsWith(".png") ? "image/png" : "image/jpeg";
133
+ const contentType = media.contentType === "image/png" || media.contentType === "image/jpeg" ? media.contentType : mimeTypeFromFilePath(imagePath) === "image/png" ? "image/png" : "image/jpeg";
121
134
  const imageBytes = new ArrayBuffer(media.buffer.byteLength);
122
135
  new Uint8Array(imageBytes).set(media.buffer);
123
136
  await blobClient.setRichMenuImage(richMenuId, new Blob([imageBytes], { type: contentType }));
@@ -274,4 +287,4 @@ function createDefaultMenuConfig() {
274
287
  };
275
288
  }
276
289
  //#endregion
277
- export { DEFAULT_ACCOUNT_ID, LineChannelConfigSchema, LineConfigSchema, buildChannelConfigSchema, buildComputedAccountStatusSnapshot, buildTemplateMessageFromPayload, buildTokenChannelStatusSummary, cancelDefaultRichMenu, clearAccountEntryFields, convertCodeBlockToFlexBubble, convertLinksToFlexBubble, convertTableToFlexBubble, createActionCard, createAgendaCard, createAppleTvRemoteCard, createAudioMessage, createButtonMenu, createButtonTemplate, createCarousel, createCarouselColumn, createConfirmTemplate, createDefaultMenuConfig, createDeviceControlCard, createEventCard, createFlexMessage, createGridLayout, createImageCard, createImageCarousel, createImageCarouselColumn, createImageMessage, createInfoCard, createLineNodeWebhookHandler, createLineWebhookMiddleware, createLinkMenu, createListCard, createLocationMessage, createMediaPlayerCard, createNotificationBubble, createProductCarousel, createQuickReplyItems, createReceiptCard, createRichMenu, createRichMenuAlias, createTemplateCarousel, createTextMessageWithQuickReplies, createVideoMessage, createYesNoConfirm, datetimePickerAction, deleteRichMenu, deleteRichMenuAlias, downloadLineMedia, extractCodeBlocks, extractLinks, extractMarkdownTables, firstDefined, formatDocsLink, getDefaultRichMenuId, getRichMenu, getRichMenuIdOfUser, getRichMenuList, getUserDisplayName, getUserProfile, hasLineDirectives, hasMarkdownToConvert, isSenderAllowed, linkRichMenuToUser, linkRichMenuToUsers, listLineAccountIds, messageAction, monitorLineProvider, normalizeAccountId, normalizeAllowFrom, normalizeDmAllowFromWithStore, parseLineDirectives, parseLineWebhookBody, postbackAction, probeLineBot, processLineMessage, pushFlexMessage, pushImageMessage, pushLocationMessage, pushMessageLine, pushMessagesLine, pushTemplateMessage, pushTextMessageWithQuickReplies, readLineWebhookRequestBody, replyMessageLine, resolveDefaultLineAccountId, resolveExactLineGroupConfigKey, resolveLineAccount, resolveLineChannelAccessToken, resolveLineGroupConfigEntry, resolveLineGroupLookupIds, resolveLineGroupsConfig, sendMessageLine, setDefaultRichMenu, setLineRuntime, setSetupChannelEnabled, showLoadingAnimation, splitSetupEntries, startLineWebhook, stripMarkdown, toFlexMessage, unlinkRichMenuFromUser, unlinkRichMenuFromUsers, uploadRichMenuImage, uriAction, validateLineSignature };
290
+ export { DEFAULT_ACCOUNT_ID, LineChannelConfigSchema, LineConfigSchema, buildChannelConfigSchema, buildComputedAccountStatusSnapshot, buildTemplateMessageFromPayload, buildTokenChannelStatusSummary, cancelDefaultRichMenu, clearAccountEntryFields, convertCodeBlockToFlexBubble, convertLinksToFlexBubble, convertTableToFlexBubble, createActionCard, createAgendaCard, createAppleTvRemoteCard, createAudioMessage, createButtonMenu, createButtonTemplate, createCarousel, createCarouselColumn, createConfirmTemplate, createDefaultMenuConfig, createDeviceControlCard, createEventCard, createFlexMessage, createGridLayout, createImageCard, createImageCarousel, createImageCarouselColumn, createImageMessage, createInfoCard, createLineNodeWebhookHandler, createLineWebhookMiddleware, createLinkMenu, createListCard, createLocationMessage, createMediaPlayerCard, createNotificationBubble, createProductCarousel, createQuickReplyItems, createReceiptCard, createRichMenu, createRichMenuAlias, createTemplateCarousel, createTextMessageWithQuickReplies, createVideoMessage, createYesNoConfirm, datetimePickerAction, deleteRichMenu, deleteRichMenuAlias, downloadLineMedia, extractCodeBlocks, extractLinks, extractMarkdownTables, firstDefined, formatDocsLink, getDefaultRichMenuId, getRichMenu, getRichMenuIdOfUser, getRichMenuList, getUserDisplayName, getUserProfile, hasLineDirectives, hasMarkdownToConvert, linkRichMenuToUser, linkRichMenuToUsers, listLineAccountIds, messageAction, monitorLineProvider, normalizeAccountId, normalizeAllowFrom, parseLineDirectives, parseLineWebhookBody, postbackAction, probeLineBot, processLineMessage, pushFlexMessage, pushImageMessage, pushLocationMessage, pushMessageLine, pushMessagesLine, pushTemplateMessage, pushTextMessageWithQuickReplies, readLineWebhookRequestBody, replyMessageLine, resolveDefaultLineAccountId, resolveExactLineGroupConfigKey, resolveLineAccount, resolveLineChannelAccessToken, resolveLineGroupConfigEntry, resolveLineGroupLookupIds, resolveLineGroupsConfig, sendMessageLine, setDefaultRichMenu, setLineRuntime, setSetupChannelEnabled, showLoadingAnimation, splitSetupEntries, startLineWebhook, stripMarkdown, toFlexMessage, unlinkRichMenuFromUser, unlinkRichMenuFromUsers, uploadRichMenuImage, uriAction, validateLineSignature };
@@ -1,3 +1,4 @@
1
+ import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-message";
1
2
  import { resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
2
3
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
3
4
  //#region extensions/line/src/outbound-media.ts
@@ -52,4 +53,21 @@ async function resolveLineOutboundMedia(mediaUrl, opts = {}) {
52
53
  throw new Error("LINE outbound media currently requires a public HTTPS URL");
53
54
  }
54
55
  //#endregion
55
- export { validateLineMediaUrl as n, resolveLineOutboundMedia as t };
56
+ //#region extensions/line/src/send-receipt.ts
57
+ function createLineSendReceipt(params) {
58
+ const messageId = params.messageId.trim();
59
+ const chatId = params.chatId.trim();
60
+ return createMessageReceiptFromOutboundResults({
61
+ results: messageId ? [{
62
+ channel: "line",
63
+ messageId,
64
+ chatId,
65
+ conversationId: chatId,
66
+ meta: { messageCount: params.messageCount ?? 1 }
67
+ }] : [],
68
+ ...chatId ? { threadId: chatId } : {},
69
+ kind: params.kind ?? "unknown"
70
+ });
71
+ }
72
+ //#endregion
73
+ export { resolveLineOutboundMedia as n, validateLineMediaUrl as r, createLineSendReceipt as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/line",
3
- "version": "2026.5.7",
3
+ "version": "2026.5.10-beta.1",
4
4
  "description": "OpenClaw LINE channel plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,7 +15,7 @@
15
15
  "openclaw": "workspace:*"
16
16
  },
17
17
  "peerDependencies": {
18
- "openclaw": ">=2026.5.7"
18
+ "openclaw": ">=2026.5.10-beta.1"
19
19
  },
20
20
  "peerDependenciesMeta": {
21
21
  "openclaw": {
@@ -45,10 +45,10 @@
45
45
  "minHostVersion": ">=2026.4.10"
46
46
  },
47
47
  "compat": {
48
- "pluginApi": ">=2026.5.7"
48
+ "pluginApi": ">=2026.5.10-beta.1"
49
49
  },
50
50
  "build": {
51
- "openclawVersion": "2026.5.7"
51
+ "openclawVersion": "2026.5.10-beta.1"
52
52
  },
53
53
  "release": {
54
54
  "publishToClawHub": true,
@@ -1,4 +0,0 @@
1
- import { S as pushMessageLine } from "./markdown-to-line-CCWK6PJh.js";
2
- import { t as monitorLineProvider } from "./monitor-B4_RxHc0.js";
3
- import { t as probeLineBot } from "./probe-D9BPzb2n.js";
4
- export { monitorLineProvider, probeLineBot, pushMessageLine };
@@ -1,2 +0,0 @@
1
- import { t as monitorLineProvider } from "./monitor-B4_RxHc0.js";
2
- export { monitorLineProvider };