@openclaw/clickclack 2026.7.2-beta.7 → 2026.8.1-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/README.md +17 -20
- package/dist/api.js +2 -2
- package/dist/{channel-CECIpaS0.js → channel-ChO3E9i2.js} +507 -82
- 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-B0ChZLhm.js} +218 -231
- package/dist/runtime-api.js +3 -3
- package/dist/{setup-claim-CP-QZ5bt.js → setup-claim--cdFCz2P.js} +1 -1
- package/dist/setup-plugin-api.js +1 -2
- package/dist/{setup-surface-DAy8vDJK.js → setup-surface-BQ1Zyohu.js} +109 -28
- 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-BQ1Zyohu.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
|
|
@@ -716,15 +866,16 @@ async function syncClickClackCommandMenu(params) {
|
|
|
716
866
|
await params.client.setBotCommands(commands);
|
|
717
867
|
} catch (error) {
|
|
718
868
|
const status = errorStatus(error);
|
|
869
|
+
const messagePrefix = `[${params.accountId}] ClickClack command menu sync`;
|
|
719
870
|
if (status === 403) {
|
|
720
|
-
params.log?.warn?.(
|
|
871
|
+
params.log?.warn?.(`${messagePrefix} skipped: ${formatErrorMessage(error)}; verify token/workspace command permissions or set commandMenu: false if menus are not needed`);
|
|
721
872
|
return;
|
|
722
873
|
}
|
|
723
874
|
if (status === 404) {
|
|
724
|
-
params.log?.debug?.(
|
|
875
|
+
params.log?.debug?.(`${messagePrefix} skipped: server does not support /api/bots/self/commands`);
|
|
725
876
|
return;
|
|
726
877
|
}
|
|
727
|
-
params.log?.warn?.(
|
|
878
|
+
params.log?.warn?.(`${messagePrefix} failed: ${formatErrorMessage(error)}`);
|
|
728
879
|
}
|
|
729
880
|
}
|
|
730
881
|
//#endregion
|
|
@@ -795,7 +946,8 @@ function activityBody(payload) {
|
|
|
795
946
|
status: payload.status,
|
|
796
947
|
summary: payload.summary,
|
|
797
948
|
progressText: payload.progressText,
|
|
798
|
-
meta: payload.meta
|
|
949
|
+
meta: payload.meta,
|
|
950
|
+
commandBearing: payload.commandBearing
|
|
799
951
|
})?.text?.trim();
|
|
800
952
|
if (line) return line;
|
|
801
953
|
const head = payload.name?.trim() || payload.title?.trim();
|
|
@@ -908,10 +1060,11 @@ function createClickClackActivityPublisher(params) {
|
|
|
908
1060
|
const kind = normalizedItemKind(payload);
|
|
909
1061
|
if (STREAMING_COMMENTARY_ITEM_KINDS.has(kind)) {
|
|
910
1062
|
handleCommentary(payload);
|
|
911
|
-
return;
|
|
1063
|
+
return false;
|
|
912
1064
|
}
|
|
913
|
-
if (SKIPPED_ITEM_KINDS.has(kind)) return;
|
|
1065
|
+
if (SKIPPED_ITEM_KINDS.has(kind)) return false;
|
|
914
1066
|
handleDiscreteItem(payload);
|
|
1067
|
+
return false;
|
|
915
1068
|
},
|
|
916
1069
|
setProvenance: (next) => {
|
|
917
1070
|
provenance = next;
|
|
@@ -1080,9 +1233,9 @@ async function sendClickClackMedia(params) {
|
|
|
1080
1233
|
mediaLocalRoots: params.mediaLocalRoots,
|
|
1081
1234
|
mediaReadFile: params.mediaReadFile
|
|
1082
1235
|
});
|
|
1083
|
-
const filename = media.fileName?.trim() || "attachment";
|
|
1084
|
-
mediaFilename = filename;
|
|
1085
1236
|
const contentType = media.contentType?.trim() || "application/octet-stream";
|
|
1237
|
+
const filename = media.fileName?.trim() || `attachment${extensionForMime(contentType) ?? ""}`;
|
|
1238
|
+
mediaFilename = filename;
|
|
1086
1239
|
await dispatch();
|
|
1087
1240
|
upload = await client.createUpload({
|
|
1088
1241
|
workspaceId,
|
|
@@ -1215,6 +1368,219 @@ async function reconcileClickClackUnknownSend(ctx) {
|
|
|
1215
1368
|
};
|
|
1216
1369
|
}
|
|
1217
1370
|
//#endregion
|
|
1371
|
+
//#region extensions/clickclack/src/progress.ts
|
|
1372
|
+
/**
|
|
1373
|
+
* Publishes ClickClack's native ephemeral agent.progress signal for one
|
|
1374
|
+
* OpenClaw turn. ClickClack renders this as its compact "Agent is
|
|
1375
|
+
* responding" status and the detailed progress lines above the composer.
|
|
1376
|
+
*/
|
|
1377
|
+
function normalizedKind(payload) {
|
|
1378
|
+
const kind = payload.kind?.trim().toLowerCase();
|
|
1379
|
+
if (!kind || kind === "preamble" || kind === "analysis" || kind === "thinking" || kind === "reasoning" || kind === "missing") return "commentary";
|
|
1380
|
+
return kind;
|
|
1381
|
+
}
|
|
1382
|
+
function progressText(payload) {
|
|
1383
|
+
const line = buildChannelProgressDraftLine({
|
|
1384
|
+
event: "item",
|
|
1385
|
+
itemId: payload.itemId,
|
|
1386
|
+
toolCallId: payload.toolCallId,
|
|
1387
|
+
itemKind: payload.kind,
|
|
1388
|
+
title: payload.title,
|
|
1389
|
+
name: payload.name,
|
|
1390
|
+
phase: payload.phase,
|
|
1391
|
+
status: payload.status,
|
|
1392
|
+
summary: payload.summary,
|
|
1393
|
+
progressText: payload.progressText,
|
|
1394
|
+
meta: payload.meta,
|
|
1395
|
+
commandBearing: payload.commandBearing
|
|
1396
|
+
})?.text?.trim();
|
|
1397
|
+
if (line) return line;
|
|
1398
|
+
return payload.progressText?.trim() || payload.summary?.trim() || payload.title?.trim() || payload.name?.trim() || payload.meta?.trim() || payload.status?.trim() || "Working";
|
|
1399
|
+
}
|
|
1400
|
+
function isFinal(payload) {
|
|
1401
|
+
const phase = payload.phase?.trim().toLowerCase();
|
|
1402
|
+
const status = payload.status?.trim().toLowerCase();
|
|
1403
|
+
return phase === "end" || status === "completed" || status === "failed" || status === "blocked";
|
|
1404
|
+
}
|
|
1405
|
+
function createLineIdResolver() {
|
|
1406
|
+
const lineIdsByIdentity = /* @__PURE__ */ new Map();
|
|
1407
|
+
const anonymousLinesByKind = /* @__PURE__ */ new Map();
|
|
1408
|
+
let anonymousSequence = 0;
|
|
1409
|
+
return (payload) => {
|
|
1410
|
+
const identities = [payload.itemId?.replace(/^(tool|command):/, ""), payload.toolCallId].map((value) => value?.trim()).filter((value) => Boolean(value));
|
|
1411
|
+
const existingId = identities.map((identity) => lineIdsByIdentity.get(identity)).find(Boolean);
|
|
1412
|
+
if (existingId) {
|
|
1413
|
+
for (const identity of identities) lineIdsByIdentity.set(identity, existingId);
|
|
1414
|
+
return existingId;
|
|
1415
|
+
}
|
|
1416
|
+
if (identities.length > 0) {
|
|
1417
|
+
const id = `item:${identities[0]}`;
|
|
1418
|
+
for (const identity of identities) lineIdsByIdentity.set(identity, id);
|
|
1419
|
+
return id;
|
|
1420
|
+
}
|
|
1421
|
+
const kind = normalizedKind(payload);
|
|
1422
|
+
const anonymousLines = anonymousLinesByKind.get(kind) ?? [];
|
|
1423
|
+
const line = (payload.phase?.trim().toLowerCase() === "start" ? void 0 : anonymousLines.toReversed().find((line) => line.active)) ?? (() => {
|
|
1424
|
+
const created = {
|
|
1425
|
+
id: `item:${kind}:${++anonymousSequence}`,
|
|
1426
|
+
active: true
|
|
1427
|
+
};
|
|
1428
|
+
anonymousLines.push(created);
|
|
1429
|
+
anonymousLinesByKind.set(kind, anonymousLines);
|
|
1430
|
+
return created;
|
|
1431
|
+
})();
|
|
1432
|
+
if (isFinal(payload)) line.active = false;
|
|
1433
|
+
return line.id;
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
const CLICKCLACK_PROGRESS_UPDATE_INTERVAL_MS = 100;
|
|
1437
|
+
const CLICKCLACK_PROGRESS_FINALIZE_GRACE_MS = 1e3;
|
|
1438
|
+
function createClickClackAgentProgressPublisher(params) {
|
|
1439
|
+
let sequence = 0;
|
|
1440
|
+
const queue = [];
|
|
1441
|
+
const queuedLines = /* @__PURE__ */ new Map();
|
|
1442
|
+
let drainPromise;
|
|
1443
|
+
let lineDrainTimer;
|
|
1444
|
+
let started = false;
|
|
1445
|
+
let cleared = false;
|
|
1446
|
+
const seenLines = /* @__PURE__ */ new Set();
|
|
1447
|
+
const resolveLineId = createLineIdResolver();
|
|
1448
|
+
const drain = () => {
|
|
1449
|
+
if (drainPromise) return drainPromise;
|
|
1450
|
+
drainPromise = (async () => {
|
|
1451
|
+
while (queue.length > 0) {
|
|
1452
|
+
const frame = queue.shift();
|
|
1453
|
+
if (!frame) continue;
|
|
1454
|
+
if (frame.lineId) queuedLines.delete(frame.lineId);
|
|
1455
|
+
try {
|
|
1456
|
+
await params.client.publishEphemeral({
|
|
1457
|
+
...params.target,
|
|
1458
|
+
type: "agent.progress",
|
|
1459
|
+
payload: {
|
|
1460
|
+
turn_id: params.turnId,
|
|
1461
|
+
seq: ++sequence,
|
|
1462
|
+
...frame.payload
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
try {
|
|
1467
|
+
params.onError?.(error);
|
|
1468
|
+
} catch {}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
})().finally(() => {
|
|
1472
|
+
drainPromise = void 0;
|
|
1473
|
+
if (queue.length > 0) drain();
|
|
1474
|
+
});
|
|
1475
|
+
return drainPromise;
|
|
1476
|
+
};
|
|
1477
|
+
const enqueue = (payload) => {
|
|
1478
|
+
queue.push({ payload });
|
|
1479
|
+
drain();
|
|
1480
|
+
};
|
|
1481
|
+
const flushQueuedLines = () => {
|
|
1482
|
+
for (const [lineId, frame] of queuedLines) {
|
|
1483
|
+
queuedLines.delete(lineId);
|
|
1484
|
+
queue.push(frame);
|
|
1485
|
+
}
|
|
1486
|
+
drain();
|
|
1487
|
+
};
|
|
1488
|
+
const discardQueuedLines = () => {
|
|
1489
|
+
queuedLines.clear();
|
|
1490
|
+
const controlFrames = queue.filter((frame) => !frame.lineId);
|
|
1491
|
+
queue.splice(0, queue.length, ...controlFrames);
|
|
1492
|
+
};
|
|
1493
|
+
const waitForDrainWithinFinalizeGrace = async (pending) => {
|
|
1494
|
+
let timeout;
|
|
1495
|
+
let drained = false;
|
|
1496
|
+
try {
|
|
1497
|
+
await Promise.race([pending.then(() => {
|
|
1498
|
+
drained = true;
|
|
1499
|
+
}), new Promise((resolve) => {
|
|
1500
|
+
timeout = setTimeout(resolve, CLICKCLACK_PROGRESS_FINALIZE_GRACE_MS);
|
|
1501
|
+
})]);
|
|
1502
|
+
return drained;
|
|
1503
|
+
} finally {
|
|
1504
|
+
if (timeout) clearTimeout(timeout);
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
const scheduleLineDrain = () => {
|
|
1508
|
+
if (lineDrainTimer) return;
|
|
1509
|
+
lineDrainTimer = setTimeout(() => {
|
|
1510
|
+
lineDrainTimer = void 0;
|
|
1511
|
+
flushQueuedLines();
|
|
1512
|
+
}, CLICKCLACK_PROGRESS_UPDATE_INTERVAL_MS);
|
|
1513
|
+
};
|
|
1514
|
+
const enqueueLine = (lineId, payload) => {
|
|
1515
|
+
const queued = queuedLines.get(lineId);
|
|
1516
|
+
if (queued) {
|
|
1517
|
+
const op = payload.op === "finalize" ? "finalize" : queued.payload.op === "append" ? "append" : payload.op;
|
|
1518
|
+
queued.payload = {
|
|
1519
|
+
...payload,
|
|
1520
|
+
...op ? { op } : {}
|
|
1521
|
+
};
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
const frame = {
|
|
1525
|
+
lineId,
|
|
1526
|
+
payload
|
|
1527
|
+
};
|
|
1528
|
+
queuedLines.set(lineId, frame);
|
|
1529
|
+
scheduleLineDrain();
|
|
1530
|
+
};
|
|
1531
|
+
return {
|
|
1532
|
+
start() {
|
|
1533
|
+
if (started) return;
|
|
1534
|
+
started = true;
|
|
1535
|
+
enqueue({
|
|
1536
|
+
op: "append",
|
|
1537
|
+
line: {
|
|
1538
|
+
id: "turn",
|
|
1539
|
+
kind: "commentary",
|
|
1540
|
+
text: params.agentLabel ? `${params.agentLabel} is responding` : "Agent is responding",
|
|
1541
|
+
status: "running"
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
},
|
|
1545
|
+
onItemEvent(payload) {
|
|
1546
|
+
if (!started || cleared) return false;
|
|
1547
|
+
const id = resolveLineId(payload);
|
|
1548
|
+
const final = isFinal(payload);
|
|
1549
|
+
const kind = normalizedKind(payload);
|
|
1550
|
+
const retractsExistingCommentary = kind === "commentary" && seenLines.has(id) && payload.progressText !== void 0 && payload.progressText.trim() === "";
|
|
1551
|
+
if (retractsExistingCommentary && queuedLines.get(id)?.payload.op === "append") {
|
|
1552
|
+
queuedLines.delete(id);
|
|
1553
|
+
seenLines.delete(id);
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
const line = {
|
|
1557
|
+
id,
|
|
1558
|
+
kind,
|
|
1559
|
+
text: retractsExistingCommentary ? "" : progressText(payload),
|
|
1560
|
+
status: payload.status?.trim() || (final ? "completed" : "running")
|
|
1561
|
+
};
|
|
1562
|
+
if (payload.name?.trim()) line.tool_name = payload.name.trim();
|
|
1563
|
+
enqueueLine(id, {
|
|
1564
|
+
op: final ? "finalize" : seenLines.has(id) ? "update" : "append",
|
|
1565
|
+
line
|
|
1566
|
+
});
|
|
1567
|
+
seenLines.add(id);
|
|
1568
|
+
return false;
|
|
1569
|
+
},
|
|
1570
|
+
async finalize() {
|
|
1571
|
+
if (!started || cleared) return;
|
|
1572
|
+
cleared = true;
|
|
1573
|
+
if (lineDrainTimer) {
|
|
1574
|
+
clearTimeout(lineDrainTimer);
|
|
1575
|
+
lineDrainTimer = void 0;
|
|
1576
|
+
}
|
|
1577
|
+
flushQueuedLines();
|
|
1578
|
+
enqueue({ op: "clear" });
|
|
1579
|
+
if (!await waitForDrainWithinFinalizeGrace(drain())) discardQueuedLines();
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
//#endregion
|
|
1218
1584
|
//#region extensions/clickclack/src/inbound.ts
|
|
1219
1585
|
const CHANNEL_ID$1 = "clickclack";
|
|
1220
1586
|
const CLICKCLACK_MESSAGE_ID_PATTERN = /^msg_[0-9a-hjkmnp-tv-z]{26}$/u;
|
|
@@ -1265,19 +1631,56 @@ async function handleClickClackInbound(params) {
|
|
|
1265
1631
|
config: params.config,
|
|
1266
1632
|
message
|
|
1267
1633
|
});
|
|
1268
|
-
if (!access.shouldDispatch) return;
|
|
1634
|
+
if (!access.shouldDispatch || !access.channelIngress) return;
|
|
1269
1635
|
const conversationId = message.channel_id || message.direct_conversation_id;
|
|
1270
1636
|
if (!conversationId) return;
|
|
1271
1637
|
const { discussionRoute, isDirect, route, target } = access.preparedRoute;
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
message,
|
|
1277
|
-
route,
|
|
1278
|
-
target,
|
|
1638
|
+
const progress = params.account.nativeProgress ? createClickClackAgentProgressPublisher({
|
|
1639
|
+
client: createClickClackClient({
|
|
1640
|
+
baseUrl: params.account.apiEndpoint,
|
|
1641
|
+
token: params.account.token,
|
|
1279
1642
|
correlationId: params.correlationId
|
|
1280
|
-
})
|
|
1643
|
+
}),
|
|
1644
|
+
target: message.channel_id ? {
|
|
1645
|
+
workspaceId: message.workspace_id,
|
|
1646
|
+
channelId: message.channel_id
|
|
1647
|
+
} : {
|
|
1648
|
+
workspaceId: message.workspace_id,
|
|
1649
|
+
conversationId
|
|
1650
|
+
},
|
|
1651
|
+
turnId: message.id,
|
|
1652
|
+
agentLabel: params.account.name?.trim() || params.account.botHandle?.trim() || params.account.agentId?.trim() || params.account.accountId,
|
|
1653
|
+
onError: (error) => {
|
|
1654
|
+
runtime.logging.getChildLogger({
|
|
1655
|
+
plugin: "clickclack",
|
|
1656
|
+
feature: "agent-progress"
|
|
1657
|
+
}).warn(`clickclack progress publish failed: ${String(error)}`);
|
|
1658
|
+
}
|
|
1659
|
+
}) : void 0;
|
|
1660
|
+
if (params.account.replyMode === "model" && !discussionRoute) {
|
|
1661
|
+
if (access.botLoopProtection) {
|
|
1662
|
+
const loopResult = recordChannelBotPairLoopAndCheckSuppression(access.botLoopProtection);
|
|
1663
|
+
if (loopResult.suppressed) {
|
|
1664
|
+
runtime.logging.getChildLogger({
|
|
1665
|
+
plugin: "clickclack",
|
|
1666
|
+
feature: "bot-loop-protection"
|
|
1667
|
+
}).warn(`[${params.account.accountId}] ClickClack bot-pair loop suppressed for ${Math.max(0, Math.ceil((loopResult.cooldownUntilMs - Date.now()) / 1e3))}s`);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
progress?.start();
|
|
1672
|
+
try {
|
|
1673
|
+
await dispatchModelReply({
|
|
1674
|
+
account: params.account,
|
|
1675
|
+
cfg: params.config,
|
|
1676
|
+
message,
|
|
1677
|
+
route,
|
|
1678
|
+
target,
|
|
1679
|
+
correlationId: params.correlationId
|
|
1680
|
+
});
|
|
1681
|
+
} finally {
|
|
1682
|
+
await progress?.finalize();
|
|
1683
|
+
}
|
|
1281
1684
|
return;
|
|
1282
1685
|
}
|
|
1283
1686
|
let turnProvenance;
|
|
@@ -1307,7 +1710,8 @@ async function handleClickClackInbound(params) {
|
|
|
1307
1710
|
timestamp: new Date(message.created_at),
|
|
1308
1711
|
body: message.body
|
|
1309
1712
|
});
|
|
1310
|
-
const ctxPayload =
|
|
1713
|
+
const ctxPayload = (params.buildContext ?? buildChannelInboundEventContext)({
|
|
1714
|
+
channelIngress: access.channelIngress,
|
|
1311
1715
|
channel: CHANNEL_ID$1,
|
|
1312
1716
|
accountId: route.accountId ?? params.account.accountId,
|
|
1313
1717
|
messageId: message.id,
|
|
@@ -1354,20 +1758,27 @@ async function handleClickClackInbound(params) {
|
|
|
1354
1758
|
}
|
|
1355
1759
|
});
|
|
1356
1760
|
const runId = resolveClickClackAgentRunId(message.id);
|
|
1357
|
-
const activityReplyOptions =
|
|
1358
|
-
onModelSelected: (ctx) => {
|
|
1761
|
+
const activityReplyOptions = {
|
|
1762
|
+
...activity ? { onModelSelected: (ctx) => {
|
|
1359
1763
|
turnProvenance = {
|
|
1360
1764
|
model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
|
|
1361
1765
|
thinking: ctx.thinkLevel
|
|
1362
1766
|
};
|
|
1363
|
-
activity
|
|
1364
|
-
},
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1767
|
+
activity.setProvenance(turnProvenance);
|
|
1768
|
+
} } : {},
|
|
1769
|
+
...progress || activity ? {
|
|
1770
|
+
onItemEvent: (payload) => {
|
|
1771
|
+
progress?.onItemEvent(payload);
|
|
1772
|
+
activity?.onItemEvent(payload);
|
|
1773
|
+
return false;
|
|
1774
|
+
},
|
|
1775
|
+
commentaryProgressEnabled: true,
|
|
1776
|
+
suppressDefaultToolProgressMessages: true,
|
|
1777
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: true
|
|
1778
|
+
} : {}
|
|
1779
|
+
};
|
|
1780
|
+
progress?.start();
|
|
1781
|
+
const dispatch = () => runtime.channel.inbound.dispatch({
|
|
1371
1782
|
cfg: params.config,
|
|
1372
1783
|
channel: CHANNEL_ID$1,
|
|
1373
1784
|
accountId: params.account.accountId,
|
|
@@ -1377,11 +1788,12 @@ async function handleClickClackInbound(params) {
|
|
|
1377
1788
|
sessionKey: route.sessionKey
|
|
1378
1789
|
},
|
|
1379
1790
|
ctxPayload,
|
|
1791
|
+
botLoopProtection: access.botLoopProtection,
|
|
1380
1792
|
toolsAllow: params.account.toolsAllow,
|
|
1381
|
-
replyOptions:
|
|
1793
|
+
replyOptions: {
|
|
1382
1794
|
...runId ? { runId } : {},
|
|
1383
1795
|
...activityReplyOptions
|
|
1384
|
-
}
|
|
1796
|
+
},
|
|
1385
1797
|
delivery: {
|
|
1386
1798
|
deliver: async (payload) => {
|
|
1387
1799
|
if (hasClickClackReplyMedia(payload)) throw new Error("ClickClack media reply requires durable delivery");
|
|
@@ -1423,8 +1835,9 @@ async function handleClickClackInbound(params) {
|
|
|
1423
1835
|
} }
|
|
1424
1836
|
});
|
|
1425
1837
|
try {
|
|
1426
|
-
await
|
|
1838
|
+
await dispatch();
|
|
1427
1839
|
} finally {
|
|
1840
|
+
await progress?.finalize();
|
|
1428
1841
|
await activity?.finalize();
|
|
1429
1842
|
}
|
|
1430
1843
|
}
|
|
@@ -1432,8 +1845,7 @@ async function handleClickClackInbound(params) {
|
|
|
1432
1845
|
//#region extensions/clickclack/src/gateway.ts
|
|
1433
1846
|
const CLICKCLACK_EVENT_PAGE_LIMIT = 500;
|
|
1434
1847
|
function payloadString(event, key) {
|
|
1435
|
-
|
|
1436
|
-
return typeof value === "string" ? value : "";
|
|
1848
|
+
return readStringField(event.payload, key) ?? "";
|
|
1437
1849
|
}
|
|
1438
1850
|
function eventCorrelationId(event) {
|
|
1439
1851
|
return normalizeClickClackCorrelationId(event.payload?.correlation_id);
|
|
@@ -1441,25 +1853,16 @@ function eventCorrelationId(event) {
|
|
|
1441
1853
|
async function resolveEventMessage(params) {
|
|
1442
1854
|
const messageId = payloadString(params.event, "message_id");
|
|
1443
1855
|
if (!messageId) return null;
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
return (await params.client.thread(rootId)).replies.find((message) => message.id === messageId) ?? null;
|
|
1856
|
+
try {
|
|
1857
|
+
return await params.client.message(messageId);
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
if (error instanceof ClickClackHttpError && error.status === 404) return null;
|
|
1860
|
+
throw error;
|
|
1450
1861
|
}
|
|
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
1862
|
}
|
|
1460
1863
|
function parseSocketEvent(data) {
|
|
1461
1864
|
try {
|
|
1462
|
-
return JSON.parse(
|
|
1865
|
+
return JSON.parse(rawDataToString(data));
|
|
1463
1866
|
} catch {
|
|
1464
1867
|
return null;
|
|
1465
1868
|
}
|
|
@@ -1476,8 +1879,11 @@ async function processEvent(params) {
|
|
|
1476
1879
|
}) : params.client,
|
|
1477
1880
|
event: params.event
|
|
1478
1881
|
});
|
|
1479
|
-
if (!message
|
|
1480
|
-
|
|
1882
|
+
if (!message) {
|
|
1883
|
+
params.log?.warn?.(`[${params.account.accountId}] skipped unreadable ClickClack message before agent dispatch: type=${params.event.type} messageId=${payloadString(params.event, "message_id") || "unknown"}`);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (message.author_id === params.botUserId) return;
|
|
1481
1887
|
const access = await resolveClickClackInboundAccess({
|
|
1482
1888
|
account: params.account,
|
|
1483
1889
|
config: params.config,
|
|
@@ -1492,6 +1898,7 @@ async function processEvent(params) {
|
|
|
1492
1898
|
config: params.config,
|
|
1493
1899
|
message,
|
|
1494
1900
|
access,
|
|
1901
|
+
buildContext: params.buildContext,
|
|
1495
1902
|
...correlationId ? { correlationId } : {}
|
|
1496
1903
|
});
|
|
1497
1904
|
}
|
|
@@ -1536,16 +1943,19 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1536
1943
|
client,
|
|
1537
1944
|
event,
|
|
1538
1945
|
botUserId: account.botUserId,
|
|
1946
|
+
buildContext: ctx.channelRuntime?.inbound.buildContext,
|
|
1539
1947
|
log: ctx.log
|
|
1540
1948
|
});
|
|
1541
1949
|
if (account.commandMenu) await syncClickClackCommandMenu({
|
|
1542
1950
|
cfg: ctx.cfg,
|
|
1543
1951
|
client,
|
|
1544
|
-
log: ctx.log
|
|
1952
|
+
log: ctx.log,
|
|
1953
|
+
accountId: account.accountId
|
|
1545
1954
|
});
|
|
1546
1955
|
ctx.setStatus({
|
|
1547
1956
|
accountId: account.accountId,
|
|
1548
1957
|
running: true,
|
|
1958
|
+
lifecycle: "starting",
|
|
1549
1959
|
configured: true,
|
|
1550
1960
|
enabled: account.enabled,
|
|
1551
1961
|
baseUrl: account.baseUrl
|
|
@@ -1601,6 +2011,9 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1601
2011
|
};
|
|
1602
2012
|
ctx.abortSignal.addEventListener("abort", abort, { once: true });
|
|
1603
2013
|
removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
|
|
2014
|
+
socket.on("open", () => {
|
|
2015
|
+
ctx.setStatus(channelReadyPatch({ accountId: account.accountId }));
|
|
2016
|
+
});
|
|
1604
2017
|
socket.on("message", (data) => {
|
|
1605
2018
|
if (closing || settled) return;
|
|
1606
2019
|
messageQueue = messageQueue.then(async () => {
|
|
@@ -1616,6 +2029,11 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1616
2029
|
});
|
|
1617
2030
|
socket.on("close", () => {
|
|
1618
2031
|
closing = true;
|
|
2032
|
+
if (!ctx.abortSignal.aborted) ctx.setStatus({
|
|
2033
|
+
accountId: account.accountId,
|
|
2034
|
+
connected: false,
|
|
2035
|
+
lifecycle: "recovering"
|
|
2036
|
+
});
|
|
1619
2037
|
finishAfterQueuedMessages();
|
|
1620
2038
|
});
|
|
1621
2039
|
socket.on("error", (error) => {
|
|
@@ -1625,6 +2043,12 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1625
2043
|
}
|
|
1626
2044
|
if (closing) return;
|
|
1627
2045
|
ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
|
|
2046
|
+
ctx.setStatus({
|
|
2047
|
+
accountId: account.accountId,
|
|
2048
|
+
connected: false,
|
|
2049
|
+
lifecycle: "recovering",
|
|
2050
|
+
lastError: error instanceof Error ? error.message : String(error)
|
|
2051
|
+
});
|
|
1628
2052
|
closing = true;
|
|
1629
2053
|
socket.close();
|
|
1630
2054
|
});
|
|
@@ -1636,10 +2060,7 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1636
2060
|
}
|
|
1637
2061
|
}
|
|
1638
2062
|
} finally {
|
|
1639
|
-
ctx.setStatus({
|
|
1640
|
-
accountId: account.accountId,
|
|
1641
|
-
running: false
|
|
1642
|
-
});
|
|
2063
|
+
ctx.setStatus(channelStoppedPatch({ accountId: account.accountId }));
|
|
1643
2064
|
}
|
|
1644
2065
|
}
|
|
1645
2066
|
//#endregion
|
|
@@ -1737,12 +2158,12 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1737
2158
|
capabilities: {
|
|
1738
2159
|
chatTypes: ["direct", "group"],
|
|
1739
2160
|
threads: true,
|
|
2161
|
+
media: true,
|
|
1740
2162
|
blockStreaming: true
|
|
1741
2163
|
},
|
|
1742
2164
|
reload: { configPrefixes: ["channels.clickclack"] },
|
|
1743
2165
|
configSchema: clickClackConfigSchema,
|
|
1744
2166
|
config: clickClackConfigAdapter,
|
|
1745
|
-
setup: clickClackSetupAdapter,
|
|
1746
2167
|
setupContract: clickClackSetupContract,
|
|
1747
2168
|
setupWizard: clickClackSetupWizard,
|
|
1748
2169
|
secrets: {
|
|
@@ -1804,7 +2225,11 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1804
2225
|
name: account.name,
|
|
1805
2226
|
enabled: account.enabled,
|
|
1806
2227
|
configured: account.configured,
|
|
1807
|
-
|
|
2228
|
+
extra: {
|
|
2229
|
+
baseUrl: account.baseUrl,
|
|
2230
|
+
tokenSource: account.tokenSource,
|
|
2231
|
+
tokenStatus: account.tokenStatus
|
|
2232
|
+
}
|
|
1808
2233
|
})
|
|
1809
2234
|
}),
|
|
1810
2235
|
gateway: { startAccount: startClickClackGatewayAccount },
|
|
@@ -1849,4 +2274,4 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1849
2274
|
}
|
|
1850
2275
|
});
|
|
1851
2276
|
//#endregion
|
|
1852
|
-
export {
|
|
2277
|
+
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 };
|