diff --git a/extensions/qqbot/src/config.test.ts b/extensions/qqbot/src/config.test.ts index b6cf4a82840..55eda7e3b37 100644 --- a/extensions/qqbot/src/config.test.ts +++ b/extensions/qqbot/src/config.test.ts @@ -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: { diff --git a/extensions/qqbot/src/config.ts b/extensions/qqbot/src/config.ts index 5ec3f5d7b1a..872933a4f9d 100644 --- a/extensions/qqbot/src/config.ts +++ b/extensions/qqbot/src/config.ts @@ -11,6 +11,15 @@ export const DEFAULT_ACCOUNT_ID = "default"; interface QQBotChannelConfig extends QQBotAccountConfig { accounts?: Record; + 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 = {}; diff --git a/extensions/qqbot/src/proactive.test.ts b/extensions/qqbot/src/proactive.test.ts new file mode 100644 index 00000000000..1b46cebfff2 --- /dev/null +++ b/extensions/qqbot/src/proactive.test.ts @@ -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"); + }); +}); diff --git a/extensions/qqbot/src/proactive.ts b/extensions/qqbot/src/proactive.ts index fda9affeace..cd3764c90f2 100644 --- a/extensions/qqbot/src/proactive.ts +++ b/extensions/qqbot/src/proactive.ts @@ -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 { - 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> { const results: Array<{ to: string; result: ProactiveSendResult }> = [];