@openclaw/nextcloud-talk 2026.8.1-beta.3 → 2026.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nextcloudTalkPlugin } from "./channel-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-n-lgbvCj.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
|
@@ -7,27 +7,27 @@ import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from
|
|
|
7
7
|
import { createAllowlistProviderRouteAllowlistWarningCollector, createConditionalWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
|
|
8
8
|
import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
9
9
|
import { FormatCapabilityProfile, convertMarkdownTables, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
10
|
-
import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
|
|
11
|
-
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
12
10
|
import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
|
|
13
11
|
import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
|
|
14
12
|
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
|
|
15
|
-
import { buildSecretInputSchema, hasConfiguredSecretInput,
|
|
13
|
+
import { buildSecretInputSchema, hasConfiguredSecretInput, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
14
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
15
|
+
import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
|
|
16
16
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
17
17
|
import { parseStrictNonNegativeInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
18
|
-
import { readProviderJsonResponse,
|
|
18
|
+
import { readProviderJsonResponse, readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
|
|
19
19
|
import { resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
|
|
20
20
|
import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
|
|
21
21
|
import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
22
|
+
import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
|
|
22
23
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
23
24
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
24
25
|
import { createHmac, randomBytes } from "node:crypto";
|
|
25
26
|
import { fileExists, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
26
|
-
import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
|
|
27
27
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
|
|
28
|
-
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
28
|
+
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ReplyToModeSchema, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
29
29
|
import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
|
|
30
|
-
import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
30
|
+
import { adaptScopedAccountAccessor, clearAccountFieldsFromConfigSection, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
31
31
|
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
32
32
|
import { z } from "zod";
|
|
33
33
|
import os from "node:os";
|
|
@@ -53,17 +53,19 @@ import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-to
|
|
|
53
53
|
function resolveNextcloudTalkApiCredentialsResult(params) {
|
|
54
54
|
const apiUser = params.apiUser?.trim();
|
|
55
55
|
if (!apiUser) return { status: "missing" };
|
|
56
|
-
const inlinePassword =
|
|
56
|
+
const inlinePassword = resolveSecretInputString({
|
|
57
57
|
value: params.apiPassword,
|
|
58
|
-
path: "channels.nextcloud-talk.apiPassword"
|
|
58
|
+
path: "channels.nextcloud-talk.apiPassword",
|
|
59
|
+
mode: params.mode
|
|
59
60
|
});
|
|
60
|
-
if (inlinePassword) return {
|
|
61
|
+
if (inlinePassword.status === "available") return {
|
|
61
62
|
status: "available",
|
|
62
63
|
value: {
|
|
63
64
|
apiUser,
|
|
64
|
-
apiPassword: inlinePassword
|
|
65
|
+
apiPassword: inlinePassword.value
|
|
65
66
|
}
|
|
66
67
|
};
|
|
68
|
+
if (inlinePassword.status === "configured_unavailable") return { status: "configured_unavailable" };
|
|
67
69
|
if (!params.apiPasswordFile?.trim()) return { status: "missing" };
|
|
68
70
|
const result = tryReadSecretFileSync(params.apiPasswordFile, "Nextcloud Talk API password", { rejectHardlinks: false }, { configPath: params.configPath ?? "channels.nextcloud-talk.apiPasswordFile" });
|
|
69
71
|
if (result.status === "available") return result.value ? {
|
|
@@ -147,13 +149,6 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
147
149
|
const accountEnabled = merged.enabled !== false;
|
|
148
150
|
const enabled = baseEnabled && accountEnabled;
|
|
149
151
|
const secretResolution = resolveNextcloudTalkSecret(accountId, merged);
|
|
150
|
-
const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
|
|
151
|
-
apiUser: merged.apiUser,
|
|
152
|
-
apiPassword: merged.apiPassword,
|
|
153
|
-
apiPasswordFile: merged.apiPasswordFile,
|
|
154
|
-
configPath: `channels.nextcloud-talk.accounts.${accountId}.apiPasswordFile`
|
|
155
|
-
});
|
|
156
|
-
const diagnostics = [secretResolution.diagnostic, apiCredentialResolution.status === "configured_unavailable" ? apiCredentialResolution.diagnostic : void 0].filter((diagnostic) => Boolean(diagnostic));
|
|
157
152
|
const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? "";
|
|
158
153
|
debugAccounts("resolve", {
|
|
159
154
|
accountId,
|
|
@@ -169,8 +164,7 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
169
164
|
secret: secretResolution.secret,
|
|
170
165
|
secretSource: secretResolution.source,
|
|
171
166
|
tokenStatus: secretResolution.status,
|
|
172
|
-
|
|
173
|
-
...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
|
|
167
|
+
...secretResolution.diagnostic ? { credentialDiagnostics: [secretResolution.diagnostic] } : {},
|
|
174
168
|
config: merged
|
|
175
169
|
};
|
|
176
170
|
};
|
|
@@ -182,6 +176,27 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
182
176
|
resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg)
|
|
183
177
|
});
|
|
184
178
|
}
|
|
179
|
+
function inspectNextcloudTalkAccount(params) {
|
|
180
|
+
const account = resolveNextcloudTalkAccount(params);
|
|
181
|
+
const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
|
|
182
|
+
apiUser: account.config.apiUser,
|
|
183
|
+
apiPassword: account.config.apiPassword,
|
|
184
|
+
apiPasswordFile: account.config.apiPasswordFile,
|
|
185
|
+
configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`,
|
|
186
|
+
mode: "inspect"
|
|
187
|
+
});
|
|
188
|
+
const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" && apiCredentialResolution.diagnostic ? [apiCredentialResolution.diagnostic] : []];
|
|
189
|
+
return {
|
|
190
|
+
...account,
|
|
191
|
+
configured: isNextcloudTalkAccountConfigured(account),
|
|
192
|
+
mode: "webhook",
|
|
193
|
+
apiCredentialStatus: apiCredentialResolution.status,
|
|
194
|
+
...credentialDiagnostics.length > 0 ? { credentialDiagnostics } : {}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function isNextcloudTalkAccountConfigured(account) {
|
|
198
|
+
return Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim());
|
|
199
|
+
}
|
|
185
200
|
//#endregion
|
|
186
201
|
//#region extensions/nextcloud-talk/src/approval-auth.ts
|
|
187
202
|
function normalizeNextcloudTalkApproverId(value) {
|
|
@@ -203,6 +218,18 @@ async function releaseNextcloudTalkGuardedResponse(params) {
|
|
|
203
218
|
if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
|
|
204
219
|
await params.release();
|
|
205
220
|
}
|
|
221
|
+
async function readNextcloudTalkErrorBody(response, ...credentials) {
|
|
222
|
+
try {
|
|
223
|
+
let body = await readProviderTextResponse(response, "Nextcloud Talk error", {
|
|
224
|
+
maxBytes: 8192,
|
|
225
|
+
chunkTimeoutMs: 1e4
|
|
226
|
+
});
|
|
227
|
+
for (const credential of credentials) body = body.replaceAll(credential, "***");
|
|
228
|
+
return redactToolPayloadText(body);
|
|
229
|
+
} catch {
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
206
233
|
//#endregion
|
|
207
234
|
//#region extensions/nextcloud-talk/src/signature.ts
|
|
208
235
|
const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
|
|
@@ -215,7 +242,8 @@ const BACKEND_HEADER = "x-nextcloud-talk-backend";
|
|
|
215
242
|
function verifyNextcloudTalkSignature(params) {
|
|
216
243
|
const { signature, random, body, secret } = params;
|
|
217
244
|
if (!signature || !random || !secret) return false;
|
|
218
|
-
|
|
245
|
+
const expected = createHmac("sha256", secret).update(random + body).digest("hex");
|
|
246
|
+
return safeEqualSecret(signature, expected);
|
|
219
247
|
}
|
|
220
248
|
/**
|
|
221
249
|
* Extract webhook headers from an incoming request.
|
|
@@ -249,7 +277,6 @@ function generateNextcloudTalkSignature(params) {
|
|
|
249
277
|
//#endregion
|
|
250
278
|
//#region extensions/nextcloud-talk/src/bot-preflight.ts
|
|
251
279
|
const BOT_FEATURE_RESPONSE = 2;
|
|
252
|
-
const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
|
253
280
|
function normalizeUrlForMatch(value) {
|
|
254
281
|
if (!value?.trim()) return "";
|
|
255
282
|
try {
|
|
@@ -314,7 +341,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
|
|
|
314
341
|
});
|
|
315
342
|
try {
|
|
316
343
|
if (!response.ok) {
|
|
317
|
-
const body = await
|
|
344
|
+
const body = await readNextcloudTalkErrorBody(response, auth, credentials.apiPassword);
|
|
318
345
|
return {
|
|
319
346
|
ok: false,
|
|
320
347
|
code: "api_error",
|
|
@@ -366,6 +393,16 @@ const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
|
|
|
366
393
|
sectionKey: "nextcloud-talk",
|
|
367
394
|
listAccountIds: listNextcloudTalkAccountIds,
|
|
368
395
|
resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
|
|
396
|
+
inspectAccount: (cfg, accountId) => {
|
|
397
|
+
const account = inspectNextcloudTalkAccount({
|
|
398
|
+
cfg,
|
|
399
|
+
accountId
|
|
400
|
+
});
|
|
401
|
+
return {
|
|
402
|
+
...account,
|
|
403
|
+
baseUrl: account.baseUrl ? "[set]" : "[missing]"
|
|
404
|
+
};
|
|
405
|
+
},
|
|
369
406
|
defaultAccountId: resolveDefaultNextcloudTalkAccountId,
|
|
370
407
|
clearBaseFields: [
|
|
371
408
|
"botSecret",
|
|
@@ -397,10 +434,11 @@ const NextcloudTalkRoomSchema = buildGroupEntrySchema({ allowFrom: z.array(z.str
|
|
|
397
434
|
const NextcloudTalkNetworkSchema = z.object({
|
|
398
435
|
/** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
|
|
399
436
|
dangerouslyAllowPrivateNetwork: z.boolean().optional() }).strict().optional();
|
|
400
|
-
const
|
|
437
|
+
const NextcloudTalkAccountSchemaBase = z.object({
|
|
401
438
|
name: z.string().optional(),
|
|
402
439
|
enabled: z.boolean().optional(),
|
|
403
440
|
configWrites: z.boolean().optional(),
|
|
441
|
+
replyToMode: ReplyToModeSchema.optional(),
|
|
404
442
|
markdown: MarkdownConfigSchema,
|
|
405
443
|
baseUrl: z.string().optional(),
|
|
406
444
|
botSecret: buildSecretInputSchema().optional(),
|
|
@@ -420,7 +458,8 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
|
|
|
420
458
|
/** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
|
|
421
459
|
network: NextcloudTalkNetworkSchema,
|
|
422
460
|
...ReplyRuntimeConfigSchemaShape
|
|
423
|
-
}).strict()
|
|
461
|
+
}).strict();
|
|
462
|
+
const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(NextcloudTalkAccountSchemaBase, {
|
|
424
463
|
optionalAccount: true,
|
|
425
464
|
refine: (value, ctx) => {
|
|
426
465
|
requireChannelOpenAllowFrom({
|
|
@@ -436,7 +475,7 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
|
|
|
436
475
|
//#region extensions/nextcloud-talk/src/replay-migration-contract.ts
|
|
437
476
|
const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
|
|
438
477
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
|
|
439
|
-
const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS =
|
|
478
|
+
const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 864e5;
|
|
440
479
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
|
|
441
480
|
//#endregion
|
|
442
481
|
//#region extensions/nextcloud-talk/src/doctor.ts
|
|
@@ -516,10 +555,12 @@ function normalizeNextcloudTalkAllowlist(values) {
|
|
|
516
555
|
return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
|
|
517
556
|
}
|
|
518
557
|
function resolveNextcloudTalkAllowlistMatch(params) {
|
|
558
|
+
const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
|
|
559
|
+
const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
|
|
519
560
|
return resolveAllowlistMatchByCandidates({
|
|
520
|
-
allowList:
|
|
561
|
+
allowList: allowFrom,
|
|
521
562
|
candidates: [{
|
|
522
|
-
value:
|
|
563
|
+
value: senderId,
|
|
523
564
|
source: "id"
|
|
524
565
|
}]
|
|
525
566
|
});
|
|
@@ -587,8 +628,8 @@ function resolveNextcloudTalkGroupRequireMention(params) {
|
|
|
587
628
|
}
|
|
588
629
|
//#endregion
|
|
589
630
|
//#region extensions/nextcloud-talk/src/room-info.ts
|
|
590
|
-
const ROOM_CACHE_TTL_MS =
|
|
591
|
-
const ROOM_CACHE_ERROR_TTL_MS =
|
|
631
|
+
const ROOM_CACHE_TTL_MS = 3e5;
|
|
632
|
+
const ROOM_CACHE_ERROR_TTL_MS = 3e4;
|
|
592
633
|
const ROOM_CACHE_MAX_ENTRIES = 1e3;
|
|
593
634
|
const NEXTCLOUD_TALK_ROOM_INFO_TIMEOUT_MS = 3e4;
|
|
594
635
|
const roomCache = /* @__PURE__ */ new Map();
|
|
@@ -650,7 +691,8 @@ async function resolveNextcloudTalkRoomKind(params) {
|
|
|
650
691
|
runtime?.log?.(`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`);
|
|
651
692
|
return;
|
|
652
693
|
}
|
|
653
|
-
const
|
|
694
|
+
const payload = await readProviderJsonResponse(response, "Nextcloud Talk room info failed");
|
|
695
|
+
const kind = resolveRoomKindFromType(parseStrictPositiveInteger(payload.ocs?.data?.type));
|
|
654
696
|
cacheRoomInfo(key, {
|
|
655
697
|
fetchedAt: Date.now(),
|
|
656
698
|
kind
|
|
@@ -696,7 +738,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
|
|
|
696
738
|
}
|
|
697
739
|
//#endregion
|
|
698
740
|
//#region extensions/nextcloud-talk/src/send.ts
|
|
699
|
-
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
|
|
700
741
|
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
|
|
701
742
|
const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
|
|
702
743
|
const NEXTCLOUD_TALK_FORMAT_PROFILE = FormatCapabilityProfile.define({
|
|
@@ -717,20 +758,12 @@ function renderNextcloudTalkMarkdown(markdown) {
|
|
|
717
758
|
escapeText: (text) => text
|
|
718
759
|
}, NEXTCLOUD_TALK_FORMAT_PROFILE);
|
|
719
760
|
}
|
|
720
|
-
/** Collapses
|
|
761
|
+
/** Collapses and caps an already-redacted error body for display. */
|
|
721
762
|
function collapseErrorSnippet(text) {
|
|
722
763
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
723
764
|
if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${truncateUtf16Safe(collapsed, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
|
|
724
765
|
return collapsed;
|
|
725
766
|
}
|
|
726
|
-
/** Reads a bounded, collapsed error-body snippet without buffering hostile responses. */
|
|
727
|
-
async function readNextcloudTalkErrorSnippet(response) {
|
|
728
|
-
try {
|
|
729
|
-
return collapseErrorSnippet(await readResponseTextLimited(response, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES));
|
|
730
|
-
} catch {
|
|
731
|
-
return "";
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
767
|
function resolveCredentials(explicit, account) {
|
|
735
768
|
const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
|
|
736
769
|
const secret = explicit.secret?.trim() ?? account.secret;
|
|
@@ -803,8 +836,9 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
803
836
|
body: message,
|
|
804
837
|
secret
|
|
805
838
|
});
|
|
839
|
+
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`;
|
|
806
840
|
const { response, release } = await fetchWithSsrFGuard({
|
|
807
|
-
url
|
|
841
|
+
url,
|
|
808
842
|
init: {
|
|
809
843
|
method: "POST",
|
|
810
844
|
headers: {
|
|
@@ -821,7 +855,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
821
855
|
});
|
|
822
856
|
try {
|
|
823
857
|
if (!response.ok) {
|
|
824
|
-
const errorBody = await
|
|
858
|
+
const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
|
|
825
859
|
const status = response.status;
|
|
826
860
|
let errorMsg = `Nextcloud Talk send failed (${status})`;
|
|
827
861
|
if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
|
|
@@ -865,8 +899,9 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
|
865
899
|
body: reaction,
|
|
866
900
|
secret
|
|
867
901
|
});
|
|
902
|
+
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`;
|
|
868
903
|
const { response, release } = await fetchWithSsrFGuard({
|
|
869
|
-
url
|
|
904
|
+
url,
|
|
870
905
|
init: {
|
|
871
906
|
method: "POST",
|
|
872
907
|
headers: {
|
|
@@ -883,7 +918,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
|
883
918
|
});
|
|
884
919
|
try {
|
|
885
920
|
if (!response.ok) {
|
|
886
|
-
const errorBody = await
|
|
921
|
+
const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
|
|
887
922
|
throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
|
|
888
923
|
}
|
|
889
924
|
return { ok: true };
|
|
@@ -954,7 +989,15 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
954
989
|
accountId: account.accountId
|
|
955
990
|
});
|
|
956
991
|
const rawBody = message.text?.trim() ?? "";
|
|
957
|
-
if (!rawBody)
|
|
992
|
+
if (!rawBody) {
|
|
993
|
+
logInboundDrop({
|
|
994
|
+
log: (messageLocal) => runtime.log?.(messageLocal),
|
|
995
|
+
channel: CHANNEL_ID,
|
|
996
|
+
reason: `empty message body (mediaType=${message.mediaType})`,
|
|
997
|
+
target: message.senderId
|
|
998
|
+
});
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
958
1001
|
const roomKind = await resolveNextcloudTalkRoomKind({
|
|
959
1002
|
account,
|
|
960
1003
|
roomToken: message.roomToken,
|
|
@@ -1270,8 +1313,8 @@ async function migrateNextcloudTalkLegacyReplayState(params) {
|
|
|
1270
1313
|
}
|
|
1271
1314
|
//#endregion
|
|
1272
1315
|
//#region extensions/nextcloud-talk/src/monitor.ts
|
|
1273
|
-
const DEFAULT_WEBHOOK_MAX_BODY_BYTES =
|
|
1274
|
-
const PREAUTH_WEBHOOK_MAX_BODY_BYTES =
|
|
1316
|
+
const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1048576;
|
|
1317
|
+
const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 65536;
|
|
1275
1318
|
const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_HEADER = "x-openclaw-delivery-accepted";
|
|
1276
1319
|
const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_VALUE = "durable";
|
|
1277
1320
|
const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
|
|
@@ -1414,6 +1457,7 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1414
1457
|
const stop = async () => {
|
|
1415
1458
|
stopRequested = true;
|
|
1416
1459
|
await closeIfListening();
|
|
1460
|
+
webhookAuthRateLimiter.dispose();
|
|
1417
1461
|
};
|
|
1418
1462
|
const start = () => {
|
|
1419
1463
|
if (stopRequested) return Promise.resolve();
|
|
@@ -1429,8 +1473,10 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1429
1473
|
});
|
|
1430
1474
|
});
|
|
1431
1475
|
};
|
|
1432
|
-
if (abortSignal)
|
|
1433
|
-
|
|
1476
|
+
if (abortSignal) {
|
|
1477
|
+
if (abortSignal.aborted) stop();
|
|
1478
|
+
else abortSignal.addEventListener("abort", () => void stop(), { once: true });
|
|
1479
|
+
}
|
|
1434
1480
|
return {
|
|
1435
1481
|
server,
|
|
1436
1482
|
start,
|
|
@@ -1440,6 +1486,12 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1440
1486
|
//#endregion
|
|
1441
1487
|
//#region extensions/nextcloud-talk/src/webhook-spool.ts
|
|
1442
1488
|
const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
|
|
1489
|
+
function describeIgnoredWebhookEvent(rawEvent) {
|
|
1490
|
+
const envelope = parseRawObject(rawEvent);
|
|
1491
|
+
const type = typeof envelope.type === "string" ? envelope.type : "unknown";
|
|
1492
|
+
const object = isRecord(envelope.object) ? envelope.object : null;
|
|
1493
|
+
return `type=${type} objectType=${typeof object?.type === "string" ? object.type : "unknown"}`;
|
|
1494
|
+
}
|
|
1443
1495
|
const NextcloudTalkWebhookPayloadSchema = z.object({
|
|
1444
1496
|
type: z.enum([
|
|
1445
1497
|
"Create",
|
|
@@ -1566,7 +1618,11 @@ function createNextcloudTalkWebhookSpool(options) {
|
|
|
1566
1618
|
if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
|
|
1567
1619
|
const receiveTask = (async () => {
|
|
1568
1620
|
await startAfterMigration;
|
|
1569
|
-
|
|
1621
|
+
if ((await monitor.admit(rawEvent)).kind === "ignored") {
|
|
1622
|
+
options.runtime.log?.(`nextcloud-talk: ignored non-message webhook event (${describeIgnoredWebhookEvent(rawEvent)})`);
|
|
1623
|
+
return "ignored";
|
|
1624
|
+
}
|
|
1625
|
+
return "accepted";
|
|
1570
1626
|
})();
|
|
1571
1627
|
inFlightReceives.add(receiveTask);
|
|
1572
1628
|
receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
|
|
@@ -1710,44 +1766,18 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1710
1766
|
});
|
|
1711
1767
|
},
|
|
1712
1768
|
logoutAccount: async ({ accountId, cfg }) => {
|
|
1713
|
-
const
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
delete nextSection.botSecret;
|
|
1720
|
-
cleared = true;
|
|
1721
|
-
changed = true;
|
|
1722
|
-
}
|
|
1723
|
-
const accountCleanup = clearAccountEntryFields({
|
|
1724
|
-
accounts: nextSection.accounts,
|
|
1725
|
-
accountId,
|
|
1726
|
-
fields: ["botSecret"]
|
|
1727
|
-
});
|
|
1728
|
-
if (accountCleanup.changed) {
|
|
1729
|
-
changed = true;
|
|
1730
|
-
if (accountCleanup.cleared) cleared = true;
|
|
1731
|
-
if (accountCleanup.nextAccounts) nextSection.accounts = accountCleanup.nextAccounts;
|
|
1732
|
-
else delete nextSection.accounts;
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
if (changed) if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
|
|
1736
|
-
...nextCfg.channels,
|
|
1737
|
-
"nextcloud-talk": nextSection
|
|
1738
|
-
};
|
|
1739
|
-
else {
|
|
1740
|
-
const nextChannels = { ...nextCfg.channels };
|
|
1741
|
-
delete nextChannels["nextcloud-talk"];
|
|
1742
|
-
if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
|
|
1743
|
-
else delete nextCfg.channels;
|
|
1744
|
-
}
|
|
1769
|
+
const { nextConfig, changed, cleared } = clearAccountFieldsFromConfigSection({
|
|
1770
|
+
cfg,
|
|
1771
|
+
sectionKey: "nextcloud-talk",
|
|
1772
|
+
accountId,
|
|
1773
|
+
fields: ["botSecret"]
|
|
1774
|
+
});
|
|
1745
1775
|
const loggedOut = resolveNextcloudTalkAccount({
|
|
1746
|
-
cfg:
|
|
1776
|
+
cfg: nextConfig,
|
|
1747
1777
|
accountId
|
|
1748
1778
|
}).secretSource === "none";
|
|
1749
1779
|
if (changed) await getNextcloudTalkRuntime().config.replaceConfigFile({
|
|
1750
|
-
nextConfig
|
|
1780
|
+
nextConfig,
|
|
1751
1781
|
afterWrite: { mode: "auto" }
|
|
1752
1782
|
});
|
|
1753
1783
|
return {
|
|
@@ -1929,6 +1959,49 @@ const nextcloudTalkDmPolicy = createChannelDmPolicy({
|
|
|
1929
1959
|
applyPatch: ({ cfg, account, patch }) => setNextcloudTalkAccountConfig(cfg, account.accountId, patch),
|
|
1930
1960
|
promptAllowFrom: promptNextcloudTalkAllowFromForAccount
|
|
1931
1961
|
});
|
|
1962
|
+
const nextcloudTalkSetupAdapter = {
|
|
1963
|
+
singleAccountKeysToMove: ["rooms"],
|
|
1964
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
|
|
1965
|
+
prepareAccountConfigInput: ({ input }) => {
|
|
1966
|
+
const setupInput = input;
|
|
1967
|
+
return {
|
|
1968
|
+
...setupInput,
|
|
1969
|
+
baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
|
|
1970
|
+
secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
|
|
1971
|
+
secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
|
|
1972
|
+
};
|
|
1973
|
+
},
|
|
1974
|
+
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
1975
|
+
cfg,
|
|
1976
|
+
channelKey: channel$1,
|
|
1977
|
+
accountId,
|
|
1978
|
+
name
|
|
1979
|
+
}),
|
|
1980
|
+
validateInput: createSetupInputPresenceValidator({
|
|
1981
|
+
defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
|
|
1982
|
+
validate: ({ input }) => {
|
|
1983
|
+
const setupInput = input;
|
|
1984
|
+
if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
|
|
1985
|
+
const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
|
|
1986
|
+
if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
|
|
1987
|
+
const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
|
|
1988
|
+
if (baseUrlError) return baseUrlError;
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
}),
|
|
1992
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
1993
|
+
const setupInput = input;
|
|
1994
|
+
return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
|
|
1995
|
+
cfg,
|
|
1996
|
+
channelKey: channel$1,
|
|
1997
|
+
accountId,
|
|
1998
|
+
name: setupInput.name
|
|
1999
|
+
}), accountId, {
|
|
2000
|
+
baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
|
|
2001
|
+
...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
|
|
2002
|
+
}, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
1932
2005
|
const nextcloudTalkSetupContract = defineChannelSetupContract({
|
|
1933
2006
|
fields: {
|
|
1934
2007
|
baseUrl: {
|
|
@@ -1994,49 +2067,7 @@ const nextcloudTalkSetupContract = defineChannelSetupContract({
|
|
|
1994
2067
|
envVars: ["NEXTCLOUD_TALK_BOT_SECRET"]
|
|
1995
2068
|
}
|
|
1996
2069
|
},
|
|
1997
|
-
legacyAdapter:
|
|
1998
|
-
singleAccountKeysToMove: ["rooms"],
|
|
1999
|
-
resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
|
|
2000
|
-
prepareAccountConfigInput: ({ input }) => {
|
|
2001
|
-
const setupInput = input;
|
|
2002
|
-
return {
|
|
2003
|
-
...setupInput,
|
|
2004
|
-
baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
|
|
2005
|
-
secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
|
|
2006
|
-
secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
|
|
2007
|
-
};
|
|
2008
|
-
},
|
|
2009
|
-
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
2010
|
-
cfg,
|
|
2011
|
-
channelKey: channel$1,
|
|
2012
|
-
accountId,
|
|
2013
|
-
name
|
|
2014
|
-
}),
|
|
2015
|
-
validateInput: createSetupInputPresenceValidator({
|
|
2016
|
-
defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
|
|
2017
|
-
validate: ({ input }) => {
|
|
2018
|
-
const setupInput = input;
|
|
2019
|
-
if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
|
|
2020
|
-
const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
|
|
2021
|
-
if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
|
|
2022
|
-
const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
|
|
2023
|
-
if (baseUrlError) return baseUrlError;
|
|
2024
|
-
return null;
|
|
2025
|
-
}
|
|
2026
|
-
}),
|
|
2027
|
-
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
2028
|
-
const setupInput = input;
|
|
2029
|
-
return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
|
|
2030
|
-
cfg,
|
|
2031
|
-
channelKey: channel$1,
|
|
2032
|
-
accountId,
|
|
2033
|
-
name: setupInput.name
|
|
2034
|
-
}), accountId, {
|
|
2035
|
-
baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
|
|
2036
|
-
...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
|
|
2037
|
-
}, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2070
|
+
legacyAdapter: nextcloudTalkSetupAdapter
|
|
2040
2071
|
});
|
|
2041
2072
|
//#endregion
|
|
2042
2073
|
//#region extensions/nextcloud-talk/src/setup-surface.ts
|
|
@@ -2221,10 +2252,10 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2221
2252
|
configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
|
|
2222
2253
|
config: {
|
|
2223
2254
|
...nextcloudTalkConfigAdapter,
|
|
2224
|
-
isConfigured:
|
|
2255
|
+
isConfigured: isNextcloudTalkAccountConfigured,
|
|
2225
2256
|
describeAccount: (account) => describeWebhookAccountSnapshot({
|
|
2226
2257
|
account,
|
|
2227
|
-
configured:
|
|
2258
|
+
configured: isNextcloudTalkAccountConfigured(account),
|
|
2228
2259
|
extra: {
|
|
2229
2260
|
secretSource: account.secretSource,
|
|
2230
2261
|
tokenStatus: account.tokenStatus,
|
|
@@ -2280,7 +2311,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2280
2311
|
accountId: account.accountId,
|
|
2281
2312
|
name: account.name,
|
|
2282
2313
|
enabled: account.enabled,
|
|
2283
|
-
configured:
|
|
2314
|
+
configured: isNextcloudTalkAccountConfigured(account),
|
|
2284
2315
|
extra: {
|
|
2285
2316
|
secretSource: account.secretSource,
|
|
2286
2317
|
tokenStatus: account.tokenStatus,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nextcloudTalkPlugin } from "./channel-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-n-lgbvCj.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, mergeAccountConfig } from "openclaw/plugin-sdk/account-core";
|
|
2
|
+
//#region extensions/nextcloud-talk/configured-state.ts
|
|
3
|
+
function hasConfiguredNextcloudAccount(account, env) {
|
|
4
|
+
return Boolean(account?.baseUrl?.trim() && (hasConfiguredAccountValue(account.botSecret) || hasConfiguredAccountValue(account.botSecretFile) || hasConfiguredAccountValue(env.NEXTCLOUD_TALK_BOT_SECRET)));
|
|
5
|
+
}
|
|
6
|
+
/** Require a Nextcloud server plus its account-owned bot credential. */
|
|
7
|
+
function hasConfiguredNextcloudTalkChannelState(params) {
|
|
8
|
+
const channel = params.cfg.channels?.["nextcloud-talk"];
|
|
9
|
+
if (channel?.enabled === false) return false;
|
|
10
|
+
const defaultAccount = channel?.accounts?.[DEFAULT_ACCOUNT_ID];
|
|
11
|
+
if (defaultAccount?.enabled !== false) {
|
|
12
|
+
if (hasConfiguredNextcloudAccount(defaultAccount ? mergeAccountConfig({
|
|
13
|
+
channelConfig: channel,
|
|
14
|
+
accountConfig: defaultAccount,
|
|
15
|
+
omitKeys: ["defaultAccount"]
|
|
16
|
+
}) : channel, params.env ?? process.env)) return true;
|
|
17
|
+
}
|
|
18
|
+
return Object.entries(channel?.accounts ?? {}).some(([accountId, account]) => accountId !== DEFAULT_ACCOUNT_ID && account.enabled !== false && hasConfiguredNextcloudAccount(mergeAccountConfig({
|
|
19
|
+
channelConfig: channel,
|
|
20
|
+
accountConfig: account,
|
|
21
|
+
omitKeys: ["defaultAccount"]
|
|
22
|
+
}), {}));
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { hasConfiguredNextcloudTalkChannelState };
|
package/openclaw.plugin.json
CHANGED
|
@@ -32,6 +32,26 @@
|
|
|
32
32
|
"configWrites": {
|
|
33
33
|
"type": "boolean"
|
|
34
34
|
},
|
|
35
|
+
"replyToMode": {
|
|
36
|
+
"anyOf": [
|
|
37
|
+
{
|
|
38
|
+
"type": "string",
|
|
39
|
+
"const": "off"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "string",
|
|
43
|
+
"const": "first"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"type": "string",
|
|
47
|
+
"const": "all"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "string",
|
|
51
|
+
"const": "batched"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
35
55
|
"markdown": {
|
|
36
56
|
"type": "object",
|
|
37
57
|
"properties": {
|
|
@@ -479,6 +499,26 @@
|
|
|
479
499
|
"configWrites": {
|
|
480
500
|
"type": "boolean"
|
|
481
501
|
},
|
|
502
|
+
"replyToMode": {
|
|
503
|
+
"anyOf": [
|
|
504
|
+
{
|
|
505
|
+
"type": "string",
|
|
506
|
+
"const": "off"
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
"type": "string",
|
|
510
|
+
"const": "first"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"type": "string",
|
|
514
|
+
"const": "all"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"type": "string",
|
|
518
|
+
"const": "batched"
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
},
|
|
482
522
|
"markdown": {
|
|
483
523
|
"type": "object",
|
|
484
524
|
"properties": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nextcloud-talk",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.2",
|
|
4
4
|
"description": "OpenClaw Nextcloud Talk channel plugin for conversations.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"openclaw": ">=2026.8.
|
|
11
|
+
"openclaw": ">=2026.8.2"
|
|
12
12
|
},
|
|
13
13
|
"peerDependenciesMeta": {
|
|
14
14
|
"openclaw": {
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"NEXTCLOUD_TALK_BOT_SECRET",
|
|
32
32
|
"NEXTCLOUD_TALK_API_PASSWORD"
|
|
33
33
|
]
|
|
34
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"specifier": "./dist/configured-state.js",
|
|
36
|
+
"exportName": "hasConfiguredNextcloudTalkChannelState"
|
|
35
37
|
},
|
|
36
38
|
"label": "Nextcloud Talk",
|
|
37
39
|
"selectionLabel": "Nextcloud Talk (self-hosted)",
|
|
@@ -127,10 +129,10 @@
|
|
|
127
129
|
"minHostVersion": ">=2026.4.10"
|
|
128
130
|
},
|
|
129
131
|
"compat": {
|
|
130
|
-
"pluginApi": ">=2026.8.
|
|
132
|
+
"pluginApi": ">=2026.8.2"
|
|
131
133
|
},
|
|
132
134
|
"build": {
|
|
133
|
-
"openclawVersion": "2026.8.
|
|
135
|
+
"openclawVersion": "2026.8.2"
|
|
134
136
|
},
|
|
135
137
|
"release": {
|
|
136
138
|
"publishToClawHub": true,
|