From fe41c8f3520bc8a1219f7caefd36d3390be2c2fe Mon Sep 17 00:00:00 2001 From: TanDianxiong <5304849+TanDianxiong@users.noreply.github.com> Date: Fri, 31 Jul 2026 01:18:51 +0800 Subject: [PATCH] feat(base): improve workflow update execution guidance --- shortcuts/base/base_shortcuts_test.go | 11 +++++ shortcuts/base/workflow_disable.go | 1 + shortcuts/base/workflow_enable.go | 2 + shortcuts/base/workflow_update.go | 5 ++- skills/lark-base/SKILL.md | 2 + .../references/lark-base-workflow-guide.md | 17 ++++++++ .../references/lark-base-workflow-schema.md | 4 +- .../base/base_workflow_update_dryrun_test.go | 41 +++++++++++++++++++ 8 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 tests/cli_e2e/base/base_workflow_update_dryrun_test.go diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index bc3e9441b1..997f07d618 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -669,9 +669,16 @@ func TestBaseWorkflowHelpGuidesAgents(t *testing.T) { shortcut: BaseWorkflowUpdate, wantTips: []string{ "lark-cli base +workflow-update --base-token --workflow-id --json @workflow.json", + "relative path such as --json @./workflow.json", + "pipe it to --json -", "PUT uses full replacement semantics", "Use +workflow-get first", "keep title/status/steps fields", + "explicit update request requires calling this command", + "use +workflow-get to verify every requested field", + "ReminderTrigger offset is directional", + "+N means N units before", + "-N means N units after", "workflow-id must start with wkf", "Updating does not enable or disable", "do not invent steps[].type/data/next/children from natural language", @@ -684,6 +691,8 @@ func TestBaseWorkflowHelpGuidesAgents(t *testing.T) { "workflow-id must start with wkf", "does not modify steps", "New workflows are created disabled", + "explicit enable request requires calling this command", + "do not change steps unless the user explicitly requested", }, }, { @@ -692,6 +701,8 @@ func TestBaseWorkflowHelpGuidesAgents(t *testing.T) { wantTips: []string{ "workflow-id must start with wkf", "does not delete the workflow or its steps", + "explicit disable request requires calling this command", + "use +workflow-get to verify status", }, }, } diff --git a/shortcuts/base/workflow_disable.go b/shortcuts/base/workflow_disable.go index 945114ffec..0806a1338f 100644 --- a/shortcuts/base/workflow_disable.go +++ b/shortcuts/base/workflow_disable.go @@ -24,6 +24,7 @@ var BaseWorkflowDisable = common.Shortcut{ Tips: []string{ "workflow-id must start with wkf; do not pass a tbl table ID from the same URL.", "Disable only changes workflow state; it does not delete the workflow or its steps.", + "An explicit disable request requires calling this command even if list/get already reports disabled; then use +workflow-get to verify status.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { diff --git a/shortcuts/base/workflow_enable.go b/shortcuts/base/workflow_enable.go index 3cca469c33..c2a110cc65 100644 --- a/shortcuts/base/workflow_enable.go +++ b/shortcuts/base/workflow_enable.go @@ -25,6 +25,8 @@ var BaseWorkflowEnable = common.Shortcut{ "workflow-id must start with wkf; do not pass a tbl table ID from the same URL.", "Enable only changes workflow state; it does not modify steps.", "New workflows are created disabled; enable after creation only when the user wants it active.", + "An explicit enable request requires calling this command even if list/get already reports enabled; then use +workflow-get to verify status.", + "If enable fails because workflow configuration is incomplete, do not change steps unless the user explicitly requested configuration changes; report the precondition.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { diff --git a/shortcuts/base/workflow_update.go b/shortcuts/base/workflow_update.go index ea13a3e17a..54d3738780 100644 --- a/shortcuts/base/workflow_update.go +++ b/shortcuts/base/workflow_update.go @@ -20,12 +20,15 @@ var BaseWorkflowUpdate = common.Shortcut{ Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, {Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true}, - {Name: "json", Desc: "workflow body JSON; read lark-base-workflow-guide.md and lark-base-workflow-schema.md before replacing steps", Required: true}, + {Name: "json", Desc: "workflow body JSON; read lark-base-workflow-guide.md and lark-base-workflow-schema.md before replacing steps", Required: true, Input: []string{common.File, common.Stdin}}, }, Tips: []string{ "lark-cli base +workflow-update --base-token --workflow-id --json @workflow.json", + "JSON files must use a relative path such as --json @./workflow.json; for generated or temporary JSON, pipe it to --json - instead of using @/absolute/path.", "PUT uses full replacement semantics; omitting steps clears the existing workflow steps.", "Use +workflow-get first, then edit the returned definition and keep title/status/steps fields you do not intend to change.", + "An explicit update request requires calling this command even if get already shows the requested values; then use +workflow-get to verify every requested field and its normalized meaning.", + "ReminderTrigger offset is directional: +N means N units before (early), while -N means N units after (delayed); translate the value back to natural language before sending the update.", "workflow-id must start with wkf; do not pass a tbl table ID.", "Step ids must be unique, and every next/children link must reference an existing step id.", "Updating does not enable or disable a workflow; call +workflow-enable or +workflow-disable separately.", diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index 556fc935c4..68bafbdbc7 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -126,6 +126,8 @@ metadata: - Dashboard 的复杂点是 block 的 `data_config`,不是 list/get/create/delete 命令参数。创建或更新 block 前先读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md),组件必须串行创建;`+dashboard-arrange` 是服务端智能布局,仅在用户明确要求重排/美化、或对本次会话从零新建的仪表盘做收尾整理时执行。`+dashboard-block-get-data` 读取图表最终计算结果,不返回 block 名称、类型、布局或 `data_config`;需要元数据先用 `+dashboard-block-get`。 - Workflow 的复杂点是 `steps` 结构。创建、更新或解释完整 workflow 时读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) 和 steps JSON SSOT [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md);enable/disable/list 只需确认 workflow ID、当前启停状态和用户意图。 +- 用户显式要求 `workflow-update/enable/disable` 时,读取现状只用于定位和构造请求,不能代替写操作:必须执行对应命令,再用 `+workflow-get` 回读目标 workflow 验证结果;即使预读状态或字段已经符合目标,也不要跳过这次明确请求。 +- `workflow-update` 只修改用户授权的字段并保留其余定义;方向性配置在写前、回读后都要用自然语言复核。ReminderTrigger 的“提前 N 个单位”必须写正 `offset=N`,“延后 N 个单位”必须写负 `offset=-N`。`workflow-enable` 因业务配置缺失等前置条件失败时,不要擅自修改 steps 绕过校验,继续处理其他对象并报告无法启用项。 - Role 的复杂点是权限 JSON。角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);`+role-create` 只支持自定义角色;`+role-update` 是 delta merge;角色 create/update 或解读完整配置时读权限 JSON SSOT [role-config.md](references/role-config.md)。`+role-delete` 只适用于自定义角色,系统角色不可删除;删除角色和关闭高级权限前必须确认目标和影响。 ## 常见恢复 diff --git a/skills/lark-base/references/lark-base-workflow-guide.md b/skills/lark-base/references/lark-base-workflow-guide.md index fb7aa363c2..07603e80e0 100644 --- a/skills/lark-base/references/lark-base-workflow-guide.md +++ b/skills/lark-base/references/lark-base-workflow-guide.md @@ -741,6 +741,23 @@ --- +## 修改、启停与验证闭环 + +用户明确要求修改、启用或停用 workflow 时,采用同一套可审计闭环: + +1. 用 `+workflow-list` / `+workflow-get` 定位准确的 `workflow_id` 并读取现状。 +2. **始终执行用户要求的写命令。** 预读结果已经符合目标,不代表本次显式请求已完成;`+workflow-enable`、`+workflow-disable` 和保持相同目标值的 `+workflow-update` 都应按请求执行。不要把这条规则扩展到重复 `+workflow-create`。 +3. 写入成功后对同一个 `workflow_id` 执行 `+workflow-get`,逐项核对用户要求的状态或字段。不能只以 update/enable/disable 的成功回执作为最终证据。 +4. 回复时把关键返回值翻译回自然语言语义。例如 ReminderTrigger 的 `offset > 0` 表示提前、`offset < 0` 表示延后;写前和回读后都明确复述“提前/延后 N 个单位”,避免只报告裸数字。 + +`+workflow-update` 是全量替换:从 get 结果构造完整定义,只改变用户授权修改的字段,保留其余 title、status、steps 和节点连线。如果回读字段被服务端丢弃、归一化为不同值或方向与需求不符,不要宣称完成;应返回可验证差异。 + +`+workflow-enable` 只改变启停状态。若它因缺少接收者、Base App 或其他业务配置而返回前置条件失败,除非用户同时明确授权修改 workflow 配置,否则停止处理该 workflow,不要调用 `+workflow-update` 改 steps 绕过校验;继续启用其余目标,最后报告未启用项和所缺前置条件。 + +每个 workflow 只有一个首节点 Trigger。多个彼此独立的事件源必须拆成多个 workflow,分别形成 `Trigger → ...Actions` 的连通链;创建后分别回读不同的 `workflow_id`,验证首节点类型、后续链路和启停状态。 + +--- + ## 构造技巧 ### Loop 构造要点 diff --git a/skills/lark-base/references/lark-base-workflow-schema.md b/skills/lark-base/references/lark-base-workflow-schema.md index 916f53f646..f6bffbc3dd 100644 --- a/skills/lark-base/references/lark-base-workflow-schema.md +++ b/skills/lark-base/references/lark-base-workflow-schema.md @@ -235,6 +235,8 @@ ### ReminderTrigger +> ⚠️ **先翻译方向,再填写数值:** `offset=N` 表示日期前 N 个单位触发(提前 N);`offset=-N` 表示日期后 N 个单位触发(延后 N)。提交和回读时都按这句自然语言复核,不要只比较绝对值。 + ```json { "table_name": "项目表", @@ -252,7 +254,7 @@ | `table_name` | 是 | 数据表名 | | `field_name` | 是 | 日期字段名(必须为 `datetime` / `created_at` / `formula` / `lookup` 类型) | | `unit` | 是 | 偏移单位:`MINUTE` / `HOUR` / `DAY` / `WEEK` / `MONTH` | -| `offset` | 是 | 提前/延后的偏移量(正数=提前,负数=延后;范围由 `unit` 决定):`MINUTE` ∈ {0, 5, 15, 30, -5, -15, -30};`HOUR` ∈ [-6, -1] ∪ [1, 6];`DAY` ∈ [-7, 7];`WEEK` ∈ [-7, -1] ∪ [1, 7];`MONTH` ∈ [-7, -1] ∪ [1, 7] | +| `offset` | 是 | 偏移量:正数=日期前/提前,负数=日期后/延后;范围由 `unit` 决定:`MINUTE` ∈ {0, 5, 15, 30, -5, -15, -30};`HOUR` ∈ [-6, -1] ∪ [1, 6];`DAY` ∈ [-7, 7];`WEEK` ∈ [-7, -1] ∪ [1, 7];`MONTH` ∈ [-7, -1] ∪ [1, 7] | | `hour` | 是 | 触发小时 (0-23),默认 9 | | `minute` | 是 | 触发分钟 (0-59),默认 0 | | `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 | diff --git a/tests/cli_e2e/base/base_workflow_update_dryrun_test.go b/tests/cli_e2e/base/base_workflow_update_dryrun_test.go new file mode 100644 index 0000000000..cd07528e2c --- /dev/null +++ b/tests/cli_e2e/base/base_workflow_update_dryrun_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/require" +) + +func TestBaseWorkflowUpdateDryRunReadsJSONFromStdin(t *testing.T) { + setBaseDryRunConfigEnv(t) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "base", "+workflow-update", + "--base-token", "app_example", + "--workflow-id", "wkf_example", + "--json", "-", + "--dry-run", + }, + Stdin: []byte(`{"title":"Updated workflow","status":"OFF","steps":[]}` + "\n"), + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + out := result.Stdout + require.Equal(t, "PUT", clie2e.DryRunGet(out, "api.0.method").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_example/workflows/wkf_example", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "Updated workflow", clie2e.DryRunGet(out, "api.0.body.title").String(), out) + require.Equal(t, "OFF", clie2e.DryRunGet(out, "api.0.body.status").String(), out) + require.True(t, clie2e.DryRunGet(out, "api.0.body.steps").IsArray(), out) +}