@openclaw/line 2026.5.14-beta.1 → 2026.5.16-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-Bf5i9X7p.js";
2
- import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-BGjkWr_B.js";
1
+ import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-CdkYokiy.js";
2
+ import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-DAaSncHr.js";
3
3
  //#region extensions/line/src/channel.setup.ts
4
4
  const lineSetupPlugin = {
5
5
  id: "line",
@@ -1,4 +1,4 @@
1
- import { i as attachFooterText } from "./schedule-cards-BwQChTdM.js";
1
+ import { i as attachFooterText } from "./schedule-cards-D-yZMHDE.js";
2
2
  //#region extensions/line/src/flex-templates/basic-cards.ts
3
3
  /**
4
4
  * Create an info card with title, body, and optional footer
@@ -1,5 +1,5 @@
1
- import { r as createReceiptCard } from "./schedule-cards-BwQChTdM.js";
2
- import { a as createListCard, i as createInfoCard, r as createImageCard, t as createActionCard } from "./basic-cards-7DTG6E-A.js";
1
+ import { r as createReceiptCard } from "./schedule-cards-D-yZMHDE.js";
2
+ import { a as createListCard, i as createInfoCard, r as createImageCard, t as createActionCard } from "./basic-cards-BISytiSa.js";
3
3
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  //#region extensions/line/src/card-command.ts
5
5
  const CARD_USAGE = `Usage: /card <type> "title" "body" [options]
@@ -1,8 +1,6 @@
1
1
  import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-UXAvdBo_.js";
2
- import { n as lineSetupAdapter, r as hasLineCredentials, t as lineSetupWizard } from "./setup-surface-BGjkWr_B.js";
3
- import { i as resolveExactLineGroupConfigKey, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-HskUUWsn.js";
4
- import { n as parseLineDirectives, o as LineChannelConfigSchema, t as hasLineDirectives } from "./reply-payload-transform-C3Dx5kxs.js";
5
- import { n as resolveLineOutboundMedia, t as createLineSendReceipt } from "./send-receipt-zpAfxQ58.js";
2
+ import { n as lineSetupAdapter, r as hasLineCredentials, t as lineSetupWizard } from "./setup-surface-DAaSncHr.js";
3
+ import { c as resolveLineOutboundMedia, f as LineChannelConfigSchema, m as resolveExactLineGroupConfigKey, n as parseLineDirectives, o as createLineSendReceipt, s as buildLineQuickReplyFallbackText, t as hasLineDirectives, u as getLineRuntime } from "./reply-payload-transform-D2yrTDIV.js";
6
4
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
7
5
  import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
8
6
  import { createRestrictSendersChannelSecurity, resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
@@ -107,8 +105,8 @@ const lineChannelPluginCommon = {
107
105
  };
108
106
  //#endregion
109
107
  //#region extensions/line/src/gateway.ts
110
- const loadLineProbeRuntime$1 = createLazyRuntimeModule(() => import("./probe.runtime-XkNsM6LR.js"));
111
- const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-C0J3cWLH.js"));
108
+ const loadLineProbeRuntime$1 = createLazyRuntimeModule(() => import("./probe.runtime-tp_IgA31.js"));
109
+ const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-D_r2vk-6.js"));
112
110
  const lineGatewayAdapter = {
113
111
  startAccount: async (ctx) => {
114
112
  const account = ctx.account;
@@ -212,7 +210,7 @@ function resolveLineGroupRequireMention(params) {
212
210
  }
213
211
  //#endregion
214
212
  //#region extensions/line/src/outbound.ts
215
- const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-BQf7o-dT.js"));
213
+ const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-BYwkszx0.js"));
216
214
  function isLineUserTarget(target) {
217
215
  const normalized = target.trim().replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
218
216
  return /^U/i.test(normalized);
@@ -502,13 +500,13 @@ const lineMessageAdapter = defineChannelMessageAdapter({
502
500
  }
503
501
  },
504
502
  receive: {
505
- defaultAckPolicy: "after_agent_dispatch",
506
- supportedAckPolicies: ["after_receive_record", "after_agent_dispatch"]
503
+ defaultAckPolicy: "after_receive_record",
504
+ supportedAckPolicies: ["after_receive_record"]
507
505
  }
508
506
  });
509
507
  //#endregion
510
508
  //#region extensions/line/src/status.ts
511
- const loadLineProbeRuntime = createLazyRuntimeModule(() => import("./probe.runtime-XkNsM6LR.js"));
509
+ const loadLineProbeRuntime = createLazyRuntimeModule(() => import("./probe.runtime-tp_IgA31.js"));
512
510
  const collectLineStatusIssues = createDependentCredentialStatusIssueCollector({
513
511
  channel: "line",
514
512
  dependencySourceKey: "tokenSource",
@@ -533,7 +531,7 @@ const lineStatusAdapter = createComputedAccountStatusAdapter({
533
531
  });
534
532
  //#endregion
535
533
  //#region extensions/line/src/channel.ts
536
- const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-Bj6SGPEZ.js"));
534
+ const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-DDWuHIBF.js"));
537
535
  const lineSecurityAdapter = createRestrictSendersChannelSecurity({
538
536
  channelKey: "line",
539
537
  resolveDmPolicy: (account) => account.config.dmPolicy,
@@ -1,2 +1,2 @@
1
- import { t as linePlugin } from "./channel-Bf5i9X7p.js";
1
+ import { t as linePlugin } from "./channel-CdkYokiy.js";
2
2
  export { linePlugin };
@@ -0,0 +1,4 @@
1
+ import { S as pushMessageLine } from "./markdown-to-line-DsVRfm5Z.js";
2
+ import { t as monitorLineProvider } from "./monitor-ClwWeWOU.js";
3
+ import { t as probeLineBot } from "./probe-CiPafR4R.js";
4
+ export { monitorLineProvider, probeLineBot, pushMessageLine };
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ let lineCardCommandPromise = null;
4
4
  async function loadLineCardCommand(api) {
5
5
  lineCardCommandPromise ??= (async () => {
6
6
  let registered = null;
7
- const { registerLineCardCommand } = await import("./card-command-Dtj3Rx7T.js");
7
+ const { registerLineCardCommand } = await import("./card-command-B1iy-vnt.js");
8
8
  registerLineCardCommand({
9
9
  ...api,
10
10
  registerCommand(command) {
@@ -1,6 +1,6 @@
1
1
  import { i as resolveLineAccount } from "./accounts-UXAvdBo_.js";
2
- import { r as validateLineMediaUrl, t as createLineSendReceipt } from "./send-receipt-zpAfxQ58.js";
3
- import { r as createReceiptCard } from "./schedule-cards-BwQChTdM.js";
2
+ import { l as validateLineMediaUrl, o as createLineSendReceipt } from "./reply-payload-transform-D2yrTDIV.js";
3
+ import { r as createReceiptCard } from "./schedule-cards-D-yZMHDE.js";
4
4
  import { messagingApi } from "@line/bot-sdk";
5
5
  import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
6
6
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
@@ -246,6 +246,7 @@ function normalizeTarget(to) {
246
246
  if (!trimmed) throw new Error("Recipient is required for LINE sends");
247
247
  const normalized = trimmed.replace(/^line:group:/i, "").replace(/^line:room:/i, "").replace(/^line:user:/i, "").replace(/^line:/i, "");
248
248
  if (!normalized) throw new Error("Recipient is required for LINE sends");
249
+ 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): ${normalized.slice(0, 4)}…`);
249
250
  return normalized;
250
251
  }
251
252
  function isLineUserChatId(chatId) {
@@ -1,6 +1,6 @@
1
1
  import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-UXAvdBo_.js";
2
- import { a as resolveLineGroupConfigEntry, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-HskUUWsn.js";
3
- import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, f as createImageMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-CrXM4Bcw.js";
2
+ import { h as resolveLineGroupConfigEntry, s as buildLineQuickReplyFallbackText, u as getLineRuntime } from "./reply-payload-transform-D2yrTDIV.js";
3
+ import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, f as createImageMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-DsVRfm5Z.js";
4
4
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
5
5
  import { createMessageReceiveContext, hasFinalChannelTurnDispatch } from "openclaw/plugin-sdk/channel-message";
6
6
  import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
@@ -13,7 +13,7 @@ import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runt
13
13
  import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
14
14
  import { isRequestBodyLimitError, normalizePluginHttpPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
15
15
  import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1 } from "openclaw/plugin-sdk/webhook-request-guards";
16
- import { DEFAULT_GROUP_HISTORY_LIMIT, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
16
+ import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
17
17
  import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
18
18
  import { buildMentionRegexes, formatInboundEnvelope, formatLocationText, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
19
19
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
@@ -427,11 +427,10 @@ async function buildLineMessageContext(params) {
427
427
  });
428
428
  }
429
429
  const historyKey = isGroup ? peerId : void 0;
430
- const inboundHistory = historyKey && groupHistories && (historyLimit ?? 0) > 0 ? (groupHistories.get(historyKey) ?? []).map((entry) => ({
431
- sender: entry.sender,
432
- body: entry.body,
433
- timestamp: entry.timestamp
434
- })) : void 0;
430
+ const inboundHistory = historyKey && groupHistories && (historyLimit ?? 0) > 0 ? createChannelHistoryWindow({ historyMap: groupHistories }).buildInboundHistory({
431
+ historyKey,
432
+ limit: historyLimit ?? 0
433
+ }) : void 0;
435
434
  const finalized = await finalizeLineInboundContext({
436
435
  cfg,
437
436
  account,
@@ -823,8 +822,7 @@ async function handleMessageEvent(event, context) {
823
822
  logVerbose(`line: skipping group message (requireMention, not mentioned)`);
824
823
  const historyKey = groupId ?? roomId;
825
824
  const senderId = sourceInfo.userId ?? "unknown";
826
- if (historyKey && context.groupHistories) recordPendingHistoryEntryIfEnabled({
827
- historyMap: context.groupHistories,
825
+ if (historyKey && context.groupHistories) createChannelHistoryWindow({ historyMap: context.groupHistories }).record({
828
826
  historyKey,
829
827
  limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
830
828
  entry: {
@@ -863,8 +861,7 @@ async function handleMessageEvent(event, context) {
863
861
  await processMessage(messageContext);
864
862
  if (isGroup && context.groupHistories) {
865
863
  const historyKey = groupId ?? roomId;
866
- if (historyKey && context.groupHistories.has(historyKey)) clearHistoryEntriesIfEnabled({
867
- historyMap: context.groupHistories,
864
+ if (historyKey && context.groupHistories.has(historyKey)) createChannelHistoryWindow({ historyMap: context.groupHistories }).clear({
868
865
  historyKey,
869
866
  limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
870
867
  });
@@ -1072,6 +1069,9 @@ async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_
1072
1069
  timeoutMs
1073
1070
  });
1074
1071
  }
1072
+ function logLineWebhookDispatchError(runtime, err) {
1073
+ runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
1074
+ }
1075
1075
  function createLineNodeWebhookHandler(params) {
1076
1076
  const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
1077
1077
  const readBody = params.readBody ?? readLineWebhookRequestBody;
@@ -1125,19 +1125,18 @@ function createLineNodeWebhookHandler(params) {
1125
1125
  id: `${Date.now()}:line:webhook`,
1126
1126
  channel: "line",
1127
1127
  message: body,
1128
- ackPolicy: body.events?.length ? "after_agent_dispatch" : "after_receive_record",
1128
+ ackPolicy: "after_receive_record",
1129
1129
  onAck: () => {
1130
1130
  res.statusCode = 200;
1131
1131
  res.setHeader("Content-Type", "application/json");
1132
1132
  res.end(JSON.stringify({ status: "ok" }));
1133
1133
  }
1134
1134
  });
1135
+ if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
1135
1136
  if (body.events && body.events.length > 0) {
1136
1137
  logVerbose(`line: received ${body.events.length} webhook events`);
1137
- await params.bot.handleWebhook(body);
1138
+ Promise.resolve().then(() => params.bot.handleWebhook(body)).catch((err) => logLineWebhookDispatchError(params.runtime, err));
1138
1139
  }
1139
- const ackStage = body.events?.length ? "agent_dispatch" : "receive_record";
1140
- if (receiveContext.shouldAckAfter(ackStage)) await receiveContext.ack();
1141
1140
  } catch (err) {
1142
1141
  await receiveContext?.nack(err);
1143
1142
  if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
@@ -1414,13 +1413,15 @@ async function monitorLineProvider(opts) {
1414
1413
  return;
1415
1414
  }
1416
1415
  requestLifecycle.release();
1417
- if (body.events && body.events.length > 0) {
1418
- logVerbose(`line: received ${body.events.length} webhook events`);
1419
- await match.target.bot.handleWebhook(body);
1420
- }
1421
1416
  res.statusCode = 200;
1422
1417
  res.setHeader("Content-Type", "application/json");
1423
1418
  res.end(JSON.stringify({ status: "ok" }));
1419
+ if (body.events && body.events.length > 0) {
1420
+ logVerbose(`line: received ${body.events.length} webhook events`);
1421
+ Promise.resolve().then(() => match.target.bot.handleWebhook(body)).catch((err) => {
1422
+ match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
1423
+ });
1424
+ }
1424
1425
  } catch (err) {
1425
1426
  if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1426
1427
  res.statusCode = 413;
@@ -0,0 +1,2 @@
1
+ import { t as monitorLineProvider } from "./monitor-ClwWeWOU.js";
2
+ export { monitorLineProvider };
@@ -1,2 +1,2 @@
1
- import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, D as sendMessageLine, S as pushMessageLine, T as pushTextMessageWithQuickReplies, c as processLineMessage, m as createQuickReplyItems, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage } from "./markdown-to-line-CrXM4Bcw.js";
1
+ import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, D as sendMessageLine, S as pushMessageLine, T as pushTextMessageWithQuickReplies, c as processLineMessage, m as createQuickReplyItems, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage } from "./markdown-to-line-DsVRfm5Z.js";
2
2
  export { buildTemplateMessageFromPayload, createQuickReplyItems, processLineMessage, pushFlexMessage, pushLocationMessage, pushMessageLine, pushMessagesLine, pushTemplateMessage, pushTextMessageWithQuickReplies, sendMessageLine };
@@ -0,0 +1,2 @@
1
+ import { t as probeLineBot } from "./probe-CiPafR4R.js";
2
+ export { probeLineBot };
@@ -1,8 +1,51 @@
1
- import { n as createEventCard, t as createAgendaCard } from "./schedule-cards-BwQChTdM.js";
1
+ import { n as createEventCard, t as createAgendaCard } from "./schedule-cards-D-yZMHDE.js";
2
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
+ import { resolveAccountEntry } from "openclaw/plugin-sdk/account-resolution";
2
4
  import { buildChannelConfigSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
3
5
  import { requireChannelOpenAllowFrom } from "openclaw/plugin-sdk/extension-shared";
4
6
  import { z } from "zod";
7
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
8
+ import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-message";
9
+ import { resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
5
10
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
11
+ //#region extensions/line/src/group-keys.ts
12
+ function resolveLineGroupLookupIds(groupId) {
13
+ const normalized = groupId?.trim();
14
+ if (!normalized) return [];
15
+ if (normalized.startsWith("group:") || normalized.startsWith("room:")) {
16
+ const rawId = normalized.split(":").slice(1).join(":");
17
+ return rawId ? [rawId, normalized] : [normalized];
18
+ }
19
+ return [
20
+ normalized,
21
+ `group:${normalized}`,
22
+ `room:${normalized}`
23
+ ];
24
+ }
25
+ function resolveLineGroupConfigEntry(groups, params) {
26
+ if (!groups) return;
27
+ for (const candidate of resolveLineGroupLookupIds(params.groupId)) {
28
+ const hit = groups[candidate];
29
+ if (hit) return hit;
30
+ }
31
+ for (const candidate of resolveLineGroupLookupIds(params.roomId)) {
32
+ const hit = groups[candidate];
33
+ if (hit) return hit;
34
+ }
35
+ return groups["*"];
36
+ }
37
+ function resolveLineGroupsConfig(cfg, accountId) {
38
+ const lineConfig = cfg.channels?.line;
39
+ if (!lineConfig) return;
40
+ const normalizedAccountId = normalizeAccountId(accountId);
41
+ return resolveAccountEntry(lineConfig.accounts, normalizedAccountId)?.groups ?? lineConfig.groups;
42
+ }
43
+ function resolveExactLineGroupConfigKey(params) {
44
+ const groups = resolveLineGroupsConfig(params.cfg, params.accountId);
45
+ if (!groups) return;
46
+ return resolveLineGroupLookupIds(params.groupId).find((candidate) => Object.hasOwn(groups, candidate));
47
+ }
48
+ //#endregion
6
49
  //#region extensions/line/src/config-schema.ts
7
50
  const DmPolicySchema = z.enum([
8
51
  "open",
@@ -71,6 +114,88 @@ const LineConfigSchema = LineCommonConfigSchemaBase.extend({
71
114
  });
72
115
  const LineChannelConfigSchema = buildChannelConfigSchema(LineConfigSchema);
73
116
  //#endregion
117
+ //#region extensions/line/src/runtime.ts
118
+ const { setRuntime: setLineRuntime, clearRuntime: clearLineRuntime, getRuntime: getLineRuntime } = createPluginRuntimeStore({
119
+ pluginId: "line",
120
+ errorMessage: "LINE runtime not initialized - plugin not registered"
121
+ });
122
+ //#endregion
123
+ //#region extensions/line/src/outbound-media.ts
124
+ const LINE_OUTBOUND_MEDIA_SSRF_POLICY = { allowPrivateNetwork: false };
125
+ async function validateLineMediaUrl(url) {
126
+ let parsed;
127
+ try {
128
+ parsed = new URL(url);
129
+ } catch {
130
+ throw new Error(`LINE outbound media URL must be a valid URL: ${url}`);
131
+ }
132
+ if (parsed.protocol !== "https:") throw new Error(`LINE outbound media URL must use HTTPS: ${url}`);
133
+ if (url.length > 2e3) throw new Error(`LINE outbound media URL must be 2000 chars or less (got ${url.length})`);
134
+ await resolvePinnedHostnameWithPolicy(parsed.hostname, { policy: LINE_OUTBOUND_MEDIA_SSRF_POLICY });
135
+ }
136
+ function isHttpsUrl(url) {
137
+ try {
138
+ return new URL(url).protocol === "https:";
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+ function detectLineMediaKindFromUrl(url) {
144
+ try {
145
+ const pathname = normalizeLowercaseStringOrEmpty(new URL(url).pathname);
146
+ if (/\.(png|jpe?g|gif|webp|bmp|heic|heif|avif)$/i.test(pathname)) return "image";
147
+ if (/\.(mp4|mov|m4v|webm)$/i.test(pathname)) return "video";
148
+ if (/\.(mp3|m4a|aac|wav|ogg|oga)$/i.test(pathname)) return "audio";
149
+ } catch {
150
+ return;
151
+ }
152
+ }
153
+ async function resolveLineOutboundMedia(mediaUrl, opts = {}) {
154
+ const trimmedUrl = mediaUrl.trim();
155
+ if (isHttpsUrl(trimmedUrl)) {
156
+ await validateLineMediaUrl(trimmedUrl);
157
+ const previewImageUrl = opts.previewImageUrl?.trim();
158
+ if (previewImageUrl) await validateLineMediaUrl(previewImageUrl);
159
+ return {
160
+ mediaUrl: trimmedUrl,
161
+ mediaKind: opts.mediaKind ?? (typeof opts.durationMs === "number" ? "audio" : void 0) ?? (opts.trackingId?.trim() ? "video" : void 0) ?? detectLineMediaKindFromUrl(trimmedUrl) ?? "image",
162
+ ...previewImageUrl ? { previewImageUrl } : {},
163
+ ...typeof opts.durationMs === "number" ? { durationMs: opts.durationMs } : {},
164
+ ...opts.trackingId ? { trackingId: opts.trackingId } : {}
165
+ };
166
+ }
167
+ try {
168
+ if (new URL(trimmedUrl).protocol !== "https:") throw new Error(`LINE outbound media URL must use HTTPS: ${trimmedUrl}`);
169
+ } catch (e) {
170
+ if (e instanceof Error && e.message.startsWith("LINE outbound")) throw e;
171
+ }
172
+ throw new Error("LINE outbound media currently requires a public HTTPS URL");
173
+ }
174
+ //#endregion
175
+ //#region extensions/line/src/quick-reply-fallback.ts
176
+ function buildLineQuickReplyFallbackText(labels) {
177
+ const normalized = (labels ?? []).map((label) => label.trim()).filter(Boolean).slice(0, 13);
178
+ if (normalized.length === 0) return "Choose an option.";
179
+ return `Options:\n${normalized.map((label) => `- ${label}`).join("\n")}`;
180
+ }
181
+ //#endregion
182
+ //#region extensions/line/src/send-receipt.ts
183
+ function createLineSendReceipt(params) {
184
+ const messageId = params.messageId.trim();
185
+ const chatId = params.chatId.trim();
186
+ return createMessageReceiptFromOutboundResults({
187
+ results: messageId ? [{
188
+ channel: "line",
189
+ messageId,
190
+ chatId,
191
+ conversationId: chatId,
192
+ meta: { messageCount: params.messageCount ?? 1 }
193
+ }] : [],
194
+ ...chatId ? { threadId: chatId } : {},
195
+ kind: params.kind ?? "unknown"
196
+ });
197
+ }
198
+ //#endregion
74
199
  //#region extensions/line/src/flex-templates/media-control-cards.ts
75
200
  /**
76
201
  * Create a media player card for Sonos, Spotify, Apple Music, etc.
@@ -727,4 +852,4 @@ function hasLineDirectives(text) {
727
852
  return /\[\[(quick_replies|location|confirm|buttons|media_player|event|agenda|device|appletv_remote):/i.test(text);
728
853
  }
729
854
  //#endregion
730
- export { createMediaPlayerCard as a, createDeviceControlCard as i, parseLineDirectives as n, LineChannelConfigSchema as o, createAppleTvRemoteCard as r, LineConfigSchema as s, hasLineDirectives as t };
855
+ export { resolveLineGroupsConfig as _, createMediaPlayerCard as a, resolveLineOutboundMedia as c, setLineRuntime as d, LineChannelConfigSchema as f, resolveLineGroupLookupIds as g, resolveLineGroupConfigEntry as h, createDeviceControlCard as i, validateLineMediaUrl as l, resolveExactLineGroupConfigKey as m, parseLineDirectives as n, createLineSendReceipt as o, LineConfigSchema as p, createAppleTvRemoteCard as r, buildLineQuickReplyFallbackText as s, hasLineDirectives as t, getLineRuntime as u };
@@ -1,11 +1,10 @@
1
1
  import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-UXAvdBo_.js";
2
- import { a as resolveLineGroupConfigEntry, i as resolveExactLineGroupConfigKey, o as resolveLineGroupLookupIds, r as setLineRuntime, s as resolveLineGroupsConfig } from "./quick-reply-fallback-HskUUWsn.js";
3
- import { a as createMediaPlayerCard, i as createDeviceControlCard, n as parseLineDirectives, o as LineChannelConfigSchema, r as createAppleTvRemoteCard, s as LineConfigSchema, t as hasLineDirectives } from "./reply-payload-transform-C3Dx5kxs.js";
4
- import { n as createEventCard, r as createReceiptCard, t as createAgendaCard } from "./schedule-cards-BwQChTdM.js";
5
- import { a as createListCard, i as createInfoCard, n as createCarousel, o as createNotificationBubble, r as createImageCard, t as createActionCard } from "./basic-cards-7DTG6E-A.js";
6
- import { A as buildTemplateMessageFromPayload, B as createYesNoConfirm, C as pushMessagesLine, D as sendMessageLine, E as replyMessageLine, F as createImageCarousel, G as toFlexMessage, H as messageAction, I as createImageCarouselColumn, L as createLinkMenu, M as createButtonTemplate, N as createCarouselColumn, O as showLoadingAnimation, P as createConfirmTemplate, R as createProductCarousel, S as pushMessageLine, T as pushTextMessageWithQuickReplies, U as postbackAction, V as datetimePickerAction, W as uriAction, _ as getUserDisplayName, a as extractLinks, b as pushImageMessage, c as processLineMessage, d as createFlexMessage, f as createImageMessage, g as createVideoMessage, h as createTextMessageWithQuickReplies, i as extractCodeBlocks, j as createButtonMenu, k as resolveLineChannelAccessToken, l as stripMarkdown, m as createQuickReplyItems, n as convertLinksToFlexBubble, o as extractMarkdownTables, p as createLocationMessage, r as convertTableToFlexBubble, s as hasMarkdownToConvert, t as convertCodeBlockToFlexBubble, u as createAudioMessage, v as getUserProfile, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage, z as createTemplateCarousel } from "./markdown-to-line-CrXM4Bcw.js";
7
- import { a as validateLineSignature, c as normalizeAllowFrom, i as parseLineWebhookBody, n as createLineNodeWebhookHandler, o as downloadLineMedia, r as readLineWebhookRequestBody, s as firstDefined, t as monitorLineProvider } from "./monitor-kPDKy2FH.js";
8
- import { t as probeLineBot } from "./probe-Cla0gZ0b.js";
2
+ import { _ as resolveLineGroupsConfig, a as createMediaPlayerCard, d as setLineRuntime, f as LineChannelConfigSchema, g as resolveLineGroupLookupIds, h as resolveLineGroupConfigEntry, i as createDeviceControlCard, m as resolveExactLineGroupConfigKey, n as parseLineDirectives, p as LineConfigSchema, r as createAppleTvRemoteCard, t as hasLineDirectives } from "./reply-payload-transform-D2yrTDIV.js";
3
+ import { n as createEventCard, r as createReceiptCard, t as createAgendaCard } from "./schedule-cards-D-yZMHDE.js";
4
+ import { a as createListCard, i as createInfoCard, n as createCarousel, o as createNotificationBubble, r as createImageCard, t as createActionCard } from "./basic-cards-BISytiSa.js";
5
+ import { A as buildTemplateMessageFromPayload, B as createYesNoConfirm, C as pushMessagesLine, D as sendMessageLine, E as replyMessageLine, F as createImageCarousel, G as toFlexMessage, H as messageAction, I as createImageCarouselColumn, L as createLinkMenu, M as createButtonTemplate, N as createCarouselColumn, O as showLoadingAnimation, P as createConfirmTemplate, R as createProductCarousel, S as pushMessageLine, T as pushTextMessageWithQuickReplies, U as postbackAction, V as datetimePickerAction, W as uriAction, _ as getUserDisplayName, a as extractLinks, b as pushImageMessage, c as processLineMessage, d as createFlexMessage, f as createImageMessage, g as createVideoMessage, h as createTextMessageWithQuickReplies, i as extractCodeBlocks, j as createButtonMenu, k as resolveLineChannelAccessToken, l as stripMarkdown, m as createQuickReplyItems, n as convertLinksToFlexBubble, o as extractMarkdownTables, p as createLocationMessage, r as convertTableToFlexBubble, s as hasMarkdownToConvert, t as convertCodeBlockToFlexBubble, u as createAudioMessage, v as getUserProfile, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage, z as createTemplateCarousel } from "./markdown-to-line-DsVRfm5Z.js";
6
+ import { a as validateLineSignature, c as normalizeAllowFrom, i as parseLineWebhookBody, n as createLineNodeWebhookHandler, o as downloadLineMedia, r as readLineWebhookRequestBody, s as firstDefined, t as monitorLineProvider } from "./monitor-ClwWeWOU.js";
7
+ import { t as probeLineBot } from "./probe-CiPafR4R.js";
9
8
  import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
10
9
  import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
11
10
  import { createMessageReceiveContext } from "openclaw/plugin-sdk/channel-message";
@@ -27,6 +26,9 @@ function parseWebhookBody(rawBody) {
27
26
  if (!rawBody) return null;
28
27
  return parseLineWebhookBody(rawBody);
29
28
  }
29
+ function logLineWebhookDispatchError(runtime, err) {
30
+ runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
31
+ }
30
32
  function createLineWebhookMiddleware(options) {
31
33
  const { channelSecret, onEvents, runtime } = options;
32
34
  return async (req, res, _next) => {
@@ -60,17 +62,16 @@ function createLineWebhookMiddleware(options) {
60
62
  id: `${Date.now()}:line:webhook`,
61
63
  channel: "line",
62
64
  message: body,
63
- ackPolicy: body.events?.length ? "after_agent_dispatch" : "after_receive_record",
65
+ ackPolicy: "after_receive_record",
64
66
  onAck: () => {
65
67
  res.status(200).json({ status: "ok" });
66
68
  }
67
69
  });
70
+ if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
68
71
  if (body.events && body.events.length > 0) {
69
72
  logVerbose(`line: received ${body.events.length} webhook events`);
70
- await onEvents(body);
73
+ Promise.resolve().then(() => onEvents(body)).catch((err) => logLineWebhookDispatchError(runtime, err));
71
74
  }
72
- const ackStage = body.events?.length ? "agent_dispatch" : "receive_record";
73
- if (receiveContext.shouldAckAfter(ackStage)) await receiveContext.ack();
74
75
  } catch (err) {
75
76
  await receiveContext?.nack(err);
76
77
  runtime?.error?.(danger(`line webhook error: ${String(err)}`));
package/dist/setup-api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-BGjkWr_B.js";
1
+ import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-DAaSncHr.js";
2
2
  export { lineSetupAdapter, lineSetupWizard };
@@ -1,6 +1,6 @@
1
1
  import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-UXAvdBo_.js";
2
2
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
3
- import { DEFAULT_ACCOUNT_ID, createAllowFromSection, createSetupInputPresenceValidator, createStandardChannelSetupStatus, formatDocsLink, mergeAllowFromEntries, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
3
+ import { DEFAULT_ACCOUNT_ID, createAllowFromSection, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, formatDocsLink, mergeAllowFromEntries, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
4
4
  //#region extensions/line/src/account-helpers.ts
5
5
  function hasLineCredentials(account) {
6
6
  return Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim());
@@ -106,22 +106,23 @@ const lineSetupAdapter = {
106
106
  };
107
107
  //#endregion
108
108
  //#region extensions/line/src/setup-surface.ts
109
+ const t = createSetupTranslator();
109
110
  const channel = "line";
110
111
  const LINE_SETUP_HELP_LINES = [
111
- "1) Open the LINE Developers Console and create or pick a Messaging API channel",
112
- "2) Copy the channel access token and channel secret",
113
- "3) Enable Use webhook in the Messaging API settings",
114
- "4) Point the webhook at https://<gateway-host>/line/webhook",
115
- `Docs: ${formatDocsLink("/channels/line", "channels/line")}`
112
+ t("wizard.line.helpOpenConsole"),
113
+ t("wizard.line.helpCopyCredentials"),
114
+ t("wizard.line.helpEnableWebhook"),
115
+ t("wizard.line.helpWebhookUrl"),
116
+ t("wizard.channels.docs", { link: formatDocsLink("/channels/line", "channels/line") })
116
117
  ];
117
118
  const LINE_ALLOW_FROM_HELP_LINES = [
118
- "Allowlist LINE DMs by user id.",
119
- "LINE ids are case-sensitive.",
120
- "Examples:",
119
+ t("wizard.line.allowlistIntro"),
120
+ t("wizard.line.idsCaseSensitive"),
121
+ t("wizard.line.examples"),
121
122
  "- U1234567890abcdef1234567890abcdef",
122
123
  "- line:user:U1234567890abcdef1234567890abcdef",
123
- "Multiple entries: comma-separated.",
124
- `Docs: ${formatDocsLink("/channels/line", "channels/line")}`
124
+ t("wizard.line.multipleEntries"),
125
+ t("wizard.channels.docs", { link: formatDocsLink("/channels/line", "channels/line") })
125
126
  ];
126
127
  const lineDmPolicy = {
127
128
  label: "LINE",
@@ -157,10 +158,10 @@ const lineSetupWizard = {
157
158
  channel,
158
159
  status: createStandardChannelSetupStatus({
159
160
  channelLabel: "LINE",
160
- configuredLabel: "configured",
161
- unconfiguredLabel: "needs token + secret",
162
- configuredHint: "configured",
163
- unconfiguredHint: "needs token + secret",
161
+ configuredLabel: t("wizard.channels.statusConfigured"),
162
+ unconfiguredLabel: t("wizard.channels.statusNeedsTokenSecret"),
163
+ configuredHint: t("wizard.channels.statusConfigured"),
164
+ unconfiguredHint: t("wizard.channels.statusNeedsTokenSecret"),
164
165
  configuredScore: 1,
165
166
  unconfiguredScore: 0,
166
167
  includeStatusLine: true,
@@ -168,20 +169,20 @@ const lineSetupWizard = {
168
169
  resolveExtraStatusLines: ({ cfg }) => [`Accounts: ${listLineAccountIds(cfg).length || 0}`]
169
170
  }),
170
171
  introNote: {
171
- title: "LINE Messaging API",
172
+ title: t("wizard.line.messagingApiTitle"),
172
173
  lines: LINE_SETUP_HELP_LINES,
173
174
  shouldShow: ({ cfg, accountId }) => !isLineConfigured(cfg, accountId ?? resolveDefaultLineAccountId(cfg))
174
175
  },
175
176
  credentials: [{
176
177
  inputKey: "token",
177
178
  providerHint: channel,
178
- credentialLabel: "channel access token",
179
+ credentialLabel: t("wizard.line.channelAccessToken"),
179
180
  preferredEnvVar: "LINE_CHANNEL_ACCESS_TOKEN",
180
- helpTitle: "LINE Messaging API",
181
+ helpTitle: t("wizard.line.messagingApiTitle"),
181
182
  helpLines: LINE_SETUP_HELP_LINES,
182
- envPrompt: "LINE_CHANNEL_ACCESS_TOKEN detected. Use env var?",
183
- keepPrompt: "LINE channel access token already configured. Keep it?",
184
- inputPrompt: "Enter LINE channel access token",
183
+ envPrompt: t("wizard.line.tokenEnvPrompt"),
184
+ keepPrompt: t("wizard.line.tokenKeepPrompt"),
185
+ inputPrompt: t("wizard.line.tokenInputPrompt"),
185
186
  allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
186
187
  inspect: ({ cfg, accountId }) => {
187
188
  const resolved = resolveLineAccount({
@@ -212,13 +213,13 @@ const lineSetupWizard = {
212
213
  }, {
213
214
  inputKey: "password",
214
215
  providerHint: "line-secret",
215
- credentialLabel: "channel secret",
216
+ credentialLabel: t("wizard.line.channelSecret"),
216
217
  preferredEnvVar: "LINE_CHANNEL_SECRET",
217
- helpTitle: "LINE Messaging API",
218
+ helpTitle: t("wizard.line.messagingApiTitle"),
218
219
  helpLines: LINE_SETUP_HELP_LINES,
219
- envPrompt: "LINE_CHANNEL_SECRET detected. Use env var?",
220
- keepPrompt: "LINE channel secret already configured. Keep it?",
221
- inputPrompt: "Enter LINE channel secret",
220
+ envPrompt: t("wizard.line.secretEnvPrompt"),
221
+ keepPrompt: t("wizard.line.secretKeepPrompt"),
222
+ inputPrompt: t("wizard.line.secretInputPrompt"),
222
223
  allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
223
224
  inspect: ({ cfg, accountId }) => {
224
225
  const resolved = resolveLineAccount({
@@ -248,11 +249,11 @@ const lineSetupWizard = {
248
249
  })
249
250
  }],
250
251
  allowFrom: createAllowFromSection({
251
- helpTitle: "LINE allowlist",
252
+ helpTitle: t("wizard.line.allowlistTitle"),
252
253
  helpLines: LINE_ALLOW_FROM_HELP_LINES,
253
- message: "LINE allowFrom (user id)",
254
+ message: t("wizard.line.allowFromPrompt"),
254
255
  placeholder: "U1234567890abcdef1234567890abcdef",
255
- invalidWithoutCredentialNote: "LINE allowFrom requires raw user ids like U1234567890abcdef1234567890abcdef.",
256
+ invalidWithoutCredentialNote: t("wizard.line.allowFromInvalid"),
256
257
  parseInputs: splitSetupEntries,
257
258
  parseId: parseLineAllowFromId,
258
259
  apply: ({ cfg, accountId, allowFrom }) => patchLineAccountConfig({
@@ -267,12 +268,12 @@ const lineSetupWizard = {
267
268
  }),
268
269
  dmPolicy: lineDmPolicy,
269
270
  completionNote: {
270
- title: "LINE webhook",
271
+ title: t("wizard.line.webhookTitle"),
271
272
  lines: [
272
- "Enable Use webhook in the LINE console after saving credentials.",
273
- "Default webhook URL: https://<gateway-host>/line/webhook",
274
- "If you set channels.line.webhookPath, update the URL to match.",
275
- `Docs: ${formatDocsLink("/channels/line", "channels/line")}`
273
+ t("wizard.line.completionEnableWebhook"),
274
+ t("wizard.line.completionDefaultWebhook"),
275
+ t("wizard.line.completionWebhookPath"),
276
+ t("wizard.channels.docs", { link: formatDocsLink("/channels/line", "channels/line") })
276
277
  ]
277
278
  },
278
279
  disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/line",
3
- "version": "2026.5.14-beta.1",
3
+ "version": "2026.5.16-beta.1",
4
4
  "description": "OpenClaw LINE channel plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,7 @@
16
16
  "openclaw": "workspace:*"
17
17
  },
18
18
  "peerDependencies": {
19
- "openclaw": ">=2026.5.14-beta.1"
19
+ "openclaw": ">=2026.5.16-beta.1"
20
20
  },
21
21
  "peerDependenciesMeta": {
22
22
  "openclaw": {
@@ -46,10 +46,10 @@
46
46
  "minHostVersion": ">=2026.4.10"
47
47
  },
48
48
  "compat": {
49
- "pluginApi": ">=2026.5.14-beta.1"
49
+ "pluginApi": ">=2026.5.16-beta.1"
50
50
  },
51
51
  "build": {
52
- "openclawVersion": "2026.5.14-beta.1"
52
+ "openclawVersion": "2026.5.16-beta.1"
53
53
  },
54
54
  "release": {
55
55
  "publishToClawHub": true,
@@ -1,4 +0,0 @@
1
- import { S as pushMessageLine } from "./markdown-to-line-CrXM4Bcw.js";
2
- import { t as monitorLineProvider } from "./monitor-kPDKy2FH.js";
3
- import { t as probeLineBot } from "./probe-Cla0gZ0b.js";
4
- export { monitorLineProvider, probeLineBot, pushMessageLine };
@@ -1,2 +0,0 @@
1
- import { t as monitorLineProvider } from "./monitor-kPDKy2FH.js";
2
- export { monitorLineProvider };
@@ -1,2 +0,0 @@
1
- import { t as probeLineBot } from "./probe-Cla0gZ0b.js";
2
- export { probeLineBot };
@@ -1,55 +0,0 @@
1
- import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
- import { resolveAccountEntry } from "openclaw/plugin-sdk/account-resolution";
3
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
4
- //#region extensions/line/src/group-keys.ts
5
- function resolveLineGroupLookupIds(groupId) {
6
- const normalized = groupId?.trim();
7
- if (!normalized) return [];
8
- if (normalized.startsWith("group:") || normalized.startsWith("room:")) {
9
- const rawId = normalized.split(":").slice(1).join(":");
10
- return rawId ? [rawId, normalized] : [normalized];
11
- }
12
- return [
13
- normalized,
14
- `group:${normalized}`,
15
- `room:${normalized}`
16
- ];
17
- }
18
- function resolveLineGroupConfigEntry(groups, params) {
19
- if (!groups) return;
20
- for (const candidate of resolveLineGroupLookupIds(params.groupId)) {
21
- const hit = groups[candidate];
22
- if (hit) return hit;
23
- }
24
- for (const candidate of resolveLineGroupLookupIds(params.roomId)) {
25
- const hit = groups[candidate];
26
- if (hit) return hit;
27
- }
28
- return groups["*"];
29
- }
30
- function resolveLineGroupsConfig(cfg, accountId) {
31
- const lineConfig = cfg.channels?.line;
32
- if (!lineConfig) return;
33
- const normalizedAccountId = normalizeAccountId(accountId);
34
- return resolveAccountEntry(lineConfig.accounts, normalizedAccountId)?.groups ?? lineConfig.groups;
35
- }
36
- function resolveExactLineGroupConfigKey(params) {
37
- const groups = resolveLineGroupsConfig(params.cfg, params.accountId);
38
- if (!groups) return;
39
- return resolveLineGroupLookupIds(params.groupId).find((candidate) => Object.hasOwn(groups, candidate));
40
- }
41
- //#endregion
42
- //#region extensions/line/src/runtime.ts
43
- const { setRuntime: setLineRuntime, clearRuntime: clearLineRuntime, getRuntime: getLineRuntime } = createPluginRuntimeStore({
44
- pluginId: "line",
45
- errorMessage: "LINE runtime not initialized - plugin not registered"
46
- });
47
- //#endregion
48
- //#region extensions/line/src/quick-reply-fallback.ts
49
- function buildLineQuickReplyFallbackText(labels) {
50
- const normalized = (labels ?? []).map((label) => label.trim()).filter(Boolean).slice(0, 13);
51
- if (normalized.length === 0) return "Choose an option.";
52
- return `Options:\n${normalized.map((label) => `- ${label}`).join("\n")}`;
53
- }
54
- //#endregion
55
- export { resolveLineGroupConfigEntry as a, resolveExactLineGroupConfigKey as i, getLineRuntime as n, resolveLineGroupLookupIds as o, setLineRuntime as r, resolveLineGroupsConfig as s, buildLineQuickReplyFallbackText as t };
@@ -1,73 +0,0 @@
1
- import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-message";
2
- import { resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
3
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
4
- //#region extensions/line/src/outbound-media.ts
5
- const LINE_OUTBOUND_MEDIA_SSRF_POLICY = { allowPrivateNetwork: false };
6
- async function validateLineMediaUrl(url) {
7
- let parsed;
8
- try {
9
- parsed = new URL(url);
10
- } catch {
11
- throw new Error(`LINE outbound media URL must be a valid URL: ${url}`);
12
- }
13
- if (parsed.protocol !== "https:") throw new Error(`LINE outbound media URL must use HTTPS: ${url}`);
14
- if (url.length > 2e3) throw new Error(`LINE outbound media URL must be 2000 chars or less (got ${url.length})`);
15
- await resolvePinnedHostnameWithPolicy(parsed.hostname, { policy: LINE_OUTBOUND_MEDIA_SSRF_POLICY });
16
- }
17
- function isHttpsUrl(url) {
18
- try {
19
- return new URL(url).protocol === "https:";
20
- } catch {
21
- return false;
22
- }
23
- }
24
- function detectLineMediaKindFromUrl(url) {
25
- try {
26
- const pathname = normalizeLowercaseStringOrEmpty(new URL(url).pathname);
27
- if (/\.(png|jpe?g|gif|webp|bmp|heic|heif|avif)$/i.test(pathname)) return "image";
28
- if (/\.(mp4|mov|m4v|webm)$/i.test(pathname)) return "video";
29
- if (/\.(mp3|m4a|aac|wav|ogg|oga)$/i.test(pathname)) return "audio";
30
- } catch {
31
- return;
32
- }
33
- }
34
- async function resolveLineOutboundMedia(mediaUrl, opts = {}) {
35
- const trimmedUrl = mediaUrl.trim();
36
- if (isHttpsUrl(trimmedUrl)) {
37
- await validateLineMediaUrl(trimmedUrl);
38
- const previewImageUrl = opts.previewImageUrl?.trim();
39
- if (previewImageUrl) await validateLineMediaUrl(previewImageUrl);
40
- return {
41
- mediaUrl: trimmedUrl,
42
- mediaKind: opts.mediaKind ?? (typeof opts.durationMs === "number" ? "audio" : void 0) ?? (opts.trackingId?.trim() ? "video" : void 0) ?? detectLineMediaKindFromUrl(trimmedUrl) ?? "image",
43
- ...previewImageUrl ? { previewImageUrl } : {},
44
- ...typeof opts.durationMs === "number" ? { durationMs: opts.durationMs } : {},
45
- ...opts.trackingId ? { trackingId: opts.trackingId } : {}
46
- };
47
- }
48
- try {
49
- if (new URL(trimmedUrl).protocol !== "https:") throw new Error(`LINE outbound media URL must use HTTPS: ${trimmedUrl}`);
50
- } catch (e) {
51
- if (e instanceof Error && e.message.startsWith("LINE outbound")) throw e;
52
- }
53
- throw new Error("LINE outbound media currently requires a public HTTPS URL");
54
- }
55
- //#endregion
56
- //#region extensions/line/src/send-receipt.ts
57
- function createLineSendReceipt(params) {
58
- const messageId = params.messageId.trim();
59
- const chatId = params.chatId.trim();
60
- return createMessageReceiptFromOutboundResults({
61
- results: messageId ? [{
62
- channel: "line",
63
- messageId,
64
- chatId,
65
- conversationId: chatId,
66
- meta: { messageCount: params.messageCount ?? 1 }
67
- }] : [],
68
- ...chatId ? { threadId: chatId } : {},
69
- kind: params.kind ?? "unknown"
70
- });
71
- }
72
- //#endregion
73
- export { resolveLineOutboundMedia as n, validateLineMediaUrl as r, createLineSendReceipt as t };
File without changes