{"record":{"id":"6702a2e312d83cc2","repo":"siyuan-note/siyuan","slug":"conf-language-314-6702a2","errorCode":null,"errorMessage":"Conf.Language(314)","messagePattern":"Conf\\.Language\\(314\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/export.go","lineNumber":614,"sourceCode":"// 用于文档级导出入口的统一 guard，避免未解锁时读出密文或空结果。\nfunc exportLockedByBlockID(id string) bool {\n\tbt := getExportBlockTree(id)\n\tif nil == bt {\n\t\treturn false // 找不到块树，交给后续流程处理\n\t}\n\treturn IsEncryptedBox(bt.BoxID) && !IsBoxUnlocked(bt.BoxID)\n}\n\n// withExportReadLockByBlockID 由 blockID 反查 boxID，若属于加密笔记本则全程持读锁执行 fn。\n// 持锁期间 LockBox（自动锁定）会阻塞等待，避免操作中途清 DEK/删导出目录导致部分明文写出。\n// 准入租约先于读锁取得，确保嵌套资源读取期间不会出现等待中的写锁。\nfunc withExportReadLockByBlockID(id string, fn func() error) error {\n\tbt := getExportBlockTree(id)\n\tif nil == bt || !IsEncryptedBox(bt.BoxID) {\n\t\treturn fn()\n\t}\n\tif !IsBoxUnlocked(bt.BoxID) {\n\t\treturn errors.New(Conf.Language(314))\n\t}\n\tif err := AcquireEncryptedBoxOperation(bt.BoxID); err != nil {\n\t\treturn errors.New(Conf.Language(314))\n\t}\n\tdefer ReleaseEncryptedBoxOperation(bt.BoxID)\n\tHoldBoxReadLock(bt.BoxID)\n\tdefer ReleaseBoxReadLock(bt.BoxID)\n\tdek, dekErr := GetDEKIfUnlocked(bt.BoxID)\n\tif dekErr != nil {\n\t\treturn errors.New(Conf.Language(314))\n\t}\n\tclear(dek)\n\treturn fn()\n}\n\nfunc ExportNotebookSY(id string) (zipPath string) {\n\t// 加密笔记本必须已解锁才能导出（DEK 在内存才能读 .sy/assets/AV 明文）\n\tif IsEncryptedBox(id) && !IsBoxUnlocked(id) {","sourceCodeStart":596,"sourceCodeEnd":632,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/export.go#L596-L632","documentation":"withExportReadLockByBlockID guards all export entry points (ExportCodeBlock, ExportAv2CSV, Export2Liandi, ExportPreview, ExportDocx, exportMarkdownHTML) against operating on encrypted notebooks. If the target block's box is encrypted and currently locked (IsBoxUnlocked false), it returns Conf.Language(314): 'Please unlock the encrypted notebook first'.","triggerScenarios":"Any of the six export APIs called with a block ID whose parent box is an encrypted notebook that has not been unlocked in this session.","commonSituations":"User restarts SiYuan (keys locked) and immediately exports a doc from an encrypted notebook; headless/API scripts running without having supplied the encryption passphrase.","solutions":["Unlock the encrypted notebook via the UI (enter passphrase) and retry the export","Call the kernel unlock API for the box before invoking the export endpoint"],"exampleFix":"// before\nawait exportDocx(blockId) // box still locked after restart\n// after\nawait unlockBox(boxId)   // provide passphrase first\nawait exportDocx(blockId)","handlingStrategy":"validation","validationCode":"const boxEncrypted = await isEncryptedBox(boxOf(blockId));\nconst unlocked = boxEncrypted ? await isBoxUnlocked(boxOf(blockId)) : true;\nif (!unlocked) throw new Error(\"Unlock the encrypted notebook before exporting\");","typeGuard":null,"tryCatchPattern":"try {\n  await exportDocx(blockId);\n} catch (e) {\n  if (String(e.msg).includes(\"unlock\")) showUnlockDialog();\n}","preventionTips":["Check box encryption/lock state via kernel API before any export call","After kernel restart, prompt users to unlock encrypted notebooks before enabling export actions"],"tags":["encryption","notebook","export","locked"],"backgroundTag":"authentication-required","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}