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

151 lines
6.8 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"
"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"
)
// DueToday: list MY todos whose planFinishDate falls within today.
//
// Unlike +overdue (which locally keeps cards whose dueTime is already in the
// past), this shortcut filters SERVER-SIDE: it passes the today window as
// planFinishDateStart / planFinishDateEnd (epoch millis) to
// get_user_todos_in_current_org, so the backend returns only todos due today.
//
// Steps:
//
// 1. compute the [today 00:00, tomorrow 00:00) window in the local timezone,
// as epoch millis;
//
// 2. list my todos via get_user_todos_in_current_org with
// planFinishDateStart = today 00:00 ms, planFinishDateEnd = tomorrow 00:00
// ms, roleTypes=["executor"] (mirroring +overdue), pageNum/pageSize as
// strings (mirroring helpers.todo list);
//
// 3. project each card to a clean {title, status, priority, creator,
// planFinishDate, taskId} shape and print via rt.Output so it honours
// --format/--jq/--fields.
//
// Read-only: it never mutates any todo, it only lists and projects.
//
// dws todo +due-today
var DueToday = shortcut.Shortcut{
OutputRollout: output.RolloutUnifiedActive,
Service: "todo",
Command: "+due-today",
Product: "todo",
Description: "列出我今天到期的待办",
Intent: "当你想快速看清自己今天(planFinishDate 落在今天 00:00 到次日 00:00 之间)到期的待办、方便安排一天的工作时使用;" +
"内部按今天的本地时间窗,把 planFinishDateStart=今天0点、planFinishDateEnd=次日0点(毫秒时间戳)传给 get_user_todos_in_current_org 做服务端过滤," +
"默认拉取你作为执行人(executor)的待办,可用 --role-types 覆盖角色范围,最后只打印这些今天到期待办的标题、状态、优先级、创建人、到期时间和任务 ID。" +
"这与 +overdue(已过期)不同:+overdue 看的是已经过了截止时间的待办,本命令看的是今天当天到期的待办。" +
"这是纯只读操作,只做列表与投影,不会修改或完成任何待办;若今天没有到期的待办则返回错误提示。",
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_due_today", CanonicalPath: "todo.shortcut_due_today", CLIPath: "todo +due-today", PrimaryCLIPath: "todo +due-today"},
Description: "列出我今天到期的待办",
Interface: &contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "Reviewed Todo composite: local-day window construction, strict full pagination and projection are owned by the executable shortcut."},
Selection: contract.SelectionSpec{
AgentSummary: "列出我今天到期的待办",
UseWhen: []string{"需要查看本地日历日今天到期的待办时"},
AvoidWhen: []string{"查看已经逾期的任务时使用 todo +overdue"},
Examples: []string{"dws todo +due-today"},
},
Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"今天到期的待办","properties":{"count":{"type":"integer","description":"任务数量"},"tasks":{"type":"array","description":"今天到期的任务","items":{"type":"object","description":"待办条目","additionalProperties":true}}},"required":["count","tasks"],"additionalProperties":false}`)},
},
Flags: []shortcut.Flag{
{
Name: "role-types",
Type: shortcut.FlagString,
Desc: "覆盖默认角色范围,逗号分隔,取值 creator/executor/participant;不传则默认 executor",
},
},
Tips: []string{
`dws todo +due-today`,
`dws todo +due-today --role-types creator,executor`,
},
Execute: func(rt *shortcut.RuntimeContext) error {
// Step 1 — compute [today 00:00, tomorrow 00:00) in the local timezone.
now := time.Now()
loc := now.Location()
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
startOfNextDay := startOfDay.AddDate(0, 0, 1)
startMs := startOfDay.UnixMilli()
endMs := startOfNextDay.UnixMilli()
// Step 2 — build params. roleTypes defaults to executor (like +overdue);
// --role-types overrides it (validated like helpers.parseRoleTypes). The
// today window is passed as planFinishDateStart / planFinishDateEnd so the
// backend filters server-side.
roleTypes := []string{"executor"}
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,
"planFinishDateStart": startMs,
"planFinishDateEnd": endMs,
}
// List ALL matching cards across pages so a todo due today beyond the
// first page is not silently dropped.
cards, err := shortcutListAllTodoCards(rt, params)
if err != nil {
return err
}
// Step 3 — project the cards. The server already filtered to today's
// window; we keep a defensive local guard against any planFinishDate that
// leaks outside [startMs, endMs).
results := make([]map[string]any, 0, len(cards))
for _, m := range cards {
due, ok := shortcutOverdueDueTime(m)
if !ok {
return shortcutTodoResponseError("malformed_due_time", "今天到期结果缺少可解析的 dueTime")
}
if due < startMs || due >= endMs {
continue
}
taskID := shortcutRelatedTaskID(m) // reused from related_tasks.go
results = append(results, shortcutRelatedProject(m, taskID)) // reused from related_tasks.go
}
// Step 4 — print the projected list.
return rt.Output(map[string]any{"count": len(results), "tasks": results})
},
}
func init() {
DueToday.Contract.Selection.AgentSummary = DueToday.Description
DueToday.Contract.Selection.UseWhen = []string{DueToday.Intent}
shortcut.Register(DueToday)
}