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,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read-rem service — 从 SDK 组装完整 RemObject
|
|
3
|
+
*
|
|
4
|
+
* 按 RemObject 接口的字段声明顺序组装对象(确定性序列化)。
|
|
5
|
+
* RichText 元素内部按 key 字母序排列。
|
|
6
|
+
*
|
|
7
|
+
* 同态命名:read_rem (action) → read-rem.ts (文件) → readRem (函数)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
|
|
11
|
+
import type {
|
|
12
|
+
RemObject,
|
|
13
|
+
RichText,
|
|
14
|
+
RemTypeValue,
|
|
15
|
+
PortalType,
|
|
16
|
+
PropertyTypeValue,
|
|
17
|
+
} from '../types';
|
|
18
|
+
import { filterNoisyChildren, filterNoisyTags } from './powerup-filter';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 读取单个 Rem,组装为完整 RemObject。
|
|
22
|
+
*
|
|
23
|
+
* @throws Error — Rem 不存在时抛 "Rem not found"
|
|
24
|
+
*/
|
|
25
|
+
export async function readRem(
|
|
26
|
+
plugin: ReactRNPlugin,
|
|
27
|
+
payload: { remId: string; includePowerup?: boolean },
|
|
28
|
+
): Promise<RemObject & { powerupFiltered?: { tags: number; children: number } }> {
|
|
29
|
+
const { includePowerup = false } = payload;
|
|
30
|
+
const rem = await plugin.rem.findOne(payload.remId);
|
|
31
|
+
if (!rem) {
|
|
32
|
+
throw new Error(`Rem not found: ${payload.remId}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 并行获取所有异步字段
|
|
36
|
+
const [
|
|
37
|
+
isDocument,
|
|
38
|
+
fontSize,
|
|
39
|
+
highlightColor,
|
|
40
|
+
isTodo,
|
|
41
|
+
todoStatus,
|
|
42
|
+
isCode,
|
|
43
|
+
isQuote,
|
|
44
|
+
isListItem,
|
|
45
|
+
isCardItem,
|
|
46
|
+
isTable,
|
|
47
|
+
isSlot,
|
|
48
|
+
isProperty,
|
|
49
|
+
enablePractice,
|
|
50
|
+
practiceDirection,
|
|
51
|
+
tagRems,
|
|
52
|
+
sourceRems,
|
|
53
|
+
aliasRems,
|
|
54
|
+
position,
|
|
55
|
+
// R-F boolean fields
|
|
56
|
+
isPowerup,
|
|
57
|
+
isPowerupEnum,
|
|
58
|
+
isPowerupProperty,
|
|
59
|
+
isPowerupPropertyListItem,
|
|
60
|
+
isPowerupSlot,
|
|
61
|
+
// Portal
|
|
62
|
+
portalType,
|
|
63
|
+
portalDirectlyIncludedRems,
|
|
64
|
+
// Property
|
|
65
|
+
propertyType,
|
|
66
|
+
// References
|
|
67
|
+
refsBeingReferenced,
|
|
68
|
+
deepRefsBeingReferenced,
|
|
69
|
+
refsReferencingThis,
|
|
70
|
+
// Tags hierarchy
|
|
71
|
+
taggedRems,
|
|
72
|
+
ancestorTagRems,
|
|
73
|
+
descendantTagRems,
|
|
74
|
+
// Hierarchy traversal
|
|
75
|
+
descendantRems,
|
|
76
|
+
siblingRems,
|
|
77
|
+
portalsAndDocsIn,
|
|
78
|
+
allRemInDocOrPortal,
|
|
79
|
+
allRemInFolderQ,
|
|
80
|
+
// Children (for powerup filtering)
|
|
81
|
+
childrenRems,
|
|
82
|
+
// Stats
|
|
83
|
+
timesSelected,
|
|
84
|
+
lastMovedTo,
|
|
85
|
+
schemaVer,
|
|
86
|
+
embeddedQueueView,
|
|
87
|
+
lastPracticed,
|
|
88
|
+
] = await Promise.all([
|
|
89
|
+
rem.isDocument(),
|
|
90
|
+
rem.getFontSize(),
|
|
91
|
+
rem.getHighlightColor(),
|
|
92
|
+
rem.isTodo(),
|
|
93
|
+
rem.getTodoStatus(),
|
|
94
|
+
rem.isCode(),
|
|
95
|
+
rem.isQuote(),
|
|
96
|
+
rem.isListItem(),
|
|
97
|
+
rem.isCardItem(),
|
|
98
|
+
rem.isTable(),
|
|
99
|
+
rem.isSlot(),
|
|
100
|
+
rem.isProperty(),
|
|
101
|
+
rem.getEnablePractice(),
|
|
102
|
+
rem.getPracticeDirection(),
|
|
103
|
+
rem.getTagRems(),
|
|
104
|
+
rem.getSources(),
|
|
105
|
+
rem.getAliases(),
|
|
106
|
+
rem.positionAmongstSiblings(),
|
|
107
|
+
// R-F boolean fields
|
|
108
|
+
rem.isPowerup(),
|
|
109
|
+
rem.isPowerupEnum(),
|
|
110
|
+
rem.isPowerupProperty(),
|
|
111
|
+
rem.isPowerupPropertyListItem(),
|
|
112
|
+
rem.isPowerupSlot(),
|
|
113
|
+
// Portal
|
|
114
|
+
rem.getPortalType(),
|
|
115
|
+
rem.getPortalDirectlyIncludedRem(),
|
|
116
|
+
// Property
|
|
117
|
+
rem.getPropertyType(),
|
|
118
|
+
// References
|
|
119
|
+
rem.remsBeingReferenced(),
|
|
120
|
+
rem.deepRemsBeingReferenced(),
|
|
121
|
+
rem.remsReferencingThis(),
|
|
122
|
+
// Tags hierarchy
|
|
123
|
+
rem.taggedRem(),
|
|
124
|
+
rem.ancestorTagRem(),
|
|
125
|
+
rem.descendantTagRem(),
|
|
126
|
+
// Hierarchy traversal
|
|
127
|
+
rem.getDescendants(),
|
|
128
|
+
rem.siblingRem(),
|
|
129
|
+
rem.portalsAndDocumentsIn(),
|
|
130
|
+
rem.allRemInDocumentOrPortal(),
|
|
131
|
+
rem.allRemInFolderQueue(),
|
|
132
|
+
// Children (for powerup filtering)
|
|
133
|
+
rem.getChildrenRem(),
|
|
134
|
+
// Stats
|
|
135
|
+
rem.timesSelectedInSearch(),
|
|
136
|
+
rem.getLastTimeMovedTo(),
|
|
137
|
+
rem.getSchemaVersion(),
|
|
138
|
+
rem.embeddedQueueViewMode(),
|
|
139
|
+
rem.getLastPracticed(),
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
// Powerup 噪音过滤
|
|
143
|
+
let filteredTagRems = tagRems;
|
|
144
|
+
let filteredChildrenIds = rem.children ?? [];
|
|
145
|
+
let filteredTagCount = 0;
|
|
146
|
+
let filteredChildCount = 0;
|
|
147
|
+
|
|
148
|
+
if (!includePowerup) {
|
|
149
|
+
filteredTagRems = await filterNoisyTags(tagRems);
|
|
150
|
+
filteredTagCount = tagRems.length - filteredTagRems.length;
|
|
151
|
+
|
|
152
|
+
const filteredChildren = await filterNoisyChildren(childrenRems);
|
|
153
|
+
filteredChildCount = childrenRems.length - filteredChildren.length;
|
|
154
|
+
filteredChildrenIds = filteredChildren.map(r => r._id);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 按 RemObject 接口字段声明顺序组装(确定性序列化)
|
|
158
|
+
const remObject: RemObject = {
|
|
159
|
+
// 核心标识
|
|
160
|
+
id: rem._id,
|
|
161
|
+
|
|
162
|
+
// 内容
|
|
163
|
+
text: sortRichTextKeys(rem.text ?? []),
|
|
164
|
+
backText: rem.backText ? sortRichTextKeys(rem.backText) : null,
|
|
165
|
+
|
|
166
|
+
// 类型系统
|
|
167
|
+
type: remTypeToString(rem.type as number),
|
|
168
|
+
isDocument,
|
|
169
|
+
|
|
170
|
+
// 结构
|
|
171
|
+
parent: rem.parent,
|
|
172
|
+
children: filteredChildrenIds,
|
|
173
|
+
|
|
174
|
+
// 格式 / 显示
|
|
175
|
+
fontSize: (fontSize as RemObject['fontSize']) ?? null,
|
|
176
|
+
highlightColor: (highlightColor as RemObject['highlightColor']) ?? null,
|
|
177
|
+
|
|
178
|
+
// 状态标记
|
|
179
|
+
isTodo,
|
|
180
|
+
todoStatus: (todoStatus as RemObject['todoStatus']) ?? null,
|
|
181
|
+
isCode,
|
|
182
|
+
isQuote,
|
|
183
|
+
isListItem,
|
|
184
|
+
isCardItem,
|
|
185
|
+
isTable: Boolean(isTable),
|
|
186
|
+
isSlot,
|
|
187
|
+
isProperty,
|
|
188
|
+
isPowerup,
|
|
189
|
+
isPowerupEnum,
|
|
190
|
+
isPowerupProperty,
|
|
191
|
+
isPowerupPropertyListItem,
|
|
192
|
+
isPowerupSlot,
|
|
193
|
+
|
|
194
|
+
// Portal 专用
|
|
195
|
+
portalType: remTypeToString(rem.type as number) === 'portal'
|
|
196
|
+
? portalTypeToString(portalType as number)
|
|
197
|
+
: null,
|
|
198
|
+
portalDirectlyIncludedRem: portalDirectlyIncludedRems.map(r => r._id),
|
|
199
|
+
|
|
200
|
+
// 属性类型
|
|
201
|
+
propertyType: (propertyType as PropertyTypeValue | undefined) ?? null,
|
|
202
|
+
|
|
203
|
+
// 练习设置
|
|
204
|
+
enablePractice,
|
|
205
|
+
practiceDirection: practiceDirection as RemObject['practiceDirection'],
|
|
206
|
+
|
|
207
|
+
// 关联 — 直接关系
|
|
208
|
+
tags: filteredTagRems.map(r => r._id),
|
|
209
|
+
sources: sourceRems.map(r => r._id),
|
|
210
|
+
aliases: aliasRems.map(r => r._id),
|
|
211
|
+
|
|
212
|
+
// 关联 — 引用关系
|
|
213
|
+
remsBeingReferenced: refsBeingReferenced.map(r => r._id),
|
|
214
|
+
deepRemsBeingReferenced: deepRefsBeingReferenced.map(r => r._id),
|
|
215
|
+
remsReferencingThis: refsReferencingThis.map(r => r._id),
|
|
216
|
+
|
|
217
|
+
// 关联 — 标签体系
|
|
218
|
+
taggedRem: taggedRems.map(r => r._id),
|
|
219
|
+
ancestorTagRem: ancestorTagRems.map(r => r._id),
|
|
220
|
+
descendantTagRem: descendantTagRems.map(r => r._id),
|
|
221
|
+
|
|
222
|
+
// 关联 — 层级遍历
|
|
223
|
+
descendants: descendantRems.map(r => r._id),
|
|
224
|
+
siblingRem: siblingRems.map(r => r._id),
|
|
225
|
+
portalsAndDocumentsIn: portalsAndDocsIn.map(r => r._id),
|
|
226
|
+
allRemInDocumentOrPortal: allRemInDocOrPortal.map(r => r._id),
|
|
227
|
+
allRemInFolderQueue: allRemInFolderQ.map(r => r._id),
|
|
228
|
+
|
|
229
|
+
// 位置 / 统计
|
|
230
|
+
positionAmongstSiblings: position ?? null,
|
|
231
|
+
timesSelectedInSearch: timesSelected,
|
|
232
|
+
lastTimeMovedTo: lastMovedTo,
|
|
233
|
+
schemaVersion: schemaVer,
|
|
234
|
+
|
|
235
|
+
// 队列视图
|
|
236
|
+
embeddedQueueViewMode: embeddedQueueView,
|
|
237
|
+
|
|
238
|
+
// 元数据 / 时间戳
|
|
239
|
+
createdAt: rem.createdAt,
|
|
240
|
+
updatedAt: rem.updatedAt,
|
|
241
|
+
localUpdatedAt: rem.localUpdatedAt,
|
|
242
|
+
lastPracticed,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
if (!includePowerup && (filteredTagCount > 0 || filteredChildCount > 0)) {
|
|
246
|
+
return { ...remObject, powerupFiltered: { tags: filteredTagCount, children: filteredChildCount } };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return remObject;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── 辅助函数 ──
|
|
253
|
+
|
|
254
|
+
/** 对 RichText 元素内部按 key 字母序排列(确定性序列化) */
|
|
255
|
+
function sortRichTextKeys(rt: RichText): RichText {
|
|
256
|
+
return rt.map(el => {
|
|
257
|
+
if (typeof el === 'string') return el;
|
|
258
|
+
const sorted: Record<string, unknown> = {};
|
|
259
|
+
for (const key of Object.keys(el).sort()) {
|
|
260
|
+
sorted[key] = el[key];
|
|
261
|
+
}
|
|
262
|
+
return sorted;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** SDK RemType 枚举值 → 字符串 */
|
|
267
|
+
function remTypeToString(type: number): RemTypeValue {
|
|
268
|
+
switch (type) {
|
|
269
|
+
case 1: return 'concept';
|
|
270
|
+
case 2: return 'descriptor';
|
|
271
|
+
case 6: return 'portal';
|
|
272
|
+
default: return 'default';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** SDK PORTAL_TYPE 枚举值 → 字符串 */
|
|
277
|
+
function portalTypeToString(pt: number): PortalType {
|
|
278
|
+
switch (pt) {
|
|
279
|
+
case 2: return 'embedded_queue';
|
|
280
|
+
case 3: return 'scaffold';
|
|
281
|
+
case 4: return 'search_portal';
|
|
282
|
+
default: return 'portal';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read-tree service — 递归遍历子树并序列化为 Markdown 大纲
|
|
3
|
+
*
|
|
4
|
+
* 同态命名:read_tree (action) → read-tree.ts (文件) → readTree (函数)
|
|
5
|
+
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* 1. 从根 Rem 递归遍历子树(受 depth 限制)
|
|
8
|
+
* 2. 对每个 Rem 调用 SDK API 获取序列化所需数据
|
|
9
|
+
* 3. 调用 utils/tree-serializer 纯函数拼接为大纲文本
|
|
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
|
+
buildOutline,
|
|
18
|
+
} from '../utils/tree-serializer';
|
|
19
|
+
import { filterNoisyChildren } from './powerup-filter';
|
|
20
|
+
import { sliceSiblings } from '../utils/elision';
|
|
21
|
+
import { buildFullSerializableRem, sanitizeNewlines } from './rem-builder';
|
|
22
|
+
|
|
23
|
+
export interface ReadTreePayload {
|
|
24
|
+
remId: string;
|
|
25
|
+
depth?: number;
|
|
26
|
+
maxNodes?: number;
|
|
27
|
+
maxSiblings?: number;
|
|
28
|
+
ancestorLevels?: number;
|
|
29
|
+
includePowerup?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 祖先节点信息(从直接父亲到最远祖先,由近及远) */
|
|
33
|
+
export interface AncestorInfo {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
childrenCount: number;
|
|
37
|
+
isDocument: boolean;
|
|
38
|
+
isTopLevel?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ReadTreeResult {
|
|
42
|
+
rootId: string;
|
|
43
|
+
depth: number;
|
|
44
|
+
nodeCount: number;
|
|
45
|
+
outline: string;
|
|
46
|
+
/** 祖先链(从直接父亲到最远祖先,由近及远) */
|
|
47
|
+
ancestors?: AncestorInfo[];
|
|
48
|
+
powerupFiltered?: { tags: number; children: number };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 读取 Rem 子树并序列化为 Markdown 大纲。
|
|
53
|
+
*
|
|
54
|
+
* @throws Error — Rem 不存在、节点数超限
|
|
55
|
+
*/
|
|
56
|
+
export async function readTree(
|
|
57
|
+
plugin: ReactRNPlugin,
|
|
58
|
+
payload: ReadTreePayload,
|
|
59
|
+
): Promise<ReadTreeResult> {
|
|
60
|
+
const {
|
|
61
|
+
remId,
|
|
62
|
+
depth = 3,
|
|
63
|
+
maxNodes = 200,
|
|
64
|
+
maxSiblings = 20,
|
|
65
|
+
ancestorLevels = 0,
|
|
66
|
+
includePowerup = false,
|
|
67
|
+
} = payload;
|
|
68
|
+
|
|
69
|
+
const rootRem = await plugin.rem.findOne(remId);
|
|
70
|
+
if (!rootRem) {
|
|
71
|
+
throw new Error(`Rem not found: ${remId}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let nodeCount = 0;
|
|
75
|
+
let totalFilteredTags = 0;
|
|
76
|
+
let totalFilteredChildren = 0;
|
|
77
|
+
const budget = { remaining: maxNodes };
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 递归构建 OutlineNode 树。
|
|
81
|
+
*
|
|
82
|
+
* @param rem SDK Rem 对象
|
|
83
|
+
* @param currentDepth 当前相对于根的深度(根 = 0)
|
|
84
|
+
* @param maxDepth 最大展开深度(-1 = 无限)
|
|
85
|
+
*/
|
|
86
|
+
async function buildNode(rem: Rem, currentDepth: number, maxDepth: number): Promise<OutlineNode> {
|
|
87
|
+
nodeCount++;
|
|
88
|
+
budget.remaining--;
|
|
89
|
+
|
|
90
|
+
const allChildren = await rem.getChildrenRem();
|
|
91
|
+
const children = includePowerup ? allChildren : await filterNoisyChildren(allChildren);
|
|
92
|
+
if (!includePowerup) totalFilteredChildren += allChildren.length - children.length;
|
|
93
|
+
const shouldFold = maxDepth !== -1 && currentDepth >= maxDepth;
|
|
94
|
+
const folded = shouldFold && children.length > 0;
|
|
95
|
+
|
|
96
|
+
const serializable = await buildFullSerializableRem(plugin, rem, children, {
|
|
97
|
+
includePowerup,
|
|
98
|
+
onFilteredTags: (count) => { totalFilteredTags += count; },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 折叠时无法确定 multiline,保守设为 false
|
|
102
|
+
if (folded) {
|
|
103
|
+
serializable.hasMultilineChildren = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 递归处理子节点(带省略逻辑)
|
|
107
|
+
const childNodes: TreeNode[] = [];
|
|
108
|
+
if (!folded) {
|
|
109
|
+
// Sibling 省略
|
|
110
|
+
const { visibleIndices, elided } = sliceSiblings(children.length, maxSiblings, rem._id);
|
|
111
|
+
|
|
112
|
+
if (visibleIndices) {
|
|
113
|
+
// 有省略:展示前 head + 省略占位 + 后 tail
|
|
114
|
+
const { head, tail } = visibleIndices;
|
|
115
|
+
|
|
116
|
+
// 前 head 个
|
|
117
|
+
for (let i = 0; i < head && budget.remaining > 0; i++) {
|
|
118
|
+
childNodes.push(await buildNode(children[i], currentDepth + 1, maxDepth));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 省略占位符
|
|
122
|
+
if (elided) {
|
|
123
|
+
// 判断 isExact:被省略的节点如果有子节点,则不精确
|
|
124
|
+
// 保守策略:只要 depth 未到底,就标记为非精确
|
|
125
|
+
const atMaxDepth = maxDepth !== -1 && currentDepth + 1 >= maxDepth;
|
|
126
|
+
childNodes.push({
|
|
127
|
+
type: 'elided',
|
|
128
|
+
count: elided.count,
|
|
129
|
+
isExact: atMaxDepth, // 到底了就是精确的(不会有更多后代)
|
|
130
|
+
parentId: elided.parentId,
|
|
131
|
+
rangeFrom: elided.rangeFrom,
|
|
132
|
+
rangeTo: elided.rangeTo,
|
|
133
|
+
totalSiblings: elided.totalSiblings,
|
|
134
|
+
} as ElidedNode);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 后 tail 个
|
|
138
|
+
const tailStart = children.length - tail;
|
|
139
|
+
for (let i = tailStart; i < children.length && budget.remaining > 0; i++) {
|
|
140
|
+
childNodes.push(await buildNode(children[i], currentDepth + 1, maxDepth));
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
// 无 sibling 省略:正常遍历,但需检查全局预算
|
|
144
|
+
for (let i = 0; i < children.length; i++) {
|
|
145
|
+
if (budget.remaining <= 0) {
|
|
146
|
+
// 全局预算耗尽:剩余 children 生成一个省略占位符
|
|
147
|
+
const remainCount = children.length - i;
|
|
148
|
+
childNodes.push({
|
|
149
|
+
type: 'elided',
|
|
150
|
+
count: remainCount,
|
|
151
|
+
isExact: false, // 可能还有后代
|
|
152
|
+
parentId: rem._id,
|
|
153
|
+
rangeFrom: i,
|
|
154
|
+
rangeTo: children.length - 1,
|
|
155
|
+
totalSiblings: children.length,
|
|
156
|
+
} as ElidedNode);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
childNodes.push(await buildNode(children[i], currentDepth + 1, maxDepth));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { rem: serializable, children: childNodes, folded };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const rootNode = await buildNode(rootRem, 0, depth);
|
|
168
|
+
|
|
169
|
+
// 检测根节点是否为顶级 Rem(无父节点)
|
|
170
|
+
const rootParent = await rootRem.getParentRem();
|
|
171
|
+
if (!rootParent) {
|
|
172
|
+
rootNode.rem.isTopLevel = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const outline = buildOutline(rootNode);
|
|
176
|
+
|
|
177
|
+
const result: ReadTreeResult = {
|
|
178
|
+
rootId: remId,
|
|
179
|
+
depth,
|
|
180
|
+
nodeCount,
|
|
181
|
+
outline,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// 祖先链构建
|
|
185
|
+
const clampedLevels = Math.min(Math.max(ancestorLevels, 0), 10);
|
|
186
|
+
if (clampedLevels > 0) {
|
|
187
|
+
const ancestors: AncestorInfo[] = [];
|
|
188
|
+
let current: Rem | undefined = rootRem;
|
|
189
|
+
for (let i = 0; i < clampedLevels; i++) {
|
|
190
|
+
const parent = await current.getParentRem();
|
|
191
|
+
if (!parent) break;
|
|
192
|
+
const [name, children, isDoc] = await Promise.all([
|
|
193
|
+
plugin.richText.toMarkdown(parent.text ?? []),
|
|
194
|
+
parent.getChildrenRem(),
|
|
195
|
+
parent.isDocument(),
|
|
196
|
+
]);
|
|
197
|
+
ancestors.push({
|
|
198
|
+
id: parent._id,
|
|
199
|
+
name: sanitizeNewlines(name),
|
|
200
|
+
childrenCount: children.length,
|
|
201
|
+
isDocument: isDoc,
|
|
202
|
+
});
|
|
203
|
+
current = parent;
|
|
204
|
+
}
|
|
205
|
+
if (ancestors.length > 0) {
|
|
206
|
+
// 检测最远祖先是否为顶级 Rem
|
|
207
|
+
const furthestAncestor = ancestors[ancestors.length - 1];
|
|
208
|
+
const furthestParent = await current!.getParentRem();
|
|
209
|
+
if (!furthestParent) {
|
|
210
|
+
furthestAncestor.isTopLevel = true;
|
|
211
|
+
}
|
|
212
|
+
result.ancestors = ancestors;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!includePowerup && (totalFilteredTags > 0 || totalFilteredChildren > 0)) {
|
|
217
|
+
result.powerupFiltered = { tags: totalFilteredTags, children: totalFilteredChildren };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rem-builder.ts — 共享的 SerializableRem 构建函数
|
|
3
|
+
*
|
|
4
|
+
* 从 read-tree.ts 和 read-context.ts 中提取的重复逻辑。
|
|
5
|
+
* 放在 services/ 目录(因为调用 SDK),不放 utils/。
|
|
6
|
+
*
|
|
7
|
+
* 依赖方向:services/rem-builder → utils/tree-serializer(单向)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
|
|
11
|
+
import type { SerializableRem } from '../utils/tree-serializer';
|
|
12
|
+
import { filterNoisyTags } from './powerup-filter';
|
|
13
|
+
|
|
14
|
+
export interface BuildFullRemOptions {
|
|
15
|
+
/** 是否保留 Powerup 系统 Tag(默认 false = 过滤掉) */
|
|
16
|
+
includePowerup?: boolean;
|
|
17
|
+
/** 过滤掉的 Tag 数量回调(用于 read-tree 统计) */
|
|
18
|
+
onFilteredTags?: (count: number) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 构建完整的 SerializableRem(并行获取所有 SDK 字段)。
|
|
23
|
+
*
|
|
24
|
+
* 被 read-tree 和 read-context 共享。
|
|
25
|
+
*/
|
|
26
|
+
export async function buildFullSerializableRem(
|
|
27
|
+
plugin: ReactRNPlugin,
|
|
28
|
+
rem: Rem,
|
|
29
|
+
children: Rem[],
|
|
30
|
+
options?: BuildFullRemOptions,
|
|
31
|
+
): Promise<SerializableRem> {
|
|
32
|
+
const includePowerup = options?.includePowerup ?? false;
|
|
33
|
+
|
|
34
|
+
const [
|
|
35
|
+
markdownText,
|
|
36
|
+
markdownBackText,
|
|
37
|
+
remType,
|
|
38
|
+
isCardItem,
|
|
39
|
+
isDocument,
|
|
40
|
+
tagRems,
|
|
41
|
+
practiceDirection,
|
|
42
|
+
fontSize,
|
|
43
|
+
isTodo,
|
|
44
|
+
todoStatus,
|
|
45
|
+
isCode,
|
|
46
|
+
hasDvPowerup,
|
|
47
|
+
portalIncludedRems,
|
|
48
|
+
] = await Promise.all([
|
|
49
|
+
plugin.richText.toMarkdown(rem.text ?? []),
|
|
50
|
+
rem.backText ? plugin.richText.toMarkdown(rem.backText) : Promise.resolve(null),
|
|
51
|
+
rem.getType(),
|
|
52
|
+
rem.isCardItem(),
|
|
53
|
+
rem.isDocument(),
|
|
54
|
+
rem.getTagRems().then(tags => {
|
|
55
|
+
if (includePowerup) return tags;
|
|
56
|
+
return filterNoisyTags(tags).then(filtered => {
|
|
57
|
+
const filteredCount = tags.length - filtered.length;
|
|
58
|
+
if (filteredCount > 0) options?.onFilteredTags?.(filteredCount);
|
|
59
|
+
return filtered;
|
|
60
|
+
});
|
|
61
|
+
}),
|
|
62
|
+
rem.getPracticeDirection(),
|
|
63
|
+
rem.getFontSize(),
|
|
64
|
+
rem.isTodo(),
|
|
65
|
+
rem.getTodoStatus(),
|
|
66
|
+
rem.isCode(),
|
|
67
|
+
rem.hasPowerup('dv'),
|
|
68
|
+
rem.type === 6 ? rem.getPortalDirectlyIncludedRem() : Promise.resolve([]),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
let hasMultilineChildren = false;
|
|
72
|
+
if (children.length > 0) {
|
|
73
|
+
const cardItemFlags = await Promise.all(children.map(c => c.isCardItem()));
|
|
74
|
+
hasMultilineChildren = cardItemFlags.some(Boolean);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const isDivider = hasDvPowerup && (rem.text ?? []).length === 0;
|
|
78
|
+
|
|
79
|
+
// 获取每个 tag 的 name(并行)
|
|
80
|
+
const tags = await Promise.all(
|
|
81
|
+
tagRems.map(async (t) => ({
|
|
82
|
+
id: t._id,
|
|
83
|
+
name: sanitizeNewlines(await plugin.richText.toMarkdown(t.text ?? [])),
|
|
84
|
+
})),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
id: rem._id,
|
|
89
|
+
markdownText: sanitizeNewlines(markdownText),
|
|
90
|
+
markdownBackText: markdownBackText !== null ? sanitizeNewlines(markdownBackText) : null,
|
|
91
|
+
type: remTypeToString(remType as number),
|
|
92
|
+
hasMultilineChildren,
|
|
93
|
+
practiceDirection: (practiceDirection as string) ?? 'none',
|
|
94
|
+
isCardItem,
|
|
95
|
+
isDocument,
|
|
96
|
+
isPortal: rem.type === 6,
|
|
97
|
+
portalRefs: portalIncludedRems.map((r: Rem) => r._id),
|
|
98
|
+
childrenCount: children.length,
|
|
99
|
+
tags,
|
|
100
|
+
fontSize: (fontSize as 'H1' | 'H2' | 'H3' | null) ?? null,
|
|
101
|
+
isTodo,
|
|
102
|
+
todoStatus: (todoStatus as 'Finished' | 'Unfinished' | null) ?? null,
|
|
103
|
+
isCode,
|
|
104
|
+
isDivider,
|
|
105
|
+
isTopLevel: rem.parent === null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 辅助函数 ──
|
|
110
|
+
|
|
111
|
+
/** toMarkdown 可能返回多行,替换为空格以保持"每 Rem 一行" */
|
|
112
|
+
export function sanitizeNewlines(text: string): string {
|
|
113
|
+
return text.replace(/\n/g, ' ');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** SDK RemType 枚举值 → 字符串 */
|
|
117
|
+
export function remTypeToString(type: number): string {
|
|
118
|
+
switch (type) {
|
|
119
|
+
case 1: return 'concept';
|
|
120
|
+
case 2: return 'descriptor';
|
|
121
|
+
case 6: return 'portal';
|
|
122
|
+
default: return 'default';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reorder-children service — 重排 children 顺序
|
|
3
|
+
*
|
|
4
|
+
* 内部原子操作:reorder_children (action) → reorder-children.ts (文件) → reorderChildren (函数)
|
|
5
|
+
*
|
|
6
|
+
* SDK 没有 setChildrenOrder API,使用逐个 setParent(parent, position) 实现。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ReactRNPlugin } from '@remnote/plugin-sdk';
|
|
10
|
+
|
|
11
|
+
export interface ReorderChildrenPayload {
|
|
12
|
+
parentId: string;
|
|
13
|
+
/** 期望的 children 顺序(remId 数组) */
|
|
14
|
+
order: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 重排父节点下 children 的顺序。
|
|
19
|
+
*
|
|
20
|
+
* 策略:按目标顺序逐个调用 setParent(parent, position) 进行定位。
|
|
21
|
+
* 只移动位置需要变化的 children。
|
|
22
|
+
*
|
|
23
|
+
* @throws Error — 父节点不存在
|
|
24
|
+
*/
|
|
25
|
+
export async function reorderChildren(
|
|
26
|
+
plugin: ReactRNPlugin,
|
|
27
|
+
payload: ReorderChildrenPayload,
|
|
28
|
+
): Promise<{ ok: true }> {
|
|
29
|
+
const { parentId, order } = payload;
|
|
30
|
+
|
|
31
|
+
const parent = await plugin.rem.findOne(parentId);
|
|
32
|
+
if (!parent) {
|
|
33
|
+
throw new Error(`Parent Rem not found: ${parentId}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 获取当前 children 顺序
|
|
37
|
+
const currentChildren = await parent.getChildrenRem();
|
|
38
|
+
const currentOrder = currentChildren.map(r => r._id);
|
|
39
|
+
|
|
40
|
+
// 检测实际需要移动的项
|
|
41
|
+
// 按目标顺序逐个放置
|
|
42
|
+
for (let targetPos = 0; targetPos < order.length; targetPos++) {
|
|
43
|
+
const remId = order[targetPos];
|
|
44
|
+
|
|
45
|
+
// 重新获取最新顺序(因为每次 setParent 会改变顺序)
|
|
46
|
+
const freshChildren = await parent.getChildrenRem();
|
|
47
|
+
const currentPos = freshChildren.findIndex(r => r._id === remId);
|
|
48
|
+
|
|
49
|
+
if (currentPos === -1) {
|
|
50
|
+
// 这个 remId 不在当前 children 中(可能已被移动或删除),跳过
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (currentPos !== targetPos) {
|
|
55
|
+
const rem = freshChildren[currentPos];
|
|
56
|
+
await rem.setParent(parent, targetPos);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|