mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
chore: replace hack with tools and remove obsolete artifacts
- Rename hack/ to tools/ for formal naming - Update plugin development and deployment script references - Remove obsolete DESIGN/MILESTONE/PLAN docs now superseded by docs/ - Remove abandoned internal/onebot implementation - Ignore stray local qq binary artifact
This commit is contained in:
@ -1,250 +0,0 @@
|
||||
package onebot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// EventHandler 处理 OneBot 推送的事件
|
||||
type EventHandler func(event *Event)
|
||||
|
||||
// Client 是 OneBot 反向 WebSocket 客户端
|
||||
// 连接到 OneBot 兼容前端(如 go-cqhttp、Lagrange)的 WS 地址
|
||||
type Client struct {
|
||||
url string
|
||||
accessToken string
|
||||
conn *websocket.Conn
|
||||
mu sync.Mutex
|
||||
done chan struct{}
|
||||
|
||||
eventHandler EventHandler
|
||||
|
||||
// 等待响应的 Action 调用
|
||||
pending sync.Map
|
||||
echoCount int64
|
||||
|
||||
connected atomic.Bool
|
||||
}
|
||||
|
||||
// NewClient 创建 OneBot 反向 WS 客户端
|
||||
// wsURL: ws://host:port/onebot/v11/ws
|
||||
func NewClient(wsURL, accessToken string) *Client {
|
||||
return &Client{
|
||||
url: wsURL,
|
||||
accessToken: accessToken,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// SetEventHandler 注册事件处理函数
|
||||
func (c *Client) SetEventHandler(h EventHandler) {
|
||||
c.eventHandler = h
|
||||
}
|
||||
|
||||
// Connected 返回是否已连接
|
||||
func (c *Client) Connected() bool {
|
||||
return c.connected.Load()
|
||||
}
|
||||
|
||||
// Connect 连接到 OneBot 前端(阻塞直到连接建立或失败)
|
||||
func (c *Client) Connect() error {
|
||||
header := make(map[string][]string)
|
||||
if c.accessToken != "" {
|
||||
header["Authorization"] = []string{"Bearer " + c.accessToken}
|
||||
}
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.Dial(c.url, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
c.conn = conn
|
||||
c.mu.Unlock()
|
||||
|
||||
c.connected.Store(true)
|
||||
log.Printf("[onebot] connected to %s", c.url)
|
||||
|
||||
go c.readLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconnect 自动重连
|
||||
func (c *Client) reconnect() {
|
||||
c.connected.Store(false)
|
||||
backoff := time.Second
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.done:
|
||||
return
|
||||
case <-time.After(backoff):
|
||||
log.Printf("[onebot] reconnecting in %v...", backoff)
|
||||
if err := c.Connect(); err != nil {
|
||||
log.Printf("[onebot] reconnect failed: %v, retry", err)
|
||||
backoff *= 2
|
||||
if backoff > 30*time.Second {
|
||||
backoff = 30 * time.Second
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Printf("[onebot] reconnected")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) readLoop() {
|
||||
defer c.connected.Store(false)
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
|
||||
if conn == nil {
|
||||
log.Printf("[onebot] read loop: not connected")
|
||||
go c.reconnect()
|
||||
return
|
||||
}
|
||||
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("[onebot] read error: %v", err)
|
||||
go c.reconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试解析为 ActionResponse(有 echo 字段)
|
||||
var resp ActionResponse
|
||||
if err := json.Unmarshal(message, &resp); err == nil && resp.Echo != "" {
|
||||
if ch, ok := c.pending.Load(resp.Echo); ok {
|
||||
select {
|
||||
case ch.(chan *ActionResponse) <- &resp:
|
||||
default:
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析为 Event
|
||||
var evt Event
|
||||
if err := json.Unmarshal(message, &evt); err != nil {
|
||||
log.Printf("[onebot] parse error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if c.eventHandler != nil {
|
||||
c.eventHandler(&evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendAction 发送一个 OneBot API 请求并等待响应
|
||||
func (c *Client) SendAction(action string, params map[string]interface{}, timeout time.Duration) (*ActionResponse, error) {
|
||||
echo := atomic.AddInt64(&c.echoCount, 1)
|
||||
echoStr := formatInt64(echo)
|
||||
|
||||
msg := Action{
|
||||
Action: action,
|
||||
Params: params,
|
||||
Echo: echoStr,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan *ActionResponse, 1)
|
||||
c.pending.Store(echoStr, ch)
|
||||
defer c.pending.Delete(echoStr)
|
||||
|
||||
c.mu.Lock()
|
||||
if c.conn == nil {
|
||||
c.mu.Unlock()
|
||||
return nil, fmt.Errorf("not connected")
|
||||
}
|
||||
err = c.conn.WriteMessage(websocket.TextMessage, data)
|
||||
c.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if timeout <= 0 {
|
||||
timeout = 10 * time.Second
|
||||
}
|
||||
|
||||
select {
|
||||
case resp := <-ch:
|
||||
if resp.Status == "failed" {
|
||||
return resp, fmt.Errorf("onebot action %s failed: retcode=%d", action, resp.RetCode)
|
||||
}
|
||||
return resp, nil
|
||||
case <-time.After(timeout):
|
||||
return nil, fmt.Errorf("onebot action %s timeout", action)
|
||||
}
|
||||
}
|
||||
|
||||
// SendPrivateMessage 发送私聊消息(便捷方法)
|
||||
func (c *Client) SendPrivateMessage(userID int64, message interface{}, autoEscape bool) (*ActionResponse, error) {
|
||||
params := map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"message": message,
|
||||
"auto_escape": autoEscape,
|
||||
}
|
||||
return c.SendAction("send_private_msg", params, 0)
|
||||
}
|
||||
|
||||
// SendGroupMessage 发送群消息(便捷方法)
|
||||
func (c *Client) SendGroupMessage(groupID int64, message interface{}, autoEscape bool) (*ActionResponse, error) {
|
||||
params := map[string]interface{}{
|
||||
"group_id": groupID,
|
||||
"message": message,
|
||||
"auto_escape": autoEscape,
|
||||
}
|
||||
return c.SendAction("send_group_msg", params, 0)
|
||||
}
|
||||
|
||||
// GetLoginInfo 获取登录号信息
|
||||
func (c *Client) GetLoginInfo() (*ActionResponse, error) {
|
||||
return c.SendAction("get_login_info", nil, 0)
|
||||
}
|
||||
|
||||
// GetGroupMemberInfo 获取群成员信息
|
||||
func (c *Client) GetGroupMemberInfo(groupID, userID int64) (*ActionResponse, error) {
|
||||
return c.SendAction("get_group_member_info", map[string]interface{}{
|
||||
"group_id": groupID,
|
||||
"user_id": userID,
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// GetGroupList 获取群列表
|
||||
func (c *Client) GetGroupList() (*ActionResponse, error) {
|
||||
return c.SendAction("get_group_list", nil, 0)
|
||||
}
|
||||
|
||||
// Close 关闭连接
|
||||
func (c *Client) Close() error {
|
||||
close(c.done)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn != nil {
|
||||
return c.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatInt64(n int64) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
@ -1,220 +0,0 @@
|
||||
package onebot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
)
|
||||
|
||||
// Device 将 OneBot 客户端包装为 IO 抽象层的 Device
|
||||
// 作为 QQ 通道与 HomeAgent 之间的桥梁
|
||||
type Device struct {
|
||||
name string
|
||||
desc string
|
||||
client *Client
|
||||
iom *agentIO.IOManager
|
||||
}
|
||||
|
||||
// NewDevice 创建 OneBot IO 设备
|
||||
// name: 设备名称(如 "qq")
|
||||
// wsURL: OneBot 前端 WebSocket 地址(如 "ws://127.0.0.1:6700")
|
||||
// accessToken: OneBot 鉴权令牌(可选)
|
||||
func NewDevice(name, wsURL, accessToken string, iom *agentIO.IOManager) *Device {
|
||||
return &Device{
|
||||
name: name,
|
||||
desc: fmt.Sprintf("OneBot 标准 QQ 通道 (%s)", wsURL),
|
||||
client: NewClient(wsURL, accessToken),
|
||||
iom: iom,
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回设备名称
|
||||
func (d *Device) Name() string { return d.name }
|
||||
|
||||
// Type 返回设备类型(双向 IO)
|
||||
func (d *Device) Type() agentIO.DeviceType { return agentIO.DeviceIO }
|
||||
|
||||
// Description 返回设备描述
|
||||
func (d *Device) Description() string { return d.desc }
|
||||
|
||||
// OutputCapabilities 返回输出能力(文本+文件+图片)
|
||||
func (d *Device) OutputCapabilities() agentIO.OutputCapability {
|
||||
return agentIO.CapText | agentIO.CapFile | agentIO.CapImage
|
||||
}
|
||||
|
||||
// Tools 返回 OneBot 标准 API 的工具定义
|
||||
// AI 可以通过这些工具调用 OneBot 功能
|
||||
func (d *Device) Tools() []agentIO.ToolDef {
|
||||
return []agentIO.ToolDef{
|
||||
{
|
||||
Name: d.name + "_send_private_msg",
|
||||
Description: "发送 QQ 私聊消息",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"user_id": map[string]interface{}{"type": "integer", "description": "目标 QQ 号"},
|
||||
"message": map[string]interface{}{"type": "string", "description": "消息内容(支持 CQ 码,如 [CQ:image,file=xxx.jpg])"},
|
||||
"auto_escape": map[string]interface{}{"type": "boolean", "description": "是否作为纯文本发送(不解析 CQ 码)"},
|
||||
},
|
||||
"required": []string{"user_id", "message"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: d.name + "_send_group_msg",
|
||||
Description: "发送 QQ 群消息",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"group_id": map[string]interface{}{"type": "integer", "description": "目标群号"},
|
||||
"message": map[string]interface{}{"type": "string", "description": "消息内容(支持 CQ 码)"},
|
||||
"auto_escape": map[string]interface{}{"type": "boolean", "description": "是否作为纯文本发送"},
|
||||
},
|
||||
"required": []string{"group_id", "message"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: d.name + "_get_group_member_info",
|
||||
Description: "获取 QQ 群成员信息",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"group_id": map[string]interface{}{"type": "integer", "description": "群号"},
|
||||
"user_id": map[string]interface{}{"type": "integer", "description": "QQ 号"},
|
||||
},
|
||||
"required": []string{"group_id", "user_id"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: d.name + "_get_group_list",
|
||||
Description: "获取 QQ 群列表",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行 OneBot 工具调用
|
||||
func (d *Device) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
||||
if !d.client.Connected() {
|
||||
return nil, fmt.Errorf("onebot 未连接到前端")
|
||||
}
|
||||
|
||||
// 去掉名称前缀以匹配方法名
|
||||
method := strings.TrimPrefix(tool, d.name+"_")
|
||||
|
||||
switch method {
|
||||
case "send_private_msg":
|
||||
userID, _ := toInt64(args["user_id"])
|
||||
message, _ := args["message"].(string)
|
||||
autoEscape, _ := args["auto_escape"].(bool)
|
||||
return d.client.SendPrivateMessage(userID, message, autoEscape)
|
||||
|
||||
case "send_group_msg":
|
||||
groupID, _ := toInt64(args["group_id"])
|
||||
message, _ := args["message"].(string)
|
||||
autoEscape, _ := args["auto_escape"].(bool)
|
||||
return d.client.SendGroupMessage(groupID, message, autoEscape)
|
||||
|
||||
case "get_group_member_info":
|
||||
groupID, _ := toInt64(args["group_id"])
|
||||
userID, _ := toInt64(args["user_id"])
|
||||
return d.client.GetGroupMemberInfo(groupID, userID)
|
||||
|
||||
case "get_group_list":
|
||||
return d.client.GetGroupList()
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown onebot tool: %s", tool)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 连接到 OneBot 前端
|
||||
func (d *Device) Start() error {
|
||||
// 非阻塞连接
|
||||
go func() {
|
||||
if err := d.client.Connect(); err != nil {
|
||||
log.Printf("[onebot] %s initial connect failed, will retry: %v", d.name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 注册事件处理:OneBot 事件 → IO InputEvent
|
||||
d.client.SetEventHandler(func(evt *Event) {
|
||||
d.handleEvent(evt)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 断开连接
|
||||
func (d *Device) Stop() error {
|
||||
return d.client.Close()
|
||||
}
|
||||
|
||||
// handleEvent 将 OneBot 事件转换为 IO InputEvent
|
||||
func (d *Device) handleEvent(evt *Event) {
|
||||
if d.iom == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch evt.PostType {
|
||||
case "message":
|
||||
var text string
|
||||
if evt.RawMessage != "" {
|
||||
text = evt.RawMessage
|
||||
} else if s, ok := evt.Message.(string); ok {
|
||||
text = s
|
||||
}
|
||||
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 构造输入源标识
|
||||
source := d.name
|
||||
payload := map[string]interface{}{
|
||||
"content": text,
|
||||
"source": source,
|
||||
"user_id": evt.UserID,
|
||||
"sender": evt.Sender,
|
||||
}
|
||||
if evt.MessageType == "group" {
|
||||
payload["group_id"] = evt.GroupID
|
||||
payload["label"] = fmt.Sprintf("group:%d:%d", evt.GroupID, evt.UserID)
|
||||
} else {
|
||||
payload["label"] = fmt.Sprintf("private:%d", evt.UserID)
|
||||
}
|
||||
|
||||
d.iom.InjectTextTo(source, d.name, text)
|
||||
|
||||
case "notice":
|
||||
log.Printf("[onebot] notice from %s: type=%s", d.name, evt.NoticeType)
|
||||
|
||||
case "request":
|
||||
log.Printf("[onebot] request from %s: type=%s flag=%s", d.name, evt.RequestType, evt.Flag)
|
||||
|
||||
case "meta_event":
|
||||
if evt.MetaEventType == "heartbeat" {
|
||||
log.Printf("[onebot] %s heartbeat: online=%v", d.name, evt.Status != nil && evt.Status.Online)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toInt64(v interface{}) (int64, bool) {
|
||||
switch n := v.(type) {
|
||||
case int64:
|
||||
return n, true
|
||||
case float64:
|
||||
return int64(n), true
|
||||
case int:
|
||||
return int64(n), true
|
||||
case json.Number:
|
||||
i, err := n.Int64()
|
||||
return i, err == nil
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
// Package onebot 实现 OneBot V11 标准协议(Reverse WebSocket 通信)
|
||||
// 参考: https://github.com/botuniverse/onebot-11
|
||||
package onebot
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Action 是 OneBot 标准 API 请求
|
||||
type Action struct {
|
||||
Action string `json:"action"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
Echo string `json:"echo,omitempty"`
|
||||
}
|
||||
|
||||
// ActionResponse 是 OneBot 标准 API 响应
|
||||
type ActionResponse struct {
|
||||
Status string `json:"status"`
|
||||
RetCode int `json:"retcode"`
|
||||
Data interface{} `json:"data"`
|
||||
Echo string `json:"echo,omitempty"`
|
||||
}
|
||||
|
||||
// Event 是 OneBot 推送的事件
|
||||
type Event struct {
|
||||
Time int64 `json:"time"`
|
||||
SelfID int64 `json:"self_id"`
|
||||
PostType string `json:"post_type"` // message, notice, request, meta_event
|
||||
DetailType string `json:"-"`
|
||||
|
||||
// 消息事件字段
|
||||
MessageType string `json:"message_type,omitempty"` // private, group
|
||||
SubType string `json:"sub_type,omitempty"`
|
||||
MessageID int64 `json:"message_id,omitempty"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
GroupID int64 `json:"group_id,omitempty"`
|
||||
Message interface{} `json:"message,omitempty"` // string 或 []MessageSegment
|
||||
RawMessage string `json:"raw_message,omitempty"`
|
||||
Font int `json:"font,omitempty"`
|
||||
Sender *Sender `json:"sender,omitempty"`
|
||||
|
||||
// 通知事件字段
|
||||
NoticeType string `json:"notice_type,omitempty"`
|
||||
|
||||
// 请求事件字段
|
||||
RequestType string `json:"request_type,omitempty"`
|
||||
Flag string `json:"flag,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// 元事件字段
|
||||
MetaEventType string `json:"meta_event_type,omitempty"`
|
||||
Interval int64 `json:"interval,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type Sender struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Sex string `json:"sex,omitempty"`
|
||||
Age int `json:"age,omitempty"`
|
||||
Card string `json:"card,omitempty"` // 群名片
|
||||
Area string `json:"area,omitempty"`
|
||||
Level string `json:"level,omitempty"`
|
||||
Role string `json:"role,omitempty"` // owner, admin, member
|
||||
Title string `json:"title,omitempty"`
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
AppInitialized bool `json:"app_initialized"`
|
||||
AppEnabled bool `json:"app_enabled"`
|
||||
PluginsGood bool `json:"plugins_good"`
|
||||
AppGood bool `json:"app_good"`
|
||||
Online bool `json:"online"`
|
||||
Good bool `json:"good"`
|
||||
}
|
||||
|
||||
// MessageSegment 表示 OneBot 消息段(数组格式)
|
||||
type MessageSegment struct {
|
||||
Type string `json:"type"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
// MessageText 快速构造纯文本消息段
|
||||
func MessageText(text string) MessageSegment {
|
||||
return MessageSegment{Type: "text", Data: map[string]string{"text": text}}
|
||||
}
|
||||
|
||||
// MessageImage 构造图片消息段
|
||||
func MessageImage(file string) MessageSegment {
|
||||
return MessageSegment{Type: "image", Data: map[string]string{"file": file}}
|
||||
}
|
||||
|
||||
// MessageAt 构造 @ 消息段
|
||||
func MessageAt(userID int64) MessageSegment {
|
||||
return MessageSegment{Type: "at", Data: map[string]string{"qq": fmt.Sprintf("%d", userID)}}
|
||||
}
|
||||
@ -1,165 +0,0 @@
|
||||
package onebot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMessageText(t *testing.T) {
|
||||
seg := MessageText("hello")
|
||||
if seg.Type != "text" {
|
||||
t.Errorf("expected type 'text', got %q", seg.Type)
|
||||
}
|
||||
if seg.Data["text"] != "hello" {
|
||||
t.Errorf("expected data.text 'hello', got %q", seg.Data["text"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImage(t *testing.T) {
|
||||
seg := MessageImage("test.jpg")
|
||||
if seg.Type != "image" {
|
||||
t.Errorf("expected 'image', got %q", seg.Type)
|
||||
}
|
||||
if seg.Data["file"] != "test.jpg" {
|
||||
t.Errorf("expected 'test.jpg', got %q", seg.Data["file"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageAt(t *testing.T) {
|
||||
seg := MessageAt(123456)
|
||||
if seg.Type != "at" {
|
||||
t.Errorf("expected 'at', got %q", seg.Type)
|
||||
}
|
||||
if seg.Data["qq"] != "123456" {
|
||||
t.Errorf("expected '123456', got %q", seg.Data["qq"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventMarshal(t *testing.T) {
|
||||
evt := Event{
|
||||
Time: 1234567890,
|
||||
SelfID: 10001,
|
||||
PostType: "message",
|
||||
MessageType: "group",
|
||||
GroupID: 999,
|
||||
UserID: 777,
|
||||
RawMessage: "hello",
|
||||
Sender: &Sender{
|
||||
UserID: 777,
|
||||
Nickname: "TestUser",
|
||||
Role: "member",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(evt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var decoded Event
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if decoded.PostType != "message" {
|
||||
t.Errorf("expected 'message', got %q", decoded.PostType)
|
||||
}
|
||||
if decoded.GroupID != 999 {
|
||||
t.Errorf("expected 999, got %d", decoded.GroupID)
|
||||
}
|
||||
if decoded.Sender.Nickname != "TestUser" {
|
||||
t.Errorf("expected 'TestUser', got %q", decoded.Sender.Nickname)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventMessagePrivate(t *testing.T) {
|
||||
evt := Event{
|
||||
PostType: "message",
|
||||
MessageType: "private",
|
||||
UserID: 123,
|
||||
RawMessage: "hi",
|
||||
}
|
||||
|
||||
if evt.PostType != "message" || evt.MessageType != "private" {
|
||||
t.Errorf("unexpected event type: %s/%s", evt.PostType, evt.MessageType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionMarshal(t *testing.T) {
|
||||
action := Action{
|
||||
Action: "send_private_msg",
|
||||
Params: map[string]interface{}{
|
||||
"user_id": 123,
|
||||
"message": "hello",
|
||||
},
|
||||
Echo: "1",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(action)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var decoded Action
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if decoded.Action != "send_private_msg" {
|
||||
t.Errorf("expected 'send_private_msg', got %q", decoded.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionResponse(t *testing.T) {
|
||||
resp := ActionResponse{
|
||||
Status: "ok",
|
||||
RetCode: 0,
|
||||
Data: map[string]interface{}{
|
||||
"message_id": 12345,
|
||||
},
|
||||
Echo: "1",
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(resp)
|
||||
var decoded ActionResponse
|
||||
json.Unmarshal(data, &decoded)
|
||||
|
||||
if decoded.Status != "ok" {
|
||||
t.Errorf("expected 'ok', got %q", decoded.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus(t *testing.T) {
|
||||
s := Status{
|
||||
AppInitialized: true,
|
||||
AppEnabled: true,
|
||||
Online: true,
|
||||
Good: true,
|
||||
}
|
||||
|
||||
if !s.Online || !s.Good {
|
||||
t.Error("status should be online and good")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventMetaHeartbeat(t *testing.T) {
|
||||
evt := Event{
|
||||
PostType: "meta_event",
|
||||
MetaEventType: "heartbeat",
|
||||
Interval: 3000,
|
||||
Status: &Status{
|
||||
Online: true,
|
||||
Good: true,
|
||||
},
|
||||
}
|
||||
|
||||
if evt.PostType != "meta_event" {
|
||||
t.Errorf("expected 'meta_event', got %q", evt.PostType)
|
||||
}
|
||||
if evt.MetaEventType != "heartbeat" {
|
||||
t.Errorf("expected 'heartbeat', got %q", evt.MetaEventType)
|
||||
}
|
||||
if !evt.Status.Online {
|
||||
t.Error("should be online")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user