remnote-bridge 0.1.10 → 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.
Files changed (68) 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 +3 -31
  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 +8 -23
  11. package/dist/cli/commands/read-globe.js +3 -20
  12. package/dist/cli/commands/read-rem.js +3 -31
  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/context-read-handler.js +5 -3
  23. package/dist/cli/handlers/read-handler.js +4 -3
  24. package/dist/cli/handlers/tree-parser.js +16 -9
  25. package/dist/cli/main.js +51 -9
  26. package/dist/cli/protocol.js +18 -4
  27. package/dist/cli/server/config-server.js +280 -14
  28. package/dist/cli/server/ws-server.js +93 -44
  29. package/dist/cli/utils/output.js +29 -0
  30. package/dist/mcp/instructions.js +103 -10
  31. package/dist/mcp/resources/edit-rem-guide.js +3 -4
  32. package/dist/mcp/resources/error-reference.js +5 -3
  33. package/dist/mcp/resources/rem-object-fields.js +3 -3
  34. package/dist/mcp/tools/infra-tools.js +54 -6
  35. package/dist/mcp/tools/read-tools.js +16 -3
  36. package/package.json +2 -2
  37. package/remnote-plugin/dist/bridge_widget-sandbox.js +17 -17
  38. package/remnote-plugin/dist/bridge_widget.js +17 -17
  39. package/remnote-plugin/dist/index-sandbox.js +31 -31
  40. package/remnote-plugin/dist/index.js +31 -31
  41. package/remnote-plugin/dist/manifest.json +1 -1
  42. package/remnote-plugin/package.json +1 -1
  43. package/remnote-plugin/public/manifest.json +1 -1
  44. package/remnote-plugin/src/bridge/message-router.ts +1 -1
  45. package/remnote-plugin/src/bridge/multi-connection-manager.ts +151 -0
  46. package/remnote-plugin/src/bridge/websocket-client.ts +62 -16
  47. package/remnote-plugin/src/services/index.ts +0 -8
  48. package/remnote-plugin/src/services/read-context.ts +13 -4
  49. package/remnote-plugin/src/services/read-rem.ts +1 -9
  50. package/remnote-plugin/src/services/search.ts +13 -10
  51. package/remnote-plugin/src/settings.ts +9 -7
  52. package/remnote-plugin/src/utils/index.ts +0 -5
  53. package/remnote-plugin/src/widgets/bridge_widget.tsx +105 -20
  54. package/remnote-plugin/src/widgets/index.tsx +41 -44
  55. package/remnote-plugin/webpack.config.js +35 -0
  56. package/skills/remnote-bridge/SKILL.md +14 -9
  57. package/skills/remnote-bridge/instructions/addon.md +134 -0
  58. package/skills/remnote-bridge/instructions/clean.md +110 -0
  59. package/skills/remnote-bridge/instructions/connect.md +80 -37
  60. package/skills/remnote-bridge/instructions/disconnect.md +22 -9
  61. package/skills/remnote-bridge/instructions/edit-rem.md +37 -9
  62. package/skills/remnote-bridge/instructions/health.md +23 -13
  63. package/skills/remnote-bridge/instructions/install-skill.md +58 -0
  64. package/skills/remnote-bridge/instructions/overall.md +76 -21
  65. package/skills/remnote-bridge/instructions/read-context.md +34 -8
  66. package/skills/remnote-bridge/instructions/read-rem.md +10 -10
  67. package/skills/remnote-bridge/instructions/search.md +73 -14
  68. package/skills/remnote-bridge/instructions/setup.md +1 -1
@@ -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": [],
@@ -49,7 +49,7 @@ export function createMessageRouter(plugin: ReactRNPlugin): (request: BridgeRequ
49
49
  case 'read_globe':
50
50
  return readGlobe(plugin, request.payload as { depth?: number; maxNodes?: number; maxSiblings?: number });
51
51
  case 'read_context':
52
- return readContext(plugin, request.payload as { mode?: 'focus' | 'page'; ancestorLevels?: number; maxNodes?: number; maxSiblings?: number; depth?: number });
52
+ return readContext(plugin, request.payload as { mode?: 'focus' | 'page'; ancestorLevels?: number; maxNodes?: number; maxSiblings?: number; depth?: number; focusRemId?: string });
53
53
  case 'search':
54
54
  return search(plugin, request.payload as { query: string; numResults?: number });
55
55
 
@@ -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
  */
@@ -27,6 +27,7 @@ export interface ReadContextPayload {
27
27
  maxNodes?: number;
28
28
  maxSiblings?: number;
29
29
  depth?: number;
30
+ focusRemId?: string;
30
31
  }
31
32
 
32
33
  export interface ReadContextResult {
@@ -46,12 +47,14 @@ export async function readContext(
46
47
  maxNodes = 200,
47
48
  maxSiblings = 20,
48
49
  depth = 3,
50
+ focusRemId,
49
51
  } = payload;
50
52
 
51
53
  if (mode === 'page') {
54
+ if (focusRemId) throw new Error('focusRemId 仅在 focus 模式下有效,page 模式下请勿指定');
52
55
  return readContextPage(plugin, { maxNodes, maxSiblings, depth });
53
56
  }
54
- return readContextFocus(plugin, { ancestorLevels, maxNodes, maxSiblings });
57
+ return readContextFocus(plugin, { ancestorLevels, maxNodes, maxSiblings, focusRemId });
55
58
  }
56
59
 
57
60
  // ────────────────────────── Page 模式 ──────────────────────────
@@ -87,10 +90,16 @@ async function readContextPage(
87
90
 
88
91
  async function readContextFocus(
89
92
  plugin: ReactRNPlugin,
90
- opts: { ancestorLevels: number; maxNodes: number; maxSiblings: number },
93
+ opts: { ancestorLevels: number; maxNodes: number; maxSiblings: number; focusRemId?: string },
91
94
  ): Promise<ReadContextResult> {
92
- const focusRem = await plugin.focus.getFocusedRem();
93
- if (!focusRem) throw new Error('当前没有聚焦的 Rem,请先在 RemNote 中点击一个 Rem');
95
+ let focusRem: Rem | undefined;
96
+ if (opts.focusRemId) {
97
+ focusRem = await plugin.rem.findOne(opts.focusRemId);
98
+ if (!focusRem) throw new Error(`指定的 Rem 不存在: ${opts.focusRemId}`);
99
+ } else {
100
+ focusRem = await plugin.focus.getFocusedRem();
101
+ if (!focusRem) throw new Error('当前没有聚焦的 Rem,请先在 RemNote 中点击一个 Rem');
102
+ }
94
103
 
95
104
  const breadcrumb = await buildBreadcrumb(plugin, focusRem);
96
105
 
@@ -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
  */