@openclaw/line 2026.9.6 → 2026.9.7
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/assets/icon.png +0 -0
- package/dist/.setup/{accounts-BBEFMYbY.mjs → accounts-DOiM8-y3.mjs} +1 -4
- package/dist/.setup/api-4RgpQmHW.mjs +3 -0
- package/dist/.setup/{api-D_06cuHl.mjs → api-w4QdlCCB.mjs} +2 -2
- package/dist/.setup/{card-command-710u8FBH.mjs → card-command-DYf4gnmI.mjs} +25 -33
- package/dist/.setup/{channel-kc3VRglA.mjs → channel-DN6Za8Vm.mjs} +789 -26
- package/dist/.setup/channel.runtime-bd5vGeSo.mjs +4 -0
- package/dist/.setup/{monitor-morJPcPd.mjs → monitor-C01hAm3f.mjs} +262 -333
- package/dist/.setup/monitor.runtime-DwH2zK48.mjs +2 -0
- package/dist/.setup/outbound.runtime-D_Gs99Dm.mjs +3 -0
- package/dist/.setup/probe.runtime-Cj08-BZ1.mjs +2 -0
- package/dist/.setup/runtime-V7yh9OeC.mjs +8 -0
- package/dist/.setup/{send-BmsHY4WC.mjs → send-DOY1WTxv.mjs} +7 -26
- package/dist/.setup/{send-retry-DbfiHBPd.mjs → send-retry-DV6I0muq.mjs} +104 -208
- package/dist/.setup/{setup-surface-CcfKJKtr.mjs → setup-surface-CbfkmkIS.mjs} +15 -4
- package/dist/.setup/{markdown-to-line-CRfEHFs0.mjs → template-messages-CxCnYsWN.mjs} +168 -189
- package/dist/api.js +2 -2
- package/dist/channel-plugin-api.js +1 -1
- package/dist/contract-api.js +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime-api.js +2 -260
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/node_modules/@line/bot-sdk/node_modules/@types/node/README.md +1 -1
- package/node_modules/@line/bot-sdk/node_modules/@types/node/child_process.d.ts +1 -1
- package/node_modules/@line/bot-sdk/node_modules/@types/node/package.json +2 -2
- package/node_modules/typebox/build/compile/code.mjs +1 -2
- package/node_modules/typebox/build/compile/validator.mjs +5 -1
- package/node_modules/typebox/build/format/iri.mjs +16 -9
- package/node_modules/typebox/build/guard/emit.mjs +2 -2
- package/node_modules/typebox/build/guard/guard.d.mts +7 -5
- package/node_modules/typebox/build/guard/guard.mjs +14 -9
- package/node_modules/typebox/build/guard/index.mjs +6 -0
- package/node_modules/typebox/build/guard/unicode/unicode.d.mts +6 -0
- package/node_modules/typebox/build/guard/unicode/unicode.mjs +35 -0
- package/node_modules/typebox/build/guard/{string.mjs → unicode/unicode_segment.mjs} +16 -19
- package/node_modules/typebox/build/schema/build.mjs +2 -3
- package/node_modules/typebox/build/schema/engine/_pathing.d.mts +1 -0
- package/node_modules/typebox/build/schema/engine/_pathing.mjs +11 -0
- package/node_modules/typebox/build/schema/engine/_refine.mjs +13 -5
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dependencies.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/format.mjs +9 -2
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/properties.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/ref.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/ref.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/schema.mjs +24 -24
- package/node_modules/typebox/build/schema/parse.mjs +3 -0
- package/node_modules/typebox/build/schema/pointer/pointer.d.mts +1 -6
- package/node_modules/typebox/build/schema/pointer/pointer.mjs +47 -64
- package/node_modules/typebox/build/system/settings/internal.d.mts +2 -0
- package/node_modules/typebox/build/system/settings/internal.mjs +20 -0
- package/node_modules/typebox/build/system/settings/settings.d.mts +11 -0
- package/node_modules/typebox/build/system/settings/settings.mjs +14 -0
- package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
- package/node_modules/typebox/build/type/extends/inference.mjs +15 -2
- package/node_modules/typebox/build/type/extends/tuple.d.mts +2 -1
- package/node_modules/typebox/build/type/extends/tuple.mjs +21 -1
- package/node_modules/typebox/build/value/parse/parse.mjs +10 -4
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +3 -1
- package/package.json +5 -5
- package/dist/.setup/account-helpers-BSUyZwLm.mjs +0 -12
- package/dist/.setup/api-RdEuQgW-.mjs +0 -3
- package/dist/.setup/channel.runtime-BtkeDELb.mjs +0 -4
- package/dist/.setup/monitor.runtime-BGhxYDKG.mjs +0 -2
- package/dist/.setup/outbound.runtime-QGvZy8Em.mjs +0 -3
- package/dist/.setup/probe.runtime-CMf8b-jB.mjs +0 -2
- package/dist/.setup/rich-messages-d8CmBVJj.mjs +0 -872
- /package/node_modules/typebox/build/guard/{string.d.mts → unicode/unicode_segment.d.mts} +0 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { n as resolveDefaultLineAccountId, r as resolveLineAccount } from "./accounts-DOiM8-y3.mjs";
|
|
2
|
+
import { a as resolveLineQuestionPostback, c as normalizeLineAllowEntry, i as parseLineQuestionPostbackData, n as createLineQuickReply, o as buildLineQuickReplyFallbackText, r as prepareLineReplyPayload, s as resolveLineGroupConfigEntry } from "./channel-DN6Za8Vm.mjs";
|
|
3
|
+
import { t as getLineRuntime } from "./runtime-V7yh9OeC.mjs";
|
|
4
|
+
import { D as recordLineQuoteToken, E as readLineQuoteToken, O as reportLineQuoteCarrierMissing, T as canCarryLineQuoteToken, j as buildLineMediaMessage, k as resolveLineQuoteToken, n as findLineHttpError, r as resolveLineNonDispatchRetryable, t as explainLineRefusal, w as applyLineQuoteToken } from "./send-retry-DV6I0muq.mjs";
|
|
4
5
|
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.mjs";
|
|
5
|
-
import { a as
|
|
6
|
-
import {
|
|
6
|
+
import { a as getUserDisplayName, g as quotesLineBotMessage, h as showLoadingAnimation, i as getLineGroupName, l as pushMessageLine, n as createLocationMessage, o as getUserProfile, p as replyMessageLine, t as createFlexMessage, u as pushMessagesLine } from "./send-DOY1WTxv.mjs";
|
|
7
|
+
import { n as processLineMessage, t as buildTemplateMessageFromPayload } from "./template-messages-CxCnYsWN.mjs";
|
|
7
8
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
8
9
|
import { resolveChannelGroupsConfigPath } from "openclaw/plugin-sdk/channel-policy";
|
|
9
10
|
import { normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
11
|
+
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
12
|
+
import { firstDefined } from "openclaw/plugin-sdk/allow-from";
|
|
10
13
|
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
|
|
11
14
|
import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, implicitMentionKindWhen, isChannelPartialDeliveryError, logInboundDrop, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
12
15
|
import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, readErrorName, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
|
|
@@ -16,133 +19,29 @@ import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForA
|
|
|
16
19
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
17
20
|
import { classifyTransientNetworkErrorCode } from "openclaw/plugin-sdk/retry-runtime";
|
|
18
21
|
import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
|
|
19
|
-
import {
|
|
20
|
-
import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
|
|
21
|
-
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
22
|
+
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
22
23
|
import crypto from "node:crypto";
|
|
23
24
|
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
|
24
25
|
import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
|
|
25
26
|
import { channelReadyPatch, channelStoppedPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
26
27
|
import { canonicalizeWebhookRouteKey, registerWebhookTargetWithPluginRoute, resolveSingleWebhookTarget, resolveWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
27
28
|
import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, runDetachedWebhookWork, sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
28
|
-
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
29
29
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
30
30
|
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
|
|
31
31
|
import { getRuntimeConfig, getRuntimeConfigSnapshot, getRuntimeConfigSourceSnapshot, selectApplicableRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
32
32
|
import { fanInChannelIngressLifecycles, resolveChannelImplicitMentions } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
33
33
|
import { reportChannelRoomJoin } from "openclaw/plugin-sdk/channel-join-intro-runtime";
|
|
34
34
|
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
|
|
35
|
-
import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist,
|
|
35
|
+
import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
36
36
|
import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
|
|
37
|
-
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
37
|
+
import { parseAgentSessionKey, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
38
38
|
import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
39
|
+
import { getSessionBindingService, inspectRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-binding-runtime";
|
|
40
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
41
|
+
import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
|
|
42
|
+
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
39
43
|
import { enqueueKeyedTask } from "openclaw/plugin-sdk/keyed-async-queue";
|
|
40
44
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
41
|
-
//#region extensions/line/src/download.ts
|
|
42
|
-
const CONTENT_READY_MAX_ATTEMPTS = 6;
|
|
43
|
-
const CONTENT_READY_BASE_DELAY_MS = 500;
|
|
44
|
-
const CONTENT_READY_MAX_DELAY_MS = 4e3;
|
|
45
|
-
const CONTENT_DOWNLOAD_TIMEOUT_MS = 15e3;
|
|
46
|
-
const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
|
|
47
|
-
var RetryableLineMediaFetchError = class extends MediaFetchError {
|
|
48
|
-
constructor(message, options) {
|
|
49
|
-
super("fetch_failed", message, options);
|
|
50
|
-
this.name = "RetryableLineMediaFetchError";
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
function contentBackoffDelayMs(attempt) {
|
|
54
|
-
return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
|
|
55
|
-
}
|
|
56
|
-
function lineContentUrl(messageId) {
|
|
57
|
-
return `${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`;
|
|
58
|
-
}
|
|
59
|
-
async function* lineResponseBodyChunks(response, messageId, signal, onComplete) {
|
|
60
|
-
const body = response.body;
|
|
61
|
-
if (!body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
62
|
-
const reader = body.getReader();
|
|
63
|
-
let completed = false;
|
|
64
|
-
try {
|
|
65
|
-
while (true) {
|
|
66
|
-
let chunk;
|
|
67
|
-
try {
|
|
68
|
-
chunk = await reader.read();
|
|
69
|
-
} catch (err) {
|
|
70
|
-
if (signal.aborted) throw signal.reason;
|
|
71
|
-
throw new RetryableLineMediaFetchError(`LINE media response stream failed for message ${messageId}`, { cause: err });
|
|
72
|
-
}
|
|
73
|
-
if (chunk.done) {
|
|
74
|
-
completed = true;
|
|
75
|
-
onComplete();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (chunk.value.byteLength > 0) yield chunk.value;
|
|
79
|
-
}
|
|
80
|
-
} finally {
|
|
81
|
-
if (!completed) await reader.cancel(signal.aborted ? signal.reason : void 0).catch(() => void 0);
|
|
82
|
-
try {
|
|
83
|
-
reader.releaseLock();
|
|
84
|
-
} catch {}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async function fetchLineContentWhenReady(messageId, channelAccessToken, signal) {
|
|
88
|
-
try {
|
|
89
|
-
for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
|
|
90
|
-
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
|
|
91
|
-
headers: { Authorization: `Bearer ${channelAccessToken}` },
|
|
92
|
-
redirect: "error",
|
|
93
|
-
signal
|
|
94
|
-
});
|
|
95
|
-
if (response.status === 200) {
|
|
96
|
-
if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
97
|
-
return response;
|
|
98
|
-
}
|
|
99
|
-
await response.body?.cancel().catch(() => void 0);
|
|
100
|
-
if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
|
|
101
|
-
if (attempt < 5) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal });
|
|
102
|
-
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
if (signal.aborted) throw signal.reason;
|
|
105
|
-
if (err instanceof MediaFetchError) throw err;
|
|
106
|
-
throw new RetryableLineMediaFetchError(`LINE media download failed for message ${messageId}`, { cause: err });
|
|
107
|
-
}
|
|
108
|
-
throw new MediaFetchError("http_error", `LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`, { status: 202 });
|
|
109
|
-
}
|
|
110
|
-
function isRetryableLineInboundMediaError(err) {
|
|
111
|
-
if (err instanceof RetryableLineMediaFetchError) return true;
|
|
112
|
-
if (!(err instanceof MediaFetchError)) return false;
|
|
113
|
-
if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10485760, options) {
|
|
117
|
-
options?.signal?.throwIfAborted();
|
|
118
|
-
const deadlineAbort = new AbortController();
|
|
119
|
-
const signal = options?.signal ? AbortSignal.any([options.signal, deadlineAbort.signal]) : deadlineAbort.signal;
|
|
120
|
-
const deadline = setTimeout(() => {
|
|
121
|
-
deadlineAbort.abort(new RetryableLineMediaFetchError(`LINE media for message ${messageId} did not download within ${CONTENT_DOWNLOAD_TIMEOUT_MS / 1e3} seconds`));
|
|
122
|
-
}, CONTENT_DOWNLOAD_TIMEOUT_MS);
|
|
123
|
-
deadline.unref();
|
|
124
|
-
try {
|
|
125
|
-
const response = await fetchLineContentWhenReady(messageId, channelAccessToken, signal);
|
|
126
|
-
let saved;
|
|
127
|
-
try {
|
|
128
|
-
saved = await saveMediaStream(lineResponseBodyChunks(response, messageId, signal, () => clearTimeout(deadline)), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
|
|
129
|
-
} catch (err) {
|
|
130
|
-
await response.body?.cancel(signal.aborted ? signal.reason : err).catch(() => void 0);
|
|
131
|
-
if (signal.aborted) throw signal.reason;
|
|
132
|
-
throw err;
|
|
133
|
-
}
|
|
134
|
-
options?.signal?.throwIfAborted();
|
|
135
|
-
logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
|
|
136
|
-
return {
|
|
137
|
-
path: saved.path,
|
|
138
|
-
contentType: saved.contentType,
|
|
139
|
-
size: saved.size
|
|
140
|
-
};
|
|
141
|
-
} finally {
|
|
142
|
-
clearTimeout(deadline);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
//#endregion
|
|
146
45
|
//#region extensions/line/src/auto-reply-delivery.ts
|
|
147
46
|
function toLineDeliveryError(error) {
|
|
148
47
|
return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
|
|
@@ -410,46 +309,73 @@ function buildPeerId(source) {
|
|
|
410
309
|
if (source.type === "user" && source.userId) return source.userId;
|
|
411
310
|
return "unknown";
|
|
412
311
|
}
|
|
413
|
-
async function
|
|
414
|
-
recordChannelActivity({
|
|
415
|
-
channel: "line",
|
|
416
|
-
accountId: params.account.accountId,
|
|
417
|
-
direction: "inbound"
|
|
418
|
-
});
|
|
312
|
+
async function prepareLineInboundRoute(params) {
|
|
419
313
|
const { userId, groupId, roomId, isGroup } = getLineSourceInfo(params.source);
|
|
420
314
|
const peerId = buildPeerId(params.source);
|
|
421
|
-
|
|
422
|
-
cfg: params.cfg,
|
|
315
|
+
const routeInput = {
|
|
423
316
|
channel: "line",
|
|
424
317
|
accountId: params.account.accountId,
|
|
425
318
|
peer: {
|
|
426
319
|
kind: isGroup ? "group" : "direct",
|
|
427
320
|
id: peerId
|
|
428
321
|
}
|
|
322
|
+
};
|
|
323
|
+
const conversation = {
|
|
324
|
+
channel: "line",
|
|
325
|
+
accountId: params.account.accountId,
|
|
326
|
+
conversationId: peerId
|
|
327
|
+
};
|
|
328
|
+
const inspection = await getSessionBindingService().inspectByConversationAsync(conversation);
|
|
329
|
+
if (inspection.status === "unavailable") throw new Error("LINE conversation binding owner is temporarily unavailable; retry the message.");
|
|
330
|
+
const resolveScopeRoute = (agentId) => resolveAgentRoute({
|
|
331
|
+
...routeInput,
|
|
332
|
+
cfg: { session: params.cfg.session },
|
|
333
|
+
defaultAgentId: agentId
|
|
429
334
|
});
|
|
430
|
-
const
|
|
335
|
+
const selection = inspectRuntimeConversationBindingRoute({
|
|
336
|
+
route: resolveScopeRoute(),
|
|
337
|
+
inspection
|
|
338
|
+
});
|
|
339
|
+
const metadataAgentId = selection.bindingRecord?.metadata?.agentId;
|
|
340
|
+
const hasBoundAgent = selection.boundSessionKey && (parseAgentSessionKey(selection.boundSessionKey) || typeof metadataAgentId === "string" && metadataAgentId.trim());
|
|
341
|
+
const baseRoute = hasBoundAgent ? resolveScopeRoute(selection.boundAgentId) : resolveAgentRoute({
|
|
342
|
+
...routeInput,
|
|
343
|
+
cfg: params.cfg
|
|
344
|
+
});
|
|
345
|
+
const configuredRoute = hasBoundAgent ? {
|
|
346
|
+
route: baseRoute,
|
|
347
|
+
bindingResolution: null
|
|
348
|
+
} : resolveConfiguredBindingRoute({
|
|
431
349
|
cfg: params.cfg,
|
|
432
|
-
route,
|
|
433
|
-
conversation
|
|
434
|
-
channel: "line",
|
|
435
|
-
accountId: params.account.accountId,
|
|
436
|
-
conversationId: peerId
|
|
437
|
-
}
|
|
350
|
+
route: baseRoute,
|
|
351
|
+
conversation
|
|
438
352
|
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
353
|
+
const runtimeRoute = inspectRuntimeConversationBindingRoute({
|
|
354
|
+
route: configuredRoute.route,
|
|
355
|
+
inspection
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
userId,
|
|
359
|
+
groupId,
|
|
360
|
+
roomId,
|
|
361
|
+
isGroup,
|
|
362
|
+
peerId,
|
|
363
|
+
route: runtimeRoute.route,
|
|
364
|
+
mentionAgentId: baseRoute.agentId,
|
|
365
|
+
runtimeRoute,
|
|
366
|
+
configuredBinding: runtimeRoute.bindingRecord ? null : configuredRoute.bindingResolution,
|
|
367
|
+
configuredBindingSessionKey: configuredRoute.boundSessionKey ?? ""
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async function resolveLineInboundRoute(params) {
|
|
371
|
+
recordChannelActivity({
|
|
372
|
+
channel: "line",
|
|
373
|
+
accountId: params.account.accountId,
|
|
374
|
+
direction: "inbound"
|
|
449
375
|
});
|
|
450
|
-
route =
|
|
376
|
+
const { userId, groupId, roomId, isGroup, peerId, route, runtimeRoute, configuredBinding, configuredBindingSessionKey } = params.preparedRoute ?? await prepareLineInboundRoute(params);
|
|
451
377
|
if (runtimeRoute.bindingRecord) {
|
|
452
|
-
|
|
378
|
+
await getSessionBindingService().touchAsync(runtimeRoute.bindingRecord.bindingId, void 0, runtimeRoute.bindingRecord.conversation);
|
|
453
379
|
logVerbose(runtimeRoute.boundSessionKey ? `line: routed via bound conversation ${peerId} -> ${runtimeRoute.boundSessionKey}` : `line: plugin-bound conversation ${peerId}`);
|
|
454
380
|
}
|
|
455
381
|
if (configuredBinding) {
|
|
@@ -539,7 +465,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
539
465
|
const body = formatInboundEnvelope({
|
|
540
466
|
channel: "LINE",
|
|
541
467
|
from: conversationLabel,
|
|
542
|
-
timestamp: params.timestamp,
|
|
468
|
+
timestamp: params.event.timestamp,
|
|
543
469
|
body: agentBody,
|
|
544
470
|
chatType: params.source.isGroup ? "group" : "direct",
|
|
545
471
|
sender: {
|
|
@@ -554,7 +480,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
554
480
|
channel: "line",
|
|
555
481
|
accountId: params.route.accountId,
|
|
556
482
|
messageId: params.messageSid,
|
|
557
|
-
timestamp: params.timestamp,
|
|
483
|
+
timestamp: params.event.timestamp,
|
|
558
484
|
from: address,
|
|
559
485
|
sender: {
|
|
560
486
|
id: senderId,
|
|
@@ -594,7 +520,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
594
520
|
const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
|
|
595
521
|
dmScope: params.cfg.session?.dmScope,
|
|
596
522
|
allowFrom: params.account.config.allowFrom,
|
|
597
|
-
normalizeEntry: (entry) =>
|
|
523
|
+
normalizeEntry: (entry) => normalizeLineAllowEntry(entry) || void 0
|
|
598
524
|
}) : null;
|
|
599
525
|
const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
|
|
600
526
|
route: params.route,
|
|
@@ -608,7 +534,14 @@ async function finalizeLineInboundContext(params) {
|
|
|
608
534
|
}
|
|
609
535
|
return {
|
|
610
536
|
ctxPayload,
|
|
537
|
+
event: params.event,
|
|
538
|
+
userId: params.source.userId,
|
|
539
|
+
groupId: params.source.groupId,
|
|
540
|
+
roomId: params.source.roomId,
|
|
541
|
+
isGroup: params.source.isGroup,
|
|
542
|
+
route: params.route,
|
|
611
543
|
replyToken: params.event.replyToken,
|
|
544
|
+
accountId: params.account.accountId,
|
|
612
545
|
skillFilter: groupConfig?.skills,
|
|
613
546
|
turn: {
|
|
614
547
|
storePath,
|
|
@@ -639,11 +572,11 @@ async function buildLineMessageContext(params) {
|
|
|
639
572
|
const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
|
|
640
573
|
source,
|
|
641
574
|
cfg,
|
|
642
|
-
account
|
|
575
|
+
account,
|
|
576
|
+
preparedRoute: params.preparedRoute
|
|
643
577
|
});
|
|
644
578
|
const message = event.message;
|
|
645
579
|
const messageId = message.id;
|
|
646
|
-
const timestamp = event.timestamp;
|
|
647
580
|
const textContent = extractMessageText(message);
|
|
648
581
|
const nativeMediaKind = extractNativeMediaKind(message);
|
|
649
582
|
const mediaFacts = allMedia.length > 0 ? allMedia.map((media) => ({
|
|
@@ -677,7 +610,7 @@ async function buildLineMessageContext(params) {
|
|
|
677
610
|
address: loc.address
|
|
678
611
|
});
|
|
679
612
|
}
|
|
680
|
-
|
|
613
|
+
return finalizeLineInboundContext({
|
|
681
614
|
cfg,
|
|
682
615
|
account,
|
|
683
616
|
event,
|
|
@@ -693,7 +626,6 @@ async function buildLineMessageContext(params) {
|
|
|
693
626
|
agentBody,
|
|
694
627
|
commandBody: resolveLineMentionStrippedText(message) || rawBody,
|
|
695
628
|
mentions: params.mentions,
|
|
696
|
-
timestamp,
|
|
697
629
|
messageSid: messageId,
|
|
698
630
|
commandAuthorized,
|
|
699
631
|
channelIngress: await params.resolveChannelIngress?.({
|
|
@@ -711,19 +643,6 @@ async function buildLineMessageContext(params) {
|
|
|
711
643
|
},
|
|
712
644
|
inboundHistory
|
|
713
645
|
});
|
|
714
|
-
return {
|
|
715
|
-
ctxPayload: finalized.ctxPayload,
|
|
716
|
-
turn: finalized.turn,
|
|
717
|
-
skillFilter: finalized.skillFilter,
|
|
718
|
-
event,
|
|
719
|
-
userId,
|
|
720
|
-
groupId,
|
|
721
|
-
roomId,
|
|
722
|
-
isGroup,
|
|
723
|
-
route,
|
|
724
|
-
replyToken: event.replyToken,
|
|
725
|
-
accountId: account.accountId
|
|
726
|
-
};
|
|
727
646
|
}
|
|
728
647
|
async function buildLinePostbackContext(params) {
|
|
729
648
|
const { event, cfg, account, commandAuthorized } = params;
|
|
@@ -748,7 +667,7 @@ async function buildLinePostbackContext(params) {
|
|
|
748
667
|
if (picked) agentBody += ` ${key}=${picked}`;
|
|
749
668
|
}
|
|
750
669
|
const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
|
|
751
|
-
|
|
670
|
+
return finalizeLineInboundContext({
|
|
752
671
|
cfg,
|
|
753
672
|
account,
|
|
754
673
|
event,
|
|
@@ -762,7 +681,6 @@ async function buildLinePostbackContext(params) {
|
|
|
762
681
|
},
|
|
763
682
|
rawBody,
|
|
764
683
|
agentBody,
|
|
765
|
-
timestamp,
|
|
766
684
|
messageSid,
|
|
767
685
|
commandAuthorized,
|
|
768
686
|
channelIngress: await params.resolveChannelIngress?.({
|
|
@@ -775,19 +693,111 @@ async function buildLinePostbackContext(params) {
|
|
|
775
693
|
media: [],
|
|
776
694
|
verboseLog: { kind: "postback" }
|
|
777
695
|
});
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
696
|
+
}
|
|
697
|
+
//#endregion
|
|
698
|
+
//#region extensions/line/src/download.ts
|
|
699
|
+
const CONTENT_READY_MAX_ATTEMPTS = 6;
|
|
700
|
+
const CONTENT_READY_BASE_DELAY_MS = 500;
|
|
701
|
+
const CONTENT_READY_MAX_DELAY_MS = 4e3;
|
|
702
|
+
const CONTENT_DOWNLOAD_TIMEOUT_MS = 15e3;
|
|
703
|
+
const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
|
|
704
|
+
var RetryableLineMediaFetchError = class extends MediaFetchError {
|
|
705
|
+
constructor(message, options) {
|
|
706
|
+
super("fetch_failed", message, options);
|
|
707
|
+
this.name = "RetryableLineMediaFetchError";
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
function contentBackoffDelayMs(attempt) {
|
|
711
|
+
return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
|
|
712
|
+
}
|
|
713
|
+
function lineContentUrl(messageId) {
|
|
714
|
+
return `${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`;
|
|
715
|
+
}
|
|
716
|
+
async function* lineResponseBodyChunks(response, messageId, signal, onComplete) {
|
|
717
|
+
const body = response.body;
|
|
718
|
+
if (!body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
719
|
+
const reader = body.getReader();
|
|
720
|
+
let completed = false;
|
|
721
|
+
try {
|
|
722
|
+
while (true) {
|
|
723
|
+
let chunk;
|
|
724
|
+
try {
|
|
725
|
+
chunk = await reader.read();
|
|
726
|
+
} catch (err) {
|
|
727
|
+
if (signal.aborted) throw signal.reason;
|
|
728
|
+
throw new RetryableLineMediaFetchError(`LINE media response stream failed for message ${messageId}`, { cause: err });
|
|
729
|
+
}
|
|
730
|
+
if (chunk.done) {
|
|
731
|
+
completed = true;
|
|
732
|
+
onComplete();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (chunk.value.byteLength > 0) yield chunk.value;
|
|
736
|
+
}
|
|
737
|
+
} finally {
|
|
738
|
+
if (!completed) await reader.cancel(signal.aborted ? signal.reason : void 0).catch(() => void 0);
|
|
739
|
+
try {
|
|
740
|
+
reader.releaseLock();
|
|
741
|
+
} catch {}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async function fetchLineContentWhenReady(messageId, channelAccessToken, signal) {
|
|
745
|
+
try {
|
|
746
|
+
for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
|
|
747
|
+
const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
|
|
748
|
+
headers: { Authorization: `Bearer ${channelAccessToken}` },
|
|
749
|
+
redirect: "error",
|
|
750
|
+
signal
|
|
751
|
+
});
|
|
752
|
+
if (response.status === 200) {
|
|
753
|
+
if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
|
|
754
|
+
return response;
|
|
755
|
+
}
|
|
756
|
+
await response.body?.cancel().catch(() => void 0);
|
|
757
|
+
if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
|
|
758
|
+
if (attempt < 5) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal });
|
|
759
|
+
}
|
|
760
|
+
} catch (err) {
|
|
761
|
+
if (signal.aborted) throw signal.reason;
|
|
762
|
+
if (err instanceof MediaFetchError) throw err;
|
|
763
|
+
throw new RetryableLineMediaFetchError(`LINE media download failed for message ${messageId}`, { cause: err });
|
|
764
|
+
}
|
|
765
|
+
throw new MediaFetchError("http_error", `LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`, { status: 202 });
|
|
766
|
+
}
|
|
767
|
+
function isRetryableLineInboundMediaError(err) {
|
|
768
|
+
if (err instanceof RetryableLineMediaFetchError) return true;
|
|
769
|
+
if (!(err instanceof MediaFetchError)) return false;
|
|
770
|
+
if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10485760, options) {
|
|
774
|
+
options?.signal?.throwIfAborted();
|
|
775
|
+
const deadlineAbort = new AbortController();
|
|
776
|
+
const signal = options?.signal ? AbortSignal.any([options.signal, deadlineAbort.signal]) : deadlineAbort.signal;
|
|
777
|
+
const deadline = setTimeout(() => {
|
|
778
|
+
deadlineAbort.abort(new RetryableLineMediaFetchError(`LINE media for message ${messageId} did not download within ${CONTENT_DOWNLOAD_TIMEOUT_MS / 1e3} seconds`));
|
|
779
|
+
}, CONTENT_DOWNLOAD_TIMEOUT_MS);
|
|
780
|
+
deadline.unref();
|
|
781
|
+
try {
|
|
782
|
+
const response = await fetchLineContentWhenReady(messageId, channelAccessToken, signal);
|
|
783
|
+
let saved;
|
|
784
|
+
try {
|
|
785
|
+
saved = await saveMediaStream(lineResponseBodyChunks(response, messageId, signal, () => clearTimeout(deadline)), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
|
|
786
|
+
} catch (err) {
|
|
787
|
+
await response.body?.cancel(signal.aborted ? signal.reason : err).catch(() => void 0);
|
|
788
|
+
if (signal.aborted) throw signal.reason;
|
|
789
|
+
throw err;
|
|
790
|
+
}
|
|
791
|
+
options?.signal?.throwIfAborted();
|
|
792
|
+
logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
|
|
793
|
+
return {
|
|
794
|
+
path: saved.path,
|
|
795
|
+
contentType: saved.contentType,
|
|
796
|
+
size: saved.size
|
|
797
|
+
};
|
|
798
|
+
} finally {
|
|
799
|
+
clearTimeout(deadline);
|
|
800
|
+
}
|
|
791
801
|
}
|
|
792
802
|
//#endregion
|
|
793
803
|
//#region extensions/line/src/group-history.ts
|
|
@@ -915,6 +925,10 @@ async function resolveLineEventAdmission(event, context) {
|
|
|
915
925
|
groupId,
|
|
916
926
|
roomId
|
|
917
927
|
});
|
|
928
|
+
if (isGroup && groupConfig?.enabled === false) {
|
|
929
|
+
logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
918
932
|
const rawText = resolveEventRawText(event);
|
|
919
933
|
const requireMention = isGroup ? groupConfig?.requireMention !== false : false;
|
|
920
934
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
@@ -925,29 +939,8 @@ async function resolveLineEventAdmission(event, context) {
|
|
|
925
939
|
});
|
|
926
940
|
const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
|
|
927
941
|
const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom));
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const peerId = groupId ?? roomId ?? userId ?? "unknown";
|
|
931
|
-
const { agentId } = resolveAgentRoute({
|
|
932
|
-
cfg,
|
|
933
|
-
channel: "line",
|
|
934
|
-
accountId: account.accountId,
|
|
935
|
-
peer: {
|
|
936
|
-
kind: "group",
|
|
937
|
-
id: peerId
|
|
938
|
-
}
|
|
939
|
-
});
|
|
940
|
-
const mentionRegexes = buildMentionRegexes(cfg, agentId);
|
|
941
|
-
const wasMentionedByNative = isLineBotMentioned(event.message);
|
|
942
|
-
const wasMentionedByPattern = event.message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
|
|
943
|
-
return {
|
|
944
|
-
canDetectMention: event.message.type === "text",
|
|
945
|
-
wasMentioned: wasMentionedByNative || wasMentionedByPattern,
|
|
946
|
-
explicitlyMentionedBot: wasMentionedByNative,
|
|
947
|
-
hasAnyMention: hasAnyLineMention(event.message),
|
|
948
|
-
implicitMentionKinds: implicitMentionKindWhen("quoted_bot", quotesLineBotMessage(account.accountId, resolveLineQuotedMessageId(event.message)))
|
|
949
|
-
};
|
|
950
|
-
})();
|
|
942
|
+
let preparedRoute;
|
|
943
|
+
let mentionFacts;
|
|
951
944
|
const resolveAccess = async (contextBinding) => await getLineRuntime().channel.inbound.ingress.resolveStable({
|
|
952
945
|
channelId: "line",
|
|
953
946
|
accountId: account.accountId,
|
|
@@ -965,10 +958,6 @@ async function resolveLineEventAdmission(event, context) {
|
|
|
965
958
|
id: (groupId ?? roomId ?? senderId) || "unknown"
|
|
966
959
|
},
|
|
967
960
|
...contextBinding ? { contextBinding } : {},
|
|
968
|
-
...isGroup && groupConfig?.enabled === false ? { route: {
|
|
969
|
-
id: "line:group-config",
|
|
970
|
-
enabled: false
|
|
971
|
-
} } : {},
|
|
972
961
|
mentionFacts,
|
|
973
962
|
event: { kind: event.type === "join" ? "system" : event.type },
|
|
974
963
|
dmPolicy,
|
|
@@ -992,35 +981,53 @@ async function resolveLineEventAdmission(event, context) {
|
|
|
992
981
|
groupOwnerAllowFrom: "none"
|
|
993
982
|
}
|
|
994
983
|
});
|
|
995
|
-
|
|
984
|
+
let access = await resolveAccess();
|
|
985
|
+
if (isGroup && event.type === "message" && isLineEventAdmitted(access)) {
|
|
986
|
+
preparedRoute = await prepareLineInboundRoute({
|
|
987
|
+
source: event.source,
|
|
988
|
+
cfg,
|
|
989
|
+
account
|
|
990
|
+
});
|
|
991
|
+
const mentionRegexes = buildMentionRegexes(cfg, preparedRoute.mentionAgentId);
|
|
992
|
+
const wasMentionedByNative = isLineBotMentioned(event.message);
|
|
993
|
+
const wasMentionedByPattern = event.message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
|
|
994
|
+
mentionFacts = {
|
|
995
|
+
canDetectMention: event.message.type === "text",
|
|
996
|
+
wasMentioned: wasMentionedByNative || wasMentionedByPattern,
|
|
997
|
+
explicitlyMentionedBot: wasMentionedByNative,
|
|
998
|
+
hasAnyMention: hasAnyLineMention(event.message),
|
|
999
|
+
implicitMentionKinds: implicitMentionKindWhen("quoted_bot", quotesLineBotMessage(account.accountId, resolveLineQuotedMessageId(event.message)))
|
|
1000
|
+
};
|
|
1001
|
+
access = await resolveAccess();
|
|
1002
|
+
}
|
|
996
1003
|
warnMissingProviderGroupPolicyFallbackOnce({
|
|
997
1004
|
providerMissingFallbackApplied,
|
|
998
1005
|
providerKey: "line",
|
|
999
1006
|
accountId: account.accountId,
|
|
1000
1007
|
log: (message) => logVerbose(message)
|
|
1001
1008
|
});
|
|
1002
|
-
if (event.type === "join") return
|
|
1009
|
+
if (event.type === "join") return groupPolicy !== "disabled" && (groupPolicy !== "allowlist" || access.state.allowlists.group.hasMatchableEntries) ? {
|
|
1003
1010
|
access,
|
|
1004
1011
|
resolveBoundAccess: resolveAccess
|
|
1005
1012
|
} : null;
|
|
1006
|
-
if (isLineEventAdmitted(access))
|
|
1007
|
-
|
|
1008
|
-
resolveBoundAccess: resolveAccess,
|
|
1009
|
-
mentions: mentionFacts ? {
|
|
1013
|
+
if (isLineEventAdmitted(access)) {
|
|
1014
|
+
const mentions = mentionFacts ? {
|
|
1010
1015
|
...mentionFacts,
|
|
1011
1016
|
wasMentioned: access.activationAccess.effectiveWasMentioned ?? mentionFacts.wasMentioned,
|
|
1012
1017
|
requireMention
|
|
1013
|
-
} : void 0
|
|
1014
|
-
|
|
1018
|
+
} : void 0;
|
|
1019
|
+
return {
|
|
1020
|
+
access,
|
|
1021
|
+
resolveBoundAccess: resolveAccess,
|
|
1022
|
+
mentions,
|
|
1023
|
+
preparedRoute
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1015
1026
|
if (access.senderAccess.decision === "allow") {
|
|
1016
1027
|
logVerbose(`Blocked line event (${access.ingress.reasonCode})`);
|
|
1017
1028
|
return null;
|
|
1018
1029
|
}
|
|
1019
1030
|
if (isGroup) {
|
|
1020
|
-
if (groupConfig?.enabled === false) {
|
|
1021
|
-
logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
|
|
1022
|
-
return null;
|
|
1023
|
-
}
|
|
1024
1031
|
if (groupConfig?.allowFrom !== void 0) {
|
|
1025
1032
|
if (!senderId) {
|
|
1026
1033
|
logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
|
|
@@ -1149,6 +1156,7 @@ async function handleMessageEvent(event, context, setParts) {
|
|
|
1149
1156
|
...context.missingParts === void 0 ? {} : { missingParts: context.missingParts },
|
|
1150
1157
|
cfg,
|
|
1151
1158
|
account,
|
|
1159
|
+
preparedRoute: decision.preparedRoute,
|
|
1152
1160
|
commandAuthorized: decision.access.commandAccess.authorized,
|
|
1153
1161
|
resolveChannelIngress: decision.resolveBoundAccess,
|
|
1154
1162
|
inboundHistory: historyReservation.inboundHistory,
|
|
@@ -1167,14 +1175,6 @@ async function handleMessageEvent(event, context, setParts) {
|
|
|
1167
1175
|
historyReservation.release();
|
|
1168
1176
|
}
|
|
1169
1177
|
}
|
|
1170
|
-
async function handleFollowEvent(event, _context) {
|
|
1171
|
-
const { userId } = getLineSourceInfo(event.source);
|
|
1172
|
-
logVerbose(`line: user ${userId ?? "unknown"} followed`);
|
|
1173
|
-
}
|
|
1174
|
-
async function handleUnfollowEvent(event, _context) {
|
|
1175
|
-
const { userId } = getLineSourceInfo(event.source);
|
|
1176
|
-
logVerbose(`line: user ${userId ?? "unknown"} unfollowed`);
|
|
1177
|
-
}
|
|
1178
1178
|
async function handleJoinEvent(event, context) {
|
|
1179
1179
|
const { groupId, roomId, isGroup } = getLineSourceInfo(event.source);
|
|
1180
1180
|
const conversationId = groupId ?? roomId;
|
|
@@ -1212,10 +1212,6 @@ async function handleJoinEvent(event, context) {
|
|
|
1212
1212
|
}
|
|
1213
1213
|
});
|
|
1214
1214
|
}
|
|
1215
|
-
async function handleLeaveEvent(event, _context) {
|
|
1216
|
-
const { groupId, roomId } = getLineSourceInfo(event.source);
|
|
1217
|
-
logVerbose(`line: bot left ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
|
|
1218
|
-
}
|
|
1219
1215
|
/** What a tap that did not answer the question has to tell the person who tapped. */
|
|
1220
1216
|
function lineQuestionOutcomeNotice(status) {
|
|
1221
1217
|
if (status === "already-terminal") return "That question is no longer waiting for an answer.";
|
|
@@ -1290,17 +1286,19 @@ async function handleLineWebhookEvent(event, context, setParts = []) {
|
|
|
1290
1286
|
await handleMessageEvent(event, context, setParts.filter((part) => part.type === "message"));
|
|
1291
1287
|
break;
|
|
1292
1288
|
case "follow":
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
await handleUnfollowEvent(event, context);
|
|
1289
|
+
case "unfollow": {
|
|
1290
|
+
const { userId } = getLineSourceInfo(event.source);
|
|
1291
|
+
logVerbose(`line: user ${userId ?? "unknown"} ${event.type}ed`);
|
|
1297
1292
|
break;
|
|
1293
|
+
}
|
|
1298
1294
|
case "join":
|
|
1299
1295
|
await handleJoinEvent(event, context);
|
|
1300
1296
|
break;
|
|
1301
|
-
case "leave":
|
|
1302
|
-
|
|
1297
|
+
case "leave": {
|
|
1298
|
+
const { groupId, roomId } = getLineSourceInfo(event.source);
|
|
1299
|
+
logVerbose(`line: bot left ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
|
|
1303
1300
|
break;
|
|
1301
|
+
}
|
|
1304
1302
|
case "postback":
|
|
1305
1303
|
await handlePostbackEvent(event, context);
|
|
1306
1304
|
break;
|
|
@@ -1310,40 +1308,17 @@ async function handleLineWebhookEvent(event, context, setParts = []) {
|
|
|
1310
1308
|
//#endregion
|
|
1311
1309
|
//#region extensions/line/src/inbound-image-set.ts
|
|
1312
1310
|
const IMAGE_SET_FLUSH_DELAY_MS = 4e3;
|
|
1313
|
-
/**
|
|
1314
|
-
* Ordered by the index the sender picked, falling back to arrival.
|
|
1315
|
-
*
|
|
1316
|
-
* `index` is optional per part in LINE's contract, so a set can arrive partly
|
|
1317
|
-
* indexed. Choosing the key per pair would make the comparator intransitive and
|
|
1318
|
-
* the resulting order depend on insertion; ranking unindexed parts last keeps
|
|
1319
|
-
* one total order for every mix.
|
|
1320
|
-
*/
|
|
1321
1311
|
function orderedParts(pending) {
|
|
1322
1312
|
return [...pending.parts.values()].toSorted((left, right) => (left.index ?? Number.MAX_SAFE_INTEGER) - (right.index ?? Number.MAX_SAFE_INTEGER) || left.arrivedAt - right.arrivedAt);
|
|
1323
1313
|
}
|
|
1324
|
-
/**
|
|
1325
|
-
* Buffers the parts of a LINE image set until the set is whole.
|
|
1326
|
-
*
|
|
1327
|
-
* The first part becomes the set's holder: its call does not resolve until the
|
|
1328
|
-
* set completes or its wait expires, and it is the one that delivers. Keeping
|
|
1329
|
-
* that call open is what keeps the combined turn inside a live ingress
|
|
1330
|
-
* adoption - a turn dispatched after every part had returned is refused by
|
|
1331
|
-
* admission, so a set that never completes would never be delivered at all.
|
|
1332
|
-
*/
|
|
1333
1314
|
function createLineImageSetIngressBuffer() {
|
|
1334
1315
|
const pendingBySet = /* @__PURE__ */ new Map();
|
|
1335
1316
|
const deliveredBySet = /* @__PURE__ */ new Map();
|
|
1336
1317
|
const pendingKey = (laneKey, senderKey, setId) => `${laneKey}\u0000${senderKey}\u0000${setId}`;
|
|
1337
1318
|
const laneChain = /* @__PURE__ */ new Map();
|
|
1338
1319
|
const enterLane = async (laneKey) => {
|
|
1339
|
-
|
|
1340
|
-
const
|
|
1341
|
-
release = resolve;
|
|
1342
|
-
});
|
|
1343
|
-
let entered = () => {};
|
|
1344
|
-
const turn = new Promise((resolve) => {
|
|
1345
|
-
entered = resolve;
|
|
1346
|
-
});
|
|
1320
|
+
const { promise: held, resolve: release } = createDeferred();
|
|
1321
|
+
const { promise: turn, resolve: entered } = createDeferred();
|
|
1347
1322
|
enqueueKeyedTask({
|
|
1348
1323
|
tails: laneChain,
|
|
1349
1324
|
key: laneKey,
|
|
@@ -1378,10 +1353,7 @@ function createLineImageSetIngressBuffer() {
|
|
|
1378
1353
|
}
|
|
1379
1354
|
return null;
|
|
1380
1355
|
}
|
|
1381
|
-
|
|
1382
|
-
const whole = new Promise((resolve) => {
|
|
1383
|
-
release = resolve;
|
|
1384
|
-
});
|
|
1356
|
+
const { promise: whole, resolve: release } = createDeferred();
|
|
1385
1357
|
const pending = {
|
|
1386
1358
|
parts: /* @__PURE__ */ new Map([[input.messageId, part]]),
|
|
1387
1359
|
total: input.total,
|
|
@@ -1772,9 +1744,9 @@ function parseLineWebhookBody(rawBody) {
|
|
|
1772
1744
|
//#endregion
|
|
1773
1745
|
//#region extensions/line/src/webhook-node.ts
|
|
1774
1746
|
const LINE_WEBHOOK_MAX_BODY_BYTES = 1048576;
|
|
1775
|
-
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES
|
|
1776
|
-
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS
|
|
1777
|
-
async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS
|
|
1747
|
+
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
|
|
1748
|
+
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
|
|
1749
|
+
async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS) {
|
|
1778
1750
|
return await readRequestBodyWithLimit(req, {
|
|
1779
1751
|
maxBytes,
|
|
1780
1752
|
timeoutMs,
|
|
@@ -1797,6 +1769,12 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1797
1769
|
const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
|
|
1798
1770
|
const readBody = params.readBody ?? readLineWebhookRequestBody;
|
|
1799
1771
|
return async (req, res) => {
|
|
1772
|
+
const targets = params.getTargets();
|
|
1773
|
+
if (req.method !== "POST" && targets.length === 0) {
|
|
1774
|
+
res.statusCode = 404;
|
|
1775
|
+
res.end("Not Found");
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1800
1778
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
1801
1779
|
if (req.method === "HEAD") {
|
|
1802
1780
|
res.statusCode = 204;
|
|
@@ -1825,14 +1803,22 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1825
1803
|
res.end(JSON.stringify({ error: "Missing X-Line-Signature header" }));
|
|
1826
1804
|
return;
|
|
1827
1805
|
}
|
|
1828
|
-
const rawBody = await readBody(req, Math.min(maxBodyBytes, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES
|
|
1829
|
-
|
|
1806
|
+
const rawBody = await readBody(req, Math.min(maxBodyBytes, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES), LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS);
|
|
1807
|
+
const match = resolveSingleWebhookTarget(targets, (target) => validateLineSignature(rawBody, signature, target.channelSecret));
|
|
1808
|
+
if (match.kind === "none") {
|
|
1830
1809
|
logVerbose("line: webhook signature validation failed");
|
|
1831
1810
|
res.statusCode = 401;
|
|
1832
1811
|
res.setHeader("Content-Type", "application/json");
|
|
1833
1812
|
res.end(JSON.stringify({ error: "Invalid signature" }));
|
|
1834
1813
|
return;
|
|
1835
1814
|
}
|
|
1815
|
+
if (match.kind === "ambiguous") {
|
|
1816
|
+
logVerbose("line: webhook signature matched multiple accounts");
|
|
1817
|
+
res.statusCode = 401;
|
|
1818
|
+
res.setHeader("Content-Type", "application/json");
|
|
1819
|
+
res.end(JSON.stringify({ error: "Ambiguous webhook target" }));
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1836
1822
|
const body = parseLineWebhookBody(rawBody);
|
|
1837
1823
|
if (!body) {
|
|
1838
1824
|
res.statusCode = 400;
|
|
@@ -1840,10 +1826,9 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1840
1826
|
res.end(JSON.stringify({ error: "Invalid webhook payload" }));
|
|
1841
1827
|
return;
|
|
1842
1828
|
}
|
|
1843
|
-
params.onRequestAuthenticated?.();
|
|
1844
1829
|
if (body.events && body.events.length > 0) {
|
|
1845
1830
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1846
|
-
await
|
|
1831
|
+
if (await match.target.bot.handleWebhook(body) === "durable") res.setHeader("x-openclaw-delivery-accepted", "durable");
|
|
1847
1832
|
}
|
|
1848
1833
|
res.statusCode = 200;
|
|
1849
1834
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -1862,8 +1847,6 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1862
1847
|
//#endregion
|
|
1863
1848
|
//#region extensions/line/src/monitor.ts
|
|
1864
1849
|
const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
|
|
1865
|
-
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 65536;
|
|
1866
|
-
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
|
|
1867
1850
|
async function registerLineWebhookTarget(params, bot) {
|
|
1868
1851
|
try {
|
|
1869
1852
|
return registerWebhookTargetWithPluginRoute(params);
|
|
@@ -2016,10 +1999,9 @@ async function monitorLineProvider(opts) {
|
|
|
2016
1999
|
});
|
|
2017
2000
|
const normalizedPath = resolveLineWebhookPath(webhookPath);
|
|
2018
2001
|
const webhookRouteKey = canonicalizeWebhookRouteKey(normalizedPath);
|
|
2019
|
-
const
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
runtime: target.runtime
|
|
2002
|
+
const handleWebhook = createLineNodeWebhookHandler({
|
|
2003
|
+
getTargets: () => lineWebhookTargets.get(webhookRouteKey) ?? [],
|
|
2004
|
+
runtime
|
|
2023
2005
|
});
|
|
2024
2006
|
const { unregister: unregisterHttp } = await registerLineWebhookTarget({
|
|
2025
2007
|
targetsByPath: lineWebhookTargets,
|
|
@@ -2038,15 +2020,8 @@ async function monitorLineProvider(opts) {
|
|
|
2038
2020
|
log: (msg) => logVerbose(msg),
|
|
2039
2021
|
throwOnFailure: true,
|
|
2040
2022
|
handler: async (req, res) => {
|
|
2041
|
-
const targets = lineWebhookTargets.get(webhookRouteKey) ?? [];
|
|
2042
|
-
const firstTarget = targets[0];
|
|
2043
2023
|
if (req.method !== "POST") {
|
|
2044
|
-
|
|
2045
|
-
res.statusCode = 404;
|
|
2046
|
-
res.end("Not Found");
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
await createScopedLineWebhookHandler(firstTarget)(req, res);
|
|
2024
|
+
await handleWebhook(req, res);
|
|
2050
2025
|
return;
|
|
2051
2026
|
}
|
|
2052
2027
|
const requestLifecycle = beginWebhookRequestPipelineOrReject({
|
|
@@ -2057,53 +2032,7 @@ async function monitorLineProvider(opts) {
|
|
|
2057
2032
|
});
|
|
2058
2033
|
if (!requestLifecycle.ok) return;
|
|
2059
2034
|
try {
|
|
2060
|
-
|
|
2061
|
-
const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
|
|
2062
|
-
if (!signature) {
|
|
2063
|
-
logVerbose("line: webhook missing X-Line-Signature header");
|
|
2064
|
-
res.statusCode = 400;
|
|
2065
|
-
res.setHeader("Content-Type", "application/json");
|
|
2066
|
-
res.end(JSON.stringify({ error: "Missing X-Line-Signature header" }));
|
|
2067
|
-
return;
|
|
2068
|
-
}
|
|
2069
|
-
const rawBody = await readLineWebhookRequestBody(req, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES, LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS);
|
|
2070
|
-
const match = resolveSingleWebhookTarget(targets, (target) => validateLineSignature(rawBody, signature, target.channelSecret));
|
|
2071
|
-
if (match.kind === "none") {
|
|
2072
|
-
logVerbose("line: webhook signature validation failed");
|
|
2073
|
-
res.statusCode = 401;
|
|
2074
|
-
res.setHeader("Content-Type", "application/json");
|
|
2075
|
-
res.end(JSON.stringify({ error: "Invalid signature" }));
|
|
2076
|
-
return;
|
|
2077
|
-
}
|
|
2078
|
-
if (match.kind === "ambiguous") {
|
|
2079
|
-
logVerbose("line: webhook signature matched multiple accounts");
|
|
2080
|
-
res.statusCode = 401;
|
|
2081
|
-
res.setHeader("Content-Type", "application/json");
|
|
2082
|
-
res.end(JSON.stringify({ error: "Ambiguous webhook target" }));
|
|
2083
|
-
return;
|
|
2084
|
-
}
|
|
2085
|
-
const body = parseLineWebhookBody(rawBody);
|
|
2086
|
-
if (!body) {
|
|
2087
|
-
res.statusCode = 400;
|
|
2088
|
-
res.setHeader("Content-Type", "application/json");
|
|
2089
|
-
res.end(JSON.stringify({ error: "Invalid webhook payload" }));
|
|
2090
|
-
return;
|
|
2091
|
-
}
|
|
2092
|
-
if (body.events && body.events.length > 0) {
|
|
2093
|
-
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
2094
|
-
if (await match.target.bot.handleWebhook(body) === "durable") res.setHeader("x-openclaw-delivery-accepted", "durable");
|
|
2095
|
-
}
|
|
2096
|
-
res.statusCode = 200;
|
|
2097
|
-
res.setHeader("Content-Type", "application/json");
|
|
2098
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
2099
|
-
} catch (err) {
|
|
2100
|
-
if (await rejectLineWebhookRequest(req, res, err)) return;
|
|
2101
|
-
runtime.error?.(danger(`line webhook error: ${formatErrorMessage(err)}`));
|
|
2102
|
-
if (!res.headersSent) {
|
|
2103
|
-
res.statusCode = 500;
|
|
2104
|
-
res.setHeader("Content-Type", "application/json");
|
|
2105
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
2106
|
-
}
|
|
2035
|
+
await handleWebhook(req, res);
|
|
2107
2036
|
} finally {
|
|
2108
2037
|
requestLifecycle.release();
|
|
2109
2038
|
}
|
|
@@ -2142,4 +2071,4 @@ async function monitorLineProvider(opts) {
|
|
|
2142
2071
|
};
|
|
2143
2072
|
}
|
|
2144
2073
|
//#endregion
|
|
2145
|
-
export {
|
|
2074
|
+
export { monitorLineProvider as t };
|