corp

package
v0.0.0-...-4877452 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 20, 2015 License: Apache-2.0 Imports: 18 Imported by: 0

README

微信企业号 golang SDK

整体架构图

架构图

示例

主動調用微信 api,corp 子包裏面的 Client 基本都是這樣的調用方式
package main

import (
	"fmt"

	"github.com/chanxuehong/wechat/corp"
	"github.com/chanxuehong/wechat/corp/menu"
)

var TokenServer = corp.NewDefaultTokenServer("corpId", "corpSecret", nil) // 一個應用只能有一個實例

func main() {
	var mn menu.Menu
	mn.Buttons = make([]menu.Button, 3)
	mn.Buttons[0].SetAsClickButton("今日歌曲", "V1001_TODAY_MUSIC")
	mn.Buttons[1].SetAsViewButton("视频", "http://v.qq.com/")

	var subButtons = make([]menu.Button, 2)
	subButtons[0].SetAsViewButton("搜索", "http://www.soso.com/")
	subButtons[1].SetAsClickButton("赞一下我们", "V1001_GOOD")

	mn.Buttons[2].SetAsSubMenuButton("子菜单", subButtons)

	clt := menu.NewClient(TokenServer, nil)
	if err := clt.CreateMenu(0 /* agentId */, mn); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("ok")
}
被動接收消息(事件)推送,一个 URL 监听一个企业号应用的消息
package main

import (
	"io"
	"log"
	"net/http"

	"github.com/chanxuehong/wechat/corp"
	"github.com/chanxuehong/wechat/corp/message/request"
	"github.com/chanxuehong/wechat/corp/message/response"
	"github.com/chanxuehong/wechat/util"
)

// 非法请求的 Handler
func InvalidRequestHandler(w http.ResponseWriter, r *http.Request, err error) {
	io.WriteString(w, err.Error())
	log.Println(err.Error())
}

// 文本消息的 Handler
func TextMessageHandler(w http.ResponseWriter, r *corp.Request) {
	// 简单起见,把用户发送过来的文本原样回复过去
	text := request.GetText(r.MixedMsg) // 可以省略...
	resp := response.NewText(text.FromUserName, text.ToUserName, text.CreateTime, text.Content)
	corp.WriteResponse(w, r, resp)
}

func main() {
	aesKey, err := util.AESKeyDecode("encodedAESKey") // 这里 encodedAESKey 改成你自己的参数
	if err != nil {
		panic(err)
	}

	messageServeMux := corp.NewMessageServeMux()
	messageServeMux.MessageHandleFunc(request.MsgTypeText, TextMessageHandler) // 注册文本处理 Handler

	// 下面函数的几个参数设置成你自己的参数: corpId, agentId, token
	agentServer := corp.NewDefaultAgentServer("corpId", 0 /* agentId */, "token", aesKey, messageServeMux)

	agentServerFrontend := corp.NewAgentServerFrontend(agentServer, corp.InvalidRequestHandlerFunc(InvalidRequestHandler))

	// 如果你在微信后台设置的回调地址是
	//   http://xxx.yyy.zzz/agent
	// 那么可以这么注册 http.Handler
	http.Handle("/agent", agentServerFrontend)
	http.ListenAndServe(":80", nil)
}
被動接收消息(事件)推送,一个 URL 监听多个企业号应用的消息
package main

import (
	"io"
	"log"
	"net/http"

	"github.com/chanxuehong/wechat/corp"
	"github.com/chanxuehong/wechat/corp/message/request"
	"github.com/chanxuehong/wechat/corp/message/response"
	"github.com/chanxuehong/wechat/util"
)

// 非法请求的 Handler
func InvalidRequestHandler(w http.ResponseWriter, r *http.Request, err error) {
	io.WriteString(w, err.Error())
	log.Println(err.Error())
}

// 文本消息的 Handler
func TextMessageHandler(w http.ResponseWriter, r *corp.Request) {
	// 简单起见,把用户发送过来的文本原样回复过去
	text := request.GetText(r.MixedMsg) // 可以省略...
	resp := response.NewText(text.FromUserName, text.ToUserName, text.CreateTime, text.Content)
	corp.WriteResponse(w, r, resp)
}

func main() {
	// agentServer1
	aesKey1, err := util.AESKeyDecode("encodedAESKey1") // 这里 encodedAESKey1 改成你自己的参数
	if err != nil {
		panic(err)
	}

	messageServeMux1 := corp.NewMessageServeMux()
	messageServeMux1.MessageHandleFunc(request.MsgTypeText, TextMessageHandler) // 注册文本处理 Handler

	// 下面函数的几个参数设置成你自己的参数: corpId1, agentId1, token1
	agentServer1 := corp.NewDefaultAgentServer("corpId1", 1 /* agentId1 */, "token1", aesKey1, messageServeMux1)

	// agentServer2
	aesKey2, err := util.AESKeyDecode("encodedAESKey2") // 这里 encodedAESKey2 改成你自己的参数
	if err != nil {
		panic(err)
	}

	messageServeMux2 := corp.NewMessageServeMux()
	messageServeMux2.MessageHandleFunc(request.MsgTypeText, TextMessageHandler) // 注册文本处理 Handler

	// 下面函数的几个参数设置成你自己的参数: corpId2, agentId2, token2
	agentServer2 := corp.NewDefaultAgentServer("corpId2", 2 /* agentId2 */, "token2", aesKey2, messageServeMux2)

	// multiAgentServerFrontend
	var multiAgentServerFrontend corp.MultiAgentServerFrontend
	multiAgentServerFrontend.SetInvalidRequestHandler(corp.InvalidRequestHandlerFunc(InvalidRequestHandler))
	multiAgentServerFrontend.SetAgentServer("agent1", agentServer1) // 回調url上面要加上 agent_server=agent1
	multiAgentServerFrontend.SetAgentServer("agent2", agentServer2) // 回調url上面要加上 agent_server=agent2

	// 如果你在微信后台设置的回调地址是
	//   http://xxx.yyy.zzz/agent
	// 那么可以这么注册 http.Handler
	http.Handle("/agent", &multiAgentServerFrontend)
	http.ListenAndServe(":80", nil)
}

Documentation

Overview

微信企业号SDK

Index

Constants

View Source
const (
	ErrCodeOK                = 0
	ErrCodeInvalidCredential = 40001 // access_token 过期(无效)返回这个错误(maybe!!!)
	ErrCodeTimeout           = 42001 // access_token 过期(无效)返回这个错误
)
View Source
const URLQueryAgentServerKeyName = "agent_server"

回调 URL 上索引 AgentServer 的 key 的名称.

比如下面的回调地址里面就可以根据 agent1 来索引对应的 AgentServer.
http://www.xxx.com/?agent_server=agent1&msg_signature=XXX&timestamp=123456789&nonce=12345678

Variables

View Source
var DefaultInvalidRequestHandler = InvalidRequestHandlerFunc(func(http.ResponseWriter, *http.Request, error) {})
View Source
var MediaHttpClient = &http.Client{
	Transport: &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		Dial: (&net.Dialer{
			Timeout:   5 * time.Second,
			KeepAlive: 30 * time.Second,
		}).Dial,
		TLSHandshakeTimeout: 5 * time.Second,
	},
	Timeout: 300 * time.Second,
}

多媒体上传下载请求的 http.Client

View Source
var TextHttpClient = &http.Client{
	Transport: &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		Dial: (&net.Dialer{
			Timeout:   5 * time.Second,
			KeepAlive: 30 * time.Second,
		}).Dial,
		TLSHandshakeTimeout: 5 * time.Second,
	},
	Timeout: 15 * time.Second,
}

一般请求的 http.Client

Functions

func HttpResponseWriter

func HttpResponseWriter(w io.Writer) http.ResponseWriter

将 io.Writer 从语义上实现 http.ResponseWriter.

某些 http 框架可能没有提供 http.ResponseWriter, 而只是提供了 io.Writer.

func ServeHTTP

func ServeHTTP(w http.ResponseWriter, r *http.Request, urlValues url.Values,
	agentServer AgentServer, invalidRequestHandler InvalidRequestHandler)

ServeHTTP 处理 http 消息请求

NOTE: 调用者保证所有参数有效

func WriteResponse

func WriteResponse(w http.ResponseWriter, r *Request, msg interface{}) (err error)

回复消息给微信服务器.

要求 msg 是有效的消息数据结构(经过 encoding/xml marshal 后符合消息的格式);
如果有必要可以修改 Request 里面的某些值, 比如 TimeStamp.

Types

type AgentServer

type AgentServer interface {
	CorpId() string // 获取应用所属的企业号Id
	AgentId() int64 // 获取应用的Id
	Token() string  // 获取应用的Token

	CurrentAESKey() [32]byte // 获取当前有效的 AES 加密 Key
	LastAESKey() [32]byte    // 获取最后一个有效的 AES 加密 Key

	MessageHandler() MessageHandler // 获取 MessageHandler
}

企业号应用的服务端接口, 处理单个应用的消息(事件)请求.

type AgentServerFrontend

type AgentServerFrontend struct {
	// contains filtered or unexported fields
}

实现了 http.Handler, 处理一个企业号应用的消息(事件)请求.

func NewAgentServerFrontend

func NewAgentServerFrontend(server AgentServer, handler InvalidRequestHandler) *AgentServerFrontend

func (*AgentServerFrontend) ServeHTTP

func (frontend *AgentServerFrontend) ServeHTTP(w http.ResponseWriter, r *http.Request)

实现 http.Handler.

type CommonMessageHeader

type CommonMessageHeader struct {
	ToUserName   string `xml:"ToUserName"   json:"ToUserName"`
	FromUserName string `xml:"FromUserName" json:"FromUserName"`
	CreateTime   int64  `xml:"CreateTime"   json:"CreateTime"`
	MsgType      string `xml:"MsgType"      json:"MsgType"`
	AgentId      int64  `xml:"AgentID"      json:"AgentID"`
}

微信服务器推送过来的消息(事件)通用的消息头

type CorpClient

type CorpClient struct {
	TokenServer
	HttpClient *http.Client
}

企业号"主动"请求功能的基本封装.

func (*CorpClient) GetCallbackIP

func (clt *CorpClient) GetCallbackIP() (ipList []string, err error)

获取微信服务器的ip段

func (*CorpClient) GetJSON

func (clt *CorpClient) GetJSON(incompleteURL string, response interface{}) (err error)

GET 微信资源, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response.

NOTE:
1. 一般不用调用这个方法, 请直接调用高层次的封装方法;
2. 最终的 URL == incompleteURL + access_token;
3. response 要求是 struct 的指针, 并且该 struct 拥有属性:
   ErrCode int `json:"errcode"` (可以是直接属性, 也可以是匿名属性里的属性)

func (*CorpClient) PostJSON

func (clt *CorpClient) PostJSON(incompleteURL string, request interface{}, response interface{}) (err error)

用 encoding/json 把 request marshal 为 JSON, 放入 http 请求的 body 中, POST 到微信服务器, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response.

NOTE:
1. 一般不用调用这个方法, 请直接调用高层次的封装方法;
2. 最终的 URL == incompleteURL + access_token;
3. response 要求是 struct 的指针, 并且该 struct 拥有属性:
   ErrCode int `json:"errcode"` (可以是直接属性, 也可以是匿名属性里的属性)

func (*CorpClient) UploadFromReader

func (clt *CorpClient) UploadFromReader(incompleteURL,
	part1FieldName, part1FileName string, part1ValueReader io.Reader,
	part2FieldName string, part2Value []byte,
	response interface{}) (err error)

通用上传接口.

--BOUNDARY
Content-Disposition: form-data; name="FIELDNAME"; filename="FILENAME"
Content-Type: application/octet-stream

FILE-CONTENT
--BOUNDARY
Content-Disposition: form-data; name="FIELDNAME"

JSON-DESCRIPTION
--BOUNDARY--

NOTE:
1. 一般不需要调用这个方法, 请直接调用高层次的封装方法;
2. 最终的 URL == incompleteURL + access_token;
3. part1 是一个文件, part2 是普通的字符串(如果不需要 part2 则把 part2FieldName 留空);
4. response 要求是 struct 的指针, 并且该 struct 拥有属性:
   ErrCode int `json:"errcode"` (可以是直接属性, 也可以是匿名属性里的属性)

type DefaultAgentServer

type DefaultAgentServer struct {
	// contains filtered or unexported fields
}

func NewDefaultAgentServer

func NewDefaultAgentServer(corpId string, agentId int64, token string,
	AESKey []byte, messageHandler MessageHandler) (srv *DefaultAgentServer)

NewDefaultAgentServer 创建一个新的 DefaultAgentServer.

func (*DefaultAgentServer) AgentId

func (srv *DefaultAgentServer) AgentId() int64

func (*DefaultAgentServer) CorpId

func (srv *DefaultAgentServer) CorpId() string

func (*DefaultAgentServer) CurrentAESKey

func (srv *DefaultAgentServer) CurrentAESKey() (key [32]byte)

func (*DefaultAgentServer) LastAESKey

func (srv *DefaultAgentServer) LastAESKey() (key [32]byte)

func (*DefaultAgentServer) MessageHandler

func (srv *DefaultAgentServer) MessageHandler() MessageHandler

func (*DefaultAgentServer) Token

func (srv *DefaultAgentServer) Token() string

func (*DefaultAgentServer) UpdateAESKey

func (srv *DefaultAgentServer) UpdateAESKey(AESKey []byte) (err error)

type DefaultTokenServer

type DefaultTokenServer struct {
	// contains filtered or unexported fields
}

TokenServer 的简单实现.

NOTE:
1. 用于单进程环境.
2. 因为 DefaultTokenServer 同时也是一个简单的中控服务器, 而不是仅仅实现 TokenServer 接口,
   所以整个系统只能存在一个 DefaultTokenServer 实例!

func NewDefaultTokenServer

func NewDefaultTokenServer(corpId, corpSecret string,
	httpClient *http.Client) (srv *DefaultTokenServer)

创建一个新的 DefaultTokenServer.

如果 httpClient == nil 则默认使用 http.DefaultClient.

func (*DefaultTokenServer) Token

func (srv *DefaultTokenServer) Token() (token string, err error)

func (*DefaultTokenServer) TokenRefresh

func (srv *DefaultTokenServer) TokenRefresh() (token string, err error)

type Error

type Error struct {
	ErrCode int    `json:"errcode"`
	ErrMsg  string `json:"errmsg"`
}

func (*Error) Error

func (e *Error) Error() string

type InvalidRequestHandler

type InvalidRequestHandler interface {
	// err 是错误信息
	ServeInvalidRequest(w http.ResponseWriter, r *http.Request, err error)
}

无效请求(非法或者错误)的处理接口.

type InvalidRequestHandlerFunc

type InvalidRequestHandlerFunc func(http.ResponseWriter, *http.Request, error)

func (InvalidRequestHandlerFunc) ServeInvalidRequest

func (fn InvalidRequestHandlerFunc) ServeInvalidRequest(w http.ResponseWriter, r *http.Request, err error)

type MessageHandler

type MessageHandler interface {
	ServeMessage(w http.ResponseWriter, r *Request)
}

微信服务器推送过来的消息(事件)处理接口

type MessageHandlerFunc

type MessageHandlerFunc func(http.ResponseWriter, *Request)

func (MessageHandlerFunc) ServeMessage

func (fn MessageHandlerFunc) ServeMessage(w http.ResponseWriter, r *Request)

type MessageServeMux

type MessageServeMux struct {
	// contains filtered or unexported fields
}

MessageServeMux 实现了一个简单的消息路由器, 同时也是一个 MessageHandler.

func NewMessageServeMux

func NewMessageServeMux() *MessageServeMux

func (*MessageServeMux) DefaultEventHandle

func (mux *MessageServeMux) DefaultEventHandle(handler MessageHandler)

注册 MessageHandler, 处理未知类型的事件.

func (*MessageServeMux) DefaultEventHandleFunc

func (mux *MessageServeMux) DefaultEventHandleFunc(handler func(http.ResponseWriter, *Request))

注册 MessageHandlerFunc, 处理未知类型的事件.

func (*MessageServeMux) DefaultMessageHandle

func (mux *MessageServeMux) DefaultMessageHandle(handler MessageHandler)

注册 MessageHandler, 处理未知类型的消息.

func (*MessageServeMux) DefaultMessageHandleFunc

func (mux *MessageServeMux) DefaultMessageHandleFunc(handler func(http.ResponseWriter, *Request))

注册 MessageHandlerFunc, 处理未知类型的消息.

func (*MessageServeMux) EventHandle

func (mux *MessageServeMux) EventHandle(eventType string, handler MessageHandler)

注册 MessageHandler, 处理特定类型的事件.

func (*MessageServeMux) EventHandleFunc

func (mux *MessageServeMux) EventHandleFunc(eventType string, handler func(http.ResponseWriter, *Request))

注册 MessageHandlerFunc, 处理特定类型的事件.

func (*MessageServeMux) MessageHandle

func (mux *MessageServeMux) MessageHandle(msgType string, handler MessageHandler)

注册 MessageHandler, 处理特定类型的消息.

func (*MessageServeMux) MessageHandleFunc

func (mux *MessageServeMux) MessageHandleFunc(msgType string, handler func(http.ResponseWriter, *Request))

注册 MessageHandlerFunc, 处理特定类型的消息.

func (*MessageServeMux) ServeMessage

func (mux *MessageServeMux) ServeMessage(w http.ResponseWriter, r *Request)

MessageServeMux 实现了 MessageHandler 接口.

type MixedMessage

type MixedMessage struct {
	XMLName struct{} `xml:"xml" json:"-"`
	CommonMessageHeader

	MsgId int64 `xml:"MsgId" json:"MsgId"`

	Content      string  `xml:"Content"      json:"Content"`
	MediaId      string  `xml:"MediaId"      json:"MediaId"`
	PicURL       string  `xml:"PicUrl"       json:"PicUrl"`
	Format       string  `xml:"Format"       json:"Format"`
	ThumbMediaId string  `xml:"ThumbMediaId" json:"ThumbMediaId"`
	LocationX    float64 `xml:"Location_X"   json:"Location_X"`
	LocationY    float64 `xml:"Location_Y"   json:"Location_Y"`
	Scale        int     `xml:"Scale"        json:"Scale"`
	Label        string  `xml:"Label"        json:"Label"`

	Event    string `xml:"Event"    json:"Event"`
	EventKey string `xml:"EventKey" json:"EventKey"`

	ScanCodeInfo struct {
		ScanType   string `xml:"ScanType"   json:"ScanType"`
		ScanResult string `xml:"ScanResult" json:"ScanResult"`
	} `xml:"ScanCodeInfo" json:"ScanCodeInfo"`

	SendPicsInfo struct {
		Count   int `xml:"Count" json:"Count"`
		PicList []struct {
			PicMD5Sum string `xml:"PicMd5Sum" json:"PicMd5Sum"`
		} `xml:"PicList>item,omitempty" json:"PicList,omitempty"`
	} `xml:"SendPicsInfo" json:"SendPicsInfo"`

	SendLocationInfo struct {
		LocationX float64 `xml:"Location_X" json:"Location_X"`
		LocationY float64 `xml:"Location_Y" json:"Location_Y"`
		Scale     int     `xml:"Scale"      json:"Scale"`
		Label     string  `xml:"Label"      json:"Label"`
		Poiname   string  `xml:"Poiname"    json:"Poiname"`
	} `xml:"SendLocationInfo" json:"SendLocationInfo"`

	Latitude  float64 `xml:"Latitude"    json:"Latitude"`
	Longitude float64 `xml:"Longitude"   json:"Longitude"`
	Precision float64 `xml:"Precision"   json:"Precision"`
}

微信服务器推送过来的消息(事件)的合集.

type MultiAgentServerFrontend

type MultiAgentServerFrontend struct {
	// contains filtered or unexported fields
}

多个 AgentServer 的前端, 负责处理 http 请求, net/http.Handler 的实现

NOTE:
MultiAgentServerFrontend 可以处理多个企业号应用的消息(事件),但是要求在回调 URL 上加上一个
查询参数,参考常量 URLQueryAgentServerKeyName,这个参数的值就是 MultiAgentServerFrontend
索引 AgentServer 的 key。

例如回调 URL 为 http://www.xxx.com/weixin?agent_server=1234567890,那么就可以在后端调用

  MultiAgentServerFrontend.SetAgentServer("1234567890", AgentServer)

来增加一个 AgentServer 来处理 agent_server=1234567890 的消息(事件)。

MultiAgentServerFrontend 并发安全,可以在运行中动态增加和删除 AgentServer。

func (*MultiAgentServerFrontend) DeleteAgentServer

func (frontend *MultiAgentServerFrontend) DeleteAgentServer(serverKey string)

删除 serverKey 对应的 AgentServer

func (*MultiAgentServerFrontend) DeleteAllAgentServer

func (frontend *MultiAgentServerFrontend) DeleteAllAgentServer()

删除所有的 AgentServer

func (*MultiAgentServerFrontend) ServeHTTP

func (frontend *MultiAgentServerFrontend) ServeHTTP(w http.ResponseWriter, r *http.Request)

实现 http.Handler

func (*MultiAgentServerFrontend) SetAgentServer

func (frontend *MultiAgentServerFrontend) SetAgentServer(serverKey string, server AgentServer)

设置 serverKey-AgentServer pair. 如果 serverKey == "" 或者 server == nil 则不做任何操作

func (*MultiAgentServerFrontend) SetInvalidRequestHandler

func (frontend *MultiAgentServerFrontend) SetInvalidRequestHandler(handler InvalidRequestHandler)

设置 InvalidRequestHandler, 如果 handler == nil 则使用默认的 DefaultInvalidRequestHandler

type Request

type Request struct {
	HttpRequest *http.Request // 可以为 nil, 因为某些 http 框架没有提供此参数

	QueryValues  url.Values // 回调请求 URL 中的查询参数集合
	MsgSignature string     // 回调请求 URL 中的消息体签名: msg_signature
	TimeStamp    int64      // 回调请求 URL 中的时间戳: timestamp
	Nonce        string     // 回调请求 URL 中的随机数: nonce

	RawMsgXML []byte        // 消息的"明文"XML 文本
	MixedMsg  *MixedMessage // RawMsgXML 解析后的消息

	AESKey [32]byte // 当前消息 AES 加密的 key
	Random []byte   // 当前消息加密时所用的 random, 16 bytes

	// 下面字段是企业号应用的基本信息
	CorpId     string // 请求消息所属企业号的 ID
	AgentId    int64  // 请求消息所属企业号应用的 ID
	AgentToken string // 请求消息所属企业号应用的 Token
}

消息(事件)请求信息

type RequestHttpBody

type RequestHttpBody struct {
	XMLName      struct{} `xml:"xml" json:"-"`
	CorpId       string   `xml:"ToUserName"`
	AgentId      int64    `xml:"AgentID"`
	EncryptedMsg string   `xml:"Encrypt"`
}

微信服务器请求 http body

type ResponseHttpBody

type ResponseHttpBody struct {
	XMLName      struct{} `xml:"xml" json:"-"`
	EncryptedMsg string   `xml:"Encrypt"`
	MsgSignature string   `xml:"MsgSignature"`
	TimeStamp    int64    `xml:"TimeStamp"`
	Nonce        string   `xml:"Nonce"`
}

回复消息的 http body

type TokenServer

type TokenServer interface {
	// 从中控服务器获取被缓存的 access_token.
	Token() (token string, err error)

	// 请求中控服务器到微信服务器刷新 access_token.
	//
	//  高并发场景下某个时间点可能有很多请求(比如缓存的access_token刚好过期时), 但是我们
	//  不期望也没有必要让这些请求都去微信服务器获取 access_token(有可能导致api超过调用限制),
	//  实际上这些请求只需要一个新的 access_token 即可, 所以建议 TokenServer 从微信服务器
	//  获取一次 access_token 之后的至多5秒内(收敛时间, 视情况而定, 理论上至多5个http或tcp周期)
	//  再次调用该函数不再去微信服务器获取, 而是直接返回之前的结果.
	TokenRefresh() (token string, err error)
}

access_token 中控服务器接口, see token_server.png

Directories

Path Synopsis
管理通讯录接口
管理通讯录接口
js-sdk 服务器端接口.
js-sdk 服务器端接口.
管理多媒体文件.
管理多媒体文件.
自定义菜单接口.
自定义菜单接口.
消息(接收和发送)接口.
消息(接收和发送)接口.
request
被动接收普通消息与事件.
被动接收普通消息与事件.
response
被动响应消息.
被动响应消息.
send
主动发消息
主动发消息
OAuth2接口.
OAuth2接口.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL