@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,8 +1,24 @@
1
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
1
+ import { i as resolveLineAccount } from "./accounts-BBEFMYbY.js";
2
+ import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
2
3
  import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
4
+ import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
+ import { getFileExtension, mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
3
6
  import { resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
4
- import { isRecord as isRecord$1 } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
5
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
7
+ import { runChannelProbe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
8
+ import { HTTPFetchError } from "@line/bot-sdk";
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
6
22
  //#region extensions/line/src/outbound-media.ts
7
23
  const LINE_OUTBOUND_MEDIA_SSRF_POLICY = { allowPrivateNetwork: false };
8
24
  async function validateLineMediaUrl(url) {
@@ -16,22 +32,39 @@ async function validateLineMediaUrl(url) {
16
32
  if (url.length > 2e3) throw new Error(`LINE outbound media URL must be 2000 chars or less (got ${url.length})`);
17
33
  await resolvePinnedHostnameWithPolicy(parsed.hostname, { policy: LINE_OUTBOUND_MEDIA_SSRF_POLICY });
18
34
  }
19
- function isHttpsUrl(url) {
20
- try {
21
- return new URL(url).protocol === "https:";
22
- } catch {
23
- return false;
24
- }
25
- }
35
+ const LINE_MEDIA_KIND_BY_MIME = {
36
+ "image/jpeg": "image",
37
+ "image/png": "image",
38
+ "video/mp4": "video",
39
+ "audio/mpeg": "audio",
40
+ "audio/x-m4a": "audio"
41
+ };
26
42
  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
- }
43
+ const mimeType = mimeTypeFromFilePath(url);
44
+ if (mimeType === void 0) return getFileExtension(url) === void 0 ? void 0 : "unsupported";
45
+ return LINE_MEDIA_KIND_BY_MIME[mimeType] ?? "unsupported";
46
+ }
47
+ function resolveLineMediaKind(url, opts) {
48
+ if (opts.mediaKind !== void 0) return {
49
+ mediaKind: opts.mediaKind,
50
+ kindSource: "declared"
51
+ };
52
+ if (typeof opts.durationMs === "number") return {
53
+ mediaKind: "audio",
54
+ kindSource: "metadata"
55
+ };
56
+ if (opts.trackingId?.trim()) return {
57
+ mediaKind: "video",
58
+ kindSource: "metadata"
59
+ };
60
+ const detected = detectLineMediaKindFromUrl(url);
61
+ return detected === void 0 ? {
62
+ mediaKind: "image",
63
+ kindSource: "fallback"
64
+ } : {
65
+ mediaKind: detected,
66
+ kindSource: "url"
67
+ };
35
68
  }
36
69
  async function resolveLineOutboundMedia(mediaUrl, opts = {}) {
37
70
  const trimmedUrl = mediaUrl.trim();
@@ -41,7 +74,7 @@ async function resolveLineOutboundMedia(mediaUrl, opts = {}) {
41
74
  if (previewImageUrl) await validateLineMediaUrl(previewImageUrl);
42
75
  return {
43
76
  mediaUrl: trimmedUrl,
44
- mediaKind: opts.mediaKind ?? (typeof opts.durationMs === "number" ? "audio" : void 0) ?? (opts.trackingId?.trim() ? "video" : void 0) ?? detectLineMediaKindFromUrl(trimmedUrl) ?? "image",
77
+ ...resolveLineMediaKind(trimmedUrl, opts),
45
78
  ...previewImageUrl ? { previewImageUrl } : {},
46
79
  ...typeof opts.durationMs === "number" ? { durationMs: opts.durationMs } : {},
47
80
  ...opts.trackingId ? { trackingId: opts.trackingId } : {}
@@ -58,37 +91,65 @@ function isLineUserTarget(target) {
58
91
  const normalized = target.trim().replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
59
92
  return /^U/i.test(normalized);
60
93
  }
61
- function hasLineSpecificMediaOptions(lineData) {
62
- return lineData.mediaKind !== void 0 || Boolean(lineData.previewImageUrl?.trim()) || typeof lineData.durationMs === "number" || Boolean(lineData.trackingId?.trim());
94
+ function createImageMessage(originalContentUrl, previewImageUrl) {
95
+ return {
96
+ type: "image",
97
+ originalContentUrl,
98
+ previewImageUrl: previewImageUrl ?? originalContentUrl
99
+ };
100
+ }
101
+ function createVideoMessage(originalContentUrl, previewImageUrl, trackingId) {
102
+ return {
103
+ type: "video",
104
+ originalContentUrl,
105
+ previewImageUrl,
106
+ ...trackingId ? { trackingId } : {}
107
+ };
108
+ }
109
+ function createAudioMessage(originalContentUrl, durationMs) {
110
+ return {
111
+ type: "audio",
112
+ originalContentUrl,
113
+ duration: durationMs
114
+ };
115
+ }
116
+ function lineMediaUrlFallback(mediaUrl) {
117
+ return {
118
+ type: "text",
119
+ text: mediaUrl
120
+ };
63
121
  }
64
122
  function buildLineMediaMessageObject(resolved, opts) {
65
123
  switch (resolved.mediaKind) {
124
+ case "unsupported": return lineMediaUrlFallback(resolved.mediaUrl);
66
125
  case "video": {
67
126
  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
- };
127
+ if (previewImageUrl) return createVideoMessage(resolved.mediaUrl, previewImageUrl, opts?.allowTrackingId ? resolved.trackingId : void 0);
128
+ if (resolved.kindSource !== "url") throw new Error("LINE video messages require previewImageUrl to reference an image URL");
129
+ return lineMediaUrlFallback(resolved.mediaUrl);
75
130
  }
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
- };
131
+ case "audio": return createAudioMessage(resolved.mediaUrl, resolved.durationMs ?? 6e4);
132
+ default: return createImageMessage(resolved.mediaUrl, resolved.previewImageUrl);
86
133
  }
87
134
  }
88
135
  async function buildLineMediaMessage(mediaUrl, opts, target) {
89
136
  return buildLineMediaMessageObject(await resolveLineOutboundMedia(mediaUrl, opts), { allowTrackingId: isLineUserTarget(target) });
90
137
  }
91
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
92
153
  //#region extensions/line/src/actions.ts
93
154
  const LINE_ACTION_LABEL_LIMIT = 20;
94
155
  const LINE_ACTION_DATA_LIMIT = 300;
@@ -117,6 +178,17 @@ function truncateLineActionLabel(label, limit = LINE_ACTION_LABEL_LIMIT) {
117
178
  function truncateLineActionData(data) {
118
179
  return truncateUtf16Safe(data, LINE_ACTION_DATA_LIMIT);
119
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
+ }
120
192
  const unavailableActionMarker = Symbol("lineUnavailableAction");
121
193
  function unavailableAction(kind, reason) {
122
194
  const action = {
@@ -139,18 +211,18 @@ const actionTypes = /* @__PURE__ */ new Set([
139
211
  "uri"
140
212
  ]);
141
213
  function isLineAction(value) {
142
- return isRecord$1(value) && typeof value.type === "string" && actionTypes.has(value.type);
214
+ return isRecord(value) && typeof value.type === "string" && actionTypes.has(value.type);
143
215
  }
144
216
  function isUnavailableAction(action) {
145
217
  return action[unavailableActionMarker] === true;
146
218
  }
147
- function normalizeNestedActions(value, labelLimit, warnings) {
148
- if (Array.isArray(value)) {
149
- const normalized = [];
150
- for (const item of value) normalized.push(normalizeNestedActions(item, labelLimit, warnings));
151
- return normalized;
219
+ function normalizeNestedContent(value, labelLimit, warnings) {
220
+ if (Array.isArray(value)) return value.map((item) => normalizeNestedContent(item, labelLimit, warnings)).filter((item) => item !== void 0);
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;
152
225
  }
153
- if (!isRecord$1(value)) return value;
154
226
  const normalized = { ...value };
155
227
  for (const [key, nested] of Object.entries(value)) if ((key === "action" || key === "defaultAction") && isLineAction(nested)) {
156
228
  const action = normalizeLineAction(nested, labelLimit);
@@ -159,42 +231,61 @@ function normalizeNestedActions(value, labelLimit, warnings) {
159
231
  warnings.push(isUnavailableAction(action) ? action.text ?? "Action unavailable." : "Action unavailable in this video.");
160
232
  } else normalized[key] = action;
161
233
  } else if (key === "actions" && Array.isArray(nested)) normalized[key] = nested.map((action) => isLineAction(action) ? normalizeLineAction(action, labelLimit) : action);
162
- 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
+ }
163
251
  return normalized;
164
252
  }
165
- function normalizeFlexBubbleActions(value) {
166
- if (!isRecord$1(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);
167
255
  const warnings = [];
168
- const normalized = normalizeNestedActions(value, 40, warnings);
169
- if (!isRecord$1(normalized) || warnings.length === 0) return normalized;
170
- const warning = {
171
- type: "text",
172
- text: [...new Set(warnings)].join("\n"),
173
- wrap: true,
174
- size: "sm",
175
- color: "#B45309",
176
- margin: "md"
177
- };
256
+ const normalized = normalizeNestedContent(value, 40, warnings);
257
+ if (!isRecord(normalized) || warnings.length === 0) return normalized;
258
+ const warning = flexWarning([...new Set(warnings)].join("\n"));
178
259
  const body = normalized.body;
179
- if (isRecord$1(body) && Array.isArray(body.contents)) normalized.body = {
180
- ...body,
181
- contents: [...body.contents, warning]
182
- };
183
- else normalized.body = {
184
- type: "box",
185
- layout: "vertical",
186
- 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
+ }
187
270
  };
188
- return normalized;
271
+ return Buffer.byteLength(JSON.stringify(withWarning), "utf8") <= Math.min(maxBytes, 3e4) ? withWarning : normalized;
189
272
  }
190
- function normalizeFlexContainerActions(value) {
191
- if (!isRecord$1(value)) return value;
192
- if (value.type === "bubble") return normalizeFlexBubbleActions(value);
193
- if (value.type === "carousel" && Array.isArray(value.contents)) return {
194
- ...value,
195
- contents: value.contents.map((bubble) => normalizeFlexBubbleActions(bubble))
196
- };
197
- return normalizeNestedActions(value, 40);
273
+ function normalizeFlexContainer(value) {
274
+ if (!isRecord(value)) return value;
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);
198
289
  }
199
290
  function unavailableImagemapAction(kind, reason, area) {
200
291
  return {
@@ -248,17 +339,20 @@ function normalizeImagemapVideo(video) {
248
339
  }
249
340
  } };
250
341
  }
251
- function normalizeLineMessageActions(message) {
342
+ function normalizeLineMessage(message) {
252
343
  let normalized;
253
344
  if (message.type === "flex") normalized = {
254
345
  ...message,
255
- contents: normalizeFlexContainerActions(message.contents)
346
+ contents: normalizeFlexContainer(message.contents)
256
347
  };
257
348
  else if (message.type === "template") {
258
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;
259
353
  normalized = {
260
354
  ...message,
261
- template: normalizeNestedActions(message.template, labelLimit)
355
+ template
262
356
  };
263
357
  } else if (message.type === "imagemap") {
264
358
  const actions = message.actions.map(normalizeImagemapAction);
@@ -274,7 +368,7 @@ function normalizeLineMessageActions(message) {
274
368
  } else normalized = { ...message };
275
369
  if (message.quickReply) normalized = {
276
370
  ...normalized,
277
- quickReply: normalizeNestedActions(message.quickReply, 20)
371
+ quickReply: normalizeNestedContent(message.quickReply, 20)
278
372
  };
279
373
  return normalized;
280
374
  }
@@ -444,7 +538,7 @@ function createInfoCard(title, body, footer) {
444
538
  flex: 1,
445
539
  margin: "lg"
446
540
  }]
447
- }, {
541
+ }, ...body ? [{
448
542
  type: "box",
449
543
  layout: "vertical",
450
544
  contents: [{
@@ -459,7 +553,7 @@ function createInfoCard(title, body, footer) {
459
553
  paddingAll: "lg",
460
554
  backgroundColor: "#F8F9FA",
461
555
  cornerRadius: "lg"
462
- }],
556
+ }] : []],
463
557
  paddingAll: "xl",
464
558
  backgroundColor: "#FFFFFF"
465
559
  }
@@ -604,14 +698,14 @@ function createActionCard(title, body, actions, options) {
604
698
  weight: "bold",
605
699
  size: "xl",
606
700
  wrap: true
607
- }, {
701
+ }, ...body ? [{
608
702
  type: "text",
609
703
  text: body,
610
704
  size: "md",
611
705
  wrap: true,
612
706
  margin: "md",
613
707
  color: "#666666"
614
- }],
708
+ }] : []],
615
709
  paddingAll: "lg"
616
710
  },
617
711
  footer: {
@@ -702,7 +796,7 @@ function createReceiptCard(params) {
702
796
  weight: item.highlight ? "bold" : "regular",
703
797
  flex: 3,
704
798
  wrap: true
705
- }, {
799
+ }, ...item.value ? [{
706
800
  type: "text",
707
801
  text: item.value,
708
802
  size: "sm",
@@ -711,7 +805,7 @@ function createReceiptCard(params) {
711
805
  flex: 2,
712
806
  align: "end",
713
807
  wrap: true
714
- }],
808
+ }] : []],
715
809
  paddingAll: "md",
716
810
  backgroundColor: index % 2 === 0 ? "#FFFFFF" : "#FAFAFA"
717
811
  }));
@@ -993,4 +1087,150 @@ function createLineSendReceipt(params) {
993
1087
  });
994
1088
  }
995
1089
  //#endregion
996
- export { hasLineSpecificMediaOptions as _, createActionCard as a, createListCard as c, normalizeLineAction as d, normalizeLineMessageActions as f, buildLineMediaMessage as g, uriAction as h, createReceiptCard as i, datetimePickerAction as l, truncateLineActionLabel as m, createAgendaCard as n, createImageCard as o, postbackAction as p, createEventCard as r, createInfoCard as s, createLineSendReceipt as t, messageAction as u, resolveLineOutboundMedia as v, validateLineMediaUrl as y };
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
1174
+ //#region extensions/line/src/send-retry.ts
1175
+ /** The LINE HTTP response carried by an error graph, when the request reached LINE. */
1176
+ function findLineHttpError(error) {
1177
+ return collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]).find((candidate) => candidate instanceof HTTPFetchError);
1178
+ }
1179
+ /**
1180
+ * LINE answered this attempt with a client error, so it rejected the request and
1181
+ * sent nothing.
1182
+ *
1183
+ * A 429 proves this attempt was refused but remains retryable by durable
1184
+ * delivery. A 408 stays ambiguous because the request may have reached LINE.
1185
+ */
1186
+ function resolveAttemptNonDispatchRetryable(error) {
1187
+ const status = findLineHttpError(error)?.status;
1188
+ if (status === 429) return true;
1189
+ if (status === 409) return;
1190
+ return status !== void 0 && status >= 400 && status < 500 && status !== 408 ? false : void 0;
1191
+ }
1192
+ const pushErrorsWithAmbiguousAttempt = /* @__PURE__ */ new WeakSet();
1193
+ /** Retryability when LINE refused every attempt, or undefined when delivery is ambiguous. */
1194
+ function resolveLineNonDispatchRetryable(error) {
1195
+ if (collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]).some((candidate) => typeof candidate === "object" && candidate !== null && pushErrorsWithAmbiguousAttempt.has(candidate))) return;
1196
+ return resolveAttemptNonDispatchRetryable(error);
1197
+ }
1198
+ function isRetryableLinePushError(error) {
1199
+ const httpError = findLineHttpError(error);
1200
+ if (httpError) return httpError.status >= 500;
1201
+ return collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]).some((candidate) => classifyTransientNetworkErrorCode(extractErrorCode(candidate)) !== void 0);
1202
+ }
1203
+ /**
1204
+ * Pushes are non-idempotent without a retry key, so the generic message-matching
1205
+ * fallback stays off and only the classification above may replay a request.
1206
+ */
1207
+ const runLinePushAttempts = createChannelApiRetryRunner({
1208
+ shouldRetry: isRetryableLinePushError,
1209
+ strictShouldRetry: true,
1210
+ verbose: true
1211
+ });
1212
+ const runLinePushWithRetries = (fn, label) => {
1213
+ let sawAmbiguousAttempt = false;
1214
+ return runLinePushAttempts(async () => {
1215
+ try {
1216
+ return await fn();
1217
+ } catch (error) {
1218
+ sawAmbiguousAttempt ||= resolveAttemptNonDispatchRetryable(error) === void 0;
1219
+ throw error;
1220
+ }
1221
+ }, label).catch((error) => {
1222
+ if (sawAmbiguousAttempt && typeof error === "object" && error !== null) pushErrorsWithAmbiguousAttempt.add(error);
1223
+ throw error;
1224
+ });
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
+ }
1235
+ //#endregion
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/dist/setup-api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-W0Dbgu58.js";
1
+ import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-CDlgkTKi.js";
2
2
  export { lineSetupAdapter, lineSetupWizard };
@@ -1,19 +1,9 @@
1
- import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BqEVsDoB.js";
1
+ import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BBEFMYbY.js";
2
+ import { n as parseLineAllowFromId, t as hasLineCredentials } from "./account-helpers-BSUyZwLm.js";
2
3
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
3
4
  import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
4
5
  import { DEFAULT_ACCOUNT_ID, createAllowFromSection, createPromptParsedAllowFromForAccount, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, parseSetupEntriesWithParser, patchScopedAccountConfig, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
5
6
  import { createChannelDmPolicy } from "openclaw/plugin-sdk/channel-dm-policy";
6
- //#region extensions/line/src/account-helpers.ts
7
- function hasLineCredentials(account) {
8
- if (account.tokenStatus && account.signingSecretStatus) return account.tokenStatus !== "missing" && account.signingSecretStatus !== "missing";
9
- return Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim());
10
- }
11
- function parseLineAllowFromId(raw) {
12
- const trimmed = raw.trim().replace(/^line:(?:user:)?/i, "");
13
- if (!/^U[a-f0-9]{32}$/i.test(trimmed)) return null;
14
- return trimmed;
15
- }
16
- //#endregion
17
7
  //#region extensions/line/src/setup-core.ts
18
8
  function patchLineAccountConfig(params) {
19
9
  return patchScopedAccountConfig({
@@ -62,6 +52,26 @@ const lineSetupAdapter = {
62
52
  const accessToken = typedInput.channelAccessToken ?? typedInput.token;
63
53
  const normalizedAccountId = normalizeAccountId(accountId);
64
54
  const useEnv = normalizedAccountId === DEFAULT_ACCOUNT_ID && Boolean(typedInput.useEnv);
55
+ const credentials = [{
56
+ fileKey: "tokenFile",
57
+ file: typedInput.tokenFile,
58
+ inlineKey: "channelAccessToken",
59
+ inline: accessToken
60
+ }, {
61
+ fileKey: "secretFile",
62
+ file: typedInput.secretFile,
63
+ inlineKey: "channelSecret",
64
+ inline: typedInput.channelSecret
65
+ }];
66
+ const patch = {};
67
+ const retired = [];
68
+ for (const credential of credentials) if (credential.file) {
69
+ patch[credential.fileKey] = credential.file;
70
+ retired.push(credential.inlineKey);
71
+ } else if (credential.inline) {
72
+ patch[credential.inlineKey] = credential.inline;
73
+ retired.push(credential.fileKey);
74
+ }
65
75
  return patchLineAccountConfig({
66
76
  cfg,
67
77
  accountId: normalizedAccountId,
@@ -71,11 +81,8 @@ const lineSetupAdapter = {
71
81
  "channelSecret",
72
82
  "tokenFile",
73
83
  "secretFile"
74
- ] : void 0,
75
- patch: useEnv ? {} : {
76
- ...typedInput.tokenFile ? { tokenFile: typedInput.tokenFile } : accessToken ? { channelAccessToken: accessToken } : {},
77
- ...typedInput.secretFile ? { secretFile: typedInput.secretFile } : typedInput.channelSecret ? { channelSecret: typedInput.channelSecret } : {}
78
- }
84
+ ] : retired.length > 0 ? retired : void 0,
85
+ patch: useEnv ? {} : patch
79
86
  });
80
87
  }
81
88
  };
@@ -152,6 +159,27 @@ const LINE_ALLOW_FROM_HELP_LINES = [
152
159
  t("wizard.line.multipleEntries"),
153
160
  t("wizard.channels.docs", { link: formatDocsLink("/channels/line", "channels/line") })
154
161
  ];
162
+ const promptLineAllowFrom = createPromptParsedAllowFromForAccount({
163
+ defaultAccountId: resolveDefaultLineAccountId,
164
+ noteTitle: t("wizard.line.allowlistTitle"),
165
+ noteLines: LINE_ALLOW_FROM_HELP_LINES,
166
+ message: t("wizard.line.allowFromPrompt"),
167
+ placeholder: "U1234567890abcdef1234567890abcdef",
168
+ parseEntries: (raw) => parseSetupEntriesWithParser(raw, (entry) => {
169
+ const id = parseLineAllowFromId(entry);
170
+ return id ? { value: id } : { error: t("wizard.line.allowFromInvalid") };
171
+ }),
172
+ getExistingAllowFrom: ({ cfg, accountId }) => resolveLineAccount({
173
+ cfg,
174
+ accountId
175
+ }).config.allowFrom ?? [],
176
+ applyAllowFrom: ({ cfg, accountId, allowFrom }) => patchLineAccountConfig({
177
+ cfg,
178
+ accountId,
179
+ enabled: true,
180
+ patch: { allowFrom }
181
+ })
182
+ });
155
183
  const lineDmPolicy = createChannelDmPolicy({
156
184
  label: "LINE",
157
185
  channel,
@@ -166,27 +194,7 @@ const lineDmPolicy = createChannelDmPolicy({
166
194
  patch,
167
195
  clearFields: patch.dmPolicy === "pairing" || patch.dmPolicy === "disabled" ? ["allowFrom"] : void 0
168
196
  }),
169
- promptAllowFrom: createPromptParsedAllowFromForAccount({
170
- defaultAccountId: resolveDefaultLineAccountId,
171
- noteTitle: t("wizard.line.allowlistTitle"),
172
- noteLines: LINE_ALLOW_FROM_HELP_LINES,
173
- message: t("wizard.line.allowFromPrompt"),
174
- placeholder: "U1234567890abcdef1234567890abcdef",
175
- parseEntries: (raw) => parseSetupEntriesWithParser(raw, (entry) => {
176
- const id = parseLineAllowFromId(entry);
177
- return id ? { value: id } : { error: t("wizard.line.allowFromInvalid") };
178
- }),
179
- getExistingAllowFrom: ({ cfg, accountId }) => resolveLineAccount({
180
- cfg,
181
- accountId
182
- }).config.allowFrom ?? [],
183
- applyAllowFrom: ({ cfg, accountId, allowFrom }) => patchLineAccountConfig({
184
- cfg,
185
- accountId,
186
- enabled: true,
187
- patch: { allowFrom }
188
- })
189
- })
197
+ promptAllowFrom: promptLineAllowFrom
190
198
  });
191
199
  function createLineTokenCredential(params) {
192
200
  return defineTokenCredential({
@@ -295,4 +303,4 @@ const lineSetupWizard = {
295
303
  disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
296
304
  };
297
305
  //#endregion
298
- export { hasLineCredentials as i, lineSetupAdapter as n, lineSetupContract as r, lineSetupWizard as t };
306
+ export { lineSetupAdapter as n, lineSetupContract as r, lineSetupWizard as t };