# 操作类动作表单确认中断恢复设计

## 1. 背景

`datacloud-analysis` 已经支持通过 ReAct 工具调用完成数据查询和数据操作。查询类动作可以直接执行，但新增、更新、删除、知识库写入等操作类动作具有真实副作用，不能让 Agent、MCP 或门户调用绕过用户确认。

本设计目标是在现有 LangGraph 中断恢复链路上扩展一类新的人工输入场景：操作表单确认。Agent 在执行操作工具前返回结构化表单，前端展示表单并采集用户确认信息；确认信息通过 LangGraph resume 回到 Agent 后，图从 checkpoint 恢复，重放原工具调用，并在恢复后的工具执行阶段完成真实业务提交。

## 2. 设计目标

- 操作类动作在执行前统一返回表单给用户确认。
- 表单 payload 支持多表单列表，但不新增独立列表容器；`rule` 使用二维数组表达多组表单字段。
- 表单字段支持普通输入、统一术语选择、默认文件等。
- 表单字段支持对象和对象数组；复杂字段不返回 `fieldValue`，统一使用 `children` 二维字段数组表达。
- 字段绑定术语时，表单只返回术语元信息，不区分枚举术语和列表术语。
- 前端遇到术语字段时，默认调用统一术语选项接口分页检索候选。
- 用户确认信息通过 LangGraph `Command(resume=...)` 恢复，继续原 Agent 执行流程。
- 中断恢复必须支持进程重启、checkpoint 重放、重复路径防护和幂等执行。
- Hook 层必须校验确认信息并在恢复执行时注入确认标记，防止 Agent 正常链路内绕过表单确认。
- SDK 执行层强制校验 `userConfirmed` 和 `_operationConfirm` 作为后续增强项；当前实现保持既有 SDK 行为，避免影响已有操作类动作测试和外部调用兼容性。

## 3. 适用范围

需要表单确认的动作：

- `OntologyAction.action_type == "operation"` 的非虚拟动作。
- 动态表写操作：`action_family in {"insert", "update", "delete"}`。
- 知识库写入操作：`action_family == "write"`。
- 后续可按动作标签扩展，如 `tags` 中包含 `requires_confirmation`、`high_risk` 等。

不需要表单确认的动作：

- 只读查询动作。
- 已由当前 Agent 中断上下文确认过的恢复调用。
- 系统内部不可见动作或明确配置为无需确认的低风险动作。

## 4. 总体架构

复用现有查询澄清链路，不新增独立图分支：

```text
LLM 生成操作工具调用
  -> OperationConfirmationPlugin.before_call_back()
  -> 构建 operation_form
  -> 抛 ClarificationNeededError(interrupt_type="operation_form")
  -> HookAwareToolNode / tool_dispatcher 捕获
  -> Command(goto="analyze_clarify")
  -> analyze_clarify_node 识别 operation_form，跳过查询澄清 SDK，直接透传表单
  -> user_clarify_node 调用 interrupt(operation_form)
  -> 图暂停，checkpoint 持久化
  -> 前端采集用户确认信息
  -> Command(resume=operation_confirm_result)
  -> user_clarify_node 写入 clarification_formatted_params
  -> 路由回 tools，重放原 tool call
  -> OperationConfirmationPlugin 读取确认参数并 patch tool_params
  -> Action.execute() 二次校验后执行真实操作
```

核心原则：

- `interrupt_type` 区分不同人工输入场景，查询澄清使用原流程，操作确认使用表单流程。
- `pending_clarification_context` 保存中断上下文，由 LangGraph checkpoint 持久化。
- `clarification_formatted_params` 保存恢复后的确认参数，由 hook 在工具重放时消费。
- `Action.execute()` 保留执行前二次校验，防止绕过 Agent。

## 5. 表单协议

### 5.1 顶层结构

```json
{
  "schemaVersion": "1.0",
  "pluginAppId": "string",
  "pluginMachineId": "string",
  "formId": "op_form_20260525120000_xxx",
  "actionCode": "update_customer_status",
  "actionName": "修改客户状态",
  "title": "待确认操作",
  "description": "请确认以下表单信息，确认后将继续执行。",
  "rule": [
    [
      {
        "itemId": "op_item_20260525120000_001",
        "formType": "input",
        "fieldCode": "customerId",
        "fieldPath": "customer.customerId",
        "fieldName": "客户 ID",
        "fieldType": "string",
        "fieldValue": "C001",
        "required": true,
        "readonly": true
      },
      {
        "formType": "term_select",
        "fieldCode": "status",
        "fieldPath": "customer.status",
        "fieldName": "状态",
        "fieldType": "string",
        "fieldValue": "TODO",
        "required": true,
        "term": {
          "termSet": "status.code",
          "termTypeCode": "status",
          "termField": "code",
          "datasetId": 100
        },
        "readonly": false
      }
    ],
    [
      {
        "itemId": "op_item_20260525120000_002",
        "formType": "input",
        "fieldCode": "customerId",
        "fieldPath": "customer.customerId",
        "fieldName": "客户 ID",
        "fieldType": "string",
        "fieldValue": "C002",
        "required": true,
        "readonly": true
      },
      {
        "formType": "term_select",
        "fieldCode": "status",
        "fieldPath": "customer.status",
        "fieldName": "状态",
        "fieldType": "string",
        "fieldValue": "TODO",
        "required": true,
        "term": {
          "termSet": "status.code",
          "termTypeCode": "status",
          "termField": "code",
          "datasetId": 100
        },
        "readonly": false
      }
    ]
  ]
}
```

字段说明：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `schemaVersion` | string | 是 | 表单协议版本 |
| `pluginAppId` | string | 否 | 插件应用 ID |
| `pluginMachineId` | string | 否 | 插件机器 ID |
| `formId` | string | 是 | 本次中断的表单 ID |
| `actionCode` | string | 是 | 本体动作编码；同一确认表单内所有表单项共享 |
| `actionName` | string | 否 | 本体动作名称 |
| `title` | string | 是 | 表单标题，用于确认表单展示 |
| `description` | string | 否 | 表单说明 |
| `rule` | array | 是 | 二维表单字段数组，第一层表示表单项列表，第二层表示该表单项的字段列表 |

表单协议只描述需要渲染和确认的业务字段，不承载提交地址、HTTP 方法、确认令牌或过期时间：

- `submitUrl`、`method` 属于 Agent 外层通信协议，由调用方接入层统一处理。
- 过期时间、原始参数哈希、确认状态等安全信息保存在服务端 `pending_clarification_context` 或 checkpoint 关联上下文中。
- 单条表单也使用二维数组，此时 `rule` 长度为 1。
- 每个内层数组是一组表单字段，字段属性和值都在字段对象中。
- `itemId` 是表单项标识，不是字段标识；由于协议不额外增加表单项容器，因此固定放在每个 `rule[i][0]` 上。
- 同一个 `rule[i]` 内的其他字段不需要重复携带 `itemId`。
- 前端只需要把 `formId`、顶层 `confirmed`、可选 `reason` 和用户确认后的二维 `rule` 作为 resume 数据传回 Agent。

### 5.2 字段结构

以下结构描述通用字段。若该字段是某个二维字段数组中的第一列字段，即 `rule[i][0]` 或复杂字段 `children[i][0]`，则需要额外携带 `itemId` 作为该条记录标识。

```json
{
  "formType": "term_select",
  "fieldCode": "status",
  "fieldPath": "customer.status",
  "fieldName": "状态",
  "fieldType": "string",
  "fieldValue": "TODO",
  "description": "本次需要确认的客户状态",
  "required": true,
  "readonly": false,
  "disabled": false,
  "isHidden": false,
  "defaultFiles": [],
  "term": {
    "termSet": "status.code",
    "termTypeCode": "status",
    "termField": "code",
    "datasetId": 100
  }
}
```

字段说明：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `formType` | string | 是 | 表单控件类型；复杂字段仅 `fieldType == "object"` 时为 `object`，仅 `fieldType == "array<object>"` 时为 `array`；术语字段为 `term_select`；带 `optional` 的普通字段为 `select`；其余字段按 `fieldType` 精确映射为 `input`、`number`、`checkbox` 或 `array` |
| `fieldCode` | string | 是 | 参数编码，对应动作参数 `param_code` |
| `fieldPath` | string | 否 | 字段路径，嵌套字段使用点号路径，如 `customer.address.city` |
| `fieldName` | string | 是 | 参数名称，对应动作参数 `param_name` |
| `fieldType` | string | 是 | 数据类型，如 `string`、`integer`、`number`、`boolean`、`array<string>`、`array<object>`、`object` |
| `fieldValue` | any | 否 | 当前待确认值；仅标量字段和普通数组字段使用，`object` / `array<object>` 字段不返回该字段 |
| `children` | array | 否 | 对象或对象数组字段的子字段二维数组，结构与顶层 `rule` 一致 |
| `description` | string | 否 | 字段说明 |
| `required` | boolean | 是 | 是否必填 |
| `readonly` | boolean | 否 | 是否只读 |
| `disabled` | boolean | 否 | 是否禁用 |
| `isHidden` | boolean | 否 | 是否隐藏 |
| `defaultFiles` | array | 否 | 默认文件列表 |
| `term` | object | 否 | 术语绑定元信息；存在时前端默认渲染术语选择控件 |

字段层级规则：

- `fieldValue` 只表示当前字段自己的简单值；可编辑标量字段使用标量值，普通数组字段使用普通数组值。
- 当字段本身是 `object` 或 `array<object>` 且需要前端按对象表单渲染时，不返回 `fieldValue`，统一使用 `children` 承载子字段结构。
- `children` 是二维字段数组，结构与顶层 `rule` 一致。
- `object` 字段只有一条数据，因此 `children` 固定为 `[[...fields]]`；`array<object>` 字段可以有多条数据，因此 `children` 为 `[[...fields], [...fields]]`。
- 普通数组字段如果元素不是对象，`fieldValue` 仍使用普通数组值，如 `["A", "B"]`，不转换为二维字段数组。
- 普通数组字段如 `array<string>` 不使用 `formType: "array"`，默认按 `input` 渲染；如果字段有 `optional`，按 `select` 渲染。
- 数字和布尔字段保留专用控件语义：`fieldType == "number"` 或 `integer` 时 `formType` 为 `number`，`fieldType == "boolean"` 时 `formType` 为 `checkbox`。
- `optional` 表示普通字段的可选值集合；存在 `optional` 且不是术语、对象、对象数组字段时，`formType` 固定为 `select`。
- `fieldPath` 用于唯一定位字段；未传时默认等于当前层级下的 `fieldCode`。
- 如果对象内部属性需要展示、编辑、绑定术语或校验，后端应将该属性展开为复杂字段 `children` 内的独立字段，并通过 `fieldPath` 标识路径。
- 如果动作返回对象列表且需要作为多份主表单确认，后端必须将列表拆成多个顶层 `rule[i]`；每个 `rule[i]` 表示一个对象对应的表单项。
- `rule` 第一层表示表单项列表，第二层表示字段列表；表单列表不再增加额外数据容器。
- `rule` 内不再单独提供表单项 `title`。表单项展示标题由前端基于表单顶层 `title`、`actionName` 和字段值组合生成。
- 每个二维字段数组的内层数组第一个字段对象必须携带 `itemId`，用于恢复匹配、幂等和审计。
- 顶层表单项的 `itemId` 只在 `rule[i][0]` 出现；复杂字段记录的 `itemId` 只在 `children[i][0]` 出现；同组其他字段不重复携带。
- 复杂字段记录的 `itemId` 只在该复杂字段内部唯一即可，不能替代顶层表单项 `itemId`。

表单项元信息字段说明：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `itemId` | string | 是 | 表单项 ID，用于恢复匹配、幂等和审计 |

对象字段值示例：

```json
{
  "itemId": "op_item_customer_001",
  "formType": "object",
  "fieldCode": "customer",
  "fieldPath": "customer",
  "fieldName": "客户信息",
  "fieldType": "object",
  "children": [
    [
      {
        "itemId": "op_item_customer_001",
        "formType": "input",
        "fieldCode": "customerId",
        "fieldPath": "customer.customerId",
        "fieldName": "客户 ID",
        "fieldType": "string",
        "fieldValue": "C001",
        "required": true,
        "readonly": true
      },
      {
        "formType": "input",
        "fieldCode": "customerName",
        "fieldPath": "customer.customerName",
        "fieldName": "客户名称",
        "fieldType": "string",
        "fieldValue": "示例客户",
        "required": true
      },
      {
        "formType": "term_select",
        "fieldCode": "status",
        "fieldPath": "customer.status",
        "fieldName": "状态",
        "fieldType": "string",
        "fieldValue": "TODO",
        "required": true,
        "term": {
          "termSet": "status.code",
          "termTypeCode": "status",
          "termField": "code",
          "datasetId": 100
        }
      }
    ]
  ],
  "readonly": false,
  "required": true
}
```

对象数组字段值示例：

```json
{
  "itemId": "op_item_contacts_001",
  "formType": "array",
  "fieldCode": "contacts",
  "fieldPath": "customer.contacts",
  "fieldName": "联系人列表",
  "fieldType": "array<object>",
  "children": [
    [
      {
        "itemId": "op_item_contact_001",
        "formType": "input",
        "fieldCode": "name",
        "fieldPath": "contact.name",
        "fieldName": "联系人姓名",
        "fieldType": "string",
        "fieldValue": "张三",
        "required": true
      },
      {
        "formType": "input",
        "fieldCode": "phone",
        "fieldPath": "contact.phone",
        "fieldName": "手机号",
        "fieldType": "string",
        "fieldValue": "13800000000",
        "required": true
      }
    ],
    [
      {
        "itemId": "op_item_contact_002",
        "formType": "input",
        "fieldCode": "name",
        "fieldPath": "contact.name",
        "fieldName": "联系人姓名",
        "fieldType": "string",
        "fieldValue": "李四",
        "required": true
      },
      {
        "formType": "input",
        "fieldCode": "phone",
        "fieldPath": "contact.phone",
        "fieldName": "手机号",
        "fieldType": "string",
        "fieldValue": "13900000000",
        "required": true
      }
    ]
  ],
  "readonly": false,
  "required": false
}
```

对象列表拆成多份主表单示例：

```json
{
  "rule": [
    [
      {
        "itemId": "op_item_contact_001",
        "formType": "input",
        "fieldCode": "name",
        "fieldPath": "contact.name",
        "fieldName": "联系人姓名",
        "fieldType": "string",
        "fieldValue": "张三",
        "required": true
      },
      {
        "formType": "input",
        "fieldCode": "phone",
        "fieldPath": "contact.phone",
        "fieldName": "手机号",
        "fieldType": "string",
        "fieldValue": "13800000000",
        "required": true
      }
    ],
    [
      {
        "itemId": "op_item_contact_002",
        "formType": "input",
        "fieldCode": "name",
        "fieldPath": "contact.name",
        "fieldName": "联系人姓名",
        "fieldType": "string",
        "fieldValue": "李四",
        "required": true
      },
      {
        "formType": "input",
        "fieldCode": "phone",
        "fieldPath": "contact.phone",
        "fieldName": "手机号",
        "fieldType": "string",
        "fieldValue": "13900000000",
        "required": true
      }
    ]
  ]
}
```

兼容说明：

- 原结构中的 `optional: ["string"]` 表达能力不足，不建议继续作为术语选项承载字段。
- 字段级不再返回 `options`、`remoteOptions`、`optionUrl`、`labelField`、`valueField`。
- 对象和对象数组字段不返回 `fieldValue`，统一通过 `children` 二维字段数组承载子字段；对象列表主表单由顶层二维 `rule` 承载。
- 术语选项接口、请求参数名和响应字段名使用全局固定约定，前端无需从每个字段中读取。
- 表单字段不再返回 `defaultValue`。前端回传用户确认后的字段结构；标量和普通数组字段回传 `fieldValue`，对象和对象数组字段回传 `children`。

## 6. 参数转换设计

用户可以修改表单值，因此必须提供双向转换：

```text
Action 参数定义 + LLM 生成参数
  -> build_operation_form()
  -> operation_form.rule
  -> 用户修改 fieldValue / children 并确认
  -> restore_action_params()
  -> Action.execute(params)
```

### 6.1 Action 参数转换为表单

`OperationConfirmationPlugin` 在首次拦截操作类动作时执行表单构建。当前实现优先读取 `OntologyAction.input_schema` 构建字段；动态表 `insert_*` 的 `records` 会拆成顶层多条 `rule[i]`，每条记录是一份主表单。

1. 读取动作参数定义，包括 `param_code`、`param_name`、类型、必填、只读、术语绑定、文件配置等。
2. 读取 LLM 已生成的 `tool_params`，按参数定义做默认值合并和类型归一化。
3. 为每个需要展示、确认或编辑的参数生成字段对象：
   - `fieldCode` 来自 `param_code`。
   - `fieldName` 来自 `param_name`。
   - `fieldPath` 为参数回写路径，简单参数默认等于 `fieldCode`，嵌套参数使用点号路径。
   - `fieldType` 来自动作参数类型。
   - `formType` 由 `fieldType`、术语绑定和 `optional` 共同推导：`object` / `array<object>` 使用复杂表单，术语字段使用 `term_select`，有 `optional` 的普通字段使用 `select`，数字/布尔字段保留 `number` / `checkbox`，其余普通字段使用 `input`。
   - 标量和普通数组字段的 `fieldValue` 使用当前待确认值，优先来自 LLM 参数，其次来自动作默认值。
4. 如果参数绑定术语，只写入 `term` 元信息，不写入枚举列表或远程选项地址。
5. 如果参数是 `object` 或 `array<object>` 且需要作为复杂表单编辑，不写入 `fieldValue`，改用 `children` 二维字段数组。
6. 如果操作一次生成多条主记录，顶层 `rule` 拆成多个表单项；每个表单项的 `rule[i][0]` 写入 `itemId`。
7. 动态表字段术语元信息从动作所属对象字段元数据补齐；如果 schema 字段自身已包含 `term`，优先使用 schema 中的术语配置。

示例：

```json
{
  "tool_params": {
    "customer": {
      "customerId": "C001",
      "status": "TODO"
    }
  },
  "rule": [
    [
      {
        "itemId": "op_item_20260525120000_001",
        "formType": "input",
        "fieldCode": "customerId",
        "fieldPath": "customer.customerId",
        "fieldName": "客户 ID",
        "fieldType": "string",
        "fieldValue": "C001",
        "readonly": true,
        "required": true
      },
      {
        "formType": "term_select",
        "fieldCode": "status",
        "fieldPath": "customer.status",
        "fieldName": "状态",
        "fieldType": "string",
        "fieldValue": "TODO",
        "required": true,
        "term": {
          "termSet": "status.code",
          "termTypeCode": "status",
          "termField": "code",
          "datasetId": 100
        }
      }
    ]
  ]
}
```

### 6.2 表单恢复为 Action 参数

`user_clarify_node` 收到 resume payload 后不直接执行业务提交，只把确认结果转换为 `clarification_formatted_params`。工具重放时，`OperationConfirmationPlugin` 再将其恢复成 `tool_params`：

1. 校验 `interrupt_type == "operation_form"`、`formId` 和服务端中断上下文一致。当前实现已校验中断类型并按 `tool_name` 匹配恢复参数；`formId`、`itemId`、用户和过期时间的强校验作为后续增强。
2. 如果顶层 `confirmed == false`，写入取消结果，不再恢复业务参数。
3. 遍历二维 `rule`，按 `fieldPath` 将字段值回写到参数对象；标量和普通数组字段读取 `fieldValue`，对象和对象数组字段读取 `children`。
4. 递归处理复杂字段：
   - `fieldType == "object"`：读取 `children[0]`，组装为对象。
   - `fieldType == "array<object>"` 且 `children` 是二维字段数组：遍历 `children[i]`，组装为对象数组。
   - `fieldType` 为普通数组类型，如 `array<string>`，且 `fieldValue` 是普通数组：直接按普通数组写入。
5. 对术语字段执行服务端二次解析和校验，确保最终值符合 `term.termField` 要求。当前实现负责透传术语元信息和恢复字段值，最终术语解析仍由既有 SDK/执行层承担。
6. 对只读字段执行防篡改校验；前端回传值必须与服务端中断上下文中的原始值一致。当前实现暂未强制只读防篡改。
7. 过滤不在原表单 schema 中的字段，避免用户注入额外业务参数。当前实现有 action metadata 时按顶层 schema 过滤，无 loader/action metadata 时按表单字段恢复。
8. 注入服务端确认标记 `_operationConfirm` 和 `userConfirmed`，交给 `Action.execute()` 二次校验。

恢复示例：

```json
{
  "confirmed": true,
  "rule": [
    [
      {
        "itemId": "op_item_20260525120000_001",
        "fieldCode": "customerId",
        "fieldPath": "customer.customerId",
        "fieldValue": "C001"
      },
      {
        "fieldCode": "status",
        "fieldPath": "customer.status",
        "fieldValue": "DISABLED"
      }
    ]
  ],
  "params": {
    "customer": {
      "customerId": "C001",
      "status": "DISABLED"
    },
    "userConfirmed": true,
    "_operationConfirm": {
      "formId": "op_form_20260525120000_xxx",
      "itemId": "op_item_20260525120000_001",
      "confirmed": true
    }
  }
}
```

转换规则：

| 场景 | 表单结构 | 恢复规则 |
|------|----------|----------|
| 标量字段 | `fieldValue = "C001"` | 按 `fieldPath` 写入标量 |
| 普通数组字段 | `fieldValue = ["A", "B"]` | 按 `fieldPath` 写入数组 |
| 对象字段 | `children = [[...fields]]` | 读取第一组字段，组装为对象 |
| 对象数组字段 | `children = [[...fields], [...fields]]` | 每组字段组装为一个对象 |
| 顶层多表单 | `rule = [[...fields], [...fields]]` | 每个 `rule[i]` 按 `itemId` 匹配服务端上下文 |
| 术语字段 | `term + fieldValue` | 服务端解析为合法 code/name 后写入 |
| 只读字段 | `readonly = true` | 校验未被篡改后写入或忽略前端值 |
| 隐藏字段 | `isHidden = true` | 不信任前端值，优先使用服务端上下文原值 |

## 7. 术语字段处理

动作参数已有术语元信息：

- `term_set`
- `term_type`
- `term_field`
- `dataset_id`

表单协议对前端不暴露“枚举/列表”的差异。只要字段包含 `term`，前端就按统一术语选择控件处理，并调用统一术语选项接口。

后端可以继续保留 `term_type` 或 `termMasterType`，用于缓存、性能优化、校验和默认检索策略，但这属于服务端实现细节，不进入表单字段协议。

字段中的术语元信息：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `termSet` | string | 是 | 术语集，如 `status.code` |
| `termTypeCode` | string | 是 | 术语类型编码，如 `status`；可由 `termSet` 前缀推导 |
| `termField` | string | 否 | 确认值对应的术语字段，通常为 `code` 或 `name` |
| `datasetId` | integer | 否 | 数据集 ID |

### 7.1 字段示例

```json
{
  "formType": "term_select",
  "fieldCode": "status",
  "fieldName": "状态",
  "fieldType": "string",
  "fieldValue": "TODO",
  "required": true,
  "term": {
    "termSet": "status.code",
    "termTypeCode": "status",
    "termField": "code",
    "datasetId": 100
  }
}
```

### 7.2 统一术语选项接口

前端遇到 `term` 后，统一调用固定术语选项接口。接口地址和字段映射不随表单字段返回，作为前后端全局协议约定。

接口定义：

```text
POST /api/datacloud/terms/options
```

用途：

- 给操作确认表单中的 `term_select` 字段提供候选项。
- 同时支持枚举术语和列表术语，前端不需要区分术语类型。
- 支持关键字检索和分页加载。

请求体参数：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `termSet` | string | 是 | 表单字段 `term.termSet` |
| `termTypeCode` | string | 是 | 表单字段 `term.termTypeCode` |
| `termField` | string | 否 | 表单字段 `term.termField`，用于指定 `value` 应返回 `code` 还是 `name` |
| `datasetId` | integer | 否 | 表单字段 `term.datasetId` |
| `keyword` | string | 否 | 搜索关键字 |
| `page` | integer | 否 | 页码，从 1 开始 |
| `pageSize` | integer | 否 | 每页数量 |

请求示例：

```http
POST /api/datacloud/terms/options
Content-Type: application/json
```

```json
{
  "termSet": "status.code",
  "termTypeCode": "status",
  "termField": "code",
  "datasetId": 100,
  "keyword": "停",
  "page": 1,
  "pageSize": 20
}
```

响应字段固定为 `label` 和 `value`，前端只依赖这两个字段渲染和提交；其它字段用于调试、展示补充或后端排查：

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "items": [
      {
        "label": "停用",
        "value": "DISABLED",
        "code": "DISABLED",
        "name": "停用",
        "metadata": {
          "sort": 20,
          "enabled": true
        }
      }
    ],
    "page": 1,
    "pageSize": 20,
    "total": 1,
    "hasMore": false
  }
}
```

`data.items[]` 字段说明：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `label` | string | 是 | 前端展示文本 |
| `value` | string | 是 | 前端提交值；由 `termField` 决定，通常为标准 code |
| `code` | string | 否 | 术语编码，便于调试和兜底 |
| `name` | string | 否 | 术语名称，便于调试和兜底 |
| `metadata` | object | 否 | 扩展信息，如排序、部门、是否可用等 |

兼容说明：如果接入层不使用统一响应包，也可以直接返回分页对象：

```json
{
  "items": [
    {
      "label": "停用",
      "value": "DISABLED",
      "code": "DISABLED",
      "name": "停用",
      "metadata": {}
    }
  ],
  "page": 1,
  "pageSize": 20,
  "total": 1,
  "hasMore": false
}
```

字段取值规则：

- `label` 优先使用术语名称；缺失时回退到 `code`。
- `value` 根据 `termField` 返回：`termField == "name"` 时返回名称，否则默认返回标准 code。
- `keyword` 同时匹配 code、name、别名等可检索字段，具体召回策略由服务端实现。
- `pageSize` 建议默认 20，最大不超过 100。
- 数据库型术语服务必须将 `page/pageSize` 转换为底层 `limit/offset` 执行分页；内存型术语映射允许服务端兜底切片。
- 确认结果恢复后服务端仍应根据 `termField` 做最终转换和校验。

错误响应示例：

```json
{
  "code": 400,
  "message": "termSet is required",
  "data": null
}
```

常见错误码：

| HTTP 状态码 | `code` | 说明 |
|-------------|--------|------|
| 400 | 400 | 缺少 `termSet`、`termTypeCode` 等必要参数 |
| 404 | 404 | 术语集不存在 |
| 500 | 500 | 术语服务异常 |

## 8. 中断上下文协议

`OperationConfirmationPlugin` 抛出的 `ClarificationNeededError.context`：

```json
{
  "interrupt_type": "operation_form",
  "tool_name": "update_customer_status",
  "structured_input": {
    "customerId": "C001",
    "status": "停用"
  },
  "operation_form": {
    "schemaVersion": "1.0",
    "formId": "op_form_20260525120000_xxx",
    "actionCode": "update_customer_status",
    "title": "待确认操作",
    "rule": [
      [
        {
          "itemId": "op_item_20260525120000_001",
          "formType": "input",
          "fieldCode": "customerId",
          "fieldPath": "customer.customerId",
          "fieldName": "客户 ID",
          "fieldType": "string",
          "fieldValue": "C001"
        },
        {
          "formType": "term_select",
          "fieldCode": "status",
          "fieldPath": "customer.status",
          "fieldName": "状态",
          "fieldType": "string",
          "fieldValue": "TODO",
          "term": {
            "termSet": "status.code",
            "termTypeCode": "status",
            "termField": "code",
            "datasetId": 100
          }
        }
      ]
    ]
  },
  "react_round_idx": 0,
  "operation_confirm_context": {
    "formId": "op_form_20260525120000_xxx",
    "actionCode": "update_customer_status",
    "items": [
      {
        "itemId": "op_item_20260525120000_001",
        "originalParamsHash": "sha256"
      }
    ]
  }
}
```

写入状态：

```json
{
  "execution_status": "clarify_needed",
  "pending_clarification_context": {
    "interrupt_type": "operation_form",
    "tool_name": "update_customer_status",
    "structured_input": {},
    "operation_form": {},
    "operation_confirm_context": {},
    "react_round_idx": 0
  }
}
```

## 9. 用户确认恢复协议

前端不直接提交业务表单，只通过 LangGraph resume 传回用户确认结果：

```json
{
  "interrupt_type": "operation_form",
  "formId": "op_form_20260525120000_xxx",
  "confirmed": true,
  "rule": [
    [
      {
        "itemId": "op_item_20260525120000_001",
        "formType": "input",
        "fieldCode": "customerId",
        "fieldPath": "customer.customerId",
        "fieldName": "客户 ID",
        "fieldType": "string",
        "fieldValue": "C001"
      },
      {
        "formType": "term_select",
        "fieldCode": "status",
        "fieldPath": "customer.status",
        "fieldName": "状态",
        "fieldType": "string",
        "fieldValue": "DISABLED"
      },
      {
        "formType": "input",
        "fieldCode": "remark",
        "fieldPath": "customer.remark",
        "fieldName": "备注",
        "fieldType": "string",
        "fieldValue": "用户确认停用"
      }
    ]
  ]
}
```

用户取消时传回同一套结构，只调整顶层确认标识：

```json
{
  "interrupt_type": "operation_form",
  "formId": "op_form_20260525120000_xxx",
  "confirmed": false,
  "reason": "用户取消操作",
  "rule": [
    [
      {
        "itemId": "op_item_20260525120000_001",
        "formType": "input",
        "fieldCode": "customerId",
        "fieldPath": "customer.customerId",
        "fieldName": "客户 ID",
        "fieldType": "string",
        "fieldValue": "C001"
      },
      {
        "formType": "term_select",
        "fieldCode": "status",
        "fieldPath": "customer.status",
        "fieldName": "状态",
        "fieldType": "string",
        "fieldValue": "DISABLED"
      },
      {
        "formType": "input",
        "fieldCode": "remark",
        "fieldPath": "customer.remark",
        "fieldName": "备注",
        "fieldType": "string",
        "fieldValue": "用户取消前填写的备注"
      }
    ]
  ]
}
```

Agent 恢复后将确认结果写入 `clarification_formatted_params`。真实业务提交不发生在前端表单阶段，而发生在后续工具重放与 `Action.execute()` 阶段：

```json
{
  "clarification_formatted_params": {
    "tool_name": "update_customer_status",
    "interrupt_type": "operation_form",
    "formId": "op_form_20260525120000_xxx",
    "confirmed": true,
    "rule": [
      [
        {
          "itemId": "op_item_20260525120000_001",
          "fieldCode": "customerId",
          "fieldPath": "customer.customerId",
          "fieldValue": "C001"
        },
        {
          "fieldCode": "status",
          "fieldPath": "customer.status",
          "fieldValue": "DISABLED"
        },
        {
          "fieldCode": "remark",
          "fieldPath": "customer.remark",
          "fieldValue": "用户确认停用"
        }
      ]
    ],
    "params": {
      "customer": {
        "customerId": "C001",
        "status": "DISABLED",
        "remark": "用户确认停用"
      },
      "userConfirmed": true,
      "_operationConfirm": {
        "itemId": "op_item_20260525120000_001",
        "confirmed": true
      }
    }
  },
  "clarify_abort": false
}
```

说明：

- 恢复 payload 统一使用二维 `rule`。用户提交和取消使用同一数据结构，只通过顶层 `confirmed` 区分是否确认执行。
- 前端只需要把用户确认后的 `fieldValue` 写回对应字段，并在顶层携带 `confirmed` 和可选 `reason`。
- `rule` 只承载表单字段和值，不承载用户提交或取消状态。
- `params` 是当前恢复执行工具使用的参数视图。单项场景下取第一个表单项；多项场景下由 `OperationConfirmationPlugin` 按当前 `tool_name/actionCode/itemId` 选择对应项并 patch 到工具参数。
- 如果顶层 `confirmed == false`，执行层不再处理任何表单项，直接进入审计或终止结果。

取消操作时建议写入可恢复的终止结果，不再回到工具执行：

```json
{
  "clarify_abort": true,
  "execution_status": "cancelled",
  "final_answer": "已取消本次操作。"
}
```

## 10. 节点改造点

### 10.1 OperationConfirmationPlugin

新增内置 hook 插件：

```text
packages/datacloud-analysis/src/datacloud_analysis/tool_hook_plugins/builtin/operation_confirmation_plugin.py
```

职责：

- 在工具执行前识别操作类动作。
- 从 loader 中读取动作参数和术语绑定信息。
- 生成 `operation_form`。
- 如果没有有效确认参数，则抛 `ClarificationNeededError`。
- 如果存在匹配的 `clarification_formatted_params`，将确认后的参数和服务端确认标记 patch 到 `tool_params` 并放行。

### 10.2 analyze_clarify_node

新增分流：

```text
if ctx.get("interrupt_type") == "operation_form":
    return {
        "clarification_analyze_result": {
            "interrupt_type": "operation_form",
            "tool_name": tool_name,
            "structured_input": structured_input,
            "operation_form": ctx["operation_form"],
        },
        "pending_clarification_context": None,
    }
```

该路径不调用 `prepare_query_clarification` 或 `_analyze_clarification()`。

### 10.3 user_clarify_node

新增分流：

- `interrupt_type` 为空或查询澄清类型：保持现有 paradigm 流程。
- `interrupt_type == "operation_form"`：调用 `interrupt()` 返回 `operation_form`，恢复后直接转换为 `clarification_formatted_params`。

该路径不调用 `finalize_query_clarification()`。

### 10.4 tool 节点

现有 `HookAwareToolNode`、`tool_dispatcher_node`、`make_tool_node` 已经能捕获 `ClarificationNeededError` 并写入 `pending_clarification_context`，只需要确保：

- `pending_clarification_context` 保留 `interrupt_type`。
- 恢复后工具重放时 `clarification_formatted_params` 尚未被提前清理。
- 重放保护逻辑仍以 `pending_clarification_context + clarification_formatted_params` 判断脏 checkpoint。

### 10.5 Action.execute

当前实现未修改 `datacloud-data` SDK 的既有执行行为，只由 `OperationConfirmationPlugin` 在恢复后的工具重放阶段注入 `userConfirmed` 和 `_operationConfirm`。SDK 执行层强制二次校验作为后续增强，目标要求如下：

- `userConfirmed` 必须为 true。
- `_operationConfirm.itemId` 必须与当前服务端确认上下文匹配。
- 必填参数必须完整。
- 术语字段必须能解析为合法 code 或 name。
- 用户确认字段必须来自表单 `rule` 及复杂字段 `children` 中允许的字段，避免注入未授权字段。

## 11. 幂等、安全与审计

确认安全信息不放入表单 payload，而是保存在服务端中断上下文中。建议结构：

```json
{
  "formId": "op_form_20260525120000_xxx",
  "items": [
    {
      "itemId": "op_item_20260525120000_001",
      "toolName": "update_customer_status",
      "actionCode": "update_customer_status",
      "originalParamsHash": "sha256"
    }
  ],
  "userId": "u001"
}
```

校验要求：

- 当前 resume 请求必须命中同一个 checkpoint/thread 的待确认上下文。
- 服务端确认上下文未过期。
- `formId` 与 `itemId` 必须存在于服务端确认上下文中。
- `itemId` 未被消费，或重复恢复返回同一执行结果。
- 当前用户与中断上下文绑定用户一致。
- `formId`、`itemId`、`toolName`、`actionCode` 与恢复上下文一致。
- 用户确认字段必须是表单 `rule` 及复杂字段 `children` 中允许的字段。
- `_operationConfirm` 由服务端在恢复后注入，不接受前端或 LLM 直接传入。

审计建议记录：

- `itemId`
- `formId`
- `toolName`
- `actionCode`
- `userId`
- `confirmed`
- `originalParams`
- `confirmedRule`
- `resolvedParams`
- `executionResult`
- `createdAt`
- `confirmedAt`

## 12. 测试建议

单元测试：

- 操作动作首次调用触发表单中断。
- 已确认恢复调用不再重复中断。
- 单个确认请求可通过二维 `rule` 返回多个表单项，前端可按表单列表渲染。
- 对象字段可通过 `children = [[...fields]]` 渲染和恢复。
- 对象数组字段可通过 `children = [[...fields], [...fields]]` 渲染和恢复。
- 对象列表主表单可拆成多个顶层 `rule[i]` 表单项确认。
- 术语字段只返回 `term` 元信息，不返回 `options`、`remoteOptions`、`optionUrl`、`labelField`、`valueField`。
- 前端根据 `term` 调用统一术语选项接口，并能正确渲染返回的 `label/value` 候选。
- 用户取消时不执行工具。
- 服务端确认上下文缺失、过期、动作不匹配时拒绝执行。

集成测试：

- `tool_dispatcher -> analyze_clarify -> user_clarify -> resume -> tool_dispatcher` 完整恢复。
- checkpoint 持久化后进程重启，仍能恢复用户确认结果。
- 并发或重复 resume 不会重复执行操作。
- 查询澄清流程不受 `operation_form` 分流影响。

## 13. 后续扩展

- 支持操作影响范围预览，如更新/删除前返回命中行数和示例记录。
- 支持按动作风险等级决定是否需要二次确认。
- 支持多步骤表单，例如先选择对象，再加载联动字段。
- 支持审批流，将 `confirmed=true` 扩展为 `approved=true`。
- 支持统一前端表单 schema 校验和版本协商。
