227 lines
9.2 KiB
Go
227 lines
9.2 KiB
Go
// 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)
|
||
}
|