{"record":{"id":"747d0456f2a758ce","repo":"siyuan-note/siyuan","slug":"duplicate-notebook-id-s","errorCode":null,"errorMessage":"duplicate notebook ID [%s]","messagePattern":"duplicate notebook ID \\[(.+?)\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/file.go","lineNumber":2912,"sourceCode":"\tfileTreeSortLock.Lock()\n\tdefer fileTreeSortLock.Unlock()\n\tboxes := map[string]*Box{}\n\tfor _, box := range Conf.GetBoxes() {\n\t\tboxes[box.ID] = box\n\t}\n\topenedBoxes := map[string]*Box{}\n\tfor _, box := range Conf.GetOpenedBoxes() {\n\t\topenedBoxes[box.ID] = box\n\t}\n\n\tnotebookPlans := make([]*notebookSortPlan, 0, len(notebookSorts))\n\tnotebookIDs := map[string]struct{}{}\n\tfor _, item := range notebookSorts {\n\t\tif nil == item {\n\t\t\treturn ret, errors.New(\"notebook sort item must not be nil\")\n\t\t}\n\t\tif _, ok := notebookIDs[item.ID]; ok {\n\t\t\treturn ret, fmt.Errorf(\"duplicate notebook ID [%s]\", item.ID)\n\t\t}\n\t\tnotebookIDs[item.ID] = struct{}{}\n\n\t\tbox := boxes[item.ID]\n\t\tif nil == box {\n\t\t\treturn ret, fmt.Errorf(\"notebook [%s] not found\", item.ID)\n\t\t}\n\t\tnotebookPlans = append(notebookPlans, &notebookSortPlan{item: item, box: box})\n\t}\n\n\tdocPlans := make([]*docSortPlan, 0, len(docSorts))\n\tdocIDs := map[string]struct{}{}\n\tfor _, item := range docSorts {\n\t\tif nil == item {\n\t\t\treturn ret, errors.New(\"document sort item must not be nil\")\n\t\t}\n\t\tif _, ok := docIDs[item.ID]; ok {\n\t\t\treturn ret, fmt.Errorf(\"duplicate document ID [%s]\", item.ID)","sourceCodeStart":2894,"sourceCodeEnd":2930,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/file.go#L2894-L2930","documentation":"Notebook sort planning rejects duplicate IDs: if the same notebook ID appears more than once in notebookSorts, the second occurrence fails with \"duplicate notebook ID [%s]\". A notebook can occupy exactly one position in an order, so duplicates represent a malformed request that would produce an ambiguous ordering.","triggerScenarios":"Sending the notebook sort API a list containing the same notebook ID twice, e.g. merging two order lists without deduplication, or double-appending in plugin code.","commonSituations":"Plugin concatenating persisted order with a new item already present; copy-paste building of sort payloads; frontend drag handler appending the item without removing it from its old slot.","solutions":["Deduplicate the notebookSorts list by ID before calling the API","When building a new order, replace the existing entry for an ID instead of appending a second one","Log and inspect the request body to find which ID repeats"],"exampleFix":"// before\ncustomSortNotebooks(append(persistedOrder, newItem))\n// after: replace-or-append by ID\nmerged := map[string]*SortItem{}\nfor _, it := range persistedOrder { merged[it.ID] = it }\nmerged[newItem.ID] = newItem\nout := make([]*SortItem, 0, len(merged))\nfor _, it := range merged { out = append(out, it) }\ncustomSortNotebooks(out)","handlingStrategy":"validation","validationCode":"const ids = notebookSorts.map(i => i.id);\nif (new Set(ids).size !== ids.length) throw new Error('duplicate notebook IDs in sort payload');","typeGuard":"const isUnique = (items) => new Set(items.map(i => i.id)).size === items.length;","tryCatchPattern":"try { await api.sortNotebooks(items); } catch (e) { if (String(e).includes('duplicate notebook ID')) { await api.sortNotebooks(dedupeById(items)); } else { throw e; } }","preventionTips":["Dedupe by ID when merging persisted and new order lists","Replace existing entries by ID rather than appending","Add a unit test asserting sort payloads have unique IDs"],"tags":["go","validation","duplicate-ids","sorting"],"backgroundTag":"invalid-argument-value","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"}