remnote-bridge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/connect.d.ts +12 -0
- package/dist/cli/commands/connect.js +124 -0
- package/dist/cli/commands/disconnect.d.ts +11 -0
- package/dist/cli/commands/disconnect.js +100 -0
- package/dist/cli/commands/edit-rem.d.ts +13 -0
- package/dist/cli/commands/edit-rem.js +83 -0
- package/dist/cli/commands/edit-tree.d.ts +14 -0
- package/dist/cli/commands/edit-tree.js +67 -0
- package/dist/cli/commands/health.d.ts +12 -0
- package/dist/cli/commands/health.js +100 -0
- package/dist/cli/commands/install-skill.d.ts +6 -0
- package/dist/cli/commands/install-skill.js +39 -0
- package/dist/cli/commands/read-context.d.ts +20 -0
- package/dist/cli/commands/read-context.js +77 -0
- package/dist/cli/commands/read-globe.d.ts +16 -0
- package/dist/cli/commands/read-globe.js +60 -0
- package/dist/cli/commands/read-rem.d.ts +16 -0
- package/dist/cli/commands/read-rem.js +80 -0
- package/dist/cli/commands/read-tree.d.ts +17 -0
- package/dist/cli/commands/read-tree.js +85 -0
- package/dist/cli/commands/search.d.ts +12 -0
- package/dist/cli/commands/search.js +65 -0
- package/dist/cli/config.d.ts +55 -0
- package/dist/cli/config.js +139 -0
- package/dist/cli/daemon/daemon.d.ts +11 -0
- package/dist/cli/daemon/daemon.js +186 -0
- package/dist/cli/daemon/dev-server.d.ts +26 -0
- package/dist/cli/daemon/dev-server.js +81 -0
- package/dist/cli/daemon/pid.d.ts +34 -0
- package/dist/cli/daemon/pid.js +67 -0
- package/dist/cli/daemon/send-request.d.ts +24 -0
- package/dist/cli/daemon/send-request.js +92 -0
- package/dist/cli/handlers/context-read-handler.d.ts +18 -0
- package/dist/cli/handlers/context-read-handler.js +24 -0
- package/dist/cli/handlers/edit-handler.d.ts +30 -0
- package/dist/cli/handlers/edit-handler.js +133 -0
- package/dist/cli/handlers/globe-read-handler.d.ts +17 -0
- package/dist/cli/handlers/globe-read-handler.js +23 -0
- package/dist/cli/handlers/read-handler.d.ts +16 -0
- package/dist/cli/handlers/read-handler.js +78 -0
- package/dist/cli/handlers/rem-cache.d.ts +19 -0
- package/dist/cli/handlers/rem-cache.js +63 -0
- package/dist/cli/handlers/tree-edit-handler.d.ts +30 -0
- package/dist/cli/handlers/tree-edit-handler.js +188 -0
- package/dist/cli/handlers/tree-parser.d.ts +95 -0
- package/dist/cli/handlers/tree-parser.js +506 -0
- package/dist/cli/handlers/tree-read-handler.d.ts +28 -0
- package/dist/cli/handlers/tree-read-handler.js +53 -0
- package/dist/cli/main.d.ts +7 -0
- package/dist/cli/main.js +300 -0
- package/dist/cli/protocol.d.ts +39 -0
- package/dist/cli/protocol.js +35 -0
- package/dist/cli/server/config-server.d.ts +26 -0
- package/dist/cli/server/config-server.js +363 -0
- package/dist/cli/server/ws-server.d.ts +68 -0
- package/dist/cli/server/ws-server.js +335 -0
- package/dist/cli/utils/output.d.ts +11 -0
- package/dist/cli/utils/output.js +13 -0
- package/dist/mcp/daemon-client.d.ts +31 -0
- package/dist/mcp/daemon-client.js +99 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.js +68 -0
- package/dist/mcp/instructions.d.ts +1 -0
- package/dist/mcp/instructions.js +249 -0
- package/dist/mcp/resources/edit-tree-guide.d.ts +1 -0
- package/dist/mcp/resources/edit-tree-guide.js +197 -0
- package/dist/mcp/resources/error-reference.d.ts +1 -0
- package/dist/mcp/resources/error-reference.js +132 -0
- package/dist/mcp/resources/outline-format.d.ts +1 -0
- package/dist/mcp/resources/outline-format.js +104 -0
- package/dist/mcp/resources/rem-object-fields.d.ts +1 -0
- package/dist/mcp/resources/rem-object-fields.js +331 -0
- package/dist/mcp/resources/separator-flashcard.d.ts +1 -0
- package/dist/mcp/resources/separator-flashcard.js +120 -0
- package/dist/mcp/tools/edit-tools.d.ts +5 -0
- package/dist/mcp/tools/edit-tools.js +47 -0
- package/dist/mcp/tools/infra-tools.d.ts +5 -0
- package/dist/mcp/tools/infra-tools.js +43 -0
- package/dist/mcp/tools/read-tools.d.ts +5 -0
- package/dist/mcp/tools/read-tools.js +195 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/types.js +4 -0
- package/docs/instruction/connect.md +158 -0
- package/docs/instruction/disconnect.md +146 -0
- package/docs/instruction/edit-rem.md +509 -0
- package/docs/instruction/edit-tree.md +419 -0
- package/docs/instruction/health.md +159 -0
- package/docs/instruction/overall.md +751 -0
- package/docs/instruction/read-context.md +353 -0
- package/docs/instruction/read-globe.md +206 -0
- package/docs/instruction/read-rem.md +476 -0
- package/docs/instruction/read-tree.md +428 -0
- package/docs/instruction/search.md +196 -0
- package/package.json +41 -0
- package/remnote-plugin/package.json +48 -0
- package/remnote-plugin/postcss.config.js +5 -0
- package/remnote-plugin/public/bridge-icon.svg +8 -0
- package/remnote-plugin/public/manifest.json +22 -0
- package/remnote-plugin/src/bridge/message-router.ts +57 -0
- package/remnote-plugin/src/bridge/websocket-client.ts +245 -0
- package/remnote-plugin/src/index.css +1 -0
- package/remnote-plugin/src/services/breadcrumb.ts +26 -0
- package/remnote-plugin/src/services/create-rem.ts +59 -0
- package/remnote-plugin/src/services/delete-rem.ts +29 -0
- package/remnote-plugin/src/services/index.ts +16 -0
- package/remnote-plugin/src/services/move-rem.ts +39 -0
- package/remnote-plugin/src/services/powerup-filter.ts +31 -0
- package/remnote-plugin/src/services/read-context.ts +368 -0
- package/remnote-plugin/src/services/read-globe.ts +197 -0
- package/remnote-plugin/src/services/read-rem.ts +284 -0
- package/remnote-plugin/src/services/read-tree.ts +222 -0
- package/remnote-plugin/src/services/rem-builder.ts +124 -0
- package/remnote-plugin/src/services/reorder-children.ts +61 -0
- package/remnote-plugin/src/services/search.ts +56 -0
- package/remnote-plugin/src/services/write-rem-fields.ts +254 -0
- package/remnote-plugin/src/settings.ts +12 -0
- package/remnote-plugin/src/style.css +45 -0
- package/remnote-plugin/src/test-scripts/AGENTS.md +46 -0
- package/remnote-plugin/src/test-scripts/test-actions.ts +230 -0
- package/remnote-plugin/src/test-scripts/test-powerup-rendering.ts +722 -0
- package/remnote-plugin/src/test-scripts/test-rem-type-mapping.ts +283 -0
- package/remnote-plugin/src/test-scripts/test-richtext-builder.ts +207 -0
- package/remnote-plugin/src/test-scripts/test-richtext-matrix.ts +332 -0
- package/remnote-plugin/src/test-scripts/test-richtext-remaining.ts +245 -0
- package/remnote-plugin/src/test-scripts/test-rw-fields.ts +399 -0
- package/remnote-plugin/src/types.ts +419 -0
- package/remnote-plugin/src/utils/elision.ts +45 -0
- package/remnote-plugin/src/utils/index.ts +10 -0
- package/remnote-plugin/src/utils/tree-serializer.ts +269 -0
- package/remnote-plugin/src/widgets/bridge_widget.tsx +170 -0
- package/remnote-plugin/src/widgets/index.tsx +82 -0
- package/remnote-plugin/tailwind.config.js +7 -0
- package/remnote-plugin/tsconfig.json +21 -0
- package/remnote-plugin/webpack.config.js +125 -0
- package/skill/SKILL.md +428 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read-context service — 上下文视图(focus / page 模式)
|
|
3
|
+
*
|
|
4
|
+
* 同态命名:read_context (action) → read-context.ts (文件) → readContext (函数)
|
|
5
|
+
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* - page 模式:以当前页面顶层 Rem 为中心展开子树
|
|
8
|
+
* - focus 模式:以当前 focus 的 Rem 为中心构建鱼眼视图
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
|
|
12
|
+
import {
|
|
13
|
+
type OutlineNode,
|
|
14
|
+
type TreeNode,
|
|
15
|
+
type ElidedNode,
|
|
16
|
+
buildOutline,
|
|
17
|
+
createMinimalSerializableRem,
|
|
18
|
+
} from '../utils/tree-serializer';
|
|
19
|
+
import { filterNoisyChildren } from './powerup-filter';
|
|
20
|
+
import { sliceSiblings } from '../utils/elision';
|
|
21
|
+
import { buildBreadcrumb } from './breadcrumb';
|
|
22
|
+
import { buildFullSerializableRem as buildFullRem } from './rem-builder';
|
|
23
|
+
|
|
24
|
+
export interface ReadContextPayload {
|
|
25
|
+
mode?: 'focus' | 'page';
|
|
26
|
+
ancestorLevels?: number;
|
|
27
|
+
maxNodes?: number;
|
|
28
|
+
maxSiblings?: number;
|
|
29
|
+
depth?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ReadContextResult {
|
|
33
|
+
nodeCount: number;
|
|
34
|
+
outline: string;
|
|
35
|
+
breadcrumb: string[];
|
|
36
|
+
mode: 'focus' | 'page';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readContext(
|
|
40
|
+
plugin: ReactRNPlugin,
|
|
41
|
+
payload: ReadContextPayload,
|
|
42
|
+
): Promise<ReadContextResult> {
|
|
43
|
+
const {
|
|
44
|
+
mode = 'focus',
|
|
45
|
+
ancestorLevels = 2,
|
|
46
|
+
maxNodes = 200,
|
|
47
|
+
maxSiblings = 20,
|
|
48
|
+
depth = 3,
|
|
49
|
+
} = payload;
|
|
50
|
+
|
|
51
|
+
if (mode === 'page') {
|
|
52
|
+
return readContextPage(plugin, { maxNodes, maxSiblings, depth });
|
|
53
|
+
}
|
|
54
|
+
return readContextFocus(plugin, { ancestorLevels, maxNodes, maxSiblings });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ────────────────────────── Page 模式 ──────────────────────────
|
|
58
|
+
|
|
59
|
+
async function readContextPage(
|
|
60
|
+
plugin: ReactRNPlugin,
|
|
61
|
+
opts: { maxNodes: number; maxSiblings: number; depth: number },
|
|
62
|
+
): Promise<ReadContextResult> {
|
|
63
|
+
const paneId = await plugin.window.getFocusedPaneId();
|
|
64
|
+
if (!paneId) throw new Error('无法获取当前面板 ID,请确保有打开的页面');
|
|
65
|
+
|
|
66
|
+
const remId = await plugin.window.getOpenPaneRemId(paneId);
|
|
67
|
+
if (!remId) throw new Error('当前面板没有打开任何 Rem');
|
|
68
|
+
|
|
69
|
+
const pageRem = await plugin.rem.findOne(remId);
|
|
70
|
+
if (!pageRem) throw new Error(`Page Rem not found: ${remId}`);
|
|
71
|
+
|
|
72
|
+
const breadcrumb = await buildBreadcrumb(plugin, pageRem);
|
|
73
|
+
|
|
74
|
+
let nodeCount = 0;
|
|
75
|
+
const budget = { remaining: opts.maxNodes };
|
|
76
|
+
|
|
77
|
+
const rootNode = await buildSubtreeNode(plugin, pageRem, 0, opts.depth, opts.maxSiblings, budget);
|
|
78
|
+
nodeCount = opts.maxNodes - budget.remaining;
|
|
79
|
+
|
|
80
|
+
const header = `<!-- page: ${breadcrumb[breadcrumb.length - 1] || remId} -->\n<!-- path: ${breadcrumb.join(' > ')} -->`;
|
|
81
|
+
const outline = header + '\n' + buildOutline(rootNode);
|
|
82
|
+
|
|
83
|
+
return { nodeCount, outline, breadcrumb, mode: 'page' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ────────────────────────── Focus 模式(鱼眼视图) ──────────────────────────
|
|
87
|
+
|
|
88
|
+
async function readContextFocus(
|
|
89
|
+
plugin: ReactRNPlugin,
|
|
90
|
+
opts: { ancestorLevels: number; maxNodes: number; maxSiblings: number },
|
|
91
|
+
): Promise<ReadContextResult> {
|
|
92
|
+
const focusRem = await plugin.focus.getFocusedRem();
|
|
93
|
+
if (!focusRem) throw new Error('当前没有聚焦的 Rem,请先在 RemNote 中点击一个 Rem');
|
|
94
|
+
|
|
95
|
+
const breadcrumb = await buildBreadcrumb(plugin, focusRem);
|
|
96
|
+
|
|
97
|
+
// 向上追溯 ancestorLevels 层
|
|
98
|
+
const ancestorPath: Rem[] = [focusRem];
|
|
99
|
+
let current: Rem | undefined = focusRem;
|
|
100
|
+
for (let i = 0; i < opts.ancestorLevels; i++) {
|
|
101
|
+
const parent = await current.getParentRem();
|
|
102
|
+
if (!parent) break;
|
|
103
|
+
ancestorPath.unshift(parent);
|
|
104
|
+
current = parent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const budget = { remaining: opts.maxNodes };
|
|
108
|
+
let nodeCount = 0;
|
|
109
|
+
|
|
110
|
+
// 从最顶层祖先开始构建鱼眼视图
|
|
111
|
+
const topAncestor = ancestorPath[0];
|
|
112
|
+
const rootNode = await buildFisheyeNode(
|
|
113
|
+
plugin, topAncestor, ancestorPath, 0, focusRem._id, opts.maxSiblings, budget,
|
|
114
|
+
);
|
|
115
|
+
nodeCount = opts.maxNodes - budget.remaining;
|
|
116
|
+
|
|
117
|
+
const focusName = breadcrumb[breadcrumb.length - 1] || focusRem._id;
|
|
118
|
+
const header = `<!-- path: ${breadcrumb.join(' > ')} -->\n<!-- focus: ${focusName} (${focusRem._id}) -->`;
|
|
119
|
+
const outline = header + '\n' + buildOutline(rootNode);
|
|
120
|
+
|
|
121
|
+
return { nodeCount, outline, breadcrumb, mode: 'focus' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 鱼眼节点构建。
|
|
126
|
+
*
|
|
127
|
+
* 展开深度梯度:
|
|
128
|
+
* - 焦点本身:depth=3
|
|
129
|
+
* - 焦点的 siblings:depth=1(前 3 个 children)
|
|
130
|
+
* - 祖先节点(路径上):展开
|
|
131
|
+
* - 祖先的 siblings(叔伯):depth=0
|
|
132
|
+
*/
|
|
133
|
+
async function buildFisheyeNode(
|
|
134
|
+
plugin: ReactRNPlugin,
|
|
135
|
+
rem: Rem,
|
|
136
|
+
ancestorPath: Rem[],
|
|
137
|
+
pathIndex: number,
|
|
138
|
+
focusId: string,
|
|
139
|
+
maxSiblings: number,
|
|
140
|
+
budget: { remaining: number },
|
|
141
|
+
): Promise<OutlineNode> {
|
|
142
|
+
budget.remaining--;
|
|
143
|
+
|
|
144
|
+
const isFocus = rem._id === focusId;
|
|
145
|
+
const isOnPath = ancestorPath.some(a => a._id === rem._id);
|
|
146
|
+
const nextPathRem = pathIndex + 1 < ancestorPath.length ? ancestorPath[pathIndex + 1] : null;
|
|
147
|
+
|
|
148
|
+
const allChildren = await rem.getChildrenRem();
|
|
149
|
+
const children = await filterNoisyChildren(allChildren);
|
|
150
|
+
|
|
151
|
+
const serializable = await buildMinimalSerializableRem(plugin, rem, children.length, isFocus);
|
|
152
|
+
|
|
153
|
+
const childNodes: TreeNode[] = [];
|
|
154
|
+
|
|
155
|
+
if (isFocus) {
|
|
156
|
+
// 焦点本身:完整展开 depth=3
|
|
157
|
+
const focusChildren = await processChildrenWithElision(
|
|
158
|
+
plugin, children, maxSiblings, rem._id, budget, 0, 3,
|
|
159
|
+
);
|
|
160
|
+
childNodes.push(...focusChildren);
|
|
161
|
+
} else if (isOnPath && nextPathRem) {
|
|
162
|
+
// 路径上的祖先:展开所有 siblings,但非路径 children 不深入
|
|
163
|
+
const { visibleIndices, elided } = sliceSiblings(children.length, maxSiblings, rem._id);
|
|
164
|
+
|
|
165
|
+
const processChild = async (child: Rem): Promise<TreeNode> => {
|
|
166
|
+
if (budget.remaining <= 0) {
|
|
167
|
+
return createBudgetExhaustedNode(rem._id, 0, 0, 0); // will be handled
|
|
168
|
+
}
|
|
169
|
+
if (child._id === nextPathRem._id) {
|
|
170
|
+
// 路径上的子节点:继续鱼眼递归
|
|
171
|
+
return buildFisheyeNode(plugin, child, ancestorPath, pathIndex + 1, focusId, maxSiblings, budget);
|
|
172
|
+
}
|
|
173
|
+
if (child._id === focusId) {
|
|
174
|
+
// 焦点节点
|
|
175
|
+
return buildFisheyeNode(plugin, child, ancestorPath, pathIndex + 1, focusId, maxSiblings, budget);
|
|
176
|
+
}
|
|
177
|
+
// 焦点的 siblings:展开 1 层
|
|
178
|
+
const isSiblingOfFocus = nextPathRem._id === focusId;
|
|
179
|
+
if (isSiblingOfFocus) {
|
|
180
|
+
return buildShallowNode(plugin, child, 1, 3, maxSiblings, budget);
|
|
181
|
+
}
|
|
182
|
+
// 叔伯节点:depth=0
|
|
183
|
+
return buildShallowNode(plugin, child, 0, 0, maxSiblings, budget);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (visibleIndices) {
|
|
187
|
+
const { head, tail } = visibleIndices;
|
|
188
|
+
for (let i = 0; i < head && budget.remaining > 0; i++) {
|
|
189
|
+
childNodes.push(await processChild(children[i]));
|
|
190
|
+
}
|
|
191
|
+
if (elided) {
|
|
192
|
+
childNodes.push({
|
|
193
|
+
type: 'elided', count: elided.count, isExact: false,
|
|
194
|
+
parentId: elided.parentId, rangeFrom: elided.rangeFrom,
|
|
195
|
+
rangeTo: elided.rangeTo, totalSiblings: elided.totalSiblings,
|
|
196
|
+
} as ElidedNode);
|
|
197
|
+
}
|
|
198
|
+
const tailStart = children.length - tail;
|
|
199
|
+
for (let i = tailStart; i < children.length && budget.remaining > 0; i++) {
|
|
200
|
+
childNodes.push(await processChild(children[i]));
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
for (let i = 0; i < children.length; i++) {
|
|
204
|
+
if (budget.remaining <= 0) {
|
|
205
|
+
childNodes.push({
|
|
206
|
+
type: 'elided', count: children.length - i, isExact: false,
|
|
207
|
+
parentId: rem._id, rangeFrom: i, rangeTo: children.length - 1,
|
|
208
|
+
totalSiblings: children.length,
|
|
209
|
+
} as ElidedNode);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
childNodes.push(await processChild(children[i]));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// 叔伯节点(不在路径上):不展开 children
|
|
217
|
+
|
|
218
|
+
return { rem: serializable, children: childNodes, folded: childNodes.length === 0 && children.length > 0 };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 浅层节点构建:展开到指定深度,每层最多 maxChildrenPreview 个子节点。
|
|
223
|
+
*/
|
|
224
|
+
async function buildShallowNode(
|
|
225
|
+
plugin: ReactRNPlugin,
|
|
226
|
+
rem: Rem,
|
|
227
|
+
remainingDepth: number,
|
|
228
|
+
maxChildrenPreview: number,
|
|
229
|
+
maxSiblings: number,
|
|
230
|
+
budget: { remaining: number },
|
|
231
|
+
): Promise<OutlineNode> {
|
|
232
|
+
budget.remaining--;
|
|
233
|
+
|
|
234
|
+
const allChildren = await rem.getChildrenRem();
|
|
235
|
+
const children = await filterNoisyChildren(allChildren);
|
|
236
|
+
|
|
237
|
+
const serializable = await buildMinimalSerializableRem(plugin, rem, children.length, false);
|
|
238
|
+
|
|
239
|
+
const childNodes: TreeNode[] = [];
|
|
240
|
+
|
|
241
|
+
if (remainingDepth > 0 && children.length > 0) {
|
|
242
|
+
const showCount = Math.min(children.length, maxChildrenPreview, maxSiblings);
|
|
243
|
+
for (let i = 0; i < showCount && budget.remaining > 0; i++) {
|
|
244
|
+
childNodes.push(await buildShallowNode(plugin, children[i], remainingDepth - 1, maxChildrenPreview, maxSiblings, budget));
|
|
245
|
+
}
|
|
246
|
+
if (showCount < children.length) {
|
|
247
|
+
childNodes.push({
|
|
248
|
+
type: 'elided', count: children.length - showCount, isExact: false,
|
|
249
|
+
parentId: rem._id, rangeFrom: showCount, rangeTo: children.length - 1,
|
|
250
|
+
totalSiblings: children.length,
|
|
251
|
+
} as ElidedNode);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
rem: serializable,
|
|
257
|
+
children: childNodes,
|
|
258
|
+
folded: childNodes.length === 0 && children.length > 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ────────────────────────── 共享辅助 ──────────────────────────
|
|
263
|
+
|
|
264
|
+
async function buildSubtreeNode(
|
|
265
|
+
plugin: ReactRNPlugin,
|
|
266
|
+
rem: Rem,
|
|
267
|
+
currentDepth: number,
|
|
268
|
+
maxDepth: number,
|
|
269
|
+
maxSiblings: number,
|
|
270
|
+
budget: { remaining: number },
|
|
271
|
+
): Promise<OutlineNode> {
|
|
272
|
+
budget.remaining--;
|
|
273
|
+
|
|
274
|
+
const allChildren = await rem.getChildrenRem();
|
|
275
|
+
const children = await filterNoisyChildren(allChildren);
|
|
276
|
+
const shouldFold = maxDepth !== -1 && currentDepth >= maxDepth;
|
|
277
|
+
const folded = shouldFold && children.length > 0;
|
|
278
|
+
|
|
279
|
+
const serializable = await buildFullRem(plugin, rem, children);
|
|
280
|
+
|
|
281
|
+
const childNodes: TreeNode[] = [];
|
|
282
|
+
if (!folded) {
|
|
283
|
+
const processed = await processChildrenWithElision(
|
|
284
|
+
plugin, children, maxSiblings, rem._id, budget, currentDepth + 1, maxDepth,
|
|
285
|
+
);
|
|
286
|
+
childNodes.push(...processed);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { rem: serializable, children: childNodes, folded };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function processChildrenWithElision(
|
|
293
|
+
plugin: ReactRNPlugin,
|
|
294
|
+
children: Rem[],
|
|
295
|
+
maxSiblings: number,
|
|
296
|
+
parentId: string,
|
|
297
|
+
budget: { remaining: number },
|
|
298
|
+
nextDepth: number,
|
|
299
|
+
maxDepth: number,
|
|
300
|
+
): Promise<TreeNode[]> {
|
|
301
|
+
const result: TreeNode[] = [];
|
|
302
|
+
const { visibleIndices, elided } = sliceSiblings(children.length, maxSiblings, parentId);
|
|
303
|
+
|
|
304
|
+
if (visibleIndices) {
|
|
305
|
+
const { head, tail } = visibleIndices;
|
|
306
|
+
for (let i = 0; i < head && budget.remaining > 0; i++) {
|
|
307
|
+
result.push(await buildSubtreeNode(plugin, children[i], nextDepth, maxDepth, maxSiblings, budget));
|
|
308
|
+
}
|
|
309
|
+
if (elided) {
|
|
310
|
+
const atMaxDepth = maxDepth !== -1 && nextDepth >= maxDepth;
|
|
311
|
+
result.push({
|
|
312
|
+
type: 'elided', count: elided.count, isExact: atMaxDepth,
|
|
313
|
+
parentId: elided.parentId, rangeFrom: elided.rangeFrom,
|
|
314
|
+
rangeTo: elided.rangeTo, totalSiblings: elided.totalSiblings,
|
|
315
|
+
} as ElidedNode);
|
|
316
|
+
}
|
|
317
|
+
const tailStart = children.length - tail;
|
|
318
|
+
for (let i = tailStart; i < children.length && budget.remaining > 0; i++) {
|
|
319
|
+
result.push(await buildSubtreeNode(plugin, children[i], nextDepth, maxDepth, maxSiblings, budget));
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
for (let i = 0; i < children.length; i++) {
|
|
323
|
+
if (budget.remaining <= 0) {
|
|
324
|
+
result.push({
|
|
325
|
+
type: 'elided', count: children.length - i, isExact: false,
|
|
326
|
+
parentId, rangeFrom: i, rangeTo: children.length - 1,
|
|
327
|
+
totalSiblings: children.length,
|
|
328
|
+
} as ElidedNode);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
result.push(await buildSubtreeNode(plugin, children[i], nextDepth, maxDepth, maxSiblings, budget));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function createBudgetExhaustedNode(parentId: string, rangeFrom: number, rangeTo: number, total: number): ElidedNode {
|
|
339
|
+
return {
|
|
340
|
+
type: 'elided', count: 1, isExact: false,
|
|
341
|
+
parentId, rangeFrom, rangeTo, totalSiblings: total,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function buildMinimalSerializableRem(
|
|
346
|
+
plugin: ReactRNPlugin,
|
|
347
|
+
rem: Rem,
|
|
348
|
+
childrenCount: number,
|
|
349
|
+
isFocusRem: boolean,
|
|
350
|
+
) {
|
|
351
|
+
const isPortal = rem.type === 6;
|
|
352
|
+
const [markdownText, isDocument, portalIncludedRems] = await Promise.all([
|
|
353
|
+
plugin.richText.toMarkdown(rem.text ?? []),
|
|
354
|
+
rem.isDocument(),
|
|
355
|
+
isPortal ? rem.getPortalDirectlyIncludedRem() : Promise.resolve([]),
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
return createMinimalSerializableRem({
|
|
359
|
+
id: rem._id,
|
|
360
|
+
markdownText: (isFocusRem ? '* ' : '') + markdownText.replace(/\n/g, ' '),
|
|
361
|
+
childrenCount,
|
|
362
|
+
isDocument,
|
|
363
|
+
isTopLevel: rem.parent === null,
|
|
364
|
+
isPortal,
|
|
365
|
+
...(isPortal ? { type: 'portal' as const, portalRefs: portalIncludedRems.map((r: Rem) => r._id) } : {}),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read-globe service — 知识库全局概览(仅 Document 层级)
|
|
3
|
+
*
|
|
4
|
+
* 同态命名:read_globe (action) → read-globe.ts (文件) → readGlobe (函数)
|
|
5
|
+
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* 1. 获取所有顶层 Rem(parent === null)
|
|
8
|
+
* 2. 只递归展开 Document 类型的节点
|
|
9
|
+
* 3. 非 Document 子 Rem 不展开(元数据中标注 children:N)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
|
|
13
|
+
import {
|
|
14
|
+
type OutlineNode,
|
|
15
|
+
type TreeNode,
|
|
16
|
+
type ElidedNode,
|
|
17
|
+
isElidedNode,
|
|
18
|
+
buildOutline,
|
|
19
|
+
serializeElidedLine,
|
|
20
|
+
createMinimalSerializableRem,
|
|
21
|
+
} from '../utils/tree-serializer';
|
|
22
|
+
import { sliceSiblings } from '../utils/elision';
|
|
23
|
+
import { filterNoisyChildren } from './powerup-filter';
|
|
24
|
+
|
|
25
|
+
export interface ReadGlobePayload {
|
|
26
|
+
depth?: number;
|
|
27
|
+
maxNodes?: number;
|
|
28
|
+
maxSiblings?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ReadGlobeResult {
|
|
32
|
+
nodeCount: number;
|
|
33
|
+
outline: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readGlobe(
|
|
37
|
+
plugin: ReactRNPlugin,
|
|
38
|
+
payload: ReadGlobePayload,
|
|
39
|
+
): Promise<ReadGlobeResult> {
|
|
40
|
+
const {
|
|
41
|
+
depth = -1,
|
|
42
|
+
maxNodes = 200,
|
|
43
|
+
maxSiblings = 20,
|
|
44
|
+
} = payload;
|
|
45
|
+
|
|
46
|
+
// 获取所有顶层 Rem(同步属性 rem.parent === null)
|
|
47
|
+
const allRems = await plugin.rem.getAll();
|
|
48
|
+
const topLevelRems = allRems.filter(rem => rem.parent === null);
|
|
49
|
+
|
|
50
|
+
// 过滤出 Document(并行)
|
|
51
|
+
const docFlags = await Promise.all(topLevelRems.map(r => r.isDocument()));
|
|
52
|
+
const topDocs = topLevelRems.filter((_, i) => docFlags[i]);
|
|
53
|
+
|
|
54
|
+
let nodeCount = 0;
|
|
55
|
+
const budget = { remaining: maxNodes };
|
|
56
|
+
|
|
57
|
+
async function buildGlobeNode(
|
|
58
|
+
rem: Rem,
|
|
59
|
+
currentDepth: number,
|
|
60
|
+
maxDepth: number,
|
|
61
|
+
): Promise<OutlineNode> {
|
|
62
|
+
nodeCount++;
|
|
63
|
+
budget.remaining--;
|
|
64
|
+
|
|
65
|
+
const allChildren = await rem.getChildrenRem();
|
|
66
|
+
const children = await filterNoisyChildren(allChildren);
|
|
67
|
+
|
|
68
|
+
// 过滤出 Document 子节点(并行)
|
|
69
|
+
const childDocFlags = await Promise.all(children.map(c => c.isDocument()));
|
|
70
|
+
const docChildren = children.filter((_, i) => childDocFlags[i]);
|
|
71
|
+
const nonDocCount = children.length - docChildren.length;
|
|
72
|
+
|
|
73
|
+
const shouldFold = maxDepth !== -1 && currentDepth >= maxDepth;
|
|
74
|
+
const folded = shouldFold && docChildren.length > 0;
|
|
75
|
+
|
|
76
|
+
const isPortal = rem.type === 6;
|
|
77
|
+
const [markdownText, portalIncludedRems] = await Promise.all([
|
|
78
|
+
plugin.richText.toMarkdown(rem.text ?? []),
|
|
79
|
+
isPortal ? rem.getPortalDirectlyIncludedRem() : Promise.resolve([]),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const serializable = createMinimalSerializableRem({
|
|
83
|
+
id: rem._id,
|
|
84
|
+
markdownText: markdownText.replace(/\n/g, ' '),
|
|
85
|
+
childrenCount: children.length,
|
|
86
|
+
isDocument: true,
|
|
87
|
+
isTopLevel: rem.parent === null,
|
|
88
|
+
isPortal,
|
|
89
|
+
...(isPortal ? { type: 'portal' as const, portalRefs: portalIncludedRems.map((r: Rem) => r._id) } : {}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const childNodes: TreeNode[] = [];
|
|
93
|
+
if (!folded && docChildren.length > 0) {
|
|
94
|
+
const { visibleIndices, elided } = sliceSiblings(docChildren.length, maxSiblings, rem._id);
|
|
95
|
+
|
|
96
|
+
if (visibleIndices) {
|
|
97
|
+
const { head, tail } = visibleIndices;
|
|
98
|
+
for (let i = 0; i < head && budget.remaining > 0; i++) {
|
|
99
|
+
childNodes.push(await buildGlobeNode(docChildren[i], currentDepth + 1, maxDepth));
|
|
100
|
+
}
|
|
101
|
+
if (elided) {
|
|
102
|
+
const atMaxDepth = maxDepth !== -1 && currentDepth + 1 >= maxDepth;
|
|
103
|
+
childNodes.push({
|
|
104
|
+
type: 'elided',
|
|
105
|
+
count: elided.count,
|
|
106
|
+
isExact: atMaxDepth,
|
|
107
|
+
parentId: elided.parentId,
|
|
108
|
+
rangeFrom: elided.rangeFrom,
|
|
109
|
+
rangeTo: elided.rangeTo,
|
|
110
|
+
totalSiblings: elided.totalSiblings,
|
|
111
|
+
} as ElidedNode);
|
|
112
|
+
}
|
|
113
|
+
const tailStart = docChildren.length - tail;
|
|
114
|
+
for (let i = tailStart; i < docChildren.length && budget.remaining > 0; i++) {
|
|
115
|
+
childNodes.push(await buildGlobeNode(docChildren[i], currentDepth + 1, maxDepth));
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
for (let i = 0; i < docChildren.length; i++) {
|
|
119
|
+
if (budget.remaining <= 0) {
|
|
120
|
+
const remainCount = docChildren.length - i;
|
|
121
|
+
childNodes.push({
|
|
122
|
+
type: 'elided',
|
|
123
|
+
count: remainCount,
|
|
124
|
+
isExact: false,
|
|
125
|
+
parentId: rem._id,
|
|
126
|
+
rangeFrom: i,
|
|
127
|
+
rangeTo: docChildren.length - 1,
|
|
128
|
+
totalSiblings: docChildren.length,
|
|
129
|
+
} as ElidedNode);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
childNodes.push(await buildGlobeNode(docChildren[i], currentDepth + 1, maxDepth));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { rem: serializable, children: childNodes, folded };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 构建虚拟根节点来容纳所有顶层 Document
|
|
141
|
+
const topNodes: TreeNode[] = [];
|
|
142
|
+
const { visibleIndices, elided } = sliceSiblings(topDocs.length, maxSiblings, 'root');
|
|
143
|
+
|
|
144
|
+
if (visibleIndices) {
|
|
145
|
+
const { head, tail } = visibleIndices;
|
|
146
|
+
for (let i = 0; i < head && budget.remaining > 0; i++) {
|
|
147
|
+
topNodes.push(await buildGlobeNode(topDocs[i], 0, depth));
|
|
148
|
+
}
|
|
149
|
+
if (elided) {
|
|
150
|
+
topNodes.push({
|
|
151
|
+
type: 'elided',
|
|
152
|
+
count: elided.count,
|
|
153
|
+
isExact: false,
|
|
154
|
+
parentId: 'root',
|
|
155
|
+
rangeFrom: elided.rangeFrom,
|
|
156
|
+
rangeTo: elided.rangeTo,
|
|
157
|
+
totalSiblings: elided.totalSiblings,
|
|
158
|
+
} as ElidedNode);
|
|
159
|
+
}
|
|
160
|
+
const tailStart = topDocs.length - tail;
|
|
161
|
+
for (let i = tailStart; i < topDocs.length && budget.remaining > 0; i++) {
|
|
162
|
+
topNodes.push(await buildGlobeNode(topDocs[i], 0, depth));
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
for (let i = 0; i < topDocs.length; i++) {
|
|
166
|
+
if (budget.remaining <= 0) {
|
|
167
|
+
const remainCount = topDocs.length - i;
|
|
168
|
+
topNodes.push({
|
|
169
|
+
type: 'elided',
|
|
170
|
+
count: remainCount,
|
|
171
|
+
isExact: false,
|
|
172
|
+
parentId: 'root',
|
|
173
|
+
rangeFrom: i,
|
|
174
|
+
rangeTo: topDocs.length - 1,
|
|
175
|
+
totalSiblings: topDocs.length,
|
|
176
|
+
} as ElidedNode);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
topNodes.push(await buildGlobeNode(topDocs[i], 0, depth));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 序列化:多个顶层节点逐个 buildOutline 再拼接
|
|
184
|
+
const lines: string[] = ['<!-- globe: 知识库概览 -->'];
|
|
185
|
+
for (const node of topNodes) {
|
|
186
|
+
if (isElidedNode(node)) {
|
|
187
|
+
lines.push(serializeElidedLine(node));
|
|
188
|
+
} else {
|
|
189
|
+
lines.push(buildOutline(node as OutlineNode));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
nodeCount,
|
|
195
|
+
outline: lines.join('\n'),
|
|
196
|
+
};
|
|
197
|
+
}
|