remnote-bridge 0.1.11 → 0.1.13

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 (70) hide show
  1. package/dist/cli/addon/addon-manager.js +163 -0
  2. package/dist/cli/addon/registry.js +24 -0
  3. package/dist/cli/commands/addon.js +149 -0
  4. package/dist/cli/commands/clean.js +121 -52
  5. package/dist/cli/commands/connect.js +72 -33
  6. package/dist/cli/commands/disconnect.js +19 -19
  7. package/dist/cli/commands/edit-rem.js +8 -36
  8. package/dist/cli/commands/edit-tree.js +3 -20
  9. package/dist/cli/commands/health.js +19 -18
  10. package/dist/cli/commands/read-context.js +3 -20
  11. package/dist/cli/commands/read-globe.js +3 -20
  12. package/dist/cli/commands/read-rem.js +6 -32
  13. package/dist/cli/commands/read-tree.js +3 -20
  14. package/dist/cli/commands/search.js +97 -21
  15. package/dist/cli/config.js +148 -72
  16. package/dist/cli/daemon/daemon.js +104 -24
  17. package/dist/cli/daemon/dev-server.js +9 -1
  18. package/dist/cli/daemon/pid.js +36 -22
  19. package/dist/cli/daemon/registry.js +160 -0
  20. package/dist/cli/daemon/send-request.js +11 -11
  21. package/dist/cli/daemon/static-server.js +97 -34
  22. package/dist/cli/handlers/edit-handler.js +49 -140
  23. package/dist/cli/handlers/read-handler.js +9 -9
  24. package/dist/cli/handlers/rem-cache.js +10 -5
  25. package/dist/cli/handlers/tree-parser.js +16 -9
  26. package/dist/cli/main.js +67 -19
  27. package/dist/cli/protocol.js +18 -4
  28. package/dist/cli/server/config-server.js +280 -14
  29. package/dist/cli/server/ws-server.js +93 -44
  30. package/dist/cli/utils/output.js +29 -0
  31. package/dist/mcp/format.js +43 -0
  32. package/dist/mcp/index.js +0 -55
  33. package/dist/mcp/instructions.js +424 -216
  34. package/dist/mcp/resources/edit-rem-guide.js +37 -158
  35. package/dist/mcp/resources/edit-tree-guide.js +1 -1
  36. package/dist/mcp/resources/error-reference.js +9 -13
  37. package/dist/mcp/resources/rem-object-fields.js +6 -6
  38. package/dist/mcp/tools/edit-tools.js +69 -8
  39. package/dist/mcp/tools/infra-tools.js +44 -8
  40. package/dist/mcp/tools/read-tools.js +136 -20
  41. package/package.json +2 -2
  42. package/remnote-plugin/dist/bridge_widget-sandbox.js +17 -17
  43. package/remnote-plugin/dist/bridge_widget.js +17 -17
  44. package/remnote-plugin/dist/index-sandbox.js +31 -31
  45. package/remnote-plugin/dist/index.js +31 -31
  46. package/remnote-plugin/dist/manifest.json +1 -1
  47. package/remnote-plugin/package.json +1 -1
  48. package/remnote-plugin/public/manifest.json +1 -1
  49. package/remnote-plugin/src/bridge/multi-connection-manager.ts +151 -0
  50. package/remnote-plugin/src/bridge/websocket-client.ts +62 -16
  51. package/remnote-plugin/src/services/index.ts +0 -8
  52. package/remnote-plugin/src/services/read-rem.ts +1 -9
  53. package/remnote-plugin/src/services/search.ts +13 -10
  54. package/remnote-plugin/src/settings.ts +9 -7
  55. package/remnote-plugin/src/utils/index.ts +0 -5
  56. package/remnote-plugin/src/widgets/bridge_widget.tsx +105 -20
  57. package/remnote-plugin/src/widgets/index.tsx +41 -44
  58. package/remnote-plugin/webpack.config.js +35 -0
  59. package/skills/remnote-bridge/SKILL.md +45 -40
  60. package/skills/remnote-bridge/instructions/addon.md +134 -0
  61. package/skills/remnote-bridge/instructions/clean.md +110 -0
  62. package/skills/remnote-bridge/instructions/connect.md +80 -37
  63. package/skills/remnote-bridge/instructions/disconnect.md +22 -9
  64. package/skills/remnote-bridge/instructions/edit-rem.md +113 -327
  65. package/skills/remnote-bridge/instructions/health.md +23 -13
  66. package/skills/remnote-bridge/instructions/install-skill.md +58 -0
  67. package/skills/remnote-bridge/instructions/overall.md +99 -35
  68. package/skills/remnote-bridge/instructions/read-rem.md +15 -15
  69. package/skills/remnote-bridge/instructions/search.md +77 -18
  70. package/skills/remnote-bridge/instructions/setup.md +5 -6
@@ -6,7 +6,7 @@
6
6
  "repoUrl": "https://github.com/baobao700508/unofficial-remnote-bridge-cli",
7
7
  "version": {
8
8
  "major": 0,
9
- "minor": 1,
9
+ "minor": 2,
10
10
  "patch": 0
11
11
  },
12
12
  "theme": [],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": true,
3
3
  "name": "unofficial-remnote-bridge-plugin",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "license": "MIT",
6
6
  "description": "RemNote 桥接层:嵌入 RemNote 的 WebSocket 桥接插件",
7
7
  "scripts": {
@@ -6,7 +6,7 @@
6
6
  "repoUrl": "https://github.com/baobao700508/unofficial-remnote-bridge-cli",
7
7
  "version": {
8
8
  "major": 0,
9
- "minor": 1,
9
+ "minor": 2,
10
10
  "patch": 0
11
11
  },
12
12
  "theme": [],
@@ -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
- this.log(`连接断开: ${event.code} ${event.reason}`, 'warn');
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 = (error) => {
145
- this.log(`WebSocket 错误: ${error}`, 'error');
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
- for (const rem of rems) {
43
- const markdownText = await safeToMarkdown(plugin, rem.text ?? []);
44
- const isDocument = await rem.isDocument();
45
- results.push({
46
- remId: rem._id,
47
- text: markdownText.replace(/\n/g, ' '),
48
- isDocument,
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
- * 定义 RemNote 设置面板中的设置项 ID 和默认值。
4
+ * 定义默认值和版本号。
5
+ * 多 daemon 连接:Plugin 同时连接 ALL_WS_PORTS 对应的 4 个槽位。
5
6
  */
6
7
 
7
- // 设置项 ID
8
- export const SETTING_WS_URL = 'bridge-ws-url';
8
+ export const DEFAULT_PLUGIN_VERSION = '0.2.0';
9
9
 
10
- // 默认值
11
- export const DEFAULT_WS_URL = 'ws://127.0.0.1:3002';
12
- export const DEFAULT_PLUGIN_VERSION = '0.1.0';
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;
@@ -2,9 +2,4 @@
2
2
  * Utils 层 — 共享辅助工具(无状态纯函数)
3
3
  *
4
4
  * 被 services 层单向依赖,不依赖任何其他业务层。
5
- *
6
- * 待实现:
7
- * - rich-text.ts → 富文本解析(extractText 等)
8
- * - content-renderer.ts → 内容渲染(Markdown / Structured)
9
- * - rem-classifier.ts → Rem 分类、别名、父级解析
10
5
  */
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Bridge Widget — 显示守护进程连接状态(纯展示)
2
+ * Bridge Widget — 显示多 daemon 连接状态(纯展示)
3
3
  *
4
- * WebSocket 连接在 index.tsx 的 onActivate 中建立,
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 { ConnectionStatus } from '../bridge/websocket-client';
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 [status, setStatus] = useState<ConnectionStatus>('disconnected');
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-status');
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
- setStatus((s as ConnectionStatus) ?? 'disconnected');
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 statusConfig = {
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 }}>RemNote Bridge</h3>
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: currentStatus.bg,
73
- color: currentStatus.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>{currentStatus.icon}</span>
79
- <span>{currentStatus.text}</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('http://127.0.0.1:3003', '_blank')}
231
+ onClick={() => window.open(`http://127.0.0.1:${configPort}`, '_blank')}
147
232
  style={{
148
233
  display: 'flex',
149
234
  alignItems: 'center',