196 lines
7.2 KiB
Go
196 lines
7.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"
|
|
"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)
|
|
}
|