Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91af2bc3b8 | ||
|
|
e2e8b3bf52 |
@@ -53,6 +53,7 @@ __KEG_ONLY_LINE__
|
||||
Pathname.new(File.join(Dir.home, ".kiro/skills/dws")),
|
||||
Pathname.new(File.join(Dir.home, ".trae/skills/dws")),
|
||||
Pathname.new(File.join(Dir.home, ".openclaw/skills/dws")),
|
||||
Pathname.new(File.join(Dir.home, ".hermes/skills/dws")),
|
||||
]
|
||||
|
||||
targets.each_with_index do |dest, index|
|
||||
|
||||
@@ -22,6 +22,7 @@ const AGENT_DIRS = [
|
||||
".kiro/skills",
|
||||
".trae/skills",
|
||||
".openclaw/skills",
|
||||
".hermes/skills",
|
||||
];
|
||||
|
||||
const PLATFORM_MAP = {
|
||||
|
||||
@@ -268,15 +268,20 @@ func newAttendanceShiftListCommand(runner executor.Runner) *cobra.Command {
|
||||
|
||||
func newAttendanceSummaryCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "summary",
|
||||
Short: "查询某个人的考勤统计摘要",
|
||||
Long: "查询某个人的考勤统计摘要。--user 与 --date 均必填。",
|
||||
Example: ` dws attendance summary --user USER_ID --date "2026-03-12 15:00:00"`,
|
||||
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")
|
||||
}
|
||||
@@ -287,10 +292,17 @@ func newAttendanceSummaryCommand(runner executor.Runner) *cobra.Command {
|
||||
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,
|
||||
@@ -311,6 +323,7 @@ func newAttendanceSummaryCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -19,7 +19,15 @@ const (
|
||||
)
|
||||
|
||||
// knownSkillDirs lists all known Agent skill directories (relative to $HOME).
|
||||
// Kept in sync with build/npm/install.js AGENT_DIRS.
|
||||
// Kept in sync with:
|
||||
// - build/npm/install.js AGENT_DIRS
|
||||
// - scripts/install.sh for-in list
|
||||
// - scripts/install.ps1 $AgentDirs
|
||||
// - scripts/install-skills.sh for-in list
|
||||
// - build/homebrew.rb.tmpl targets
|
||||
// - test/scripts/package_script_test.go expectedPackagedSkillTargets
|
||||
// - scripts/release/verify-package-managers.sh HOME_AGENT_PARENTS / HOME_SKILL_TARGETS
|
||||
//
|
||||
// The first entry (.agents/skills) is always updated; subsequent entries are
|
||||
// only updated when their parent directory already exists.
|
||||
var knownSkillDirs = []string{
|
||||
@@ -36,6 +44,7 @@ var knownSkillDirs = []string{
|
||||
".kiro/skills",
|
||||
".trae/skills",
|
||||
".openclaw/skills",
|
||||
".hermes/skills",
|
||||
}
|
||||
|
||||
// skillDirBlacklist contains parent directories whose skills are managed by
|
||||
|
||||
@@ -116,7 +116,8 @@ install_skills_to_root() {
|
||||
".amp/skills" \
|
||||
".kiro/skills" \
|
||||
".trae/skills" \
|
||||
".openclaw/skills"
|
||||
".openclaw/skills" \
|
||||
".hermes/skills"
|
||||
do
|
||||
base_dir="$root/$agent_dir"
|
||||
parent_gate="$(dirname "$base_dir")"
|
||||
|
||||
+2
-1
@@ -44,7 +44,8 @@ $AgentDirs = @(
|
||||
".amp\skills",
|
||||
".kiro\skills",
|
||||
".trae\skills",
|
||||
".openclaw\skills"
|
||||
".openclaw\skills",
|
||||
".hermes\skills"
|
||||
)
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+2
-1
@@ -201,7 +201,8 @@ install_skills_to_homes() {
|
||||
".amp/skills" \
|
||||
".kiro/skills" \
|
||||
".trae/skills" \
|
||||
".openclaw/skills"
|
||||
".openclaw/skills" \
|
||||
".hermes/skills"
|
||||
do
|
||||
base_dir="$root/$agent_dir"
|
||||
parent_gate="$(dirname "$base_dir")"
|
||||
|
||||
@@ -37,6 +37,7 @@ HOME_AGENT_PARENTS="
|
||||
.kiro
|
||||
.trae
|
||||
.openclaw
|
||||
.hermes
|
||||
"
|
||||
HOME_SKILL_TARGETS="
|
||||
.agents/skills/dws
|
||||
@@ -52,6 +53,7 @@ HOME_SKILL_TARGETS="
|
||||
.kiro/skills/dws
|
||||
.trae/skills/dws
|
||||
.openclaw/skills/dws
|
||||
.hermes/skills/dws
|
||||
"
|
||||
cleanup() {
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
|
||||
@@ -32,12 +32,16 @@ Flags:
|
||||
Usage:
|
||||
dws attendance summary [flags]
|
||||
Example:
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00"
|
||||
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
|
||||
Flags:
|
||||
--date string 工作日期, 格式 yyyy-MM-dd HH:mm:ss (必填)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
--date string 工作日期, 格式 yyyy-MM-dd HH:mm:ss (必填)
|
||||
--stats-type string 统计类型:week(周统计)或 month(月统计)(必填,钉钉服务端业务层强制要求;CLI 层会直接拒绝缺失/非法值)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
```
|
||||
|
||||
> ⚠️ **重要**:`--stats-type` 在钉钉 schema 中标记为 `required: []`(看似可选),但服务端业务层**强制要求**,不传服务端会回 `C0002 / 统计类型错误`。CLI 已在客户端层做了 fail-fast:缺失或非 `week`/`month` 的取值会直接被 CLI 拒绝,不会发出请求。
|
||||
|
||||
### 查询考勤组与考勤规则
|
||||
```
|
||||
Usage:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cli_compat_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -146,7 +148,11 @@ func TestAttendanceShiftList_should_filter_empty_user_ids(t *testing.T) {
|
||||
func TestAttendanceSummary_should_call_tool_with_user_and_date(t *testing.T) {
|
||||
cap := setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{"user": "U001", "date": "2026-03-12 15:00:00"})
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -157,8 +163,9 @@ func TestAttendanceSummary_should_pass_user_and_date_flags(t *testing.T) {
|
||||
cap := setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
_ = execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
last := cap.last()
|
||||
if last == nil {
|
||||
@@ -200,10 +207,14 @@ func TestAttendanceSummary_should_error_when_user_missing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_pass_only_user_flag(t *testing.T) {
|
||||
func TestAttendanceSummary_should_pass_user_id_through_vo(t *testing.T) {
|
||||
cap := setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
_ = execCmd(t, root, []string{"attendance", "summary"}, map[string]string{"user": "U002", "date": "2026-03-12 15:00:00"})
|
||||
_ = execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U002",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
last := cap.last()
|
||||
if last == nil {
|
||||
t.Fatal("no call captured")
|
||||
@@ -220,7 +231,11 @@ func TestAttendanceSummary_should_pass_only_user_flag(t *testing.T) {
|
||||
func TestAttendanceSummary_should_use_dry_run_mode(t *testing.T) {
|
||||
cap := setupTestDepsWithDryRun(t, "attendance")
|
||||
root := buildRoot()
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{"user": "U001", "date": "2026-03-12 15:00:00"})
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -229,6 +244,97 @@ func TestAttendanceSummary_should_use_dry_run_mode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// execSummaryDryRun runs attendance summary with --dry-run and returns the
|
||||
// parsed QueryUserAttendVO map from the helper_invocation payload. This is
|
||||
// necessary because the attendance handler is a custom helper (not a dynamic
|
||||
// MCP-route), so the test framework's mcpCallCapture cannot intercept the
|
||||
// call in non-dry-run mode.
|
||||
func execSummaryDryRun(t *testing.T, flags map[string]string) map[string]any {
|
||||
t.Helper()
|
||||
root := buildRoot()
|
||||
_ = setupTestDeps(t, "attendance")
|
||||
|
||||
cliArgs := []string{"-f", "json", "attendance", "summary", "--dry-run"}
|
||||
for k, v := range flags {
|
||||
if v != "" {
|
||||
cliArgs = append(cliArgs, "--"+k, v)
|
||||
}
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&bytes.Buffer{})
|
||||
root.SetArgs(cliArgs)
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("unexpected CLI error: %v", err)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Params map[string]any `json:"params"`
|
||||
}
|
||||
if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("failed to parse dry-run output as JSON: %v\noutput: %s", err, out.String())
|
||||
}
|
||||
vo, ok := payload.Params["QueryUserAttendVO"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected QueryUserAttendVO map in dry-run params, got %T: %v", payload.Params["QueryUserAttendVO"], payload.Params)
|
||||
}
|
||||
return vo
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_pass_stats_type_when_provided(t *testing.T) {
|
||||
vo := execSummaryDryRun(t, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
if vo["statsType"] != "month" {
|
||||
t.Errorf("expected statsType=month in VO, got %v", vo["statsType"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_pass_stats_type_week(t *testing.T) {
|
||||
vo := execSummaryDryRun(t, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "week",
|
||||
})
|
||||
if vo["statsType"] != "week" {
|
||||
t.Errorf("expected statsType=week in VO, got %v", vo["statsType"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_error_when_stats_type_missing(t *testing.T) {
|
||||
_ = setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when --stats-type is missing")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "stats-type") {
|
||||
t.Errorf("expected error message to mention stats-type, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_error_when_stats_type_invalid(t *testing.T) {
|
||||
_ = setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "foobar",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when --stats-type is neither week nor month")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "week") || !strings.Contains(err.Error(), "month") {
|
||||
t.Errorf("expected error message to mention week and month, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ── attendance rules ───────────────────────────────────────
|
||||
|
||||
func TestAttendanceRules_should_call_tool_with_date(t *testing.T) {
|
||||
|
||||
@@ -23,6 +23,7 @@ var expectedPackagedSkillTargets = []string{
|
||||
".kiro/skills/dws",
|
||||
".trae/skills/dws",
|
||||
".openclaw/skills/dws",
|
||||
".hermes/skills/dws",
|
||||
}
|
||||
|
||||
// seedDistArtifacts creates fake goreleaser output archives (empty tar.gz/zip
|
||||
|
||||
Reference in New Issue
Block a user