fix(sheet): validate merge range up front, fix startCell key, correct CHANGELOG
Three review P1s. 1. CHANGELOG no longer asserts a breaking Schema change this PR does not ship. sheet export / sheet create deliver main's mcp + interface_ref and schema-compatibility reports ok (0 changed fields), so the "declared as composite (breaking)" entry was false and is removed; the set-style entry drops the "breaking" framing (accepted as compatible by the reviewed mapping-exclusion carve-out); the duplicate ### Changed heading is merged. 2. firstNonEmptySheetSpecCell reads the start cell via pickStr(spec, "startCell", "start_cell"). Sheet specs are forwarded verbatim to table_put, whose wire fields are camelCase in this repo. The prior snake_case-only read meant a user passing the real startCell would have data written at the offset while the probe read A1 — a false "写入未生效" on a successful write. camelCase preferred, snake_case kept for tolerance. 3. planStyleOps now parses cell_merges range with parseA1Range, matching the cell_styles branch. planStyleOps is dry-run once before create_workspace_sheet as the up-front structural gate, so an invalid range like "not-a-range" is now rejected before any RPC instead of failing only at the final merge_cells call and leaving an unrollbackable partially-completed document. merge_cells' range contract is A1:B3-style, which parseA1Range covers (and it strips a Sheet1! prefix). Tests: cell-merges-invalid-range added to TestSheetCreateValidatesBeforeCreating Document (asserts calls == 0); TestFirstNonEmptySheetSpecCell covers both startCell and start_cell. Changed-code coverage stays at 100%; targeted sheet suites pass; CHANGELOG has a single Changed section with no false breaking claim.
This commit is contained in:
1 parent
dbecf23bc4
commit
ecaa375be8
3 files changed
+24
-8
No files matched your search
+1
-5
@@ -17,12 +17,8 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
### Changed
|
||||
|
||||
- **`sheet range set-style` 后端切换为 `set_cell_range`(破坏性 Schema 变更)** — 样式统一走 cellStyles 路径(仅设样式、保留原值),这是斜体/下划线删除线/字体族/边框唯一可用的通道。`interface_ref` 由 `update_range` 变为 `set_cell_range`,12 个样式 flag 改为 reviewed mapping exclusion。CLI 用法向后兼容,无 flag 删除。
|
||||
- **`sheet range set-style` 后端切换为 `set_cell_range`** — 样式统一走 cellStyles 路径(仅设样式、保留原值),这是斜体/下划线删除线/字体族/边框唯一可用的通道。`interface_ref` 由 `update_range` 变为 `set_cell_range`,12 个样式 flag 改为 reviewed mapping exclusion。CLI 用法向后兼容、无 flag 删除;schema-compatibility 经 reviewed 豁免判定为兼容(0 changed fields)。
|
||||
- **`sheet range batch-set-style` 改为单次原子提交** — 由本地循环多次 `update_range` 改为一次 `batch_update`,任一项失败默认整批回滚;`--continue-on-error` 由本地控制改为透传服务端。新增批量上限:最多 100 个区域且累计不超过 200000 个单元格。
|
||||
- **`sheet export` / `sheet create` 声明为 composite(破坏性 Schema 变更)** — 两者都会按参数路由到多个工具(`--export-format csv` 走 `get_range_as_csv`;建表带数据串起建表→探活→写入→回读→样式),`mcp` 单一 ref 不准确。
|
||||
|
||||
### Changed
|
||||
|
||||
- **Doc import upload fallback** — `dws doc import` no longer fails on file
|
||||
formats outside the conversion whitelist (html, pdf, zip, extensionless,
|
||||
and any future format): it now hands the file to the document-space upload
|
||||
|
||||
@@ -580,6 +580,14 @@ func planStyleOps(nodeID, sheetID string, ops sheetStyleOps) ([]styleCall, error
|
||||
if rangeAddr == "" {
|
||||
return nil, fmt.Errorf("cell_merges[%d] 缺少必填的 range", i)
|
||||
}
|
||||
// 严格解析合并区域地址,与 cell_styles 分支一致:planStyleOps 会在创建文档
|
||||
// **之前**被干跑一次做前置校验,非法 range(如 "not-a-range")必须在此拦下,
|
||||
// 否则会走到建文档→写数据→应用前序样式,直到最后 merge_cells 才被服务端
|
||||
// 拒绝,留下无法回滚的部分完成文档。merge_cells 的 range 契约是 A1:B3 形态,
|
||||
// parseA1Range 正好覆盖(并自动剥离 Sheet1! 前缀)。
|
||||
if _, _, err := parseA1Range(rangeAddr); err != nil {
|
||||
return nil, fmt.Errorf("cell_merges[%d].range: %w", i, err)
|
||||
}
|
||||
mergeType, err := feishuMergeType(pickStr(item, "merge_type", "mergeType"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cell_merges[%d]: %w", i, err)
|
||||
@@ -814,13 +822,17 @@ func sheetSpecGrid(spec map[string]any) [][]any {
|
||||
}
|
||||
|
||||
// firstNonEmptySheetSpecCell 返回该 sheet spec 首个预期非空单元格的 A1 地址。
|
||||
// 尊重 start_cell 偏移(默认 A1)。第二返回值为 false 表示该 spec 没有任何要写入
|
||||
// 尊重起始格偏移(默认 A1)。第二返回值为 false 表示该 spec 没有任何要写入
|
||||
// 的内容(例如只给了 name),这类工作表本就应为空,调用方跳过回读,避免把合法的
|
||||
// 空表误判为数据丢失。逻辑与 --values 分支的 firstNonEmptyValuesCell 一致:不能死盯
|
||||
// A1,首行/首列为空的合法数据不应被误报。
|
||||
//
|
||||
// 起始格键名以 table_put 线上契约的 camelCase startCell 为准(与本仓库
|
||||
// startCell / "startCell": "A1" 一致);同时兼容 snake_case start_cell,避免探针
|
||||
// 与实际写入位置错位导致「写成功却回读为空」的假阳性。
|
||||
func firstNonEmptySheetSpecCell(spec map[string]any) (string, bool) {
|
||||
col0, row0 := 1, 1
|
||||
if sc, ok := spec["start_cell"].(string); ok && sc != "" {
|
||||
if sc := pickStr(spec, "startCell", "start_cell"); sc != "" {
|
||||
if c, r, err := parseA1Cell(strings.ToUpper(sc)); err == nil {
|
||||
col0, row0 = c, r
|
||||
}
|
||||
|
||||
@@ -250,6 +250,13 @@ func TestSheetCreateValidatesBeforeCreatingDocument(t *testing.T) {
|
||||
map[string]string{"name": "X", "values": `[[1]]`, "styles": `{"styles":[{"name":"S","cell_merges":[{"range":"A1:B1","merge_type":"invalid"}]}]}`},
|
||||
`cell_merges[0]: merge_type 非法: "invalid"`,
|
||||
},
|
||||
{
|
||||
// 非法合并区域地址同样只在最后 merge_cells 才被拒,会留下部分完成文档。
|
||||
// 必须在建文档之前严格解析拦下(与 cell_styles 的 range 校验一致)。
|
||||
"cell-merges-invalid-range",
|
||||
map[string]string{"name": "X", "values": `[[1]]`, "styles": `{"styles":[{"name":"S","cell_merges":[{"range":"not-a-range"}]}]}`},
|
||||
"cell_merges[0].range",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -396,7 +403,8 @@ func TestFirstNonEmptySheetSpecCell(t *testing.T) {
|
||||
{`{"name":"S","columns":["",""],"data":[["","x"]]}`, "B2", true}, // 首列表头空
|
||||
{`{"name":"S","data":[[1]]}`, "A1", true}, // 无表头,纯 data
|
||||
{`{"name":"S","data":[5]}`, "A1", true}, // data 行是单值而非数组
|
||||
{`{"name":"S","columns":["id"],"data":[[1]],"start_cell":"C3"}`, "C3", true}, // start_cell 偏移
|
||||
{`{"name":"S","columns":["id"],"data":[[1]],"startCell":"C3"}`, "C3", true}, // startCell(table_put 线上 camelCase)
|
||||
{`{"name":"S","columns":["id"],"data":[[1]],"start_cell":"C3"}`, "C3", true}, // start_cell(snake_case 兼容)
|
||||
{`{"name":"S"}`, "", false}, // 只有 name
|
||||
{`{"name":"S","columns":[],"data":[]}`, "", false}, // 空
|
||||
} {
|
||||
|
||||
Reference in new issue
Block a user