@openclaw/clickclack 2026.7.2-beta.3 → 2026.7.2-beta.5
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 +125 -0
- package/dist/api.js +2 -2
- package/dist/{channel-CEYthZ9N.js → channel-DfcFlLjC.js} +397 -13
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +48 -0
- package/dist/index.js +3 -1
- package/dist/runtime-api-D1H9vheM.js +1029 -0
- package/dist/runtime-api.js +4 -4
- package/dist/{setup-claim-BUWvZjtu.js → setup-claim-BFU16zoo.js} +33 -11
- package/dist/setup-plugin-api.js +2 -1
- package/dist/{setup-surface-CNjp76Xx.js → setup-surface-CD6MUC1g.js} +286 -89
- package/node_modules/ws/lib/receiver.js +15 -32
- package/node_modules/ws/lib/websocket-server.js +4 -4
- package/node_modules/ws/lib/websocket.js +4 -4
- package/node_modules/ws/package.json +5 -1
- package/openclaw.plugin.json +49 -13
- package/package.json +91 -16
- package/npm-shrinkwrap.json +0 -54
package/README.md
CHANGED
|
@@ -10,6 +10,18 @@ openclaw plugins install @openclaw/clickclack
|
|
|
10
10
|
|
|
11
11
|
## Setup
|
|
12
12
|
|
|
13
|
+
The recommended setup path uses the one-time command generated by ClickClack:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
openclaw channels add clickclack --code 'https://clickclack.example.com/#XXXX-XXXX-XXXX'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Split-origin and path-mounted ClickClack deployments generate an exact
|
|
20
|
+
`/api/bot-setup-codes/claim#CODE` endpoint. OpenClaw validates the versioned
|
|
21
|
+
claim response and saves its canonical API base.
|
|
22
|
+
|
|
23
|
+
For manual token setup:
|
|
24
|
+
|
|
13
25
|
```sh
|
|
14
26
|
openclaw channels add clickclack \
|
|
15
27
|
--base-url https://clickclack.example.com \
|
|
@@ -24,6 +36,31 @@ Run `openclaw onboard` for guided setup. The workspace value can be a
|
|
|
24
36
|
For the default account only, `--use-env` reads `CLICKCLACK_BOT_TOKEN`; config
|
|
25
37
|
storage is the normal setup path.
|
|
26
38
|
|
|
39
|
+
### Public and private endpoints
|
|
40
|
+
|
|
41
|
+
`baseUrl` is the public ClickClack URL used in links that people open. Set the
|
|
42
|
+
optional `apiBaseUrl` when the gateway should call ClickClack through a
|
|
43
|
+
different server-to-server endpoint:
|
|
44
|
+
|
|
45
|
+
```json5
|
|
46
|
+
{
|
|
47
|
+
channels: {
|
|
48
|
+
clickclack: {
|
|
49
|
+
baseUrl: "https://clack.openclaw.ai",
|
|
50
|
+
apiBaseUrl: "http://127.0.0.1:8484",
|
|
51
|
+
token: { source: "env", provider: "default", id: "CLICKCLACK_BOT_TOKEN" },
|
|
52
|
+
workspace: "default",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This same-host pattern lets the public hostname stay fully protected by an
|
|
59
|
+
authentication gateway such as Cloudflare Access while the local OpenClaw
|
|
60
|
+
gateway talks directly to ClickClack. REST requests, setup verification, and
|
|
61
|
+
the realtime WebSocket use `apiBaseUrl`; browser-facing discussion links keep
|
|
62
|
+
using `baseUrl`. When `apiBaseUrl` is unset, it defaults to `baseUrl`.
|
|
63
|
+
|
|
27
64
|
## Command menus
|
|
28
65
|
|
|
29
66
|
ClickClack command menus are enabled by default. At gateway startup, the
|
|
@@ -35,6 +72,94 @@ Set `commandMenu: false` on an account to disable menu sync. Sync failures do
|
|
|
35
72
|
not prevent the gateway from starting, so older tokens and ClickClack servers
|
|
36
73
|
continue to work without a menu.
|
|
37
74
|
|
|
75
|
+
## Discussions
|
|
76
|
+
|
|
77
|
+
ClickClack can create one managed channel for each OpenClaw session:
|
|
78
|
+
|
|
79
|
+
The account token needs `channels:write`, which is included in `bot:admin` but
|
|
80
|
+
not in the normal `bot:write` setup token. The ClickClack server must also
|
|
81
|
+
support and return the managed-channel fields used by this integration.
|
|
82
|
+
|
|
83
|
+
```json5
|
|
84
|
+
{
|
|
85
|
+
channels: {
|
|
86
|
+
clickclack: {
|
|
87
|
+
baseUrl: "https://clickclack.example.com",
|
|
88
|
+
token: { source: "env", provider: "default", id: "CLICKCLACK_BOT_TOKEN" },
|
|
89
|
+
workspace: "default",
|
|
90
|
+
discussions: {
|
|
91
|
+
enabled: true,
|
|
92
|
+
workspace: "default",
|
|
93
|
+
controlUrlBase: "https://team.openclaw.ai",
|
|
94
|
+
section: "Sessions",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Opening a session discussion creates a public, externally managed channel and
|
|
102
|
+
stores its binding in the ClickClack plugin's SQLite state. Session archive,
|
|
103
|
+
restore, label, category, and deletion changes are reflected in the channel;
|
|
104
|
+
deletion archives the channel and never deletes its messages. `workspace`
|
|
105
|
+
defaults to the account workspace, and `section` defaults to `Sessions`.
|
|
106
|
+
`controlUrlBase` adds a link back to `/chat?session=<session-key>` in the
|
|
107
|
+
OpenClaw Control UI.
|
|
108
|
+
|
|
109
|
+
ClickClack-managed embed URLs explicitly advertise host-theme support. The
|
|
110
|
+
Control UI uses that provider-owned capability to apply its full palette before
|
|
111
|
+
the sidebar first paints and to stream live palette changes without rewriting
|
|
112
|
+
opaque or signed discussion URLs from other providers.
|
|
113
|
+
|
|
114
|
+
Enable discussions on exactly one ClickClack account. Multiple enabled
|
|
115
|
+
discussion accounts are rejected because the session discussion provider does
|
|
116
|
+
not have an account selector.
|
|
117
|
+
|
|
118
|
+
Messages in the managed channel run in a stable side session under the same
|
|
119
|
+
agent id as the attached main session. The plugin installs a scoped host grant
|
|
120
|
+
for `sessions_history`, `session_status`, and `sessions_send` between that side
|
|
121
|
+
session and its attached main session, so `tools.sessions.visibility` can stay
|
|
122
|
+
at its safer default `tree`. A second host-side policy blocks session discovery
|
|
123
|
+
and alternate targets; the side-agent prompt is not the authorization boundary.
|
|
124
|
+
The agent still needs those three tools in its effective tool allowlist.
|
|
125
|
+
|
|
126
|
+
The binding, managed ownership reference, and side-session identity include the
|
|
127
|
+
concrete OpenClaw session id as well as the pinned server and channel. Resetting
|
|
128
|
+
a reusable session key, replacing a binding, or retargeting it therefore revokes
|
|
129
|
+
the old channel locally, archives it when the old credential remains usable, and
|
|
130
|
+
starts a fresh channel and side transcript.
|
|
131
|
+
Messages arriving through an archived, reset, disabled, or retargeted managed
|
|
132
|
+
binding are dropped instead of falling back to the account's normal channel
|
|
133
|
+
routing. Released bindings leave a durable revoked-channel marker so delayed
|
|
134
|
+
realtime events remain fail-closed. Remote ownership is keyed by ClickClack
|
|
135
|
+
server and channel id, so renaming the local account cannot turn a managed
|
|
136
|
+
channel into an ordinary one.
|
|
137
|
+
|
|
138
|
+
Managed-channel ownership references include a durable per-installation id, so
|
|
139
|
+
two OpenClaw gateways using the same ClickClack workspace do not adopt each
|
|
140
|
+
other's discussion channels. They also include the destination and a durable
|
|
141
|
+
binding generation, so an account or workspace round trip cannot re-adopt a
|
|
142
|
+
previous channel. Changing or removing `controlUrlBase` is reflected on the next
|
|
143
|
+
lifecycle reconciliation pass.
|
|
144
|
+
|
|
145
|
+
If a channel-create response is lost, the pending ownership reservation
|
|
146
|
+
temporarily quarantines otherwise-unbound events in that workspace. The same
|
|
147
|
+
coarse reconciler then adopts the created channel or clears/archives the
|
|
148
|
+
ambiguous attempt; a reset cannot make the old channel fall through to ordinary
|
|
149
|
+
routing.
|
|
150
|
+
|
|
151
|
+
When a workspace move keeps the original workspace credential configured, the
|
|
152
|
+
plugin archives the old channel before release. If the token is replaced with a
|
|
153
|
+
workspace-scoped credential that cannot access the old workspace, OpenClaw
|
|
154
|
+
releases the binding into the revoked-channel marker without trying the new token
|
|
155
|
+
against the old channel; archive that leftover channel from ClickClack.
|
|
156
|
+
|
|
157
|
+
The main session gets a read-only `discussion` tool that pulls the latest
|
|
158
|
+
channel messages, including recent thread replies. The pull uses bounded
|
|
159
|
+
history and thread-request budgets; its output says when older active threads
|
|
160
|
+
may have been omitted. It never posts, archives, renames, or otherwise mutates
|
|
161
|
+
the discussion.
|
|
162
|
+
|
|
38
163
|
## Docs
|
|
39
164
|
|
|
40
165
|
See `docs/channels/clickclack.md` in the OpenClaw repository, or the published docs at `https://docs.openclaw.ai/channels/clickclack`.
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { S as resolveDefaultClickClackAccountId, b as listEnabledClickClackAccounts, d as createClickClackClient, m as clickClackConfigSchema, v as DEFAULT_ACCOUNT_ID, x as resolveClickClackAccount, y as listClickClackAccountIds } from "./setup-surface-CD6MUC1g.js";
|
|
2
|
+
import { D as setClickClackRuntime, E as getClickClackRuntime, n as buildClickClackTarget, r as parseClickClackTarget, t as clickClackPlugin } from "./channel-DfcFlLjC.js";
|
|
3
3
|
export { DEFAULT_ACCOUNT_ID, buildClickClackTarget, clickClackConfigSchema, clickClackPlugin, createClickClackClient, getClickClackRuntime, listClickClackAccountIds, listEnabledClickClackAccounts, parseClickClackTarget, resolveClickClackAccount, resolveDefaultClickClackAccountId, setClickClackRuntime };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as clickClackMeta, b as listEnabledClickClackAccounts, c as resolveChannelId, d as createClickClackClient, g as clickClackConfigAdapter, h as CLICKCLACK_CHANNEL_ID, l as resolveWorkspaceId, m as clickClackConfigSchema, n as clickClackSetupAdapter, p as normalizeClickClackCorrelationId, r as clickClackSetupContract, t as clickClackSetupWizard, v as DEFAULT_ACCOUNT_ID, x as resolveClickClackAccount } from "./setup-surface-CD6MUC1g.js";
|
|
2
2
|
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BKkzzjBA.js";
|
|
3
3
|
import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
4
4
|
import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-outbound";
|
|
@@ -10,9 +10,9 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
|
10
10
|
import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
|
|
11
11
|
import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel-inbound";
|
|
12
12
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
13
|
-
import { createHash } from "node:crypto";
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
14
|
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
|
15
|
-
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
15
|
+
import { FormatCapabilityProfile, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
16
16
|
//#region extensions/clickclack/src/runtime.ts
|
|
17
17
|
/**
|
|
18
18
|
* Runtime store for host-provided OpenClaw services used by the ClickClack
|
|
@@ -328,6 +328,353 @@ function createClickClackActivityPublisher(params) {
|
|
|
328
328
|
};
|
|
329
329
|
}
|
|
330
330
|
//#endregion
|
|
331
|
+
//#region extensions/clickclack/src/discussions/binding-generation.ts
|
|
332
|
+
const DISCUSSION_GENERATIONS_NAMESPACE = "discussion-binding-generations";
|
|
333
|
+
const MAX_PENDING_DISCUSSION_GENERATIONS = 1e4;
|
|
334
|
+
const storesByRuntime$2 = /* @__PURE__ */ new WeakMap();
|
|
335
|
+
function getGenerationStore(runtime) {
|
|
336
|
+
const existing = storesByRuntime$2.get(runtime);
|
|
337
|
+
if (existing) return existing;
|
|
338
|
+
const created = runtime.state.openSyncKeyedStore({
|
|
339
|
+
namespace: DISCUSSION_GENERATIONS_NAMESPACE,
|
|
340
|
+
maxEntries: MAX_PENDING_DISCUSSION_GENERATIONS,
|
|
341
|
+
overflowPolicy: "reject-new"
|
|
342
|
+
});
|
|
343
|
+
storesByRuntime$2.set(runtime, created);
|
|
344
|
+
return created;
|
|
345
|
+
}
|
|
346
|
+
/** Reserves a generation so an interrupted channel create can be adopted on retry. */
|
|
347
|
+
function reserveDiscussionBindingGeneration(params) {
|
|
348
|
+
const store = getGenerationStore(params.runtime);
|
|
349
|
+
const existing = store.lookup(params.sessionKey);
|
|
350
|
+
if (existing?.destinationIdentity === params.destinationIdentity) return existing.generation;
|
|
351
|
+
const generation = (params.createGeneration ?? randomUUID)();
|
|
352
|
+
store.register(params.sessionKey, {
|
|
353
|
+
destinationIdentity: params.destinationIdentity,
|
|
354
|
+
generation
|
|
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);
|
|
365
|
+
}
|
|
366
|
+
/** Quarantines a destination before the first fallible channel create. */
|
|
367
|
+
function recordPendingDiscussionOpen(params) {
|
|
368
|
+
const store = getGenerationStore(params.runtime);
|
|
369
|
+
const existing = store.lookup(params.sessionKey);
|
|
370
|
+
if (!existing || existing.generation !== params.generation) throw new Error("ClickClack discussion generation changed before channel creation");
|
|
371
|
+
store.register(params.sessionKey, {
|
|
372
|
+
...existing,
|
|
373
|
+
pending: params.pending
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function listPendingDiscussionOpens(runtime) {
|
|
377
|
+
return getGenerationStore(runtime).entries().flatMap((entry) => entry.value.pending ? [{
|
|
378
|
+
sessionKey: entry.key,
|
|
379
|
+
generation: entry.value.generation,
|
|
380
|
+
...entry.value.pending
|
|
381
|
+
}] : []);
|
|
382
|
+
}
|
|
383
|
+
function hasPendingDiscussionOpenForDestination(params) {
|
|
384
|
+
const serverBaseUrl = params.serverBaseUrl.replace(/\/+$/u, "");
|
|
385
|
+
return listPendingDiscussionOpens(params.runtime).some((pending) => pending.serverBaseUrl === serverBaseUrl && pending.workspaceId === params.workspaceId);
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region extensions/clickclack/src/discussions/naming.ts
|
|
389
|
+
function shortSessionHash(sessionKey) {
|
|
390
|
+
return createHash("sha256").update(sessionKey).digest("hex").slice(0, 32);
|
|
391
|
+
}
|
|
392
|
+
function slugifyLabel(label) {
|
|
393
|
+
return label.normalize("NFKD").replace(/[\u0300-\u036f]/gu, "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 80).replace(/-+$/gu, "");
|
|
394
|
+
}
|
|
395
|
+
function fallbackDiscussionLabel(sessionKey, agentId) {
|
|
396
|
+
const agentSegment = agentId?.trim() ? slugifyLabel(agentId) : "";
|
|
397
|
+
const hash = shortSessionHash(sessionKey).slice(0, 8);
|
|
398
|
+
return `s-${agentSegment ? `${agentSegment}-` : ""}${hash}`;
|
|
399
|
+
}
|
|
400
|
+
function resolveDiscussionLabel(entry, sessionKey, agentId) {
|
|
401
|
+
return entry.label?.trim() || entry.displayName?.trim() || entry.subject?.trim() || fallbackDiscussionLabel(sessionKey, agentId);
|
|
402
|
+
}
|
|
403
|
+
function truncateDiscussionDisplayTitle(label) {
|
|
404
|
+
return Array.from(label).slice(0, 200).join("").trim();
|
|
405
|
+
}
|
|
406
|
+
function slugifyDiscussionLabel(label, sessionKey) {
|
|
407
|
+
return slugifyLabel(label) || fallbackDiscussionLabel(sessionKey);
|
|
408
|
+
}
|
|
409
|
+
function discussionSessionPeerId(identity) {
|
|
410
|
+
return `disc-${createHash("sha256").update([
|
|
411
|
+
identity.mainSessionKey,
|
|
412
|
+
identity.sessionId,
|
|
413
|
+
identity.serverBaseUrl,
|
|
414
|
+
identity.channelId,
|
|
415
|
+
identity.externalRef
|
|
416
|
+
].join("\0")).digest("hex").slice(0, 32)}`;
|
|
417
|
+
}
|
|
418
|
+
function isDiscussionSessionKey(sessionKey) {
|
|
419
|
+
return sessionKey.includes(":clickclack:") && /:channel:disc-[0-9a-f]{32}$/u.test(sessionKey);
|
|
420
|
+
}
|
|
421
|
+
function discussionExternalRef(installationId, mainSessionKey, sessionId, destinationIdentity, bindingGeneration) {
|
|
422
|
+
return `openclaw:${installationId}:${shortSessionHash([
|
|
423
|
+
mainSessionKey,
|
|
424
|
+
sessionId,
|
|
425
|
+
destinationIdentity,
|
|
426
|
+
bindingGeneration
|
|
427
|
+
].join("\0"))}`;
|
|
428
|
+
}
|
|
429
|
+
function discussionCredentialFingerprint(token) {
|
|
430
|
+
return createHash("sha256").update(token).digest("hex");
|
|
431
|
+
}
|
|
432
|
+
function discussionSessionKey(params) {
|
|
433
|
+
return params.runtime.channel.routing.buildAgentSessionKey({
|
|
434
|
+
agentId: params.agentId,
|
|
435
|
+
channel: "clickclack",
|
|
436
|
+
accountId: params.accountId,
|
|
437
|
+
peer: {
|
|
438
|
+
kind: "channel",
|
|
439
|
+
id: discussionSessionPeerId(params)
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region extensions/clickclack/src/discussions/binding-store.ts
|
|
445
|
+
function bindingMatchesSessionIncarnation(runtime, sessionKey, binding) {
|
|
446
|
+
const entry = runtime.agent.session.getSessionEntry({
|
|
447
|
+
sessionKey,
|
|
448
|
+
readConsistency: "latest"
|
|
449
|
+
});
|
|
450
|
+
return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId);
|
|
451
|
+
}
|
|
452
|
+
function bindingMatchesActiveSessionIncarnation(runtime, sessionKey, binding) {
|
|
453
|
+
const entry = runtime.agent.session.getSessionEntry({
|
|
454
|
+
sessionKey,
|
|
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}`;
|
|
464
|
+
}
|
|
465
|
+
/** SQLite-backed session/channel bindings with a process-local inbound lookup index. */
|
|
466
|
+
var ClickClackDiscussionBindingStore = class {
|
|
467
|
+
#store;
|
|
468
|
+
#sessionByChannel = /* @__PURE__ */ new Map();
|
|
469
|
+
#mainByDiscussionSession = /* @__PURE__ */ new Map();
|
|
470
|
+
#runtime;
|
|
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
|
+
}
|
|
555
|
+
};
|
|
556
|
+
function getClickClackDiscussionBindingStore(runtime) {
|
|
557
|
+
const existing = storesByRuntime$1.get(runtime);
|
|
558
|
+
if (existing) return existing;
|
|
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, "");
|
|
570
|
+
}
|
|
571
|
+
/** Resolves the sole live account and rejects bindings pinned to an older destination. */
|
|
572
|
+
function resolveDiscussionBindingAccount(cfg, binding) {
|
|
573
|
+
const accounts = discussionAccounts(cfg);
|
|
574
|
+
if (accounts.length !== 1) return { state: "unavailable" };
|
|
575
|
+
const account = accounts[0];
|
|
576
|
+
if (!account) return { state: "unavailable" };
|
|
577
|
+
if (account.accountId !== binding.accountId || normalizedServerBaseUrl(account) !== binding.serverBaseUrl || account.discussions.workspace !== binding.workspaceRef || binding.credentialFingerprint !== void 0 && discussionCredentialFingerprint(account.token) !== binding.credentialFingerprint) return {
|
|
578
|
+
state: "stale",
|
|
579
|
+
account
|
|
580
|
+
};
|
|
581
|
+
return {
|
|
582
|
+
state: "active",
|
|
583
|
+
account
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/** Public embed/open URLs for one bound discussion channel. */
|
|
587
|
+
function discussionInfoForBinding(binding, account) {
|
|
588
|
+
const baseUrl = normalizedServerBaseUrl(account);
|
|
589
|
+
return {
|
|
590
|
+
state: "open",
|
|
591
|
+
embedUrl: `${baseUrl}/embed/channel/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}?openclawHostTheme=1`,
|
|
592
|
+
openUrl: `${baseUrl}/app/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}`
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region extensions/clickclack/src/discussions/revoked-channel-store.ts
|
|
597
|
+
const REVOKED_CHANNELS_NAMESPACE = "discussion-revoked-channels";
|
|
598
|
+
const MAX_REVOKED_CHANNELS = 1e5;
|
|
599
|
+
const storesByRuntime = /* @__PURE__ */ new WeakMap();
|
|
600
|
+
function revokedChannelKey(params) {
|
|
601
|
+
return [params.serverBaseUrl.replace(/\/+$/u, ""), params.channelId].join("\0");
|
|
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;
|
|
613
|
+
}
|
|
614
|
+
/** Records managed ownership before its live binding is released. */
|
|
615
|
+
function markClickClackDiscussionChannelRevoked(runtime, binding) {
|
|
616
|
+
const value = {
|
|
617
|
+
accountId: binding.accountId,
|
|
618
|
+
serverBaseUrl: binding.serverBaseUrl,
|
|
619
|
+
channelId: binding.channelId,
|
|
620
|
+
revokedAt: Date.now()
|
|
621
|
+
};
|
|
622
|
+
getStore(runtime).register(revokedChannelKey(value), value);
|
|
623
|
+
}
|
|
624
|
+
function markClickClackDiscussionChannelIdentityRevoked(params) {
|
|
625
|
+
const value = {
|
|
626
|
+
accountId: params.accountId,
|
|
627
|
+
serverBaseUrl: params.serverBaseUrl.replace(/\/+$/u, ""),
|
|
628
|
+
channelId: params.channelId,
|
|
629
|
+
revokedAt: Date.now()
|
|
630
|
+
};
|
|
631
|
+
getStore(params.runtime).register(revokedChannelKey(value), value);
|
|
632
|
+
}
|
|
633
|
+
function clearClickClackDiscussionChannelRevoked(params) {
|
|
634
|
+
getStore(params.runtime).delete(revokedChannelKey(params));
|
|
635
|
+
}
|
|
636
|
+
/** Distinguishes a released managed channel from a genuinely ordinary channel. */
|
|
637
|
+
function isClickClackDiscussionChannelRevoked(params) {
|
|
638
|
+
return Boolean(getStore(params.runtime).lookup(revokedChannelKey(params)));
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region extensions/clickclack/src/discussions/routing.ts
|
|
642
|
+
function resolveClickClackDiscussionRoute(params) {
|
|
643
|
+
if (isClickClackDiscussionChannelRevoked(params)) return { state: "revoked" };
|
|
644
|
+
const matched = getClickClackDiscussionBindingStore(params.runtime).getByChannel(params.serverBaseUrl, params.channelId);
|
|
645
|
+
if (!matched) return { state: hasPendingDiscussionOpenForDestination(params) ? "revoked" : "unbound" };
|
|
646
|
+
if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
|
|
647
|
+
if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
|
|
648
|
+
if (matched.binding.archived) return { state: "revoked" };
|
|
649
|
+
if (resolveDiscussionBindingAccount(params.config, matched.binding).state !== "active") return { state: "revoked" };
|
|
650
|
+
if (!bindingMatchesActiveSessionIncarnation(params.runtime, matched.sessionKey, matched.binding)) return { state: "revoked" };
|
|
651
|
+
const sessionKey = discussionSessionKey({
|
|
652
|
+
runtime: params.runtime,
|
|
653
|
+
agentId: matched.binding.agentId,
|
|
654
|
+
mainSessionKey: matched.sessionKey,
|
|
655
|
+
sessionId: matched.binding.sessionId,
|
|
656
|
+
accountId: params.accountId,
|
|
657
|
+
serverBaseUrl: matched.binding.serverBaseUrl,
|
|
658
|
+
channelId: matched.binding.channelId,
|
|
659
|
+
externalRef: matched.binding.externalRef
|
|
660
|
+
});
|
|
661
|
+
if (!sessionKey) return { state: "revoked" };
|
|
662
|
+
return {
|
|
663
|
+
state: "active",
|
|
664
|
+
route: {
|
|
665
|
+
agentId: matched.binding.agentId,
|
|
666
|
+
sessionKey,
|
|
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(" ")
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
//#endregion
|
|
331
678
|
//#region extensions/clickclack/src/target.ts
|
|
332
679
|
/**
|
|
333
680
|
* Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
|
|
@@ -378,6 +725,23 @@ function looksLikeClickClackTarget(raw) {
|
|
|
378
725
|
* and direct messages.
|
|
379
726
|
*/
|
|
380
727
|
const CLICKCLACK_MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
|
|
728
|
+
const CLICKCLACK_FORMAT_PROFILE = FormatCapabilityProfile.define({
|
|
729
|
+
mechanism: "markdown",
|
|
730
|
+
chunk: {
|
|
731
|
+
limit: 1024 * 1024,
|
|
732
|
+
unit: "bytes"
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
function renderClickClackMarkdown(markdown) {
|
|
736
|
+
return renderMarkdownWithMarkers({
|
|
737
|
+
text: markdown,
|
|
738
|
+
styles: [],
|
|
739
|
+
links: []
|
|
740
|
+
}, {
|
|
741
|
+
styleMarkers: {},
|
|
742
|
+
escapeText: (text) => text
|
|
743
|
+
}, CLICKCLACK_FORMAT_PROFILE);
|
|
744
|
+
}
|
|
381
745
|
async function createTargetMessage(params) {
|
|
382
746
|
const parsed = parseClickClackTarget(params.to);
|
|
383
747
|
const explicitThreadId = params.threadId == null ? "" : String(params.threadId);
|
|
@@ -453,7 +817,7 @@ function createOutboundContext(params) {
|
|
|
453
817
|
return {
|
|
454
818
|
account,
|
|
455
819
|
client: createClickClackClient({
|
|
456
|
-
baseUrl: account.
|
|
820
|
+
baseUrl: account.apiEndpoint,
|
|
457
821
|
token: account.token,
|
|
458
822
|
correlationId: params.correlationId
|
|
459
823
|
})
|
|
@@ -464,7 +828,7 @@ function createOutboundContext(params) {
|
|
|
464
828
|
* message id, or undefined when sanitization removes all content.
|
|
465
829
|
*/
|
|
466
830
|
async function sendClickClackText(params) {
|
|
467
|
-
const text = sanitizeAssistantVisibleText(params.text);
|
|
831
|
+
const text = renderClickClackMarkdown(sanitizeAssistantVisibleText(params.text));
|
|
468
832
|
if (!text) return;
|
|
469
833
|
const { account, client } = createOutboundContext(params);
|
|
470
834
|
const workspaceId = await resolveWorkspaceId(client, account.workspace);
|
|
@@ -524,7 +888,7 @@ async function sendClickClackMedia(params) {
|
|
|
524
888
|
...nonces.upload ? { nonce: nonces.upload } : {}
|
|
525
889
|
});
|
|
526
890
|
}
|
|
527
|
-
const text = sanitizeAssistantVisibleText(params.text) || mediaFilename || upload.filename || "attachment";
|
|
891
|
+
const text = renderClickClackMarkdown(sanitizeAssistantVisibleText(params.text)) || mediaFilename || upload.filename || "attachment";
|
|
528
892
|
const message = await createTargetMessage({
|
|
529
893
|
client,
|
|
530
894
|
workspaceId,
|
|
@@ -753,13 +1117,29 @@ async function handleClickClackInbound(params) {
|
|
|
753
1117
|
kind: "channel",
|
|
754
1118
|
id: message.channel_id ?? ""
|
|
755
1119
|
});
|
|
756
|
-
const
|
|
1120
|
+
const accountRoute = resolveAccountAgentRoute({
|
|
757
1121
|
cfg: params.config,
|
|
758
1122
|
account: params.account,
|
|
759
1123
|
target,
|
|
760
1124
|
isDirect
|
|
761
1125
|
});
|
|
762
|
-
|
|
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) {
|
|
763
1143
|
await dispatchModelReply({
|
|
764
1144
|
account: params.account,
|
|
765
1145
|
cfg: params.config,
|
|
@@ -774,7 +1154,7 @@ async function handleClickClackInbound(params) {
|
|
|
774
1154
|
let activity;
|
|
775
1155
|
if (params.account.agentActivity && (message.channel_id || message.direct_conversation_id)) activity = createClickClackActivityPublisher({
|
|
776
1156
|
client: createClickClackClient({
|
|
777
|
-
baseUrl: params.account.
|
|
1157
|
+
baseUrl: params.account.apiEndpoint,
|
|
778
1158
|
token: params.account.token,
|
|
779
1159
|
correlationId: params.correlationId
|
|
780
1160
|
}),
|
|
@@ -841,7 +1221,10 @@ async function handleClickClackInbound(params) {
|
|
|
841
1221
|
wasMentioned: !isDirect
|
|
842
1222
|
}
|
|
843
1223
|
},
|
|
844
|
-
extra: {
|
|
1224
|
+
extra: {
|
|
1225
|
+
GroupChannel: message.channel_id,
|
|
1226
|
+
...discussionRoute ? { GroupSystemPrompt: discussionRoute.systemPrompt } : {}
|
|
1227
|
+
}
|
|
845
1228
|
});
|
|
846
1229
|
const runId = resolveClickClackAgentRunId(message.id);
|
|
847
1230
|
const activityReplyOptions = activity ? {
|
|
@@ -960,7 +1343,7 @@ async function processEvent(params) {
|
|
|
960
1343
|
const correlationId = eventCorrelationId(params.event);
|
|
961
1344
|
const message = await resolveEventMessage({
|
|
962
1345
|
client: correlationId ? createClickClackClient({
|
|
963
|
-
baseUrl: params.account.
|
|
1346
|
+
baseUrl: params.account.apiEndpoint,
|
|
964
1347
|
token: params.account.token,
|
|
965
1348
|
correlationId
|
|
966
1349
|
}) : params.client,
|
|
@@ -1006,7 +1389,7 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1006
1389
|
});
|
|
1007
1390
|
if (!configuredAccount.configured) throw new Error(`ClickClack is not configured for account "${configuredAccount.accountId}"`);
|
|
1008
1391
|
const client = createClickClackClient({
|
|
1009
|
-
baseUrl: configuredAccount.
|
|
1392
|
+
baseUrl: configuredAccount.apiEndpoint,
|
|
1010
1393
|
token: configuredAccount.token
|
|
1011
1394
|
});
|
|
1012
1395
|
const workspaceId = await resolveWorkspaceId(client, configuredAccount.workspace);
|
|
@@ -1228,6 +1611,7 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1228
1611
|
configSchema: clickClackConfigSchema,
|
|
1229
1612
|
config: clickClackConfigAdapter,
|
|
1230
1613
|
setup: clickClackSetupAdapter,
|
|
1614
|
+
setupContract: clickClackSetupContract,
|
|
1231
1615
|
setupWizard: clickClackSetupWizard,
|
|
1232
1616
|
secrets: {
|
|
1233
1617
|
secretTargetRegistryEntries,
|
|
@@ -1333,4 +1717,4 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1333
1717
|
}
|
|
1334
1718
|
});
|
|
1335
1719
|
//#endregion
|
|
1336
|
-
export { setClickClackRuntime as
|
|
1720
|
+
export { listPendingDiscussionOpens as C, setClickClackRuntime as D, getClickClackRuntime as E, clearDiscussionBindingGeneration as S, reserveDiscussionBindingGeneration as T, fallbackDiscussionLabel as _, isClickClackDiscussionChannelRevoked as a, slugifyDiscussionLabel as b, discussionAccounts as c, resolveDiscussionBindingAccount as d, bindingMatchesActiveSessionIncarnation as f, discussionExternalRef as g, discussionCredentialFingerprint as h, clearClickClackDiscussionChannelRevoked as i, discussionInfoForBinding as l, getClickClackDiscussionBindingStore as m, buildClickClackTarget as n, markClickClackDiscussionChannelIdentityRevoked as o, bindingMatchesSessionIncarnation as p, parseClickClackTarget as r, markClickClackDiscussionChannelRevoked as s, clickClackPlugin as t, normalizedServerBaseUrl as u, isDiscussionSessionKey as v, recordPendingDiscussionOpen as w, truncateDiscussionDisplayTitle as x, resolveDiscussionLabel as y };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as clickClackPlugin } from "./channel-
|
|
1
|
+
import { t as clickClackPlugin } from "./channel-DfcFlLjC.js";
|
|
2
2
|
export { clickClackPlugin };
|