{"record":{"id":"e4d2e6816d1ee98e","repo":"siyuan-note/siyuan","slug":"the-top-level-notebook-document-cannot-be-removed","errorCode":null,"errorMessage":"The top-level notebook document cannot be removed or moved","messagePattern":"The top-level notebook document cannot be removed or moved","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/file.go","lineNumber":1757,"sourceCode":"\t\treturn\n\t}\n\ttoPath = normalizeBoxDocTarget(toBoxID, toPath)\n\tpathsBoxes, err := getBoxesByPathsStrict(fromPaths)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfromPaths = util.FilterMoveDocFromPaths(fromPaths, toPath)\n\tif 1 > len(fromPaths) {\n\t\treturn\n\t}\n\n\tfromPaths = orderMoveDocPaths(fromPaths, pathsBoxes)\n\n\tfor _, fromPath := range fromPaths {\n\t\tfromBox := pathsBoxes[fromPath]\n\t\tif nil != fromBox && IsBoxDocPath(fromBox.ID, fromPath) {\n\t\t\treturn errors.New(Conf.Language(341))\n\t\t}\n\t}\n\n\tif 1 == len(fromPaths) {\n\t\t// 移动到自己的父文档下的情况相当于不移动，直接返回\n\t\tif fromBox := pathsBoxes[fromPaths[0]]; nil != fromBox && fromBox.ID == toBoxID {\n\t\t\tparentDir := path.Dir(fromPaths[0])\n\t\t\tif (\"/\" == toPath && \"/\" == parentDir) || (parentDir+\".sy\" == toPath) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\t// 检查路径深度是否超过限制\n\tfor _, fromPath := range fromPaths {\n\t\tfromBox := pathsBoxes[fromPath]\n\t\tchildDepth := util.GetChildDocDepth(filepath.Join(util.DataDir, fromBox.ID, fromPath))\n\t\tif depth := strings.Count(toPath, \"/\") + childDepth; 6 < depth && !Conf.FileTree.AllowCreateDeeper {","sourceCodeStart":1739,"sourceCodeEnd":1775,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/model/file.go#L1739-L1775","documentation":"Thrown by MoveDocs (kernel/model/file.go:1757) when one of the fromPaths resolves to the notebook's own top-level (box) document. IsBoxDocPath extracts the tree ID from the path via util.GetTreeID and compares it with the notebook ID (kernel/model/box_doc.go:317-324); equality means the path is /<boxID>.sy, the special root document of the notebook. Moving that document would be equivalent to moving or destroying the notebook itself, so it is rejected with Conf.Language(341).","triggerScenarios":"POST /api/filetree/moveDocs where fromPaths contains \"/<notebookID>.sy\" or a path whose base filename (minus .sy) equals the notebook ID. Typically happens when a client selects all documents in a notebook (the root doc is included) or computes parent paths and accidentally feeds the box doc path back in.","commonSituations":"Select-all + drag in the doc tree; bulk-move scripts that build paths by walking data/<box>/ and include the top-level <boxID>.sy file; tools that map a block ID back to a path without excluding the box document; new users trying to relocate the first top-level doc like a normal document.","solutions":["Filter out paths whose basename without .sy equals the notebook ID before calling moveDocs (see exampleFix)","If the goal is to move the whole notebook, there is no move API — export/import the notebook or recreate it, or use renameNotebook if only the name matters","If the goal is to move all children of the notebook, list its child docs (path depth 1) and move those instead of the root doc"],"exampleFix":"// before\nfetchPost('/api/filetree/moveDocs', {fromPaths: allSelectedPaths, toNotebook, toPath})\n\n// after\nconst movable = allSelectedPaths.filter(p => {\n  const base = p.split('/').pop().replace(/\\.sy$/, '')\n  return base !== fromNotebookId // skip the notebook's own root doc\n})\nif (movable.length) fetchPost('/api/filetree/moveDocs', {fromPaths: movable, toNotebook, toPath})","handlingStrategy":"validation","validationCode":"// Strip notebook root docs (/<boxID>.sy) from a move batch\nfunc filterBoxDocPaths(paths []string, boxes map[string]*Box) (ret []string) {\n\tfor _, p := range paths {\n\t\tif box := boxes[p]; nil != box && !IsBoxDocPath(box.ID, p) {\n\t\t\tret = append(ret, p)\n\t\t}\n\t}\n\treturn\n}","typeGuard":"func isMovableDocPath(boxID, p string) bool {\n\treturn \"\" != util.GetTreeID(p) && util.GetTreeID(p) != boxID\n}","tryCatchPattern":null,"preventionTips":["When building 'all documents' batches, exclude paths whose basename minus .sy equals the notebook ID","Remember RenameDoc routes box docs to RenameBox — for moves there is no notebook-move equivalent; plan UX accordingly","Log any filtered root-doc paths so silent no-ops are visible in tooling"],"tags":["siyuan","filetree","move-docs","box-doc","validation"],"backgroundTag":"operation-not-permitted-on-root","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}