release: v1.1.0
移除 V1 死代码(6 个导出方法、4 个内部函数、~300 行), 新增 cache.go/ai.go/matcher.go/export.go 拆分 app.go(原 1645 行), 修复 AICache 3 个并发 bug(TOCTOU、反序列化、全排序), 提取 8 个命名常量,前端添加 isRunning 守卫和 CSS 变量。 Go 升级至 1.24.0。
This commit is contained in:
136
cache.go
Normal file
136
cache.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ---------- AI 缓存 ----------
|
||||
|
||||
// AICacheEntry 单条缓存记录
|
||||
type AICacheEntry struct {
|
||||
PromptHash string `json:"promptHash"`
|
||||
Response string `json:"response"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
// AICache AI 响应缓存(持久化到临时文件)
|
||||
type AICache struct {
|
||||
Entries []AICacheEntry `json:"entries"`
|
||||
mu sync.RWMutex // 小写,必须保持非导出以兼容 JSON 序列化
|
||||
filePath string
|
||||
maxSize int // 最大缓存条目数
|
||||
}
|
||||
|
||||
// cacheFileName 缓存文件名
|
||||
const cacheFileName = "data-matcher-ai-cache.json"
|
||||
|
||||
// newAICache 创建缓存实例并加载已有数据
|
||||
func newAICache() *AICache {
|
||||
c := &AICache{
|
||||
filePath: filepath.Join(os.TempDir(), cacheFileName),
|
||||
maxSize: cacheMaxSize,
|
||||
}
|
||||
c.loadFromFile()
|
||||
return c
|
||||
}
|
||||
|
||||
// loadFromFile 从磁盘加载缓存
|
||||
func (c *AICache) loadFromFile() {
|
||||
data, err := os.ReadFile(c.filePath)
|
||||
if err != nil {
|
||||
return // 文件不存在或无法读取,从空缓存开始
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var loaded AICache
|
||||
if err := json.Unmarshal(data, &loaded); err != nil || len(loaded.Entries) == 0 {
|
||||
return // 解析失败或无数据,保留当前缓存
|
||||
}
|
||||
// 验证每个条目字段完整性
|
||||
for _, e := range loaded.Entries {
|
||||
if e.PromptHash == "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Entries = loaded.Entries
|
||||
}
|
||||
|
||||
// saveToFile 将缓存写入磁盘(线程安全)
|
||||
func (c *AICache) saveToFile() {
|
||||
c.mu.RLock()
|
||||
// 仅序列化 Entries,避免泄露 filePath 等内部字段
|
||||
entries := make([]AICacheEntry, len(c.Entries))
|
||||
copy(entries, c.Entries)
|
||||
c.mu.RUnlock()
|
||||
|
||||
data, err := json.Marshal(entries)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(c.filePath, data, 0644)
|
||||
}
|
||||
|
||||
// get 根据 hash 查找缓存,命中返回响应,否则返回空
|
||||
func (c *AICache) get(hash string) (string, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
for i := range c.Entries {
|
||||
if c.Entries[i].PromptHash == hash {
|
||||
return c.Entries[i].Response, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// put 存入一条缓存(线程安全 + 自动裁剪)
|
||||
func (c *AICache) put(hash, response string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// 去重:如果已存在则覆盖
|
||||
for i := range c.Entries {
|
||||
if c.Entries[i].PromptHash == hash {
|
||||
c.Entries[i].Response = response
|
||||
c.Entries[i].CreatedAt = time.Now().Unix()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 超过上限则移除最旧条目,避免全部排序
|
||||
if len(c.Entries) >= c.maxSize {
|
||||
oldestIdx := 0
|
||||
oldestTime := c.Entries[0].CreatedAt
|
||||
for i := 1; i < len(c.Entries); i++ {
|
||||
if c.Entries[i].CreatedAt < oldestTime {
|
||||
oldestIdx = i
|
||||
oldestTime = c.Entries[i].CreatedAt
|
||||
}
|
||||
}
|
||||
c.Entries = append(c.Entries[:oldestIdx], c.Entries[oldestIdx+1:]...)
|
||||
}
|
||||
|
||||
c.Entries = append(c.Entries, AICacheEntry{
|
||||
PromptHash: hash,
|
||||
Response: response,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// clear 清空所有缓存
|
||||
func (c *AICache) clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.Entries = nil
|
||||
_ = os.Remove(c.filePath)
|
||||
}
|
||||
|
||||
// stat 返回缓存统计
|
||||
func (c *AICache) stat() (count int, path string) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.Entries), c.filePath
|
||||
}
|
||||
Reference in New Issue
Block a user