@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.
package/dist/api.js CHANGED
@@ -1,11 +1,10 @@
1
- import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-D4i_K4fQ.js";
2
- import { n as lineSetupAdapter, r as lineSetupContract, t as lineSetupWizard } from "./setup-surface-DRQ7n0En.js";
1
+ import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-CAdQJRw7.js";
2
+ import { r as lineSetupContract, t as lineSetupWizard } from "./setup-surface-W0Dbgu58.js";
3
3
  //#region extensions/line/src/channel.setup.ts
4
4
  const lineSetupPlugin = {
5
5
  id: "line",
6
6
  ...lineChannelPluginCommon,
7
7
  setupWizard: lineSetupWizard,
8
- setup: lineSetupAdapter,
9
8
  setupContract: lineSetupContract
10
9
  };
11
10
  //#endregion
@@ -1,6 +1,6 @@
1
- import { _ as postbackAction, d as createListCard, l as createImageCard, m as messageAction, o as createReceiptCard, s as createActionCard, u as createInfoCard, v as uriAction } from "./flex-templates-iKJsyCq-.js";
1
+ import { c as createImageCard, f as messageAction, g as uriAction, h as postbackAction, l as createInfoCard, o as createReceiptCard, s as createActionCard, u as createListCard } from "./flex-templates-BBBlNqpV.js";
2
+ import { n as createFlexMessage } from "./send-CFFr0-rZ.js";
2
3
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
3
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
4
4
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
5
5
  //#region extensions/line/src/card-command.ts
6
6
  const CARD_USAGE = `Usage: /card <type> "title" "body" [options]
@@ -21,6 +21,13 @@ Examples:
21
21
  function buildLineReply(lineData) {
22
22
  return { channelData: { line: lineData } };
23
23
  }
24
+ function buildLineFlexReply(altText, contents) {
25
+ const message = createFlexMessage(altText, contents);
26
+ return buildLineReply({ flexMessage: {
27
+ altText: message.altText,
28
+ contents: message.contents
29
+ } });
30
+ }
24
31
  /**
25
32
  * Parse action string format: "Label|data,Label2|data2"
26
33
  * Data can be a URL (uri action) or plain text (message action) or key=value (postback)
@@ -118,40 +125,28 @@ function registerLineCardCommand(api) {
118
125
  case "info": {
119
126
  const [title = "Info", body = "", footer] = args;
120
127
  const bubble = createInfoCard(title, body, footer);
121
- return buildLineReply({ flexMessage: {
122
- altText: truncateUtf16Safe(`${title}: ${body}`, 400),
123
- contents: bubble
124
- } });
128
+ return buildLineFlexReply(`${title}: ${body}`, bubble);
125
129
  }
126
130
  case "image": {
127
131
  const [title = "Image", caption = ""] = args;
128
132
  const imageUrl = flags.url || flags.image;
129
133
  if (!imageUrl) return { text: "Error: Image card requires --url <image-url>" };
130
134
  const bubble = createImageCard(imageUrl, title, caption);
131
- return buildLineReply({ flexMessage: {
132
- altText: truncateUtf16Safe(`${title}: ${caption}`, 400),
133
- contents: bubble
134
- } });
135
+ return buildLineFlexReply(`${title}: ${caption}`, bubble);
135
136
  }
136
137
  case "action": {
137
138
  const [title = "Actions", body = ""] = args;
138
139
  const actions = parseActions(flags.actions);
139
140
  if (actions.length === 0) return { text: "Error: Action card requires --actions \"Label1|data1,Label2|data2\"" };
140
141
  const bubble = createActionCard(title, body, actions, { imageUrl: flags.url || flags.image });
141
- return buildLineReply({ flexMessage: {
142
- altText: truncateUtf16Safe(`${title}: ${body}`, 400),
143
- contents: bubble
144
- } });
142
+ return buildLineFlexReply(`${title}: ${body}`, bubble);
145
143
  }
146
144
  case "list": {
147
145
  const [title = "List", itemsStr = ""] = args;
148
146
  const items = parseListItems(itemsStr || flags.items || "");
149
147
  if (items.length === 0) return { text: "Error: List card requires items. Usage: /card list \"Title\" \"Item1|Desc1,Item2|Desc2\"" };
150
148
  const bubble = createListCard(title, items);
151
- return buildLineReply({ flexMessage: {
152
- altText: truncateUtf16Safe(`${title}: ${items.map((i) => i.title).join(", ")}`, 400),
153
- contents: bubble
154
- } });
149
+ return buildLineFlexReply(`${title}: ${items.map((item) => item.title).join(", ")}`, bubble);
155
150
  }
156
151
  case "receipt": {
157
152
  const [title = "Receipt", itemsStr = ""] = args;
@@ -168,10 +163,7 @@ function registerLineCardCommand(api) {
168
163
  total,
169
164
  footer
170
165
  });
171
- return buildLineReply({ flexMessage: {
172
- altText: truncateUtf16Safe(`${title}: ${items.map((i) => `${i.name} ${i.value}`).join(", ")}`, 400),
173
- contents: bubble
174
- } });
166
+ return buildLineFlexReply(`${title}: ${items.map((item) => `${item.name} ${item.value}`).join(", ")}`, bubble);
175
167
  }
176
168
  case "confirm": {
177
169
  const [question = "Confirm?"] = args;
@@ -1,8 +1,8 @@
1
1
  import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BqEVsDoB.js";
2
- import { i as hasLineCredentials, n as lineSetupAdapter, r as lineSetupContract, t as lineSetupWizard } from "./setup-surface-DRQ7n0En.js";
2
+ import { i as hasLineCredentials, r as lineSetupContract, t as lineSetupWizard } from "./setup-surface-W0Dbgu58.js";
3
3
  import { i as resolveExactLineGroupConfigKey, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-D86FwAfr.js";
4
- import { n as parseLineDirectives, r as LineChannelConfigSchema, t as hasLineDirectives } from "./reply-payload-transform-D5-dHB8b.js";
5
- import { i as resolveLineOutboundMedia, n as buildLineMediaMessage, r as hasLineSpecificMediaOptions, t as createLineSendReceipt } from "./send-receipt-CY2ey8XH.js";
4
+ import { n as parseLineDirectives, r as LineChannelConfigSchema, t as hasLineDirectives } from "./reply-payload-transform-BOPGHVPo.js";
5
+ import { _ as createLineSendReceipt, b as resolveLineOutboundMedia, v as buildLineMediaMessage, y as hasLineSpecificMediaOptions } from "./flex-templates-BBBlNqpV.js";
6
6
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
7
7
  import { buildDmGroupAccountAllowlistAdapter, createFlatAllowlistOverrideResolver } from "openclaw/plugin-sdk/allowlist-config-edit";
8
8
  import { buildChannelOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
@@ -14,11 +14,11 @@ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1 } from "openclaw/plugin-sdk/
14
14
  import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
15
15
  import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
16
16
  import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
17
- import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
17
+ import { createAccountStatusSink, defineChannelMessageAdapter, listMessageReceiptPlatformIds } from "openclaw/plugin-sdk/channel-outbound";
18
+ import { createChannelPartialDeliveryError } from "openclaw/plugin-sdk/channel-inbound";
18
19
  import { createAttachedChannelResultAdapter, createEmptyChannelResult } from "openclaw/plugin-sdk/channel-send-result";
19
20
  import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
20
21
  import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
21
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
22
22
  import { buildTokenChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, createDependentCredentialStatusIssueCollector } from "openclaw/plugin-sdk/status-helpers";
23
23
  //#region extensions/line/src/bindings.ts
24
24
  function normalizeLineConversationId(raw) {
@@ -71,9 +71,11 @@ const lineConfigAdapter = createScopedChannelConfigAdapter({
71
71
  }),
72
72
  defaultAccountId: resolveDefaultLineAccountId,
73
73
  clearBaseFields: [
74
+ "channelAccessToken",
74
75
  "channelSecret",
75
76
  "tokenFile",
76
- "secretFile"
77
+ "secretFile",
78
+ "name"
77
79
  ],
78
80
  resolveAllowFrom: (account) => account.config.allowFrom,
79
81
  formatAllowFrom: (allowFrom) => normalizeStringEntries(allowFrom).map(normalizeLineAllowFrom)
@@ -118,14 +120,19 @@ const lineChannelPluginCommon = {
118
120
  //#endregion
119
121
  //#region extensions/line/src/gateway.ts
120
122
  const loadLineProbeRuntime$1 = createLazyRuntimeModule(() => import("./probe.runtime-D13ZtXsd.js"));
121
- const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-CZquR7jI.js"));
123
+ const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-72isajCp.js"));
122
124
  const lineGatewayAdapter = {
123
125
  startAccount: async (ctx) => {
124
126
  const account = ctx.account;
127
+ const statusSink = createAccountStatusSink({
128
+ accountId: account.accountId,
129
+ setStatus: ctx.setStatus
130
+ });
125
131
  const token = account.channelAccessToken.trim();
126
132
  const secret = account.channelSecret.trim();
127
133
  if (!token) throw new Error(`LINE webhook mode requires a non-empty channel access token for account "${account.accountId}".`);
128
134
  if (!secret) throw new Error(`LINE webhook mode requires a non-empty channel secret for account "${account.accountId}".`);
135
+ statusSink({ lifecycle: "starting" });
129
136
  let lineBotLabel = "";
130
137
  try {
131
138
  const probe = await (await loadLineProbeRuntime$1()).probeLineBot(token, 2500);
@@ -141,8 +148,10 @@ const lineGatewayAdapter = {
141
148
  accountId: account.accountId,
142
149
  config: ctx.cfg,
143
150
  runtime: ctx.runtime,
151
+ buildContext: ctx.channelRuntime?.inbound.buildContext,
144
152
  abortSignal: ctx.abortSignal,
145
- webhookPath: account.config.webhookPath
153
+ webhookPath: account.config.webhookPath,
154
+ statusSink
146
155
  });
147
156
  },
148
157
  logoutAccount: async ({ accountId, cfg }) => {
@@ -233,8 +242,7 @@ function inferLineTargetChatType(target) {
233
242
  }
234
243
  //#endregion
235
244
  //#region extensions/line/src/outbound.ts
236
- const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-BS0IhU_M.js"));
237
- const LINE_FLEX_ALT_TEXT_LIMIT = 1500;
245
+ const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-CWQMIE7N.js"));
238
246
  const lineOutboundAdapter = {
239
247
  deliveryMode: "direct",
240
248
  chunker: (text, limit) => getLineRuntime().channel.text.chunkMarkdownText(text, limit),
@@ -245,6 +253,9 @@ const lineOutboundAdapter = {
245
253
  const outboundRuntime = await loadLineOutboundRuntime();
246
254
  const lineData = payload.channelData?.line ?? {};
247
255
  const lineRuntime = runtime.channel.line;
256
+ const location = lineData.location;
257
+ const locationMessage = location ? outboundRuntime.createLocationMessage(location) : null;
258
+ const validLocation = locationMessage ? location : void 0;
248
259
  const sendText = lineRuntime?.pushMessageLine ?? outboundRuntime.pushMessageLine;
249
260
  const sendBatch = lineRuntime?.pushMessagesLine ?? outboundRuntime.pushMessagesLine;
250
261
  const sendFlex = lineRuntime?.pushFlexMessage ?? outboundRuntime.pushFlexMessage;
@@ -256,7 +267,15 @@ const lineOutboundAdapter = {
256
267
  const recordResult = async (resultPromise) => {
257
268
  const result = await resultPromise;
258
269
  lastResult = result;
259
- await onDeliveryResult?.(createEmptyChannelResult("line", { ...result }));
270
+ try {
271
+ await onDeliveryResult?.(createEmptyChannelResult("line", { ...result }));
272
+ } catch (error) {
273
+ throw createChannelPartialDeliveryError(error, {
274
+ messageIds: listMessageReceiptPlatformIds(result.receipt),
275
+ receipt: result.receipt,
276
+ visibleReplySent: true
277
+ });
278
+ }
260
279
  return result;
261
280
  };
262
281
  const quickReplies = lineData.quickReplies ?? [];
@@ -278,7 +297,11 @@ const lineOutboundAdapter = {
278
297
  flexMessages: []
279
298
  };
280
299
  const chunkLimit = runtime.channel.text.resolveTextChunkLimit?.(cfg, "line", accountId ?? void 0, { fallbackLimit: 5e3 }) ?? 5e3;
281
- const chunks = processed.text ? runtime.channel.text.chunkMarkdownText(processed.text, chunkLimit) : [];
300
+ const orderedMessages = processed.segments?.flatMap((segment) => segment.type === "flex" ? [segment.message] : runtime.channel.text.chunkMarkdownText(segment.text, chunkLimit).map((text) => ({
301
+ type: "text",
302
+ text
303
+ })));
304
+ const chunks = orderedMessages ? orderedMessages.flatMap((message) => message.type === "text" ? [message.text] : []) : processed.text ? runtime.channel.text.chunkMarkdownText(processed.text, chunkLimit) : [];
282
305
  const mediaUrls = resolveOutboundMediaUrls(payload);
283
306
  const useLineSpecificMedia = hasLineSpecificMediaOptions(lineData);
284
307
  const mediaOptions = {
@@ -331,23 +354,42 @@ const lineOutboundAdapter = {
331
354
  accountId: accountId ?? void 0
332
355
  }));
333
356
  }
334
- if (lineData.location) await recordResult(sendLocation(to, lineData.location, {
357
+ if (validLocation) await recordResult(sendLocation(to, validLocation, {
358
+ verbose: false,
359
+ cfg,
360
+ accountId: accountId ?? void 0
361
+ }));
362
+ if (!orderedMessages) for (const flexMsg of processed.flexMessages) await recordResult(sendFlex(to, flexMsg.altText, flexMsg.contents, {
335
363
  verbose: false,
336
364
  cfg,
337
365
  accountId: accountId ?? void 0
338
366
  }));
339
- for (const flexMsg of processed.flexMessages) {
340
- const flexContents = flexMsg.contents;
341
- await recordResult(sendFlex(to, flexMsg.altText, flexContents, {
342
- verbose: false,
343
- cfg,
344
- accountId: accountId ?? void 0
345
- }));
346
- }
347
367
  }
348
368
  const sendMediaAfterText = !(hasQuickReplies && chunks.length > 0);
349
369
  if (mediaUrls.length > 0 && !shouldSendQuickRepliesInline && !sendMediaAfterText) await sendMediaMessages();
350
- if (chunks.length > 0) for (const [i, chunk] of chunks.entries()) if (i === chunks.length - 1 && hasQuickReplies) await recordResult(sendQuickReplies(to, chunk, quickReplies, {
370
+ if (orderedMessages) for (const [index, message] of orderedMessages.entries()) {
371
+ const isLast = index === orderedMessages.length - 1;
372
+ if (message.type === "flex") if (isLast && quickReply) await sendMessageBatch([{
373
+ ...message,
374
+ quickReply
375
+ }]);
376
+ else await recordResult(sendFlex(to, message.altText, message.contents, {
377
+ verbose: false,
378
+ cfg,
379
+ accountId: accountId ?? void 0
380
+ }));
381
+ else if (isLast && hasQuickReplies) await recordResult(sendQuickReplies(to, message.text, quickReplies, {
382
+ verbose: false,
383
+ cfg,
384
+ accountId: accountId ?? void 0
385
+ }));
386
+ else await recordResult(sendText(to, message.text, {
387
+ verbose: false,
388
+ cfg,
389
+ accountId: accountId ?? void 0
390
+ }));
391
+ }
392
+ else if (chunks.length > 0) for (const [i, chunk] of chunks.entries()) if (i === chunks.length - 1 && hasQuickReplies) await recordResult(sendQuickReplies(to, chunk, quickReplies, {
351
393
  verbose: false,
352
394
  cfg,
353
395
  accountId: accountId ?? void 0
@@ -359,27 +401,13 @@ const lineOutboundAdapter = {
359
401
  }));
360
402
  else if (shouldSendQuickRepliesInline) {
361
403
  const quickReplyMessages = [];
362
- if (lineData.flexMessage) quickReplyMessages.push({
363
- type: "flex",
364
- altText: truncateUtf16Safe(lineData.flexMessage.altText, LINE_FLEX_ALT_TEXT_LIMIT),
365
- contents: lineData.flexMessage.contents
366
- });
404
+ if (lineData.flexMessage) quickReplyMessages.push(outboundRuntime.createFlexMessage(lineData.flexMessage.altText, lineData.flexMessage.contents));
367
405
  if (lineData.templateMessage) {
368
406
  const template = buildTemplate(lineData.templateMessage);
369
407
  if (template) quickReplyMessages.push(template);
370
408
  }
371
- if (lineData.location) quickReplyMessages.push({
372
- type: "location",
373
- title: truncateUtf16Safe(lineData.location.title, 100),
374
- address: truncateUtf16Safe(lineData.location.address, 100),
375
- latitude: lineData.location.latitude,
376
- longitude: lineData.location.longitude
377
- });
378
- for (const flexMsg of processed.flexMessages) quickReplyMessages.push({
379
- type: "flex",
380
- altText: truncateUtf16Safe(flexMsg.altText, LINE_FLEX_ALT_TEXT_LIMIT),
381
- contents: flexMsg.contents
382
- });
409
+ if (locationMessage) quickReplyMessages.push(locationMessage);
410
+ for (const flexMsg of processed.flexMessages) quickReplyMessages.push(outboundRuntime.createFlexMessage(flexMsg.altText, flexMsg.contents));
383
411
  for (const url of mediaUrls) {
384
412
  const trimmed = url?.trim();
385
413
  if (!trimmed) continue;
@@ -400,44 +428,15 @@ const lineOutboundAdapter = {
400
428
  }
401
429
  if (mediaUrls.length > 0 && !shouldSendQuickRepliesInline && sendMediaAfterText) await sendMediaMessages();
402
430
  const completedResult = lastResult;
403
- if (completedResult) return createEmptyChannelResult("line", { ...completedResult });
404
- return createEmptyChannelResult("line", {
405
- messageId: "empty",
406
- chatId: to
407
- });
431
+ if (!completedResult) throw new Error("Message must be non-empty for LINE sends");
432
+ return createEmptyChannelResult("line", { ...completedResult });
408
433
  },
409
434
  ...createAttachedChannelResultAdapter({
410
435
  channel: "line",
411
- sendText: async ({ cfg, to, text, accountId }) => {
412
- const outboundRuntime = await loadLineOutboundRuntime();
413
- const sendText = outboundRuntime.pushMessageLine;
414
- const sendFlex = outboundRuntime.pushFlexMessage;
415
- const processed = outboundRuntime.processLineMessage(text);
416
- let result;
417
- if (processed.text.trim()) result = await sendText(to, processed.text, {
418
- verbose: false,
419
- cfg,
420
- accountId: accountId ?? void 0
421
- });
422
- else result = {
423
- messageId: "processed",
424
- chatId: to,
425
- receipt: createLineSendReceipt({
426
- messageId: "processed",
427
- chatId: to,
428
- kind: "card"
429
- })
430
- };
431
- for (const flexMsg of processed.flexMessages) {
432
- const flexContents = flexMsg.contents;
433
- await sendFlex(to, flexMsg.altText, flexContents, {
434
- verbose: false,
435
- cfg,
436
- accountId: accountId ?? void 0
437
- });
438
- }
439
- return result;
440
- },
436
+ sendText: async (ctx) => await lineOutboundAdapter.sendPayload({
437
+ ...ctx,
438
+ payload: { text: ctx.text }
439
+ }),
441
440
  sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => await (await loadLineOutboundRuntime()).sendMessageLine(to, text, {
442
441
  verbose: false,
443
442
  mediaUrl,
@@ -531,7 +530,7 @@ const lineStatusAdapter = createComputedAccountStatusAdapter({
531
530
  });
532
531
  //#endregion
533
532
  //#region extensions/line/src/channel.ts
534
- const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-BdhK8Xw7.js"));
533
+ const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-CuKoy9xw.js"));
535
534
  const lineSecurityAdapter = createRestrictSendersChannelSecurity({
536
535
  channelKey: "line",
537
536
  resolveDmPolicy: (account) => account.config.dmPolicy,
@@ -612,7 +611,6 @@ const linePlugin = createChatChannelPlugin({
612
611
  }
613
612
  },
614
613
  directory: createEmptyChannelDirectoryAdapter(),
615
- setup: lineSetupAdapter,
616
614
  setupContract: lineSetupContract,
617
615
  status: lineStatusAdapter,
618
616
  gateway: lineGatewayAdapter,
@@ -1,2 +1,2 @@
1
- import { t as linePlugin } from "./channel-D4i_K4fQ.js";
1
+ import { t as linePlugin } from "./channel-CAdQJRw7.js";
2
2
  export { linePlugin };
@@ -0,0 +1,4 @@
1
+ import { t as monitorLineProvider } from "./monitor-QEh_ZqyR.js";
2
+ import { t as probeLineBot } from "./probe-CM03wo6r.js";
3
+ import { p as pushMessageLine } from "./send-CFFr0-rZ.js";
4
+ export { monitorLineProvider, probeLineBot, pushMessageLine };
@@ -1,5 +1,111 @@
1
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
1
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
2
+ import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
3
+ import { resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
2
4
  import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
5
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
6
+ //#region extensions/line/src/outbound-media.ts
7
+ const LINE_OUTBOUND_MEDIA_SSRF_POLICY = { allowPrivateNetwork: false };
8
+ async function validateLineMediaUrl(url) {
9
+ let parsed;
10
+ try {
11
+ parsed = new URL(url);
12
+ } catch {
13
+ throw new Error("LINE outbound media URL must be a valid URL");
14
+ }
15
+ if (parsed.protocol !== "https:") throw new Error("LINE outbound media URL must use HTTPS");
16
+ if (url.length > 2e3) throw new Error(`LINE outbound media URL must be 2000 chars or less (got ${url.length})`);
17
+ await resolvePinnedHostnameWithPolicy(parsed.hostname, { policy: LINE_OUTBOUND_MEDIA_SSRF_POLICY });
18
+ }
19
+ function isHttpsUrl(url) {
20
+ try {
21
+ return new URL(url).protocol === "https:";
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function detectLineMediaKindFromUrl(url) {
27
+ try {
28
+ const pathname = normalizeLowercaseStringOrEmpty(new URL(url).pathname);
29
+ if (/\.(png|jpe?g|gif|webp|bmp|heic|heif|avif)$/i.test(pathname)) return "image";
30
+ if (/\.(mp4|mov|m4v|webm)$/i.test(pathname)) return "video";
31
+ if (/\.(mp3|m4a|aac|wav|ogg|oga)$/i.test(pathname)) return "audio";
32
+ } catch {
33
+ return;
34
+ }
35
+ }
36
+ async function resolveLineOutboundMedia(mediaUrl, opts = {}) {
37
+ const trimmedUrl = mediaUrl.trim();
38
+ if (isHttpsUrl(trimmedUrl)) {
39
+ await validateLineMediaUrl(trimmedUrl);
40
+ const previewImageUrl = opts.previewImageUrl?.trim();
41
+ if (previewImageUrl) await validateLineMediaUrl(previewImageUrl);
42
+ return {
43
+ mediaUrl: trimmedUrl,
44
+ mediaKind: opts.mediaKind ?? (typeof opts.durationMs === "number" ? "audio" : void 0) ?? (opts.trackingId?.trim() ? "video" : void 0) ?? detectLineMediaKindFromUrl(trimmedUrl) ?? "image",
45
+ ...previewImageUrl ? { previewImageUrl } : {},
46
+ ...typeof opts.durationMs === "number" ? { durationMs: opts.durationMs } : {},
47
+ ...opts.trackingId ? { trackingId: opts.trackingId } : {}
48
+ };
49
+ }
50
+ let parsed;
51
+ try {
52
+ parsed = new URL(trimmedUrl);
53
+ } catch {}
54
+ if (parsed) throw new Error("LINE outbound media URL must use HTTPS");
55
+ throw new Error("LINE outbound media currently requires a public HTTPS URL");
56
+ }
57
+ function isLineUserTarget(target) {
58
+ const normalized = target.trim().replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
59
+ return /^U/i.test(normalized);
60
+ }
61
+ function hasLineSpecificMediaOptions(lineData) {
62
+ return lineData.mediaKind !== void 0 || Boolean(lineData.previewImageUrl?.trim()) || typeof lineData.durationMs === "number" || Boolean(lineData.trackingId?.trim());
63
+ }
64
+ function buildLineMediaMessageObject(resolved, opts) {
65
+ switch (resolved.mediaKind) {
66
+ case "video": {
67
+ const previewImageUrl = resolved.previewImageUrl?.trim();
68
+ if (!previewImageUrl) throw new Error("LINE video messages require previewImageUrl to reference an image URL");
69
+ return {
70
+ type: "video",
71
+ originalContentUrl: resolved.mediaUrl,
72
+ previewImageUrl,
73
+ ...opts?.allowTrackingId && resolved.trackingId ? { trackingId: resolved.trackingId } : {}
74
+ };
75
+ }
76
+ case "audio": return {
77
+ type: "audio",
78
+ originalContentUrl: resolved.mediaUrl,
79
+ duration: resolved.durationMs ?? 6e4
80
+ };
81
+ default: return {
82
+ type: "image",
83
+ originalContentUrl: resolved.mediaUrl,
84
+ previewImageUrl: resolved.previewImageUrl ?? resolved.mediaUrl
85
+ };
86
+ }
87
+ }
88
+ async function buildLineMediaMessage(mediaUrl, opts, target) {
89
+ return buildLineMediaMessageObject(await resolveLineOutboundMedia(mediaUrl, opts), { allowTrackingId: isLineUserTarget(target) });
90
+ }
91
+ //#endregion
92
+ //#region extensions/line/src/send-receipt.ts
93
+ function createLineSendReceipt(params) {
94
+ const messageIds = (params.messageIds ?? [params.messageId]).map((messageId) => messageId.trim()).filter(Boolean);
95
+ const chatId = params.chatId.trim();
96
+ return createMessageReceiptFromOutboundResults({
97
+ results: messageIds.map((messageId) => ({
98
+ channel: "line",
99
+ messageId,
100
+ chatId,
101
+ conversationId: chatId,
102
+ meta: { messageCount: params.messageCount ?? 1 }
103
+ })),
104
+ ...chatId ? { threadId: chatId } : {},
105
+ kind: params.kind ?? "unknown"
106
+ });
107
+ }
108
+ //#endregion
3
109
  //#region extensions/line/src/actions.ts
4
110
  const LINE_ACTION_LABEL_LIMIT = 20;
5
111
  const LINE_ACTION_DATA_LIMIT = 300;
@@ -546,85 +652,6 @@ function createActionCard(title, body, actions, options) {
546
652
  };
547
653
  return bubble;
548
654
  }
549
- /**
550
- * Create a carousel container from multiple bubbles
551
- * LINE allows max 12 bubbles in a carousel
552
- */
553
- function createCarousel(bubbles) {
554
- return {
555
- type: "carousel",
556
- contents: bubbles.slice(0, 12)
557
- };
558
- }
559
- /**
560
- * Create a notification bubble (for alerts, status updates)
561
- *
562
- * Editorial design: Bold status indicator with accent color,
563
- * clear typography, optional icon for context.
564
- */
565
- function createNotificationBubble(text, options) {
566
- const typeColors = {
567
- info: {
568
- accent: "#3B82F6",
569
- bg: "#EFF6FF"
570
- },
571
- success: {
572
- accent: "#06C755",
573
- bg: "#F0FDF4"
574
- },
575
- warning: {
576
- accent: "#F59E0B",
577
- bg: "#FFFBEB"
578
- },
579
- error: {
580
- accent: "#EF4444",
581
- bg: "#FEF2F2"
582
- }
583
- }[options?.type ?? "info"];
584
- const contents = [];
585
- contents.push({
586
- type: "box",
587
- layout: "vertical",
588
- contents: [],
589
- width: "4px",
590
- backgroundColor: typeColors.accent,
591
- cornerRadius: "2px"
592
- });
593
- const textContents = [];
594
- if (options?.title) textContents.push({
595
- type: "text",
596
- text: options.title,
597
- size: "md",
598
- weight: "bold",
599
- color: "#111111",
600
- wrap: true
601
- });
602
- textContents.push({
603
- type: "text",
604
- text,
605
- size: options?.title ? "sm" : "md",
606
- color: options?.title ? "#666666" : "#333333",
607
- wrap: true,
608
- margin: options?.title ? "sm" : void 0
609
- });
610
- contents.push({
611
- type: "box",
612
- layout: "vertical",
613
- contents: textContents,
614
- flex: 1,
615
- paddingStart: "lg"
616
- });
617
- return {
618
- type: "bubble",
619
- body: {
620
- type: "box",
621
- layout: "horizontal",
622
- contents,
623
- paddingAll: "xl",
624
- backgroundColor: typeColors.bg
625
- }
626
- };
627
- }
628
655
  //#endregion
629
656
  //#region extensions/line/src/flex-templates/schedule-cards.ts
630
657
  function buildTitleSubtitleHeader(params) {
@@ -1336,4 +1363,4 @@ function createDeviceControlCard(params) {
1336
1363
  return bubble;
1337
1364
  }
1338
1365
  //#endregion
1339
- export { postbackAction as _, createEventCard as a, createCarousel as c, createListCard as d, createNotificationBubble as f, normalizeLineMessageActions as g, normalizeLineAction as h, createAgendaCard as i, createImageCard as l, messageAction as m, createDeviceControlCard as n, createReceiptCard as o, datetimePickerAction as p, createMediaPlayerCard as r, createActionCard as s, createAppleTvRemoteCard as t, createInfoCard as u, uriAction as v };
1366
+ export { createLineSendReceipt as _, createEventCard as a, resolveLineOutboundMedia as b, createImageCard as c, datetimePickerAction as d, messageAction as f, uriAction as g, postbackAction as h, createAgendaCard as i, createInfoCard as l, normalizeLineMessageActions as m, createDeviceControlCard as n, createReceiptCard as o, normalizeLineAction as p, createMediaPlayerCard as r, createActionCard as s, createAppleTvRemoteCard as t, createListCard as u, buildLineMediaMessage as v, validateLineMediaUrl as x, hasLineSpecificMediaOptions as y };
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
4
4
  function createLineCardCommandLoader(api) {
5
5
  return createLazyRuntimeModule(async () => {
6
6
  let registered = null;
7
- const { registerLineCardCommand } = await import("./card-command-ty11OJNt.js");
7
+ const { registerLineCardCommand } = await import("./card-command-DALdUSnc.js");
8
8
  registerLineCardCommand({
9
9
  ...api,
10
10
  registerCommand(command) {