@openclaw/line 2026.7.1 → 2026.7.2-beta.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.
- package/dist/api.js +2 -2
- package/dist/{card-command--jtkAqp8.js → card-command-DQ-epsgt.js} +7 -3
- package/dist/{channel-C_8hLniz.js → channel-BHR-GoJ0.js} +63 -84
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel.runtime-DFgZ_Xwn.js +4 -0
- package/dist/{flex-templates-DEIXn69v.js → flex-templates-K1lS9i8L.js} +2 -3
- package/dist/index.js +1 -1
- package/dist/{markdown-to-line-DmpZY78n.js → markdown-to-line-XceqeKRP.js} +20 -20
- package/dist/{monitor-0eXlx_Pe.js → monitor-Db2rZsSX.js} +283 -228
- package/dist/monitor.runtime-CZuQgsEE.js +2 -0
- package/dist/{outbound.runtime-BwC7Y2U9.js → outbound.runtime-DnpU0Cms.js} +1 -1
- package/dist/{reply-payload-transform-C7r5npxP.js → reply-payload-transform-r9sa-TYT.js} +121 -84
- package/dist/runtime-api.js +8 -34
- package/dist/setup-api.js +1 -1
- package/dist/{setup-surface-Bc36YSS3.js → setup-surface-B7W6pwn3.js} +36 -44
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +57 -57
- package/package.json +4 -4
- package/dist/channel.runtime-Bh_AQM0k.js +0 -4
- package/dist/monitor.runtime-BGIpxzfB.js +0 -2
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-DJVOv1JI.js";
|
|
2
|
-
import {
|
|
3
|
-
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage,
|
|
2
|
+
import { a as buildLineMediaMessage, i as buildLineQuickReplyFallbackText, l as getLineRuntime, m as resolveLineGroupConfigEntry, o as hasLineSpecificMediaOptions } from "./reply-payload-transform-r9sa-TYT.js";
|
|
3
|
+
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-XceqeKRP.js";
|
|
4
4
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
5
5
|
import { normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
6
|
import { createMessageReceiveContext } from "openclaw/plugin-sdk/channel-outbound";
|
|
7
7
|
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
8
|
+
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
8
9
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
10
|
+
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
9
11
|
import { firstDefined } from "openclaw/plugin-sdk/allow-from";
|
|
10
|
-
import {
|
|
12
|
+
import { Readable } from "node:stream";
|
|
13
|
+
import { finished } from "node:stream/promises";
|
|
14
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
11
15
|
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
12
16
|
import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
|
|
17
|
+
import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
|
|
13
18
|
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
|
14
19
|
import { buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
15
20
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
16
21
|
import { isRequestBodyLimitError, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
|
|
17
|
-
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1 } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
22
|
+
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1, runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
18
23
|
import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
|
|
19
24
|
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
20
25
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
21
|
-
import {
|
|
26
|
+
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
|
|
22
27
|
import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
23
|
-
import {
|
|
28
|
+
import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
24
29
|
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
25
30
|
import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
26
31
|
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
|
27
32
|
import crypto from "node:crypto";
|
|
33
|
+
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
28
34
|
//#region extensions/line/src/bot-access.ts
|
|
29
35
|
function normalizeLineAllowEntry(value) {
|
|
30
36
|
const trimmed = String(value).trim();
|
|
@@ -42,8 +48,51 @@ const normalizeAllowFrom = (list) => {
|
|
|
42
48
|
};
|
|
43
49
|
//#endregion
|
|
44
50
|
//#region extensions/line/src/download.ts
|
|
51
|
+
const CONTENT_READY_MAX_ATTEMPTS = 6;
|
|
52
|
+
const CONTENT_READY_BASE_DELAY_MS = 500;
|
|
53
|
+
const CONTENT_READY_MAX_DELAY_MS = 4e3;
|
|
54
|
+
const CONTENT_READY_TIMEOUT_MS = 15e3;
|
|
55
|
+
const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
|
|
56
|
+
function contentBackoffDelayMs(attempt) {
|
|
57
|
+
return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
|
|
58
|
+
}
|
|
59
|
+
async function fetchLineContentWhenReady(messageId, channelAccessToken) {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
|
|
62
|
+
deadline.unref();
|
|
63
|
+
try {
|
|
64
|
+
for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
|
|
65
|
+
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(`${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`, {
|
|
66
|
+
headers: { Authorization: `Bearer ${channelAccessToken}` },
|
|
67
|
+
redirect: "error",
|
|
68
|
+
signal: controller.signal
|
|
69
|
+
});
|
|
70
|
+
if (response.status === 200) {
|
|
71
|
+
if (!response.body) throw new Error(`LINE media response for message ${messageId} had no body`);
|
|
72
|
+
return Readable.fromWeb(response.body);
|
|
73
|
+
}
|
|
74
|
+
await response.body?.cancel();
|
|
75
|
+
if (response.status !== 202) throw new Error(`LINE media download failed for message ${messageId} (HTTP ${response.status})`);
|
|
76
|
+
if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (controller.signal.aborted) throw new Error(`LINE media for message ${messageId} did not become ready within ${CONTENT_READY_TIMEOUT_MS / 1e3} seconds`, { cause: err });
|
|
80
|
+
throw err;
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(deadline);
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`);
|
|
85
|
+
}
|
|
45
86
|
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
|
|
46
|
-
const
|
|
87
|
+
const content = await fetchLineContentWhenReady(messageId, channelAccessToken);
|
|
88
|
+
let saved;
|
|
89
|
+
try {
|
|
90
|
+
saved = await saveMediaStream(content, void 0, "inbound", maxBytes, options?.originalFilename);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
content.destroy();
|
|
93
|
+
await finished(content).catch(() => void 0);
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
47
96
|
logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
|
|
48
97
|
return {
|
|
49
98
|
path: saved.path,
|
|
@@ -53,15 +102,19 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
|
|
|
53
102
|
}
|
|
54
103
|
//#endregion
|
|
55
104
|
//#region extensions/line/src/auto-reply-delivery.ts
|
|
105
|
+
function toLineDeliveryError(error) {
|
|
106
|
+
return error instanceof Error ? error : new Error("LINE rich or media message send failed", { cause: error });
|
|
107
|
+
}
|
|
56
108
|
function markLineVisibleDeliveryError(error) {
|
|
57
|
-
|
|
58
|
-
|
|
109
|
+
const deliveryError = toLineDeliveryError(error);
|
|
110
|
+
if (Object.isExtensible(deliveryError)) {
|
|
111
|
+
Object.assign(deliveryError, {
|
|
59
112
|
sentBeforeError: true,
|
|
60
113
|
visibleReplySent: true
|
|
61
114
|
});
|
|
62
|
-
return
|
|
115
|
+
return deliveryError;
|
|
63
116
|
}
|
|
64
|
-
const visibleError = new Error("LINE rich or media message send failed", { cause:
|
|
117
|
+
const visibleError = new Error("LINE rich or media message send failed", { cause: deliveryError });
|
|
65
118
|
Object.assign(visibleError, {
|
|
66
119
|
sentBeforeError: true,
|
|
67
120
|
visibleReplySent: true
|
|
@@ -71,12 +124,26 @@ function markLineVisibleDeliveryError(error) {
|
|
|
71
124
|
async function deliverLineAutoReply(params) {
|
|
72
125
|
const { payload, lineData, replyToken, accountId, to, textLimit, deps } = params;
|
|
73
126
|
let replyTokenUsed = params.replyTokenUsed;
|
|
127
|
+
let visibleReplySent = false;
|
|
128
|
+
const sendVisible = async (send) => {
|
|
129
|
+
try {
|
|
130
|
+
const result = await send();
|
|
131
|
+
visibleReplySent = true;
|
|
132
|
+
return result;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (visibleReplySent) throw markLineVisibleDeliveryError(error);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const replyVisible = (...args) => sendVisible(() => deps.replyMessageLine(...args));
|
|
139
|
+
const pushTextVisible = (...args) => sendVisible(() => deps.pushMessageLine(...args));
|
|
140
|
+
const pushQuickRepliesVisible = (...args) => sendVisible(() => deps.pushTextMessageWithQuickReplies(...args));
|
|
74
141
|
const pushLineMessages = async (messages) => {
|
|
75
142
|
if (messages.length === 0) return;
|
|
76
|
-
for (let i = 0; i < messages.length; i += 5) await deps.pushMessagesLine(to, messages.slice(i, i + 5), {
|
|
143
|
+
for (let i = 0; i < messages.length; i += 5) await sendVisible(() => deps.pushMessagesLine(to, messages.slice(i, i + 5), {
|
|
77
144
|
cfg: params.cfg,
|
|
78
145
|
accountId
|
|
79
|
-
});
|
|
146
|
+
}));
|
|
80
147
|
};
|
|
81
148
|
const sendLineMessages = async (messages, allowReplyToken) => {
|
|
82
149
|
if (messages.length === 0) return;
|
|
@@ -84,7 +151,7 @@ async function deliverLineAutoReply(params) {
|
|
|
84
151
|
if (allowReplyToken && replyToken && !replyTokenUsed) {
|
|
85
152
|
const replyBatch = remaining.slice(0, 5);
|
|
86
153
|
try {
|
|
87
|
-
await
|
|
154
|
+
await replyVisible(replyToken, replyBatch, {
|
|
88
155
|
cfg: params.cfg,
|
|
89
156
|
accountId
|
|
90
157
|
});
|
|
@@ -105,21 +172,38 @@ async function deliverLineAutoReply(params) {
|
|
|
105
172
|
if (templateMsg) richMessages.push(templateMsg);
|
|
106
173
|
}
|
|
107
174
|
if (lineData.location) richMessages.push(deps.createLocationMessage(lineData.location));
|
|
108
|
-
const
|
|
175
|
+
const visibleText = payload.text ? sanitizeAssistantVisibleText(payload.text) : "";
|
|
176
|
+
const processed = visibleText ? deps.processLineMessage(visibleText) : {
|
|
109
177
|
text: "",
|
|
110
178
|
flexMessages: []
|
|
111
179
|
};
|
|
112
180
|
for (const flexMsg of processed.flexMessages) richMessages.push(deps.createFlexMessage(truncateUtf16Safe(flexMsg.altText, 400), flexMsg.contents));
|
|
113
181
|
const chunks = processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : [];
|
|
114
|
-
const
|
|
182
|
+
const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
|
|
183
|
+
const mediaOpts = {
|
|
184
|
+
mediaKind: hasLineSpecificMediaOptions(lineData) ? lineData.mediaKind : "image",
|
|
185
|
+
previewImageUrl: lineData.previewImageUrl,
|
|
186
|
+
durationMs: lineData.durationMs,
|
|
187
|
+
trackingId: lineData.trackingId
|
|
188
|
+
};
|
|
189
|
+
const mediaMessages = [];
|
|
190
|
+
let richMediaError;
|
|
191
|
+
for (const rawUrl of mediaUrls) {
|
|
192
|
+
const url = rawUrl?.trim();
|
|
193
|
+
if (!url) continue;
|
|
194
|
+
try {
|
|
195
|
+
mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
|
|
196
|
+
} catch (err) {
|
|
197
|
+
richMediaError ??= err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
115
200
|
if (chunks.length > 0) {
|
|
116
201
|
const hasRichOrMedia = richMessages.length > 0 || mediaMessages.length > 0;
|
|
117
202
|
const sendRichBeforeText = hasQuickReplies && hasRichOrMedia;
|
|
118
|
-
let richMediaError;
|
|
119
203
|
if (sendRichBeforeText) try {
|
|
120
204
|
await sendLineMessages([...richMessages, ...mediaMessages], false);
|
|
121
205
|
} catch (err) {
|
|
122
|
-
richMediaError
|
|
206
|
+
richMediaError ??= err;
|
|
123
207
|
}
|
|
124
208
|
const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
|
|
125
209
|
to,
|
|
@@ -129,23 +213,19 @@ async function deliverLineAutoReply(params) {
|
|
|
129
213
|
replyTokenUsed,
|
|
130
214
|
cfg: params.cfg,
|
|
131
215
|
accountId,
|
|
132
|
-
replyMessageLine:
|
|
133
|
-
pushMessageLine:
|
|
134
|
-
pushTextMessageWithQuickReplies:
|
|
135
|
-
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies
|
|
216
|
+
replyMessageLine: replyVisible,
|
|
217
|
+
pushMessageLine: pushTextVisible,
|
|
218
|
+
pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
|
|
219
|
+
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
|
|
220
|
+
onReplyError: deps.onReplyError
|
|
136
221
|
});
|
|
137
222
|
replyTokenUsed = nextReplyTokenUsed;
|
|
138
223
|
if (!sendRichBeforeText) try {
|
|
139
224
|
await sendLineMessages(richMessages, false);
|
|
140
225
|
if (mediaMessages.length > 0) await sendLineMessages(mediaMessages, false);
|
|
141
226
|
} catch (err) {
|
|
142
|
-
richMediaError
|
|
227
|
+
richMediaError ??= err;
|
|
143
228
|
}
|
|
144
|
-
if (richMediaError !== void 0) return {
|
|
145
|
-
status: "partial",
|
|
146
|
-
replyTokenUsed,
|
|
147
|
-
error: markLineVisibleDeliveryError(richMediaError)
|
|
148
|
-
};
|
|
149
229
|
} else {
|
|
150
230
|
const combined = [...richMessages, ...mediaMessages];
|
|
151
231
|
if (hasQuickReplies && combined.length === 0) {
|
|
@@ -157,9 +237,9 @@ async function deliverLineAutoReply(params) {
|
|
|
157
237
|
replyTokenUsed,
|
|
158
238
|
cfg: params.cfg,
|
|
159
239
|
accountId,
|
|
160
|
-
replyMessageLine:
|
|
161
|
-
pushMessageLine:
|
|
162
|
-
pushTextMessageWithQuickReplies:
|
|
240
|
+
replyMessageLine: replyVisible,
|
|
241
|
+
pushMessageLine: pushTextVisible,
|
|
242
|
+
pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
|
|
163
243
|
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
|
|
164
244
|
onReplyError: deps.onReplyError
|
|
165
245
|
});
|
|
@@ -176,9 +256,19 @@ async function deliverLineAutoReply(params) {
|
|
|
176
256
|
await sendLineMessages(combined, true);
|
|
177
257
|
}
|
|
178
258
|
}
|
|
259
|
+
if (richMediaError !== void 0) {
|
|
260
|
+
if (!visibleReplySent) throw toLineDeliveryError(richMediaError);
|
|
261
|
+
return {
|
|
262
|
+
status: "partial",
|
|
263
|
+
replyTokenUsed,
|
|
264
|
+
visibleReplySent: true,
|
|
265
|
+
error: markLineVisibleDeliveryError(richMediaError)
|
|
266
|
+
};
|
|
267
|
+
}
|
|
179
268
|
return {
|
|
180
269
|
status: "delivered",
|
|
181
|
-
replyTokenUsed
|
|
270
|
+
replyTokenUsed,
|
|
271
|
+
visibleReplySent
|
|
182
272
|
};
|
|
183
273
|
}
|
|
184
274
|
//#endregion
|
|
@@ -403,7 +493,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
403
493
|
sessionKey: params.route.sessionKey
|
|
404
494
|
});
|
|
405
495
|
if (shouldLogVerbose()) {
|
|
406
|
-
const preview = body
|
|
496
|
+
const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
|
|
407
497
|
const mediaInfo = params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1 ? ` mediaCount=${params.verboseLog.mediaCount}` : "";
|
|
408
498
|
logVerbose(`${params.verboseLog.kind === "inbound" ? "line inbound" : "line postback"}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
|
|
409
499
|
}
|
|
@@ -434,7 +524,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
434
524
|
};
|
|
435
525
|
}
|
|
436
526
|
async function buildLineMessageContext(params) {
|
|
437
|
-
const { event, allMedia, mediaUnavailable, cfg, account, commandAuthorized,
|
|
527
|
+
const { event, allMedia, mediaUnavailable, cfg, account, commandAuthorized, inboundHistory } = params;
|
|
438
528
|
const source = event.source;
|
|
439
529
|
const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
|
|
440
530
|
source,
|
|
@@ -464,11 +554,6 @@ async function buildLineMessageContext(params) {
|
|
|
464
554
|
address: loc.address
|
|
465
555
|
});
|
|
466
556
|
}
|
|
467
|
-
const historyKey = isGroup ? peerId : void 0;
|
|
468
|
-
const inboundHistory = historyKey && groupHistories && (historyLimit ?? 0) > 0 ? createChannelHistoryWindow({ historyMap: groupHistories }).buildInboundHistory({
|
|
469
|
-
historyKey,
|
|
470
|
-
limit: historyLimit ?? 0
|
|
471
|
-
}) : void 0;
|
|
472
557
|
const finalized = await finalizeLineInboundContext({
|
|
473
558
|
cfg,
|
|
474
559
|
account,
|
|
@@ -569,6 +654,41 @@ async function buildLinePostbackContext(params) {
|
|
|
569
654
|
};
|
|
570
655
|
}
|
|
571
656
|
//#endregion
|
|
657
|
+
//#region extensions/line/src/group-history.ts
|
|
658
|
+
const reservedEntries = /* @__PURE__ */ new WeakSet();
|
|
659
|
+
function reserveLineGroupHistory(historyMap, historyKey, limit) {
|
|
660
|
+
if (!historyMap || !historyKey || limit <= 0) return {
|
|
661
|
+
commit: () => {},
|
|
662
|
+
release: () => {}
|
|
663
|
+
};
|
|
664
|
+
const consumedEntries = (historyMap.get(historyKey) ?? []).filter((entry) => !reservedEntries.has(entry));
|
|
665
|
+
for (const entry of consumedEntries) reservedEntries.add(entry);
|
|
666
|
+
const inboundHistory = createChannelHistoryWindow({ historyMap: /* @__PURE__ */ new Map([[historyKey, consumedEntries]]) }).buildInboundHistory({
|
|
667
|
+
historyKey,
|
|
668
|
+
limit
|
|
669
|
+
});
|
|
670
|
+
let settled = false;
|
|
671
|
+
const settle = () => {
|
|
672
|
+
if (settled) return false;
|
|
673
|
+
settled = true;
|
|
674
|
+
for (const entry of consumedEntries) reservedEntries.delete(entry);
|
|
675
|
+
return true;
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
inboundHistory,
|
|
679
|
+
commit: () => {
|
|
680
|
+
if (!settle() || consumedEntries.length === 0) return;
|
|
681
|
+
const consumed = new Set(consumedEntries);
|
|
682
|
+
const kept = (historyMap.get(historyKey) ?? []).filter((entry) => !consumed.has(entry));
|
|
683
|
+
if (kept.length > 0) historyMap.set(historyKey, kept);
|
|
684
|
+
else historyMap.delete(historyKey);
|
|
685
|
+
},
|
|
686
|
+
release: () => {
|
|
687
|
+
settle();
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
572
692
|
//#region extensions/line/src/bot-handlers.ts
|
|
573
693
|
const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
574
694
|
"image",
|
|
@@ -584,16 +704,13 @@ const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
|
|
|
584
704
|
function normalizeLineIngressEntry(value) {
|
|
585
705
|
return normalizeLineAllowEntry(value) || null;
|
|
586
706
|
}
|
|
587
|
-
var LineRetryableWebhookError = class extends Error {
|
|
588
|
-
constructor(message, options) {
|
|
589
|
-
super(message, options);
|
|
590
|
-
this.name = "LineRetryableWebhookError";
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
707
|
function createLineWebhookReplayCache() {
|
|
594
|
-
return
|
|
595
|
-
|
|
596
|
-
|
|
708
|
+
return createChannelReplayGuard({
|
|
709
|
+
dedupe: {
|
|
710
|
+
ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
|
|
711
|
+
memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
|
|
712
|
+
},
|
|
713
|
+
buildReplayKey: ({ event, accountId }) => buildLineWebhookReplayKey(event, accountId)?.key
|
|
597
714
|
});
|
|
598
715
|
}
|
|
599
716
|
function buildLineWebhookReplayKey(event, accountId) {
|
|
@@ -613,29 +730,6 @@ function buildLineWebhookReplayKey(event, accountId) {
|
|
|
613
730
|
eventId: `event:${eventId}`
|
|
614
731
|
};
|
|
615
732
|
}
|
|
616
|
-
function getLineReplayCandidate(event, context) {
|
|
617
|
-
const replay = buildLineWebhookReplayKey(event, context.account.accountId);
|
|
618
|
-
const cache = context.replayCache;
|
|
619
|
-
if (!replay || !cache) return null;
|
|
620
|
-
return {
|
|
621
|
-
key: replay.key,
|
|
622
|
-
eventId: replay.eventId,
|
|
623
|
-
cache
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
async function claimLineReplayEvent(candidate) {
|
|
627
|
-
const claim = await candidate.cache.claim(candidate.key);
|
|
628
|
-
if (claim.kind === "claimed") return { skip: false };
|
|
629
|
-
if (claim.kind === "inflight") {
|
|
630
|
-
logVerbose(`line: skipped in-flight replayed webhook event ${candidate.eventId}`);
|
|
631
|
-
return {
|
|
632
|
-
skip: true,
|
|
633
|
-
inFlightResult: claim.pending.then(() => void 0)
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
logVerbose(`line: skipped replayed webhook event ${candidate.eventId}`);
|
|
637
|
-
return { skip: true };
|
|
638
|
-
}
|
|
639
733
|
function resolveLineGroupConfig(params) {
|
|
640
734
|
return resolveLineGroupConfigEntry(params.config.groups, {
|
|
641
735
|
groupId: params.groupId,
|
|
@@ -710,7 +804,7 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
710
804
|
defaultGroupPolicy: resolveDefaultGroupPolicy(cfg)
|
|
711
805
|
});
|
|
712
806
|
const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
|
|
713
|
-
const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom
|
|
807
|
+
const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom));
|
|
714
808
|
const mentionFacts = (() => {
|
|
715
809
|
if (!isGroup || event.type !== "message") return {
|
|
716
810
|
canDetectMention: false,
|
|
@@ -775,7 +869,7 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
775
869
|
allowFrom: normalizeStringEntries(account.config.allowFrom),
|
|
776
870
|
groupAllowFrom,
|
|
777
871
|
command: {
|
|
778
|
-
hasControlCommand:
|
|
872
|
+
hasControlCommand: hasControlCommand(rawText, cfg),
|
|
779
873
|
groupOwnerAllowFrom: "none"
|
|
780
874
|
}
|
|
781
875
|
});
|
|
@@ -873,42 +967,41 @@ async function handleMessageEvent(event, context) {
|
|
|
873
967
|
});
|
|
874
968
|
return;
|
|
875
969
|
}
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
logVerbose("line: skipping empty message");
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
await processMessage(messageContext);
|
|
906
|
-
if (isGroup && context.groupHistories) {
|
|
907
|
-
const historyKey = groupId ?? roomId;
|
|
908
|
-
if (historyKey && context.groupHistories.has(historyKey)) createChannelHistoryWindow({ historyMap: context.groupHistories }).clear({
|
|
909
|
-
historyKey,
|
|
910
|
-
limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
970
|
+
const groupHistoryKey = isGroup ? groupId ?? roomId : void 0;
|
|
971
|
+
const historyReservation = reserveLineGroupHistory(context.groupHistories, groupHistoryKey, context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT);
|
|
972
|
+
try {
|
|
973
|
+
const allMedia = [];
|
|
974
|
+
let mediaUnavailable = false;
|
|
975
|
+
if (isDownloadableLineMessageType(message.type)) try {
|
|
976
|
+
const originalFilename = message.type === "file" ? normalizeOptionalString(message.fileName) : void 0;
|
|
977
|
+
const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes, { originalFilename });
|
|
978
|
+
allMedia.push({
|
|
979
|
+
path: media.path,
|
|
980
|
+
contentType: media.contentType
|
|
981
|
+
});
|
|
982
|
+
} catch (err) {
|
|
983
|
+
mediaUnavailable = true;
|
|
984
|
+
const errMsg = String(err);
|
|
985
|
+
if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
|
|
986
|
+
else runtime.error?.(danger(`line: failed to download media: ${errMsg}`));
|
|
987
|
+
}
|
|
988
|
+
const messageContext = await buildLineMessageContext({
|
|
989
|
+
event,
|
|
990
|
+
allMedia,
|
|
991
|
+
mediaUnavailable,
|
|
992
|
+
cfg,
|
|
993
|
+
account,
|
|
994
|
+
commandAuthorized: decision.commandAccess.authorized,
|
|
995
|
+
inboundHistory: historyReservation.inboundHistory
|
|
911
996
|
});
|
|
997
|
+
if (!messageContext) {
|
|
998
|
+
logVerbose("line: skipping empty message");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
await processMessage(messageContext);
|
|
1002
|
+
historyReservation.commit();
|
|
1003
|
+
} finally {
|
|
1004
|
+
historyReservation.release();
|
|
912
1005
|
}
|
|
913
1006
|
}
|
|
914
1007
|
async function handleFollowEvent(event, _context) {
|
|
@@ -943,50 +1036,55 @@ async function handlePostbackEvent(event, context) {
|
|
|
943
1036
|
}
|
|
944
1037
|
async function handleLineWebhookEvents(events, context) {
|
|
945
1038
|
let firstError;
|
|
946
|
-
for (const event of events) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1039
|
+
for (const event of events) try {
|
|
1040
|
+
if (!context.replayCache) {
|
|
1041
|
+
await handleLineWebhookEvent(event, context);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
const replayEvent = {
|
|
1045
|
+
event,
|
|
1046
|
+
accountId: context.account.accountId
|
|
1047
|
+
};
|
|
1048
|
+
const result = await context.replayCache.processGuarded(replayEvent, async () => await handleLineWebhookEvent(event, context), { onError: "commit" });
|
|
1049
|
+
const replayId = buildLineWebhookReplayKey(event, context.account.accountId)?.eventId;
|
|
1050
|
+
if (result.kind === "inflight") {
|
|
1051
|
+
logVerbose(`line: skipped in-flight replayed webhook event ${replayId ?? "unknown"}`);
|
|
1052
|
+
try {
|
|
1053
|
+
await result.pending;
|
|
952
1054
|
} catch (err) {
|
|
953
1055
|
context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
|
|
954
1056
|
firstError ??= err;
|
|
955
1057
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
case "message":
|
|
961
|
-
await handleMessageEvent(event, context);
|
|
962
|
-
break;
|
|
963
|
-
case "follow":
|
|
964
|
-
await handleFollowEvent(event, context);
|
|
965
|
-
break;
|
|
966
|
-
case "unfollow":
|
|
967
|
-
await handleUnfollowEvent(event, context);
|
|
968
|
-
break;
|
|
969
|
-
case "join":
|
|
970
|
-
await handleJoinEvent(event, context);
|
|
971
|
-
break;
|
|
972
|
-
case "leave":
|
|
973
|
-
await handleLeaveEvent(event, context);
|
|
974
|
-
break;
|
|
975
|
-
case "postback":
|
|
976
|
-
await handlePostbackEvent(event, context);
|
|
977
|
-
break;
|
|
978
|
-
default: logVerbose(`line: unhandled event type: ${event.type}`);
|
|
979
|
-
}
|
|
980
|
-
if (replayCandidate) await replayCandidate.cache.commit(replayCandidate.key);
|
|
981
|
-
} catch (err) {
|
|
982
|
-
if (replayCandidate) if (err instanceof LineRetryableWebhookError) replayCandidate.cache.release(replayCandidate.key, { error: err });
|
|
983
|
-
else await replayCandidate.cache.commit(replayCandidate.key);
|
|
984
|
-
context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
|
|
985
|
-
firstError ??= err;
|
|
986
|
-
}
|
|
1058
|
+
} else if (result.kind === "duplicate") logVerbose(`line: skipped replayed webhook event ${replayId ?? "unknown"}`);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
|
|
1061
|
+
firstError ??= err;
|
|
987
1062
|
}
|
|
988
1063
|
if (firstError) throw toLintErrorObject(firstError, "Non-Error thrown");
|
|
989
1064
|
}
|
|
1065
|
+
async function handleLineWebhookEvent(event, context) {
|
|
1066
|
+
switch (event.type) {
|
|
1067
|
+
case "message":
|
|
1068
|
+
await handleMessageEvent(event, context);
|
|
1069
|
+
break;
|
|
1070
|
+
case "follow":
|
|
1071
|
+
await handleFollowEvent(event, context);
|
|
1072
|
+
break;
|
|
1073
|
+
case "unfollow":
|
|
1074
|
+
await handleUnfollowEvent(event, context);
|
|
1075
|
+
break;
|
|
1076
|
+
case "join":
|
|
1077
|
+
await handleJoinEvent(event, context);
|
|
1078
|
+
break;
|
|
1079
|
+
case "leave":
|
|
1080
|
+
await handleLeaveEvent(event, context);
|
|
1081
|
+
break;
|
|
1082
|
+
case "postback":
|
|
1083
|
+
await handlePostbackEvent(event, context);
|
|
1084
|
+
break;
|
|
1085
|
+
default: logVerbose(`line: unhandled event type: ${event.type}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
990
1088
|
function toLintErrorObject(value, fallbackMessage) {
|
|
991
1089
|
if (value instanceof Error) return value;
|
|
992
1090
|
if (typeof value === "string") return new Error(value);
|
|
@@ -1044,43 +1142,48 @@ function resolveLineDurableReplyOptions(params) {
|
|
|
1044
1142
|
//#endregion
|
|
1045
1143
|
//#region extensions/line/src/reply-chunks.ts
|
|
1046
1144
|
async function sendLineReplyChunks(params) {
|
|
1047
|
-
const
|
|
1145
|
+
const quickReplies = params.quickReplies?.length ? params.quickReplies : void 0;
|
|
1048
1146
|
let replyTokenUsed = Boolean(params.replyTokenUsed);
|
|
1049
1147
|
if (params.chunks.length === 0) return { replyTokenUsed };
|
|
1050
|
-
if (params.replyToken && !replyTokenUsed)
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1148
|
+
if (params.replyToken && !replyTokenUsed) {
|
|
1149
|
+
let replySucceeded = false;
|
|
1150
|
+
try {
|
|
1151
|
+
const replyBatch = params.chunks.slice(0, 5);
|
|
1152
|
+
const remaining = params.chunks.slice(replyBatch.length);
|
|
1153
|
+
const replyMessages = replyBatch.map((chunk) => ({
|
|
1154
|
+
type: "text",
|
|
1155
|
+
text: chunk
|
|
1156
|
+
}));
|
|
1157
|
+
if (quickReplies && remaining.length === 0 && replyMessages.length > 0) {
|
|
1158
|
+
const lastIndex = replyMessages.length - 1;
|
|
1159
|
+
replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"), quickReplies);
|
|
1160
|
+
}
|
|
1161
|
+
await params.replyMessageLine(params.replyToken, replyMessages, {
|
|
1162
|
+
cfg: params.cfg,
|
|
1163
|
+
accountId: params.accountId
|
|
1164
|
+
});
|
|
1165
|
+
replyTokenUsed = true;
|
|
1166
|
+
replySucceeded = true;
|
|
1167
|
+
for (const [i, chunk] of remaining.entries()) if (i === remaining.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
|
1168
|
+
cfg: params.cfg,
|
|
1169
|
+
accountId: params.accountId
|
|
1170
|
+
});
|
|
1171
|
+
else await params.pushMessageLine(params.to, chunk, {
|
|
1172
|
+
cfg: params.cfg,
|
|
1173
|
+
accountId: params.accountId
|
|
1174
|
+
});
|
|
1175
|
+
return { replyTokenUsed };
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
if (replySucceeded) throw err;
|
|
1178
|
+
params.onReplyError?.(err);
|
|
1179
|
+
replyTokenUsed = true;
|
|
1060
1180
|
}
|
|
1061
|
-
await params.replyMessageLine(params.replyToken, replyMessages, {
|
|
1062
|
-
cfg: params.cfg,
|
|
1063
|
-
accountId: params.accountId
|
|
1064
|
-
});
|
|
1065
|
-
replyTokenUsed = true;
|
|
1066
|
-
for (let i = 0; i < remaining.length; i += 1) if (i === remaining.length - 1 && hasQuickReplies) await params.pushTextMessageWithQuickReplies(params.to, remaining[i], params.quickReplies, {
|
|
1067
|
-
cfg: params.cfg,
|
|
1068
|
-
accountId: params.accountId
|
|
1069
|
-
});
|
|
1070
|
-
else await params.pushMessageLine(params.to, remaining[i], {
|
|
1071
|
-
cfg: params.cfg,
|
|
1072
|
-
accountId: params.accountId
|
|
1073
|
-
});
|
|
1074
|
-
return { replyTokenUsed };
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
params.onReplyError?.(err);
|
|
1077
|
-
replyTokenUsed = true;
|
|
1078
1181
|
}
|
|
1079
|
-
for (
|
|
1182
|
+
for (const [i, chunk] of params.chunks.entries()) if (i === params.chunks.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
|
1080
1183
|
cfg: params.cfg,
|
|
1081
1184
|
accountId: params.accountId
|
|
1082
1185
|
});
|
|
1083
|
-
else await params.pushMessageLine(params.to,
|
|
1186
|
+
else await params.pushMessageLine(params.to, chunk, {
|
|
1084
1187
|
cfg: params.cfg,
|
|
1085
1188
|
accountId: params.accountId
|
|
1086
1189
|
});
|
|
@@ -1089,16 +1192,7 @@ async function sendLineReplyChunks(params) {
|
|
|
1089
1192
|
//#endregion
|
|
1090
1193
|
//#region extensions/line/src/signature.ts
|
|
1091
1194
|
function validateLineSignature(body, signature, channelSecret) {
|
|
1092
|
-
|
|
1093
|
-
const hashBuffer = Buffer.from(hash);
|
|
1094
|
-
const signatureBuffer = Buffer.from(signature);
|
|
1095
|
-
const maxLen = Math.max(hashBuffer.length, signatureBuffer.length);
|
|
1096
|
-
const paddedHash = Buffer.alloc(maxLen);
|
|
1097
|
-
const paddedSig = Buffer.alloc(maxLen);
|
|
1098
|
-
hashBuffer.copy(paddedHash);
|
|
1099
|
-
signatureBuffer.copy(paddedSig);
|
|
1100
|
-
const timingResult = crypto.timingSafeEqual(paddedHash, paddedSig);
|
|
1101
|
-
return hashBuffer.length === signatureBuffer.length && timingResult;
|
|
1195
|
+
return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
|
|
1102
1196
|
}
|
|
1103
1197
|
//#endregion
|
|
1104
1198
|
//#region extensions/line/src/webhook-utils.ts
|
|
@@ -1186,7 +1280,7 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1186
1280
|
if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
|
|
1187
1281
|
if (body.events && body.events.length > 0) {
|
|
1188
1282
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1189
|
-
|
|
1283
|
+
runDetachedWebhookWork(() => params.bot.handleWebhook(body)).catch((err) => logLineWebhookDispatchError(params.runtime, err));
|
|
1190
1284
|
}
|
|
1191
1285
|
} catch (err) {
|
|
1192
1286
|
await receiveContext?.nack(err);
|
|
@@ -1213,24 +1307,10 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1213
1307
|
}
|
|
1214
1308
|
//#endregion
|
|
1215
1309
|
//#region extensions/line/src/monitor.ts
|
|
1216
|
-
const runtimeState = /* @__PURE__ */ new Map();
|
|
1217
1310
|
const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
|
|
1218
1311
|
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 64 * 1024;
|
|
1219
1312
|
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
|
|
1220
1313
|
const lineWebhookTargets = /* @__PURE__ */ new Map();
|
|
1221
|
-
function recordChannelRuntimeState(params) {
|
|
1222
|
-
const key = `${params.channel}:${params.accountId}`;
|
|
1223
|
-
const existing = runtimeState.get(key) ?? {
|
|
1224
|
-
running: false,
|
|
1225
|
-
lastStartAt: null,
|
|
1226
|
-
lastStopAt: null,
|
|
1227
|
-
lastError: null
|
|
1228
|
-
};
|
|
1229
|
-
runtimeState.set(key, {
|
|
1230
|
-
...existing,
|
|
1231
|
-
...params.state
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
1314
|
function startLineLoadingKeepalive(params) {
|
|
1235
1315
|
const intervalMs = params.intervalMs ?? 18e3;
|
|
1236
1316
|
const loadingSeconds = params.loadingSeconds ?? 20;
|
|
@@ -1267,11 +1347,6 @@ async function monitorLineProvider(opts) {
|
|
|
1267
1347
|
onMessage: async (ctx) => {
|
|
1268
1348
|
if (!ctx) return;
|
|
1269
1349
|
const { ctxPayload, replyToken, route } = ctx;
|
|
1270
|
-
recordChannelRuntimeState({
|
|
1271
|
-
channel: "line",
|
|
1272
|
-
accountId: resolvedAccountId,
|
|
1273
|
-
state: { lastInboundAt: Date.now() }
|
|
1274
|
-
});
|
|
1275
1350
|
const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
|
|
1276
1351
|
const displayNamePromise = ctx.userId ? getUserDisplayName(ctx.userId, {
|
|
1277
1352
|
cfg: config,
|
|
@@ -1343,7 +1418,7 @@ async function monitorLineProvider(opts) {
|
|
|
1343
1418
|
createTextMessageWithQuickReplies,
|
|
1344
1419
|
pushMessagesLine,
|
|
1345
1420
|
createFlexMessage,
|
|
1346
|
-
|
|
1421
|
+
buildMediaMessage: buildLineMediaMessage,
|
|
1347
1422
|
createLocationMessage,
|
|
1348
1423
|
onReplyError: (replyErr) => {
|
|
1349
1424
|
logVerbose(`line: reply token failed, falling back to push: ${String(replyErr)}`);
|
|
@@ -1352,11 +1427,7 @@ async function monitorLineProvider(opts) {
|
|
|
1352
1427
|
});
|
|
1353
1428
|
replyTokenUsed = deliveryResult.replyTokenUsed;
|
|
1354
1429
|
if (deliveryResult.status === "partial") throw deliveryResult.error;
|
|
1355
|
-
|
|
1356
|
-
channel: "line",
|
|
1357
|
-
accountId: resolvedAccountId,
|
|
1358
|
-
state: { lastOutboundAt: Date.now() }
|
|
1359
|
-
});
|
|
1430
|
+
return { visibleReplySent: deliveryResult.visibleReplySent };
|
|
1360
1431
|
},
|
|
1361
1432
|
onError: (err, info) => {
|
|
1362
1433
|
runtime.error?.(danger(`line ${info.kind} reply failed: ${String(err)}`));
|
|
@@ -1462,7 +1533,7 @@ async function monitorLineProvider(opts) {
|
|
|
1462
1533
|
res.end(JSON.stringify({ status: "ok" }));
|
|
1463
1534
|
if (body.events && body.events.length > 0) {
|
|
1464
1535
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1465
|
-
|
|
1536
|
+
runDetachedWebhookWork(() => match.target.bot.handleWebhook(body)).catch((err) => {
|
|
1466
1537
|
match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
|
|
1467
1538
|
});
|
|
1468
1539
|
}
|
|
@@ -1491,14 +1562,6 @@ async function monitorLineProvider(opts) {
|
|
|
1491
1562
|
}
|
|
1492
1563
|
}
|
|
1493
1564
|
});
|
|
1494
|
-
recordChannelRuntimeState({
|
|
1495
|
-
channel: "line",
|
|
1496
|
-
accountId: resolvedAccountId,
|
|
1497
|
-
state: {
|
|
1498
|
-
running: true,
|
|
1499
|
-
lastStartAt: Date.now()
|
|
1500
|
-
}
|
|
1501
|
-
});
|
|
1502
1565
|
logVerbose(`line: registered webhook handler at ${normalizedPath}`);
|
|
1503
1566
|
let stopped = false;
|
|
1504
1567
|
const stopHandler = () => {
|
|
@@ -1506,14 +1569,6 @@ async function monitorLineProvider(opts) {
|
|
|
1506
1569
|
stopped = true;
|
|
1507
1570
|
logVerbose(`line: stopping provider for account ${resolvedAccountId}`);
|
|
1508
1571
|
unregisterHttp();
|
|
1509
|
-
recordChannelRuntimeState({
|
|
1510
|
-
channel: "line",
|
|
1511
|
-
accountId: resolvedAccountId,
|
|
1512
|
-
state: {
|
|
1513
|
-
running: false,
|
|
1514
|
-
lastStopAt: Date.now()
|
|
1515
|
-
}
|
|
1516
|
-
});
|
|
1517
1572
|
};
|
|
1518
1573
|
if (abortSignal?.aborted) stopHandler();
|
|
1519
1574
|
else if (abortSignal) {
|