@openclaw/line 2026.9.8 → 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.
@@ -82,74 +82,51 @@ function resolveLineMediaKind(url, opts) {
82
82
  kindSource: "url"
83
83
  };
84
84
  }
85
- async function resolveLineOutboundMedia(mediaUrl, opts = {}) {
86
- const trimmedUrl = mediaUrl.trim();
87
- if (isHttpsUrl(trimmedUrl)) {
88
- await validateLineMediaUrl(trimmedUrl);
89
- const previewImageUrl = opts.previewImageUrl?.trim();
90
- if (previewImageUrl) await validateLineMediaUrl(previewImageUrl);
91
- return {
92
- mediaUrl: trimmedUrl,
93
- ...resolveLineMediaKind(trimmedUrl, opts),
94
- ...previewImageUrl ? { previewImageUrl } : {},
95
- ...typeof opts.durationMs === "number" ? { durationMs: opts.durationMs } : {},
96
- ...opts.trackingId ? { trackingId: opts.trackingId } : {}
97
- };
98
- }
99
- let parsed;
100
- try {
101
- parsed = new URL(trimmedUrl);
102
- } catch {}
103
- if (parsed) throw new Error("LINE outbound media URL must use HTTPS");
104
- throw new Error("LINE outbound media currently requires a public HTTPS URL");
105
- }
106
85
  function isLineUserTarget(target) {
107
86
  const normalized = target.trim().replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
108
87
  return /^U/i.test(normalized);
109
88
  }
110
- function createImageMessage(originalContentUrl, previewImageUrl) {
111
- return {
112
- type: "image",
113
- originalContentUrl,
114
- previewImageUrl: previewImageUrl ?? originalContentUrl
115
- };
116
- }
117
- function createVideoMessage(originalContentUrl, previewImageUrl, trackingId) {
118
- return {
119
- type: "video",
120
- originalContentUrl,
121
- previewImageUrl,
122
- ...trackingId ? { trackingId } : {}
123
- };
124
- }
125
- function createAudioMessage(originalContentUrl, durationMs) {
126
- return {
127
- type: "audio",
128
- originalContentUrl,
129
- duration: durationMs
130
- };
131
- }
132
- function lineMediaUrlFallback(mediaUrl) {
133
- return {
134
- type: "text",
135
- text: mediaUrl
136
- };
137
- }
138
- function buildLineMediaMessageObject(resolved, opts) {
139
- switch (resolved.mediaKind) {
140
- case "unsupported": return lineMediaUrlFallback(resolved.mediaUrl);
141
- case "video": {
142
- const previewImageUrl = resolved.previewImageUrl?.trim();
143
- if (previewImageUrl) return createVideoMessage(resolved.mediaUrl, previewImageUrl, opts?.allowTrackingId ? resolved.trackingId : void 0);
144
- if (resolved.kindSource !== "url") throw new Error("LINE video messages require previewImageUrl to reference an image URL");
145
- return lineMediaUrlFallback(resolved.mediaUrl);
146
- }
147
- case "audio": return createAudioMessage(resolved.mediaUrl, resolved.durationMs ?? 6e4);
148
- default: return createImageMessage(resolved.mediaUrl, resolved.previewImageUrl);
149
- }
150
- }
151
89
  async function buildLineMediaMessage(mediaUrl, opts, target) {
152
- return buildLineMediaMessageObject(await resolveLineOutboundMedia(mediaUrl, opts), { allowTrackingId: isLineUserTarget(target) });
90
+ const trimmedUrl = mediaUrl.trim();
91
+ if (!isHttpsUrl(trimmedUrl)) {
92
+ let parsed;
93
+ try {
94
+ parsed = new URL(trimmedUrl);
95
+ } catch {}
96
+ throw new Error(parsed ? "LINE outbound media URL must use HTTPS" : "LINE outbound media currently requires a public HTTPS URL");
97
+ }
98
+ await validateLineMediaUrl(trimmedUrl);
99
+ const previewImageUrl = opts.previewImageUrl?.trim();
100
+ if (previewImageUrl) await validateLineMediaUrl(previewImageUrl);
101
+ const { mediaKind, kindSource } = resolveLineMediaKind(trimmedUrl, opts);
102
+ switch (mediaKind) {
103
+ case "unsupported": return {
104
+ type: "text",
105
+ text: trimmedUrl
106
+ };
107
+ case "video":
108
+ if (previewImageUrl) return {
109
+ type: "video",
110
+ originalContentUrl: trimmedUrl,
111
+ previewImageUrl,
112
+ ...isLineUserTarget(target) && opts.trackingId ? { trackingId: opts.trackingId } : {}
113
+ };
114
+ if (kindSource !== "url") throw new Error("LINE video messages require previewImageUrl to reference an image URL");
115
+ return {
116
+ type: "text",
117
+ text: trimmedUrl
118
+ };
119
+ case "audio": return {
120
+ type: "audio",
121
+ originalContentUrl: trimmedUrl,
122
+ duration: typeof opts.durationMs === "number" ? opts.durationMs : 6e4
123
+ };
124
+ default: return {
125
+ type: "image",
126
+ originalContentUrl: trimmedUrl,
127
+ previewImageUrl: previewImageUrl || trimmedUrl
128
+ };
129
+ }
153
130
  }
154
131
  //#endregion
155
132
  //#region extensions/line/src/quote-tokens.ts
@@ -563,6 +540,16 @@ function postbackAction(label, data, displayText) {
563
540
  }
564
541
  //#endregion
565
542
  //#region extensions/line/src/flex-templates/common.ts
543
+ function createCardTitle(text) {
544
+ return {
545
+ type: "text",
546
+ text,
547
+ weight: "bold",
548
+ size: "xl",
549
+ color: "#111111",
550
+ wrap: true
551
+ };
552
+ }
566
553
  function createCardBubble(bodyContents, footer) {
567
554
  const bubble = {
568
555
  type: "bubble",
@@ -605,12 +592,7 @@ function createInfoCard(title, body, footer) {
605
592
  backgroundColor: "#06C755",
606
593
  cornerRadius: "2px"
607
594
  }, {
608
- type: "text",
609
- text: title,
610
- weight: "bold",
611
- size: "xl",
612
- color: "#111111",
613
- wrap: true,
595
+ ...createCardTitle(title),
614
596
  flex: 1,
615
597
  margin: "lg"
616
598
  }]
@@ -679,14 +661,7 @@ function createListCard(title, items) {
679
661
  return itemBox;
680
662
  });
681
663
  return createCardBubble([
682
- {
683
- type: "text",
684
- text: title,
685
- weight: "bold",
686
- size: "xl",
687
- color: "#111111",
688
- wrap: true
689
- },
664
+ createCardTitle(title),
690
665
  {
691
666
  type: "separator",
692
667
  margin: "lg",
@@ -766,14 +741,7 @@ function createActionCard(title, body, actions, options) {
766
741
  //#region extensions/line/src/flex-templates/schedule-cards.ts
767
742
  function buildTitleSubtitleHeader(params) {
768
743
  const { title, subtitle } = params;
769
- const headerContents = [{
770
- type: "text",
771
- text: title,
772
- weight: "bold",
773
- size: "xl",
774
- color: "#111111",
775
- wrap: true
776
- }];
744
+ const headerContents = [createCardTitle(title)];
777
745
  if (subtitle) headerContents.push({
778
746
  type: "text",
779
747
  text: subtitle,
@@ -1230,4 +1198,4 @@ async function explainLineRefusal(params) {
1230
1198
  };
1231
1199
  }
1232
1200
  //#endregion
1233
- export { withoutLineQuoteTokens as A, toFlexMessage as C, recordLineQuoteToken as D, readLineQuoteToken as E, inferLineTargetChatType as M, normalizeLineMessagingTarget as N, reportLineQuoteCarrierMissing as O, fitsLineFlexBubble as S, canCarryLineQuoteToken as T, normalizeLineAction as _, probeLineBot as a, truncateLineActionLabel as b, createAgendaCard as c, createActionCard as d, createImageCard as f, messageAction as g, createCardBubble as h, runLinePushWithRetries as i, buildLineMediaMessage as j, resolveLineQuoteToken as k, createEventCard as l, createListCard as m, findLineHttpError as n, resolveLineChannelAccessToken as o, createInfoCard as p, resolveLineNonDispatchRetryable as r, createLineSendReceipt as s, explainLineRefusal as t, createReceiptCard as u, normalizeLineMessage as v, applyLineQuoteToken as w, uriAction as x, postbackAction as y };
1201
+ export { withoutLineQuoteTokens as A, toFlexMessage as C, recordLineQuoteToken as D, readLineQuoteToken as E, inferLineTargetChatType as M, normalizeLineMessagingTarget as N, reportLineQuoteCarrierMissing as O, fitsLineFlexBubble as S, canCarryLineQuoteToken as T, messageAction as _, probeLineBot as a, truncateLineActionLabel as b, createAgendaCard as c, createActionCard as d, createImageCard as f, createCardTitle as g, createCardBubble as h, runLinePushWithRetries as i, buildLineMediaMessage as j, resolveLineQuoteToken as k, createEventCard as l, createListCard as m, findLineHttpError as n, resolveLineChannelAccessToken as o, createInfoCard as p, resolveLineNonDispatchRetryable as r, createLineSendReceipt as s, explainLineRefusal as t, createReceiptCard as u, normalizeLineMessage as v, applyLineQuoteToken as w, uriAction as x, postbackAction as y };
@@ -4,13 +4,18 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runti
4
4
  import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
5
5
  import { createAllowFromSection, createPromptParsedAllowFromForAccount, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, parseSetupEntriesWithParser, patchScopedAccountConfig, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
6
6
  import { createChannelDmPolicy } from "openclaw/plugin-sdk/channel-dm-policy";
7
+ //#region extensions/line/src/bot-access.ts
8
+ function normalizeLineAllowEntry(value) {
9
+ return String(value).trim().replace(/^line:(?:user:)?/i, "");
10
+ }
11
+ //#endregion
7
12
  //#region extensions/line/src/account-helpers.ts
8
13
  function hasLineCredentials(account) {
9
14
  if (account.tokenStatus && account.signingSecretStatus) return account.tokenStatus !== "missing" && account.signingSecretStatus !== "missing";
10
15
  return Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim());
11
16
  }
12
17
  function parseLineAllowFromId(raw) {
13
- const trimmed = raw.trim().replace(/^line:(?:user:)?/i, "");
18
+ const trimmed = normalizeLineAllowEntry(raw);
14
19
  if (!/^U[a-f0-9]{32}$/i.test(trimmed)) return null;
15
20
  return trimmed;
16
21
  }
@@ -322,4 +327,4 @@ const lineSetupWizard = {
322
327
  disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
323
328
  };
324
329
  //#endregion
325
- export { hasLineCredentials as i, lineSetupAdapter as n, lineSetupContract as r, lineSetupWizard as t };
330
+ export { normalizeLineAllowEntry as a, hasLineCredentials as i, lineSetupAdapter as n, lineSetupContract as r, lineSetupWizard as t };
@@ -1,5 +1,6 @@
1
- import { C as toFlexMessage, S as fitsLineFlexBubble, _ as normalizeLineAction, g as messageAction, u as createReceiptCard, x as uriAction, y as postbackAction } from "./send-retry-DV6I0muq.mjs";
1
+ import { C as toFlexMessage, S as fitsLineFlexBubble, _ as messageAction, u as createReceiptCard, x as uriAction, y as postbackAction } from "./send-retry-B9Nor0-J.mjs";
2
2
  import { markdownToIRWithMeta, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
3
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
3
4
  //#region extensions/line/src/markdown-to-line.ts
4
5
  const LINE_MARKDOWN_OPTIONS = {
5
6
  assistantTranscriptRoleHeaders: true,
@@ -18,9 +19,6 @@ function parseLineMarkdown(text, tableMode = "block") {
18
19
  tableMode
19
20
  });
20
21
  }
21
- function codeBlockSpans(ir) {
22
- return ir.styles.filter((span) => span.style === "code_block");
23
- }
24
22
  function toCodeBlock(ir, span) {
25
23
  return {
26
24
  ...span.language ? { language: span.language } : {},
@@ -305,7 +303,7 @@ function convertCodeBlockToFlexBubble(block) {
305
303
  /** Parse once, route existing block surfaces to Flex, and project the remainder as plain text. */
306
304
  function processLineMessage(text) {
307
305
  const { ir, tables } = parseLineMarkdown(text);
308
- const codeSpans = codeBlockSpans(ir);
306
+ const codeSpans = ir.styles.filter((span) => span.style === "code_block");
309
307
  const plainTextInsertions = [];
310
308
  for (const table of tables) {
311
309
  const bubble = convertTableToFlexBubble(table);
@@ -338,17 +336,55 @@ function processLineMessage(text) {
338
336
  });
339
337
  }
340
338
  const segments = [];
341
- return {
342
- text: projectPlainText(ir, codeSpans, plainTextInsertions, (segment) => segments.push(segment)),
343
- flexMessages: segments.flatMap((segment) => segment.type === "flex" ? [segment.message] : []),
344
- ...plainTextInsertions.length > 0 ? { segments } : {}
345
- };
339
+ projectPlainText(ir, codeSpans, plainTextInsertions, (segment) => segments.push(segment));
340
+ return segments;
341
+ }
342
+ //#endregion
343
+ //#region packages/normalization-core/src/utf16-slice.ts
344
+ function isHighSurrogate(codeUnit) {
345
+ return codeUnit >= 55296 && codeUnit <= 56319;
346
+ }
347
+ function isLowSurrogate(codeUnit) {
348
+ return codeUnit >= 56320 && codeUnit <= 57343;
349
+ }
350
+ /** Moves a chunk boundary away from the middle of a UTF-16 surrogate pair. */
351
+ function avoidTrailingHighSurrogateBreak(text, start, end) {
352
+ if (end <= start || end >= text.length || !isHighSurrogate(text.charCodeAt(end - 1)) || !isLowSurrogate(text.charCodeAt(end))) return end;
353
+ const adjusted = end - 1;
354
+ return adjusted > start ? adjusted : end + 1;
355
+ }
356
+ //#endregion
357
+ //#region packages/normalization-core/src/grapheme.ts
358
+ let graphemeSegmenter;
359
+ function getGraphemeSegmenter() {
360
+ graphemeSegmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
361
+ return graphemeSegmenter;
362
+ }
363
+ /** Uses the same source text that created segments. */
364
+ function containingSegment(segments, text, index) {
365
+ const position = Number.isNaN(index) ? 0 : Math.trunc(index);
366
+ const codePoint = text.codePointAt(position);
367
+ return segments.containing(codePoint !== void 0 && codePoint > 65535 ? position + 1 : position);
368
+ }
369
+ /**
370
+ * Chooses a whole-grapheme cut within the hard budget, honoring a usable preference.
371
+ * If no whole grapheme fits, allowPartial permits a surrogate-safe progress cut;
372
+ * a leading surrogate pair can exceed maxEnd by one code unit.
373
+ */
374
+ function findGraphemeChunkEnd(text, start, maxEnd, preferredEnd = maxEnd, allowPartial = true) {
375
+ const hardEnd = Math.min(maxEnd, text.length);
376
+ if (hardEnd <= start) return start;
377
+ const preferred = Number.isInteger(preferredEnd) && preferredEnd > start && preferredEnd <= hardEnd ? preferredEnd : hardEnd;
378
+ if (preferred === text.length) return preferred;
379
+ const segments = getGraphemeSegmenter().segment(text);
380
+ let end = containingSegment(segments, text, preferred)?.index ?? preferred;
381
+ if (end <= start && preferred < hardEnd) end = hardEnd === text.length ? hardEnd : containingSegment(segments, text, hardEnd)?.index ?? hardEnd;
382
+ return end > start ? end : allowPartial ? avoidTrailingHighSurrogateBreak(text, start, hardEnd) : start;
346
383
  }
347
384
  //#endregion
348
385
  //#region extensions/line/src/template-messages.ts
349
386
  const COMPACT_TEMPLATE_TEXT_LIMIT = 60;
350
387
  const TEMPLATE_ALT_TEXT_LIMIT = 1500;
351
- const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
352
388
  function buildTemplatePayloadAction(action) {
353
389
  if (action.type === "uri" && action.uri) return uriAction(action.label, action.uri);
354
390
  if (action.type === "postback" && action.data) return postbackAction(action.label, action.data, action.label);
@@ -361,40 +397,19 @@ function resolveTemplateTextLimit(params) {
361
397
  return params.title !== void 0 || params.thumbnailImageUrl !== void 0 ? COMPACT_TEMPLATE_TEXT_LIMIT : params.textOnlyLimit;
362
398
  }
363
399
  function truncateTemplateText(text, limit) {
364
- let result = "";
365
- for (const { segment } of graphemeSegmenter.segment(text)) {
366
- if (result.length + segment.length > limit) {
367
- if (!result) for (const codePoint of segment) {
368
- if (result.length + codePoint.length > limit) break;
369
- result += codePoint;
370
- }
371
- break;
372
- }
373
- result += segment;
374
- }
375
- return result;
376
- }
377
- function truncateOptionalTemplateText(value, limit) {
378
- return value === void 0 ? void 0 : truncateTemplateText(value, limit);
400
+ const end = findGraphemeChunkEnd(text, 0, limit, limit, false);
401
+ return end > 0 ? text.slice(0, end) : truncateUtf16Safe(text, limit);
379
402
  }
380
403
  function resolveTemplateAltText(value, fallback) {
381
404
  return truncateTemplateText(value ?? fallback, TEMPLATE_ALT_TEXT_LIMIT);
382
405
  }
383
- function normalizeCarouselColumn(column) {
384
- return {
385
- ...column,
386
- title: column.title || void 0,
387
- actions: column.actions.map((action) => normalizeLineAction(action)).filter((action) => action.label !== void 0 && action.label !== "").slice(0, 3),
388
- defaultAction: column.defaultAction === void 0 ? void 0 : normalizeLineAction(column.defaultAction)
389
- };
390
- }
391
406
  function describeCarouselColumn(column) {
392
407
  const body = column.title ? `${column.title}: ${column.text}` : column.text;
393
408
  const labels = column.actions.map((action) => action.label).filter((label) => label !== void 0 && label !== "");
394
409
  return labels.length > 0 ? `${body} (${labels.join(" / ")})` : body;
395
410
  }
396
411
  function normalizeCarousel(columns, altText) {
397
- const normalized = columns.slice(0, 10).map(normalizeCarouselColumn);
412
+ const normalized = columns.slice(0, 10);
398
413
  const first = normalized[0];
399
414
  if (!(!first || normalized.some((column) => column.text === "" || column.actions.length === 0 || column.title === void 0 !== (first.title === void 0) || column.actions.length !== first.actions.length))) return {
400
415
  kind: "template",
@@ -406,104 +421,72 @@ function normalizeCarousel(columns, altText) {
406
421
  text
407
422
  } : { kind: "empty" };
408
423
  }
409
- function createCarouselMessage(columns, options) {
410
- const template = {
411
- type: "carousel",
412
- columns,
413
- imageAspectRatio: options?.imageAspectRatio ?? "rectangle",
414
- imageSize: options?.imageSize ?? "cover"
415
- };
416
- return {
417
- type: "template",
418
- altText: resolveTemplateAltText(options?.altText, "View carousel"),
419
- template
420
- };
421
- }
422
- /**
423
- * Create a confirm template (yes/no style dialog)
424
- */
425
- function createConfirmTemplate(text, confirmAction, cancelAction, altText) {
426
- const template = {
427
- type: "confirm",
428
- text: truncateTemplateText(text, 240),
429
- actions: [normalizeLineAction(confirmAction), normalizeLineAction(cancelAction)]
430
- };
431
- return {
432
- type: "template",
433
- altText: resolveTemplateAltText(altText, text),
434
- template
435
- };
436
- }
437
- /**
438
- * Create a button template with title, text, and action buttons
439
- */
440
- function createButtonTemplate(title, text, actions, options) {
441
- const normalizedTitle = title || void 0;
442
- const textLimit = resolveTemplateTextLimit({
443
- title: normalizedTitle,
444
- thumbnailImageUrl: options?.thumbnailImageUrl,
445
- textOnlyLimit: 160
446
- });
447
- const template = {
448
- type: "buttons",
449
- ...normalizedTitle ? { title: truncateTemplateText(normalizedTitle, 40) } : {},
450
- text: truncateTemplateText(text, textLimit),
451
- actions: actions.slice(0, 4).map((action) => normalizeLineAction(action)),
452
- thumbnailImageUrl: options?.thumbnailImageUrl,
453
- imageAspectRatio: options?.imageAspectRatio ?? "rectangle",
454
- imageSize: options?.imageSize ?? "cover",
455
- imageBackgroundColor: options?.imageBackgroundColor,
456
- defaultAction: options?.defaultAction === void 0 ? void 0 : normalizeLineAction(options.defaultAction)
457
- };
458
- return {
459
- type: "template",
460
- altText: resolveTemplateAltText(options?.altText, normalizedTitle ? `${normalizedTitle}: ${text}` : text),
461
- template
462
- };
463
- }
464
- function createCarouselColumn(params) {
465
- const normalizedTitle = params.title || void 0;
466
- const textLimit = resolveTemplateTextLimit({
467
- ...params,
468
- title: normalizedTitle,
469
- textOnlyLimit: 120
470
- });
471
- return normalizeCarouselColumn({
472
- title: truncateOptionalTemplateText(normalizedTitle, 40),
473
- text: truncateTemplateText(params.text, textLimit),
474
- actions: params.actions,
475
- thumbnailImageUrl: params.thumbnailImageUrl,
476
- imageBackgroundColor: params.imageBackgroundColor,
477
- defaultAction: params.defaultAction
478
- });
479
- }
480
- /**
481
- * Convert a TemplateMessagePayload from ReplyPayload to a LINE TemplateMessage
482
- */
424
+ /** Convert portable template payloads at the LINE provider boundary. */
483
425
  function buildTemplateMessageFromPayload(payload) {
484
426
  switch (payload.type) {
485
- case "confirm": return createConfirmTemplate(payload.text, buildInferredTemplateAction(payload.confirmLabel, payload.confirmData), buildInferredTemplateAction(payload.cancelLabel, payload.cancelData), payload.altText);
427
+ case "confirm": return {
428
+ type: "template",
429
+ altText: resolveTemplateAltText(payload.altText, payload.text),
430
+ template: {
431
+ type: "confirm",
432
+ text: truncateTemplateText(payload.text, 240),
433
+ actions: [buildInferredTemplateAction(payload.confirmLabel, payload.confirmData), buildInferredTemplateAction(payload.cancelLabel, payload.cancelData)]
434
+ }
435
+ };
486
436
  case "buttons": {
487
- const actions = payload.actions.slice(0, 4).map(buildTemplatePayloadAction);
488
- return createButtonTemplate(payload.title, payload.text, actions, {
437
+ const title = payload.title || void 0;
438
+ const textLimit = resolveTemplateTextLimit({
439
+ title,
489
440
  thumbnailImageUrl: payload.thumbnailImageUrl,
490
- altText: payload.altText
441
+ textOnlyLimit: 160
491
442
  });
443
+ return {
444
+ type: "template",
445
+ altText: resolveTemplateAltText(payload.altText, title ? `${title}: ${payload.text}` : payload.text),
446
+ template: {
447
+ type: "buttons",
448
+ ...title ? { title: truncateTemplateText(title, 40) } : {},
449
+ text: truncateTemplateText(payload.text, textLimit),
450
+ actions: payload.actions.slice(0, 4).map(buildTemplatePayloadAction),
451
+ thumbnailImageUrl: payload.thumbnailImageUrl,
452
+ imageAspectRatio: "rectangle",
453
+ imageSize: "cover",
454
+ imageBackgroundColor: void 0,
455
+ defaultAction: void 0
456
+ }
457
+ };
492
458
  }
493
459
  case "carousel": {
494
- const outcome = normalizeCarousel(payload.columns.map((col) => {
495
- return createCarouselColumn({
496
- title: col.title,
497
- text: col.text,
498
- thumbnailImageUrl: col.thumbnailImageUrl,
499
- actions: col.actions.map(buildTemplatePayloadAction)
460
+ const outcome = normalizeCarousel(payload.columns.map((column) => {
461
+ const title = column.title || void 0;
462
+ const textLimit = resolveTemplateTextLimit({
463
+ title,
464
+ thumbnailImageUrl: column.thumbnailImageUrl,
465
+ textOnlyLimit: 120
500
466
  });
467
+ return {
468
+ title: title === void 0 ? void 0 : truncateTemplateText(title, 40),
469
+ text: truncateTemplateText(column.text, textLimit),
470
+ actions: column.actions.map(buildTemplatePayloadAction).filter((action) => action.label !== void 0 && action.label !== "").slice(0, 3),
471
+ thumbnailImageUrl: column.thumbnailImageUrl,
472
+ imageBackgroundColor: void 0,
473
+ defaultAction: void 0
474
+ };
501
475
  }), payload.altText);
502
476
  if (outcome.kind === "empty") return null;
503
477
  return outcome.kind === "text" ? {
504
478
  type: "text",
505
479
  text: outcome.text
506
- } : createCarouselMessage(outcome.columns, { altText: payload.altText });
480
+ } : {
481
+ type: "template",
482
+ altText: resolveTemplateAltText(payload.altText, "View carousel"),
483
+ template: {
484
+ type: "carousel",
485
+ columns: outcome.columns,
486
+ imageAspectRatio: "rectangle",
487
+ imageSize: "cover"
488
+ }
489
+ };
507
490
  }
508
491
  default: return null;
509
492
  }
@@ -52,8 +52,5 @@ function laneKeyFor(event, eventId) {
52
52
  }
53
53
  return eventId;
54
54
  }
55
- function errorText(error) {
56
- return error instanceof Error ? error.message : String(error);
57
- }
58
55
  //#endregion
59
- export { eventIdFor as a, errorText as i, LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE as n, laneKeyFor as o, LineWebhookPayloadError as r, legacyEventIdFor as s, LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON as t };
56
+ export { laneKeyFor as a, eventIdFor as i, LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE as n, legacyEventIdFor as o, LineWebhookPayloadError as r, LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON as t };
package/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as linePlugin } from "./.setup/channel-DN6Za8Vm.mjs";
2
- import { t as lineSetupPlugin } from "./.setup/api-w4QdlCCB.mjs";
1
+ import { t as linePlugin } from "./.setup/channel-InKuXaRS.mjs";
2
+ import { t as lineSetupPlugin } from "./.setup/api-CMU7bG-C.mjs";
3
3
  export { linePlugin, lineSetupPlugin };
@@ -1,2 +1,2 @@
1
- import { t as linePlugin } from "./.setup/channel-DN6Za8Vm.mjs";
1
+ import { t as linePlugin } from "./.setup/channel-InKuXaRS.mjs";
2
2
  export { linePlugin };
@@ -1,5 +1,6 @@
1
- import { a as eventIdFor, i as errorText, o as laneKeyFor, r as LineWebhookPayloadError, s as legacyEventIdFor, t as LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON } from "./.setup/webhook-spool-contract-1rY8OJw8.mjs";
1
+ import { a as laneKeyFor, i as eventIdFor, o as legacyEventIdFor, r as LineWebhookPayloadError, t as LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON } from "./.setup/webhook-spool-contract-msjTl60Q.mjs";
2
2
  import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
3
+ import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
3
4
  //#region extensions/line/src/webhook-spool-migration.ts
4
5
  /** Pre-drain (#109655) rows stored the event object under `event` instead of the
5
6
  * canonical serialized `rawEvent`; anything else is not a migratable row. */
@@ -49,7 +50,7 @@ async function migrateLineLegacySpoolRows(queue) {
49
50
  try {
50
51
  if ((await queue.resubmit?.(row.id, { resubmittedAt: row.receivedAt }))?.kind === "resubmitted") result.recovered += 1;
51
52
  } catch (error) {
52
- result.failures.push(`row ${row.id}: ${errorText(error)}`);
53
+ result.failures.push(`row ${row.id}: ${coerceErrorMessage(error)}`);
53
54
  }
54
55
  }
55
56
  const pending = await queue.listPending({
@@ -66,7 +67,7 @@ async function migrateLineLegacySpoolRows(queue) {
66
67
  } catch (error) {
67
68
  await queue.fail(record.id, {
68
69
  reason: LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON,
69
- message: errorText(error)
70
+ message: coerceErrorMessage(error)
70
71
  });
71
72
  result.deadLettered += 1;
72
73
  continue;
@@ -84,7 +85,7 @@ async function migrateLineLegacySpoolRows(queue) {
84
85
  if (admitted.kind === "completed" || admitted.kind === "failed") result.reconciled += 1;
85
86
  else result.migrated += 1;
86
87
  } catch (error) {
87
- result.failures.push(`row ${record.id}: ${errorText(error)}`);
88
+ result.failures.push(`row ${record.id}: ${coerceErrorMessage(error)}`);
88
89
  }
89
90
  }
90
91
  return result;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
2
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
3
3
  //#region extensions/line/index.ts
4
- const loadLineCardCommand = createLazyRuntimeModule(() => import("./.setup/card-command-DYf4gnmI.mjs"));
4
+ const loadLineCardCommand = createLazyRuntimeModule(() => import("./.setup/card-command-BrU6u6pm.mjs"));
5
5
  var line_default = defineBundledChannelEntry({
6
6
  id: "line",
7
7
  name: "LINE",
package/dist/setup-api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as lineSetupAdapter, t as lineSetupWizard } from "./.setup/setup-surface-CbfkmkIS.mjs";
1
+ import { n as lineSetupAdapter, t as lineSetupWizard } from "./.setup/setup-surface-1jkHGWg7.mjs";
2
2
  export { lineSetupAdapter, lineSetupWizard };
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  var setup_entry_default = defineBundledChannelSetupEntry({
5
5
  importMetaUrl: import.meta.url,
6
6
  plugin: {
7
- specifier: fileURLToPath(new URL(".setup/api-4RgpQmHW.mjs", import.meta.url).href),
7
+ specifier: fileURLToPath(new URL(".setup/api-BBSLQek1.mjs", import.meta.url).href),
8
8
  exportName: "lineSetupPlugin"
9
9
  }
10
10
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/line",
3
- "version": "2026.9.8",
3
+ "version": "2026.10.1-beta.1",
4
4
  "description": "OpenClaw LINE channel plugin for LINE Bot API chats.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,7 @@
13
13
  "zod": "4.6.5"
14
14
  },
15
15
  "peerDependencies": {
16
- "openclaw": ">=2026.9.8"
16
+ "openclaw": ">=2026.10.1-beta.1"
17
17
  },
18
18
  "peerDependenciesMeta": {
19
19
  "openclaw": {
@@ -122,10 +122,10 @@
122
122
  "minHostVersion": ">=2026.4.10"
123
123
  },
124
124
  "compat": {
125
- "pluginApi": ">=2026.9.8"
125
+ "pluginApi": ">=2026.10.1-beta.1"
126
126
  },
127
127
  "build": {
128
- "openclawVersion": "2026.9.8"
128
+ "openclawVersion": "2026.10.1-beta.1"
129
129
  },
130
130
  "release": {
131
131
  "publishToClawHub": true,
@@ -1,3 +0,0 @@
1
- import { t as linePlugin } from "./channel-DN6Za8Vm.mjs";
2
- import { t as lineSetupPlugin } from "./api-w4QdlCCB.mjs";
3
- export { linePlugin, lineSetupPlugin };
@@ -1,2 +0,0 @@
1
- import { t as monitorLineProvider } from "./monitor-C01hAm3f.mjs";
2
- export { monitorLineProvider };
@@ -1,2 +0,0 @@
1
- import { a as probeLineBot } from "./send-retry-DV6I0muq.mjs";
2
- export { probeLineBot };