@openclaw/clickclack 2026.7.2-beta.4 → 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 +5 -0
- package/dist/api.js +2 -2
- package/dist/{channel-BDq0KWDB.js → channel-DfcFlLjC.js} +27 -29
- package/dist/channel-plugin-api.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{runtime-api-CV1cOqFO.js → runtime-api-D1H9vheM.js} +320 -73
- package/dist/runtime-api.js +3 -3
- package/dist/{setup-claim-CJIUfZS7.js → setup-claim-BFU16zoo.js} +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{setup-surface-Baf26N-D.js → setup-surface-CD6MUC1g.js} +13 -54
- package/package.json +4 -5
- package/npm-shrinkwrap.json +0 -54
package/README.md
CHANGED
|
@@ -106,6 +106,11 @@ defaults to the account workspace, and `section` defaults to `Sessions`.
|
|
|
106
106
|
`controlUrlBase` adds a link back to `/chat?session=<session-key>` in the
|
|
107
107
|
OpenClaw Control UI.
|
|
108
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
|
+
|
|
109
114
|
Enable discussions on exactly one ClickClack account. Multiple enabled
|
|
110
115
|
discussion accounts are rejected because the session discussion provider does
|
|
111
116
|
not have an account selector.
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
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-
|
|
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 { _ 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-
|
|
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";
|
|
@@ -12,7 +12,7 @@ import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel
|
|
|
12
12
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
13
13
|
import { createHash, randomUUID } from "node:crypto";
|
|
14
14
|
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
|
15
|
-
import { renderMarkdownWithMarkers, 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
|
|
@@ -389,14 +389,22 @@ function hasPendingDiscussionOpenForDestination(params) {
|
|
|
389
389
|
function shortSessionHash(sessionKey) {
|
|
390
390
|
return createHash("sha256").update(sessionKey).digest("hex").slice(0, 32);
|
|
391
391
|
}
|
|
392
|
-
function
|
|
393
|
-
return
|
|
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
394
|
}
|
|
395
|
-
function
|
|
396
|
-
|
|
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();
|
|
397
405
|
}
|
|
398
406
|
function slugifyDiscussionLabel(label, sessionKey) {
|
|
399
|
-
return label
|
|
407
|
+
return slugifyLabel(label) || fallbackDiscussionLabel(sessionKey);
|
|
400
408
|
}
|
|
401
409
|
function discussionSessionPeerId(identity) {
|
|
402
410
|
return `disc-${createHash("sha256").update([
|
|
@@ -575,6 +583,15 @@ function resolveDiscussionBindingAccount(cfg, binding) {
|
|
|
575
583
|
account
|
|
576
584
|
};
|
|
577
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
|
+
}
|
|
578
595
|
//#endregion
|
|
579
596
|
//#region extensions/clickclack/src/discussions/revoked-channel-store.ts
|
|
580
597
|
const REVOKED_CHANNELS_NAMESPACE = "discussion-revoked-channels";
|
|
@@ -708,32 +725,13 @@ function looksLikeClickClackTarget(raw) {
|
|
|
708
725
|
* and direct messages.
|
|
709
726
|
*/
|
|
710
727
|
const CLICKCLACK_MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
|
|
711
|
-
const CLICKCLACK_FORMAT_PROFILE = {
|
|
728
|
+
const CLICKCLACK_FORMAT_PROFILE = FormatCapabilityProfile.define({
|
|
712
729
|
mechanism: "markdown",
|
|
713
|
-
constructs: {
|
|
714
|
-
bold: "native",
|
|
715
|
-
italic: "native",
|
|
716
|
-
underline: "native",
|
|
717
|
-
strikethrough: "native",
|
|
718
|
-
spoiler: "native",
|
|
719
|
-
codeInline: "native",
|
|
720
|
-
codeBlock: "native",
|
|
721
|
-
codeLanguage: "native",
|
|
722
|
-
linkLabel: "native",
|
|
723
|
-
heading: "native",
|
|
724
|
-
bulletList: "native",
|
|
725
|
-
orderedList: "native",
|
|
726
|
-
taskList: "native",
|
|
727
|
-
table: "native",
|
|
728
|
-
blockquote: "native",
|
|
729
|
-
image: "native",
|
|
730
|
-
mention: "native"
|
|
731
|
-
},
|
|
732
730
|
chunk: {
|
|
733
731
|
limit: 1024 * 1024,
|
|
734
732
|
unit: "bytes"
|
|
735
733
|
}
|
|
736
|
-
};
|
|
734
|
+
});
|
|
737
735
|
function renderClickClackMarkdown(markdown) {
|
|
738
736
|
return renderMarkdownWithMarkers({
|
|
739
737
|
text: markdown,
|
|
@@ -1719,4 +1717,4 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1719
1717
|
}
|
|
1720
1718
|
});
|
|
1721
1719
|
//#endregion
|
|
1722
|
-
export {
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as registerClickClackDiscussions } from "./runtime-api-
|
|
1
|
+
import { t as registerClickClackDiscussions } from "./runtime-api-D1H9vheM.js";
|
|
2
2
|
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
|
3
3
|
//#region extensions/clickclack/index.ts
|
|
4
4
|
/**
|
|
@@ -1,11 +1,130 @@
|
|
|
1
|
-
import { d as createClickClackClient, f as isClickClackChannelNameConflict, u as ClickClackHttpError, x as resolveClickClackAccount, y as listClickClackAccountIds } from "./setup-surface-
|
|
2
|
-
import { C as
|
|
1
|
+
import { d as createClickClackClient, f as isClickClackChannelNameConflict, u as ClickClackHttpError, x as resolveClickClackAccount, y as listClickClackAccountIds } from "./setup-surface-CD6MUC1g.js";
|
|
2
|
+
import { C as listPendingDiscussionOpens, S as clearDiscussionBindingGeneration, T as reserveDiscussionBindingGeneration, _ as fallbackDiscussionLabel, a as isClickClackDiscussionChannelRevoked, b as slugifyDiscussionLabel, c as discussionAccounts, d as resolveDiscussionBindingAccount, f as bindingMatchesActiveSessionIncarnation, g as discussionExternalRef, h as discussionCredentialFingerprint, i as clearClickClackDiscussionChannelRevoked, l as discussionInfoForBinding, m as getClickClackDiscussionBindingStore, o as markClickClackDiscussionChannelIdentityRevoked, p as bindingMatchesSessionIncarnation, s as markClickClackDiscussionChannelRevoked, u as normalizedServerBaseUrl, v as isDiscussionSessionKey, w as recordPendingDiscussionOpen, x as truncateDiscussionDisplayTitle, y as resolveDiscussionLabel } from "./channel-DfcFlLjC.js";
|
|
3
3
|
import "./api.js";
|
|
4
4
|
import { resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { registerSessionDiscussionProvider } from "openclaw/plugin-sdk/session-discussion";
|
|
7
7
|
import { createSessionVisibilityChecker } from "openclaw/plugin-sdk/session-visibility";
|
|
8
|
+
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
8
9
|
import { textResult } from "openclaw/plugin-sdk/tool-results";
|
|
10
|
+
//#region extensions/clickclack/src/discussions/control-session-url.ts
|
|
11
|
+
const SESSION_UUID_SUFFIX_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/iu;
|
|
12
|
+
const SHORT_SESSION_REF_RE = /^(?:.*-)?([0-9a-f]{8,32})$/iu;
|
|
13
|
+
const VALID_AGENT_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/iu;
|
|
14
|
+
const INVALID_AGENT_ID_CHARS_RE = /[^a-z0-9_-]+/giu;
|
|
15
|
+
const FIXED_RESERVED_SESSION_RESTS = /* @__PURE__ */ new Set([
|
|
16
|
+
"main",
|
|
17
|
+
"global",
|
|
18
|
+
"boot",
|
|
19
|
+
"sessions"
|
|
20
|
+
]);
|
|
21
|
+
const SESSION_SLUG_MAX_LENGTH = 48;
|
|
22
|
+
const DEFAULT_AGENT_ID = "main";
|
|
23
|
+
const DEFAULT_MAIN_KEY = "main";
|
|
24
|
+
function optionalString(value) {
|
|
25
|
+
return (value?.trim() ?? "") || null;
|
|
26
|
+
}
|
|
27
|
+
function normalizeAgentId$1(value) {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!trimmed) return DEFAULT_AGENT_ID;
|
|
30
|
+
if (VALID_AGENT_ID_RE.test(trimmed)) return trimmed.toLowerCase();
|
|
31
|
+
return trimmed.toLowerCase().replace(INVALID_AGENT_ID_CHARS_RE, "-").replace(/^-+|-+$/gu, "").slice(0, 64) || DEFAULT_AGENT_ID;
|
|
32
|
+
}
|
|
33
|
+
function normalizeBasePath(basePath) {
|
|
34
|
+
const trimmed = basePath.trim().replace(/^\/+|\/+$/gu, "");
|
|
35
|
+
return trimmed ? `/${trimmed}` : "";
|
|
36
|
+
}
|
|
37
|
+
function agentSessionKeyParts(sessionKey) {
|
|
38
|
+
const parts = sessionKey.split(":");
|
|
39
|
+
if (parts.length < 3 || parts[0]?.toLowerCase() !== "agent") return null;
|
|
40
|
+
const agentId = optionalString(parts[1]);
|
|
41
|
+
const restSegments = parts.slice(2);
|
|
42
|
+
if (!agentId || restSegments.some((segment) => !segment)) return null;
|
|
43
|
+
return {
|
|
44
|
+
agentId: normalizeAgentId$1(agentId),
|
|
45
|
+
rest: restSegments.join(":")
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function encodePathSegment(segment) {
|
|
49
|
+
if (segment === ".") return "~dot";
|
|
50
|
+
if (segment === "..") return "~dotdot";
|
|
51
|
+
const encoded = encodeURIComponent(segment).replaceAll(".", "%2E");
|
|
52
|
+
return encoded.startsWith("~") ? `~${encoded}` : encoded;
|
|
53
|
+
}
|
|
54
|
+
function isReservedSessionRest(rest, mainKey) {
|
|
55
|
+
const normalized = rest.toLowerCase();
|
|
56
|
+
return FIXED_RESERVED_SESSION_RESTS.has(normalized) || normalized === (optionalString(mainKey)?.toLowerCase() ?? DEFAULT_MAIN_KEY);
|
|
57
|
+
}
|
|
58
|
+
function sessionSlug(displayName) {
|
|
59
|
+
const tokens = (displayName ?? "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").split("-").filter(Boolean);
|
|
60
|
+
while (tokens.length > 0 && /^[0-9a-f]+$/u.test(tokens.at(-1) ?? "")) tokens.pop();
|
|
61
|
+
return tokens.join("-").slice(0, SESSION_SLUG_MAX_LENGTH).replace(/-+$/gu, "");
|
|
62
|
+
}
|
|
63
|
+
function sessionKeyUuid(sessionKey) {
|
|
64
|
+
const uuid = (agentSessionKeyParts(sessionKey)?.rest)?.match(SESSION_UUID_SUFFIX_RE)?.[1];
|
|
65
|
+
return uuid ? uuid.toLowerCase().replaceAll("-", "") : null;
|
|
66
|
+
}
|
|
67
|
+
function sessionPath(params) {
|
|
68
|
+
const rawKey = optionalString(params.sessionKey);
|
|
69
|
+
const parsed = rawKey ? agentSessionKeyParts(rawKey) : null;
|
|
70
|
+
const fallbackAgentId = optionalString(params.fallbackAgentId);
|
|
71
|
+
const agentId = parsed?.agentId ?? (fallbackAgentId ? normalizeAgentId$1(fallbackAgentId) : null);
|
|
72
|
+
if (!rawKey || !agentId || !parsed && rawKey.toLowerCase().startsWith("agent:")) return null;
|
|
73
|
+
const namespace = `${normalizeBasePath(params.basePath)}/chat`;
|
|
74
|
+
const encodedAgentId = encodePathSegment(agentId);
|
|
75
|
+
const rest = parsed?.rest ?? rawKey;
|
|
76
|
+
const normalizedRest = rest.toLowerCase();
|
|
77
|
+
const mainKey = optionalString(params.mainKey)?.toLowerCase() ?? DEFAULT_MAIN_KEY;
|
|
78
|
+
if (!parsed && normalizedRest === DEFAULT_MAIN_KEY || normalizedRest === mainKey || normalizedRest === "global") return `${namespace}/${encodedAgentId}`;
|
|
79
|
+
const uuid = parsed ? sessionKeyUuid(rawKey) : null;
|
|
80
|
+
if (uuid) {
|
|
81
|
+
let length = 8;
|
|
82
|
+
const slug = sessionSlug(params.displayName);
|
|
83
|
+
let sessionRef = `${slug ? `${slug}-` : ""}${uuid.slice(0, length)}`;
|
|
84
|
+
while (length < uuid.length && isReservedSessionRest(sessionRef, params.mainKey)) {
|
|
85
|
+
length += 1;
|
|
86
|
+
sessionRef = `${slug ? `${slug}-` : ""}${uuid.slice(0, length)}`;
|
|
87
|
+
}
|
|
88
|
+
return isReservedSessionRest(sessionRef, params.mainKey) ? null : `${namespace}/${encodedAgentId}/${sessionRef}`;
|
|
89
|
+
}
|
|
90
|
+
const segments = rest.split(":");
|
|
91
|
+
if (segments.some((segment) => !segment)) return null;
|
|
92
|
+
if (segments.length === 1) {
|
|
93
|
+
const segment = segments[0] ?? "";
|
|
94
|
+
if (!isReservedSessionRest(segment, params.mainKey) && SHORT_SESSION_REF_RE.test(segment)) return `${namespace}/${encodedAgentId}/~key/${encodePathSegment(segment)}`;
|
|
95
|
+
}
|
|
96
|
+
return `${namespace}/${encodedAgentId}/${segments.map(encodePathSegment).join("/")}`;
|
|
97
|
+
}
|
|
98
|
+
function controlSessionUrl(baseUrl, sessionKey, fallbackAgentId, mainKey, displayName) {
|
|
99
|
+
if (!baseUrl) return;
|
|
100
|
+
const url = new URL(baseUrl);
|
|
101
|
+
const path = sessionPath({
|
|
102
|
+
sessionKey,
|
|
103
|
+
fallbackAgentId,
|
|
104
|
+
basePath: url.pathname,
|
|
105
|
+
displayName,
|
|
106
|
+
mainKey
|
|
107
|
+
});
|
|
108
|
+
if (!path) return;
|
|
109
|
+
url.pathname = path;
|
|
110
|
+
url.hash = "";
|
|
111
|
+
return url.toString();
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region extensions/clickclack/src/discussions/history-format.ts
|
|
115
|
+
function discussionRecordJson(value) {
|
|
116
|
+
return JSON.stringify(value).replace(/[\u0085\u2028\u2029]/gu, (separator) => `\\u${separator.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
117
|
+
}
|
|
118
|
+
/** Renders channel history as line-per-message records for the discussion tool. */
|
|
119
|
+
function formatDiscussionHistory(history) {
|
|
120
|
+
const text = history.messages.map((message) => {
|
|
121
|
+
const author = message.author?.display_name || message.author?.handle || message.author_id || "Unknown";
|
|
122
|
+
return `timestamp=${discussionRecordJson(message.created_at)} [Author ${discussionRecordJson(author)} id=${discussionRecordJson(message.author_id)}] text=${discussionRecordJson(message.body)}`;
|
|
123
|
+
}).join("\n");
|
|
124
|
+
const truncationNote = history.truncated ? "\n[History scan reached its safety bound; older active threads may be omitted.]" : "";
|
|
125
|
+
return text ? `${text}${truncationNote}` : "";
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
9
128
|
//#region extensions/clickclack/src/discussions/installation.ts
|
|
10
129
|
const INSTALLATION_NAMESPACE = "discussion-installation";
|
|
11
130
|
const INSTALLATION_KEY = "current";
|
|
@@ -23,6 +142,85 @@ function getClickClackDiscussionInstallationId(runtime) {
|
|
|
23
142
|
return store.lookup(INSTALLATION_KEY)?.id ?? id;
|
|
24
143
|
}
|
|
25
144
|
//#endregion
|
|
145
|
+
//#region extensions/clickclack/src/discussions/reconcile-scheduler.ts
|
|
146
|
+
const RECONCILE_DEBOUNCE_MS = 250;
|
|
147
|
+
const RECONCILE_RETRY_MS = 1e3;
|
|
148
|
+
const RECONCILE_RETRY_MAX_MS = 6e4;
|
|
149
|
+
/**
|
|
150
|
+
* Per-session debounced reconcile scheduling with failure backoff.
|
|
151
|
+
* Contracts: bursts coalesce onto the earliest scheduled run (steady event
|
|
152
|
+
* traffic can never postpone one); a failed run sets a not-before floor with
|
|
153
|
+
* growing backoff that later events cannot undercut; events during an
|
|
154
|
+
* in-flight run collapse into one follow-up scheduled after settlement; and
|
|
155
|
+
* `supersede()` invalidates callbacks from earlier activations so stop/start
|
|
156
|
+
* cycles cannot corrupt the bookkeeping of the next one.
|
|
157
|
+
*/
|
|
158
|
+
var DiscussionReconcileScheduler = class {
|
|
159
|
+
#hooks;
|
|
160
|
+
#timers = /* @__PURE__ */ new Map();
|
|
161
|
+
#retryState = /* @__PURE__ */ new Map();
|
|
162
|
+
#inFlight = /* @__PURE__ */ new Set();
|
|
163
|
+
#followUps = /* @__PURE__ */ new Set();
|
|
164
|
+
#generation = 0;
|
|
165
|
+
constructor(hooks) {
|
|
166
|
+
this.#hooks = hooks;
|
|
167
|
+
}
|
|
168
|
+
/** Invalidates callbacks scheduled by prior activations. */
|
|
169
|
+
supersede() {
|
|
170
|
+
this.#generation += 1;
|
|
171
|
+
}
|
|
172
|
+
clear() {
|
|
173
|
+
for (const scheduled of this.#timers.values()) clearTimeout(scheduled.timer);
|
|
174
|
+
this.#timers.clear();
|
|
175
|
+
this.#retryState.clear();
|
|
176
|
+
this.#inFlight.clear();
|
|
177
|
+
this.#followUps.clear();
|
|
178
|
+
}
|
|
179
|
+
schedule(sessionKey, delayMs = RECONCILE_DEBOUNCE_MS) {
|
|
180
|
+
if (!this.#hooks.shouldSchedule(sessionKey)) return;
|
|
181
|
+
if (this.#inFlight.has(sessionKey)) {
|
|
182
|
+
this.#followUps.add(sessionKey);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const retry = this.#retryState.get(sessionKey);
|
|
186
|
+
const fireAt = Math.max(Date.now() + delayMs, retry?.notBefore ?? 0);
|
|
187
|
+
const existing = this.#timers.get(sessionKey);
|
|
188
|
+
if (existing) {
|
|
189
|
+
if (existing.fireAt <= fireAt) return;
|
|
190
|
+
clearTimeout(existing.timer);
|
|
191
|
+
}
|
|
192
|
+
const generation = this.#generation;
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
if (generation !== this.#generation) return;
|
|
195
|
+
this.#timers.delete(sessionKey);
|
|
196
|
+
this.#inFlight.add(sessionKey);
|
|
197
|
+
this.#hooks.run(sessionKey).then(() => {
|
|
198
|
+
if (generation !== this.#generation) return;
|
|
199
|
+
this.#retryState.delete(sessionKey);
|
|
200
|
+
}, (error) => {
|
|
201
|
+
if (generation !== this.#generation || !this.#hooks.shouldSchedule(sessionKey)) return;
|
|
202
|
+
const previous = this.#retryState.get(sessionKey);
|
|
203
|
+
const nextRetryMs = Math.min(previous === void 0 ? RECONCILE_RETRY_MS : previous.delayMs * 2, RECONCILE_RETRY_MAX_MS);
|
|
204
|
+
this.#retryState.set(sessionKey, {
|
|
205
|
+
delayMs: nextRetryMs,
|
|
206
|
+
notBefore: Date.now() + nextRetryMs
|
|
207
|
+
});
|
|
208
|
+
this.#hooks.warn(`discussion event reconcile failed for ${sessionKey}; retrying in ${nextRetryMs}ms: ${String(error)}`);
|
|
209
|
+
this.#followUps.add(sessionKey);
|
|
210
|
+
}).finally(() => {
|
|
211
|
+
if (generation !== this.#generation) return;
|
|
212
|
+
this.#inFlight.delete(sessionKey);
|
|
213
|
+
if (this.#followUps.delete(sessionKey)) this.schedule(sessionKey);
|
|
214
|
+
});
|
|
215
|
+
}, Math.max(fireAt - Date.now(), 0));
|
|
216
|
+
timer.unref?.();
|
|
217
|
+
this.#timers.set(sessionKey, {
|
|
218
|
+
fireAt,
|
|
219
|
+
timer
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
//#endregion
|
|
26
224
|
//#region extensions/clickclack/src/discussions/service-open.ts
|
|
27
225
|
const CHANNEL_NAME_MUTATION_ATTEMPTS$1 = 4;
|
|
28
226
|
function isDefinitiveNoCreateHttpError(error) {
|
|
@@ -34,20 +232,16 @@ function isDefinitiveNoCreateHttpError(error) {
|
|
|
34
232
|
429
|
|
35
233
|
].includes(error.status);
|
|
36
234
|
}
|
|
37
|
-
function controlSessionUrl(baseUrl, sessionKey) {
|
|
38
|
-
if (!baseUrl) return;
|
|
39
|
-
const url = new URL(baseUrl);
|
|
40
|
-
url.pathname = `${url.pathname.replace(/\/+$/u, "")}/chat`;
|
|
41
|
-
url.hash = "";
|
|
42
|
-
url.searchParams.set("session", sessionKey);
|
|
43
|
-
return url.toString();
|
|
44
|
-
}
|
|
45
235
|
async function resolveAvailableChannelName(params) {
|
|
46
236
|
const desired = slugifyDiscussionLabel(params.label, params.sessionKey);
|
|
47
237
|
const channels = params.channels ?? await params.client.channels(params.workspaceId);
|
|
48
238
|
const occupied = new Set(channels.filter((channel) => channel.id !== params.ownChannelId).map((channel) => channel.name));
|
|
49
239
|
if (!occupied.has(desired)) return desired;
|
|
50
|
-
|
|
240
|
+
for (let suffix = 2; suffix <= 20; suffix += 1) {
|
|
241
|
+
const candidate = `${desired}-${suffix}`;
|
|
242
|
+
if (!occupied.has(candidate)) return candidate;
|
|
243
|
+
}
|
|
244
|
+
const fallback = fallbackDiscussionLabel(params.sessionKey, params.agentId);
|
|
51
245
|
if (!occupied.has(fallback)) return fallback;
|
|
52
246
|
for (let suffix = 2;; suffix += 1) {
|
|
53
247
|
const candidate = `${fallback}-${suffix}`;
|
|
@@ -60,16 +254,19 @@ function assertChannelPatch(channel, patch) {
|
|
|
60
254
|
"external_managed",
|
|
61
255
|
"external_ref",
|
|
62
256
|
"external_url",
|
|
257
|
+
"display_title",
|
|
63
258
|
"name",
|
|
64
259
|
"sidebar_section"
|
|
65
260
|
]) {
|
|
66
261
|
const expected = patch[key];
|
|
67
262
|
if (expected === void 0) continue;
|
|
263
|
+
if (key === "display_title" && !(key in channel)) continue;
|
|
68
264
|
if ((key === "external_ref" || key === "external_url" || key === "sidebar_section" ? channel[key] ?? "" : channel[key]) !== expected) throw new Error(`ClickClack channel update did not apply ${key}`);
|
|
69
265
|
}
|
|
70
266
|
}
|
|
71
267
|
function assertManagedChannelContract(channel, expected) {
|
|
72
|
-
|
|
268
|
+
const displayTitleMatches = !("display_title" in channel) || channel.display_title === expected.displayTitle;
|
|
269
|
+
if (channel.external_managed !== true || (channel.external_ref ?? "") !== (expected.externalRef ?? "") || (channel.sidebar_section ?? "") !== (expected.section ?? "") || (channel.external_url ?? "") !== (expected.externalUrl ?? "") || !displayTitleMatches) throw new Error(`ClickClack server does not support the managed discussion channel contract for ${expected.sessionKey}`);
|
|
73
270
|
}
|
|
74
271
|
function assertManagedChannelListContract(channels) {
|
|
75
272
|
if (channels.some((channel) => typeof channel.external_managed !== "boolean" || channel.external_ref !== void 0 && typeof channel.external_ref !== "string" || channel.external_url !== void 0 && typeof channel.external_url !== "string" || channel.sidebar_section !== void 0 && typeof channel.sidebar_section !== "string")) throw new Error("ClickClack server does not advertise the managed discussion contract");
|
|
@@ -93,9 +290,13 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
93
290
|
await params.reconcilePendingOpen(unresolved);
|
|
94
291
|
if (listPendingDiscussionOpens(runtime).some((pending) => pending.sessionKey === sessionKey)) throw new Error("A previous ClickClack discussion open is still unresolved; restore its credential and retry");
|
|
95
292
|
}
|
|
96
|
-
const
|
|
293
|
+
const config = runtime.config.current();
|
|
294
|
+
const agentId = resolveAgentIdFromSessionKey(sessionKey, resolveDefaultAgentId(config));
|
|
295
|
+
const fallback = fallbackDiscussionLabel(sessionKey, agentId);
|
|
296
|
+
const label = resolveDiscussionLabel(entry, sessionKey, agentId);
|
|
297
|
+
const displayTitle = label === fallback ? "" : truncateDiscussionDisplayTitle(label);
|
|
97
298
|
const section = entry.category?.trim() || account.discussions.section;
|
|
98
|
-
const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey);
|
|
299
|
+
const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey, account.agentId ?? "main", config.session?.mainKey, label);
|
|
99
300
|
const archived = entry.archivedAt !== void 0;
|
|
100
301
|
return await params.withChannelMutationLock(async () => {
|
|
101
302
|
if (!store.hasCapacity(sessionKey)) throw new Error("ClickClack discussion binding capacity is exhausted");
|
|
@@ -120,13 +321,15 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
120
321
|
workspaceId: workspace.id,
|
|
121
322
|
label,
|
|
122
323
|
sessionKey,
|
|
324
|
+
agentId,
|
|
123
325
|
channels,
|
|
124
326
|
ownChannelId: adopted?.id
|
|
125
327
|
}),
|
|
126
328
|
external_managed: true,
|
|
127
329
|
external_ref: externalRef,
|
|
128
330
|
external_url: externalUrl ?? "",
|
|
129
|
-
sidebar_section: section
|
|
331
|
+
sidebar_section: section,
|
|
332
|
+
display_title: displayTitle
|
|
130
333
|
};
|
|
131
334
|
recordPendingDiscussionOpen({
|
|
132
335
|
runtime,
|
|
@@ -213,7 +416,8 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
213
416
|
sessionKey,
|
|
214
417
|
externalRef,
|
|
215
418
|
section,
|
|
216
|
-
externalUrl
|
|
419
|
+
externalUrl,
|
|
420
|
+
displayTitle
|
|
217
421
|
});
|
|
218
422
|
if (adopted) assertChannelPatch(resolved, {
|
|
219
423
|
...managedFields,
|
|
@@ -252,7 +456,7 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
252
456
|
}
|
|
253
457
|
const nextBinding = {
|
|
254
458
|
accountId: account.accountId,
|
|
255
|
-
agentId
|
|
459
|
+
agentId,
|
|
256
460
|
sessionId: entry.sessionId,
|
|
257
461
|
serverBaseUrl,
|
|
258
462
|
credentialFingerprint,
|
|
@@ -265,7 +469,8 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
265
469
|
workspaceRouteId: workspace.route_id,
|
|
266
470
|
section,
|
|
267
471
|
archived,
|
|
268
|
-
label
|
|
472
|
+
label,
|
|
473
|
+
displayTitle: "display_title" in channel ? channel.display_title : void 0
|
|
269
474
|
};
|
|
270
475
|
const currentEntry = runtime.agent.session.getSessionEntry({
|
|
271
476
|
sessionKey,
|
|
@@ -307,17 +512,6 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
307
512
|
//#region extensions/clickclack/src/discussions/service.ts
|
|
308
513
|
const RECONCILE_INTERVAL_MS = 6e4;
|
|
309
514
|
const CHANNEL_NAME_MUTATION_ATTEMPTS = 4;
|
|
310
|
-
function discussionInfoForBinding(binding, account) {
|
|
311
|
-
const baseUrl = normalizedServerBaseUrl(account);
|
|
312
|
-
return {
|
|
313
|
-
state: "open",
|
|
314
|
-
embedUrl: `${baseUrl}/embed/channel/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}`,
|
|
315
|
-
openUrl: `${baseUrl}/app/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}`
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
function discussionRecordJson(value) {
|
|
319
|
-
return JSON.stringify(value).replace(/[\u0085\u2028\u2029]/gu, (separator) => `\\u${separator.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
320
|
-
}
|
|
321
515
|
var ClickClackDiscussionService = class {
|
|
322
516
|
#runtime;
|
|
323
517
|
#store;
|
|
@@ -326,9 +520,16 @@ var ClickClackDiscussionService = class {
|
|
|
326
520
|
#bindingGenerationFactory;
|
|
327
521
|
#timersEnabled;
|
|
328
522
|
#sessionLocks = /* @__PURE__ */ new Map();
|
|
523
|
+
#reconcileScheduler = new DiscussionReconcileScheduler({
|
|
524
|
+
shouldSchedule: (sessionKey) => !this.#closed && (this.#store.get(sessionKey) !== void 0 || listPendingDiscussionOpens(this.#runtime).some((pending) => pending.sessionKey === sessionKey)),
|
|
525
|
+
run: async (sessionKey) => await this.reconcile(sessionKey),
|
|
526
|
+
warn: (message) => this.#logger().warn(message)
|
|
527
|
+
});
|
|
329
528
|
#channelMutationLock = Promise.resolve();
|
|
330
529
|
#timer;
|
|
331
530
|
#reconcileAllPromise;
|
|
531
|
+
#unsubscribeSessionsChanged;
|
|
532
|
+
#closed = false;
|
|
332
533
|
constructor(runtime, options = {}) {
|
|
333
534
|
this.#runtime = runtime;
|
|
334
535
|
this.#store = getClickClackDiscussionBindingStore(runtime);
|
|
@@ -344,6 +545,17 @@ var ClickClackDiscussionService = class {
|
|
|
344
545
|
info: async ({ sessionKey }) => await this.info(sessionKey),
|
|
345
546
|
open: async ({ sessionKey }) => await this.open(sessionKey)
|
|
346
547
|
};
|
|
548
|
+
if (options.gatewayEvents) this.bindGatewayEvents(options.gatewayEvents);
|
|
549
|
+
if (this.#timersEnabled) this.#ensureTimer();
|
|
550
|
+
}
|
|
551
|
+
bindGatewayEvents(gatewayEvents) {
|
|
552
|
+
this.#unsubscribeSessionsChanged?.();
|
|
553
|
+
this.#closed = false;
|
|
554
|
+
this.#reconcileScheduler.supersede();
|
|
555
|
+
this.#unsubscribeSessionsChanged = gatewayEvents?.onSessionsChanged((event) => {
|
|
556
|
+
this.#reconcileScheduler.schedule(event.sessionKey);
|
|
557
|
+
});
|
|
558
|
+
for (const sessionKey of this.#listReconcileSessionKeys()) this.#reconcileScheduler.schedule(sessionKey, 0);
|
|
347
559
|
if (this.#timersEnabled) this.#ensureTimer();
|
|
348
560
|
}
|
|
349
561
|
hasEnabledAccount() {
|
|
@@ -391,44 +603,49 @@ var ClickClackDiscussionService = class {
|
|
|
391
603
|
if (current) return discussionInfoForBinding(current, resolved.account);
|
|
392
604
|
}
|
|
393
605
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
606
|
+
let binding;
|
|
607
|
+
try {
|
|
608
|
+
binding = await openClickClackDiscussionBinding({
|
|
609
|
+
runtime: this.#runtime,
|
|
610
|
+
store: this.#store,
|
|
611
|
+
account,
|
|
612
|
+
clientFactory: this.#clientFactory,
|
|
613
|
+
installationId: this.#installationId,
|
|
614
|
+
bindingGenerationFactory: this.#bindingGenerationFactory,
|
|
615
|
+
sessionKey,
|
|
616
|
+
ensureTimer: () => this.#ensureTimer(),
|
|
617
|
+
reconcilePendingOpen: async (pending) => await this.#reconcilePendingOpen(pending, { allowRetry: false }),
|
|
618
|
+
withChannelMutationLock: async (run) => await this.#withChannelMutationLock(run),
|
|
619
|
+
finalizePendingBinding: (key, nextBinding) => this.#finalizePendingBinding(key, nextBinding),
|
|
620
|
+
warn: (message) => this.#logger().warn(message)
|
|
621
|
+
});
|
|
622
|
+
} finally {
|
|
623
|
+
this.#ensureTimer();
|
|
624
|
+
}
|
|
408
625
|
if (!binding) return { state: "available" };
|
|
409
|
-
this.#ensureTimer();
|
|
410
626
|
return discussionInfoForBinding(binding, account);
|
|
411
627
|
});
|
|
412
628
|
}
|
|
413
629
|
async reconcile(sessionKey) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
630
|
+
try {
|
|
631
|
+
await this.#withSessionLock(sessionKey, async () => {
|
|
632
|
+
const binding = this.#store.get(sessionKey);
|
|
633
|
+
if (binding) await this.#reconcileBinding(sessionKey, binding);
|
|
634
|
+
});
|
|
635
|
+
const pending = listPendingDiscussionOpens(this.#runtime).find((candidate) => candidate.sessionKey === sessionKey);
|
|
636
|
+
if (pending) await this.#reconcilePendingOpen(pending);
|
|
637
|
+
} finally {
|
|
638
|
+
this.#ensureTimer();
|
|
639
|
+
}
|
|
418
640
|
}
|
|
419
641
|
async reconcileAll() {
|
|
420
642
|
if (this.#reconcileAllPromise) return await this.#reconcileAllPromise;
|
|
421
643
|
this.#reconcileAllPromise = (async () => {
|
|
422
|
-
for (const
|
|
644
|
+
for (const sessionKey of this.#listReconcileSessionKeys()) try {
|
|
423
645
|
await this.reconcile(sessionKey);
|
|
424
646
|
} catch (error) {
|
|
425
647
|
this.#logger().warn(`discussion reconcile failed for ${sessionKey}: ${String(error)}`);
|
|
426
648
|
}
|
|
427
|
-
for (const pending of listPendingDiscussionOpens(this.#runtime)) try {
|
|
428
|
-
await this.#reconcilePendingOpen(pending);
|
|
429
|
-
} catch (error) {
|
|
430
|
-
this.#logger().warn(`discussion pending-open reconcile failed for ${pending.sessionKey}: ${String(error)}`);
|
|
431
|
-
}
|
|
432
649
|
})().finally(() => {
|
|
433
650
|
this.#reconcileAllPromise = void 0;
|
|
434
651
|
});
|
|
@@ -447,18 +664,17 @@ var ClickClackDiscussionService = class {
|
|
|
447
664
|
serverBaseUrl: binding.serverBaseUrl,
|
|
448
665
|
channelId: binding.channelId
|
|
449
666
|
})) return { text: "No discussion is bound to this session." };
|
|
450
|
-
const history = await this.#clientFactory(resolved.account).latestChannelMessages(binding.channelId, limit);
|
|
451
|
-
const text = history.messages.map((message) => {
|
|
452
|
-
const author = message.author?.display_name || message.author?.handle || message.author_id || "Unknown";
|
|
453
|
-
return `timestamp=${discussionRecordJson(message.created_at)} [Author ${discussionRecordJson(author)} id=${discussionRecordJson(message.author_id)}] text=${discussionRecordJson(message.body)}`;
|
|
454
|
-
}).join("\n");
|
|
455
|
-
const truncationNote = history.truncated ? "\n[History scan reached its safety bound; older active threads may be omitted.]" : "";
|
|
456
667
|
return {
|
|
457
668
|
binding,
|
|
458
|
-
text:
|
|
669
|
+
text: formatDiscussionHistory(await this.#clientFactory(resolved.account).latestChannelMessages(binding.channelId, limit)) || "The bound discussion has no messages yet."
|
|
459
670
|
};
|
|
460
671
|
}
|
|
461
672
|
cleanup() {
|
|
673
|
+
this.#closed = true;
|
|
674
|
+
this.#unsubscribeSessionsChanged?.();
|
|
675
|
+
this.#unsubscribeSessionsChanged = void 0;
|
|
676
|
+
this.#reconcileScheduler.supersede();
|
|
677
|
+
this.#reconcileScheduler.clear();
|
|
462
678
|
if (this.#timer) {
|
|
463
679
|
clearInterval(this.#timer);
|
|
464
680
|
this.#timer = void 0;
|
|
@@ -499,12 +715,17 @@ var ClickClackDiscussionService = class {
|
|
|
499
715
|
}
|
|
500
716
|
const archived = entry ? entry.archivedAt !== void 0 : true;
|
|
501
717
|
const deleted = entry === void 0;
|
|
502
|
-
const
|
|
718
|
+
const fallback = fallbackDiscussionLabel(sessionKey, binding.agentId);
|
|
719
|
+
const label = entry ? resolveDiscussionLabel(entry, sessionKey, binding.agentId) : binding.label;
|
|
503
720
|
const section = entry?.category?.trim() || account.discussions.section;
|
|
504
|
-
const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey) ?? "";
|
|
721
|
+
const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey, account.agentId ?? "main", this.#currentConfig().session?.mainKey, label) ?? "";
|
|
505
722
|
const patch = {};
|
|
506
723
|
if (archived !== binding.archived) patch.archived = archived;
|
|
507
724
|
const labelChanged = label !== binding.label;
|
|
725
|
+
const desiredDisplayTitle = label === fallback ? "" : truncateDiscussionDisplayTitle(label);
|
|
726
|
+
const serverSupportsDisplayTitle = this.#store.entries().some(({ binding: candidate }) => candidate.displayTitle !== void 0 && candidate.serverBaseUrl === binding.serverBaseUrl && candidate.accountId === binding.accountId);
|
|
727
|
+
const shouldBackfillDisplayTitle = desiredDisplayTitle !== "" && binding.displayTitle !== desiredDisplayTitle && serverSupportsDisplayTitle;
|
|
728
|
+
if (labelChanged || shouldBackfillDisplayTitle) patch.display_title = desiredDisplayTitle;
|
|
508
729
|
if (section !== binding.section) patch.sidebar_section = section;
|
|
509
730
|
if (externalUrl !== binding.externalUrl) patch.external_url = externalUrl;
|
|
510
731
|
if (Object.keys(patch).length === 0 && !labelChanged) {
|
|
@@ -512,24 +733,31 @@ var ClickClackDiscussionService = class {
|
|
|
512
733
|
return;
|
|
513
734
|
}
|
|
514
735
|
const client = this.#clientFactory(account);
|
|
515
|
-
|
|
736
|
+
let updated;
|
|
737
|
+
if (labelChanged) updated = await this.#withChannelMutationLock(async () => {
|
|
516
738
|
for (let attempt = 0; attempt < CHANNEL_NAME_MUTATION_ATTEMPTS; attempt += 1) {
|
|
517
739
|
patch.name = await resolveAvailableChannelName({
|
|
518
740
|
client,
|
|
519
741
|
workspaceId: binding.workspaceId,
|
|
520
742
|
label,
|
|
521
743
|
sessionKey,
|
|
744
|
+
agentId: binding.agentId,
|
|
522
745
|
ownChannelId: binding.channelId
|
|
523
746
|
});
|
|
524
747
|
try {
|
|
525
|
-
|
|
526
|
-
|
|
748
|
+
const renamed = await client.updateChannel(binding.channelId, patch);
|
|
749
|
+
assertChannelPatch(renamed, patch);
|
|
750
|
+
return renamed;
|
|
527
751
|
} catch (error) {
|
|
528
752
|
if (!isClickClackChannelNameConflict(error) || attempt === CHANNEL_NAME_MUTATION_ATTEMPTS - 1) throw error;
|
|
529
753
|
}
|
|
530
754
|
}
|
|
755
|
+
throw new Error("ClickClack discussion channel name retries were exhausted");
|
|
531
756
|
});
|
|
532
|
-
else
|
|
757
|
+
else {
|
|
758
|
+
updated = await client.updateChannel(binding.channelId, patch);
|
|
759
|
+
assertChannelPatch(updated, patch);
|
|
760
|
+
}
|
|
533
761
|
if (deleted) {
|
|
534
762
|
this.#revokeAndDeleteBinding(sessionKey, binding);
|
|
535
763
|
return;
|
|
@@ -539,7 +767,8 @@ var ClickClackDiscussionService = class {
|
|
|
539
767
|
archived,
|
|
540
768
|
externalUrl,
|
|
541
769
|
label,
|
|
542
|
-
section
|
|
770
|
+
section,
|
|
771
|
+
displayTitle: "display_title" in updated ? updated.display_title : void 0
|
|
543
772
|
});
|
|
544
773
|
}
|
|
545
774
|
async #reconcilePendingOpen(pending, options = {}) {
|
|
@@ -637,19 +866,28 @@ var ClickClackDiscussionService = class {
|
|
|
637
866
|
return this.#runtime.config.current();
|
|
638
867
|
}
|
|
639
868
|
#ensureTimer() {
|
|
640
|
-
|
|
869
|
+
const hasPendingOpens = listPendingDiscussionOpens(this.#runtime).length > 0;
|
|
870
|
+
const needsBindingPoll = this.#unsubscribeSessionsChanged === void 0 && this.#store.entries().length > 0;
|
|
871
|
+
if (this.#closed || !hasPendingOpens && !needsBindingPoll) {
|
|
872
|
+
if (this.#timer) {
|
|
873
|
+
clearInterval(this.#timer);
|
|
874
|
+
this.#timer = void 0;
|
|
875
|
+
}
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (!this.#timersEnabled || this.#timer) return;
|
|
641
879
|
this.#timer = setInterval(() => {
|
|
642
880
|
this.reconcileAll().catch((error) => {
|
|
643
881
|
this.#logger().warn(`discussion reconcile pass failed: ${String(error)}`);
|
|
644
882
|
}).finally(() => {
|
|
645
|
-
|
|
646
|
-
clearInterval(this.#timer);
|
|
647
|
-
this.#timer = void 0;
|
|
648
|
-
}
|
|
883
|
+
this.#ensureTimer();
|
|
649
884
|
});
|
|
650
885
|
}, RECONCILE_INTERVAL_MS);
|
|
651
886
|
this.#timer.unref?.();
|
|
652
887
|
}
|
|
888
|
+
#listReconcileSessionKeys() {
|
|
889
|
+
return /* @__PURE__ */ new Set([...this.#store.entries().map(({ sessionKey }) => sessionKey), ...listPendingDiscussionOpens(this.#runtime).map(({ sessionKey }) => sessionKey)]);
|
|
890
|
+
}
|
|
653
891
|
#logger() {
|
|
654
892
|
return this.#runtime.logging.getChildLogger({
|
|
655
893
|
plugin: "clickclack",
|
|
@@ -751,6 +989,15 @@ function registerClickClackDiscussions(api) {
|
|
|
751
989
|
return;
|
|
752
990
|
}
|
|
753
991
|
const service = new ClickClackDiscussionService(api.runtime);
|
|
992
|
+
api.registerService({
|
|
993
|
+
id: "clickclack-discussion-session-events",
|
|
994
|
+
start: ({ gatewayEvents }) => {
|
|
995
|
+
service.bindGatewayEvents(gatewayEvents);
|
|
996
|
+
},
|
|
997
|
+
stop: () => {
|
|
998
|
+
service.cleanup();
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
754
1001
|
api.registerTool((context) => createClickClackDiscussionTool({
|
|
755
1002
|
service,
|
|
756
1003
|
sessionKey: context.sessionKey
|
package/dist/runtime-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as createClickClackClient, x as resolveClickClackAccount } from "./setup-surface-
|
|
2
|
-
import {
|
|
3
|
-
import { t as registerClickClackDiscussions } from "./runtime-api-
|
|
1
|
+
import { d as createClickClackClient, x as resolveClickClackAccount } from "./setup-surface-CD6MUC1g.js";
|
|
2
|
+
import { D as setClickClackRuntime, r as parseClickClackTarget } from "./channel-DfcFlLjC.js";
|
|
3
|
+
import { t as registerClickClackDiscussions } from "./runtime-api-D1H9vheM.js";
|
|
4
4
|
export { createClickClackClient, parseClickClackTarget, registerClickClackDiscussions, resolveClickClackAccount, setClickClackRuntime };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as buildClickClackSetupClaimUrl, o as isClickClackSetupLoopbackHost, s as requireClickClackSetupApiBaseUrl } from "./setup-surface-
|
|
1
|
+
import { a as buildClickClackSetupClaimUrl, o as isClickClackSetupLoopbackHost, s as requireClickClackSetupApiBaseUrl } from "./setup-surface-CD6MUC1g.js";
|
|
2
2
|
import { createProviderOperationDeadline, readProviderJsonResponse, readResponseTextLimited, resolveProviderOperationTimeoutMs } from "openclaw/plugin-sdk/provider-http";
|
|
3
3
|
import { fetchWithSsrFGuard, resolvePinnedHostnameWithPolicy, ssrfPolicyFromHttpBaseUrlAllowedHostname } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
4
4
|
import { withTimeout } from "openclaw/plugin-sdk/text-utility-runtime";
|
package/dist/setup-plugin-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as clickClackMeta, g as clickClackConfigAdapter, m as clickClackConfigSchema, n as clickClackSetupAdapter, r as clickClackSetupContract, t as clickClackSetupWizard } from "./setup-surface-
|
|
1
|
+
import { _ as clickClackMeta, g as clickClackConfigAdapter, m as clickClackConfigSchema, n as clickClackSetupAdapter, r as clickClackSetupContract, t as clickClackSetupWizard } from "./setup-surface-CD6MUC1g.js";
|
|
2
2
|
//#region extensions/clickclack/src/channel.setup.ts
|
|
3
3
|
const clickClackSetupPlugin = {
|
|
4
4
|
id: "clickclack",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createAccountListHelpers, hasConfiguredAccountValue } from "openclaw/plugin-sdk/account-helpers";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
|
-
import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
|
|
4
3
|
import { resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution-runtime";
|
|
5
4
|
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
|
|
6
5
|
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
|
|
@@ -15,7 +14,7 @@ import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
|
|
|
15
14
|
import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
|
16
15
|
import { WebSocket } from "ws";
|
|
17
16
|
import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
|
|
18
|
-
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, applyAccountNameToChannelSection,
|
|
17
|
+
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, applyAccountNameToChannelSection, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, hasConfiguredSecretInput, migrateBaseNameToDefaultAccount, moveSingleAccountChannelSectionToDefaultAccount, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
|
19
18
|
import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup-runtime";
|
|
20
19
|
import net from "node:net";
|
|
21
20
|
//#region \0rolldown/runtime.js
|
|
@@ -39,8 +38,10 @@ const DEFAULT_RECONNECT_MS = 1500;
|
|
|
39
38
|
const MIN_RECONNECT_MS = 100;
|
|
40
39
|
const MAX_RECONNECT_MS = 6e4;
|
|
41
40
|
const DEFAULT_DISCUSSIONS_SECTION = "Sessions";
|
|
42
|
-
const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId } = createAccountListHelpers("clickclack", {
|
|
41
|
+
const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId, resolveAccountConfig: resolveMergedClickClackAccountConfig } = createAccountListHelpers("clickclack", {
|
|
43
42
|
normalizeAccountId,
|
|
43
|
+
omitKeys: ["defaultAccount"],
|
|
44
|
+
nestedObjectKeys: ["discussions"],
|
|
44
45
|
hasImplicitDefaultAccount: (cfg) => {
|
|
45
46
|
const channel = cfg.channels?.clickclack;
|
|
46
47
|
return Boolean(channel?.baseUrl?.trim() && (hasConfiguredAccountValue(channel.token) || Boolean(channel.tokenFile?.trim()) || Boolean(process.env.CLICKCLACK_BOT_TOKEN?.trim())) && channel.workspace?.trim());
|
|
@@ -48,14 +49,7 @@ const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resol
|
|
|
48
49
|
});
|
|
49
50
|
function resolveClickClackAccountConfig(cfg, accountId) {
|
|
50
51
|
const channel = cfg.channels?.clickclack;
|
|
51
|
-
const merged =
|
|
52
|
-
channelConfig: cfg.channels?.clickclack,
|
|
53
|
-
accounts: channel?.accounts,
|
|
54
|
-
accountId,
|
|
55
|
-
omitKeys: ["defaultAccount"],
|
|
56
|
-
nestedObjectKeys: ["discussions"],
|
|
57
|
-
normalizeAccountId
|
|
58
|
-
});
|
|
52
|
+
const merged = resolveMergedClickClackAccountConfig(cfg, accountId);
|
|
59
53
|
const account = resolveNormalizedAccountEntry(channel?.accounts, accountId, normalizeAccountId);
|
|
60
54
|
const accountTokenFile = account?.tokenFile?.trim();
|
|
61
55
|
if (accountTokenFile) return {
|
|
@@ -682,59 +676,24 @@ function applyClickClackSetupConfigPatch(params) {
|
|
|
682
676
|
accountId,
|
|
683
677
|
name: params.name
|
|
684
678
|
});
|
|
685
|
-
return
|
|
679
|
+
return patchScopedAccountConfig({
|
|
686
680
|
cfg: accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount({
|
|
687
681
|
cfg: namedConfig,
|
|
688
682
|
channelKey: channel$1
|
|
689
683
|
}) : namedConfig,
|
|
690
684
|
channelKey: channel$1,
|
|
691
685
|
accountId,
|
|
692
|
-
patch: params.patch
|
|
686
|
+
patch: params.patch,
|
|
687
|
+
...params.clearFields ? { clearFields: params.clearFields } : {}
|
|
693
688
|
});
|
|
694
689
|
}
|
|
695
|
-
function clearClickClackSetupConfigFields(params) {
|
|
696
|
-
const clickclack = params.cfg.channels?.clickclack;
|
|
697
|
-
if (!clickclack) return params.cfg;
|
|
698
|
-
const accountId = normalizeAccountId(params.accountId);
|
|
699
|
-
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
700
|
-
const nextClickClack = { ...clickclack };
|
|
701
|
-
for (const field of params.fields) delete nextClickClack[field];
|
|
702
|
-
return {
|
|
703
|
-
...params.cfg,
|
|
704
|
-
channels: {
|
|
705
|
-
...params.cfg.channels,
|
|
706
|
-
clickclack: nextClickClack
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
const currentAccount = clickclack.accounts?.[accountId];
|
|
711
|
-
if (!currentAccount) return params.cfg;
|
|
712
|
-
const nextAccount = { ...currentAccount };
|
|
713
|
-
for (const field of params.fields) delete nextAccount[field];
|
|
714
|
-
return {
|
|
715
|
-
...params.cfg,
|
|
716
|
-
channels: {
|
|
717
|
-
...params.cfg.channels,
|
|
718
|
-
clickclack: {
|
|
719
|
-
...clickclack,
|
|
720
|
-
accounts: {
|
|
721
|
-
...clickclack.accounts,
|
|
722
|
-
[accountId]: nextAccount
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
690
|
function applyClickClackCredentialConfig(params) {
|
|
729
691
|
const fieldsToClear = params.useEnv ? ["token", "tokenFile"] : params.tokenFile ? ["token"] : params.token !== void 0 ? ["tokenFile"] : [];
|
|
730
|
-
return
|
|
731
|
-
cfg:
|
|
732
|
-
cfg: params.cfg,
|
|
733
|
-
accountId: params.accountId,
|
|
734
|
-
patch: params.useEnv ? {} : params.tokenFile ? { tokenFile: params.tokenFile } : params.token !== void 0 ? { token: params.token } : {}
|
|
735
|
-
}),
|
|
692
|
+
return applyClickClackSetupConfigPatch({
|
|
693
|
+
cfg: params.cfg,
|
|
736
694
|
accountId: params.accountId,
|
|
737
|
-
|
|
695
|
+
clearFields: fieldsToClear,
|
|
696
|
+
patch: params.useEnv ? {} : params.tokenFile ? { tokenFile: params.tokenFile } : params.token !== void 0 ? { token: params.token } : {}
|
|
738
697
|
});
|
|
739
698
|
}
|
|
740
699
|
const clickClackSetupAdapter = {
|
|
@@ -751,7 +710,7 @@ const clickClackSetupAdapter = {
|
|
|
751
710
|
const claimUrl = setup.exactClaimUrl && !privateApiBaseUrl ? setup.exactClaimUrl : buildClickClackSetupClaimUrl(privateApiBaseUrl ?? setup.baseUrl);
|
|
752
711
|
let claim;
|
|
753
712
|
try {
|
|
754
|
-
const { claimClickClackSetupCode } = await import("./setup-claim-
|
|
713
|
+
const { claimClickClackSetupCode } = await import("./setup-claim-BFU16zoo.js");
|
|
755
714
|
claim = await claimClickClackSetupCode({
|
|
756
715
|
claimUrl,
|
|
757
716
|
code: setup.code,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/clickclack",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.5",
|
|
4
4
|
"description": "OpenClaw ClickClack channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"zod": "4.4.3"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"openclaw": ">=2026.7.2-beta.
|
|
16
|
+
"openclaw": ">=2026.7.2-beta.5"
|
|
17
17
|
},
|
|
18
18
|
"peerDependenciesMeta": {
|
|
19
19
|
"openclaw": {
|
|
@@ -136,10 +136,10 @@
|
|
|
136
136
|
"allowInvalidConfigRecovery": true
|
|
137
137
|
},
|
|
138
138
|
"compat": {
|
|
139
|
-
"pluginApi": ">=2026.7.2-beta.
|
|
139
|
+
"pluginApi": ">=2026.7.2-beta.5"
|
|
140
140
|
},
|
|
141
141
|
"build": {
|
|
142
|
-
"openclawVersion": "2026.7.2-beta.
|
|
142
|
+
"openclawVersion": "2026.7.2-beta.5",
|
|
143
143
|
"bundledDist": false
|
|
144
144
|
},
|
|
145
145
|
"release": {
|
|
@@ -158,7 +158,6 @@
|
|
|
158
158
|
"files": [
|
|
159
159
|
"dist/**",
|
|
160
160
|
"openclaw.plugin.json",
|
|
161
|
-
"npm-shrinkwrap.json",
|
|
162
161
|
"README.md"
|
|
163
162
|
],
|
|
164
163
|
"bundledDependencies": [
|
package/npm-shrinkwrap.json
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@openclaw/clickclack",
|
|
3
|
-
"version": "2026.7.2-beta.4",
|
|
4
|
-
"lockfileVersion": 3,
|
|
5
|
-
"requires": true,
|
|
6
|
-
"packages": {
|
|
7
|
-
"": {
|
|
8
|
-
"name": "@openclaw/clickclack",
|
|
9
|
-
"version": "2026.7.2-beta.4",
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"ws": "8.21.1",
|
|
12
|
-
"zod": "4.4.3"
|
|
13
|
-
},
|
|
14
|
-
"peerDependencies": {
|
|
15
|
-
"openclaw": ">=2026.7.2-beta.4"
|
|
16
|
-
},
|
|
17
|
-
"peerDependenciesMeta": {
|
|
18
|
-
"openclaw": {
|
|
19
|
-
"optional": true
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
"node_modules/ws": {
|
|
24
|
-
"version": "8.21.1",
|
|
25
|
-
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
|
26
|
-
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
|
27
|
-
"license": "MIT",
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=10.0.0"
|
|
30
|
-
},
|
|
31
|
-
"peerDependencies": {
|
|
32
|
-
"bufferutil": "^4.0.1",
|
|
33
|
-
"utf-8-validate": ">=5.0.2"
|
|
34
|
-
},
|
|
35
|
-
"peerDependenciesMeta": {
|
|
36
|
-
"bufferutil": {
|
|
37
|
-
"optional": true
|
|
38
|
-
},
|
|
39
|
-
"utf-8-validate": {
|
|
40
|
-
"optional": true
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"node_modules/zod": {
|
|
45
|
-
"version": "4.4.3",
|
|
46
|
-
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
|
47
|
-
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
|
48
|
-
"license": "MIT",
|
|
49
|
-
"funding": {
|
|
50
|
-
"url": "https://github.com/sponsors/colinhacks"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|