{"record":{"id":"747f91c4c1683d02","repo":"siyuan-note/siyuan","slug":"notebook-closed","errorCode":null,"errorMessage":"notebook closed","messagePattern":"notebook closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/tree.go","lineNumber":203,"sourceCode":"\t\t// 从绝对路径推导 box 内相对路径作为 AAD\n\t\trelPath := filepath.ToSlash(strings.TrimPrefix(localPath, filepath.Join(util.DataDir, boxID)+string(os.PathSeparator)))\n\t\tif data, err = DecryptFile(boxID, relPath, dek, data); err != nil {\n\t\t\tlogging.LogErrorf(\"decrypt tree [path=%s] failed: %s\", localPath, err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tret, err = dataparser.ParseJSONWithoutFix(data, luteEngine.ParseOptions)\n\tif err != nil {\n\t\tlogging.LogErrorf(\"parse json to tree [%s] failed: %s\", localPath, err)\n\t\treturn\n\t}\n\treturn\n}\n\nvar (\n\tErrBoxNotFound   = errors.New(\"notebook not found\")\n\tErrBoxClosed     = errors.New(\"notebook closed\")\n\tErrBlockNotFound = errors.New(\"block not found\")\n\tErrTreeNotFound  = errors.New(\"tree not found\")\n\tErrIndexing      = errors.New(\"indexing\")\n\tErrBoxUnindexed  = errors.New(\"notebook unindexed\")\n\tErrInvalidID     = errors.New(\"invalid id\")\n)\n\nfunc LoadTreeByBlockIDWithReindex(id string) (ret *parse.Tree, err error) {\n\treturn LoadTreeByBlockIDWithReindexInBox(id, \"\")\n}\n\n// LoadTreeByBlockIDWithReindexInBox 与 LoadTreeByBlockIDWithReindex 一致，但按 boxID 路由 blocktree 查询。\nfunc LoadTreeByBlockIDWithReindexInBox(id, boxID string) (ret *parse.Tree, err error) {\n\tif \"\" == id {\n\t\tlogging.LogWarnf(\"block id is empty\")\n\t\treturn nil, ErrTreeNotFound\n\t}\n","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/model/tree.go#L185-L221","documentation":"Sentinel error model.ErrBoxClosed (kernel/model/tree.go:203). The notebook exists in configuration but is closed (unmounted), so operations that need a mounted notebook refuse to run - GetBoxByID-style lookups return it from mount.go:55, and import/doc tests assert it for closed notebooks.","triggerScenarios":"Closing a notebook in the UI (or via /api/notebook/closeNotebook) and then issuing doc/import/file operations that still reference its boxID; calling import into a notebook that is configured but never opened.","commonSituations":"Background jobs or plugins holding box IDs of notebooks the user closed to save startup time; batch import scripts not checking notebook state first.","solutions":["Reopen the notebook first: UI notebook panel or POST /api/notebook/openNotebook with the box id","Wait for the open to complete before retrying the operation","Guard with errors.Is(err, model.ErrBoxClosed) and surface a 'notebook is closed' message instead of a generic failure"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Pre-check state before heavy operations:\n// GET /api/notebook/lsNotebooks -> notebooks[].closed\n// If closed, POST /api/notebook/openNotebook {\"notebook\": boxID} first and wait for it to finish opening.","typeGuard":"func isBoxClosed(err error) bool {\n\treturn errors.Is(err, model.ErrBoxClosed)\n}","tryCatchPattern":"if err := importIntoBox(boxID, ...); err != nil {\n\tif errors.Is(err, model.ErrBoxClosed) {\n\t\t// open the notebook, wait, then retry once\n\t}\n}","preventionTips":["Open the notebook and wait for it to finish mounting before batch imports","Check the closed flag from lsNotebooks in job preconditions","Distinguish closed (recoverable by opening) from not-found (stale id) in error handling"],"tags":["notebook","sentinel-error","closed-state","kernel-api"],"backgroundTag":"notebook-closed","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}