@openclaw/clickclack 2026.7.1 → 2026.7.2-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 +27 -0
- package/dist/api.js +2 -1
- package/dist/{channel-1ZnJZTPl.js → channel-BnREQIP8.js} +571 -429
- package/dist/channel-plugin-api.js +1 -1
- package/dist/runtime-api.js +2 -1
- package/dist/secret-contract-BKkzzjBA.js +35 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +18 -0
- package/dist/setup-surface-B346XaAu.js +828 -0
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +12 -0
- package/package.json +14 -6
|
@@ -1,155 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
|
|
4
|
-
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
|
|
5
|
-
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
|
|
6
|
-
import { buildSecretInputSchema, normalizeResolvedSecretInputString, normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
7
|
-
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
1
|
+
import { a as createClickClackClient, c as CLICKCLACK_CHANNEL_ID, d as DEFAULT_ACCOUNT_ID, i as resolveWorkspaceId, l as clickClackConfigAdapter, m as resolveClickClackAccount, n as clickClackSetupAdapter, o as normalizeClickClackCorrelationId, r as resolveChannelId, s as clickClackConfigSchema, t as clickClackSetupWizard, u as clickClackMeta } from "./setup-surface-B346XaAu.js";
|
|
2
|
+
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BKkzzjBA.js";
|
|
8
3
|
import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
9
|
-
import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
|
|
10
|
-
import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
|
|
4
|
+
import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-outbound";
|
|
11
5
|
import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
12
|
-
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
13
|
-
import { z } from "zod";
|
|
14
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
|
+
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
|
15
8
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
16
9
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
17
|
-
import {
|
|
18
|
-
import { WebSocket } from "ws";
|
|
10
|
+
import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
|
|
19
11
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* account overrides, and secret-provider references.
|
|
24
|
-
*/
|
|
25
|
-
const DEFAULT_RECONNECT_MS = 1500;
|
|
26
|
-
const MIN_RECONNECT_MS = 100;
|
|
27
|
-
const MAX_RECONNECT_MS = 6e4;
|
|
28
|
-
const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId } = createAccountListHelpers("clickclack", {
|
|
29
|
-
normalizeAccountId,
|
|
30
|
-
hasImplicitDefaultAccount: (cfg) => {
|
|
31
|
-
const channel = cfg.channels?.clickclack;
|
|
32
|
-
return Boolean(channel?.baseUrl?.trim() && hasConfiguredAccountValue(channel.token) && channel.workspace?.trim());
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
function resolveMergedClickClackAccountConfig(cfg, accountId) {
|
|
36
|
-
return resolveMergedAccountConfig({
|
|
37
|
-
channelConfig: cfg.channels?.clickclack,
|
|
38
|
-
accounts: cfg.channels?.clickclack?.accounts,
|
|
39
|
-
accountId,
|
|
40
|
-
omitKeys: ["defaultAccount"],
|
|
41
|
-
normalizeAccountId
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
function resolveClickClackToken(params) {
|
|
45
|
-
const resolved = resolveSecretInputString({
|
|
46
|
-
value: params.value,
|
|
47
|
-
path: params.accountId === DEFAULT_ACCOUNT_ID ? "channels.clickclack.token" : `channels.clickclack.accounts.${params.accountId}.token`,
|
|
48
|
-
defaults: params.cfg.secrets?.defaults,
|
|
49
|
-
mode: "inspect"
|
|
50
|
-
});
|
|
51
|
-
if (resolved.status !== "available") {
|
|
52
|
-
if (resolved.status === "configured_unavailable" && resolved.ref.source === "env") {
|
|
53
|
-
const providerConfig = params.cfg.secrets?.providers?.[resolved.ref.provider];
|
|
54
|
-
if (providerConfig) {
|
|
55
|
-
if (providerConfig.source !== "env") throw new Error(`Secret provider "${resolved.ref.provider}" has source "${providerConfig.source}" but ref requests "env".`);
|
|
56
|
-
if (providerConfig.allowlist && !providerConfig.allowlist.includes(resolved.ref.id)) throw new Error(`Environment variable "${resolved.ref.id}" is not allowlisted in secrets.providers.${resolved.ref.provider}.allowlist.`);
|
|
57
|
-
} else if (resolved.ref.provider !== resolveDefaultSecretProviderAlias({ secrets: params.cfg.secrets }, "env")) throw new Error(`Secret provider "${resolved.ref.provider}" is not configured (ref: env:${resolved.ref.provider}:${resolved.ref.id}).`);
|
|
58
|
-
return normalizeSecretInputString((params.env ?? process.env)[resolved.ref.id]) ?? "";
|
|
59
|
-
}
|
|
60
|
-
return "";
|
|
61
|
-
}
|
|
62
|
-
return normalizeResolvedSecretInputString({
|
|
63
|
-
value: resolved.value,
|
|
64
|
-
path: "channels.clickclack.token"
|
|
65
|
-
}) ?? "";
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Builds the normalized account snapshot used by gateway, outbound delivery,
|
|
69
|
-
* status reporting, and channel routing.
|
|
70
|
-
*/
|
|
71
|
-
function resolveClickClackAccount(params) {
|
|
72
|
-
const accountId = normalizeAccountId(params.accountId);
|
|
73
|
-
const merged = resolveMergedClickClackAccountConfig(params.cfg, accountId);
|
|
74
|
-
const enabled = params.cfg.channels?.clickclack?.enabled !== false && merged.enabled !== false;
|
|
75
|
-
const baseUrl = merged.baseUrl?.trim().replace(/\/$/, "") ?? "";
|
|
76
|
-
const token = resolveClickClackToken({
|
|
77
|
-
cfg: params.cfg,
|
|
78
|
-
value: merged.token,
|
|
79
|
-
accountId,
|
|
80
|
-
env: params.env
|
|
81
|
-
});
|
|
82
|
-
const workspace = merged.workspace?.trim() ?? "";
|
|
83
|
-
return {
|
|
84
|
-
accountId,
|
|
85
|
-
enabled,
|
|
86
|
-
configured: Boolean(baseUrl && token && workspace),
|
|
87
|
-
name: normalizeOptionalString(merged.name),
|
|
88
|
-
baseUrl,
|
|
89
|
-
token,
|
|
90
|
-
workspace,
|
|
91
|
-
botUserId: normalizeOptionalString(merged.botUserId),
|
|
92
|
-
agentId: normalizeOptionalString(merged.agentId),
|
|
93
|
-
replyMode: merged.replyMode === "model" ? "model" : "agent",
|
|
94
|
-
model: normalizeOptionalString(merged.model),
|
|
95
|
-
systemPrompt: normalizeOptionalString(merged.systemPrompt),
|
|
96
|
-
timeoutSeconds: merged.timeoutSeconds,
|
|
97
|
-
toolsAllow: merged.toolsAllow,
|
|
98
|
-
defaultTo: merged.defaultTo?.trim() || "channel:general",
|
|
99
|
-
allowFrom: merged.allowFrom ?? ["*"],
|
|
100
|
-
reconnectMs: resolveIntegerOption(merged.reconnectMs, DEFAULT_RECONNECT_MS, {
|
|
101
|
-
min: MIN_RECONNECT_MS,
|
|
102
|
-
max: MAX_RECONNECT_MS
|
|
103
|
-
}),
|
|
104
|
-
agentActivity: merged.agentActivity === true,
|
|
105
|
-
config: {
|
|
106
|
-
...merged,
|
|
107
|
-
allowFrom: merged.allowFrom ?? ["*"]
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Returns all enabled accounts, including the implicit default account when
|
|
113
|
-
* legacy top-level ClickClack config is present.
|
|
114
|
-
*/
|
|
115
|
-
function listEnabledClickClackAccounts(cfg) {
|
|
116
|
-
return listClickClackAccountIds(cfg).map((accountId) => resolveClickClackAccount({
|
|
117
|
-
cfg,
|
|
118
|
-
accountId
|
|
119
|
-
})).filter((account) => account.enabled);
|
|
120
|
-
}
|
|
121
|
-
//#endregion
|
|
122
|
-
//#region extensions/clickclack/src/config-schema.ts
|
|
123
|
-
/**
|
|
124
|
-
* Zod-backed config schema for ClickClack channel accounts.
|
|
125
|
-
*/
|
|
126
|
-
const ClickClackAccountConfigSchema = z.object({
|
|
127
|
-
name: z.string().optional(),
|
|
128
|
-
enabled: z.boolean().optional(),
|
|
129
|
-
baseUrl: z.string().url().optional(),
|
|
130
|
-
token: buildSecretInputSchema().optional(),
|
|
131
|
-
workspace: z.string().optional(),
|
|
132
|
-
botUserId: z.string().optional(),
|
|
133
|
-
agentId: z.string().optional(),
|
|
134
|
-
replyMode: z.enum(["agent", "model"]).optional(),
|
|
135
|
-
model: z.string().optional(),
|
|
136
|
-
systemPrompt: z.string().optional(),
|
|
137
|
-
timeoutSeconds: z.number().int().min(1).max(3600).optional(),
|
|
138
|
-
toolsAllow: z.array(z.string()).optional(),
|
|
139
|
-
defaultTo: z.string().optional(),
|
|
140
|
-
allowFrom: z.array(z.string()).optional(),
|
|
141
|
-
reconnectMs: z.number().int().min(100).max(6e4).optional(),
|
|
142
|
-
agentActivity: z.boolean().optional()
|
|
143
|
-
}).strict();
|
|
144
|
-
/**
|
|
145
|
-
* Config schema exported to core so `openclaw doctor` and config validation
|
|
146
|
-
* understand both default and named ClickClack accounts.
|
|
147
|
-
*/
|
|
148
|
-
const clickClackConfigSchema = buildChannelConfigSchema(ClickClackAccountConfigSchema.extend({
|
|
149
|
-
accounts: z.record(z.string(), ClickClackAccountConfigSchema.partial()).optional(),
|
|
150
|
-
defaultAccount: z.string().optional()
|
|
151
|
-
}).strict());
|
|
152
|
-
//#endregion
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
|
14
|
+
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
153
15
|
//#region extensions/clickclack/src/runtime.ts
|
|
154
16
|
/**
|
|
155
17
|
* Runtime store for host-provided OpenClaw services used by the ClickClack
|
|
@@ -212,139 +74,62 @@ async function resolveClickClackInboundAccess(params) {
|
|
|
212
74
|
};
|
|
213
75
|
}
|
|
214
76
|
//#endregion
|
|
215
|
-
//#region extensions/clickclack/src/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
* are ignored by servers without the provenance columns, so these are safe
|
|
223
|
-
* to send unconditionally when present.
|
|
224
|
-
*/
|
|
225
|
-
function provenanceFields(provenance) {
|
|
226
|
-
const fields = {};
|
|
227
|
-
if (provenance?.model?.trim()) fields.author_model = provenance.model.trim();
|
|
228
|
-
if (provenance?.thinking?.trim()) fields.author_thinking = provenance.thinking.trim();
|
|
229
|
-
if (provenance?.runtime?.trim()) fields.author_runtime = provenance.runtime.trim();
|
|
230
|
-
return fields;
|
|
77
|
+
//#region extensions/clickclack/src/command-menu.ts
|
|
78
|
+
const CLICKCLACK_COMMAND_PATTERN = /^[a-z0-9_-]{1,32}$/u;
|
|
79
|
+
const CLICKCLACK_MAX_COMMANDS = 100;
|
|
80
|
+
const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
|
|
81
|
+
const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
|
|
82
|
+
function truncateCodePoints(value, maxLength) {
|
|
83
|
+
return Array.from(value).slice(0, maxLength).join("");
|
|
231
84
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (!response.ok) {
|
|
252
|
-
const detail = await readResponseTextLimited(response, CLICKCLACK_ERROR_BODY_LIMIT_BYTES);
|
|
253
|
-
throw new Error(`ClickClack ${response.status}: ${detail}`);
|
|
85
|
+
function commandArgsHint(spec) {
|
|
86
|
+
if (spec.args?.length) return truncateCodePoints(spec.args.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" "), CLICKCLACK_MAX_ARGS_HINT_LENGTH);
|
|
87
|
+
return spec.acceptsArgs ? "[args]" : "";
|
|
88
|
+
}
|
|
89
|
+
function errorStatus(error) {
|
|
90
|
+
if (!error || typeof error !== "object" || !("status" in error)) return;
|
|
91
|
+
const status = error.status;
|
|
92
|
+
return typeof status === "number" ? status : void 0;
|
|
93
|
+
}
|
|
94
|
+
function mapNativeCommandSpecsToClickClackMenu(specs, log) {
|
|
95
|
+
const commands = [];
|
|
96
|
+
const seen = /* @__PURE__ */ new Set();
|
|
97
|
+
const invalidNames = /* @__PURE__ */ new Set();
|
|
98
|
+
for (const spec of specs) {
|
|
99
|
+
if (spec.isAlias) continue;
|
|
100
|
+
const command = spec.name.trim().toLowerCase();
|
|
101
|
+
if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
|
|
102
|
+
invalidNames.add(spec.name.trim() || "<empty>");
|
|
103
|
+
continue;
|
|
254
104
|
}
|
|
255
|
-
|
|
105
|
+
if (seen.has(command)) continue;
|
|
106
|
+
seen.add(command);
|
|
107
|
+
const description = spec.description.trim() || command;
|
|
108
|
+
commands.push({
|
|
109
|
+
command,
|
|
110
|
+
description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
|
|
111
|
+
args_hint: commandArgsHint(spec)
|
|
112
|
+
});
|
|
256
113
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return
|
|
269
|
-
},
|
|
270
|
-
directMessages: async (conversationId, afterSeq, limit = 20) => {
|
|
271
|
-
return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
|
|
272
|
-
},
|
|
273
|
-
thread: async (messageId) => await request(`/api/messages/${encodeURIComponent(messageId)}/thread`),
|
|
274
|
-
createChannelMessage: async (channelId, body, opts) => {
|
|
275
|
-
return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages`, {
|
|
276
|
-
method: "POST",
|
|
277
|
-
body: JSON.stringify({
|
|
278
|
-
body,
|
|
279
|
-
...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {},
|
|
280
|
-
...provenanceFields(opts?.provenance)
|
|
281
|
-
})
|
|
282
|
-
})).message;
|
|
283
|
-
},
|
|
284
|
-
createThreadReply: async (messageId, body, opts) => {
|
|
285
|
-
return (await request(`/api/messages/${encodeURIComponent(messageId)}/thread/replies`, {
|
|
286
|
-
method: "POST",
|
|
287
|
-
body: JSON.stringify({
|
|
288
|
-
body,
|
|
289
|
-
...provenanceFields(opts?.provenance)
|
|
290
|
-
})
|
|
291
|
-
})).message;
|
|
292
|
-
},
|
|
293
|
-
createDirectConversation: async (workspaceId, memberIds) => {
|
|
294
|
-
return (await request("/api/dms", {
|
|
295
|
-
method: "POST",
|
|
296
|
-
body: JSON.stringify({
|
|
297
|
-
workspace_id: workspaceId,
|
|
298
|
-
member_ids: memberIds
|
|
299
|
-
})
|
|
300
|
-
})).conversation;
|
|
301
|
-
},
|
|
302
|
-
/**
|
|
303
|
-
* POSTs a durable agent activity row (agent_commentary / agent_tool)
|
|
304
|
-
* through the normal message create path. Requires a bot token carrying
|
|
305
|
-
* the agent_activity:write scope on the ClickClack side.
|
|
306
|
-
*/
|
|
307
|
-
createActivityMessage: async (params) => {
|
|
308
|
-
if (!params.channelId && !params.conversationId) throw new Error("createActivityMessage requires a channelId or conversationId");
|
|
309
|
-
return (await request(params.channelId ? `/api/channels/${encodeURIComponent(params.channelId)}/messages` : `/api/dms/${encodeURIComponent(params.conversationId ?? "")}/messages`, {
|
|
310
|
-
method: "POST",
|
|
311
|
-
body: JSON.stringify({
|
|
312
|
-
body: params.body,
|
|
313
|
-
kind: params.kind,
|
|
314
|
-
turn_id: params.turnId,
|
|
315
|
-
...provenanceFields(params.provenance)
|
|
316
|
-
})
|
|
317
|
-
})).message;
|
|
318
|
-
},
|
|
319
|
-
/** PATCHes the body of an existing message (activity row coalescing). */
|
|
320
|
-
updateMessageBody: async (messageId, body) => {
|
|
321
|
-
return (await request(`/api/messages/${encodeURIComponent(messageId)}`, {
|
|
322
|
-
method: "PATCH",
|
|
323
|
-
body: JSON.stringify({ body })
|
|
324
|
-
})).message;
|
|
325
|
-
},
|
|
326
|
-
createDirectMessage: async (conversationId, body, opts) => {
|
|
327
|
-
return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages`, {
|
|
328
|
-
method: "POST",
|
|
329
|
-
body: JSON.stringify({
|
|
330
|
-
body,
|
|
331
|
-
...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {}
|
|
332
|
-
})
|
|
333
|
-
})).message;
|
|
334
|
-
},
|
|
335
|
-
events: async (workspaceId, afterCursor) => {
|
|
336
|
-
const query = new URLSearchParams({ workspace_id: workspaceId });
|
|
337
|
-
if (afterCursor) query.set("after_cursor", afterCursor);
|
|
338
|
-
return (await request(`/api/realtime/events?${query.toString()}`)).events;
|
|
339
|
-
},
|
|
340
|
-
websocket: (workspaceId, afterCursor) => {
|
|
341
|
-
const url = new URL(`${baseUrl}/api/realtime/ws`);
|
|
342
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
343
|
-
url.searchParams.set("workspace_id", workspaceId);
|
|
344
|
-
if (afterCursor) url.searchParams.set("after_cursor", afterCursor);
|
|
345
|
-
return new WebSocket(url, { headers: { Authorization: `Bearer ${options.token}` } });
|
|
114
|
+
if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
|
|
115
|
+
return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
|
|
116
|
+
}
|
|
117
|
+
async function syncClickClackCommandMenu(params) {
|
|
118
|
+
try {
|
|
119
|
+
const commands = mapNativeCommandSpecsToClickClackMenu(listNativeCommandSpecsForConfig(params.cfg, { provider: "clickclack" }), params.log);
|
|
120
|
+
await params.client.setBotCommands(commands);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const status = errorStatus(error);
|
|
123
|
+
if (status === 403) {
|
|
124
|
+
params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
|
|
125
|
+
return;
|
|
346
126
|
}
|
|
347
|
-
|
|
127
|
+
if (status === 404) {
|
|
128
|
+
params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
|
|
132
|
+
}
|
|
348
133
|
}
|
|
349
134
|
//#endregion
|
|
350
135
|
//#region extensions/clickclack/src/activity.ts
|
|
@@ -542,27 +327,6 @@ function createClickClackActivityPublisher(params) {
|
|
|
542
327
|
};
|
|
543
328
|
}
|
|
544
329
|
//#endregion
|
|
545
|
-
//#region extensions/clickclack/src/resolve.ts
|
|
546
|
-
/**
|
|
547
|
-
* Resolves a workspace slug/name/id from config to a ClickClack workspace id.
|
|
548
|
-
*/
|
|
549
|
-
async function resolveWorkspaceId(client, workspace) {
|
|
550
|
-
if (workspace.startsWith("wsp_")) return workspace;
|
|
551
|
-
const found = (await client.workspaces()).find((candidate) => candidate.id === workspace || candidate.slug === workspace || candidate.name === workspace);
|
|
552
|
-
if (!found) throw new Error(`ClickClack workspace not found: ${workspace}`);
|
|
553
|
-
return found.id;
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* Resolves a channel name/id from config or target input to a ClickClack
|
|
557
|
-
* channel id.
|
|
558
|
-
*/
|
|
559
|
-
async function resolveChannelId(client, workspaceId, channel) {
|
|
560
|
-
if (channel.startsWith("chn_")) return channel;
|
|
561
|
-
const found = (await client.channels(workspaceId)).find((candidate) => candidate.id === channel || candidate.name === channel);
|
|
562
|
-
if (!found) throw new Error(`ClickClack channel not found: ${channel}`);
|
|
563
|
-
return found.id;
|
|
564
|
-
}
|
|
565
|
-
//#endregion
|
|
566
330
|
//#region extensions/clickclack/src/target.ts
|
|
567
331
|
/**
|
|
568
332
|
* Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
|
|
@@ -612,52 +376,285 @@ function looksLikeClickClackTarget(raw) {
|
|
|
612
376
|
* Outbound ClickClack delivery helpers for channel messages, thread replies,
|
|
613
377
|
* and direct messages.
|
|
614
378
|
*/
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
379
|
+
const CLICKCLACK_MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
|
|
380
|
+
async function createTargetMessage(params) {
|
|
381
|
+
const parsed = parseClickClackTarget(params.to);
|
|
382
|
+
const explicitThreadId = params.threadId == null ? "" : String(params.threadId);
|
|
383
|
+
const replyToId = params.replyToId == null ? "" : String(params.replyToId);
|
|
384
|
+
if (explicitThreadId || parsed.kind === "thread") {
|
|
385
|
+
const rootId = explicitThreadId || parsed.id;
|
|
386
|
+
await params.onPlatformSendDispatch?.();
|
|
387
|
+
return await params.client.createThreadReply(rootId, params.text, {
|
|
388
|
+
provenance: params.provenance,
|
|
389
|
+
nonce: params.nonce
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (parsed.kind === "dm") {
|
|
393
|
+
await params.onPlatformSendDispatch?.();
|
|
394
|
+
const dm = await params.client.createDirectConversation(params.workspaceId, [parsed.id]);
|
|
395
|
+
return await params.client.createDirectMessage(dm.id, params.text, {
|
|
396
|
+
quotedMessageId: replyToId || void 0,
|
|
397
|
+
nonce: params.nonce
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
const channelId = await resolveChannelId(params.client, params.workspaceId, parsed.id);
|
|
401
|
+
await params.onPlatformSendDispatch?.();
|
|
402
|
+
return await params.client.createChannelMessage(channelId, params.text, {
|
|
403
|
+
provenance: params.provenance,
|
|
404
|
+
quotedMessageId: replyToId || void 0,
|
|
405
|
+
nonce: params.nonce
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function durableDeliveryDigest(params) {
|
|
409
|
+
if (!params.deliveryQueueId) return;
|
|
410
|
+
if (!Number.isSafeInteger(params.deliveryPartIndex) || (params.deliveryPartIndex ?? -1) < 0) throw new Error("ClickClack durable delivery requires a stable delivery part index");
|
|
411
|
+
return createHash("sha256").update(`${params.deliveryQueueId}\n${params.deliveryPartIndex}`).digest("hex");
|
|
412
|
+
}
|
|
413
|
+
function mediaDeliveryNonces(params) {
|
|
414
|
+
const digest = durableDeliveryDigest(params);
|
|
415
|
+
if (!digest) return {};
|
|
416
|
+
return {
|
|
417
|
+
message: `openclaw-media:${digest}`,
|
|
418
|
+
upload: `openclaw-upload:${digest}`
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function textDeliveryNonce(params) {
|
|
422
|
+
const digest = durableDeliveryDigest(params);
|
|
423
|
+
return digest ? `openclaw-text:${digest}` : void 0;
|
|
424
|
+
}
|
|
425
|
+
function createDispatchOnce(onPlatformSendDispatch) {
|
|
426
|
+
let dispatched = false;
|
|
427
|
+
return async () => {
|
|
428
|
+
if (dispatched) return;
|
|
429
|
+
await onPlatformSendDispatch?.();
|
|
430
|
+
dispatched = true;
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
async function attachUploadRetrySafe(params) {
|
|
434
|
+
try {
|
|
435
|
+
await params.client.attachUpload(params.messageId, params.uploadId);
|
|
436
|
+
} catch (firstError) {
|
|
437
|
+
try {
|
|
438
|
+
if ((await params.client.message(params.messageId)).attachments?.some((attachment) => attachment.id === params.uploadId)) return;
|
|
439
|
+
} catch {}
|
|
440
|
+
try {
|
|
441
|
+
await params.client.attachUpload(params.messageId, params.uploadId);
|
|
442
|
+
} catch {
|
|
443
|
+
throw firstError;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function createOutboundContext(params) {
|
|
620
448
|
const account = resolveClickClackAccount({
|
|
621
449
|
cfg: params.cfg,
|
|
622
450
|
accountId: params.accountId
|
|
623
451
|
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
452
|
+
return {
|
|
453
|
+
account,
|
|
454
|
+
client: createClickClackClient({
|
|
455
|
+
baseUrl: account.baseUrl,
|
|
456
|
+
token: account.token,
|
|
457
|
+
correlationId: params.correlationId
|
|
458
|
+
})
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Sends visible text to a normalized ClickClack target and returns the created
|
|
463
|
+
* message id, or undefined when sanitization removes all content.
|
|
464
|
+
*/
|
|
465
|
+
async function sendClickClackText(params) {
|
|
466
|
+
const text = sanitizeAssistantVisibleText(params.text);
|
|
467
|
+
if (!text) return;
|
|
468
|
+
const { account, client } = createOutboundContext(params);
|
|
469
|
+
const workspaceId = await resolveWorkspaceId(client, account.workspace);
|
|
470
|
+
const dispatch = createDispatchOnce(params.onPlatformSendDispatch);
|
|
471
|
+
return (await createTargetMessage({
|
|
472
|
+
client,
|
|
473
|
+
workspaceId,
|
|
474
|
+
to: params.to,
|
|
475
|
+
text,
|
|
476
|
+
threadId: params.threadId,
|
|
477
|
+
replyToId: params.replyToId,
|
|
478
|
+
provenance: params.provenance,
|
|
479
|
+
nonce: textDeliveryNonce({
|
|
480
|
+
deliveryQueueId: params.deliveryQueueId,
|
|
481
|
+
deliveryPartIndex: params.deliveryPartIndex
|
|
482
|
+
}),
|
|
483
|
+
onPlatformSendDispatch: dispatch
|
|
484
|
+
})).id;
|
|
485
|
+
}
|
|
486
|
+
/** Resolves, uploads, sends, then attaches one file to a ClickClack message. */
|
|
487
|
+
async function sendClickClackMedia(params) {
|
|
488
|
+
const nonces = mediaDeliveryNonces({
|
|
489
|
+
deliveryQueueId: params.deliveryQueueId,
|
|
490
|
+
deliveryPartIndex: params.deliveryPartIndex
|
|
627
491
|
});
|
|
492
|
+
const preloadedMedia = nonces.upload ? void 0 : await loadOutboundMediaFromUrl(params.mediaUrl, {
|
|
493
|
+
maxBytes: CLICKCLACK_MAX_UPLOAD_BYTES,
|
|
494
|
+
mediaAccess: params.mediaAccess,
|
|
495
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
496
|
+
mediaReadFile: params.mediaReadFile
|
|
497
|
+
});
|
|
498
|
+
const { account, client } = createOutboundContext(params);
|
|
628
499
|
const workspaceId = await resolveWorkspaceId(client, account.workspace);
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
500
|
+
const persistedUpload = nonces.upload ? await client.findUploadByNonce({
|
|
501
|
+
workspaceId,
|
|
502
|
+
nonce: nonces.upload
|
|
503
|
+
}) : void 0;
|
|
504
|
+
const dispatch = createDispatchOnce(params.onPlatformSendDispatch);
|
|
505
|
+
let upload = persistedUpload;
|
|
506
|
+
let mediaFilename = preloadedMedia?.fileName?.trim();
|
|
507
|
+
if (!upload) {
|
|
508
|
+
const media = preloadedMedia ?? await loadOutboundMediaFromUrl(params.mediaUrl, {
|
|
509
|
+
maxBytes: CLICKCLACK_MAX_UPLOAD_BYTES,
|
|
510
|
+
mediaAccess: params.mediaAccess,
|
|
511
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
512
|
+
mediaReadFile: params.mediaReadFile
|
|
513
|
+
});
|
|
514
|
+
const filename = media.fileName?.trim() || "attachment";
|
|
515
|
+
mediaFilename = filename;
|
|
516
|
+
const contentType = media.contentType?.trim() || "application/octet-stream";
|
|
517
|
+
await dispatch();
|
|
518
|
+
upload = await client.createUpload({
|
|
519
|
+
workspaceId,
|
|
520
|
+
buffer: media.buffer,
|
|
521
|
+
filename,
|
|
522
|
+
contentType,
|
|
523
|
+
...nonces.upload ? { nonce: nonces.upload } : {}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const text = sanitizeAssistantVisibleText(params.text) || mediaFilename || upload.filename || "attachment";
|
|
527
|
+
const message = await createTargetMessage({
|
|
528
|
+
client,
|
|
529
|
+
workspaceId,
|
|
530
|
+
to: params.to,
|
|
531
|
+
text,
|
|
532
|
+
threadId: params.threadId,
|
|
533
|
+
replyToId: params.replyToId,
|
|
534
|
+
nonce: nonces.message,
|
|
535
|
+
onPlatformSendDispatch: dispatch
|
|
536
|
+
});
|
|
537
|
+
await attachUploadRetrySafe({
|
|
538
|
+
client,
|
|
539
|
+
messageId: message.id,
|
|
540
|
+
uploadId: upload.id
|
|
541
|
+
});
|
|
542
|
+
return message.id;
|
|
543
|
+
}
|
|
544
|
+
function collectReconciliationMediaUrls(ctx) {
|
|
545
|
+
const planned = ctx.renderedBatchPlan?.items[0]?.mediaUrls;
|
|
546
|
+
if (planned?.length) return planned.map((url) => url.trim()).filter(Boolean);
|
|
547
|
+
const payload = ctx.payloads[0];
|
|
548
|
+
return [payload?.mediaUrl, ...payload?.mediaUrls ?? []].map((url) => url?.trim()).filter((url) => Boolean(url));
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Completes an unknown durable send through ClickClack's message/upload nonces.
|
|
552
|
+
* Media recovery never rereads the original source after restart.
|
|
553
|
+
*/
|
|
554
|
+
async function reconcileClickClackUnknownSend(ctx) {
|
|
555
|
+
if (ctx.payloads.length !== 1 || (ctx.renderedBatchPlan?.items.length ?? 1) !== 1) return {
|
|
556
|
+
status: "unresolved",
|
|
557
|
+
error: "ClickClack reconciliation requires exactly one payload"
|
|
558
|
+
};
|
|
559
|
+
const mediaUrls = collectReconciliationMediaUrls(ctx);
|
|
560
|
+
const { account, client } = createOutboundContext({
|
|
561
|
+
cfg: ctx.cfg,
|
|
562
|
+
accountId: ctx.accountId
|
|
563
|
+
});
|
|
564
|
+
const workspaceId = await resolveWorkspaceId(client, account.workspace);
|
|
565
|
+
const effectiveReplyToId = ctx.effectiveReplyToId !== void 0 ? ctx.effectiveReplyToId : ctx.replyToMode === "off" ? void 0 : ctx.replyToId;
|
|
566
|
+
const payload = ctx.payloads[0];
|
|
567
|
+
const caption = ctx.renderedBatchPlan?.items[0]?.text ?? payload?.text ?? "";
|
|
568
|
+
if (mediaUrls.length === 0) {
|
|
569
|
+
const nonce = textDeliveryNonce({
|
|
570
|
+
deliveryQueueId: ctx.queueId,
|
|
571
|
+
deliveryPartIndex: 0
|
|
572
|
+
});
|
|
573
|
+
if (!nonce || !sanitizeAssistantVisibleText(caption)) return { status: "not_sent" };
|
|
574
|
+
const message = await client.findMessageByNonce({
|
|
575
|
+
workspaceId,
|
|
576
|
+
nonce
|
|
577
|
+
});
|
|
578
|
+
if (!message) return { status: "not_sent" };
|
|
579
|
+
const receipt = createMessageReceiptFromOutboundResults({
|
|
580
|
+
results: [{
|
|
581
|
+
channel: "clickclack",
|
|
582
|
+
messageId: message.id
|
|
583
|
+
}],
|
|
584
|
+
threadId: ctx.threadId == null ? void 0 : String(ctx.threadId),
|
|
585
|
+
replyToId: effectiveReplyToId ?? void 0,
|
|
586
|
+
kind: "text"
|
|
587
|
+
});
|
|
635
588
|
return {
|
|
636
|
-
|
|
637
|
-
messageId: message.id
|
|
589
|
+
status: "sent",
|
|
590
|
+
messageId: message.id,
|
|
591
|
+
receipt
|
|
638
592
|
};
|
|
639
593
|
}
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
594
|
+
const parts = await Promise.all(mediaUrls.map(async (_mediaUrl, index) => {
|
|
595
|
+
const nonces = mediaDeliveryNonces({
|
|
596
|
+
deliveryQueueId: ctx.queueId,
|
|
597
|
+
deliveryPartIndex: index
|
|
598
|
+
});
|
|
599
|
+
if (!nonces.upload || !nonces.message) throw new Error("ClickClack durable media nonces were not derived");
|
|
600
|
+
const [upload, message] = await Promise.all([client.findUploadByNonce({
|
|
601
|
+
workspaceId,
|
|
602
|
+
nonce: nonces.upload
|
|
603
|
+
}), client.findMessageByNonce({
|
|
604
|
+
workspaceId,
|
|
605
|
+
nonce: nonces.message
|
|
606
|
+
})]);
|
|
643
607
|
return {
|
|
644
|
-
|
|
645
|
-
|
|
608
|
+
upload,
|
|
609
|
+
message
|
|
646
610
|
};
|
|
611
|
+
}));
|
|
612
|
+
for (const part of parts) {
|
|
613
|
+
if (part.message && !part.upload) return {
|
|
614
|
+
status: "unresolved",
|
|
615
|
+
error: `ClickClack message ${part.message.id} exists without its nonce-keyed upload`,
|
|
616
|
+
retryable: false
|
|
617
|
+
};
|
|
618
|
+
if (!part.message) return { status: "not_sent" };
|
|
647
619
|
}
|
|
648
|
-
const
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
620
|
+
const messageIds = [];
|
|
621
|
+
for (const part of parts) {
|
|
622
|
+
const message = part.message;
|
|
623
|
+
const upload = part.upload;
|
|
624
|
+
if (!message || !upload) throw new Error("ClickClack reconciliation state changed unexpectedly");
|
|
625
|
+
if (!message.attachments?.some((attachment) => attachment.id === upload.id)) await attachUploadRetrySafe({
|
|
626
|
+
client,
|
|
627
|
+
messageId: message.id,
|
|
628
|
+
uploadId: upload.id
|
|
629
|
+
});
|
|
630
|
+
messageIds.push(message.id);
|
|
631
|
+
}
|
|
632
|
+
const receipt = createMessageReceiptFromOutboundResults({
|
|
633
|
+
results: messageIds.map((messageId) => ({
|
|
634
|
+
channel: "clickclack",
|
|
635
|
+
messageId
|
|
636
|
+
})),
|
|
637
|
+
threadId: ctx.threadId == null ? void 0 : String(ctx.threadId),
|
|
638
|
+
replyToId: effectiveReplyToId ?? void 0,
|
|
639
|
+
kind: "media"
|
|
652
640
|
});
|
|
641
|
+
const messageId = messageIds.at(-1);
|
|
653
642
|
return {
|
|
654
|
-
|
|
655
|
-
messageId:
|
|
643
|
+
status: "sent",
|
|
644
|
+
...messageId ? { messageId } : {},
|
|
645
|
+
receipt
|
|
656
646
|
};
|
|
657
647
|
}
|
|
658
648
|
//#endregion
|
|
659
649
|
//#region extensions/clickclack/src/inbound.ts
|
|
660
650
|
const CHANNEL_ID$1 = "clickclack";
|
|
651
|
+
const CLICKCLACK_MESSAGE_ID_PATTERN = /^msg_[0-9a-hjkmnp-tv-z]{26}$/u;
|
|
652
|
+
function hasClickClackReplyMedia(payload) {
|
|
653
|
+
return Boolean(payload.mediaUrl?.trim() || payload.mediaUrls?.some((mediaUrl) => typeof mediaUrl === "string" && mediaUrl.trim()));
|
|
654
|
+
}
|
|
655
|
+
function resolveClickClackAgentRunId(messageId) {
|
|
656
|
+
return CLICKCLACK_MESSAGE_ID_PATTERN.test(messageId) ? `${CHANNEL_ID$1}:${messageId}` : void 0;
|
|
657
|
+
}
|
|
661
658
|
function resolveAccountAgentRoute(params) {
|
|
662
659
|
const runtime = getClickClackRuntime();
|
|
663
660
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
@@ -702,10 +699,10 @@ function resolveAccountAgentRoute(params) {
|
|
|
702
699
|
};
|
|
703
700
|
}
|
|
704
701
|
async function dispatchModelReply(params) {
|
|
705
|
-
const
|
|
702
|
+
const runtime = getClickClackRuntime();
|
|
703
|
+
const text = (await runtime.llm.complete({
|
|
706
704
|
agentId: params.route.agentId,
|
|
707
705
|
model: params.account.model,
|
|
708
|
-
maxTokens: 96,
|
|
709
706
|
purpose: "clickclack bot reply",
|
|
710
707
|
systemPrompt: params.account.systemPrompt,
|
|
711
708
|
messages: [{
|
|
@@ -713,14 +710,21 @@ async function dispatchModelReply(params) {
|
|
|
713
710
|
content: params.message.body
|
|
714
711
|
}]
|
|
715
712
|
})).text.trim();
|
|
716
|
-
if (!text)
|
|
713
|
+
if (!text) {
|
|
714
|
+
runtime.logging.getChildLogger({
|
|
715
|
+
plugin: "clickclack",
|
|
716
|
+
feature: "model-reply"
|
|
717
|
+
}).warn(`[${params.account.accountId}] ClickClack model reply produced no sendable text`);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
717
720
|
await sendClickClackText({
|
|
718
721
|
cfg: params.cfg,
|
|
719
722
|
accountId: params.account.accountId,
|
|
720
723
|
to: params.target,
|
|
721
724
|
text,
|
|
722
725
|
threadId: params.message.parent_message_id ? params.message.thread_root_id : void 0,
|
|
723
|
-
replyToId: params.message.id
|
|
726
|
+
replyToId: params.message.id,
|
|
727
|
+
correlationId: params.correlationId
|
|
724
728
|
});
|
|
725
729
|
}
|
|
726
730
|
/**
|
|
@@ -758,7 +762,8 @@ async function handleClickClackInbound(params) {
|
|
|
758
762
|
cfg: params.config,
|
|
759
763
|
message,
|
|
760
764
|
route,
|
|
761
|
-
target
|
|
765
|
+
target,
|
|
766
|
+
correlationId: params.correlationId
|
|
762
767
|
});
|
|
763
768
|
return;
|
|
764
769
|
}
|
|
@@ -767,7 +772,8 @@ async function handleClickClackInbound(params) {
|
|
|
767
772
|
if (params.account.agentActivity && (message.channel_id || message.direct_conversation_id)) activity = createClickClackActivityPublisher({
|
|
768
773
|
client: createClickClackClient({
|
|
769
774
|
baseUrl: params.account.baseUrl,
|
|
770
|
-
token: params.account.token
|
|
775
|
+
token: params.account.token,
|
|
776
|
+
correlationId: params.correlationId
|
|
771
777
|
}),
|
|
772
778
|
target: message.channel_id ? { channelId: message.channel_id } : { conversationId: message.direct_conversation_id },
|
|
773
779
|
turnId: message.id,
|
|
@@ -820,6 +826,20 @@ async function handleClickClackInbound(params) {
|
|
|
820
826
|
OriginatingTo: target,
|
|
821
827
|
CommandAuthorized: access.commandAuthorized
|
|
822
828
|
});
|
|
829
|
+
const runId = resolveClickClackAgentRunId(message.id);
|
|
830
|
+
const activityReplyOptions = activity ? {
|
|
831
|
+
onModelSelected: (ctx) => {
|
|
832
|
+
turnProvenance = {
|
|
833
|
+
model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
|
|
834
|
+
thinking: ctx.thinkLevel
|
|
835
|
+
};
|
|
836
|
+
activity?.setProvenance(turnProvenance);
|
|
837
|
+
},
|
|
838
|
+
onItemEvent: activity.onItemEvent,
|
|
839
|
+
commentaryProgressEnabled: true,
|
|
840
|
+
suppressDefaultToolProgressMessages: true,
|
|
841
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: true
|
|
842
|
+
} : void 0;
|
|
823
843
|
const dispatchPromise = runtime.channel.inbound.dispatchReply({
|
|
824
844
|
cfg: params.config,
|
|
825
845
|
channel: CHANNEL_ID$1,
|
|
@@ -831,21 +851,13 @@ async function handleClickClackInbound(params) {
|
|
|
831
851
|
recordInboundSession: runtime.channel.session.recordInboundSession,
|
|
832
852
|
dispatchReplyWithBufferedBlockDispatcher: runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
833
853
|
toolsAllow: params.account.toolsAllow,
|
|
834
|
-
replyOptions:
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
|
|
838
|
-
thinking: ctx.thinkLevel
|
|
839
|
-
};
|
|
840
|
-
activity?.setProvenance(turnProvenance);
|
|
841
|
-
},
|
|
842
|
-
onItemEvent: activity.onItemEvent,
|
|
843
|
-
commentaryProgressEnabled: true,
|
|
844
|
-
suppressDefaultToolProgressMessages: true,
|
|
845
|
-
allowProgressCallbacksWhenSourceDeliverySuppressed: true
|
|
854
|
+
replyOptions: runId || activityReplyOptions ? {
|
|
855
|
+
...runId ? { runId } : {},
|
|
856
|
+
...activityReplyOptions
|
|
846
857
|
} : void 0,
|
|
847
858
|
delivery: {
|
|
848
859
|
deliver: async (payload) => {
|
|
860
|
+
if (hasClickClackReplyMedia(payload)) throw new Error("ClickClack media reply requires durable delivery");
|
|
849
861
|
const text = payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "";
|
|
850
862
|
if (!text.trim()) return;
|
|
851
863
|
await sendClickClackText({
|
|
@@ -855,9 +867,25 @@ async function handleClickClackInbound(params) {
|
|
|
855
867
|
text,
|
|
856
868
|
threadId: message.parent_message_id ? message.thread_root_id : void 0,
|
|
857
869
|
replyToId: message.id,
|
|
858
|
-
provenance: turnProvenance
|
|
870
|
+
provenance: turnProvenance,
|
|
871
|
+
correlationId: params.correlationId
|
|
859
872
|
});
|
|
860
873
|
},
|
|
874
|
+
durable: (payload) => {
|
|
875
|
+
if (!hasClickClackReplyMedia(payload)) return false;
|
|
876
|
+
const threadId = message.parent_message_id ? message.thread_root_id : void 0;
|
|
877
|
+
return {
|
|
878
|
+
to: target,
|
|
879
|
+
threadId,
|
|
880
|
+
replyToId: message.id,
|
|
881
|
+
requiredCapabilities: deriveDurableFinalDeliveryRequirements({
|
|
882
|
+
payload,
|
|
883
|
+
threadId,
|
|
884
|
+
replyToId: message.id,
|
|
885
|
+
reconcileUnknownSend: true
|
|
886
|
+
})
|
|
887
|
+
};
|
|
888
|
+
},
|
|
861
889
|
onError: (error) => {
|
|
862
890
|
throw error instanceof Error ? error : /* @__PURE__ */ new Error(`clickclack dispatch failed: ${String(error)}`);
|
|
863
891
|
}
|
|
@@ -875,10 +903,14 @@ async function handleClickClackInbound(params) {
|
|
|
875
903
|
}
|
|
876
904
|
//#endregion
|
|
877
905
|
//#region extensions/clickclack/src/gateway.ts
|
|
906
|
+
const CLICKCLACK_EVENT_PAGE_LIMIT = 500;
|
|
878
907
|
function payloadString(event, key) {
|
|
879
908
|
const value = event.payload?.[key];
|
|
880
909
|
return typeof value === "string" ? value : "";
|
|
881
910
|
}
|
|
911
|
+
function eventCorrelationId(event) {
|
|
912
|
+
return normalizeClickClackCorrelationId(event.payload?.correlation_id);
|
|
913
|
+
}
|
|
882
914
|
async function resolveEventMessage(params) {
|
|
883
915
|
const messageId = payloadString(params.event, "message_id");
|
|
884
916
|
if (!messageId) return null;
|
|
@@ -908,8 +940,13 @@ function parseSocketEvent(data) {
|
|
|
908
940
|
async function processEvent(params) {
|
|
909
941
|
if (params.event.type !== "message.created" && params.event.type !== "thread.reply_created") return;
|
|
910
942
|
if (payloadString(params.event, "author_id") === params.botUserId) return;
|
|
943
|
+
const correlationId = eventCorrelationId(params.event);
|
|
911
944
|
const message = await resolveEventMessage({
|
|
912
|
-
client:
|
|
945
|
+
client: correlationId ? createClickClackClient({
|
|
946
|
+
baseUrl: params.account.baseUrl,
|
|
947
|
+
token: params.account.token,
|
|
948
|
+
correlationId
|
|
949
|
+
}) : params.client,
|
|
913
950
|
event: params.event
|
|
914
951
|
});
|
|
915
952
|
if (!message || message.author_id === params.botUserId) return;
|
|
@@ -924,9 +961,27 @@ async function processEvent(params) {
|
|
|
924
961
|
account: params.account,
|
|
925
962
|
config: params.config,
|
|
926
963
|
message,
|
|
927
|
-
access
|
|
964
|
+
access,
|
|
965
|
+
...correlationId ? { correlationId } : {}
|
|
928
966
|
});
|
|
929
967
|
}
|
|
968
|
+
async function drainEventBacklog(params) {
|
|
969
|
+
let afterCursor = params.afterCursor;
|
|
970
|
+
while (!params.abortSignal.aborted) {
|
|
971
|
+
const events = (await params.client.eventPage(params.workspaceId, {
|
|
972
|
+
afterCursor,
|
|
973
|
+
limit: CLICKCLACK_EVENT_PAGE_LIMIT
|
|
974
|
+
})).events;
|
|
975
|
+
for (const event of events) {
|
|
976
|
+
if (params.abortSignal.aborted) return afterCursor;
|
|
977
|
+
if (!event.cursor || event.cursor === afterCursor) throw new Error("ClickClack event backlog returned a non-advancing cursor");
|
|
978
|
+
await params.onEvent(event);
|
|
979
|
+
afterCursor = event.cursor;
|
|
980
|
+
}
|
|
981
|
+
if (events.length === 0) return afterCursor;
|
|
982
|
+
}
|
|
983
|
+
return afterCursor;
|
|
984
|
+
}
|
|
930
985
|
async function startClickClackGatewayAccount(ctx) {
|
|
931
986
|
const configuredAccount = resolveClickClackAccount({
|
|
932
987
|
cfg: ctx.cfg,
|
|
@@ -944,6 +999,18 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
944
999
|
workspace: workspaceId,
|
|
945
1000
|
botUserId: configuredAccount.botUserId ?? me.id
|
|
946
1001
|
};
|
|
1002
|
+
const processIncomingEvent = (event) => processEvent({
|
|
1003
|
+
account,
|
|
1004
|
+
config: ctx.cfg,
|
|
1005
|
+
client,
|
|
1006
|
+
event,
|
|
1007
|
+
botUserId: account.botUserId
|
|
1008
|
+
});
|
|
1009
|
+
if (account.commandMenu) await syncClickClackCommandMenu({
|
|
1010
|
+
cfg: ctx.cfg,
|
|
1011
|
+
client,
|
|
1012
|
+
log: ctx.log
|
|
1013
|
+
});
|
|
947
1014
|
ctx.setStatus({
|
|
948
1015
|
accountId: account.accountId,
|
|
949
1016
|
running: true,
|
|
@@ -955,23 +1022,25 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
955
1022
|
let initialized = false;
|
|
956
1023
|
try {
|
|
957
1024
|
while (!ctx.abortSignal.aborted) {
|
|
958
|
-
const backlog = await client.events(workspaceId, afterCursor);
|
|
959
1025
|
if (!initialized) {
|
|
960
|
-
|
|
1026
|
+
const page = await client.eventPage(workspaceId, { includeTail: true });
|
|
1027
|
+
if (page.tailCursor !== void 0) afterCursor = page.tailCursor;
|
|
1028
|
+
else for (const event of page.events) afterCursor = event.cursor || afterCursor;
|
|
961
1029
|
initialized = true;
|
|
962
|
-
} else
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
});
|
|
971
|
-
}
|
|
1030
|
+
} else afterCursor = await drainEventBacklog({
|
|
1031
|
+
client,
|
|
1032
|
+
workspaceId,
|
|
1033
|
+
afterCursor,
|
|
1034
|
+
abortSignal: ctx.abortSignal,
|
|
1035
|
+
onEvent: processIncomingEvent
|
|
1036
|
+
});
|
|
1037
|
+
if (ctx.abortSignal.aborted) break;
|
|
972
1038
|
const socket = client.websocket(workspaceId, afterCursor);
|
|
973
|
-
await new Promise((resolve
|
|
1039
|
+
await new Promise((resolve) => {
|
|
974
1040
|
let settled = false;
|
|
1041
|
+
let closing = false;
|
|
1042
|
+
let loggedMessageFailure = false;
|
|
1043
|
+
let messageQueue = Promise.resolve();
|
|
975
1044
|
let removeAbortListener;
|
|
976
1045
|
const finishSocketCycle = () => {
|
|
977
1046
|
if (settled) return;
|
|
@@ -980,6 +1049,20 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
980
1049
|
removeAbortListener = void 0;
|
|
981
1050
|
resolve();
|
|
982
1051
|
};
|
|
1052
|
+
const finishAfterQueuedMessages = () => {
|
|
1053
|
+
messageQueue.then(() => finishSocketCycle(), () => finishSocketCycle());
|
|
1054
|
+
};
|
|
1055
|
+
const reconnectAfterMessageFailure = (error) => {
|
|
1056
|
+
if (settled || ctx.abortSignal.aborted) return;
|
|
1057
|
+
if (!loggedMessageFailure) {
|
|
1058
|
+
loggedMessageFailure = true;
|
|
1059
|
+
ctx.log?.warn?.(`[${account.accountId}] ClickClack event processing failed; reconnecting: ${error instanceof Error ? error.message : formatErrorMessage(error)}`);
|
|
1060
|
+
}
|
|
1061
|
+
if (!closing) {
|
|
1062
|
+
closing = true;
|
|
1063
|
+
socket.close();
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
983
1066
|
const abort = () => {
|
|
984
1067
|
socket.close();
|
|
985
1068
|
finishSocketCycle();
|
|
@@ -987,36 +1070,38 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
987
1070
|
ctx.abortSignal.addEventListener("abort", abort, { once: true });
|
|
988
1071
|
removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
|
|
989
1072
|
socket.on("message", (data) => {
|
|
990
|
-
(
|
|
1073
|
+
if (closing || settled) return;
|
|
1074
|
+
messageQueue = messageQueue.then(async () => {
|
|
991
1075
|
const event = parseSocketEvent(data);
|
|
992
1076
|
if (!event) {
|
|
993
1077
|
ctx.log?.warn?.(`[${account.accountId}] skipped malformed ClickClack websocket event`);
|
|
994
1078
|
return;
|
|
995
1079
|
}
|
|
1080
|
+
await processIncomingEvent(event);
|
|
996
1081
|
afterCursor = event.cursor || afterCursor;
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
});
|
|
1004
|
-
})().catch((e) => reject(e instanceof Error ? e : new Error(`ClickClack ws message failed: ${formatErrorMessage(e)}`, { cause: e })));
|
|
1082
|
+
});
|
|
1083
|
+
messageQueue.catch(reconnectAfterMessageFailure);
|
|
1084
|
+
});
|
|
1085
|
+
socket.on("close", () => {
|
|
1086
|
+
closing = true;
|
|
1087
|
+
finishAfterQueuedMessages();
|
|
1005
1088
|
});
|
|
1006
|
-
socket.on("close", finishSocketCycle);
|
|
1007
1089
|
socket.on("error", (error) => {
|
|
1008
1090
|
if (settled || ctx.abortSignal.aborted) {
|
|
1009
1091
|
finishSocketCycle();
|
|
1010
1092
|
return;
|
|
1011
1093
|
}
|
|
1094
|
+
if (closing) return;
|
|
1012
1095
|
ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
|
|
1013
|
-
|
|
1096
|
+
closing = true;
|
|
1014
1097
|
socket.close();
|
|
1015
1098
|
});
|
|
1016
1099
|
});
|
|
1017
|
-
if (!ctx.abortSignal.aborted)
|
|
1018
|
-
|
|
1019
|
-
})
|
|
1100
|
+
if (!ctx.abortSignal.aborted) try {
|
|
1101
|
+
await sleepWithAbort(account.reconnectMs, ctx.abortSignal);
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
if (!ctx.abortSignal.aborted) throw error;
|
|
1104
|
+
}
|
|
1020
1105
|
}
|
|
1021
1106
|
} finally {
|
|
1022
1107
|
ctx.setStatus({
|
|
@@ -1031,40 +1116,84 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1031
1116
|
* ClickClack channel plugin definition: target parsing, account config, status,
|
|
1032
1117
|
* gateway startup, and outbound delivery wiring.
|
|
1033
1118
|
*/
|
|
1034
|
-
const CHANNEL_ID =
|
|
1035
|
-
const meta = { ...getChatChannelMeta(CHANNEL_ID) };
|
|
1119
|
+
const CHANNEL_ID = CLICKCLACK_CHANNEL_ID;
|
|
1036
1120
|
const clickClackMessageAdapter = defineChannelMessageAdapter({
|
|
1037
1121
|
id: CHANNEL_ID,
|
|
1038
|
-
durableFinal: {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
})
|
|
1066
|
-
|
|
1067
|
-
|
|
1122
|
+
durableFinal: {
|
|
1123
|
+
capabilities: {
|
|
1124
|
+
text: true,
|
|
1125
|
+
media: true,
|
|
1126
|
+
replyTo: true,
|
|
1127
|
+
thread: true,
|
|
1128
|
+
messageSendingHooks: true,
|
|
1129
|
+
reconcileUnknownSend: true
|
|
1130
|
+
},
|
|
1131
|
+
reconcileUnknownSendKinds: {
|
|
1132
|
+
text: true,
|
|
1133
|
+
media: true
|
|
1134
|
+
},
|
|
1135
|
+
reconcileUnknownSend: reconcileClickClackUnknownSend
|
|
1136
|
+
},
|
|
1137
|
+
send: {
|
|
1138
|
+
text: async (ctx) => {
|
|
1139
|
+
const messageId = await sendClickClackText({
|
|
1140
|
+
cfg: ctx.cfg,
|
|
1141
|
+
accountId: ctx.accountId,
|
|
1142
|
+
to: ctx.to,
|
|
1143
|
+
text: ctx.text,
|
|
1144
|
+
threadId: ctx.threadId,
|
|
1145
|
+
replyToId: ctx.replyToId,
|
|
1146
|
+
deliveryQueueId: ctx.deliveryQueueId,
|
|
1147
|
+
deliveryPartIndex: ctx.deliveryPartIndex,
|
|
1148
|
+
onPlatformSendDispatch: ctx.onPlatformSendDispatch
|
|
1149
|
+
});
|
|
1150
|
+
const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
|
|
1151
|
+
const replyToId = ctx.replyToId ?? void 0;
|
|
1152
|
+
return {
|
|
1153
|
+
...messageId ? { messageId } : {},
|
|
1154
|
+
receipt: createMessageReceiptFromOutboundResults({
|
|
1155
|
+
results: messageId ? [{
|
|
1156
|
+
channel: CHANNEL_ID,
|
|
1157
|
+
messageId
|
|
1158
|
+
}] : [],
|
|
1159
|
+
threadId,
|
|
1160
|
+
replyToId,
|
|
1161
|
+
kind: "text"
|
|
1162
|
+
})
|
|
1163
|
+
};
|
|
1164
|
+
},
|
|
1165
|
+
media: async (ctx) => {
|
|
1166
|
+
const messageId = await sendClickClackMedia({
|
|
1167
|
+
cfg: ctx.cfg,
|
|
1168
|
+
accountId: ctx.accountId,
|
|
1169
|
+
to: ctx.to,
|
|
1170
|
+
text: ctx.text,
|
|
1171
|
+
mediaUrl: ctx.mediaUrl,
|
|
1172
|
+
mediaAccess: ctx.mediaAccess,
|
|
1173
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
1174
|
+
mediaReadFile: ctx.mediaReadFile,
|
|
1175
|
+
threadId: ctx.threadId,
|
|
1176
|
+
replyToId: ctx.replyToId,
|
|
1177
|
+
deliveryQueueId: ctx.deliveryQueueId,
|
|
1178
|
+
deliveryPartIndex: ctx.deliveryPartIndex,
|
|
1179
|
+
onPlatformSendDispatch: ctx.onPlatformSendDispatch
|
|
1180
|
+
});
|
|
1181
|
+
const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
|
|
1182
|
+
const replyToId = ctx.replyToId ?? void 0;
|
|
1183
|
+
return {
|
|
1184
|
+
messageId,
|
|
1185
|
+
receipt: createMessageReceiptFromOutboundResults({
|
|
1186
|
+
results: [{
|
|
1187
|
+
channel: CHANNEL_ID,
|
|
1188
|
+
messageId
|
|
1189
|
+
}],
|
|
1190
|
+
threadId,
|
|
1191
|
+
replyToId,
|
|
1192
|
+
kind: "media"
|
|
1193
|
+
})
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1068
1197
|
});
|
|
1069
1198
|
/**
|
|
1070
1199
|
* Channel plugin instance registered by the bundled ClickClack entry.
|
|
@@ -1072,7 +1201,7 @@ const clickClackMessageAdapter = defineChannelMessageAdapter({
|
|
|
1072
1201
|
const clickClackPlugin = createChatChannelPlugin({
|
|
1073
1202
|
base: {
|
|
1074
1203
|
id: CHANNEL_ID,
|
|
1075
|
-
meta,
|
|
1204
|
+
meta: clickClackMeta,
|
|
1076
1205
|
capabilities: {
|
|
1077
1206
|
chatTypes: ["direct", "group"],
|
|
1078
1207
|
threads: true,
|
|
@@ -1080,22 +1209,12 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1080
1209
|
},
|
|
1081
1210
|
reload: { configPrefixes: ["channels.clickclack"] },
|
|
1082
1211
|
configSchema: clickClackConfigSchema,
|
|
1083
|
-
config:
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
defaultAccountId: (cfg) => resolveDefaultClickClackAccountId(cfg),
|
|
1090
|
-
isConfigured: (account) => account.configured,
|
|
1091
|
-
resolveAllowFrom: ({ cfg, accountId }) => resolveClickClackAccount({
|
|
1092
|
-
cfg,
|
|
1093
|
-
accountId
|
|
1094
|
-
}).allowFrom,
|
|
1095
|
-
resolveDefaultTo: ({ cfg, accountId }) => resolveClickClackAccount({
|
|
1096
|
-
cfg,
|
|
1097
|
-
accountId
|
|
1098
|
-
}).defaultTo
|
|
1212
|
+
config: clickClackConfigAdapter,
|
|
1213
|
+
setup: clickClackSetupAdapter,
|
|
1214
|
+
setupWizard: clickClackSetupWizard,
|
|
1215
|
+
secrets: {
|
|
1216
|
+
secretTargetRegistryEntries,
|
|
1217
|
+
collectRuntimeConfigAssignments
|
|
1099
1218
|
},
|
|
1100
1219
|
messaging: {
|
|
1101
1220
|
targetPrefixes: ["clickclack", "cc"],
|
|
@@ -1162,16 +1281,39 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1162
1281
|
base: { deliveryMode: "direct" },
|
|
1163
1282
|
attachedResults: {
|
|
1164
1283
|
channel: CHANNEL_ID,
|
|
1165
|
-
sendText: async ({ cfg, to, text, accountId, threadId, replyToId }) =>
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1284
|
+
sendText: async ({ cfg, to, text, accountId, threadId, replyToId, deliveryQueueId, deliveryPartIndex, onPlatformSendDispatch }) => {
|
|
1285
|
+
return { messageId: await sendClickClackText({
|
|
1286
|
+
cfg,
|
|
1287
|
+
accountId,
|
|
1288
|
+
to,
|
|
1289
|
+
text,
|
|
1290
|
+
threadId,
|
|
1291
|
+
replyToId,
|
|
1292
|
+
deliveryQueueId,
|
|
1293
|
+
deliveryPartIndex,
|
|
1294
|
+
onPlatformSendDispatch
|
|
1295
|
+
}) ?? "" };
|
|
1296
|
+
},
|
|
1297
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile, accountId, threadId, replyToId, deliveryQueueId, deliveryPartIndex, onPlatformSendDispatch }) => {
|
|
1298
|
+
if (!mediaUrl) throw new Error("ClickClack media send requires mediaUrl");
|
|
1299
|
+
return { messageId: await sendClickClackMedia({
|
|
1300
|
+
cfg,
|
|
1301
|
+
accountId,
|
|
1302
|
+
to,
|
|
1303
|
+
text,
|
|
1304
|
+
mediaUrl,
|
|
1305
|
+
mediaAccess,
|
|
1306
|
+
mediaLocalRoots,
|
|
1307
|
+
mediaReadFile,
|
|
1308
|
+
threadId,
|
|
1309
|
+
replyToId,
|
|
1310
|
+
deliveryQueueId,
|
|
1311
|
+
deliveryPartIndex,
|
|
1312
|
+
onPlatformSendDispatch
|
|
1313
|
+
}) };
|
|
1314
|
+
}
|
|
1173
1315
|
}
|
|
1174
1316
|
}
|
|
1175
1317
|
});
|
|
1176
1318
|
//#endregion
|
|
1177
|
-
export {
|
|
1319
|
+
export { setClickClackRuntime as a, getClickClackRuntime as i, buildClickClackTarget as n, parseClickClackTarget as r, clickClackPlugin as t };
|