@lark-project/cli
Version:
飞书项目插件开发工具
201 lines (130 loc) • 21.1 kB
Markdown
# mode=plan:分析需求 + 规划功能实现
> Stage Config 已生成了各点位的模板代码(`local-config set` 本地暂存 + `update --source-type=local` 推送远端并自动拉回模板,写入 `src/features/<resourceId>/index.tsx`)。
> plan 阶段的目标是**规划用户真实需求的功能实现方案**,而非生成模板。
## P0:点位配置的声明边界(CRITICAL · Stage Config / Stage Code 职责边界 · 防越权写声明)
Stage Code **只写代码方案**,properties / outputs / 点位字段 / 能力开关的**声明**归 Stage Config 管。
plan 阶段本质是"**理解用户意图 → 匹配点位能力能否实现**"——读 doc 时判断当前点位提供的能力能不能覆盖用户要的功能,这个过程常常会**反向发现上游配置缺口**:代码能写出来,但没有对应的点位配置支持时运行不起来。发现缺口的第一时间停下,回到本 skill 的 Stage Config 补齐配置再回到 Stage Code 继续,不要自己跑 `local-config set`,也不要直接改 `plugin.config.json`(见 [`shared.md`](shared.md#安全规则))。
**前置数据**:Stage Config 已跑完 config.apply(`local-config set` + `update --source-type=local` 两步都跑完),**点位配置的权威快照是工程根目录的 `point.config.local.json`**(CLI 在 set 成功时写入;`.lpm-cache/config/remote.json` 同步被清理,不要再找)。若 `point.config.local.json` 缺失或过期,重新跑一次 `lpm update --source-type=local` 即可(拉回最新远端配置)。
**常见缺口模式**(见一个就停下、回到 Stage Config 补配置再回来,不要自己补):
| 缺口类型 | 具体表现 | 典型场景 |
|---|---|---|
| 属性未声明 | 代码里用到的 `propKey`(`getProps()[propKey]` / `getDataSourceResult(propKey, ...)` / `notify(propKey, ...)`)在 `point.config.local.json` 里找不到 | 轻应用组件需要消费/提供某个配置,但 `properties` 数组里没这项 |
| 属性类型不对 | propKey 在,但 `prop_type` 与代码用法不匹配(如想当 `data_source` 消费,实际声明成 `text`) | 属性类型选错,API 调用无法生效 |
| 能力开关未启用 | 快照里某个属性的功能开关没开(如 `data_source` 属性缺 `with_field: true`,导致无法按字段级消费数据源) | liteAppComponent 想消费数据源里的具体字段,代码走 `with_field` 但配置没开 |
| 订阅/事件未声明 | 代码想监听某类事件,但 `listen_event` 里没声明对应 `event_type` | 拦截器 / 事件监听型点位代码走通了但不会被触发 |
| 子字段未声明 | customField 代码用到某个 subfield,快照里没有 | 字段模板缺 subfield 声明 |
判断流程:**不要 Read 整个 `point.config.local.json` 进上下文**(点位多时 JSON 很大)。用 `lpm ai peek` 带 `[field=value]` 过滤按需取:
```bash
# 某点位某 propKey 的完整声明(propKey 缺失 / prop_type 不对 / 能力开关未开 都能一眼看出来)
lpm --cwd "<projectRoot>" ai peek point.config.local.json 'liteAppComponent[key=<点位key>].properties[prop_key=<propKey>]'
# 某点位的整张声明(含 properties 数组全貌,可快速扫 propKey 清单)
lpm --cwd "<projectRoot>" ai peek point.config.local.json 'liteAppComponent[key=<点位key>]'
```
**代码里的 `propKey` 必须是 `point.config.local.json` 里已声明的,不是 AI 自创。**
⛔ **HARD-GATE**:写任何 `getProps()` / `getDataSourceResult()` / `notify()` / `watch()` 前 MUST 先跑 `lpm --cwd "<projectRoot>" ai peek point.config.local.json 'liteAppComponent[key=<点位key>]'` 把真实 `prop_key` 列表 echo 到上下文,**逐字 copy**——凭记忆/猜会跨 stage 不对齐运行时全 undefined。
⛔ **若 peek 输出空 / 文件不存在 / 找不到目标点位 key** → STOP 写代码,回 Stage Config 跑 `lpm local-config set` 声明组件。**严禁手写 `plugin.config.json` resources 数组或自填 `xxxxxx` 占位绕过 lpm CLI**——手写 propKey 不符 schema pattern `^[a-z0-9]{6}$`,schema 校验会拒。
## P1:读取要实现代码的 entry 文件
用 `lpm ai peek` 从 `plugin.config.json` 取 resources 数组(含每个点位的 id + entry):
```bash
lpm --cwd "<projectRoot>" ai peek plugin.config.json resources
```
然后 Read 所有 entry 文件(单文件体量小,Read 整份 OK)了解 CLI 已生成的代码结构、导出方式、初始化逻辑。多个 entry 互不依赖,支持并行 tool call 的 agent 一次发起;不支持的顺序 Read,结果相同。
## P2:查能力(按点位开发的真实动线)
### 2.1 定位点位类型
从 P1 拿到的 resources 列表里读 `id` 前缀即点位类型(如 `board_web_xxx` → `board`、`button_web_xxx` → `button`)。作为后续 2.2 的锚点。
**普适前置 — context 调用方式**:每个点位的 context 挂在自己 namespace 下(`button.getContext()` / `dashboard.getContext()` / `view.getContext()` …),**互不通用**——`page` 代码里调 `view.getContext()` 拿到 undefined,namespace 错了 tsc 过、运行时全 undefined。写依赖 context 的代码前 **MUST** 查清当前点位的 context 调用方式和返回字段:按下方 2.2 表 dispatch 走 doc;点位无 doc(如 `button` / `customField` / `componentSchedule` / `control` / `dashboard` / `intercept` / `listen_event`)则 MCP 关键词 `<点位类型> getContext 返回` / `<点位类型> 上下文` 当 Step 2 **硬前置查询**(不是下方「常见查询模板」里的可选项)。
**`aiNode` / `aiField` 例外**:AI 应用 webhook 业务逻辑在开发者自己后端,前端通常没有 JSSDK getContext 调用(仅 ai_node 开节点卡片时前端才需要——那条前端面 Read [`feature-point-types/ai_node/card.md`](feature-point-types/ai_node/card.md))。需要查协议时走 `lpm --cwd "<projectRoot>" schema` 拿 schema 路径 → `lpm --cwd "<projectRoot>" ai peek <schema-path> AINodePoint`(或 `AIFieldPoint`)切片读 description——schema description 即该点位的权威 doc,包含 webhook 协议、属性数据类型、字段类型约束、官方文档链接。
### 2.2 查询顺序(步骤化,不可跳步)
**三类合法信息源**(与 `../SKILL.md` 代码溯源协议对齐):
- **doc**(主参考源)= 点位标准能力 doc(`references/point-types/<点位类型>/`);`aiNode / aiField` 例外,走 schema description(`lpm --cwd "<projectRoot>" ai peek <lpm schema 输出路径> AINodePoint|AIFieldPoint`)
- **用户提供的代码**(被动源)= 用户在对话里粘贴的实现片段、现有工程代码,已在 context 中,不需要主动查询;写代码时按情况参考
- **MCP**(fallback)= 飞书项目知识 MCP,doc 未覆盖时关键词查功能→方法
主动查询动作只针对 doc 和 MCP,用户代码是被动参考,不占步骤。
**强制步骤**(缺任一步直接进 P3 = Stage Code 流程错误):
> **`aiNode` / `aiField` 例外**:跳过下方"强制步骤 1-4"(无 `point-types/<type>/index.md`,不要假装 Read)。直接 `lpm --cwd "<projectRoot>" schema` 拿 schema 路径 → `lpm --cwd "<projectRoot>" ai peek <schema-path> AINodePoint`(或 `AIFieldPoint`)切片读 description,读完进入 P3(schema description 自含 webhook 协议 / 属性数据类型 / 字段类型约束 / 官方文档链接,不需要 Step 2 MCP fallback)。**唯一例外:ai_node 开节点卡片(`needCustomCard=true`)的前端产物有专门 doc** → Read [`feature-point-types/ai_node/card.md`](feature-point-types/ai_node/card.md)(前端 JSSDK 面 + 数据流),不走本条 schema-only 流程。
1. Read 点位 doc 入口 `references/point-types/<点位类型>/index.md`
2. 输出「Phase 1 维度判定表」(AI 自判命中哪几条维度,不问用户)
3. 按命中维度 Read 对应 sub-doc 文件/章节
4. 输出「Phase 2 补全」(带实际章节号 + 关键字段结论)
5. doc 未覆盖的能力才走 Step 2 (MCP fallback);完全覆盖 → 跳到 P3
**1-4 步每步都是闸口,任一未完成不得进入第 5 步(走 Step 2 / 跳 P3)**——跳过任何一步 = 写出来的代码大概率运行时炸(tsc 过不代表对)。
---
**Step 1 — 点位标准能力 doc(开发起点)**
点位 doc 不是可选参考材料,是这个点位上**已知踩坑的场景集合**和**业务侧强约束的落地说明**——哪些场景必须靠特定 API 组合才能跑通、哪些 return 形态要按 doc 里说的解构、哪些 propKey/fieldKey 归属容易混、哪些订阅要配合 watch 当触发时机才能响应。这些都是真实踩出来并沉淀到 doc 的,跳过 doc 直接走 MCP 路线,基本就是在重走别人踩过的坑:tsc 能过、运行时必炸。
**进入 P2 后你的下一条 tool call MUST 是 Read `references/point-types/<点位类型>/index.md`**——不要先做其他事(不先查 MCP、不先写 Phase 1 判定表、不先 Edit 代码、不先跑任何 CLI)。没 Read index.md 就开干 = 执行错误。
Read 完 index.md 后,**由 AI 自判**(不要问用户)命中哪几条操作维度,产出一份「维度判定表」作为"AI 真的读了 doc" + "后面围绕哪些维度展开"的锚点。
**Phase 1 判定**(读 index.md 前就能写出来,不带 §章节号 / 不带 propType 细节):
```
本次需求维度判定(liteAppComponent)
────────────────
用户需求(AI 复述):<一句话>
[ ] 读组件属性:用户需求是否涉及"读组件自己被管理员配的任一值"?
[ ] 推输出属性:用户需求是否涉及"把数据暴露给下游/其他组件订阅"?
[ ] 响应属性变化:用户需求是否涉及"上游数据或管理员配置变了要实时响应"?
```
按**行为目的**判,不依赖字面关键词。一个需求常命中多条(如"展示上游工作项列表" = 读组件属性[data_source] + 响应属性变化)。维度→文件映射以及典型需求→维度的映射见 `index.md` 的"三条操作维度"和"需求→维度 的典型映射"两节。按命中条目 Read 对应文件/章节,不要预加载全部。
> **并行优化**:命中多条维度时,**在同一个 message 里用多个 Read tool call 并行 Read**(比如同时命中"读组件属性"和"响应属性变化" → 单 message 同时 Read `index.md` 和 `read-props.md`,不要串行等第一个 Read 完成再发第二个)。顺序读会把这一步从 1 轮变成 N 轮,耗时翻 N 倍。
**Phase 2 补全**(Read 完对应 doc 章节后回填):
**Phase 2 只补全 Phase 1 命中 `[x]` 的维度;未命中的维度直接省略,不占位、不留 `[ ]`。**
```
(仅示意:命中 3 条全勾时的样子)
[✓] 读组件属性
· prop_type 具体值:<写进 config 的值用 schema snake_case(text/number/select/boolean/multi_select/precise_date/date_range/work_item_instance/work_item_type/view_select/data_source);MCP `LiteAppPropTemplateType` 给的是 camelCase 运行时名,需映射回 snake_case 再写入>
· propType 类别:<基础值 / 数据源 / 引用型>(决定走 read-props.md §1 / §2 / §3)
· 若是数据源 / 实例 / 工作项类型:是否要读字段(`字段` toggle 状态):<是 / 否>
· 引用章节:read-props.md §<实际读到的章节号>
[✓] 推输出属性
· 输出 prop_type 具体值:<写进 config 用 schema snake_case,默认首选 `data_source`(推 moql 数据集);MCP `LiteAppSubscribedPropertyValueType` 把这种类型的运行时名叫 dataSet,**别把 dataSet 写进 config(会校验失败),config 值是 data_source**>
· 引用章节:write-outputs.md §<章节号>
[✓] 响应属性变化
· 引用章节:read-props.md §4
```
**Phase 2 未贴 = 进不了后续任何步骤**(不能进 Step 2、不能进 P3、不能写代码、不能 Edit 文件)。Phase 2 贴出来的章节号必须对应你**真的 Read 过的**章节,不是猜的——这是 AI 向自己和用户证明"doc 确实读了"的唯一凭据。
读 doc 过程中判断:用户想要的功能是不是这个点位标准能力覆盖的范围?代码能写但需要点位配置侧的开关/声明支持(典型如消费数据源字段要开 `字段` toggle),回流到 P0 的缺口模式处理,不要假设"配置默认开着"。
| 点位类型 | doc 路径 |
|---|---|
| `liteAppComponent` | `references/point-types/liteAppComponent/` 目录:先 Read `index.md`,按判定结果 Read `read-props.md` / `write-outputs.md` 中需要的章节;注意"响应属性变化"维度对应 `read-props.md §4`(同文件的小节,不是独立文件),三维度只有两个物理文件 |
| `dashboard`(Tab)| `references/point-types/dashboard/index.md` 单文件(3 维度:读工作项上下文 / 读工作项+字段数据 / 订阅数据变化;订阅维度 doc 未覆盖走 MCP)|
| `button` | `references/point-types/button/index.md` 单文件(两种点击后效果:弹窗 + `containerModal.configure/close` / 脚本模式;上下文字段走 MCP 查)|
| `component`(schedule)| `references/point-types/componentSchedule/index.md` 单文件(2 维度:读排期上下文 / 更新排期;更新排期 API doc 未覆盖走 MCP)|
| `control` | `references/point-types/control/` 目录:先 Read `index.md`,按场景分别 Read `form-control.md`(React 产物,覆盖表单 + 表格双击编辑 4 场景)/ `table-cell.md`(表格列展示态 DSL 基座 + `$fieldValue` 特供节)——表格列展示态**无代码**,必须走 DSL |
| `customField` | `references/point-types/customField/` 目录:先 Read `index.md`,命中"读写字段值 / 企业名片表单"维度 Read `value-shape.md`(含 field_key 映射 + 企业名片综合示例 + disabled/405 状态机);命中"表格列 DSL `$fieldValue` 展示"维度 Read [`feature-point-types/control/table-cell.md §3`](feature-point-types/control/table-cell.md)(和 control 共用的 DSL 基座 + customField 特供节)|
| `aiNode` / `aiField` | **配置 / webhook / OpenAPI 写回走 schema description**(无对应 point-types 目录):`lpm --cwd "<projectRoot>" schema` 拿 schema 路径 → `lpm --cwd "<projectRoot>" ai peek <schema-path> AINodePoint`(或 `AIFieldPoint`)切片读 description,含 webhook 协议、属性数据类型、字段类型约束、运行模型说明、官方文档链接(**不要 Read 整个 schema yaml**,peek 切片读)。**例外:ai_node 开节点卡片(`needCustomCard=true`)的前端代码** → Read [`feature-point-types/ai_node/card.md`](feature-point-types/ai_node/card.md)(前端 JSSDK 面 + 数据流编排;ai_field 无前端卡片)|
| `configuration` / `page` / `view` | `references/point-types/context-only.md`(Tier 0 仅上下文型合并速查;这三类点位标准能力极单薄,只给挂载位置 + getContext 返回;余下 API 走 Step 2 MCP fallback)|
| `intercept` / `listen_event` | **无 JSSDK**——这两个点位是**服务端 webhook**,本 skill 不生成此类产物代码;走 Step 2 MCP fallback(关键词:事件载荷协议 / 拦截响应协议 / 签名校验)|
**跨点位共享场景先查这里**:某些需求本质是"多个点位共享同一运行时上下文"(典型如**详情页表单联动** —— control / customField 挂同页时天生共享 form state)。这类 MCP 拼不出的跨点位心智 + 参与点位清单集中在 [`point-types/shared-scenes.md`](feature-point-types/shared-scenes.md),**涉及跨点位联动的需求先 Read 它**,再按它指示去 MCP 查具体 API 签名。
**Step 2 — doc 未覆盖或点位无 doc 时,查飞书项目知识 MCP**
用关键词查功能对应的方法——MCP 返回里通常就带方法定义(签名、参数、返回值说明),拿到即可写代码。不要再去找 SDK 原始 types 自行拼签名。
**调用动作**:MCP 工具由用户自行安装,名字不固定;在当前可见工具里挑带 `search_meegle_plugin_docs` 之类含义的那个调用即可。关键词按**功能场景**拟,不按 SDK 方法名猜(例:要"打开工作项详情页",搜 `打开工作项详情页`,不要猜 `openModal`)。
**常见查询模板**(按需求类型套公式,doc 不预置 API 签名):
| 需求 | 关键词公式 |
|---|---|
| 读当前点位上下文(getContext 返回了什么)| `<点位类型> 上下文` / `<点位类型> getContext 返回`(如 `button 上下文` / `dashboard getContext 返回`)|
| 基于上下文某个 id 加载真实数据 | `<业务对象> 加载 API`(如 `工作项 加载` / `工作项类型 加载`)|
| 触发平台动作(弹窗 / 跳转 / toast / 存储)| 按动作名称:`打开工作项详情` / `containerModal configure close` / `toast success` / `storage getItem` |
| 某个字段读写 / 错码清单 | `<namespace> <方法名>` / `<方法名> 错码` |
| 运行模式相关限制 | `<点位> 运行模式 脚本模式` / `<点位> 脚本 限制` |
MCP 也拿不到线索 → 走 shared 规则的"无源即停"停下问用户,不要凭经验直接写 `window.JSSDK.xxx`——这类猜测能过 tsc 但运行时全废。
**MCP 缓存协议**:见 [`shared.md`](shared.md) 「MCP 检索技巧」末尾。`.lpm-cache/` 由 CLI 自动管理(gitignore + 自动清理),AI 只写不清理。
### 2.3 冲突裁决(两源矛盾时用)
**优先级**:doc > MCP。doc 是业务踩坑沉淀,讲组合顺序和 return 形态;MCP 只讲单点方法定义。不一致时以 doc 为准,提醒用户可能 doc 过期或 MCP 片段断章。
跳过 doc 直接拼 API 是最常见的失败模式,踩过的坑:**把 propKey 当 fieldKey 用 / `getDataSourceResult` 忘了配 watch 刷新 / `notify` 推数组而非 moql**——这些方法签名完全合法、tsc 检查全过、运行时全挂。
## P3:逐调用确认来源(plan 阶段,不落代码注释)
P2 查完能力后、写代码前,对每段 SDK 调用确认它指回 P2 的哪个源(doc §章节 / MCP 场景 / 用户对话)。**这个确认只在 plan 阶段做,不要把来源写进代码**——生成的业务代码里不出现 `// source:` 之类指向 skill 内部 doc 路径的注释。
**涉及解构 return 形态**(如 `const { data, total } = await getDataSourceResult(...)`)**或多个 API 组合顺序**(如 `watch(cb)` 回调内调 `getDataSourceResult`)的调用,来源**必须能指向点位 doc § 章节**——MCP 片段不约束嵌套形态也不讲组合顺序,不能当依据。指不到对应 § → 回 P2 Step 1 补场景匹配 + Read 章节,不要硬写。
> 方法名是否真实存在,由 apply 阶段 `lpm ai audit-jssdk` 脚本兜底校验(见 [`feature-code-apply.md A1.5`](feature-code-apply.md))。
## P3.5:组件要 SDK 给不了的平台数据 → 走自建后端代理(记下"后端那一半")
P2 查能力时若发现:组件要的某个平台数据,当前点位的 SDK 上下文 / API 给不了(典型如要读"当前组件作用域之外的工作项 / 字段 / 视图数据",点位 context 没暴露)——按 `AUTHORING.md §8.1`,前端**不能裸调 OpenAPI**(鉴权 token 进前端 bundle = 公开发布凭据),只能 `fetch('/api/proxy/<resource>')`(相对路径)让自建后端持鉴权去调 OpenAPI 再返回。
发现这种情况时:
1. 在前端代码里用 `fetch('/api/proxy/<resource>')`(相对路径,不写任何 OpenAPI 域名 / 鉴权头)
2. 在 plan 产出里追加一份**代理路由清单**——每条路由:路径 + 请求参数 + 响应形态(前端期望解构出什么字段)
3. 标注"**这个 feature 需要后端那一半**" → 这清单会在 code.verify 收尾时(连同 webhook 形态点位)一起进 feature 末尾的「→ 后端那一半」,落进交接包给后端会话去实现这些路由
这一步只定**契约**(前端要哪些代理路由、出入参长什么样),不在这里写后端代码——后端代码由接手的后端会话写。
## P4:规划功能实现方案
**读取用户原始需求**:被 workflow phase 编排时用 `lpm --cwd "<projectRoot>" ai state get` 取 checkpoint 的 `context.originalRequirement`(输出为空 = 无 checkpoint;Phase 1 尾回录的版本,逐字保留、不受会话压缩影响);独立调用时从对话上下文读。
结合原始需求 + P2 查到的点位能力,做方案设计:
1. 分析用户需求涉及的 UI 组件、数据交互、状态管理
2. 基于 doc(必要时 + MCP)查询结果确认要用的 JSSDK API 和 FeatureContext 字段
3. 对照 P0 缺口模式检查是否有配置缺口(若有 → 回到 Stage Config 补齐)
4. 规划代码修改方案——在模板基础上需要新增/修改哪些部分
向用户确认实现方案后,进入 apply 阶段。