@openclaw/line 2026.7.2-beta.3 → 2026.7.2-beta.4
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 +4 -3
- package/dist/basic-cards-Rb-Ow7LN.js +307 -0
- package/dist/{card-command-DQ-epsgt.js → card-command-Ch0TwcDW.js} +2 -1
- package/dist/{channel-uT5RTtpi.js → channel-CNcv74n2.js} +11 -7
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel.runtime-BBCZvRa_.js +4 -0
- package/dist/{flex-templates-K1lS9i8L.js → flex-templates-KiKaVYgi.js} +1 -318
- package/dist/index.js +1 -1
- package/dist/{markdown-to-line-D1vapcdP.js → markdown-to-line-DWIdzBfy.js} +213 -218
- package/dist/{monitor-Bk-lpoOZ.js → monitor-BinAXvIY.js} +276 -270
- package/dist/monitor.runtime-DKj4FHkA.js +2 -0
- package/dist/outbound.runtime-DRyoVa81.js +2 -0
- package/dist/quick-reply-fallback-D86FwAfr.js +56 -0
- package/dist/{reply-payload-transform-r9sa-TYT.js → reply-payload-transform-d57WYHFm.js} +3 -164
- package/dist/runtime-api.js +7 -5
- package/dist/send-receipt-CY2ey8XH.js +107 -0
- package/dist/setup-api.js +1 -1
- package/dist/{setup-surface-BEt4mazl.js → setup-surface-Bili5gWy.js} +62 -4
- 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/npm-shrinkwrap.json +10 -10
- package/openclaw.plugin.json +0 -18
- package/package.json +71 -6
- package/dist/channel.runtime-BqNglqnr.js +0 -4
- package/dist/monitor.runtime-CHlqxGVL.js +0 -2
- package/dist/outbound.runtime-ByZ0Dsvu.js +0 -2
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BUBOoW5Q.js";
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
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-DWIdzBfy.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 { 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";
|
|
@@ -52,44 +53,85 @@ const CONTENT_READY_BASE_DELAY_MS = 500;
|
|
|
52
53
|
const CONTENT_READY_MAX_DELAY_MS = 4e3;
|
|
53
54
|
const CONTENT_READY_TIMEOUT_MS = 15e3;
|
|
54
55
|
const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
|
|
56
|
+
var RetryableLineMediaFetchError = class extends MediaFetchError {
|
|
57
|
+
constructor(message, options) {
|
|
58
|
+
super("fetch_failed", message, options);
|
|
59
|
+
this.name = "RetryableLineMediaFetchError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
55
62
|
function contentBackoffDelayMs(attempt) {
|
|
56
63
|
return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
|
|
57
64
|
}
|
|
65
|
+
function lineContentUrl(messageId) {
|
|
66
|
+
return `${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`;
|
|
67
|
+
}
|
|
68
|
+
async function* lineResponseBodyChunks(response, messageId) {
|
|
69
|
+
const body = response.body;
|
|
70
|
+
if (!body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
71
|
+
const reader = body.getReader();
|
|
72
|
+
let completed = false;
|
|
73
|
+
try {
|
|
74
|
+
while (true) {
|
|
75
|
+
let chunk;
|
|
76
|
+
try {
|
|
77
|
+
chunk = await reader.read();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
throw new RetryableLineMediaFetchError(`LINE media response stream failed for message ${messageId}`, { cause: err });
|
|
80
|
+
}
|
|
81
|
+
if (chunk.done) {
|
|
82
|
+
completed = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (chunk.value.byteLength > 0) yield chunk.value;
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
if (!completed) await reader.cancel().catch(() => void 0);
|
|
89
|
+
try {
|
|
90
|
+
reader.releaseLock();
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
58
94
|
async function fetchLineContentWhenReady(messageId, channelAccessToken) {
|
|
59
95
|
const controller = new AbortController();
|
|
60
96
|
const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
|
|
61
97
|
deadline.unref();
|
|
62
98
|
try {
|
|
63
99
|
for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
|
|
64
|
-
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(
|
|
100
|
+
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
|
|
65
101
|
headers: { Authorization: `Bearer ${channelAccessToken}` },
|
|
66
102
|
redirect: "error",
|
|
67
103
|
signal: controller.signal
|
|
68
104
|
});
|
|
69
105
|
if (response.status === 200) {
|
|
70
|
-
if (!response.body) throw new
|
|
71
|
-
return
|
|
106
|
+
if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
107
|
+
return response;
|
|
72
108
|
}
|
|
73
|
-
await response.body?.cancel();
|
|
74
|
-
if (response.status !== 202) throw new
|
|
109
|
+
await response.body?.cancel().catch(() => void 0);
|
|
110
|
+
if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
|
|
75
111
|
if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
|
|
76
112
|
}
|
|
77
113
|
} catch (err) {
|
|
78
|
-
if (controller.signal.aborted) throw new
|
|
79
|
-
throw err;
|
|
114
|
+
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 });
|
|
115
|
+
if (err instanceof MediaFetchError) throw err;
|
|
116
|
+
throw new RetryableLineMediaFetchError(`LINE media download failed for message ${messageId}`, { cause: err });
|
|
80
117
|
} finally {
|
|
81
118
|
clearTimeout(deadline);
|
|
82
119
|
}
|
|
83
|
-
throw new
|
|
120
|
+
throw new MediaFetchError("http_error", `LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`, { status: 202 });
|
|
121
|
+
}
|
|
122
|
+
function isRetryableLineInboundMediaError(err) {
|
|
123
|
+
if (err instanceof RetryableLineMediaFetchError) return true;
|
|
124
|
+
if (!(err instanceof MediaFetchError)) return false;
|
|
125
|
+
if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
|
|
126
|
+
return false;
|
|
84
127
|
}
|
|
85
128
|
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
|
|
86
|
-
const
|
|
129
|
+
const response = await fetchLineContentWhenReady(messageId, channelAccessToken);
|
|
87
130
|
let saved;
|
|
88
131
|
try {
|
|
89
|
-
saved = await saveMediaStream(
|
|
132
|
+
saved = await saveMediaStream(lineResponseBodyChunks(response, messageId), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
|
|
90
133
|
} catch (err) {
|
|
91
|
-
|
|
92
|
-
await finished(content).catch(() => void 0);
|
|
134
|
+
await response.body?.cancel().catch(() => void 0);
|
|
93
135
|
throw err;
|
|
94
136
|
}
|
|
95
137
|
logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
|
|
@@ -102,7 +144,11 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
|
|
|
102
144
|
//#endregion
|
|
103
145
|
//#region extensions/line/src/auto-reply-delivery.ts
|
|
104
146
|
function toLineDeliveryError(error) {
|
|
105
|
-
return error instanceof Error ? error : new Error("LINE
|
|
147
|
+
return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
|
|
148
|
+
}
|
|
149
|
+
function getLineHttpError(error) {
|
|
150
|
+
if (error instanceof HTTPFetchError) return error;
|
|
151
|
+
return error instanceof Error && error.cause instanceof HTTPFetchError ? error.cause : void 0;
|
|
106
152
|
}
|
|
107
153
|
function markLineVisibleDeliveryError(error) {
|
|
108
154
|
const deliveryError = toLineDeliveryError(error);
|
|
@@ -113,7 +159,7 @@ function markLineVisibleDeliveryError(error) {
|
|
|
113
159
|
});
|
|
114
160
|
return deliveryError;
|
|
115
161
|
}
|
|
116
|
-
const visibleError = new Error("LINE
|
|
162
|
+
const visibleError = new Error("LINE message send failed", { cause: deliveryError });
|
|
117
163
|
Object.assign(visibleError, {
|
|
118
164
|
sentBeforeError: true,
|
|
119
165
|
visibleReplySent: true
|
|
@@ -135,14 +181,25 @@ async function deliverLineAutoReply(params) {
|
|
|
135
181
|
}
|
|
136
182
|
};
|
|
137
183
|
const replyVisible = (...args) => sendVisible(() => deps.replyMessageLine(...args));
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const pushLineMessages = async (messages) => {
|
|
184
|
+
const failedPushSegments = /* @__PURE__ */ new WeakMap();
|
|
185
|
+
const pushLineMessages = async (messages, allowFailedBatchTextRecovery, externalTail = []) => {
|
|
141
186
|
if (messages.length === 0) return;
|
|
142
|
-
for (let i = 0; i < messages.length; i += 5)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
187
|
+
for (let i = 0; i < messages.length; i += 5) {
|
|
188
|
+
const batch = messages.slice(i, i + 5);
|
|
189
|
+
try {
|
|
190
|
+
await sendVisible(() => deps.pushMessagesLine(to, batch, {
|
|
191
|
+
cfg: params.cfg,
|
|
192
|
+
accountId
|
|
193
|
+
}));
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (typeof error === "object" && error !== null) failedPushSegments.set(error, {
|
|
196
|
+
allowFailedBatchTextRecovery,
|
|
197
|
+
failedBatch: batch,
|
|
198
|
+
unattemptedTail: [...messages.slice(i + batch.length), ...externalTail]
|
|
199
|
+
});
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
146
203
|
};
|
|
147
204
|
const sendLineMessages = async (messages, allowReplyToken) => {
|
|
148
205
|
if (messages.length === 0) return;
|
|
@@ -156,12 +213,13 @@ async function deliverLineAutoReply(params) {
|
|
|
156
213
|
});
|
|
157
214
|
} catch (err) {
|
|
158
215
|
deps.onReplyError?.(err);
|
|
159
|
-
await pushLineMessages(replyBatch);
|
|
216
|
+
await pushLineMessages(replyBatch, getLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
|
|
217
|
+
} finally {
|
|
218
|
+
replyTokenUsed = true;
|
|
160
219
|
}
|
|
161
|
-
replyTokenUsed = true;
|
|
162
220
|
remaining = remaining.slice(replyBatch.length);
|
|
163
221
|
}
|
|
164
|
-
if (remaining.length > 0) await pushLineMessages(remaining);
|
|
222
|
+
if (remaining.length > 0) await pushLineMessages(remaining, true);
|
|
165
223
|
};
|
|
166
224
|
const richMessages = [];
|
|
167
225
|
const hasQuickReplies = Boolean(lineData.quickReplies?.length);
|
|
@@ -186,82 +244,59 @@ async function deliverLineAutoReply(params) {
|
|
|
186
244
|
trackingId: lineData.trackingId
|
|
187
245
|
};
|
|
188
246
|
const mediaMessages = [];
|
|
189
|
-
let
|
|
247
|
+
let deliveryError;
|
|
190
248
|
for (const rawUrl of mediaUrls) {
|
|
191
249
|
const url = rawUrl?.trim();
|
|
192
250
|
if (!url) continue;
|
|
193
251
|
try {
|
|
194
252
|
mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
|
|
195
253
|
} catch (err) {
|
|
196
|
-
|
|
254
|
+
deliveryError ??= err;
|
|
197
255
|
}
|
|
198
256
|
}
|
|
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
|
-
}
|
|
257
|
+
const textMessages = chunks.map((text) => ({
|
|
258
|
+
type: "text",
|
|
259
|
+
text
|
|
260
|
+
}));
|
|
261
|
+
const richMediaMessages = [...richMessages, ...mediaMessages];
|
|
262
|
+
if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
|
|
263
|
+
type: "text",
|
|
264
|
+
text: buildLineQuickReplyFallbackText(lineData.quickReplies)
|
|
265
|
+
});
|
|
266
|
+
if (hasQuickReplies) {
|
|
267
|
+
const targetMessages = textMessages.length > 0 ? textMessages : richMediaMessages;
|
|
268
|
+
const lastIndex = targetMessages.length - 1;
|
|
269
|
+
targetMessages[lastIndex] = {
|
|
270
|
+
...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
|
|
271
|
+
quickReply: deps.createQuickReplyItems(lineData.quickReplies)
|
|
272
|
+
};
|
|
257
273
|
}
|
|
258
|
-
|
|
259
|
-
|
|
274
|
+
const messages = hasQuickReplies ? [...richMediaMessages, ...textMessages] : [...textMessages, ...richMediaMessages];
|
|
275
|
+
try {
|
|
276
|
+
await sendLineMessages(messages, true);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
deliveryError ??= err;
|
|
279
|
+
const failedSegment = typeof err === "object" && err !== null ? failedPushSegments.get(err) : void 0;
|
|
280
|
+
const httpError = getLineHttpError(err);
|
|
281
|
+
const retryCandidates = failedSegment ? [...failedSegment.allowFailedBatchTextRecovery ? failedSegment.failedBatch : [], ...failedSegment.unattemptedTail] : [];
|
|
282
|
+
const retryTextMessages = retryCandidates.filter((message) => message.type === "text");
|
|
283
|
+
const quickRepliesNeedCarrier = hasQuickReplies && retryCandidates.some((message) => "quickReply" in message);
|
|
284
|
+
const retryMessages = retryTextMessages.length > 0 ? retryTextMessages : quickRepliesNeedCarrier ? [{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: buildLineQuickReplyFallbackText(lineData.quickReplies),
|
|
287
|
+
quickReply: deps.createQuickReplyItems(lineData.quickReplies)
|
|
288
|
+
}] : [];
|
|
289
|
+
if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400) try {
|
|
290
|
+
await sendLineMessages(retryMessages, false);
|
|
291
|
+
} catch {}
|
|
292
|
+
}
|
|
293
|
+
if (deliveryError !== void 0) {
|
|
294
|
+
if (!visibleReplySent) throw toLineDeliveryError(deliveryError);
|
|
260
295
|
return {
|
|
261
296
|
status: "partial",
|
|
262
297
|
replyTokenUsed,
|
|
263
298
|
visibleReplySent: true,
|
|
264
|
-
error: markLineVisibleDeliveryError(
|
|
299
|
+
error: markLineVisibleDeliveryError(deliveryError)
|
|
265
300
|
};
|
|
266
301
|
}
|
|
267
302
|
return {
|
|
@@ -397,13 +432,13 @@ function extractMessageText(message) {
|
|
|
397
432
|
}
|
|
398
433
|
return "";
|
|
399
434
|
}
|
|
400
|
-
function
|
|
435
|
+
function extractNativeMediaKind(message) {
|
|
401
436
|
switch (message.type) {
|
|
402
|
-
case "image": return "
|
|
403
|
-
case "video": return "
|
|
404
|
-
case "audio": return "
|
|
405
|
-
case "file": return "
|
|
406
|
-
default: return
|
|
437
|
+
case "image": return "image";
|
|
438
|
+
case "video": return "video";
|
|
439
|
+
case "audio": return "audio";
|
|
440
|
+
case "file": return "document";
|
|
441
|
+
default: return;
|
|
407
442
|
}
|
|
408
443
|
}
|
|
409
444
|
function resolveLineConversationLabel(params) {
|
|
@@ -439,6 +474,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
439
474
|
sessionKey: params.route.sessionKey
|
|
440
475
|
});
|
|
441
476
|
const agentBody = params.agentBody ?? params.rawBody;
|
|
477
|
+
const media = toInboundMediaFacts(params.media);
|
|
442
478
|
const body = formatInboundEnvelope({
|
|
443
479
|
channel: "LINE",
|
|
444
480
|
from: conversationLabel,
|
|
@@ -467,12 +503,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
467
503
|
Surface: "line",
|
|
468
504
|
MessageSid: params.messageSid,
|
|
469
505
|
Timestamp: params.timestamp,
|
|
470
|
-
|
|
471
|
-
MediaType: params.media.firstContentType,
|
|
472
|
-
MediaUrl: params.media.firstPath,
|
|
473
|
-
MediaPaths: params.media.paths,
|
|
474
|
-
MediaUrls: params.media.paths,
|
|
475
|
-
MediaTypes: params.media.types,
|
|
506
|
+
media,
|
|
476
507
|
...params.locationContext,
|
|
477
508
|
CommandAuthorized: params.commandAuthorized,
|
|
478
509
|
OriginatingChannel: "line",
|
|
@@ -535,15 +566,17 @@ async function buildLineMessageContext(params) {
|
|
|
535
566
|
const messageId = message.id;
|
|
536
567
|
const timestamp = event.timestamp;
|
|
537
568
|
const textContent = extractMessageText(message);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
569
|
+
const nativeMediaKind = extractNativeMediaKind(message);
|
|
570
|
+
const mediaFacts = allMedia.length > 0 ? allMedia.map((media) => ({
|
|
571
|
+
...media,
|
|
572
|
+
kind: nativeMediaKind
|
|
573
|
+
})) : nativeMediaKind ? [{ kind: nativeMediaKind }] : [];
|
|
574
|
+
const rawBody = textContent;
|
|
541
575
|
const agentBody = mediaUnavailable ? formatInboundMediaUnavailableText({
|
|
542
576
|
body: rawBody,
|
|
543
|
-
mediaPlaceholder: placeholder,
|
|
544
577
|
notice: "[line attachment unavailable]"
|
|
545
578
|
}) : rawBody;
|
|
546
|
-
if (!agentBody &&
|
|
579
|
+
if (!agentBody && mediaFacts.length === 0) return null;
|
|
547
580
|
let locationContext;
|
|
548
581
|
if (message.type === "location") {
|
|
549
582
|
const loc = message;
|
|
@@ -571,12 +604,7 @@ async function buildLineMessageContext(params) {
|
|
|
571
604
|
timestamp,
|
|
572
605
|
messageSid: messageId,
|
|
573
606
|
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
|
-
},
|
|
607
|
+
media: mediaFacts,
|
|
580
608
|
locationContext,
|
|
581
609
|
verboseLog: {
|
|
582
610
|
kind: "inbound",
|
|
@@ -632,12 +660,7 @@ async function buildLinePostbackContext(params) {
|
|
|
632
660
|
timestamp,
|
|
633
661
|
messageSid,
|
|
634
662
|
commandAuthorized,
|
|
635
|
-
media:
|
|
636
|
-
firstPath: "",
|
|
637
|
-
firstContentType: void 0,
|
|
638
|
-
paths: void 0,
|
|
639
|
-
types: void 0
|
|
640
|
-
},
|
|
663
|
+
media: [],
|
|
641
664
|
verboseLog: { kind: "postback" }
|
|
642
665
|
});
|
|
643
666
|
return {
|
|
@@ -952,6 +975,7 @@ async function handleMessageEvent(event, context) {
|
|
|
952
975
|
contentType: media.contentType
|
|
953
976
|
});
|
|
954
977
|
} catch (err) {
|
|
978
|
+
if (isRetryableLineInboundMediaError(err)) throw err;
|
|
955
979
|
mediaUnavailable = true;
|
|
956
980
|
const errMsg = String(err);
|
|
957
981
|
if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
|
|
@@ -1052,6 +1076,7 @@ const LINE_WEBHOOK_SPOOL_VERSION = 1;
|
|
|
1052
1076
|
const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
|
|
1053
1077
|
const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
|
|
1054
1078
|
const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
|
|
1079
|
+
const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
|
|
1055
1080
|
const LINE_WEBHOOK_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
|
|
1056
1081
|
const LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES = 4096;
|
|
1057
1082
|
var LineWebhookPayloadError = class extends Error {
|
|
@@ -1099,15 +1124,13 @@ function laneKeyFor(event, eventId) {
|
|
|
1099
1124
|
}
|
|
1100
1125
|
return eventId;
|
|
1101
1126
|
}
|
|
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.");
|
|
1127
|
+
function parseStoredEvent(rawEvent) {
|
|
1104
1128
|
let event;
|
|
1105
1129
|
try {
|
|
1106
|
-
event = JSON.parse(
|
|
1130
|
+
event = JSON.parse(rawEvent);
|
|
1107
1131
|
} catch (error) {
|
|
1108
1132
|
throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
|
|
1109
1133
|
}
|
|
1110
|
-
if (eventIdFor(event) !== claimedId) throw new LineWebhookPayloadError("LINE webhook event id changed after durable admission.");
|
|
1111
1134
|
return event;
|
|
1112
1135
|
}
|
|
1113
1136
|
function errorText(error) {
|
|
@@ -1118,41 +1141,61 @@ function isLineAuthenticationFailure(error) {
|
|
|
1118
1141
|
const status = error.status;
|
|
1119
1142
|
return status === 401 || status === 403;
|
|
1120
1143
|
}
|
|
1144
|
+
async function waitForActiveDeliveriesBeforeDispose(activeDeliveries) {
|
|
1145
|
+
let timeout;
|
|
1146
|
+
try {
|
|
1147
|
+
return await Promise.race([Promise.allSettled(activeDeliveries).then(() => true), new Promise((resolve) => {
|
|
1148
|
+
timeout = setTimeout(() => resolve(false), LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS);
|
|
1149
|
+
timeout.unref?.();
|
|
1150
|
+
})]);
|
|
1151
|
+
} finally {
|
|
1152
|
+
if (timeout) clearTimeout(timeout);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1121
1155
|
function createLineWebhookSpool(options) {
|
|
1122
1156
|
const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
|
|
1123
|
-
const shutdown = new AbortController();
|
|
1124
1157
|
const activeDeliveries = /* @__PURE__ */ new Set();
|
|
1125
1158
|
const deferredClaims = /* @__PURE__ */ new Map();
|
|
1126
|
-
|
|
1159
|
+
let acceptsDeferredClaims = true;
|
|
1160
|
+
const monitor = createChannelIngressMonitor({
|
|
1127
1161
|
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
|
|
1162
|
+
inspect: ({ event }) => {
|
|
1163
|
+
const eventId = eventIdFor(event);
|
|
1164
|
+
return {
|
|
1165
|
+
eventId,
|
|
1166
|
+
laneKey: laneKeyFor(event, eventId)
|
|
1141
1167
|
};
|
|
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
1168
|
},
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1169
|
+
payload: {
|
|
1170
|
+
version: LINE_WEBHOOK_SPOOL_VERSION,
|
|
1171
|
+
serialize: ({ event, destination }) => ({
|
|
1172
|
+
rawEvent: JSON.stringify(event),
|
|
1173
|
+
destination
|
|
1174
|
+
}),
|
|
1175
|
+
deserialize: ({ rawEvent, destination }) => ({
|
|
1176
|
+
event: parseStoredEvent(rawEvent),
|
|
1177
|
+
destination
|
|
1178
|
+
}),
|
|
1179
|
+
encode: ({ version, body }) => ({
|
|
1180
|
+
version,
|
|
1181
|
+
rawEvent: body.rawEvent,
|
|
1182
|
+
destination: body.destination
|
|
1183
|
+
}),
|
|
1184
|
+
decode: (payload) => {
|
|
1185
|
+
if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
|
|
1186
|
+
return {
|
|
1187
|
+
version: payload.version,
|
|
1188
|
+
body: {
|
|
1189
|
+
rawEvent: payload.rawEvent,
|
|
1190
|
+
destination: payload.destination
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
},
|
|
1194
|
+
createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? "LINE webhook spool payload is invalid." : "LINE webhook event identity changed after durable admission.")
|
|
1195
|
+
},
|
|
1196
|
+
deliver: async ({ event, destination }, lifecycle, claim) => {
|
|
1155
1197
|
const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
|
|
1198
|
+
let handedOff = false;
|
|
1156
1199
|
let resolveDeferredClaim;
|
|
1157
1200
|
const deferredClaim = new Promise((resolve) => {
|
|
1158
1201
|
resolveDeferredClaim = resolve;
|
|
@@ -1161,12 +1204,13 @@ function createLineWebhookSpool(options) {
|
|
|
1161
1204
|
const settleDeferredClaim = () => {
|
|
1162
1205
|
if (deferredClaimSettled) return;
|
|
1163
1206
|
deferredClaimSettled = true;
|
|
1164
|
-
if (deferredClaims.get(
|
|
1207
|
+
if (deferredClaims.get(claim.id) === deferredClaim) deferredClaims.delete(claim.id);
|
|
1165
1208
|
resolveDeferredClaim();
|
|
1166
1209
|
};
|
|
1167
|
-
const delivery = options.deliver(event,
|
|
1210
|
+
const delivery = options.deliver(event, destination, { turnAdoptionLifecycle: {
|
|
1168
1211
|
...boundLifecycle,
|
|
1169
1212
|
onAdopted: async () => {
|
|
1213
|
+
handedOff = true;
|
|
1170
1214
|
try {
|
|
1171
1215
|
await boundLifecycle.onAdopted();
|
|
1172
1216
|
} finally {
|
|
@@ -1174,10 +1218,19 @@ function createLineWebhookSpool(options) {
|
|
|
1174
1218
|
}
|
|
1175
1219
|
},
|
|
1176
1220
|
onDeferred: () => {
|
|
1177
|
-
|
|
1221
|
+
handedOff = true;
|
|
1222
|
+
if (!acceptsDeferredClaims) {
|
|
1223
|
+
settleDeferredClaim();
|
|
1224
|
+
Promise.resolve().then(() => boundLifecycle.onAbandoned()).catch((error) => {
|
|
1225
|
+
options.runtime.error?.(danger(`line: failed to abandon a late webhook delivery: ${errorText(error)}`));
|
|
1226
|
+
});
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (!deferredClaimSettled) deferredClaims.set(claim.id, deferredClaim);
|
|
1178
1230
|
boundLifecycle.onDeferred();
|
|
1179
1231
|
},
|
|
1180
1232
|
onAbandoned: async () => {
|
|
1233
|
+
handedOff = true;
|
|
1181
1234
|
try {
|
|
1182
1235
|
await boundLifecycle.onAbandoned();
|
|
1183
1236
|
} finally {
|
|
@@ -1191,73 +1244,80 @@ function createLineWebhookSpool(options) {
|
|
|
1191
1244
|
} finally {
|
|
1192
1245
|
activeDeliveries.delete(delivery);
|
|
1193
1246
|
}
|
|
1194
|
-
|
|
1247
|
+
if (stopTask && !handedOff) return {
|
|
1248
|
+
kind: "failed-retryable",
|
|
1249
|
+
error: /* @__PURE__ */ new Error("LINE webhook spool stopped before delivery handoff.")
|
|
1250
|
+
};
|
|
1251
|
+
},
|
|
1252
|
+
pollIntervalMs: LINE_WEBHOOK_DRAIN_INTERVAL_MS,
|
|
1253
|
+
retention: {
|
|
1254
|
+
pruneIntervalMs: 0,
|
|
1255
|
+
completedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
|
|
1256
|
+
completedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES,
|
|
1257
|
+
failedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
|
|
1258
|
+
failedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES
|
|
1259
|
+
},
|
|
1260
|
+
appendRetryDelaysMs: [0],
|
|
1261
|
+
waitForDeliveryIdleBeforeRepump: false,
|
|
1262
|
+
waitForDeliveryIdleOnStop: false,
|
|
1263
|
+
runPumpTask: runDetachedWebhookWork,
|
|
1264
|
+
admissionMode: "durable-after-stop",
|
|
1265
|
+
drain: {
|
|
1266
|
+
adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
|
|
1267
|
+
orderBy: "received",
|
|
1268
|
+
scanLimit: LINE_WEBHOOK_DRAIN_SCAN_LIMIT,
|
|
1269
|
+
startLimit: LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
|
|
1270
|
+
retryPolicy: {
|
|
1271
|
+
maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
|
|
1272
|
+
deadLetterMinAgeMs: 0
|
|
1273
|
+
},
|
|
1274
|
+
resolveNonRetryableFailure: (error) => {
|
|
1275
|
+
if (error instanceof LineWebhookPayloadError) return {
|
|
1276
|
+
reason: "invalid-event",
|
|
1277
|
+
message: error.message
|
|
1278
|
+
};
|
|
1279
|
+
if (error instanceof LineWebhookTerminalDeliveryError) return {
|
|
1280
|
+
reason: error.reason,
|
|
1281
|
+
message: error.message
|
|
1282
|
+
};
|
|
1283
|
+
if (isLineAuthenticationFailure(error)) return {
|
|
1284
|
+
reason: "authentication-failed",
|
|
1285
|
+
message: errorText(error)
|
|
1286
|
+
};
|
|
1287
|
+
return null;
|
|
1288
|
+
},
|
|
1289
|
+
onLog: (message) => options.runtime.error?.(danger(`line: ${message}`))
|
|
1290
|
+
},
|
|
1291
|
+
createStoppedError: () => /* @__PURE__ */ new Error("LINE webhook spool is stopped."),
|
|
1292
|
+
onError: (error) => options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`))
|
|
1195
1293
|
});
|
|
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
|
-
};
|
|
1294
|
+
let stopTask;
|
|
1217
1295
|
return {
|
|
1218
1296
|
accept: async (body) => {
|
|
1219
1297
|
const events = body.events ?? [];
|
|
1220
1298
|
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();
|
|
1299
|
+
await monitor.admitBatch(events.map((event) => ({
|
|
1300
|
+
event,
|
|
1301
|
+
destination: body.destination ?? ""
|
|
1302
|
+
})), { receivedAt: Date.now() });
|
|
1240
1303
|
},
|
|
1241
1304
|
start: () => {
|
|
1242
|
-
if (
|
|
1243
|
-
running = true;
|
|
1244
|
-
requestDrain();
|
|
1245
|
-
drainTimer = setInterval(requestDrain, LINE_WEBHOOK_DRAIN_INTERVAL_MS);
|
|
1246
|
-
drainTimer.unref?.();
|
|
1305
|
+
if (!stopTask) monitor.start();
|
|
1247
1306
|
},
|
|
1248
|
-
stop:
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1307
|
+
stop: () => {
|
|
1308
|
+
stopTask ??= (async () => {
|
|
1309
|
+
await monitor.pause();
|
|
1310
|
+
try {
|
|
1311
|
+
const deliveriesSettled = await waitForActiveDeliveriesBeforeDispose(activeDeliveries);
|
|
1312
|
+
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`));
|
|
1313
|
+
while (deferredClaims.size > 0) await Promise.allSettled(deferredClaims.values());
|
|
1314
|
+
acceptsDeferredClaims = false;
|
|
1315
|
+
if (deliveriesSettled) await monitor.waitForIdle();
|
|
1316
|
+
} finally {
|
|
1317
|
+
await monitor.stop();
|
|
1318
|
+
}
|
|
1319
|
+
})();
|
|
1320
|
+
return stopTask;
|
|
1261
1321
|
}
|
|
1262
1322
|
};
|
|
1263
1323
|
}
|
|
@@ -1311,56 +1371,6 @@ function resolveLineDurableReplyOptions(params) {
|
|
|
1311
1371
|
return { to: params.to };
|
|
1312
1372
|
}
|
|
1313
1373
|
//#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
1374
|
//#region extensions/line/src/signature.ts
|
|
1365
1375
|
function validateLineSignature(body, signature, channelSecret) {
|
|
1366
1376
|
return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
|
|
@@ -1576,12 +1586,8 @@ async function monitorLineProvider(opts) {
|
|
|
1576
1586
|
buildTemplateMessageFromPayload,
|
|
1577
1587
|
processLineMessage,
|
|
1578
1588
|
chunkMarkdownText,
|
|
1579
|
-
sendLineReplyChunks,
|
|
1580
1589
|
replyMessageLine,
|
|
1581
|
-
pushMessageLine,
|
|
1582
|
-
pushTextMessageWithQuickReplies,
|
|
1583
1590
|
createQuickReplyItems,
|
|
1584
|
-
createTextMessageWithQuickReplies,
|
|
1585
1591
|
pushMessagesLine,
|
|
1586
1592
|
createFlexMessage,
|
|
1587
1593
|
buildMediaMessage: buildLineMediaMessage,
|