UNPKG

@lark-project/cli

Version:

飞书项目插件开发工具

269 lines (180 loc) 19.6 kB
# mode=plan:理解需求 + 交互补全 + 生成配置文件 ## 点位类型速查 | 类型 | 一句话用途 | |------|---------| | `customField` | 扩展字段类型(飞项一方字段类型的开发者 SDK 版;admin 装上后享筛/排/度量/流转条件/字段配置/布局配置/OpenAPI 读写)| | `control` | 自定义 UI 组件 / 面板 / 卡片(嵌表格列 / 详情页 / 节点表单 / 新建页;数据自管,平台不存)| | `liteAppComponent` | 轻应用组件(搭建器拖拽组件) | | `dashboard` | **工作项详情页 Tab**(绑定具体工作项实例,展示/组合该工作项数据;运行时 namespace 是 `tab`)| | `button` | 自定义按钮(工作项详情页 / 节点页)| | `component` | 节点排期组件(`component_type: schedule`;替换空间所有排期入口)| | `page` | 空间级独立全屏页(顶部/左侧导航扩展页,**不绑工作项**)| | `view` | 工作项列表自定义视图 | | `configuration` | 插件级配置入口 | | `intercept` | 状态流转拦截器(同步返回放行/拒绝)| | `listen_event` | 工作项事件异步监听(创建/更新/状态变更等)| > **必填字段 / 长度 / 枚举合法性以 `lpm schema` 输出为准**,由 `local-config set` 强制校验——避免静态镜像随 schema 迭代漂移。 > **平台命名不一致,别被带偏**:类型名 ≠ key 前缀 ≠ 运行时 namespace。`page` 类型 key 前缀却是 `board_`(叫 board 实为导航页,**不是**看板/仪表盘);`dashboard` 类型 key 前缀 `dashboard_`、运行时 namespace 是 `tab`(详情页 Tab)。 > **再分清 key vs resourceId**:点位 **key** 形如 `dashboard_xxxxxx`(无 `_web_`,schema 校验对象);**resourceId** 形如 `dashboard_web_xxxxxx`(带 `_web_` 端标记,是 `platform.web.resourceId.resource` + `src/features/<id>/` 目录名)。文档里满屏的 `board_web_xxx` / `dashboard_web_xxx` 都是 resourceId,**不是 key**。 ## P1:识别操作类型 + 粗扫候选点位类型 P1 做两件事: 1. 操作类型识别(增 / 改 / 删) 2. 粗扫候选点位类型——按文件顶部 [点位类型速查](#点位类型速查) 一句话用途表 + 下文 [术语→点位类型映射](#术语点位类型映射组件一词歧义) 表识别(不重述) **特例**:字段语义需求("字段值是 X" / "挂工作项上的字段")→ candidate 同时含 `customField` + `control`(二选,P2.5 决策树定)。 **特例**:词面歧义触发双候选——用户原话含"组件 / 控件 / 筛选器 / 卡片 / 看板"等点位类别字面通用词时(典型 trap:"筛选器**组件**" 既可 `control` 字段控件又可 `liteAppComponent` 独立组件),P1 必须输出 ≥2 候选,禁止单候选退化;进 P2.5 走 step 3 第三分支(其他双候选)拍板(**不**走 §P2.5.1,§P2.5.1 仅限 customField + control 对)。 **跨点位协作**(详情页表单联动 等)→ 加读 [`point-types/shared-scenes.md`](feature-point-types/shared-scenes.md)。 P1 **不做精细选型**(customField vs control 二选等),精细选型放到 P2.5(schema + candidate index.md 都读完后做)。 用户原话的读取方式: - **被 workflow phase 编排时**:用 `lpm --cwd "<projectRoot>" ai state get` 取 checkpoint 的 `context.originalRequirement`(输出为空 = 无 checkpoint;Phase 1 尾回录的版本,逐字保留、不受会话压缩影响) - **独立调用时**:从当前对话上下文读 输出:N 个独立需求 + 各自候选点位列表,等 P2 + P2.5 精细选型。 **MCP 缓存协议**:见 [`shared.md`](shared.md) 「MCP 检索技巧」末尾。`.lpm-cache/` 由 CLI 自动 gitignore + 在 `local-config set` / `publish` 成功后自动清理,AI 只写不清理。 ## P2:获取 Schema 和当前完整配置 ```bash lpm --cwd "<projectRoot>" schema lpm --cwd "<projectRoot>" local-config get --remote ``` - 两条命令都是**文件优先**:CLI 把结果写到 `.lpm-cache/schema/point-schema.json``.lpm-cache/config/remote.json`,stdout 只回一行相对路径 - **两个命令并行执行**,不要等第一个完成再跑第二个 - 若 `local-config get` 对应的远端为空,`remote.json` 内容是 `{}`,作为后续增改的基础 - 后续所有消费(切片、写 draft、diff 对比)都走文件,**不要整读 JSON 到 context** ### 按点位分片读取 schema `.lpm-cache/schema/point-schema.json`2400+ 行(~30K tokens),用 `lpm ai peek` 切片: ```bash # 取某个点位完整定义 + 自动追 $ref(BuilderLayout / PointKey 等子 schema 一次拿全) lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint # 选型阶段:列所有点位类型(一句话描述) lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json --index # 只要 description 字段(看 AI 行为指引,忽略 pattern/enum 校验噪音) lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint --descriptions-only # 不追 $ref(仅主 schema) lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint --no-follow ``` `lpm ai peek` 默认行为:结构模式自动取完整子树 + 追 `$ref` 闭包 + 循环检测,不会截断。**只取当前要操作的点位类型**,不要用 `--index` 当整读 schema 的借口。 **`--summary` / `--index` 的使用边界**(防止基于目录级信息下结论): - `--summary` / `--index` / `--list` 只用于**导航和选型**(决定"我要改哪个点位 / 用哪种类型") - 一旦确定了目标点位类型或 key,**落笔前必须再 peek 一次到具体符号**取完整定义: - 写配置 → `lpm --cwd "<projectRoot>" ai peek point-schema.json <PointName>` 取完整字段约束 - 写 key 级改动 → `lpm --cwd "<projectRoot>" ai peek remote.json 'data[point_type=xxx].point_config[key=yyy]'` 取完整当前值 - "我看了 summary 知道有这个点位""我知道这个点位的字段长什么样"——前者只够**选**,不够**改** ### 涉及 DSL 字段(table_cell / table_layout)的额外引导 触达以下字段时,schema 给的只是**字段约束**(type enum / style 字段 / 嵌套深度 / event action 枚举 等),**靠约束写不出有意义的 DSL 值**——DSL 的表达式语法(`{{...}}``$container` / `$colorTokens` / `$i18n` / `$fieldValue` 等平台变量、`data` 变量和数据接口返回字段的对应、事件 action 的 params 结构、色值多语言 token 的运行时行为)都在 MCP 里。 | 字段 | 归属点位 | |---|---| | `platform.web.table_cell` | `control`(scene 包含 1 时必填)| | `platform.web.table_layout` | `customField`(必填)| **MUST 先**走 MCP fallback 关键词 `开发表格页控件` / `表格列控件 DSL` 拉原文,按 MCP 缓存协议写到 `.lpm-cache/mcp/table-cell-dsl-spec.md`;**然后**对照 schema 的 `dslSchema` + MCP 里的使用示例 + 用户需求组合出合规 DSL。 - schema 告诉你**什么字段合法**(type 枚举 / style 支持哪些键 / 嵌套深度) - MCP 告诉你**怎么用才有意义**(表达式语法 / 平台变量含义 / data 变量对接数据接口的链路 / 事件 params 形态) **冲突裁决**:schema > MCP。字段命名不一致时以 schema 为准(例:schema 写 `$colorTokens.light`/`dark`,MCP 原文示例曾拼作 `lignt_mode`/`dark_mode`;写配置时用 schema 的拼写)。 **Mustache 表达式的能力边界**:schema 和 MCP 都只明确列出 5 类操作符(算术 `+-*/%` / 比较 `== != >= <= > <` / 三元 `x?y:z` / 属性访问 `x.y` / 数组访问 `a[i]`);**调 JS 方法**(`.toFixed()` / `.length` / `.slice()` / `.toUpperCase()` 等)两边都没说支持——**保守不用**,需要格式化就在**数据接口返回端预处理成字符串**(type: string 变量)或**走 MCP fallback** 进一步确认。 ### 按需读取 remote.json `.lpm-cache/config/remote.json` 是远端全量配置,同样用 `lpm ai peek````bash # 某类型下的某点位 lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/remote.json 'liteAppComponent[0].key' # 类型 + key 列表(diff / 删除确认最小视野) lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/remote.json --index ``` 或者 remote.json 文件较小时直接 Read 整文件也 OK(判断标准:空 `{}` 或 <10 个点位时整读)。 ### 术语→点位类型映射("组件"一词歧义) schema 中 `liteAppComponent`"轻应用组件"`component`"组件位",但用户日常口语里"组件"可以指任何东西。识别规则:**先从用户描述里抽核心名词(拓展字段 / 控件 / 轻应用 / 排期),再映射点位类型。"组件"二字是修饰语,不是判断依据。** | 用户可能的说法 | 核心名词 | 正确点位类型 | 易错映射 | |--------------|---------|------------|---------| | "拓展字段"/"拓展字段组件"/"字段模板"/"自定义字段" | **拓展字段** | `customField` | ~~liteAppComponent~~ | | "控件"/"控件组件" | **控件** | `control` | ~~liteAppComponent~~ | | "轻应用组件"/"概览组件"/"构建器组件"/"拖拽组件" | **轻应用/概览/构建器** | `liteAppComponent` | — | | "排期组件"/"节点排期" | **排期** | `component` | ~~liteAppComponent~~ | | "详情页 Tab"/"详情页加 Tab"/"标签页"/"详情页区块" | **详情页 Tab** | `dashboard` | ~~page~~(page 是空间级独立页、不绑工作项;详情页 Tab 必须绑工作项实例 → dashboard)| ## P2.5:精细点位选型 + 方案确认 **前置**:P2 已拉 schema + remote.json。 **主公式**(4 步): 1. **Read 候选点位 index.md**:含独立 doc 的 6 类(customField / control / liteAppComponent / dashboard / button / componentSchedule);其余 5 类(page / view / configuration / intercept / listen_event)走 [`code-plan.md`](feature-code-plan.md) Step 2 MCP fallback。**字段语义候选 customField + control 两份必读**。 2. **拉 schema 决策依据**:候选 = customField + control 是**唯一双候选选型边界**——必跑 `lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json ControlPoint` 拿完整 $comment(含完整能力对比表 + 三层决策树);其他点位单候选选型靠 index.md 即可。 3. **按候选数分支**: - **单候选** → "已选 [类型],理由:[X]。确认吗?" 等用户确认 - **候选 = customField + control 双候选** → 走 §P2.5.1,**不要走单候选话术** - **其他双候选**(如 P1 词面歧义触发的 control + liteAppComponent)→ echo 两候选并列:每个写一句话能力 + 利弊 + 倾向理由,等用户答 "确认 / 改 / 不知道";不要单候选话术绕过 4. **用户答完进 P3**——选型不对在这里 catch 比生成 draft 后再发现成本低。 ### P2.5.1:customField vs control 选型边界(特例分支) **触发**:P1 候选**同时含** customField + control 两份。其他点位选型**不进入此节**。 **为什么是特例**:customField vs control 是 schema 里唯一一对"语义重叠 + 多维度可换路径"的点位,需要"列双候选 + 多维度对比 + 用户拍板"。其他点位是单选,不需要这个开销。 **强制动作**(按顺序,每步缺失 = HARD 失败): 1. **echo schema $comment 完整能力对比表**(peek 拿到的内容逐字搬到本 plan 输出)——这是双候选 derive 路径差异的事实依据,不 echo 等于没看 2. **走 schema $comment 三层决策树**(Step 0 字面术语 → Step 1 独家信号 → Step 2 歧义停下问) 3. Step 0/1 命中 → 一行话术告知用户 "已选 [类型],理由:[命中信号原文]。确认吗?" + 等回复 4. **Step 2 命中 → 强制四动作**: - 扫题面 3 类信号:① 数据来源(CRM / ERP / 不落地)② 数据存储归属(落地飞项 vs 业务后端)③ 数据形态(表格列展示 / 表单交互)——3 类必扫,无命中写"无"。**端支持差异不在此列**(mobile 不展示两路径都能搞定,非真歧义) - 每条命中信号配 1 个对用户的关键决策问题(不漏) - 列两候选并列展开(基于完整能力对比表 derive 各自路径下针对本需求的实施 + 代价) - 全填完才向用户拍板 **铁律**(违反 = HARD): - 禁止预先表态选型("应选 customField" / "候选点位类型 customField" / "走 control 路径" 都算) - 必两候选并列展开(单个不算"列") - 3 类信号必扫完才能拍板 - 拍板前禁止写产物代码 ## P3:交互补全缺失字段 **规则:一次只问一个问题**,语义化表达(例:不说"key 是什么",而说"这个点位的唯一标识符打算用什么?建议格式如 my_board_point")。 **外部数据源识别**:补字段时遇用户提到外部系统(CRM / ERP / 自建后端 / oncall 值班人 / 任何非飞项内的数据源)→ stop-and-ask 数据来源:"这个值是飞项内字段还是外部系统?外部需自建后端代理调(前端不裸调外部 API,token 进 bundle = 公开发布凭据)"。**不要硬编默认值或编造 OpenAPI**。 ### 对于修改操作 1. `lpm ai peek` 定位目标点位(不要 Read 整文件):`lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/remote.json '<type>[key=<key>]'` 2. 仅询问需要修改的字段 3. 其余字段保持原值 ## P4:生成完整配置文件 **draft 不是从零写,而是从远端基线改出来的。** set 是全量替换(规则定义见 [`shared.md`](shared.md) 的"全量提交约束")——draft 必须是「远端全量配置 + 本次改动」,漏写任一现有点位 = 推送时永久删除它。 ### P4.0:从远端基线建 draft 起点(不可跳过) ```bash # 从 remote.json 一步建 draft:自动拼 draft-<ts>.json 文件名 + 路径校验,stdout 回显 draft 路径 DRAFT=$(lpm --cwd "<projectRoot>" ai init-draft) ``` `remote.json` 与 draft 同构(CLI 已反向转换成本地 DSL 形态),`init-draft` 直接把它快照成 draft 起点;远端为空时是 `{}`,draft 也从 `{}` 起。 **这一步让"保留所有现有点位"成为构造的天然结果**——下面的增 / 改 / 删都是**在这份 draft 副本上动具体条目**,不是凭记忆把全量 JSON 重新拼一遍。先判能不能改现有点位满足需求(改它、不新建);只有用户明确要删时,才从副本里移除——减少点位才会触发 A2/set 的删除闸口。 > 改 draft 用 `lpm ai patch-json <draft> --set-path/--add-path/--delete/--merge-path ...`:磁盘层按 path 精准动几处,draft 在盘上保持全量、context 里只装 delta,不必整读整写全量 JSON。下面三类操作即用它落地。 ### 添加操作(在副本上) 在对应类型数组中 append 新点位对象,**保留所有其他类型和其他点位不变**。 ``` 远端: { "page": [A], "button": [B1, B2] } 用户: 新增一个 liteAppComponent 结果: { "page": [A], "button": [B1, B2], "liteAppComponent": [新点位] } ``` 对于 Single 类型(page/view/dashboard/config/intercept/listen_event/component),如果远端已有该类型的点位,不能再添加(maxItems=1,schema 会拒绝)。应提示用户是要替换还是修改现有点位。 ### 修改操作(在副本上) 找到匹配 key 的点位,合并更新字段,**其余字段和其他点位原样保留**。 ``` 远端: { "button": [{ "key": "button_x1", "name": "旧名" }, { "key": "button_x2", ... }] } 用户: 把 button_x1 的名字改成"新名" 结果: { "button": [{ "key": "button_x1", "name": "新名" }, { "key": "button_x2", 原样 }] } ``` ### 删除操作(在副本上 · CRITICAL · 根原则 2 数据完整性 · config.plan 实施点) 从对应类型数组中移除匹配 key 的点位,**其余原样保留**。 ``` 远端: { "page": [A], "button": [B1, B2] } 用户: 删除 B2 结果: { "page": [A], "button": [B1] } 用户: 删除整个 button 类型的所有点位 结果: { "page": [A] } ``` (如果只传 `{ "liteAppComponent": [新点位] }` 而不包含 page 和 button,远端的 page 和 button 会被清除。) **删除确认规则**:draft 相比远端基线减少了任何点位时,先向用户列出被删除的点位清单(key + name)并获得确认,再执行 set——不要静默删除。 ### 生成前自检(仅语义级,schema 强校项让 A1 `lpm local-config set` 兜底) - [ ] **从副本改出**:draft 由 P4.0`remote.json` 副本编辑而来(含远端所有现有类型),非从零拼。只需复核没误删——而非"是否凑齐了全量" > 其他规则(key 唯一 / Single 类型 maxItems / 必填字段 / 枚举合法 / name 长度 / platform 字段类型限定)schema 强校,A1 阶段 lpm 拒就拒,按 [`config-apply.md`](feature-config-apply.md) §A1「错误分类处理」修复重跑。AI **不要在 P4 复述**——校两次浪费 token。 ## P5:判定前端点位 / webhook 形态点位(结构性,下游 stage 用) 从这次 draft(+ 远端基线)的点位 config 里,机械判出两组——下游 Stage Code 用它决定要不要跑、(可能的)后端那一半用它知道要接哪些 webhook 端点: - **有前端的点位** = 在 `plugin.config.json` 里有 `resource` / `entry` 的点位(渲染点位)。这组非空 → Stage Code 要跑;这组为空 → 这个 feature 没有前端渲染点位,Stage Code 整段跳过。 - **webhook 形态的点位**(= 一定有后端): | 点位类型 | webhook url/token 在哪 | |---|---| | `ai_node` / `ai_field``app_type=ai_node` / `ai_field` 工程)| url/token 在一个 `listen_event` extension 里 | | `intercept` / `listen_event` | 点位顶层有 `url`(+ `token`)| | `control`(嵌新建页等场景)| 点位顶层 `control[].url` | `ai_node` 可两半都有(节点卡片 resource + 算结果的 webhook);`ai_field` 通常只后端那半。这组非空 → 这个 feature 需要后端那一半(Stage Config 之后走 feature 末尾的「→ 后端那一半」产交接包)。 webhook 形态点位的 url/token 在 Stage Config 这步可以先填占位 / localhost——真公网地址在后端那一半的联调收口(第 2`local-config set`)才指过去。 ### P5.1:后端可行性粗核(可选,命中 webhook 形态点位时) 写前端前可选跑 `lpm perm check --apis <按业务意图粗判的接口中文名>` 核一次:两类工程都极少"做不了"(AI 应用恒 `satisfied``normal``needApply` 可申请、不阻塞),唯一要 catch 的是 `unknown`(接口名写错 → 重查;此 app 确实没有 → 停下告知用户)。完整 perm 核对在「→ 后端那一半(relay)」A2 确认权限就绪,这里不替代。 ## P6:输出 plan 阶段完成后输出: - 生成的配置文件路径:`.lpm-cache/config/draft-{timestamp}.json` - 变更摘要:添加/修改/删除了哪些点位(类型 + key) - 前端 / 后端判定:有前端的点位有哪些(→ Stage Code 是否跑)、有 webhook 形态点位吗(→ 是否需要后端那一半) > 中间产物(`.lpm-cache/config/remote.json` / `draft-*.json` / `schema/point-schema.json` / `mcp/*.md`)由 CLI 在 `local-config set` / `publish` 成功后自动清理,skill 不负责清理。