Multi-step / intelligent shortcuts under internal/shortcut/smart: name→ID resolvers (user/base/table/dept/space), name-based actions (chat +dm/+broadcast/ +group-members, todo +assign, calendar +book with rollback/+free/+invite/ +suggest-time), time & self intelligence (calendar +today/+tomorrow/+week/ +next-event/+my-free and +conflicts/+free-slots scheduling intelligence), convenience reads (contact +me, oa +pending/+done-approvals, todo +due-today/ +related-tasks, mail +recent-mail/+find-mail-user, attendance +this-month) and aggregation (minutes +detail, aitable +record-share-links). Projections hardened against real DingTalk responses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
221 lines
7.5 KiB
Go
221 lines
7.5 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 (
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||
)
|
||
|
||
// ReportLatest: fetch the detail of MY most recently submitted report (日志/汇报)
|
||
// in one step (DingTalk-native).
|
||
//
|
||
// Steps:
|
||
//
|
||
// 1. list the reports I sent via get_send_report_list. cursor/size/startTime/
|
||
// endTime mirror helpers.runReportSent — the server caps a single query span
|
||
// at 20 days, so we default to the last-20-days window just like the helper.
|
||
// An optional --keyword is passed through as report_template_name (the same
|
||
// filter the helper exposes as --template-name).
|
||
//
|
||
// 2. locate the newest entry: the item with the largest create time
|
||
// (createTime/gmtCreate/sendTime), falling back to the first item that
|
||
// carries a reportId (lists come back newest-first).
|
||
//
|
||
// 3. print that report's body via get_report_entry_details (report_id param
|
||
// mirrors helpers.runReportDetail). If nothing carries a reportId we fall
|
||
// back to printing the picked list row via rt.Output.
|
||
//
|
||
// If I have not sent any report in the window it reports "暂无日志" instead of
|
||
// failing obscurely.
|
||
//
|
||
// dws report +report-latest
|
||
// dws report +report-latest --keyword 日报
|
||
var ReportLatest = shortcut.Shortcut{
|
||
Service: "report",
|
||
Command: "+report-latest",
|
||
Product: "report",
|
||
Description: "取我最新提交的一篇日志/汇报详情(钉钉原生)",
|
||
Intent: "当你只想快速看回自己最近发出的一篇日志/汇报,却不想先翻发件箱列表、复制 reportId 再查详情时使用;" +
|
||
"内部先列出你近期发出的日志(默认最近 20 天,服务端单次查询跨度上限 20 天;可用 --keyword 按日志模板名过滤)," +
|
||
"自动挑出创建时间最新的一条,再拉取它的正文详情(字段明细、发送人、时间、钉钉跳转链接等)。" +
|
||
"这是只读操作,不会创建或修改任何日志;若这段时间内你没有发出过日志则提示「暂无日志」。",
|
||
Risk: shortcut.RiskRead,
|
||
Flags: []shortcut.Flag{
|
||
{Name: "keyword", Type: shortcut.FlagString, Desc: "按日志模板名过滤(可选,对应 report outbox list 的 --template-name)", Required: false},
|
||
},
|
||
Tips: []string{
|
||
`dws report +report-latest`,
|
||
`dws report +report-latest --keyword 日报`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
// Step 1 — list the reports I sent. Mirror helpers.runReportSent: the
|
||
// server caps a single get_send_report_list query at a 20-day span, so
|
||
// default startTime/endTime to the last-20-days window. cursor/size are
|
||
// floats and report_template_name is the optional keyword filter.
|
||
now := time.Now()
|
||
startMs := now.AddDate(0, 0, -20).Truncate(24 * time.Hour).UnixMilli()
|
||
endMs := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).UnixMilli()
|
||
|
||
listArgs := map[string]any{
|
||
"cursor": float64(0),
|
||
"size": float64(20),
|
||
"startTime": float64(startMs),
|
||
"endTime": float64(endMs),
|
||
}
|
||
if kw := strings.TrimSpace(rt.Str("keyword")); kw != "" {
|
||
listArgs["report_template_name"] = kw
|
||
}
|
||
|
||
data, err := rt.CallMCPData("report", "get_send_report_list", listArgs)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Step 2 — pick the newest sent report.
|
||
row, reportID := shortcutReportLatestPick(data)
|
||
if row == nil {
|
||
return apperrors.NewValidation("暂无日志")
|
||
}
|
||
|
||
// Step 3 — print its detail (report_id mirrors helpers.runReportDetail).
|
||
// If we somehow could not read a reportId, fall back to the list row.
|
||
if reportID == "" {
|
||
return rt.Output(row)
|
||
}
|
||
return rt.CallMCP("get_report_entry_details", map[string]any{
|
||
"report_id": reportID,
|
||
})
|
||
},
|
||
}
|
||
|
||
// shortcutReportLatestPick walks a get_send_report_list response, finds the
|
||
// report entries, and returns the newest one (largest create time, falling back
|
||
// to the first entry that carries a reportId) together with its reportId.
|
||
// Returns (nil, "") when there are no entries.
|
||
func shortcutReportLatestPick(data map[string]any) (map[string]any, string) {
|
||
items := shortcutReportLatestItems(data)
|
||
if len(items) == 0 {
|
||
return nil, ""
|
||
}
|
||
|
||
var firstItem map[string]any
|
||
firstID := ""
|
||
var bestItem map[string]any
|
||
bestID := ""
|
||
var bestTime int64
|
||
haveTime := false
|
||
|
||
for _, m := range items {
|
||
id := shortcutReportLatestID(m)
|
||
if id == "" {
|
||
continue
|
||
}
|
||
if firstItem == nil {
|
||
firstItem = m
|
||
firstID = id
|
||
}
|
||
if t, ok := shortcutReportLatestCreateMillis(m); ok {
|
||
if !haveTime || t > bestTime {
|
||
haveTime = true
|
||
bestTime = t
|
||
bestItem = m
|
||
bestID = id
|
||
}
|
||
}
|
||
}
|
||
|
||
if haveTime && bestItem != nil {
|
||
return bestItem, bestID
|
||
}
|
||
if firstItem != nil {
|
||
return firstItem, firstID
|
||
}
|
||
// No entry carried a reportId; still surface the first raw item so the
|
||
// caller can print something rather than mis-report "暂无日志".
|
||
return items[0], ""
|
||
}
|
||
|
||
// shortcutReportLatestItems pulls the report entries out of a
|
||
// get_send_report_list response, probing the same container keys the helper's
|
||
// findReportListItems uses (list/items/data/reports/records/report_list/
|
||
// reportList nested under the top level or under result).
|
||
func shortcutReportLatestItems(data map[string]any) []map[string]any {
|
||
keys := []string{"list", "items", "data", "reports", "records", "report_list", "reportList", "result"}
|
||
for _, key := range keys {
|
||
if arr, ok := data[key].([]any); ok {
|
||
return shortcutReportLatestToMaps(arr)
|
||
}
|
||
if inner, ok := data[key].(map[string]any); ok {
|
||
for _, k2 := range []string{"list", "items", "data", "reports", "records", "report_list", "reportList"} {
|
||
if arr, ok := inner[k2].([]any); ok {
|
||
return shortcutReportLatestToMaps(arr)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func shortcutReportLatestToMaps(arr []any) []map[string]any {
|
||
out := make([]map[string]any, 0, len(arr))
|
||
for _, it := range arr {
|
||
if m, ok := it.(map[string]any); ok {
|
||
out = append(out, m)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// shortcutReportLatestID reads an entry's reportId, mirroring the key aliases in
|
||
// helpers.reportIDFromMap.
|
||
func shortcutReportLatestID(m map[string]any) string {
|
||
for _, key := range []string{"reportId", "reportID", "report_id", "report_Id", "report-id"} {
|
||
if id, ok := m[key].(string); ok {
|
||
if id = strings.TrimSpace(id); id != "" {
|
||
return id
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// shortcutReportLatestCreateMillis reads an entry's create time in millis,
|
||
// mirroring the create-time keys helpers.reportReadableCreateTimeMillis probes.
|
||
func shortcutReportLatestCreateMillis(m map[string]any) (int64, bool) {
|
||
for _, key := range []string{"createTime", "gmtCreate", "sendTime", "time", "modifiedTime"} {
|
||
switch v := m[key].(type) {
|
||
case float64:
|
||
if v > 0 {
|
||
return int64(v), true
|
||
}
|
||
case string:
|
||
if s := strings.TrimSpace(v); s != "" {
|
||
if n, err := strconv.ParseInt(s, 10, 64); err == nil && n > 0 {
|
||
return n, true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
func init() {
|
||
shortcut.Register(ReportLatest)
|
||
}
|