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.
- package/README.md +30 -6
- package/dist/cli/commands/connect.js +31 -2
- package/dist/cli/commands/health.js +111 -1
- package/dist/cli/commands/setup.js +112 -0
- package/dist/cli/daemon/daemon.js +101 -20
- package/dist/cli/daemon/headless-browser.js +291 -0
- package/dist/cli/daemon/static-server.js +84 -0
- package/dist/cli/handlers/edit-handler.js +89 -3
- package/dist/cli/handlers/read-handler.js +16 -0
- package/dist/cli/handlers/tree-edit-handler.js +59 -28
- package/dist/cli/handlers/tree-parser.js +110 -3
- package/dist/cli/main.js +22 -6
- package/dist/cli/server/ws-server.js +62 -1
- package/dist/mcp/daemon-client.js +4 -1
- package/dist/mcp/instructions.js +97 -12
- package/dist/mcp/resources/edit-rem-guide.js +53 -0
- package/dist/mcp/resources/edit-tree-guide.js +60 -0
- package/dist/mcp/resources/error-reference.js +8 -1
- package/dist/mcp/resources/outline-format.js +29 -1
- package/dist/mcp/resources/rem-object-fields.js +6 -4
- package/dist/mcp/resources/separator-flashcard.js +5 -5
- package/dist/mcp/tools/infra-tools.js +39 -9
- package/package.json +5 -1
- package/remnote-plugin/dist/bridge-icon.svg +8 -0
- package/remnote-plugin/dist/bridge_widget-sandbox.js +65 -0
- package/remnote-plugin/dist/bridge_widget.js +65 -0
- package/remnote-plugin/dist/index-sandbox.css +591 -0
- package/remnote-plugin/dist/index-sandbox.js +64 -0
- package/remnote-plugin/dist/index.css +591 -0
- package/remnote-plugin/dist/index.html +9 -0
- package/remnote-plugin/dist/index.js +64 -0
- package/remnote-plugin/dist/manifest.json +22 -0
- package/remnote-plugin/src/bridge/message-router.ts +11 -0
- package/remnote-plugin/src/services/add-to-portal.ts +40 -0
- package/remnote-plugin/src/services/create-portal.ts +47 -0
- package/remnote-plugin/src/services/remove-from-portal.ts +40 -0
- package/remnote-plugin/src/services/write-rem-fields.ts +39 -0
- package/remnote-plugin/src/types.ts +7 -4
- package/skills/remnote-bridge/SKILL.md +90 -8
- package/skills/remnote-bridge/instructions/connect.md +48 -10
- package/skills/remnote-bridge/instructions/disconnect.md +1 -1
- package/skills/remnote-bridge/instructions/edit-rem.md +67 -4
- package/skills/remnote-bridge/instructions/edit-tree.md +100 -10
- package/skills/remnote-bridge/instructions/health.md +67 -1
- package/skills/remnote-bridge/instructions/overall.md +19 -4
- package/skills/remnote-bridge/instructions/read-rem.md +5 -2
- 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]
|
|
25
|
-
* - [
|
|
26
|
-
* - [R
|
|
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
|
-
/** [
|
|
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 和
|
|
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 输出的
|
|
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
|
-
###
|
|
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
|
-
|
|
|
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
|
-
##
|
|
21
|
+
## 两种模式
|
|
22
22
|
|
|
23
|
-
|
|
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 输出的
|
|
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
|
-
|
|
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": "守护进程启动超时(
|
|
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
|
|
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
|
-
├─
|
|
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 | 启动失败(超时、端口冲突、
|
|
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 |
|
|
207
|
+
| devServerPort | 8080 | Plugin 服务端口 |
|
|
170
208
|
| configPort | 3003 | ConfigServer 端口 |
|
|
171
209
|
| daemonTimeoutMinutes | 30 | 无活动自动关闭的分钟数 |
|
|
172
210
|
|