@openclaw/line 2026.9.1-beta.1 → 2026.9.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.
Files changed (46) hide show
  1. package/dist/account-helpers-BSUyZwLm.js +12 -0
  2. package/dist/{accounts-BqEVsDoB.js → accounts-BBEFMYbY.js} +2 -1
  3. package/dist/api.js +2 -2
  4. package/dist/card-command-pwvE48Wb.js +252 -0
  5. package/dist/{channel-CsQLcrsA.js → channel-BJvELOIg.js} +111 -380
  6. package/dist/channel-plugin-api.js +1 -1
  7. package/dist/channel.runtime-CZOszXXj.js +4 -0
  8. package/dist/contract-api.js +1 -1
  9. package/dist/doctor-contract-api.js +141 -0
  10. package/dist/index.js +5 -17
  11. package/dist/{markdown-to-line-BmZ4hhfC.js → markdown-to-line-B-sj-7II.js} +22 -23
  12. package/dist/{monitor-Bd9XDV1i.js → monitor-m9h5osFz.js} +339 -237
  13. package/dist/monitor.runtime-C90LL-P3.js +2 -0
  14. package/dist/outbound.runtime-CGqyJ-Ug.js +3 -0
  15. package/dist/probe.runtime-CbtMgU4s.js +2 -0
  16. package/dist/rich-messages-DY1AGKAJ.js +742 -0
  17. package/dist/runtime-api.js +14 -15
  18. package/dist/{send-Dce0m539.js → send-CQQxpxPp.js} +142 -112
  19. package/dist/{send-receipt-CCPTalf9.js → send-retry-ChpfJ-LM.js} +326 -86
  20. package/dist/setup-api.js +1 -1
  21. package/dist/{setup-surface-W0Dbgu58.js → setup-surface-CDlgkTKi.js} +47 -39
  22. package/dist/webhook-spool-contract-1rY8OJw8.js +59 -0
  23. package/node_modules/typebox/build/format/email.mjs +1 -1
  24. package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
  25. package/node_modules/typebox/build/format/json_pointer.d.mts +2 -2
  26. package/node_modules/typebox/build/format/json_pointer.mjs +2 -2
  27. package/node_modules/typebox/build/format/json_pointer_uri_fragment.d.mts +2 -2
  28. package/node_modules/typebox/build/format/json_pointer_uri_fragment.mjs +2 -2
  29. package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
  30. package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
  31. package/node_modules/typebox/build/system/memory/assign.mjs +2 -1
  32. package/node_modules/typebox/build/system/memory/create.mjs +3 -4
  33. package/node_modules/typebox/build/system/memory/discard.mjs +2 -2
  34. package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
  35. package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
  36. package/node_modules/typebox/build/system/memory/update.mjs +3 -2
  37. package/node_modules/typebox/package.json +1 -1
  38. package/openclaw.plugin.json +16 -3
  39. package/package.json +5 -5
  40. package/dist/card-command-CmU47gD9.js +0 -230
  41. package/dist/channel.runtime-B83x7k_i.js +0 -4
  42. package/dist/media-control-cards-CksNB8Wk.js +0 -474
  43. package/dist/monitor.runtime-BCCa6qTX.js +0 -2
  44. package/dist/outbound.runtime-D4w0-Gkc.js +0 -3
  45. package/dist/probe-CM03wo6r.js +0 -28
  46. package/dist/probe.runtime-D13ZtXsd.js +0 -2
@@ -1,22 +1,22 @@
1
- import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BqEVsDoB.js";
2
- import { a as resolveExactLineGroupConfigKey, c as resolveLineGroupsConfig, d as LineChannelConfigSchema, f as LineConfigSchema, n as createDeviceControlCard, o as resolveLineGroupConfigEntry, r as createMediaPlayerCard, s as resolveLineGroupLookupIds, t as createAppleTvRemoteCard, u as setLineRuntime } from "./media-control-cards-CksNB8Wk.js";
3
- import { a as createActionCard, c as createListCard, h as uriAction, i as createReceiptCard, l as datetimePickerAction, n as createAgendaCard, o as createImageCard, p as postbackAction, r as createEventCard, s as createInfoCard, u as messageAction } from "./send-receipt-CCPTalf9.js";
4
- 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-Bd9XDV1i.js";
5
- import { t as probeLineBot } from "./probe-CM03wo6r.js";
6
- import { a as stripMarkdown, c as createButtonTemplate, d as createTemplateCarousel, i as processLineMessage, l as createCarouselColumn, n as convertTableToFlexBubble, o as toFlexMessage, r as hasMarkdownToConvert, s as buildTemplateMessageFromPayload, t as convertCodeBlockToFlexBubble, u as createConfirmTemplate } from "./markdown-to-line-BmZ4hhfC.js";
7
- import { _ as replyMessageLine, a as createQuickReplyItems, b as resolveLineChannelAccessToken, c as getUserDisplayName, d as pushImageMessage, f as pushLocationMessage, g as pushTextMessageWithQuickReplies, h as pushTemplateMessage, i as createLocationMessage, l as getUserProfile, m as pushMessagesLine, n as createFlexMessage, o as createTextMessageWithQuickReplies, p as pushMessageLine, r as createImageMessage, s as createVideoMessage, t as createAudioMessage, u as pushFlexMessage, v as sendMessageLine, y as showLoadingAnimation } from "./send-Dce0m539.js";
1
+ import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BBEFMYbY.js";
2
+ import { _ as LineChannelConfigSchema, c as createDeviceControlCard, d as resolveExactLineGroupConfigKey, f as resolveLineGroupConfigEntry, g as setLineRuntime, l as createMediaPlayerCard, m as resolveLineGroupsConfig, p as resolveLineGroupLookupIds, s as createAppleTvRemoteCard, v as LineConfigSchema } from "./rich-messages-DY1AGKAJ.js";
3
+ import { C as toFlexMessage, D as createVideoMessage, E as createImageMessage, T as createAudioMessage, a as probeLineBot, c as createAgendaCard, d as createActionCard, f as createImageCard, g as messageAction, h as datetimePickerAction, l as createEventCard, m as createListCard, o as resolveLineChannelAccessToken, p as createInfoCard, u as createReceiptCard, x as uriAction, y as postbackAction } from "./send-retry-ChpfJ-LM.js";
4
+ 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-m9h5osFz.js";
5
+ import { a as stripMarkdown, c as createCarouselColumn, i as processLineMessage, l as createConfirmTemplate, n as convertTableToFlexBubble, o as buildTemplateMessageFromPayload, r as hasMarkdownToConvert, s as createButtonTemplate, t as convertCodeBlockToFlexBubble, u as createTemplateCarousel } from "./markdown-to-line-B-sj-7II.js";
6
+ import { _ as showLoadingAnimation, c as pushFlexMessage, d as pushMessageLine, f as pushMessagesLine, g as sendMessageLine, h as replyMessageLine, i as createTextMessageWithQuickReplies, l as pushImageMessage, m as pushTextMessageWithQuickReplies, n as createLocationMessage, o as getUserDisplayName, p as pushTemplateMessage, r as createQuickReplyItems, s as getUserProfile, t as createFlexMessage, u as pushLocationMessage } from "./send-CQQxpxPp.js";
8
7
  import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
9
- import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
8
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
9
+ import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
10
+ import { messagingApi } from "@line/bot-sdk";
10
11
  import { DEFAULT_ACCOUNT_ID, formatDocsLink, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
11
12
  import { buildComputedAccountStatusSnapshot, buildTokenChannelStatusSummary } from "openclaw/plugin-sdk/status-helpers";
13
+ import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
12
14
  import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
13
- import { messagingApi } from "@line/bot-sdk";
14
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
15
+ import { bufferToBlobPart } from "openclaw/plugin-sdk/blob-runtime";
15
16
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-local-roots";
16
- import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
17
17
  import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
18
18
  //#region extensions/line/src/webhook.ts
19
- const LINE_WEBHOOK_MAX_RAW_BODY_BYTES = 64 * 1024;
19
+ const LINE_WEBHOOK_MAX_RAW_BODY_BYTES = 65536;
20
20
  function readRawBody(req) {
21
21
  const rawBody = req.rawBody ?? (typeof req.body === "string" || Buffer.isBuffer(req.body) ? req.body : null);
22
22
  if (!rawBody) return null;
@@ -123,9 +123,8 @@ async function uploadRichMenuImage(richMenuId, imagePath, opts) {
123
123
  const blobClient = getBlobClient(opts);
124
124
  const media = await loadWebMediaRaw(imagePath, { localRoots: opts.mediaLocalRoots ?? getAgentScopedMediaLocalRoots(opts.cfg) });
125
125
  const contentType = media.contentType === "image/png" || media.contentType === "image/jpeg" ? media.contentType : mimeTypeFromFilePath(imagePath) === "image/png" ? "image/png" : "image/jpeg";
126
- const imageBytes = new ArrayBuffer(media.buffer.byteLength);
127
- new Uint8Array(imageBytes).set(media.buffer);
128
- await blobClient.setRichMenuImage(richMenuId, new Blob([imageBytes], { type: contentType }));
126
+ const blob = new Blob([bufferToBlobPart(media.buffer)], { type: contentType });
127
+ await blobClient.setRichMenuImage(richMenuId, blob);
129
128
  if (opts.verbose) logVerbose(`line: uploaded image to rich menu ${richMenuId}`);
130
129
  }
131
130
  async function setDefaultRichMenu(richMenuId, opts) {
@@ -1,55 +1,80 @@
1
- import { i as resolveLineAccount } from "./accounts-BqEVsDoB.js";
2
- import { f as normalizeLineMessageActions, t as createLineSendReceipt, u as messageAction, y as validateLineMediaUrl } from "./send-receipt-CCPTalf9.js";
1
+ import { i as resolveLineAccount } from "./accounts-BBEFMYbY.js";
2
+ import { g as messageAction, i as runLinePushWithRetries, o as resolveLineChannelAccessToken, s as createLineSendReceipt, v as normalizeLineMessage, w as buildLineMediaMessage } from "./send-retry-ChpfJ-LM.js";
3
3
  import { createChannelPartialDeliveryError } from "openclaw/plugin-sdk/channel-inbound";
4
4
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
5
- import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
6
- import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
7
5
  import { HTTPFetchError, messagingApi } from "@line/bot-sdk";
8
- import { collectErrorGraphCandidates, extractErrorCode } from "openclaw/plugin-sdk/error-runtime";
6
+ import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
7
+ import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
8
+ import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
9
9
  import { randomUUID } from "node:crypto";
10
10
  import lineBotSdkPackage from "@line/bot-sdk/package.json" with { type: "json" };
11
11
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
12
12
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
13
13
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
14
- import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
15
- import { classifyTransientNetworkErrorCode, createChannelApiRetryRunner } from "openclaw/plugin-sdk/retry-runtime";
16
- //#region extensions/line/src/channel-access-token.ts
17
- function resolveLineChannelAccessToken(explicit, params) {
18
- if (explicit?.trim()) return explicit.trim();
19
- if (!params.channelAccessToken) throw new Error(`LINE channel access token missing for account "${params.accountId}" (set channels.line.channelAccessToken or LINE_CHANNEL_ACCESS_TOKEN).`);
20
- return params.channelAccessToken.trim();
14
+ import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
15
+ //#region extensions/line/src/outbound-message-log.ts
16
+ const RECENT_SENT_LIMIT = 500;
17
+ const recentSentByAccount = /* @__PURE__ */ new Map();
18
+ function recentSentFor(accountId) {
19
+ const existing = recentSentByAccount.get(accountId);
20
+ if (existing) return existing;
21
+ const created = createDedupeCache({
22
+ ttlMs: 0,
23
+ maxSize: RECENT_SENT_LIMIT
24
+ });
25
+ recentSentByAccount.set(accountId, created);
26
+ return created;
27
+ }
28
+ function recordLineSentMessages(accountId, messageIds) {
29
+ if (messageIds.length === 0) return;
30
+ const recentSent = recentSentFor(accountId);
31
+ for (const messageId of messageIds) recentSent.check(messageId);
32
+ }
33
+ function quotesLineBotMessage(accountId, quotedMessageId) {
34
+ return recentSentByAccount.get(accountId)?.peek(quotedMessageId) ?? false;
21
35
  }
22
- //#endregion
23
- //#region extensions/line/src/send-retry.ts
24
- function isRetryableLinePushError(error) {
25
- const candidates = collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]);
26
- const httpError = candidates.find((candidate) => candidate instanceof HTTPFetchError);
27
- if (httpError) return httpError.status >= 500;
28
- return candidates.some((candidate) => classifyTransientNetworkErrorCode(extractErrorCode(candidate)) !== void 0);
29
- }
30
- /**
31
- * Pushes are non-idempotent without a retry key, so the generic message-matching
32
- * fallback stays off and only the classification above may replay a request.
33
- */
34
- const runLinePushWithRetries = createChannelApiRetryRunner({
35
- shouldRetry: isRetryableLinePushError,
36
- strictShouldRetry: true,
37
- verbose: true
38
- });
39
36
  //#endregion
40
37
  //#region extensions/line/src/send.ts
41
- const userProfileCache = /* @__PURE__ */ new Map();
42
- const PROFILE_CACHE_TTL_MS = 300 * 1e3;
38
+ const profileCache = {
39
+ values: /* @__PURE__ */ new Map(),
40
+ pending: /* @__PURE__ */ new Map()
41
+ };
42
+ const groupNameCache = {
43
+ values: /* @__PURE__ */ new Map(),
44
+ pending: /* @__PURE__ */ new Map()
45
+ };
46
+ const PROFILE_CACHE_TTL_MS = 3e5;
43
47
  const PROFILE_CACHE_MAX_ENTRIES = 1e3;
44
48
  const LINE_FLEX_ALT_TEXT_LIMIT = 1500;
45
49
  const LINE_LOCATION_LABEL_LIMIT = 100;
46
- const LINE_PROVIDER_RESPONSE_MAX_BYTES = 16 * 1024;
47
- function cacheUserProfile(userId, profile) {
48
- userProfileCache.delete(userId);
49
- userProfileCache.set(userId, profile);
50
- if (userProfileCache.size <= PROFILE_CACHE_MAX_ENTRIES) return;
51
- for (const [key, cached] of userProfileCache) if (profile.fetchedAt - cached.fetchedAt >= PROFILE_CACHE_TTL_MS) userProfileCache.delete(key);
52
- pruneMapToMaxSize(userProfileCache, PROFILE_CACHE_MAX_ENTRIES);
50
+ const LINE_PROVIDER_RESPONSE_MAX_BYTES = 16384;
51
+ function rememberLineIdentity(cache, key, value) {
52
+ const entry = {
53
+ value,
54
+ fetchedAt: Date.now()
55
+ };
56
+ cache.values.delete(key);
57
+ cache.values.set(key, entry);
58
+ if (cache.values.size <= PROFILE_CACHE_MAX_ENTRIES) return;
59
+ for (const [cachedKey, cached] of cache.values) if (entry.fetchedAt - cached.fetchedAt >= PROFILE_CACHE_TTL_MS) cache.values.delete(cachedKey);
60
+ pruneMapToMaxSize(cache.values, PROFILE_CACHE_MAX_ENTRIES);
61
+ }
62
+ async function loadLineIdentity(cache, key, load) {
63
+ const cached = cache.values.get(key);
64
+ if (cached && Date.now() - cached.fetchedAt < PROFILE_CACHE_TTL_MS) return cached.value;
65
+ const pending = cache.pending.get(key);
66
+ if (pending) return await pending;
67
+ const lookup = load().then((value) => {
68
+ rememberLineIdentity(cache, key, value);
69
+ return value;
70
+ });
71
+ cache.pending.set(key, lookup);
72
+ pruneMapToMaxSize(cache.pending, PROFILE_CACHE_MAX_ENTRIES);
73
+ try {
74
+ return await lookup;
75
+ } finally {
76
+ if (cache.pending.get(key) === lookup) cache.pending.delete(key);
77
+ }
53
78
  }
54
79
  function resolveLineProviderMessageIds(response, operation) {
55
80
  const sentMessages = Array.isArray(response?.sentMessages) ? response.sentMessages : [];
@@ -76,12 +101,10 @@ function normalizeTarget(to) {
76
101
  if (normalized.length >= 33 && !/^[CUR]/.test(normalized)) throw new Error(`Recipient is not a valid LINE id (case-sensitive; expected leading capital C/U/R): ${truncateUtf16Safe(normalized, 4)}…`);
77
102
  return normalized;
78
103
  }
79
- function isLineUserChatId(chatId) {
80
- return /^U/i.test(chatId);
81
- }
82
104
  function resolveLineMessagingAccount(opts) {
105
+ const cfg = requireRuntimeConfig(opts.cfg, "LINE send");
83
106
  const account = resolveLineAccount({
84
- cfg: requireRuntimeConfig(opts.cfg, "LINE send"),
107
+ cfg,
85
108
  accountId: opts.accountId
86
109
  });
87
110
  return {
@@ -140,28 +163,6 @@ function createTextMessage(text) {
140
163
  text
141
164
  };
142
165
  }
143
- function createImageMessage(originalContentUrl, previewImageUrl) {
144
- return {
145
- type: "image",
146
- originalContentUrl,
147
- previewImageUrl: previewImageUrl ?? originalContentUrl
148
- };
149
- }
150
- function createVideoMessage(originalContentUrl, previewImageUrl, trackingId) {
151
- return {
152
- type: "video",
153
- originalContentUrl,
154
- previewImageUrl,
155
- ...trackingId ? { trackingId } : {}
156
- };
157
- }
158
- function createAudioMessage(originalContentUrl, durationMs) {
159
- return {
160
- type: "audio",
161
- originalContentUrl,
162
- duration: durationMs
163
- };
164
- }
165
166
  function isValidLineLocation(location) {
166
167
  return location.title.trim().length > 0 && location.address.trim().length > 0;
167
168
  }
@@ -184,9 +185,13 @@ function createLocationMessage(location) {
184
185
  function logLineHttpError(err, context) {
185
186
  if (!err || typeof err !== "object") return;
186
187
  const { status, statusText, body } = err;
187
- if (typeof body === "string") logVerbose(`line: ${context} failed (${status ? `${status} ${statusText ?? ""}`.trim() : "unknown status"}): ${body}`);
188
+ if (typeof body === "string") {
189
+ const summary = status ? `${status} ${statusText ?? ""}`.trim() : "unknown status";
190
+ logVerbose(`line: ${context} failed (${summary}): ${body}`);
191
+ }
188
192
  }
189
193
  function recordLineOutboundActivity(accountId, delivery) {
194
+ recordLineSentMessages(accountId, delivery.messageIds);
190
195
  try {
191
196
  recordChannelActivity({
192
197
  channel: "line",
@@ -212,7 +217,7 @@ function resolveLineReceiptKind(messages) {
212
217
  async function pushLineMessages(to, messages, opts, behavior = {}) {
213
218
  if (messages.length === 0) throw new Error("Message must be non-empty for LINE sends");
214
219
  const { account, token, chatId } = createLinePushContext(to, opts);
215
- const normalizedMessages = messages.map(normalizeLineMessageActions);
220
+ const normalizedMessages = messages.map(normalizeLineMessage);
216
221
  const retryKey = randomUUID();
217
222
  const { messageId, messageIds } = resolveLineProviderMessageIds(await runLinePushWithRetries(async () => {
218
223
  try {
@@ -240,7 +245,10 @@ async function pushLineMessages(to, messages, opts, behavior = {}) {
240
245
  messageIds,
241
246
  receipt: result.receipt
242
247
  });
243
- if (opts.verbose) logVerbose(behavior.verboseMessage?.(chatId, messages.length) ?? `line: pushed ${messages.length} messages to ${chatId}`);
248
+ if (opts.verbose) {
249
+ const logMessage = behavior.verboseMessage?.(chatId, messages.length) ?? `line: pushed ${messages.length} messages to ${chatId}`;
250
+ logVerbose(logMessage);
251
+ }
244
252
  return result;
245
253
  }
246
254
  async function replyLineMessages(replyToken, messages, opts) {
@@ -248,7 +256,7 @@ async function replyLineMessages(replyToken, messages, opts) {
248
256
  return {
249
257
  ...resolveLineProviderMessageIds(await sendLineProviderMessages("reply", token, {
250
258
  replyToken,
251
- messages: messages.map(normalizeLineMessageActions)
259
+ messages: messages.map(normalizeLineMessage)
252
260
  }), "reply"),
253
261
  accountId: account.accountId
254
262
  };
@@ -257,29 +265,12 @@ async function sendMessageLine(to, text, opts) {
257
265
  const chatId = normalizeTarget(to);
258
266
  const messages = [];
259
267
  const mediaUrl = opts.mediaUrl?.trim();
260
- if (mediaUrl) {
261
- await validateLineMediaUrl(mediaUrl);
262
- switch (opts.mediaKind) {
263
- case "video": {
264
- const previewImageUrl = opts.previewImageUrl?.trim();
265
- if (!previewImageUrl) throw new Error("LINE video messages require previewImageUrl to reference an image URL");
266
- await validateLineMediaUrl(previewImageUrl);
267
- const trackingId = isLineUserChatId(chatId) ? opts.trackingId : void 0;
268
- messages.push(createVideoMessage(mediaUrl, previewImageUrl, trackingId));
269
- break;
270
- }
271
- case "audio":
272
- messages.push(createAudioMessage(mediaUrl, opts.durationMs ?? 6e4));
273
- break;
274
- default:
275
- {
276
- const previewImageUrl = opts.previewImageUrl?.trim() || mediaUrl;
277
- await validateLineMediaUrl(previewImageUrl);
278
- messages.push(createImageMessage(mediaUrl, previewImageUrl));
279
- }
280
- break;
281
- }
282
- }
268
+ if (mediaUrl) messages.push(await buildLineMediaMessage(mediaUrl, {
269
+ mediaKind: opts.mediaKind,
270
+ previewImageUrl: opts.previewImageUrl,
271
+ durationMs: opts.durationMs,
272
+ trackingId: opts.trackingId
273
+ }, chatId));
283
274
  if (text?.trim()) messages.push(createTextMessage(text.trim()));
284
275
  if (messages.length === 0) throw new Error("Message must be non-empty for LINE sends");
285
276
  if (opts.replyToken) {
@@ -326,9 +317,10 @@ function createFlexMessage(altText, contents) {
326
317
  };
327
318
  }
328
319
  async function pushImageMessage(to, originalContentUrl, previewImageUrl, opts) {
329
- await validateLineMediaUrl(originalContentUrl);
330
- if (previewImageUrl) await validateLineMediaUrl(previewImageUrl);
331
- return pushLineMessages(to, [createImageMessage(originalContentUrl, previewImageUrl)], opts, { verboseMessage: (chatId) => `line: pushed image to ${chatId}` });
320
+ return pushLineMessages(to, [await buildLineMediaMessage(originalContentUrl, {
321
+ mediaKind: "image",
322
+ previewImageUrl
323
+ }, to)], opts, { verboseMessage: (chatId) => `line: pushed image to ${chatId}` });
332
324
  }
333
325
  async function pushLocationMessage(to, location, opts) {
334
326
  return pushLineMessages(to, [createLocationMessage(location)], opts, { verboseMessage: (chatId) => `line: pushed location to ${chatId}` });
@@ -370,26 +362,43 @@ async function showLoadingAnimation(chatId, opts) {
370
362
  logVerbose(`line: loading animation failed (non-fatal): ${String(err)}`);
371
363
  }
372
364
  }
365
+ function lineProfileCacheKey(accountId, userId, scope) {
366
+ const conversation = scope.groupId ? ["group", scope.groupId] : scope.roomId ? ["room", scope.roomId] : ["direct"];
367
+ return JSON.stringify([
368
+ accountId,
369
+ "profile",
370
+ ...conversation,
371
+ userId
372
+ ]);
373
+ }
374
+ function fetchLineMemberProfile(client, userId, scope) {
375
+ if (scope.groupId) return client.getGroupMemberProfile(scope.groupId, userId);
376
+ if (scope.roomId) return client.getRoomMemberProfile(scope.roomId, userId);
377
+ return client.getProfile(userId);
378
+ }
373
379
  async function getUserProfile(userId, opts) {
374
- if (opts.useCache ?? true) {
375
- const cached = userProfileCache.get(userId);
376
- if (cached && Date.now() - cached.fetchedAt < PROFILE_CACHE_TTL_MS) return {
377
- displayName: cached.displayName,
378
- pictureUrl: cached.pictureUrl
379
- };
380
- }
381
- const { client } = createLineMessagingClient(opts);
380
+ const useCache = opts.useCache ?? true;
382
381
  try {
383
- const profile = await client.getProfile(userId);
384
- const result = {
385
- displayName: profile.displayName,
386
- pictureUrl: profile.pictureUrl
382
+ const { account, token } = resolveLineMessagingAccount(opts);
383
+ const cacheKey = lineProfileCacheKey(account.accountId, userId, opts);
384
+ const load = async () => {
385
+ try {
386
+ const profile = await fetchLineMemberProfile(new messagingApi.MessagingApiClient({ channelAccessToken: token }), userId, opts);
387
+ return {
388
+ displayName: profile.displayName,
389
+ pictureUrl: profile.pictureUrl
390
+ };
391
+ } catch (err) {
392
+ logVerbose(`line: failed to fetch profile for ${userId}: ${String(err)}`);
393
+ return null;
394
+ }
387
395
  };
388
- cacheUserProfile(userId, {
389
- ...result,
390
- fetchedAt: Date.now()
391
- });
392
- return result;
396
+ if (!useCache) {
397
+ const profile = await load();
398
+ rememberLineIdentity(profileCache, cacheKey, profile);
399
+ return profile;
400
+ }
401
+ return await loadLineIdentity(profileCache, cacheKey, load);
393
402
  } catch (err) {
394
403
  logVerbose(`line: failed to fetch profile for ${userId}: ${String(err)}`);
395
404
  return null;
@@ -398,5 +407,26 @@ async function getUserProfile(userId, opts) {
398
407
  async function getUserDisplayName(userId, opts) {
399
408
  return (await getUserProfile(userId, opts))?.displayName ?? userId;
400
409
  }
410
+ async function getLineGroupName(groupId, opts) {
411
+ try {
412
+ const { account, token } = resolveLineMessagingAccount(opts);
413
+ const cacheKey = JSON.stringify([
414
+ account.accountId,
415
+ "group",
416
+ groupId
417
+ ]);
418
+ return await loadLineIdentity(groupNameCache, cacheKey, async () => {
419
+ try {
420
+ return (await new messagingApi.MessagingApiClient({ channelAccessToken: token }).getGroupSummary(groupId)).groupName.trim() || void 0;
421
+ } catch (err) {
422
+ logVerbose(`line: failed to fetch group summary for ${groupId}: ${String(err)}`);
423
+ return;
424
+ }
425
+ });
426
+ } catch (err) {
427
+ logVerbose(`line: failed to fetch group summary for ${groupId}: ${String(err)}`);
428
+ return;
429
+ }
430
+ }
401
431
  //#endregion
402
- export { replyMessageLine as _, createQuickReplyItems as a, resolveLineChannelAccessToken as b, getUserDisplayName as c, pushImageMessage as d, pushLocationMessage as f, pushTextMessageWithQuickReplies as g, pushTemplateMessage as h, createLocationMessage as i, getUserProfile as l, pushMessagesLine as m, createFlexMessage as n, createTextMessageWithQuickReplies as o, pushMessageLine as p, createImageMessage as r, createVideoMessage as s, createAudioMessage as t, pushFlexMessage as u, sendMessageLine as v, showLoadingAnimation as y };
432
+ export { showLoadingAnimation as _, getLineGroupName as a, pushFlexMessage as c, pushMessageLine as d, pushMessagesLine as f, sendMessageLine as g, replyMessageLine as h, createTextMessageWithQuickReplies as i, pushImageMessage as l, pushTextMessageWithQuickReplies as m, createLocationMessage as n, getUserDisplayName as o, pushTemplateMessage as p, createQuickReplyItems as r, getUserProfile as s, createFlexMessage as t, pushLocationMessage as u, quotesLineBotMessage as v };