mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 20:00:22 +00:00
Outbound: move target resolution heuristics behind plugins
This commit is contained in:
committed by
Val Alexander
parent
50a2be72fe
commit
0d438921ea
@@ -7,6 +7,8 @@ let resetDirectoryCache: TargetResolverModule["resetDirectoryCache"];
|
||||
let resolveMessagingTarget: TargetResolverModule["resolveMessagingTarget"];
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listPeers: vi.fn(),
|
||||
listPeersLive: vi.fn(),
|
||||
listGroups: vi.fn(),
|
||||
listGroupsLive: vi.fn(),
|
||||
resolveTarget: vi.fn(),
|
||||
@@ -16,6 +18,8 @@ const mocks = vi.hoisted(() => ({
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
mocks.listPeers.mockReset();
|
||||
mocks.listPeersLive.mockReset();
|
||||
mocks.listGroups.mockReset();
|
||||
mocks.listGroupsLive.mockReset();
|
||||
mocks.resolveTarget.mockReset();
|
||||
@@ -39,6 +43,8 @@ describe("resolveMessagingTarget (directory fallback)", () => {
|
||||
resetDirectoryCache();
|
||||
mocks.getChannelPlugin.mockReturnValue({
|
||||
directory: {
|
||||
listPeers: mocks.listPeers,
|
||||
listPeersLive: mocks.listPeersLive,
|
||||
listGroups: mocks.listGroups,
|
||||
listGroupsLive: mocks.listGroupsLive,
|
||||
},
|
||||
@@ -134,4 +140,51 @@ describe("resolveMessagingTarget (directory fallback)", () => {
|
||||
expect(mocks.listGroups).not.toHaveBeenCalled();
|
||||
expect(mocks.listGroupsLive).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses plugin chat-type inference for directory lookups and plugin fallback on miss", async () => {
|
||||
mocks.getChannelPlugin.mockReturnValue({
|
||||
directory: {
|
||||
listPeers: mocks.listPeers,
|
||||
listPeersLive: mocks.listPeersLive,
|
||||
},
|
||||
messaging: {
|
||||
inferTargetChatType: () => "direct",
|
||||
targetResolver: {
|
||||
looksLikeId: () => false,
|
||||
resolveTarget: mocks.resolveTarget,
|
||||
},
|
||||
},
|
||||
});
|
||||
mocks.listPeers.mockResolvedValue([]);
|
||||
mocks.listPeersLive.mockResolvedValue([]);
|
||||
mocks.resolveTarget.mockResolvedValue({
|
||||
to: "+15551234567",
|
||||
kind: "user",
|
||||
source: "normalized",
|
||||
});
|
||||
|
||||
const result = await resolveMessagingTarget({
|
||||
cfg,
|
||||
channel: "imessage",
|
||||
input: "+15551234567",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.target).toEqual({
|
||||
to: "+15551234567",
|
||||
kind: "user",
|
||||
source: "normalized",
|
||||
display: undefined,
|
||||
});
|
||||
}
|
||||
expect(mocks.listPeers).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.listPeersLive).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.listGroups).not.toHaveBeenCalled();
|
||||
expect(mocks.resolveTarget).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: "+15551234567",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,23 @@ export async function maybeResolveIdLikeTarget(params: {
|
||||
accountId?: string | null;
|
||||
preferredKind?: TargetResolveKind;
|
||||
}): Promise<ResolvedMessagingTarget | undefined> {
|
||||
const raw = normalizeChannelTargetInput(params.input);
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
return await maybeResolvePluginTarget(params, { requireIdLike: true });
|
||||
}
|
||||
|
||||
async function maybeResolvePluginTarget(
|
||||
params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: ChannelId;
|
||||
input: string;
|
||||
accountId?: string | null;
|
||||
preferredKind?: TargetResolveKind;
|
||||
},
|
||||
options?: { requireIdLike?: boolean },
|
||||
): Promise<ResolvedMessagingTarget | undefined> {
|
||||
const raw = normalizeChannelTargetInput(params.input);
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
@@ -57,7 +74,7 @@ export async function maybeResolveIdLikeTarget(params: {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw;
|
||||
if (resolver.looksLikeId && !resolver.looksLikeId(raw, normalized)) {
|
||||
if (options?.requireIdLike && resolver.looksLikeId && !resolver.looksLikeId(raw, normalized)) {
|
||||
return undefined;
|
||||
}
|
||||
const resolved = await resolver.resolveTarget({
|
||||
@@ -196,6 +213,16 @@ function detectTargetKind(
|
||||
if (!trimmed) {
|
||||
return "group";
|
||||
}
|
||||
const inferredChatType = getChannelPlugin(channel)?.messaging?.inferTargetChatType?.({ to: raw });
|
||||
if (inferredChatType === "direct") {
|
||||
return "user";
|
||||
}
|
||||
if (inferredChatType === "channel") {
|
||||
return "channel";
|
||||
}
|
||||
if (inferredChatType === "group") {
|
||||
return "group";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("@") || /^<@!?/.test(trimmed) || /^user:/i.test(trimmed)) {
|
||||
return "user";
|
||||
@@ -204,11 +231,6 @@ function detectTargetKind(
|
||||
return "group";
|
||||
}
|
||||
|
||||
// For some channels (e.g., BlueBubbles/iMessage), bare phone numbers are almost always DM targets.
|
||||
if ((channel === "bluebubbles" || channel === "imessage") && /^\+?\d{6,}$/.test(trimmed)) {
|
||||
return "user";
|
||||
}
|
||||
|
||||
return "group";
|
||||
}
|
||||
|
||||
@@ -410,11 +432,6 @@ export async function resolveMessagingTarget(params: {
|
||||
return true;
|
||||
}
|
||||
if (/^\+?\d{6,}$/.test(trimmed)) {
|
||||
// BlueBubbles/iMessage phone numbers should usually resolve via the directory to a DM chat,
|
||||
// otherwise the provider may pick an existing group containing that handle.
|
||||
if (params.channel === "bluebubbles" || params.channel === "imessage") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes("@thread")) {
|
||||
@@ -491,18 +508,18 @@ export async function resolveMessagingTarget(params: {
|
||||
candidates: match.entries,
|
||||
};
|
||||
}
|
||||
// For iMessage-style channels, allow sending directly to the normalized handle
|
||||
// even if the directory doesn't contain an entry yet.
|
||||
if (
|
||||
(params.channel === "bluebubbles" || params.channel === "imessage") &&
|
||||
/^\+?\d{6,}$/.test(query)
|
||||
) {
|
||||
return buildNormalizedResolveResult({
|
||||
channel: params.channel,
|
||||
raw,
|
||||
normalized,
|
||||
kind,
|
||||
});
|
||||
const resolvedFallbackTarget = await maybeResolvePluginTarget({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
input: raw,
|
||||
accountId: params.accountId,
|
||||
preferredKind: params.preferredKind,
|
||||
});
|
||||
if (resolvedFallbackTarget) {
|
||||
return {
|
||||
ok: true,
|
||||
target: resolvedFallbackTarget,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user