@openclaw/line 2026.9.1-beta.1 → 2026.9.1
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/account-helpers-BSUyZwLm.js +12 -0
- package/dist/{accounts-BqEVsDoB.js → accounts-BBEFMYbY.js} +2 -1
- package/dist/api.js +2 -2
- package/dist/card-command-pwvE48Wb.js +252 -0
- package/dist/{channel-CsQLcrsA.js → channel-CF2AeULJ.js} +111 -380
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel.runtime-BF4Wk2dM.js +4 -0
- package/dist/contract-api.js +1 -1
- package/dist/doctor-contract-api.js +141 -0
- package/dist/index.js +5 -17
- package/dist/{markdown-to-line-BmZ4hhfC.js → markdown-to-line-B-sj-7II.js} +22 -23
- package/dist/{monitor-Bd9XDV1i.js → monitor-D0fHFXCi.js} +323 -234
- package/dist/monitor.runtime-Mgecav_D.js +2 -0
- package/dist/outbound.runtime-CGqyJ-Ug.js +3 -0
- package/dist/probe.runtime-CbtMgU4s.js +2 -0
- package/dist/rich-messages-DY1AGKAJ.js +742 -0
- package/dist/runtime-api.js +14 -15
- package/dist/{send-Dce0m539.js → send-CQQxpxPp.js} +142 -112
- package/dist/{send-receipt-CCPTalf9.js → send-retry-ChpfJ-LM.js} +326 -86
- package/dist/setup-api.js +1 -1
- package/dist/{setup-surface-W0Dbgu58.js → setup-surface-CDlgkTKi.js} +47 -39
- package/dist/webhook-spool-contract-1rY8OJw8.js +59 -0
- package/node_modules/typebox/build/format/email.mjs +1 -1
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
- package/node_modules/typebox/package.json +1 -1
- package/openclaw.plugin.json +16 -2
- package/package.json +5 -5
- package/dist/card-command-CmU47gD9.js +0 -230
- package/dist/channel.runtime-B83x7k_i.js +0 -4
- package/dist/media-control-cards-CksNB8Wk.js +0 -474
- package/dist/monitor.runtime-BCCa6qTX.js +0 -2
- package/dist/outbound.runtime-D4w0-Gkc.js +0 -3
- package/dist/probe-CM03wo6r.js +0 -28
- package/dist/probe.runtime-D13ZtXsd.js +0 -2
|
@@ -1,34 +1,36 @@
|
|
|
1
|
-
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BBEFMYbY.js";
|
|
2
|
+
import { f as resolveLineGroupConfigEntry, h as getLineRuntime, i as prepareLineReplyPayload, n as createLineQuickReply, u as buildLineQuickReplyFallbackText } from "./rich-messages-DY1AGKAJ.js";
|
|
3
|
+
import { n as findLineHttpError, r as resolveLineNonDispatchRetryable, t as explainLineRefusal, w as buildLineMediaMessage } from "./send-retry-ChpfJ-LM.js";
|
|
4
|
+
import { a as eventIdFor, n as LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE, o as laneKeyFor, r as LineWebhookPayloadError, t as LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON } from "./webhook-spool-contract-1rY8OJw8.js";
|
|
5
|
+
import { i as processLineMessage, o as buildTemplateMessageFromPayload } from "./markdown-to-line-B-sj-7II.js";
|
|
6
|
+
import { _ as showLoadingAnimation, a as getLineGroupName, d as pushMessageLine, f as pushMessagesLine, h as replyMessageLine, n as createLocationMessage, o as getUserDisplayName, s as getUserProfile, t as createFlexMessage, v as quotesLineBotMessage } from "./send-CQQxpxPp.js";
|
|
6
7
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
7
|
-
import {
|
|
8
|
-
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions,
|
|
9
|
-
import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, isChannelPartialDeliveryError, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
8
|
+
import { normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
9
|
+
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
|
|
10
|
+
import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, implicitMentionKindWhen, isChannelPartialDeliveryError, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
11
|
+
import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, readErrorName, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
|
|
10
12
|
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
11
13
|
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
12
14
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
15
|
+
import { classifyTransientNetworkErrorCode } from "openclaw/plugin-sdk/retry-runtime";
|
|
16
|
+
import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
|
|
13
17
|
import { firstDefined } from "openclaw/plugin-sdk/allow-from";
|
|
14
18
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
15
19
|
import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
|
|
16
20
|
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
17
21
|
import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal, warn } from "openclaw/plugin-sdk/runtime-env";
|
|
18
|
-
import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
|
|
19
|
-
import { HTTPFetchError } from "@line/bot-sdk";
|
|
20
|
-
import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, readErrorName, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
|
|
21
22
|
import crypto from "node:crypto";
|
|
22
23
|
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
|
23
|
-
import {
|
|
24
|
+
import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
|
|
24
25
|
import { channelReadyPatch, channelStoppedPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
25
26
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
26
|
-
import { canonicalizeWebhookRouteKey,
|
|
27
|
-
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError
|
|
27
|
+
import { canonicalizeWebhookRouteKey, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
|
|
28
|
+
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, runDetachedWebhookWork, sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
28
29
|
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
29
30
|
import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
|
|
30
|
-
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
31
|
-
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
31
|
+
import { getRuntimeConfig, getRuntimeConfigSnapshot, getRuntimeConfigSourceSnapshot, selectApplicableRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
32
|
+
import { resolveChannelImplicitMentions, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
33
|
+
import { reportChannelRoomJoin } from "openclaw/plugin-sdk/channel-join-intro-runtime";
|
|
32
34
|
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
|
|
33
35
|
import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
34
36
|
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
@@ -110,7 +112,7 @@ async function fetchLineContentWhenReady(messageId, channelAccessToken, signal)
|
|
|
110
112
|
}
|
|
111
113
|
await response.body?.cancel().catch(() => void 0);
|
|
112
114
|
if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
|
|
113
|
-
if (attempt <
|
|
115
|
+
if (attempt < 5) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal });
|
|
114
116
|
}
|
|
115
117
|
} catch (err) {
|
|
116
118
|
if (signal.aborted) throw signal.reason;
|
|
@@ -125,7 +127,7 @@ function isRetryableLineInboundMediaError(err) {
|
|
|
125
127
|
if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
|
|
126
128
|
return false;
|
|
127
129
|
}
|
|
128
|
-
async function downloadLineMedia(messageId, channelAccessToken, maxBytes =
|
|
130
|
+
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10485760, options) {
|
|
129
131
|
options?.signal?.throwIfAborted();
|
|
130
132
|
const deadlineAbort = new AbortController();
|
|
131
133
|
const signal = options?.signal ? AbortSignal.any([options.signal, deadlineAbort.signal]) : deadlineAbort.signal;
|
|
@@ -159,11 +161,8 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
|
|
|
159
161
|
function toLineDeliveryError(error) {
|
|
160
162
|
return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
|
|
161
163
|
}
|
|
162
|
-
function getLineHttpError(error) {
|
|
163
|
-
return collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]).find((candidate) => candidate instanceof HTTPFetchError);
|
|
164
|
-
}
|
|
165
164
|
function canFallbackAfterLineReplyFailure(error) {
|
|
166
|
-
const httpError =
|
|
165
|
+
const httpError = findLineHttpError(error);
|
|
167
166
|
if (httpError) return httpError.status >= 400 && httpError.status < 500 && httpError.status !== 408;
|
|
168
167
|
const candidates = collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]);
|
|
169
168
|
if (candidates.some((candidate) => readErrorName(candidate) === "AbortError" || classifyTransientNetworkErrorCode(extractErrorCode(candidate)) === "ambiguous")) return false;
|
|
@@ -235,7 +234,7 @@ async function deliverLineAutoReply(params) {
|
|
|
235
234
|
} catch (err) {
|
|
236
235
|
if (isChannelPartialDeliveryError(err) || !canFallbackAfterLineReplyFailure(err)) throw err;
|
|
237
236
|
deps.onReplyError?.(err);
|
|
238
|
-
await pushLineMessages(replyBatch,
|
|
237
|
+
await pushLineMessages(replyBatch, findLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
|
|
239
238
|
} finally {
|
|
240
239
|
replyTokenUsed = true;
|
|
241
240
|
}
|
|
@@ -244,7 +243,15 @@ async function deliverLineAutoReply(params) {
|
|
|
244
243
|
if (remaining.length > 0) await pushLineMessages(remaining, true);
|
|
245
244
|
};
|
|
246
245
|
const richMessages = [];
|
|
247
|
-
const
|
|
246
|
+
const quickReplyItems = lineData.quickReplyItems?.length ? lineData.quickReplyItems : (lineData.quickReplies ?? []).map((label) => ({
|
|
247
|
+
label,
|
|
248
|
+
action: {
|
|
249
|
+
type: "command",
|
|
250
|
+
command: label
|
|
251
|
+
}
|
|
252
|
+
}));
|
|
253
|
+
const quickReplyLabels = quickReplyItems.map((item) => item.label);
|
|
254
|
+
const hasQuickReplies = quickReplyItems.length > 0;
|
|
248
255
|
if (lineData.flexMessage) richMessages.push(deps.createFlexMessage(lineData.flexMessage.altText, lineData.flexMessage.contents));
|
|
249
256
|
if (lineData.templateMessage) {
|
|
250
257
|
const templateMsg = deps.buildTemplateMessageFromPayload(lineData.templateMessage);
|
|
@@ -264,7 +271,7 @@ async function deliverLineAutoReply(params) {
|
|
|
264
271
|
const chunks = orderedMessages ? orderedMessages.flatMap((message) => message.type === "text" ? [message.text] : []) : processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : [];
|
|
265
272
|
const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
|
|
266
273
|
const mediaOpts = {
|
|
267
|
-
mediaKind:
|
|
274
|
+
mediaKind: lineData.mediaKind,
|
|
268
275
|
previewImageUrl: lineData.previewImageUrl,
|
|
269
276
|
durationMs: lineData.durationMs,
|
|
270
277
|
trackingId: lineData.trackingId
|
|
@@ -292,14 +299,14 @@ async function deliverLineAutoReply(params) {
|
|
|
292
299
|
];
|
|
293
300
|
if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
|
|
294
301
|
type: "text",
|
|
295
|
-
text: buildLineQuickReplyFallbackText(
|
|
302
|
+
text: buildLineQuickReplyFallbackText(quickReplyLabels)
|
|
296
303
|
});
|
|
297
304
|
if (hasQuickReplies) {
|
|
298
305
|
const targetMessages = orderedDeliveryMessages?.length ? orderedDeliveryMessages : textMessages.length > 0 ? textMessages : richMediaMessages;
|
|
299
306
|
const lastIndex = targetMessages.length - 1;
|
|
300
307
|
targetMessages[lastIndex] = {
|
|
301
308
|
...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
|
|
302
|
-
quickReply:
|
|
309
|
+
quickReply: createLineQuickReply(quickReplyItems)
|
|
303
310
|
};
|
|
304
311
|
}
|
|
305
312
|
const messages = hasQuickReplies ? [...richMediaMessages, ...orderedDeliveryMessages ?? textMessages] : [...orderedDeliveryMessages ?? textMessages, ...richMediaMessages];
|
|
@@ -308,25 +315,33 @@ async function deliverLineAutoReply(params) {
|
|
|
308
315
|
} catch (err) {
|
|
309
316
|
deliveryError ??= err;
|
|
310
317
|
const failedSegment = typeof err === "object" && err !== null ? failedPushSegments.get(err) : void 0;
|
|
311
|
-
const httpError =
|
|
318
|
+
const httpError = findLineHttpError(err);
|
|
312
319
|
const retryCandidates = failedSegment ? [...failedSegment.allowFailedBatchTextRecovery ? failedSegment.failedBatch : [], ...failedSegment.unattemptedTail] : [];
|
|
313
320
|
const retryTextMessages = retryCandidates.filter((message) => message.type === "text");
|
|
314
321
|
const quickRepliesNeedCarrier = hasQuickReplies && retryCandidates.some((message) => "quickReply" in message);
|
|
315
322
|
const retryMessages = retryTextMessages.length > 0 ? retryTextMessages : quickRepliesNeedCarrier ? [{
|
|
316
323
|
type: "text",
|
|
317
|
-
text: buildLineQuickReplyFallbackText(
|
|
318
|
-
quickReply:
|
|
324
|
+
text: buildLineQuickReplyFallbackText(quickReplyLabels),
|
|
325
|
+
quickReply: createLineQuickReply(quickReplyItems)
|
|
319
326
|
}] : [];
|
|
320
|
-
if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400) {
|
|
327
|
+
if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400 && resolveLineNonDispatchRetryable(err) !== void 0) {
|
|
321
328
|
const lastRetryMessage = retryMessages.at(-1);
|
|
322
|
-
if (quickRepliesNeedCarrier && lastRetryMessage && !lastRetryMessage.quickReply) lastRetryMessage.quickReply =
|
|
329
|
+
if (quickRepliesNeedCarrier && lastRetryMessage && !lastRetryMessage.quickReply) lastRetryMessage.quickReply = createLineQuickReply(quickReplyItems);
|
|
323
330
|
try {
|
|
324
331
|
await sendLineMessages(retryMessages, false);
|
|
325
332
|
} catch {}
|
|
326
333
|
}
|
|
327
334
|
}
|
|
328
335
|
if (deliveryError !== void 0) {
|
|
329
|
-
if (!visibleReplySent)
|
|
336
|
+
if (!visibleReplySent) {
|
|
337
|
+
const named = toLineDeliveryError(deliveryError);
|
|
338
|
+
const refusal = await explainLineRefusal({
|
|
339
|
+
error: named,
|
|
340
|
+
cfg: params.cfg,
|
|
341
|
+
accountId
|
|
342
|
+
});
|
|
343
|
+
throw refusal.reason !== named.message ? new Error(refusal.reason, { cause: deliveryError }) : named;
|
|
344
|
+
}
|
|
330
345
|
return {
|
|
331
346
|
status: "partial",
|
|
332
347
|
replyTokenUsed,
|
|
@@ -341,6 +356,44 @@ async function deliverLineAutoReply(params) {
|
|
|
341
356
|
};
|
|
342
357
|
}
|
|
343
358
|
//#endregion
|
|
359
|
+
//#region extensions/line/src/mentions.ts
|
|
360
|
+
function getLineMentionees(message) {
|
|
361
|
+
return message.type === "text" ? message.mention?.mentionees ?? [] : [];
|
|
362
|
+
}
|
|
363
|
+
function addressesLineBot(mentionee) {
|
|
364
|
+
return mentionee.type === "all" || mentionee.isSelf === true;
|
|
365
|
+
}
|
|
366
|
+
function isLineBotMentioned(message) {
|
|
367
|
+
return getLineMentionees(message).some(addressesLineBot);
|
|
368
|
+
}
|
|
369
|
+
function hasAnyLineMention(message) {
|
|
370
|
+
return getLineMentionees(message).length > 0;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Text to interpret as a command, with the mention that addressed the bot removed.
|
|
374
|
+
*
|
|
375
|
+
* LINE writes a mention as the plain channel display name, so no pattern can
|
|
376
|
+
* tell it apart from a member's name; `mentionees[].index`/`length` (UTF-16
|
|
377
|
+
* code units, the unit LINE counts text in) is the only authoritative marker.
|
|
378
|
+
* Leaving it in place makes a group `@<bot> /status` parse as prose, and group
|
|
379
|
+
* chats require the mention to reach the bot at all.
|
|
380
|
+
*/
|
|
381
|
+
function resolveLineMentionStrippedText(message) {
|
|
382
|
+
const text = message.type === "text" ? message.text : "";
|
|
383
|
+
const spans = getLineMentionees(message).filter(addressesLineBot).map((mentionee) => ({
|
|
384
|
+
start: mentionee.index,
|
|
385
|
+
end: mentionee.index + mentionee.length
|
|
386
|
+
})).toSorted((left, right) => left.start - right.start);
|
|
387
|
+
let stripped = "";
|
|
388
|
+
let cursor = 0;
|
|
389
|
+
for (const span of spans) {
|
|
390
|
+
stripped += text.slice(cursor, span.start);
|
|
391
|
+
if (/\S$/u.test(stripped) && /^\S/u.test(text.slice(span.end))) stripped += " ";
|
|
392
|
+
cursor = Math.max(cursor, span.end);
|
|
393
|
+
}
|
|
394
|
+
return `${stripped}${text.slice(cursor)}`.trim();
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
344
397
|
//#region extensions/line/src/bot-message-context.ts
|
|
345
398
|
function getLineSourceInfo(source) {
|
|
346
399
|
if (!source) return {
|
|
@@ -425,30 +478,23 @@ async function resolveLineInboundRoute(params) {
|
|
|
425
478
|
route
|
|
426
479
|
};
|
|
427
480
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
"
|
|
441
|
-
|
|
442
|
-
};
|
|
443
|
-
function describeStickerKeywords(sticker) {
|
|
444
|
-
const keywords = sticker.keywords;
|
|
445
|
-
if (keywords && keywords.length > 0) return keywords.slice(0, 3).join(", ");
|
|
446
|
-
const stickerText = sticker.text;
|
|
447
|
-
if (stickerText) return stickerText;
|
|
448
|
-
return "";
|
|
481
|
+
/**
|
|
482
|
+
* Describe a sticker from what its webhook actually carries: LINE sends up to
|
|
483
|
+
* 15 keywords for the sticker, and a message sticker also carries the sender's
|
|
484
|
+
* own text. The package name is not among those facts and cannot be derived
|
|
485
|
+
* from the package id, so it is not part of the description.
|
|
486
|
+
*/
|
|
487
|
+
function describeLineSticker(sticker) {
|
|
488
|
+
const description = readNonEmptyStringPreservingWhitespace(sticker.text) ?? normalizeStringEntries(sticker.keywords ?? []).slice(0, 3).join(", ");
|
|
489
|
+
return description ? `[Sent a sticker: ${description}]` : "[Sent a sticker]";
|
|
490
|
+
}
|
|
491
|
+
function readLineTextMessageBody(message) {
|
|
492
|
+
let text = message.text;
|
|
493
|
+
for (const { index, length } of (message.emojis ?? []).toSorted((a, b) => b.index - a.index)) if (index >= 0 && length === 2 && text.slice(index, index + length) === "()") text = `${text.slice(0, index)}[emoji]${text.slice(index + length)}`;
|
|
494
|
+
return text;
|
|
449
495
|
}
|
|
450
496
|
function extractMessageText(message) {
|
|
451
|
-
if (message.type === "text") return message
|
|
497
|
+
if (message.type === "text") return readLineTextMessageBody(message);
|
|
452
498
|
if (message.type === "location") {
|
|
453
499
|
const loc = message;
|
|
454
500
|
return formatLocationText({
|
|
@@ -458,13 +504,7 @@ function extractMessageText(message) {
|
|
|
458
504
|
address: loc.address
|
|
459
505
|
}) ?? "";
|
|
460
506
|
}
|
|
461
|
-
if (message.type === "sticker")
|
|
462
|
-
const sticker = message;
|
|
463
|
-
const packageName = STICKER_PACKAGES[sticker.packageId] ?? "sticker";
|
|
464
|
-
const keywords = describeStickerKeywords(sticker);
|
|
465
|
-
if (keywords) return `[Sent a ${packageName} sticker: ${keywords}]`;
|
|
466
|
-
return `[Sent a ${packageName} sticker]`;
|
|
467
|
-
}
|
|
507
|
+
if (message.type === "sticker") return describeLineSticker(message);
|
|
468
508
|
return "";
|
|
469
509
|
}
|
|
470
510
|
function extractNativeMediaKind(message) {
|
|
@@ -478,9 +518,23 @@ function extractNativeMediaKind(message) {
|
|
|
478
518
|
}
|
|
479
519
|
async function finalizeLineInboundContext(params) {
|
|
480
520
|
const senderId = params.source.userId ?? "unknown";
|
|
481
|
-
const
|
|
482
|
-
|
|
521
|
+
const clientOpts = {
|
|
522
|
+
cfg: params.cfg,
|
|
523
|
+
accountId: params.account.accountId,
|
|
524
|
+
channelAccessToken: params.account.channelAccessToken
|
|
525
|
+
};
|
|
526
|
+
const [senderName, groupName] = await Promise.all([params.source.userId ? getUserProfile(params.source.userId, {
|
|
527
|
+
...clientOpts,
|
|
528
|
+
groupId: params.source.groupId,
|
|
529
|
+
roomId: params.source.roomId
|
|
530
|
+
}).then((profile) => profile?.displayName) : void 0, params.source.groupId ? getLineGroupName(params.source.groupId, clientOpts) : void 0]);
|
|
531
|
+
const senderLabel = senderName ?? (params.source.userId ? `user:${params.source.userId}` : "unknown");
|
|
532
|
+
const conversationLabel = params.source.isGroup ? groupName ?? (params.source.groupId ? `group:${params.source.groupId}` : params.source.roomId ? `room:${params.source.roomId}` : "unknown-group") : senderLabel;
|
|
483
533
|
const address = params.source.groupId ? `line:group:${params.source.groupId}` : params.source.roomId ? `line:room:${params.source.roomId}` : `line:${params.source.userId ?? params.source.peerId}`;
|
|
534
|
+
const groupConfig = params.source.isGroup ? resolveLineGroupConfigEntry(params.account.config.groups, {
|
|
535
|
+
groupId: params.source.groupId,
|
|
536
|
+
roomId: params.source.roomId
|
|
537
|
+
}) : void 0;
|
|
484
538
|
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
|
485
539
|
cfg: params.cfg,
|
|
486
540
|
agentId: params.route.agentId,
|
|
@@ -494,7 +548,10 @@ async function finalizeLineInboundContext(params) {
|
|
|
494
548
|
timestamp: params.timestamp,
|
|
495
549
|
body: agentBody,
|
|
496
550
|
chatType: params.source.isGroup ? "group" : "direct",
|
|
497
|
-
sender: {
|
|
551
|
+
sender: {
|
|
552
|
+
id: senderId,
|
|
553
|
+
name: senderName
|
|
554
|
+
},
|
|
498
555
|
previousTimestamp,
|
|
499
556
|
envelope: envelopeOptions
|
|
500
557
|
});
|
|
@@ -505,7 +562,10 @@ async function finalizeLineInboundContext(params) {
|
|
|
505
562
|
messageId: params.messageSid,
|
|
506
563
|
timestamp: params.timestamp,
|
|
507
564
|
from: address,
|
|
508
|
-
sender: {
|
|
565
|
+
sender: {
|
|
566
|
+
id: senderId,
|
|
567
|
+
name: senderName
|
|
568
|
+
},
|
|
509
569
|
conversation: {
|
|
510
570
|
kind: params.source.isGroup ? "group" : "direct",
|
|
511
571
|
id: params.source.peerId,
|
|
@@ -525,18 +585,18 @@ async function finalizeLineInboundContext(params) {
|
|
|
525
585
|
body,
|
|
526
586
|
bodyForAgent: agentBody,
|
|
527
587
|
rawBody: params.rawBody,
|
|
528
|
-
commandBody: params.rawBody,
|
|
588
|
+
commandBody: params.commandBody ?? params.rawBody,
|
|
529
589
|
inboundHistory: params.inboundHistory
|
|
530
590
|
},
|
|
531
|
-
access: {
|
|
591
|
+
access: {
|
|
592
|
+
commands: { authorized: params.commandAuthorized },
|
|
593
|
+
mentions: params.mentions
|
|
594
|
+
},
|
|
532
595
|
media,
|
|
533
596
|
extra: {
|
|
534
597
|
...params.locationContext,
|
|
535
|
-
GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
|
|
536
|
-
GroupSystemPrompt:
|
|
537
|
-
groupId: params.source.groupId,
|
|
538
|
-
roomId: params.source.roomId
|
|
539
|
-
})?.systemPrompt) : void 0
|
|
598
|
+
GroupSubject: params.source.isGroup ? groupName ?? params.source.groupId ?? params.source.roomId : void 0,
|
|
599
|
+
GroupSystemPrompt: normalizeOptionalString(groupConfig?.systemPrompt)
|
|
540
600
|
}
|
|
541
601
|
});
|
|
542
602
|
const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
|
|
@@ -551,11 +611,13 @@ async function finalizeLineInboundContext(params) {
|
|
|
551
611
|
if (shouldLogVerbose()) {
|
|
552
612
|
const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
|
|
553
613
|
const mediaInfo = params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1 ? ` mediaCount=${params.verboseLog.mediaCount}` : "";
|
|
554
|
-
|
|
614
|
+
const label = params.verboseLog.kind === "inbound" ? "line inbound" : "line postback";
|
|
615
|
+
logVerbose(`${label}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
|
|
555
616
|
}
|
|
556
617
|
return {
|
|
557
618
|
ctxPayload,
|
|
558
619
|
replyToken: params.event.replyToken,
|
|
620
|
+
skillFilter: groupConfig?.skills,
|
|
559
621
|
turn: {
|
|
560
622
|
storePath,
|
|
561
623
|
record: {
|
|
@@ -626,15 +688,17 @@ async function buildLineMessageContext(params) {
|
|
|
626
688
|
},
|
|
627
689
|
rawBody,
|
|
628
690
|
agentBody,
|
|
691
|
+
commandBody: resolveLineMentionStrippedText(message) || rawBody,
|
|
692
|
+
mentions: params.mentions,
|
|
629
693
|
timestamp,
|
|
630
694
|
messageSid: messageId,
|
|
631
695
|
commandAuthorized,
|
|
632
|
-
channelIngress:
|
|
696
|
+
channelIngress: await params.resolveChannelIngress?.({
|
|
633
697
|
agentId: route.agentId,
|
|
634
698
|
sessionKey: route.sessionKey,
|
|
635
699
|
messageId,
|
|
636
700
|
inboundEventKind: "user_request"
|
|
637
|
-
})
|
|
701
|
+
}),
|
|
638
702
|
buildContext: params.buildContext,
|
|
639
703
|
media: mediaFacts,
|
|
640
704
|
locationContext,
|
|
@@ -647,6 +711,7 @@ async function buildLineMessageContext(params) {
|
|
|
647
711
|
return {
|
|
648
712
|
ctxPayload: finalized.ctxPayload,
|
|
649
713
|
turn: finalized.turn,
|
|
714
|
+
skillFilter: finalized.skillFilter,
|
|
650
715
|
event,
|
|
651
716
|
userId,
|
|
652
717
|
groupId,
|
|
@@ -666,14 +731,18 @@ async function buildLinePostbackContext(params) {
|
|
|
666
731
|
account
|
|
667
732
|
});
|
|
668
733
|
const timestamp = event.timestamp;
|
|
669
|
-
const
|
|
670
|
-
if (!
|
|
671
|
-
let
|
|
672
|
-
if (
|
|
673
|
-
const searchParams = new URLSearchParams(
|
|
734
|
+
const rawBody = event.postback?.data?.trim() ?? "";
|
|
735
|
+
if (!rawBody) return null;
|
|
736
|
+
let agentBody = rawBody;
|
|
737
|
+
if (rawBody.includes("line.action=")) {
|
|
738
|
+
const searchParams = new URLSearchParams(rawBody);
|
|
674
739
|
const action = searchParams.get("line.action") ?? "";
|
|
675
740
|
const device = searchParams.get("line.device");
|
|
676
|
-
|
|
741
|
+
agentBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
|
|
742
|
+
}
|
|
743
|
+
for (const [key, value] of Object.entries(event.postback.params ?? {}).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
|
|
744
|
+
const picked = normalizeOptionalString(value);
|
|
745
|
+
if (picked) agentBody += ` ${key}=${picked}`;
|
|
677
746
|
}
|
|
678
747
|
const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
|
|
679
748
|
const finalized = await finalizeLineInboundContext({
|
|
@@ -689,15 +758,16 @@ async function buildLinePostbackContext(params) {
|
|
|
689
758
|
peerId
|
|
690
759
|
},
|
|
691
760
|
rawBody,
|
|
761
|
+
agentBody,
|
|
692
762
|
timestamp,
|
|
693
763
|
messageSid,
|
|
694
764
|
commandAuthorized,
|
|
695
|
-
channelIngress:
|
|
765
|
+
channelIngress: await params.resolveChannelIngress?.({
|
|
696
766
|
agentId: route.agentId,
|
|
697
767
|
sessionKey: route.sessionKey,
|
|
698
768
|
messageId: messageSid,
|
|
699
769
|
inboundEventKind: "user_request"
|
|
700
|
-
})
|
|
770
|
+
}),
|
|
701
771
|
buildContext: params.buildContext,
|
|
702
772
|
media: [],
|
|
703
773
|
verboseLog: { kind: "postback" }
|
|
@@ -705,6 +775,7 @@ async function buildLinePostbackContext(params) {
|
|
|
705
775
|
return {
|
|
706
776
|
ctxPayload: finalized.ctxPayload,
|
|
707
777
|
turn: finalized.turn,
|
|
778
|
+
skillFilter: finalized.skillFilter,
|
|
708
779
|
event,
|
|
709
780
|
userId,
|
|
710
781
|
groupId,
|
|
@@ -764,12 +835,6 @@ function isDownloadableLineMessageType(messageType) {
|
|
|
764
835
|
function normalizeLineIngressEntry(value) {
|
|
765
836
|
return normalizeLineAllowEntry(value) || null;
|
|
766
837
|
}
|
|
767
|
-
function resolveLineGroupConfig(params) {
|
|
768
|
-
return resolveLineGroupConfigEntry(params.config.groups, {
|
|
769
|
-
groupId: params.groupId,
|
|
770
|
-
roomId: params.roomId
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
838
|
async function sendLinePairingReply(params) {
|
|
774
839
|
const { senderId, replyToken, context } = params;
|
|
775
840
|
const idLabel = (() => {
|
|
@@ -821,12 +886,11 @@ async function sendLinePairingReply(params) {
|
|
|
821
886
|
}
|
|
822
887
|
});
|
|
823
888
|
}
|
|
824
|
-
async function
|
|
889
|
+
async function resolveLineEventAdmission(event, context) {
|
|
825
890
|
const { cfg, account } = context;
|
|
826
891
|
const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
|
|
827
892
|
const senderId = userId ?? "";
|
|
828
|
-
const groupConfig =
|
|
829
|
-
config: account.config,
|
|
893
|
+
const groupConfig = resolveLineGroupConfigEntry(account.config.groups, {
|
|
830
894
|
groupId,
|
|
831
895
|
roomId
|
|
832
896
|
});
|
|
@@ -841,11 +905,7 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
841
905
|
const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
|
|
842
906
|
const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom));
|
|
843
907
|
const mentionFacts = (() => {
|
|
844
|
-
if (!isGroup || event.type !== "message") return
|
|
845
|
-
canDetectMention: false,
|
|
846
|
-
wasMentioned: false,
|
|
847
|
-
hasAnyMention: false
|
|
848
|
-
};
|
|
908
|
+
if (!isGroup || event.type !== "message") return;
|
|
849
909
|
const peerId = groupId ?? roomId ?? userId ?? "unknown";
|
|
850
910
|
const { agentId } = resolveAgentRoute({
|
|
851
911
|
cfg,
|
|
@@ -862,7 +922,9 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
862
922
|
return {
|
|
863
923
|
canDetectMention: event.message.type === "text",
|
|
864
924
|
wasMentioned: wasMentionedByNative || wasMentionedByPattern,
|
|
865
|
-
|
|
925
|
+
explicitlyMentionedBot: wasMentionedByNative,
|
|
926
|
+
hasAnyMention: hasAnyLineMention(event.message),
|
|
927
|
+
implicitMentionKinds: implicitMentionKindWhen("quoted_bot", quotesLineBotMessage(account.accountId, resolveLineQuotedMessageId(event.message)))
|
|
866
928
|
};
|
|
867
929
|
})();
|
|
868
930
|
const resolveAccess = async (contextBinding) => await resolveStableChannelMessageIngress({
|
|
@@ -876,7 +938,7 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
876
938
|
},
|
|
877
939
|
cfg,
|
|
878
940
|
readStoreAllowFrom: async () => await readChannelAllowFromStore("line", void 0, account.accountId),
|
|
879
|
-
subject: { stableId: senderId },
|
|
941
|
+
subject: event.type === "join" ? {} : { stableId: senderId },
|
|
880
942
|
conversation: {
|
|
881
943
|
kind: isGroup ? "group" : "direct",
|
|
882
944
|
id: (groupId ?? roomId ?? senderId) || "unknown"
|
|
@@ -886,20 +948,20 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
886
948
|
id: "line:group-config",
|
|
887
949
|
enabled: false
|
|
888
950
|
} } : {},
|
|
889
|
-
mentionFacts
|
|
890
|
-
|
|
891
|
-
wasMentioned: mentionFacts.wasMentioned,
|
|
892
|
-
hasAnyMention: mentionFacts.hasAnyMention,
|
|
893
|
-
implicitMentionKinds: []
|
|
894
|
-
} : void 0,
|
|
895
|
-
event: { kind: event.type === "postback" ? "postback" : "message" },
|
|
951
|
+
mentionFacts,
|
|
952
|
+
event: { kind: event.type === "join" ? "system" : event.type },
|
|
896
953
|
dmPolicy,
|
|
897
954
|
groupPolicy,
|
|
898
955
|
policy: {
|
|
899
956
|
groupAllowFromFallbackToAllowFrom: false,
|
|
900
957
|
activation: {
|
|
901
958
|
requireMention: isGroup && event.type === "message" && requireMention,
|
|
902
|
-
allowTextCommands: true
|
|
959
|
+
allowTextCommands: true,
|
|
960
|
+
implicitMentions: resolveChannelImplicitMentions({
|
|
961
|
+
cfg,
|
|
962
|
+
channel: "line",
|
|
963
|
+
accountId: account.accountId
|
|
964
|
+
})
|
|
903
965
|
}
|
|
904
966
|
},
|
|
905
967
|
allowFrom: normalizeStringEntries(account.config.allowFrom),
|
|
@@ -916,9 +978,18 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
916
978
|
accountId: account.accountId,
|
|
917
979
|
log: (message) => logVerbose(message)
|
|
918
980
|
});
|
|
919
|
-
if (
|
|
981
|
+
if (event.type === "join") return groupConfig?.enabled !== false && groupPolicy !== "disabled" && (groupPolicy !== "allowlist" || access.state.allowlists.group.hasMatchableEntries) ? {
|
|
920
982
|
access,
|
|
921
983
|
resolveBoundAccess: resolveAccess
|
|
984
|
+
} : null;
|
|
985
|
+
if (access.senderAccess.decision === "allow" && (access.ingress.admission === "dispatch" || access.ingress.admission === "observe" || access.ingress.admission === "skip")) return {
|
|
986
|
+
access,
|
|
987
|
+
resolveBoundAccess: resolveAccess,
|
|
988
|
+
mentions: mentionFacts ? {
|
|
989
|
+
...mentionFacts,
|
|
990
|
+
wasMentioned: access.activationAccess.effectiveWasMentioned ?? mentionFacts.wasMentioned,
|
|
991
|
+
requireMention
|
|
992
|
+
} : void 0
|
|
922
993
|
};
|
|
923
994
|
if (access.senderAccess.decision === "allow") {
|
|
924
995
|
logVerbose(`Blocked line event (${access.ingress.reasonCode})`);
|
|
@@ -964,21 +1035,13 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
964
1035
|
logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${account.config.dmPolicy ?? "pairing"})`);
|
|
965
1036
|
return null;
|
|
966
1037
|
}
|
|
967
|
-
function
|
|
968
|
-
|
|
969
|
-
const mentionees = message.mention?.mentionees;
|
|
970
|
-
return Array.isArray(mentionees) ? mentionees : [];
|
|
971
|
-
}
|
|
972
|
-
function isLineBotMentioned(message) {
|
|
973
|
-
return getLineMentionees(message).some((m) => m.isSelf === true || m.type === "all");
|
|
974
|
-
}
|
|
975
|
-
function hasAnyLineMention(message) {
|
|
976
|
-
return getLineMentionees(message).length > 0;
|
|
1038
|
+
function resolveLineQuotedMessageId(message) {
|
|
1039
|
+
return message.type === "text" || message.type === "sticker" ? message.quotedMessageId : void 0;
|
|
977
1040
|
}
|
|
978
1041
|
function resolveEventRawText(event) {
|
|
979
1042
|
if (event.type === "message") {
|
|
980
1043
|
const msg = event.message;
|
|
981
|
-
if (msg.type === "text") return msg
|
|
1044
|
+
if (msg.type === "text") return readLineTextMessageBody(msg);
|
|
982
1045
|
return "";
|
|
983
1046
|
}
|
|
984
1047
|
if (event.type === "postback") return event.postback?.data?.trim() ?? "";
|
|
@@ -987,24 +1050,34 @@ function resolveEventRawText(event) {
|
|
|
987
1050
|
async function handleMessageEvent(event, context) {
|
|
988
1051
|
const { cfg, account, runtime, mediaMaxBytes, processMessage } = context;
|
|
989
1052
|
const message = event.message;
|
|
990
|
-
const decision = await
|
|
1053
|
+
const decision = await resolveLineEventAdmission(event, context);
|
|
991
1054
|
if (!decision) return;
|
|
992
1055
|
const { isGroup, groupId, roomId } = getLineSourceInfo(event.source);
|
|
993
1056
|
if (isGroup && decision.access.activationAccess.shouldSkip) {
|
|
994
|
-
const rawText = message.type === "text" ? message
|
|
1057
|
+
const rawText = message.type === "text" ? readLineTextMessageBody(message) : "";
|
|
995
1058
|
const sourceInfo = getLineSourceInfo(event.source);
|
|
996
1059
|
logVerbose(`line: skipping group message (requireMention, not mentioned)`);
|
|
997
1060
|
const historyKey = groupId ?? roomId;
|
|
998
1061
|
const senderId = sourceInfo.userId ?? "unknown";
|
|
999
|
-
if (historyKey && context.groupHistories)
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1062
|
+
if (historyKey && context.groupHistories) {
|
|
1063
|
+
const displayName = sourceInfo.userId ? await getUserDisplayName(sourceInfo.userId, {
|
|
1064
|
+
cfg,
|
|
1065
|
+
accountId: account.accountId,
|
|
1066
|
+
channelAccessToken: account.channelAccessToken,
|
|
1067
|
+
groupId,
|
|
1068
|
+
roomId
|
|
1069
|
+
}) : senderId;
|
|
1070
|
+
const sender = displayName === senderId ? senderId : `${displayName} (${senderId})`;
|
|
1071
|
+
createChannelHistoryWindow({ historyMap: context.groupHistories }).record({
|
|
1072
|
+
historyKey,
|
|
1073
|
+
limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
|
|
1074
|
+
entry: {
|
|
1075
|
+
sender,
|
|
1076
|
+
body: rawText || `<${message.type}>`,
|
|
1077
|
+
timestamp: event.timestamp
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1008
1081
|
return;
|
|
1009
1082
|
}
|
|
1010
1083
|
const groupHistoryKey = isGroup ? groupId ?? roomId : void 0;
|
|
@@ -1023,7 +1096,8 @@ async function handleMessageEvent(event, context) {
|
|
|
1023
1096
|
abortSignal?.throwIfAborted();
|
|
1024
1097
|
allMedia.push({
|
|
1025
1098
|
path: media.path,
|
|
1026
|
-
contentType: media.contentType
|
|
1099
|
+
contentType: media.contentType,
|
|
1100
|
+
...originalFilename ? { fileName: originalFilename } : {}
|
|
1027
1101
|
});
|
|
1028
1102
|
} catch (err) {
|
|
1029
1103
|
if (abortSignal?.aborted) throw abortSignal.reason;
|
|
@@ -1043,13 +1117,17 @@ async function handleMessageEvent(event, context) {
|
|
|
1043
1117
|
commandAuthorized: decision.access.commandAccess.authorized,
|
|
1044
1118
|
resolveChannelIngress: decision.resolveBoundAccess,
|
|
1045
1119
|
inboundHistory: historyReservation.inboundHistory,
|
|
1120
|
+
mentions: decision.mentions,
|
|
1046
1121
|
buildContext: context.buildContext
|
|
1047
1122
|
});
|
|
1048
1123
|
if (!messageContext) {
|
|
1049
1124
|
logVerbose("line: skipping empty message");
|
|
1050
1125
|
return;
|
|
1051
1126
|
}
|
|
1052
|
-
await processMessage(messageContext,
|
|
1127
|
+
await processMessage(messageContext, {
|
|
1128
|
+
cfg,
|
|
1129
|
+
...context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {}
|
|
1130
|
+
});
|
|
1053
1131
|
historyReservation.commit();
|
|
1054
1132
|
} finally {
|
|
1055
1133
|
historyReservation.release();
|
|
@@ -1063,9 +1141,42 @@ async function handleUnfollowEvent(event, _context) {
|
|
|
1063
1141
|
const { userId } = getLineSourceInfo(event.source);
|
|
1064
1142
|
logVerbose(`line: user ${userId ?? "unknown"} unfollowed`);
|
|
1065
1143
|
}
|
|
1066
|
-
async function handleJoinEvent(event,
|
|
1067
|
-
const { groupId, roomId } = getLineSourceInfo(event.source);
|
|
1144
|
+
async function handleJoinEvent(event, context) {
|
|
1145
|
+
const { groupId, roomId, isGroup } = getLineSourceInfo(event.source);
|
|
1146
|
+
const conversationId = groupId ?? roomId;
|
|
1147
|
+
if (!isGroup || !conversationId) return;
|
|
1068
1148
|
logVerbose(`line: bot joined ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
|
|
1149
|
+
const { cfg, account } = context;
|
|
1150
|
+
const roomAllowed = Boolean(await resolveLineEventAdmission(event, context));
|
|
1151
|
+
await reportChannelRoomJoin({
|
|
1152
|
+
cfg,
|
|
1153
|
+
channel: "line",
|
|
1154
|
+
accountId: account.accountId,
|
|
1155
|
+
conversationId,
|
|
1156
|
+
deliverTo: conversationId,
|
|
1157
|
+
route: resolveAgentRoute({
|
|
1158
|
+
cfg,
|
|
1159
|
+
channel: "line",
|
|
1160
|
+
accountId: account.accountId,
|
|
1161
|
+
peer: {
|
|
1162
|
+
kind: "group",
|
|
1163
|
+
id: conversationId
|
|
1164
|
+
}
|
|
1165
|
+
}),
|
|
1166
|
+
roomAllowed,
|
|
1167
|
+
resolveRoomContext: async () => {
|
|
1168
|
+
const roomContext = { historyUnavailable: true };
|
|
1169
|
+
const title = groupId ? await getLineGroupName(groupId, {
|
|
1170
|
+
cfg,
|
|
1171
|
+
accountId: account.accountId,
|
|
1172
|
+
channelAccessToken: account.channelAccessToken
|
|
1173
|
+
}) : void 0;
|
|
1174
|
+
return title ? {
|
|
1175
|
+
...roomContext,
|
|
1176
|
+
title
|
|
1177
|
+
} : roomContext;
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1069
1180
|
}
|
|
1070
1181
|
async function handleLeaveEvent(event, _context) {
|
|
1071
1182
|
const { groupId, roomId } = getLineSourceInfo(event.source);
|
|
@@ -1074,7 +1185,7 @@ async function handleLeaveEvent(event, _context) {
|
|
|
1074
1185
|
async function handlePostbackEvent(event, context) {
|
|
1075
1186
|
const data = event.postback.data;
|
|
1076
1187
|
logVerbose(`line: received postback: ${data}`);
|
|
1077
|
-
const decision = await
|
|
1188
|
+
const decision = await resolveLineEventAdmission(event, context);
|
|
1078
1189
|
if (!decision) return;
|
|
1079
1190
|
const postbackContext = await buildLinePostbackContext({
|
|
1080
1191
|
event,
|
|
@@ -1085,7 +1196,10 @@ async function handlePostbackEvent(event, context) {
|
|
|
1085
1196
|
buildContext: context.buildContext
|
|
1086
1197
|
});
|
|
1087
1198
|
if (!postbackContext) return;
|
|
1088
|
-
await context.processMessage(postbackContext,
|
|
1199
|
+
await context.processMessage(postbackContext, {
|
|
1200
|
+
cfg: context.cfg,
|
|
1201
|
+
...context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {}
|
|
1202
|
+
});
|
|
1089
1203
|
}
|
|
1090
1204
|
async function handleLineWebhookEvents(events, context) {
|
|
1091
1205
|
let firstError;
|
|
@@ -1122,12 +1236,10 @@ async function handleLineWebhookEvent(event, context) {
|
|
|
1122
1236
|
}
|
|
1123
1237
|
//#endregion
|
|
1124
1238
|
//#region extensions/line/src/webhook-spool.ts
|
|
1125
|
-
const LINE_WEBHOOK_SPOOL_VERSION = 1;
|
|
1126
1239
|
const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
|
|
1127
1240
|
const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
|
|
1128
1241
|
const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
|
|
1129
1242
|
const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
|
|
1130
|
-
const LineWebhookPayloadError = createChannelIngressError("LineWebhookPayloadError");
|
|
1131
1243
|
var LineWebhookTerminalDeliveryError = class extends Error {
|
|
1132
1244
|
constructor(message, options) {
|
|
1133
1245
|
super(message, options);
|
|
@@ -1135,35 +1247,6 @@ var LineWebhookTerminalDeliveryError = class extends Error {
|
|
|
1135
1247
|
this.name = "LineWebhookTerminalDeliveryError";
|
|
1136
1248
|
}
|
|
1137
1249
|
};
|
|
1138
|
-
/** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
|
|
1139
|
-
function eventIdFor(event) {
|
|
1140
|
-
if (!event || typeof event !== "object") throw new LineWebhookPayloadError("LINE webhook event must be an object.");
|
|
1141
|
-
const candidate = event;
|
|
1142
|
-
if (candidate.type === "message") {
|
|
1143
|
-
const messageId = normalizeNullableString(candidate.message?.id);
|
|
1144
|
-
if (messageId) return `message:${messageId}`;
|
|
1145
|
-
}
|
|
1146
|
-
const webhookEventId = normalizeNullableString(candidate.webhookEventId);
|
|
1147
|
-
if (webhookEventId) return `event:${webhookEventId}`;
|
|
1148
|
-
throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
|
|
1149
|
-
}
|
|
1150
|
-
function laneKeyFor(event, eventId) {
|
|
1151
|
-
if (!event || typeof event !== "object") return eventId;
|
|
1152
|
-
const source = event.source;
|
|
1153
|
-
if (source?.type === "group") {
|
|
1154
|
-
const groupId = normalizeNullableString(source.groupId);
|
|
1155
|
-
if (groupId) return `group:${groupId}`;
|
|
1156
|
-
}
|
|
1157
|
-
if (source?.type === "room") {
|
|
1158
|
-
const roomId = normalizeNullableString(source.roomId);
|
|
1159
|
-
if (roomId) return `room:${roomId}`;
|
|
1160
|
-
}
|
|
1161
|
-
if (source?.type === "user") {
|
|
1162
|
-
const userId = normalizeNullableString(source.userId);
|
|
1163
|
-
if (userId) return `user:${userId}`;
|
|
1164
|
-
}
|
|
1165
|
-
return eventId;
|
|
1166
|
-
}
|
|
1167
1250
|
function parseStoredEvent(rawEvent) {
|
|
1168
1251
|
let event;
|
|
1169
1252
|
try {
|
|
@@ -1203,7 +1286,7 @@ function createLineWebhookSpool(options) {
|
|
|
1203
1286
|
};
|
|
1204
1287
|
},
|
|
1205
1288
|
payload: {
|
|
1206
|
-
version:
|
|
1289
|
+
version: 1,
|
|
1207
1290
|
serialize: ({ event, destination }) => ({
|
|
1208
1291
|
rawEvent: JSON.stringify(event),
|
|
1209
1292
|
destination
|
|
@@ -1218,7 +1301,7 @@ function createLineWebhookSpool(options) {
|
|
|
1218
1301
|
destination: body.destination
|
|
1219
1302
|
}),
|
|
1220
1303
|
decode: (payload) => {
|
|
1221
|
-
if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError(
|
|
1304
|
+
if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError(LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE);
|
|
1222
1305
|
return {
|
|
1223
1306
|
version: payload.version,
|
|
1224
1307
|
body: {
|
|
@@ -1227,7 +1310,7 @@ function createLineWebhookSpool(options) {
|
|
|
1227
1310
|
}
|
|
1228
1311
|
};
|
|
1229
1312
|
},
|
|
1230
|
-
createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ?
|
|
1313
|
+
createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE : "LINE webhook event identity changed after durable admission.")
|
|
1231
1314
|
},
|
|
1232
1315
|
deliver: async ({ event, destination }, lifecycle) => {
|
|
1233
1316
|
const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
|
|
@@ -1287,7 +1370,7 @@ function createLineWebhookSpool(options) {
|
|
|
1287
1370
|
},
|
|
1288
1371
|
resolveNonRetryableFailure: (error) => {
|
|
1289
1372
|
if (error instanceof LineWebhookPayloadError) return {
|
|
1290
|
-
reason:
|
|
1373
|
+
reason: LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON,
|
|
1291
1374
|
message: error.message
|
|
1292
1375
|
};
|
|
1293
1376
|
if (error instanceof LineWebhookTerminalDeliveryError) return {
|
|
@@ -1308,12 +1391,12 @@ function createLineWebhookSpool(options) {
|
|
|
1308
1391
|
let stopTask;
|
|
1309
1392
|
return {
|
|
1310
1393
|
accept: async (body) => {
|
|
1311
|
-
const events = body.events ?? [];
|
|
1312
|
-
if (events.length === 0) return;
|
|
1313
|
-
await monitor.admitBatch(events.map((event) => ({
|
|
1394
|
+
const events = (body.events ?? []).filter((event) => event.mode !== "standby");
|
|
1395
|
+
if (events.length === 0) return "ignored";
|
|
1396
|
+
return (await monitor.admitBatch(events.map((event) => ({
|
|
1314
1397
|
event,
|
|
1315
1398
|
destination: body.destination ?? ""
|
|
1316
|
-
})), { receivedAt: Date.now() });
|
|
1399
|
+
})), { receivedAt: Date.now() })).some((admission) => admission.kind === "durable") ? "durable" : "ignored";
|
|
1317
1400
|
},
|
|
1318
1401
|
start: () => {
|
|
1319
1402
|
if (!stopTask) monitor.start();
|
|
@@ -1340,9 +1423,17 @@ function createLineWebhookSpool(options) {
|
|
|
1340
1423
|
const DEFAULT_MEDIA_MAX_MB = 10;
|
|
1341
1424
|
function createLineBot(opts) {
|
|
1342
1425
|
const runtime = opts.runtime ?? createNonExitingRuntime();
|
|
1343
|
-
const
|
|
1426
|
+
const startupConfig = opts.config ?? getRuntimeConfig();
|
|
1427
|
+
const startupRuntimeConfig = getRuntimeConfigSnapshot();
|
|
1428
|
+
const startupRuntimeSourceConfig = getRuntimeConfigSourceSnapshot();
|
|
1429
|
+
const followsRuntimeConfig = opts.config === void 0 || startupRuntimeConfig === startupConfig || startupRuntimeSourceConfig !== null && selectApplicableRuntimeConfig({
|
|
1430
|
+
inputConfig: startupConfig,
|
|
1431
|
+
runtimeConfig: startupRuntimeConfig,
|
|
1432
|
+
runtimeSourceConfig: startupRuntimeSourceConfig
|
|
1433
|
+
}) === startupRuntimeConfig;
|
|
1434
|
+
const resolveTurnConfig = () => (followsRuntimeConfig ? getRuntimeConfigSnapshot() : void 0) ?? startupConfig;
|
|
1344
1435
|
const account = resolveLineAccount({
|
|
1345
|
-
cfg,
|
|
1436
|
+
cfg: startupConfig,
|
|
1346
1437
|
accountId: opts.accountId
|
|
1347
1438
|
});
|
|
1348
1439
|
const mediaMaxBytes = ([opts.mediaMaxMb, account.config.mediaMaxMb].find((value) => typeof value === "number" && value > 0) ?? DEFAULT_MEDIA_MAX_MB) * 1024 * 1024;
|
|
@@ -1353,17 +1444,20 @@ function createLineBot(opts) {
|
|
|
1353
1444
|
const spool = createLineWebhookSpool({
|
|
1354
1445
|
accountId: account.accountId,
|
|
1355
1446
|
runtime,
|
|
1356
|
-
deliver: async (event, _destination, control) =>
|
|
1357
|
-
cfg
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1447
|
+
deliver: async (event, _destination, control) => {
|
|
1448
|
+
const cfg = resolveTurnConfig();
|
|
1449
|
+
await handleLineWebhookEvents([event], {
|
|
1450
|
+
cfg,
|
|
1451
|
+
account,
|
|
1452
|
+
runtime,
|
|
1453
|
+
buildContext: opts.buildContext,
|
|
1454
|
+
mediaMaxBytes,
|
|
1455
|
+
processMessage,
|
|
1456
|
+
...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
|
|
1457
|
+
groupHistories,
|
|
1458
|
+
historyLimit: account.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1367
1461
|
});
|
|
1368
1462
|
spool.start();
|
|
1369
1463
|
return {
|
|
@@ -1389,7 +1483,8 @@ function resolveLineDurableReplyOptions(params) {
|
|
|
1389
1483
|
//#endregion
|
|
1390
1484
|
//#region extensions/line/src/signature.ts
|
|
1391
1485
|
function validateLineSignature(body, signature, channelSecret) {
|
|
1392
|
-
|
|
1486
|
+
const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
|
|
1487
|
+
return safeEqualSecret(signature, hash);
|
|
1393
1488
|
}
|
|
1394
1489
|
//#endregion
|
|
1395
1490
|
//#region extensions/line/src/webhook-utils.ts
|
|
@@ -1402,15 +1497,28 @@ function parseLineWebhookBody(rawBody) {
|
|
|
1402
1497
|
}
|
|
1403
1498
|
//#endregion
|
|
1404
1499
|
//#region extensions/line/src/webhook-node.ts
|
|
1405
|
-
const LINE_WEBHOOK_MAX_BODY_BYTES =
|
|
1406
|
-
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 =
|
|
1500
|
+
const LINE_WEBHOOK_MAX_BODY_BYTES = 1048576;
|
|
1501
|
+
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 65536;
|
|
1407
1502
|
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1 = 5e3;
|
|
1408
1503
|
async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1) {
|
|
1409
1504
|
return await readRequestBodyWithLimit(req, {
|
|
1410
1505
|
maxBytes,
|
|
1411
|
-
timeoutMs
|
|
1506
|
+
timeoutMs,
|
|
1507
|
+
destroyOnLimit: false
|
|
1412
1508
|
});
|
|
1413
1509
|
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Answer a body-limit failure through the connection owner.
|
|
1512
|
+
*
|
|
1513
|
+
* The reader defers destruction for these two codes, so the connection is already fenced
|
|
1514
|
+
* and only the owner can still write: responding directly would race the teardown and LINE
|
|
1515
|
+
* would see a reset instead of the status.
|
|
1516
|
+
*/
|
|
1517
|
+
async function rejectLineWebhookRequest(req, res, error) {
|
|
1518
|
+
if (!isRequestBodyLimitError(error, "PAYLOAD_TOO_LARGE") && !isRequestBodyLimitError(error, "REQUEST_BODY_TIMEOUT")) return false;
|
|
1519
|
+
await sendHttpRequestRejection(req, res, error.statusCode, JSON.stringify({ error: requestBodyErrorToText(error.code) }), "application/json");
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1414
1522
|
function createLineNodeWebhookHandler(params) {
|
|
1415
1523
|
const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
|
|
1416
1524
|
const readBody = params.readBody ?? readLineWebhookRequestBody;
|
|
@@ -1467,18 +1575,7 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1467
1575
|
res.setHeader("Content-Type", "application/json");
|
|
1468
1576
|
res.end(JSON.stringify({ status: "ok" }));
|
|
1469
1577
|
} catch (err) {
|
|
1470
|
-
if (
|
|
1471
|
-
res.statusCode = 413;
|
|
1472
|
-
res.setHeader("Content-Type", "application/json");
|
|
1473
|
-
res.end(JSON.stringify({ error: "Payload too large" }));
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
if (isRequestBodyLimitError$1(err, "REQUEST_BODY_TIMEOUT")) {
|
|
1477
|
-
res.statusCode = 408;
|
|
1478
|
-
res.setHeader("Content-Type", "application/json");
|
|
1479
|
-
res.end(JSON.stringify({ error: requestBodyErrorToText$1("REQUEST_BODY_TIMEOUT") }));
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1578
|
+
if (await rejectLineWebhookRequest(req, res, err)) return;
|
|
1482
1579
|
params.runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
|
|
1483
1580
|
if (!res.headersSent) {
|
|
1484
1581
|
res.statusCode = 500;
|
|
@@ -1491,7 +1588,7 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1491
1588
|
//#endregion
|
|
1492
1589
|
//#region extensions/line/src/monitor.ts
|
|
1493
1590
|
const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
|
|
1494
|
-
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES =
|
|
1591
|
+
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
|
|
1495
1592
|
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
|
|
1496
1593
|
async function registerLineWebhookTarget(params, bot) {
|
|
1497
1594
|
try {
|
|
@@ -1539,20 +1636,22 @@ async function monitorLineProvider(opts) {
|
|
|
1539
1636
|
onMessage: async (ctx, deliveryControl) => {
|
|
1540
1637
|
if (!ctx) return;
|
|
1541
1638
|
const { ctxPayload, replyToken, route } = ctx;
|
|
1542
|
-
const
|
|
1543
|
-
const
|
|
1544
|
-
cfg:
|
|
1545
|
-
accountId: ctx.accountId
|
|
1546
|
-
}) : Promise.resolve(ctxPayload.From);
|
|
1547
|
-
const stopLoading = shouldShowLoading ? startLineLoadingKeepalive({
|
|
1548
|
-
cfg: config,
|
|
1639
|
+
const turnConfig = deliveryControl.cfg;
|
|
1640
|
+
const stopLoading = Boolean(ctx.userId && !ctx.isGroup) ? startLineLoadingKeepalive({
|
|
1641
|
+
cfg: turnConfig,
|
|
1549
1642
|
userId: ctx.userId,
|
|
1550
1643
|
accountId: ctx.accountId
|
|
1551
1644
|
}) : null;
|
|
1552
|
-
logVerbose(`line: received message from ${
|
|
1645
|
+
logVerbose(`line: received message from ${ctxPayload.SenderName ?? ctx.userId ?? ctxPayload.From} (${ctxPayload.From})`);
|
|
1553
1646
|
let replyTokenUsed = false;
|
|
1554
1647
|
let turnAdopted = false;
|
|
1555
1648
|
const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
|
|
1649
|
+
const turnAbortSignal = ingressLifecycle?.abortSignal;
|
|
1650
|
+
const skillFilter = ctx.skillFilter;
|
|
1651
|
+
const replyOptions = turnAbortSignal || skillFilter ? {
|
|
1652
|
+
...turnAbortSignal ? { abortSignal: turnAbortSignal } : {},
|
|
1653
|
+
...skillFilter ? { skillFilter } : {}
|
|
1654
|
+
} : void 0;
|
|
1556
1655
|
try {
|
|
1557
1656
|
const textLimit = 5e3;
|
|
1558
1657
|
const turnResult = await getLineRuntime().channel.inbound.run({
|
|
@@ -1573,7 +1672,7 @@ async function monitorLineProvider(opts) {
|
|
|
1573
1672
|
rawText: ctxPayload.RawBody ?? ctxPayload.BodyForAgent ?? ""
|
|
1574
1673
|
}),
|
|
1575
1674
|
resolveTurn: () => ({
|
|
1576
|
-
cfg:
|
|
1675
|
+
cfg: turnConfig,
|
|
1577
1676
|
channel: "line",
|
|
1578
1677
|
accountId: route.accountId,
|
|
1579
1678
|
route: {
|
|
@@ -1583,8 +1682,10 @@ async function monitorLineProvider(opts) {
|
|
|
1583
1682
|
ctxPayload,
|
|
1584
1683
|
record: ctx.turn.record,
|
|
1585
1684
|
replyPipeline: {},
|
|
1586
|
-
|
|
1685
|
+
dispatcherOptions: { humanDelay: resolveHumanDelayConfig(turnConfig, route.agentId) },
|
|
1686
|
+
...replyOptions ? { replyOptions } : {},
|
|
1587
1687
|
delivery: {
|
|
1688
|
+
preparePayload: prepareLineReplyPayload,
|
|
1588
1689
|
durable: (payload, info) => resolveLineDurableReplyOptions({
|
|
1589
1690
|
payload,
|
|
1590
1691
|
infoKind: info.kind,
|
|
@@ -1595,7 +1696,7 @@ async function monitorLineProvider(opts) {
|
|
|
1595
1696
|
deliver: async (payload) => {
|
|
1596
1697
|
const lineData = payload.channelData?.line ?? {};
|
|
1597
1698
|
if (ctx.userId && !ctx.isGroup) showLoadingAnimation(ctx.userId, {
|
|
1598
|
-
cfg:
|
|
1699
|
+
cfg: turnConfig,
|
|
1599
1700
|
accountId: ctx.accountId
|
|
1600
1701
|
}).catch(() => {});
|
|
1601
1702
|
const deliveryResult = await deliverLineAutoReply({
|
|
@@ -1605,14 +1706,13 @@ async function monitorLineProvider(opts) {
|
|
|
1605
1706
|
replyToken,
|
|
1606
1707
|
replyTokenUsed,
|
|
1607
1708
|
accountId: ctx.accountId,
|
|
1608
|
-
cfg:
|
|
1709
|
+
cfg: turnConfig,
|
|
1609
1710
|
textLimit,
|
|
1610
1711
|
deps: {
|
|
1611
1712
|
buildTemplateMessageFromPayload,
|
|
1612
1713
|
processLineMessage,
|
|
1613
1714
|
chunkMarkdownText,
|
|
1614
1715
|
replyMessageLine,
|
|
1615
|
-
createQuickReplyItems,
|
|
1616
1716
|
pushMessagesLine,
|
|
1617
1717
|
createFlexMessage,
|
|
1618
1718
|
buildMediaMessage: buildLineMediaMessage,
|
|
@@ -1633,7 +1733,8 @@ async function monitorLineProvider(opts) {
|
|
|
1633
1733
|
})
|
|
1634
1734
|
}
|
|
1635
1735
|
});
|
|
1636
|
-
|
|
1736
|
+
const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
|
|
1737
|
+
if (!hasFinalInboundReplyDispatch(dispatchResult)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
|
|
1637
1738
|
} catch (err) {
|
|
1638
1739
|
runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
|
|
1639
1740
|
if (turnAdopted || replyTokenUsed) throw new LineWebhookTerminalDeliveryError("LINE delivery failed after consuming the event reply token.", { cause: err });
|
|
@@ -1720,25 +1821,13 @@ async function monitorLineProvider(opts) {
|
|
|
1720
1821
|
}
|
|
1721
1822
|
if (body.events && body.events.length > 0) {
|
|
1722
1823
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1723
|
-
await match.target.bot.handleWebhook(body);
|
|
1724
|
-
res.setHeader("x-openclaw-delivery-accepted", "durable");
|
|
1824
|
+
if (await match.target.bot.handleWebhook(body) === "durable") res.setHeader("x-openclaw-delivery-accepted", "durable");
|
|
1725
1825
|
}
|
|
1726
1826
|
res.statusCode = 200;
|
|
1727
1827
|
res.setHeader("Content-Type", "application/json");
|
|
1728
1828
|
res.end(JSON.stringify({ status: "ok" }));
|
|
1729
1829
|
} catch (err) {
|
|
1730
|
-
if (
|
|
1731
|
-
res.statusCode = 413;
|
|
1732
|
-
res.setHeader("Content-Type", "application/json");
|
|
1733
|
-
res.end(JSON.stringify({ error: "Payload too large" }));
|
|
1734
|
-
return;
|
|
1735
|
-
}
|
|
1736
|
-
if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
|
|
1737
|
-
res.statusCode = 408;
|
|
1738
|
-
res.setHeader("Content-Type", "application/json");
|
|
1739
|
-
res.end(JSON.stringify({ error: requestBodyErrorToText("REQUEST_BODY_TIMEOUT") }));
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1830
|
+
if (await rejectLineWebhookRequest(req, res, err)) return;
|
|
1742
1831
|
runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
|
|
1743
1832
|
if (!res.headersSent) {
|
|
1744
1833
|
res.statusCode = 500;
|