# 跨点位共享场景

某些业务场景**跨多个点位共享同一份运行时上下文**——这些心智 / 参与点位 / 边界 MCP 拼不出来。具体 API 签名走 MCP 查。

## onWorkItemFormValueChanged（5 点位共享的字段变化监听）

### 一句话核心心智

**该 API 监听的对象是"代码当前运行所在那个表单的 form state"**——5 个点位（button / tab / control / customField / aiNode）各有自家 namespace，签名 / 参数 / 返回完全一致；但能不能调通，取决于**当前代码所在的位置是不是一个能拿到 form state 的场景**。

判断标准（写代码前必看）：

- **表单场景**（详情页主表单 / 节点表单 / 新建页表单）→ 可用，监听同表单内的字段变化
- **挂在表单页面但自己不是表单项的位置**（详情页 button、详情页 Tab）→ 可用，监听该详情页主表单的字段变化
- **不属于表单场景的位置**（表格列展示态、表格双击编辑弹窗、视图批量按钮、新建按钮、WBS、字段配置弹窗等）→ **不可用**，没有 form state 可监听

### 5 点位 × 可用场景速查矩阵

| 点位 | namespace | 可用场景 | 不可用场景 | Web | Mobile |
|---|---|---|---|---|---|
| **button** | `window.JSSDK.button` | 详情页 3 位：工作项实例-更多 / 节点-更多 / 节点流转（监听所在详情页主表单的 state） | 新建按钮 / WBS 计划表-更多 / 视图批量操作（不在表单场景内） | ✅ | ❌ |
| **tab**（dashboard 点位） | `window.JSSDK.tab` | 详情页 Tab（监听所在详情页主表单的 state） | — | ✅ | ❌ |
| **control** | `window.JSSDK.control` | 详情页表单 / 节点表单 / 新建页表单 | 表格列展示态（DSL 无 JSSDK）/ **表格列双击编辑弹窗（产物虽是表单代码但场景不算表单）** | ✅ | V7.35.0+ |
| **customField** | `window.JSSDK.customField` | 同 control 表单 3 子场景 | 同 control + 字段配置弹窗（`FEATURE_CUSTOMFIELD_CONFIG` 入口）| ✅ | V7.42.0+ |
| **aiNode** | `window.JSSDK.aiNode` | AI 节点各场景通用 | — | ✅ | ✅ |

> **namespace 严格不串**：button 点位代码只能调 `window.JSSDK.button.*`，不能调 `window.JSSDK.tab.*` 或别家——平台按 namespace 路由 context，串调 runtime 报错。

> **表格列双击编辑弹窗是最易踩的坑**：它加载的就是表单 React 产物（前提是 Stage Config 同时勾选"表格列 + 表单"），代码逻辑共用 OK；但**那个弹窗本身不属于表单场景**——`onWorkItemFormValueChanged` 调进去拿不到 form state。表格弹窗里要拿初始值用 `getTableCellInitProps`（customField）或自己存（control），**不要走监听**。

### 完整签名

```ts
type WatchKey = {
  key: string | AttributeType;  // field_key（系统字段如 'priority' / 自建 'field_xxxxxx' hash）/ 'name' / 'template'
  type: Exclude<FieldType,
    | FieldType.richText
    | FieldType.singleSignal
    | FieldType.multiSignal
    | FieldType.singleVoting
    | FieldType.multiVoting
    | FieldType.simpleVoting
  >;
};

onWorkItemFormValueChanged(
  options: { watchKeys: WatchKey[] },        // 一次最多 10 个 key
  callback: (changedValue: Record<string, unknown>) => void,
): Promise<() => void>;                       // 返回 cleanup fn
```

### 5 条硬约束（写代码必须满足）

1. **一次最多 10 个 watchKey**——超出按需拆多次注册
2. **6 个 FieldType 不支持**：`richText` / `singleSignal` / `multiSignal` / `singleVoting` / `multiVoting` / `simpleVoting`——传了报错或回调不触发
3. **返回是 `Promise<() => void>`，两层异步**：先 `await` 拿到 cleanup fn，再在 React unmount 时调用——漏 cleanup → 内存泄漏 + 重复回调
4. **fieldKey 识别**：系统字段（`name` / `priority` / `owner` 等）/ 自建字段（`field_xxxxxx` hash）的识别走 [`liteAppComponent/read-props.md §2.4`](liteAppComponent/read-props.md) 2a/2b 两路
5. **FieldType 完整枚举值** 走 MCP fallback 关键词 `FieldType 枚举` 查

### 最小代码模板（用 control 做示例）

```ts
import { FieldType } from '@lark-project/js-sdk';

const watchKeys = [
  { key: 'priority', type: FieldType.singleSelect },
  { key: 'owner', type: FieldType.multiUser },
];

const off = await window.JSSDK.control.onWorkItemFormValueChanged(
  { watchKeys },
  (changed) => {
    console.log('字段变了:', changed);  // { priority: '...', owner: [...] }
  },
);

// React unmount 时
off();
```

**切换到其他点位**：把 `window.JSSDK.control.*` 改成 `window.JSSDK.button.*` / `window.JSSDK.tab.*` / `window.JSSDK.customField.*` / `window.JSSDK.aiNode.*` 即可，签名 / watchKeys / cleanup 行为一致。

### 各点位自家边界 / 代码片段入口

挂哪个点位，看自家 doc 的「监听其他字段变化」节：
- button → [`button/index.md`](button/index.md)「监听其他字段变化」
- tab（dashboard 点位）→ [`dashboard/index.md`](dashboard/index.md)「监听其他字段变化」
- control → [`control/form-control.md §3`](control/form-control.md)「联动其他表单项」
- customField → 表单场景按上面模板把 namespace 换成 `customField`；本字段自身值的读写见 [`customField/index.md`](customField/index.md) / [`customField/value-shape.md`](customField/value-shape.md)
- aiNode → 按上面模板把 namespace 换成 `aiNode`；卡片点位整体能力（getContext / getProps / watch + 数据流编排）见 [`ai_node/card.md`](ai_node/card.md)

### 边界（doc 未明走 MCP）

- **节点表单和详情页主表单是否共享同一个 form state** → MCP fallback 关键词 `节点表单 详情页 字段联动`
- **新建页表单是另一个独立 state**（工作项还没创建），和详情页主表单不共享
- **watchKey 超 10 上限的策略** → 按需拆多次注册

### 典型组合需求 → 选点位

| 需求 | 选 |
|---|---|
| 详情页字段 A 变 → 控件 B 展示/计算自动更新 | `control` + `onWorkItemFormValueChanged` |
| 详情页字段 A 变 → 存储值到平台的字段 B 自动更新 | `customField`（落库）+ `onWorkItemFormValueChanged` |
| 详情页字段 A 变 → Tab 内的统计 / 图表 / 关联工作项列表自动刷新 | `tab` + `onWorkItemFormValueChanged` |
| 详情页 button 点击前判断字段是否合法 | `button` 详情页场景 + `onWorkItemFormValueChanged`（mount 时注册，点击时读最新值） |
| 节点流转前服务端拦截判断多字段是否合法 | `intercept` 点位（服务端 webhook，不在本 skill 生成范围）|

---

## 数据获取（WorkItem.load / Context.load）

### 一句话核心心智

`WorkItem` / `Context` 是**全局 namespace**（`window.JSSDK.WorkItem.*` / `window.JSSDK.Context.*`），**不分点位**——任何能从自家 `getContext()` 拿到 `{spaceId, workObjectId, workItemId}` 的点位都能用它**主动读工作项实例数据 / 字段当前值**。

它和上面的 `onWorkItemFormValueChanged` 是**互补的两半**：

- `onWorkItemFormValueChanged` = 监听字段**变化**（只给变更后的增量，挂载时不给当前值）
- `WorkItem.load().getFieldValue()` = 读字段**当前值**（一次性快照）

典型配对：**挂载时 `WorkItem.load` 读一次当前值 + `onWorkItemFormValueChanged` 接后续增量**。只挂监听不读初值 → 打开页面时若该字段之后无变更，UI 会一直空白。

### WorkItem.load —— 读工作项实例数据 + 字段值

> source：飞书项目知识库 help `gvuo2oip`（《WorkItem》）。Web ✅ / Mobile V7.35.0+。

```ts
const workItem = await window.JSSDK.WorkItem.load({
  spaceId,       // string  ← 三件套均来自各点位 getContext()
  workObjectId,  // string  （OpenAPI 里叫 work_item_type_key）
  workItemId,    // number  ⚠️ 数字类型
});

// 返回的 workItem 上：
// workItem.id / name / spaceId / workObjectId / templateId
// workItem.isAborted / isDeleted: boolean       —— 实例是否终止 / 删除
// workItem.roleOwnersList: RoleOwners[]
// workItem.getRoleList(): Promise<Role[]>        —— 角色与人员列表
// workItem.getFieldValue(fieldId: string): Promise<any>  —— 读指定字段当前值

const value = await workItem.getFieldValue('priority');  // 以 fieldKey 取值
```

- **fieldKey 识别**：系统字段（`name` / `priority` / `owner` 等）/ 自建字段（`field_xxxxxx` hash）走 [`liteAppComponent/read-props.md §2.4`](liteAppComponent/read-props.md) 2a/2b 两路。
- **字段值的精确 JSON 形态**因字段类型而异（文本 / 单选 / 多选人员 / 日期 …）→ 不确定 `console.log` 实测，或 MCP 关键词 `字段与属性解析格式` 查。
- 移动端有版本门槛（V7.35.0+）；更低版本或要复杂查询走自建后端 OpenAPI。

### Context.load —— 读运行环境（语言等）

```ts
const { language } = await window.JSSDK.Context.load();  // 'zh_CN' | 'en_US' | 'ja_JP'；新增 locale 走 raw 兜底
```

用于 i18n 文案按用户语言取值。其余字段走 MCP 关键词 `Context load 返回` 查。

### 各点位入口

identity 来自各点位 `getContext()`。**liteAppComponent 例外**：多数场景用 `getDataSourceResult` 直出 `workitem.fields[fieldKey]`（见 [`liteAppComponent/read-props.md §2`](liteAppComponent/read-props.md)），不需要 `WorkItem.load`；dashboard / ai_node 卡片等"只有 ID"的场景才用 `WorkItem.load`。

---

## （预留）其他跨点位共享场景

以后如果出现其他"跨点位共享上下文" 类的场景（例如"视图上下文"等），加在本文下方章节。
