Files
dingtalk-workspace-cli/internal/shortcut/smart/related_tasks.go

227 lines
9.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package smart
import (
"encoding/json"
"fmt"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// RelatedTasks: list ALL todos related to me in one step — the union of the
// todos where I am the creator, the executor, or a participant — mirroring
// the creator+executor+participant roles-union view.
//
// Steps:
//
// 1. list my todos via get_user_todos_in_current_org (pageNum / pageSize as
// strings, mirroring helpers.todo list) with roleTypes defaulting to all
// three enum values ["creator","executor","participant"], so the server
// returns todos where I hold ANY of those roles. creator / executor /
// participant are exactly the values accepted by helpers.parseRoleTypes.
// --role-types (CSV) can override the default; --status is passed through
// as todoStatus, both mirroring helpers.buildListTodoTaskArgs;
//
// 2. dedupe the returned cards by taskId (one todo can appear more than once
// when I hold several roles on it), then project each surviving card to a
// clean {title, status, priority, creator, planFinishDate, taskId} shape
// with defensive multi-key field probing;
//
// 3. print the deduped list via rt.Output so it honours --format/--jq/--fields.
//
// Read-only: it only lists, dedupes and projects, it never mutates any todo.
//
// dws todo +related-tasks
var RelatedTasks = shortcut.Shortcut{
OutputRollout: output.RolloutUnifiedActive,
Service: "todo",
Command: "+get-related-tasks",
Aliases: []string{"+related-tasks"},
Product: "todo",
Description: "一次性列出与我相关的全部待办(我作为创建人/执行人/参与人三种角色的并集,按 taskId 去重)",
Intent: "当你想一次看清『所有和我有关的待办』——不管是我创建(creator)的、指派给我执行(executor)的、还是我作为参与人(participant)协作的——时使用;" +
"内部默认拉取你当前组织下 roleTypes=[\"creator\",\"executor\",\"participant\"] 三种角色的待办并集(这三个值正是待办列表支持的角色枚举)," +
"再在本地按任务 ID(taskId) 去重(同一条待办可能因多角色重复出现),把每条投影成标题、状态、优先级、创建人、计划完成时间和 taskId 打印出来。" +
"可用 --role-types 以逗号分隔覆盖默认角色(取值 creator/executor/participant),可用 --status 透传 todoStatus 过滤状态。" +
"这是纯只读操作,只做列表、去重与投影,不会创建或修改任何待办;若没有与你相关的待办则返回空列表。",
Risk: shortcut.RiskRead,
Safety: contract.SafetySpec{
Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: corecmd.ContractDecl{
Identity: contract.ToolIdentitySpec{ProductID: "todo", Name: "shortcut_get_related_tasks", CanonicalPath: "todo.shortcut_get_related_tasks", CLIPath: "todo +get-related-tasks", PrimaryCLIPath: "todo +get-related-tasks", Aliases: []string{"todo +related-tasks"}},
Description: "一次性列出与我相关的全部待办(创建人/执行人/参与人并集)",
Interface: &contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "Reviewed Todo composite: strict full pagination, role union, deduplication and projection are owned by the executable shortcut."},
Selection: contract.SelectionSpec{
AgentSummary: "列出与我相关的全部待办",
UseWhen: []string{"需要查看我作为创建人、执行人或参与人的全部待办并集时"},
AvoidWhen: []string{"只需默认执行人视角列表时使用 todo +get-my-tasks"},
Examples: []string{"dws todo +get-related-tasks"},
},
Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"与当前用户相关的待办并集","properties":{"count":{"type":"integer","description":"去重后的任务数量"},"tasks":{"type":"array","description":"按 taskId 去重的任务","items":{"type":"object","description":"待办条目","additionalProperties":true}}},"required":["count","tasks"],"additionalProperties":false}`)},
},
Flags: []shortcut.Flag{
{
Name: "role-types",
Type: shortcut.FlagString,
Desc: "覆盖默认角色范围,逗号分隔,取值 creator/executor/participant;不传则默认三者并集",
},
{
Name: "status",
Type: shortcut.FlagString,
Desc: "按 todoStatus 过滤(透传给 get_user_todos_in_current_org)",
},
},
Tips: []string{
`dws todo +get-related-tasks`,
`dws todo +get-related-tasks --role-types creator,executor`,
`dws todo +get-related-tasks --status TODO`,
},
Execute: func(rt *shortcut.RuntimeContext) error {
// Step 1 — build params. roleTypes defaults to the three-role union;
// --role-types overrides it (validated like helpers.parseRoleTypes).
roleTypes := []string{"creator", "executor", "participant"}
if rt.Changed("role-types") {
parsed, err := parseRelatedRoleTypes(rt.Str("role-types"))
if err != nil {
return err
}
if len(parsed) > 0 {
roleTypes = parsed
}
}
params := map[string]any{
"roleTypes": roleTypes,
}
if rt.Changed("status") {
if v := strings.TrimSpace(rt.Str("status")); v != "" {
params["todoStatus"] = v
}
}
// List ALL related cards across pages so a todo beyond the first page is
// not silently dropped from the union/dedupe.
cards, err := shortcutListAllTodoCards(rt, params)
if err != nil {
return err
}
// Step 2 — dedupe by taskId and project.
seen := make(map[string]bool, len(cards))
results := make([]map[string]any, 0, len(cards))
for _, m := range cards {
taskID := shortcutRelatedTaskID(m)
if taskID != "" {
if seen[taskID] {
continue
}
seen[taskID] = true
}
results = append(results, shortcutRelatedProject(m, taskID))
}
// Step 3 — print the deduped, projected list.
return rt.Output(map[string]any{"tasks": results, "count": len(results)})
},
}
// parseRelatedRoleTypes parses a comma-separated role-types string and validates
// each value, mirroring helpers.parseRoleTypes. Only creator / executor /
// participant are allowed.
func parseRelatedRoleTypes(s string) ([]string, error) {
allowed := map[string]bool{"creator": true, "executor": true, "participant": true}
out := make([]string, 0, 3)
for _, part := range strings.Split(s, ",") {
p := strings.TrimSpace(part)
if p == "" {
continue
}
if !allowed[p] {
return nil, apperrors.NewValidation(fmt.Sprintf("非法的 role-type %q,合法取值:creator, executor, participant", p))
}
out = append(out, p)
}
return out, nil
}
// shortcutRelatedTaskID reads a card's task id, probing common aliases and
// tolerating both string and numeric JSON encodings.
func shortcutRelatedTaskID(m map[string]any) string {
for _, key := range []string{"taskId", "todoId", "id"} {
switch v := m[key].(type) {
case string:
if s := strings.TrimSpace(v); s != "" {
return s
}
case float64:
if v != 0 {
return fmt.Sprintf("%.0f", v)
}
case int64:
if v != 0 {
return fmt.Sprintf("%d", v)
}
}
}
return ""
}
// shortcutRelatedProject projects a raw todo card to a clean output shape,
// probing multiple candidate keys per field defensively.
func shortcutRelatedProject(m map[string]any, taskID string) map[string]any {
item := map[string]any{"taskId": taskID}
if v := shortcutRelatedStr(m, "subject", "title", "name"); v != "" {
item["title"] = v
}
if v := shortcutRelatedStr(m, "todoStatus", "status", "finalStatusStage", "statusStage"); v != "" {
item["status"] = v
}
if v, ok := m["priority"]; ok {
item["priority"] = v
}
if v := shortcutRelatedStr(m, "creatorName", "creator", "creatorId"); v != "" {
item["creator"] = v
}
if due, ok := shortcutOverdueDueTime(m); ok {
item["planFinishDate"] = due
}
return item
}
// shortcutRelatedStr returns the first non-empty string value among the given
// candidate keys.
func shortcutRelatedStr(m map[string]any, keys ...string) string {
for _, key := range keys {
if s, ok := m[key].(string); ok {
if s = strings.TrimSpace(s); s != "" {
return s
}
}
}
return ""
}
func init() {
RelatedTasks.Contract.Selection.AgentSummary = RelatedTasks.Description
RelatedTasks.Contract.Selection.UseWhen = []string{RelatedTasks.Intent}
shortcut.Register(RelatedTasks)
}