/**
* QQ Bot 文件发送工具
* 支持私聊和群聊发送图片、视频、语音文件
*/
import * as fs from "fs";
import * as path from "path";
import { getAccessToken } from "/root/.openclaw/extensions/qqbot/src/api.js";
// 媒体文件类型枚举
enum MediaFileType {
IMAGE = 1,
VIDEO = 2,
VOICE = 3,
FILE = 4,
}
// 文件类型映射
const FILE_TYPE_MAP = {
image: MediaFileType.IMAGE,
video: MediaFileType.VIDEO,
voice: MediaFileType.VOICE,
file: MediaFileType.FILE,
};
// 从QQ Bot插件导入API函数
async function importApiFunctions() {
try {
const api = await import("/root/.openclaw/extensions/qqbot/src/api.js");
return api;
} catch (error) {
console.error("[qqbot-file-sender] Failed to import QQ Bot API:", error);
throw new Error("QQ Bot API not available. Please ensure qqbot extension is installed.");
}
}
// 从配置获取QQ Bot的appId和clientSecret
function getQQBotConfig() {
try {
const configPath = path.join(process.env.HOME || "/root", ".openclaw", "openclaw.json");
if (!fs.existsSync(configPath)) {
throw new Error("OpenClaw config file not found");
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const qqbotConfig = config.channels?.qqbot;
if (!qqbotConfig?.appId || !qqbotConfig?.clientSecret) {
throw new Error("QQ Bot config not found in openclaw.json");
}
return {
appId: qqbotConfig.appId,
clientSecret: qqbotConfig.clientSecret,
};
} catch (error) {
console.error("[qqbot-file-sender] Failed to load QQ Bot config:", error);
throw new Error("Failed to load QQ Bot configuration. Please run: openclaw channels add --channel qqbot --token \"AppID:AppSecret\"");
}
}
// 读取本地文件并转换为Base64
function fileToBase64(filePath: string): string {
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const fileBuffer = fs.readFileSync(filePath);
return fileBuffer.toString("base64");
}
// 验证文件类型
function validateFileType(fileType: string): MediaFileType {
const type = FILE_TYPE_MAP[fileType.toLowerCase() as keyof typeof FILE_TYPE_MAP];
if (!type) {
throw new Error(`Invalid file type: ${fileType}. Supported types: image, video, voice`);
}
if (type === MediaFileType.FILE) {
console.warn("[qqbot-file-sender] WARNING: FILE type is marked as 'not yet opened' in QQ Bot API. It may not work.");
}
return type;
}
/**
* 发送QQ文件主函数
*
* @param options - 发送选项
* @returns 发送结果
*/
export async function sendQQFile(options: {
targetId: string;
targetType: "c2c" | "group";
fileType: "image" | "video" | "voice";
filePath?: string;
fileUrl?: string;
content?: string;
msgId?: string;
}): Promise<{ id: string; timestamp: number | string }> {
const { targetId, targetType, fileType, filePath, fileUrl, content, msgId } = options;
// 参数验证
if (!targetId) {
throw new Error("targetId is required");
}
if (!targetType || (targetType !== "c2c" && targetType !== "group")) {
throw new Error("targetType must be 'c2c' or 'group'");
}
if (!filePath && !fileUrl) {
throw new Error("Either filePath or fileUrl must be provided");
}
if (filePath && fileUrl) {
throw new Error("Cannot provide both filePath and fileUrl");
}
// 验证文件类型
const mediaType = validateFileType(fileType);
console.log(`[qqbot-file-sender] Preparing to send ${fileType} to ${targetType} ${targetId}`);
try {
// 导入API函数
const api = await importApiFunctions();
// 获取配置
const config = getQQBotConfig();
// 获取access token
console.log(`[qqbot-file-sender] Getting access token...`);
const accessToken = await getAccessToken(config.appId, config.clientSecret);
let uploadResult: any;
// 上传文件
if (filePath) {
// 本地文件:转换为Base64并上传
console.log(`[qqbot-file-sender] Reading local file: ${filePath}`);
const base64Data = fileToBase64(filePath);
console.log(`[qqbot-file-sender] File converted to Base64 (${base64Data.length} chars)`);
if (targetType === "c2c") {
uploadResult = await api.uploadC2CMedia(
accessToken,
targetId,
mediaType,
undefined,
base64Data,
false
);
} else {
uploadResult = await api.uploadGroupMedia(
accessToken,
targetId,
mediaType,
undefined,
base64Data,
false
);
}
} else if (fileUrl) {
// 网络URL:直接上传
console.log(`[qqbot-file-sender] Using file URL: ${fileUrl}`);
if (targetType === "c2c") {
uploadResult = await api.uploadC2CMedia(
accessToken,
targetId,
mediaType,
fileUrl,
undefined,
false
);
} else {
uploadResult = await api.uploadGroupMedia(
accessToken,
targetId,
mediaType,
fileUrl,
undefined,
false
);
}
}
console.log(`[qqbot-file-sender] File uploaded successfully:`, {
file_uuid: uploadResult.file_uuid,
ttl: uploadResult.ttl,
});
// 发送消息
console.log(`[qqbot-file-sender] Sending message...`);
let sendResult: any;
if (targetType === "c2c") {
sendResult = await api.sendC2CMediaMessage(
accessToken,
targetId,
uploadResult.file_info,
msgId,
content
);
} else {
sendResult = await api.sendGroupMediaMessage(
accessToken,
targetId,
uploadResult.file_info,
msgId,
content
);
}
console.log(`[qqbot-file-sender] Message sent successfully:`, {
messageId: sendResult.id,
timestamp: sendResult.timestamp,
});
return {
id: sendResult.id,
timestamp: sendResult.timestamp,
};
} catch (error) {
console.error("[qqbot-file-sender] Failed to send file:", error);
throw error;
}
}
/**
* 简化的图片发送函数(私聊)
*/
export async function sendQQImageToUser(
userId: string,
imagePathOrUrl: string,
content?: string,
msgId?: string
): Promise<{ id: string; timestamp: number | string }> {
const isUrl = imagePathOrUrl.startsWith("http://") || imagePathOrUrl.startsWith("https://");
return sendQQFile({
targetId: userId,
targetType: "c2c",
fileType: "image",
...(isUrl ? { fileUrl: imagePathOrUrl } : { filePath: imagePathOrUrl }),
content,
msgId,
});
}
/**
* 简化的图片发送函数(群聊)
*/
export async function sendQQImageToGroup(
groupId: string,
imagePathOrUrl: string,
content?: string,
msgId?: string
): Promise<{ id: string; timestamp: number | string }> {
const isUrl = imagePathOrUrl.startsWith("http://") || imagePathOrUrl.startsWith("https://");
return sendQQFile({
targetId: groupId,
targetType: "group",
fileType: "image",
...(isUrl ? { fileUrl: imagePathOrUrl } : { filePath: imagePathOrUrl }),
content,
msgId,
});
}
// 工具函数:检查文件是否存在
export function checkFileExists(filePath: string): boolean {
try {
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
} catch {
return false;
}
}
// 工具函数:获取文件大小
export function getFileSize(filePath: string): number | null {
try {
if (fs.existsSync(filePath)) {
return fs.statSync(filePath).size;
}
return null;
} catch {
return null;
}
}
// 工具函数:格式化文件大小
export function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
// 如果是直接运行此文件,执行测试
if (import.meta.url === `file://${process.argv[1]}`) {
console.log("[qqbot-file-sender] Running test...");
// 测试参数(需要根据实际环境修改)
const testOptions = {
targetId: "33558EFB7CF362DA4A894FBE2E93DFDE",
targetType: "c2c" as const,
fileType: "image" as const,
filePath: "/path/to/test/image.jpg",
content: "测试发送图片",
};
sendQQFile(testOptions)
.then(result => {
console.log("[qqbot-file-sender] Test success:", result);
})
.catch(error => {
console.error("[qqbot-file-sender] Test failed:", error);
});
}
export default {
sendQQFile,
sendQQImageToUser,
sendQQImageToGroup,
checkFileExists,
getFileSize,
formatFileSize,
};