remnote-bridge 0.1.1 → 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
@@ -10,6 +10,37 @@ Bridge toolkit that exposes your RemNote knowledge base to AI agents. Single pac
10
10
  npm install -g remnote-bridge
11
11
  ```
12
12
 
13
+ ## Super Quick Start (with AI)
14
+
15
+ One step to connect, then let AI guide you through the rest.
16
+
17
+ ### Option A: Install Skill (works with Claude Code, Cursor, Windsurf, and 40+ tools)
18
+
19
+ ```bash
20
+ npx skills add baobao700508/unofficial-remnote-bridge-cli -s remnote-bridge
21
+ ```
22
+
23
+ ### Option B: Configure MCP Server (for any MCP-compatible AI client)
24
+
25
+ Add the following to your AI client's MCP settings:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "remnote-bridge": {
31
+ "command": "remnote-bridge",
32
+ "args": ["mcp"]
33
+ }
34
+ }
35
+ }
36
+ ```
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
+
40
+ Once connected, the AI will guide you through connecting to RemNote, loading the plugin, and everything else.
41
+
42
+ ---
43
+
13
44
  ## Quick Start
14
45
 
15
46
  ```bash
@@ -70,7 +101,7 @@ remnote-bridge disconnect
70
101
  | Command | Description |
71
102
  |:--------|:------------|
72
103
  | `mcp` | Start the MCP Server (stdio transport) |
73
- | `install skill` | Install Claude Code skill to `~/.claude/skills/remnote-bridge/` |
104
+ | `install skill` | Install AI agent skill (via [Vercel Skills](https://github.com/vercel-labs/skills)) |
74
105
 
75
106
  ## MCP Server
76
107
 
@@ -89,13 +120,46 @@ Use `remnote-bridge mcp` as an MCP server for AI clients:
89
120
 
90
121
  The MCP server exposes all CLI commands as tools, plus documentation resources.
91
122
 
92
- ## Claude Code Skill
123
+ ## AI Agent Skill
124
+
125
+ The Skill provides detailed instructions (SKILL.md + 11 command docs) that teach AI agents how to use remnote-bridge — including command selection, workflows, safety rules, and flashcard operations.
126
+
127
+ ### Install via Vercel Skills (recommended)
128
+
129
+ Powered by the [Vercel Skills](https://github.com/vercel-labs/skills) ecosystem. Supports **40+ AI coding tools** including Claude Code, Cursor, Windsurf, GitHub Copilot, Cline, and more.
93
130
 
94
131
  ```bash
132
+ # Direct — interactive agent selection
133
+ npx skills add baobao700508/unofficial-remnote-bridge-cli -s remnote-bridge
134
+
135
+ # Or through the built-in wrapper (same interactive experience)
95
136
  remnote-bridge install skill
96
137
  ```
97
138
 
98
- Installs the skill to `~/.claude/skills/remnote-bridge/`, enabling Claude Code to operate your RemNote knowledge base through natural language.
139
+ The interactive installer will detect your installed AI tools and let you choose which ones to install the skill for.
140
+
141
+ ### Fallback: Claude Code only
142
+
143
+ If `npx` is not available, or you prefer manual installation:
144
+
145
+ ```bash
146
+ remnote-bridge install skill --copy
147
+ ```
148
+
149
+ This copies the skill files directly to `~/.claude/skills/remnote-bridge/`.
150
+
151
+ ### What gets installed
152
+
153
+ ```
154
+ <agent-skills-dir>/remnote-bridge/
155
+ ├── SKILL.md # Core skill — command decisions, workflows, safety rules
156
+ └── instructions/ # Detailed per-command documentation
157
+ ├── overall.md # Global overview
158
+ ├── connect.md # connect command
159
+ ├── read-tree.md # read-tree command
160
+ ├── edit-tree.md # edit-tree command
161
+ └── ... # 8 more command docs
162
+ ```
99
163
 
100
164
  ## JSON Mode
101
165
 
@@ -1,16 +1,57 @@
1
1
  /**
2
2
  * install skill 命令
3
3
  *
4
- * SKILL.md docs/instruction/*.md 安装到 ~/.claude/skills/remnote-bridge/
4
+ * 薄层封装 Vercel Skills CLI(npx skills add),利用其交互式选择让用户适配不同 AI 编程工具。
5
+ * npx 不可用时 fallback 到直接复制(仅 Claude Code)。
5
6
  */
6
7
  import fs from 'fs';
7
8
  import path from 'path';
8
9
  import os from 'os';
10
+ import { spawn, execSync } from 'child_process';
11
+ const GITHUB_REPO = 'baobao700508/unofficial-remnote-bridge-cli';
12
+ const SKILL_NAME = 'remnote-bridge';
9
13
  export async function installSkillCommand() {
14
+ // 检测 npx 是否可用
15
+ let hasNpx = false;
16
+ try {
17
+ execSync('npx --version', { stdio: 'pipe', timeout: 5000 });
18
+ hasNpx = true;
19
+ }
20
+ catch {
21
+ // npx 不可用
22
+ }
23
+ if (!hasNpx) {
24
+ console.log('未检测到 npx,使用内置方式安装到 Claude Code...\n');
25
+ copySkillFiles();
26
+ return;
27
+ }
28
+ // 直接调用 npx skills add,继承 stdio 让用户看到交互式选择界面
29
+ console.log(`通过 Vercel Skills CLI 安装 ${SKILL_NAME}...\n`);
30
+ const child = spawn('npx', ['skills', 'add', GITHUB_REPO, '-s', SKILL_NAME], {
31
+ stdio: 'inherit',
32
+ shell: true,
33
+ });
34
+ await new Promise((resolve) => {
35
+ child.on('close', (code) => {
36
+ if (code !== 0) {
37
+ console.log(`\nVercel Skills CLI 退出码: ${code}`);
38
+ console.log('如需使用内置方式安装(仅 Claude Code),请运行:');
39
+ console.log(' remnote-bridge install skill --copy\n');
40
+ process.exitCode = 1;
41
+ }
42
+ resolve();
43
+ });
44
+ });
45
+ }
46
+ export async function installSkillCopyCommand() {
47
+ copySkillFiles();
48
+ }
49
+ function copySkillFiles() {
10
50
  // 从包安装路径计算包根(dist/cli/commands/install-skill.js → 包根)
11
51
  const packageRoot = path.resolve(import.meta.dirname, '..', '..', '..');
12
- const skillSource = path.join(packageRoot, 'skill', 'SKILL.md');
13
- const instructionDir = path.join(packageRoot, 'docs', 'instruction');
52
+ const skillDir = path.join(packageRoot, 'skills', 'remnote-bridge');
53
+ const skillSource = path.join(skillDir, 'SKILL.md');
54
+ const instructionDir = path.join(skillDir, 'instructions');
14
55
  if (!fs.existsSync(skillSource)) {
15
56
  console.error(`错误: 找不到 SKILL.md: ${skillSource}`);
16
57
  process.exitCode = 1;
@@ -19,15 +60,12 @@ export async function installSkillCommand() {
19
60
  // 目标目录
20
61
  const targetDir = path.join(os.homedir(), '.claude', 'skills', 'remnote-bridge');
21
62
  fs.mkdirSync(targetDir, { recursive: true });
22
- // 复制 SKILL.md,更新 instruction 路径引用
23
- let skillContent = fs.readFileSync(skillSource, 'utf-8');
24
- const targetInstructionDir = path.join(targetDir, 'instruction');
25
- // 替换相对路径 docs/instruction/ 为安装后的绝对路径
26
- skillContent = skillContent.replace(/docs\/instruction\//g, targetInstructionDir + '/');
27
- fs.writeFileSync(path.join(targetDir, 'SKILL.md'), skillContent, 'utf-8');
63
+ // 复制 SKILL.md
64
+ fs.copyFileSync(skillSource, path.join(targetDir, 'SKILL.md'));
28
65
  console.log(` SKILL.md → ${targetDir}/SKILL.md`);
29
- // 复制 docs/instruction/*.md
66
+ // 复制 instructions/*.md
30
67
  if (fs.existsSync(instructionDir)) {
68
+ const targetInstructionDir = path.join(targetDir, 'instructions');
31
69
  fs.mkdirSync(targetInstructionDir, { recursive: true });
32
70
  const files = fs.readdirSync(instructionDir).filter(f => f.endsWith('.md'));
33
71
  for (const file of files) {
@@ -35,5 +73,7 @@ export async function installSkillCommand() {
35
73
  console.log(` ${file} → ${targetInstructionDir}/${file}`);
36
74
  }
37
75
  }
38
- console.log(`\nSkill 已安装到 ${targetDir}`);
76
+ console.log(`\nSkill 已安装到 ${targetDir}(仅 Claude Code)`);
77
+ console.log('如需安装到其他 AI 工具,请运行:');
78
+ console.log(` npx skills add ${GITHUB_REPO} -s ${SKILL_NAME}\n`);
39
79
  }
package/dist/cli/main.js CHANGED
@@ -15,7 +15,7 @@ import { editTreeCommand } from './commands/edit-tree.js';
15
15
  import { readGlobeCommand } from './commands/read-globe.js';
16
16
  import { readContextCommand } from './commands/read-context.js';
17
17
  import { searchCommand } from './commands/search.js';
18
- import { installSkillCommand } from './commands/install-skill.js';
18
+ import { installSkillCommand, installSkillCopyCommand } from './commands/install-skill.js';
19
19
  const program = new Command();
20
20
  /**
21
21
  * --json 模式下解析 JSON 输入参数。
@@ -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.1')
56
+ .version('0.1.3')
57
57
  .option('--json', '以 JSON 格式输出(适用于程序化调用)');
58
58
  program
59
59
  .command('connect')
@@ -295,6 +295,14 @@ program.command('mcp')
295
295
  // install 子命令组
296
296
  const installCmd = program.command('install').description('安装组件');
297
297
  installCmd.command('skill')
298
- .description('安装 Skill ~/.claude/skills/remnote-bridge/')
299
- .action(async () => { await installSkillCommand(); });
298
+ .description('安装 Skill(推荐使用 npx skills add,或 --copy 直接复制)')
299
+ .option('--copy', '直接复制到 ~/.claude/skills/(不使用 Vercel Skills CLI)')
300
+ .action(async (opts) => {
301
+ if (opts.copy) {
302
+ await installSkillCopyCommand();
303
+ }
304
+ else {
305
+ await installSkillCommand();
306
+ }
307
+ });
300
308
  program.parse();
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.1',
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,25 +25,35 @@ 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
 
32
- ### Separator 与闪卡创建
32
+ ### 闪卡的 CLI 操作方式
33
33
 
34
- 分隔符决定 Rem 的 typebackText 和练习方向。创建闪卡的本质是设置正确的分隔符:
34
+ 闪卡由 Rem 的 \\\`type\\\`、\\\`backText\\\`、\\\`practiceDirection\\\` 三个字段控制。通过 CLI 创建/修改闪卡,操作的是这些**字段**,而非分隔符。
35
35
 
36
- | 分隔符 | type | 用途 |
37
- |:-------|:-----|:-----|
38
- | \\\`::\\\` | concept | 概念定义(双向) |
39
- | \\\`;;\\\` | descriptor | 描述属性(正向) |
40
- | \\\`>>\\\` | default | 正向问答 |
41
- | \\\`<<\\\` | default | 反向问答 |
42
- | \\\`<>\\\` | default | 双向问答 |
43
- | \\\`>>>\\\` | default | 多行答案(子 Rem 为答案) |
44
- | \\\`{{}}\\\` | default | 完形填空 |
36
+ **禁止**:在文本中插入分隔符(\\\`::\\\`、\\\`;;\\\`、\\\`>>\\\` 等)来创建闪卡。分隔符是 RemNote 编辑器的输入语法,CLI 无法识别。
45
37
 
46
- 完整分隔符-闪卡映射表见 \\\`resource://separator-flashcard\\\`。
38
+ | 闪卡操作 | CLI 方法 |
39
+ |:---------|:---------|
40
+ | 创建概念定义 | \\\`edit_tree\\\` 新增行 \\\`概念 ↔ 定义\\\`,再 \\\`edit_rem\\\` 设 \\\`type: "concept"\\\` |
41
+ | 创建正向问答 | \\\`edit_tree\\\` 新增行 \\\`问题 → 答案\\\` |
42
+ | 创建多行答案 | \\\`edit_tree\\\` 新增行 \\\`问题 ↓\\\`(子行自动成为答案) |
43
+ | 改变闪卡类型 | \\\`edit_rem\\\` 修改 \\\`type\\\`、\\\`backText\\\`、\\\`practiceDirection\\\` |
44
+
45
+ ### 理解用户意图:分隔符映射
46
+
47
+ 用户在 RemNote 编辑器中通过分隔符创建闪卡。当用户提到这些分隔符时,你需要理解其意图并映射到 CLI 操作:
48
+
49
+ | 用户说 / 编辑器分隔符 | 对应的 type | 对应的 practiceDirection |
50
+ |:----------------------|:-----------|:-----------------------|
51
+ | \\\`::\\\` | concept | both |
52
+ | \\\`;;\\\` | descriptor | forward |
53
+ | \\\`>>\\\` / \\\`<<\\\` / \\\`<>\\\` | default | forward / backward / both |
54
+ | \\\`>>>\\\` / \\\`::>\\\` / \\\`;;>\\\` | default / concept / descriptor | forward / both / forward(多行) |
55
+
56
+ 完整映射表见 \\\`resource://separator-flashcard\\\`。
47
57
 
48
58
  ### 链接机制
49
59
 
@@ -66,6 +76,8 @@ RemNote 的格式设置(标题、高亮、代码等)会注入隐藏的系统
66
76
  \\\`\\\`\\\`
67
77
  connect → 启动 daemon(幂等,重复调用安全)
68
78
 
79
+ ⚠️ 用户操作:确保 RemNote 中已加载插件(见下方说明)
80
+
69
81
  health → 确认三层就绪(daemon / Plugin / SDK)
70
82
 
71
83
  业务操作(read / search / edit)
@@ -79,6 +91,25 @@ disconnect → 关闭 daemon,清空所有缓存
79
91
  - \\\`disconnect\\\` 会销毁所有缓存,之前的 read 结果全部失效
80
92
  - daemon 默认 30 分钟无活动自动关闭
81
93
 
94
+ ### ⚠️ connect 后需要用户配合(重要)
95
+
96
+ \\\`connect\\\` 成功只意味着 daemon 和 webpack-dev-server 已启动,**Plugin 并未自动连接**。用户必须在 RemNote 中完成以下操作,Plugin 才能连接到 daemon:
97
+
98
+ **首次使用**(RemNote 从未加载过此插件):
99
+ 1. 打开 RemNote 桌面端或网页端
100
+ 2. 点击左侧边栏底部的插件图标(拼图形状)
101
+ 3. 点击「开发你的插件」(Develop Your Plugin)
102
+ 4. 在输入框中填入 \\\`http://localhost:8080\\\`(即 connect 输出的 webpack-dev-server 地址)
103
+ 5. 等待插件加载完成
104
+
105
+ **非首次使用**(之前已加载过此插件):
106
+ - 只需**刷新 RemNote 页面**即可(浏览器 F5 或 Cmd+R),插件会自动重新连接
107
+
108
+ **你必须这样做**:
109
+ 1. 执行 \\\`connect\\\` 后,**立即告知用户需要完成上述操作**
110
+ 2. **不要在 connect 后直接调用业务命令**——此时 Plugin 尚未连接,命令会失败
111
+ 3. 引导用户完成操作后,用 \\\`health\\\` 确认三层就绪,再执行业务命令
112
+
82
113
  ---
83
114
 
84
115
  ## 3. Common Scenarios
@@ -119,7 +150,35 @@ disconnect → 关闭 daemon,清空所有缓存
119
150
  2. 在返回的 JSON 文本中定位要修改的部分
120
151
  3. \\\`edit_rem\\\` 用 str_replace 替换:oldStr 精确匹配原文,newStr 是修改后的文本
121
152
 
122
- 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 前置 |
123
182
 
124
183
  ### 场景 E:修改结构(新增/删除/移动/重排)
125
184
 
@@ -136,26 +195,32 @@ str_replace 操作的是格式化 JSON 文本(2 空格缩进)。oldStr 要
136
195
 
137
196
  **红线**:edit_tree **禁止修改已有行的文字内容**——改内容必须用 edit_rem。edit_tree 只做结构操作。
138
197
 
139
- ### 场景 F:创建闪卡
198
+ ### 场景 F:创建 / 修改闪卡
140
199
 
141
- > 用户说:"创建一个概念定义"、"做个正向问答卡"
200
+ > 用户说:"创建一个概念定义"、"做个正向问答卡"、"把这个改成 concept"
142
201
 
143
- 创建闪卡的本质是创建带正确分隔符的 Rem。通过 \\\`edit_tree\\\` 新增行时使用箭头分隔符:
202
+ 闪卡由 \\\`type\\\`、\\\`backText\\\`、\\\`practiceDirection\\\` 三个字段控制。操作方式:
144
203
 
145
- - 概念定义:\\\`新概念 定义内容\\\`(然后用 edit_rem 将 type 改为 concept)
204
+ **创建新闪卡**(\\\`edit_tree\\\` 新增行 + 箭头):
146
205
  - 正向问答:\\\`问题 → 答案\\\`
147
- - 多行答案:\\\`问题 ↓\\\`(子行自动成为答案)
148
206
  - 双向问答:\\\`问题 ↔ 答案\\\`
207
+ - 多行答案:\\\`问题 ↓\\\`(子行自动成为答案)
208
+ - 概念定义:\\\`概念 ↔ 定义\\\`,再用 \\\`edit_rem\\\` 设 \\\`type: "concept"\\\`
209
+
210
+ **修改现有 Rem 的闪卡行为**(\\\`read_rem\\\` → \\\`edit_rem\\\`):
211
+ - 改类型:修改 \\\`type\\\` 字段(\\\`"default"\\\` → \\\`"concept"\\\`)
212
+ - 改方向:修改 \\\`practiceDirection\\\`(\\\`"forward"\\\` / \\\`"backward"\\\` / \\\`"both"\\\` / \\\`"none"\\\`)
213
+ - 加/改背面:修改 \\\`backText\\\` 字段
149
214
 
150
- 如果要修改现有 Rem 的闪卡行为(改分隔符类型),使用 \\\`read_rem\\\` \\\`edit_rem\\\` 修改 type、backText、practiceDirection 等字段。
215
+ **禁止**:在文本内容中插入 \\\`::\\\`、\\\`;;\\\`、\\\`>>\\\` 等分隔符——这些是 RemNote 编辑器语法,CLI 不识别。
151
216
 
152
217
  ### 场景 G:排查连接问题
153
218
 
154
219
  > 用户说:"连不上"、"命令报错了"
155
220
 
156
221
  使用 \\\`health\\\` 检查三层状态,然后对症处理:
157
- - daemon 未运行 → 执行 \\\`connect\\\`
158
- - Plugin 未连接 → 提醒用户打开 RemNote 并确认插件已加载
222
+ - daemon 未运行 → 执行 \\\`connect\\\`,然后引导用户在 RemNote 中加载插件
223
+ - Plugin 未连接 → 提醒用户:首次使用需在 RemNote 的「开发你的插件」中填入 \\\`http://localhost:8080\\\`;非首次使用只需刷新 RemNote 页面
159
224
  - SDK 未就绪 → 等待几秒后重试 health
160
225
 
161
226
  ---
@@ -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
+ `;