perf: optimize hot path & reorganize project config

Backend (app.go):
- AICache: replace linear scan with map-based O(1) lookup (get/getRow/put/putRow)
- runMatchOnData: pre-compute B-column cleaned values, parsed times, extract values
  to eliminate O(n*m) regex/time-parse from inner loop
- calcSimilarity: eliminate double rune conversion (levenshteinDistance now takes []rune)
- Add similarityFromCleaned to skip redundant regex step in hot path
- Fix corrupted bare 'n' literal causing build failure
- Move saveToFile out of inner match loop (was called per item)
- dataMu: Mutex -> RWMutex (exportHeaders/ExportResults use RLock)
- buildGenericAIPrompt: fix truncation check order (check after write)

Project:
- .gitignore: deduplicate & tighten rules; track package-lock.json and .vscode/*
- Clean up stale root binary (data-matcher.exe)
This commit is contained in:
RainySY
2026-05-20 14:16:13 +08:00
parent 40c3966e9a
commit b5a91cb115
8 changed files with 1325 additions and 207 deletions

37
.gitignore vendored
View File

@@ -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

21
.vscode/settings.json vendored Normal file
View File

@@ -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
}
}

13
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "dev (wails dev)",
"type": "shell",
"command": "wails dev",
"isBackground": true,
"problemMatcher": [],
"group": "build"
}
]
}

503
app.go
View File

@@ -27,17 +27,9 @@ 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"`
// 公共字段
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"`
@@ -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 序列化
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 // 最大缓存条目数
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,
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()
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)
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 {
if !hasBTime[bIdx] {
continue
}
tB := parsedBTime[bIdx]
td := timeA.Sub(tB)
if td < -windowDuration || td > windowDuration {
if td < -prep.windowDuration || td > prep.windowDuration {
continue
}
timeDiff = td
}
cleanB := cleanWithRegex(matchStrB, reg)
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
for batchStart := 0; batchStart < totalUnmatched; batchStart += defaultBatchSize {
end := batchStart + defaultBatchSize
if end > totalUnmatched {
end = totalUnmatched
// 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)
}
}
a.emitProgress(batchStart+1, totalUnmatched,
fmt.Sprintf("AI 分析中 %d/%d (第 %d 批)...", end, totalUnmatched, (batchStart/defaultBatchSize)+1),
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")
batch := unmatchedA[batchStart:end]
for batchStart := 0; batchStart < totalUnmatched; batchStart += defaultBatchSize {
end := min(batchStart+defaultBatchSize, totalUnmatched)
batchNum := (batchStart / defaultBatchSize) + 1
a.emitProgress(batchStart+1, totalUnmatched,
fmt.Sprintf("AI 分析中 %d/%d (第 %d 批)...", end, totalUnmatched, batchNum),
"ai-enhancing")
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
}
@@ -931,12 +1074,26 @@ func (a *App) RunMatchWithAI(config MatchConfig) ([]MatchResult, error) {
}
results = append(results, mr)
aiMatched++
}
}
a.emitProgress(totalUnmatched, totalUnmatched,
fmt.Sprintf("AI 增强完成!基础匹配 %d 条 + AI 补充 %d 条 = 共 %d 条",
len(results)-aiMatched, aiMatched, len(results)), "done")
// 写入行级缓存
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()
// 构建完成消息
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(',')

884
frontend/package-lock.json generated Normal file
View File

@@ -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
}
}
}
}
}

View File

@@ -8,11 +8,9 @@ export function CleanString(arg1:string):Promise<string>;
export function ClearAICache():Promise<string>;
export function DeepseekEnhanceMatching(arg1:string,arg2:string):Promise<Array<main.MatchResult>>;
export function ExportResults(arg1:Array<main.MatchResult>):Promise<string>;
export function GetAICacheInfo():Promise<Record<string, any>>;
export function GetAICacheInfo():Promise<main.AICacheInfo>;
export function GetDeepseekStatus():Promise<boolean>;
@@ -31,5 +29,3 @@ export function RunMatch(arg1:main.MatchConfig):Promise<Array<main.MatchResult>>
export function RunMatchWithAI(arg1:main.MatchConfig):Promise<Array<main.MatchResult>>;
export function SetDeepseekAPIKey(arg1:string):Promise<string>;
export function StartMatching(arg1:string,arg2:string):Promise<Array<main.MatchResult>>;

View File

@@ -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);
}

View File

@@ -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"];