This commit is contained in:
Kuzey 2026-01-30 16:15:09 +00:00 committed by GitHub
commit ec4c4e2bc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 188 additions and 6 deletions

View File

@ -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));
}

View File

@ -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,

View 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");
});
});

View File

@ -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");
});
});

View File

@ -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: {

View 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");
});
});

View File

@ -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();

View File

@ -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 () => {

View File

@ -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 },
});