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:
sakuradairong
2026-06-05 14:40:55 +08:00
parent b3ec20fd77
commit 40745f5632
9 changed files with 723 additions and 1162 deletions

136
cache.go Normal file
View 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
}