update
This commit is contained in:
48
wxmsg/src/wx/access-token.ts
Normal file
48
wxmsg/src/wx/access-token.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// const accessURL = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET'
|
||||
const accessURL = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential';
|
||||
|
||||
type AccessData = {
|
||||
"access_token": string;
|
||||
"expires_in": number; // 7200, 单位秒 2小时
|
||||
"accessToken": string;
|
||||
"expiredAt": number; // 到期时间戳,单位毫秒
|
||||
}
|
||||
|
||||
type ErrorData = {
|
||||
errcode: number;
|
||||
errmsg: string;
|
||||
}
|
||||
export const getAccessURL = (appId: string, appSecret: string): string => {
|
||||
return `${accessURL}&appid=${appId}&secret=${appSecret}`;
|
||||
};
|
||||
|
||||
export const getAccessToken = async (appId: string, appSecret: string): Promise<{
|
||||
code: number;
|
||||
data?: AccessData;
|
||||
message?: string;
|
||||
}> => {
|
||||
const url = getAccessURL(appId, appSecret);
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
if ((data as ErrorData).errcode) {
|
||||
return {
|
||||
code: 500,
|
||||
message: (data as ErrorData).errmsg,
|
||||
}
|
||||
}
|
||||
console.log('access token data', data);
|
||||
data.accessToken = data.access_token;
|
||||
data.expiredAt = Date.now() + data.expires_in * 1000;
|
||||
return {
|
||||
code: 200,
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
if(require.main === module) {
|
||||
// 测试代码
|
||||
const appId = 'wxff97d569b1db16b6';
|
||||
const appSecret = '012d84d0d2b914de95f4e9ca84923aed';
|
||||
const res = await getAccessToken(appId, appSecret);
|
||||
console.log('getAccessToken res', res);
|
||||
}
|
||||
78
wxmsg/src/wx/index.ts
Normal file
78
wxmsg/src/wx/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { getAccessToken } from './access-token';
|
||||
import { Redis } from 'ioredis';
|
||||
import { WxCustomServiceMsg, WxMsgText } from './type/custom-service.ts';
|
||||
|
||||
export * from './type/custom-service.ts';
|
||||
export * from './type/send.ts';
|
||||
|
||||
/**
|
||||
* 从
|
||||
* @param str
|
||||
* @returns
|
||||
*/
|
||||
export const changeToLowerCase = (str: string): string => {
|
||||
return str.toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
export class Wx {
|
||||
private appId: string;
|
||||
private appSecret: string;
|
||||
public redis: Redis | null = null;
|
||||
constructor({ appId, appSecret, redis }: { appId: string; appSecret: string; redis?: Redis }) {
|
||||
this.appId = appId;
|
||||
this.appSecret = appSecret;
|
||||
this.redis = redis! || null;
|
||||
}
|
||||
|
||||
public async getAccessToken(): Promise<string> {
|
||||
const _accessToken = await this.redis?.get(`wx:access_token:${this.appId}`);
|
||||
if (_accessToken) {
|
||||
return _accessToken;
|
||||
}
|
||||
const res = await getAccessToken(this.appId, this.appSecret);
|
||||
if (res.code !== 200 || !res.data) {
|
||||
throw new Error(`Failed to get access token: ${res.message || 'unknown error'}`);
|
||||
}
|
||||
const { accessToken, expires_in } = res.data;
|
||||
await this.redis?.set(`wx:access_token:${this.appId}`, accessToken, 'EX', expires_in - 200);
|
||||
return accessToken;
|
||||
}
|
||||
public async analyzeUserMsg(msg: WxCustomServiceMsg) {
|
||||
const touser = msg.fromusername;
|
||||
const msgType = msg.msgtype;
|
||||
if (msgType !== 'text') {
|
||||
console.log('Only text messages are supported for auto-reply.');
|
||||
console.log('Analyzed message:', { touser, msgType });
|
||||
return;
|
||||
}
|
||||
const txtMsg = msg as WxMsgText;
|
||||
const content = txtMsg.content;
|
||||
|
||||
console.log('Analyzing user message:', { touser, msgType, content });
|
||||
const sendData = {
|
||||
touser,
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: 'Hello World',
|
||||
},
|
||||
}
|
||||
this.sendUserMessage(sendData);
|
||||
}
|
||||
public async sendUserMessage(data: any) {
|
||||
const accessToken = await this.getAccessToken();
|
||||
const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`
|
||||
|
||||
const res = await this.post(url, data);
|
||||
if (res.errcode !== 0) {
|
||||
console.error('Failed to send user message:', res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
public async post(url: string, data: any) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}).then((res) => res.json());
|
||||
}
|
||||
}
|
||||
16
wxmsg/src/wx/send-user.ts
Normal file
16
wxmsg/src/wx/send-user.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const sendUser = async (accessToken: string) => {
|
||||
const data = {
|
||||
touser: 'omcvy7AHC6bAA0QM4x9_bE0fGD1g',
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: 'Hello World',
|
||||
},
|
||||
};
|
||||
const url = 'https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN';
|
||||
const link = url.replace('ACCESS_TOKEN', accessToken);
|
||||
const res = await fetch(link, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}).then((res) => res.json());
|
||||
console.log('res', res);
|
||||
};
|
||||
94
wxmsg/src/wx/type/custom-service.ts
Normal file
94
wxmsg/src/wx/type/custom-service.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
export type WxMsgBase<T = any> = {
|
||||
tousername: string;
|
||||
fromusername: string;
|
||||
createtime: string;
|
||||
msgtype: T extends { msgtype: infer U } ? U : string;
|
||||
msgid: string;
|
||||
} & T;
|
||||
export type WxMsgText = WxMsgBase<{
|
||||
msgtype: 'text';
|
||||
content: string;
|
||||
}>;
|
||||
export type WxMsgEvent = WxMsgBase<{
|
||||
msgtype: 'event';
|
||||
event: 'subscribe' | 'unsubscribe' | 'click' | 'location' | 'scan';
|
||||
eventkey: string;
|
||||
/**
|
||||
*
|
||||
* 用户同意上报地理位置后,每次进入服务号会话时,都会在进入时上报地理位置,
|
||||
* 或在进入会话后每5秒上报一次地理位置,服务号可以在公众平台网站中修改以上设置。
|
||||
* 上报地理位置时,微信会将上报地理位置事件推送到开发者填写的URL。
|
||||
*/
|
||||
longitude?: string; // 经度
|
||||
latitude?: string; // 纬度
|
||||
precision?: string; // 精度
|
||||
|
||||
/**
|
||||
* 用户未关注时,进行关注后的扫描带参数二维码事件推送
|
||||
* eventkey为 qrscene_为前缀,后面为二维码的场景值ID
|
||||
*/
|
||||
ticket?: string;
|
||||
}>;
|
||||
export type WxMsgVoice = WxMsgBase<{
|
||||
mediaid: string;
|
||||
format: string; // 语音格式,如amr,speex等
|
||||
recognition?: string; // 语音识别结果,UTF8编码
|
||||
mediaid16k?: string; // 语音播放时间长度,单位为秒
|
||||
}>;
|
||||
|
||||
export type WxMsgImage = WxMsgBase<{
|
||||
msgtype: 'image';
|
||||
picurl: string;
|
||||
mediaid: string;
|
||||
}>;
|
||||
|
||||
export type WxMsgLocation = WxMsgBase<{
|
||||
msgtype: 'location';
|
||||
location_x: string; // 地理位置纬度
|
||||
location_y: string; // 地理位置经度
|
||||
scale: string; // 地图缩放大小
|
||||
label: string; // 地理位置信息
|
||||
}>;
|
||||
export type WxMsgVideo = WxMsgBase<{
|
||||
msgtype: 'video';
|
||||
mediaid: string;
|
||||
thumbmediaid: string;
|
||||
}>;
|
||||
|
||||
export type WxMsgLink = WxMsgBase<{
|
||||
msgtype: 'link';
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
}>;
|
||||
|
||||
export type WxMsgFile = WxMsgBase<{
|
||||
msgtype: 'file';
|
||||
title: string;
|
||||
description: string;
|
||||
filekey: string;
|
||||
filemd5: string;
|
||||
filetotallen: string;
|
||||
}>;
|
||||
|
||||
export type WxCustomServiceMsg = WxMsgText | WxMsgEvent | WxMsgVoice | WxMsgImage | WxMsgLocation | WxMsgVideo | WxMsgLink | WxMsgFile;
|
||||
|
||||
export const parseWxMessage = (msg: any): WxCustomServiceMsg => {
|
||||
const keys = Object.keys(msg);
|
||||
let values = keys.map((key) => {
|
||||
const value = msg[key];
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
return value[0];
|
||||
}
|
||||
return value;
|
||||
}).reduce((acc, curr, index) => {
|
||||
const newKey = keys[index].toLowerCase();
|
||||
if (newKey === 'event') {
|
||||
curr = curr.toLowerCase();
|
||||
}
|
||||
acc[newKey] = curr;
|
||||
return acc;
|
||||
}, {} as any);
|
||||
return values as WxCustomServiceMsg;
|
||||
|
||||
}
|
||||
91
wxmsg/src/wx/type/send.ts
Normal file
91
wxmsg/src/wx/type/send.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export type Send<T = any> = {
|
||||
touser: string;
|
||||
msgtype: 'text' | 'image' | 'voice' | 'video' | 'music' | 'news' | 'mpnews' | 'mpnewsarticle' | 'msgmenu' | 'wxcard' | 'miniprogrampage' | 'customservice' | 'aimsgcontext';
|
||||
} & T;
|
||||
|
||||
type SendText = Send<{
|
||||
text: {
|
||||
content: string;
|
||||
}
|
||||
}>;
|
||||
|
||||
type SendImage = Send<{
|
||||
image: {
|
||||
media_id: string;
|
||||
}
|
||||
}>;
|
||||
type SendVoice = Send<{
|
||||
voice: {
|
||||
media_id: string;
|
||||
}
|
||||
}>
|
||||
type SendVideo = Send<{
|
||||
video: {
|
||||
media_id: string;
|
||||
thumb_media_id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
}>
|
||||
type SendMusic = Send<{
|
||||
music: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
musicurl?: string;
|
||||
hqmusicurl?: string;
|
||||
thumb_media_id: string;
|
||||
}
|
||||
}>
|
||||
type SendNews = Send<{
|
||||
news: {
|
||||
articles: Array<{
|
||||
title: string;
|
||||
description?: string;
|
||||
url: string;
|
||||
picurl?: string;
|
||||
}>
|
||||
}
|
||||
}>
|
||||
type SendMpnews = Send<{
|
||||
mpnews: {
|
||||
media_id: string;
|
||||
}
|
||||
}>
|
||||
type SendMsgmenu = Send<{
|
||||
msgmenu: {
|
||||
head_content: string;
|
||||
tail_content: string;
|
||||
list: Array<{
|
||||
id: string;
|
||||
content: string;
|
||||
}>
|
||||
}
|
||||
}>
|
||||
type SendWxcard = Send<{
|
||||
wxcard: {
|
||||
card_id: string;
|
||||
}
|
||||
}>
|
||||
type SendMiniprogrampage = Send<{
|
||||
miniprogrampage: {
|
||||
title: string;
|
||||
appid: string;
|
||||
pagepath: string;
|
||||
thumb_media_id: string;
|
||||
}
|
||||
}>
|
||||
type SendCustomservice = Send<{
|
||||
customservice: {
|
||||
kf_account: string;
|
||||
}
|
||||
}>
|
||||
type SendAimsgcontext = Send<{
|
||||
aimsgcontext: {
|
||||
/**
|
||||
* 消息下方增加灰色 wording “内容由第三方AI生成” 0 不增加 1 增加
|
||||
*/
|
||||
is_ai_msg: number;
|
||||
}
|
||||
}>
|
||||
|
||||
export type WxSendMsgType = SendText | SendImage | SendVoice | SendVideo | SendMusic | SendNews | SendMpnews | SendMsgmenu | SendWxcard | SendMiniprogrampage | SendCustomservice | SendAimsgcontext;
|
||||
Reference in New Issue
Block a user