// 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" "strings" "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) // Overdue: list MY overdue-and-unfinished todos in one step. // // Steps: // // 1. list my todos via get_user_todos_in_current_org (pageNum / pageSize as // strings, roleTypes=["executor"], mirroring helpers.todo list); // // 2. in Go, keep only the cards whose dueTime exists and is strictly before // time.Now().UnixMilli() AND that are not yet done (isDone not true and // finalStatusStage not a completed marker) — field parsing is defensive; // // 3. project each surviving card to {subject, dueTime, taskId} and print the // list via rt.Output so it honours --format/--jq/--fields. // // Read-only: it never mutates any todo, it only lists and filters locally. // // dws todo +overdue var Overdue = shortcut.Shortcut{ OutputRollout: output.RolloutUnifiedActive, Service: "todo", Command: "+overdue", Product: "todo", Description: "列出我已过期未完成的待办", Intent: "当你想快速看清自己有哪些待办已经过了截止时间却还没做完、方便优先处理时使用;" + "内部先拉取你当前组织下作为执行人(executor)的待办列表,再在本地按「有截止时间(dueTime) 且早于当前时刻 且尚未完成」的条件筛选," + "最后只打印这些逾期待办的标题(subject)、截止时间(dueTime) 和任务 ID(taskId)。" + "这是纯只读操作,只做列表与本地过滤,不会修改或完成任何待办;若没有逾期待办则返回空列表。", 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_overdue", CanonicalPath: "todo.shortcut_overdue", CLIPath: "todo +overdue", PrimaryCLIPath: "todo +overdue", }, Description: "列出我已过期未完成的待办", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"逾期未完成待办","properties":{"overdue":{"type":"array","description":"逾期未完成任务","items":{"type":"object","description":"待办条目","additionalProperties":true}}},"required":["overdue"],"additionalProperties":false}`)}, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出我已过期未完成的待办", UseWhen: []string{"当你想快速看清自己有哪些待办已经过了截止时间却还没做完、方便优先处理时使用;内部先拉取你当前组织下作为执行人(executor)的待办列表,再在本地按「有截止时间(dueTime) 且早于当前时刻 且尚未完成」的条件筛选,最后只打印这些逾期待办的标题(subject)、截止时间(dueTime) 和任务 ID(taskId)。这是纯只读操作,只做列表与本地过滤,不会修改或完成任何待办;若没有逾期待办则返回空列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws todo +overdue"}, }, }, Flags: []shortcut.Flag{}, Tips: []string{ `dws todo +overdue`, }, Execute: func(rt *shortcut.RuntimeContext) error { // Step 1 — list ALL my todos across pages (roleTypes defaults to executor, // mirroring helpers.buildListTodoTaskArgs) so an overdue todo beyond the // first page is not silently undercounted. cards, err := shortcutListAllTodoCards(rt, map[string]any{ "roleTypes": []string{"executor"}, }) if err != nil { return err } // Step 2 — filter locally: dueTime present, in the past, not done. now := time.Now().UnixMilli() overdue := make([]map[string]any, 0, len(cards)) for _, m := range cards { due, ok := shortcutOverdueDueTime(m) if !ok || due >= now { continue } if shortcutOverdueIsDone(m) { continue } taskID := shortcutTodoTaskID(m) // reused from todo_done.go subject, _ := m["subject"].(string) overdue = append(overdue, map[string]any{ "subject": subject, "dueTime": due, "taskId": taskID, }) } // Step 3 — print the projected overdue list. return rt.Output(map[string]any{"overdue": overdue}) }, } // shortcutOverdueDueTime reads a card's dueTime as epoch millis, tolerating both // numeric (float64) and string JSON encodings. Returns ok=false when the field // is absent, zero, or unparseable. func shortcutOverdueDueTime(m map[string]any) (int64, bool) { for _, key := range []string{"dueTime", "planFinishDate", "gmtDue"} { switch v := m[key].(type) { case float64: if v > 0 { return int64(v), true } case int64: if v > 0 { return v, true } case string: s := strings.TrimSpace(v) if s == "" { continue } // dueTime is normally epoch millis; parse leniently. var n int64 var neg bool ok := len(s) > 0 for i, c := range s { if i == 0 && c == '-' { neg = true continue } if c < '0' || c > '9' { ok = false break } n = n*10 + int64(c-'0') } if ok && n > 0 && !neg { return n, true } } } return 0, false } // shortcutOverdueIsDone reports whether a card is already completed, probing the // common completion markers defensively: a boolean isDone, or a // finalStatusStage/statusStage string that reads as finished/done. func shortcutOverdueIsDone(m map[string]any) bool { switch v := m["isDone"].(type) { case bool: if v { return true } case string: if strings.EqualFold(strings.TrimSpace(v), "true") { return true } } if v, ok := m["done"].(bool); ok && v { return true } for _, key := range []string{"finalStatusStage", "statusStage", "status"} { if s, ok := m[key].(string); ok { s = strings.ToUpper(strings.TrimSpace(s)) switch s { case "DONE", "FINISHED", "COMPLETED", "COMPLETE": return true } } } return false } func init() { shortcut.Register(Overdue) }