task

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 28, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// ErrCodeTaskInvalidParams is returned when request parameters are invalid.
	ErrCodeTaskInvalidParams = 1470400
	// ErrCodeTaskPermissionDenied is returned when the user has no permission.
	ErrCodeTaskPermissionDenied = 1470403
	// ErrCodeTaskNotFound is returned when the resource is not found.
	ErrCodeTaskNotFound = 1470404
	// ErrCodeTaskConflict is returned when concurrent call conflict.
	ErrCodeTaskConflict = 1470422
	// ErrCodeTaskInternalError is returned when server error occurs.
	ErrCodeTaskInternalError = 1470500
	// ErrCodeTaskAssigneeLimit is returned when assignee limit exceeded.
	ErrCodeTaskAssigneeLimit = 1470610
	// ErrCodeTaskFollowerLimit is returned when follower limit exceeded.
	ErrCodeTaskFollowerLimit = 1470611
	// ErrCodeTasklistMemberLimit is returned when tasklist member limit exceeded.
	ErrCodeTasklistMemberLimit = 1470612
	// ErrCodeTaskReminderExists is returned when reminder already exists.
	ErrCodeTaskReminderExists = 1470613
)

Variables

View Source
var AddTaskToTasklist = common.Shortcut{
	Service:     "task",
	Command:     "+tasklist-task-add",
	Description: "add tasks to a tasklist",
	Risk:        "write",
	Scopes:      []string{"task:task:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "tasklist-id", Desc: "tasklist id", Required: true},
		{Name: "task-id", Desc: "task id (comma-separated for multiple)", Required: true},
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		taskIds := strings.Split(runtime.Str("task-id"), ",")
		taskId := url.PathEscape(strings.TrimSpace(taskIds[0]))

		body := map[string]interface{}{
			"tasklist_guid": extractTasklistGuid(runtime.Str("tasklist-id")),
		}

		return common.NewDryRunAPI().
			POST("/open-apis/task/v2/tasks/" + taskId + "/add_tasklist").
			Params(map[string]interface{}{"user_id_type": "open_id"}).
			Body(body)
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		tasklistGuid := extractTasklistGuid(runtime.Str("tasklist-id"))
		taskIds := strings.Split(runtime.Str("task-id"), ",")

		queryParams := make(larkcore.QueryParams)
		queryParams.Set("user_id_type", "open_id")

		body := map[string]interface{}{
			"tasklist_guid": tasklistGuid,
		}

		var successful []map[string]interface{}
		var failed []map[string]interface{}

		for _, taskId := range taskIds {
			taskId = strings.TrimSpace(taskId)
			if taskId == "" {
				continue
			}

			apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
				HttpMethod:  http.MethodPost,
				ApiPath:     "/open-apis/task/v2/tasks/" + url.PathEscape(taskId) + "/add_tasklist",
				QueryParams: queryParams,
				Body:        body,
			})

			var result map[string]interface{}
			if err == nil {
				if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil {
					err = WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse add task response")
				}
			}

			data, err := HandleTaskApiResult(result, err, "add task to tasklist")
			if err != nil {
				failDetail := map[string]interface{}{
					"guid": taskId,
				}
				if exitErr, ok := err.(*output.ExitError); ok && exitErr.Detail != nil {
					failDetail["type"] = exitErr.Detail.Type
					failDetail["code"] = exitErr.Detail.Code
					failDetail["message"] = exitErr.Detail.Message
					failDetail["hint"] = exitErr.Detail.Hint
				} else {
					failDetail["type"] = "api_error"
					failDetail["message"] = err.Error()
				}
				failed = append(failed, failDetail)
			} else {
				task, _ := data["task"].(map[string]interface{})
				guid, _ := task["guid"].(string)
				taskUrl, _ := task["url"].(string)
				taskUrl = truncateTaskURL(taskUrl)
				successful = append(successful, map[string]interface{}{
					"guid": guid,
					"url":  taskUrl,
				})
			}
		}

		resultData := map[string]interface{}{
			"successful_tasks": successful,
			"failed_tasks":     failed,
			"tasklist_guid":    tasklistGuid,
		}

		runtime.OutFormat(resultData, nil, func(w io.Writer) {
			fmt.Fprintf(w, "✅ Tasks added to tasklist %s!\n", tasklistGuid)
			fmt.Fprintf(w, "Successful: %d, Failed: %d\n", len(successful), len(failed))

			if len(successful) > 0 {
				fmt.Fprintln(w, "Successful Tasks:")
				for _, t := range successful {
					guid, _ := t["guid"].(string)
					taskUrl, _ := t["url"].(string)
					fmt.Fprintf(w, "  - ID: %s", guid)
					if taskUrl != "" {
						fmt.Fprintf(w, ", URL: %s", taskUrl)
					}
					fmt.Fprintln(w)
				}
			}

			if len(failed) > 0 {
				fmt.Fprintln(w, "Failed Tasks:")
				for _, f := range failed {
					fmt.Fprintf(w, "  - %s: %s\n", f["guid"], f["message"])
				}
			}
		})
		return nil
	},
}
View Source
var AssignTask = common.Shortcut{
	Service:     "task",
	Command:     "+assign",
	Description: "assign or remove task members",
	Risk:        "write",
	Scopes:      []string{"task:task:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "task-id", Desc: "task id", Required: true},
		{Name: "add", Desc: "comma-separated open_ids to add as assignees"},
		{Name: "remove", Desc: "comma-separated open_ids to remove from assignees"},
		{Name: "idempotency-key", Desc: "client token for idempotency (used for add_members)"},
	},

	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if runtime.Str("add") == "" && runtime.Str("remove") == "" {
			return WrapTaskError(ErrCodeTaskInvalidParams, "must specify either --add or --remove", "validate assign")
		}
		return nil
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		d := common.NewDryRunAPI()
		taskId := url.PathEscape(runtime.Str("task-id"))

		if addStr := runtime.Str("add"); addStr != "" {
			body := buildMembersBody(addStr, runtime.Str("idempotency-key"))
			d.POST("/open-apis/task/v2/tasks/" + taskId + "/add_members").
				Params(map[string]interface{}{"user_id_type": "open_id"}).
				Body(body)
		}

		if removeStr := runtime.Str("remove"); removeStr != "" {
			body := buildMembersBody(removeStr, "")
			d.POST("/open-apis/task/v2/tasks/" + taskId + "/remove_members").
				Params(map[string]interface{}{"user_id_type": "open_id"}).
				Body(body)
		}

		return d
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		taskId := url.PathEscape(runtime.Str("task-id"))
		queryParams := make(larkcore.QueryParams)
		queryParams.Set("user_id_type", "open_id")

		var lastData map[string]interface{}

		if addStr := runtime.Str("add"); addStr != "" {
			body := buildMembersBody(addStr, runtime.Str("idempotency-key"))
			apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
				HttpMethod:  http.MethodPost,
				ApiPath:     "/open-apis/task/v2/tasks/" + taskId + "/add_members",
				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 add members")
				}
			}

			data, err := HandleTaskApiResult(result, err, "add task members")
			if err != nil {
				return err
			}
			lastData = data
		}

		if removeStr := runtime.Str("remove"); removeStr != "" {
			body := buildMembersBody(removeStr, "")
			apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
				HttpMethod:  http.MethodPost,
				ApiPath:     "/open-apis/task/v2/tasks/" + taskId + "/remove_members",
				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 remove members")
				}
			}

			data, err := HandleTaskApiResult(result, err, "remove task members")
			if err != nil {
				return err
			}
			lastData = data
		}

		task, _ := lastData["task"].(map[string]interface{})
		urlVal, _ := task["url"].(string)
		urlVal = truncateTaskURL(urlVal)

		outData := map[string]interface{}{
			"guid": taskId,
			"url":  urlVal,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			fmt.Fprintf(w, "✅ Task assignees updated successfully!\n")
			fmt.Fprintf(w, "Task ID: %s\n", taskId)
			if urlVal != "" {
				fmt.Fprintf(w, "Task URL: %s\n", urlVal)
			}

			if members, ok := task["members"].([]interface{}); ok {
				fmt.Fprintf(w, "Current Assignees: %d\n", len(members))
			}
		})
		return nil
	},
}
View Source
var CommentTask = common.Shortcut{
	Service:     "task",
	Command:     "+comment",
	Description: "add a comment to a task",
	Risk:        "write",
	Scopes:      []string{"task:comment:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "task-id", Desc: "task id", Required: true},
		{Name: "content", Desc: "comment content", Required: true},
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body := map[string]interface{}{
			"content":       runtime.Str("content"),
			"resource_id":   runtime.Str("task-id"),
			"resource_type": "task",
		}
		return common.NewDryRunAPI().
			POST("/open-apis/task/v2/comments").
			Params(map[string]interface{}{"user_id_type": "open_id"}).
			Body(body)
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		body := map[string]interface{}{
			"content":       runtime.Str("content"),
			"resource_id":   runtime.Str("task-id"),
			"resource_type": "task",
		}

		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/comments",
			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 comment response")
			}
		}

		data, err := HandleTaskApiResult(result, err, "add task comment")
		if err != nil {
			return err
		}

		comment, _ := data["comment"].(map[string]interface{})
		id, _ := comment["id"].(string)

		outData := map[string]interface{}{
			"id": id,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			fmt.Fprintf(w, "✅ Comment added successfully!\n")
			if id != "" {
				fmt.Fprintf(w, "Comment ID: %s\n", id)
			}
		})
		return nil
	},
}
View Source
var CompleteTask = common.Shortcut{
	Service:     "task",
	Command:     "+complete",
	Description: "mark a task as complete",
	Risk:        "write",
	Scopes:      []string{"task:task:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "task-id", Desc: "task id", Required: true},
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body := buildCompleteBody()
		taskId := url.PathEscape(runtime.Str("task-id"))
		return common.NewDryRunAPI().
			PATCH("/open-apis/task/v2/tasks/" + taskId).
			Params(map[string]interface{}{"user_id_type": "open_id"}).
			Body(body)
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		taskId := url.PathEscape(runtime.Str("task-id"))
		body := buildCompleteBody()

		queryParams := make(larkcore.QueryParams)
		queryParams.Set("user_id_type", "open_id")

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod:  http.MethodPatch,
			ApiPath:     "/open-apis/task/v2/tasks/" + taskId,
			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 complete response")
			}
		}

		data, err := HandleTaskApiResult(result, err, "complete task")
		if err != nil {
			return err
		}

		task, _ := data["task"].(map[string]interface{})
		guid, _ := task["guid"].(string)
		urlVal, _ := task["url"].(string)
		urlVal = truncateTaskURL(urlVal)

		outData := map[string]interface{}{
			"guid": guid,
			"url":  urlVal,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			summary, _ := task["summary"].(string)
			fmt.Fprintf(w, "✅ Task completed successfully!\n")
			if guid != "" {
				fmt.Fprintf(w, "Task ID: %s\n", guid)
			}
			if summary != "" {
				fmt.Fprintf(w, "Summary: %s\n", summary)
			}
			if urlVal != "" {
				fmt.Fprintf(w, "Task URL: %s\n", urlVal)
			}
		})
		return nil
	},
}
View Source
var CreateTask = common.Shortcut{
	Service:     "task",
	Command:     "+create",
	Description: "create a task",
	Risk:        "write",
	Scopes:      []string{"task:task:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "summary", Desc: "task title"},
		{Name: "description", Desc: "task description"},
		{Name: "assignee", Desc: "assignee open_id"},
		{Name: "due", Desc: "due date (ISO 8601 / date:YYYY-MM-DD / relative:+2d / ms timestamp)"},
		{Name: "tasklist-id", Desc: "tasklist id or applink URL"},
		{Name: "idempotency-key", Desc: "client token for idempotency"},
		{Name: "data", Desc: "JSON payload for creating task"},
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body, err := buildTaskCreateBody(runtime)
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		return common.NewDryRunAPI().
			POST("/open-apis/task/v2/tasks").
			Params(map[string]interface{}{"user_id_type": "open_id"}).
			Body(body)
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		body, err := buildTaskCreateBody(runtime)
		if err != nil {
			return WrapTaskError(ErrCodeTaskInvalidParams, err.Error(), "create task")
		}

		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/tasks",
			QueryParams: queryParams,
			Body:        body,
		})

		var result map[string]interface{}
		if err == nil {
			if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil {
				return fmt.Errorf("failed to parse response: %v", parseErr)
			}
		}

		data, err := HandleTaskApiResult(result, err, "create task")
		if err != nil {
			return err
		}

		task, _ := data["task"].(map[string]interface{})
		guid, _ := task["guid"].(string)
		urlVal, _ := task["url"].(string)
		urlVal = truncateTaskURL(urlVal)

		outData := map[string]interface{}{
			"guid": guid,
			"url":  urlVal,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			fmt.Fprintf(w, "✅ Task created successfully!\n")
			fmt.Fprintf(w, "Summary: %s\n", body["summary"])
			if guid != "" {
				fmt.Fprintf(w, "Task ID: %s\n", guid)
			}
			if urlVal != "" {
				fmt.Fprintf(w, "Task URL: %s\n", urlVal)
			}
		})
		return nil
	},
}
View Source
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()
					}()

					tDef["tasklists"] = []map[string]interface{}{
						{
							"tasklist_guid": tasklistGuid,
						},
					}

					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()
		}

		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
	},
}
View Source
var FollowersTask = common.Shortcut{
	Service:     "task",
	Command:     "+followers",
	Description: "manage task followers",
	Risk:        "write",
	Scopes:      []string{"task:task:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "task-id", Desc: "task id", Required: true},
		{Name: "add", Desc: "comma-separated open_ids to add as followers"},
		{Name: "remove", Desc: "comma-separated open_ids to remove from followers"},
		{Name: "idempotency-key", Desc: "client token for idempotency (used for add_members)"},
	},

	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if runtime.Str("add") == "" && runtime.Str("remove") == "" {
			return WrapTaskError(ErrCodeTaskInvalidParams, "must specify either --add or --remove", "validate followers")
		}
		return nil
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		d := common.NewDryRunAPI()
		taskId := url.PathEscape(runtime.Str("task-id"))

		if addStr := runtime.Str("add"); addStr != "" {
			body := buildFollowersBody(addStr, runtime.Str("idempotency-key"))
			d.POST("/open-apis/task/v2/tasks/" + taskId + "/add_members").
				Params(map[string]interface{}{"user_id_type": "open_id"}).
				Body(body)
		}

		if removeStr := runtime.Str("remove"); removeStr != "" {
			body := buildFollowersBody(removeStr, "")
			d.POST("/open-apis/task/v2/tasks/" + taskId + "/remove_members").
				Params(map[string]interface{}{"user_id_type": "open_id"}).
				Body(body)
		}

		return d
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		taskId := url.PathEscape(runtime.Str("task-id"))
		queryParams := make(larkcore.QueryParams)
		queryParams.Set("user_id_type", "open_id")

		var lastData map[string]interface{}

		if addStr := runtime.Str("add"); addStr != "" {
			body := buildFollowersBody(addStr, runtime.Str("idempotency-key"))
			apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
				HttpMethod:  http.MethodPost,
				ApiPath:     "/open-apis/task/v2/tasks/" + taskId + "/add_members",
				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 add followers")
				}
			}

			data, err := HandleTaskApiResult(result, err, "add task followers")
			if err != nil {
				return err
			}
			lastData = data
		}

		if removeStr := runtime.Str("remove"); removeStr != "" {
			body := buildFollowersBody(removeStr, "")
			apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
				HttpMethod:  http.MethodPost,
				ApiPath:     "/open-apis/task/v2/tasks/" + taskId + "/remove_members",
				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 remove followers")
				}
			}

			data, err := HandleTaskApiResult(result, err, "remove task followers")
			if err != nil {
				return err
			}
			lastData = data
		}

		task, _ := lastData["task"].(map[string]interface{})
		urlVal, _ := task["url"].(string)
		urlVal = truncateTaskURL(urlVal)

		outData := map[string]interface{}{
			"guid": taskId,
			"url":  urlVal,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			fmt.Fprintf(w, "✅ Task followers updated successfully!\n")
			fmt.Fprintf(w, "Task ID: %s\n", taskId)
			if urlVal != "" {
				fmt.Fprintf(w, "Task URL: %s\n", urlVal)
			}
		})
		return nil
	},
}
View Source
var GetMyTasks = common.Shortcut{
	Service:     "task",
	Command:     "+get-my-tasks",
	Description: "List tasks assigned to me",
	Risk:        "read",
	Scopes:      []string{"task:task:read"},
	AuthTypes:   []string{"user"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "query", Desc: "search for tasks by summary (exact match first, then partial match)"},
		{Name: "complete", Type: "bool", Desc: "if true, query completed tasks; default is false"},
		{Name: "created_at", Desc: "query tasks created after this time (date/relative/ms)"},
		{Name: "due-start", Desc: "query tasks with due date after this time (date/relative/ms)"},
		{Name: "due-end", Desc: "query tasks with due date before this time (date/relative/ms)"},
		{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 with --page-all)"},
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		d := common.NewDryRunAPI()

		params := map[string]interface{}{
			"type":         "my_tasks",
			"user_id_type": "open_id",
			"page_size":    50,
		}
		if runtime.Cmd.Flags().Changed("complete") {
			params["completed"] = runtime.Bool("complete")
		}

		return d.GET("/open-apis/task/v2/tasks").Params(params)
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		startTime := time.Now()

		queryParams := make(larkcore.QueryParams)
		queryParams.Set("type", "my_tasks")
		queryParams.Set("user_id_type", "open_id")
		queryParams.Set("page_size", "50")
		if runtime.Cmd.Flags().Changed("complete") {
			if runtime.Bool("complete") {
				queryParams.Set("completed", "true")
			} else {
				queryParams.Set("completed", "false")
			}
		}

		// parse time flags to ms timestamp if provided
		var createdAfterMs, dueStartMs, dueEndMs int64
		if createdStr := runtime.Str("created_at"); createdStr != "" {
			tStr, err := parseTimeFlagSec(createdStr, "start")
			if err != nil {
				return WrapTaskError(ErrCodeTaskInvalidParams, fmt.Sprintf("invalid created_at: %v", err), "parse created_at")
			}
			createdAfterMs, _ = strconv.ParseInt(tStr, 10, 64)
			createdAfterMs *= 1000
		}

		if dueStartStr := runtime.Str("due-start"); dueStartStr != "" {
			tStr, err := parseTimeFlagSec(dueStartStr, "start")
			if err != nil {
				return WrapTaskError(ErrCodeTaskInvalidParams, fmt.Sprintf("invalid due-start: %v", err), "parse due-start")
			}
			dueStartMs, _ = strconv.ParseInt(tStr, 10, 64)
			dueStartMs *= 1000
		}

		if dueEndStr := runtime.Str("due-end"); dueEndStr != "" {
			tStr, err := parseTimeFlagSec(dueEndStr, "end")
			if err != nil {
				return WrapTaskError(ErrCodeTaskInvalidParams, fmt.Sprintf("invalid due-end: %v", err), "parse due-end")
			}
			dueEndMs, _ = strconv.ParseInt(tStr, 10, 64)
			dueEndMs *= 1000
		}

		var allItems []interface{}
		var lastPageToken string
		var lastHasMore bool
		pageCount := 0
		pageLimit := runtime.Int("page-limit")
		if runtime.Bool("page-all") {
			pageLimit = 40
		}

		for {
			pageCount++
			apiReq := &larkcore.ApiReq{
				HttpMethod:  "GET",
				ApiPath:     "/open-apis/task/v2/tasks",
				QueryParams: queryParams,
			}

			apiResp, err := runtime.DoAPI(apiReq)

			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 my tasks")
				}
			}

			data, err := HandleTaskApiResult(result, err, "list tasks")
			if err != nil {
				return err
			}

			itemsRaw, _ := data["items"].([]interface{})
			allItems = append(allItems, itemsRaw...)

			hasMore, _ := data["has_more"].(bool)
			lastHasMore = hasMore
			lastPageToken, _ = data["page_token"].(string)

			if !hasMore || lastPageToken == "" {
				break
			}

			if pageCount >= pageLimit {
				break
			}

			queryParams.Set("page_token", lastPageToken)
		}

		var filteredItems []map[string]interface{}

		for _, itemRaw := range allItems {
			item, ok := itemRaw.(map[string]interface{})
			if !ok {
				continue
			}

			if createdAfterMs > 0 {
				createdAtStr, _ := item["created_at"].(string)
				createdAtMs, _ := strconv.ParseInt(createdAtStr, 10, 64)
				if createdAtMs < createdAfterMs {
					continue
				}
			}

			if dueStartMs > 0 || dueEndMs > 0 {
				dueObj, _ := item["due"].(map[string]interface{})
				if dueObj == nil {

					continue
				}
				dueTimeStr, _ := dueObj["timestamp"].(string)
				dueTimeMs, _ := strconv.ParseInt(dueTimeStr, 10, 64)

				if dueStartMs > 0 && dueTimeMs < dueStartMs {
					continue
				}
				if dueEndMs > 0 && dueTimeMs > dueEndMs {
					continue
				}
			}

			filteredItems = append(filteredItems, item)
		}

		if query := runtime.Str("query"); query != "" {
			var exactMatches []map[string]interface{}
			var partialMatches []map[string]interface{}
			for _, item := range filteredItems {
				summary, _ := item["summary"].(string)
				if summary == query {
					exactMatches = append(exactMatches, item)
				} else if strings.Contains(summary, query) {
					partialMatches = append(partialMatches, item)
				}
			}

			if len(exactMatches) > 0 {
				filteredItems = exactMatches
			} else {
				filteredItems = partialMatches
			}
		}

		var outputItems []interface{}
		for _, item := range filteredItems {
			urlVal, _ := item["url"].(string)
			urlVal = truncateTaskURL(urlVal)
			outputItem := map[string]interface{}{
				"guid":    item["guid"],
				"summary": item["summary"],
				"url":     urlVal,
			}
			if createdAtStr, ok := item["created_at"].(string); ok {
				if ts, err := strconv.ParseInt(createdAtStr, 10, 64); err == nil {
					outputItem["created_at"] = time.UnixMilli(ts).UTC().Format(time.RFC3339)
				}
			}
			if dueObj, ok := item["due"].(map[string]interface{}); ok {
				if tsStr, ok := dueObj["timestamp"].(string); ok {
					if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil {
						outputItem["due_at"] = time.UnixMilli(ts).UTC().Format(time.RFC3339)
					}
				}
			}
			outputItems = append(outputItems, outputItem)
		}

		outData := map[string]interface{}{
			"items":      outputItems,
			"page_token": lastPageToken,
			"has_more":   lastHasMore,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			if len(filteredItems) == 0 {
				fmt.Fprintln(w, "No tasks found.")
				return
			}

			for i, item := range filteredItems {
				guid, _ := item["guid"].(string)
				summary, _ := item["summary"].(string)
				urlVal, _ := item["url"].(string)
				urlVal = truncateTaskURL(urlVal)

				var dueTimeStr string
				if dueObj, ok := item["due"].(map[string]interface{}); ok {
					if tsStr, ok := dueObj["timestamp"].(string); ok {
						if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil {
							dueTimeStr = time.UnixMilli(ts).Format("2006-01-02 15:04")
						}
					}
				}

				var createdDateStr string
				if createdStr, ok := item["created_at"].(string); ok {
					if ts, err := strconv.ParseInt(createdStr, 10, 64); err == nil {
						createdDateStr = time.UnixMilli(ts).Format("2006-01-02")
					}
				}

				fmt.Fprintf(w, "[%d] %s\n", i+1, summary)
				fmt.Fprintf(w, "    ID: %s\n", guid)
				if urlVal != "" {
					fmt.Fprintf(w, "    URL: %s\n", urlVal)
				}
				if dueTimeStr != "" {
					fmt.Fprintf(w, "    Due: %s\n", dueTimeStr)
				}
				if createdDateStr != "" {
					fmt.Fprintf(w, "    Created: %s\n", createdDateStr)
				}
				fmt.Fprintln(w)
			}

			if lastHasMore && lastPageToken != "" && !runtime.Cmd.Flags().Changed("page-limit") && !runtime.Cmd.Flags().Changed("page-all") {
				fmt.Fprintf(w, "\n[Warning] Too many tasks! Stopped after fetching %d pages.\n", pageLimit)
			}

			fmt.Fprintf(w, "\nTotal execution time: %v\n", time.Since(startTime))
		})

		return nil
	},
}
View Source
var MembersTasklist = common.Shortcut{
	Service:     "task",
	Command:     "+tasklist-members",
	Description: "manage tasklist members",
	Risk:        "write",
	Scopes:      []string{"task:tasklist:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "tasklist-id", Desc: "tasklist id", Required: true},
		{Name: "set", Desc: "comma-separated open_ids to set as exact members (replaces existing)"},
		{Name: "add", Desc: "comma-separated open_ids to add as members"},
		{Name: "remove", Desc: "comma-separated open_ids to remove from members"},
	},

	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		hasSet := runtime.Str("set") != ""
		hasAdd := runtime.Str("add") != ""
		hasRemove := runtime.Str("remove") != ""

		if hasSet && (hasAdd || hasRemove) {
			return WrapTaskError(ErrCodeTaskInvalidParams, "cannot combine --set with --add or --remove", "validate tasklist members")
		}
		return nil
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		d := common.NewDryRunAPI()
		tlId := url.PathEscape(extractTasklistGuid(runtime.Str("tasklist-id")))

		if runtime.Str("set") != "" || (runtime.Str("add") == "" && runtime.Str("remove") == "") {
			d.Desc("GET tasklist details/members").
				GET("/open-apis/task/v2/tasklists/" + tlId).
				Params(map[string]interface{}{"user_id_type": "open_id"})
		}

		if runtime.Str("add") != "" {
			body := buildTlMembersBody(runtime.Str("add"))
			d.Desc("Add members").
				POST("/open-apis/task/v2/tasklists/" + tlId + "/add_members").
				Params(map[string]interface{}{"user_id_type": "open_id"}).
				Body(body)
		}
		if runtime.Str("remove") != "" {
			body := buildTlMembersBody(runtime.Str("remove"))
			d.Desc("Remove members").
				POST("/open-apis/task/v2/tasklists/" + tlId + "/remove_members").
				Params(map[string]interface{}{"user_id_type": "open_id"}).
				Body(body)
		}

		return d
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		tlId := url.PathEscape(extractTasklistGuid(runtime.Str("tasklist-id")))
		queryParams := make(larkcore.QueryParams)
		queryParams.Set("user_id_type", "open_id")

		setStr := runtime.Str("set")
		addStr := runtime.Str("add")
		removeStr := runtime.Str("remove")

		if setStr == "" && addStr == "" && removeStr == "" {
			getResp, err := runtime.DoAPI(&larkcore.ApiReq{
				HttpMethod:  http.MethodGet,
				ApiPath:     "/open-apis/task/v2/tasklists/" + tlId,
				QueryParams: queryParams,
			})

			var getResult map[string]interface{}
			if err == nil {
				if parseErr := json.Unmarshal(getResp.RawBody, &getResult); parseErr != nil {
					return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse tasklist details")
				}
			}

			data, err := HandleTaskApiResult(getResult, err, "get tasklist members")
			if err != nil {
				return err
			}

			tl, _ := data["tasklist"].(map[string]interface{})
			membersRaw, _ := tl["members"].([]interface{})
			tlUrl, _ := tl["url"].(string)
			tlUrl = truncateTaskURL(tlUrl)

			var members []interface{}
			for _, m := range membersRaw {
				if mObj, ok := m.(map[string]interface{}); ok {
					members = append(members, map[string]interface{}{
						"id":   mObj["id"],
						"role": mObj["role"],
						"type": mObj["type"],
					})
				}
			}

			outData := map[string]interface{}{
				"guid":    tlId,
				"url":     tlUrl,
				"name":    tl["name"],
				"members": members,
			}

			runtime.OutFormat(outData, nil, func(w io.Writer) {
				fmt.Fprintf(w, "Tasklist: %s (%s)\n", tl["name"], tlId)
				if tlUrl != "" {
					fmt.Fprintf(w, "Tasklist URL: %s\n", tlUrl)
				}
				fmt.Fprintf(w, "Members (%d):\n", len(members))
				for _, m := range members {
					if mObj, ok := m.(map[string]interface{}); ok {
						fmt.Fprintf(w, "  - %s (%s)\n", mObj["id"], mObj["role"])
					}
				}
			})
			return nil
		}

		var lastTasklist map[string]interface{}
		if setStr != "" {

			getResp, err := runtime.DoAPI(&larkcore.ApiReq{
				HttpMethod:  http.MethodGet,
				ApiPath:     "/open-apis/task/v2/tasklists/" + tlId,
				QueryParams: queryParams,
			})

			var getResult map[string]interface{}
			if err == nil {
				if parseErr := json.Unmarshal(getResp.RawBody, &getResult); parseErr != nil {
					return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse tasklist details")
				}
			}

			data, err := HandleTaskApiResult(getResult, err, "get tasklist details for set")
			if err != nil {
				return err
			}
			lastTasklist, _ = data["tasklist"].(map[string]interface{})

			var existingIds []string
			if members, ok := lastTasklist["members"].([]interface{}); ok {
				for _, m := range members {
					if mObj, ok := m.(map[string]interface{}); ok {
						if id, ok := mObj["id"].(string); ok {
							existingIds = append(existingIds, id)
						}
					}
				}
			}

			targetIds := strings.Split(setStr, ",")
			var targetClean []string
			for _, t := range targetIds {
				t = strings.TrimSpace(t)
				if t != "" {
					targetClean = append(targetClean, t)
				}
			}

			// Diff
			var toAdd []string
			var toRemove []string

			for _, t := range targetClean {
				if !contains(existingIds, t) {
					toAdd = append(toAdd, t)
				}
			}
			for _, e := range existingIds {
				if !contains(targetClean, e) {
					toRemove = append(toRemove, e)
				}
			}

			if len(toAdd) > 0 {
				body := buildTlMembersBody(strings.Join(toAdd, ","))
				apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
					HttpMethod:  http.MethodPost,
					ApiPath:     "/open-apis/task/v2/tasklists/" + tlId + "/add_members",
					QueryParams: queryParams,
					Body:        body,
				})

				var addResult map[string]interface{}
				if err == nil {
					if parseErr := json.Unmarshal(apiResp.RawBody, &addResult); parseErr != nil {
						return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse add members")
					}
				}

				data, err := HandleTaskApiResult(addResult, err, "add tasklist members")
				if err != nil {
					return err
				}
				lastTasklist, _ = data["tasklist"].(map[string]interface{})
			}

			if len(toRemove) > 0 {
				body := buildTlMembersBody(strings.Join(toRemove, ","))
				apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
					HttpMethod:  http.MethodPost,
					ApiPath:     "/open-apis/task/v2/tasklists/" + tlId + "/remove_members",
					QueryParams: queryParams,
					Body:        body,
				})

				var removeResult map[string]interface{}
				if err == nil {
					if parseErr := json.Unmarshal(apiResp.RawBody, &removeResult); parseErr != nil {
						return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse remove members")
					}
				}

				data, err := HandleTaskApiResult(removeResult, err, "remove tasklist members")
				if err != nil {
					return err
				}
				lastTasklist, _ = data["tasklist"].(map[string]interface{})
			}

		} else {

			if addStr != "" {
				body := buildTlMembersBody(addStr)
				apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
					HttpMethod:  http.MethodPost,
					ApiPath:     "/open-apis/task/v2/tasklists/" + tlId + "/add_members",
					QueryParams: queryParams,
					Body:        body,
				})

				var addResult map[string]interface{}
				if err == nil {
					if parseErr := json.Unmarshal(apiResp.RawBody, &addResult); parseErr != nil {
						return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse add members")
					}
				}

				data, err := HandleTaskApiResult(addResult, err, "add tasklist members")
				if err != nil {
					return err
				}
				lastTasklist, _ = data["tasklist"].(map[string]interface{})
			}

			if removeStr != "" {
				body := buildTlMembersBody(removeStr)
				apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
					HttpMethod:  http.MethodPost,
					ApiPath:     "/open-apis/task/v2/tasklists/" + tlId + "/remove_members",
					QueryParams: queryParams,
					Body:        body,
				})

				var removeResult map[string]interface{}
				if err == nil {
					if parseErr := json.Unmarshal(apiResp.RawBody, &removeResult); parseErr != nil {
						return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse remove members")
					}
				}

				data, err := HandleTaskApiResult(removeResult, err, "remove tasklist members")
				if err != nil {
					return err
				}
				lastTasklist, _ = data["tasklist"].(map[string]interface{})
			}
		}

		tlUrl, _ := lastTasklist["url"].(string)
		tlUrl = truncateTaskURL(tlUrl)

		outData := map[string]interface{}{
			"guid": tlId,
			"url":  tlUrl,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			fmt.Fprintf(w, "✅ Tasklist members updated successfully!\n")
			fmt.Fprintf(w, "Tasklist ID: %s\n", tlId)
			if tlUrl != "" {
				fmt.Fprintf(w, "Tasklist URL: %s\n", tlUrl)
			}
		})
		return nil
	},
}
View Source
var ReminderTask = common.Shortcut{
	Service:     "task",
	Command:     "+reminder",
	Description: "manage task reminders",
	Risk:        "write",
	Scopes:      []string{"task:task:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "task-id", Desc: "task id", Required: true},
		{Name: "set", Desc: "relative fire minutes to set (e.g. 15m, 1h, 1d)"},
		{Name: "remove", Type: "bool", Desc: "removes all existing reminders"},
	},

	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if runtime.Str("set") == "" && !runtime.Bool("remove") {
			return WrapTaskError(ErrCodeTaskInvalidParams, "must specify either --set or --remove", "validate reminder")
		}
		if runtime.Str("set") != "" && runtime.Bool("remove") {
			return WrapTaskError(ErrCodeTaskInvalidParams, "cannot specify both --set and --remove", "validate reminder")
		}
		return nil
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		d := common.NewDryRunAPI()
		taskId := url.PathEscape(runtime.Str("task-id"))

		if runtime.Bool("remove") {
			d.Desc("1. GET task to find existing reminder IDs").
				GET("/open-apis/task/v2/tasks/" + taskId).
				Params(map[string]interface{}{"user_id_type": "open_id"}).
				Desc("2. POST to remove_reminders with found IDs")
		} else if setStr := runtime.Str("set"); setStr != "" {
			d.Desc("1. GET task to check existing reminders").
				GET("/open-apis/task/v2/tasks/" + taskId).
				Params(map[string]interface{}{"user_id_type": "open_id"}).
				Desc("2. POST to remove_reminders if any exist").
				Desc("3. POST to add_reminders").
				POST("/open-apis/task/v2/tasks/" + taskId + "/add_reminders")
		}

		return d
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		taskId := url.PathEscape(runtime.Str("task-id"))
		queryParams := make(larkcore.QueryParams)
		queryParams.Set("user_id_type", "open_id")

		getResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod:  http.MethodGet,
			ApiPath:     "/open-apis/task/v2/tasks/" + taskId,
			QueryParams: queryParams,
		})

		var getResult map[string]interface{}
		if err == nil {
			if parseErr := json.Unmarshal(getResp.RawBody, &getResult); parseErr != nil {
				return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse task details: %v", parseErr), "parse task details")
			}
		}

		data, err := HandleTaskApiResult(getResult, err, "get task reminders")
		if err != nil {
			return err
		}

		taskObj, _ := data["task"].(map[string]interface{})
		reminders, _ := taskObj["reminders"].([]interface{})

		if runtime.Bool("remove") {
			if len(reminders) == 0 {
				runtime.OutFormat(map[string]interface{}{"guid": taskId}, nil, func(w io.Writer) {
					fmt.Fprintln(w, "No existing reminders to remove.")
				})
				return nil
			}

			var reminderIds []string
			for _, r := range reminders {
				if rMap, ok := r.(map[string]interface{}); ok {
					if id, ok := rMap["id"].(string); ok {
						reminderIds = append(reminderIds, id)
					}
				}
			}

			if len(reminderIds) > 0 {
				body := map[string]interface{}{
					"reminder_ids": reminderIds,
				}
				apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
					HttpMethod:  http.MethodPost,
					ApiPath:     "/open-apis/task/v2/tasks/" + taskId + "/remove_reminders",
					QueryParams: queryParams,
					Body:        body,
				})

				var removeResult map[string]interface{}
				if err == nil {
					if parseErr := json.Unmarshal(apiResp.RawBody, &removeResult); parseErr != nil {
						return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse remove response")
					}
				}

				if _, err := HandleTaskApiResult(removeResult, err, "remove task reminders"); err != nil {
					return err
				}
			}
		} else if setStr := runtime.Str("set"); setStr != "" {
			// Parse relative time string (e.g. 15m, 1h, 1d, or plain 30)
			var minutes int
			var parseErr error

			if strings.HasSuffix(setStr, "m") {
				minutes, parseErr = strconv.Atoi(strings.TrimSuffix(setStr, "m"))
			} else if strings.HasSuffix(setStr, "h") {
				h, e := strconv.Atoi(strings.TrimSuffix(setStr, "h"))
				if e == nil {
					minutes = h * 60
				}
				parseErr = e
			} else if strings.HasSuffix(setStr, "d") {
				d, e := strconv.Atoi(strings.TrimSuffix(setStr, "d"))
				if e == nil {
					minutes = d * 24 * 60
				}
				parseErr = e
			} else {

				minutes, parseErr = strconv.Atoi(setStr)
			}

			if parseErr != nil {
				return WrapTaskError(ErrCodeTaskInvalidParams, parseErr.Error(), "set reminder")
			}

			if len(reminders) > 0 {
				var reminderIds []string
				for _, r := range reminders {
					if rMap, ok := r.(map[string]interface{}); ok {
						if id, ok := rMap["id"].(string); ok {
							reminderIds = append(reminderIds, id)
						}
					}
				}

				if len(reminderIds) > 0 {
					body := map[string]interface{}{
						"reminder_ids": reminderIds,
					}
					apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
						HttpMethod:  http.MethodPost,
						ApiPath:     "/open-apis/task/v2/tasks/" + taskId + "/remove_reminders",
						QueryParams: queryParams,
						Body:        body,
					})

					var removeResult map[string]interface{}
					if err == nil {
						if parseErr := json.Unmarshal(apiResp.RawBody, &removeResult); parseErr != nil {
							return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse remove response")
						}
					}

					if _, err := HandleTaskApiResult(removeResult, err, "remove existing task reminders before setting new one"); err != nil {
						return err
					}
				}
			}

			body := map[string]interface{}{
				"reminders": []map[string]interface{}{
					{
						"relative_fire_minute": minutes,
					},
				},
			}
			apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
				HttpMethod:  http.MethodPost,
				ApiPath:     "/open-apis/task/v2/tasks/" + taskId + "/add_reminders",
				QueryParams: queryParams,
				Body:        body,
			})

			var addResult map[string]interface{}
			if err == nil {
				if parseErr := json.Unmarshal(apiResp.RawBody, &addResult); parseErr != nil {
					return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse add response")
				}
			}

			if _, err := HandleTaskApiResult(addResult, err, "add task reminder"); err != nil {
				return err
			}
		}

		urlVal, _ := taskObj["url"].(string)
		urlVal = truncateTaskURL(urlVal)

		outData := map[string]interface{}{
			"guid": taskId,
			"url":  urlVal,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			fmt.Fprintf(w, "✅ Task reminders updated successfully!\n")
			fmt.Fprintf(w, "Task ID: %s\n", taskId)
			if urlVal != "" {
				fmt.Fprintf(w, "Task URL: %s\n", urlVal)
			}
		})
		return nil
	},
}
View Source
var ReopenTask = common.Shortcut{
	Service:     "task",
	Command:     "+reopen",
	Description: "reopen a completed task",
	Risk:        "write",
	Scopes:      []string{"task:task:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "task-id", Desc: "task id", Required: true},
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body := buildReopenBody()
		taskId := url.PathEscape(runtime.Str("task-id"))
		return common.NewDryRunAPI().
			PATCH("/open-apis/task/v2/tasks/" + taskId).
			Params(map[string]interface{}{"user_id_type": "open_id"}).
			Body(body)
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		taskId := url.PathEscape(runtime.Str("task-id"))
		body := buildReopenBody()

		queryParams := make(larkcore.QueryParams)
		queryParams.Set("user_id_type", "open_id")

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod:  http.MethodPatch,
			ApiPath:     "/open-apis/task/v2/tasks/" + taskId,
			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 reopen response")
			}
		}

		data, err := HandleTaskApiResult(result, err, "reopen task")
		if err != nil {
			return err
		}

		task, _ := data["task"].(map[string]interface{})
		guid, _ := task["guid"].(string)
		urlVal, _ := task["url"].(string)
		urlVal = truncateTaskURL(urlVal)

		outData := map[string]interface{}{
			"guid": guid,
			"url":  urlVal,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			summary, _ := task["summary"].(string)
			fmt.Fprintf(w, "✅ Task reopened successfully!\n")
			if guid != "" {
				fmt.Fprintf(w, "Task ID: %s\n", guid)
			}
			if summary != "" {
				fmt.Fprintf(w, "Summary: %s\n", summary)
			}
			if urlVal != "" {
				fmt.Fprintf(w, "Task URL: %s\n", urlVal)
			}
		})
		return nil
	},
}
View Source
var UpdateTask = common.Shortcut{
	Service:     "task",
	Command:     "+update",
	Description: "update task attributes",
	Risk:        "write",
	Scopes:      []string{"task:task:write"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,

	Flags: []common.Flag{
		{Name: "task-id", Desc: "task id (comma-separated for multiple)", Required: true},
		{Name: "summary", Desc: "task title"},
		{Name: "description", Desc: "task description"},
		{Name: "due", Desc: "due date (ISO 8601 / date:YYYY-MM-DD / relative:+2d / ms timestamp)"},
		{Name: "data", Desc: "JSON payload for task object"},
	},

	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body, err := buildTaskUpdateBody(runtime)
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		taskIds := strings.Split(runtime.Str("task-id"), ",")
		taskId := url.PathEscape(strings.TrimSpace(taskIds[0]))
		return common.NewDryRunAPI().
			PATCH("/open-apis/task/v2/tasks/" + taskId).
			Params(map[string]interface{}{"user_id_type": "open_id"}).
			Body(body)
	},

	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		body, err := buildTaskUpdateBody(runtime)
		if err != nil {
			return WrapTaskError(ErrCodeTaskInvalidParams, err.Error(), "update task")
		}

		taskIds := strings.Split(runtime.Str("task-id"), ",")
		var updatedTasks []map[string]interface{}

		for _, taskId := range taskIds {
			taskId = strings.TrimSpace(taskId)
			if taskId == "" {
				continue
			}

			queryParams := make(larkcore.QueryParams)
			queryParams.Set("user_id_type", "open_id")

			apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
				HttpMethod:  http.MethodPatch,
				ApiPath:     "/open-apis/task/v2/tasks/" + url.PathEscape(taskId),
				QueryParams: queryParams,
				Body:        body,
			})

			var result map[string]interface{}
			if err == nil {
				if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil {
					return fmt.Errorf("failed to parse response for task %s: %v", taskId, parseErr)
				}
			}

			data, err := HandleTaskApiResult(result, err, "update task "+taskId)
			if err != nil {
				return err
			}

			taskObj, _ := data["task"].(map[string]interface{})
			if taskObj != nil {
				updatedTasks = append(updatedTasks, taskObj)
			}
		}

		var tasks []map[string]interface{}
		for _, task := range updatedTasks {
			guid, _ := task["guid"].(string)
			urlVal, _ := task["url"].(string)
			urlVal = truncateTaskURL(urlVal)
			tasks = append(tasks, map[string]interface{}{
				"guid": guid,
				"url":  urlVal,
			})
		}

		outData := map[string]interface{}{
			"tasks": tasks,
		}

		runtime.OutFormat(outData, &output.Meta{Count: len(updatedTasks)}, func(w io.Writer) {
			for _, task := range updatedTasks {
				guid, _ := task["guid"].(string)
				summary, _ := task["summary"].(string)
				urlVal, _ := task["url"].(string)
				urlVal = truncateTaskURL(urlVal)
				fmt.Fprintf(w, "✅ Task updated successfully!\n")
				fmt.Fprintf(w, "Task ID: %s\n", guid)
				if summary != "" {
					fmt.Fprintf(w, "Summary: %s\n", summary)
				}
				if urlVal != "" {
					fmt.Fprintf(w, "Task URL: %s\n", urlVal)
				}
				fmt.Fprintln(w, strings.Repeat("-", 20))
			}
		})
		return nil
	},
}

Functions

func HandleTaskApiResult

func HandleTaskApiResult(result interface{}, err error, action string) (map[string]interface{}, error)

HandleTaskApiResult is a wrapper around common.HandleApiResult that applies task-specific error mapping.

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all shortcuts for task and tasklist domain.

func WrapTaskError

func WrapTaskError(larkCode int, rawMsg string, action string) error

WrapTaskError wraps a Lark API error into a standardized ExitError based on task-specific rules.

Types

type TaskErrorInfo

type TaskErrorInfo struct {
	Type     string
	Message  string
	Hint     string
	ExitCode int
}

TaskErrorCode maps Lark error codes to standardized error info.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL