@openclaw/line 2026.7.2-beta.3 → 2026.7.2-beta.5
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/README.md +1 -1
- package/dist/{accounts-BUBOoW5Q.js → accounts-CSH5sk8U.js} +9 -11
- package/dist/api.js +4 -3
- package/dist/{card-command-DQ-epsgt.js → card-command-BEMIm8D8.js} +1 -1
- package/dist/{channel-uT5RTtpi.js → channel-CTaWrzfl.js} +15 -11
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel.runtime-D4Paci2y.js +4 -0
- package/dist/contract-api.js +1 -1
- package/dist/{flex-templates-K1lS9i8L.js → flex-templates-DVy_wog4.js} +318 -107
- package/dist/index.js +1 -1
- package/dist/{markdown-to-line-D1vapcdP.js → markdown-to-line-Dn00k1bq.js} +239 -229
- package/dist/{monitor-Bk-lpoOZ.js → monitor-DzFU5UPV.js} +317 -323
- package/dist/monitor.runtime-B8xjVqev.js +2 -0
- package/dist/outbound.runtime-eHl7NNHu.js +2 -0
- package/dist/{probe-BslD77tJ.js → probe-CM03wo6r.js} +7 -9
- package/dist/probe.runtime-D13ZtXsd.js +2 -0
- package/dist/quick-reply-fallback-D86FwAfr.js +56 -0
- package/dist/{reply-payload-transform-r9sa-TYT.js → reply-payload-transform-BRM2fRvK.js} +3 -164
- package/dist/runtime-api.js +9 -8
- package/dist/send-receipt-CY2ey8XH.js +107 -0
- package/dist/setup-api.js +1 -1
- package/dist/{setup-surface-BEt4mazl.js → setup-surface-BHQOSD8R.js} +107 -67
- package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBubble.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexIconSize.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexSpanSize.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/postbackAction.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/version.d.ts +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/version.js +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts +2 -1
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts +24 -0
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts.map +1 -0
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js +14 -0
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js.map +1 -0
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts +1 -0
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js +1 -0
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js.map +1 -1
- package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBubble.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexIconSize.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexSpanSize.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/messaging-api/model/postbackAction.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/version.d.ts +1 -1
- package/node_modules/@line/bot-sdk/dist/version.js +1 -1
- package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts +2 -1
- package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts +24 -0
- package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts.map +1 -0
- package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js +13 -0
- package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js.map +1 -0
- package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts +1 -0
- package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/webhook/model/models.js +1 -0
- package/node_modules/@line/bot-sdk/dist/webhook/model/models.js.map +1 -1
- package/node_modules/@line/bot-sdk/lib/insight/model/getNumberOfFollowersResponse.ts +1 -1
- package/node_modules/@line/bot-sdk/lib/liff/model/liffScope.ts +1 -1
- package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupCreateRoute.ts +1 -4
- package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupFailedType.ts +1 -2
- package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobFailedType.ts +1 -2
- package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobStatus.ts +1 -4
- package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupStatus.ts +1 -6
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/botInfoResponse.ts +1 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxBorderWidth.ts +1 -6
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxCornerRadius.ts +1 -7
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBubble.ts +1 -7
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexIconSize.ts +1 -10
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexSpanSize.ts +1 -10
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexTextFontSize.ts +1 -10
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/getAggregationUnitNameListResponse.ts +1 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/getFollowersResponse.ts +1 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/getJoinedMembershipUsersResponse.ts +1 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/issueLinkTokenResponse.ts +1 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/numberOfMessagesResponse.ts +2 -5
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/postbackAction.ts +1 -4
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/subscriptionPeriodDemographic.ts +1 -5
- package/node_modules/@line/bot-sdk/lib/version.ts +1 -1
- package/node_modules/@line/bot-sdk/lib/webhook/.openapi-generator/FILES +1 -0
- package/node_modules/@line/bot-sdk/lib/webhook/model/event.ts +2 -0
- package/node_modules/@line/bot-sdk/lib/webhook/model/messageEditedEvent.ts +32 -0
- package/node_modules/@line/bot-sdk/lib/webhook/model/models.ts +1 -0
- package/node_modules/@line/bot-sdk/lib/webhook/model/stickerMessageContent.ts +1 -1
- package/node_modules/@line/bot-sdk/package.json +3 -3
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/fs/promises.d.ts +16 -5
- package/node_modules/@types/node/package.json +3 -3
- package/openclaw.plugin.json +0 -18
- package/package.json +71 -7
- package/dist/channel.runtime-BqNglqnr.js +0 -4
- package/dist/monitor.runtime-CHlqxGVL.js +0 -2
- package/dist/outbound.runtime-ByZ0Dsvu.js +0 -2
- package/dist/probe.runtime-CRhVGqwl.js +0 -2
- package/npm-shrinkwrap.json +0 -60
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
1
|
+
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-CSH5sk8U.js";
|
|
2
|
+
import { a as resolveLineGroupConfigEntry, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-D86FwAfr.js";
|
|
3
|
+
import { n as buildLineMediaMessage, r as hasLineSpecificMediaOptions } from "./send-receipt-CY2ey8XH.js";
|
|
4
|
+
import { E as buildTemplateMessageFromPayload, S as replyMessageLine, i as processLineMessage, l as createLocationMessage, p as getUserDisplayName, s as createFlexMessage, u as createQuickReplyItems, v as pushMessageLine, w as showLoadingAnimation, y as pushMessagesLine } from "./markdown-to-line-Dn00k1bq.js";
|
|
4
5
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
5
6
|
import { normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
|
-
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions,
|
|
7
|
+
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
|
|
7
8
|
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
8
9
|
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
9
10
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
10
11
|
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
11
12
|
import { firstDefined } from "openclaw/plugin-sdk/allow-from";
|
|
12
|
-
import { Readable } from "node:stream";
|
|
13
|
-
import { finished } from "node:stream/promises";
|
|
14
13
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
14
|
+
import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
|
|
15
15
|
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
16
|
-
import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
|
|
16
|
+
import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal, warn } from "openclaw/plugin-sdk/runtime-env";
|
|
17
17
|
import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
|
|
18
|
+
import { HTTPFetchError } from "@line/bot-sdk";
|
|
18
19
|
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
|
19
|
-
import { buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
20
|
+
import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFacts, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
20
21
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
21
22
|
import { isRequestBodyLimitError, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
|
|
22
23
|
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1, runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
@@ -27,7 +28,6 @@ import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
|
|
|
27
28
|
import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
28
29
|
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
29
30
|
import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
30
|
-
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
|
31
31
|
import crypto from "node:crypto";
|
|
32
32
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
33
33
|
//#region extensions/line/src/bot-access.ts
|
|
@@ -52,44 +52,85 @@ const CONTENT_READY_BASE_DELAY_MS = 500;
|
|
|
52
52
|
const CONTENT_READY_MAX_DELAY_MS = 4e3;
|
|
53
53
|
const CONTENT_READY_TIMEOUT_MS = 15e3;
|
|
54
54
|
const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
|
|
55
|
+
var RetryableLineMediaFetchError = class extends MediaFetchError {
|
|
56
|
+
constructor(message, options) {
|
|
57
|
+
super("fetch_failed", message, options);
|
|
58
|
+
this.name = "RetryableLineMediaFetchError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
55
61
|
function contentBackoffDelayMs(attempt) {
|
|
56
62
|
return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
|
|
57
63
|
}
|
|
64
|
+
function lineContentUrl(messageId) {
|
|
65
|
+
return `${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`;
|
|
66
|
+
}
|
|
67
|
+
async function* lineResponseBodyChunks(response, messageId) {
|
|
68
|
+
const body = response.body;
|
|
69
|
+
if (!body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
70
|
+
const reader = body.getReader();
|
|
71
|
+
let completed = false;
|
|
72
|
+
try {
|
|
73
|
+
while (true) {
|
|
74
|
+
let chunk;
|
|
75
|
+
try {
|
|
76
|
+
chunk = await reader.read();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new RetryableLineMediaFetchError(`LINE media response stream failed for message ${messageId}`, { cause: err });
|
|
79
|
+
}
|
|
80
|
+
if (chunk.done) {
|
|
81
|
+
completed = true;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (chunk.value.byteLength > 0) yield chunk.value;
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
if (!completed) await reader.cancel().catch(() => void 0);
|
|
88
|
+
try {
|
|
89
|
+
reader.releaseLock();
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
58
93
|
async function fetchLineContentWhenReady(messageId, channelAccessToken) {
|
|
59
94
|
const controller = new AbortController();
|
|
60
95
|
const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
|
|
61
96
|
deadline.unref();
|
|
62
97
|
try {
|
|
63
98
|
for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
|
|
64
|
-
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(
|
|
99
|
+
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
|
|
65
100
|
headers: { Authorization: `Bearer ${channelAccessToken}` },
|
|
66
101
|
redirect: "error",
|
|
67
102
|
signal: controller.signal
|
|
68
103
|
});
|
|
69
104
|
if (response.status === 200) {
|
|
70
|
-
if (!response.body) throw new
|
|
71
|
-
return
|
|
105
|
+
if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
106
|
+
return response;
|
|
72
107
|
}
|
|
73
|
-
await response.body?.cancel();
|
|
74
|
-
if (response.status !== 202) throw new
|
|
108
|
+
await response.body?.cancel().catch(() => void 0);
|
|
109
|
+
if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
|
|
75
110
|
if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
|
|
76
111
|
}
|
|
77
112
|
} catch (err) {
|
|
78
|
-
if (controller.signal.aborted) throw new
|
|
79
|
-
throw err;
|
|
113
|
+
if (controller.signal.aborted) throw new RetryableLineMediaFetchError(`LINE media for message ${messageId} did not become ready within ${CONTENT_READY_TIMEOUT_MS / 1e3} seconds`, { cause: err });
|
|
114
|
+
if (err instanceof MediaFetchError) throw err;
|
|
115
|
+
throw new RetryableLineMediaFetchError(`LINE media download failed for message ${messageId}`, { cause: err });
|
|
80
116
|
} finally {
|
|
81
117
|
clearTimeout(deadline);
|
|
82
118
|
}
|
|
83
|
-
throw new
|
|
119
|
+
throw new MediaFetchError("http_error", `LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`, { status: 202 });
|
|
120
|
+
}
|
|
121
|
+
function isRetryableLineInboundMediaError(err) {
|
|
122
|
+
if (err instanceof RetryableLineMediaFetchError) return true;
|
|
123
|
+
if (!(err instanceof MediaFetchError)) return false;
|
|
124
|
+
if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
|
|
125
|
+
return false;
|
|
84
126
|
}
|
|
85
127
|
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
|
|
86
|
-
const
|
|
128
|
+
const response = await fetchLineContentWhenReady(messageId, channelAccessToken);
|
|
87
129
|
let saved;
|
|
88
130
|
try {
|
|
89
|
-
saved = await saveMediaStream(
|
|
131
|
+
saved = await saveMediaStream(lineResponseBodyChunks(response, messageId), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
|
|
90
132
|
} catch (err) {
|
|
91
|
-
|
|
92
|
-
await finished(content).catch(() => void 0);
|
|
133
|
+
await response.body?.cancel().catch(() => void 0);
|
|
93
134
|
throw err;
|
|
94
135
|
}
|
|
95
136
|
logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
|
|
@@ -102,7 +143,11 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
|
|
|
102
143
|
//#endregion
|
|
103
144
|
//#region extensions/line/src/auto-reply-delivery.ts
|
|
104
145
|
function toLineDeliveryError(error) {
|
|
105
|
-
return error instanceof Error ? error : new Error("LINE
|
|
146
|
+
return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
|
|
147
|
+
}
|
|
148
|
+
function getLineHttpError(error) {
|
|
149
|
+
if (error instanceof HTTPFetchError) return error;
|
|
150
|
+
return error instanceof Error && error.cause instanceof HTTPFetchError ? error.cause : void 0;
|
|
106
151
|
}
|
|
107
152
|
function markLineVisibleDeliveryError(error) {
|
|
108
153
|
const deliveryError = toLineDeliveryError(error);
|
|
@@ -113,7 +158,7 @@ function markLineVisibleDeliveryError(error) {
|
|
|
113
158
|
});
|
|
114
159
|
return deliveryError;
|
|
115
160
|
}
|
|
116
|
-
const visibleError = new Error("LINE
|
|
161
|
+
const visibleError = new Error("LINE message send failed", { cause: deliveryError });
|
|
117
162
|
Object.assign(visibleError, {
|
|
118
163
|
sentBeforeError: true,
|
|
119
164
|
visibleReplySent: true
|
|
@@ -135,14 +180,25 @@ async function deliverLineAutoReply(params) {
|
|
|
135
180
|
}
|
|
136
181
|
};
|
|
137
182
|
const replyVisible = (...args) => sendVisible(() => deps.replyMessageLine(...args));
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const pushLineMessages = async (messages) => {
|
|
183
|
+
const failedPushSegments = /* @__PURE__ */ new WeakMap();
|
|
184
|
+
const pushLineMessages = async (messages, allowFailedBatchTextRecovery, externalTail = []) => {
|
|
141
185
|
if (messages.length === 0) return;
|
|
142
|
-
for (let i = 0; i < messages.length; i += 5)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
186
|
+
for (let i = 0; i < messages.length; i += 5) {
|
|
187
|
+
const batch = messages.slice(i, i + 5);
|
|
188
|
+
try {
|
|
189
|
+
await sendVisible(() => deps.pushMessagesLine(to, batch, {
|
|
190
|
+
cfg: params.cfg,
|
|
191
|
+
accountId
|
|
192
|
+
}));
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (typeof error === "object" && error !== null) failedPushSegments.set(error, {
|
|
195
|
+
allowFailedBatchTextRecovery,
|
|
196
|
+
failedBatch: batch,
|
|
197
|
+
unattemptedTail: [...messages.slice(i + batch.length), ...externalTail]
|
|
198
|
+
});
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
146
202
|
};
|
|
147
203
|
const sendLineMessages = async (messages, allowReplyToken) => {
|
|
148
204
|
if (messages.length === 0) return;
|
|
@@ -156,12 +212,13 @@ async function deliverLineAutoReply(params) {
|
|
|
156
212
|
});
|
|
157
213
|
} catch (err) {
|
|
158
214
|
deps.onReplyError?.(err);
|
|
159
|
-
await pushLineMessages(replyBatch);
|
|
215
|
+
await pushLineMessages(replyBatch, getLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
|
|
216
|
+
} finally {
|
|
217
|
+
replyTokenUsed = true;
|
|
160
218
|
}
|
|
161
|
-
replyTokenUsed = true;
|
|
162
219
|
remaining = remaining.slice(replyBatch.length);
|
|
163
220
|
}
|
|
164
|
-
if (remaining.length > 0) await pushLineMessages(remaining);
|
|
221
|
+
if (remaining.length > 0) await pushLineMessages(remaining, true);
|
|
165
222
|
};
|
|
166
223
|
const richMessages = [];
|
|
167
224
|
const hasQuickReplies = Boolean(lineData.quickReplies?.length);
|
|
@@ -186,82 +243,59 @@ async function deliverLineAutoReply(params) {
|
|
|
186
243
|
trackingId: lineData.trackingId
|
|
187
244
|
};
|
|
188
245
|
const mediaMessages = [];
|
|
189
|
-
let
|
|
246
|
+
let deliveryError;
|
|
190
247
|
for (const rawUrl of mediaUrls) {
|
|
191
248
|
const url = rawUrl?.trim();
|
|
192
249
|
if (!url) continue;
|
|
193
250
|
try {
|
|
194
251
|
mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
|
|
195
252
|
} catch (err) {
|
|
196
|
-
|
|
253
|
+
deliveryError ??= err;
|
|
197
254
|
}
|
|
198
255
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
replyMessageLine: replyVisible,
|
|
216
|
-
pushMessageLine: pushTextVisible,
|
|
217
|
-
pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
|
|
218
|
-
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
|
|
219
|
-
onReplyError: deps.onReplyError
|
|
220
|
-
});
|
|
221
|
-
replyTokenUsed = nextReplyTokenUsed;
|
|
222
|
-
if (!sendRichBeforeText) try {
|
|
223
|
-
await sendLineMessages(richMessages, false);
|
|
224
|
-
if (mediaMessages.length > 0) await sendLineMessages(mediaMessages, false);
|
|
225
|
-
} catch (err) {
|
|
226
|
-
richMediaError ??= err;
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
const combined = [...richMessages, ...mediaMessages];
|
|
230
|
-
if (hasQuickReplies && combined.length === 0) {
|
|
231
|
-
const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
|
|
232
|
-
to,
|
|
233
|
-
chunks: [buildLineQuickReplyFallbackText(lineData.quickReplies)],
|
|
234
|
-
quickReplies: lineData.quickReplies,
|
|
235
|
-
replyToken,
|
|
236
|
-
replyTokenUsed,
|
|
237
|
-
cfg: params.cfg,
|
|
238
|
-
accountId,
|
|
239
|
-
replyMessageLine: replyVisible,
|
|
240
|
-
pushMessageLine: pushTextVisible,
|
|
241
|
-
pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
|
|
242
|
-
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
|
|
243
|
-
onReplyError: deps.onReplyError
|
|
244
|
-
});
|
|
245
|
-
replyTokenUsed = nextReplyTokenUsed;
|
|
246
|
-
} else {
|
|
247
|
-
if (hasQuickReplies && combined.length > 0) {
|
|
248
|
-
const quickReply = deps.createQuickReplyItems(lineData.quickReplies);
|
|
249
|
-
const targetIndex = replyToken && !replyTokenUsed ? Math.min(4, combined.length - 1) : combined.length - 1;
|
|
250
|
-
combined[targetIndex] = {
|
|
251
|
-
...combined[targetIndex],
|
|
252
|
-
quickReply
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
await sendLineMessages(combined, true);
|
|
256
|
-
}
|
|
256
|
+
const textMessages = chunks.map((text) => ({
|
|
257
|
+
type: "text",
|
|
258
|
+
text
|
|
259
|
+
}));
|
|
260
|
+
const richMediaMessages = [...richMessages, ...mediaMessages];
|
|
261
|
+
if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
|
|
262
|
+
type: "text",
|
|
263
|
+
text: buildLineQuickReplyFallbackText(lineData.quickReplies)
|
|
264
|
+
});
|
|
265
|
+
if (hasQuickReplies) {
|
|
266
|
+
const targetMessages = textMessages.length > 0 ? textMessages : richMediaMessages;
|
|
267
|
+
const lastIndex = targetMessages.length - 1;
|
|
268
|
+
targetMessages[lastIndex] = {
|
|
269
|
+
...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
|
|
270
|
+
quickReply: deps.createQuickReplyItems(lineData.quickReplies)
|
|
271
|
+
};
|
|
257
272
|
}
|
|
258
|
-
|
|
259
|
-
|
|
273
|
+
const messages = hasQuickReplies ? [...richMediaMessages, ...textMessages] : [...textMessages, ...richMediaMessages];
|
|
274
|
+
try {
|
|
275
|
+
await sendLineMessages(messages, true);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
deliveryError ??= err;
|
|
278
|
+
const failedSegment = typeof err === "object" && err !== null ? failedPushSegments.get(err) : void 0;
|
|
279
|
+
const httpError = getLineHttpError(err);
|
|
280
|
+
const retryCandidates = failedSegment ? [...failedSegment.allowFailedBatchTextRecovery ? failedSegment.failedBatch : [], ...failedSegment.unattemptedTail] : [];
|
|
281
|
+
const retryTextMessages = retryCandidates.filter((message) => message.type === "text");
|
|
282
|
+
const quickRepliesNeedCarrier = hasQuickReplies && retryCandidates.some((message) => "quickReply" in message);
|
|
283
|
+
const retryMessages = retryTextMessages.length > 0 ? retryTextMessages : quickRepliesNeedCarrier ? [{
|
|
284
|
+
type: "text",
|
|
285
|
+
text: buildLineQuickReplyFallbackText(lineData.quickReplies),
|
|
286
|
+
quickReply: deps.createQuickReplyItems(lineData.quickReplies)
|
|
287
|
+
}] : [];
|
|
288
|
+
if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400) try {
|
|
289
|
+
await sendLineMessages(retryMessages, false);
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
if (deliveryError !== void 0) {
|
|
293
|
+
if (!visibleReplySent) throw toLineDeliveryError(deliveryError);
|
|
260
294
|
return {
|
|
261
295
|
status: "partial",
|
|
262
296
|
replyTokenUsed,
|
|
263
297
|
visibleReplySent: true,
|
|
264
|
-
error: markLineVisibleDeliveryError(
|
|
298
|
+
error: markLineVisibleDeliveryError(deliveryError)
|
|
265
299
|
};
|
|
266
300
|
}
|
|
267
301
|
return {
|
|
@@ -397,48 +431,27 @@ function extractMessageText(message) {
|
|
|
397
431
|
}
|
|
398
432
|
return "";
|
|
399
433
|
}
|
|
400
|
-
function
|
|
434
|
+
function extractNativeMediaKind(message) {
|
|
401
435
|
switch (message.type) {
|
|
402
|
-
case "image": return "
|
|
403
|
-
case "video": return "
|
|
404
|
-
case "audio": return "
|
|
405
|
-
case "file": return "
|
|
406
|
-
default: return
|
|
436
|
+
case "image": return "image";
|
|
437
|
+
case "video": return "video";
|
|
438
|
+
case "audio": return "audio";
|
|
439
|
+
case "file": return "document";
|
|
440
|
+
default: return;
|
|
407
441
|
}
|
|
408
442
|
}
|
|
409
|
-
function resolveLineConversationLabel(params) {
|
|
410
|
-
return params.isGroup ? params.groupId ? `group:${params.groupId}` : params.roomId ? `room:${params.roomId}` : "unknown-group" : params.senderLabel;
|
|
411
|
-
}
|
|
412
|
-
function resolveLineAddresses(params) {
|
|
413
|
-
const fromAddress = params.isGroup ? params.groupId ? `line:group:${params.groupId}` : params.roomId ? `line:room:${params.roomId}` : `line:${params.peerId}` : `line:${params.userId ?? params.peerId}`;
|
|
414
|
-
return {
|
|
415
|
-
fromAddress,
|
|
416
|
-
toAddress: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`,
|
|
417
|
-
originatingTo: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
443
|
async function finalizeLineInboundContext(params) {
|
|
421
|
-
const { fromAddress, toAddress, originatingTo } = resolveLineAddresses({
|
|
422
|
-
isGroup: params.source.isGroup,
|
|
423
|
-
groupId: params.source.groupId,
|
|
424
|
-
roomId: params.source.roomId,
|
|
425
|
-
userId: params.source.userId,
|
|
426
|
-
peerId: params.source.peerId
|
|
427
|
-
});
|
|
428
444
|
const senderId = params.source.userId ?? "unknown";
|
|
429
445
|
const senderLabel = params.source.userId ? `user:${params.source.userId}` : "unknown";
|
|
430
|
-
const conversationLabel =
|
|
431
|
-
|
|
432
|
-
groupId: params.source.groupId,
|
|
433
|
-
roomId: params.source.roomId,
|
|
434
|
-
senderLabel
|
|
435
|
-
});
|
|
446
|
+
const conversationLabel = params.source.isGroup ? params.source.groupId ? `group:${params.source.groupId}` : params.source.roomId ? `room:${params.source.roomId}` : "unknown-group" : senderLabel;
|
|
447
|
+
const address = params.source.groupId ? `line:group:${params.source.groupId}` : params.source.roomId ? `line:room:${params.source.roomId}` : `line:${params.source.userId ?? params.source.peerId}`;
|
|
436
448
|
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
|
437
449
|
cfg: params.cfg,
|
|
438
450
|
agentId: params.route.agentId,
|
|
439
451
|
sessionKey: params.route.sessionKey
|
|
440
452
|
});
|
|
441
453
|
const agentBody = params.agentBody ?? params.rawBody;
|
|
454
|
+
const media = toInboundMediaFacts(params.media);
|
|
442
455
|
const body = formatInboundEnvelope({
|
|
443
456
|
channel: "LINE",
|
|
444
457
|
from: conversationLabel,
|
|
@@ -449,39 +462,45 @@ async function finalizeLineInboundContext(params) {
|
|
|
449
462
|
previousTimestamp,
|
|
450
463
|
envelope: envelopeOptions
|
|
451
464
|
});
|
|
452
|
-
const ctxPayload =
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
465
|
+
const ctxPayload = buildChannelInboundEventContext({
|
|
466
|
+
channel: "line",
|
|
467
|
+
accountId: params.route.accountId,
|
|
468
|
+
messageId: params.messageSid,
|
|
469
|
+
timestamp: params.timestamp,
|
|
470
|
+
from: address,
|
|
471
|
+
sender: { id: senderId },
|
|
472
|
+
conversation: {
|
|
473
|
+
kind: params.source.isGroup ? "group" : "direct",
|
|
474
|
+
id: params.source.peerId,
|
|
475
|
+
label: conversationLabel
|
|
476
|
+
},
|
|
477
|
+
route: {
|
|
478
|
+
agentId: params.route.agentId,
|
|
479
|
+
dmScope: params.route.dmScope,
|
|
480
|
+
accountId: params.route.accountId,
|
|
481
|
+
routeSessionKey: params.route.sessionKey
|
|
482
|
+
},
|
|
483
|
+
reply: {
|
|
484
|
+
to: address,
|
|
485
|
+
originatingTo: address
|
|
486
|
+
},
|
|
487
|
+
message: {
|
|
488
|
+
body,
|
|
489
|
+
bodyForAgent: agentBody,
|
|
490
|
+
rawBody: params.rawBody,
|
|
491
|
+
commandBody: params.rawBody,
|
|
492
|
+
inboundHistory: params.inboundHistory
|
|
493
|
+
},
|
|
494
|
+
access: { commands: { authorized: params.commandAuthorized } },
|
|
495
|
+
media,
|
|
496
|
+
extra: {
|
|
497
|
+
...params.locationContext,
|
|
498
|
+
GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
|
|
499
|
+
GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
|
|
500
|
+
groupId: params.source.groupId,
|
|
501
|
+
roomId: params.source.roomId
|
|
502
|
+
})?.systemPrompt) : void 0
|
|
503
|
+
}
|
|
485
504
|
});
|
|
486
505
|
const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
|
|
487
506
|
dmScope: params.cfg.session?.dmScope,
|
|
@@ -535,15 +554,17 @@ async function buildLineMessageContext(params) {
|
|
|
535
554
|
const messageId = message.id;
|
|
536
555
|
const timestamp = event.timestamp;
|
|
537
556
|
const textContent = extractMessageText(message);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
557
|
+
const nativeMediaKind = extractNativeMediaKind(message);
|
|
558
|
+
const mediaFacts = allMedia.length > 0 ? allMedia.map((media) => ({
|
|
559
|
+
...media,
|
|
560
|
+
kind: nativeMediaKind
|
|
561
|
+
})) : nativeMediaKind ? [{ kind: nativeMediaKind }] : [];
|
|
562
|
+
const rawBody = textContent;
|
|
541
563
|
const agentBody = mediaUnavailable ? formatInboundMediaUnavailableText({
|
|
542
564
|
body: rawBody,
|
|
543
|
-
mediaPlaceholder: placeholder,
|
|
544
565
|
notice: "[line attachment unavailable]"
|
|
545
566
|
}) : rawBody;
|
|
546
|
-
if (!agentBody &&
|
|
567
|
+
if (!agentBody && mediaFacts.length === 0) return null;
|
|
547
568
|
let locationContext;
|
|
548
569
|
if (message.type === "location") {
|
|
549
570
|
const loc = message;
|
|
@@ -571,12 +592,7 @@ async function buildLineMessageContext(params) {
|
|
|
571
592
|
timestamp,
|
|
572
593
|
messageSid: messageId,
|
|
573
594
|
commandAuthorized,
|
|
574
|
-
media:
|
|
575
|
-
firstPath: allMedia[0]?.path,
|
|
576
|
-
firstContentType: allMedia[0]?.contentType,
|
|
577
|
-
paths: allMedia.length > 0 ? allMedia.map((m) => m.path) : void 0,
|
|
578
|
-
types: allMedia.length > 0 ? allMedia.map((m) => m.contentType).filter(Boolean) : void 0
|
|
579
|
-
},
|
|
595
|
+
media: mediaFacts,
|
|
580
596
|
locationContext,
|
|
581
597
|
verboseLog: {
|
|
582
598
|
kind: "inbound",
|
|
@@ -632,12 +648,7 @@ async function buildLinePostbackContext(params) {
|
|
|
632
648
|
timestamp,
|
|
633
649
|
messageSid,
|
|
634
650
|
commandAuthorized,
|
|
635
|
-
media:
|
|
636
|
-
firstPath: "",
|
|
637
|
-
firstContentType: void 0,
|
|
638
|
-
paths: void 0,
|
|
639
|
-
types: void 0
|
|
640
|
-
},
|
|
651
|
+
media: [],
|
|
641
652
|
verboseLog: { kind: "postback" }
|
|
642
653
|
});
|
|
643
654
|
return {
|
|
@@ -952,6 +963,7 @@ async function handleMessageEvent(event, context) {
|
|
|
952
963
|
contentType: media.contentType
|
|
953
964
|
});
|
|
954
965
|
} catch (err) {
|
|
966
|
+
if (isRetryableLineInboundMediaError(err)) throw err;
|
|
955
967
|
mediaUnavailable = true;
|
|
956
968
|
const errMsg = String(err);
|
|
957
969
|
if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
|
|
@@ -1052,6 +1064,7 @@ const LINE_WEBHOOK_SPOOL_VERSION = 1;
|
|
|
1052
1064
|
const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
|
|
1053
1065
|
const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
|
|
1054
1066
|
const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
|
|
1067
|
+
const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
|
|
1055
1068
|
const LINE_WEBHOOK_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
|
|
1056
1069
|
const LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES = 4096;
|
|
1057
1070
|
var LineWebhookPayloadError = class extends Error {
|
|
@@ -1099,15 +1112,13 @@ function laneKeyFor(event, eventId) {
|
|
|
1099
1112
|
}
|
|
1100
1113
|
return eventId;
|
|
1101
1114
|
}
|
|
1102
|
-
function
|
|
1103
|
-
if (payload.version !== LINE_WEBHOOK_SPOOL_VERSION || typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
|
|
1115
|
+
function parseStoredEvent(rawEvent) {
|
|
1104
1116
|
let event;
|
|
1105
1117
|
try {
|
|
1106
|
-
event = JSON.parse(
|
|
1118
|
+
event = JSON.parse(rawEvent);
|
|
1107
1119
|
} catch (error) {
|
|
1108
1120
|
throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
|
|
1109
1121
|
}
|
|
1110
|
-
if (eventIdFor(event) !== claimedId) throw new LineWebhookPayloadError("LINE webhook event id changed after durable admission.");
|
|
1111
1122
|
return event;
|
|
1112
1123
|
}
|
|
1113
1124
|
function errorText(error) {
|
|
@@ -1118,41 +1129,61 @@ function isLineAuthenticationFailure(error) {
|
|
|
1118
1129
|
const status = error.status;
|
|
1119
1130
|
return status === 401 || status === 403;
|
|
1120
1131
|
}
|
|
1132
|
+
async function waitForActiveDeliveriesBeforeDispose(activeDeliveries) {
|
|
1133
|
+
let timeout;
|
|
1134
|
+
try {
|
|
1135
|
+
return await Promise.race([Promise.allSettled(activeDeliveries).then(() => true), new Promise((resolve) => {
|
|
1136
|
+
timeout = setTimeout(() => resolve(false), LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS);
|
|
1137
|
+
timeout.unref?.();
|
|
1138
|
+
})]);
|
|
1139
|
+
} finally {
|
|
1140
|
+
if (timeout) clearTimeout(timeout);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1121
1143
|
function createLineWebhookSpool(options) {
|
|
1122
1144
|
const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
|
|
1123
|
-
const shutdown = new AbortController();
|
|
1124
1145
|
const activeDeliveries = /* @__PURE__ */ new Set();
|
|
1125
1146
|
const deferredClaims = /* @__PURE__ */ new Map();
|
|
1126
|
-
|
|
1147
|
+
let acceptsDeferredClaims = true;
|
|
1148
|
+
const monitor = createChannelIngressMonitor({
|
|
1127
1149
|
queue,
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
retryPolicy: {
|
|
1134
|
-
maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
|
|
1135
|
-
deadLetterMinAgeMs: 0
|
|
1136
|
-
},
|
|
1137
|
-
resolveNonRetryableFailure: (error) => {
|
|
1138
|
-
if (error instanceof LineWebhookPayloadError) return {
|
|
1139
|
-
reason: "invalid-event",
|
|
1140
|
-
message: error.message
|
|
1150
|
+
inspect: ({ event }) => {
|
|
1151
|
+
const eventId = eventIdFor(event);
|
|
1152
|
+
return {
|
|
1153
|
+
eventId,
|
|
1154
|
+
laneKey: laneKeyFor(event, eventId)
|
|
1141
1155
|
};
|
|
1142
|
-
if (error instanceof LineWebhookTerminalDeliveryError) return {
|
|
1143
|
-
reason: error.reason,
|
|
1144
|
-
message: error.message
|
|
1145
|
-
};
|
|
1146
|
-
if (isLineAuthenticationFailure(error)) return {
|
|
1147
|
-
reason: "authentication-failed",
|
|
1148
|
-
message: errorText(error)
|
|
1149
|
-
};
|
|
1150
|
-
return null;
|
|
1151
1156
|
},
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1157
|
+
payload: {
|
|
1158
|
+
version: LINE_WEBHOOK_SPOOL_VERSION,
|
|
1159
|
+
serialize: ({ event, destination }) => ({
|
|
1160
|
+
rawEvent: JSON.stringify(event),
|
|
1161
|
+
destination
|
|
1162
|
+
}),
|
|
1163
|
+
deserialize: ({ rawEvent, destination }) => ({
|
|
1164
|
+
event: parseStoredEvent(rawEvent),
|
|
1165
|
+
destination
|
|
1166
|
+
}),
|
|
1167
|
+
encode: ({ version, body }) => ({
|
|
1168
|
+
version,
|
|
1169
|
+
rawEvent: body.rawEvent,
|
|
1170
|
+
destination: body.destination
|
|
1171
|
+
}),
|
|
1172
|
+
decode: (payload) => {
|
|
1173
|
+
if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
|
|
1174
|
+
return {
|
|
1175
|
+
version: payload.version,
|
|
1176
|
+
body: {
|
|
1177
|
+
rawEvent: payload.rawEvent,
|
|
1178
|
+
destination: payload.destination
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
},
|
|
1182
|
+
createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? "LINE webhook spool payload is invalid." : "LINE webhook event identity changed after durable admission.")
|
|
1183
|
+
},
|
|
1184
|
+
deliver: async ({ event, destination }, lifecycle, claim) => {
|
|
1155
1185
|
const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
|
|
1186
|
+
let handedOff = false;
|
|
1156
1187
|
let resolveDeferredClaim;
|
|
1157
1188
|
const deferredClaim = new Promise((resolve) => {
|
|
1158
1189
|
resolveDeferredClaim = resolve;
|
|
@@ -1161,12 +1192,13 @@ function createLineWebhookSpool(options) {
|
|
|
1161
1192
|
const settleDeferredClaim = () => {
|
|
1162
1193
|
if (deferredClaimSettled) return;
|
|
1163
1194
|
deferredClaimSettled = true;
|
|
1164
|
-
if (deferredClaims.get(
|
|
1195
|
+
if (deferredClaims.get(claim.id) === deferredClaim) deferredClaims.delete(claim.id);
|
|
1165
1196
|
resolveDeferredClaim();
|
|
1166
1197
|
};
|
|
1167
|
-
const delivery = options.deliver(event,
|
|
1198
|
+
const delivery = options.deliver(event, destination, { turnAdoptionLifecycle: {
|
|
1168
1199
|
...boundLifecycle,
|
|
1169
1200
|
onAdopted: async () => {
|
|
1201
|
+
handedOff = true;
|
|
1170
1202
|
try {
|
|
1171
1203
|
await boundLifecycle.onAdopted();
|
|
1172
1204
|
} finally {
|
|
@@ -1174,10 +1206,19 @@ function createLineWebhookSpool(options) {
|
|
|
1174
1206
|
}
|
|
1175
1207
|
},
|
|
1176
1208
|
onDeferred: () => {
|
|
1177
|
-
|
|
1209
|
+
handedOff = true;
|
|
1210
|
+
if (!acceptsDeferredClaims) {
|
|
1211
|
+
settleDeferredClaim();
|
|
1212
|
+
Promise.resolve().then(() => boundLifecycle.onAbandoned()).catch((error) => {
|
|
1213
|
+
options.runtime.error?.(danger(`line: failed to abandon a late webhook delivery: ${errorText(error)}`));
|
|
1214
|
+
});
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (!deferredClaimSettled) deferredClaims.set(claim.id, deferredClaim);
|
|
1178
1218
|
boundLifecycle.onDeferred();
|
|
1179
1219
|
},
|
|
1180
1220
|
onAbandoned: async () => {
|
|
1221
|
+
handedOff = true;
|
|
1181
1222
|
try {
|
|
1182
1223
|
await boundLifecycle.onAbandoned();
|
|
1183
1224
|
} finally {
|
|
@@ -1191,73 +1232,80 @@ function createLineWebhookSpool(options) {
|
|
|
1191
1232
|
} finally {
|
|
1192
1233
|
activeDeliveries.delete(delivery);
|
|
1193
1234
|
}
|
|
1194
|
-
|
|
1235
|
+
if (stopTask && !handedOff) return {
|
|
1236
|
+
kind: "failed-retryable",
|
|
1237
|
+
error: /* @__PURE__ */ new Error("LINE webhook spool stopped before delivery handoff.")
|
|
1238
|
+
};
|
|
1239
|
+
},
|
|
1240
|
+
pollIntervalMs: LINE_WEBHOOK_DRAIN_INTERVAL_MS,
|
|
1241
|
+
retention: {
|
|
1242
|
+
pruneIntervalMs: 0,
|
|
1243
|
+
completedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
|
|
1244
|
+
completedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES,
|
|
1245
|
+
failedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
|
|
1246
|
+
failedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES
|
|
1247
|
+
},
|
|
1248
|
+
appendRetryDelaysMs: [0],
|
|
1249
|
+
waitForDeliveryIdleBeforeRepump: false,
|
|
1250
|
+
waitForDeliveryIdleOnStop: false,
|
|
1251
|
+
runPumpTask: runDetachedWebhookWork,
|
|
1252
|
+
admissionMode: "durable-after-stop",
|
|
1253
|
+
drain: {
|
|
1254
|
+
adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
|
|
1255
|
+
orderBy: "received",
|
|
1256
|
+
scanLimit: LINE_WEBHOOK_DRAIN_SCAN_LIMIT,
|
|
1257
|
+
startLimit: LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
|
|
1258
|
+
retryPolicy: {
|
|
1259
|
+
maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
|
|
1260
|
+
deadLetterMinAgeMs: 0
|
|
1261
|
+
},
|
|
1262
|
+
resolveNonRetryableFailure: (error) => {
|
|
1263
|
+
if (error instanceof LineWebhookPayloadError) return {
|
|
1264
|
+
reason: "invalid-event",
|
|
1265
|
+
message: error.message
|
|
1266
|
+
};
|
|
1267
|
+
if (error instanceof LineWebhookTerminalDeliveryError) return {
|
|
1268
|
+
reason: error.reason,
|
|
1269
|
+
message: error.message
|
|
1270
|
+
};
|
|
1271
|
+
if (isLineAuthenticationFailure(error)) return {
|
|
1272
|
+
reason: "authentication-failed",
|
|
1273
|
+
message: errorText(error)
|
|
1274
|
+
};
|
|
1275
|
+
return null;
|
|
1276
|
+
},
|
|
1277
|
+
onLog: (message) => options.runtime.error?.(danger(`line: ${message}`))
|
|
1278
|
+
},
|
|
1279
|
+
createStoppedError: () => /* @__PURE__ */ new Error("LINE webhook spool is stopped."),
|
|
1280
|
+
onError: (error) => options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`))
|
|
1195
1281
|
});
|
|
1196
|
-
let
|
|
1197
|
-
let drainRequested = false;
|
|
1198
|
-
let drainTask;
|
|
1199
|
-
let drainTimer;
|
|
1200
|
-
const requestDrain = () => {
|
|
1201
|
-
if (!running || shutdown.signal.aborted) return;
|
|
1202
|
-
drainRequested = true;
|
|
1203
|
-
if (drainTask) return;
|
|
1204
|
-
drainTask = runDetachedWebhookWork(async () => {
|
|
1205
|
-
while (drainRequested && !shutdown.signal.aborted) {
|
|
1206
|
-
if (!running) break;
|
|
1207
|
-
drainRequested = false;
|
|
1208
|
-
await drain.drainOnce({ shouldStop: () => !running || shutdown.signal.aborted || activeDeliveries.size >= LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES });
|
|
1209
|
-
}
|
|
1210
|
-
}).catch((error) => {
|
|
1211
|
-
options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`));
|
|
1212
|
-
}).finally(() => {
|
|
1213
|
-
drainTask = void 0;
|
|
1214
|
-
if (running && drainRequested && !shutdown.signal.aborted) requestDrain();
|
|
1215
|
-
});
|
|
1216
|
-
};
|
|
1282
|
+
let stopTask;
|
|
1217
1283
|
return {
|
|
1218
1284
|
accept: async (body) => {
|
|
1219
1285
|
const events = body.events ?? [];
|
|
1220
1286
|
if (events.length === 0) return;
|
|
1221
|
-
await
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
failedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES
|
|
1226
|
-
});
|
|
1227
|
-
const receivedAt = Date.now();
|
|
1228
|
-
for (const event of events) {
|
|
1229
|
-
const eventId = eventIdFor(event);
|
|
1230
|
-
await queue.enqueue(eventId, {
|
|
1231
|
-
version: LINE_WEBHOOK_SPOOL_VERSION,
|
|
1232
|
-
rawEvent: JSON.stringify(event),
|
|
1233
|
-
destination: body.destination ?? ""
|
|
1234
|
-
}, {
|
|
1235
|
-
receivedAt,
|
|
1236
|
-
laneKey: laneKeyFor(event, eventId)
|
|
1237
|
-
});
|
|
1238
|
-
}
|
|
1239
|
-
requestDrain();
|
|
1287
|
+
await monitor.admitBatch(events.map((event) => ({
|
|
1288
|
+
event,
|
|
1289
|
+
destination: body.destination ?? ""
|
|
1290
|
+
})), { receivedAt: Date.now() });
|
|
1240
1291
|
},
|
|
1241
1292
|
start: () => {
|
|
1242
|
-
if (
|
|
1243
|
-
running = true;
|
|
1244
|
-
requestDrain();
|
|
1245
|
-
drainTimer = setInterval(requestDrain, LINE_WEBHOOK_DRAIN_INTERVAL_MS);
|
|
1246
|
-
drainTimer.unref?.();
|
|
1293
|
+
if (!stopTask) monitor.start();
|
|
1247
1294
|
},
|
|
1248
|
-
stop:
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1295
|
+
stop: () => {
|
|
1296
|
+
stopTask ??= (async () => {
|
|
1297
|
+
await monitor.pause();
|
|
1298
|
+
try {
|
|
1299
|
+
const deliveriesSettled = await waitForActiveDeliveriesBeforeDispose(activeDeliveries);
|
|
1300
|
+
if (!deliveriesSettled) options.runtime.log(warn(`line: timed out after ${LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS}ms waiting for active webhook deliveries; releasing drain ownership`));
|
|
1301
|
+
while (deferredClaims.size > 0) await Promise.allSettled(deferredClaims.values());
|
|
1302
|
+
acceptsDeferredClaims = false;
|
|
1303
|
+
if (deliveriesSettled) await monitor.waitForIdle();
|
|
1304
|
+
} finally {
|
|
1305
|
+
await monitor.stop();
|
|
1306
|
+
}
|
|
1307
|
+
})();
|
|
1308
|
+
return stopTask;
|
|
1261
1309
|
}
|
|
1262
1310
|
};
|
|
1263
1311
|
}
|
|
@@ -1311,56 +1359,6 @@ function resolveLineDurableReplyOptions(params) {
|
|
|
1311
1359
|
return { to: params.to };
|
|
1312
1360
|
}
|
|
1313
1361
|
//#endregion
|
|
1314
|
-
//#region extensions/line/src/reply-chunks.ts
|
|
1315
|
-
async function sendLineReplyChunks(params) {
|
|
1316
|
-
const quickReplies = params.quickReplies?.length ? params.quickReplies : void 0;
|
|
1317
|
-
let replyTokenUsed = Boolean(params.replyTokenUsed);
|
|
1318
|
-
if (params.chunks.length === 0) return { replyTokenUsed };
|
|
1319
|
-
if (params.replyToken && !replyTokenUsed) {
|
|
1320
|
-
let replySucceeded = false;
|
|
1321
|
-
try {
|
|
1322
|
-
const replyBatch = params.chunks.slice(0, 5);
|
|
1323
|
-
const remaining = params.chunks.slice(replyBatch.length);
|
|
1324
|
-
const replyMessages = replyBatch.map((chunk) => ({
|
|
1325
|
-
type: "text",
|
|
1326
|
-
text: chunk
|
|
1327
|
-
}));
|
|
1328
|
-
if (quickReplies && remaining.length === 0 && replyMessages.length > 0) {
|
|
1329
|
-
const lastIndex = replyMessages.length - 1;
|
|
1330
|
-
replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"), quickReplies);
|
|
1331
|
-
}
|
|
1332
|
-
await params.replyMessageLine(params.replyToken, replyMessages, {
|
|
1333
|
-
cfg: params.cfg,
|
|
1334
|
-
accountId: params.accountId
|
|
1335
|
-
});
|
|
1336
|
-
replyTokenUsed = true;
|
|
1337
|
-
replySucceeded = true;
|
|
1338
|
-
for (const [i, chunk] of remaining.entries()) if (i === remaining.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
|
1339
|
-
cfg: params.cfg,
|
|
1340
|
-
accountId: params.accountId
|
|
1341
|
-
});
|
|
1342
|
-
else await params.pushMessageLine(params.to, chunk, {
|
|
1343
|
-
cfg: params.cfg,
|
|
1344
|
-
accountId: params.accountId
|
|
1345
|
-
});
|
|
1346
|
-
return { replyTokenUsed };
|
|
1347
|
-
} catch (err) {
|
|
1348
|
-
if (replySucceeded) throw err;
|
|
1349
|
-
params.onReplyError?.(err);
|
|
1350
|
-
replyTokenUsed = true;
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
for (const [i, chunk] of params.chunks.entries()) if (i === params.chunks.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
|
1354
|
-
cfg: params.cfg,
|
|
1355
|
-
accountId: params.accountId
|
|
1356
|
-
});
|
|
1357
|
-
else await params.pushMessageLine(params.to, chunk, {
|
|
1358
|
-
cfg: params.cfg,
|
|
1359
|
-
accountId: params.accountId
|
|
1360
|
-
});
|
|
1361
|
-
return { replyTokenUsed };
|
|
1362
|
-
}
|
|
1363
|
-
//#endregion
|
|
1364
1362
|
//#region extensions/line/src/signature.ts
|
|
1365
1363
|
function validateLineSignature(body, signature, channelSecret) {
|
|
1366
1364
|
return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
|
|
@@ -1576,12 +1574,8 @@ async function monitorLineProvider(opts) {
|
|
|
1576
1574
|
buildTemplateMessageFromPayload,
|
|
1577
1575
|
processLineMessage,
|
|
1578
1576
|
chunkMarkdownText,
|
|
1579
|
-
sendLineReplyChunks,
|
|
1580
1577
|
replyMessageLine,
|
|
1581
|
-
pushMessageLine,
|
|
1582
|
-
pushTextMessageWithQuickReplies,
|
|
1583
1578
|
createQuickReplyItems,
|
|
1584
|
-
createTextMessageWithQuickReplies,
|
|
1585
1579
|
pushMessagesLine,
|
|
1586
1580
|
createFlexMessage,
|
|
1587
1581
|
buildMediaMessage: buildLineMediaMessage,
|