@openclaw/line 2026.9.1-beta.1 → 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.
- package/dist/account-helpers-BSUyZwLm.js +12 -0
- package/dist/{accounts-BqEVsDoB.js → accounts-BBEFMYbY.js} +2 -1
- package/dist/api.js +2 -2
- package/dist/card-command-pwvE48Wb.js +252 -0
- package/dist/{channel-CsQLcrsA.js → channel-CF2AeULJ.js} +111 -380
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel.runtime-BF4Wk2dM.js +4 -0
- package/dist/contract-api.js +1 -1
- package/dist/doctor-contract-api.js +141 -0
- package/dist/index.js +5 -17
- package/dist/{markdown-to-line-BmZ4hhfC.js → markdown-to-line-B-sj-7II.js} +22 -23
- package/dist/{monitor-Bd9XDV1i.js → monitor-D0fHFXCi.js} +323 -234
- package/dist/monitor.runtime-Mgecav_D.js +2 -0
- package/dist/outbound.runtime-CGqyJ-Ug.js +3 -0
- package/dist/probe.runtime-CbtMgU4s.js +2 -0
- package/dist/rich-messages-DY1AGKAJ.js +742 -0
- package/dist/runtime-api.js +14 -15
- package/dist/{send-Dce0m539.js → send-CQQxpxPp.js} +142 -112
- package/dist/{send-receipt-CCPTalf9.js → send-retry-ChpfJ-LM.js} +326 -86
- package/dist/setup-api.js +1 -1
- package/dist/{setup-surface-W0Dbgu58.js → setup-surface-CDlgkTKi.js} +47 -39
- package/dist/webhook-spool-contract-1rY8OJw8.js +59 -0
- package/node_modules/typebox/build/format/email.mjs +1 -1
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
- package/node_modules/typebox/package.json +1 -1
- package/openclaw.plugin.json +16 -2
- package/package.json +5 -5
- package/dist/card-command-CmU47gD9.js +0 -230
- package/dist/channel.runtime-B83x7k_i.js +0 -4
- package/dist/media-control-cards-CksNB8Wk.js +0 -474
- package/dist/monitor.runtime-BCCa6qTX.js +0 -2
- package/dist/outbound.runtime-D4w0-Gkc.js +0 -3
- package/dist/probe-CM03wo6r.js +0 -28
- package/dist/probe.runtime-D13ZtXsd.js +0 -2
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
return
|
|
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 (
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
148
|
-
if (Array.isArray(value))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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
|
|
166
|
-
if (!isRecord
|
|
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 =
|
|
169
|
-
if (!isRecord
|
|
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
|
-
|
|
180
|
-
...
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
191
|
-
if (!isRecord
|
|
192
|
-
if (value.type === "bubble") return
|
|
193
|
-
if (value.type === "carousel" && Array.isArray(value.contents))
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
342
|
+
function normalizeLineMessage(message) {
|
|
252
343
|
let normalized;
|
|
253
344
|
if (message.type === "flex") normalized = {
|
|
254
345
|
...message,
|
|
255
|
-
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
|
|
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:
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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:
|
|
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 {
|
|
306
|
+
export { lineSetupAdapter as n, lineSetupContract as r, lineSetupWizard as t };
|