@openclaw/line 2026.7.2-beta.7 → 2026.8.1-beta.2

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,799 +0,0 @@
1
- import { i as resolveLineAccount } from "./accounts-BqEVsDoB.js";
2
- import { a as validateLineMediaUrl, t as createLineSendReceipt } from "./send-receipt-CY2ey8XH.js";
3
- import { _ as postbackAction, g as normalizeLineMessageActions, h as normalizeLineAction, m as messageAction, o as createReceiptCard, v as uriAction } from "./flex-templates-iKJsyCq-.js";
4
- import { markdownToIRWithMeta, stripMarkdown, stripMarkdown as stripMarkdown$1 } from "openclaw/plugin-sdk/text-chunking";
5
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
6
- import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
7
- import { messagingApi } from "@line/bot-sdk";
8
- import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
9
- import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
10
- import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
11
- //#region extensions/line/src/flex-templates/message.ts
12
- /**
13
- * Wrap a FlexContainer in a FlexMessage
14
- */
15
- function toFlexMessage(altText, contents) {
16
- return {
17
- type: "flex",
18
- altText,
19
- contents
20
- };
21
- }
22
- //#endregion
23
- //#region extensions/line/src/template-messages.ts
24
- const COMPACT_TEMPLATE_TEXT_LIMIT = 60;
25
- const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
26
- function buildTemplatePayloadAction(action) {
27
- if (action.type === "uri" && action.uri) return uriAction(action.label, action.uri);
28
- if (action.type === "postback" && action.data) return postbackAction(action.label, action.data, action.label);
29
- return messageAction(action.label, action.data ?? action.label);
30
- }
31
- function resolveTemplateTextLimit(params) {
32
- return params.title !== void 0 || params.thumbnailImageUrl !== void 0 ? COMPACT_TEMPLATE_TEXT_LIMIT : params.textOnlyLimit;
33
- }
34
- function truncateTemplateText(text, limit) {
35
- let result = "";
36
- for (const { segment } of graphemeSegmenter.segment(text)) {
37
- if (result.length + segment.length > limit) {
38
- if (!result) for (const codePoint of segment) {
39
- if (result.length + codePoint.length > limit) break;
40
- result += codePoint;
41
- }
42
- break;
43
- }
44
- result += segment;
45
- }
46
- return result;
47
- }
48
- function truncateOptionalTemplateText(value, limit) {
49
- return value === void 0 ? void 0 : truncateTemplateText(value, limit);
50
- }
51
- function formatProductCarouselText(description, price) {
52
- if (!price) return description;
53
- const priceText = truncateTemplateText(price, COMPACT_TEMPLATE_TEXT_LIMIT);
54
- const descriptionText = truncateTemplateText(description, Math.max(0, COMPACT_TEMPLATE_TEXT_LIMIT - priceText.length - 1));
55
- return descriptionText ? `${descriptionText}\n${priceText}` : priceText;
56
- }
57
- function normalizeCarouselColumnActions(column) {
58
- return {
59
- ...column,
60
- actions: column.actions.map((action) => normalizeLineAction(action)),
61
- defaultAction: column.defaultAction === void 0 ? void 0 : normalizeLineAction(column.defaultAction)
62
- };
63
- }
64
- function normalizeImageCarouselColumnAction(column) {
65
- return {
66
- ...column,
67
- action: normalizeLineAction(column.action, 12)
68
- };
69
- }
70
- /**
71
- * Create a confirm template (yes/no style dialog)
72
- */
73
- function createConfirmTemplate(text, confirmAction, cancelAction, altText) {
74
- const template = {
75
- type: "confirm",
76
- text: truncateTemplateText(text, 240),
77
- actions: [normalizeLineAction(confirmAction), normalizeLineAction(cancelAction)]
78
- };
79
- return {
80
- type: "template",
81
- altText: truncateOptionalTemplateText(altText, 400) ?? truncateTemplateText(text, 400),
82
- template
83
- };
84
- }
85
- /**
86
- * Create a button template with title, text, and action buttons
87
- */
88
- function createButtonTemplate(title, text, actions, options) {
89
- const normalizedTitle = title || void 0;
90
- const textLimit = resolveTemplateTextLimit({
91
- title: normalizedTitle,
92
- thumbnailImageUrl: options?.thumbnailImageUrl,
93
- textOnlyLimit: 160
94
- });
95
- const template = {
96
- type: "buttons",
97
- ...normalizedTitle ? { title: truncateTemplateText(normalizedTitle, 40) } : {},
98
- text: truncateTemplateText(text, textLimit),
99
- actions: actions.slice(0, 4).map((action) => normalizeLineAction(action)),
100
- thumbnailImageUrl: options?.thumbnailImageUrl,
101
- imageAspectRatio: options?.imageAspectRatio ?? "rectangle",
102
- imageSize: options?.imageSize ?? "cover",
103
- imageBackgroundColor: options?.imageBackgroundColor,
104
- defaultAction: options?.defaultAction === void 0 ? void 0 : normalizeLineAction(options.defaultAction)
105
- };
106
- return {
107
- type: "template",
108
- altText: truncateOptionalTemplateText(options?.altText, 400) ?? truncateTemplateText(normalizedTitle ? `${normalizedTitle}: ${text}` : text, 400),
109
- template
110
- };
111
- }
112
- /**
113
- * Create a carousel template with multiple columns
114
- */
115
- function createTemplateCarousel(columns, options) {
116
- const template = {
117
- type: "carousel",
118
- columns: columns.slice(0, 10).map(normalizeCarouselColumnActions),
119
- imageAspectRatio: options?.imageAspectRatio ?? "rectangle",
120
- imageSize: options?.imageSize ?? "cover"
121
- };
122
- return {
123
- type: "template",
124
- altText: truncateOptionalTemplateText(options?.altText, 400) ?? "View carousel",
125
- template
126
- };
127
- }
128
- /**
129
- * Create a carousel column for use with createTemplateCarousel
130
- */
131
- function createCarouselColumn(params) {
132
- const textLimit = resolveTemplateTextLimit({
133
- ...params,
134
- textOnlyLimit: 120
135
- });
136
- return {
137
- title: truncateOptionalTemplateText(params.title, 40),
138
- text: truncateTemplateText(params.text, textLimit),
139
- actions: params.actions.slice(0, 3).map((action) => normalizeLineAction(action)),
140
- thumbnailImageUrl: params.thumbnailImageUrl,
141
- imageBackgroundColor: params.imageBackgroundColor,
142
- defaultAction: params.defaultAction === void 0 ? void 0 : normalizeLineAction(params.defaultAction)
143
- };
144
- }
145
- /**
146
- * Create an image carousel template (simpler, image-focused carousel)
147
- */
148
- function createImageCarousel(columns, altText) {
149
- const template = {
150
- type: "image_carousel",
151
- columns: columns.slice(0, 10).map(normalizeImageCarouselColumnAction)
152
- };
153
- return {
154
- type: "template",
155
- altText: truncateOptionalTemplateText(altText, 400) ?? "View images",
156
- template
157
- };
158
- }
159
- /**
160
- * Create an image carousel column for use with createImageCarousel
161
- */
162
- function createImageCarouselColumn(imageUrl, action) {
163
- return {
164
- imageUrl,
165
- action: normalizeLineAction(action, 12)
166
- };
167
- }
168
- /**
169
- * Create a simple yes/no confirmation dialog
170
- */
171
- function createYesNoConfirm(question, options) {
172
- return createConfirmTemplate(question, options?.yesData ? postbackAction(options.yesText ?? "Yes", options.yesData, options.yesText ?? "Yes") : messageAction(options?.yesText ?? "Yes"), options?.noData ? postbackAction(options.noText ?? "No", options.noData, options.noText ?? "No") : messageAction(options?.noText ?? "No"), options?.altText);
173
- }
174
- /**
175
- * Create a button menu with simple text buttons
176
- */
177
- function createButtonMenu(title, text, buttons, options) {
178
- return createButtonTemplate(title, text, buttons.slice(0, 4).map((btn) => messageAction(btn.label, btn.text)), {
179
- thumbnailImageUrl: options?.thumbnailImageUrl,
180
- altText: options?.altText
181
- });
182
- }
183
- /**
184
- * Create a button menu with URL links
185
- */
186
- function createLinkMenu(title, text, links, options) {
187
- return createButtonTemplate(title, text, links.slice(0, 4).map((link) => uriAction(link.label, link.url)), {
188
- thumbnailImageUrl: options?.thumbnailImageUrl,
189
- altText: options?.altText
190
- });
191
- }
192
- /**
193
- * Create a simple product/item carousel
194
- */
195
- function createProductCarousel(products, altText) {
196
- return createTemplateCarousel(products.slice(0, 10).map((product) => {
197
- const actions = [];
198
- if (product.actionUrl) actions.push(uriAction(product.actionLabel ?? "View", product.actionUrl));
199
- else if (product.actionData) actions.push(postbackAction(product.actionLabel ?? "Select", product.actionData));
200
- else actions.push(messageAction(product.actionLabel ?? "Select", product.title));
201
- return createCarouselColumn({
202
- title: product.title,
203
- text: formatProductCarouselText(product.description, product.price),
204
- thumbnailImageUrl: product.imageUrl,
205
- actions
206
- });
207
- }), { altText });
208
- }
209
- /**
210
- * Convert a TemplateMessagePayload from ReplyPayload to a LINE TemplateMessage
211
- */
212
- function buildTemplateMessageFromPayload(payload) {
213
- switch (payload.type) {
214
- case "confirm": {
215
- const confirmAction = payload.confirmData.startsWith("http") ? uriAction(payload.confirmLabel, payload.confirmData) : payload.confirmData.includes("=") ? postbackAction(payload.confirmLabel, payload.confirmData, payload.confirmLabel) : messageAction(payload.confirmLabel, payload.confirmData);
216
- const cancelAction = payload.cancelData.startsWith("http") ? uriAction(payload.cancelLabel, payload.cancelData) : payload.cancelData.includes("=") ? postbackAction(payload.cancelLabel, payload.cancelData, payload.cancelLabel) : messageAction(payload.cancelLabel, payload.cancelData);
217
- return createConfirmTemplate(payload.text, confirmAction, cancelAction, payload.altText);
218
- }
219
- case "buttons": {
220
- const actions = payload.actions.slice(0, 4).map((action) => buildTemplatePayloadAction(action));
221
- return createButtonTemplate(payload.title, payload.text, actions, {
222
- thumbnailImageUrl: payload.thumbnailImageUrl,
223
- altText: payload.altText
224
- });
225
- }
226
- case "carousel": return createTemplateCarousel(payload.columns.slice(0, 10).map((col) => {
227
- const colActions = col.actions.slice(0, 3).map((action) => buildTemplatePayloadAction(action));
228
- return createCarouselColumn({
229
- title: col.title,
230
- text: col.text,
231
- thumbnailImageUrl: col.thumbnailImageUrl,
232
- actions: colActions
233
- });
234
- }), { altText: payload.altText });
235
- default: return null;
236
- }
237
- }
238
- //#endregion
239
- //#region extensions/line/src/channel-access-token.ts
240
- function resolveLineChannelAccessToken(explicit, params) {
241
- if (explicit?.trim()) return explicit.trim();
242
- if (!params.channelAccessToken) throw new Error(`LINE channel access token missing for account "${params.accountId}" (set channels.line.channelAccessToken or LINE_CHANNEL_ACCESS_TOKEN).`);
243
- return params.channelAccessToken.trim();
244
- }
245
- //#endregion
246
- //#region extensions/line/src/send.ts
247
- const userProfileCache = /* @__PURE__ */ new Map();
248
- const PROFILE_CACHE_TTL_MS = 300 * 1e3;
249
- const PROFILE_CACHE_MAX_ENTRIES = 1e3;
250
- function cacheUserProfile(userId, profile) {
251
- userProfileCache.delete(userId);
252
- userProfileCache.set(userId, profile);
253
- if (userProfileCache.size <= PROFILE_CACHE_MAX_ENTRIES) return;
254
- for (const [key, cached] of userProfileCache) if (profile.fetchedAt - cached.fetchedAt >= PROFILE_CACHE_TTL_MS) userProfileCache.delete(key);
255
- pruneMapToMaxSize(userProfileCache, PROFILE_CACHE_MAX_ENTRIES);
256
- }
257
- function normalizeTarget(to) {
258
- const trimmed = to.trim();
259
- if (!trimmed) throw new Error("Recipient is required for LINE sends");
260
- const normalized = trimmed.replace(/^line:group:/i, "").replace(/^line:room:/i, "").replace(/^line:user:/i, "").replace(/^line:/i, "");
261
- if (!normalized) throw new Error("Recipient is required for LINE sends");
262
- if (normalized.length >= 33 && !/^[CUR]/.test(normalized)) throw new Error(`Recipient is not a valid LINE id (case-sensitive; expected leading capital C/U/R): ${truncateUtf16Safe(normalized, 4)}…`);
263
- return normalized;
264
- }
265
- function isLineUserChatId(chatId) {
266
- return /^U/i.test(chatId);
267
- }
268
- function createLineMessagingClient(opts) {
269
- const account = resolveLineAccount({
270
- cfg: requireRuntimeConfig(opts.cfg, "LINE send"),
271
- accountId: opts.accountId
272
- });
273
- const token = resolveLineChannelAccessToken(opts.channelAccessToken, account);
274
- return {
275
- account,
276
- client: new messagingApi.MessagingApiClient({ channelAccessToken: token })
277
- };
278
- }
279
- function createLinePushContext(to, opts) {
280
- const { account, client } = createLineMessagingClient(opts);
281
- return {
282
- account,
283
- client,
284
- chatId: normalizeTarget(to)
285
- };
286
- }
287
- function createTextMessage(text) {
288
- return {
289
- type: "text",
290
- text
291
- };
292
- }
293
- function createImageMessage(originalContentUrl, previewImageUrl) {
294
- return {
295
- type: "image",
296
- originalContentUrl,
297
- previewImageUrl: previewImageUrl ?? originalContentUrl
298
- };
299
- }
300
- function createVideoMessage(originalContentUrl, previewImageUrl, trackingId) {
301
- return {
302
- type: "video",
303
- originalContentUrl,
304
- previewImageUrl,
305
- ...trackingId ? { trackingId } : {}
306
- };
307
- }
308
- function createAudioMessage(originalContentUrl, durationMs) {
309
- return {
310
- type: "audio",
311
- originalContentUrl,
312
- duration: durationMs
313
- };
314
- }
315
- function createLocationMessage(location) {
316
- return {
317
- type: "location",
318
- title: truncateUtf16Safe(location.title, 100),
319
- address: truncateUtf16Safe(location.address, 100),
320
- latitude: location.latitude,
321
- longitude: location.longitude
322
- };
323
- }
324
- function logLineHttpError(err, context) {
325
- if (!err || typeof err !== "object") return;
326
- const { status, statusText, body } = err;
327
- if (typeof body === "string") logVerbose(`line: ${context} failed (${status ? `${status} ${statusText ?? ""}`.trim() : "unknown status"}): ${body}`);
328
- }
329
- function recordLineOutboundActivity(accountId) {
330
- recordChannelActivity({
331
- channel: "line",
332
- accountId,
333
- direction: "outbound"
334
- });
335
- }
336
- function resolveLineReceiptKind(messages) {
337
- const types = new Set(messages.map((message) => message.type));
338
- if (types.has("audio")) return "voice";
339
- if (types.has("image") || types.has("video")) return "media";
340
- if (types.has("flex") || types.has("template") || types.has("location")) return "card";
341
- if (types.has("text")) return "text";
342
- return "unknown";
343
- }
344
- async function pushLineMessages(to, messages, opts, behavior = {}) {
345
- if (messages.length === 0) throw new Error("Message must be non-empty for LINE sends");
346
- const { account, client, chatId } = createLinePushContext(to, opts);
347
- const normalizedMessages = messages.map(normalizeLineMessageActions);
348
- const pushRequest = client.pushMessage({
349
- to: chatId,
350
- messages: normalizedMessages
351
- });
352
- if (behavior.errorContext) await pushRequest.catch((err) => {
353
- logLineHttpError(err, behavior.errorContext);
354
- throw err;
355
- });
356
- else await pushRequest;
357
- recordLineOutboundActivity(account.accountId);
358
- if (opts.verbose) logVerbose(behavior.verboseMessage?.(chatId, messages.length) ?? `line: pushed ${messages.length} messages to ${chatId}`);
359
- return {
360
- messageId: "push",
361
- chatId,
362
- receipt: createLineSendReceipt({
363
- messageId: "push",
364
- chatId,
365
- kind: resolveLineReceiptKind(messages),
366
- messageCount: messages.length
367
- })
368
- };
369
- }
370
- async function replyLineMessages(replyToken, messages, opts, behavior = {}) {
371
- const { account, client } = createLineMessagingClient(opts);
372
- const normalizedMessages = messages.map(normalizeLineMessageActions);
373
- await client.replyMessage({
374
- replyToken,
375
- messages: normalizedMessages
376
- });
377
- recordLineOutboundActivity(account.accountId);
378
- if (opts.verbose) logVerbose(behavior.verboseMessage?.(messages.length) ?? `line: replied with ${messages.length} messages`);
379
- }
380
- async function sendMessageLine(to, text, opts) {
381
- const chatId = normalizeTarget(to);
382
- const messages = [];
383
- const mediaUrl = opts.mediaUrl?.trim();
384
- if (mediaUrl) {
385
- await validateLineMediaUrl(mediaUrl);
386
- switch (opts.mediaKind) {
387
- case "video": {
388
- const previewImageUrl = opts.previewImageUrl?.trim();
389
- if (!previewImageUrl) throw new Error("LINE video messages require previewImageUrl to reference an image URL");
390
- await validateLineMediaUrl(previewImageUrl);
391
- const trackingId = isLineUserChatId(chatId) ? opts.trackingId : void 0;
392
- messages.push(createVideoMessage(mediaUrl, previewImageUrl, trackingId));
393
- break;
394
- }
395
- case "audio":
396
- messages.push(createAudioMessage(mediaUrl, opts.durationMs ?? 6e4));
397
- break;
398
- default:
399
- {
400
- const previewImageUrl = opts.previewImageUrl?.trim() || mediaUrl;
401
- await validateLineMediaUrl(previewImageUrl);
402
- messages.push(createImageMessage(mediaUrl, previewImageUrl));
403
- }
404
- break;
405
- }
406
- }
407
- if (text?.trim()) messages.push(createTextMessage(text.trim()));
408
- if (messages.length === 0) throw new Error("Message must be non-empty for LINE sends");
409
- if (opts.replyToken) {
410
- await replyLineMessages(opts.replyToken, messages, opts, { verboseMessage: () => `line: replied to ${chatId}` });
411
- return {
412
- messageId: "reply",
413
- chatId,
414
- receipt: createLineSendReceipt({
415
- messageId: "reply",
416
- chatId,
417
- kind: resolveLineReceiptKind(messages),
418
- messageCount: messages.length
419
- })
420
- };
421
- }
422
- return pushLineMessages(chatId, messages, opts, { verboseMessage: (resolvedChatId) => `line: pushed message to ${resolvedChatId}` });
423
- }
424
- async function pushMessageLine(to, text, opts) {
425
- return sendMessageLine(to, text, {
426
- ...opts,
427
- replyToken: void 0
428
- });
429
- }
430
- async function replyMessageLine(replyToken, messages, opts) {
431
- await replyLineMessages(replyToken, messages, opts);
432
- }
433
- async function pushMessagesLine(to, messages, opts) {
434
- return pushLineMessages(to, messages, opts, { errorContext: "push message" });
435
- }
436
- function createFlexMessage(altText, contents) {
437
- return {
438
- type: "flex",
439
- altText,
440
- contents
441
- };
442
- }
443
- async function pushImageMessage(to, originalContentUrl, previewImageUrl, opts) {
444
- await validateLineMediaUrl(originalContentUrl);
445
- if (previewImageUrl) await validateLineMediaUrl(previewImageUrl);
446
- return pushLineMessages(to, [createImageMessage(originalContentUrl, previewImageUrl)], opts, { verboseMessage: (chatId) => `line: pushed image to ${chatId}` });
447
- }
448
- async function pushLocationMessage(to, location, opts) {
449
- return pushLineMessages(to, [createLocationMessage(location)], opts, { verboseMessage: (chatId) => `line: pushed location to ${chatId}` });
450
- }
451
- async function pushFlexMessage(to, altText, contents, opts) {
452
- return pushLineMessages(to, [{
453
- type: "flex",
454
- altText: truncateUtf16Safe(altText, 400),
455
- contents
456
- }], opts, {
457
- errorContext: "push flex message",
458
- verboseMessage: (chatId) => `line: pushed flex message to ${chatId}`
459
- });
460
- }
461
- async function pushTemplateMessage(to, template, opts) {
462
- return pushLineMessages(to, [template], opts, { verboseMessage: (chatId) => `line: pushed template message to ${chatId}` });
463
- }
464
- async function pushTextMessageWithQuickReplies(to, text, quickReplyLabels, opts) {
465
- return pushLineMessages(to, [createTextMessageWithQuickReplies(text, quickReplyLabels)], opts, { verboseMessage: (chatId) => `line: pushed message with quick replies to ${chatId}` });
466
- }
467
- function createQuickReplyItems(labels) {
468
- return { items: labels.slice(0, 13).map((label) => ({
469
- type: "action",
470
- action: messageAction(label, label)
471
- })) };
472
- }
473
- function createTextMessageWithQuickReplies(text, quickReplyLabels) {
474
- return {
475
- type: "text",
476
- text,
477
- quickReply: createQuickReplyItems(quickReplyLabels)
478
- };
479
- }
480
- async function showLoadingAnimation(chatId, opts) {
481
- const { client } = createLineMessagingClient(opts);
482
- try {
483
- await client.showLoadingAnimation({
484
- chatId: normalizeTarget(chatId),
485
- loadingSeconds: opts.loadingSeconds ?? 20
486
- });
487
- logVerbose(`line: showing loading animation to ${chatId}`);
488
- } catch (err) {
489
- logVerbose(`line: loading animation failed (non-fatal): ${String(err)}`);
490
- }
491
- }
492
- async function getUserProfile(userId, opts) {
493
- if (opts.useCache ?? true) {
494
- const cached = userProfileCache.get(userId);
495
- if (cached && Date.now() - cached.fetchedAt < PROFILE_CACHE_TTL_MS) return {
496
- displayName: cached.displayName,
497
- pictureUrl: cached.pictureUrl
498
- };
499
- }
500
- const { client } = createLineMessagingClient(opts);
501
- try {
502
- const profile = await client.getProfile(userId);
503
- const result = {
504
- displayName: profile.displayName,
505
- pictureUrl: profile.pictureUrl
506
- };
507
- cacheUserProfile(userId, {
508
- ...result,
509
- fetchedAt: Date.now()
510
- });
511
- return result;
512
- } catch (err) {
513
- logVerbose(`line: failed to fetch profile for ${userId}: ${String(err)}`);
514
- return null;
515
- }
516
- }
517
- async function getUserDisplayName(userId, opts) {
518
- return (await getUserProfile(userId, opts))?.displayName ?? userId;
519
- }
520
- //#endregion
521
- //#region extensions/line/src/markdown-to-line.ts
522
- const LINE_MARKDOWN_OPTIONS = {
523
- assistantTranscriptRoleHeaders: true,
524
- autolink: false,
525
- blockquotePrefix: "",
526
- headingStyle: "none",
527
- horizontalRuleText: "",
528
- linkify: false,
529
- preserveSourceBlockSpacing: true
530
- };
531
- const TRANSCRIPT_ROLE_PREFIX = "[assistant-authored transcript] ";
532
- function parseLineMarkdown(text, tableMode = "block") {
533
- return markdownToIRWithMeta(text, {
534
- ...LINE_MARKDOWN_OPTIONS,
535
- tableMode
536
- });
537
- }
538
- function toMarkdownTable(table) {
539
- return {
540
- headers: table.headers,
541
- rows: table.rows,
542
- headerCells: table.headerCells,
543
- rowCells: table.rowCells
544
- };
545
- }
546
- function codeBlockSpans(ir) {
547
- return ir.styles.filter((span) => span.style === "code_block");
548
- }
549
- function toCodeBlock(ir, span) {
550
- return {
551
- ...span.language ? { language: span.language } : {},
552
- code: ir.text.slice(span.start, span.end).trim()
553
- };
554
- }
555
- function rangesOverlap(left, right) {
556
- return left.start < right.end && right.start < left.end;
557
- }
558
- function projectPlainText(ir, omitted = []) {
559
- const insertions = [];
560
- for (const link of ir.links) {
561
- if (omitted.some((range) => rangesOverlap(range, link))) continue;
562
- const href = link.href.trim();
563
- const label = ir.text.slice(link.start, link.end).trim();
564
- const comparableHref = href.startsWith("mailto:") ? href.slice(7) : href;
565
- if (href && label && label !== href && label !== comparableHref) insertions.push({
566
- position: link.end,
567
- text: ` (${href})`
568
- });
569
- }
570
- for (const annotation of ir.annotations ?? []) if (annotation.type === "assistant_transcript_role" && !omitted.some((range) => rangesOverlap(range, annotation))) insertions.push({
571
- position: annotation.start,
572
- text: TRANSCRIPT_ROLE_PREFIX
573
- });
574
- const inlineCodeSpans = ir.styles.filter((span) => span.style === "code" && !omitted.some((range) => rangesOverlap(range, span)));
575
- for (const span of inlineCodeSpans) if (stripMarkdown(ir.text.slice(span.start, span.end), { assistantTranscriptRoleHeaders: true }).startsWith(TRANSCRIPT_ROLE_PREFIX)) insertions.push({
576
- position: span.start,
577
- text: TRANSCRIPT_ROLE_PREFIX
578
- });
579
- insertions.sort((left, right) => left.position - right.position);
580
- const underlineTags = [...ir.text.matchAll(/<\/?u>/gi)].map((match) => ({
581
- start: match.index,
582
- end: match.index + match[0].length
583
- })).filter((tag) => !inlineCodeSpans.some((span) => rangesOverlap(tag, span)) && !omitted.some((span) => rangesOverlap(tag, span)));
584
- const removed = [...omitted, ...underlineTags].toSorted((left, right) => left.start - right.start);
585
- let output = "";
586
- let cursor = 0;
587
- let insertionIndex = 0;
588
- const appendRange = (end) => {
589
- while (insertionIndex < insertions.length) {
590
- const insertion = insertions[insertionIndex];
591
- if (!insertion || insertion.position > end) break;
592
- if (insertion.position >= cursor) {
593
- output += ir.text.slice(cursor, insertion.position) + insertion.text;
594
- cursor = insertion.position;
595
- }
596
- insertionIndex += 1;
597
- }
598
- output += ir.text.slice(cursor, end);
599
- cursor = end;
600
- };
601
- for (const range of removed) {
602
- appendRange(range.start);
603
- cursor = Math.max(cursor, range.end);
604
- while (insertionIndex < insertions.length && (insertions[insertionIndex]?.position ?? cursor) < cursor) insertionIndex += 1;
605
- }
606
- appendRange(ir.text.length);
607
- return output.trim();
608
- }
609
- function sameSpanStyle(left, right) {
610
- return left.weight === right.weight && left.style === right.style && left.decoration === right.decoration;
611
- }
612
- function renderTableCell(cell, fallback) {
613
- if (!cell?.text.trim()) return {
614
- text: fallback,
615
- hasMarkup: false
616
- };
617
- const codeSpans = cell.styles.filter((span) => span.style === "code");
618
- const tags = [...cell.text.matchAll(/<\/?u>/gi)].map((match) => ({
619
- start: match.index,
620
- end: match.index + match[0].length,
621
- closing: match[0][1] === "/"
622
- })).filter((tag) => !codeSpans.some((span) => rangesOverlap(tag, span)));
623
- const boundaries = /* @__PURE__ */ new Set([0, cell.text.length]);
624
- for (const style of cell.styles) {
625
- boundaries.add(style.start);
626
- boundaries.add(style.end);
627
- }
628
- for (const link of cell.links) boundaries.add(link.end);
629
- for (const tag of tags) {
630
- boundaries.add(tag.start);
631
- boundaries.add(tag.end);
632
- }
633
- const sortedBoundaries = [...boundaries].toSorted((left, right) => left - right);
634
- const spans = [];
635
- let underlineDepth = 0;
636
- let hasMarkup = false;
637
- const appendSpan = (span) => {
638
- const previous = spans.at(-1);
639
- if (previous && sameSpanStyle(previous, span)) previous.text = `${previous.text ?? ""}${span.text ?? ""}`;
640
- else spans.push(span);
641
- };
642
- for (let index = 0; index < sortedBoundaries.length - 1; index += 1) {
643
- const start = sortedBoundaries[index];
644
- const end = sortedBoundaries[index + 1];
645
- if (start === void 0 || end === void 0) continue;
646
- const tag = tags.find((candidate) => candidate.start === start);
647
- if (tag) {
648
- underlineDepth += tag.closing ? -1 : 1;
649
- hasMarkup = true;
650
- continue;
651
- }
652
- const text = cell.text.slice(start, end);
653
- if (text) {
654
- const active = new Set(cell.styles.filter((style) => style.start <= start && style.end >= end).map((style) => style.style));
655
- const weight = active.has("bold") ? "bold" : void 0;
656
- const style = active.has("italic") ? "italic" : void 0;
657
- const decoration = active.has("strikethrough") ? "line-through" : underlineDepth > 0 ? "underline" : void 0;
658
- hasMarkup ||= weight !== void 0 || style !== void 0 || decoration !== void 0;
659
- appendSpan({
660
- type: "span",
661
- text,
662
- weight,
663
- style,
664
- decoration
665
- });
666
- }
667
- for (const link of cell.links.filter((candidate) => candidate.end === end)) {
668
- const href = link.href.trim();
669
- const label = cell.text.slice(link.start, link.end).trim();
670
- if (href && label && label !== href) {
671
- appendSpan({
672
- type: "span",
673
- text: ` (${href})`
674
- });
675
- hasMarkup = true;
676
- }
677
- }
678
- }
679
- return {
680
- text: spans.map((span) => span.text ?? "").join("").trim() || fallback,
681
- ...hasMarkup ? { contents: spans } : {},
682
- hasMarkup
683
- };
684
- }
685
- function plainTableCell(text) {
686
- return parseLineMarkdown(text, "off").ir;
687
- }
688
- /** Convert a markdown table to a LINE Flex Message bubble. */
689
- function convertTableToFlexBubble(table) {
690
- const headerCells = (table.headerCells ?? table.headers.map(plainTableCell)).map((cell) => renderTableCell(cell, "-"));
691
- const rowCells = (table.rowCells ?? table.rows.map((row) => row.map(plainTableCell))).map((row) => row.map((cell) => renderTableCell(cell, "-")));
692
- const hasInlineMarkup = headerCells.some((cell) => cell.hasMarkup) || rowCells.some((row) => row.some((cell) => cell.hasMarkup));
693
- if (table.headers.length === 2 && !hasInlineMarkup) return createReceiptCard({
694
- title: headerCells.map((cell) => cell.text).join(" / "),
695
- items: rowCells.map((row) => ({
696
- name: row[0]?.text ?? "-",
697
- value: row[1]?.text ?? "-"
698
- }))
699
- });
700
- return {
701
- type: "bubble",
702
- body: {
703
- type: "box",
704
- layout: "vertical",
705
- contents: [
706
- {
707
- type: "box",
708
- layout: "horizontal",
709
- contents: headerCells.map((cell) => ({
710
- type: "text",
711
- text: cell.text,
712
- contents: cell.contents,
713
- weight: "bold",
714
- size: "sm",
715
- color: "#333333",
716
- flex: 1,
717
- wrap: true
718
- })),
719
- paddingBottom: "sm"
720
- },
721
- {
722
- type: "separator",
723
- margin: "sm"
724
- },
725
- ...rowCells.slice(0, 10).map((row, rowIndex) => ({
726
- type: "box",
727
- layout: "horizontal",
728
- contents: table.headers.map((_, colIndex) => {
729
- const cell = row[colIndex] ?? {
730
- text: "-",
731
- hasMarkup: false
732
- };
733
- return {
734
- type: "text",
735
- text: cell.text,
736
- contents: cell.contents,
737
- size: "sm",
738
- color: "#666666",
739
- flex: 1,
740
- wrap: true
741
- };
742
- }),
743
- margin: rowIndex === 0 ? "md" : "sm"
744
- }))
745
- ],
746
- paddingAll: "lg"
747
- }
748
- };
749
- }
750
- /** Convert a code block to a LINE Flex Message bubble. */
751
- function convertCodeBlockToFlexBubble(block) {
752
- const titleText = block.language ? `Code (${block.language})` : "Code";
753
- const displayCode = block.code.length > 2e3 ? truncateUtf16Safe(block.code, 2e3) + "\n..." : block.code;
754
- return {
755
- type: "bubble",
756
- body: {
757
- type: "box",
758
- layout: "vertical",
759
- contents: [{
760
- type: "text",
761
- text: titleText,
762
- weight: "bold",
763
- size: "sm",
764
- color: "#666666"
765
- }, {
766
- type: "box",
767
- layout: "vertical",
768
- contents: [{
769
- type: "text",
770
- text: displayCode,
771
- size: "xs",
772
- color: "#333333",
773
- wrap: true
774
- }],
775
- backgroundColor: "#F5F5F5",
776
- paddingAll: "md",
777
- cornerRadius: "md",
778
- margin: "sm"
779
- }],
780
- paddingAll: "lg"
781
- }
782
- };
783
- }
784
- /** Parse once, route existing block surfaces to Flex, and project the remainder as plain text. */
785
- function processLineMessage(text) {
786
- const { ir, tables } = parseLineMarkdown(text);
787
- const codeSpans = codeBlockSpans(ir);
788
- return {
789
- text: projectPlainText(ir, codeSpans),
790
- flexMessages: [...tables.map((table) => toFlexMessage("Table", convertTableToFlexBubble(toMarkdownTable(table)))), ...codeSpans.map((span) => toFlexMessage("Code", convertCodeBlockToFlexBubble(toCodeBlock(ir, span))))]
791
- };
792
- }
793
- /** Check if text contains markdown that needs conversion. */
794
- function hasMarkdownToConvert(text) {
795
- const { ir, tables } = parseLineMarkdown(text);
796
- return tables.length > 0 || ir.styles.length > 0 || ir.links.length > 0 || /<\/?u>/i.test(ir.text) || ir.text !== text.trimEnd();
797
- }
798
- //#endregion
799
- export { createConfirmTemplate as A, sendMessageLine as C, createButtonMenu as D, buildTemplateMessageFromPayload as E, createTemplateCarousel as F, createYesNoConfirm as I, toFlexMessage as L, createImageCarouselColumn as M, createLinkMenu as N, createButtonTemplate as O, createProductCarousel as P, replyMessageLine as S, resolveLineChannelAccessToken as T, pushLocationMessage as _, stripMarkdown$1 as a, pushTemplateMessage as b, createImageMessage as c, createTextMessageWithQuickReplies as d, createVideoMessage as f, pushImageMessage as g, pushFlexMessage as h, processLineMessage as i, createImageCarousel as j, createCarouselColumn as k, createLocationMessage as l, getUserProfile as m, convertTableToFlexBubble as n, createAudioMessage as o, getUserDisplayName as p, hasMarkdownToConvert as r, createFlexMessage as s, convertCodeBlockToFlexBubble as t, createQuickReplyItems as u, pushMessageLine as v, showLoadingAnimation as w, pushTextMessageWithQuickReplies as x, pushMessagesLine as y };