@openclaw/line 2026.7.2-beta.2 → 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/{accounts-DJVOv1JI.js → accounts-BUBOoW5Q.js} +95 -33
- 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-BHR-GoJ0.js → channel-CNcv74n2.js} +22 -10
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel.runtime-BBCZvRa_.js +4 -0
- package/dist/contract-api.js +1 -1
- package/dist/{flex-templates-K1lS9i8L.js → flex-templates-KiKaVYgi.js} +1 -318
- package/dist/index.js +1 -1
- package/dist/{markdown-to-line-XceqeKRP.js → markdown-to-line-DWIdzBfy.js} +224 -220
- package/dist/{monitor-Db2rZsSX.js → monitor-BinAXvIY.js} +454 -288
- 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 +10 -24
- package/dist/send-receipt-CY2ey8XH.js +107 -0
- package/dist/setup-api.js +1 -1
- package/dist/{setup-surface-B7W6pwn3.js → setup-surface-Bili5gWy.js} +64 -5
- 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-DFgZ_Xwn.js +0 -4
- package/dist/monitor.runtime-CZuQgsEE.js +0 -2
- package/dist/outbound.runtime-DnpU0Cms.js +0 -2
|
@@ -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-BUBOoW5Q.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-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 {
|
|
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";
|
|
@@ -25,7 +26,6 @@ import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
|
25
26
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
26
27
|
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
|
-
import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
29
29
|
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
30
30
|
import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
31
31
|
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
|
@@ -53,44 +53,85 @@ const CONTENT_READY_BASE_DELAY_MS = 500;
|
|
|
53
53
|
const CONTENT_READY_MAX_DELAY_MS = 4e3;
|
|
54
54
|
const CONTENT_READY_TIMEOUT_MS = 15e3;
|
|
55
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
|
+
};
|
|
56
62
|
function contentBackoffDelayMs(attempt) {
|
|
57
63
|
return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
|
|
58
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
|
+
}
|
|
59
94
|
async function fetchLineContentWhenReady(messageId, channelAccessToken) {
|
|
60
95
|
const controller = new AbortController();
|
|
61
96
|
const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
|
|
62
97
|
deadline.unref();
|
|
63
98
|
try {
|
|
64
99
|
for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
|
|
65
|
-
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(
|
|
100
|
+
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
|
|
66
101
|
headers: { Authorization: `Bearer ${channelAccessToken}` },
|
|
67
102
|
redirect: "error",
|
|
68
103
|
signal: controller.signal
|
|
69
104
|
});
|
|
70
105
|
if (response.status === 200) {
|
|
71
|
-
if (!response.body) throw new
|
|
72
|
-
return
|
|
106
|
+
if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
107
|
+
return response;
|
|
73
108
|
}
|
|
74
|
-
await response.body?.cancel();
|
|
75
|
-
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 });
|
|
76
111
|
if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
|
|
77
112
|
}
|
|
78
113
|
} catch (err) {
|
|
79
|
-
if (controller.signal.aborted) throw new
|
|
80
|
-
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 });
|
|
81
117
|
} finally {
|
|
82
118
|
clearTimeout(deadline);
|
|
83
119
|
}
|
|
84
|
-
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;
|
|
85
127
|
}
|
|
86
128
|
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
|
|
87
|
-
const
|
|
129
|
+
const response = await fetchLineContentWhenReady(messageId, channelAccessToken);
|
|
88
130
|
let saved;
|
|
89
131
|
try {
|
|
90
|
-
saved = await saveMediaStream(
|
|
132
|
+
saved = await saveMediaStream(lineResponseBodyChunks(response, messageId), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
|
|
91
133
|
} catch (err) {
|
|
92
|
-
|
|
93
|
-
await finished(content).catch(() => void 0);
|
|
134
|
+
await response.body?.cancel().catch(() => void 0);
|
|
94
135
|
throw err;
|
|
95
136
|
}
|
|
96
137
|
logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
|
|
@@ -103,7 +144,11 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
|
|
|
103
144
|
//#endregion
|
|
104
145
|
//#region extensions/line/src/auto-reply-delivery.ts
|
|
105
146
|
function toLineDeliveryError(error) {
|
|
106
|
-
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;
|
|
107
152
|
}
|
|
108
153
|
function markLineVisibleDeliveryError(error) {
|
|
109
154
|
const deliveryError = toLineDeliveryError(error);
|
|
@@ -114,7 +159,7 @@ function markLineVisibleDeliveryError(error) {
|
|
|
114
159
|
});
|
|
115
160
|
return deliveryError;
|
|
116
161
|
}
|
|
117
|
-
const visibleError = new Error("LINE
|
|
162
|
+
const visibleError = new Error("LINE message send failed", { cause: deliveryError });
|
|
118
163
|
Object.assign(visibleError, {
|
|
119
164
|
sentBeforeError: true,
|
|
120
165
|
visibleReplySent: true
|
|
@@ -136,14 +181,25 @@ async function deliverLineAutoReply(params) {
|
|
|
136
181
|
}
|
|
137
182
|
};
|
|
138
183
|
const replyVisible = (...args) => sendVisible(() => deps.replyMessageLine(...args));
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const pushLineMessages = async (messages) => {
|
|
184
|
+
const failedPushSegments = /* @__PURE__ */ new WeakMap();
|
|
185
|
+
const pushLineMessages = async (messages, allowFailedBatchTextRecovery, externalTail = []) => {
|
|
142
186
|
if (messages.length === 0) return;
|
|
143
|
-
for (let i = 0; i < messages.length; i += 5)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
}
|
|
147
203
|
};
|
|
148
204
|
const sendLineMessages = async (messages, allowReplyToken) => {
|
|
149
205
|
if (messages.length === 0) return;
|
|
@@ -157,12 +213,13 @@ async function deliverLineAutoReply(params) {
|
|
|
157
213
|
});
|
|
158
214
|
} catch (err) {
|
|
159
215
|
deps.onReplyError?.(err);
|
|
160
|
-
await pushLineMessages(replyBatch);
|
|
216
|
+
await pushLineMessages(replyBatch, getLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
|
|
217
|
+
} finally {
|
|
218
|
+
replyTokenUsed = true;
|
|
161
219
|
}
|
|
162
|
-
replyTokenUsed = true;
|
|
163
220
|
remaining = remaining.slice(replyBatch.length);
|
|
164
221
|
}
|
|
165
|
-
if (remaining.length > 0) await pushLineMessages(remaining);
|
|
222
|
+
if (remaining.length > 0) await pushLineMessages(remaining, true);
|
|
166
223
|
};
|
|
167
224
|
const richMessages = [];
|
|
168
225
|
const hasQuickReplies = Boolean(lineData.quickReplies?.length);
|
|
@@ -187,82 +244,59 @@ async function deliverLineAutoReply(params) {
|
|
|
187
244
|
trackingId: lineData.trackingId
|
|
188
245
|
};
|
|
189
246
|
const mediaMessages = [];
|
|
190
|
-
let
|
|
247
|
+
let deliveryError;
|
|
191
248
|
for (const rawUrl of mediaUrls) {
|
|
192
249
|
const url = rawUrl?.trim();
|
|
193
250
|
if (!url) continue;
|
|
194
251
|
try {
|
|
195
252
|
mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
|
|
196
253
|
} catch (err) {
|
|
197
|
-
|
|
254
|
+
deliveryError ??= err;
|
|
198
255
|
}
|
|
199
256
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
quickReplies: lineData.quickReplies,
|
|
236
|
-
replyToken,
|
|
237
|
-
replyTokenUsed,
|
|
238
|
-
cfg: params.cfg,
|
|
239
|
-
accountId,
|
|
240
|
-
replyMessageLine: replyVisible,
|
|
241
|
-
pushMessageLine: pushTextVisible,
|
|
242
|
-
pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
|
|
243
|
-
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
|
|
244
|
-
onReplyError: deps.onReplyError
|
|
245
|
-
});
|
|
246
|
-
replyTokenUsed = nextReplyTokenUsed;
|
|
247
|
-
} else {
|
|
248
|
-
if (hasQuickReplies && combined.length > 0) {
|
|
249
|
-
const quickReply = deps.createQuickReplyItems(lineData.quickReplies);
|
|
250
|
-
const targetIndex = replyToken && !replyTokenUsed ? Math.min(4, combined.length - 1) : combined.length - 1;
|
|
251
|
-
combined[targetIndex] = {
|
|
252
|
-
...combined[targetIndex],
|
|
253
|
-
quickReply
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
await sendLineMessages(combined, true);
|
|
257
|
-
}
|
|
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
|
+
};
|
|
273
|
+
}
|
|
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 {}
|
|
258
292
|
}
|
|
259
|
-
if (
|
|
260
|
-
if (!visibleReplySent) throw toLineDeliveryError(
|
|
293
|
+
if (deliveryError !== void 0) {
|
|
294
|
+
if (!visibleReplySent) throw toLineDeliveryError(deliveryError);
|
|
261
295
|
return {
|
|
262
296
|
status: "partial",
|
|
263
297
|
replyTokenUsed,
|
|
264
298
|
visibleReplySent: true,
|
|
265
|
-
error: markLineVisibleDeliveryError(
|
|
299
|
+
error: markLineVisibleDeliveryError(deliveryError)
|
|
266
300
|
};
|
|
267
301
|
}
|
|
268
302
|
return {
|
|
@@ -398,13 +432,13 @@ function extractMessageText(message) {
|
|
|
398
432
|
}
|
|
399
433
|
return "";
|
|
400
434
|
}
|
|
401
|
-
function
|
|
435
|
+
function extractNativeMediaKind(message) {
|
|
402
436
|
switch (message.type) {
|
|
403
|
-
case "image": return "
|
|
404
|
-
case "video": return "
|
|
405
|
-
case "audio": return "
|
|
406
|
-
case "file": return "
|
|
407
|
-
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;
|
|
408
442
|
}
|
|
409
443
|
}
|
|
410
444
|
function resolveLineConversationLabel(params) {
|
|
@@ -440,6 +474,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
440
474
|
sessionKey: params.route.sessionKey
|
|
441
475
|
});
|
|
442
476
|
const agentBody = params.agentBody ?? params.rawBody;
|
|
477
|
+
const media = toInboundMediaFacts(params.media);
|
|
443
478
|
const body = formatInboundEnvelope({
|
|
444
479
|
channel: "LINE",
|
|
445
480
|
from: conversationLabel,
|
|
@@ -458,6 +493,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
458
493
|
From: fromAddress,
|
|
459
494
|
To: toAddress,
|
|
460
495
|
SessionKey: params.route.sessionKey,
|
|
496
|
+
DmScope: params.route.dmScope,
|
|
461
497
|
AccountId: params.route.accountId,
|
|
462
498
|
ChatType: params.source.isGroup ? "group" : "direct",
|
|
463
499
|
ConversationLabel: 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 {
|
|
@@ -699,37 +722,9 @@ const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
699
722
|
function isDownloadableLineMessageType(messageType) {
|
|
700
723
|
return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
|
|
701
724
|
}
|
|
702
|
-
const LINE_WEBHOOK_REPLAY_WINDOW_MS = 600 * 1e3;
|
|
703
|
-
const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
|
|
704
725
|
function normalizeLineIngressEntry(value) {
|
|
705
726
|
return normalizeLineAllowEntry(value) || null;
|
|
706
727
|
}
|
|
707
|
-
function createLineWebhookReplayCache() {
|
|
708
|
-
return createChannelReplayGuard({
|
|
709
|
-
dedupe: {
|
|
710
|
-
ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
|
|
711
|
-
memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
|
|
712
|
-
},
|
|
713
|
-
buildReplayKey: ({ event, accountId }) => buildLineWebhookReplayKey(event, accountId)?.key
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
function buildLineWebhookReplayKey(event, accountId) {
|
|
717
|
-
if (event.type === "message") {
|
|
718
|
-
const messageId = event.message?.id?.trim();
|
|
719
|
-
if (messageId) return {
|
|
720
|
-
key: `${accountId}|message:${messageId}`,
|
|
721
|
-
eventId: `message:${messageId}`
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
const eventId = event.webhookEventId?.trim();
|
|
725
|
-
if (!eventId) return null;
|
|
726
|
-
const source = event.source;
|
|
727
|
-
const sourceId = source?.type === "group" ? `group:${source.groupId ?? ""}` : source?.type === "room" ? `room:${source.roomId ?? ""}` : `user:${source?.userId ?? ""}`;
|
|
728
|
-
return {
|
|
729
|
-
key: `${accountId}|${event.type}|${sourceId}|${eventId}`,
|
|
730
|
-
eventId: `event:${eventId}`
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
728
|
function resolveLineGroupConfig(params) {
|
|
734
729
|
return resolveLineGroupConfigEntry(params.config.groups, {
|
|
735
730
|
groupId: params.groupId,
|
|
@@ -980,6 +975,7 @@ async function handleMessageEvent(event, context) {
|
|
|
980
975
|
contentType: media.contentType
|
|
981
976
|
});
|
|
982
977
|
} catch (err) {
|
|
978
|
+
if (isRetryableLineInboundMediaError(err)) throw err;
|
|
983
979
|
mediaUnavailable = true;
|
|
984
980
|
const errMsg = String(err);
|
|
985
981
|
if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
|
|
@@ -998,7 +994,7 @@ async function handleMessageEvent(event, context) {
|
|
|
998
994
|
logVerbose("line: skipping empty message");
|
|
999
995
|
return;
|
|
1000
996
|
}
|
|
1001
|
-
await processMessage(messageContext);
|
|
997
|
+
await processMessage(messageContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
|
|
1002
998
|
historyReservation.commit();
|
|
1003
999
|
} finally {
|
|
1004
1000
|
historyReservation.release();
|
|
@@ -1032,30 +1028,12 @@ async function handlePostbackEvent(event, context) {
|
|
|
1032
1028
|
commandAuthorized: decision.commandAccess.authorized
|
|
1033
1029
|
});
|
|
1034
1030
|
if (!postbackContext) return;
|
|
1035
|
-
await context.processMessage(postbackContext);
|
|
1031
|
+
await context.processMessage(postbackContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
|
|
1036
1032
|
}
|
|
1037
1033
|
async function handleLineWebhookEvents(events, context) {
|
|
1038
1034
|
let firstError;
|
|
1039
1035
|
for (const event of events) try {
|
|
1040
|
-
|
|
1041
|
-
await handleLineWebhookEvent(event, context);
|
|
1042
|
-
continue;
|
|
1043
|
-
}
|
|
1044
|
-
const replayEvent = {
|
|
1045
|
-
event,
|
|
1046
|
-
accountId: context.account.accountId
|
|
1047
|
-
};
|
|
1048
|
-
const result = await context.replayCache.processGuarded(replayEvent, async () => await handleLineWebhookEvent(event, context), { onError: "commit" });
|
|
1049
|
-
const replayId = buildLineWebhookReplayKey(event, context.account.accountId)?.eventId;
|
|
1050
|
-
if (result.kind === "inflight") {
|
|
1051
|
-
logVerbose(`line: skipped in-flight replayed webhook event ${replayId ?? "unknown"}`);
|
|
1052
|
-
try {
|
|
1053
|
-
await result.pending;
|
|
1054
|
-
} catch (err) {
|
|
1055
|
-
context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
|
|
1056
|
-
firstError ??= err;
|
|
1057
|
-
}
|
|
1058
|
-
} else if (result.kind === "duplicate") logVerbose(`line: skipped replayed webhook event ${replayId ?? "unknown"}`);
|
|
1036
|
+
await handleLineWebhookEvent(event, context);
|
|
1059
1037
|
} catch (err) {
|
|
1060
1038
|
context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
|
|
1061
1039
|
firstError ??= err;
|
|
@@ -1093,6 +1071,257 @@ function toLintErrorObject(value, fallbackMessage) {
|
|
|
1093
1071
|
return error;
|
|
1094
1072
|
}
|
|
1095
1073
|
//#endregion
|
|
1074
|
+
//#region extensions/line/src/webhook-spool.ts
|
|
1075
|
+
const LINE_WEBHOOK_SPOOL_VERSION = 1;
|
|
1076
|
+
const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
|
|
1077
|
+
const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
|
|
1078
|
+
const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
|
|
1079
|
+
const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
|
|
1080
|
+
const LINE_WEBHOOK_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
|
|
1081
|
+
const LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES = 4096;
|
|
1082
|
+
var LineWebhookPayloadError = class extends Error {
|
|
1083
|
+
constructor(message, options) {
|
|
1084
|
+
super(message, options);
|
|
1085
|
+
this.name = "LineWebhookPayloadError";
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
var LineWebhookTerminalDeliveryError = class extends Error {
|
|
1089
|
+
constructor(message, options) {
|
|
1090
|
+
super(message, options);
|
|
1091
|
+
this.reason = "delivery-side-effects-committed";
|
|
1092
|
+
this.name = "LineWebhookTerminalDeliveryError";
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
function nonEmptyString(value) {
|
|
1096
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1097
|
+
}
|
|
1098
|
+
/** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
|
|
1099
|
+
function eventIdFor(event) {
|
|
1100
|
+
if (!event || typeof event !== "object") throw new LineWebhookPayloadError("LINE webhook event must be an object.");
|
|
1101
|
+
const candidate = event;
|
|
1102
|
+
if (candidate.type === "message") {
|
|
1103
|
+
const messageId = nonEmptyString(candidate.message?.id);
|
|
1104
|
+
if (messageId) return `message:${messageId}`;
|
|
1105
|
+
}
|
|
1106
|
+
const webhookEventId = nonEmptyString(candidate.webhookEventId);
|
|
1107
|
+
if (webhookEventId) return `event:${webhookEventId}`;
|
|
1108
|
+
throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
|
|
1109
|
+
}
|
|
1110
|
+
function laneKeyFor(event, eventId) {
|
|
1111
|
+
if (!event || typeof event !== "object") return eventId;
|
|
1112
|
+
const source = event.source;
|
|
1113
|
+
if (source?.type === "group") {
|
|
1114
|
+
const groupId = nonEmptyString(source.groupId);
|
|
1115
|
+
if (groupId) return `group:${groupId}`;
|
|
1116
|
+
}
|
|
1117
|
+
if (source?.type === "room") {
|
|
1118
|
+
const roomId = nonEmptyString(source.roomId);
|
|
1119
|
+
if (roomId) return `room:${roomId}`;
|
|
1120
|
+
}
|
|
1121
|
+
if (source?.type === "user") {
|
|
1122
|
+
const userId = nonEmptyString(source.userId);
|
|
1123
|
+
if (userId) return `user:${userId}`;
|
|
1124
|
+
}
|
|
1125
|
+
return eventId;
|
|
1126
|
+
}
|
|
1127
|
+
function parseStoredEvent(rawEvent) {
|
|
1128
|
+
let event;
|
|
1129
|
+
try {
|
|
1130
|
+
event = JSON.parse(rawEvent);
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
|
|
1133
|
+
}
|
|
1134
|
+
return event;
|
|
1135
|
+
}
|
|
1136
|
+
function errorText(error) {
|
|
1137
|
+
return error instanceof Error ? error.message : String(error);
|
|
1138
|
+
}
|
|
1139
|
+
function isLineAuthenticationFailure(error) {
|
|
1140
|
+
if (!error || typeof error !== "object") return false;
|
|
1141
|
+
const status = error.status;
|
|
1142
|
+
return status === 401 || status === 403;
|
|
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
|
+
}
|
|
1155
|
+
function createLineWebhookSpool(options) {
|
|
1156
|
+
const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
|
|
1157
|
+
const activeDeliveries = /* @__PURE__ */ new Set();
|
|
1158
|
+
const deferredClaims = /* @__PURE__ */ new Map();
|
|
1159
|
+
let acceptsDeferredClaims = true;
|
|
1160
|
+
const monitor = createChannelIngressMonitor({
|
|
1161
|
+
queue,
|
|
1162
|
+
inspect: ({ event }) => {
|
|
1163
|
+
const eventId = eventIdFor(event);
|
|
1164
|
+
return {
|
|
1165
|
+
eventId,
|
|
1166
|
+
laneKey: laneKeyFor(event, eventId)
|
|
1167
|
+
};
|
|
1168
|
+
},
|
|
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) => {
|
|
1197
|
+
const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
|
|
1198
|
+
let handedOff = false;
|
|
1199
|
+
let resolveDeferredClaim;
|
|
1200
|
+
const deferredClaim = new Promise((resolve) => {
|
|
1201
|
+
resolveDeferredClaim = resolve;
|
|
1202
|
+
});
|
|
1203
|
+
let deferredClaimSettled = false;
|
|
1204
|
+
const settleDeferredClaim = () => {
|
|
1205
|
+
if (deferredClaimSettled) return;
|
|
1206
|
+
deferredClaimSettled = true;
|
|
1207
|
+
if (deferredClaims.get(claim.id) === deferredClaim) deferredClaims.delete(claim.id);
|
|
1208
|
+
resolveDeferredClaim();
|
|
1209
|
+
};
|
|
1210
|
+
const delivery = options.deliver(event, destination, { turnAdoptionLifecycle: {
|
|
1211
|
+
...boundLifecycle,
|
|
1212
|
+
onAdopted: async () => {
|
|
1213
|
+
handedOff = true;
|
|
1214
|
+
try {
|
|
1215
|
+
await boundLifecycle.onAdopted();
|
|
1216
|
+
} finally {
|
|
1217
|
+
settleDeferredClaim();
|
|
1218
|
+
}
|
|
1219
|
+
},
|
|
1220
|
+
onDeferred: () => {
|
|
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);
|
|
1230
|
+
boundLifecycle.onDeferred();
|
|
1231
|
+
},
|
|
1232
|
+
onAbandoned: async () => {
|
|
1233
|
+
handedOff = true;
|
|
1234
|
+
try {
|
|
1235
|
+
await boundLifecycle.onAbandoned();
|
|
1236
|
+
} finally {
|
|
1237
|
+
settleDeferredClaim();
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
} });
|
|
1241
|
+
activeDeliveries.add(delivery);
|
|
1242
|
+
try {
|
|
1243
|
+
await delivery;
|
|
1244
|
+
} finally {
|
|
1245
|
+
activeDeliveries.delete(delivery);
|
|
1246
|
+
}
|
|
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)}`))
|
|
1293
|
+
});
|
|
1294
|
+
let stopTask;
|
|
1295
|
+
return {
|
|
1296
|
+
accept: async (body) => {
|
|
1297
|
+
const events = body.events ?? [];
|
|
1298
|
+
if (events.length === 0) return;
|
|
1299
|
+
await monitor.admitBatch(events.map((event) => ({
|
|
1300
|
+
event,
|
|
1301
|
+
destination: body.destination ?? ""
|
|
1302
|
+
})), { receivedAt: Date.now() });
|
|
1303
|
+
},
|
|
1304
|
+
start: () => {
|
|
1305
|
+
if (!stopTask) monitor.start();
|
|
1306
|
+
},
|
|
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;
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
//#endregion
|
|
1096
1325
|
//#region extensions/line/src/bot.ts
|
|
1097
1326
|
function createLineBot(opts) {
|
|
1098
1327
|
const runtime = opts.runtime ?? createNonExitingRuntime();
|
|
@@ -1105,24 +1334,26 @@ function createLineBot(opts) {
|
|
|
1105
1334
|
const processMessage = opts.onMessage ?? (async () => {
|
|
1106
1335
|
logVerbose("line: no message handler configured");
|
|
1107
1336
|
});
|
|
1108
|
-
const replayCache = createLineWebhookReplayCache();
|
|
1109
1337
|
const groupHistories = /* @__PURE__ */ new Map();
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1338
|
+
const spool = createLineWebhookSpool({
|
|
1339
|
+
accountId: account.accountId,
|
|
1340
|
+
runtime,
|
|
1341
|
+
deliver: async (event, _destination, control) => await handleLineWebhookEvents([event], {
|
|
1113
1342
|
cfg,
|
|
1114
1343
|
account,
|
|
1115
1344
|
runtime,
|
|
1116
1345
|
mediaMaxBytes,
|
|
1117
1346
|
processMessage,
|
|
1118
|
-
|
|
1347
|
+
...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
|
|
1119
1348
|
groupHistories,
|
|
1120
1349
|
historyLimit: cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
1121
|
-
})
|
|
1122
|
-
};
|
|
1350
|
+
})
|
|
1351
|
+
});
|
|
1352
|
+
spool.start();
|
|
1123
1353
|
return {
|
|
1124
|
-
handleWebhook,
|
|
1125
|
-
account
|
|
1354
|
+
handleWebhook: spool.accept,
|
|
1355
|
+
account,
|
|
1356
|
+
stop: spool.stop
|
|
1126
1357
|
};
|
|
1127
1358
|
}
|
|
1128
1359
|
//#endregion
|
|
@@ -1140,56 +1371,6 @@ function resolveLineDurableReplyOptions(params) {
|
|
|
1140
1371
|
return { to: params.to };
|
|
1141
1372
|
}
|
|
1142
1373
|
//#endregion
|
|
1143
|
-
//#region extensions/line/src/reply-chunks.ts
|
|
1144
|
-
async function sendLineReplyChunks(params) {
|
|
1145
|
-
const quickReplies = params.quickReplies?.length ? params.quickReplies : void 0;
|
|
1146
|
-
let replyTokenUsed = Boolean(params.replyTokenUsed);
|
|
1147
|
-
if (params.chunks.length === 0) return { replyTokenUsed };
|
|
1148
|
-
if (params.replyToken && !replyTokenUsed) {
|
|
1149
|
-
let replySucceeded = false;
|
|
1150
|
-
try {
|
|
1151
|
-
const replyBatch = params.chunks.slice(0, 5);
|
|
1152
|
-
const remaining = params.chunks.slice(replyBatch.length);
|
|
1153
|
-
const replyMessages = replyBatch.map((chunk) => ({
|
|
1154
|
-
type: "text",
|
|
1155
|
-
text: chunk
|
|
1156
|
-
}));
|
|
1157
|
-
if (quickReplies && remaining.length === 0 && replyMessages.length > 0) {
|
|
1158
|
-
const lastIndex = replyMessages.length - 1;
|
|
1159
|
-
replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"), quickReplies);
|
|
1160
|
-
}
|
|
1161
|
-
await params.replyMessageLine(params.replyToken, replyMessages, {
|
|
1162
|
-
cfg: params.cfg,
|
|
1163
|
-
accountId: params.accountId
|
|
1164
|
-
});
|
|
1165
|
-
replyTokenUsed = true;
|
|
1166
|
-
replySucceeded = true;
|
|
1167
|
-
for (const [i, chunk] of remaining.entries()) if (i === remaining.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
|
1168
|
-
cfg: params.cfg,
|
|
1169
|
-
accountId: params.accountId
|
|
1170
|
-
});
|
|
1171
|
-
else await params.pushMessageLine(params.to, chunk, {
|
|
1172
|
-
cfg: params.cfg,
|
|
1173
|
-
accountId: params.accountId
|
|
1174
|
-
});
|
|
1175
|
-
return { replyTokenUsed };
|
|
1176
|
-
} catch (err) {
|
|
1177
|
-
if (replySucceeded) throw err;
|
|
1178
|
-
params.onReplyError?.(err);
|
|
1179
|
-
replyTokenUsed = true;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
for (const [i, chunk] of params.chunks.entries()) if (i === params.chunks.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
|
1183
|
-
cfg: params.cfg,
|
|
1184
|
-
accountId: params.accountId
|
|
1185
|
-
});
|
|
1186
|
-
else await params.pushMessageLine(params.to, chunk, {
|
|
1187
|
-
cfg: params.cfg,
|
|
1188
|
-
accountId: params.accountId
|
|
1189
|
-
});
|
|
1190
|
-
return { replyTokenUsed };
|
|
1191
|
-
}
|
|
1192
|
-
//#endregion
|
|
1193
1374
|
//#region extensions/line/src/signature.ts
|
|
1194
1375
|
function validateLineSignature(body, signature, channelSecret) {
|
|
1195
1376
|
return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
|
|
@@ -1214,9 +1395,6 @@ async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_
|
|
|
1214
1395
|
timeoutMs
|
|
1215
1396
|
});
|
|
1216
1397
|
}
|
|
1217
|
-
function logLineWebhookDispatchError(runtime, err) {
|
|
1218
|
-
runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
|
|
1219
|
-
}
|
|
1220
1398
|
function createLineNodeWebhookHandler(params) {
|
|
1221
1399
|
const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
|
|
1222
1400
|
const readBody = params.readBody ?? readLineWebhookRequestBody;
|
|
@@ -1239,7 +1417,6 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1239
1417
|
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
1240
1418
|
return;
|
|
1241
1419
|
}
|
|
1242
|
-
let receiveContext;
|
|
1243
1420
|
try {
|
|
1244
1421
|
const signatureHeader = req.headers["x-line-signature"];
|
|
1245
1422
|
const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
|
|
@@ -1266,24 +1443,14 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1266
1443
|
return;
|
|
1267
1444
|
}
|
|
1268
1445
|
params.onRequestAuthenticated?.();
|
|
1269
|
-
receiveContext = createMessageReceiveContext({
|
|
1270
|
-
id: `${Date.now()}:line:webhook`,
|
|
1271
|
-
channel: "line",
|
|
1272
|
-
message: body,
|
|
1273
|
-
ackPolicy: "after_receive_record",
|
|
1274
|
-
onAck: () => {
|
|
1275
|
-
res.statusCode = 200;
|
|
1276
|
-
res.setHeader("Content-Type", "application/json");
|
|
1277
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
1278
|
-
}
|
|
1279
|
-
});
|
|
1280
|
-
if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
|
|
1281
1446
|
if (body.events && body.events.length > 0) {
|
|
1282
1447
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1283
|
-
|
|
1448
|
+
await params.bot.handleWebhook(body);
|
|
1284
1449
|
}
|
|
1450
|
+
res.statusCode = 200;
|
|
1451
|
+
res.setHeader("Content-Type", "application/json");
|
|
1452
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
1285
1453
|
} catch (err) {
|
|
1286
|
-
await receiveContext?.nack(err);
|
|
1287
1454
|
if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
|
|
1288
1455
|
res.statusCode = 413;
|
|
1289
1456
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -1344,7 +1511,7 @@ async function monitorLineProvider(opts) {
|
|
|
1344
1511
|
accountId,
|
|
1345
1512
|
runtime,
|
|
1346
1513
|
config,
|
|
1347
|
-
onMessage: async (ctx) => {
|
|
1514
|
+
onMessage: async (ctx, deliveryControl) => {
|
|
1348
1515
|
if (!ctx) return;
|
|
1349
1516
|
const { ctxPayload, replyToken, route } = ctx;
|
|
1350
1517
|
const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
|
|
@@ -1358,14 +1525,23 @@ async function monitorLineProvider(opts) {
|
|
|
1358
1525
|
accountId: ctx.accountId
|
|
1359
1526
|
}) : null;
|
|
1360
1527
|
logVerbose(`line: received message from ${await displayNamePromise} (${ctxPayload.From})`);
|
|
1528
|
+
let replyTokenUsed = false;
|
|
1529
|
+
let turnAdopted = false;
|
|
1530
|
+
const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
|
|
1361
1531
|
try {
|
|
1362
1532
|
const textLimit = 5e3;
|
|
1363
|
-
|
|
1364
|
-
const core = getLineRuntime();
|
|
1365
|
-
const turnResult = await core.channel.inbound.run({
|
|
1533
|
+
const turnResult = await getLineRuntime().channel.inbound.run({
|
|
1366
1534
|
channel: "line",
|
|
1367
1535
|
accountId: route.accountId,
|
|
1368
1536
|
raw: ctx,
|
|
1537
|
+
turnAdoptionLifecycle: {
|
|
1538
|
+
...ingressLifecycle,
|
|
1539
|
+
admission: "exclusive",
|
|
1540
|
+
onAdopted: async () => {
|
|
1541
|
+
await ingressLifecycle?.onAdopted();
|
|
1542
|
+
turnAdopted = true;
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1369
1545
|
adapter: {
|
|
1370
1546
|
ingest: () => ({
|
|
1371
1547
|
id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
|
|
@@ -1375,14 +1551,14 @@ async function monitorLineProvider(opts) {
|
|
|
1375
1551
|
cfg: config,
|
|
1376
1552
|
channel: "line",
|
|
1377
1553
|
accountId: route.accountId,
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1554
|
+
route: {
|
|
1555
|
+
agentId: route.agentId,
|
|
1556
|
+
sessionKey: route.sessionKey
|
|
1557
|
+
},
|
|
1381
1558
|
ctxPayload,
|
|
1382
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
1383
|
-
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
1384
1559
|
record: ctx.turn.record,
|
|
1385
1560
|
replyPipeline: {},
|
|
1561
|
+
...ingressLifecycle?.abortSignal ? { replyOptions: { abortSignal: ingressLifecycle.abortSignal } } : {},
|
|
1386
1562
|
delivery: {
|
|
1387
1563
|
durable: (payload, info) => resolveLineDurableReplyOptions({
|
|
1388
1564
|
payload,
|
|
@@ -1410,12 +1586,8 @@ async function monitorLineProvider(opts) {
|
|
|
1410
1586
|
buildTemplateMessageFromPayload,
|
|
1411
1587
|
processLineMessage,
|
|
1412
1588
|
chunkMarkdownText,
|
|
1413
|
-
sendLineReplyChunks,
|
|
1414
1589
|
replyMessageLine,
|
|
1415
|
-
pushMessageLine,
|
|
1416
|
-
pushTextMessageWithQuickReplies,
|
|
1417
1590
|
createQuickReplyItems,
|
|
1418
|
-
createTextMessageWithQuickReplies,
|
|
1419
1591
|
pushMessagesLine,
|
|
1420
1592
|
createFlexMessage,
|
|
1421
1593
|
buildMediaMessage: buildLineMediaMessage,
|
|
@@ -1439,17 +1611,8 @@ async function monitorLineProvider(opts) {
|
|
|
1439
1611
|
if (!hasFinalInboundReplyDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
|
|
1440
1612
|
} catch (err) {
|
|
1441
1613
|
runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
|
|
1442
|
-
if (
|
|
1443
|
-
|
|
1444
|
-
type: "text",
|
|
1445
|
-
text: "Sorry, I encountered an error processing your message."
|
|
1446
|
-
}], {
|
|
1447
|
-
cfg: config,
|
|
1448
|
-
accountId: ctx.accountId
|
|
1449
|
-
});
|
|
1450
|
-
} catch (replyErr) {
|
|
1451
|
-
runtime.error?.(danger(`line: error reply failed: ${String(replyErr)}`));
|
|
1452
|
-
}
|
|
1614
|
+
if (turnAdopted || replyTokenUsed) throw new LineWebhookTerminalDeliveryError("LINE delivery failed after consuming the event reply token.", { cause: err });
|
|
1615
|
+
throw err;
|
|
1453
1616
|
} finally {
|
|
1454
1617
|
stopLoading?.();
|
|
1455
1618
|
}
|
|
@@ -1527,16 +1690,13 @@ async function monitorLineProvider(opts) {
|
|
|
1527
1690
|
res.end(JSON.stringify({ error: "Invalid webhook payload" }));
|
|
1528
1691
|
return;
|
|
1529
1692
|
}
|
|
1530
|
-
requestLifecycle.release();
|
|
1531
|
-
res.statusCode = 200;
|
|
1532
|
-
res.setHeader("Content-Type", "application/json");
|
|
1533
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
1534
1693
|
if (body.events && body.events.length > 0) {
|
|
1535
1694
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1536
|
-
|
|
1537
|
-
match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
|
|
1538
|
-
});
|
|
1695
|
+
await match.target.bot.handleWebhook(body);
|
|
1539
1696
|
}
|
|
1697
|
+
res.statusCode = 200;
|
|
1698
|
+
res.setHeader("Content-Type", "application/json");
|
|
1699
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
1540
1700
|
} catch (err) {
|
|
1541
1701
|
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
|
1542
1702
|
res.statusCode = 413;
|
|
@@ -1564,23 +1724,29 @@ async function monitorLineProvider(opts) {
|
|
|
1564
1724
|
});
|
|
1565
1725
|
logVerbose(`line: registered webhook handler at ${normalizedPath}`);
|
|
1566
1726
|
let stopped = false;
|
|
1727
|
+
let stopPromise;
|
|
1567
1728
|
const stopHandler = () => {
|
|
1568
|
-
if (
|
|
1729
|
+
if (stopPromise) return stopPromise;
|
|
1730
|
+
if (stopped) return Promise.resolve();
|
|
1569
1731
|
stopped = true;
|
|
1570
1732
|
logVerbose(`line: stopping provider for account ${resolvedAccountId}`);
|
|
1571
1733
|
unregisterHttp();
|
|
1734
|
+
stopPromise = bot.stop();
|
|
1735
|
+
return stopPromise;
|
|
1572
1736
|
};
|
|
1573
|
-
|
|
1737
|
+
const stopOnAbort = () => void stopHandler();
|
|
1738
|
+
if (abortSignal?.aborted) await stopHandler();
|
|
1574
1739
|
else if (abortSignal) {
|
|
1575
|
-
abortSignal.addEventListener("abort",
|
|
1740
|
+
abortSignal.addEventListener("abort", stopOnAbort, { once: true });
|
|
1576
1741
|
await waitForAbortSignal(abortSignal);
|
|
1742
|
+
await stopHandler();
|
|
1577
1743
|
}
|
|
1578
1744
|
return {
|
|
1579
1745
|
account: bot.account,
|
|
1580
1746
|
handleWebhook: bot.handleWebhook,
|
|
1581
|
-
stop: () => {
|
|
1582
|
-
stopHandler();
|
|
1583
|
-
abortSignal?.removeEventListener("abort",
|
|
1747
|
+
stop: async () => {
|
|
1748
|
+
await stopHandler();
|
|
1749
|
+
abortSignal?.removeEventListener("abort", stopOnAbort);
|
|
1584
1750
|
}
|
|
1585
1751
|
};
|
|
1586
1752
|
}
|