@openclaw/line 2026.9.1-beta.1 → 2026.9.2
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-BJvELOIg.js} +111 -380
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel.runtime-CZOszXXj.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-m9h5osFz.js} +339 -237
- package/dist/monitor.runtime-C90LL-P3.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/build/format/json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/json_pointer.mjs +2 -2
- package/node_modules/typebox/build/format/json_pointer_uri_fragment.d.mts +2 -2
- package/node_modules/typebox/build/format/json_pointer_uri_fragment.mjs +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
- package/node_modules/typebox/build/system/memory/assign.mjs +2 -1
- package/node_modules/typebox/build/system/memory/create.mjs +3 -4
- package/node_modules/typebox/build/system/memory/discard.mjs +2 -2
- package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
- package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
- package/node_modules/typebox/build/system/memory/update.mjs +3 -2
- package/node_modules/typebox/package.json +1 -1
- package/openclaw.plugin.json +16 -3
- 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,37 @@
|
|
|
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 {
|
|
9
|
-
import {
|
|
8
|
+
import { resolveChannelGroupsConfigPath } from "openclaw/plugin-sdk/channel-policy";
|
|
9
|
+
import { normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
10
|
+
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
|
|
11
|
+
import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, implicitMentionKindWhen, isChannelPartialDeliveryError, logInboundDrop, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
12
|
+
import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, readErrorName, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
|
|
10
13
|
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
11
14
|
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
12
15
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
16
|
+
import { classifyTransientNetworkErrorCode } from "openclaw/plugin-sdk/retry-runtime";
|
|
17
|
+
import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
|
|
13
18
|
import { firstDefined } from "openclaw/plugin-sdk/allow-from";
|
|
14
19
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
15
20
|
import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
|
|
16
21
|
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
17
22
|
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
23
|
import crypto from "node:crypto";
|
|
22
24
|
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
|
23
|
-
import {
|
|
25
|
+
import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
|
|
24
26
|
import { channelReadyPatch, channelStoppedPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
25
27
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
26
|
-
import { canonicalizeWebhookRouteKey,
|
|
27
|
-
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError
|
|
28
|
+
import { canonicalizeWebhookRouteKey, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
|
|
29
|
+
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, runDetachedWebhookWork, sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
28
30
|
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
29
31
|
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";
|
|
32
|
+
import { getRuntimeConfig, getRuntimeConfigSnapshot, getRuntimeConfigSourceSnapshot, selectApplicableRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
33
|
+
import { resolveChannelImplicitMentions, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
34
|
+
import { reportChannelRoomJoin } from "openclaw/plugin-sdk/channel-join-intro-runtime";
|
|
32
35
|
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
|
|
33
36
|
import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
34
37
|
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
@@ -110,7 +113,7 @@ async function fetchLineContentWhenReady(messageId, channelAccessToken, signal)
|
|
|
110
113
|
}
|
|
111
114
|
await response.body?.cancel().catch(() => void 0);
|
|
112
115
|
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 <
|
|
116
|
+
if (attempt < 5) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal });
|
|
114
117
|
}
|
|
115
118
|
} catch (err) {
|
|
116
119
|
if (signal.aborted) throw signal.reason;
|
|
@@ -125,7 +128,7 @@ function isRetryableLineInboundMediaError(err) {
|
|
|
125
128
|
if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
|
|
126
129
|
return false;
|
|
127
130
|
}
|
|
128
|
-
async function downloadLineMedia(messageId, channelAccessToken, maxBytes =
|
|
131
|
+
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10485760, options) {
|
|
129
132
|
options?.signal?.throwIfAborted();
|
|
130
133
|
const deadlineAbort = new AbortController();
|
|
131
134
|
const signal = options?.signal ? AbortSignal.any([options.signal, deadlineAbort.signal]) : deadlineAbort.signal;
|
|
@@ -159,11 +162,8 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
|
|
|
159
162
|
function toLineDeliveryError(error) {
|
|
160
163
|
return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
|
|
161
164
|
}
|
|
162
|
-
function getLineHttpError(error) {
|
|
163
|
-
return collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]).find((candidate) => candidate instanceof HTTPFetchError);
|
|
164
|
-
}
|
|
165
165
|
function canFallbackAfterLineReplyFailure(error) {
|
|
166
|
-
const httpError =
|
|
166
|
+
const httpError = findLineHttpError(error);
|
|
167
167
|
if (httpError) return httpError.status >= 400 && httpError.status < 500 && httpError.status !== 408;
|
|
168
168
|
const candidates = collectErrorGraphCandidates(error, (candidate) => [candidate.cause, candidate.error]);
|
|
169
169
|
if (candidates.some((candidate) => readErrorName(candidate) === "AbortError" || classifyTransientNetworkErrorCode(extractErrorCode(candidate)) === "ambiguous")) return false;
|
|
@@ -235,7 +235,7 @@ async function deliverLineAutoReply(params) {
|
|
|
235
235
|
} catch (err) {
|
|
236
236
|
if (isChannelPartialDeliveryError(err) || !canFallbackAfterLineReplyFailure(err)) throw err;
|
|
237
237
|
deps.onReplyError?.(err);
|
|
238
|
-
await pushLineMessages(replyBatch,
|
|
238
|
+
await pushLineMessages(replyBatch, findLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
|
|
239
239
|
} finally {
|
|
240
240
|
replyTokenUsed = true;
|
|
241
241
|
}
|
|
@@ -244,7 +244,15 @@ async function deliverLineAutoReply(params) {
|
|
|
244
244
|
if (remaining.length > 0) await pushLineMessages(remaining, true);
|
|
245
245
|
};
|
|
246
246
|
const richMessages = [];
|
|
247
|
-
const
|
|
247
|
+
const quickReplyItems = lineData.quickReplyItems?.length ? lineData.quickReplyItems : (lineData.quickReplies ?? []).map((label) => ({
|
|
248
|
+
label,
|
|
249
|
+
action: {
|
|
250
|
+
type: "command",
|
|
251
|
+
command: label
|
|
252
|
+
}
|
|
253
|
+
}));
|
|
254
|
+
const quickReplyLabels = quickReplyItems.map((item) => item.label);
|
|
255
|
+
const hasQuickReplies = quickReplyItems.length > 0;
|
|
248
256
|
if (lineData.flexMessage) richMessages.push(deps.createFlexMessage(lineData.flexMessage.altText, lineData.flexMessage.contents));
|
|
249
257
|
if (lineData.templateMessage) {
|
|
250
258
|
const templateMsg = deps.buildTemplateMessageFromPayload(lineData.templateMessage);
|
|
@@ -264,7 +272,7 @@ async function deliverLineAutoReply(params) {
|
|
|
264
272
|
const chunks = orderedMessages ? orderedMessages.flatMap((message) => message.type === "text" ? [message.text] : []) : processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : [];
|
|
265
273
|
const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
|
|
266
274
|
const mediaOpts = {
|
|
267
|
-
mediaKind:
|
|
275
|
+
mediaKind: lineData.mediaKind,
|
|
268
276
|
previewImageUrl: lineData.previewImageUrl,
|
|
269
277
|
durationMs: lineData.durationMs,
|
|
270
278
|
trackingId: lineData.trackingId
|
|
@@ -292,14 +300,14 @@ async function deliverLineAutoReply(params) {
|
|
|
292
300
|
];
|
|
293
301
|
if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
|
|
294
302
|
type: "text",
|
|
295
|
-
text: buildLineQuickReplyFallbackText(
|
|
303
|
+
text: buildLineQuickReplyFallbackText(quickReplyLabels)
|
|
296
304
|
});
|
|
297
305
|
if (hasQuickReplies) {
|
|
298
306
|
const targetMessages = orderedDeliveryMessages?.length ? orderedDeliveryMessages : textMessages.length > 0 ? textMessages : richMediaMessages;
|
|
299
307
|
const lastIndex = targetMessages.length - 1;
|
|
300
308
|
targetMessages[lastIndex] = {
|
|
301
309
|
...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
|
|
302
|
-
quickReply:
|
|
310
|
+
quickReply: createLineQuickReply(quickReplyItems)
|
|
303
311
|
};
|
|
304
312
|
}
|
|
305
313
|
const messages = hasQuickReplies ? [...richMediaMessages, ...orderedDeliveryMessages ?? textMessages] : [...orderedDeliveryMessages ?? textMessages, ...richMediaMessages];
|
|
@@ -308,25 +316,33 @@ async function deliverLineAutoReply(params) {
|
|
|
308
316
|
} catch (err) {
|
|
309
317
|
deliveryError ??= err;
|
|
310
318
|
const failedSegment = typeof err === "object" && err !== null ? failedPushSegments.get(err) : void 0;
|
|
311
|
-
const httpError =
|
|
319
|
+
const httpError = findLineHttpError(err);
|
|
312
320
|
const retryCandidates = failedSegment ? [...failedSegment.allowFailedBatchTextRecovery ? failedSegment.failedBatch : [], ...failedSegment.unattemptedTail] : [];
|
|
313
321
|
const retryTextMessages = retryCandidates.filter((message) => message.type === "text");
|
|
314
322
|
const quickRepliesNeedCarrier = hasQuickReplies && retryCandidates.some((message) => "quickReply" in message);
|
|
315
323
|
const retryMessages = retryTextMessages.length > 0 ? retryTextMessages : quickRepliesNeedCarrier ? [{
|
|
316
324
|
type: "text",
|
|
317
|
-
text: buildLineQuickReplyFallbackText(
|
|
318
|
-
quickReply:
|
|
325
|
+
text: buildLineQuickReplyFallbackText(quickReplyLabels),
|
|
326
|
+
quickReply: createLineQuickReply(quickReplyItems)
|
|
319
327
|
}] : [];
|
|
320
|
-
if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400) {
|
|
328
|
+
if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400 && resolveLineNonDispatchRetryable(err) !== void 0) {
|
|
321
329
|
const lastRetryMessage = retryMessages.at(-1);
|
|
322
|
-
if (quickRepliesNeedCarrier && lastRetryMessage && !lastRetryMessage.quickReply) lastRetryMessage.quickReply =
|
|
330
|
+
if (quickRepliesNeedCarrier && lastRetryMessage && !lastRetryMessage.quickReply) lastRetryMessage.quickReply = createLineQuickReply(quickReplyItems);
|
|
323
331
|
try {
|
|
324
332
|
await sendLineMessages(retryMessages, false);
|
|
325
333
|
} catch {}
|
|
326
334
|
}
|
|
327
335
|
}
|
|
328
336
|
if (deliveryError !== void 0) {
|
|
329
|
-
if (!visibleReplySent)
|
|
337
|
+
if (!visibleReplySent) {
|
|
338
|
+
const named = toLineDeliveryError(deliveryError);
|
|
339
|
+
const refusal = await explainLineRefusal({
|
|
340
|
+
error: named,
|
|
341
|
+
cfg: params.cfg,
|
|
342
|
+
accountId
|
|
343
|
+
});
|
|
344
|
+
throw refusal.reason !== named.message ? new Error(refusal.reason, { cause: deliveryError }) : named;
|
|
345
|
+
}
|
|
330
346
|
return {
|
|
331
347
|
status: "partial",
|
|
332
348
|
replyTokenUsed,
|
|
@@ -341,6 +357,44 @@ async function deliverLineAutoReply(params) {
|
|
|
341
357
|
};
|
|
342
358
|
}
|
|
343
359
|
//#endregion
|
|
360
|
+
//#region extensions/line/src/mentions.ts
|
|
361
|
+
function getLineMentionees(message) {
|
|
362
|
+
return message.type === "text" ? message.mention?.mentionees ?? [] : [];
|
|
363
|
+
}
|
|
364
|
+
function addressesLineBot(mentionee) {
|
|
365
|
+
return mentionee.type === "all" || mentionee.isSelf === true;
|
|
366
|
+
}
|
|
367
|
+
function isLineBotMentioned(message) {
|
|
368
|
+
return getLineMentionees(message).some(addressesLineBot);
|
|
369
|
+
}
|
|
370
|
+
function hasAnyLineMention(message) {
|
|
371
|
+
return getLineMentionees(message).length > 0;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Text to interpret as a command, with the mention that addressed the bot removed.
|
|
375
|
+
*
|
|
376
|
+
* LINE writes a mention as the plain channel display name, so no pattern can
|
|
377
|
+
* tell it apart from a member's name; `mentionees[].index`/`length` (UTF-16
|
|
378
|
+
* code units, the unit LINE counts text in) is the only authoritative marker.
|
|
379
|
+
* Leaving it in place makes a group `@<bot> /status` parse as prose, and group
|
|
380
|
+
* chats require the mention to reach the bot at all.
|
|
381
|
+
*/
|
|
382
|
+
function resolveLineMentionStrippedText(message) {
|
|
383
|
+
const text = message.type === "text" ? message.text : "";
|
|
384
|
+
const spans = getLineMentionees(message).filter(addressesLineBot).map((mentionee) => ({
|
|
385
|
+
start: mentionee.index,
|
|
386
|
+
end: mentionee.index + mentionee.length
|
|
387
|
+
})).toSorted((left, right) => left.start - right.start);
|
|
388
|
+
let stripped = "";
|
|
389
|
+
let cursor = 0;
|
|
390
|
+
for (const span of spans) {
|
|
391
|
+
stripped += text.slice(cursor, span.start);
|
|
392
|
+
if (/\S$/u.test(stripped) && /^\S/u.test(text.slice(span.end))) stripped += " ";
|
|
393
|
+
cursor = Math.max(cursor, span.end);
|
|
394
|
+
}
|
|
395
|
+
return `${stripped}${text.slice(cursor)}`.trim();
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
344
398
|
//#region extensions/line/src/bot-message-context.ts
|
|
345
399
|
function getLineSourceInfo(source) {
|
|
346
400
|
if (!source) return {
|
|
@@ -425,30 +479,23 @@ async function resolveLineInboundRoute(params) {
|
|
|
425
479
|
route
|
|
426
480
|
};
|
|
427
481
|
}
|
|
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 "";
|
|
482
|
+
/**
|
|
483
|
+
* Describe a sticker from what its webhook actually carries: LINE sends up to
|
|
484
|
+
* 15 keywords for the sticker, and a message sticker also carries the sender's
|
|
485
|
+
* own text. The package name is not among those facts and cannot be derived
|
|
486
|
+
* from the package id, so it is not part of the description.
|
|
487
|
+
*/
|
|
488
|
+
function describeLineSticker(sticker) {
|
|
489
|
+
const description = readNonEmptyStringPreservingWhitespace(sticker.text) ?? normalizeStringEntries(sticker.keywords ?? []).slice(0, 3).join(", ");
|
|
490
|
+
return description ? `[Sent a sticker: ${description}]` : "[Sent a sticker]";
|
|
491
|
+
}
|
|
492
|
+
function readLineTextMessageBody(message) {
|
|
493
|
+
let text = message.text;
|
|
494
|
+
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)}`;
|
|
495
|
+
return text;
|
|
449
496
|
}
|
|
450
497
|
function extractMessageText(message) {
|
|
451
|
-
if (message.type === "text") return message
|
|
498
|
+
if (message.type === "text") return readLineTextMessageBody(message);
|
|
452
499
|
if (message.type === "location") {
|
|
453
500
|
const loc = message;
|
|
454
501
|
return formatLocationText({
|
|
@@ -458,13 +505,7 @@ function extractMessageText(message) {
|
|
|
458
505
|
address: loc.address
|
|
459
506
|
}) ?? "";
|
|
460
507
|
}
|
|
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
|
-
}
|
|
508
|
+
if (message.type === "sticker") return describeLineSticker(message);
|
|
468
509
|
return "";
|
|
469
510
|
}
|
|
470
511
|
function extractNativeMediaKind(message) {
|
|
@@ -478,9 +519,23 @@ function extractNativeMediaKind(message) {
|
|
|
478
519
|
}
|
|
479
520
|
async function finalizeLineInboundContext(params) {
|
|
480
521
|
const senderId = params.source.userId ?? "unknown";
|
|
481
|
-
const
|
|
482
|
-
|
|
522
|
+
const clientOpts = {
|
|
523
|
+
cfg: params.cfg,
|
|
524
|
+
accountId: params.account.accountId,
|
|
525
|
+
channelAccessToken: params.account.channelAccessToken
|
|
526
|
+
};
|
|
527
|
+
const [senderName, groupName] = await Promise.all([params.source.userId ? getUserProfile(params.source.userId, {
|
|
528
|
+
...clientOpts,
|
|
529
|
+
groupId: params.source.groupId,
|
|
530
|
+
roomId: params.source.roomId
|
|
531
|
+
}).then((profile) => profile?.displayName) : void 0, params.source.groupId ? getLineGroupName(params.source.groupId, clientOpts) : void 0]);
|
|
532
|
+
const senderLabel = senderName ?? (params.source.userId ? `user:${params.source.userId}` : "unknown");
|
|
533
|
+
const conversationLabel = params.source.isGroup ? groupName ?? (params.source.groupId ? `group:${params.source.groupId}` : params.source.roomId ? `room:${params.source.roomId}` : "unknown-group") : senderLabel;
|
|
483
534
|
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}`;
|
|
535
|
+
const groupConfig = params.source.isGroup ? resolveLineGroupConfigEntry(params.account.config.groups, {
|
|
536
|
+
groupId: params.source.groupId,
|
|
537
|
+
roomId: params.source.roomId
|
|
538
|
+
}) : void 0;
|
|
484
539
|
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
|
485
540
|
cfg: params.cfg,
|
|
486
541
|
agentId: params.route.agentId,
|
|
@@ -494,7 +549,10 @@ async function finalizeLineInboundContext(params) {
|
|
|
494
549
|
timestamp: params.timestamp,
|
|
495
550
|
body: agentBody,
|
|
496
551
|
chatType: params.source.isGroup ? "group" : "direct",
|
|
497
|
-
sender: {
|
|
552
|
+
sender: {
|
|
553
|
+
id: senderId,
|
|
554
|
+
name: senderName
|
|
555
|
+
},
|
|
498
556
|
previousTimestamp,
|
|
499
557
|
envelope: envelopeOptions
|
|
500
558
|
});
|
|
@@ -505,7 +563,10 @@ async function finalizeLineInboundContext(params) {
|
|
|
505
563
|
messageId: params.messageSid,
|
|
506
564
|
timestamp: params.timestamp,
|
|
507
565
|
from: address,
|
|
508
|
-
sender: {
|
|
566
|
+
sender: {
|
|
567
|
+
id: senderId,
|
|
568
|
+
name: senderName
|
|
569
|
+
},
|
|
509
570
|
conversation: {
|
|
510
571
|
kind: params.source.isGroup ? "group" : "direct",
|
|
511
572
|
id: params.source.peerId,
|
|
@@ -525,18 +586,18 @@ async function finalizeLineInboundContext(params) {
|
|
|
525
586
|
body,
|
|
526
587
|
bodyForAgent: agentBody,
|
|
527
588
|
rawBody: params.rawBody,
|
|
528
|
-
commandBody: params.rawBody,
|
|
589
|
+
commandBody: params.commandBody ?? params.rawBody,
|
|
529
590
|
inboundHistory: params.inboundHistory
|
|
530
591
|
},
|
|
531
|
-
access: {
|
|
592
|
+
access: {
|
|
593
|
+
commands: { authorized: params.commandAuthorized },
|
|
594
|
+
mentions: params.mentions
|
|
595
|
+
},
|
|
532
596
|
media,
|
|
533
597
|
extra: {
|
|
534
598
|
...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
|
|
599
|
+
GroupSubject: params.source.isGroup ? groupName ?? params.source.groupId ?? params.source.roomId : void 0,
|
|
600
|
+
GroupSystemPrompt: normalizeOptionalString(groupConfig?.systemPrompt)
|
|
540
601
|
}
|
|
541
602
|
});
|
|
542
603
|
const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
|
|
@@ -551,11 +612,13 @@ async function finalizeLineInboundContext(params) {
|
|
|
551
612
|
if (shouldLogVerbose()) {
|
|
552
613
|
const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
|
|
553
614
|
const mediaInfo = params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1 ? ` mediaCount=${params.verboseLog.mediaCount}` : "";
|
|
554
|
-
|
|
615
|
+
const label = params.verboseLog.kind === "inbound" ? "line inbound" : "line postback";
|
|
616
|
+
logVerbose(`${label}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
|
|
555
617
|
}
|
|
556
618
|
return {
|
|
557
619
|
ctxPayload,
|
|
558
620
|
replyToken: params.event.replyToken,
|
|
621
|
+
skillFilter: groupConfig?.skills,
|
|
559
622
|
turn: {
|
|
560
623
|
storePath,
|
|
561
624
|
record: {
|
|
@@ -626,15 +689,17 @@ async function buildLineMessageContext(params) {
|
|
|
626
689
|
},
|
|
627
690
|
rawBody,
|
|
628
691
|
agentBody,
|
|
692
|
+
commandBody: resolveLineMentionStrippedText(message) || rawBody,
|
|
693
|
+
mentions: params.mentions,
|
|
629
694
|
timestamp,
|
|
630
695
|
messageSid: messageId,
|
|
631
696
|
commandAuthorized,
|
|
632
|
-
channelIngress:
|
|
697
|
+
channelIngress: await params.resolveChannelIngress?.({
|
|
633
698
|
agentId: route.agentId,
|
|
634
699
|
sessionKey: route.sessionKey,
|
|
635
700
|
messageId,
|
|
636
701
|
inboundEventKind: "user_request"
|
|
637
|
-
})
|
|
702
|
+
}),
|
|
638
703
|
buildContext: params.buildContext,
|
|
639
704
|
media: mediaFacts,
|
|
640
705
|
locationContext,
|
|
@@ -647,6 +712,7 @@ async function buildLineMessageContext(params) {
|
|
|
647
712
|
return {
|
|
648
713
|
ctxPayload: finalized.ctxPayload,
|
|
649
714
|
turn: finalized.turn,
|
|
715
|
+
skillFilter: finalized.skillFilter,
|
|
650
716
|
event,
|
|
651
717
|
userId,
|
|
652
718
|
groupId,
|
|
@@ -666,14 +732,18 @@ async function buildLinePostbackContext(params) {
|
|
|
666
732
|
account
|
|
667
733
|
});
|
|
668
734
|
const timestamp = event.timestamp;
|
|
669
|
-
const
|
|
670
|
-
if (!
|
|
671
|
-
let
|
|
672
|
-
if (
|
|
673
|
-
const searchParams = new URLSearchParams(
|
|
735
|
+
const rawBody = event.postback?.data?.trim() ?? "";
|
|
736
|
+
if (!rawBody) return null;
|
|
737
|
+
let agentBody = rawBody;
|
|
738
|
+
if (rawBody.includes("line.action=")) {
|
|
739
|
+
const searchParams = new URLSearchParams(rawBody);
|
|
674
740
|
const action = searchParams.get("line.action") ?? "";
|
|
675
741
|
const device = searchParams.get("line.device");
|
|
676
|
-
|
|
742
|
+
agentBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
|
|
743
|
+
}
|
|
744
|
+
for (const [key, value] of Object.entries(event.postback.params ?? {}).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
|
|
745
|
+
const picked = normalizeOptionalString(value);
|
|
746
|
+
if (picked) agentBody += ` ${key}=${picked}`;
|
|
677
747
|
}
|
|
678
748
|
const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
|
|
679
749
|
const finalized = await finalizeLineInboundContext({
|
|
@@ -689,15 +759,16 @@ async function buildLinePostbackContext(params) {
|
|
|
689
759
|
peerId
|
|
690
760
|
},
|
|
691
761
|
rawBody,
|
|
762
|
+
agentBody,
|
|
692
763
|
timestamp,
|
|
693
764
|
messageSid,
|
|
694
765
|
commandAuthorized,
|
|
695
|
-
channelIngress:
|
|
766
|
+
channelIngress: await params.resolveChannelIngress?.({
|
|
696
767
|
agentId: route.agentId,
|
|
697
768
|
sessionKey: route.sessionKey,
|
|
698
769
|
messageId: messageSid,
|
|
699
770
|
inboundEventKind: "user_request"
|
|
700
|
-
})
|
|
771
|
+
}),
|
|
701
772
|
buildContext: params.buildContext,
|
|
702
773
|
media: [],
|
|
703
774
|
verboseLog: { kind: "postback" }
|
|
@@ -705,6 +776,7 @@ async function buildLinePostbackContext(params) {
|
|
|
705
776
|
return {
|
|
706
777
|
ctxPayload: finalized.ctxPayload,
|
|
707
778
|
turn: finalized.turn,
|
|
779
|
+
skillFilter: finalized.skillFilter,
|
|
708
780
|
event,
|
|
709
781
|
userId,
|
|
710
782
|
groupId,
|
|
@@ -764,12 +836,6 @@ function isDownloadableLineMessageType(messageType) {
|
|
|
764
836
|
function normalizeLineIngressEntry(value) {
|
|
765
837
|
return normalizeLineAllowEntry(value) || null;
|
|
766
838
|
}
|
|
767
|
-
function resolveLineGroupConfig(params) {
|
|
768
|
-
return resolveLineGroupConfigEntry(params.config.groups, {
|
|
769
|
-
groupId: params.groupId,
|
|
770
|
-
roomId: params.roomId
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
839
|
async function sendLinePairingReply(params) {
|
|
774
840
|
const { senderId, replyToken, context } = params;
|
|
775
841
|
const idLabel = (() => {
|
|
@@ -821,12 +887,11 @@ async function sendLinePairingReply(params) {
|
|
|
821
887
|
}
|
|
822
888
|
});
|
|
823
889
|
}
|
|
824
|
-
async function
|
|
890
|
+
async function resolveLineEventAdmission(event, context) {
|
|
825
891
|
const { cfg, account } = context;
|
|
826
892
|
const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
|
|
827
893
|
const senderId = userId ?? "";
|
|
828
|
-
const groupConfig =
|
|
829
|
-
config: account.config,
|
|
894
|
+
const groupConfig = resolveLineGroupConfigEntry(account.config.groups, {
|
|
830
895
|
groupId,
|
|
831
896
|
roomId
|
|
832
897
|
});
|
|
@@ -841,11 +906,7 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
841
906
|
const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
|
|
842
907
|
const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom));
|
|
843
908
|
const mentionFacts = (() => {
|
|
844
|
-
if (!isGroup || event.type !== "message") return
|
|
845
|
-
canDetectMention: false,
|
|
846
|
-
wasMentioned: false,
|
|
847
|
-
hasAnyMention: false
|
|
848
|
-
};
|
|
909
|
+
if (!isGroup || event.type !== "message") return;
|
|
849
910
|
const peerId = groupId ?? roomId ?? userId ?? "unknown";
|
|
850
911
|
const { agentId } = resolveAgentRoute({
|
|
851
912
|
cfg,
|
|
@@ -862,7 +923,9 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
862
923
|
return {
|
|
863
924
|
canDetectMention: event.message.type === "text",
|
|
864
925
|
wasMentioned: wasMentionedByNative || wasMentionedByPattern,
|
|
865
|
-
|
|
926
|
+
explicitlyMentionedBot: wasMentionedByNative,
|
|
927
|
+
hasAnyMention: hasAnyLineMention(event.message),
|
|
928
|
+
implicitMentionKinds: implicitMentionKindWhen("quoted_bot", quotesLineBotMessage(account.accountId, resolveLineQuotedMessageId(event.message)))
|
|
866
929
|
};
|
|
867
930
|
})();
|
|
868
931
|
const resolveAccess = async (contextBinding) => await resolveStableChannelMessageIngress({
|
|
@@ -876,7 +939,7 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
876
939
|
},
|
|
877
940
|
cfg,
|
|
878
941
|
readStoreAllowFrom: async () => await readChannelAllowFromStore("line", void 0, account.accountId),
|
|
879
|
-
subject: { stableId: senderId },
|
|
942
|
+
subject: event.type === "join" ? {} : { stableId: senderId },
|
|
880
943
|
conversation: {
|
|
881
944
|
kind: isGroup ? "group" : "direct",
|
|
882
945
|
id: (groupId ?? roomId ?? senderId) || "unknown"
|
|
@@ -886,20 +949,20 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
886
949
|
id: "line:group-config",
|
|
887
950
|
enabled: false
|
|
888
951
|
} } : {},
|
|
889
|
-
mentionFacts
|
|
890
|
-
|
|
891
|
-
wasMentioned: mentionFacts.wasMentioned,
|
|
892
|
-
hasAnyMention: mentionFacts.hasAnyMention,
|
|
893
|
-
implicitMentionKinds: []
|
|
894
|
-
} : void 0,
|
|
895
|
-
event: { kind: event.type === "postback" ? "postback" : "message" },
|
|
952
|
+
mentionFacts,
|
|
953
|
+
event: { kind: event.type === "join" ? "system" : event.type },
|
|
896
954
|
dmPolicy,
|
|
897
955
|
groupPolicy,
|
|
898
956
|
policy: {
|
|
899
957
|
groupAllowFromFallbackToAllowFrom: false,
|
|
900
958
|
activation: {
|
|
901
959
|
requireMention: isGroup && event.type === "message" && requireMention,
|
|
902
|
-
allowTextCommands: true
|
|
960
|
+
allowTextCommands: true,
|
|
961
|
+
implicitMentions: resolveChannelImplicitMentions({
|
|
962
|
+
cfg,
|
|
963
|
+
channel: "line",
|
|
964
|
+
accountId: account.accountId
|
|
965
|
+
})
|
|
903
966
|
}
|
|
904
967
|
},
|
|
905
968
|
allowFrom: normalizeStringEntries(account.config.allowFrom),
|
|
@@ -916,9 +979,18 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
916
979
|
accountId: account.accountId,
|
|
917
980
|
log: (message) => logVerbose(message)
|
|
918
981
|
});
|
|
919
|
-
if (
|
|
982
|
+
if (event.type === "join") return groupConfig?.enabled !== false && groupPolicy !== "disabled" && (groupPolicy !== "allowlist" || access.state.allowlists.group.hasMatchableEntries) ? {
|
|
920
983
|
access,
|
|
921
984
|
resolveBoundAccess: resolveAccess
|
|
985
|
+
} : null;
|
|
986
|
+
if (access.senderAccess.decision === "allow" && (access.ingress.admission === "dispatch" || access.ingress.admission === "observe" || access.ingress.admission === "skip")) return {
|
|
987
|
+
access,
|
|
988
|
+
resolveBoundAccess: resolveAccess,
|
|
989
|
+
mentions: mentionFacts ? {
|
|
990
|
+
...mentionFacts,
|
|
991
|
+
wasMentioned: access.activationAccess.effectiveWasMentioned ?? mentionFacts.wasMentioned,
|
|
992
|
+
requireMention
|
|
993
|
+
} : void 0
|
|
922
994
|
};
|
|
923
995
|
if (access.senderAccess.decision === "allow") {
|
|
924
996
|
logVerbose(`Blocked line event (${access.ingress.reasonCode})`);
|
|
@@ -964,21 +1036,13 @@ async function shouldProcessLineEvent(event, context) {
|
|
|
964
1036
|
logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${account.config.dmPolicy ?? "pairing"})`);
|
|
965
1037
|
return null;
|
|
966
1038
|
}
|
|
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;
|
|
1039
|
+
function resolveLineQuotedMessageId(message) {
|
|
1040
|
+
return message.type === "text" || message.type === "sticker" ? message.quotedMessageId : void 0;
|
|
977
1041
|
}
|
|
978
1042
|
function resolveEventRawText(event) {
|
|
979
1043
|
if (event.type === "message") {
|
|
980
1044
|
const msg = event.message;
|
|
981
|
-
if (msg.type === "text") return msg
|
|
1045
|
+
if (msg.type === "text") return readLineTextMessageBody(msg);
|
|
982
1046
|
return "";
|
|
983
1047
|
}
|
|
984
1048
|
if (event.type === "postback") return event.postback?.data?.trim() ?? "";
|
|
@@ -987,24 +1051,46 @@ function resolveEventRawText(event) {
|
|
|
987
1051
|
async function handleMessageEvent(event, context) {
|
|
988
1052
|
const { cfg, account, runtime, mediaMaxBytes, processMessage } = context;
|
|
989
1053
|
const message = event.message;
|
|
990
|
-
const decision = await
|
|
1054
|
+
const decision = await resolveLineEventAdmission(event, context);
|
|
991
1055
|
if (!decision) return;
|
|
992
|
-
const { isGroup, groupId, roomId } = getLineSourceInfo(event.source);
|
|
1056
|
+
const { isGroup, groupId, roomId, userId } = getLineSourceInfo(event.source);
|
|
993
1057
|
if (isGroup && decision.access.activationAccess.shouldSkip) {
|
|
994
|
-
const rawText = message.type === "text" ? message
|
|
995
|
-
const sourceInfo = getLineSourceInfo(event.source);
|
|
996
|
-
logVerbose(`line: skipping group message (requireMention, not mentioned)`);
|
|
1058
|
+
const rawText = message.type === "text" ? readLineTextMessageBody(message) : "";
|
|
997
1059
|
const historyKey = groupId ?? roomId;
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1060
|
+
const groupsConfigPath = resolveChannelGroupsConfigPath({
|
|
1061
|
+
cfg,
|
|
1062
|
+
channel: "line",
|
|
1063
|
+
accountId: account.accountId,
|
|
1064
|
+
groups: account.config.groups
|
|
1065
|
+
});
|
|
1066
|
+
logInboundDrop({
|
|
1067
|
+
log: runtime.log,
|
|
1068
|
+
channel: "line",
|
|
1069
|
+
reason: "no mention",
|
|
1070
|
+
target: historyKey,
|
|
1071
|
+
onceKey: JSON.stringify([account.accountId, historyKey]),
|
|
1072
|
+
hint: `Mention patterns can be derived from the agent identity name. Set ${groupsConfigPath}[${JSON.stringify(historyKey)}].requireMention=false to process messages without a mention. Preserve existing groups entries; when adding the first groups map, include "*": {} to keep other chats admitted.`
|
|
1007
1073
|
});
|
|
1074
|
+
const senderId = userId ?? "unknown";
|
|
1075
|
+
if (historyKey && context.groupHistories) {
|
|
1076
|
+
const displayName = userId ? await getUserDisplayName(userId, {
|
|
1077
|
+
cfg,
|
|
1078
|
+
accountId: account.accountId,
|
|
1079
|
+
channelAccessToken: account.channelAccessToken,
|
|
1080
|
+
groupId,
|
|
1081
|
+
roomId
|
|
1082
|
+
}) : senderId;
|
|
1083
|
+
const sender = displayName === senderId ? senderId : `${displayName} (${senderId})`;
|
|
1084
|
+
createChannelHistoryWindow({ historyMap: context.groupHistories }).record({
|
|
1085
|
+
historyKey,
|
|
1086
|
+
limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
|
|
1087
|
+
entry: {
|
|
1088
|
+
sender,
|
|
1089
|
+
body: rawText || `<${message.type}>`,
|
|
1090
|
+
timestamp: event.timestamp
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1008
1094
|
return;
|
|
1009
1095
|
}
|
|
1010
1096
|
const groupHistoryKey = isGroup ? groupId ?? roomId : void 0;
|
|
@@ -1023,7 +1109,8 @@ async function handleMessageEvent(event, context) {
|
|
|
1023
1109
|
abortSignal?.throwIfAborted();
|
|
1024
1110
|
allMedia.push({
|
|
1025
1111
|
path: media.path,
|
|
1026
|
-
contentType: media.contentType
|
|
1112
|
+
contentType: media.contentType,
|
|
1113
|
+
...originalFilename ? { fileName: originalFilename } : {}
|
|
1027
1114
|
});
|
|
1028
1115
|
} catch (err) {
|
|
1029
1116
|
if (abortSignal?.aborted) throw abortSignal.reason;
|
|
@@ -1043,13 +1130,17 @@ async function handleMessageEvent(event, context) {
|
|
|
1043
1130
|
commandAuthorized: decision.access.commandAccess.authorized,
|
|
1044
1131
|
resolveChannelIngress: decision.resolveBoundAccess,
|
|
1045
1132
|
inboundHistory: historyReservation.inboundHistory,
|
|
1133
|
+
mentions: decision.mentions,
|
|
1046
1134
|
buildContext: context.buildContext
|
|
1047
1135
|
});
|
|
1048
1136
|
if (!messageContext) {
|
|
1049
1137
|
logVerbose("line: skipping empty message");
|
|
1050
1138
|
return;
|
|
1051
1139
|
}
|
|
1052
|
-
await processMessage(messageContext,
|
|
1140
|
+
await processMessage(messageContext, {
|
|
1141
|
+
cfg,
|
|
1142
|
+
...context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {}
|
|
1143
|
+
});
|
|
1053
1144
|
historyReservation.commit();
|
|
1054
1145
|
} finally {
|
|
1055
1146
|
historyReservation.release();
|
|
@@ -1063,9 +1154,42 @@ async function handleUnfollowEvent(event, _context) {
|
|
|
1063
1154
|
const { userId } = getLineSourceInfo(event.source);
|
|
1064
1155
|
logVerbose(`line: user ${userId ?? "unknown"} unfollowed`);
|
|
1065
1156
|
}
|
|
1066
|
-
async function handleJoinEvent(event,
|
|
1067
|
-
const { groupId, roomId } = getLineSourceInfo(event.source);
|
|
1157
|
+
async function handleJoinEvent(event, context) {
|
|
1158
|
+
const { groupId, roomId, isGroup } = getLineSourceInfo(event.source);
|
|
1159
|
+
const conversationId = groupId ?? roomId;
|
|
1160
|
+
if (!isGroup || !conversationId) return;
|
|
1068
1161
|
logVerbose(`line: bot joined ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
|
|
1162
|
+
const { cfg, account } = context;
|
|
1163
|
+
const roomAllowed = Boolean(await resolveLineEventAdmission(event, context));
|
|
1164
|
+
await reportChannelRoomJoin({
|
|
1165
|
+
cfg,
|
|
1166
|
+
channel: "line",
|
|
1167
|
+
accountId: account.accountId,
|
|
1168
|
+
conversationId,
|
|
1169
|
+
deliverTo: conversationId,
|
|
1170
|
+
route: resolveAgentRoute({
|
|
1171
|
+
cfg,
|
|
1172
|
+
channel: "line",
|
|
1173
|
+
accountId: account.accountId,
|
|
1174
|
+
peer: {
|
|
1175
|
+
kind: "group",
|
|
1176
|
+
id: conversationId
|
|
1177
|
+
}
|
|
1178
|
+
}),
|
|
1179
|
+
roomAllowed,
|
|
1180
|
+
resolveRoomContext: async () => {
|
|
1181
|
+
const roomContext = { historyUnavailable: true };
|
|
1182
|
+
const title = groupId ? await getLineGroupName(groupId, {
|
|
1183
|
+
cfg,
|
|
1184
|
+
accountId: account.accountId,
|
|
1185
|
+
channelAccessToken: account.channelAccessToken
|
|
1186
|
+
}) : void 0;
|
|
1187
|
+
return title ? {
|
|
1188
|
+
...roomContext,
|
|
1189
|
+
title
|
|
1190
|
+
} : roomContext;
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1069
1193
|
}
|
|
1070
1194
|
async function handleLeaveEvent(event, _context) {
|
|
1071
1195
|
const { groupId, roomId } = getLineSourceInfo(event.source);
|
|
@@ -1074,7 +1198,7 @@ async function handleLeaveEvent(event, _context) {
|
|
|
1074
1198
|
async function handlePostbackEvent(event, context) {
|
|
1075
1199
|
const data = event.postback.data;
|
|
1076
1200
|
logVerbose(`line: received postback: ${data}`);
|
|
1077
|
-
const decision = await
|
|
1201
|
+
const decision = await resolveLineEventAdmission(event, context);
|
|
1078
1202
|
if (!decision) return;
|
|
1079
1203
|
const postbackContext = await buildLinePostbackContext({
|
|
1080
1204
|
event,
|
|
@@ -1085,7 +1209,10 @@ async function handlePostbackEvent(event, context) {
|
|
|
1085
1209
|
buildContext: context.buildContext
|
|
1086
1210
|
});
|
|
1087
1211
|
if (!postbackContext) return;
|
|
1088
|
-
await context.processMessage(postbackContext,
|
|
1212
|
+
await context.processMessage(postbackContext, {
|
|
1213
|
+
cfg: context.cfg,
|
|
1214
|
+
...context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {}
|
|
1215
|
+
});
|
|
1089
1216
|
}
|
|
1090
1217
|
async function handleLineWebhookEvents(events, context) {
|
|
1091
1218
|
let firstError;
|
|
@@ -1122,12 +1249,10 @@ async function handleLineWebhookEvent(event, context) {
|
|
|
1122
1249
|
}
|
|
1123
1250
|
//#endregion
|
|
1124
1251
|
//#region extensions/line/src/webhook-spool.ts
|
|
1125
|
-
const LINE_WEBHOOK_SPOOL_VERSION = 1;
|
|
1126
1252
|
const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
|
|
1127
1253
|
const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
|
|
1128
1254
|
const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
|
|
1129
1255
|
const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
|
|
1130
|
-
const LineWebhookPayloadError = createChannelIngressError("LineWebhookPayloadError");
|
|
1131
1256
|
var LineWebhookTerminalDeliveryError = class extends Error {
|
|
1132
1257
|
constructor(message, options) {
|
|
1133
1258
|
super(message, options);
|
|
@@ -1135,35 +1260,6 @@ var LineWebhookTerminalDeliveryError = class extends Error {
|
|
|
1135
1260
|
this.name = "LineWebhookTerminalDeliveryError";
|
|
1136
1261
|
}
|
|
1137
1262
|
};
|
|
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
1263
|
function parseStoredEvent(rawEvent) {
|
|
1168
1264
|
let event;
|
|
1169
1265
|
try {
|
|
@@ -1203,7 +1299,7 @@ function createLineWebhookSpool(options) {
|
|
|
1203
1299
|
};
|
|
1204
1300
|
},
|
|
1205
1301
|
payload: {
|
|
1206
|
-
version:
|
|
1302
|
+
version: 1,
|
|
1207
1303
|
serialize: ({ event, destination }) => ({
|
|
1208
1304
|
rawEvent: JSON.stringify(event),
|
|
1209
1305
|
destination
|
|
@@ -1218,7 +1314,7 @@ function createLineWebhookSpool(options) {
|
|
|
1218
1314
|
destination: body.destination
|
|
1219
1315
|
}),
|
|
1220
1316
|
decode: (payload) => {
|
|
1221
|
-
if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError(
|
|
1317
|
+
if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError(LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE);
|
|
1222
1318
|
return {
|
|
1223
1319
|
version: payload.version,
|
|
1224
1320
|
body: {
|
|
@@ -1227,7 +1323,7 @@ function createLineWebhookSpool(options) {
|
|
|
1227
1323
|
}
|
|
1228
1324
|
};
|
|
1229
1325
|
},
|
|
1230
|
-
createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ?
|
|
1326
|
+
createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? LINE_WEBHOOK_SPOOL_INVALID_PAYLOAD_MESSAGE : "LINE webhook event identity changed after durable admission.")
|
|
1231
1327
|
},
|
|
1232
1328
|
deliver: async ({ event, destination }, lifecycle) => {
|
|
1233
1329
|
const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
|
|
@@ -1287,7 +1383,7 @@ function createLineWebhookSpool(options) {
|
|
|
1287
1383
|
},
|
|
1288
1384
|
resolveNonRetryableFailure: (error) => {
|
|
1289
1385
|
if (error instanceof LineWebhookPayloadError) return {
|
|
1290
|
-
reason:
|
|
1386
|
+
reason: LINE_WEBHOOK_SPOOL_INVALID_EVENT_REASON,
|
|
1291
1387
|
message: error.message
|
|
1292
1388
|
};
|
|
1293
1389
|
if (error instanceof LineWebhookTerminalDeliveryError) return {
|
|
@@ -1308,12 +1404,12 @@ function createLineWebhookSpool(options) {
|
|
|
1308
1404
|
let stopTask;
|
|
1309
1405
|
return {
|
|
1310
1406
|
accept: async (body) => {
|
|
1311
|
-
const events = body.events ?? [];
|
|
1312
|
-
if (events.length === 0) return;
|
|
1313
|
-
await monitor.admitBatch(events.map((event) => ({
|
|
1407
|
+
const events = (body.events ?? []).filter((event) => event.mode !== "standby");
|
|
1408
|
+
if (events.length === 0) return "ignored";
|
|
1409
|
+
return (await monitor.admitBatch(events.map((event) => ({
|
|
1314
1410
|
event,
|
|
1315
1411
|
destination: body.destination ?? ""
|
|
1316
|
-
})), { receivedAt: Date.now() });
|
|
1412
|
+
})), { receivedAt: Date.now() })).some((admission) => admission.kind === "durable") ? "durable" : "ignored";
|
|
1317
1413
|
},
|
|
1318
1414
|
start: () => {
|
|
1319
1415
|
if (!stopTask) monitor.start();
|
|
@@ -1340,9 +1436,17 @@ function createLineWebhookSpool(options) {
|
|
|
1340
1436
|
const DEFAULT_MEDIA_MAX_MB = 10;
|
|
1341
1437
|
function createLineBot(opts) {
|
|
1342
1438
|
const runtime = opts.runtime ?? createNonExitingRuntime();
|
|
1343
|
-
const
|
|
1439
|
+
const startupConfig = opts.config ?? getRuntimeConfig();
|
|
1440
|
+
const startupRuntimeConfig = getRuntimeConfigSnapshot();
|
|
1441
|
+
const startupRuntimeSourceConfig = getRuntimeConfigSourceSnapshot();
|
|
1442
|
+
const followsRuntimeConfig = opts.config === void 0 || startupRuntimeConfig === startupConfig || startupRuntimeSourceConfig !== null && selectApplicableRuntimeConfig({
|
|
1443
|
+
inputConfig: startupConfig,
|
|
1444
|
+
runtimeConfig: startupRuntimeConfig,
|
|
1445
|
+
runtimeSourceConfig: startupRuntimeSourceConfig
|
|
1446
|
+
}) === startupRuntimeConfig;
|
|
1447
|
+
const resolveTurnConfig = () => (followsRuntimeConfig ? getRuntimeConfigSnapshot() : void 0) ?? startupConfig;
|
|
1344
1448
|
const account = resolveLineAccount({
|
|
1345
|
-
cfg,
|
|
1449
|
+
cfg: startupConfig,
|
|
1346
1450
|
accountId: opts.accountId
|
|
1347
1451
|
});
|
|
1348
1452
|
const mediaMaxBytes = ([opts.mediaMaxMb, account.config.mediaMaxMb].find((value) => typeof value === "number" && value > 0) ?? DEFAULT_MEDIA_MAX_MB) * 1024 * 1024;
|
|
@@ -1353,17 +1457,20 @@ function createLineBot(opts) {
|
|
|
1353
1457
|
const spool = createLineWebhookSpool({
|
|
1354
1458
|
accountId: account.accountId,
|
|
1355
1459
|
runtime,
|
|
1356
|
-
deliver: async (event, _destination, control) =>
|
|
1357
|
-
cfg
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1460
|
+
deliver: async (event, _destination, control) => {
|
|
1461
|
+
const cfg = resolveTurnConfig();
|
|
1462
|
+
await handleLineWebhookEvents([event], {
|
|
1463
|
+
cfg,
|
|
1464
|
+
account,
|
|
1465
|
+
runtime,
|
|
1466
|
+
buildContext: opts.buildContext,
|
|
1467
|
+
mediaMaxBytes,
|
|
1468
|
+
processMessage,
|
|
1469
|
+
...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
|
|
1470
|
+
groupHistories,
|
|
1471
|
+
historyLimit: account.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1367
1474
|
});
|
|
1368
1475
|
spool.start();
|
|
1369
1476
|
return {
|
|
@@ -1389,7 +1496,8 @@ function resolveLineDurableReplyOptions(params) {
|
|
|
1389
1496
|
//#endregion
|
|
1390
1497
|
//#region extensions/line/src/signature.ts
|
|
1391
1498
|
function validateLineSignature(body, signature, channelSecret) {
|
|
1392
|
-
|
|
1499
|
+
const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
|
|
1500
|
+
return safeEqualSecret(signature, hash);
|
|
1393
1501
|
}
|
|
1394
1502
|
//#endregion
|
|
1395
1503
|
//#region extensions/line/src/webhook-utils.ts
|
|
@@ -1402,15 +1510,28 @@ function parseLineWebhookBody(rawBody) {
|
|
|
1402
1510
|
}
|
|
1403
1511
|
//#endregion
|
|
1404
1512
|
//#region extensions/line/src/webhook-node.ts
|
|
1405
|
-
const LINE_WEBHOOK_MAX_BODY_BYTES =
|
|
1406
|
-
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 =
|
|
1513
|
+
const LINE_WEBHOOK_MAX_BODY_BYTES = 1048576;
|
|
1514
|
+
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 65536;
|
|
1407
1515
|
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1 = 5e3;
|
|
1408
1516
|
async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1) {
|
|
1409
1517
|
return await readRequestBodyWithLimit(req, {
|
|
1410
1518
|
maxBytes,
|
|
1411
|
-
timeoutMs
|
|
1519
|
+
timeoutMs,
|
|
1520
|
+
destroyOnLimit: false
|
|
1412
1521
|
});
|
|
1413
1522
|
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Answer a body-limit failure through the connection owner.
|
|
1525
|
+
*
|
|
1526
|
+
* The reader defers destruction for these two codes, so the connection is already fenced
|
|
1527
|
+
* and only the owner can still write: responding directly would race the teardown and LINE
|
|
1528
|
+
* would see a reset instead of the status.
|
|
1529
|
+
*/
|
|
1530
|
+
async function rejectLineWebhookRequest(req, res, error) {
|
|
1531
|
+
if (!isRequestBodyLimitError(error, "PAYLOAD_TOO_LARGE") && !isRequestBodyLimitError(error, "REQUEST_BODY_TIMEOUT")) return false;
|
|
1532
|
+
await sendHttpRequestRejection(req, res, error.statusCode, JSON.stringify({ error: requestBodyErrorToText(error.code) }), "application/json");
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1414
1535
|
function createLineNodeWebhookHandler(params) {
|
|
1415
1536
|
const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
|
|
1416
1537
|
const readBody = params.readBody ?? readLineWebhookRequestBody;
|
|
@@ -1467,18 +1588,7 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1467
1588
|
res.setHeader("Content-Type", "application/json");
|
|
1468
1589
|
res.end(JSON.stringify({ status: "ok" }));
|
|
1469
1590
|
} 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
|
-
}
|
|
1591
|
+
if (await rejectLineWebhookRequest(req, res, err)) return;
|
|
1482
1592
|
params.runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
|
|
1483
1593
|
if (!res.headersSent) {
|
|
1484
1594
|
res.statusCode = 500;
|
|
@@ -1491,7 +1601,7 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1491
1601
|
//#endregion
|
|
1492
1602
|
//#region extensions/line/src/monitor.ts
|
|
1493
1603
|
const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
|
|
1494
|
-
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES =
|
|
1604
|
+
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
|
|
1495
1605
|
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
|
|
1496
1606
|
async function registerLineWebhookTarget(params, bot) {
|
|
1497
1607
|
try {
|
|
@@ -1539,20 +1649,22 @@ async function monitorLineProvider(opts) {
|
|
|
1539
1649
|
onMessage: async (ctx, deliveryControl) => {
|
|
1540
1650
|
if (!ctx) return;
|
|
1541
1651
|
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,
|
|
1652
|
+
const turnConfig = deliveryControl.cfg;
|
|
1653
|
+
const stopLoading = Boolean(ctx.userId && !ctx.isGroup) ? startLineLoadingKeepalive({
|
|
1654
|
+
cfg: turnConfig,
|
|
1549
1655
|
userId: ctx.userId,
|
|
1550
1656
|
accountId: ctx.accountId
|
|
1551
1657
|
}) : null;
|
|
1552
|
-
logVerbose(`line: received message from ${
|
|
1658
|
+
logVerbose(`line: received message from ${ctxPayload.SenderName ?? ctx.userId ?? ctxPayload.From} (${ctxPayload.From})`);
|
|
1553
1659
|
let replyTokenUsed = false;
|
|
1554
1660
|
let turnAdopted = false;
|
|
1555
1661
|
const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
|
|
1662
|
+
const turnAbortSignal = ingressLifecycle?.abortSignal;
|
|
1663
|
+
const skillFilter = ctx.skillFilter;
|
|
1664
|
+
const replyOptions = turnAbortSignal || skillFilter ? {
|
|
1665
|
+
...turnAbortSignal ? { abortSignal: turnAbortSignal } : {},
|
|
1666
|
+
...skillFilter ? { skillFilter } : {}
|
|
1667
|
+
} : void 0;
|
|
1556
1668
|
try {
|
|
1557
1669
|
const textLimit = 5e3;
|
|
1558
1670
|
const turnResult = await getLineRuntime().channel.inbound.run({
|
|
@@ -1573,7 +1685,7 @@ async function monitorLineProvider(opts) {
|
|
|
1573
1685
|
rawText: ctxPayload.RawBody ?? ctxPayload.BodyForAgent ?? ""
|
|
1574
1686
|
}),
|
|
1575
1687
|
resolveTurn: () => ({
|
|
1576
|
-
cfg:
|
|
1688
|
+
cfg: turnConfig,
|
|
1577
1689
|
channel: "line",
|
|
1578
1690
|
accountId: route.accountId,
|
|
1579
1691
|
route: {
|
|
@@ -1583,8 +1695,10 @@ async function monitorLineProvider(opts) {
|
|
|
1583
1695
|
ctxPayload,
|
|
1584
1696
|
record: ctx.turn.record,
|
|
1585
1697
|
replyPipeline: {},
|
|
1586
|
-
|
|
1698
|
+
dispatcherOptions: { humanDelay: resolveHumanDelayConfig(turnConfig, route.agentId) },
|
|
1699
|
+
...replyOptions ? { replyOptions } : {},
|
|
1587
1700
|
delivery: {
|
|
1701
|
+
preparePayload: prepareLineReplyPayload,
|
|
1588
1702
|
durable: (payload, info) => resolveLineDurableReplyOptions({
|
|
1589
1703
|
payload,
|
|
1590
1704
|
infoKind: info.kind,
|
|
@@ -1595,7 +1709,7 @@ async function monitorLineProvider(opts) {
|
|
|
1595
1709
|
deliver: async (payload) => {
|
|
1596
1710
|
const lineData = payload.channelData?.line ?? {};
|
|
1597
1711
|
if (ctx.userId && !ctx.isGroup) showLoadingAnimation(ctx.userId, {
|
|
1598
|
-
cfg:
|
|
1712
|
+
cfg: turnConfig,
|
|
1599
1713
|
accountId: ctx.accountId
|
|
1600
1714
|
}).catch(() => {});
|
|
1601
1715
|
const deliveryResult = await deliverLineAutoReply({
|
|
@@ -1605,14 +1719,13 @@ async function monitorLineProvider(opts) {
|
|
|
1605
1719
|
replyToken,
|
|
1606
1720
|
replyTokenUsed,
|
|
1607
1721
|
accountId: ctx.accountId,
|
|
1608
|
-
cfg:
|
|
1722
|
+
cfg: turnConfig,
|
|
1609
1723
|
textLimit,
|
|
1610
1724
|
deps: {
|
|
1611
1725
|
buildTemplateMessageFromPayload,
|
|
1612
1726
|
processLineMessage,
|
|
1613
1727
|
chunkMarkdownText,
|
|
1614
1728
|
replyMessageLine,
|
|
1615
|
-
createQuickReplyItems,
|
|
1616
1729
|
pushMessagesLine,
|
|
1617
1730
|
createFlexMessage,
|
|
1618
1731
|
buildMediaMessage: buildLineMediaMessage,
|
|
@@ -1633,7 +1746,8 @@ async function monitorLineProvider(opts) {
|
|
|
1633
1746
|
})
|
|
1634
1747
|
}
|
|
1635
1748
|
});
|
|
1636
|
-
|
|
1749
|
+
const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
|
|
1750
|
+
if (!hasFinalInboundReplyDispatch(dispatchResult)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
|
|
1637
1751
|
} catch (err) {
|
|
1638
1752
|
runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
|
|
1639
1753
|
if (turnAdopted || replyTokenUsed) throw new LineWebhookTerminalDeliveryError("LINE delivery failed after consuming the event reply token.", { cause: err });
|
|
@@ -1720,25 +1834,13 @@ async function monitorLineProvider(opts) {
|
|
|
1720
1834
|
}
|
|
1721
1835
|
if (body.events && body.events.length > 0) {
|
|
1722
1836
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1723
|
-
await match.target.bot.handleWebhook(body);
|
|
1724
|
-
res.setHeader("x-openclaw-delivery-accepted", "durable");
|
|
1837
|
+
if (await match.target.bot.handleWebhook(body) === "durable") res.setHeader("x-openclaw-delivery-accepted", "durable");
|
|
1725
1838
|
}
|
|
1726
1839
|
res.statusCode = 200;
|
|
1727
1840
|
res.setHeader("Content-Type", "application/json");
|
|
1728
1841
|
res.end(JSON.stringify({ status: "ok" }));
|
|
1729
1842
|
} 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
|
-
}
|
|
1843
|
+
if (await rejectLineWebhookRequest(req, res, err)) return;
|
|
1742
1844
|
runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
|
|
1743
1845
|
if (!res.headersSent) {
|
|
1744
1846
|
res.statusCode = 500;
|