Back to skills
SKILL.md
H3yun Subtable Write
ASecurity安全地把子表明细写入氚云指定记录时使用。触发场景:用户要求「更新子表」「写入/追加明细」「批量改明细」「把这些数据填到某条记录」「改报价单/订单的产品行」等,涉及 h3yun_update_bizobject 携带子表数据的操作。强制走「读取→匹配→diff 确认→写入→回读校验」流程,避免重复追加、静默丢数据和误删。
- 417 stars
- 0 votes
- 0 copies
- 0 views
- Added November 8, 2026
Works with
Security analysis
100/100npx -y skills add infometa/workbuddyskills --skill h3yun-subtable-write --agent claude-codeAre you the author of H3yun Subtable Write?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/infometa-h3yun-subtable-write)---
name: h3yun-subtable-write
description: 安全地把子表明细写入氚云指定记录时使用。触发场景:用户要求「更新子表」「写入/追加明细」「批量改明细」「把这些数据填到某条记录」「改报价单/订单的产品行」等,涉及 h3yun_update_bizobject 携带子表数据的操作。强制走「读取→匹配→diff 确认→写入→回读校验」流程,避免重复追加、静默丢数据和误删。
---
# 氚云子表写入
把一批明细数据安全地写进某条氚云记录的指定子表。
## 一、先看能力边界(三条硬约束)
这三条决定了「能做什么、不能做什么」,任何情况下都不得绕过:
| 约束 | 事实 | 后果 |
| -------- | ------------------------------------- | ---------------------------- |
| **不能删行** | 省略行不会删除,空数组被禁止(报 `invalid_parameter`) | 批次里「少了的行」不会被删,只会静默保留 |
| **不幂等** | 不带 `ObjectId` 的行一律追加,服务端不做值去重 | 同一批跑两次 = 明细翻倍,且多出来的行 API 删不掉 |
| **不能清空** | 禁止 `null`、空字符串、空数组、清空 | 无法用「传空」实现重置 |
**因此:本技能只做「合并(merge)」,绝不做「替换(replace)」。** 用户若要求「把明细覆盖成这批」,必须明确告知做不到,并给出替代方案(见 §6)。
## 二、标准流程(五步,不可跳步)
### 第 1 步:读现状
```
h3yun_get_bizobject(schemaCode, bizObjectId)
```
拿到 `values` 里子表编码对应的行数组,每行含 `ObjectId`、`ParentObjectId` 和各业务字段。
**必须原样保存 `ObjectId`**,它是后续匹配的唯一可靠依据。
### 第 2 步:确认业务唯一键
在子表字段里找能唯一标识一行的字段,优先级:
1. 编码类:产品编码、SKU、物料号、工号
2. 型号 / 规格
3. 名称
4. 组合键(如 型号 + 规格)
**若子表没有任何可作唯一键的字段**(例如整行只有一个数值或一个备注):
- 停止合并,降级为「纯追加」;
- 必须向用户明示:「该子表缺少可用于匹配的字段,只能追加新行,无法更新已有行。继续会产生重复行,确认吗?」
**不要用「整行值完全相等」当作匹配依据** —— 两行合法地拥有相同值是完全正常的(实测中同一张子表就有两行 `45789`),按值匹配会改错行。
**业务键不能选「你正想修改的那个字段」。** 按值匹配意味着键值一变就匹配不上:若子表唯一的可改字段就是那个键(例如整行只有一个数值 `F0000002`),把 `33235` 改成 `35000` 会被判成「新增一行 35000」而不是「修改原行」。
这种情形必须走**显式行 ID**:目标行里带上 `ObjectId`,字段值随便改都能正确定位。
```json
{ "ObjectId": "43405329-6004-4cdc-bcf5-9f3b5f3b415b", "F0000002": "35000" }
```
`plan_merge.py` 已支持:目标行进里带 `ObjectId` 时优先按 ID 定位,不看业务键。反过来说,**如果子表只有一个字段、且这个字段就是你要改的内容,那么「批量改值」只能靠先列出各行 ID 让用户指定**,不能靠键自动匹配。
### 工具名说明
本文档中的 `h3yun_get_bizobject` 等是逻辑工具名。实际调用需按当前连接器的前缀拼装,例如 `mcp__h3dev__h3yun_get_bizobject`(自定义连接器 `h3dev`)或 `mcp__h3yun-connector__h3yun_get_bizobject`(官方连接器)。**连接器指向哪个环境决定你操作的是哪套数据**,动手前先确认。
### 第 3 步:算出 diff(用脚本,不要心算)
把当前行和目标行落成两个 JSON 文件,跑:
```bash
python3 scripts/plan_merge.py \
--current current.json \
--desired desired.json \
--key <业务键字段编码> \
--schema-code <子表编码> \
--output plan.json
```
脚本输出四类结果:
| 分类 | 含义 | payload 里怎么处理 |
| --------- | ----------- | ------------------- |
| `updates` | 匹配上且有字段变化 | 带 `ObjectId`,只放变化字段 |
| `adds` | 未匹配上 | 不带 `ObjectId`,放全部字段 |
| `noops` | 匹配上且值未变 | 不放 |
| `orphans` | 库里有、目标批次里没有 | **不放**(放了也不会删),仅告警 |
### 第 4 步:把 diff 给用户确认(不可省略)
用 §7 的模板输出。**尤其必须点明 `orphans`**:这些行不会被删除,用户以为「覆盖了」实际没覆盖,是长期脏数据的主要来源。
用户确认后,才进入第 5 步。
### 第 5 步:写入 + 回读校验
```
h3yun_update_bizobject(schemaCode=<主表编码>, bizObjectId, bizObject={
"<子表编码>": [ ...updates..., ...adds... ]
})
```
写入后**必须再读一次**,校验:
- 行数变化 = `len(adds)`
- `updates` 中每行的目标字段已变为新值
- 原有行 `ObjectId` 未变
- `orphans` 行仍在(预期如此)
任一不符,向用户报告实际结果,不要自行重试。
## 三、payload 构造规则
```json
{
"schemaCode": "<主表编码>",
"bizObjectId": "<父记录ID>",
"bizObject": {
"<子表编码>": [
{ "ObjectId": "<已有行ID>", "F0000002": "新值" },
{ "F0000002": "新增行的值" }
]
}
}
```
- 子表编码作为 `bizObject` 的键,值是**行对象数组**
- 已有行:`ObjectId` 必传,但**不可修改**系统定位字段(`ObjectId`、`ParentObjectId` 只用于定位)
- 新增行:**不要**传 `ObjectId`,也不要传 `ParentObjectId`,服务端会自动关联
- 未出现的字段由 Engine 从既有对象补全,不需要整行回传
- 禁止任何字段传 `null` / 空字符串 / 空数组
## 四、幂等保护
写入前先做一次「重复检测」:若 `adds` 中有行的业务键已存在于当前记录,说明可能是重复执行,**暂停并要求用户确认**。
工具明确声明「非幂等、无版本冲突保护、禁止自动重试」。收到以下情况一律**不得重试**,先回读核实:
- `h3yun.write.result_unknown`(写入结果未知)
- 请求超时 / 网络中断
回读方式:
- 核对**子表行内容**必须用 `h3yun_get_bizobject(schemaCode, bizObjectId)`——它返回全量行;
- `h3yun_filter_bizobject_list` 只用来「按名称/键定位是哪条记录」(`operator: "eq"`),**它的子表固定截断到 10 行**,不能用来数行数(详见 §5)。
## 五、错误码处置
| 错误码 | 含义 | 处理 |
| ------------------------------------ | ------------------ | -------------------------------------------------- |
| `h3yun.bizobject.field_not_writable` | 字段不可写,message 会点名 | 移除该字段重发(不是权限问题,是该字段本就不开放写入) |
| `h3yun.request.invalid_parameter` | 参数值不合法 | 检查:是否传了 null/空串/空数组、子表是否写成裸对象而非数组、字段编码是否拼错 |
| `h3yun.write.result_unknown` | 结果未知 | **禁止重试**,先回读核实 |
| `h3yun.bizobject.write_rejected` | 行 ObjectId 不存在于该子表 | 说明传入的行 ID 不属于本记录。**工具会拒绝而非当新增行处理**,这是好事,重读行 ID 后重试 |
| `h3yun.bizobject.unavailable` | 记录不存在或无权访问 | 核对 `bizObjectId` 与身份是否同租户 |
| `h3yun.form.edit_permission_denied` | 无编辑权限 | 停止,报告用户 |
### 回读必须用 `get_bizobject`,不能用 `filter_bizobject_list`
实测:`filter_bizobject_list` 返回的子表**固定截断到 10 行**,并在 `truncatedSubTables` 中标记该子表。用它核对写入结果会误判「只写进 10 行」。
`get_bizobject` 返回全量。实测 200 行子表完整返回,无截断。
### 已实测容量上限
| 项 | 实测结果 |
| --------------------- | ---------- |
| 单次写入 50 行(1 改 + 49 增) | ✅ 成功 |
| 单次写入 150 行(全为新增) | ✅ 成功 |
| 子表累计 200 行 | ✅ 完整读回,无截断 |
| 含非法行的批次 | ✅ 整批回滚,无半写 |
单次 150 行未触及上限,但未继续往上压测。超过 200 行时建议分批,并在每批后回读确认。
完整错误码表见 `references/payload-examples.md`。
## 六、用户要做「覆盖式更新」时怎么办
用户说「把明细改成这批」「以我这批为准」,正确回应:
1. 明确说明:当前 API 不支持删除子表行,所以无法实现真正的覆盖;
2. 给出三个选项:
- **A. 合并**:有则改、无则增,多余行保留(推荐,最安全)
- **B. 删后重建**:整条记录删掉重建 —— 会丢失 `ObjectId`、创建时间和流程历史,需明确告知
- **C. 人工清理**:先合并,再由用户在页面上手删多余行
3. 等用户选择,不要自行决定。
## 七、给用户确认的模板
```
将要写入记录:<主表名> / <记录名或ID>
子表:<子表名>(当前 N 行 → 写入后 M 行)
【修改】k 行
- <业务键值>:字段A 旧值 → 新值
- ...
【新增】m 行
- <业务键值>:字段A=值, 字段B=值
- ...
【不处理】p 行(重要)
以下行在记录中存在,但不在你给的批次里。API 不支持删除,它们会原样保留:
- <业务键值> ...
如需删除,请写入后在页面上手动操作。
确认执行?
```
## 八、反模式(不要做)
- ❌ 按「整行值相等」匹配已有行 —— 相同值可能合法共存,会改错行
- ❌ 一上来就写,不先 `get_bizobject` 读行 ID —— 必然全部变成追加
- ❌ 把用户给的批次当成「全量」直接传 —— 不会删,但会让你误判已覆盖
- ❌ 省略 `ObjectId` 去「更新」已存在的行 —— 结果是新增重复行
- ❌ 写入失败后自动重试 —— 非幂等,可能已经写成功
- ❌ 传空数组表达「清空子表」 —— 直接报错
- ❌ 把子表写成单个对象而不是数组
## 九、参考
- `references/payload-examples.md` —— 真实请求/响应样本、语义规则、完整错误码表
- `scripts/plan_merge.py` —— diff 计算脚本(`--help` 查看参数)
Attribution
Related skills
View all in development →Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.