{"record":{"id":"18fd5ca6c08ce73a","repo":"siyuan-note/siyuan","slug":"conf-language-341-18fd5c","errorCode":null,"errorMessage":"Conf.Language(341)","messagePattern":"Conf\\.Language\\(341\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/file.go","lineNumber":1726,"sourceCode":"\n\t// 在移动任何文档前拒绝自身及后代目标，避免调用方将未执行的移动视为成功。\n\tfor _, fromPath := range fromPaths {\n\t\tif fromBox := pathsBoxes[fromPath]; nil != fromBox && fromBox.ID == toBoxID &&\n\t\t\t(toPath == fromPath || strings.HasPrefix(toPath, strings.TrimSuffix(fromPath, \".sy\")+\"/\")) {\n\t\t\treturn errors.New(Conf.Language(87))\n\t\t}\n\t}\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":1708,"sourceCodeEnd":1744,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/file.go#L1708-L1744","documentation":"MoveDocs refuses to move or remove a notebook's top-level (root) document ('The top-level notebook document cannot be removed or moved', key 341). The root .sy anchors the notebook tree and SiYuan forbids relocating or deleting it. The check runs before any mutation.","triggerScenarios":"moveDocs or removeDoc where a fromPath is the notebook root (IsBoxDocPath true, i.e. the doc at path '/<id>.sy' root level); also triggered by removeDoc on the top-level document.","commonSituations":"Scripts selecting all docs including the root; UI attempts to delete the first document in a notebook; recursive cleanup jobs starting at '/'.","solutions":["Exclude the root document (path equal to the top-level .sy) from fromPaths","Move or delete the entire notebook instead if the root must go","Skip root entries when building the source list"],"exampleFix":"// before\nawait fetchPost('/api/filetree/moveDocs', {fromPaths: ['/' + rootDocID + '.sy'], toNotebook: otherBox, toPath: '/'})\n// after\nconst paths = fromPaths.filter(p => p.split('/').length > 2) // drop top-level docs\nif (paths.length) await fetchPost('/api/filetree/moveDocs', {fromPaths: paths, toNotebook: otherBox, toPath: '/'})","handlingStrategy":"validation","validationCode":"const isTopLevel = p => p.split('/').length <= 2; fromPaths = fromPaths.filter(p => !isTopLevel(p))","typeGuard":null,"tryCatchPattern":"try { await fetchPost('/api/filetree/removeDoc', {notebook: boxID, path: p}) } catch (e) { if (String(e).includes('top-level')) { /* skip root doc */ } }","preventionTips":["Exclude the notebook root document from move/remove batches","Delete whole notebooks via the notebook API instead of their root doc","Document that root .sy files are immovable"],"tags":["move","validation","root-document"],"backgroundTag":"unsupported-operation","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"}