fix(outbound): unify resolved cfg threading across send paths (#33987)

This commit is contained in:
Josh Avant
2026-03-04 00:20:44 -06:00
committed by GitHub
parent 4d183af0cf
commit 646817dd80
62 changed files with 1780 additions and 117 deletions

View File

@@ -5,7 +5,7 @@ import {
type RequestClient,
} from "@buape/carbon";
import { ChannelType, Routes } from "discord-api-types/v10";
import { loadConfig } from "../config/config.js";
import { loadConfig, type OpenClawConfig } from "../config/config.js";
import { recordChannelActivity } from "../infra/channel-activity.js";
import { loadWebMedia } from "../web/media.js";
import { resolveDiscordAccount } from "./accounts.js";
@@ -41,6 +41,7 @@ function extractComponentAttachmentNames(spec: DiscordComponentMessageSpec): str
}
type DiscordComponentSendOpts = {
cfg?: OpenClawConfig;
accountId?: string;
token?: string;
rest?: RequestClient;
@@ -58,10 +59,10 @@ export async function sendDiscordComponentMessage(
spec: DiscordComponentMessageSpec,
opts: DiscordComponentSendOpts = {},
): Promise<DiscordSendResult> {
const cfg = loadConfig();
const cfg = opts.cfg ?? loadConfig();
const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId });
const { token, rest, request } = createDiscordClient(opts, cfg);
const recipient = await parseAndResolveRecipient(to, opts.accountId);
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
const { channelId } = await resolveChannelId(rest, recipient, request);
const channelType = await resolveDiscordChannelType(rest, channelId);

View File

@@ -4,7 +4,7 @@ import path from "node:path";
import { serializePayload, type MessagePayloadObject, type RequestClient } from "@buape/carbon";
import { ChannelType, Routes } from "discord-api-types/v10";
import { resolveChunkMode } from "../auto-reply/chunk.js";
import { loadConfig } from "../config/config.js";
import { loadConfig, type OpenClawConfig } from "../config/config.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import { recordChannelActivity } from "../infra/channel-activity.js";
import type { RetryConfig } from "../infra/retry.js";
@@ -44,6 +44,7 @@ import {
} from "./voice-message.js";
type DiscordSendOpts = {
cfg?: OpenClawConfig;
token?: string;
accountId?: string;
mediaUrl?: string;
@@ -121,9 +122,9 @@ async function resolveDiscordSendTarget(
to: string,
opts: DiscordSendOpts,
): Promise<{ rest: RequestClient; request: DiscordClientRequest; channelId: string }> {
const cfg = loadConfig();
const cfg = opts.cfg ?? loadConfig();
const { rest, request } = createDiscordClient(opts, cfg);
const recipient = await parseAndResolveRecipient(to, opts.accountId);
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
const { channelId } = await resolveChannelId(rest, recipient, request);
return { rest, request, channelId };
}
@@ -133,7 +134,7 @@ export async function sendMessageDiscord(
text: string,
opts: DiscordSendOpts = {},
): Promise<DiscordSendResult> {
const cfg = loadConfig();
const cfg = opts.cfg ?? loadConfig();
const accountInfo = resolveDiscordAccount({
cfg,
accountId: opts.accountId,
@@ -149,7 +150,7 @@ export async function sendMessageDiscord(
accountId: accountInfo.accountId,
});
const { token, rest, request } = createDiscordClient(opts, cfg);
const recipient = await parseAndResolveRecipient(to, opts.accountId);
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
const { channelId } = await resolveChannelId(rest, recipient, request);
// Forum/Media channels reject POST /messages; auto-create a thread post instead.
@@ -310,6 +311,7 @@ export async function sendMessageDiscord(
}
type DiscordWebhookSendOpts = {
cfg?: OpenClawConfig;
webhookId: string;
webhookToken: string;
accountId?: string;
@@ -385,7 +387,7 @@ export async function sendWebhookMessageDiscord(
};
try {
const account = resolveDiscordAccount({
cfg: loadConfig(),
cfg: opts.cfg ?? loadConfig(),
accountId: opts.accountId,
});
recordChannelActivity({
@@ -464,6 +466,7 @@ export async function sendPollDiscord(
}
type VoiceMessageOpts = {
cfg?: OpenClawConfig;
token?: string;
accountId?: string;
verbose?: boolean;
@@ -509,7 +512,7 @@ export async function sendVoiceMessageDiscord(
let channelId: string | undefined;
try {
const cfg = loadConfig();
const cfg = opts.cfg ?? loadConfig();
const accountInfo = resolveDiscordAccount({
cfg,
accountId: opts.accountId,
@@ -518,7 +521,7 @@ export async function sendVoiceMessageDiscord(
token = client.token;
rest = client.rest;
const request = client.request;
const recipient = await parseAndResolveRecipient(to, opts.accountId);
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
channelId = (await resolveChannelId(rest, recipient, request)).channelId;
// Convert to OGG/Opus if needed

View File

@@ -5,7 +5,6 @@ import {
createDiscordClient,
formatReactionEmoji,
normalizeReactionEmoji,
resolveDiscordRest,
} from "./send.shared.js";
import type { DiscordReactionSummary, DiscordReactOpts } from "./send.types.js";
@@ -15,7 +14,7 @@ export async function reactMessageDiscord(
emoji: string,
opts: DiscordReactOpts = {},
) {
const cfg = loadConfig();
const cfg = opts.cfg ?? loadConfig();
const { rest, request } = createDiscordClient(opts, cfg);
const encoded = normalizeReactionEmoji(emoji);
await request(
@@ -31,7 +30,8 @@ export async function removeReactionDiscord(
emoji: string,
opts: DiscordReactOpts = {},
) {
const rest = resolveDiscordRest(opts);
const cfg = opts.cfg ?? loadConfig();
const { rest } = createDiscordClient(opts, cfg);
const encoded = normalizeReactionEmoji(emoji);
await rest.delete(Routes.channelMessageOwnReaction(channelId, messageId, encoded));
return { ok: true };
@@ -42,7 +42,8 @@ export async function removeOwnReactionsDiscord(
messageId: string,
opts: DiscordReactOpts = {},
): Promise<{ ok: true; removed: string[] }> {
const rest = resolveDiscordRest(opts);
const cfg = opts.cfg ?? loadConfig();
const { rest } = createDiscordClient(opts, cfg);
const message = (await rest.get(Routes.channelMessage(channelId, messageId))) as {
reactions?: Array<{ emoji: { id?: string | null; name?: string | null } }>;
};
@@ -73,7 +74,8 @@ export async function fetchReactionsDiscord(
messageId: string,
opts: DiscordReactOpts & { limit?: number } = {},
): Promise<DiscordReactionSummary[]> {
const rest = resolveDiscordRest(opts);
const cfg = opts.cfg ?? loadConfig();
const { rest } = createDiscordClient(opts, cfg);
const message = (await rest.get(Routes.channelMessage(channelId, messageId))) as {
reactions?: Array<{
count: number;

View File

@@ -10,7 +10,7 @@ import { PollLayoutType } from "discord-api-types/payloads/v10";
import type { RESTAPIPoll } from "discord-api-types/rest/v10";
import { Routes, type APIChannel, type APIEmbed } from "discord-api-types/v10";
import type { ChunkMode } from "../auto-reply/chunk.js";
import { loadConfig } from "../config/config.js";
import { loadConfig, type OpenClawConfig } from "../config/config.js";
import type { RetryRunner } from "../infra/retry-policy.js";
import { buildOutboundMediaLoadOptions } from "../media/load-options.js";
import { normalizePollDurationHours, normalizePollInput, type PollInput } from "../polls.js";
@@ -80,9 +80,10 @@ function parseRecipient(raw: string): DiscordRecipient {
export async function parseAndResolveRecipient(
raw: string,
accountId?: string,
cfg?: OpenClawConfig,
): Promise<DiscordRecipient> {
const cfg = loadConfig();
const accountInfo = resolveDiscordAccount({ cfg, accountId });
const resolvedCfg = cfg ?? loadConfig();
const accountInfo = resolveDiscordAccount({ cfg: resolvedCfg, accountId });
// First try to resolve using directory lookup (handles usernames)
const trimmed = raw.trim();
@@ -93,7 +94,7 @@ export async function parseAndResolveRecipient(
const resolved = await resolveDiscordTarget(
raw,
{
cfg,
cfg: resolvedCfg,
accountId: accountInfo.accountId,
},
parseOptions,

View File

@@ -1,4 +1,5 @@
import type { RequestClient } from "@buape/carbon";
import type { OpenClawConfig } from "../config/config.js";
import type { RetryConfig } from "../infra/retry.js";
export class DiscordSendError extends Error {
@@ -28,6 +29,7 @@ export type DiscordSendResult = {
};
export type DiscordReactOpts = {
cfg?: OpenClawConfig;
token?: string;
accountId?: string;
rest?: RequestClient;

View File

@@ -2,6 +2,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { sendWebhookMessageDiscord } from "./send.js";
const recordChannelActivityMock = vi.hoisted(() => vi.fn());
const loadConfigMock = vi.hoisted(() => vi.fn(() => ({ channels: { discord: {} } })));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => loadConfigMock(),
};
});
vi.mock("../infra/channel-activity.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../infra/channel-activity.js")>();
@@ -14,6 +23,7 @@ vi.mock("../infra/channel-activity.js", async (importOriginal) => {
describe("sendWebhookMessageDiscord activity", () => {
beforeEach(() => {
recordChannelActivityMock.mockClear();
loadConfigMock.mockClear();
vi.stubGlobal(
"fetch",
vi.fn(async () => {
@@ -30,7 +40,15 @@ describe("sendWebhookMessageDiscord activity", () => {
});
it("records outbound channel activity for webhook sends", async () => {
const cfg = {
channels: {
discord: {
token: "resolved-token",
},
},
};
const result = await sendWebhookMessageDiscord("hello world", {
cfg,
webhookId: "wh-1",
webhookToken: "tok-1",
accountId: "runtime",
@@ -46,5 +64,6 @@ describe("sendWebhookMessageDiscord activity", () => {
accountId: "runtime",
direction: "outbound",
});
expect(loadConfigMock).not.toHaveBeenCalled();
});
});