mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
* feat: surface search API notices sa: safe doc: none cfg: none test: unit test * fix: surface search notices in default output * docs: add search notice doc comments * docs: expand search notice doc comments
205 lines
6.1 KiB
Go
205 lines
6.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package task
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
const (
|
|
taskSearchDefaultPageLimit = 20
|
|
taskSearchMaxPageLimit = 40
|
|
)
|
|
|
|
var SearchTask = common.Shortcut{
|
|
Service: "task",
|
|
Command: "+search",
|
|
Description: "search tasks",
|
|
Risk: "read",
|
|
Scopes: []string{"task:task:read"},
|
|
AuthTypes: []string{"user"},
|
|
HasFormat: true,
|
|
Flags: []common.Flag{
|
|
{Name: "query", Desc: "search keyword"},
|
|
{Name: "page-all", Type: "bool", Desc: "automatically paginate through all pages (max 40)"},
|
|
{Name: "page-limit", Type: "int", Default: "20", Desc: "max page limit (default 20, max 40)"},
|
|
{Name: "page-token", Desc: "page token"},
|
|
{Name: "creator", Desc: "creator open_ids, comma-separated"},
|
|
{Name: "assignee", Desc: "assignee open_ids, comma-separated"},
|
|
{Name: "completed", Type: "bool", Desc: "set true for completed or false for incomplete tasks"},
|
|
{Name: "due", Desc: "due time range: start,end (supports ISO/date/relative/ms)"},
|
|
{Name: "follower", Desc: "follower open_ids, comma-separated"},
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
body, err := buildTaskSearchBody(runtime)
|
|
if err != nil {
|
|
return common.NewDryRunAPI().Set("error", err.Error())
|
|
}
|
|
return common.NewDryRunAPI().
|
|
POST("/open-apis/task/v2/tasks/search").
|
|
Body(body).
|
|
Desc("Then GET /open-apis/task/v2/tasks/:guid for each search hit to render standard output")
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
_, err := buildTaskSearchBody(runtime)
|
|
return err
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
body, err := buildTaskSearchBody(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pageLimit := runtime.Int("page-limit")
|
|
if pageLimit <= 0 {
|
|
pageLimit = taskSearchDefaultPageLimit
|
|
}
|
|
if runtime.Bool("page-all") {
|
|
pageLimit = taskSearchMaxPageLimit
|
|
}
|
|
if pageLimit > taskSearchMaxPageLimit {
|
|
pageLimit = taskSearchMaxPageLimit
|
|
}
|
|
|
|
var rawItems []interface{}
|
|
var lastPageToken string
|
|
var lastHasMore bool
|
|
var notice string
|
|
currentBody := body
|
|
for page := 0; page < pageLimit; page++ {
|
|
data, err := callTaskAPITyped(runtime, http.MethodPost, "/open-apis/task/v2/tasks/search", nil, currentBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if notice == "" {
|
|
notice, _ = data["notice"].(string)
|
|
}
|
|
items, _ := data["items"].([]interface{})
|
|
rawItems = append(rawItems, items...)
|
|
lastHasMore, _ = data["has_more"].(bool)
|
|
lastPageToken, _ = data["page_token"].(string)
|
|
if !lastHasMore || lastPageToken == "" {
|
|
break
|
|
}
|
|
currentBody["page_token"] = lastPageToken
|
|
}
|
|
|
|
enriched := make([]map[string]interface{}, 0, len(rawItems))
|
|
for _, item := range rawItems {
|
|
itemMap, _ := item.(map[string]interface{})
|
|
taskID, _ := itemMap["id"].(string)
|
|
if taskID == "" {
|
|
continue
|
|
}
|
|
|
|
task, err := getTaskDetail(runtime, taskID)
|
|
if err != nil {
|
|
metaData, _ := itemMap["meta_data"].(map[string]interface{})
|
|
appLink, _ := metaData["app_link"].(string)
|
|
enriched = append(enriched, map[string]interface{}{
|
|
"guid": taskID,
|
|
"url": truncateTaskURL(appLink),
|
|
})
|
|
continue
|
|
}
|
|
enriched = append(enriched, outputTaskSummary(task))
|
|
}
|
|
|
|
outData := map[string]interface{}{
|
|
"items": enriched,
|
|
"page_token": lastPageToken,
|
|
"has_more": lastHasMore,
|
|
}
|
|
if notice != "" {
|
|
outData["notice"] = notice
|
|
}
|
|
runtime.OutFormat(outData, &output.Meta{Count: len(enriched)}, func(w io.Writer) {
|
|
if len(enriched) == 0 {
|
|
fmt.Fprintln(w, "No tasks found.")
|
|
return
|
|
}
|
|
for i, item := range enriched {
|
|
fmt.Fprintf(w, "[%d] %v\n", i+1, item["summary"])
|
|
fmt.Fprintf(w, " GUID: %v\n", item["guid"])
|
|
if created, _ := item["created_at"].(string); created != "" {
|
|
fmt.Fprintf(w, " Created: %s\n", created)
|
|
}
|
|
if dueAt, _ := item["due_at"].(string); dueAt != "" {
|
|
fmt.Fprintf(w, " Due: %s\n", dueAt)
|
|
}
|
|
if urlVal, _ := item["url"].(string); urlVal != "" {
|
|
fmt.Fprintf(w, " URL: %s\n", urlVal)
|
|
}
|
|
fmt.Fprintln(w)
|
|
}
|
|
if lastHasMore && lastPageToken != "" {
|
|
fmt.Fprintf(w, "Next page token: %s\n", lastPageToken)
|
|
}
|
|
})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func buildTaskSearchBody(runtime *common.RuntimeContext) (map[string]interface{}, error) {
|
|
filter := map[string]interface{}{}
|
|
|
|
if ids := splitAndTrimCSV(runtime.Str("creator")); len(ids) > 0 {
|
|
filter["creator_ids"] = ids
|
|
}
|
|
if ids := splitAndTrimCSV(runtime.Str("assignee")); len(ids) > 0 {
|
|
filter["assignee_ids"] = ids
|
|
}
|
|
if ids := splitAndTrimCSV(runtime.Str("follower")); len(ids) > 0 {
|
|
filter["follower_ids"] = ids
|
|
}
|
|
if runtime.Cmd.Flags().Changed("completed") {
|
|
filter["is_completed"] = runtime.Bool("completed")
|
|
}
|
|
if dueRange := runtime.Str("due"); dueRange != "" {
|
|
start, end, err := parseTimeRangeRFC3339(dueRange)
|
|
if err != nil {
|
|
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid due: %v", err).WithParam("--due")
|
|
}
|
|
if dueFilter := buildTimeRangeFilter("due_time", start, end); dueFilter != nil {
|
|
mergeIntoFilter(filter, dueFilter)
|
|
}
|
|
}
|
|
if err := requireSearchFilter(runtime.Str("query"), filter, "build task search"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"query": runtime.Str("query"),
|
|
}
|
|
if len(filter) > 0 {
|
|
body["filter"] = filter
|
|
}
|
|
if pageToken := runtime.Str("page-token"); pageToken != "" {
|
|
body["page_token"] = pageToken
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func getTaskDetail(runtime *common.RuntimeContext, taskID string) (map[string]interface{}, error) {
|
|
params := map[string]interface{}{"user_id_type": "open_id"}
|
|
|
|
data, err := callTaskAPITyped(runtime, http.MethodGet, "/open-apis/task/v2/tasks/"+url.PathEscape(taskID), params, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
task, _ := data["task"].(map[string]interface{})
|
|
if task == nil {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "task detail response missing task object")
|
|
}
|
|
return task, nil
|
|
}
|