package helpers import ( "encoding/json" "errors" "fmt" "strconv" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // ────────────────────────────────────────────────────────── // dws contact — 通讯录 // ────────────────────────────────────────────────────────── func parseCSVValues(raw string) []string { raw = strings.TrimSpace(raw) if raw == "" { return nil } if strings.HasPrefix(raw, "[") && strings.HasSuffix(raw, "]") { var values []string if err := json.Unmarshal([]byte(raw), &values); err == nil { return cleanStringValues(values) } raw = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(raw, "["), "]")) } parts := strings.Split(raw, ",") return cleanStringValues(parts) } func cleanStringValues(parts []string) []string { values := make([]string, 0, len(parts)) for _, p := range parts { v := strings.Trim(strings.TrimSpace(p), `"'`) if v != "" { values = append(values, v) } } return values } // contactUserIDFlagKeys 汇总 contact user get 支持的所有 flag 名(含 camelCase 派生与全小写写法), // 在 RunE 中统一引用,避免每个调用点重复维护别名列表。 // camelCase 版本 --userId / --userIds 由 RegisterCamelCaseAliases 自动派生,--userid 为手写全小写别名。 var contactUserIDFlagKeys = []string{"ids", "user-id", "user-ids", "userId", "userIds", "userid"} // contactRootDeptLikeTokens 是用户/模型常写错的"根部门占位符"。钉钉根部门 deptId 恒为 1。 // 在 contactParseInt64WithAliases 与 list-members 的 CSV 解析里命中这类值时,给出就近提示,避免调用方再去猜。 var contactRootDeptLikeTokens = map[string]struct{}{ "self": {}, "me": {}, "root": {}, "0": {}, } // contactFirstSetFlagName 返回 names 中第一个被用户显式传入的 flag 名(Changed=true)。 // 用于让报错文案显示用户实际输入的 flag 名,而不是主 flag 名,避免 // 出现 "用户传 --ids me 却被报 flag --id 不合法" 的错位。 func contactFirstSetFlagName(cmd *cobra.Command, names ...string) string { for _, n := range names { if f := cmd.Flag(n); f != nil && f.Changed { return n } } if len(names) > 0 { return names[0] } return "" } func contactAnyFlagChanged(cmd *cobra.Command, names ...string) bool { for _, n := range names { if f := cmd.Flag(n); f != nil && f.Changed { return true } } return false } func contactGetBoolWithAliases(cmd *cobra.Command, names ...string) (bool, bool) { for _, name := range names { if flag := cmd.Flag(name); flag != nil && flag.Changed { value, err := cmd.Flags().GetBool(name) return value, err == nil } } return false, false } func contactOptionalString(cmd *cobra.Command, primary string, aliases ...string) (string, bool) { names := append([]string{primary}, aliases...) if !contactAnyFlagChanged(cmd, names...) { return "", false } return strings.TrimSpace(flagOrFallback(cmd, primary, aliases...)), true } func contactOptionalDepartments(cmd *cobra.Command) ([]map[string]any, bool, error) { if !cmd.Flags().Changed("depts") { return nil, false, nil } raw := strings.TrimSpace(mustGetFlag(cmd, "depts")) if raw == "" { return nil, false, nil } var departments []map[string]any if err := json.Unmarshal([]byte(raw), &departments); err != nil { return nil, false, fmt.Errorf("--depts JSON 解析失败: %w\n hint: 正确格式: [{\"deptId\":1}]", err) } return departments, true, nil } // contactParseInt64WithAliases 先在主 flag 与全部别名中找出用户实际传入的值(空则报 missing), // 再走根部门占位符警告 + int64 解析,避免用户传别名时 RunE 读不到。 // 报错文案中使用用户实际输入的 flag 名(比如用户传 --ids me,错误里显示 --ids 而不是主 flag --id), // 防止用户/LLM 被"我明明没传这个 flag 为啥报它"的错位文案带偏。 func contactParseInt64WithAliases(cmd *cobra.Command, primary string, aliases ...string) (int64, error) { if err := validateRequiredFlagWithAliases(cmd, primary, aliases...); err != nil { return 0, err } raw := strings.TrimSpace(flagOrFallback(cmd, primary, aliases...)) setName := contactFirstSetFlagName(cmd, append([]string{primary}, aliases...)...) if _, ok := contactRootDeptLikeTokens[strings.ToLower(raw)]; ok { return 0, fmt.Errorf( "flag --%s 必须是整数;钉钉根部门 deptId=1,请使用 --%s 1", setName, setName) } v, err := strconv.ParseInt(raw, 10, 64) if err != nil { return 0, fmt.Errorf("flag --%s must be an integer: %w", setName, err) } return v, nil } func newContactDeptCreateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "创建部门", Long: `在当前企业下创建部门。--create-dept-group 必须显式传 true 或 false。 不传 --parent 时使用企业根部门。该写操作执行前需要确认,自动化场景在用户明确授权后传 --yes。`, Example: ` dws contact dept create --name "新产品部" --create-dept-group=true dws contact dept create --name "研发一组" --parent 12345 --create-dept-group=false`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { if err := validateRequiredFlagWithAliases(cmd, "name", "dept-name", "deptName"); err != nil { return err } name := strings.TrimSpace(flagOrFallback(cmd, "name", "dept-name", "deptName")) if name == "" { return fmt.Errorf("--%s 不能为空", contactFirstSetFlagName(cmd, "name", "dept-name", "deptName")) } createGroup, supplied := contactGetBoolWithAliases(cmd, "create-dept-group", "createDeptGroup") if !supplied { return fmt.Errorf("--create-dept-group 是必填参数,请显式指定 true 或 false") } toolArgs := map[string]any{ "deptName": name, "createDeptGroup": createGroup, } if contactAnyFlagChanged(cmd, "parent", "super-dept-id", "super-dept", "superDeptId") { parentID, err := contactParseInt64WithAliases(cmd, "parent", "super-dept-id", "super-dept", "superDeptId") if err != nil { return err } toolArgs["superDeptId"] = parentID } return callMCPTool("department_create", toolArgs) }, } cmd.Flags().String("name", "", "部门名称 (必填)") cmd.Flags().String("dept-name", "", "--name 的别名") _ = cmd.Flags().MarkHidden("dept-name") cmd.Flags().String("parent", "", "父部门 ID(可选,不传默认根部门)") cmd.Flags().String("super-dept-id", "", "--parent 的别名") cmd.Flags().String("super-dept", "", "--parent 的别名") _ = cmd.Flags().MarkHidden("super-dept-id") _ = cmd.Flags().MarkHidden("super-dept") cmd.Flags().Bool("create-dept-group", false, "是否创建部门群 (必填,需显式传 true 或 false)") cli.AnnotateRuntimeRequiredFlags(cmd, "name", "create-dept-group") return cmd } func newContactDeptUpdateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "update", Aliases: []string{"modify", "edit"}, Short: "更新部门信息", Long: "更新部门名称,并可选择调整父部门。该写操作执行前需要确认,自动化场景在用户明确授权后传 --yes。", Example: ` dws contact dept update --dept 12345 --name "新部门名" dws contact dept update --dept 12345 --name "新名称" --parent 67890`, RunE: func(cmd *cobra.Command, _ []string) error { deptID, err := contactParseInt64WithAliases(cmd, "dept", "id", "ids", "dept-id", "dept-ids", "deptId", "deptIds") if err != nil { return err } if err := validateRequiredFlagWithAliases(cmd, "name", "dept-name", "deptName"); err != nil { return err } name := strings.TrimSpace(flagOrFallback(cmd, "name", "dept-name", "deptName")) if name == "" { return fmt.Errorf("--%s 不能为空", contactFirstSetFlagName(cmd, "name", "dept-name", "deptName")) } toolArgs := map[string]any{"deptId": deptID, "deptName": name} if contactAnyFlagChanged(cmd, "parent", "super-dept-id", "super-dept", "superDeptId") { parentID, err := contactParseInt64WithAliases(cmd, "parent", "super-dept-id", "super-dept", "superDeptId") if err != nil { return err } toolArgs["superDeptId"] = parentID } return callMCPTool("department_update", toolArgs) }, } cmd.Flags().String("dept", "", "部门 ID (必填)") cmd.Flags().String("name", "", "新部门名称 (必填)") cmd.Flags().String("dept-name", "", "--name 的别名") _ = cmd.Flags().MarkHidden("dept-name") cmd.Flags().String("parent", "", "新父部门 ID(可选)") cmd.Flags().String("super-dept-id", "", "--parent 的别名") cmd.Flags().String("super-dept", "", "--parent 的别名") _ = cmd.Flags().MarkHidden("super-dept-id") _ = cmd.Flags().MarkHidden("super-dept") cli.AnnotateRuntimeRequiredFlags(cmd, "dept", "name") return cmd } func newContactUserUpdateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "update", Aliases: []string{"modify", "edit"}, Short: "修改员工信息", Long: "修改员工的企业内姓名、所属部门或直属主管。至少提供一个修改项,执行前需要确认。", Example: ` dws contact user update --user-id user001 --org-user-name "张三三" dws contact user update --user-id user001 --depts '[{"deptId":1}]'`, RunE: func(cmd *cobra.Command, _ []string) error { if err := validateRequiredFlagWithAliases(cmd, "user-id", "id", "userid", "userId"); err != nil { return err } userID := strings.TrimSpace(flagOrFallback(cmd, "user-id", "id", "userid", "userId")) if userID == "" { return fmt.Errorf("--user-id 不能为空") } toolArgs := map[string]any{"userId": userID} changed := false if value, supplied := contactOptionalString(cmd, "org-user-name", "orgUserName"); supplied && value != "" { toolArgs["orgUserName"] = value changed = true } departments, supplied, err := contactOptionalDepartments(cmd) if err != nil { return err } if supplied { toolArgs["depts"] = departments changed = true } if value, supplied := contactOptionalString(cmd, "master-user-id", "masterUserId"); supplied && value != "" { toolArgs["masterUserId"] = value changed = true } if !changed { return fmt.Errorf("至少需要一个修改项:--org-user-name、--depts 或 --master-user-id") } return callMCPTool("employee_update", toolArgs) }, } cmd.Flags().String("user-id", "", "要修改的员工 userId (必填)") cmd.Flags().String("id", "", "--user-id 的别名") cmd.Flags().String("userid", "", "--user-id 的别名") _ = cmd.Flags().MarkHidden("id") _ = cmd.Flags().MarkHidden("userid") cmd.Flags().String("org-user-name", "", "员工在企业内的名称(可选)") cmd.Flags().String("depts", "", "员工所属部门列表 JSON 数组(可选),格式: [{\"deptId\":1}]") cmd.Flags().String("master-user-id", "", "直属主管 userId(可选)") cli.AnnotateRuntimeRequiredFlags(cmd, "user-id") cli.AnnotateRuntimeConstraints(cmd, cli.RuntimeSchemaConstraints{ RequireOneOf: [][]string{{"org-user-name", "depts", "master-user-id"}}, }) return cmd } func newContactUserUpdateSelfCommand() *cobra.Command { cmd := &cobra.Command{ Use: "update-self", Aliases: []string{"update-me", "update-self-profile", "edit-self", "modify-self"}, Short: "更新当前用户自己的 profile 信息", Long: "更新当前用户的昵称或头像。头像需先上传到钉盘取得 fileId;执行前需要确认。", Example: ` dws contact user update-self --nick "新昵称" dws contact user update-self --avatar-file-id "file-id"`, RunE: func(cmd *cobra.Command, _ []string) error { toolArgs := map[string]any{} if value, supplied := contactOptionalString(cmd, "nick"); supplied && value != "" { toolArgs["nick"] = value } if value, supplied := contactOptionalString(cmd, "avatar-file-id", "avatarFileId"); supplied && value != "" { toolArgs["avatarFileId"] = value } if len(toolArgs) == 0 { return fmt.Errorf("至少需要一个修改项:--nick 或 --avatar-file-id") } return callMCPTool("self_user_profile_update", toolArgs) }, } cmd.Flags().String("nick", "", "新昵称(可选)") cmd.Flags().String("avatar-file-id", "", "新头像在钉盘的 fileId(可选)") cli.AnnotateRuntimeConstraints(cmd, cli.RuntimeSchemaConstraints{ RequireOneOf: [][]string{{"nick", "avatar-file-id"}}, }) return cmd } func newContactUserUpdateOwnnessCommand() *cobra.Command { cmd := &cobra.Command{ Use: "update-ownness", Aliases: []string{"set-ownness"}, Short: "更新用户个人状态", Long: "更新指定用户的个人状态文本(展示在个人资料与聊天会话中,如「居家办公中」)。执行前需要确认,自动化场景在用户明确授权后传 --yes。", Example: ` dws contact user update-ownness --user-id user001 --ownness-text "居家办公中"`, RunE: func(cmd *cobra.Command, _ []string) error { if err := validateRequiredFlagWithAliases(cmd, "user-id", "id", "userid", "userId"); err != nil { return err } userID := strings.TrimSpace(flagOrFallback(cmd, "user-id", "id", "userid", "userId")) if userID == "" { return fmt.Errorf("--user-id 不能为空") } if err := validateRequiredFlagWithAliases(cmd, "ownness-text", "ownnessText"); err != nil { return err } ownnessText := strings.TrimSpace(flagOrFallback(cmd, "ownness-text", "ownnessText")) if ownnessText == "" { return fmt.Errorf("--ownness-text 不能为空") } return callMCPTool("user_ownness_update", map[string]any{ "userId": userID, "ownnessText": ownnessText, }) }, } cmd.Flags().String("user-id", "", "要更新个人状态的用户 userId (必填)") cmd.Flags().String("id", "", "--user-id 的别名") cmd.Flags().String("userid", "", "--user-id 的别名") _ = cmd.Flags().MarkHidden("id") _ = cmd.Flags().MarkHidden("userid") cmd.Flags().String("ownness-text", "", "个人状态文本 (必填),如 \"居家办公中\"") cli.AnnotateRuntimeRequiredFlags(cmd, "user-id", "ownness-text") return cmd } func newContactAccountUpdateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "update", Aliases: []string{"modify", "edit"}, Short: "更新企业账号用户信息", Long: "更新企业账号的员工姓名、部门、直属主管、昵称或头像。至少提供一个修改项,执行前需要确认。", Example: ` dws contact account update --user-id user001 --org-user-name "张三" dws contact account update --user-id user001 --nick "新昵称" --avatar-file-id "file-id"`, RunE: func(cmd *cobra.Command, _ []string) error { if err := validateRequiredFlagWithAliases(cmd, "user-id", "id", "userid", "userId"); err != nil { return err } userID := strings.TrimSpace(flagOrFallback(cmd, "user-id", "id", "userid", "userId")) if userID == "" { return fmt.Errorf("--user-id 不能为空") } toolArgs := map[string]any{"userId": userID} if value, supplied := contactOptionalString(cmd, "org-user-name", "orgUserName"); supplied && value != "" { toolArgs["orgUserName"] = value } departments, supplied, err := contactOptionalDepartments(cmd) if err != nil { return err } if supplied { toolArgs["depts"] = departments } if value, supplied := contactOptionalString(cmd, "master-user-id", "masterUserId"); supplied && value != "" { toolArgs["masterUserId"] = value } if value, supplied := contactOptionalString(cmd, "nick"); supplied && value != "" { toolArgs["nick"] = value } if value, supplied := contactOptionalString(cmd, "avatar-file-id", "avatarFileId"); supplied && value != "" { toolArgs["avatarFileId"] = value } if len(toolArgs) == 1 { return fmt.Errorf("至少需要一个修改项:--org-user-name、--depts、--master-user-id、--nick 或 --avatar-file-id") } return callMCPTool("exclusive_account_user_update", toolArgs) }, } cmd.Flags().String("user-id", "", "被修改企业账号的 userId (必填)") cmd.Flags().String("id", "", "--user-id 的别名") cmd.Flags().String("userid", "", "--user-id 的别名") _ = cmd.Flags().MarkHidden("id") _ = cmd.Flags().MarkHidden("userid") cmd.Flags().String("org-user-name", "", "企业账号在企业内的员工姓名(可选)") cmd.Flags().String("depts", "", "部门列表 JSON 数组(可选),格式: [{\"deptId\":1}]") cmd.Flags().String("master-user-id", "", "直属主管 userId(可选)") cmd.Flags().String("nick", "", "企业账号自身昵称(可选)") cmd.Flags().String("avatar-file-id", "", "企业账号头像在钉盘的 fileId(可选)") cli.AnnotateRuntimeRequiredFlags(cmd, "user-id") cli.AnnotateRuntimeConstraints(cmd, cli.RuntimeSchemaConstraints{ RequireOneOf: [][]string{{"org-user-name", "depts", "master-user-id", "nick", "avatar-file-id"}}, }) return cmd } func newContactCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/contact.json // products.contact). Catalog assembly stamps provenance contract_final. contract.RegisterProductDecl(contract.ProductDecl{ ID: "contact", Selection: contract.ProductSelectionDecl{ AgentSummary: "查询通讯录与花名册,并管理企业、部门、员工及企业账号", UseWhen: []string{ "按姓名/手机号/userId/部门条件做通讯录精确查询,或明确执行企业与员工入企管理", }, AvoidWhen: []string{ "职责/上级等语义找人优先 aisearch person;不要用 contact 发消息;写操作前确认当前企业和目标信息", }, }, }) root := &cobra.Command{ Use: "contact", Short: "通讯录 / 用户 / 部门 / 角色 / 人员关系", Long: `查询钉钉通讯录:用户搜索、手机号查找、部门搜索、子部门 / 成员列表、人员关系;用户花名册档案信息(学历、家庭、银行卡、合同等)与离职员工信息。 通讯录功能: - contact user get-self/search/search-mobile/get: 通讯录用户查询 - contact user invite/update/update-self/update-ownness: 邀请与更新员工 - contact dept search/get-info/list-children/list-members/create/update: 部门查询与管理 - contact relation list-my-followings: 特别关注人查询 企业管理功能: - contact org create: 创建企业 - contact account create/update: 创建与更新企业专属账号 基础人事功能(HR 花名册): - contact user profile fields/get: 员工花名册档案查询(学历、家庭、银行卡等) - contact user dismission search: 离职员工列表查询`, RunE: groupRunE, } userCmd := &cobra.Command{ Use: "user", Short: "人员管理", Long: `人员管理:通讯录用户查询、修改员工信息、邀请员工加入企业、用户档案(花名册)查询、离职员工查询。 【何时用哪个命令】 - 查询用户的部门、主管、管理员权限 → contact user get - 修改员工信息(姓名 / 部门 / 直属主管) → contact user update - 更新当前用户自己的 profile(昵称 / 头像) → contact user update-self - 更新用户个人状态(如「居家办公中」) → contact user update-ownness - 邀请员工加入企业 → contact user invite - 查询用户的学历、家庭、银行卡、合同等档案 → contact user profile get - 查询离职员工列表 → contact user dismission search`, RunE: groupRunE, } contactUserGetSelfCmd := &cobra.Command{ Use: "get-self", Aliases: []string{"self", "me", "whoami", "current"}, Short: "获取当前用户信息(我是谁 / 本人)", Long: "获取当前登录用户的 userId 与基本信息。\n\n触发词:我是谁 / 我的信息 / 我的 userId / 当前用户 / 本人 / self / me / whoami / current。\n别名:self / me / whoami / current 均等价于 get-self。\n无需参数;禁止用 `dws contact user get --ids me/self` 代替(会返回空数据的假成功)。", Example: ` dws contact user get-self dws contact user self # 别名(等价) dws contact user me # 别名(等价)`, RunE: func(cmd *cobra.Command, args []string) error { return callMCPTool("get_current_user_profile", nil) }, } DeclareLeafMetadata(contactUserGetSelfCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "get_current_user_profile", CanonicalPath: "contact.get_current_user_profile", CLIPath: "contact user get-self", PrimaryCLIPath: "contact user get-self", }, Description: "获取当前登录用户资料与 userId", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "contact", RPCName: "get_current_user_profile"}, }, Selection: contract.SelectionSpec{ AgentSummary: "获取当前登录用户资料与 userId", UseWhen: []string{"用户问我是谁/我的 userId/当前账号信息"}, AvoidWhen: []string{"查他人详情用 contact user get 或先搜索"}, Examples: []string{"dws contact user get-self --format json"}, }, }, }) relationCmd := &cobra.Command{Use: "relation", Short: "人员关系查询", Long: `查询钉钉人员关系:特别关注人。`, RunE: groupRunE} contactRelationListMyFollowingsCmd := &cobra.Command{ Use: "list-my-followings", Short: "获取当前用户的特别关注列表", Example: ` dws contact relation list-my-followings`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return callMCPTool("list_my_followings", nil) }, } DeclareLeafMetadata(contactRelationListMyFollowingsCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "list_my_followings", CanonicalPath: "contact.list_my_followings", CLIPath: "contact relation list-my-followings", PrimaryCLIPath: "contact relation list-my-followings", }, Description: "获取当前用户的特别关注列表", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "contact", RPCName: "list_my_followings"}, }, Selection: contract.SelectionSpec{ AgentSummary: "获取当前用户的特别关注列表", UseWhen: []string{"需要查看当前用户特别关注或星标联系人时"}, AvoidWhen: []string{"需要搜索企业通讯录或查询任意员工详情时不要使用;这里只返回当前用户的特别关注列表。"}, Examples: []string{"dws contact relation list-my-followings --format json"}, }, }, }) contactUserSearchCmd := &cobra.Command{ Use: "search", Short: "按关键词搜索用户", Example: ` dws contact user search --query "张三"`, RunE: func(cmd *cobra.Command, args []string) error { // 兼容 --query / --keyword / --name 三种写法(--name 为隐藏别名,对齐 dept search)。 if err := validateRequiredFlagWithAliases(cmd, "query", "keyword", "name"); err != nil { return err } kw := flagOrFallback(cmd, "query", "keyword", "name") return callMCPTool("search_contact_by_key_word", map[string]any{ "keyword": kw, }) }, } DeclareLeafMetadata(contactUserSearchCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "search_contact_by_key_word", CanonicalPath: "contact.search_contact_by_key_word", CLIPath: "contact user search", PrimaryCLIPath: "contact user search", }, Description: "按关键词搜索好友和同事,提取 userId/openDingTalkId", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "contact", RPCName: "search_contact_by_key_word"}, }, Selection: contract.SelectionSpec{ AgentSummary: "按关键词搜索好友和同事,提取 userId/openDingTalkId", UseWhen: []string{"按姓名等关键词在通讯录里精确搜人,并需要 userId 或 openDingTalkId"}, AvoidWhen: []string{ "职责/上级/技能等语义找人优先 aisearch person", "已有 userId 查详情用 contact user get", "查自己用 contact user get-self", }, Examples: []string{"dws contact user search --query \"张三\" --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "query", Property: "keyword"}, }, }, }) contactUserSearchMobileCmd := &cobra.Command{ Use: "search-mobile", Short: "按手机号搜索用户", Example: ` dws contact user search-mobile --mobile 13800138000`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "mobile"); err != nil { return err } return callMCPTool("search_user_by_mobile", map[string]any{ "mobile": mustGetFlag(cmd, "mobile"), }) }, } DeclareLeafMetadata(contactUserSearchMobileCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "search_user_by_mobile", CanonicalPath: "contact.search_user_by_mobile", CLIPath: "contact user search-mobile", PrimaryCLIPath: "contact user search-mobile", }, Description: "按手机号搜索用户", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "contact", RPCName: "search_user_by_mobile"}, }, Selection: contract.SelectionSpec{ AgentSummary: "按手机号搜索用户", UseWhen: []string{"已知手机号,需要精确搜索用户时"}, AvoidWhen: []string{"已有用户 ID 或需要批量用户详情时改用批量详情命令;该命令仅按手机号定位用户。"}, Examples: []string{"dws contact user search-mobile --mobile 13800138000 --format json"}, }, }, }) contactUserGetCmd := &cobra.Command{ Use: "get", Short: "批量获取用户详情(组织管理信息)", Long: `批量获取用户详情,返回用户的组织管理信息(来自通讯录领域)。 返回字段: - isAdmin: 是否为管理员 - orgEmployeeModel.orgUserId / orgUserName: 用户 ID / 姓名 - orgEmployeeModel.orgName / orgId: 所属组织名称 / ID - orgEmployeeModel.orgMasterUserId / orgMasterDisplayName: 直属主管 - orgEmployeeModel.depts: 所属部门列表(含 deptId、deptName) - orgEmployeeModel.labels: 角色列表 【适用场景】 - 想知道某个用户在哪个部门、上级是谁、是不是管理员 【不适用场景】 - 查询学历、家庭、银行卡、合同、紧急联系人等档案信息 → 请用 contact user profile get`, Example: ` dws contact user get --ids userId1,userId2 # 查询 userId: dws contact user search --query "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlagWithAliases(cmd, contactUserIDFlagKeys[0], contactUserIDFlagKeys[1:]...); err != nil { return err } raw := flagOrFallback(cmd, contactUserIDFlagKeys[0], contactUserIDFlagKeys[1:]...) // 拦截“假 userId”:me/self/current/whoami/i/me 代替真实 userId 会得到空数据的假成功。 for _, part := range parseCSVValues(raw) { switch strings.ToLower(strings.TrimSpace(part)) { case "me", "self", "current", "whoami", "i": return fmt.Errorf("--ids 需要真实的 userId,不接受 %q 这类占位符\n hint: 获取当前用户用: dws contact user get-self", part) } } return callMCPTool("get_user_info_by_user_ids", map[string]any{ "user_id_list": parseCSVValues(raw), }) }, } DeclareLeafMetadata(contactUserGetCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "get_user_info_by_user_ids", CanonicalPath: "contact.get_user_info_by_user_ids", CLIPath: "contact user get", PrimaryCLIPath: "contact user get", }, Description: "按 userId 批量获取员工详情(部门/主管等,受可见性限制)", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "contact", RPCName: "get_user_info_by_user_ids"}, }, Selection: contract.SelectionSpec{ AgentSummary: "按 userId 批量获取员工详情(部门/主管等,受可见性限制)", UseWhen: []string{"已有一个或多个 userId,需要部门、主管等详情"}, AvoidWhen: []string{ "还没有 userId 时先 search / aisearch person", "查自己不要传 me/self,用 contact user get-self", }, Examples: []string{"dws contact user get --ids userId1,userId2"}, }, Parameters: []contract.ParamDecl{ {Name: "ids", Property: "user_id_list"}, }, }, }) // ── label 角色 ────────────────────────────────────────────────── contactLabelCmd := &cobra.Command{ Use: "label", Aliases: []string{"role"}, Short: "角色查询", Long: `角色查询:获取企业所有角色列表、根据角色名称查询角色ID、根据角色ID查询角色下的成员。 【何时用哪个命令】 - 获取企业所有角色列表 → contact label list - 根据角色名称查询角色ID → contact label get - 根据角色ID查询角色下的成员 → contact label list-members 【典型场景:查询某类角色的人员(如主管、管理员、财务等)】 1. contact label list → 获取企业全部角色列表 2. 从返回结果中匹配目标角色名称及 labelId 3. contact label list-members --id → 获取该角色下的成员`, RunE: groupRunE, } runContactLabelList := func(cmd *cobra.Command, args []string) error { if len(args) > 0 { return fmt.Errorf("contact label list 不接受位置参数: %s", strings.Join(args, " ")) } return callMCPTool("get_org_labels", map[string]any{}) } runContactLabelGet := func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlagWithAliases(cmd, "names", "name", "query", "keyword"); err != nil { return err } raw := flagOrFallback(cmd, "names", "name", "query", "keyword") return callMCPTool("search_label_by_name", map[string]any{ "labelNames": parseCSVValues(raw), }) } runContactLabelMembers := func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlagWithAliases(cmd, "id", "label-id", "role-id"); err != nil { return err } return callMCPTool("get_label_members_by_labelId", map[string]any{ "labelId": flagOrFallback(cmd, "id", "label-id", "role-id"), }) } contactLabelGetCmd := &cobra.Command{ Use: "get", Short: "根据角色名称查询角色", Long: `根据角色名称精确匹配查询角色信息(角色ID、名称等)。支持同时查询多个角色名称,逗号分隔。无需分页。 注意:精确匹配可能无结果(如用户输入"管理员"但企业只有"主管理员"和"子管理员"), 此时应降级使用 label list 获取全部角色列表,从中模糊匹配包含关键词的角色。`, Example: ` dws contact label get --names "管理员" dws contact label get --names "管理员,财务"`, RunE: runContactLabelGet, } contactLabelListMembersCmd := &cobra.Command{ Use: "list-members", Short: "查询角色下的成员", Long: `根据角色ID查询该角色下的成员列表。`, Example: ` dws contact label list-members --id 12345 # 查询 labelId: dws contact label get --names "角色名"`, RunE: runContactLabelMembers, } contactLabelGetCmd.Flags().String("names", "", "角色名称,逗号分隔 (必填)") contactLabelGetCmd.Flags().String("name", "", "--names 的别名") contactLabelGetCmd.Flags().String("query", "", "--names 的别名") contactLabelGetCmd.Flags().String("keyword", "", "--names 的别名") _ = contactLabelGetCmd.Flags().MarkHidden("name") _ = contactLabelGetCmd.Flags().MarkHidden("query") _ = contactLabelGetCmd.Flags().MarkHidden("keyword") contactLabelListMembersCmd.Flags().String("id", "", "角色 ID (必填)") contactLabelListMembersCmd.Flags().String("label-id", "", "--id 的别名") contactLabelListMembersCmd.Flags().String("role-id", "", "--id 的别名") _ = contactLabelListMembersCmd.Flags().MarkHidden("label-id") _ = contactLabelListMembersCmd.Flags().MarkHidden("role-id") contactLabelListAllCmd := &cobra.Command{ Use: "list", Short: "获取企业所有角色列表", Long: `获取当前企业的所有角色(标签)列表,返回角色ID、角色名称等信息。无需参数。 用于不知道准确角色名称时,先列出全部角色,再根据需要选择目标角色查询成员。 【典型场景】 - 用户说"企业所有主管/查所有管理员/财务人员有哪些" → 先 label list 浏览全部角色,匹配目标角色后 label list-members 获取成员`, Example: ` dws contact label list`, RunE: runContactLabelList, } contactLabelCmd.AddCommand(contactLabelListAllCmd, contactLabelGetCmd, contactLabelListMembersCmd) contactDeptCmd := &cobra.Command{Use: "dept", Short: "部门查询", RunE: groupRunE} contactDeptSearchCmd := &cobra.Command{ Use: "search", Short: "搜索部门", Example: ` dws contact dept search --query "技术部"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlagWithAliases(cmd, "query", "keyword", "name"); err != nil { return err } return callMCPTool("search_dept_by_keyword", map[string]any{ "query": flagOrFallback(cmd, "query", "keyword", "name"), }) }, } DeclareLeafMetadata(contactDeptSearchCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "search_dept_by_keyword", CanonicalPath: "contact.search_dept_by_keyword", CLIPath: "contact dept search", PrimaryCLIPath: "contact dept search", }, Description: "按关键词搜索部门", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "contact", RPCName: "search_dept_by_keyword"}, }, Selection: contract.SelectionSpec{ AgentSummary: "按关键词搜索部门", UseWhen: []string{"只知道部门名称或关键词,需要定位 deptId 时"}, AvoidWhen: []string{"已经有准确部门 ID 并需要详情时改用部门详情命令;该命令用于关键词定位部门。"}, Examples: []string{"dws contact dept search --query \"技术部\" --format json"}, }, }, }) contactDeptListChildrenCmd := &cobra.Command{ Use: "list-children", Short: "查看子部门", Example: ` dws contact dept list-children --dept 12345`, RunE: func(cmd *cobra.Command, args []string) error { // list-children 主 flag 为 --dept;接受 --id / --ids / --dept-id / --dept-ids 作为别名。 deptID, err := contactParseInt64WithAliases(cmd, "dept", "id", "ids", "dept-id", "dept-ids", "deptId", "deptIds") if err != nil { return err } return callMCPTool("get_sub_depts_by_dept_id", map[string]any{ "deptId": deptID, }) }, } DeclareLeafMetadata(contactDeptListChildrenCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "get_sub_depts_by_dept_id", CanonicalPath: "contact.get_sub_depts_by_dept_id", CLIPath: "contact dept list-children", PrimaryCLIPath: "contact dept list-children", }, Description: "列出指定部门的直属子部门", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "contact", RPCName: "get_sub_depts_by_dept_id"}, }, Selection: contract.SelectionSpec{ AgentSummary: "列出指定部门的直属子部门", UseWhen: []string{"已知父部门 deptId,需要列出直属子部门时"}, AvoidWhen: []string{"需要父部门自身详情或递归组织树时不要使用;该命令只列直属子部门。"}, Examples: []string{"dws contact dept list-children --dept 12345 --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "dept", Property: "deptId"}, }, }, }) contactDeptGetInfoCmd := &cobra.Command{ Use: "get-info", Short: "获取部门详情(部门ID、名称、人数)", Example: ` dws contact dept get-info --dept 12345 # 查询 deptId: dws contact dept search`, RunE: func(cmd *cobra.Command, args []string) error { // get-info 主 flag 为 --dept;接受 --id / --ids / --dept-id / --dept-ids 作为别名。 deptID, err := contactParseInt64WithAliases(cmd, "dept", "id", "ids", "dept-id", "dept-ids", "deptId", "deptIds") if err != nil { return err } return callMCPTool("get_dept_info_by_dept_id", map[string]any{ "deptId": deptID, }) }, } DeclareLeafMetadata(contactDeptGetInfoCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "get_dept_info_by_dept_id", CanonicalPath: "contact.get_dept_info_by_dept_id", CLIPath: "contact dept get-info", PrimaryCLIPath: "contact dept get-info", }, Description: "获取指定部门详情", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "contact", RPCName: "get_dept_info_by_dept_id"}, }, Selection: contract.SelectionSpec{ AgentSummary: "获取指定部门详情", UseWhen: []string{"已知 deptId,需要部门名称、人数等详情时"}, AvoidWhen: []string{"只知道部门名称时应先搜索部门;需要列出直属子部门时改用子部门查询。"}, Examples: []string{"dws contact dept get-info --dept 12345 --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "dept", Property: "deptId"}, }, }, }) contactDeptListMembersCmd := &cobra.Command{ Use: "list-members", Short: "查看部门成员(仅本部门,不含下级)", Long: "查看指定部门的成员列表。\n\n范围:仅返回传入 deptId 的**本部门**直接成员,**不递归下级部门**。\n跨层级需求:先 'dws contact dept list-children --dept <父deptId>' 枚举子部门,再对子 deptId 分别或合并调用本命令。", Example: ` dws contact dept list-members --depts 12345,67890 # 查询 deptId: dws contact dept search 或 dws contact dept list-children`, RunE: func(cmd *cobra.Command, args []string) error { // list-members 主 flag 为 --depts;接受 --ids / --id / --dept-id / --dept-ids 作为别名。 if err := validateRequiredFlagWithAliases(cmd, "depts", "ids", "id", "dept-id", "dept-ids", "deptId", "deptIds"); err != nil { return err } raw := flagOrFallback(cmd, "depts", "ids", "id", "dept-id", "dept-ids", "deptId", "deptIds") // 拦截逗号分隔列表中的根部门占位符(self/me/root/0),提示应用 --depts 1。 // 报错里显示用户实际输入的 flag 名,避免出现 "用户传 --id self 却被报 --depts 不合法" 的错位。 setName := contactFirstSetFlagName(cmd, "depts", "ids", "id", "dept-id", "dept-ids", "deptId", "deptIds") for _, t := range parseCSVValues(raw) { if _, ok := contactRootDeptLikeTokens[strings.ToLower(strings.TrimSpace(t))]; ok { return fmt.Errorf( "flag --%s 包含非法占位符 %q;钉钉根部门 deptId=1,请使用 --%s 1", setName, t, setName) } } return callMCPTool("get_dept_members_by_deptId", map[string]any{ "deptIds": parseCSVValues(raw), }) }, } DeclareLeafMetadata(contactDeptListMembersCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "get_dept_members_by_deptId", CanonicalPath: "contact.get_dept_members_by_deptId", CLIPath: "contact dept list-members", PrimaryCLIPath: "contact dept list-members", }, Description: "查看部门成员(逗号分隔 deptId)", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "contact", RPCName: "get_dept_members_by_deptId"}, }, Selection: contract.SelectionSpec{ AgentSummary: "查看部门成员(逗号分隔 deptId)", UseWhen: []string{"需要按一个或多个 deptId 查看部门成员名单时"}, AvoidWhen: []string{"只需部门详情或人数时使用 contact dept get-info;需要直属子部门时使用 contact dept list-children"}, Examples: []string{"dws contact dept list-members --depts 12345,67890 --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "depts", Property: "deptIds"}, }, }, }) // ── user profile 用户档案(花名册) ──────────────────────────────────── contactUserProfileCmd := &cobra.Command{ Use: "profile", Short: "用户档案(花名册)", Long: `用户档案(花名册):查询花名册字段列表、查询员工花名册字段信息。 花名册字段包含:试用/转正信息、个人/家庭信息、学历信息、银行卡/合同信息、 紧急联系人和其他企业自定义信息。 【与 contact user get 的区别】 - contact user get: 组织管理信息(部门、主管、管理员权限) - contact user profile get: 个人档案信息(学历、家庭、银行卡等)`, RunE: groupRunE, } contactUserProfileFieldsCmd := &cobra.Command{ Use: "fields", Short: "查询花名册有权限的字段列表", Long: `查询花名册有权限的字段列表,根据当前用户查询花名册有权限的字段列表。 花名册字段包含:试用/转正信息、个人/家庭信息、学历信息、银行卡/合同信息、 紧急联系人和其他企业自定义信息。 认证信息(corpId、optUserId)由系统自动注入,无需手动传入。 【典型用法】 通常作为 contact user profile get 的前置步骤,用于获取可查询的字段 code 列表。`, Example: ` dws contact user profile fields`, RunE: func(cmd *cobra.Command, args []string) error { return callMCPToolOnServer("hrmregister", "list_authorized_roster_fields", map[string]any{}) }, } DeclareLeafMetadata(contactUserProfileFieldsCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "list_authorized_roster_fields", CanonicalPath: "contact.list_authorized_roster_fields", CLIPath: "contact user profile fields", PrimaryCLIPath: "contact user profile fields", }, Description: "查询当前用户有权查看的花名册字段", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "hrmregister", RPCName: "list_authorized_roster_fields"}, }, Selection: contract.SelectionSpec{ AgentSummary: "查询当前用户有权查看的花名册字段", UseWhen: []string{"查询花名册前,需要先确认当前用户可见的字段 code 时"}, AvoidWhen: []string{"需要读取某位员工的具体花名册字段值时不要停在字段目录,应再调用员工花名册查询。"}, Examples: []string{"dws contact user profile fields --format json"}, }, }, }) contactUserProfileGetCmd := &cobra.Command{ Use: "get", Short: "查询员工花名册字段信息(个人档案)", Long: `查询员工花名册字段信息,根据当前用户指定员工和字段列表,查询相应管理范围内员工的字段值信息。 花名册字段包含:试用/转正信息、个人/家庭信息、学历信息、银行卡/合同信息、 紧急联系人和其他企业自定义信息。 返回字段枚举说明: - employeeType 员工类型:0 无类型,1 全职,2 兼职,3 实习,4 劳务派遣,5 退休返聘,6 劳务外包 - employeeStatus 员工状态:-1 无状态,1 待入职,2 试用,3 正式,4 离职,5 待离职,6 试岗,7 已退休 认证信息(corpId、optUserId)由系统自动注入,无需手动传入。 --staff-id 为查询员工 ID,--fields 为指定字段集合(逗号分隔),可通过 contact user profile fields 获取可用字段列表。 【适用场景】 - 查询某员工的学历、家庭、银行卡、紧急联系人、合同等档案信息 【不适用场景】 - 查询用户的部门、主管、管理员权限 → 请用 contact user get`, Example: ` dws contact user profile get --staff-id STAFF_ID dws contact user profile get --staff-id STAFF_ID --fields fieldCode1,fieldCode2`, RunE: func(cmd *cobra.Command, args []string) error { params := map[string]any{} if v := mustGetFlag(cmd, "staff-id"); v != "" { params["staffId"] = v } if v, _ := cmd.Flags().GetString("fields"); v != "" { fieldCodes := parseCSVValues(v) if len(fieldCodes) > 0 { params["fieldCodeList"] = fieldCodes } } return callMCPToolOnServer("hrmregister", "get_authorized_emp_rosterInfo", params) }, } DeclareLeafMetadata(contactUserProfileGetCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "get_authorized_emp_rosterInfo", CanonicalPath: "contact.get_authorized_emp_rosterInfo", CLIPath: "contact user profile get", PrimaryCLIPath: "contact user profile get", }, Description: "按字段 code 查询指定员工花名册字段值", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "hrmregister", RPCName: "get_authorized_emp_rosterInfo"}, }, Selection: contract.SelectionSpec{ AgentSummary: "按字段 code 查询指定员工花名册字段值", UseWhen: []string{"已确认 staffId 与可见字段 code,需要读花名册字段"}, AvoidWhen: []string{ "尚不知道可见字段时先用 contact user profile fields", "普通通讯录姓名搜索不要走花名册", }, Examples: []string{"dws contact user profile get --staff-id STAFF_ID --fields fieldCode1,fieldCode2"}, }, Parameters: []contract.ParamDecl{ {Name: "fields", Property: "fieldCodeList", Required: boolPtr(false)}, {Name: "staff-id", Required: boolPtr(false)}, }, }, }) contactUserProfileGetCmd.Flags().String("staff-id", "", "查询员工 ID(可选)") contactUserProfileGetCmd.Flags().String("fields", "", "指定字段集合, 逗号分隔, 可通过 profile fields 获取(可选)") contactUserProfileCmd.AddCommand(contactUserProfileFieldsCmd, contactUserProfileGetCmd) // ── user dismission 离职员工 ─────────────────────────────────────────── contactUserDismissionCmd := &cobra.Command{ Use: "dismission", Short: "离职员工查询", Long: `离职员工查询:分页获取离职员工列表,支持按员工姓名、离职时间范围、部门进行过滤。`, RunE: groupRunE, } contactUserDismissionSearchCmd := &cobra.Command{ Use: "search", Short: "分页获取离职员工列表", Long: `分页获取离职员工列表,支持按员工姓名、离职时间范围、部门进行过滤。 认证信息(corpId、optUserId)由系统自动注入,无需手动传入。 --name 员工姓名,模糊搜索(可选) --start 离职日期查询范围开始,格式 YYYY-MM-DD(可选) --end 离职日期查询范围结束,格式 YYYY-MM-DD(可选) --depts 部门 ID 列表,逗号分隔(可选) --hide-retirement 是否隐藏退休,默认 true(可选) --hide-partner 是否隐藏合作伙伴,默认 false(可选) --page 页码,从 1 开始(可选,默认 1) --limit 页大小,200 以内(可选,默认 20) 注意:--start 和 --end 必须同时设置或同时不设置,不允许只设置其中一个。 【适用场景】 - 查询公司离职员工名单 - 按时间范围/部门/姓名筛选离职员工 【不适用场景】 - 查询在职员工 → 使用 contact user search`, Example: ` dws contact user dismission search dws contact user dismission search --name "张三" dws contact user dismission search --start 2026-01-01 --end 2026-03-31 dws contact user dismission search --depts 123456,789012 --page 1 --limit 50`, RunE: func(cmd *cobra.Command, args []string) error { startStr, _ := cmd.Flags().GetString("start") endStr, _ := cmd.Flags().GetString("end") if (startStr == "") != (endStr == "") { return fmt.Errorf("--start 和 --end 必须同时设置或同时不设置") } searchVO := map[string]any{} if v, _ := cmd.Flags().GetString("name"); v != "" { searchVO["empName"] = v } if startStr != "" { ts, err := parseDateToTimestamp(startStr, "start") if err != nil { return err } searchVO["startDate"] = ts } if endStr != "" { ts, err := parseDateToTimestamp(endStr, "end") if err != nil { return err } searchVO["endDate"] = ts } if v, _ := cmd.Flags().GetString("depts"); v != "" { searchVO["depts"] = parseCSVInts(v) } if cmd.Flags().Changed("hide-retirement") { v, _ := cmd.Flags().GetBool("hide-retirement") searchVO["hideRetirement"] = v } if cmd.Flags().Changed("hide-partner") { v, _ := cmd.Flags().GetBool("hide-partner") searchVO["hidePartner"] = v } params := map[string]any{ "searchVO": searchVO, } if v, _ := cmd.Flags().GetInt("page"); v > 0 { params["pageNum"] = v } if v, _ := cmd.Flags().GetInt("limit"); v > 0 { params["pageSize"] = v } return callMCPToolOnServer("hrmregister", "query_dismission_employee_list", params) }, } DeclareLeafMetadata(contactUserDismissionSearchCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "query_dismission_employee_list", CanonicalPath: "contact.query_dismission_employee_list", CLIPath: "contact user dismission search", PrimaryCLIPath: "contact user dismission search", }, Description: "查询离职员工列表", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "hrmregister", RPCName: "query_dismission_employee_list"}, }, Selection: contract.SelectionSpec{ AgentSummary: "查询离职员工列表", UseWhen: []string{"需要按姓名、离职时间或部门查询离职员工时"}, AvoidWhen: []string{"需要查询在职员工或修改离职信息时不要使用;该命令只检索离职员工记录。"}, Examples: []string{ "dws contact user dismission search --format json", "dws contact user dismission search --name \"张三\" --format json", }, }, Parameters: []contract.ParamDecl{ {Name: "depts", Property: "searchVO.depts"}, {Name: "end", Property: "searchVO.endDate"}, {Name: "hide-partner", Property: "searchVO.hidePartner"}, {Name: "hide-retirement", Property: "searchVO.hideRetirement"}, {Name: "limit", Property: "pageSize"}, {Name: "name", Property: "searchVO.empName"}, {Name: "page", Property: "pageNum"}, {Name: "start", Property: "searchVO.startDate"}, }, }, }) contactUserDismissionSearchCmd.Flags().String("name", "", "员工姓名,模糊搜索(可选)") contactUserDismissionSearchCmd.Flags().String("start", "", "离职日期查询范围开始,格式 YYYY-MM-DD(可选),与end要么都不填要么都填") contactUserDismissionSearchCmd.Flags().String("end", "", "离职日期查询范围结束,格式 YYYY-MM-DD(可选),与start要么都不填要么都填") contactUserDismissionSearchCmd.Flags().String("depts", "", "部门 ID 列表,逗号分隔(可选)") contactUserDismissionSearchCmd.Flags().Bool("hide-retirement", true, "是否隐藏退休,默认 true(可选)") contactUserDismissionSearchCmd.Flags().Bool("hide-partner", false, "是否隐藏合作伙伴,默认 false(可选)") contactUserDismissionSearchCmd.Flags().Int("page", 1, "页码,从 1 开始(可选)") contactUserDismissionSearchCmd.Flags().Int("limit", 20, "页大小,200 以内(可选)") contactUserDismissionCmd.AddCommand(contactUserDismissionSearchCmd) contactUserInviteCmd := &cobra.Command{ Use: "invite", Short: "邀请员工加入企业", Long: `通过手机号邀请单个员工加入当前企业。 参数: --org-user-name 员工在企业内的名称 --org-user-mobile 员工手机号 --depts 员工所属部门列表 JSON 数组,格式: [{"deptId":1}] 认证信息(corpId、optUserId)由系统自动注入,无需手动传入。`, Example: ` dws contact user invite --org-user-name "张三" --org-user-mobile "13800138000" --depts '[{"deptId":1}]'`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "org-user-name", "org-user-mobile"); err != nil { return err } deptsJSON := mustGetFlag(cmd, "depts") var depts []map[string]any if deptsJSON != "" { if err := json.Unmarshal([]byte(deptsJSON), &depts); err != nil { return fmt.Errorf("--depts JSON 解析失败: %w\n hint: 正确格式: [{\"deptId\":1}]", err) } } name := strings.TrimSpace(mustGetFlag(cmd, "org-user-name")) mobile := strings.TrimSpace(mustGetFlag(cmd, "org-user-mobile")) if name == "" || mobile == "" { return fmt.Errorf("--org-user-name 和 --org-user-mobile 不能为空") } return callMCPTool("add_employee", map[string]any{ "orgUserName": name, "orgUserMobile": mobile, "depts": depts, }) }, } DeclareLeafMetadata(contactUserInviteCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "add_employee", CanonicalPath: "contact.add_employee", CLIPath: "contact user invite", PrimaryCLIPath: "contact user invite", }, Description: "按手机号邀请一名员工加入当前企业", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: the executable CLI maps employee identity and decoded department JSON to contact/add_employee, which is absent from the pinned MCP metadata snapshot.", }, Selection: contract.SelectionSpec{ AgentSummary: "按手机号邀请一名员工加入当前企业", UseWhen: []string{"用户明确要求邀请或添加员工到当前企业,且已提供企业内姓名和手机号"}, AvoidWhen: []string{"需要创建企业专属登录账号时使用 contact account create;需要创建企业组织本身时使用 contact org create"}, Examples: []string{"dws contact user invite --org-user-name \"张三\" --org-user-mobile \"13800138000\" --depts '[{\"deptId\":1}]'"}, }, Parameters: []contract.ParamDecl{ {Name: "depts", Property: "depts", Required: boolPtr(false), InterfaceType: "array"}, {Name: "org-user-mobile", Property: "orgUserMobile", Required: boolPtr(true)}, {Name: "org-user-name", Property: "orgUserName", Required: boolPtr(true)}, }, }, }) contactUserInviteCmd.Flags().String("org-user-name", "", "员工在企业内的名称 (必填)") contactUserInviteCmd.Flags().String("org-user-mobile", "", "员工手机号 (必填)") contactUserInviteCmd.Flags().String("depts", "", "员工所属部门列表 JSON 数组(可选),格式: [{\"deptId\":1}]") contactUserUpdateCmd := newContactUserUpdateCommand() DeclareLeafMetadata(contactUserUpdateCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "employee_update", CanonicalPath: "contact.employee_update", CLIPath: "contact user update", PrimaryCLIPath: "contact user update", }, Description: "修改指定员工的企业内姓名、所属部门或直属主管", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: the executable CLI maps employee update flags to contact/employee_update, which is absent from the pinned MCP metadata snapshot.", }, Selection: contract.SelectionSpec{ AgentSummary: "修改指定员工的企业内姓名、所属部门或直属主管", UseWhen: []string{"用户明确要求更新已有员工的组织信息,且已确认目标 userId 和至少一个修改项"}, AvoidWhen: []string{"修改当前用户自己的昵称或头像应使用 contact user update-self;创建企业专属账号应使用 contact account create"}, Examples: []string{"dws contact user update --user-id user001 --org-user-name \"张三三\""}, }, Parameters: []contract.ParamDecl{ {Name: "depts", Property: "depts", Required: boolPtr(false), InterfaceType: "array"}, {Name: "id", Property: "userId", Required: boolPtr(false)}, {Name: "master-user-id", Property: "masterUserId", Required: boolPtr(false)}, {Name: "org-user-name", Property: "orgUserName", Required: boolPtr(false)}, {Name: "user-id", Property: "userId", Required: boolPtr(true)}, {Name: "userid", Property: "userId", Required: boolPtr(false)}, }, }, }) contactUserUpdateSelfCmd := newContactUserUpdateSelfCommand() DeclareLeafMetadata(contactUserUpdateSelfCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "self_user_profile_update", CanonicalPath: "contact.self_user_profile_update", CLIPath: "contact user update-self", PrimaryCLIPath: "contact user update-self", }, Description: "更新当前登录用户自己的昵称或头像", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: the executable CLI maps self-profile update flags to contact/self_user_profile_update, which is absent from the pinned MCP metadata snapshot.", }, Selection: contract.SelectionSpec{ AgentSummary: "更新当前登录用户自己的昵称或头像", UseWhen: []string{"用户明确要求修改自己的 profile 昵称或头像 fileId"}, AvoidWhen: []string{"修改其他员工的组织信息应使用 contact user update;修改企业专属账号应使用 contact account update"}, Examples: []string{"dws contact user update-self --nick \"新昵称\""}, }, Parameters: []contract.ParamDecl{ {Name: "avatar-file-id", Property: "avatarFileId", Required: boolPtr(false)}, {Name: "nick", Property: "nick", Required: boolPtr(false)}, }, }, }) contactUserUpdateOwnnessCmd := newContactUserUpdateOwnnessCommand() DeclareLeafMetadata(contactUserUpdateOwnnessCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "user_ownness_update", CanonicalPath: "contact.user_ownness_update", CLIPath: "contact user update-ownness", PrimaryCLIPath: "contact user update-ownness", }, Description: "更新指定用户的个人状态文本(展示在个人资料与聊天会话中,如「居家办公中」)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: the executable CLI maps personal-status update flags to contact/user_ownness_update, which is absent from the pinned MCP metadata snapshot.", }, Selection: contract.SelectionSpec{ AgentSummary: "更新指定用户的个人状态文本(如「居家办公中」)", UseWhen: []string{"用户明确要求设置或修改自己/指定用户的个人状态文本,且已确认目标 userId 和状态内容"}, AvoidWhen: []string{"修改员工组织信息(姓名 / 部门 / 主管)应使用 contact user update;修改当前用户昵称或头像应使用 contact user update-self"}, Examples: []string{"dws contact user update-ownness --user-id user001 --ownness-text \"居家办公中\""}, }, Parameters: []contract.ParamDecl{ {Name: "id", Property: "userId", Required: boolPtr(false)}, {Name: "ownness-text", Property: "ownnessText", Required: boolPtr(true)}, {Name: "user-id", Property: "userId", Required: boolPtr(true)}, {Name: "userid", Property: "userId", Required: boolPtr(false)}, }, }, }) // ── flags 注册 ─────────────────────────────────────────────── contactUserSearchCmd.Flags().String("query", "", "搜索关键词 (必填)") contactUserSearchCmd.Flags().String("keyword", "", "--query 的别名") contactUserSearchCmd.Flags().String("name", "", "--query 的别名") _ = contactUserSearchCmd.Flags().MarkHidden("keyword") _ = contactUserSearchCmd.Flags().MarkHidden("name") contactUserSearchMobileCmd.Flags().String("mobile", "", "手机号 (必填)") contactUserGetCmd.Flags().String("ids", "", "用户 ID 列表 (必填)") contactUserGetCmd.Flags().String("user-id", "", "--ids 的别名") contactUserGetCmd.Flags().String("user-ids", "", "--ids 的别名") contactUserGetCmd.Flags().String("userid", "", "--ids 的别名(全小写)") _ = contactUserGetCmd.Flags().MarkHidden("user-id") _ = contactUserGetCmd.Flags().MarkHidden("user-ids") _ = contactUserGetCmd.Flags().MarkHidden("userid") userCmd.AddCommand( contactUserGetSelfCmd, contactUserSearchCmd, contactUserSearchMobileCmd, contactUserGetCmd, contactUserInviteCmd, // 邀请员工加入企业 contactUserUpdateCmd, // 修改员工信息 contactUserUpdateSelfCmd, // 更新当前用户自己的 profile 信息 contactUserUpdateOwnnessCmd, // 更新用户个人状态 contactUserProfileCmd, // 花名册档案 contactUserDismissionCmd, // 离职员工 ) contactDeptSearchCmd.Flags().String("query", "", "搜索关键词 (必填)") contactDeptSearchCmd.Flags().String("keyword", "", "--query 的别名") contactDeptSearchCmd.Flags().String("name", "", "--query 的别名") _ = contactDeptSearchCmd.Flags().MarkHidden("keyword") _ = contactDeptSearchCmd.Flags().MarkHidden("name") // 主 flag 与 RunE 读取保持一致:get-info / list-children 用 --dept,list-members 用 --depts。 // 历史上主 flag 曾误注册为 --id/--ids,导致 RunE 读的 --dept/--depts 未注册、命令行传入报 unknown flag。 contactDeptGetInfoCmd.Flags().String("dept", "", "部门 ID (必填)") contactDeptListChildrenCmd.Flags().String("dept", "", "部门 ID (必填)") contactDeptListMembersCmd.Flags().String("depts", "", "部门 ID 列表 (必填)") // dept 系列命令统一接受 --id / --ids / --dept-id / --dept-ids 别名(集中注册避免逐命令重复写)。 // camelCase --deptId / --deptIds 由 RegisterCamelCaseAliases 自动派生,无需手写。 type deptIDAliasSpec struct { cmd *cobra.Command aliases []string } contactDeptCreateCmd := newContactDeptCreateCommand() DeclareLeafMetadata(contactDeptCreateCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "non_idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "department_create", CanonicalPath: "contact.department_create", CLIPath: "contact dept create", PrimaryCLIPath: "contact dept create", }, Description: "在当前企业的根部门或指定父部门下创建部门", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: the executable CLI maps department creation flags to contact/department_create, which is absent from the pinned MCP metadata snapshot.", }, Selection: contract.SelectionSpec{ AgentSummary: "在当前企业的根部门或指定父部门下创建部门", UseWhen: []string{"用户明确要求新建部门,且已确认部门名称、父部门及是否同步创建部门群"}, AvoidWhen: []string{"修改已有部门名称或父级应使用 contact dept update;仅查找部门应使用 contact dept search"}, Examples: []string{"dws contact dept create --name \"新产品部\" --create-dept-group=true"}, }, Parameters: []contract.ParamDecl{ {Name: "create-dept-group", Property: "createDeptGroup", Required: boolPtr(true), InterfaceType: "boolean"}, {Name: "dept-name", Property: "deptName", Required: boolPtr(false)}, {Name: "name", Property: "deptName", Required: boolPtr(true)}, {Name: "parent", Property: "superDeptId", Required: boolPtr(false), InterfaceType: "integer"}, {Name: "super-dept", Property: "superDeptId", Required: boolPtr(false), InterfaceType: "integer"}, {Name: "super-dept-id", Property: "superDeptId", Required: boolPtr(false), InterfaceType: "integer"}, }, }, }) contactDeptUpdateCmd := newContactDeptUpdateCommand() DeclareLeafMetadata(contactDeptUpdateCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "department_update", CanonicalPath: "contact.department_update", CLIPath: "contact dept update", PrimaryCLIPath: "contact dept update", }, Description: "更新指定部门的名称,并可调整父部门", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: the executable CLI maps department update flags to contact/department_update, which is absent from the pinned MCP metadata snapshot.", }, Selection: contract.SelectionSpec{ AgentSummary: "更新指定部门的名称,并可调整父部门", UseWhen: []string{"用户明确要求修改已有部门名称或迁移父部门,且已确认目标 deptId"}, AvoidWhen: []string{"创建新部门应使用 contact dept create;仅查看部门信息应使用 contact dept get-info"}, Examples: []string{"dws contact dept update --dept 12345 --name \"研发中心\""}, }, Parameters: []contract.ParamDecl{ {Name: "dept", Property: "deptId", Required: boolPtr(true), InterfaceType: "integer"}, {Name: "dept-id", Property: "deptId", Required: boolPtr(false), InterfaceType: "integer"}, {Name: "dept-ids", Property: "deptId", Required: boolPtr(false), InterfaceType: "integer"}, {Name: "dept-name", Property: "deptName", Required: boolPtr(false)}, {Name: "id", Property: "deptId", Required: boolPtr(false), InterfaceType: "integer"}, {Name: "ids", Property: "deptId", Required: boolPtr(false), InterfaceType: "integer"}, {Name: "name", Property: "deptName", Required: boolPtr(true)}, {Name: "parent", Property: "superDeptId", Required: boolPtr(false), InterfaceType: "integer"}, {Name: "super-dept", Property: "superDeptId", Required: boolPtr(false), InterfaceType: "integer"}, {Name: "super-dept-id", Property: "superDeptId", Required: boolPtr(false), InterfaceType: "integer"}, }, }, }) for _, s := range []deptIDAliasSpec{ {contactDeptGetInfoCmd, []string{"id", "dept-id", "ids", "dept-ids"}}, {contactDeptListChildrenCmd, []string{"id", "ids", "dept-id", "dept-ids"}}, {contactDeptListMembersCmd, []string{"ids", "id", "dept-id", "dept-ids"}}, {contactDeptUpdateCmd, []string{"id", "ids", "dept-id", "dept-ids"}}, } { for _, name := range s.aliases { s.cmd.Flags().String(name, "", "部门 ID 别名(等价于当前命令的主 flag)") _ = s.cmd.Flags().MarkHidden(name) } } contactDeptCmd.AddCommand( contactDeptSearchCmd, contactDeptGetInfoCmd, contactDeptListChildrenCmd, contactDeptListMembersCmd, contactDeptCreateCmd, contactDeptUpdateCmd, ) // ── org 企业管理 ────────────────────────────────────────────────── contactOrgCmd := &cobra.Command{ Use: "org", Short: "企业管理", Long: `企业管理:创建企业。 【何时用哪个命令】 - 创建新企业 → contact org create - 创建企业专属账号 → contact account create - 邀请员工加入企业 → contact user invite`, RunE: groupRunE, } contactOrgCreateCmd := &cobra.Command{ Use: "create", Short: "创建企业", Long: `创建一个新的钉钉企业。需提供企业名称和当前用户在企业内的名称(作为创建者)。 认证信息(corpId、optUserId)由系统自动注入,无需手动传入。`, Example: ` dws contact org create --org-name "我的企业" --creator-username "张三"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "org-name", "creator-username"); err != nil { return err } orgName := strings.TrimSpace(mustGetFlag(cmd, "org-name")) creatorUsername := strings.TrimSpace(mustGetFlag(cmd, "creator-username")) if orgName == "" || creatorUsername == "" { return fmt.Errorf("--org-name 和 --creator-username 不能为空") } return callMCPTool("org_create", map[string]any{ "orgName": orgName, "creatorUsername": creatorUsername, }) }, } DeclareLeafMetadata(contactOrgCreateCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "org_create", CanonicalPath: "contact.org_create", CLIPath: "contact org create", PrimaryCLIPath: "contact org create", }, Description: "创建一个新的钉钉企业组织", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: the executable CLI maps organization and creator names to contact/org_create, which is absent from the pinned MCP metadata snapshot.", }, Selection: contract.SelectionSpec{ AgentSummary: "创建一个新的钉钉企业组织", UseWhen: []string{"用户明确要求创建、新建、开通或初始化企业组织,并已提供企业名称和创建者企业内名称"}, AvoidWhen: []string{"请求中包含企业账号、专属账号或登录账号时改用 contact account create;邀请员工时改用 contact user invite"}, Examples: []string{"dws contact org create --org-name \"我的企业\" --creator-username \"张三\""}, }, Parameters: []contract.ParamDecl{ {Name: "creator-username", Property: "creatorUsername", Required: boolPtr(true)}, {Name: "org-name", Property: "orgName", Required: boolPtr(true)}, }, }, }) contactOrgCreateCmd.Flags().String("org-name", "", "企业名称 (必填)") contactOrgCreateCmd.Flags().String("creator-username", "", "创建者在企业内的名称,对应 creatorUsername (必填)") contactOrgCmd.AddCommand(contactOrgCreateCmd) // ── account 企业账号管理 ────────────────────────────────────────── contactAccountCmd := &cobra.Command{ Use: "account", Short: "企业账号管理", Long: "企业账号管理:创建或更新企业专属账号。", RunE: groupRunE, } contactAccountCreateCmd := &cobra.Command{ Use: "create", Short: "创建企业专属账号", Long: `为当前企业创建一个专属登录账号。 必填:--org-user-name、--login-id 可选:--org-user-mobile、--email、--dept-ids、--send-pwd-via-sms 注意: - 登录号(--login-id)请勿包含手机号等联系方式,否则可能被运营商拦截短信 - --send-pwd-via-sms 控制是否通过手机短信/邮件发送登录邀请 认证信息(corpId、optUserId)由系统自动注入,无需手动传入。`, Example: ` dws contact account create --org-user-name "张三" --login-id "zhangsan001" --org-user-mobile "13800138000" --email "zhangsan@example.com" --dept-ids "1,2,3" --send-pwd-via-sms`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "org-user-name", "login-id"); err != nil { return err } orgUserName := strings.TrimSpace(mustGetFlag(cmd, "org-user-name")) loginID := strings.TrimSpace(mustGetFlag(cmd, "login-id")) if orgUserName == "" || loginID == "" { return fmt.Errorf("--org-user-name 和 --login-id 不能为空") } toolArgs := map[string]any{ "orgUserName": orgUserName, "loginId": loginID, } if cmd.Flags().Changed("send-pwd-via-sms") { sendPwdViaSMS, _ := cmd.Flags().GetBool("send-pwd-via-sms") toolArgs["sendPwdViaSms"] = sendPwdViaSMS } if mobile := strings.TrimSpace(mustGetFlag(cmd, "org-user-mobile")); mobile != "" { toolArgs["orgUserMobile"] = mobile } if email := strings.TrimSpace(mustGetFlag(cmd, "email")); email != "" { toolArgs["email"] = email } if cmd.Flags().Changed("dept-ids") { ids, err := parseCSVIntsStrict(mustGetFlag(cmd, "dept-ids")) if err != nil { return fmt.Errorf("--dept-ids 解析失败: %w", err) } toolArgs["deptIds"] = ids } return callMCPTool("exclusive_account_create", toolArgs) }, } DeclareLeafMetadata(contactAccountCreateCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "exclusive_account_create", CanonicalPath: "contact.exclusive_account_create", CLIPath: "contact account create", PrimaryCLIPath: "contact account create", }, Description: "为当前企业创建专属登录账号", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: the executable CLI maps enterprise-account flags to contact/exclusive_account_create, which is absent from the pinned MCP metadata snapshot.", }, Selection: contract.SelectionSpec{ AgentSummary: "为当前企业创建专属登录账号", UseWhen: []string{"用户明确要求创建企业账号、专属账号或企业登录账号,并已提供员工名称和登录号"}, AvoidWhen: []string{"创建企业组织本身应使用 contact org create;仅邀请已有手机号员工入企应使用 contact user invite"}, Examples: []string{"dws contact account create --org-user-name \"张三\" --login-id \"zhangsan001\" --org-user-mobile \"13800138000\""}, }, Parameters: []contract.ParamDecl{ {Name: "dept-ids", Property: "deptIds", Required: boolPtr(false), InterfaceType: "array"}, {Name: "email", Property: "email", Required: boolPtr(false)}, {Name: "login-id", Property: "loginId", Required: boolPtr(true)}, {Name: "org-user-mobile", Property: "orgUserMobile", Required: boolPtr(false)}, {Name: "org-user-name", Property: "orgUserName", Required: boolPtr(true)}, {Name: "send-pwd-via-sms", Property: "sendPwdViaSms", Required: boolPtr(false), InterfaceType: "boolean"}, }, }, }) contactAccountCreateCmd.Flags().String("org-user-name", "", "员工在企业内的名称 (必填)") contactAccountCreateCmd.Flags().String("login-id", "", "登录号 (必填),请勿包含手机号") contactAccountCreateCmd.Flags().String("org-user-mobile", "", "员工手机号(可选)") contactAccountCreateCmd.Flags().String("email", "", "邮箱(可选)") contactAccountCreateCmd.Flags().String("dept-ids", "", "要加入的部门 ID 列表,逗号分隔(可选)") contactAccountCreateCmd.Flags().Bool("send-pwd-via-sms", false, "是否通过手机短信/邮件发送登录邀请(可选)") contactAccountUpdateCmd := newContactAccountUpdateCommand() DeclareLeafMetadata(contactAccountUpdateCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "exclusive_account_user_update", CanonicalPath: "contact.exclusive_account_user_update", CLIPath: "contact account update", PrimaryCLIPath: "contact account update", }, Description: "更新企业专属账号的组织信息或个人资料", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: the executable CLI maps enterprise-account update flags to contact/exclusive_account_user_update, which is absent from the pinned MCP metadata snapshot.", }, Selection: contract.SelectionSpec{ AgentSummary: "更新企业专属账号的组织信息或个人资料", UseWhen: []string{"用户明确要求修改已有企业专属账号的姓名、部门、主管、昵称或头像"}, AvoidWhen: []string{"创建新企业专属账号应使用 contact account create;修改普通员工组织信息应使用 contact user update"}, Examples: []string{"dws contact account update --user-id user001 --nick \"新昵称\""}, }, Parameters: []contract.ParamDecl{ {Name: "avatar-file-id", Property: "avatarFileId", Required: boolPtr(false)}, {Name: "depts", Property: "depts", Required: boolPtr(false), InterfaceType: "array"}, {Name: "id", Property: "userId", Required: boolPtr(false)}, {Name: "master-user-id", Property: "masterUserId", Required: boolPtr(false)}, {Name: "nick", Property: "nick", Required: boolPtr(false)}, {Name: "org-user-name", Property: "orgUserName", Required: boolPtr(false)}, {Name: "user-id", Property: "userId", Required: boolPtr(true)}, {Name: "userid", Property: "userId", Required: boolPtr(false)}, }, }, }) contactAccountCmd.AddCommand(contactAccountCreateCmd, contactAccountUpdateCmd) relationCmd.AddCommand(contactRelationListMyFollowingsCmd) root.AddCommand(userCmd, contactDeptCmd, contactLabelCmd, relationCmd, contactOrgCmd, contactAccountCmd) addQueryFlags := func(cmd *cobra.Command) { cmd.Flags().String("query", "", "搜索关键词 (必填)") cmd.Flags().String("keyword", "", "--query 的别名") cmd.Flags().String("name", "", "--query 的别名") _ = cmd.Flags().MarkHidden("keyword") _ = cmd.Flags().MarkHidden("name") } addUserIDFlags := func(cmd *cobra.Command) { cmd.Flags().String("ids", "", "用户 ID 列表") cmd.Flags().String("user-id", "", "--ids 的别名") cmd.Flags().String("user-ids", "", "--ids 的别名") cmd.Flags().String("userid", "", "--ids 的别名(全小写)") _ = cmd.Flags().MarkHidden("user-id") _ = cmd.Flags().MarkHidden("user-ids") _ = cmd.Flags().MarkHidden("userid") } addLabelNameFlags := func(cmd *cobra.Command) { cmd.Flags().String("names", "", "角色名称,逗号分隔") cmd.Flags().String("name", "", "--names 的别名") cmd.Flags().String("query", "", "--names 的别名") cmd.Flags().String("keyword", "", "--names 的别名") _ = cmd.Flags().MarkHidden("name") _ = cmd.Flags().MarkHidden("query") _ = cmd.Flags().MarkHidden("keyword") } addLabelIDFlags := func(cmd *cobra.Command) { cmd.Flags().String("id", "", "角色 ID") cmd.Flags().String("label-id", "", "--id 的别名") cmd.Flags().String("role-id", "", "--id 的别名") _ = cmd.Flags().MarkHidden("label-id") _ = cmd.Flags().MarkHidden("role-id") } addDeptIDFlags := func(cmd *cobra.Command) { cmd.Flags().String("dept", "", "部门 ID") cmd.Flags().String("id", "", "--dept 的别名") cmd.Flags().String("dept-id", "", "--dept 的别名") cmd.Flags().String("dept-ids", "", "--dept 的别名") _ = cmd.Flags().MarkHidden("id") _ = cmd.Flags().MarkHidden("dept-id") _ = cmd.Flags().MarkHidden("dept-ids") } runContactUserGet := func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlagWithAliases(cmd, contactUserIDFlagKeys[0], contactUserIDFlagKeys[1:]...); err != nil { return err } raw := flagOrFallback(cmd, contactUserIDFlagKeys[0], contactUserIDFlagKeys[1:]...) for _, part := range parseCSVValues(raw) { switch strings.ToLower(strings.TrimSpace(part)) { case "me", "self", "current", "whoami", "i": return fmt.Errorf("--ids 需要真实的 userId,不接受 %q 这类占位符\n hint: 获取当前用户用: dws contact user get-self", part) } } return callMCPTool("get_user_info_by_user_ids", map[string]any{ "user_id_list": parseCSVValues(raw), }) } runContactUserSearch := func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlagWithAliases(cmd, "query", "keyword", "name"); err != nil { return err } return callMCPTool("search_contact_by_key_word", map[string]any{ "keyword": flagOrFallback(cmd, "query", "keyword", "name"), }) } contactRootSearchCmd := &cobra.Command{ Use: "search", Short: "按关键词搜索用户(兼容入口)", Hidden: true, RunE: runContactUserSearch, } contactRootFindCmd := &cobra.Command{ Use: "find", Short: "按关键词搜索用户(兼容入口)", Hidden: true, RunE: runContactUserSearch, } addQueryFlags(contactRootSearchCmd) addQueryFlags(contactRootFindCmd) contactRootGetCmd := &cobra.Command{ Use: "get", Short: "获取用户/部门/角色详情(兼容入口)", Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { switch { case contactAnyFlagChanged(cmd, contactUserIDFlagKeys...): return runContactUserGet(cmd, args) case contactAnyFlagChanged(cmd, "dept", "id", "dept-id", "dept-ids"): deptID, err := contactParseInt64WithAliases(cmd, "dept", "id", "dept-id", "dept-ids") if err != nil { return err } return callMCPTool("get_dept_info_by_dept_id", map[string]any{"deptId": deptID}) case contactAnyFlagChanged(cmd, "names", "name", "query", "keyword"): return runContactLabelGet(cmd, args) case contactAnyFlagChanged(cmd, "label-id", "role-id"): return runContactLabelMembers(cmd, args) default: return fmt.Errorf("contact get 需要指定 --ids 、--dept 、--names <角色名> 或 --label-id <角色ID>") } }, } addUserIDFlags(contactRootGetCmd) addDeptIDFlags(contactRootGetCmd) addLabelNameFlags(contactRootGetCmd) contactRootGetCmd.Flags().String("label-id", "", "角色 ID") contactRootGetCmd.Flags().String("role-id", "", "--label-id 的别名") _ = contactRootGetCmd.Flags().MarkHidden("role-id") contactRootListCmd := &cobra.Command{ Use: "list", Short: "列出角色/部门成员/用户详情(兼容入口)", Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { switch { case contactAnyFlagChanged(cmd, "depts", "id", "dept-id", "dept-ids"): if err := validateRequiredFlagWithAliases(cmd, "depts", "id", "dept-id", "dept-ids"); err != nil { return err } raw := flagOrFallback(cmd, "depts", "id", "dept-id", "dept-ids") setName := contactFirstSetFlagName(cmd, "depts", "id", "dept-id", "dept-ids") for _, t := range parseCSVValues(raw) { if _, ok := contactRootDeptLikeTokens[strings.ToLower(strings.TrimSpace(t))]; ok { return fmt.Errorf("flag --%s 包含非法占位符 %q;钉钉根部门 deptId=1,请使用 --%s 1", setName, t, setName) } } return callMCPTool("get_dept_members_by_deptId", map[string]any{"deptIds": parseCSVValues(raw)}) case contactAnyFlagChanged(cmd, contactUserIDFlagKeys...): return runContactUserGet(cmd, args) case contactAnyFlagChanged(cmd, "names", "name", "query", "keyword"): return runContactLabelGet(cmd, args) default: return runContactLabelList(cmd, args) } }, } contactRootListCmd.Flags().String("depts", "", "部门 ID 列表") contactRootListCmd.Flags().String("id", "", "--depts 的别名") contactRootListCmd.Flags().String("dept-id", "", "--depts 的别名") contactRootListCmd.Flags().String("dept-ids", "", "--depts 的别名") _ = contactRootListCmd.Flags().MarkHidden("id") _ = contactRootListCmd.Flags().MarkHidden("dept-id") _ = contactRootListCmd.Flags().MarkHidden("dept-ids") addUserIDFlags(contactRootListCmd) addLabelNameFlags(contactRootListCmd) for _, use := range []string{"self", "me", "whoami", "get-self", "user-self", "current-user"} { root.AddCommand(&cobra.Command{ Use: use, Short: "获取当前用户信息(兼容入口)", Hidden: true, RunE: contactUserGetSelfCmd.RunE, }) } root.AddCommand(contactRootSearchCmd, contactRootFindCmd, contactRootGetCmd, contactRootListCmd) contactHintSubCmd := func(use, suggestion string) *cobra.Command { c := hintSubCmd(use, suggestion) runHint := c.RunE c.DisableFlagParsing = true c.RunE = func(cmd *cobra.Command, args []string) error { for _, arg := range args { if arg == "--help" || arg == "-h" { return cmd.Help() } } return runHint(cmd, args) } return c } root.AddCommand(contactHintSubCmd("department", "use: dws contact dept [search|list-members|list-children|get-info]")) // hint: dws contact user find/list/info/detail → 指向 user search / user get userCmd.AddCommand(contactHintSubCmd("find", "use: dws contact user search --query <关键词>")) userCmd.AddCommand(contactHintSubCmd("list", "use: dws contact user search --query <关键词>")) // 针对 LLM 常混淆的 REST 风格子命名:user info / user detail / user get-info userCmd.AddCommand(contactHintSubCmd("info", "use: dws contact user get --ids <用户ID> or dws contact user get-self")) userCmd.AddCommand(contactHintSubCmd("detail", "use: dws contact user get --ids <用户ID>")) userCmd.AddCommand(contactHintSubCmd("get-info", "use: dws contact user get --ids <用户ID>")) // 注:me / whoami / current 现已是 get-self 的真别名(Aliases),不再注册 hintSubCmd(会与真别名冲突)。 // hint: dws contact dept list / dept info / dept detail → 指向 list-members / list-children / get-info contactDeptCmd.AddCommand(contactHintSubCmd("list", "use: dws contact dept list-members --depts <部门ID> or dws contact dept list-children --dept <父部门ID>")) contactDeptCmd.AddCommand(contactHintSubCmd("info", "use: dws contact dept get-info --dept <部门ID>")) contactDeptCmd.AddCommand(contactHintSubCmd("detail", "use: dws contact dept get-info --dept <部门ID>")) // dws contact label find/search/info/detail/list-all → 真实兼容入口。 // 注:list 已是真命令(label list),不再注册 hintSubCmd(会与真命令冲突)。 for _, use := range []string{"find", "search", "info"} { cmd := &cobra.Command{Use: use, Hidden: true, RunE: runContactLabelGet} addLabelNameFlags(cmd) contactLabelCmd.AddCommand(cmd) } contactLabelDetailCmd := &cobra.Command{Use: "detail", Hidden: true, RunE: runContactLabelMembers} addLabelIDFlags(contactLabelDetailCmd) contactLabelCmd.AddCommand(contactLabelDetailCmd) contactLabelCmd.AddCommand(&cobra.Command{Use: "list-all", Hidden: true, RunE: runContactLabelList}) // contact 子树统一错误兜底:任何 flag 解析失败均在尾部追加 "See ' --help' for usage." // 与 docker / kubectl / gh 的 UX 一致。unknown subcommand 由 cobra 自带 Did-You-Mean 处理。 var attachContactHelpHint func(c *cobra.Command) attachContactHelpHint = func(c *cobra.Command) { c.SetFlagErrorFunc(func(cc *cobra.Command, err error) error { // 与 root 级 flagErrorWithSuggestions 保持同款尾部 hint 格式(句号结尾为全树 UX 约定)。 msg := fmt.Sprintf("%s\nSee '%s --help' for usage.", err.Error(), cc.CommandPath()) return errors.New(msg) }) for _, sub := range c.Commands() { attachContactHelpHint(sub) } } attachContactHelpHint(root) return root } // parseCSVInts 解析逗号分隔的整数字符串为 []int64 切片, // 去除空白并过滤无法解析的项。 func parseCSVInts(s string) []int64 { parts := strings.Split(s, ",") result := make([]int64, 0, len(parts)) for _, p := range parts { if trimmed := strings.TrimSpace(p); trimmed != "" { if n, err := strconv.ParseInt(trimmed, 10, 64); err == nil { result = append(result, n) } } } return result } func parseCSVIntsStrict(s string) ([]int64, error) { parts := strings.Split(s, ",") result := make([]int64, 0, len(parts)) for i, p := range parts { trimmed := strings.TrimSpace(p) if trimmed == "" { return nil, fmt.Errorf("第 %d 项为空", i+1) } n, err := strconv.ParseInt(trimmed, 10, 64) if err != nil { return nil, fmt.Errorf("第 %d 项 %q 不是整数: %w", i+1, trimmed, err) } result = append(result, n) } return result, nil }