feat: Introduce multi-account support for Discord, ensuring session keys and RPC IDs are account-aware.
This commit is contained in:
parent
fdcac0ccf4
commit
56ee60d853
@ -20,6 +20,15 @@ function listConfiguredAccountIds(cfg: MoltbotConfig): string[] {
|
||||
|
||||
export function listDiscordAccountIds(cfg: MoltbotConfig): 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,
|
||||
|
||||
99
src/discord/multi-account.test.ts
Normal file
99
src/discord/multi-account.test.ts
Normal file
@ -0,0 +1,99 @@
|
||||
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: {}, // no token
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as MoltbotConfig;
|
||||
|
||||
const tokenA = resolveDiscordToken(config, { accountId: "bot_a" });
|
||||
// Current implementation might strictly require per-account token if it's not the default account.
|
||||
// Let's verify behavior.
|
||||
// Actually, looking at token.ts:
|
||||
// const accountToken = ...
|
||||
// if (accountToken) return ...
|
||||
// const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||
// const configToken = allowEnv ? ... : undefined;
|
||||
|
||||
// So valid behavior: non-default accounts MUST have a specific token.
|
||||
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");
|
||||
});
|
||||
});
|
||||
@ -139,11 +139,22 @@ 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 `agent:${normalizeAgentId(params.agentId)}:dm:${peerId}:${accountId}`;
|
||||
}
|
||||
return `agent:${normalizeAgentId(params.agentId)}:dm:${peerId}:${accountId}`;
|
||||
}
|
||||
return buildAgentMainSessionKey({
|
||||
agentId: params.agentId,
|
||||
mainKey: params.mainKey,
|
||||
@ -151,7 +162,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: {
|
||||
|
||||
59
src/routing/shared-channel.test.ts
Normal file
59
src/routing/shared-channel.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
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);
|
||||
|
||||
// Should NOT have :default
|
||||
expect(route.sessionKey).toBe("agent:main:discord:channel:channel_123");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user