openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
115 lines (114 loc) • 5.75 kB
JavaScript
import { c as normalizeOptionalString } from "../../string-coerce-mnp54Vah.js";
import { t as escapeRegExp } from "../../regexp-BZyMFTlj.js";
import { r as fetchWithSsrFGuard } from "../../fetch-guard-BttkNCLm.js";
import "../../string-coerce-runtime-CEGJWkQ_.js";
import "../../text-utility-runtime-BZ3jGr-e.js";
import { t as definePluginEntry } from "../../plugin-entry-C7DUzV0e.js";
import { n as resolveLivePluginConfigObject } from "../../plugin-config-runtime-CpE6DYgJ.js";
import { u as ssrfPolicyFromDangerouslyAllowPrivateNetwork } from "../../ssrf-policy-sZLyyYht.js";
import "../../api-BO_xD0Zj.js";
//#region extensions/thread-ownership/index.ts
const mentionedThreads = /* @__PURE__ */ new Map();
const MENTION_TTL_MS = 300 * 1e3;
function isThreadOwnershipConfig(value) {
return value !== null && typeof value === "object";
}
function resolveThreadToken(value) {
return typeof value === "string" || typeof value === "number" ? String(value) : "";
}
function resolveSlackConversationId(value) {
const raw = normalizeOptionalString(value) ?? "";
if (!raw) return "";
const trimmed = raw.trim();
const resolved = /^(?:slack:)?channel:(.+)$/i.exec(trimmed)?.[1]?.trim() || trimmed;
return /^[CDGUW][A-Z0-9]+$/i.test(resolved) ? resolved.toUpperCase() : resolved;
}
function cleanExpiredMentions() {
const now = Date.now();
for (const [key, ts] of mentionedThreads) if (now - ts > MENTION_TTL_MS) mentionedThreads.delete(key);
}
function containsAgentNameMention(text, agentName) {
const trimmedName = agentName.trim();
if (!trimmedName) return false;
return new RegExp(`(^|[^\\w])@${escapeRegExp(trimmedName)}(?=$|[^\\w])`, "i").test(text);
}
function resolveOwnershipAgent(config) {
const list = Array.isArray(config.agents?.list) ? config.agents.list.filter((entry) => entry !== null && typeof entry === "object") : [];
const selected = list.find((entry) => entry.default === true) ?? list[0];
const id = normalizeOptionalString(selected?.id) ?? "unknown";
const identityName = normalizeOptionalString(selected?.identity?.name) ?? "";
const fallbackName = normalizeOptionalString(selected?.name) ?? "";
return {
id,
name: identityName || fallbackName
};
}
var thread_ownership_default = definePluginEntry({
id: "thread-ownership",
name: "Thread Ownership",
description: "Slack thread claim coordination for multi-agent setups",
register(api) {
const resolveCurrentState = () => {
const currentConfig = api.runtime.config?.current?.() ?? api.config;
const livePluginCfg = resolveLivePluginConfigObject(api.runtime.config?.current ? () => api.runtime.config.current() : void 0, "thread-ownership", isThreadOwnershipConfig(api.pluginConfig) ? api.pluginConfig : void 0);
const pluginCfg = isThreadOwnershipConfig(livePluginCfg) ? livePluginCfg : {};
return {
currentConfig,
forwarderUrl: (pluginCfg.forwarderUrl ?? process.env.SLACK_FORWARDER_URL ?? "http://slack-forwarder:8750").replace(/\/$/, ""),
abTestChannels: new Set((pluginCfg.abTestChannels ?? process.env.THREAD_OWNERSHIP_CHANNELS?.split(",").filter(Boolean) ?? []).map((entry) => resolveSlackConversationId(entry)).filter(Boolean)),
botUserId: process.env.SLACK_BOT_USER_ID ?? "",
agent: resolveOwnershipAgent(currentConfig)
};
};
api.on("message_received", async (event, ctx) => {
if (ctx.channelId !== "slack") return;
const { agent, botUserId } = resolveCurrentState();
const text = event.content ?? "";
const threadTs = resolveThreadToken(event.threadId) || resolveThreadToken(event.metadata?.threadId) || resolveThreadToken(event.metadata?.threadTs);
const channelId = resolveSlackConversationId(ctx.conversationId) || resolveSlackConversationId(event.metadata?.channelId) || "";
if (!threadTs || !channelId) return;
if (containsAgentNameMention(text, agent.name) || botUserId && text.includes(`<@${botUserId}>`)) {
cleanExpiredMentions();
mentionedThreads.set(`${channelId}:${threadTs}`, Date.now());
}
});
api.on("message_sending", async (event, ctx) => {
if (ctx.channelId !== "slack") return;
const { abTestChannels, agent, forwarderUrl } = resolveCurrentState();
const threadTs = resolveThreadToken(event.replyToId) || resolveThreadToken(event.threadId) || resolveThreadToken(event.metadata?.threadId) || resolveThreadToken(event.metadata?.threadTs);
const channelId = resolveSlackConversationId(ctx.conversationId) || resolveSlackConversationId(event.metadata?.channelId) || resolveSlackConversationId(event.to) || "";
if (!threadTs || !channelId) return;
if (abTestChannels.size > 0 && !abTestChannels.has(channelId)) return;
cleanExpiredMentions();
if (mentionedThreads.has(`${channelId}:${threadTs}`)) return;
try {
const { response: resp, release } = await fetchWithSsrFGuard({
url: `${forwarderUrl}/api/v1/ownership/${channelId}/${threadTs}`,
init: {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_id: agent.id })
},
timeoutMs: 3e3,
policy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(true),
auditContext: "thread-ownership"
});
try {
if (resp.ok) return;
if (resp.status === 409) {
const body = await resp.json();
api.logger.info?.(`thread-ownership: cancelled send to ${channelId}:${threadTs} — owned by ${body.owner}`);
return { cancel: true };
}
api.logger.warn?.(`thread-ownership: unexpected status ${resp.status}, allowing send`);
} finally {
await release();
}
} catch (err) {
api.logger.warn?.(`thread-ownership: ownership check failed (${String(err)}), allowing send`);
}
});
}
});
//#endregion
export { thread_ownership_default as default };