mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Task commands now return structured, typed errors instead of the legacy exit-code envelope: every failure carries a stable category, subtype, and recovery hint, so callers can branch on the error class instead of parsing messages. Exit codes derive from the error category — input validation exits 2, a permission denial exits 3, other API errors exit 1. Batch operations (adding tasks to a tasklist, creating a tasklist with tasks) now report partial failure honestly: the per-item successes and failures stay on stdout and the command exits non-zero instead of masking failures as a success.
230 lines
6.3 KiB
Go
230 lines
6.3 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package task
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var CreateTasklist = common.Shortcut{
|
|
Service: "task",
|
|
Command: "+tasklist-create",
|
|
Description: "create a tasklist and optionally add tasks",
|
|
Risk: "write",
|
|
Scopes: []string{"task:tasklist:write", "task:task:write"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
HasFormat: true,
|
|
|
|
Flags: []common.Flag{
|
|
{Name: "name", Desc: "tasklist name", Required: true},
|
|
{Name: "member", Desc: "comma-separated open_ids to add as editors"},
|
|
{Name: "data", Desc: "JSON array of tasks to create within this tasklist"},
|
|
},
|
|
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
body := buildTasklistCreateBody(runtime)
|
|
|
|
d := common.NewDryRunAPI().
|
|
Desc("1. Create Tasklist").
|
|
POST("/open-apis/task/v2/tasklists").
|
|
Params(map[string]interface{}{"user_id_type": "open_id"}).
|
|
Body(body)
|
|
|
|
if dataStr := runtime.Str("data"); dataStr != "" {
|
|
d.Desc("2. Create Tasks within the new tasklist (concurrently)")
|
|
}
|
|
|
|
return d
|
|
},
|
|
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
body := buildTasklistCreateBody(runtime)
|
|
params := map[string]interface{}{"user_id_type": "open_id"}
|
|
|
|
// Validate --data (client input) before any remote write, so a malformed
|
|
// payload fails fast without creating an orphan tasklist.
|
|
var tasks []map[string]interface{}
|
|
if dataStr := runtime.Str("data"); dataStr != "" {
|
|
if err := json.Unmarshal([]byte(dataStr), &tasks); err != nil {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "failed to parse --data as JSON array: %v", err).WithParam("--data")
|
|
}
|
|
}
|
|
|
|
data, err := callTaskAPITyped(runtime, http.MethodPost, "/open-apis/task/v2/tasklists", params, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tasklist, _ := data["tasklist"].(map[string]interface{})
|
|
tasklistGuid, _ := tasklist["guid"].(string)
|
|
tasklistName, _ := tasklist["name"].(string)
|
|
tasklistUrl, _ := tasklist["url"].(string)
|
|
tasklistUrl = truncateTaskURL(tasklistUrl)
|
|
|
|
var createdTasks []map[string]interface{}
|
|
var failedTasks []map[string]interface{}
|
|
|
|
if len(tasks) > 0 {
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
|
|
for i, taskDef := range tasks {
|
|
wg.Add(1)
|
|
go func(idx int, tDef map[string]interface{}) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
fmt.Fprintf(runtime.IO().ErrOut, "recovered in defer: %v\n", r)
|
|
}
|
|
wg.Done()
|
|
}()
|
|
|
|
// Add tasklist_guid to the task definition
|
|
tDef["tasklists"] = []map[string]interface{}{
|
|
{
|
|
"tasklist_guid": tasklistGuid,
|
|
},
|
|
}
|
|
|
|
// If assignee is provided as string, convert it to members
|
|
if assignee, ok := tDef["assignee"].(string); ok {
|
|
tDef["members"] = []map[string]interface{}{
|
|
{
|
|
"id": assignee,
|
|
"role": "assignee",
|
|
"type": "user",
|
|
},
|
|
}
|
|
delete(tDef, "assignee")
|
|
}
|
|
|
|
tData, tErr := callTaskAPITyped(runtime, http.MethodPost, "/open-apis/task/v2/tasks", params, tDef)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if tErr != nil {
|
|
summary, _ := tDef["summary"].(string)
|
|
failedTasks = append(failedTasks, buildTaskCreateFailure(idx, summary, tErr))
|
|
return
|
|
}
|
|
|
|
if t, ok := tData["task"].(map[string]interface{}); ok {
|
|
guid, _ := t["guid"].(string)
|
|
urlVal, _ := t["url"].(string)
|
|
urlVal = truncateTaskURL(urlVal)
|
|
createdTasks = append(createdTasks, map[string]interface{}{
|
|
"guid": guid,
|
|
"url": urlVal,
|
|
})
|
|
}
|
|
}(i, taskDef)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// Standardized write output: return resource identifiers
|
|
outData := map[string]interface{}{
|
|
"guid": tasklistGuid,
|
|
"url": tasklistUrl,
|
|
"created_tasks": createdTasks,
|
|
"failed_tasks": failedTasks,
|
|
}
|
|
|
|
pretty := func(w io.Writer) {
|
|
fmt.Fprintf(w, "✅ Tasklist created successfully!\n")
|
|
fmt.Fprintf(w, "Tasklist Name: %s\n", tasklistName)
|
|
fmt.Fprintf(w, "Tasklist ID: %s\n", tasklistGuid)
|
|
if tasklistUrl != "" {
|
|
fmt.Fprintf(w, "Tasklist URL: %s\n", tasklistUrl)
|
|
}
|
|
|
|
if len(tasks) > 0 {
|
|
fmt.Fprintln(w, strings.Repeat("-", 20))
|
|
fmt.Fprintf(w, "Tasks created: %d/%d\n", len(createdTasks), len(tasks))
|
|
for _, t := range createdTasks {
|
|
guid, _ := t["guid"].(string)
|
|
urlVal, _ := t["url"].(string)
|
|
fmt.Fprintf(w, " - ID: %s", guid)
|
|
if urlVal != "" {
|
|
fmt.Fprintf(w, ", URL: %s", urlVal)
|
|
}
|
|
fmt.Fprintln(w)
|
|
}
|
|
if len(failedTasks) > 0 {
|
|
fmt.Fprintf(w, "\nFailed tasks:\n")
|
|
for _, f := range failedTasks {
|
|
fmt.Fprintf(w, " - Index %v (%s): %s\n", f["index"], f["summary"], f["message"])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sub-task creation failures surface as a non-zero exit. JSON/JQ callers
|
|
// need an ok:false envelope, while pretty output should preserve the
|
|
// command-specific human-readable summary.
|
|
if len(failedTasks) > 0 {
|
|
if runtime.Format == "pretty" && runtime.JqExpr == "" {
|
|
runtime.OutFormat(outData, nil, pretty)
|
|
return output.PartialFailure(output.ExitAPI)
|
|
}
|
|
return runtime.OutPartialFailure(outData, nil)
|
|
}
|
|
|
|
runtime.OutFormat(outData, nil, pretty)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func buildTaskCreateFailure(index int, summary string, err error) map[string]interface{} {
|
|
failDetail := map[string]interface{}{
|
|
"index": index,
|
|
"summary": summary,
|
|
}
|
|
if p, ok := errs.ProblemOf(err); ok {
|
|
failDetail["type"] = string(p.Subtype)
|
|
failDetail["code"] = p.Code
|
|
failDetail["message"] = p.Message
|
|
failDetail["hint"] = p.Hint
|
|
} else {
|
|
failDetail["type"] = "api_error"
|
|
failDetail["message"] = err.Error()
|
|
}
|
|
return failDetail
|
|
}
|
|
|
|
func buildTasklistCreateBody(runtime *common.RuntimeContext) map[string]interface{} {
|
|
body := map[string]interface{}{
|
|
"name": runtime.Str("name"),
|
|
}
|
|
|
|
if memberStr := runtime.Str("member"); memberStr != "" {
|
|
ids := strings.Split(memberStr, ",")
|
|
var members []map[string]interface{}
|
|
for _, id := range ids {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
members = append(members, map[string]interface{}{
|
|
"id": id,
|
|
"role": "editor",
|
|
"type": "user",
|
|
})
|
|
}
|
|
body["members"] = members
|
|
}
|
|
|
|
return body
|
|
}
|