📄 index.ts

/**
 * 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,
};