remnote-bridge 0.1.11 → 0.1.13
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/addon/addon-manager.js +163 -0
- package/dist/cli/addon/registry.js +24 -0
- package/dist/cli/commands/addon.js +149 -0
- package/dist/cli/commands/clean.js +121 -52
- package/dist/cli/commands/connect.js +72 -33
- package/dist/cli/commands/disconnect.js +19 -19
- package/dist/cli/commands/edit-rem.js +8 -36
- package/dist/cli/commands/edit-tree.js +3 -20
- package/dist/cli/commands/health.js +19 -18
- package/dist/cli/commands/read-context.js +3 -20
- package/dist/cli/commands/read-globe.js +3 -20
- package/dist/cli/commands/read-rem.js +6 -32
- package/dist/cli/commands/read-tree.js +3 -20
- package/dist/cli/commands/search.js +97 -21
- package/dist/cli/config.js +148 -72
- package/dist/cli/daemon/daemon.js +104 -24
- package/dist/cli/daemon/dev-server.js +9 -1
- package/dist/cli/daemon/pid.js +36 -22
- package/dist/cli/daemon/registry.js +160 -0
- package/dist/cli/daemon/send-request.js +11 -11
- package/dist/cli/daemon/static-server.js +97 -34
- package/dist/cli/handlers/edit-handler.js +49 -140
- package/dist/cli/handlers/read-handler.js +9 -9
- package/dist/cli/handlers/rem-cache.js +10 -5
- package/dist/cli/handlers/tree-parser.js +16 -9
- package/dist/cli/main.js +67 -19
- package/dist/cli/protocol.js +18 -4
- package/dist/cli/server/config-server.js +280 -14
- package/dist/cli/server/ws-server.js +93 -44
- package/dist/cli/utils/output.js +29 -0
- package/dist/mcp/format.js +43 -0
- package/dist/mcp/index.js +0 -55
- package/dist/mcp/instructions.js +424 -216
- package/dist/mcp/resources/edit-rem-guide.js +37 -158
- package/dist/mcp/resources/edit-tree-guide.js +1 -1
- package/dist/mcp/resources/error-reference.js +9 -13
- package/dist/mcp/resources/rem-object-fields.js +6 -6
- package/dist/mcp/tools/edit-tools.js +69 -8
- package/dist/mcp/tools/infra-tools.js +44 -8
- package/dist/mcp/tools/read-tools.js +136 -20
- package/package.json +2 -2
- package/remnote-plugin/dist/bridge_widget-sandbox.js +17 -17
- package/remnote-plugin/dist/bridge_widget.js +17 -17
- package/remnote-plugin/dist/index-sandbox.js +31 -31
- package/remnote-plugin/dist/index.js +31 -31
- package/remnote-plugin/dist/manifest.json +1 -1
- package/remnote-plugin/package.json +1 -1
- package/remnote-plugin/public/manifest.json +1 -1
- package/remnote-plugin/src/bridge/multi-connection-manager.ts +151 -0
- package/remnote-plugin/src/bridge/websocket-client.ts +62 -16
- package/remnote-plugin/src/services/index.ts +0 -8
- package/remnote-plugin/src/services/read-rem.ts +1 -9
- package/remnote-plugin/src/services/search.ts +13 -10
- package/remnote-plugin/src/settings.ts +9 -7
- package/remnote-plugin/src/utils/index.ts +0 -5
- package/remnote-plugin/src/widgets/bridge_widget.tsx +105 -20
- package/remnote-plugin/src/widgets/index.tsx +41 -44
- package/remnote-plugin/webpack.config.js +35 -0
- package/skills/remnote-bridge/SKILL.md +45 -40
- package/skills/remnote-bridge/instructions/addon.md +134 -0
- package/skills/remnote-bridge/instructions/clean.md +110 -0
- package/skills/remnote-bridge/instructions/connect.md +80 -37
- package/skills/remnote-bridge/instructions/disconnect.md +22 -9
- package/skills/remnote-bridge/instructions/edit-rem.md +113 -327
- package/skills/remnote-bridge/instructions/health.md +23 -13
- package/skills/remnote-bridge/instructions/install-skill.md +58 -0
- package/skills/remnote-bridge/instructions/overall.md +99 -35
- package/skills/remnote-bridge/instructions/read-rem.md +15 -15
- package/skills/remnote-bridge/instructions/search.md +77 -18
- package/skills/remnote-bridge/instructions/setup.md +5 -6
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# edit-rem
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 直接修改单个 Rem 的属性字段。两道防线确保编辑安全。
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
`edit-rem`
|
|
9
|
+
`edit-rem` 直接修改 Rem 的属性字段——通过 `changes` 对象指定要修改的字段及其新值,无需构造 str_replace。
|
|
10
10
|
|
|
11
11
|
核心特性:
|
|
12
|
-
-
|
|
13
|
-
-
|
|
12
|
+
- **直接字段修改**:传入 `{字段名: 新值}` 的 changes 对象
|
|
13
|
+
- **两道防线**:缓存存在性 → 乐观并发检测
|
|
14
|
+
- **字段白名单校验**:21 个可写字段通过,只读和未知字段产生警告
|
|
14
15
|
- **前置条件**:必须先 `read-rem` 建立缓存,否则防线 1 拒绝
|
|
15
16
|
|
|
16
17
|
---
|
|
@@ -33,14 +34,13 @@
|
|
|
33
34
|
### 人类模式
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
|
-
remnote-bridge edit-rem <remId> --
|
|
37
|
+
remnote-bridge edit-rem <remId> --changes '{"type":"concept"}'
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
| 参数/选项 | 类型 | 必需 | 说明 |
|
|
40
41
|
|-----------|------|:----:|------|
|
|
41
42
|
| `remId` | string(位置参数) | 是 | Rem ID |
|
|
42
|
-
| `--
|
|
43
|
-
| `--new-str <newStr>` | string | 是 | 替换后的新文本片段 |
|
|
43
|
+
| `--changes <changesJson>` | string | 是 | 要修改的字段及新值(JSON 字符串) |
|
|
44
44
|
|
|
45
45
|
输出示例(成功):
|
|
46
46
|
|
|
@@ -51,13 +51,13 @@ remnote-bridge edit-rem <remId> --old-str <oldStr> --new-str <newStr>
|
|
|
51
51
|
输出示例(无变更):
|
|
52
52
|
|
|
53
53
|
```
|
|
54
|
-
|
|
54
|
+
无变更(未发现可写入的变更字段)
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
### JSON 模式
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
remnote-bridge edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","
|
|
60
|
+
remnote-bridge edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","changes":{"type":"concept"}}'
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
---
|
|
@@ -67,8 +67,7 @@ remnote-bridge edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","oldStr":"\"concept
|
|
|
67
67
|
| 字段 | 类型 | 必需 | 说明 |
|
|
68
68
|
|------|------|:----:|------|
|
|
69
69
|
| `remId` | string | 是 | Rem ID |
|
|
70
|
-
| `
|
|
71
|
-
| `newStr` | string | 是 | 替换后的新文本片段 |
|
|
70
|
+
| `changes` | object | 是 | 要修改的字段及新值(键=字段名,值=新值) |
|
|
72
71
|
|
|
73
72
|
---
|
|
74
73
|
|
|
@@ -120,35 +119,13 @@ remnote-bridge edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","oldStr":"\"concept
|
|
|
120
119
|
}
|
|
121
120
|
```
|
|
122
121
|
|
|
123
|
-
###
|
|
122
|
+
### 枚举值非法
|
|
124
123
|
|
|
125
124
|
```json
|
|
126
125
|
{
|
|
127
126
|
"ok": false,
|
|
128
127
|
"command": "edit-rem",
|
|
129
|
-
"error": "
|
|
130
|
-
"timestamp": "2026-03-06T10:00:00.000Z"
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### 防线 3 拒绝:old_str 多次匹配
|
|
135
|
-
|
|
136
|
-
```json
|
|
137
|
-
{
|
|
138
|
-
"ok": false,
|
|
139
|
-
"command": "edit-rem",
|
|
140
|
-
"error": "old_str matches 3 locations in rem kLrIOHJLyMd8Y2lyA. Make old_str more specific to match exactly once.",
|
|
141
|
-
"timestamp": "2026-03-06T10:00:00.000Z"
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### 后处理:替换产生非法 JSON
|
|
146
|
-
|
|
147
|
-
```json
|
|
148
|
-
{
|
|
149
|
-
"ok": false,
|
|
150
|
-
"command": "edit-rem",
|
|
151
|
-
"error": "The replacement produced invalid JSON. Check your new_str for syntax errors.",
|
|
128
|
+
"error": "Invalid value for 'type': \"invalid\". Allowed: \"concept\", \"descriptor\", \"default\"",
|
|
152
129
|
"timestamp": "2026-03-06T10:00:00.000Z"
|
|
153
130
|
}
|
|
154
131
|
```
|
|
@@ -180,12 +157,24 @@ remnote-bridge edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","oldStr":"\"concept
|
|
|
180
157
|
}
|
|
181
158
|
```
|
|
182
159
|
|
|
160
|
+
### 含未知字段警告
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"ok": true,
|
|
165
|
+
"command": "edit-rem",
|
|
166
|
+
"changes": ["text"],
|
|
167
|
+
"warnings": ["Field 'fooBar' is unknown and was ignored"],
|
|
168
|
+
"timestamp": "2026-03-06T10:00:00.000Z"
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
183
172
|
---
|
|
184
173
|
|
|
185
174
|
## 内部流程
|
|
186
175
|
|
|
187
176
|
```
|
|
188
|
-
1. CLI 解析参数(remId,
|
|
177
|
+
1. CLI 解析参数(remId, changes)
|
|
189
178
|
2. sendRequest → WS → daemon
|
|
190
179
|
3. daemon EditHandler:
|
|
191
180
|
│
|
|
@@ -194,40 +183,32 @@ remnote-bridge edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","oldStr":"\"concept
|
|
|
194
183
|
│
|
|
195
184
|
├─ 防线 2: 乐观并发检测
|
|
196
185
|
│ ├─ forwardToPlugin('read_rem', { remId }) → 获取当前 RemObject
|
|
197
|
-
│ ├─ JSON.stringify
|
|
198
|
-
│
|
|
199
|
-
│
|
|
186
|
+
│ ├─ JSON.stringify 比较当前 vs 缓存
|
|
187
|
+
│ └─ 不匹配 → 抛出错误(不更新缓存)
|
|
188
|
+
│
|
|
189
|
+
├─ 遍历 changes keys
|
|
190
|
+
│ ├─ READ_ONLY_FIELDS → warnings
|
|
191
|
+
│ ├─ 不在 WRITABLE_FIELDS 中 → warnings
|
|
192
|
+
│ └─ 通过 → writableChanges
|
|
200
193
|
│
|
|
201
|
-
├─
|
|
202
|
-
│ ├─ 是 → 进入 Portal 专用路径(简化 JSON 上执行 str_replace)
|
|
203
|
-
│ └─ 否 → 继续普通 Rem 路径
|
|
194
|
+
├─ 枚举值范围校验(type, practiceDirection, highlightColor, fontSize, todoStatus)
|
|
204
195
|
│
|
|
205
|
-
├─
|
|
206
|
-
│ ├─ countOccurrences(cachedJson, oldStr)
|
|
207
|
-
│ ├─ 0 次 → 抛出"未找到"错误
|
|
208
|
-
│ ├─ >1 次 → 抛出"多次匹配"错误
|
|
209
|
-
│ └─ 恰好 1 次 → cachedJson.replace(oldStr, newStr)
|
|
196
|
+
├─ 语义校验:todoStatus 非 null 但 isTodo=false → 警告
|
|
210
197
|
│
|
|
211
|
-
├─
|
|
212
|
-
│ ├─ JSON.parse(modifiedJson) → 失败则抛"非法 JSON"错误
|
|
213
|
-
│ ├─ 逐字段对比 original vs modified
|
|
214
|
-
│ ├─ READ_ONLY_FIELDS → 产生警告,不执行写入
|
|
215
|
-
│ ├─ 语义校验:todoStatus 非 null 但 isTodo=false → 警告
|
|
216
|
-
│ └─ 无变更 → 返回 ok=true, changes=[]
|
|
198
|
+
├─ 空变更检查 → 直接返回 ok
|
|
217
199
|
│
|
|
218
200
|
├─ 发送到 Plugin
|
|
219
201
|
│ ├─ forwardToPlugin('write_rem_fields', { remId, changes })
|
|
220
|
-
│ ├─ Plugin 逐字段调用 SDK setter
|
|
221
202
|
│ └─ 首个失败即终止 → 返回 applied + failed
|
|
222
203
|
│
|
|
223
204
|
└─ 缓存更新
|
|
224
|
-
├─ 写入成功 → 从 Plugin re-read
|
|
225
|
-
└─ 写入失败 →
|
|
205
|
+
├─ 写入成功 → 从 Plugin re-read → 更新缓存
|
|
206
|
+
└─ 写入失败 → 不更新缓存
|
|
226
207
|
```
|
|
227
208
|
|
|
228
209
|
---
|
|
229
210
|
|
|
230
|
-
##
|
|
211
|
+
## 两道防线详解
|
|
231
212
|
|
|
232
213
|
### 防线 1:缓存存在性检查
|
|
233
214
|
|
|
@@ -251,6 +232,7 @@ if cache.get('rem:' + remId) === null:
|
|
|
251
232
|
```
|
|
252
233
|
currentRemObject = forwardToPlugin('read_rem', { remId })
|
|
253
234
|
currentJson = JSON.stringify(currentRemObject, null, 2)
|
|
235
|
+
cachedJson = JSON.stringify(cachedObj, null, 2)
|
|
254
236
|
|
|
255
237
|
if currentJson !== cachedJson:
|
|
256
238
|
// 不更新缓存 — 迫使 AI 重新 read 获取最新状态
|
|
@@ -258,61 +240,16 @@ if currentJson !== cachedJson:
|
|
|
258
240
|
```
|
|
259
241
|
|
|
260
242
|
**关键设计**:
|
|
261
|
-
-
|
|
243
|
+
- 比较方式:**将当前 RemObject 和缓存 RemObject 分别 JSON.stringify 后做文本比较**
|
|
262
244
|
- 失败时**不更新缓存**:防止 AI 跳过 re-read 直接重试
|
|
263
245
|
- RichText key 排序保证序列化确定性(`sortRichTextKeys()`)
|
|
264
246
|
|
|
265
247
|
**恢复方式**:执行 `read-rem <remId>` 获取最新状态后重试。
|
|
266
248
|
|
|
267
|
-
###
|
|
268
|
-
|
|
269
|
-
**目的**:确保替换精确定位到唯一位置,避免意外修改。
|
|
270
|
-
|
|
271
|
-
```
|
|
272
|
-
function countOccurrences(haystack, needle):
|
|
273
|
-
count = 0, pos = 0
|
|
274
|
-
while true:
|
|
275
|
-
pos = haystack.indexOf(needle, pos)
|
|
276
|
-
if pos === -1: break
|
|
277
|
-
count++
|
|
278
|
-
pos += needle.length // 非重叠匹配
|
|
279
|
-
return count
|
|
280
|
-
|
|
281
|
-
matchCount = countOccurrences(cachedJson, oldStr)
|
|
249
|
+
### 两道防线判断树
|
|
282
250
|
|
|
283
|
-
switch matchCount:
|
|
284
|
-
case 0: throw "old_str not found in the serialized JSON of rem {remId}"
|
|
285
|
-
case 1: modifiedJson = cachedJson.replace(oldStr, newStr) // 执行替换
|
|
286
|
-
default: throw "old_str matches {matchCount} locations in rem {remId}. Make old_str more specific to match exactly once."
|
|
287
251
|
```
|
|
288
|
-
|
|
289
|
-
**后处理校验**:
|
|
290
|
-
|
|
291
|
-
```
|
|
292
|
-
// 1. JSON 语法检查
|
|
293
|
-
modified = JSON.parse(modifiedJson)
|
|
294
|
-
// 失败 → throw "The replacement produced invalid JSON. Check your new_str for syntax errors."
|
|
295
|
-
|
|
296
|
-
// 2. 推导变更字段
|
|
297
|
-
for key in modified:
|
|
298
|
-
if modified[key] !== original[key]:
|
|
299
|
-
if key in READ_ONLY_FIELDS:
|
|
300
|
-
warnings.push("Field '{key}' is read-only and was ignored")
|
|
301
|
-
else:
|
|
302
|
-
changes[key] = modified[key]
|
|
303
|
-
|
|
304
|
-
// 3. 语义校验
|
|
305
|
-
if 'todoStatus' in changes && todoStatus !== null && !isTodo:
|
|
306
|
-
warnings.push("Setting 'todoStatus' without 'isTodo: true' may have no effect")
|
|
307
|
-
|
|
308
|
-
// 4. 空变更
|
|
309
|
-
if changes is empty: return { ok: true, changes: [], warnings }
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### 三道防线判断树
|
|
313
|
-
|
|
314
|
-
```
|
|
315
|
-
edit-rem(remId, oldStr, newStr)
|
|
252
|
+
edit-rem(remId, changes)
|
|
316
253
|
│
|
|
317
254
|
├─ 防线 1: 缓存存在?
|
|
318
255
|
│ ├─ 否 → ERROR: "has not been read yet"
|
|
@@ -322,79 +259,22 @@ edit-rem(remId, oldStr, newStr)
|
|
|
322
259
|
│ ├─ 否 → ERROR: "has been modified since last read"
|
|
323
260
|
│ └─ 是 → 继续
|
|
324
261
|
│
|
|
325
|
-
├─
|
|
326
|
-
│ ├─
|
|
327
|
-
│ ├─
|
|
328
|
-
│ └─
|
|
262
|
+
├─ 字段分类
|
|
263
|
+
│ ├─ 只读字段 → 警告
|
|
264
|
+
│ ├─ 未知字段 → 警告
|
|
265
|
+
│ └─ 可写字段 → 继续
|
|
329
266
|
│
|
|
330
|
-
├─
|
|
331
|
-
│ ├─ 否 → ERROR: "
|
|
332
|
-
│ └─ 是 →
|
|
267
|
+
├─ 枚举校验通过?
|
|
268
|
+
│ ├─ 否 → ERROR: "Invalid value for..."
|
|
269
|
+
│ └─ 是 → 继续
|
|
333
270
|
│
|
|
334
|
-
├─
|
|
271
|
+
├─ 有可写变更?
|
|
335
272
|
│ ├─ 否 → OK: changes=[]
|
|
336
273
|
│ └─ 是 → 发送到 Plugin
|
|
337
274
|
│
|
|
338
275
|
└─ Plugin 写入结果?
|
|
339
276
|
├─ 全部成功 → 更新缓存 → OK: changes=[...]
|
|
340
|
-
└─ 部分失败 → 不更新缓存 → ERROR
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
---
|
|
344
|
-
|
|
345
|
-
## Portal 编辑专用路径
|
|
346
|
-
|
|
347
|
-
当 edit-rem 检测到被编辑的 Rem 是 Portal(`type === 'portal'`)时,自动切换到 Portal 专用编辑路径。
|
|
348
|
-
|
|
349
|
-
### 简化 JSON 作为操作目标
|
|
350
|
-
|
|
351
|
-
**问题**:缓存中存储完整 51 字段 JSON,但 AI 看到的是 9 字段简化 JSON。oldStr 来自简化输出,在完整 JSON 上匹配不到。
|
|
352
|
-
|
|
353
|
-
**方案**:Portal 路径在**简化 JSON**(9 字段)上执行 str_replace:
|
|
354
|
-
|
|
355
|
-
1. 防线 1 + 2:不变(完整 JSON 对比)
|
|
356
|
-
2. **str_replace**:将缓存的完整 JSON 转换为简化 JSON,在简化 JSON 上执行 str_replace
|
|
357
|
-
3. 解析 str_replace 后的简化 JSON,推导变更字段
|
|
358
|
-
4. 调用写入
|
|
359
|
-
|
|
360
|
-
### Portal 简化 JSON 格式
|
|
361
|
-
|
|
362
|
-
```json
|
|
363
|
-
{
|
|
364
|
-
"id": "abc123",
|
|
365
|
-
"type": "portal",
|
|
366
|
-
"portalType": "portal",
|
|
367
|
-
"portalDirectlyIncludedRem": ["remId1", "remId2"],
|
|
368
|
-
"parent": "parentId",
|
|
369
|
-
"positionAmongstSiblings": 3,
|
|
370
|
-
"children": ["remId1", "remId2"],
|
|
371
|
-
"createdAt": 1709000000000,
|
|
372
|
-
"updatedAt": 1709000000000
|
|
373
|
-
}
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### Portal 可写字段
|
|
377
|
-
|
|
378
|
-
| 字段 | 写入方式 |
|
|
379
|
-
|:-----|:---------|
|
|
380
|
-
| `portalDirectlyIncludedRem` | diff 数组,新增调 `addToPortal()`,移除调 `removeFromPortal()` |
|
|
381
|
-
| `parent` | 调 `setParent()` |
|
|
382
|
-
| `positionAmongstSiblings` | 调 `setParent(parent, position)` |
|
|
383
|
-
|
|
384
|
-
### Portal 只读字段
|
|
385
|
-
|
|
386
|
-
id、type、portalType、children、createdAt、updatedAt — 修改只产生警告。
|
|
387
|
-
|
|
388
|
-
### Portal 编辑示例
|
|
389
|
-
|
|
390
|
-
```bash
|
|
391
|
-
# 添加一个引用
|
|
392
|
-
edit-rem abc123 --old-str '"portalDirectlyIncludedRem": ["remId1", "remId2"]' \
|
|
393
|
-
--new-str '"portalDirectlyIncludedRem": ["remId1", "remId2", "remId3"]'
|
|
394
|
-
|
|
395
|
-
# 移除一个引用
|
|
396
|
-
edit-rem abc123 --old-str '"portalDirectlyIncludedRem": ["remId1", "remId2"]' \
|
|
397
|
-
--new-str '"portalDirectlyIncludedRem": ["remId1"]'
|
|
277
|
+
└─ 部分失败 → 不更新缓存 → ERROR
|
|
398
278
|
```
|
|
399
279
|
|
|
400
280
|
---
|
|
@@ -462,7 +342,7 @@ for id in currentSet:
|
|
|
462
342
|
|
|
463
343
|
## 只读字段列表
|
|
464
344
|
|
|
465
|
-
以下 30 个字段在
|
|
345
|
+
以下 30 个字段在 changes 中出现时,**只产生警告,不执行写入**:
|
|
466
346
|
|
|
467
347
|
```
|
|
468
348
|
id,
|
|
@@ -486,196 +366,105 @@ isPowerupPropertyListItem, isPowerupSlot
|
|
|
486
366
|
|
|
487
367
|
---
|
|
488
368
|
|
|
489
|
-
##
|
|
490
|
-
|
|
491
|
-
### 操作对象
|
|
369
|
+
## changes 对象使用指南
|
|
492
370
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
示例(部分):
|
|
371
|
+
### 简单属性修改
|
|
496
372
|
|
|
497
373
|
```json
|
|
498
|
-
{
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
],
|
|
503
|
-
"type": "concept",
|
|
504
|
-
"fontSize": null,
|
|
505
|
-
"isTodo": false
|
|
506
|
-
}
|
|
374
|
+
{"type": "concept"}
|
|
375
|
+
{"highlightColor": "Yellow"}
|
|
376
|
+
{"fontSize": "H1"}
|
|
377
|
+
{"isTodo": true, "todoStatus": "Unfinished"}
|
|
507
378
|
```
|
|
508
379
|
|
|
509
|
-
###
|
|
510
|
-
|
|
511
|
-
- **非重叠匹配**:与 `String.prototype.replace()` 行为一致
|
|
512
|
-
- **必须恰好匹配一次**:0 次=未找到错误,>1 次=多匹配错误
|
|
513
|
-
- **大小写敏感**:精确匹配,无模糊匹配
|
|
514
|
-
|
|
515
|
-
### 使用技巧
|
|
516
|
-
|
|
517
|
-
1. **包含足够上下文**:oldStr 应包含字段名和前后结构,避免模糊匹配
|
|
518
|
-
|
|
519
|
-
```
|
|
520
|
-
正确: "\"type\": \"concept\"" → 匹配字段名+值
|
|
521
|
-
错误: "concept" → 可能匹配到 text 内容中的 "concept"
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
2. **替换后必须是合法 JSON**:检查引号、逗号、括号的完整性
|
|
525
|
-
|
|
526
|
-
3. **修改 RichText 字段**:直接操作 JSON 数组结构(见下方完整示例)
|
|
527
|
-
|
|
528
|
-
---
|
|
529
|
-
|
|
530
|
-
## RichText 编辑实战指南
|
|
531
|
-
|
|
532
|
-
### 理解格式化 JSON 中的 RichText
|
|
533
|
-
|
|
534
|
-
`edit-rem` 的 str_replace 操作对象是 `JSON.stringify(remObject, null, 2)` 的格式化文本。RichText 数组在格式化后是多行缩进结构,**不是**紧凑的单行 JSON。
|
|
535
|
-
|
|
536
|
-
以下是一个包含 RichText 的 RemObject **实际输出片段**:
|
|
380
|
+
### 多字段批量修改
|
|
537
381
|
|
|
538
382
|
```json
|
|
539
383
|
{
|
|
540
|
-
"id": "kLrIOHJLyMd8Y2lyA",
|
|
541
|
-
"text": [
|
|
542
|
-
"这是",
|
|
543
|
-
{
|
|
544
|
-
"b": true,
|
|
545
|
-
"i": "m",
|
|
546
|
-
"text": "粗体"
|
|
547
|
-
},
|
|
548
|
-
"普通文本"
|
|
549
|
-
],
|
|
550
|
-
"backText": null,
|
|
551
384
|
"type": "concept",
|
|
552
|
-
"highlightColor":
|
|
553
|
-
"
|
|
385
|
+
"highlightColor": "Yellow",
|
|
386
|
+
"fontSize": "H1",
|
|
387
|
+
"isTodo": true,
|
|
388
|
+
"todoStatus": "Unfinished"
|
|
554
389
|
}
|
|
555
390
|
```
|
|
556
391
|
|
|
557
|
-
|
|
558
|
-
- RichText 对象内部的 key 按**字母序**排列(`b` < `i` < `text`),由 Plugin 端 `sortRichTextKeys()` 保证
|
|
559
|
-
- `_id` 中的 `_` 在 Unicode 中排在小写字母之前,所以 `_id` 排在所有小写 key 的最前面
|
|
560
|
-
- 每个 key-value 对占一行,缩进 4 空格(对象在数组中时嵌套 2+2)
|
|
561
|
-
- 纯字符串元素直接是 `"字符串"`,对象元素展开为多行
|
|
392
|
+
### RichText 修改(text / backText)
|
|
562
393
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
**read-rem 返回**(部分):
|
|
394
|
+
传入完整的 RichText 数组作为 text 或 backText 的新值:
|
|
566
395
|
|
|
567
396
|
```json
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
397
|
+
{"text": ["新文本内容"]}
|
|
398
|
+
{"text": [{"b": true, "i": "m", "text": "粗体文本"}]}
|
|
399
|
+
{"text": ["普通文本", {"i": "m", "iUrl": "https://example.com", "text": "超链接"}]}
|
|
400
|
+
{"backText": ["背面答案"]}
|
|
401
|
+
{"backText": null}
|
|
571
402
|
```
|
|
572
403
|
|
|
573
|
-
**
|
|
404
|
+
**RichText 编辑要点**:
|
|
405
|
+
- RichText 是 JSON 数组,元素为纯字符串或格式化对象
|
|
406
|
+
- 格式化对象的 key 按**字母序**排列(`_id` < `b` < `i` < `text`)
|
|
407
|
+
- 修改 text/backText 时,传入的是**完整的新数组**,不是部分替换
|
|
574
408
|
|
|
575
|
-
|
|
576
|
-
oldStr: "\"text\": [\n \"普通标题\"\n ]"
|
|
577
|
-
|
|
578
|
-
newStr: "\"text\": [\n {\n \"b\": true,\n \"i\": \"m\",\n \"text\": \"粗体标题\"\n }\n ]"
|
|
579
|
-
```
|
|
409
|
+
### Tags Diff 操作
|
|
580
410
|
|
|
581
|
-
|
|
411
|
+
传入完整的目标 tags 数组,系统自动计算差异并执行增删:
|
|
582
412
|
|
|
583
413
|
```json
|
|
584
|
-
|
|
585
|
-
{
|
|
586
|
-
"b": true,
|
|
587
|
-
"i": "m",
|
|
588
|
-
"text": "粗体标题"
|
|
589
|
-
}
|
|
590
|
-
],
|
|
414
|
+
{"tags": ["tagId1", "tagId2", "newTagId3"]}
|
|
591
415
|
```
|
|
592
416
|
|
|
593
|
-
###
|
|
417
|
+
### Portal 引用列表修改
|
|
594
418
|
|
|
595
|
-
|
|
419
|
+
仅 type=portal 的 Rem 可修改此字段:
|
|
596
420
|
|
|
597
421
|
```json
|
|
598
|
-
|
|
599
|
-
"参考 ",
|
|
600
|
-
{
|
|
601
|
-
"_id": "abc123",
|
|
602
|
-
"i": "q"
|
|
603
|
-
},
|
|
604
|
-
" 的内容"
|
|
605
|
-
],
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
将 " 的内容" 替换为 " 的详细说明":
|
|
609
|
-
|
|
610
|
-
```
|
|
611
|
-
oldStr: " 的内容"
|
|
612
|
-
newStr: " 的详细说明"
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
> 注意:纯字符串可以直接匹配,不需要包含数组结构。但如果 " 的内容" 在 JSON 中出现多次,需要加上下文:
|
|
616
|
-
> `oldStr: " \" 的内容\"\n ]"`
|
|
617
|
-
|
|
618
|
-
### 示例 3:给文本添加超链接
|
|
619
|
-
|
|
620
|
-
**read-rem 返回**(部分):
|
|
621
|
-
|
|
622
|
-
```json
|
|
623
|
-
"text": [
|
|
624
|
-
"点击访问官网"
|
|
625
|
-
],
|
|
422
|
+
{"portalDirectlyIncludedRem": ["remId1", "remId2", "newRemId3"]}
|
|
626
423
|
```
|
|
627
424
|
|
|
628
|
-
|
|
425
|
+
---
|
|
629
426
|
|
|
630
|
-
|
|
631
|
-
oldStr: "\"text\": [\n \"点击访问官网\"\n ]"
|
|
427
|
+
## highlightColor vs h
|
|
632
428
|
|
|
633
|
-
|
|
634
|
-
```
|
|
429
|
+
两种完全不同的高亮机制,不可混淆:
|
|
635
430
|
|
|
636
|
-
|
|
431
|
+
| 属性 | 位置 | 值类型 | 效果 | 修改方式 |
|
|
432
|
+
|:-----|:-----|:-------|:-----|:---------|
|
|
433
|
+
| `highlightColor` | RemObject 顶层字段 | 字符串 `"Red"`/`"Yellow"` 等,或 `null` | 整行背景色(左侧彩色竖条) | changes 中直接设置 |
|
|
434
|
+
| `h` | RichText 元素内部 | 数字 0-9 | 文字片段的荧光底色 | 在 text 数组的 RichText 对象中设置 |
|
|
637
435
|
|
|
638
|
-
|
|
436
|
+
### RichText `h` 颜色值对照表(必须用数字,不是字符串)
|
|
639
437
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
438
|
+
| 值 | 颜色 | 值 | 颜色 | 值 | 颜色 |
|
|
439
|
+
|:---|:-----|:---|:-----|:---|:-----|
|
|
440
|
+
| 0 | 无(默认) | 4 | Green | 7 | Gray |
|
|
441
|
+
| 1 | Red | 5 | Purple | 8 | Brown |
|
|
442
|
+
| 2 | Orange | 6 | Blue | 9 | Pink |
|
|
443
|
+
| 3 | Yellow | — | — | — | — |
|
|
644
444
|
|
|
645
|
-
|
|
445
|
+
### 设置/清除整行背景色(highlightColor)
|
|
646
446
|
|
|
447
|
+
```json
|
|
448
|
+
{"highlightColor": "Yellow"}
|
|
449
|
+
{"highlightColor": null}
|
|
647
450
|
```
|
|
648
|
-
oldStr: "\"text\": [\n \"普通文本\"\n ]"
|
|
649
|
-
newStr: "\"text\": [\n {\n \"h\": 1,\n \"i\": \"m\",\n \"text\": \"红色高亮文本\"\n }\n ]"
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
> **区分**:`highlightColor` 是 RemObject 顶层字段,值为字符串(`"Red"`, `"Blue"` 等);RichText 的 `h` 是行内格式标记,值为数字(1=Red, 2=Orange 等,见 RemColor 枚举)。
|
|
653
451
|
|
|
654
|
-
###
|
|
452
|
+
### 给文字加/去荧光底色(RichText h 字段)
|
|
655
453
|
|
|
656
|
-
|
|
454
|
+
通过修改 text 数组中 RichText 对象的 `h` 值实现。需要传入包含完整 text 数组的 changes:
|
|
657
455
|
|
|
658
456
|
```json
|
|
457
|
+
{
|
|
659
458
|
"text": [
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
oldStr: "\"text\": [\n \"光合作用需要阳光\"\n ]"
|
|
668
|
-
|
|
669
|
-
newStr: "\"text\": [\n \"光合作用需要\",\n {\n \"cId\": \"cloze1\",\n \"i\": \"m\",\n \"text\": \"阳光\"\n }\n ]"
|
|
459
|
+
{
|
|
460
|
+
"h": 3,
|
|
461
|
+
"i": "m",
|
|
462
|
+
"text": "黄色荧光文字"
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
}
|
|
670
466
|
```
|
|
671
467
|
|
|
672
|
-
### 常见错误
|
|
673
|
-
|
|
674
|
-
1. **忘记 key 字母序**:写 `{"text":"xx","i":"m","b":true}` 不会被匹配——实际存储为 `{"b":true,"i":"m","text":"xx"}`
|
|
675
|
-
2. **缩进不匹配**:格式化 JSON 使用 2 空格缩进,数组内对象的 key 缩进 6 空格(顶层 2 + 数组 2 + 对象 2),但在 `JSON.stringify(obj, null, 2)` 中数组元素的对象缩进 4 空格(数组 2 + 对象内 2)
|
|
676
|
-
3. **混淆 highlightColor 和 h**:前者是字符串 `"Red"`,后者是数字 `1`
|
|
677
|
-
4. **忘记 `i:"a"` 的 `onlyAudio` 必填**:缺少此字段 SDK 拒绝写入
|
|
678
|
-
|
|
679
468
|
---
|
|
680
469
|
|
|
681
470
|
## 缓存更新行为
|
|
@@ -685,11 +474,10 @@ newStr: "\"text\": [\n \"光合作用需要\",\n {\n \"cId\": \"cloz
|
|
|
685
474
|
| 写入成功 | 从 Plugin re-read 最新状态 → 覆盖缓存 | 确保缓存与 SDK 状态同步 |
|
|
686
475
|
| 防线 1 拒绝 | 无缓存,不操作 | — |
|
|
687
476
|
| 防线 2 拒绝 | **不更新缓存** | 迫使 AI 执行 read-rem 获取最新状态 |
|
|
688
|
-
|
|
|
689
|
-
| JSON 语法错误 | 缓存保持不变 | AI 可调整 newStr 后重试 |
|
|
477
|
+
| 枚举值非法 | 缓存保持不变 | AI 可调整值后重试 |
|
|
690
478
|
| 部分写入失败 | **不更新缓存** | 迫使 AI 执行 read-rem 重新评估状态 |
|
|
691
479
|
|
|
692
|
-
**关键设计**:写入成功后**永远从 Plugin
|
|
480
|
+
**关键设计**:写入成功后**永远从 Plugin 重新读取**最新状态,而非本地推导修改后的值。这保证缓存与实际 SDK 状态完全同步。
|
|
693
481
|
|
|
694
482
|
---
|
|
695
483
|
|
|
@@ -709,10 +497,8 @@ newStr: "\"text\": [\n \"光合作用需要\",\n {\n \"cId\": \"cloz
|
|
|
709
497
|
|----------|------|----------|
|
|
710
498
|
| `has not been read yet` | 未先执行 read-rem | 执行 `read-rem <remId>` 后重试 |
|
|
711
499
|
| `has been modified since last read` | Rem 在 read 和 edit 之间被外部修改 | 执行 `read-rem <remId>` 获取最新状态后重试 |
|
|
712
|
-
| `
|
|
713
|
-
| `old_str matches N locations` | oldStr 匹配到多个位置 | 扩大 oldStr 范围,包含更多上下文以唯一定位 |
|
|
714
|
-
| `invalid JSON` | 替换后的文本不是合法 JSON | 检查 newStr 的引号、逗号、括号完整性 |
|
|
500
|
+
| `Invalid value for '...'` | 枚举字段的值不在允许范围内 | 检查允许的枚举值(见可编辑字段约束表) |
|
|
715
501
|
| `Failed to update field` | SDK setter 调用失败 | 检查字段值是否在允许范围内(如 type 不能设为 portal) |
|
|
716
|
-
| `Field '...' is read-only and was ignored` |
|
|
717
|
-
| `
|
|
502
|
+
| `Field '...' is read-only and was ignored` | changes 中包含只读字段 | 该字段只能读取,不可通过 edit-rem 修改 |
|
|
503
|
+
| `Field '...' is unknown and was ignored` | changes 中包含不存在的字段名 | 检查字段名拼写,确认在 21 个可写字段或 30 个只读字段中 |
|
|
718
504
|
| `守护进程未运行` | daemon 未启动 | 执行 `remnote-bridge connect` |
|