diff --git a/.gitignore b/.gitignore index be14f9c..2be89a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,27 @@ -# --- Build output --- -build/bin -build/*.exe -build/*.app +# === 构建产物 === +build/ -# --- Frontend --- -node_modules/ -frontend/dist -frontend/node_modules/ -frontend/package-lock.json - -# --- Go --- -*.exe -*.exe~ -*.dll -*.so -*.dylib +# === Go === +vendor/ *.test *.out -vendor/ -# --- IDE --- +# === IDE / 编辑器 === .idea/ *.swp *.swo *~ + +# === 操作系统 === .DS_Store Thumbs.db -# VS Code(保留 .vscode/extensions.json 以便共享推荐) -.vscode/* -!.vscode/extensions.json +# === 前端 === +frontend/node_modules/ +frontend/dist/ + +# === Wails 运行时(开发模式临时文件)=== +# 注意:frontend/wailsjs/ 是 Wails 自动生成的绑定代码,必须提交 + +# === AI 缓存(本地临时文件,不提交)=== +data-matcher-ai-cache.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5ef6ae1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "editor.formatOnSave": true, + "editor.tabSize": 2, + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "[go]": { + "editor.tabSize": 4, + "editor.insertSpaces": false, + "editor.formatOnSave": true + }, + "[vue]": { + "editor.tabSize": 2, + "editor.formatOnSave": true + }, + "search.exclude": { + "frontend/dist": true, + "build": true, + "node_modules": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..7365e38 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "dev (wails dev)", + "type": "shell", + "command": "wails dev", + "isBackground": true, + "problemMatcher": [], + "group": "build" + } + ] +} \ No newline at end of file diff --git a/app.go b/app.go index 2174bba..8918f7c 100644 --- a/app.go +++ b/app.go @@ -27,20 +27,12 @@ import ( // MatchResult 匹配结果 type MatchResult struct { - // 新字段(通用化) - RowAData []string `json:"rowAData"` // A 表原始所有列(新) - RowBKey string `json:"rowBKey"` // B 表匹配列的值(新) - ExtractValue string `json:"extractValue"` // 从 B 表提取的目标列值(新) - - // 旧字段(向后兼容) - MonthlyCellName string `json:"monthlyCellName"` - DailyCellID string `json:"dailyCellId"` - InterruptReason string `json:"interruptReason"` - - // 公共字段 - TimeDiff string `json:"timeDiff"` + RowAData []string `json:"rowAData"` // A 表原始所有列 + RowBKey string `json:"rowBKey"` // B 表匹配列的值 + ExtractValue string `json:"extractValue"` // 从 B 表提取的目标列值 + TimeDiff string `json:"timeDiff"` SimilarityScore float64 `json:"similarityScore"` - AIMatched bool `json:"aiMatched"` + AIMatched bool `json:"aiMatched"` } // ProgressPayload 进度信息 @@ -113,19 +105,30 @@ type AICacheInfo struct { FilePath string `json:"filePath"` } -// AICacheEntry 单条缓存记录 +// AICacheEntry 单条缓存记录(批量 prompt → 响应) type AICacheEntry struct { PromptHash string `json:"promptHash"` Response string `json:"response"` CreatedAt int64 `json:"createdAt"` } +// AIRowCacheEntry 单行 AI 匹配缓存(跨批次复用) +type AIRowCacheEntry struct { + Key string `json:"key"` + Value string `json:"value"` // AI 匹配到的 extractValue + CreatedAt int64 `json:"createdAt"` +} + // AICache AI 响应缓存(持久化到临时文件) type AICache struct { - Entries []AICacheEntry `json:"entries"` - mu sync.RWMutex // 小写,必须保持非导出以兼容 JSON 序列化 - filePath string - maxSize int // 最大缓存条目数 + Entries []AICacheEntry `json:"entries"` + RowEntries []AIRowCacheEntry `json:"rowEntries"` + entriesIdx map[string]int // promptHash → index in Entries (O(1) lookup) + rowEntriesIdx map[string]int // key → index in RowEntries (O(1) lookup) + mu sync.RWMutex + filePath string + maxSize int // 批量缓存最大条目数 + maxRowSize int // 行级缓存最大条目数 } // cacheFileName 缓存文件名 @@ -139,13 +142,26 @@ const ( defaultMaxPreview = 3 defaultMaxBNoTime = 200 defaultAIWindowPadH = 3.0 + maxPromptBChars = 80000 // B 表数据在 prompt 中的最大字符数 ) +// rebuildIndexes 从切片重建索引 map(反序列化或裁剪后调用) +func (c *AICache) rebuildIndexes() { + c.entriesIdx = make(map[string]int, len(c.Entries)) + for i := range c.Entries { + c.entriesIdx[c.Entries[i].PromptHash] = i + } + c.rowEntriesIdx = make(map[string]int, len(c.RowEntries)) + for i := range c.RowEntries { + c.rowEntriesIdx[c.RowEntries[i].Key] = i + } +} // newAICache 创建缓存实例并加载已有数据 func newAICache() *AICache { c := &AICache{ - filePath: filepath.Join(os.TempDir(), cacheFileName), - maxSize: 500, + filePath: filepath.Join(os.TempDir(), cacheFileName), + maxSize: 500, + maxRowSize: 5000, } c.loadFromFile() return c @@ -155,11 +171,20 @@ func newAICache() *AICache { func (c *AICache) loadFromFile() { data, err := os.ReadFile(c.filePath) if err != nil { - return // 文件不存在或无法读取,从空缓存开始 + // 文件不存在或无法读取,从空缓存开始 + if !os.IsNotExist(err) { + fmt.Printf("[CACHE] 读取缓存文件失败: %v\n", err) + } + return } c.mu.Lock() defer c.mu.Unlock() - _ = json.Unmarshal(data, c) // 忽略解析错误,重置为 entries + if err := json.Unmarshal(data, c); err != nil { + fmt.Printf("[CACHE] 解析缓存文件失败,重置为空: %v\n", err) + c.Entries = nil + c.RowEntries = nil + } + c.rebuildIndexes() } // saveToFile 将缓存写入磁盘 @@ -168,19 +193,20 @@ func (c *AICache) saveToFile() { data, err := json.Marshal(c) c.mu.RUnlock() if err != nil { + fmt.Printf("[CACHE] 序列化缓存失败: %v\n", err) return } - _ = os.WriteFile(c.filePath, data, 0600) + if err := os.WriteFile(c.filePath, data, 0600); err != nil { + fmt.Printf("[CACHE] 写入缓存文件失败: %v\n", err) + } } // 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 - } + if idx, ok := c.entriesIdx[hash]; ok && idx < len(c.Entries) && c.Entries[idx].PromptHash == hash { + return c.Entries[idx].Response, true } return "", false } @@ -191,27 +217,28 @@ func (c *AICache) put(hash, response string) { 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 idx, ok := c.entriesIdx[hash]; ok && idx < len(c.Entries) && c.Entries[idx].PromptHash == hash { + c.Entries[idx].Response = response + c.Entries[idx].CreatedAt = time.Now().Unix() + return } + // 新增条目 + idx := len(c.Entries) c.Entries = append(c.Entries, AICacheEntry{ PromptHash: hash, Response: response, CreatedAt: time.Now().Unix(), }) + c.entriesIdx[hash] = idx // 超过上限则删除最旧的条目 if len(c.Entries) > c.maxSize { - // 按 CreatedAt 排序保留最新的 sort.Slice(c.Entries, func(i, j int) bool { return c.Entries[i].CreatedAt > c.Entries[j].CreatedAt }) c.Entries = c.Entries[:c.maxSize] + c.rebuildIndexes() } } @@ -220,6 +247,9 @@ func (c *AICache) clear() { c.mu.Lock() defer c.mu.Unlock() c.Entries = nil + c.RowEntries = nil + c.entriesIdx = nil + c.rowEntriesIdx = nil _ = os.Remove(c.filePath) } @@ -227,7 +257,66 @@ func (c *AICache) clear() { func (c *AICache) stat() (count int, path string) { c.mu.RLock() defer c.mu.RUnlock() - return len(c.Entries), c.filePath + return len(c.Entries) + len(c.RowEntries), c.filePath +} + +// rowKey 为单行匹配构建缓存键 +func (a *App) buildRowCacheKey(matchValue, timeStr string, config MatchConfig) string { + parts := fmt.Sprintf("%s|%s|%s|%.1f|%s", + matchValue, timeStr, config.RegexPattern, config.TimeWindow, + filepath.Base(config.FileBPath)) + h := sha256.Sum256([]byte(parts)) + return hex.EncodeToString(h[:]) +} + +// getRow 查找行级缓存 +func (c *AICache) getRow(key string) (string, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + if idx, ok := c.rowEntriesIdx[key]; ok && idx < len(c.RowEntries) && c.RowEntries[idx].Key == key { + return c.RowEntries[idx].Value, true + } + return "", false +} + +// putRow 存入行级缓存(线程安全 + 自动裁剪) +func (c *AICache) putRow(key, value string) { + c.mu.Lock() + defer c.mu.Unlock() + + // 去重:更新已存在的条目 + if idx, ok := c.rowEntriesIdx[key]; ok && idx < len(c.RowEntries) && c.RowEntries[idx].Key == key { + c.RowEntries[idx].Value = value + c.RowEntries[idx].CreatedAt = time.Now().Unix() + return + } + + // 新增条目 + idx := len(c.RowEntries) + c.RowEntries = append(c.RowEntries, AIRowCacheEntry{ + Key: key, + Value: value, + CreatedAt: time.Now().Unix(), + }) + c.rowEntriesIdx[key] = idx + + // 超过上限则删除最旧的条目 + if len(c.RowEntries) > c.maxRowSize { + sort.Slice(c.RowEntries, func(i, j int) bool { + return c.RowEntries[i].CreatedAt > c.RowEntries[j].CreatedAt + }) + c.RowEntries = c.RowEntries[:c.maxRowSize] + c.rebuildIndexes() + } +} + +// matchPrep 匹配准备的中间结果 +type matchPrep struct { + dataA, dataB [][]string + reg *regexp.Regexp + timeWindow float64 + threshold float64 + windowDuration time.Duration } // ---------- App 结构体 ---------- @@ -238,6 +327,7 @@ type App struct { aiCache *AICache // 最近一次匹配的配置和表头(供导出使用) + dataMu sync.RWMutex lastConfig MatchConfig headersA []string headersB []string @@ -411,9 +501,7 @@ func parseTimeFlexible(timeStr string) (time.Time, error) { // ---------- Levenshtein 距离算法 ---------- -func levenshteinDistance(s1, s2 string) int { - runes1 := []rune(s1) - runes2 := []rune(s2) +func levenshteinDistance(runes1, runes2 []rune) int { m, n := len(runes1), len(runes2) // 使用一维数组优化空间复杂度 @@ -438,12 +526,6 @@ func levenshteinDistance(s1, s2 string) int { return dp[n] } -func min(a, b int) int { - if a < b { - return a - } - return b -} // CalculateSimilarity 计算清洗后中文名称的相似度(基于 Levenshtein 距离归一化) func (a *App) CalculateSimilarity(s1, s2 string) float64 { @@ -473,7 +555,7 @@ func calcSimilarity(s1, s2 string, reg *regexp.Regexp, caseSensitive bool) float return 0.0 } - dist := levenshteinDistance(clean1, clean2) + dist := levenshteinDistance(r1, r2) maxLen := math.Max(float64(len(r1)), float64(len(r2))) return 1.0 - float64(dist)/maxLen } @@ -485,6 +567,24 @@ func cleanWithRegex(input string, reg *regexp.Regexp) string { } return reg.ReplaceAllString(input, "") } +// similarityFromCleaned 基于已清洗字符串计算相似度(跳过 regex 步骤,避免重复清洗) +func similarityFromCleaned(clean1, clean2 string, caseSensitive bool) float64 { + if !caseSensitive { + clean1 = strings.ToLower(clean1) + clean2 = strings.ToLower(clean2) + } + r1 := []rune(clean1) + r2 := []rune(clean2) + if len(r1) == 0 && len(r2) == 0 { + return 1.0 + } + if len(r1) == 0 || len(r2) == 0 { + return 0.0 + } + dist := levenshteinDistance(r1, r2) + maxLen := math.Max(float64(len(r1)), float64(len(r2))) + return 1.0 - float64(dist)/maxLen +} // ---------- 文件读取(通用)---------- @@ -551,25 +651,22 @@ func getCell(row []string, idx int) string { } -// RunMatch 接收完整 MatchConfig,按列索引执行通用匹配 -func (a *App) RunMatch(config MatchConfig) ([]MatchResult, error) { - // 1. 编译正则 +// prepareMatch 编译正则、读取文件、初始化默认值(RunMatch / RunMatchWithAI 共用) +func (a *App) prepareMatch(config MatchConfig) (*matchPrep, error) { reg, err := compileRegex(config.RegexPattern) if err != nil { return nil, err } - // 2. 默认值兜底 - timeWindow := config.TimeWindow - if timeWindow <= 0 { - timeWindow = defaultTimeWindowH + tw := config.TimeWindow + if tw <= 0 { + tw = defaultTimeWindowH } - threshold := config.Threshold - if threshold <= 0 { - threshold = defaultThreshold + th := config.Threshold + if th <= 0 { + th = defaultThreshold } - // 3. 读取原始数据 a.emitProgress(0, 100, "正在读取 A 表...", "reading") rowsA, err := a.readRawRows(config.FileAPath) if err != nil { @@ -587,22 +684,35 @@ func (a *App) RunMatch(config MatchConfig) ([]MatchResult, error) { return nil, fmt.Errorf("B 表无有效数据行") } - // 保存表头供导出使用 + a.dataMu.Lock() a.headersA = rowsA[0] a.headersB = rowsB[0] a.lastConfig = config + a.dataMu.Unlock() - dataA := rowsA[1:] - dataB := rowsB[1:] - windowDuration := time.Duration(timeWindow * float64(time.Hour)) - - return a.runMatchOnData(dataA, dataB, reg, config, timeWindow, threshold, windowDuration) + return &matchPrep{ + dataA: rowsA[1:], + dataB: rowsB[1:], + reg: reg, + timeWindow: tw, + threshold: th, + windowDuration: time.Duration(tw * float64(time.Hour)), + }, nil } -// runMatchOnData 在已读取的数据上执行匹配(内部使用,避免重复 I/O) -func (a *App) runMatchOnData(dataA, dataB [][]string, reg *regexp.Regexp, config MatchConfig, _, threshold float64, windowDuration time.Duration) ([]MatchResult, error) { +// RunMatch 接收完整 MatchConfig,按列索引执行通用匹配 +func (a *App) RunMatch(config MatchConfig) ([]MatchResult, error) { + prep, err := a.prepareMatch(config) + if err != nil { + return nil, err + } + return a.runMatchOnData(prep, config) +} + +// runMatchOnData 在已读取的数据上执行匹配 +func (a *App) runMatchOnData(prep *matchPrep, config MatchConfig) ([]MatchResult, error) { useTime := config.ColATimeIndex >= 0 && config.ColBTimeIndex >= 0 - totalA := len(dataA) + totalA := len(prep.dataA) var results []MatchResult useAllMatches := config.AllMatches @@ -611,7 +721,40 @@ func (a *App) runMatchOnData(dataA, dataB [][]string, reg *regexp.Regexp, config maxPreview = defaultMaxPreview } - for i, rowA := range dataA { + // 预计算 B 表清洗后的匹配值,避免内层循环中重复 regex 替换(O(n*m)→O(n+m)) + totalB := len(prep.dataB) + cleanedBMatch := make([]string, totalB) + origBMatch := make([]string, totalB) + parsedBTime := make([]time.Time, totalB) + hasBTime := make([]bool, totalB) + bExtractVal := make([]string, totalB) + for bIdx, rowB := range prep.dataB { + matchStrB := getCell(rowB, config.ColBMatchIndex) + origBMatch[bIdx] = matchStrB + if matchStrB == "" { + cleanedBMatch[bIdx] = "" + } else { + cleanedBMatch[bIdx] = cleanWithRegex(matchStrB, prep.reg) + } + if useTime { + t, err := parseTimeFlexible(getCell(rowB, config.ColBTimeIndex)) + if err == nil { + parsedBTime[bIdx] = t + hasBTime[bIdx] = true + } + } + bExtractVal[bIdx] = getCell(rowB, config.ColBExtractIndex) + } + for i, rowA := range prep.dataA { + // 定期检查是否取消 + if a.ctx != nil { + select { + case <-a.ctx.Done(): + return results, a.ctx.Err() + default: + } + } + if i%10 == 0 || i == totalA-1 { pct := (i + 1) * 100 / totalA a.emitProgress(i+1, totalA, @@ -633,46 +776,46 @@ func (a *App) runMatchOnData(dataA, dataB [][]string, reg *regexp.Regexp, config } } - cleanA := cleanWithRegex(matchStrA, reg) + cleanA := cleanWithRegex(matchStrA, prep.reg) // 收集该 A 行的所有候选匹配 var candidates []MatchResult - for _, rowB := range dataB { - matchStrB := getCell(rowB, config.ColBMatchIndex) - if matchStrB == "" { - continue - } + for bIdx := range prep.dataB { + matchStrB := origBMatch[bIdx] + if matchStrB == "" { + continue + } - var timeDiff time.Duration - if hasTimeA && useTime { - tB, err := parseTimeFlexible(getCell(rowB, config.ColBTimeIndex)) - if err != nil { - continue - } - td := timeA.Sub(tB) - if td < -windowDuration || td > windowDuration { - continue - } - timeDiff = td - } + var timeDiff time.Duration + if hasTimeA && useTime { + if !hasBTime[bIdx] { + continue + } + tB := parsedBTime[bIdx] + td := timeA.Sub(tB) + if td < -prep.windowDuration || td > prep.windowDuration { + continue + } + timeDiff = td + } - cleanB := cleanWithRegex(matchStrB, reg) - if cleanA == "" || cleanB == "" { - continue - } + cleanB := cleanedBMatch[bIdx] + if cleanA == "" || cleanB == "" { + continue + } - similarity := calcSimilarity(matchStrA, matchStrB, reg, config.CaseSensitive) + similarity := similarityFromCleaned(cleanA, cleanB, config.CaseSensitive) if i < maxPreview { fmt.Printf("[DEBUG] | A[%d]='%s'→'%s' | B='%s'→'%s' | 相似度=%.4f\n", i, matchStrA, cleanA, matchStrB, cleanB, similarity) } - if similarity >= threshold { + if similarity >= prep.threshold { mr := MatchResult{ RowAData: rowA, RowBKey: matchStrB, - ExtractValue: getCell(rowB, config.ColBExtractIndex), + ExtractValue: bExtractVal[bIdx], TimeDiff: formatTimeDiff(timeDiff), SimilarityScore: math.Round(similarity*10000) / 10000, AIMatched: false, @@ -757,60 +900,25 @@ func (a *App) RunMatchWithAI(config MatchConfig) ([]MatchResult, error) { return nil, fmt.Errorf("请先设置 Deepseek API 密钥") } - // 1. 编译正则 - reg, err := compileRegex(config.RegexPattern) + prep, err := a.prepareMatch(config) if err != nil { return nil, err } - // 2. 默认值兜底 - timeWindow := config.TimeWindow - if timeWindow <= 0 { - timeWindow = defaultTimeWindowH - } - threshold := config.Threshold - if threshold <= 0 { - threshold = defaultThreshold - } - windowDuration := time.Duration(timeWindow * float64(time.Hour)) - - // 3. 一次读取数据,供匹配和 AI 使用 - a.emitProgress(0, 100, "正在读取 A 表...", "reading") - rowsA, err := a.readRawRows(config.FileAPath) - if err != nil { - return nil, fmt.Errorf("读取 A 表失败: %v", err) - } - a.emitProgress(0, 100, "正在读取 B 表...", "reading") - rowsB, err := a.readRawRows(config.FileBPath) - if err != nil { - return nil, fmt.Errorf("读取 B 表失败: %v", err) - } - if len(rowsA) < 2 || len(rowsB) < 2 { - return nil, fmt.Errorf("数据表无有效数据行") - } - - // 保存表头供导出使用 - a.headersA = rowsA[0] - a.headersB = rowsB[0] - a.lastConfig = config - - dataA := rowsA[1:] - dataB := rowsB[1:] - - // 4. 先执行基础匹配(使用已读取的数据,避免重复 I/O) - results, err := a.runMatchOnData(dataA, dataB, reg, config, timeWindow, threshold, windowDuration) + // 1. 先执行基础匹配 + results, err := a.runMatchOnData(prep, config) if err != nil { return nil, err } - // 5. 找出未被基础匹配覆盖的 A 表行 + // 2. 找出未被基础匹配覆盖的 A 表行 matchedSet := make(map[string]bool) for _, r := range results { matchedSet[strings.Join(r.RowAData, "\x00")] = true } var unmatchedA [][]string - for _, row := range dataA { + for _, row := range prep.dataA { if !matchedSet[strings.Join(row, "\x00")] { unmatchedA = append(unmatchedA, row) } @@ -821,26 +929,61 @@ func (a *App) RunMatchWithAI(config MatchConfig) ([]MatchResult, error) { return results, nil } - // 6. AI 增强匹配 + // 3. AI 增强匹配(先查行级缓存,减少 API 调用) useTime := config.ColATimeIndex >= 0 && config.ColBTimeIndex >= 0 - totalUnmatched := len(unmatchedA) - a.emitProgress(0, totalUnmatched, - fmt.Sprintf("AI 增强匹配:还有 %d 条未匹配记录,正在调用 Deepseek...", totalUnmatched), - "ai-enhancing") aiMatched := 0 + var failedBatches []int + + // 3a. 检查行级缓存,命中则直接加入结果 + var uncachedA [][]string + cacheHits := 0 + for _, row := range unmatchedA { + matchVal := getCell(row, config.ColAMatchIndex) + timeStr := "" + if useTime { + timeStr = getCell(row, config.ColATimeIndex) + } + cacheKey := a.buildRowCacheKey(matchVal, timeStr, config) + if cachedVal, ok := a.aiCache.getRow(cacheKey); ok { + results = append(results, MatchResult{ + RowAData: row, + RowBKey: "", + ExtractValue: cachedVal, + SimilarityScore: 0, + AIMatched: true, + }) + aiMatched++ + cacheHits++ + } else { + uncachedA = append(uncachedA, row) + } + } + + if cacheHits > 0 { + fmt.Printf("[CACHE] ✓ 行级缓存命中 %d 条,剩余 %d 条需 AI 处理\n", cacheHits, len(uncachedA)) + } + + if len(uncachedA) == 0 { + a.emitProgress(1, 1, + fmt.Sprintf("AI 增强完成!全部 %d 条命中缓存", cacheHits), "done") + return results, nil + } + + totalUnmatched := len(uncachedA) + a.emitProgress(0, totalUnmatched, + fmt.Sprintf("AI 增强匹配:%d 条命中缓存,%d 条需调用 Deepseek...", cacheHits, totalUnmatched), + "ai-enhancing") for batchStart := 0; batchStart < totalUnmatched; batchStart += defaultBatchSize { - end := batchStart + defaultBatchSize - if end > totalUnmatched { - end = totalUnmatched - } + end := min(batchStart+defaultBatchSize, totalUnmatched) + batchNum := (batchStart / defaultBatchSize) + 1 a.emitProgress(batchStart+1, totalUnmatched, - fmt.Sprintf("AI 分析中 %d/%d (第 %d 批)...", end, totalUnmatched, (batchStart/defaultBatchSize)+1), + fmt.Sprintf("AI 分析中 %d/%d (第 %d 批)...", end, totalUnmatched, batchNum), "ai-enhancing") - batch := unmatchedA[batchStart:end] + batch := uncachedA[batchStart:end] // 计算本批 A 表的时间范围 var minTime, maxTime time.Time @@ -868,10 +1011,10 @@ func (a *App) RunMatchWithAI(config MatchConfig) ([]MatchResult, error) { // 过滤 B 表在时间窗口内的行(用户配置时间窗口 + 额外余量覆盖批次跨度) var relevantB [][]string if hasBatchTime && useTime { - padding := windowDuration + time.Duration(defaultAIWindowPadH)*time.Hour + padding := prep.windowDuration + time.Duration(defaultAIWindowPadH)*time.Hour ws := minTime.Add(-padding) we := maxTime.Add(padding) - for _, row := range dataB { + for _, row := range prep.dataB { t, err := parseTimeFlexible(getCell(row, config.ColBTimeIndex)) if err != nil || t.Before(ws) || t.After(we) { continue @@ -880,17 +1023,16 @@ func (a *App) RunMatchWithAI(config MatchConfig) ([]MatchResult, error) { } } else { // 无时间列时限制 B 表条数以控制 token 消耗 - maxB := defaultMaxBNoTime - if len(dataB) < maxB { - maxB = len(dataB) - } - relevantB = dataB[:maxB] + maxB := min(defaultMaxBNoTime, len(prep.dataB)) + relevantB = prep.dataB[:maxB] } // 构建 AI 提示 - prompt := a.buildGenericAIPrompt(batch, relevantB, config, windowDuration, hasBatchTime) + prompt := a.buildGenericAIPrompt(batch, relevantB, config, prep.windowDuration, hasBatchTime) aiResp, err := a.callDeepseekAPI(prompt) if err != nil { + fmt.Printf("[AI-WARN] 第 %d 批 API 调用失败: %v\n", batchNum, err) + failedBatches = append(failedBatches, batchNum) continue } @@ -911,7 +1053,8 @@ func (a *App) RunMatchWithAI(config MatchConfig) ([]MatchResult, error) { } if parseErr != nil { fmt.Printf("[AI-WARN] 响应解析失败 (第 %d 批): %s\n 原始响应: %.200s\n", - (batchStart/defaultBatchSize)+1, parseErr.Error(), aiResp) + batchNum, parseErr.Error(), aiResp) + failedBatches = append(failedBatches, batchNum) continue } @@ -930,13 +1073,27 @@ func (a *App) RunMatchWithAI(config MatchConfig) ([]MatchResult, error) { AIMatched: true, } results = append(results, mr) - aiMatched++ + aiMatched++ + + // 写入行级缓存 + matchVal := getCell(rowA, config.ColAMatchIndex) + timeStr := "" + if useTime { + timeStr = getCell(rowA, config.ColATimeIndex) + } + cacheKey := a.buildRowCacheKey(matchVal, timeStr, config) + a.aiCache.putRow(cacheKey, val) } } + a.aiCache.saveToFile() - a.emitProgress(totalUnmatched, totalUnmatched, - fmt.Sprintf("AI 增强完成!基础匹配 %d 条 + AI 补充 %d 条 = 共 %d 条", - len(results)-aiMatched, aiMatched, len(results)), "done") + // 构建完成消息 + msg := fmt.Sprintf("AI 增强完成!基础匹配 %d 条 + AI 补充 %d 条 = 共 %d 条", + len(results)-aiMatched, aiMatched, len(results)) + if len(failedBatches) > 0 { + msg += fmt.Sprintf("(警告:第 %v 批失败)", failedBatches) + } + a.emitProgress(totalUnmatched, totalUnmatched, msg, "done") return results, nil } @@ -967,7 +1124,8 @@ func (a *App) buildGenericAIPrompt(unmatched, bRows [][]string, config MatchConf } sb.WriteString(fmt.Sprintf("\nB 表参考数据(共 %d 条):\n", len(bRows))) - for _, row := range bRows { + truncated := false + for i, row := range bRows { matchVal := getCell(row, config.ColBMatchIndex) extractVal := getCell(row, config.ColBExtractIndex) sb.WriteString(fmt.Sprintf(" 「%s」 → 目标列值: 「%s」", matchVal, extractVal)) @@ -975,6 +1133,15 @@ func (a *App) buildGenericAIPrompt(unmatched, bRows [][]string, config MatchConf sb.WriteString(fmt.Sprintf(", 时间=%s", getCell(row, config.ColBTimeIndex))) } sb.WriteString("\n") + // 限制 B 表部分总字符数,防止 prompt 超出 token 限制 + if sb.Len() > maxPromptBChars { + fmt.Printf("[AI-WARN] Prompt B 表数据超长 (%d 条,%d 字符),截断于第 %d 条\n", len(bRows), sb.Len(), i) + truncated = true + } + if truncated { + sb.WriteString(fmt.Sprintf(" ... 已截断,省略 %d 条\n", len(bRows)-i-1)) + break + } } sb.WriteString("\n请返回 JSON 格式的匹配结果。") @@ -1037,7 +1204,10 @@ func (a *App) callDeepseekAPI(messages []deepseekMessage) (string, error) { } defer resp.Body.Close() - respBytes, _ := io.ReadAll(resp.Body) + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取 Deepseek 响应失败: %v", err) + } var dr deepseekResponse if err := json.Unmarshal(respBytes, &dr); err != nil { return "", fmt.Errorf("解析 Deepseek 响应失败: %v", err) @@ -1091,7 +1261,12 @@ func (a *App) ExportResults(results []MatchResult) (string, error) { return "", fmt.Errorf("没有匹配结果可以导出") } - isCSV := a.lastConfig.ExportFormat == "csv" + a.dataMu.RLock() + useCSV := a.lastConfig.ExportFormat == "csv" + includeHdr := a.lastConfig.IncludeHeader + a.dataMu.RUnlock() + + isCSV := useCSV ext := ".xlsx" filterDisplay := "Excel 文件 (*.xlsx)" filterPattern := "*.xlsx" @@ -1119,16 +1294,21 @@ func (a *App) ExportResults(results []MatchResult) (string, error) { } if isCSV { - return a.exportResultsCSV(results, savePath) + return a.exportResultsCSV(results, savePath, includeHdr) } - return a.exportResultsXLSX(results, savePath) + return a.exportResultsXLSX(results, savePath, includeHdr) } // exportHeaders 构建导出表头行(使用真实表头或回退默认) func (a *App) exportHeaders(numACols int) []string { + a.dataMu.RLock() + hdrA := make([]string, len(a.headersA)) + copy(hdrA, a.headersA) + a.dataMu.RUnlock() + headers := make([]string, 0, numACols+1) - if len(a.headersA) >= numACols { - for _, h := range a.headersA[:numACols] { + if len(hdrA) >= numACols { + for _, h := range hdrA[:numACols] { n := h if n == "" { n = fmt.Sprintf("Col%d", len(headers)+1) @@ -1144,7 +1324,7 @@ func (a *App) exportHeaders(numACols int) []string { return headers } -func (a *App) exportResultsXLSX(results []MatchResult, savePath string) (string, error) { +func (a *App) exportResultsXLSX(results []MatchResult, savePath string, includeHeader bool) (string, error) { f := excelize.NewFile() defer f.Close() sheetName := "匹配结果" @@ -1157,7 +1337,7 @@ func (a *App) exportResultsXLSX(results []MatchResult, savePath string) (string, extractCol := numACols // 表头 - if a.lastConfig.IncludeHeader { + if includeHeader { for i, h := range headers { f.SetCellValue(sheetName, fmt.Sprintf("%s1", colLetter(i)), h) } @@ -1166,12 +1346,41 @@ func (a *App) exportResultsXLSX(results []MatchResult, savePath string) (string, Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}}, }) f.SetCellStyle(sheetName, "A1", fmt.Sprintf("%s1", colLetter(extractCol)), headerStyle) + // 数据行样式(带边框和行号字体) + dataStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Size: 11}, + Border: []excelize.Border{ + {Type: "bottom", Color: "D9D9D9", Style: 1}, + }, + }) + firstDataRow := 2 + lastDataRow := len(results) + 1 + for ci := 0; ci <= numACols; ci++ { + f.SetCellStyle(sheetName, + fmt.Sprintf("%s%d", colLetter(ci), firstDataRow), + fmt.Sprintf("%s%d", colLetter(ci), lastDataRow), + dataStyle) + } + } else { + dataStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Size: 11}, + Border: []excelize.Border{ + {Type: "bottom", Color: "D9D9D9", Style: 1}, + }, + }) + lastDataRow := len(results) + for ci := 0; ci <= numACols; ci++ { + f.SetCellStyle(sheetName, + fmt.Sprintf("%s%d", colLetter(ci), 1), + fmt.Sprintf("%s%d", colLetter(ci), lastDataRow), + dataStyle) + } } // 数据行 for i, r := range results { rowNum := i + 2 - if !a.lastConfig.IncludeHeader { + if !includeHeader { rowNum = i + 1 } for ci := 0; ci < numACols; ci++ { @@ -1191,7 +1400,7 @@ func (a *App) exportResultsXLSX(results []MatchResult, savePath string) (string, return savePath, nil } -func (a *App) exportResultsCSV(results []MatchResult, savePath string) (string, error) { +func (a *App) exportResultsCSV(results []MatchResult, savePath string, includeHeader bool) (string, error) { var buf bytes.Buffer // 使用 UTF-8 BOM 帮助 Excel 正确识别编码 buf.Write([]byte{0xEF, 0xBB, 0xBF}) @@ -1200,7 +1409,7 @@ func (a *App) exportResultsCSV(results []MatchResult, savePath string) (string, headers := a.exportHeaders(numACols) // 表头行 - if a.lastConfig.IncludeHeader { + if includeHeader { for i, h := range headers { if i > 0 { buf.WriteByte(',') diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..dc95d08 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,884 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "vue": "^3.2.37" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^3.0.3", + "vite": "^3.0.7" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz", + "integrity": "sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^3.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/vite": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index dfa9de4..45e71a1 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -8,11 +8,9 @@ export function CleanString(arg1:string):Promise; export function ClearAICache():Promise; -export function DeepseekEnhanceMatching(arg1:string,arg2:string):Promise>; - export function ExportResults(arg1:Array):Promise; -export function GetAICacheInfo():Promise>; +export function GetAICacheInfo():Promise; export function GetDeepseekStatus():Promise; @@ -31,5 +29,3 @@ export function RunMatch(arg1:main.MatchConfig):Promise> export function RunMatchWithAI(arg1:main.MatchConfig):Promise>; export function SetDeepseekAPIKey(arg1:string):Promise; - -export function StartMatching(arg1:string,arg2:string):Promise>; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 641d8ab..f93a406 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -14,10 +14,6 @@ export function ClearAICache() { return window['go']['main']['App']['ClearAICache'](); } -export function DeepseekEnhanceMatching(arg1, arg2) { - return window['go']['main']['App']['DeepseekEnhanceMatching'](arg1, arg2); -} - export function ExportResults(arg1) { return window['go']['main']['App']['ExportResults'](arg1); } @@ -61,7 +57,3 @@ export function RunMatchWithAI(arg1) { export function SetDeepseekAPIKey(arg1) { return window['go']['main']['App']['SetDeepseekAPIKey'](arg1); } - -export function StartMatching(arg1, arg2) { - return window['go']['main']['App']['StartMatching'](arg1, arg2); -} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index f2504b0..1d0ce96 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1,5 +1,19 @@ export namespace main { + export class AICacheInfo { + count: number; + filePath: string; + + static createFrom(source: any = {}) { + return new AICacheInfo(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.count = source["count"]; + this.filePath = source["filePath"]; + } + } export class MatchConfig { fileAPath: string; fileBPath: string; @@ -46,9 +60,6 @@ export namespace main { rowAData: string[]; rowBKey: string; extractValue: string; - monthlyCellName: string; - dailyCellId: string; - interruptReason: string; timeDiff: string; similarityScore: number; aiMatched: boolean; @@ -62,9 +73,6 @@ export namespace main { this.rowAData = source["rowAData"]; this.rowBKey = source["rowBKey"]; this.extractValue = source["extractValue"]; - this.monthlyCellName = source["monthlyCellName"]; - this.dailyCellId = source["dailyCellId"]; - this.interruptReason = source["interruptReason"]; this.timeDiff = source["timeDiff"]; this.similarityScore = source["similarityScore"]; this.aiMatched = source["aiMatched"];