mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-07-03 12:28:10 +08:00
P0 — Feishu platform concurrency safety (platform/feishu/feishu.go): - Add sync.RWMutex to Platform struct - Protect botOpenID, handler, cancel, server fields with getter helpers - Fix TOCTOU race in Stop() by capturing cancel/server under lock P2 — Log silently discarded errors (5 locations): - core/engine.go: cron EnableJob/DisableJob, outgoing rate limit wait - platform/telegram/telegram.go: invalid thread ID parsing - agent/claudecode/session.go: stdin close, SIGTERM, force kill failures - agent/claudecode/claude_usage.go: ptmx close, process kill failures P4 — Windows path hardening (2 locations): - core/message.go: normalize backslashes with filepath.ToSlash + strings.ReplaceAll for cross-platform defense in depth - core/dir_history.go: tighten MkdirAll permissions from 0755 to 0700 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
168 lines
3.5 KiB
Go
168 lines
3.5 KiB
Go
package core
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
defaultDirHistorySize = 10
|
|
dirHistoryFileName = "dir_history.json"
|
|
)
|
|
|
|
// DirHistory manages directory switch history per project.
|
|
type DirHistory struct {
|
|
mu sync.RWMutex
|
|
storePath string
|
|
entries map[string][]string // project name -> dir list (most recent first)
|
|
maxSize int
|
|
}
|
|
|
|
// NewDirHistory creates a new DirHistory with the given data directory.
|
|
func NewDirHistory(dataDir string) *DirHistory {
|
|
dh := &DirHistory{
|
|
storePath: filepath.Join(dataDir, dirHistoryFileName),
|
|
entries: make(map[string][]string),
|
|
maxSize: defaultDirHistorySize,
|
|
}
|
|
dh.load()
|
|
return dh
|
|
}
|
|
|
|
// Add adds a directory to the history for the given project.
|
|
// If the directory already exists, it's moved to the front.
|
|
func (dh *DirHistory) Add(project, dir string) {
|
|
if dir == "" {
|
|
return
|
|
}
|
|
|
|
dh.mu.Lock()
|
|
defer dh.mu.Unlock()
|
|
|
|
entries := dh.entries[project]
|
|
|
|
// Remove if exists
|
|
for i, d := range entries {
|
|
if d == dir {
|
|
entries = append(entries[:i], entries[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Add to front
|
|
entries = append([]string{dir}, entries...)
|
|
|
|
// Trim to max size
|
|
if len(entries) > dh.maxSize {
|
|
entries = entries[:dh.maxSize]
|
|
}
|
|
|
|
dh.entries[project] = entries
|
|
dh.saveLocked()
|
|
}
|
|
|
|
// List returns the history for the given project.
|
|
func (dh *DirHistory) List(project string) []string {
|
|
dh.mu.RLock()
|
|
defer dh.mu.RUnlock()
|
|
|
|
entries := dh.entries[project]
|
|
if entries == nil {
|
|
return nil
|
|
}
|
|
|
|
// Return a copy
|
|
result := make([]string, len(entries))
|
|
copy(result, entries)
|
|
return result
|
|
}
|
|
|
|
// Get returns the directory at the given index (1-based) for the project.
|
|
// Returns empty string if index is out of range.
|
|
func (dh *DirHistory) Get(project string, index int) string {
|
|
dh.mu.RLock()
|
|
defer dh.mu.RUnlock()
|
|
|
|
entries := dh.entries[project]
|
|
if index < 1 || index > len(entries) {
|
|
return ""
|
|
}
|
|
return entries[index-1]
|
|
}
|
|
|
|
// Previous returns the previous directory (index 2, since index 1 is current).
|
|
func (dh *DirHistory) Previous(project string) string {
|
|
return dh.Get(project, 2)
|
|
}
|
|
|
|
// Contains checks if a directory is in the history for the given project.
|
|
func (dh *DirHistory) Contains(project, dir string) bool {
|
|
dh.mu.RLock()
|
|
defer dh.mu.RUnlock()
|
|
|
|
entries := dh.entries[project]
|
|
for _, d := range entries {
|
|
if d == dir {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SetMaxSize sets the maximum history size.
|
|
func (dh *DirHistory) SetMaxSize(size int) {
|
|
if size < 1 {
|
|
size = 1
|
|
}
|
|
dh.mu.Lock()
|
|
defer dh.mu.Unlock()
|
|
dh.maxSize = size
|
|
}
|
|
|
|
func (dh *DirHistory) load() {
|
|
if dh.storePath == "" {
|
|
return
|
|
}
|
|
|
|
data, err := os.ReadFile(dh.storePath)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
slog.Error("dir_history: failed to read", "path", dh.storePath, "error", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
var entries map[string][]string
|
|
if err := json.Unmarshal(data, &entries); err != nil {
|
|
slog.Error("dir_history: failed to unmarshal", "path", dh.storePath, "error", err)
|
|
return
|
|
}
|
|
|
|
if entries != nil {
|
|
dh.entries = entries
|
|
}
|
|
}
|
|
|
|
func (dh *DirHistory) saveLocked() {
|
|
if dh.storePath == "" {
|
|
return
|
|
}
|
|
|
|
data, err := json.MarshalIndent(dh.entries, "", " ")
|
|
if err != nil {
|
|
slog.Error("dir_history: failed to marshal", "error", err)
|
|
return
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(dh.storePath), 0700); err != nil {
|
|
slog.Error("dir_history: failed to create dir", "error", err)
|
|
return
|
|
}
|
|
|
|
if err := AtomicWriteFile(dh.storePath, data, 0644); err != nil {
|
|
slog.Error("dir_history: failed to write", "path", dh.storePath, "error", err)
|
|
}
|
|
} |