From 2c94b42434a0618aea29d99195c1983c5e168873 Mon Sep 17 00:00:00 2001 From: scotthuang Date: Thu, 27 Aug 2026 19:40:00 +0800 Subject: [PATCH] fix: expose exact outbound session routes --- src/channel.test.ts | 60 +++++++++++++++++++++++++++++++++++++++++++++ src/channel.ts | 41 +++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/channel.test.ts diff --git a/src/channel.test.ts b/src/channel.test.ts new file mode 100644 index 0000000..a4b0d58 --- /dev/null +++ b/src/channel.test.ts @@ -0,0 +1,60 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { registerWeixinAccountId } from "./auth/accounts.js"; +import { weixinPlugin } from "./channel.js"; + +let stateDir: string; + +beforeEach(() => { + stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "weixin-session-route-")); + process.env.OPENCLAW_STATE_DIR = stateDir; +}); + +afterEach(() => { + delete process.env.OPENCLAW_STATE_DIR; + fs.rmSync(stateDir, { recursive: true, force: true }); +}); + +async function resolveRoute(params: { target: string; accountId?: string }) { + const resolver = weixinPlugin.messaging?.resolveOutboundSessionRoute; + if (!resolver) throw new Error("expected outbound session route resolver"); + return await resolver({ + cfg: { session: { dmScope: "per-account-channel-peer" } }, + agentId: "main", + target: params.target, + accountId: params.accountId, + }); +} + +describe("Weixin outbound session route", () => { + it("certifies an explicit account and canonical Weixin user", async () => { + const route = await resolveRoute({ + target: "openclaw-weixin:user:Alice@im.wechat", + accountId: "Bot@One", + }); + + expect(route).toMatchObject({ + sessionKey: "agent:main:openclaw-weixin:bot-one:direct:alice@im.wechat", + recipientSessionExact: true, + peer: { kind: "direct", id: "Alice@im.wechat" }, + to: "Alice@im.wechat", + }); + }); + + it("uses the only registered account instead of the default account", async () => { + registerWeixinAccountId("bot-one"); + + const route = await resolveRoute({ target: "alice@im.wechat" }); + + expect(route).toMatchObject({ + sessionKey: "agent:main:openclaw-weixin:bot-one:direct:alice@im.wechat", + recipientSessionExact: true, + }); + }); + + it("rejects targets that cannot identify an inbound Weixin session", async () => { + await expect(resolveRoute({ target: "group:alice@im.wechat" })).resolves.toBeNull(); + }); +}); diff --git a/src/channel.ts b/src/channel.ts index 2a084df..88b128d 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,6 +1,14 @@ import path from "node:path"; -import type { ChannelPlugin, OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core"; +import { + buildChannelOutboundSessionRoute, + stripChannelTargetPrefix, + stripTargetKindPrefix, + type ChannelOutboundSessionRouteParams, + type ChannelPlugin, + type OpenClawConfig, + type PluginRuntime, +} from "openclaw/plugin-sdk/core"; import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/infra-runtime"; @@ -106,6 +114,34 @@ function resolveOutboundAccountId( ); } +function normalizeWeixinUserId(raw: string): string | null { + const channelTarget = stripChannelTargetPrefix(raw.trim(), "openclaw-weixin"); + const userId = stripTargetKindPrefix(channelTarget).trim(); + if (userId !== channelTarget.trim() && !/^(user|dm):/i.test(channelTarget)) return null; + return userId.endsWith("@im.wechat") ? userId : null; +} + +function resolveWeixinOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) { + const userId = normalizeWeixinUserId(params.target); + if (!userId) return null; + const accountId = params.accountId?.trim() + ? resolveWeixinAccount(params.cfg, params.accountId).accountId + : resolveOutboundAccountId(params.cfg, userId); + return { + ...buildChannelOutboundSessionRoute({ + cfg: params.cfg, + agentId: params.agentId, + channel: "openclaw-weixin", + accountId, + peer: { kind: "direct", id: userId }, + chatType: "direct", + from: userId, + to: userId, + }), + recipientSessionExact: true as const, + }; +} + async function sendWeixinOutbound(params: { cfg: OpenClawConfig; to: string; @@ -190,8 +226,9 @@ export const weixinPlugin: ChannelPlugin = { messaging: { targetResolver: { // Weixin user IDs always end with @im.wechat; treat as direct IDs, skip directory lookup. - looksLikeId: (raw) => raw.endsWith("@im.wechat"), + looksLikeId: (raw) => normalizeWeixinUserId(raw) !== null, }, + resolveOutboundSessionRoute: resolveWeixinOutboundSessionRoute, }, agentPrompt: { messageToolHints: () => [