diff --git a/extensions/discord/src/actions/runtime.guild.ts b/extensions/discord/src/actions/runtime.guild.ts index 53fe666f9ce..bc91f9b76e4 100644 --- a/extensions/discord/src/actions/runtime.guild.ts +++ b/extensions/discord/src/actions/runtime.guild.ts @@ -98,7 +98,7 @@ export async function handleDiscordGuildAction( action: string, params: Record, isActionEnabled: ActionGate, - cfg?: OpenClawConfig, + cfg: OpenClawConfig, options?: { mediaLocalRoots?: readonly string[] }, ): Promise> { const accountId = readStringParam(params, "accountId"); diff --git a/extensions/discord/src/actions/runtime.messaging.ts b/extensions/discord/src/actions/runtime.messaging.ts index 57a50a2b0ae..fd464c0112b 100644 --- a/extensions/discord/src/actions/runtime.messaging.ts +++ b/extensions/discord/src/actions/runtime.messaging.ts @@ -118,11 +118,11 @@ export async function handleDiscordMessagingAction( action: string, params: Record, isActionEnabled: ActionGate, + cfg: OpenClawConfig, options?: { mediaLocalRoots?: readonly string[]; mediaReadFile?: (filePath: string) => Promise; }, - cfg?: OpenClawConfig, ): Promise> { const resolveChannelId = () => discordMessagingActionRuntime.resolveDiscordChannelId( diff --git a/extensions/discord/src/actions/runtime.moderation.ts b/extensions/discord/src/actions/runtime.moderation.ts index 23dc1db4b6a..722ec93484e 100644 --- a/extensions/discord/src/actions/runtime.moderation.ts +++ b/extensions/discord/src/actions/runtime.moderation.ts @@ -54,7 +54,7 @@ export async function handleDiscordModerationAction( action: string, params: Record, isActionEnabled: ActionGate, - cfg?: OpenClawConfig, + cfg: OpenClawConfig, ): Promise> { if (!isDiscordModerationAction(action)) { throw new Error(`Unknown action: ${action}`); diff --git a/extensions/discord/src/actions/runtime.test.ts b/extensions/discord/src/actions/runtime.test.ts index 4d11d96e551..d9e1334a011 100644 --- a/extensions/discord/src/actions/runtime.test.ts +++ b/extensions/discord/src/actions/runtime.test.ts @@ -89,13 +89,13 @@ function handleMessagingAction( action: string, params: Record, isActionEnabled: (key: keyof DiscordActionConfig) => boolean, + cfg: OpenClawConfig = DISCORD_TEST_CFG, options?: { mediaLocalRoots?: readonly string[]; mediaReadFile?: (filePath: string) => Promise; }, - cfg: OpenClawConfig = DISCORD_TEST_CFG, ) { - return handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg); + return handleDiscordMessagingAction(action, params, isActionEnabled, cfg, options); } function handleGuildAction( @@ -178,7 +178,6 @@ describe("handleDiscordMessagingAction", () => { emoji: "✅", }, enableAllActions, - undefined, { channels: { discord: { @@ -363,7 +362,7 @@ describe("handleDiscordMessagingAction", () => { }, }, } as OpenClawConfig; - await handleMessagingAction("readMessages", { channelId: "C1" }, enableAllActions, {}, cfg); + await handleMessagingAction("readMessages", { channelId: "C1" }, enableAllActions, cfg); expect(readMessagesDiscord).toHaveBeenCalledWith("C1", expect.any(Object), { cfg }); }); @@ -397,7 +396,6 @@ describe("handleDiscordMessagingAction", () => { "fetchMessage", { guildId: "G1", channelId: "C1", messageId: "M1" }, enableAllActions, - {}, cfg, ); expect(fetchMessageDiscord).toHaveBeenCalledWith("C1", "M1", { cfg }); @@ -471,6 +469,7 @@ describe("handleDiscordMessagingAction", () => { mediaUrl: "/tmp/image.png", }, enableAllActions, + DISCORD_TEST_CFG, { mediaLocalRoots: ["/tmp/agent-root"] }, ); expect(sendMessageDiscord).toHaveBeenCalledWith( @@ -496,6 +495,7 @@ describe("handleDiscordMessagingAction", () => { components: {}, }, enableAllActions, + DISCORD_TEST_CFG, { mediaLocalRoots: ["/tmp/agent-root"] }, ); diff --git a/extensions/discord/src/actions/runtime.ts b/extensions/discord/src/actions/runtime.ts index 838e9b64d4b..63ec13aa5bc 100644 --- a/extensions/discord/src/actions/runtime.ts +++ b/extensions/discord/src/actions/runtime.ts @@ -66,7 +66,7 @@ export async function handleDiscordAction( const isActionEnabled = createDiscordActionGate({ cfg, accountId }); if (messagingActions.has(action)) { - return await handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg); + return await handleDiscordMessagingAction(action, params, isActionEnabled, cfg, options); } if (guildActions.has(action)) { return await handleDiscordGuildAction(action, params, isActionEnabled, cfg, options); diff --git a/extensions/discord/src/approval-handler.runtime.ts b/extensions/discord/src/approval-handler.runtime.ts index 16964e61a4b..05ef65f13a4 100644 --- a/extensions/discord/src/approval-handler.runtime.ts +++ b/extensions/discord/src/approval-handler.runtime.ts @@ -358,10 +358,11 @@ async function updateMessage(params: { container: DiscordUiContainer; }): Promise { try { - const { rest, request: discordRequest } = createDiscordClient( - { token: params.token, accountId: params.accountId }, - params.cfg, - ); + const { rest, request: discordRequest } = createDiscordClient({ + cfg: params.cfg, + token: params.token, + accountId: params.accountId, + }); const payload = buildExecApprovalPayload(params.container); await discordRequest( () => @@ -389,10 +390,11 @@ async function finalizeMessage(params: { return; } try { - const { rest, request: discordRequest } = createDiscordClient( - { token: params.token, accountId: params.accountId }, - params.cfg, - ); + const { rest, request: discordRequest } = createDiscordClient({ + cfg: params.cfg, + token: params.token, + accountId: params.accountId, + }); await discordRequest( () => rest.delete(Routes.channelMessage(params.channelId, params.messageId)) as Promise, "delete-approval", @@ -517,10 +519,11 @@ export const discordApprovalNativeRuntime = createChannelApprovalNativeRuntimeAd }, }; } - const { rest, request: discordRequest } = createDiscordClient( - { token: resolved.context.token, accountId: resolved.accountId }, + const { rest, request: discordRequest } = createDiscordClient({ cfg, - ); + token: resolved.context.token, + accountId: resolved.accountId, + }); const userId = plannedTarget.target.to; const dmChannel = (await discordRequest( () => @@ -553,10 +556,11 @@ export const discordApprovalNativeRuntime = createChannelApprovalNativeRuntimeAd if (!resolved) { return null; } - const { rest, request: discordRequest } = createDiscordClient( - { token: resolved.context.token, accountId: resolved.accountId }, + const { rest, request: discordRequest } = createDiscordClient({ cfg, - ); + token: resolved.context.token, + accountId: resolved.accountId, + }); const message = (await discordRequest( () => rest.post(Routes.channelMessages(preparedTarget.discordChannelId), { diff --git a/extensions/discord/src/audit-core.ts b/extensions/discord/src/audit-core.ts index 2f3c9c5aa1f..f124a066e34 100644 --- a/extensions/discord/src/audit-core.ts +++ b/extensions/discord/src/audit-core.ts @@ -1,6 +1,7 @@ import type { DiscordGuildChannelConfig, DiscordGuildEntry, + OpenClawConfig, } from "openclaw/plugin-sdk/config-runtime"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; @@ -76,13 +77,14 @@ export function collectDiscordAuditChannelIdsForGuilds( } export async function auditDiscordChannelPermissionsWithFetcher(params: { + cfg: OpenClawConfig; token: string; accountId?: string | null; channelIds: string[]; timeoutMs: number; fetchChannelPermissions: ( channelId: string, - params: { token: string; accountId?: string }, + params: { cfg: OpenClawConfig; token: string; accountId?: string }, ) => Promise<{ permissions: string[]; }>; @@ -105,6 +107,7 @@ export async function auditDiscordChannelPermissionsWithFetcher(params: { for (const channelId of params.channelIds) { try { const perms = await params.fetchChannelPermissions(channelId, { + cfg: params.cfg, token, accountId: params.accountId ?? undefined, }); diff --git a/extensions/discord/src/audit.test.ts b/extensions/discord/src/audit.test.ts index dc8256b7c84..fb9b015d90e 100644 --- a/extensions/discord/src/audit.test.ts +++ b/extensions/discord/src/audit.test.ts @@ -56,6 +56,7 @@ describe("discord audit", () => { }); const audit = await auditDiscordChannelPermissionsWithFetcher({ + cfg, token: "t", accountId: "default", channelIds: collected.channelIds, diff --git a/extensions/discord/src/audit.ts b/extensions/discord/src/audit.ts index b206e2b6405..60ffb524c54 100644 --- a/extensions/discord/src/audit.ts +++ b/extensions/discord/src/audit.ts @@ -19,6 +19,7 @@ export function collectDiscordAuditChannelIds(params: { } export async function auditDiscordChannelPermissions(params: { + cfg: OpenClawConfig; token: string; accountId?: string | null; channelIds: string[]; diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 1b8b69834d4..1d510ab1638 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -621,10 +621,23 @@ export const discordPlugin: ChannelPlugin ], }; } + const statusCfg: OpenClawConfig = { + channels: { + discord: { + accounts: { + [account.accountId]: { + ...account.config, + token, + }, + }, + }, + }, + }; try { const perms = await ( await loadDiscordSendModule() ).fetchChannelPermissionsDiscord(parsedTarget.id, { + cfg: statusCfg, token, accountId: account.accountId ?? undefined, }); @@ -681,6 +694,7 @@ export const discordPlugin: ChannelPlugin }; } const audit = await auditDiscordChannelPermissions({ + cfg, token: botToken, accountId: account.accountId, channelIds, diff --git a/extensions/discord/src/client.proxy.test.ts b/extensions/discord/src/client.proxy.test.ts index e8833cb19c3..3d5e583547d 100644 --- a/extensions/discord/src/client.proxy.test.ts +++ b/extensions/discord/src/client.proxy.test.ts @@ -35,7 +35,7 @@ describe("createDiscordRestClient proxy support", () => { }, } as OpenClawConfig; - const { rest } = createDiscordRestClient({}, cfg); + const { rest } = createDiscordRestClient({ cfg }); const requestClient = rest as unknown as { customFetch?: typeof fetch; options?: { fetch?: typeof fetch }; @@ -54,7 +54,7 @@ describe("createDiscordRestClient proxy support", () => { }, } as OpenClawConfig; - const { rest } = createDiscordRestClient({}, cfg); + const { rest } = createDiscordRestClient({ cfg }); const requestClient = rest as unknown as { options?: { fetch?: typeof fetch }; }; @@ -72,7 +72,7 @@ describe("createDiscordRestClient proxy support", () => { }, } as OpenClawConfig; - const { rest } = createDiscordRestClient({}, cfg); + const { rest } = createDiscordRestClient({ cfg }); const requestClient = rest as unknown as { options?: { fetch?: typeof fetch }; }; @@ -91,7 +91,7 @@ describe("createDiscordRestClient proxy support", () => { }, } as OpenClawConfig; - const { rest } = createDiscordRestClient({}, cfg); + const { rest } = createDiscordRestClient({ cfg }); const requestClient = rest as unknown as { options?: { fetch?: typeof fetch }; }; @@ -110,7 +110,7 @@ describe("createDiscordRestClient proxy support", () => { }, } as OpenClawConfig; - const { rest } = createDiscordRestClient({}, cfg); + const { rest } = createDiscordRestClient({ cfg }); const requestClient = rest as unknown as { options?: { fetch?: typeof fetch }; }; diff --git a/extensions/discord/src/client.test.ts b/extensions/discord/src/client.test.ts index 717be98d44c..986c1468938 100644 --- a/extensions/discord/src/client.test.ts +++ b/extensions/discord/src/client.test.ts @@ -19,13 +19,7 @@ describe("createDiscordRestClient", () => { }, } as OpenClawConfig; - const result = createDiscordRestClient( - { - token: "Bot explicit-token", - rest: fakeRest, - }, - cfg, - ); + const result = createDiscordRestClient({ cfg, token: "Bot explicit-token", rest: fakeRest }); expect(result.token).toBe("explicit-token"); expect(result.rest).toBe(fakeRest); @@ -52,14 +46,12 @@ describe("createDiscordRestClient", () => { }, } as OpenClawConfig; - const result = createDiscordRestClient( - { - accountId: "ops", - token: "Bot explicit-account-token", - rest: fakeRest, - }, + const result = createDiscordRestClient({ cfg, - ); + accountId: "ops", + token: "Bot explicit-account-token", + rest: fakeRest, + }); expect(result.token).toBe("explicit-account-token"); expect(result.account.accountId).toBe("ops"); @@ -79,13 +71,6 @@ describe("createDiscordRestClient", () => { }, } as OpenClawConfig; - expect(() => - createDiscordRestClient( - { - rest: fakeRest, - }, - cfg, - ), - ).toThrow(/unresolved SecretRef/i); + expect(() => createDiscordRestClient({ cfg, rest: fakeRest })).toThrow(/unresolved SecretRef/i); }); }); diff --git a/extensions/discord/src/client.ts b/extensions/discord/src/client.ts index 1aa6e0a9a95..8483eb7cced 100644 --- a/extensions/discord/src/client.ts +++ b/extensions/discord/src/client.ts @@ -16,7 +16,7 @@ import type { DiscordRuntimeAccountContext } from "./send.types.js"; import { normalizeDiscordToken } from "./token.js"; export type DiscordClientOpts = { - cfg?: OpenClawConfig; + cfg: OpenClawConfig; token?: string; accountId?: string; rest?: RequestClient; @@ -36,16 +36,9 @@ export function createDiscordRuntimeAccountContext(params: { export function resolveDiscordClientAccountContext( opts: Pick, - cfg?: OpenClawConfig, runtime?: Pick, ) { - const config = opts.cfg ?? cfg; - if (!config) { - throw new Error( - "Discord client requires a resolved runtime config. Load and resolve config at the command or gateway boundary, then pass cfg through the runtime path.", - ); - } - const resolvedCfg = requireRuntimeConfig(config, "Discord client"); + const resolvedCfg = requireRuntimeConfig(opts.cfg, "Discord client"); const account = resolveAccountWithoutToken({ cfg: resolvedCfg, accountId: opts.accountId, @@ -69,10 +62,9 @@ function resolveToken(params: { accountId: string; fallbackToken?: string }) { export function resolveDiscordProxyFetch( opts: Pick, - cfg?: OpenClawConfig, runtime?: Pick, ): typeof fetch | undefined { - return resolveDiscordClientAccountContext(opts, cfg, runtime).proxyFetch; + return resolveDiscordClientAccountContext(opts, runtime).proxyFetch; } function resolveRest( @@ -110,9 +102,9 @@ function resolveAccountWithoutToken(params: { }; } -export function createDiscordRestClient(opts: DiscordClientOpts, cfg?: OpenClawConfig) { +export function createDiscordRestClient(opts: DiscordClientOpts) { const explicitToken = normalizeDiscordToken(opts.token, "channels.discord.token"); - const proxyContext = resolveDiscordClientAccountContext(opts, cfg); + const proxyContext = resolveDiscordClientAccountContext(opts); const resolvedCfg = proxyContext.cfg; const account = explicitToken ? proxyContext.account @@ -127,11 +119,12 @@ export function createDiscordRestClient(opts: DiscordClientOpts, cfg?: OpenClawC return { token, rest, account }; } -export function createDiscordClient( - opts: DiscordClientOpts, - cfg?: OpenClawConfig, -): { token: string; rest: RequestClient; request: RetryRunner } { - const { token, rest, account } = createDiscordRestClient(opts, opts.cfg ?? cfg); +export function createDiscordClient(opts: DiscordClientOpts): { + token: string; + rest: RequestClient; + request: RetryRunner; +} { + const { token, rest, account } = createDiscordRestClient(opts); const request = createDiscordRetryRunner({ retry: opts.retry, configRetry: account.config.retry, @@ -141,5 +134,5 @@ export function createDiscordClient( } export function resolveDiscordRest(opts: DiscordClientOpts) { - return createDiscordRestClient(opts, opts.cfg).rest; + return createDiscordRestClient(opts).rest; } diff --git a/extensions/discord/src/draft-chunking.test.ts b/extensions/discord/src/draft-chunking.test.ts index b593fcd6532..8814ee23a89 100644 --- a/extensions/discord/src/draft-chunking.test.ts +++ b/extensions/discord/src/draft-chunking.test.ts @@ -4,7 +4,7 @@ import { resolveDiscordDraftStreamingChunking } from "./draft-chunking.js"; describe("resolveDiscordDraftStreamingChunking", () => { it("returns sane defaults when discord draft chunking is unset", () => { - expect(resolveDiscordDraftStreamingChunking(undefined)).toEqual({ + expect(resolveDiscordDraftStreamingChunking({} as OpenClawConfig)).toEqual({ minChars: 200, maxChars: 800, breakPreference: "paragraph", diff --git a/extensions/discord/src/draft-chunking.ts b/extensions/discord/src/draft-chunking.ts index a1e0dea0a64..24a15fe60a4 100644 --- a/extensions/discord/src/draft-chunking.ts +++ b/extensions/discord/src/draft-chunking.ts @@ -9,7 +9,7 @@ const DEFAULT_DISCORD_DRAFT_STREAM_MIN = 200; const DEFAULT_DISCORD_DRAFT_STREAM_MAX = 800; export function resolveDiscordDraftStreamingChunking( - cfg: OpenClawConfig | undefined, + cfg: OpenClawConfig, accountId?: string | null, ): { minChars: number; diff --git a/extensions/discord/src/monitor/message-handler.preflight.test.ts b/extensions/discord/src/monitor/message-handler.preflight.test.ts index ad90ebb400a..84457031e5d 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.test.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.test.ts @@ -1244,6 +1244,7 @@ describe("shouldIgnoreBoundThreadWebhookMessage", () => { it("returns true for recently unbound thread webhook echoes", async () => { const manager = createThreadBindingManager({ + cfg: DEFAULT_PREFLIGHT_CFG, accountId: "default", persist: false, enableSweeper: false, diff --git a/extensions/discord/src/monitor/message-handler.process.test.ts b/extensions/discord/src/monitor/message-handler.process.test.ts index 07c7daea7f8..e656631b156 100644 --- a/extensions/discord/src/monitor/message-handler.process.test.ts +++ b/extensions/discord/src/monitor/message-handler.process.test.ts @@ -683,6 +683,7 @@ describe("processDiscordMessage session routing", () => { it("prefers bound session keys and sets MessageThreadId for bound thread messages", async () => { const threadBindings = createThreadBindingManager({ + cfg: {} as import("openclaw/plugin-sdk/config-runtime").OpenClawConfig, accountId: "default", persist: false, enableSweeper: false, diff --git a/extensions/discord/src/monitor/message-handler.process.ts b/extensions/discord/src/monitor/message-handler.process.ts index c7e995f342b..94706759195 100644 --- a/extensions/discord/src/monitor/message-handler.process.ts +++ b/extensions/discord/src/monitor/message-handler.process.ts @@ -826,6 +826,8 @@ export async function processDiscordMessage( } notifyFinalReplyStart(); await editMessageDiscord(deliverChannelId, previewMessageId, edit, { + cfg, + accountId, rest: deliveryRest, }); }, diff --git a/extensions/discord/src/monitor/monitor.threading-utils.test.ts b/extensions/discord/src/monitor/monitor.threading-utils.test.ts index 050ddd28375..b7f448b4a33 100644 --- a/extensions/discord/src/monitor/monitor.threading-utils.test.ts +++ b/extensions/discord/src/monitor/monitor.threading-utils.test.ts @@ -1,5 +1,6 @@ import type { Client } from "@buape/carbon"; import type { GatewayPresenceUpdate } from "discord-api-types/v10"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing"; import { beforeEach, describe, expect, it } from "vitest"; import type { DiscordChannelConfigResolved } from "./allow-list.js"; @@ -23,6 +24,8 @@ import { resolveDiscordReplyDeliveryPlan, } from "./threading.js"; +const DEFAULT_CFG = {} as OpenClawConfig; + describe("resolveDiscordOwnerAllowFrom", () => { it("returns undefined when no allowlist is configured", () => { const result = resolveDiscordOwnerAllowFrom({ @@ -432,6 +435,7 @@ describe("maybeCreateDiscordAutoThread", () => { threadChannel: null, baseText: "hello", combinedBody: "hello", + cfg: DEFAULT_CFG, }; } @@ -489,6 +493,7 @@ describe("resolveDiscordAutoThreadReplyPlan", () => { threadChannel: overrides?.threadChannel ?? null, baseText: "hello", combinedBody: "hello", + cfg: DEFAULT_CFG, replyToMode: "all" as const, agentId: "agent", channel: "discord" as const, diff --git a/extensions/discord/src/monitor/reply-delivery.test.ts b/extensions/discord/src/monitor/reply-delivery.test.ts index fe767d8400a..8f36ab9a815 100644 --- a/extensions/discord/src/monitor/reply-delivery.test.ts +++ b/extensions/discord/src/monitor/reply-delivery.test.ts @@ -13,6 +13,7 @@ const sendDiscordTextMock = vi.hoisted(() => vi.fn()); const buildDiscordSendErrorMock = vi.hoisted(() => vi.fn<(err: unknown, ctx?: unknown) => Promise>(async (err: unknown) => err), ); +const DEFAULT_CFG = {} as OpenClawConfig; const retryAsyncMock = vi.hoisted(() => vi.fn( async ( @@ -100,6 +101,7 @@ describe("deliverDiscordReply", () => { }> = {}, ) => { const threadBindings = createThreadBindingManager({ + cfg: DEFAULT_CFG, accountId: "default", persist: false, enableSweeper: false, diff --git a/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts b/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts index 20b0330818a..b14f053af38 100644 --- a/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts @@ -9,6 +9,7 @@ const DEFAULT_SEND_RESULT = { messageId: "msg-1", channelId: "thread-1", }; +const DEFAULT_CFG = {} as OpenClawConfig; const restGet = vi.fn<(...args: unknown[]) => Promise>(); const sendMessageDiscord = vi.fn(); @@ -30,6 +31,17 @@ beforeAll(async () => { await import("./thread-bindings.discord-api.js")); }); +function resolveTestChannelIdForBinding( + params: Omit[0], "cfg"> & { + cfg?: OpenClawConfig; + }, +) { + return resolveChannelIdForBinding({ + cfg: DEFAULT_CFG, + ...params, + }); +} + describe("resolveChannelIdForBinding", () => { beforeEach(() => { vi.restoreAllMocks(); @@ -59,7 +71,7 @@ describe("resolveChannelIdForBinding", () => { }); it("returns explicit channelId without resolving route", async () => { - const resolved = await resolveChannelIdForBinding({ + const resolved = await resolveTestChannelIdForBinding({ accountId: "default", threadId: "thread-1", channelId: "channel-explicit", @@ -71,7 +83,7 @@ describe("resolveChannelIdForBinding", () => { }); it("normalizes prefixed explicit channelId without resolving route", async () => { - const resolved = await resolveChannelIdForBinding({ + const resolved = await resolveTestChannelIdForBinding({ accountId: "default", threadId: "thread-1", channelId: "channel:123456789012345678", @@ -88,7 +100,7 @@ describe("resolveChannelIdForBinding", () => { type: ChannelType.GuildText, }); - const resolved = await resolveChannelIdForBinding({ + const resolved = await resolveTestChannelIdForBinding({ accountId: "default", threadId: "channel:123456789012345678", }); @@ -106,7 +118,7 @@ describe("resolveChannelIdForBinding", () => { parent_id: "channel-parent", }); - const resolved = await resolveChannelIdForBinding({ + const resolved = await resolveTestChannelIdForBinding({ accountId: "default", threadId: "thread-1", }); @@ -124,14 +136,16 @@ describe("resolveChannelIdForBinding", () => { parent_id: "channel-parent", }); - await resolveChannelIdForBinding({ + await resolveTestChannelIdForBinding({ cfg, accountId: "default", threadId: "thread-1", }); const createDiscordRestClientCalls = createDiscordRestClient.mock.calls as unknown[][]; - expect(createDiscordRestClientCalls[0]?.[1]).toBe(cfg); + expect( + (createDiscordRestClientCalls[0]?.[0] as { cfg?: OpenClawConfig } | undefined)?.cfg, + ).toBe(cfg); }); it("keeps non-thread channel id even when parent_id exists", async () => { @@ -141,7 +155,7 @@ describe("resolveChannelIdForBinding", () => { parent_id: "category-1", }); - const resolved = await resolveChannelIdForBinding({ + const resolved = await resolveTestChannelIdForBinding({ accountId: "default", threadId: "channel-text", }); @@ -156,7 +170,7 @@ describe("resolveChannelIdForBinding", () => { parent_id: "category-1", }); - const resolved = await resolveChannelIdForBinding({ + const resolved = await resolveTestChannelIdForBinding({ accountId: "default", threadId: "forum-1", }); diff --git a/extensions/discord/src/monitor/thread-bindings.discord-api.ts b/extensions/discord/src/monitor/thread-bindings.discord-api.ts index 92bbf83b2d3..0c2555db671 100644 --- a/extensions/discord/src/monitor/thread-bindings.discord-api.ts +++ b/extensions/discord/src/monitor/thread-bindings.discord-api.ts @@ -173,19 +173,17 @@ export async function maybeSendBindingMessage(params: { } export async function createWebhookForChannel(params: { - cfg?: OpenClawConfig; + cfg: OpenClawConfig; accountId: string; token?: string; channelId: string; }): Promise<{ webhookId?: string; webhookToken?: string }> { try { - const rest = createDiscordRestClient( - { - accountId: params.accountId, - token: params.token, - }, - params.cfg, - ).rest; + const rest = createDiscordRestClient({ + cfg: params.cfg, + accountId: params.accountId, + token: params.token, + }).rest; const created = (await rest.post(Routes.channelWebhooks(params.channelId), { body: { name: "OpenClaw Agents", @@ -240,7 +238,7 @@ export function findReusableWebhook(params: { accountId: string; channelId: stri } export async function resolveChannelIdForBinding(params: { - cfg?: OpenClawConfig; + cfg: OpenClawConfig; accountId: string; token?: string; threadId: string; @@ -255,13 +253,11 @@ export async function resolveChannelIdForBinding(params: { return null; } try { - const rest = createDiscordRestClient( - { - accountId: params.accountId, - token: params.token, - }, - params.cfg, - ).rest; + const rest = createDiscordRestClient({ + cfg: params.cfg, + accountId: params.accountId, + token: params.token, + }).rest; const channel = (await rest.get(Routes.channel(lookupThreadId))) as { id?: string; type?: number; @@ -291,7 +287,7 @@ export async function resolveChannelIdForBinding(params: { } export async function createThreadForBinding(params: { - cfg?: OpenClawConfig; + cfg: OpenClawConfig; accountId: string; token?: string; channelId: string; diff --git a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts index f9f29b9fd53..27beb19f250 100644 --- a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts @@ -70,6 +70,17 @@ const discordClientModule = await import("../client.js"); const discordThreadBindingApi = await import("./thread-bindings.discord-api.js"); const acpRuntime = await import("openclaw/plugin-sdk/acp-runtime"); +function createTestThreadBindingManager( + params: Omit[0], "cfg"> & { + cfg?: OpenClawConfig; + }, +) { + return createThreadBindingManager({ + cfg: {} as OpenClawConfig, + ...params, + }); +} + describe("thread binding lifecycle", () => { beforeEach(() => { __testing.resetThreadBindingsForTests(); @@ -200,7 +211,7 @@ describe("thread binding lifecycle", () => { }); const createDefaultSweeperManager = () => - createThreadBindingManager({ + createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -257,7 +268,7 @@ describe("thread binding lifecycle", () => { it("auto-unfocuses idle-expired bindings and sends inactivity message", async () => { vi.useFakeTimers(); try { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", cfg: {} as OpenClawConfig, persist: false, @@ -297,7 +308,7 @@ describe("thread binding lifecycle", () => { it("auto-unfocuses max-age-expired bindings and sends max-age message", async () => { vi.useFakeTimers(); try { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", cfg: {} as OpenClawConfig, persist: false, @@ -378,7 +389,7 @@ describe("thread binding lifecycle", () => { vi.useFakeTimers(); try { vi.setSystemTime(new Date("2026-02-20T23:00:00.000Z")); - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -423,7 +434,7 @@ describe("thread binding lifecycle", () => { vi.useFakeTimers(); try { vi.setSystemTime(new Date("2026-02-20T10:00:00.000Z")); - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -464,7 +475,7 @@ describe("thread binding lifecycle", () => { vi.useFakeTimers(); try { vi.setSystemTime(new Date("2026-02-20T10:00:00.000Z")); - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -519,7 +530,7 @@ describe("thread binding lifecycle", () => { it("keeps binding when idle timeout is disabled per session key", async () => { vi.useFakeTimers(); try { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -561,7 +572,7 @@ describe("thread binding lifecycle", () => { it("keeps a binding when activity is touched during the same sweep pass", async () => { vi.useFakeTimers(); try { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -626,7 +637,7 @@ describe("thread binding lifecycle", () => { vi.useFakeTimers(); try { vi.setSystemTime(new Date("2026-02-20T00:00:00.000Z")); - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -667,7 +678,7 @@ describe("thread binding lifecycle", () => { try { __testing.resetThreadBindingsForTests(); vi.setSystemTime(new Date("2026-02-20T00:00:00.000Z")); - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: true, enableSweeper: false, @@ -690,7 +701,7 @@ describe("thread binding lifecycle", () => { manager.touchThread({ threadId: "thread-1" }); __testing.resetThreadBindingsForTests(); - const reloaded = createThreadBindingManager({ + const reloaded = createTestThreadBindingManager({ accountId: "default", persist: true, enableSweeper: false, @@ -719,7 +730,7 @@ describe("thread binding lifecycle", () => { }); it("reuses webhook credentials after unbind when rebinding in the same channel", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -756,7 +767,7 @@ describe("thread binding lifecycle", () => { }); it("creates a new thread when spawning from an already bound thread", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -798,7 +809,7 @@ describe("thread binding lifecycle", () => { }); it("resolves parent channel when thread target is passed via to without threadId", async () => { - createThreadBindingManager({ + createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -838,7 +849,7 @@ describe("thread binding lifecycle", () => { const cfg = { channels: { discord: { token: "tok" } }, } as OpenClawConfig; - createThreadBindingManager({ + createTestThreadBindingManager({ accountId: "runtime", token: "runtime-token", cfg, @@ -894,7 +905,7 @@ describe("thread binding lifecycle", () => { const refreshedCfg = { channels: { discord: { token: "refreshed-token" } }, } as OpenClawConfig; - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "runtime", token: "runtime-token", cfg: startupCfg, @@ -945,7 +956,7 @@ describe("thread binding lifecycle", () => { }); it("refreshes manager token when an existing manager is reused", async () => { - createThreadBindingManager({ + createTestThreadBindingManager({ accountId: "runtime", token: "token-old", persist: false, @@ -953,7 +964,7 @@ describe("thread binding lifecycle", () => { idleTimeoutMs: 24 * 60 * 60 * 1000, maxAgeMs: 0, }); - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "runtime", token: "token-new", persist: false, @@ -987,7 +998,7 @@ describe("thread binding lifecycle", () => { }); it("normalizes prefixed parentConversationId before creating child thread bindings", async () => { - createThreadBindingManager({ + createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1032,7 +1043,7 @@ describe("thread binding lifecycle", () => { }); it("preserves prefixed current channel conversation ids as binding keys", async () => { - createThreadBindingManager({ + createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1086,7 +1097,7 @@ describe("thread binding lifecycle", () => { }); it("binds current Discord DMs as direct conversation bindings", async () => { - createThreadBindingManager({ + createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1137,7 +1148,7 @@ describe("thread binding lifecycle", () => { }); it("preserves direct-binding metadata when rebinding the same conversation", async () => { - createThreadBindingManager({ + createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1198,14 +1209,14 @@ describe("thread binding lifecycle", () => { }); it("keeps overlapping thread ids isolated per account", async () => { - const a = createThreadBindingManager({ + const a = createTestThreadBindingManager({ accountId: "a", persist: false, enableSweeper: false, idleTimeoutMs: 24 * 60 * 60 * 1000, maxAgeMs: 0, }); - const b = createThreadBindingManager({ + const b = createTestThreadBindingManager({ accountId: "b", persist: false, enableSweeper: false, @@ -1243,7 +1254,7 @@ describe("thread binding lifecycle", () => { }); it("removes stale ACP bindings during startup reconciliation", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1326,7 +1337,7 @@ describe("thread binding lifecycle", () => { }); it("keeps ACP bindings when session store reads fail during startup reconciliation", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1370,7 +1381,7 @@ describe("thread binding lifecycle", () => { }); it("does not reconcile plugin-owned direct bindings as stale ACP sessions", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1411,7 +1422,7 @@ describe("thread binding lifecycle", () => { }); it("removes ACP bindings when health probe marks running session as stale", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1455,7 +1466,7 @@ describe("thread binding lifecycle", () => { }); it("keeps running ACP bindings when health probe is uncertain", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1503,7 +1514,7 @@ describe("thread binding lifecycle", () => { }); it("keeps ACP bindings in stored error state when no explicit stale probe verdict exists", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1550,7 +1561,7 @@ describe("thread binding lifecycle", () => { }); it("starts ACP health probes in parallel during startup reconciliation", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1626,7 +1637,7 @@ describe("thread binding lifecycle", () => { }); it("caps ACP startup health probe concurrency", async () => { - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, @@ -1745,7 +1756,7 @@ describe("thread binding lifecycle", () => { "utf-8", ); - const manager = createThreadBindingManager({ + const manager = createTestThreadBindingManager({ accountId: "default", persist: false, enableSweeper: false, diff --git a/extensions/discord/src/monitor/thread-bindings.manager.ts b/extensions/discord/src/monitor/thread-bindings.manager.ts index 4628fb67a4f..636b63bf6e5 100644 --- a/extensions/discord/src/monitor/thread-bindings.manager.ts +++ b/extensions/discord/src/monitor/thread-bindings.manager.ts @@ -185,17 +185,15 @@ function toSessionBindingRecord( }; } -export function createThreadBindingManager( - params: { - accountId?: string; - token?: string; - cfg?: OpenClawConfig; - persist?: boolean; - enableSweeper?: boolean; - idleTimeoutMs?: number; - maxAgeMs?: number; - } = {}, -): ThreadBindingManager { +export function createThreadBindingManager(params: { + accountId?: string; + token?: string; + cfg: OpenClawConfig; + persist?: boolean; + enableSweeper?: boolean; + idleTimeoutMs?: number; + maxAgeMs?: number; +}): ThreadBindingManager { ensureBindingsLoaded(); const accountId = normalizeAccountId(params.accountId); const existing = MANAGERS_BY_ACCOUNT_ID.get(accountId); @@ -280,13 +278,11 @@ export function createThreadBindingManager( if (!rest) { try { const cfg = resolveCurrentCfg(); - rest = createDiscordRestClient( - { - accountId, - token: resolveCurrentToken(), - }, + rest = createDiscordRestClient({ cfg, - ).rest; + accountId, + token: resolveCurrentToken(), + }).rest; } catch { return; } diff --git a/extensions/discord/src/monitor/thread-bindings.shared-state.test.ts b/extensions/discord/src/monitor/thread-bindings.shared-state.test.ts index 51729b8ef30..636fe95e750 100644 --- a/extensions/discord/src/monitor/thread-bindings.shared-state.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.shared-state.test.ts @@ -1,3 +1,4 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { beforeEach, describe, expect, it } from "vitest"; import { __testing as threadBindingsTesting, @@ -23,6 +24,7 @@ describe("thread binding manager state", () => { const viaJiti = await loadThreadBindingsViaAlternateLoader(); createThreadBindingManager({ + cfg: {} as OpenClawConfig, accountId: "work", persist: false, enableSweeper: false, diff --git a/extensions/discord/src/monitor/threading.auto-thread.test.ts b/extensions/discord/src/monitor/threading.auto-thread.test.ts index bf9186c8eb0..9bb5d479870 100644 --- a/extensions/discord/src/monitor/threading.auto-thread.test.ts +++ b/extensions/discord/src/monitor/threading.auto-thread.test.ts @@ -12,6 +12,7 @@ vi.mock("./thread-title.js", () => ({ })); let maybeCreateDiscordAutoThread: MaybeCreateDiscordAutoThreadFn; +const DEFAULT_CFG = {} as OpenClawConfig; const postMock = vi.fn(); const getMock = vi.fn(); @@ -37,6 +38,7 @@ function createBaseParams( channelType: ChannelType.GuildText, baseText: "test", combinedBody: "test", + cfg: DEFAULT_CFG, ...overrides, }; } diff --git a/extensions/discord/src/monitor/threading.ts b/extensions/discord/src/monitor/threading.ts index 162a4dfb35b..9aefe42ba16 100644 --- a/extensions/discord/src/monitor/threading.ts +++ b/extensions/discord/src/monitor/threading.ts @@ -450,7 +450,7 @@ type MaybeCreateDiscordAutoThreadParams = { channelDescription?: string; baseText: string; combinedBody: string; - cfg?: OpenClawConfig; + cfg: OpenClawConfig; agentId?: string; }; @@ -459,7 +459,7 @@ export async function resolveDiscordAutoThreadReplyPlan( replyToMode: ReplyToMode; agentId: string; channel: string; - cfg?: OpenClawConfig; + cfg: OpenClawConfig; threadParentInheritanceEnabled?: boolean; }, ): Promise { diff --git a/extensions/discord/src/proxy-fetch.ts b/extensions/discord/src/proxy-fetch.ts index 5bcdbfcaf95..2d95c3b70bb 100644 --- a/extensions/discord/src/proxy-fetch.ts +++ b/extensions/discord/src/proxy-fetch.ts @@ -8,7 +8,7 @@ import type { ResolvedDiscordAccount } from "./accounts.js"; export function resolveDiscordProxyUrl( account: Pick, - cfg?: OpenClawConfig, + cfg: OpenClawConfig, ): string | undefined { const accountProxy = account.config.proxy?.trim(); if (accountProxy) { @@ -31,7 +31,7 @@ export function resolveDiscordProxyFetchByUrl( export function resolveDiscordProxyFetchForAccount( account: Pick, - cfg?: OpenClawConfig, + cfg: OpenClawConfig, runtime?: Pick, ): typeof fetch | undefined { return resolveDiscordProxyFetchByUrl(resolveDiscordProxyUrl(account, cfg), runtime); diff --git a/extensions/discord/src/recipient-resolution.ts b/extensions/discord/src/recipient-resolution.ts index c61f74e63ee..d87c5cdf2f8 100644 --- a/extensions/discord/src/recipient-resolution.ts +++ b/extensions/discord/src/recipient-resolution.ts @@ -15,8 +15,8 @@ type DiscordRecipient = export async function parseAndResolveRecipient( raw: string, + cfg: OpenClawConfig, accountId?: string, - cfg?: OpenClawConfig, parseOptions: DiscordTargetParseOptions = {}, ): Promise { if (!cfg) { diff --git a/extensions/discord/src/send.channels.ts b/extensions/discord/src/send.channels.ts index c77150e4f8c..ae7aef9e564 100644 --- a/extensions/discord/src/send.channels.ts +++ b/extensions/discord/src/send.channels.ts @@ -11,7 +11,7 @@ import type { export async function createChannelDiscord( payload: DiscordChannelCreate, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); const body: Record = { @@ -39,7 +39,7 @@ export async function createChannelDiscord( export async function editChannelDiscord( payload: DiscordChannelEdit, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); const body: Record = {}; @@ -84,13 +84,13 @@ export async function editChannelDiscord( })) as APIChannel; } -export async function deleteChannelDiscord(channelId: string, opts: DiscordReactOpts = {}) { +export async function deleteChannelDiscord(channelId: string, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); await rest.delete(Routes.channel(channelId)); return { ok: true, channelId }; } -export async function moveChannelDiscord(payload: DiscordChannelMove, opts: DiscordReactOpts = {}) { +export async function moveChannelDiscord(payload: DiscordChannelMove, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); const body: Array> = [ { @@ -105,7 +105,7 @@ export async function moveChannelDiscord(payload: DiscordChannelMove, opts: Disc export async function setChannelPermissionDiscord( payload: DiscordChannelPermissionSet, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ) { const rest = resolveDiscordRest(opts); const body: Record = { @@ -124,7 +124,7 @@ export async function setChannelPermissionDiscord( export async function removeChannelPermissionDiscord( channelId: string, targetId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ) { const rest = resolveDiscordRest(opts); await rest.delete(`/channels/${channelId}/permissions/${targetId}`); diff --git a/extensions/discord/src/send.components.ts b/extensions/discord/src/send.components.ts index 35f19c20ba7..59c32f0ca6e 100644 --- a/extensions/discord/src/send.components.ts +++ b/extensions/discord/src/send.components.ts @@ -265,8 +265,8 @@ export async function sendDiscordComponentMessage( const cfg = requireRuntimeConfig(opts.cfg, "Discord component send"); const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId }); - const { token, rest, request } = createDiscordClient(opts, cfg); - const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg); + const { token, rest, request } = createDiscordClient({ ...opts, cfg }); + const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId); const { channelId } = await resolveChannelId(rest, recipient, request); const channelType = await resolveDiscordChannelType(rest, channelId); @@ -325,8 +325,8 @@ export async function editDiscordComponentMessage( ): Promise { const cfg = requireRuntimeConfig(opts.cfg, "Discord component edit"); const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId }); - const { token, rest, request } = createDiscordClient(opts, cfg); - const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg); + const { token, rest, request } = createDiscordClient({ ...opts, cfg }); + const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId); const { channelId } = await resolveChannelId(rest, recipient, request); const { body, buildResult } = await buildDiscordComponentPayload({ spec, diff --git a/extensions/discord/src/send.emojis-stickers.ts b/extensions/discord/src/send.emojis-stickers.ts index 43208c31468..f768ece979c 100644 --- a/extensions/discord/src/send.emojis-stickers.ts +++ b/extensions/discord/src/send.emojis-stickers.ts @@ -5,12 +5,12 @@ import { normalizeEmojiName, resolveDiscordRest } from "./send.shared.js"; import type { DiscordEmojiUpload, DiscordReactOpts, DiscordStickerUpload } from "./send.types.js"; import { DISCORD_MAX_EMOJI_BYTES, DISCORD_MAX_STICKER_BYTES } from "./send.types.js"; -export async function listGuildEmojisDiscord(guildId: string, opts: DiscordReactOpts = {}) { +export async function listGuildEmojisDiscord(guildId: string, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); return await rest.get(Routes.guildEmojis(guildId)); } -export async function uploadEmojiDiscord(payload: DiscordEmojiUpload, opts: DiscordReactOpts = {}) { +export async function uploadEmojiDiscord(payload: DiscordEmojiUpload, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_EMOJI_BYTES); const contentType = normalizeOptionalLowercaseString(media.contentType); @@ -31,10 +31,7 @@ export async function uploadEmojiDiscord(payload: DiscordEmojiUpload, opts: Disc }); } -export async function uploadStickerDiscord( - payload: DiscordStickerUpload, - opts: DiscordReactOpts = {}, -) { +export async function uploadStickerDiscord(payload: DiscordStickerUpload, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_STICKER_BYTES); const contentType = normalizeOptionalLowercaseString(media.contentType); diff --git a/extensions/discord/src/send.guild.ts b/extensions/discord/src/send.guild.ts index e6191577853..7a7f42ccc4d 100644 --- a/extensions/discord/src/send.guild.ts +++ b/extensions/discord/src/send.guild.ts @@ -21,7 +21,7 @@ import { DISCORD_MAX_EVENT_COVER_BYTES } from "./send.types.js"; export async function fetchMemberInfoDiscord( guildId: string, userId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.get(Routes.guildMember(guildId, userId))) as APIGuildMember; @@ -29,19 +29,19 @@ export async function fetchMemberInfoDiscord( export async function fetchRoleInfoDiscord( guildId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.get(Routes.guildRoles(guildId))) as APIRole[]; } -export async function addRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts = {}) { +export async function addRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); await rest.put(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId)); return { ok: true }; } -export async function removeRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts = {}) { +export async function removeRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); await rest.delete(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId)); return { ok: true }; @@ -49,7 +49,7 @@ export async function removeRoleDiscord(payload: DiscordRoleChange, opts: Discor export async function fetchChannelInfoDiscord( channelId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.get(Routes.channel(channelId))) as APIChannel; @@ -57,7 +57,7 @@ export async function fetchChannelInfoDiscord( export async function listGuildChannelsDiscord( guildId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.get(Routes.guildChannels(guildId))) as APIChannel[]; @@ -66,7 +66,7 @@ export async function listGuildChannelsDiscord( export async function fetchVoiceStatusDiscord( guildId: string, userId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.get(Routes.guildVoiceState(guildId, userId))) as APIVoiceState; @@ -74,7 +74,7 @@ export async function fetchVoiceStatusDiscord( export async function listScheduledEventsDiscord( guildId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.get(Routes.guildScheduledEvents(guildId))) as APIGuildScheduledEvent[]; @@ -102,7 +102,7 @@ export async function resolveEventCoverImage( export async function createScheduledEventDiscord( guildId: string, payload: RESTPostAPIGuildScheduledEventJSONBody, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.post(Routes.guildScheduledEvents(guildId), { @@ -112,7 +112,7 @@ export async function createScheduledEventDiscord( export async function timeoutMemberDiscord( payload: DiscordTimeoutTarget, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); let until = payload.until; @@ -128,10 +128,7 @@ export async function timeoutMemberDiscord( })) as APIGuildMember; } -export async function kickMemberDiscord( - payload: DiscordModerationTarget, - opts: DiscordReactOpts = {}, -) { +export async function kickMemberDiscord(payload: DiscordModerationTarget, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); await rest.delete(Routes.guildMember(payload.guildId, payload.userId), { headers: payload.reason @@ -143,7 +140,7 @@ export async function kickMemberDiscord( export async function banMemberDiscord( payload: DiscordModerationTarget & { deleteMessageDays?: number }, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ) { const rest = resolveDiscordRest(opts); const deleteMessageDays = diff --git a/extensions/discord/src/send.messages.ts b/extensions/discord/src/send.messages.ts index 54484def68f..3a0234cf157 100644 --- a/extensions/discord/src/send.messages.ts +++ b/extensions/discord/src/send.messages.ts @@ -13,7 +13,7 @@ import type { export async function readMessagesDiscord( channelId: string, query: DiscordMessageQuery = {}, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); const limit = @@ -39,7 +39,7 @@ export async function readMessagesDiscord( export async function fetchMessageDiscord( channelId: string, messageId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.get(Routes.channelMessage(channelId, messageId))) as APIMessage; @@ -49,7 +49,7 @@ export async function editMessageDiscord( channelId: string, messageId: string, payload: DiscordMessageEdit, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.patch(Routes.channelMessage(channelId, messageId), { @@ -60,7 +60,7 @@ export async function editMessageDiscord( export async function deleteMessageDiscord( channelId: string, messageId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ) { const rest = resolveDiscordRest(opts); await rest.delete(Routes.channelMessage(channelId, messageId)); @@ -70,7 +70,7 @@ export async function deleteMessageDiscord( export async function pinMessageDiscord( channelId: string, messageId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ) { const rest = resolveDiscordRest(opts); await rest.put(Routes.channelPin(channelId, messageId)); @@ -80,7 +80,7 @@ export async function pinMessageDiscord( export async function unpinMessageDiscord( channelId: string, messageId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ) { const rest = resolveDiscordRest(opts); await rest.delete(Routes.channelPin(channelId, messageId)); @@ -89,7 +89,7 @@ export async function unpinMessageDiscord( export async function listPinsDiscord( channelId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); return (await rest.get(Routes.channelPins(channelId))) as APIMessage[]; @@ -98,7 +98,7 @@ export async function listPinsDiscord( export async function createThreadDiscord( channelId: string, payload: DiscordThreadCreate, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ) { const rest = resolveDiscordRest(opts); const body: Record = { name: payload.name }; @@ -150,7 +150,7 @@ export async function createThreadDiscord( return thread; } -export async function listThreadsDiscord(payload: DiscordThreadList, opts: DiscordReactOpts = {}) { +export async function listThreadsDiscord(payload: DiscordThreadList, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); if (payload.includeArchived) { if (!payload.channelId) { @@ -168,10 +168,7 @@ export async function listThreadsDiscord(payload: DiscordThreadList, opts: Disco return await rest.get(Routes.guildActiveThreads(payload.guildId)); } -export async function searchMessagesDiscord( - query: DiscordSearchQuery, - opts: DiscordReactOpts = {}, -) { +export async function searchMessagesDiscord(query: DiscordSearchQuery, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); const params = new URLSearchParams(); params.set("content", query.content); diff --git a/extensions/discord/src/send.outbound.ts b/extensions/discord/src/send.outbound.ts index d21f3692392..cb7ab8d3962 100644 --- a/extensions/discord/src/send.outbound.ts +++ b/extensions/discord/src/send.outbound.ts @@ -129,8 +129,8 @@ async function resolveDiscordSendTarget( opts: DiscordSendOpts, ): Promise<{ rest: RequestClient; request: DiscordClientRequest; channelId: string }> { const cfg = requireRuntimeConfig(opts.cfg, "Discord send target resolution"); - const { rest, request } = createDiscordClient(opts, cfg); - const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg); + const { rest, request } = createDiscordClient({ ...opts, cfg }); + const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId); const { channelId } = await resolveChannelId(rest, recipient, request); return { rest, request, channelId }; } @@ -159,8 +159,8 @@ export async function sendMessageDiscord( const textWithMentions = rewriteDiscordKnownMentions(textWithTables, { accountId: accountInfo.accountId, }); - const { token, rest, request } = createDiscordClient(opts, cfg); - const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg); + const { token, rest, request } = createDiscordClient({ ...opts, cfg }); + const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId); const { channelId } = await resolveChannelId(rest, recipient, request); // Forum/Media channels reject POST /messages; auto-create a thread post instead. @@ -543,19 +543,18 @@ export async function sendVoiceMessageDiscord( let token: string | undefined; let rest: RequestClient | undefined; let channelId: string | undefined; - let cfg: OpenClawConfig | undefined; + const cfg = requireRuntimeConfig(opts.cfg, "Discord voice send"); try { - cfg = requireRuntimeConfig(opts.cfg, "Discord voice send"); const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId, }); - const client = createDiscordClient(opts, cfg); + const client = createDiscordClient({ ...opts, cfg }); token = client.token; rest = client.rest; const request = client.request; - const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg); + const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId); channelId = (await resolveChannelId(rest, recipient, request)).channelId; // Convert to OGG/Opus if needed @@ -589,7 +588,7 @@ export async function sendVoiceMessageDiscord( return toDiscordSendResult(result, channelId); } catch (err) { - if (channelId && rest && token && cfg) { + if (channelId && rest && token) { throw await buildDiscordSendError(err, { channelId, cfg, diff --git a/extensions/discord/src/send.permissions.authz.test.ts b/extensions/discord/src/send.permissions.authz.test.ts index 29926244f4f..521a72b5cd1 100644 --- a/extensions/discord/src/send.permissions.authz.test.ts +++ b/extensions/discord/src/send.permissions.authz.test.ts @@ -1,10 +1,12 @@ import type { RequestClient } from "@buape/carbon"; import { PermissionFlagsBits, Routes } from "discord-api-types/v10"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mockRest = vi.hoisted(() => ({ get: vi.fn(), })); +const DISCORD_TEST_OPTS = { cfg: {} as OpenClawConfig }; vi.mock("./client.js", () => ({ resolveDiscordRest: () => mockRest as unknown as RequestClient, @@ -59,7 +61,11 @@ describe("discord guild permission authorization", () => { it("returns null when user is not a guild member", async () => { mockRest.get.mockRejectedValueOnce(new Error("404 Member not found")); - const result = await fetchMemberGuildPermissionsDiscord("guild-1", "user-1"); + const result = await fetchMemberGuildPermissionsDiscord( + "guild-1", + "user-1", + DISCORD_TEST_OPTS, + ); expect(result).toBeNull(); }); @@ -72,7 +78,11 @@ describe("discord guild permission authorization", () => { memberRoles: ["role-mod"], }); - const result = await fetchMemberGuildPermissionsDiscord("guild-1", "user-1"); + const result = await fetchMemberGuildPermissionsDiscord( + "guild-1", + "user-1", + DISCORD_TEST_OPTS, + ); expect(result).not.toBeNull(); expect((result! & PermissionFlagsBits.ViewChannel) === PermissionFlagsBits.ViewChannel).toBe( true, @@ -93,9 +103,12 @@ describe("discord guild permission authorization", () => { memberRoles: ["role-mod"], }); - const result = await hasAnyGuildPermissionDiscord("guild-1", "user-1", [ - PermissionFlagsBits.KickMembers, - ]); + const result = await hasAnyGuildPermissionDiscord( + "guild-1", + "user-1", + [PermissionFlagsBits.KickMembers], + DISCORD_TEST_OPTS, + ); expect(result).toBe(true); }); @@ -111,9 +124,12 @@ describe("discord guild permission authorization", () => { memberRoles: ["role-admin"], }); - const result = await hasAnyGuildPermissionDiscord("guild-1", "user-1", [ - PermissionFlagsBits.KickMembers, - ]); + const result = await hasAnyGuildPermissionDiscord( + "guild-1", + "user-1", + [PermissionFlagsBits.KickMembers], + DISCORD_TEST_OPTS, + ); expect(result).toBe(true); }); @@ -123,10 +139,12 @@ describe("discord guild permission authorization", () => { memberRoles: [], }); - const result = await hasAnyGuildPermissionDiscord("guild-1", "user-1", [ - PermissionFlagsBits.BanMembers, - PermissionFlagsBits.KickMembers, - ]); + const result = await hasAnyGuildPermissionDiscord( + "guild-1", + "user-1", + [PermissionFlagsBits.BanMembers, PermissionFlagsBits.KickMembers], + DISCORD_TEST_OPTS, + ); expect(result).toBe(false); }); }); @@ -141,10 +159,12 @@ describe("discord guild permission authorization", () => { memberRoles: ["role-mod"], }); - const result = await hasAllGuildPermissionsDiscord("guild-1", "user-1", [ - PermissionFlagsBits.KickMembers, - PermissionFlagsBits.BanMembers, - ]); + const result = await hasAllGuildPermissionsDiscord( + "guild-1", + "user-1", + [PermissionFlagsBits.KickMembers, PermissionFlagsBits.BanMembers], + DISCORD_TEST_OPTS, + ); expect(result).toBe(false); }); @@ -157,10 +177,12 @@ describe("discord guild permission authorization", () => { memberRoles: ["role-admin"], }); - const result = await hasAllGuildPermissionsDiscord("guild-1", "user-1", [ - PermissionFlagsBits.KickMembers, - PermissionFlagsBits.BanMembers, - ]); + const result = await hasAllGuildPermissionsDiscord( + "guild-1", + "user-1", + [PermissionFlagsBits.KickMembers, PermissionFlagsBits.BanMembers], + DISCORD_TEST_OPTS, + ); expect(result).toBe(true); }); }); diff --git a/extensions/discord/src/send.permissions.ts b/extensions/discord/src/send.permissions.ts index dfe4d91c81b..22d83710d61 100644 --- a/extensions/discord/src/send.permissions.ts +++ b/extensions/discord/src/send.permissions.ts @@ -60,7 +60,7 @@ async function fetchBotUserId(rest: RequestClient) { export async function fetchMemberGuildPermissionsDiscord( guildId: string, userId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); try { @@ -96,7 +96,7 @@ async function hasGuildPermissionsDiscord( userId: string, requiredPermissions: bigint[], check: (permissions: bigint, requiredPermissions: bigint[]) => boolean, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const permissions = await fetchMemberGuildPermissionsDiscord(guildId, userId, opts); if (permissions === null) { @@ -115,7 +115,7 @@ export async function hasAnyGuildPermissionDiscord( guildId: string, userId: string, requiredPermissions: bigint[], - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { return await hasGuildPermissionsDiscord( guildId, @@ -134,7 +134,7 @@ export async function hasAllGuildPermissionsDiscord( guildId: string, userId: string, requiredPermissions: bigint[], - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { return await hasGuildPermissionsDiscord( guildId, @@ -153,7 +153,7 @@ export const hasGuildPermissionDiscord = hasAnyGuildPermissionDiscord; export async function fetchChannelPermissionsDiscord( channelId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise { const rest = resolveDiscordRest(opts); const channel = (await rest.get(Routes.channel(channelId))) as APIChannel; diff --git a/extensions/discord/src/send.reactions.ts b/extensions/discord/src/send.reactions.ts index b68629b97ab..67f091d05ec 100644 --- a/extensions/discord/src/send.reactions.ts +++ b/extensions/discord/src/send.reactions.ts @@ -13,7 +13,7 @@ import type { } from "./send.types.js"; function createDiscordReactionRuntimeClient(opts: DiscordReactionRuntimeContext) { - return createDiscordClient(opts, opts.cfg); + return createDiscordClient(opts); } function resolveDiscordReactionClient(opts: DiscordReactOpts) { @@ -23,7 +23,7 @@ function resolveDiscordReactionClient(opts: DiscordReactOpts) { ); } const cfg = requireRuntimeConfig(opts.cfg, "Discord reactions"); - return createDiscordClient(opts, cfg); + return createDiscordClient({ ...opts, cfg }); } function isDiscordReactionRuntimeContext( @@ -36,7 +36,7 @@ export async function reactMessageDiscord( channelId: string, messageId: string, emoji: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ) { const { rest, request } = isDiscordReactionRuntimeContext(opts) ? createDiscordReactionRuntimeClient(opts) @@ -53,7 +53,7 @@ export async function removeReactionDiscord( channelId: string, messageId: string, emoji: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ) { const { rest } = isDiscordReactionRuntimeContext(opts) ? createDiscordReactionRuntimeClient(opts) @@ -66,7 +66,7 @@ export async function removeReactionDiscord( export async function removeOwnReactionsDiscord( channelId: string, messageId: string, - opts: DiscordReactOpts = {}, + opts: DiscordReactOpts, ): Promise<{ ok: true; removed: string[] }> { const { rest } = isDiscordReactionRuntimeContext(opts) ? createDiscordReactionRuntimeClient(opts) @@ -99,7 +99,7 @@ export async function removeOwnReactionsDiscord( export async function fetchReactionsDiscord( channelId: string, messageId: string, - opts: DiscordReactOpts & { limit?: number } = {}, + opts: DiscordReactOpts & { limit?: number }, ): Promise { const { rest } = isDiscordReactionRuntimeContext(opts) ? createDiscordReactionRuntimeClient(opts) diff --git a/extensions/discord/src/send.shared.ts b/extensions/discord/src/send.shared.ts index 0d0c8d16cfc..f54cabb7f45 100644 --- a/extensions/discord/src/send.shared.ts +++ b/extensions/discord/src/send.shared.ts @@ -234,10 +234,10 @@ async function resolveDiscordTargetChannelId( opts: DiscordClientOpts & { cfg: OpenClawConfig }, ): Promise<{ channelId: string; dm?: boolean }> { const cfg = requireRuntimeConfig(opts.cfg, "Discord target channel resolution"); - const recipient = await parseAndResolveRecipient(raw, opts.accountId, cfg, { + const recipient = await parseAndResolveRecipient(raw, cfg, opts.accountId, { defaultKind: "channel", }); - const { rest, request } = createDiscordClient(opts, cfg); + const { rest, request } = createDiscordClient(opts); return await resolveChannelId(rest, recipient, request); } diff --git a/extensions/discord/src/send.types.ts b/extensions/discord/src/send.types.ts index 2caf17bbf3a..c6a578feaff 100644 --- a/extensions/discord/src/send.types.ts +++ b/extensions/discord/src/send.types.ts @@ -37,7 +37,7 @@ export type DiscordRuntimeAccountContext = { }; export type DiscordReactOpts = { - cfg?: OpenClawConfig; + cfg: OpenClawConfig; accountId?: string; token?: string; rest?: RequestClient; diff --git a/extensions/discord/src/send.typing.test.ts b/extensions/discord/src/send.typing.test.ts index 99ba91f123c..b304612cee2 100644 --- a/extensions/discord/src/send.typing.test.ts +++ b/extensions/discord/src/send.typing.test.ts @@ -1,8 +1,10 @@ import type { RequestClient } from "@buape/carbon"; import { Routes } from "discord-api-types/v10"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveDiscordRestMock = vi.hoisted(() => vi.fn()); +const DEFAULT_CFG = {} as OpenClawConfig; vi.mock("./client.js", () => ({ resolveDiscordRest: resolveDiscordRestMock, @@ -25,9 +27,9 @@ describe("sendTypingDiscord", () => { post, } as unknown as RequestClient); - const result = await sendTypingDiscord("12345", { accountId: "ops" }); + const result = await sendTypingDiscord("12345", { cfg: DEFAULT_CFG, accountId: "ops" }); - expect(resolveDiscordRestMock).toHaveBeenCalledWith({ accountId: "ops" }); + expect(resolveDiscordRestMock).toHaveBeenCalledWith({ cfg: DEFAULT_CFG, accountId: "ops" }); expect(post).toHaveBeenCalledWith(Routes.channelTyping("12345")); expect(result).toEqual({ ok: true, channelId: "12345" }); }); diff --git a/extensions/discord/src/send.typing.ts b/extensions/discord/src/send.typing.ts index cf1db7fa484..294685950cd 100644 --- a/extensions/discord/src/send.typing.ts +++ b/extensions/discord/src/send.typing.ts @@ -2,7 +2,7 @@ import { Routes } from "discord-api-types/v10"; import { resolveDiscordRest } from "./client.js"; import type { DiscordReactOpts } from "./send.types.js"; -export async function sendTypingDiscord(channelId: string, opts: DiscordReactOpts = {}) { +export async function sendTypingDiscord(channelId: string, opts: DiscordReactOpts) { const rest = resolveDiscordRest(opts); await rest.post(Routes.channelTyping(channelId)); return { ok: true, channelId }; diff --git a/extensions/discord/src/token.ts b/extensions/discord/src/token.ts index bb66deb3d6b..37f843cdea0 100644 --- a/extensions/discord/src/token.ts +++ b/extensions/discord/src/token.ts @@ -19,7 +19,7 @@ export function normalizeDiscordToken(raw: unknown, path: string): string | unde } export function resolveDiscordToken( - cfg?: OpenClawConfig, + cfg: OpenClawConfig, opts: { accountId?: string | null; envToken?: string | null } = {}, ): DiscordTokenResolution { const accountId = normalizeAccountId(opts.accountId);