
































@@ -13,6 +13,9 @@ type CarouselColumn = messagingApi.CarouselColumn;
1313type ImageCarouselTemplate = messagingApi.ImageCarouselTemplate;
1414type ImageCarouselColumn = messagingApi.ImageCarouselColumn;
151516+const COMPACT_TEMPLATE_TEXT_LIMIT = 60;
17+const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
18+1619type TemplatePayloadAction = {
1720type?: "uri" | "postback" | "message";
1821uri?: string;
@@ -30,6 +33,48 @@ function buildTemplatePayloadAction(action: TemplatePayloadAction): Action {
3033return messageAction(action.label, action.data ?? action.label);
3134}
323536+function resolveTemplateTextLimit(params: {
37+title?: string;
38+thumbnailImageUrl?: string;
39+textOnlyLimit: number;
40+}): number {
41+return params.title !== undefined || params.thumbnailImageUrl !== undefined
42+ ? COMPACT_TEMPLATE_TEXT_LIMIT
43+ : params.textOnlyLimit;
44+}
45+46+function truncateTemplateText(text: string, limit: number): string {
47+let result = "";
48+for (const { segment } of graphemeSegmenter.segment(text)) {
49+if (result.length + segment.length > limit) {
50+// A pathological grapheme can exceed LINE's whole field limit. Preserve
51+// graphemes normally, but keep required text non-empty without splitting
52+// a surrogate pair when the first grapheme alone cannot fit.
53+if (!result) {
54+for (const codePoint of segment) {
55+if (result.length + codePoint.length > limit) {
56+break;
57+}
58+result += codePoint;
59+}
60+}
61+break;
62+}
63+result += segment;
64+}
65+return result;
66+}
67+68+function formatProductCarouselText(description: string, price?: string): string {
69+if (!price) {
70+return description;
71+}
72+const priceText = truncateTemplateText(price, COMPACT_TEMPLATE_TEXT_LIMIT);
73+const descriptionLimit = Math.max(0, COMPACT_TEMPLATE_TEXT_LIMIT - priceText.length - 1);
74+const descriptionText = truncateTemplateText(description, descriptionLimit);
75+return descriptionText ? `${descriptionText}\n${priceText}` : priceText;
76+}
77+3378/**
3479 * Create a confirm template (yes/no style dialog)
3580 */
@@ -68,12 +113,15 @@ export function createButtonTemplate(
68113altText?: string;
69114},
70115): TemplateMessage {
71-const hasThumbnail = Boolean(options?.thumbnailImageUrl?.trim());
72-const textLimit = hasThumbnail ? 160 : 60;
116+const textLimit = resolveTemplateTextLimit({
117+ title,
118+thumbnailImageUrl: options?.thumbnailImageUrl,
119+textOnlyLimit: 160,
120+});
73121const template: ButtonsTemplate = {
74122type: "buttons",
75123title: title.slice(0, 40), // LINE limit
76-text: text.slice(0, textLimit), // LINE limit (60 if no thumbnail, 160 with thumbnail)
124+text: truncateTemplateText(text, textLimit),
77125actions: actions.slice(0, 4), // LINE limit: max 4 actions
78126thumbnailImageUrl: options?.thumbnailImageUrl,
79127imageAspectRatio: options?.imageAspectRatio ?? "rectangle",
@@ -125,9 +173,14 @@ export function createCarouselColumn(params: {
125173imageBackgroundColor?: string;
126174defaultAction?: Action;
127175}): CarouselColumn {
176+// LINE caps a carousel column's text at 60 chars when the column carries a
177+// title or thumbnail image, and 120 chars otherwise. Sending an over-length
178+// text makes LINE reject the whole carousel, so mirror the conditional limit
179+// the buttons template already applies above.
180+const textLimit = resolveTemplateTextLimit({ ...params, textOnlyLimit: 120 });
128181return {
129182title: params.title?.slice(0, 40),
130-text: params.text.slice(0, 120), // LINE limit
183+text: truncateTemplateText(params.text, textLimit),
131184actions: params.actions.slice(0, 3), // LINE limit: max 3 actions per column
132185thumbnailImageUrl: params.thumbnailImageUrl,
133186imageBackgroundColor: params.imageBackgroundColor,
@@ -256,9 +309,7 @@ export function createProductCarousel(
256309257310return createCarouselColumn({
258311title: product.title,
259-text: product.price
260- ? `${product.description}\n${product.price}`.slice(0, 120)
261- : product.description,
312+text: formatProductCarouselText(product.description, product.price),
262313thumbnailImageUrl: product.imageUrl,
263314 actions,
264315});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。