mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 22:21:33 +00:00
refactor: route session target matching through plugin parsers
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { parseExplicitTargetForChannel } from "./target-parsing.js";
|
||||
import {
|
||||
comparableChannelTargetsMatch,
|
||||
comparableChannelTargetsShareRoute,
|
||||
parseExplicitTargetForChannel,
|
||||
resolveComparableTargetForChannel,
|
||||
} from "./target-parsing.js";
|
||||
|
||||
function parseTelegramTargetForTest(raw: string): {
|
||||
to: string;
|
||||
@@ -111,4 +116,42 @@ describe("parseExplicitTargetForChannel", () => {
|
||||
chatType: "direct",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds comparable targets from plugin-owned grammar", () => {
|
||||
expect(
|
||||
resolveComparableTargetForChannel({
|
||||
channel: "telegram",
|
||||
rawTarget: "telegram:group:-100123:topic:77",
|
||||
}),
|
||||
).toEqual({
|
||||
rawTo: "telegram:group:-100123:topic:77",
|
||||
to: "-100123",
|
||||
threadId: 77,
|
||||
chatType: "group",
|
||||
});
|
||||
});
|
||||
|
||||
it("matches comparable targets when only the plugin grammar differs", () => {
|
||||
const topicTarget = resolveComparableTargetForChannel({
|
||||
channel: "telegram",
|
||||
rawTarget: "telegram:-100123:topic:77",
|
||||
});
|
||||
const bareTarget = resolveComparableTargetForChannel({
|
||||
channel: "telegram",
|
||||
rawTarget: "-100123",
|
||||
});
|
||||
|
||||
expect(
|
||||
comparableChannelTargetsMatch({
|
||||
left: topicTarget,
|
||||
right: bareTarget,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
comparableChannelTargetsShareRoute({
|
||||
left: topicTarget,
|
||||
right: bareTarget,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,26 @@ export type ParsedChannelExplicitTarget = {
|
||||
chatType?: ChatType;
|
||||
};
|
||||
|
||||
export type ComparableChannelTarget = {
|
||||
rawTo: string;
|
||||
to: string;
|
||||
threadId?: string | number;
|
||||
chatType?: ChatType;
|
||||
};
|
||||
|
||||
function normalizeComparableThreadId(
|
||||
threadId?: string | number | null,
|
||||
): string | number | undefined {
|
||||
if (typeof threadId === "number") {
|
||||
return Number.isFinite(threadId) ? Math.trunc(threadId) : undefined;
|
||||
}
|
||||
if (typeof threadId !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = threadId.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function parseWithPlugin(
|
||||
rawChannel: string,
|
||||
rawTarget: string,
|
||||
@@ -25,3 +45,52 @@ export function parseExplicitTargetForChannel(
|
||||
): ParsedChannelExplicitTarget | null {
|
||||
return parseWithPlugin(channel, rawTarget);
|
||||
}
|
||||
|
||||
export function resolveComparableTargetForChannel(params: {
|
||||
channel: string;
|
||||
rawTarget?: string | null;
|
||||
fallbackThreadId?: string | number | null;
|
||||
}): ComparableChannelTarget | null {
|
||||
const rawTo = params.rawTarget?.trim();
|
||||
if (!rawTo) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseExplicitTargetForChannel(params.channel, rawTo);
|
||||
const fallbackThreadId = normalizeComparableThreadId(params.fallbackThreadId);
|
||||
return {
|
||||
rawTo,
|
||||
to: parsed?.to ?? rawTo,
|
||||
threadId: normalizeComparableThreadId(parsed?.threadId ?? fallbackThreadId),
|
||||
chatType: parsed?.chatType,
|
||||
};
|
||||
}
|
||||
|
||||
export function comparableChannelTargetsMatch(params: {
|
||||
left?: ComparableChannelTarget | null;
|
||||
right?: ComparableChannelTarget | null;
|
||||
}): boolean {
|
||||
const left = params.left;
|
||||
const right = params.right;
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
}
|
||||
return left.to === right.to && left.threadId === right.threadId;
|
||||
}
|
||||
|
||||
export function comparableChannelTargetsShareRoute(params: {
|
||||
left?: ComparableChannelTarget | null;
|
||||
right?: ComparableChannelTarget | null;
|
||||
}): boolean {
|
||||
const left = params.left;
|
||||
const right = params.right;
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
}
|
||||
if (left.to !== right.to) {
|
||||
return false;
|
||||
}
|
||||
if (left.threadId == null || right.threadId == null) {
|
||||
return true;
|
||||
}
|
||||
return left.threadId === right.threadId;
|
||||
}
|
||||
|
||||
@@ -817,6 +817,25 @@ describe("resolveSessionDeliveryTarget — cross-channel reply guard (#24152)",
|
||||
expect(resolved.threadId).toBe(1122);
|
||||
});
|
||||
|
||||
it("matches bare stored Telegram routes against topic-scoped turn routes via plugin grammar", () => {
|
||||
const resolved = resolveSessionDeliveryTarget({
|
||||
entry: {
|
||||
sessionId: "sess-forum-topic-mixed-shape",
|
||||
updatedAt: 1,
|
||||
lastChannel: "telegram",
|
||||
lastTo: "-1001234567890",
|
||||
lastThreadId: 1122,
|
||||
},
|
||||
requestedChannel: "last",
|
||||
turnSourceChannel: "telegram",
|
||||
turnSourceTo: "telegram:-1001234567890:topic:1122",
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("telegram");
|
||||
expect(resolved.to).toBe("telegram:-1001234567890:topic:1122");
|
||||
expect(resolved.threadId).toBe(1122);
|
||||
});
|
||||
|
||||
it("does not fall back to session lastThreadId when turnSourceChannel differs from session channel", () => {
|
||||
const resolved = resolveSessionDeliveryTarget({
|
||||
entry: {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { normalizeChatType, type ChatType } from "../../channels/chat-type.js";
|
||||
import {
|
||||
comparableChannelTargetsShareRoute,
|
||||
parseExplicitTargetForChannel,
|
||||
resolveComparableTargetForChannel,
|
||||
} from "../../channels/plugins/target-parsing.js";
|
||||
import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js";
|
||||
import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@@ -75,11 +80,7 @@ function parseExplicitTargetWithPlugin(params: {
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
resolveOutboundChannelPlugin({ channel: provider })?.messaging?.parseExplicitTarget?.({
|
||||
raw,
|
||||
}) ?? null
|
||||
);
|
||||
return parseExplicitTargetForChannel(provider, raw);
|
||||
}
|
||||
|
||||
export function resolveSessionDeliveryTarget(params: {
|
||||
@@ -113,12 +114,26 @@ export function resolveSessionDeliveryTarget(params: {
|
||||
const context = deliveryContextFromSession(params.entry);
|
||||
const sessionLastChannel =
|
||||
context?.channel && isDeliverableMessageChannel(context.channel) ? context.channel : undefined;
|
||||
const parsedSessionTarget = sessionLastChannel
|
||||
? resolveComparableTargetForChannel({
|
||||
channel: sessionLastChannel,
|
||||
rawTarget: context?.to,
|
||||
fallbackThreadId: context?.threadId,
|
||||
})
|
||||
: null;
|
||||
|
||||
// When a turn-source channel is provided, use only turn-scoped metadata.
|
||||
// Falling back to mutable session fields would re-introduce routing races.
|
||||
const hasTurnSourceChannel = params.turnSourceChannel != null;
|
||||
const hasTurnSourceThreadId =
|
||||
params.turnSourceThreadId != null && params.turnSourceThreadId !== "";
|
||||
const parsedTurnSourceTarget =
|
||||
hasTurnSourceChannel && params.turnSourceChannel
|
||||
? resolveComparableTargetForChannel({
|
||||
channel: params.turnSourceChannel,
|
||||
rawTarget: params.turnSourceTo,
|
||||
fallbackThreadId: params.turnSourceThreadId,
|
||||
})
|
||||
: null;
|
||||
const hasTurnSourceThreadId = parsedTurnSourceTarget?.threadId != null;
|
||||
const lastChannel = hasTurnSourceChannel ? params.turnSourceChannel : sessionLastChannel;
|
||||
const lastTo = hasTurnSourceChannel ? params.turnSourceTo : context?.to;
|
||||
const lastAccountId = hasTurnSourceChannel ? params.turnSourceAccountId : context?.accountId;
|
||||
@@ -126,13 +141,19 @@ export function resolveSessionDeliveryTarget(params: {
|
||||
// match the session context. This avoids mixing a turn-scoped `to` with a stale session-scoped
|
||||
// threadId from a different chat/topic in shared-session scenarios.
|
||||
const turnToMatchesSession =
|
||||
!params.turnSourceTo || !context?.to || params.turnSourceTo === context.to;
|
||||
!params.turnSourceTo ||
|
||||
!context?.to ||
|
||||
(params.turnSourceChannel === sessionLastChannel &&
|
||||
comparableChannelTargetsShareRoute({
|
||||
left: parsedTurnSourceTarget,
|
||||
right: parsedSessionTarget,
|
||||
}));
|
||||
const lastThreadId = hasTurnSourceThreadId
|
||||
? params.turnSourceThreadId
|
||||
? parsedTurnSourceTarget?.threadId
|
||||
: hasTurnSourceChannel &&
|
||||
(params.turnSourceChannel !== sessionLastChannel || !turnToMatchesSession)
|
||||
? undefined
|
||||
: context?.threadId;
|
||||
: parsedSessionTarget?.threadId;
|
||||
|
||||
const rawRequested = params.requestedChannel ?? "last";
|
||||
const requested = rawRequested === "last" ? "last" : normalizeMessageChannel(rawRequested);
|
||||
|
||||
Reference in New Issue
Block a user