Merge remote-tracking branch 'origin/main' into feat/lark-sheets-develop

This commit is contained in:
xiongyuanwen-byted
2026-06-09 19:52:08 +08:00
5 changed files with 99 additions and 6 deletions

View File

@@ -0,0 +1,58 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"os"
"strings"
"testing"
"github.com/larksuite/cli/internal/cmdutil"
_ "github.com/larksuite/cli/internal/vfs/localfileio"
"github.com/larksuite/cli/shortcuts/common"
"github.com/spf13/cobra"
)
func newCSVGuardRuntime(csvVal string) *common.RuntimeContext {
cmd := &cobra.Command{Use: "test"}
cmd.Flags().String("csv", "", "")
cmd.ParseFlags(nil)
cmd.Flags().Set("csv", csvVal)
return &common.RuntimeContext{Cmd: cmd}
}
// TestGuardCSVValueIsNotFilePath verifies the guard flags a bare --csv value
// only when it names a real file (a forgotten @), while leaving genuine inline
// content alone — including the case the old name-shape heuristic got wrong:
// prose that merely ends in or mentions a filename.
func TestGuardCSVValueIsNotFilePath(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
if err := os.WriteFile("data.csv", []byte("a,b\n1,2\n"), 0644); err != nil {
t.Fatal(err)
}
// Bare value naming an existing file → guarded with a fix-it hint.
err := guardCSVValueIsNotFilePath(newCSVGuardRuntime("data.csv"))
if err == nil {
t.Fatal("expected guard error when --csv names an existing file")
}
if !strings.Contains(err.Error(), "existing file") || !strings.Contains(err.Error(), "@data.csv") {
t.Errorf("error should flag the file and suggest @data.csv, got: %v", err)
}
// Content that is not a real file must pass through unchanged.
for _, v := range []string{
"改完记得更新config.json", // prose ending in a filename — not a real file
"remember to update data.csv", // mentions the real file but isn't its name
"a,b\n1,2", // multi-cell CSV
"hello world",
"nope.csv", // path-shaped but no such file
"",
} {
if err := guardCSVValueIsNotFilePath(newCSVGuardRuntime(v)); err != nil {
t.Errorf("content %q must pass through, got: %v", v, err)
}
}
}

View File

@@ -219,7 +219,12 @@ var CsvPut = common.Shortcut{
}
cmd.MarkFlagsOneRequired("start-cell", "range")
},
Validate: validateViaInput(csvPutInput),
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if err := guardCSVValueIsNotFilePath(runtime); err != nil {
return err
}
return validateViaInput(csvPutInput)(ctx, runtime)
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
sheetID, sheetName, _ := resolveSheetSelector(runtime)
@@ -295,6 +300,36 @@ func csvPutWriteRangeFromInput(input map[string]interface{}) (string, bool) {
return fmt.Sprintf("%s:%s%d", anchor, endCol, endRow), true
}
// guardCSVValueIsNotFilePath catches the common slip of passing a CSV file path
// to --csv without the "@" that reads it (e.g. `--csv data.csv` instead of
// `--csv @data.csv`). Because any string is a valid one-cell CSV, the mistake
// would otherwise be written silently as the literal text "data.csv". It runs
// in +csv-put's Validate, after resolveInputFlags — so an @file / stdin value is
// already its contents (a real CSV blob, never a path) and only a bare value
// reaches here unchanged. It flags the value only when it actually names an
// existing file in the cwd subtree; checking real existence (not name shape)
// means inline content that merely ends in a filename ("see config.json") is
// never misjudged. Fails open: any Stat error or a directory leaves the value
// untouched. Scoped to --csv only — no other flag is affected.
func guardCSVValueIsNotFilePath(runtime *common.RuntimeContext) error {
raw := strings.TrimSpace(runtime.Str("csv"))
if raw == "" {
return nil
}
fio := runtime.FileIO()
if fio == nil {
return nil
}
info, err := fio.Stat(raw)
if err != nil || info == nil || info.IsDir() {
return nil //nolint:nilerr // fail-open: a missing/unreadable path is treated as inline content, not a forgotten @
}
return common.FlagErrorf(
"--csv value %q is an existing file, not inline CSV; to read it use --csv @%s, or pass the literal text via stdin (--csv -)",
raw, raw,
)
}
func csvPutInput(runtime flagView, token, sheetID, sheetName string) (map[string]interface{}, error) {
if err := requireSheetSelector(sheetID, sheetName); err != nil {
return nil, err

View File

@@ -73,7 +73,7 @@ lark-cli docs +create --api-version v2 --doc-format markdown --content $'# 项
## 最佳实践
- 文档标题从内容中自动提取XML `<title>` 或 Markdown `#`),不要在内容开头重复写标题
- **创建较长的文档时只建骨架**`--content` 仅传标题 + 各级 heading + 简短占位摘要;正文留给后续 `docs +update --command append``block_insert_after` 分段追加。一次性塞超长 `--content` 既容易触发参数限制,调试也更难。
- **创建较长的文档时只建骨架**`--content` 仅传标题 + 各级 heading + 简短占位摘要;正文留给后续 `block_insert_after --block-id <章节标题 block_id>` 分段追加。一次性塞超长 `--content` 既容易触发参数限制,调试也更难。
- **视觉丰富度**:必须遵循 [`lark-doc-style.md`](style/lark-doc-style.md) 中的样式指南,主动使用结构化 block 丰富文档
## 参考

View File

@@ -40,8 +40,8 @@
| `block_replace` | 替换指定 block同一 block 仅限一次) | `--block-id` `--content` |
| `block_delete` | 删除指定 block逗号分隔可批量 | `--block-id` |
| `overwrite` | ⚠️ 清空文档后全文重写(可能丢失图片、评论) | `--content` |
| `append` | 在文档末尾追加内容(等价于 `block_insert_after --block-id -1` | `--content` |
| `block_move_after` | 移动已有 block 到指定位置 | `--block-id` + (`--content` `--src-block-ids`) |
| `append` | ⚠️ 在文档**末尾**追加内容(等价于 `block_insert_after --block-id -1`。**不适用于逐章填充**——逐章写入请用 `block_insert_after` 并指定对应标题的 `--block-id` | `--content` |
| `block_move_after` | 移动已有 block 到指定位置 | `--block-id` `--src-block-ids` |
## 指令示例

View File

@@ -22,14 +22,14 @@
2. 设计大纲——每个 h1/h2 章节至少规划 1 个非文本 block承载重要信息的章节优先规划画板
3. `docs +create --api-version v2` **只建骨架**:标题 + 开头 `<callout>` + 各级标题 + 每节一句占位摘要
- ⚠️ **不要**一次性把完整章节内容塞进 `--content`。超长 `--content` 容易触发字符/参数限制。
- 完整内容留到第二波,由各 Agent 用 `docs +update --command append``block_insert_after` 分段写入。
- 完整内容留到第二波,由各 Agent 用 `block_insert_after --block-id <章节标题 block_id>` 分段写入。
### 第二波 — 内容撰写(并行 Agent
4. Spawn Agent 并行撰写各章节。每个 Agent 需收到:
- 文档 token、负责的章节范围、期望的 block 类型
- `lark-doc-xml.md``lark-doc-style.md` 的完整路径Agent 须先读取)
- 使用 `docs +update --command append``block_insert_after` 写入
- 使用 `block_insert_after --block-id <章节标题 block_id>` 写入对应章节内容
### 第三波 — 整合审查 + 画板意图识别(串行)