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,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tree-serializer.ts — 树大纲序列化纯函数
|
|
3
|
+
*
|
|
4
|
+
* 职责:将 SerializableRem 数据拼接为带缩进和元数据注释的大纲行。
|
|
5
|
+
* 约束:纯函数、无副作用、不调用 SDK、不依赖其他层。
|
|
6
|
+
*
|
|
7
|
+
* SDK 调用(toMarkdown、getChildrenRem 等)在 services/read-tree.ts 中完成,
|
|
8
|
+
* 本文件只接收已转换好的字符串数据。
|
|
9
|
+
*
|
|
10
|
+
* ── 分隔符设计(v2)──
|
|
11
|
+
*
|
|
12
|
+
* 行内分隔符只表示 practiceDirection,不编码 type 信息。
|
|
13
|
+
* type(concept/descriptor)由元数据标记 `type:` 承载。
|
|
14
|
+
*
|
|
15
|
+
* | 箭头 | 含义 |
|
|
16
|
+
* |:-----|:-------------------------------|
|
|
17
|
+
* | → | 有 backText, forward |
|
|
18
|
+
* | ← | 有 backText, backward |
|
|
19
|
+
* | ↔ | 有 backText, both |
|
|
20
|
+
* | ↓ | multiline, forward(无 backText)|
|
|
21
|
+
* | ↑ | multiline, backward |
|
|
22
|
+
* | ↕ | multiline, both |
|
|
23
|
+
*
|
|
24
|
+
* ── 元数据标记 ──
|
|
25
|
+
*
|
|
26
|
+
* | 标记 | 含义 |
|
|
27
|
+
* |:----------------|:--------------------------------|
|
|
28
|
+
* | type:concept | Rem type = concept |
|
|
29
|
+
* | type:descriptor | Rem type = descriptor |
|
|
30
|
+
* | type:portal | Rem type = portal |
|
|
31
|
+
* | doc | isDocument = true |
|
|
32
|
+
* | role:card-item | isCardItem = true(多行答案行) |
|
|
33
|
+
* | children:N | 折叠时的隐藏子节点数 |
|
|
34
|
+
* | tag:Name(id) | 每个 tag 独立一个标记 |
|
|
35
|
+
* | top | 知识库顶级 Rem |
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// ────────────────────────── 接口 ──────────────────────────
|
|
39
|
+
|
|
40
|
+
/** Tag 信息 */
|
|
41
|
+
export interface TagInfo {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** services/read-tree.ts 准备好的单个 Rem 数据 */
|
|
47
|
+
export interface SerializableRem {
|
|
48
|
+
id: string;
|
|
49
|
+
markdownText: string;
|
|
50
|
+
markdownBackText: string | null;
|
|
51
|
+
/** 'concept' | 'descriptor' | 'default' | 'portal' */
|
|
52
|
+
type: string;
|
|
53
|
+
hasMultilineChildren: boolean;
|
|
54
|
+
/** 'forward' | 'backward' | 'both' | 'none' */
|
|
55
|
+
practiceDirection: string;
|
|
56
|
+
isCardItem: boolean;
|
|
57
|
+
isDocument: boolean;
|
|
58
|
+
isPortal: boolean;
|
|
59
|
+
/** Portal 直接引用的 Rem ID 列表(仅 Portal 类型有值) */
|
|
60
|
+
portalRefs: string[];
|
|
61
|
+
childrenCount: number;
|
|
62
|
+
tags: TagInfo[];
|
|
63
|
+
// Markdown 语法映射
|
|
64
|
+
fontSize: 'H1' | 'H2' | 'H3' | null;
|
|
65
|
+
isTodo: boolean;
|
|
66
|
+
todoStatus: 'Finished' | 'Unfinished' | null;
|
|
67
|
+
isCode: boolean;
|
|
68
|
+
isDivider: boolean;
|
|
69
|
+
/** 是否为知识库顶级 Rem(无父节点) */
|
|
70
|
+
isTopLevel?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 递归树节点,用于 buildOutline */
|
|
74
|
+
export interface OutlineNode {
|
|
75
|
+
rem: SerializableRem;
|
|
76
|
+
children: TreeNode[];
|
|
77
|
+
/** true = 子树超过 depth 限制被折叠 */
|
|
78
|
+
folded: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 省略占位节点 */
|
|
82
|
+
export interface ElidedNode {
|
|
83
|
+
type: 'elided';
|
|
84
|
+
/** 被省略的同级节点数 */
|
|
85
|
+
count: number;
|
|
86
|
+
/** 是否精确计数(false = ">=N",表示省略的节点可能还有后代) */
|
|
87
|
+
isExact: boolean;
|
|
88
|
+
parentId: string;
|
|
89
|
+
rangeFrom: number;
|
|
90
|
+
rangeTo: number;
|
|
91
|
+
totalSiblings: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 树节点统一类型 */
|
|
95
|
+
export type TreeNode = OutlineNode | ElidedNode;
|
|
96
|
+
|
|
97
|
+
/** 类型守卫 */
|
|
98
|
+
export function isElidedNode(node: TreeNode): node is ElidedNode {
|
|
99
|
+
return 'type' in node && node.type === 'elided';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ────────────────────────── 行内容拼接 ──────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 根据 practiceDirection 选择方向箭头,拼接行内容(不含缩进和元数据标记)。
|
|
106
|
+
*
|
|
107
|
+
* 箭头只表示 direction,不编码 type。type 由元数据标记承载。
|
|
108
|
+
*/
|
|
109
|
+
function buildLineContent(rem: SerializableRem): string {
|
|
110
|
+
// Divider(最高优先)
|
|
111
|
+
if (rem.isDivider) return '---';
|
|
112
|
+
|
|
113
|
+
const { markdownText, markdownBackText, hasMultilineChildren, practiceDirection } = rem;
|
|
114
|
+
|
|
115
|
+
let baseContent: string;
|
|
116
|
+
|
|
117
|
+
if (markdownBackText !== null) {
|
|
118
|
+
// 有 backText:按 direction 选择水平/垂直箭头
|
|
119
|
+
const arrow = hasMultilineChildren
|
|
120
|
+
? (practiceDirection === 'backward' ? ' ↑ ' : practiceDirection === 'both' ? ' ↕ ' : ' ↓ ')
|
|
121
|
+
: (practiceDirection === 'backward' ? ' ← ' : practiceDirection === 'both' ? ' ↔ ' : ' → ');
|
|
122
|
+
baseContent = markdownText + arrow + markdownBackText;
|
|
123
|
+
} else if (hasMultilineChildren) {
|
|
124
|
+
// 无 backText + multiline:尾部箭头
|
|
125
|
+
const arrow = practiceDirection === 'backward' ? ' ↑' : practiceDirection === 'both' ? ' ↕' : ' ↓';
|
|
126
|
+
baseContent = markdownText + arrow;
|
|
127
|
+
} else {
|
|
128
|
+
baseContent = markdownText;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Code 包裹(最内层)
|
|
132
|
+
if (rem.isCode) baseContent = '`' + baseContent + '`';
|
|
133
|
+
|
|
134
|
+
// Todo 前缀
|
|
135
|
+
if (rem.isTodo) {
|
|
136
|
+
const cb = rem.todoStatus === 'Finished' ? '- [x] ' : '- [ ] ';
|
|
137
|
+
baseContent = cb + baseContent;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Header 前缀(最外层)
|
|
141
|
+
if (rem.fontSize) {
|
|
142
|
+
const hd = rem.fontSize === 'H1' ? '# ' : rem.fontSize === 'H2' ? '## ' : '### ';
|
|
143
|
+
baseContent = hd + baseContent;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return baseContent;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ────────────────────────── 元数据 ──────────────────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 构建元数据标记列表。每个标记对应一个独立字段,不合并。
|
|
153
|
+
*/
|
|
154
|
+
function buildMetadata(rem: SerializableRem, folded: boolean): string[] {
|
|
155
|
+
const parts: string[] = [];
|
|
156
|
+
|
|
157
|
+
// type(default 不输出)
|
|
158
|
+
if (rem.type === 'concept') parts.push('type:concept');
|
|
159
|
+
else if (rem.type === 'descriptor') parts.push('type:descriptor');
|
|
160
|
+
else if (rem.type === 'portal') {
|
|
161
|
+
parts.push('type:portal');
|
|
162
|
+
if (rem.portalRefs.length > 0) parts.push('refs:' + rem.portalRefs.join(','));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// doc(独立于 type 的维度)
|
|
166
|
+
if (rem.isDocument) parts.push('doc');
|
|
167
|
+
|
|
168
|
+
// role
|
|
169
|
+
if (rem.isCardItem) parts.push('role:card-item');
|
|
170
|
+
|
|
171
|
+
// children(折叠时才输出)
|
|
172
|
+
if (folded && rem.childrenCount > 0) parts.push('children:' + rem.childrenCount);
|
|
173
|
+
|
|
174
|
+
// tags(每个 tag 独立一个标记)
|
|
175
|
+
for (const tag of rem.tags) {
|
|
176
|
+
parts.push('tag:' + tag.name + '(' + tag.id + ')');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 顶级标记
|
|
180
|
+
if (rem.isTopLevel) parts.push('top');
|
|
181
|
+
|
|
182
|
+
return parts;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ────────────────────────── 工厂函数 ──────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 创建一个带合理默认值的 SerializableRem。
|
|
189
|
+
*
|
|
190
|
+
* 用于 read-globe、read-context 等场景中只需基础字段的节点。
|
|
191
|
+
* 必须提供 id、markdownText、childrenCount,其余字段使用默认值。
|
|
192
|
+
*/
|
|
193
|
+
export function createMinimalSerializableRem(
|
|
194
|
+
overrides: Partial<SerializableRem> & Pick<SerializableRem, 'id' | 'markdownText' | 'childrenCount'>,
|
|
195
|
+
): SerializableRem {
|
|
196
|
+
return {
|
|
197
|
+
markdownBackText: null,
|
|
198
|
+
type: 'default',
|
|
199
|
+
hasMultilineChildren: false,
|
|
200
|
+
practiceDirection: 'none',
|
|
201
|
+
isCardItem: false,
|
|
202
|
+
isDocument: false,
|
|
203
|
+
isPortal: false,
|
|
204
|
+
portalRefs: [],
|
|
205
|
+
tags: [],
|
|
206
|
+
fontSize: null,
|
|
207
|
+
isTodo: false,
|
|
208
|
+
todoStatus: null,
|
|
209
|
+
isCode: false,
|
|
210
|
+
isDivider: false,
|
|
211
|
+
...overrides,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ────────────────────────── 公开 API ──────────────────────────
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 序列化单个 Rem 为一行(不含缩进)。
|
|
219
|
+
*
|
|
220
|
+
* 格式:`{行内容} <!--{remId} {元数据}-->`
|
|
221
|
+
*/
|
|
222
|
+
export function serializeRemLine(rem: SerializableRem, folded: boolean = false): string {
|
|
223
|
+
const content = buildLineContent(rem);
|
|
224
|
+
const metadata = buildMetadata(rem, folded);
|
|
225
|
+
const metaStr = metadata.length > 0 ? ' ' + metadata.join(' ') : '';
|
|
226
|
+
return `${content} <!--${rem.id}${metaStr}-->`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 序列化省略占位符行(不含缩进)。
|
|
231
|
+
*
|
|
232
|
+
* 精确:`<!--...elided {N} siblings (parent:{id} range:{from}-{to} total:{total})-->`
|
|
233
|
+
* 非精确:`<!--...elided >={N} nodes (parent:{id} range:{from}-{to} total:{total})-->`
|
|
234
|
+
*/
|
|
235
|
+
export function serializeElidedLine(node: ElidedNode): string {
|
|
236
|
+
const label = node.isExact ? 'siblings' : 'nodes';
|
|
237
|
+
const prefix = node.isExact ? '' : '>=';
|
|
238
|
+
return `<!--...elided ${prefix}${node.count} ${label} (parent:${node.parentId} range:${node.rangeFrom}-${node.rangeTo} total:${node.totalSiblings})-->`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 将递归树结构序列化为完整的 Markdown 大纲文本。
|
|
243
|
+
*
|
|
244
|
+
* 每级缩进 2 个空格。
|
|
245
|
+
*/
|
|
246
|
+
export function buildOutline(root: OutlineNode): string {
|
|
247
|
+
const lines: string[] = [];
|
|
248
|
+
|
|
249
|
+
function walkNode(node: TreeNode, depth: number): void {
|
|
250
|
+
const indent = ' '.repeat(depth);
|
|
251
|
+
|
|
252
|
+
if (isElidedNode(node)) {
|
|
253
|
+
lines.push(indent + serializeElidedLine(node));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const line = serializeRemLine(node.rem, node.folded);
|
|
258
|
+
lines.push(indent + line);
|
|
259
|
+
|
|
260
|
+
if (!node.folded) {
|
|
261
|
+
for (const child of node.children) {
|
|
262
|
+
walkNode(child, depth + 1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
walkNode(root, 0);
|
|
268
|
+
return lines.join('\n');
|
|
269
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge Widget — 显示守护进程连接状态(纯展示)
|
|
3
|
+
*
|
|
4
|
+
* WebSocket 连接在 index.tsx 的 onActivate 中建立,
|
|
5
|
+
* 此 widget 每秒从 plugin.storage 读取并显示状态和日志。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { renderWidget, usePlugin } from '@remnote/plugin-sdk';
|
|
9
|
+
import React, { useEffect, useState } from 'react';
|
|
10
|
+
import type { ConnectionStatus } from '../bridge/websocket-client';
|
|
11
|
+
|
|
12
|
+
interface StoredLog {
|
|
13
|
+
time: number;
|
|
14
|
+
message: string;
|
|
15
|
+
level: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function BridgeWidget() {
|
|
19
|
+
const plugin = usePlugin();
|
|
20
|
+
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
|
|
21
|
+
const [logs, setLogs] = useState<StoredLog[]>([]);
|
|
22
|
+
|
|
23
|
+
// 每秒轮询 plugin.storage 获取最新状态
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let active = true;
|
|
26
|
+
|
|
27
|
+
async function poll() {
|
|
28
|
+
if (!active) return;
|
|
29
|
+
const s = await plugin.storage.getSession('bridge-status');
|
|
30
|
+
const l = await plugin.storage.getSession('bridge-logs');
|
|
31
|
+
if (active) {
|
|
32
|
+
setStatus((s as ConnectionStatus) ?? 'disconnected');
|
|
33
|
+
setLogs((l as StoredLog[]) ?? []);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
poll();
|
|
38
|
+
const timer = setInterval(poll, 1000);
|
|
39
|
+
return () => {
|
|
40
|
+
active = false;
|
|
41
|
+
clearInterval(timer);
|
|
42
|
+
};
|
|
43
|
+
}, [plugin]);
|
|
44
|
+
|
|
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];
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div style={{ padding: '12px', fontFamily: 'system-ui, sans-serif', fontSize: '13px' }}>
|
|
55
|
+
{/* 头部 */}
|
|
56
|
+
<div
|
|
57
|
+
style={{
|
|
58
|
+
display: 'flex',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
justifyContent: 'space-between',
|
|
61
|
+
marginBottom: '12px',
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<h3 style={{ margin: 0, fontSize: '14px', fontWeight: 600 }}>RemNote Bridge</h3>
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
display: 'flex',
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
gap: '6px',
|
|
70
|
+
padding: '4px 8px',
|
|
71
|
+
borderRadius: '12px',
|
|
72
|
+
backgroundColor: currentStatus.bg,
|
|
73
|
+
color: currentStatus.color,
|
|
74
|
+
fontSize: '12px',
|
|
75
|
+
fontWeight: 500,
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<span>{currentStatus.icon}</span>
|
|
79
|
+
<span>{currentStatus.text}</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* 日志 */}
|
|
84
|
+
<div
|
|
85
|
+
style={{
|
|
86
|
+
border: '1px solid #e5e7eb',
|
|
87
|
+
borderRadius: '6px',
|
|
88
|
+
backgroundColor: '#f9fafb',
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
fontSize: '11px',
|
|
94
|
+
fontWeight: 600,
|
|
95
|
+
padding: '8px 10px',
|
|
96
|
+
borderBottom: '1px solid #e5e7eb',
|
|
97
|
+
color: '#6b7280',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
日志
|
|
101
|
+
</div>
|
|
102
|
+
<div style={{ maxHeight: '150px', overflowY: 'auto' }}>
|
|
103
|
+
{logs.length === 0 ? (
|
|
104
|
+
<div style={{ padding: '12px', color: '#9ca3af', textAlign: 'center' }}>暂无日志</div>
|
|
105
|
+
) : (
|
|
106
|
+
logs
|
|
107
|
+
.slice()
|
|
108
|
+
.reverse()
|
|
109
|
+
.map((log, index) => (
|
|
110
|
+
<div
|
|
111
|
+
key={index}
|
|
112
|
+
style={{
|
|
113
|
+
padding: '6px 10px',
|
|
114
|
+
borderBottom: index < logs.length - 1 ? '1px solid #e5e7eb' : 'none',
|
|
115
|
+
fontSize: '11px',
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<span style={{ color: '#9ca3af' }}>
|
|
119
|
+
{new Date(log.time).toLocaleTimeString([], {
|
|
120
|
+
hour: '2-digit',
|
|
121
|
+
minute: '2-digit',
|
|
122
|
+
second: '2-digit',
|
|
123
|
+
})}
|
|
124
|
+
</span>
|
|
125
|
+
<span
|
|
126
|
+
style={{
|
|
127
|
+
marginLeft: '8px',
|
|
128
|
+
color:
|
|
129
|
+
log.level === 'error'
|
|
130
|
+
? '#ef4444'
|
|
131
|
+
: log.level === 'warn'
|
|
132
|
+
? '#f59e0b'
|
|
133
|
+
: '#374151',
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{log.message}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
))
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* 配置按钮 */}
|
|
145
|
+
<button
|
|
146
|
+
onClick={() => window.open('http://127.0.0.1:3003', '_blank')}
|
|
147
|
+
style={{
|
|
148
|
+
display: 'flex',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
justifyContent: 'center',
|
|
151
|
+
gap: '6px',
|
|
152
|
+
width: '100%',
|
|
153
|
+
marginTop: '12px',
|
|
154
|
+
padding: '8px 0',
|
|
155
|
+
background: '#f9fafb',
|
|
156
|
+
border: '1px solid #e5e7eb',
|
|
157
|
+
borderRadius: '6px',
|
|
158
|
+
cursor: 'pointer',
|
|
159
|
+
fontSize: '13px',
|
|
160
|
+
color: '#374151',
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<span style={{ fontSize: '16px' }}>⚙</span>
|
|
164
|
+
<span>打开配置页面</span>
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
renderWidget(BridgeWidget);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { declareIndexPlugin, type ReactRNPlugin, WidgetLocation } from '@remnote/plugin-sdk';
|
|
2
|
+
import '../style.css';
|
|
3
|
+
import '../index.css';
|
|
4
|
+
import { SETTING_WS_URL, DEFAULT_WS_URL, DEFAULT_PLUGIN_VERSION } from '../settings';
|
|
5
|
+
import { WebSocketClient } from '../bridge/websocket-client';
|
|
6
|
+
import { createMessageRouter } from '../bridge/message-router';
|
|
7
|
+
// test-scripts 已完成数据收集,不再打包进生产代码
|
|
8
|
+
// 如需重跑,取消注释对应 import 和 onActivate 调用
|
|
9
|
+
// import { runActionTests } from '../test-scripts/test-actions';
|
|
10
|
+
// import { runRichTextBuilderTest } from '../test-scripts/test-richtext-builder';
|
|
11
|
+
// import { runRichTextRemainingTest } from '../test-scripts/test-richtext-remaining';
|
|
12
|
+
// import { runRichTextMatrixTest } from '../test-scripts/test-richtext-matrix';
|
|
13
|
+
// import { runPowerupRenderingTest } from '../test-scripts/test-powerup-rendering';
|
|
14
|
+
|
|
15
|
+
let wsClient: WebSocketClient | null = null;
|
|
16
|
+
// 本地日志缓冲区:避免 onLog 并发读写 plugin.storage 的竞态
|
|
17
|
+
const logBuffer: Array<{ time: number; message: string; level: string }> = [];
|
|
18
|
+
let logFlushPending = false;
|
|
19
|
+
|
|
20
|
+
async function onActivate(plugin: ReactRNPlugin) {
|
|
21
|
+
// 注册 WS Server URL 设置
|
|
22
|
+
await plugin.settings.registerStringSetting({
|
|
23
|
+
id: SETTING_WS_URL,
|
|
24
|
+
title: 'Bridge WS Server URL',
|
|
25
|
+
description: '守护进程 WebSocket Server 地址(默认 ws://127.0.0.1:3002)',
|
|
26
|
+
defaultValue: DEFAULT_WS_URL,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// 注册 Bridge Widget(右侧边栏)
|
|
30
|
+
await plugin.app.registerWidget('bridge_widget', WidgetLocation.RightSidebar, {
|
|
31
|
+
dimensions: { height: 'auto', width: '100%' },
|
|
32
|
+
widgetTabIcon: 'https://cdn.jsdelivr.net/npm/lucide-static@0.460.0/icons/globe.svg',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 读取 WS URL 设置
|
|
36
|
+
const wsUrl =
|
|
37
|
+
(await plugin.settings.getSetting<string>(SETTING_WS_URL)) || DEFAULT_WS_URL;
|
|
38
|
+
|
|
39
|
+
// 在 onActivate 中建立 WebSocket 连接(插件激活即连接,无需打开 widget)
|
|
40
|
+
wsClient = new WebSocketClient({
|
|
41
|
+
url: wsUrl,
|
|
42
|
+
pluginVersion: DEFAULT_PLUGIN_VERSION,
|
|
43
|
+
sdkReady: true,
|
|
44
|
+
maxReconnectAttempts: 10,
|
|
45
|
+
initialReconnectDelay: 1000,
|
|
46
|
+
maxReconnectDelay: 30000,
|
|
47
|
+
onStatusChange: (status) => {
|
|
48
|
+
void plugin.storage.setSession('bridge-status', status);
|
|
49
|
+
},
|
|
50
|
+
onLog: (message, level) => {
|
|
51
|
+
logBuffer.push({ time: Date.now(), message, level });
|
|
52
|
+
if (logBuffer.length > 30) logBuffer.splice(0, logBuffer.length - 30);
|
|
53
|
+
// 合并写入:避免并发 read-modify-write 竞态
|
|
54
|
+
if (!logFlushPending) {
|
|
55
|
+
logFlushPending = true;
|
|
56
|
+
queueMicrotask(async () => {
|
|
57
|
+
logFlushPending = false;
|
|
58
|
+
await plugin.storage.setSession('bridge-logs', logBuffer.slice());
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 路由守护进程转发的请求到 services 层
|
|
65
|
+
wsClient.setMessageHandler(createMessageRouter(plugin));
|
|
66
|
+
|
|
67
|
+
wsClient.connect();
|
|
68
|
+
|
|
69
|
+
// test-scripts 已完成数据收集,不再自动运行
|
|
70
|
+
// runActionTests(plugin).catch((err) => console.error('[ACTION-TEST] 顶层错误:', err));
|
|
71
|
+
// runRichTextBuilderTest(plugin).catch((err) => console.error('[RICHTEXT-BUILDER-TEST] 顶层错误:', err));
|
|
72
|
+
// runRichTextRemainingTest(plugin).catch((err) => console.error('[RICHTEXT-REMAINING-TEST] 顶层错误:', err));
|
|
73
|
+
// runRichTextMatrixTest(plugin).catch((err) => console.error('[RICHTEXT-MATRIX-TEST] 顶层错误:', err));
|
|
74
|
+
// runPowerupRenderingTest(plugin).catch((err) => console.error('[POWERUP-RENDER] 顶层错误:', err));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function onDeactivate(_: ReactRNPlugin) {
|
|
78
|
+
wsClient?.disconnect();
|
|
79
|
+
wsClient = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
declareIndexPlugin(onActivate, onDeactivate);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"strictNullChecks": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"esModuleInterop": true
|
|
14
|
+
},
|
|
15
|
+
"ts-node": {
|
|
16
|
+
"compilerOptions": {
|
|
17
|
+
"module": "CommonJS"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["src"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const { resolve } = require('path');
|
|
2
|
+
var glob = require('glob');
|
|
3
|
+
var path = require('path');
|
|
4
|
+
|
|
5
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
6
|
+
const { EsbuildPlugin } = require('esbuild-loader');
|
|
7
|
+
const { ProvidePlugin, BannerPlugin } = require('webpack');
|
|
8
|
+
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
9
|
+
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
|
10
|
+
|
|
11
|
+
const CopyPlugin = require('copy-webpack-plugin');
|
|
12
|
+
|
|
13
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
14
|
+
const isDevelopment = !isProd;
|
|
15
|
+
|
|
16
|
+
const fastRefresh = isDevelopment ? new ReactRefreshWebpackPlugin() : null;
|
|
17
|
+
|
|
18
|
+
const SANDBOX_SUFFIX = '-sandbox';
|
|
19
|
+
|
|
20
|
+
const config = {
|
|
21
|
+
mode: isProd ? 'production' : 'development',
|
|
22
|
+
entry: glob.sync('./src/widgets/**/*.tsx').reduce((obj, el) => {
|
|
23
|
+
const rel = path
|
|
24
|
+
.relative('src/widgets', el)
|
|
25
|
+
.replace(/\.[tj]sx?$/, '')
|
|
26
|
+
.replace(/\\/g, '/');
|
|
27
|
+
|
|
28
|
+
obj[rel] = el;
|
|
29
|
+
obj[`${rel}${SANDBOX_SUFFIX}`] = el;
|
|
30
|
+
return obj;
|
|
31
|
+
}, {}),
|
|
32
|
+
|
|
33
|
+
output: {
|
|
34
|
+
path: resolve(__dirname, 'dist'),
|
|
35
|
+
filename: `[name].js`,
|
|
36
|
+
publicPath: '',
|
|
37
|
+
},
|
|
38
|
+
resolve: {
|
|
39
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
|
40
|
+
},
|
|
41
|
+
module: {
|
|
42
|
+
rules: [
|
|
43
|
+
{
|
|
44
|
+
test: /\.(ts|tsx|jsx|js)?$/,
|
|
45
|
+
loader: 'esbuild-loader',
|
|
46
|
+
options: {
|
|
47
|
+
loader: 'tsx',
|
|
48
|
+
target: 'es2020',
|
|
49
|
+
minify: false,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
test: /\.css$/i,
|
|
54
|
+
use: [
|
|
55
|
+
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
|
|
56
|
+
{ loader: 'css-loader', options: { url: false } },
|
|
57
|
+
'postcss-loader',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
plugins: [
|
|
63
|
+
isDevelopment
|
|
64
|
+
? undefined
|
|
65
|
+
: new MiniCssExtractPlugin({
|
|
66
|
+
filename: '[name].css',
|
|
67
|
+
}),
|
|
68
|
+
new HtmlWebpackPlugin({
|
|
69
|
+
templateContent: `
|
|
70
|
+
<body></body>
|
|
71
|
+
<script type="text/javascript">
|
|
72
|
+
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
73
|
+
const queryParams = Object.fromEntries(urlSearchParams.entries());
|
|
74
|
+
const widgetName = queryParams["widgetName"];
|
|
75
|
+
if (widgetName == undefined) {document.body.innerHTML+="Widget ID not specified."}
|
|
76
|
+
|
|
77
|
+
const s = document.createElement('script');
|
|
78
|
+
s.type = "module";
|
|
79
|
+
s.src = widgetName+"${SANDBOX_SUFFIX}.js";
|
|
80
|
+
document.body.appendChild(s);
|
|
81
|
+
</script>
|
|
82
|
+
`,
|
|
83
|
+
filename: 'index.html',
|
|
84
|
+
inject: false,
|
|
85
|
+
}),
|
|
86
|
+
new ProvidePlugin({
|
|
87
|
+
React: 'react',
|
|
88
|
+
reactDOM: 'react-dom',
|
|
89
|
+
}),
|
|
90
|
+
new BannerPlugin({
|
|
91
|
+
banner: (file) => {
|
|
92
|
+
return !file.chunk.name.includes(SANDBOX_SUFFIX) ? 'const IMPORT_META=import.meta;' : '';
|
|
93
|
+
},
|
|
94
|
+
raw: true,
|
|
95
|
+
}),
|
|
96
|
+
new CopyPlugin({
|
|
97
|
+
patterns: [
|
|
98
|
+
{ from: 'public', to: '' },
|
|
99
|
+
],
|
|
100
|
+
}),
|
|
101
|
+
fastRefresh,
|
|
102
|
+
].filter(Boolean),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (isProd) {
|
|
106
|
+
config.optimization = {
|
|
107
|
+
minimize: isProd,
|
|
108
|
+
minimizer: [new EsbuildPlugin()],
|
|
109
|
+
};
|
|
110
|
+
} else {
|
|
111
|
+
// for more information, see https://webpack.js.org/configuration/dev-server
|
|
112
|
+
config.devServer = {
|
|
113
|
+
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
|
|
114
|
+
open: true,
|
|
115
|
+
hot: true,
|
|
116
|
+
compress: true,
|
|
117
|
+
watchFiles: ['src/*'],
|
|
118
|
+
headers: {
|
|
119
|
+
'Access-Control-Allow-Origin': '*',
|
|
120
|
+
'Access-Control-Allow-Headers': 'baggage, sentry-trace',
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = config;
|