aitable-workflow-core 0.1.23 → 0.1.24
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/index.d.ts +16 -4
- package/dist/index.js +190 -188
- package/package.json +2 -2
- package/skills/aitable-workflow/SKILL.md +18 -28
- package/skills/aitable-workflow/{reference → references}/config-surface.md +15 -1
- package/skills/aitable-workflow/{reference → references}/event-handlers.md +3 -1
- package/skills/aitable-workflow/references/event-sources.md +229 -0
- package/skills/aitable-workflow/references/field-types.md +73 -0
- package/skills/aitable-workflow/{reference → references}/generated/event-sources.md +16 -17
- package/skills/aitable-workflow/{reference → references}/invariants.md +4 -1
- package/skills/aitable-workflow/{reference → references}/multi-workflow.md +13 -7
- package/skills/aitable-workflow/{reference → references}/profile-and-roles.md +9 -3
- package/skills/aitable-workflow/{reference → references}/runtime-refs.md +8 -40
- package/skills/aitable-workflow/{reference → references}/workflow-yml.md +12 -10
- package/skills/aitable-workflow-design/SKILL.md +7 -5
- package/skills/aitable-workflow-design/patterns/brownfield-editing.md +2 -2
- package/skills/aitable-workflow-design/patterns/clarification-checklist.md +21 -28
- package/skills/aitable-workflow-design/patterns/config-in-aitable.md +4 -5
- package/skills/aitable-workflow-design/patterns/dingtalk-bot-patterns.md +129 -0
- package/skills/aitable-workflow-design/patterns/external-aitable-read.md +132 -0
- package/skills/aitable-workflow-design/patterns/greenfield-checklist.md +10 -10
- package/skills/aitable-workflow-design/patterns/output-and-report.md +2 -2
- package/skills/aitable-workflow/reference/event-sources.md +0 -217
- /package/skills/aitable-workflow/{reference → references}/aitable-cell-values.md +0 -0
- /package/skills/aitable-workflow/{reference → references}/automations.md +0 -0
- /package/skills/aitable-workflow/{reference → references}/generated/core-api.md +0 -0
- /package/skills/aitable-workflow/{reference → references}/generated/toolkits.md +0 -0
- /package/skills/aitable-workflow/{reference → references}/recipe-contract.md +0 -0
- /package/skills/aitable-workflow/{reference → references}/workspace-context.md +0 -0
|
@@ -18,24 +18,6 @@ recipe:
|
|
|
18
18
|
api_key: "${env.IDEALAB_API_KEY}"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
### 可选引用 `${env.NAME?}`
|
|
22
|
-
|
|
23
|
-
带 `?` 后缀表示可选:
|
|
24
|
-
|
|
25
|
-
- 整值引用 → 解析为 `undefined`,该字段视为**未设置**
|
|
26
|
-
- 嵌入引用(`"prefix-${env.X?}"`)→ 解析为空字符串
|
|
27
|
-
|
|
28
|
-
用于「两条凭据路径任选其一」的场景:
|
|
29
|
-
|
|
30
|
-
```yaml
|
|
31
|
-
toolkit_config:
|
|
32
|
-
messaging:
|
|
33
|
-
webhook: "${env.DINGTALK_WEBHOOK?}" # 二者
|
|
34
|
-
robots: "${env.DINGTALK_ROBOTS?}" # satisfy 其一即可
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
`validate` 对可选引用只给 suggestion,不报 error。
|
|
38
|
-
|
|
39
21
|
### JSON 自动解析
|
|
40
22
|
|
|
41
23
|
环境变量值以 `{` 或 `[` 开头时会被 **`JSON.parse`**(解析失败直接抛错)。这是 JSON map 型凭据的实现方式:
|
|
@@ -69,18 +51,6 @@ DINGTALK_WEBHOOK=
|
|
|
69
51
|
|
|
70
52
|
**绝不**把真实凭据写进 `.env.example`,也绝不生成 `.env`(那是用户自己填的)。
|
|
71
53
|
|
|
72
|
-
### 事件源 config 里的整值引用会被保护
|
|
73
|
-
|
|
74
|
-
`event_sources[].config` 中**整值**的运行时引用在配置加载插值时会被**遮蔽保留**,不会被提前求值——这样它们能保持 live-read 语义交给 `ConfigCenter`。但**嵌入式**引用(`"prefix-${env.X}"`)仍会被立即插值。
|
|
75
|
-
|
|
76
|
-
所以事件源里要 live-read 就写整值形式:
|
|
77
|
-
|
|
78
|
-
```yaml
|
|
79
|
-
config:
|
|
80
|
-
conversation_ids: "${globalVar.group_pull_list}" # ✓ 整值,保持 live-read
|
|
81
|
-
# conversation_ids: "list-${globalVar.x}" # ✗ 嵌入式,会被提前求值
|
|
82
|
-
```
|
|
83
|
-
|
|
84
54
|
## `${config.a.b}` — 项目配置
|
|
85
55
|
|
|
86
56
|
调用时 live-read,dot-path 访问 `aitable-workflow.config.yml` 的内容。配置热更新后下次调用即生效。
|
|
@@ -116,13 +86,13 @@ const admins = ctx.getGlobalVar<string[]>("admins") ?? [];
|
|
|
116
86
|
### 配置里消费
|
|
117
87
|
|
|
118
88
|
```yaml
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
89
|
+
workflows:
|
|
90
|
+
<workflowId>:
|
|
91
|
+
event_sources:
|
|
92
|
+
群消息监听:
|
|
93
|
+
conversation_ids: "${globalVar.group_pull_list}"
|
|
94
|
+
handler_config:
|
|
95
|
+
admins: "${globalVar.admins}"
|
|
126
96
|
```
|
|
127
97
|
|
|
128
98
|
事件源支持 `subscribe`,发布方写入时会触发消费方 reconcile,不必自己轮询。
|
|
@@ -139,11 +109,10 @@ event_sources:
|
|
|
139
109
|
步骤配置里的 ref 必须自己解:直接把 `config.x` 当值用会拿到字面串。因为 step/handler 的 config schema
|
|
140
110
|
普遍宽松,这类错误**不报错只静默降级**(名单恒为空、开关恒为假),比抛错难排得多。
|
|
141
111
|
|
|
142
|
-
完整的「配置存 AI 表格 + 自动同步」落地模式见 `../../aitable-workflow-design/patterns/config-in-aitable.md`。
|
|
143
112
|
|
|
144
113
|
## 选择指南
|
|
145
114
|
|
|
146
|
-
- 值是**凭据或部署差异** → `${env.X}
|
|
115
|
+
- 值是**凭据或部署差异** → `${env.X}`
|
|
147
116
|
- 值**已经在 config 里**,只是想复用 → `${config.a.b}`
|
|
148
117
|
- 值由**另一个 workflow 在运行期决定** → `${globalVar.x}`
|
|
149
118
|
- 值是**另一个 workflow 的静态声明**(tracker 表、事件源配置) → `{ ref: "workflow:..." }`,见 `multi-workflow.md`
|
|
@@ -154,4 +123,3 @@ event_sources:
|
|
|
154
123
|
- [ ] 可选凭据用了 `${env.X?}`
|
|
155
124
|
- [ ] 没有生成 `.env`,没有在 `.env.example` 写真实凭据
|
|
156
125
|
- [ ] `globalVar` 的消费方能容忍值不存在(重启后为空)
|
|
157
|
-
- [ ] 事件源里需要 live-read 的引用是整值形式,不是嵌入式
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`workflow.yml` 是工作流的声明式定义。运行时由 `parseWorkflowTemplate` 解析,结构错误会 fail-fast。
|
|
4
4
|
|
|
5
|
+
profile_v2 项目中,运行配置以 `profile/profile.yml` 为唯一事实源;`workflow.yml` 只放结构、tracker、字段、步骤编排与事件源身份
|
|
6
|
+
|
|
5
7
|
## 顶层结构
|
|
6
8
|
|
|
7
9
|
```yaml
|
|
@@ -18,7 +20,6 @@ tracker: # 必填,结构见下方
|
|
|
18
20
|
steps: [...] # 必填
|
|
19
21
|
edges: [...] # 必填
|
|
20
22
|
event_sources: [...] # 仅事件驱动场景,详见 event-sources.md
|
|
21
|
-
toolkit_config: {...} # 可选,本 workflow 级别的 toolkit 配置
|
|
22
23
|
notification: {...} # 可选
|
|
23
24
|
```
|
|
24
25
|
|
|
@@ -55,13 +56,15 @@ tracker:
|
|
|
55
56
|
### field_definitions 规则
|
|
56
57
|
|
|
57
58
|
1. **所有业务字段都必须在 `field_definitions` 中显式声明**(包括 `text` 类型),写在 `tracker` 内(根级已废弃)。
|
|
58
|
-
2.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
2. 完整合法类型见 [`field-types.md`](./field-types.md);常见选择:
|
|
60
|
+
- 含「日期」「时间」关键词的字段用 `date`。
|
|
61
|
+
- 有限可选项用 `singleSelect` / `multipleSelect`。
|
|
62
|
+
- 表示成员/负责人且需要发 IM 时,声明为 `user` 人员字段。
|
|
63
|
+
3. 状态字段的 `options` 必须与 `tracker.states` 的值**完全一致**。
|
|
64
|
+
4. `creator` / `lastModifier` / `createdTime` / `lastModifiedTime` 是**系统只读字段**,不写入、不出现在 FormDesigner `visible_fields`。
|
|
65
|
+
5. 系统骨架字段(`标题`/`状态`/`当前步骤`/`处理人`/`事件日志`)**无需声明**,setup 自动创建。
|
|
66
|
+
6. 业务标题字段尽量复用主键语义,不要生成无业务含义的重复标题。
|
|
67
|
+
7. `formula` / `lookup` 不在 `field_definitions` 能力范围内(只透传 `formatter` / `description`)。
|
|
65
68
|
|
|
66
69
|
### 单一决策字段模式
|
|
67
70
|
|
|
@@ -88,7 +91,6 @@ steps:
|
|
|
88
91
|
output_mapping: # key=recipe 返回 output 的键,value=写回的 AI 表格字段名
|
|
89
92
|
category: "问题类型"
|
|
90
93
|
priority: "紧急程度"
|
|
91
|
-
config: {} # 可选,该 step 的 recipe 配置(详见 config-surface.md)
|
|
92
94
|
engine: claude # 可选,覆盖该 step 的 agent 引擎
|
|
93
95
|
```
|
|
94
96
|
|
|
@@ -228,7 +230,7 @@ tracker:
|
|
|
228
230
|
- `type` 只能是 `Grid`、`FormDesigner`、`Kanban`、`Calendar`、`Gallery` 或 `Gantt`。
|
|
229
231
|
- `Grid` 用 `field_order` 控制列及顺序;其他视图用 `visible_fields` 控制展示字段及顺序。
|
|
230
232
|
- `Kanban` / `Gallery` 的 `group_by`、`Calendar` / `Gantt` 的 `date_field` 必须引用已有字段。
|
|
231
|
-
- `filter` 只对 `Grid` 下发;数组条件之间为 AND。每项必须含 `field`,并在 `equals
|
|
233
|
+
- `filter` 只对 `Grid` 下发;数组条件之间为 AND。每项必须含 `field`,并在 `equals`(单个字符串)或 `any_of`(非空字符串数组)二选一;字段与选项值必须真实存在。多值筛选写 `any_of: ["待处理", "处理中"]`。
|
|
232
234
|
- `for_each_step: needs_human | all` 只在确实需要按步骤展开视图时使用。
|
|
233
235
|
- 用户明确需要填表入口时,追加 `input_form`(`FormDesigner`);其 `visible_fields` 放用户输入字段和必要状态字段,不放「当前步骤」「事件日志」及 `creator`、`lastModifier`、`createdTime`、`lastModifiedTime` 等系统只读字段。
|
|
234
236
|
- `field_order` / `visible_fields` / `group_by` / `date_field` / `filter[].field` 不得凭空创建字段。视图需要新列时,先在该 workflow 的 `tracker.field_definitions` 中声明。
|
|
@@ -8,7 +8,7 @@ description: 把业务需求变成 aitable-workflow 产物的工作方法。当
|
|
|
8
8
|
本包回答「**怎么把需求变成产物**」。框架的字段名、语法与契约一律去 `../aitable-workflow/SKILL.md` 路由,**不要在本包里找契约,也不要凭记忆写**。
|
|
9
9
|
|
|
10
10
|
<IMPORTANT>
|
|
11
|
-
|
|
11
|
+
先路由,再动手。
|
|
12
12
|
|
|
13
13
|
不要预读全部文件;按下面的步骤,每步只读命中的 1-2 个。
|
|
14
14
|
</IMPORTANT>
|
|
@@ -22,10 +22,12 @@ description: 把业务需求变成 aitable-workflow 产物的工作方法。当
|
|
|
22
22
|
2. **判定项目形态**,选一条主线(两份清单不要混用):
|
|
23
23
|
- 全新项目(无 workflow.yml)→ `patterns/greenfield-checklist.md`
|
|
24
24
|
- 修改已有项目(已有 workflow.yml / recipes)→ `patterns/brownfield-editing.md`
|
|
25
|
-
3. **写代码前**读框架契约:从 `../aitable-workflow/SKILL.md` 的路由表按你实际要产出的部分加载
|
|
25
|
+
3. **写代码前**读框架契约:从 `../aitable-workflow/SKILL.md` 的路由表按你实际要产出的部分加载 references 文件;不可违反项见 `../aitable-workflow/references/invariants.md`。
|
|
26
26
|
4. **命中架构决策**时读对应模式:
|
|
27
27
|
- 需要跨表落库、回消息带「点回那一行」的链接、或公式计算 → `patterns/recipe-vs-automation.md`(先定引擎/自动化分工,再写任何一侧)
|
|
28
28
|
- 配置要让运营在 AI 表格里自助维护 → `patterns/config-in-aitable.md`(这是一个**独立 workflow**,不是一个 step)
|
|
29
|
+
- recipe 要读取用户已有 AI 表格、FAQ 表或外部业务参考表 → `patterns/external-aitable-read.md`
|
|
30
|
+
- 涉及钉钉企业内机器人、群机器人、Stream 监听或消息回复通道 → `patterns/dingtalk-bot-patterns.md`
|
|
29
31
|
- 需要 LLM 输出可被稳定解析 → `patterns/json-contract-and-repair.md`
|
|
30
32
|
5. **收尾** → `patterns/output-and-report.md`:核对产物清单,按三段格式汇报(已生成 / 需确认项 / 还需你补齐)。
|
|
31
33
|
6. 回到 `patterns/clarification-checklist.md` 自查一遍。
|
|
@@ -39,9 +41,9 @@ description: 把业务需求变成 aitable-workflow 产物的工作方法。当
|
|
|
39
41
|
| 改已有项目 | 已有 workflow.yml / recipes;「新增一个步骤」「加一个 workflow」 | `patterns/brownfield-editing.md` |
|
|
40
42
|
| 定引擎与自动化的分工 | 跨表落库、消息要带「点回那一行」的链接、公式计算、`automations/*.json` | `patterns/recipe-vs-automation.md` |
|
|
41
43
|
| 配置存 AI 表格由运营维护 | 「运营自己改」「不重启生效」「配置表」、globalVar 发布订阅 | `patterns/config-in-aitable.md` |
|
|
44
|
+
| 读取外部 AI 表格 | FAQ 表、用户已有表、业务参考表、跨 Base 表、整表读取 | `patterns/external-aitable-read.md` |
|
|
45
|
+
| 设计钉钉机器人通道 | 企业内机器人、自定义机器人、Stream、@机器人、群消息回复、`robot_creds` | `patterns/dingtalk-bot-patterns.md` |
|
|
42
46
|
| 让 LLM 输出可被解析 | JSON 契约、解析失败、重试、降级 | `patterns/json-contract-and-repair.md` |
|
|
43
47
|
| 收尾与汇报 | 文件写完了 | `patterns/output-and-report.md` |
|
|
44
48
|
| 查框架语法与契约 | workflow.yml / recipe / 事件源 / 字段 / profile 的准确写法 | `../aitable-workflow/SKILL.md`(那里再路由) |
|
|
45
|
-
| 查 toolkit 方法与配置 | ctx.toolkit.<name>.<method> | `../aitable-workflow/
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
| 查 toolkit 方法与配置 | ctx.toolkit.<name>.<method> | `../aitable-workflow/references/generated/toolkits.md` |
|
|
@@ -68,7 +68,7 @@ step id 是 StepEngine 的状态锚点,也是 profile 的锚点。重命名会
|
|
|
68
68
|
|
|
69
69
|
- 新配置写进 `profile/profile.yml` 的 `workflows.<id>.{toolkit_config,steps,event_sources}`;角色写进 profile 的 `roles:` 段
|
|
70
70
|
- 不要绕过 profile 直接改 `workflow.yml` 的配置(会被 profile 覆盖,改了没效果)
|
|
71
|
-
详见 `../../aitable-workflow/
|
|
71
|
+
详见 `../../aitable-workflow/references/profile-and-roles.md`。
|
|
72
72
|
|
|
73
73
|
## 常见改动的正确做法
|
|
74
74
|
|
|
@@ -91,7 +91,7 @@ step id 是 StepEngine 的状态锚点,也是 profile 的锚点。重命名会
|
|
|
91
91
|
|
|
92
92
|
### 给已有事件源加筛选条件
|
|
93
93
|
|
|
94
|
-
如果发现现有 `aitable_record` 事件源没有 `filter`,**这是应该主动修的性能隐患**(见 `../../aitable-workflow/
|
|
94
|
+
如果发现现有 `aitable_record` 事件源没有 `filter`,**这是应该主动修的性能隐患**(见 `../../aitable-workflow/references/event-sources.md`)。加 `filter` 时确认字段名与该 workflow 的 `field_definitions` 一致。
|
|
95
95
|
|
|
96
96
|
### 改字段类型或名称
|
|
97
97
|
|
|
@@ -17,15 +17,13 @@
|
|
|
17
17
|
| **需求过于笼统** | 「做个客服机器人」「帮我自动化一下招聘」 | 业务流程本身:谁发起、经过哪几个环节、每个环节谁做什么、什么算做完。**先把流程问出来**,再问下面的技术决策点 |
|
|
18
18
|
| **需求过于单薄** | 「监听群消息,是 X 类问题就回复,其他不回」 | 缺的是:哪些群、群列表由谁维护、要不要 @ 才响应、机器人自己的消息是否处理、判断靠关键词还是模型、要不要留痕、判断不了时怎么办 |
|
|
19
19
|
| **有多种合理拆法且选择会改变架构** | 「审核后发送」——审核是人工卡点还是模型判定? | 直接把两种拆法摆出来让用户选 |
|
|
20
|
+
| **涉及用户真实资源但没给定位信息** | 「我们已经有张表,要自动处理」「就在现有群里答」 | 先问用户给 Base/Table 链接或 ID、群链接/群名/openConversationId,或确认由哪张表/哪个群维护;不要默认去 AI 表格或钉钉会话列表里替用户找 |
|
|
20
21
|
|
|
21
22
|
一两句话的需求天然属于第二种:能跑通不等于决策有依据,**这种情况应当提问**,而不是把下面 11 项默默替用户定了。
|
|
22
23
|
|
|
23
|
-
**只有流程清楚了,下面的技术决策点才有意义。** 不要在还不知道业务流程长什么样时就去问 `record_policy`。
|
|
24
24
|
|
|
25
25
|
## 选择要问的问题
|
|
26
26
|
|
|
27
|
-
本文件只定义**该问什么**。问题如何展示与收集答案由当前入口注入的协议决定;
|
|
28
|
-
本文件不规定具体传递方式。
|
|
29
27
|
|
|
30
28
|
选择问题时遵循:
|
|
31
29
|
|
|
@@ -35,15 +33,6 @@
|
|
|
35
33
|
- 选项穷不尽的(判断口径、话术、边界条件)应允许用户自由描述,不要硬凑成封闭选项。
|
|
36
34
|
- 具体每轮问几项、用什么字段格式、何时停下,严格遵循当前入口提供的发问协议。
|
|
37
35
|
|
|
38
|
-
## 没人能回答时
|
|
39
|
-
|
|
40
|
-
非交互执行(`--yes` / `--skip-questions` / 服务端自动流程),或用户明确说「你自己定」时,
|
|
41
|
-
按下面每项的「缺省假设」推进,但**必须**:
|
|
42
|
-
|
|
43
|
-
- 在产物 YAML / recipe 里以注释写明「假设:…(需确认)」
|
|
44
|
-
- 在最终汇报里单列「需确认项」清单
|
|
45
|
-
|
|
46
|
-
**绝不静默猜测。** 假设可以有,隐藏假设不行。
|
|
47
36
|
|
|
48
37
|
## 值得问的决策点
|
|
49
38
|
|
|
@@ -52,36 +41,40 @@
|
|
|
52
41
|
### 1. 触发方式
|
|
53
42
|
|
|
54
43
|
- 这个流程被什么触发?群消息 / 群内@机器人(支持单聊)/ 定时 / AI 表格记录新增或变更 / 表单提交 / 手动新建?
|
|
55
|
-
- **决定**:是否生成 `event_sources`,以及用哪种 `type`(见 `../../aitable-workflow/
|
|
44
|
+
- **决定**:是否生成 `event_sources`,以及用哪种 `type`(见 `../../aitable-workflow/references/event-sources.md`)。
|
|
56
45
|
- **缺省假设**:表单提交或手动新建 → **不生成** `event_sources`,靠 tracker 自身轮询。
|
|
57
46
|
- **不澄清的后果**:给表单场景生成了事件源 → 多一套永不触发的死配置;给群消息场景漏了事件源 → 流程根本不会启动。
|
|
58
47
|
|
|
59
48
|
触发或通知涉及钉钉机器人时,还要在写 `workflow.yml` 前确认:
|
|
60
49
|
|
|
50
|
+
先读 `patterns/dingtalk-bot-patterns.md`,按那里的通道模型落配置。
|
|
61
51
|
- 企业里是否已有可复用的机器人?有则复用,不默认新建。
|
|
62
52
|
- 需要新建且要求「@机器人响应」时,用户是否接受公开发布及可能的管理员审批等待?
|
|
63
53
|
不接受时改用群消息轮询 / 自定义机器人,并明确「无法判断是否被 @」等行为差异。
|
|
64
54
|
- 是否包含不归属当前企业的外部群?有则不能假设所有群共用一种通道。
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
- 要监听或加机器人的目标群是哪几个?**群属于用户的真实业务资源,先问用户提供群链接、群名或
|
|
56
|
+
openConversationId,或者确认群列表由 AI 表格配置表维护。不要默认枚举当前账号可见会话并自行匹配。**
|
|
57
|
+
- 使用 Stream 机器人时,默认在有效 @ 消息进入处理后贴处理中的表情反馈;用户明确不要时再移除。
|
|
67
58
|
|
|
68
59
|
### 2. 数据放新表还是已有的表
|
|
69
60
|
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- ID
|
|
61
|
+
- 用户是否已经表达了「我们有现成的 AI 表格 / 就写进那张表 / 从现有表触发」?
|
|
62
|
+
- **决定**:第二步产物是否只声明新表骨架,还是必须按已有 Base/Table 的字段现实来设计。
|
|
63
|
+
- **默认路径**:用户没有提到已有资源时,`tracker.base_id` 留空、根级 `base_name` 给一个业务化名字
|
|
64
|
+
(缺省会回落成项目目录名)。留空是正确的默认值,不是待办;setup 会再问用户新建还是复用。
|
|
65
|
+
- **用户提到已有表时必须设计期澄清**:先问用户提供 Base/Table 链接或 ID,并确认是「在已有 Base
|
|
66
|
+
里新建一张流程表」还是「直接写进某张已有 Table」。不要默认去 AI 表格列表里替用户找。
|
|
67
|
+
- 用户明确要写进某张已有 Table 时,`field_definitions` 必须**照那张表现有的列写** —— 凭空发明字段名不会报错,
|
|
68
|
+
只会往用户的真实业务表里静默加列,而字段建出来之后改名等于数据迁移。此时本项连带绑死第 8 项。
|
|
69
|
+
- ID 一律由用户给,**不要猜、不要默认搜索** —— 猜出来的 ID 轻则指向不存在的资源,重则指向别人的表。
|
|
70
|
+
只有用户明确说「你帮我找一下」时,才允许用当前账号可见列表辅助定位;找到后也必须把名称/链接/ID
|
|
71
|
+
回给用户确认,再写入配置。
|
|
79
72
|
|
|
80
73
|
### 3. AI 表格数据源的筛选条件 ⚠️ 最高优先级
|
|
81
74
|
|
|
82
75
|
- 只在触发方式含「AI 表格记录新增或变更」时适用。
|
|
83
76
|
- **只处理满足什么条件的记录?**(例:状态 = 待处理;知识沉淀 = 待沉淀)
|
|
84
|
-
- **决定**:`aitable_record` 的 `filter` / `filters`(见 `../../aitable-workflow/
|
|
77
|
+
- **决定**:`aitable_record` 的 `filter` / `filters`(见 `../../aitable-workflow/references/event-sources.md`)。
|
|
85
78
|
- **缺省假设**:用状态字段 = `tracker.states.initial`。
|
|
86
79
|
- **不澄清的后果**:**这是已发生过的线上故障。** 无筛选条件时每轮全表分页扫描,几百行时正常,涨到几万行后 dws 查询直接超时,链路静默停摆且无报错。
|
|
87
80
|
- **这一项永远不允许留空**——哪怕是猜的也必须写一个条件并标注假设。
|
|
@@ -96,7 +89,7 @@
|
|
|
96
89
|
### 5. 留痕策略
|
|
97
90
|
|
|
98
91
|
- 是否**每个事件**都要在表里留一行?还是只在需要人工介入 / 有结论时才建记录?
|
|
99
|
-
- **决定**:`record_policy: eager` vs `on_demand` + 透传 step 返回 `tracker: false`(见 `../../aitable-workflow/
|
|
92
|
+
- **决定**:`record_policy: eager` vs `on_demand` + 透传 step 返回 `tracker: false`(见 `../../aitable-workflow/references/workflow-yml.md`)。
|
|
100
93
|
- **缺省假设**:`eager`(全部留痕)。
|
|
101
94
|
- **不澄清的后果**:高频事件源(活跃群、频繁变更的表)用 `eager` 会让表在几天内堆到几万行噪音,运营无法使用,且反过来让筛选性能问题雪上加霜。
|
|
102
95
|
|
|
@@ -104,7 +97,7 @@
|
|
|
104
97
|
|
|
105
98
|
- 只在存在 `requiresAgent: true` 的步骤时适用。
|
|
106
99
|
- Agent 干活时需要:业务规格文档 / 知识库文章 / 历史处理记录 / 都不需要?
|
|
107
|
-
- **决定**:`workspaces/<role.id>/AGENTS.md`、角色的 `knowledge_scope.kb_collections` + `wiki/` 骨架、memory toolkit(见 `../../aitable-workflow/
|
|
100
|
+
- **决定**:`workspaces/<role.id>/AGENTS.md`、角色的 `knowledge_scope.kb_collections` + `wiki/` 骨架、memory toolkit(见 `../../aitable-workflow/references/workspace-context.md`)。
|
|
108
101
|
- **缺省假设**:产出 `AGENTS.md`(**无论如何都要产**,否则 Agent 没有输出契约),不配知识库和 memory。
|
|
109
102
|
- **不澄清的后果**:没有 `AGENTS.md` 时 Agent 返回格式不稳定,下游解析随机失败;漏配 `kb_collections` 时知识库完全不注入且**不报错**,表现为「Agent 好像不知道这些知识」。
|
|
110
103
|
|
|
@@ -118,7 +111,7 @@
|
|
|
118
111
|
### 8. 要记录哪些信息
|
|
119
112
|
|
|
120
113
|
- 这个流程要记录 / 沉淀哪些关键信息?
|
|
121
|
-
- **决定**:`field_definitions`(见 `../../aitable-workflow/
|
|
114
|
+
- **决定**:`field_definitions`(见 `../../aitable-workflow/references/workflow-yml.md`)。
|
|
122
115
|
- **缺省假设**:从 scene 里能识别的名词 + 系统骨架字段。
|
|
123
116
|
- **不澄清的后果**:字段缺失导致 `input_mapping` / `output_mapping` 引用不存在的字段;字段冗余导致表格臃肿。**字段一旦被 setup 建出来,改名等于数据迁移。**
|
|
124
117
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
| 排查难度 | 低(配置就在代码里) | 中(要看表里的值 + globalVar 当前状态) |
|
|
13
13
|
| 适合 | 配置基本不变、只有开发会改 | 名单/规则/开关这类运营高频调整的配置 |
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**默认选静态。**。
|
|
16
16
|
|
|
17
17
|
## 架构
|
|
18
18
|
|
|
@@ -32,7 +32,7 @@ globalVar(进程内 KV 总线)
|
|
|
32
32
|
|
|
33
33
|
**tracker 和「配置表」是两个不同的概念,不要混用。** tracker 是 config-sync workflow 自己的**留痕表**——每行是一次同步执行的结果记录(同步了多少条、有没有报错),随这个 workflow 的状态机流转;配置表是运营维护的数据源表,本质是外部数据,跟状态机无关,两者不是同一张表。
|
|
34
34
|
|
|
35
|
-
配置表的读取**不走** `ctx.toolkit.table.queryRows()`(那个 API 只能批量读「当前 workflow 自己的 tracker」,见 `../../aitable-workflow/
|
|
35
|
+
配置表的读取**不走** `ctx.toolkit.table.queryRows()`(那个 API 只能批量读「当前 workflow 自己的 tracker」,见 `../../aitable-workflow/references/generated/core-api.md` 的 `TableToolkit`)。config-sync 通常读取任意一张(或多张)配置表,走 **dws CLI**(`aitable-workflow-core` 导出的 `DwsClient`)整表查询,与该表是不是某个 workflow 的 tracker 无关:
|
|
36
36
|
|
|
37
37
|
```typescript
|
|
38
38
|
import { DwsClient } from "aitable-workflow-core";
|
|
@@ -62,7 +62,7 @@ meta:
|
|
|
62
62
|
tracker:
|
|
63
63
|
base_id: "bXXXXXXXX" # 留痕表:记录每轮同步执行的结果,不是配置表本身
|
|
64
64
|
table_id: ""
|
|
65
|
-
states: # 必填(active ≥2 且显式 review),见 ../../aitable-workflow/
|
|
65
|
+
states: # 必填(active ≥2 且显式 review),见 ../../aitable-workflow/references/multi-workflow.md
|
|
66
66
|
initial: "待处理"
|
|
67
67
|
active: [ "处理中", "审核中" ]
|
|
68
68
|
review: "审核中"
|
|
@@ -162,7 +162,7 @@ export default recipe;
|
|
|
162
162
|
|
|
163
163
|
## 4. 消费方
|
|
164
164
|
|
|
165
|
-
在业务 workflow 的配置里用**整值** `${globalVar.*}`
|
|
165
|
+
在业务 workflow 的配置里用**整值** `${globalVar.*}` 引用:
|
|
166
166
|
|
|
167
167
|
```yaml
|
|
168
168
|
# workflows/<consumer>/workflow.yml
|
|
@@ -225,7 +225,6 @@ config:
|
|
|
225
225
|
- [ ] config-sync 的 `record_policy: on_demand` + recipe 返回 `tracker: false`(同步本身不需要留痕)
|
|
226
226
|
- [ ] config-sync 的 `tracker` 是同步执行的留痕表,**不是**配置表本身
|
|
227
227
|
- [ ] 配置表读取走 dws CLI(`DwsClient`),不是 `ctx.toolkit.table.queryRows()`
|
|
228
|
-
- [ ] 消费方用的是**整值** `${globalVar.x}`,不是嵌入式
|
|
229
228
|
- [ ] 消费方能容忍 globalVar 为空(重启后的窗口期)
|
|
230
229
|
- [ ] 读到空配置有防抖(连续 N 轮才清空)
|
|
231
230
|
- [ ] globalVar 的 key 全局唯一且有语义
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# 钉钉机器人设计模式
|
|
2
|
+
|
|
3
|
+
本文件只讲设计期决策与产物落法;创建机器人、发布版本、加群、审批等操作步骤归 setup 阶段的 `aitable-workflow-setup/references/bot-provisioning.md`。
|
|
4
|
+
|
|
5
|
+
## 先判定通道
|
|
6
|
+
|
|
7
|
+
1. 用户希望「@ 机器人后才答」或需要单聊提问:优先用企业内机器人 Stream。
|
|
8
|
+
2. 用户希望监听群里所有消息,或企业内机器人审批来不及:用 `dingtalk_group_message` / `dingtalk_user_message` 轮询入站。
|
|
9
|
+
3. 群很多时优先 `dingtalk_user_message`;群少且需要简单明确时可用 `dingtalk_group_message`。
|
|
10
|
+
4. 自定义群机器人 Webhook 只适合作出站降级,不作为入站监听来源。
|
|
11
|
+
|
|
12
|
+
## 设计期必须问清
|
|
13
|
+
|
|
14
|
+
- 是否已经有可复用的企业内机器人;没有时是否接受管理员审批与发布等待。
|
|
15
|
+
- 要监听几个机器人;每个机器人服务哪些群或单聊入口。
|
|
16
|
+
- 是否必须 @ 提问者;`sessionWebhook` 能 @,robot API 主动推送不能真 @,只能正文前置称呼。
|
|
17
|
+
- 是否存在外部群或跨组织群;群归属会影响企业内机器人是否能被加入和触发。
|
|
18
|
+
- 是否允许用户在 AI 表格里自助维护机器人名单和凭据;允许时用独立 config-sync workflow 发布配置。
|
|
19
|
+
|
|
20
|
+
## 推荐配置
|
|
21
|
+
|
|
22
|
+
动态多机器人只使用一个字段:`robot_creds`。它不是名单,而是凭据映射;配置多少个 credRef,就监听多少个机器人。
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
toolkit_config:
|
|
26
|
+
messaging:
|
|
27
|
+
robot_creds: "${globalVar.robot_creds}"
|
|
28
|
+
|
|
29
|
+
event_sources:
|
|
30
|
+
企业机器人监听:
|
|
31
|
+
type: dingtalk_stream
|
|
32
|
+
robot_creds: "${globalVar.robot_creds}"
|
|
33
|
+
conversation_types: ["group"]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`robot_creds` 的值形如:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"support-bot": {
|
|
41
|
+
"client_id": "dingxxxx",
|
|
42
|
+
"client_secret": "******"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`support-bot` 是 credRef。Stream 入站事件会携带实际接收消息的 credRef;回复时同一个 credRef 会用于选择正确机器人身份,避免多机器人串号。
|
|
48
|
+
|
|
49
|
+
## 配置来源选择
|
|
50
|
+
|
|
51
|
+
- 单一机器人、凭据不需要运营改:可用 `.env`,例如 `robot_creds: "${env.DINGTALK_ROBOTS}"`。
|
|
52
|
+
- 多机器人、运营要自己加减:用 AI 表格配置表 + config-sync,把启用的机器人凭据发布成 `globalVar.robot_creds`。
|
|
53
|
+
- 首轮 config-sync 前 `globalVar.robot_creds` 为空是正常状态;产物注释里要说明这会导致 Stream 暂不建连或机器人身份出站暂不可用。
|
|
54
|
+
|
|
55
|
+
## Recipe 发消息
|
|
56
|
+
|
|
57
|
+
业务 recipe 优先只用:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
ctx.toolkit.messaging.replyToConversation(conversationId, content, options)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
不要在业务 recipe 里直接拼 webhook、直接读取 appKey/appSecret,或手写钉钉发送逻辑。需要卡片时用 `replyToConversationWithButtons` 或已存在的 messaging 方法。
|
|
64
|
+
|
|
65
|
+
## conversationId / senderId 怎么传到 replyToConversation
|
|
66
|
+
|
|
67
|
+
`replyToConversation(conversationId, answer, options)` 的两个定位参数**不从 handler 直接透传**,而是走 tracker 表字段 + `input_mapping`:
|
|
68
|
+
|
|
69
|
+
1. **Handler** 把 `conversationId` / `senderStaffId` / `senderName` 写进 tracker 表字段(`output_mapping` 写到 `create_record` 的 `fields` 里)。
|
|
70
|
+
2. **`workflow.yml`** 在 `tracker.field_definitions` 声明这些字段,并在下游回复 step 的 `input_mapping` 里把它们映射成 recipe 入参名(如 `conversationId` / `senderId` / `senderName`)。
|
|
71
|
+
3. **回复 step** 从 `ctx.input.conversationId` 读,传给 `replyToConversation`;`mentionSender` 用 `ctx.input.senderId` 或 `false`。
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
# workflow.yml
|
|
75
|
+
tracker:
|
|
76
|
+
field_definitions:
|
|
77
|
+
- name: 群ID
|
|
78
|
+
- name: 提问人ID
|
|
79
|
+
- name: 提问人
|
|
80
|
+
|
|
81
|
+
steps:
|
|
82
|
+
- id: reply
|
|
83
|
+
recipe: reply-to-group
|
|
84
|
+
input_mapping:
|
|
85
|
+
conversationId: "群ID"
|
|
86
|
+
senderId: "提问人ID"
|
|
87
|
+
senderName: "提问人"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// 回复 step
|
|
92
|
+
const conversationId = String(ctx.input.conversationId ?? "");
|
|
93
|
+
const senderId = String(ctx.input.senderId ?? "");
|
|
94
|
+
const res = await ctx.toolkit.messaging.replyToConversation(conversationId, answer, {
|
|
95
|
+
mentionSender: senderId || false,
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
handler 写、下游读,**不要**试图绕开 tracker 表把 ID 放在内存里传 —— WorkItem 是跨进程的瞬态载体,只有 tracker 表字段稳。
|
|
100
|
+
|
|
101
|
+
## Typing 表情反馈("正在输入…")
|
|
102
|
+
|
|
103
|
+
Stream 机器人默认加处理中的表情反馈:handler 收到有效 @ 消息并决定进入处理流程时,调用 `toolkit.messaging.beginDingTalkProcessingFeedback(ctx)` 给原消息加一个随机表情反馈。用户明确不要即时反馈时才去掉。
|
|
104
|
+
|
|
105
|
+
参数需要 `openMsgId` / `conversationId` / `robotCode` / `credRef` 四项,按 `type: "dingtalk_stream_message"` 组成一个 EventContext 形状的对象(这些字段来自 `event.payload`,如 `payload.dingtalk.openMsgId`):
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { resolveDingTalkStreamMessageEventContext } from "aitable-workflow-core";
|
|
109
|
+
|
|
110
|
+
const dingtalk = (event.payload.dingtalk ?? {}) as Record<string, unknown>;
|
|
111
|
+
const streamCtx = resolveDingTalkStreamMessageEventContext({
|
|
112
|
+
type: "dingtalk_stream_message",
|
|
113
|
+
openMsgId: String(dingtalk.openMsgId ?? ""),
|
|
114
|
+
conversationId,
|
|
115
|
+
robotCode: String(dingtalk.robotCode ?? ""),
|
|
116
|
+
credRef: String(dingtalk.credRef ?? ""),
|
|
117
|
+
});
|
|
118
|
+
if (streamCtx) {
|
|
119
|
+
ctx.toolkit.messaging?.beginDingTalkProcessingFeedback?.(streamCtx);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
只在 stream 通道有意义;轮询入站(`dingtalk_group_message` / `dingtalk_user_message`)和 webhook 通道不调。空消息、机器人自身消息、被规则过滤的消息不要贴反馈。
|
|
124
|
+
|
|
125
|
+
## 避免重复文档
|
|
126
|
+
|
|
127
|
+
- 本文件讲「怎么设计」。
|
|
128
|
+
- `aitable-workflow/references/event-sources.md` 讲事件源字段与框架能力。
|
|
129
|
+
- `aitable-workflow-setup/references/bot-provisioning.md` 讲建号、发布、加群、审批。
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# 读取外部 AI 表格最佳实践
|
|
2
|
+
|
|
3
|
+
当 recipe 需要读取用户已有表、FAQ 表、配置表或业务参考表时,用这个模式。它适用于“表不是当前 workflow tracker,但运行时要拿它的数据做判断”的场景。
|
|
4
|
+
|
|
5
|
+
## 什么时候用
|
|
6
|
+
|
|
7
|
+
- FAQ 自动答疑:读取已有 FAQ 表,按问题/相似问/答案做匹配。
|
|
8
|
+
- 业务参考表:读取产品、客户、负责人、规则等静态或半静态数据。
|
|
9
|
+
- 配置表同步:读取运营维护的配置表,再发布到 `globalVar`。
|
|
10
|
+
|
|
11
|
+
如果只读当前 workflow tracker 表,用 `ctx.toolkit.table.queryRows()`。如果按 recordId 读取指定表的一行,用 `ctx.toolkit.table.readRecordIn()`。如果要分页拉取指定外部表的多行,走 `DwsClient`。
|
|
12
|
+
|
|
13
|
+
## 配置声明
|
|
14
|
+
|
|
15
|
+
外部表引用用 `tableRefConfigSchema`,不要手写 `{ baseId, tableId }`。
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { tableRefConfigSchema } from "aitable-workflow-core";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
config: z.object({
|
|
22
|
+
faq_table: tableRefConfigSchema({
|
|
23
|
+
label: "FAQ 表",
|
|
24
|
+
description: "用户已有的 FAQ 数据表",
|
|
25
|
+
ownership: "external",
|
|
26
|
+
access: "read",
|
|
27
|
+
}).optional(),
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
运行期必须检查未配置态,给出可读错误或降级结果。
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const table = ctx.config.faq_table as { baseId?: string; tableId?: string } | undefined;
|
|
35
|
+
if (!table?.baseId || !table.tableId) {
|
|
36
|
+
return {
|
|
37
|
+
status: "success",
|
|
38
|
+
output: { action: "manual", reply: "FAQ 表还未配置,稍等人工客服来处理" },
|
|
39
|
+
logs: ["FAQ 表未配置,已降级转人工"],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 标准读取流程
|
|
45
|
+
|
|
46
|
+
1. 用 `DwsClient` 跑 `aitable field get`,建立字段名到字段 ID 的映射。
|
|
47
|
+
2. 用 `aitable record query --all` 拉记录。
|
|
48
|
+
3. 从 `record.cells` 取值;cells 通常按字段 ID 键控,不要假设字段名直接可用。
|
|
49
|
+
4. 把单元格值归一成业务对象,再交给匹配或生成逻辑。
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { DwsClient, dwsPayload, findArrayPayload } from "aitable-workflow-core";
|
|
53
|
+
|
|
54
|
+
const dws = new DwsClient({ bin: "dws", timeoutMs: 30_000 });
|
|
55
|
+
|
|
56
|
+
async function readExternalRows(target: { baseId: string; tableId: string }): Promise<Record<string, unknown>[]> {
|
|
57
|
+
const fieldsRaw = await dws.runJson([
|
|
58
|
+
"aitable", "field", "get",
|
|
59
|
+
"--base-id", target.baseId,
|
|
60
|
+
"--table-id", target.tableId,
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const fieldNameById = new Map<string, string>();
|
|
64
|
+
for (const field of findArrayPayload(fieldsRaw)) {
|
|
65
|
+
const item = dwsPayload(field);
|
|
66
|
+
if (!item || typeof item !== "object") continue;
|
|
67
|
+
const record = item as Record<string, unknown>;
|
|
68
|
+
const id = typeof record.fieldId === "string" ? record.fieldId : "";
|
|
69
|
+
const name = typeof record.fieldName === "string" ? record.fieldName : "";
|
|
70
|
+
if (id && name) fieldNameById.set(id, name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const recordsRaw = await dws.runJson([
|
|
74
|
+
"aitable", "record", "query",
|
|
75
|
+
"--base-id", target.baseId,
|
|
76
|
+
"--table-id", target.tableId,
|
|
77
|
+
"--all",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
return findArrayPayload(recordsRaw).map((row) => {
|
|
81
|
+
const item = dwsPayload(row);
|
|
82
|
+
const cells = item && typeof item === "object"
|
|
83
|
+
? ((item as Record<string, unknown>).cells ?? (item as Record<string, unknown>).fields ?? {})
|
|
84
|
+
: {};
|
|
85
|
+
if (!cells || typeof cells !== "object") return {};
|
|
86
|
+
return Object.fromEntries(
|
|
87
|
+
Object.entries(cells as Record<string, unknown>).map(([fieldId, value]) => [
|
|
88
|
+
fieldNameById.get(fieldId) ?? fieldId,
|
|
89
|
+
normalizeCellValue(value),
|
|
90
|
+
]),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 单元格归一
|
|
97
|
+
|
|
98
|
+
真实表里常见 cellValue 不止字符串。recipe 里先归一,再写业务判断。
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
function normalizeCellValue(value: unknown): string {
|
|
102
|
+
if (value == null) return "";
|
|
103
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
104
|
+
return String(value);
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
return value.map(normalizeCellValue).filter(Boolean).join(" ");
|
|
108
|
+
}
|
|
109
|
+
if (typeof value === "object") {
|
|
110
|
+
const record = value as Record<string, unknown>;
|
|
111
|
+
if (typeof record.markdown === "string") return record.markdown;
|
|
112
|
+
if (typeof record.text === "string") return record.text;
|
|
113
|
+
if (Array.isArray(record.text)) return record.text.map(normalizeCellValue).filter(Boolean).join(" ");
|
|
114
|
+
if (typeof record.value === "string") return record.value;
|
|
115
|
+
}
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## FAQ 表建议
|
|
121
|
+
|
|
122
|
+
- 先探测字段清单,再把实际采用的字段名写进 recipe config 或代码常量。
|
|
123
|
+
- FAQ 行数不大时可以全表读取后本地匹配;数据持续增长时,应改成离线索引或配置同步 workflow,不要每条消息都全量拉表。
|
|
124
|
+
- 答案里含图片或附件时,保留 markdown/richText 内容;回复通道不支持的富内容要降级为文本说明。
|
|
125
|
+
|
|
126
|
+
## 自查
|
|
127
|
+
|
|
128
|
+
- [ ] 表引用用 `tableRefConfigSchema({ access: "read" })`。
|
|
129
|
+
- [ ] 未配置 `baseId/tableId` 时有降级,不抛给用户看不懂的异常。
|
|
130
|
+
- [ ] 先 `field get` 建字段映射,再读 `record.cells`。
|
|
131
|
+
- [ ] 解析函数覆盖 string、数组、richText/markdown、空值。
|
|
132
|
+
- [ ] 大表读取有缓存、同步表或索引方案,不在高频消息里无界全表扫描。
|