{"record":{"id":"6c1c664a19acd0f8","repo":"siyuan-note/siyuan","slug":"conf-language-87","errorCode":null,"errorMessage":"Conf.Language(87)","messagePattern":"Conf\\.Language\\(87\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/file.go","lineNumber":1713,"sourceCode":"}\n\nfunc MoveDocs(fromPaths []string, toBoxID, toPath string, callback any) (err error) {\n\ttoBox := Conf.Box(toBoxID)\n\tif nil == toBox {\n\t\terr = errors.New(Conf.Language(0))\n\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\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// 移动到自己的父文档下的情况相当于不移动，直接返回","sourceCodeStart":1695,"sourceCodeEnd":1731,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/file.go#L1695-L1731","documentation":"MoveDocs rejects moves where the destination path is the source document itself or one of its descendants ('Cannot move to this location', key 87). Moving a document into itself would create a cycle, so the check runs before any filesystem change, guaranteeing the failed call has no side effects.","triggerScenarios":"moveDocs where toPath equals a fromPath or lies under it (e.g. moving /a.sy to /a/b); also triggered when trimming '.sy' and the target is a child path of the source.","commonSituations":"Bulk-move scripts with computed target paths; UI drag onto a collapsed ancestor; recursive batch jobs that skip the parent check.","solutions":["Check strings.HasPrefix(toPath, strings.TrimSuffix(fromPath, '.sy') + '/') before calling and skip such pairs","Move the documents to a sibling folder instead of into themselves","Filter source list so no document is an ancestor of the target"],"exampleFix":"// before\nawait fetchPost('/api/filetree/moveDocs', {fromPaths: ['/a.sy'], toNotebook: boxID, toPath: '/a/b'})\n// after\nif (toPath !== '/a.sy' && !toPath.startsWith('/a/')) {\n  await fetchPost('/api/filetree/moveDocs', {fromPaths: ['/a.sy'], toNotebook: boxID, toPath: toPath})\n}","handlingStrategy":"validation","validationCode":"const isSelfOrDescendant = (fromPath, toPath) => toPath === fromPath || toPath.startsWith(fromPath.replace(/\\.sy$/, '') + '/'); fromPaths = fromPaths.filter(p => !isSelfOrDescendant(p, toPath))","typeGuard":null,"tryCatchPattern":"try { await fetchPost('/api/filetree/moveDocs', args) } catch (e) { if (String(e).includes('Cannot move to this location')) { /* skip or recompute target */ } }","preventionTips":["Filter ancestors of the target out of fromPaths before moving","In bulk jobs, sort sources so parents are never moved into their own subtree","Test move scripts on a scratch notebook first"],"tags":["move","validation","cyclic-move"],"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"}