Skip to content

Commit 5a54bc0

Browse files
fix(base): classify +form-submit as high-risk-write (#1969)
Form submission writes and submits data through a public share link, an irreversible action that should require explicit confirmation. Reclassify the shortcut from write to high-risk-write so the runner's --yes gate fires before execution, matching +form-delete and other high-risk base commands. Update the lark-base skill docs (--yes on all examples, param table, tips) and add tests pinning the confirmation gate (unit) and dry-run structure (e2e). Co-authored-by: yballul-bytedance <273011618+yballul-bytedance@users.noreply.github.com>
1 parent a528b3c commit 5a54bc0

5 files changed

Lines changed: 94 additions & 12 deletions

File tree

shortcuts/base/base_form_submit.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ var BaseFormSubmit = common.Shortcut{
2626
Service: "base",
2727
Command: "+form-submit",
2828
Description: "Submit a form (fill and submit form data)",
29-
Risk: "write",
29+
Risk: "high-risk-write",
3030
Scopes: []string{"base:form:update", "docs:document.media:upload"},
3131
AuthTypes: authTypes(),
3232
HasFormat: true,
@@ -39,6 +39,7 @@ var BaseFormSubmit = common.Shortcut{
3939
`Example (no attachments): --share-token shrXXXX --json '{"fields":{"Service Rating":5,"Review":"Good service"}}'`,
4040
`Example (with attachments): --share-token shrXXXX --base-token basXXX --json '{"fields":{"Service Rating":5},"attachments":{"Attachment":["./report.pdf"]}}'`,
4141
`Cell values in "fields" follow lark-base-cell-value.md conventions; "attachments" maps field names to local file path arrays — the CLI uploads them in parallel and merges them into the submission.`,
42+
baseHighRiskYesTip,
4243
},
4344
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
4445
return validateFormSubmit(runtime)

shortcuts/base/base_shortcuts_test.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,8 +2056,8 @@ func TestBaseFormSubmitShortcut(t *testing.T) {
20562056
if s.Service != "base" {
20572057
t.Fatalf("Service=%q want base", s.Service)
20582058
}
2059-
if s.Risk != "write" {
2060-
t.Fatalf("Risk=%q want write", s.Risk)
2059+
if s.Risk != "high-risk-write" {
2060+
t.Fatalf("Risk=%q want high-risk-write", s.Risk)
20612061
}
20622062
if !s.HasFormat {
20632063
t.Fatal("HasFormat should be true")
@@ -2357,6 +2357,7 @@ func TestExecuteFormSubmit(t *testing.T) {
23572357
"+form-submit",
23582358
"--share-token", "shr_exec1",
23592359
"--json", `{"fields":{"Name":"Alice","Rating":5}}`,
2360+
"--yes",
23602361
}
23612362
if err := runShortcut(t, BaseFormSubmit, args, factory, stdout); err != nil {
23622363
t.Fatalf("err=%v", err)
@@ -2425,6 +2426,7 @@ func TestExecuteFormSubmit(t *testing.T) {
24252426
"--share-token", "shr_exec6",
24262427
"--base-token", "bas_exec6",
24272428
"--json", `{"attachments":{"File":["./nonexistent.pdf"]}}`,
2429+
"--yes",
24282430
}
24292431
err := runShortcut(t, BaseFormSubmit, args, factory, stdout)
24302432
if err == nil {
@@ -2473,6 +2475,7 @@ func TestExecuteFormSubmit(t *testing.T) {
24732475
"--share-token", "shr_dedup",
24742476
"--base-token", "bas_dedup",
24752477
"--json", `{"attachments":{"FieldA":["./shared.pdf"],"FieldB":["./shared.pdf"]}}`,
2478+
"--yes",
24762479
}
24772480
if err := runShortcut(t, BaseFormSubmit, args, factory, stdout); err != nil {
24782481
t.Fatalf("err=%v", err)
@@ -2484,6 +2487,33 @@ func TestExecuteFormSubmit(t *testing.T) {
24842487
})
24852488
}
24862489

2490+
// TestFormSubmitRequiresConfirmation pins the high-risk-write classification:
2491+
// without --yes the runner's confirmation gate must fire before Execute runs,
2492+
// returning a typed confirmation_required error and touching no API.
2493+
func TestFormSubmitRequiresConfirmation(t *testing.T) {
2494+
if BaseFormSubmit.Risk != "high-risk-write" {
2495+
t.Fatalf("Risk=%q want high-risk-write", BaseFormSubmit.Risk)
2496+
}
2497+
2498+
factory, stdout, _ := newExecuteFactory(t)
2499+
args := []string{
2500+
"+form-submit",
2501+
"--share-token", "shr_confirm",
2502+
"--json", `{"fields":{"Rating":5}}`,
2503+
}
2504+
err := runShortcut(t, BaseFormSubmit, args, factory, stdout)
2505+
if err == nil {
2506+
t.Fatal("expected confirmation_required error without --yes")
2507+
}
2508+
problem, ok := errs.ProblemOf(err)
2509+
if !ok {
2510+
t.Fatalf("expected typed error, got %T: %v", err, err)
2511+
}
2512+
if problem.Subtype != errs.SubtypeConfirmationRequired {
2513+
t.Fatalf("subtype=%q want %q", problem.Subtype, errs.SubtypeConfirmationRequired)
2514+
}
2515+
}
2516+
24872517
func TestUploadAttachmentsParallel(t *testing.T) {
24882518
t.Run("single file upload via execute path", func(t *testing.T) {
24892519
tmpDir := t.TempDir()
@@ -2520,6 +2550,7 @@ func TestUploadAttachmentsParallel(t *testing.T) {
25202550
"--share-token", "shr_para1",
25212551
"--base-token", "bas_para1",
25222552
"--json", `{"attachments":{"Doc":["./doc.txt"]}}`,
2553+
"--yes",
25232554
}
25242555
if err := runShortcut(t, BaseFormSubmit, args, factory, stdout); err != nil {
25252556
t.Fatalf("err=%v", err)
@@ -2554,6 +2585,7 @@ func TestUploadAttachmentsParallel(t *testing.T) {
25542585
"--share-token", "shr_err",
25552586
"--base-token", "bas_err",
25562587
"--json", `{"attachments":{"Bad":["./bad.txt"]}}`,
2588+
"--yes",
25572589
}
25582590
err := runShortcut(t, BaseFormSubmit, args, factory, stdout)
25592591
if err == nil {

skills/lark-base/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,13 @@ metadata:
109109
- 附件上传、下载、删除走专用 `+record-*-attachment` 命令。
110110
- 写字段前先读 [lark-base-field-json.md](references/lark-base-field-json.md);涉及 `formula` / `lookup` 时必须读 [formula-field-guide.md](references/formula-field-guide.md) / [lookup-field-guide.md](references/lookup-field-guide.md)
111111
- 表名、字段名、视图名、workflow 配置中的名称必须来自真实返回;跨表场景还要读取目标表结构。
112-
- 删除、角色更新、字段更新等高风险操作遵循 CLI 的 confirmation gate;目标不明确时先用 get/list 消歧。
112+
- 删除、角色更新、字段更新、表单提交(`+form-submit`)等高风险操作遵循 CLI 的 confirmation gate,必须带 `--yes`;目标不明确时先用 get/list 消歧。
113113
- 批量写入单批最多 200 条;连续写同一表时串行执行,遇到 `1254291` 按短暂等待后重试处理。
114114
- `select` 字段只支持写入字段中已有的选项;构造 CellValue 前先用 `+field-list``+field-search-options` 确认目标选项存在。
115115

116116
## 表单与视图细节
117117

118-
- `+form-submit` 前必须先跑 `+form-detail`,读取 `questions[].type``required``filter` 和附件场景需要的 `base_token`;不要填写被 filter 隐藏的问题。
118+
- `+form-submit` 是高风险写操作,必须带 `--yes` 确认;调用前必须先跑 `+form-detail`,读取 `questions[].type``required``filter` 和附件场景需要的 `base_token`;不要填写被 filter 隐藏的问题。
119119
- 表单附件不要写进 `fields`,放在 `--json.attachments`;提交附件时必须同时传表单所属 Base 的 `--base-token`
120120
- `+view-set-filter` 是唯一保留的 view reference;sort/group/card/timebar/visible-fields 这类配置先用对应 get 命令读现状,保留未修改字段,只替换用户要求变更的配置。
121121
- 视图适合持久化、共享和 UI 复用;一次性筛选/排序可先用 `+record-list` / `+record-search` 的 filter/sort 验证结果,再按需要沉淀为持久视图。

skills/lark-base/references/lark-base-form-submit.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
55
通过表单分享链接填写并提交多维表格表单。仅支持分享模式(share_token),支持填写普通字段值和上传本地文件作为附件。
66

7+
> **⚠️ 高风险写操作(high-risk-write):** 本命令会向表单写入并提交数据,属于高风险写操作,必须额外传递 `--yes` 进行确认,否则会返回 `confirmation_required` 错误并退出。当用户明确要求提交且目标表单无歧义时,直接附加 `--yes`,无需再次询问。
8+
79
## 填写前必读:先获取表单详情
810

911
**在调用 `+form-submit` 之前,必须先使用 `+form-detail` 获取表单详情。** 原因如下:
@@ -21,10 +23,11 @@ lark-cli base +form-detail --share-token <share_token>
2123

2224
# 2️⃣ 根据返回的 questions 列表,按 type 格式化值、检查 required、判断 filter 条件
2325

24-
# 3️⃣ 再提交
26+
# 3️⃣ 再提交(高风险写操作,必须带 --yes)
2527
lark-cli base +form-submit \
2628
--share-token <share_token> \
27-
--json '{"fields":{...}}'
29+
--json '{"fields":{...}}' \
30+
--yes
2831
```
2932

3033
`+form-detail` 的返回中要重点读取 `questions[].type``questions[].required`、题目 `filter` 和附件场景所需的 `data.base_token`
@@ -35,7 +38,8 @@ lark-cli base +form-submit \
3538
# 基本提交(填写普通字段)
3639
lark-cli base +form-submit \
3740
--share-token <share_token> \
38-
--json '{"fields":{"服务评分":5,"评价内容":"服务态度好"}}'
41+
--json '{"fields":{"服务评分":5,"评价内容":"服务态度好"}}' \
42+
--yes
3943

4044
# 带附件提交(需要额外提供 --base-token)
4145
lark-cli base +form-submit \
@@ -47,15 +51,17 @@ lark-cli base +form-submit \
4751
"附件字段名": ["./report.pdf", "./photo.png"],
4852
"另一个附件字段": ["./doc.docx"]
4953
}
50-
}'
54+
}' \
55+
--yes
5156

5257
# 使用应用身份(bot)
5358
lark-cli base +form-submit \
5459
--share-token <share_token> \
5560
--json '{"fields":{...}}' \
56-
--as bot
61+
--as bot \
62+
--yes
5763

58-
# 预览 API 调用(不实际执行)
64+
# 预览 API 调用(不实际执行,dry-run 无需 --yes
5965
lark-cli base +form-submit \
6066
--share-token <share_token> \
6167
--json '{"fields":{...}}' \
@@ -69,6 +75,7 @@ lark-cli base +form-submit \
6975
| `--share-token <token>` || 表单分享 Token(必填),从表单分享链接中提取 |
7076
| `--base-token <token>` | 条件必填 | Base token;**`--json` 包含 `attachments` 时必须提供**,用于将附件上传到 Base Drive Media |
7177
| `--json <json>` || JSON 对象,包含 `"fields"`(普通字段值)和 `"attachments"`(附件上传),详见下方说明 |
78+
| `--yes` || 确认高风险写操作。本命令为 high-risk-write,不带 `--yes` 会返回 `confirmation_required` |
7279
| `--format` || 输出格式:json(默认)\| pretty \| table \| ndjson \| csv |
7380
| `--as` || 身份:user(默认)\| bot |
7481
| `--dry-run` || 预览 API 调用,不执行 |
@@ -138,7 +145,8 @@ https://www.example.com/share/base/form/shrbcvST8eZy0vk8zjVZ1CAXNye
138145
```bash
139146
lark-cli base +form-submit \
140147
--share-token shrbcvST8eZy0vk8zjVZ1CAXNye \
141-
--json '{"fields":{...}}'
148+
--json '{"fields":{...}}' \
149+
--yes
142150
```
143151

144152
## 输出格式
@@ -158,6 +166,7 @@ lark-cli base +form-submit \
158166

159167
## 提示
160168

169+
- **本命令为高风险写操作(high-risk-write),必须额外传递 `--yes` 确认**,否则返回 `confirmation_required` 并以非零码退出;`--dry-run` 预览除外
161170
- 本命令仅支持通过表单分享链接(share_token)提交,不支持通过 base_token + table_id + view_id 方式提交
162171
- **`--json` 包含 `attachments` 时,必须额外提供 `--base-token`**,因为附件上传到 Base Drive Media 需要指定目标 Base
163172
- 附件字段只需在 `--json.attachments` 中提供本地路径即可,CLI 自动完成校验、并行上传、Token 获取和合并写入
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package base
5+
6+
import (
7+
"context"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
clie2e "github.com/larksuite/cli/tests/cli_e2e"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestBaseFormSubmitDryRun(t *testing.T) {
18+
setBaseDryRunConfigEnv(t)
19+
20+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
21+
t.Cleanup(cancel)
22+
23+
result, err := clie2e.RunCmd(ctx, clie2e.Request{
24+
Args: []string{
25+
"base", "+form-submit",
26+
"--share-token", "shrXXXX",
27+
"--json", `{"fields":{"Rating":5}}`,
28+
"--dry-run",
29+
},
30+
DefaultAs: "bot",
31+
})
32+
require.NoError(t, err)
33+
result.AssertExitCode(t, 0)
34+
35+
output := strings.TrimSpace(result.Stdout)
36+
assert.Contains(t, output, "/open-apis/base/v3/bases/tables/forms/submit")
37+
assert.Contains(t, output, `"share_token"`)
38+
assert.Contains(t, output, "shrXXXX")
39+
assert.Contains(t, output, `"method": "POST"`)
40+
}

0 commit comments

Comments
 (0)