Files
DeepSeek-Reasonix/desktop/bot_connection_app.go
T
YHH 4fbbca6a99 refactor(go): adopt modern stdlib idioms and gate them (#7831)
* refactor(go): adopt modern stdlib idioms across kernel and desktop

Mechanical sweep with golangci-lint's modernize/intrange/copyloopvar/
usestdlibvars fixers plus hand-fixes for what they could not rewrite:
slices.Contains, min/max, slices.Backward, WaitGroup.Go, reflect.TypeFor,
maps.Copy, strings.Cut/CutPrefix/SplitSeq, strings.Builder, range-over-int.
Net -549 lines with no behavior change.

Three loops in session.go stay index-based: slices.Backward yields element
copies, so the fixer's rewrite silently dropped writes to DecisionReceipts
and ToolCalls (caught by TestDelivery* and TestDependentSameBatch*). All 49
slices.Backward sites were audited for that shape; only these three write
through the index, and they carry a nolint with the reason.

CaptureAfter's scan loop had an empty body and never ran any logic, so the
fixer could not express it. Drop it and the accessor it was the only caller
of. omitzero is deliberately untouched: turning omitempty into omitzero
changes the JSON written for nested structs, which is a wire-format change
to session files and the extension protocol.

* build(lint): gate the modern-idiom linters

Nothing stopped the codebase from drifting back to pre-1.22 idioms. Both
modules are now clean for modernize, intrange, copyloopvar, and
usestdlibvars, so enable them rather than leaving the sweep as a one-off.

omitzero stays disabled with the reason inline; usetesting, perfsprint, and
errorlint still have residue and land separately.
2026-08-07 15:30:33 +08:00

983 lines
33 KiB
Go

package main
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
"reasonix/internal/bot"
"reasonix/internal/bot/feishu"
"reasonix/internal/bot/weixin"
"reasonix/internal/botruntime"
"reasonix/internal/config"
)
type BotConnectionCredentialView struct {
AppID string `json:"appId"`
AppSecretEnv string `json:"appSecretEnv"`
AccountID string `json:"accountId"`
TokenEnv string `json:"tokenEnv"`
SecretSet bool `json:"secretSet"`
}
type BotConnectionSessionMappingView struct {
RemoteID string `json:"remoteId"`
SessionID string `json:"sessionId"`
SessionSource string `json:"sessionSource"`
ChatType string `json:"chatType"`
UserID string `json:"userId"`
ThreadID string `json:"threadId"`
Scope string `json:"scope"`
WorkspaceRoot string `json:"workspaceRoot"`
UpdatedAt string `json:"updatedAt"`
}
type BotConnectionView struct {
ID string `json:"id"`
Provider string `json:"provider"`
Domain string `json:"domain"`
Label string `json:"label"`
Enabled bool `json:"enabled"`
Status string `json:"status"`
Model string `json:"model"`
ToolApprovalMode string `json:"toolApprovalMode"`
WorkspaceRoot string `json:"workspaceRoot"`
Access BotAccessView `json:"access"`
Credential BotConnectionCredentialView `json:"credential"`
SessionMappings []BotConnectionSessionMappingView `json:"sessionMappings"`
LastError string `json:"lastError"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type BotInstallStartResult struct {
OK bool `json:"ok"`
Provider string `json:"provider"`
Domain string `json:"domain"`
InstallID string `json:"installId"`
URL string `json:"url"`
DeviceCode string `json:"deviceCode"`
UserCode string `json:"userCode"`
Interval int `json:"interval"`
ExpireIn int `json:"expireIn"`
Message string `json:"message"`
}
type BotInstallPollResult struct {
Done bool `json:"done"`
Connection BotConnectionView `json:"connection"`
Status string `json:"status"`
Message string `json:"message"`
Error string `json:"error"`
}
type BotConnectionDiagnostic struct {
ID string `json:"id"`
Label string `json:"label"`
Status string `json:"status"`
Message string `json:"message"`
MessageID string `json:"messageId"`
Phase string `json:"phase"`
Code string `json:"code"`
ReportKind string `json:"reportKind"`
ReportDetail string `json:"reportDetail"`
OccurredAt string `json:"occurredAt"`
}
type botInstallSession struct {
Provider string
Domain string
PollDomain string
DeviceCode string
UserCode string
StartedAt time.Time
ExpireAt time.Time
Weixin *weixin.LoginSession
}
func (a *App) StartBotConnectionInstall(provider, domain string) (BotInstallStartResult, error) {
provider, domain = normalizeBotInstallTarget(provider, domain)
if provider == "weixin" {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
session, err := weixin.StartLogin(ctx)
if err != nil {
return BotInstallStartResult{OK: false, Provider: provider, Domain: domain, Message: err.Error()}, nil
}
installID := randomInstallID()
a.mu.Lock()
if a.botInstalls == nil {
a.botInstalls = map[string]*botInstallSession{}
}
a.botInstalls[installID] = &botInstallSession{
Provider: provider,
Domain: domain,
DeviceCode: session.QRCode,
StartedAt: session.StartedAt,
ExpireAt: time.Now().Add(2 * time.Minute),
Weixin: session,
}
a.mu.Unlock()
return BotInstallStartResult{
OK: true, Provider: provider, Domain: domain, InstallID: installID, URL: firstNonEmptyBot(session.QRCodeURL, session.QRCode),
DeviceCode: session.QRCode, Interval: 3, ExpireIn: 120, Message: "请使用微信扫码完成连接。",
}, nil
}
if provider != "feishu" {
return BotInstallStartResult{OK: false, Provider: provider, Domain: domain, Message: "unsupported bot provider"}, nil
}
return a.startFeishuConnectionInstall(domain)
}
func (a *App) PollBotConnectionInstall(installID string) (BotInstallPollResult, error) {
installID = strings.TrimSpace(installID)
// Copy the session under a.mu: overlapping polls of the same install can
// race the locked PollDomain upgrade below with unlocked field reads.
a.mu.RLock()
sessionPtr := a.botInstalls[installID]
var sessionCopy botInstallSession
if sessionPtr != nil {
sessionCopy = *sessionPtr
}
a.mu.RUnlock()
if sessionPtr == nil {
return BotInstallPollResult{Error: "install session not found"}, nil
}
session := &sessionCopy
if time.Now().After(session.ExpireAt) {
a.deleteBotInstall(installID)
return BotInstallPollResult{Status: "expired", Error: "install session expired"}, nil
}
if session.Provider == "weixin" {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
result, status, err := weixin.PollLogin(ctx, session.Weixin)
if err != nil {
return BotInstallPollResult{Status: status, Error: err.Error()}, nil
}
if result == nil {
return BotInstallPollResult{Status: status, Message: weixinInstallStatusMessage(status)}, nil
}
a.deleteBotInstall(installID)
conn, err := a.upsertBotConnection(config.BotConnectionConfig{
ID: connectionID("weixin", "weixin"),
Provider: "weixin",
Domain: "weixin",
Label: "微信",
Enabled: true,
Status: "connected",
Access: botInstallAccess(result.UserID),
Credential: config.BotConnectionCredential{AccountID: result.AccountID, TokenEnv: "WEIXIN_BOT_TOKEN"},
}, func(c *config.Config) {
c.Bot.Enabled = true
c.Bot.Weixin.Enabled = true
c.Bot.Weixin.AccountID = result.AccountID
c.Bot.Weixin.APIBase = result.BaseURL
if c.Bot.Weixin.TokenEnv == "" {
c.Bot.Weixin.TokenEnv = "WEIXIN_BOT_TOKEN"
}
c.Bot.Allowlist.WeixinUsers = appendUniqueBotString(c.Bot.Allowlist.WeixinUsers, result.UserID)
})
if err != nil {
return BotInstallPollResult{Status: "error", Error: err.Error()}, nil
}
a.refreshBotRuntimeAsync()
return BotInstallPollResult{Done: true, Status: "connected", Connection: conn, Message: "微信已连接。"}, nil
}
return a.pollFeishuConnectionInstall(installID, session)
}
func (a *App) DiagnoseBotConnection(id string) (BotConnectionDiagnostic, error) {
cfg, err := a.loadDesktopBotConfig()
if err != nil {
return botConnectionDiagnostic(nil, id, "error", "config", "config_load_failed", err.Error(), true), nil
}
for _, conn := range cfg.Bot.Connections {
if conn.ID == id {
status := "ok"
message := "连接配置已保存。"
phase := "config"
code := "config_ok"
reportable := false
if !conn.Enabled {
status = "disabled"
message = "连接已保存但未启用。"
code = "connection_disabled"
} else if conn.Status != "connected" {
status = firstNonEmptyBot(conn.Status, "pending")
message = firstNonEmptyBot(conn.LastError, "连接还未完成。")
phase = "install"
code = "connection_not_connected"
reportable = status == "error" || strings.TrimSpace(conn.LastError) != ""
} else if conn.Credential.AppSecretEnv != "" && strings.TrimSpace(conn.Credential.AppSecretEnv) != "" && !envIsSet(conn.Credential.AppSecretEnv) {
status = "warning"
message = conn.Credential.AppSecretEnv + " 未设置。"
phase = "credential"
code = "secret_missing"
reportable = true
} else if conn.Credential.TokenEnv != "" && strings.TrimSpace(conn.Credential.TokenEnv) != "" && !botCredentialSecretSet(conn) {
status = "warning"
message = conn.Credential.TokenEnv + " 未设置,且未找到已保存的登录凭据。"
phase = "credential"
code = "secret_missing"
reportable = true
} else if conn.Provider == "weixin" && !botCredentialSecretSet(conn) {
status = "warning"
message = "未找到已保存的微信登录凭据。"
phase = "credential"
code = "secret_missing"
reportable = true
}
return botConnectionDiagnostic(&conn, conn.ID, status, phase, code, message, reportable), nil
}
}
return botConnectionDiagnostic(nil, id, "missing", "config", "connection_missing", "未找到连接。", true), nil
}
func (a *App) TestBotConnection(id, target string) (BotConnectionDiagnostic, error) {
cfg, err := a.loadDesktopBotConfig()
if err != nil {
return botConnectionDiagnostic(nil, id, "error", "config", "config_load_failed", err.Error(), true), nil
}
var conn *config.BotConnectionConfig
for i := range cfg.Bot.Connections {
if cfg.Bot.Connections[i].ID == strings.TrimSpace(id) {
conn = &cfg.Bot.Connections[i]
break
}
}
if conn == nil {
return botConnectionDiagnostic(nil, id, "missing", "config", "connection_missing", "未找到连接。", true), nil
}
target = firstNonEmptyBot(strings.TrimSpace(target), firstSessionRemoteID(conn.SessionMappings))
if conn.Provider != "feishu" && conn.Provider != "weixin" {
return botConnectionDiagnostic(conn, conn.ID, "warning", "send", "test_send_unsupported", "当前渠道暂不支持桌面端主动发送测试消息,可使用诊断检查基础配置。", false), nil
}
if target == "" {
return botConnectionDiagnostic(conn, conn.ID, "warning", "send", "test_target_missing", "请输入测试会话 ID 后再发送测试消息。", false), nil
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
var result bot.SendResult
switch conn.Provider {
case "feishu":
feishuCfg := cfg.Bot.Feishu
feishuCfg.Enabled = true
feishuCfg.Domain = firstNonEmptyBot(conn.Domain, feishuCfg.Domain)
feishuCfg.AppID = firstNonEmptyBot(conn.Credential.AppID, feishuCfg.AppID)
feishuCfg.AppSecretEnv = firstNonEmptyBot(conn.Credential.AppSecretEnv, feishuCfg.AppSecretEnv)
result, err = feishu.SendText(ctx, feishuCfg, target, "Reasonix bot 测试消息:连接和发送链路可用。")
case "weixin":
weixinCfg := cfg.Bot.Weixin
weixinCfg.Enabled = true
weixinCfg.AccountID = firstNonEmptyBot(conn.Credential.AccountID, weixinCfg.AccountID)
weixinCfg.TokenEnv = firstNonEmptyBot(conn.Credential.TokenEnv, weixinCfg.TokenEnv)
result, err = weixin.SendText(ctx, weixinCfg, target, "Reasonix bot 测试消息:连接和发送链路可用。")
}
if err != nil {
return botConnectionDiagnostic(conn, conn.ID, "error", "send", "test_send_failed", err.Error(), true), nil
}
_ = a.rememberBotConnectionRemote(conn.ID, target)
msg := "测试消息已发送。"
if result.MessageID != "" {
msg += " Message ID: " + result.MessageID
}
diag := botConnectionDiagnostic(conn, conn.ID, "ok", "send", "test_send_ok", msg, false)
diag.MessageID = result.MessageID
return diag, nil
}
func botConnectionDiagnostic(conn *config.BotConnectionConfig, id, status, phase, code, message string, reportable bool) BotConnectionDiagnostic {
id = strings.TrimSpace(id)
label := ""
if conn != nil {
id = firstNonEmptyBot(strings.TrimSpace(conn.ID), id)
label = strings.TrimSpace(conn.Label)
}
occurredAt := time.Now().UTC().Format(time.RFC3339)
diag := BotConnectionDiagnostic{
ID: id,
Label: label,
Status: strings.TrimSpace(status),
Message: strings.TrimSpace(message),
Phase: strings.TrimSpace(phase),
Code: strings.TrimSpace(code),
OccurredAt: occurredAt,
}
if reportable {
diag.ReportKind = "bot"
diag.ReportDetail = botConnectionReportDetail(conn, id, diag.Status, diag.Phase, diag.Code, diag.Message, occurredAt)
if diag.ReportDetail == "" {
diag.ReportKind = ""
}
}
return diag
}
func botConnectionReportDetail(conn *config.BotConnectionConfig, fallbackID, status, phase, code, message, occurredAt string) string {
provider := "unknown"
domain := "unknown"
configuredStatus := ""
enabled := false
workspaceScope := "global"
sessionMappings := 0
appIDSet := false
appSecretEnvConfigured := false
tokenEnvConfigured := false
secretAvailable := false
if conn != nil {
provider = firstNonEmptyBot(strings.TrimSpace(conn.Provider), provider)
domain = firstNonEmptyBot(strings.TrimSpace(conn.Domain), domain)
configuredStatus = strings.TrimSpace(conn.Status)
enabled = conn.Enabled
if strings.TrimSpace(conn.WorkspaceRoot) != "" {
workspaceScope = "project"
}
sessionMappings = len(conn.SessionMappings)
appIDSet = strings.TrimSpace(conn.Credential.AppID) != ""
appSecretEnvConfigured = strings.TrimSpace(conn.Credential.AppSecretEnv) != ""
tokenEnvConfigured = strings.TrimSpace(conn.Credential.TokenEnv) != ""
secretAvailable = botCredentialSecretSet(*conn)
}
summary := botConnectionReportSummary(code, message)
lines := []string{
"Bot connection diagnostic",
"",
"connection_id: " + safeBotReportValue(fallbackID),
"provider: " + safeBotReportValue(provider),
"domain: " + safeBotReportValue(domain),
"status: " + safeBotReportValue(status),
"phase: " + safeBotReportValue(phase),
"code: " + safeBotReportValue(code),
fmt.Sprintf("enabled: %t", enabled),
"configured_status: " + safeBotReportValue(configuredStatus),
fmt.Sprintf("app_id_set: %t", appIDSet),
fmt.Sprintf("app_secret_env_configured: %t", appSecretEnvConfigured),
fmt.Sprintf("token_env_configured: %t", tokenEnvConfigured),
fmt.Sprintf("secret_available: %t", secretAvailable),
"workspace_scope: " + workspaceScope,
fmt.Sprintf("session_mappings: %d", sessionMappings),
"",
"summary: " + summary,
}
payload := frontendCrashPayload{
SchemaVersion: 2,
Kind: "bot",
Source: "bot.runtime",
Label: botConnectionReportLabel(provider, domain, phase),
Message: strings.Join(lines, "\n"),
ErrorType: "BotConnectionDiagnostic",
ErrorMessage: summary,
TopFrame: "bot." + safeBotReportSegment(phase),
OccurredAt: occurredAt,
}
detail, err := json.Marshal(payload)
if err != nil {
return ""
}
return string(detail)
}
func botConnectionReportSummary(code, message string) string {
switch strings.TrimSpace(code) {
case "config_load_failed":
return "desktop bot config could not be loaded: " + scrubSensitiveText(message)
case "connection_missing":
return "bot connection record was not found"
case "connection_not_connected":
return "bot connection is not connected: " + scrubSensitiveText(message)
case "secret_missing":
return "required bot credential is not available"
case "test_send_failed":
return "bot test message failed: " + scrubSensitiveText(message)
default:
if strings.TrimSpace(message) == "" {
return strings.TrimSpace(code)
}
return scrubSensitiveText(message)
}
}
func botConnectionReportLabel(provider, domain, phase string) string {
parts := []string{"bot", safeBotReportSegment(provider), safeBotReportSegment(domain), safeBotReportSegment(phase)}
return strings.Trim(strings.Join(parts, "."), ".")
}
func safeBotReportSegment(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return "unknown"
}
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
b.WriteRune(r)
continue
}
if b.Len() == 0 || strings.HasSuffix(b.String(), ".") {
continue
}
b.WriteByte('.')
}
out := strings.Trim(b.String(), ".")
if out == "" {
return "unknown"
}
return out
}
func safeBotReportValue(s string) string {
s = safeBotReportSegment(s)
if len(s) > 80 {
return s[:80]
}
return s
}
func (a *App) startFeishuConnectionInstall(domain string) (BotInstallStartResult, error) {
// The official registration SDK always begins on the Feishu accounts domain.
// Lark tenants are detected from the first poll response, then polling moves
// to the Lark accounts domain for the final credential exchange.
beginDomain := "feishu"
data, err := postFeishuInstallForm(feishuAccountsBase(beginDomain), map[string]string{
"action": "begin", "archetype": "PersonalAgent", "auth_method": "client_secret", "request_user_info": "open_id",
})
if err != nil {
return BotInstallStartResult{OK: false, Provider: "feishu", Domain: domain, Message: err.Error()}, nil
}
deviceCode := stringValue(data["device_code"])
verifyURL := stringValue(data["verification_uri_complete"])
userCode := stringValue(data["user_code"])
if deviceCode == "" || verifyURL == "" {
return BotInstallStartResult{OK: false, Provider: "feishu", Domain: domain, Message: "飞书/Lark 授权响应缺少 device_code 或二维码 URL。"}, nil
}
qrURL, err := feishuRegistrationQRCodeURL(verifyURL)
if err != nil {
return BotInstallStartResult{OK: false, Provider: "feishu", Domain: domain, Message: err.Error()}, nil
}
installID := randomInstallID()
interval := intValue(data["interval"], 5)
expireIn := intValue(firstAny(data["expire_in"], data["expires_in"]), 300)
a.mu.Lock()
if a.botInstalls == nil {
a.botInstalls = map[string]*botInstallSession{}
}
a.botInstalls[installID] = &botInstallSession{
Provider: "feishu", Domain: domain, PollDomain: beginDomain, DeviceCode: deviceCode, UserCode: userCode,
StartedAt: time.Now(), ExpireAt: time.Now().Add(time.Duration(expireIn) * time.Second),
}
a.mu.Unlock()
return BotInstallStartResult{OK: true, Provider: "feishu", Domain: domain, InstallID: installID, URL: qrURL, DeviceCode: deviceCode, UserCode: userCode, Interval: interval, ExpireIn: expireIn}, nil
}
func (a *App) pollFeishuConnectionInstall(installID string, session *botInstallSession) (BotInstallPollResult, error) {
pollDomain := firstNonEmptyBot(session.PollDomain, session.Domain, "feishu")
data, statusCode, err := postFeishuInstallFormResult(feishuAccountsBase(pollDomain), map[string]string{"action": "poll", "device_code": session.DeviceCode})
if err != nil {
return BotInstallPollResult{Status: "error", Error: err.Error()}, nil
}
if errText := stringValue(data["error"]); errText != "" {
if errText == "authorization_pending" || errText == "slow_down" {
return BotInstallPollResult{Status: "pending", Message: "等待扫码授权。"}, nil
}
a.deleteBotInstall(installID)
return BotInstallPollResult{Status: "error", Error: firstNonEmptyBot(stringValue(data["error_description"]), errText)}, nil
}
if statusCode >= 400 {
a.deleteBotInstall(installID)
return BotInstallPollResult{Status: "error", Error: fmt.Sprintf("HTTP %d", statusCode)}, nil
}
if feishuInstallDomain(session.Domain, data) == "lark" && pollDomain != "lark" {
a.mu.Lock()
if current := a.botInstalls[installID]; current != nil {
current.PollDomain = "lark"
}
a.mu.Unlock()
return BotInstallPollResult{Status: "pending", Message: "已识别为 Lark 授权,继续等待授权完成。"}, nil
}
appID := stringValue(data["client_id"])
appSecret := stringValue(data["client_secret"])
if appID == "" || appSecret == "" {
return BotInstallPollResult{Status: "pending", Message: "等待授权完成。"}, nil
}
a.deleteBotInstall(installID)
domain := feishuInstallDomain(firstNonEmptyBot(pollDomain, session.Domain), data)
userID := feishuInstallUserID(data)
secretEnv := "FEISHU_BOT_APP_SECRET"
if domain == "lark" {
secretEnv = "LARK_BOT_APP_SECRET"
}
if err := upsertDotEnv(secretEnv, appSecret); err != nil {
return BotInstallPollResult{Status: "error", Error: err.Error()}, nil
}
label := "飞书"
if domain == "lark" {
label = "Lark"
}
conn, err := a.upsertBotConnection(config.BotConnectionConfig{
ID: connectionID("feishu", domain),
Provider: "feishu",
Domain: domain,
Label: label,
Enabled: true,
Status: "connected",
Access: botInstallAccess(userID),
Credential: config.BotConnectionCredential{AppID: appID, AppSecretEnv: secretEnv},
}, func(c *config.Config) {
c.Bot.Enabled = true
c.Bot.Feishu.Enabled = true
c.Bot.Feishu.Domain = domain
c.Bot.Feishu.AppID = appID
c.Bot.Feishu.AppSecretEnv = secretEnv
c.Bot.Feishu.Mode = "websocket"
c.Bot.Feishu.RequireMention = true
c.Bot.Allowlist.FeishuUsers = appendUniqueBotString(c.Bot.Allowlist.FeishuUsers, userID)
})
if err != nil {
return BotInstallPollResult{Status: "error", Error: err.Error()}, nil
}
a.refreshBotRuntimeAsync()
return BotInstallPollResult{Done: true, Status: "connected", Connection: conn, Message: label + " 已连接。"}, nil
}
func (a *App) upsertBotConnection(conn config.BotConnectionConfig, updateLegacy func(*config.Config)) (BotConnectionView, error) {
now := time.Now().UTC().Format(time.RFC3339)
if conn.CreatedAt == "" {
conn.CreatedAt = now
}
conn.UpdatedAt = now
if conn.Status == "" {
conn.Status = "connected"
}
if normalizeBotConnectionToolApprovalMode(conn.ToolApprovalMode) == "" {
conn.ToolApprovalMode = "ask"
}
if conn.ID == "" {
conn.ID = connectionID(conn.Provider, conn.Domain)
}
err := a.applyConfigOnly(func(c *config.Config) error {
if updateLegacy != nil {
updateLegacy(c)
}
replaced := false
for i, existing := range c.Bot.Connections {
if existing.ID == conn.ID {
conn.CreatedAt = firstNonEmptyBot(existing.CreatedAt, conn.CreatedAt)
if !botruntime.BotAccessActive(conn.Access) && botruntime.BotAccessActive(existing.Access) {
conn.Access = existing.Access
}
c.Bot.Connections[i] = conn
replaced = true
break
}
}
if !replaced {
c.Bot.Connections = append(c.Bot.Connections, conn)
}
return nil
})
return botConnectionView(conn), err
}
func (a *App) rememberBotConnectionRemote(id, remoteID string) error {
id = strings.TrimSpace(id)
remoteID = strings.TrimSpace(remoteID)
if id == "" || remoteID == "" {
return nil
}
now := time.Now().UTC().Format(time.RFC3339)
return a.applyConfigOnly(func(c *config.Config) error {
for i := range c.Bot.Connections {
if c.Bot.Connections[i].ID != id {
continue
}
for j := range c.Bot.Connections[i].SessionMappings {
if c.Bot.Connections[i].SessionMappings[j].RemoteID == remoteID {
workspaceRoot := firstNonEmptyBot(c.Bot.Connections[i].SessionMappings[j].WorkspaceRoot, c.Bot.Connections[i].WorkspaceRoot)
scope := botMappingScope(c.Bot.Connections[i].SessionMappings[j].Scope, workspaceRoot)
c.Bot.Connections[i].SessionMappings[j].Scope = scope
c.Bot.Connections[i].SessionMappings[j].WorkspaceRoot = botMappingWorkspaceRoot(scope, workspaceRoot)
c.Bot.Connections[i].SessionMappings[j].UpdatedAt = now
c.Bot.Connections[i].UpdatedAt = now
return nil
}
}
scope := botMappingScope("", c.Bot.Connections[i].WorkspaceRoot)
c.Bot.Connections[i].SessionMappings = append(c.Bot.Connections[i].SessionMappings, config.BotConnectionSessionMapping{
RemoteID: remoteID,
SessionID: "",
Scope: scope,
WorkspaceRoot: botMappingWorkspaceRoot(scope, c.Bot.Connections[i].WorkspaceRoot),
UpdatedAt: now,
})
c.Bot.Connections[i].UpdatedAt = now
return nil
}
return nil
})
}
func firstSessionRemoteID(mappings []config.BotConnectionSessionMapping) string {
for _, mapping := range mappings {
if strings.TrimSpace(mapping.RemoteID) != "" {
return strings.TrimSpace(mapping.RemoteID)
}
}
return ""
}
func (a *App) deleteBotInstall(installID string) {
a.mu.Lock()
delete(a.botInstalls, installID)
a.mu.Unlock()
}
func normalizeBotInstallTarget(provider, domain string) (string, string) {
provider = strings.ToLower(strings.TrimSpace(provider))
domain = strings.ToLower(strings.TrimSpace(domain))
if provider == "lark" {
provider = "feishu"
domain = "lark"
}
if provider == "weixin" || provider == "wechat" {
return "weixin", "weixin"
}
if domain != "lark" {
domain = "feishu"
}
return "feishu", domain
}
func feishuAccountsBase(domain string) string {
if domain == "lark" {
return "https://accounts.larksuite.com"
}
return "https://accounts.feishu.cn"
}
func feishuRegistrationQRCodeURL(rawURL string) (string, error) {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "", err
}
query := parsedURL.Query()
query.Set("from", "sdk")
query.Set("tp", "sdk")
query.Set("source", "go-sdk")
parsedURL.RawQuery = query.Encode()
return parsedURL.String(), nil
}
func postFeishuInstallForm(base string, body map[string]string) (map[string]any, error) {
data, status, err := postFeishuInstallFormResult(base, body)
if err != nil {
return nil, err
}
if status >= 400 {
return nil, fmt.Errorf("HTTP %d: %s", status, firstNonEmptyBot(stringValue(data["error_description"]), stringValue(data["message"])))
}
return data, nil
}
func postFeishuInstallFormResult(base string, body map[string]string) (map[string]any, int, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
reqBody := url.Values{}
for k, v := range body {
reqBody.Set(k, v)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(base, "/")+"/oauth/v1/app/registration", strings.NewReader(reqBody.Encode()))
if err != nil {
return nil, 0, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
var out map[string]any
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, resp.StatusCode, err
}
return out, resp.StatusCode, nil
}
func botConnectionView(conn config.BotConnectionConfig) BotConnectionView {
return BotConnectionView{
ID: conn.ID, Provider: conn.Provider, Domain: conn.Domain, Label: conn.Label, Enabled: conn.Enabled, Status: conn.Status,
Model: conn.Model, ToolApprovalMode: normalizeBotConnectionToolApprovalMode(conn.ToolApprovalMode), WorkspaceRoot: conn.WorkspaceRoot,
Access: botAccessViewFromConfig(conn.Access),
Credential: BotConnectionCredentialView{
AppID: conn.Credential.AppID, AppSecretEnv: conn.Credential.AppSecretEnv, AccountID: conn.Credential.AccountID, TokenEnv: conn.Credential.TokenEnv,
SecretSet: botCredentialSecretSet(conn),
},
SessionMappings: botSessionMappingViews(conn.SessionMappings, conn.WorkspaceRoot),
LastError: conn.LastError, CreatedAt: conn.CreatedAt, UpdatedAt: conn.UpdatedAt,
}
}
func botCredentialSecretSet(conn config.BotConnectionConfig) bool {
if conn.Credential.AppSecretEnv != "" {
return envIsSet(conn.Credential.AppSecretEnv)
}
if conn.Credential.TokenEnv != "" && envIsSet(conn.Credential.TokenEnv) {
return true
}
if conn.Provider == "weixin" {
return weixin.HasSavedAccount(conn.Credential.AccountID)
}
return false
}
func feishuInstallDomain(fallback string, data map[string]any) string {
if userInfo, ok := data["user_info"].(map[string]any); ok {
if strings.EqualFold(stringValue(userInfo["tenant_brand"]), "lark") {
return "lark"
}
return "feishu"
}
if strings.EqualFold(fallback, "lark") {
return "lark"
}
return "feishu"
}
func feishuInstallUserID(data map[string]any) string {
if userInfo, ok := data["user_info"].(map[string]any); ok {
return firstNonEmptyBot(
stringValue(userInfo["open_id"]),
stringValue(userInfo["union_id"]),
stringValue(userInfo["user_id"]),
)
}
return ""
}
func botConnectionViews(connections []config.BotConnectionConfig) []BotConnectionView {
if connections == nil {
return []BotConnectionView{}
}
out := make([]BotConnectionView, 0, len(connections))
for _, conn := range connections {
out = append(out, botConnectionView(conn))
}
return out
}
func botConnectionConfig(view BotConnectionView) config.BotConnectionConfig {
return config.BotConnectionConfig{
ID: strings.TrimSpace(view.ID),
Provider: strings.TrimSpace(view.Provider),
Domain: strings.TrimSpace(view.Domain),
Label: strings.TrimSpace(view.Label),
Enabled: view.Enabled,
Status: strings.TrimSpace(view.Status),
Model: strings.TrimSpace(view.Model),
ToolApprovalMode: firstNonEmptyBot(normalizeBotConnectionToolApprovalMode(view.ToolApprovalMode), "ask"),
WorkspaceRoot: strings.TrimSpace(view.WorkspaceRoot),
Access: botAccessConfigFromView(view.Access),
Credential: config.BotConnectionCredential{
AppID: strings.TrimSpace(view.Credential.AppID),
AppSecretEnv: strings.TrimSpace(view.Credential.AppSecretEnv),
AccountID: strings.TrimSpace(view.Credential.AccountID),
TokenEnv: strings.TrimSpace(view.Credential.TokenEnv),
},
SessionMappings: botSessionMappingConfigs(view.SessionMappings, view.WorkspaceRoot),
LastError: strings.TrimSpace(view.LastError),
CreatedAt: strings.TrimSpace(view.CreatedAt),
UpdatedAt: strings.TrimSpace(view.UpdatedAt),
}
}
func normalizeBotConnectionToolApprovalMode(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "ask":
return "ask"
case "auto":
return "auto"
case "yolo", "full", "full-access", "bypass":
return "yolo"
default:
return ""
}
}
func botConnectionConfigs(views []BotConnectionView) []config.BotConnectionConfig {
if views == nil {
return nil
}
out := make([]config.BotConnectionConfig, 0, len(views))
for _, view := range views {
cfg := botConnectionConfig(view)
if cfg.ID == "" || cfg.Provider == "" {
continue
}
out = append(out, cfg)
}
return out
}
func botMappingScope(scope, workspaceRoot string) string {
if strings.TrimSpace(scope) == "project" {
return "project"
}
if strings.TrimSpace(workspaceRoot) != "" {
return "project"
}
return "global"
}
func botMappingWorkspaceRoot(scope, workspaceRoot string) string {
if botMappingScope(scope, workspaceRoot) != "project" {
return ""
}
return strings.TrimSpace(workspaceRoot)
}
func botSessionMappingViews(mappings []config.BotConnectionSessionMapping, connectionWorkspaceRoot string) []BotConnectionSessionMappingView {
if mappings == nil {
return []BotConnectionSessionMappingView{}
}
out := make([]BotConnectionSessionMappingView, 0, len(mappings))
for _, m := range mappings {
workspaceRoot := firstNonEmptyBot(m.WorkspaceRoot, connectionWorkspaceRoot)
scope := botMappingScope(m.Scope, workspaceRoot)
out = append(out, BotConnectionSessionMappingView{
RemoteID: m.RemoteID,
SessionID: m.SessionID,
SessionSource: m.SessionSource,
ChatType: m.ChatType,
UserID: m.UserID,
ThreadID: m.ThreadID,
Scope: scope,
WorkspaceRoot: botMappingWorkspaceRoot(scope, workspaceRoot),
UpdatedAt: m.UpdatedAt,
})
}
return out
}
func botSessionMappingConfigs(mappings []BotConnectionSessionMappingView, connectionWorkspaceRoot string) []config.BotConnectionSessionMapping {
if mappings == nil {
return nil
}
out := make([]config.BotConnectionSessionMapping, 0, len(mappings))
for _, m := range mappings {
workspaceRoot := firstNonEmptyBot(m.WorkspaceRoot, connectionWorkspaceRoot)
scope := botMappingScope(m.Scope, workspaceRoot)
out = append(out, config.BotConnectionSessionMapping{
RemoteID: strings.TrimSpace(m.RemoteID),
SessionID: strings.TrimSpace(m.SessionID),
SessionSource: strings.TrimSpace(m.SessionSource),
ChatType: strings.TrimSpace(m.ChatType),
UserID: strings.TrimSpace(m.UserID),
ThreadID: strings.TrimSpace(m.ThreadID),
Scope: scope,
WorkspaceRoot: botMappingWorkspaceRoot(scope, workspaceRoot),
UpdatedAt: strings.TrimSpace(m.UpdatedAt),
})
}
return out
}
func connectionID(provider, domain string) string {
return strings.Trim(strings.ToLower(provider+"-"+domain), "-")
}
func botInstallAccess(userID string) config.BotAccessConfig {
userID = strings.TrimSpace(userID)
access := config.BotAccessConfig{Enabled: true, PairingEnabled: true}
if userID != "" {
access.Users = []string{userID}
}
return access
}
func randomInstallID() string {
var b [12]byte
if _, err := rand.Read(b[:]); err != nil {
return fmt.Sprintf("install-%d", time.Now().UnixNano())
}
return hex.EncodeToString(b[:])
}
func envIsSet(name string) bool {
return strings.TrimSpace(name) != "" && strings.TrimSpace(os.Getenv(name)) != ""
}
func firstAny(values ...any) any {
for _, value := range values {
if value != nil {
return value
}
}
return nil
}
func firstNonEmptyBot(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func appendUniqueBotString(values []string, next string) []string {
next = strings.TrimSpace(next)
if next == "" {
return values
}
for _, value := range values {
if strings.TrimSpace(value) == next {
return values
}
}
return append(values, next)
}
func stringValue(value any) string {
if value == nil {
return ""
}
return strings.TrimSpace(fmt.Sprint(value))
}
func intValue(value any, fallback int) int {
switch v := value.(type) {
case float64:
if v > 0 {
return int(v)
}
case int:
if v > 0 {
return v
}
case string:
var n int
if _, err := fmt.Sscanf(v, "%d", &n); err == nil && n > 0 {
return n
}
}
return fallback
}
func weixinInstallStatusMessage(status string) string {
switch status {
case "scaned":
return "已扫码,请在微信里确认。"
case "scaned_but_redirect":
return "已扫码,正在切换微信授权节点。"
default:
return "等待扫码。"
}
}