Initial qiwei secondary development handoff

This commit is contained in:
2026-06-23 21:11:20 +08:00
commit 858cb68f4f
207 changed files with 52782 additions and 0 deletions

324
operation_record.go Normal file
View File

@@ -0,0 +1,324 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"unicode/utf8"
"golang.org/x/text/encoding/simplifiedchinese"
"qiweimanager/logger"
)
var operationLogFileMu sync.Mutex
func SaveLogEntry(logDir string, entry logger.LogEntry) error {
operationLogFileMu.Lock()
defer operationLogFileMu.Unlock()
entry = normalizeOperationLogEntry(entry)
operationLogDir := filepath.Join(logDir, "operations")
if err := os.MkdirAll(operationLogDir, 0755); err != nil {
return fmt.Errorf("无法创建操作日志目录: %v", err)
}
logFilePath := filepath.Join(operationLogDir, fmt.Sprintf("%s_operations.json", time.Now().Format("2006-01-02")))
logEntries := []logger.LogEntry{}
if info, err := os.Stat(logFilePath); err == nil && info.Size() > 0 {
byteValue, err := ioutil.ReadFile(logFilePath)
if err != nil {
return fmt.Errorf("读取日志文件失败: %v", err)
}
entries, repaired, err := parseOperationLogData(byteValue)
if err != nil {
if renameErr := quarantineCorruptOperationLog(logFilePath); renameErr != nil {
return fmt.Errorf("解析日志文件失败: %v备份损坏文件失败: %v", err, renameErr)
}
logEntries = []logger.LogEntry{}
} else {
logEntries = entries
if repaired {
_ = backupOperationLog(logFilePath, "repaired")
}
}
}
logEntries = append(logEntries, entry)
if len(logEntries) > 1000 {
logEntries = logEntries[len(logEntries)-1000:]
}
jsonData, err := json.MarshalIndent(logEntries, "", " ")
if err != nil {
return fmt.Errorf("序列化日志失败: %v", err)
}
if err := writeFileAtomically(logFilePath, jsonData, 0644); err != nil {
return fmt.Errorf("写入日志文件失败: %v", err)
}
go manageLogFiles(operationLogDir)
return nil
}
func LoadLogEntries(logDir string, page, pageSize int, logType string) ([]logger.LogEntry, int, error) {
operationLogFileMu.Lock()
defer operationLogFileMu.Unlock()
operationLogDir := filepath.Join(logDir, "operations")
if _, err := os.Stat(operationLogDir); os.IsNotExist(err) {
return []logger.LogEntry{}, 0, nil
}
files, err := ioutil.ReadDir(operationLogDir)
if err != nil {
return nil, 0, fmt.Errorf("读取日志目录失败: %v", err)
}
sort.Slice(files, func(i, j int) bool {
return files[i].ModTime().After(files[j].ModTime())
})
var allEntries []logger.LogEntry
for _, file := range files {
if !strings.HasSuffix(file.Name(), ".json") || strings.Contains(file.Name(), ".corrupt.") {
continue
}
filePath := filepath.Join(operationLogDir, file.Name())
byteValue, err := ioutil.ReadFile(filePath)
if err != nil {
continue
}
entries, repaired, err := parseOperationLogData(byteValue)
if err != nil {
_ = quarantineCorruptOperationLog(filePath)
continue
}
if repaired {
_ = backupOperationLog(filePath, "repaired")
if data, marshalErr := json.MarshalIndent(entries, "", " "); marshalErr == nil {
_ = writeFileAtomically(filePath, data, 0644)
}
}
allEntries = append(allEntries, entries...)
}
sort.Slice(allEntries, func(i, j int) bool {
return allEntries[i].ID > allEntries[j].ID
})
if logType != "all" {
filteredEntries := make([]logger.LogEntry, 0, len(allEntries))
for _, entry := range allEntries {
if entry.Type == logType {
filteredEntries = append(filteredEntries, entry)
}
}
allEntries = filteredEntries
}
totalCount := len(allEntries)
start := (page - 1) * pageSize
end := start + pageSize
if start >= totalCount {
return []logger.LogEntry{}, totalCount, nil
}
if end > totalCount {
end = totalCount
}
return allEntries[start:end], totalCount, nil
}
func loadOperationLogEntriesFromFile(filePath string) ([]logger.LogEntry, error) {
operationLogFileMu.Lock()
defer operationLogFileMu.Unlock()
data, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}
entries, repaired, err := parseOperationLogData(data)
if err != nil {
_ = quarantineCorruptOperationLog(filePath)
return nil, err
}
if repaired {
_ = backupOperationLog(filePath, "repaired")
if clean, marshalErr := json.MarshalIndent(entries, "", " "); marshalErr == nil {
_ = writeFileAtomically(filePath, clean, 0644)
}
}
return entries, nil
}
func parseOperationLogData(data []byte) ([]logger.LogEntry, bool, error) {
var entries []logger.LogEntry
if err := json.Unmarshal(data, &entries); err == nil {
return normalizeOperationLogEntries(entries), operationLogEntriesNeedRepair(entries), nil
}
trimmed := bytes.TrimSpace(data)
lastArrayEnd := bytes.LastIndexByte(trimmed, ']')
if lastArrayEnd < 0 {
return nil, false, fmt.Errorf("日志文件不是有效JSON数组")
}
recovered := trimmed[:lastArrayEnd+1]
if err := json.Unmarshal(recovered, &entries); err != nil {
return nil, false, err
}
return normalizeOperationLogEntries(entries), true, nil
}
func normalizeOperationLogEntries(entries []logger.LogEntry) []logger.LogEntry {
out := make([]logger.LogEntry, len(entries))
for i, entry := range entries {
out[i] = normalizeOperationLogEntry(entry)
}
return out
}
func normalizeOperationLogEntry(entry logger.LogEntry) logger.LogEntry {
entry.Source = repairMojibakeText(entry.Source)
entry.Type = repairMojibakeText(entry.Type)
entry.Content = repairMojibakeText(entry.Content)
return entry
}
func operationLogEntriesNeedRepair(entries []logger.LogEntry) bool {
for _, entry := range entries {
if looksLikeMojibake(entry.Source) || looksLikeMojibake(entry.Type) || looksLikeMojibake(entry.Content) {
return true
}
}
return false
}
func repairMojibakeText(text string) string {
if !looksLikeMojibake(text) {
return text
}
replaced := replaceKnownOperationMojibake(text)
if mojibakeScore(replaced) < mojibakeScore(text) {
text = replaced
}
encoded, err := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(text))
if err != nil || !utf8.Valid(encoded) {
return text
}
repaired := string(encoded)
if mojibakeScore(repaired) < mojibakeScore(text) {
return repaired
}
return text
}
func replaceKnownOperationMojibake(text string) string {
replacements := map[string]string{
"绋嬪簭鍒濆鎴愬姛": "程序初始成功",
"HTTP瀹㈡埛绔垵濮嬪寲瀹屾垚锛岀鍙?": "HTTP客户端初始化完成端口:",
"鍙戦€佽姹傚埌杈呭姪绋嬪簭锛屾秷鎭被鍨?": "发送请求到辅助程序,消息类型:",
"璋冪敤杈呭姪绋嬪簭鎴愬姛锛屾秷鎭被鍨?": "调用辅助程序成功,消息类型:",
"璋冪敤杈呭姪绋嬪簭杩斿洖澶辫触锛屾秷鎭被鍨?": "调用辅助程序返回失败,消息类型:",
"璋冪敤杈呭姪绋嬪簭澶辫触": "调用辅助程序失败",
"閲嶆柊璋冪敤杈呭姪绋嬪簭澶辫触": "重新调用辅助程序失败",
"鎿嶄綔鏃ュ織璋冭瘯璇锋眰瀹屾垚": "操作日志调试请求完成",
"鎴愬姛鑾峰彇": "成功获取",
"涓紒寰处鍙?": "个企微账号",
"浼佷笟寰俊鏈嶅姟杩炴帴鎴愬姛": "企业微信服务连接成功",
"鍐呭瓨浣跨敤鐜囪秴杩?0%": "内存使用率超过80%",
"鎴愬姛鍒犻櫎璐﹀彿": "成功删除账号",
}
for old, replacement := range replacements {
text = strings.ReplaceAll(text, old, replacement)
}
return text
}
func looksLikeMojibake(text string) bool {
return mojibakeScore(text) >= 2
}
func mojibakeScore(text string) int {
score := 0
for _, marker := range []string{"锛", "涓", "绋", "鎴", "鍙", "杈", "濂", "瀹", "熷", "€", "<22>"} {
score += strings.Count(text, marker)
}
return score
}
func writeFileAtomically(path string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(path)
tmp, err := ioutil.TempFile(dir, filepath.Base(path)+".tmp-*")
if err != nil {
return err
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := os.Chmod(tmpPath, perm); err != nil {
return err
}
_ = os.Remove(path)
return os.Rename(tmpPath, path)
}
func quarantineCorruptOperationLog(path string) error {
if strings.TrimSpace(path) == "" {
return nil
}
if _, err := os.Stat(path); err != nil {
return err
}
ext := filepath.Ext(path)
base := strings.TrimSuffix(path, ext)
if ext == "" {
ext = ".json"
}
target := fmt.Sprintf("%s.corrupt.%s%s", base, time.Now().Format("20060102_150405_000000000"), ext)
return os.Rename(path, target)
}
func backupOperationLog(path string, label string) error {
if strings.TrimSpace(path) == "" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
ext := filepath.Ext(path)
base := strings.TrimSuffix(path, ext)
if ext == "" {
ext = ".json"
}
target := fmt.Sprintf("%s.%s.%s%s", base, label, time.Now().Format("20060102_150405_000000000"), ext)
return os.WriteFile(target, data, 0644)
}
func manageLogFiles(operationLogDir string) {
files, err := ioutil.ReadDir(operationLogDir)
if err != nil {
return
}
sevenDaysAgo := time.Now().AddDate(0, 0, -7)
for _, file := range files {
if file.ModTime().Before(sevenDaysAgo) {
_ = os.Remove(filepath.Join(operationLogDir, file.Name()))
}
}
}