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,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"name": "unofficial-remnote-bridge-plugin",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "RemNote 桥接层:嵌入 RemNote 的 WebSocket 桥接插件",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"check-types": "tsc",
|
|
9
|
+
"dev": "cross-env NODE_ENV=development webpack-dev-server --color --progress --no-open",
|
|
10
|
+
"build": "npx remnote-plugin validate && shx rm -rf dist && cross-env NODE_ENV=production webpack --color --progress && cd dist && bestzip ../PluginZip.zip ./*",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@remnote/plugin-sdk": "0.0.46",
|
|
16
|
+
"react": "^17.0.2",
|
|
17
|
+
"react-dom": "^17.0.2"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
|
21
|
+
"@types/node": "^20.11.0",
|
|
22
|
+
"@types/react": "^17.0.2",
|
|
23
|
+
"@types/react-dom": "^17.0.2",
|
|
24
|
+
"autoprefixer": "^10.4.20",
|
|
25
|
+
"bestzip": "^2.2.1",
|
|
26
|
+
"concurrently": "^9.1.0",
|
|
27
|
+
"copy-webpack-plugin": "^12.0.0",
|
|
28
|
+
"cross-env": "^7.0.3",
|
|
29
|
+
"css-loader": "^7.1.0",
|
|
30
|
+
"esbuild-loader": "^4.4.0",
|
|
31
|
+
"html-webpack-plugin": "^5.6.0",
|
|
32
|
+
"mini-css-extract-plugin": "^2.10.0",
|
|
33
|
+
"postcss": "^8.4.49",
|
|
34
|
+
"postcss-import": "^16.1.0",
|
|
35
|
+
"postcss-loader": "^8.2.0",
|
|
36
|
+
"react-refresh": "^0.14.2",
|
|
37
|
+
"shx": "^0.3.4",
|
|
38
|
+
"style-loader": "^4.0.0",
|
|
39
|
+
"tailwindcss": "^3.4.17",
|
|
40
|
+
"ts-node": "^10.9.2",
|
|
41
|
+
"typescript": "^5.4.0",
|
|
42
|
+
"vitest": "^1.3.0",
|
|
43
|
+
"happy-dom": "^14.0.0",
|
|
44
|
+
"webpack": "^5.97.0",
|
|
45
|
+
"webpack-cli": "^5.1.4",
|
|
46
|
+
"webpack-dev-server": "^5.2.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Bidirectional arrows -->
|
|
3
|
+
<path d="M3 8 L9 8 M7 6 L9 8 L7 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
4
|
+
<path d="M21 8 L15 8 M17 6 L15 8 L17 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
5
|
+
|
|
6
|
+
<!-- MCP text -->
|
|
7
|
+
<text x="12" y="19" text-anchor="middle" font-size="7" fill="currentColor" font-weight="700" font-family="system-ui, -apple-system, sans-serif">MCP</text>
|
|
8
|
+
</svg>
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Router — 请求路由分发
|
|
3
|
+
*
|
|
4
|
+
* bridge 层的 API 控制器:接收守护进程转发的请求,
|
|
5
|
+
* 根据 action 路由到 services 层对应方法。
|
|
6
|
+
*
|
|
7
|
+
* 依赖方向:bridge/message-router → services(单向)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ReactRNPlugin } from '@remnote/plugin-sdk';
|
|
11
|
+
import type { BridgeRequest } from './websocket-client';
|
|
12
|
+
import { readRem } from '../services/read-rem';
|
|
13
|
+
import { readTree } from '../services/read-tree';
|
|
14
|
+
import { readGlobe } from '../services/read-globe';
|
|
15
|
+
import { readContext } from '../services/read-context';
|
|
16
|
+
import { writeRemFields } from '../services/write-rem-fields';
|
|
17
|
+
import { createRem } from '../services/create-rem';
|
|
18
|
+
import { deleteRem } from '../services/delete-rem';
|
|
19
|
+
import { moveRem } from '../services/move-rem';
|
|
20
|
+
import { reorderChildren } from '../services/reorder-children';
|
|
21
|
+
import { search } from '../services/search';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 创建消息路由处理器
|
|
25
|
+
*
|
|
26
|
+
* 返回一个函数供 WebSocketClient.setMessageHandler() 使用。
|
|
27
|
+
* 按 action 分发到 services 层对应方法(同态命名)。
|
|
28
|
+
*/
|
|
29
|
+
export function createMessageRouter(plugin: ReactRNPlugin): (request: BridgeRequest) => Promise<unknown> {
|
|
30
|
+
return async (request: BridgeRequest): Promise<unknown> => {
|
|
31
|
+
switch (request.action) {
|
|
32
|
+
case 'read_rem':
|
|
33
|
+
return readRem(plugin, request.payload as { remId: string; includePowerup?: boolean });
|
|
34
|
+
case 'read_tree':
|
|
35
|
+
return readTree(plugin, request.payload as { remId: string; depth?: number; maxNodes?: number; maxSiblings?: number; ancestorLevels?: number; includePowerup?: boolean });
|
|
36
|
+
case 'write_rem_fields':
|
|
37
|
+
return writeRemFields(plugin, request.payload as { remId: string; changes: Record<string, unknown> });
|
|
38
|
+
case 'create_rem':
|
|
39
|
+
return createRem(plugin, request.payload as { content: string; parentId: string; position: number });
|
|
40
|
+
case 'delete_rem':
|
|
41
|
+
return deleteRem(plugin, request.payload as { remId: string });
|
|
42
|
+
case 'move_rem':
|
|
43
|
+
return moveRem(plugin, request.payload as { remId: string; newParentId: string; position: number });
|
|
44
|
+
case 'reorder_children':
|
|
45
|
+
return reorderChildren(plugin, request.payload as { parentId: string; order: string[] });
|
|
46
|
+
case 'read_globe':
|
|
47
|
+
return readGlobe(plugin, request.payload as { depth?: number; maxNodes?: number; maxSiblings?: number });
|
|
48
|
+
case 'read_context':
|
|
49
|
+
return readContext(plugin, request.payload as { mode?: 'focus' | 'page'; ancestorLevels?: number; maxNodes?: number; maxSiblings?: number; depth?: number });
|
|
50
|
+
case 'search':
|
|
51
|
+
return search(plugin, request.payload as { query: string; numResults?: number });
|
|
52
|
+
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`未实现的 action: ${request.action}`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Client — Plugin 侧连接守护进程
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 连接到 CLI 守护进程的 WS Server
|
|
6
|
+
* 2. 发送 hello 消息(携带版本 + SDK 状态)
|
|
7
|
+
* 3. 响应 ping 心跳返回 pong
|
|
8
|
+
* 4. 接收并执行来自守护进程转发的请求
|
|
9
|
+
* 5. 断线后指数退避自动重连
|
|
10
|
+
*
|
|
11
|
+
* 协议类型在 Plugin 层独立定义(与 CLI 层不共享代码)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── 协议类型(独立定义)──
|
|
15
|
+
|
|
16
|
+
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';
|
|
17
|
+
|
|
18
|
+
export interface HelloMessage {
|
|
19
|
+
type: 'hello';
|
|
20
|
+
version: string;
|
|
21
|
+
sdkReady: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PingMessage {
|
|
25
|
+
type: 'ping';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PongMessage {
|
|
29
|
+
type: 'pong';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BridgeRequest {
|
|
33
|
+
id: string;
|
|
34
|
+
action: string;
|
|
35
|
+
payload: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BridgeResponse {
|
|
39
|
+
id: string;
|
|
40
|
+
result?: unknown;
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── 配置 ──
|
|
45
|
+
|
|
46
|
+
export interface WebSocketClientConfig {
|
|
47
|
+
url: string;
|
|
48
|
+
pluginVersion: string;
|
|
49
|
+
sdkReady: boolean;
|
|
50
|
+
maxReconnectAttempts?: number;
|
|
51
|
+
initialReconnectDelay?: number;
|
|
52
|
+
maxReconnectDelay?: number;
|
|
53
|
+
onStatusChange?: (status: ConnectionStatus) => void;
|
|
54
|
+
onLog?: (message: string, level: 'info' | 'warn' | 'error') => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── WebSocket Client 实现 ──
|
|
58
|
+
|
|
59
|
+
export class WebSocketClient {
|
|
60
|
+
private ws: WebSocket | null = null;
|
|
61
|
+
private reconnectAttempts = 0;
|
|
62
|
+
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
63
|
+
private messageHandler: ((request: BridgeRequest) => Promise<unknown>) | null = null;
|
|
64
|
+
private status: ConnectionStatus = 'disconnected';
|
|
65
|
+
private isShuttingDown = false;
|
|
66
|
+
private _sdkReady: boolean;
|
|
67
|
+
|
|
68
|
+
private config: Required<Omit<WebSocketClientConfig, 'onStatusChange' | 'onLog'>> & {
|
|
69
|
+
onStatusChange?: (status: ConnectionStatus) => void;
|
|
70
|
+
onLog?: (message: string, level: 'info' | 'warn' | 'error') => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
constructor(config: WebSocketClientConfig) {
|
|
74
|
+
this._sdkReady = config.sdkReady;
|
|
75
|
+
this.config = {
|
|
76
|
+
url: config.url,
|
|
77
|
+
pluginVersion: config.pluginVersion,
|
|
78
|
+
sdkReady: config.sdkReady,
|
|
79
|
+
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
|
80
|
+
initialReconnectDelay: config.initialReconnectDelay ?? 1000,
|
|
81
|
+
maxReconnectDelay: config.maxReconnectDelay ?? 30000,
|
|
82
|
+
onStatusChange: config.onStatusChange,
|
|
83
|
+
onLog: config.onLog,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private log(message: string, level: 'info' | 'warn' | 'error' = 'info'): void {
|
|
88
|
+
this.config.onLog?.(message, level);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private setStatus(newStatus: ConnectionStatus): void {
|
|
92
|
+
if (this.status !== newStatus) {
|
|
93
|
+
this.status = newStatus;
|
|
94
|
+
this.config.onStatusChange?.(newStatus);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private sendHello(): void {
|
|
99
|
+
const hello: HelloMessage = {
|
|
100
|
+
type: 'hello',
|
|
101
|
+
version: this.config.pluginVersion,
|
|
102
|
+
sdkReady: this._sdkReady,
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
this.ws?.send(JSON.stringify(hello));
|
|
106
|
+
this.log(`发送 hello(v${this.config.pluginVersion}, sdkReady=${this._sdkReady})`);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this.log(`发送 hello 失败: ${error}`, 'warn');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
connect(): void {
|
|
113
|
+
if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.isShuttingDown = false;
|
|
118
|
+
this.setStatus('connecting');
|
|
119
|
+
this.log(`正在连接 ${this.config.url}...`);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
this.ws = new WebSocket(this.config.url);
|
|
123
|
+
|
|
124
|
+
this.ws.onopen = () => {
|
|
125
|
+
this.log('已连接到守护进程');
|
|
126
|
+
this.reconnectAttempts = 0;
|
|
127
|
+
this.setStatus('connected');
|
|
128
|
+
this.sendHello();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
this.ws.onmessage = async (event) => {
|
|
132
|
+
await this.handleMessage(typeof event.data === 'string' ? event.data : String(event.data));
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
this.ws.onclose = (event) => {
|
|
136
|
+
this.log(`连接断开: ${event.code} ${event.reason}`, 'warn');
|
|
137
|
+
this.setStatus('disconnected');
|
|
138
|
+
|
|
139
|
+
if (!this.isShuttingDown) {
|
|
140
|
+
this.scheduleReconnect();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
this.ws.onerror = (error) => {
|
|
145
|
+
this.log(`WebSocket 错误: ${error}`, 'error');
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.log(`连接失败: ${error}`, 'error');
|
|
149
|
+
this.setStatus('disconnected');
|
|
150
|
+
this.scheduleReconnect();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async handleMessage(data: string): Promise<void> {
|
|
155
|
+
try {
|
|
156
|
+
const message = JSON.parse(data);
|
|
157
|
+
|
|
158
|
+
// 心跳响应
|
|
159
|
+
if (message.type === 'ping') {
|
|
160
|
+
this.ws?.send(JSON.stringify({ type: 'pong' } as PongMessage));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 处理来自守护进程转发的请求
|
|
165
|
+
if (message.id && message.action && this.messageHandler) {
|
|
166
|
+
const request = message as BridgeRequest;
|
|
167
|
+
this.log(`收到请求: ${request.action}`);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const result = await this.messageHandler(request);
|
|
171
|
+
const response: BridgeResponse = { id: request.id, result };
|
|
172
|
+
this.ws?.send(JSON.stringify(response));
|
|
173
|
+
this.log(`完成: ${request.action}`);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
176
|
+
const response: BridgeResponse = { id: request.id, error: errorMessage };
|
|
177
|
+
this.ws?.send(JSON.stringify(response));
|
|
178
|
+
this.log(`失败: ${request.action} - ${errorMessage}`, 'error');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
this.log(`处理消息失败: ${error}`, 'error');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private scheduleReconnect(): void {
|
|
187
|
+
if (this.isShuttingDown) return;
|
|
188
|
+
|
|
189
|
+
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
190
|
+
this.log('已达最大重连次数', 'error');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 指数退避 + 抖动
|
|
195
|
+
const baseDelay = Math.min(
|
|
196
|
+
this.config.initialReconnectDelay * Math.pow(2, this.reconnectAttempts),
|
|
197
|
+
this.config.maxReconnectDelay,
|
|
198
|
+
);
|
|
199
|
+
const jitter = Math.random() * 0.3 * baseDelay;
|
|
200
|
+
const delay = baseDelay + jitter;
|
|
201
|
+
|
|
202
|
+
this.reconnectAttempts++;
|
|
203
|
+
this.log(
|
|
204
|
+
`${Math.round(delay)}ms 后重连(第 ${this.reconnectAttempts}/${this.config.maxReconnectAttempts} 次)`,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
208
|
+
this.connect();
|
|
209
|
+
}, delay);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
setMessageHandler(handler: (request: BridgeRequest) => Promise<unknown>): void {
|
|
213
|
+
this.messageHandler = handler;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
setSdkReady(ready: boolean): void {
|
|
217
|
+
this._sdkReady = ready;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
disconnect(): void {
|
|
221
|
+
this.isShuttingDown = true;
|
|
222
|
+
|
|
223
|
+
if (this.reconnectTimeout) {
|
|
224
|
+
clearTimeout(this.reconnectTimeout);
|
|
225
|
+
this.reconnectTimeout = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (this.ws) {
|
|
229
|
+
this.ws.close(1000, 'Plugin disconnect');
|
|
230
|
+
this.ws = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.setStatus('disconnected');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
reconnect(): void {
|
|
237
|
+
this.reconnectAttempts = 0;
|
|
238
|
+
this.disconnect();
|
|
239
|
+
this.connect();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getStatus(): ConnectionStatus {
|
|
243
|
+
return this.status;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Add your plugin styles here. This file is named <widget-name>.css; in this case, index.css*/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* breadcrumb.ts — 面包屑路径构建
|
|
3
|
+
*
|
|
4
|
+
* SDK 辅助函数。被 read-globe、read-context 共享。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 从 rem 向上追溯到根,返回路径名称数组(从根到当前)。
|
|
11
|
+
*/
|
|
12
|
+
export async function buildBreadcrumb(
|
|
13
|
+
plugin: ReactRNPlugin,
|
|
14
|
+
rem: Rem,
|
|
15
|
+
): Promise<string[]> {
|
|
16
|
+
const path: string[] = [];
|
|
17
|
+
let current: Rem | undefined = rem;
|
|
18
|
+
|
|
19
|
+
while (current) {
|
|
20
|
+
const text = await plugin.richText.toMarkdown(current.text ?? []);
|
|
21
|
+
path.unshift(text.replace(/\n/g, ' ').trim() || current._id);
|
|
22
|
+
current = await current.getParentRem();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create-rem service — 创建新 Rem
|
|
3
|
+
*
|
|
4
|
+
* 内部原子操作:create_rem (action) → create-rem.ts (文件) → createRem (函数)
|
|
5
|
+
*
|
|
6
|
+
* 使用 createSingleRemWithMarkdown 一步到位创建:
|
|
7
|
+
* - 自动解析 Markdown 中的闪卡语法(::, >>, <<, <>, ;;, >>>, {{}} 等)
|
|
8
|
+
* - 自动设置 type、backText、practiceDirection 等属性
|
|
9
|
+
* - 自动设置父节点
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ReactRNPlugin } from '@remnote/plugin-sdk';
|
|
13
|
+
|
|
14
|
+
export interface CreateRemPayload {
|
|
15
|
+
/** RemNote Markdown 格式的内容 */
|
|
16
|
+
content: string;
|
|
17
|
+
/** 父节点 Rem ID */
|
|
18
|
+
parentId: string;
|
|
19
|
+
/** 在兄弟中的位置(0-based) */
|
|
20
|
+
position: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CreateRemResult {
|
|
24
|
+
remId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 创建新 Rem。
|
|
29
|
+
*
|
|
30
|
+
* @throws Error — 父节点不存在、创建失败
|
|
31
|
+
*/
|
|
32
|
+
export async function createRem(
|
|
33
|
+
plugin: ReactRNPlugin,
|
|
34
|
+
payload: CreateRemPayload,
|
|
35
|
+
): Promise<CreateRemResult> {
|
|
36
|
+
const { content, parentId, position } = payload;
|
|
37
|
+
|
|
38
|
+
// 验证父节点存在
|
|
39
|
+
const parent = await plugin.rem.findOne(parentId);
|
|
40
|
+
if (!parent) {
|
|
41
|
+
throw new Error(`Parent Rem not found: ${parentId}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 使用 createSingleRemWithMarkdown 创建(自动解析闪卡语法)
|
|
45
|
+
const newRem = await plugin.rem.createSingleRemWithMarkdown(content, parentId);
|
|
46
|
+
if (!newRem) {
|
|
47
|
+
throw new Error(`Failed to create Rem with content: ${content.slice(0, 50)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 设置位置(createSingleRemWithMarkdown 默认追加到末尾)
|
|
51
|
+
// 如果 position 不是末尾,需要用 setParent 调整
|
|
52
|
+
const siblings = await parent.getChildrenRem();
|
|
53
|
+
const currentIndex = siblings.findIndex(r => r._id === newRem._id);
|
|
54
|
+
if (currentIndex !== -1 && currentIndex !== position && position < siblings.length) {
|
|
55
|
+
await newRem.setParent(parent, position);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { remId: newRem._id };
|
|
59
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delete-rem service — 删除 Rem
|
|
3
|
+
*
|
|
4
|
+
* 内部原子操作:delete_rem (action) → delete-rem.ts (文件) → deleteRem (函数)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ReactRNPlugin } from '@remnote/plugin-sdk';
|
|
8
|
+
|
|
9
|
+
export interface DeleteRemPayload {
|
|
10
|
+
remId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 删除指定 Rem。
|
|
15
|
+
*
|
|
16
|
+
* @throws Error — Rem 不存在
|
|
17
|
+
*/
|
|
18
|
+
export async function deleteRem(
|
|
19
|
+
plugin: ReactRNPlugin,
|
|
20
|
+
payload: DeleteRemPayload,
|
|
21
|
+
): Promise<{ ok: true }> {
|
|
22
|
+
const rem = await plugin.rem.findOne(payload.remId);
|
|
23
|
+
if (!rem) {
|
|
24
|
+
throw new Error(`Rem not found: ${payload.remId}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await rem.remove();
|
|
28
|
+
return { ok: true };
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Services 层 — 业务操作(与 CLI 命令同态命名)
|
|
3
|
+
*
|
|
4
|
+
* 每个文件对应一个 CLI 命令,封装完整的 RemNote SDK 操作链。
|
|
5
|
+
* 由 bridge 层调用,不直接暴露给 widgets。
|
|
6
|
+
*
|
|
7
|
+
* 依赖方向:services → utils(单向)
|
|
8
|
+
*
|
|
9
|
+
* 待实现:
|
|
10
|
+
* - read-note.ts → readNote()
|
|
11
|
+
* - create-note.ts → createNote()
|
|
12
|
+
* - update-note.ts → updateNote()
|
|
13
|
+
* - search.ts → search()
|
|
14
|
+
* - search-by-tag.ts → searchByTag()
|
|
15
|
+
* - append-journal.ts → appendJournal()
|
|
16
|
+
*/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* move-rem service — 移动 Rem(改变父节点)
|
|
3
|
+
*
|
|
4
|
+
* 内部原子操作:move_rem (action) → move-rem.ts (文件) → moveRem (函数)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ReactRNPlugin } from '@remnote/plugin-sdk';
|
|
8
|
+
|
|
9
|
+
export interface MoveRemPayload {
|
|
10
|
+
remId: string;
|
|
11
|
+
newParentId: string;
|
|
12
|
+
/** 在新父节点 children 中的位置(0-based) */
|
|
13
|
+
position: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 将 Rem 移动到新的父节点下。
|
|
18
|
+
*
|
|
19
|
+
* @throws Error — Rem 或新父节点不存在
|
|
20
|
+
*/
|
|
21
|
+
export async function moveRem(
|
|
22
|
+
plugin: ReactRNPlugin,
|
|
23
|
+
payload: MoveRemPayload,
|
|
24
|
+
): Promise<{ ok: true }> {
|
|
25
|
+
const { remId, newParentId, position } = payload;
|
|
26
|
+
|
|
27
|
+
const rem = await plugin.rem.findOne(remId);
|
|
28
|
+
if (!rem) {
|
|
29
|
+
throw new Error(`Rem not found: ${remId}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const newParent = await plugin.rem.findOne(newParentId);
|
|
33
|
+
if (!newParent) {
|
|
34
|
+
throw new Error(`New parent Rem not found: ${newParentId}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await rem.setParent(newParent, position);
|
|
38
|
+
return { ok: true };
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* powerup-filter — Powerup 噪音过滤工具
|
|
3
|
+
*
|
|
4
|
+
* RemNote 的格式设置(fontSize、highlightColor、isCode 等)底层通过 Powerup 机制实现,
|
|
5
|
+
* 会向 Rem 注入隐藏的 Tag 和 descriptor 子 Rem。这些在 UI 中不可见,对 AI Agent 是噪音。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PluginRem as Rem } from '@remnote/plugin-sdk';
|
|
9
|
+
|
|
10
|
+
/** 判断 Rem 是否为 Powerup 产生的隐藏子 Rem */
|
|
11
|
+
export async function isNoisyPowerupChild(rem: Rem): Promise<boolean> {
|
|
12
|
+
const [a, b, c, d] = await Promise.all([
|
|
13
|
+
rem.isPowerupProperty(),
|
|
14
|
+
rem.isPowerupSlot(),
|
|
15
|
+
rem.isPowerupPropertyListItem(),
|
|
16
|
+
rem.isPowerupEnum(),
|
|
17
|
+
]);
|
|
18
|
+
return a || b || c || d;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 批量过滤:返回非 Powerup 隐藏子 Rem */
|
|
22
|
+
export async function filterNoisyChildren(rems: Rem[]): Promise<Rem[]> {
|
|
23
|
+
const flags = await Promise.all(rems.map(isNoisyPowerupChild));
|
|
24
|
+
return rems.filter((_, i) => !flags[i]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 过滤 Tag:去掉 isPowerup=true 的系统 Tag */
|
|
28
|
+
export async function filterNoisyTags(tagRems: Rem[]): Promise<Rem[]> {
|
|
29
|
+
const flags = await Promise.all(tagRems.map(t => t.isPowerup()));
|
|
30
|
+
return tagRems.filter((_, i) => !flags[i]);
|
|
31
|
+
}
|