Files
qiweimanager-master/helper/client_state.go

1055 lines
29 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
"time"
)
const (
clientStatusConnected = "connected"
clientStatusIdentifying = "identifying"
clientStatusIdentified = "identified"
clientStatusMessageReady = "message_ready"
clientStatusUnidentified = "unidentified"
injectionStatusNotStarted = "not_started"
injectionStatusInjecting = "injecting"
injectionStatusConnected = "connected"
injectionStatusIdentified = "identified"
injectionStatusFailed = "failed"
)
type ClientRuntimeState struct {
ClientID uint32 `json:"clientId"`
PID uint32 `json:"pid"`
UserID string `json:"userId"`
Status string `json:"status"`
LastEvent string `json:"lastEvent"`
LastError string `json:"lastError"`
ConnectedAt string `json:"connectedAt"`
IdentifiedAt string `json:"identifiedAt"`
LastSeenAt string `json:"lastSeenAt"`
LastIdentifyRequestAt string `json:"lastIdentifyRequestAt"`
LastIdentifyResponseType string `json:"lastIdentifyResponseType"`
IgnoredIdentifyEvents int `json:"ignoredIdentifyEvents"`
LastIgnoredIdentifyReason string `json:"lastIgnoredIdentifyReason"`
LastIgnoredIdentifyEvent string `json:"lastIgnoredIdentifyEvent"`
FirstMessageAt string `json:"firstMessageAt"`
LastMessageAt string `json:"lastMessageAt"`
MessageCount int `json:"messageCount"`
}
type ClientProbeRecord struct {
Time string `json:"time"`
ClientID int32 `json:"clientId"`
Type string `json:"type"`
Summary string `json:"summary"`
Raw interface{} `json:"raw,omitempty"`
}
type ClientProbeResult struct {
ClientID int32 `json:"clientId"`
Started string `json:"started"`
Duration int `json:"durationSeconds"`
Records []ClientProbeRecord `json:"records"`
Message string `json:"message"`
}
var (
clientStateMu sync.Mutex
clientStates = make(map[uint32]*ClientRuntimeState)
clientProbeMu sync.Mutex
clientProbes = make(map[int32][]chan ClientProbeRecord)
injectionMu sync.Mutex
injectionStatus = injectionStatusNotStarted
injectionLastPID uint32
injectionLastError string
injectionLastUpdate string
injectedPIDs = make(map[uint32]time.Time)
)
func nowText() string {
return time.Now().Format("2006-01-02 15:04:05")
}
func registerClientConnected(clientID uint32) {
if clientID == 0 {
return
}
clientStateMu.Lock()
state := ensureClientStateLocked(clientID)
if state.Status == "" {
state.Status = clientStatusConnected
}
state.LastEvent = "connect"
state.LastSeenAt = nowText()
clientStateMu.Unlock()
clientIdMutex.Lock()
if _, exists := globalClientMap[clientID]; !exists {
globalClientMap[clientID] = ""
}
clientIdMutex.Unlock()
}
func registerClientPID(clientID uint32, pid uint32) {
if clientID == 0 {
return
}
clientStateMu.Lock()
if pid != 0 {
for otherClientID, state := range clientStates {
if otherClientID != clientID && state.PID == pid {
delete(clientStates, otherClientID)
clientIdMutex.Lock()
delete(globalClientMap, otherClientID)
clientIdMutex.Unlock()
}
}
}
state := ensureClientStateLocked(clientID)
state.PID = pid
if state.Status == "" || state.Status == clientStatusConnected {
state.Status = clientStatusConnected
}
state.LastEvent = "11024_connect_event"
state.LastSeenAt = nowText()
clientStateMu.Unlock()
}
func startClientIdentification(clientID uint32) {
if clientID == 0 {
return
}
if !supportsAccountInfoRequest() {
markClientError(clientID, clientStatusUnidentified, "account info request disabled for this WXWork/DLL version; bind client manually")
return
}
clientStateMu.Lock()
state := ensureClientStateLocked(clientID)
if state.Status == clientStatusIdentified || state.Status == clientStatusIdentifying {
clientStateMu.Unlock()
return
}
if state.Status != clientStatusMessageReady {
state.Status = clientStatusIdentifying
}
state.LastError = ""
state.LastEvent = "account_identify_started"
state.LastSeenAt = nowText()
clientStateMu.Unlock()
go identifyClientAccountV2(clientID)
}
func identifyClientAccount(clientID uint32) {
if _, exists := GetResponseChannel(int32(clientID)); exists {
markClientError(clientID, clientStatusUnidentified, "账号识别失败:响应通道占用")
return
}
responseChan := make(chan ClientResponseData, 1)
defer close(responseChan)
defer RemoveResponseChannel(int32(clientID))
SetResponseChannel(int32(clientID), responseChan)
request := `{"type":11035,"data":{}}`
_, err := handleSendWxWorkData(map[string]interface{}{
"data": request,
"clientId": clientID,
})
if err != nil {
markClientError(clientID, clientStatusUnidentified, "账号信息请求失败: "+err.Error())
return
}
select {
case response := <-responseChan:
userID, accountData := extractAccountIdentity(response.Data)
if userID != "" {
markClientIdentified(clientID, userID, accountData)
return
}
if existingUserID := getClientUserID(clientID); existingUserID != "" {
return
}
markClientError(clientID, clientStatusUnidentified, "账号信息响应缺少user_id")
case <-time.After(10 * time.Second):
if getClientUserID(clientID) == "" && !isClientUsable(clientID) {
markClientError(clientID, clientStatusUnidentified, "账号信息请求超时未收到11026/11179")
}
}
}
func retryClientIdentification(clientID uint32) {
if clientID == 0 {
return
}
if !supportsAccountInfoRequest() {
markClientError(clientID, clientStatusUnidentified, "account info request disabled for this WXWork/DLL version; bind client manually")
return
}
clientStateMu.Lock()
state := ensureClientStateLocked(clientID)
if state.Status == clientStatusIdentified || state.Status == clientStatusIdentifying {
clientStateMu.Unlock()
return
}
if state.Status != clientStatusMessageReady {
state.Status = clientStatusIdentifying
}
state.LastError = ""
state.LastEvent = "account_identify_retry"
state.LastSeenAt = nowText()
clientStateMu.Unlock()
go identifyClientAccountV2(clientID)
}
func identifyClientAccountV2(clientID uint32) {
if _, exists := GetResponseChannel(int32(clientID)); exists {
markClientError(clientID, clientStatusUnidentified, "account identify failed: response channel is busy")
return
}
responseChan := make(chan ClientResponseData, 1)
defer close(responseChan)
defer RemoveResponseChannel(int32(clientID))
SetResponseChannel(int32(clientID), responseChan)
request := `{"type":11035,"data":{}}`
markClientIdentifyRequest(clientID)
_, err := handleSendWxWorkData(map[string]interface{}{
"data": request,
"clientId": clientID,
})
if err != nil {
markClientError(clientID, clientStatusUnidentified, "account info request failed: "+err.Error())
return
}
timeout := time.After(10 * time.Second)
for {
if existingUserID := getClientUserID(clientID); existingUserID != "" {
return
}
select {
case response := <-responseChan:
markClientIdentifyResponse(clientID, response.Data)
if shouldIgnoreIdentifyResponse(response.Data) {
markClientIdentifyIgnored(clientID, response.Data, "non-account event")
continue
}
userID, accountData := extractAccountIdentity(response.Data)
if userID != "" {
markClientIdentified(clientID, userID, accountData)
return
}
if existingUserID := getClientUserID(clientID); existingUserID != "" {
return
}
markClientIdentifyIgnored(clientID, response.Data, "missing account identity")
case <-timeout:
if getClientUserID(clientID) == "" && !isClientUsable(clientID) {
markClientError(clientID, clientStatusUnidentified, "account identify timeout: no account info response")
}
return
}
}
}
func shouldIgnoreIdentifyResponse(data map[string]interface{}) bool {
switch responseTypeString(data) {
case "11024", "10002", "11041", "11042", "11043", "11044", "11045", "11046", "11047", "20002", "20003", "20004", "20005", "20012", "20014":
return true
default:
return false
}
}
func markClientIdentifyRequest(clientID uint32) {
clientStateMu.Lock()
state := ensureClientStateLocked(clientID)
state.LastIdentifyRequestAt = nowText()
state.LastIdentifyResponseType = ""
state.IgnoredIdentifyEvents = 0
state.LastIgnoredIdentifyReason = ""
state.LastIgnoredIdentifyEvent = ""
state.LastEvent = "account_identify_requested"
state.LastSeenAt = state.LastIdentifyRequestAt
clientStateMu.Unlock()
}
func markClientIdentifyResponse(clientID uint32, data map[string]interface{}) {
clientStateMu.Lock()
state, exists := clientStates[clientID]
if !exists {
clientStateMu.Unlock()
return
}
state.LastIdentifyResponseType = responseTypeString(data)
state.LastSeenAt = nowText()
clientStateMu.Unlock()
}
func markClientIdentifyIgnored(clientID uint32, data map[string]interface{}, reason string) {
clientStateMu.Lock()
state, exists := clientStates[clientID]
if !exists {
clientStateMu.Unlock()
return
}
state.IgnoredIdentifyEvents++
state.LastIgnoredIdentifyReason = reason
state.LastIgnoredIdentifyEvent = summarizeIdentifyEvent(data)
state.LastSeenAt = nowText()
clientStateMu.Unlock()
}
func markClientIdentified(clientID uint32, userID string, accountData map[string]interface{}) {
markClientIdentifiedWithOptions(clientID, userID, accountData, true)
}
func markClientManuallyBound(clientID uint32, userID string, accountData map[string]interface{}) {
markClientIdentifiedWithOptions(clientID, userID, accountData, false)
}
func markClientIdentifiedWithOptions(clientID uint32, userID string, accountData map[string]interface{}, refreshIdentity bool) {
userID = strings.TrimSpace(userID)
if clientID == 0 || userID == "" {
return
}
if accountData == nil {
accountData = make(map[string]interface{})
}
accountData["user_id"] = userID
accountData["client_id"] = clientID
clientStateMu.Lock()
state := ensureClientStateLocked(clientID)
state.UserID = userID
state.Status = clientStatusIdentified
state.LastError = ""
state.LastEvent = "account_identified"
state.IdentifiedAt = nowText()
state.LastSeenAt = state.IdentifiedAt
clientStateMu.Unlock()
clientIdMutex.Lock()
oldUserID := strings.TrimSpace(globalClientMap[clientID])
globalClientMap[clientID] = userID
clientIdMutex.Unlock()
if globalLogger != nil && oldUserID != "" && oldUserID != userID {
globalLogger.Warn("[辅助程序] 账号映射变化: clientId=%d old=%s new=%s event=%s", clientID, oldUserID, userID, responseTypeString(accountData))
} else if globalLogger != nil {
globalLogger.Info("[辅助程序] 账号映射确认: clientId=%d user_id=%s event=%s", clientID, userID, responseTypeString(accountData))
}
setInjectionStatus(injectionStatusIdentified, 0, "")
updateClientStatusWithResponseData(userID, accountData)
if autoReplyEngine != nil {
autoReplyEngine.observeCurrentAccountIdentity(clientID, userID, accountData)
}
if refreshIdentity && autoReplyEngine != nil {
cfg := autoReplyEngine.getConfig()
if cfg.Identity.RefreshOnStart {
go func() {
time.Sleep(800 * time.Millisecond)
autoReplyEngine.refreshIdentityContactsAsync("client_identified")
}()
}
}
}
func markClientMessageReady(clientID uint32, raw map[string]interface{}) {
if clientID == 0 {
return
}
if isAccountIdentityEvent(raw) {
if userID, accountData := extractAccountIdentity(raw); userID != "" {
markClientIdentified(clientID, userID, accountData)
return
}
}
now := nowText()
clientStateMu.Lock()
state := ensureClientStateLocked(clientID)
if state.Status != clientStatusIdentified {
state.Status = clientStatusMessageReady
}
state.LastError = ""
state.LastEvent = "message_ready"
if state.FirstMessageAt == "" {
state.FirstMessageAt = now
}
state.LastMessageAt = now
state.MessageCount++
state.LastSeenAt = now
clientStateMu.Unlock()
clientIdMutex.Lock()
if _, exists := globalClientMap[clientID]; !exists {
globalClientMap[clientID] = ""
}
clientIdMutex.Unlock()
setInjectionStatus(injectionStatusConnected, 0, "")
}
func markClientError(clientID uint32, status string, message string) {
if clientID == 0 {
return
}
clientStateMu.Lock()
state, exists := clientStates[clientID]
if !exists {
clientStateMu.Unlock()
return
}
state.Status = status
state.LastError = message
state.LastSeenAt = nowText()
clientStateMu.Unlock()
if message != "" {
globalLogger.Warn("[辅助程序] client %d %s: %s", clientID, status, message)
}
}
func removeClientState(clientID uint32) {
clientStateMu.Lock()
delete(clientStates, clientID)
remaining := len(clientStates)
clientStateMu.Unlock()
clientIdMutex.Lock()
delete(globalClientMap, clientID)
if globalClientId == clientID {
globalClientId = 0
}
clientIdMutex.Unlock()
if remaining == 0 {
setInjectionStatus(injectionStatusNotStarted, 0, "")
}
}
func getClientUserID(clientID uint32) string {
clientIdMutex.Lock()
defer clientIdMutex.Unlock()
return globalClientMap[clientID]
}
func recognizedClientCount() int {
clientIdMutex.Lock()
defer clientIdMutex.Unlock()
count := 0
for _, userID := range globalClientMap {
if strings.TrimSpace(userID) != "" {
count++
}
}
return count
}
func isUsableClientState(state *ClientRuntimeState) bool {
if state == nil {
return false
}
return state.Status == clientStatusIdentified || state.Status == clientStatusMessageReady
}
func usableClientCount() int {
clientStateMu.Lock()
defer clientStateMu.Unlock()
count := 0
for _, state := range clientStates {
if isUsableClientState(state) {
count++
}
}
return count
}
func isClientUsable(clientID uint32) bool {
clientStateMu.Lock()
defer clientStateMu.Unlock()
return isUsableClientState(clientStates[clientID])
}
func connectedClientCount() int {
clientStateMu.Lock()
defer clientStateMu.Unlock()
return len(clientStates)
}
func unidentifiedClientCount() int {
clientStateMu.Lock()
defer clientStateMu.Unlock()
count := 0
for _, state := range clientStates {
if strings.TrimSpace(state.UserID) == "" && state.Status != clientStatusMessageReady {
count++
}
}
return count
}
func runtimeRobotID(clientID uint32) string {
if clientID == 0 {
return ""
}
if userID := strings.TrimSpace(getClientUserID(clientID)); userID != "" {
return userID
}
return fmt.Sprintf("client:%d", clientID)
}
func clientConnectedAt(clientID uint32) time.Time {
clientStateMu.Lock()
defer clientStateMu.Unlock()
if state, ok := clientStates[clientID]; ok {
return parseClientStateTime(state.ConnectedAt)
}
return time.Time{}
}
func clientIdentifiedAt(clientID uint32) time.Time {
clientStateMu.Lock()
defer clientStateMu.Unlock()
if state, ok := clientStates[clientID]; ok {
return parseClientStateTime(state.IdentifiedAt)
}
return time.Time{}
}
func parseClientStateTime(value string) time.Time {
value = strings.TrimSpace(value)
if value == "" {
return time.Time{}
}
t, err := time.ParseInLocation("2006-01-02 15:04:05", value, time.Local)
if err != nil {
return time.Time{}
}
return t
}
func getUsableClientsMap() map[string]string {
clientStateMu.Lock()
defer clientStateMu.Unlock()
clients := make(map[string]string)
for clientID, state := range clientStates {
if !isUsableClientState(state) {
continue
}
userID := strings.TrimSpace(state.UserID)
if userID == "" {
userID = fmt.Sprintf("client:%d", clientID)
}
clients[fmt.Sprintf("%d", clientID)] = userID
}
return clients
}
func getIdentifiedClientsMap() map[string]string {
clientIdMutex.Lock()
defer clientIdMutex.Unlock()
clients := make(map[string]string)
for clientID, userID := range globalClientMap {
if strings.TrimSpace(userID) != "" {
clients[fmt.Sprintf("%d", clientID)] = userID
}
}
return clients
}
func getIdentifiedUserIDSet() map[string]bool {
clientIdMutex.Lock()
defer clientIdMutex.Unlock()
users := make(map[string]bool)
for _, userID := range globalClientMap {
userID = strings.TrimSpace(userID)
if userID != "" {
users[userID] = true
}
}
return users
}
func getRuntimeAccountRows() []map[string]interface{} {
clientStateMu.Lock()
states := make([]ClientRuntimeState, 0, len(clientStates))
for _, state := range clientStates {
if isUsableClientState(state) {
states = append(states, *state)
}
}
clientStateMu.Unlock()
sort.Slice(states, func(i, j int) bool {
return states[i].ClientID < states[j].ClientID
})
rows := make([]map[string]interface{}, 0, len(states))
for _, state := range states {
userID := strings.TrimSpace(state.UserID)
username := userID
runtimeOnly := false
if userID == "" {
userID = fmt.Sprintf("client:%d", state.ClientID)
username = fmt.Sprintf("未识别账号 client %d", state.ClientID)
runtimeOnly = true
}
healthState, healthMessage := clientHealthSnapshot(state)
rows = append(rows, map[string]interface{}{
"user_id": userID,
"username": username,
"avatar": "",
"corp_short_name": "",
"corp_name": "",
"status": 1,
"client_id": state.ClientID,
"pid": state.PID,
"runtime_status": state.Status,
"runtime_only": runtimeOnly,
"health_state": healthState,
"health_message": healthMessage,
"first_message_at": state.FirstMessageAt,
"last_message_at": state.LastMessageAt,
"message_count": state.MessageCount,
})
}
return rows
}
func getFirstAvailableClientID() uint32 {
clientStateMu.Lock()
defer clientStateMu.Unlock()
var messageReady uint32
var fallback uint32
for clientID, state := range clientStates {
if state.Status == clientStatusIdentified && strings.TrimSpace(state.UserID) != "" {
return clientID
}
if messageReady == 0 && state.Status == clientStatusMessageReady {
messageReady = clientID
}
if fallback == 0 {
fallback = clientID
}
}
if messageReady != 0 {
return messageReady
}
return fallback
}
func clientHealthSnapshot(state ClientRuntimeState) (string, string) {
if strings.TrimSpace(state.LastError) != "" {
return "error", state.LastError
}
if strings.TrimSpace(state.UserID) == "" {
return "unidentified", "账号已连接,等待识别或扫码登录"
}
lastSeen := parseClientStateTime(state.LastSeenAt)
if !lastSeen.IsZero() && time.Since(lastSeen) > 5*time.Minute {
return "stale", "超过5分钟未收到连接或消息事件"
}
if state.Status == clientStatusIdentified || state.Status == clientStatusMessageReady {
return "ok", "运行正常"
}
return state.Status, state.Status
}
func getClientDiagnostics() map[string]interface{} {
clientStateMu.Lock()
states := make([]ClientRuntimeState, 0, len(clientStates))
for _, state := range clientStates {
states = append(states, *state)
}
clientStateMu.Unlock()
sort.Slice(states, func(i, j int) bool {
return states[i].ClientID < states[j].ClientID
})
injectionMu.Lock()
injection := map[string]interface{}{
"status": injectionStatus,
"lastPid": injectionLastPID,
"lastError": injectionLastError,
"lastUpdate": injectionLastUpdate,
}
injectionMu.Unlock()
clientRows := make([]map[string]interface{}, 0, len(states))
for _, state := range states {
healthState, healthMessage := clientHealthSnapshot(state)
clientRows = append(clientRows, map[string]interface{}{
"clientId": state.ClientID,
"pid": state.PID,
"userId": state.UserID,
"status": state.Status,
"lastEvent": state.LastEvent,
"lastError": state.LastError,
"connectedAt": state.ConnectedAt,
"identifiedAt": state.IdentifiedAt,
"lastSeenAt": state.LastSeenAt,
"lastIdentifyRequestAt": state.LastIdentifyRequestAt,
"lastIdentifyResponseType": state.LastIdentifyResponseType,
"ignoredIdentifyEvents": state.IgnoredIdentifyEvents,
"lastIgnoredIdentifyReason": state.LastIgnoredIdentifyReason,
"lastIgnoredIdentifyEvent": state.LastIgnoredIdentifyEvent,
"firstMessageAt": state.FirstMessageAt,
"lastMessageAt": state.LastMessageAt,
"messageCount": state.MessageCount,
"healthState": healthState,
"healthMessage": healthMessage,
})
}
return map[string]interface{}{
"recognizedClientCount": recognizedClientCount(),
"usableClientCount": usableClientCount(),
"unidentifiedClientCount": unidentifiedClientCount(),
"connectionCount": connectedClientCount(),
"clients": clientRows,
"injection": injection,
"version": getWxWorkVersionDiagnostics(),
}
}
func getAccountDiagnosticMessage() string {
diagnostics := getClientDiagnostics()
recognized, _ := diagnostics["recognizedClientCount"].(int)
usable, _ := diagnostics["usableClientCount"].(int)
unidentified, _ := diagnostics["unidentifiedClientCount"].(int)
connectionCount, _ := diagnostics["connectionCount"].(int)
if recognized > 0 {
return ""
}
if usable > 0 {
return ""
}
if unidentified > 0 {
return "已连接到企业微信进程,但还未识别到账号信息;请查看 dashboard 的账号识别状态。"
}
if connectionCount > 0 {
return "已收到连接事件,但没有可用的企业微信账号信息。"
}
return "未收到企业微信账号连接;请先点击启动企微。"
}
func tryBeginWxWorkInjection() (bool, map[string]interface{}) {
if connectedClientCount() > 0 {
return false, wxWorkStartStatusPayload("已有企业微信连接,未重复注入", true, true)
}
injectionMu.Lock()
defer injectionMu.Unlock()
if injectionStatus == injectionStatusInjecting || injectionStatus == injectionStatusConnected || injectionStatus == injectionStatusIdentified {
return false, wxWorkStartStatusPayloadLocked("正在注入企业微信,请稍候", true, true)
}
injectionStatus = injectionStatusInjecting
injectionLastError = ""
injectionLastUpdate = nowText()
return true, nil
}
func setInjectionStatus(status string, pid uint32, errMsg string) {
injectionMu.Lock()
injectionStatus = status
if pid != 0 {
injectionLastPID = pid
}
injectionLastError = errMsg
injectionLastUpdate = nowText()
injectionMu.Unlock()
}
func watchWxWorkConnectionTimeout(pid uint32) {
time.Sleep(10 * time.Second)
if connectedClientCount() > 0 {
return
}
injectionMu.Lock()
if injectionStatus == injectionStatusConnected && injectionLastPID == pid {
injectionStatus = injectionStatusFailed
injectionLastError = "注入后未收到企业微信连接事件"
injectionLastUpdate = nowText()
}
injectionMu.Unlock()
}
func wxWorkStartStatusPayload(message string, success bool, skipped bool) map[string]interface{} {
injectionMu.Lock()
defer injectionMu.Unlock()
return wxWorkStartStatusPayloadLocked(message, success, skipped)
}
func wxWorkStartStatusPayloadLocked(message string, success bool, skipped bool) map[string]interface{} {
return map[string]interface{}{
"success": success,
"skipped": skipped,
"message": message,
"injectionStatus": injectionStatus,
"processId": injectionLastPID,
"recognizedClientCount": recognizedClientCount(),
"usableClientCount": usableClientCount(),
"unidentifiedClientCount": unidentifiedClientCount(),
"connectionCount": connectedClientCount(),
"lastError": injectionLastError,
}
}
func markPIDInjected(pid uint32) bool {
if pid == 0 {
return false
}
injectionMu.Lock()
defer injectionMu.Unlock()
if _, exists := injectedPIDs[pid]; exists {
return false
}
injectedPIDs[pid] = time.Now()
return true
}
func unmarkPIDInjected(pid uint32) {
if pid == 0 {
return
}
injectionMu.Lock()
delete(injectedPIDs, pid)
injectionMu.Unlock()
}
func ensureClientStateLocked(clientID uint32) *ClientRuntimeState {
state, exists := clientStates[clientID]
if !exists {
now := nowText()
state = &ClientRuntimeState{
ClientID: clientID,
Status: clientStatusConnected,
ConnectedAt: now,
LastSeenAt: now,
}
clientStates[clientID] = state
}
return state
}
func extractAccountIdentity(value map[string]interface{}) (string, map[string]interface{}) {
if value == nil {
return "", nil
}
if !isAccountIdentityEvent(value) {
return "", nil
}
return extractAccountIdentityFromMap(value)
}
func isAccountIdentityEvent(value map[string]interface{}) bool {
if value == nil {
return false
}
switch responseTypeString(value) {
case "11026", "11179":
return true
}
if data, ok := value["data"].(map[string]interface{}); ok {
switch responseTypeString(data) {
case "11026", "11179":
return true
}
}
return false
}
func extractAccountIdentityFromMap(data map[string]interface{}) (string, map[string]interface{}) {
return extractAccountIdentityFromMapDepth(data, 0)
}
func extractAccountIdentityFromMapDepth(data map[string]interface{}, depth int) (string, map[string]interface{}) {
if data == nil {
return "", nil
}
userID := strings.TrimSpace(valueToString(firstExisting(data, "user_id", "userId", "robotId", "acctid", "account", "wxid")))
if userID == "" {
if depth >= 6 {
return "", nil
}
for _, key := range []string{"data", "user", "accountInfo", "loginUser", "profile"} {
if nested, ok := data[key].(map[string]interface{}); ok {
if nestedUserID, nestedData := extractAccountIdentityFromMapDepth(nested, depth+1); nestedUserID != "" {
return nestedUserID, nestedData
}
}
}
return "", nil
}
accountData := make(map[string]interface{}, len(data)+1)
for key, value := range data {
accountData[key] = value
}
accountData["user_id"] = userID
if _, exists := accountData["username"]; !exists {
if name := valueToString(firstExisting(data, "username", "name", "nickname")); name != "" {
accountData["username"] = name
}
}
return userID, accountData
}
func firstExisting(data map[string]interface{}, keys ...string) interface{} {
for _, key := range keys {
if value, exists := data[key]; exists {
return value
}
}
return nil
}
func responseTypeString(data map[string]interface{}) string {
if data == nil {
return ""
}
return strings.TrimSpace(valueToString(firstExisting(data, "type", "event")))
}
func summarizeIdentifyEvent(data map[string]interface{}) string {
if data == nil {
return ""
}
encoded, err := json.Marshal(data)
if err != nil {
return fmt.Sprint(data)
}
text := string(encoded)
if len(text) > 260 {
return text[:260] + "..."
}
return text
}
func recordClientProbeEvent(clientID int32, data map[string]interface{}, raw string) {
clientProbeMu.Lock()
probes := append([]chan ClientProbeRecord(nil), clientProbes[clientID]...)
clientProbeMu.Unlock()
if len(probes) == 0 {
return
}
record := ClientProbeRecord{
Time: nowText(),
ClientID: clientID,
Type: responseTypeString(data),
Summary: dashboardMessageSummary(data, raw),
}
if data != nil {
record.Raw = data
} else if strings.TrimSpace(raw) != "" {
record.Raw = raw
}
for _, ch := range probes {
select {
case ch <- record:
default:
}
}
}
func runClientAccountProbe(clientID uint32) ClientProbeResult {
started := nowText()
result := ClientProbeResult{
ClientID: int32(clientID),
Started: started,
Duration: 15,
Records: []ClientProbeRecord{},
Message: "no account callback received",
}
if clientID == 0 {
result.Message = "invalid clientId"
return result
}
if !supportsAccountInfoRequest() {
result.Message = "account info request is disabled for this WXWork/DLL version because 11035 can crash WXWork; bind the client manually."
return result
}
ch := make(chan ClientProbeRecord, 64)
clientProbeMu.Lock()
clientProbes[int32(clientID)] = append(clientProbes[int32(clientID)], ch)
clientProbeMu.Unlock()
defer func() {
clientProbeMu.Lock()
probes := clientProbes[int32(clientID)]
for i, item := range probes {
if item == ch {
clientProbes[int32(clientID)] = append(probes[:i], probes[i+1:]...)
break
}
}
if len(clientProbes[int32(clientID)]) == 0 {
delete(clientProbes, int32(clientID))
}
clientProbeMu.Unlock()
close(ch)
}()
markClientIdentifyRequest(clientID)
_, err := handleSendWxWorkData(map[string]interface{}{
"data": `{"type":11035,"data":{}}`,
"clientId": clientID,
})
if err != nil {
result.Message = "probe send 11035 failed: " + err.Error()
return result
}
timer := time.After(15 * time.Second)
for {
select {
case record := <-ch:
result.Records = append(result.Records, record)
if data, ok := record.Raw.(map[string]interface{}); ok {
markClientIdentifyResponse(clientID, data)
if userID, accountData := extractAccountIdentity(data); userID != "" {
markClientIdentified(clientID, userID, accountData)
result.Message = "account callback received; client upgraded to identified"
}
}
case <-timer:
if len(result.Records) == 0 {
result.Message = "15 seconds passed with no callbacks. This DLL/WXWork version may not return account info; use 11041 messages to enter message_ready mode."
} else if result.Message == "no account callback received" {
result.Message = "callbacks received, but no account identity fields were found; use 11041 messages to enter message_ready mode."
}
return result
}
}
}
func valueToString(value interface{}) string {
switch v := value.(type) {
case string:
return strings.TrimSpace(v)
case fmt.Stringer:
return strings.TrimSpace(v.String())
case nil:
return ""
default:
return strings.TrimSpace(fmt.Sprint(v))
}
}
func supportsAccountInfoRequest() bool {
bundle := resolveDLLBundle()
return bundle.HelperVersion != "5.0.8.6009" && bundle.WxWorkVersion != "5.0.8.6009"
}