Files
dingtalk-workspace-cli/internal/shortcut/smart/report_latest.go
DennisandClaude Opus 4.8 4e4705c673 feat(shortcut): smart orchestration layer (68 shortcuts)
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>
2026-07-10 10:04:10 +08:00

221 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}