@lark-apaas/coding-steering 0.1.71 → 0.1.73-alpha.20261026071512

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/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-steering",
3
- "version": "0.1.71",
3
+ "version": "0.1.73-alpha.20261026071512",
4
4
  "description": "Stack-specific steering content for miaoda-coding templates",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "steering"
8
8
  ],
9
+ "scripts": {
10
+ "lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
11
+ },
9
12
  "devDependencies": {
10
13
  "markdownlint-cli": "^0.47.0"
11
14
  },
@@ -17,8 +20,5 @@
17
20
  "miaoda",
18
21
  "coding-steering"
19
22
  ],
20
- "license": "MIT",
21
- "scripts": {
22
- "lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
23
- }
24
- }
23
+ "license": "MIT"
24
+ }
@@ -220,6 +220,8 @@ const structured = await capabilityClient
220
220
 
221
221
  > **已知必填配置值的传入**:用户对话中已给出的必填配置(如飞书 base URL 含 AppToken/TableID),CREATE 时**必须显式列入 `requirementsSummary` 字段**让平台 LLM 装配到 formValue。例:`requirementsSummary: "创建飞书多维表格写入实例,appToken=ZeHhbA4MxxxX, tableID=tblMQ9OgxxxW, 用于把 Excel 行批量插入"`。
222
222
 
223
+ > **feishu-bitable 字段类型的传入**:用户只给了 Base 链接、没有在界面上选择表格时,平台不读取源表结构,`fields` 的类型由平台 LLM 按 `requirementsSummary` 生成,会按字段名猜(如「价格」→ Currency、「状态」→ SingleSelect)。CREATE 前,lark-cli 可用时先执行 `lark-cli base +field-list --base-token <appToken> --table-id <tableID>` 读取真实字段,在 `requirementsSummary` 里逐个写明「字段名:类型」,单选 / 多选字段附上选项值。例:`字段:物料编码:Text, 标准价格:Text, 同步状态:SingleSelect(待同步/已同步)`。多维表格实例不允许 UPDATE,类型写错只能重新 CREATE。
224
+
223
225
  ## 插件调用错误处理
224
226
 
225
227
  错误分类与应对策略详见 `references/plugin-coding-guide.md`「调用错误分类与应对」。必须遵守:
@@ -60,8 +60,8 @@
60
60
  record.v1.0版本 // 会被错误解析
61
61
  ```
62
62
 
63
- 4. **特殊字段需特化展示**:Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
64
- 5. **特殊字段需专用录入组件**:SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
63
+ 4. **特殊字段需特化展示**:Text(`BitableText` 分段渲染:链接可点击、保留换行)、Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
64
+ 5. **特殊字段需专用录入组件**:Text(多行文本框 `Textarea`)、SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
65
65
 
66
66
  ---
67
67
 
@@ -89,7 +89,8 @@
89
89
 
90
90
  | BizType | 写入格式 | 读取格式 | 说明 |
91
91
  | --------------------------------------- | ---------------- | ------------------ | ------------------------ |
92
- | `Text`/`Email`/`Barcode` | `string` | `{ text: string }` | 写入纯字符串,读取为对象 |
92
+ | `Text` | `string \| TextSegment[]` | `{ text: string; originalValue?: TextSegment[] }` | 写入:纯文本直接传字符串;需要保留或写入链接、@ 时传分段数组,通常就是读到的 `originalValue` 修改后原样回传(见「文本字段写入分段」)。读取:`text` 是拼接后的纯文本,用于筛选、排序、比较;展示用 `originalValue`。读取时传 `textFormat: 'original'` 才有 `originalValue`;服务端开关关闭时也可能缺失,缺失时退回 `text` |
93
+ | `Email`/`Barcode` | `string` | `{ text: string }` | 写入纯字符串,读取为对象 |
93
94
  | `Phone` | `string` | `string` | 读写均为字符串 |
94
95
  | `Number`/`Progress`/`Currency`/`Rating` | `number` | `number` | 数值类型 |
95
96
  | `SingleSelect` | `string` | `string` | 必须匹配 enumValues |
@@ -100,6 +101,22 @@
100
101
  | `Url` | `{ text, link }` | `{ text, link }` | 读写格式一致 |
101
102
  | `Attachment` | `string[]` | 复杂对象 | 写入文件 URL 数组 |
102
103
 
104
+ `TextSegment` 定义:
105
+
106
+ ```typescript
107
+ // 多维表格文本字段的一段内容,原样来自多维表格;各段 text 依次拼接等于外层的 text
108
+ type TextSegment = {
109
+ type: string; // 'text' 普通文字(可能含 \n,也可能是空串)/ 'url' 超链接 / 'mention' @文档或@人;遇到不认识的按 text 显示
110
+ text: string;
111
+ link?: string; // url 段、@文档段的地址
112
+ mentionType?: string | number; // mention 段:读出是 Bitable / Docx / Wiki / User 等字符串,取值开放,按有没有 link 判断能否点击;写入接受字符串或数字,必须与链接的文档类型一致
113
+ token?: string; // @文档的文档 token;@人时是飞书 open_id,不是妙搭用户 ID,不要拿去查用户或和人员字段比较
114
+ [k: string]: unknown; // 其他字段原样保留
115
+ };
116
+ ```
117
+
118
+ 兼容说明:`originalValue` 是可选的新增字段,不传 `textFormat` 时返回内容与原来完全一致,只读 `.text` 的存量代码不用改也能正常运行。要链接可点击、保留换行时,再按下文迁移:读取加 `textFormat: 'original'`,展示从 `.text` 换成 `BitableText`。写入同理:Text 字段继续传字符串的存量代码行为不变,分段数组是新增的可选写法。
119
+
103
120
  ### 只读字段
104
121
 
105
122
  | BizType | 读取格式 |
@@ -175,7 +192,7 @@
175
192
 
176
193
  ### dimensions value 类型(常见)
177
194
 
178
- - `Text` / `Email` / `Barcode`: `{ text: string }`
195
+ - `Text` / `Email` / `Barcode`: `{ text: string }`(Text 分组维度只有 `text`,没有 `originalValue`,不要按 originalValue 渲染分组标签)
179
196
  - `Number`: `string`
180
197
  - `Progress`: `string`
181
198
  - `Currency`: `string`
@@ -228,6 +245,9 @@ const response = await capabilityClient.load('plugin_instance_id').call<{
228
245
  conditions: [{ fieldName: '状态', operator: 'is', value: ['进行中'] }],
229
246
  },
230
247
  pageSize: 20,
248
+ // 列表展示文本字段(链接可点击、保留换行)时传;不传就没有 originalValue,页面只能显示纯文本。
249
+ // 同步入库、导出、统计等不展示的读取不要传,见「代码实现注意事项」第 6 条
250
+ textFormat: 'original',
231
251
  });
232
252
 
233
253
  // ⚠️ 响应数据直接从 response 访问,不要加 .data 或 .output 前缀
@@ -240,17 +260,17 @@ logger.info(`共 ${total} 条,本页 ${records.length} 条,还有更多: ${h
240
260
  const detail = await capabilityClient.load('plugin_instance_id').call<{
241
261
  id: string;
242
262
  record: Record<string, any>;
243
- }>('getRecord', { recordID: 'recXXX' });
263
+ }>('getRecord', { recordID: 'recXXX', textFormat: 'original' }); // 详情 / 编辑页要展示文本字段,传 textFormat
244
264
  // ✅ 正确:detail.id / detail.record
245
265
  // ❌ 错误:detail.data.output.id
246
- const title = detail.record['标题']?.text;
266
+ const title = detail.record['标题']?.text; // 纯文本,用于筛选、比较、导出;展示用 BitableText
247
267
 
248
268
  // 批量新增(注意:写入时用写入格式,字段名必须用引号包裹)
249
269
  await capabilityClient.load('plugin_instance_id').call('batchAddRecords', {
250
270
  records: [
251
271
  {
252
272
  record: {
253
- 文本字段: '内容', // Text: string(非 { text: string })
273
+ 文本字段: '内容', // Text: string(非 { text: string });要带链接时传 TextSegment[],见「文本字段写入分段」
254
274
  数字字段: 100, // Number: number
255
275
  单选字段: '选项A', // SingleSelect: string(必须匹配 enumValues)
256
276
  多选字段: ['标签1', '标签2'], // MultiSelect: string[]
@@ -263,7 +283,7 @@ await capabilityClient.load('plugin_instance_id').call('batchAddRecords', {
263
283
  ],
264
284
  });
265
285
 
266
- // 批量更新
286
+ // 批量更新(record 里只放改过的字段,见下方「代码实现注意事项」第 4 条)
267
287
  await capabilityClient.load('plugin_instance_id').call('batchUpdateRecords', {
268
288
  records: [{ id: 'record_id', record: { 文本字段: '新内容' } }],
269
289
  });
@@ -274,7 +294,7 @@ await capabilityClient
274
294
  .call('deleteRecords', { recordIDs: ['id1', 'id2'] });
275
295
 
276
296
  // 读取时注意格式差异
277
- const textValue = record.record['文本字段']?.text; // 读取时是 { text: string }
297
+ const textValue = record.record['文本字段']?.text; // 纯文本,用于筛选、比较、导出;展示用 BitableText
278
298
  const numberValue = record.record['数字字段']; // number
279
299
  const userIds = record.record['人员字段']; // number[]
280
300
 
@@ -300,23 +320,125 @@ const chartData =
300
320
  1. **响应数据直接访问,禁止猜测嵌套路径**。`capabilityClient.call()` 返回的就是最终数据,直接用 `response.records`、`response.total` 等。**禁止**加 `.data`、`.output`、`.data.output` 等前缀。如果不确定响应结构,用 `logger.info('response', response)` 临时输出查看(或在浏览器开发者工具 Network 面板看)。
301
321
  2. 在进行写入操作时,比如确认提交时,应该有loading效果,禁止用户重复点击。
302
322
  3. 如果capabilityClient call失败,命中了error逻辑,要打印日志,同时把error message提示给用户。
303
- 4. 在进行更新操作时,只更新需要更新的字段即可,不需要对全量字段进行更新。
323
+ 4. **只提交改过的字段(硬约束)**。编辑后调用 batchUpdateRecords 时,**只提交和读取时相比值有变化的字段**,逐字段比较后再组装 `record`。Text 字段没改动时**禁止**放进请求:多维表格按字段整体覆盖,而编辑表单的文本框提交的是纯字符串,带链接的文本会被写成纯文本,链接永久丢失。要改文字又保留链接,按下方「文本字段写入分段」写分段数组。
324
+
325
+ ```typescript
326
+ // 编辑页读取:含链接提示依赖 originalValue,getRecord 也要传 textFormat(见第 6 条)
327
+ const { record: original } = await capabilityClient.load(pluginInstanceId).call<{ id: string; record: Record<string, any> }>('getRecord', {
328
+ recordID: recordId,
329
+ textFormat: 'original',
330
+ });
331
+
332
+ // 表单回填:Text 字段用 .text(表单值是 string),不要把整个 { text, originalValue } 对象放进表单,
333
+ // 否则文本框显示 [object Object],保存时还会把对象当写入值提交
334
+ const initForm = {
335
+ 文本字段: original['文本字段']?.text ?? '',
336
+ 数字字段: original['数字字段'],
337
+ 多选字段: original['多选字段'] ?? [],
338
+ // ...其余可编辑字段;只读字段(创建时间、创建人、公式等)不要放进表单
339
+ // Attachment 读取是复杂对象、写入是文件 URL 数组,编辑时按写入格式单独维护
340
+ };
341
+
342
+ // 保存:form 是以 initForm 初始化、用户编辑后的表单值;逐字段和读取值比较,只组装改过的字段
343
+ const changed: Record<string, unknown> = {};
344
+ // Text:用 .text 和表单值比
345
+ if (form['文本字段'] !== (original['文本字段']?.text ?? '')) changed['文本字段'] = form['文本字段'];
346
+ // 标量字段:直接比较
347
+ if (form['数字字段'] !== original['数字字段']) changed['数字字段'] = form['数字字段'];
348
+ // 数组、对象字段(多选、人员、超链接等)按值比较,不要用 !==(引用不同会被误判为已修改)
349
+ const sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
350
+ if (!sameValue(form['多选字段'], original['多选字段'] ?? [])) changed['多选字段'] = form['多选字段'];
351
+ // ...其余字段同样逐个比较;只读字段不要放进 changed
352
+ // Attachment 读取格式与写入格式不同:未改动时不提交,改动时按写入格式(文件 URL 数组)提交
353
+ if (Object.keys(changed).length > 0) {
354
+ await capabilityClient.load(pluginInstanceId).call('batchUpdateRecords', {
355
+ records: [{ id: recordId, record: changed }],
356
+ });
357
+ }
358
+ ```
359
+
360
+ 含链接文本的编辑提示:编辑控件检测到 `originalValue` 里有 `url` 或 `mention` 段时,在控件下方提示「该文本包含链接,保存后链接将变为纯文本」(示例见「推荐 UI 组件」)。编辑表单读取记录时必须传 `textFormat: 'original'`,否则拿不到分段,提示永远不出现。
304
361
  5. **分页必须使用游标分页,禁止全量拉取后切片**。先拉全部数据再 `slice` 会导致性能极差、浪费带宽和内存。每次只拉当前页数据,通过 `pageToken` 翻页:
305
362
 
306
363
  ```typescript
307
- const result = await capabilityClient.load(pluginInstanceId).call('searchRecords', {
364
+ const result = await capabilityClient.load(pluginInstanceId).call<{
365
+ records: Array<{ id: string; record: Record<string, any> }>;
366
+ hasMore: boolean;
367
+ pageToken?: string;
368
+ total: number;
369
+ }>('searchRecords', {
308
370
  pageSize: 10,
309
371
  pageToken: cursor, // 上一页返回的 pageToken
372
+ // 要把文本字段展示给用户时再加 textFormat: 'original'(见第 6 条)
310
373
  });
311
374
  // 返回:{ records, pageToken(下一页游标), hasMore, total }
312
375
  ```
313
376
 
377
+ 6. **展示或编辑文本字段时显式要原始分段**。列表、详情、编辑表单回显这类要把文本字段展示给用户(或在编辑时提示链接)的读取,searchRecords / getRecord 传 `textFormat: 'original'`;不传就没有 `originalValue`,页面只能显示纯文本。同步入库、导出、统计等不展示的读取不要传——返回体会变大,也没有用处。编辑表单读取记录时同样要传,否则拿不到分段,第 4 条的含链接提示永远不出现。传了也可能拿不到(服务端开关关闭),渲染时必须能退回 `text`。
378
+
379
+ ### 文本字段写入分段
380
+
381
+ Text 字段写入除了字符串,还可以传分段数组 `TextSegment[]`,用来保留或写入链接、@。
382
+
383
+ **原样写回 / 改文字保链接**:读取时传 `textFormat: 'original'`,把要改的普通文字段(`type: 'text'`)的 `text` 改掉,其余段(url、mention、空段)**原样保留不要删字段**,整个数组作为写入值。
384
+
385
+ ```typescript
386
+ const detail = await capabilityClient.load(pluginInstanceId).call<{ id: string; record: Record<string, any> }>('getRecord', {
387
+ recordID: recordId,
388
+ textFormat: 'original',
389
+ });
390
+ const value = detail.record['文本字段'] as { text: string; originalValue?: TextSegment[] } | null | undefined;
391
+ // 空单元格读回来没有这个字段(null),按空分段处理;有文字却拿不到 originalValue(未传 textFormat、服务端开关关闭或回滚)才报错:
392
+ // 这时不能按分段写回(写入 [] 会清空单元格),也不能静默跳过(用户会以为已保存)
393
+ const segments: TextSegment[] | undefined = value?.originalValue ?? (value?.text ? undefined : []);
394
+ if (!segments) {
395
+ throw new Error('暂时无法保留文本中的链接,请稍后重试');
396
+ }
397
+ // 改开头的文字:第一段是普通文字就改它,否则(空单元格、开头是链接或 @)在最前面补一段文字
398
+ const next: TextSegment[] =
399
+ segments[0]?.type === 'text'
400
+ ? [{ ...segments[0], text: '新的开头文字' }, ...segments.slice(1)]
401
+ : [{ type: 'text', text: '新的开头文字' }, ...segments];
402
+ await capabilityClient.load(pluginInstanceId).call('batchUpdateRecords', {
403
+ records: [{ id: recordId, record: { 文本字段: next } }],
404
+ });
405
+ ```
406
+
407
+ **程序化写入链接**:
408
+
409
+ ```typescript
410
+ await capabilityClient.load(pluginInstanceId).call('batchAddRecords', {
411
+ records: [
412
+ {
413
+ record: {
414
+ 文本字段: [
415
+ { type: 'text', text: '详情见 ' },
416
+ { type: 'url', text: '需求文档', link: 'https://example.com/doc' },
417
+ ],
418
+ },
419
+ },
420
+ ],
421
+ });
422
+ ```
423
+
424
+ **写入规则**(每段缺字符串 `type` 或 `text` 时插件入参校验直接报错、不会到多维表格;其余不满足的由多维表格拒绝,错误码 800074060,message 说明原因):
425
+
426
+ - 每段必须有字符串 `type` 和 `text`(空段写 `''`)
427
+ - `url` 段必须有 `text` 和 `link`(`link` 不要传空串:多维表格会把它补成 `http://<显示文字>`)
428
+ - @文档(非 User 的 mention)必须同时有 `token` 和 `link`,且 `mentionType` 必须与链接的文档类型一致——**不要自己拼 @文档,复用读到的 mention 段**
429
+ - @人段 `mentionType: 'User'`、`token` 为读到的 open_id
430
+ - `mentionNotify`:原样透传。BOE 实测通过接口写入(含原样写回)未观察到 @ 通知,以多维表格侧说明为准;不要为了「不打扰」去删改读到的 mention 段
431
+ - 空数组 `[]` 会清空单元格;要清空才这样写
432
+
433
+ **编辑表单不变**:用户在文本框里编辑文本仍提交字符串(链接会变纯文本,保留第 4 条的编辑提示);文本框未改动时仍然不提交该字段(与「只提交改过的字段」一致)。不要把文本框的字符串和 `originalValue` 拼接。
434
+
314
435
  ---
315
436
 
316
437
  ## 推荐 UI 组件
317
438
 
318
439
  | BizType | 展示 | 录入 |
319
440
  | ---------------------------- | -------------------------------------------------------------------- | ------------------------------------------- |
441
+ | `Text` | `<BitableText value={val} />`(见下方示例) | `<Textarea />`(多行;不能用单行 `<Input>`,否则换行会在编辑时被吞掉) |
320
442
  | `User` | `<UserDisplay value={ids.map(id => id.toString())} />` | `<UserSelect multiple />` |
321
443
  | `Url` | `<Hyperlink value={val} readOnly />` | `<Hyperlink readOnly={false} />` |
322
444
  | `SingleSelect`/`MultiSelect` | 文本/标签 | `<Select>` 选项来自 enumValues |
@@ -328,8 +450,53 @@ const chartData =
328
450
 
329
451
  BizType 为 `User` 的字段展示必须使用 business-ui `UserDisplay`;不要直接渲染、拼接或 `join` 用户 ID 文本。
330
452
 
453
+ BizType 为 `Text` 的字段展示必须使用下方 `BitableText`;不要直接渲染 `.text`(多行会挤成一行,链接不可点击)。
454
+
331
455
  ### 组件代码示例
332
456
 
457
+ ```tsx
458
+ import { Textarea } from '@/components/ui/textarea';
459
+
460
+ // 文本展示:按 originalValue 分段渲染(TextSegment 定义见「BizType 读写格式对照表」)
461
+ function BitableText({ value }: { value?: { text: string; originalValue?: TextSegment[] } }) {
462
+ if (!value?.text) return <span>-</span>;
463
+ // 未传 textFormat、服务端开关关闭或回滚时没有 originalValue,退回纯文本
464
+ const segments = value.originalValue ?? [{ type: 'text', text: value.text }];
465
+ return (
466
+ // pre-wrap 保留多行文本的换行
467
+ <span className="whitespace-pre-wrap break-words">
468
+ {segments.map((seg, i) =>
469
+ // 只有 http(s) 链接可点击;@人没有 link,按文字显示
470
+ typeof seg.link === 'string' && /^https?:\/\//.test(seg.link) ? (
471
+ <a key={i} href={seg.link} target="_blank" rel="noopener noreferrer" className="text-primary underline">
472
+ {seg.text}
473
+ </a>
474
+ ) : (
475
+ <span key={i}>{seg.text}</span>
476
+ ),
477
+ )}
478
+ </span>
479
+ );
480
+ }
481
+ ```
482
+
483
+ 示例里的 `/^https?:\/\//` 校验不能省:链接来自表格里的用户输入,直接塞进 `href` 会放过 `javascript:` 这类链接。
484
+
485
+ ```tsx
486
+ // 文本录入:多行 Textarea(不能用单行 Input,换行会被吞掉)
487
+ // formData 回填时 Text 字段取 .text(字符串),见「代码实现注意事项」第 4 条
488
+ <Textarea
489
+ value={formData['文本字段'] ?? ''}
490
+ onChange={(e) => setFormData({ ...formData, 文本字段: e.target.value })}
491
+ placeholder="请输入"
492
+ />
493
+ {record['文本字段']?.originalValue?.some((seg) => seg.type === 'url' || seg.type === 'mention') && (
494
+ <p className="text-xs text-muted-foreground">该文本包含链接,保存后链接将变为纯文本</p>
495
+ )}
496
+
497
+ // 保存时只提交改过的字段,见「代码实现注意事项」第 4 条
498
+ ```
499
+
333
500
  ```tsx
334
501
  // 人员展示(business-ui UserDisplay 使用 value;不要传 users)
335
502
  <UserDisplay
@@ -379,8 +546,10 @@ BizType 为 `User` 的字段展示必须使用 business-ui `UserDisplay`;不
379
546
 
380
547
  ```tsx
381
548
  const columns = [
382
- // 文本字段 - 注意读取格式是 { text: string }
383
- { title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' },
549
+ // 文本字段 - 用 BitableText 分段渲染(链接可点击、保留换行),不要 dataIndex 直接取 text
550
+ { title: '文本', key: 'text', render: (_, r) => <BitableText value={r.record['文本字段']} /> },
551
+ // @deprecated 旧写法:{ title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' }
552
+ // 仍能运行,但多行挤成一行、链接不可点击;新页面不要再用,存量页面改到这一列时换成上面的 BitableText 写法
384
553
  // 人员字段 - 使用 business-ui UserDisplay 组件,传 value(用户 ID 字符串数组),不要传 users
385
554
  {
386
555
  title: '负责人',
@@ -424,6 +593,28 @@ const columns = [
424
593
 
425
594
  ---
426
595
 
596
+ ## ⚠️ 文本字段(Text)常见错误
597
+
598
+ **1. 直接渲染 `record['文本字段'].text` → 多行挤成一行、链接不可点击**
599
+
600
+ ```tsx
601
+ // ❌ 错误
602
+ <span>{record.record['文本字段']?.text}</span>
603
+
604
+ // ✅ 正确:用 BitableText(见「推荐 UI 组件」)
605
+ <BitableText value={record.record['文本字段']} />
606
+ ```
607
+
608
+ **2. 用 `originalValue` 做筛选、排序或等值比较** — 一律用 `text`;`originalValue` 只用于展示
609
+
610
+ **3. 编辑表单把整条记录原样提交 → 没改的文本字段里的链接被覆盖成纯文本** — 只提交改过的字段;表单回填 Text 用 `.text`(见「代码实现注意事项」第 4 条)
611
+
612
+ **4. 读取时不传 `textFormat`,却去读 `originalValue` → 永远拿不到分段** — 列表、详情、编辑页这类展示或编辑文本字段的读取,searchRecords / getRecord 都传 `textFormat: 'original'`;同步、导出、统计等不展示的读取不传
613
+
614
+ **5. 改写分段时删掉 mention 段的 `token`/`link` 或改了 `mentionType` → 写入被拒** — 只改 `type: 'text'` 段的 `text`,其余段原样保留(见「文本字段写入分段」)
615
+
616
+ ---
617
+
427
618
  ## 搜索过滤条件规则
428
619
 
429
620
  ### 核心规则
@@ -387,6 +387,8 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
387
387
  4. 调用侧在生成调用代码前,**必须**通过 get_plugin_ai_json 获取该插件实例的运行时投影(plugin_Instance.ai.json),并以其作为入参/出参 schema 与 action 列表的唯一权威依据,禁止自行猜测。
388
388
  5. 若插件表单字段中包含 card_content 且其语义为飞书消息卡片,则生成/更新 PluginInstance 配置时,formValue.card_content 必须是 JSON Object,不能是转义字符串;并且卡片 DSL 的最后两个元素必须固定。
389
389
 
390
+ > **feishu-bitable 字段类型的传入**:用户只给了 Base 链接、没有在界面上选择表格时,平台不读取源表结构,`fields` 的类型由平台 LLM 按 `requirementsSummary` 生成,会按字段名猜(如「价格」→ Currency、「状态」→ SingleSelect)。CREATE 前,lark-cli 可用时先执行 `lark-cli base +field-list --base-token <appToken> --table-id <tableID>` 读取真实字段,在 `requirementsSummary` 里写明 appToken、tableID,并逐个写明「字段名:类型」,单选 / 多选字段附上选项值。例:`字段:物料编码:Text, 标准价格:Text, 同步状态:SingleSelect(待同步/已同步)`。多维表格实例不允许 UPDATE,类型写错只能重新 CREATE。
391
+
390
392
  ---
391
393
 
392
394
  ## GetPluginInstanceAIJson 工具使用指南
@@ -56,8 +56,8 @@
56
56
  record.v1.0版本 // 会被错误解析
57
57
  ```
58
58
 
59
- 4. **特殊字段需特化展示**:Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
60
- 5. **特殊字段需专用录入组件**:SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
59
+ 4. **特殊字段需特化展示**:Text(`BitableText` 分段渲染:链接可点击、保留换行)、Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
60
+ 5. **特殊字段需专用录入组件**:Text(多行文本框 `Textarea`)、SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
61
61
 
62
62
  ---
63
63
 
@@ -85,7 +85,8 @@
85
85
 
86
86
  | BizType | 写入格式 | 读取格式 | 说明 |
87
87
  | --------------------------------------- | ---------------- | ------------------ | ------------------------ |
88
- | `Text`/`Email`/`Barcode` | `string` | `{ text: string }` | 写入纯字符串,读取为对象 |
88
+ | `Text` | `string \| TextSegment[]` | `{ text: string; originalValue?: TextSegment[] }` | 写入:纯文本直接传字符串;需要保留或写入链接、@ 时传分段数组,通常就是读到的 `originalValue` 修改后原样回传(见「文本字段写入分段」)。读取:`text` 是拼接后的纯文本,用于筛选、排序、比较;展示用 `originalValue`。读取时传 `textFormat: 'original'` 才有 `originalValue`;服务端开关关闭时也可能缺失,缺失时退回 `text` |
89
+ | `Email`/`Barcode` | `string` | `{ text: string }` | 写入纯字符串,读取为对象 |
89
90
  | `Phone` | `string` | `string` | 读写均为字符串 |
90
91
  | `Number`/`Progress`/`Currency`/`Rating` | `number` | `number` | 数值类型 |
91
92
  | `SingleSelect` | `string` | `string` | 必须匹配 enumValues |
@@ -96,6 +97,22 @@
96
97
  | `Url` | `{ text, link }` | `{ text, link }` | 读写格式一致 |
97
98
  | `Attachment` | `string[]` | 复杂对象 | 写入文件 URL 数组 |
98
99
 
100
+ `TextSegment` 定义:
101
+
102
+ ```typescript
103
+ // 多维表格文本字段的一段内容,原样来自多维表格;各段 text 依次拼接等于外层的 text
104
+ type TextSegment = {
105
+ type: string; // 'text' 普通文字(可能含 \n,也可能是空串)/ 'url' 超链接 / 'mention' @文档或@人;遇到不认识的按 text 显示
106
+ text: string;
107
+ link?: string; // url 段、@文档段的地址
108
+ mentionType?: string | number; // mention 段:读出是 Bitable / Docx / Wiki / User 等字符串,取值开放,按有没有 link 判断能否点击;写入接受字符串或数字,必须与链接的文档类型一致
109
+ token?: string; // @文档的文档 token;@人时是飞书 open_id,不是妙搭用户 ID,不要拿去查用户或和人员字段比较
110
+ [k: string]: unknown; // 其他字段原样保留
111
+ };
112
+ ```
113
+
114
+ 兼容说明:`originalValue` 是可选的新增字段,不传 `textFormat` 时返回内容与原来完全一致,只读 `.text` 的存量代码不用改也能正常运行。要链接可点击、保留换行时,再按下文迁移:读取加 `textFormat: 'original'`,展示从 `.text` 换成 `BitableText`。写入同理:Text 字段继续传字符串的存量代码行为不变,分段数组是新增的可选写法。
115
+
99
116
  ### 只读字段
100
117
 
101
118
  | BizType | 读取格式 |
@@ -157,7 +174,7 @@
157
174
 
158
175
  ### dimensions value 类型(常见)
159
176
 
160
- - `Text` / `Email` / `Barcode`: `{ text: string }`
177
+ - `Text` / `Email` / `Barcode`: `{ text: string }`(Text 分组维度只有 `text`,没有 `originalValue`,不要按 originalValue 渲染分组标签)
161
178
  - `Number`: `string`
162
179
  - `Progress`: `string`
163
180
  - `Currency`: `string`
@@ -207,6 +224,9 @@ const response = await (bitable as any).call('searchRecords', {
207
224
  conditions: [{ fieldName: '状态', operator: 'is', value: ['进行中'] }],
208
225
  },
209
226
  pageSize: 20,
227
+ // 列表展示文本字段(链接可点击、保留换行)时传;不传就没有 originalValue,页面只能显示纯文本。
228
+ // 同步入库、导出、统计等不展示的读取不要传,见「代码实现注意事项」第 6 条
229
+ textFormat: 'original',
210
230
  });
211
231
 
212
232
  // ⚠️ 响应数据直接从 response 访问,不要加 .data 或 .output 前缀
@@ -216,17 +236,17 @@ const { records, hasMore, total, pageToken } = response;
216
236
  console.log(`共 ${total} 条,本页 ${records.length} 条,还有更多: ${hasMore}`);
217
237
 
218
238
  // 获取单条记录
219
- const detail = await (bitable as any).call('getRecord', { recordID: 'recXXX' });
239
+ const detail = await (bitable as any).call('getRecord', { recordID: 'recXXX', textFormat: 'original' }); // 详情 / 编辑页要展示文本字段,传 textFormat
220
240
  // ✅ 正确:detail.id / detail.record
221
241
  // ❌ 错误:detail.data.output.id
222
- const title = detail.record['标题']?.text;
242
+ const title = detail.record['标题']?.text; // 纯文本,用于筛选、比较、导出;展示用 BitableText
223
243
 
224
244
  // 批量新增(注意:写入时用写入格式,字段名必须用引号包裹)
225
245
  await (bitable as any).call('batchAddRecords', {
226
246
  records: [
227
247
  {
228
248
  record: {
229
- 文本字段: '内容', // Text: string(非 { text: string })
249
+ 文本字段: '内容', // Text: string(非 { text: string });要带链接时传 TextSegment[],见「文本字段写入分段」
230
250
  数字字段: 100, // Number: number
231
251
  单选字段: '选项A', // SingleSelect: string(必须匹配 enumValues)
232
252
  多选字段: ['标签1', '标签2'], // MultiSelect: string[]
@@ -239,7 +259,7 @@ await (bitable as any).call('batchAddRecords', {
239
259
  ],
240
260
  });
241
261
 
242
- // 批量更新
262
+ // 批量更新(record 里只放改过的字段,见下方「代码实现注意事项」第 4 条)
243
263
  await (bitable as any).call('batchUpdateRecords', {
244
264
  records: [{ id: 'record_id', record: { 文本字段: '新内容' } }],
245
265
  });
@@ -248,7 +268,7 @@ await (bitable as any).call('batchUpdateRecords', {
248
268
  await (bitable as any).call('deleteRecords', { recordIDs: ['id1', 'id2'] });
249
269
 
250
270
  // 读取时注意格式差异
251
- const textValue = record.record['文本字段']?.text; // 读取时是 { text: string }
271
+ const textValue = record.record['文本字段']?.text; // 纯文本,用于筛选、比较、导出;展示用 BitableText
252
272
  const numberValue = record.record['数字字段']; // number
253
273
  const userIds = record.record['人员字段']; // number[]
254
274
 
@@ -272,7 +292,44 @@ const chartData =
272
292
  1. **响应数据直接访问,禁止猜测嵌套路径**。`capabilityClient.call()` 返回的就是最终数据,直接用 `response.records`、`response.total` 等。**禁止**加 `.data`、`.output`、`.data.output` 等前缀。如果不确定响应结构,先用 `console.log` 打印完整响应再编码,禁止凭猜测修改数据路径。
273
293
  2. 在进行写入操作时,比如确认提交时,应该有loading效果,禁止用户重复点击。
274
294
  3. 如果capabilityClient call失败,命中了error逻辑,要打印日志,同时把error message提示给用户。
275
- 4. 在进行更新操作时,只更新需要更新的字段即可,不需要对全量字段进行更新。
295
+ 4. **只提交改过的字段(硬约束)**。编辑后调用 batchUpdateRecords 时,**只提交和读取时相比值有变化的字段**,逐字段比较后再组装 `record`。Text 字段没改动时**禁止**放进请求:多维表格按字段整体覆盖,而编辑表单的文本框提交的是纯字符串,带链接的文本会被写成纯文本,链接永久丢失。要改文字又保留链接,按下方「文本字段写入分段」写分段数组。
296
+
297
+ ```typescript
298
+ // 编辑页读取:含链接提示依赖 originalValue,getRecord 也要传 textFormat(见第 6 条)
299
+ const { record: original } = await (bitable as any).call('getRecord', {
300
+ recordID: recordId,
301
+ textFormat: 'original',
302
+ });
303
+
304
+ // 表单回填:Text 字段用 .text(表单值是 string),不要把整个 { text, originalValue } 对象放进表单,
305
+ // 否则文本框显示 [object Object],保存时还会把对象当写入值提交
306
+ const initForm = {
307
+ 文本字段: original['文本字段']?.text ?? '',
308
+ 数字字段: original['数字字段'],
309
+ 多选字段: original['多选字段'] ?? [],
310
+ // ...其余可编辑字段;只读字段(创建时间、创建人、公式等)不要放进表单
311
+ // Attachment 读取是复杂对象、写入是文件 URL 数组,编辑时按写入格式单独维护
312
+ };
313
+
314
+ // 保存:form 是以 initForm 初始化、用户编辑后的表单值;逐字段和读取值比较,只组装改过的字段
315
+ const changed: Record<string, unknown> = {};
316
+ // Text:用 .text 和表单值比
317
+ if (form['文本字段'] !== (original['文本字段']?.text ?? '')) changed['文本字段'] = form['文本字段'];
318
+ // 标量字段:直接比较
319
+ if (form['数字字段'] !== original['数字字段']) changed['数字字段'] = form['数字字段'];
320
+ // 数组、对象字段(多选、人员、超链接等)按值比较,不要用 !==(引用不同会被误判为已修改)
321
+ const sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
322
+ if (!sameValue(form['多选字段'], original['多选字段'] ?? [])) changed['多选字段'] = form['多选字段'];
323
+ // ...其余字段同样逐个比较;只读字段不要放进 changed
324
+ // Attachment 读取格式与写入格式不同:未改动时不提交,改动时按写入格式(文件 URL 数组)提交
325
+ if (Object.keys(changed).length > 0) {
326
+ await (bitable as any).call('batchUpdateRecords', {
327
+ records: [{ id: recordId, record: changed }],
328
+ });
329
+ }
330
+ ```
331
+
332
+ 含链接文本的编辑提示:编辑控件检测到 `originalValue` 里有 `url` 或 `mention` 段时,在控件下方提示「该文本包含链接,保存后链接将变为纯文本」(示例见「推荐 UI 组件」)。编辑表单读取记录时必须传 `textFormat: 'original'`,否则拿不到分段,提示永远不出现。
276
333
  5. **分页必须使用游标分页,禁止全量拉取后切片**。先拉全部数据再 `slice` 会导致性能极差、浪费带宽和内存。每次只拉当前页数据,通过 `pageToken` 翻页:
277
334
 
278
335
  ```typescript
@@ -280,10 +337,69 @@ const chartData =
280
337
  const result = await (ex as any).call('searchRecords', {
281
338
  pageSize: 10,
282
339
  pageToken: cursor, // 上一页返回的 pageToken
340
+ // 要把文本字段展示给用户时再加 textFormat: 'original'(见第 6 条)
283
341
  });
284
342
  // 返回:{ records, pageToken(下一页游标), hasMore, total }
285
343
  ```
286
344
 
345
+ 6. **展示或编辑文本字段时显式要原始分段**。列表、详情、编辑表单回显这类要把文本字段展示给用户(或在编辑时提示链接)的读取,searchRecords / getRecord 传 `textFormat: 'original'`;不传就没有 `originalValue`,页面只能显示纯文本。同步入库、导出、统计等不展示的读取不要传——返回体会变大,也没有用处。编辑表单读取记录时同样要传,否则拿不到分段,第 4 条的含链接提示永远不出现。传了也可能拿不到(服务端开关关闭),渲染时必须能退回 `text`。
346
+
347
+ ### 文本字段写入分段
348
+
349
+ Text 字段写入除了字符串,还可以传分段数组 `TextSegment[]`,用来保留或写入链接、@。
350
+
351
+ **原样写回 / 改文字保链接**:读取时传 `textFormat: 'original'`,把要改的普通文字段(`type: 'text'`)的 `text` 改掉,其余段(url、mention、空段)**原样保留不要删字段**,整个数组作为写入值。
352
+
353
+ ```typescript
354
+ const detail = await (bitable as any).call('getRecord', {
355
+ recordID: recordId,
356
+ textFormat: 'original',
357
+ });
358
+ const value = detail.record['文本字段'] as { text: string; originalValue?: TextSegment[] } | null | undefined;
359
+ // 空单元格读回来没有这个字段(null),按空分段处理;有文字却拿不到 originalValue(未传 textFormat、服务端开关关闭或回滚)才报错:
360
+ // 这时不能按分段写回(写入 [] 会清空单元格),也不能静默跳过(用户会以为已保存)
361
+ const segments: TextSegment[] | undefined = value?.originalValue ?? (value?.text ? undefined : []);
362
+ if (!segments) {
363
+ throw new Error('暂时无法保留文本中的链接,请稍后重试');
364
+ }
365
+ // 改开头的文字:第一段是普通文字就改它,否则(空单元格、开头是链接或 @)在最前面补一段文字
366
+ const next: TextSegment[] =
367
+ segments[0]?.type === 'text'
368
+ ? [{ ...segments[0], text: '新的开头文字' }, ...segments.slice(1)]
369
+ : [{ type: 'text', text: '新的开头文字' }, ...segments];
370
+ await (bitable as any).call('batchUpdateRecords', {
371
+ records: [{ id: recordId, record: { 文本字段: next } }],
372
+ });
373
+ ```
374
+
375
+ **程序化写入链接**:
376
+
377
+ ```typescript
378
+ await (bitable as any).call('batchAddRecords', {
379
+ records: [
380
+ {
381
+ record: {
382
+ 文本字段: [
383
+ { type: 'text', text: '详情见 ' },
384
+ { type: 'url', text: '需求文档', link: 'https://example.com/doc' },
385
+ ],
386
+ },
387
+ },
388
+ ],
389
+ });
390
+ ```
391
+
392
+ **写入规则**(每段缺字符串 `type` 或 `text` 时插件入参校验直接报错、不会到多维表格;其余不满足的由多维表格拒绝,错误码 800074060,message 说明原因):
393
+
394
+ - 每段必须有字符串 `type` 和 `text`(空段写 `''`)
395
+ - `url` 段必须有 `text` 和 `link`(`link` 不要传空串:多维表格会把它补成 `http://<显示文字>`)
396
+ - @文档(非 User 的 mention)必须同时有 `token` 和 `link`,且 `mentionType` 必须与链接的文档类型一致——**不要自己拼 @文档,复用读到的 mention 段**
397
+ - @人段 `mentionType: 'User'`、`token` 为读到的 open_id
398
+ - `mentionNotify`:原样透传。BOE 实测通过接口写入(含原样写回)未观察到 @ 通知,以多维表格侧说明为准;不要为了「不打扰」去删改读到的 mention 段
399
+ - 空数组 `[]` 会清空单元格;要清空才这样写
400
+
401
+ **编辑表单不变**:用户在文本框里编辑文本仍提交字符串(链接会变纯文本,保留第 4 条的编辑提示);文本框未改动时仍然不提交该字段(与「只提交改过的字段」一致)。不要把文本框的字符串和 `originalValue` 拼接。
402
+
287
403
  ---
288
404
 
289
405
  ## 推荐 UI 组件(vite-react / shadcn-ui)
@@ -292,6 +408,7 @@ vite-react 用的是 shadcn-ui + Radix Primitives(无 spark UI 的 `<UserDispl
292
408
 
293
409
  | BizType | 展示 | 录入 |
294
410
  | ---------------------------- | -------------------------------------------------------------------- | --------------------------------------------------- |
411
+ | `Text` | `<BitableText value={val} />`(见下方示例,原生元素 + tailwind) | shadcn `<Textarea />`(多行;不能用单行 `<Input>`,否则换行会在编辑时被吞掉) |
295
412
  | `User` | `<Avatar>` + 名字(按需查询 `getCurrentUserProfile` 等接口) | 自己拼:`<Select>` / `<Combobox>` 加搜索接口 |
296
413
  | `Url` | `<a href={val.link} target="_blank">{val.text}</a>` | 两个 `<Input>`(一个填 text,一个填 link) |
297
414
  | `SingleSelect`/`MultiSelect` | 文本 / `<Badge>` 标签 | `<Select>` 选项来自 enumValues |
@@ -303,12 +420,15 @@ vite-react 用的是 shadcn-ui + Radix Primitives(无 spark UI 的 `<UserDispl
303
420
 
304
421
  > spark 体系下的 `<UserDisplay>` / `<UserSelect>` / `<Hyperlink>` 在 vite-react 不可用。`@lark-apaas/client-toolkit-lite` 没有暴露这些组件 —— 需要自己用 shadcn 的 `<Avatar>` / `<Select>` / `<Combobox>` 拼装。人员数据的获取需要走业务 API(视具体 capability 而定)。
305
422
 
423
+ BizType 为 `Text` 的字段展示必须使用下方 `BitableText`(原生元素 + tailwind 实现,不依赖 spark 组件);不要直接渲染 `.text`(多行会挤成一行,链接不可点击)。
424
+
306
425
  ### 组件代码示例
307
426
 
308
427
  ```tsx
309
428
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
310
429
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
311
430
  import { Checkbox } from '@/components/ui/checkbox';
431
+ import { Textarea } from '@/components/ui/textarea';
312
432
 
313
433
  // 单选下拉(选项必须来自 enumValues)
314
434
  <Select value={value} onValueChange={setValue}>
@@ -333,16 +453,57 @@ import { Checkbox } from '@/components/ui/checkbox';
333
453
  <a href={record['超链接']?.link} target="_blank" rel="noreferrer" className="text-primary underline">
334
454
  {record['超链接']?.text}
335
455
  </a>
456
+
457
+ // 文本录入:shadcn 多行 Textarea(不能用单行 Input,换行会被吞掉)
458
+ // formData 回填时 Text 字段取 .text(字符串),见「代码实现注意事项」第 4 条
459
+ <Textarea
460
+ value={formData['文本字段'] ?? ''}
461
+ onChange={(e) => setFormData({ ...formData, 文本字段: e.target.value })}
462
+ placeholder="请输入"
463
+ />
464
+ {record['文本字段']?.originalValue?.some((seg) => seg.type === 'url' || seg.type === 'mention') && (
465
+ <p className="text-xs text-muted-foreground">该文本包含链接,保存后链接将变为纯文本</p>
466
+ )}
467
+
468
+ // 保存时只提交改过的字段,见「代码实现注意事项」第 4 条
469
+ ```
470
+
471
+ ```tsx
472
+ // 文本展示:按 originalValue 分段渲染(TextSegment 定义见「BizType 读写格式对照表」)
473
+ function BitableText({ value }: { value?: { text: string; originalValue?: TextSegment[] } }) {
474
+ if (!value?.text) return <span>-</span>;
475
+ // 未传 textFormat、服务端开关关闭或回滚时没有 originalValue,退回纯文本
476
+ const segments = value.originalValue ?? [{ type: 'text', text: value.text }];
477
+ return (
478
+ // pre-wrap 保留多行文本的换行
479
+ <span className="whitespace-pre-wrap break-words">
480
+ {segments.map((seg, i) =>
481
+ // 只有 http(s) 链接可点击;@人没有 link,按文字显示
482
+ typeof seg.link === 'string' && /^https?:\/\//.test(seg.link) ? (
483
+ <a key={i} href={seg.link} target="_blank" rel="noopener noreferrer" className="text-primary underline">
484
+ {seg.text}
485
+ </a>
486
+ ) : (
487
+ <span key={i}>{seg.text}</span>
488
+ ),
489
+ )}
490
+ </span>
491
+ );
492
+ }
336
493
  ```
337
494
 
495
+ 示例里的 `/^https?:\/\//` 校验不能省:链接来自表格里的用户输入,直接塞进 `href` 会放过 `javascript:` 这类链接。
496
+
338
497
  ---
339
498
 
340
499
  ## 表格列渲染示例
341
500
 
342
501
  ```tsx
343
502
  const columns = [
344
- // 文本字段 - 注意读取格式是 { text: string }
345
- { title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' },
503
+ // 文本字段 - 用 BitableText 分段渲染(链接可点击、保留换行),不要 dataIndex 直接取 text
504
+ { title: '文本', key: 'text', render: (_, r) => <BitableText value={r.record['文本字段']} /> },
505
+ // @deprecated 旧写法:{ title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' }
506
+ // 仍能运行,但多行挤成一行、链接不可点击;新页面不要再用,存量页面改到这一列时换成上面的 BitableText 写法
346
507
  // 人员字段 - vite-react 自行用 Avatar + 名字渲染(数据来源走业务接口)
347
508
  {
348
509
  title: '负责人',
@@ -391,6 +552,36 @@ const columns = [
391
552
 
392
553
  ---
393
554
 
555
+ ## ⚠️ 文本字段(Text)常见错误
556
+
557
+ ### 1. 直接渲染 `record['文本字段'].text` → 多行挤成一行、链接不可点击
558
+
559
+ ```tsx
560
+ // ❌ 错误
561
+ <span>{record.record['文本字段']?.text}</span>
562
+
563
+ // ✅ 正确:用 BitableText(见「推荐 UI 组件」)
564
+ <BitableText value={record.record['文本字段']} />
565
+ ```
566
+
567
+ ### 2. 用 `originalValue` 做筛选、排序或等值比较
568
+
569
+ 一律用 `text`;`originalValue` 只用于展示。
570
+
571
+ ### 3. 编辑表单把整条记录原样提交 → 没改的文本字段里的链接被覆盖成纯文本
572
+
573
+ 只提交改过的字段;表单回填 Text 用 `.text`(见「代码实现注意事项」第 4 条)。
574
+
575
+ ### 4. 读取时不传 `textFormat`,却去读 `originalValue` → 永远拿不到分段
576
+
577
+ 列表、详情、编辑页这类展示或编辑文本字段的读取,searchRecords / getRecord 都传 `textFormat: 'original'`;同步、导出、统计等不展示的读取不传。
578
+
579
+ ### 5. 改写分段时删掉 mention 段的 `token`/`link` 或改了 `mentionType` → 写入被拒
580
+
581
+ 只改 `type: 'text'` 段的 `text`,其余段原样保留(见「文本字段写入分段」)。
582
+
583
+ ---
584
+
394
585
  ## 搜索过滤条件规则
395
586
 
396
587
  ### 核心规则