接收消息

元信息

yaml
---
name: app_chat_message_create
description: "机器人所在会话收到新消息时触发此事件。"
metadata:
  business_domain: "im"
  updated-on: "2026-05-28"
  tags: "消息与群组"
  decrypt-type: "[AES-CBC](https://365.kdocs.cn/3rd/open/documents/app-integration-dev/wps365/server/event-subscription/security-verification)"
  scopes: "应用授权:app:kso.chat_message.readwrite"
---

事件描述

机器人所在会话收到新消息时触发此事件。应用可据此实现自动回复、命令处理等逻辑。仅对应用(机器人)已加入的会话触发,未加入的会话不会推送。

事件信息说明

属性说明
事件主题kso.app_chat.message
事件行为create
业务域im
权限要求应用授权:app:kso.chat_message.readwrite(查询和管理会话消息)
解密方式AES-CBC
重试策略应用收到 HTTP POST 请求后,需要在 3 秒内以 HTTP 200 状态码响应该请求。否则 WPS 开放平台认为本次推送失败,并以 1 秒、2 秒的间隔重新推送事件,最多重试 2 次,如果 2 次失败后仍未成功,则停止推送该事件,但不影响其他事件的推送

事件体(解密前)

全平台统一,所有解密前事件体有且仅有以下参数信息。

参数名称参数类型是否必填可选值限制示例说明
topicstringkso.app_chat.message固定为kso.app_chat.messagekso.app_chat.message事件主题
operationstringcreate:创建固定为createcreate收到新消息
timeinteger-秒级时间戳1779926400事件发生时间,秒级时间戳
noncestring-字符长度[1,128]71***********7IV 向量(解密时使用)
signaturestring-字符长度[1,512]w6**********6Q消息签名
encrypted_datastring-字符长度[1,2048]B7**********iA==业务事件体加密字段

事件体示例(解密前)

json
{
  "topic": "kso.app_chat.message",
  "operation": "create",
  "time": 1779926400,
  "nonce": "71***********7",
  "signature": "w6**********6Q",
  "encrypted_data": "B7**********iA=="
}

业务事件体(encrypted_data解密后)

参数名称参数类型是否必填可选值限制示例说明
company_idstring--"demo_company_id"企业 ID
chatobject--会话信息
∟ idstring--"demo_chat_id"会话 ID
∟ typestring--"p2p"会话类型
senderobject--消息发送者
∟ idstring--"demo_sender_id"身份 ID
∟ typestringuser:用户
sp:服务主体
app:应用
unknown:未知
-"user"身份类型
send_timeinteger-秒级时间戳1779926400消息发送时间戳
messageobject--消息内容
∟ idstring--"demo_message_id"消息 ID
∟ typestringtext:文本
rich_text:富文本
image:图片
file:文件
sticker:表情
audio:音频
video:视频
-"text"消息类型
∟ contentobject--消息内容,结构随 type 不同而变化
∟ mentionsarray[object]--消息被 @ 人列表
∟∟ idstring--"0"at 操作的实体索引 ID,与消息正文中 <at id={index}> 标记匹配
∟∟ typestringall:所有人
user:用户
-"user"at 操作对象类型
∟∟ identityobject--被 at 的用户信息,at 所有人时为空
∟ quote_msg_idstring--"demo_quote_msg_id"被引用的消息 ID

业务事件体示例(解密后)

json
{
  "company_id": "demo_company_id",
  "chat": {
    "id": "demo_chat_id",
    "type": "p2p"
  },
  "sender": {
    "id": "demo_sender_id",
    "type": "user"
  },
  "send_time": 1779926400,
  "message": {
    "id": "demo_message_id",
    "type": "text",
    "content": {
      "text": {
        "content": "你好<at id=\"0\">机器人</at>"
      }
    },
    "mentions": [
      {
        "id": "0",
        "type": "user",
        "identity": {
          "type": "sp",
          "id": "demo_app_id"
        }
      }
    ]
  }
}

接收示例

Go

go
package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/md5"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

type Event struct {
	Topic         string `json:"topic"`
	Operation     string `json:"operation"`
	Time          int64  `json:"time"`
	Nonce         string `json:"nonce"`
	Signature     string `json:"signature"`
	EncryptedData string `json:"encrypted_data"`
}

type ChatInfo struct {
	ID   string `json:"id"`
	Type string `json:"type"`
}

type Identity struct {
	ID   string `json:"id"`
	Type string `json:"type"`
}

type Mention struct {
	ID       string    `json:"id"`
	Type     string    `json:"type"`
	Identity *Identity `json:"identity,omitempty"`
}

type MessageInfo struct {
	ID         string    `json:"id"`
	Type       string    `json:"type"`
	Content    any       `json:"content"`
	Mentions   []Mention `json:"mentions,omitempty"`
	QuoteMsgID *string   `json:"quote_msg_id,omitempty"`
}

type AppChatMessageCreateData struct {
	CompanyID string      `json:"company_id"`
	Chat      ChatInfo    `json:"chat"`
	Sender    Identity    `json:"sender"`
	SendTime  int64       `json:"send_time"`
	Message   MessageInfo `json:"message"`
}

const (
	accessKey = "your_access_key"
	secretKey = "your_secret_key"
)

func Decrypt(encryptedData, cipherKey, nonce string) (string, error) {
	data, err := base64.StdEncoding.DecodeString(encryptedData)
	if err != nil {
		return "", err
	}
	rawData, err := AESCBCPKCS7Decrypt(data, []byte(cipherKey), []byte(nonce))
	if err != nil {
		return "", err
	}
	return string(rawData), nil
}

func AESCBCPKCS7Decrypt(encryptData, key, nonce []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	if len(encryptData) < blockSize {
		return nil, errors.New("cipher text too short")
	}
	if len(encryptData)%blockSize != 0 {
		return nil, errors.New("cipher text is not a multiple of the block size")
	}

	iv := nonce[:blockSize]
	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(encryptData, encryptData)

	length := len(encryptData)
	unPadding := int(encryptData[length-1])
	return encryptData[:(length - unPadding)], nil
}

func Md5Hex(s string) string {
	h := md5.New()
	h.Write([]byte(s))
	return hex.EncodeToString(h.Sum(nil))
}

func HmacSha256(message, secret string) string {
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(message))
	return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}

func main() {
	router := gin.Default()

	router.POST("/open/receive", func(c *gin.Context) {
		event := &Event{}
		if err := c.ShouldBindJSON(event); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		content := fmt.Sprintf("%s:%s:%s:%d:%s",
			accessKey, event.Topic, event.Nonce, event.Time, event.EncryptedData)
		signature := HmacSha256(content, secretKey)
		if signature != event.Signature {
			c.JSON(http.StatusBadRequest, gin.H{"error": "signature check failed"})
			return
		}

		cipherKey := Md5Hex(secretKey)
		decryptedData, err := Decrypt(event.EncryptedData, cipherKey, event.Nonce)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		data := &AppChatMessageCreateData{}
		if err = json.Unmarshal([]byte(decryptedData), data); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// TODO 处理业务逻辑
		fmt.Printf("事件内容: %+v\n", data)

		c.JSON(http.StatusOK, gin.H{"code": 0})
	})

	_ = router.Run()
}