返回 DeepSeek-Reasonix
types.go
根目录 / internal / bot / types.go
1 // Package bot 实现 Reasonix 多渠道 IM bot 消息网关,支持 QQ、飞书、微信。
2 // 架构参考 Hermes Agent 的 gateway/adapter/session 模式。
3 package bot
4
5 import (
6 "context"
7 "slices"
8 "strings"
9 )
10
11 // Platform 标识 IM 平台。
12 type Platform string
13
14 const (
15 PlatformQQ Platform = "qq"
16 PlatformFeishu Platform = "feishu"
17 PlatformWeixin Platform = "weixin"
18 PlatformDingtalk Platform = "dingtalk"
19 )
20
21 // ChatType 标识会话类型。
22 type ChatType string
23
24 const (
25 ChatDM ChatType = "dm"
26 ChatGroup ChatType = "group"
27 ChatGuild ChatType = "guild"
28 ChatDirect ChatType = "direct"
29 ChatThread ChatType = "thread"
30 )
31
32 // SessionSource 是会话的复合标识,用于生成稳定的 session key。
33 type SessionSource struct {
34 Platform Platform `json:"platform"`
35 ConnectionID string `json:"connection_id,omitempty"`
36 Domain string `json:"domain,omitempty"`
37 ChatType ChatType `json:"chat_type"`
38 ChatID string `json:"chat_id"`
39 UserID string `json:"user_id"`
40 ThreadID string `json:"thread_id,omitempty"`
41 }
42
43 // InboundMedia is an authenticated inbound attachment. Adapters may provide
44 // Data directly, or a lazy Load callback for platform resources that must not
45 // be fetched until the gateway has admitted the sender through its allowlist.
46 type InboundMedia struct {
47 Name string `json:"name,omitempty"`
48 MIME string `json:"mime,omitempty"`
49 Data []byte `json:"-"`
50 Load func(context.Context) ([]byte, string, error) `json:"-"`
51 FailureText string `json:"-"`
52 }
53
54 // InboundMessage 是从任一平台收到的入站消息。
55 type InboundMessage struct {
56 Platform Platform `json:"platform"`
57 ConnectionID string `json:"connection_id,omitempty"`
58 Domain string `json:"domain,omitempty"`
59 ChatType ChatType `json:"chat_type"`
60 ChatID string `json:"chat_id"`
61 UserID string `json:"user_id"`
62 UserName string `json:"user_name"`
63 // OperatorID, when set, is the authenticated actor gated by the allowlist; UserID stays routing-only.
64 OperatorID string `json:"operator_id,omitempty"`
65 Text string `json:"text"`
66 MessageID string `json:"message_id"`
67 ThreadID string `json:"thread_id,omitempty"`
68 // SessionWebhook is the platform reply webhook carried by the inbound
69 // message, used by webhook-addressed channels (DingTalk) to reply to the
70 // conversation. Channels with a global send API leave it empty.
71 SessionWebhook string `json:"session_webhook,omitempty"`
72 MediaURLs []string `json:"media_urls,omitempty"`
73 Media []InboundMedia `json:"-"`
74 // ResolveUserName performs optional platform enrichment after admission.
75 // UserName remains the safe fallback when the callback is nil or fails.
76 ResolveUserName func(context.Context) string `json:"-"`
77 Raw any `json:"-"`
78 }
79
80 // Session derives the SessionSource from this message.
81 func (m InboundMessage) Session() SessionSource {
82 return SessionSource{
83 Platform: m.Platform,
84 ConnectionID: m.ConnectionID,
85 Domain: m.Domain,
86 ChatType: m.ChatType,
87 ChatID: m.ChatID,
88 UserID: m.UserID,
89 ThreadID: m.ThreadID,
90 }
91 }
92
93 // OutboundMessage 是发送到平台的消息。
94 type OutboundMessage struct {
95 ConnectionID string `json:"connection_id,omitempty"`
96 Domain string `json:"domain,omitempty"`
97 ChatID string `json:"chat_id"`
98 ChatType ChatType `json:"chat_type,omitempty"`
99 Text string `json:"text,omitempty"`
100 MediaURLs []string `json:"media_urls,omitempty"`
101 ReplyToMsgID string `json:"reply_to_msg_id,omitempty"`
102 // SessionWebhook 是入站消息携带的会话回复 webhook(webhook 寻址渠道如
103 // 钉钉),由 sendText 从入站消息透传,保证 gateway 重启后持久化恢复的
104 // 消息仍能回复(无需等用户再次发消息重新学习)。
105 SessionWebhook string `json:"session_webhook,omitempty"`
106 Keyboard *InlineKeyboard `json:"keyboard,omitempty"`
107 Card *InteractiveCard `json:"card,omitempty"`
108 }
109
110 // InlineKeyboard 是内联键盘(用于 QQ 审批)。
111 type InlineKeyboard struct {
112 Rows []InlineKeyboardRow `json:"rows"`
113 }
114
115 // InlineKeyboardRow 是一行按钮。
116 type InlineKeyboardRow struct {
117 Buttons []InlineKeyboardButton `json:"buttons"`
118 }
119
120 // InlineKeyboardButton 是一个按钮。
121 type InlineKeyboardButton struct {
122 ID string `json:"id"`
123 Label string `json:"label"`
124 Style int `json:"style,omitempty"` // 0=default, 1=primary, 2=danger
125 CallbackID string `json:"callback_id,omitempty"`
126 }
127
128 // InteractiveCard 是交互式卡片(用于飞书审批/问答)。
129 type InteractiveCard struct {
130 Header string `json:"header"`
131 Elements []InteractiveCardElement `json:"elements"`
132 }
133
134 // InteractiveCardElement 是卡片内元素。
135 type InteractiveCardElement struct {
136 Tag string `json:"tag"`
137 Content string `json:"content,omitempty"`
138 Extra map[string]any `json:"extra,omitempty"`
139 }
140
141 // SendResult 是发送消息的结果。
142 type SendResult struct {
143 MessageID string `json:"message_id,omitempty"`
144 MessageIDs []string `json:"message_ids,omitempty"`
145 Err error `json:"err,omitempty"`
146 }
147
148 // DeliveredMessageIDs returns every known delivered message ID, including the
149 // legacy singular MessageID field, in delivery order without duplicates.
150 func (r SendResult) DeliveredMessageIDs() []string {
151 ids := make([]string, 0, len(r.MessageIDs)+1)
152 add := func(id string) {
153 id = strings.TrimSpace(id)
154 if id == "" {
155 return
156 }
157 if slices.Contains(ids, id) {
158 return
159 }
160 ids = append(ids, id)
161 }
162 for _, id := range r.MessageIDs {
163 add(id)
164 }
165 add(r.MessageID)
166 return ids
167 }
168
169 // Merge appends delivered IDs from another send while keeping MessageID as the
170 // last delivered ID for callers using the legacy singular field.
171 func (r *SendResult) Merge(delivered SendResult) {
172 for _, id := range delivered.DeliveredMessageIDs() {
173 duplicate := slices.Contains(r.MessageIDs, id)
174 if !duplicate {
175 r.MessageIDs = append(r.MessageIDs, id)
176 }
177 r.MessageID = id
178 }
179 }
180
181 // Adapter 是平台适配器接口,每个平台实现一个。
182 type Adapter interface {
183 // Platform 返回平台标识。
184 Platform() Platform
185
186 // Start 启动适配器,连接平台 gateway。
187 Start(ctx context.Context) error
188
189 // Stop 优雅关闭适配器。
190 Stop() error
191
192 // Send 发送一条出站消息。
193 Send(ctx context.Context, msg OutboundMessage) (SendResult, error)
194
195 // SendTyping 发送"正在输入"状态。
196 SendTyping(ctx context.Context, chatID string) error
197
198 // Messages 返回入站消息通道。
199 Messages() <-chan InboundMessage
200
201 // Name 返回适配器实例名(用于日志)。
202 Name() string
203 }
204
205 // TestSender 由支持主动发送测试消息的适配器实现(钉钉需先学到会话
206 // webhook 才能回复,测试发送走最近交互过的会话)。
207 type TestSender interface {
208 TestSend(ctx context.Context, text string) (SendResult, error)
209 }
210
211 // MessageHandler 是 BotGateway 处理入站消息的回调。
212 type MessageHandler func(ctx context.Context, msg InboundMessage)
213
213 lines GO