remnote-bridge 0.1.12 → 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.
@@ -1,16 +1,17 @@
1
1
  # edit-rem
2
2
 
3
- > 通过 str_replace 语义修改单个 Rem 的属性。三道防线确保编辑安全。
3
+ > 直接修改单个 Rem 的属性字段。两道防线确保编辑安全。
4
4
 
5
5
  ---
6
6
 
7
7
  ## 功能
8
8
 
9
- `edit-rem` 使用 str_replace 语义修改 Rem 属性——在 Rem 的序列化 JSON 文本中,将 `oldStr` 替换为 `newStr`,然后推导出变更字段并写入 SDK
9
+ `edit-rem` 直接修改 Rem 的属性字段——通过 `changes` 对象指定要修改的字段及其新值,无需构造 str_replace
10
10
 
11
11
  核心特性:
12
- - **str_replace 语义**:操作对象是 `JSON.stringify(remObject, null, 2)` 的文本
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> --old-str <oldStr> --new-str <newStr>
37
+ remnote-bridge edit-rem <remId> --changes '{"type":"concept"}'
37
38
  ```
38
39
 
39
40
  | 参数/选项 | 类型 | 必需 | 说明 |
40
41
  |-----------|------|:----:|------|
41
42
  | `remId` | string(位置参数) | 是 | Rem ID |
42
- | `--old-str <oldStr>` | string | 是 | 要替换的原始文本片段 |
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
- 无变更(old_str 和 new_str 产生相同结果)
54
+ 无变更(未发现可写入的变更字段)
55
55
  ```
56
56
 
57
57
  ### JSON 模式
58
58
 
59
59
  ```bash
60
- remnote-bridge edit-rem --json '{"remId":"kLrIOHJLyMd8Y2lyA","oldStr":"\"concept\"","newStr":"\"descriptor\""}'
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
- | `oldStr` | string | 是 | 要替换的原始文本片段(在序列化 JSON 中精确匹配) |
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
- ### 防线 3 拒绝:old_str 未找到
122
+ ### 枚举值非法
124
123
 
125
124
  ```json
126
125
  {
127
126
  "ok": false,
128
127
  "command": "edit-rem",
129
- "error": "old_str not found in the serialized JSON of rem kLrIOHJLyMd8Y2lyA",
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, oldStr, newStr
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(currentRemObject, null, 2)
198
- ├─ 与缓存 JSON 严格比较
199
- └─ 不匹配 → 抛出错误(不更新缓存,迫使 AI re-read)
186
+ │ ├─ JSON.stringify 比较当前 vs 缓存
187
+ └─ 不匹配 抛出错误(不更新缓存)
188
+
189
+ ├─ 遍历 changes keys
190
+ │ ├─ READ_ONLY_FIELDS → warnings
191
+ │ ├─ 不在 WRITABLE_FIELDS 中 → warnings
192
+ │ └─ 通过 → writableChanges
200
193
 
201
- ├─ Portal 检测:type === 'portal'?
202
- │ ├─ 是 → 进入 Portal 专用路径(简化 JSON 上执行 str_replace)
203
- │ └─ 否 → 继续普通 Rem 路径
194
+ ├─ 枚举值范围校验(type, practiceDirection, highlightColor, fontSize, todoStatus)
204
195
 
205
- ├─ 防线 3: str_replace 精确匹配
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
- └─ 写入失败 → 不更新缓存(迫使 AI re-read)
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
- - 比较方式:**整个 JSON 文本严格二进制比较**(包括格式化空白)
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
- ### 防线 3:str_replace 精确匹配
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
- ├─ 防线 3: old_str 匹配次数?
326
- │ ├─ 0 ERROR: "old_str not found"
327
- │ ├─ >1 ERROR: "old_str matches N locations"
328
- │ └─ 1 执行替换
262
+ ├─ 字段分类
263
+ │ ├─ 只读字段警告
264
+ │ ├─ 未知字段警告
265
+ │ └─ 可写字段继续
329
266
 
330
- ├─ 后处理: JSON 合法?
331
- │ ├─ 否 → ERROR: "invalid JSON"
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: failed field info
341
- ```
342
-
343
- ---
344
-
345
- ## Portal 编辑专用路径
346
-
347
- 当 edit-rem 检测到被编辑的 Rem 是 Portal(`type === 'portal'`)时,自动切换到 Portal 专用编辑路径。
348
-
349
- ### 简化 JSON 作为操作目标
350
-
351
- **问题**:缓存中存储完整 51 字段 JSON,但 AI 看到的是 8 字段简化 JSON。oldStr 来自简化输出,在完整 JSON 上匹配不到。
352
-
353
- **方案**:Portal 路径在**简化 JSON**(8 字段)上执行 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 个字段在 str_replace 中被修改时,**只产生警告,不执行写入**:
345
+ 以下 30 个字段在 changes 中出现时,**只产生警告,不执行写入**:
466
346
 
467
347
  ```
468
348
  id,
@@ -486,163 +366,74 @@ isPowerupPropertyListItem, isPowerupSlot
486
366
 
487
367
  ---
488
368
 
489
- ## str_replace 语义详解
490
-
491
- ### 操作对象
492
-
493
- str_replace 操作的对象是 `JSON.stringify(remObject, null, 2)` 的文本——格式化缩进 2 空格的 JSON。
369
+ ## changes 对象使用指南
494
370
 
495
- 示例(部分):
371
+ ### 简单属性修改
496
372
 
497
373
  ```json
498
- {
499
- "id": "kLrIOHJLyMd8Y2lyA",
500
- "text": [
501
- "Hello World"
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": null,
553
- "isTodo": false
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
- - 纯字符串元素直接是 `"字符串"`,对象元素展开为多行
562
-
563
- ### 示例 1:将纯文本改为粗体
392
+ ### RichText 修改(text / backText)
564
393
 
565
- **read-rem 返回**(部分):
394
+ 传入完整的 RichText 数组作为 text 或 backText 的新值:
566
395
 
567
396
  ```json
568
- "text": [
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
- **edit-rem 调用**:
404
+ **RichText 编辑要点**:
405
+ - RichText 是 JSON 数组,元素为纯字符串或格式化对象
406
+ - 格式化对象的 key 按**字母序**排列(`_id` < `b` < `i` < `text`)
407
+ - 修改 text/backText 时,传入的是**完整的新数组**,不是部分替换
574
408
 
575
- ```
576
- oldStr: "\"text\": [\n \"普通标题\"\n ]"
409
+ ### Tags Diff 操作
577
410
 
578
- newStr: "\"text\": [\n {\n \"b\": true,\n \"i\": \"m\",\n \"text\": \"粗体标题\"\n }\n ]"
579
- ```
580
-
581
- 替换后 JSON 变为:
411
+ 传入完整的目标 tags 数组,系统自动计算差异并执行增删:
582
412
 
583
413
  ```json
584
- "text": [
585
- {
586
- "b": true,
587
- "i": "m",
588
- "text": "粗体标题"
589
- }
590
- ],
591
- ```
592
-
593
- ### 示例 2:修改 Rem 引用旁的文本
594
-
595
- **read-rem 返回**(部分):
596
-
597
- ```json
598
- "text": [
599
- "参考 ",
600
- {
601
- "_id": "abc123",
602
- "i": "q"
603
- },
604
- " 的内容"
605
- ],
606
- ```
607
-
608
- 将 " 的内容" 替换为 " 的详细说明":
609
-
610
- ```
611
- oldStr: " 的内容"
612
- newStr: " 的详细说明"
414
+ {"tags": ["tagId1", "tagId2", "newTagId3"]}
613
415
  ```
614
416
 
615
- > 注意:纯字符串可以直接匹配,不需要包含数组结构。但如果 " 的内容" 在 JSON 中出现多次,需要加上下文:
616
- > `oldStr: " \" 的内容\"\n ]"`
417
+ ### Portal 引用列表修改
617
418
 
618
- ### 示例 3:给文本添加超链接
619
-
620
- **read-rem 返回**(部分):
419
+ type=portal 的 Rem 可修改此字段:
621
420
 
622
421
  ```json
623
- "text": [
624
- "点击访问官网"
625
- ],
422
+ {"portalDirectlyIncludedRem": ["remId1", "remId2", "newRemId3"]}
626
423
  ```
627
424
 
628
- **edit-rem 调用**:
629
-
630
- ```
631
- oldStr: "\"text\": [\n \"点击访问官网\"\n ]"
632
-
633
- newStr: "\"text\": [\n \"点击\",\n {\n \"i\": \"m\",\n \"iUrl\": \"https://remnote.com\",\n \"text\": \"访问官网\"\n }\n ]"
634
- ```
425
+ ---
635
426
 
636
- ### 示例 4:修改高亮颜色(Rem 级别 vs RichText 级别)
427
+ ## highlightColor vs h
637
428
 
638
- #### ⚠️ highlightColor vs h — 两种完全不同的高亮
429
+ 两种完全不同的高亮机制,不可混淆:
639
430
 
640
431
  | 属性 | 位置 | 值类型 | 效果 | 修改方式 |
641
432
  |:-----|:-----|:-------|:-----|:---------|
642
- | `highlightColor` | RemObject 顶层字段 | 字符串 `"Red"`/`"Yellow"` 等,或 `null` | 整行背景色(左侧彩色竖条) | str_replace 顶层字段 |
643
- | `h` | RichText 元素内部 | 数字 0-9 | 文字片段的荧光底色 | str_replace text 数组内的对象 |
433
+ | `highlightColor` | RemObject 顶层字段 | 字符串 `"Red"`/`"Yellow"` 等,或 `null` | 整行背景色(左侧彩色竖条) | changes 中直接设置 |
434
+ | `h` | RichText 元素内部 | 数字 0-9 | 文字片段的荧光底色 | text 数组的 RichText 对象中设置 |
644
435
 
645
- #### RichText `h` 颜色值对照表(必须用数字,不是字符串)
436
+ ### RichText `h` 颜色值对照表(必须用数字,不是字符串)
646
437
 
647
438
  | 值 | 颜色 | 值 | 颜色 | 值 | 颜色 |
648
439
  |:---|:-----|:---|:-----|:---|:-----|
@@ -651,59 +442,29 @@ newStr: "\"text\": [\n \"点击\",\n {\n \"i\": \"m\",\n \"iUrl
651
442
  | 2 | Orange | 6 | Blue | 9 | Pink |
652
443
  | 3 | Yellow | — | — | — | — |
653
444
 
654
- #### 4a. 设置/清除整行背景色(highlightColor)
655
-
656
- ```
657
- // 设置为黄色背景
658
- oldStr: "\"highlightColor\": null"
659
- newStr: "\"highlightColor\": \"Yellow\""
660
-
661
- // 清除背景色
662
- oldStr: "\"highlightColor\": \"Yellow\""
663
- newStr: "\"highlightColor\": null"
664
- ```
665
-
666
- #### 4b. 给文字加/去荧光底色(RichText h 字段)
445
+ ### 设置/清除整行背景色(highlightColor)
667
446
 
668
- 先 read_rem 找到 text 数组中目标文字对象的精确 JSON。用 `h` 值旁边的 `"i"` 和 `"text"` 字段一起匹配,确保唯一。
669
-
670
- ```
671
- // "Todo List" 文字加黄色荧光(h: 0 → 3)
672
- oldStr: "\"h\": 0,\n \"i\": \"m\",\n \"text\": \"Todo List \""
673
- newStr: "\"h\": 3,\n \"i\": \"m\",\n \"text\": \"Todo List \""
674
-
675
- // 去掉荧光(h: 3 → 0)
676
- oldStr: "\"h\": 3,\n \"i\": \"m\",\n \"text\": \"Todo List \""
677
- newStr: "\"h\": 0,\n \"i\": \"m\",\n \"text\": \"Todo List \""
447
+ ```json
448
+ {"highlightColor": "Yellow"}
449
+ {"highlightColor": null}
678
450
  ```
679
451
 
680
- ⚠️ **关键**:oldStr 中必须包含足够的上下文(如 `"i": "m"` 和 `"text": "..."`)来唯一定位。不能只写 `"h": 0` — 可能匹配到多处。
681
-
682
- ### 示例 5:添加完形填空
452
+ ### 给文字加/去荧光底色(RichText h 字段)
683
453
 
684
- **read-rem 返回**(部分):
454
+ 通过修改 text 数组中 RichText 对象的 `h` 值实现。需要传入包含完整 text 数组的 changes:
685
455
 
686
456
  ```json
457
+ {
687
458
  "text": [
688
- "光合作用需要阳光"
689
- ],
690
- ```
691
-
692
- 将 "阳光" 变成完形填空:
693
-
694
- ```
695
- oldStr: "\"text\": [\n \"光合作用需要阳光\"\n ]"
696
-
697
- 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
+ }
698
466
  ```
699
467
 
700
- ### 常见错误
701
-
702
- 1. **忘记 key 字母序**:写 `{"text":"xx","i":"m","b":true}` 不会被匹配——实际存储为 `{"b":true,"i":"m","text":"xx"}`
703
- 2. **缩进不匹配**:格式化 JSON 使用 2 空格缩进,数组内对象的 key 缩进 6 空格(顶层 2 + 数组 2 + 对象 2),但在 `JSON.stringify(obj, null, 2)` 中数组元素的对象缩进 4 空格(数组 2 + 对象内 2)
704
- 3. **混淆 highlightColor 和 h**:前者是字符串 `"Red"`,后者是数字 `1`
705
- 4. **忘记 `i:"a"` 的 `onlyAudio` 必填**:缺少此字段 SDK 拒绝写入
706
-
707
468
  ---
708
469
 
709
470
  ## 缓存更新行为
@@ -713,11 +474,10 @@ newStr: "\"text\": [\n \"光合作用需要\",\n {\n \"cId\": \"cloz
713
474
  | 写入成功 | 从 Plugin re-read 最新状态 → 覆盖缓存 | 确保缓存与 SDK 状态同步 |
714
475
  | 防线 1 拒绝 | 无缓存,不操作 | — |
715
476
  | 防线 2 拒绝 | **不更新缓存** | 迫使 AI 执行 read-rem 获取最新状态 |
716
- | 防线 3 拒绝 | 缓存保持不变 | AI 可调整 oldStr 后重试 |
717
- | JSON 语法错误 | 缓存保持不变 | AI 可调整 newStr 后重试 |
477
+ | 枚举值非法 | 缓存保持不变 | AI 可调整值后重试 |
718
478
  | 部分写入失败 | **不更新缓存** | 迫使 AI 执行 read-rem 重新评估状态 |
719
479
 
720
- **关键设计**:写入成功后**永远从 Plugin 重新读取**最新状态,而非本地推导修改后的 JSON。这保证缓存与实际 SDK 状态完全同步。
480
+ **关键设计**:写入成功后**永远从 Plugin 重新读取**最新状态,而非本地推导修改后的值。这保证缓存与实际 SDK 状态完全同步。
721
481
 
722
482
  ---
723
483
 
@@ -737,10 +497,8 @@ newStr: "\"text\": [\n \"光合作用需要\",\n {\n \"cId\": \"cloz
737
497
  |----------|------|----------|
738
498
  | `has not been read yet` | 未先执行 read-rem | 执行 `read-rem <remId>` 后重试 |
739
499
  | `has been modified since last read` | Rem 在 read 和 edit 之间被外部修改 | 执行 `read-rem <remId>` 获取最新状态后重试 |
740
- | `old_str not found` | oldStr 在序列化 JSON 中不存在 | 检查 oldStr 是否精确匹配(含引号、空格、换行) |
741
- | `old_str matches N locations` | oldStr 匹配到多个位置 | 扩大 oldStr 范围,包含更多上下文以唯一定位 |
742
- | `invalid JSON` | 替换后的文本不是合法 JSON | 检查 newStr 的引号、逗号、括号完整性 |
500
+ | `Invalid value for '...'` | 枚举字段的值不在允许范围内 | 检查允许的枚举值(见可编辑字段约束表) |
743
501
  | `Failed to update field` | SDK setter 调用失败 | 检查字段值是否在允许范围内(如 type 不能设为 portal) |
744
- | `Field '...' is read-only and was ignored` | 修改了只读字段 | 该字段只能读取,不可通过 edit-rem 修改 |
745
- | `old_str not found in the simplified Portal JSON` | Portal 编辑时 oldStr 在简化 JSON 中不匹配 | 检查 oldStr 是否匹配 8 字段简化 JSON 格式(而非完整 51 字段 JSON) |
502
+ | `Field '...' is read-only and was ignored` | changes 中包含只读字段 | 该字段只能读取,不可通过 edit-rem 修改 |
503
+ | `Field '...' is unknown and was ignored` | changes 中包含不存在的字段名 | 检查字段名拼写,确认在 21 个可写字段或 30 个只读字段中 |
746
504
  | `守护进程未运行` | daemon 未启动 | 执行 `remnote-bridge connect` |