@lark-base-open/base-database-cli 1.0.0 → 1.1.1
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/CHANGELOG.md +24 -0
- package/README.md +89 -29
- package/config-schema.json +6 -0
- package/dist/agent-guide.d.ts +1 -0
- package/dist/agent-guide.d.ts.map +1 -1
- package/dist/agent-guide.js +3 -2
- package/dist/agent-guide.js.map +1 -1
- package/dist/auth.d.ts +7 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +57 -17
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -1
- package/dist/config.js.map +1 -1
- package/dist/database-skill.d.ts +4 -2
- package/dist/database-skill.d.ts.map +1 -1
- package/dist/database-skill.js +23 -10
- package/dist/database-skill.js.map +1 -1
- package/dist/deploy.d.ts +3 -1
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +16 -10
- package/dist/deploy.js.map +1 -1
- package/dist/distribution-commands.d.ts +3 -0
- package/dist/distribution-commands.d.ts.map +1 -0
- package/dist/distribution-commands.js +64 -0
- package/dist/distribution-commands.js.map +1 -0
- package/dist/distribution-npm.d.ts +23 -0
- package/dist/distribution-npm.d.ts.map +1 -0
- package/dist/distribution-npm.js +133 -0
- package/dist/distribution-npm.js.map +1 -0
- package/dist/environment.d.ts +1 -0
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +1 -1
- package/dist/environment.js.map +1 -1
- package/dist/init.d.ts +2 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +27 -4
- package/dist/init.js.map +1 -1
- package/dist/install.d.ts +31 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +150 -0
- package/dist/install.js.map +1 -0
- package/dist/lark-cli.d.ts +7 -1
- package/dist/lark-cli.d.ts.map +1 -1
- package/dist/lark-cli.js +289 -21
- package/dist/lark-cli.js.map +1 -1
- package/dist/preview-receipt.d.ts +5 -4
- package/dist/preview-receipt.d.ts.map +1 -1
- package/dist/preview-receipt.js +12 -3
- package/dist/preview-receipt.js.map +1 -1
- package/dist/profile-name.d.ts +2 -0
- package/dist/profile-name.d.ts.map +1 -0
- package/dist/profile-name.js +10 -0
- package/dist/profile-name.js.map +1 -0
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +19 -11
- package/dist/program.js.map +1 -1
- package/dist/pull.d.ts +2 -0
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +4 -2
- package/dist/pull.js.map +1 -1
- package/dist/skill.d.ts +9 -2
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +128 -11
- package/dist/skill.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/updates.d.ts +20 -0
- package/dist/updates.d.ts.map +1 -0
- package/dist/updates.js +98 -0
- package/dist/updates.js.map +1 -0
- package/package.json +6 -4
- package/skills/lark-base-database/SKILL.md +73 -273
- package/skills/lark-base-database/references/base-capability-routing.md +12 -20
- package/skills/lark-base-database/references/base-sql-unsupported.md +2 -2
- package/skills/lark-base-database/references/database-capability-skill.md +27 -11
- package/skills/lark-base-database/references/development.md +26 -553
- package/skills/lark-base-database/references/faas.md +563 -0
- package/skills/lark-base-database/references/file-transfer.md +62 -199
- package/skills/lark-base-database/references/frontend-design-guide.md +97 -0
- package/skills/lark-base-database/references/notifications.md +48 -0
- package/skills/lark-base-database/references/permission-design.md +32 -38
- package/skills/lark-base-database/references/release.md +53 -45
- package/skills/lark-base-database/references/sql-function.md +17 -4
- package/skills/lark-base-database/references/style-guide.md +6 -8
- package/skills/lark-base-database/references/verification.md +57 -0
- package/skills/lark-base-database/references/workspace.md +170 -0
- package/templates/database-capability/SKILL.md.tmpl +28 -11
- package/templates/database-starter/README.md +1 -1
- package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +43 -40
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
# FaaS 与记录协议
|
|
2
|
+
|
|
3
|
+
实现服务端业务写入或诊断下游调用时读取。Page 调用方式见[Page 开发](development.md),查询类型与参数见[SQL Function](sql-function.md),权限判定见[权限设计](permission-design.md)。
|
|
4
|
+
|
|
5
|
+
## 阅读路径
|
|
6
|
+
|
|
7
|
+
- [FaaS 开发](#faas-开发):目录、handler 与 context
|
|
8
|
+
- [远程调用与写入链路设计](#远程调用与写入链路设计):依赖、一致性与重试
|
|
9
|
+
- [下游错误归一化](#下游错误归一化):业务结果与诊断
|
|
10
|
+
- [应用记录 写入协议](#应用记录-写入协议):请求、响应与部分成功
|
|
11
|
+
- [创建实现示例](#创建实现示例):固定操作示例
|
|
12
|
+
- [Base 读写协议](#base-读写协议):字段格式与集合更新
|
|
13
|
+
|
|
14
|
+
## FaaS 开发
|
|
15
|
+
|
|
16
|
+
每个由 Page 主动发起的写入 operation 的目录:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
server-functions/functions/<functionId>/
|
|
20
|
+
├── index.ts
|
|
21
|
+
└── meta.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"name": "创建任务",
|
|
27
|
+
"description": "校验输入并创建一条任务记录",
|
|
28
|
+
"inputSchema": {
|
|
29
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"title": { "type": "string", "minLength": 1 },
|
|
33
|
+
"status": { "type": "string", "enum": ["待处理", "进行中", "已完成"] }
|
|
34
|
+
},
|
|
35
|
+
"required": ["title", "status"],
|
|
36
|
+
"additionalProperties": false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`inputSchema` 是 Agent 调用该 Function 时 `params` 业务值的结构,不包含外层 `{"params": ...}`;FaaS Function 必须显式声明。CLI 会校验并上传该 Schema,发布后的能力发现接口据此返回可调用参数。只使用 JSON Schema 2020-12 的受限子集:`type`、`description`、`properties`、`required`、`additionalProperties`、`items`、`prefixItems`、`minItems`、`maxItems`、`enum`、`minimum`、`maximum`、`minLength`、`maxLength`;不使用 `$ref`、组合关键字、`pattern`、`format` 或未知关键字。Schema 最大 32 KiB、嵌套不超过 16 层、节点不超过 256 个。
|
|
42
|
+
|
|
43
|
+
SQL Function 的 `inputSchema` 不手写,由 CLI 根据 `meta.params` 的顺序和 `kind` 确定性生成固定长度数组 Schema;`TIMESTAMP` 与 `DURATION` 均明确为毫秒整数。
|
|
44
|
+
|
|
45
|
+
上传时 CLI 把 Schema 对象放入 `sourceCode` JSON 字符串:SQL 为 `{sql,meta,inputSchema}`,FaaS 为 `{inputSchema}`,不发送独立的 Function `inputSchema` 字段。FaaS 的可执行代码仍在源码包,`meta.json` 的写法不变;发现接口仍独立返回 `inputSchema` JSON 字符串。
|
|
46
|
+
|
|
47
|
+
loader 读取 HTTP `req.body.params`,把这个业务对象作为 handler 第一个参数 `rawParams`;第二个参数是 runtime context:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
interface FunctionContext {
|
|
51
|
+
fetch: typeof fetch
|
|
52
|
+
invokeFunction(request: {
|
|
53
|
+
projectId: string
|
|
54
|
+
functionId: string
|
|
55
|
+
params: unknown[]
|
|
56
|
+
}): Promise<unknown>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface FaasHandlerResult<T> {
|
|
60
|
+
data: T | null
|
|
61
|
+
code: number
|
|
62
|
+
msg: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface CreateTaskInput {
|
|
66
|
+
title: string
|
|
67
|
+
status: '待处理' | '进行中' | '已完成'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseCreateTask(value: unknown): CreateTaskInput {
|
|
71
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
72
|
+
throw new Error('任务参数必须是对象')
|
|
73
|
+
}
|
|
74
|
+
const input = value as Record<string, unknown>
|
|
75
|
+
if (
|
|
76
|
+
Object.keys(input).some((key) => key !== 'title' && key !== 'status') ||
|
|
77
|
+
typeof input.title !== 'string' || input.title.length === 0 ||
|
|
78
|
+
typeof input.status !== 'string' ||
|
|
79
|
+
!['待处理', '进行中', '已完成'].includes(input.status)
|
|
80
|
+
) {
|
|
81
|
+
throw new Error('任务名称或状态无效')
|
|
82
|
+
}
|
|
83
|
+
return { title: input.title, status: input.status as CreateTaskInput['status'] }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
默认导出 `handler(rawParams, context)`,完整创建示例见下文。上述 meta.json、入参校验和 Page 的 createTask 示例共同使用 title/status;实际业务须同时按 Catalog 更新 Schema、调用参数和 handler,不只改其中一处。
|
|
88
|
+
|
|
89
|
+
`rawParams` 已经是业务对象。输入校验失败返回稳定业务码、`data:null` 和可读 `msg`。`FaasHandlerResult` 只描述 handler 的返回约定,不得作为页面 `invokeFunction` 的泛型参数。
|
|
90
|
+
|
|
91
|
+
### 远程调用与写入链路设计
|
|
92
|
+
|
|
93
|
+
实现前先按设计结果确认一致性、部分成功、重试语义和远程调用依赖。以一次 Page/FaaS operation 为边界列出 SQL Function、`context.fetch` 和写入 OpenAPI;标记哪些输入在 operation 开始时已经确定、哪些必须等待前一步响应。默认使用满足业务语义的最少远程等待阶段:
|
|
94
|
+
|
|
95
|
+
- 同表多条创建、更新或删除合并到一个 batch 请求,单批不超过 200 条。
|
|
96
|
+
- 同一 operation 调用多个 SQL Function 或 FaaS Function 时,输入已确定、互不依赖,且并发不改变一致性或部分成功语义的调用尽量并发;只有真实响应依赖才串行。
|
|
97
|
+
- 多个无副作用读取或请求的参数已经确定且互不依赖时,先启动全部 Promise,再在共同消费结果处等待;存在真实响应依赖时才串行。
|
|
98
|
+
- 原始 Promise 仍可能 reject 且需要知道每一路结果时使用 `Promise.allSettled`;helper 已把异常归一化为结构化结果时可以使用 `Promise.all`。
|
|
99
|
+
- 写前 SQL 只用于服务端必须确认的权限、关键状态或其他可信数据;写后 SQL 回读用于验收或明确要求的业务校验,不作为每次生产写入的默认步骤。
|
|
100
|
+
- 页面状态仅可用于允许弱一致性的非安全敏感计算。用户身份、权限、租户隔离、支付、库存、额度等约束不得信任页面传值。
|
|
101
|
+
- 多路写入返回值区分全部成功、部分成功和全部失败,并携带已取得的 record ID 与安全错误信息。非幂等创建结果未知时不自动整体重试。
|
|
102
|
+
- 需要事务、唯一约束、条件更新或原子 compare-and-set 才能保证正确性的 operation,在当前 Base 能力下标记为不支持,不使用多次远程请求模拟。
|
|
103
|
+
|
|
104
|
+
无依赖的多路写入可以采用:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const writes = await Promise.allSettled([
|
|
108
|
+
updatePrimaryRecord(context, input),
|
|
109
|
+
createAuditRecord(context, input),
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
return summarizeWrites(writes)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
性能敏感的 operation 记录总耗时和各远程调用耗时;只使用成功业务请求评价写入性能,鉴权失败、参数失败和平台凭证缺失只用于诊断。日志和返回值不得包含 token、cookie 或请求凭证。
|
|
116
|
+
|
|
117
|
+
### 下游错误归一化
|
|
118
|
+
|
|
119
|
+
FaaS 调用 Base OpenAPI 或其他 应用 Function 时,把当前 operation 的业务结果与下游诊断分开:
|
|
120
|
+
|
|
121
|
+
- 顶层 `code` 和 `msg` 表达稳定、面向调用方的业务语义,不直接使用上游错误码,也不把技术细节拼进 `msg`。
|
|
122
|
+
- 成功或部分成功的业务结果保留在 `data`;仅在失败或部分成功时,把失败步骤放入 `data.errors`。
|
|
123
|
+
- 上游只返回错误摘要时原样保留经清理的摘要和 LogID,不猜测或拼造不存在的校验详情。
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
type DownstreamErrorKind =
|
|
127
|
+
| 'openapi'
|
|
128
|
+
| 'function'
|
|
129
|
+
| 'transport'
|
|
130
|
+
| 'invalid_response'
|
|
131
|
+
|
|
132
|
+
interface DownstreamError {
|
|
133
|
+
source: 'base_openapi' | 'site_function'
|
|
134
|
+
step: string
|
|
135
|
+
kind: DownstreamErrorKind
|
|
136
|
+
operation: string
|
|
137
|
+
httpStatus?: number
|
|
138
|
+
code?: number
|
|
139
|
+
msg: string
|
|
140
|
+
logId?: string
|
|
141
|
+
retryable: boolean
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type DownstreamOutcome<T> =
|
|
145
|
+
| { ok: true; value: T }
|
|
146
|
+
| { ok: false; error: DownstreamError }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
下游 helper 捕获预期失败并返回 `DownstreamOutcome<T>`,业务 handler 再汇总各步骤;代码自身的非预期异常仍由 handler 最外层捕获并返回通用错误,不向页面泄漏原始异常或堆栈。分类规则固定为:
|
|
150
|
+
|
|
151
|
+
- Base OpenAPI 返回合法 JSON 且含非零数值 `code`:`openapi`;
|
|
152
|
+
- 应用 Function 返回合法 envelope 且含非零数值 `code`:`function`;
|
|
153
|
+
- `fetch` reject、超时、连接中断等未取得合法响应:`transport`;
|
|
154
|
+
- 非 JSON、缺少约定字段,或成功响应缺少预期结果:`invalid_response`。
|
|
155
|
+
|
|
156
|
+
错误对象只允许返回诊断所需的白名单字段。`msg` 取上游安全摘要,去除首尾空白并限制在 300 字符内;`logId` 优先取响应 `x-tt-logid`,可回退 `x-request-id`,限制在 128 字符内。不得返回 URL、请求 body、完整响应、完整 headers、stack、token、cookie 或其他凭证。对于 `context.invokeFunction`,只从其合法返回 envelope 中提取同等字段。
|
|
157
|
+
|
|
158
|
+
`retryable` 同时取决于错误和写入语义,不能只看 HTTP 状态。明确幂等的固定字段更新或删除遇到 transport、408、429 或 5xx 时可以标记为可重试;非幂等创建出现 transport 或结果未知时必须为 `false`,除非业务已有稳定幂等键和去重机制。页面不得仅因为 `retryable: true` 自动重提,仍按 operation 契约决定交互。
|
|
159
|
+
|
|
160
|
+
多路写入的顶层业务码至少区分输入无效、部分成功、下游全部失败和内部异常,并在同一 operation 内保持稳定。全部成功时省略 `errors`;部分成功时返回已完成结果、失败步骤和明确的后续动作,避免用户重复提交已经成功的非幂等写入。
|
|
161
|
+
|
|
162
|
+
应用 invoke 网关在 handler 返回前产生的 504、运行时崩溃或加载失败无法被上述结构捕获。排查这类错误时保留 invoke HTTP 响应自身的 `x-tt-logid`,不要伪装成某次 OpenAPI 调用错误。
|
|
163
|
+
|
|
164
|
+
### 写入行(记录)
|
|
165
|
+
|
|
166
|
+
FaaS 使用 `context.fetch` 调用固定的批量写入路由;只使用线上 origin:
|
|
167
|
+
|
|
168
|
+
| 目标 | Origin |
|
|
169
|
+
|---|---|
|
|
170
|
+
| Online | `https://open.feishu.cn` |
|
|
171
|
+
|
|
172
|
+
#### 应用记录 写入协议
|
|
173
|
+
|
|
174
|
+
FaaS 只使用以下五个固定路由,均为 `POST`,请求头为
|
|
175
|
+
`Content-Type: application/json; charset=utf-8`:
|
|
176
|
+
|
|
177
|
+
```http
|
|
178
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/records/batch_create
|
|
179
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/records/batch_update
|
|
180
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/records/batch_delete
|
|
181
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/append_attachments
|
|
182
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/remove_attachments
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
后两条是附件字段专用的增量路由(注意不在 `records/` 段下):请求只提交本次增删的
|
|
186
|
+
`file_token`,服务端读取、合并后整格提交,保留读到的其它附件及宽高;不保证原子性或去重。目标环境必须已部署对应 FaaS runtime 和服务端能力,不能仅凭 Skill 存在就判定可用。什么时候必须用它们、什么时候仍可整格写,
|
|
187
|
+
见[文件传输](file-transfer.md)的「写单元格」。
|
|
188
|
+
|
|
189
|
+
路径首个参数是 **Base token,不是 应用 appToken**:取 `catalog.json` 中该表的
|
|
190
|
+
`tables[].baseToken`,与 table ID 一起作为 Function 内的工程常量。应用 appToken 只用于
|
|
191
|
+
`context.invokeFunction` 的 `projectId`,两者不要混用。
|
|
192
|
+
|
|
193
|
+
请求体必须严格使用对应结构,不混用普通 Base OpenAPI、MCP 或其他 batch 接口的字段:
|
|
194
|
+
|
|
195
|
+
| operation | 请求体 | 成功时 `data.record_id_list` 的语义 |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `batch_create` | `{"create_records":[{字段名: CellValue}, ...]}` | 与 `create_records` 按位置一一对应的新记录 ID |
|
|
198
|
+
| `batch_update` | `{"update_records":{"rec_xxx":{字段名: CellValue}, ...}}` | 实际存在并处理的记录 ID;不存在的 ID 放入 `record_not_found` |
|
|
199
|
+
| `batch_delete` | `{"record_id_list":["rec_xxx", ...]}` | 原样回显请求 ID;不存在、因此未删除的 ID 另放入 `record_not_found` |
|
|
200
|
+
| `append_attachments` | `{"attachments":{"rec_xxx":{"字段名或字段ID":[{"file_token":"...","image_width":640,"image_height":480}]}}}` | 无 `record_id_list`,见下方附件增量响应 |
|
|
201
|
+
| `remove_attachments` | 同 `append_attachments`,数组项只需 `file_token` | 同上 |
|
|
202
|
+
|
|
203
|
+
五个请求体都是 strict JSON Schema:不接受表格中未列出的额外顶层字段。「每批 1~200 条
|
|
204
|
+
记录」只适用于前三条 batch 路由;附件增量路由的限制是**一次操作的 `file_token` 总数不超过
|
|
205
|
+
50**;附件映射及每个 record-field 数组没有非空 Schema 约束。record ID 必须匹配
|
|
206
|
+
`^rec[A-Za-z0-9]{1,11}$`;删除列表不允许重复,
|
|
207
|
+
更新映射的 key 自然唯一。`batch_update` 使用 `update_records` 为不同记录分别提交
|
|
208
|
+
patch;不要混用 MCP 工具层 `patch_records_batch` 的 `record_id_list + patch` 请求结构。
|
|
209
|
+
|
|
210
|
+
**附件非空检查由调用方负责**:Page/FaaS 先过滤空字段数组、再移除空记录映射,全部为空时
|
|
211
|
+
本地短路,不发送请求。不要依赖平台拒绝空输入:合法目标上的 `append []` 仍会读取并写回
|
|
212
|
+
已有列表,`remove []` 不产生实际变更并可返回成功。图片宽高须由 Page 从本地 `File` 解码
|
|
213
|
+
取得,不能依赖上传返回值中的可选宽高;非图片与解码失败的处理见[文件传输](file-transfer.md)。
|
|
214
|
+
|
|
215
|
+
所有写入的成功 HTTP body 都只有一层 OpenAPI envelope(batch 路由示例如下):
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"code": 0,
|
|
220
|
+
"msg": "",
|
|
221
|
+
"data": {
|
|
222
|
+
"record_id_list": ["rec_xxx"],
|
|
223
|
+
"record_not_found": ["rec_missing"]
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Node MCP 直接响应中的 `meta` 不会经过当前 FaaS Entry 的合成响应返回,FaaS 不得依赖该字段。
|
|
229
|
+
|
|
230
|
+
`batch_create` 和 `batch_update` 的 `data` 还可能包含被忽略的只读或不支持字段;未忽略任何字段时
|
|
231
|
+
`ignored_fields` 不存在:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"record_id_list": ["rec_xxx"],
|
|
236
|
+
"ignored_fields": [
|
|
237
|
+
{
|
|
238
|
+
"id": "fld_xxx",
|
|
239
|
+
"name": "创建时间",
|
|
240
|
+
"reason": "READONLY: created_at field is auto-created by the system."
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
附件增量路由的成功响应没有 `record_id_list`:`data.attachments` 按记录、字段回显
|
|
247
|
+
**本次操作合并出的附件列表**(`file_token` / `name` / `size`),页面用它更新本地状态;
|
|
248
|
+
它不是包含全部并发写入的最终快照,不要立即用 SQL 回读替换它(从库可能尚未同步)。超时或结果未知时保留已上传 token,按[文件传输](file-transfer.md#超时与结果未知)有界核对;append 不幂等,不能自动重提或重新上传。字段设置了「仅限手机拍摄」时,
|
|
249
|
+
`append_attachments` 对该字段的追加被忽略并进入 `ignored_fields`;`remove_attachments`
|
|
250
|
+
不受该限制,仍可移除。校验纪律同 batch:只认约定字段,不递归找。
|
|
251
|
+
|
|
252
|
+
runtime 识别后返回的是合成响应,因此这条链路上:
|
|
253
|
+
|
|
254
|
+
- HTTP 状态恒为 `200`,业务成败只看 body 的 `code`,不能只判 `response.ok`;
|
|
255
|
+
- 响应头没有 `x-tt-logid`,`DownstreamError.logId` 必然为空。排查改用 invoke 响应
|
|
256
|
+
自身的 LogID,见[下游错误归一化](#下游错误归一化)。
|
|
257
|
+
|
|
258
|
+
**三条 batch 路由**的成功响应固定读取 `body.data.record_id_list`,未找到的记录固定读取
|
|
259
|
+
`body.data.record_not_found`;**附件增量路由**的成功响应固定读取 `body.data.attachments`,
|
|
260
|
+
两类不得混用同一解析器。不得递归遍历整个响应寻找字符串,不得猜测或兼容 `record_id`、
|
|
261
|
+
`recordId`、`records[].id` 等未约定结构,也不得把 `data.record_id_list` 当作 handler 自定义返回的
|
|
262
|
+
`recordId`。三种 batch operation 分别校验:
|
|
263
|
+
|
|
264
|
+
- `batch_create`:`record_id_list.length` 必须与 `create_records.length` 一致,且不得返回 `record_not_found`。
|
|
265
|
+
- `batch_update`:`record_id_list` 是实际存在的记录,允许为空;它与 `record_not_found` 必须是请求 key 的不重叠、无遗漏分组。
|
|
266
|
+
- `batch_delete`:`record_id_list` 必须与请求列表一致,`record_not_found` 必须是其子集;实际删除的 ID 是两者的差集。
|
|
267
|
+
|
|
268
|
+
`code === 0` 且 `record_not_found` 非空是 Node MCP batch 的合法部分成功,不是 `invalid_response`。FaaS handler 必须在
|
|
269
|
+
业务结果中保留已完成和未找到的 ID;需要向 Page 表达部分成功时,使用当前 operation 的稳定业务码,
|
|
270
|
+
但不得伪装成下游失败或整体重试。只有响应缺失、类型不符或不满足上述 operation 约束时才按
|
|
271
|
+
`invalid_response` 处理;非幂等创建不得自动重试。
|
|
272
|
+
|
|
273
|
+
可复用的严格解析器:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
interface IgnoredRecordField {
|
|
277
|
+
id: string
|
|
278
|
+
name: string
|
|
279
|
+
reason: string
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
interface SiteRecordsWriteData {
|
|
283
|
+
record_id_list: string[]
|
|
284
|
+
record_not_found?: string[]
|
|
285
|
+
ignored_fields?: IgnoredRecordField[]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface SiteRecordsWriteEnvelope {
|
|
289
|
+
code: number
|
|
290
|
+
msg: string
|
|
291
|
+
data: SiteRecordsWriteData
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function parseSiteRecordsWriteData(
|
|
295
|
+
value: unknown,
|
|
296
|
+
): SiteRecordsWriteData | undefined {
|
|
297
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
298
|
+
const envelope = value as Record<string, unknown>
|
|
299
|
+
if (envelope.code !== 0) return undefined
|
|
300
|
+
if (typeof envelope.data !== 'object' || envelope.data === null) {
|
|
301
|
+
return undefined
|
|
302
|
+
}
|
|
303
|
+
const data = envelope.data as Record<string, unknown>
|
|
304
|
+
const recordIdPattern = /^rec[A-Za-z0-9]{1,11}$/
|
|
305
|
+
const parseRecordIDs = (input: unknown): string[] | undefined => {
|
|
306
|
+
if (
|
|
307
|
+
!Array.isArray(input) ||
|
|
308
|
+
input.length > 200 ||
|
|
309
|
+
input.some(
|
|
310
|
+
(recordId) =>
|
|
311
|
+
typeof recordId !== 'string' || !recordIdPattern.test(recordId),
|
|
312
|
+
) ||
|
|
313
|
+
new Set(input).size !== input.length
|
|
314
|
+
) {
|
|
315
|
+
return undefined
|
|
316
|
+
}
|
|
317
|
+
return input as string[]
|
|
318
|
+
}
|
|
319
|
+
const recordIDs = parseRecordIDs(data.record_id_list)
|
|
320
|
+
const recordNotFound =
|
|
321
|
+
data.record_not_found === undefined
|
|
322
|
+
? undefined
|
|
323
|
+
: parseRecordIDs(data.record_not_found)
|
|
324
|
+
if (
|
|
325
|
+
!recordIDs ||
|
|
326
|
+
(data.record_not_found !== undefined && !recordNotFound) ||
|
|
327
|
+
(recordIDs.length === 0 && (recordNotFound?.length ?? 0) === 0)
|
|
328
|
+
) {
|
|
329
|
+
return undefined
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
record_id_list: recordIDs,
|
|
333
|
+
...(recordNotFound ? { record_not_found: recordNotFound } : {}),
|
|
334
|
+
...(Array.isArray(data.ignored_fields)
|
|
335
|
+
? { ignored_fields: data.ignored_fields as IgnoredRecordField[] }
|
|
336
|
+
: {}),
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
该 helper 只负责成功 payload 的固定结构;HTTP 失败、非零业务码和错误摘要仍按
|
|
342
|
+
[下游错误归一化](#下游错误归一化)处理。
|
|
343
|
+
|
|
344
|
+
#### 创建实现示例
|
|
345
|
+
|
|
346
|
+
与上文的类型、输入校验和 batch 解析器组合使用;`BASE_TOKEN`、`TABLE_ID` 是根据 Catalog 设置的工程常量。`created:null` 表示结果未知,页面不能把它当作未写入后自动重试。
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
export default async function handler(
|
|
350
|
+
rawParams: unknown,
|
|
351
|
+
context: FunctionContext,
|
|
352
|
+
): Promise<FaasHandlerResult<unknown>> {
|
|
353
|
+
let input: CreateTaskInput
|
|
354
|
+
try {
|
|
355
|
+
input = parseCreateTask(rawParams)
|
|
356
|
+
} catch {
|
|
357
|
+
return { data: null, code: 40001, msg: '任务名称或状态无效' }
|
|
358
|
+
}
|
|
359
|
+
const OPENAPI_ORIGIN = 'https://open.feishu.cn'
|
|
360
|
+
let response: Response
|
|
361
|
+
try {
|
|
362
|
+
response = await context.fetch(
|
|
363
|
+
`${OPENAPI_ORIGIN}/open-apis/bitable/v1/apps/${encodeURIComponent(BASE_TOKEN)}` +
|
|
364
|
+
`/tables/${encodeURIComponent(TABLE_ID)}/records/batch_create`,
|
|
365
|
+
{
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
368
|
+
body: JSON.stringify({
|
|
369
|
+
create_records: [
|
|
370
|
+
{
|
|
371
|
+
任务名称: input.title,
|
|
372
|
+
状态: input.status,
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
}),
|
|
376
|
+
},
|
|
377
|
+
)
|
|
378
|
+
} catch {
|
|
379
|
+
return {
|
|
380
|
+
data: { created: null, errors: [{
|
|
381
|
+
source: 'base_openapi', step: 'createTask', kind: 'transport',
|
|
382
|
+
operation: 'batch_create', msg: '未取得创建响应', retryable: false,
|
|
383
|
+
}] },
|
|
384
|
+
code: 50202,
|
|
385
|
+
msg: '创建结果待确认,请勿重复提交',
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const body: unknown = await response.json().catch(() => null)
|
|
390
|
+
|
|
391
|
+
if (
|
|
392
|
+
!response.ok ||
|
|
393
|
+
typeof body !== 'object' ||
|
|
394
|
+
body === null ||
|
|
395
|
+
!('code' in body) ||
|
|
396
|
+
body.code !== 0
|
|
397
|
+
) {
|
|
398
|
+
const logId =
|
|
399
|
+
response.headers.get('x-tt-logid') ??
|
|
400
|
+
response.headers.get('x-request-id') ??
|
|
401
|
+
undefined
|
|
402
|
+
const upstreamCode =
|
|
403
|
+
typeof body === 'object' &&
|
|
404
|
+
body !== null &&
|
|
405
|
+
'code' in body &&
|
|
406
|
+
typeof body.code === 'number'
|
|
407
|
+
? body.code
|
|
408
|
+
: undefined
|
|
409
|
+
const rawUpstreamMsg =
|
|
410
|
+
typeof body === 'object' &&
|
|
411
|
+
body !== null &&
|
|
412
|
+
'msg' in body &&
|
|
413
|
+
typeof body.msg === 'string'
|
|
414
|
+
? body.msg.trim().slice(0, 300)
|
|
415
|
+
: ''
|
|
416
|
+
const upstreamMsg =
|
|
417
|
+
rawUpstreamMsg || response.statusText || `OpenAPI HTTP ${response.status}`
|
|
418
|
+
const isOpenApiError =
|
|
419
|
+
typeof upstreamCode === 'number' && upstreamCode !== 0
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
data: {
|
|
423
|
+
created: isOpenApiError ? false : null,
|
|
424
|
+
errors: [
|
|
425
|
+
{
|
|
426
|
+
source: 'base_openapi',
|
|
427
|
+
step: 'createTask',
|
|
428
|
+
kind: isOpenApiError ? 'openapi' : 'invalid_response',
|
|
429
|
+
operation: 'batch_create',
|
|
430
|
+
httpStatus: response.status,
|
|
431
|
+
...(typeof upstreamCode === 'number'
|
|
432
|
+
? { code: upstreamCode }
|
|
433
|
+
: {}),
|
|
434
|
+
msg: upstreamMsg,
|
|
435
|
+
...(logId ? { logId: logId.trim().slice(0, 128) } : {}),
|
|
436
|
+
retryable: false,
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
},
|
|
440
|
+
code: 50201,
|
|
441
|
+
msg: isOpenApiError ? '创建任务失败' : '创建结果待确认,请勿重复提交',
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const created = parseSiteRecordsWriteData(body)
|
|
446
|
+
if (
|
|
447
|
+
!created ||
|
|
448
|
+
created.record_id_list.length !== 1 ||
|
|
449
|
+
(created.record_not_found?.length ?? 0) > 0
|
|
450
|
+
) {
|
|
451
|
+
return {
|
|
452
|
+
data: {
|
|
453
|
+
created: null,
|
|
454
|
+
errors: [
|
|
455
|
+
{
|
|
456
|
+
source: 'base_openapi',
|
|
457
|
+
step: 'createTask',
|
|
458
|
+
kind: 'invalid_response',
|
|
459
|
+
operation: 'batch_create',
|
|
460
|
+
msg: '响应中的记录 ID 数量不符合请求',
|
|
461
|
+
retryable: false,
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
code: 50202,
|
|
466
|
+
msg: '创建结果待确认,请勿重复提交',
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const [recordId] = created.record_id_list
|
|
471
|
+
if (created.ignored_fields?.length) {
|
|
472
|
+
return {
|
|
473
|
+
data: { created: true, recordId, ignoredFields: created.ignored_fields },
|
|
474
|
+
code: 40901,
|
|
475
|
+
msg: '任务已创建,但部分字段未写入,请勿重复创建',
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return { data: { created: true, recordId }, code: 0, msg: 'success' }
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
FaaS 写入 Base 时,必须调用目标环境的
|
|
483
|
+
`/open-apis/bitable/v1/apps/:base_token/tables/:table_id/records/batch_*`(记录批量写)或
|
|
484
|
+
`.../append_attachments`、`.../remove_attachments`(附件增量)路由;
|
|
485
|
+
`:base_token` 取 `catalog.json` 的 `tables[].baseToken`,并检查 HTTP 状态和业务码。
|
|
486
|
+
|
|
487
|
+
需要读 Base 时调用固定 SQL Function:
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
const result: unknown = await context.invokeFunction({
|
|
491
|
+
projectId: PROJECT_ID,
|
|
492
|
+
functionId: 'findTaskById',
|
|
493
|
+
params: [input.recordId],
|
|
494
|
+
})
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
确需写前查重、权限范围、状态检查或写后查询时复用固定 SQL Function;可以在一次查询中完成的检查不拆成多个串行调用。前一个 SQL 返回的关联 ID 仅用于定位后一个 SQL 的记录时,优先把关联查询下推为一个 SQL Function。
|
|
498
|
+
|
|
499
|
+
## Base 读写协议
|
|
500
|
+
|
|
501
|
+
同一个字段在创建 schema、SQL 读取和 FaaS 应用记录 写入时使用三套不同但相关的数据结构。下表只适用于 FaaS 固定 batch 与附件增量路由;开发期 lark-cli 示例数据使用[发布手册中的独立输入契约](release.md#从零建站示例数据验收)。
|
|
502
|
+
|
|
503
|
+
| Field JSON | SQL 读取 | FaaS 应用记录 CellValue |
|
|
504
|
+
|---|---|---|
|
|
505
|
+
| `text` | `TEXT` string | string |
|
|
506
|
+
| `number` | `DOUBLE` number | finite number |
|
|
507
|
+
| 单选 `select` | `OPTION` string | 合法选项名 string |
|
|
508
|
+
| 多选 `select` | `COLLECTION<OPTION>` | 合法选项名 string[] |
|
|
509
|
+
| `datetime` | `TIMESTAMP` 毫秒 number | `YYYY-MM-DD HH:mm:ss` |
|
|
510
|
+
| `checkbox` | `BOOL` boolean | boolean |
|
|
511
|
+
| 单/多 `user` | `USER` / `COLLECTION<USER>` | `[{"id":"6882577547121393684"}]`;`id` 是内部 `user_id` |
|
|
512
|
+
| 单/多 `group_chat` | `GROUP` / `COLLECTION<GROUP>` | `[{"id":"oc_xxx"}]` |
|
|
513
|
+
| `link` | `COLLECTION<RECORD_ID>` | `[{"id":"rec_xxx"}]` |
|
|
514
|
+
| `attachment` | `COLLECTION<FILE>` | `[{"file_token":"...","image_width":number,"image_height":number}]`;追加/移除走附件增量路由,`batch_update` 写该字段是整格覆盖、会丢已有图片宽高,仅用于置空或明确重设。见[文件传输](file-transfer.md) |
|
|
515
|
+
| `location` | `UNSUPPORTED` | `{"lng":number,"lat":number}` |
|
|
516
|
+
| 公式、lookup、系统字段 | 推导类型或 `UNSUPPORTED` | 只读 |
|
|
517
|
+
|
|
518
|
+
写入时 `null` 清空字段,`[]` 表示空集合,省略字段表示不修改。选项名、用户/群 ID 和关联 record ID 必须来自真实数据。
|
|
519
|
+
|
|
520
|
+
应用 行写入只使用 [应用记录 写入协议](#应用记录-写入协议)中的固定路由。
|
|
521
|
+
|
|
522
|
+
创建一行或多行时,`create_records` 中每一项是字段到 cell value 的映射,不同记录可以填写不同字段:
|
|
523
|
+
|
|
524
|
+
```json
|
|
525
|
+
{
|
|
526
|
+
"create_records": [
|
|
527
|
+
{
|
|
528
|
+
"任务名称": "客户回访",
|
|
529
|
+
"状态": "待处理",
|
|
530
|
+
"负责人": [{ "id": "6882577547121393684" }]
|
|
531
|
+
}
|
|
532
|
+
]
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
更新时把以 record ID 为 key 的字段 patch 放入 `update_records`:
|
|
537
|
+
|
|
538
|
+
```json
|
|
539
|
+
{
|
|
540
|
+
"update_records": {
|
|
541
|
+
"rec_xxx": { "状态": "已完成" },
|
|
542
|
+
"rec_yyy": { "状态": "进行中", "备注": "处理中" }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
删除时传入 record ID 列表:
|
|
548
|
+
|
|
549
|
+
```json
|
|
550
|
+
{
|
|
551
|
+
"record_id_list": ["rec_xxx", "rec_yyy"]
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
检查 HTTP 状态和外层业务码后,还必须按固定成功响应协议校验:batch 路由校验
|
|
556
|
+
`data.record_id_list`,附件增量路由校验 `data.attachments`;需要后续写入、关联或回读时,
|
|
557
|
+
只使用约定字段中的记录 ID。
|
|
558
|
+
|
|
559
|
+
batch 路由单批最多 200 条。多选、多人员、关联这些集合字段是**整格覆盖**:追加或移除必须先读现有值、合并出完整列表再整格写回,直接写会丢掉单元格里已有的项。**附件字段是唯一的例外**:追加/移除走上面的附件增量路由,禁止用读-合并-整格写回实现——服务端不会补齐省略的宽高,而 SQL 读不回旧图宽高,基于 SQL 列表的重写无法重建宽高;并发时整格写回还会互相覆盖。见[文件传输](file-transfer.md)的「写单元格」。
|
|
560
|
+
|
|
561
|
+
**整格覆盖的合并基准必须是可信的完整列表。** SQL 回读走从库,刚写完就回读可能拿到写入前的旧值;拿缺项的列表做基准整格写回,会静默删掉其它项,而写入仍返回成功。读取失败、从未读到过、或回读缺少本次刚写入的项时,禁掉入口让用户稍后重试,不要写。
|
|
562
|
+
|
|
563
|
+
页面内可串行处理同一记录字段的更新,但不能因此承诺跨访问者原子性。文件字节的上传与展示是另外的路,见[文件传输](file-transfer.md)。
|