@lark-project/cli
Version:
飞书项目插件开发工具
174 lines (117 loc) • 12.4 kB
Markdown
# mode=apply:校验 + 预览 + 推送远端
> 本文所有 `lpm` 命令都带 `--cwd "<projectRoot>"` 前缀([`shared.md` 执行约定](shared.md))。下方各步的 **Checkpoint 写入**走 `lpm --cwd "<projectRoot>" ai state set '<完整 JSON>'`(协议见 [`checkpoint.md`](checkpoint.md)),示例里的 `{ lastStep: ... }` 是写入的内容。
## 前置
- 需要 plan 阶段生成的 `.lpm-cache/config/draft-{timestamp}.json`
- 若无该文件,先执行 `mode=plan`
**Checkpoint 恢复检查**(仅在 `lpm --cwd "<projectRoot>" ai state get` 有输出时):
- `lastStep === "A0"` + `"success"` → 跳过 A0,从 A1 开始(用户已确认过清单,避免重复打扰)
- `lastCommand` 含 `local-config set` + `"success"` → 跳过 A0+A1,从 A2 开始
- `lastCommand` 含 `local-config diff` + `"success"` → 跳过 A0+A1+A2,从 A3 开始
- `lastCommand` 含 `update --source-type=local` + `"success"` → 跳过 A0+A1+A2+A3,直接执行 A4(输出)
- 其他 → 从 A0 开始
## A0:向用户确认变更(CRITICAL · ADDED/MODIFIED 的最后人工闸口)
> **不可跳过**。A1 的 `local-config set` 写本地前也会对**删除**做强阻塞 gate(draft 丢了远端点位 → exit 2、不写本地);无删除时才改写 `point.config.local.json` 并触发归一化。A2 diff 同样对**删除**强阻塞,对**新增 / 修改**仅非阻塞展示。A0 与 A2 分工:
>
> - **A0 = [ADDED] / [MODIFIED] 的最后人工闸口**(基于 draft 自比 remote 计算,set 之前)
> - **A2 = [DELETED] 的最后人工闸口**(基于 set 之后的本地文件 + CLI 真值,更可信)
>
> A0 拦的是"AI 写 draft 偏离用户意图";对"draft 漏写远端已有点位"的兜底由 CLI 在 set / diff / update 三处的删除 gate 完成(见 [`shared.md` §删除点位前置检查协议](shared.md))。两者不可互替。
**操作**:
1. 列出 draft 的点位清单 + 对比远端,按类型分类输出。**三类必须都列**——无该类时写一行 `(无)`,不要省略类别行:
```
[ADDED] <type> [<key>] — "<name>"
[MODIFIED] <type> [<key>] — "<name>"
变更字段:
name: "旧值" → "新值"
description: "旧值" → "新值"
(只列有差异的字段;无差异字段不出现)
[DELETED] <type> [<key>] — "<name>"
(此处仅展示,真正的删除确认 gate 在 A2,由 CLI 真值算)
```
切片来源(互不依赖,**支持并行 tool call 的 agent** 一次发起;**不支持的 agent** 顺序执行,结果相同):
- `lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/draft-{timestamp}.json --index`
- `lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/remote.json --index`
禁止整读 JSON 到 context。
2. 把清单完整呈现给用户,明确告知:"以上变更确认后会先写本地 `point.config.local.json`,再推送到 Meego 后台修改真实点位配置。请确认是否继续。"
3. 等待用户**明示**同意("确认" / "OK" / "推送" 等),才能进 A1。
4. 用户明示同意后,写入 Checkpoint:
```
{ lastStep: "A0", lastCommandStatus: "success", nextCommand: "local-config set", nextStep: "A1 本地校验+暂存" }
```
5. 用户拒绝 / 提出修改 → **回 plan 修正 draft** → 重跑 A0;不要在 apply 阶段就地编辑 draft 绕过 plan。
**正向约束**:
- A1 之前必须完成 A0 的明示同意收集(A1 段落已强制前置)
- 清单必须三类齐全(`[ADDED]` / `[MODIFIED]` / `[DELETED]`),无该类写"(无)"——不要漏 [MODIFIED],用户可能没意识到字段被改
- A0 和 A2 各管一类变更:A0 管 ADDED/MODIFIED,A2 管 DELETED——不要把两者合并到任何单一闸口(违反 = [`shared.md` §三条根原则](shared.md) 中"数据完整性"根原则违背)
## A1:本地校验 + 暂存 draft(local-config set)
**前置:A0 已获得用户明示确认**。无确认不得执行 A1。
**Checkpoint**:执行前写入 `{ nextCommand: "local-config set ...", nextStep: "A1 本地校验+暂存", lastCommandStatus: "running" }`
```bash
lpm --cwd "<projectRoot>" local-config set --from .lpm-cache/config/draft-{timestamp}.json
```
> `--cwd` 先把 CLI chdir 到插件根,`--from` 的相对路径随之解析到 `<projectRoot>/.lpm-cache/...`(也接受绝对路径)。CLI 完成:schema 本地校验 → URL 校验 → 后端 validator 校验 → 删除闸口 → 归一化 DSL → 写入本地 `point.config.local.json` → 清理 draft + `.lpm-cache/config/remote.json` 基线。**这一步还没推到远端**——推送在 A3。
### 删除闸口(exit 2 · 写本地之前)
draft 相比远端基线丢了点位时,set 在写本地**之前** exit 2,打印 `⚠️ DELETION_REQUIRES_CONFIRMATION` 清单——按下方 [A2 exit 2 分支](#exit-code-分支ai-的核心判定依据) 同一协议**逐字转呈用户取明示确认**。用户明示同意后,才 `local-config set --from <draft> --allow-delete` 重跑。正常按 [config-plan P4.0](feature-config-plan.md) 的 `lpm ai init-draft` 流程从远端基线建 draft,增 / 改不会触发此 gate;触发它通常意味着 draft 没建在基线上。
### 成功
**Checkpoint**:`{ lastCommand: "local-config set", lastCommandStatus: "success", nextCommand: "local-config diff", nextStep: "A2 预览本地 vs 远端差异" }`
继续执行 A2。
### 失败 — 错误分类处理
**Checkpoint**:`{ lastCommand: "local-config set", lastCommandStatus: "failed" }`
| 错误类型 | 处理方式 |
|----------|---------|
| 必填字段缺失 | 向用户补充询问,更新配置文件,**重新执行 A1** |
| 枚举值非法 | 列出合法值,询问用户选择,更新后**重新执行 A1** |
| key 重复冲突 | AI 自动在 key 后追加数字后缀(如 `_2`、`_3`)生成不冲突的 key,更新配置后**重新执行 A1** |
| name 超长 | 提示最大长度限制,要求用户缩短,**重新执行 A1** |
| URL 模式违规(`must match "^https?://"` / URL 缺失 / 为空) | 这类**结构性非法**仍是硬阻止 `exit 1`。把 CLI 报错原文转呈用户,由用户决定提供真实 URL 或其他处理,确认后**重新执行 A1**。注意:换成 example.com / your-server / localhost 之类**占位 URL 并不会被拦下**——CLI 只打印 `⚠️ NOTICE` 提示后照常推送;**AI 应把该 NOTICE 原文转呈用户、提醒上线前替换成真实后端地址**(对齐「Stage Config 先填占位、联调出真公网地址后再 set 替换」的两步流程)。所以**不要拿占位 URL 去「绕过」校验**,那只是把没填真地址的问题推到运行时。AI 也不要自行伪造 URL。(发布前还会由 publish 阶段 A0 的 `lpm check diff` ④段再兜一道) |
| Token 缺失(`must have required property 'token'`) | 当 `table_url.url` / `url` 有值时,对应 `token` 由 CLI 自动生成(36 位 UUID),AI **不需要**手写。若仍报错,说明 CLI 版本过旧或生成链路失败,如实上报用户排查;**切勿**自行伪造 token 值 |
| `table_cell must be object` | `table_cell` 支持 object 或 JSON string 两种形态,CLI 自动归一化。报这个错说明 CLI 版本过旧 → 提示用户升级 CLI 后**重新执行 A1**。AI 不要自行把 string 改成 object 绕过 |
| `platform.web` 字段类型不符(如 `table_url` 用在非 customField)| 不同点位类型有各自的 `PlatformWebFor{Type}` schema:`table_url` 是 customField 专有 / `mode` `init_size` 是 button 专有 / `scene` 是 control 专有 等,**不跨类型混用**。把字段挪回正确点位类型下,或删除误配字段,**重新执行 A1** |
| 未知错误 | 展示原始错误给用户,共同分析后**重新执行 A1** |
循环直至 set 成功。
> **通用原则:** 校验失败时 AI 的默认动作是"停下来问用户",不是"悄悄发明绕过方案"。任何自主修正都必须(1)不改变被测语义;(2)明确在对话中说明;(3)记入最终产物的 TODO 标记。
## A2:预览 diff(local-config diff · CRITICAL · 根原则 2 数据完整性)
> **不可跳过**。删除点位在 A1(set)/ A2(diff)/ A3(update)三处都会 `exit 2` 硬拦——A2 diff 是**无副作用的纯预览**,最适合让用户看"即将被删的点位清单"。三处任一 exit 2 都按本节协议转呈 + 取确认。
**Checkpoint**:执行前写入 `{ nextCommand: "local-config diff", nextStep: "A2 预览本地 vs 远端差异", lastCommandStatus: "running" }`
```bash
lpm --cwd "<projectRoot>" local-config diff
```
CLI 会对比 A1 写入的 `point.config.local.json` 与远端当前配置,按点位类型 + key 分类输出:
```
[ADDED] liteAppComponent [board_web_x9y2] — "Release Calendar"
[MODIFIED] button [button_submit] — "保存"
[DELETED] page [board_abc123] — "Board Demo"
Summary: 1 added, 1 modified, 1 deleted.
```
### exit code 分支(AI 的核心判定依据)
**exit 0(无删除)** → 安全推进:
- 把 stdout 摘要(最末行 `Summary: ...`)给用户看一眼作为变更预览,直接进 A3
- **Checkpoint**:`{ lastCommand: "local-config diff", lastCommandStatus: "success", nextCommand: "update --source-type=local", nextStep: "A3 推送远端 + 拉回模板" }`
**exit 2(有删除)** → **立即停止,不得自动继续**:
1. 把 CLI 的 stderr 完整内容(含 `⚠️ DELETION_REQUIRES_CONFIRMATION` 标题 + 被删点位清单)**逐字转呈**给用户——不改写、不压缩、不"帮用户总结"
2. 等待用户**明示**同意删除具体的点位(形如"同意删除 page[board_abc123]"或"全部删掉")
3. 用户确认后进 A3,且 A3 **必须带 `--allow-delete`**(否则 A3 自己的删除闸口会再次 exit 2 拦下推送);用户拒绝或有疑问 → 回 plan 修正 draft → 重跑 A1 → A2
**绝对禁止**(违反 = [`shared.md` §三条根原则](shared.md) 中"数据完整性"根原则违背,与"自己发布插件"同级):
- 看到 exit 2 不转呈 stderr,自己判断"这是废弃点位应该删"
- 假设"用户跑了 apply 就是同意所有变更"——用户可能根本没意识到 draft 里缺了某个 key
- 未经用户明示同意就加 `--allow-delete` 绕过任何一处删除闸口——它是"用户确认后的放行开关",不是"让命令别报错"的消音键
**exit 1(命令错误,非 diff 结果)** → 拉远端 / 读本地失败等,报错给用户分析。
## A3:推送远端 + 拉回模板(update --source-type=local)
**Checkpoint**:执行前写入 `{ nextCommand: "update --source-type=local", nextStep: "A3 推送远端 + 拉回模板", lastCommandStatus: "running" }`
```bash
# 无删除(A2 exit 0):直接推
lpm --cwd "<projectRoot>" update --source-type=local
# 有删除且用户已在 A2/A1 明示同意:带 --allow-delete 推
lpm --cwd "<projectRoot>" update --source-type=local --allow-delete
```
这一条命令完成两件事:
1. 推:把 A1 写入本地的 `point.config.local.json` 推送到远端
2. 拉:自动回拉远端最新配置 + 后端生成的模板代码,刷新 `plugin.config.json.resources` 和 `src/features/<resourceId>/index.tsx`
> **push 前 update 自己也跑一次删除闸口**:本地配置相比远端丢了点位且**未带** `--allow-delete` → exit 2、打印 `DELETION_REQUIRES_CONFIRMATION`、**不推送**。这不是非阻塞提醒,是硬闸口。所以 A2 检测到删除、用户已明示同意后,A3 必须带 `--allow-delete` 才能推过去;没带 = 推不动。远端不可达时此闸口降级为告警放行(push 命中后端会再校)。
- 成功 → **Checkpoint**:`{ lastCommand: "update --source-type=local", lastCommandStatus: "success", nextStep: "A4 输出" }` → 继续执行 A4
- 失败 → **Checkpoint**:`{ lastCommand: "update --source-type=local", lastCommandStatus: "failed" }` → 展示错误原文给用户;如果是推送阶段失败,本地已保存的 draft 仍在 `point.config.local.json`,修好后重跑本命令即可
## A4:输出
```
✅ 配置已成功推送至远端,新增点位模板已更新
变更:[添加/修改/删除] page[test_board_v1]
```