@openclaw/clickclack 2026.7.2-beta.5 → 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-DfcFlLjC.js → channel-DaHgaTy1.js} +1319 -769
- 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-D1H9vheM.js → runtime-api-CPL3m5AD.js} +218 -231
- package/dist/runtime-api.js +3 -3
- package/dist/{setup-claim-BFU16zoo.js → setup-claim-CQjgPGxU.js} +1 -1
- package/dist/setup-plugin-api.js +1 -2
- package/dist/{setup-surface-CD6MUC1g.js → setup-surface-DjT-gmMe.js} +101 -31
- package/openclaw.plugin.json +272 -11
- package/package.json +8 -5
|
@@ -1,722 +1,1078 @@
|
|
|
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
|
-
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
10
|
-
import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
|
|
11
|
-
import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel-inbound";
|
|
12
14
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
13
15
|
import { createHash, randomUUID } from "node:crypto";
|
|
16
|
+
import { buildMentionRegexes, normalizeMentionText } from "openclaw/plugin-sdk/channel-mention-gating";
|
|
17
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
18
|
+
import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
|
|
19
|
+
import { buildChannelInboundEventContext, createChannelInboundEnvelopeBuilder, recordChannelBotPairLoopAndCheckSuppression } from "openclaw/plugin-sdk/channel-inbound";
|
|
20
|
+
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
|
|
14
21
|
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
|
15
22
|
import { FormatCapabilityProfile, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
16
|
-
//#region extensions/clickclack/src/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*/
|
|
31
|
-
const CHANNEL_ID$2 = "clickclack";
|
|
32
|
-
function normalizeClickClackUserId(value) {
|
|
33
|
-
const trimmed = value.trim();
|
|
34
|
-
if (!trimmed) return null;
|
|
35
|
-
const withoutProvider = trimmed.replace(/^(clickclack|cc):/i, "").trim();
|
|
36
|
-
return withoutProvider.match(/^dm:(.+)$/i)?.[1]?.trim() || withoutProvider || null;
|
|
23
|
+
//#region extensions/clickclack/src/discussions/binding-generation.ts
|
|
24
|
+
const DISCUSSION_GENERATIONS_NAMESPACE = "discussion-binding-generations";
|
|
25
|
+
const MAX_PENDING_DISCUSSION_GENERATIONS = 1e4;
|
|
26
|
+
const storesByRuntime$2 = /* @__PURE__ */ new WeakMap();
|
|
27
|
+
function getGenerationStore(runtime) {
|
|
28
|
+
const existing = storesByRuntime$2.get(runtime);
|
|
29
|
+
if (existing) return existing;
|
|
30
|
+
const created = runtime.state.openSyncKeyedStore({
|
|
31
|
+
namespace: DISCUSSION_GENERATIONS_NAMESPACE,
|
|
32
|
+
maxEntries: MAX_PENDING_DISCUSSION_GENERATIONS,
|
|
33
|
+
overflowPolicy: "reject-new"
|
|
34
|
+
});
|
|
35
|
+
storesByRuntime$2.set(runtime, created);
|
|
36
|
+
return created;
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
cfg,
|
|
59
|
-
subject: { stableId: params.message.author_id },
|
|
60
|
-
conversation: {
|
|
61
|
-
kind: isDirect ? "direct" : "group",
|
|
62
|
-
id: isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id
|
|
63
|
-
},
|
|
64
|
-
allowFrom: params.account.allowFrom,
|
|
65
|
-
dmPolicy: "allowlist",
|
|
66
|
-
groupPolicy: "allowlist",
|
|
67
|
-
command: shouldCheckCommand ? {
|
|
68
|
-
cfg,
|
|
69
|
-
modeWhenAccessGroupsOff: "configured"
|
|
70
|
-
} : false
|
|
38
|
+
/** Reserves a generation so an interrupted channel create can be adopted on retry. */
|
|
39
|
+
function reserveDiscussionBindingGeneration(params) {
|
|
40
|
+
const store = getGenerationStore(params.runtime);
|
|
41
|
+
const existing = store.lookup(params.sessionKey);
|
|
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
|
+
}
|
|
52
|
+
const generation = (params.createGeneration ?? randomUUID)();
|
|
53
|
+
store.register(params.sessionKey, {
|
|
54
|
+
accountId: params.accountId,
|
|
55
|
+
credentialFingerprint: params.credentialFingerprint,
|
|
56
|
+
destinationIdentity: params.destinationIdentity,
|
|
57
|
+
generation
|
|
71
58
|
});
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
return generation;
|
|
60
|
+
}
|
|
61
|
+
/** Clears only the completed reservation; future opens must mint a new ownership ref. */
|
|
62
|
+
function clearDiscussionBindingGeneration(params) {
|
|
63
|
+
const store = getGenerationStore(params.runtime);
|
|
64
|
+
const existing = store.lookup(params.sessionKey);
|
|
65
|
+
if (!existing) return;
|
|
66
|
+
if (params.expectedGeneration && existing.generation !== params.expectedGeneration) return;
|
|
67
|
+
store.delete(params.sessionKey);
|
|
68
|
+
}
|
|
69
|
+
/** Quarantines a destination before the first fallible channel create. */
|
|
70
|
+
function recordPendingDiscussionOpen(params) {
|
|
71
|
+
const store = getGenerationStore(params.runtime);
|
|
72
|
+
const existing = store.lookup(params.sessionKey);
|
|
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");
|
|
75
|
+
store.register(params.sessionKey, {
|
|
76
|
+
...existing,
|
|
77
|
+
pending: params.pending
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function listPendingDiscussionOpens(runtime) {
|
|
81
|
+
return getGenerationStore(runtime).entries().flatMap((entry) => entry.value.pending ? [{
|
|
82
|
+
sessionKey: entry.key,
|
|
83
|
+
generation: entry.value.generation,
|
|
84
|
+
...entry.value.pending
|
|
85
|
+
}] : []);
|
|
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
|
+
}
|
|
99
|
+
function hasPendingDiscussionOpenForDestination(params) {
|
|
100
|
+
const serverBaseUrl = params.serverBaseUrl.replace(/\/+$/u, "");
|
|
101
|
+
return listPendingDiscussionOpens(params.runtime).some((pending) => pending.serverBaseUrl === serverBaseUrl && pending.workspaceId === params.workspaceId);
|
|
76
102
|
}
|
|
77
103
|
//#endregion
|
|
78
|
-
//#region extensions/clickclack/src/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
|
|
82
|
-
const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
|
|
83
|
-
function truncateCodePoints(value, maxLength) {
|
|
84
|
-
return Array.from(value).slice(0, maxLength).join("");
|
|
104
|
+
//#region extensions/clickclack/src/discussions/naming.ts
|
|
105
|
+
function shortSessionHash(sessionKey) {
|
|
106
|
+
return createHash("sha256").update(sessionKey).digest("hex").slice(0, 32);
|
|
85
107
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
return spec.acceptsArgs ? "[args]" : "";
|
|
108
|
+
function slugifyLabel(label) {
|
|
109
|
+
return label.normalize("NFKD").replace(/[\u0300-\u036f]/gu, "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 80).replace(/-+$/gu, "");
|
|
89
110
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
return
|
|
111
|
+
function fallbackDiscussionLabel(sessionKey, agentId) {
|
|
112
|
+
const agentSegment = agentId?.trim() ? slugifyLabel(agentId) : "";
|
|
113
|
+
const hash = shortSessionHash(sessionKey).slice(0, 8);
|
|
114
|
+
return `s-${agentSegment ? `${agentSegment}-` : ""}${hash}`;
|
|
94
115
|
}
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
const seen = /* @__PURE__ */ new Set();
|
|
98
|
-
const invalidNames = /* @__PURE__ */ new Set();
|
|
99
|
-
for (const spec of specs) {
|
|
100
|
-
if (spec.isAlias) continue;
|
|
101
|
-
const command = spec.name.trim().toLowerCase();
|
|
102
|
-
if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
|
|
103
|
-
invalidNames.add(spec.name.trim() || "<empty>");
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (seen.has(command)) continue;
|
|
107
|
-
seen.add(command);
|
|
108
|
-
const description = spec.description.trim() || command;
|
|
109
|
-
commands.push({
|
|
110
|
-
command,
|
|
111
|
-
description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
|
|
112
|
-
args_hint: commandArgsHint(spec)
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
|
|
116
|
-
return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
|
|
116
|
+
function resolveDiscussionLabel(entry, sessionKey, agentId) {
|
|
117
|
+
return entry.label?.trim() || entry.displayName?.trim() || entry.subject?.trim() || fallbackDiscussionLabel(sessionKey, agentId);
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
function truncateDiscussionDisplayTitle(label) {
|
|
120
|
+
return Array.from(label).slice(0, 200).join("").trim();
|
|
121
|
+
}
|
|
122
|
+
function slugifyDiscussionLabel(label, sessionKey) {
|
|
123
|
+
return slugifyLabel(label) || fallbackDiscussionLabel(sessionKey);
|
|
124
|
+
}
|
|
125
|
+
function discussionSessionPeerId(identity) {
|
|
126
|
+
return `disc-${createHash("sha256").update([
|
|
127
|
+
identity.mainSessionKey,
|
|
128
|
+
identity.sessionId,
|
|
129
|
+
identity.serverBaseUrl,
|
|
130
|
+
identity.channelId,
|
|
131
|
+
identity.externalRef
|
|
132
|
+
].join("\0")).digest("hex").slice(0, 32)}`;
|
|
133
|
+
}
|
|
134
|
+
function isDiscussionSessionKey(sessionKey) {
|
|
135
|
+
return sessionKey.includes(":clickclack:") && /:channel:disc-[0-9a-f]{32}$/u.test(sessionKey);
|
|
136
|
+
}
|
|
137
|
+
function discussionExternalRef(installationId, mainSessionKey, destinationIdentity, bindingGeneration) {
|
|
138
|
+
return `openclaw:${installationId}:${shortSessionHash([
|
|
139
|
+
mainSessionKey,
|
|
140
|
+
destinationIdentity,
|
|
141
|
+
bindingGeneration
|
|
142
|
+
].join("\0"))}`;
|
|
143
|
+
}
|
|
144
|
+
function discussionCredentialFingerprint(token) {
|
|
145
|
+
return createHash("sha256").update(token).digest("hex");
|
|
146
|
+
}
|
|
147
|
+
function discussionSessionKey(params) {
|
|
148
|
+
return params.runtime.channel.routing.buildAgentSessionKey({
|
|
149
|
+
agentId: params.agentId,
|
|
150
|
+
channel: "clickclack",
|
|
151
|
+
accountId: params.accountId,
|
|
152
|
+
peer: {
|
|
153
|
+
kind: "channel",
|
|
154
|
+
id: discussionSessionPeerId(params)
|
|
131
155
|
}
|
|
132
|
-
|
|
133
|
-
}
|
|
156
|
+
});
|
|
134
157
|
}
|
|
135
158
|
//#endregion
|
|
136
|
-
//#region extensions/clickclack/src/
|
|
159
|
+
//#region extensions/clickclack/src/discussions/binding-store.ts
|
|
160
|
+
function bindingMatchesActiveSessionIncarnation(runtime, sessionKey, binding) {
|
|
161
|
+
const entry = runtime.agent.session.getSessionEntry({
|
|
162
|
+
sessionKey,
|
|
163
|
+
readConsistency: "latest"
|
|
164
|
+
});
|
|
165
|
+
return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId && entry.archivedAt === void 0);
|
|
166
|
+
}
|
|
137
167
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* Ported from the clickglass agent-bridge sidecar, adapted from gateway
|
|
143
|
-
* websocket frames to the in-process `replyOptions.onItemEvent` seam:
|
|
144
|
-
*
|
|
145
|
-
* - Commentary/reasoning arrives as cumulative text snapshots per item id.
|
|
146
|
-
* Each segment becomes one durable row: POSTed when the segment starts
|
|
147
|
-
* streaming and PATCHed (debounced) as the snapshot grows, so prose
|
|
148
|
-
* interleaves chronologically with tool rows.
|
|
149
|
-
* - Tool/step items can emit several frames (start/update/complete) for the
|
|
150
|
-
* same call, sometimes with lane-prefixed ids (`tool:X`, `command:X`).
|
|
151
|
-
* Normalize the prefix away to one key per call, POST one row on the first
|
|
152
|
-
* frame, and PATCH it when a later frame carries a strictly longer body.
|
|
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.
|
|
153
171
|
*/
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"reasoning"
|
|
174
|
-
]);
|
|
175
|
-
/** Provider-specific reasoning lanes normalized into one ClickClack label. */
|
|
176
|
-
const THINKING_ITEM_KINDS = /* @__PURE__ */ new Set([
|
|
177
|
-
"analysis",
|
|
178
|
-
"thinking",
|
|
179
|
-
"reasoning"
|
|
180
|
-
]);
|
|
181
|
-
function normalizedItemKind(payload) {
|
|
182
|
-
return payload.kind?.trim().toLowerCase() ?? "";
|
|
183
|
-
}
|
|
184
|
-
function commentaryBody(payload) {
|
|
185
|
-
const text = payload.progressText?.trim() || payload.summary?.trim() || payload.meta?.trim() || "";
|
|
186
|
-
if (!text) return "";
|
|
187
|
-
const kind = normalizedItemKind(payload);
|
|
188
|
-
if (THINKING_ITEM_KINDS.has(kind)) return `**Thinking**\n\n${text}`;
|
|
189
|
-
return text;
|
|
172
|
+
function attachBindingToCurrentActiveSession(params) {
|
|
173
|
+
const entry = params.runtime.agent.session.getSessionEntry({
|
|
174
|
+
sessionKey: params.sessionKey,
|
|
175
|
+
readConsistency: "latest"
|
|
176
|
+
});
|
|
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;
|
|
190
191
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
title: payload.title,
|
|
198
|
-
name: payload.name,
|
|
199
|
-
phase: payload.phase,
|
|
200
|
-
status: payload.status,
|
|
201
|
-
summary: payload.summary,
|
|
202
|
-
progressText: payload.progressText,
|
|
203
|
-
meta: payload.meta
|
|
204
|
-
})?.text?.trim();
|
|
205
|
-
if (line) return line;
|
|
206
|
-
const head = payload.name?.trim() || payload.title?.trim();
|
|
207
|
-
const text = payload.progressText?.trim() || payload.summary?.trim();
|
|
208
|
-
if (head && text) return `**${head}**\n\n${text}`;
|
|
209
|
-
if (text) return text;
|
|
210
|
-
if (head) return head;
|
|
211
|
-
return payload.status?.trim() || payload.kind?.trim() || "";
|
|
192
|
+
const DISCUSSION_BINDINGS_NAMESPACE = "discussion-bindings";
|
|
193
|
+
const MAX_DISCUSSION_BINDINGS = 1e4;
|
|
194
|
+
const MAX_RETAINED_DETACHED_DISCUSSION_BINDINGS = 1e3;
|
|
195
|
+
const storesByRuntime$1 = /* @__PURE__ */ new WeakMap();
|
|
196
|
+
function channelKey(serverBaseUrl, channelId) {
|
|
197
|
+
return `${serverBaseUrl.replace(/\/+$/u, "")}\0${channelId}`;
|
|
212
198
|
}
|
|
213
|
-
/**
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
*/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
199
|
+
/** SQLite-backed session/channel bindings with a process-local inbound lookup index. */
|
|
200
|
+
var ClickClackDiscussionBindingStore = class {
|
|
201
|
+
#store;
|
|
202
|
+
#sessionByChannel = /* @__PURE__ */ new Map();
|
|
203
|
+
#mainByDiscussionSession = /* @__PURE__ */ new Map();
|
|
204
|
+
#detachedAtBySession = /* @__PURE__ */ new Map();
|
|
205
|
+
#runtime;
|
|
206
|
+
constructor(runtime) {
|
|
207
|
+
this.#runtime = runtime;
|
|
208
|
+
this.#store = runtime.state.openSyncKeyedStore({
|
|
209
|
+
namespace: DISCUSSION_BINDINGS_NAMESPACE,
|
|
210
|
+
maxEntries: MAX_DISCUSSION_BINDINGS,
|
|
211
|
+
overflowPolicy: "reject-new"
|
|
226
212
|
});
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
213
|
+
for (const entry of this.#store.entries()) this.#index(entry.key, entry.value);
|
|
214
|
+
}
|
|
215
|
+
get(sessionKey) {
|
|
216
|
+
return this.#store.lookup(sessionKey);
|
|
217
|
+
}
|
|
218
|
+
hasCapacity(sessionKey) {
|
|
219
|
+
return this.get(sessionKey) !== void 0 || this.#store.entries().length < MAX_DISCUSSION_BINDINGS;
|
|
220
|
+
}
|
|
221
|
+
getByChannel(serverBaseUrl, channelId) {
|
|
222
|
+
const key = channelKey(serverBaseUrl, channelId);
|
|
223
|
+
const sessionKey = this.#sessionByChannel.get(key);
|
|
224
|
+
if (!sessionKey) return;
|
|
225
|
+
const binding = this.get(sessionKey);
|
|
226
|
+
if (!binding) {
|
|
227
|
+
this.#sessionByChannel.delete(key);
|
|
228
|
+
return;
|
|
243
229
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
230
|
+
return {
|
|
231
|
+
sessionKey,
|
|
232
|
+
binding
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
set(sessionKey, binding) {
|
|
236
|
+
const previous = this.get(sessionKey);
|
|
237
|
+
this.#store.register(sessionKey, binding);
|
|
238
|
+
if (previous) this.#unindex(sessionKey, previous);
|
|
239
|
+
this.#index(sessionKey, binding);
|
|
240
|
+
}
|
|
241
|
+
delete(sessionKey) {
|
|
242
|
+
const previous = this.get(sessionKey);
|
|
243
|
+
const deleted = this.#store.delete(sessionKey);
|
|
244
|
+
if (deleted && previous) this.#unindex(sessionKey, previous);
|
|
245
|
+
return deleted;
|
|
246
|
+
}
|
|
247
|
+
getByDiscussionSession(sideSessionKey) {
|
|
248
|
+
const sessionKey = this.#mainByDiscussionSession.get(sideSessionKey);
|
|
249
|
+
if (!sessionKey) return;
|
|
250
|
+
const binding = this.get(sessionKey);
|
|
251
|
+
return binding ? {
|
|
252
|
+
sessionKey,
|
|
253
|
+
binding
|
|
254
|
+
} : void 0;
|
|
255
|
+
}
|
|
256
|
+
entries() {
|
|
257
|
+
return this.#store.entries().map((entry) => ({
|
|
258
|
+
sessionKey: entry.key,
|
|
259
|
+
binding: entry.value
|
|
260
|
+
}));
|
|
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
271
|
}
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
flushCommentary(key);
|
|
278
|
-
}, flushMs);
|
|
279
|
-
};
|
|
280
|
-
const toolRowKey = (payload) => {
|
|
281
|
-
const toolCallId = payload.toolCallId?.trim();
|
|
282
|
-
if (toolCallId) return toolCallId;
|
|
283
|
-
return (payload.itemId?.trim() ?? "").replace(/^(tool|command):/, "");
|
|
284
|
-
};
|
|
285
|
-
const handleDiscreteItem = (payload) => {
|
|
286
|
-
const body = activityBody(payload);
|
|
287
|
-
if (!body) return;
|
|
288
|
-
const kind = TOOL_ITEM_KINDS.has(payload.kind?.trim().toLowerCase() ?? "") ? "agent_tool" : "agent_commentary";
|
|
289
|
-
flushAllCommentary();
|
|
290
|
-
const key = `${kind}:${toolRowKey(payload)}`;
|
|
291
|
-
const existing = toolRows.get(key);
|
|
292
|
-
if (!existing || !toolRowKey(payload)) {
|
|
293
|
-
const row = { body };
|
|
294
|
-
if (toolRowKey(payload)) toolRows.set(key, row);
|
|
295
|
-
enqueue(async () => {
|
|
296
|
-
const posted = await postRow(kind, row.body);
|
|
297
|
-
row.messageId = posted.id;
|
|
298
|
-
row.sentBody = row.body;
|
|
299
|
-
});
|
|
300
|
-
return;
|
|
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();
|
|
301
277
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
278
|
+
return {
|
|
279
|
+
sessionKey: oldestSessionKey,
|
|
280
|
+
binding
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
#index(sessionKey, binding) {
|
|
284
|
+
this.#sessionByChannel.set(channelKey(binding.serverBaseUrl, binding.channelId), sessionKey);
|
|
285
|
+
const sideSessionKey = discussionSessionKey({
|
|
286
|
+
runtime: this.#runtime,
|
|
287
|
+
agentId: binding.agentId,
|
|
288
|
+
mainSessionKey: sessionKey,
|
|
289
|
+
sessionId: binding.sessionId,
|
|
290
|
+
accountId: binding.accountId,
|
|
291
|
+
serverBaseUrl: binding.serverBaseUrl,
|
|
292
|
+
channelId: binding.channelId,
|
|
293
|
+
externalRef: binding.externalRef
|
|
294
|
+
});
|
|
295
|
+
if (sideSessionKey) this.#mainByDiscussionSession.set(sideSessionKey, sessionKey);
|
|
296
|
+
if (binding.detachedAt !== void 0) this.#detachedAtBySession.set(sessionKey, binding.detachedAt);
|
|
297
|
+
}
|
|
298
|
+
#unindex(sessionKey, binding) {
|
|
299
|
+
this.#sessionByChannel.delete(channelKey(binding.serverBaseUrl, binding.channelId));
|
|
300
|
+
const sideSessionKey = discussionSessionKey({
|
|
301
|
+
runtime: this.#runtime,
|
|
302
|
+
agentId: binding.agentId,
|
|
303
|
+
mainSessionKey: sessionKey,
|
|
304
|
+
sessionId: binding.sessionId,
|
|
305
|
+
accountId: binding.accountId,
|
|
306
|
+
serverBaseUrl: binding.serverBaseUrl,
|
|
307
|
+
channelId: binding.channelId,
|
|
308
|
+
externalRef: binding.externalRef
|
|
309
309
|
});
|
|
310
|
+
if (sideSessionKey) this.#mainByDiscussionSession.delete(sideSessionKey);
|
|
311
|
+
this.#detachedAtBySession.delete(sessionKey);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
function getClickClackDiscussionBindingStore(runtime) {
|
|
315
|
+
const existing = storesByRuntime$1.get(runtime);
|
|
316
|
+
if (existing) return existing;
|
|
317
|
+
const created = new ClickClackDiscussionBindingStore(runtime);
|
|
318
|
+
storesByRuntime$1.set(runtime, created);
|
|
319
|
+
return created;
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region extensions/clickclack/src/discussions/eligibility.ts
|
|
323
|
+
function discussionAccounts(cfg) {
|
|
324
|
+
return listEnabledClickClackAccounts(cfg).filter((account) => account.configured && account.discussions.enabled);
|
|
325
|
+
}
|
|
326
|
+
function normalizedServerBaseUrl(account) {
|
|
327
|
+
return account.baseUrl.replace(/\/+$/u, "");
|
|
328
|
+
}
|
|
329
|
+
/** Resolves the sole live account and rejects bindings pinned to an older destination. */
|
|
330
|
+
function resolveDiscussionBindingAccount(cfg, binding) {
|
|
331
|
+
const accounts = discussionAccounts(cfg);
|
|
332
|
+
if (accounts.length !== 1) return { state: "unavailable" };
|
|
333
|
+
const account = accounts[0];
|
|
334
|
+
if (!account) return { state: "unavailable" };
|
|
335
|
+
if (account.accountId !== binding.accountId || normalizedServerBaseUrl(account) !== binding.serverBaseUrl || account.discussions.workspace !== binding.workspaceRef || binding.credentialFingerprint !== void 0 && discussionCredentialFingerprint(account.token) !== binding.credentialFingerprint) return {
|
|
336
|
+
state: "stale",
|
|
337
|
+
account
|
|
310
338
|
};
|
|
311
339
|
return {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
},
|
|
324
|
-
finalize: async () => {
|
|
325
|
-
await flushAllCommentary();
|
|
326
|
-
await chain;
|
|
327
|
-
}
|
|
340
|
+
state: "active",
|
|
341
|
+
account
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/** Public embed/open URLs for one bound discussion channel. */
|
|
345
|
+
function discussionInfoForBinding(binding, account) {
|
|
346
|
+
const baseUrl = normalizedServerBaseUrl(account);
|
|
347
|
+
return {
|
|
348
|
+
state: "open",
|
|
349
|
+
embedUrl: `${baseUrl}/embed/channel/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}?openclawHostTheme=1`,
|
|
350
|
+
openUrl: `${baseUrl}/app/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}`
|
|
328
351
|
};
|
|
329
352
|
}
|
|
330
353
|
//#endregion
|
|
331
|
-
//#region extensions/clickclack/src/discussions/
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
const storesByRuntime
|
|
335
|
-
function
|
|
336
|
-
|
|
354
|
+
//#region extensions/clickclack/src/discussions/revoked-channel-store.ts
|
|
355
|
+
const REVOKED_CHANNELS_NAMESPACE = "discussion-revoked-channels";
|
|
356
|
+
const MAX_REVOKED_CHANNELS = 1e5;
|
|
357
|
+
const storesByRuntime = /* @__PURE__ */ new WeakMap();
|
|
358
|
+
function revokedChannelKey(params) {
|
|
359
|
+
return [params.serverBaseUrl.replace(/\/+$/u, ""), params.channelId].join("\0");
|
|
360
|
+
}
|
|
361
|
+
function getStore(runtime) {
|
|
362
|
+
const existing = storesByRuntime.get(runtime);
|
|
337
363
|
if (existing) return existing;
|
|
338
364
|
const created = runtime.state.openSyncKeyedStore({
|
|
339
|
-
namespace:
|
|
340
|
-
maxEntries:
|
|
365
|
+
namespace: REVOKED_CHANNELS_NAMESPACE,
|
|
366
|
+
maxEntries: MAX_REVOKED_CHANNELS,
|
|
341
367
|
overflowPolicy: "reject-new"
|
|
342
368
|
});
|
|
343
|
-
storesByRuntime
|
|
369
|
+
storesByRuntime.set(runtime, created);
|
|
344
370
|
return created;
|
|
345
371
|
}
|
|
346
|
-
/**
|
|
347
|
-
function
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
});
|
|
356
|
-
return generation;
|
|
357
|
-
}
|
|
358
|
-
/** Clears only the completed reservation; future opens must mint a new ownership ref. */
|
|
359
|
-
function clearDiscussionBindingGeneration(params) {
|
|
360
|
-
const store = getGenerationStore(params.runtime);
|
|
361
|
-
const existing = store.lookup(params.sessionKey);
|
|
362
|
-
if (!existing) return;
|
|
363
|
-
if (params.expectedGeneration && existing.generation !== params.expectedGeneration) return;
|
|
364
|
-
store.delete(params.sessionKey);
|
|
372
|
+
/** Records managed ownership before its live binding is released. */
|
|
373
|
+
function markClickClackDiscussionChannelRevoked(runtime, binding) {
|
|
374
|
+
const value = {
|
|
375
|
+
accountId: binding.accountId,
|
|
376
|
+
serverBaseUrl: binding.serverBaseUrl,
|
|
377
|
+
channelId: binding.channelId,
|
|
378
|
+
revokedAt: Date.now()
|
|
379
|
+
};
|
|
380
|
+
getStore(runtime).register(revokedChannelKey(value), value);
|
|
365
381
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
});
|
|
382
|
+
function markClickClackDiscussionChannelIdentityRevoked(params) {
|
|
383
|
+
const value = {
|
|
384
|
+
accountId: params.accountId,
|
|
385
|
+
serverBaseUrl: params.serverBaseUrl.replace(/\/+$/u, ""),
|
|
386
|
+
channelId: params.channelId,
|
|
387
|
+
revokedAt: Date.now()
|
|
388
|
+
};
|
|
389
|
+
getStore(params.runtime).register(revokedChannelKey(value), value);
|
|
375
390
|
}
|
|
376
|
-
function
|
|
377
|
-
|
|
378
|
-
sessionKey: entry.key,
|
|
379
|
-
generation: entry.value.generation,
|
|
380
|
-
...entry.value.pending
|
|
381
|
-
}] : []);
|
|
391
|
+
function clearClickClackDiscussionChannelRevoked(params) {
|
|
392
|
+
getStore(params.runtime).delete(revokedChannelKey(params));
|
|
382
393
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return
|
|
394
|
+
/** Distinguishes a released managed channel from a genuinely ordinary channel. */
|
|
395
|
+
function isClickClackDiscussionChannelRevoked(params) {
|
|
396
|
+
return Boolean(getStore(params.runtime).lookup(revokedChannelKey(params)));
|
|
386
397
|
}
|
|
387
398
|
//#endregion
|
|
388
|
-
//#region extensions/clickclack/src/discussions/
|
|
389
|
-
function
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
399
|
+
//#region extensions/clickclack/src/discussions/routing.ts
|
|
400
|
+
function resolveClickClackDiscussionRoute(params) {
|
|
401
|
+
if (isClickClackDiscussionChannelRevoked(params)) return { state: "revoked" };
|
|
402
|
+
const store = getClickClackDiscussionBindingStore(params.runtime);
|
|
403
|
+
const matched = store.getByChannel(params.serverBaseUrl, params.channelId);
|
|
404
|
+
if (!matched) return { state: hasPendingDiscussionOpenForDestination(params) ? "revoked" : "unbound" };
|
|
405
|
+
if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
|
|
406
|
+
if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
|
|
407
|
+
if (resolveDiscussionBindingAccount(params.config, matched.binding).state !== "active") return { state: "revoked" };
|
|
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" };
|
|
424
|
+
const sessionKey = discussionSessionKey({
|
|
425
|
+
runtime: params.runtime,
|
|
426
|
+
agentId: binding.agentId,
|
|
427
|
+
mainSessionKey: matched.sessionKey,
|
|
428
|
+
sessionId: binding.sessionId,
|
|
429
|
+
accountId: params.accountId,
|
|
430
|
+
serverBaseUrl: binding.serverBaseUrl,
|
|
431
|
+
channelId: binding.channelId,
|
|
432
|
+
externalRef: binding.externalRef
|
|
433
|
+
});
|
|
434
|
+
if (!sessionKey) return { state: "revoked" };
|
|
435
|
+
return {
|
|
436
|
+
state: "active",
|
|
437
|
+
route: {
|
|
438
|
+
agentId: binding.agentId,
|
|
439
|
+
sessionKey,
|
|
440
|
+
systemPrompt: [
|
|
441
|
+
"You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
|
|
442
|
+
`The main session key is ${matched.sessionKey}.`,
|
|
443
|
+
"Observe it with sessions_history and session_status (using changesSince for incremental checks).",
|
|
444
|
+
"Use sessions_send to relay or steer the main session only when the humans in this discussion ask you to.",
|
|
445
|
+
"These session tools are host-scoped to the attached main session; do not attempt session discovery or alternate targets."
|
|
446
|
+
].join(" ")
|
|
447
|
+
}
|
|
448
|
+
};
|
|
394
449
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region extensions/clickclack/src/group-policy.ts
|
|
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
|
+
};
|
|
399
466
|
}
|
|
400
|
-
|
|
401
|
-
|
|
467
|
+
/**
|
|
468
|
+
* Resolves the effective group policy for a ClickClack channel.
|
|
469
|
+
*
|
|
470
|
+
* Lookup order:
|
|
471
|
+
* 1. Exact channel ID in `groups`
|
|
472
|
+
* 2. Wildcard `'*'` entry in `groups`
|
|
473
|
+
* 3. Account-level `requireMention` / `mentionPatterns`
|
|
474
|
+
* 4. Backward-compatible default: { requireMention: false, mentionPatterns: [] }
|
|
475
|
+
*/
|
|
476
|
+
function resolveClickClackGroupPolicy(params) {
|
|
477
|
+
const { account, channelId } = params;
|
|
478
|
+
const accountPolicy = {
|
|
479
|
+
requireMention: account.requireMention === true,
|
|
480
|
+
mentionPatterns: account.mentionPatterns ?? []
|
|
481
|
+
};
|
|
482
|
+
const channelKey = channelId?.trim();
|
|
483
|
+
const groups = channelKey ? account.groups : void 0;
|
|
484
|
+
const wildcard = groups?.["*"];
|
|
485
|
+
const exact = channelKey ? groups?.[channelKey] : void 0;
|
|
486
|
+
return {
|
|
487
|
+
requireMention: exact?.requireMention ?? wildcard?.requireMention ?? accountPolicy.requireMention,
|
|
488
|
+
mentionPatterns: exact?.mentionPatterns ?? wildcard?.mentionPatterns ?? accountPolicy.mentionPatterns
|
|
489
|
+
};
|
|
402
490
|
}
|
|
403
|
-
|
|
404
|
-
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region extensions/clickclack/src/mention-facts.ts
|
|
493
|
+
/**
|
|
494
|
+
* Detects whether a ClickClack group message contains a direct mention of the
|
|
495
|
+
* current account.
|
|
496
|
+
*
|
|
497
|
+
* Pure helper – no side effects, no runtime imports.
|
|
498
|
+
*/
|
|
499
|
+
const CLICKCLACK_MENTION_PATTERN = /(?:^|[^a-z0-9_@-])@([a-z0-9][a-z0-9_-]{1,31})(?![a-z0-9_-])/giu;
|
|
500
|
+
function buildLocalMentionRegexes(params) {
|
|
501
|
+
if (params.mentionPatterns.length === 0) return [];
|
|
502
|
+
const cfg = params.cfg;
|
|
503
|
+
return buildMentionRegexes({
|
|
504
|
+
...cfg,
|
|
505
|
+
messages: {
|
|
506
|
+
...cfg?.messages,
|
|
507
|
+
groupChat: {
|
|
508
|
+
...cfg?.messages?.groupChat,
|
|
509
|
+
mentionPatterns: params.mentionPatterns
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}, void 0, {
|
|
513
|
+
provider: "clickclack",
|
|
514
|
+
conversationId: params.channelId
|
|
515
|
+
});
|
|
405
516
|
}
|
|
406
|
-
function
|
|
407
|
-
return
|
|
517
|
+
function resolveMentionHandles(body) {
|
|
518
|
+
return [...body.matchAll(CLICKCLACK_MENTION_PATTERN)].map((match) => match[1]?.toLowerCase()).filter((handle) => Boolean(handle));
|
|
408
519
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
520
|
+
/**
|
|
521
|
+
* Builds mention facts for a ClickClack message.
|
|
522
|
+
*
|
|
523
|
+
* Rules:
|
|
524
|
+
* - DMs always have canDetectMention: false, wasMentioned: false
|
|
525
|
+
* (DMs bypass mention gating).
|
|
526
|
+
* - Group messages: canDetectMention: true when body text is available.
|
|
527
|
+
* - Checks the message body against shared and account-local mention patterns.
|
|
528
|
+
* - If botHandle is provided and the message body contains its ClickClack
|
|
529
|
+
* `@handle`, treat it as a mention.
|
|
530
|
+
* - Plain display names do not count unless explicitly configured as a pattern.
|
|
531
|
+
*/
|
|
532
|
+
function resolveClickClackMentionFacts(params) {
|
|
533
|
+
const { isDirect, body, mentionPatterns, botHandle, cfg, agentId, channelId } = params;
|
|
534
|
+
if (isDirect) return {
|
|
535
|
+
canDetectMention: false,
|
|
536
|
+
wasMentioned: false
|
|
537
|
+
};
|
|
538
|
+
if (!body) return {
|
|
539
|
+
canDetectMention: true,
|
|
540
|
+
wasMentioned: false,
|
|
541
|
+
hasAnyMention: false
|
|
542
|
+
};
|
|
543
|
+
const sharedMentionRegexes = buildMentionRegexes(cfg, agentId, {
|
|
544
|
+
provider: "clickclack",
|
|
545
|
+
conversationId: channelId
|
|
546
|
+
});
|
|
547
|
+
const localMentionRegexes = buildLocalMentionRegexes({
|
|
548
|
+
cfg,
|
|
549
|
+
mentionPatterns,
|
|
550
|
+
channelId
|
|
551
|
+
});
|
|
552
|
+
const mentionRegexes = [...sharedMentionRegexes, ...localMentionRegexes];
|
|
553
|
+
const bodyForRegex = normalizeMentionText(body);
|
|
554
|
+
const hasConfiguredMention = mentionRegexes.some((regex) => regex.test(bodyForRegex));
|
|
555
|
+
const mentionHandles = resolveMentionHandles(body);
|
|
556
|
+
const normalizedBotHandle = botHandle?.replace(/^@/u, "").trim().toLowerCase();
|
|
557
|
+
return {
|
|
558
|
+
canDetectMention: true,
|
|
559
|
+
wasMentioned: (normalizedBotHandle ? mentionHandles.includes(normalizedBotHandle) : false) || hasConfiguredMention,
|
|
560
|
+
hasAnyMention: mentionHandles.length > 0 || hasConfiguredMention
|
|
561
|
+
};
|
|
417
562
|
}
|
|
418
|
-
|
|
419
|
-
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region extensions/clickclack/src/runtime.ts
|
|
565
|
+
/**
|
|
566
|
+
* Runtime store for host-provided OpenClaw services used by the ClickClack
|
|
567
|
+
* bundled plugin.
|
|
568
|
+
*/
|
|
569
|
+
const { setRuntime: setClickClackRuntime, getRuntime: getClickClackRuntime } = createPluginRuntimeStore({
|
|
570
|
+
pluginId: "clickclack",
|
|
571
|
+
errorMessage: "ClickClack runtime not initialized"
|
|
572
|
+
});
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region extensions/clickclack/src/target.ts
|
|
575
|
+
/**
|
|
576
|
+
* Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
|
|
577
|
+
*/
|
|
578
|
+
function parseClickClackTarget(raw) {
|
|
579
|
+
const value = raw.trim();
|
|
580
|
+
if (!value) throw new Error("ClickClack target is required");
|
|
581
|
+
const [prefix, ...rest] = value.split(":");
|
|
582
|
+
const body = rest.join(":").trim();
|
|
583
|
+
if (prefix === "channel" && body) return {
|
|
584
|
+
chatType: "group",
|
|
585
|
+
kind: "channel",
|
|
586
|
+
id: body
|
|
587
|
+
};
|
|
588
|
+
if (prefix === "thread" && body) return {
|
|
589
|
+
chatType: "group",
|
|
590
|
+
kind: "thread",
|
|
591
|
+
id: body
|
|
592
|
+
};
|
|
593
|
+
if (prefix === "dm" && body) return {
|
|
594
|
+
chatType: "direct",
|
|
595
|
+
kind: "dm",
|
|
596
|
+
id: body
|
|
597
|
+
};
|
|
598
|
+
if (!body) return {
|
|
599
|
+
chatType: "group",
|
|
600
|
+
kind: "channel",
|
|
601
|
+
id: value
|
|
602
|
+
};
|
|
603
|
+
throw new Error(`Unsupported ClickClack target: ${raw}`);
|
|
420
604
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
sessionId,
|
|
425
|
-
destinationIdentity,
|
|
426
|
-
bindingGeneration
|
|
427
|
-
].join("\0"))}`;
|
|
605
|
+
/** Formats a parsed ClickClack target back into canonical target syntax. */
|
|
606
|
+
function buildClickClackTarget(target) {
|
|
607
|
+
return `${target.kind}:${target.id}`;
|
|
428
608
|
}
|
|
429
|
-
|
|
430
|
-
|
|
609
|
+
/** Normalizes user-entered ClickClack target text for channel routing. */
|
|
610
|
+
function normalizeClickClackTarget(raw) {
|
|
611
|
+
return buildClickClackTarget(parseClickClackTarget(raw));
|
|
431
612
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
channel: "clickclack",
|
|
436
|
-
accountId: params.accountId,
|
|
437
|
-
peer: {
|
|
438
|
-
kind: "channel",
|
|
439
|
-
id: discussionSessionPeerId(params)
|
|
440
|
-
}
|
|
441
|
-
});
|
|
613
|
+
/** Reports whether a target string can be offered to the ClickClack parser. */
|
|
614
|
+
function looksLikeClickClackTarget(raw) {
|
|
615
|
+
return /^(channel|thread|dm):/i.test(raw.trim()) || raw.trim().length > 0;
|
|
442
616
|
}
|
|
443
617
|
//#endregion
|
|
444
|
-
//#region extensions/clickclack/src/
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
readConsistency: "latest"
|
|
456
|
-
});
|
|
457
|
-
return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId && entry.archivedAt === void 0);
|
|
458
|
-
}
|
|
459
|
-
const DISCUSSION_BINDINGS_NAMESPACE = "discussion-bindings";
|
|
460
|
-
const MAX_DISCUSSION_BINDINGS = 1e4;
|
|
461
|
-
const storesByRuntime$1 = /* @__PURE__ */ new WeakMap();
|
|
462
|
-
function channelKey(serverBaseUrl, channelId) {
|
|
463
|
-
return `${serverBaseUrl.replace(/\/+$/u, "")}\0${channelId}`;
|
|
618
|
+
//#region extensions/clickclack/src/access.ts
|
|
619
|
+
/**
|
|
620
|
+
* Maps ClickClack senders and conversations onto the shared channel ingress
|
|
621
|
+
* allowlist/command authorization contract.
|
|
622
|
+
*/
|
|
623
|
+
const CHANNEL_ID$2 = "clickclack";
|
|
624
|
+
function normalizeClickClackUserId(value) {
|
|
625
|
+
const trimmed = value.trim();
|
|
626
|
+
if (!trimmed) return null;
|
|
627
|
+
const withoutProvider = trimmed.replace(/^(clickclack|cc):/i, "").trim();
|
|
628
|
+
return withoutProvider.match(/^dm:(.+)$/i)?.[1]?.trim() || withoutProvider || null;
|
|
464
629
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
constructor(runtime) {
|
|
472
|
-
this.#runtime = runtime;
|
|
473
|
-
this.#store = runtime.state.openSyncKeyedStore({
|
|
474
|
-
namespace: DISCUSSION_BINDINGS_NAMESPACE,
|
|
475
|
-
maxEntries: MAX_DISCUSSION_BINDINGS,
|
|
476
|
-
overflowPolicy: "reject-new"
|
|
477
|
-
});
|
|
478
|
-
for (const entry of this.#store.entries()) this.#index(entry.key, entry.value);
|
|
479
|
-
}
|
|
480
|
-
get(sessionKey) {
|
|
481
|
-
return this.#store.lookup(sessionKey);
|
|
482
|
-
}
|
|
483
|
-
hasCapacity(sessionKey) {
|
|
484
|
-
return this.get(sessionKey) !== void 0 || this.#store.entries().length < MAX_DISCUSSION_BINDINGS;
|
|
485
|
-
}
|
|
486
|
-
getByChannel(serverBaseUrl, channelId) {
|
|
487
|
-
const key = channelKey(serverBaseUrl, channelId);
|
|
488
|
-
const sessionKey = this.#sessionByChannel.get(key);
|
|
489
|
-
if (!sessionKey) return;
|
|
490
|
-
const binding = this.get(sessionKey);
|
|
491
|
-
if (!binding) {
|
|
492
|
-
this.#sessionByChannel.delete(key);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
return {
|
|
496
|
-
sessionKey,
|
|
497
|
-
binding
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
set(sessionKey, binding) {
|
|
501
|
-
const previous = this.get(sessionKey);
|
|
502
|
-
this.#store.register(sessionKey, binding);
|
|
503
|
-
if (previous) this.#unindex(sessionKey, previous);
|
|
504
|
-
this.#index(sessionKey, binding);
|
|
505
|
-
}
|
|
506
|
-
delete(sessionKey) {
|
|
507
|
-
const previous = this.get(sessionKey);
|
|
508
|
-
const deleted = this.#store.delete(sessionKey);
|
|
509
|
-
if (deleted && previous) this.#unindex(sessionKey, previous);
|
|
510
|
-
return deleted;
|
|
511
|
-
}
|
|
512
|
-
getByDiscussionSession(sideSessionKey) {
|
|
513
|
-
const sessionKey = this.#mainByDiscussionSession.get(sideSessionKey);
|
|
514
|
-
if (!sessionKey) return;
|
|
515
|
-
const binding = this.get(sessionKey);
|
|
516
|
-
return binding ? {
|
|
517
|
-
sessionKey,
|
|
518
|
-
binding
|
|
519
|
-
} : void 0;
|
|
520
|
-
}
|
|
521
|
-
entries() {
|
|
522
|
-
return this.#store.entries().map((entry) => ({
|
|
523
|
-
sessionKey: entry.key,
|
|
524
|
-
binding: entry.value
|
|
525
|
-
}));
|
|
526
|
-
}
|
|
527
|
-
#index(sessionKey, binding) {
|
|
528
|
-
this.#sessionByChannel.set(channelKey(binding.serverBaseUrl, binding.channelId), sessionKey);
|
|
529
|
-
const sideSessionKey = discussionSessionKey({
|
|
530
|
-
runtime: this.#runtime,
|
|
531
|
-
agentId: binding.agentId,
|
|
532
|
-
mainSessionKey: sessionKey,
|
|
533
|
-
sessionId: binding.sessionId,
|
|
534
|
-
accountId: binding.accountId,
|
|
535
|
-
serverBaseUrl: binding.serverBaseUrl,
|
|
536
|
-
channelId: binding.channelId,
|
|
537
|
-
externalRef: binding.externalRef
|
|
538
|
-
});
|
|
539
|
-
if (sideSessionKey) this.#mainByDiscussionSession.set(sideSessionKey, sessionKey);
|
|
540
|
-
}
|
|
541
|
-
#unindex(sessionKey, binding) {
|
|
542
|
-
this.#sessionByChannel.delete(channelKey(binding.serverBaseUrl, binding.channelId));
|
|
543
|
-
const sideSessionKey = discussionSessionKey({
|
|
544
|
-
runtime: this.#runtime,
|
|
545
|
-
agentId: binding.agentId,
|
|
546
|
-
mainSessionKey: sessionKey,
|
|
547
|
-
sessionId: binding.sessionId,
|
|
548
|
-
accountId: binding.accountId,
|
|
549
|
-
serverBaseUrl: binding.serverBaseUrl,
|
|
550
|
-
channelId: binding.channelId,
|
|
551
|
-
externalRef: binding.externalRef
|
|
552
|
-
});
|
|
553
|
-
if (sideSessionKey) this.#mainByDiscussionSession.delete(sideSessionKey);
|
|
554
|
-
}
|
|
630
|
+
const clickClackIngressIdentity = {
|
|
631
|
+
key: "user-id",
|
|
632
|
+
normalizeEntry: normalizeClickClackUserId,
|
|
633
|
+
normalizeSubject: normalizeClickClackUserId,
|
|
634
|
+
isWildcardEntry: (entry) => normalizeClickClackUserId(entry) === "*",
|
|
635
|
+
entryIdPrefix: "clickclack-user"
|
|
555
636
|
};
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const created = new ClickClackDiscussionBindingStore(runtime);
|
|
560
|
-
storesByRuntime$1.set(runtime, created);
|
|
561
|
-
return created;
|
|
562
|
-
}
|
|
563
|
-
//#endregion
|
|
564
|
-
//#region extensions/clickclack/src/discussions/eligibility.ts
|
|
565
|
-
function discussionAccounts(cfg) {
|
|
566
|
-
return listEnabledClickClackAccounts(cfg).filter((account) => account.configured && account.discussions.enabled);
|
|
567
|
-
}
|
|
568
|
-
function normalizedServerBaseUrl(account) {
|
|
569
|
-
return account.baseUrl.replace(/\/+$/u, "");
|
|
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;
|
|
570
640
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
641
|
+
function resolveAccountAgentRoute(params) {
|
|
642
|
+
const runtime = getClickClackRuntime();
|
|
643
|
+
const peer = {
|
|
644
|
+
kind: params.isDirect ? "direct" : "channel",
|
|
645
|
+
id: params.target
|
|
646
|
+
};
|
|
647
|
+
const route = runtime.channel.routing.resolveAgentRoute({
|
|
648
|
+
cfg: params.cfg,
|
|
649
|
+
channel: CHANNEL_ID$2,
|
|
650
|
+
accountId: params.account.accountId,
|
|
651
|
+
peer
|
|
652
|
+
});
|
|
653
|
+
const agentId = normalizeAgentId(params.account.agentId ?? route.agentId);
|
|
654
|
+
if (agentId === route.agentId) return route;
|
|
655
|
+
const dmScope = params.cfg.session?.dmScope ?? "main";
|
|
656
|
+
const sessionKey = runtime.channel.routing.buildAgentSessionKey({
|
|
657
|
+
agentId,
|
|
658
|
+
mainKey: params.cfg.session?.mainKey,
|
|
659
|
+
channel: CHANNEL_ID$2,
|
|
660
|
+
accountId: params.account.accountId,
|
|
661
|
+
peer,
|
|
662
|
+
dmScope,
|
|
663
|
+
identityLinks: params.cfg.session?.identityLinks
|
|
664
|
+
});
|
|
665
|
+
const mainSessionKey = runtime.channel.routing.buildAgentSessionKey({
|
|
666
|
+
agentId,
|
|
667
|
+
mainKey: params.cfg.session?.mainKey,
|
|
668
|
+
channel: CHANNEL_ID$2,
|
|
669
|
+
accountId: params.account.accountId,
|
|
670
|
+
dmScope: "main"
|
|
671
|
+
});
|
|
672
|
+
return {
|
|
673
|
+
...route,
|
|
674
|
+
agentId,
|
|
675
|
+
dmScope,
|
|
676
|
+
sessionKey,
|
|
677
|
+
mainSessionKey,
|
|
678
|
+
lastRoutePolicy: sessionKey === mainSessionKey ? "main" : "session"
|
|
580
679
|
};
|
|
680
|
+
}
|
|
681
|
+
function resolvePreparedInboundRoute(params) {
|
|
682
|
+
const runtime = getClickClackRuntime();
|
|
683
|
+
const isDirect = Boolean(params.message.direct_conversation_id);
|
|
684
|
+
const target = buildClickClackTarget(isDirect ? {
|
|
685
|
+
chatType: "direct",
|
|
686
|
+
kind: "dm",
|
|
687
|
+
id: params.message.author_id
|
|
688
|
+
} : {
|
|
689
|
+
chatType: "group",
|
|
690
|
+
kind: "channel",
|
|
691
|
+
id: params.message.channel_id ?? ""
|
|
692
|
+
});
|
|
693
|
+
const accountRoute = resolveAccountAgentRoute({
|
|
694
|
+
cfg: params.config,
|
|
695
|
+
account: params.account,
|
|
696
|
+
target,
|
|
697
|
+
isDirect
|
|
698
|
+
});
|
|
699
|
+
const discussionResolution = !isDirect && params.message.channel_id ? resolveClickClackDiscussionRoute({
|
|
700
|
+
runtime,
|
|
701
|
+
config: params.config,
|
|
702
|
+
accountId: params.account.accountId,
|
|
703
|
+
serverBaseUrl: params.account.baseUrl,
|
|
704
|
+
workspaceId: params.message.workspace_id,
|
|
705
|
+
channelId: params.message.channel_id
|
|
706
|
+
}) : { state: "unbound" };
|
|
707
|
+
const discussionRoute = discussionResolution.state === "active" ? discussionResolution.route : void 0;
|
|
581
708
|
return {
|
|
582
|
-
|
|
583
|
-
|
|
709
|
+
isDirect,
|
|
710
|
+
target,
|
|
711
|
+
route: discussionRoute ? {
|
|
712
|
+
...accountRoute,
|
|
713
|
+
agentId: discussionRoute.agentId,
|
|
714
|
+
sessionKey: discussionRoute.sessionKey,
|
|
715
|
+
lastRoutePolicy: "session"
|
|
716
|
+
} : accountRoute,
|
|
717
|
+
discussionRoute,
|
|
718
|
+
revoked: discussionResolution.state === "revoked"
|
|
584
719
|
};
|
|
585
720
|
}
|
|
586
|
-
/**
|
|
587
|
-
|
|
588
|
-
|
|
721
|
+
/**
|
|
722
|
+
* Resolves whether a ClickClack message should enter the agent pipeline and
|
|
723
|
+
* whether its command-style body may run tools.
|
|
724
|
+
*/
|
|
725
|
+
async function resolveClickClackInboundAccess(params) {
|
|
726
|
+
const runtime = getClickClackRuntime();
|
|
727
|
+
const cfg = params.config;
|
|
728
|
+
const preparedRoute = resolvePreparedInboundRoute(params);
|
|
729
|
+
const shouldCheckCommand = runtime.channel.commands.shouldComputeCommandAuthorized(params.message.body, cfg);
|
|
730
|
+
const effectiveGroupPolicy = resolveClickClackGroupPolicy({
|
|
731
|
+
account: params.account,
|
|
732
|
+
channelId: params.message.channel_id
|
|
733
|
+
});
|
|
734
|
+
const mentionFacts = resolveClickClackMentionFacts({
|
|
735
|
+
isDirect: preparedRoute.isDirect,
|
|
736
|
+
body: params.message.body,
|
|
737
|
+
mentionPatterns: effectiveGroupPolicy.mentionPatterns,
|
|
738
|
+
botHandle: params.account.botHandle,
|
|
739
|
+
cfg,
|
|
740
|
+
agentId: preparedRoute.route.agentId,
|
|
741
|
+
channelId: params.message.channel_id
|
|
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;
|
|
778
|
+
const allowTextCommands = params.account.replyMode === "agent" && runtime.channel.commands.shouldHandleTextCommands({
|
|
779
|
+
cfg,
|
|
780
|
+
surface: CHANNEL_ID$2,
|
|
781
|
+
commandSource: "text"
|
|
782
|
+
});
|
|
783
|
+
const resolved = await resolveStableChannelMessageIngress({
|
|
784
|
+
channelId: CHANNEL_ID$2,
|
|
785
|
+
accountId: params.account.accountId,
|
|
786
|
+
identity: clickClackIngressIdentity,
|
|
787
|
+
cfg,
|
|
788
|
+
subject: { stableId: params.message.author_id },
|
|
789
|
+
conversation: {
|
|
790
|
+
kind: preparedRoute.isDirect ? "direct" : "group",
|
|
791
|
+
id: preparedRoute.isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id
|
|
792
|
+
},
|
|
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,
|
|
800
|
+
dmPolicy: "allowlist",
|
|
801
|
+
groupPolicy: "allowlist",
|
|
802
|
+
mentionFacts,
|
|
803
|
+
policy: { activation: {
|
|
804
|
+
requireMention: effectiveGroupPolicy.requireMention,
|
|
805
|
+
allowTextCommands
|
|
806
|
+
} },
|
|
807
|
+
command: shouldCheckCommand ? {
|
|
808
|
+
cfg,
|
|
809
|
+
modeWhenAccessGroupsOff: "configured"
|
|
810
|
+
} : false
|
|
811
|
+
});
|
|
589
812
|
return {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
813
|
+
shouldDispatch: !preparedRoute.revoked && resolved.ingress.admission === "dispatch",
|
|
814
|
+
commandAuthorized: resolved.commandAccess.requested ? resolved.commandAccess.authorized : resolved.senderAccess.allowed,
|
|
815
|
+
requireMention: effectiveGroupPolicy.requireMention,
|
|
816
|
+
mentionFacts,
|
|
817
|
+
botLoopProtection,
|
|
818
|
+
preparedRoute,
|
|
819
|
+
channelIngress: resolved
|
|
593
820
|
};
|
|
594
821
|
}
|
|
595
822
|
//#endregion
|
|
596
|
-
//#region extensions/clickclack/src/
|
|
597
|
-
const
|
|
598
|
-
const
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
function getStore(runtime) {
|
|
604
|
-
const existing = storesByRuntime.get(runtime);
|
|
605
|
-
if (existing) return existing;
|
|
606
|
-
const created = runtime.state.openSyncKeyedStore({
|
|
607
|
-
namespace: REVOKED_CHANNELS_NAMESPACE,
|
|
608
|
-
maxEntries: MAX_REVOKED_CHANNELS,
|
|
609
|
-
overflowPolicy: "reject-new"
|
|
610
|
-
});
|
|
611
|
-
storesByRuntime.set(runtime, created);
|
|
612
|
-
return created;
|
|
823
|
+
//#region extensions/clickclack/src/command-menu.ts
|
|
824
|
+
const CLICKCLACK_COMMAND_PATTERN = /^[a-z0-9_-]{1,32}$/u;
|
|
825
|
+
const CLICKCLACK_MAX_COMMANDS = 100;
|
|
826
|
+
const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
|
|
827
|
+
const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
|
|
828
|
+
function truncateCodePoints(value, maxLength) {
|
|
829
|
+
return Array.from(value).slice(0, maxLength).join("");
|
|
613
830
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
accountId: binding.accountId,
|
|
618
|
-
serverBaseUrl: binding.serverBaseUrl,
|
|
619
|
-
channelId: binding.channelId,
|
|
620
|
-
revokedAt: Date.now()
|
|
621
|
-
};
|
|
622
|
-
getStore(runtime).register(revokedChannelKey(value), value);
|
|
831
|
+
function commandArgsHint(spec) {
|
|
832
|
+
if (spec.args?.length) return truncateCodePoints(spec.args.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" "), CLICKCLACK_MAX_ARGS_HINT_LENGTH);
|
|
833
|
+
return spec.acceptsArgs ? "[args]" : "";
|
|
623
834
|
}
|
|
624
|
-
function
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
channelId: params.channelId,
|
|
629
|
-
revokedAt: Date.now()
|
|
630
|
-
};
|
|
631
|
-
getStore(params.runtime).register(revokedChannelKey(value), value);
|
|
835
|
+
function errorStatus(error) {
|
|
836
|
+
if (!error || typeof error !== "object" || !("status" in error)) return;
|
|
837
|
+
const status = error.status;
|
|
838
|
+
return typeof status === "number" ? status : void 0;
|
|
632
839
|
}
|
|
633
|
-
function
|
|
634
|
-
|
|
840
|
+
function mapNativeCommandSpecsToClickClackMenu(specs, log) {
|
|
841
|
+
const commands = [];
|
|
842
|
+
const seen = /* @__PURE__ */ new Set();
|
|
843
|
+
const invalidNames = /* @__PURE__ */ new Set();
|
|
844
|
+
for (const spec of specs) {
|
|
845
|
+
if (spec.isAlias) continue;
|
|
846
|
+
const command = spec.name.trim().toLowerCase();
|
|
847
|
+
if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
|
|
848
|
+
invalidNames.add(spec.name.trim() || "<empty>");
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (seen.has(command)) continue;
|
|
852
|
+
seen.add(command);
|
|
853
|
+
const description = spec.description.trim() || command;
|
|
854
|
+
commands.push({
|
|
855
|
+
command,
|
|
856
|
+
description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
|
|
857
|
+
args_hint: commandArgsHint(spec)
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
|
|
861
|
+
return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
|
|
635
862
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
863
|
+
async function syncClickClackCommandMenu(params) {
|
|
864
|
+
try {
|
|
865
|
+
const commands = mapNativeCommandSpecsToClickClackMenu(listNativeCommandSpecsForConfig(params.cfg, { provider: "clickclack" }), params.log);
|
|
866
|
+
await params.client.setBotCommands(commands);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
const status = errorStatus(error);
|
|
869
|
+
if (status === 403) {
|
|
870
|
+
params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (status === 404) {
|
|
874
|
+
params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
|
|
878
|
+
}
|
|
639
879
|
}
|
|
640
880
|
//#endregion
|
|
641
|
-
//#region extensions/clickclack/src/
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
881
|
+
//#region extensions/clickclack/src/activity.ts
|
|
882
|
+
/**
|
|
883
|
+
* Publishes agent activity (streamed commentary + tool progress) into
|
|
884
|
+
* ClickClack as durable `agent_commentary` / `agent_tool` message rows,
|
|
885
|
+
* coalesced so one logical step becomes one row instead of a row per frame.
|
|
886
|
+
*
|
|
887
|
+
* Ported from the clickglass agent-bridge sidecar, adapted from gateway
|
|
888
|
+
* websocket frames to the in-process `replyOptions.onItemEvent` seam:
|
|
889
|
+
*
|
|
890
|
+
* - Commentary/reasoning arrives as cumulative text snapshots per item id.
|
|
891
|
+
* Each segment becomes one durable row: POSTed when the segment starts
|
|
892
|
+
* streaming and PATCHed (debounced) as the snapshot grows, so prose
|
|
893
|
+
* interleaves chronologically with tool rows.
|
|
894
|
+
* - Tool/step items can emit several frames (start/update/complete) for the
|
|
895
|
+
* same call, sometimes with lane-prefixed ids (`tool:X`, `command:X`).
|
|
896
|
+
* Normalize the prefix away to one key per call, POST one row on the first
|
|
897
|
+
* frame, and PATCH it when a later frame carries a strictly longer body.
|
|
898
|
+
*/
|
|
899
|
+
/** Debounce window for PATCHing streaming commentary snapshots. */
|
|
900
|
+
const CLICKCLACK_COMMENTARY_FLUSH_MS = 700;
|
|
901
|
+
/** Item kinds rendered as agent_tool rows; everything else is commentary. */
|
|
902
|
+
const TOOL_ITEM_KINDS = /* @__PURE__ */ new Set([
|
|
903
|
+
"tool",
|
|
904
|
+
"command",
|
|
905
|
+
"command_output",
|
|
906
|
+
"patch",
|
|
907
|
+
"search",
|
|
908
|
+
"api"
|
|
909
|
+
]);
|
|
910
|
+
/** Item kinds that never become durable rows (ephemeral or plumbing lanes). */
|
|
911
|
+
const SKIPPED_ITEM_KINDS = /* @__PURE__ */ new Set(["lifecycle"]);
|
|
912
|
+
/** Item kinds emitted as cumulative commentary snapshots. */
|
|
913
|
+
const STREAMING_COMMENTARY_ITEM_KINDS = /* @__PURE__ */ new Set([
|
|
914
|
+
"preamble",
|
|
915
|
+
"commentary",
|
|
916
|
+
"analysis",
|
|
917
|
+
"thinking",
|
|
918
|
+
"reasoning"
|
|
919
|
+
]);
|
|
920
|
+
/** Provider-specific reasoning lanes normalized into one ClickClack label. */
|
|
921
|
+
const THINKING_ITEM_KINDS = /* @__PURE__ */ new Set([
|
|
922
|
+
"analysis",
|
|
923
|
+
"thinking",
|
|
924
|
+
"reasoning"
|
|
925
|
+
]);
|
|
926
|
+
function normalizedItemKind(payload) {
|
|
927
|
+
return payload.kind?.trim().toLowerCase() ?? "";
|
|
928
|
+
}
|
|
929
|
+
function commentaryBody(payload) {
|
|
930
|
+
const text = payload.progressText?.trim() || payload.summary?.trim() || payload.meta?.trim() || "";
|
|
931
|
+
if (!text) return "";
|
|
932
|
+
const kind = normalizedItemKind(payload);
|
|
933
|
+
if (THINKING_ITEM_KINDS.has(kind)) return `**Thinking**\n\n${text}`;
|
|
934
|
+
return text;
|
|
935
|
+
}
|
|
936
|
+
function activityBody(payload) {
|
|
937
|
+
const line = buildChannelProgressDraftLine({
|
|
938
|
+
event: "item",
|
|
939
|
+
itemId: payload.itemId,
|
|
940
|
+
toolCallId: payload.toolCallId,
|
|
941
|
+
itemKind: payload.kind,
|
|
942
|
+
title: payload.title,
|
|
943
|
+
name: payload.name,
|
|
944
|
+
phase: payload.phase,
|
|
945
|
+
status: payload.status,
|
|
946
|
+
summary: payload.summary,
|
|
947
|
+
progressText: payload.progressText,
|
|
948
|
+
meta: payload.meta,
|
|
949
|
+
commandBearing: payload.commandBearing
|
|
950
|
+
})?.text?.trim();
|
|
951
|
+
if (line) return line;
|
|
952
|
+
const head = payload.name?.trim() || payload.title?.trim();
|
|
953
|
+
const text = payload.progressText?.trim() || payload.summary?.trim();
|
|
954
|
+
if (head && text) return `**${head}**\n\n${text}`;
|
|
955
|
+
if (text) return text;
|
|
956
|
+
if (head) return head;
|
|
957
|
+
return payload.status?.trim() || payload.kind?.trim() || "";
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Creates a per-turn activity publisher. Publishing is best-effort: transport
|
|
961
|
+
* failures are reported through `onError` and never interrupt the reply turn.
|
|
962
|
+
*/
|
|
963
|
+
function createClickClackActivityPublisher(params) {
|
|
964
|
+
const flushMs = params.flushMs ?? CLICKCLACK_COMMENTARY_FLUSH_MS;
|
|
965
|
+
const commentaryByItem = /* @__PURE__ */ new Map();
|
|
966
|
+
const toolRows = /* @__PURE__ */ new Map();
|
|
967
|
+
let provenance;
|
|
968
|
+
let chain = Promise.resolve();
|
|
969
|
+
const enqueue = (work) => {
|
|
970
|
+
chain = chain.then(work).catch((error) => {
|
|
971
|
+
params.onError?.(error);
|
|
972
|
+
});
|
|
973
|
+
return chain;
|
|
974
|
+
};
|
|
975
|
+
const postRow = (kind, body) => params.client.createActivityMessage({
|
|
976
|
+
channelId: params.target.channelId,
|
|
977
|
+
conversationId: params.target.conversationId,
|
|
978
|
+
body,
|
|
979
|
+
kind,
|
|
980
|
+
turnId: params.turnId,
|
|
981
|
+
provenance
|
|
660
982
|
});
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
systemPrompt: [
|
|
668
|
-
"You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
|
|
669
|
-
`The main session key is ${matched.sessionKey}.`,
|
|
670
|
-
"Observe it with sessions_history and session_status (using changesSince for incremental checks).",
|
|
671
|
-
"Use sessions_send to relay or steer the main session only when the humans in this discussion ask you to.",
|
|
672
|
-
"These session tools are host-scoped to the attached main session; do not attempt session discovery or alternate targets."
|
|
673
|
-
].join(" ")
|
|
983
|
+
const flushCommentary = (segmentKey) => {
|
|
984
|
+
const segment = commentaryByItem.get(segmentKey);
|
|
985
|
+
if (!segment) return Promise.resolve();
|
|
986
|
+
if (segment.timer) {
|
|
987
|
+
clearTimeout(segment.timer);
|
|
988
|
+
segment.timer = void 0;
|
|
674
989
|
}
|
|
990
|
+
if (!segment.dirty || !segment.body.trim()) return Promise.resolve();
|
|
991
|
+
segment.dirty = false;
|
|
992
|
+
const body = segment.body;
|
|
993
|
+
return enqueue(async () => {
|
|
994
|
+
if (segment.messageId) {
|
|
995
|
+
await params.client.updateMessageBody(segment.messageId, body);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const posted = await postRow("agent_commentary", body);
|
|
999
|
+
segment.messageId = posted.id;
|
|
1000
|
+
});
|
|
1001
|
+
};
|
|
1002
|
+
const flushAllCommentary = () => {
|
|
1003
|
+
const flushes = [...commentaryByItem.keys()].map((key) => flushCommentary(key));
|
|
1004
|
+
return Promise.all(flushes).then(() => void 0);
|
|
675
1005
|
};
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1006
|
+
const handleCommentary = (payload) => {
|
|
1007
|
+
const body = commentaryBody(payload);
|
|
1008
|
+
if (!body.trim()) return;
|
|
1009
|
+
const key = payload.itemId?.trim() || "turn";
|
|
1010
|
+
let segment = commentaryByItem.get(key);
|
|
1011
|
+
if (!segment) {
|
|
1012
|
+
segment = {
|
|
1013
|
+
body: "",
|
|
1014
|
+
dirty: false
|
|
1015
|
+
};
|
|
1016
|
+
commentaryByItem.set(key, segment);
|
|
1017
|
+
}
|
|
1018
|
+
if (body.length < segment.body.length || body === segment.body) return;
|
|
1019
|
+
segment.body = body;
|
|
1020
|
+
segment.dirty = true;
|
|
1021
|
+
if (!segment.timer) segment.timer = setTimeout(() => {
|
|
1022
|
+
segment.timer = void 0;
|
|
1023
|
+
flushCommentary(key);
|
|
1024
|
+
}, flushMs);
|
|
691
1025
|
};
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1026
|
+
const toolRowKey = (payload) => {
|
|
1027
|
+
const toolCallId = payload.toolCallId?.trim();
|
|
1028
|
+
if (toolCallId) return toolCallId;
|
|
1029
|
+
return (payload.itemId?.trim() ?? "").replace(/^(tool|command):/, "");
|
|
696
1030
|
};
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1031
|
+
const handleDiscreteItem = (payload) => {
|
|
1032
|
+
const body = activityBody(payload);
|
|
1033
|
+
if (!body) return;
|
|
1034
|
+
const kind = TOOL_ITEM_KINDS.has(payload.kind?.trim().toLowerCase() ?? "") ? "agent_tool" : "agent_commentary";
|
|
1035
|
+
flushAllCommentary();
|
|
1036
|
+
const key = `${kind}:${toolRowKey(payload)}`;
|
|
1037
|
+
const existing = toolRows.get(key);
|
|
1038
|
+
if (!existing || !toolRowKey(payload)) {
|
|
1039
|
+
const row = { body };
|
|
1040
|
+
if (toolRowKey(payload)) toolRows.set(key, row);
|
|
1041
|
+
enqueue(async () => {
|
|
1042
|
+
const posted = await postRow(kind, row.body);
|
|
1043
|
+
row.messageId = posted.id;
|
|
1044
|
+
row.sentBody = row.body;
|
|
1045
|
+
});
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (body.length <= existing.body.length) return;
|
|
1049
|
+
existing.body = body;
|
|
1050
|
+
enqueue(async () => {
|
|
1051
|
+
if (existing.messageId && existing.body !== existing.sentBody) {
|
|
1052
|
+
await params.client.updateMessageBody(existing.messageId, existing.body);
|
|
1053
|
+
existing.sentBody = existing.body;
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
701
1056
|
};
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1057
|
+
return {
|
|
1058
|
+
onItemEvent: (payload) => {
|
|
1059
|
+
const kind = normalizedItemKind(payload);
|
|
1060
|
+
if (STREAMING_COMMENTARY_ITEM_KINDS.has(kind)) {
|
|
1061
|
+
handleCommentary(payload);
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
if (SKIPPED_ITEM_KINDS.has(kind)) return false;
|
|
1065
|
+
handleDiscreteItem(payload);
|
|
1066
|
+
return false;
|
|
1067
|
+
},
|
|
1068
|
+
setProvenance: (next) => {
|
|
1069
|
+
provenance = next;
|
|
1070
|
+
},
|
|
1071
|
+
finalize: async () => {
|
|
1072
|
+
await flushAllCommentary();
|
|
1073
|
+
await chain;
|
|
1074
|
+
}
|
|
706
1075
|
};
|
|
707
|
-
throw new Error(`Unsupported ClickClack target: ${raw}`);
|
|
708
|
-
}
|
|
709
|
-
/** Formats a parsed ClickClack target back into canonical target syntax. */
|
|
710
|
-
function buildClickClackTarget(target) {
|
|
711
|
-
return `${target.kind}:${target.id}`;
|
|
712
|
-
}
|
|
713
|
-
/** Normalizes user-entered ClickClack target text for channel routing. */
|
|
714
|
-
function normalizeClickClackTarget(raw) {
|
|
715
|
-
return buildClickClackTarget(parseClickClackTarget(raw));
|
|
716
|
-
}
|
|
717
|
-
/** Reports whether a target string can be offered to the ClickClack parser. */
|
|
718
|
-
function looksLikeClickClackTarget(raw) {
|
|
719
|
-
return /^(channel|thread|dm):/i.test(raw.trim()) || raw.trim().length > 0;
|
|
720
1076
|
}
|
|
721
1077
|
//#endregion
|
|
722
1078
|
//#region extensions/clickclack/src/outbound.ts
|
|
@@ -876,9 +1232,9 @@ async function sendClickClackMedia(params) {
|
|
|
876
1232
|
mediaLocalRoots: params.mediaLocalRoots,
|
|
877
1233
|
mediaReadFile: params.mediaReadFile
|
|
878
1234
|
});
|
|
879
|
-
const filename = media.fileName?.trim() || "attachment";
|
|
880
|
-
mediaFilename = filename;
|
|
881
1235
|
const contentType = media.contentType?.trim() || "application/octet-stream";
|
|
1236
|
+
const filename = media.fileName?.trim() || `attachment${extensionForMime(contentType) ?? ""}`;
|
|
1237
|
+
mediaFilename = filename;
|
|
882
1238
|
await dispatch();
|
|
883
1239
|
upload = await client.createUpload({
|
|
884
1240
|
workspaceId,
|
|
@@ -1011,6 +1367,219 @@ async function reconcileClickClackUnknownSend(ctx) {
|
|
|
1011
1367
|
};
|
|
1012
1368
|
}
|
|
1013
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
|
|
1014
1583
|
//#region extensions/clickclack/src/inbound.ts
|
|
1015
1584
|
const CHANNEL_ID$1 = "clickclack";
|
|
1016
1585
|
const CLICKCLACK_MESSAGE_ID_PATTERN = /^msg_[0-9a-hjkmnp-tv-z]{26}$/u;
|
|
@@ -1020,49 +1589,6 @@ function hasClickClackReplyMedia(payload) {
|
|
|
1020
1589
|
function resolveClickClackAgentRunId(messageId) {
|
|
1021
1590
|
return CLICKCLACK_MESSAGE_ID_PATTERN.test(messageId) ? `${CHANNEL_ID$1}:${messageId}` : void 0;
|
|
1022
1591
|
}
|
|
1023
|
-
function resolveAccountAgentRoute(params) {
|
|
1024
|
-
const runtime = getClickClackRuntime();
|
|
1025
|
-
const route = runtime.channel.routing.resolveAgentRoute({
|
|
1026
|
-
cfg: params.cfg,
|
|
1027
|
-
channel: CHANNEL_ID$1,
|
|
1028
|
-
accountId: params.account.accountId,
|
|
1029
|
-
peer: {
|
|
1030
|
-
kind: params.isDirect ? "direct" : "channel",
|
|
1031
|
-
id: params.target
|
|
1032
|
-
}
|
|
1033
|
-
});
|
|
1034
|
-
const agentId = normalizeAgentId(params.account.agentId ?? route.agentId);
|
|
1035
|
-
if (agentId === route.agentId) return route;
|
|
1036
|
-
const peer = {
|
|
1037
|
-
kind: params.isDirect ? "direct" : "channel",
|
|
1038
|
-
id: params.target
|
|
1039
|
-
};
|
|
1040
|
-
const dmScope = params.cfg.session?.dmScope ?? "main";
|
|
1041
|
-
const sessionKey = runtime.channel.routing.buildAgentSessionKey({
|
|
1042
|
-
agentId,
|
|
1043
|
-
mainKey: params.cfg.session?.mainKey,
|
|
1044
|
-
channel: CHANNEL_ID$1,
|
|
1045
|
-
accountId: params.account.accountId,
|
|
1046
|
-
peer,
|
|
1047
|
-
dmScope,
|
|
1048
|
-
identityLinks: params.cfg.session?.identityLinks
|
|
1049
|
-
});
|
|
1050
|
-
const mainSessionKey = runtime.channel.routing.buildAgentSessionKey({
|
|
1051
|
-
agentId,
|
|
1052
|
-
mainKey: params.cfg.session?.mainKey,
|
|
1053
|
-
channel: CHANNEL_ID$1,
|
|
1054
|
-
accountId: params.account.accountId,
|
|
1055
|
-
dmScope: "main"
|
|
1056
|
-
});
|
|
1057
|
-
return {
|
|
1058
|
-
...route,
|
|
1059
|
-
agentId,
|
|
1060
|
-
dmScope,
|
|
1061
|
-
sessionKey,
|
|
1062
|
-
mainSessionKey,
|
|
1063
|
-
lastRoutePolicy: sessionKey === mainSessionKey ? "main" : "session"
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
1592
|
async function dispatchModelReply(params) {
|
|
1067
1593
|
const runtime = getClickClackRuntime();
|
|
1068
1594
|
const text = (await runtime.llm.complete({
|
|
@@ -1104,50 +1630,56 @@ async function handleClickClackInbound(params) {
|
|
|
1104
1630
|
config: params.config,
|
|
1105
1631
|
message
|
|
1106
1632
|
});
|
|
1107
|
-
if (!access.shouldDispatch) return;
|
|
1633
|
+
if (!access.shouldDispatch || !access.channelIngress) return;
|
|
1108
1634
|
const conversationId = message.channel_id || message.direct_conversation_id;
|
|
1109
1635
|
if (!conversationId) return;
|
|
1110
|
-
const isDirect =
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
} : {
|
|
1116
|
-
chatType: "group",
|
|
1117
|
-
kind: "channel",
|
|
1118
|
-
id: message.channel_id ?? ""
|
|
1119
|
-
});
|
|
1120
|
-
const accountRoute = resolveAccountAgentRoute({
|
|
1121
|
-
cfg: params.config,
|
|
1122
|
-
account: params.account,
|
|
1123
|
-
target,
|
|
1124
|
-
isDirect
|
|
1125
|
-
});
|
|
1126
|
-
const discussionResolution = !isDirect && message.channel_id ? resolveClickClackDiscussionRoute({
|
|
1127
|
-
runtime,
|
|
1128
|
-
config: params.config,
|
|
1129
|
-
accountId: params.account.accountId,
|
|
1130
|
-
serverBaseUrl: params.account.baseUrl,
|
|
1131
|
-
workspaceId: message.workspace_id,
|
|
1132
|
-
channelId: message.channel_id
|
|
1133
|
-
}) : { state: "unbound" };
|
|
1134
|
-
if (discussionResolution.state === "revoked") return;
|
|
1135
|
-
const discussionRoute = discussionResolution.state === "active" ? discussionResolution.route : void 0;
|
|
1136
|
-
const route = discussionRoute ? {
|
|
1137
|
-
...accountRoute,
|
|
1138
|
-
agentId: discussionRoute.agentId,
|
|
1139
|
-
sessionKey: discussionRoute.sessionKey,
|
|
1140
|
-
lastRoutePolicy: "session"
|
|
1141
|
-
} : accountRoute;
|
|
1142
|
-
if (params.account.replyMode === "model" && !discussionRoute) {
|
|
1143
|
-
await dispatchModelReply({
|
|
1144
|
-
account: params.account,
|
|
1145
|
-
cfg: params.config,
|
|
1146
|
-
message,
|
|
1147
|
-
route,
|
|
1148
|
-
target,
|
|
1636
|
+
const { discussionRoute, isDirect, route, target } = access.preparedRoute;
|
|
1637
|
+
const progress = params.account.nativeProgress ? createClickClackAgentProgressPublisher({
|
|
1638
|
+
client: createClickClackClient({
|
|
1639
|
+
baseUrl: params.account.apiEndpoint,
|
|
1640
|
+
token: params.account.token,
|
|
1149
1641
|
correlationId: params.correlationId
|
|
1150
|
-
})
|
|
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
|
+
}
|
|
1151
1683
|
return;
|
|
1152
1684
|
}
|
|
1153
1685
|
let turnProvenance;
|
|
@@ -1177,7 +1709,8 @@ async function handleClickClackInbound(params) {
|
|
|
1177
1709
|
timestamp: new Date(message.created_at),
|
|
1178
1710
|
body: message.body
|
|
1179
1711
|
});
|
|
1180
|
-
const ctxPayload =
|
|
1712
|
+
const ctxPayload = (params.buildContext ?? buildChannelInboundEventContext)({
|
|
1713
|
+
channelIngress: access.channelIngress,
|
|
1181
1714
|
channel: CHANNEL_ID$1,
|
|
1182
1715
|
accountId: route.accountId ?? params.account.accountId,
|
|
1183
1716
|
messageId: message.id,
|
|
@@ -1216,10 +1749,7 @@ async function handleClickClackInbound(params) {
|
|
|
1216
1749
|
},
|
|
1217
1750
|
access: {
|
|
1218
1751
|
commands: { authorized: access.commandAuthorized },
|
|
1219
|
-
mentions:
|
|
1220
|
-
canDetectMention: !isDirect,
|
|
1221
|
-
wasMentioned: !isDirect
|
|
1222
|
-
}
|
|
1752
|
+
mentions: access.mentionFacts
|
|
1223
1753
|
},
|
|
1224
1754
|
extra: {
|
|
1225
1755
|
GroupChannel: message.channel_id,
|
|
@@ -1227,20 +1757,27 @@ async function handleClickClackInbound(params) {
|
|
|
1227
1757
|
}
|
|
1228
1758
|
});
|
|
1229
1759
|
const runId = resolveClickClackAgentRunId(message.id);
|
|
1230
|
-
const activityReplyOptions =
|
|
1231
|
-
onModelSelected: (ctx) => {
|
|
1760
|
+
const activityReplyOptions = {
|
|
1761
|
+
...activity ? { onModelSelected: (ctx) => {
|
|
1232
1762
|
turnProvenance = {
|
|
1233
1763
|
model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
|
|
1234
1764
|
thinking: ctx.thinkLevel
|
|
1235
1765
|
};
|
|
1236
|
-
activity
|
|
1237
|
-
},
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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({
|
|
1244
1781
|
cfg: params.config,
|
|
1245
1782
|
channel: CHANNEL_ID$1,
|
|
1246
1783
|
accountId: params.account.accountId,
|
|
@@ -1250,11 +1787,12 @@ async function handleClickClackInbound(params) {
|
|
|
1250
1787
|
sessionKey: route.sessionKey
|
|
1251
1788
|
},
|
|
1252
1789
|
ctxPayload,
|
|
1790
|
+
botLoopProtection: access.botLoopProtection,
|
|
1253
1791
|
toolsAllow: params.account.toolsAllow,
|
|
1254
|
-
replyOptions:
|
|
1792
|
+
replyOptions: {
|
|
1255
1793
|
...runId ? { runId } : {},
|
|
1256
1794
|
...activityReplyOptions
|
|
1257
|
-
}
|
|
1795
|
+
},
|
|
1258
1796
|
delivery: {
|
|
1259
1797
|
deliver: async (payload) => {
|
|
1260
1798
|
if (hasClickClackReplyMedia(payload)) throw new Error("ClickClack media reply requires durable delivery");
|
|
@@ -1296,8 +1834,9 @@ async function handleClickClackInbound(params) {
|
|
|
1296
1834
|
} }
|
|
1297
1835
|
});
|
|
1298
1836
|
try {
|
|
1299
|
-
await
|
|
1837
|
+
await dispatch();
|
|
1300
1838
|
} finally {
|
|
1839
|
+
await progress?.finalize();
|
|
1301
1840
|
await activity?.finalize();
|
|
1302
1841
|
}
|
|
1303
1842
|
}
|
|
@@ -1305,8 +1844,7 @@ async function handleClickClackInbound(params) {
|
|
|
1305
1844
|
//#region extensions/clickclack/src/gateway.ts
|
|
1306
1845
|
const CLICKCLACK_EVENT_PAGE_LIMIT = 500;
|
|
1307
1846
|
function payloadString(event, key) {
|
|
1308
|
-
|
|
1309
|
-
return typeof value === "string" ? value : "";
|
|
1847
|
+
return readStringField(event.payload, key) ?? "";
|
|
1310
1848
|
}
|
|
1311
1849
|
function eventCorrelationId(event) {
|
|
1312
1850
|
return normalizeClickClackCorrelationId(event.payload?.correlation_id);
|
|
@@ -1314,25 +1852,16 @@ function eventCorrelationId(event) {
|
|
|
1314
1852
|
async function resolveEventMessage(params) {
|
|
1315
1853
|
const messageId = payloadString(params.event, "message_id");
|
|
1316
1854
|
if (!messageId) return null;
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
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;
|
|
1323
1860
|
}
|
|
1324
|
-
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;
|
|
1325
|
-
return null;
|
|
1326
|
-
}
|
|
1327
|
-
function decodeSocketMessage(data) {
|
|
1328
|
-
if (typeof data === "string") return data;
|
|
1329
|
-
if (Buffer.isBuffer(data)) return data.toString("utf8");
|
|
1330
|
-
if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
|
|
1331
|
-
return Buffer.concat(data).toString("utf8");
|
|
1332
1861
|
}
|
|
1333
1862
|
function parseSocketEvent(data) {
|
|
1334
1863
|
try {
|
|
1335
|
-
return JSON.parse(
|
|
1864
|
+
return JSON.parse(rawDataToString(data));
|
|
1336
1865
|
} catch {
|
|
1337
1866
|
return null;
|
|
1338
1867
|
}
|
|
@@ -1349,19 +1878,26 @@ async function processEvent(params) {
|
|
|
1349
1878
|
}) : params.client,
|
|
1350
1879
|
event: params.event
|
|
1351
1880
|
});
|
|
1352
|
-
if (!message
|
|
1353
|
-
|
|
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;
|
|
1354
1886
|
const access = await resolveClickClackInboundAccess({
|
|
1355
1887
|
account: params.account,
|
|
1356
1888
|
config: params.config,
|
|
1357
1889
|
message
|
|
1358
1890
|
});
|
|
1359
|
-
if (!access.shouldDispatch)
|
|
1891
|
+
if (!access.shouldDispatch) {
|
|
1892
|
+
params.log?.info(`[${params.account.accountId}] skipped ClickClack message before agent dispatch: kind=${message.direct_conversation_id ? "dm" : "group"} requireMention=${access.requireMention ?? "unknown"} wasMentioned=${access.mentionFacts.wasMentioned} hasAnyMention=${access.mentionFacts.hasAnyMention ?? "unknown"} commandAuthorized=${access.commandAuthorized}`);
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1360
1895
|
await handleClickClackInbound({
|
|
1361
1896
|
account: params.account,
|
|
1362
1897
|
config: params.config,
|
|
1363
1898
|
message,
|
|
1364
1899
|
access,
|
|
1900
|
+
buildContext: params.buildContext,
|
|
1365
1901
|
...correlationId ? { correlationId } : {}
|
|
1366
1902
|
});
|
|
1367
1903
|
}
|
|
@@ -1397,14 +1933,17 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1397
1933
|
const account = {
|
|
1398
1934
|
...configuredAccount,
|
|
1399
1935
|
workspace: workspaceId,
|
|
1400
|
-
botUserId: configuredAccount.botUserId ?? me.id
|
|
1936
|
+
botUserId: configuredAccount.botUserId ?? me.id,
|
|
1937
|
+
botHandle: me.handle
|
|
1401
1938
|
};
|
|
1402
1939
|
const processIncomingEvent = (event) => processEvent({
|
|
1403
1940
|
account,
|
|
1404
1941
|
config: ctx.cfg,
|
|
1405
1942
|
client,
|
|
1406
1943
|
event,
|
|
1407
|
-
botUserId: account.botUserId
|
|
1944
|
+
botUserId: account.botUserId,
|
|
1945
|
+
buildContext: ctx.channelRuntime?.inbound.buildContext,
|
|
1946
|
+
log: ctx.log
|
|
1408
1947
|
});
|
|
1409
1948
|
if (account.commandMenu) await syncClickClackCommandMenu({
|
|
1410
1949
|
cfg: ctx.cfg,
|
|
@@ -1414,6 +1953,7 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1414
1953
|
ctx.setStatus({
|
|
1415
1954
|
accountId: account.accountId,
|
|
1416
1955
|
running: true,
|
|
1956
|
+
lifecycle: "starting",
|
|
1417
1957
|
configured: true,
|
|
1418
1958
|
enabled: account.enabled,
|
|
1419
1959
|
baseUrl: account.baseUrl
|
|
@@ -1469,6 +2009,9 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1469
2009
|
};
|
|
1470
2010
|
ctx.abortSignal.addEventListener("abort", abort, { once: true });
|
|
1471
2011
|
removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
|
|
2012
|
+
socket.on("open", () => {
|
|
2013
|
+
ctx.setStatus(channelReadyPatch({ accountId: account.accountId }));
|
|
2014
|
+
});
|
|
1472
2015
|
socket.on("message", (data) => {
|
|
1473
2016
|
if (closing || settled) return;
|
|
1474
2017
|
messageQueue = messageQueue.then(async () => {
|
|
@@ -1484,6 +2027,11 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1484
2027
|
});
|
|
1485
2028
|
socket.on("close", () => {
|
|
1486
2029
|
closing = true;
|
|
2030
|
+
if (!ctx.abortSignal.aborted) ctx.setStatus({
|
|
2031
|
+
accountId: account.accountId,
|
|
2032
|
+
connected: false,
|
|
2033
|
+
lifecycle: "recovering"
|
|
2034
|
+
});
|
|
1487
2035
|
finishAfterQueuedMessages();
|
|
1488
2036
|
});
|
|
1489
2037
|
socket.on("error", (error) => {
|
|
@@ -1493,6 +2041,12 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1493
2041
|
}
|
|
1494
2042
|
if (closing) return;
|
|
1495
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
|
+
});
|
|
1496
2050
|
closing = true;
|
|
1497
2051
|
socket.close();
|
|
1498
2052
|
});
|
|
@@ -1504,10 +2058,7 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1504
2058
|
}
|
|
1505
2059
|
}
|
|
1506
2060
|
} finally {
|
|
1507
|
-
ctx.setStatus({
|
|
1508
|
-
accountId: account.accountId,
|
|
1509
|
-
running: false
|
|
1510
|
-
});
|
|
2061
|
+
ctx.setStatus(channelStoppedPatch({ accountId: account.accountId }));
|
|
1511
2062
|
}
|
|
1512
2063
|
}
|
|
1513
2064
|
//#endregion
|
|
@@ -1610,7 +2161,6 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1610
2161
|
reload: { configPrefixes: ["channels.clickclack"] },
|
|
1611
2162
|
configSchema: clickClackConfigSchema,
|
|
1612
2163
|
config: clickClackConfigAdapter,
|
|
1613
|
-
setup: clickClackSetupAdapter,
|
|
1614
2164
|
setupContract: clickClackSetupContract,
|
|
1615
2165
|
setupWizard: clickClackSetupWizard,
|
|
1616
2166
|
secrets: {
|
|
@@ -1717,4 +2267,4 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1717
2267
|
}
|
|
1718
2268
|
});
|
|
1719
2269
|
//#endregion
|
|
1720
|
-
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 };
|