Newer
Older
WeComCompanyPlugin / wecom-api.js
import { logger } from "./logger.js";

class WeComApiClient {
  constructor() {
    this.tokens = new Map();
  }

  async getAccessToken(corpId, corpSecret) {
    if (!corpId || !corpSecret) {
      throw new Error("Missing corpId or corpSecret for WeCom API call");
    }
    
    const key = `${corpId}:${corpSecret}`;
    const cached = this.tokens.get(key);
    // Allow 5 minutes buffer before expiration
    if (cached && cached.expiresAt > Date.now() + 5 * 60 * 1000) {
      return cached.token;
    }

    logger.debug("WeCom API: Fetching new access token", { corpId });
    const res = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpId}&corpsecret=${corpSecret}`);
    const data = await res.json();
    
    if (data.errcode !== 0) {
      throw new Error(`WeCom gettoken failed: ${data.errmsg} (${data.errcode})`);
    }

    this.tokens.set(key, {
      token: data.access_token,
      // expires_in is usually 7200 seconds
      expiresAt: Date.now() + data.expires_in * 1000,
    });
    
    return data.access_token;
  }

  async uploadMedia(corpId, corpSecret, type, filePath, fileName) {
    const { readFile } = await import("fs/promises");
    const { Buffer } = await import("buffer");
    
    logger.debug("WeCom API: Uploading media", { corpId, type, filePath });
    const token = await this.getAccessToken(corpId, corpSecret);
    
    const fileBuffer = await readFile(filePath);
    const formData = new FormData();
    const blob = new Blob([fileBuffer]);
    formData.append('media', blob, fileName || 'file');

    const res = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=${token}&type=${type}`, {
      method: "POST",
      body: formData,
    });
    
    const data = await res.json();
    if (data.errcode !== 0) {
      logger.error("WeCom API: media/upload failed", { error: data.errmsg, code: data.errcode, corpId, type, filePath });
      throw new Error(`WeCom media/upload failed: ${data.errmsg} (${data.errcode})`);
    }
    
    logger.info("WeCom API: Media uploaded successfully", { mediaId: data.media_id, type });
    return data; // contains media_id, type, created_at
  }

  async sendTextMessage(corpId, corpSecret, agentId, toUser, text) {
    logger.debug("WeCom API: Sending async text message", { corpId, agentId, toUser, textPreview: text.substring(0, 50) });
    const token = await this.getAccessToken(corpId, corpSecret);
    const res = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        touser: toUser,
        msgtype: "text",
        agentid: agentId,
        text: { content: text },
      }),
    });
    const data = await res.json();
    if (data.errcode !== 0) {
      logger.error("WeCom API: message/send text failed", { error: data.errmsg, code: data.errcode, corpId, agentId, toUser });
      throw new Error(`WeCom message/send text failed: ${data.errmsg} (${data.errcode})`);
    }
    return data;
  }

  async sendImageMessage(corpId, corpSecret, agentId, toUser, mediaId) {
    logger.debug("WeCom API: Sending async image message", { corpId, agentId, toUser, mediaId });
    const token = await this.getAccessToken(corpId, corpSecret);
    const res = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        touser: toUser,
        msgtype: "image",
        agentid: agentId,
        image: { media_id: mediaId },
      }),
    });
    const data = await res.json();
    if (data.errcode !== 0) {
      throw new Error(`WeCom message/send image failed: ${data.errmsg} (${data.errcode})`);
    }
    return data;
  }

  async sendVoiceMessage(corpId, corpSecret, agentId, toUser, mediaId) {
    logger.debug("WeCom API: Sending async voice message", { corpId, agentId, toUser, mediaId });
    const token = await this.getAccessToken(corpId, corpSecret);
    const res = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        touser: toUser,
        msgtype: "voice",
        agentid: agentId,
        voice: { media_id: mediaId },
      }),
    });
    const data = await res.json();
    if (data.errcode !== 0) {
      throw new Error(`WeCom message/send voice failed: ${data.errmsg} (${data.errcode})`);
    }
    return data;
  }

  async sendVideoMessage(corpId, corpSecret, agentId, toUser, mediaId) {
    logger.debug("WeCom API: Sending async video message", { corpId, agentId, toUser, mediaId });
    const token = await this.getAccessToken(corpId, corpSecret);
    const res = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        touser: toUser,
        msgtype: "video",
        agentid: agentId,
        video: { media_id: mediaId },
      }),
    });
    const data = await res.json();
    if (data.errcode !== 0) {
      throw new Error(`WeCom message/send video failed: ${data.errmsg} (${data.errcode})`);
    }
    return data;
  }

  async sendFileMessage(corpId, corpSecret, agentId, toUser, mediaId) {
    logger.debug("WeCom API: Sending async file message", { corpId, agentId, toUser, mediaId });
    const token = await this.getAccessToken(corpId, corpSecret);
    const res = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        touser: toUser,
        msgtype: "file",
        agentid: agentId,
        file: { media_id: mediaId },
      }),
    });
    const data = await res.json();
    if (data.errcode !== 0) {
      throw new Error(`WeCom message/send file failed: ${data.errmsg} (${data.errcode})`);
    }
    return data;
  }
}

export const wecomApi = new WeComApiClient();