openclaw/extensions/qqbot/src/proactive.ts

325 lines
8.7 KiB
TypeScript

/**
* QQ Bot proactive messaging helpers.
*
* This module sends proactive messages and manages known-user queries.
* Known-user storage is delegated to `./known-users.ts`.
*/
import type { ResolvedQQBotAccount } from "./types.js";
import { debugLog, debugError } from "./utils/debug-log.js";
// Re-export known-user types and functions from the canonical module.
export type { KnownUser } from "./known-users.js";
export {
recordKnownUser,
listKnownUsers as listKnownUsersFromStore,
getKnownUser as getKnownUserFromStore,
removeKnownUser as removeKnownUserFromStore,
clearKnownUsers as clearKnownUsersFromStore,
flushKnownUsers,
} from "./known-users.js";
import {
listKnownUsers as listKnownUsersImpl,
removeKnownUser as removeKnownUserImpl,
clearKnownUsers as clearKnownUsersImpl,
getKnownUser as getKnownUserImpl,
} from "./known-users.js";
/** Options for proactive message sending. */
export interface ProactiveSendOptions {
to: string;
text: string;
type?: "c2c" | "group" | "channel";
imageUrl?: string;
accountId?: string;
}
/** Result returned from proactive sends. */
export interface ProactiveSendResult {
success: boolean;
messageId?: string;
timestamp?: number | string;
error?: string;
}
/** Filters for listing known users. */
export interface ListKnownUsersOptions {
type?: "c2c" | "group" | "channel";
accountId?: string;
sortByLastInteraction?: boolean;
limit?: number;
}
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
getAccessToken,
sendProactiveC2CMessage,
sendProactiveGroupMessage,
sendChannelMessage,
sendC2CImageMessage,
sendGroupImageMessage,
} from "./api.js";
import { resolveQQBotAccount } from "./config.js";
/** Look up a known user entry (adapter for the old proactive API shape). */
export function getKnownUser(
type: string,
openid: string,
accountId: string,
): ReturnType<typeof getKnownUserImpl> {
return getKnownUserImpl(accountId, openid, type as "c2c" | "group");
}
/** List known users with optional filtering and sorting (adapter). */
export function listKnownUsers(
options?: ListKnownUsersOptions,
): ReturnType<typeof listKnownUsersImpl> {
const type = options?.type;
return listKnownUsersImpl({
type: type === "channel" ? undefined : (type as "c2c" | "group" | undefined),
accountId: options?.accountId,
limit: options?.limit,
sortBy: options?.sortByLastInteraction !== false ? "lastSeenAt" : undefined,
sortOrder: "desc",
});
}
/** Remove one known user entry (adapter). */
export function removeKnownUser(type: string, openid: string, accountId: string): boolean {
return removeKnownUserImpl(accountId, openid, type as "c2c" | "group");
}
/** Clear all known users, optionally scoped to a single account (adapter). */
export function clearKnownUsers(accountId?: string): number {
return clearKnownUsersImpl(accountId);
}
/** Resolve account config and send a proactive message. */
export async function sendProactive(
options: ProactiveSendOptions,
cfg: OpenClawConfig,
): Promise<ProactiveSendResult> {
const { to, text, type = "c2c", imageUrl, accountId = "default" } = options;
const account = resolveQQBotAccount(cfg, accountId);
if (!account.appId || !account.clientSecret) {
return {
success: false,
error: "QQBot not configured (missing appId or clientSecret)",
};
}
try {
const accessToken = await getAccessToken(account.appId, account.clientSecret);
if (imageUrl) {
try {
if (type === "c2c") {
await sendC2CImageMessage(account.appId, accessToken, to, imageUrl, undefined, undefined);
} else if (type === "group") {
await sendGroupImageMessage(
account.appId,
accessToken,
to,
imageUrl,
undefined,
undefined,
);
}
debugLog(`[qqbot:proactive] Sent image to ${type}:${to}`);
} catch (err) {
debugError(`[qqbot:proactive] Failed to send image: ${err}`);
}
}
let result: { id: string; timestamp: number | string };
if (type === "c2c") {
result = await sendProactiveC2CMessage(account.appId, accessToken, to, text);
} else if (type === "group") {
result = await sendProactiveGroupMessage(account.appId, accessToken, to, text);
} else if (type === "channel") {
return {
success: false,
error: "Channel proactive messages are not supported. Please use group or c2c.",
};
} else {
return {
success: false,
error: `Unknown message type: ${type}`,
};
}
debugLog(`[qqbot:proactive] Sent message to ${type}:${to}, id: ${result.id}`);
return {
success: true,
messageId: result.id,
timestamp: result.timestamp,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
debugError(`[qqbot:proactive] Failed to send message: ${message}`);
return {
success: false,
error: message,
};
}
}
/** Send one proactive message to each recipient. */
export async function sendBulkProactiveMessage(
recipients: string[],
text: string,
type: "c2c" | "group",
cfg: OpenClawConfig,
accountId = "default",
): Promise<Array<{ to: string; result: ProactiveSendResult }>> {
const results: Array<{ to: string; result: ProactiveSendResult }> = [];
for (const to of recipients) {
const result = await sendProactive({ to, text, type, accountId }, cfg);
results.push({ to, result });
// Add a small delay to reduce rate-limit pressure.
await new Promise((resolve) => setTimeout(resolve, 500));
}
return results;
}
/**
* Send a message to all known users.
*
* @param text Message content.
* @param cfg OpenClaw config.
* @param options Optional filters.
* @returns Aggregate send statistics.
*/
export async function broadcastMessage(
text: string,
cfg: OpenClawConfig,
options?: {
type?: "c2c" | "group";
accountId?: string;
limit?: number;
},
): Promise<{
total: number;
success: number;
failed: number;
results: Array<{ to: string; result: ProactiveSendResult }>;
}> {
const users = listKnownUsers({
type: options?.type,
accountId: options?.accountId,
limit: options?.limit,
sortByLastInteraction: true,
});
// Channel recipients do not support proactive sends.
const validUsers = users.filter((u) => u.type === "c2c" || u.type === "group");
const results: Array<{ to: string; result: ProactiveSendResult }> = [];
let success = 0;
let failed = 0;
for (const user of validUsers) {
const targetId = user.type === "group" ? (user.groupOpenid ?? user.openid) : user.openid;
const result = await sendProactive(
{
to: targetId,
text,
type: user.type as "c2c" | "group",
accountId: user.accountId,
},
cfg,
);
results.push({ to: targetId, result });
if (result.success) {
success++;
} else {
failed++;
}
// Add a small delay to reduce rate-limit pressure.
await new Promise((resolve) => setTimeout(resolve, 500));
}
return {
total: validUsers.length,
success,
failed,
results,
};
}
// Helpers.
/**
* Send a proactive message using a resolved account without a full config object.
*
* @param account Resolved account configuration.
* @param to Target openid.
* @param text Message content.
* @param type Message type.
*/
export async function sendProactiveMessageDirect(
account: ResolvedQQBotAccount,
to: string,
text: string,
type: "c2c" | "group" = "c2c",
): Promise<ProactiveSendResult> {
if (!account.appId || !account.clientSecret) {
return {
success: false,
error: "QQBot not configured (missing appId or clientSecret)",
};
}
try {
const accessToken = await getAccessToken(account.appId, account.clientSecret);
let result: { id: string; timestamp: number | string };
if (type === "c2c") {
result = await sendProactiveC2CMessage(account.appId, accessToken, to, text);
} else {
result = await sendProactiveGroupMessage(account.appId, accessToken, to, text);
}
return {
success: true,
messageId: result.id,
timestamp: result.timestamp,
};
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : String(err),
};
}
}
/**
* Return known-user counts for the selected account.
*/
export function getKnownUsersStats(accountId?: string): {
total: number;
c2c: number;
group: number;
channel: number;
} {
const users = listKnownUsers({ accountId });
return {
total: users.length,
c2c: users.filter((u) => u.type === "c2c").length,
group: users.filter((u) => u.type === "group").length,
channel: 0, // Channel users are not tracked in known-users storage.
};
}