Parcourir la source

添加微信客服SDK (#436)

* 添加微信客服SDK

* polish:优化签名函数

* polish:优化注释内容

* polish:复用已有的Token以及CommonError,移除无用的输出

* polish:复用已有的消息加解密

* fix:修复错误信息被覆盖的问题

* polish:go fmt 文件
Afeyer il y a 4 ans
Parent
commit
8ceabc2d0b

+ 4 - 1
work/config/config.go

@@ -5,11 +5,14 @@ import (
 	"github.com/silenceper/wechat/v2/cache"
 )
 
-// Config config for 企业微信
+// Config for 企业微信
 type Config struct {
 	CorpID        string `json:"corp_id"`     // corp_id
 	CorpSecret    string `json:"corp_secret"` // corp_secret,如果需要获取会话存档实例,当前参数请填写聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看
 	AgentID       string `json:"agent_id"`    // agent_id
 	Cache         cache.Cache
 	RasPrivateKey string // 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存
+
+	Token          string `json:"token"`            // 微信客服回调配置,用于生成签名校验回调请求的合法性
+	EncodingAESKey string `json:"encoding_aes_key"` // 微信客服回调p配置,用于解密回调消息内容对应的密文
 }

+ 3 - 0
work/kf/README.md

@@ -0,0 +1,3 @@
+### 微信客服SDK
+
+相关文档正在梳理中...

+ 185 - 0
work/kf/account.go

@@ -0,0 +1,185 @@
+package kf
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/util"
+)
+
+const (
+	//添加客服账号
+	accountAddAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/add?access_token=%s"
+	// 删除客服账号
+	accountDelAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/del?access_token=%s"
+	// 修改客服账号
+	accountUpdateAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/update?access_token=%s"
+	// 获取客服账号列表
+	accountListAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/list?access_token=%s"
+	//获取客服账号链接
+	addContactWayAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/add_contact_way?access_token=%s"
+)
+
+// AccountAddOptions 添加客服账号请求参数
+type AccountAddOptions struct {
+	Name    string `json:"name"`     // 客服帐号名称, 不多于16个字符
+	MediaID string `json:"media_id"` // 客服头像临时素材。可以调用上传临时素材接口获取, 不多于128个字节
+}
+
+// AccountAddSchema 添加客服账号响应内容
+type AccountAddSchema struct {
+	util.CommonError
+	OpenKFID string `json:"open_kfid"` // 新创建的客服张号ID
+}
+
+// AccountAdd 添加客服账号
+func (r *Client) AccountAdd(options AccountAddOptions) (info AccountAddSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(accountAddAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// AccountDelOptions 删除客服账号请求参数
+type AccountDelOptions struct {
+	OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节
+}
+
+// AccountDel 删除客服账号
+func (r *Client) AccountDel(options AccountDelOptions) (info util.CommonError, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(accountDelAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// AccountUpdateOptions 修改客服账号请求参数
+type AccountUpdateOptions struct {
+	OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节
+	Name     string `json:"name"`      // 客服帐号名称, 不多于16个字符
+	MediaID  string `json:"media_id"`  // 客服头像临时素材。可以调用上传临时素材接口获取, 不多于128个字节
+}
+
+// AccountUpdate 修复客服账号
+func (r *Client) AccountUpdate(options AccountUpdateOptions) (info util.CommonError, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(accountUpdateAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// AccountInfoSchema 客服详情
+type AccountInfoSchema struct {
+	OpenKFID string `json:"open_kfid"` // 客服帐号ID
+	Name     string `json:"name"`      // 客服帐号名称
+	Avatar   string `json:"avatar"`    // 客服头像URL
+}
+
+// AccountListSchema 获取客服账号列表响应内容
+type AccountListSchema struct {
+	util.CommonError
+	AccountList []AccountInfoSchema `json:"account_list"` // 客服账号列表
+}
+
+// AccountList 获取客服账号列表
+func (r *Client) AccountList() (info AccountListSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.HTTPGet(fmt.Sprintf(accountListAddr, accessToken))
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// AddContactWayOptions 获取客服账号链接
+type AddContactWayOptions struct {
+	OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节
+	Scene    string `json:"scene"`     // 场景值,字符串类型,由开发者自定义, 不多于32字节, 字符串取值范围(正则表达式):[0-9a-zA-Z_-]*
+}
+
+// AddContactWaySchema 获取客服账号链接响应内容
+type AddContactWaySchema struct {
+	util.CommonError
+	URL string `json:"url"` // 客服链接,开发者可将该链接嵌入到H5页面中,用户点击链接即可向对应的微信客服帐号发起咨询。开发者也可根据该url自行生成需要的二维码图片
+}
+
+// AddContactWay 获取客服账号链接
+func (r *Client) AddContactWay(options AddContactWayOptions) (info AddContactWaySchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(addContactWayAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}

+ 43 - 0
work/kf/client.go

@@ -0,0 +1,43 @@
+package kf
+
+import (
+	"github.com/silenceper/wechat/v2/cache"
+	"github.com/silenceper/wechat/v2/credential"
+	"github.com/silenceper/wechat/v2/work/config"
+	"github.com/silenceper/wechat/v2/work/context"
+)
+
+// Client 微信客服实例
+type Client struct {
+	corpID         string // 企业ID:企业开通的每个微信客服,都对应唯一的企业ID,企业可在微信客服管理后台的企业信息处查看
+	secret         string // Secret是微信客服用于校验开发者身份的访问密钥,企业成功注册微信客服后,可在「微信客服管理后台-开发配置」处获取
+	token          string // 用于生成签名校验回调请求的合法性
+	encodingAESKey string // 回调消息加解密参数是AES密钥的Base64编码,用于解密回调消息内容对应的密文
+	cache          cache.Cache
+	ctx            *context.Context
+}
+
+// NewClient 初始化微信客服实例
+func NewClient(cfg *config.Config) (client *Client, err error) {
+	if cfg.Cache == nil {
+		return nil, NewSDKErr(50001)
+	}
+
+	//初始化 AccessToken Handle
+	defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, credential.CacheKeyWorkPrefix, cfg.Cache)
+	ctx := &context.Context{
+		Config:            cfg,
+		AccessTokenHandle: defaultAkHandle,
+	}
+
+	client = &Client{
+		corpID:         cfg.CorpID,
+		secret:         cfg.CorpSecret,
+		token:          cfg.Token,
+		encodingAESKey: cfg.EncodingAESKey,
+		cache:          cfg.Cache,
+		ctx:            ctx,
+	}
+
+	return client, nil
+}

+ 56 - 0
work/kf/customer.go

@@ -0,0 +1,56 @@
+package kf
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/util"
+)
+
+const (
+	customerBatchGetAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/batchget?access_token=%s"
+)
+
+// CustomerBatchGetOptions 客户基本信息获取请求参数
+type CustomerBatchGetOptions struct {
+	ExternalUserIDList []string `json:"external_userid_list"` // external_userid列表
+}
+
+// CustomerSchema 微信客户基本资料
+type CustomerSchema struct {
+	ExternalUserID string `json:"external_userid"` // 微信客户的external_userid
+	NickName       string `json:"nickname"`        // 微信昵称
+	Avatar         string `json:"avatar"`          // 微信头像。第三方不可获取
+	Gender         int    `json:"gender"`          // 性别
+	UnionID        string `json:"unionid"`         // unionid,需要绑定微信开发者帐号才能获取到,查看绑定方法: https://open.work.weixin.qq.com/kf/doc/92512/93143/94769#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%BE%AE%E4%BF%A1%E5%AE%A2%E6%88%B7%E7%9A%84unionid
+}
+
+// CustomerBatchGetSchema 获取客户基本信息响应内容
+type CustomerBatchGetSchema struct {
+	util.CommonError
+	CustomerList          []CustomerSchema `json:"customer_list"`           // 微信客户信息列表
+	InvalidExternalUserID []string         `json:"invalid_external_userid"` // 无效的微信客户ID
+}
+
+// CustomerBatchGet 客户基本信息获取
+func (r *Client) CustomerBatchGet(options CustomerBatchGetOptions) (info CustomerBatchGetSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(customerBatchGetAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}

+ 67 - 0
work/kf/error.go

@@ -0,0 +1,67 @@
+package kf
+
+import (
+	"reflect"
+	"strings"
+)
+
+// Error 错误
+type Error string
+
+const (
+	// SDKInitFailed 错误码:50001
+	SDKInitFailed Error = "SDK初始化失败"
+	// SDKCacheUnavailable 错误码:50002
+	SDKCacheUnavailable Error = "缓存无效"
+	// SDKUnknownError 错误码:50003
+	SDKUnknownError Error = "未知错误"
+	// SDKInvalidCredential 错误码:40001
+	SDKInvalidCredential Error = "不合法的secret参数"
+	// SDKInvalidCorpID 错误码:40013
+	SDKInvalidCorpID Error = "无效的 CorpID"
+	// SDKAccessTokenInvalid 错误码:40014
+	SDKAccessTokenInvalid Error = "AccessToken 无效"
+	// SDKAccessTokenMissing 错误码:41001
+	SDKAccessTokenMissing Error = "缺少AccessToken参数"
+	// SDKAccessTokenExpired 错误码:42001
+	SDKAccessTokenExpired Error = "AccessToken 已过期"
+	// SDKApiFreqOutOfLimit 错误码:45009
+	SDKApiFreqOutOfLimit Error = "接口请求次数超频"
+	// SDKWeWorkAlready 错误码:95011
+	SDKWeWorkAlready Error = "已在企业微信使用微信客服"
+)
+
+//Error 输出错误信息
+func (r Error) Error() string {
+	return reflect.ValueOf(r).String()
+}
+
+// NewSDKErr 初始化SDK实例错误信息
+func NewSDKErr(code int64, msgList ...string) Error {
+	switch code {
+	case 50001:
+		return SDKInitFailed
+	case 50002:
+		return SDKCacheUnavailable
+	case 40001:
+		return SDKInvalidCredential
+	case 41001:
+		return SDKAccessTokenMissing
+	case 42001:
+		return SDKAccessTokenExpired
+	case 40013:
+		return SDKInvalidCorpID
+	case 40014:
+		return SDKAccessTokenInvalid
+	case 45009:
+		return SDKApiFreqOutOfLimit
+	case 95011:
+		return SDKWeWorkAlready
+	default:
+		//返回未知的自定义错误
+		if len(msgList) > 0 {
+			return Error(strings.Join(msgList, ","))
+		}
+		return SDKUnknownError
+	}
+}

+ 42 - 0
work/kf/sendmsg.go

@@ -0,0 +1,42 @@
+package kf
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/util"
+)
+
+const (
+	//发送消息
+	sendMsgAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token=%s"
+)
+
+// SendMsgSchema 发送消息响应内容
+type SendMsgSchema struct {
+	util.CommonError
+	MsgID string `json:"msgid"` // 消息ID。如果请求参数指定了msgid,则原样返回,否则系统自动生成并返回。不多于32字节, 字符串取值范围(正则表达式):[0-9a-zA-Z_-]*
+}
+
+// SendMsg 获取消息
+func (r *Client) SendMsg(options interface{}) (info SendMsgSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(sendMsgAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}

+ 127 - 0
work/kf/sendmsg/message.go

@@ -0,0 +1,127 @@
+package sendmsg
+
+// Message 发送消息
+type Message struct {
+	ToUser   string `json:"touser"`    // 指定接收消息的客户UserID
+	OpenKFID string `json:"open_kfid"` // 指定发送消息的客服帐号ID
+	MsgID    string `json:"msgid"`     // 指定消息ID
+}
+
+// Text 发送文本消息
+type Text struct {
+	Message
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:text
+	Text    struct {
+		Content string `json:"content"` // 消息内容,最长不超过2048个字节
+	} `json:"text"` // 文本消息
+}
+
+// Image 发送图片消息
+type Image struct {
+	Message
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:image
+	Image   struct {
+		MediaID string `json:"media_id"` // 图片文件id,可以调用上传临时素材接口获取
+	} `json:"image"` // 图片消息
+}
+
+// Voice 发送语音消息
+type Voice struct {
+	Message
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:voice
+	Voice   struct {
+		MediaID string `json:"media_id"` // 语音文件id,可以调用上传临时素材接口获取
+	} `json:"voice"` // 语音消息
+}
+
+// Video 发送视频消息
+type Video struct {
+	Message
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:video
+	Video   struct {
+		MediaID string `json:"media_id"` // 视频文件id,可以调用上传临时素材接口获取
+	} `json:"video"` // 视频消息
+}
+
+// File 发送文件消息
+type File struct {
+	Message
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:file
+	File    struct {
+		MediaID string `json:"media_id"` // 文件id,可以调用上传临时素材接口获取
+	} `json:"file"` // 文件消息
+}
+
+// Link 图文链接消息
+type Link struct {
+	Message
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:link
+	Link    struct {
+		Title        string `json:"title"`          // 标题,不超过128个字节,超过会自动截断
+		Desc         string `json:"desc"`           // 描述,不超过512个字节,超过会自动截断
+		URL          string `json:"url"`            // 点击后跳转的链接。 最长2048字节,请确保包含了协议头(http/https)
+		ThumbMediaID string `json:"thumb_media_id"` // 缩略图的media_id, 可以通过素材管理接口获得。此处thumb_media_id即上传接口返回的media_id
+	} `json:"link"` // 链接消息
+}
+
+// MiniProgram 小程序消息
+type MiniProgram struct {
+	Message
+	MsgType     string `json:"msgtype"` // 消息类型,此时固定为:miniprogram
+	MiniProgram struct {
+		AppID        string `json:"appid"`          // 小程序appid,必须是关联到企业的小程序应用
+		Title        string `json:"title"`          // 小程序消息标题,最多64个字节,超过会自动截断
+		ThumbMediaID string `json:"thumb_media_id"` // 小程序消息封面的mediaid,封面图建议尺寸为520*416
+		PagePath     string `json:"pagepath"`       // 点击消息卡片后进入的小程序页面路径
+	} `json:"miniprogram"` // 小程序消息
+}
+
+// Menu 发送菜单消息
+type Menu struct {
+	Message
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:msgmenu
+	MsgMenu struct {
+		HeadContent string        `json:"head_content"` // 消息内容,不多于1024字节
+		List        []interface{} `json:"list"`         // 菜单项配置
+	} `json:"msgmenu"`
+}
+
+// MenuClick 回复菜单
+type MenuClick struct {
+	Type  string `json:"type"` // 菜单类型: click 回复菜单
+	Click struct {
+		ID      string `json:"id"`      // 菜单ID, 不少于1字节, 不多于64字节
+		Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于128字节
+	} `json:"click"`
+}
+
+// MenuView 超链接菜单
+type MenuView struct {
+	Type string `json:"type"` // 菜单类型: view 超链接菜单
+	View struct {
+		URL     string `json:"url"`     // 点击后跳转的链接, 不少于1字节, 不多于2048字节
+		Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于1024字节
+	} `json:"view"`
+}
+
+// MenuMiniProgram 小程序菜单
+type MenuMiniProgram struct {
+	Type        string `json:"type"` // 菜单类型: miniprogram 小程序菜单
+	MiniProgram struct {
+		AppID    string `json:"appid"`    // 小程序appid, 不少于1字节, 不多于32字节
+		PagePath string `json:"pagepath"` // 点击后进入的小程序页面, 不少于1字节, 不多于1024字节
+		Content  string `json:"content"`  // 菜单显示内容, 不少于1字节, 不多于1024字节
+	} `json:"miniprogram"`
+}
+
+// Location 地理位置消息
+type Location struct {
+	Message
+	MsgType  string `json:"msgtype"` // 消息类型,此时固定为:location
+	Location struct {
+		Latitude  float32 `json:"latitude"`  // 纬度, 浮点数,范围为90 ~ -90
+		Longitude float32 `json:"longitude"` // 经度, 浮点数,范围为180 ~ -180
+		Name      string  `json:"name"`      // 位置名
+		Address   string  `json:"address"`   // 地址详情说明
+	} `json:"location"`
+}

+ 1 - 0
work/kf/sendmsg/sendmsg.go

@@ -0,0 +1 @@
+package sendmsg

+ 110 - 0
work/kf/servicer.go

@@ -0,0 +1,110 @@
+package kf
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/util"
+)
+
+const (
+	//添加接待人员
+	receptionistAddAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/add?access_token=%s"
+	//删除接待人员
+	receptionistDelAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/del?access_token=%s"
+	//获取接待人员列表
+	receptionistListAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/list?access_token=%s&open_kfid=%s"
+)
+
+// ReceptionistOptions 添加接待人员请求参数
+type ReceptionistOptions struct {
+	OpenKFID   string   `json:"open_kfid"`   // 客服帐号ID
+	UserIDList []string `json:"userid_list"` // 接待人员userid列表
+}
+
+// ReceptionistSchema 添加接待人员响应内容
+type ReceptionistSchema struct {
+	util.CommonError
+	ResultList []struct {
+		UserID string `json:"userid"`
+		util.CommonError
+	} `json:"result_list"`
+}
+
+// ReceptionistAdd 添加接待人员
+func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info ReceptionistSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(receptionistAddAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// ReceptionistDel 删除接待人员
+func (r *Client) ReceptionistDel(options ReceptionistOptions) (info ReceptionistSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(receptionistDelAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// ReceptionistListSchema 获取接待人员列表响应内容
+type ReceptionistListSchema struct {
+	util.CommonError
+	ReceptionistList []struct {
+		UserID string `json:"userid"` // 接待人员的userid。第三方应用获取到的为密文userid,即open_userid
+		Status int    `json:"status"` // 接待人员的接待状态。0:接待中,1:停止接待。第三方应用需具有“管理帐号、分配会话和收发消息”权限才可获取
+	} `json:"servicer_list"`
+}
+
+// ReceptionistList 获取接待人员列表
+func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.HTTPGet(fmt.Sprintf(receptionistListAddr, accessToken, kfID))
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}

+ 87 - 0
work/kf/servicestate.go

@@ -0,0 +1,87 @@
+package kf
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/util"
+)
+
+const (
+	//获取会话状态
+	serviceStateGetAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/get?access_token=%s"
+	// 变更会话状态
+	serviceStateTransAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/trans?access_token=%s"
+)
+
+// ServiceStateGetOptions 获取会话状态请求参数
+type ServiceStateGetOptions struct {
+	OpenKFID       string `json:"open_kfid"`       // 客服帐号ID
+	ExternalUserID string `json:"external_userid"` // 微信客户的external_userid
+}
+
+// ServiceStateGetSchema 获取会话状态响应内容
+type ServiceStateGetSchema struct {
+	util.CommonError
+	ServiceState  int    `json:"service_state"`  // 当前的会话状态,状态定义参考概述中的表格
+	ServiceUserID string `json:"service_userid"` // 接待人员的userid,仅当state=3时有效
+}
+
+// ServiceStateGet 获取会话状态
+//0	未处理	新会话接入。可选择:1.直接用API自动回复消息。2.放进待接入池等待接待人员接待。3.指定接待人员进行接待
+//1	由智能助手接待	可使用API回复消息。可选择转入待接入池或者指定接待人员处理。
+//2	待接入池排队中	在待接入池中排队等待接待人员接入。可选择转为指定人员接待
+//3	由人工接待	人工接待中。可选择结束会话
+//4	已结束	会话已经结束。不允许变更会话状态,等待用户重新发起咨询
+func (r *Client) ServiceStateGet(options ServiceStateGetOptions) (info ServiceStateGetSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(serviceStateGetAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// ServiceStateTransOptions 变更会话状态请求参数
+type ServiceStateTransOptions struct {
+	OpenKFID       string `json:"open_kfid"`       // 客服帐号ID
+	ExternalUserID string `json:"external_userid"` // 微信客户的external_userid
+	ServiceState   int    `json:"service_state"`   // 变更的目标状态,状态定义和所允许的变更可参考概述中的流程图和表格
+	ServicerUserID string `json:"servicer_userid"` // 接待人员的userid,当state=3时要求必填
+}
+
+// ServiceStateTrans 变更会话状态
+func (r *Client) ServiceStateTrans(options ServiceStateTransOptions) (info util.CommonError, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(serviceStateTransAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}

+ 108 - 0
work/kf/syncmsg.go

@@ -0,0 +1,108 @@
+package kf
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/util"
+	"github.com/silenceper/wechat/v2/work/kf/syncmsg"
+)
+
+const (
+	//获取消息
+	syncMsgAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/sync_msg?access_token=%s"
+)
+
+// SyncMsgOptions 获取消息查询参数
+type SyncMsgOptions struct {
+	Cursor string `json:"cursor"` // 上一次调用时返回的next_cursor,第一次拉取可以不填, 不多于64字节
+	Token  string `json:"token"`  // 回调事件返回的token字段,10分钟内有效;可不填,如果不填接口有严格的频率限制, 不多于128字节
+	Limit  uint   `json:"limit"`  // 期望请求的数据量,默认值和最大值都为1000, 注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。
+}
+
+// SyncMsgSchema 获取消息查询响应内容
+type syncMsgSchema struct {
+	ErrCode    int32                    `json:"errcode"`     // 返回码
+	ErrMsg     string                   `json:"errmsg"`      // 错误码描述
+	NextCursor string                   `json:"next_cursor"` // 下次调用带上该值则从该key值往后拉,用于增量拉取
+	HasMore    uint32                   `json:"has_more"`    // 是否还有更多数据。0-否;1-是。不能通过判断msg_list是否空来停止拉取,可能会出现has_more为1,而msg_list为空的情况
+	MsgList    []map[string]interface{} `json:"msg_list"`    // 消息列表
+}
+
+// SyncMsgSchema 获取消息查询响应内容
+type SyncMsgSchema struct {
+	ErrCode    int32             `json:"errcode"`     // 返回码
+	ErrMsg     string            `json:"errmsg"`      // 错误码描述
+	NextCursor string            `json:"next_cursor"` // 下次调用带上该值则从该key值往后拉,用于增量拉取
+	HasMore    uint32            `json:"has_more"`    // 是否还有更多数据。0-否;1-是。不能通过判断msg_list是否空来停止拉取,可能会出现has_more为1,而msg_list为空的情况
+	MsgList    []syncmsg.Message `json:"msg_list"`    // 消息列表
+}
+
+// SyncMsg 获取消息
+func (r *Client) SyncMsg(options SyncMsgOptions) (info SyncMsgSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(syncMsgAddr, accessToken), options)
+	if err != nil {
+		return
+	}
+	originInfo := syncMsgSchema{}
+	if err = json.Unmarshal(data, &originInfo); err != nil {
+		return
+	}
+	if originInfo.ErrCode != 0 {
+		return info, errors.New(originInfo.ErrMsg)
+	}
+	msgList := make([]syncmsg.Message, 0)
+	if len(originInfo.MsgList) > 0 {
+		for _, msg := range originInfo.MsgList {
+			newMsg := syncmsg.Message{}
+			if val, ok := msg["msgid"].(string); ok {
+				newMsg.MsgID = val
+			}
+			if val, ok := msg["open_kfid"].(string); ok {
+				newMsg.OpenKFID = val
+			}
+			if val, ok := msg["external_userid"].(string); ok {
+				newMsg.ExternalUserID = val
+			}
+			if val, ok := msg["send_time"].(float64); ok {
+				newMsg.SendTime = uint64(val)
+			}
+			if val, ok := msg["origin"].(float64); ok {
+				newMsg.Origin = uint32(val)
+			}
+
+			if val, ok := msg["msgtype"].(string); ok {
+				newMsg.MsgType = val
+			}
+			if newMsg.MsgType == "event" {
+				if event, ok := msg["event"].(map[string]interface{}); ok {
+					if eType, ok := event["event_type"].(string); ok {
+						newMsg.EventType = eType
+					}
+				}
+			}
+			originData, err := json.Marshal(msg)
+			if err != nil {
+				return info, err
+			}
+			newMsg.OriginData = originData
+			msgList = append(msgList, newMsg)
+		}
+	}
+	return SyncMsgSchema{
+		ErrCode:    originInfo.ErrCode,
+		ErrMsg:     originInfo.ErrMsg,
+		NextCursor: originInfo.NextCursor,
+		HasMore:    originInfo.HasMore,
+		MsgList:    msgList,
+	}, nil
+}

+ 10 - 0
work/kf/syncmsg/callback.go

@@ -0,0 +1,10 @@
+package syncmsg
+
+// Event 微信客服回调事件
+type Event struct {
+	ToUserName string `json:"to_user_name"` // 微信客服组件ID
+	CreateTime int    `json:"create_time"`  // 消息创建时间,unix时间戳
+	MsgType    string `json:"msgtype"`      // 消息的类型,此时固定为 event
+	Event      string `json:"event"`        // 事件的类型,此时固定为 kf_msg_or_event
+	Token      string `json:"token"`        // 调用拉取消息接口时,需要传此token,用于校验请求的合法性
+}

+ 161 - 0
work/kf/syncmsg/message.go

@@ -0,0 +1,161 @@
+package syncmsg
+
+// BaseMessage 接收消息
+type BaseMessage struct {
+	MsgID              string `json:"msgid"`           // 消息ID
+	OpenKFID           string `json:"open_kfid"`       // 客服帐号ID
+	ExternalUserID     string `json:"external_userid"` // 客户UserID
+	ReceptionistUserID string `json:"servicer_userid"` // 接待客服userID
+	SendTime           uint64 `json:"send_time"`       // 消息发送时间
+	Origin             uint32 `json:"origin"`          // 消息来源。3-客户回复的消息 4-系统推送的消息 5-客服回复消息
+}
+
+// Text 文本消息
+type Text struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:text
+	Text    struct {
+		Content string `json:"content"` // 文本内容
+		MenuID  string `json:"menu_id"` // 客户点击菜单消息,触发的回复消息中附带的菜单ID
+	} `json:"text"` // 文本消息
+}
+
+// Image 图片消息
+type Image struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:image
+	Image   struct {
+		MediaID string `json:"media_id"` // 图片文件ID
+	} `json:"image"` // 图片消息
+}
+
+// Voice 语音消息
+type Voice struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:voice
+	Voice   struct {
+		MediaID string `json:"media_id"` // 语音文件ID
+	} `json:"voice"` // 语音消息
+}
+
+// Video 视频消息
+type Video struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:video
+	Video   struct {
+		MediaID string `json:"media_id"` // 文件ID
+	} `json:"video"` // 视频消息
+}
+
+// File 文件消息
+type File struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:file
+	File    struct {
+		MediaID string `json:"media_id"` // 文件ID
+	} `json:"file"` // 文件消息
+}
+
+// Location 地理位置消息
+type Location struct {
+	BaseMessage
+	MsgType  string `json:"msgtype"` // 消息类型,此时固定为:location
+	Location struct {
+		Latitude  float32 `json:"latitude"`  // 纬度
+		Longitude float32 `json:"longitude"` // 经度
+		Name      string  `json:"name"`      // 位置名
+		Address   string  `json:"address"`   // 地址详情说明
+	} `json:"location"` // 地理位置消息
+}
+
+// Link 链接消息
+type Link struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:link
+	Link    struct {
+		Title  string `json:"title"`   // 标题
+		Desc   string `json:"desc"`    // 描述
+		URL    string `json:"url"`     // 点击后跳转的链接
+		PicURL string `json:"pic_url"` // 缩略图链接
+	} `json:"link"` // 链接消息
+}
+
+// BusinessCard 名片消息
+type BusinessCard struct {
+	BaseMessage
+	MsgType      string `json:"msgtype"` // 消息类型,此时固定为:business_card
+	BusinessCard struct {
+		UserID string `json:"userid"` // 名片 userid
+	} `json:"business_card"` // 名片消息
+}
+
+// MiniProgram 小程序消息
+type MiniProgram struct {
+	BaseMessage
+	MsgType     string `json:"msgtype"` // 消息类型,此时固定为:miniprogram
+	MiniProgram struct {
+		AppID        string `json:"appid"`          // 小程序appid,必须是关联到企业的小程序应用
+		Title        string `json:"title"`          // 小程序消息标题,最多64个字节,超过会自动截断
+		ThumbMediaID string `json:"thumb_media_id"` // 小程序消息封面的mediaid,封面图建议尺寸为520*416
+		PagePath     string `json:"pagepath"`       // 点击消息卡片后进入的小程序页面路径
+	} `json:"miniprogram"` // 小程序消息
+}
+
+// EventMessage 事件消息
+type EventMessage struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:event
+	Event   struct {
+		EventType string `json:"event_type"` // 事件类型
+	} `json:"event"` // 事件消息
+}
+
+// EnterSessionEvent 用户进入会话事件
+type EnterSessionEvent struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:event
+	Event   struct {
+		EventType      string `json:"event_type"`      // 事件类型。此处固定为:enter_session
+		OpenKFID       string `json:"open_kfid"`       // 客服账号ID
+		ExternalUserID string `json:"external_userid"` // 客户UserID
+		Scene          string `json:"scene"`           // 进入会话的场景值,获取客服帐号链接开发者自定义的场景值
+	} `json:"event"` // 事件消息
+}
+
+// MsgSendFailEvent 消息发送失败事件
+type MsgSendFailEvent struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:event
+	Event   struct {
+		EventType      string `json:"event_type"`      // 事件类型。此处固定为:msg_send_fail
+		OpenKFID       string `json:"open_kfid"`       // 客服账号ID
+		ExternalUserID string `json:"external_userid"` // 客户UserID
+		FailMsgID      string `json:"fail_msgid"`      // 发送失败的消息msgid
+		FailType       uint32 `json:"fail_type"`       // 失败类型。0-未知原因 1-客服账号已删除 2-应用已关闭 4-会话已过期,超过48小时 5-会话已关闭 6-超过5条限制 7-未绑定视频号 8-主体未验证 9-未绑定视频号且主体未验证 10-用户拒收
+	} `json:"event"` // 事件消息
+}
+
+// ReceptionistStatusChangeEvent 客服人员接待状态变更事件
+type ReceptionistStatusChangeEvent struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:event
+	Event   struct {
+		EventType          string `json:"event_type"`      // 事件类型。此处固定为:servicer_status_change
+		ReceptionistUserID string `json:"servicer_userid"` // 客服人员userid
+		Status             uint32 `json:"status"`          // 状态类型。1-接待中 2-停止接待
+	} `json:"event"`
+}
+
+// SessionStatusChangeEvent 会话状态变更事件
+type SessionStatusChangeEvent struct {
+	BaseMessage
+	MsgType string `json:"msgtype"` // 消息类型,此时固定为:event
+	Event   struct {
+		EventType             string `json:"event_type"`          // 事件类型。此处固定为:session_status_change
+		OpenKFID              string `json:"open_kfid"`           // 客服账号ID
+		ExternalUserID        string `json:"external_userid"`     // 客户UserID
+		ChangeType            uint32 `json:"change_type"`         // 变更类型。1-从接待池接入会话 2-转接会话 3-结束会话
+		OldReceptionistUserID string `json:"old_servicer_userid"` // 老的客服人员userid。仅change_type为2和3有值
+		NewReceptionistUserID string `json:"new_servicer_userid"` // 新的客服人员userid。仅change_type为1和2有值
+	} `json:"event"` // 事件消息
+}

+ 102 - 0
work/kf/syncmsg/syncmsg.go

@@ -0,0 +1,102 @@
+package syncmsg
+
+import "encoding/json"
+
+// Message 同步的消息内容
+type Message struct {
+	MsgID          string `json:"msgid"`           // 消息ID
+	OpenKFID       string `json:"open_kfid"`       // 客服帐号ID
+	ExternalUserID string `json:"external_userid"` // 客户UserID
+	SendTime       uint64 `json:"send_time"`       // 消息发送时间
+	Origin         uint32 `json:"origin"`          // 消息来源。3-客户回复的消息 4-系统推送的消 息
+	MsgType        string `json:"msgtype"`         // 消息类型
+	EventType      string `json:"event_type"`      // 事件类型
+	OriginData     []byte `json:"origin_data"`     // 原始数据内容
+}
+
+// GetOriginMessage 获取原始消息
+func (r Message) GetOriginMessage() (info []byte) {
+	return r.OriginData
+}
+
+// GetTextMessage 获取文本消息
+func (r Message) GetTextMessage() (info Text, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetImageMessage 获取图片消息
+func (r Message) GetImageMessage() (info Image, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetVoiceMessage 获取语音消息
+func (r Message) GetVoiceMessage() (info Voice, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetVideoMessage 获取视频消息
+func (r Message) GetVideoMessage() (info Video, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetFileMessage 获取文件消息
+func (r Message) GetFileMessage() (info File, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetLocationMessage 获取文件消息
+func (r Message) GetLocationMessage() (info Location, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetLinkMessage 获取链接消息
+func (r Message) GetLinkMessage() (info Link, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetBusinessCardMessage 获取名片消息
+func (r Message) GetBusinessCardMessage() (info BusinessCard, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetMiniProgramMessage 获取小程序消息
+func (r Message) GetMiniProgramMessage() (info MiniProgram, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetEnterSessionEvent 用户进入会话事件
+func (r Message) GetEnterSessionEvent() (info EnterSessionEvent, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	info.OpenKFID = info.Event.OpenKFID
+	info.ExternalUserID = info.Event.ExternalUserID
+	return info, err
+}
+
+// GetMsgSendFailEvent 消息发送失败事件
+func (r Message) GetMsgSendFailEvent() (info MsgSendFailEvent, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetReceptionistStatusChangeEvent 客服人员接待状态变更事件
+func (r Message) GetReceptionistStatusChangeEvent() (info ReceptionistStatusChangeEvent, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	return info, err
+}
+
+// GetSessionStatusChangeEvent 会话状态变更事件
+func (r Message) GetSessionStatusChangeEvent() (info SessionStatusChangeEvent, err error) {
+	err = json.Unmarshal(r.OriginData, &info)
+	info.OpenKFID = info.Event.OpenKFID
+	info.ExternalUserID = info.Event.ExternalUserID
+	return info, err
+}

+ 187 - 0
work/kf/upgrade.go

@@ -0,0 +1,187 @@
+package kf
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/v2/util"
+)
+
+const (
+	//获取配置的专员与客户群
+	upgradeServiceConfigAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/get_upgrade_service_config?access_token=%s"
+	// 为客户升级为专员或客户群服务
+	upgradeService = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/upgrade_service?access_token=%s"
+	//为客户取消推荐
+	upgradeServiceCancel = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/cancel_upgrade_service?access_token=%s"
+)
+
+// UpgradeServiceConfigSchema 获取配置的专员与客户群
+type UpgradeServiceConfigSchema struct {
+	util.CommonError
+	MemberRange struct {
+		UserIDList       []string `json:"userid_list"`        // 专员userid列表
+		DepartmentIDList []string `json:"department_id_list"` // 专员部门列表
+	} `json:"member_range"` // 专员服务配置范围
+	GroupChatRange struct {
+		ChatIDList []string `json:"chat_id_list"` // 客户群列表
+	} `json:"groupchat_range"` // 客户群配置范围
+}
+
+// UpgradeServiceConfig 获取配置的专员与客户群
+func (r *Client) UpgradeServiceConfig() (info UpgradeServiceConfigSchema, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.HTTPGet(fmt.Sprintf(upgradeServiceConfigAddr, accessToken))
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// UpgradeServiceOptions 为客户升级为专员或客户群服务请求参数
+type UpgradeServiceOptions struct {
+	OpenKFID       string `json:"open_kfid"`       // 客服帐号ID
+	ExternalUserID string `json:"external_userid"` // 微信客户的external_userid
+	Type           int    `json:"type"`            // 表示是升级到专员服务还是客户群服务。1:专员服务。2:客户群服务
+	Member         struct {
+		UserID  string `json:"userid"`  // 服务专员的userid
+		Wording string `json:"wording"` // 推荐语
+	} `json:"member"` // 推荐的服务专员,type等于1时有效
+	GroupChat struct {
+		ChatID  string `json:"chat_id"` // 客户群id
+		Wording string `json:"wording"` // 推荐语
+	} `json:"groupchat"` // 推荐的客户群,type等于2时有效
+}
+
+// UpgradeService 为客户升级为专员或客户群服务
+func (r *Client) UpgradeService(options UpgradeServiceOptions) (info util.CommonError, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(upgradeService, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// UpgradeMemberServiceOptions 为客户升级为专员服务请求参数
+type UpgradeMemberServiceOptions struct {
+	OpenKFID       string `json:"open_kfid"`       // 客服帐号ID
+	ExternalUserID string `json:"external_userid"` // 微信客户的external_userid
+	Type           int    `json:"type"`            // 表示是升级到专员服务还是客户群服务。1:专员服务
+	Member         struct {
+		UserID  string `json:"userid"`  // 服务专员的userid
+		Wording string `json:"wording"` // 推荐语
+	} `json:"member"` // 推荐的服务专员,type等于1时有效
+}
+
+// UpgradeMemberService 为客户升级为专员服务
+func (r *Client) UpgradeMemberService(options UpgradeMemberServiceOptions) (info util.CommonError, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(upgradeService, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// UpgradeServiceGroupChatOptions 为客户升级为客户群服务请求参数
+type UpgradeServiceGroupChatOptions struct {
+	OpenKFID       string `json:"open_kfid"`       // 客服帐号ID
+	ExternalUserID string `json:"external_userid"` // 微信客户的external_userid
+	Type           int    `json:"type"`            // 表示是升级到专员服务还是客户群服务。2:客户群服务
+	GroupChat      struct {
+		ChatID  string `json:"chat_id"` // 客户群id
+		Wording string `json:"wording"` // 推荐语
+	} `json:"groupchat"` // 推荐的客户群,type等于2时有效
+}
+
+// UpgradeGroupChatService 为客户升级为客户群服务
+func (r *Client) UpgradeGroupChatService(options UpgradeServiceGroupChatOptions) (info util.CommonError, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(upgradeService, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}
+
+// UpgradeServiceCancelOptions 为客户取消推荐
+type UpgradeServiceCancelOptions struct {
+	OpenKFID       string `json:"open_kfid"`       // 客服帐号ID
+	ExternalUserID string `json:"external_userid"` // 微信客户的external_userid
+}
+
+// UpgradeServiceCancel 为客户取消推荐
+func (r *Client) UpgradeServiceCancel(options UpgradeServiceCancelOptions) (info util.CommonError, err error) {
+	var (
+		accessToken string
+		data        []byte
+	)
+	accessToken, err = r.ctx.GetAccessToken()
+	if err != nil {
+		return
+	}
+	data, err = util.PostJSON(fmt.Sprintf(upgradeServiceCancel, accessToken), options)
+	if err != nil {
+		return
+	}
+	if err = json.Unmarshal(data, &info); err != nil {
+		return
+	}
+	if info.ErrCode != 0 {
+		return info, NewSDKErr(info.ErrCode, info.ErrMsg)
+	}
+	return info, nil
+}

+ 6 - 0
work/work.go

@@ -4,6 +4,7 @@ import (
 	"github.com/silenceper/wechat/v2/credential"
 	"github.com/silenceper/wechat/v2/work/config"
 	"github.com/silenceper/wechat/v2/work/context"
+	"github.com/silenceper/wechat/v2/work/kf"
 	"github.com/silenceper/wechat/v2/work/msgaudit"
 	"github.com/silenceper/wechat/v2/work/oauth"
 )
@@ -37,3 +38,8 @@ func (wk *Work) GetOauth() *oauth.Oauth {
 func (wk *Work) GetMsgAudit() (*msgaudit.Client, error) {
 	return msgaudit.NewClient(wk.ctx.Config)
 }
+
+// GetKF get kf
+func (wk *Work) GetKF() (*kf.Client, error) {
+	return kf.NewClient(wk.ctx.Config)
+}