package main import ( "fmt" "os" "path/filepath" "strings" "sync" "time" ) // Logger 简单文件日志(windowsgui 子系统无控制台,统一落盘)。 // 所有写出内容都经过 Redact:访问 token 不会明文落盘。 type Logger struct { mu sync.Mutex file *os.File path string } func NewLogger() *Logger { dir := logDir() _ = os.MkdirAll(dir, 0o755) p := filepath.Join(dir, "tray-"+time.Now().Format("20060102")+".log") f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) if err != nil { f = nil } return &Logger{file: f, path: p} } func (l *Logger) Printf(format string, args ...any) { l.mu.Lock() defer l.mu.Unlock() body := Redact(fmt.Sprintf(format, args...)) line := fmt.Sprintf("[%s] %s\n", time.Now().Format("2006-01-02 15:04:05"), body) if l.file != nil { _, _ = l.file.WriteString(line) } } func (l *Logger) Path() string { return l.path } func (l *Logger) Close() { l.mu.Lock() defer l.mu.Unlock() if l.file != nil { _ = l.file.Close() l.file = nil } } // Redact 脱敏访问 token(token=xxxx -> token=***),避免凭据明文落盘。 func Redact(s string) string { const marker = "token=" var b strings.Builder for { i := strings.Index(s, marker) if i < 0 { b.WriteString(s) return b.String() } end := i + len(marker) b.WriteString(s[:end]) b.WriteString("***") j := end for j < len(s) && !isTokenTerminator(s[j]) { j++ } s = s[j:] } } func isTokenTerminator(c byte) bool { switch c { case ' ', '\t', '\r', '\n', '"', '\'', '&', '<', '>', '#', '?': return true } return false }