remnote-bridge 0.1.8 → 0.1.10
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.js +3 -1
- package/dist/cli/commands/disconnect.js +5 -0
- package/dist/cli/daemon/headless-browser.js +88 -0
- package/dist/cli/daemon/static-server.js +4 -1
- package/dist/mcp/instructions.js +2 -0
- package/package.json +1 -1
- package/remnote-plugin/dist/index-sandbox.js +26 -26
- package/remnote-plugin/dist/index.js +26 -26
- package/remnote-plugin/src/services/breadcrumb.ts +2 -1
- package/remnote-plugin/src/services/read-context.ts +2 -2
- package/remnote-plugin/src/services/read-globe.ts +2 -1
- package/remnote-plugin/src/services/read-tree.ts +2 -2
- package/remnote-plugin/src/services/rem-builder.ts +53 -3
- package/remnote-plugin/src/services/search.ts +2 -1
- package/skills/remnote-bridge/SKILL.md +2 -0
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
|
|
8
|
+
import { safeToMarkdown } from './rem-builder';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* 从 rem 向上追溯到根,返回路径名称数组(从根到当前)。
|
|
@@ -17,7 +18,7 @@ export async function buildBreadcrumb(
|
|
|
17
18
|
let current: Rem | undefined = rem;
|
|
18
19
|
|
|
19
20
|
while (current) {
|
|
20
|
-
const text = await plugin
|
|
21
|
+
const text = await safeToMarkdown(plugin, current.text ?? []);
|
|
21
22
|
path.unshift(text.replace(/\n/g, ' ').trim() || current._id);
|
|
22
23
|
current = await current.getParentRem();
|
|
23
24
|
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { filterNoisyChildren } from './powerup-filter';
|
|
20
20
|
import { sliceSiblings } from '../utils/elision';
|
|
21
21
|
import { buildBreadcrumb } from './breadcrumb';
|
|
22
|
-
import { buildFullSerializableRem as buildFullRem } from './rem-builder';
|
|
22
|
+
import { buildFullSerializableRem as buildFullRem, safeToMarkdown } from './rem-builder';
|
|
23
23
|
|
|
24
24
|
export interface ReadContextPayload {
|
|
25
25
|
mode?: 'focus' | 'page';
|
|
@@ -350,7 +350,7 @@ async function buildMinimalSerializableRem(
|
|
|
350
350
|
) {
|
|
351
351
|
const isPortal = rem.type === 6;
|
|
352
352
|
const [markdownText, isDocument, portalIncludedRems] = await Promise.all([
|
|
353
|
-
plugin
|
|
353
|
+
safeToMarkdown(plugin, rem.text ?? []),
|
|
354
354
|
rem.isDocument(),
|
|
355
355
|
isPortal ? rem.getPortalDirectlyIncludedRem() : Promise.resolve([]),
|
|
356
356
|
]);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '../utils/tree-serializer';
|
|
22
22
|
import { sliceSiblings } from '../utils/elision';
|
|
23
23
|
import { filterNoisyChildren } from './powerup-filter';
|
|
24
|
+
import { safeToMarkdown } from './rem-builder';
|
|
24
25
|
|
|
25
26
|
export interface ReadGlobePayload {
|
|
26
27
|
depth?: number;
|
|
@@ -75,7 +76,7 @@ export async function readGlobe(
|
|
|
75
76
|
|
|
76
77
|
const isPortal = rem.type === 6;
|
|
77
78
|
const [markdownText, portalIncludedRems] = await Promise.all([
|
|
78
|
-
plugin
|
|
79
|
+
safeToMarkdown(plugin, rem.text ?? []),
|
|
79
80
|
isPortal ? rem.getPortalDirectlyIncludedRem() : Promise.resolve([]),
|
|
80
81
|
]);
|
|
81
82
|
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from '../utils/tree-serializer';
|
|
19
19
|
import { filterNoisyChildren } from './powerup-filter';
|
|
20
20
|
import { sliceSiblings } from '../utils/elision';
|
|
21
|
-
import { buildFullSerializableRem, sanitizeNewlines } from './rem-builder';
|
|
21
|
+
import { buildFullSerializableRem, sanitizeNewlines, safeToMarkdown } from './rem-builder';
|
|
22
22
|
|
|
23
23
|
export interface ReadTreePayload {
|
|
24
24
|
remId: string;
|
|
@@ -190,7 +190,7 @@ export async function readTree(
|
|
|
190
190
|
const parent = await current.getParentRem();
|
|
191
191
|
if (!parent) break;
|
|
192
192
|
const [name, children, isDoc] = await Promise.all([
|
|
193
|
-
plugin
|
|
193
|
+
safeToMarkdown(plugin, parent.text ?? []),
|
|
194
194
|
parent.getChildrenRem(),
|
|
195
195
|
parent.isDocument(),
|
|
196
196
|
]);
|
|
@@ -11,6 +11,56 @@ import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
|
|
|
11
11
|
import type { SerializableRem } from '../utils/tree-serializer';
|
|
12
12
|
import { filterNoisyTags } from './powerup-filter';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* SDK richText.toMarkdown 的安全包装。
|
|
16
|
+
* SDK 不认识某些 RichText 类型(如 "i":"u" URL 链接),会抛 Invalid input。
|
|
17
|
+
* 失败时回退到本地解析。
|
|
18
|
+
*/
|
|
19
|
+
export async function safeToMarkdown(
|
|
20
|
+
plugin: ReactRNPlugin,
|
|
21
|
+
richText: unknown[],
|
|
22
|
+
): Promise<string> {
|
|
23
|
+
try {
|
|
24
|
+
return await plugin.richText.toMarkdown(richText);
|
|
25
|
+
} catch {
|
|
26
|
+
return richTextFallback(richText);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 本地 RichText → 纯文本回退,处理 SDK 不支持的类型。
|
|
32
|
+
*
|
|
33
|
+
* 覆盖 RICH_TEXT_ELEMENT_TYPE 枚举全部 12 种类型 + 遗留 "u" 类型:
|
|
34
|
+
* m=TEXT, q=REM, i=IMAGE, a=AUDIO, x=LATEX, p=PLUGIN,
|
|
35
|
+
* g=GLOBAL_NAME, s=CARD_DELIMITER, n=ANNOTATION,
|
|
36
|
+
* fi=FLASHCARD_ICON, ai=ADD_ICON, o=DEPRECATED_CODE, u=URL(遗留)
|
|
37
|
+
*/
|
|
38
|
+
function richTextFallback(richText: unknown[]): string {
|
|
39
|
+
return richText.map(item => {
|
|
40
|
+
if (typeof item === 'string') return item;
|
|
41
|
+
if (typeof item !== 'object' || item === null) return '';
|
|
42
|
+
const obj = item as Record<string, unknown>;
|
|
43
|
+
switch (obj.i) {
|
|
44
|
+
case 'm': return String(obj.text ?? '');
|
|
45
|
+
case 'q': return `[[${String(obj._id ?? '')}]]`;
|
|
46
|
+
case 'u': return obj.title
|
|
47
|
+
? `[${String(obj.title)}](${String(obj.url)})`
|
|
48
|
+
: String(obj.url ?? '');
|
|
49
|
+
case 'x': return `$${String(obj.text ?? '')}$`;
|
|
50
|
+
case 'i': return `})`;
|
|
51
|
+
case 'a': return `[audio](${String(obj.url ?? '')})`;
|
|
52
|
+
case 'p': return String(obj.text ?? ''); // PLUGIN
|
|
53
|
+
case 'g': return String(obj.text ?? ''); // GLOBAL_NAME
|
|
54
|
+
case 'n': return String(obj.text ?? ''); // ANNOTATION
|
|
55
|
+
case 'o': return String(obj.text ?? ''); // DEPRECATED_CODE
|
|
56
|
+
case 's': // CARD_DELIMITER
|
|
57
|
+
case 'fi': // FLASHCARD_ICON
|
|
58
|
+
case 'ai': return ''; // ADD_ICON(纯视觉标记)
|
|
59
|
+
default: return String(obj.text ?? obj.url ?? '');
|
|
60
|
+
}
|
|
61
|
+
}).join('');
|
|
62
|
+
}
|
|
63
|
+
|
|
14
64
|
export interface BuildFullRemOptions {
|
|
15
65
|
/** 是否保留 Powerup 系统 Tag(默认 false = 过滤掉) */
|
|
16
66
|
includePowerup?: boolean;
|
|
@@ -46,8 +96,8 @@ export async function buildFullSerializableRem(
|
|
|
46
96
|
hasDvPowerup,
|
|
47
97
|
portalIncludedRems,
|
|
48
98
|
] = await Promise.all([
|
|
49
|
-
plugin
|
|
50
|
-
rem.backText ? plugin
|
|
99
|
+
safeToMarkdown(plugin, rem.text ?? []),
|
|
100
|
+
rem.backText ? safeToMarkdown(plugin, rem.backText) : Promise.resolve(null),
|
|
51
101
|
rem.getType(),
|
|
52
102
|
rem.isCardItem(),
|
|
53
103
|
rem.isDocument(),
|
|
@@ -80,7 +130,7 @@ export async function buildFullSerializableRem(
|
|
|
80
130
|
const tags = await Promise.all(
|
|
81
131
|
tagRems.map(async (t) => ({
|
|
82
132
|
id: t._id,
|
|
83
|
-
name: sanitizeNewlines(await plugin
|
|
133
|
+
name: sanitizeNewlines(await safeToMarkdown(plugin, t.text ?? [])),
|
|
84
134
|
})),
|
|
85
135
|
);
|
|
86
136
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ReactRNPlugin } from '@remnote/plugin-sdk';
|
|
10
|
+
import { safeToMarkdown } from './rem-builder';
|
|
10
11
|
|
|
11
12
|
export interface SearchPayload {
|
|
12
13
|
query: string;
|
|
@@ -39,7 +40,7 @@ export async function search(
|
|
|
39
40
|
|
|
40
41
|
const results: SearchResultItem[] = [];
|
|
41
42
|
for (const rem of rems) {
|
|
42
|
-
const markdownText = await plugin
|
|
43
|
+
const markdownText = await safeToMarkdown(plugin, rem.text ?? []);
|
|
43
44
|
const isDocument = await rem.isDocument();
|
|
44
45
|
results.push({
|
|
45
46
|
remId: rem._id,
|
|
@@ -267,6 +267,8 @@ Rem 的属性(文本、类型、格式、标签) → edit-rem (前置
|
|
|
267
267
|
|
|
268
268
|
标准模式每次 connect 后都需要用户手动操作 RemNote。Headless 模式通过 setup(一次性)+ headless Chrome 实现自动连接,后续 connect 无需用户介入。
|
|
269
269
|
|
|
270
|
+
**⚠️ 模式选择建议**:日常使用推荐**标准模式**。Headless 模式下 Chrome 在后台运行,**无法感知用户正在 RemNote 中浏览和操作的界面**(`read-context` 返回的是 headless Chrome 的上下文,而非用户的浏览器)。只有在全自动化场景(CI/CD、定时任务、批量操作等无需与用户界面交互的场景)才建议使用 Headless 模式。
|
|
271
|
+
|
|
270
272
|
#### 首次使用(setup)
|
|
271
273
|
|
|
272
274
|
setup 会弹出 Chrome 窗口,用户需要完成两件事:
|