mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 22:24:31 +08:00
* feat(task): add task shortcuts with skill docs and tests * docs(task): document task event payload shape * refactor(task): remove unused buildUserIDs helper * fix(task): handle api error codes in set-ancestor * docs(task): clarify get-related-tasks page-token unit * feat(task): support bot identity for subscribe-event * docs(task): clarify bot subscribe-event scope * docs(task): clarify related-task pagination semantics * docs(task): add BOE selftest report (boe_task_tasklist_oapi_support) * docs(task): prefer related-task shortcuts over search for scoped queries * docs(task): clarify tasklist search routing * docs(task): route keywordless tasklist queries to list API * docs(task): refine search routing heuristics * feat(event): include task user-access updates in catch-all subscribe * docs(task): remove auth status --json guidance
210 lines
6.5 KiB
Go
210 lines
6.5 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package task
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
|
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
const (
|
|
tasklistSearchDefaultPageLimit = 20
|
|
tasklistSearchMaxPageLimit = 40
|
|
)
|
|
|
|
var SearchTasklist = common.Shortcut{
|
|
Service: "task",
|
|
Command: "+tasklist-search",
|
|
Description: "search tasklists",
|
|
Risk: "read",
|
|
Scopes: []string{"task:tasklist: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: "create-time", Desc: "create time range: start,end (supports ISO/date/relative/ms)"},
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
body, err := buildTasklistSearchBody(runtime)
|
|
if err != nil {
|
|
return common.NewDryRunAPI().Set("error", err.Error())
|
|
}
|
|
return common.NewDryRunAPI().
|
|
POST("/open-apis/task/v2/tasklists/search").
|
|
Body(body).
|
|
Desc("Then GET /open-apis/task/v2/tasklists/:guid for each search hit to render standard output")
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
_, err := buildTasklistSearchBody(runtime)
|
|
return err
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
body, err := buildTasklistSearchBody(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pageLimit := runtime.Int("page-limit")
|
|
if pageLimit <= 0 {
|
|
pageLimit = tasklistSearchDefaultPageLimit
|
|
}
|
|
if runtime.Bool("page-all") {
|
|
pageLimit = tasklistSearchMaxPageLimit
|
|
}
|
|
if pageLimit > tasklistSearchMaxPageLimit {
|
|
pageLimit = tasklistSearchMaxPageLimit
|
|
}
|
|
|
|
var rawItems []interface{}
|
|
var lastPageToken string
|
|
var lastHasMore bool
|
|
currentBody := body
|
|
for page := 0; page < pageLimit; page++ {
|
|
apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
|
|
HttpMethod: http.MethodPost,
|
|
ApiPath: "/open-apis/task/v2/tasklists/search",
|
|
Body: currentBody,
|
|
})
|
|
var result map[string]interface{}
|
|
if err == nil {
|
|
if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil {
|
|
return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse tasklist search")
|
|
}
|
|
}
|
|
data, err := HandleTaskApiResult(result, err, "search tasklists")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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
|
|
}
|
|
|
|
tasklists := make([]map[string]interface{}, 0, len(rawItems))
|
|
for _, item := range rawItems {
|
|
itemMap, _ := item.(map[string]interface{})
|
|
tasklistID, _ := itemMap["id"].(string)
|
|
if tasklistID == "" {
|
|
continue
|
|
}
|
|
|
|
tasklist, err := getTasklistDetail(runtime, tasklistID)
|
|
if err != nil {
|
|
// Keep a stable identifier and avoid rendering "<nil>" in pretty output.
|
|
tasklists = append(tasklists, map[string]interface{}{
|
|
"guid": tasklistID,
|
|
"name": fmt.Sprintf("(unknown tasklist: %s)", tasklistID),
|
|
})
|
|
continue
|
|
}
|
|
urlVal, _ := tasklist["url"].(string)
|
|
urlVal = truncateTaskURL(urlVal)
|
|
tasklists = append(tasklists, map[string]interface{}{
|
|
"guid": tasklist["guid"],
|
|
"name": tasklist["name"],
|
|
"url": urlVal,
|
|
"creator": tasklist["creator"],
|
|
})
|
|
}
|
|
|
|
outData := map[string]interface{}{
|
|
"items": tasklists,
|
|
"page_token": lastPageToken,
|
|
"has_more": lastHasMore,
|
|
}
|
|
runtime.OutFormat(outData, &output.Meta{Count: len(tasklists)}, func(w io.Writer) {
|
|
if len(tasklists) == 0 {
|
|
fmt.Fprintln(w, "No tasklists found.")
|
|
return
|
|
}
|
|
for i, tasklist := range tasklists {
|
|
fmt.Fprintf(w, "[%d] %v\n", i+1, tasklist["name"])
|
|
fmt.Fprintf(w, " GUID: %v\n", tasklist["guid"])
|
|
if urlVal, _ := tasklist["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 buildTasklistSearchBody(runtime *common.RuntimeContext) (map[string]interface{}, error) {
|
|
filter := map[string]interface{}{}
|
|
if ids := splitAndTrimCSV(runtime.Str("creator")); len(ids) > 0 {
|
|
filter["user_id"] = ids
|
|
}
|
|
if createTime := runtime.Str("create-time"); createTime != "" {
|
|
start, end, err := parseTimeRangeRFC3339(createTime)
|
|
if err != nil {
|
|
return nil, WrapTaskError(ErrCodeTaskInvalidParams, fmt.Sprintf("invalid create-time: %v", err), "build tasklist search")
|
|
}
|
|
if timeFilter := buildTimeRangeFilter("create_time", start, end); timeFilter != nil {
|
|
mergeIntoFilter(filter, timeFilter)
|
|
}
|
|
}
|
|
if err := requireSearchFilter(runtime.Str("query"), filter, "build tasklist 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 getTasklistDetail(runtime *common.RuntimeContext, tasklistID string) (map[string]interface{}, error) {
|
|
queryParams := make(larkcore.QueryParams)
|
|
queryParams.Set("user_id_type", "open_id")
|
|
|
|
apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
|
|
HttpMethod: http.MethodGet,
|
|
ApiPath: "/open-apis/task/v2/tasklists/" + url.PathEscape(tasklistID),
|
|
QueryParams: queryParams,
|
|
})
|
|
var result map[string]interface{}
|
|
if err == nil {
|
|
if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil {
|
|
return nil, WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse tasklist detail response: %v", parseErr), "parse tasklist detail")
|
|
}
|
|
}
|
|
data, err := HandleTaskApiResult(result, err, "get tasklist detail "+tasklistID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tasklist, _ := data["tasklist"].(map[string]interface{})
|
|
if tasklist == nil {
|
|
return nil, WrapTaskError(ErrCodeTaskInternalError, "tasklist detail response missing tasklist object", "get tasklist detail")
|
|
}
|
|
return tasklist, nil
|
|
}
|