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