mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(hooks): use live thread ownership config
This commit is contained in:
@@ -5,6 +5,7 @@ import register from "./index.js";
|
||||
describe("thread-ownership plugin", () => {
|
||||
const hooks: Record<string, Function> = {};
|
||||
const fetchMock = vi.fn() as unknown as typeof globalThis.fetch;
|
||||
let configFile: Record<string, unknown> = {};
|
||||
const api = {
|
||||
pluginConfig: {},
|
||||
config: {
|
||||
@@ -12,6 +13,11 @@ describe("thread-ownership plugin", () => {
|
||||
list: [{ id: "test-agent", default: true, identity: { name: "TestBot" } }],
|
||||
},
|
||||
},
|
||||
runtime: {
|
||||
config: {
|
||||
loadConfig: () => configFile,
|
||||
},
|
||||
},
|
||||
id: "thread-ownership",
|
||||
name: "Thread Ownership",
|
||||
logger: { info: vi.fn(), warn: vi.fn(), debug: vi.fn() },
|
||||
@@ -26,6 +32,9 @@ describe("thread-ownership plugin", () => {
|
||||
delete hooks[key];
|
||||
}
|
||||
api.pluginConfig = {};
|
||||
configFile = {
|
||||
agents: api.config.agents,
|
||||
};
|
||||
|
||||
process.env.SLACK_FORWARDER_URL = "http://localhost:8750";
|
||||
process.env.SLACK_BOT_USER_ID = "U999";
|
||||
@@ -173,6 +182,35 @@ describe("thread-ownership plugin", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses live runtime allowlists when deciding whether to claim ownership", async () => {
|
||||
api.pluginConfig = { abTestChannels: ["C123"] };
|
||||
configFile = {
|
||||
...configFile,
|
||||
plugins: {
|
||||
entries: {
|
||||
"thread-ownership": {
|
||||
config: {
|
||||
abTestChannels: ["C999"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
register.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
const result = await hooks.message_sending(
|
||||
{
|
||||
content: "hello",
|
||||
replyToId: "1234.5678",
|
||||
to: "C123",
|
||||
},
|
||||
{ channelId: "slack", conversationId: "C123" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels when thread owned by another agent", async () => {
|
||||
vi.mocked(globalThis.fetch).mockResolvedValue(
|
||||
new Response(JSON.stringify({ owner: "other-agent" }), { status: 409 }),
|
||||
@@ -326,6 +364,57 @@ describe("thread-ownership plugin", () => {
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the live runtime agent identity for ownership claims", async () => {
|
||||
configFile = {
|
||||
...configFile,
|
||||
agents: {
|
||||
list: [{ id: "live-agent", default: true, identity: { name: "LiveBot" } }],
|
||||
},
|
||||
};
|
||||
vi.mocked(globalThis.fetch).mockResolvedValue(
|
||||
new Response(JSON.stringify({ owner: "live-agent" }), { status: 200 }),
|
||||
);
|
||||
|
||||
await hooks.message_sending(
|
||||
{ content: "On it!", replyToId: "8888.0005", metadata: { channelId: "C789" }, to: "C789" },
|
||||
{ channelId: "slack", conversationId: "C789" },
|
||||
);
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8750/api/v1/ownership/C789/8888.0005",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({ agent_id: "live-agent" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the live runtime agent name for mention tracking", async () => {
|
||||
configFile = {
|
||||
...configFile,
|
||||
agents: {
|
||||
list: [{ id: "live-agent", default: true, identity: { name: "LiveBot" } }],
|
||||
},
|
||||
};
|
||||
|
||||
await hooks.message_received(
|
||||
{
|
||||
content: "hey @LiveBot help",
|
||||
threadId: "8888.0006",
|
||||
metadata: { channelId: "C789" },
|
||||
},
|
||||
{ channelId: "slack", conversationId: "C789" },
|
||||
);
|
||||
|
||||
const result = await hooks.message_sending(
|
||||
{ content: "On it!", replyToId: "8888.0006", metadata: { channelId: "C789" }, to: "C789" },
|
||||
{ channelId: "slack", conversationId: "C789" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not treat superset handles as agent-name mentions", async () => {
|
||||
await hooks.message_received(
|
||||
{
|
||||
|
||||
@@ -72,35 +72,56 @@ function resolveOwnershipAgent(config: OpenClawConfig): { id: string; name: stri
|
||||
return { id, name };
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function resolveThreadOwnershipPluginConfigFromConfig(
|
||||
config: OpenClawConfig,
|
||||
): ThreadOwnershipConfig | undefined {
|
||||
return asRecord(asRecord(config.plugins?.entries)?.["thread-ownership"])?.config as
|
||||
| ThreadOwnershipConfig
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "thread-ownership",
|
||||
name: "Thread Ownership",
|
||||
description: "Slack thread claim coordination for multi-agent setups",
|
||||
register(api: OpenClawPluginApi) {
|
||||
const pluginCfg = (api.pluginConfig ?? {}) as ThreadOwnershipConfig;
|
||||
const forwarderUrl = (
|
||||
pluginCfg.forwarderUrl ??
|
||||
process.env.SLACK_FORWARDER_URL ??
|
||||
"http://slack-forwarder:8750"
|
||||
).replace(/\/$/, "");
|
||||
|
||||
const abTestChannels = new Set(
|
||||
(
|
||||
pluginCfg.abTestChannels ??
|
||||
process.env.THREAD_OWNERSHIP_CHANNELS?.split(",").filter(Boolean) ??
|
||||
[]
|
||||
)
|
||||
.map((entry) => resolveSlackConversationId(entry))
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
const { id: agentId, name: agentName } = resolveOwnershipAgent(api.config);
|
||||
const botUserId = process.env.SLACK_BOT_USER_ID ?? "";
|
||||
const resolveCurrentState = () => {
|
||||
const currentConfig = api.runtime.config?.loadConfig?.() ?? api.config;
|
||||
const pluginCfg =
|
||||
resolveThreadOwnershipPluginConfigFromConfig(currentConfig) ||
|
||||
((api.pluginConfig ?? {}) as ThreadOwnershipConfig);
|
||||
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 =
|
||||
@@ -116,7 +137,7 @@ export default definePluginEntry({
|
||||
}
|
||||
|
||||
const mentioned =
|
||||
containsAgentNameMention(text, agentName) ||
|
||||
containsAgentNameMention(text, agent.name) ||
|
||||
(botUserId && text.includes(`<@${botUserId}>`));
|
||||
if (mentioned) {
|
||||
cleanExpiredMentions();
|
||||
@@ -128,6 +149,7 @@ export default definePluginEntry({
|
||||
if (ctx.channelId !== "slack") {
|
||||
return undefined;
|
||||
}
|
||||
const { abTestChannels, agent, forwarderUrl } = resolveCurrentState();
|
||||
|
||||
const threadTs =
|
||||
resolveThreadToken(event.replyToId) ||
|
||||
@@ -159,7 +181,7 @@ export default definePluginEntry({
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ agent_id: agentId }),
|
||||
body: JSON.stringify({ agent_id: agent.id }),
|
||||
},
|
||||
timeoutMs: 3000,
|
||||
policy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(true),
|
||||
|
||||
Reference in New Issue
Block a user