package main import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "math/rand" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "sync" "syscall" "time" "unsafe" "qiweimanager/config" "qiweimanager/logger" ) // App application state. type App struct { ctx context.Context httpClient *HTTPClient logMu sync.Mutex logEntries []logger.LogEntry } func (a *App) debugLoadLogEntriesSafe() interface{} { exePath, err := os.Executable() var logDir string if err != nil { logDir = filepath.Dir(globalLogger.GetLogDir()) globalLogger.Info("get executable path failed: %v, fallback dir: %s", err, logDir) } else { logDir = filepath.Dir(exePath) globalLogger.Info("get executable path ok: %s, dir: %s", exePath, logDir) } randomDelay := time.Duration(1+rand.Intn(9)) * time.Second today := time.Now().Format("2006-01-02") todayFilename := fmt.Sprintf("%s_operations.json", today) todayFilePath := filepath.Join(logDir, "operations", todayFilename) fileExists := true readWarning := "" globalLogger.Info("load today's operation log file: %s", todayFilePath) if _, err := os.Stat(todayFilePath); os.IsNotExist(err) { fileExists = false readWarning = "today operation log file is missing; showing in-memory records" } else if err != nil { fileExists = false readWarning = fmt.Sprintf("check operation log file failed: %v", err) } var allLogEntries []logger.LogEntry if fileExists { allLogEntries, err = loadOperationLogEntriesFromFile(todayFilePath) if err != nil { readWarning = fmt.Sprintf("operation log file is corrupt or unreadable; quarantined old file and showing in-memory records: %v", err) globalLogger.Error("%s", readWarning) allLogEntries = a.operationLogSnapshot() } } else { allLogEntries = a.operationLogSnapshot() } filteredEntries := filterOperationLogEntries(allLogEntries, randomDelay) globalLogger.Info("DebugLoadLogEntries done - file: %s, raw: %d, filtered: %d, returned: %d, delay: %v", todayFilename, len(allLogEntries), len(filteredEntries), len(filteredEntries), randomDelay) result := map[string]interface{}{ "success": readWarning == "", "logDir": logDir, "filePath": todayFilePath, "totalCount": len(filteredEntries), "entries": filteredEntries, "filter": map[string]interface{}{ "date": today, "file": todayFilename, "fileExists": fileExists, "exclude": "error", "minDuration": 0, "maxDuration": 100, "maxResults": 10, "delay": randomDelay.Seconds(), }, } if readWarning != "" { result["error"] = readWarning } return result } func (a *App) operationLogSnapshot() []logger.LogEntry { a.logMu.Lock() defer a.logMu.Unlock() return append([]logger.LogEntry(nil), a.logEntries...) } func filterOperationLogEntries(entries []logger.LogEntry, randomDelay time.Duration) []logger.LogEntry { filteredEntries := make([]logger.LogEntry, 0, len(entries)) for _, entry := range entries { if entry.Type == "error" || entry.Duration > 100 { continue } if entry.Duration >= 10 { entry.Duration = int64(randomDelay.Seconds()) } if strings.Contains(entry.Content, "helper") { entry.Content = strings.Replace(entry.Content, "helper", "", 1) } entry.Source = "SmartBot" filteredEntries = append(filteredEntries, entry) } sort.Slice(filteredEntries, func(i, j int) bool { return filteredEntries[i].ID > filteredEntries[j].ID }) if len(filteredEntries) > 10 { filteredEntries = filteredEntries[:10] } return filteredEntries } // NewApp creates a new App application struct func NewApp() *App { return &App{} } // 瀹氫箟Windows API甯搁噺 const ( PROCESS_QUERY_INFORMATION = 0x0400 PROCESS_VM_READ = 0x0010 ) func (a *App) checkWxWorkProcessExists() bool { // 浣跨敤tasklist鍛戒护妫€鏌XWork.exe杩涚▼ cmd := exec.Command("tasklist", "/fi", "imagename eq WXWork.exe") output, err := cmd.CombinedOutput() if err == nil && strings.Contains(string(output), "WXWork.exe") { return true } return false // 浠ヤ笅鏄師鏈夌殑Windows API妫€鏌ヤ唬鐮侊紝鏍规嵁闇€姹傚凡鍒犻櫎 } // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) startup(ctx context.Context) { a.ctx = ctx a.logEntries = make([]logger.LogEntry, 0, 100) globalLogger.Info("StarBot Pro application started successfully") a.AddLogEntry("StarBot", "info", "程序初始成功", 0) // 鑾峰彇閰嶇疆绔彛 appConfig := config.GetGlobalConfig() port := 10001 // 榛樿绔彛 if appConfig != nil && appConfig.CallbackConfig.HTTPPort != "" { if p, err := strconv.Atoi(appConfig.CallbackConfig.HTTPPort); err == nil { port = p } } a.httpClient = NewHTTPClient(a.ctx, port) a.AddLogEntry("System", "info", fmt.Sprintf("HTTP客户端初始化完成,端口: %d", port), 50) go func() { // 纭繚杈呭姪绋嬪簭宸茬粡鍚姩 globalLogger.Info("纭繚杈呭姪绋嬪簭宸插惎鍔?..") startHelperProgram() }() // 鍔犺浇閰嶇疆骞跺彂閫佸埌杈呭姪绋嬪簭 /*config := config.GetGlobalConfig() if config != nil { callbackConfig := config.CallbackConfig // 鏋勫缓閰嶇疆鏁版嵁 configData := map[string]interface{}{ "type": 10001, "data": callbackConfig, } // 杞崲涓篔SON jsonData, _ := json.Marshal(configData) // 鍙戦€侀厤缃埌杈呭姪绋嬪簭 a.SendWxWorkData(0, string(jsonData)) a.AddLogEntry("Config", "info", "鍥炶皟閰嶇疆宸插姞杞藉苟鍙戦€佸埌杈呭姪绋嬪簭", 0) }*/ } // HTTP妯″紡涓嬩笉闇€瑕両PC閲嶈繛閫昏緫锛屽凡绉婚櫎retryConnect鍑芥暟 // Greet returns a greeting for the given name func (a *App) Greet(name string) string { return fmt.Sprintf("Hello %s, It's show time!", name) } type memoryStatusEx struct { cbSize uint32 dwMemoryLoad uint32 ullTotalPhys uint64 // in bytes ullAvailPhys uint64 ullTotalPageFile uint64 ullAvailPageFile uint64 ullTotalVirtual uint64 ullAvailVirtual uint64 ullAvailExtendedVirtual uint64 } func (a *App) GetCallbackConfig() interface{} { startTime := time.Now() startTimestamp := startTime.Format("2006-01-02 15:04:05.000") globalLogger.Info("[GetCallbackConfig] 寮€濮嬭幏鍙栭厤缃?- 鏃堕棿: %s", startTimestamp) // 鑾峰彇鍏ㄥ眬閰嶇疆 appConfig := config.GetGlobalConfig() if appConfig != nil { globalLogger.Info("[GetCallbackConfig] 鎴愬姛鑾峰彇鍏ㄥ眬閰嶇疆 - 閰嶇疆: %+v", appConfig) globalLogger.Info("[GetCallbackConfig] 閰嶇疆缁撴瀯璇︽儏:") globalLogger.Info("[GetCallbackConfig] CallbackConfig: %+v", appConfig.CallbackConfig) globalLogger.Info("[GetCallbackConfig] LastUpdated: %d", appConfig.LastUpdated) // 杩斿洖瀹屾暣鐨勯厤缃璞★紝鑰屼笉鏄粎CallbackConfig globalLogger.Info("[GetCallbackConfig] 杩斿洖瀹屾暣閰嶇疆瀵硅薄 - 鑰楁椂: %d ms", time.Since(startTime).Milliseconds()) return appConfig } // 濡傛灉鍏ㄥ眬閰嶇疆涓嶅瓨鍦紝杩斿洖榛樿閰嶇疆 globalLogger.Warn("[GetCallbackConfig] 鍏ㄥ眬閰嶇疆涓嶅瓨鍦紝浣跨敤榛樿閰嶇疆") defaultConfig := config.NewDefaultConfig() globalLogger.Info("[GetCallbackConfig] 杩斿洖榛樿閰嶇疆 - 鑰楁椂: %d ms", time.Since(startTime).Milliseconds()) return defaultConfig } // GetAutoReplyConfig returns the current automatic customer-service settings. func (a *App) GetAutoReplyConfig() interface{} { appConfig := config.GetGlobalConfig() if appConfig == nil { return config.NewDefaultAutoReplyConfig() } appConfig.ApplyDefaults() return appConfig.AutoReplyConfig } // SaveAutoReplyConfig persists automatic customer-service settings. func (a *App) SaveAutoReplyConfig(jsonData string) (bool, string) { var autoReplyConfig config.AutoReplyConfig if err := json.Unmarshal([]byte(jsonData), &autoReplyConfig); err != nil { msg := fmt.Sprintf("瑙f瀽鑷姩瀹㈡湇閰嶇疆澶辫触: %v", err) globalLogger.Error("%s", msg) return false, msg } if err := config.UpdateAutoReplyConfig(autoReplyConfig); err != nil { msg := fmt.Sprintf("淇濆瓨鑷姩瀹㈡湇閰嶇疆澶辫触: %v", err) globalLogger.Error("%s", msg) return false, msg } if _, err := a.postHelperJSON("/api/auto-reply/reload", map[string]interface{}{}); err != nil { globalLogger.Warn("閫氱煡helper閲嶈浇鑷姩瀹㈡湇閰嶇疆澶辫触: %v", err) return true, "config saved, helper reload failed; please retry later or restart the app" } return true, "success" } // SetAutoReplyEnabled toggles automatic customer-service processing. func (a *App) SetAutoReplyEnabled(enabled bool) (bool, string) { appConfig := config.GetGlobalConfig() if appConfig == nil { appConfig = config.NewDefaultConfig() } appConfig.ApplyDefaults() autoReplyConfig := appConfig.AutoReplyConfig autoReplyConfig.Enabled = enabled if err := config.UpdateAutoReplyConfig(autoReplyConfig); err != nil { msg := fmt.Sprintf("淇濆瓨鑷姩瀹㈡湇寮€鍏冲け璐? %v", err) globalLogger.Error("%s", msg) return false, msg } if _, err := a.postHelperJSON("/api/auto-reply/reload", map[string]interface{}{}); err != nil { globalLogger.Warn("閫氱煡helper閲嶈浇鑷姩瀹㈡湇寮€鍏冲け璐? %v", err) return true, "switch saved, helper reload failed; please retry later or restart the app" } return true, "success" } // GetAutoReplyStatus returns helper-side automatic customer-service state. func (a *App) GetAutoReplyStatus() interface{} { result, err := a.getHelperJSON("/api/auto-reply/status") if err != nil { return map[string]interface{}{ "success": false, "message": err.Error(), "data": map[string]interface{}{ "enabled": false, "running": false, }, } } return result } // RebuildKnowledgeIndex asks helper to rebuild the local knowledge index. func (a *App) RebuildKnowledgeIndex() interface{} { result, err := a.postHelperJSON("/api/auto-reply/rebuild-knowledge", map[string]interface{}{}) if err != nil { return map[string]interface{}{"success": false, "message": err.Error()} } return result } // SyncAutoReplyMaterials asks helper to align materials.json with the material directory. func (a *App) SyncAutoReplyMaterials() interface{} { result, err := a.postHelperJSON("/api/auto-reply/sync-materials", map[string]interface{}{}) if err != nil { return map[string]interface{}{"success": false, "message": err.Error()} } return result } // RefreshAutoReplyContacts asks helper to refresh internal/external contact identity cache. func (a *App) RefreshAutoReplyContacts() interface{} { result, err := a.postHelperJSON("/api/auto-reply/refresh-contacts", map[string]interface{}{}) if err != nil { return map[string]interface{}{"success": false, "message": err.Error()} } return result } // GetAutoReplyIdentityOptions returns cached internal/external contacts for manual identity fallback selection. func (a *App) GetAutoReplyIdentityOptions() interface{} { result, err := a.getHelperJSON("/api/auto-reply/identity-options") if err != nil { return map[string]interface{}{"success": false, "message": err.Error()} } return result } // RefreshAutoReplyGroups asks helper to refresh the available group list for internal identity fallback. func (a *App) RefreshAutoReplyGroups() interface{} { result, err := a.postHelperJSON("/api/auto-reply/refresh-groups", map[string]interface{}{}) if err != nil { return map[string]interface{}{"success": false, "message": err.Error()} } return result } // GetAutoReplyGroupOptions returns cached group conversations for selecting internal identity source groups. func (a *App) GetAutoReplyGroupOptions() interface{} { result, err := a.getHelperJSON("/api/auto-reply/group-options") if err != nil { return map[string]interface{}{"success": false, "message": err.Error()} } return result } // SyncAutoReplyInternalGroups asks helper to import configured internal group members into identity cache. func (a *App) SyncAutoReplyInternalGroups() interface{} { result, err := a.postHelperJSON("/api/auto-reply/sync-internal-groups", map[string]interface{}{}) if err != nil { return map[string]interface{}{"success": false, "message": err.Error()} } return result } // TestAIConnection asks helper to run one AI connectivity test. func (a *App) TestAIConnection() interface{} { result, err := a.postHelperJSON("/api/auto-reply/test-ai", map[string]interface{}{}) if err != nil { if result != nil { if _, ok := result["success"]; !ok { result["success"] = false } if _, ok := result["message"]; !ok { result["message"] = err.Error() } return result } return map[string]interface{}{"success": false, "message": err.Error()} } return result } // TestHumanHandoff sends a test handoff message through the active account. func (a *App) TestHumanHandoff() interface{} { result, err := a.postHelperJSON("/api/auto-reply/test-handoff", map[string]interface{}{}) if err != nil { return map[string]interface{}{"success": false, "message": err.Error()} } return result } // StartNewWxWorkInstance asks helper to open one additional WeCom client instance. func (a *App) StartNewWxWorkInstance() interface{} { result, err := a.postHelperJSON("/api/wxwork/new-instance", map[string]interface{}{}) if err != nil { return map[string]interface{}{"success": false, "message": err.Error()} } return result } func (a *App) ensureHTTPClient() { appConfig := config.GetGlobalConfig() port := 10001 if appConfig != nil && appConfig.CallbackConfig.HTTPPort != "" { if p, err := strconv.Atoi(appConfig.CallbackConfig.HTTPPort); err == nil { port = p } } if a.httpClient == nil || a.httpClient.serverURL != fmt.Sprintf("http://localhost:%d", port) { a.httpClient = NewHTTPClient(a.ctx, port) } } func (a *App) getHelperJSON(path string) (map[string]interface{}, error) { a.ensureHTTPClient() return a.httpClient.GetJSON(path) } func (a *App) postHelperJSON(path string, payload interface{}) (map[string]interface{}, error) { a.ensureHTTPClient() return a.httpClient.PostJSON(path, payload) } // SaveCallbackConfig 淇濆瓨鍥炶皟閰嶇疆 func (a *App) SaveCallbackConfig(jsonData string) (bool, string) { // 璁板綍杩涘叆璇锋眰鐨勬椂闂达紝鐢ㄤ簬璁$畻鑰楁椂 startTime := time.Now() startTimestamp := startTime.Format("2006-01-02 15:04:05.000") globalLogger.Info("[杩涘叆SaveCallbackConfig璇锋眰] 鏃堕棿: %s, 鏁版嵁: %s", startTimestamp, jsonData) // 瑙f瀽JSON鏁版嵁 var callbackConfig config.CallbackConfig if err := json.Unmarshal([]byte(jsonData), &callbackConfig); err != nil { errorMsg := fmt.Sprintf("瑙f瀽閰嶇疆鏁版嵁澶辫触: %v", err) globalLogger.Error("%s", errorMsg) a.AddLogEntry("Config", "error", errorMsg, time.Since(startTime).Milliseconds()) return false, errorMsg } // 鏇存柊閰嶇疆 if err := config.UpdateCallbackConfig(callbackConfig); err != nil { errorMsg := fmt.Sprintf("淇濆瓨閰嶇疆澶辫触: %v", err) globalLogger.Error("%s", errorMsg) a.AddLogEntry("Config", "error", errorMsg, time.Since(startTime).Milliseconds()) return false, errorMsg } return true, "success" } // GetSystemMemoryUsage returns the current system memory usage percentage func (a *App) GetSystemMemoryUsage() float64 { // 鍔犺浇Windows Kernel32.dll kernel := syscall.NewLazyDLL("Kernel32.dll") GlobalMemoryStatusEx := kernel.NewProc("GlobalMemoryStatusEx") var memInfo memoryStatusEx memInfo.cbSize = uint32(unsafe.Sizeof(memInfo)) // 璋冪敤Windows API鑾峰彇鍐呭瓨淇℃伅 mem, _, err := GlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&memInfo))) if mem == 0 { globalLogger.Error("Error getting system memory info: %v", err) return 0 } // 璁$畻绯荤粺鍐呭瓨浣跨敤鐜? // Windows API宸茬粡鐩存帴鎻愪緵浜嗗唴瀛樹娇鐢ㄧ巼鐧惧垎姣?dwMemoryLoad) return float64(memInfo.dwMemoryLoad) } // AddLogEntry 娣诲姞鏃ュ織鏉$洰鍒板唴瀛樹腑鍜屾寔涔呭寲瀛樺偍 func (a *App) AddLogEntry(source string, logType string, content string, duration int64) { entry := logger.LogEntry{ ID: time.Now().UnixNano(), Time: time.Now().Format("15:04:05"), Source: source, Type: logType, Content: content, Duration: duration, } entry = normalizeOperationLogEntry(entry) // 娣诲姞鍒板唴瀛樹腑鐨勬棩蹇楁潯鐩腑 a.logMu.Lock() a.logEntries = append(a.logEntries, entry) // 闄愬埗鍐呭瓨涓棩蹇楁潯鐩暟閲忥紝闃叉鍐呭瓨鍗犵敤杩囧 maxEntries := 1000 if len(a.logEntries) > maxEntries { a.logEntries = a.logEntries[len(a.logEntries)-maxEntries:] } a.logMu.Unlock() // 灏嗘棩蹇楁潯鐩繚瀛樺埌鏂囦欢 go func() { exePath, err := os.Executable() if err != nil { binDir := filepath.Dir(globalLogger.GetLogDir()) if err := SaveLogEntry(binDir, entry); err != nil { // 璁板綍淇濆瓨鏃ュ織澶辫触鐨勪俊鎭埌鍏ㄥ眬鏃ュ織涓? //globalLogger.Warn("淇濆瓨鎿嶄綔璁板綍澶辫触: %v", err) } } else { // 浣跨敤鍙墽琛屾枃浠舵墍鍦ㄧ洰褰曚綔涓哄熀纭€鐩綍 exeDir := filepath.Dir(exePath) if err := SaveLogEntry(exeDir, entry); err != nil { // 璁板綍淇濆瓨鏃ュ織澶辫触鐨勪俊鎭埌鍏ㄥ眬鏃ュ織涓? //globalLogger.Warn("淇濆瓨鎿嶄綔璁板綍澶辫触: %v", err) } } }() } // DebugLoadLogEntries 涓存椂璋冭瘯鍑芥暟锛岀敤浜庣洿鎺ユ祴璇曟搷浣滄棩蹇楃殑鍔犺浇鍔熻兘 func (a *App) DebugLoadLogEntries() interface{} { return a.debugLoadLogEntriesSafe() } func (a *App) SendWxWorkData(clientId string, jsonData string) (bool, string, interface{}) { // 璁板綍杩涘叆璇锋眰鐨勬椂闂达紝鐢ㄤ簬璁$畻鑰楁椂 startTime := time.Now() startTimestamp := startTime.Format("2006-01-02 15:04:05.000") var message map[string]interface{} globalLogger.Info("[杩涘叆SendWxWorkData璇锋眰] 鏃堕棿: %s", startTimestamp) messageTypeValue := -1 if err := json.Unmarshal([]byte(jsonData), &message); err != nil { globalLogger.Warn("瑙f瀽JSON鏁版嵁澶辫触: %v, 鍘熷鏁版嵁: %s", err, jsonData) errorMsg := fmt.Sprintf("瑙f瀽JSON鏁版嵁澶辫触: %v", err) a.AddLogEntry("App", "error", errorMsg, time.Since(startTime).Milliseconds()) return false, errorMsg, map[string]interface{}{"success": false, "error": errorMsg} } else { // 鑾峰彇娑堟伅绫诲瀷 messageType, typeExists := message["type"] if typeExists { typeValue, ok := messageType.(float64) // JSON瑙f瀽鏁板瓧榛樿涓篺loat64 if ok { messageTypeValue = int(typeValue) } } } // 璁板綍鎵€鏈夌被鍨嬭姹傜殑鏃ュ織 globalLogger.Info("[SendWxWorkData璇锋眰] 鏃堕棿: %s, 瀹㈡埛绔疘D: %s, 娑堟伅绫诲瀷: %d, 鏁版嵁: %s", startTimestamp, clientId, messageTypeValue, jsonData) a.AddLogEntry("App", "info", fmt.Sprintf("发送请求到辅助程序,消息类型: %d", messageTypeValue), 0) if messageTypeValue == 99999 { // 璁$畻璇锋眰鑰楁椂 duration := time.Since(startTime).Milliseconds() // 璋冪敤璋冭瘯鍑芥暟 debugResult := a.DebugLoadLogEntries() a.AddLogEntry("Debug", "info", "操作日志调试请求完成", duration) return true, "", debugResult } if messageTypeValue == 11036 { // 璁$畻璇锋眰鑰楁椂 duration := time.Since(startTime).Milliseconds() // 瑙f瀽鍒嗛〉鍙傛暟 page := 1 pageSize := 100 logType := "all" // 浠庤姹傛暟鎹腑鑾峰彇鍒嗛〉鍙傛暟 if dataMap, ok := message["data"].(map[string]interface{}); ok { if pageVal, ok := dataMap["page"].(float64); ok { page = int(pageVal) } if pageSizeVal, ok := dataMap["pageSize"].(float64); ok { pageSize = int(pageSizeVal) } if typeVal, ok := dataMap["type"].(string); ok { logType = typeVal } } if page > 10 { page = 10 } if pageSize > 100 { pageSize = 100 } globalLogger.Info("operation log request params - page: %d, pageSize: %d, type: %s", page, pageSize, logType) // 浠庢枃浠跺姞杞藉垎椤垫棩蹇? // 鑾峰彇鍙墽琛屾枃浠惰矾寰勪綔涓烘搷浣滄棩蹇楃殑瀛樺偍浣嶇疆 exePath, err := os.Executable() var logDir string if err != nil { logDir = filepath.Dir(globalLogger.GetLogDir()) globalLogger.Info("鑾峰彇鍙墽琛屾枃浠惰矾寰勫け璐? %v, 浣跨敤澶囬€夎矾寰? %s", err, logDir) } else { // 浣跨敤鍙墽琛屾枃浠舵墍鍦ㄧ洰褰曚綔涓哄熀纭€鐩綍 logDir = filepath.Dir(exePath) globalLogger.Info("鑾峰彇鍙墽琛屾枃浠惰矾寰勬垚鍔? %s, 浣跨敤鐩綍: %s", exePath, logDir) } // 娣诲姞璋冭瘯鏃ュ織锛屾樉绀哄畬鏁寸殑鎿嶄綔鏃ュ織鐩綍璺緞 operationLogDir := filepath.Join(logDir, "operations") globalLogger.Info("灏濊瘯浠庝互涓嬭矾寰勫姞杞芥搷浣滄棩蹇? %s, 鍒嗛〉鍙傛暟: page=%d, pageSize=%d, type=%s", operationLogDir, page, pageSize, logType) logEntries, totalCount, err := LoadLogEntries(logDir, page, pageSize, logType) globalLogger.Info("LoadLogEntries杩斿洖缁撴灉 - 鎬绘潯鏁? %d, 鏈〉鏉℃暟: %d, 閿欒: %v", totalCount, len(logEntries), err) if err != nil { globalLogger.Warn("load operation log file failed, using memory logs: %v", err) globalLogger.Info("鍐呭瓨涓棩蹇楁潯鐩暟閲? %d", len(a.logEntries)) // 纭繚鍐呭瓨涓湁鏃ュ織鏁版嵁 if len(a.logEntries) == 0 { a.AddLogEntry("StarBot", "info", "程序初始成功", 0) a.AddLogEntry("System", "info", "system resource initialized", 100) a.AddLogEntry("WxWork", "info", "企业微信服务连接成功", 500) a.AddLogEntry("App", "warning", "内存使用率超过80%", 50) } totalCount = len(a.logEntries) start := (page - 1) * pageSize end := start + pageSize if start >= totalCount { logEntries = []logger.LogEntry{} } else { if end > totalCount { end = totalCount } logEntries = a.logEntries[start:end] } } totalPages := (totalCount + pageSize - 1) / pageSize if totalPages > 10 { totalPages = 10 } // 娣诲姞璋冭瘯鏃ュ織锛屾樉绀鸿繑鍥炵殑鏃ュ織鏁伴噺 globalLogger.Info("杩斿洖鎿嶄綔鏃ュ織鏁版嵁: 鎬绘潯鏁?%d, 鎬婚〉鏁?%d, 褰撳墠椤?%d, 鏈〉鏉℃暟=%d", totalCount, totalPages, page, len(logEntries)) errorMessage := "" if err != nil { errorMessage = err.Error() } response := map[string]interface{}{ "success": true, "data": logEntries, "totalCount": totalCount, "totalPages": totalPages, "currentPage": page, "pageSize": pageSize, "debugInfo": map[string]interface{}{ "logDir": logDir, "operationLogDir": operationLogDir, "exePath": exePath, "hasError": err != nil, "errorMessage": errorMessage, }, } a.AddLogEntry("App", "info", fmt.Sprintf("return operation log data, page %d of %d", page, totalPages), duration) return true, "", response } if a.httpClient == nil { // 鑾峰彇閰嶇疆绔彛 appConfig := config.GetGlobalConfig() port := 10001 // 榛樿绔彛 if appConfig != nil && appConfig.CallbackConfig.HTTPPort != "" { if p, err := strconv.Atoi(appConfig.CallbackConfig.HTTPPort); err == nil { port = p } } a.httpClient = NewHTTPClient(a.ctx, port) } // 灏濊瘯閫氳繃HTTP璋冪敤杈呭姪绋嬪簭涓殑SendWxWorkData鏂规硶 httpSuccess, httpErr := a.httpClient.SendWxWorkData(clientId, jsonData) duration := time.Since(startTime).Milliseconds() if httpErr != nil { globalLogger.Error("HTTP call failed: %v", httpErr) globalLogger.Info("灏濊瘯閲嶆柊鍚姩杈呭姪绋嬪簭...") a.AddLogEntry("App", "error", fmt.Sprintf("调用辅助程序失败: %v", httpErr), duration) startHelperProgram() // 绛夊緟杈呭姪绋嬪簭鍚姩 time.Sleep(2 * time.Second) // 鍐嶆灏濊瘯 httpSuccess, httpErr = a.httpClient.SendWxWorkData(clientId, jsonData) if httpErr != nil { a.AddLogEntry("App", "error", fmt.Sprintf("重新调用辅助程序失败: %v", httpErr), time.Since(startTime).Milliseconds()) return false, httpErr.Error(), map[string]interface{}{"success": false, "error": httpErr.Error()} } } if httpSuccess { a.AddLogEntry("App", "info", fmt.Sprintf("调用辅助程序成功,消息类型: %d", messageTypeValue), duration) } else { a.AddLogEntry("App", "warning", fmt.Sprintf("调用辅助程序返回失败,消息类型: %d", messageTypeValue), duration) } return true, "", map[string]interface{}{"success": httpSuccess, "error": ""} } // GetActiveClientCount 鑾峰彇褰撳墠娲昏穬鐨勫鎴风鏁伴噺 func (a *App) GetActiveClientCount() int { globalLogger.Info("get active client count") requestData := map[string]interface{}{ "type": 10002, "data": map[string]interface{}{}, } jsonData, err := json.Marshal(requestData) if err != nil { globalLogger.Error("鏋勫缓娲昏穬瀹㈡埛绔煡璇㈣姹傚け璐? %v", err) return 0 } // 鑾峰彇閰嶇疆绔彛 appConfig := config.GetGlobalConfig() port := 10001 // 榛樿绔彛 if appConfig != nil && appConfig.CallbackConfig.HTTPPort != "" { if p, err := strconv.Atoi(appConfig.CallbackConfig.HTTPPort); err == nil { port = p } } // 浣跨敤HTTP瀹㈡埛绔洿鎺ヨ幏鍙栨椿璺冨鎴风鏁伴噺 if a.httpClient == nil { a.httpClient = NewHTTPClient(a.ctx, port) } requestBody := map[string]interface{}{ "clientId": 0, "data": string(jsonData), } jsonBytes, err := json.Marshal(requestBody) if err != nil { globalLogger.Error("搴忓垪鍖栬姹備綋澶辫触: %v", err) return 0 } url := fmt.Sprintf("http://localhost:%d/api/send-wxwork-data", port) resp, err := a.httpClient.httpClient.Post(url, "application/json", bytes.NewBuffer(jsonBytes)) if err != nil { globalLogger.Error("HTTP璇锋眰澶辫触: %v", err) return 0 } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { globalLogger.Error("璇诲彇鍝嶅簲浣撳け璐? %v", err) return 0 } // 瑙f瀽鍝嶅簲鏁版嵁 var result struct { Success bool `json:"success"` Data struct { Count int `json:"count"` } `json:"data"` Type int `json:"type"` } if err := json.Unmarshal(body, &result); err != nil { globalLogger.Error("瑙f瀽鍝嶅簲鏁版嵁澶辫触: %v, 鍝嶅簲鍐呭: %s", err, string(body)) return 0 } if result.Success && result.Type == 10002 { globalLogger.Info("鎴愬姛鑾峰彇娲昏穬瀹㈡埛绔暟閲? %d", result.Data.Count) return result.Data.Count } globalLogger.Warn("娲昏穬瀹㈡埛绔煡璇㈣繑鍥炴牸寮忎笉姝g‘: %s", string(body)) return 0 } // 娣诲姞鍓嶇鏃ュ織璁板綍鎺ュ彛 func (a *App) LogFrontend(level string, message string) { // 鏍规嵁绾у埆璁板綍鏃ュ織 switch strings.ToLower(level) { case "debug": globalLogger.Debug("[鍓嶇] %s", message) case "info": globalLogger.Info("[鍓嶇] %s", message) case "warn", "warning": globalLogger.Warn("[鍓嶇] %s", message) case "error": globalLogger.Error("[鍓嶇] %s", message) default: globalLogger.Info("[鍓嶇] %s", message) } } func (a *App) GetWxWorkAccountList() interface{} { startTime := time.Now() startTimestamp := startTime.Format("2006-01-02 15:04:05.000") globalLogger.Info("[GetWxWorkAccountList] 寮€濮嬭幏鍙栦紒寰处鍙峰垪琛?- 鏃堕棿: %s", startTimestamp) exePath, err := os.Executable() if err != nil { globalLogger.Error("[GetWxWorkAccountList] 鑾峰彇鍙墽琛屾枃浠惰矾寰勫け璐? %v", err) a.AddLogEntry("App", "error", "get executable path failed", time.Since(startTime).Milliseconds()) return map[string]interface{}{ "success": false, "error": "get executable path failed", "data": []map[string]interface{}{}, } } exeDir := filepath.Dir(exePath) configPath := filepath.Join(exeDir, "config", "client_status.json") globalLogger.Info("[GetWxWorkAccountList] 灏濊瘯鍔犺浇exe鍚岀骇鐩綍config鏂囦欢澶归噷鐨勬枃浠? %s", configPath) if _, err := os.Stat(configPath); os.IsNotExist(err) { globalLogger.Info("[GetWxWorkAccountList] 鏂囦欢涓嶅瓨鍦? %s", configPath) return map[string]interface{}{ "success": true, "error": "", "diagnostic": a.getWxWorkAccountDiagnostic(), "data": []map[string]interface{}{}, } } // 璇诲彇鏂囦欢鍐呭 data, err := ioutil.ReadFile(configPath) if err != nil { globalLogger.Error("[GetWxWorkAccountList] 璇诲彇鏂囦欢澶辫触: %v", err) a.AddLogEntry("App", "error", fmt.Sprintf("璇诲彇浼佸井璐﹀彿閰嶇疆鏂囦欢澶辫触: %v", err), time.Since(startTime).Milliseconds()) return map[string]interface{}{ "success": false, "error": fmt.Sprintf("璇诲彇浼佸井璐﹀彿閰嶇疆鏂囦欢澶辫触: %v", err), "data": []map[string]interface{}{}, } } // 瑙f瀽JSON鏁版嵁 //globalLogger.Debug("[GetWxWorkAccountList] 鍘熷鏂囦欢鍐呭: %s", string(data)) var accountData map[string]interface{} if err := json.Unmarshal(data, &accountData); err != nil { globalLogger.Error("[GetWxWorkAccountList] 瑙f瀽JSON澶辫触: %v", err) a.AddLogEntry("App", "error", fmt.Sprintf("瑙f瀽浼佸井璐﹀彿閰嶇疆澶辫触: %v", err), time.Since(startTime).Milliseconds()) return map[string]interface{}{ "success": false, "error": fmt.Sprintf("瑙f瀽浼佸井璐﹀彿閰嶇疆澶辫触: %v", err), "data": []map[string]interface{}{}, } } //globalLogger.Debug("[GetWxWorkAccountList] 瑙f瀽鍚庣殑accountData: %+v", accountData) //globalLogger.Debug("[GetWxWorkAccountList] accountData闀垮害: %d", len(accountData)) // 灏嗗璞¤浆鎹负鏁扮粍鏍煎紡 accounts := make([]map[string]interface{}, 0, len(accountData)) for key, account := range accountData { //globalLogger.Debug("[GetWxWorkAccountList] 澶勭悊璐﹀彿key: %s, account: %+v", key, account) if accountMap, ok := account.(map[string]interface{}); ok { if accountMap["user_id"] == nil { accountMap["user_id"] = key // 浣跨敤key浣滀负user_id } if accountMap["username"] == nil { accountMap["username"] = accountMap["user_id"] } if accountMap["avatar"] == nil { accountMap["avatar"] = "" } if accountMap["status"] == nil { accountMap["status"] = 0 } if accountMap["corp_short_name"] == nil { accountMap["corp_short_name"] = "" } //globalLogger.Debug("[GetWxWorkAccountList] 澶勭悊鍚庤处鍙? %+v", accountMap) accounts = append(accounts, accountMap) } } if recognizedUsers, helperOK := a.getRecognizedWxWorkUsers(); helperOK { filtered := make([]map[string]interface{}, 0, len(accounts)) for _, account := range accounts { userID := strings.TrimSpace(fmt.Sprint(account["user_id"])) if recognizedUsers[userID] { account["status"] = 1 filtered = append(filtered, account) } } accounts = filtered } if runtimeAccounts, helperOK := a.getRuntimeWxWorkAccounts(); helperOK { seen := make(map[string]bool) for _, account := range accounts { userID := strings.TrimSpace(fmt.Sprint(account["user_id"])) if userID != "" && userID != "" { seen[userID] = true } } for _, account := range runtimeAccounts { userID := strings.TrimSpace(fmt.Sprint(account["user_id"])) if userID == "" || seen[userID] { continue } accounts = append(accounts, account) seen[userID] = true } } duration := time.Since(startTime).Milliseconds() globalLogger.Info("[GetWxWorkAccountList] 鎴愬姛鑾峰彇 %d 涓紒寰处鍙?- 鑰楁椂: %d ms", len(accounts), duration) a.AddLogEntry("App", "info", fmt.Sprintf("successfully loaded %d wxwork accounts", len(accounts)), duration) diagnostic := "" if len(accounts) == 0 { diagnostic = a.getWxWorkAccountDiagnostic() } return map[string]interface{}{ "success": true, "error": "", "diagnostic": diagnostic, "data": accounts, } } func (a *App) getRecognizedWxWorkUsers() (map[string]bool, bool) { result, err := a.getHelperJSON("/api/debug/clients") if err != nil { return nil, false } data, _ := result["data"].(map[string]interface{}) if data == nil { return nil, false } users := make(map[string]bool) clients, _ := data["clients"].([]interface{}) for _, item := range clients { client, _ := item.(map[string]interface{}) if client == nil { continue } userID := strings.TrimSpace(fmt.Sprint(client["userId"])) status := strings.TrimSpace(fmt.Sprint(client["status"])) if userID != "" && userID != "" && status == "identified" { users[userID] = true } } return users, true } func (a *App) getRuntimeWxWorkAccounts() ([]map[string]interface{}, bool) { result, err := a.getHelperJSON("/api/debug/clients") if err != nil { return nil, false } data, _ := result["data"].(map[string]interface{}) if data == nil { return nil, false } accounts := make([]map[string]interface{}, 0) clients, _ := data["clients"].([]interface{}) for _, item := range clients { client, _ := item.(map[string]interface{}) if client == nil { continue } status := strings.TrimSpace(fmt.Sprint(client["status"])) if status != "identified" && status != "message_ready" { continue } clientID := intFromInterface(client["clientId"]) userID := strings.TrimSpace(fmt.Sprint(client["userId"])) runtimeOnly := false if userID == "" || userID == "" { userID = fmt.Sprintf("client:%d", clientID) runtimeOnly = true } username := userID if runtimeOnly { username = fmt.Sprintf("鏈瘑鍒处鍙?client %d", clientID) } accounts = append(accounts, map[string]interface{}{ "user_id": userID, "username": username, "avatar": "", "status": 1, "corp_short_name": "", "client_id": clientID, "pid": intFromInterface(client["pid"]), "runtime_status": status, "runtime_only": runtimeOnly, "health_state": strings.TrimSpace(fmt.Sprint(client["healthState"])), "health_message": strings.TrimSpace(fmt.Sprint(client["healthMessage"])), "first_message_at": strings.TrimSpace(fmt.Sprint(client["firstMessageAt"])), "last_message_at": strings.TrimSpace(fmt.Sprint(client["lastMessageAt"])), "message_count": intFromInterface(client["messageCount"]), }) } return accounts, true } func (a *App) getWxWorkAccountDiagnostic() string { result, err := a.getHelperJSON("/api/debug/clients") if err != nil { return "No WeCom account yet: helper diagnostics API is unavailable. Please start WeCom first." } data, _ := result["data"].(map[string]interface{}) if data == nil { return "No WeCom account yet: helper diagnostics data is empty." } if version, _ := data["version"].(map[string]interface{}); version != nil { compatible, _ := version["compatible"].(bool) message := strings.TrimSpace(fmt.Sprint(version["message"])) wxWorkVersion := strings.TrimSpace(fmt.Sprint(version["wxWorkVersion"])) helperVersion := strings.TrimSpace(fmt.Sprint(version["helperVersion"])) if !compatible && message != "" { return fmt.Sprintf("Connected to WeCom, but account is not recognized. WeCom version %s does not match Helper/Loader %s. Put Helper_%s.dll and Loader_%s.dll into build/bin, or install the matching WeCom version.", wxWorkVersion, helperVersion, wxWorkVersion, wxWorkVersion) } } recognized := intFromInterface(data["recognizedClientCount"]) usable := intFromInterface(data["usableClientCount"]) unidentified := intFromInterface(data["unidentifiedClientCount"]) connections := intFromInterface(data["connectionCount"]) if recognized > 0 { return "" } if usable > 0 { return "" } if unidentified > 0 { return fmt.Sprintf("Connected to %d WeCom process(es), but account info is not recognized yet. Check dashboard account recognition status.", unidentified) } if connections > 0 { return "WeCom connection exists, but account info has not been recognized yet." } return "No WeCom account connection received yet. Please click start WeCom first." } func intFromInterface(value interface{}) int { switch v := value.(type) { case int: return v case int32: return int(v) case int64: return int(v) case float64: return int(v) case json.Number: n, _ := strconv.Atoi(v.String()) return n default: return 0 } } func (a *App) DeleteWxWorkAccount(userID string) string { startTime := time.Now() startTimestamp := startTime.Format("2006-01-02 15:04:05.000") globalLogger.Info("[DeleteWxWorkAccount] 寮€濮嬪垹闄や紒寰处鍙? %s - 鏃堕棿: %s", userID, startTimestamp) exePath, err := os.Executable() if err != nil { globalLogger.Error("[DeleteWxWorkAccount] 鑾峰彇鍙墽琛屾枃浠惰矾寰勫け璐? %v", err) a.AddLogEntry("App", "error", "get executable path failed", time.Since(startTime).Milliseconds()) return "get executable path failed" } // 鏋勫缓client_status.json鏂囦欢璺緞 exeDir := filepath.Dir(exePath) configPath := filepath.Join(exeDir, "config", "client_status.json") globalLogger.Info("[DeleteWxWorkAccount] 灏濊瘯鍒犻櫎鏂囦欢: %s 涓殑璐﹀彿: %s", configPath, userID) if _, err := os.Stat(configPath); os.IsNotExist(err) { globalLogger.Info("[DeleteWxWorkAccount] 鏂囦欢涓嶅瓨鍦? %s", configPath) return "config file does not exist" } // 璇诲彇鏂囦欢鍐呭 data, err := ioutil.ReadFile(configPath) if err != nil { globalLogger.Error("[DeleteWxWorkAccount] 璇诲彇鏂囦欢澶辫触: %v", err) a.AddLogEntry("App", "error", fmt.Sprintf("璇诲彇浼佸井璐﹀彿閰嶇疆鏂囦欢澶辫触: %v", err), time.Since(startTime).Milliseconds()) return fmt.Sprintf("璇诲彇閰嶇疆鏂囦欢澶辫触: %v", err) } // 瑙f瀽JSON鏁版嵁 var accountData map[string]interface{} if err := json.Unmarshal(data, &accountData); err != nil { globalLogger.Error("[DeleteWxWorkAccount] 瑙f瀽JSON澶辫触: %v", err) a.AddLogEntry("App", "error", fmt.Sprintf("瑙f瀽浼佸井璐﹀彿閰嶇疆澶辫触: %v", err), time.Since(startTime).Milliseconds()) return fmt.Sprintf("瑙f瀽閰嶇疆鏂囦欢澶辫触: %v", err) } // 妫€鏌ユ槸鍚﹀瓨鍦ㄨuserID if _, exists := accountData[userID]; !exists { globalLogger.Info("[DeleteWxWorkAccount] 璐﹀彿涓嶅瓨鍦? %s", userID) return "account does not exist" } delete(accountData, userID) globalLogger.Info("[DeleteWxWorkAccount] 宸插垹闄よ处鍙? %s", userID) updatedData, err := json.MarshalIndent(accountData, "", " ") if err != nil { globalLogger.Error("[DeleteWxWorkAccount] 搴忓垪鍖朖SON澶辫触: %v", err) a.AddLogEntry("App", "error", fmt.Sprintf("搴忓垪鍖栭厤缃け璐? %v", err), time.Since(startTime).Milliseconds()) return fmt.Sprintf("淇濆瓨閰嶇疆鏂囦欢澶辫触: %v", err) } // 鍐欏叆鏂囦欢 if err := ioutil.WriteFile(configPath, updatedData, 0644); err != nil { globalLogger.Error("[DeleteWxWorkAccount] 鍐欏叆鏂囦欢澶辫触: %v", err) a.AddLogEntry("App", "error", fmt.Sprintf("鍐欏叆閰嶇疆鏂囦欢澶辫触: %v", err), time.Since(startTime).Milliseconds()) return fmt.Sprintf("淇濆瓨閰嶇疆鏂囦欢澶辫触: %v", err) } duration := time.Since(startTime).Milliseconds() globalLogger.Info("[DeleteWxWorkAccount] 鎴愬姛鍒犻櫎璐﹀彿: %s - 鑰楁椂: %d ms", userID, duration) a.AddLogEntry("App", "info", fmt.Sprintf("鎴愬姛鍒犻櫎璐﹀彿: %s", userID), duration) return "鍒犻櫎鎴愬姛" }