@openclaw/line 2026.7.2-beta.7 → 2026.7.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/.setup/accounts-DJVOv1JI.mjs +107 -0
- package/dist/.setup/api-D3h9w6oz.mjs +3 -0
- package/dist/.setup/api-DdlGHKoX.mjs +11 -0
- package/dist/.setup/basic-cards-C8Qwh2X1.mjs +307 -0
- package/dist/{card-command-ty11OJNt.js → .setup/card-command-CWrkUiGY.mjs} +51 -23
- package/dist/{channel-D4i_K4fQ.js → .setup/channel-Du0PrvOH.mjs} +76 -66
- package/dist/.setup/channel.runtime-b3suPB-W.mjs +4 -0
- package/dist/.setup/flex-templates-BP6VHjzK.mjs +814 -0
- package/dist/{markdown-to-line-DMDQTMhx.js → .setup/markdown-to-line-o89GT1Qn.mjs} +252 -254
- package/dist/{monitor-DXEodYFJ.js → .setup/monitor-DpxjN6Ks.mjs} +491 -658
- package/dist/.setup/monitor.runtime-BS-cGD5Q.mjs +2 -0
- package/dist/.setup/outbound.runtime-BeNDuEIF.mjs +2 -0
- package/dist/{probe-CM03wo6r.js → .setup/probe-BslD77tJ.mjs} +9 -7
- package/dist/.setup/probe.runtime-YwWmWIlk.mjs +2 -0
- package/dist/{reply-payload-transform-D5-dHB8b.js → .setup/reply-payload-transform-CYQIy_x8.mjs} +204 -80
- package/dist/{setup-surface-DRQ7n0En.js → .setup/setup-surface-M1ivmZ-G.mjs} +143 -158
- package/dist/api.js +2 -11
- package/dist/channel-plugin-api.js +1 -1
- package/dist/contract-api.js +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime-api.js +56 -15
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +2 -1
- 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 +1 -2
- 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/models.d.ts +0 -1
- 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 +0 -1
- 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 +1 -2
- package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts.map +1 -1
- package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts +0 -1
- 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 +0 -1
- 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 +4 -1
- package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupFailedType.ts +2 -1
- package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobFailedType.ts +2 -1
- package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobStatus.ts +4 -1
- package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupStatus.ts +6 -1
- 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 +6 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxCornerRadius.ts +7 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBubble.ts +7 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexIconSize.ts +10 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexSpanSize.ts +10 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexTextFontSize.ts +10 -1
- 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 +5 -2
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/postbackAction.ts +4 -1
- package/node_modules/@line/bot-sdk/lib/messaging-api/model/subscriptionPeriodDemographic.ts +5 -1
- package/node_modules/@line/bot-sdk/lib/version.ts +1 -1
- package/node_modules/@line/bot-sdk/lib/webhook/.openapi-generator/FILES +0 -1
- package/node_modules/@line/bot-sdk/lib/webhook/model/event.ts +0 -2
- package/node_modules/@line/bot-sdk/lib/webhook/model/models.ts +0 -1
- package/node_modules/@line/bot-sdk/lib/webhook/model/stickerMessageContent.ts +1 -1
- package/node_modules/@line/bot-sdk/node_modules/@types/node/README.md +1 -1
- package/node_modules/@line/bot-sdk/node_modules/@types/node/fs/promises.d.ts +5 -16
- package/node_modules/@line/bot-sdk/node_modules/@types/node/package.json +3 -3
- package/node_modules/@line/bot-sdk/package.json +3 -3
- package/openclaw.plugin.json +75 -57
- package/package.json +6 -71
- package/dist/accounts-BqEVsDoB.js +0 -103
- package/dist/channel.runtime-BdhK8Xw7.js +0 -4
- package/dist/flex-templates-iKJsyCq-.js +0 -1339
- package/dist/monitor.runtime-CZquR7jI.js +0 -2
- package/dist/outbound.runtime-BS0IhU_M.js +0 -2
- package/dist/probe.runtime-D13ZtXsd.js +0 -2
- package/dist/quick-reply-fallback-D86FwAfr.js +0 -56
- package/dist/send-receipt-CY2ey8XH.js +0 -107
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts +0 -24
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts.map +0 -1
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js +0 -14
- package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js.map +0 -1
- package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts +0 -24
- package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts.map +0 -1
- package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js +0 -13
- package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js.map +0 -1
- package/node_modules/@line/bot-sdk/lib/webhook/model/messageEditedEvent.ts +0 -32
|
@@ -1,36 +1,30 @@
|
|
|
1
|
-
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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-DMDQTMhx.js";
|
|
1
|
+
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-DJVOv1JI.mjs";
|
|
2
|
+
import { f as resolveLineGroupConfigEntry, i as buildLineQuickReplyFallbackText, s as getLineRuntime } from "./reply-payload-transform-CYQIy_x8.mjs";
|
|
3
|
+
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, f as createImageMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-o89GT1Qn.mjs";
|
|
5
4
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
|
+
import { createMessageReceiveContext } from "openclaw/plugin-sdk/channel-outbound";
|
|
8
7
|
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
9
|
-
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
10
8
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
11
|
-
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
12
9
|
import { firstDefined } from "openclaw/plugin-sdk/allow-from";
|
|
13
|
-
import {
|
|
14
|
-
import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
|
|
10
|
+
import { messagingApi } from "@line/bot-sdk";
|
|
15
11
|
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
16
|
-
import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal
|
|
17
|
-
import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
|
|
18
|
-
import { HTTPFetchError } from "@line/bot-sdk";
|
|
19
|
-
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
12
|
+
import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
|
|
20
13
|
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
|
21
|
-
import {
|
|
14
|
+
import { buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
22
15
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
23
16
|
import { isRequestBodyLimitError, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
|
|
24
|
-
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1
|
|
17
|
+
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1 } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
25
18
|
import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
|
|
26
19
|
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
27
20
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
28
|
-
import {
|
|
21
|
+
import { shouldComputeCommandAuthorized } from "openclaw/plugin-sdk/command-auth-native";
|
|
29
22
|
import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
23
|
+
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
30
24
|
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
31
25
|
import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
26
|
+
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
|
32
27
|
import crypto from "node:crypto";
|
|
33
|
-
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
34
28
|
//#region extensions/line/src/bot-access.ts
|
|
35
29
|
function normalizeLineAllowEntry(value) {
|
|
36
30
|
const trimmed = String(value).trim();
|
|
@@ -48,92 +42,9 @@ const normalizeAllowFrom = (list) => {
|
|
|
48
42
|
};
|
|
49
43
|
//#endregion
|
|
50
44
|
//#region extensions/line/src/download.ts
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const CONTENT_READY_TIMEOUT_MS = 15e3;
|
|
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
|
-
};
|
|
62
|
-
function contentBackoffDelayMs(attempt) {
|
|
63
|
-
return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
|
|
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
|
-
}
|
|
94
|
-
async function fetchLineContentWhenReady(messageId, channelAccessToken) {
|
|
95
|
-
const controller = new AbortController();
|
|
96
|
-
const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
|
|
97
|
-
deadline.unref();
|
|
98
|
-
try {
|
|
99
|
-
for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
|
|
100
|
-
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
|
|
101
|
-
headers: { Authorization: `Bearer ${channelAccessToken}` },
|
|
102
|
-
redirect: "error",
|
|
103
|
-
signal: controller.signal
|
|
104
|
-
});
|
|
105
|
-
if (response.status === 200) {
|
|
106
|
-
if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
107
|
-
return response;
|
|
108
|
-
}
|
|
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 });
|
|
111
|
-
if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
|
|
112
|
-
}
|
|
113
|
-
} catch (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 });
|
|
117
|
-
} finally {
|
|
118
|
-
clearTimeout(deadline);
|
|
119
|
-
}
|
|
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;
|
|
127
|
-
}
|
|
128
|
-
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
|
|
129
|
-
const response = await fetchLineContentWhenReady(messageId, channelAccessToken);
|
|
130
|
-
let saved;
|
|
131
|
-
try {
|
|
132
|
-
saved = await saveMediaStream(lineResponseBodyChunks(response, messageId), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
|
|
133
|
-
} catch (err) {
|
|
134
|
-
await response.body?.cancel().catch(() => void 0);
|
|
135
|
-
throw err;
|
|
136
|
-
}
|
|
45
|
+
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10485760, options) {
|
|
46
|
+
const response = await new messagingApi.MessagingApiBlobClient({ channelAccessToken }).getMessageContent(messageId);
|
|
47
|
+
const saved = await saveMediaStream(response, void 0, "inbound", maxBytes, options?.originalFilename);
|
|
137
48
|
logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
|
|
138
49
|
return {
|
|
139
50
|
path: saved.path,
|
|
@@ -143,23 +54,15 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
|
|
|
143
54
|
}
|
|
144
55
|
//#endregion
|
|
145
56
|
//#region extensions/line/src/auto-reply-delivery.ts
|
|
146
|
-
function toLineDeliveryError(error) {
|
|
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;
|
|
152
|
-
}
|
|
153
57
|
function markLineVisibleDeliveryError(error) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Object.assign(deliveryError, {
|
|
58
|
+
if (error instanceof Error && Object.isExtensible(error)) {
|
|
59
|
+
Object.assign(error, {
|
|
157
60
|
sentBeforeError: true,
|
|
158
61
|
visibleReplySent: true
|
|
159
62
|
});
|
|
160
|
-
return
|
|
63
|
+
return error;
|
|
161
64
|
}
|
|
162
|
-
const visibleError = new Error("LINE message send failed", { cause:
|
|
65
|
+
const visibleError = new Error("LINE rich or media message send failed", { cause: error });
|
|
163
66
|
Object.assign(visibleError, {
|
|
164
67
|
sentBeforeError: true,
|
|
165
68
|
visibleReplySent: true
|
|
@@ -169,37 +72,12 @@ function markLineVisibleDeliveryError(error) {
|
|
|
169
72
|
async function deliverLineAutoReply(params) {
|
|
170
73
|
const { payload, lineData, replyToken, accountId, to, textLimit, deps } = params;
|
|
171
74
|
let replyTokenUsed = params.replyTokenUsed;
|
|
172
|
-
|
|
173
|
-
const sendVisible = async (send) => {
|
|
174
|
-
try {
|
|
175
|
-
const result = await send();
|
|
176
|
-
visibleReplySent = true;
|
|
177
|
-
return result;
|
|
178
|
-
} catch (error) {
|
|
179
|
-
if (visibleReplySent) throw markLineVisibleDeliveryError(error);
|
|
180
|
-
throw error;
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
const replyVisible = (...args) => sendVisible(() => deps.replyMessageLine(...args));
|
|
184
|
-
const failedPushSegments = /* @__PURE__ */ new WeakMap();
|
|
185
|
-
const pushLineMessages = async (messages, allowFailedBatchTextRecovery, externalTail = []) => {
|
|
75
|
+
const pushLineMessages = async (messages) => {
|
|
186
76
|
if (messages.length === 0) return;
|
|
187
|
-
for (let i = 0; i < messages.length; i += 5) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
}
|
|
77
|
+
for (let i = 0; i < messages.length; i += 5) await deps.pushMessagesLine(to, messages.slice(i, i + 5), {
|
|
78
|
+
cfg: params.cfg,
|
|
79
|
+
accountId
|
|
80
|
+
});
|
|
203
81
|
};
|
|
204
82
|
const sendLineMessages = async (messages, allowReplyToken) => {
|
|
205
83
|
if (messages.length === 0) return;
|
|
@@ -207,19 +85,18 @@ async function deliverLineAutoReply(params) {
|
|
|
207
85
|
if (allowReplyToken && replyToken && !replyTokenUsed) {
|
|
208
86
|
const replyBatch = remaining.slice(0, 5);
|
|
209
87
|
try {
|
|
210
|
-
await
|
|
88
|
+
await deps.replyMessageLine(replyToken, replyBatch, {
|
|
211
89
|
cfg: params.cfg,
|
|
212
90
|
accountId
|
|
213
91
|
});
|
|
214
92
|
} catch (err) {
|
|
215
93
|
deps.onReplyError?.(err);
|
|
216
|
-
await pushLineMessages(replyBatch
|
|
217
|
-
} finally {
|
|
218
|
-
replyTokenUsed = true;
|
|
94
|
+
await pushLineMessages(replyBatch);
|
|
219
95
|
}
|
|
96
|
+
replyTokenUsed = true;
|
|
220
97
|
remaining = remaining.slice(replyBatch.length);
|
|
221
98
|
}
|
|
222
|
-
if (remaining.length > 0) await pushLineMessages(remaining
|
|
99
|
+
if (remaining.length > 0) await pushLineMessages(remaining);
|
|
223
100
|
};
|
|
224
101
|
const richMessages = [];
|
|
225
102
|
const hasQuickReplies = Boolean(lineData.quickReplies?.length);
|
|
@@ -229,80 +106,80 @@ async function deliverLineAutoReply(params) {
|
|
|
229
106
|
if (templateMsg) richMessages.push(templateMsg);
|
|
230
107
|
}
|
|
231
108
|
if (lineData.location) richMessages.push(deps.createLocationMessage(lineData.location));
|
|
232
|
-
const
|
|
233
|
-
const processed = visibleText ? deps.processLineMessage(visibleText) : {
|
|
109
|
+
const processed = payload.text ? deps.processLineMessage(payload.text) : {
|
|
234
110
|
text: "",
|
|
235
111
|
flexMessages: []
|
|
236
112
|
};
|
|
237
113
|
for (const flexMsg of processed.flexMessages) richMessages.push(deps.createFlexMessage(truncateUtf16Safe(flexMsg.altText, 400), flexMsg.contents));
|
|
238
114
|
const chunks = processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : [];
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const mediaMessages = [];
|
|
247
|
-
let deliveryError;
|
|
248
|
-
for (const rawUrl of mediaUrls) {
|
|
249
|
-
const url = rawUrl?.trim();
|
|
250
|
-
if (!url) continue;
|
|
251
|
-
try {
|
|
252
|
-
mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
|
|
115
|
+
const mediaMessages = resolveSendableOutboundReplyParts(payload).mediaUrls.map((url) => url?.trim()).filter((url) => Boolean(url)).map((url) => deps.createImageMessage(url));
|
|
116
|
+
if (chunks.length > 0) {
|
|
117
|
+
const hasRichOrMedia = richMessages.length > 0 || mediaMessages.length > 0;
|
|
118
|
+
const sendRichBeforeText = hasQuickReplies && hasRichOrMedia;
|
|
119
|
+
let richMediaError;
|
|
120
|
+
if (sendRichBeforeText) try {
|
|
121
|
+
await sendLineMessages([...richMessages, ...mediaMessages], false);
|
|
253
122
|
} catch (err) {
|
|
254
|
-
|
|
123
|
+
richMediaError = err;
|
|
255
124
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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);
|
|
295
|
-
return {
|
|
125
|
+
const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
|
|
126
|
+
to,
|
|
127
|
+
chunks,
|
|
128
|
+
quickReplies: lineData.quickReplies,
|
|
129
|
+
replyToken,
|
|
130
|
+
replyTokenUsed,
|
|
131
|
+
cfg: params.cfg,
|
|
132
|
+
accountId,
|
|
133
|
+
replyMessageLine: deps.replyMessageLine,
|
|
134
|
+
pushMessageLine: deps.pushMessageLine,
|
|
135
|
+
pushTextMessageWithQuickReplies: deps.pushTextMessageWithQuickReplies,
|
|
136
|
+
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies
|
|
137
|
+
});
|
|
138
|
+
replyTokenUsed = nextReplyTokenUsed;
|
|
139
|
+
if (!sendRichBeforeText) try {
|
|
140
|
+
await sendLineMessages(richMessages, false);
|
|
141
|
+
if (mediaMessages.length > 0) await sendLineMessages(mediaMessages, false);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
richMediaError = err;
|
|
144
|
+
}
|
|
145
|
+
if (richMediaError !== void 0) return {
|
|
296
146
|
status: "partial",
|
|
297
147
|
replyTokenUsed,
|
|
298
|
-
|
|
299
|
-
error: markLineVisibleDeliveryError(deliveryError)
|
|
148
|
+
error: markLineVisibleDeliveryError(richMediaError)
|
|
300
149
|
};
|
|
150
|
+
} else {
|
|
151
|
+
const combined = [...richMessages, ...mediaMessages];
|
|
152
|
+
if (hasQuickReplies && combined.length === 0) {
|
|
153
|
+
const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
|
|
154
|
+
to,
|
|
155
|
+
chunks: [buildLineQuickReplyFallbackText(lineData.quickReplies)],
|
|
156
|
+
quickReplies: lineData.quickReplies,
|
|
157
|
+
replyToken,
|
|
158
|
+
replyTokenUsed,
|
|
159
|
+
cfg: params.cfg,
|
|
160
|
+
accountId,
|
|
161
|
+
replyMessageLine: deps.replyMessageLine,
|
|
162
|
+
pushMessageLine: deps.pushMessageLine,
|
|
163
|
+
pushTextMessageWithQuickReplies: deps.pushTextMessageWithQuickReplies,
|
|
164
|
+
createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
|
|
165
|
+
onReplyError: deps.onReplyError
|
|
166
|
+
});
|
|
167
|
+
replyTokenUsed = nextReplyTokenUsed;
|
|
168
|
+
} else {
|
|
169
|
+
if (hasQuickReplies && combined.length > 0) {
|
|
170
|
+
const quickReply = deps.createQuickReplyItems(lineData.quickReplies);
|
|
171
|
+
const targetIndex = replyToken && !replyTokenUsed ? Math.min(4, combined.length - 1) : combined.length - 1;
|
|
172
|
+
combined[targetIndex] = {
|
|
173
|
+
...combined[targetIndex],
|
|
174
|
+
quickReply
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
await sendLineMessages(combined, true);
|
|
178
|
+
}
|
|
301
179
|
}
|
|
302
180
|
return {
|
|
303
181
|
status: "delivered",
|
|
304
|
-
replyTokenUsed
|
|
305
|
-
visibleReplySent
|
|
182
|
+
replyTokenUsed
|
|
306
183
|
};
|
|
307
184
|
}
|
|
308
185
|
//#endregion
|
|
@@ -432,27 +309,48 @@ function extractMessageText(message) {
|
|
|
432
309
|
}
|
|
433
310
|
return "";
|
|
434
311
|
}
|
|
435
|
-
function
|
|
312
|
+
function extractMediaPlaceholder(message) {
|
|
436
313
|
switch (message.type) {
|
|
437
|
-
case "image": return "image";
|
|
438
|
-
case "video": return "video";
|
|
439
|
-
case "audio": return "audio";
|
|
440
|
-
case "file": return "document";
|
|
441
|
-
default: return;
|
|
314
|
+
case "image": return "<media:image>";
|
|
315
|
+
case "video": return "<media:video>";
|
|
316
|
+
case "audio": return "<media:audio>";
|
|
317
|
+
case "file": return "<media:document>";
|
|
318
|
+
default: return "";
|
|
442
319
|
}
|
|
443
320
|
}
|
|
321
|
+
function resolveLineConversationLabel(params) {
|
|
322
|
+
return params.isGroup ? params.groupId ? `group:${params.groupId}` : params.roomId ? `room:${params.roomId}` : "unknown-group" : params.senderLabel;
|
|
323
|
+
}
|
|
324
|
+
function resolveLineAddresses(params) {
|
|
325
|
+
const fromAddress = params.isGroup ? params.groupId ? `line:group:${params.groupId}` : params.roomId ? `line:room:${params.roomId}` : `line:${params.peerId}` : `line:${params.userId ?? params.peerId}`;
|
|
326
|
+
return {
|
|
327
|
+
fromAddress,
|
|
328
|
+
toAddress: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`,
|
|
329
|
+
originatingTo: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`
|
|
330
|
+
};
|
|
331
|
+
}
|
|
444
332
|
async function finalizeLineInboundContext(params) {
|
|
333
|
+
const { fromAddress, toAddress, originatingTo } = resolveLineAddresses({
|
|
334
|
+
isGroup: params.source.isGroup,
|
|
335
|
+
groupId: params.source.groupId,
|
|
336
|
+
roomId: params.source.roomId,
|
|
337
|
+
userId: params.source.userId,
|
|
338
|
+
peerId: params.source.peerId
|
|
339
|
+
});
|
|
445
340
|
const senderId = params.source.userId ?? "unknown";
|
|
446
341
|
const senderLabel = params.source.userId ? `user:${params.source.userId}` : "unknown";
|
|
447
|
-
const conversationLabel =
|
|
448
|
-
|
|
342
|
+
const conversationLabel = resolveLineConversationLabel({
|
|
343
|
+
isGroup: params.source.isGroup,
|
|
344
|
+
groupId: params.source.groupId,
|
|
345
|
+
roomId: params.source.roomId,
|
|
346
|
+
senderLabel
|
|
347
|
+
});
|
|
449
348
|
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
|
450
349
|
cfg: params.cfg,
|
|
451
350
|
agentId: params.route.agentId,
|
|
452
351
|
sessionKey: params.route.sessionKey
|
|
453
352
|
});
|
|
454
353
|
const agentBody = params.agentBody ?? params.rawBody;
|
|
455
|
-
const media = await toInboundMediaFactsWithMetadata(params.media);
|
|
456
354
|
const body = formatInboundEnvelope({
|
|
457
355
|
channel: "LINE",
|
|
458
356
|
from: conversationLabel,
|
|
@@ -463,45 +361,38 @@ async function finalizeLineInboundContext(params) {
|
|
|
463
361
|
previousTimestamp,
|
|
464
362
|
envelope: envelopeOptions
|
|
465
363
|
});
|
|
466
|
-
const ctxPayload =
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
...params.locationContext,
|
|
499
|
-
GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
|
|
500
|
-
GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
|
|
501
|
-
groupId: params.source.groupId,
|
|
502
|
-
roomId: params.source.roomId
|
|
503
|
-
})?.systemPrompt) : void 0
|
|
504
|
-
}
|
|
364
|
+
const ctxPayload = finalizeInboundContext({
|
|
365
|
+
Body: body,
|
|
366
|
+
BodyForAgent: agentBody,
|
|
367
|
+
RawBody: params.rawBody,
|
|
368
|
+
CommandBody: params.rawBody,
|
|
369
|
+
From: fromAddress,
|
|
370
|
+
To: toAddress,
|
|
371
|
+
SessionKey: params.route.sessionKey,
|
|
372
|
+
AccountId: params.route.accountId,
|
|
373
|
+
ChatType: params.source.isGroup ? "group" : "direct",
|
|
374
|
+
ConversationLabel: conversationLabel,
|
|
375
|
+
GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
|
|
376
|
+
SenderId: senderId,
|
|
377
|
+
Provider: "line",
|
|
378
|
+
Surface: "line",
|
|
379
|
+
MessageSid: params.messageSid,
|
|
380
|
+
Timestamp: params.timestamp,
|
|
381
|
+
MediaPath: params.media.firstPath,
|
|
382
|
+
MediaType: params.media.firstContentType,
|
|
383
|
+
MediaUrl: params.media.firstPath,
|
|
384
|
+
MediaPaths: params.media.paths,
|
|
385
|
+
MediaUrls: params.media.paths,
|
|
386
|
+
MediaTypes: params.media.types,
|
|
387
|
+
...params.locationContext,
|
|
388
|
+
CommandAuthorized: params.commandAuthorized,
|
|
389
|
+
OriginatingChannel: "line",
|
|
390
|
+
OriginatingTo: originatingTo,
|
|
391
|
+
GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
|
|
392
|
+
groupId: params.source.groupId,
|
|
393
|
+
roomId: params.source.roomId
|
|
394
|
+
})?.systemPrompt) : void 0,
|
|
395
|
+
InboundHistory: params.inboundHistory
|
|
505
396
|
});
|
|
506
397
|
const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
|
|
507
398
|
dmScope: params.cfg.session?.dmScope,
|
|
@@ -515,7 +406,8 @@ async function finalizeLineInboundContext(params) {
|
|
|
515
406
|
if (shouldLogVerbose()) {
|
|
516
407
|
const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
|
|
517
408
|
const mediaInfo = params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1 ? ` mediaCount=${params.verboseLog.mediaCount}` : "";
|
|
518
|
-
|
|
409
|
+
const label = params.verboseLog.kind === "inbound" ? "line inbound" : "line postback";
|
|
410
|
+
logVerbose(`${label}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
|
|
519
411
|
}
|
|
520
412
|
return {
|
|
521
413
|
ctxPayload,
|
|
@@ -544,7 +436,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
544
436
|
};
|
|
545
437
|
}
|
|
546
438
|
async function buildLineMessageContext(params) {
|
|
547
|
-
const { event, allMedia, mediaUnavailable, cfg, account, commandAuthorized,
|
|
439
|
+
const { event, allMedia, mediaUnavailable, cfg, account, commandAuthorized, groupHistories, historyLimit } = params;
|
|
548
440
|
const source = event.source;
|
|
549
441
|
const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
|
|
550
442
|
source,
|
|
@@ -555,17 +447,15 @@ async function buildLineMessageContext(params) {
|
|
|
555
447
|
const messageId = message.id;
|
|
556
448
|
const timestamp = event.timestamp;
|
|
557
449
|
const textContent = extractMessageText(message);
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
kind: nativeMediaKind
|
|
562
|
-
})) : nativeMediaKind ? [{ kind: nativeMediaKind }] : [];
|
|
563
|
-
const rawBody = textContent;
|
|
450
|
+
const placeholder = extractMediaPlaceholder(message);
|
|
451
|
+
let rawBody = textContent || placeholder;
|
|
452
|
+
if (!rawBody && allMedia.length > 0) rawBody = `<media:image>${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`;
|
|
564
453
|
const agentBody = mediaUnavailable ? formatInboundMediaUnavailableText({
|
|
565
454
|
body: rawBody,
|
|
455
|
+
mediaPlaceholder: placeholder,
|
|
566
456
|
notice: "[line attachment unavailable]"
|
|
567
457
|
}) : rawBody;
|
|
568
|
-
if (!agentBody &&
|
|
458
|
+
if (!agentBody && allMedia.length === 0) return null;
|
|
569
459
|
let locationContext;
|
|
570
460
|
if (message.type === "location") {
|
|
571
461
|
const loc = message;
|
|
@@ -576,6 +466,11 @@ async function buildLineMessageContext(params) {
|
|
|
576
466
|
address: loc.address
|
|
577
467
|
});
|
|
578
468
|
}
|
|
469
|
+
const historyKey = isGroup ? peerId : void 0;
|
|
470
|
+
const inboundHistory = historyKey && groupHistories && (historyLimit ?? 0) > 0 ? createChannelHistoryWindow({ historyMap: groupHistories }).buildInboundHistory({
|
|
471
|
+
historyKey,
|
|
472
|
+
limit: historyLimit ?? 0
|
|
473
|
+
}) : void 0;
|
|
579
474
|
const finalized = await finalizeLineInboundContext({
|
|
580
475
|
cfg,
|
|
581
476
|
account,
|
|
@@ -593,7 +488,12 @@ async function buildLineMessageContext(params) {
|
|
|
593
488
|
timestamp,
|
|
594
489
|
messageSid: messageId,
|
|
595
490
|
commandAuthorized,
|
|
596
|
-
media:
|
|
491
|
+
media: {
|
|
492
|
+
firstPath: allMedia[0]?.path,
|
|
493
|
+
firstContentType: allMedia[0]?.contentType,
|
|
494
|
+
paths: allMedia.length > 0 ? allMedia.map((m) => m.path) : void 0,
|
|
495
|
+
types: allMedia.length > 0 ? allMedia.map((m) => m.contentType).filter(Boolean) : void 0
|
|
496
|
+
},
|
|
597
497
|
locationContext,
|
|
598
498
|
verboseLog: {
|
|
599
499
|
kind: "inbound",
|
|
@@ -623,14 +523,18 @@ async function buildLinePostbackContext(params) {
|
|
|
623
523
|
account
|
|
624
524
|
});
|
|
625
525
|
const timestamp = event.timestamp;
|
|
626
|
-
const
|
|
627
|
-
if (!
|
|
628
|
-
let
|
|
629
|
-
if (
|
|
630
|
-
const searchParams = new URLSearchParams(
|
|
526
|
+
const rawBody = event.postback?.data?.trim() ?? "";
|
|
527
|
+
if (!rawBody) return null;
|
|
528
|
+
let agentBody = rawBody;
|
|
529
|
+
if (rawBody.includes("line.action=")) {
|
|
530
|
+
const searchParams = new URLSearchParams(rawBody);
|
|
631
531
|
const action = searchParams.get("line.action") ?? "";
|
|
632
532
|
const device = searchParams.get("line.device");
|
|
633
|
-
|
|
533
|
+
agentBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
|
|
534
|
+
}
|
|
535
|
+
for (const [key, value] of Object.entries(event.postback.params ?? {}).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
|
|
536
|
+
const picked = normalizeOptionalString(value);
|
|
537
|
+
if (picked) agentBody += ` ${key}=${picked}`;
|
|
634
538
|
}
|
|
635
539
|
const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
|
|
636
540
|
const finalized = await finalizeLineInboundContext({
|
|
@@ -646,10 +550,16 @@ async function buildLinePostbackContext(params) {
|
|
|
646
550
|
peerId
|
|
647
551
|
},
|
|
648
552
|
rawBody,
|
|
553
|
+
agentBody,
|
|
649
554
|
timestamp,
|
|
650
555
|
messageSid,
|
|
651
556
|
commandAuthorized,
|
|
652
|
-
media:
|
|
557
|
+
media: {
|
|
558
|
+
firstPath: "",
|
|
559
|
+
firstContentType: void 0,
|
|
560
|
+
paths: void 0,
|
|
561
|
+
types: void 0
|
|
562
|
+
},
|
|
653
563
|
verboseLog: { kind: "postback" }
|
|
654
564
|
});
|
|
655
565
|
return {
|
|
@@ -666,41 +576,6 @@ async function buildLinePostbackContext(params) {
|
|
|
666
576
|
};
|
|
667
577
|
}
|
|
668
578
|
//#endregion
|
|
669
|
-
//#region extensions/line/src/group-history.ts
|
|
670
|
-
const reservedEntries = /* @__PURE__ */ new WeakSet();
|
|
671
|
-
function reserveLineGroupHistory(historyMap, historyKey, limit) {
|
|
672
|
-
if (!historyMap || !historyKey || limit <= 0) return {
|
|
673
|
-
commit: () => {},
|
|
674
|
-
release: () => {}
|
|
675
|
-
};
|
|
676
|
-
const consumedEntries = (historyMap.get(historyKey) ?? []).filter((entry) => !reservedEntries.has(entry));
|
|
677
|
-
for (const entry of consumedEntries) reservedEntries.add(entry);
|
|
678
|
-
const inboundHistory = createChannelHistoryWindow({ historyMap: /* @__PURE__ */ new Map([[historyKey, consumedEntries]]) }).buildInboundHistory({
|
|
679
|
-
historyKey,
|
|
680
|
-
limit
|
|
681
|
-
});
|
|
682
|
-
let settled = false;
|
|
683
|
-
const settle = () => {
|
|
684
|
-
if (settled) return false;
|
|
685
|
-
settled = true;
|
|
686
|
-
for (const entry of consumedEntries) reservedEntries.delete(entry);
|
|
687
|
-
return true;
|
|
688
|
-
};
|
|
689
|
-
return {
|
|
690
|
-
inboundHistory,
|
|
691
|
-
commit: () => {
|
|
692
|
-
if (!settle() || consumedEntries.length === 0) return;
|
|
693
|
-
const consumed = new Set(consumedEntries);
|
|
694
|
-
const kept = (historyMap.get(historyKey) ?? []).filter((entry) => !consumed.has(entry));
|
|
695
|
-
if (kept.length > 0) historyMap.set(historyKey, kept);
|
|
696
|
-
else historyMap.delete(historyKey);
|
|
697
|
-
},
|
|
698
|
-
release: () => {
|
|
699
|
-
settle();
|
|
700
|
-
}
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
//#endregion
|
|
704
579
|
//#region extensions/line/src/bot-handlers.ts
|
|
705
580
|
const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
706
581
|
"image",
|
|
@@ -711,9 +586,63 @@ const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
711
586
|
function isDownloadableLineMessageType(messageType) {
|
|
712
587
|
return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
|
|
713
588
|
}
|
|
589
|
+
const LINE_WEBHOOK_REPLAY_WINDOW_MS = 6e5;
|
|
590
|
+
const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
|
|
714
591
|
function normalizeLineIngressEntry(value) {
|
|
715
592
|
return normalizeLineAllowEntry(value) || null;
|
|
716
593
|
}
|
|
594
|
+
var LineRetryableWebhookError = class extends Error {
|
|
595
|
+
constructor(message, options) {
|
|
596
|
+
super(message, options);
|
|
597
|
+
this.name = "LineRetryableWebhookError";
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
function createLineWebhookReplayCache() {
|
|
601
|
+
return createClaimableDedupe({
|
|
602
|
+
ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
|
|
603
|
+
memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
function buildLineWebhookReplayKey(event, accountId) {
|
|
607
|
+
if (event.type === "message") {
|
|
608
|
+
const messageId = event.message?.id?.trim();
|
|
609
|
+
if (messageId) return {
|
|
610
|
+
key: `${accountId}|message:${messageId}`,
|
|
611
|
+
eventId: `message:${messageId}`
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const eventId = event.webhookEventId?.trim();
|
|
615
|
+
if (!eventId) return null;
|
|
616
|
+
const source = event.source;
|
|
617
|
+
const sourceId = source?.type === "group" ? `group:${source.groupId ?? ""}` : source?.type === "room" ? `room:${source.roomId ?? ""}` : `user:${source?.userId ?? ""}`;
|
|
618
|
+
return {
|
|
619
|
+
key: `${accountId}|${event.type}|${sourceId}|${eventId}`,
|
|
620
|
+
eventId: `event:${eventId}`
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function getLineReplayCandidate(event, context) {
|
|
624
|
+
const replay = buildLineWebhookReplayKey(event, context.account.accountId);
|
|
625
|
+
const cache = context.replayCache;
|
|
626
|
+
if (!replay || !cache) return null;
|
|
627
|
+
return {
|
|
628
|
+
key: replay.key,
|
|
629
|
+
eventId: replay.eventId,
|
|
630
|
+
cache
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
async function claimLineReplayEvent(candidate) {
|
|
634
|
+
const claim = await candidate.cache.claim(candidate.key);
|
|
635
|
+
if (claim.kind === "claimed") return { skip: false };
|
|
636
|
+
if (claim.kind === "inflight") {
|
|
637
|
+
logVerbose(`line: skipped in-flight replayed webhook event ${candidate.eventId}`);
|
|
638
|
+
return {
|
|
639
|
+
skip: true,
|
|
640
|
+
inFlightResult: claim.pending.then(() => void 0)
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
logVerbose(`line: skipped replayed webhook event ${candidate.eventId}`);
|
|
644
|
+
return { skip: true };
|
|
645
|
+
}
|
|
717
646
|
function resolveLineGroupConfig(params) {
|
|
718
647
|
return resolveLineGroupConfigEntry(params.config.groups, {
|
|
719
648
|
groupId: params.groupId,
|
|
@@ -788,7 +717,7 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
788
717
|
defaultGroupPolicy: resolveDefaultGroupPolicy(cfg)
|
|
789
718
|
});
|
|
790
719
|
const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
|
|
791
|
-
const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom));
|
|
720
|
+
const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom, account.config.allowFrom?.length ? account.config.allowFrom : void 0));
|
|
792
721
|
const mentionFacts = (() => {
|
|
793
722
|
if (!isGroup || event.type !== "message") return {
|
|
794
723
|
canDetectMention: false,
|
|
@@ -853,7 +782,7 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
853
782
|
allowFrom: normalizeStringEntries(account.config.allowFrom),
|
|
854
783
|
groupAllowFrom,
|
|
855
784
|
command: {
|
|
856
|
-
hasControlCommand:
|
|
785
|
+
hasControlCommand: shouldComputeCommandAuthorized(rawText, cfg),
|
|
857
786
|
groupOwnerAllowFrom: "none"
|
|
858
787
|
}
|
|
859
788
|
});
|
|
@@ -951,42 +880,42 @@ async function handleMessageEvent(event, context) {
|
|
|
951
880
|
});
|
|
952
881
|
return;
|
|
953
882
|
}
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
try {
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
883
|
+
const allMedia = [];
|
|
884
|
+
let mediaUnavailable = false;
|
|
885
|
+
if (isDownloadableLineMessageType(message.type)) try {
|
|
886
|
+
const originalFilename = message.type === "file" ? normalizeOptionalString(message.fileName) : void 0;
|
|
887
|
+
const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes, { originalFilename });
|
|
888
|
+
allMedia.push({
|
|
889
|
+
path: media.path,
|
|
890
|
+
contentType: media.contentType
|
|
891
|
+
});
|
|
892
|
+
} catch (err) {
|
|
893
|
+
mediaUnavailable = true;
|
|
894
|
+
const errMsg = String(err);
|
|
895
|
+
if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
|
|
896
|
+
else runtime.error?.(danger(`line: failed to download media: ${errMsg}`));
|
|
897
|
+
}
|
|
898
|
+
const messageContext = await buildLineMessageContext({
|
|
899
|
+
event,
|
|
900
|
+
allMedia,
|
|
901
|
+
mediaUnavailable,
|
|
902
|
+
cfg,
|
|
903
|
+
account,
|
|
904
|
+
commandAuthorized: decision.commandAccess.authorized,
|
|
905
|
+
groupHistories: context.groupHistories,
|
|
906
|
+
historyLimit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
907
|
+
});
|
|
908
|
+
if (!messageContext) {
|
|
909
|
+
logVerbose("line: skipping empty message");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
await processMessage(messageContext);
|
|
913
|
+
if (isGroup && context.groupHistories) {
|
|
914
|
+
const historyKey = groupId ?? roomId;
|
|
915
|
+
if (historyKey && context.groupHistories.has(historyKey)) createChannelHistoryWindow({ historyMap: context.groupHistories }).clear({
|
|
916
|
+
historyKey,
|
|
917
|
+
limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
981
918
|
});
|
|
982
|
-
if (!messageContext) {
|
|
983
|
-
logVerbose("line: skipping empty message");
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
await processMessage(messageContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
|
|
987
|
-
historyReservation.commit();
|
|
988
|
-
} finally {
|
|
989
|
-
historyReservation.release();
|
|
990
919
|
}
|
|
991
920
|
}
|
|
992
921
|
async function handleFollowEvent(event, _context) {
|
|
@@ -1017,41 +946,56 @@ async function handlePostbackEvent(event, context) {
|
|
|
1017
946
|
commandAuthorized: decision.commandAccess.authorized
|
|
1018
947
|
});
|
|
1019
948
|
if (!postbackContext) return;
|
|
1020
|
-
await context.processMessage(postbackContext
|
|
949
|
+
await context.processMessage(postbackContext);
|
|
1021
950
|
}
|
|
1022
951
|
async function handleLineWebhookEvents(events, context) {
|
|
1023
952
|
let firstError;
|
|
1024
|
-
for (const event of events)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
953
|
+
for (const event of events) {
|
|
954
|
+
const replayCandidate = getLineReplayCandidate(event, context);
|
|
955
|
+
const replaySkip = replayCandidate ? await claimLineReplayEvent(replayCandidate) : null;
|
|
956
|
+
if (replaySkip?.skip) {
|
|
957
|
+
if (replaySkip.inFlightResult) try {
|
|
958
|
+
await replaySkip.inFlightResult;
|
|
959
|
+
} catch (err) {
|
|
960
|
+
context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
|
|
961
|
+
firstError ??= err;
|
|
962
|
+
}
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
switch (event.type) {
|
|
967
|
+
case "message":
|
|
968
|
+
await handleMessageEvent(event, context);
|
|
969
|
+
break;
|
|
970
|
+
case "follow":
|
|
971
|
+
await handleFollowEvent(event, context);
|
|
972
|
+
break;
|
|
973
|
+
case "unfollow":
|
|
974
|
+
await handleUnfollowEvent(event, context);
|
|
975
|
+
break;
|
|
976
|
+
case "join":
|
|
977
|
+
await handleJoinEvent(event, context);
|
|
978
|
+
break;
|
|
979
|
+
case "leave":
|
|
980
|
+
await handleLeaveEvent(event, context);
|
|
981
|
+
break;
|
|
982
|
+
case "postback":
|
|
983
|
+
await handlePostbackEvent(event, context);
|
|
984
|
+
break;
|
|
985
|
+
default: logVerbose(`line: unhandled event type: ${event.type}`);
|
|
986
|
+
}
|
|
987
|
+
if (replayCandidate) await replayCandidate.cache.commit(replayCandidate.key);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
if (replayCandidate) {
|
|
990
|
+
if (err instanceof LineRetryableWebhookError) replayCandidate.cache.release(replayCandidate.key, { error: err });
|
|
991
|
+
else await replayCandidate.cache.commit(replayCandidate.key);
|
|
992
|
+
}
|
|
993
|
+
context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
|
|
994
|
+
firstError ??= err;
|
|
995
|
+
}
|
|
1029
996
|
}
|
|
1030
997
|
if (firstError) throw toLintErrorObject(firstError, "Non-Error thrown");
|
|
1031
998
|
}
|
|
1032
|
-
async function handleLineWebhookEvent(event, context) {
|
|
1033
|
-
switch (event.type) {
|
|
1034
|
-
case "message":
|
|
1035
|
-
await handleMessageEvent(event, context);
|
|
1036
|
-
break;
|
|
1037
|
-
case "follow":
|
|
1038
|
-
await handleFollowEvent(event, context);
|
|
1039
|
-
break;
|
|
1040
|
-
case "unfollow":
|
|
1041
|
-
await handleUnfollowEvent(event, context);
|
|
1042
|
-
break;
|
|
1043
|
-
case "join":
|
|
1044
|
-
await handleJoinEvent(event, context);
|
|
1045
|
-
break;
|
|
1046
|
-
case "leave":
|
|
1047
|
-
await handleLeaveEvent(event, context);
|
|
1048
|
-
break;
|
|
1049
|
-
case "postback":
|
|
1050
|
-
await handlePostbackEvent(event, context);
|
|
1051
|
-
break;
|
|
1052
|
-
default: logVerbose(`line: unhandled event type: ${event.type}`);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
999
|
function toLintErrorObject(value, fallbackMessage) {
|
|
1056
1000
|
if (value instanceof Error) return value;
|
|
1057
1001
|
if (typeof value === "string") return new Error(value);
|
|
@@ -1060,224 +1004,6 @@ function toLintErrorObject(value, fallbackMessage) {
|
|
|
1060
1004
|
return error;
|
|
1061
1005
|
}
|
|
1062
1006
|
//#endregion
|
|
1063
|
-
//#region extensions/line/src/webhook-spool.ts
|
|
1064
|
-
const LINE_WEBHOOK_SPOOL_VERSION = 1;
|
|
1065
|
-
const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
|
|
1066
|
-
const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
|
|
1067
|
-
const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
|
|
1068
|
-
const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
|
|
1069
|
-
const LineWebhookPayloadError = createChannelIngressError("LineWebhookPayloadError");
|
|
1070
|
-
var LineWebhookTerminalDeliveryError = class extends Error {
|
|
1071
|
-
constructor(message, options) {
|
|
1072
|
-
super(message, options);
|
|
1073
|
-
this.reason = "delivery-side-effects-committed";
|
|
1074
|
-
this.name = "LineWebhookTerminalDeliveryError";
|
|
1075
|
-
}
|
|
1076
|
-
};
|
|
1077
|
-
/** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
|
|
1078
|
-
function eventIdFor(event) {
|
|
1079
|
-
if (!event || typeof event !== "object") throw new LineWebhookPayloadError("LINE webhook event must be an object.");
|
|
1080
|
-
const candidate = event;
|
|
1081
|
-
if (candidate.type === "message") {
|
|
1082
|
-
const messageId = normalizeNullableString(candidate.message?.id);
|
|
1083
|
-
if (messageId) return `message:${messageId}`;
|
|
1084
|
-
}
|
|
1085
|
-
const webhookEventId = normalizeNullableString(candidate.webhookEventId);
|
|
1086
|
-
if (webhookEventId) return `event:${webhookEventId}`;
|
|
1087
|
-
throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
|
|
1088
|
-
}
|
|
1089
|
-
function laneKeyFor(event, eventId) {
|
|
1090
|
-
if (!event || typeof event !== "object") return eventId;
|
|
1091
|
-
const source = event.source;
|
|
1092
|
-
if (source?.type === "group") {
|
|
1093
|
-
const groupId = normalizeNullableString(source.groupId);
|
|
1094
|
-
if (groupId) return `group:${groupId}`;
|
|
1095
|
-
}
|
|
1096
|
-
if (source?.type === "room") {
|
|
1097
|
-
const roomId = normalizeNullableString(source.roomId);
|
|
1098
|
-
if (roomId) return `room:${roomId}`;
|
|
1099
|
-
}
|
|
1100
|
-
if (source?.type === "user") {
|
|
1101
|
-
const userId = normalizeNullableString(source.userId);
|
|
1102
|
-
if (userId) return `user:${userId}`;
|
|
1103
|
-
}
|
|
1104
|
-
return eventId;
|
|
1105
|
-
}
|
|
1106
|
-
function parseStoredEvent(rawEvent) {
|
|
1107
|
-
let event;
|
|
1108
|
-
try {
|
|
1109
|
-
event = JSON.parse(rawEvent);
|
|
1110
|
-
} catch (error) {
|
|
1111
|
-
throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
|
|
1112
|
-
}
|
|
1113
|
-
return event;
|
|
1114
|
-
}
|
|
1115
|
-
function errorText(error) {
|
|
1116
|
-
return error instanceof Error ? error.message : String(error);
|
|
1117
|
-
}
|
|
1118
|
-
function isLineAuthenticationFailure(error) {
|
|
1119
|
-
if (!error || typeof error !== "object") return false;
|
|
1120
|
-
const status = error.status;
|
|
1121
|
-
return status === 401 || status === 403;
|
|
1122
|
-
}
|
|
1123
|
-
async function waitForActiveDeliveriesBeforeDispose(activeDeliveries) {
|
|
1124
|
-
let timeout;
|
|
1125
|
-
try {
|
|
1126
|
-
return await Promise.race([Promise.allSettled(activeDeliveries).then(() => true), new Promise((resolve) => {
|
|
1127
|
-
timeout = setTimeout(() => resolve(false), LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS);
|
|
1128
|
-
timeout.unref?.();
|
|
1129
|
-
})]);
|
|
1130
|
-
} finally {
|
|
1131
|
-
if (timeout) clearTimeout(timeout);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
function createLineWebhookSpool(options) {
|
|
1135
|
-
const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
|
|
1136
|
-
const activeDeliveries = /* @__PURE__ */ new Set();
|
|
1137
|
-
let acceptsDeferredClaims = true;
|
|
1138
|
-
const monitor = createChannelIngressMonitor({
|
|
1139
|
-
queue,
|
|
1140
|
-
inspect: ({ event }) => {
|
|
1141
|
-
const eventId = eventIdFor(event);
|
|
1142
|
-
return {
|
|
1143
|
-
eventId,
|
|
1144
|
-
laneKey: laneKeyFor(event, eventId)
|
|
1145
|
-
};
|
|
1146
|
-
},
|
|
1147
|
-
payload: {
|
|
1148
|
-
version: LINE_WEBHOOK_SPOOL_VERSION,
|
|
1149
|
-
serialize: ({ event, destination }) => ({
|
|
1150
|
-
rawEvent: JSON.stringify(event),
|
|
1151
|
-
destination
|
|
1152
|
-
}),
|
|
1153
|
-
deserialize: ({ rawEvent, destination }) => ({
|
|
1154
|
-
event: parseStoredEvent(rawEvent),
|
|
1155
|
-
destination
|
|
1156
|
-
}),
|
|
1157
|
-
encode: ({ version, body }) => ({
|
|
1158
|
-
version,
|
|
1159
|
-
rawEvent: body.rawEvent,
|
|
1160
|
-
destination: body.destination
|
|
1161
|
-
}),
|
|
1162
|
-
decode: (payload) => {
|
|
1163
|
-
if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
|
|
1164
|
-
return {
|
|
1165
|
-
version: payload.version,
|
|
1166
|
-
body: {
|
|
1167
|
-
rawEvent: payload.rawEvent,
|
|
1168
|
-
destination: payload.destination
|
|
1169
|
-
}
|
|
1170
|
-
};
|
|
1171
|
-
},
|
|
1172
|
-
createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? "LINE webhook spool payload is invalid." : "LINE webhook event identity changed after durable admission.")
|
|
1173
|
-
},
|
|
1174
|
-
deliver: async ({ event, destination }, lifecycle) => {
|
|
1175
|
-
const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
|
|
1176
|
-
let handedOff = false;
|
|
1177
|
-
const delivery = options.deliver(event, destination, { turnAdoptionLifecycle: {
|
|
1178
|
-
...boundLifecycle,
|
|
1179
|
-
onAdopted: async () => {
|
|
1180
|
-
handedOff = true;
|
|
1181
|
-
await boundLifecycle.onAdopted();
|
|
1182
|
-
},
|
|
1183
|
-
onDeferred: () => {
|
|
1184
|
-
handedOff = true;
|
|
1185
|
-
if (!acceptsDeferredClaims) {
|
|
1186
|
-
Promise.resolve().then(() => boundLifecycle.onAbandoned()).catch((error) => {
|
|
1187
|
-
options.runtime.error?.(danger(`line: failed to abandon a late webhook delivery: ${errorText(error)}`));
|
|
1188
|
-
});
|
|
1189
|
-
return;
|
|
1190
|
-
}
|
|
1191
|
-
boundLifecycle.onDeferred();
|
|
1192
|
-
},
|
|
1193
|
-
onAbandoned: async () => {
|
|
1194
|
-
handedOff = true;
|
|
1195
|
-
await boundLifecycle.onAbandoned();
|
|
1196
|
-
}
|
|
1197
|
-
} });
|
|
1198
|
-
activeDeliveries.add(delivery);
|
|
1199
|
-
try {
|
|
1200
|
-
await delivery;
|
|
1201
|
-
} finally {
|
|
1202
|
-
activeDeliveries.delete(delivery);
|
|
1203
|
-
}
|
|
1204
|
-
if (stopTask && !handedOff) return {
|
|
1205
|
-
kind: "failed-retryable",
|
|
1206
|
-
error: /* @__PURE__ */ new Error("LINE webhook spool stopped before delivery handoff.")
|
|
1207
|
-
};
|
|
1208
|
-
},
|
|
1209
|
-
pollIntervalMs: LINE_WEBHOOK_DRAIN_INTERVAL_MS,
|
|
1210
|
-
retention: {
|
|
1211
|
-
pruneIntervalMs: 0,
|
|
1212
|
-
completedMaxEntries: 4096,
|
|
1213
|
-
failedMaxEntries: 4096
|
|
1214
|
-
},
|
|
1215
|
-
appendRetryDelaysMs: [0],
|
|
1216
|
-
waitForDeliveryIdleBeforeRepump: false,
|
|
1217
|
-
waitForDeliveryIdleOnStop: false,
|
|
1218
|
-
deferredClaims: "manual",
|
|
1219
|
-
runPumpTask: runDetachedWebhookWork,
|
|
1220
|
-
admissionMode: "durable-after-stop",
|
|
1221
|
-
drain: {
|
|
1222
|
-
adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
|
|
1223
|
-
orderBy: "received",
|
|
1224
|
-
scanLimit: LINE_WEBHOOK_DRAIN_SCAN_LIMIT,
|
|
1225
|
-
startLimit: LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
|
|
1226
|
-
retryPolicy: {
|
|
1227
|
-
maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
|
|
1228
|
-
deadLetterMinAgeMs: 0
|
|
1229
|
-
},
|
|
1230
|
-
resolveNonRetryableFailure: (error) => {
|
|
1231
|
-
if (error instanceof LineWebhookPayloadError) return {
|
|
1232
|
-
reason: "invalid-event",
|
|
1233
|
-
message: error.message
|
|
1234
|
-
};
|
|
1235
|
-
if (error instanceof LineWebhookTerminalDeliveryError) return {
|
|
1236
|
-
reason: error.reason,
|
|
1237
|
-
message: error.message
|
|
1238
|
-
};
|
|
1239
|
-
if (isLineAuthenticationFailure(error)) return {
|
|
1240
|
-
reason: "authentication-failed",
|
|
1241
|
-
message: errorText(error)
|
|
1242
|
-
};
|
|
1243
|
-
return null;
|
|
1244
|
-
},
|
|
1245
|
-
onLog: (message) => options.runtime.error?.(danger(`line: ${message}`))
|
|
1246
|
-
},
|
|
1247
|
-
createStoppedError: () => /* @__PURE__ */ new Error("LINE webhook spool is stopped."),
|
|
1248
|
-
onError: (error) => options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`))
|
|
1249
|
-
});
|
|
1250
|
-
let stopTask;
|
|
1251
|
-
return {
|
|
1252
|
-
accept: async (body) => {
|
|
1253
|
-
const events = body.events ?? [];
|
|
1254
|
-
if (events.length === 0) return;
|
|
1255
|
-
await monitor.admitBatch(events.map((event) => ({
|
|
1256
|
-
event,
|
|
1257
|
-
destination: body.destination ?? ""
|
|
1258
|
-
})), { receivedAt: Date.now() });
|
|
1259
|
-
},
|
|
1260
|
-
start: () => {
|
|
1261
|
-
if (!stopTask) monitor.start();
|
|
1262
|
-
},
|
|
1263
|
-
stop: () => {
|
|
1264
|
-
stopTask ??= (async () => {
|
|
1265
|
-
await monitor.pause();
|
|
1266
|
-
try {
|
|
1267
|
-
const deliveriesSettled = await waitForActiveDeliveriesBeforeDispose(activeDeliveries);
|
|
1268
|
-
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`));
|
|
1269
|
-
await monitor.waitForDeferredClaims();
|
|
1270
|
-
acceptsDeferredClaims = false;
|
|
1271
|
-
if (deliveriesSettled) await monitor.waitForIdle();
|
|
1272
|
-
} finally {
|
|
1273
|
-
await monitor.stop();
|
|
1274
|
-
}
|
|
1275
|
-
})();
|
|
1276
|
-
return stopTask;
|
|
1277
|
-
}
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1280
|
-
//#endregion
|
|
1281
1007
|
//#region extensions/line/src/bot.ts
|
|
1282
1008
|
function createLineBot(opts) {
|
|
1283
1009
|
const runtime = opts.runtime ?? createNonExitingRuntime();
|
|
@@ -1290,26 +1016,24 @@ function createLineBot(opts) {
|
|
|
1290
1016
|
const processMessage = opts.onMessage ?? (async () => {
|
|
1291
1017
|
logVerbose("line: no message handler configured");
|
|
1292
1018
|
});
|
|
1019
|
+
const replayCache = createLineWebhookReplayCache();
|
|
1293
1020
|
const groupHistories = /* @__PURE__ */ new Map();
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
deliver: async (event, _destination, control) => await handleLineWebhookEvents([event], {
|
|
1021
|
+
const handleWebhook = async (body) => {
|
|
1022
|
+
if (!body.events || body.events.length === 0) return;
|
|
1023
|
+
await handleLineWebhookEvents(body.events, {
|
|
1298
1024
|
cfg,
|
|
1299
1025
|
account,
|
|
1300
1026
|
runtime,
|
|
1301
1027
|
mediaMaxBytes,
|
|
1302
1028
|
processMessage,
|
|
1303
|
-
|
|
1029
|
+
replayCache,
|
|
1304
1030
|
groupHistories,
|
|
1305
1031
|
historyLimit: cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
1306
|
-
})
|
|
1307
|
-
}
|
|
1308
|
-
spool.start();
|
|
1032
|
+
});
|
|
1033
|
+
};
|
|
1309
1034
|
return {
|
|
1310
|
-
handleWebhook
|
|
1311
|
-
account
|
|
1312
|
-
stop: spool.stop
|
|
1035
|
+
handleWebhook,
|
|
1036
|
+
account
|
|
1313
1037
|
};
|
|
1314
1038
|
}
|
|
1315
1039
|
//#endregion
|
|
@@ -1327,9 +1051,63 @@ function resolveLineDurableReplyOptions(params) {
|
|
|
1327
1051
|
return { to: params.to };
|
|
1328
1052
|
}
|
|
1329
1053
|
//#endregion
|
|
1054
|
+
//#region extensions/line/src/reply-chunks.ts
|
|
1055
|
+
async function sendLineReplyChunks(params) {
|
|
1056
|
+
const hasQuickReplies = Boolean(params.quickReplies?.length);
|
|
1057
|
+
let replyTokenUsed = Boolean(params.replyTokenUsed);
|
|
1058
|
+
if (params.chunks.length === 0) return { replyTokenUsed };
|
|
1059
|
+
if (params.replyToken && !replyTokenUsed) try {
|
|
1060
|
+
const replyBatch = params.chunks.slice(0, 5);
|
|
1061
|
+
const remaining = params.chunks.slice(replyBatch.length);
|
|
1062
|
+
const replyMessages = replyBatch.map((chunk) => ({
|
|
1063
|
+
type: "text",
|
|
1064
|
+
text: chunk
|
|
1065
|
+
}));
|
|
1066
|
+
if (hasQuickReplies && remaining.length === 0 && replyMessages.length > 0) {
|
|
1067
|
+
const lastIndex = replyMessages.length - 1;
|
|
1068
|
+
replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(replyBatch[lastIndex], params.quickReplies);
|
|
1069
|
+
}
|
|
1070
|
+
await params.replyMessageLine(params.replyToken, replyMessages, {
|
|
1071
|
+
cfg: params.cfg,
|
|
1072
|
+
accountId: params.accountId
|
|
1073
|
+
});
|
|
1074
|
+
replyTokenUsed = true;
|
|
1075
|
+
for (let i = 0; i < remaining.length; i += 1) if (i === remaining.length - 1 && hasQuickReplies) await params.pushTextMessageWithQuickReplies(params.to, remaining[i], params.quickReplies, {
|
|
1076
|
+
cfg: params.cfg,
|
|
1077
|
+
accountId: params.accountId
|
|
1078
|
+
});
|
|
1079
|
+
else await params.pushMessageLine(params.to, remaining[i], {
|
|
1080
|
+
cfg: params.cfg,
|
|
1081
|
+
accountId: params.accountId
|
|
1082
|
+
});
|
|
1083
|
+
return { replyTokenUsed };
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
params.onReplyError?.(err);
|
|
1086
|
+
replyTokenUsed = true;
|
|
1087
|
+
}
|
|
1088
|
+
for (let i = 0; i < params.chunks.length; i += 1) if (i === params.chunks.length - 1 && hasQuickReplies) await params.pushTextMessageWithQuickReplies(params.to, params.chunks[i], params.quickReplies, {
|
|
1089
|
+
cfg: params.cfg,
|
|
1090
|
+
accountId: params.accountId
|
|
1091
|
+
});
|
|
1092
|
+
else await params.pushMessageLine(params.to, params.chunks[i], {
|
|
1093
|
+
cfg: params.cfg,
|
|
1094
|
+
accountId: params.accountId
|
|
1095
|
+
});
|
|
1096
|
+
return { replyTokenUsed };
|
|
1097
|
+
}
|
|
1098
|
+
//#endregion
|
|
1330
1099
|
//#region extensions/line/src/signature.ts
|
|
1331
1100
|
function validateLineSignature(body, signature, channelSecret) {
|
|
1332
|
-
|
|
1101
|
+
const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
|
|
1102
|
+
const hashBuffer = Buffer.from(hash);
|
|
1103
|
+
const signatureBuffer = Buffer.from(signature);
|
|
1104
|
+
const maxLen = Math.max(hashBuffer.length, signatureBuffer.length);
|
|
1105
|
+
const paddedHash = Buffer.alloc(maxLen);
|
|
1106
|
+
const paddedSig = Buffer.alloc(maxLen);
|
|
1107
|
+
hashBuffer.copy(paddedHash);
|
|
1108
|
+
signatureBuffer.copy(paddedSig);
|
|
1109
|
+
const timingResult = crypto.timingSafeEqual(paddedHash, paddedSig);
|
|
1110
|
+
return hashBuffer.length === signatureBuffer.length && timingResult;
|
|
1333
1111
|
}
|
|
1334
1112
|
//#endregion
|
|
1335
1113
|
//#region extensions/line/src/webhook-utils.ts
|
|
@@ -1342,8 +1120,8 @@ function parseLineWebhookBody(rawBody) {
|
|
|
1342
1120
|
}
|
|
1343
1121
|
//#endregion
|
|
1344
1122
|
//#region extensions/line/src/webhook-node.ts
|
|
1345
|
-
const LINE_WEBHOOK_MAX_BODY_BYTES =
|
|
1346
|
-
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 =
|
|
1123
|
+
const LINE_WEBHOOK_MAX_BODY_BYTES = 1048576;
|
|
1124
|
+
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 65536;
|
|
1347
1125
|
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1 = 5e3;
|
|
1348
1126
|
async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1) {
|
|
1349
1127
|
return await readRequestBodyWithLimit(req, {
|
|
@@ -1351,6 +1129,9 @@ async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_
|
|
|
1351
1129
|
timeoutMs
|
|
1352
1130
|
});
|
|
1353
1131
|
}
|
|
1132
|
+
function logLineWebhookDispatchError(runtime, err) {
|
|
1133
|
+
runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
|
|
1134
|
+
}
|
|
1354
1135
|
function createLineNodeWebhookHandler(params) {
|
|
1355
1136
|
const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
|
|
1356
1137
|
const readBody = params.readBody ?? readLineWebhookRequestBody;
|
|
@@ -1373,6 +1154,7 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1373
1154
|
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
1374
1155
|
return;
|
|
1375
1156
|
}
|
|
1157
|
+
let receiveContext;
|
|
1376
1158
|
try {
|
|
1377
1159
|
const signatureHeader = req.headers["x-line-signature"];
|
|
1378
1160
|
const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
|
|
@@ -1399,14 +1181,24 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1399
1181
|
return;
|
|
1400
1182
|
}
|
|
1401
1183
|
params.onRequestAuthenticated?.();
|
|
1184
|
+
receiveContext = createMessageReceiveContext({
|
|
1185
|
+
id: `${Date.now()}:line:webhook`,
|
|
1186
|
+
channel: "line",
|
|
1187
|
+
message: body,
|
|
1188
|
+
ackPolicy: "after_receive_record",
|
|
1189
|
+
onAck: () => {
|
|
1190
|
+
res.statusCode = 200;
|
|
1191
|
+
res.setHeader("Content-Type", "application/json");
|
|
1192
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
|
|
1402
1196
|
if (body.events && body.events.length > 0) {
|
|
1403
1197
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1404
|
-
|
|
1198
|
+
Promise.resolve().then(() => params.bot.handleWebhook(body)).catch((err) => logLineWebhookDispatchError(params.runtime, err));
|
|
1405
1199
|
}
|
|
1406
|
-
res.statusCode = 200;
|
|
1407
|
-
res.setHeader("Content-Type", "application/json");
|
|
1408
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
1409
1200
|
} catch (err) {
|
|
1201
|
+
await receiveContext?.nack(err);
|
|
1410
1202
|
if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
|
|
1411
1203
|
res.statusCode = 413;
|
|
1412
1204
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -1419,7 +1211,7 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1419
1211
|
res.end(JSON.stringify({ error: requestBodyErrorToText$1("REQUEST_BODY_TIMEOUT") }));
|
|
1420
1212
|
return;
|
|
1421
1213
|
}
|
|
1422
|
-
params.runtime.error?.(danger(`line webhook error: ${
|
|
1214
|
+
params.runtime.error?.(danger(`line webhook error: ${String(err)}`));
|
|
1423
1215
|
if (!res.headersSent) {
|
|
1424
1216
|
res.statusCode = 500;
|
|
1425
1217
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -1430,10 +1222,24 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1430
1222
|
}
|
|
1431
1223
|
//#endregion
|
|
1432
1224
|
//#region extensions/line/src/monitor.ts
|
|
1225
|
+
const runtimeState = /* @__PURE__ */ new Map();
|
|
1433
1226
|
const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
|
|
1434
|
-
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES =
|
|
1227
|
+
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
|
|
1435
1228
|
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
|
|
1436
1229
|
const lineWebhookTargets = /* @__PURE__ */ new Map();
|
|
1230
|
+
function recordChannelRuntimeState(params) {
|
|
1231
|
+
const key = `${params.channel}:${params.accountId}`;
|
|
1232
|
+
const existing = runtimeState.get(key) ?? {
|
|
1233
|
+
running: false,
|
|
1234
|
+
lastStartAt: null,
|
|
1235
|
+
lastStopAt: null,
|
|
1236
|
+
lastError: null
|
|
1237
|
+
};
|
|
1238
|
+
runtimeState.set(key, {
|
|
1239
|
+
...existing,
|
|
1240
|
+
...params.state
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1437
1243
|
function startLineLoadingKeepalive(params) {
|
|
1438
1244
|
const intervalMs = params.intervalMs ?? 18e3;
|
|
1439
1245
|
const loadingSeconds = params.loadingSeconds ?? 20;
|
|
@@ -1467,9 +1273,14 @@ async function monitorLineProvider(opts) {
|
|
|
1467
1273
|
accountId,
|
|
1468
1274
|
runtime,
|
|
1469
1275
|
config,
|
|
1470
|
-
onMessage: async (ctx
|
|
1276
|
+
onMessage: async (ctx) => {
|
|
1471
1277
|
if (!ctx) return;
|
|
1472
1278
|
const { ctxPayload, replyToken, route } = ctx;
|
|
1279
|
+
recordChannelRuntimeState({
|
|
1280
|
+
channel: "line",
|
|
1281
|
+
accountId: resolvedAccountId,
|
|
1282
|
+
state: { lastInboundAt: Date.now() }
|
|
1283
|
+
});
|
|
1473
1284
|
const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
|
|
1474
1285
|
const displayNamePromise = ctx.userId ? getUserDisplayName(ctx.userId, {
|
|
1475
1286
|
cfg: config,
|
|
@@ -1480,24 +1291,16 @@ async function monitorLineProvider(opts) {
|
|
|
1480
1291
|
userId: ctx.userId,
|
|
1481
1292
|
accountId: ctx.accountId
|
|
1482
1293
|
}) : null;
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
let turnAdopted = false;
|
|
1486
|
-
const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
|
|
1294
|
+
const displayName = await displayNamePromise;
|
|
1295
|
+
logVerbose(`line: received message from ${displayName} (${ctxPayload.From})`);
|
|
1487
1296
|
try {
|
|
1488
1297
|
const textLimit = 5e3;
|
|
1489
|
-
|
|
1298
|
+
let replyTokenUsed = false;
|
|
1299
|
+
const core = getLineRuntime();
|
|
1300
|
+
const turnResult = await core.channel.inbound.run({
|
|
1490
1301
|
channel: "line",
|
|
1491
1302
|
accountId: route.accountId,
|
|
1492
1303
|
raw: ctx,
|
|
1493
|
-
turnAdoptionLifecycle: {
|
|
1494
|
-
...ingressLifecycle,
|
|
1495
|
-
admission: "exclusive",
|
|
1496
|
-
onAdopted: async () => {
|
|
1497
|
-
await ingressLifecycle?.onAdopted();
|
|
1498
|
-
turnAdopted = true;
|
|
1499
|
-
}
|
|
1500
|
-
},
|
|
1501
1304
|
adapter: {
|
|
1502
1305
|
ingest: () => ({
|
|
1503
1306
|
id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
|
|
@@ -1507,14 +1310,14 @@ async function monitorLineProvider(opts) {
|
|
|
1507
1310
|
cfg: config,
|
|
1508
1311
|
channel: "line",
|
|
1509
1312
|
accountId: route.accountId,
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
},
|
|
1313
|
+
agentId: route.agentId,
|
|
1314
|
+
routeSessionKey: route.sessionKey,
|
|
1315
|
+
storePath: ctx.turn.storePath,
|
|
1514
1316
|
ctxPayload,
|
|
1317
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
1318
|
+
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
1515
1319
|
record: ctx.turn.record,
|
|
1516
1320
|
replyPipeline: {},
|
|
1517
|
-
...ingressLifecycle?.abortSignal ? { replyOptions: { abortSignal: ingressLifecycle.abortSignal } } : {},
|
|
1518
1321
|
delivery: {
|
|
1519
1322
|
durable: (payload, info) => resolveLineDurableReplyOptions({
|
|
1520
1323
|
payload,
|
|
@@ -1542,11 +1345,15 @@ async function monitorLineProvider(opts) {
|
|
|
1542
1345
|
buildTemplateMessageFromPayload,
|
|
1543
1346
|
processLineMessage,
|
|
1544
1347
|
chunkMarkdownText,
|
|
1348
|
+
sendLineReplyChunks,
|
|
1545
1349
|
replyMessageLine,
|
|
1350
|
+
pushMessageLine,
|
|
1351
|
+
pushTextMessageWithQuickReplies,
|
|
1546
1352
|
createQuickReplyItems,
|
|
1353
|
+
createTextMessageWithQuickReplies,
|
|
1547
1354
|
pushMessagesLine,
|
|
1548
1355
|
createFlexMessage,
|
|
1549
|
-
|
|
1356
|
+
createImageMessage,
|
|
1550
1357
|
createLocationMessage,
|
|
1551
1358
|
onReplyError: (replyErr) => {
|
|
1552
1359
|
logVerbose(`line: reply token failed, falling back to push: ${String(replyErr)}`);
|
|
@@ -1555,7 +1362,11 @@ async function monitorLineProvider(opts) {
|
|
|
1555
1362
|
});
|
|
1556
1363
|
replyTokenUsed = deliveryResult.replyTokenUsed;
|
|
1557
1364
|
if (deliveryResult.status === "partial") throw deliveryResult.error;
|
|
1558
|
-
|
|
1365
|
+
recordChannelRuntimeState({
|
|
1366
|
+
channel: "line",
|
|
1367
|
+
accountId: resolvedAccountId,
|
|
1368
|
+
state: { lastOutboundAt: Date.now() }
|
|
1369
|
+
});
|
|
1559
1370
|
},
|
|
1560
1371
|
onError: (err, info) => {
|
|
1561
1372
|
runtime.error?.(danger(`line ${info.kind} reply failed: ${String(err)}`));
|
|
@@ -1564,11 +1375,21 @@ async function monitorLineProvider(opts) {
|
|
|
1564
1375
|
})
|
|
1565
1376
|
}
|
|
1566
1377
|
});
|
|
1567
|
-
|
|
1378
|
+
const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
|
|
1379
|
+
if (!hasFinalInboundReplyDispatch(dispatchResult)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
|
|
1568
1380
|
} catch (err) {
|
|
1569
1381
|
runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
|
|
1570
|
-
if (
|
|
1571
|
-
|
|
1382
|
+
if (replyToken) try {
|
|
1383
|
+
await replyMessageLine(replyToken, [{
|
|
1384
|
+
type: "text",
|
|
1385
|
+
text: "Sorry, I encountered an error processing your message."
|
|
1386
|
+
}], {
|
|
1387
|
+
cfg: config,
|
|
1388
|
+
accountId: ctx.accountId
|
|
1389
|
+
});
|
|
1390
|
+
} catch (replyErr) {
|
|
1391
|
+
runtime.error?.(danger(`line: error reply failed: ${String(replyErr)}`));
|
|
1392
|
+
}
|
|
1572
1393
|
} finally {
|
|
1573
1394
|
stopLoading?.();
|
|
1574
1395
|
}
|
|
@@ -1646,14 +1467,16 @@ async function monitorLineProvider(opts) {
|
|
|
1646
1467
|
res.end(JSON.stringify({ error: "Invalid webhook payload" }));
|
|
1647
1468
|
return;
|
|
1648
1469
|
}
|
|
1649
|
-
|
|
1650
|
-
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1651
|
-
await match.target.bot.handleWebhook(body);
|
|
1652
|
-
res.setHeader("x-openclaw-delivery-accepted", "durable");
|
|
1653
|
-
}
|
|
1470
|
+
requestLifecycle.release();
|
|
1654
1471
|
res.statusCode = 200;
|
|
1655
1472
|
res.setHeader("Content-Type", "application/json");
|
|
1656
1473
|
res.end(JSON.stringify({ status: "ok" }));
|
|
1474
|
+
if (body.events && body.events.length > 0) {
|
|
1475
|
+
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1476
|
+
Promise.resolve().then(() => match.target.bot.handleWebhook(body)).catch((err) => {
|
|
1477
|
+
match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1657
1480
|
} catch (err) {
|
|
1658
1481
|
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
|
1659
1482
|
res.statusCode = 413;
|
|
@@ -1667,7 +1490,7 @@ async function monitorLineProvider(opts) {
|
|
|
1667
1490
|
res.end(JSON.stringify({ error: requestBodyErrorToText("REQUEST_BODY_TIMEOUT") }));
|
|
1668
1491
|
return;
|
|
1669
1492
|
}
|
|
1670
|
-
runtime.error?.(danger(`line webhook error: ${
|
|
1493
|
+
runtime.error?.(danger(`line webhook error: ${String(err)}`));
|
|
1671
1494
|
if (!res.headersSent) {
|
|
1672
1495
|
res.statusCode = 500;
|
|
1673
1496
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -1679,31 +1502,41 @@ async function monitorLineProvider(opts) {
|
|
|
1679
1502
|
}
|
|
1680
1503
|
}
|
|
1681
1504
|
});
|
|
1505
|
+
recordChannelRuntimeState({
|
|
1506
|
+
channel: "line",
|
|
1507
|
+
accountId: resolvedAccountId,
|
|
1508
|
+
state: {
|
|
1509
|
+
running: true,
|
|
1510
|
+
lastStartAt: Date.now()
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1682
1513
|
logVerbose(`line: registered webhook handler at ${normalizedPath}`);
|
|
1683
1514
|
let stopped = false;
|
|
1684
|
-
let stopPromise;
|
|
1685
1515
|
const stopHandler = () => {
|
|
1686
|
-
if (
|
|
1687
|
-
if (stopped) return Promise.resolve();
|
|
1516
|
+
if (stopped) return;
|
|
1688
1517
|
stopped = true;
|
|
1689
1518
|
logVerbose(`line: stopping provider for account ${resolvedAccountId}`);
|
|
1690
1519
|
unregisterHttp();
|
|
1691
|
-
|
|
1692
|
-
|
|
1520
|
+
recordChannelRuntimeState({
|
|
1521
|
+
channel: "line",
|
|
1522
|
+
accountId: resolvedAccountId,
|
|
1523
|
+
state: {
|
|
1524
|
+
running: false,
|
|
1525
|
+
lastStopAt: Date.now()
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1693
1528
|
};
|
|
1694
|
-
|
|
1695
|
-
if (abortSignal?.aborted) await stopHandler();
|
|
1529
|
+
if (abortSignal?.aborted) stopHandler();
|
|
1696
1530
|
else if (abortSignal) {
|
|
1697
|
-
abortSignal.addEventListener("abort",
|
|
1531
|
+
abortSignal.addEventListener("abort", stopHandler, { once: true });
|
|
1698
1532
|
await waitForAbortSignal(abortSignal);
|
|
1699
|
-
await stopHandler();
|
|
1700
1533
|
}
|
|
1701
1534
|
return {
|
|
1702
1535
|
account: bot.account,
|
|
1703
1536
|
handleWebhook: bot.handleWebhook,
|
|
1704
|
-
stop:
|
|
1705
|
-
|
|
1706
|
-
abortSignal?.removeEventListener("abort",
|
|
1537
|
+
stop: () => {
|
|
1538
|
+
stopHandler();
|
|
1539
|
+
abortSignal?.removeEventListener("abort", stopHandler);
|
|
1707
1540
|
}
|
|
1708
1541
|
};
|
|
1709
1542
|
}
|