Merge dbddfaeb54 into 09be5d45d5
This commit is contained in:
commit
ec4c4e2bc2
@ -20,6 +20,15 @@ function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
|
||||
|
||||
export function listDiscordAccountIds(cfg: OpenClawConfig): string[] {
|
||||
const ids = listConfiguredAccountIds(cfg);
|
||||
const explicitDefault = ids.includes(DEFAULT_ACCOUNT_ID);
|
||||
|
||||
if (!explicitDefault) {
|
||||
const defaultResolution = resolveDiscordToken(cfg, { accountId: DEFAULT_ACCOUNT_ID });
|
||||
if (defaultResolution.source !== "none") {
|
||||
ids.push(DEFAULT_ACCOUNT_ID);
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
||||
return ids.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
@ -284,6 +284,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
Surface: "discord" as const,
|
||||
WasMentioned: effectiveWasMentioned,
|
||||
MessageSid: message.id,
|
||||
RpcId: `discord:${route.accountId}:${message.id}`,
|
||||
ParentSessionKey: autoThreadContext?.ParentSessionKey ?? threadKeys.parentSessionKey,
|
||||
ThreadStarterBody: threadStarterBody,
|
||||
ThreadLabel: threadLabel,
|
||||
|
||||
91
src/discord/multi-account.test.ts
Normal file
91
src/discord/multi-account.test.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { type MoltbotConfig } from "../config/config.js";
|
||||
import { listDiscordAccountIds } from "./accounts.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
describe("discord multi-account logic", () => {
|
||||
it("should list all configured accounts", () => {
|
||||
const config = {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
bot_a: { token: "token_a", enabled: true },
|
||||
bot_b: { token: "token_b", enabled: true },
|
||||
bot_c: { token: "token_c", enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as MoltbotConfig;
|
||||
|
||||
const ids = listDiscordAccountIds(config);
|
||||
expect(ids).toEqual(["bot_a", "bot_b", "bot_c"]);
|
||||
});
|
||||
|
||||
it("should resolve specific account tokens", () => {
|
||||
const config = {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
bot_a: { token: "token_a" },
|
||||
bot_b: { token: "token_b" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as MoltbotConfig;
|
||||
|
||||
const tokenA = resolveDiscordToken(config, { accountId: "bot_a" });
|
||||
const tokenB = resolveDiscordToken(config, { accountId: "bot_b" });
|
||||
const tokenUnknown = resolveDiscordToken(config, { accountId: "bot_unknown" });
|
||||
|
||||
expect(tokenA.token).toBe("token_a");
|
||||
expect(tokenB.token).toBe("token_b");
|
||||
expect(tokenUnknown.token).toBe("");
|
||||
});
|
||||
|
||||
it("should fall back to default token if no account token specified", () => {
|
||||
const config = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "default_token",
|
||||
accounts: {
|
||||
bot_a: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as MoltbotConfig;
|
||||
|
||||
const tokenA = resolveDiscordToken(config, { accountId: "bot_a" });
|
||||
|
||||
expect(tokenA.token).toBe("");
|
||||
});
|
||||
|
||||
it("should include default account if top-level token is present", () => {
|
||||
const config = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "default_token",
|
||||
accounts: {
|
||||
bot_a: { token: "token_a" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as MoltbotConfig;
|
||||
|
||||
const ids = listDiscordAccountIds(config);
|
||||
expect(ids).toContain("default");
|
||||
expect(ids).toContain("bot_a");
|
||||
});
|
||||
|
||||
it("should resolve default account correctly", () => {
|
||||
const config = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "default_token",
|
||||
},
|
||||
},
|
||||
} as unknown as MoltbotConfig;
|
||||
|
||||
const token = resolveDiscordToken(config, { accountId: "default" });
|
||||
expect(token.token).toBe("default_token");
|
||||
});
|
||||
});
|
||||
@ -104,7 +104,7 @@ describe("resolveAgentRoute", () => {
|
||||
peer: { kind: "dm", id: "+1000" },
|
||||
});
|
||||
expect(route.agentId).toBe("a");
|
||||
expect(route.sessionKey).toBe("agent:a:main");
|
||||
expect(route.sessionKey).toBe("agent:a:main:biz");
|
||||
expect(route.matchedBy).toBe("binding.peer");
|
||||
});
|
||||
|
||||
@ -224,7 +224,8 @@ describe("resolveAgentRoute", () => {
|
||||
peer: { kind: "dm", id: "+1000" },
|
||||
});
|
||||
expect(route.agentId).toBe("home");
|
||||
expect(route.sessionKey).toBe("agent:home:main");
|
||||
// Updated: Non-default accounts get distinct session keys
|
||||
expect(route.sessionKey).toBe("agent:home:main:biz");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -139,11 +139,27 @@ export function buildAgentPeerSessionKey(params: {
|
||||
}
|
||||
if (dmScope === "per-channel-peer" && peerId) {
|
||||
const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
if (accountId !== DEFAULT_ACCOUNT_ID) {
|
||||
return `agent:${normalizeAgentId(params.agentId)}:${channel}:${accountId}:dm:${peerId}`;
|
||||
}
|
||||
return `agent:${normalizeAgentId(params.agentId)}:${channel}:dm:${peerId}`;
|
||||
}
|
||||
if (dmScope === "per-peer" && peerId) {
|
||||
return `agent:${normalizeAgentId(params.agentId)}:dm:${peerId}`;
|
||||
}
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
if (accountId && accountId !== DEFAULT_ACCOUNT_ID) {
|
||||
if (dmScope === "main") {
|
||||
return (
|
||||
buildAgentMainSessionKey({
|
||||
agentId: params.agentId,
|
||||
mainKey: params.mainKey,
|
||||
}) + `:${accountId}`
|
||||
);
|
||||
}
|
||||
return `agent:${normalizeAgentId(params.agentId)}:dm:${peerId}:${accountId}`;
|
||||
}
|
||||
return buildAgentMainSessionKey({
|
||||
agentId: params.agentId,
|
||||
mainKey: params.mainKey,
|
||||
@ -151,7 +167,13 @@ export function buildAgentPeerSessionKey(params: {
|
||||
}
|
||||
const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
|
||||
const peerId = ((params.peerId ?? "").trim() || "unknown").toLowerCase();
|
||||
return `agent:${normalizeAgentId(params.agentId)}:${channel}:${peerKind}:${peerId}`;
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const baseKey = `agent:${normalizeAgentId(params.agentId)}:${channel}:${peerKind}:${peerId}`;
|
||||
|
||||
if (accountId && accountId !== DEFAULT_ACCOUNT_ID) {
|
||||
return `${baseKey}:${accountId}`;
|
||||
}
|
||||
return baseKey;
|
||||
}
|
||||
|
||||
function resolveLinkedPeerId(params: {
|
||||
|
||||
58
src/routing/shared-channel.test.ts
Normal file
58
src/routing/shared-channel.test.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { resolveAgentRoute, type ResolveAgentRouteInput } from "./resolve-route.js";
|
||||
import type { MoltbotConfig } from "../config/config.js";
|
||||
|
||||
const mockConfig: MoltbotConfig = {
|
||||
agents: { list: [{ id: "main" }] },
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
bot_a: { token: "taken_a" },
|
||||
bot_b: { token: "token_b" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("resolveAgentRoute - Shared Channel", () => {
|
||||
it("should generate distinct keys for SAME channel on DIFFERENT bots", () => {
|
||||
const inputA: ResolveAgentRouteInput = {
|
||||
cfg: mockConfig,
|
||||
channel: "discord",
|
||||
accountId: "bot_a",
|
||||
peer: { kind: "channel", id: "channel_123" },
|
||||
};
|
||||
|
||||
const inputB: ResolveAgentRouteInput = {
|
||||
cfg: mockConfig,
|
||||
channel: "discord",
|
||||
accountId: "bot_b",
|
||||
peer: { kind: "channel", id: "channel_123" },
|
||||
};
|
||||
|
||||
const routeA = resolveAgentRoute(inputA);
|
||||
const routeB = resolveAgentRoute(inputB);
|
||||
|
||||
console.log("Channel RouteA:", routeA.sessionKey);
|
||||
console.log("Channel RouteB:", routeB.sessionKey);
|
||||
|
||||
expect(routeA.sessionKey).not.toEqual(routeB.sessionKey);
|
||||
expect(routeA.sessionKey).toContain(":bot_a");
|
||||
expect(routeB.sessionKey).toContain(":bot_b");
|
||||
});
|
||||
|
||||
it("should preserve backward compatibility for default bot in channel", () => {
|
||||
const input: ResolveAgentRouteInput = {
|
||||
cfg: mockConfig,
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
peer: { kind: "channel", id: "channel_123" },
|
||||
};
|
||||
|
||||
const route = resolveAgentRoute(input);
|
||||
console.log("Channel Default:", route.sessionKey);
|
||||
|
||||
expect(route.sessionKey).toBe("agent:main:discord:channel:channel_123");
|
||||
});
|
||||
});
|
||||
@ -200,7 +200,7 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.AccountId).toBe("opie");
|
||||
expect(payload.SessionKey).toBe("agent:opie:main");
|
||||
expect(payload.SessionKey).toBe("agent:opie:main:opie");
|
||||
});
|
||||
it("allows per-group requireMention override", async () => {
|
||||
onSpy.mockReset();
|
||||
|
||||
@ -1260,7 +1260,7 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.AccountId).toBe("opie");
|
||||
expect(payload.SessionKey).toBe("agent:opie:main");
|
||||
expect(payload.SessionKey).toBe("agent:opie:main:opie");
|
||||
});
|
||||
|
||||
it("allows per-group requireMention override", async () => {
|
||||
|
||||
@ -242,7 +242,7 @@ describe("partial reply gating", () => {
|
||||
});
|
||||
it("updates last-route for group chats with account id", async () => {
|
||||
const now = Date.now();
|
||||
const groupSessionKey = "agent:main:whatsapp:group:123@g.us";
|
||||
const groupSessionKey = "agent:main:whatsapp:group:123@g.us:work";
|
||||
const store = await makeSessionStore({
|
||||
[groupSessionKey]: { sessionId: "sid", updatedAt: now - 1 },
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user