{"record":{"id":"05ea64471cf9ffd7","repo":"siyuan-note/siyuan","slug":"s-ocr-relink-batch","errorCode":null,"errorMessage":"%s","messagePattern":"%s","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/ocr_relink_batch.go","lineNumber":130,"sourceCode":"\t\t\tfor _, i := range group {\n\t\t\t\tp.Results[i].Reason = \"ocr_target_conflict\"\n\t\t\t}\n\t\t}\n\t}\n\treturn p, nil\n}\n\n// Save 将通过预检的映射合并为一次保存，保留源键；同值重试不生成历史或写入文件。\nfunc (p *AssetTextRelinkPlan) Save(historyDir string, selected []int) ([]int, error) {\n\tvar changed []int\n\ttexts := maps.Clone(p.texts)\n\tfor _, i := range selected {\n\t\tif i < 0 || i >= len(p.Results) {\n\t\t\treturn nil, errors.New(\"invalid OCR mapping index\")\n\t\t}\n\t\tresult := p.Results[i]\n\t\tif result.Reason != \"\" {\n\t\t\treturn nil, errors.New(result.Reason)\n\t\t}\n\t\tif !result.Changed {\n\t\t\tcontinue\n\t\t}\n\t\ttexts[(&url.URL{Path: p.mappings[i].NewPath}).EscapedPath()] = p.values[i]\n\t\tchanged = append(changed, i)\n\t}\n\tif len(changed) == 0 {\n\t\treturn nil, nil\n\t}\n\tassetsTextsLock.Lock()\n\tdefer assetsTextsLock.Unlock()\n\tif !maps.Equal(p.memory, assetsTexts) {\n\t\treturn nil, errors.New(\"OCR metadata changed during asset relink\")\n\t}\n\tbackup, err := json.MarshalIndent(p.texts, \"\", \"  \")\n\tif err != nil {\n\t\treturn nil, err","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/ocr_relink_batch.go#L112-L148","documentation":"During Save, each selected mapping is re-checked: if its pre-computed Result carries a non-empty Reason (ocr_source_conflict or ocr_target_conflict), Save refuses to apply it and returns that reason as an error. The plan is atomic — one conflicted mapping aborts the whole save — because applying it would overwrite differing OCR recognition results.","triggerScenarios":"Calling plan.Save on a plan where any selected index has Results[i].Reason != \"\" — i.e. the caller skipped inspecting Results and passed indices flagged as conflicted by PrepareAssetTextRelinks (source path has conflicting OCR values, or the target already exists with a different value, or multiple sources merge into one target with different values).","commonSituations":"Batch rename UI selecting all rows including conflicted ones; CopyAssetTextForRelink-style single mappings where the dry-run step was skipped; merging folders where several renamed assets map onto one target name with divergent OCR text.","solutions":["Filter selected indices to those with Results[i].Reason == \"\" before calling Save.","For ocr_target_conflict, choose a different NewPath or manually reconcile the target's OCR value in ocr-texts.json.","For ocr_source_conflict, clean up duplicate/conflicting keys for the source path in data/assets/ocr-texts.json first.","Run PrepareAssetTextRelinks again after manual edits — Reasons are computed against a snapshot and a fresh plan may now pass.","Handle conflicts per mapping instead of all-or-nothing by splitting the batch into conflicted and clean groups."],"exampleFix":"// before: pass all indices regardless of conflict\nplan.Save(historyDir, allIndices)\n// after: only save entries that passed the pre-check\nvar ok []int\nfor _, i := range allIndices {\n    if plan.Results[i].Reason == \"\" {\n        ok = append(ok, i)\n    }\n}\nplan.Save(historyDir, ok)","handlingStrategy":"validation","validationCode":"var selected []int\nfor i, r := range plan.Results {\n    if r.Reason == \"\" && r.Changed {\n        selected = append(selected, i)\n    }\n}\n// selected is guaranteed Save-safe for these indices","typeGuard":null,"tryCatchPattern":"if _, err := plan.Save(historyDir, selected); err != nil {\n    if err.Error() == \"ocr_source_conflict\" || err.Error() == \"ocr_target_conflict\" {\n        // report per-mapping conflicts to the user; re-prepare after edits\n    }\n}","preventionTips":["Only pass indices whose Results[i].Reason is empty into Save","Show conflict reasons in the UI before committing a batch rename","Re-run PrepareAssetTextRelinks after any manual ocr-texts.json edit","Remember Save is all-or-nothing: split conflicted rows out of the batch"],"tags":["ocr","conflict","relink","precondition"],"backgroundTag":"conflicting-config-options","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}