Skip to content

Commit 0ce9467

Browse files
committed
✨ encrypted-notebook box-scoped read: doc/backlink/search + block-ref boundary + AI isolation #18034
Stage 8 Part 2 剩余读路径 + 块引边界防护 + AI/LLM 隔离。 读路径 InBox 版(深度全路由,加密 box 读取绝不泄漏到全局 db): - GetDocInBox(打开文档)、GetBacklinkInBox/GetBacklink2InBox(反链面板) - FullTextSearchBlockInBox(笔记本内搜索)、SearchRefBlockInBox(块引搜索) - sql 层补全 12 个 InBox 函数 + queryRawStmtForBox/ArgsForBox wrapper - model 层私有 helper(buildLinkRefs/toSubTree/searchBackmention/highlightByFTS 等)转发到 InBox 版 块引跨加密边界防护(三层防御): - 前端 searchRefBlock 调用点带 notebook(hint/extend + hint/index) - handler searchRefBlock 加 notebook 分流 - transaction doUpdate/doInsert0 落库兜底:degradeCrossBoundaryBlockRefs 降级跨边界引用为纯文本 IndexRefs 修复(加密 box 反链索引建立): - 加密 box 改用 filesys.LoadTree 透明解密,替代 filelock.ReadFile + LoadTreeByData - defBlock 按 box 路由 loadTreeByBlockIDInBox 加载 AI/LLM 隔离(元数据层显式过滤,正文层靠物理 db 隔离): - MCP notebook.list 过滤加密 box - MCP document.list 拒绝加密 box - 智能体编辑器上下文不下发加密 box 的 notebookID/activeDocTitle
1 parent b0d932c commit 0ce9467

23 files changed

Lines changed: 854 additions & 108 deletions

File tree

app/src/editor/util.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {getInstanceById, getWndByLayout, pdfIsLoading, setPanelFocus} from "../l
55
import {getDockByType} from "../layout/tabUtil";
66
import {getAllModels, getAllTabs} from "../layout/getAll";
77
import {highlightById, scrollCenter} from "../util/highlightById";
8-
import {getDisplayName, getDocDisplayName, pathPosix, useShell} from "../util/pathName";
8+
import {getDisplayName, getDocDisplayName, isEncryptedBox, pathPosix, useShell} from "../util/pathName";
99
import {Constants} from "../constants";
1010
import {Files} from "../layout/dock/Files";
1111
import {fetchPost, fetchSyncPost} from "../util/fetch";
@@ -375,11 +375,15 @@ const switchEditor = (editor: Editor, options: IOpenFileOptions, allModels: IMod
375375
}
376376
});
377377
if ((!nodeElement || nodeElement?.clientHeight === 0) && options.id !== options.rootID) {
378-
fetchPost("/api/filetree/getDoc", {
378+
const getDocParam: IObject = {
379379
id: options.id,
380380
mode: (options.action && options.action.includes(Constants.CB_GET_CONTEXT)) ? 3 : 0,
381381
size: window.siyuan.config.editor.dynamicLoadBlocks,
382-
}, getResponse => {
382+
};
383+
if (isEncryptedBox(editor.editor.protyle.notebookId)) {
384+
getDocParam.notebook = editor.editor.protyle.notebookId;
385+
}
386+
fetchPost("/api/filetree/getDoc", getDocParam, getResponse => {
383387
onGet({
384388
data: getResponse,
385389
protyle: editor.editor.protyle,
@@ -702,13 +706,17 @@ export const updateBacklinkGraph = (models: IModels, protyle: IProtyle) => {
702706
return;
703707
}
704708
item.element.querySelector('.block__icon[data-type="refresh"] svg').classList.add("fn__rotate");
705-
fetchPost("/api/ref/getBacklink2", {
709+
const backlinkParam: IObject = {
706710
sort: item.status[blockId] ? item.status[blockId].sort.toString() : window.siyuan.config.editor.backlinkSort.toString(),
707711
mSort: item.status[blockId] ? item.status[blockId].mSort.toString() : window.siyuan.config.editor.backmentionSort.toString(),
708712
id: blockId || "",
709713
k: item.inputsElement[0].value,
710714
mk: item.inputsElement[1].value,
711-
}, response => {
715+
};
716+
if (protyle && isEncryptedBox(protyle.notebookId)) {
717+
backlinkParam.notebook = protyle.notebookId;
718+
}
719+
fetchPost("/api/ref/getBacklink2", backlinkParam, response => {
712720
if (!isCurrentEditor(blockId) || item.blockId === blockId) {
713721
item.element.querySelector('.block__icon[data-type="refresh"] svg').classList.remove("fn__rotate");
714722
return;

app/src/layout/dock/Backlink.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {Protyle} from "../../protyle";
1111
import {MenuItem} from "../../menus/Menu";
1212
import {App} from "../../index";
1313
import {isSupportCSSHL, searchMarkRender} from "../../protyle/render/searchMarkRender";
14-
import {getDocDisplayName} from "../../util/pathName";
14+
import {getDocDisplayName, isEncryptedBox} from "../../util/pathName";
15+
import {getAllModels} from "../getAll";
1516

1617
export class Backlink extends Model {
1718
public element: HTMLElement;
@@ -518,13 +519,27 @@ export class Backlink extends Model {
518519
return;
519520
}
520521
element.classList.add("fn__rotate");
521-
fetchPost("/api/ref/getBacklink2", {
522+
// 解析当前反链面板所属 box:优先用已记录的 notebookId,首次为空时按 rootId 在已打开的编辑器里查找
523+
let notebookId = this.notebookId;
524+
if (!notebookId && this.rootId) {
525+
getAllModels().editor.some(item => {
526+
if (item.editor.protyle.block.rootID === this.rootId) {
527+
notebookId = item.editor.protyle.notebookId;
528+
return true;
529+
}
530+
});
531+
}
532+
const param: IObject = {
522533
sort: parseInt(this.tree.element.previousElementSibling.querySelector('[data-type="sort"]').getAttribute("data-sort")).toString(),
523534
mSort: parseInt(this.mTree.element.previousElementSibling.querySelector('[data-type="mSort"]').getAttribute("data-sort")).toString(),
524535
k: this.inputsElement[0].value,
525536
mk: this.inputsElement[1].value,
526537
id: this.blockId,
527-
}, response => {
538+
};
539+
if (isEncryptedBox(notebookId)) {
540+
param.notebook = notebookId;
541+
}
542+
fetchPost("/api/ref/getBacklink2", param, response => {
528543
if (!init) {
529544
this.saveStatus();
530545
}

app/src/layout/dock/agent/AgentChat.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {setPanelFocus} from "../../util";
1616
import {escapeAriaLabel, escapeHtml} from "../../../util/escape";
1717
import {setPosition} from "../../../util/setPosition";
1818
import {fetchPost, fetchSyncPost} from "../../../util/fetch";
19+
import {isEncryptedBox} from "../../../util/pathName";
1920
import {Constants} from "../../../constants";
2021
import {confirmDialog} from "../../../dialog/confirmDialog";
2122
import {showMessage} from "../../../dialog/message";
@@ -1478,6 +1479,8 @@ export class AgentChat extends Model {
14781479
const focusedBlockID = p.block?.id;
14791480
const activeDocTitle = p.title?.editElement?.textContent?.trim() || undefined;
14801481
const notebookID = p.notebookId || undefined;
1482+
// AI/LLM 不得感知加密笔记本:当前文档位于加密 box 时,不下发 notebookID 与文档标题等元数据
1483+
const encryptedCtx = isEncryptedBox(notebookID);
14811484

14821485
const selectedBlockIDs: string[] = [];
14831486
p.wysiwyg?.element?.querySelectorAll("[data-node-id].protyle-wysiwyg--select")
@@ -1523,10 +1526,10 @@ export class AgentChat extends Model {
15231526
if (activeDocID) {
15241527
ctx.activeDocID = activeDocID;
15251528
}
1526-
if (activeDocTitle) {
1529+
if (activeDocTitle && !encryptedCtx) {
15271530
ctx.activeDocTitle = activeDocTitle;
15281531
}
1529-
if (notebookID) {
1532+
if (notebookID && !encryptedCtx) {
15301533
ctx.notebookID = notebookID;
15311534
}
15321535
if (focusedBlockID && focusedBlockID !== activeDocID) {

app/src/mobile/dock/MobileBacklinks.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {fetchPost} from "../../util/fetch";
33
import {Constants} from "../../constants";
44
import {openMobileFileById} from "../editor";
55
import {App} from "../../index";
6+
import {isEncryptedBox} from "../../util/pathName";
67

78
export class MobileBacklinks {
89
public element: HTMLElement;
@@ -106,12 +107,16 @@ export class MobileBacklinks {
106107
}
107108

108109
public update() {
109-
fetchPost("/api/ref/getBacklink", {
110+
const param: IObject = {
110111
id: window.siyuan.mobile.editor.protyle.block.id,
111112
beforeLen: this.beforeLen,
112113
k: "",
113114
mk: "",
114-
}, response => {
115+
};
116+
if (isEncryptedBox(window.siyuan.mobile.editor.protyle.notebookId)) {
117+
param.notebook = window.siyuan.mobile.editor.protyle.notebookId;
118+
}
119+
fetchPost("/api/ref/getBacklink", param, response => {
115120
this.notebookId = response.data.box;
116121
this.tree.updateData(response.data.backlinks);
117122
this.mTree.updateData(response.data.backmentions);

app/src/mobile/menu/search.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {fetchPost} from "../../util/fetch";
55
import {getIconByType} from "../../editor/getIcon";
66
import {preventScroll} from "../../protyle/scroll/preventScroll";
77
import {openModel} from "./model";
8-
import {getDisplayName, getNotebookIcon, getNotebookName, movePathTo, pathPosix} from "../../util/pathName";
8+
import {getDisplayName, getNotebookIcon, getNotebookName, isEncryptedBox, movePathTo, pathPosix} from "../../util/pathName";
99
import {getKeyByLiElement, initCriteriaMenu, moreMenu} from "../../search/menu";
1010
import {setStorageVal} from "../../protyle/util/compatibility";
1111
import {escapeHtml} from "../../util/escape";
@@ -290,7 +290,7 @@ export const updateSearchResult = (config: Config.IUILayoutTabSearchConfig, elem
290290
previousElement.setAttribute("disabled", "disabled");
291291
}
292292
const endpoint = config.method === 4 ? "/api/search/semanticSearchBlock" : "/api/search/fullTextSearchBlock";
293-
fetchPost(endpoint, {
293+
const searchParam: IObject = {
294294
query: config.query,
295295
method: config.method,
296296
types: config.types,
@@ -300,7 +300,16 @@ export const updateSearchResult = (config: Config.IUILayoutTabSearchConfig, elem
300300
orderBy: config.sort,
301301
page: config.page,
302302
pageSize: 32,
303-
}, (response) => {
303+
};
304+
// 限定在单个加密 box 内搜索时带 notebook,让内核走加密 db;跨 box 或全局搜索走原函数
305+
const idPaths = config.idPath || [];
306+
if (idPaths.length > 0) {
307+
const box = idPaths[0].split("/")[0];
308+
if (isEncryptedBox(box) && idPaths.every(p => p.split("/")[0] === box)) {
309+
searchParam.notebook = box;
310+
}
311+
}
312+
fetchPost(endpoint, searchParam, (response) => {
304313
onRecentBlocks(response.data.blocks, config, response, focusId);
305314
loadingElement.classList.add("fn__none");
306315
if (config.page < response.data.pageCount) {

app/src/protyle/hint/extend.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {hasClosestBlock, hasClosestByClassName} from "../util/hasClosest";
1111
import {getContenteditableElement, getTopAloneElement} from "../wysiwyg/getBlock";
1212
import {replaceFileName} from "../../editor/rename";
1313
import {transaction} from "../wysiwyg/transaction";
14-
import {getAssetName, getDisplayName, pathPosix} from "../../util/pathName";
14+
import {getAssetName, getDisplayName, isEncryptedBox, pathPosix} from "../../util/pathName";
1515
import {genEmptyElement} from "../../block/util";
1616
import {updateListOrder} from "../wysiwyg/list";
1717
import {escapeHtml} from "../../util/escape";
@@ -458,14 +458,18 @@ export const genHintItemHTML = (item: IBlock) => {
458458
export const hintRef = (key: string, protyle: IProtyle, source: THintSource): IHintData[] => {
459459
const nodeElement = hasClosestBlock(getEditorRange(protyle.wysiwyg.element).startContainer);
460460
protyle.hint.genLoading(protyle);
461-
fetchPost("/api/search/searchRefBlock", {
461+
const refParam: IObject = {
462462
k: key,
463463
id: nodeElement ? nodeElement.getAttribute("data-node-id") : protyle.block.parentID,
464464
beforeLen: Math.floor((Math.max(protyle.element.clientWidth / 2, 320) - 58) / 28.8),
465465
rootID: source === "av" ? "" : protyle.block.rootID,
466466
isDatabase: source === "av",
467467
isSquareBrackets: ["[[", "【【"].includes(protyle.hint.splitChar)
468-
}, (response) => {
468+
};
469+
if (isEncryptedBox(protyle.notebookId)) {
470+
refParam.notebook = protyle.notebookId;
471+
}
472+
fetchPost("/api/search/searchRefBlock", refParam, (response) => {
469473
const dataList: IHintData[] = [];
470474
if (response.data.newDoc) {
471475
const newFileName = Lute.UnEscapeHTMLStr(replaceFileName(response.data.k));
@@ -514,13 +518,17 @@ export const hintEmbed = (key: string, protyle: IProtyle): IHintData[] => {
514518
}
515519
protyle.hint.genLoading(protyle);
516520
const nodeElement = hasClosestBlock(getEditorRange(protyle.wysiwyg.element).startContainer);
517-
fetchPost("/api/search/searchRefBlock", {
521+
const embedParam: IObject = {
518522
k: key,
519523
isDatabase: false,
520524
beforeLen: Math.floor((Math.max(protyle.element.clientWidth / 2, 320) - 58) / 28.8),
521525
id: nodeElement ? nodeElement.getAttribute("data-node-id") : protyle.block.parentID,
522526
rootID: protyle.block.rootID,
523-
}, (response) => {
527+
};
528+
if (isEncryptedBox(protyle.notebookId)) {
529+
embedParam.notebook = protyle.notebookId;
530+
}
531+
fetchPost("/api/search/searchRefBlock", embedParam, (response) => {
524532
const dataList: IHintData[] = [];
525533
response.data.blocks.forEach((item: IBlock) => {
526534
dataList.push({

app/src/protyle/hint/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {highlightRender} from "../render/highlightRender";
2525
import {assetMenu, imgMenu} from "../../menus/protyle";
2626
import {hideElements} from "../ui/hideElements";
2727
import {fetchPost} from "../../util/fetch";
28-
import {getDisplayName, pathPosix} from "../../util/pathName";
28+
import {getDisplayName, isEncryptedBox, pathPosix} from "../../util/pathName";
2929
import {
3030
addEmoji,
3131
filterEmoji,
@@ -360,13 +360,17 @@ ${unicode2Emoji(emoji.unicode)}</button>`;
360360

361361
private genSearchHTML(protyle: IProtyle, searchElement: HTMLInputElement, nodeElement: false | HTMLElement, oldValue: string, source: THintSource) {
362362
this.element.lastElementChild.innerHTML = '<div class="ft__center"><img style="height:32px;width:32px;" src="/stage/loading-pure.svg"></div>';
363-
fetchPost("/api/search/searchRefBlock", {
363+
const searchParam: IObject = {
364364
k: searchElement.value,
365365
id: nodeElement ? nodeElement.getAttribute("data-node-id") : protyle.block.parentID,
366366
beforeLen: Math.floor((Math.max(protyle.element.clientWidth / 2, 320) - 58) / 28.8),
367367
rootID: source === "av" ? "" : protyle.block.rootID,
368368
isDatabase: source === "av",
369-
}, (response) => {
369+
};
370+
if (isEncryptedBox(protyle.notebookId)) {
371+
searchParam.notebook = protyle.notebookId;
372+
}
373+
fetchPost("/api/search/searchRefBlock", searchParam, (response) => {
370374
let searchHTML = "";
371375
if (response.data.newDoc) {
372376
const blockRefText = `((newFile "${oldValue}"${Constants.ZWSP}'${response.data.k}${Lute.Caret}'))`;

app/src/search/util.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {openFile, openFileById} from "../editor/util";
99
import {showMessage} from "../dialog/message";
1010
import {reloadProtyle} from "../protyle/util/reload";
1111
import {MenuItem} from "../menus/Menu";
12-
import {getDisplayName, getNotebookIcon, getNotebookName, movePathTo, pathPosix, useShell} from "../util/pathName";
12+
import {getDisplayName, getNotebookIcon, getNotebookName, isEncryptedBox, movePathTo, pathPosix, useShell} from "../util/pathName";
1313
import {Protyle} from "../protyle";
1414
import {onGet} from "../protyle/util/onGet";
1515
import {addLoading} from "../protyle/ui/initUI";
@@ -1349,7 +1349,7 @@ export const inputEvent = (element: Element, config: Config.IUILayoutTabSearchCo
13491349
previousElement.setAttribute("disabled", "disabled");
13501350
}
13511351
const endpoint = config.method === 4 ? "/api/search/semanticSearchBlock" : "/api/search/fullTextSearchBlock";
1352-
fetchPost(endpoint, {
1352+
const searchParam: IObject = {
13531353
query: config.query,
13541354
method: config.method,
13551355
types: config.types,
@@ -1359,7 +1359,16 @@ export const inputEvent = (element: Element, config: Config.IUILayoutTabSearchCo
13591359
orderBy: config.sort,
13601360
page: config.page || 1,
13611361
pageSize: 32,
1362-
}, (response) => {
1362+
};
1363+
// 限定在单个加密 box 内搜索时带 notebook,让内核走加密 db;跨 box 或全局搜索走原函数
1364+
const idPaths = config.idPath || [];
1365+
if (idPaths.length > 0) {
1366+
const box = idPaths[0].split("/")[0];
1367+
if (isEncryptedBox(box) && idPaths.every(p => p.split("/")[0] === box)) {
1368+
searchParam.notebook = box;
1369+
}
1370+
}
1371+
fetchPost(endpoint, searchParam, (response) => {
13631372
const searchReqId = config.method === 4
13641373
? window.siyuan.reqIds["/api/search/semanticSearchBlock"]
13651374
: window.siyuan.reqIds["/api/search/fullTextSearchBlock"];

app/src/util/pathName.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,18 @@ export const setNoteBook = (cb?: (notebook: INotebook[]) => void, flashcard = fa
771771
});
772772
};
773773

774+
/**
775+
* 返回指定 boxID 是否为加密笔记本。
776+
* 用于前端在加密 box 上下文里给 getDoc / 反链 / 搜索请求带上 notebook 参数,
777+
* 让内核走 InBox 版(查加密 blocktree + content db)。
778+
*/
779+
export const isEncryptedBox = (boxId: string): boolean => {
780+
if (!boxId) {
781+
return false;
782+
}
783+
return !!window.siyuan.notebooks?.find((item) => item.id === boxId && item.encrypted);
784+
};
785+
774786
/**
775787
* 规范化并校验相对路径:允许子目录,但禁止通过 ".." 穿越到根外。
776788
* 用于插件存储,确保路径不逃出指定根目录。

kernel/api/filetree.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,8 +1200,21 @@ func getDoc(c *gin.Context) {
12001200
highlight = highlightArg.(bool)
12011201
}
12021202

1203-
blockCount, content, parentID, parent2ID, rootID, typ, eof, scroll, boxID, docPath, isBacklinkExpand, keywords, err :=
1204-
model.GetDoc(startID, endID, id, index, query, queryTypes, querySubTypes, queryMethod, mode, size, isBacklink, originalRefBlockIDs, highlight)
1203+
var blockCount int
1204+
var content, parentID, parent2ID, rootID, typ string
1205+
var eof, scroll bool
1206+
var boxID, docPath string
1207+
var isBacklinkExpand bool
1208+
var keywords []string
1209+
var err error
1210+
// 加密笔记本的打开文档走 InBox 版(查加密 blocktree + content db)
1211+
if notebook, ok := arg["notebook"].(string); ok && notebook != "" && model.IsEncryptedBox(notebook) {
1212+
blockCount, content, parentID, parent2ID, rootID, typ, eof, scroll, boxID, docPath, isBacklinkExpand, keywords, err =
1213+
model.GetDocInBox(startID, endID, id, index, query, queryTypes, querySubTypes, queryMethod, mode, size, isBacklink, originalRefBlockIDs, highlight, notebook)
1214+
} else {
1215+
blockCount, content, parentID, parent2ID, rootID, typ, eof, scroll, boxID, docPath, isBacklinkExpand, keywords, err =
1216+
model.GetDoc(startID, endID, id, index, query, queryTypes, querySubTypes, queryMethod, mode, size, isBacklink, originalRefBlockIDs, highlight)
1217+
}
12051218
if errors.Is(err, model.ErrBlockNotFound) {
12061219
ret.Code = 3
12071220
return

0 commit comments

Comments
 (0)