@kodelyth/line 2026.5.42 → 2026.6.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.
- package/klaw.plugin.json +329 -2
- package/package.json +16 -4
- package/api.ts +0 -11
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -5
- package/index.ts +0 -53
- package/runtime-api.ts +0 -179
- package/secret-contract-api.ts +0 -4
- package/setup-api.ts +0 -2
- package/setup-entry.ts +0 -9
- package/src/account-helpers.ts +0 -16
- package/src/accounts.test.ts +0 -288
- package/src/accounts.ts +0 -187
- package/src/actions.ts +0 -61
- package/src/auto-reply-delivery.test.ts +0 -253
- package/src/auto-reply-delivery.ts +0 -200
- package/src/bindings.ts +0 -65
- package/src/bot-access.ts +0 -30
- package/src/bot-handlers.test.ts +0 -1094
- package/src/bot-handlers.ts +0 -620
- package/src/bot-message-context.test.ts +0 -420
- package/src/bot-message-context.ts +0 -586
- package/src/bot.ts +0 -66
- package/src/card-command.ts +0 -347
- package/src/channel-access-token.ts +0 -14
- package/src/channel-api.ts +0 -17
- package/src/channel-setup-status.contract.test.ts +0 -70
- package/src/channel-shared.ts +0 -48
- package/src/channel.logout.test.ts +0 -145
- package/src/channel.runtime.ts +0 -3
- package/src/channel.sendPayload.test.ts +0 -659
- package/src/channel.setup.ts +0 -11
- package/src/channel.status.test.ts +0 -63
- package/src/channel.ts +0 -155
- package/src/config-adapter.ts +0 -29
- package/src/config-schema.test.ts +0 -53
- package/src/config-schema.ts +0 -81
- package/src/download.test.ts +0 -164
- package/src/download.ts +0 -34
- package/src/flex-templates/basic-cards.ts +0 -395
- package/src/flex-templates/common.ts +0 -20
- package/src/flex-templates/media-control-cards.ts +0 -555
- package/src/flex-templates/message.ts +0 -13
- package/src/flex-templates/schedule-cards.ts +0 -467
- package/src/flex-templates/types.ts +0 -22
- package/src/flex-templates.ts +0 -32
- package/src/gateway.ts +0 -129
- package/src/group-keys.test.ts +0 -123
- package/src/group-keys.ts +0 -65
- package/src/group-policy.ts +0 -22
- package/src/markdown-to-line.test.ts +0 -348
- package/src/markdown-to-line.ts +0 -416
- package/src/message-cards.test.ts +0 -204
- package/src/monitor-durable.test.ts +0 -57
- package/src/monitor-durable.ts +0 -37
- package/src/monitor.lifecycle.test.ts +0 -499
- package/src/monitor.runtime.ts +0 -1
- package/src/monitor.ts +0 -507
- package/src/outbound-media.test.ts +0 -194
- package/src/outbound-media.ts +0 -120
- package/src/outbound.runtime.ts +0 -12
- package/src/outbound.ts +0 -427
- package/src/probe.contract.test.ts +0 -9
- package/src/probe.runtime.ts +0 -1
- package/src/probe.ts +0 -34
- package/src/quick-reply-fallback.ts +0 -10
- package/src/reply-chunks.test.ts +0 -180
- package/src/reply-chunks.ts +0 -110
- package/src/reply-payload-transform.test.ts +0 -392
- package/src/reply-payload-transform.ts +0 -317
- package/src/rich-menu.test.ts +0 -315
- package/src/rich-menu.ts +0 -326
- package/src/runtime.ts +0 -32
- package/src/send-receipt.ts +0 -32
- package/src/send.test.ts +0 -453
- package/src/send.ts +0 -531
- package/src/setup-core.ts +0 -149
- package/src/setup-runtime-api.ts +0 -9
- package/src/setup-surface.test.ts +0 -481
- package/src/setup-surface.ts +0 -229
- package/src/signature.test.ts +0 -34
- package/src/signature.ts +0 -24
- package/src/status.ts +0 -37
- package/src/template-messages.ts +0 -333
- package/src/types.ts +0 -130
- package/src/webhook-node.test.ts +0 -598
- package/src/webhook-node.ts +0 -155
- package/src/webhook-utils.ts +0 -10
- package/src/webhook.ts +0 -135
- package/tsconfig.json +0 -16
package/src/markdown-to-line.ts
DELETED
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
import type { messagingApi } from "@line/bot-sdk";
|
|
2
|
-
import { stripMarkdown } from "klaw/plugin-sdk/text-chunking";
|
|
3
|
-
import { createReceiptCard, toFlexMessage, type FlexBubble } from "./flex-templates.js";
|
|
4
|
-
export { stripMarkdown } from "klaw/plugin-sdk/text-chunking";
|
|
5
|
-
|
|
6
|
-
type FlexMessage = messagingApi.FlexMessage;
|
|
7
|
-
type FlexComponent = messagingApi.FlexComponent;
|
|
8
|
-
type FlexText = messagingApi.FlexText;
|
|
9
|
-
type FlexBox = messagingApi.FlexBox;
|
|
10
|
-
|
|
11
|
-
export interface ProcessedLineMessage {
|
|
12
|
-
/** The processed text with markdown stripped */
|
|
13
|
-
text: string;
|
|
14
|
-
/** Flex messages extracted from tables/code blocks */
|
|
15
|
-
flexMessages: FlexMessage[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Regex patterns for markdown detection
|
|
20
|
-
*/
|
|
21
|
-
const MARKDOWN_TABLE_REGEX = /^\|(.+)\|[\r\n]+\|[-:\s|]+\|[\r\n]+((?:\|.+\|[\r\n]*)+)/gm;
|
|
22
|
-
const MARKDOWN_CODE_BLOCK_REGEX = /```(\w*)\n([\s\S]*?)```/g;
|
|
23
|
-
const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Detect and extract markdown tables from text
|
|
27
|
-
*/
|
|
28
|
-
export function extractMarkdownTables(text: string): {
|
|
29
|
-
tables: MarkdownTable[];
|
|
30
|
-
textWithoutTables: string;
|
|
31
|
-
} {
|
|
32
|
-
const tables: MarkdownTable[] = [];
|
|
33
|
-
let textWithoutTables = text;
|
|
34
|
-
|
|
35
|
-
// Reset regex state
|
|
36
|
-
MARKDOWN_TABLE_REGEX.lastIndex = 0;
|
|
37
|
-
|
|
38
|
-
let match: RegExpExecArray | null;
|
|
39
|
-
const matches: { fullMatch: string; table: MarkdownTable }[] = [];
|
|
40
|
-
|
|
41
|
-
while ((match = MARKDOWN_TABLE_REGEX.exec(text)) !== null) {
|
|
42
|
-
const fullMatch = match[0];
|
|
43
|
-
const headerLine = match[1];
|
|
44
|
-
const bodyLines = match[2];
|
|
45
|
-
|
|
46
|
-
const headers = parseTableRow(headerLine);
|
|
47
|
-
const rows = bodyLines
|
|
48
|
-
.trim()
|
|
49
|
-
.split(/[\r\n]+/)
|
|
50
|
-
.filter((line) => line.trim())
|
|
51
|
-
.map(parseTableRow);
|
|
52
|
-
|
|
53
|
-
if (headers.length > 0 && rows.length > 0) {
|
|
54
|
-
matches.push({
|
|
55
|
-
fullMatch,
|
|
56
|
-
table: { headers, rows },
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Remove tables from text in reverse order to preserve indices
|
|
62
|
-
for (let i = matches.length - 1; i >= 0; i--) {
|
|
63
|
-
const { fullMatch, table } = matches[i];
|
|
64
|
-
tables.unshift(table);
|
|
65
|
-
textWithoutTables = textWithoutTables.replace(fullMatch, "");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { tables, textWithoutTables };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface MarkdownTable {
|
|
72
|
-
headers: string[];
|
|
73
|
-
rows: string[][];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Parse a single table row (pipe-separated values)
|
|
78
|
-
*/
|
|
79
|
-
function parseTableRow(row: string): string[] {
|
|
80
|
-
return row
|
|
81
|
-
.split("|")
|
|
82
|
-
.map((cell) => cell.trim())
|
|
83
|
-
.filter((cell, index, arr) => {
|
|
84
|
-
// Filter out empty cells at start/end (from leading/trailing pipes)
|
|
85
|
-
if (index === 0 && cell === "") {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
if (index === arr.length - 1 && cell === "") {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
return true;
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Convert a markdown table to a LINE Flex Message bubble
|
|
97
|
-
*/
|
|
98
|
-
export function convertTableToFlexBubble(table: MarkdownTable): FlexBubble {
|
|
99
|
-
const parseCell = (
|
|
100
|
-
value: string | undefined,
|
|
101
|
-
): { text: string; bold: boolean; hasMarkup: boolean } => {
|
|
102
|
-
const raw = value?.trim() ?? "";
|
|
103
|
-
if (!raw) {
|
|
104
|
-
return { text: "-", bold: false, hasMarkup: false };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
let hasMarkup = false;
|
|
108
|
-
const stripped = raw.replace(/\*\*(.+?)\*\*/g, (_, inner) => {
|
|
109
|
-
hasMarkup = true;
|
|
110
|
-
return String(inner);
|
|
111
|
-
});
|
|
112
|
-
const text = stripped.trim() || "-";
|
|
113
|
-
const bold = /^\*\*.+\*\*$/.test(raw);
|
|
114
|
-
|
|
115
|
-
return { text, bold, hasMarkup };
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const headerCells = table.headers.map((header) => parseCell(header));
|
|
119
|
-
const rowCells = table.rows.map((row) => row.map((cell) => parseCell(cell)));
|
|
120
|
-
const hasInlineMarkup =
|
|
121
|
-
headerCells.some((cell) => cell.hasMarkup) ||
|
|
122
|
-
rowCells.some((row) => row.some((cell) => cell.hasMarkup));
|
|
123
|
-
|
|
124
|
-
// For simple 2-column tables, use receipt card format
|
|
125
|
-
if (table.headers.length === 2 && !hasInlineMarkup) {
|
|
126
|
-
const items = rowCells.map((row) => ({
|
|
127
|
-
name: row[0]?.text ?? "-",
|
|
128
|
-
value: row[1]?.text ?? "-",
|
|
129
|
-
}));
|
|
130
|
-
|
|
131
|
-
return createReceiptCard({
|
|
132
|
-
title: headerCells.map((cell) => cell.text).join(" / "),
|
|
133
|
-
items,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// For multi-column tables, create a custom layout
|
|
138
|
-
const headerRow: FlexComponent = {
|
|
139
|
-
type: "box",
|
|
140
|
-
layout: "horizontal",
|
|
141
|
-
contents: headerCells.map((cell) => ({
|
|
142
|
-
type: "text",
|
|
143
|
-
text: cell.text,
|
|
144
|
-
weight: "bold",
|
|
145
|
-
size: "sm",
|
|
146
|
-
color: "#333333",
|
|
147
|
-
flex: 1,
|
|
148
|
-
wrap: true,
|
|
149
|
-
})) as FlexText[],
|
|
150
|
-
paddingBottom: "sm",
|
|
151
|
-
} as FlexBox;
|
|
152
|
-
|
|
153
|
-
const dataRows: FlexComponent[] = rowCells.slice(0, 10).map((row, rowIndex) => {
|
|
154
|
-
const rowContents = table.headers.map((_, colIndex) => {
|
|
155
|
-
const cell = row[colIndex] ?? { text: "-", bold: false, hasMarkup: false };
|
|
156
|
-
return {
|
|
157
|
-
type: "text",
|
|
158
|
-
text: cell.text,
|
|
159
|
-
size: "sm",
|
|
160
|
-
color: "#666666",
|
|
161
|
-
flex: 1,
|
|
162
|
-
wrap: true,
|
|
163
|
-
weight: cell.bold ? "bold" : undefined,
|
|
164
|
-
};
|
|
165
|
-
}) as FlexText[];
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
type: "box",
|
|
169
|
-
layout: "horizontal",
|
|
170
|
-
contents: rowContents,
|
|
171
|
-
margin: rowIndex === 0 ? "md" : "sm",
|
|
172
|
-
} as FlexBox;
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
type: "bubble",
|
|
177
|
-
body: {
|
|
178
|
-
type: "box",
|
|
179
|
-
layout: "vertical",
|
|
180
|
-
contents: [headerRow, { type: "separator", margin: "sm" }, ...dataRows],
|
|
181
|
-
paddingAll: "lg",
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Detect and extract code blocks from text
|
|
188
|
-
*/
|
|
189
|
-
export function extractCodeBlocks(text: string): {
|
|
190
|
-
codeBlocks: CodeBlock[];
|
|
191
|
-
textWithoutCode: string;
|
|
192
|
-
} {
|
|
193
|
-
const codeBlocks: CodeBlock[] = [];
|
|
194
|
-
let textWithoutCode = text;
|
|
195
|
-
|
|
196
|
-
// Reset regex state
|
|
197
|
-
MARKDOWN_CODE_BLOCK_REGEX.lastIndex = 0;
|
|
198
|
-
|
|
199
|
-
let match: RegExpExecArray | null;
|
|
200
|
-
const matches: { fullMatch: string; block: CodeBlock }[] = [];
|
|
201
|
-
|
|
202
|
-
while ((match = MARKDOWN_CODE_BLOCK_REGEX.exec(text)) !== null) {
|
|
203
|
-
const fullMatch = match[0];
|
|
204
|
-
const language = match[1] || undefined;
|
|
205
|
-
const code = match[2];
|
|
206
|
-
|
|
207
|
-
matches.push({
|
|
208
|
-
fullMatch,
|
|
209
|
-
block: { language, code: code.trim() },
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Remove code blocks in reverse order
|
|
214
|
-
for (let i = matches.length - 1; i >= 0; i--) {
|
|
215
|
-
const { fullMatch, block } = matches[i];
|
|
216
|
-
codeBlocks.unshift(block);
|
|
217
|
-
textWithoutCode = textWithoutCode.replace(fullMatch, "");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return { codeBlocks, textWithoutCode };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export interface CodeBlock {
|
|
224
|
-
language?: string;
|
|
225
|
-
code: string;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Convert a code block to a LINE Flex Message bubble
|
|
230
|
-
*/
|
|
231
|
-
export function convertCodeBlockToFlexBubble(block: CodeBlock): FlexBubble {
|
|
232
|
-
const titleText = block.language ? `Code (${block.language})` : "Code";
|
|
233
|
-
|
|
234
|
-
// Truncate very long code to fit LINE's limits
|
|
235
|
-
const displayCode = block.code.length > 2000 ? block.code.slice(0, 2000) + "\n..." : block.code;
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
type: "bubble",
|
|
239
|
-
body: {
|
|
240
|
-
type: "box",
|
|
241
|
-
layout: "vertical",
|
|
242
|
-
contents: [
|
|
243
|
-
{
|
|
244
|
-
type: "text",
|
|
245
|
-
text: titleText,
|
|
246
|
-
weight: "bold",
|
|
247
|
-
size: "sm",
|
|
248
|
-
color: "#666666",
|
|
249
|
-
} as FlexText,
|
|
250
|
-
{
|
|
251
|
-
type: "box",
|
|
252
|
-
layout: "vertical",
|
|
253
|
-
contents: [
|
|
254
|
-
{
|
|
255
|
-
type: "text",
|
|
256
|
-
text: displayCode,
|
|
257
|
-
size: "xs",
|
|
258
|
-
color: "#333333",
|
|
259
|
-
wrap: true,
|
|
260
|
-
} as FlexText,
|
|
261
|
-
],
|
|
262
|
-
backgroundColor: "#F5F5F5",
|
|
263
|
-
paddingAll: "md",
|
|
264
|
-
cornerRadius: "md",
|
|
265
|
-
margin: "sm",
|
|
266
|
-
} as FlexBox,
|
|
267
|
-
],
|
|
268
|
-
paddingAll: "lg",
|
|
269
|
-
},
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Extract markdown links from text
|
|
275
|
-
*/
|
|
276
|
-
export function extractLinks(text: string): { links: MarkdownLink[]; textWithLinks: string } {
|
|
277
|
-
const links: MarkdownLink[] = [];
|
|
278
|
-
|
|
279
|
-
// Reset regex state
|
|
280
|
-
MARKDOWN_LINK_REGEX.lastIndex = 0;
|
|
281
|
-
|
|
282
|
-
let match: RegExpExecArray | null;
|
|
283
|
-
while ((match = MARKDOWN_LINK_REGEX.exec(text)) !== null) {
|
|
284
|
-
links.push({
|
|
285
|
-
text: match[1],
|
|
286
|
-
url: match[2],
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Replace markdown links with just the text (for plain text output)
|
|
291
|
-
const textWithLinks = text.replace(MARKDOWN_LINK_REGEX, "$1");
|
|
292
|
-
|
|
293
|
-
return { links, textWithLinks };
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export interface MarkdownLink {
|
|
297
|
-
text: string;
|
|
298
|
-
url: string;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Create a Flex Message with tappable link buttons
|
|
303
|
-
*/
|
|
304
|
-
export function convertLinksToFlexBubble(links: MarkdownLink[]): FlexBubble {
|
|
305
|
-
const buttons: FlexComponent[] = links.slice(0, 4).map((link, index) => ({
|
|
306
|
-
type: "button",
|
|
307
|
-
action: {
|
|
308
|
-
type: "uri",
|
|
309
|
-
label: link.text.slice(0, 20), // LINE button label limit
|
|
310
|
-
uri: link.url,
|
|
311
|
-
},
|
|
312
|
-
style: index === 0 ? "primary" : "secondary",
|
|
313
|
-
margin: index > 0 ? "sm" : undefined,
|
|
314
|
-
}));
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
type: "bubble",
|
|
318
|
-
body: {
|
|
319
|
-
type: "box",
|
|
320
|
-
layout: "vertical",
|
|
321
|
-
contents: [
|
|
322
|
-
{
|
|
323
|
-
type: "text",
|
|
324
|
-
text: "Links",
|
|
325
|
-
weight: "bold",
|
|
326
|
-
size: "md",
|
|
327
|
-
color: "#333333",
|
|
328
|
-
} as FlexText,
|
|
329
|
-
],
|
|
330
|
-
paddingAll: "lg",
|
|
331
|
-
paddingBottom: "sm",
|
|
332
|
-
},
|
|
333
|
-
footer: {
|
|
334
|
-
type: "box",
|
|
335
|
-
layout: "vertical",
|
|
336
|
-
contents: buttons,
|
|
337
|
-
paddingAll: "md",
|
|
338
|
-
},
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Main function: Process text for LINE output
|
|
344
|
-
* - Extracts tables → Flex Messages
|
|
345
|
-
* - Extracts code blocks → Flex Messages
|
|
346
|
-
* - Strips remaining markdown
|
|
347
|
-
* - Returns processed text + Flex Messages
|
|
348
|
-
*/
|
|
349
|
-
export function processLineMessage(text: string): ProcessedLineMessage {
|
|
350
|
-
const flexMessages: FlexMessage[] = [];
|
|
351
|
-
let processedText = text;
|
|
352
|
-
|
|
353
|
-
// 1. Extract and convert tables
|
|
354
|
-
const { tables, textWithoutTables } = extractMarkdownTables(processedText);
|
|
355
|
-
processedText = textWithoutTables;
|
|
356
|
-
|
|
357
|
-
for (const table of tables) {
|
|
358
|
-
const bubble = convertTableToFlexBubble(table);
|
|
359
|
-
flexMessages.push(toFlexMessage("Table", bubble));
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// 2. Extract and convert code blocks
|
|
363
|
-
const { codeBlocks, textWithoutCode } = extractCodeBlocks(processedText);
|
|
364
|
-
processedText = textWithoutCode;
|
|
365
|
-
|
|
366
|
-
for (const block of codeBlocks) {
|
|
367
|
-
const bubble = convertCodeBlockToFlexBubble(block);
|
|
368
|
-
flexMessages.push(toFlexMessage("Code", bubble));
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// 3. Handle links - convert [text](url) to plain text for display
|
|
372
|
-
// (We could also create link buttons, but that can get noisy)
|
|
373
|
-
const { textWithLinks } = extractLinks(processedText);
|
|
374
|
-
processedText = textWithLinks;
|
|
375
|
-
|
|
376
|
-
// 4. Strip remaining markdown formatting
|
|
377
|
-
processedText = stripMarkdown(processedText);
|
|
378
|
-
|
|
379
|
-
return {
|
|
380
|
-
text: processedText,
|
|
381
|
-
flexMessages,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Check if text contains markdown that needs conversion
|
|
387
|
-
*/
|
|
388
|
-
export function hasMarkdownToConvert(text: string): boolean {
|
|
389
|
-
// Check for tables
|
|
390
|
-
MARKDOWN_TABLE_REGEX.lastIndex = 0;
|
|
391
|
-
if (MARKDOWN_TABLE_REGEX.test(text)) {
|
|
392
|
-
return true;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Check for code blocks
|
|
396
|
-
MARKDOWN_CODE_BLOCK_REGEX.lastIndex = 0;
|
|
397
|
-
if (MARKDOWN_CODE_BLOCK_REGEX.test(text)) {
|
|
398
|
-
return true;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Check for other markdown patterns
|
|
402
|
-
if (/\*\*[^*]+\*\*/.test(text)) {
|
|
403
|
-
return true;
|
|
404
|
-
} // bold
|
|
405
|
-
if (/~~[^~]+~~/.test(text)) {
|
|
406
|
-
return true;
|
|
407
|
-
} // strikethrough
|
|
408
|
-
if (/^#{1,6}\s+/m.test(text)) {
|
|
409
|
-
return true;
|
|
410
|
-
} // headers
|
|
411
|
-
if (/^>\s+/m.test(text)) {
|
|
412
|
-
return true;
|
|
413
|
-
} // blockquotes
|
|
414
|
-
|
|
415
|
-
return false;
|
|
416
|
-
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
createActionCard,
|
|
4
|
-
createCarousel,
|
|
5
|
-
createDeviceControlCard,
|
|
6
|
-
createEventCard,
|
|
7
|
-
createImageCard,
|
|
8
|
-
createInfoCard,
|
|
9
|
-
createListCard,
|
|
10
|
-
} from "./flex-templates.js";
|
|
11
|
-
import {
|
|
12
|
-
createConfirmTemplate,
|
|
13
|
-
createButtonTemplate,
|
|
14
|
-
createTemplateCarousel,
|
|
15
|
-
createCarouselColumn,
|
|
16
|
-
createImageCarousel,
|
|
17
|
-
createImageCarouselColumn,
|
|
18
|
-
createProductCarousel,
|
|
19
|
-
messageAction,
|
|
20
|
-
} from "./template-messages.js";
|
|
21
|
-
|
|
22
|
-
describe("createConfirmTemplate", () => {
|
|
23
|
-
it("truncates text to 240 characters", () => {
|
|
24
|
-
const longText = "x".repeat(300);
|
|
25
|
-
const template = createConfirmTemplate(longText, messageAction("Yes"), messageAction("No"));
|
|
26
|
-
|
|
27
|
-
expect((template.template as { text: string }).text.length).toBe(240);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("createButtonTemplate", () => {
|
|
32
|
-
it("limits actions to 4", () => {
|
|
33
|
-
const actions = Array.from({ length: 6 }, (_, i) => messageAction(`Button ${i}`));
|
|
34
|
-
const template = createButtonTemplate("Title", "Text", actions);
|
|
35
|
-
|
|
36
|
-
expect((template.template as { actions: unknown[] }).actions.length).toBe(4);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("truncates title to 40 characters", () => {
|
|
40
|
-
const longTitle = "x".repeat(50);
|
|
41
|
-
const template = createButtonTemplate(longTitle, "Text", [messageAction("OK")]);
|
|
42
|
-
|
|
43
|
-
expect((template.template as { title: string }).title.length).toBe(40);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("truncates text to 60 chars when no thumbnail is provided", () => {
|
|
47
|
-
const longText = "x".repeat(100);
|
|
48
|
-
const template = createButtonTemplate("Title", longText, [messageAction("OK")]);
|
|
49
|
-
|
|
50
|
-
expect((template.template as { text: string }).text.length).toBe(60);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("keeps longer text when thumbnail is provided", () => {
|
|
54
|
-
const longText = "x".repeat(100);
|
|
55
|
-
const template = createButtonTemplate("Title", longText, [messageAction("OK")], {
|
|
56
|
-
thumbnailImageUrl: "https://example.com/thumb.jpg",
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect((template.template as { text: string }).text.length).toBe(100);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("createCarouselColumn", () => {
|
|
64
|
-
it("limits actions to 3", () => {
|
|
65
|
-
const column = createCarouselColumn({
|
|
66
|
-
text: "Text",
|
|
67
|
-
actions: [
|
|
68
|
-
messageAction("A1"),
|
|
69
|
-
messageAction("A2"),
|
|
70
|
-
messageAction("A3"),
|
|
71
|
-
messageAction("A4"),
|
|
72
|
-
messageAction("A5"),
|
|
73
|
-
],
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
expect(column.actions.length).toBe(3);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("truncates text to 120 characters", () => {
|
|
80
|
-
const longText = "x".repeat(150);
|
|
81
|
-
const column = createCarouselColumn({ text: longText, actions: [messageAction("OK")] });
|
|
82
|
-
|
|
83
|
-
expect(column.text.length).toBe(120);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("carousel column limits", () => {
|
|
88
|
-
it.each([
|
|
89
|
-
{
|
|
90
|
-
createTemplate: () =>
|
|
91
|
-
createTemplateCarousel(
|
|
92
|
-
Array.from({ length: 15 }, () =>
|
|
93
|
-
createCarouselColumn({ text: "Text", actions: [messageAction("OK")] }),
|
|
94
|
-
),
|
|
95
|
-
),
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
createTemplate: () =>
|
|
99
|
-
createImageCarousel(
|
|
100
|
-
Array.from({ length: 15 }, (_, i) =>
|
|
101
|
-
createImageCarouselColumn(`https://example.com/${i}.jpg`, messageAction("View")),
|
|
102
|
-
),
|
|
103
|
-
),
|
|
104
|
-
},
|
|
105
|
-
])("limits columns to 10", ({ createTemplate }) => {
|
|
106
|
-
const template = createTemplate();
|
|
107
|
-
expect((template.template as { columns: unknown[] }).columns.length).toBe(10);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("createProductCarousel", () => {
|
|
112
|
-
it.each([
|
|
113
|
-
{
|
|
114
|
-
title: "Product",
|
|
115
|
-
description: "Desc",
|
|
116
|
-
actionLabel: "Buy",
|
|
117
|
-
actionUrl: "https://shop.com/buy",
|
|
118
|
-
expectedType: "uri",
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
title: "Product",
|
|
122
|
-
description: "Desc",
|
|
123
|
-
actionLabel: "Select",
|
|
124
|
-
actionData: "product_id=123",
|
|
125
|
-
expectedType: "postback",
|
|
126
|
-
},
|
|
127
|
-
])("uses expected action type for product action", ({ expectedType, ...item }) => {
|
|
128
|
-
const template = createProductCarousel([item]);
|
|
129
|
-
const columns = (template.template as { columns: Array<{ actions: Array<{ type: string }> }> })
|
|
130
|
-
.columns;
|
|
131
|
-
expect(columns[0].actions[0].type).toBe(expectedType);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe("flex cards", () => {
|
|
136
|
-
it("includes footer when provided", () => {
|
|
137
|
-
const card = createInfoCard("Title", "Body", "Footer text");
|
|
138
|
-
|
|
139
|
-
const footer = card.footer as { contents: Array<{ text: string }> };
|
|
140
|
-
expect(footer.contents[0].text).toBe("Footer text");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("limits list items to 8", () => {
|
|
144
|
-
const items = Array.from({ length: 15 }, (_, i) => ({ title: `Item ${i}` }));
|
|
145
|
-
const card = createListCard("List", items);
|
|
146
|
-
|
|
147
|
-
const body = card.body as { contents: Array<{ type: string; contents?: unknown[] }> };
|
|
148
|
-
const listBox = body.contents[2] as { contents: unknown[] };
|
|
149
|
-
expect(listBox.contents.length).toBe(8);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("includes image-card body text when provided", () => {
|
|
153
|
-
const card = createImageCard("https://example.com/img.jpg", "Title", "Body text");
|
|
154
|
-
|
|
155
|
-
const body = card.body as { contents: Array<{ text: string }> };
|
|
156
|
-
expect(body.contents.length).toBe(2);
|
|
157
|
-
expect(body.contents[1].text).toBe("Body text");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("limits action-card actions to 4", () => {
|
|
161
|
-
const actions = Array.from({ length: 6 }, (_, i) => ({
|
|
162
|
-
label: `Action ${i}`,
|
|
163
|
-
action: { type: "message" as const, label: `A${i}`, text: `action${i}` },
|
|
164
|
-
}));
|
|
165
|
-
const card = createActionCard("Title", "Body", actions);
|
|
166
|
-
|
|
167
|
-
const footer = card.footer as { contents: unknown[] };
|
|
168
|
-
expect(footer.contents.length).toBe(4);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("limits carousels to 12 bubbles", () => {
|
|
172
|
-
const bubbles = Array.from({ length: 15 }, (_, i) => createInfoCard(`Card ${i}`, `Body ${i}`));
|
|
173
|
-
const carousel = createCarousel(bubbles);
|
|
174
|
-
|
|
175
|
-
expect(carousel.contents.length).toBe(12);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("limits device controls to 6", () => {
|
|
179
|
-
const card = createDeviceControlCard({
|
|
180
|
-
deviceName: "Device",
|
|
181
|
-
controls: Array.from({ length: 10 }, (_, i) => ({
|
|
182
|
-
label: `Control ${i}`,
|
|
183
|
-
data: `action=${i}`,
|
|
184
|
-
})),
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const footer = card.footer as { contents: unknown[] };
|
|
188
|
-
expect(footer.contents.length).toBeLessThanOrEqual(3);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("keeps event-card optional fields together", () => {
|
|
192
|
-
const card = createEventCard({
|
|
193
|
-
title: "Team Offsite",
|
|
194
|
-
date: "February 15, 2026",
|
|
195
|
-
time: "9:00 AM - 5:00 PM",
|
|
196
|
-
location: "Mountain View Office",
|
|
197
|
-
description: "Annual team building event",
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
expect(card.size).toBe("mega");
|
|
201
|
-
const body = card.body as { contents: Array<{ type: string }> };
|
|
202
|
-
expect(body.contents).toHaveLength(3);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { resolveLineDurableReplyOptions } from "./monitor-durable.js";
|
|
3
|
-
|
|
4
|
-
describe("resolveLineDurableReplyOptions", () => {
|
|
5
|
-
it("enables durable final delivery for push-only text replies", () => {
|
|
6
|
-
expect(
|
|
7
|
-
resolveLineDurableReplyOptions({
|
|
8
|
-
payload: { text: "hello" },
|
|
9
|
-
infoKind: "final",
|
|
10
|
-
to: "U123",
|
|
11
|
-
replyToken: "reply-token",
|
|
12
|
-
replyTokenUsed: true,
|
|
13
|
-
}),
|
|
14
|
-
).toEqual({
|
|
15
|
-
to: "U123",
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("keeps unused reply-token delivery on the legacy path", () => {
|
|
20
|
-
expect(
|
|
21
|
-
resolveLineDurableReplyOptions({
|
|
22
|
-
payload: { text: "hello" },
|
|
23
|
-
infoKind: "final",
|
|
24
|
-
to: "U123",
|
|
25
|
-
replyToken: "reply-token",
|
|
26
|
-
replyTokenUsed: false,
|
|
27
|
-
}),
|
|
28
|
-
).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("keeps rich, media, and non-final replies on the legacy path", () => {
|
|
32
|
-
expect(
|
|
33
|
-
resolveLineDurableReplyOptions({
|
|
34
|
-
payload: { text: "hello", channelData: { line: { quickReplies: ["One"] } } },
|
|
35
|
-
infoKind: "final",
|
|
36
|
-
to: "U123",
|
|
37
|
-
replyTokenUsed: true,
|
|
38
|
-
}),
|
|
39
|
-
).toBe(false);
|
|
40
|
-
expect(
|
|
41
|
-
resolveLineDurableReplyOptions({
|
|
42
|
-
payload: { text: "photo", mediaUrl: "https://example.com/image.png" },
|
|
43
|
-
infoKind: "final",
|
|
44
|
-
to: "U123",
|
|
45
|
-
replyTokenUsed: true,
|
|
46
|
-
}),
|
|
47
|
-
).toBe(false);
|
|
48
|
-
expect(
|
|
49
|
-
resolveLineDurableReplyOptions({
|
|
50
|
-
payload: { text: "hello" },
|
|
51
|
-
infoKind: "block",
|
|
52
|
-
to: "U123",
|
|
53
|
-
replyTokenUsed: true,
|
|
54
|
-
}),
|
|
55
|
-
).toBe(false);
|
|
56
|
-
});
|
|
57
|
-
});
|
package/src/monitor-durable.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { resolveSendableOutboundReplyParts } from "klaw/plugin-sdk/reply-payload";
|
|
2
|
-
import type { ReplyPayload } from "klaw/plugin-sdk/reply-runtime";
|
|
3
|
-
import type { LineChannelData } from "./types.js";
|
|
4
|
-
|
|
5
|
-
export type LineDurableReplyOptions = {
|
|
6
|
-
to: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
function hasLineChannelData(payload: ReplyPayload): boolean {
|
|
10
|
-
const lineData = payload.channelData?.line as LineChannelData | undefined;
|
|
11
|
-
return Boolean(lineData && Object.keys(lineData).length > 0);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function resolveLineDurableReplyOptions(params: {
|
|
15
|
-
payload: ReplyPayload;
|
|
16
|
-
infoKind: string;
|
|
17
|
-
to: string;
|
|
18
|
-
replyToken?: string | null;
|
|
19
|
-
replyTokenUsed: boolean;
|
|
20
|
-
}): LineDurableReplyOptions | false {
|
|
21
|
-
if (params.infoKind !== "final") {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
if (params.replyToken && !params.replyTokenUsed) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
if (hasLineChannelData(params.payload)) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
const reply = resolveSendableOutboundReplyParts(params.payload);
|
|
31
|
-
if (reply.hasMedia || !reply.hasText) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
to: params.to,
|
|
36
|
-
};
|
|
37
|
-
}
|