@openclaw/line 2026.7.2-beta.2 → 2026.7.2-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{accounts-DJVOv1JI.js → accounts-BUBOoW5Q.js} +95 -33
- package/dist/api.js +2 -2
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel-BHR-GoJ0.js → channel-uT5RTtpi.js} +15 -7
- package/dist/{channel.runtime-DFgZ_Xwn.js → channel.runtime-BqNglqnr.js} +2 -2
- package/dist/contract-api.js +1 -1
- package/dist/{markdown-to-line-XceqeKRP.js → markdown-to-line-D1vapcdP.js} +11 -2
- package/dist/{monitor-Db2rZsSX.js → monitor-Bk-lpoOZ.js} +273 -113
- package/dist/monitor.runtime-CHlqxGVL.js +2 -0
- package/dist/{outbound.runtime-DnpU0Cms.js → outbound.runtime-ByZ0Dsvu.js} +1 -1
- package/dist/runtime-api.js +5 -21
- package/dist/setup-api.js +1 -1
- package/dist/{setup-surface-B7W6pwn3.js → setup-surface-BEt4mazl.js} +2 -1
- package/npm-shrinkwrap.json +3 -3
- package/package.json +4 -4
- package/dist/monitor.runtime-CZuQgsEE.js +0 -2
|
@@ -1,55 +1,113 @@
|
|
|
1
1
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
2
|
import { resolveAccountEntry } from "openclaw/plugin-sdk/account-resolution";
|
|
3
|
-
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/
|
|
3
|
+
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
|
|
4
4
|
//#region extensions/line/src/accounts.ts
|
|
5
|
-
function
|
|
6
|
-
return tryReadSecretFileSync(filePath, "LINE credential file", { rejectSymlink: true });
|
|
5
|
+
function readCredentialFile(filePath, configPath) {
|
|
6
|
+
return tryReadSecretFileSync(filePath, "LINE credential file", { rejectSymlink: true }, { configPath });
|
|
7
7
|
}
|
|
8
8
|
function resolveToken(params) {
|
|
9
9
|
const { accountId, baseConfig, accountConfig } = params;
|
|
10
10
|
if (accountConfig?.channelAccessToken?.trim()) return {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const accountFileToken = readFileIfExists(accountConfig?.tokenFile);
|
|
15
|
-
if (accountFileToken) return {
|
|
16
|
-
token: accountFileToken,
|
|
17
|
-
tokenSource: "file"
|
|
11
|
+
value: accountConfig.channelAccessToken.trim(),
|
|
12
|
+
source: "config",
|
|
13
|
+
status: "available"
|
|
18
14
|
};
|
|
15
|
+
if (accountConfig?.tokenFile?.trim()) {
|
|
16
|
+
const result = readCredentialFile(accountConfig.tokenFile, `channels.line.accounts.${accountId}.tokenFile`);
|
|
17
|
+
return result.status === "available" ? {
|
|
18
|
+
value: result.value,
|
|
19
|
+
source: "file",
|
|
20
|
+
status: "available"
|
|
21
|
+
} : {
|
|
22
|
+
value: "",
|
|
23
|
+
source: "file",
|
|
24
|
+
status: "configured_unavailable",
|
|
25
|
+
diagnostic: result.diagnostic
|
|
26
|
+
};
|
|
27
|
+
}
|
|
19
28
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
20
29
|
if (baseConfig?.channelAccessToken?.trim()) return {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const baseFileToken = readFileIfExists(baseConfig?.tokenFile);
|
|
25
|
-
if (baseFileToken) return {
|
|
26
|
-
token: baseFileToken,
|
|
27
|
-
tokenSource: "file"
|
|
30
|
+
value: baseConfig.channelAccessToken.trim(),
|
|
31
|
+
source: "config",
|
|
32
|
+
status: "available"
|
|
28
33
|
};
|
|
34
|
+
if (baseConfig?.tokenFile?.trim()) {
|
|
35
|
+
const result = readCredentialFile(baseConfig.tokenFile, "channels.line.tokenFile");
|
|
36
|
+
return result.status === "available" ? {
|
|
37
|
+
value: result.value,
|
|
38
|
+
source: "file",
|
|
39
|
+
status: "available"
|
|
40
|
+
} : {
|
|
41
|
+
value: "",
|
|
42
|
+
source: "file",
|
|
43
|
+
status: "configured_unavailable",
|
|
44
|
+
diagnostic: result.diagnostic
|
|
45
|
+
};
|
|
46
|
+
}
|
|
29
47
|
const envToken = process.env.LINE_CHANNEL_ACCESS_TOKEN?.trim();
|
|
30
48
|
if (envToken) return {
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
value: envToken,
|
|
50
|
+
source: "env",
|
|
51
|
+
status: "available"
|
|
33
52
|
};
|
|
34
53
|
}
|
|
35
54
|
return {
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
value: "",
|
|
56
|
+
source: "none",
|
|
57
|
+
status: "missing"
|
|
38
58
|
};
|
|
39
59
|
}
|
|
40
60
|
function resolveSecret(params) {
|
|
41
61
|
const { accountId, baseConfig, accountConfig } = params;
|
|
42
|
-
if (accountConfig?.channelSecret?.trim()) return
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
if (accountConfig?.channelSecret?.trim()) return {
|
|
63
|
+
value: accountConfig.channelSecret.trim(),
|
|
64
|
+
source: "config",
|
|
65
|
+
status: "available"
|
|
66
|
+
};
|
|
67
|
+
if (accountConfig?.secretFile?.trim()) {
|
|
68
|
+
const result = readCredentialFile(accountConfig.secretFile, `channels.line.accounts.${accountId}.secretFile`);
|
|
69
|
+
return result.status === "available" ? {
|
|
70
|
+
value: result.value,
|
|
71
|
+
source: "file",
|
|
72
|
+
status: "available"
|
|
73
|
+
} : {
|
|
74
|
+
value: "",
|
|
75
|
+
source: "file",
|
|
76
|
+
status: "configured_unavailable",
|
|
77
|
+
diagnostic: result.diagnostic
|
|
78
|
+
};
|
|
79
|
+
}
|
|
45
80
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
46
|
-
if (baseConfig?.channelSecret?.trim()) return
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
if (baseConfig?.channelSecret?.trim()) return {
|
|
82
|
+
value: baseConfig.channelSecret.trim(),
|
|
83
|
+
source: "config",
|
|
84
|
+
status: "available"
|
|
85
|
+
};
|
|
86
|
+
if (baseConfig?.secretFile?.trim()) {
|
|
87
|
+
const result = readCredentialFile(baseConfig.secretFile, "channels.line.secretFile");
|
|
88
|
+
return result.status === "available" ? {
|
|
89
|
+
value: result.value,
|
|
90
|
+
source: "file",
|
|
91
|
+
status: "available"
|
|
92
|
+
} : {
|
|
93
|
+
value: "",
|
|
94
|
+
source: "file",
|
|
95
|
+
status: "configured_unavailable",
|
|
96
|
+
diagnostic: result.diagnostic
|
|
97
|
+
};
|
|
98
|
+
}
|
|
49
99
|
const envSecret = process.env.LINE_CHANNEL_SECRET?.trim();
|
|
50
|
-
if (envSecret) return
|
|
100
|
+
if (envSecret) return {
|
|
101
|
+
value: envSecret,
|
|
102
|
+
source: "env",
|
|
103
|
+
status: "available"
|
|
104
|
+
};
|
|
51
105
|
}
|
|
52
|
-
return
|
|
106
|
+
return {
|
|
107
|
+
value: "",
|
|
108
|
+
source: "none",
|
|
109
|
+
status: "missing"
|
|
110
|
+
};
|
|
53
111
|
}
|
|
54
112
|
function resolveLineAccount(params) {
|
|
55
113
|
const cfg = params.cfg;
|
|
@@ -57,7 +115,7 @@ function resolveLineAccount(params) {
|
|
|
57
115
|
const lineConfig = cfg.channels?.line;
|
|
58
116
|
const accounts = lineConfig?.accounts;
|
|
59
117
|
const accountConfig = resolveAccountEntry(accounts, accountId);
|
|
60
|
-
const
|
|
118
|
+
const token = resolveToken({
|
|
61
119
|
accountId,
|
|
62
120
|
baseConfig: lineConfig,
|
|
63
121
|
accountConfig
|
|
@@ -79,9 +137,13 @@ function resolveLineAccount(params) {
|
|
|
79
137
|
accountId,
|
|
80
138
|
name: accountConfig?.name ?? (accountId === DEFAULT_ACCOUNT_ID ? lineConfig?.name : void 0),
|
|
81
139
|
enabled,
|
|
82
|
-
channelAccessToken: token,
|
|
83
|
-
channelSecret: secret,
|
|
84
|
-
tokenSource,
|
|
140
|
+
channelAccessToken: token.value,
|
|
141
|
+
channelSecret: secret.value,
|
|
142
|
+
tokenSource: token.source,
|
|
143
|
+
signingSecretSource: secret.source,
|
|
144
|
+
tokenStatus: token.status,
|
|
145
|
+
signingSecretStatus: secret.status,
|
|
146
|
+
...[token.diagnostic, secret.diagnostic].some(Boolean) ? { credentialDiagnostics: [token.diagnostic, secret.diagnostic].filter((diagnostic) => Boolean(diagnostic)) } : {},
|
|
85
147
|
config: mergedConfig
|
|
86
148
|
};
|
|
87
149
|
}
|
package/dist/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-
|
|
2
|
-
import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-
|
|
1
|
+
import { n as lineChannelPluginCommon, t as linePlugin } from "./channel-uT5RTtpi.js";
|
|
2
|
+
import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-BEt4mazl.js";
|
|
3
3
|
//#region extensions/line/src/channel.setup.ts
|
|
4
4
|
const lineSetupPlugin = {
|
|
5
5
|
id: "line",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as linePlugin } from "./channel-
|
|
1
|
+
import { t as linePlugin } from "./channel-uT5RTtpi.js";
|
|
2
2
|
export { linePlugin };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-
|
|
2
|
-
import { n as lineSetupAdapter, r as hasLineCredentials, t as lineSetupWizard } from "./setup-surface-
|
|
1
|
+
import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BUBOoW5Q.js";
|
|
2
|
+
import { n as lineSetupAdapter, r as hasLineCredentials, t as lineSetupWizard } from "./setup-surface-BEt4mazl.js";
|
|
3
3
|
import { a as buildLineMediaMessage, d as LineChannelConfigSchema, i as buildLineQuickReplyFallbackText, l as getLineRuntime, n as parseLineDirectives, o as hasLineSpecificMediaOptions, p as resolveExactLineGroupConfigKey, r as createLineSendReceipt, s as resolveLineOutboundMedia, t as hasLineDirectives } from "./reply-payload-transform-r9sa-TYT.js";
|
|
4
4
|
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
5
5
|
import { buildDmGroupAccountAllowlistAdapter, createFlatAllowlistOverrideResolver } from "openclaw/plugin-sdk/allowlist-config-edit";
|
|
@@ -8,10 +8,10 @@ import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing
|
|
|
8
8
|
import { buildChannelGroupsScopeTree, createRestrictSendersChannelSecurity, resolveScopeRequireMention } from "openclaw/plugin-sdk/channel-policy";
|
|
9
9
|
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
|
10
10
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1 } from "openclaw/plugin-sdk/account-id";
|
|
11
|
-
import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
|
|
12
11
|
import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
13
12
|
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
14
13
|
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
14
|
+
import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
|
|
15
15
|
import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
|
|
16
16
|
import { createAttachedChannelResultAdapter, createEmptyChannelResult } from "openclaw/plugin-sdk/channel-send-result";
|
|
17
17
|
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
|
|
@@ -104,14 +104,19 @@ const lineChannelPluginCommon = {
|
|
|
104
104
|
describeAccount: (account) => describeWebhookAccountSnapshot({
|
|
105
105
|
account,
|
|
106
106
|
configured: hasLineCredentials(account),
|
|
107
|
-
extra: {
|
|
107
|
+
extra: {
|
|
108
|
+
tokenSource: account.tokenSource ?? void 0,
|
|
109
|
+
signingSecretSource: account.signingSecretSource ?? void 0,
|
|
110
|
+
tokenStatus: account.tokenStatus,
|
|
111
|
+
signingSecretStatus: account.signingSecretStatus
|
|
112
|
+
}
|
|
108
113
|
})
|
|
109
114
|
}
|
|
110
115
|
};
|
|
111
116
|
//#endregion
|
|
112
117
|
//#region extensions/line/src/gateway.ts
|
|
113
118
|
const loadLineProbeRuntime$1 = createLazyRuntimeModule(() => import("./probe.runtime-CRhVGqwl.js"));
|
|
114
|
-
const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-
|
|
119
|
+
const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime-CHlqxGVL.js"));
|
|
115
120
|
const lineGatewayAdapter = {
|
|
116
121
|
startAccount: async (ctx) => {
|
|
117
122
|
const account = ctx.account;
|
|
@@ -226,7 +231,7 @@ function inferLineTargetChatType(target) {
|
|
|
226
231
|
}
|
|
227
232
|
//#endregion
|
|
228
233
|
//#region extensions/line/src/outbound.ts
|
|
229
|
-
const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-
|
|
234
|
+
const loadLineOutboundRuntime = createLazyRuntimeModule(() => import("./outbound.runtime-ByZ0Dsvu.js"));
|
|
230
235
|
const lineOutboundAdapter = {
|
|
231
236
|
deliveryMode: "direct",
|
|
232
237
|
chunker: (text, limit) => getLineRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
@@ -514,13 +519,16 @@ const lineStatusAdapter = createComputedAccountStatusAdapter({
|
|
|
514
519
|
configured: hasLineCredentials(account),
|
|
515
520
|
extra: {
|
|
516
521
|
tokenSource: account.tokenSource,
|
|
522
|
+
signingSecretSource: account.signingSecretSource,
|
|
523
|
+
tokenStatus: account.tokenStatus,
|
|
524
|
+
signingSecretStatus: account.signingSecretStatus,
|
|
517
525
|
mode: "webhook"
|
|
518
526
|
}
|
|
519
527
|
})
|
|
520
528
|
});
|
|
521
529
|
//#endregion
|
|
522
530
|
//#region extensions/line/src/channel.ts
|
|
523
|
-
const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-
|
|
531
|
+
const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-BqNglqnr.js"));
|
|
524
532
|
const lineSecurityAdapter = createRestrictSendersChannelSecurity({
|
|
525
533
|
channelKey: "line",
|
|
526
534
|
resolveDmPolicy: (account) => account.config.dmPolicy,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as monitorLineProvider } from "./monitor-
|
|
1
|
+
import { t as monitorLineProvider } from "./monitor-Bk-lpoOZ.js";
|
|
2
2
|
import { t as probeLineBot } from "./probe-BslD77tJ.js";
|
|
3
|
-
import { S as pushMessageLine } from "./markdown-to-line-
|
|
3
|
+
import { S as pushMessageLine } from "./markdown-to-line-D1vapcdP.js";
|
|
4
4
|
export { monitorLineProvider, probeLineBot, pushMessageLine };
|
package/dist/contract-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-
|
|
1
|
+
import { i as resolveLineAccount, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BUBOoW5Q.js";
|
|
2
2
|
export { listLineAccountIds, resolveDefaultLineAccountId, resolveLineAccount };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as resolveLineAccount } from "./accounts-
|
|
1
|
+
import { i as resolveLineAccount } from "./accounts-BUBOoW5Q.js";
|
|
2
2
|
import { c as validateLineMediaUrl, r as createLineSendReceipt } from "./reply-payload-transform-r9sa-TYT.js";
|
|
3
3
|
import { c as uriAction, d as createReceiptCard, o as messageAction, s as postbackAction, t as toFlexMessage } from "./flex-templates-K1lS9i8L.js";
|
|
4
4
|
import { stripMarkdown, stripMarkdown as stripMarkdown$1 } from "openclaw/plugin-sdk/text-chunking";
|
|
@@ -7,6 +7,7 @@ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
|
7
7
|
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
|
8
8
|
import { messagingApi } from "@line/bot-sdk";
|
|
9
9
|
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
|
10
|
+
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
10
11
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
11
12
|
//#region extensions/line/src/template-messages.ts
|
|
12
13
|
const COMPACT_TEMPLATE_TEXT_LIMIT = 60;
|
|
@@ -221,6 +222,14 @@ function resolveLineChannelAccessToken(explicit, params) {
|
|
|
221
222
|
//#region extensions/line/src/send.ts
|
|
222
223
|
const userProfileCache = /* @__PURE__ */ new Map();
|
|
223
224
|
const PROFILE_CACHE_TTL_MS = 300 * 1e3;
|
|
225
|
+
const PROFILE_CACHE_MAX_ENTRIES = 1e3;
|
|
226
|
+
function cacheUserProfile(userId, profile) {
|
|
227
|
+
userProfileCache.delete(userId);
|
|
228
|
+
userProfileCache.set(userId, profile);
|
|
229
|
+
if (userProfileCache.size <= PROFILE_CACHE_MAX_ENTRIES) return;
|
|
230
|
+
for (const [key, cached] of userProfileCache) if (profile.fetchedAt - cached.fetchedAt >= PROFILE_CACHE_TTL_MS) userProfileCache.delete(key);
|
|
231
|
+
pruneMapToMaxSize(userProfileCache, PROFILE_CACHE_MAX_ENTRIES);
|
|
232
|
+
}
|
|
224
233
|
function normalizeTarget(to) {
|
|
225
234
|
const trimmed = to.trim();
|
|
226
235
|
if (!trimmed) throw new Error("Recipient is required for LINE sends");
|
|
@@ -469,7 +478,7 @@ async function getUserProfile(userId, opts) {
|
|
|
469
478
|
displayName: profile.displayName,
|
|
470
479
|
pictureUrl: profile.pictureUrl
|
|
471
480
|
};
|
|
472
|
-
|
|
481
|
+
cacheUserProfile(userId, {
|
|
473
482
|
...result,
|
|
474
483
|
fetchedAt: Date.now()
|
|
475
484
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-
|
|
1
|
+
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BUBOoW5Q.js";
|
|
2
2
|
import { a as buildLineMediaMessage, i as buildLineQuickReplyFallbackText, l as getLineRuntime, m as resolveLineGroupConfigEntry, o as hasLineSpecificMediaOptions } from "./reply-payload-transform-r9sa-TYT.js";
|
|
3
|
-
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-
|
|
3
|
+
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-D1vapcdP.js";
|
|
4
4
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
5
5
|
import { normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
|
-
import {
|
|
6
|
+
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressDrain } from "openclaw/plugin-sdk/channel-outbound";
|
|
7
7
|
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
8
8
|
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
9
9
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
@@ -25,7 +25,6 @@ import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
|
25
25
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
26
26
|
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
|
|
27
27
|
import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
28
|
-
import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
29
28
|
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
|
30
29
|
import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
31
30
|
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
|
@@ -458,6 +457,7 @@ async function finalizeLineInboundContext(params) {
|
|
|
458
457
|
From: fromAddress,
|
|
459
458
|
To: toAddress,
|
|
460
459
|
SessionKey: params.route.sessionKey,
|
|
460
|
+
DmScope: params.route.dmScope,
|
|
461
461
|
AccountId: params.route.accountId,
|
|
462
462
|
ChatType: params.source.isGroup ? "group" : "direct",
|
|
463
463
|
ConversationLabel: conversationLabel,
|
|
@@ -699,37 +699,9 @@ const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
699
699
|
function isDownloadableLineMessageType(messageType) {
|
|
700
700
|
return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
|
|
701
701
|
}
|
|
702
|
-
const LINE_WEBHOOK_REPLAY_WINDOW_MS = 600 * 1e3;
|
|
703
|
-
const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
|
|
704
702
|
function normalizeLineIngressEntry(value) {
|
|
705
703
|
return normalizeLineAllowEntry(value) || null;
|
|
706
704
|
}
|
|
707
|
-
function createLineWebhookReplayCache() {
|
|
708
|
-
return createChannelReplayGuard({
|
|
709
|
-
dedupe: {
|
|
710
|
-
ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
|
|
711
|
-
memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
|
|
712
|
-
},
|
|
713
|
-
buildReplayKey: ({ event, accountId }) => buildLineWebhookReplayKey(event, accountId)?.key
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
function buildLineWebhookReplayKey(event, accountId) {
|
|
717
|
-
if (event.type === "message") {
|
|
718
|
-
const messageId = event.message?.id?.trim();
|
|
719
|
-
if (messageId) return {
|
|
720
|
-
key: `${accountId}|message:${messageId}`,
|
|
721
|
-
eventId: `message:${messageId}`
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
const eventId = event.webhookEventId?.trim();
|
|
725
|
-
if (!eventId) return null;
|
|
726
|
-
const source = event.source;
|
|
727
|
-
const sourceId = source?.type === "group" ? `group:${source.groupId ?? ""}` : source?.type === "room" ? `room:${source.roomId ?? ""}` : `user:${source?.userId ?? ""}`;
|
|
728
|
-
return {
|
|
729
|
-
key: `${accountId}|${event.type}|${sourceId}|${eventId}`,
|
|
730
|
-
eventId: `event:${eventId}`
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
705
|
function resolveLineGroupConfig(params) {
|
|
734
706
|
return resolveLineGroupConfigEntry(params.config.groups, {
|
|
735
707
|
groupId: params.groupId,
|
|
@@ -998,7 +970,7 @@ async function handleMessageEvent(event, context) {
|
|
|
998
970
|
logVerbose("line: skipping empty message");
|
|
999
971
|
return;
|
|
1000
972
|
}
|
|
1001
|
-
await processMessage(messageContext);
|
|
973
|
+
await processMessage(messageContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
|
|
1002
974
|
historyReservation.commit();
|
|
1003
975
|
} finally {
|
|
1004
976
|
historyReservation.release();
|
|
@@ -1032,30 +1004,12 @@ async function handlePostbackEvent(event, context) {
|
|
|
1032
1004
|
commandAuthorized: decision.commandAccess.authorized
|
|
1033
1005
|
});
|
|
1034
1006
|
if (!postbackContext) return;
|
|
1035
|
-
await context.processMessage(postbackContext);
|
|
1007
|
+
await context.processMessage(postbackContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
|
|
1036
1008
|
}
|
|
1037
1009
|
async function handleLineWebhookEvents(events, context) {
|
|
1038
1010
|
let firstError;
|
|
1039
1011
|
for (const event of events) try {
|
|
1040
|
-
|
|
1041
|
-
await handleLineWebhookEvent(event, context);
|
|
1042
|
-
continue;
|
|
1043
|
-
}
|
|
1044
|
-
const replayEvent = {
|
|
1045
|
-
event,
|
|
1046
|
-
accountId: context.account.accountId
|
|
1047
|
-
};
|
|
1048
|
-
const result = await context.replayCache.processGuarded(replayEvent, async () => await handleLineWebhookEvent(event, context), { onError: "commit" });
|
|
1049
|
-
const replayId = buildLineWebhookReplayKey(event, context.account.accountId)?.eventId;
|
|
1050
|
-
if (result.kind === "inflight") {
|
|
1051
|
-
logVerbose(`line: skipped in-flight replayed webhook event ${replayId ?? "unknown"}`);
|
|
1052
|
-
try {
|
|
1053
|
-
await result.pending;
|
|
1054
|
-
} catch (err) {
|
|
1055
|
-
context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
|
|
1056
|
-
firstError ??= err;
|
|
1057
|
-
}
|
|
1058
|
-
} else if (result.kind === "duplicate") logVerbose(`line: skipped replayed webhook event ${replayId ?? "unknown"}`);
|
|
1012
|
+
await handleLineWebhookEvent(event, context);
|
|
1059
1013
|
} catch (err) {
|
|
1060
1014
|
context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
|
|
1061
1015
|
firstError ??= err;
|
|
@@ -1093,6 +1047,221 @@ function toLintErrorObject(value, fallbackMessage) {
|
|
|
1093
1047
|
return error;
|
|
1094
1048
|
}
|
|
1095
1049
|
//#endregion
|
|
1050
|
+
//#region extensions/line/src/webhook-spool.ts
|
|
1051
|
+
const LINE_WEBHOOK_SPOOL_VERSION = 1;
|
|
1052
|
+
const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
|
|
1053
|
+
const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
|
|
1054
|
+
const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
|
|
1055
|
+
const LINE_WEBHOOK_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
|
|
1056
|
+
const LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES = 4096;
|
|
1057
|
+
var LineWebhookPayloadError = class extends Error {
|
|
1058
|
+
constructor(message, options) {
|
|
1059
|
+
super(message, options);
|
|
1060
|
+
this.name = "LineWebhookPayloadError";
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
var LineWebhookTerminalDeliveryError = class extends Error {
|
|
1064
|
+
constructor(message, options) {
|
|
1065
|
+
super(message, options);
|
|
1066
|
+
this.reason = "delivery-side-effects-committed";
|
|
1067
|
+
this.name = "LineWebhookTerminalDeliveryError";
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
function nonEmptyString(value) {
|
|
1071
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1072
|
+
}
|
|
1073
|
+
/** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
|
|
1074
|
+
function eventIdFor(event) {
|
|
1075
|
+
if (!event || typeof event !== "object") throw new LineWebhookPayloadError("LINE webhook event must be an object.");
|
|
1076
|
+
const candidate = event;
|
|
1077
|
+
if (candidate.type === "message") {
|
|
1078
|
+
const messageId = nonEmptyString(candidate.message?.id);
|
|
1079
|
+
if (messageId) return `message:${messageId}`;
|
|
1080
|
+
}
|
|
1081
|
+
const webhookEventId = nonEmptyString(candidate.webhookEventId);
|
|
1082
|
+
if (webhookEventId) return `event:${webhookEventId}`;
|
|
1083
|
+
throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
|
|
1084
|
+
}
|
|
1085
|
+
function laneKeyFor(event, eventId) {
|
|
1086
|
+
if (!event || typeof event !== "object") return eventId;
|
|
1087
|
+
const source = event.source;
|
|
1088
|
+
if (source?.type === "group") {
|
|
1089
|
+
const groupId = nonEmptyString(source.groupId);
|
|
1090
|
+
if (groupId) return `group:${groupId}`;
|
|
1091
|
+
}
|
|
1092
|
+
if (source?.type === "room") {
|
|
1093
|
+
const roomId = nonEmptyString(source.roomId);
|
|
1094
|
+
if (roomId) return `room:${roomId}`;
|
|
1095
|
+
}
|
|
1096
|
+
if (source?.type === "user") {
|
|
1097
|
+
const userId = nonEmptyString(source.userId);
|
|
1098
|
+
if (userId) return `user:${userId}`;
|
|
1099
|
+
}
|
|
1100
|
+
return eventId;
|
|
1101
|
+
}
|
|
1102
|
+
function parseClaimedEvent(payload, claimedId) {
|
|
1103
|
+
if (payload.version !== LINE_WEBHOOK_SPOOL_VERSION || typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
|
|
1104
|
+
let event;
|
|
1105
|
+
try {
|
|
1106
|
+
event = JSON.parse(payload.rawEvent);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
|
|
1109
|
+
}
|
|
1110
|
+
if (eventIdFor(event) !== claimedId) throw new LineWebhookPayloadError("LINE webhook event id changed after durable admission.");
|
|
1111
|
+
return event;
|
|
1112
|
+
}
|
|
1113
|
+
function errorText(error) {
|
|
1114
|
+
return error instanceof Error ? error.message : String(error);
|
|
1115
|
+
}
|
|
1116
|
+
function isLineAuthenticationFailure(error) {
|
|
1117
|
+
if (!error || typeof error !== "object") return false;
|
|
1118
|
+
const status = error.status;
|
|
1119
|
+
return status === 401 || status === 403;
|
|
1120
|
+
}
|
|
1121
|
+
function createLineWebhookSpool(options) {
|
|
1122
|
+
const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
|
|
1123
|
+
const shutdown = new AbortController();
|
|
1124
|
+
const activeDeliveries = /* @__PURE__ */ new Set();
|
|
1125
|
+
const deferredClaims = /* @__PURE__ */ new Map();
|
|
1126
|
+
const drain = createChannelIngressDrain({
|
|
1127
|
+
queue,
|
|
1128
|
+
abortSignal: shutdown.signal,
|
|
1129
|
+
adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
|
|
1130
|
+
orderBy: "received",
|
|
1131
|
+
scanLimit: LINE_WEBHOOK_DRAIN_SCAN_LIMIT,
|
|
1132
|
+
startLimit: LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
|
|
1133
|
+
retryPolicy: {
|
|
1134
|
+
maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
|
|
1135
|
+
deadLetterMinAgeMs: 0
|
|
1136
|
+
},
|
|
1137
|
+
resolveNonRetryableFailure: (error) => {
|
|
1138
|
+
if (error instanceof LineWebhookPayloadError) return {
|
|
1139
|
+
reason: "invalid-event",
|
|
1140
|
+
message: error.message
|
|
1141
|
+
};
|
|
1142
|
+
if (error instanceof LineWebhookTerminalDeliveryError) return {
|
|
1143
|
+
reason: error.reason,
|
|
1144
|
+
message: error.message
|
|
1145
|
+
};
|
|
1146
|
+
if (isLineAuthenticationFailure(error)) return {
|
|
1147
|
+
reason: "authentication-failed",
|
|
1148
|
+
message: errorText(error)
|
|
1149
|
+
};
|
|
1150
|
+
return null;
|
|
1151
|
+
},
|
|
1152
|
+
onLog: (message) => options.runtime.error?.(danger(`line: ${message}`)),
|
|
1153
|
+
dispatchClaimedEvent: async (claimed, lifecycle) => {
|
|
1154
|
+
const event = parseClaimedEvent(claimed.payload, claimed.id);
|
|
1155
|
+
const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
|
|
1156
|
+
let resolveDeferredClaim;
|
|
1157
|
+
const deferredClaim = new Promise((resolve) => {
|
|
1158
|
+
resolveDeferredClaim = resolve;
|
|
1159
|
+
});
|
|
1160
|
+
let deferredClaimSettled = false;
|
|
1161
|
+
const settleDeferredClaim = () => {
|
|
1162
|
+
if (deferredClaimSettled) return;
|
|
1163
|
+
deferredClaimSettled = true;
|
|
1164
|
+
if (deferredClaims.get(claimed.id) === deferredClaim) deferredClaims.delete(claimed.id);
|
|
1165
|
+
resolveDeferredClaim();
|
|
1166
|
+
};
|
|
1167
|
+
const delivery = options.deliver(event, claimed.payload.destination, { turnAdoptionLifecycle: {
|
|
1168
|
+
...boundLifecycle,
|
|
1169
|
+
onAdopted: async () => {
|
|
1170
|
+
try {
|
|
1171
|
+
await boundLifecycle.onAdopted();
|
|
1172
|
+
} finally {
|
|
1173
|
+
settleDeferredClaim();
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
onDeferred: () => {
|
|
1177
|
+
if (!deferredClaimSettled) deferredClaims.set(claimed.id, deferredClaim);
|
|
1178
|
+
boundLifecycle.onDeferred();
|
|
1179
|
+
},
|
|
1180
|
+
onAbandoned: async () => {
|
|
1181
|
+
try {
|
|
1182
|
+
await boundLifecycle.onAbandoned();
|
|
1183
|
+
} finally {
|
|
1184
|
+
settleDeferredClaim();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
} });
|
|
1188
|
+
activeDeliveries.add(delivery);
|
|
1189
|
+
try {
|
|
1190
|
+
await delivery;
|
|
1191
|
+
} finally {
|
|
1192
|
+
activeDeliveries.delete(delivery);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
let running = false;
|
|
1197
|
+
let drainRequested = false;
|
|
1198
|
+
let drainTask;
|
|
1199
|
+
let drainTimer;
|
|
1200
|
+
const requestDrain = () => {
|
|
1201
|
+
if (!running || shutdown.signal.aborted) return;
|
|
1202
|
+
drainRequested = true;
|
|
1203
|
+
if (drainTask) return;
|
|
1204
|
+
drainTask = runDetachedWebhookWork(async () => {
|
|
1205
|
+
while (drainRequested && !shutdown.signal.aborted) {
|
|
1206
|
+
if (!running) break;
|
|
1207
|
+
drainRequested = false;
|
|
1208
|
+
await drain.drainOnce({ shouldStop: () => !running || shutdown.signal.aborted || activeDeliveries.size >= LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES });
|
|
1209
|
+
}
|
|
1210
|
+
}).catch((error) => {
|
|
1211
|
+
options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`));
|
|
1212
|
+
}).finally(() => {
|
|
1213
|
+
drainTask = void 0;
|
|
1214
|
+
if (running && drainRequested && !shutdown.signal.aborted) requestDrain();
|
|
1215
|
+
});
|
|
1216
|
+
};
|
|
1217
|
+
return {
|
|
1218
|
+
accept: async (body) => {
|
|
1219
|
+
const events = body.events ?? [];
|
|
1220
|
+
if (events.length === 0) return;
|
|
1221
|
+
await queue.prune({
|
|
1222
|
+
completedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
|
|
1223
|
+
completedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES,
|
|
1224
|
+
failedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
|
|
1225
|
+
failedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES
|
|
1226
|
+
});
|
|
1227
|
+
const receivedAt = Date.now();
|
|
1228
|
+
for (const event of events) {
|
|
1229
|
+
const eventId = eventIdFor(event);
|
|
1230
|
+
await queue.enqueue(eventId, {
|
|
1231
|
+
version: LINE_WEBHOOK_SPOOL_VERSION,
|
|
1232
|
+
rawEvent: JSON.stringify(event),
|
|
1233
|
+
destination: body.destination ?? ""
|
|
1234
|
+
}, {
|
|
1235
|
+
receivedAt,
|
|
1236
|
+
laneKey: laneKeyFor(event, eventId)
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
requestDrain();
|
|
1240
|
+
},
|
|
1241
|
+
start: () => {
|
|
1242
|
+
if (running) return;
|
|
1243
|
+
running = true;
|
|
1244
|
+
requestDrain();
|
|
1245
|
+
drainTimer = setInterval(requestDrain, LINE_WEBHOOK_DRAIN_INTERVAL_MS);
|
|
1246
|
+
drainTimer.unref?.();
|
|
1247
|
+
},
|
|
1248
|
+
stop: async () => {
|
|
1249
|
+
if (!running && shutdown.signal.aborted) return;
|
|
1250
|
+
running = false;
|
|
1251
|
+
if (drainTimer) {
|
|
1252
|
+
clearInterval(drainTimer);
|
|
1253
|
+
drainTimer = void 0;
|
|
1254
|
+
}
|
|
1255
|
+
shutdown.abort();
|
|
1256
|
+
await drainTask;
|
|
1257
|
+
await Promise.allSettled(activeDeliveries);
|
|
1258
|
+
await Promise.allSettled(deferredClaims.values());
|
|
1259
|
+
await drain.waitForIdle();
|
|
1260
|
+
drain.dispose();
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
//#endregion
|
|
1096
1265
|
//#region extensions/line/src/bot.ts
|
|
1097
1266
|
function createLineBot(opts) {
|
|
1098
1267
|
const runtime = opts.runtime ?? createNonExitingRuntime();
|
|
@@ -1105,24 +1274,26 @@ function createLineBot(opts) {
|
|
|
1105
1274
|
const processMessage = opts.onMessage ?? (async () => {
|
|
1106
1275
|
logVerbose("line: no message handler configured");
|
|
1107
1276
|
});
|
|
1108
|
-
const replayCache = createLineWebhookReplayCache();
|
|
1109
1277
|
const groupHistories = /* @__PURE__ */ new Map();
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1278
|
+
const spool = createLineWebhookSpool({
|
|
1279
|
+
accountId: account.accountId,
|
|
1280
|
+
runtime,
|
|
1281
|
+
deliver: async (event, _destination, control) => await handleLineWebhookEvents([event], {
|
|
1113
1282
|
cfg,
|
|
1114
1283
|
account,
|
|
1115
1284
|
runtime,
|
|
1116
1285
|
mediaMaxBytes,
|
|
1117
1286
|
processMessage,
|
|
1118
|
-
|
|
1287
|
+
...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
|
|
1119
1288
|
groupHistories,
|
|
1120
1289
|
historyLimit: cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
1121
|
-
})
|
|
1122
|
-
};
|
|
1290
|
+
})
|
|
1291
|
+
});
|
|
1292
|
+
spool.start();
|
|
1123
1293
|
return {
|
|
1124
|
-
handleWebhook,
|
|
1125
|
-
account
|
|
1294
|
+
handleWebhook: spool.accept,
|
|
1295
|
+
account,
|
|
1296
|
+
stop: spool.stop
|
|
1126
1297
|
};
|
|
1127
1298
|
}
|
|
1128
1299
|
//#endregion
|
|
@@ -1214,9 +1385,6 @@ async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_
|
|
|
1214
1385
|
timeoutMs
|
|
1215
1386
|
});
|
|
1216
1387
|
}
|
|
1217
|
-
function logLineWebhookDispatchError(runtime, err) {
|
|
1218
|
-
runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
|
|
1219
|
-
}
|
|
1220
1388
|
function createLineNodeWebhookHandler(params) {
|
|
1221
1389
|
const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
|
|
1222
1390
|
const readBody = params.readBody ?? readLineWebhookRequestBody;
|
|
@@ -1239,7 +1407,6 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1239
1407
|
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
1240
1408
|
return;
|
|
1241
1409
|
}
|
|
1242
|
-
let receiveContext;
|
|
1243
1410
|
try {
|
|
1244
1411
|
const signatureHeader = req.headers["x-line-signature"];
|
|
1245
1412
|
const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
|
|
@@ -1266,24 +1433,14 @@ function createLineNodeWebhookHandler(params) {
|
|
|
1266
1433
|
return;
|
|
1267
1434
|
}
|
|
1268
1435
|
params.onRequestAuthenticated?.();
|
|
1269
|
-
receiveContext = createMessageReceiveContext({
|
|
1270
|
-
id: `${Date.now()}:line:webhook`,
|
|
1271
|
-
channel: "line",
|
|
1272
|
-
message: body,
|
|
1273
|
-
ackPolicy: "after_receive_record",
|
|
1274
|
-
onAck: () => {
|
|
1275
|
-
res.statusCode = 200;
|
|
1276
|
-
res.setHeader("Content-Type", "application/json");
|
|
1277
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
1278
|
-
}
|
|
1279
|
-
});
|
|
1280
|
-
if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
|
|
1281
1436
|
if (body.events && body.events.length > 0) {
|
|
1282
1437
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1283
|
-
|
|
1438
|
+
await params.bot.handleWebhook(body);
|
|
1284
1439
|
}
|
|
1440
|
+
res.statusCode = 200;
|
|
1441
|
+
res.setHeader("Content-Type", "application/json");
|
|
1442
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
1285
1443
|
} catch (err) {
|
|
1286
|
-
await receiveContext?.nack(err);
|
|
1287
1444
|
if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
|
|
1288
1445
|
res.statusCode = 413;
|
|
1289
1446
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -1344,7 +1501,7 @@ async function monitorLineProvider(opts) {
|
|
|
1344
1501
|
accountId,
|
|
1345
1502
|
runtime,
|
|
1346
1503
|
config,
|
|
1347
|
-
onMessage: async (ctx) => {
|
|
1504
|
+
onMessage: async (ctx, deliveryControl) => {
|
|
1348
1505
|
if (!ctx) return;
|
|
1349
1506
|
const { ctxPayload, replyToken, route } = ctx;
|
|
1350
1507
|
const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
|
|
@@ -1358,14 +1515,23 @@ async function monitorLineProvider(opts) {
|
|
|
1358
1515
|
accountId: ctx.accountId
|
|
1359
1516
|
}) : null;
|
|
1360
1517
|
logVerbose(`line: received message from ${await displayNamePromise} (${ctxPayload.From})`);
|
|
1518
|
+
let replyTokenUsed = false;
|
|
1519
|
+
let turnAdopted = false;
|
|
1520
|
+
const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
|
|
1361
1521
|
try {
|
|
1362
1522
|
const textLimit = 5e3;
|
|
1363
|
-
|
|
1364
|
-
const core = getLineRuntime();
|
|
1365
|
-
const turnResult = await core.channel.inbound.run({
|
|
1523
|
+
const turnResult = await getLineRuntime().channel.inbound.run({
|
|
1366
1524
|
channel: "line",
|
|
1367
1525
|
accountId: route.accountId,
|
|
1368
1526
|
raw: ctx,
|
|
1527
|
+
turnAdoptionLifecycle: {
|
|
1528
|
+
...ingressLifecycle,
|
|
1529
|
+
admission: "exclusive",
|
|
1530
|
+
onAdopted: async () => {
|
|
1531
|
+
await ingressLifecycle?.onAdopted();
|
|
1532
|
+
turnAdopted = true;
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1369
1535
|
adapter: {
|
|
1370
1536
|
ingest: () => ({
|
|
1371
1537
|
id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
|
|
@@ -1375,14 +1541,14 @@ async function monitorLineProvider(opts) {
|
|
|
1375
1541
|
cfg: config,
|
|
1376
1542
|
channel: "line",
|
|
1377
1543
|
accountId: route.accountId,
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1544
|
+
route: {
|
|
1545
|
+
agentId: route.agentId,
|
|
1546
|
+
sessionKey: route.sessionKey
|
|
1547
|
+
},
|
|
1381
1548
|
ctxPayload,
|
|
1382
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
1383
|
-
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
1384
1549
|
record: ctx.turn.record,
|
|
1385
1550
|
replyPipeline: {},
|
|
1551
|
+
...ingressLifecycle?.abortSignal ? { replyOptions: { abortSignal: ingressLifecycle.abortSignal } } : {},
|
|
1386
1552
|
delivery: {
|
|
1387
1553
|
durable: (payload, info) => resolveLineDurableReplyOptions({
|
|
1388
1554
|
payload,
|
|
@@ -1439,17 +1605,8 @@ async function monitorLineProvider(opts) {
|
|
|
1439
1605
|
if (!hasFinalInboundReplyDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
|
|
1440
1606
|
} catch (err) {
|
|
1441
1607
|
runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
|
|
1442
|
-
if (
|
|
1443
|
-
|
|
1444
|
-
type: "text",
|
|
1445
|
-
text: "Sorry, I encountered an error processing your message."
|
|
1446
|
-
}], {
|
|
1447
|
-
cfg: config,
|
|
1448
|
-
accountId: ctx.accountId
|
|
1449
|
-
});
|
|
1450
|
-
} catch (replyErr) {
|
|
1451
|
-
runtime.error?.(danger(`line: error reply failed: ${String(replyErr)}`));
|
|
1452
|
-
}
|
|
1608
|
+
if (turnAdopted || replyTokenUsed) throw new LineWebhookTerminalDeliveryError("LINE delivery failed after consuming the event reply token.", { cause: err });
|
|
1609
|
+
throw err;
|
|
1453
1610
|
} finally {
|
|
1454
1611
|
stopLoading?.();
|
|
1455
1612
|
}
|
|
@@ -1527,16 +1684,13 @@ async function monitorLineProvider(opts) {
|
|
|
1527
1684
|
res.end(JSON.stringify({ error: "Invalid webhook payload" }));
|
|
1528
1685
|
return;
|
|
1529
1686
|
}
|
|
1530
|
-
requestLifecycle.release();
|
|
1531
|
-
res.statusCode = 200;
|
|
1532
|
-
res.setHeader("Content-Type", "application/json");
|
|
1533
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
1534
1687
|
if (body.events && body.events.length > 0) {
|
|
1535
1688
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
1536
|
-
|
|
1537
|
-
match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
|
|
1538
|
-
});
|
|
1689
|
+
await match.target.bot.handleWebhook(body);
|
|
1539
1690
|
}
|
|
1691
|
+
res.statusCode = 200;
|
|
1692
|
+
res.setHeader("Content-Type", "application/json");
|
|
1693
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
1540
1694
|
} catch (err) {
|
|
1541
1695
|
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
|
1542
1696
|
res.statusCode = 413;
|
|
@@ -1564,23 +1718,29 @@ async function monitorLineProvider(opts) {
|
|
|
1564
1718
|
});
|
|
1565
1719
|
logVerbose(`line: registered webhook handler at ${normalizedPath}`);
|
|
1566
1720
|
let stopped = false;
|
|
1721
|
+
let stopPromise;
|
|
1567
1722
|
const stopHandler = () => {
|
|
1568
|
-
if (
|
|
1723
|
+
if (stopPromise) return stopPromise;
|
|
1724
|
+
if (stopped) return Promise.resolve();
|
|
1569
1725
|
stopped = true;
|
|
1570
1726
|
logVerbose(`line: stopping provider for account ${resolvedAccountId}`);
|
|
1571
1727
|
unregisterHttp();
|
|
1728
|
+
stopPromise = bot.stop();
|
|
1729
|
+
return stopPromise;
|
|
1572
1730
|
};
|
|
1573
|
-
|
|
1731
|
+
const stopOnAbort = () => void stopHandler();
|
|
1732
|
+
if (abortSignal?.aborted) await stopHandler();
|
|
1574
1733
|
else if (abortSignal) {
|
|
1575
|
-
abortSignal.addEventListener("abort",
|
|
1734
|
+
abortSignal.addEventListener("abort", stopOnAbort, { once: true });
|
|
1576
1735
|
await waitForAbortSignal(abortSignal);
|
|
1736
|
+
await stopHandler();
|
|
1577
1737
|
}
|
|
1578
1738
|
return {
|
|
1579
1739
|
account: bot.account,
|
|
1580
1740
|
handleWebhook: bot.handleWebhook,
|
|
1581
|
-
stop: () => {
|
|
1582
|
-
stopHandler();
|
|
1583
|
-
abortSignal?.removeEventListener("abort",
|
|
1741
|
+
stop: async () => {
|
|
1742
|
+
await stopHandler();
|
|
1743
|
+
abortSignal?.removeEventListener("abort", stopOnAbort);
|
|
1584
1744
|
}
|
|
1585
1745
|
};
|
|
1586
1746
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, D as sendMessageLine, S as pushMessageLine, T as pushTextMessageWithQuickReplies, c as processLineMessage, m as createQuickReplyItems, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage } from "./markdown-to-line-
|
|
1
|
+
import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, D as sendMessageLine, S as pushMessageLine, T as pushTextMessageWithQuickReplies, c as processLineMessage, m as createQuickReplyItems, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage } from "./markdown-to-line-D1vapcdP.js";
|
|
2
2
|
export { buildTemplateMessageFromPayload, createQuickReplyItems, processLineMessage, pushFlexMessage, pushLocationMessage, pushMessageLine, pushMessagesLine, pushTemplateMessage, pushTextMessageWithQuickReplies, sendMessageLine };
|
package/dist/runtime-api.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-
|
|
1
|
+
import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BUBOoW5Q.js";
|
|
2
2
|
import { d as LineChannelConfigSchema, f as LineConfigSchema, g as resolveLineGroupsConfig, h as resolveLineGroupLookupIds, m as resolveLineGroupConfigEntry, n as parseLineDirectives, p as resolveExactLineGroupConfigKey, t as hasLineDirectives, u as setLineRuntime } from "./reply-payload-transform-r9sa-TYT.js";
|
|
3
3
|
import { _ as createNotificationBubble, a as datetimePickerAction, c as uriAction, d as createReceiptCard, f as createActionCard, g as createListCard, h as createInfoCard, i as createMediaPlayerCard, l as createAgendaCard, m as createImageCard, n as createAppleTvRemoteCard, o as messageAction, p as createCarousel, r as createDeviceControlCard, s as postbackAction, t as toFlexMessage, u as createEventCard } from "./flex-templates-K1lS9i8L.js";
|
|
4
|
-
import { a as validateLineSignature, c as normalizeAllowFrom, i as parseLineWebhookBody, n as createLineNodeWebhookHandler, o as downloadLineMedia, r as readLineWebhookRequestBody, s as firstDefined, t as monitorLineProvider } from "./monitor-
|
|
4
|
+
import { a as validateLineSignature, c as normalizeAllowFrom, i as parseLineWebhookBody, n as createLineNodeWebhookHandler, o as downloadLineMedia, r as readLineWebhookRequestBody, s as firstDefined, t as monitorLineProvider } from "./monitor-Bk-lpoOZ.js";
|
|
5
5
|
import { t as probeLineBot } from "./probe-BslD77tJ.js";
|
|
6
|
-
import { A as buildTemplateMessageFromPayload, B as createYesNoConfirm, C as pushMessagesLine, D as sendMessageLine, E as replyMessageLine, F as createImageCarousel, I as createImageCarouselColumn, L as createLinkMenu, M as createButtonTemplate, N as createCarouselColumn, O as showLoadingAnimation, P as createConfirmTemplate, R as createProductCarousel, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, a as extractLinks, b as pushImageMessage, c as processLineMessage, d as createFlexMessage, f as createImageMessage, g as createVideoMessage, h as createTextMessageWithQuickReplies, i as extractCodeBlocks, j as createButtonMenu, k as resolveLineChannelAccessToken, l as stripMarkdown, m as createQuickReplyItems, n as convertLinksToFlexBubble, o as extractMarkdownTables, p as createLocationMessage, r as convertTableToFlexBubble, s as hasMarkdownToConvert, t as convertCodeBlockToFlexBubble, u as createAudioMessage, v as getUserProfile, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage, z as createTemplateCarousel } from "./markdown-to-line-
|
|
6
|
+
import { A as buildTemplateMessageFromPayload, B as createYesNoConfirm, C as pushMessagesLine, D as sendMessageLine, E as replyMessageLine, F as createImageCarousel, I as createImageCarouselColumn, L as createLinkMenu, M as createButtonTemplate, N as createCarouselColumn, O as showLoadingAnimation, P as createConfirmTemplate, R as createProductCarousel, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, a as extractLinks, b as pushImageMessage, c as processLineMessage, d as createFlexMessage, f as createImageMessage, g as createVideoMessage, h as createTextMessageWithQuickReplies, i as extractCodeBlocks, j as createButtonMenu, k as resolveLineChannelAccessToken, l as stripMarkdown, m as createQuickReplyItems, n as convertLinksToFlexBubble, o as extractMarkdownTables, p as createLocationMessage, r as convertTableToFlexBubble, s as hasMarkdownToConvert, t as convertCodeBlockToFlexBubble, u as createAudioMessage, v as getUserProfile, w as pushTemplateMessage, x as pushLocationMessage, y as pushFlexMessage, z as createTemplateCarousel } from "./markdown-to-line-D1vapcdP.js";
|
|
7
7
|
import { clearAccountEntryFields } from "openclaw/plugin-sdk/core";
|
|
8
8
|
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
9
|
-
import { createMessageReceiveContext } from "openclaw/plugin-sdk/channel-outbound";
|
|
10
9
|
import { DEFAULT_ACCOUNT_ID, formatDocsLink, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
|
|
11
10
|
import { buildComputedAccountStatusSnapshot, buildTokenChannelStatusSummary } from "openclaw/plugin-sdk/status-helpers";
|
|
12
11
|
import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
|
13
12
|
import { messagingApi } from "@line/bot-sdk";
|
|
14
|
-
import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
15
13
|
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/agent-media-payload";
|
|
16
14
|
import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
|
|
17
15
|
import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
|
|
@@ -26,13 +24,9 @@ function parseWebhookBody(rawBody) {
|
|
|
26
24
|
if (!rawBody) return null;
|
|
27
25
|
return parseLineWebhookBody(rawBody);
|
|
28
26
|
}
|
|
29
|
-
function logLineWebhookDispatchError(runtime, err) {
|
|
30
|
-
runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
|
|
31
|
-
}
|
|
32
27
|
function createLineWebhookMiddleware(options) {
|
|
33
28
|
const { channelSecret, onEvents, runtime } = options;
|
|
34
29
|
return async (req, res, _next) => {
|
|
35
|
-
let receiveContext;
|
|
36
30
|
try {
|
|
37
31
|
const signature = req.headers["x-line-signature"];
|
|
38
32
|
if (!signature || typeof signature !== "string") {
|
|
@@ -58,22 +52,12 @@ function createLineWebhookMiddleware(options) {
|
|
|
58
52
|
res.status(400).json({ error: "Invalid webhook payload" });
|
|
59
53
|
return;
|
|
60
54
|
}
|
|
61
|
-
receiveContext = createMessageReceiveContext({
|
|
62
|
-
id: `${Date.now()}:line:webhook`,
|
|
63
|
-
channel: "line",
|
|
64
|
-
message: body,
|
|
65
|
-
ackPolicy: "after_receive_record",
|
|
66
|
-
onAck: () => {
|
|
67
|
-
res.status(200).json({ status: "ok" });
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
|
|
71
55
|
if (body.events && body.events.length > 0) {
|
|
72
56
|
logVerbose(`line: received ${body.events.length} webhook events`);
|
|
73
|
-
|
|
57
|
+
await onEvents(body);
|
|
74
58
|
}
|
|
59
|
+
res.status(200).json({ status: "ok" });
|
|
75
60
|
} catch (err) {
|
|
76
|
-
await receiveContext?.nack(err);
|
|
77
61
|
runtime?.error?.(danger(`line webhook error: ${String(err)}`));
|
|
78
62
|
if (!res.headersSent) res.status(500).json({ error: "Internal server error" });
|
|
79
63
|
}
|
package/dist/setup-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-
|
|
1
|
+
import { n as lineSetupAdapter, t as lineSetupWizard } from "./setup-surface-BEt4mazl.js";
|
|
2
2
|
export { lineSetupAdapter, lineSetupWizard };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-
|
|
1
|
+
import { i as resolveLineAccount, n as normalizeAccountId, r as resolveDefaultLineAccountId, t as listLineAccountIds } from "./accounts-BUBOoW5Q.js";
|
|
2
2
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
import { DEFAULT_ACCOUNT_ID, createAllowFromSection, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, mergeAllowFromEntries, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
|
|
4
4
|
//#region extensions/line/src/account-helpers.ts
|
|
5
5
|
function hasLineCredentials(account) {
|
|
6
|
+
if (account.tokenStatus && account.signingSecretStatus) return account.tokenStatus !== "missing" && account.signingSecretStatus !== "missing";
|
|
6
7
|
return Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim());
|
|
7
8
|
}
|
|
8
9
|
function parseLineAllowFromId(raw) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/line",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/line",
|
|
9
|
-
"version": "2026.7.2-beta.
|
|
9
|
+
"version": "2026.7.2-beta.3",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@line/bot-sdk": "11.1.0",
|
|
12
12
|
"zod": "4.4.3"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"openclaw": ">=2026.7.2-beta.
|
|
15
|
+
"openclaw": ">=2026.7.2-beta.3"
|
|
16
16
|
},
|
|
17
17
|
"peerDependenciesMeta": {
|
|
18
18
|
"openclaw": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/line",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.3",
|
|
4
4
|
"description": "OpenClaw LINE channel plugin for LINE Bot API chats.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"zod": "4.4.3"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"openclaw": ">=2026.7.2-beta.
|
|
15
|
+
"openclaw": ">=2026.7.2-beta.3"
|
|
16
16
|
},
|
|
17
17
|
"peerDependenciesMeta": {
|
|
18
18
|
"openclaw": {
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"minHostVersion": ">=2026.4.10"
|
|
43
43
|
},
|
|
44
44
|
"compat": {
|
|
45
|
-
"pluginApi": ">=2026.7.2-beta.
|
|
45
|
+
"pluginApi": ">=2026.7.2-beta.3"
|
|
46
46
|
},
|
|
47
47
|
"build": {
|
|
48
|
-
"openclawVersion": "2026.7.2-beta.
|
|
48
|
+
"openclawVersion": "2026.7.2-beta.3"
|
|
49
49
|
},
|
|
50
50
|
"release": {
|
|
51
51
|
"publishToClawHub": true,
|