@openclaw/line 2026.8.2 → 2026.9.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.
@@ -1,11 +1,24 @@
1
+ import { i as resolveLineAccount } from "./accounts-BBEFMYbY.js";
2
+ import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
1
3
  import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
2
- import { collectErrorGraphCandidates, extractErrorCode } from "openclaw/plugin-sdk/error-runtime";
4
+ import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
3
5
  import { getFileExtension, mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
4
6
  import { resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
5
- import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
6
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
7
+ import { runChannelProbe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
7
8
  import { HTTPFetchError } from "@line/bot-sdk";
8
9
  import { classifyTransientNetworkErrorCode, createChannelApiRetryRunner } from "openclaw/plugin-sdk/retry-runtime";
10
+ import { assertOkOrThrowHttpError, createProviderOperationDeadline, createProviderOperationTimeoutResolver, fetchWithTimeout, readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
11
+ import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
12
+ //#region extensions/line/src/media-url.ts
13
+ function isHttpsUrl(value) {
14
+ if (typeof value !== "string") return false;
15
+ try {
16
+ return new URL(value).protocol === "https:";
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ //#endregion
9
22
  //#region extensions/line/src/outbound-media.ts
10
23
  const LINE_OUTBOUND_MEDIA_SSRF_POLICY = { allowPrivateNetwork: false };
11
24
  async function validateLineMediaUrl(url) {
@@ -19,13 +32,6 @@ async function validateLineMediaUrl(url) {
19
32
  if (url.length > 2e3) throw new Error(`LINE outbound media URL must be 2000 chars or less (got ${url.length})`);
20
33
  await resolvePinnedHostnameWithPolicy(parsed.hostname, { policy: LINE_OUTBOUND_MEDIA_SSRF_POLICY });
21
34
  }
22
- function isHttpsUrl(url) {
23
- try {
24
- return new URL(url).protocol === "https:";
25
- } catch {
26
- return false;
27
- }
28
- }
29
35
  const LINE_MEDIA_KIND_BY_MIME = {
30
36
  "image/jpeg": "image",
31
37
  "image/png": "image",
@@ -130,6 +136,20 @@ async function buildLineMediaMessage(mediaUrl, opts, target) {
130
136
  return buildLineMediaMessageObject(await resolveLineOutboundMedia(mediaUrl, opts), { allowTrackingId: isLineUserTarget(target) });
131
137
  }
132
138
  //#endregion
139
+ //#region extensions/line/src/flex-templates/message.ts
140
+ const LINE_FLEX_BUBBLE_MAX_BYTES = 3e4;
141
+ const LINE_FLEX_CAROUSEL_MAX_BYTES = 5e4;
142
+ function fitsLineFlexBubble(bubble) {
143
+ return Buffer.byteLength(JSON.stringify(bubble), "utf8") <= LINE_FLEX_BUBBLE_MAX_BYTES;
144
+ }
145
+ function toFlexMessage(altText, contents) {
146
+ return {
147
+ type: "flex",
148
+ altText,
149
+ contents
150
+ };
151
+ }
152
+ //#endregion
133
153
  //#region extensions/line/src/actions.ts
134
154
  const LINE_ACTION_LABEL_LIMIT = 20;
135
155
  const LINE_ACTION_DATA_LIMIT = 300;
@@ -158,6 +178,17 @@ function truncateLineActionLabel(label, limit = LINE_ACTION_LABEL_LIMIT) {
158
178
  function truncateLineActionData(data) {
159
179
  return truncateUtf16Safe(data, LINE_ACTION_DATA_LIMIT);
160
180
  }
181
+ const UNDELIVERABLE_IMAGE_WARNING = "Image unavailable: URL must use HTTPS.";
182
+ function flexWarning(text) {
183
+ return {
184
+ type: "text",
185
+ text,
186
+ wrap: true,
187
+ size: "sm",
188
+ color: "#B45309",
189
+ margin: "md"
190
+ };
191
+ }
161
192
  const unavailableActionMarker = Symbol("lineUnavailableAction");
162
193
  function unavailableAction(kind, reason) {
163
194
  const action = {
@@ -185,13 +216,13 @@ function isLineAction(value) {
185
216
  function isUnavailableAction(action) {
186
217
  return action[unavailableActionMarker] === true;
187
218
  }
188
- function normalizeNestedActions(value, labelLimit, warnings) {
189
- if (Array.isArray(value)) {
190
- const normalized = [];
191
- for (const item of value) normalized.push(normalizeNestedActions(item, labelLimit, warnings));
192
- return normalized;
193
- }
219
+ function normalizeNestedContent(value, labelLimit, warnings) {
220
+ if (Array.isArray(value)) return value.map((item) => normalizeNestedContent(item, labelLimit, warnings)).filter((item) => item !== void 0);
194
221
  if (!isRecord(value)) return value;
222
+ if (warnings && (value.type === "image" || value.type === "icon") && !isHttpsUrl(value.url)) {
223
+ warnings.push(UNDELIVERABLE_IMAGE_WARNING);
224
+ return;
225
+ }
195
226
  const normalized = { ...value };
196
227
  for (const [key, nested] of Object.entries(value)) if ((key === "action" || key === "defaultAction") && isLineAction(nested)) {
197
228
  const action = normalizeLineAction(nested, labelLimit);
@@ -200,42 +231,61 @@ function normalizeNestedActions(value, labelLimit, warnings) {
200
231
  warnings.push(isUnavailableAction(action) ? action.text ?? "Action unavailable." : "Action unavailable in this video.");
201
232
  } else normalized[key] = action;
202
233
  } else if (key === "actions" && Array.isArray(nested)) normalized[key] = nested.map((action) => isLineAction(action) ? normalizeLineAction(action, labelLimit) : action);
203
- else normalized[key] = normalizeNestedActions(nested, labelLimit, warnings);
234
+ else {
235
+ const content = normalizeNestedContent(nested, labelLimit, warnings);
236
+ if (content === void 0) delete normalized[key];
237
+ else normalized[key] = content;
238
+ }
239
+ if (warnings && value.type === "video") {
240
+ const altContent = normalized.altContent ?? {
241
+ type: "box",
242
+ layout: "vertical",
243
+ contents: [flexWarning(UNDELIVERABLE_IMAGE_WARNING)]
244
+ };
245
+ if (!isHttpsUrl(value.url) || !isHttpsUrl(value.previewUrl)) {
246
+ warnings.push("Video unavailable: video and preview URLs must use HTTPS.");
247
+ return altContent;
248
+ }
249
+ normalized.altContent = altContent;
250
+ }
204
251
  return normalized;
205
252
  }
206
- function normalizeFlexBubbleActions(value) {
207
- if (!isRecord(value) || value.type !== "bubble") return normalizeNestedActions(value, 40);
253
+ function normalizeFlexBubble(value, maxBytes = LINE_FLEX_BUBBLE_MAX_BYTES) {
254
+ if (!isRecord(value) || value.type !== "bubble") return normalizeNestedContent(value, 40);
208
255
  const warnings = [];
209
- const normalized = normalizeNestedActions(value, 40, warnings);
256
+ const normalized = normalizeNestedContent(value, 40, warnings);
210
257
  if (!isRecord(normalized) || warnings.length === 0) return normalized;
211
- const warning = {
212
- type: "text",
213
- text: [...new Set(warnings)].join("\n"),
214
- wrap: true,
215
- size: "sm",
216
- color: "#B45309",
217
- margin: "md"
218
- };
258
+ const warning = flexWarning([...new Set(warnings)].join("\n"));
219
259
  const body = normalized.body;
220
- if (isRecord(body) && Array.isArray(body.contents)) normalized.body = {
221
- ...body,
222
- contents: [...body.contents, warning]
223
- };
224
- else normalized.body = {
225
- type: "box",
226
- layout: "vertical",
227
- contents: [warning]
260
+ const withWarning = {
261
+ ...normalized,
262
+ body: isRecord(body) && Array.isArray(body.contents) ? {
263
+ ...body,
264
+ contents: [...body.contents, warning]
265
+ } : {
266
+ type: "box",
267
+ layout: "vertical",
268
+ contents: [warning]
269
+ }
228
270
  };
229
- return normalized;
271
+ return Buffer.byteLength(JSON.stringify(withWarning), "utf8") <= Math.min(maxBytes, 3e4) ? withWarning : normalized;
230
272
  }
231
- function normalizeFlexContainerActions(value) {
273
+ function normalizeFlexContainer(value) {
232
274
  if (!isRecord(value)) return value;
233
- if (value.type === "bubble") return normalizeFlexBubbleActions(value);
234
- if (value.type === "carousel" && Array.isArray(value.contents)) return {
235
- ...value,
236
- contents: value.contents.map((bubble) => normalizeFlexBubbleActions(bubble))
237
- };
238
- return normalizeNestedActions(value, 40);
275
+ if (value.type === "bubble") return normalizeFlexBubble(value);
276
+ if (value.type === "carousel" && Array.isArray(value.contents)) {
277
+ let remainingBytes = LINE_FLEX_CAROUSEL_MAX_BYTES - Buffer.byteLength(JSON.stringify(value), "utf8");
278
+ return {
279
+ ...value,
280
+ contents: value.contents.map((bubble) => {
281
+ const originalBytes = Buffer.byteLength(JSON.stringify(bubble), "utf8");
282
+ const normalized = normalizeFlexBubble(bubble, originalBytes + remainingBytes);
283
+ remainingBytes += originalBytes - Buffer.byteLength(JSON.stringify(normalized), "utf8");
284
+ return normalized;
285
+ })
286
+ };
287
+ }
288
+ return normalizeNestedContent(value, 40);
239
289
  }
240
290
  function unavailableImagemapAction(kind, reason, area) {
241
291
  return {
@@ -289,17 +339,20 @@ function normalizeImagemapVideo(video) {
289
339
  }
290
340
  } };
291
341
  }
292
- function normalizeLineMessageActions(message) {
342
+ function normalizeLineMessage(message) {
293
343
  let normalized;
294
344
  if (message.type === "flex") normalized = {
295
345
  ...message,
296
- contents: normalizeFlexContainerActions(message.contents)
346
+ contents: normalizeFlexContainer(message.contents)
297
347
  };
298
348
  else if (message.type === "template") {
299
349
  const labelLimit = message.template.type === "image_carousel" ? 12 : 20;
350
+ const template = normalizeNestedContent(message.template, labelLimit);
351
+ const columns = template.type === "carousel" ? template.columns : template.type === "buttons" ? [template] : [];
352
+ if (columns.some((column) => !isHttpsUrl(column.thumbnailImageUrl))) for (const column of columns) delete column.thumbnailImageUrl;
300
353
  normalized = {
301
354
  ...message,
302
- template: normalizeNestedActions(message.template, labelLimit)
355
+ template
303
356
  };
304
357
  } else if (message.type === "imagemap") {
305
358
  const actions = message.actions.map(normalizeImagemapAction);
@@ -315,7 +368,7 @@ function normalizeLineMessageActions(message) {
315
368
  } else normalized = { ...message };
316
369
  if (message.quickReply) normalized = {
317
370
  ...normalized,
318
- quickReply: normalizeNestedActions(message.quickReply, 20)
371
+ quickReply: normalizeNestedContent(message.quickReply, 20)
319
372
  };
320
373
  return normalized;
321
374
  }
@@ -1034,6 +1087,90 @@ function createLineSendReceipt(params) {
1034
1087
  });
1035
1088
  }
1036
1089
  //#endregion
1090
+ //#region extensions/line/src/channel-access-token.ts
1091
+ function resolveLineChannelAccessToken(explicit, params) {
1092
+ if (explicit?.trim()) return explicit.trim();
1093
+ if (!params.channelAccessToken) throw new Error(`LINE channel access token missing for account "${params.accountId}" (set channels.line.channelAccessToken or LINE_CHANNEL_ACCESS_TOKEN).`);
1094
+ return params.channelAccessToken.trim();
1095
+ }
1096
+ //#endregion
1097
+ //#region extensions/line/src/probe.ts
1098
+ const LINE_QUOTA_TIMEOUT_MS = 2e3;
1099
+ const LINE_JSON_MAX_BYTES = 16384;
1100
+ function createLineApiReader(channelAccessToken, timeoutMs) {
1101
+ const remaining = createProviderOperationTimeoutResolver({
1102
+ deadline: createProviderOperationDeadline({
1103
+ timeoutMs,
1104
+ label: "LINE API"
1105
+ }),
1106
+ defaultTimeoutMs: timeoutMs
1107
+ });
1108
+ const headers = { Authorization: `Bearer ${channelAccessToken}` };
1109
+ return async (endpoint) => {
1110
+ const response = await fetchWithTimeout(`https://api.line.me/v2/bot/${endpoint}`, { headers }, remaining(), fetchWithRuntimeDispatcherOrMockedGlobal);
1111
+ await assertOkOrThrowHttpError(response, "LINE API", {
1112
+ bodyTimeoutMs: remaining,
1113
+ requestHeaders: headers
1114
+ });
1115
+ return await readProviderJsonResponse(response, "LINE API", {
1116
+ maxBytes: LINE_JSON_MAX_BYTES,
1117
+ timeoutMs: remaining,
1118
+ requestHeaders: headers
1119
+ });
1120
+ };
1121
+ }
1122
+ async function readLineMessageQuota(channelAccessToken, budgetMs) {
1123
+ if (!Number.isFinite(budgetMs) || budgetMs <= 0) return;
1124
+ try {
1125
+ const read = createLineApiReader(channelAccessToken, budgetMs);
1126
+ const quota = await read("message/quota");
1127
+ if (quota.type === "none") return { kind: "unlimited" };
1128
+ if (quota.type !== "limited" || typeof quota.value !== "number" || !Number.isSafeInteger(quota.value) || quota.value < 0) return;
1129
+ const { totalUsage } = await read("message/quota/consumption");
1130
+ return typeof totalUsage === "number" && Number.isSafeInteger(totalUsage) && totalUsage >= 0 ? {
1131
+ kind: "limited",
1132
+ limit: quota.value,
1133
+ used: totalUsage
1134
+ } : void 0;
1135
+ } catch {
1136
+ return;
1137
+ }
1138
+ }
1139
+ async function readLineAccountMessageQuota(params) {
1140
+ try {
1141
+ return await readLineMessageQuota(resolveLineChannelAccessToken(void 0, resolveLineAccount({
1142
+ cfg: params.cfg,
1143
+ accountId: params.accountId ?? void 0
1144
+ })), LINE_QUOTA_TIMEOUT_MS);
1145
+ } catch {
1146
+ return;
1147
+ }
1148
+ }
1149
+ async function probeLineBot(channelAccessToken, timeoutMs = 5e3) {
1150
+ if (!channelAccessToken?.trim()) return {
1151
+ ok: false,
1152
+ error: "Channel access token not configured"
1153
+ };
1154
+ const token = channelAccessToken.trim();
1155
+ return await runChannelProbe(timeoutMs, async ({ elapsedMs }) => {
1156
+ const profile = await createLineApiReader(token, timeoutMs)("info");
1157
+ const quota = await readLineMessageQuota(token, Math.floor(Math.max(timeoutMs - elapsedMs(), 0) / 2));
1158
+ return {
1159
+ ok: true,
1160
+ bot: {
1161
+ displayName: profile.displayName,
1162
+ userId: profile.userId,
1163
+ basicId: profile.basicId,
1164
+ pictureUrl: profile.pictureUrl
1165
+ },
1166
+ ...quota ? { quota } : {}
1167
+ };
1168
+ }, (error) => ({
1169
+ ok: false,
1170
+ error: formatErrorMessage(error)
1171
+ }));
1172
+ }
1173
+ //#endregion
1037
1174
  //#region extensions/line/src/send-retry.ts
1038
1175
  /** The LINE HTTP response carried by an error graph, when the request reached LINE. */
1039
1176
  function findLineHttpError(error) {
@@ -1086,5 +1223,14 @@ const runLinePushWithRetries = (fn, label) => {
1086
1223
  throw error;
1087
1224
  });
1088
1225
  };
1226
+ async function explainLineRefusal(params) {
1227
+ const retryable = resolveLineNonDispatchRetryable(params.error);
1228
+ const quota = retryable === true && findLineHttpError(params.error)?.status === 429 ? await readLineAccountMessageQuota(params) : void 0;
1229
+ const exhausted = quota?.kind === "limited" && quota.used >= quota.limit;
1230
+ return {
1231
+ retryable: exhausted ? false : retryable,
1232
+ reason: exhausted ? `LINE refused the push: ${quota.used}/${quota.limit} monthly messages used. Check the account allowance or plan before retrying.` : params.error instanceof Error ? params.error.message : "LINE rejected the message"
1233
+ };
1234
+ }
1089
1235
  //#endregion
1090
- export { createVideoMessage as S, truncateLineActionLabel as _, createAgendaCard as a, createAudioMessage as b, createActionCard as c, createListCard as d, datetimePickerAction as f, postbackAction as g, normalizeLineMessageActions as h, createLineSendReceipt as i, createImageCard as l, normalizeLineAction as m, resolveLineNonDispatchRetryable as n, createEventCard as o, messageAction as p, runLinePushWithRetries as r, createReceiptCard as s, findLineHttpError as t, createInfoCard as u, uriAction as v, createImageMessage as x, buildLineMediaMessage as y };
1236
+ export { toFlexMessage as C, createVideoMessage as D, createImageMessage as E, fitsLineFlexBubble as S, createAudioMessage as T, normalizeLineAction as _, probeLineBot as a, truncateLineActionLabel as b, createAgendaCard as c, createActionCard as d, createImageCard as f, messageAction as g, datetimePickerAction as h, runLinePushWithRetries as i, createEventCard as l, createListCard as m, findLineHttpError as n, resolveLineChannelAccessToken as o, createInfoCard as p, resolveLineNonDispatchRetryable as r, createLineSendReceipt as s, explainLineRefusal as t, createReceiptCard as u, normalizeLineMessage as v, buildLineMediaMessage as w, uriAction as x, postbackAction as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/line",
3
- "version": "2026.8.2",
3
+ "version": "2026.9.1",
4
4
  "description": "OpenClaw LINE channel plugin for LINE Bot API chats.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,7 @@
13
13
  "zod": "4.4.3"
14
14
  },
15
15
  "peerDependencies": {
16
- "openclaw": ">=2026.8.2"
16
+ "openclaw": ">=2026.9.1"
17
17
  },
18
18
  "peerDependenciesMeta": {
19
19
  "openclaw": {
@@ -112,10 +112,10 @@
112
112
  "minHostVersion": ">=2026.4.10"
113
113
  },
114
114
  "compat": {
115
- "pluginApi": ">=2026.8.2"
115
+ "pluginApi": ">=2026.9.1"
116
116
  },
117
117
  "build": {
118
- "openclawVersion": "2026.8.2"
118
+ "openclawVersion": "2026.9.1"
119
119
  },
120
120
  "release": {
121
121
  "publishToClawHub": true,
@@ -1,2 +0,0 @@
1
- import { t as monitorLineProvider } from "./monitor-CSpwQCBI.js";
2
- export { monitorLineProvider };
@@ -1,28 +0,0 @@
1
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
2
- import { runChannelProbe } from "openclaw/plugin-sdk/text-utility-runtime";
3
- import { messagingApi } from "@line/bot-sdk";
4
- //#region extensions/line/src/probe.ts
5
- async function probeLineBot(channelAccessToken, timeoutMs = 5e3) {
6
- if (!channelAccessToken?.trim()) return {
7
- ok: false,
8
- error: "Channel access token not configured"
9
- };
10
- const client = new messagingApi.MessagingApiClient({ channelAccessToken: channelAccessToken.trim() });
11
- return await runChannelProbe(timeoutMs, async () => {
12
- const profile = await client.getBotInfo();
13
- return {
14
- ok: true,
15
- bot: {
16
- displayName: profile.displayName,
17
- userId: profile.userId,
18
- basicId: profile.basicId,
19
- pictureUrl: profile.pictureUrl
20
- }
21
- };
22
- }, (error) => ({
23
- ok: false,
24
- error: formatErrorMessage(error)
25
- }));
26
- }
27
- //#endregion
28
- export { probeLineBot as t };
@@ -1,2 +0,0 @@
1
- import { t as probeLineBot } from "./probe-4gc_YQ2L.js";
2
- export { probeLineBot };