@openclaw/line 2026.9.7 → 2026.10.1-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.
@@ -1,10 +1,11 @@
1
1
  import { n as resolveDefaultLineAccountId, r as resolveLineAccount } from "./accounts-DOiM8-y3.mjs";
2
- import { a as resolveLineQuestionPostback, c as normalizeLineAllowEntry, i as parseLineQuestionPostbackData, n as createLineQuickReply, o as buildLineQuickReplyFallbackText, r as prepareLineReplyPayload, s as resolveLineGroupConfigEntry } from "./channel-DN6Za8Vm.mjs";
2
+ import { a as resolveLineQuestionPostback, i as parseLineQuestionPostbackData, n as createLineQuickReply, o as buildLineQuickReplyFallbackText, r as prepareLineReplyPayload, s as resolveLineGroupConfigEntry } from "./channel-InKuXaRS.mjs";
3
+ import { a as normalizeLineAllowEntry } from "./setup-surface-1jkHGWg7.mjs";
3
4
  import { t as getLineRuntime } from "./runtime-V7yh9OeC.mjs";
4
- import { D as recordLineQuoteToken, E as readLineQuoteToken, O as reportLineQuoteCarrierMissing, T as canCarryLineQuoteToken, j as buildLineMediaMessage, k as resolveLineQuoteToken, n as findLineHttpError, r as resolveLineNonDispatchRetryable, t as explainLineRefusal, w as applyLineQuoteToken } from "./send-retry-DV6I0muq.mjs";
5
- import { a as eventIdFor, n as LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE, o as laneKeyFor, r as LineWebhookPayloadError, t as LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON } from "./webhook-spool-contract-1rY8OJw8.mjs";
6
- import { a as getUserDisplayName, g as quotesLineBotMessage, h as showLoadingAnimation, i as getLineGroupName, l as pushMessageLine, n as createLocationMessage, o as getUserProfile, p as replyMessageLine, t as createFlexMessage, u as pushMessagesLine } from "./send-DOY1WTxv.mjs";
7
- import { n as processLineMessage, t as buildTemplateMessageFromPayload } from "./template-messages-CxCnYsWN.mjs";
5
+ import { D as recordLineQuoteToken, E as readLineQuoteToken, O as reportLineQuoteCarrierMissing, T as canCarryLineQuoteToken, j as buildLineMediaMessage, k as resolveLineQuoteToken, n as findLineHttpError, r as resolveLineNonDispatchRetryable, t as explainLineRefusal, w as applyLineQuoteToken } from "./send-retry-B9Nor0-J.mjs";
6
+ import { a as laneKeyFor, i as eventIdFor, n as LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE, r as LineWebhookPayloadError, t as LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON } from "./webhook-spool-contract-msjTl60Q.mjs";
7
+ import { a as getUserDisplayName, g as quotesLineBotMessage, h as showLoadingAnimation, i as getLineGroupName, l as pushMessageLine, n as createLocationMessage, o as getUserProfile, p as replyMessageLine, t as createFlexMessage, u as pushMessagesLine } from "./send-DAwROcyQ.mjs";
8
+ import { n as processLineMessage, t as buildTemplateMessageFromPayload } from "./template-messages-DdF9zv_j.mjs";
8
9
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
9
10
  import { resolveChannelGroupsConfigPath } from "openclaw/plugin-sdk/channel-policy";
10
11
  import { normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -16,7 +17,7 @@ import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, read
16
17
  import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
17
18
  import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
18
19
  import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal, warn } from "openclaw/plugin-sdk/runtime-env";
19
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
20
+ import { safeParseJson, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
20
21
  import { classifyTransientNetworkErrorCode } from "openclaw/plugin-sdk/retry-runtime";
21
22
  import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
22
23
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
@@ -40,6 +41,7 @@ import { getSessionBindingService, inspectRuntimeConversationBindingRoute } from
40
41
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
41
42
  import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
42
43
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
44
+ import { raceWithTimeout } from "openclaw/plugin-sdk/time-runtime";
43
45
  import { enqueueKeyedTask } from "openclaw/plugin-sdk/keyed-async-queue";
44
46
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
45
47
  //#region extensions/line/src/auto-reply-delivery.ts
@@ -144,16 +146,10 @@ async function deliverLineAutoReply(params) {
144
146
  }
145
147
  if (lineData.location) richMessages.push(createLocationMessage(lineData.location));
146
148
  const visibleText = payload.text ? sanitizeAssistantVisibleText(payload.text) : "";
147
- const processed = visibleText ? processLineMessage(visibleText) : {
148
- text: "",
149
- flexMessages: []
150
- };
151
- if (!processed.segments) for (const flexMsg of processed.flexMessages) richMessages.push(createFlexMessage(flexMsg.altText, flexMsg.contents));
152
- const orderedMessages = processed.segments?.flatMap((segment) => segment.type === "flex" ? [createFlexMessage(segment.message.altText, segment.message.contents)] : chunkMarkdownText(segment.text, textLimit).map((text) => ({
149
+ const markdownMessages = (visibleText ? processLineMessage(visibleText) : []).flatMap((segment) => segment.type === "flex" ? [createFlexMessage(segment.message.altText, segment.message.contents)] : chunkMarkdownText(segment.text, textLimit).map((text) => ({
153
150
  type: "text",
154
151
  text
155
152
  })));
156
- const chunks = orderedMessages ? orderedMessages.flatMap((message) => message.type === "text" ? [message.text] : []) : processed.text ? chunkMarkdownText(processed.text, textLimit) : [];
157
153
  const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
158
154
  const mediaOpts = {
159
155
  mediaKind: lineData.mediaKind,
@@ -163,23 +159,16 @@ async function deliverLineAutoReply(params) {
163
159
  };
164
160
  const mediaMessages = [];
165
161
  let deliveryError;
166
- for (const rawUrl of mediaUrls) {
167
- const url = rawUrl?.trim();
168
- if (!url) continue;
169
- try {
170
- mediaMessages.push(await buildLineMediaMessage(url, mediaOpts, to));
171
- } catch (err) {
172
- deliveryError ??= err;
173
- }
162
+ for (const url of mediaUrls) try {
163
+ mediaMessages.push(await buildLineMediaMessage(url, mediaOpts, to));
164
+ } catch (err) {
165
+ deliveryError ??= err;
174
166
  }
175
- const textMessages = chunks.map((text) => ({
176
- type: "text",
177
- text
178
- }));
179
- const orderedDeliveryMessages = hasQuickReplies && chunks.length === 0 ? void 0 : orderedMessages;
167
+ const inlineMarkdown = hasQuickReplies && !markdownMessages.some((message) => message.type === "text");
168
+ const textMessages = inlineMarkdown ? [] : markdownMessages;
180
169
  const richMediaMessages = [
181
170
  ...richMessages,
182
- ...orderedDeliveryMessages ? [] : orderedMessages ?? [],
171
+ ...inlineMarkdown ? markdownMessages : [],
183
172
  ...mediaMessages
184
173
  ];
185
174
  if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
@@ -187,14 +176,14 @@ async function deliverLineAutoReply(params) {
187
176
  text: buildLineQuickReplyFallbackText(quickReplyLabels)
188
177
  });
189
178
  if (hasQuickReplies) {
190
- const targetMessages = orderedDeliveryMessages?.length ? orderedDeliveryMessages : textMessages.length > 0 ? textMessages : richMediaMessages;
179
+ const targetMessages = textMessages.length > 0 ? textMessages : richMediaMessages;
191
180
  const lastIndex = targetMessages.length - 1;
192
181
  targetMessages[lastIndex] = {
193
182
  ...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
194
183
  quickReply: createLineQuickReply(quickReplyItems)
195
184
  };
196
185
  }
197
- const ordered = hasQuickReplies ? [...richMediaMessages, ...orderedDeliveryMessages ?? textMessages] : [...orderedDeliveryMessages ?? textMessages, ...richMediaMessages];
186
+ const ordered = hasQuickReplies ? [...richMediaMessages, ...textMessages] : [...textMessages, ...richMediaMessages];
198
187
  const replyQuoteToken = resolveLineQuoteToken({
199
188
  cfg: params.cfg,
200
189
  accountId,
@@ -296,7 +285,7 @@ function getLineSourceInfo(source) {
296
285
  isGroup: false
297
286
  };
298
287
  return {
299
- userId: source.type === "user" ? source.userId : source.type === "group" ? source.userId : source.type === "room" ? source.userId : void 0,
288
+ userId: source.type === "user" || source.type === "group" || source.type === "room" ? source.userId : void 0,
300
289
  groupId: source.type === "group" ? source.groupId : void 0,
301
290
  roomId: source.type === "room" ? source.roomId : void 0,
302
291
  isGroup: source.type === "group" || source.type === "room"
@@ -373,7 +362,8 @@ async function resolveLineInboundRoute(params) {
373
362
  accountId: params.account.accountId,
374
363
  direction: "inbound"
375
364
  });
376
- const { userId, groupId, roomId, isGroup, peerId, route, runtimeRoute, configuredBinding, configuredBindingSessionKey } = params.preparedRoute ?? await prepareLineInboundRoute(params);
365
+ const prepared = params.preparedRoute ?? await prepareLineInboundRoute(params);
366
+ const { peerId, runtimeRoute, configuredBinding, configuredBindingSessionKey } = prepared;
377
367
  if (runtimeRoute.bindingRecord) {
378
368
  await getSessionBindingService().touchAsync(runtimeRoute.bindingRecord.bindingId, void 0, runtimeRoute.bindingRecord.conversation);
379
369
  logVerbose(runtimeRoute.boundSessionKey ? `line: routed via bound conversation ${peerId} -> ${runtimeRoute.boundSessionKey}` : `line: plugin-bound conversation ${peerId}`);
@@ -389,14 +379,7 @@ async function resolveLineInboundRoute(params) {
389
379
  }
390
380
  logVerbose(`line: using configured ACP binding for ${peerId} -> ${configuredBindingSessionKey}`);
391
381
  }
392
- return {
393
- userId,
394
- groupId,
395
- roomId,
396
- isGroup,
397
- peerId,
398
- route
399
- };
382
+ return prepared;
400
383
  }
401
384
  /**
402
385
  * Describe a sticker from what its webhook actually carries: LINE sends up to
@@ -429,9 +412,9 @@ function extractMessageText(message) {
429
412
  }
430
413
  function extractNativeMediaKind(message) {
431
414
  switch (message.type) {
432
- case "image": return "image";
433
- case "video": return "video";
434
- case "audio": return "audio";
415
+ case "image":
416
+ case "video":
417
+ case "audio": return message.type;
435
418
  case "file": return "document";
436
419
  default: return;
437
420
  }
@@ -568,22 +551,21 @@ async function finalizeLineInboundContext(params) {
568
551
  }
569
552
  async function buildLineMessageContext(params) {
570
553
  const { event, allMedia, mediaUnavailable, cfg, account, commandAuthorized, inboundHistory } = params;
571
- const source = event.source;
572
- const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
573
- source,
554
+ const source = await resolveLineInboundRoute({
555
+ source: event.source,
574
556
  cfg,
575
557
  account,
576
558
  preparedRoute: params.preparedRoute
577
559
  });
560
+ const { peerId, route } = source;
578
561
  const message = event.message;
579
562
  const messageId = message.id;
580
- const textContent = extractMessageText(message);
563
+ const rawBody = extractMessageText(message);
581
564
  const nativeMediaKind = extractNativeMediaKind(message);
582
565
  const mediaFacts = allMedia.length > 0 ? allMedia.map((media) => ({
583
566
  ...media,
584
567
  kind: nativeMediaKind
585
568
  })) : nativeMediaKind ? [{ kind: nativeMediaKind }] : [];
586
- const rawBody = textContent;
587
569
  const shortfallNotice = params.missingParts ? `[line: ${params.missingParts === 1 ? "1 more image in this send was" : `${params.missingParts} more images in this send were`} not delivered]` : void 0;
588
570
  const withShortfall = shortfallNotice ? formatInboundMediaUnavailableText({
589
571
  body: rawBody,
@@ -601,27 +583,18 @@ async function buildLineMessageContext(params) {
601
583
  quoteToken: readLineQuoteToken(message)
602
584
  });
603
585
  let locationContext;
604
- if (message.type === "location") {
605
- const loc = message;
606
- locationContext = toLocationContext({
607
- latitude: loc.latitude,
608
- longitude: loc.longitude,
609
- name: loc.title,
610
- address: loc.address
611
- });
612
- }
586
+ if (message.type === "location") locationContext = toLocationContext({
587
+ latitude: message.latitude,
588
+ longitude: message.longitude,
589
+ name: message.title,
590
+ address: message.address
591
+ });
613
592
  return finalizeLineInboundContext({
614
593
  cfg,
615
594
  account,
616
595
  event,
617
596
  route,
618
- source: {
619
- userId,
620
- groupId,
621
- roomId,
622
- isGroup,
623
- peerId
624
- },
597
+ source,
625
598
  rawBody,
626
599
  agentBody,
627
600
  commandBody: resolveLineMentionStrippedText(message) || rawBody,
@@ -646,13 +619,12 @@ async function buildLineMessageContext(params) {
646
619
  }
647
620
  async function buildLinePostbackContext(params) {
648
621
  const { event, cfg, account, commandAuthorized } = params;
649
- const source = event.source;
650
- const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
651
- source,
622
+ const source = await resolveLineInboundRoute({
623
+ source: event.source,
652
624
  cfg,
653
625
  account
654
626
  });
655
- const timestamp = event.timestamp;
627
+ const { route } = source;
656
628
  const rawBody = event.postback?.data?.trim() ?? "";
657
629
  if (!rawBody) return null;
658
630
  let agentBody = rawBody;
@@ -666,19 +638,13 @@ async function buildLinePostbackContext(params) {
666
638
  const picked = normalizeOptionalString(value);
667
639
  if (picked) agentBody += ` ${key}=${picked}`;
668
640
  }
669
- const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
641
+ const messageSid = `postback:${event.replyToken || event.timestamp}`;
670
642
  return finalizeLineInboundContext({
671
643
  cfg,
672
644
  account,
673
645
  event,
674
646
  route,
675
- source: {
676
- userId,
677
- groupId,
678
- roomId,
679
- isGroup,
680
- peerId
681
- },
647
+ source,
682
648
  rawBody,
683
649
  agentBody,
684
650
  messageSid,
@@ -1274,37 +1240,34 @@ async function handleLineWebhookEvents(events, context) {
1274
1240
  const [event, ...setParts] = events;
1275
1241
  if (!event) return;
1276
1242
  try {
1277
- await handleLineWebhookEvent(event, context, setParts);
1243
+ switch (event.type) {
1244
+ case "message":
1245
+ await handleMessageEvent(event, context, setParts.filter((part) => part.type === "message"));
1246
+ break;
1247
+ case "follow":
1248
+ case "unfollow": {
1249
+ const { userId } = getLineSourceInfo(event.source);
1250
+ logVerbose(`line: user ${userId ?? "unknown"} ${event.type}ed`);
1251
+ break;
1252
+ }
1253
+ case "join":
1254
+ await handleJoinEvent(event, context);
1255
+ break;
1256
+ case "leave": {
1257
+ const { groupId, roomId } = getLineSourceInfo(event.source);
1258
+ logVerbose(`line: bot left ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
1259
+ break;
1260
+ }
1261
+ case "postback":
1262
+ await handlePostbackEvent(event, context);
1263
+ break;
1264
+ default: logVerbose(`line: unhandled event type: ${event.type}`);
1265
+ }
1278
1266
  } catch (err) {
1279
1267
  context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
1280
1268
  throw toErrorObject(err, "Non-Error thrown");
1281
1269
  }
1282
1270
  }
1283
- async function handleLineWebhookEvent(event, context, setParts = []) {
1284
- switch (event.type) {
1285
- case "message":
1286
- await handleMessageEvent(event, context, setParts.filter((part) => part.type === "message"));
1287
- break;
1288
- case "follow":
1289
- case "unfollow": {
1290
- const { userId } = getLineSourceInfo(event.source);
1291
- logVerbose(`line: user ${userId ?? "unknown"} ${event.type}ed`);
1292
- break;
1293
- }
1294
- case "join":
1295
- await handleJoinEvent(event, context);
1296
- break;
1297
- case "leave": {
1298
- const { groupId, roomId } = getLineSourceInfo(event.source);
1299
- logVerbose(`line: bot left ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
1300
- break;
1301
- }
1302
- case "postback":
1303
- await handlePostbackEvent(event, context);
1304
- break;
1305
- default: logVerbose(`line: unhandled event type: ${event.type}`);
1306
- }
1307
- }
1308
1271
  //#endregion
1309
1272
  //#region extensions/line/src/inbound-image-set.ts
1310
1273
  const IMAGE_SET_FLUSH_DELAY_MS = 4e3;
@@ -1425,17 +1388,6 @@ function isLineAuthenticationFailure(error) {
1425
1388
  const status = error.status;
1426
1389
  return status === 401 || status === 403;
1427
1390
  }
1428
- async function waitForActiveDeliveriesBeforeDispose(activeDeliveries) {
1429
- let timeout;
1430
- try {
1431
- return await Promise.race([Promise.allSettled(activeDeliveries).then(() => true), new Promise((resolve) => {
1432
- timeout = setTimeout(() => resolve(false), LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS);
1433
- timeout.unref?.();
1434
- })]);
1435
- } finally {
1436
- if (timeout) clearTimeout(timeout);
1437
- }
1438
- }
1439
1391
  /** The imageSet a LINE inbound event belongs to, when it reported one. */
1440
1392
  function senderKeyFor(event) {
1441
1393
  const source = event.source;
@@ -1638,7 +1590,7 @@ function createLineWebhookSpool(options) {
1638
1590
  stopTask ??= (async () => {
1639
1591
  await monitor.pause();
1640
1592
  try {
1641
- const deliveriesSettled = await waitForActiveDeliveriesBeforeDispose(activeDeliveries);
1593
+ const deliveriesSettled = await raceWithTimeout(Promise.allSettled(activeDeliveries).then(() => true), LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS, () => false, { ref: false });
1642
1594
  if (!deliveriesSettled) options.runtime.log(warn(`line: timed out after ${LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS}ms waiting for active webhook deliveries; releasing drain ownership`));
1643
1595
  await monitor.waitForDeferredClaims();
1644
1596
  acceptsDeferredClaims = false;
@@ -1721,27 +1673,6 @@ function validateLineSignature(body, signature, channelSecret) {
1721
1673
  return safeEqualSecret(signature, hash);
1722
1674
  }
1723
1675
  //#endregion
1724
- //#region extensions/line/src/webhook-utils.ts
1725
- /** Route the gateway serves when an account configures no `webhookPath`. */
1726
- const LINE_DEFAULT_WEBHOOK_PATH = "/line/webhook";
1727
- /** The route this account's monitor serves, which is the one an operator has to register
1728
- * with LINE. Every surface resolves it here so a warning cannot name a path the gateway
1729
- * does not answer on. Route registration canonicalizes further, and matches requests the
1730
- * same way, so a path resolved here always reaches the route the monitor registered. */
1731
- function resolveLineWebhookPath(webhookPath) {
1732
- return resolveWebhookPath({
1733
- webhookPath,
1734
- defaultPath: LINE_DEFAULT_WEBHOOK_PATH
1735
- }) ?? LINE_DEFAULT_WEBHOOK_PATH;
1736
- }
1737
- function parseLineWebhookBody(rawBody) {
1738
- try {
1739
- return JSON.parse(rawBody);
1740
- } catch {
1741
- return null;
1742
- }
1743
- }
1744
- //#endregion
1745
1676
  //#region extensions/line/src/webhook-node.ts
1746
1677
  const LINE_WEBHOOK_MAX_BODY_BYTES = 1048576;
1747
1678
  const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
@@ -1765,6 +1696,11 @@ async function rejectLineWebhookRequest(req, res, error) {
1765
1696
  await sendHttpRequestRejection(req, res, error.statusCode, JSON.stringify({ error: requestBodyErrorToText(error.code) }), "application/json");
1766
1697
  return true;
1767
1698
  }
1699
+ function sendLineWebhookJson(res, statusCode, body) {
1700
+ res.statusCode = statusCode;
1701
+ res.setHeader("Content-Type", "application/json");
1702
+ res.end(JSON.stringify(body));
1703
+ }
1768
1704
  function createLineNodeWebhookHandler(params) {
1769
1705
  const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
1770
1706
  const readBody = params.readBody ?? readLineWebhookRequestBody;
@@ -1787,10 +1723,8 @@ function createLineNodeWebhookHandler(params) {
1787
1723
  return;
1788
1724
  }
1789
1725
  if (req.method !== "POST") {
1790
- res.statusCode = 405;
1791
1726
  res.setHeader("Allow", "GET, HEAD, POST");
1792
- res.setHeader("Content-Type", "application/json");
1793
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
1727
+ sendLineWebhookJson(res, 405, { error: "Method Not Allowed" });
1794
1728
  return;
1795
1729
  }
1796
1730
  try {
@@ -1798,53 +1732,53 @@ function createLineNodeWebhookHandler(params) {
1798
1732
  const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
1799
1733
  if (!signature) {
1800
1734
  logVerbose("line: webhook missing X-Line-Signature header");
1801
- res.statusCode = 400;
1802
- res.setHeader("Content-Type", "application/json");
1803
- res.end(JSON.stringify({ error: "Missing X-Line-Signature header" }));
1735
+ sendLineWebhookJson(res, 400, { error: "Missing X-Line-Signature header" });
1804
1736
  return;
1805
1737
  }
1806
1738
  const rawBody = await readBody(req, Math.min(maxBodyBytes, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES), LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS);
1807
1739
  const match = resolveSingleWebhookTarget(targets, (target) => validateLineSignature(rawBody, signature, target.channelSecret));
1808
1740
  if (match.kind === "none") {
1809
1741
  logVerbose("line: webhook signature validation failed");
1810
- res.statusCode = 401;
1811
- res.setHeader("Content-Type", "application/json");
1812
- res.end(JSON.stringify({ error: "Invalid signature" }));
1742
+ sendLineWebhookJson(res, 401, { error: "Invalid signature" });
1813
1743
  return;
1814
1744
  }
1815
1745
  if (match.kind === "ambiguous") {
1816
1746
  logVerbose("line: webhook signature matched multiple accounts");
1817
- res.statusCode = 401;
1818
- res.setHeader("Content-Type", "application/json");
1819
- res.end(JSON.stringify({ error: "Ambiguous webhook target" }));
1747
+ sendLineWebhookJson(res, 401, { error: "Ambiguous webhook target" });
1820
1748
  return;
1821
1749
  }
1822
- const body = parseLineWebhookBody(rawBody);
1750
+ const body = safeParseJson(rawBody);
1823
1751
  if (!body) {
1824
- res.statusCode = 400;
1825
- res.setHeader("Content-Type", "application/json");
1826
- res.end(JSON.stringify({ error: "Invalid webhook payload" }));
1752
+ sendLineWebhookJson(res, 400, { error: "Invalid webhook payload" });
1827
1753
  return;
1828
1754
  }
1829
1755
  if (body.events && body.events.length > 0) {
1830
1756
  logVerbose(`line: received ${body.events.length} webhook events`);
1831
1757
  if (await match.target.bot.handleWebhook(body) === "durable") res.setHeader("x-openclaw-delivery-accepted", "durable");
1832
1758
  }
1833
- res.statusCode = 200;
1834
- res.setHeader("Content-Type", "application/json");
1835
- res.end(JSON.stringify({ status: "ok" }));
1759
+ sendLineWebhookJson(res, 200, { status: "ok" });
1836
1760
  } catch (err) {
1837
1761
  if (await rejectLineWebhookRequest(req, res, err)) return;
1838
1762
  params.runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
1839
- if (!res.headersSent) {
1840
- res.statusCode = 500;
1841
- res.setHeader("Content-Type", "application/json");
1842
- res.end(JSON.stringify({ error: "Internal server error" }));
1843
- }
1763
+ if (!res.headersSent) sendLineWebhookJson(res, 500, { error: "Internal server error" });
1844
1764
  }
1845
1765
  };
1846
1766
  }
1847
1767
  //#endregion
1768
+ //#region extensions/line/src/webhook-utils.ts
1769
+ /** Route the gateway serves when an account configures no `webhookPath`. */
1770
+ const LINE_DEFAULT_WEBHOOK_PATH = "/line/webhook";
1771
+ /** The route this account's monitor serves, which is the one an operator has to register
1772
+ * with LINE. Every surface resolves it here so a warning cannot name a path the gateway
1773
+ * does not answer on. Route registration canonicalizes further, and matches requests the
1774
+ * same way, so a path resolved here always reaches the route the monitor registered. */
1775
+ function resolveLineWebhookPath(webhookPath) {
1776
+ return resolveWebhookPath({
1777
+ webhookPath,
1778
+ defaultPath: LINE_DEFAULT_WEBHOOK_PATH
1779
+ }) ?? LINE_DEFAULT_WEBHOOK_PATH;
1780
+ }
1781
+ //#endregion
1848
1782
  //#region extensions/line/src/monitor.ts
1849
1783
  const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
1850
1784
  async function registerLineWebhookTarget(params, bot) {
@@ -1884,8 +1818,6 @@ async function monitorLineProvider(opts) {
1884
1818
  if (!token) throw new Error("LINE webhook mode requires a non-empty channel access token.");
1885
1819
  if (!secret) throw new Error("LINE webhook mode requires a non-empty channel secret.");
1886
1820
  const bot = createLineBot({
1887
- channelAccessToken: token,
1888
- channelSecret: secret,
1889
1821
  accountId,
1890
1822
  runtime,
1891
1823
  buildContext,
@@ -0,0 +1,2 @@
1
+ import { t as monitorLineProvider } from "./monitor-Bb3HieCd.mjs";
2
+ export { monitorLineProvider };
@@ -1,3 +1,3 @@
1
- import { c as pushLocationMessage, d as pushTemplateMessage, f as pushTextMessageWithQuickReplies, l as pushMessageLine, m as sendMessageLine, n as createLocationMessage, r as createQuickReplyItems, s as pushFlexMessage, t as createFlexMessage, u as pushMessagesLine } from "./send-DOY1WTxv.mjs";
2
- import { n as processLineMessage, t as buildTemplateMessageFromPayload } from "./template-messages-CxCnYsWN.mjs";
1
+ import { c as pushLocationMessage, d as pushTemplateMessage, f as pushTextMessageWithQuickReplies, l as pushMessageLine, m as sendMessageLine, n as createLocationMessage, r as createQuickReplyItems, s as pushFlexMessage, t as createFlexMessage, u as pushMessagesLine } from "./send-DAwROcyQ.mjs";
2
+ import { n as processLineMessage, t as buildTemplateMessageFromPayload } from "./template-messages-DdF9zv_j.mjs";
3
3
  export { buildTemplateMessageFromPayload, createFlexMessage, createLocationMessage, createQuickReplyItems, processLineMessage, pushFlexMessage, pushLocationMessage, pushMessageLine, pushMessagesLine, pushTemplateMessage, pushTextMessageWithQuickReplies, sendMessageLine };
@@ -0,0 +1,2 @@
1
+ import { a as probeLineBot } from "./send-retry-B9Nor0-J.mjs";
2
+ export { probeLineBot };
@@ -1,5 +1,5 @@
1
1
  import { r as resolveLineAccount } from "./accounts-DOiM8-y3.mjs";
2
- import { A as withoutLineQuoteTokens, g as messageAction, i as runLinePushWithRetries, j as buildLineMediaMessage, n as findLineHttpError, o as resolveLineChannelAccessToken, s as createLineSendReceipt, v as normalizeLineMessage, w as applyLineQuoteToken } from "./send-retry-DV6I0muq.mjs";
2
+ import { A as withoutLineQuoteTokens, _ as messageAction, i as runLinePushWithRetries, j as buildLineMediaMessage, n as findLineHttpError, o as resolveLineChannelAccessToken, s as createLineSendReceipt, v as normalizeLineMessage, w as applyLineQuoteToken } from "./send-retry-B9Nor0-J.mjs";
3
3
  import { createChannelPartialDeliveryError } from "openclaw/plugin-sdk/channel-inbound";
4
4
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
5
5
  import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
@@ -159,12 +159,6 @@ async function postLineProviderMessages(operation, token, request, retryKey, aut
159
159
  });
160
160
  }
161
161
  }
162
- function createTextMessage(text) {
163
- return {
164
- type: "text",
165
- text
166
- };
167
- }
168
162
  function isValidLineLocation(location) {
169
163
  return location.title.trim().length > 0 && location.address.trim().length > 0;
170
164
  }
@@ -274,7 +268,10 @@ async function sendMessageLine(to, text, opts) {
274
268
  durationMs: opts.durationMs,
275
269
  trackingId: opts.trackingId
276
270
  }, chatId));
277
- if (text?.trim()) messages.push(createTextMessage(text.trim()));
271
+ if (text?.trim()) messages.push({
272
+ type: "text",
273
+ text: text.trim()
274
+ });
278
275
  if (messages.length === 0) throw new Error("Message must be non-empty for LINE sends");
279
276
  if (opts.replyToken) {
280
277
  const { messageId, messageIds, accountId } = await replyLineMessages(opts.replyToken, messages, opts);
@@ -332,7 +329,11 @@ async function pushTemplateMessage(to, template, opts) {
332
329
  return pushLineMessages(to, [template], opts, { verboseMessage: (chatId) => `line: pushed template message to ${chatId}` });
333
330
  }
334
331
  async function pushTextMessageWithQuickReplies(to, text, quickReplyLabels, opts) {
335
- return pushLineMessages(to, [createTextMessageWithQuickReplies(text, quickReplyLabels)], opts, { verboseMessage: (chatId) => `line: pushed message with quick replies to ${chatId}` });
332
+ return pushLineMessages(to, [{
333
+ type: "text",
334
+ text,
335
+ quickReply: createQuickReplyItems(quickReplyLabels)
336
+ }], opts, { verboseMessage: (chatId) => `line: pushed message with quick replies to ${chatId}` });
336
337
  }
337
338
  function createQuickReplyItems(labels) {
338
339
  return { items: labels.slice(0, 13).map((label) => ({
@@ -340,13 +341,6 @@ function createQuickReplyItems(labels) {
340
341
  action: messageAction(label, label)
341
342
  })) };
342
343
  }
343
- function createTextMessageWithQuickReplies(text, quickReplyLabels) {
344
- return {
345
- type: "text",
346
- text,
347
- quickReply: createQuickReplyItems(quickReplyLabels)
348
- };
349
- }
350
344
  async function showLoadingAnimation(chatId, opts) {
351
345
  const { token } = resolveLineMessagingAccount(opts);
352
346
  const client = new messagingApi.MessagingApiClient({ channelAccessToken: token });