@openclaw/line 2026.7.2-beta.2 → 2026.7.2-beta.3

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.
@@ -1,55 +1,113 @@
1
1
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
2
2
  import { resolveAccountEntry } from "openclaw/plugin-sdk/account-resolution";
3
- import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
3
+ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
4
4
  //#region extensions/line/src/accounts.ts
5
- function readFileIfExists(filePath) {
6
- return tryReadSecretFileSync(filePath, "LINE credential file", { rejectSymlink: true });
5
+ function readCredentialFile(filePath, configPath) {
6
+ return tryReadSecretFileSync(filePath, "LINE credential file", { rejectSymlink: true }, { configPath });
7
7
  }
8
8
  function resolveToken(params) {
9
9
  const { accountId, baseConfig, accountConfig } = params;
10
10
  if (accountConfig?.channelAccessToken?.trim()) return {
11
- token: accountConfig.channelAccessToken.trim(),
12
- tokenSource: "config"
13
- };
14
- const accountFileToken = readFileIfExists(accountConfig?.tokenFile);
15
- if (accountFileToken) return {
16
- token: accountFileToken,
17
- tokenSource: "file"
11
+ value: accountConfig.channelAccessToken.trim(),
12
+ source: "config",
13
+ status: "available"
18
14
  };
15
+ if (accountConfig?.tokenFile?.trim()) {
16
+ const result = readCredentialFile(accountConfig.tokenFile, `channels.line.accounts.${accountId}.tokenFile`);
17
+ return result.status === "available" ? {
18
+ value: result.value,
19
+ source: "file",
20
+ status: "available"
21
+ } : {
22
+ value: "",
23
+ source: "file",
24
+ status: "configured_unavailable",
25
+ diagnostic: result.diagnostic
26
+ };
27
+ }
19
28
  if (accountId === DEFAULT_ACCOUNT_ID) {
20
29
  if (baseConfig?.channelAccessToken?.trim()) return {
21
- token: baseConfig.channelAccessToken.trim(),
22
- tokenSource: "config"
23
- };
24
- const baseFileToken = readFileIfExists(baseConfig?.tokenFile);
25
- if (baseFileToken) return {
26
- token: baseFileToken,
27
- tokenSource: "file"
30
+ value: baseConfig.channelAccessToken.trim(),
31
+ source: "config",
32
+ status: "available"
28
33
  };
34
+ if (baseConfig?.tokenFile?.trim()) {
35
+ const result = readCredentialFile(baseConfig.tokenFile, "channels.line.tokenFile");
36
+ return result.status === "available" ? {
37
+ value: result.value,
38
+ source: "file",
39
+ status: "available"
40
+ } : {
41
+ value: "",
42
+ source: "file",
43
+ status: "configured_unavailable",
44
+ diagnostic: result.diagnostic
45
+ };
46
+ }
29
47
  const envToken = process.env.LINE_CHANNEL_ACCESS_TOKEN?.trim();
30
48
  if (envToken) return {
31
- token: envToken,
32
- tokenSource: "env"
49
+ value: envToken,
50
+ source: "env",
51
+ status: "available"
33
52
  };
34
53
  }
35
54
  return {
36
- token: "",
37
- tokenSource: "none"
55
+ value: "",
56
+ source: "none",
57
+ status: "missing"
38
58
  };
39
59
  }
40
60
  function resolveSecret(params) {
41
61
  const { accountId, baseConfig, accountConfig } = params;
42
- if (accountConfig?.channelSecret?.trim()) return accountConfig.channelSecret.trim();
43
- const accountFileSecret = readFileIfExists(accountConfig?.secretFile);
44
- if (accountFileSecret) return accountFileSecret;
62
+ if (accountConfig?.channelSecret?.trim()) return {
63
+ value: accountConfig.channelSecret.trim(),
64
+ source: "config",
65
+ status: "available"
66
+ };
67
+ if (accountConfig?.secretFile?.trim()) {
68
+ const result = readCredentialFile(accountConfig.secretFile, `channels.line.accounts.${accountId}.secretFile`);
69
+ return result.status === "available" ? {
70
+ value: result.value,
71
+ source: "file",
72
+ status: "available"
73
+ } : {
74
+ value: "",
75
+ source: "file",
76
+ status: "configured_unavailable",
77
+ diagnostic: result.diagnostic
78
+ };
79
+ }
45
80
  if (accountId === DEFAULT_ACCOUNT_ID) {
46
- if (baseConfig?.channelSecret?.trim()) return baseConfig.channelSecret.trim();
47
- const baseFileSecret = readFileIfExists(baseConfig?.secretFile);
48
- if (baseFileSecret) return baseFileSecret;
81
+ if (baseConfig?.channelSecret?.trim()) return {
82
+ value: baseConfig.channelSecret.trim(),
83
+ source: "config",
84
+ status: "available"
85
+ };
86
+ if (baseConfig?.secretFile?.trim()) {
87
+ const result = readCredentialFile(baseConfig.secretFile, "channels.line.secretFile");
88
+ return result.status === "available" ? {
89
+ value: result.value,
90
+ source: "file",
91
+ status: "available"
92
+ } : {
93
+ value: "",
94
+ source: "file",
95
+ status: "configured_unavailable",
96
+ diagnostic: result.diagnostic
97
+ };
98
+ }
49
99
  const envSecret = process.env.LINE_CHANNEL_SECRET?.trim();
50
- if (envSecret) return envSecret;
100
+ if (envSecret) return {
101
+ value: envSecret,
102
+ source: "env",
103
+ status: "available"
104
+ };
51
105
  }
52
- return "";
106
+ return {
107
+ value: "",
108
+ source: "none",
109
+ status: "missing"
110
+ };
53
111
  }
54
112
  function resolveLineAccount(params) {
55
113
  const cfg = params.cfg;
@@ -57,7 +115,7 @@ function resolveLineAccount(params) {
57
115
  const lineConfig = cfg.channels?.line;
58
116
  const accounts = lineConfig?.accounts;
59
117
  const accountConfig = resolveAccountEntry(accounts, accountId);
60
- const { token, tokenSource } = resolveToken({
118
+ const token = resolveToken({
61
119
  accountId,
62
120
  baseConfig: lineConfig,
63
121
  accountConfig
@@ -79,9 +137,13 @@ function resolveLineAccount(params) {
79
137
  accountId,
80
138
  name: accountConfig?.name ?? (accountId === DEFAULT_ACCOUNT_ID ? lineConfig?.name : void 0),
81
139
  enabled,
82
- channelAccessToken: token,
83
- channelSecret: secret,
84
- tokenSource,
140
+ channelAccessToken: token.value,
141
+ channelSecret: secret.value,
142
+ tokenSource: token.source,
143
+ signingSecretSource: secret.source,
144
+ tokenStatus: token.status,
145
+ signingSecretStatus: secret.status,
146
+ ...[token.diagnostic, secret.diagnostic].some(Boolean) ? { credentialDiagnostics: [token.diagnostic, secret.diagnostic].filter((diagnostic) => Boolean(diagnostic)) } : {},
85
147
  config: mergedConfig
86
148
  };
87
149
  }
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-BHR-GoJ0.js";
2
- import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-B7W6pwn3.js";
1
+ import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-uT5RTtpi.js";
2
+ import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-BEt4mazl.js";
3
3
  //#region extensions/line/src/channel.setup.ts
4
4
  const lineSetupPlugin = {
5
5
  id: "line",
@@ -1,2 +1,2 @@
1
- import { t as linePlugin } from "./channel-BHR-GoJ0.js";
1
+ import { t as linePlugin } from "./channel-uT5RTtpi.js";
2
2
  export { linePlugin };
@@ -1,5 +1,5 @@
1
- import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-DJVOv1JI.js";
2
- import { n as lineSetupAdapter, r as hasLineCredentials, t as lineSetupWizard } from "./setup-surface-B7W6pwn3.js";
1
+ import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BUBOoW5Q.js";
2
+ import { n as lineSetupAdapter, r as hasLineCredentials, t as lineSetupWizard } from "./setup-surface-BEt4mazl.js";
3
3
  import { a as buildLineMediaMessage, d as LineChannelConfigSchema, i as buildLineQuickReplyFallbackText, l as getLineRuntime, n as parseLineDirectives, o as hasLineSpecificMediaOptions, p as resolveExactLineGroupConfigKey, r as createLineSendReceipt, s as resolveLineOutboundMedia, t as hasLineDirectives } from "./reply-payload-transform-r9sa-TYT.js";
4
4
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
5
5
  import { buildDmGroupAccountAllowlistAdapter, createFlatAllowlistOverrideResolver } from "openclaw/plugin-sdk/allowlist-config-edit";
@@ -8,10 +8,10 @@ import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing
8
8
  import { buildChannelGroupsScopeTree, createRestrictSendersChannelSecurity, resolveScopeRequireMention } from "openclaw/plugin-sdk/channel-policy";
9
9
  import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
10
10
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1 } from "openclaw/plugin-sdk/account-id";
11
- import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
12
11
  import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
13
12
  import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
14
13
  import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
14
+ import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
15
15
  import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
16
16
  import { createAttachedChannelResultAdapter, createEmptyChannelResult } from "openclaw/plugin-sdk/channel-send-result";
17
17
  import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
@@ -104,14 +104,19 @@ const lineChannelPluginCommon = {
104
104
  describeAccount: (account) => describeWebhookAccountSnapshot({
105
105
  account,
106
106
  configured: hasLineCredentials(account),
107
- extra: { tokenSource: account.tokenSource ?? void 0 }
107
+ extra: {
108
+ tokenSource: account.tokenSource ?? void 0,
109
+ signingSecretSource: account.signingSecretSource ?? void 0,
110
+ tokenStatus: account.tokenStatus,
111
+ signingSecretStatus: account.signingSecretStatus
112
+ }
108
113
  })
109
114
  }
110
115
  };
111
116
  //#endregion
112
117
  //#region extensions/line/src/gateway.ts
113
118
  const loadLineProbeRuntime$1 = createLazyRuntimeModule(() => import("./probe.runtime-CRhVGqwl.js"));
114
- const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-CZuQgsEE.js"));
119
+ const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-CHlqxGVL.js"));
115
120
  const lineGatewayAdapter = {
116
121
  startAccount: async (ctx) => {
117
122
  const account = ctx.account;
@@ -226,7 +231,7 @@ function inferLineTargetChatType(target) {
226
231
  }
227
232
  //#endregion
228
233
  //#region extensions/line/src/outbound.ts
229
- const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-DnpU0Cms.js"));
234
+ const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-ByZ0Dsvu.js"));
230
235
  const lineOutboundAdapter = {
231
236
  deliveryMode: "direct",
232
237
  chunker: (text, limit) => getLineRuntime().channel.text.chunkMarkdownText(text, limit),
@@ -514,13 +519,16 @@ const lineStatusAdapter = createComputedAccountStatusAdapter({
514
519
  configured: hasLineCredentials(account),
515
520
  extra: {
516
521
  tokenSource: account.tokenSource,
522
+ signingSecretSource: account.signingSecretSource,
523
+ tokenStatus: account.tokenStatus,
524
+ signingSecretStatus: account.signingSecretStatus,
517
525
  mode: "webhook"
518
526
  }
519
527
  })
520
528
  });
521
529
  //#endregion
522
530
  //#region extensions/line/src/channel.ts
523
- const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-DFgZ_Xwn.js"));
531
+ const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-BqNglqnr.js"));
524
532
  const lineSecurityAdapter = createRestrictSendersChannelSecurity({
525
533
  channelKey: "line",
526
534
  resolveDmPolicy: (account) => account.config.dmPolicy,
@@ -1,4 +1,4 @@
1
- import { t as monitorLineProvider } from "./monitor-Db2rZsSX.js";
1
+ import { t as monitorLineProvider } from "./monitor-Bk-lpoOZ.js";
2
2
  import { t as probeLineBot } from "./probe-BslD77tJ.js";
3
- import { S as pushMessageLine } from "./markdown-to-line-XceqeKRP.js";
3
+ import { S as pushMessageLine } from "./markdown-to-line-D1vapcdP.js";
4
4
  export { monitorLineProvider, probeLineBot, pushMessageLine };
@@ -1,2 +1,2 @@
1
- import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-DJVOv1JI.js";
1
+ import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BUBOoW5Q.js";
2
2
  export { listLineAccountIds, resolveDefaultLineAccountId, resolveLineAccount };
@@ -1,4 +1,4 @@
1
- import { i as resolveLineAccount } from "./accounts-DJVOv1JI.js";
1
+ import { i as resolveLineAccount } from "./accounts-BUBOoW5Q.js";
2
2
  import { c as validateLineMediaUrl, r as createLineSendReceipt } from "./reply-payload-transform-r9sa-TYT.js";
3
3
  import { c as uriAction, d as createReceiptCard, o as messageAction, s as postbackAction, t as toFlexMessage } from "./flex-templates-K1lS9i8L.js";
4
4
  import { stripMarkdown, stripMarkdown as stripMarkdown$1 } from "openclaw/plugin-sdk/text-chunking";
@@ -7,6 +7,7 @@ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
7
7
  import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
8
8
  import { messagingApi } from "@line/bot-sdk";
9
9
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
10
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
10
11
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
11
12
  //#region extensions/line/src/template-messages.ts
12
13
  const COMPACT_TEMPLATE_TEXT_LIMIT = 60;
@@ -221,6 +222,14 @@ function resolveLineChannelAccessToken(explicit, params) {
221
222
  //#region extensions/line/src/send.ts
222
223
  const userProfileCache = /* @__PURE__ */ new Map();
223
224
  const PROFILE_CACHE_TTL_MS = 300 * 1e3;
225
+ const PROFILE_CACHE_MAX_ENTRIES = 1e3;
226
+ function cacheUserProfile(userId, profile) {
227
+ userProfileCache.delete(userId);
228
+ userProfileCache.set(userId, profile);
229
+ if (userProfileCache.size <= PROFILE_CACHE_MAX_ENTRIES) return;
230
+ for (const [key, cached] of userProfileCache) if (profile.fetchedAt - cached.fetchedAt >= PROFILE_CACHE_TTL_MS) userProfileCache.delete(key);
231
+ pruneMapToMaxSize(userProfileCache, PROFILE_CACHE_MAX_ENTRIES);
232
+ }
224
233
  function normalizeTarget(to) {
225
234
  const trimmed = to.trim();
226
235
  if (!trimmed) throw new Error("Recipient is required for LINE sends");
@@ -469,7 +478,7 @@ async function getUserProfile(userId, opts) {
469
478
  displayName: profile.displayName,
470
479
  pictureUrl: profile.pictureUrl
471
480
  };
472
- userProfileCache.set(userId, {
481
+ cacheUserProfile(userId, {
473
482
  ...result,
474
483
  fetchedAt: Date.now()
475
484
  });
@@ -1,9 +1,9 @@
1
- import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-DJVOv1JI.js";
1
+ import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BUBOoW5Q.js";
2
2
  import { a as buildLineMediaMessage, i as buildLineQuickReplyFallbackText, l as getLineRuntime, m as resolveLineGroupConfigEntry, o as hasLineSpecificMediaOptions } from "./reply-payload-transform-r9sa-TYT.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, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-XceqeKRP.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, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-D1vapcdP.js";
4
4
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
5
5
  import { normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
6
- import { createMessageReceiveContext } from "openclaw/plugin-sdk/channel-outbound";
6
+ import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressDrain } from "openclaw/plugin-sdk/channel-outbound";
7
7
  import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
8
8
  import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
9
9
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -25,7 +25,6 @@ import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
25
25
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
26
26
  import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
27
27
  import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
28
- import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
29
28
  import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
30
29
  import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
31
30
  import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
@@ -458,6 +457,7 @@ async function finalizeLineInboundContext(params) {
458
457
  From: fromAddress,
459
458
  To: toAddress,
460
459
  SessionKey: params.route.sessionKey,
460
+ DmScope: params.route.dmScope,
461
461
  AccountId: params.route.accountId,
462
462
  ChatType: params.source.isGroup ? "group" : "direct",
463
463
  ConversationLabel: conversationLabel,
@@ -699,37 +699,9 @@ const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
699
699
  function isDownloadableLineMessageType(messageType) {
700
700
  return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
701
701
  }
702
- const LINE_WEBHOOK_REPLAY_WINDOW_MS = 600 * 1e3;
703
- const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
704
702
  function normalizeLineIngressEntry(value) {
705
703
  return normalizeLineAllowEntry(value) || null;
706
704
  }
707
- function createLineWebhookReplayCache() {
708
- return createChannelReplayGuard({
709
- dedupe: {
710
- ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
711
- memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
712
- },
713
- buildReplayKey: ({ event, accountId }) => buildLineWebhookReplayKey(event, accountId)?.key
714
- });
715
- }
716
- function buildLineWebhookReplayKey(event, accountId) {
717
- if (event.type === "message") {
718
- const messageId = event.message?.id?.trim();
719
- if (messageId) return {
720
- key: `${accountId}|message:${messageId}`,
721
- eventId: `message:${messageId}`
722
- };
723
- }
724
- const eventId = event.webhookEventId?.trim();
725
- if (!eventId) return null;
726
- const source = event.source;
727
- const sourceId = source?.type === "group" ? `group:${source.groupId ?? ""}` : source?.type === "room" ? `room:${source.roomId ?? ""}` : `user:${source?.userId ?? ""}`;
728
- return {
729
- key: `${accountId}|${event.type}|${sourceId}|${eventId}`,
730
- eventId: `event:${eventId}`
731
- };
732
- }
733
705
  function resolveLineGroupConfig(params) {
734
706
  return resolveLineGroupConfigEntry(params.config.groups, {
735
707
  groupId: params.groupId,
@@ -998,7 +970,7 @@ async function handleMessageEvent(event, context) {
998
970
  logVerbose("line: skipping empty message");
999
971
  return;
1000
972
  }
1001
- await processMessage(messageContext);
973
+ await processMessage(messageContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
1002
974
  historyReservation.commit();
1003
975
  } finally {
1004
976
  historyReservation.release();
@@ -1032,30 +1004,12 @@ async function handlePostbackEvent(event, context) {
1032
1004
  commandAuthorized: decision.commandAccess.authorized
1033
1005
  });
1034
1006
  if (!postbackContext) return;
1035
- await context.processMessage(postbackContext);
1007
+ await context.processMessage(postbackContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
1036
1008
  }
1037
1009
  async function handleLineWebhookEvents(events, context) {
1038
1010
  let firstError;
1039
1011
  for (const event of events) try {
1040
- if (!context.replayCache) {
1041
- await handleLineWebhookEvent(event, context);
1042
- continue;
1043
- }
1044
- const replayEvent = {
1045
- event,
1046
- accountId: context.account.accountId
1047
- };
1048
- const result = await context.replayCache.processGuarded(replayEvent, async () => await handleLineWebhookEvent(event, context), { onError: "commit" });
1049
- const replayId = buildLineWebhookReplayKey(event, context.account.accountId)?.eventId;
1050
- if (result.kind === "inflight") {
1051
- logVerbose(`line: skipped in-flight replayed webhook event ${replayId ?? "unknown"}`);
1052
- try {
1053
- await result.pending;
1054
- } catch (err) {
1055
- context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
1056
- firstError ??= err;
1057
- }
1058
- } else if (result.kind === "duplicate") logVerbose(`line: skipped replayed webhook event ${replayId ?? "unknown"}`);
1012
+ await handleLineWebhookEvent(event, context);
1059
1013
  } catch (err) {
1060
1014
  context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
1061
1015
  firstError ??= err;
@@ -1093,6 +1047,221 @@ function toLintErrorObject(value, fallbackMessage) {
1093
1047
  return error;
1094
1048
  }
1095
1049
  //#endregion
1050
+ //#region extensions/line/src/webhook-spool.ts
1051
+ const LINE_WEBHOOK_SPOOL_VERSION = 1;
1052
+ const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
1053
+ const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
1054
+ const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
1055
+ const LINE_WEBHOOK_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
1056
+ const LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES = 4096;
1057
+ var LineWebhookPayloadError = class extends Error {
1058
+ constructor(message, options) {
1059
+ super(message, options);
1060
+ this.name = "LineWebhookPayloadError";
1061
+ }
1062
+ };
1063
+ var LineWebhookTerminalDeliveryError = class extends Error {
1064
+ constructor(message, options) {
1065
+ super(message, options);
1066
+ this.reason = "delivery-side-effects-committed";
1067
+ this.name = "LineWebhookTerminalDeliveryError";
1068
+ }
1069
+ };
1070
+ function nonEmptyString(value) {
1071
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1072
+ }
1073
+ /** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
1074
+ function eventIdFor(event) {
1075
+ if (!event || typeof event !== "object") throw new LineWebhookPayloadError("LINE webhook event must be an object.");
1076
+ const candidate = event;
1077
+ if (candidate.type === "message") {
1078
+ const messageId = nonEmptyString(candidate.message?.id);
1079
+ if (messageId) return `message:${messageId}`;
1080
+ }
1081
+ const webhookEventId = nonEmptyString(candidate.webhookEventId);
1082
+ if (webhookEventId) return `event:${webhookEventId}`;
1083
+ throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
1084
+ }
1085
+ function laneKeyFor(event, eventId) {
1086
+ if (!event || typeof event !== "object") return eventId;
1087
+ const source = event.source;
1088
+ if (source?.type === "group") {
1089
+ const groupId = nonEmptyString(source.groupId);
1090
+ if (groupId) return `group:${groupId}`;
1091
+ }
1092
+ if (source?.type === "room") {
1093
+ const roomId = nonEmptyString(source.roomId);
1094
+ if (roomId) return `room:${roomId}`;
1095
+ }
1096
+ if (source?.type === "user") {
1097
+ const userId = nonEmptyString(source.userId);
1098
+ if (userId) return `user:${userId}`;
1099
+ }
1100
+ return eventId;
1101
+ }
1102
+ function parseClaimedEvent(payload, claimedId) {
1103
+ if (payload.version !== LINE_WEBHOOK_SPOOL_VERSION || typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
1104
+ let event;
1105
+ try {
1106
+ event = JSON.parse(payload.rawEvent);
1107
+ } catch (error) {
1108
+ throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
1109
+ }
1110
+ if (eventIdFor(event) !== claimedId) throw new LineWebhookPayloadError("LINE webhook event id changed after durable admission.");
1111
+ return event;
1112
+ }
1113
+ function errorText(error) {
1114
+ return error instanceof Error ? error.message : String(error);
1115
+ }
1116
+ function isLineAuthenticationFailure(error) {
1117
+ if (!error || typeof error !== "object") return false;
1118
+ const status = error.status;
1119
+ return status === 401 || status === 403;
1120
+ }
1121
+ function createLineWebhookSpool(options) {
1122
+ const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
1123
+ const shutdown = new AbortController();
1124
+ const activeDeliveries = /* @__PURE__ */ new Set();
1125
+ const deferredClaims = /* @__PURE__ */ new Map();
1126
+ const drain = createChannelIngressDrain({
1127
+ queue,
1128
+ abortSignal: shutdown.signal,
1129
+ adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
1130
+ orderBy: "received",
1131
+ scanLimit: LINE_WEBHOOK_DRAIN_SCAN_LIMIT,
1132
+ startLimit: LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
1133
+ retryPolicy: {
1134
+ maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
1135
+ deadLetterMinAgeMs: 0
1136
+ },
1137
+ resolveNonRetryableFailure: (error) => {
1138
+ if (error instanceof LineWebhookPayloadError) return {
1139
+ reason: "invalid-event",
1140
+ message: error.message
1141
+ };
1142
+ if (error instanceof LineWebhookTerminalDeliveryError) return {
1143
+ reason: error.reason,
1144
+ message: error.message
1145
+ };
1146
+ if (isLineAuthenticationFailure(error)) return {
1147
+ reason: "authentication-failed",
1148
+ message: errorText(error)
1149
+ };
1150
+ return null;
1151
+ },
1152
+ onLog: (message) => options.runtime.error?.(danger(`line: ${message}`)),
1153
+ dispatchClaimedEvent: async (claimed, lifecycle) => {
1154
+ const event = parseClaimedEvent(claimed.payload, claimed.id);
1155
+ const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
1156
+ let resolveDeferredClaim;
1157
+ const deferredClaim = new Promise((resolve) => {
1158
+ resolveDeferredClaim = resolve;
1159
+ });
1160
+ let deferredClaimSettled = false;
1161
+ const settleDeferredClaim = () => {
1162
+ if (deferredClaimSettled) return;
1163
+ deferredClaimSettled = true;
1164
+ if (deferredClaims.get(claimed.id) === deferredClaim) deferredClaims.delete(claimed.id);
1165
+ resolveDeferredClaim();
1166
+ };
1167
+ const delivery = options.deliver(event, claimed.payload.destination, { turnAdoptionLifecycle: {
1168
+ ...boundLifecycle,
1169
+ onAdopted: async () => {
1170
+ try {
1171
+ await boundLifecycle.onAdopted();
1172
+ } finally {
1173
+ settleDeferredClaim();
1174
+ }
1175
+ },
1176
+ onDeferred: () => {
1177
+ if (!deferredClaimSettled) deferredClaims.set(claimed.id, deferredClaim);
1178
+ boundLifecycle.onDeferred();
1179
+ },
1180
+ onAbandoned: async () => {
1181
+ try {
1182
+ await boundLifecycle.onAbandoned();
1183
+ } finally {
1184
+ settleDeferredClaim();
1185
+ }
1186
+ }
1187
+ } });
1188
+ activeDeliveries.add(delivery);
1189
+ try {
1190
+ await delivery;
1191
+ } finally {
1192
+ activeDeliveries.delete(delivery);
1193
+ }
1194
+ }
1195
+ });
1196
+ let running = false;
1197
+ let drainRequested = false;
1198
+ let drainTask;
1199
+ let drainTimer;
1200
+ const requestDrain = () => {
1201
+ if (!running || shutdown.signal.aborted) return;
1202
+ drainRequested = true;
1203
+ if (drainTask) return;
1204
+ drainTask = runDetachedWebhookWork(async () => {
1205
+ while (drainRequested && !shutdown.signal.aborted) {
1206
+ if (!running) break;
1207
+ drainRequested = false;
1208
+ await drain.drainOnce({ shouldStop: () => !running || shutdown.signal.aborted || activeDeliveries.size >= LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES });
1209
+ }
1210
+ }).catch((error) => {
1211
+ options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`));
1212
+ }).finally(() => {
1213
+ drainTask = void 0;
1214
+ if (running && drainRequested && !shutdown.signal.aborted) requestDrain();
1215
+ });
1216
+ };
1217
+ return {
1218
+ accept: async (body) => {
1219
+ const events = body.events ?? [];
1220
+ if (events.length === 0) return;
1221
+ await queue.prune({
1222
+ completedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
1223
+ completedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES,
1224
+ failedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
1225
+ failedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES
1226
+ });
1227
+ const receivedAt = Date.now();
1228
+ for (const event of events) {
1229
+ const eventId = eventIdFor(event);
1230
+ await queue.enqueue(eventId, {
1231
+ version: LINE_WEBHOOK_SPOOL_VERSION,
1232
+ rawEvent: JSON.stringify(event),
1233
+ destination: body.destination ?? ""
1234
+ }, {
1235
+ receivedAt,
1236
+ laneKey: laneKeyFor(event, eventId)
1237
+ });
1238
+ }
1239
+ requestDrain();
1240
+ },
1241
+ start: () => {
1242
+ if (running) return;
1243
+ running = true;
1244
+ requestDrain();
1245
+ drainTimer = setInterval(requestDrain, LINE_WEBHOOK_DRAIN_INTERVAL_MS);
1246
+ drainTimer.unref?.();
1247
+ },
1248
+ stop: async () => {
1249
+ if (!running && shutdown.signal.aborted) return;
1250
+ running = false;
1251
+ if (drainTimer) {
1252
+ clearInterval(drainTimer);
1253
+ drainTimer = void 0;
1254
+ }
1255
+ shutdown.abort();
1256
+ await drainTask;
1257
+ await Promise.allSettled(activeDeliveries);
1258
+ await Promise.allSettled(deferredClaims.values());
1259
+ await drain.waitForIdle();
1260
+ drain.dispose();
1261
+ }
1262
+ };
1263
+ }
1264
+ //#endregion
1096
1265
  //#region extensions/line/src/bot.ts
1097
1266
  function createLineBot(opts) {
1098
1267
  const runtime = opts.runtime ?? createNonExitingRuntime();
@@ -1105,24 +1274,26 @@ function createLineBot(opts) {
1105
1274
  const processMessage = opts.onMessage ?? (async () => {
1106
1275
  logVerbose("line: no message handler configured");
1107
1276
  });
1108
- const replayCache = createLineWebhookReplayCache();
1109
1277
  const groupHistories = /* @__PURE__ */ new Map();
1110
- const handleWebhook = async (body) => {
1111
- if (!body.events || body.events.length === 0) return;
1112
- await handleLineWebhookEvents(body.events, {
1278
+ const spool = createLineWebhookSpool({
1279
+ accountId: account.accountId,
1280
+ runtime,
1281
+ deliver: async (event, _destination, control) => await handleLineWebhookEvents([event], {
1113
1282
  cfg,
1114
1283
  account,
1115
1284
  runtime,
1116
1285
  mediaMaxBytes,
1117
1286
  processMessage,
1118
- replayCache,
1287
+ ...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
1119
1288
  groupHistories,
1120
1289
  historyLimit: cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
1121
- });
1122
- };
1290
+ })
1291
+ });
1292
+ spool.start();
1123
1293
  return {
1124
- handleWebhook,
1125
- account
1294
+ handleWebhook: spool.accept,
1295
+ account,
1296
+ stop: spool.stop
1126
1297
  };
1127
1298
  }
1128
1299
  //#endregion
@@ -1214,9 +1385,6 @@ async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_
1214
1385
  timeoutMs
1215
1386
  });
1216
1387
  }
1217
- function logLineWebhookDispatchError(runtime, err) {
1218
- runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
1219
- }
1220
1388
  function createLineNodeWebhookHandler(params) {
1221
1389
  const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
1222
1390
  const readBody = params.readBody ?? readLineWebhookRequestBody;
@@ -1239,7 +1407,6 @@ function createLineNodeWebhookHandler(params) {
1239
1407
  res.end(JSON.stringify({ error: "Method Not Allowed" }));
1240
1408
  return;
1241
1409
  }
1242
- let receiveContext;
1243
1410
  try {
1244
1411
  const signatureHeader = req.headers["x-line-signature"];
1245
1412
  const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
@@ -1266,24 +1433,14 @@ function createLineNodeWebhookHandler(params) {
1266
1433
  return;
1267
1434
  }
1268
1435
  params.onRequestAuthenticated?.();
1269
- receiveContext = createMessageReceiveContext({
1270
- id: `${Date.now()}:line:webhook`,
1271
- channel: "line",
1272
- message: body,
1273
- ackPolicy: "after_receive_record",
1274
- onAck: () => {
1275
- res.statusCode = 200;
1276
- res.setHeader("Content-Type", "application/json");
1277
- res.end(JSON.stringify({ status: "ok" }));
1278
- }
1279
- });
1280
- if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
1281
1436
  if (body.events && body.events.length > 0) {
1282
1437
  logVerbose(`line: received ${body.events.length} webhook events`);
1283
- runDetachedWebhookWork(() => params.bot.handleWebhook(body)).catch((err) => logLineWebhookDispatchError(params.runtime, err));
1438
+ await params.bot.handleWebhook(body);
1284
1439
  }
1440
+ res.statusCode = 200;
1441
+ res.setHeader("Content-Type", "application/json");
1442
+ res.end(JSON.stringify({ status: "ok" }));
1285
1443
  } catch (err) {
1286
- await receiveContext?.nack(err);
1287
1444
  if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
1288
1445
  res.statusCode = 413;
1289
1446
  res.setHeader("Content-Type", "application/json");
@@ -1344,7 +1501,7 @@ async function monitorLineProvider(opts) {
1344
1501
  accountId,
1345
1502
  runtime,
1346
1503
  config,
1347
- onMessage: async (ctx) => {
1504
+ onMessage: async (ctx, deliveryControl) => {
1348
1505
  if (!ctx) return;
1349
1506
  const { ctxPayload, replyToken, route } = ctx;
1350
1507
  const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
@@ -1358,14 +1515,23 @@ async function monitorLineProvider(opts) {
1358
1515
  accountId: ctx.accountId
1359
1516
  }) : null;
1360
1517
  logVerbose(`line: received message from ${await displayNamePromise} (${ctxPayload.From})`);
1518
+ let replyTokenUsed = false;
1519
+ let turnAdopted = false;
1520
+ const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
1361
1521
  try {
1362
1522
  const textLimit = 5e3;
1363
- let replyTokenUsed = false;
1364
- const core = getLineRuntime();
1365
- const turnResult = await core.channel.inbound.run({
1523
+ const turnResult = await getLineRuntime().channel.inbound.run({
1366
1524
  channel: "line",
1367
1525
  accountId: route.accountId,
1368
1526
  raw: ctx,
1527
+ turnAdoptionLifecycle: {
1528
+ ...ingressLifecycle,
1529
+ admission: "exclusive",
1530
+ onAdopted: async () => {
1531
+ await ingressLifecycle?.onAdopted();
1532
+ turnAdopted = true;
1533
+ }
1534
+ },
1369
1535
  adapter: {
1370
1536
  ingest: () => ({
1371
1537
  id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
@@ -1375,14 +1541,14 @@ async function monitorLineProvider(opts) {
1375
1541
  cfg: config,
1376
1542
  channel: "line",
1377
1543
  accountId: route.accountId,
1378
- agentId: route.agentId,
1379
- routeSessionKey: route.sessionKey,
1380
- storePath: ctx.turn.storePath,
1544
+ route: {
1545
+ agentId: route.agentId,
1546
+ sessionKey: route.sessionKey
1547
+ },
1381
1548
  ctxPayload,
1382
- recordInboundSession: core.channel.session.recordInboundSession,
1383
- dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
1384
1549
  record: ctx.turn.record,
1385
1550
  replyPipeline: {},
1551
+ ...ingressLifecycle?.abortSignal ? { replyOptions: { abortSignal: ingressLifecycle.abortSignal } } : {},
1386
1552
  delivery: {
1387
1553
  durable: (payload, info) => resolveLineDurableReplyOptions({
1388
1554
  payload,
@@ -1439,17 +1605,8 @@ async function monitorLineProvider(opts) {
1439
1605
  if (!hasFinalInboundReplyDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1440
1606
  } catch (err) {
1441
1607
  runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
1442
- if (replyToken) try {
1443
- await replyMessageLine(replyToken, [{
1444
- type: "text",
1445
- text: "Sorry, I encountered an error processing your message."
1446
- }], {
1447
- cfg: config,
1448
- accountId: ctx.accountId
1449
- });
1450
- } catch (replyErr) {
1451
- runtime.error?.(danger(`line: error reply failed: ${String(replyErr)}`));
1452
- }
1608
+ if (turnAdopted || replyTokenUsed) throw new LineWebhookTerminalDeliveryError("LINE delivery failed after consuming the event reply token.", { cause: err });
1609
+ throw err;
1453
1610
  } finally {
1454
1611
  stopLoading?.();
1455
1612
  }
@@ -1527,16 +1684,13 @@ async function monitorLineProvider(opts) {
1527
1684
  res.end(JSON.stringify({ error: "Invalid webhook payload" }));
1528
1685
  return;
1529
1686
  }
1530
- requestLifecycle.release();
1531
- res.statusCode = 200;
1532
- res.setHeader("Content-Type", "application/json");
1533
- res.end(JSON.stringify({ status: "ok" }));
1534
1687
  if (body.events && body.events.length > 0) {
1535
1688
  logVerbose(`line: received ${body.events.length} webhook events`);
1536
- runDetachedWebhookWork(() => match.target.bot.handleWebhook(body)).catch((err) => {
1537
- match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
1538
- });
1689
+ await match.target.bot.handleWebhook(body);
1539
1690
  }
1691
+ res.statusCode = 200;
1692
+ res.setHeader("Content-Type", "application/json");
1693
+ res.end(JSON.stringify({ status: "ok" }));
1540
1694
  } catch (err) {
1541
1695
  if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1542
1696
  res.statusCode = 413;
@@ -1564,23 +1718,29 @@ async function monitorLineProvider(opts) {
1564
1718
  });
1565
1719
  logVerbose(`line: registered webhook handler at ${normalizedPath}`);
1566
1720
  let stopped = false;
1721
+ let stopPromise;
1567
1722
  const stopHandler = () => {
1568
- if (stopped) return;
1723
+ if (stopPromise) return stopPromise;
1724
+ if (stopped) return Promise.resolve();
1569
1725
  stopped = true;
1570
1726
  logVerbose(`line: stopping provider for account ${resolvedAccountId}`);
1571
1727
  unregisterHttp();
1728
+ stopPromise = bot.stop();
1729
+ return stopPromise;
1572
1730
  };
1573
- if (abortSignal?.aborted) stopHandler();
1731
+ const stopOnAbort = () => void stopHandler();
1732
+ if (abortSignal?.aborted) await stopHandler();
1574
1733
  else if (abortSignal) {
1575
- abortSignal.addEventListener("abort", stopHandler, { once: true });
1734
+ abortSignal.addEventListener("abort", stopOnAbort, { once: true });
1576
1735
  await waitForAbortSignal(abortSignal);
1736
+ await stopHandler();
1577
1737
  }
1578
1738
  return {
1579
1739
  account: bot.account,
1580
1740
  handleWebhook: bot.handleWebhook,
1581
- stop: () => {
1582
- stopHandler();
1583
- abortSignal?.removeEventListener("abort", stopHandler);
1741
+ stop: async () => {
1742
+ await stopHandler();
1743
+ abortSignal?.removeEventListener("abort", stopOnAbort);
1584
1744
  }
1585
1745
  };
1586
1746
  }
@@ -0,0 +1,2 @@
1
+ import { t as monitorLineProvider } from "./monitor-Bk-lpoOZ.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-XceqeKRP.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-D1vapcdP.js";
2
2
  export { buildTemplateMessageFromPayload, createQuickReplyItems, processLineMessage, pushFlexMessage, pushLocationMessage, pushMessageLine, pushMessagesLine, pushTemplateMessage, pushTextMessageWithQuickReplies, sendMessageLine };
@@ -1,17 +1,15 @@
1
- import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-DJVOv1JI.js";
1
+ import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BUBOoW5Q.js";
2
2
  import { d as LineChannelConfigSchema, f as LineConfigSchema, g as resolveLineGroupsConfig, h as resolveLineGroupLookupIds, m as resolveLineGroupConfigEntry, n as parseLineDirectives, p as resolveExactLineGroupConfigKey, t as hasLineDirectives, u as setLineRuntime } from "./reply-payload-transform-r9sa-TYT.js";
3
3
  import { _ as createNotificationBubble, a as datetimePickerAction, c as uriAction, d as createReceiptCard, f as createActionCard, g as createListCard, h as createInfoCard, i as createMediaPlayerCard, l as createAgendaCard, m as createImageCard, n as createAppleTvRemoteCard, o as messageAction, p as createCarousel, r as createDeviceControlCard, s as postbackAction, t as toFlexMessage, u as createEventCard } from "./flex-templates-K1lS9i8L.js";
4
- 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-Db2rZsSX.js";
4
+ 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-Bk-lpoOZ.js";
5
5
  import { t as probeLineBot } from "./probe-BslD77tJ.js";
6
- import { A as buildTemplateMessageFromPayload, B as createYesNoConfirm, C as pushMessagesLine, D as sendMessageLine, E as replyMessageLine, F as createImageCarousel, 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, _ 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-XceqeKRP.js";
6
+ import { A as buildTemplateMessageFromPayload, B as createYesNoConfirm, C as pushMessagesLine, D as sendMessageLine, E as replyMessageLine, F as createImageCarousel, 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, _ 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-D1vapcdP.js";
7
7
  import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
8
8
  import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
9
- import { createMessageReceiveContext } from "openclaw/plugin-sdk/channel-outbound";
10
9
  import { DEFAULT_ACCOUNT_ID, formatDocsLink, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
11
10
  import { buildComputedAccountStatusSnapshot, buildTokenChannelStatusSummary } from "openclaw/plugin-sdk/status-helpers";
12
11
  import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
13
12
  import { messagingApi } from "@line/bot-sdk";
14
- import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
15
13
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/agent-media-payload";
16
14
  import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
17
15
  import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
@@ -26,13 +24,9 @@ function parseWebhookBody(rawBody) {
26
24
  if (!rawBody) return null;
27
25
  return parseLineWebhookBody(rawBody);
28
26
  }
29
- function logLineWebhookDispatchError(runtime, err) {
30
- runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
31
- }
32
27
  function createLineWebhookMiddleware(options) {
33
28
  const { channelSecret, onEvents, runtime } = options;
34
29
  return async (req, res, _next) => {
35
- let receiveContext;
36
30
  try {
37
31
  const signature = req.headers["x-line-signature"];
38
32
  if (!signature || typeof signature !== "string") {
@@ -58,22 +52,12 @@ function createLineWebhookMiddleware(options) {
58
52
  res.status(400).json({ error: "Invalid webhook payload" });
59
53
  return;
60
54
  }
61
- receiveContext = createMessageReceiveContext({
62
- id: `${Date.now()}:line:webhook`,
63
- channel: "line",
64
- message: body,
65
- ackPolicy: "after_receive_record",
66
- onAck: () => {
67
- res.status(200).json({ status: "ok" });
68
- }
69
- });
70
- if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
71
55
  if (body.events && body.events.length > 0) {
72
56
  logVerbose(`line: received ${body.events.length} webhook events`);
73
- runDetachedWebhookWork(() => onEvents(body)).catch((err) => logLineWebhookDispatchError(runtime, err));
57
+ await onEvents(body);
74
58
  }
59
+ res.status(200).json({ status: "ok" });
75
60
  } catch (err) {
76
- await receiveContext?.nack(err);
77
61
  runtime?.error?.(danger(`line webhook error: ${String(err)}`));
78
62
  if (!res.headersSent) res.status(500).json({ error: "Internal server error" });
79
63
  }
package/dist/setup-api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-B7W6pwn3.js";
1
+ import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-BEt4mazl.js";
2
2
  export { lineSetupAdapter, lineSetupWizard };
@@ -1,8 +1,9 @@
1
- import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-DJVOv1JI.js";
1
+ import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BUBOoW5Q.js";
2
2
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
3
3
  import { DEFAULT_ACCOUNT_ID, createAllowFromSection, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, mergeAllowFromEntries, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
4
4
  //#region extensions/line/src/account-helpers.ts
5
5
  function hasLineCredentials(account) {
6
+ if (account.tokenStatus && account.signingSecretStatus) return account.tokenStatus !== "missing" && account.signingSecretStatus !== "missing";
6
7
  return Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim());
7
8
  }
8
9
  function parseLineAllowFromId(raw) {
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@openclaw/line",
3
- "version": "2026.7.2-beta.2",
3
+ "version": "2026.7.2-beta.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/line",
9
- "version": "2026.7.2-beta.2",
9
+ "version": "2026.7.2-beta.3",
10
10
  "dependencies": {
11
11
  "@line/bot-sdk": "11.1.0",
12
12
  "zod": "4.4.3"
13
13
  },
14
14
  "peerDependencies": {
15
- "openclaw": ">=2026.7.2-beta.2"
15
+ "openclaw": ">=2026.7.2-beta.3"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "openclaw": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/line",
3
- "version": "2026.7.2-beta.2",
3
+ "version": "2026.7.2-beta.3",
4
4
  "description": "OpenClaw LINE channel plugin for LINE Bot API chats.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,7 +12,7 @@
12
12
  "zod": "4.4.3"
13
13
  },
14
14
  "peerDependencies": {
15
- "openclaw": ">=2026.7.2-beta.2"
15
+ "openclaw": ">=2026.7.2-beta.3"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "openclaw": {
@@ -42,10 +42,10 @@
42
42
  "minHostVersion": ">=2026.4.10"
43
43
  },
44
44
  "compat": {
45
- "pluginApi": ">=2026.7.2-beta.2"
45
+ "pluginApi": ">=2026.7.2-beta.3"
46
46
  },
47
47
  "build": {
48
- "openclawVersion": "2026.7.2-beta.2"
48
+ "openclawVersion": "2026.7.2-beta.3"
49
49
  },
50
50
  "release": {
51
51
  "publishToClawHub": true,
@@ -1,2 +0,0 @@
1
- import { t as monitorLineProvider } from "./monitor-Db2rZsSX.js";
2
- export { monitorLineProvider };