siyuan-note/siyuan · error

%s

Error message

%s

What it means

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.

Solutions

  1. Filter selected indices to those with Results[i].Reason == "" before calling Save.
  2. For ocr_target_conflict, choose a different NewPath or manually reconcile the target's OCR value in ocr-texts.json.
  3. For ocr_source_conflict, clean up duplicate/conflicting keys for the source path in data/assets/ocr-texts.json first.
  4. Run PrepareAssetTextRelinks again after manual edits — Reasons are computed against a snapshot and a fresh plan may now pass.
  5. Handle conflicts per mapping instead of all-or-nothing by splitting the batch into conflicted and clean groups.

Example fix

// before: pass all indices regardless of conflict
plan.Save(historyDir, allIndices)
// after: only save entries that passed the pre-check
var ok []int
for _, i := range allIndices {
    if plan.Results[i].Reason == "" {
        ok = append(ok, i)
    }
}
plan.Save(historyDir, ok)
Defensive patterns

Strategy: validation

Validate before calling

var selected []int
for i, r := range plan.Results {
    if r.Reason == "" && r.Changed {
        selected = append(selected, i)
    }
}
// selected is guaranteed Save-safe for these indices

Try / catch

if _, err := plan.Save(historyDir, selected); err != nil {
    if err.Error() == "ocr_source_conflict" || err.Error() == "ocr_target_conflict" {
        // report per-mapping conflicts to the user; re-prepare after edits
    }
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/05ea64471cf9ffd7. Report an issue: GitHub.

Appendix: source

Thrown at kernel/util/ocr_relink_batch.go:130

			for _, i := range group {
				p.Results[i].Reason = "ocr_target_conflict"
			}
		}
	}
	return p, nil
}

// Save 将通过预检的映射合并为一次保存,保留源键;同值重试不生成历史或写入文件。
func (p *AssetTextRelinkPlan) Save(historyDir string, selected []int) ([]int, error) {
	var changed []int
	texts := maps.Clone(p.texts)
	for _, i := range selected {
		if i < 0 || i >= len(p.Results) {
			return nil, errors.New("invalid OCR mapping index")
		}
		result := p.Results[i]
		if result.Reason != "" {
			return nil, errors.New(result.Reason)
		}
		if !result.Changed {
			continue
		}
		texts[(&url.URL{Path: p.mappings[i].NewPath}).EscapedPath()] = p.values[i]
		changed = append(changed, i)
	}
	if len(changed) == 0 {
		return nil, nil
	}
	assetsTextsLock.Lock()
	defer assetsTextsLock.Unlock()
	if !maps.Equal(p.memory, assetsTexts) {
		return nil, errors.New("OCR metadata changed during asset relink")
	}
	backup, err := json.MarshalIndent(p.texts, "", "  ")
	if err != nil {
		return nil, err

View on GitHub (pinned to 9f775e8a12)