Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions shortcuts/drive/drive_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import (
)

// driveSearchErrUserNotVisible is the Lark service code returned by
// doc_wiki/search when an open_id referenced in --creator-ids / --sharer-ids
// falls outside the app's user-visibility scope (different from the
// search:docs:read API scope).
// doc_wiki/search when an open_id referenced in an identity filter falls
// outside the app's user-visibility scope (different from the search:docs:read
// API scope).
const driveSearchErrUserNotVisible = 99992351

// open_time has a server-side cap of 3 months per request. Rather than
Expand Down Expand Up @@ -79,6 +79,8 @@ var DriveSearch = common.Shortcut{

{Name: "mine", Type: "bool", Desc: "restrict to docs I own (server-side owner semantic, NOT original creator; uses current user's open_id)"},
{Name: "creator-ids", Desc: "comma-separated owner open_ids (API field is creator_ids but matched by owner); mutually exclusive with --mine"},
{Name: "created-by-me", Type: "bool", Desc: "restrict to docs originally created by me (uses current user's open_id as original_creator_ids)"},
{Name: "original-creator-ids", Desc: "comma-separated original creator open_ids; mutually exclusive with --created-by-me"},

{Name: "edited-since", Desc: "start of [my edited] time window (e.g. 7d, 1m, 1y, 2026-04-01, RFC3339, unix seconds)"},
{Name: "edited-until", Desc: "end of [my edited] time window"},
Expand Down Expand Up @@ -108,7 +110,7 @@ var DriveSearch = common.Shortcut{
Tips: []string{
"Time flags accept relative (e.g. 7d, 1m, 1y), absolute (2026-04-01, RFC3339), or unix seconds.",
"my_edit_time and my_comment_time are hour-aggregated server-side; sub-hour inputs are snapped and a notice is printed to stderr.",
"Use --mine for a quick \"docs I own\" filter (owner semantic, not original creator). For other people, use --creator-ids ou_xxx,ou_yyy.",
"Use --created-by-me for \"docs I created\". Use --mine for \"docs I own\" (owner semantic).",
"--folder-tokens limits to doc-only search; --space-ids limits to wiki-only. They cannot be combined.",
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
Expand Down Expand Up @@ -164,6 +166,9 @@ type driveSearchSpec struct {
Mine bool
CreatorIDs []string

CreatedByMe bool
OriginalCreatorIDs []string

EditedSince string
EditedUntil string
CommentedSince string
Expand Down Expand Up @@ -193,6 +198,9 @@ func readDriveSearchSpec(runtime *common.RuntimeContext) driveSearchSpec {
Mine: runtime.Bool("mine"),
CreatorIDs: common.SplitCSV(runtime.Str("creator-ids")),

CreatedByMe: runtime.Bool("created-by-me"),
OriginalCreatorIDs: common.SplitCSV(runtime.Str("original-creator-ids")),

EditedSince: runtime.Str("edited-since"),
EditedUntil: runtime.Str("edited-until"),
CommentedSince: runtime.Str("commented-since"),
Expand Down Expand Up @@ -221,12 +229,18 @@ func buildDriveSearchRequest(spec driveSearchSpec, userOpenID string, now time.T
if spec.Mine && len(spec.CreatorIDs) > 0 {
return nil, nil, output.ErrValidation("cannot combine --mine and --creator-ids")
}
if spec.CreatedByMe && len(spec.OriginalCreatorIDs) > 0 {
return nil, nil, output.ErrValidation("cannot combine --created-by-me and --original-creator-ids")
}
if len(spec.FolderTokens) > 0 && len(spec.SpaceIDs) > 0 {
return nil, nil, output.ErrValidation("cannot combine --folder-tokens and --space-ids; doc and wiki scoped search cannot be combined")
}
if spec.Mine && userOpenID == "" {
return nil, nil, output.ErrValidation("--mine requires a logged-in user open_id, but none is configured; run `lark-cli auth login` or set user open_id in config")
}
if spec.CreatedByMe && userOpenID == "" {
return nil, nil, output.ErrValidation("--created-by-me requires a logged-in user open_id, but none is configured; run `lark-cli auth login` or set user open_id in config")
}

if err := validateDocTypes(spec.DocTypes); err != nil {
return nil, nil, err
Expand Down Expand Up @@ -256,13 +270,20 @@ func buildDriveSearchRequest(spec driveSearchSpec, userOpenID string, now time.T
notices = append(notices, n)
}

// Creator identity.
// Identity filters. creator_ids is owner; original_creator_ids is the
// immutable document creator.
switch {
case spec.Mine:
filter["creator_ids"] = []string{userOpenID}
case len(spec.CreatorIDs) > 0:
filter["creator_ids"] = spec.CreatorIDs
}
switch {
case spec.CreatedByMe:
filter["original_creator_ids"] = []string{userOpenID}
case len(spec.OriginalCreatorIDs) > 0:
filter["original_creator_ids"] = spec.OriginalCreatorIDs
}

// Time dimensions — each fills at most one filter key; hour-aggregated ones
// also contribute notices.
Expand Down Expand Up @@ -358,6 +379,11 @@ func validateDriveSearchIDs(spec driveSearchSpec) error {
return output.ErrValidation("--creator-ids %q: %s", id, err)
}
}
for _, id := range spec.OriginalCreatorIDs {
if _, err := common.ValidateUserID(id); err != nil {
return output.ErrValidation("--original-creator-ids %q: %s", id, err)
}
}
if n := len(spec.ChatIDs); n > driveSearchMaxChatIDs {
return output.ErrValidation("--chat-ids: max %d values per request, got %d", driveSearchMaxChatIDs, n)
}
Expand Down Expand Up @@ -638,7 +664,7 @@ func enrichDriveSearchError(err error) error {
return err
}
detail := *exitErr.Detail
detail.Hint = "one or more open_ids in --creator-ids / --sharer-ids are outside this app's user-visibility scope (this is the app's contact visibility, not the search:docs:read API scope); ask an admin to grant the app visibility to those users in the developer console, or drop the unreachable open_ids"
detail.Hint = "one or more open_ids in --creator-ids / --original-creator-ids / --sharer-ids are outside this app's user-visibility scope (this is the app's contact visibility, not the search:docs:read API scope); ask an admin to grant the app visibility to those users in the developer console, or drop the unreachable open_ids"
return &output.ExitError{
Code: exitErr.Code,
Detail: &detail,
Expand Down
57 changes: 54 additions & 3 deletions shortcuts/drive/drive_search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,10 @@ func TestValidateDriveSearchIDs(t *testing.T) {
t.Run("all valid", func(t *testing.T) {
t.Parallel()
spec := driveSearchSpec{
CreatorIDs: []string{"ou_aaa"},
ChatIDs: []string{"oc_xxx"},
SharerIDs: []string{"ou_bbb"},
CreatorIDs: []string{"ou_aaa"},
OriginalCreatorIDs: []string{"ou_ccc"},
ChatIDs: []string{"oc_xxx"},
SharerIDs: []string{"ou_bbb"},
}
if err := validateDriveSearchIDs(spec); err != nil {
t.Fatalf("unexpected error: %v", err)
Expand All @@ -260,6 +261,14 @@ func TestValidateDriveSearchIDs(t *testing.T) {
}
})

t.Run("bad original creator id format", func(t *testing.T) {
t.Parallel()
err := validateDriveSearchIDs(driveSearchSpec{OriginalCreatorIDs: []string{"u_bad"}})
if err == nil || !strings.Contains(err.Error(), "--original-creator-ids") {
t.Fatalf("expected --original-creator-ids error, got: %v", err)
}
})

t.Run("bad chat id format", func(t *testing.T) {
t.Parallel()
err := validateDriveSearchIDs(driveSearchSpec{ChatIDs: []string{"chat_bad"}})
Expand Down Expand Up @@ -724,6 +733,31 @@ func TestBuildDriveSearchRequest(t *testing.T) {
}
})

t.Run("--created-by-me fills original_creator_ids from userOpenID", func(t *testing.T) {
t.Parallel()
req, _, err := buildDriveSearchRequest(driveSearchSpec{CreatedByMe: true}, userOpenID, now)
if err != nil {
t.Fatalf("err: %v", err)
}
got := req["doc_filter"].(map[string]interface{})["original_creator_ids"].([]string)
if len(got) != 1 || got[0] != userOpenID {
t.Fatalf("expected [userOpenID], got %v", got)
}
})

t.Run("--original-creator-ids fills original_creator_ids", func(t *testing.T) {
t.Parallel()
spec := driveSearchSpec{OriginalCreatorIDs: []string{"ou_a", "ou_b"}}
req, _, err := buildDriveSearchRequest(spec, userOpenID, now)
if err != nil {
t.Fatalf("err: %v", err)
}
got := req["wiki_filter"].(map[string]interface{})["original_creator_ids"].([]string)
if !reflect.DeepEqual(got, []string{"ou_a", "ou_b"}) {
t.Fatalf("expected explicit original creator ids, got %v", got)
}
})

t.Run("--mine without userOpenID errors", func(t *testing.T) {
t.Parallel()
_, _, err := buildDriveSearchRequest(driveSearchSpec{Mine: true}, "", now)
Expand All @@ -732,6 +766,14 @@ func TestBuildDriveSearchRequest(t *testing.T) {
}
})

t.Run("--created-by-me without userOpenID errors", func(t *testing.T) {
t.Parallel()
_, _, err := buildDriveSearchRequest(driveSearchSpec{CreatedByMe: true}, "", now)
if err == nil || !strings.Contains(err.Error(), "--created-by-me") {
t.Fatalf("expected --created-by-me error, got: %v", err)
}
})

t.Run("--mine + --creator-ids mutually exclusive", func(t *testing.T) {
t.Parallel()
spec := driveSearchSpec{Mine: true, CreatorIDs: []string{"ou_x"}}
Expand All @@ -741,6 +783,15 @@ func TestBuildDriveSearchRequest(t *testing.T) {
}
})

t.Run("--created-by-me + --original-creator-ids mutually exclusive", func(t *testing.T) {
t.Parallel()
spec := driveSearchSpec{CreatedByMe: true, OriginalCreatorIDs: []string{"ou_x"}}
_, _, err := buildDriveSearchRequest(spec, userOpenID, now)
if err == nil || !strings.Contains(err.Error(), "--created-by-me") {
t.Fatalf("expected exclusion error, got: %v", err)
}
})

t.Run("--folder-tokens + --space-ids mutually exclusive", func(t *testing.T) {
t.Parallel()
spec := driveSearchSpec{
Expand Down
4 changes: 2 additions & 2 deletions skills/lark-drive/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ metadata:

## 快速决策

- 用户要**搜文档 / Wiki / 电子表格 / 多维表格 / 云空间对象**,优先使用 `lark-cli drive +search`。自然语言里"最近我编辑过的"、"我创建的"(→ `--mine`,实为 owner 语义)、"最近一周我打开过的 xxx"、"某人 owner 的 docx" 等直接映射到扁平 flag,避免手写嵌套 JSON。
- 用户要**搜文档 / Wiki / 电子表格 / 多维表格 / 云空间对象**,优先使用 `lark-cli drive +search`。自然语言里"最近我编辑过的"、"我创建的"(→ `--created-by-me`,原始创建者语义)、"我负责/owner 的"(→ `--mine`,owner 语义)、"最近一周我打开过的 xxx"、"某人 owner 的 docx" 等直接映射到扁平 flag,避免手写嵌套 JSON。
- 用户要把本地 `.xlsx` / `.csv` / `.base` 导入成 Base / 多维表格 / bitable,第一步必须使用 `lark-cli drive +import --type bitable`。
- 用户要把本地 `.md` / `.docx` / `.doc` / `.txt` / `.html` 导入成在线文档,使用 `lark-cli drive +import --type docx`。
- 用户要在 Drive 里上传、创建、读取、局部 patch 或覆盖更新**原生 `.md` 文件**(不是导入成 docx),切到 [`lark-markdown`](../lark-markdown/SKILL.md)。
Expand Down Expand Up @@ -258,7 +258,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli drive +<verb> [flags]`)

| Shortcut | 说明 |
|----------|------|
| [`+search`](references/lark-drive-search.md) | Search Lark docs, Wiki, and spreadsheet files with flat filter flags. Natural-language-friendly: `--edited-since`, `--mine`, `--doc-types`, etc. |
| [`+search`](references/lark-drive-search.md) | Search Lark docs, Wiki, and spreadsheet files with flat filter flags. Natural-language-friendly: `--edited-since`, `--created-by-me`, `--mine`, `--doc-types`, etc. |
| [`+upload`](references/lark-drive-upload.md) | Upload a local file to a Drive folder or wiki node |
| [`+create-folder`](references/lark-drive-create-folder.md) | Create a Drive folder, optionally under a parent folder, with bot auto-grant support |
| [`+download`](references/lark-drive-download.md) | Download a file from Drive to local |
Expand Down
Loading
Loading