mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:20:43 +00:00
fix(slack): honor focused thread bindings
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
import type { App } from "@slack/bolt";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
registerSessionBindingAdapter,
|
||||
unregisterSessionBindingAdapter,
|
||||
type SessionBindingAdapter,
|
||||
type SessionBindingRecord,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
||||
import { expectChannelInboundContextContract as expectInboundContextContract } from "openclaw/plugin-sdk/testing";
|
||||
@@ -592,6 +598,76 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
// MessageThreadId should be set for the reply
|
||||
expect(prepared!.ctxPayload.MessageThreadId).toBe("500.000");
|
||||
});
|
||||
|
||||
it("routes Slack thread replies through runtime conversation bindings", async () => {
|
||||
const targetSessionKey = "agent:review:acp:session-67739";
|
||||
const binding: SessionBindingRecord = {
|
||||
bindingId: "test-binding",
|
||||
targetSessionKey,
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "100.000",
|
||||
parentConversationId: "C123",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: Date.now(),
|
||||
metadata: {},
|
||||
};
|
||||
const resolveByConversation: SessionBindingAdapter["resolveByConversation"] = vi.fn((ref) =>
|
||||
ref.channel === "slack" &&
|
||||
ref.accountId === "default" &&
|
||||
ref.conversationId === "100.000" &&
|
||||
ref.parentConversationId === "C123"
|
||||
? binding
|
||||
: null,
|
||||
);
|
||||
const touch: NonNullable<SessionBindingAdapter["touch"]> = vi.fn();
|
||||
const adapter: SessionBindingAdapter = {
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
listBySession: () => [],
|
||||
resolveByConversation,
|
||||
touch,
|
||||
};
|
||||
registerSessionBindingAdapter(adapter);
|
||||
try {
|
||||
const replies = vi.fn().mockResolvedValue({
|
||||
messages: [{ text: "starter", user: "U2", ts: "100.000" }],
|
||||
response_metadata: { next_cursor: "" },
|
||||
});
|
||||
const slackCtx = createThreadSlackCtx({
|
||||
cfg: {
|
||||
channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } },
|
||||
} as OpenClawConfig,
|
||||
replies,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
|
||||
const prepared = await prepareThreadMessage(slackCtx, {
|
||||
text: "bound reply",
|
||||
ts: "101.000",
|
||||
thread_ts: "100.000",
|
||||
});
|
||||
|
||||
expect(prepared).toBeTruthy();
|
||||
expect(prepared!.route.sessionKey).toBe(targetSessionKey);
|
||||
expect(prepared!.route.agentId).toBe("review");
|
||||
expect(prepared!.ctxPayload.SessionKey).toBe(targetSessionKey);
|
||||
expect(prepared!.ctxPayload.ParentSessionKey).toBeUndefined();
|
||||
expect(resolveByConversation).toHaveBeenCalledWith({
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "100.000",
|
||||
parentConversationId: "C123",
|
||||
});
|
||||
expect(touch).toHaveBeenCalledWith("test-binding", undefined);
|
||||
} finally {
|
||||
unregisterSessionBindingAdapter({ channel: "slack", accountId: "default", adapter });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareSlackMessage sender prefix", () => {
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
|
||||
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth";
|
||||
import { shouldHandleTextCommands } from "openclaw/plugin-sdk/command-auth";
|
||||
import {
|
||||
resolveRuntimeConversationBindingRoute,
|
||||
type RuntimeConversationBindingRouteResult,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import {
|
||||
@@ -106,6 +110,7 @@ type SlackAuthorizationContext = {
|
||||
|
||||
type SlackRoutingContext = {
|
||||
route: ReturnType<typeof resolveAgentRoute>;
|
||||
runtimeBinding: RuntimeConversationBindingRouteResult["bindingRecord"];
|
||||
chatType: "direct" | "group" | "channel";
|
||||
replyToMode: ReturnType<typeof resolveSlackReplyToMode>;
|
||||
threadContext: ReturnType<typeof resolveSlackThreadContext>;
|
||||
@@ -116,6 +121,15 @@ type SlackRoutingContext = {
|
||||
historyKey: string;
|
||||
};
|
||||
|
||||
function resolveSlackBaseConversationId(params: {
|
||||
message: SlackMessageEvent;
|
||||
isDirectMessage: boolean;
|
||||
}): string {
|
||||
return params.isDirectMessage
|
||||
? `user:${params.message.user ?? "unknown"}`
|
||||
: params.message.channel;
|
||||
}
|
||||
|
||||
async function resolveSlackConversationContext(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
account: ResolvedSlackAccount;
|
||||
@@ -272,7 +286,7 @@ function resolveSlackRoutingContext(params: {
|
||||
isRoomish: boolean;
|
||||
}): SlackRoutingContext {
|
||||
const { ctx, account, message, isDirectMessage, isGroupDm, isRoom, isRoomish } = params;
|
||||
const route = resolveAgentRoute({
|
||||
let route = resolveAgentRoute({
|
||||
cfg: ctx.cfg,
|
||||
channel: "slack",
|
||||
accountId: account.accountId,
|
||||
@@ -302,17 +316,45 @@ function resolveSlackRoutingContext(params: {
|
||||
// isolated sessions per message (regression from #10686).
|
||||
const roomThreadId = isThreadReply && threadTs ? threadTs : undefined;
|
||||
const canonicalThreadId = isRoomish ? roomThreadId : isThreadReply ? threadTs : autoThreadId;
|
||||
const threadKeys = resolveThreadSessionKeys({
|
||||
baseSessionKey: route.sessionKey,
|
||||
threadId: canonicalThreadId,
|
||||
parentSessionKey: canonicalThreadId && ctx.threadInheritParent ? route.sessionKey : undefined,
|
||||
});
|
||||
const baseConversationId = resolveSlackBaseConversationId({ message, isDirectMessage });
|
||||
const boundThreadRoute = canonicalThreadId
|
||||
? resolveRuntimeConversationBindingRoute({
|
||||
route,
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: account.accountId,
|
||||
conversationId: canonicalThreadId,
|
||||
parentConversationId: baseConversationId,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
const runtimeRoute =
|
||||
boundThreadRoute?.boundSessionKey || boundThreadRoute?.bindingRecord
|
||||
? boundThreadRoute
|
||||
: resolveRuntimeConversationBindingRoute({
|
||||
route,
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: account.accountId,
|
||||
conversationId: baseConversationId,
|
||||
},
|
||||
});
|
||||
route = runtimeRoute.route;
|
||||
const threadKeys = runtimeRoute.boundSessionKey
|
||||
? { sessionKey: route.sessionKey, parentSessionKey: undefined }
|
||||
: resolveThreadSessionKeys({
|
||||
baseSessionKey: route.sessionKey,
|
||||
threadId: canonicalThreadId,
|
||||
parentSessionKey:
|
||||
canonicalThreadId && ctx.threadInheritParent ? route.sessionKey : undefined,
|
||||
});
|
||||
const sessionKey = threadKeys.sessionKey;
|
||||
const historyKey =
|
||||
isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel;
|
||||
|
||||
return {
|
||||
route,
|
||||
runtimeBinding: runtimeRoute.bindingRecord,
|
||||
chatType,
|
||||
replyToMode,
|
||||
threadContext,
|
||||
@@ -364,6 +406,7 @@ export async function prepareSlackMessage(params: {
|
||||
});
|
||||
const {
|
||||
route,
|
||||
runtimeBinding,
|
||||
replyToMode,
|
||||
threadContext,
|
||||
threadTs,
|
||||
@@ -372,6 +415,11 @@ export async function prepareSlackMessage(params: {
|
||||
sessionKey,
|
||||
historyKey,
|
||||
} = routing;
|
||||
if (runtimeBinding && shouldLogVerbose()) {
|
||||
logVerbose(
|
||||
`slack: routed via bound conversation ${runtimeBinding.conversation.conversationId} -> ${runtimeBinding.targetSessionKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
const mentionRegexes = resolveCachedMentionRegexes(ctx, route.agentId);
|
||||
const hasAnyMention = /<@[^>]+>/.test(message.text ?? "");
|
||||
|
||||
Reference in New Issue
Block a user