# 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 阶段。
