// 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 helpers import ( "fmt" "strings" "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" "github.com/spf13/cobra" ) func init() { RegisterPublic(func() Handler { return attendanceHandler{} }) } type attendanceHandler struct{} func (attendanceHandler) Name() string { return "attendance" } func (attendanceHandler) Command(runner executor.Runner) *cobra.Command { root := &cobra.Command{ Use: "attendance", Short: "考勤打卡 / 排班 / 统计", Long: `管理钉钉考勤:查询个人考勤详情、批量查询排班、获取考勤统计摘要、查询考勤组与规则。 子命令: record 考勤记录(个人考勤详情) shift 排班管理(批量查询班次 / 排班信息) summary 获取考勤统计摘要 rules 查询考勤组与考勤规则`, Args: cobra.NoArgs, TraverseChildren: true, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, } record := &cobra.Command{ Use: "record", Short: "考勤记录", Args: cobra.NoArgs, TraverseChildren: true, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, } record.AddCommand( newAttendanceRecordGetCommand(runner), newAttendanceRecordListCommand(runner), ) shift := &cobra.Command{ Use: "shift", Short: "排班管理", Args: cobra.NoArgs, TraverseChildren: true, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, } shift.AddCommand(newAttendanceShiftListCommand(runner)) root.AddCommand( record, shift, newAttendanceSummaryCommand(runner), newAttendanceRulesCommand(runner), ) return root } // ── record get ───────────────────────────────────────────── func newAttendanceRecordGetCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ Use: "get", Short: "查询个人考勤详情", Example: " dws attendance record get --user USER_ID --date 2026-03-08", Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { userID, _ := cmd.Flags().GetString("user") dateStr, _ := cmd.Flags().GetString("date") if userID == "" { return apperrors.NewValidation("--user is required") } if dateStr == "" { return apperrors.NewValidation("--date is required (format: YYYY-MM-DD)") } t, err := time.ParseInLocation("2006-01-02", dateStr, time.Local) if err != nil { return apperrors.NewValidation("--date format error, use YYYY-MM-DD, e.g. 2026-03-08") } params := map[string]any{ "userId": userID, "workDate": t.UnixMilli(), } if commandDryRun(cmd) { return writeCommandPayload(cmd, executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "get_user_attendance_record", params, )) } result, err := runner.Run(cmd.Context(), executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "get_user_attendance_record", params, )) if err != nil { return err } return writeCommandPayload(cmd, result) }, } cmd.Flags().String("user", "", "钉钉用户 ID (必填)") cmd.Flags().String("date", "", "查询日期,格式 YYYY-MM-DD (必填)") preferLegacyLeaf(cmd) return cmd } // ── record list ──────────────────────────────────────────── func newAttendanceRecordListCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "批量查询考勤打卡记录", Long: "批量查询多个员工在指定日期范围的考勤打卡记录。", Hidden: true, Example: " dws attendance record list --users userId1,userId2 --start 2026-03-03 --end 2026-03-07", Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { usersStr, _ := cmd.Flags().GetString("users") startStr, _ := cmd.Flags().GetString("start") endStr, _ := cmd.Flags().GetString("end") if usersStr == "" { return apperrors.NewValidation("--users is required") } if startStr == "" { return apperrors.NewValidation("--start is required (format: YYYY-MM-DD)") } if endStr == "" { return apperrors.NewValidation("--end is required (format: YYYY-MM-DD)") } startT, err := time.ParseInLocation("2006-01-02", startStr, time.Local) if err != nil { return apperrors.NewValidation("--start date format error, use YYYY-MM-DD") } endT, err := time.ParseInLocation("2006-01-02", endStr, time.Local) if err != nil { return apperrors.NewValidation("--end date format error, use YYYY-MM-DD") } var userIds []any for _, u := range strings.Split(usersStr, ",") { if s := strings.TrimSpace(u); s != "" { userIds = append(userIds, s) } } params := map[string]any{ "userIds": userIds, "fromDateTime": startT.UnixMilli(), "toDateTime": endT.UnixMilli(), } if commandDryRun(cmd) { return writeCommandPayload(cmd, executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "get_user_attendance_record", params, )) } result, err := runner.Run(cmd.Context(), executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "get_user_attendance_record", params, )) if err != nil { return err } return writeCommandPayload(cmd, result) }, } cmd.Flags().String("users", "", "Comma-separated user ID list (required)") cmd.Flags().String("start", "", "Start date, format YYYY-MM-DD (required)") cmd.Flags().String("end", "", "End date, format YYYY-MM-DD (required)") preferLegacyLeaf(cmd) return cmd } // ── shift list ───────────────────────────────────────────── func newAttendanceShiftListCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "批量查询员工班次信息", Long: "批量查询多个员工在指定日期的班次信息。最多 7 天,最多 50 人。", Example: " dws attendance shift list --users userId1,userId2 --start 2026-03-03 --end 2026-03-07", Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { usersStr, _ := cmd.Flags().GetString("users") startStr, _ := cmd.Flags().GetString("start") endStr, _ := cmd.Flags().GetString("end") if usersStr == "" { return apperrors.NewValidation("--users is required") } if startStr == "" { return apperrors.NewValidation("--start is required (format: YYYY-MM-DD)") } if endStr == "" { return apperrors.NewValidation("--end is required (format: YYYY-MM-DD)") } startT, err := time.ParseInLocation("2006-01-02", startStr, time.Local) if err != nil { return apperrors.NewValidation("--start date format error, use YYYY-MM-DD") } endT, err := time.ParseInLocation("2006-01-02", endStr, time.Local) if err != nil { return apperrors.NewValidation("--end date format error, use YYYY-MM-DD") } var userIds []any for _, u := range strings.Split(usersStr, ",") { if s := strings.TrimSpace(u); s != "" { userIds = append(userIds, s) } } params := map[string]any{ "userIds": userIds, "fromDateTime": startT.UnixMilli(), "toDateTime": endT.UnixMilli(), } if commandDryRun(cmd) { return writeCommandPayload(cmd, executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "batch_get_employee_shifts", params, )) } result, err := runner.Run(cmd.Context(), executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "batch_get_employee_shifts", params, )) if err != nil { return err } return writeCommandPayload(cmd, result) }, } cmd.Flags().String("users", "", "Comma-separated user ID list, max 50 (required)") cmd.Flags().String("start", "", "Start date, format YYYY-MM-DD (required)") cmd.Flags().String("end", "", "End date, format YYYY-MM-DD (required)") preferLegacyLeaf(cmd) return cmd } // ── summary ──────────────────────────────────────────────── func newAttendanceSummaryCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ Use: "summary", Short: "查询某个人的考勤统计摘要", Long: `查询某个人的考勤统计摘要。 --user、--date、--stats-type 均必填。 钉钉服务端业务层强制要求 --stats-type(week/month),不填会返回 C0002 统计类型错误。`, Example: ` dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type month dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type week`, Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { userID, _ := cmd.Flags().GetString("user") workDateStr, _ := cmd.Flags().GetString("date") statsType, _ := cmd.Flags().GetString("stats-type") if userID == "" { return apperrors.NewValidation("--user is required, provide DingTalk user ID") } if workDateStr == "" { return apperrors.NewValidation("--date is required, format yyyy-MM-dd HH:mm:ss") } _, err := time.ParseInLocation("2006-01-02 15:04:05", workDateStr, time.Local) if err != nil { return apperrors.NewValidation("--date format error, use yyyy-MM-dd HH:mm:ss") } if statsType == "" { return apperrors.NewValidation(`--stats-type is required (week|month), enforced by DingTalk server`) } if statsType != "week" && statsType != "month" { return apperrors.NewValidation(`--stats-type must be "week" or "month"`) } // Build nested structure QueryUserAttendVO vo := map[string]any{ "userId": userID, "queryDate": workDateStr, "statsType": statsType, } params := map[string]any{ "QueryUserAttendVO": vo, } if commandDryRun(cmd) { return writeCommandPayload(cmd, executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "get_attendance_summary", params, )) } result, err := runner.Run(cmd.Context(), executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "get_attendance_summary", params, )) if err != nil { return err } return writeCommandPayload(cmd, result) }, } cmd.Flags().String("user", "", "钉钉用户 ID(必填)") cmd.Flags().String("date", "", "工作日期,格式 yyyy-MM-dd HH:mm:ss,如 2026-03-12 15:00:00(必填)") cmd.Flags().String("stats-type", "", "统计类型:week(周统计)或 month(月统计)(必填,钉钉服务端业务层强制要求)") preferLegacyLeaf(cmd) return cmd } // ── rules ────────────────────────────────────────────────── func newAttendanceRulesCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ Use: "rules", Short: "查询考勤组与考勤规则", Long: "调用 MCP 工具 query_attendance_group_or_rules 查询考勤组/考勤规则。\n例如:我属于哪个考勤组、打卡范围是什么、弹性工时怎么算。", Example: " dws attendance rules --date 2026-03-14\n dws attendance rules --date \"2026-03-14 09:00:00\"", Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { dateStr, _ := cmd.Flags().GetString("date") if dateStr == "" { return apperrors.NewValidation("--date is required") } // Support YYYY-MM-DD or yyyy-MM-dd HH:mm:ss, normalize to yyyy-MM-dd HH:mm:ss var dateFormatted string if t, err := time.ParseInLocation("2006-01-02 15:04:05", dateStr, time.Local); err == nil { dateFormatted = t.Format("2006-01-02 15:04:05") } else if t, err := time.ParseInLocation("2006-01-02", dateStr, time.Local); err == nil { dateFormatted = t.Format("2006-01-02 15:04:05") } else { return apperrors.NewValidation(fmt.Sprintf("--date format error, use YYYY-MM-DD or yyyy-MM-dd HH:mm:ss: %s", dateStr)) } params := map[string]any{ "date": dateFormatted, } if commandDryRun(cmd) { return writeCommandPayload(cmd, executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "query_attendance_group_or_rules", params, )) } result, err := runner.Run(cmd.Context(), executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "attendance", "query_attendance_group_or_rules", params, )) if err != nil { return err } return writeCommandPayload(cmd, result) }, } cmd.Flags().String("date", "", "考勤日期,格式 YYYY-MM-DD 或 yyyy-MM-dd HH:mm:ss (必填)") preferLegacyLeaf(cmd) return cmd }