fix: honor qqbot default account config

This commit is contained in:
Tak Hoffman 2026-04-03 12:36:46 -05:00
parent b929a4c27d
commit acfa09679c
No known key found for this signature in database
4 changed files with 137 additions and 5 deletions

View File

@ -5,7 +5,11 @@ import { validateJsonSchemaValue } from "../../../src/plugins/schema-validator.j
import { qqbotPlugin } from "./channel.js";
import { qqbotSetupPlugin } from "./channel.setup.js";
import { QQBotConfigSchema } from "./config-schema.js";
import { DEFAULT_ACCOUNT_ID, resolveQQBotAccount } from "./config.js";
import {
DEFAULT_ACCOUNT_ID,
resolveDefaultQQBotAccountId,
resolveQQBotAccount,
} from "./config.js";
describe("qqbot config", () => {
it("accepts top-level speech overrides in the manifest schema", () => {
@ -62,6 +66,23 @@ describe("qqbot config", () => {
expect(result.ok).toBe(true);
});
it("honors configured defaultAccount when resolving the default QQ Bot account id", () => {
const cfg = {
channels: {
qqbot: {
defaultAccount: "bot2",
accounts: {
bot2: {
appId: "654321",
},
},
},
},
} as OpenClawConfig;
expect(resolveDefaultQQBotAccountId(cfg)).toBe("bot2");
});
it("accepts SecretRef-backed credentials in the runtime schema", () => {
const parsed = QQBotConfigSchema.safeParse({
defaultAccount: "bot2",
@ -142,6 +163,30 @@ describe("qqbot config", () => {
expect(resolved.config.upgradeMode).toBe("hot-reload");
});
it("uses configured defaultAccount when accountId is omitted", () => {
const cfg = {
channels: {
qqbot: {
defaultAccount: "bot2",
accounts: {
bot2: {
appId: "654321",
clientSecret: "secret-value",
name: "Bot Two",
},
},
},
},
} as OpenClawConfig;
const resolved = resolveQQBotAccount(cfg);
expect(resolved.accountId).toBe("bot2");
expect(resolved.appId).toBe("654321");
expect(resolved.clientSecret).toBe("secret-value");
expect(resolved.name).toBe("Bot Two");
});
it("rejects unresolved SecretRefs on runtime resolution", () => {
const cfg = {
channels: {

View File

@ -11,6 +11,15 @@ export const DEFAULT_ACCOUNT_ID = "default";
interface QQBotChannelConfig extends QQBotAccountConfig {
accounts?: Record<string, QQBotAccountConfig>;
defaultAccount?: string;
}
function normalizeConfiguredDefaultAccountId(raw: unknown): string | null {
if (typeof raw !== "string") {
return null;
}
const trimmed = raw.trim().toLowerCase();
return trimmed ? trimmed : null;
}
function normalizeQQBotAccountConfig(account: QQBotAccountConfig | undefined): QQBotAccountConfig {
@ -51,6 +60,14 @@ export function listQQBotAccountIds(cfg: OpenClawConfig): string[] {
/** Resolve the default QQBot account ID. */
export function resolveDefaultQQBotAccountId(cfg: OpenClawConfig): string {
const qqbot = cfg.channels?.qqbot as QQBotChannelConfig | undefined;
const configuredDefaultAccountId = normalizeConfiguredDefaultAccountId(qqbot?.defaultAccount);
if (
configuredDefaultAccountId &&
(configuredDefaultAccountId === DEFAULT_ACCOUNT_ID ||
Boolean(qqbot?.accounts?.[configuredDefaultAccountId]?.appId))
) {
return configuredDefaultAccountId;
}
if (qqbot?.appId || process.env.QQBOT_APP_ID) {
return DEFAULT_ACCOUNT_ID;
}
@ -69,7 +86,7 @@ export function resolveQQBotAccount(
accountId?: string | null,
opts?: { allowUnresolvedSecretRef?: boolean },
): ResolvedQQBotAccount {
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
const resolvedAccountId = accountId ?? resolveDefaultQQBotAccountId(cfg);
const qqbot = cfg.channels?.qqbot as QQBotChannelConfig | undefined;
let accountConfig: QQBotAccountConfig = {};

View File

@ -0,0 +1,64 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { sendProactive } from "./proactive.js";
const apiMocks = vi.hoisted(() => ({
getAccessToken: vi.fn(),
sendProactiveC2CMessage: vi.fn(),
}));
vi.mock("./api.js", () => ({
getAccessToken: apiMocks.getAccessToken,
sendProactiveC2CMessage: apiMocks.sendProactiveC2CMessage,
sendProactiveGroupMessage: vi.fn(),
sendChannelMessage: vi.fn(),
sendC2CImageMessage: vi.fn(),
sendGroupImageMessage: vi.fn(),
}));
describe("qqbot proactive sends", () => {
beforeEach(() => {
apiMocks.getAccessToken.mockReset();
apiMocks.sendProactiveC2CMessage.mockReset();
});
it("uses configured defaultAccount when accountId is omitted", async () => {
apiMocks.getAccessToken.mockResolvedValue("access-token");
apiMocks.sendProactiveC2CMessage.mockResolvedValue({
id: "msg-1",
timestamp: 123,
});
const cfg = {
channels: {
qqbot: {
defaultAccount: "bot2",
accounts: {
bot2: {
appId: "654321",
clientSecret: "secret-value",
},
},
},
},
} as OpenClawConfig;
const result = await sendProactive(
{
to: "openid-1",
text: "hello",
},
cfg,
);
expect(apiMocks.getAccessToken).toHaveBeenCalledWith("654321", "secret-value");
expect(apiMocks.sendProactiveC2CMessage).toHaveBeenCalledWith(
"654321",
"access-token",
"openid-1",
"hello",
);
expect(result.success).toBe(true);
expect(result.messageId).toBe("msg-1");
});
});

View File

@ -58,7 +58,7 @@ import {
sendC2CImageMessage,
sendGroupImageMessage,
} from "./api.js";
import { resolveQQBotAccount } from "./config.js";
import { resolveDefaultQQBotAccountId, resolveQQBotAccount } from "./config.js";
/** Look up a known user entry (adapter for the old proactive API shape). */
export function getKnownUser(
@ -98,7 +98,13 @@ export async function sendProactive(
options: ProactiveSendOptions,
cfg: OpenClawConfig,
): Promise<ProactiveSendResult> {
const { to, text, type = "c2c", imageUrl, accountId = "default" } = options;
const {
to,
text,
type = "c2c",
imageUrl,
accountId = resolveDefaultQQBotAccountId(cfg),
} = options;
const account = resolveQQBotAccount(cfg, accountId);
@ -174,7 +180,7 @@ export async function sendBulkProactiveMessage(
text: string,
type: "c2c" | "group",
cfg: OpenClawConfig,
accountId = "default",
accountId = resolveDefaultQQBotAccountId(cfg),
): Promise<Array<{ to: string; result: ProactiveSendResult }>> {
const results: Array<{ to: string; result: ProactiveSendResult }> = [];