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();