Files
larksuite-cli/shortcuts/task/tasklist_create.go
梁硕 83dfb068ad feat: open-source lark-cli — the official CLI for Lark/Feishu
Change-Id: I113d9cdb5403cec347efe4595415e34a18b7decf
2026-03-28 10:36:25 +08:00

224 lines
6.0 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"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
"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)
queryParams := make(larkcore.QueryParams)
queryParams.Set("user_id_type", "open_id")
apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
HttpMethod: http.MethodPost,
ApiPath: "/open-apis/task/v2/tasklists",
QueryParams: queryParams,
Body: body,
})
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 create tasklist")
}
}
data, err := HandleTaskApiResult(result, err, "create tasklist")
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)
// Create tasks if data is provided
var tasks []map[string]interface{}
var createdTasks []map[string]interface{}
var failedTasks []string
if dataStr := runtime.Str("data"); dataStr != "" {
if err := json.Unmarshal([]byte(dataStr), &tasks); err != nil {
return WrapTaskError(ErrCodeTaskInvalidParams, fmt.Sprintf("failed to parse --data as JSON array: %v", err), "parse data")
}
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")
}
tResp, tErr := runtime.DoAPI(&larkcore.ApiReq{
HttpMethod: http.MethodPost,
ApiPath: "/open-apis/task/v2/tasks",
QueryParams: queryParams,
Body: tDef,
})
mu.Lock()
defer mu.Unlock()
var tResult map[string]interface{}
if tErr == nil {
if json.Unmarshal(tResp.RawBody, &tResult) != nil {
tErr = WrapTaskError(ErrCodeTaskInternalError, "failed to parse task response", "parse task")
}
}
tData, tErr := HandleTaskApiResult(tResult, tErr, "create task in tasklist")
if tErr != nil {
summary, _ := tDef["summary"].(string)
failedTasks = append(failedTasks, fmt.Sprintf("Index %d (%s): %v", 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,
}
runtime.OutFormat(outData, nil, 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, " - %s\n", f)
}
}
}
})
return nil
},
}
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
}