@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,275 @@
|
|
|
1
|
+
# 文件传输
|
|
2
|
+
|
|
3
|
+
本文负责文件字节在页面、父页面与 drive 之间的传输:附件字段的值只是指向 drive 的指针,字节走的是一条独立通道,与 Page / SQL Function / FaaS 三条边界都不重合。
|
|
4
|
+
|
|
5
|
+
与普通字段不同,附件的追加与移除只向专用增量路由提交本次增删项(见「写单元格」),不由页面合并旧快照后整格覆盖。服务端内部仍是读取、合并后整格提交,不保证原子性。CellValue 形态见[代码开发手册](development.md#base-读写协议),`FILE` 子字段的读取见 [SQL Function 专项](sql-function.md)。需求涉及上传、展示、下载或删除附件时完整读取本文。
|
|
6
|
+
|
|
7
|
+
**前提**:上传依赖父页面的 `attachment.upload` 协议方法;单元格增删还依赖目标环境的 FaaS runtime 与服务端已支持下文的附件路由。上传或增删能力未开通时按平台限制处理,经用户同意回退图片 URL 字段,不要手写其他网关路径试探、反复重试或回退成读旧列表后 `batch_update`。展示与下载复用既有端点,也须确认目标环境的父页面、鉴权和下载配置可用;更新 starter 不代表服务端已发布。
|
|
8
|
+
|
|
9
|
+
判定依据错误码,不依据错误文案:文案会变,`code` 是契约。
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const code =
|
|
13
|
+
typeof error === 'object' && error !== null
|
|
14
|
+
? Reflect.get(error, 'code')
|
|
15
|
+
: undefined
|
|
16
|
+
if (code === 'ATTACHMENT_UNAVAILABLE') { /* 走降级 */ }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 四条独立路径
|
|
20
|
+
|
|
21
|
+
**文本、选项、人员、日期、关联这些字段 SQL 查出来即可直接渲染;附件 SQL 返回的是凭据,不是内容。**
|
|
22
|
+
|
|
23
|
+
| 操作 | 入口 | 约束来源 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| 上传文件 | `siteRuntime.uploadAttachment()` | 页面的 HTTP 代理只收字符串,二进制无法通过;身份 header 须由父页面注入 |
|
|
26
|
+
| 展示缩略图 / 预览 | `buildAttachmentCoverUrl(context, ref)` → `<img src>` | `file_token` 不是 URL;模板拼出的地址浏览器可直接取 |
|
|
27
|
+
| 下载原文件 | `fetchAttachmentDownloadUrl(context, ref)` → `<a download>` | 模板经 `temporary_download_url` 换回明文自证链接,由浏览器原生下载 |
|
|
28
|
+
| 追加 / 移除附件 | 附件增量路由 `append_attachments` / `remove_attachments` | 请求只带本次增删 token;服务端保留读到的其它附件及宽高,但仍有并发覆盖窗口,见「写单元格」 |
|
|
29
|
+
|
|
30
|
+
上传流程在 Page 里封装成一个业务方法;展示与下载直接复用 starter helper。业务组件只调这些收口入口,不在多处重复编排。
|
|
31
|
+
|
|
32
|
+
## 上传
|
|
33
|
+
|
|
34
|
+
字节由父页面代传,因此文件不能用 `fetch` 直接上传,只能调 `uploadAttachment`;父页面会校验 `(baseToken, tableId)` 属于本应用绑定。
|
|
35
|
+
|
|
36
|
+
**上传校验的是应用权限,不需要底表任何权限。** 文件挂在底表上,但鉴权对象是应用。
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
await siteRuntime.uploadAttachment({ baseToken, tableId, file, onProgress })
|
|
40
|
+
// → { fileToken, name, size, mimeType? }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`baseToken` 取自 `catalog.json` 的 `tables[].baseToken`。`file` 必须是 `File`,不接受 `Blob`。`onProgress` 接收 `{ loaded, total }`。
|
|
44
|
+
|
|
45
|
+
**不传 `fieldId`**:上传把文件挂在底表上,字段只在下一步写单元格时才有意义。
|
|
46
|
+
|
|
47
|
+
**返回的 `name` / `size` / `mimeType` 是客户端自报值**,上传 SDK 的成功回调不带服务端 fileMeta。持久化的权威值以 SQL 读回的为准(见下方 SQL 章节)。
|
|
48
|
+
|
|
49
|
+
### 图片宽高在上传前从本地文件读取
|
|
50
|
+
|
|
51
|
+
当前父页面不返回图片宽高;Channel 类型中的可选 `width` / `height` 不代表运行时会提供。
|
|
52
|
+
Page 应在上传前解码本地 `File`,读取 `naturalWidth` / `naturalHeight`,随后和上传得到的
|
|
53
|
+
`fileToken` 一起保留,用于挂载请求。不要用预览元素的 CSS 尺寸或缩略图档位代替原图尺寸。
|
|
54
|
+
|
|
55
|
+
下面的业务 helper 在 Page 内使用,非图片不传宽高;图片解码失败或尺寸无效时抛错,由上传入口
|
|
56
|
+
提示用户并停止本次上传、挂载,不伪造 `0` 或默认尺寸。`file.type` 为空时先识别文件类型,
|
|
57
|
+
不要把可能的图片直接当作非图片跳过尺寸提取。
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
async function readImageDimensions(file: File) {
|
|
61
|
+
if (!file.type) throw new Error('无法识别文件类型,请确认文件格式')
|
|
62
|
+
if (!file.type.startsWith('image/')) return undefined
|
|
63
|
+
const url = URL.createObjectURL(file)
|
|
64
|
+
try {
|
|
65
|
+
const image = new Image()
|
|
66
|
+
image.src = url
|
|
67
|
+
await image.decode()
|
|
68
|
+
const width = image.naturalWidth
|
|
69
|
+
const height = image.naturalHeight
|
|
70
|
+
if (width <= 0 || height <= 0) throw new Error('无法读取图片尺寸')
|
|
71
|
+
return { image_width: width, image_height: height }
|
|
72
|
+
} finally {
|
|
73
|
+
URL.revokeObjectURL(url)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dimensions = await readImageDimensions(file)
|
|
78
|
+
const uploaded = await siteRuntime.uploadAttachment({ baseToken, tableId, file, onProgress })
|
|
79
|
+
const newAttachment = { file_token: uploaded.fileToken, ...dimensions }
|
|
80
|
+
// 将 newAttachment 放入本次 append_attachments 请求,不合并页面旧附件列表。
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 上传后的即时展示
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const previewUrl = URL.createObjectURL(file) // 组件卸载时 URL.revokeObjectURL(previewUrl)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**刚上传的文件不适用下面的「展示与下载」**,有两个原因叠加:服务端要核对附件位置,而它读的是从库,单元格刚写完可能还没同步到;就算位置对上了,**封面本身也是异步生成的**,刚传完大概率还没有。表现为图片不显示、刷新后恢复正常,没有任何报错。
|
|
90
|
+
|
|
91
|
+
本地 blob 没有这个问题:文件在本地,零请求。等用户下次打开页面时再走远程地址,那时两件事都已完成。
|
|
92
|
+
|
|
93
|
+
| 触发条件 | 错误码 | 该怎么处理 |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| 超过单文件上限 / 大小为 0 / 不是 `File` | `INVALID_ATTACHMENT_TARGET` | 说清是文件本身的问题,重试无用;最好在选完文件后就先拦一次 |
|
|
96
|
+
| `(baseToken, tableId)` 不在本应用绑定内 | `INVALID_ATTACHMENT_TARGET` | 应用开发配置错误,不是用户能解决的,按异常上报 |
|
|
97
|
+
| 单元格已有 100 张 | 写单元格时返回参数错误 | 上传前按当前张数拦住,不要等写入失败 |
|
|
98
|
+
| 父页面版本不支持 | `ATTACHMENT_UNAVAILABLE` | 用它做能力探测与降级 |
|
|
99
|
+
| 上传失败 | `ATTACHMENT_UPLOAD_FAILED`,原始 message 保留 | 可以重试,但**不要自动重试**(见注意事项,上传不幂等) |
|
|
100
|
+
| 首条进度之后,连续 120s 没有新进度 | `ATTACHMENT_UPLOAD_STALLED` | 已判定卡死并中止,提示网络问题并让用户重新触发 |
|
|
101
|
+
|
|
102
|
+
**每个码都要有区分度的提示。** 全都归到「请稍后重试」会让文件超限、单元格已满这两类永远重试不成功的情况看起来像偶发故障。
|
|
103
|
+
|
|
104
|
+
### 上传进度
|
|
105
|
+
|
|
106
|
+
上传耗时不确定,单文件上限 2 GB,因此**要让用户看到进展**,不要只给一个静态的「上传中」。`onProgress` 提供了这个能力,展示形式不限——百分比文字、进度条、已传分片数都可以,按页面风格选。
|
|
107
|
+
|
|
108
|
+
`onProgress` 的回调节奏与常见的字节流进度不同,直接按平滑增长做动画会出问题:
|
|
109
|
+
|
|
110
|
+
- **按分片跳变。** 40 MiB 的文件只回调三次,5 MiB 直接 0% → 100%,与文件体积无关。
|
|
111
|
+
- **100% 不代表结束**,之后还有约 3 秒收尾,完成信号只有 Promise resolve。
|
|
112
|
+
- **可能一次都不触发**:旧版本父页面不回播进度,初始态要自带可用值。
|
|
113
|
+
- **停滞检测从首条进度事件之后才生效**:首条进度到达前卡死不会返回 `STALLED`,只有约 2 小时的调用上限兜底,且超时不通知父页面取消。需要更紧的首进度兜底时页面自行计时并提示用户,不要替用户自动取消或重试。
|
|
114
|
+
- 百分比的分母取回调给出的 `total`,不取 `file.size`。
|
|
115
|
+
- 并发上传互不干扰。
|
|
116
|
+
|
|
117
|
+
## 展示与下载
|
|
118
|
+
|
|
119
|
+
`file_token` 不是 URL。**缩略图和下载走两条不同的路**,字节都不进 JS:
|
|
120
|
+
|
|
121
|
+
| 用途 | 端点 | 页面用法 |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| 缩略图 / 预览 | `GET box/stream/download/v2/cover/<file_token>` | 拼出 URL 直接给 `<img src>`,由浏览器请求 |
|
|
124
|
+
| 下载原文件 | `POST box/file/temporary_download_url` | 换回自证链接给 `<a download>` |
|
|
125
|
+
|
|
126
|
+
两条都要带**位置声明** `extra`,由 starter 的两个 helper 在 Page 本地拼出。Page 从 SQL Function 取得附件坐标,再调用这两个 helper;不需要独立的应用附件查询接口,也不需要在 append/remove 前先查询附件列表。
|
|
127
|
+
|
|
128
|
+
### 第一步 · 位置声明 `extra`
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const context = await resolveDatabaseContext()
|
|
132
|
+
const ref = { tableId, fieldId, recordId, fileToken }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`tableId` / `fieldId` 取自 `catalog.json`,`recordId` / `fileToken` 取自 SQL 结果。两个 helper 用这四个值加应用 token 拼出位置声明。
|
|
136
|
+
|
|
137
|
+
它是声明不是凭证:服务端据此核对「这个 token 是否真的挂在这个字段的这条记录上」,**四个值必须如实填**,位置对不上会被拒,且不是抛错,是拿不到内容。
|
|
138
|
+
|
|
139
|
+
#### `baseAppToken` 决定鉴权对象
|
|
140
|
+
|
|
141
|
+
它决定服务端**按哪个对象判权**:带上就按应用判,不带就退化成「访问者对底表有没有权限」。而应用访客按设计**不该有底表权限**(记录读写是应用 owner 代执行的),所以缺了它,访客一律读不到。starter helper 会带上它。
|
|
142
|
+
|
|
143
|
+
用一个「对应用有权限、对其绑定底表无权限(直接读记录报 `91403`)」的真实身份实测:
|
|
144
|
+
|
|
145
|
+
| 操作 | 带 `baseAppToken` | 不带 |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| 缩略图 `cover` | **200 image/png** | **403** |
|
|
148
|
+
| 下载 `temporary_download_url` | 200 + 链接 | `{"code":4,"message":"Forbidden"}` |
|
|
149
|
+
|
|
150
|
+
### 第二步 A · 缩略图
|
|
151
|
+
|
|
152
|
+
不需要换链接、取字节或包装 Blob,拼出地址赋给 `src` 即可:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const src = buildAttachmentCoverUrl(context, ref, 360)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
尺寸向上取到服务端档位 120 / 360 / 720 / 1280,不传时默认 360。
|
|
159
|
+
|
|
160
|
+
**档位也是「点击查看大图」的实现方式**:列表用 360,点击后换 1280 重拼一次地址,不必换链接或取字节。原文件常远大于 1280 档,展示场景不要走下载端点。
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
{src ? <img src={src} loading="lazy" onError={retryLater} /> : null}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
两件事要知道:
|
|
167
|
+
|
|
168
|
+
- **返回的是绝对 URL。** 页面跑在产物域,相对路径会落到产物域而不是业务网关。网关 origin 由模板推断:先读 `location.ancestorOrigins[0]`(Chromium 精确),再读 `document.referrer`(父页面的 referrer policy 保证跨源时只给到 origin)。都拿不到时 helper 返回空字符串,不猜域名。此时缩略图跳过渲染即可,但**下载这类动作入口不能静默跳过**——按钮要么禁用并说明原因,要么点击后给出提示,不要点了没反应。
|
|
169
|
+
- **封面是服务端异步生成的。** 取不到时返回 `202`(`{"code":90002006,"message":"Cover No Exist"}`),`<img>` 表现为 `onError`。视频尤其慢:35 MB 的录屏在 20 分钟内持续返回 202,约 2 小时后才生成。**202 不表示该格式不受支持**,要留重试(间隔数十秒,更换 cache-bust 参数后重新设置 `src`),重试成本很低,因为字节根本不进 JS。
|
|
170
|
+
|
|
171
|
+
覆盖面实测:png / jpg ✅、**pdf ✅**(回首页 PNG)、**mp4 ✅**(回截帧 JPEG)、**mov ✅**(同上,生成更慢)。用 `dd` 造的填充文件一律 202,无法验证封面生成;测试前先用 `xxd` 确认文件头含 `ftyp`。
|
|
172
|
+
|
|
173
|
+
附带效果:`loading="lazy"` 生效(视口外不发请求)、可用浏览器 HTTP 缓存、不占用 JS 堆、无需管理 Blob 生命周期。
|
|
174
|
+
|
|
175
|
+
### 第二步 B · 下载原文件
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const url = await fetchAttachmentDownloadUrl(context, ref)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
拿到的 `url` 是**明文自证链接**(`.../stream/download/authcode/?code=...`,签名与有效期烧在 `code` 里,实测窗口 24h):
|
|
182
|
+
|
|
183
|
+
- **不依赖 cookie**,无登录态的上下文同样可取回完整原文件
|
|
184
|
+
- 直接交给 `<a href={url} download>` 让浏览器原生下载——有进度条、可续传、**字节不进 JS**,35 MB 的 `.mov` 实测正常
|
|
185
|
+
- 跨源时 `download` 属性的**值**被忽略,文件名由服务端 `content-disposition` 决定,支持中文
|
|
186
|
+
- 落盘要求父页面 iframe 的 sandbox 带 `allow-downloads`;缺了会被浏览器静默拦截,不抛错、无事件,只在 console 留一条 warning
|
|
187
|
+
|
|
188
|
+
**不要给这个端点传 `width` / `height`**,它只签原文件,尺寸参数会被忽略(实测传与不传的字节数完全相同)。要缩略图走上面的 `cover`。
|
|
189
|
+
|
|
190
|
+
这个 URL 也可直接作为 `<img>` 的 `src`(实测可渲染),但只在用户明确要求原文件画质时如此使用。一般的「点击查看大图」走 `cover` 的 1280 档,见上一节。
|
|
191
|
+
|
|
192
|
+
### 链接时效
|
|
193
|
+
|
|
194
|
+
**不落库、不写进 SQL、不长期缓存。** 持久化与传递一律用 `file_token`,要展示时现换。页面停留很久后图片失效属于预期,重走第二步即可。
|
|
195
|
+
|
|
196
|
+
图多的列表页靠页面侧缓解(`loading="lazy"`、分页、先只显示文件名)。
|
|
197
|
+
|
|
198
|
+
## 写单元格
|
|
199
|
+
|
|
200
|
+
附件的追加与移除走**专用增量路由**:Page 调 FaaS Function,FaaS 打 `append_attachments` / `remove_attachments`(路由与请求体见[代码开发手册](development.md#应用记录写入协议)),不走 `batch_update` 整格覆盖。
|
|
201
|
+
|
|
202
|
+
- **追加只提交新增项** `[{file_token, image_width, image_height}]`,图片宽高在上传前从本地 `File` 解码取得(见上文),非图片省略宽高。Page 不需要读取或合并旧列表;服务端保留它读到的已有附件及宽高,再追加新项。该基准可能尚未包含另一请求刚写入的附件,不能据此承诺并发不丢失。
|
|
203
|
+
- **移除只提交待删的 `file_token`**,按 token 匹配并移除**全部**同名实例。SQL 读回的键名是 `id`,提交前要 `.map((s) => ({ file_token: s.id }))`。
|
|
204
|
+
- **无增删项时在调用方短路**:Page/FaaS 组装请求时过滤空字段数组和空记录映射,过滤后无项就不发请求、保留页面状态。平台 Schema 不保证拒绝空输入;合法目标上的 `append []` 仍会读后写回已有列表,不是无副作用操作。`remove []` 不产生实际变更,但也不必发送。
|
|
205
|
+
- **成功响应回显本次操作合并出的列表**,用它更新页面本地状态;它不保证包含其他并发写入,也不保证随后不会被另一请求覆盖。不要立即用 SQL 回读替换它,从库可能还没同步。
|
|
206
|
+
- **append 不按 file_token 去重,也没有幂等键**:重复提交可能产生重复引用。发送前去掉本次新增项中的重复 token;这只能防止本批重复,不能提供跨请求幂等。结果未知时按下一节核对,不自动重试。
|
|
207
|
+
- `batch_update` 对附件字段只用于**整格置空**(传 `[]`)或明确要整格重设的场景:它是整格覆盖,服务端不会补齐省略的宽高,而 SQL 读不回旧图宽高——基于 SQL 列表的重设无法重建宽高,旧图会显示成占位方块。
|
|
208
|
+
|
|
209
|
+
**并发边界要让业务方知情。** 此路径复用原生附件能力,消除 Page 长时间持有旧快照再整格覆盖的用法;没有实现记录层原子增量、版本冲突检测或幂等去重。服务端从读基线到提交之间仍有覆盖窗口,两个请求都返回成功也可能丢项。有限样本中低于 0.5 秒可复现覆盖,0.8 秒及以上未复现;这不是固定安全阈值,不得靠等待 0.8 秒宣称并发安全。要求多访问者同时修改同一单元格且绝不丢项的需求,当前能力不满足,须明确记录限制。
|
|
210
|
+
|
|
211
|
+
**页面侧串行化只针对同一个单元格**,目的是避免重复提交与本页操作交错,不承担跨访客的数据安全。不同记录、不同字段之间不冲突,上传本身也互不干扰。不要用一个全局标志锁住整个页面的上传入口——那会让其他行的入口不可用。
|
|
212
|
+
|
|
213
|
+
### 超时与结果未知
|
|
214
|
+
|
|
215
|
+
写入请求超时、连接中断或返回网关错误,不能据此判断没有副作用。页面应区分「上传中」「已上传、待挂载」「挂载结果待确认」,并保留已上传的 `fileToken` 和图片宽高。
|
|
216
|
+
|
|
217
|
+
1. 通过页面已有的 SQL Function 按记录读取附件 token,核对本次目标:追加项是否存在、移除项是否消失。回读失败或追加 token 暂未出现都不能证明上一请求没执行;读到的状态也不是跨并发请求的最终保证。
|
|
218
|
+
2. 允许延迟后做有界的只读核对。仍不能确认时保留待确认状态并提示用户,禁止盲目重提 append、拿旧列表整格覆盖或重新上传字节生成另一个 token。
|
|
219
|
+
3. 后续确需重做挂载时,沿用已经上传的 token 和宽高,只提交核对后需要补挂的项,并提示重复引用和并发覆盖风险;当前服务端不保证每次操作仅执行一次。移除按 token 匹配当前引用,但它与追加交错仍受同一覆盖窗口限制。
|
|
220
|
+
|
|
221
|
+
## SQL 侧的可读信息
|
|
222
|
+
|
|
223
|
+
SQL 只提供定位信息,不提供内容。`FILE` 子字段的可读范围与 `UNNEST` 用法见 [SQL Function 专项](sql-function.md)。
|
|
224
|
+
|
|
225
|
+
对附件来说要记住的是:SQL 给出的 `id`(即 `file_token`)与 `name`,加上 `record_id`,即 helper 需要的全部业务坐标;`url` 恒为 null,展示与下载一律走上面两节。
|
|
226
|
+
|
|
227
|
+
**读回的形状不能直接提交。** SQL 给的键名是 `id`,增量路由要的是 `file_token`——移除时必须先 `.map((s) => ({ file_token: s.id }))` 再提交;追加则完全不需要读旧列表(见「写单元格」)。若同一个数组还要用于更新页面本地状态,映射只加在提交那一处,本地状态继续用读回的形状——两者形状不同,共用一个变量就会有一侧出错。
|
|
228
|
+
|
|
229
|
+
## 限额
|
|
230
|
+
|
|
231
|
+
| 限额 | 值 | 说明 |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| 单文件大小 | 2 GB | **父页面当前的实现值**(与 lark-cli 对齐),由父页面在上传前拒绝,不是平台上限 |
|
|
234
|
+
| 单个单元格附件数 | 100 | |
|
|
235
|
+
| 单个 Base 附件总数 | 2000000 | 计数**只增不减**:删除只移除单元格引用,不删云盘文件 |
|
|
236
|
+
|
|
237
|
+
**只有单文件体积这一项会被拒绝,没有任何累计校验。** 传 100 个 19 MB 的文件全部放行。需要控制总量的业务自己在页面侧限制。
|
|
238
|
+
|
|
239
|
+
## 注意事项
|
|
240
|
+
|
|
241
|
+
- **`property.capture`(仅限手机拍摄)字段的写入会被服务端静默忽略**,降级进响应的 `ignored_fields`,表现为「传了没反应」。Page 必须检查 `ignored_fields` 并明确提示,不得判定为成功。该限制只约束写入:这类字段的附件仍可正常读取和删除。
|
|
242
|
+
- **`file_token` 不能跨 Base**。只能写进本应用绑定的那个 Base 的表。
|
|
243
|
+
- **图片宽高只来自调用方**。追加时传从本地 `File` 解码得到的 `image_width` / `image_height`,不要依赖上传返回值中的可选宽高;缺失会显示成占位方块。服务端会从文件元信息补 `name` / `size` / `mime_type`,但**不会补齐省略的宽高**,SQL 也读不回宽高——重写旧项时无从重建宽高,这正是追加/移除必须走增量路由的原因。读回口径:`name` 整列投影即可读到,`size` 须走 `UNNEST`,`mime_type` 与宽高 SQL 一律读不到。
|
|
244
|
+
- **上传成功不等于写入成功**。拿到 `fileToken` 只是文件进了云盘,还没进单元格;两步之间失败会留下孤儿文件。
|
|
245
|
+
- **`uploadAttachment` 不是幂等的**。同一个文件调两次会得到两个不同的 `fileToken` 和两份云盘文件。所以上传失败**不要自动重试**——除非能确认上一次确实没成功;无法判断时交给用户重新触发。文件越大中断概率越高,这条越要紧。
|
|
246
|
+
- **删除附件只移除单元格里的引用,不删云盘文件**,也不释放单 Base 的附件计数。三条路径(页面整格覆盖、官方 `remove_attachments`、CLI 的 remove)行为一致。不要向用户承诺「删除可释放空间」。
|
|
247
|
+
|
|
248
|
+
## 常见错误
|
|
249
|
+
|
|
250
|
+
| 现象 | 原因 |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `<img>` 加载不出来,src 是一串字母 | 把 `file_token` 当 URL 用了,要拼成 `cover` 地址 |
|
|
253
|
+
| `<img>` 请求打到产物域、404 | `cover` 地址用了相对路径,必须是指向业务网关的绝对 URL |
|
|
254
|
+
| **应用开发者本人正常,其他访客的封面全部加载失败** | `extra` 漏了 `baseAppToken`,鉴权退化成「访问者对底表有权限」,而访客不该有 |
|
|
255
|
+
| 缩略图 403 | `extra` 里 `fieldId`/`recordId`/`fileToken` 对不上真实位置(`tableId` 错是 400) |
|
|
256
|
+
| 视频/大文件一直没有封面 | 封面异步生成,大文件按小时;留 `onError` 重试,不应据此判定该格式不受支持 |
|
|
257
|
+
| 点击查看大图后仍然模糊 | 弹层复用了列表那个 360 档的 `src`,要用同一个 `ref` 重拼 1280 档 |
|
|
258
|
+
| 下载链接很快失效 | 把链接持久化了,改成点击时即时获取 |
|
|
259
|
+
| 点下载没反应,console 有 warning | 父页面 iframe sandbox 缺 `allow-downloads` |
|
|
260
|
+
| 点下载没反应,console 也没有输出 | 拿不到网关 origin 时静默 return 了,动作入口要给提示 |
|
|
261
|
+
| 一处在上传,其他行的上传按钮全点不动 | 串行化做成了全局标志,只需按单元格串行 |
|
|
262
|
+
| 追加后旧附件不见了 / 少了几张 | 追加走了 `batch_update` 整格覆盖;必须走 `append_attachments` 增量路由,见「写单元格」 |
|
|
263
|
+
| 追加后旧图片全部变为占位方块 | 同上:整格覆盖把旧项重写成无宽高,改用增量路由 |
|
|
264
|
+
| 提交报「参数无效」,payload 里的键是 `id` / `name` | 把 SQL 读回的形状直接提交了,增量路由要 `file_token`,见「SQL 侧的可读信息」 |
|
|
265
|
+
| 移除一张后整格没变化 | 同上;移除 payload 漏了 `id → file_token` 映射 |
|
|
266
|
+
| 同一张图在单元格里出现两份 | `append_attachments` 不按 token 去重;超时后重新提交前先读取确认 |
|
|
267
|
+
| `f.url` 拿到 null | SQL 层不提供链接,见上表 |
|
|
268
|
+
| 刚上传的图不显示,刷新后正常 | 写完立刻去取封面了。改成用本地 blob 展示 |
|
|
269
|
+
| 换下载链接报 `[object Object]`,看不出原因 | 通道层 reject 的是 `{code,msg}` 普通对象不是 `Error`,`String(error)` 取不到内容;改成读 `code` |
|
|
270
|
+
|
|
271
|
+
## Related
|
|
272
|
+
|
|
273
|
+
- [代码开发手册](development.md) — 工程结构、Function 契约、行写入协议
|
|
274
|
+
- [SQL Function 专项](sql-function.md) — `FILE` 子字段读取
|
|
275
|
+
- [风格手册](style-guide.md) — 附件展示的视觉规范
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# 数据库应用权限与访问设计
|
|
2
|
+
|
|
3
|
+
本文负责应用的访问与权限设计:在平台权限模块之外,用 bindings、SQL Function 和 FaaS 校验把「谁能看什么、谁能做什么」设计成可验证的事实。用法是「访问模型先行」:理解需求时先形成访问模型,再让它约束 binding、SQL 与写入链路的实现。实现层协议见[代码开发手册](development.md)与 [SQL Function 专项](sql-function.md);原生多维表格侧的角色与高级权限配置见[原生 Base 能力发现与路由](base-capability-routing.md)。
|
|
4
|
+
|
|
5
|
+
本文内容分两层:**平台安全模型**是当前平台的事实,不是设计选项,所有方案都必须在它成立的前提下推导;**设计默认路径**(成员表配方、分表方式等)是被验证过的出发点而非唯一解——可以按业务另择实现,但替换方案必须重新通过对应小节的判定问题。
|
|
6
|
+
|
|
7
|
+
## 平台安全模型
|
|
8
|
+
|
|
9
|
+
应用在飞书里是一篇多维表格文档。当前平台的权限事实:
|
|
10
|
+
|
|
11
|
+
- **准入只有一道门**:能打开线上应用的人 = 对这篇应用文档有阅读权限的人。范围由文档分享设置决定,掌握在用户手里,应用代码无法收窄它。Preview 环境只有应用 owner 本人能访问。
|
|
12
|
+
- **门内以 owner 身份执行**:应用读写 Base 数据一律以应用 owner 的身份进行,访问者本人对底层 Base 有没有协作权限不参与判定。底表的原生高级权限(行列权限)不约束应用访问者的读取——原生权限管的是直接打开多维表格的人,应用是另一个入口,两边要分别设计;应用内的读取范围由 SQL Function 自己写了什么决定。
|
|
13
|
+
- **调用面对全体访问者开放**:任何能打开应用的人都可以用任意参数调用任何已发布的 SQL/FaaS Function,并且可以对 bindings 内的每张表直接使用平台记录写入通道(批量创建、更新、删除),不经过页面 UI。UI 显隐、路由守卫和前端校验只是体验,不是边界。
|
|
14
|
+
- **唯一可信身份在 SQL 层**:`CURRENT_USER_ID()` 是访问者身份唯一可信的来源,由平台服务端注入、页面无法伪造,且只能在 SQL Function 内使用。Page 和 FaaS 的业务代码拿不到可信的访问者 user id,任何「当前用户是谁」的判断都必须落到 SQL。
|
|
15
|
+
- **FaaS 不是更高权限**:FaaS 与 Page 的数据读写权限完全相同。FaaS 的安全价值是提供「访问者改不了的校验位置」,不是提权。
|
|
16
|
+
|
|
17
|
+
由此得出应用的威胁模型:**应用的真实安全边界 = 已发布的 Function 集合 + bindings 声明的表,面对的是分享范围内权限最低、且可能绕过 UI 直调接口的访问者**。每个 Function、每张 bind 的表都按这个假设设计。
|
|
18
|
+
|
|
19
|
+
## 访问模型先行
|
|
20
|
+
|
|
21
|
+
理解需求时,与用户角色、核心对象一起回答三个问题,形成一份紧凑的访问模型(放在内部规划中,不创建额外文档):
|
|
22
|
+
|
|
23
|
+
1. **谁能打开应用**:分享范围是团队内、全组织还是更大?这是用户手里的旋钮,把它当输入而不是假设;需求未说明时按「全组织可见」的口径设计门内权限。数据越敏感,越不能让安全性依赖「反正没分享给别人」。
|
|
24
|
+
2. **门内谁能看什么**:哪些数据全员可见,哪些只能看与自己相关的(「我的 X」),哪些按角色区分?
|
|
25
|
+
3. **谁能做什么**:哪些写入人人可做,哪些限定角色,哪些只属于管理员(owner)?
|
|
26
|
+
|
|
27
|
+
三问答案都是「全员一样」的应用不需要角色结构,不为不存在的需求预建成员表和权限判定;但身份过滤(第 2 问)与写入校验的通用规则仍然适用。只有数据所有权、权限或安全存在实质歧义时才询问用户,口径与[风格手册](style-guide.md)一致。
|
|
28
|
+
|
|
29
|
+
## Bind 即暴露
|
|
30
|
+
|
|
31
|
+
**原则**:一张表 bind 进应用,等于把 owner 权限内该表的全部行列同时置于两个暴露面之下——读的一侧是「SQL Function 写什么就能查什么」,写的一侧是「平台写入通道全开」。未 bind 的表读写都会被服务端拒绝:bindings 是当前唯一有平台强制力的收窄手段。
|
|
32
|
+
|
|
33
|
+
**判定**:对每张要 bind 的表问——它最敏感的字段被分享范围内任何人读到、最关键的记录被任何人改掉或删掉,业务能否接受?
|
|
34
|
+
|
|
35
|
+
**默认路径**:只 bind 页面与 Function 实际需要的表。读判定不通过时,回到数据建模按权限边界分表(风格手册的业务设计原则),让敏感字段留在不 bind 的表里。写判定不通过时要清楚:FaaS 校验只约束走 FaaS 的路径,挡不住直调平台写入通道的访问者——篡改需要主动构造请求,普通使用不会发生,但有能力的访问者可以做到。这是当前平台的已知残余风险:是否可接受由数据敏感度和分享面共同决定,不可接受时不把该数据放进应用,或与用户确认收窄分享面,并把结论写进交付说明。
|
|
36
|
+
|
|
37
|
+
## 读路径:SQL Function 就是策略面
|
|
38
|
+
|
|
39
|
+
**原则**:Page 只能调用发布时固化的 SQL Function,因此 SQL Function 集合就是应用事实上的数据访问策略——SELECT 的列清单是列级权限,`CURRENT_USER_ID()` 过滤是行级权限。
|
|
40
|
+
|
|
41
|
+
**判定**:分享范围内权限最低的访问者,用任意参数调用这个 Function,返回的每一行每一列都是他该看的吗?
|
|
42
|
+
|
|
43
|
+
**默认路径**:
|
|
44
|
+
|
|
45
|
+
- 「我的 X」一律在 SQL 内用 `CURRENT_USER_ID()` 与人员字段比较过滤(写法见 [SQL Function 专项](sql-function.md)),不通过参数传 user id。**参数是过滤器,不是权限**:任何参数值都可能被任意访问者传入。
|
|
46
|
+
- 按角色区分可见性时,在查询 SQL 中关联成员表完成判定(见下节),而不是发布「高权限版」和「低权限版」两个 Function 靠前端选择调用。
|
|
47
|
+
- 敏感列不进 SELECT 就不会泄露;不用「全量查出、前端隐藏」。
|
|
48
|
+
|
|
49
|
+
## 操作权限:成员表配方
|
|
50
|
+
|
|
51
|
+
需要「某些人能、其他人不能」时的默认路径(不是唯一解,但替换方案必须同样以 `CURRENT_USER_ID()` 为判定源):
|
|
52
|
+
|
|
53
|
+
1. **成员表**:一张成员表,人员字段存用户、受控单选存角色,bind 进应用。注意成员表自身就是典型的写敏感表,同样处于上节的残余风险之下;团队内场景通常可接受,敏感场景按上节处理并在交付中说明。
|
|
54
|
+
2. **whoami**:一个固定 SQL Function 按 `CURRENT_USER_ID()` 查成员表,返回当前访问者的成员记录与角色;查不到按「无角色」处理,业务层默认拒绝。
|
|
55
|
+
3. **前端显隐**:页面用 whoami 的结果决定入口与视图的显隐——这是体验优化,真正的边界在读写路径。
|
|
56
|
+
4. **角色敏感写入走 FaaS**:FaaS 先调用含 `CURRENT_USER_ID()` 的固定校验 SQL(角色、归属、状态尽量并入一次查询,见[代码开发手册](development.md#远程调用与写入链路设计)),通过后再执行写入,不通过返回稳定业务码。
|
|
57
|
+
5. **操作者与审计**:写入需要记录「谁做的」时,操作者身份取自可信 SQL 的查询结果,不取页面传值;需要审计的 operation 在业务字段中显式记录操作者与时间。
|
|
58
|
+
|
|
59
|
+
owner 专属的管理功能用平台下发的 `isOwner` 信号做门控(服务端裁决,缺失按 false 处理),参考 starter 中的现有用法。
|
|
60
|
+
|
|
61
|
+
### 配方示例
|
|
62
|
+
|
|
63
|
+
以报销应用为例:全员可提交报销单,管理员可见全部并审批,普通成员只能看自己的。示例展示默认路径的关键写法;角色档位、判定并入查询还是拆分按业务调整,但判定源必须是 `CURRENT_USER_ID()`,SQL 语法约束以 [SQL Function 专项](sql-function.md)为准。
|
|
64
|
+
|
|
65
|
+
成员表 schema——人员字段显式声明 `"multiple": false`(默认为 true),单人字段在 SQL 中才是标量 `USER`,可以直接比较子字段;多人字段是集合,要按专项走 `UNNEST`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"name": "成员",
|
|
70
|
+
"fields": [
|
|
71
|
+
{ "type": "text", "name": "姓名" },
|
|
72
|
+
{ "type": "user", "name": "用户", "multiple": false },
|
|
73
|
+
{
|
|
74
|
+
"type": "select",
|
|
75
|
+
"name": "角色",
|
|
76
|
+
"multiple": false,
|
|
77
|
+
"options": [{ "name": "管理员" }, { "name": "成员" }]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
whoami(无参数 Function)——查不到行即「无角色」,前端据此渲染,服务端默认拒绝。注意标量人员子字段按方言约定不带表别名:
|
|
84
|
+
|
|
85
|
+
```sql
|
|
86
|
+
SELECT rec_id AS member_id, "姓名" AS name, "角色" AS role
|
|
87
|
+
FROM "成员"
|
|
88
|
+
WHERE "用户".userId = CURRENT_USER_ID()
|
|
89
|
+
LIMIT 1
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
角色判定并入业务查询(管理员看全部,成员只看自己的),用派生表带出当前访问者的角色,不发布高低权限两个版本:
|
|
93
|
+
|
|
94
|
+
```sql
|
|
95
|
+
SELECT e.rec_id AS record_id, e."标题" AS title, e."金额" AS amount, e."状态" AS status
|
|
96
|
+
FROM "报销单" AS e
|
|
97
|
+
CROSS JOIN (
|
|
98
|
+
SELECT COUNT(rec_id) AS admin_count
|
|
99
|
+
FROM "成员"
|
|
100
|
+
WHERE "用户".userId = CURRENT_USER_ID() AND "角色" = '管理员'
|
|
101
|
+
) AS me
|
|
102
|
+
WHERE me.admin_count > 0 OR "申请人".userId = CURRENT_USER_ID()
|
|
103
|
+
ORDER BY e.rec_id DESC
|
|
104
|
+
LIMIT 50
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
FaaS 写前校验(审批动作,`$1` 声明为 `RECORD_ID`)——角色、目标记录存在性和状态并入一次查询,FaaS 只在结果为 1 时执行写入:
|
|
108
|
+
|
|
109
|
+
```sql
|
|
110
|
+
SELECT COUNT(e.rec_id) AS approvable
|
|
111
|
+
FROM "报销单" AS e
|
|
112
|
+
CROSS JOIN (
|
|
113
|
+
SELECT COUNT(rec_id) AS admin_count
|
|
114
|
+
FROM "成员"
|
|
115
|
+
WHERE "用户".userId = CURRENT_USER_ID() AND "角色" = '管理员'
|
|
116
|
+
) AS me
|
|
117
|
+
WHERE e.rec_id = $1 AND e."状态" = '待审批' AND me.admin_count > 0
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
FaaS 调用 SQL 与写入的协议见[代码开发手册](development.md#faas-开发)。
|
|
121
|
+
|
|
122
|
+
## 常见失分点
|
|
123
|
+
|
|
124
|
+
最常见的失误例子,不是完备清单——没列到的情况回到威胁模型推导:
|
|
125
|
+
|
|
126
|
+
- 通过参数传 user id 实现「我的 X」,任何人换个参数就能看别人的数据。
|
|
127
|
+
- 全量查询后由前端过滤敏感行列——数据已经离开服务端,过滤只是表演。
|
|
128
|
+
- bind 了敏感表,靠「页面没做那个功能」当保护。
|
|
129
|
+
- 以为底表开了高级权限,应用访问者也随之受约束。
|
|
130
|
+
- FaaS 校验的依据是页面传入的 user id、角色或 isAdmin 标记。
|
|
131
|
+
- 把「还在 Preview、没人看得到」当作暂缓权限设计的理由——Preview 的 owner-only 是环境属性,发布 Prod 不会自动带来任何权限。
|
|
132
|
+
|
|
133
|
+
## 交付与验收
|
|
134
|
+
|
|
135
|
+
- 交付说明中包含访问模型结论:分享面假设、按人/按角色的可见性设计,以及写敏感表的残余风险(如有)。
|
|
136
|
+
- 对外分发或扩大分享范围前,提醒用户核对应用文档的分享设置——这是唯一的准入旋钮,且不在代码里。
|
|
137
|
+
- 角色化行为在 Preview 无法由 owner 之外的人验证(Preview 仅 owner 可访问,`CURRENT_USER_ID()` 恒为 owner);多角色验收在 Prod 环境用真实账号完成,交付时说明这一限制。
|
|
138
|
+
- 修改原生 Base 角色或高级权限、删除 binding 属于危险操作,按 [Skill 主流程](../SKILL.md#危险操作确认)执行两阶段文字确认。
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 数据库应用发布与验收
|
|
2
|
+
|
|
3
|
+
常规发布只使用 `npx @lark-base-open/base-database-cli deploy`。CLI 内部固定执行:生成 Function metadata → 全量 SQL 校验并汇总失败项 → 根据依赖指纹安装必要依赖并构建 → 归档 → Functions 全量更新 → Artifact 上传与源码回读 → Preview 发布与状态轮询。存在任一 SQL 校验失败时不会安装依赖或执行 Page/FaaS build;依赖指纹未变化且对应 `node_modules` 仍存在时自动跳过 `npm ci`,但仍重新执行 build。仅在排障时使用 `--force-install` 强制重装或用 `--no-install` 无条件跳过安装。
|
|
4
|
+
|
|
5
|
+
## Preview
|
|
6
|
+
|
|
7
|
+
每次 Preview / Prod 发布成功后还会自动生成、安装或更新本应用专属 Skill。交付时按 `agentGuide.skill.status` 报告安装结果并提示用户后续可直接使用本应用能力;生成文件每次部署覆盖更新,函数及 Schema 则在使用时实时发现。Preview 的 Skill 仍只指向正式版。安装失败单独报告,不重发应用;修复见[应用能力 Skill](database-capability-skill.md)。
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @lark-base-open/base-database-cli deploy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
该命令通常需要约 30 秒到 1 分钟,中间不显示阶段进度。
|
|
14
|
+
|
|
15
|
+
成功结果包含制品大小、SHA-256、publish ID 和设置 `mode=edit` 的顶层 `baseAppUrl`。CLI 只读取成功态发布轮询的 `result.baseAppUrl`,不使用 `catalog.json` 或 OpenAPI GetProject 兜底;成功态缺少该字段时立即报错。命令结果不返回管理后台 URL、旧 Preview URL、`preview`/`production` URL 对象或发布平台静态资源 `appUrl`/`staticUrl`;静态资源地址仅保存在内部 Preview 回执。
|
|
16
|
+
|
|
17
|
+
部署成功后,从零创建了新 Base 的应用先完成下述“示例数据闭环”;复用已有 Base 或修改已有应用时不擅自添加示例记录,直接提供顶层 `baseAppUrl`、本次变更摘要和短验收清单。Agent 始终不展示管理后台 URL、旧 Preview URL 或内部静态资源 `staticUrl`。用户继续在飞书登录态确认:
|
|
18
|
+
|
|
19
|
+
1. 路由不是 404/5xx;
|
|
20
|
+
2. HTML 与静态资源加载成功;
|
|
21
|
+
3. 页面出现本次版本内容;
|
|
22
|
+
4. SQL Function 能读取真实 Base 数据;
|
|
23
|
+
5. 平台支持 FaaS 时,从页面写入并由 SQL Function 回读同一记录。
|
|
24
|
+
6. 页面通过 `createArgusCsrfPlugin()` 加载 Argus Runtime;真实 JSSDK 写请求携带 `x-secsdk-csrf-token`,且端到端调用成功。
|
|
25
|
+
|
|
26
|
+
提示用户体验完成后回复“部署正式版”。
|
|
27
|
+
|
|
28
|
+
### 从零搭建应用示例数据闭环
|
|
29
|
+
|
|
30
|
+
这是从零搭建应用在 Preview 发布后的窄范围运行态验收,也是“发布后不调用 Function”通用规则的唯一例外:
|
|
31
|
+
|
|
32
|
+
1. 从 `catalog.json` 读取真实 Base/table/字段信息,通过当前项目的 Base Database CLI 路由先执行 `lark-cli base +field-list`,再执行 `lark-cli base +record-batch-create` 写入小而完整、带稳定可见标记的非敏感示例数据;没有项目级路由时直接使用同名 `lark-cli ... --as user`。不得静默自动播种或使用静态 fixture。
|
|
33
|
+
2. 校验 `record_id_list` 数量和 `ignored_fields`。存在业务数据展示时,再通过 `/open-apis/base/v3/sites/:site_token/invoke_function/:function_id?env=preview` 调用页面实际使用的 SQL Function,确认示例记录、关系和聚合已经进入真实 Base;不得用写入返回值、`+record-list` 或 mock 代替展示链路验收。
|
|
34
|
+
3. 失败时保存脱敏 `code`、`msg` 和 LogID。重试前先用同一 SQL Function 确认是否已经部分写入;写入问题检查 Catalog、CellValue、关联 ID 和权限,查询问题必须修改 SQL Function、参数或响应解析。实现问题修复后重新 `deploy` 再验证。
|
|
35
|
+
4. 只有认证、权限或平台服务存在证据充分的外部阻塞时才停止并标记 `blocked`;写入或 SQL 回读未通过时不得交付为完成。通过后停止运行态验收,再执行必要的通知入口收尾并向用户交付 Preview。
|
|
36
|
+
|
|
37
|
+
此闭环不需要浏览器,不授权发布 Prod、删除示例数据或读取用户凭证,也不替代 Page 写入与 UI 的 E2E 验收。
|
|
38
|
+
|
|
39
|
+
### 原生通知入口收尾
|
|
40
|
+
|
|
41
|
+
发布前逐个检查 `LarkMessageAction`,按固定优先级确定入口:用户明确要求进入多维表格时使用 `base_action`;用户明确要求通知纯展示、无需点击入口时使用 `informational`;其他所有与应用业务对象关联的通知一律使用 `site_action`。不得从通知正文足够完整或用户未指定跳转目标推断 `informational`。
|
|
42
|
+
|
|
43
|
+
1. Page 先实现通知对应的真实功能路由。新 `site_action` workflow 在 Preview 发布前不创建;从零搭建应用还必须等示例数据闭环通过后再创建,避免示例记录触发真实通知。不得用 `$.<trigger>.recordLink`、Base URL、管理后台 URL 或自行拼接 URL 兜底。
|
|
44
|
+
2. `deploy` 成功后只使用响应返回的顶层 `baseAppUrl` 作为用户入口。新 workflow 在保留 `mode=edit` 等既有查询参数的前提下加入 Page 已实现的功能定位参数和 workflow 上下文中的目标对象标识;已有 workflow 才先 `+workflow-get`,再用完整定义执行 `+workflow-update`。例如访客接待通知进入当前访客登记详情,考试通知进入对应考试详情或答题页;不得使用内部静态资源 `staticUrl`。
|
|
45
|
+
3. `informational` 只有在用户明确要求通知纯展示、无需任何点击入口时才允许 `btn_list=[]`,并必须记录用户原文。空按钮列表不代表原生通知不可点击,也不能证明它不会沿平台默认行为跳转 Base。
|
|
46
|
+
4. `base_action` 只在用户明确要求通知进入多维表格时使用;回读并记录该授权语义,不得将 Base `recordLink` 描述成应用入口。
|
|
47
|
+
5. 执行 `+workflow-enable`,随后 `+workflow-get` 回读全部 `LarkMessageAction`:核对入口策略、启用状态、按钮列表;`site_action` 还要确认业务按钮基于本次顶层 `baseAppUrl`,并能从 URL 参数定位到当前 Preview 应用中对应的功能与业务对象。任一条件不满足时立即执行 `+workflow-disable`,并把结果标记为 partial 或 blocked。
|
|
48
|
+
6. 在 Preview 交付清单中记录每个相关 workflow ID、入口策略、按钮字段路径,以及环境无关的功能定位参数;没有按钮时记录平台默认跳转限制。Prod 切换必须复用这份清单,不得只更新碰巧记得的一个按钮。
|
|
49
|
+
7. 完成后立即交付 Preview;不得借此继续修改 Page、调用 Function 或发布 Prod。
|
|
50
|
+
|
|
51
|
+
## Prod
|
|
52
|
+
|
|
53
|
+
只有用户体验本次 Preview 后明确回复“部署正式版”,才执行:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx @lark-base-open/base-database-cli deploy --production --yes
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
CLI 校验 Preview 记录、应用 token 和 Artifact SHA-256,随后直接发布同一制品,不重建、不替换。`--yes` 只表示调用方已经取得本次 Prod 授权;不能用任务开始时笼统的“搭建并发布”代替,也不能由 Agent 自行验收后授权。
|
|
60
|
+
|
|
61
|
+
`deploy --production` 只发布应用制品,不会自动更新 Base workflow。Prod 发布成功后,只要 Preview 交付清单包含通知入口,就必须对清单中的每个 workflow 执行 `get` → 完整 `update` → `get`。`site_action` 使用响应返回的顶层 `baseAppUrl`,只替换环境入口,逐字保留对应的功能定位参数、动态对象参数、其他步骤和启用状态;`informational` 和 `base_action` 不改写入口语义,只复核按钮列表、平台限制或用户明确要求仍然成立。回读验收必须同时满足:每个应用业务 `openLink` 都基于生产 `baseAppUrl`、功能定位参数与 Preview 阶段一致、链接中不再包含 `mode=edit`,且 workflow 启用状态未变化。`site_action` 不得退化为应用首页、Base 记录或应用管理后台。
|
|
62
|
+
|
|
63
|
+
Prod 应用发布成功但任一 workflow 更新或回读失败时,不要重新发布 Prod,也不要报告整体完成;明确告知用户“Prod 已发布,但通知入口切换未完成”,附上失败的 workflow ID 和脱敏错误,修复后重新执行 `get` → `update` → `get` 收尾。
|
|
64
|
+
|
|
65
|
+
## 发布状态
|
|
66
|
+
|
|
67
|
+
CLI 使用 publish 响应中的 `publishId`,并通过 query 调用 status:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
GET /open-apis/base/v3/sites/:site_token/publish_status?publishId=:publish_id
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
每 5 秒轮询,最多 3 分钟:`0/1` 继续,`10` 成功,`20` 失败,未知状态停止。只有成功态的 `publish.result` 返回 `staticUrl` 和 `baseAppUrl`;CLI 只使用其中的 `baseAppUrl`,不读取本地 `catalog.json`,也不调用 OpenAPI GetProject。缺少 `baseAppUrl` 时发布命令失败,不猜测用户入口。Preview 设置 `mode=edit`,Prod 移除 `mode`;发布平台静态资源 `result.staticUrl` 不向用户暴露。
|
|
74
|
+
|
|
75
|
+
## 故障证据
|
|
76
|
+
|
|
77
|
+
失败时保存环境、应用 token、阶段、publish ID、脱敏响应和 LogID/Edge request ID。
|
|
78
|
+
|
|
79
|
+
依赖安装使用 CLI 随包固定的 npm,不受宿主 PATH 中 npm 版本影响。安装失败时保留子项目路径、npm 版本与原始错误:lock 不一致先核对本次 `package.json` 变更,按 CLI 错误提示修复该子项目的 lock 并检查差异,再重试一次;网络或权限错误先解决对应原因。不要盲删 lock、循环切换 npm 版本,也不要用 `--no-install` 掩盖安装失败;重试仍失败时保留证据并定位原因,不自动报告部署成功。
|
|
80
|
+
|
|
81
|
+
写请求失败时先排除登录、权限和业务错误;若未加载 Argus Runtime 或请求缺少 `x-secsdk-csrf-token`,升级 JSSDK 并重新构建发布,不得手写 Token。
|