siyuan-note/siyuan · error

Conf.Language(341)

Error message

Conf.Language(341)

What it means

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.

Solutions

  1. Exclude the root document (path equal to the top-level .sy) from fromPaths
  2. Move or delete the entire notebook instead if the root must go
  3. Skip root entries when building the source list

Example fix

// before
await fetchPost('/api/filetree/moveDocs', {fromPaths: ['/' + rootDocID + '.sy'], toNotebook: otherBox, toPath: '/'})
// after
const paths = fromPaths.filter(p => p.split('/').length > 2) // drop top-level docs
if (paths.length) await fetchPost('/api/filetree/moveDocs', {fromPaths: paths, toNotebook: otherBox, toPath: '/'})
Defensive patterns

Strategy: validation

Validate before calling

const isTopLevel = p => p.split('/').length <= 2; fromPaths = fromPaths.filter(p => !isTopLevel(p))

Try / catch

try { await fetchPost('/api/filetree/removeDoc', {notebook: boxID, path: p}) } catch (e) { if (String(e).includes('top-level')) { /* skip root doc */ } }

Prevention

When it happens

Trigger: 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.

Common situations: Scripts selecting all docs including the root; UI attempts to delete the first document in a notebook; recursive cleanup jobs starting at '/'.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/18fd5ca6c08ce73a. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/file.go:1726

	// 在移动任何文档前拒绝自身及后代目标,避免调用方将未执行的移动视为成功。
	for _, fromPath := range fromPaths {
		if fromBox := pathsBoxes[fromPath]; nil != fromBox && fromBox.ID == toBoxID &&
			(toPath == fromPath || strings.HasPrefix(toPath, strings.TrimSuffix(fromPath, ".sy")+"/")) {
			return errors.New(Conf.Language(87))
		}
	}
	fromPaths = util.FilterMoveDocFromPaths(fromPaths, toPath)
	if 1 > len(fromPaths) {
		return
	}

	fromPaths = orderMoveDocPaths(fromPaths, pathsBoxes)

	for _, fromPath := range fromPaths {
		fromBox := pathsBoxes[fromPath]
		if nil != fromBox && IsBoxDocPath(fromBox.ID, fromPath) {
			return errors.New(Conf.Language(341))
		}
	}

	if 1 == len(fromPaths) {
		// 移动到自己的父文档下的情况相当于不移动,直接返回
		if fromBox := pathsBoxes[fromPaths[0]]; nil != fromBox && fromBox.ID == toBoxID {
			parentDir := path.Dir(fromPaths[0])
			if ("/" == toPath && "/" == parentDir) || (parentDir+".sy" == toPath) {
				return
			}
		}
	}

	// 检查路径深度是否超过限制
	for _, fromPath := range fromPaths {
		fromBox := pathsBoxes[fromPath]
		childDepth := util.GetChildDocDepth(filepath.Join(util.DataDir, fromBox.ID, fromPath))
		if depth := strings.Count(toPath, "/") + childDepth; 6 < depth && !Conf.FileTree.AllowCreateDeeper {

View on GitHub (pinned to 9f775e8a12)