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" }