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.
Files changed (135) hide show
  1. package/dist/cli/commands/connect.d.ts +12 -0
  2. package/dist/cli/commands/connect.js +124 -0
  3. package/dist/cli/commands/disconnect.d.ts +11 -0
  4. package/dist/cli/commands/disconnect.js +100 -0
  5. package/dist/cli/commands/edit-rem.d.ts +13 -0
  6. package/dist/cli/commands/edit-rem.js +83 -0
  7. package/dist/cli/commands/edit-tree.d.ts +14 -0
  8. package/dist/cli/commands/edit-tree.js +67 -0
  9. package/dist/cli/commands/health.d.ts +12 -0
  10. package/dist/cli/commands/health.js +100 -0
  11. package/dist/cli/commands/install-skill.d.ts +6 -0
  12. package/dist/cli/commands/install-skill.js +39 -0
  13. package/dist/cli/commands/read-context.d.ts +20 -0
  14. package/dist/cli/commands/read-context.js +77 -0
  15. package/dist/cli/commands/read-globe.d.ts +16 -0
  16. package/dist/cli/commands/read-globe.js +60 -0
  17. package/dist/cli/commands/read-rem.d.ts +16 -0
  18. package/dist/cli/commands/read-rem.js +80 -0
  19. package/dist/cli/commands/read-tree.d.ts +17 -0
  20. package/dist/cli/commands/read-tree.js +85 -0
  21. package/dist/cli/commands/search.d.ts +12 -0
  22. package/dist/cli/commands/search.js +65 -0
  23. package/dist/cli/config.d.ts +55 -0
  24. package/dist/cli/config.js +139 -0
  25. package/dist/cli/daemon/daemon.d.ts +11 -0
  26. package/dist/cli/daemon/daemon.js +186 -0
  27. package/dist/cli/daemon/dev-server.d.ts +26 -0
  28. package/dist/cli/daemon/dev-server.js +81 -0
  29. package/dist/cli/daemon/pid.d.ts +34 -0
  30. package/dist/cli/daemon/pid.js +67 -0
  31. package/dist/cli/daemon/send-request.d.ts +24 -0
  32. package/dist/cli/daemon/send-request.js +92 -0
  33. package/dist/cli/handlers/context-read-handler.d.ts +18 -0
  34. package/dist/cli/handlers/context-read-handler.js +24 -0
  35. package/dist/cli/handlers/edit-handler.d.ts +30 -0
  36. package/dist/cli/handlers/edit-handler.js +133 -0
  37. package/dist/cli/handlers/globe-read-handler.d.ts +17 -0
  38. package/dist/cli/handlers/globe-read-handler.js +23 -0
  39. package/dist/cli/handlers/read-handler.d.ts +16 -0
  40. package/dist/cli/handlers/read-handler.js +78 -0
  41. package/dist/cli/handlers/rem-cache.d.ts +19 -0
  42. package/dist/cli/handlers/rem-cache.js +63 -0
  43. package/dist/cli/handlers/tree-edit-handler.d.ts +30 -0
  44. package/dist/cli/handlers/tree-edit-handler.js +188 -0
  45. package/dist/cli/handlers/tree-parser.d.ts +95 -0
  46. package/dist/cli/handlers/tree-parser.js +506 -0
  47. package/dist/cli/handlers/tree-read-handler.d.ts +28 -0
  48. package/dist/cli/handlers/tree-read-handler.js +53 -0
  49. package/dist/cli/main.d.ts +7 -0
  50. package/dist/cli/main.js +300 -0
  51. package/dist/cli/protocol.d.ts +39 -0
  52. package/dist/cli/protocol.js +35 -0
  53. package/dist/cli/server/config-server.d.ts +26 -0
  54. package/dist/cli/server/config-server.js +363 -0
  55. package/dist/cli/server/ws-server.d.ts +68 -0
  56. package/dist/cli/server/ws-server.js +335 -0
  57. package/dist/cli/utils/output.d.ts +11 -0
  58. package/dist/cli/utils/output.js +13 -0
  59. package/dist/mcp/daemon-client.d.ts +31 -0
  60. package/dist/mcp/daemon-client.js +99 -0
  61. package/dist/mcp/index.d.ts +7 -0
  62. package/dist/mcp/index.js +68 -0
  63. package/dist/mcp/instructions.d.ts +1 -0
  64. package/dist/mcp/instructions.js +249 -0
  65. package/dist/mcp/resources/edit-tree-guide.d.ts +1 -0
  66. package/dist/mcp/resources/edit-tree-guide.js +197 -0
  67. package/dist/mcp/resources/error-reference.d.ts +1 -0
  68. package/dist/mcp/resources/error-reference.js +132 -0
  69. package/dist/mcp/resources/outline-format.d.ts +1 -0
  70. package/dist/mcp/resources/outline-format.js +104 -0
  71. package/dist/mcp/resources/rem-object-fields.d.ts +1 -0
  72. package/dist/mcp/resources/rem-object-fields.js +331 -0
  73. package/dist/mcp/resources/separator-flashcard.d.ts +1 -0
  74. package/dist/mcp/resources/separator-flashcard.js +120 -0
  75. package/dist/mcp/tools/edit-tools.d.ts +5 -0
  76. package/dist/mcp/tools/edit-tools.js +47 -0
  77. package/dist/mcp/tools/infra-tools.d.ts +5 -0
  78. package/dist/mcp/tools/infra-tools.js +43 -0
  79. package/dist/mcp/tools/read-tools.d.ts +5 -0
  80. package/dist/mcp/tools/read-tools.js +195 -0
  81. package/dist/mcp/types.d.ts +12 -0
  82. package/dist/mcp/types.js +4 -0
  83. package/docs/instruction/connect.md +158 -0
  84. package/docs/instruction/disconnect.md +146 -0
  85. package/docs/instruction/edit-rem.md +509 -0
  86. package/docs/instruction/edit-tree.md +419 -0
  87. package/docs/instruction/health.md +159 -0
  88. package/docs/instruction/overall.md +751 -0
  89. package/docs/instruction/read-context.md +353 -0
  90. package/docs/instruction/read-globe.md +206 -0
  91. package/docs/instruction/read-rem.md +476 -0
  92. package/docs/instruction/read-tree.md +428 -0
  93. package/docs/instruction/search.md +196 -0
  94. package/package.json +41 -0
  95. package/remnote-plugin/package.json +48 -0
  96. package/remnote-plugin/postcss.config.js +5 -0
  97. package/remnote-plugin/public/bridge-icon.svg +8 -0
  98. package/remnote-plugin/public/manifest.json +22 -0
  99. package/remnote-plugin/src/bridge/message-router.ts +57 -0
  100. package/remnote-plugin/src/bridge/websocket-client.ts +245 -0
  101. package/remnote-plugin/src/index.css +1 -0
  102. package/remnote-plugin/src/services/breadcrumb.ts +26 -0
  103. package/remnote-plugin/src/services/create-rem.ts +59 -0
  104. package/remnote-plugin/src/services/delete-rem.ts +29 -0
  105. package/remnote-plugin/src/services/index.ts +16 -0
  106. package/remnote-plugin/src/services/move-rem.ts +39 -0
  107. package/remnote-plugin/src/services/powerup-filter.ts +31 -0
  108. package/remnote-plugin/src/services/read-context.ts +368 -0
  109. package/remnote-plugin/src/services/read-globe.ts +197 -0
  110. package/remnote-plugin/src/services/read-rem.ts +284 -0
  111. package/remnote-plugin/src/services/read-tree.ts +222 -0
  112. package/remnote-plugin/src/services/rem-builder.ts +124 -0
  113. package/remnote-plugin/src/services/reorder-children.ts +61 -0
  114. package/remnote-plugin/src/services/search.ts +56 -0
  115. package/remnote-plugin/src/services/write-rem-fields.ts +254 -0
  116. package/remnote-plugin/src/settings.ts +12 -0
  117. package/remnote-plugin/src/style.css +45 -0
  118. package/remnote-plugin/src/test-scripts/AGENTS.md +46 -0
  119. package/remnote-plugin/src/test-scripts/test-actions.ts +230 -0
  120. package/remnote-plugin/src/test-scripts/test-powerup-rendering.ts +722 -0
  121. package/remnote-plugin/src/test-scripts/test-rem-type-mapping.ts +283 -0
  122. package/remnote-plugin/src/test-scripts/test-richtext-builder.ts +207 -0
  123. package/remnote-plugin/src/test-scripts/test-richtext-matrix.ts +332 -0
  124. package/remnote-plugin/src/test-scripts/test-richtext-remaining.ts +245 -0
  125. package/remnote-plugin/src/test-scripts/test-rw-fields.ts +399 -0
  126. package/remnote-plugin/src/types.ts +419 -0
  127. package/remnote-plugin/src/utils/elision.ts +45 -0
  128. package/remnote-plugin/src/utils/index.ts +10 -0
  129. package/remnote-plugin/src/utils/tree-serializer.ts +269 -0
  130. package/remnote-plugin/src/widgets/bridge_widget.tsx +170 -0
  131. package/remnote-plugin/src/widgets/index.tsx +82 -0
  132. package/remnote-plugin/tailwind.config.js +7 -0
  133. package/remnote-plugin/tsconfig.json +21 -0
  134. package/remnote-plugin/webpack.config.js +125 -0
  135. package/skill/SKILL.md +428 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * search service — 知识库文本搜索
3
+ *
4
+ * 同态命名:search (action) → search.ts (文件) → search (函数)
5
+ *
6
+ * 职责:调用 plugin.search.search() 搜索 Rem,返回结果列表
7
+ */
8
+
9
+ import type { ReactRNPlugin } from '@remnote/plugin-sdk';
10
+
11
+ export interface SearchPayload {
12
+ query: string;
13
+ numResults?: number;
14
+ }
15
+
16
+ export interface SearchResultItem {
17
+ remId: string;
18
+ text: string;
19
+ isDocument: boolean;
20
+ }
21
+
22
+ export interface SearchResult {
23
+ query: string;
24
+ results: SearchResultItem[];
25
+ totalFound: number;
26
+ }
27
+
28
+ export async function search(
29
+ plugin: ReactRNPlugin,
30
+ payload: SearchPayload,
31
+ ): Promise<SearchResult> {
32
+ const { query, numResults = 20 } = payload;
33
+
34
+ if (!query || query.trim() === '') {
35
+ throw new Error('search query 不能为空');
36
+ }
37
+
38
+ const rems = await plugin.search.search([query], undefined, { numResults });
39
+
40
+ const results: SearchResultItem[] = [];
41
+ for (const rem of rems) {
42
+ const markdownText = await plugin.richText.toMarkdown(rem.text ?? []);
43
+ const isDocument = await rem.isDocument();
44
+ results.push({
45
+ remId: rem._id,
46
+ text: markdownText.replace(/\n/g, ' '),
47
+ isDocument,
48
+ });
49
+ }
50
+
51
+ return {
52
+ query,
53
+ results,
54
+ totalFound: results.length,
55
+ };
56
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * write-rem-fields service — 按字段写入 SDK
3
+ *
4
+ * 内部 action(daemon 编排用),无对应 CLI 命令。
5
+ * 接收 { remId, changes } payload,按顺序逐字段调用 SDK setter。
6
+ * 首个失败即终止,返回已成功和失败的字段信息。
7
+ *
8
+ * 同态命名例外:write_rem_fields (action) → write-rem-fields.ts (文件) → writeRemFields (函数)
9
+ * 原因:edit-rem CLI 命令的编排逻辑在 daemon 层,Plugin 只负责原子写入。
10
+ */
11
+
12
+ import type { ReactRNPlugin, PluginRem as Rem, RichTextInterface } from '@remnote/plugin-sdk';
13
+
14
+ export interface WriteRemFieldsPayload {
15
+ remId: string;
16
+ changes: Record<string, unknown>;
17
+ }
18
+
19
+ export interface WriteRemFieldsResult {
20
+ applied: string[];
21
+ failed?: { field: string; error: string };
22
+ }
23
+
24
+ /**
25
+ * 逐字段写入 SDK。
26
+ *
27
+ * @throws Error — Rem 不存在时抛 "Rem not found"
28
+ */
29
+ export async function writeRemFields(
30
+ plugin: ReactRNPlugin,
31
+ payload: WriteRemFieldsPayload,
32
+ ): Promise<WriteRemFieldsResult> {
33
+ const rem = await plugin.rem.findOne(payload.remId);
34
+ if (!rem) {
35
+ throw new Error(`Rem not found: ${payload.remId}`);
36
+ }
37
+
38
+ const applied: string[] = [];
39
+ const changes = payload.changes;
40
+
41
+ // parent 和 positionAmongstSiblings 联动处理
42
+ // 如果两者都在 changes 中,合并为一次 setParent 调用
43
+ const hasParent = 'parent' in changes;
44
+ const hasPosition = 'positionAmongstSiblings' in changes;
45
+
46
+ for (const field of Object.keys(changes)) {
47
+ // 跳过 positionAmongstSiblings(已与 parent 合并处理)
48
+ if (field === 'positionAmongstSiblings') {
49
+ continue;
50
+ }
51
+
52
+ try {
53
+ await applyField(rem, plugin, field, changes[field], {
54
+ // 当 parent 变更时,附带 position
55
+ position: field === 'parent' && hasPosition
56
+ ? changes.positionAmongstSiblings as number | undefined
57
+ : undefined,
58
+ });
59
+ applied.push(field);
60
+ // 如果 parent 变更时附带了 position,也标记 position 已 applied
61
+ if (field === 'parent' && hasPosition) {
62
+ applied.push('positionAmongstSiblings');
63
+ }
64
+ } catch (err) {
65
+ return {
66
+ applied,
67
+ failed: {
68
+ field,
69
+ error: err instanceof Error ? err.message : String(err),
70
+ },
71
+ };
72
+ }
73
+ }
74
+
75
+ // 如果只有 positionAmongstSiblings 没有 parent,单独处理
76
+ if (hasPosition && !hasParent) {
77
+ try {
78
+ const parent = rem.parent;
79
+ if (parent) {
80
+ await rem.setParent(parent, changes.positionAmongstSiblings as number);
81
+ }
82
+ applied.push('positionAmongstSiblings');
83
+ } catch (err) {
84
+ return {
85
+ applied,
86
+ failed: {
87
+ field: 'positionAmongstSiblings',
88
+ error: err instanceof Error ? err.message : String(err),
89
+ },
90
+ };
91
+ }
92
+ }
93
+
94
+ return { applied };
95
+ }
96
+
97
+ // ── 字段 → SDK setter 映射 ──
98
+
99
+ async function applyField(
100
+ rem: Rem,
101
+ plugin: ReactRNPlugin,
102
+ field: string,
103
+ value: unknown,
104
+ opts: { position?: number | undefined },
105
+ ): Promise<void> {
106
+ // 值来自 JSON 反序列化,类型为 unknown,需要显式转型到 SDK 期望的字面量/接口类型
107
+ switch (field) {
108
+ // 内容
109
+ case 'text':
110
+ await rem.setText(value as RichTextInterface);
111
+ break;
112
+ case 'backText':
113
+ if (value === null) {
114
+ await rem.setBackText([]);
115
+ } else if (typeof value === 'string') {
116
+ // 从 edit-tree 箭头分隔符解析出的 backText 是纯文本字符串,
117
+ // 需要包装为 RichText 数组才能被 SDK 接受
118
+ await rem.setBackText([value]);
119
+ } else {
120
+ await rem.setBackText(value as RichTextInterface);
121
+ }
122
+ break;
123
+
124
+ // 类型系统
125
+ case 'type':
126
+ await rem.setType(remTypeStringToEnum(value as string) as any);
127
+ break;
128
+ case 'isDocument':
129
+ await rem.setIsDocument(value as boolean);
130
+ break;
131
+
132
+ // 结构
133
+ case 'parent':
134
+ await rem.setParent(value as string, opts.position);
135
+ break;
136
+
137
+ // 格式 / 显示
138
+ case 'fontSize':
139
+ await rem.setFontSize(value === null ? undefined : value as 'H1' | 'H2' | 'H3');
140
+ break;
141
+ case 'highlightColor':
142
+ if (value === null) {
143
+ // setHighlightColor(null) 被 SDK 拒绝,通过 removePowerup('h') 从底层移除高亮 Tag
144
+ await rem.removePowerup('h');
145
+ } else {
146
+ await rem.setHighlightColor(value as 'Red' | 'Orange' | 'Yellow' | 'Green' | 'Blue' | 'Purple');
147
+ }
148
+ break;
149
+
150
+ // 状态标记
151
+ case 'isTodo':
152
+ await rem.setIsTodo(value as boolean);
153
+ break;
154
+ case 'todoStatus':
155
+ if (value !== null) {
156
+ await rem.setTodoStatus(value as 'Finished' | 'Unfinished');
157
+ }
158
+ // null → 跳过:todoStatus=null 的语义是"非 todo",应通过 isTodo=false 实现
159
+ break;
160
+ case 'isCode':
161
+ await rem.setIsCode(value as boolean);
162
+ break;
163
+ case 'isQuote':
164
+ await rem.setIsQuote(value as boolean);
165
+ break;
166
+ case 'isListItem':
167
+ await rem.setIsListItem(value as boolean);
168
+ break;
169
+ case 'isCardItem':
170
+ await rem.setIsCardItem(value as boolean);
171
+ break;
172
+ case 'isSlot':
173
+ await rem.setIsSlot(value as boolean);
174
+ break;
175
+ case 'isProperty':
176
+ await rem.setIsProperty(value as boolean);
177
+ break;
178
+
179
+ // 练习设置
180
+ case 'enablePractice':
181
+ await rem.setEnablePractice(value as boolean);
182
+ break;
183
+ case 'practiceDirection':
184
+ await rem.setPracticeDirection(value as 'forward' | 'backward' | 'both' | 'none');
185
+ break;
186
+
187
+ // 关联 — tags(diff based)
188
+ case 'tags':
189
+ await applyTagsDiff(rem, value as string[]);
190
+ break;
191
+
192
+ // 关联 — sources(diff based)
193
+ case 'sources':
194
+ await applySourcesDiff(rem, value as string[]);
195
+ break;
196
+
197
+ // Powerup 操作
198
+ case 'addPowerup':
199
+ await rem.addPowerup(value as string);
200
+ break;
201
+
202
+ default:
203
+ throw new Error(`不可写入的字段: ${field}`);
204
+ }
205
+ }
206
+
207
+ // ── 辅助函数 ──
208
+
209
+ /** tags diff: 对比当前和目标,计算 add/remove */
210
+ async function applyTagsDiff(rem: Rem, targetIds: string[]): Promise<void> {
211
+ const currentTags = await rem.getTagRems();
212
+ const currentIds = new Set(currentTags.map((r: Rem) => r._id));
213
+ const targetSet = new Set(targetIds);
214
+
215
+ for (const id of targetIds) {
216
+ if (!currentIds.has(id)) {
217
+ await rem.addTag(id as string);
218
+ }
219
+ }
220
+ for (const id of currentIds) {
221
+ if (!targetSet.has(id as string)) {
222
+ await rem.removeTag(id as string);
223
+ }
224
+ }
225
+ }
226
+
227
+ /** sources diff: 对比当前和目标,计算 add/remove */
228
+ async function applySourcesDiff(rem: Rem, targetIds: string[]): Promise<void> {
229
+ const currentSources = await rem.getSources();
230
+ const currentIds = new Set(currentSources.map((r: Rem) => r._id));
231
+ const targetSet = new Set(targetIds);
232
+
233
+ for (const id of targetIds) {
234
+ if (!currentIds.has(id)) {
235
+ await rem.addSource(id as string);
236
+ }
237
+ }
238
+ for (const id of currentIds) {
239
+ if (!targetSet.has(id as string)) {
240
+ await rem.removeSource(id as string);
241
+ }
242
+ }
243
+ }
244
+
245
+ /** 字符串类型值 → SDK SetRemType 枚举数值。Portal 不可通过 setType() 设置。 */
246
+ function remTypeStringToEnum(type: string): 1 | 2 | 'DEFAULT_TYPE' {
247
+ switch (type) {
248
+ case 'concept': return 1;
249
+ case 'descriptor': return 2;
250
+ case 'default': return 'DEFAULT_TYPE';
251
+ case 'portal': throw new Error('Portal 不可通过 setType() 设置,只能通过 createPortal() 创建');
252
+ default: throw new Error(`未知的 Rem 类型: ${type}`);
253
+ }
254
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Plugin 设置常量
3
+ *
4
+ * 定义 RemNote 设置面板中的设置项 ID 和默认值。
5
+ */
6
+
7
+ // 设置项 ID
8
+ export const SETTING_WS_URL = 'bridge-ws-url';
9
+
10
+ // 默认值
11
+ export const DEFAULT_WS_URL = 'ws://127.0.0.1:3002';
12
+ export const DEFAULT_PLUGIN_VERSION = '0.1.0';
@@ -0,0 +1,45 @@
1
+ /*
2
+ * This file contains the tailwind directives and equivalent styles
3
+ * to replicate the behavior inside the Shadow DOM of native plugins.
4
+ */
5
+
6
+ @tailwind base;
7
+ @tailwind components;
8
+ @tailwind utilities;
9
+
10
+ /*
11
+ 1. Use a consistent sensible line-height in all browsers.
12
+ 2. Prevent adjustments of font size after orientation changes in iOS.
13
+ 3. Use a more readable tab size.
14
+ */
15
+
16
+ html,
17
+ :host-context(div) {
18
+ line-height: 1.5; /* 1 */
19
+ -webkit-text-size-adjust: 100%; /* 2 */
20
+ -moz-tab-size: 4; /* 3 */
21
+ tab-size: 4; /* 3 */
22
+ }
23
+
24
+ /*
25
+ 1. Remove the margin in all browsers.
26
+ 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
27
+ */
28
+
29
+ body,
30
+ :host {
31
+ margin: 0; /* 1 */
32
+ line-height: inherit; /* 2 */
33
+ }
34
+
35
+ /**
36
+ */
37
+
38
+ body,
39
+ :host {
40
+ margin: 0;
41
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
42
+ 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
43
+ -webkit-font-smoothing: antialiased;
44
+ -moz-osx-font-smoothing: grayscale;
45
+ }
@@ -0,0 +1,46 @@
1
+ # test-scripts — RemNote Plugin SDK 探测脚本
2
+
3
+ ## 这是什么
4
+
5
+ 一次性探测脚本,用于在 RemNote Plugin 运行时环境中验证 SDK 行为。
6
+ 脚本运行后将结果写入 RemNote 知识库("mcp 测试" 文档下),供人工或 CLI 读取。
7
+
8
+ ## 为什么在 src/ 下
9
+
10
+ 这些脚本**必须**在 RemNote Plugin 沙箱内执行(依赖 `plugin: ReactRNPlugin` 对象调用 SDK API),
11
+ 所以必须被 webpack 打包。但它们**不是生产代码**——数据收集完毕后,应从 `widgets/index.tsx` 中注释掉 import 和调用,避免打包进生产 bundle。
12
+
13
+ ## 当前状态
14
+
15
+ 所有脚本已完成数据收集,`widgets/index.tsx` 中的 import 和调用**已注释掉**。
16
+
17
+ ## 运行机制
18
+
19
+ 1. 脚本通过 `widgets/index.tsx` 的 `onActivate()` 调用
20
+ 2. 每个脚本用 `plugin.storage.getSession(key)` 做防重跑检查(同一会话只跑一次)
21
+ 3. 执行流程:找到 "mcp 测试" 文档 → 创建隔离 wrapper Rem → 逐项测试 → 结果写入子 Rem
22
+ 4. 刷新 RemNote 页面即触发运行(session storage 随页面刷新重置)
23
+
24
+ ## 如何重跑
25
+
26
+ 1. 在 `widgets/index.tsx` 中取消对应 import 和调用的注释
27
+ 2. 启动 dev server:`cd remnote-plugin && npm run dev`
28
+ 3. 刷新 RemNote 页面
29
+
30
+ ## 脚本清单
31
+
32
+ | 文件 | 探测内容 | 产出 |
33
+ |:--|:--|:--|
34
+ | `test-actions.ts` | Rem CRUD 操作(create/remove/merge/collapse 等) | 各操作成功/失败记录 |
35
+ | `test-rw-fields.ts` | Rem 字段读写(highlightColor、backText 等) | 字段读写兼容性 |
36
+ | `test-richtext-builder.ts` | RichText Builder API 输出结构(video/audio) | Builder 产出的 JSON 对照 |
37
+ | `test-richtext-remaining.ts` | RichText 剩余未测字段(cId、qId、block、title、i:"g" 等) | 各字段写入+回读结果 |
38
+ | `test-richtext-matrix.ts` | 7 种元素类型 × 12 种格式化字段交叉兼容性 | 84 格矩阵(72✅ 12❌ 0⚠️) |
39
+
40
+ ## 编写新脚本的约定
41
+
42
+ - 导出一个 `async function run...(plugin: ReactRNPlugin): Promise<void>`
43
+ - 用 `plugin.storage.getSession/setSession` 做防重跑
44
+ - 创建 wrapper Rem 隔离测试数据,避免污染其他测试
45
+ - 每个测试项单独 try/catch,不要因一项失败中断全部
46
+ - 最终汇总写入一个 JSON Rem,方便 CLI `read-rem` 机器读取
@@ -0,0 +1,230 @@
1
+ /**
2
+ * 纯动作方法 (W) 行为观察测试
3
+ *
4
+ * 测试流程:创建初态 → 暂停(截图初态)→ 执行动作 → 暂停(截图终态)
5
+ * 测试的动作方法:remove / indent / outdent / merge / collapse / expand / addToPortal
6
+ */
7
+ import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
8
+
9
+ async function delay(ms: number) {
10
+ return new Promise((r) => setTimeout(r, ms));
11
+ }
12
+
13
+ export async function runActionTests(plugin: ReactRNPlugin): Promise<void> {
14
+ const alreadyRan = await plugin.storage.getSession('action-test-v1-ran');
15
+ if (alreadyRan) {
16
+ console.log('[ACTION-TEST] 已运行过,跳过');
17
+ return;
18
+ }
19
+ await plugin.storage.setSession('action-test-v1-ran', true);
20
+
21
+ console.log('[ACTION-TEST] ========== 开始纯动作行为观察 ==========');
22
+
23
+ try {
24
+ // ── 找到 "mcp 测试" 文档 ──
25
+ const allRem = await plugin.rem.getAll();
26
+ let parentDoc: Rem | undefined;
27
+ for (const r of allRem) {
28
+ const t = r.text ? await plugin.richText.toString(r.text) : '';
29
+ if (t.trim() === 'mcp 测试') { parentDoc = r; break; }
30
+ }
31
+ if (!parentDoc) { console.error('[ACTION-TEST] 找不到 "mcp 测试"'); return; }
32
+
33
+ // ── 状态指示 Rem ──
34
+ const statusRem = await plugin.rem.createRem();
35
+ if (!statusRem) return;
36
+ await statusRem.setParent(parentDoc._id, 0);
37
+ await statusRem.setHighlightColor('Yellow');
38
+ await statusRem.setText(['⏳ 动作测试:正在创建初态...']);
39
+ await delay(300);
40
+
41
+ let pos = 1;
42
+
43
+ // ═══════════════════════════════════════════════════
44
+ // 创建所有初态 Rem
45
+ // ═══════════════════════════════════════════════════
46
+
47
+ // --- 1. remove() 测试 ---
48
+ const removeLabel = await plugin.rem.createRem();
49
+ if (!removeLabel) return;
50
+ await removeLabel.setParent(parentDoc._id, pos++);
51
+ await removeLabel.setText(['── 1. remove() 测试 ──']);
52
+ await delay(200);
53
+
54
+ const removeTarget = await plugin.rem.createRem();
55
+ if (!removeTarget) return;
56
+ await removeTarget.setParent(parentDoc._id, pos++);
57
+ await removeTarget.setHighlightColor('Red');
58
+ await removeTarget.setText(['❌ 这个 Rem 将被 remove() 删除']);
59
+ await delay(200);
60
+
61
+ // --- 2. indent() 测试 ---
62
+ const indentLabel = await plugin.rem.createRem();
63
+ if (!indentLabel) return;
64
+ await indentLabel.setParent(parentDoc._id, pos++);
65
+ await indentLabel.setText(['── 2. indent() 测试 ──']);
66
+ await delay(200);
67
+
68
+ const indentParent = await plugin.rem.createRem();
69
+ if (!indentParent) return;
70
+ await indentParent.setParent(parentDoc._id, pos++);
71
+ await indentParent.setText(['indent 的上方兄弟(将变成父级)']);
72
+ await delay(200);
73
+
74
+ const indentTarget = await plugin.rem.createRem();
75
+ if (!indentTarget) return;
76
+ await indentTarget.setParent(parentDoc._id, pos++);
77
+ await indentTarget.setHighlightColor('Orange');
78
+ await indentTarget.setText(['→ 这个 Rem 将被 indent()(向右缩进)']);
79
+ await delay(200);
80
+
81
+ // --- 3. outdent() 测试 ---
82
+ const outdentLabel = await plugin.rem.createRem();
83
+ if (!outdentLabel) return;
84
+ await outdentLabel.setParent(parentDoc._id, pos++);
85
+ await outdentLabel.setText(['── 3. outdent() 测试 ──']);
86
+ await delay(200);
87
+
88
+ const outdentWrapper = await plugin.rem.createRem();
89
+ if (!outdentWrapper) return;
90
+ await outdentWrapper.setParent(parentDoc._id, pos++);
91
+ await outdentWrapper.setText(['outdent 的当前父级']);
92
+ await delay(200);
93
+
94
+ const outdentTarget = await plugin.rem.createRem();
95
+ if (!outdentTarget) return;
96
+ await outdentTarget.setParent(outdentWrapper._id, 0); // 作为子级
97
+ await outdentTarget.setHighlightColor('Orange');
98
+ await outdentTarget.setText(['← 这个子 Rem 将被 outdent()(向左提升)']);
99
+ await delay(200);
100
+
101
+ // --- 4. merge() 测试 ---
102
+ const mergeLabel = await plugin.rem.createRem();
103
+ if (!mergeLabel) return;
104
+ await mergeLabel.setParent(parentDoc._id, pos++);
105
+ await mergeLabel.setText(['── 4. merge() 测试 ──']);
106
+ await delay(200);
107
+
108
+ const mergeKeep = await plugin.rem.createRem();
109
+ if (!mergeKeep) return;
110
+ await mergeKeep.setParent(parentDoc._id, pos++);
111
+ await mergeKeep.setHighlightColor('Green');
112
+ await mergeKeep.setText(['保留的 Rem(merge 目标)']);
113
+ await delay(200);
114
+
115
+ const mergeDisappear = await plugin.rem.createRem();
116
+ if (!mergeDisappear) return;
117
+ await mergeDisappear.setParent(parentDoc._id, pos++);
118
+ await mergeDisappear.setHighlightColor('Red');
119
+ await mergeDisappear.setText(['将被合并消失的 Rem']);
120
+ await delay(200);
121
+
122
+ // --- 5. collapse() / expand() 测试 ---
123
+ const collapseLabel = await plugin.rem.createRem();
124
+ if (!collapseLabel) return;
125
+ await collapseLabel.setParent(parentDoc._id, pos++);
126
+ await collapseLabel.setText(['── 5. collapse()/expand() 测试 ──']);
127
+ await delay(200);
128
+
129
+ const collapseParent = await plugin.rem.createRem();
130
+ if (!collapseParent) return;
131
+ await collapseParent.setParent(parentDoc._id, pos++);
132
+ await collapseParent.setHighlightColor('Blue');
133
+ await collapseParent.setText(['将被 collapse() 的父 Rem']);
134
+ await delay(200);
135
+
136
+ // 给它创建 3 个子级
137
+ for (let i = 1; i <= 3; i++) {
138
+ const child = await plugin.rem.createRem();
139
+ if (child) {
140
+ await child.setParent(collapseParent._id, i - 1);
141
+ await child.setText([`子级 ${i}(collapse 后应隐藏)`]);
142
+ await delay(150);
143
+ }
144
+ }
145
+
146
+ // --- 6. addToPortal() 测试 ---
147
+ const portalLabel = await plugin.rem.createRem();
148
+ if (!portalLabel) return;
149
+ await portalLabel.setParent(parentDoc._id, pos++);
150
+ await portalLabel.setText(['── 6. addToPortal() 测试 ──']);
151
+ await delay(200);
152
+
153
+ const portalTarget = await plugin.rem.createRem();
154
+ if (!portalTarget) return;
155
+ await portalTarget.setParent(parentDoc._id, pos++);
156
+ await portalTarget.setHighlightColor('Purple');
157
+ await portalTarget.setText(['将被添加到 Portal 的 Rem']);
158
+ await delay(200);
159
+
160
+ // 创建一个 Portal
161
+ const portal = await plugin.rem.createPortal();
162
+ if (portal) {
163
+ await portal.setParent(parentDoc._id, pos++);
164
+ await delay(200);
165
+ }
166
+
167
+ // ═══════════════════════════════════════════════════
168
+ // 初态就绪,暂停 15 秒供截图
169
+ // ═══════════════════════════════════════════════════
170
+ await statusRem.setText(['📸 初态就绪!请截图(15 秒后执行动作)']);
171
+ console.log('[ACTION-TEST] ===== 初态就绪,等待 15 秒截图 =====');
172
+ await delay(15000);
173
+
174
+ // ═══════════════════════════════════════════════════
175
+ // 逐个执行动作
176
+ // ═══════════════════════════════════════════════════
177
+ await statusRem.setText(['⚡ 正在执行动作...']);
178
+
179
+ // 1. remove()
180
+ console.log('[ACTION-TEST] 执行 remove()');
181
+ await removeTarget.remove();
182
+ await delay(500);
183
+
184
+ // 2. indent()
185
+ console.log('[ACTION-TEST] 执行 indent()');
186
+ await indentTarget.indent();
187
+ await delay(500);
188
+
189
+ // 3. outdent()
190
+ console.log('[ACTION-TEST] 执行 outdent()');
191
+ await outdentTarget.outdent();
192
+ await delay(500);
193
+
194
+ // 4. merge() — 把 mergeDisappear 合并到 mergeKeep
195
+ console.log('[ACTION-TEST] 执行 merge()');
196
+ await mergeKeep.merge(mergeDisappear._id);
197
+ await delay(500);
198
+
199
+ // 5. collapse()
200
+ console.log('[ACTION-TEST] 执行 collapse()');
201
+ await collapseParent.collapse();
202
+ await delay(500);
203
+
204
+ // 6. addToPortal()
205
+ if (portal) {
206
+ console.log('[ACTION-TEST] 执行 addToPortal()');
207
+ await portalTarget.addToPortal(portal);
208
+ await delay(500);
209
+ }
210
+
211
+ // ═══════════════════════════════════════════════════
212
+ // 动作执行完毕,暂停供截图
213
+ // ═══════════════════════════════════════════════════
214
+ await statusRem.setText(['📸 全部动作已执行!请截图观察终态']);
215
+ await statusRem.setHighlightColor('Green');
216
+ console.log('[ACTION-TEST] ===== 全部动作已执行,请截图终态 =====');
217
+
218
+ // 20 秒后执行 expand() 让 collapse 的效果能对比
219
+ await delay(20000);
220
+ console.log('[ACTION-TEST] 执行 expand()(恢复折叠)');
221
+ const refetchCollapse = await plugin.rem.findOne(collapseParent._id);
222
+ if (refetchCollapse) {
223
+ await refetchCollapse.expand();
224
+ }
225
+ await statusRem.setText(['📸 expand() 已执行,子级应重新可见']);
226
+
227
+ } catch (err) {
228
+ console.error('[ACTION-TEST] 出错:', err);
229
+ }
230
+ }