remnote-bridge 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -35,6 +35,8 @@ Add the following to your AI client's MCP settings:
35
35
  }
36
36
  ```
37
37
 
38
+ **Tip: Install both for best results.** MCP documentation is kept concise (loaded all at once), while Skill documentation is detailed (loaded on demand). Together they complement each other. That said, either one works independently — you don't need both.
39
+
38
40
  Once connected, the AI will guide you through connecting to RemNote, loading the plugin, and everything else.
39
41
 
40
42
  ---
package/dist/cli/main.js CHANGED
@@ -53,7 +53,7 @@ function parseJsonInput(command, jsonStr, requiredFields = []) {
53
53
  program
54
54
  .name('remnote-bridge')
55
55
  .description('RemNote Bridge — CLI + MCP Server + Plugin')
56
- .version('0.1.2')
56
+ .version('0.1.3')
57
57
  .option('--json', '以 JSON 格式输出(适用于程序化调用)');
58
58
  program
59
59
  .command('connect')
package/dist/mcp/index.js CHANGED
@@ -11,13 +11,14 @@ import { registerEditTools } from './tools/edit-tools.js';
11
11
  import { registerInfraTools } from './tools/infra-tools.js';
12
12
  import { OUTLINE_FORMAT_CONTENT } from './resources/outline-format.js';
13
13
  import { REM_OBJECT_FIELDS_CONTENT } from './resources/rem-object-fields.js';
14
+ import { EDIT_REM_GUIDE_CONTENT } from './resources/edit-rem-guide.js';
14
15
  import { EDIT_TREE_GUIDE_CONTENT } from './resources/edit-tree-guide.js';
15
16
  import { ERROR_REFERENCE_CONTENT } from './resources/error-reference.js';
16
17
  import { SEPARATOR_FLASHCARD_CONTENT } from './resources/separator-flashcard.js';
17
18
  export async function startMcpServer() {
18
19
  const server = new FastMCP({
19
20
  name: 'remnote-bridge',
20
- version: '0.1.2',
21
+ version: '0.1.3',
21
22
  instructions: SERVER_INSTRUCTIONS,
22
23
  });
23
24
  registerInfraTools(server);
@@ -40,6 +41,14 @@ export async function startMcpServer() {
40
41
  return { text: REM_OBJECT_FIELDS_CONTENT };
41
42
  },
42
43
  });
44
+ server.addResource({
45
+ uri: 'remnote://edit-rem-guide',
46
+ name: 'edit_rem 操作指南',
47
+ mimeType: 'text/markdown',
48
+ async load() {
49
+ return { text: EDIT_REM_GUIDE_CONTENT };
50
+ },
51
+ });
43
52
  server.addResource({
44
53
  uri: 'remnote://edit-tree-guide',
45
54
  name: 'edit_tree 操作指南',
@@ -25,7 +25,7 @@ Rem 有两个独立维度:**type**(闪卡语义)和 **isDocument**(页
25
25
  | type | 含义 | UI 表现 |
26
26
  |:-----|:-----|:--------|
27
27
  | \\\`concept\\\` | 概念定义 | 文字**加粗** |
28
- | \\\`descriptor\\\` | 描述/属性 | 文字*斜体* |
28
+ | \\\`descriptor\\\` | 描述/属性 | 正常字重(与 default 无视觉差异) |
29
29
  | \\\`default\\\` | 普通 Rem | 正常字重 |
30
30
  | \\\`portal\\\` | 嵌入引用容器 | 紫色边框(**只读**,不可设置) |
31
31
 
@@ -150,7 +150,35 @@ disconnect → 关闭 daemon,清空所有缓存
150
150
  2. 在返回的 JSON 文本中定位要修改的部分
151
151
  3. \\\`edit_rem\\\` 用 str_replace 替换:oldStr 精确匹配原文,newStr 是修改后的文本
152
152
 
153
- str_replace 操作的是格式化 JSON 文本(2 空格缩进)。oldStr 要包含足够上下文(如字段名 + 值),避免模糊匹配。替换后必须是合法 JSON。
153
+ str_replace 操作的是 \\\`JSON.stringify(remObject, null, 2)\\\` 格式化文本。oldStr 要包含足够上下文(如字段名 + 值),避免模糊匹配。替换后必须是合法 JSON。
154
+
155
+ **RichText 编辑要点**(\\\`text\\\` 和 \\\`backText\\\` 字段):
156
+
157
+ RichText 在格式化 JSON 中是多行结构,对象内 key 按**字母序**排列(\\\`_id\\\` 排最前,因为 \\\`_\\\` < \\\`a\\\`)。
158
+
159
+ 示例——将纯文本改为粗体:
160
+
161
+ \\\`\\\`\\\`
162
+ oldStr: "text": [\\n "普通标题"\\n ]
163
+ newStr: "text": [\\n {\\n "b": true,\\n "i": "m",\\n "text": "粗体标题"\\n }\\n ]
164
+ \\\`\\\`\\\`
165
+
166
+ 示例——给文本加超链接:
167
+
168
+ \\\`\\\`\\\`
169
+ oldStr: "text": [\\n "点击访问官网"\\n ]
170
+ newStr: "text": [\\n "点击",\\n {\\n "i": "m",\\n "iUrl": "https://remnote.com",\\n "text": "访问官网"\\n }\\n ]
171
+ \\\`\\\`\\\`
172
+
173
+ 注意:\\\`highlightColor\\\`(Rem 顶层字段,字符串如 \\\`"Red"\\\`)和 RichText 的 \\\`h\\\`(行内格式标记,数字 0-9)是两个独立属性。详见 \\\`resource://rem-object-fields\\\`。
174
+
175
+ ### 缓存行为速查
176
+
177
+ | 工具 | 缓存行为 | 用途 |
178
+ |:-----|:---------|:-----|
179
+ | \\\`read_rem\\\` | 写入缓存 | 供 \\\`edit_rem\\\` 使用 |
180
+ | \\\`read_tree\\\` | 写入缓存 | 供 \\\`edit_tree\\\` 使用 |
181
+ | \\\`search\\\` / \\\`read_globe\\\` / \\\`read_context\\\` | **不缓存** | 不能作为 edit 前置 |
154
182
 
155
183
  ### 场景 E:修改结构(新增/删除/移动/重排)
156
184
 
@@ -0,0 +1,192 @@
1
+ export const EDIT_REM_GUIDE_CONTENT = `
2
+ # edit_rem 操作指南
3
+
4
+ edit_rem 通过 str_replace 语义修改单个 Rem 的属性。操作对象是 \\\`JSON.stringify(remObject, null, 2)\\\` 的格式化文本。
5
+
6
+ ---
7
+
8
+ ## 前置条件
9
+
10
+ 必须先 \\\`read_rem\\\` 同一个 remId,建立缓存后才能 \\\`edit_rem\\\`。跳过会触发防线 1 错误。
11
+
12
+ 工作流:\\\`read_rem\\\` → 查看 JSON → \\\`edit_rem\\\`(oldStr/newStr)。
13
+
14
+ ---
15
+
16
+ ## str_replace 语义
17
+
18
+ ### 操作对象
19
+
20
+ 格式化缩进 2 空格的 JSON 文本。示例片段:
21
+
22
+ \\\`\\\`\\\`json
23
+ {
24
+ "id": "kLrIOHJLyMd8Y2lyA",
25
+ "text": [
26
+ "Hello World"
27
+ ],
28
+ "backText": null,
29
+ "type": "concept",
30
+ "highlightColor": null,
31
+ "isTodo": false
32
+ }
33
+ \\\`\\\`\\\`
34
+
35
+ ### 匹配规则
36
+
37
+ - oldStr 必须在 JSON 文本中**恰好匹配 1 次**(0 次=未找到,>1 次=多匹配,均报错)
38
+ - 大小写敏感,精确匹配
39
+ - oldStr 建议包含字段名 + 值,避免匹配到 text 内容中的同名字符串
40
+
41
+ ### 替换后校验
42
+
43
+ 替换后的文本必须是合法 JSON,否则报 "invalid JSON" 错误。
44
+
45
+ ---
46
+
47
+ ## 三道防线
48
+
49
+ | 防线 | 检查内容 | 失败时 |
50
+ |:-----|:---------|:-------|
51
+ | 1. 缓存存在 | 是否已 read_rem | 报 "has not been read yet" → 先 read_rem |
52
+ | 2. 并发检测 | 当前 Rem 是否被外部修改 | 报 "has been modified since last read" → 重新 read_rem |
53
+ | 3. 精确匹配 | oldStr 匹配次数 | 0 次或多次 → 调整 oldStr 使其唯一 |
54
+
55
+ 防线 2 的关键:edit 时会从 Plugin 重新读取最新数据与缓存比较。如果不一致(含空白和格式),拒绝编辑并**不更新缓存**,迫使你重新 read_rem。
56
+
57
+ ---
58
+
59
+ ## RichText 编辑实战
60
+
61
+ \\\`text\\\` 和 \\\`backText\\\` 字段使用 RichText 格式。在格式化 JSON 中,RichText 数组内的对象展开为多行,key 按**字母序**排列。
62
+
63
+ ### 关键排序规则
64
+
65
+ - \\\`_id\\\`(U+005F)排在所有小写字母之前,所以 \\\`_id\\\` 总是第一个 key
66
+ - 示例排序:\\\`_id\\\` < \\\`b\\\` < \\\`cId\\\` < \\\`h\\\` < \\\`i\\\` < \\\`iUrl\\\` < \\\`text\\\`
67
+
68
+ ### 示例 1:纯文本 → 粗体
69
+
70
+ read_rem 返回:
71
+
72
+ \\\`\\\`\\\`json
73
+ "text": [
74
+ "普通标题"
75
+ ],
76
+ \\\`\\\`\\\`
77
+
78
+ edit_rem 调用:
79
+
80
+ \\\`\\\`\\\`
81
+ oldStr: "text": [\\n "普通标题"\\n ]
82
+ newStr: "text": [\\n {\\n "b": true,\\n "i": "m",\\n "text": "粗体标题"\\n }\\n ]
83
+ \\\`\\\`\\\`
84
+
85
+ 替换后变为:
86
+
87
+ \\\`\\\`\\\`json
88
+ "text": [
89
+ {
90
+ "b": true,
91
+ "i": "m",
92
+ "text": "粗体标题"
93
+ }
94
+ ],
95
+ \\\`\\\`\\\`
96
+
97
+ ### 示例 2:纯文本 → 部分超链接
98
+
99
+ \\\`\\\`\\\`
100
+ oldStr: "text": [\\n "点击访问官网"\\n ]
101
+ newStr: "text": [\\n "点击",\\n {\\n "i": "m",\\n "iUrl": "https://remnote.com",\\n "text": "访问官网"\\n }\\n ]
102
+ \\\`\\\`\\\`
103
+
104
+ ### 示例 3:修改引用旁的文本
105
+
106
+ read_rem 返回:
107
+
108
+ \\\`\\\`\\\`json
109
+ "text": [
110
+ "参考 ",
111
+ {
112
+ "_id": "abc123",
113
+ "i": "q"
114
+ },
115
+ " 的内容"
116
+ ],
117
+ \\\`\\\`\\\`
118
+
119
+ 只改文字部分(纯字符串可直接匹配):
120
+
121
+ \\\`\\\`\\\`
122
+ oldStr: " 的内容"
123
+ newStr: " 的详细说明"
124
+ \\\`\\\`\\\`
125
+
126
+ 如果 " 的内容" 出现多次导致多匹配,加上下文:
127
+
128
+ \\\`\\\`\\\`
129
+ oldStr: "q"\\n },\\n " 的内容"
130
+ newStr: "q"\\n },\\n " 的详细说明"
131
+ \\\`\\\`\\\`
132
+
133
+ ### 示例 4:添加完形填空
134
+
135
+ \\\`\\\`\\\`
136
+ oldStr: "text": [\\n "光合作用需要阳光"\\n ]
137
+ newStr: "text": [\\n "光合作用需要",\\n {\\n "cId": "cloze1",\\n "i": "m",\\n "text": "阳光"\\n }\\n ]
138
+ \\\`\\\`\\\`
139
+
140
+ ### 示例 5:修改简单属性
141
+
142
+ \\\`\\\`\\\`
143
+ oldStr: "type": "default"
144
+ newStr: "type": "concept"
145
+ \\\`\\\`\\\`
146
+
147
+ \\\`\\\`\\\`
148
+ oldStr: "highlightColor": null
149
+ newStr: "highlightColor": "Red"
150
+ \\\`\\\`\\\`
151
+
152
+ \\\`\\\`\\\`
153
+ oldStr: "practiceDirection": "forward"
154
+ newStr: "practiceDirection": "both"
155
+ \\\`\\\`\\\`
156
+
157
+ ---
158
+
159
+ ## highlightColor vs h
160
+
161
+ | 属性 | 位置 | 值类型 | 作用范围 |
162
+ |:-----|:-----|:-------|:---------|
163
+ | \\\`highlightColor\\\` | RemObject 顶层字段 | 字符串(\\\`"Red"\\\`, \\\`"Blue"\\\` 等)或 \\\`null\\\` | 整行背景色 |
164
+ | \\\`h\\\` | RichText 元素内格式标记 | 数字 0-9(RemColor 枚举) | 行内文字片段 |
165
+
166
+ 两者完全独立。\\\`highlightColor\\\` 通过 \\\`setHighlightColor()\\\` 写入,\\\`h\\\` 通过 \\\`setText()\\\` 随 RichText 一起写入。
167
+
168
+ ---
169
+
170
+ ## 常见错误
171
+
172
+ | 错误 | 原因 | 解决 |
173
+ |:-----|:-----|:-----|
174
+ | key 顺序错 | 写 \\\`{"text":"xx","i":"m"}\\\` 但实际是 \\\`{"i":"m","text":"xx"}\\\` | 按字母序排列 key |
175
+ | 缩进不匹配 | 空格数不对 | 仔细对照 read_rem 返回的缩进 |
176
+ | 混淆 highlightColor 和 h | 前者字符串 \\\`"Red"\\\`,后者数字 \\\`1\\\` | 参考上方对比表 |
177
+ | 漏 onlyAudio | \\\`i:"a"\\\` 的 \\\`onlyAudio\\\` 是必填 | true=音频,false=视频 |
178
+ | JSON 语法错 | 引号、逗号、括号不完整 | 检查替换边界 |
179
+
180
+ ---
181
+
182
+ ## 缓存更新行为
183
+
184
+ | 场景 | 缓存 |
185
+ |:-----|:-----|
186
+ | 写入成功 | 从 Plugin 重新读取最新状态 → 覆盖缓存 |
187
+ | 防线 2 拒绝 | **不更新**缓存(迫使重新 read_rem) |
188
+ | 防线 3 拒绝 | 缓存不变(可调整 oldStr 重试) |
189
+ | 部分写入失败 | **不更新**缓存(迫使重新 read_rem) |
190
+
191
+ 写入成功后**永远从 Plugin 重新读取**,不做本地推导,保证缓存与 SDK 状态完全同步。
192
+ `;
@@ -25,21 +25,21 @@ RemObject 是本项目对 RemNote Rem 的标准化表示,包含 51 个字段
25
25
 
26
26
  | 字段 | 类型 | 权限 | 说明 |
27
27
  |------|------|:----:|------|
28
- | \\\`text\\\` | \\\`RichText\\\` | RW | 正面文本(RichText 数组) |
29
- | \\\`backText\\\` | \\\`RichText \\| null\\\` | RW | 背面文本。null=无背面;设值即产生闪卡正反面结构 |
28
+ | \\\`text\\\` | \\\`RichText\\\` | RW | 正面文本(RichText 数组)。UI:文本内容立即更新显示 |
29
+ | \\\`backText\\\` | \\\`RichText \\| null\\\` | RW | 背面文本。null=无背面;设值即产生闪卡正反面结构。UI:显示为"正面 → 背面"箭头分隔格式 |
30
30
 
31
31
  ## 类型系统
32
32
 
33
33
  | 字段 | 类型 | 权限 | 说明 |
34
34
  |------|------|:----:|------|
35
35
  | \\\`type\\\` | \\\`RemTypeValue\\\` | RW | \\\`concept\\\` / \\\`descriptor\\\` / \\\`default\\\` / \\\`portal\\\` |
36
- | \\\`isDocument\\\` | \\\`boolean\\\` | RW | 是否作为独立文档页面。独立于 type |
36
+ | \\\`isDocument\\\` | \\\`boolean\\\` | RW | 是否作为独立文档页面。独立于 type。UI:bullet(•)变为文档图标,可独立打开 |
37
37
 
38
38
  ## 结构
39
39
 
40
40
  | 字段 | 类型 | 权限 | 说明 |
41
41
  |------|------|:----:|------|
42
- | \\\`parent\\\` | \\\`string \\| null\\\` | RW | 父 Rem ID。null=顶级 |
42
+ | \\\`parent\\\` | \\\`string \\| null\\\` | RW | 父 Rem ID。null=顶级。UI:Rem 从原位置消失,出现在新父级下 |
43
43
  | \\\`children\\\` | \\\`string[]\\\` | R | 子 Rem ID 有序数组 |
44
44
 
45
45
  ## 格式 / 显示
@@ -47,21 +47,21 @@ RemObject 是本项目对 RemNote Rem 的标准化表示,包含 51 个字段
47
47
  | 字段 | 类型 | 权限 | 说明 |
48
48
  |------|------|:----:|------|
49
49
  | \\\`fontSize\\\` | \\\`FontSize \\| null\\\` | RW | 标题大小:\\\`H1\\\` / \\\`H2\\\` / \\\`H3\\\`。null=普通 |
50
- | \\\`highlightColor\\\` | \\\`HighlightColor \\| null\\\` | RW | 高亮颜色(9 种)。null=无高亮 |
50
+ | \\\`highlightColor\\\` | \\\`HighlightColor \\| null\\\` | RW | 高亮颜色(9 种)。null=无高亮。UI:整行背景变为对应颜色,bullet 也着色 |
51
51
 
52
52
  ## 状态标记
53
53
 
54
54
  | 字段 | 类型 | 权限 | 说明 |
55
55
  |------|------|:----:|------|
56
- | \\\`isTodo\\\` | \\\`boolean\\\` | RW | 是否待办。设为 true 时自动初始化 todoStatus |
57
- | \\\`todoStatus\\\` | \\\`TodoStatus \\| null\\\` | RW | \\\`Finished\\\` / \\\`Unfinished\\\`。需先 isTodo=true |
58
- | \\\`isCode\\\` | \\\`boolean\\\` | RW | 是否代码块 |
59
- | \\\`isQuote\\\` | \\\`boolean\\\` | RW | 是否引用块 |
60
- | \\\`isListItem\\\` | \\\`boolean\\\` | RW | 是否列表项(有序列表样式) |
61
- | \\\`isCardItem\\\` | \\\`boolean\\\` | RW | 是否卡片项(多行答案行标记) |
56
+ | \\\`isTodo\\\` | \\\`boolean\\\` | RW | 是否待办。设为 true 时自动初始化 todoStatus。UI:文本前出现空心 checkbox(☐) |
57
+ | \\\`todoStatus\\\` | \\\`TodoStatus \\| null\\\` | RW | \\\`Finished\\\` / \\\`Unfinished\\\`。需先 isTodo=true。UI:Finished=蓝色已勾选(☑)+文本删除线 |
58
+ | \\\`isCode\\\` | \\\`boolean\\\` | RW | 是否代码块。UI:等宽字体、灰色背景、块级缩进 |
59
+ | \\\`isQuote\\\` | \\\`boolean\\\` | RW | 是否引用块。UI:左侧灰色竖线+背景浅灰(blockquote 样式) |
60
+ | \\\`isListItem\\\` | \\\`boolean\\\` | RW | 是否列表项。UI:bullet(•)变为数字编号"1."(有序列表) |
61
+ | \\\`isCardItem\\\` | \\\`boolean\\\` | RW | 是否卡片项(多行答案行标记)。UI:无明显变化,在 Card View 中生效 |
62
62
  | \\\`isTable\\\` | \\\`boolean\\\` | R | 是否表格(只读) |
63
- | \\\`isSlot\\\` | \\\`boolean\\\` | RW | 是否 Powerup 插槽。与 isProperty 底层相同 |
64
- | \\\`isProperty\\\` | \\\`boolean\\\` | RW | 是否 Tag 属性(表格列)。与 isSlot 底层相同 |
63
+ | \\\`isSlot\\\` | \\\`boolean\\\` | RW | 是否 Powerup 插槽。与 isProperty 底层相同。UI:bullet(•)变为方形图标(☐) |
64
+ | \\\`isProperty\\\` | \\\`boolean\\\` | RW | 是否 Tag 属性(表格列)。与 isSlot 底层相同。UI:bullet(•)变为方形图标(☐) |
65
65
 
66
66
  ## Powerup 系统标识
67
67
 
@@ -90,15 +90,15 @@ RemObject 是本项目对 RemNote Rem 的标准化表示,包含 51 个字段
90
90
 
91
91
  | 字段 | 类型 | 权限 | 说明 |
92
92
  |------|------|:----:|------|
93
- | \\\`enablePractice\\\` | \\\`boolean\\\` | RW | 是否启用间隔重复练习 |
94
- | \\\`practiceDirection\\\` | \\\`PracticeDirection\\\` | RW | 练习方向:\\\`forward\\\` / \\\`backward\\\` / \\\`both\\\` / \\\`none\\\` |
93
+ | \\\`enablePractice\\\` | \\\`boolean\\\` | RW | 是否启用间隔重复练习。UI:无明显变化 |
94
+ | \\\`practiceDirection\\\` | \\\`PracticeDirection\\\` | RW | 练习方向:\\\`forward\\\` / \\\`backward\\\` / \\\`both\\\` / \\\`none\\\`。UI:无明显变化 |
95
95
 
96
96
  ## 关联 — 直接关系
97
97
 
98
98
  | 字段 | 类型 | 权限 | 说明 |
99
99
  |------|------|:----:|------|
100
- | \\\`tags\\\` | \\\`string[]\\\` | RW | 标签 Rem ID 数组。写入时使用 diff 机制(add/remove |
101
- | \\\`sources\\\` | \\\`string[]\\\` | RW | 来源 Rem ID 数组。写入时使用 diff 机制 |
100
+ | \\\`tags\\\` | \\\`string[]\\\` | RW | 标签 Rem ID 数组。写入时使用 diff 机制(add/remove)。UI:行右侧出现标签徽章 |
101
+ | \\\`sources\\\` | \\\`string[]\\\` | RW | 来源 Rem ID 数组。写入时使用 diff 机制。UI:Rem 下方出现灰色来源引用框 |
102
102
  | \\\`aliases\\\` | \\\`string[]\\\` | R | 别名 Rem ID 数组 |
103
103
 
104
104
  ## 关联 — 引用关系
@@ -131,7 +131,7 @@ RemObject 是本项目对 RemNote Rem 的标准化表示,包含 51 个字段
131
131
 
132
132
  | 字段 | 类型 | 权限 | 说明 |
133
133
  |------|------|:----:|------|
134
- | \\\`positionAmongstSiblings\\\` | \\\`number \\| null\\\` | RW | 在兄弟间的位置(0 起始) |
134
+ | \\\`positionAmongstSiblings\\\` | \\\`number \\| null\\\` | RW | 在兄弟间的位置(0 起始)。UI:Rem 在父级子列表中的显示位置改变 |
135
135
  | \\\`timesSelectedInSearch\\\` | \\\`number\\\` | R-F | 搜索中被选次数 |
136
136
  | \\\`lastTimeMovedTo\\\` | \\\`number\\\` | R-F | 上次移动时间(毫秒时间戳) |
137
137
  | \\\`schemaVersion\\\` | \\\`number\\\` | R-F | Schema 版本号 |
@@ -303,29 +303,71 @@ RemObject 中的 \\\`text\\\` 和 \\\`backText\\\` 字段使用 RichText 格式
303
303
 
304
304
  ### 元素类型
305
305
 
306
- | \\\`i\\\` 值 | 含义 | 核心字段 |
307
- |--------|------|----------|
308
- | (纯 string) | 纯文本片段 | — |
309
- | \\\`"m"\\\` | 带格式文本 | \\\`text\\\` + 格式标记 |
310
- | \\\`"q"\\\` | Rem 引用 | \\\`_id\\\`(被引用 Rem ID) |
311
- | \\\`"i"\\\` | 图片 | \\\`url\\\`, \\\`width\\\`, \\\`height\\\` |
312
- | \\\`"x"\\\` | LaTeX | \\\`text\\\` |
313
- | \\\`"a"\\\` | 音频 | \\\`url\\\` |
306
+ | \\\`i\\\` 值 | 含义 | 必填字段 | 可选字段 |
307
+ |--------|------|----------|----------|
308
+ | (纯 string) | 纯文本片段 | — | — |
309
+ | \\\`"m"\\\` | 带格式文本 | \\\`text\\\` | 格式标记(见下表) |
310
+ | \\\`"q"\\\` | Rem 引用 | \\\`_id\\\` | \\\`content\\\`, \\\`showFullName\\\`, \\\`aliasId\\\` |
311
+ | \\\`"i"\\\` | 图片 | \\\`url\\\` | \\\`width\\\`, \\\`height\\\`, \\\`percent\\\`(25/50/100) |
312
+ | \\\`"x"\\\` | LaTeX | \\\`text\\\` | \\\`block\\\`(true=块级公式) |
313
+ | \\\`"a"\\\` | 音频/视频 | \\\`url\\\`, \\\`onlyAudio\\\`(**必填**) | \\\`width\\\`, \\\`height\\\` |
314
+ | \\\`"s"\\\` | 卡片分隔符 | — | \\\`delimiterCharacterForSerialization\\\` |
314
315
 
315
- ### 行内格式标记(\\\`i:"m"\\\` 元素内)
316
+ **注意**:\\\`i:"a"\\\` 的 \\\`onlyAudio\\\` 是**必填**字段(\\\`true\\\`=音频,\\\`false\\\`=视频),缺少会导致 SDK 拒绝写入。
317
+
318
+ ### 格式标记(主要用于 \\\`i:"m"\\\`,但 \\\`i:"q"\\\` 等元素也支持)
316
319
 
317
320
  | 字段 | 类型 | 含义 |
318
321
  |------|------|------|
319
322
  | \\\`b\\\` | \\\`true\\\` | 加粗 |
320
- | \\\`l\\\` | \\\`true\\\` | 斜体 |
323
+ | \\\`l\\\` | \\\`true\\\` | 斜体(小写字母 L,不是 I) |
321
324
  | \\\`u\\\` | \\\`true\\\` | 下划线 |
322
- | \\\`h\\\` | \\\`number\\\` | 高亮颜色(1=红, 2=橙, 3=黄, 4=绿, 5=蓝, 6=紫) |
323
- | \\\`tc\\\` | \\\`number\\\` | 文字颜色 |
324
- | \\\`code\\\` | \\\`true\\\` | 行内代码 |
325
+ | \\\`h\\\` | \\\`number\\\` | 高亮颜色(RemColor 枚举:1=Red, 2=Orange, 3=Yellow, 4=Green, 5=Purple, 6=Blue, 7=Gray, 8=Brown, 9=Pink) |
326
+ | \\\`tc\\\` | \\\`number\\\` | 文字颜色(同 RemColor 枚举) |
327
+ | \\\`q\\\` | \\\`true\\\` | 行内代码(红色等宽样式) |
328
+ | \\\`code\\\` | \\\`true\\\` | 代码块(带语言标签和复制按钮) |
329
+ | \\\`language\\\` | \\\`string\\\` | 代码块语言(如 \\\`"javascript"\\\`、\\\`"python"\\\`) |
325
330
  | \\\`cId\\\` | \\\`string\\\` | 完形填空 ID |
326
- | \\\`iUrl\\\` | \\\`string\\\` | 外部链接 URL |
331
+ | \\\`hiddenCloze\\\` | \\\`true\\\` | 完形填空隐藏状态 |
332
+ | \\\`revealedCloze\\\` | \\\`true\\\` | 完形填空已揭示状态 |
333
+ | \\\`iUrl\\\` | \\\`string\\\` | 外部超链接 URL(\\\`url\\\` 字段已废弃,必须用 \\\`iUrl\\\`) |
334
+ | \\\`qId\\\` | \\\`string\\\` | 行内引用链接的 Rem ID |
335
+
336
+ ### RemColor 颜色枚举(\\\`h\\\` 和 \\\`tc\\\` 共用)
337
+
338
+ | 值 | 颜色 | 值 | 颜色 | 值 | 颜色 |
339
+ |:---|:-----|:---|:-----|:---|:-----|
340
+ | 0 | 无颜色/默认 | 4 | Green | 7 | Gray |
341
+ | 1 | Red | 5 | Purple | 8 | Brown |
342
+ | 2 | Orange | 6 | Blue | 9 | Pink |
343
+ | 3 | Yellow | — | — | — | — |
344
+
345
+ ### 常用构造示例
346
+
347
+ 以下为 key 字母序排列的格式:
348
+
349
+ \\\`\\\`\\\`jsonc
350
+ { "b": true, "i": "m", "text": "粗体" } // 粗体(key 序:b < i < text)
351
+ { "i": "m", "q": true, "text": "code" } // 行内代码
352
+ { "i": "m", "iUrl": "https://...", "text": "链接" } // 超链接(iUrl 不是 url!)
353
+ { "b": true, "h": 1, "i": "m", "text": "重点" } // 粗体+红色高亮(h 是数字 0-9)
354
+ { "cId": "c1", "i": "m", "text": "答案" } // 完形填空
355
+ { "_id": "remId", "b": true, "i": "q" } // Rem 引用加粗(_id 排最前)
356
+ { "i": "x", "text": "E = mc^2" } // LaTeX
357
+ { "i": "a", "onlyAudio": false, "url": "..." } // 视频(onlyAudio 必填!)
358
+ { "i": "a", "onlyAudio": true, "url": "..." } // 音频
359
+ \\\`\\\`\\\`
360
+
361
+ > 在 RemObject 格式化 JSON 中,数组内对象会展开为多行(每个 key 一行)。构造 \\\`edit_rem\\\` 的 oldStr/newStr 时必须使用实际的多行格式。
362
+
363
+ ### highlightColor(Rem 级别)vs h(RichText 行内)
364
+
365
+ - \\\`highlightColor\\\`:RemObject 顶层字段,值为字符串(\\\`"Red"\\\`, \\\`"Blue"\\\` 等)或 \\\`null\\\`,整行背景色
366
+ - \\\`h\\\`:RichText 元素内格式标记,值为数字 0-9(RemColor 枚举),行内文字片段高亮
367
+
368
+ 两者完全独立,互不影响。
327
369
 
328
370
  ### 序列化确定性
329
371
 
330
- RichText 对象元素内部按 **key 字母序排列**(Plugin 端 \\\`sortRichTextKeys()\\\` 处理),确保同一内容的序列化 JSON 始终一致。这对 \\\`edit_rem\\\` 的 str_replace 和乐观并发检测至关重要。
372
+ RichText 对象元素内部按 **key 字母序排列**(Plugin 端 \\\`sortRichTextKeys()\\\` 处理),确保同一内容的序列化 JSON 始终一致。\\\`_id\\\` 中的 \\\`_\\\`(U+005F)排在所有小写字母(\\\`a\\\`=U+0061)之前,所以 \\\`_id\\\` 总是第一个 key。这对 \\\`edit_rem\\\` 的 str_replace 和乐观并发检测至关重要。
331
373
  `;
@@ -9,7 +9,7 @@ export function registerEditTools(server) {
9
9
  // -------------------------------------------------------------------------
10
10
  server.addTool({
11
11
  name: 'edit_rem',
12
- description: '通过 str_replace 语义修改单个 Rem 的属性字段。\n\n操作对象是 Rem 序列化后的 JSON 文本(JSON.stringify 2 空格缩进),\n在其中将 oldStr 精确替换为 newStr,自动推导变更字段并写入。\n\n适用场景:修改文本、类型、标题级别、practiceDirection、高亮色、Todo 状态等属性;\n修改分隔符以改变闪卡类型。不适合修改子树结构(用 edit_tree)。\n\n前置条件:必须先 read_rem 建立缓存,否则被防线拒绝。\n工作流:read_rem → 查看 JSON → edit_rem 替换。\n\n三道防线:缓存存在 → 并发检测 → 精确匹配(恰好 1 次)。\nstr_replace 要点:oldStr 建议包含字段名避免歧义(如 "text": "旧值")。替换后须是合法 JSON。\n关联工具:read_rem(前置)、edit_tree(结构编辑)',
12
+ description: '通过 str_replace 语义修改单个 Rem 的属性字段。\n\n操作对象是 Rem 序列化后的 JSON 文本(JSON.stringify 2 空格缩进),\n在其中将 oldStr 精确替换为 newStr,自动推导变更字段并写入。\n\n适用场景:修改文本、类型、标题级别、practiceDirection、高亮色、Todo 状态等属性;\n修改分隔符以改变闪卡类型。不适合修改子树结构(用 edit_tree)。\n\n前置条件:必须先 read_rem 建立缓存,否则被防线拒绝。\n工作流:read_rem → 查看 JSON → edit_rem 替换。\n\n三道防线:缓存存在 → 并发检测 → 精确匹配(恰好 1 次)。\nstr_replace 要点:oldStr 建议包含字段名避免歧义(如 "text": "旧值")。替换后须是合法 JSON。\n详细操作指南(含 RichText 编辑实战示例)见 resource://edit-rem-guide。\n关联工具:read_rem(前置)、edit_tree(结构编辑)',
13
13
  parameters: z.object({
14
14
  remId: z.string().describe('目标 Rem 的 ID'),
15
15
  oldStr: z.string().describe('要替换的原始文本(必须精确匹配缓存中的内容,且恰好匹配 1 次)'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remnote-bridge",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "RemNote 自动化桥接工具集:CLI + MCP Server + Plugin",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,7 +42,7 @@ RemNote 中所有内容的基本单元都是 **Rem**。文档、文件夹、闪
42
42
 
43
43
  两个独立维度:
44
44
 
45
- - **type**(闪卡语义):`concept`(加粗)、`descriptor`(斜体)、`default`(普通)、`portal`(只读)
45
+ - **type**(闪卡语义):`concept`(加粗)、`descriptor`(正常字重)、`default`(普通)、`portal`(只读)
46
46
  - **isDocument**(页面语义):与 type 完全独立
47
47
 
48
48
  ### CDF 框架(Concept-Descriptor Framework)
@@ -98,16 +98,90 @@ Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的
98
98
  ["纯文本", {"i":"m","b":true,"text":"粗体"}, {"i":"q","_id":"remId"}]
99
99
  ```
100
100
 
101
- | `i` 值 | 类型 | 核心字段 |
102
- |:-------|:-----|:---------|
103
- | 纯 string | 纯文本 | — |
104
- | `"m"` | 带格式文本 | `text` + `b`/`l`/`u`/`h`/`tc`/`code`/`cId`/`iUrl` |
105
- | `"q"` | Rem 引用 | `_id` |
106
- | `"i"` | 图片 | `url`, `width`, `height` |
107
- | `"x"` | LaTeX | `text` |
108
- | `"a"` | 音频 | `url` |
101
+ #### 元素类型
109
102
 
110
- **序列化确定性**:RichText 对象内部按 **key 字母序排列**(如 `{"b":true,"i":"m","text":"粗体"}`),确保 JSON 始终一致。构造 edit-rem oldStr 时必须保持相同的 key 顺序,否则匹配失败。
103
+ | `i` | 类型 | 必填字段 | 可选字段 |
104
+ |:-------|:-----|:---------|:---------|
105
+ | 纯 string | 纯文本 | — | — |
106
+ | `"m"` | 带格式文本 | `text` | 格式标记(见下表) |
107
+ | `"q"` | Rem 引用 | `_id` | `content`, `showFullName`, `aliasId` |
108
+ | `"i"` | 图片 | `url` | `width`, `height`, `percent`(25/50/100) |
109
+ | `"x"` | LaTeX | `text` | `block`(true=块级公式) |
110
+ | `"a"` | 音频/视频 | `url`, `onlyAudio` | `width`, `height` |
111
+ | `"s"` | 卡片分隔符 | — | `delimiterCharacterForSerialization` |
112
+
113
+ **注意**:`i:"a"` 的 `onlyAudio` 是**必填**字段(`true`=音频,`false`=视频),缺少会导致 SDK 拒绝写入。
114
+
115
+ #### 格式标记(主要用于 `i:"m"`,但 `i:"q"` 等也支持)
116
+
117
+ | 字段 | 类型 | 含义 |
118
+ |:-----|:-----|:-----|
119
+ | `b` | `true` | 加粗 |
120
+ | `l` | `true` | 斜体(小写字母 L,不是 I) |
121
+ | `u` | `true` | 下划线 |
122
+ | `h` | `number` | 高亮颜色(见 RemColor 枚举) |
123
+ | `tc` | `number` | 文字颜色(见 RemColor 枚举) |
124
+ | `q` | `true` | 行内代码(红色等宽样式) |
125
+ | `code` | `true` | 代码块(带语言标签和复制按钮) |
126
+ | `language` | `string` | 代码块语言(如 `"javascript"`、`"python"`) |
127
+ | `cId` | `string` | 完形填空 ID |
128
+ | `hiddenCloze` | `true` | 完形填空隐藏状态 |
129
+ | `revealedCloze` | `true` | 完形填空已揭示状态 |
130
+ | `iUrl` | `string` | 外部超链接 URL |
131
+ | `qId` | `string` | 行内引用链接的 Rem ID |
132
+
133
+ **注意**:`url` 字段已废弃无效,超链接必须用 `iUrl`。
134
+
135
+ #### RemColor 颜色枚举(`h` 和 `tc` 共用)
136
+
137
+ | 值 | 颜色 | 值 | 颜色 | 值 | 颜色 |
138
+ |:---|:-----|:---|:-----|:---|:-----|
139
+ | 0 | 无颜色/默认 | 4 | Green | 7 | Gray |
140
+ | 1 | Red | 5 | Purple | 8 | Brown |
141
+ | 2 | Orange | 6 | Blue | 9 | Pink |
142
+ | 3 | Yellow | — | — | — | — |
143
+
144
+ #### 常用构造示例
145
+
146
+ 以下为 `JSON.stringify(null, 2)` 格式化后的实际样式(key 按字母序):
147
+
148
+ ```jsonc
149
+ // 粗体(注意 key 顺序:b < i < text)
150
+ { "b": true, "i": "m", "text": "粗体" }
151
+ // 行内代码
152
+ { "i": "m", "q": true, "text": "console.log()" }
153
+ // 超链接(iUrl 不是 url!)
154
+ { "i": "m", "iUrl": "https://example.com", "text": "点击访问" }
155
+ // 红色高亮 + 粗体(h 是数字,不是字符串)
156
+ { "b": true, "h": 1, "i": "m", "text": "重点" }
157
+ // 完形填空
158
+ { "cId": "cloze1", "i": "m", "text": "答案内容" }
159
+ // Rem 引用(_id 排在所有小写 key 之前)
160
+ { "_id": "remId", "i": "q" }
161
+ // Rem 引用加粗
162
+ { "_id": "remId", "b": true, "i": "q" }
163
+ // LaTeX 公式
164
+ { "i": "x", "text": "E = mc^2" }
165
+ // 图片
166
+ { "i": "i", "url": "https://...", "width": 200, "height": 100 }
167
+ // 视频(onlyAudio 必填!)
168
+ { "i": "a", "onlyAudio": false, "url": "https://youtube.com/watch?v=xxx" }
169
+ // 音频
170
+ { "i": "a", "onlyAudio": true, "url": "https://example.com/audio.mp3" }
171
+ ```
172
+
173
+ > **注意**:在 RemObject 的格式化 JSON 中,数组内的对象会展开为多行(每个 key 一行,缩进 4+2 空格)。以上为简写——构造 edit-rem 的 oldStr/newStr 时必须使用实际的多行格式。
174
+
175
+ #### highlightColor(Rem 级别)vs h(RichText 行内)
176
+
177
+ - `highlightColor`:RemObject 顶层字段,值为字符串(`"Red"`, `"Blue"` 等)或 `null`,作用于整行背景
178
+ - `h`:RichText 元素内格式标记,值为数字 0-9(RemColor 枚举),作用于行内文字片段
179
+
180
+ 两者完全独立,互不影响。
181
+
182
+ #### 序列化确定性
183
+
184
+ RichText 对象内部按 **key 字母序排列**(`sortRichTextKeys()`),确保 JSON 始终一致。`_id` 中的 `_`(U+005F)排在所有小写字母(`a`=U+0061)之前。构造 edit-rem 的 oldStr 时必须保持相同的 key 顺序,否则匹配失败。
111
185
 
112
186
  ### Powerup 机制与噪音过滤
113
187
 
@@ -461,12 +461,158 @@ str_replace 操作的对象是 `JSON.stringify(remObject, null, 2)` 的文本—
461
461
 
462
462
  2. **替换后必须是合法 JSON**:检查引号、逗号、括号的完整性
463
463
 
464
- 3. **修改 RichText 字段**:直接操作 JSON 数组结构
464
+ 3. **修改 RichText 字段**:直接操作 JSON 数组结构(见下方完整示例)
465
465
 
466
- ```
467
- oldStr: "\"text\": [\n \"Hello\"\n ]"
468
- newStr: "\"text\": [\n \"World\"\n ]"
469
- ```
466
+ ---
467
+
468
+ ## RichText 编辑实战指南
469
+
470
+ ### 理解格式化 JSON 中的 RichText
471
+
472
+ `edit-rem` 的 str_replace 操作对象是 `JSON.stringify(remObject, null, 2)` 的格式化文本。RichText 数组在格式化后是多行缩进结构,**不是**紧凑的单行 JSON。
473
+
474
+ 以下是一个包含 RichText 的 RemObject **实际输出片段**:
475
+
476
+ ```json
477
+ {
478
+ "id": "kLrIOHJLyMd8Y2lyA",
479
+ "text": [
480
+ "这是",
481
+ {
482
+ "b": true,
483
+ "i": "m",
484
+ "text": "粗体"
485
+ },
486
+ "普通文本"
487
+ ],
488
+ "backText": null,
489
+ "type": "concept",
490
+ "highlightColor": null,
491
+ "isTodo": false
492
+ }
493
+ ```
494
+
495
+ **关键要点**:
496
+ - RichText 对象内部的 key 按**字母序**排列(`b` < `i` < `text`),由 Plugin 端 `sortRichTextKeys()` 保证
497
+ - `_id` 中的 `_` 在 Unicode 中排在小写字母之前,所以 `_id` 排在所有小写 key 的最前面
498
+ - 每个 key-value 对占一行,缩进 4 空格(对象在数组中时嵌套 2+2)
499
+ - 纯字符串元素直接是 `"字符串"`,对象元素展开为多行
500
+
501
+ ### 示例 1:将纯文本改为粗体
502
+
503
+ **read-rem 返回**(部分):
504
+
505
+ ```json
506
+ "text": [
507
+ "普通标题"
508
+ ],
509
+ ```
510
+
511
+ **edit-rem 调用**:
512
+
513
+ ```
514
+ oldStr: "\"text\": [\n \"普通标题\"\n ]"
515
+
516
+ newStr: "\"text\": [\n {\n \"b\": true,\n \"i\": \"m\",\n \"text\": \"粗体标题\"\n }\n ]"
517
+ ```
518
+
519
+ 替换后 JSON 变为:
520
+
521
+ ```json
522
+ "text": [
523
+ {
524
+ "b": true,
525
+ "i": "m",
526
+ "text": "粗体标题"
527
+ }
528
+ ],
529
+ ```
530
+
531
+ ### 示例 2:修改 Rem 引用旁的文本
532
+
533
+ **read-rem 返回**(部分):
534
+
535
+ ```json
536
+ "text": [
537
+ "参考 ",
538
+ {
539
+ "_id": "abc123",
540
+ "i": "q"
541
+ },
542
+ " 的内容"
543
+ ],
544
+ ```
545
+
546
+ 将 " 的内容" 替换为 " 的详细说明":
547
+
548
+ ```
549
+ oldStr: " 的内容"
550
+ newStr: " 的详细说明"
551
+ ```
552
+
553
+ > 注意:纯字符串可以直接匹配,不需要包含数组结构。但如果 " 的内容" 在 JSON 中出现多次,需要加上下文:
554
+ > `oldStr: " \" 的内容\"\n ]"`
555
+
556
+ ### 示例 3:给文本添加超链接
557
+
558
+ **read-rem 返回**(部分):
559
+
560
+ ```json
561
+ "text": [
562
+ "点击访问官网"
563
+ ],
564
+ ```
565
+
566
+ **edit-rem 调用**:
567
+
568
+ ```
569
+ oldStr: "\"text\": [\n \"点击访问官网\"\n ]"
570
+
571
+ newStr: "\"text\": [\n \"点击\",\n {\n \"i\": \"m\",\n \"iUrl\": \"https://remnote.com\",\n \"text\": \"访问官网\"\n }\n ]"
572
+ ```
573
+
574
+ ### 示例 4:修改高亮颜色(Rem 级别 vs RichText 级别)
575
+
576
+ **Rem 级别的 `highlightColor`**(整行背景色,值为英文字符串):
577
+
578
+ ```
579
+ oldStr: "\"highlightColor\": null"
580
+ newStr: "\"highlightColor\": \"Red\""
581
+ ```
582
+
583
+ **RichText 级别的 `h`**(行内文字高亮,值为数字 0-9):
584
+
585
+ ```
586
+ oldStr: "\"text\": [\n \"普通文本\"\n ]"
587
+ newStr: "\"text\": [\n {\n \"h\": 1,\n \"i\": \"m\",\n \"text\": \"红色高亮文本\"\n }\n ]"
588
+ ```
589
+
590
+ > **区分**:`highlightColor` 是 RemObject 顶层字段,值为字符串(`"Red"`, `"Blue"` 等);RichText 的 `h` 是行内格式标记,值为数字(1=Red, 2=Orange 等,见 RemColor 枚举)。
591
+
592
+ ### 示例 5:添加完形填空
593
+
594
+ **read-rem 返回**(部分):
595
+
596
+ ```json
597
+ "text": [
598
+ "光合作用需要阳光"
599
+ ],
600
+ ```
601
+
602
+ 将 "阳光" 变成完形填空:
603
+
604
+ ```
605
+ oldStr: "\"text\": [\n \"光合作用需要阳光\"\n ]"
606
+
607
+ newStr: "\"text\": [\n \"光合作用需要\",\n {\n \"cId\": \"cloze1\",\n \"i\": \"m\",\n \"text\": \"阳光\"\n }\n ]"
608
+ ```
609
+
610
+ ### 常见错误
611
+
612
+ 1. **忘记 key 字母序**:写 `{"text":"xx","i":"m","b":true}` 不会被匹配——实际存储为 `{"b":true,"i":"m","text":"xx"}`
613
+ 2. **缩进不匹配**:格式化 JSON 使用 2 空格缩进,数组内对象的 key 缩进 6 空格(顶层 2 + 数组 2 + 对象 2),但在 `JSON.stringify(obj, null, 2)` 中数组元素的对象缩进 4 空格(数组 2 + 对象内 2)
614
+ 3. **混淆 highlightColor 和 h**:前者是字符串 `"Red"`,后者是数字 `1`
615
+ 4. **忘记 `i:"a"` 的 `onlyAudio` 必填**:缺少此字段 SDK 拒绝写入
470
616
 
471
617
  ---
472
618
 
@@ -474,16 +474,58 @@ Portal:portalType [R], portalDirectlyIncludedRem [R]
474
474
  ]
475
475
  ```
476
476
 
477
- | `i` 值 | 类型 | 核心字段 |
478
- |:-------|:-----|:---------|
479
- | (纯 string) | 纯文本 | |
480
- | `"m"` | 带格式文本 | `text` + `b`/`l`/`u`/`h`/`code`/`cId`/`iUrl` |
481
- | `"q"` | Rem 引用 | `_id`(被引用 Rem ID) |
482
- | `"i"` | 图片 | `url`, `width`, `height` |
483
- | `"x"` | LaTeX | `text` |
484
- | `"a"` | 音频 | `url` |
485
-
486
- **序列化确定性**:RichText 对象内部按 key 字母序排列(`sortRichTextKeys()`),确保同一内容的 JSON 始终一致。这对 edit-rem str_replace 和并发检测至关重要。
477
+ #### 元素类型
478
+
479
+ | `i` | 类型 | 必填字段 | 可选字段 |
480
+ |:-------|:-----|:---------|:---------|
481
+ | (纯 string) | 纯文本 | | |
482
+ | `"m"` | 带格式文本 | `text` | 格式标记(见下表) |
483
+ | `"q"` | Rem 引用 | `_id` | `content`, `showFullName`, `aliasId` |
484
+ | `"i"` | 图片 | `url` | `width`, `height`, `percent`(25/50/100) |
485
+ | `"x"` | LaTeX | `text` | `block`(true=块级公式) |
486
+ | `"a"` | 音频/视频 | `url`, `onlyAudio`(**必填**) | `width`, `height` |
487
+ | `"s"` | 卡片分隔符 | — | `delimiterCharacterForSerialization` |
488
+
489
+ **注意**:`i:"a"` 的 `onlyAudio` 是必填字段(`true`=音频,`false`=视频),缺少会导致 SDK 拒绝写入。
490
+
491
+ #### 格式标记(主要用于 `i:"m"`,但 `i:"q"` 等也支持)
492
+
493
+ | 字段 | 类型 | 含义 |
494
+ |:-----|:-----|:-----|
495
+ | `b` | `true` | 加粗 |
496
+ | `l` | `true` | 斜体(小写字母 L) |
497
+ | `u` | `true` | 下划线 |
498
+ | `h` | `number` | 高亮颜色(RemColor 枚举:1=Red, 2=Orange, 3=Yellow, 4=Green, 5=Purple, 6=Blue, 7=Gray, 8=Brown, 9=Pink) |
499
+ | `tc` | `number` | 文字颜色(同 RemColor 枚举) |
500
+ | `q` | `true` | 行内代码(红色等宽样式) |
501
+ | `code` | `true` | 代码块(带语言标签和复制按钮) |
502
+ | `language` | `string` | 代码块语言(如 `"javascript"`) |
503
+ | `cId` | `string` | 完形填空 ID |
504
+ | `hiddenCloze` | `true` | 完形填空隐藏状态 |
505
+ | `iUrl` | `string` | 外部超链接 URL(`url` 字段已废弃,必须用 `iUrl`) |
506
+ | `qId` | `string` | 行内引用链接的 Rem ID |
507
+
508
+ #### 常用构造示例
509
+
510
+ 以下为 `JSON.stringify(null, 2)` 格式化后的 key 字母序排列:
511
+
512
+ ```jsonc
513
+ { "b": true, "i": "m", "text": "粗体" } // 粗体
514
+ { "i": "m", "q": true, "text": "code" } // 行内代码
515
+ { "i": "m", "iUrl": "https://...", "text": "链接" } // 超链接
516
+ { "b": true, "h": 1, "i": "m", "text": "重点" } // 粗体+红色高亮(h 是数字)
517
+ { "cId": "c1", "i": "m", "text": "答案" } // 完形填空
518
+ { "_id": "remId", "b": true, "i": "q" } // Rem 引用加粗(_id 排最前)
519
+ { "i": "x", "text": "E = mc^2" } // LaTeX
520
+ { "i": "a", "onlyAudio": false, "url": "..." } // 视频(onlyAudio 必填!)
521
+ { "i": "a", "onlyAudio": true, "url": "..." } // 音频
522
+ ```
523
+
524
+ > 在 RemObject 格式化 JSON 中,数组内对象展开为多行。构造 edit-rem 的 oldStr/newStr 必须用多行格式。
525
+
526
+ **highlightColor vs h**:`highlightColor` 是 RemObject 顶层字段(字符串如 `"Red"`),`h` 是 RichText 行内格式标记(数字 0-9)。两者独立。
527
+
528
+ **序列化确定性**:RichText 对象内部按 key 字母序排列(`sortRichTextKeys()`)。`_id` 的 `_`(U+005F)排在所有小写字母之前。这对 edit-rem 的 str_replace 和并发检测至关重要。
487
529
 
488
530
  ---
489
531
 
@@ -184,21 +184,21 @@ RemObject 共 51 个字段,按读写权限分为三类:
184
184
 
185
185
  | 字段 | 类型 | 权限 | 说明 |
186
186
  |------|------|:----:|------|
187
- | `text` | `RichText` | RW | 正面文本(RichText 数组) |
188
- | `backText` | `RichText \| null` | RW | 背面文本。null=无背面;设值即产生闪卡正反面结构 |
187
+ | `text` | `RichText` | RW | 正面文本(RichText 数组)。UI:文本内容立即更新显示 |
188
+ | `backText` | `RichText \| null` | RW | 背面文本。null=无背面;设值即产生闪卡正反面结构。UI:显示为"正面 → 背面"箭头分隔格式 |
189
189
 
190
190
  ### 类型系统
191
191
 
192
192
  | 字段 | 类型 | 权限 | 说明 |
193
193
  |------|------|:----:|------|
194
194
  | `type` | `RemTypeValue` | RW | `concept` / `descriptor` / `default` / `portal` |
195
- | `isDocument` | `boolean` | RW | 是否作为独立文档页面。独立于 type |
195
+ | `isDocument` | `boolean` | RW | 是否作为独立文档页面。独立于 type。UI:bullet(•)变为文档图标,可独立打开 |
196
196
 
197
197
  ### 结构
198
198
 
199
199
  | 字段 | 类型 | 权限 | 说明 |
200
200
  |------|------|:----:|------|
201
- | `parent` | `string \| null` | RW | 父 Rem ID。null=顶级 |
201
+ | `parent` | `string \| null` | RW | 父 Rem ID。null=顶级。UI:Rem 从原位置消失,出现在新父级下 |
202
202
  | `children` | `string[]` | R | 子 Rem ID 有序数组 |
203
203
 
204
204
  ### 格式 / 显示
@@ -206,21 +206,21 @@ RemObject 共 51 个字段,按读写权限分为三类:
206
206
  | 字段 | 类型 | 权限 | 说明 |
207
207
  |------|------|:----:|------|
208
208
  | `fontSize` | `FontSize \| null` | RW | 标题大小:`H1` / `H2` / `H3`。null=普通 |
209
- | `highlightColor` | `HighlightColor \| null` | RW | 高亮颜色(9 种)。null=无高亮 |
209
+ | `highlightColor` | `HighlightColor \| null` | RW | 高亮颜色(9 种)。null=无高亮。UI:整行背景变为对应颜色,bullet 也着色 |
210
210
 
211
211
  ### 状态标记
212
212
 
213
213
  | 字段 | 类型 | 权限 | 说明 |
214
214
  |------|------|:----:|------|
215
- | `isTodo` | `boolean` | RW | 是否待办。设为 true 时自动初始化 todoStatus |
216
- | `todoStatus` | `TodoStatus \| null` | RW | `Finished` / `Unfinished`。需先 isTodo=true |
217
- | `isCode` | `boolean` | RW | 是否代码块 |
218
- | `isQuote` | `boolean` | RW | 是否引用块 |
219
- | `isListItem` | `boolean` | RW | 是否列表项(有序列表样式) |
220
- | `isCardItem` | `boolean` | RW | 是否卡片项(多行答案行标记) |
215
+ | `isTodo` | `boolean` | RW | 是否待办。设为 true 时自动初始化 todoStatus。UI:文本前出现空心 checkbox(☐) |
216
+ | `todoStatus` | `TodoStatus \| null` | RW | `Finished` / `Unfinished`。需先 isTodo=true。UI:Finished=蓝色已勾选(☑)+文本删除线 |
217
+ | `isCode` | `boolean` | RW | 是否代码块。UI:等宽字体、灰色背景、块级缩进 |
218
+ | `isQuote` | `boolean` | RW | 是否引用块。UI:左侧灰色竖线+背景浅灰(blockquote 样式) |
219
+ | `isListItem` | `boolean` | RW | 是否列表项。UI:bullet(•)变为数字编号"1."(有序列表) |
220
+ | `isCardItem` | `boolean` | RW | 是否卡片项(多行答案行标记)。UI:无明显变化,在 Card View 中生效 |
221
221
  | `isTable` | `boolean` | R | 是否表格(只读) |
222
- | `isSlot` | `boolean` | RW | 是否 Powerup 插槽。与 isProperty 底层相同 |
223
- | `isProperty` | `boolean` | RW | 是否 Tag 属性(表格列)。与 isSlot 底层相同 |
222
+ | `isSlot` | `boolean` | RW | 是否 Powerup 插槽。与 isProperty 底层相同。UI:bullet(•)变为方形图标(☐) |
223
+ | `isProperty` | `boolean` | RW | 是否 Tag 属性(表格列)。与 isSlot 底层相同。UI:bullet(•)变为方形图标(☐) |
224
224
 
225
225
  ### Powerup 系统标识
226
226
 
@@ -249,15 +249,15 @@ RemObject 共 51 个字段,按读写权限分为三类:
249
249
 
250
250
  | 字段 | 类型 | 权限 | 说明 |
251
251
  |------|------|:----:|------|
252
- | `enablePractice` | `boolean` | RW | 是否启用间隔重复练习 |
253
- | `practiceDirection` | `PracticeDirection` | RW | 练习方向:`forward` / `backward` / `both` / `none` |
252
+ | `enablePractice` | `boolean` | RW | 是否启用间隔重复练习。UI:无明显变化 |
253
+ | `practiceDirection` | `PracticeDirection` | RW | 练习方向:`forward` / `backward` / `both` / `none`。UI:无明显变化 |
254
254
 
255
255
  ### 关联 — 直接关系
256
256
 
257
257
  | 字段 | 类型 | 权限 | 说明 |
258
258
  |------|------|:----:|------|
259
- | `tags` | `string[]` | RW | 标签 Rem ID 数组。写入时使用 diff 机制(add/remove |
260
- | `sources` | `string[]` | RW | 来源 Rem ID 数组。写入时使用 diff 机制 |
259
+ | `tags` | `string[]` | RW | 标签 Rem ID 数组。写入时使用 diff 机制(add/remove)。UI:行右侧出现标签徽章 |
260
+ | `sources` | `string[]` | RW | 来源 Rem ID 数组。写入时使用 diff 机制。UI:Rem 下方出现灰色来源引用框 |
261
261
  | `aliases` | `string[]` | R | 别名 Rem ID 数组 |
262
262
 
263
263
  ### 关联 — 引用关系
@@ -290,7 +290,7 @@ RemObject 共 51 个字段,按读写权限分为三类:
290
290
 
291
291
  | 字段 | 类型 | 权限 | 说明 |
292
292
  |------|------|:----:|------|
293
- | `positionAmongstSiblings` | `number \| null` | RW | 在兄弟间的位置(0 起始) |
293
+ | `positionAmongstSiblings` | `number \| null` | RW | 在兄弟间的位置(0 起始)。UI:Rem 在父级子列表中的显示位置改变 |
294
294
  | `timesSelectedInSearch` | `number` | R-F | 搜索中被选次数 |
295
295
  | `lastTimeMovedTo` | `number` | R-F | 上次移动时间(毫秒时间戳) |
296
296
  | `schemaVersion` | `number` | R-F | Schema 版本号 |
@@ -394,38 +394,175 @@ localUpdatedAt, lastPracticed
394
394
 
395
395
  ---
396
396
 
397
+ ## 可编辑字段约束表
398
+
399
+ 以下为 20 个可编辑字段(RW)的写入约束:
400
+
401
+ | 字段 | SDK setter | 约束 / 特殊处理 |
402
+ |------|-----------|-----------------|
403
+ | `text` | `rem.setText()` | RichText 数组 |
404
+ | `backText` | `rem.setBackText()` | `null` → `setBackText([])`(清除背面);裸字符串自动包装为 `[string]` |
405
+ | `type` | `rem.setType()` | `portal` 不可设置(只能通过 `createPortal()` 创建) |
406
+ | `isDocument` | `rem.setIsDocument()` | — |
407
+ | `parent` | `rem.setParent(parentId, position?)` | 与 `positionAmongstSiblings` 联动(见下方说明) |
408
+ | `fontSize` | `rem.setFontSize()` | `null` → `setFontSize(undefined)`(恢复普通大小) |
409
+ | `highlightColor` | `rem.setHighlightColor()` / `rem.removePowerup('h')` | `null` → `removePowerup('h')`(SDK 不接受 null) |
410
+ | `isTodo` | `rem.setIsTodo()` | 设为 true 时自动初始化 todoStatus |
411
+ | `todoStatus` | `rem.setTodoStatus()` | `null` → 跳过(清除 todo 应通过 `isTodo=false`) |
412
+ | `isCode` | `rem.setIsCode()` | — |
413
+ | `isQuote` | `rem.setIsQuote()` | — |
414
+ | `isListItem` | `rem.setIsListItem()` | — |
415
+ | `isCardItem` | `rem.setIsCardItem()` | — |
416
+ | `isSlot` | `rem.setIsSlot()` | 与 `isProperty` 底层相同 |
417
+ | `isProperty` | `rem.setIsProperty()` | 与 `isSlot` 底层相同 |
418
+ | `enablePractice` | `rem.setEnablePractice()` | — |
419
+ | `practiceDirection` | `rem.setPracticeDirection()` | `forward` / `backward` / `both` / `none` |
420
+ | `tags` | `rem.addTag()` / `rem.removeTag()` | **Diff 机制**:对比当前 vs 目标,增删差异项。必须列出完整目标数组,缺少的会被删除 |
421
+ | `sources` | `rem.addSource()` / `rem.removeSource()` | **Diff 机制**:同 tags |
422
+ | `positionAmongstSiblings` | `rem.setParent(parent, position)` | 与 `parent` 联动(见下方说明) |
423
+
424
+ ### parent + positionAmongstSiblings 联动
425
+
426
+ 这两个字段通过同一个 SDK 调用 `rem.setParent(parentId, position)` 写入:
427
+
428
+ | 场景 | 行为 |
429
+ |------|------|
430
+ | 两个字段都变更 | 合并为一次 `setParent(newParent, newPosition)` 调用 |
431
+ | 只有 `parent` 变更 | `setParent(newParent)` 不带 position(保持末尾) |
432
+ | 只有 `positionAmongstSiblings` 变更 | 获取当前 parent → `setParent(currentParent, newPosition)` |
433
+
434
+ **应在同一次 str_replace 中同时修改这两个字段。**
435
+
436
+ ---
437
+
397
438
  ## RichText 格式
398
439
 
399
440
  RemObject 中的 `text` 和 `backText` 字段使用 RichText 格式——一个 JSON 数组,每个元素为纯字符串或带 `i` 字段的格式化对象。
400
441
 
401
442
  ### 元素类型
402
443
 
403
- | `i` 值 | 含义 | 核心字段 |
404
- |--------|------|----------|
405
- | (纯 string) | 纯文本片段 | — |
406
- | `"m"` | 带格式文本 | `text` + 格式标记 |
407
- | `"q"` | Rem 引用 | `_id`(被引用 Rem ID) |
408
- | `"i"` | 图片 | `url`, `width`, `height` |
409
- | `"x"` | LaTeX | `text` |
410
- | `"a"` | 音频 | `url` |
444
+ | `i` 值 | 含义 | 必填字段 | 可选字段 |
445
+ |--------|------|----------|----------|
446
+ | (纯 string) | 纯文本片段 | — | — |
447
+ | `"m"` | 带格式文本 | `text` | 格式标记(见下表) |
448
+ | `"q"` | Rem 引用 | `_id`(被引用 Rem ID) | `content`, `showFullName`, `aliasId` |
449
+ | `"i"` | 图片 | `url` | `width`, `height`, `percent`(25/50/100) |
450
+ | `"x"` | LaTeX | `text` | `block`(true=块级公式) |
451
+ | `"a"` | 音频/视频 | `url`, `onlyAudio`(**必填**) | `width`, `height` |
452
+ | `"s"` | 卡片分隔符 | — | `delimiterCharacterForSerialization` |
411
453
 
412
- ### 行内格式标记(`i:"m"` 元素内)
454
+ **注意**:`i:"a"` 的 `onlyAudio` 是**必填**字段(`true`=音频,`false`=视频),缺少会导致 SDK 拒绝写入(`Invalid input`)。
455
+
456
+ ### 格式标记(主要用于 `i:"m"`,但 `i:"q"` 等元素也支持)
413
457
 
414
458
  | 字段 | 类型 | 含义 |
415
459
  |------|------|------|
416
460
  | `b` | `true` | 加粗 |
417
- | `l` | `true` | 斜体 |
461
+ | `l` | `true` | 斜体(小写字母 L,不是 I) |
418
462
  | `u` | `true` | 下划线 |
419
- | `h` | `number` | 高亮颜色(1=红, 2=橙, 3=黄, 4=绿, 5=蓝, 6=紫) |
420
- | `tc` | `number` | 文字颜色 |
421
- | `code` | `true` | 行内代码 |
463
+ | `h` | `number` | 高亮颜色(见 RemColor 枚举) |
464
+ | `tc` | `number` | 文字颜色(见 RemColor 枚举) |
465
+ | `q` | `true` | 行内代码(红色等宽样式) |
466
+ | `code` | `true` | 代码块(带语言标签和复制按钮) |
467
+ | `language` | `string` | 代码块语言(如 `"javascript"`、`"python"`) |
422
468
  | `cId` | `string` | 完形填空 ID |
423
- | `iUrl` | `string` | 外部链接 URL |
469
+ | `hiddenCloze` | `true` | 完形填空隐藏状态 |
470
+ | `revealedCloze` | `true` | 完形填空已揭示状态 |
471
+ | `iUrl` | `string` | 外部超链接 URL(**注意**:`url` 字段已废弃无效,必须用 `iUrl`) |
472
+ | `qId` | `string` | 行内引用链接的 Rem ID |
473
+
474
+ ### RemColor 颜色枚举(`h` 和 `tc` 共用)
475
+
476
+ | 值 | 颜色 | 值 | 颜色 | 值 | 颜色 |
477
+ |:---|:-----|:---|:-----|:---|:-----|
478
+ | 0 | 无颜色/默认 | 4 | Green | 7 | Gray |
479
+ | 1 | Red | 5 | Purple | 8 | Brown |
480
+ | 2 | Orange | 6 | Blue | 9 | Pink |
481
+ | 3 | Yellow | — | — | — | — |
482
+
483
+ ### 常用构造示例
484
+
485
+ 以下示例展示实际 `JSON.stringify(null, 2)` 格式化后的样子(key 按字母序排列):
486
+
487
+ ```jsonc
488
+ // 粗体
489
+ {
490
+ "b": true,
491
+ "i": "m",
492
+ "text": "粗体"
493
+ }
494
+ // 行内代码
495
+ {
496
+ "i": "m",
497
+ "q": true,
498
+ "text": "console.log()"
499
+ }
500
+ // 超链接
501
+ {
502
+ "i": "m",
503
+ "iUrl": "https://example.com",
504
+ "text": "点击访问"
505
+ }
506
+ // 红色高亮 + 粗体(h 是 RichText 行内格式标记,值为数字)
507
+ {
508
+ "b": true,
509
+ "h": 1,
510
+ "i": "m",
511
+ "text": "重点"
512
+ }
513
+ // 完形填空
514
+ {
515
+ "cId": "cloze1",
516
+ "i": "m",
517
+ "text": "答案内容"
518
+ }
519
+ // Rem 引用(_id 排在小写字母之前)
520
+ {
521
+ "_id": "remId",
522
+ "i": "q"
523
+ }
524
+ // Rem 引用 + 加粗
525
+ {
526
+ "_id": "remId",
527
+ "b": true,
528
+ "i": "q"
529
+ }
530
+ // LaTeX 块级公式
531
+ {
532
+ "block": true,
533
+ "i": "x",
534
+ "text": "\\frac{a}{b}"
535
+ }
536
+ // 视频(onlyAudio 必填!)
537
+ {
538
+ "i": "a",
539
+ "onlyAudio": false,
540
+ "url": "https://youtube.com/watch?v=xxx"
541
+ }
542
+ // 音频
543
+ {
544
+ "i": "a",
545
+ "onlyAudio": true,
546
+ "url": "https://example.com/audio.mp3"
547
+ }
548
+ ```
549
+
550
+ ### highlightColor(Rem 级别)vs h(RichText 行内格式)
551
+
552
+ | 属性 | 位置 | 值类型 | 作用范围 | 示例 |
553
+ |------|------|--------|----------|------|
554
+ | `highlightColor` | RemObject 顶层字段 | 字符串(`"Red"`, `"Blue"` 等)或 `null` | 整个 Rem(整行背景色) | `"highlightColor": "Red"` |
555
+ | `h` | RichText 元素内的格式标记 | 数字(0-9,见 RemColor 枚举) | 行内文字片段 | `{"h": 1, "i": "m", "text": "高亮"}` |
556
+
557
+ 两者完全独立。`highlightColor` 通过 `rem.setHighlightColor()` / `rem.removePowerup('h')` 写入;`h` 是 RichText JSON 内的字段,通过 `rem.setText()` 写入。
424
558
 
425
559
  ### 序列化确定性
426
560
 
427
561
  RichText 对象元素内部按 **key 字母序排列**(Plugin 端 `sortRichTextKeys()` 处理),确保同一内容的序列化 JSON 始终一致。这对 `edit-rem` 的 str_replace 和乐观并发检测至关重要。
428
562
 
563
+ - `_`(下划线)在 Unicode 中排在所有小写字母之前(`_` = U+005F,`a` = U+0061),所以 `_id` 总是排在第一位
564
+ - 排序由 `Object.keys().sort()` 决定,即 JavaScript 默认的 Unicode 字典序
565
+
429
566
  ---
430
567
 
431
568
  ## Powerup 过滤