@lark-base-open/base-database-cli 1.0.0
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 +11 -0
- package/README.md +169 -0
- package/changelog/CHANGELOG-0.x.md +103 -0
- package/config-schema.json +28 -0
- package/dist/agent-guide.d.ts +18 -0
- package/dist/agent-guide.d.ts.map +1 -0
- package/dist/agent-guide.js +30 -0
- package/dist/agent-guide.js.map +1 -0
- package/dist/archive.d.ts +16 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +162 -0
- package/dist/archive.js.map +1 -0
- package/dist/assets.d.ts +4 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +6 -0
- package/dist/assets.js.map +1 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +63 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/database-skill.d.ts +22 -0
- package/dist/database-skill.d.ts.map +1 -0
- package/dist/database-skill.js +135 -0
- package/dist/database-skill.js.map +1 -0
- package/dist/deploy.d.ts +28 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +476 -0
- package/dist/deploy.js.map +1 -0
- package/dist/environment.d.ts +12 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +27 -0
- package/dist/environment.js.map +1 -0
- package/dist/function-schema.d.ts +3 -0
- package/dist/function-schema.d.ts.map +1 -0
- package/dist/function-schema.js +310 -0
- package/dist/function-schema.js.map +1 -0
- package/dist/init.d.ts +77 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +418 -0
- package/dist/init.js.map +1 -0
- package/dist/lark-cli.d.ts +8 -0
- package/dist/lark-cli.d.ts.map +1 -0
- package/dist/lark-cli.js +228 -0
- package/dist/lark-cli.js.map +1 -0
- package/dist/npm.d.ts +13 -0
- package/dist/npm.d.ts.map +1 -0
- package/dist/npm.js +93 -0
- package/dist/npm.js.map +1 -0
- package/dist/page-title.d.ts +3 -0
- package/dist/page-title.d.ts.map +1 -0
- package/dist/page-title.js +47 -0
- package/dist/page-title.js.map +1 -0
- package/dist/preview-receipt.d.ts +23 -0
- package/dist/preview-receipt.d.ts.map +1 -0
- package/dist/preview-receipt.js +133 -0
- package/dist/preview-receipt.js.map +1 -0
- package/dist/process.d.ts +17 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +53 -0
- package/dist/process.js.map +1 -0
- package/dist/program.d.ts +12 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +204 -0
- package/dist/program.js.map +1 -0
- package/dist/pull.d.ts +18 -0
- package/dist/pull.d.ts.map +1 -0
- package/dist/pull.js +324 -0
- package/dist/pull.js.map +1 -0
- package/dist/skill.d.ts +7 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +28 -0
- package/dist/skill.js.map +1 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-fingerprint.d.ts +8 -0
- package/dist/workspace-fingerprint.d.ts.map +1 -0
- package/dist/workspace-fingerprint.js +64 -0
- package/dist/workspace-fingerprint.js.map +1 -0
- package/package.json +54 -0
- package/skills/lark-base-database/SKILL.md +307 -0
- package/skills/lark-base-database/references/base-capability-routing.md +81 -0
- package/skills/lark-base-database/references/base-sql-unsupported.md +42 -0
- package/skills/lark-base-database/references/database-capability-skill.md +24 -0
- package/skills/lark-base-database/references/development.md +642 -0
- package/skills/lark-base-database/references/file-transfer.md +275 -0
- package/skills/lark-base-database/references/permission-design.md +138 -0
- package/skills/lark-base-database/references/release.md +81 -0
- package/skills/lark-base-database/references/sql-function.md +276 -0
- package/skills/lark-base-database/references/style-guide.md +88 -0
- package/templates/database-capability/SKILL.md.tmpl +41 -0
- package/templates/database-starter/README.md +11 -0
- package/templates/database-starter/gitignore +3 -0
- package/templates/database-starter/page/.prettierignore +4 -0
- package/templates/database-starter/page/.prettierrc.json +7 -0
- package/templates/database-starter/page/README.md +29 -0
- package/templates/database-starter/page/index.html +24 -0
- package/templates/database-starter/page/package-lock.json +2675 -0
- package/templates/database-starter/page/package.json +44 -0
- package/templates/database-starter/page/public/favicon.png +0 -0
- package/templates/database-starter/page/src/App.tsx +18 -0
- package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +267 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/index.css +32 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/index.test.ts +24 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +478 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +175 -0
- package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +201 -0
- package/templates/database-starter/page/src/components/database-runtime-ready.tsx +21 -0
- package/templates/database-starter/page/src/components/database-user-menu/index.tsx +238 -0
- package/templates/database-starter/page/src/components/feedback/page-state.tsx +66 -0
- package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +23 -0
- package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +873 -0
- package/templates/database-starter/page/src/components/floating-button/index.tsx +196 -0
- package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +25 -0
- package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +197 -0
- package/templates/database-starter/page/src/components/floating-button/utils/current-user.ts +56 -0
- package/templates/database-starter/page/src/components/floating-button/utils/env.ts +18 -0
- package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +79 -0
- package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +252 -0
- package/templates/database-starter/page/src/components/ui/button.tsx +56 -0
- package/templates/database-starter/page/src/index.css +67 -0
- package/templates/database-starter/page/src/lib/cn.test.ts +9 -0
- package/templates/database-starter/page/src/lib/cn.ts +7 -0
- package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +168 -0
- package/templates/database-starter/page/src/lib/database-runtime-context.test.ts +111 -0
- package/templates/database-starter/page/src/lib/database-runtime.test.ts +103 -0
- package/templates/database-starter/page/src/lib/database-runtime.ts +389 -0
- package/templates/database-starter/page/src/main.tsx +34 -0
- package/templates/database-starter/page/src/vite-env.d.ts +1 -0
- package/templates/database-starter/page/tsconfig.json +23 -0
- package/templates/database-starter/page/vite.config.ts +13 -0
- package/templates/database-starter/prettierignore +1 -0
- package/templates/database-starter/server-functions/.prettierignore +3 -0
- package/templates/database-starter/server-functions/.prettierrc.json +7 -0
- package/templates/database-starter/server-functions/README.md +11 -0
- package/templates/database-starter/server-functions/package-lock.json +1687 -0
- package/templates/database-starter/server-functions/package.json +22 -0
- package/templates/database-starter/server-functions/scripts/run-tsc.mjs +50 -0
- package/templates/database-starter/server-functions/tsconfig.json +20 -0
- package/templates/database-starter/sql/README.md +8 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
# 数据库应用代码开发手册
|
|
2
|
+
|
|
3
|
+
本文负责 Page、SQL Function 和 FaaS 的代码边界与 ABI。搭建和发布命令见 [Skill 主流程](../SKILL.md),SQL 方言与类型见 [SQL Function 专项](sql-function.md),工程取舍见 [风格手册](style-guide.md)。
|
|
4
|
+
|
|
5
|
+
## 阅读路径
|
|
6
|
+
|
|
7
|
+
- [数据库应用代码开发手册](#数据库应用代码开发手册)
|
|
8
|
+
- [阅读路径](#阅读路径)
|
|
9
|
+
- [工程结构](#工程结构)
|
|
10
|
+
- [Function 调用契约](#function-调用契约)
|
|
11
|
+
- [Page 调用](#page-调用)
|
|
12
|
+
- [自动化实现边界](#自动化实现边界)
|
|
13
|
+
- [FaaS 开发](#faas-开发)
|
|
14
|
+
- [远程调用与写入链路设计](#远程调用与写入链路设计)
|
|
15
|
+
- [下游错误归一化](#下游错误归一化)
|
|
16
|
+
- [写入行(记录)](#写入行记录)
|
|
17
|
+
- [应用记录 写入协议](#应用记录写入协议)
|
|
18
|
+
- [创建实现示例](#创建实现示例)
|
|
19
|
+
- [Base 读写协议](#base-读写协议)
|
|
20
|
+
|
|
21
|
+
## 工程结构
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
project/
|
|
25
|
+
├── page/
|
|
26
|
+
├── sql/<functionId>/{main.sql,meta.json}
|
|
27
|
+
└── server-functions/functions/<functionId>/{index.ts,meta.json}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
SQL 与 FaaS 的 `functionId` 在应用内全局唯一,匹配 `/^[A-Za-z0-9_-]+$/`;SQL ID 长度为 1~64。`functionId` 会作为 URL 路径段参与运行态调用,除该字符集外不接受其它字符。不同 FaaS Function 目录独立构建,不跨目录 import。Page 和 `server-functions/` 分别维护自己的 npm 工程与 `package-lock.json`。
|
|
31
|
+
|
|
32
|
+
## Function 调用契约
|
|
33
|
+
|
|
34
|
+
ABI 是 Page、应用加载器 与 Function 之间固定的数据边界。Page 侧两类 Function 都通过 `invokeDatabaseFunction(functionId, params)` 调用,但 `params` 类型不同:
|
|
35
|
+
|
|
36
|
+
`functionId` 使用源码目录名,是工程常量。应用 token 由 `page/src/lib/database-runtime.ts` 从产物托管路径解析,不由业务代码传入;FaaS 内部的 `context.invokeFunction` 仍使用 `{projectId,functionId,params}`,两者不要混用。
|
|
37
|
+
|
|
38
|
+
| Function | `params` | 实现 | 结果 |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| SQL | 按 `$1`、`$2` 排列的数组 | `main.sql` + `meta.json` | 一层 `{data:{rows},code,msg}` |
|
|
41
|
+
| FaaS | 业务对象 | `index.ts` + `meta.json` | 一层 `{data,code,msg}`,`data` 是 handler 业务数据 |
|
|
42
|
+
|
|
43
|
+
SQL 示例:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
await invokeDatabaseFunction<{ rows: unknown[] }>('listTasks', ['待处理'])
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
FaaS 创建操作示例:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
await invokeDatabaseFunction<{ created: boolean }>('createTask', {
|
|
53
|
+
title: '准备评审',
|
|
54
|
+
status: '待处理',
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`invokeDatabaseFunction` 打统一网关的 `POST /space/api/bitable/site/v1/:token/invoke_function/:function_id`,由 BaseApp 父页面以同源身份代发;应用 token 与环境由 `src/lib/database-runtime.ts` 统一解析,`env` 走 query,请求体固定为 `{"params": <上述 params>}`。返回统一包裹结构 `{code,msg,data}`,页面使用当前登录用户身份,不保存也不传递任何凭证。
|
|
59
|
+
|
|
60
|
+
## Page 调用
|
|
61
|
+
|
|
62
|
+
默认 Page 的 `page/index.html` 需要把 `<title>` 设置为本次生成的应用名称,不留空。
|
|
63
|
+
|
|
64
|
+
默认 Page 必须引用并渲染 `page/src/components/floating-button/index.tsx` 中的 `FloatingButton` 组件,并显式传入 `language`。该组件自行解析应用 token,不接受 `projectId`。悬浮控件现在只有「编辑应用」和 Chatbot 两个入口,没有「三个点」菜单。
|
|
65
|
+
|
|
66
|
+
当前账号信息(头像、姓名、组织、退出登录)由**应用自己的导航**承载,用 `page/src/components/database-user-menu/index.tsx` 的 `DatabaseUserMenu`。位置由你按页面布局决定,放在用户会去找账号的地方——顶栏右上角、侧边栏左下角都行,不要再塞进浮层:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
// 顶栏右上角
|
|
70
|
+
<header className="flex items-center justify-between ...">
|
|
71
|
+
<h1>设备台账</h1>
|
|
72
|
+
{/* 判据:员工 / 部门 / 领用归还属于组织内多角色协作 */}
|
|
73
|
+
<DatabaseUserMenu language="zh" audience="enterprise" />
|
|
74
|
+
</header>
|
|
75
|
+
|
|
76
|
+
// 侧边栏左下角
|
|
77
|
+
<aside className="flex flex-col ...">
|
|
78
|
+
<nav className="flex-1">…</nav>
|
|
79
|
+
<DatabaseUserMenu language="zh" audience="enterprise" align="left" side="up" />
|
|
80
|
+
</aside>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`align` 控制弹层贴左还是贴右,`side` 控制向上还是向下展开,`showName={false}` 用于窄侧栏只留头像。
|
|
84
|
+
|
|
85
|
+
`audience` 决定这块身份信息出不出现。它交代的是「你是谁、属于哪个组织、怎么退出」,只有会被本人以外的人打开的系统才需要;个人系统里是噪音,`personal` 时组件整块不渲染。判定在应用开发时做一次并写死在调用处,运行时不推断——产物里没有可靠信号(应用数据模型普遍不建模「用户」这一维),而忽隐忽现比判错更糟。
|
|
86
|
+
|
|
87
|
+
判定规则,命中任一即 `enterprise`:
|
|
88
|
+
|
|
89
|
+
1. 需求里出现组织角色词:员工、客户、供应商、部门、审批、工单、门店、团队成员;
|
|
90
|
+
2. 数据模型里存在**第三方实体表**:供应商、客户、合作方、申请人;
|
|
91
|
+
3. 存在跨人流程:提交→审批、分配 / 指派、对账、交接;
|
|
92
|
+
4. 用户明确说要分享给同事或团队使用。
|
|
93
|
+
|
|
94
|
+
全不命中且主体是「我」(我的账本、习惯打卡、收藏夹、健身记录、背单词——数据只描述自己的行为)时用 `personal`。
|
|
95
|
+
|
|
96
|
+
**拿不准一律用 `enterprise`。** 代价不对称:多显示一个头像只是碍眼,少显示会让用户在应用内找不到退出登录入口。
|
|
97
|
+
|
|
98
|
+
判定结果要在建模确认环节一并向用户说明,并写清判据,便于用户当场纠正。判错时用户改这一行重新部署即可,不需要重建应用。
|
|
99
|
+
|
|
100
|
+
`page/src/main.tsx` 已在首屏挂载后通过 `DatabaseRuntimeReady` 上报 `siteRuntime.ready()`,并在启动失败时上报 `reportLoadFailed`。宿主从握手完成起 10 秒内等不到 ready 会展示加载失败态,因此不要把 ready 上报移到数据加载完成之后,也不要删除该组件。
|
|
101
|
+
|
|
102
|
+
涉及人员搜索或人员选择的页面,优先引用 `page/src/components/UserSelectorEditor` 和 `page/src/components/UserSelectorPanel` 等 `page/src/components/` 下的人员组件;只有现有组件无法满足交互契约时才做局部扩展。
|
|
103
|
+
选人组件返回的 `User.user_id` 是 Base 人员单元格使用的内部用户 ID;写入人员字段时固定传 `[{"id": user.user_id}]`。`User.open_id` 只是可选的开放平台标识,不得填入应用记录 写入 payload。
|
|
104
|
+
|
|
105
|
+
业务请求集中在 operation/runtime client,不散落在组件中:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { invokeDatabaseFunction } from '../lib/database-runtime'
|
|
109
|
+
|
|
110
|
+
interface SqlRows<TRow> {
|
|
111
|
+
rows: TRow[]
|
|
112
|
+
columns?: unknown[]
|
|
113
|
+
row_count?: number
|
|
114
|
+
log_id?: string
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
浏览器 SDK 对 SQL 和 FaaS 都只返回一层 `{data,code,msg}`。`invokeDatabaseFunction<TData>` 的 `TData` 只描述当前 operation 约定的顶层 `data`:SQL 通常是 ResultJSON,FaaS 则以各 handler 的业务结果为准,不能统一假设存在 `recordId`。页面只检查一次顶层 `code`,再按 operation 契约校验 `data`;不得写成 `invokeDatabaseFunction<FaasHandlerResult<TData>>`,也不得对 `result.data` 二次解包。否则后端已经写入成功时,页面仍可能误报失败。
|
|
119
|
+
|
|
120
|
+
运行时业务数据在边界处校验,不能只依赖 TypeScript 泛型。`functionId` 和所有路由是工程常量;用户输入只进入已声明的业务参数。
|
|
121
|
+
|
|
122
|
+
Page 的所有业务 HTTP 都经父页面代理,使用 `page/src/lib/database-runtime.ts` 导出的 `fetch` 或 `invokeHttp`,URL 必须是以单个 `/` 开头的同源相对路径;绝对 URL、跨域 URL、路径穿越以及 Cookie、Authorization、Host、Origin 等受限 Header 都会被父页面拒绝。请求体只支持字符串,4xx/5xx 会正常返回 `Response`,业务必须自行判断 `response.ok`。禁止直连业务域名,也不得把直连作为代理失败时的兜底。Page 发起的 Base 业务读取和写入仍分别走 SQL Function 与 FaaS。
|
|
123
|
+
|
|
124
|
+
#### 从零搭建应用的示例数据
|
|
125
|
+
|
|
126
|
+
从零创建 Base 时,交付前必须写入一组可识别的真实示例记录。Preview 发布后,从 `catalog.json` 取得真实 Base token、table ID、字段类型和选项,通过项目级 Base Database CLI 路由先执行 `lark-cli base +field-list` 核对可写字段,再执行 `lark-cli base +record-batch-create` 写入;当前环境提供等价 CLI 前缀时可以替换前缀,没有项目级路由时直接使用 `lark-cli ... --as user`。示例初始化不要求创建 seed 专用 Function;页面的真实业务写入使用 FaaS。
|
|
127
|
+
|
|
128
|
+
示例数据保持小而自洽,覆盖默认展示需要的核心对象、典型状态和必要关联;使用非敏感值,并在可见业务主字段中带稳定“示例”标记,不增加只为播种服务的字段。不要在页面加载 Effect 中自动播种。修复后再次写入前先用 SQL Function 查询稳定标记或上一轮返回的 record ID,只补齐缺失记录;非幂等结果未知时不能盲目重试。
|
|
129
|
+
|
|
130
|
+
列表、详情、看板、统计等展示必须通过 Page 实际使用的 SQL Function 读取 Base。写入成功后通过 `/open-apis/base/v3/sites/:site_token/invoke_function/:function_id?env=preview` 调用该 SQL Function,请求体使用 `{"params":[...]}`,确认返回真实示例记录及预期关系/聚合;页面不得用写入返回值、硬编码数组、`localStorage` 或 fallback mock 冒充查询成功。`lark-cli` 写入失败、写入数量不符或 SQL 回读缺失都属于闭环失败;实现问题必须修复、重新发布 Preview 并重跑写入—查询闭环,认证或平台阻塞与实现失败分开记录。
|
|
131
|
+
|
|
132
|
+
## 自动化实现边界
|
|
133
|
+
|
|
134
|
+
代码实现前复核需求拆解结果:Page 主动提交且需要服务端校验的即时业务写入可以实现为 FaaS;定时、记录变化触发、提醒、通知等自动化优先使用原生多维表格能力,不创建 FaaS Function,也不从 Page 调用 FaaS 模拟自动化。
|
|
135
|
+
|
|
136
|
+
无法确认一项需求是否由原生 Base 支持时,必须先执行[原生 Base 能力发现与路由](base-capability-routing.md):查询当前 `lark-cli base` shortcut、对应命令帮助和目标 Base 的真实资源,再决定由原生 Base、数据库应用开发链路或两者组合实现。未完成该查询前不得记录能力缺口;查询后仍受限时,必须区分原生能力缺口、当前 CLI 覆盖缺口和权限或环境阻塞。
|
|
137
|
+
|
|
138
|
+
## FaaS 开发
|
|
139
|
+
|
|
140
|
+
每个由 Page 主动发起的写入 operation 的目录:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
server-functions/functions/<functionId>/
|
|
144
|
+
├── index.ts
|
|
145
|
+
└── meta.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"name": "创建任务",
|
|
151
|
+
"description": "校验输入并创建一条任务记录",
|
|
152
|
+
"inputSchema": {
|
|
153
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
154
|
+
"type": "object",
|
|
155
|
+
"properties": {
|
|
156
|
+
"title": { "type": "string", "minLength": 1 },
|
|
157
|
+
"priority": { "type": "string", "enum": ["高", "中", "低"] }
|
|
158
|
+
},
|
|
159
|
+
"required": ["title"],
|
|
160
|
+
"additionalProperties": false
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`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 个。
|
|
166
|
+
|
|
167
|
+
SQL Function 的 `inputSchema` 不手写,由 CLI 根据 `meta.params` 的顺序和 `kind` 确定性生成固定长度数组 Schema;`TIMESTAMP` 与 `DURATION` 均明确为毫秒整数。
|
|
168
|
+
|
|
169
|
+
上传时 CLI 把 Schema 对象放入 `sourceCode` JSON 字符串:SQL 为 `{sql,meta,inputSchema}`,FaaS 为 `{inputSchema}`,不发送独立的 Function `inputSchema` 字段。FaaS 的可执行代码仍在源码包,`meta.json` 的写法不变;发现接口仍独立返回 `inputSchema` JSON 字符串。
|
|
170
|
+
|
|
171
|
+
loader 读取 HTTP `req.body.params`,把这个业务对象作为 handler 第一个参数 `rawParams`;第二个参数是 runtime context:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
interface FunctionContext {
|
|
175
|
+
fetch: typeof fetch
|
|
176
|
+
invokeFunction(request: {
|
|
177
|
+
projectId: string
|
|
178
|
+
functionId: string
|
|
179
|
+
params: unknown[]
|
|
180
|
+
}): Promise<unknown>
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface FaasHandlerResult<T> {
|
|
184
|
+
data: T | null
|
|
185
|
+
code: number
|
|
186
|
+
msg: string
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default async function handler(
|
|
190
|
+
rawParams: unknown,
|
|
191
|
+
context: FunctionContext,
|
|
192
|
+
): Promise<FaasHandlerResult<unknown>> {
|
|
193
|
+
const input = parseCreateTask(rawParams)
|
|
194
|
+
// 执行业务写入。
|
|
195
|
+
return { data: { title: input.title }, code: 0, msg: 'success' }
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`rawParams` 已经是业务对象。输入校验失败返回稳定业务码、`data:null` 和可读 `msg`。`FaasHandlerResult` 只描述 handler 的返回约定,不得作为页面 `invokeFunction` 的泛型参数。
|
|
200
|
+
|
|
201
|
+
### 远程调用与写入链路设计
|
|
202
|
+
|
|
203
|
+
实现前先按设计结果确认一致性、部分成功、重试语义和远程调用依赖。以一次 Page/FaaS operation 为边界列出 SQL Function、`context.fetch` 和写入 OpenAPI;标记哪些输入在 operation 开始时已经确定、哪些必须等待前一步响应。默认使用满足业务语义的最少远程等待阶段:
|
|
204
|
+
|
|
205
|
+
- 同表多条创建、更新或删除合并到一个 batch 请求,单批不超过 200 条。
|
|
206
|
+
- 同一 operation 调用多个 SQL Function 或 FaaS Function 时,输入已确定、互不依赖,且并发不改变一致性或部分成功语义的调用尽量并发;只有真实响应依赖才串行。
|
|
207
|
+
- 多个无副作用读取或请求的参数已经确定且互不依赖时,先启动全部 Promise,再在共同消费结果处等待;存在真实响应依赖时才串行。
|
|
208
|
+
- 原始 Promise 仍可能 reject 且需要知道每一路结果时使用 `Promise.allSettled`;helper 已把异常归一化为结构化结果时可以使用 `Promise.all`。
|
|
209
|
+
- 写前 SQL 只用于服务端必须确认的权限、关键状态或其他可信数据;写后 SQL 回读用于验收或明确要求的业务校验,不作为每次生产写入的默认步骤。
|
|
210
|
+
- 页面状态仅可用于允许弱一致性的非安全敏感计算。用户身份、权限、租户隔离、支付、库存、额度等约束不得信任页面传值。
|
|
211
|
+
- 多路写入返回值区分全部成功、部分成功和全部失败,并携带已取得的 record ID 与安全错误信息。非幂等创建结果未知时不自动整体重试。
|
|
212
|
+
- 需要事务、唯一约束、条件更新或原子 compare-and-set 才能保证正确性的 operation,在当前 Base 能力下标记为不支持,不使用多次远程请求模拟。
|
|
213
|
+
|
|
214
|
+
无依赖的多路写入可以采用:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const writes = await Promise.allSettled([
|
|
218
|
+
updatePrimaryRecord(context, input),
|
|
219
|
+
createAuditRecord(context, input),
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
return summarizeWrites(writes)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
性能敏感的 operation 记录总耗时和各远程调用耗时;只使用成功业务请求评价写入性能,鉴权失败、参数失败和平台凭证缺失只用于诊断。日志和返回值不得包含 token、cookie 或请求凭证。
|
|
226
|
+
|
|
227
|
+
### 下游错误归一化
|
|
228
|
+
|
|
229
|
+
FaaS 调用 Base OpenAPI 或其他应用 Function 时,把当前 operation 的业务结果与下游诊断分开:
|
|
230
|
+
|
|
231
|
+
- 顶层 `code` 和 `msg` 表达稳定、面向调用方的业务语义,不直接使用上游错误码,也不把技术细节拼进 `msg`。
|
|
232
|
+
- 成功或部分成功的业务结果保留在 `data`;仅在失败或部分成功时,把失败步骤放入 `data.errors`。
|
|
233
|
+
- 上游只返回错误摘要时原样保留经清理的摘要和 LogID,不猜测或拼造不存在的校验详情。
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
type DownstreamErrorKind =
|
|
237
|
+
| 'openapi'
|
|
238
|
+
| 'function'
|
|
239
|
+
| 'transport'
|
|
240
|
+
| 'invalid_response'
|
|
241
|
+
|
|
242
|
+
interface DownstreamError {
|
|
243
|
+
source: 'base_openapi' | 'site_function'
|
|
244
|
+
step: string
|
|
245
|
+
kind: DownstreamErrorKind
|
|
246
|
+
operation: string
|
|
247
|
+
httpStatus?: number
|
|
248
|
+
code?: number
|
|
249
|
+
msg: string
|
|
250
|
+
logId?: string
|
|
251
|
+
retryable: boolean
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
type DownstreamOutcome<T> =
|
|
255
|
+
| { ok: true; value: T }
|
|
256
|
+
| { ok: false; error: DownstreamError }
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
下游 helper 捕获预期失败并返回 `DownstreamOutcome<T>`,业务 handler 再汇总各步骤;代码自身的非预期异常仍由 handler 最外层捕获并返回通用错误,不向页面泄漏原始异常或堆栈。分类规则固定为:
|
|
260
|
+
|
|
261
|
+
- Base OpenAPI 返回合法 JSON 且含非零数值 `code`:`openapi`;
|
|
262
|
+
- 应用 Function 返回合法 envelope 且含非零数值 `code`:`function`;
|
|
263
|
+
- `fetch` reject、超时、连接中断等未取得合法响应:`transport`;
|
|
264
|
+
- 非 JSON、缺少约定字段,或成功响应缺少预期结果:`invalid_response`。
|
|
265
|
+
|
|
266
|
+
错误对象只允许返回诊断所需的白名单字段。`msg` 取上游安全摘要,去除首尾空白并限制在 300 字符内;`logId` 优先取响应 `x-tt-logid`,可回退 `x-request-id`,限制在 128 字符内。不得返回 URL、请求 body、完整响应、完整 headers、stack、token、cookie 或其他凭证。对于 `context.invokeFunction`,只从其合法返回 envelope 中提取同等字段。
|
|
267
|
+
|
|
268
|
+
`retryable` 同时取决于错误和写入语义,不能只看 HTTP 状态。明确幂等的固定字段更新或删除遇到 transport、408、429 或 5xx 时可以标记为可重试;非幂等创建出现 transport 或结果未知时必须为 `false`,除非业务已有稳定幂等键和去重机制。页面不得仅因为 `retryable: true` 自动重提,仍按 operation 契约决定交互。
|
|
269
|
+
|
|
270
|
+
多路写入的顶层业务码至少区分输入无效、部分成功、下游全部失败和内部异常,并在同一 operation 内保持稳定。全部成功时省略 `errors`;部分成功时返回已完成结果、失败步骤和明确的后续动作,避免用户重复提交已经成功的非幂等写入。
|
|
271
|
+
|
|
272
|
+
应用调用网关在 handler 返回前产生的 504、运行时崩溃或加载失败无法被上述结构捕获。排查这类错误时保留 invoke HTTP 响应自身的 `x-tt-logid`,不要伪装成某次 OpenAPI 调用错误。
|
|
273
|
+
|
|
274
|
+
### 写入行(记录)
|
|
275
|
+
|
|
276
|
+
FaaS 使用 `context.fetch` 调用固定的批量写入路由;只使用线上 origin:
|
|
277
|
+
|
|
278
|
+
| 目标 | Origin |
|
|
279
|
+
|---|---|
|
|
280
|
+
| Online | `https://open.feishu.cn` |
|
|
281
|
+
|
|
282
|
+
#### 应用记录 写入协议
|
|
283
|
+
|
|
284
|
+
FaaS 只使用以下五个固定路由,均为 `POST`,请求头为
|
|
285
|
+
`Content-Type: application/json; charset=utf-8`:
|
|
286
|
+
|
|
287
|
+
```http
|
|
288
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/records/batch_create
|
|
289
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/records/batch_update
|
|
290
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/records/batch_delete
|
|
291
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/append_attachments
|
|
292
|
+
POST /open-apis/bitable/v1/apps/:base_token/tables/:table_id/remove_attachments
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
后两条是附件字段专用的增量路由(注意不在 `records/` 段下):请求只提交本次增删的
|
|
296
|
+
`file_token`,服务端读取、合并后整格提交,保留读到的其它附件及宽高;不保证原子性或去重。目标环境必须已部署对应 FaaS runtime 和服务端能力,不能仅凭 Skill 存在就判定可用。什么时候必须用它们、什么时候仍可整格写,
|
|
297
|
+
见[文件传输](file-transfer.md)的「写单元格」。
|
|
298
|
+
|
|
299
|
+
路径首个参数是 **Base token,不是应用 token**:取 `catalog.json` 中该表的
|
|
300
|
+
`tables[].baseToken`,与 table ID 一起作为 Function 内的工程常量。应用 token 只用于
|
|
301
|
+
`context.invokeFunction` 的 `projectId`,两者不要混用。
|
|
302
|
+
|
|
303
|
+
请求体必须严格使用对应结构,不混用普通 Base OpenAPI、MCP 或其他 batch 接口的字段:
|
|
304
|
+
|
|
305
|
+
| operation | 请求体 | 成功时 `data.record_id_list` 的语义 |
|
|
306
|
+
|---|---|---|
|
|
307
|
+
| `batch_create` | `{"create_records":[{字段名: CellValue}, ...]}` | 与 `create_records` 按位置一一对应的新记录 ID |
|
|
308
|
+
| `batch_update` | `{"update_records":{"rec_xxx":{字段名: CellValue}, ...}}` | 实际存在并处理的记录 ID;不存在的 ID 放入 `record_not_found` |
|
|
309
|
+
| `batch_delete` | `{"record_id_list":["rec_xxx", ...]}` | 原样回显请求 ID;不存在、因此未删除的 ID 另放入 `record_not_found` |
|
|
310
|
+
| `append_attachments` | `{"attachments":{"rec_xxx":{"字段名或字段ID":[{"file_token":"...","image_width":640,"image_height":480}]}}}` | 无 `record_id_list`,见下方附件增量响应 |
|
|
311
|
+
| `remove_attachments` | 同 `append_attachments`,数组项只需 `file_token` | 同上 |
|
|
312
|
+
|
|
313
|
+
五个请求体都是 strict JSON Schema:不接受表格中未列出的额外顶层字段。「每批 1~200 条
|
|
314
|
+
记录」只适用于前三条 batch 路由;附件增量路由的限制是**一次操作的 `file_token` 总数不超过
|
|
315
|
+
50**;附件映射及每个 record-field 数组没有非空 Schema 约束。record ID 必须匹配
|
|
316
|
+
`^rec[A-Za-z0-9]{1,11}$`;删除列表不允许重复,
|
|
317
|
+
更新映射的 key 自然唯一。`batch_update` 使用 `update_records` 为不同记录分别提交
|
|
318
|
+
patch;不要混用 MCP 工具层 `patch_records_batch` 的 `record_id_list + patch` 请求结构。
|
|
319
|
+
|
|
320
|
+
**附件非空检查由调用方负责**:Page/FaaS 先过滤空字段数组、再移除空记录映射,全部为空时
|
|
321
|
+
本地短路,不发送请求。不要依赖平台拒绝空输入:合法目标上的 `append []` 仍会读取并写回
|
|
322
|
+
已有列表,`remove []` 不产生实际变更并可返回成功。图片宽高须由 Page 从本地 `File` 解码
|
|
323
|
+
取得,不能依赖上传返回值中的可选宽高;非图片与解码失败的处理见[文件传输](file-transfer.md)。
|
|
324
|
+
|
|
325
|
+
所有写入的成功 HTTP body 都只有一层 OpenAPI envelope(batch 路由示例如下):
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"code": 0,
|
|
330
|
+
"msg": "",
|
|
331
|
+
"data": {
|
|
332
|
+
"record_id_list": ["rec_xxx"],
|
|
333
|
+
"record_not_found": ["rec_missing"]
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Node MCP 直接响应中的 `meta` 不会经过当前 FaaS Entry 的合成响应返回,FaaS 不得依赖该字段。
|
|
339
|
+
|
|
340
|
+
`batch_create` 和 `batch_update` 的 `data` 还可能包含被忽略的只读或不支持字段;未忽略任何字段时
|
|
341
|
+
`ignored_fields` 不存在:
|
|
342
|
+
|
|
343
|
+
```json
|
|
344
|
+
{
|
|
345
|
+
"record_id_list": ["rec_xxx"],
|
|
346
|
+
"ignored_fields": [
|
|
347
|
+
{
|
|
348
|
+
"id": "fld_xxx",
|
|
349
|
+
"name": "创建时间",
|
|
350
|
+
"reason": "READONLY: created_at field is auto-created by the system."
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
附件增量路由的成功响应没有 `record_id_list`:`data.attachments` 按记录、字段回显
|
|
357
|
+
**本次操作合并出的附件列表**(`file_token` / `name` / `size`),页面用它更新本地状态;
|
|
358
|
+
它不是包含全部并发写入的最终快照,不要立即用 SQL 回读替换它(从库可能尚未同步)。超时或结果未知时保留已上传 token,按[文件传输](file-transfer.md#超时与结果未知)有界核对;append 不幂等,不能自动重提或重新上传。字段设置了「仅限手机拍摄」时,
|
|
359
|
+
`append_attachments` 对该字段的追加被忽略并进入 `ignored_fields`;`remove_attachments`
|
|
360
|
+
不受该限制,仍可移除。校验纪律同 batch:只认约定字段,不递归找。
|
|
361
|
+
|
|
362
|
+
runtime 识别后返回的是合成响应,因此这条链路上:
|
|
363
|
+
|
|
364
|
+
- HTTP 状态恒为 `200`,业务成败只看 body 的 `code`,不能只判 `response.ok`;
|
|
365
|
+
- 响应头没有 `x-tt-logid`,`DownstreamError.logId` 必然为空。排查改用 invoke 响应
|
|
366
|
+
自身的 LogID,见[下游错误归一化](#下游错误归一化)。
|
|
367
|
+
|
|
368
|
+
**三条 batch 路由**的成功响应固定读取 `body.data.record_id_list`,未找到的记录固定读取
|
|
369
|
+
`body.data.record_not_found`;**附件增量路由**的成功响应固定读取 `body.data.attachments`,
|
|
370
|
+
两类不得混用同一解析器。不得递归遍历整个响应寻找字符串,不得猜测或兼容 `record_id`、
|
|
371
|
+
`recordId`、`records[].id` 等未约定结构,也不得把 `data.record_id_list` 当作 handler 自定义返回的
|
|
372
|
+
`recordId`。三种 batch operation 分别校验:
|
|
373
|
+
|
|
374
|
+
- `batch_create`:`record_id_list.length` 必须与 `create_records.length` 一致,且不得返回 `record_not_found`。
|
|
375
|
+
- `batch_update`:`record_id_list` 是实际存在的记录,允许为空;它与 `record_not_found` 必须是请求 key 的不重叠、无遗漏分组。
|
|
376
|
+
- `batch_delete`:`record_id_list` 必须与请求列表一致,`record_not_found` 必须是其子集;实际删除的 ID 是两者的差集。
|
|
377
|
+
|
|
378
|
+
`code === 0` 且 `record_not_found` 非空是 Node MCP batch 的合法部分成功,不是 `invalid_response`。FaaS handler 必须在
|
|
379
|
+
业务结果中保留已完成和未找到的 ID;需要向 Page 表达部分成功时,使用当前 operation 的稳定业务码,
|
|
380
|
+
但不得伪装成下游失败或整体重试。只有响应缺失、类型不符或不满足上述 operation 约束时才按
|
|
381
|
+
`invalid_response` 处理;非幂等创建不得自动重试。
|
|
382
|
+
|
|
383
|
+
可复用的严格解析器:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
interface IgnoredRecordField {
|
|
387
|
+
id: string
|
|
388
|
+
name: string
|
|
389
|
+
reason: string
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
interface SiteRecordsWriteData {
|
|
393
|
+
record_id_list: string[]
|
|
394
|
+
record_not_found?: string[]
|
|
395
|
+
ignored_fields?: IgnoredRecordField[]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
interface SiteRecordsWriteEnvelope {
|
|
399
|
+
code: number
|
|
400
|
+
msg: string
|
|
401
|
+
data: SiteRecordsWriteData
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function parseDatabaseRecordsWriteData(
|
|
405
|
+
value: unknown,
|
|
406
|
+
): SiteRecordsWriteData | undefined {
|
|
407
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
408
|
+
const envelope = value as Record<string, unknown>
|
|
409
|
+
if (envelope.code !== 0) return undefined
|
|
410
|
+
if (typeof envelope.data !== 'object' || envelope.data === null) {
|
|
411
|
+
return undefined
|
|
412
|
+
}
|
|
413
|
+
const data = envelope.data as Record<string, unknown>
|
|
414
|
+
const recordIdPattern = /^rec[A-Za-z0-9]{1,11}$/
|
|
415
|
+
const parseRecordIDs = (input: unknown): string[] | undefined => {
|
|
416
|
+
if (
|
|
417
|
+
!Array.isArray(input) ||
|
|
418
|
+
input.length > 200 ||
|
|
419
|
+
input.some(
|
|
420
|
+
(recordId) =>
|
|
421
|
+
typeof recordId !== 'string' || !recordIdPattern.test(recordId),
|
|
422
|
+
) ||
|
|
423
|
+
new Set(input).size !== input.length
|
|
424
|
+
) {
|
|
425
|
+
return undefined
|
|
426
|
+
}
|
|
427
|
+
return input as string[]
|
|
428
|
+
}
|
|
429
|
+
const recordIDs = parseRecordIDs(data.record_id_list)
|
|
430
|
+
const recordNotFound =
|
|
431
|
+
data.record_not_found === undefined
|
|
432
|
+
? undefined
|
|
433
|
+
: parseRecordIDs(data.record_not_found)
|
|
434
|
+
if (
|
|
435
|
+
!recordIDs ||
|
|
436
|
+
(data.record_not_found !== undefined && !recordNotFound) ||
|
|
437
|
+
(recordIDs.length === 0 && (recordNotFound?.length ?? 0) === 0)
|
|
438
|
+
) {
|
|
439
|
+
return undefined
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
record_id_list: recordIDs,
|
|
443
|
+
...(recordNotFound ? { record_not_found: recordNotFound } : {}),
|
|
444
|
+
...(Array.isArray(data.ignored_fields)
|
|
445
|
+
? { ignored_fields: data.ignored_fields as IgnoredRecordField[] }
|
|
446
|
+
: {}),
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
该 helper 只负责成功 payload 的固定结构;HTTP 失败、非零业务码和错误摘要仍按
|
|
452
|
+
[下游错误归一化](#下游错误归一化)处理。
|
|
453
|
+
|
|
454
|
+
#### 创建实现示例
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
const OPENAPI_ORIGIN = 'https://open.feishu.cn'
|
|
458
|
+
const response = await context.fetch(
|
|
459
|
+
`${OPENAPI_ORIGIN}/open-apis/bitable/v1/apps/${encodeURIComponent(BASE_TOKEN)}` +
|
|
460
|
+
`/tables/${encodeURIComponent(TABLE_ID)}/records/batch_create`,
|
|
461
|
+
{
|
|
462
|
+
method: 'POST',
|
|
463
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
464
|
+
body: JSON.stringify({
|
|
465
|
+
create_records: [
|
|
466
|
+
{
|
|
467
|
+
任务名称: input.title,
|
|
468
|
+
状态: input.status,
|
|
469
|
+
截止时间: input.dueAt,
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
}),
|
|
473
|
+
},
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
const body: unknown = await response.json().catch(() => null)
|
|
477
|
+
|
|
478
|
+
if (
|
|
479
|
+
!response.ok ||
|
|
480
|
+
typeof body !== 'object' ||
|
|
481
|
+
body === null ||
|
|
482
|
+
!('code' in body) ||
|
|
483
|
+
body.code !== 0
|
|
484
|
+
) {
|
|
485
|
+
const logId =
|
|
486
|
+
response.headers.get('x-tt-logid') ??
|
|
487
|
+
response.headers.get('x-request-id') ??
|
|
488
|
+
undefined
|
|
489
|
+
const upstreamCode =
|
|
490
|
+
typeof body === 'object' &&
|
|
491
|
+
body !== null &&
|
|
492
|
+
'code' in body &&
|
|
493
|
+
typeof body.code === 'number'
|
|
494
|
+
? body.code
|
|
495
|
+
: undefined
|
|
496
|
+
const rawUpstreamMsg =
|
|
497
|
+
typeof body === 'object' &&
|
|
498
|
+
body !== null &&
|
|
499
|
+
'msg' in body &&
|
|
500
|
+
typeof body.msg === 'string'
|
|
501
|
+
? body.msg.trim().slice(0, 300)
|
|
502
|
+
: ''
|
|
503
|
+
const upstreamMsg =
|
|
504
|
+
rawUpstreamMsg || response.statusText || `OpenAPI HTTP ${response.status}`
|
|
505
|
+
const isOpenApiError =
|
|
506
|
+
typeof upstreamCode === 'number' && upstreamCode !== 0
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
data: {
|
|
510
|
+
created: false,
|
|
511
|
+
errors: [
|
|
512
|
+
{
|
|
513
|
+
source: 'base_openapi',
|
|
514
|
+
step: 'createTask',
|
|
515
|
+
kind: isOpenApiError ? 'openapi' : 'invalid_response',
|
|
516
|
+
operation: 'batch_create',
|
|
517
|
+
httpStatus: response.status,
|
|
518
|
+
...(typeof upstreamCode === 'number'
|
|
519
|
+
? { code: upstreamCode }
|
|
520
|
+
: {}),
|
|
521
|
+
msg: upstreamMsg,
|
|
522
|
+
...(logId ? { logId: logId.trim().slice(0, 128) } : {}),
|
|
523
|
+
retryable: false,
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
},
|
|
527
|
+
code: 50201,
|
|
528
|
+
msg: '创建任务失败',
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const created = parseDatabaseRecordsWriteData(body)
|
|
533
|
+
if (
|
|
534
|
+
!created ||
|
|
535
|
+
created.record_id_list.length !== 1 ||
|
|
536
|
+
(created.record_not_found?.length ?? 0) > 0
|
|
537
|
+
) {
|
|
538
|
+
return {
|
|
539
|
+
data: {
|
|
540
|
+
created: true,
|
|
541
|
+
errors: [
|
|
542
|
+
{
|
|
543
|
+
source: 'base_openapi',
|
|
544
|
+
step: 'createTask',
|
|
545
|
+
kind: 'invalid_response',
|
|
546
|
+
operation: 'batch_create',
|
|
547
|
+
msg: '创建成功但响应中的记录 ID 数量不符合请求',
|
|
548
|
+
retryable: false,
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
code: 50202,
|
|
553
|
+
msg: '任务已创建,但无法确认记录 ID',
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const [recordId] = created.record_id_list
|
|
558
|
+
return { data: { created: true, recordId }, code: 0, msg: 'success' }
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
FaaS 写入 Base 时,必须调用目标环境的
|
|
562
|
+
`/open-apis/bitable/v1/apps/:base_token/tables/:table_id/records/batch_*`(记录批量写)或
|
|
563
|
+
`.../append_attachments`、`.../remove_attachments`(附件增量)路由;
|
|
564
|
+
`:base_token` 取 `catalog.json` 的 `tables[].baseToken`,并检查 HTTP 状态和业务码。
|
|
565
|
+
|
|
566
|
+
需要读 Base 时调用固定 SQL Function:
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
const result: unknown = await context.invokeFunction({
|
|
570
|
+
projectId: PROJECT_ID,
|
|
571
|
+
functionId: 'findTaskById',
|
|
572
|
+
params: [input.recordId],
|
|
573
|
+
})
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
确需写前查重、权限范围、状态检查或写后查询时复用固定 SQL Function;可以在一次查询中完成的检查不拆成多个串行调用。前一个 SQL 返回的关联 ID 仅用于定位后一个 SQL 的记录时,优先把关联查询下推为一个 SQL Function。
|
|
577
|
+
|
|
578
|
+
## Base 读写协议
|
|
579
|
+
|
|
580
|
+
同一个字段在创建 schema、SQL 读取和 OpenAPI 写入时使用三套不同但相关的数据结构:
|
|
581
|
+
|
|
582
|
+
| Field JSON | SQL 读取 | OpenAPI CellValue |
|
|
583
|
+
|---|---|---|
|
|
584
|
+
| `text` | `TEXT` string | string |
|
|
585
|
+
| `number` | `DOUBLE` number | finite number |
|
|
586
|
+
| 单选 `select` | `OPTION` string | 合法选项名 string |
|
|
587
|
+
| 多选 `select` | `COLLECTION<OPTION>` | 合法选项名 string[] |
|
|
588
|
+
| `datetime` | `TIMESTAMP` 毫秒 number | `YYYY-MM-DD HH:mm:ss` |
|
|
589
|
+
| `checkbox` | `BOOL` boolean | boolean |
|
|
590
|
+
| 单/多 `user` | `USER` / `COLLECTION<USER>` | `[{"id":"6882577547121393684"}]`;`id` 是内部 `user_id` |
|
|
591
|
+
| 单/多 `group_chat` | `GROUP` / `COLLECTION<GROUP>` | `[{"id":"oc_xxx"}]` |
|
|
592
|
+
| `link` | `COLLECTION<RECORD_ID>` | `[{"id":"rec_xxx"}]` |
|
|
593
|
+
| `attachment` | `COLLECTION<FILE>` | `[{"file_token":"...","image_width":number,"image_height":number}]`;追加/移除走附件增量路由,`batch_update` 写该字段是整格覆盖、会丢已有图片宽高,仅用于置空或明确重设。见[文件传输](file-transfer.md) |
|
|
594
|
+
| `location` | `UNSUPPORTED` | `{"lng":number,"lat":number}` |
|
|
595
|
+
| 公式、lookup、系统字段 | 推导类型或 `UNSUPPORTED` | 只读 |
|
|
596
|
+
|
|
597
|
+
写入时 `null` 清空字段,`[]` 表示空集合,省略字段表示不修改。选项名、用户/群 ID 和关联 record ID 必须来自真实数据。
|
|
598
|
+
|
|
599
|
+
应用行写入只使用 [应用记录 写入协议](#应用记录写入协议)中的固定路由。
|
|
600
|
+
|
|
601
|
+
创建一行或多行时,`create_records` 中每一项是字段到 cell value 的映射,不同记录可以填写不同字段:
|
|
602
|
+
|
|
603
|
+
```json
|
|
604
|
+
{
|
|
605
|
+
"create_records": [
|
|
606
|
+
{
|
|
607
|
+
"任务名称": "客户回访",
|
|
608
|
+
"状态": "待处理",
|
|
609
|
+
"负责人": [{ "id": "6882577547121393684" }]
|
|
610
|
+
}
|
|
611
|
+
]
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
更新时把以 record ID 为 key 的字段 patch 放入 `update_records`:
|
|
616
|
+
|
|
617
|
+
```json
|
|
618
|
+
{
|
|
619
|
+
"update_records": {
|
|
620
|
+
"rec_xxx": { "状态": "已完成" },
|
|
621
|
+
"rec_yyy": { "状态": "进行中", "备注": "处理中" }
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
删除时传入 record ID 列表:
|
|
627
|
+
|
|
628
|
+
```json
|
|
629
|
+
{
|
|
630
|
+
"record_id_list": ["rec_xxx", "rec_yyy"]
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
检查 HTTP 状态和外层业务码后,还必须按固定成功响应协议校验:batch 路由校验
|
|
635
|
+
`data.record_id_list`,附件增量路由校验 `data.attachments`;需要后续写入、关联或回读时,
|
|
636
|
+
只使用约定字段中的记录 ID。
|
|
637
|
+
|
|
638
|
+
batch 路由单批最多 200 条。多选、多人员、关联这些集合字段是**整格覆盖**:追加或移除必须先读现有值、合并出完整列表再整格写回,直接写会丢掉单元格里已有的项。**附件字段是唯一的例外**:追加/移除走上面的附件增量路由,禁止用读-合并-整格写回实现——服务端不会补齐省略的宽高,而 SQL 读不回旧图宽高,基于 SQL 列表的重写无法重建宽高;并发时整格写回还会互相覆盖。见[文件传输](file-transfer.md)的「写单元格」。
|
|
639
|
+
|
|
640
|
+
**整格覆盖的合并基准必须是可信的完整列表。** SQL 回读走从库,刚写完就回读可能拿到写入前的旧值;拿缺项的列表做基准整格写回,会静默删掉其它项,而写入仍返回成功。读取失败、从未读到过、或回读缺少本次刚写入的项时,禁掉入口让用户稍后重试,不要写。
|
|
641
|
+
|
|
642
|
+
文件字节的上传与展示是另外的路,见[文件传输](file-transfer.md)。
|