remnote-bridge 0.1.6 → 0.1.8

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 (47) hide show
  1. package/README.md +30 -6
  2. package/dist/cli/commands/connect.js +31 -2
  3. package/dist/cli/commands/health.js +111 -1
  4. package/dist/cli/commands/setup.js +112 -0
  5. package/dist/cli/daemon/daemon.js +101 -20
  6. package/dist/cli/daemon/headless-browser.js +291 -0
  7. package/dist/cli/daemon/static-server.js +84 -0
  8. package/dist/cli/handlers/edit-handler.js +89 -3
  9. package/dist/cli/handlers/read-handler.js +16 -0
  10. package/dist/cli/handlers/tree-edit-handler.js +59 -28
  11. package/dist/cli/handlers/tree-parser.js +110 -3
  12. package/dist/cli/main.js +22 -6
  13. package/dist/cli/server/ws-server.js +62 -1
  14. package/dist/mcp/daemon-client.js +4 -1
  15. package/dist/mcp/instructions.js +97 -12
  16. package/dist/mcp/resources/edit-rem-guide.js +53 -0
  17. package/dist/mcp/resources/edit-tree-guide.js +60 -0
  18. package/dist/mcp/resources/error-reference.js +8 -1
  19. package/dist/mcp/resources/outline-format.js +29 -1
  20. package/dist/mcp/resources/rem-object-fields.js +6 -4
  21. package/dist/mcp/resources/separator-flashcard.js +5 -5
  22. package/dist/mcp/tools/infra-tools.js +39 -9
  23. package/package.json +5 -1
  24. package/remnote-plugin/dist/bridge-icon.svg +8 -0
  25. package/remnote-plugin/dist/bridge_widget-sandbox.js +65 -0
  26. package/remnote-plugin/dist/bridge_widget.js +65 -0
  27. package/remnote-plugin/dist/index-sandbox.css +591 -0
  28. package/remnote-plugin/dist/index-sandbox.js +64 -0
  29. package/remnote-plugin/dist/index.css +591 -0
  30. package/remnote-plugin/dist/index.html +9 -0
  31. package/remnote-plugin/dist/index.js +64 -0
  32. package/remnote-plugin/dist/manifest.json +22 -0
  33. package/remnote-plugin/src/bridge/message-router.ts +11 -0
  34. package/remnote-plugin/src/services/add-to-portal.ts +40 -0
  35. package/remnote-plugin/src/services/create-portal.ts +47 -0
  36. package/remnote-plugin/src/services/remove-from-portal.ts +40 -0
  37. package/remnote-plugin/src/services/write-rem-fields.ts +39 -0
  38. package/remnote-plugin/src/types.ts +7 -4
  39. package/skills/remnote-bridge/SKILL.md +90 -8
  40. package/skills/remnote-bridge/instructions/connect.md +48 -10
  41. package/skills/remnote-bridge/instructions/disconnect.md +1 -1
  42. package/skills/remnote-bridge/instructions/edit-rem.md +67 -4
  43. package/skills/remnote-bridge/instructions/edit-tree.md +100 -10
  44. package/skills/remnote-bridge/instructions/health.md +67 -1
  45. package/skills/remnote-bridge/instructions/overall.md +19 -4
  46. package/skills/remnote-bridge/instructions/read-rem.md +5 -2
  47. package/skills/remnote-bridge/instructions/setup.md +130 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "manifestVersion": 1,
3
+ "id": "unofficial_remnote_bridge",
4
+ "name": "Unofficial RemNote Bridge",
5
+ "author": "baobao700508",
6
+ "repoUrl": "https://github.com/baobao700508/unofficial-remnote-bridge-cli",
7
+ "version": {
8
+ "major": 0,
9
+ "minor": 1,
10
+ "patch": 0
11
+ },
12
+ "theme": [],
13
+ "enableOnMobile": false,
14
+ "description": "Unofficial RemNote Bridge 桥接插件:通过 WebSocket 将 RemNote API 暴露给外部 CLI/MCP 调用",
15
+ "requestNative": false,
16
+ "requiredScopes": [
17
+ {
18
+ "type": "All",
19
+ "level": "ReadCreateModifyDelete"
20
+ }
21
+ ]
22
+ }
@@ -19,6 +19,9 @@ import { deleteRem } from '../services/delete-rem';
19
19
  import { moveRem } from '../services/move-rem';
20
20
  import { reorderChildren } from '../services/reorder-children';
21
21
  import { search } from '../services/search';
22
+ import { createPortal } from '../services/create-portal';
23
+ import { addToPortal } from '../services/add-to-portal';
24
+ import { removeFromPortal } from '../services/remove-from-portal';
22
25
 
23
26
  /**
24
27
  * 创建消息路由处理器
@@ -50,6 +53,14 @@ export function createMessageRouter(plugin: ReactRNPlugin): (request: BridgeRequ
50
53
  case 'search':
51
54
  return search(plugin, request.payload as { query: string; numResults?: number });
52
55
 
56
+ // Portal 操作
57
+ case 'create_portal':
58
+ return createPortal(plugin, request.payload as { parentId: string; position?: number });
59
+ case 'add_to_portal':
60
+ return addToPortal(plugin, request.payload as { portalId: string; remId: string });
61
+ case 'remove_from_portal':
62
+ return removeFromPortal(plugin, request.payload as { portalId: string; remId: string });
63
+
53
64
  default:
54
65
  throw new Error(`未实现的 action: ${request.action}`);
55
66
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * add-to-portal service — 向 Portal 添加引用
3
+ *
4
+ * 同态命名:add_to_portal (action) → add-to-portal.ts (文件) → addToPortal (函数)
5
+ *
6
+ * 注意调用方向:addToPortal() 是在被引用的 Rem 上调用,参数是 Portal Rem。
7
+ */
8
+
9
+ import type { ReactRNPlugin } from '@remnote/plugin-sdk';
10
+
11
+ export interface AddToPortalPayload {
12
+ /** Portal Rem ID */
13
+ portalId: string;
14
+ /** 要添加到 Portal 的 Rem ID */
15
+ remId: string;
16
+ }
17
+
18
+ /**
19
+ * 将指定 Rem 添加到 Portal 的引用列表。
20
+ *
21
+ * @throws Error — Portal 不存在、Rem 不存在
22
+ */
23
+ export async function addToPortal(
24
+ plugin: ReactRNPlugin,
25
+ payload: AddToPortalPayload,
26
+ ): Promise<void> {
27
+ const { portalId, remId } = payload;
28
+
29
+ const portal = await plugin.rem.findOne(portalId);
30
+ if (!portal) {
31
+ throw new Error(`Portal not found: ${portalId}`);
32
+ }
33
+
34
+ const rem = await plugin.rem.findOne(remId);
35
+ if (!rem) {
36
+ throw new Error(`Rem not found: ${remId}`);
37
+ }
38
+
39
+ await rem.addToPortal(portal);
40
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * create-portal service — 创建 Portal Rem
3
+ *
4
+ * 同态命名:create_portal (action) → create-portal.ts (文件) → createPortal (函数)
5
+ *
6
+ * Portal 只能通过 plugin.rem.createPortal() 创建,不能通过 setType() 将已有 Rem 转为 Portal。
7
+ */
8
+
9
+ import type { ReactRNPlugin } from '@remnote/plugin-sdk';
10
+
11
+ export interface CreatePortalPayload {
12
+ /** 父节点 Rem ID */
13
+ parentId: string;
14
+ /** 在兄弟中的位置(0-based),可选 */
15
+ position?: number;
16
+ }
17
+
18
+ export interface CreatePortalResult {
19
+ remId: string;
20
+ }
21
+
22
+ /**
23
+ * 创建空 Portal 并设置父节点。
24
+ *
25
+ * @throws Error — 创建失败、父节点不存在
26
+ */
27
+ export async function createPortal(
28
+ plugin: ReactRNPlugin,
29
+ payload: CreatePortalPayload,
30
+ ): Promise<CreatePortalResult> {
31
+ const { parentId, position } = payload;
32
+
33
+ // 创建空 Portal
34
+ const portal = await plugin.rem.createPortal();
35
+ if (!portal) {
36
+ throw new Error('Failed to create portal');
37
+ }
38
+
39
+ // 设置父节点和位置
40
+ const parent = await plugin.rem.findOne(parentId);
41
+ if (!parent) {
42
+ throw new Error(`Parent Rem not found: ${parentId}`);
43
+ }
44
+ await portal.setParent(parent, position);
45
+
46
+ return { remId: portal._id };
47
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * remove-from-portal service — 从 Portal 移除引用
3
+ *
4
+ * 同态命名:remove_from_portal (action) → remove-from-portal.ts (文件) → removeFromPortal (函数)
5
+ *
6
+ * 注意调用方向:removeFromPortal() 是在被引用的 Rem 上调用,参数是 Portal Rem。
7
+ */
8
+
9
+ import type { ReactRNPlugin } from '@remnote/plugin-sdk';
10
+
11
+ export interface RemoveFromPortalPayload {
12
+ /** Portal Rem ID */
13
+ portalId: string;
14
+ /** 要从 Portal 移除的 Rem ID */
15
+ remId: string;
16
+ }
17
+
18
+ /**
19
+ * 将指定 Rem 从 Portal 的引用列表中移除。
20
+ *
21
+ * @throws Error — Portal 不存在、Rem 不存在
22
+ */
23
+ export async function removeFromPortal(
24
+ plugin: ReactRNPlugin,
25
+ payload: RemoveFromPortalPayload,
26
+ ): Promise<void> {
27
+ const { portalId, remId } = payload;
28
+
29
+ const portal = await plugin.rem.findOne(portalId);
30
+ if (!portal) {
31
+ throw new Error(`Portal not found: ${portalId}`);
32
+ }
33
+
34
+ const rem = await plugin.rem.findOne(remId);
35
+ if (!rem) {
36
+ throw new Error(`Rem not found: ${remId}`);
37
+ }
38
+
39
+ await rem.removeFromPortal(portal);
40
+ }
@@ -194,6 +194,11 @@ async function applyField(
194
194
  await applySourcesDiff(rem, value as string[]);
195
195
  break;
196
196
 
197
+ // Portal 引用(diff based)
198
+ case 'portalDirectlyIncludedRem':
199
+ await applyPortalRefsDiff(rem, plugin, value as string[]);
200
+ break;
201
+
197
202
  // Powerup 操作
198
203
  case 'addPowerup':
199
204
  await rem.addPowerup(value as string);
@@ -242,6 +247,40 @@ async function applySourcesDiff(rem: Rem, targetIds: string[]): Promise<void> {
242
247
  }
243
248
  }
244
249
 
250
+ /**
251
+ * portalDirectlyIncludedRem diff: 对比当前和目标,通过 addToPortal/removeFromPortal 增删。
252
+ * 注意调用方向:addToPortal/removeFromPortal 是在被引用 Rem 上调用,参数是 Portal Rem。
253
+ */
254
+ async function applyPortalRefsDiff(portalRem: Rem, plugin: ReactRNPlugin, targetIds: string[]): Promise<void> {
255
+ // 防御:非 Portal Rem 不可修改此字段
256
+ // 使用 rem.type === 6(portal 类型的枚举值),而非 getPortalType()
257
+ // 因为 getPortalType() 可能对新创建的 Portal 返回 undefined
258
+ if ((portalRem as unknown as { type: number }).type !== 6) {
259
+ throw new Error('portalDirectlyIncludedRem can only be modified on Portal Rem');
260
+ }
261
+
262
+ const currentRefs = await portalRem.getPortalDirectlyIncludedRem();
263
+ const currentIds = new Set(currentRefs.map((r: Rem) => r._id));
264
+ const targetSet = new Set(targetIds);
265
+
266
+ // 添加缺少的
267
+ for (const id of targetIds) {
268
+ if (!currentIds.has(id)) {
269
+ const remToAdd = await plugin.rem.findOne(id);
270
+ if (!remToAdd) throw new Error(`Rem not found: ${id}`);
271
+ await remToAdd.addToPortal(portalRem);
272
+ }
273
+ }
274
+ // 删除多余的
275
+ for (const id of currentIds) {
276
+ if (!targetSet.has(id as string)) {
277
+ const remToRemove = await plugin.rem.findOne(id as string);
278
+ if (!remToRemove) throw new Error(`Rem not found: ${id}`);
279
+ await remToRemove.removeFromPortal(portalRem);
280
+ }
281
+ }
282
+ }
283
+
245
284
  /** 字符串类型值 → SDK SetRemType 枚举数值。Portal 不可通过 setType() 设置。 */
246
285
  function remTypeStringToEnum(type: string): 1 | 2 | 'DEFAULT_TYPE' {
247
286
  switch (type) {
@@ -21,13 +21,16 @@
21
21
  * 以下每个 [RW] 字段的注释标注了其底层是 Powerup 机制还是纯字段修改
22
22
  *
23
23
  * 读写标注:
24
- * - [RW] = 可读可写(SDK 有对应的 setter/add/remove 方法)
25
- * - [R] = 只读,默认输出(SDK 仅有 getter)
26
- * - [R-F] = 只读,仅 --full 模式输出(低频 / 细粒度 / 可由其他字段推导)
24
+ * - [RW] = 可读可写(SDK 有对应的 setter/add/remove 方法)
25
+ * - [Portal-W] = Portal 专用可写(仅 Portal Rem 可通过 addToPortal/removeFromPortal 修改)
26
+ * - [R] = 只读,默认输出(SDK 仅有 getter)
27
+ * - [R-F] = 只读,仅 --full 模式输出(低频 / 细粒度 / 可由其他字段推导)
27
28
  *
28
29
  * 输出模式(CLI --full 选项):
29
30
  * - 默认模式:输出 [RW] + [R] 字段(34 个),覆盖 Agent 常用场景
30
31
  * - --full 模式:额外输出 [R-F] 字段(+17 个),用于调试或深度分析
32
+ * - Portal 模式:type === 'portal' 时自动输出 9 个关键字段(id、type、portalType、
33
+ * portalDirectlyIncludedRem、parent、positionAmongstSiblings、children、createdAt、updatedAt)
31
34
  *
32
35
  * 实测标注(2026-03-03 在 RemNote UI 中逐字段截图观察):
33
36
  * - ✅ 已实测 = 在真实 RemNote 环境中创建独立 Rem 并截图观察视觉行为
@@ -254,7 +257,7 @@ export interface RemObject {
254
257
 
255
258
  /** [R] Portal 子类型。仅 type === 'portal' 时有值。SDK: getPortalType() */
256
259
  portalType: PortalType | null;
257
- /** [R] Portal 直接包含的 Rem ID 数组。SDK: getPortalDirectlyIncludedRem() */
260
+ /** [Portal-W] Portal 直接包含的 Rem ID 数组。SDK: getPortalDirectlyIncludedRem() / addToPortal() / removeFromPortal()。写入时使用 diff 机制。仅 type === 'portal' 时可修改 */
258
261
  portalDirectlyIncludedRem: string[];
259
262
 
260
263
  // ══════════════════════════════════════════════════════════
@@ -11,6 +11,7 @@ description: "RemNote 知识库操作指南。通过 remnote-bridge 命令行工
11
11
 
12
12
  | 命令 | 文档路径 |
13
13
  |:-----|:---------|
14
+ | setup | `instructions/setup.md` |
14
15
  | connect | `instructions/connect.md` |
15
16
  | disconnect | `instructions/disconnect.md` |
16
17
  | health | `instructions/health.md` |
@@ -42,7 +43,7 @@ RemNote 中所有内容的基本单元都是 **Rem**。文档、文件夹、闪
42
43
 
43
44
  两个独立维度:
44
45
 
45
- - **type**(闪卡语义):`concept`(加粗)、`descriptor`(正常字重)、`default`(普通)、`portal`(只读)
46
+ - **type**(闪卡语义):`concept`(加粗)、`descriptor`(正常字重)、`default`(普通)、`portal`(嵌入引用容器)
46
47
  - **isDocument**(页面语义):与 type 完全独立
47
48
 
48
49
  ### CDF 框架(Concept-Descriptor Framework)
@@ -88,7 +89,17 @@ RemNote 推荐的知识结构化方法:
88
89
  | **Tag** | `##` | Rem 的 tags 数组 | read-rem 的 `tags` 字段 |
89
90
  | **Portal** | `((` | 嵌入实时视图(**编辑同步**) | read-tree 标记 `type:portal refs:id1,id2` |
90
91
 
91
- Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的 Rem 不会被 read-tree 自动展开,需要对 refs 中的 ID 单独 read-tree。
92
+ Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的 Rem 不会被 read-tree 自动展开,需要对 refs 中的 ID 单独 read-tree。Portal 引用列表可通过 `edit-rem` 修改(str_replace 简化 JSON 中的 `portalDirectlyIncludedRem` 数组)。
93
+
94
+ #### Portal 操作速查
95
+
96
+ | 操作 | 命令 | 方式 |
97
+ |:-----|:-----|:-----|
98
+ | 创建 Portal | `edit-tree` | 新增行 `<!--portal refs:id1,id2-->` |
99
+ | 删除 Portal | `edit-tree` | 从大纲中移除 Portal 行(与删除普通行相同) |
100
+ | 修改引用列表(增删引用的 Rem) | `edit-rem` | str_replace 简化 JSON 中的 `portalDirectlyIncludedRem` 数组 |
101
+ | 移动 Portal(换父节点/位置) | `edit-tree` | 与移动普通行相同 |
102
+ | 读取 Portal | `read-rem` | 自动输出 9 字段简化 JSON |
92
103
 
93
104
  ### RichText 格式
94
105
 
@@ -236,15 +247,15 @@ Rem 的属性(文本、类型、格式、标签) → edit-rem (前置
236
247
 
237
248
  ## 3. 标准工作流
238
249
 
239
- ### ⚠️ connect 后需要用户配合(重要)
250
+ ### ⚠️ 标准模式:connect 后需要用户配合
240
251
 
241
- `connect` 成功只意味着 daemon 和 webpack-dev-server 已启动,**Plugin 并未自动连接**。用户必须在 RemNote 中完成操作,Plugin 才能连接到 daemon:
252
+ `connect` 成功只意味着 daemon 和 Plugin 服务已启动,**Plugin 并未自动连接**。用户必须在 RemNote 中完成操作,Plugin 才能连接到 daemon:
242
253
 
243
254
  **首次使用**(RemNote 从未加载过此插件):
244
255
  1. 打开 RemNote 桌面端或网页端
245
256
  2. 点击左侧边栏底部的插件图标(拼图形状)
246
257
  3. 点击「开发你的插件」(Develop Your Plugin)
247
- 4. 在输入框中填入 `http://localhost:8080`(即 connect 输出的 webpack-dev-server 地址)
258
+ 4. 在输入框中填入 `http://localhost:8080`(即 connect 输出的 Plugin 服务地址)
248
259
  5. 等待插件加载完成
249
260
 
250
261
  **非首次使用**(之前已加载过此插件):
@@ -252,7 +263,50 @@ Rem 的属性(文本、类型、格式、标签) → edit-rem (前置
252
263
 
253
264
  **你必须**:执行 `connect` 后,**立即告知用户需要完成上述操作**,不要直接调用业务命令。引导用户完成后,用 `health` 确认三层就绪再继续。
254
265
 
255
- ### 完整流程
266
+ ### Headless 模式:自动连接
267
+
268
+ 标准模式每次 connect 后都需要用户手动操作 RemNote。Headless 模式通过 setup(一次性)+ headless Chrome 实现自动连接,后续 connect 无需用户介入。
269
+
270
+ #### 首次使用(setup)
271
+
272
+ setup 会弹出 Chrome 窗口,用户需要完成两件事:
273
+ 1. **登录 RemNote**
274
+ 2. **配置 dev plugin**:插件图标 → 开发你的插件 → 填入 `http://localhost:8080`
275
+
276
+ 完成后**彻底退出 Chrome**(macOS 必须 Cmd+Q,仅关窗口不够)。
277
+
278
+ **Agent 交互方式**:
279
+ ```
280
+ 1. 调用 setup
281
+ 2. 立即告知用户:
282
+ "已打开 Chrome 浏览器。请完成以下操作:
283
+ 1. 登录 RemNote
284
+ 2. 在 RemNote 中配置开发插件:点击左下角插件图标 → 开发你的插件 → 输入 http://localhost:8080
285
+ 3. 完成后彻底退出 Chrome(macOS 请按 Cmd+Q)"
286
+ 3. 等待 setup 返回(阻塞,最长 10 分钟)
287
+ 4. 成功 → 进入下一步 connect --headless
288
+ ```
289
+
290
+ setup 只需执行一次。之后每次连接直接用 `connect --headless`。
291
+
292
+ #### 后续使用(connect --headless)
293
+
294
+ ```
295
+ 1. connect --headless -- 启动 daemon + headless Chrome 自动加载 RemNote 和 Plugin
296
+ 2. health -- 等待三层就绪(Plugin 需要 10-30 秒连接,可多次轮询)
297
+ 3. 业务操作
298
+ 4. disconnect -- 结束会话
299
+ ```
300
+
301
+ **无需任何用户操作**——headless Chrome 在后台自动完成登录和 Plugin 加载。
302
+
303
+ #### 排查
304
+
305
+ - `health --diagnose`:截图 + Chrome 状态 + console 错误(确认页面是否正常加载)
306
+ - `health --reload`:重载 headless Chrome 页面(Plugin 未连接时尝试)
307
+ - 如果 Plugin 始终不连接,可能是 RemNote 登录 session 过期,需重新 setup
308
+
309
+ ### 完整流程(标准模式)
256
310
 
257
311
  ```
258
312
  1. connect -- 启动会话(幂等,重复调用安全)
@@ -423,6 +477,30 @@ read-tree / read-globe / read-context 输出 Markdown 大纲,edit-tree 基于
423
477
  `代码块`
424
478
  ```
425
479
 
480
+ ### 新增行指定类型/属性(metadata-only 注释)
481
+
482
+ 新增行可在行尾添加 HTML 注释来指定 type、isDocument、tag,格式中不含 remId:
483
+
484
+ ```markdown
485
+ 新概念 <!--type:concept-->
486
+ 新文档页 <!--type:concept doc-->
487
+ 带标签的描述 <!--type:descriptor tag:数学(tag01)-->
488
+ 多标记组合 <!--type:concept doc tag:基础(tag02) tag:数学(tag01)-->
489
+ ```
490
+
491
+ 支持的标记:`type:concept`、`type:descriptor`、`doc`、`tag:Name(id)`(可多个,空格分隔)。
492
+
493
+ ### 新增 Portal
494
+
495
+ 在 newStr 中用特殊格式创建 Portal:
496
+
497
+ ```markdown
498
+ <!--portal refs:id1,id2--> 创建 Portal 并引用 id1、id2
499
+ <!--portal--> 创建空 Portal
500
+ ```
501
+
502
+ 注意:与已有 Portal 行(`<!--remId type:portal refs:id1,id2-->`)不同,新增 Portal 以 `<!--portal` 开头,无 remId。Portal 不能通过 `edit-rem` 设 `type: "portal"` 创建,只能用此格式或 SDK `createPortal()`。
503
+
426
504
  ### 嵌套新增(一次创建父+子结构)
427
505
 
428
506
  新增行下面可以再嵌套新增行,缩进表示父子关系:
@@ -474,14 +552,14 @@ oldStr: "\"text\": [\n \"Hello\"\n ]"
474
552
  newStr: "\"text\": [\n \"World\"\n ]"
475
553
  ```
476
554
 
477
- ### 20 个可编辑字段
555
+ ### 21 个可编辑字段
478
556
 
479
557
  ```
480
558
  text, backText, type, isDocument, parent,
481
559
  fontSize, highlightColor,
482
560
  isTodo, todoStatus, isCode, isQuote, isListItem, isCardItem, isSlot, isProperty,
483
561
  enablePractice, practiceDirection,
484
- tags, sources, positionAmongstSiblings
562
+ tags, sources, positionAmongstSiblings, portalDirectlyIncludedRem
485
563
  ```
486
564
 
487
565
  ### 特殊字段处理规则
@@ -495,6 +573,7 @@ tags, sources, positionAmongstSiblings
495
573
  | `type` | 不可设为 `portal`(只能通过 SDK `createPortal()` 创建) |
496
574
  | `parent` + `positionAmongstSiblings` | 共享同一 SDK 调用 `setParent(parentId, position)`,**应在同一次 str_replace 中同时修改** |
497
575
  | `tags` / `sources` | **Diff 机制**:对比当前 vs 目标数组,逐项 add/remove。必须列出完整目标数组,缺少的会被删除 |
576
+ | `portalDirectlyIncludedRem` | Portal 专用可写。**Diff 机制**:对比当前 vs 目标数组,逐项 addToPortal/removeFromPortal。仅 type=portal 时可修改。edit-rem 对 Portal 使用 9 字段简化 JSON |
498
577
 
499
578
  ### 常用只读字段(修改只产生警告,不生效)
500
579
 
@@ -508,6 +587,7 @@ aliases, descendants, siblingRem, isTable, portalType, propertyType
508
587
  | 模式 | 字段数 | 用法 |
509
588
  |:-----|:-------|:-----|
510
589
  | 默认 | 34(RW + R) | 常用场景 |
590
+ | Portal 简化 | 9(id, type, portalType, portalDirectlyIncludedRem, parent, positionAmongstSiblings, children, createdAt, updatedAt) | type=portal 时自动切换,`--full` 和 `--fields` 可覆盖 |
511
591
  | `--full` | 51(含低频 R-F) | 需要 Powerup 标识、时间戳等 |
512
592
  | `--fields` | 自选 + id | 精确获取特定字段 |
513
593
 
@@ -531,6 +611,8 @@ aliases, descendants, siblingRem, isTable, portalType, propertyType
531
611
  | folded_delete | 删了有隐藏子节点的行 | 用更大 depth 重新 read-tree |
532
612
  | elided_modified | 删/改了省略占位符 | 用更大 depth/maxSiblings 重新 read-tree |
533
613
  | indent_skip | 缩进跳级(如 0→4 空格) | 每级 2 空格,不可跳级 |
614
+ | children_captured | 新行劫持已有子节点 | 把新行插到兄弟末尾 |
615
+ | old_str not found in simplified Portal JSON | Portal 编辑时 oldStr 不匹配简化 JSON | 检查 Portal 简化 JSON 格式 |
534
616
  | Rem not found | remId 无效或已删除 | 用 search 重新定位 |
535
617
 
536
618
  ---
@@ -11,23 +11,52 @@
11
11
  | 服务 | 默认端口 | 用途 |
12
12
  |------|----------|------|
13
13
  | WS Server | 3002 | CLI 命令 ↔ daemon ↔ Plugin 的双向通信 |
14
- | webpack-dev-server | 8080 | 将 remnote-plugin 热加载到 RemNote 浏览器 |
14
+ | Plugin 服务 | 8080 | 将 remnote-plugin 加载到 RemNote 浏览器(默认静态服务器,`--dev` 时为 webpack-dev-server) |
15
15
  | ConfigServer | 3003 | HTTP 配置管理界面 |
16
16
 
17
17
  daemon 启动后脱离父进程(detached),CLI 进程退出但 daemon 继续运行。
18
18
 
19
19
  ---
20
20
 
21
- ## ⚠️ connect 后需要用户配合
21
+ ## 两种模式
22
22
 
23
- `connect` 成功只意味着 daemon 和 webpack-dev-server 已启动,**Plugin 并未自动连接**。用户必须在 RemNote 中完成以下操作:
23
+ ### 标准模式(默认)
24
+
25
+ 启动 daemon 后需要用户手动在 RemNote 中加载 Plugin。适用于用户已打开 RemNote 的场景。
26
+
27
+ ### Headless 模式(`--headless`)
28
+
29
+ 自动启动 headless Chrome 加载 Plugin,无需用户操作。适用于无 GUI 环境或全自动连接场景。
30
+
31
+ **前置条件**:必须先执行 `setup` 完成 RemNote 登录。
32
+
33
+ ```bash
34
+ # 首次:先 setup 登录
35
+ remnote-bridge setup
36
+
37
+ # 然后启动 headless 连接
38
+ remnote-bridge connect --headless
39
+
40
+ # JSON 模式
41
+ remnote-bridge --json connect --headless
42
+ ```
43
+
44
+ Headless 模式下 Plugin 可能需要 10-30 秒才能连接到 daemon,使用 `health` 确认就绪。
45
+
46
+ 排查工具:`health --diagnose`(截图+状态+console 错误)、`health --reload`(重载 Chrome 页面)。
47
+
48
+ ---
49
+
50
+ ## ⚠️ 标准模式:connect 后需要用户配合
51
+
52
+ `connect`(不传 `--headless`)成功只意味着 daemon 和 Plugin 服务已启动,**Plugin 并未自动连接**。用户必须在 RemNote 中完成以下操作:
24
53
 
25
54
  ### 首次使用(RemNote 从未加载过此插件)
26
55
 
27
56
  1. 打开 RemNote 桌面端或网页端
28
57
  2. 点击左侧边栏底部的插件图标(拼图形状)
29
58
  3. 点击「开发你的插件」(Develop Your Plugin)
30
- 4. 在输入框中填入 `http://localhost:8080`(即 connect 输出的 webpack-dev-server 地址)
59
+ 4. 在输入框中填入 `http://localhost:8080`(即 connect 输出的 Plugin 服务地址)
31
60
  5. 等待插件加载完成
32
61
 
33
62
  ### 非首次使用(之前已加载过此插件)
@@ -55,7 +84,7 @@ remnote-bridge connect
55
84
  ```
56
85
  守护进程已启动(PID: 12345)
57
86
  WS Server: ws://127.0.0.1:3002
58
- webpack-dev-server: http://localhost:8080
87
+ Plugin 服务: http://localhost:8080
59
88
  配置页面: http://127.0.0.1:3003
60
89
  超时: 30 分钟无 CLI 交互后自动关闭
61
90
  ```
@@ -106,7 +135,7 @@ remnote-bridge --json connect
106
135
  {
107
136
  "ok": false,
108
137
  "command": "connect",
109
- "error": "守护进程启动超时(10 秒)",
138
+ "error": "守护进程启动超时(60 秒)",
110
139
  "timestamp": "2026-03-06T10:00:00.000Z"
111
140
  }
112
141
  ```
@@ -125,19 +154,28 @@ remnote-bridge --json connect
125
154
  3. daemon 内部按顺序启动:
126
155
  ├─ WS Server(必须成功,否则 daemon 退出)
127
156
  ├─ ConfigServer(非关键,失败不阻塞)
128
- └─ webpack-dev-server(必须成功,否则 daemon 退出)
157
+ └─ Plugin 服务(默认静态服务器;--dev 时为 webpack-dev-server + 依赖自动安装 + 崩溃重试)
129
158
 
130
159
  4. daemon 写入 PID 文件
131
160
 
132
161
  5. daemon 通过 IPC 发送 ready 信号给父进程
133
162
 
134
163
  6. 父进程(CLI)收到 ready → 输出结果 → 退出
135
- ├─ 10 秒内未收到 → 超时失败
164
+ ├─ 60 秒内未收到 → 超时失败
136
165
  └─ 收到 error → 启动失败
137
166
  ```
138
167
 
139
168
  ---
140
169
 
170
+ ## Windows 注意事项
171
+
172
+ - **默认模式秒级启动**:使用预构建 plugin,无需安装依赖
173
+ - **`--dev` 模式首次较慢**:会自动安装 remnote-plugin 的依赖(约 600+ 个包),在 Windows 上可能需要 30-60 秒。connect 命令的超时为 60 秒
174
+ - **`--dev` 依赖自动修复**:如果 webpack-dev-server 因依赖损坏而崩溃,daemon 会自动执行清洁重装(删除 node_modules + package-lock.json 后重新安装)并重试,最多重试 2 次,无需手动干预
175
+ - **端口残留**:多次 connect 失败后可能出现端口被占用(`EADDRINUSE`),先执行 `remnote-bridge disconnect`,如仍有残留可通过 `netstat -ano | findstr 3002` 定位 PID 后 `taskkill /F /PID <pid>` 强制终止
176
+
177
+ ---
178
+
141
179
  ## 超时机制
142
180
 
143
181
  daemon 启动后开始计时,默认 **30 分钟无 CLI 交互**自动关闭(执行优雅 shutdown)。每次收到 CLI 请求时重置计时器。
@@ -157,7 +195,7 @@ daemon 启动后开始计时,默认 **30 分钟无 CLI 交互**自动关闭(
157
195
  | 退出码 | 含义 |
158
196
  |--------|------|
159
197
  | 0 | 启动成功,或已在运行 |
160
- | 1 | 启动失败(超时、端口冲突、webpack-dev-server 异常等) |
198
+ | 1 | 启动失败(超时、端口冲突、Plugin 服务异常等) |
161
199
 
162
200
  ---
163
201
 
@@ -166,7 +204,7 @@ daemon 启动后开始计时,默认 **30 分钟无 CLI 交互**自动关闭(
166
204
  | 配置项 | 默认值 | 说明 |
167
205
  |--------|--------|------|
168
206
  | wsPort | 3002 | WS Server 监听端口 |
169
- | devServerPort | 8080 | webpack-dev-server 端口 |
207
+ | devServerPort | 8080 | Plugin 服务端口 |
170
208
  | configPort | 3003 | ConfigServer 端口 |
171
209
  | daemonTimeoutMinutes | 30 | 无活动自动关闭的分钟数 |
172
210
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  1. 关闭 WS Server(断开所有连接)
12
12
  2. 关闭 ConfigServer
13
- 3. 停止 webpack-dev-server 子进程
13
+ 3. 停止 Plugin 服务(静态文件服务器 或 webpack-dev-server
14
14
  4. 删除 PID 文件
15
15
  5. 内存缓存随进程退出自动消失
16
16