refactor(channels): centralize runtime binding routes

This commit is contained in:
Peter Steinberger
2026-04-22 23:16:44 +01:00
parent 85d2a9ec1f
commit f88da75ed9
13 changed files with 351 additions and 148 deletions

View File

@@ -262,7 +262,7 @@ const {
mockEnsureConfiguredBindingRouteReady: vi.fn(
async (_params?: unknown): Promise<BindingReadiness> => ({ ok: true }),
),
mockResolveBoundConversation: vi.fn(() => null as BoundConversation),
mockResolveBoundConversation: vi.fn((_ref?: unknown) => null as BoundConversation),
mockTouchBinding: vi.fn(),
mockResolveFeishuReasoningPreviewEnabled: vi.fn(() => false),
}));
@@ -297,6 +297,30 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
...actual,
resolveConfiguredBindingRoute: (params: unknown) =>
mockResolveConfiguredBindingRoute(params as { route: ResolvedAgentRoute }),
resolveRuntimeConversationBindingRoute: (params: {
route: ResolvedAgentRoute;
conversation: Parameters<
ReturnType<typeof actual.getSessionBindingService>["resolveByConversation"]
>[0];
}) => {
const bindingRecord = mockResolveBoundConversation(params.conversation);
const boundSessionKey = bindingRecord?.targetSessionKey?.trim();
if (!bindingRecord || !boundSessionKey) {
return { bindingRecord: null, route: params.route };
}
mockTouchBinding(bindingRecord.bindingId);
return {
bindingRecord,
boundSessionKey,
boundAgentId: params.route.agentId,
route: {
...params.route,
sessionKey: boundSessionKey,
lastRoutePolicy: boundSessionKey === params.route.mainSessionKey ? "main" : "session",
matchedBy: "binding.channel",
},
};
},
ensureConfiguredBindingRouteReady: (params: unknown) =>
mockEnsureConfiguredBindingRouteReady(params),
getSessionBindingService: () => ({

View File

@@ -2,8 +2,8 @@ import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pair
import {
ensureConfiguredBindingRouteReady,
resolveConfiguredBindingRoute,
resolveRuntimeConversationBindingRoute,
} from "openclaw/plugin-sdk/conversation-runtime";
import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
import { resolveAgentOutboundIdentity } from "openclaw/plugin-sdk/outbound-runtime";
import {
buildPendingHistoryContextFromMap,
@@ -12,8 +12,6 @@ import {
recordPendingHistoryEntryIfEnabled,
type HistoryEntry,
} from "openclaw/plugin-sdk/reply-history";
import { deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing";
import { resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
import {
resolveDefaultGroupPolicy,
resolveOpenProviderRuntimeGroupPolicy,
@@ -651,28 +649,22 @@ export async function handleFeishuMessage(params: {
// Bound Feishu conversations intentionally require an exact live conversation-id match.
// Sender-scoped topic sessions therefore bind on `chat:topic:root:sender:user`, while
// configured ACP bindings may still inherit the shared `chat:topic:root` topic session.
const threadBinding = getSessionBindingService().resolveByConversation({
channel: "feishu",
accountId: account.accountId,
conversationId: currentConversationId,
...(parentConversationId ? { parentConversationId } : {}),
const runtimeRoute = resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "feishu",
accountId: account.accountId,
conversationId: currentConversationId,
...(parentConversationId ? { parentConversationId } : {}),
},
});
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
if (threadBinding && boundSessionKey) {
route = {
...route,
sessionKey: boundSessionKey,
agentId: resolveAgentIdFromSessionKey(boundSessionKey) || route.agentId,
lastRoutePolicy: deriveLastRoutePolicy({
sessionKey: boundSessionKey,
mainSessionKey: route.mainSessionKey,
}),
matchedBy: "binding.channel",
};
route = runtimeRoute.route;
if (runtimeRoute.bindingRecord) {
configuredBinding = null;
getSessionBindingService().touch(threadBinding.bindingId);
log(
`feishu[${account.accountId}]: routed via bound conversation ${currentConversationId} -> ${boundSessionKey}`,
runtimeRoute.boundSessionKey
? `feishu[${account.accountId}]: routed via bound conversation ${currentConversationId} -> ${runtimeRoute.boundSessionKey}`
: `feishu[${account.accountId}]: plugin-bound conversation ${currentConversationId}`,
);
}
}

View File

@@ -45,7 +45,7 @@ type FeishuLifecycleTestMocks = {
monitorWebhookMock: AsyncUnknownMock;
createFeishuThreadBindingManagerMock: UnknownMock;
createFeishuReplyDispatcherMock: CreateFeishuReplyDispatcherMock;
resolveBoundConversationMock: Mock<() => BoundConversation | null>;
resolveBoundConversationMock: Mock<(ref?: unknown) => BoundConversation | null>;
touchBindingMock: UnknownMock;
resolveAgentRouteMock: UnknownMock;
resolveConfiguredBindingRouteMock: UnknownMock;
@@ -66,7 +66,7 @@ const feishuLifecycleTestMocks = vi.hoisted(
monitorWebhookMock: vi.fn(async () => {}),
createFeishuThreadBindingManagerMock: vi.fn(() => ({ stop: vi.fn() })),
createFeishuReplyDispatcherMock: vi.fn(),
resolveBoundConversationMock: vi.fn<() => BoundConversation | null>(() => null),
resolveBoundConversationMock: vi.fn<(ref?: unknown) => BoundConversation | null>(() => null),
touchBindingMock: vi.fn(),
resolveAgentRouteMock: vi.fn(),
resolveConfiguredBindingRouteMock: vi.fn(),
@@ -155,6 +155,36 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
resolveConfiguredBindingRouteMock.getMockImplementation()
? resolveConfiguredBindingRouteMock(params)
: actual.resolveConfiguredBindingRoute(params),
resolveRuntimeConversationBindingRoute: (
params: Parameters<typeof actual.resolveRuntimeConversationBindingRoute>[0],
) => {
const conversation =
"conversation" in params
? params.conversation
: {
channel: params.channel,
accountId: params.accountId,
conversationId: params.conversationId,
parentConversationId: params.parentConversationId,
};
const bindingRecord = resolveBoundConversationMock(conversation);
const boundSessionKey = bindingRecord?.targetSessionKey?.trim();
if (!bindingRecord || !boundSessionKey) {
return { bindingRecord: null, route: params.route };
}
touchBindingMock(bindingRecord.bindingId);
return {
bindingRecord,
boundSessionKey,
boundAgentId: params.route.agentId,
route: {
...params.route,
sessionKey: boundSessionKey,
lastRoutePolicy: boundSessionKey === params.route.mainSessionKey ? "main" : "session",
matchedBy: "binding.channel",
},
};
},
ensureConfiguredBindingRouteReady: (
params: Parameters<typeof actual.ensureConfiguredBindingRouteReady>[0],
) =>