diff --git a/work/config/config.go b/work/config/config.go index f4f35ee..84aef7c 100644 --- a/work/config/config.go +++ b/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配置,用于解密回调消息内容对应的密文 } diff --git a/work/kf/README.md b/work/kf/README.md new file mode 100644 index 0000000..f736183 --- /dev/null +++ b/work/kf/README.md @@ -0,0 +1,3 @@ +### 微信客服SDK + +相关文档正在梳理中... \ No newline at end of file diff --git a/work/kf/account.go b/work/kf/account.go new file mode 100644 index 0000000..1a319cc --- /dev/null +++ b/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 +} diff --git a/work/kf/client.go b/work/kf/client.go new file mode 100644 index 0000000..730efd1 --- /dev/null +++ b/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 +} diff --git a/work/kf/customer.go b/work/kf/customer.go new file mode 100644 index 0000000..3420ee3 --- /dev/null +++ b/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 +} diff --git a/work/kf/error.go b/work/kf/error.go new file mode 100644 index 0000000..7ddb9ba --- /dev/null +++ b/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 + } +} diff --git a/work/kf/sendmsg.go b/work/kf/sendmsg.go new file mode 100644 index 0000000..2f2049f --- /dev/null +++ b/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 +} diff --git a/work/kf/sendmsg/message.go b/work/kf/sendmsg/message.go new file mode 100644 index 0000000..48cc77a --- /dev/null +++ b/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"` +} diff --git a/work/kf/sendmsg/sendmsg.go b/work/kf/sendmsg/sendmsg.go new file mode 100644 index 0000000..58e7e58 --- /dev/null +++ b/work/kf/sendmsg/sendmsg.go @@ -0,0 +1 @@ +package sendmsg diff --git a/work/kf/servicer.go b/work/kf/servicer.go new file mode 100644 index 0000000..d1ba656 --- /dev/null +++ b/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 +} diff --git a/work/kf/servicestate.go b/work/kf/servicestate.go new file mode 100644 index 0000000..ef77a01 --- /dev/null +++ b/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 +} diff --git a/work/kf/syncmsg.go b/work/kf/syncmsg.go new file mode 100644 index 0000000..2843b46 --- /dev/null +++ b/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 +} diff --git a/work/kf/syncmsg/callback.go b/work/kf/syncmsg/callback.go new file mode 100644 index 0000000..9558aa9 --- /dev/null +++ b/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,用于校验请求的合法性 +} diff --git a/work/kf/syncmsg/message.go b/work/kf/syncmsg/message.go new file mode 100644 index 0000000..fda1288 --- /dev/null +++ b/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"` // 事件消息 +} diff --git a/work/kf/syncmsg/syncmsg.go b/work/kf/syncmsg/syncmsg.go new file mode 100644 index 0000000..96c9a60 --- /dev/null +++ b/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 +} diff --git a/work/kf/upgrade.go b/work/kf/upgrade.go new file mode 100644 index 0000000..4b11f48 --- /dev/null +++ b/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 +} diff --git a/work/work.go b/work/work.go index 5461370..b974c35 100644 --- a/work/work.go +++ b/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) +}