remnote-bridge 0.1.11 → 0.1.12
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/addon/addon-manager.js +163 -0
- package/dist/cli/addon/registry.js +24 -0
- package/dist/cli/commands/addon.js +149 -0
- package/dist/cli/commands/clean.js +121 -52
- package/dist/cli/commands/connect.js +72 -33
- package/dist/cli/commands/disconnect.js +19 -19
- package/dist/cli/commands/edit-rem.js +3 -31
- package/dist/cli/commands/edit-tree.js +3 -20
- package/dist/cli/commands/health.js +19 -18
- package/dist/cli/commands/read-context.js +3 -20
- package/dist/cli/commands/read-globe.js +3 -20
- package/dist/cli/commands/read-rem.js +3 -31
- package/dist/cli/commands/read-tree.js +3 -20
- package/dist/cli/commands/search.js +97 -21
- package/dist/cli/config.js +148 -72
- package/dist/cli/daemon/daemon.js +104 -24
- package/dist/cli/daemon/dev-server.js +9 -1
- package/dist/cli/daemon/pid.js +36 -22
- package/dist/cli/daemon/registry.js +160 -0
- package/dist/cli/daemon/send-request.js +11 -11
- package/dist/cli/daemon/static-server.js +97 -34
- package/dist/cli/handlers/read-handler.js +4 -3
- package/dist/cli/handlers/tree-parser.js +16 -9
- package/dist/cli/main.js +49 -9
- package/dist/cli/protocol.js +18 -4
- package/dist/cli/server/config-server.js +280 -14
- package/dist/cli/server/ws-server.js +93 -44
- package/dist/cli/utils/output.js +29 -0
- package/dist/mcp/instructions.js +101 -9
- package/dist/mcp/resources/edit-rem-guide.js +3 -4
- package/dist/mcp/resources/error-reference.js +2 -2
- package/dist/mcp/resources/rem-object-fields.js +3 -3
- package/dist/mcp/tools/infra-tools.js +54 -6
- package/dist/mcp/tools/read-tools.js +9 -2
- package/package.json +2 -2
- package/remnote-plugin/dist/bridge_widget-sandbox.js +17 -17
- package/remnote-plugin/dist/bridge_widget.js +17 -17
- package/remnote-plugin/dist/index-sandbox.js +31 -31
- package/remnote-plugin/dist/index.js +31 -31
- package/remnote-plugin/dist/manifest.json +1 -1
- package/remnote-plugin/package.json +1 -1
- package/remnote-plugin/public/manifest.json +1 -1
- package/remnote-plugin/src/bridge/multi-connection-manager.ts +151 -0
- package/remnote-plugin/src/bridge/websocket-client.ts +62 -16
- package/remnote-plugin/src/services/index.ts +0 -8
- package/remnote-plugin/src/services/read-rem.ts +1 -9
- package/remnote-plugin/src/services/search.ts +13 -10
- package/remnote-plugin/src/settings.ts +9 -7
- package/remnote-plugin/src/utils/index.ts +0 -5
- package/remnote-plugin/src/widgets/bridge_widget.tsx +105 -20
- package/remnote-plugin/src/widgets/index.tsx +41 -44
- package/remnote-plugin/webpack.config.js +35 -0
- package/skills/remnote-bridge/SKILL.md +14 -9
- package/skills/remnote-bridge/instructions/addon.md +134 -0
- package/skills/remnote-bridge/instructions/clean.md +110 -0
- package/skills/remnote-bridge/instructions/connect.md +80 -37
- package/skills/remnote-bridge/instructions/disconnect.md +22 -9
- package/skills/remnote-bridge/instructions/edit-rem.md +37 -9
- package/skills/remnote-bridge/instructions/health.md +23 -13
- package/skills/remnote-bridge/instructions/install-skill.md +58 -0
- package/skills/remnote-bridge/instructions/overall.md +76 -21
- package/skills/remnote-bridge/instructions/read-rem.md +10 -10
- package/skills/remnote-bridge/instructions/search.md +73 -14
- package/skills/remnote-bridge/instructions/setup.md +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Connection Manager — 管理 Plugin 到多个 Daemon 的连接
|
|
3
|
+
*
|
|
4
|
+
* 一个 Plugin 同时连接最多 4 个 daemon,通过"孪生优先级"机制
|
|
5
|
+
* 保证各 daemon 与其原生 Plugin 的亲和性,同时允许空闲 daemon 被接管。
|
|
6
|
+
*
|
|
7
|
+
* 依赖方向:multi-connection-manager → websocket-client(单向)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { WebSocketClient } from './websocket-client';
|
|
11
|
+
import type { ConnectionStatus, BridgeRequest } from './websocket-client';
|
|
12
|
+
import { ALL_WS_PORTS, SCAN_INTERVAL_MS } from '../settings';
|
|
13
|
+
|
|
14
|
+
// ── 类型定义 ──
|
|
15
|
+
|
|
16
|
+
export type DisconnectReason = 'not_started' | 'preempted' | 'twin_occupied' | 'other_occupied' | null;
|
|
17
|
+
|
|
18
|
+
export interface SlotState {
|
|
19
|
+
slotIndex: number;
|
|
20
|
+
wsPort: number;
|
|
21
|
+
status: ConnectionStatus;
|
|
22
|
+
isTwin: boolean;
|
|
23
|
+
disconnectReason: DisconnectReason;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MultiConnectionManagerConfig {
|
|
27
|
+
twinSlotIndex: number;
|
|
28
|
+
pluginVersion: string;
|
|
29
|
+
sdkReady: boolean;
|
|
30
|
+
onSlotsChange: (slots: SlotState[]) => void;
|
|
31
|
+
onLog: (slotIndex: number, message: string, level: 'info' | 'warn' | 'error') => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── 实现 ──
|
|
35
|
+
|
|
36
|
+
export class MultiConnectionManager {
|
|
37
|
+
private clients: WebSocketClient[] = [];
|
|
38
|
+
private slotStates: SlotState[] = [];
|
|
39
|
+
private scanTimer: ReturnType<typeof setInterval> | null = null;
|
|
40
|
+
private config: MultiConnectionManagerConfig;
|
|
41
|
+
|
|
42
|
+
constructor(config: MultiConnectionManagerConfig) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
|
|
45
|
+
// 初始化 4 个槽位状态 + 创建 4 个 WebSocketClient
|
|
46
|
+
for (let i = 0; i < ALL_WS_PORTS.length; i++) {
|
|
47
|
+
const isTwin = (i === config.twinSlotIndex);
|
|
48
|
+
|
|
49
|
+
this.slotStates.push({
|
|
50
|
+
slotIndex: i,
|
|
51
|
+
wsPort: ALL_WS_PORTS[i],
|
|
52
|
+
status: 'disconnected',
|
|
53
|
+
isTwin,
|
|
54
|
+
disconnectReason: 'not_started',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.clients.push(new WebSocketClient({
|
|
58
|
+
url: `ws://127.0.0.1:${ALL_WS_PORTS[i]}`,
|
|
59
|
+
pluginVersion: config.pluginVersion,
|
|
60
|
+
sdkReady: config.sdkReady,
|
|
61
|
+
twinSlotIndex: config.twinSlotIndex,
|
|
62
|
+
isTwinConnection: isTwin,
|
|
63
|
+
maxReconnectAttempts: isTwin ? 10 : 0, // 非孪生不自动重连
|
|
64
|
+
initialReconnectDelay: 1000,
|
|
65
|
+
maxReconnectDelay: 30000,
|
|
66
|
+
onStatusChange: (status) => this.handleStatusChange(i, status),
|
|
67
|
+
onLog: (message, level) => config.onLog(i, message, level),
|
|
68
|
+
onPreempted: () => this.handleDisconnectReason(i, 'preempted'),
|
|
69
|
+
onTwinOccupied: () => this.handleDisconnectReason(i, 'twin_occupied'),
|
|
70
|
+
onOtherOccupied: () => this.handleDisconnectReason(i, 'other_occupied'),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 设置消息处理器(所有连接共享同一个 messageHandler) */
|
|
76
|
+
setMessageHandler(handler: (request: BridgeRequest) => Promise<unknown>): void {
|
|
77
|
+
for (const client of this.clients) {
|
|
78
|
+
client.setMessageHandler(handler);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 启动连接:先连孪生槽位,延迟 2s 后连其余 */
|
|
83
|
+
start(): void {
|
|
84
|
+
const twinIdx = this.config.twinSlotIndex;
|
|
85
|
+
|
|
86
|
+
// 先连孪生
|
|
87
|
+
this.clients[twinIdx].connect();
|
|
88
|
+
|
|
89
|
+
// 延迟 2s 后连其余
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
for (let i = 0; i < this.clients.length; i++) {
|
|
92
|
+
if (i !== twinIdx) {
|
|
93
|
+
this.clients[i].connect();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, 2000);
|
|
97
|
+
|
|
98
|
+
// 启动周期扫描
|
|
99
|
+
this.scanTimer = setInterval(() => this.scanAndReconnect(), SCAN_INTERVAL_MS);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 停止所有连接和定时器 */
|
|
103
|
+
stop(): void {
|
|
104
|
+
if (this.scanTimer) {
|
|
105
|
+
clearInterval(this.scanTimer);
|
|
106
|
+
this.scanTimer = null;
|
|
107
|
+
}
|
|
108
|
+
for (const client of this.clients) {
|
|
109
|
+
client.disconnect();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 获取当前所有槽位状态 */
|
|
114
|
+
getSlots(): SlotState[] {
|
|
115
|
+
return this.slotStates.slice();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── 内部方法 ──
|
|
119
|
+
|
|
120
|
+
private handleStatusChange(slotIndex: number, status: ConnectionStatus): void {
|
|
121
|
+
const slot = this.slotStates[slotIndex];
|
|
122
|
+
slot.status = status;
|
|
123
|
+
if (status === 'connected') {
|
|
124
|
+
slot.disconnectReason = null;
|
|
125
|
+
}
|
|
126
|
+
this.notifySlotsChange();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private handleDisconnectReason(slotIndex: number, reason: DisconnectReason): void {
|
|
130
|
+
this.slotStates[slotIndex].disconnectReason = reason;
|
|
131
|
+
this.notifySlotsChange();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private notifySlotsChange(): void {
|
|
135
|
+
this.config.onSlotsChange(this.slotStates.slice());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 周期扫描:对所有未连接的非孪生槽位尝试重连 */
|
|
139
|
+
private scanAndReconnect(): void {
|
|
140
|
+
for (let i = 0; i < this.clients.length; i++) {
|
|
141
|
+
const slot = this.slotStates[i];
|
|
142
|
+
// 跳过孪生(有自己的重连逻辑)、已连接
|
|
143
|
+
if (slot.isTwin) continue;
|
|
144
|
+
if (slot.status === 'connected' || slot.status === 'connecting') continue;
|
|
145
|
+
// 所有非孪生槽位都参与轮询(含 preempted/twin_occupied/other_occupied)
|
|
146
|
+
// 对方 Plugin 可能已断开,daemon 空闲后我们能感知到
|
|
147
|
+
|
|
148
|
+
this.clients[i].connect();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -13,12 +13,23 @@
|
|
|
13
13
|
|
|
14
14
|
// ── 协议类型(独立定义)──
|
|
15
15
|
|
|
16
|
+
/** 已有其他 Plugin 连接(非孪生),拒绝 */
|
|
17
|
+
const WS_CLOSE_OTHER_CONNECTED = 4000;
|
|
18
|
+
/** 孪生已连,拒绝非孪生 */
|
|
19
|
+
const WS_CLOSE_TWIN_EXISTS = 4003;
|
|
20
|
+
|
|
16
21
|
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';
|
|
17
22
|
|
|
18
23
|
export interface HelloMessage {
|
|
19
24
|
type: 'hello';
|
|
20
25
|
version: string;
|
|
21
26
|
sdkReady: boolean;
|
|
27
|
+
twinSlotIndex: number; // Plugin 的孪生 daemon 槽位索引 (0-3)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PreemptedMessage {
|
|
31
|
+
type: 'preempted';
|
|
32
|
+
reason: string; // 'twin_plugin_connected'
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
export interface PingMessage {
|
|
@@ -47,11 +58,16 @@ export interface WebSocketClientConfig {
|
|
|
47
58
|
url: string;
|
|
48
59
|
pluginVersion: string;
|
|
49
60
|
sdkReady: boolean;
|
|
61
|
+
twinSlotIndex: number; // Plugin 的孪生槽位
|
|
62
|
+
isTwinConnection?: boolean; // 本连接是否孪生连接
|
|
50
63
|
maxReconnectAttempts?: number;
|
|
51
64
|
initialReconnectDelay?: number;
|
|
52
65
|
maxReconnectDelay?: number;
|
|
53
66
|
onStatusChange?: (status: ConnectionStatus) => void;
|
|
54
67
|
onLog?: (message: string, level: 'info' | 'warn' | 'error') => void;
|
|
68
|
+
onPreempted?: () => void; // 被孪生 Plugin 抢占回调
|
|
69
|
+
onTwinOccupied?: () => void; // 被拒绝:孪生 Plugin 已连(不可重试)
|
|
70
|
+
onOtherOccupied?: () => void; // 被拒绝:已有其他非孪生 Plugin(可重试)
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
// ── WebSocket Client 实现 ──
|
|
@@ -63,11 +79,16 @@ export class WebSocketClient {
|
|
|
63
79
|
private messageHandler: ((request: BridgeRequest) => Promise<unknown>) | null = null;
|
|
64
80
|
private status: ConnectionStatus = 'disconnected';
|
|
65
81
|
private isShuttingDown = false;
|
|
82
|
+
private isPreempted = false;
|
|
66
83
|
private _sdkReady: boolean;
|
|
67
84
|
|
|
68
|
-
private config: Required<Omit<WebSocketClientConfig, 'onStatusChange' | 'onLog'>> & {
|
|
85
|
+
private config: Required<Omit<WebSocketClientConfig, 'onStatusChange' | 'onLog' | 'onPreempted' | 'onTwinOccupied' | 'onOtherOccupied' | 'isTwinConnection'>> & {
|
|
69
86
|
onStatusChange?: (status: ConnectionStatus) => void;
|
|
70
87
|
onLog?: (message: string, level: 'info' | 'warn' | 'error') => void;
|
|
88
|
+
onPreempted?: () => void;
|
|
89
|
+
onTwinOccupied?: () => void;
|
|
90
|
+
onOtherOccupied?: () => void;
|
|
91
|
+
isTwinConnection: boolean;
|
|
71
92
|
};
|
|
72
93
|
|
|
73
94
|
constructor(config: WebSocketClientConfig) {
|
|
@@ -76,11 +97,16 @@ export class WebSocketClient {
|
|
|
76
97
|
url: config.url,
|
|
77
98
|
pluginVersion: config.pluginVersion,
|
|
78
99
|
sdkReady: config.sdkReady,
|
|
100
|
+
twinSlotIndex: config.twinSlotIndex,
|
|
101
|
+
isTwinConnection: config.isTwinConnection ?? false,
|
|
79
102
|
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
|
80
103
|
initialReconnectDelay: config.initialReconnectDelay ?? 1000,
|
|
81
104
|
maxReconnectDelay: config.maxReconnectDelay ?? 30000,
|
|
82
105
|
onStatusChange: config.onStatusChange,
|
|
83
106
|
onLog: config.onLog,
|
|
107
|
+
onPreempted: config.onPreempted,
|
|
108
|
+
onTwinOccupied: config.onTwinOccupied,
|
|
109
|
+
onOtherOccupied: config.onOtherOccupied,
|
|
84
110
|
};
|
|
85
111
|
}
|
|
86
112
|
|
|
@@ -100,10 +126,11 @@ export class WebSocketClient {
|
|
|
100
126
|
type: 'hello',
|
|
101
127
|
version: this.config.pluginVersion,
|
|
102
128
|
sdkReady: this._sdkReady,
|
|
129
|
+
twinSlotIndex: this.config.twinSlotIndex,
|
|
103
130
|
};
|
|
104
131
|
try {
|
|
105
132
|
this.ws?.send(JSON.stringify(hello));
|
|
106
|
-
this.log(`发送 hello(v${this.config.pluginVersion}, sdkReady=${this._sdkReady})`);
|
|
133
|
+
this.log(`发送 hello(v${this.config.pluginVersion}, sdkReady=${this._sdkReady}, twinSlot=${this.config.twinSlotIndex})`);
|
|
107
134
|
} catch (error) {
|
|
108
135
|
this.log(`发送 hello 失败: ${error}`, 'warn');
|
|
109
136
|
}
|
|
@@ -115,8 +142,8 @@ export class WebSocketClient {
|
|
|
115
142
|
}
|
|
116
143
|
|
|
117
144
|
this.isShuttingDown = false;
|
|
145
|
+
this.isPreempted = false;
|
|
118
146
|
this.setStatus('connecting');
|
|
119
|
-
this.log(`正在连接 ${this.config.url}...`);
|
|
120
147
|
|
|
121
148
|
try {
|
|
122
149
|
this.ws = new WebSocket(this.config.url);
|
|
@@ -133,16 +160,26 @@ export class WebSocketClient {
|
|
|
133
160
|
};
|
|
134
161
|
|
|
135
162
|
this.ws.onclose = (event) => {
|
|
136
|
-
|
|
163
|
+
// 1006 = 连接从未建立(daemon 未运行),不打日志
|
|
164
|
+
if (event.code !== 1006) {
|
|
165
|
+
this.log(`连接断开: ${event.code} ${event.reason}`, 'warn');
|
|
166
|
+
}
|
|
137
167
|
this.setStatus('disconnected');
|
|
138
168
|
|
|
169
|
+
// 被 daemon 拒绝
|
|
170
|
+
if (event.code === WS_CLOSE_TWIN_EXISTS) {
|
|
171
|
+
this.config.onTwinOccupied?.();
|
|
172
|
+
} else if (event.code === WS_CLOSE_OTHER_CONNECTED) {
|
|
173
|
+
this.config.onOtherOccupied?.();
|
|
174
|
+
}
|
|
175
|
+
|
|
139
176
|
if (!this.isShuttingDown) {
|
|
140
177
|
this.scheduleReconnect();
|
|
141
178
|
}
|
|
142
179
|
};
|
|
143
180
|
|
|
144
|
-
this.ws.onerror = (
|
|
145
|
-
|
|
181
|
+
this.ws.onerror = () => {
|
|
182
|
+
// 连接失败的错误由 onclose 处理,此处不重复打日志
|
|
146
183
|
};
|
|
147
184
|
} catch (error) {
|
|
148
185
|
this.log(`连接失败: ${error}`, 'error');
|
|
@@ -155,6 +192,14 @@ export class WebSocketClient {
|
|
|
155
192
|
try {
|
|
156
193
|
const message = JSON.parse(data);
|
|
157
194
|
|
|
195
|
+
// 抢占通知
|
|
196
|
+
if (message.type === 'preempted') {
|
|
197
|
+
this.isPreempted = true;
|
|
198
|
+
this.log(`被孪生 Plugin 抢占: ${message.reason}`, 'warn');
|
|
199
|
+
this.config.onPreempted?.();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
158
203
|
// 心跳响应
|
|
159
204
|
if (message.type === 'ping') {
|
|
160
205
|
this.ws?.send(JSON.stringify({ type: 'pong' } as PongMessage));
|
|
@@ -186,6 +231,17 @@ export class WebSocketClient {
|
|
|
186
231
|
private scheduleReconnect(): void {
|
|
187
232
|
if (this.isShuttingDown) return;
|
|
188
233
|
|
|
234
|
+
// 被抢占 → 不在此处重连,由外部轮询驱动
|
|
235
|
+
if (this.isPreempted) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 非孪生连接 → 不在此处重连,由外部轮询驱动
|
|
240
|
+
if (!this.config.isTwinConnection) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 孪生连接保留指数退避重连
|
|
189
245
|
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
190
246
|
this.log('已达最大重连次数', 'error');
|
|
191
247
|
return;
|
|
@@ -213,10 +269,6 @@ export class WebSocketClient {
|
|
|
213
269
|
this.messageHandler = handler;
|
|
214
270
|
}
|
|
215
271
|
|
|
216
|
-
setSdkReady(ready: boolean): void {
|
|
217
|
-
this._sdkReady = ready;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
272
|
disconnect(): void {
|
|
221
273
|
this.isShuttingDown = true;
|
|
222
274
|
|
|
@@ -233,12 +285,6 @@ export class WebSocketClient {
|
|
|
233
285
|
this.setStatus('disconnected');
|
|
234
286
|
}
|
|
235
287
|
|
|
236
|
-
reconnect(): void {
|
|
237
|
-
this.reconnectAttempts = 0;
|
|
238
|
-
this.disconnect();
|
|
239
|
-
this.connect();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
288
|
getStatus(): ConnectionStatus {
|
|
243
289
|
return this.status;
|
|
244
290
|
}
|
|
@@ -5,12 +5,4 @@
|
|
|
5
5
|
* 由 bridge 层调用,不直接暴露给 widgets。
|
|
6
6
|
*
|
|
7
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
8
|
*/
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
PropertyTypeValue,
|
|
17
17
|
} from '../types';
|
|
18
18
|
import { filterNoisyChildren, filterNoisyTags } from './powerup-filter';
|
|
19
|
+
import { remTypeToString } from './rem-builder';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* 读取单个 Rem,组装为完整 RemObject。
|
|
@@ -263,15 +264,6 @@ function sortRichTextKeys(rt: RichText): RichText {
|
|
|
263
264
|
});
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
/** SDK RemType 枚举值 → 字符串 */
|
|
267
|
-
function remTypeToString(type: number): RemTypeValue {
|
|
268
|
-
switch (type) {
|
|
269
|
-
case 1: return 'concept';
|
|
270
|
-
case 2: return 'descriptor';
|
|
271
|
-
case 6: return 'portal';
|
|
272
|
-
default: return 'default';
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
267
|
|
|
276
268
|
/** SDK PORTAL_TYPE 枚举值 → 字符串 */
|
|
277
269
|
function portalTypeToString(pt: number): PortalType {
|
|
@@ -38,16 +38,19 @@ export async function search(
|
|
|
38
38
|
|
|
39
39
|
const rems = await plugin.search.search([query], undefined, { numResults });
|
|
40
40
|
|
|
41
|
-
const results: SearchResultItem[] =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
const results: SearchResultItem[] = await Promise.all(
|
|
42
|
+
rems.map(async (rem) => {
|
|
43
|
+
const [markdownText, isDocument] = await Promise.all([
|
|
44
|
+
safeToMarkdown(plugin, rem.text ?? []),
|
|
45
|
+
rem.isDocument(),
|
|
46
|
+
]);
|
|
47
|
+
return {
|
|
48
|
+
remId: rem._id,
|
|
49
|
+
text: markdownText.replace(/\n/g, ' '),
|
|
50
|
+
isDocument,
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
51
54
|
|
|
52
55
|
return {
|
|
53
56
|
query,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin
|
|
2
|
+
* Plugin 常量
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 定义默认值和版本号。
|
|
5
|
+
* 多 daemon 连接:Plugin 同时连接 ALL_WS_PORTS 对应的 4 个槽位。
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
export const SETTING_WS_URL = 'bridge-ws-url';
|
|
8
|
+
export const DEFAULT_PLUGIN_VERSION = '0.2.0';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
export const
|
|
12
|
-
|
|
10
|
+
/** 4 个固定 WS 端口,对应 4 个 daemon 槽位 */
|
|
11
|
+
export const ALL_WS_PORTS = [29100, 29110, 29120, 29130] as const;
|
|
12
|
+
|
|
13
|
+
/** 非孪生槽位周期扫描间隔(ms) */
|
|
14
|
+
export const SCAN_INTERVAL_MS = 18_000;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bridge Widget —
|
|
2
|
+
* Bridge Widget — 显示多 daemon 连接状态(纯展示)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 此 widget 每秒从 plugin.storage
|
|
4
|
+
* 多连接管理在 index.tsx 的 onActivate 中建立,
|
|
5
|
+
* 此 widget 每秒从 plugin.storage 读取并显示所有槽位状态和日志。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { renderWidget, usePlugin } from '@remnote/plugin-sdk';
|
|
9
9
|
import React, { useEffect, useState } from 'react';
|
|
10
|
-
import type {
|
|
10
|
+
import type { SlotState, DisconnectReason } from '../bridge/multi-connection-manager';
|
|
11
11
|
|
|
12
12
|
interface StoredLog {
|
|
13
13
|
time: number;
|
|
@@ -15,10 +15,27 @@ interface StoredLog {
|
|
|
15
15
|
level: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** 未连接时的提示文案 */
|
|
19
|
+
function getDisconnectHint(reason: DisconnectReason): string {
|
|
20
|
+
switch (reason) {
|
|
21
|
+
case 'preempted':
|
|
22
|
+
return '被孪生 Plugin 抢占,已断开';
|
|
23
|
+
case 'twin_occupied':
|
|
24
|
+
return '孪生 Plugin 已连接,当前无法连接';
|
|
25
|
+
case 'other_occupied':
|
|
26
|
+
return '已有其他 Plugin 连接,等待重试';
|
|
27
|
+
case 'not_started':
|
|
28
|
+
default:
|
|
29
|
+
return '该实例未启动,或端口被占用';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
function BridgeWidget() {
|
|
19
34
|
const plugin = usePlugin();
|
|
20
|
-
const [
|
|
35
|
+
const [slots, setSlots] = useState<SlotState[]>([]);
|
|
21
36
|
const [logs, setLogs] = useState<StoredLog[]>([]);
|
|
37
|
+
const [instanceName, setInstanceName] = useState<string>('');
|
|
38
|
+
const [configPort, setConfigPort] = useState<number>(29102);
|
|
22
39
|
|
|
23
40
|
// 每秒轮询 plugin.storage 获取最新状态
|
|
24
41
|
useEffect(() => {
|
|
@@ -26,11 +43,15 @@ function BridgeWidget() {
|
|
|
26
43
|
|
|
27
44
|
async function poll() {
|
|
28
45
|
if (!active) return;
|
|
29
|
-
const s = await plugin.storage.getSession('bridge-
|
|
46
|
+
const s = await plugin.storage.getSession('bridge-slots');
|
|
30
47
|
const l = await plugin.storage.getSession('bridge-logs');
|
|
48
|
+
const inst = await plugin.storage.getSession('bridge-instance');
|
|
49
|
+
const cp = await plugin.storage.getSession('bridge-config-port');
|
|
31
50
|
if (active) {
|
|
32
|
-
|
|
51
|
+
setSlots((s as SlotState[]) ?? []);
|
|
33
52
|
setLogs((l as StoredLog[]) ?? []);
|
|
53
|
+
setInstanceName((inst as string) ?? '');
|
|
54
|
+
setConfigPort((cp as number) ?? 29102);
|
|
34
55
|
}
|
|
35
56
|
}
|
|
36
57
|
|
|
@@ -42,13 +63,7 @@ function BridgeWidget() {
|
|
|
42
63
|
};
|
|
43
64
|
}, [plugin]);
|
|
44
65
|
|
|
45
|
-
const
|
|
46
|
-
connected: { color: '#22c55e', bg: '#dcfce7', icon: '\u25cf', text: '已连接' },
|
|
47
|
-
connecting: { color: '#f59e0b', bg: '#fef3c7', icon: '\u25d0', text: '连接中...' },
|
|
48
|
-
disconnected: { color: '#ef4444', bg: '#fee2e2', icon: '\u25cb', text: '未连接' },
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const currentStatus = statusConfig[status];
|
|
66
|
+
const connectedCount = slots.filter(s => s.status === 'connected').length;
|
|
52
67
|
|
|
53
68
|
return (
|
|
54
69
|
<div style={{ padding: '12px', fontFamily: 'system-ui, sans-serif', fontSize: '13px' }}>
|
|
@@ -61,7 +76,9 @@ function BridgeWidget() {
|
|
|
61
76
|
marginBottom: '12px',
|
|
62
77
|
}}
|
|
63
78
|
>
|
|
64
|
-
<h3 style={{ margin: 0, fontSize: '14px', fontWeight: 600 }}>
|
|
79
|
+
<h3 style={{ margin: 0, fontSize: '14px', fontWeight: 600 }}>
|
|
80
|
+
RemNote Bridge{instanceName ? ` (${instanceName})` : ''}
|
|
81
|
+
</h3>
|
|
65
82
|
<div
|
|
66
83
|
style={{
|
|
67
84
|
display: 'flex',
|
|
@@ -69,15 +86,83 @@ function BridgeWidget() {
|
|
|
69
86
|
gap: '6px',
|
|
70
87
|
padding: '4px 8px',
|
|
71
88
|
borderRadius: '12px',
|
|
72
|
-
backgroundColor:
|
|
73
|
-
color:
|
|
89
|
+
backgroundColor: connectedCount > 0 ? '#dcfce7' : '#fee2e2',
|
|
90
|
+
color: connectedCount > 0 ? '#22c55e' : '#ef4444',
|
|
74
91
|
fontSize: '12px',
|
|
75
92
|
fontWeight: 500,
|
|
76
93
|
}}
|
|
77
94
|
>
|
|
78
|
-
<span>{
|
|
79
|
-
<span>{
|
|
95
|
+
<span>{connectedCount > 0 ? '\u25cf' : '\u25cb'}</span>
|
|
96
|
+
<span>{connectedCount}</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* 连接状态 */}
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
border: '1px solid #e5e7eb',
|
|
104
|
+
borderRadius: '6px',
|
|
105
|
+
backgroundColor: '#f9fafb',
|
|
106
|
+
marginBottom: '12px',
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<div
|
|
110
|
+
style={{
|
|
111
|
+
fontSize: '11px',
|
|
112
|
+
fontWeight: 600,
|
|
113
|
+
padding: '8px 10px',
|
|
114
|
+
borderBottom: '1px solid #e5e7eb',
|
|
115
|
+
color: '#6b7280',
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
连接状态
|
|
80
119
|
</div>
|
|
120
|
+
{slots.map((slot) => (
|
|
121
|
+
<div key={slot.slotIndex}>
|
|
122
|
+
<div
|
|
123
|
+
style={{
|
|
124
|
+
display: 'flex',
|
|
125
|
+
alignItems: 'center',
|
|
126
|
+
justifyContent: 'space-between',
|
|
127
|
+
padding: '6px 10px',
|
|
128
|
+
borderBottom: '1px solid #f3f4f6',
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<span style={{ fontSize: '12px' }}>
|
|
132
|
+
{slot.isTwin ? '\u2605 ' : ' '}
|
|
133
|
+
槽位 {slot.slotIndex} ({slot.wsPort})
|
|
134
|
+
</span>
|
|
135
|
+
<span
|
|
136
|
+
style={{
|
|
137
|
+
fontSize: '11px',
|
|
138
|
+
color:
|
|
139
|
+
slot.status === 'connected'
|
|
140
|
+
? '#22c55e'
|
|
141
|
+
: slot.status === 'connecting'
|
|
142
|
+
? '#f59e0b'
|
|
143
|
+
: '#9ca3af',
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{slot.status === 'connected'
|
|
147
|
+
? '\u25cf 已连接'
|
|
148
|
+
: slot.status === 'connecting'
|
|
149
|
+
? '\u25d0 连接中...'
|
|
150
|
+
: '\u25cb 未连接'}
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
{slot.status === 'disconnected' && (
|
|
154
|
+
<div
|
|
155
|
+
style={{
|
|
156
|
+
padding: '2px 10px 6px 26px',
|
|
157
|
+
fontSize: '10px',
|
|
158
|
+
color: '#9ca3af',
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{getDisconnectHint(slot.disconnectReason)}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
81
166
|
</div>
|
|
82
167
|
|
|
83
168
|
{/* 日志 */}
|
|
@@ -143,7 +228,7 @@ function BridgeWidget() {
|
|
|
143
228
|
|
|
144
229
|
{/* 配置按钮 */}
|
|
145
230
|
<button
|
|
146
|
-
onClick={() => window.open(
|
|
231
|
+
onClick={() => window.open(`http://127.0.0.1:${configPort}`, '_blank')}
|
|
147
232
|
style={{
|
|
148
233
|
display: 'flex',
|
|
149
234
|
alignItems: 'center',
|