@openclaw/clickclack 2026.7.2-beta.7 → 2026.8.1-beta.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/README.md +17 -20
- package/dist/api.js +2 -2
- package/dist/{channel-CECIpaS0.js → channel-DaHgaTy1.js} +495 -77
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{runtime-api-CZ1rmHjv.js → runtime-api-CPL3m5AD.js} +218 -231
- package/dist/runtime-api.js +3 -3
- package/dist/{setup-claim-CP-QZ5bt.js → setup-claim-CQjgPGxU.js} +1 -1
- package/dist/setup-plugin-api.js +1 -2
- package/dist/{setup-surface-DAy8vDJK.js → setup-surface-DjT-gmMe.js} +63 -16
- package/openclaw.plugin.json +201 -0
- package/package.json +8 -5
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as DEFAULT_ACCOUNT_ID, b as resolveClickClackAccount, c as resolveWorkspaceId, f as normalizeClickClackCorrelationId, g as clickClackMeta, h as clickClackConfigAdapter, l as ClickClackHttpError, m as CLICKCLACK_CHANNEL_ID, n as clickClackSetupContract, p as clickClackConfigSchema, s as resolveChannelId, t as clickClackSetupWizard, u as createClickClackClient, y as listEnabledClickClackAccounts } from "./setup-surface-DjT-gmMe.js";
|
|
2
2
|
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BKkzzjBA.js";
|
|
3
|
+
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
|
|
4
|
+
import { mergePairLoopGuardConfig } from "openclaw/plugin-sdk/pair-loop-guard-runtime";
|
|
5
|
+
import { readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
6
|
import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
4
7
|
import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-outbound";
|
|
5
8
|
import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
6
9
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
10
|
+
import { channelReadyPatch, channelStoppedPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
11
|
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
|
12
|
+
import { rawDataToString } from "openclaw/plugin-sdk/webhook-ingress";
|
|
8
13
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
9
14
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
10
15
|
import { createHash, randomUUID } from "node:crypto";
|
|
11
16
|
import { buildMentionRegexes, normalizeMentionText } from "openclaw/plugin-sdk/channel-mention-gating";
|
|
12
17
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
13
18
|
import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
|
|
14
|
-
import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel-inbound";
|
|
19
|
+
import { buildChannelInboundEventContext, createChannelInboundEnvelopeBuilder, recordChannelBotPairLoopAndCheckSuppression } from "openclaw/plugin-sdk/channel-inbound";
|
|
20
|
+
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
|
|
15
21
|
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
|
16
22
|
import { FormatCapabilityProfile, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
17
23
|
//#region extensions/clickclack/src/discussions/binding-generation.ts
|
|
@@ -33,9 +39,20 @@ function getGenerationStore(runtime) {
|
|
|
33
39
|
function reserveDiscussionBindingGeneration(params) {
|
|
34
40
|
const store = getGenerationStore(params.runtime);
|
|
35
41
|
const existing = store.lookup(params.sessionKey);
|
|
36
|
-
|
|
42
|
+
const existingAccountId = existing?.accountId ?? existing?.pending?.accountId;
|
|
43
|
+
const existingCredentialFingerprint = existing?.credentialFingerprint ?? existing?.pending?.credentialFingerprint;
|
|
44
|
+
if (existing?.destinationIdentity === params.destinationIdentity && existingAccountId === params.accountId && existingCredentialFingerprint === params.credentialFingerprint) {
|
|
45
|
+
if (!existing.accountId || !existing.credentialFingerprint) store.register(params.sessionKey, {
|
|
46
|
+
...existing,
|
|
47
|
+
accountId: params.accountId,
|
|
48
|
+
credentialFingerprint: params.credentialFingerprint
|
|
49
|
+
});
|
|
50
|
+
return existing.generation;
|
|
51
|
+
}
|
|
37
52
|
const generation = (params.createGeneration ?? randomUUID)();
|
|
38
53
|
store.register(params.sessionKey, {
|
|
54
|
+
accountId: params.accountId,
|
|
55
|
+
credentialFingerprint: params.credentialFingerprint,
|
|
39
56
|
destinationIdentity: params.destinationIdentity,
|
|
40
57
|
generation
|
|
41
58
|
});
|
|
@@ -54,6 +71,7 @@ function recordPendingDiscussionOpen(params) {
|
|
|
54
71
|
const store = getGenerationStore(params.runtime);
|
|
55
72
|
const existing = store.lookup(params.sessionKey);
|
|
56
73
|
if (!existing || existing.generation !== params.generation) throw new Error("ClickClack discussion generation changed before channel creation");
|
|
74
|
+
if (existing.accountId !== params.pending.accountId || existing.credentialFingerprint !== params.pending.credentialFingerprint) throw new Error("ClickClack discussion ownership changed before channel creation");
|
|
57
75
|
store.register(params.sessionKey, {
|
|
58
76
|
...existing,
|
|
59
77
|
pending: params.pending
|
|
@@ -66,6 +84,18 @@ function listPendingDiscussionOpens(runtime) {
|
|
|
66
84
|
...entry.value.pending
|
|
67
85
|
}] : []);
|
|
68
86
|
}
|
|
87
|
+
/** Stops destination-wide quarantine after the exact remote channel is known. */
|
|
88
|
+
function clearPendingDiscussionOpen(params) {
|
|
89
|
+
const store = getGenerationStore(params.runtime);
|
|
90
|
+
const existing = store.lookup(params.sessionKey);
|
|
91
|
+
if (!existing || existing.generation !== params.expectedGeneration || !existing.pending) return;
|
|
92
|
+
store.register(params.sessionKey, {
|
|
93
|
+
accountId: existing.accountId ?? existing.pending.accountId,
|
|
94
|
+
credentialFingerprint: existing.credentialFingerprint ?? existing.pending.credentialFingerprint,
|
|
95
|
+
destinationIdentity: existing.destinationIdentity,
|
|
96
|
+
generation: existing.generation
|
|
97
|
+
});
|
|
98
|
+
}
|
|
69
99
|
function hasPendingDiscussionOpenForDestination(params) {
|
|
70
100
|
const serverBaseUrl = params.serverBaseUrl.replace(/\/+$/u, "");
|
|
71
101
|
return listPendingDiscussionOpens(params.runtime).some((pending) => pending.serverBaseUrl === serverBaseUrl && pending.workspaceId === params.workspaceId);
|
|
@@ -104,10 +134,9 @@ function discussionSessionPeerId(identity) {
|
|
|
104
134
|
function isDiscussionSessionKey(sessionKey) {
|
|
105
135
|
return sessionKey.includes(":clickclack:") && /:channel:disc-[0-9a-f]{32}$/u.test(sessionKey);
|
|
106
136
|
}
|
|
107
|
-
function discussionExternalRef(installationId, mainSessionKey,
|
|
137
|
+
function discussionExternalRef(installationId, mainSessionKey, destinationIdentity, bindingGeneration) {
|
|
108
138
|
return `openclaw:${installationId}:${shortSessionHash([
|
|
109
139
|
mainSessionKey,
|
|
110
|
-
sessionId,
|
|
111
140
|
destinationIdentity,
|
|
112
141
|
bindingGeneration
|
|
113
142
|
].join("\0"))}`;
|
|
@@ -128,22 +157,41 @@ function discussionSessionKey(params) {
|
|
|
128
157
|
}
|
|
129
158
|
//#endregion
|
|
130
159
|
//#region extensions/clickclack/src/discussions/binding-store.ts
|
|
131
|
-
function
|
|
160
|
+
function bindingMatchesActiveSessionIncarnation(runtime, sessionKey, binding) {
|
|
132
161
|
const entry = runtime.agent.session.getSessionEntry({
|
|
133
162
|
sessionKey,
|
|
134
163
|
readConsistency: "latest"
|
|
135
164
|
});
|
|
136
|
-
return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId);
|
|
165
|
+
return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId && entry.archivedAt === void 0);
|
|
137
166
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Refresh the replaceable session attachment without changing the durable room identity.
|
|
169
|
+
* The store registers persisted state before reindexing, so a failed write leaves the
|
|
170
|
+
* previous attachment authoritative in both persistence and memory.
|
|
171
|
+
*/
|
|
172
|
+
function attachBindingToCurrentActiveSession(params) {
|
|
173
|
+
const entry = params.runtime.agent.session.getSessionEntry({
|
|
174
|
+
sessionKey: params.sessionKey,
|
|
141
175
|
readConsistency: "latest"
|
|
142
176
|
});
|
|
143
|
-
|
|
177
|
+
if (!entry?.sessionId || entry.archivedAt !== void 0) return;
|
|
178
|
+
if (entry.sessionId === params.binding.sessionId) {
|
|
179
|
+
if (params.binding.detachedAt === void 0) return params.binding;
|
|
180
|
+
const { detachedAt: _detachedAt, ...attached } = params.binding;
|
|
181
|
+
params.store.set(params.sessionKey, attached);
|
|
182
|
+
return attached;
|
|
183
|
+
}
|
|
184
|
+
const { detachedAt: _detachedAt, ...retained } = params.binding;
|
|
185
|
+
const attached = {
|
|
186
|
+
...retained,
|
|
187
|
+
sessionId: entry.sessionId
|
|
188
|
+
};
|
|
189
|
+
params.store.set(params.sessionKey, attached);
|
|
190
|
+
return attached;
|
|
144
191
|
}
|
|
145
192
|
const DISCUSSION_BINDINGS_NAMESPACE = "discussion-bindings";
|
|
146
193
|
const MAX_DISCUSSION_BINDINGS = 1e4;
|
|
194
|
+
const MAX_RETAINED_DETACHED_DISCUSSION_BINDINGS = 1e3;
|
|
147
195
|
const storesByRuntime$1 = /* @__PURE__ */ new WeakMap();
|
|
148
196
|
function channelKey(serverBaseUrl, channelId) {
|
|
149
197
|
return `${serverBaseUrl.replace(/\/+$/u, "")}\0${channelId}`;
|
|
@@ -153,6 +201,7 @@ var ClickClackDiscussionBindingStore = class {
|
|
|
153
201
|
#store;
|
|
154
202
|
#sessionByChannel = /* @__PURE__ */ new Map();
|
|
155
203
|
#mainByDiscussionSession = /* @__PURE__ */ new Map();
|
|
204
|
+
#detachedAtBySession = /* @__PURE__ */ new Map();
|
|
156
205
|
#runtime;
|
|
157
206
|
constructor(runtime) {
|
|
158
207
|
this.#runtime = runtime;
|
|
@@ -210,6 +259,27 @@ var ClickClackDiscussionBindingStore = class {
|
|
|
210
259
|
binding: entry.value
|
|
211
260
|
}));
|
|
212
261
|
}
|
|
262
|
+
detachedCount() {
|
|
263
|
+
return this.#detachedAtBySession.size;
|
|
264
|
+
}
|
|
265
|
+
oldestDetached() {
|
|
266
|
+
let oldestSessionKey;
|
|
267
|
+
let oldestDetachedAt = Number.POSITIVE_INFINITY;
|
|
268
|
+
for (const [sessionKey, detachedAt] of this.#detachedAtBySession) if (detachedAt < oldestDetachedAt || detachedAt === oldestDetachedAt && (oldestSessionKey === void 0 || sessionKey < oldestSessionKey)) {
|
|
269
|
+
oldestSessionKey = sessionKey;
|
|
270
|
+
oldestDetachedAt = detachedAt;
|
|
271
|
+
}
|
|
272
|
+
if (!oldestSessionKey) return;
|
|
273
|
+
const binding = this.get(oldestSessionKey);
|
|
274
|
+
if (!binding || binding.detachedAt === void 0) {
|
|
275
|
+
this.#detachedAtBySession.delete(oldestSessionKey);
|
|
276
|
+
return this.oldestDetached();
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
sessionKey: oldestSessionKey,
|
|
280
|
+
binding
|
|
281
|
+
};
|
|
282
|
+
}
|
|
213
283
|
#index(sessionKey, binding) {
|
|
214
284
|
this.#sessionByChannel.set(channelKey(binding.serverBaseUrl, binding.channelId), sessionKey);
|
|
215
285
|
const sideSessionKey = discussionSessionKey({
|
|
@@ -223,6 +293,7 @@ var ClickClackDiscussionBindingStore = class {
|
|
|
223
293
|
externalRef: binding.externalRef
|
|
224
294
|
});
|
|
225
295
|
if (sideSessionKey) this.#mainByDiscussionSession.set(sideSessionKey, sessionKey);
|
|
296
|
+
if (binding.detachedAt !== void 0) this.#detachedAtBySession.set(sessionKey, binding.detachedAt);
|
|
226
297
|
}
|
|
227
298
|
#unindex(sessionKey, binding) {
|
|
228
299
|
this.#sessionByChannel.delete(channelKey(binding.serverBaseUrl, binding.channelId));
|
|
@@ -237,6 +308,7 @@ var ClickClackDiscussionBindingStore = class {
|
|
|
237
308
|
externalRef: binding.externalRef
|
|
238
309
|
});
|
|
239
310
|
if (sideSessionKey) this.#mainByDiscussionSession.delete(sideSessionKey);
|
|
311
|
+
this.#detachedAtBySession.delete(sessionKey);
|
|
240
312
|
}
|
|
241
313
|
};
|
|
242
314
|
function getClickClackDiscussionBindingStore(runtime) {
|
|
@@ -327,28 +399,43 @@ function isClickClackDiscussionChannelRevoked(params) {
|
|
|
327
399
|
//#region extensions/clickclack/src/discussions/routing.ts
|
|
328
400
|
function resolveClickClackDiscussionRoute(params) {
|
|
329
401
|
if (isClickClackDiscussionChannelRevoked(params)) return { state: "revoked" };
|
|
330
|
-
const
|
|
402
|
+
const store = getClickClackDiscussionBindingStore(params.runtime);
|
|
403
|
+
const matched = store.getByChannel(params.serverBaseUrl, params.channelId);
|
|
331
404
|
if (!matched) return { state: hasPendingDiscussionOpenForDestination(params) ? "revoked" : "unbound" };
|
|
332
405
|
if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
|
|
333
406
|
if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
|
|
334
|
-
if (matched.binding.archived) return { state: "revoked" };
|
|
335
407
|
if (resolveDiscussionBindingAccount(params.config, matched.binding).state !== "active") return { state: "revoked" };
|
|
336
|
-
|
|
408
|
+
let binding;
|
|
409
|
+
try {
|
|
410
|
+
binding = attachBindingToCurrentActiveSession({
|
|
411
|
+
runtime: params.runtime,
|
|
412
|
+
store,
|
|
413
|
+
sessionKey: matched.sessionKey,
|
|
414
|
+
binding: matched.binding
|
|
415
|
+
});
|
|
416
|
+
} catch (error) {
|
|
417
|
+
params.runtime.logging.getChildLogger({
|
|
418
|
+
plugin: "clickclack",
|
|
419
|
+
feature: "discussions"
|
|
420
|
+
}).warn(`discussion attachment refresh failed for channel ${params.channelId}: ${String(error)}`);
|
|
421
|
+
return { state: "revoked" };
|
|
422
|
+
}
|
|
423
|
+
if (!binding) return { state: "revoked" };
|
|
337
424
|
const sessionKey = discussionSessionKey({
|
|
338
425
|
runtime: params.runtime,
|
|
339
|
-
agentId:
|
|
426
|
+
agentId: binding.agentId,
|
|
340
427
|
mainSessionKey: matched.sessionKey,
|
|
341
|
-
sessionId:
|
|
428
|
+
sessionId: binding.sessionId,
|
|
342
429
|
accountId: params.accountId,
|
|
343
|
-
serverBaseUrl:
|
|
344
|
-
channelId:
|
|
345
|
-
externalRef:
|
|
430
|
+
serverBaseUrl: binding.serverBaseUrl,
|
|
431
|
+
channelId: binding.channelId,
|
|
432
|
+
externalRef: binding.externalRef
|
|
346
433
|
});
|
|
347
434
|
if (!sessionKey) return { state: "revoked" };
|
|
348
435
|
return {
|
|
349
436
|
state: "active",
|
|
350
437
|
route: {
|
|
351
|
-
agentId:
|
|
438
|
+
agentId: binding.agentId,
|
|
352
439
|
sessionKey,
|
|
353
440
|
systemPrompt: [
|
|
354
441
|
"You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
|
|
@@ -363,6 +450,21 @@ function resolveClickClackDiscussionRoute(params) {
|
|
|
363
450
|
//#endregion
|
|
364
451
|
//#region extensions/clickclack/src/group-policy.ts
|
|
365
452
|
/**
|
|
453
|
+
* Resolves bot-authored message policy using the same exact, wildcard, and
|
|
454
|
+
* account-level precedence as mention gating.
|
|
455
|
+
*/
|
|
456
|
+
function resolveClickClackBotPolicy(params) {
|
|
457
|
+
const { account, channelId } = params;
|
|
458
|
+
const channelKey = channelId?.trim();
|
|
459
|
+
const groups = channelKey ? account.groups : void 0;
|
|
460
|
+
const wildcard = groups?.["*"];
|
|
461
|
+
const exact = channelKey ? groups?.[channelKey] : void 0;
|
|
462
|
+
return {
|
|
463
|
+
allowBots: exact?.allowBots ?? wildcard?.allowBots ?? account.allowBots ?? false,
|
|
464
|
+
botLoopProtection: mergePairLoopGuardConfig(account.botLoopProtection, wildcard?.botLoopProtection, exact?.botLoopProtection)
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
366
468
|
* Resolves the effective group policy for a ClickClack channel.
|
|
367
469
|
*
|
|
368
470
|
* Lookup order:
|
|
@@ -377,9 +479,10 @@ function resolveClickClackGroupPolicy(params) {
|
|
|
377
479
|
requireMention: account.requireMention === true,
|
|
378
480
|
mentionPatterns: account.mentionPatterns ?? []
|
|
379
481
|
};
|
|
380
|
-
const wildcard = account.groups?.["*"];
|
|
381
482
|
const channelKey = channelId?.trim();
|
|
382
|
-
const
|
|
483
|
+
const groups = channelKey ? account.groups : void 0;
|
|
484
|
+
const wildcard = groups?.["*"];
|
|
485
|
+
const exact = channelKey ? groups?.[channelKey] : void 0;
|
|
383
486
|
return {
|
|
384
487
|
requireMention: exact?.requireMention ?? wildcard?.requireMention ?? accountPolicy.requireMention,
|
|
385
488
|
mentionPatterns: exact?.mentionPatterns ?? wildcard?.mentionPatterns ?? accountPolicy.mentionPatterns
|
|
@@ -531,6 +634,10 @@ const clickClackIngressIdentity = {
|
|
|
531
634
|
isWildcardEntry: (entry) => normalizeClickClackUserId(entry) === "*",
|
|
532
635
|
entryIdPrefix: "clickclack-user"
|
|
533
636
|
};
|
|
637
|
+
function resolveClickClackBotLoopConversationId(params) {
|
|
638
|
+
if (params.message.parent_message_id && params.message.thread_root_id) return params.message.thread_root_id;
|
|
639
|
+
return params.isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id ?? params.message.author_id;
|
|
640
|
+
}
|
|
534
641
|
function resolveAccountAgentRoute(params) {
|
|
535
642
|
const runtime = getClickClackRuntime();
|
|
536
643
|
const peer = {
|
|
@@ -633,6 +740,41 @@ async function resolveClickClackInboundAccess(params) {
|
|
|
633
740
|
agentId: preparedRoute.route.agentId,
|
|
634
741
|
channelId: params.message.channel_id
|
|
635
742
|
});
|
|
743
|
+
if (params.message.kind !== void 0 && params.message.kind !== "message") return {
|
|
744
|
+
shouldDispatch: false,
|
|
745
|
+
commandAuthorized: false,
|
|
746
|
+
requireMention: effectiveGroupPolicy.requireMention,
|
|
747
|
+
mentionFacts,
|
|
748
|
+
preparedRoute
|
|
749
|
+
};
|
|
750
|
+
const effectiveBotPolicy = resolveClickClackBotPolicy({
|
|
751
|
+
account: params.account,
|
|
752
|
+
channelId: params.message.channel_id
|
|
753
|
+
});
|
|
754
|
+
const isBotAuthor = params.message.author?.kind === "bot";
|
|
755
|
+
const ingressAllowFrom = isBotAuthor ? params.account.allowFrom.filter((entry) => normalizeClickClackUserId(entry) !== "*") : params.account.allowFrom;
|
|
756
|
+
if (!(!isBotAuthor || effectiveBotPolicy.allowBots === true || effectiveBotPolicy.allowBots === "mentions" && (preparedRoute.isDirect || mentionFacts.wasMentioned))) return {
|
|
757
|
+
shouldDispatch: false,
|
|
758
|
+
commandAuthorized: false,
|
|
759
|
+
requireMention: effectiveGroupPolicy.requireMention,
|
|
760
|
+
mentionFacts,
|
|
761
|
+
preparedRoute
|
|
762
|
+
};
|
|
763
|
+
const botLoopNowMs = parseDateStringTimestampMs(params.message.created_at);
|
|
764
|
+
const botLoopProtection = isBotAuthor && params.message.author_id !== params.account.botUserId && params.account.botUserId ? {
|
|
765
|
+
scopeId: params.account.workspace,
|
|
766
|
+
conversationId: resolveClickClackBotLoopConversationId({
|
|
767
|
+
message: params.message,
|
|
768
|
+
isDirect: preparedRoute.isDirect
|
|
769
|
+
}),
|
|
770
|
+
senderId: params.message.author_id,
|
|
771
|
+
receiverId: params.account.botUserId,
|
|
772
|
+
eventId: params.message.id,
|
|
773
|
+
...botLoopNowMs !== void 0 ? { nowMs: botLoopNowMs } : {},
|
|
774
|
+
config: effectiveBotPolicy.botLoopProtection,
|
|
775
|
+
defaultsConfig: cfg.channels?.defaults?.botLoopProtection,
|
|
776
|
+
defaultEnabled: true
|
|
777
|
+
} : void 0;
|
|
636
778
|
const allowTextCommands = params.account.replyMode === "agent" && runtime.channel.commands.shouldHandleTextCommands({
|
|
637
779
|
cfg,
|
|
638
780
|
surface: CHANNEL_ID$2,
|
|
@@ -648,7 +790,13 @@ async function resolveClickClackInboundAccess(params) {
|
|
|
648
790
|
kind: preparedRoute.isDirect ? "direct" : "group",
|
|
649
791
|
id: preparedRoute.isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id
|
|
650
792
|
},
|
|
651
|
-
|
|
793
|
+
contextBinding: {
|
|
794
|
+
agentId: preparedRoute.route.agentId,
|
|
795
|
+
sessionKey: preparedRoute.route.sessionKey,
|
|
796
|
+
messageId: params.message.id,
|
|
797
|
+
inboundEventKind: "user_request"
|
|
798
|
+
},
|
|
799
|
+
allowFrom: ingressAllowFrom,
|
|
652
800
|
dmPolicy: "allowlist",
|
|
653
801
|
groupPolicy: "allowlist",
|
|
654
802
|
mentionFacts,
|
|
@@ -666,7 +814,9 @@ async function resolveClickClackInboundAccess(params) {
|
|
|
666
814
|
commandAuthorized: resolved.commandAccess.requested ? resolved.commandAccess.authorized : resolved.senderAccess.allowed,
|
|
667
815
|
requireMention: effectiveGroupPolicy.requireMention,
|
|
668
816
|
mentionFacts,
|
|
669
|
-
|
|
817
|
+
botLoopProtection,
|
|
818
|
+
preparedRoute,
|
|
819
|
+
channelIngress: resolved
|
|
670
820
|
};
|
|
671
821
|
}
|
|
672
822
|
//#endregion
|
|
@@ -795,7 +945,8 @@ function activityBody(payload) {
|
|
|
795
945
|
status: payload.status,
|
|
796
946
|
summary: payload.summary,
|
|
797
947
|
progressText: payload.progressText,
|
|
798
|
-
meta: payload.meta
|
|
948
|
+
meta: payload.meta,
|
|
949
|
+
commandBearing: payload.commandBearing
|
|
799
950
|
})?.text?.trim();
|
|
800
951
|
if (line) return line;
|
|
801
952
|
const head = payload.name?.trim() || payload.title?.trim();
|
|
@@ -908,10 +1059,11 @@ function createClickClackActivityPublisher(params) {
|
|
|
908
1059
|
const kind = normalizedItemKind(payload);
|
|
909
1060
|
if (STREAMING_COMMENTARY_ITEM_KINDS.has(kind)) {
|
|
910
1061
|
handleCommentary(payload);
|
|
911
|
-
return;
|
|
1062
|
+
return false;
|
|
912
1063
|
}
|
|
913
|
-
if (SKIPPED_ITEM_KINDS.has(kind)) return;
|
|
1064
|
+
if (SKIPPED_ITEM_KINDS.has(kind)) return false;
|
|
914
1065
|
handleDiscreteItem(payload);
|
|
1066
|
+
return false;
|
|
915
1067
|
},
|
|
916
1068
|
setProvenance: (next) => {
|
|
917
1069
|
provenance = next;
|
|
@@ -1080,9 +1232,9 @@ async function sendClickClackMedia(params) {
|
|
|
1080
1232
|
mediaLocalRoots: params.mediaLocalRoots,
|
|
1081
1233
|
mediaReadFile: params.mediaReadFile
|
|
1082
1234
|
});
|
|
1083
|
-
const filename = media.fileName?.trim() || "attachment";
|
|
1084
|
-
mediaFilename = filename;
|
|
1085
1235
|
const contentType = media.contentType?.trim() || "application/octet-stream";
|
|
1236
|
+
const filename = media.fileName?.trim() || `attachment${extensionForMime(contentType) ?? ""}`;
|
|
1237
|
+
mediaFilename = filename;
|
|
1086
1238
|
await dispatch();
|
|
1087
1239
|
upload = await client.createUpload({
|
|
1088
1240
|
workspaceId,
|
|
@@ -1215,6 +1367,219 @@ async function reconcileClickClackUnknownSend(ctx) {
|
|
|
1215
1367
|
};
|
|
1216
1368
|
}
|
|
1217
1369
|
//#endregion
|
|
1370
|
+
//#region extensions/clickclack/src/progress.ts
|
|
1371
|
+
/**
|
|
1372
|
+
* Publishes ClickClack's native ephemeral agent.progress signal for one
|
|
1373
|
+
* OpenClaw turn. ClickClack renders this as its compact "Agent is
|
|
1374
|
+
* responding" status and the detailed progress lines above the composer.
|
|
1375
|
+
*/
|
|
1376
|
+
function normalizedKind(payload) {
|
|
1377
|
+
const kind = payload.kind?.trim().toLowerCase();
|
|
1378
|
+
if (!kind || kind === "preamble" || kind === "analysis" || kind === "thinking" || kind === "reasoning" || kind === "missing") return "commentary";
|
|
1379
|
+
return kind;
|
|
1380
|
+
}
|
|
1381
|
+
function progressText(payload) {
|
|
1382
|
+
const line = buildChannelProgressDraftLine({
|
|
1383
|
+
event: "item",
|
|
1384
|
+
itemId: payload.itemId,
|
|
1385
|
+
toolCallId: payload.toolCallId,
|
|
1386
|
+
itemKind: payload.kind,
|
|
1387
|
+
title: payload.title,
|
|
1388
|
+
name: payload.name,
|
|
1389
|
+
phase: payload.phase,
|
|
1390
|
+
status: payload.status,
|
|
1391
|
+
summary: payload.summary,
|
|
1392
|
+
progressText: payload.progressText,
|
|
1393
|
+
meta: payload.meta,
|
|
1394
|
+
commandBearing: payload.commandBearing
|
|
1395
|
+
})?.text?.trim();
|
|
1396
|
+
if (line) return line;
|
|
1397
|
+
return payload.progressText?.trim() || payload.summary?.trim() || payload.title?.trim() || payload.name?.trim() || payload.meta?.trim() || payload.status?.trim() || "Working";
|
|
1398
|
+
}
|
|
1399
|
+
function isFinal(payload) {
|
|
1400
|
+
const phase = payload.phase?.trim().toLowerCase();
|
|
1401
|
+
const status = payload.status?.trim().toLowerCase();
|
|
1402
|
+
return phase === "end" || status === "completed" || status === "failed" || status === "blocked";
|
|
1403
|
+
}
|
|
1404
|
+
function createLineIdResolver() {
|
|
1405
|
+
const lineIdsByIdentity = /* @__PURE__ */ new Map();
|
|
1406
|
+
const anonymousLinesByKind = /* @__PURE__ */ new Map();
|
|
1407
|
+
let anonymousSequence = 0;
|
|
1408
|
+
return (payload) => {
|
|
1409
|
+
const identities = [payload.itemId?.replace(/^(tool|command):/, ""), payload.toolCallId].map((value) => value?.trim()).filter((value) => Boolean(value));
|
|
1410
|
+
const existingId = identities.map((identity) => lineIdsByIdentity.get(identity)).find(Boolean);
|
|
1411
|
+
if (existingId) {
|
|
1412
|
+
for (const identity of identities) lineIdsByIdentity.set(identity, existingId);
|
|
1413
|
+
return existingId;
|
|
1414
|
+
}
|
|
1415
|
+
if (identities.length > 0) {
|
|
1416
|
+
const id = `item:${identities[0]}`;
|
|
1417
|
+
for (const identity of identities) lineIdsByIdentity.set(identity, id);
|
|
1418
|
+
return id;
|
|
1419
|
+
}
|
|
1420
|
+
const kind = normalizedKind(payload);
|
|
1421
|
+
const anonymousLines = anonymousLinesByKind.get(kind) ?? [];
|
|
1422
|
+
const line = (payload.phase?.trim().toLowerCase() === "start" ? void 0 : anonymousLines.toReversed().find((line) => line.active)) ?? (() => {
|
|
1423
|
+
const created = {
|
|
1424
|
+
id: `item:${kind}:${++anonymousSequence}`,
|
|
1425
|
+
active: true
|
|
1426
|
+
};
|
|
1427
|
+
anonymousLines.push(created);
|
|
1428
|
+
anonymousLinesByKind.set(kind, anonymousLines);
|
|
1429
|
+
return created;
|
|
1430
|
+
})();
|
|
1431
|
+
if (isFinal(payload)) line.active = false;
|
|
1432
|
+
return line.id;
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
const CLICKCLACK_PROGRESS_UPDATE_INTERVAL_MS = 100;
|
|
1436
|
+
const CLICKCLACK_PROGRESS_FINALIZE_GRACE_MS = 1e3;
|
|
1437
|
+
function createClickClackAgentProgressPublisher(params) {
|
|
1438
|
+
let sequence = 0;
|
|
1439
|
+
const queue = [];
|
|
1440
|
+
const queuedLines = /* @__PURE__ */ new Map();
|
|
1441
|
+
let drainPromise;
|
|
1442
|
+
let lineDrainTimer;
|
|
1443
|
+
let started = false;
|
|
1444
|
+
let cleared = false;
|
|
1445
|
+
const seenLines = /* @__PURE__ */ new Set();
|
|
1446
|
+
const resolveLineId = createLineIdResolver();
|
|
1447
|
+
const drain = () => {
|
|
1448
|
+
if (drainPromise) return drainPromise;
|
|
1449
|
+
drainPromise = (async () => {
|
|
1450
|
+
while (queue.length > 0) {
|
|
1451
|
+
const frame = queue.shift();
|
|
1452
|
+
if (!frame) continue;
|
|
1453
|
+
if (frame.lineId) queuedLines.delete(frame.lineId);
|
|
1454
|
+
try {
|
|
1455
|
+
await params.client.publishEphemeral({
|
|
1456
|
+
...params.target,
|
|
1457
|
+
type: "agent.progress",
|
|
1458
|
+
payload: {
|
|
1459
|
+
turn_id: params.turnId,
|
|
1460
|
+
seq: ++sequence,
|
|
1461
|
+
...frame.payload
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
try {
|
|
1466
|
+
params.onError?.(error);
|
|
1467
|
+
} catch {}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
})().finally(() => {
|
|
1471
|
+
drainPromise = void 0;
|
|
1472
|
+
if (queue.length > 0) drain();
|
|
1473
|
+
});
|
|
1474
|
+
return drainPromise;
|
|
1475
|
+
};
|
|
1476
|
+
const enqueue = (payload) => {
|
|
1477
|
+
queue.push({ payload });
|
|
1478
|
+
drain();
|
|
1479
|
+
};
|
|
1480
|
+
const flushQueuedLines = () => {
|
|
1481
|
+
for (const [lineId, frame] of queuedLines) {
|
|
1482
|
+
queuedLines.delete(lineId);
|
|
1483
|
+
queue.push(frame);
|
|
1484
|
+
}
|
|
1485
|
+
drain();
|
|
1486
|
+
};
|
|
1487
|
+
const discardQueuedLines = () => {
|
|
1488
|
+
queuedLines.clear();
|
|
1489
|
+
const controlFrames = queue.filter((frame) => !frame.lineId);
|
|
1490
|
+
queue.splice(0, queue.length, ...controlFrames);
|
|
1491
|
+
};
|
|
1492
|
+
const waitForDrainWithinFinalizeGrace = async (pending) => {
|
|
1493
|
+
let timeout;
|
|
1494
|
+
let drained = false;
|
|
1495
|
+
try {
|
|
1496
|
+
await Promise.race([pending.then(() => {
|
|
1497
|
+
drained = true;
|
|
1498
|
+
}), new Promise((resolve) => {
|
|
1499
|
+
timeout = setTimeout(resolve, CLICKCLACK_PROGRESS_FINALIZE_GRACE_MS);
|
|
1500
|
+
})]);
|
|
1501
|
+
return drained;
|
|
1502
|
+
} finally {
|
|
1503
|
+
if (timeout) clearTimeout(timeout);
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
const scheduleLineDrain = () => {
|
|
1507
|
+
if (lineDrainTimer) return;
|
|
1508
|
+
lineDrainTimer = setTimeout(() => {
|
|
1509
|
+
lineDrainTimer = void 0;
|
|
1510
|
+
flushQueuedLines();
|
|
1511
|
+
}, CLICKCLACK_PROGRESS_UPDATE_INTERVAL_MS);
|
|
1512
|
+
};
|
|
1513
|
+
const enqueueLine = (lineId, payload) => {
|
|
1514
|
+
const queued = queuedLines.get(lineId);
|
|
1515
|
+
if (queued) {
|
|
1516
|
+
const op = payload.op === "finalize" ? "finalize" : queued.payload.op === "append" ? "append" : payload.op;
|
|
1517
|
+
queued.payload = {
|
|
1518
|
+
...payload,
|
|
1519
|
+
...op ? { op } : {}
|
|
1520
|
+
};
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const frame = {
|
|
1524
|
+
lineId,
|
|
1525
|
+
payload
|
|
1526
|
+
};
|
|
1527
|
+
queuedLines.set(lineId, frame);
|
|
1528
|
+
scheduleLineDrain();
|
|
1529
|
+
};
|
|
1530
|
+
return {
|
|
1531
|
+
start() {
|
|
1532
|
+
if (started) return;
|
|
1533
|
+
started = true;
|
|
1534
|
+
enqueue({
|
|
1535
|
+
op: "append",
|
|
1536
|
+
line: {
|
|
1537
|
+
id: "turn",
|
|
1538
|
+
kind: "commentary",
|
|
1539
|
+
text: params.agentLabel ? `${params.agentLabel} is responding` : "Agent is responding",
|
|
1540
|
+
status: "running"
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
},
|
|
1544
|
+
onItemEvent(payload) {
|
|
1545
|
+
if (!started || cleared) return false;
|
|
1546
|
+
const id = resolveLineId(payload);
|
|
1547
|
+
const final = isFinal(payload);
|
|
1548
|
+
const kind = normalizedKind(payload);
|
|
1549
|
+
const retractsExistingCommentary = kind === "commentary" && seenLines.has(id) && payload.progressText !== void 0 && payload.progressText.trim() === "";
|
|
1550
|
+
if (retractsExistingCommentary && queuedLines.get(id)?.payload.op === "append") {
|
|
1551
|
+
queuedLines.delete(id);
|
|
1552
|
+
seenLines.delete(id);
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1555
|
+
const line = {
|
|
1556
|
+
id,
|
|
1557
|
+
kind,
|
|
1558
|
+
text: retractsExistingCommentary ? "" : progressText(payload),
|
|
1559
|
+
status: payload.status?.trim() || (final ? "completed" : "running")
|
|
1560
|
+
};
|
|
1561
|
+
if (payload.name?.trim()) line.tool_name = payload.name.trim();
|
|
1562
|
+
enqueueLine(id, {
|
|
1563
|
+
op: final ? "finalize" : seenLines.has(id) ? "update" : "append",
|
|
1564
|
+
line
|
|
1565
|
+
});
|
|
1566
|
+
seenLines.add(id);
|
|
1567
|
+
return false;
|
|
1568
|
+
},
|
|
1569
|
+
async finalize() {
|
|
1570
|
+
if (!started || cleared) return;
|
|
1571
|
+
cleared = true;
|
|
1572
|
+
if (lineDrainTimer) {
|
|
1573
|
+
clearTimeout(lineDrainTimer);
|
|
1574
|
+
lineDrainTimer = void 0;
|
|
1575
|
+
}
|
|
1576
|
+
flushQueuedLines();
|
|
1577
|
+
enqueue({ op: "clear" });
|
|
1578
|
+
if (!await waitForDrainWithinFinalizeGrace(drain())) discardQueuedLines();
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
//#endregion
|
|
1218
1583
|
//#region extensions/clickclack/src/inbound.ts
|
|
1219
1584
|
const CHANNEL_ID$1 = "clickclack";
|
|
1220
1585
|
const CLICKCLACK_MESSAGE_ID_PATTERN = /^msg_[0-9a-hjkmnp-tv-z]{26}$/u;
|
|
@@ -1265,19 +1630,56 @@ async function handleClickClackInbound(params) {
|
|
|
1265
1630
|
config: params.config,
|
|
1266
1631
|
message
|
|
1267
1632
|
});
|
|
1268
|
-
if (!access.shouldDispatch) return;
|
|
1633
|
+
if (!access.shouldDispatch || !access.channelIngress) return;
|
|
1269
1634
|
const conversationId = message.channel_id || message.direct_conversation_id;
|
|
1270
1635
|
if (!conversationId) return;
|
|
1271
1636
|
const { discussionRoute, isDirect, route, target } = access.preparedRoute;
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
message,
|
|
1277
|
-
route,
|
|
1278
|
-
target,
|
|
1637
|
+
const progress = params.account.nativeProgress ? createClickClackAgentProgressPublisher({
|
|
1638
|
+
client: createClickClackClient({
|
|
1639
|
+
baseUrl: params.account.apiEndpoint,
|
|
1640
|
+
token: params.account.token,
|
|
1279
1641
|
correlationId: params.correlationId
|
|
1280
|
-
})
|
|
1642
|
+
}),
|
|
1643
|
+
target: message.channel_id ? {
|
|
1644
|
+
workspaceId: message.workspace_id,
|
|
1645
|
+
channelId: message.channel_id
|
|
1646
|
+
} : {
|
|
1647
|
+
workspaceId: message.workspace_id,
|
|
1648
|
+
conversationId
|
|
1649
|
+
},
|
|
1650
|
+
turnId: message.id,
|
|
1651
|
+
agentLabel: params.account.name?.trim() || params.account.botHandle?.trim() || params.account.agentId?.trim() || params.account.accountId,
|
|
1652
|
+
onError: (error) => {
|
|
1653
|
+
runtime.logging.getChildLogger({
|
|
1654
|
+
plugin: "clickclack",
|
|
1655
|
+
feature: "agent-progress"
|
|
1656
|
+
}).warn(`clickclack progress publish failed: ${String(error)}`);
|
|
1657
|
+
}
|
|
1658
|
+
}) : void 0;
|
|
1659
|
+
if (params.account.replyMode === "model" && !discussionRoute) {
|
|
1660
|
+
if (access.botLoopProtection) {
|
|
1661
|
+
const loopResult = recordChannelBotPairLoopAndCheckSuppression(access.botLoopProtection);
|
|
1662
|
+
if (loopResult.suppressed) {
|
|
1663
|
+
runtime.logging.getChildLogger({
|
|
1664
|
+
plugin: "clickclack",
|
|
1665
|
+
feature: "bot-loop-protection"
|
|
1666
|
+
}).warn(`[${params.account.accountId}] ClickClack bot-pair loop suppressed for ${Math.max(0, Math.ceil((loopResult.cooldownUntilMs - Date.now()) / 1e3))}s`);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
progress?.start();
|
|
1671
|
+
try {
|
|
1672
|
+
await dispatchModelReply({
|
|
1673
|
+
account: params.account,
|
|
1674
|
+
cfg: params.config,
|
|
1675
|
+
message,
|
|
1676
|
+
route,
|
|
1677
|
+
target,
|
|
1678
|
+
correlationId: params.correlationId
|
|
1679
|
+
});
|
|
1680
|
+
} finally {
|
|
1681
|
+
await progress?.finalize();
|
|
1682
|
+
}
|
|
1281
1683
|
return;
|
|
1282
1684
|
}
|
|
1283
1685
|
let turnProvenance;
|
|
@@ -1307,7 +1709,8 @@ async function handleClickClackInbound(params) {
|
|
|
1307
1709
|
timestamp: new Date(message.created_at),
|
|
1308
1710
|
body: message.body
|
|
1309
1711
|
});
|
|
1310
|
-
const ctxPayload =
|
|
1712
|
+
const ctxPayload = (params.buildContext ?? buildChannelInboundEventContext)({
|
|
1713
|
+
channelIngress: access.channelIngress,
|
|
1311
1714
|
channel: CHANNEL_ID$1,
|
|
1312
1715
|
accountId: route.accountId ?? params.account.accountId,
|
|
1313
1716
|
messageId: message.id,
|
|
@@ -1354,20 +1757,27 @@ async function handleClickClackInbound(params) {
|
|
|
1354
1757
|
}
|
|
1355
1758
|
});
|
|
1356
1759
|
const runId = resolveClickClackAgentRunId(message.id);
|
|
1357
|
-
const activityReplyOptions =
|
|
1358
|
-
onModelSelected: (ctx) => {
|
|
1760
|
+
const activityReplyOptions = {
|
|
1761
|
+
...activity ? { onModelSelected: (ctx) => {
|
|
1359
1762
|
turnProvenance = {
|
|
1360
1763
|
model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
|
|
1361
1764
|
thinking: ctx.thinkLevel
|
|
1362
1765
|
};
|
|
1363
|
-
activity
|
|
1364
|
-
},
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1766
|
+
activity.setProvenance(turnProvenance);
|
|
1767
|
+
} } : {},
|
|
1768
|
+
...progress || activity ? {
|
|
1769
|
+
onItemEvent: (payload) => {
|
|
1770
|
+
progress?.onItemEvent(payload);
|
|
1771
|
+
activity?.onItemEvent(payload);
|
|
1772
|
+
return false;
|
|
1773
|
+
},
|
|
1774
|
+
commentaryProgressEnabled: true,
|
|
1775
|
+
suppressDefaultToolProgressMessages: true,
|
|
1776
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: true
|
|
1777
|
+
} : {}
|
|
1778
|
+
};
|
|
1779
|
+
progress?.start();
|
|
1780
|
+
const dispatch = () => runtime.channel.inbound.dispatch({
|
|
1371
1781
|
cfg: params.config,
|
|
1372
1782
|
channel: CHANNEL_ID$1,
|
|
1373
1783
|
accountId: params.account.accountId,
|
|
@@ -1377,11 +1787,12 @@ async function handleClickClackInbound(params) {
|
|
|
1377
1787
|
sessionKey: route.sessionKey
|
|
1378
1788
|
},
|
|
1379
1789
|
ctxPayload,
|
|
1790
|
+
botLoopProtection: access.botLoopProtection,
|
|
1380
1791
|
toolsAllow: params.account.toolsAllow,
|
|
1381
|
-
replyOptions:
|
|
1792
|
+
replyOptions: {
|
|
1382
1793
|
...runId ? { runId } : {},
|
|
1383
1794
|
...activityReplyOptions
|
|
1384
|
-
}
|
|
1795
|
+
},
|
|
1385
1796
|
delivery: {
|
|
1386
1797
|
deliver: async (payload) => {
|
|
1387
1798
|
if (hasClickClackReplyMedia(payload)) throw new Error("ClickClack media reply requires durable delivery");
|
|
@@ -1423,8 +1834,9 @@ async function handleClickClackInbound(params) {
|
|
|
1423
1834
|
} }
|
|
1424
1835
|
});
|
|
1425
1836
|
try {
|
|
1426
|
-
await
|
|
1837
|
+
await dispatch();
|
|
1427
1838
|
} finally {
|
|
1839
|
+
await progress?.finalize();
|
|
1428
1840
|
await activity?.finalize();
|
|
1429
1841
|
}
|
|
1430
1842
|
}
|
|
@@ -1432,8 +1844,7 @@ async function handleClickClackInbound(params) {
|
|
|
1432
1844
|
//#region extensions/clickclack/src/gateway.ts
|
|
1433
1845
|
const CLICKCLACK_EVENT_PAGE_LIMIT = 500;
|
|
1434
1846
|
function payloadString(event, key) {
|
|
1435
|
-
|
|
1436
|
-
return typeof value === "string" ? value : "";
|
|
1847
|
+
return readStringField(event.payload, key) ?? "";
|
|
1437
1848
|
}
|
|
1438
1849
|
function eventCorrelationId(event) {
|
|
1439
1850
|
return normalizeClickClackCorrelationId(event.payload?.correlation_id);
|
|
@@ -1441,25 +1852,16 @@ function eventCorrelationId(event) {
|
|
|
1441
1852
|
async function resolveEventMessage(params) {
|
|
1442
1853
|
const messageId = payloadString(params.event, "message_id");
|
|
1443
1854
|
if (!messageId) return null;
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
return (await params.client.thread(rootId)).replies.find((message) => message.id === messageId) ?? null;
|
|
1855
|
+
try {
|
|
1856
|
+
return await params.client.message(messageId);
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
if (error instanceof ClickClackHttpError && error.status === 404) return null;
|
|
1859
|
+
throw error;
|
|
1450
1860
|
}
|
|
1451
|
-
if (params.event.channel_id && typeof params.event.seq === "number") return (await params.client.channelMessages(params.event.channel_id, params.event.seq - 1, 10)).find((message) => message.id === messageId) ?? null;
|
|
1452
|
-
return null;
|
|
1453
|
-
}
|
|
1454
|
-
function decodeSocketMessage(data) {
|
|
1455
|
-
if (typeof data === "string") return data;
|
|
1456
|
-
if (Buffer.isBuffer(data)) return data.toString("utf8");
|
|
1457
|
-
if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
|
|
1458
|
-
return Buffer.concat(data).toString("utf8");
|
|
1459
1861
|
}
|
|
1460
1862
|
function parseSocketEvent(data) {
|
|
1461
1863
|
try {
|
|
1462
|
-
return JSON.parse(
|
|
1864
|
+
return JSON.parse(rawDataToString(data));
|
|
1463
1865
|
} catch {
|
|
1464
1866
|
return null;
|
|
1465
1867
|
}
|
|
@@ -1476,8 +1878,11 @@ async function processEvent(params) {
|
|
|
1476
1878
|
}) : params.client,
|
|
1477
1879
|
event: params.event
|
|
1478
1880
|
});
|
|
1479
|
-
if (!message
|
|
1480
|
-
|
|
1881
|
+
if (!message) {
|
|
1882
|
+
params.log?.warn?.(`[${params.account.accountId}] skipped unreadable ClickClack message before agent dispatch: type=${params.event.type} messageId=${payloadString(params.event, "message_id") || "unknown"}`);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (message.author_id === params.botUserId) return;
|
|
1481
1886
|
const access = await resolveClickClackInboundAccess({
|
|
1482
1887
|
account: params.account,
|
|
1483
1888
|
config: params.config,
|
|
@@ -1492,6 +1897,7 @@ async function processEvent(params) {
|
|
|
1492
1897
|
config: params.config,
|
|
1493
1898
|
message,
|
|
1494
1899
|
access,
|
|
1900
|
+
buildContext: params.buildContext,
|
|
1495
1901
|
...correlationId ? { correlationId } : {}
|
|
1496
1902
|
});
|
|
1497
1903
|
}
|
|
@@ -1536,6 +1942,7 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1536
1942
|
client,
|
|
1537
1943
|
event,
|
|
1538
1944
|
botUserId: account.botUserId,
|
|
1945
|
+
buildContext: ctx.channelRuntime?.inbound.buildContext,
|
|
1539
1946
|
log: ctx.log
|
|
1540
1947
|
});
|
|
1541
1948
|
if (account.commandMenu) await syncClickClackCommandMenu({
|
|
@@ -1546,6 +1953,7 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1546
1953
|
ctx.setStatus({
|
|
1547
1954
|
accountId: account.accountId,
|
|
1548
1955
|
running: true,
|
|
1956
|
+
lifecycle: "starting",
|
|
1549
1957
|
configured: true,
|
|
1550
1958
|
enabled: account.enabled,
|
|
1551
1959
|
baseUrl: account.baseUrl
|
|
@@ -1601,6 +2009,9 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1601
2009
|
};
|
|
1602
2010
|
ctx.abortSignal.addEventListener("abort", abort, { once: true });
|
|
1603
2011
|
removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
|
|
2012
|
+
socket.on("open", () => {
|
|
2013
|
+
ctx.setStatus(channelReadyPatch({ accountId: account.accountId }));
|
|
2014
|
+
});
|
|
1604
2015
|
socket.on("message", (data) => {
|
|
1605
2016
|
if (closing || settled) return;
|
|
1606
2017
|
messageQueue = messageQueue.then(async () => {
|
|
@@ -1616,6 +2027,11 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1616
2027
|
});
|
|
1617
2028
|
socket.on("close", () => {
|
|
1618
2029
|
closing = true;
|
|
2030
|
+
if (!ctx.abortSignal.aborted) ctx.setStatus({
|
|
2031
|
+
accountId: account.accountId,
|
|
2032
|
+
connected: false,
|
|
2033
|
+
lifecycle: "recovering"
|
|
2034
|
+
});
|
|
1619
2035
|
finishAfterQueuedMessages();
|
|
1620
2036
|
});
|
|
1621
2037
|
socket.on("error", (error) => {
|
|
@@ -1625,6 +2041,12 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1625
2041
|
}
|
|
1626
2042
|
if (closing) return;
|
|
1627
2043
|
ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
|
|
2044
|
+
ctx.setStatus({
|
|
2045
|
+
accountId: account.accountId,
|
|
2046
|
+
connected: false,
|
|
2047
|
+
lifecycle: "recovering",
|
|
2048
|
+
lastError: error instanceof Error ? error.message : String(error)
|
|
2049
|
+
});
|
|
1628
2050
|
closing = true;
|
|
1629
2051
|
socket.close();
|
|
1630
2052
|
});
|
|
@@ -1636,10 +2058,7 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1636
2058
|
}
|
|
1637
2059
|
}
|
|
1638
2060
|
} finally {
|
|
1639
|
-
ctx.setStatus({
|
|
1640
|
-
accountId: account.accountId,
|
|
1641
|
-
running: false
|
|
1642
|
-
});
|
|
2061
|
+
ctx.setStatus(channelStoppedPatch({ accountId: account.accountId }));
|
|
1643
2062
|
}
|
|
1644
2063
|
}
|
|
1645
2064
|
//#endregion
|
|
@@ -1742,7 +2161,6 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1742
2161
|
reload: { configPrefixes: ["channels.clickclack"] },
|
|
1743
2162
|
configSchema: clickClackConfigSchema,
|
|
1744
2163
|
config: clickClackConfigAdapter,
|
|
1745
|
-
setup: clickClackSetupAdapter,
|
|
1746
2164
|
setupContract: clickClackSetupContract,
|
|
1747
2165
|
setupWizard: clickClackSetupWizard,
|
|
1748
2166
|
secrets: {
|
|
@@ -1849,4 +2267,4 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1849
2267
|
}
|
|
1850
2268
|
});
|
|
1851
2269
|
//#endregion
|
|
1852
|
-
export {
|
|
2270
|
+
export { slugifyDiscussionLabel as C, listPendingDiscussionOpens as D, clearPendingDiscussionOpen as E, recordPendingDiscussionOpen as O, resolveDiscussionLabel as S, clearDiscussionBindingGeneration as T, getClickClackDiscussionBindingStore as _, setClickClackRuntime as a, fallbackDiscussionLabel as b, markClickClackDiscussionChannelIdentityRevoked as c, discussionInfoForBinding as d, normalizedServerBaseUrl as f, bindingMatchesActiveSessionIncarnation as g, attachBindingToCurrentActiveSession as h, getClickClackRuntime as i, reserveDiscussionBindingGeneration as k, markClickClackDiscussionChannelRevoked as l, MAX_RETAINED_DETACHED_DISCUSSION_BINDINGS as m, buildClickClackTarget as n, clearClickClackDiscussionChannelRevoked as o, resolveDiscussionBindingAccount as p, parseClickClackTarget as r, isClickClackDiscussionChannelRevoked as s, clickClackPlugin as t, discussionAccounts as u, discussionCredentialFingerprint as v, truncateDiscussionDisplayTitle as w, isDiscussionSessionKey as x, discussionExternalRef as y };
|