返回 DeepSeek-Reasonix
runtime.go
根目录 / internal / botruntime / runtime.go
1 package botruntime
2
3 import (
4 "log/slog"
5 "path/filepath"
6 "strings"
7 "sync"
8 "time"
9
10 "reasonix/internal/bot"
11 "reasonix/internal/bot/feishu"
12 "reasonix/internal/bot/qq"
13 "reasonix/internal/bot/weixin"
14 "reasonix/internal/config"
15 )
16
17 // EnabledPlatforms resolves the requested channel list against the saved config.
18 // "lark" is a domain alias for the Feishu adapter platform.
19 func EnabledPlatforms(cfg *config.Config, channels []string) (map[bot.Platform]bool, []string) {
20 enabled := make(map[bot.Platform]bool)
21 var warnings []string
22 if len(channels) > 0 {
23 for _, ch := range channels {
24 ch = strings.TrimSpace(ch)
25 switch bot.Platform(ch) {
26 case bot.PlatformQQ:
27 enabled[bot.PlatformQQ] = PlatformConfigured(cfg, bot.PlatformQQ)
28 case bot.PlatformFeishu:
29 enabled[bot.PlatformFeishu] = PlatformConfigured(cfg, bot.PlatformFeishu)
30 case bot.PlatformWeixin:
31 enabled[bot.PlatformWeixin] = PlatformConfigured(cfg, bot.PlatformWeixin)
32 default:
33 if strings.EqualFold(ch, "lark") {
34 enabled[bot.PlatformFeishu] = PlatformConfigured(cfg, bot.PlatformFeishu)
35 } else if ch != "" {
36 warnings = append(warnings, ch)
37 }
38 }
39 }
40 return enabled, warnings
41 }
42 enabled[bot.PlatformQQ] = PlatformConfigured(cfg, bot.PlatformQQ)
43 enabled[bot.PlatformFeishu] = PlatformConfigured(cfg, bot.PlatformFeishu)
44 enabled[bot.PlatformWeixin] = PlatformConfigured(cfg, bot.PlatformWeixin)
45 return enabled, warnings
46 }
47
48 // RequestedFeishuDomains returns the Feishu-family domains the caller explicitly
49 // named ("feishu"/"lark"), or nil when neither was requested (no restriction).
50 func RequestedFeishuDomains(channels []string) map[string]bool {
51 domains := make(map[string]bool)
52 for _, ch := range channels {
53 switch {
54 case strings.EqualFold(strings.TrimSpace(ch), string(bot.PlatformFeishu)):
55 domains["feishu"] = true
56 case strings.EqualFold(strings.TrimSpace(ch), "lark"):
57 domains["lark"] = true
58 }
59 }
60 if len(domains) == 0 {
61 return nil
62 }
63 return domains
64 }
65
66 func feishuDomainKey(domain string) string {
67 if strings.EqualFold(strings.TrimSpace(domain), "lark") {
68 return "lark"
69 }
70 return "feishu"
71 }
72
73 func HasEnabledPlatform(enabled map[bot.Platform]bool) bool {
74 for _, value := range enabled {
75 if value {
76 return true
77 }
78 }
79 return false
80 }
81
82 func PlatformConfigured(cfg *config.Config, platform bot.Platform) bool {
83 if cfg == nil {
84 return false
85 }
86 switch platform {
87 case bot.PlatformQQ:
88 if cfg.Bot.QQ.Enabled {
89 return true
90 }
91 case bot.PlatformFeishu:
92 if cfg.Bot.Feishu.Enabled {
93 return true
94 }
95 case bot.PlatformWeixin:
96 if cfg.Bot.Weixin.Enabled {
97 return true
98 }
99 }
100 for _, conn := range cfg.Bot.Connections {
101 if conn.Enabled && bot.Platform(strings.TrimSpace(conn.Provider)) == platform {
102 return true
103 }
104 }
105 return false
106 }
107
108 func ChannelConfigs(connections []config.BotConnectionConfig, includeModel bool, includeWorkspaceRoot bool) map[bot.Platform]bot.ChannelConfig {
109 if len(connections) == 0 {
110 return nil
111 }
112 out := make(map[bot.Platform]bot.ChannelConfig)
113 for _, conn := range connections {
114 if !conn.Enabled {
115 continue
116 }
117 plat := bot.Platform(strings.TrimSpace(conn.Provider))
118 switch plat {
119 case bot.PlatformQQ, bot.PlatformFeishu, bot.PlatformWeixin:
120 default:
121 continue
122 }
123 channel := out[plat]
124 if includeModel {
125 channel.Model = strings.TrimSpace(conn.Model)
126 }
127 if includeWorkspaceRoot {
128 channel.WorkspaceRoot = strings.TrimSpace(conn.WorkspaceRoot)
129 }
130 if value := normalizeToolApprovalMode(conn.ToolApprovalMode); value != "" {
131 channel.ToolApprovalMode = value
132 }
133 if channel.Model != "" || channel.WorkspaceRoot != "" || channel.ToolApprovalMode != "" {
134 out[plat] = channel
135 }
136 }
137 if len(out) == 0 {
138 return nil
139 }
140 return out
141 }
142
143 func ConnectionChannelConfigs(connections []config.BotConnectionConfig, includeModel bool, includeWorkspaceRoot bool) map[string]bot.ChannelConfig {
144 if len(connections) == 0 {
145 return nil
146 }
147 out := make(map[string]bot.ChannelConfig)
148 for _, conn := range connections {
149 if !conn.Enabled {
150 continue
151 }
152 id := ConnectionRuntimeID(conn)
153 if id == "" {
154 continue
155 }
156 var channel bot.ChannelConfig
157 if includeModel {
158 channel.Model = strings.TrimSpace(conn.Model)
159 }
160 if includeWorkspaceRoot {
161 channel.WorkspaceRoot = strings.TrimSpace(conn.WorkspaceRoot)
162 channel.SessionMappings = botSessionMappings(conn.SessionMappings)
163 }
164 if value := normalizeToolApprovalMode(conn.ToolApprovalMode); value != "" {
165 channel.ToolApprovalMode = value
166 }
167 if channel.Model != "" || channel.WorkspaceRoot != "" || channel.ToolApprovalMode != "" || len(channel.SessionMappings) > 0 {
168 out[id] = channel
169 }
170 }
171 if len(out) == 0 {
172 return nil
173 }
174 return out
175 }
176
177 func ConnectionAccessConfigs(cfg *config.Config) map[string]bot.AccessConfig {
178 if cfg == nil {
179 return nil
180 }
181 out := make(map[string]bot.AccessConfig)
182 if BotAccessActive(cfg.Bot.QQ.Access) {
183 out[string(bot.PlatformQQ)] = botAccessConfig(cfg.Bot.QQ.Access)
184 }
185 for _, conn := range cfg.Bot.Connections {
186 if !conn.Enabled {
187 continue
188 }
189 id := ConnectionRuntimeID(conn)
190 if id == "" || !BotAccessActive(conn.Access) {
191 continue
192 }
193 out[id] = botAccessConfig(conn.Access)
194 }
195 if len(out) == 0 {
196 return nil
197 }
198 return out
199 }
200
201 func BotAccessActive(access config.BotAccessConfig) bool {
202 return access.Enabled ||
203 access.AllowAll ||
204 access.PairingEnabled ||
205 len(access.Users) > 0 ||
206 len(access.Groups) > 0 ||
207 len(access.Approvers) > 0 ||
208 len(access.Admins) > 0
209 }
210
211 func botAccessConfig(access config.BotAccessConfig) bot.AccessConfig {
212 return bot.AccessConfig{
213 Enabled: access.Enabled,
214 AllowAll: access.AllowAll,
215 PairingEnabled: access.PairingEnabled,
216 Users: trimStringSlice(access.Users),
217 Groups: trimStringSlice(access.Groups),
218 Approvers: trimStringSlice(access.Approvers),
219 Admins: trimStringSlice(access.Admins),
220 }
221 }
222
223 func trimStringSlice(values []string) []string {
224 if len(values) == 0 {
225 return nil
226 }
227 out := make([]string, 0, len(values))
228 for _, value := range values {
229 value = strings.TrimSpace(value)
230 if value != "" {
231 out = append(out, value)
232 }
233 }
234 return out
235 }
236
237 func botSessionMappings(mappings []config.BotConnectionSessionMapping) []bot.SessionMapping {
238 if len(mappings) == 0 {
239 return nil
240 }
241 out := make([]bot.SessionMapping, 0, len(mappings))
242 for _, mapping := range mappings {
243 out = append(out, bot.SessionMapping{
244 RemoteID: strings.TrimSpace(mapping.RemoteID),
245 SessionID: strings.TrimSpace(mapping.SessionID),
246 SessionSource: strings.TrimSpace(mapping.SessionSource),
247 ChatType: strings.TrimSpace(mapping.ChatType),
248 UserID: strings.TrimSpace(mapping.UserID),
249 ThreadID: strings.TrimSpace(mapping.ThreadID),
250 Scope: strings.TrimSpace(mapping.Scope),
251 WorkspaceRoot: strings.TrimSpace(mapping.WorkspaceRoot),
252 UpdatedAt: strings.TrimSpace(mapping.UpdatedAt),
253 })
254 }
255 return out
256 }
257
258 func RouteConfigs(routes []config.BotRouteConfig, includeModel bool, includeWorkspaceRoot bool) []bot.RouteConfig {
259 if len(routes) == 0 {
260 return nil
261 }
262 out := make([]bot.RouteConfig, 0, len(routes))
263 for _, route := range routes {
264 var channel bot.ChannelConfig
265 if includeModel {
266 channel.Model = strings.TrimSpace(route.Model)
267 }
268 if includeWorkspaceRoot {
269 channel.WorkspaceRoot = strings.TrimSpace(route.WorkspaceRoot)
270 }
271 if value := normalizeToolApprovalMode(route.ToolApprovalMode); value != "" {
272 channel.ToolApprovalMode = value
273 }
274 if channel.Model == "" && channel.WorkspaceRoot == "" && channel.ToolApprovalMode == "" {
275 continue
276 }
277 out = append(out, bot.RouteConfig{
278 ConnectionID: strings.TrimSpace(route.ConnectionID),
279 Platform: bot.Platform(strings.TrimSpace(route.Platform)),
280 ChatType: bot.ChatType(strings.TrimSpace(route.ChatType)),
281 ChatID: strings.TrimSpace(route.ChatID),
282 UserID: strings.TrimSpace(route.UserID),
283 ThreadID: strings.TrimSpace(route.ThreadID),
284 Channel: channel,
285 })
286 }
287 if len(out) == 0 {
288 return nil
289 }
290 return out
291 }
292
293 func normalizeToolApprovalMode(mode string) string {
294 switch strings.ToLower(strings.TrimSpace(mode)) {
295 case "ask":
296 return "ask"
297 case "auto":
298 return "auto"
299 case "yolo", "full", "full-access", "bypass":
300 return "yolo"
301 default:
302 return ""
303 }
304 }
305
306 func AdapterBindings(cfg *config.Config, enabled map[bot.Platform]bool, feishuDomains map[string]bool, logger *slog.Logger) []bot.AdapterBinding {
307 if cfg == nil {
308 return nil
309 }
310 var bindings []bot.AdapterBinding
311 hasConnection := make(map[bot.Platform]bool)
312 for _, conn := range cfg.Bot.Connections {
313 if !conn.Enabled {
314 continue
315 }
316 platform := bot.Platform(strings.TrimSpace(conn.Provider))
317 if !enabled[platform] {
318 continue
319 }
320 id := ConnectionRuntimeID(conn)
321 switch platform {
322 case bot.PlatformQQ:
323 qqCfg := cfg.Bot.QQ
324 qqCfg.Enabled = true
325 qqCfg.AppID = firstNonEmptyString(strings.TrimSpace(conn.Credential.AppID), qqCfg.AppID)
326 qqCfg.AppSecretEnv = firstNonEmptyString(strings.TrimSpace(conn.Credential.AppSecretEnv), qqCfg.AppSecretEnv)
327 bindings = append(bindings, bot.AdapterBinding{ID: id, Domain: strings.TrimSpace(conn.Domain), Platform: platform, Adapter: qq.New(qqCfg, logger)})
328 hasConnection[platform] = true
329 case bot.PlatformFeishu:
330 feishuCfg := cfg.Bot.Feishu
331 feishuCfg.Enabled = true
332 feishuCfg.Domain = firstNonEmptyString(strings.TrimSpace(conn.Domain), feishuCfg.Domain)
333 if feishuDomains != nil && !feishuDomains[feishuDomainKey(feishuCfg.Domain)] {
334 continue
335 }
336 feishuCfg.AppID = firstNonEmptyString(strings.TrimSpace(conn.Credential.AppID), feishuCfg.AppID)
337 feishuCfg.AppSecretEnv = firstNonEmptyString(strings.TrimSpace(conn.Credential.AppSecretEnv), feishuCfg.AppSecretEnv)
338 bindings = append(bindings, bot.AdapterBinding{ID: id, Domain: feishuCfg.Domain, Platform: platform, Adapter: feishu.New(feishuCfg, logger)})
339 hasConnection[platform] = true
340 case bot.PlatformWeixin:
341 weixinCfg := cfg.Bot.Weixin
342 weixinCfg.Enabled = true
343 weixinCfg.AccountID = firstNonEmptyString(strings.TrimSpace(conn.Credential.AccountID), weixinCfg.AccountID)
344 weixinCfg.TokenEnv = firstNonEmptyString(strings.TrimSpace(conn.Credential.TokenEnv), weixinCfg.TokenEnv)
345 bindings = append(bindings, bot.AdapterBinding{ID: id, Domain: strings.TrimSpace(conn.Domain), Platform: platform, Adapter: weixin.New(weixinCfg, logger)})
346 hasConnection[platform] = true
347 }
348 }
349 if enabled[bot.PlatformQQ] && !hasConnection[bot.PlatformQQ] {
350 bindings = append(bindings, bot.AdapterBinding{ID: string(bot.PlatformQQ), Platform: bot.PlatformQQ, Adapter: qq.New(cfg.Bot.QQ, logger)})
351 }
352 if enabled[bot.PlatformFeishu] && !hasConnection[bot.PlatformFeishu] {
353 if feishuDomains == nil || feishuDomains[feishuDomainKey(cfg.Bot.Feishu.Domain)] {
354 bindings = append(bindings, bot.AdapterBinding{ID: string(bot.PlatformFeishu), Domain: cfg.Bot.Feishu.Domain, Platform: bot.PlatformFeishu, Adapter: feishu.New(cfg.Bot.Feishu, logger)})
355 }
356 }
357 if enabled[bot.PlatformWeixin] && !hasConnection[bot.PlatformWeixin] {
358 bindings = append(bindings, bot.AdapterBinding{ID: string(bot.PlatformWeixin), Domain: "weixin", Platform: bot.PlatformWeixin, Adapter: weixin.New(cfg.Bot.Weixin, logger)})
359 }
360 return bindings
361 }
362
363 func ConnectionRuntimeID(conn config.BotConnectionConfig) string {
364 if id := strings.TrimSpace(conn.ID); id != "" {
365 return id
366 }
367 provider := strings.TrimSpace(conn.Provider)
368 domain := strings.TrimSpace(conn.Domain)
369 if provider == "" {
370 return ""
371 }
372 if domain == "" {
373 return provider
374 }
375 return provider + "-" + domain
376 }
377
378 func ModelName(cfg *config.Config, override string) string {
379 if strings.TrimSpace(override) != "" {
380 return strings.TrimSpace(override)
381 }
382 if cfg == nil {
383 return ""
384 }
385 if strings.TrimSpace(cfg.Bot.Model) != "" {
386 return strings.TrimSpace(cfg.Bot.Model)
387 }
388 return strings.TrimSpace(cfg.DefaultModel)
389 }
390
391 func AllowlistUserCount(a config.BotAllowlist) int {
392 return len(a.QQUsers) + len(a.FeishuUsers) + len(a.WeixinUsers) +
393 len(a.QQApprovers) + len(a.FeishuApprovers) + len(a.WeixinApprovers) +
394 len(a.QQAdmins) + len(a.FeishuAdmins) + len(a.WeixinAdmins)
395 }
396
397 func BotAccessUserCount(access config.BotAccessConfig) int {
398 return len(access.Users) + len(access.Groups) + len(access.Approvers) + len(access.Admins)
399 }
400
401 func BotConfigHasAccessControl(bc config.BotConfig) bool {
402 if bc.Allowlist.AllowAll || bc.Pairing.Enabled || (bc.Allowlist.Enabled && AllowlistUserCount(bc.Allowlist) > 0) {
403 return true
404 }
405 if BotAccessActive(bc.QQ.Access) {
406 return true
407 }
408 for _, conn := range bc.Connections {
409 if conn.Enabled && BotAccessActive(conn.Access) {
410 return true
411 }
412 }
413 return false
414 }
415
416 func NewRemoteRememberer(logger *slog.Logger) func(bot.InboundMessage) {
417 var mu sync.Mutex
418 seen := make(map[string]bool)
419 return func(msg bot.InboundMessage) {
420 remoteID := strings.TrimSpace(msg.ChatID)
421 if remoteID == "" {
422 return
423 }
424 key := strings.Join([]string{
425 string(msg.Platform),
426 strings.TrimSpace(msg.ConnectionID),
427 strings.TrimSpace(msg.Domain),
428 string(msg.ChatType),
429 remoteID,
430 strings.TrimSpace(msg.UserID),
431 }, "\x00")
432 mu.Lock()
433 if seen[key] {
434 mu.Unlock()
435 return
436 }
437 seen[key] = true
438 mu.Unlock()
439
440 if err := RememberInbound(msg); err != nil && logger != nil {
441 logger.Warn("remember bot remote failed", "platform", msg.Platform, "err", err)
442 }
443 }
444 }
445
446 func NewSessionRememberer(logger *slog.Logger) func(bot.InboundMessage, string) error {
447 return NewSessionRemembererWithWorkspace(logger, "")
448 }
449
450 func NewSessionRemembererWithWorkspace(logger *slog.Logger, workspaceRoot string) func(bot.InboundMessage, string) error {
451 return func(msg bot.InboundMessage, sessionID string) error {
452 if err := RememberInboundSessionWorkspace(msg, sessionID, workspaceRoot); err != nil {
453 if logger != nil {
454 logger.Warn("remember bot session failed", "platform", msg.Platform, "err", err)
455 }
456 return err
457 }
458 return nil
459 }
460 }
461
462 func RememberInbound(msg bot.InboundMessage) error {
463 return rememberInbound(msg, "", "")
464 }
465
466 func RememberInboundSession(msg bot.InboundMessage, sessionID string) error {
467 return RememberInboundSessionWorkspace(msg, sessionID, "")
468 }
469
470 func RememberInboundSessionWorkspace(msg bot.InboundMessage, sessionID string, workspaceRoot string) error {
471 return rememberInbound(msg, strings.TrimSpace(sessionID), strings.TrimSpace(workspaceRoot))
472 }
473
474 func ForgetAutoSessionMappingsForPath(sessionPath string) error {
475 target := normalizedBotSessionPath(sessionPath)
476 if target == "" {
477 return nil
478 }
479 userPath := config.UserConfigPath()
480 if strings.TrimSpace(userPath) == "" {
481 return nil
482 }
483 unlock := config.LockUserConfigEdits()
484 defer unlock()
485
486 cfg := config.LoadForEdit(userPath)
487 now := time.Now().UTC().Format(time.RFC3339)
488 changed := false
489 for i := range cfg.Bot.Connections {
490 conn := &cfg.Bot.Connections[i]
491 next := conn.SessionMappings[:0]
492 removed := false
493 for _, mapping := range conn.SessionMappings {
494 if strings.TrimSpace(mapping.SessionSource) == "auto" && normalizedBotSessionPath(mapping.SessionID) == target {
495 removed = true
496 continue
497 }
498 next = append(next, mapping)
499 }
500 if !removed {
501 continue
502 }
503 conn.SessionMappings = next
504 conn.UpdatedAt = now
505 changed = true
506 }
507 if !changed {
508 return nil
509 }
510 return cfg.SaveTo(userPath)
511 }
512
513 func rememberInbound(msg bot.InboundMessage, sessionID string, actualWorkspaceRoot string) error {
514 userPath := config.UserConfigPath()
515 platform := msg.Platform
516 remoteID := strings.TrimSpace(msg.ChatID)
517 if userPath == "" || remoteID == "" {
518 return nil
519 }
520 unlock := config.LockUserConfigEdits()
521 defer unlock()
522
523 cfg := config.LoadForEdit(userPath)
524 now := time.Now().UTC().Format(time.RFC3339)
525 changed := false
526 for i := range cfg.Bot.Connections {
527 conn := &cfg.Bot.Connections[i]
528 if strings.TrimSpace(conn.Provider) != string(platform) || !conn.Enabled || !connectionMatchesInbound(*conn, msg) {
529 continue
530 }
531 mappingIndex := -1
532 for j := range conn.SessionMappings {
533 if botSessionMappingMatches(conn.SessionMappings[j], msg) {
534 mappingIndex = j
535 break
536 }
537 }
538 if mappingIndex >= 0 {
539 if sessionID == "" {
540 continue
541 }
542 mapping := &conn.SessionMappings[mappingIndex]
543 current := strings.TrimSpace(mapping.SessionID)
544 if current == sessionID || botSessionMappingHasExplicitTarget(*mapping) {
545 continue
546 }
547 mapping.SessionID = sessionID
548 mapping.SessionSource = "auto"
549 mapping.UpdatedAt = now
550 conn.UpdatedAt = now
551 changed = true
552 continue
553 }
554 scope := "global"
555 workspaceRoot := ""
556 if strings.TrimSpace(conn.WorkspaceRoot) != "" {
557 scope = "project"
558 workspaceRoot = strings.TrimSpace(conn.WorkspaceRoot)
559 } else if actualWorkspaceRoot != "" {
560 scope = "project"
561 workspaceRoot = actualWorkspaceRoot
562 }
563 chatType, userID, threadID := botSessionMappingIdentity(msg)
564 conn.SessionMappings = append(conn.SessionMappings, config.BotConnectionSessionMapping{
565 RemoteID: remoteID,
566 SessionID: sessionID,
567 SessionSource: botSessionSource(sessionID),
568 ChatType: chatType,
569 UserID: userID,
570 ThreadID: threadID,
571 Scope: scope,
572 WorkspaceRoot: workspaceRoot,
573 UpdatedAt: now,
574 })
575 conn.UpdatedAt = now
576 changed = true
577 }
578 if rememberAllowlist(&cfg.Bot.Allowlist, platform, msg.UserID, remoteID, msg.ChatType) {
579 changed = true
580 }
581 if !changed {
582 return nil
583 }
584 return cfg.SaveTo(userPath)
585 }
586
587 func botSessionMappingMatches(mapping config.BotConnectionSessionMapping, msg bot.InboundMessage) bool {
588 if strings.TrimSpace(mapping.RemoteID) != strings.TrimSpace(msg.ChatID) {
589 return false
590 }
591 chatType, userID, threadID := botSessionMappingIdentity(msg)
592 mappingChatType := strings.TrimSpace(mapping.ChatType)
593 if mappingChatType == "" {
594 return chatType == ""
595 }
596 if mappingChatType != chatType {
597 return false
598 }
599 if strings.TrimSpace(mapping.UserID) != userID {
600 return false
601 }
602 return strings.TrimSpace(mapping.ThreadID) == threadID
603 }
604
605 func botSessionMappingIdentity(msg bot.InboundMessage) (chatType string, userID string, threadID string) {
606 switch msg.ChatType {
607 case bot.ChatGroup, bot.ChatGuild:
608 chatType = string(msg.ChatType)
609 userID = strings.TrimSpace(msg.UserID)
610 case bot.ChatThread:
611 chatType = string(msg.ChatType)
612 threadID = strings.TrimSpace(msg.ThreadID)
613 if threadID == "" {
614 threadID = strings.TrimSpace(msg.ChatID)
615 }
616 }
617 return chatType, userID, threadID
618 }
619
620 func botSessionMappingHasExplicitTarget(mapping config.BotConnectionSessionMapping) bool {
621 sessionID := strings.TrimSpace(mapping.SessionID)
622 if sessionID == "" || strings.TrimSpace(mapping.SessionSource) == "auto" {
623 return false
624 }
625 return true
626 }
627
628 func botSessionSource(sessionID string) string {
629 if strings.TrimSpace(sessionID) == "" {
630 return ""
631 }
632 return "auto"
633 }
634
635 func normalizedBotSessionPath(sessionID string) string {
636 sessionID = strings.TrimSpace(sessionID)
637 if sessionID == "" {
638 return ""
639 }
640 if strings.HasPrefix(strings.ToLower(sessionID), "path:") {
641 sessionID = strings.TrimSpace(sessionID[5:])
642 }
643 if sessionID == "" {
644 return ""
645 }
646 if !(strings.HasSuffix(sessionID, ".jsonl") || strings.Contains(sessionID, "/") || strings.Contains(sessionID, `\`) || strings.HasPrefix(sessionID, "~")) {
647 return ""
648 }
649 return filepath.Clean(sessionID)
650 }
651
652 func connectionMatchesInbound(conn config.BotConnectionConfig, msg bot.InboundMessage) bool {
653 if msg.ConnectionID != "" {
654 return ConnectionRuntimeID(conn) == strings.TrimSpace(msg.ConnectionID)
655 }
656 if msg.Domain != "" && strings.TrimSpace(conn.Domain) != "" {
657 return strings.EqualFold(strings.TrimSpace(conn.Domain), strings.TrimSpace(msg.Domain))
658 }
659 return true
660 }
661
662 func rememberAllowlist(allowlist *config.BotAllowlist, platform bot.Platform, userID string, chatID string, chatType bot.ChatType) bool {
663 if allowlist == nil {
664 return false
665 }
666 changed := false
667 userID = strings.TrimSpace(userID)
668 if userID != "" {
669 switch platform {
670 case bot.PlatformQQ:
671 allowlist.QQUsers, changed = appendUniqueString(allowlist.QQUsers, userID)
672 case bot.PlatformFeishu:
673 allowlist.FeishuUsers, changed = appendUniqueString(allowlist.FeishuUsers, userID)
674 case bot.PlatformWeixin:
675 allowlist.WeixinUsers, changed = appendUniqueString(allowlist.WeixinUsers, userID)
676 }
677 }
678 if !chatUsesGroupAllowlist(chatType) {
679 return changed
680 }
681 groupID := strings.TrimSpace(chatID)
682 if groupID == "" {
683 return changed
684 }
685 groupChanged := false
686 switch platform {
687 case bot.PlatformQQ:
688 allowlist.QQGroups, groupChanged = appendUniqueString(allowlist.QQGroups, groupID)
689 case bot.PlatformFeishu:
690 allowlist.FeishuGroups, groupChanged = appendUniqueString(allowlist.FeishuGroups, groupID)
691 case bot.PlatformWeixin:
692 allowlist.WeixinGroups, groupChanged = appendUniqueString(allowlist.WeixinGroups, groupID)
693 }
694 return changed || groupChanged
695 }
696
697 func appendUniqueString(values []string, next string) ([]string, bool) {
698 next = strings.TrimSpace(next)
699 if next == "" {
700 return values, false
701 }
702 for _, value := range values {
703 if strings.TrimSpace(value) == next {
704 return values, false
705 }
706 }
707 return append(values, next), true
708 }
709
710 func chatUsesGroupAllowlist(chatType bot.ChatType) bool {
711 switch chatType {
712 case bot.ChatGroup, bot.ChatGuild, bot.ChatThread:
713 return true
714 default:
715 return false
716 }
717 }
718
719 func firstNonEmptyString(vals ...string) string {
720 for _, val := range vals {
721 if strings.TrimSpace(val) != "" {
722 return val
723 }
724 }
725 return ""
726 }
727
727 lines GO