{"record":{"id":"4a7374cf718462da","repo":"siyuan-note/siyuan","slug":"duplicate-ai-editor-action-id-s","errorCode":null,"errorMessage":"duplicate AI editor action ID [%s]","messagePattern":"duplicate AI editor action ID \\[(.+?)\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/ai_editor.go","lineNumber":178,"sourceCode":"\t\treturn nil, fmt.Errorf(\"read AI editor actions failed: %w\", err)\n\t}\n\tif err = gulu.JSON.UnmarshalJSON(data, ret); err != nil {\n\t\treturn nil, fmt.Errorf(\"unmarshal AI editor actions failed: %w\", err)\n\t}\n\tif ret.Version != aiEditorActionsVersion {\n\t\treturn nil, fmt.Errorf(\"unsupported AI editor actions version [%d]\", ret.Version)\n\t}\n\tif ret.Actions == nil {\n\t\tret.Actions = []*AIEditorAction{}\n\t}\n\n\tids := make(map[string]struct{}, len(ret.Actions))\n\tfor _, action := range ret.Actions {\n\t\tif action == nil || !ast.IsNodeIDPattern(action.ID) || (action.Name == \"\" && action.Action == \"\") {\n\t\t\treturn nil, errors.New(\"invalid AI editor action data\")\n\t\t}\n\t\tif _, ok := ids[action.ID]; ok {\n\t\t\treturn nil, fmt.Errorf(\"duplicate AI editor action ID [%s]\", action.ID)\n\t\t}\n\t\tids[action.ID] = struct{}{}\n\t}\n\treturn ret, nil\n}\n\nfunc saveAIEditorActions(data *aiEditorActionsData) (err error) {\n\tdata.Version = aiEditorActionsVersion\n\tif data.Actions == nil {\n\t\tdata.Actions = []*AIEditorAction{}\n\t}\n\n\tdirPath := filepath.Dir(aiEditorActionsPath())\n\tif err = os.MkdirAll(dirPath, 0755); err != nil {\n\t\treturn fmt.Errorf(\"create AI editor actions directory failed: %w\", err)\n\t}\n\tbytes, err := gulu.JSON.MarshalIndentJSON(data, \"\", \"  \")\n\tif err != nil {","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/model/ai_editor.go#L160-L196","documentation":"loadAIEditorActions (kernel/model/ai_editor.go:178) builds an ID set while scanning entries and rejects the file if two actions share the same ID. Duplicate IDs would make update/delete ambiguous (SaveAIEditorAction replaces the first match), so the loader refuses the whole store instead of picking a winner.","triggerScenarios":"actions.json contains two objects with the same id — usually after a manual copy-paste of an entry, a sync/merge that duplicated a block, or two devices generating colliding hand-written IDs. The JSON parses and each entry is individually valid; only the cross-entry uniqueness check fails.","commonSituations":"Duplicating an action by copying its JSON to make a variant and forgetting to change the id; a cloud-sync merge duplicating lines; importing an action list that already contains the exported ID.","solutions":["Open actions.json, find the duplicated id, and give one copy a fresh unique ID (or delete the redundant entry)","If unsure which to keep, delete the whole file — legacy actions are re-imported from localStorage \"local-ai\" and defaults regenerated","When exporting/importing actions, strip ids on import and let the server re-issue them"],"exampleFix":"// before: two entries with \"id\":\"20240101120000-a1b2c3d\"\n// after: keep one; second becomes \"id\":\"20240101120001-b2c3d4e\" (any unique 14-digit-time + 7-char-suffix value)","handlingStrategy":"try-catch","validationCode":"ids := map[string]bool{}\nfor _, a := range actions {\n    if ids[a.ID] {\n        return fmt.Errorf(\"duplicate id %s in import payload; dedupe first\", a.ID)\n    }\n    ids[a.ID] = true\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.HasPrefix(err.Error(), \"duplicate AI editor action ID\") {\n    dedupeActionsFileByID() // keep first occurrence, re-id the rest\n    actions, err = model.GetAIEditorActions()\n}","preventionTips":["When duplicating an entry to make a variant, always mint a new unique ID","On import, strip IDs and let SaveAIEditorAction re-issue them","Treat sync-merge duplicates as data bugs: re-id, never delete blindly"],"tags":["ai","duplicate","data-corruption","json"],"backgroundTag":"duplicate-entity-id","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}