diff --git a/.changes/edu-college-vendor-extensions.md b/.changes/edu-college-vendor-extensions.md new file mode 100644 index 00000000..463f8c96 --- /dev/null +++ b/.changes/edu-college-vendor-extensions.md @@ -0,0 +1,5 @@ +--- +category: Added +--- + +- **Edu & College vendor extensions** — adds five hidden vendor extension commands for education scenarios: `dws edu-contact` (school/class/family/teacher contact management), `dws edu-group` (student/class group lifecycle), `dws edu-app` (homework, notices, report cards, diplomas, class circles), `dws edu-familygroup` (family group management, child binding, app permissions), and `dws college-contact` (university dept/employee/alumni/graduate management). All route to dedicated MCP servers via `callMCPToolOnServer`. diff --git a/internal/helpers/college_contact.go b/internal/helpers/college_contact.go new file mode 100644 index 00000000..65fb4ca2 --- /dev/null +++ b/internal/helpers/college_contact.go @@ -0,0 +1,6012 @@ +package helpers + +import ( + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" +) + +// ────────────────────────────────────────────────────────── +// dws college-contact — 高校通讯录扩展 +// 共 65 个工具,按 dept / employee / alumni / graduate / group 分组 +// ────────────────────────────────────────────────────────── + +func newCollegeContactCommand() *cobra.Command { + contract.RegisterProductDecl(contract.ProductDecl{ + ID: "college-contact", + Selection: contract.ProductSelectionDecl{ + AgentSummary: "高校通讯录:高校组织架构/院系部门管理/师生员工管理/校友/毕业年级/规则管理", + UseWhen: []string{ + "用户要查询或管理钉钉高校通讯录,包括组织架构、部门、教职工、学生、校友、毕业年级等信息。", + }, + AvoidWhen: []string{ + "家校通讯录用 edu-contact;家校群用 edu-group;家庭群用 edu-familygroup;家校应用用 edu-app。", + }, + }, + }) + root := &cobra.Command{ + Use: "college-contact", + Short: "高校通讯录", + Long: `钉钉高校通讯录扩展:查询高校组织架构/院系/师生信息等。`, + Hidden: true, + RunE: groupRunE, + } + + // ════════════════════════════════════════════════════════════ + // dept 子命令组 — 部门管理 + // ════════════════════════════════════════════════════════════ + + deptCmd := &cobra.Command{Use: "dept", Short: "部门管理", RunE: groupRunE} + + getStandardStructureCmd := &cobra.Command{ + Use: "get-standard-structure", + Short: "查询高校标准架构信息", + Long: `查询高校标准架构信息,返回组织 ID、行政架构部门 ID 及架构固定部门 ID 映射。 +所有参数均可选: + --dept-id 按部门 ID 过滤 + --staff-id 按员工 staffId 过滤 + --keyword 按关键词搜索 + --offset / --size 分页参数`, + Example: ` dws college-contact dept get-standard-structure + dws college-contact dept get-standard-structure --dept-id 12345 + dws college-contact dept get-standard-structure --keyword 计算机 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if v, _ := cmd.Flags().GetString("dept-id"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input["deptId"] = n + } + if v, _ := cmd.Flags().GetString("staff-id"); strings.TrimSpace(v) != "" { + input["staffId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("keyword"); strings.TrimSpace(v) != "" { + input["keyword"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("offset"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--offset 须为整数: %w", err) + } + input["offset"] = n + } + if v, _ := cmd.Flags().GetString("size"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--size 须为整数: %w", err) + } + input["size"] = n + } + return callMCPToolOnServer("college-contact", "get_college_standard_structure", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(getStandardStructureCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_college_standard_structure", + CanonicalPath: "college-contact.get_college_standard_structure", + CLIPath: "college-contact dept get-standard-structure", + PrimaryCLIPath: "college-contact dept get-standard-structure", + }, + Description: "查询高校标准架构信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_college_standard_structure"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询高校标准架构信息", + UseWhen: []string{"需要查询高校标准架构信息时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact dept get-standard-structure --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(false)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "keyword", Property: "input.keyword", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + getDeptDetailCmd := &cobra.Command{ + Use: "get-detail", + Short: "查询部门详情", + Long: `查询高校部门详情,返回组织 ID、部门 ID、部门类型、部门编码及行政架构部门 ID。 +--dept-id 为必填,其余可选: + --staff-id 按员工 staffId 过滤 + --keyword 按关键词搜索 + --offset / --size 分页参数(size 默认 20)`, + Example: ` dws college-contact dept get-detail --dept-id 12345 + dws college-contact dept get-detail --dept-id 12345 --keyword 计算机 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIDRaw, _ := cmd.Flags().GetString("dept-id") + deptIDRaw = strings.TrimSpace(deptIDRaw) + if deptIDRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptID, err := strconv.ParseInt(deptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input := map[string]any{"deptId": deptID} + if v, _ := cmd.Flags().GetString("staff-id"); strings.TrimSpace(v) != "" { + input["staffId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("keyword"); strings.TrimSpace(v) != "" { + input["keyword"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("offset"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--offset 须为整数: %w", err) + } + input["offset"] = n + } + if v, _ := cmd.Flags().GetString("size"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--size 须为整数: %w", err) + } + input["size"] = n + } + return callMCPToolOnServer("college-contact", "get_college_dept_detail", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(getDeptDetailCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_college_dept_detail", + CanonicalPath: "college-contact.get_college_dept_detail", + CLIPath: "college-contact dept get-detail", + PrimaryCLIPath: "college-contact dept get-detail", + }, + Description: "查询部门详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_college_dept_detail"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询部门详情", + UseWhen: []string{"需要查询部门详情时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact dept get-detail --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "keyword", Property: "input.keyword", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + getDeptChainCmd := &cobra.Command{ + Use: "get-chain", + Short: "查询部门链", + Long: `查询部门链信息,返回从根节点到当前部门的部门列表及部门行业化信息。 +--dept-id 为必填,其余可选: + --staff-id 按员工 staffId 过滤 + --keyword 按关键词搜索 + --offset / --size 分页参数(size 默认 20)`, + Example: ` dws college-contact dept get-chain --dept-id 12345 + dws college-contact dept get-chain --dept-id 12345 --keyword 计算机 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIDRaw, _ := cmd.Flags().GetString("dept-id") + deptIDRaw = strings.TrimSpace(deptIDRaw) + if deptIDRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptID, err := strconv.ParseInt(deptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input := map[string]any{"deptId": deptID} + if v, _ := cmd.Flags().GetString("staff-id"); strings.TrimSpace(v) != "" { + input["staffId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("keyword"); strings.TrimSpace(v) != "" { + input["keyword"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("offset"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--offset 须为整数: %w", err) + } + input["offset"] = n + } + if v, _ := cmd.Flags().GetString("size"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--size 须为整数: %w", err) + } + input["size"] = n + } + return callMCPToolOnServer("college-contact", "get_college_dept_chain", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(getDeptChainCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_college_dept_chain", + CanonicalPath: "college-contact.get_college_dept_chain", + CLIPath: "college-contact dept get-chain", + PrimaryCLIPath: "college-contact dept get-chain", + }, + Description: "查询部门链", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_college_dept_chain"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询部门链", + UseWhen: []string{"需要查询部门链时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact dept get-chain --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "keyword", Property: "input.keyword", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + searchContactCmd := &cobra.Command{ + Use: "search", + Short: "搜索通讯录", + Long: `按关键词搜索高校通讯录,返回匹配的人员、部门及角色集合。 +--dept-id 与 --keyword 均为必填,其余可选: + --staff-id 按员工 staffId 过滤 + --offset / --size 分页参数(size 默认 20)`, + Example: ` dws college-contact dept search --dept-id 12345 --keyword 张三 + dws college-contact dept search --dept-id 12345 --keyword 计算机 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIDRaw, _ := cmd.Flags().GetString("dept-id") + deptIDRaw = strings.TrimSpace(deptIDRaw) + if deptIDRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptID, err := strconv.ParseInt(deptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + keyword, _ := cmd.Flags().GetString("keyword") + keyword = strings.TrimSpace(keyword) + if keyword == "" { + return fmt.Errorf("--keyword 为必填参数") + } + input := map[string]any{"deptId": deptID, "keyword": keyword} + if v, _ := cmd.Flags().GetString("staff-id"); strings.TrimSpace(v) != "" { + input["staffId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("offset"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--offset 须为整数: %w", err) + } + input["offset"] = n + } + if v, _ := cmd.Flags().GetString("size"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--size 须为整数: %w", err) + } + input["size"] = n + } + return callMCPToolOnServer("college-contact", "search_college_contact", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(searchContactCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "search_college_contact", + CanonicalPath: "college-contact.search_college_contact", + CLIPath: "college-contact dept search", + PrimaryCLIPath: "college-contact dept search", + }, + Description: "搜索通讯录", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "search_college_contact"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "搜索通讯录", + UseWhen: []string{"需要搜索通讯录时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact dept search --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "keyword", Property: "input.keyword", Required: boolPtr(true)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // flags + 构建命令树 + // ════════════════════════════════════════════════════════════ + + getStandardStructureCmd.Flags().String("dept-id", "", "部门 ID") + getStandardStructureCmd.Flags().String("staff-id", "", "员工 staffId") + getStandardStructureCmd.Flags().String("keyword", "", "搜索关键词") + getStandardStructureCmd.Flags().String("offset", "", "分页偏移量") + getStandardStructureCmd.Flags().String("size", "", "分页大小") + + getDeptDetailCmd.Flags().String("dept-id", "", "部门 ID(必填)") + getDeptDetailCmd.Flags().String("staff-id", "", "员工 staffId") + getDeptDetailCmd.Flags().String("keyword", "", "搜索关键词") + getDeptDetailCmd.Flags().String("offset", "", "分页偏移量") + getDeptDetailCmd.Flags().String("size", "", "分页大小(默认 20)") + + getDeptChainCmd.Flags().String("dept-id", "", "部门 ID(必填)") + getDeptChainCmd.Flags().String("staff-id", "", "员工 staffId") + getDeptChainCmd.Flags().String("keyword", "", "搜索关键词") + getDeptChainCmd.Flags().String("offset", "", "分页偏移量") + getDeptChainCmd.Flags().String("size", "", "分页大小(默认 20)") + + searchContactCmd.Flags().String("dept-id", "", "部门 ID(必填)") + searchContactCmd.Flags().String("keyword", "", "搜索关键词(必填)") + searchContactCmd.Flags().String("staff-id", "", "员工 staffId") + searchContactCmd.Flags().String("offset", "", "分页偏移量") + searchContactCmd.Flags().String("size", "", "分页大小(默认 20)") + + createDeptCmd := &cobra.Command{ + Use: "create", + Short: "创建部门", + Long: `创建高校部门,返回创建成功的部门 ID。 +必填: + --super-id 上级部门 ID + --stru-dept-id 所属组织架构部门 ID + --name 部门名称 + --dept-type 部门类型 + --create-dept-group 是否创建部门群(true/false) +可选: + --dept-id 部门 ID + --dept-code 部门编码 + --brief 简介 + --phone 电话`, + Example: ` dws college-contact dept create --super-id 100 --stru-dept-id 200 --name 计算机学院 --dept-type college --create-dept-group true + dws college-contact dept create --super-id 100 --stru-dept-id 200 --name 计算机学院 --dept-type college --create-dept-group false --brief "学院简介" --phone 010-12345678 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"super-id", "superId"}, + {"stru-dept-id", "struDeptId"}, + } { + raw, _ := cmd.Flags().GetString(pair.flag) + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--%s 为必填参数", pair.flag) + } + n, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + + // 必填 string + for _, flag := range []string{"name", "dept-type"} { + v, _ := cmd.Flags().GetString(flag) + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("--%s 为必填参数", flag) + } + key := flag + if flag == "dept-type" { + key = "deptType" + } + input[key] = v + } + + // 必填 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + if !cmd.Flags().Changed("create-dept-group") { + return fmt.Errorf("--create-dept-group 为必填参数(true/false)") + } + cdgRaw, _ := cmd.Flags().GetString("create-dept-group") + createDeptGroup, err := strconv.ParseBool(strings.TrimSpace(cdgRaw)) + if err != nil { + return fmt.Errorf("--create-dept-group 须为 true 或 false: %w", err) + } + input["createDeptGroup"] = createDeptGroup + + // 可选 int64 + if v, _ := cmd.Flags().GetString("dept-id"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input["deptId"] = n + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"dept-code", "deptCode"}, + {"brief", "brief"}, + {"phone", "phone"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + return callMCPToolOnServer("college-contact", "create_college_contact_dept", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(createDeptCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "create_college_contact_dept", + CanonicalPath: "college-contact.create_college_contact_dept", + CLIPath: "college-contact dept create", + PrimaryCLIPath: "college-contact dept create", + }, + Description: "创建部门", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "create_college_contact_dept"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "创建部门", + UseWhen: []string{"需要创建部门时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact dept create --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "super-id", Property: "input.superId", Required: boolPtr(true)}, + {Name: "stru-dept-id", Property: "input.struDeptId", Required: boolPtr(true)}, + {Name: "name", Property: "input.name", Required: boolPtr(true)}, + {Name: "dept-type", Property: "input.deptType", Required: boolPtr(true)}, + {Name: "create-dept-group", Property: "input.createDeptGroup", Required: boolPtr(true)}, + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(false)}, + {Name: "dept-code", Property: "input.deptCode", Required: boolPtr(false)}, + {Name: "brief", Property: "input.brief", Required: boolPtr(false)}, + {Name: "phone", Property: "input.phone", Required: boolPtr(false)}, + }, + }, + }) + + createDeptCmd.Flags().String("super-id", "", "上级部门 ID(必填)") + createDeptCmd.Flags().String("stru-dept-id", "", "所属组织架构部门 ID(必填)") + createDeptCmd.Flags().String("name", "", "部门名称(必填)") + createDeptCmd.Flags().String("dept-type", "", "部门类型(必填)") + createDeptCmd.Flags().String("create-dept-group", "", "是否创建部门群(必填,true/false)") + createDeptCmd.Flags().String("dept-id", "", "部门 ID") + createDeptCmd.Flags().String("dept-code", "", "部门编码") + createDeptCmd.Flags().String("brief", "", "简介") + createDeptCmd.Flags().String("phone", "", "电话") + + updateDeptCmd := &cobra.Command{ + Use: "update", + Short: "更新部门", + Long: `更新高校部门信息,返回操作是否成功。 +必填: + --dept-id 部门 ID + --dept-type 部门类型 +可选: + --name 部门名称 + --stru-dept-id 所属组织架构部门 ID + --super-id 上级部门 ID + --dept-code 部门编码 + --brief 简介 + --phone 电话 + --create-dept-group 是否创建部门群(true/false)`, + Example: ` dws college-contact dept update --dept-id 12345 --dept-type college + dws college-contact dept update --dept-id 12345 --dept-type college --name 计算机学院 --brief 学院简介 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 int64 + deptIDRaw, _ := cmd.Flags().GetString("dept-id") + deptIDRaw = strings.TrimSpace(deptIDRaw) + if deptIDRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptID, err := strconv.ParseInt(deptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input["deptId"] = deptID + + // 必填 string + v, _ := cmd.Flags().GetString("dept-type") + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("--dept-type 为必填参数") + } + input["deptType"] = v + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"stru-dept-id", "struDeptId"}, + {"super-id", "superId"}, + } { + if raw, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(raw) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + if cmd.Flags().Changed("create-dept-group") { + cdgRaw, _ := cmd.Flags().GetString("create-dept-group") + createDeptGroup, err := strconv.ParseBool(strings.TrimSpace(cdgRaw)) + if err != nil { + return fmt.Errorf("--create-dept-group 须为 true 或 false: %w", err) + } + input["createDeptGroup"] = createDeptGroup + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"name", "name"}, + {"dept-code", "deptCode"}, + {"brief", "brief"}, + {"phone", "phone"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + return callMCPToolOnServer("college-contact", "update_college_contact_dept", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(updateDeptCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "update_college_contact_dept", + CanonicalPath: "college-contact.update_college_contact_dept", + CLIPath: "college-contact dept update", + PrimaryCLIPath: "college-contact dept update", + }, + Description: "更新部门", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "update_college_contact_dept"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "更新部门", + UseWhen: []string{"需要更新部门时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact dept update --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "dept-type", Property: "input.deptType", Required: boolPtr(true)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "stru-dept-id", Property: "input.struDeptId", Required: boolPtr(false)}, + {Name: "super-id", Property: "input.superId", Required: boolPtr(false)}, + {Name: "dept-code", Property: "input.deptCode", Required: boolPtr(false)}, + {Name: "brief", Property: "input.brief", Required: boolPtr(false)}, + {Name: "phone", Property: "input.phone", Required: boolPtr(false)}, + {Name: "create-dept-group", Property: "input.createDeptGroup", Required: boolPtr(false)}, + }, + }, + }) + + updateDeptCmd.Flags().String("dept-id", "", "部门 ID(必填)") + updateDeptCmd.Flags().String("dept-type", "", "部门类型(必填)") + updateDeptCmd.Flags().String("name", "", "部门名称") + updateDeptCmd.Flags().String("stru-dept-id", "", "所属组织架构部门 ID") + updateDeptCmd.Flags().String("super-id", "", "上级部门 ID") + updateDeptCmd.Flags().String("dept-code", "", "部门编码") + updateDeptCmd.Flags().String("brief", "", "简介") + updateDeptCmd.Flags().String("phone", "", "电话") + updateDeptCmd.Flags().String("create-dept-group", "", "是否创建部门群(true/false)") + + deleteDeptCmd := &cobra.Command{ + Use: "delete", + Short: "删除部门", + Long: `删除高校部门,返回操作是否成功。 +--dept-id 为必填。 + +注意:删除操作不可逆。非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact dept delete --dept-id 12345 --dry-run + dws college-contact dept delete --dept-id 12345 -y + dws college-contact dept delete --dept-id 12345 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIDRaw, _ := cmd.Flags().GetString("dept-id") + deptIDRaw = strings.TrimSpace(deptIDRaw) + if deptIDRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptID, err := strconv.ParseInt(deptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + if err := collegeContactConfirmDestructive(cmd, "删除部门"); err != nil { + return err + } + return callMCPToolOnServer("college-contact", "delete_college_contact_dept", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + + DeclareLeafMetadata(deleteDeptCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "delete_college_contact_dept", + CanonicalPath: "college-contact.delete_college_contact_dept", + CLIPath: "college-contact dept delete", + PrimaryCLIPath: "college-contact dept delete", + }, + Description: "删除部门", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "delete_college_contact_dept"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "删除部门", + UseWhen: []string{"需要删除部门时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact dept delete --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + deleteDeptCmd.Flags().String("dept-id", "", "部门 ID(必填)") + + batchUpdateDeptTypeCmd := &cobra.Command{ + Use: "batch-update-type", + Short: "批量修改部门类型", + Long: `批量修改部门类型,返回操作是否成功。 +必填: + --dept-ids 部门 ID 列表,逗号分隔(如 1,2,3) + --target-dept-type 目标部门类型`, + Example: ` dws college-contact dept batch-update-type --dept-ids 100,200,300 --target-dept-type college + dws college-contact dept batch-update-type --dept-ids 100,200 --target-dept-type department -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIDsRaw, _ := cmd.Flags().GetString("dept-ids") + deptIDsRaw = strings.TrimSpace(deptIDsRaw) + if deptIDsRaw == "" { + return fmt.Errorf("--dept-ids 为必填参数") + } + var deptIDs []int64 + for _, part := range strings.Split(deptIDsRaw, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + n, err := strconv.ParseInt(part, 10, 64) + if err != nil { + return fmt.Errorf("--dept-ids 中 %q 须为整数: %w", part, err) + } + deptIDs = append(deptIDs, n) + } + if len(deptIDs) == 0 { + return fmt.Errorf("--dept-ids 不能为空列表") + } + + targetDeptType, _ := cmd.Flags().GetString("target-dept-type") + targetDeptType = strings.TrimSpace(targetDeptType) + if targetDeptType == "" { + return fmt.Errorf("--target-dept-type 为必填参数") + } + + input := map[string]any{ + "deptIds": deptIDs, + "targetDeptType": targetDeptType, + } + + return callMCPToolOnServer("college-contact", "batch_update_dept_type", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(batchUpdateDeptTypeCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "batch_update_dept_type", + CanonicalPath: "college-contact.batch_update_dept_type", + CLIPath: "college-contact dept batch-update-type", + PrimaryCLIPath: "college-contact dept batch-update-type", + }, + Description: "批量修改部门类型", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "batch_update_dept_type"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "批量修改部门类型", + UseWhen: []string{"需要批量修改部门类型时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact dept batch-update-type --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-ids", Property: "input.deptIds", Required: boolPtr(true)}, + {Name: "target-dept-type", Property: "input.targetDeptType", Required: boolPtr(true)}, + }, + }, + }) + + batchUpdateDeptTypeCmd.Flags().String("dept-ids", "", "部门 ID 列表,逗号分隔(必填)") + batchUpdateDeptTypeCmd.Flags().String("target-dept-type", "", "目标部门类型(必填)") + + listEmployeesCmd := &cobra.Command{ + Use: "list-employees", + Short: "查询部门员工列表", + Long: `查询指定部门的员工列表,返回员工总数及员工列表。 +必填: + --dept-id 部门 ID +可选: + --staff-id 员工 staffId + --name 员工姓名 + --mobile 手机号 + --job-number 工号 + --main-dept-id 主部门 ID + --login-id-type 登录类型 + --exclusive-account 是否独占账号(true/false) + --send-active-sms 是否发送激活短信(true/false) + --staff-ids 批量操作的 staffId 列表,逗号分隔 + --target-dept-id 变更目标部门 ID + --offset / --size 分页参数 + --order-field 排序字段(如 job_number / order_in_dept) + --ordering 排序顺序(asc / desc)`, + Example: ` dws college-contact employee list-employees --dept-id 12345 + dws college-contact employee list-employees --dept-id 12345 --name 张三 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 int64 + deptIDRaw, _ := cmd.Flags().GetString("dept-id") + deptIDRaw = strings.TrimSpace(deptIDRaw) + if deptIDRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptID, err := strconv.ParseInt(deptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input["deptId"] = deptID + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"main-dept-id", "mainDeptId"}, + {"target-dept-id", "targetDeptId"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if raw, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(raw) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + for _, pair := range []struct { + flag string + key string + }{ + {"exclusive-account", "exclusiveAccount"}, + {"send-active-sms", "sendActiveSms"}, + } { + if cmd.Flags().Changed(pair.flag) { + raw, _ := cmd.Flags().GetString(pair.flag) + v, err := strconv.ParseBool(strings.TrimSpace(raw)) + if err != nil { + return fmt.Errorf("--%s 须为 true 或 false: %w", pair.flag, err) + } + input[pair.key] = v + } + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"staff-id", "staffId"}, + {"name", "name"}, + {"mobile", "mobile"}, + {"job-number", "jobNumber"}, + {"login-id-type", "loginIdType"}, + {"order-field", "orderField"}, + {"ordering", "ordering"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + // 可选 []string: --staff-ids + if raw, _ := cmd.Flags().GetString("staff-ids"); strings.TrimSpace(raw) != "" { + var staffIDs []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIDs = append(staffIDs, part) + } + } + if len(staffIDs) > 0 { + input["staffIds"] = staffIDs + } + } + + return callMCPToolOnServer("college-contact", "list_employees", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(listEmployeesCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "list_employees", + CanonicalPath: "college-contact.list_employees", + CLIPath: "college-contact employee list-employees", + PrimaryCLIPath: "college-contact employee list-employees", + }, + Description: "查询部门员工列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "list_employees"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询部门员工列表", + UseWhen: []string{"需要查询部门员工列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee list-employees --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "job-number", Property: "input.jobNumber", Required: boolPtr(false)}, + {Name: "main-dept-id", Property: "input.mainDeptId", Required: boolPtr(false)}, + {Name: "login-id-type", Property: "input.loginIdType", Required: boolPtr(false)}, + {Name: "exclusive-account", Property: "input.exclusiveAccount", Required: boolPtr(false)}, + {Name: "send-active-sms", Property: "input.sendActiveSms", Required: boolPtr(false)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(false)}, + {Name: "target-dept-id", Property: "input.targetDeptId", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(false)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(false)}, + }, + }, + }) + + listEmployeesCmd.Flags().String("dept-id", "", "部门 ID(必填)") + listEmployeesCmd.Flags().String("staff-id", "", "员工 staffId") + listEmployeesCmd.Flags().String("name", "", "员工姓名") + listEmployeesCmd.Flags().String("mobile", "", "手机号") + listEmployeesCmd.Flags().String("job-number", "", "工号") + listEmployeesCmd.Flags().String("main-dept-id", "", "主部门 ID") + listEmployeesCmd.Flags().String("login-id-type", "", "登录类型") + listEmployeesCmd.Flags().String("exclusive-account", "", "是否独占账号(true/false)") + listEmployeesCmd.Flags().String("send-active-sms", "", "是否发送激活短信(true/false)") + listEmployeesCmd.Flags().String("staff-ids", "", "批量操作的 staffId 列表,逗号分隔") + listEmployeesCmd.Flags().String("target-dept-id", "", "变更目标部门 ID") + listEmployeesCmd.Flags().String("offset", "", "分页偏移量") + listEmployeesCmd.Flags().String("size", "", "分页大小") + listEmployeesCmd.Flags().String("order-field", "", "排序字段(如 job_number / order_in_dept)") + listEmployeesCmd.Flags().String("ordering", "", "排序顺序(asc / desc)") + + listUnacceptedEmployeesCmd := &cobra.Command{ + Use: "list-unaccepted", + Short: "查询未接受邀请的员工列表", + Long: `查询指定部门下未接受邀请的员工列表,返回员工总数及员工列表。 +必填: + --dept-id 部门 ID +可选: + --staff-id 员工 staffId + --name 员工姓名 + --mobile 手机号 + --job-number 工号 + --emp-type 员工类型(college_student / college_teacher) + --main-dept-id 主部门 ID + --login-id-type 登录类型 + --exclusive-account 是否独占账号(true/false) + --send-active-sms 是否发送激活短信(true/false) + --staff-ids 批量操作的 staffId 列表,逗号分隔 + --target-dept-id 变更目标部门 ID + --offset / --size 分页参数 + --order-field 排序字段 + --ordering 排序顺序(asc / desc)`, + Example: ` dws college-contact employee list-unaccepted --dept-id 12345 + dws college-contact employee list-unaccepted --dept-id 12345 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 int64 + deptIDRaw, _ := cmd.Flags().GetString("dept-id") + deptIDRaw = strings.TrimSpace(deptIDRaw) + if deptIDRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptID, err := strconv.ParseInt(deptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input["deptId"] = deptID + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"main-dept-id", "mainDeptId"}, + {"target-dept-id", "targetDeptId"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if raw, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(raw) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + for _, pair := range []struct { + flag string + key string + }{ + {"exclusive-account", "exclusiveAccount"}, + {"send-active-sms", "sendActiveSms"}, + } { + if cmd.Flags().Changed(pair.flag) { + raw, _ := cmd.Flags().GetString(pair.flag) + v, err := strconv.ParseBool(strings.TrimSpace(raw)) + if err != nil { + return fmt.Errorf("--%s 须为 true 或 false: %w", pair.flag, err) + } + input[pair.key] = v + } + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"staff-id", "staffId"}, + {"name", "name"}, + {"mobile", "mobile"}, + {"job-number", "jobNumber"}, + {"emp-type", "empType"}, + {"login-id-type", "loginIdType"}, + {"order-field", "orderField"}, + {"ordering", "ordering"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + // 可选 []string: --staff-ids + if raw, _ := cmd.Flags().GetString("staff-ids"); strings.TrimSpace(raw) != "" { + var staffIDs []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIDs = append(staffIDs, part) + } + } + if len(staffIDs) > 0 { + input["staffIds"] = staffIDs + } + } + + return callMCPToolOnServer("college-contact", "list_unaccepted_employees", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(listUnacceptedEmployeesCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "list_unaccepted_employees", + CanonicalPath: "college-contact.list_unaccepted_employees", + CLIPath: "college-contact employee list-unaccepted", + PrimaryCLIPath: "college-contact employee list-unaccepted", + }, + Description: "查询未接受邀请的员工列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "list_unaccepted_employees"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询未接受邀请的员工列表", + UseWhen: []string{"需要查询未接受邀请的员工列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee list-unaccepted --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "job-number", Property: "input.jobNumber", Required: boolPtr(false)}, + {Name: "emp-type", Property: "input.empType", Required: boolPtr(false)}, + {Name: "main-dept-id", Property: "input.mainDeptId", Required: boolPtr(false)}, + {Name: "login-id-type", Property: "input.loginIdType", Required: boolPtr(false)}, + {Name: "exclusive-account", Property: "input.exclusiveAccount", Required: boolPtr(false)}, + {Name: "send-active-sms", Property: "input.sendActiveSms", Required: boolPtr(false)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(false)}, + {Name: "target-dept-id", Property: "input.targetDeptId", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(false)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(false)}, + }, + }, + }) + + listUnacceptedEmployeesCmd.Flags().String("dept-id", "", "部门 ID(必填)") + listUnacceptedEmployeesCmd.Flags().String("staff-id", "", "员工 staffId") + listUnacceptedEmployeesCmd.Flags().String("name", "", "员工姓名") + listUnacceptedEmployeesCmd.Flags().String("mobile", "", "手机号") + listUnacceptedEmployeesCmd.Flags().String("job-number", "", "工号") + listUnacceptedEmployeesCmd.Flags().String("emp-type", "", "员工类型(college_student / college_teacher)") + listUnacceptedEmployeesCmd.Flags().String("main-dept-id", "", "主部门 ID") + listUnacceptedEmployeesCmd.Flags().String("login-id-type", "", "登录类型") + listUnacceptedEmployeesCmd.Flags().String("exclusive-account", "", "是否独占账号(true/false)") + listUnacceptedEmployeesCmd.Flags().String("send-active-sms", "", "是否发送激活短信(true/false)") + listUnacceptedEmployeesCmd.Flags().String("staff-ids", "", "批量操作的 staffId 列表,逗号分隔") + listUnacceptedEmployeesCmd.Flags().String("target-dept-id", "", "变更目标部门 ID") + listUnacceptedEmployeesCmd.Flags().String("offset", "", "分页偏移量") + listUnacceptedEmployeesCmd.Flags().String("size", "", "分页大小") + listUnacceptedEmployeesCmd.Flags().String("order-field", "", "排序字段") + listUnacceptedEmployeesCmd.Flags().String("ordering", "", "排序顺序(asc / desc)") + + listUnactiveEmployeesCmd := &cobra.Command{ + Use: "list-unactive", + Short: "查询未激活的员工列表", + Long: `查询指定部门下未激活的员工列表,返回员工总数及员工列表。 +必填: + --dept-id 部门 ID +可选: + --staff-id 员工 staffId + --name 员工姓名 + --mobile 手机号 + --job-number 工号 + --main-dept-id 主部门 ID + --login-id-type 登录类型 + --exclusive-account 是否独占账号(true/false) + --send-active-sms 是否发送激活短信(true/false) + --staff-ids 批量操作的 staffId 列表,逗号分隔 + --target-dept-id 变更目标部门 ID + --offset / --size 分页参数 + --order-field 排序字段 + --ordering 排序顺序(asc / desc)`, + Example: ` dws college-contact employee list-unactive --dept-id 12345 + dws college-contact employee list-unactive --dept-id 12345 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 int64 + deptIDRaw, _ := cmd.Flags().GetString("dept-id") + deptIDRaw = strings.TrimSpace(deptIDRaw) + if deptIDRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptID, err := strconv.ParseInt(deptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input["deptId"] = deptID + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"main-dept-id", "mainDeptId"}, + {"target-dept-id", "targetDeptId"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if raw, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(raw) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + for _, pair := range []struct { + flag string + key string + }{ + {"exclusive-account", "exclusiveAccount"}, + {"send-active-sms", "sendActiveSms"}, + } { + if cmd.Flags().Changed(pair.flag) { + raw, _ := cmd.Flags().GetString(pair.flag) + v, err := strconv.ParseBool(strings.TrimSpace(raw)) + if err != nil { + return fmt.Errorf("--%s 须为 true 或 false: %w", pair.flag, err) + } + input[pair.key] = v + } + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"staff-id", "staffId"}, + {"name", "name"}, + {"mobile", "mobile"}, + {"job-number", "jobNumber"}, + {"login-id-type", "loginIdType"}, + {"order-field", "orderField"}, + {"ordering", "ordering"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + // 可选 []string: --staff-ids + if raw, _ := cmd.Flags().GetString("staff-ids"); strings.TrimSpace(raw) != "" { + var staffIDs []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIDs = append(staffIDs, part) + } + } + if len(staffIDs) > 0 { + input["staffIds"] = staffIDs + } + } + + return callMCPToolOnServer("college-contact", "list_unactive_employees", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(listUnactiveEmployeesCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "list_unactive_employees", + CanonicalPath: "college-contact.list_unactive_employees", + CLIPath: "college-contact employee list-unactive", + PrimaryCLIPath: "college-contact employee list-unactive", + }, + Description: "查询未激活的员工列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "list_unactive_employees"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询未激活的员工列表", + UseWhen: []string{"需要查询未激活的员工列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee list-unactive --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "job-number", Property: "input.jobNumber", Required: boolPtr(false)}, + {Name: "main-dept-id", Property: "input.mainDeptId", Required: boolPtr(false)}, + {Name: "login-id-type", Property: "input.loginIdType", Required: boolPtr(false)}, + {Name: "exclusive-account", Property: "input.exclusiveAccount", Required: boolPtr(false)}, + {Name: "send-active-sms", Property: "input.sendActiveSms", Required: boolPtr(false)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(false)}, + {Name: "target-dept-id", Property: "input.targetDeptId", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(false)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(false)}, + }, + }, + }) + + listUnactiveEmployeesCmd.Flags().String("dept-id", "", "部门 ID(必填)") + listUnactiveEmployeesCmd.Flags().String("staff-id", "", "员工 staffId") + listUnactiveEmployeesCmd.Flags().String("name", "", "员工姓名") + listUnactiveEmployeesCmd.Flags().String("mobile", "", "手机号") + listUnactiveEmployeesCmd.Flags().String("job-number", "", "工号") + listUnactiveEmployeesCmd.Flags().String("main-dept-id", "", "主部门 ID") + listUnactiveEmployeesCmd.Flags().String("login-id-type", "", "登录类型") + listUnactiveEmployeesCmd.Flags().String("exclusive-account", "", "是否独占账号(true/false)") + listUnactiveEmployeesCmd.Flags().String("send-active-sms", "", "是否发送激活短信(true/false)") + listUnactiveEmployeesCmd.Flags().String("staff-ids", "", "批量操作的 staffId 列表,逗号分隔") + listUnactiveEmployeesCmd.Flags().String("target-dept-id", "", "变更目标部门 ID") + listUnactiveEmployeesCmd.Flags().String("offset", "", "分页偏移量") + listUnactiveEmployeesCmd.Flags().String("size", "", "分页大小") + listUnactiveEmployeesCmd.Flags().String("order-field", "", "排序字段") + listUnactiveEmployeesCmd.Flags().String("ordering", "", "排序顺序(asc / desc)") + + deptCmd.AddCommand(getStandardStructureCmd, getDeptDetailCmd, getDeptChainCmd, searchContactCmd, createDeptCmd, updateDeptCmd, deleteDeptCmd, batchUpdateDeptTypeCmd) + + // ════════════════════════════════════════════════════════════ + // dept 子命令组 - 部门管理 + // ════════════════════════════════════════════════════════════ + + overviewStatisticsCmd := &cobra.Command{ + Use: "overview", + Short: "查询高校概览统计", + Long: `查询高校概览统计,返回学生数、教师数、全校人数、未激活员工数、未接受邀请员工数、院系数、行政班数。 +所有参数均可选: + --dept-id 按部门 ID 过滤 + --staff-id 按员工 staffId 过滤 + --keyword 按关键词搜索 + --offset / --size 分页参数`, + Example: ` dws college-contact dept overview + dws college-contact dept overview --dept-id 12345 + dws college-contact dept overview --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if v, _ := cmd.Flags().GetString("dept-id"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input["deptId"] = n + } + if v, _ := cmd.Flags().GetString("staff-id"); strings.TrimSpace(v) != "" { + input["staffId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("keyword"); strings.TrimSpace(v) != "" { + input["keyword"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("offset"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--offset 须为整数: %w", err) + } + input["offset"] = n + } + if v, _ := cmd.Flags().GetString("size"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--size 须为整数: %w", err) + } + input["size"] = n + } + return callMCPToolOnServer("college-contact", "get_college_overview_statistics", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(overviewStatisticsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_college_overview_statistics", + CanonicalPath: "college-contact.get_college_overview_statistics", + CLIPath: "college-contact dept overview", + PrimaryCLIPath: "college-contact dept overview", + }, + Description: "查询高校概览统计", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_college_overview_statistics"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询高校概览统计", + UseWhen: []string{"需要查询高校概览统计时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact dept overview --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(false)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "keyword", Property: "input.keyword", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + overviewStatisticsCmd.Flags().String("dept-id", "", "部门 ID") + overviewStatisticsCmd.Flags().String("staff-id", "", "员工 staffId") + overviewStatisticsCmd.Flags().String("keyword", "", "搜索关键词") + overviewStatisticsCmd.Flags().String("offset", "", "分页偏移量") + overviewStatisticsCmd.Flags().String("size", "", "分页大小") + + deptCmd.AddCommand(overviewStatisticsCmd) + + upgradeStatusCmd := &cobra.Command{ + Use: "upgrade-status", + Short: "查询升级状态", + Long: `查询高校升级状态,返回升级状态码及描述信息。 +所有参数均可选: + --dept-id 按部门 ID 过滤 + --staff-id 按员工 staffId 过滤 + --keyword 按关键词搜索 + --offset / --size 分页参数`, + Example: ` dws college-contact employee upgrade-status + dws college-contact employee upgrade-status --dept-id 12345 + dws college-contact employee upgrade-status --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if v, _ := cmd.Flags().GetString("dept-id"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input["deptId"] = n + } + if v, _ := cmd.Flags().GetString("staff-id"); strings.TrimSpace(v) != "" { + input["staffId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("keyword"); strings.TrimSpace(v) != "" { + input["keyword"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("offset"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--offset 须为整数: %w", err) + } + input["offset"] = n + } + if v, _ := cmd.Flags().GetString("size"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--size 须为整数: %w", err) + } + input["size"] = n + } + return callMCPToolOnServer("college-contact", "get_college_upgrade_status", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(upgradeStatusCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_college_upgrade_status", + CanonicalPath: "college-contact.get_college_upgrade_status", + CLIPath: "college-contact employee upgrade-status", + PrimaryCLIPath: "college-contact employee upgrade-status", + }, + Description: "查询升级状态", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_college_upgrade_status"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询升级状态", + UseWhen: []string{"需要查询升级状态时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee upgrade-status --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(false)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "keyword", Property: "input.keyword", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + upgradeStatusCmd.Flags().String("dept-id", "", "部门 ID") + upgradeStatusCmd.Flags().String("staff-id", "", "员工 staffId") + upgradeStatusCmd.Flags().String("keyword", "", "搜索关键词") + upgradeStatusCmd.Flags().String("offset", "", "分页偏移量") + upgradeStatusCmd.Flags().String("size", "", "分页大小") + + startUpgradeCmd := &cobra.Command{ + Use: "start-upgrade", + Short: "启动升级", + Long: `启动高校通讯录升级,返回操作是否成功。`, + Example: ` dws college-contact employee start-upgrade + dws college-contact employee start-upgrade -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("college-contact", "start_upgrade", map[string]any{ + "input": "", + }) + }, + } + + DeclareLeafMetadata(startUpgradeCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "start_upgrade", + CanonicalPath: "college-contact.start_upgrade", + CLIPath: "college-contact employee start-upgrade", + PrimaryCLIPath: "college-contact employee start-upgrade", + }, + Description: "启动升级", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "start_upgrade"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "启动升级", + UseWhen: []string{"需要启动升级时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee start-upgrade --help"}, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + // ════════════════════════════════════════════════════════════ + // employee 子命令组 — 员工管理 + // ════════════════════════════════════════════════════════════ + + employeeCmd := &cobra.Command{Use: "employee", Short: "员工管理", RunE: groupRunE} + + getEmployeeDetailCmd := &cobra.Command{ + Use: "get-detail", + Short: "查询员工详情", + Long: `查询员工详情,返回员工基本信息、主部门、所在部门列表、账号状态等。 +必填: + --staff-id 员工 staffId +可选: + --dept-id 部门 ID + --name 员工姓名 + --mobile 手机号 + --job-number 工号 + --emp-type 员工类型(college_student / college_teacher) + --main-dept-id 主部门 ID + --login-id-type 登录类型 + --exclusive-account 是否独占账号(true/false) + --send-active-sms 是否发送激活短信(true/false) + --staff-ids 批量操作的 staffId 列表,逗号分隔 + --target-dept-id 变更目标部门 ID + --offset / --size 分页参数 + --order-field 排序字段 + --ordering 排序顺序(asc / desc)`, + Example: ` dws college-contact employee get-detail --staff-id S12345 + dws college-contact employee get-detail --staff-id S12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 string + staffID, _ := cmd.Flags().GetString("staff-id") + staffID = strings.TrimSpace(staffID) + if staffID == "" { + return fmt.Errorf("--staff-id 为必填参数") + } + input["staffId"] = staffID + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"dept-id", "deptId"}, + {"main-dept-id", "mainDeptId"}, + {"target-dept-id", "targetDeptId"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if raw, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(raw) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + for _, pair := range []struct { + flag string + key string + }{ + {"exclusive-account", "exclusiveAccount"}, + {"send-active-sms", "sendActiveSms"}, + } { + if cmd.Flags().Changed(pair.flag) { + raw, _ := cmd.Flags().GetString(pair.flag) + v, err := strconv.ParseBool(strings.TrimSpace(raw)) + if err != nil { + return fmt.Errorf("--%s 须为 true 或 false: %w", pair.flag, err) + } + input[pair.key] = v + } + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"name", "name"}, + {"mobile", "mobile"}, + {"job-number", "jobNumber"}, + {"emp-type", "empType"}, + {"login-id-type", "loginIdType"}, + {"order-field", "orderField"}, + {"ordering", "ordering"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + // 可选 []string: --staff-ids + if raw, _ := cmd.Flags().GetString("staff-ids"); strings.TrimSpace(raw) != "" { + var staffIDs []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIDs = append(staffIDs, part) + } + } + if len(staffIDs) > 0 { + input["staffIds"] = staffIDs + } + } + + return callMCPToolOnServer("college-contact", "get_employee_detail", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(getEmployeeDetailCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_employee_detail", + CanonicalPath: "college-contact.get_employee_detail", + CLIPath: "college-contact employee get-detail", + PrimaryCLIPath: "college-contact employee get-detail", + }, + Description: "查询员工详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_employee_detail"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询员工详情", + UseWhen: []string{"需要查询员工详情时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee get-detail --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(true)}, + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "job-number", Property: "input.jobNumber", Required: boolPtr(false)}, + {Name: "emp-type", Property: "input.empType", Required: boolPtr(false)}, + {Name: "main-dept-id", Property: "input.mainDeptId", Required: boolPtr(false)}, + {Name: "login-id-type", Property: "input.loginIdType", Required: boolPtr(false)}, + {Name: "exclusive-account", Property: "input.exclusiveAccount", Required: boolPtr(false)}, + {Name: "send-active-sms", Property: "input.sendActiveSms", Required: boolPtr(false)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(false)}, + {Name: "target-dept-id", Property: "input.targetDeptId", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(false)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(false)}, + }, + }, + }) + + getEmployeeDetailCmd.Flags().String("staff-id", "", "员工 staffId(必填)") + getEmployeeDetailCmd.Flags().String("dept-id", "", "部门 ID") + getEmployeeDetailCmd.Flags().String("name", "", "员工姓名") + getEmployeeDetailCmd.Flags().String("mobile", "", "手机号") + getEmployeeDetailCmd.Flags().String("job-number", "", "工号") + getEmployeeDetailCmd.Flags().String("emp-type", "", "员工类型(college_student / college_teacher)") + getEmployeeDetailCmd.Flags().String("main-dept-id", "", "主部门 ID") + getEmployeeDetailCmd.Flags().String("login-id-type", "", "登录类型") + getEmployeeDetailCmd.Flags().String("exclusive-account", "", "是否独占账号(true/false)") + getEmployeeDetailCmd.Flags().String("send-active-sms", "", "是否发送激活短信(true/false)") + getEmployeeDetailCmd.Flags().String("staff-ids", "", "批量操作的 staffId 列表,逗号分隔") + getEmployeeDetailCmd.Flags().String("target-dept-id", "", "变更目标部门 ID") + getEmployeeDetailCmd.Flags().String("offset", "", "分页偏移量") + getEmployeeDetailCmd.Flags().String("size", "", "分页大小") + getEmployeeDetailCmd.Flags().String("order-field", "", "排序字段") + getEmployeeDetailCmd.Flags().String("ordering", "", "排序顺序(asc / desc)") + + addEmployeeCmd := &cobra.Command{ + Use: "add", + Short: "添加员工", + Long: `添加高校员工,返回添加成功/失败数量及邮箱初始密码映射。 +必填: + --emp-type 员工类型(college_student / college_teacher) + --main-dept-id 主部门 ID + --exclusive-account 是否独占账号(true/false) +可选: + --dept-id 部门 ID + --staff-id 员工 staffId + --name 员工姓名 + --mobile 手机号 + --job-number 工号 + --login-id-type 登录类型 + --send-active-sms 是否发送激活短信(true/false) + --staff-ids 批量操作的 staffId 列表,逗号分隔 + --target-dept-id 变更目标部门 ID + --offset / --size 分页参数 + --order-field 排序字段 + --ordering 排序顺序(asc / desc)`, + Example: ` dws college-contact employee add --emp-type college_student --main-dept-id 100 --exclusive-account true --name 张三 --mobile 13800138000 + dws college-contact employee add --emp-type college_teacher --main-dept-id 100 --exclusive-account false --name 李老师 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 string + empType, _ := cmd.Flags().GetString("emp-type") + empType = strings.TrimSpace(empType) + if empType == "" { + return fmt.Errorf("--emp-type 为必填参数") + } + input["empType"] = empType + + // 必填 int64 + mainDeptIDRaw, _ := cmd.Flags().GetString("main-dept-id") + mainDeptIDRaw = strings.TrimSpace(mainDeptIDRaw) + if mainDeptIDRaw == "" { + return fmt.Errorf("--main-dept-id 为必填参数") + } + mainDeptID, err := strconv.ParseInt(mainDeptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--main-dept-id 须为整数: %w", err) + } + input["mainDeptId"] = mainDeptID + + // 必填 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + if !cmd.Flags().Changed("exclusive-account") { + return fmt.Errorf("--exclusive-account 为必填参数(true/false)") + } + eaRaw, _ := cmd.Flags().GetString("exclusive-account") + exclusiveAccount, err := strconv.ParseBool(strings.TrimSpace(eaRaw)) + if err != nil { + return fmt.Errorf("--exclusive-account 须为 true 或 false: %w", err) + } + input["exclusiveAccount"] = exclusiveAccount + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"dept-id", "deptId"}, + {"target-dept-id", "targetDeptId"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if raw, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(raw) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + if cmd.Flags().Changed("send-active-sms") { + sasRaw, _ := cmd.Flags().GetString("send-active-sms") + v, err := strconv.ParseBool(strings.TrimSpace(sasRaw)) + if err != nil { + return fmt.Errorf("--send-active-sms 须为 true 或 false: %w", err) + } + input["sendActiveSms"] = v + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"staff-id", "staffId"}, + {"name", "name"}, + {"mobile", "mobile"}, + {"job-number", "jobNumber"}, + {"login-id-type", "loginIdType"}, + {"order-field", "orderField"}, + {"ordering", "ordering"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + // 可选 []string: --staff-ids + if raw, _ := cmd.Flags().GetString("staff-ids"); strings.TrimSpace(raw) != "" { + var staffIDs []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIDs = append(staffIDs, part) + } + } + if len(staffIDs) > 0 { + input["staffIds"] = staffIDs + } + } + + return callMCPToolOnServer("college-contact", "add_employee", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(addEmployeeCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "add_employee", + CanonicalPath: "college-contact.add_employee", + CLIPath: "college-contact employee add", + PrimaryCLIPath: "college-contact employee add", + }, + Description: "添加员工", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "add_employee"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "添加员工", + UseWhen: []string{"需要添加员工时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee add --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "emp-type", Property: "input.empType", Required: boolPtr(true)}, + {Name: "main-dept-id", Property: "input.mainDeptId", Required: boolPtr(true)}, + {Name: "exclusive-account", Property: "input.exclusiveAccount", Required: boolPtr(true)}, + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(false)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "job-number", Property: "input.jobNumber", Required: boolPtr(false)}, + {Name: "login-id-type", Property: "input.loginIdType", Required: boolPtr(false)}, + {Name: "send-active-sms", Property: "input.sendActiveSms", Required: boolPtr(false)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(false)}, + {Name: "target-dept-id", Property: "input.targetDeptId", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(false)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(false)}, + }, + }, + }) + + addEmployeeCmd.Flags().String("emp-type", "", "员工类型(必填,college_student / college_teacher)") + addEmployeeCmd.Flags().String("main-dept-id", "", "主部门 ID(必填)") + addEmployeeCmd.Flags().String("exclusive-account", "", "是否独占账号(必填,true/false)") + addEmployeeCmd.Flags().String("dept-id", "", "部门 ID") + addEmployeeCmd.Flags().String("staff-id", "", "员工 staffId") + addEmployeeCmd.Flags().String("name", "", "员工姓名") + addEmployeeCmd.Flags().String("mobile", "", "手机号") + addEmployeeCmd.Flags().String("job-number", "", "工号") + addEmployeeCmd.Flags().String("login-id-type", "", "登录类型") + addEmployeeCmd.Flags().String("send-active-sms", "", "是否发送激活短信(true/false)") + addEmployeeCmd.Flags().String("staff-ids", "", "批量操作的 staffId 列表,逗号分隔") + addEmployeeCmd.Flags().String("target-dept-id", "", "变更目标部门 ID") + addEmployeeCmd.Flags().String("offset", "", "分页偏移量") + addEmployeeCmd.Flags().String("size", "", "分页大小") + addEmployeeCmd.Flags().String("order-field", "", "排序字段") + addEmployeeCmd.Flags().String("ordering", "", "排序顺序(asc / desc)") + + removeEmployeeCmd := &cobra.Command{ + Use: "remove", + Short: "移除员工", + Long: `移除高校员工,返回移除成功/失败数量。 +必填: + --staff-ids 员工 staffId 列表,逗号分隔 +可选: + --dept-id 部门 ID + --staff-id 员工 staffId + --name 员工姓名 + --mobile 手机号 + --job-number 工号 + --emp-type 员工类型(college_student / college_teacher) + --main-dept-id 主部门 ID + --login-id-type 登录类型 + --exclusive-account 是否独占账号(true/false) + --send-active-sms 是否发送激活短信(true/false) + --target-dept-id 变更目标部门 ID + --offset / --size 分页参数 + --order-field 排序字段 + --ordering 排序顺序(asc / desc) + +注意:移除操作不可逆。非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact employee remove --staff-ids S12345,S12346 --dry-run + dws college-contact employee remove --staff-ids S12345 -y + dws college-contact employee remove --staff-ids S12345,S12346 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 []string: --staff-ids + raw, _ := cmd.Flags().GetString("staff-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--staff-ids 为必填参数") + } + var staffIDs []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIDs = append(staffIDs, part) + } + } + if len(staffIDs) == 0 { + return fmt.Errorf("--staff-ids 不能为空列表") + } + input["staffIds"] = staffIDs + + if err := collegeContactConfirmDestructive(cmd, "移除员工"); err != nil { + return err + } + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"dept-id", "deptId"}, + {"main-dept-id", "mainDeptId"}, + {"target-dept-id", "targetDeptId"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + for _, pair := range []struct { + flag string + key string + }{ + {"exclusive-account", "exclusiveAccount"}, + {"send-active-sms", "sendActiveSms"}, + } { + if cmd.Flags().Changed(pair.flag) { + raw, _ := cmd.Flags().GetString(pair.flag) + v, err := strconv.ParseBool(strings.TrimSpace(raw)) + if err != nil { + return fmt.Errorf("--%s 须为 true 或 false: %w", pair.flag, err) + } + input[pair.key] = v + } + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"staff-id", "staffId"}, + {"name", "name"}, + {"mobile", "mobile"}, + {"job-number", "jobNumber"}, + {"emp-type", "empType"}, + {"login-id-type", "loginIdType"}, + {"order-field", "orderField"}, + {"ordering", "ordering"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + return callMCPToolOnServer("college-contact", "remove_employee", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(removeEmployeeCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "remove_employee", + CanonicalPath: "college-contact.remove_employee", + CLIPath: "college-contact employee remove", + PrimaryCLIPath: "college-contact employee remove", + }, + Description: "移除员工", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "remove_employee"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "移除员工", + UseWhen: []string{"需要移除员工时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee remove --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(true)}, + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(false)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "job-number", Property: "input.jobNumber", Required: boolPtr(false)}, + {Name: "emp-type", Property: "input.empType", Required: boolPtr(false)}, + {Name: "main-dept-id", Property: "input.mainDeptId", Required: boolPtr(false)}, + {Name: "login-id-type", Property: "input.loginIdType", Required: boolPtr(false)}, + {Name: "exclusive-account", Property: "input.exclusiveAccount", Required: boolPtr(false)}, + {Name: "send-active-sms", Property: "input.sendActiveSms", Required: boolPtr(false)}, + {Name: "target-dept-id", Property: "input.targetDeptId", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(false)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(false)}, + }, + }, + }) + + removeEmployeeCmd.Flags().String("staff-ids", "", "员工 staffId 列表,逗号分隔(必填)") + removeEmployeeCmd.Flags().String("dept-id", "", "部门 ID") + removeEmployeeCmd.Flags().String("staff-id", "", "员工 staffId") + removeEmployeeCmd.Flags().String("name", "", "员工姓名") + removeEmployeeCmd.Flags().String("mobile", "", "手机号") + removeEmployeeCmd.Flags().String("job-number", "", "工号") + removeEmployeeCmd.Flags().String("emp-type", "", "员工类型(college_student / college_teacher)") + removeEmployeeCmd.Flags().String("main-dept-id", "", "主部门 ID") + removeEmployeeCmd.Flags().String("login-id-type", "", "登录类型") + removeEmployeeCmd.Flags().String("exclusive-account", "", "是否独占账号(true/false)") + removeEmployeeCmd.Flags().String("send-active-sms", "", "是否发送激活短信(true/false)") + removeEmployeeCmd.Flags().String("target-dept-id", "", "变更目标部门 ID") + removeEmployeeCmd.Flags().String("offset", "", "分页偏移量") + removeEmployeeCmd.Flags().String("size", "", "分页大小") + removeEmployeeCmd.Flags().String("order-field", "", "排序字段") + removeEmployeeCmd.Flags().String("ordering", "", "排序顺序(asc / desc)") + + changeEmployeeTypeCmd := &cobra.Command{ + Use: "change-type", + Short: "变更员工类型", + Long: `变更高校员工类型,返回变更成功/失败数量。 +必填: + --emp-type 员工类型(college_student / college_teacher) + --staff-id 员工 staffId +可选: + --dept-id 部门 ID + --name 员工姓名 + --mobile 手机号 + --job-number 工号 + --main-dept-id 主部门 ID + --login-id-type 登录类型 + --exclusive-account 是否独占账号(true/false) + --send-active-sms 是否发送激活短信(true/false) + --staff-ids 批量操作的 staffId 列表,逗号分隔 + --target-dept-id 变更目标部门 ID + --offset / --size 分页参数 + --order-field 排序字段 + --ordering 排序顺序(asc / desc)`, + Example: ` dws college-contact employee change-type --staff-id S12345 --emp-type college_teacher + dws college-contact employee change-type --staff-id S12345 --emp-type college_student -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 string + for _, pair := range []struct { + flag string + key string + }{ + {"staff-id", "staffId"}, + {"emp-type", "empType"}, + } { + v, _ := cmd.Flags().GetString(pair.flag) + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("--%s 为必填参数", pair.flag) + } + input[pair.key] = v + } + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"dept-id", "deptId"}, + {"main-dept-id", "mainDeptId"}, + {"target-dept-id", "targetDeptId"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if raw, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(raw) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + for _, pair := range []struct { + flag string + key string + }{ + {"exclusive-account", "exclusiveAccount"}, + {"send-active-sms", "sendActiveSms"}, + } { + if cmd.Flags().Changed(pair.flag) { + raw, _ := cmd.Flags().GetString(pair.flag) + v, err := strconv.ParseBool(strings.TrimSpace(raw)) + if err != nil { + return fmt.Errorf("--%s 须为 true 或 false: %w", pair.flag, err) + } + input[pair.key] = v + } + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"name", "name"}, + {"mobile", "mobile"}, + {"job-number", "jobNumber"}, + {"login-id-type", "loginIdType"}, + {"order-field", "orderField"}, + {"ordering", "ordering"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + // 可选 []string: --staff-ids + if raw, _ := cmd.Flags().GetString("staff-ids"); strings.TrimSpace(raw) != "" { + var staffIDs []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIDs = append(staffIDs, part) + } + } + if len(staffIDs) > 0 { + input["staffIds"] = staffIDs + } + } + + return callMCPToolOnServer("college-contact", "change_employee_type", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(changeEmployeeTypeCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "change_employee_type", + CanonicalPath: "college-contact.change_employee_type", + CLIPath: "college-contact employee change-type", + PrimaryCLIPath: "college-contact employee change-type", + }, + Description: "变更员工类型", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "change_employee_type"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "变更员工类型", + UseWhen: []string{"需要变更员工类型时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee change-type --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(true)}, + {Name: "emp-type", Property: "input.empType", Required: boolPtr(true)}, + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "job-number", Property: "input.jobNumber", Required: boolPtr(false)}, + {Name: "main-dept-id", Property: "input.mainDeptId", Required: boolPtr(false)}, + {Name: "login-id-type", Property: "input.loginIdType", Required: boolPtr(false)}, + {Name: "exclusive-account", Property: "input.exclusiveAccount", Required: boolPtr(false)}, + {Name: "send-active-sms", Property: "input.sendActiveSms", Required: boolPtr(false)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(false)}, + {Name: "target-dept-id", Property: "input.targetDeptId", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(false)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(false)}, + }, + }, + }) + + changeEmployeeTypeCmd.Flags().String("staff-id", "", "员工 staffId(必填)") + changeEmployeeTypeCmd.Flags().String("emp-type", "", "员工类型(必填,college_student / college_teacher)") + changeEmployeeTypeCmd.Flags().String("dept-id", "", "部门 ID") + changeEmployeeTypeCmd.Flags().String("name", "", "员工姓名") + changeEmployeeTypeCmd.Flags().String("mobile", "", "手机号") + changeEmployeeTypeCmd.Flags().String("job-number", "", "工号") + changeEmployeeTypeCmd.Flags().String("main-dept-id", "", "主部门 ID") + changeEmployeeTypeCmd.Flags().String("login-id-type", "", "登录类型") + changeEmployeeTypeCmd.Flags().String("exclusive-account", "", "是否独占账号(true/false)") + changeEmployeeTypeCmd.Flags().String("send-active-sms", "", "是否发送激活短信(true/false)") + changeEmployeeTypeCmd.Flags().String("staff-ids", "", "批量操作的 staffId 列表,逗号分隔") + changeEmployeeTypeCmd.Flags().String("target-dept-id", "", "变更目标部门 ID") + changeEmployeeTypeCmd.Flags().String("offset", "", "分页偏移量") + changeEmployeeTypeCmd.Flags().String("size", "", "分页大小") + changeEmployeeTypeCmd.Flags().String("order-field", "", "排序字段") + changeEmployeeTypeCmd.Flags().String("ordering", "", "排序顺序(asc / desc)") + + changeEmployeeDeptCmd := &cobra.Command{ + Use: "change-dept", + Short: "变更员工部门", + Long: `变更高校员工所属部门,返回变更成功/失败数量。 +必填: + --staff-id 员工 staffId + --target-dept-id 变更目标部门 ID +可选: + --dept-id 部门 ID + --name 员工姓名 + --mobile 手机号 + --job-number 工号 + --emp-type 员工类型(college_student / college_teacher) + --main-dept-id 主部门 ID + --login-id-type 登录类型 + --exclusive-account 是否独占账号(true/false) + --send-active-sms 是否发送激活短信(true/false) + --staff-ids 批量操作的 staffId 列表,逗号分隔 + --offset / --size 分页参数 + --order-field 排序字段 + --ordering 排序顺序(asc / desc)`, + Example: ` dws college-contact employee change-dept --staff-id S12345 --target-dept-id 200 + dws college-contact employee change-dept --staff-id S12345 --target-dept-id 200 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 string + staffID, _ := cmd.Flags().GetString("staff-id") + staffID = strings.TrimSpace(staffID) + if staffID == "" { + return fmt.Errorf("--staff-id 为必填参数") + } + input["staffId"] = staffID + + // 必填 int64 + targetDeptIDRaw, _ := cmd.Flags().GetString("target-dept-id") + targetDeptIDRaw = strings.TrimSpace(targetDeptIDRaw) + if targetDeptIDRaw == "" { + return fmt.Errorf("--target-dept-id 为必填参数") + } + targetDeptID, err := strconv.ParseInt(targetDeptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--target-dept-id 须为整数: %w", err) + } + input["targetDeptId"] = targetDeptID + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"dept-id", "deptId"}, + {"main-dept-id", "mainDeptId"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if raw, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(raw) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + for _, pair := range []struct { + flag string + key string + }{ + {"exclusive-account", "exclusiveAccount"}, + {"send-active-sms", "sendActiveSms"}, + } { + if cmd.Flags().Changed(pair.flag) { + raw, _ := cmd.Flags().GetString(pair.flag) + v, err := strconv.ParseBool(strings.TrimSpace(raw)) + if err != nil { + return fmt.Errorf("--%s 须为 true 或 false: %w", pair.flag, err) + } + input[pair.key] = v + } + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"name", "name"}, + {"mobile", "mobile"}, + {"job-number", "jobNumber"}, + {"emp-type", "empType"}, + {"login-id-type", "loginIdType"}, + {"order-field", "orderField"}, + {"ordering", "ordering"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + // 可选 []string: --staff-ids + if raw, _ := cmd.Flags().GetString("staff-ids"); strings.TrimSpace(raw) != "" { + var staffIDs []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIDs = append(staffIDs, part) + } + } + if len(staffIDs) > 0 { + input["staffIds"] = staffIDs + } + } + + return callMCPToolOnServer("college-contact", "change_employee_dept", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(changeEmployeeDeptCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "change_employee_dept", + CanonicalPath: "college-contact.change_employee_dept", + CLIPath: "college-contact employee change-dept", + PrimaryCLIPath: "college-contact employee change-dept", + }, + Description: "变更员工部门", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "change_employee_dept"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "变更员工部门", + UseWhen: []string{"需要变更员工部门时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee change-dept --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(true)}, + {Name: "target-dept-id", Property: "input.targetDeptId", Required: boolPtr(true)}, + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "job-number", Property: "input.jobNumber", Required: boolPtr(false)}, + {Name: "emp-type", Property: "input.empType", Required: boolPtr(false)}, + {Name: "main-dept-id", Property: "input.mainDeptId", Required: boolPtr(false)}, + {Name: "login-id-type", Property: "input.loginIdType", Required: boolPtr(false)}, + {Name: "exclusive-account", Property: "input.exclusiveAccount", Required: boolPtr(false)}, + {Name: "send-active-sms", Property: "input.sendActiveSms", Required: boolPtr(false)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(false)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(false)}, + }, + }, + }) + + changeEmployeeDeptCmd.Flags().String("staff-id", "", "员工 staffId(必填)") + changeEmployeeDeptCmd.Flags().String("target-dept-id", "", "变更目标部门 ID(必填)") + changeEmployeeDeptCmd.Flags().String("dept-id", "", "部门 ID") + changeEmployeeDeptCmd.Flags().String("name", "", "员工姓名") + changeEmployeeDeptCmd.Flags().String("mobile", "", "手机号") + changeEmployeeDeptCmd.Flags().String("job-number", "", "工号") + changeEmployeeDeptCmd.Flags().String("emp-type", "", "员工类型(college_student / college_teacher)") + changeEmployeeDeptCmd.Flags().String("main-dept-id", "", "主部门 ID") + changeEmployeeDeptCmd.Flags().String("login-id-type", "", "登录类型") + changeEmployeeDeptCmd.Flags().String("exclusive-account", "", "是否独占账号(true/false)") + changeEmployeeDeptCmd.Flags().String("send-active-sms", "", "是否发送激活短信(true/false)") + changeEmployeeDeptCmd.Flags().String("staff-ids", "", "批量操作的 staffId 列表,逗号分隔") + changeEmployeeDeptCmd.Flags().String("offset", "", "分页偏移量") + changeEmployeeDeptCmd.Flags().String("size", "", "分页大小") + changeEmployeeDeptCmd.Flags().String("order-field", "", "排序字段") + changeEmployeeDeptCmd.Flags().String("ordering", "", "排序顺序(asc / desc)") + + sendActiveSmsCmd := &cobra.Command{ + Use: "send-active-sms", + Short: "发送激活短信", + Long: `向指定部门下的员工发送激活短信,返回操作是否成功。 +必填: + --dept-id 部门 ID +可选: + --staff-id 员工 staffId + --name 员工姓名 + --mobile 手机号 + --job-number 工号 + --emp-type 员工类型(college_student / college_teacher) + --main-dept-id 主部门 ID + --login-id-type 登录类型 + --exclusive-account 是否独占账号(true/false) + --send-active-sms 是否发送激活短信(true/false) + --staff-ids 批量操作的 staffId 列表,逗号分隔 + --target-dept-id 变更目标部门 ID + --offset / --size 分页参数 + --order-field 排序字段 + --ordering 排序顺序(asc / desc)`, + Example: ` dws college-contact employee send-active-sms --dept-id 100 + dws college-contact employee send-active-sms --dept-id 100 --staff-ids S12345,S12346 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 int64 + deptIDRaw, _ := cmd.Flags().GetString("dept-id") + deptIDRaw = strings.TrimSpace(deptIDRaw) + if deptIDRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptID, err := strconv.ParseInt(deptIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + input["deptId"] = deptID + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"main-dept-id", "mainDeptId"}, + {"target-dept-id", "targetDeptId"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if raw, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(raw) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + for _, pair := range []struct { + flag string + key string + }{ + {"exclusive-account", "exclusiveAccount"}, + {"send-active-sms", "sendActiveSms"}, + } { + if cmd.Flags().Changed(pair.flag) { + raw, _ := cmd.Flags().GetString(pair.flag) + v, err := strconv.ParseBool(strings.TrimSpace(raw)) + if err != nil { + return fmt.Errorf("--%s 须为 true 或 false: %w", pair.flag, err) + } + input[pair.key] = v + } + } + + // 可选 string + for _, pair := range []struct { + flag string + key string + }{ + {"staff-id", "staffId"}, + {"name", "name"}, + {"mobile", "mobile"}, + {"job-number", "jobNumber"}, + {"emp-type", "empType"}, + {"login-id-type", "loginIdType"}, + {"order-field", "orderField"}, + {"ordering", "ordering"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + input[pair.key] = strings.TrimSpace(v) + } + } + + // 可选 []string: --staff-ids + if raw, _ := cmd.Flags().GetString("staff-ids"); strings.TrimSpace(raw) != "" { + var staffIDs []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIDs = append(staffIDs, part) + } + } + if len(staffIDs) > 0 { + input["staffIds"] = staffIDs + } + } + + return callMCPToolOnServer("college-contact", "send_active_sms", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(sendActiveSmsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "send_active_sms", + CanonicalPath: "college-contact.send_active_sms", + CLIPath: "college-contact employee send-active-sms", + PrimaryCLIPath: "college-contact employee send-active-sms", + }, + Description: "发送激活短信", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "send_active_sms"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "发送激活短信", + UseWhen: []string{"需要发送激活短信时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact employee send-active-sms --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "job-number", Property: "input.jobNumber", Required: boolPtr(false)}, + {Name: "emp-type", Property: "input.empType", Required: boolPtr(false)}, + {Name: "main-dept-id", Property: "input.mainDeptId", Required: boolPtr(false)}, + {Name: "login-id-type", Property: "input.loginIdType", Required: boolPtr(false)}, + {Name: "exclusive-account", Property: "input.exclusiveAccount", Required: boolPtr(false)}, + {Name: "send-active-sms", Property: "input.sendActiveSms", Required: boolPtr(false)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(false)}, + {Name: "target-dept-id", Property: "input.targetDeptId", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(false)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(false)}, + }, + }, + }) + + sendActiveSmsCmd.Flags().String("dept-id", "", "部门 ID(必填)") + sendActiveSmsCmd.Flags().String("staff-id", "", "员工 staffId") + sendActiveSmsCmd.Flags().String("name", "", "员工姓名") + sendActiveSmsCmd.Flags().String("mobile", "", "手机号") + sendActiveSmsCmd.Flags().String("job-number", "", "工号") + sendActiveSmsCmd.Flags().String("emp-type", "", "员工类型(college_student / college_teacher)") + sendActiveSmsCmd.Flags().String("main-dept-id", "", "主部门 ID") + sendActiveSmsCmd.Flags().String("login-id-type", "", "登录类型") + sendActiveSmsCmd.Flags().String("exclusive-account", "", "是否独占账号(true/false)") + sendActiveSmsCmd.Flags().String("send-active-sms", "", "是否发送激活短信(true/false)") + sendActiveSmsCmd.Flags().String("staff-ids", "", "批量操作的 staffId 列表,逗号分隔") + sendActiveSmsCmd.Flags().String("target-dept-id", "", "变更目标部门 ID") + sendActiveSmsCmd.Flags().String("offset", "", "分页偏移量") + sendActiveSmsCmd.Flags().String("size", "", "分页大小") + sendActiveSmsCmd.Flags().String("order-field", "", "排序字段") + sendActiveSmsCmd.Flags().String("ordering", "", "排序顺序(asc / desc)") + + // ════════════════════════════════════════════════════════════ + // alumni 子命令组 — 校友管理 + // ════════════════════════════════════════════════════════════ + + alumniCmd := &cobra.Command{Use: "alumni", Short: "校友管理", RunE: groupRunE} + + getAlumniDeptTreeCmd := &cobra.Command{ + Use: "get-dept-tree", + Short: "查询校友部门树", + Long: `查询校友部门树,返回指定校友部门的下级部门列表。 +必填: + --alumni-dept-id 校友部门 ID`, + Example: ` dws college-contact alumni get-dept-tree --alumni-dept-id 12345 + dws college-contact alumni get-dept-tree --alumni-dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + return callMCPToolOnServer("college-contact", "get_alumni_dept_tree", map[string]any{ + "input": map[string]any{"alumniDeptId": alumniDeptId}, + }) + }, + } + + DeclareLeafMetadata(getAlumniDeptTreeCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_alumni_dept_tree", + CanonicalPath: "college-contact.get_alumni_dept_tree", + CLIPath: "college-contact alumni get-dept-tree", + PrimaryCLIPath: "college-contact alumni get-dept-tree", + }, + Description: "查询校友部门树", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_alumni_dept_tree"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询校友部门树", + UseWhen: []string{"需要查询校友部门树时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni get-dept-tree --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + }, + }, + }) + + getAlumniDeptTreeCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + + getAlumniDeptInfoCmd := &cobra.Command{ + Use: "get-info", + Short: "查询校友部门详情", + Long: `查询校友部门详情,返回部门名称、部门人数、负责人列表、是否有下级部门等。 +必填: + --alumni-dept-id 校友部门 ID`, + Example: ` dws college-contact alumni get-info --alumni-dept-id 12345 + dws college-contact alumni get-info --alumni-dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + return callMCPToolOnServer("college-contact", "get_alumni_dept_info", map[string]any{ + "input": map[string]any{"alumniDeptId": alumniDeptId}, + }) + }, + } + + DeclareLeafMetadata(getAlumniDeptInfoCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_alumni_dept_info", + CanonicalPath: "college-contact.get_alumni_dept_info", + CLIPath: "college-contact alumni get-info", + PrimaryCLIPath: "college-contact alumni get-info", + }, + Description: "查询校友部门详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_alumni_dept_info"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询校友部门详情", + UseWhen: []string{"需要查询校友部门详情时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni get-info --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + }, + }, + }) + + getAlumniDeptInfoCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + + listAlumniCmd := &cobra.Command{ + Use: "list", + Short: "查询校友列表", + Long: `查询指定校友部门下的校友列表,返回校友列表。 +必填: + --alumni-dept-id 校友部门 ID + --order-field 排序字段(如 "dept_entry"、"custom") + --ordering 排序顺序("asc" 或 "desc") +可选: + --offset / --size 分页参数`, + Example: ` dws college-contact alumni list --alumni-dept-id 12345 --order-field dept_entry --ordering asc + dws college-contact alumni list --alumni-dept-id 12345 --order-field dept_entry --ordering asc --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 int64 + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + input["alumniDeptId"] = alumniDeptId + + // 必填 string + for _, flag := range []string{"order-field", "ordering"} { + v, _ := cmd.Flags().GetString(flag) + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("--%s 为必填参数", flag) + } + key := flag + if flag == "order-field" { + key = "orderField" + } + input[key] = v + } + + // 可选 int64 + for _, pair := range []struct { + flag string + key string + }{ + {"offset", "offset"}, + {"size", "size"}, + } { + if v, _ := cmd.Flags().GetString(pair.flag); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", pair.flag, err) + } + input[pair.key] = n + } + } + + return callMCPToolOnServer("college-contact", "list_alumni", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(listAlumniCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "list_alumni", + CanonicalPath: "college-contact.list_alumni", + CLIPath: "college-contact alumni list", + PrimaryCLIPath: "college-contact alumni list", + }, + Description: "查询校友列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "list_alumni"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询校友列表", + UseWhen: []string{"需要查询校友列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni list --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + {Name: "order-field", Property: "input.orderField", Required: boolPtr(true)}, + {Name: "ordering", Property: "input.ordering", Required: boolPtr(true)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + listAlumniCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + listAlumniCmd.Flags().String("order-field", "", "排序字段(必填,如 dept_entry / custom)") + listAlumniCmd.Flags().String("ordering", "", "排序顺序(必填,asc / desc)") + listAlumniCmd.Flags().String("offset", "", "分页偏移量") + listAlumniCmd.Flags().String("size", "", "分页大小") + + queryAlumnusCmd := &cobra.Command{ + Use: "query", + Short: "查询校友详情", + Long: `查询校友详情,返回校友基本信息、入学/毕业时间、工作单位、部门列表等。 +必填: + --staff-id 校友 staffId`, + Example: ` dws college-contact alumni query --staff-id S12345 + dws college-contact alumni query --staff-id S12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + staffId, _ := cmd.Flags().GetString("staff-id") + staffId = strings.TrimSpace(staffId) + if staffId == "" { + return fmt.Errorf("--staff-id 为必填参数") + } + return callMCPToolOnServer("college-contact", "query_alumnus", map[string]any{ + "input": map[string]any{"staffId": staffId}, + }) + }, + } + + DeclareLeafMetadata(queryAlumnusCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_alumnus", + CanonicalPath: "college-contact.query_alumnus", + CLIPath: "college-contact alumni query", + PrimaryCLIPath: "college-contact alumni query", + }, + Description: "查询校友详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_alumnus"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询校友详情", + UseWhen: []string{"需要查询校友详情时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni query --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(true)}, + }, + }, + }) + + queryAlumnusCmd.Flags().String("staff-id", "", "校友 staffId(必填)") + + // ── search ──────────────────────────────────────────────── + searchAlumniCmd := &cobra.Command{ + Use: "search", + Short: "搜索校友", + Long: `按关键词搜索校友,返回匹配校友列表。 + +参数说明: + --keyword 搜索关键词(必填) + --offset 分页偏移量(可选) + --size 分页大小(可选)`, + Example: ` dws college-contact alumni search --keyword 张三 + dws college-contact alumni search --keyword 计算机 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 string - keyword + keyword, _ := cmd.Flags().GetString("keyword") + keyword = strings.TrimSpace(keyword) + if keyword == "" { + return fmt.Errorf("--keyword 为必填参数") + } + input["keyword"] = keyword + + // 可选 int64 - offset, size + for _, p := range []struct{ flag, key string }{ + {"offset", "offset"}, {"size", "size"}, + } { + if v, _ := cmd.Flags().GetString(p.flag); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", p.flag, err) + } + input[p.key] = n + } + } + + return callMCPToolOnServer("college-contact", "search_alumni", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(searchAlumniCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "search_alumni", + CanonicalPath: "college-contact.search_alumni", + CLIPath: "college-contact alumni search", + PrimaryCLIPath: "college-contact alumni search", + }, + Description: "搜索校友", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "search_alumni"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "搜索校友", + UseWhen: []string{"需要搜索校友时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni search --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "keyword", Property: "input.keyword", Required: boolPtr(true)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + searchAlumniCmd.Flags().String("keyword", "", "搜索关键词(必填)") + searchAlumniCmd.Flags().String("offset", "", "分页偏移量(可选)") + searchAlumniCmd.Flags().String("size", "", "分页大小(可选)") + + // ── list-unaccepted ─────────────────────────────────────── + listUnacceptedAlumnusCmd := &cobra.Command{ + Use: "list-unaccepted", + Short: "查询未接受邀请的校友列表", + Long: `查询指定校友部门下未接受邀请的校友列表,返回校友列表及总数。 + +参数说明: + --alumni-dept-id 校友部门 ID(必填) + --offset 分页偏移量(可选) + --size 分页大小(可选)`, + Example: ` dws college-contact alumni list-unaccepted --alumni-dept-id 12345 + dws college-contact alumni list-unaccepted --alumni-dept-id 12345 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 int64 - alumniDeptId + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + input["alumniDeptId"] = alumniDeptId + + // 可选 int64 - offset, size + for _, p := range []struct{ flag, key string }{ + {"offset", "offset"}, {"size", "size"}, + } { + if v, _ := cmd.Flags().GetString(p.flag); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", p.flag, err) + } + input[p.key] = n + } + } + + return callMCPToolOnServer("college-contact", "query_unaccepted_alumnus", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(listUnacceptedAlumnusCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_unaccepted_alumnus", + CanonicalPath: "college-contact.query_unaccepted_alumnus", + CLIPath: "college-contact alumni list-unaccepted", + PrimaryCLIPath: "college-contact alumni list-unaccepted", + }, + Description: "查询未接受邀请的校友列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_unaccepted_alumnus"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询未接受邀请的校友列表", + UseWhen: []string{"需要查询未接受邀请的校友列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni list-unaccepted --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + listUnacceptedAlumnusCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + listUnacceptedAlumnusCmd.Flags().String("offset", "", "分页偏移量(可选)") + listUnacceptedAlumnusCmd.Flags().String("size", "", "分页大小(可选)") + + // ── get-group ───────────────────────────────────────────── + getAlumniGroupCmd := &cobra.Command{ + Use: "get-group", + Short: "查询校友群", + Long: `查询指定校友部门的校友群信息,返回群名称、群 CID、群主信息等。 + +必填: + --alumni-dept-id 校友部门 ID`, + Example: ` dws college-contact alumni get-group --alumni-dept-id 12345 + dws college-contact alumni get-group --alumni-dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + return callMCPToolOnServer("college-contact", "get_alumni_group", map[string]any{ + "input": map[string]any{"alumniDeptId": alumniDeptId}, + }) + }, + } + + DeclareLeafMetadata(getAlumniGroupCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_alumni_group", + CanonicalPath: "college-contact.get_alumni_group", + CLIPath: "college-contact alumni get-group", + PrimaryCLIPath: "college-contact alumni get-group", + }, + Description: "查询校友群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_alumni_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询校友群", + UseWhen: []string{"需要查询校友群时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni get-group --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + }, + }, + }) + + getAlumniGroupCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + + // ── create-dept ─────────────────────────────────────────── + createAlumniDeptsCmd := &cobra.Command{ + Use: "create-dept", + Short: "创建校友子部门", + Long: `在指定校友部门下创建子部门,返回创建的部门列表。 + +必填: + --alumni-dept-id 父校友部门 ID + --dept-name 新部门名称`, + Example: ` dws college-contact alumni create-dept --alumni-dept-id 12345 --dept-name 2020级校友 + dws college-contact alumni create-dept --alumni-dept-id 12345 --dept-name 2020级校友 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + deptName, _ := cmd.Flags().GetString("dept-name") + deptName = strings.TrimSpace(deptName) + if deptName == "" { + return fmt.Errorf("--dept-name 为必填参数") + } + return callMCPToolOnServer("college-contact", "create_alumni_depts", map[string]any{ + "input": map[string]any{"alumniDeptId": alumniDeptId, "deptName": deptName}, + }) + }, + } + + DeclareLeafMetadata(createAlumniDeptsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "create_alumni_depts", + CanonicalPath: "college-contact.create_alumni_depts", + CLIPath: "college-contact alumni create-dept", + PrimaryCLIPath: "college-contact alumni create-dept", + }, + Description: "创建校友子部门", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "create_alumni_depts"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "创建校友子部门", + UseWhen: []string{"需要创建校友子部门时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni create-dept --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + {Name: "dept-name", Property: "input.deptName", Required: boolPtr(true)}, + }, + }, + }) + + createAlumniDeptsCmd.Flags().String("alumni-dept-id", "", "父校友部门 ID(必填)") + createAlumniDeptsCmd.Flags().String("dept-name", "", "新部门名称(必填)") + + // ── update-dept ─────────────────────────────────────────── + updateAlumniDeptCmd := &cobra.Command{ + Use: "update-dept", + Short: "更新校友部门", + Long: `更新校友部门名称。 + +必填: + --alumni-dept-id 校友部门 ID + --dept-name 新部门名称`, + Example: ` dws college-contact alumni update-dept --alumni-dept-id 12345 --dept-name 2021级校友 + dws college-contact alumni update-dept --alumni-dept-id 12345 --dept-name 2021级校友 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + deptName, _ := cmd.Flags().GetString("dept-name") + deptName = strings.TrimSpace(deptName) + if deptName == "" { + return fmt.Errorf("--dept-name 为必填参数") + } + return callMCPToolOnServer("college-contact", "update_alumni_dept", map[string]any{ + "input": map[string]any{"alumniDeptId": alumniDeptId, "deptName": deptName}, + }) + }, + } + + DeclareLeafMetadata(updateAlumniDeptCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "update_alumni_dept", + CanonicalPath: "college-contact.update_alumni_dept", + CLIPath: "college-contact alumni update-dept", + PrimaryCLIPath: "college-contact alumni update-dept", + }, + Description: "更新校友部门", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "update_alumni_dept"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "更新校友部门", + UseWhen: []string{"需要更新校友部门时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni update-dept --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + {Name: "dept-name", Property: "input.deptName", Required: boolPtr(true)}, + }, + }, + }) + + updateAlumniDeptCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + updateAlumniDeptCmd.Flags().String("dept-name", "", "新部门名称(必填)") + + // ── delete-dept ─────────────────────────────────────────── + deleteAlumniDeptCmd := &cobra.Command{ + Use: "delete-dept", + Short: "删除校友部门", + Long: `删除指定校友部门。注意:删除操作不可逆。 + +必填: + --alumni-dept-id 校友部门 ID + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact alumni delete-dept --alumni-dept-id 12345 --dry-run + dws college-contact alumni delete-dept --alumni-dept-id 12345 -y + dws college-contact alumni delete-dept --alumni-dept-id 12345 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + if err := collegeContactConfirmDestructive(cmd, "删除校友部门"); err != nil { + return err + } + return callMCPToolOnServer("college-contact", "delete_alumni_dept", map[string]any{ + "input": map[string]any{"alumniDeptId": alumniDeptId}, + }) + }, + } + + DeclareLeafMetadata(deleteAlumniDeptCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "delete_alumni_dept", + CanonicalPath: "college-contact.delete_alumni_dept", + CLIPath: "college-contact alumni delete-dept", + PrimaryCLIPath: "college-contact alumni delete-dept", + }, + Description: "删除校友部门", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "delete_alumni_dept"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "删除校友部门", + UseWhen: []string{"需要删除校友部门时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni delete-dept --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + }, + }, + }) + + deleteAlumniDeptCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + + // ── update-managers ─────────────────────────────────────── + updateAlumniDeptManagersCmd := &cobra.Command{ + Use: "update-managers", + Short: "设置校友部门负责人", + Long: `设置指定校友部门的负责人(管理员)列表。 +必填: + --alumni-dept-id 校友部门 ID + --admin-user-ids 管理员 userId 列表,逗号分隔`, + Example: ` dws college-contact alumni update-managers --alumni-dept-id 12345 --admin-user-ids user001,user002 + dws college-contact alumni update-managers --alumni-dept-id 12345 --admin-user-ids user001 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + + raw, _ := cmd.Flags().GetString("admin-user-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--admin-user-ids 为必填参数") + } + var adminUserIds []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + adminUserIds = append(adminUserIds, part) + } + } + if len(adminUserIds) == 0 { + return fmt.Errorf("--admin-user-ids 不能为空列表") + } + + return callMCPToolOnServer("college-contact", "update_alumni_dept_managers", map[string]any{ + "input": map[string]any{ + "alumniDeptId": alumniDeptId, + "adminUserIds": adminUserIds, + }, + }) + }, + } + + DeclareLeafMetadata(updateAlumniDeptManagersCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "update_alumni_dept_managers", + CanonicalPath: "college-contact.update_alumni_dept_managers", + CLIPath: "college-contact alumni update-managers", + PrimaryCLIPath: "college-contact alumni update-managers", + }, + Description: "设置校友部门负责人", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "update_alumni_dept_managers"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "设置校友部门负责人", + UseWhen: []string{"需要设置校友部门负责人时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni update-managers --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + {Name: "admin-user-ids", Property: "input.adminUserIds", Required: boolPtr(true)}, + }, + }, + }) + + updateAlumniDeptManagersCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + updateAlumniDeptManagersCmd.Flags().String("admin-user-ids", "", "管理员 userId 列表,逗号分隔(必填)") + + // ── add-alumnus ─────────────────────────────────────────── + addAlumnusCmd := &cobra.Command{ + Use: "add-alumnus", + Short: "添加校友", + Long: `添加校友到指定校友部门,返回添加是否成功。 +必填: + --name 校友姓名 + --mobile 手机号 + --dept-ids 校友部门 ID 列表,逗号分隔 +可选: + --student-number 学号 + --email 邮箱 + --intake 入学年份 + --outtake 毕业年份`, + Example: ` dws college-contact alumni add-alumnus --name 张三 --mobile 13800138000 --dept-ids 12345,67890 + dws college-contact alumni add-alumnus --name 张三 --mobile 13800138000 --dept-ids 12345 --student-number 2020001 --intake 2020 --outtake 2024 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 string + for _, p := range []struct{ flag, key string }{ + {"name", "name"}, {"mobile", "mobile"}, + } { + v, _ := cmd.Flags().GetString(p.flag) + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("--%s 为必填参数", p.flag) + } + input[p.key] = v + } + + // 必填 []int64: --dept-ids + raw, _ := cmd.Flags().GetString("dept-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--dept-ids 为必填参数") + } + var deptIds []int64 + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + n, err := strconv.ParseInt(part, 10, 64) + if err != nil { + return fmt.Errorf("--dept-ids 元素须为整数: %w", err) + } + deptIds = append(deptIds, n) + } + if len(deptIds) == 0 { + return fmt.Errorf("--dept-ids 不能为空列表") + } + input["deptIds"] = deptIds + + // 可选 string + for _, p := range []struct{ flag, key string }{ + {"student-number", "studentNumber"}, + {"email", "email"}, + {"intake", "intake"}, + {"outtake", "outtake"}, + } { + if v, _ := cmd.Flags().GetString(p.flag); strings.TrimSpace(v) != "" { + input[p.key] = strings.TrimSpace(v) + } + } + + return callMCPToolOnServer("college-contact", "add_alumnus", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(addAlumnusCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "add_alumnus", + CanonicalPath: "college-contact.add_alumnus", + CLIPath: "college-contact alumni add-alumnus", + PrimaryCLIPath: "college-contact alumni add-alumnus", + }, + Description: "添加校友", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "add_alumnus"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "添加校友", + UseWhen: []string{"需要添加校友时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni add-alumnus --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(false)}, + {Name: "dept-ids", Property: "input.deptIds", Required: boolPtr(true)}, + {Name: "student-number", Property: "input.studentNumber", Required: boolPtr(false)}, + {Name: "email", Property: "input.email", Required: boolPtr(false)}, + {Name: "intake", Property: "input.intake", Required: boolPtr(false)}, + {Name: "outtake", Property: "input.outtake", Required: boolPtr(false)}, + }, + }, + }) + + addAlumnusCmd.Flags().String("name", "", "校友姓名(必填)") + addAlumnusCmd.Flags().String("mobile", "", "手机号(必填)") + addAlumnusCmd.Flags().String("dept-ids", "", "校友部门 ID 列表,逗号分隔(必填)") + addAlumnusCmd.Flags().String("student-number", "", "学号") + addAlumnusCmd.Flags().String("email", "", "邮箱") + addAlumnusCmd.Flags().String("intake", "", "入学年份") + addAlumnusCmd.Flags().String("outtake", "", "毕业年份") + + // ── update-alumnus ──────────────────────────────────────── + updateAlumnusCmd := &cobra.Command{ + Use: "update-alumnus", + Short: "更新校友信息", + Long: `更新校友信息,返回更新是否成功。 +必填: + --staff-id 校友 staffId + --name 校友姓名 + --dept-ids 校友部门 ID 列表,逗号分隔 +可选: + --student-number 学号 + --email 邮箱 + --intake 入学年份 + --outtake 毕业年份`, + Example: ` dws college-contact alumni update-alumnus --staff-id staff001 --name 张三 --dept-ids 12345,67890 + dws college-contact alumni update-alumnus --staff-id staff001 --name 张三 --dept-ids 12345 --student-number 2020001 --intake 2020 --outtake 2024 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 string + for _, p := range []struct{ flag, key string }{ + {"staff-id", "staffId"}, {"name", "name"}, + } { + v, _ := cmd.Flags().GetString(p.flag) + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("--%s 为必填参数", p.flag) + } + input[p.key] = v + } + + // 必填 []int64: --dept-ids + raw, _ := cmd.Flags().GetString("dept-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--dept-ids 为必填参数") + } + var deptIds []int64 + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + n, err := strconv.ParseInt(part, 10, 64) + if err != nil { + return fmt.Errorf("--dept-ids 元素须为整数: %w", err) + } + deptIds = append(deptIds, n) + } + if len(deptIds) == 0 { + return fmt.Errorf("--dept-ids 不能为空列表") + } + input["deptIds"] = deptIds + + // 可选 string + for _, p := range []struct{ flag, key string }{ + {"student-number", "studentNumber"}, + {"email", "email"}, + {"intake", "intake"}, + {"outtake", "outtake"}, + } { + if v, _ := cmd.Flags().GetString(p.flag); strings.TrimSpace(v) != "" { + input[p.key] = strings.TrimSpace(v) + } + } + + return callMCPToolOnServer("college-contact", "update_alumnus", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(updateAlumnusCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "update_alumnus", + CanonicalPath: "college-contact.update_alumnus", + CLIPath: "college-contact alumni update-alumnus", + PrimaryCLIPath: "college-contact alumni update-alumnus", + }, + Description: "更新校友信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "update_alumnus"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "更新校友信息", + UseWhen: []string{"需要更新校友信息时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni update-alumnus --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(false)}, + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "dept-ids", Property: "input.deptIds", Required: boolPtr(true)}, + {Name: "student-number", Property: "input.studentNumber", Required: boolPtr(false)}, + {Name: "email", Property: "input.email", Required: boolPtr(false)}, + {Name: "intake", Property: "input.intake", Required: boolPtr(false)}, + {Name: "outtake", Property: "input.outtake", Required: boolPtr(false)}, + }, + }, + }) + + updateAlumnusCmd.Flags().String("staff-id", "", "校友 staffId(必填)") + updateAlumnusCmd.Flags().String("name", "", "校友姓名(必填)") + updateAlumnusCmd.Flags().String("dept-ids", "", "校友部门 ID 列表,逗号分隔(必填)") + updateAlumnusCmd.Flags().String("student-number", "", "学号") + updateAlumnusCmd.Flags().String("email", "", "邮箱") + updateAlumnusCmd.Flags().String("intake", "", "入学年份") + updateAlumnusCmd.Flags().String("outtake", "", "毕业年份") + + // ── remove-alumnus ──────────────────────────────────────── + removeAlumnusCmd := &cobra.Command{ + Use: "remove-alumnus", + Short: "删除校友", + Long: `从校友部门中删除指定校友。注意:删除操作不可逆。 + +必填: + --staff-id 校友 staffId + --alumni-dept-id 校友部门 ID + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact alumni remove-alumnus --staff-id staff001 --alumni-dept-id 12345 --dry-run + dws college-contact alumni remove-alumnus --staff-id staff001 --alumni-dept-id 12345 -y + dws college-contact alumni remove-alumnus --staff-id staff001 --alumni-dept-id 12345 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + staffId, _ := cmd.Flags().GetString("staff-id") + staffId = strings.TrimSpace(staffId) + if staffId == "" { + return fmt.Errorf("--staff-id 为必填参数") + } + + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + + if err := collegeContactConfirmDestructive(cmd, "删除校友"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "delete_alumnus", map[string]any{ + "input": map[string]any{ + "staffId": staffId, + "alumniDeptId": alumniDeptId, + }, + }) + }, + } + + DeclareLeafMetadata(removeAlumnusCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "delete_alumnus", + CanonicalPath: "college-contact.delete_alumnus", + CLIPath: "college-contact alumni remove-alumnus", + PrimaryCLIPath: "college-contact alumni remove-alumnus", + }, + Description: "删除校友", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "delete_alumnus"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "删除校友", + UseWhen: []string{"需要删除校友时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni remove-alumnus --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "staff-id", Property: "input.staffId", Required: boolPtr(true)}, + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + }, + }, + }) + + removeAlumnusCmd.Flags().String("staff-id", "", "校友 staffId(必填)") + removeAlumnusCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + + // ── cancel-invite ───────────────────────────────────────── + cancelAlumniInviteCmd := &cobra.Command{ + Use: "cancel-invite", + Short: "取消校友邀请", + Long: `取消校友邀请记录,返回操作是否成功。注意:取消操作不可逆。 + +必填: + --alumni-dept-id 校友部门 ID + --staff-ids staffId 列表(作为 inviteId 使用),逗号分隔 + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact alumni cancel-invite --alumni-dept-id 12345 --staff-ids staff001,staff002 --dry-run + dws college-contact alumni cancel-invite --alumni-dept-id 12345 --staff-ids staff001 -y + dws college-contact alumni cancel-invite --alumni-dept-id 12345 --staff-ids staff001 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + + raw, _ := cmd.Flags().GetString("staff-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--staff-ids 为必填参数") + } + var staffIds []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIds = append(staffIds, part) + } + } + if len(staffIds) == 0 { + return fmt.Errorf("--staff-ids 不能为空列表") + } + + if err := collegeContactConfirmDestructive(cmd, "取消校友邀请"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "delete_alumni_invite_record", map[string]any{ + "input": map[string]any{ + "alumniDeptId": alumniDeptId, + "staffIds": staffIds, + }, + }) + }, + } + + DeclareLeafMetadata(cancelAlumniInviteCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "delete_alumni_invite_record", + CanonicalPath: "college-contact.delete_alumni_invite_record", + CLIPath: "college-contact alumni cancel-invite", + PrimaryCLIPath: "college-contact alumni cancel-invite", + }, + Description: "取消校友邀请", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "delete_alumni_invite_record"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "取消校友邀请", + UseWhen: []string{"需要取消校友邀请时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni cancel-invite --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(true)}, + }, + }, + }) + + cancelAlumniInviteCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + cancelAlumniInviteCmd.Flags().String("staff-ids", "", "staffId 列表(作为 inviteId 使用),逗号分隔(必填)") + + // ── create-group ────────────────────────────────────────── + createAlumniGroupCmd := &cobra.Command{ + Use: "create-group", + Short: "创建校友群", + Long: `为指定校友部门创建校友群,返回群信息(corpId/deptId/cid/name/owner)。 + +必填: + --alumni-dept-id 校友部门 ID`, + Example: ` dws college-contact alumni create-group --alumni-dept-id 12345 --dry-run + dws college-contact alumni create-group --alumni-dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + + return callMCPToolOnServer("college-contact", "create_alumni_group", map[string]any{ + "input": map[string]any{ + "alumniDeptId": alumniDeptId, + }, + }) + }, + } + + DeclareLeafMetadata(createAlumniGroupCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "create_alumni_group", + CanonicalPath: "college-contact.create_alumni_group", + CLIPath: "college-contact alumni create-group", + PrimaryCLIPath: "college-contact alumni create-group", + }, + Description: "创建校友群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "create_alumni_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "创建校友群", + UseWhen: []string{"需要创建校友群时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni create-group --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + }, + }, + }) + + createAlumniGroupCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + + // ── disband-group ───────────────────────────────────────── + disbandAlumniGroupCmd := &cobra.Command{ + Use: "disband-group", + Short: "解散校友群", + Long: `解散指定校友部门的校友群。注意:解散操作不可逆。 + +必填: + --alumni-dept-id 校友部门 ID + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact alumni disband-group --alumni-dept-id 12345 --dry-run + dws college-contact alumni disband-group --alumni-dept-id 12345 -y + dws college-contact alumni disband-group --alumni-dept-id 12345 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + alumniDeptIdRaw, _ := cmd.Flags().GetString("alumni-dept-id") + alumniDeptIdRaw = strings.TrimSpace(alumniDeptIdRaw) + if alumniDeptIdRaw == "" { + return fmt.Errorf("--alumni-dept-id 为必填参数") + } + alumniDeptId, err := strconv.ParseInt(alumniDeptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--alumni-dept-id 须为整数: %w", err) + } + + if err := collegeContactConfirmDestructive(cmd, "解散校友群"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "disband_alumni_group", map[string]any{ + "input": map[string]any{ + "alumniDeptId": alumniDeptId, + }, + }) + }, + } + + DeclareLeafMetadata(disbandAlumniGroupCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "disband_alumni_group", + CanonicalPath: "college-contact.disband_alumni_group", + CLIPath: "college-contact alumni disband-group", + PrimaryCLIPath: "college-contact alumni disband-group", + }, + Description: "解散校友群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "disband_alumni_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "解散校友群", + UseWhen: []string{"需要解散校友群时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni disband-group --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "alumni-dept-id", Property: "input.alumniDeptId", Required: boolPtr(true)}, + }, + }, + }) + + disbandAlumniGroupCmd.Flags().String("alumni-dept-id", "", "校友部门 ID(必填)") + + // ── get-alumni-org-from-graduate ─────────────────────────── + getAlumniOrgFromGraduateCmd := &cobra.Command{ + Use: "get-alumni-org-from-graduate", + Short: "查询毕业生校友组织", + Long: `查询毕业生校友组织信息,无入参,直接返回毕业生校友组织信息。`, + Example: ` dws college-contact alumni get-alumni-org-from-graduate --dry-run + dws college-contact alumni get-alumni-org-from-graduate -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("college-contact", "get_alumni_org_from_graduate", map[string]any{ + "input": map[string]any{}, + }) + }, + } + + DeclareLeafMetadata(getAlumniOrgFromGraduateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_alumni_org_from_graduate", + CanonicalPath: "college-contact.get_alumni_org_from_graduate", + CLIPath: "college-contact alumni get-alumni-org-from-graduate", + PrimaryCLIPath: "college-contact alumni get-alumni-org-from-graduate", + }, + Description: "查询毕业生校友组织", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_alumni_org_from_graduate"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询毕业生校友组织", + UseWhen: []string{"需要查询毕业生校友组织时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni get-alumni-org-from-graduate --help"}, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + // ── create-alumni-org ────────────────────────────────────── + createAlumniOrgCmd := &cobra.Command{ + Use: "create-alumni-org", + Short: "创建校友会组织", + Long: `创建校友会组织,返回新组织信息。 + +必填: + --org-name 校友会组织名称`, + Example: ` dws college-contact alumni create-alumni-org --org-name 某大学校友会 --dry-run + dws college-contact alumni create-alumni-org --org-name 某大学校友会 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + orgName, _ := cmd.Flags().GetString("org-name") + orgName = strings.TrimSpace(orgName) + if orgName == "" { + return fmt.Errorf("--org-name 为必填参数") + } + + return callMCPToolOnServer("college-contact", "create_alumni_org", map[string]any{ + "input": map[string]any{ + "orgName": orgName, + }, + }) + }, + } + + DeclareLeafMetadata(createAlumniOrgCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "create_alumni_org", + CanonicalPath: "college-contact.create_alumni_org", + CLIPath: "college-contact alumni create-alumni-org", + PrimaryCLIPath: "college-contact alumni create-alumni-org", + }, + Description: "创建校友会组织", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "create_alumni_org"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "创建校友会组织", + UseWhen: []string{"需要创建校友会组织时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni create-alumni-org --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "org-name", Property: "input.orgName", Required: boolPtr(true)}, + }, + }, + }) + + createAlumniOrgCmd.Flags().String("org-name", "", "校友会组织名称(必填)") + + // ── add-alumni-org-main-admins ──────────────────────────── + addAlumniOrgMainAdminsCmd := &cobra.Command{ + Use: "add-alumni-org-main-admins", + Short: "添加校友会组织管理员", + Long: `为校友会组织添加管理员(主管理员)。 + +必填: + --admin-user-ids 管理员 userId 列表,逗号分隔`, + Example: ` dws college-contact alumni add-alumni-org-main-admins --admin-user-ids user001,user002 --dry-run + dws college-contact alumni add-alumni-org-main-admins --admin-user-ids user001 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + raw, _ := cmd.Flags().GetString("admin-user-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--admin-user-ids 为必填参数") + } + var adminUserIds []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + adminUserIds = append(adminUserIds, part) + } + } + if len(adminUserIds) == 0 { + return fmt.Errorf("--admin-user-ids 不能为空列表") + } + + return callMCPToolOnServer("college-contact", "add_alumni_org_main_admins", map[string]any{ + "input": map[string]any{ + "adminUserIds": adminUserIds, + }, + }) + }, + } + + DeclareLeafMetadata(addAlumniOrgMainAdminsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "add_alumni_org_main_admins", + CanonicalPath: "college-contact.add_alumni_org_main_admins", + CLIPath: "college-contact alumni add-alumni-org-main-admins", + PrimaryCLIPath: "college-contact alumni add-alumni-org-main-admins", + }, + Description: "添加校友会组织管理员", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "add_alumni_org_main_admins"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "添加校友会组织管理员", + UseWhen: []string{"需要添加校友会组织管理员时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact alumni add-alumni-org-main-admins --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "admin-user-ids", Property: "input.adminUserIds", Required: boolPtr(true)}, + }, + }, + }) + + addAlumniOrgMainAdminsCmd.Flags().String("admin-user-ids", "", "管理员 userId 列表,逗号分隔(必填)") + + // ════════════════════════════════════════════════════════════ + // graduate 子命令组 — 毕业年级管理 + // ════════════════════════════════════════════════════════════ + + graduateCmd := &cobra.Command{Use: "graduate", Short: "毕业年级管理", RunE: groupRunE} + + // ── query-graduate-years ────────────────────────────────── + queryGraduateYearsCmd := &cobra.Command{ + Use: "query-graduate-years", + Short: "查询毕业年级列表", + Long: `查询毕业年级列表,无入参,直接返回所有毕业年级。`, + Example: ` dws college-contact graduate query-graduate-years --dry-run + dws college-contact graduate query-graduate-years -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("college-contact", "query_graduate_years", map[string]any{ + "input": map[string]any{}, + }) + }, + } + + DeclareLeafMetadata(queryGraduateYearsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_graduate_years", + CanonicalPath: "college-contact.query_graduate_years", + CLIPath: "college-contact graduate query-graduate-years", + PrimaryCLIPath: "college-contact graduate query-graduate-years", + }, + Description: "查询毕业年级列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_graduate_years"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询毕业年级列表", + UseWhen: []string{"需要查询毕业年级列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate query-graduate-years --help"}, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + // ── query-graduate-depts ────────────────────────────────── + queryGraduateDeptsCmd := &cobra.Command{ + Use: "query-graduate-depts", + Short: "查询待毕业部门列表", + Long: `查询指定部门下的待毕业部门列表。 + +必填: + --dept-id 部门 ID + +可选: + --graduate-year 毕业年份`, + Example: ` dws college-contact graduate query-graduate-depts --dept-id 12345 --dry-run + dws college-contact graduate query-graduate-depts --dept-id 12345 --graduate-year 2026 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIdRaw, _ := cmd.Flags().GetString("dept-id") + deptIdRaw = strings.TrimSpace(deptIdRaw) + if deptIdRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptId, err := strconv.ParseInt(deptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + + input := map[string]any{"deptId": deptId} + if v, _ := cmd.Flags().GetString("graduate-year"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--graduate-year 须为整数: %w", err) + } + input["graduateYear"] = n + } + + return callMCPToolOnServer("college-contact", "query_graduate_depts", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(queryGraduateDeptsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_graduate_depts", + CanonicalPath: "college-contact.query_graduate_depts", + CLIPath: "college-contact graduate query-graduate-depts", + PrimaryCLIPath: "college-contact graduate query-graduate-depts", + }, + Description: "查询待毕业部门列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_graduate_depts"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询待毕业部门列表", + UseWhen: []string{"需要查询待毕业部门列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate query-graduate-depts --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "graduate-year", Property: "input.graduateYear", Required: boolPtr(false)}, + }, + }, + }) + + queryGraduateDeptsCmd.Flags().String("dept-id", "", "部门 ID(必填)") + queryGraduateDeptsCmd.Flags().String("graduate-year", "", "毕业年份") + + // ── query-graduate-sub-depts ────────────────────────────── + queryGraduateSubDeptsCmd := &cobra.Command{ + Use: "query-graduate-sub-depts", + Short: "查询毕业子部门列表", + Long: `查询指定部门的毕业子部门列表。 + +必填: + --dept-id 部门 ID`, + Example: ` dws college-contact graduate query-graduate-sub-depts --dept-id 12345 --dry-run + dws college-contact graduate query-graduate-sub-depts --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIdRaw, _ := cmd.Flags().GetString("dept-id") + deptIdRaw = strings.TrimSpace(deptIdRaw) + if deptIdRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptId, err := strconv.ParseInt(deptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + + return callMCPToolOnServer("college-contact", "query_graduate_sub_depts", map[string]any{ + "input": map[string]any{ + "deptId": deptId, + }, + }) + }, + } + + DeclareLeafMetadata(queryGraduateSubDeptsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_graduate_sub_depts", + CanonicalPath: "college-contact.query_graduate_sub_depts", + CLIPath: "college-contact graduate query-graduate-sub-depts", + PrimaryCLIPath: "college-contact graduate query-graduate-sub-depts", + }, + Description: "查询毕业子部门列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_graduate_sub_depts"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询毕业子部门列表", + UseWhen: []string{"需要查询毕业子部门列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate query-graduate-sub-depts --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + queryGraduateSubDeptsCmd.Flags().String("dept-id", "", "部门 ID(必填)") + + // ── query-page-graduate-users ───────────────────────────── + queryPageGraduateUsersCmd := &cobra.Command{ + Use: "query-page-graduate-users", + Short: "分页查询待毕业学生列表", + Long: `分页查询指定部门下的待毕业学生列表。 + +必填: + --dept-id 部门 ID + +可选: + --graduate-year 毕业年份 + --offset 分页偏移量 + --size 分页大小`, + Example: ` dws college-contact graduate query-page-graduate-users --dept-id 12345 --dry-run + dws college-contact graduate query-page-graduate-users --dept-id 12345 --graduate-year 2026 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIdRaw, _ := cmd.Flags().GetString("dept-id") + deptIdRaw = strings.TrimSpace(deptIdRaw) + if deptIdRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptId, err := strconv.ParseInt(deptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + + input := map[string]any{"deptId": deptId} + for _, opt := range []struct { + flag string + key string + }{ + {"graduate-year", "graduateYear"}, + {"offset", "offset"}, + {"size", "size"}, + } { + if v, _ := cmd.Flags().GetString(opt.flag); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", opt.flag, err) + } + input[opt.key] = n + } + } + + return callMCPToolOnServer("college-contact", "query_page_graduate_users", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(queryPageGraduateUsersCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_page_graduate_users", + CanonicalPath: "college-contact.query_page_graduate_users", + CLIPath: "college-contact graduate query-page-graduate-users", + PrimaryCLIPath: "college-contact graduate query-page-graduate-users", + }, + Description: "分页查询待毕业学生列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_page_graduate_users"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "分页查询待毕业学生列表", + UseWhen: []string{"需要分页查询待毕业学生列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate query-page-graduate-users --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "graduate-year", Property: "input.graduateYear", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + queryPageGraduateUsersCmd.Flags().String("dept-id", "", "部门 ID(必填)") + queryPageGraduateUsersCmd.Flags().String("graduate-year", "", "毕业年份") + queryPageGraduateUsersCmd.Flags().String("offset", "", "分页偏移量") + queryPageGraduateUsersCmd.Flags().String("size", "", "分页大小") + + // ── get-task-result ─────────────────────────────────────── + getTaskResultCmd := &cobra.Command{ + Use: "get-task-result", + Short: "查询异步任务执行结果", + Long: `查询异步任务的执行结果(如毕业、恢复等异步操作)。 + +必填: + --request-no 异步任务请求号 + +可选: + --type 异步任务类型(RESTORE / ADD_GRADUATE / GRADUATE)`, + Example: ` dws college-contact graduate get-task-result --request-no xxx123 --dry-run + dws college-contact graduate get-task-result --request-no xxx123 --type GRADUATE -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + requestNo, _ := cmd.Flags().GetString("request-no") + requestNo = strings.TrimSpace(requestNo) + if requestNo == "" { + return fmt.Errorf("--request-no 为必填参数") + } + + input := map[string]any{"requestNo": requestNo} + if v, _ := cmd.Flags().GetString("type"); strings.TrimSpace(v) != "" { + input["type"] = strings.TrimSpace(v) + } + + return callMCPToolOnServer("college-contact", "get_task_result", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(getTaskResultCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_task_result", + CanonicalPath: "college-contact.get_task_result", + CLIPath: "college-contact graduate get-task-result", + PrimaryCLIPath: "college-contact graduate get-task-result", + }, + Description: "查询异步任务执行结果", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_task_result"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询异步任务执行结果", + UseWhen: []string{"需要查询异步任务执行结果时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate get-task-result --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "request-no", Property: "input.requestNo", Required: boolPtr(true)}, + {Name: "type", Property: "input.type", Required: boolPtr(false)}, + }, + }, + }) + + getTaskResultCmd.Flags().String("request-no", "", "异步任务请求号(必填)") + getTaskResultCmd.Flags().String("type", "", "异步任务类型:RESTORE / ADD_GRADUATE / GRADUATE") + + // ── get-alumni-org ──────────────────────────────────────── + getAlumniOrgCmd := &cobra.Command{ + Use: "get-alumni-org", + Short: "查询校友组织信息", + Long: `查询校友组织信息(组织 ID、名称、主管理员等),无入参,直接返回组织信息。`, + Example: ` dws college-contact graduate get-alumni-org --dry-run + dws college-contact graduate get-alumni-org -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("college-contact", "get_alumni_org", map[string]any{ + "input": map[string]any{}, + }) + }, + } + + DeclareLeafMetadata(getAlumniOrgCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_alumni_org", + CanonicalPath: "college-contact.get_alumni_org", + CLIPath: "college-contact graduate get-alumni-org", + PrimaryCLIPath: "college-contact graduate get-alumni-org", + }, + Description: "查询校友组织信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_alumni_org"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询校友组织信息", + UseWhen: []string{"需要查询校友组织信息时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate get-alumni-org --help"}, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + // ── query-restore-sub-depts ────────────────────────────── + queryRestoreSubDeptsCmd := &cobra.Command{ + Use: "query-restore-sub-depts", + Short: "查询可恢复子部门列表", + Long: `查询指定部门下的可恢复子部门列表。 + +必填: + --dept-id 部门 ID`, + Example: ` dws college-contact graduate query-restore-sub-depts --dept-id 12345 --dry-run + dws college-contact graduate query-restore-sub-depts --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIdRaw, _ := cmd.Flags().GetString("dept-id") + deptIdRaw = strings.TrimSpace(deptIdRaw) + if deptIdRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptId, err := strconv.ParseInt(deptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + + return callMCPToolOnServer("college-contact", "query_restore_sub_depts", map[string]any{ + "input": map[string]any{ + "deptId": deptId, + }, + }) + }, + } + + DeclareLeafMetadata(queryRestoreSubDeptsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_restore_sub_depts", + CanonicalPath: "college-contact.query_restore_sub_depts", + CLIPath: "college-contact graduate query-restore-sub-depts", + PrimaryCLIPath: "college-contact graduate query-restore-sub-depts", + }, + Description: "查询可恢复子部门列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_restore_sub_depts"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询可恢复子部门列表", + UseWhen: []string{"需要查询可恢复子部门列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate query-restore-sub-depts --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + queryRestoreSubDeptsCmd.Flags().String("dept-id", "", "部门 ID(必填)") + + // ── query-dept-deleted-emps ────────────────────────────── + queryDeptDeletedEmpsCmd := &cobra.Command{ + Use: "query-dept-deleted-emps", + Short: "查询部门可恢复员工列表", + Long: `分页查询指定部门下的可恢复员工列表。 + +必填: + --dept-id 部门 ID + +可选: + --offset 分页偏移量 + --size 分页大小`, + Example: ` dws college-contact graduate query-dept-deleted-emps --dept-id 12345 --dry-run + dws college-contact graduate query-dept-deleted-emps --dept-id 12345 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIdRaw, _ := cmd.Flags().GetString("dept-id") + deptIdRaw = strings.TrimSpace(deptIdRaw) + if deptIdRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptId, err := strconv.ParseInt(deptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + + input := map[string]any{"deptId": deptId} + for _, opt := range []struct { + flag string + key string + }{ + {"offset", "offset"}, + {"size", "size"}, + } { + if v, _ := cmd.Flags().GetString(opt.flag); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", opt.flag, err) + } + input[opt.key] = n + } + } + + return callMCPToolOnServer("college-contact", "query_dept_deleted_emps", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(queryDeptDeletedEmpsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_dept_deleted_emps", + CanonicalPath: "college-contact.query_dept_deleted_emps", + CLIPath: "college-contact graduate query-dept-deleted-emps", + PrimaryCLIPath: "college-contact graduate query-dept-deleted-emps", + }, + Description: "查询部门可恢复员工列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_dept_deleted_emps"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询部门可恢复员工列表", + UseWhen: []string{"需要查询部门可恢复员工列表时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate query-dept-deleted-emps --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + queryDeptDeletedEmpsCmd.Flags().String("dept-id", "", "部门 ID(必填)") + queryDeptDeletedEmpsCmd.Flags().String("offset", "", "分页偏移量") + queryDeptDeletedEmpsCmd.Flags().String("size", "", "分页大小") + + // ── search-graduate ─────────────────────────────────────── + searchGraduateCmd := &cobra.Command{ + Use: "search-graduate", + Short: "搜索毕业部门与员工", + Long: `按关键词搜索毕业相关的部门与员工。 + +必填: + --keyword 搜索关键词 + +可选: + --offset 分页偏移量 + --size 分页大小`, + Example: ` dws college-contact graduate search-graduate --keyword 计算机 --dry-run + dws college-contact graduate search-graduate --keyword 计算机 --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + keyword, _ := cmd.Flags().GetString("keyword") + keyword = strings.TrimSpace(keyword) + if keyword == "" { + return fmt.Errorf("--keyword 为必填参数") + } + + input := map[string]any{"keyword": keyword} + for _, opt := range []struct { + flag string + key string + }{ + {"offset", "offset"}, + {"size", "size"}, + } { + if v, _ := cmd.Flags().GetString(opt.flag); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--%s 须为整数: %w", opt.flag, err) + } + input[opt.key] = n + } + } + + return callMCPToolOnServer("college-contact", "search_graduate", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(searchGraduateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "search_graduate", + CanonicalPath: "college-contact.search_graduate", + CLIPath: "college-contact graduate search-graduate", + PrimaryCLIPath: "college-contact graduate search-graduate", + }, + Description: "搜索毕业部门与员工", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "search_graduate"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "搜索毕业部门与员工", + UseWhen: []string{"需要搜索毕业部门与员工时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate search-graduate --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "keyword", Property: "input.keyword", Required: boolPtr(true)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + searchGraduateCmd.Flags().String("keyword", "", "搜索关键词(必填)") + searchGraduateCmd.Flags().String("offset", "", "分页偏移量") + searchGraduateCmd.Flags().String("size", "", "分页大小") + + // ── commit-graduate ─────────────────────────────────────── + commitGraduateCmd := &cobra.Command{ + Use: "commit-graduate", + Short: "提交毕业", + Long: `对指定部门提交毕业操作,发起异步毕业任务,返回异步任务编号。 +注意:提交毕业不可逆。 + +必填: + --graduate-dept-ids 待毕业部门 ID 列表,逗号分隔 + --graduate-year 毕业年份 + +可选: + --request-no 异步任务请求号 + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact graduate commit-graduate --graduate-dept-ids 12345,12346 --graduate-year 2026 --dry-run + dws college-contact graduate commit-graduate --graduate-dept-ids 12345,12346 --graduate-year 2026 -y + dws college-contact graduate commit-graduate --graduate-dept-ids 12345,12346 --graduate-year 2026 --request-no xxx123 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + raw, _ := cmd.Flags().GetString("graduate-dept-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--graduate-dept-ids 为必填参数") + } + var graduateDeptIds []int64 + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + n, err := strconv.ParseInt(part, 10, 64) + if err != nil { + return fmt.Errorf("--graduate-dept-ids 须为逗号分隔的整数列表: %w", err) + } + graduateDeptIds = append(graduateDeptIds, n) + } + if len(graduateDeptIds) == 0 { + return fmt.Errorf("--graduate-dept-ids 不能为空列表") + } + + graduateYearRaw, _ := cmd.Flags().GetString("graduate-year") + graduateYearRaw = strings.TrimSpace(graduateYearRaw) + if graduateYearRaw == "" { + return fmt.Errorf("--graduate-year 为必填参数") + } + graduateYear, err := strconv.ParseInt(graduateYearRaw, 10, 64) + if err != nil { + return fmt.Errorf("--graduate-year 须为整数: %w", err) + } + + input := map[string]any{ + "graduateDeptIds": graduateDeptIds, + "graduateYear": graduateYear, + } + if v, _ := cmd.Flags().GetString("request-no"); strings.TrimSpace(v) != "" { + input["requestNo"] = strings.TrimSpace(v) + } + + if err := collegeContactConfirmDestructive(cmd, "提交毕业"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "commit_graduate", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(commitGraduateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "commit_graduate", + CanonicalPath: "college-contact.commit_graduate", + CLIPath: "college-contact graduate commit-graduate", + PrimaryCLIPath: "college-contact graduate commit-graduate", + }, + Description: "提交毕业", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "commit_graduate"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "提交毕业", + UseWhen: []string{"需要提交毕业时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate commit-graduate --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "graduate-dept-ids", Property: "input.graduateDeptIds", Required: boolPtr(true)}, + {Name: "graduate-year", Property: "input.graduateYear", Required: boolPtr(true)}, + {Name: "request-no", Property: "input.requestNo", Required: boolPtr(false)}, + }, + }, + }) + + commitGraduateCmd.Flags().String("graduate-dept-ids", "", "待毕业部门 ID 列表,逗号分隔(必填)") + commitGraduateCmd.Flags().String("graduate-year", "", "毕业年份(必填)") + commitGraduateCmd.Flags().String("request-no", "", "异步任务请求号") + + // ── all-graduate ────────────────────────────────────────── + allGraduateCmd := &cobra.Command{ + Use: "all-graduate", + Short: "全部毕业", + Long: `对指定毕业年份的全部待毕业部门提交毕业操作。 +注意:全部毕业不可逆。 + +必填: + --graduate-year 毕业年份 + +可选: + --request-no 异步任务请求号 + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact graduate all-graduate --graduate-year 2026 --dry-run + dws college-contact graduate all-graduate --graduate-year 2026 -y + dws college-contact graduate all-graduate --graduate-year 2026 --request-no xxx123 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + graduateYearRaw, _ := cmd.Flags().GetString("graduate-year") + graduateYearRaw = strings.TrimSpace(graduateYearRaw) + if graduateYearRaw == "" { + return fmt.Errorf("--graduate-year 为必填参数") + } + graduateYear, err := strconv.ParseInt(graduateYearRaw, 10, 64) + if err != nil { + return fmt.Errorf("--graduate-year 须为整数: %w", err) + } + + input := map[string]any{"graduateYear": graduateYear} + if v, _ := cmd.Flags().GetString("request-no"); strings.TrimSpace(v) != "" { + input["requestNo"] = strings.TrimSpace(v) + } + + if err := collegeContactConfirmDestructive(cmd, "全部毕业"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "all_graduate", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(allGraduateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "all_graduate", + CanonicalPath: "college-contact.all_graduate", + CLIPath: "college-contact graduate all-graduate", + PrimaryCLIPath: "college-contact graduate all-graduate", + }, + Description: "全部毕业", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "all_graduate"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "全部毕业", + UseWhen: []string{"需要全部毕业时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate all-graduate --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "graduate-year", Property: "input.graduateYear", Required: boolPtr(true)}, + {Name: "request-no", Property: "input.requestNo", Required: boolPtr(false)}, + }, + }, + }) + + allGraduateCmd.Flags().String("graduate-year", "", "毕业年份(必填)") + allGraduateCmd.Flags().String("request-no", "", "异步任务请求号") + + // ── batch-graduate ──────────────────────────────────────── + batchGraduateCmd := &cobra.Command{ + Use: "batch-graduate", + Short: "批量毕业", + Long: `对指定部门下的学生批量提交毕业,返回批量毕业结果(成功/失败数量)。 +注意:批量毕业不可逆。 + +必填: + --dept-id 部门 ID + --staff-ids 学生 staffId 列表,逗号分隔 + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact graduate batch-graduate --dept-id 12345 --staff-ids staff001,staff002 --dry-run + dws college-contact graduate batch-graduate --dept-id 12345 --staff-ids staff001,staff002 -y + dws college-contact graduate batch-graduate --dept-id 12345 --staff-ids staff001,staff002 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIdRaw, _ := cmd.Flags().GetString("dept-id") + deptIdRaw = strings.TrimSpace(deptIdRaw) + if deptIdRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptId, err := strconv.ParseInt(deptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + + raw, _ := cmd.Flags().GetString("staff-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--staff-ids 为必填参数") + } + var staffIds []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIds = append(staffIds, part) + } + } + if len(staffIds) == 0 { + return fmt.Errorf("--staff-ids 不能为空列表") + } + + if err := collegeContactConfirmDestructive(cmd, "批量毕业"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "batch_graduate", map[string]any{ + "input": map[string]any{ + "deptId": deptId, + "staffIds": staffIds, + }, + }) + }, + } + + DeclareLeafMetadata(batchGraduateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "batch_graduate", + CanonicalPath: "college-contact.batch_graduate", + CLIPath: "college-contact graduate batch-graduate", + PrimaryCLIPath: "college-contact graduate batch-graduate", + }, + Description: "批量毕业", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "batch_graduate"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "批量毕业", + UseWhen: []string{"需要批量毕业时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate batch-graduate --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(true)}, + }, + }, + }) + + batchGraduateCmd.Flags().String("dept-id", "", "部门 ID(必填)") + batchGraduateCmd.Flags().String("staff-ids", "", "学生 staffId 列表,逗号分隔(必填)") + + // ── delete-and-graduate ─────────────────────────────────── + deleteAndGraduateCmd := &cobra.Command{ + Use: "delete-and-graduate", + Short: "删除并毕业", + Long: `对指定部门下的学生执行删除并毕业操作,返回批量毕业结果(成功/失败数量)。 +注意:删除并毕业不可逆。 + +必填: + --dept-id 部门 ID + --staff-ids 学生 staffId 列表,逗号分隔 + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact graduate delete-and-graduate --dept-id 12345 --staff-ids staff001,staff002 --dry-run + dws college-contact graduate delete-and-graduate --dept-id 12345 --staff-ids staff001,staff002 -y + dws college-contact graduate delete-and-graduate --dept-id 12345 --staff-ids staff001,staff002 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIdRaw, _ := cmd.Flags().GetString("dept-id") + deptIdRaw = strings.TrimSpace(deptIdRaw) + if deptIdRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptId, err := strconv.ParseInt(deptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + + raw, _ := cmd.Flags().GetString("staff-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--staff-ids 为必填参数") + } + var staffIds []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIds = append(staffIds, part) + } + } + if len(staffIds) == 0 { + return fmt.Errorf("--staff-ids 不能为空列表") + } + + if err := collegeContactConfirmDestructive(cmd, "删除并毕业"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "delete_and_graduate", map[string]any{ + "input": map[string]any{ + "deptId": deptId, + "staffIds": staffIds, + }, + }) + }, + } + + DeclareLeafMetadata(deleteAndGraduateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "delete_and_graduate", + CanonicalPath: "college-contact.delete_and_graduate", + CLIPath: "college-contact graduate delete-and-graduate", + PrimaryCLIPath: "college-contact graduate delete-and-graduate", + }, + Description: "删除并毕业", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "delete_and_graduate"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "删除并毕业", + UseWhen: []string{"需要删除并毕业时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate delete-and-graduate --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(true)}, + }, + }, + }) + + deleteAndGraduateCmd.Flags().String("dept-id", "", "部门 ID(必填)") + deleteAndGraduateCmd.Flags().String("staff-ids", "", "学生 staffId 列表,逗号分隔(必填)") + + // ── batch-delete-pending ────────────────────────────────── + batchDeletePendingCmd := &cobra.Command{ + Use: "batch-delete-pending", + Short: "批量删除待毕业学生", + Long: `对指定部门下的待毕业学生执行批量删除,返回操作是否成功。 +注意:批量删除不可逆。 + +必填: + --dept-id 部门 ID + --staff-ids 学生 staffId 列表,逗号分隔 + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact graduate batch-delete-pending --dept-id 12345 --staff-ids staff001,staff002 --dry-run + dws college-contact graduate batch-delete-pending --dept-id 12345 --staff-ids staff001,staff002 -y + dws college-contact graduate batch-delete-pending --dept-id 12345 --staff-ids staff001,staff002 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIdRaw, _ := cmd.Flags().GetString("dept-id") + deptIdRaw = strings.TrimSpace(deptIdRaw) + if deptIdRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptId, err := strconv.ParseInt(deptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + + raw, _ := cmd.Flags().GetString("staff-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--staff-ids 为必填参数") + } + var staffIds []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIds = append(staffIds, part) + } + } + if len(staffIds) == 0 { + return fmt.Errorf("--staff-ids 不能为空列表") + } + + if err := collegeContactConfirmDestructive(cmd, "批量删除待毕业学生"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "batch_delete_pending", map[string]any{ + "input": map[string]any{ + "deptId": deptId, + "staffIds": staffIds, + }, + }) + }, + } + + DeclareLeafMetadata(batchDeletePendingCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "batch_delete_pending", + CanonicalPath: "college-contact.batch_delete_pending", + CLIPath: "college-contact graduate batch-delete-pending", + PrimaryCLIPath: "college-contact graduate batch-delete-pending", + }, + Description: "批量删除待毕业学生", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "batch_delete_pending"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "批量删除待毕业学生", + UseWhen: []string{"需要批量删除待毕业学生时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate batch-delete-pending --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(true)}, + }, + }, + }) + + batchDeletePendingCmd.Flags().String("dept-id", "", "部门 ID(必填)") + batchDeletePendingCmd.Flags().String("staff-ids", "", "学生 staffId 列表,逗号分隔(必填)") + + // ── batch-update-pending ────────────────────────────────── + batchUpdatePendingCmd := &cobra.Command{ + Use: "batch-update-pending", + Short: "批量更新待毕业学生", + Long: `批量更新指定部门下待毕业学生的毕业年份,返回操作是否成功。 +注意:批量更新影响面大,请谨慎执行。 + +必填: + --dept-id 部门 ID + --staff-ids 学生 staffId 列表,逗号分隔 + --graduate-year 毕业年份 + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact graduate batch-update-pending --dept-id 12345 --staff-ids staff001,staff002 --graduate-year 2026 --dry-run + dws college-contact graduate batch-update-pending --dept-id 12345 --staff-ids staff001,staff002 --graduate-year 2026 -y + dws college-contact graduate batch-update-pending --dept-id 12345 --staff-ids staff001,staff002 --graduate-year 2026 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptIdRaw, _ := cmd.Flags().GetString("dept-id") + deptIdRaw = strings.TrimSpace(deptIdRaw) + if deptIdRaw == "" { + return fmt.Errorf("--dept-id 为必填参数") + } + deptId, err := strconv.ParseInt(deptIdRaw, 10, 64) + if err != nil { + return fmt.Errorf("--dept-id 须为整数: %w", err) + } + + raw, _ := cmd.Flags().GetString("staff-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--staff-ids 为必填参数") + } + var staffIds []string + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part != "" { + staffIds = append(staffIds, part) + } + } + if len(staffIds) == 0 { + return fmt.Errorf("--staff-ids 不能为空列表") + } + + graduateYearRaw, _ := cmd.Flags().GetString("graduate-year") + graduateYearRaw = strings.TrimSpace(graduateYearRaw) + if graduateYearRaw == "" { + return fmt.Errorf("--graduate-year 为必填参数") + } + graduateYear, err := strconv.ParseInt(graduateYearRaw, 10, 64) + if err != nil { + return fmt.Errorf("--graduate-year 须为整数: %w", err) + } + + if err := collegeContactConfirmDestructive(cmd, "批量更新待毕业学生"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "batch_update_pending", map[string]any{ + "input": map[string]any{ + "deptId": deptId, + "staffIds": staffIds, + "graduateYear": graduateYear, + }, + }) + }, + } + + DeclareLeafMetadata(batchUpdatePendingCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "batch_update_pending", + CanonicalPath: "college-contact.batch_update_pending", + CLIPath: "college-contact graduate batch-update-pending", + PrimaryCLIPath: "college-contact graduate batch-update-pending", + }, + Description: "批量更新待毕业学生", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "batch_update_pending"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "批量更新待毕业学生", + UseWhen: []string{"需要批量更新待毕业学生时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate batch-update-pending --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "staff-ids", Property: "input.staffIds", Required: boolPtr(true)}, + {Name: "graduate-year", Property: "input.graduateYear", Required: boolPtr(true)}, + }, + }, + }) + + batchUpdatePendingCmd.Flags().String("dept-id", "", "部门 ID(必填)") + batchUpdatePendingCmd.Flags().String("staff-ids", "", "学生 staffId 列表,逗号分隔(必填)") + batchUpdatePendingCmd.Flags().String("graduate-year", "", "毕业年份(必填)") + + // ── commit-restore ─────────────────────────────────────── + commitRestoreCmd := &cobra.Command{ + Use: "commit-restore", + Short: "提交恢复", + Long: `对指定部门提交恢复操作,发起异步恢复任务,返回异步任务编号。 +注意:提交恢复不可逆。 + +必填: + --graduate-dept-ids 待毕业部门 ID 列表,逗号分隔 + +可选: + --request-no 异步任务请求号 + +非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact graduate commit-restore --graduate-dept-ids 12345,12346 --dry-run + dws college-contact graduate commit-restore --graduate-dept-ids 12345,12346 -y + dws college-contact graduate commit-restore --graduate-dept-ids 12345,12346 --request-no xxx123 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + raw, _ := cmd.Flags().GetString("graduate-dept-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--graduate-dept-ids 为必填参数") + } + var graduateDeptIds []int64 + for _, part := range strings.Split(raw, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + n, err := strconv.ParseInt(part, 10, 64) + if err != nil { + return fmt.Errorf("--graduate-dept-ids 须为逗号分隔的整数列表: %w", err) + } + graduateDeptIds = append(graduateDeptIds, n) + } + if len(graduateDeptIds) == 0 { + return fmt.Errorf("--graduate-dept-ids 不能为空列表") + } + + input := map[string]any{"graduateDeptIds": graduateDeptIds} + if v, _ := cmd.Flags().GetString("request-no"); strings.TrimSpace(v) != "" { + input["requestNo"] = strings.TrimSpace(v) + } + + if err := collegeContactConfirmDestructive(cmd, "提交恢复"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "commit_restore", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(commitRestoreCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "commit_restore", + CanonicalPath: "college-contact.commit_restore", + CLIPath: "college-contact graduate commit-restore", + PrimaryCLIPath: "college-contact graduate commit-restore", + }, + Description: "提交恢复", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "commit_restore"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "提交恢复", + UseWhen: []string{"需要提交恢复时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact graduate commit-restore --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "graduate-dept-ids", Property: "input.graduateDeptIds", Required: boolPtr(true)}, + {Name: "request-no", Property: "input.requestNo", Required: boolPtr(false)}, + }, + }, + }) + + commitRestoreCmd.Flags().String("graduate-dept-ids", "", "待毕业部门 ID 列表,逗号分隔(必填)") + commitRestoreCmd.Flags().String("request-no", "", "异步任务请求号") + + // ── group 子命令组 ──────────────────────────────────────── + groupCmd := &cobra.Command{ + Use: "group", + Short: "规则管理", + RunE: groupRunE, + } + + // ── query-group-rule ───────────────────────────────────── + queryGroupRuleCmd := &cobra.Command{ + Use: "query-group-rule", + Short: "查询规则", + Long: `分页查询规则列表,支持按规则名称过滤。 + +可选: + --name 规则名称 + --offset 分页偏移量 + --size 分页大小`, + Example: ` dws college-contact group query-group-rule + dws college-contact group query-group-rule --name 分组规则 + dws college-contact group query-group-rule --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if v, _ := cmd.Flags().GetString("name"); strings.TrimSpace(v) != "" { + input["name"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("offset"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return fmt.Errorf("--offset 须为整数: %w", err) + } + input["offset"] = n + } + if v, _ := cmd.Flags().GetString("size"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return fmt.Errorf("--size 须为整数: %w", err) + } + input["size"] = n + } + + return callMCPToolOnServer("college-contact", "query_group_rule", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(queryGroupRuleCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_group_rule", + CanonicalPath: "college-contact.query_group_rule", + CLIPath: "college-contact group query-group-rule", + PrimaryCLIPath: "college-contact group query-group-rule", + }, + Description: "查询规则", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_group_rule"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询规则", + UseWhen: []string{"需要查询规则时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact group query-group-rule --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "name", Property: "input.name", Required: boolPtr(false)}, + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + queryGroupRuleCmd.Flags().String("name", "", "规则名称") + queryGroupRuleCmd.Flags().String("offset", "", "分页偏移量") + queryGroupRuleCmd.Flags().String("size", "", "分页大小") + + // ── get-group-rule-schedule ────────────────────────────── + getGroupRuleScheduleCmd := &cobra.Command{ + Use: "get-group-rule-schedule", + Short: "查询规则调度", + Long: `查询规则调度 cron 表达式,无入参。`, + Example: ` dws college-contact group get-group-rule-schedule + dws college-contact group get-group-rule-schedule -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("college-contact", "get_group_rule_schedule", map[string]any{ + "input": map[string]any{}, + }) + }, + } + + DeclareLeafMetadata(getGroupRuleScheduleCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "get_group_rule_schedule", + CanonicalPath: "college-contact.get_group_rule_schedule", + CLIPath: "college-contact group get-group-rule-schedule", + PrimaryCLIPath: "college-contact group get-group-rule-schedule", + }, + Description: "查询规则调度", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "get_group_rule_schedule"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询规则调度", + UseWhen: []string{"需要查询规则调度时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact group get-group-rule-schedule --help"}, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + // ── query-preview-data ─────────────────────────────────── + queryPreviewDataCmd := &cobra.Command{ + Use: "query-preview-data", + Short: "查询规则预览数据", + Long: `分页查询规则预览数据。 + +可选: + --offset 分页偏移量 + --size 分页大小`, + Example: ` dws college-contact group query-preview-data + dws college-contact group query-preview-data --offset 0 --size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if v, _ := cmd.Flags().GetString("offset"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return fmt.Errorf("--offset 须为整数: %w", err) + } + input["offset"] = n + } + if v, _ := cmd.Flags().GetString("size"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return fmt.Errorf("--size 须为整数: %w", err) + } + input["size"] = n + } + + return callMCPToolOnServer("college-contact", "query_preview_data", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(queryPreviewDataCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "query_preview_data", + CanonicalPath: "college-contact.query_preview_data", + CLIPath: "college-contact group query-preview-data", + PrimaryCLIPath: "college-contact group query-preview-data", + }, + Description: "查询规则预览数据", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "query_preview_data"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询规则预览数据", + UseWhen: []string{"需要查询规则预览数据时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact group query-preview-data --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "offset", Property: "input.offset", Required: boolPtr(false)}, + {Name: "size", Property: "input.size", Required: boolPtr(false)}, + }, + }, + }) + + queryPreviewDataCmd.Flags().String("offset", "", "分页偏移量") + queryPreviewDataCmd.Flags().String("size", "", "分页大小") + + // ── create-group-rule ──────────────────────────────────── + createGroupRuleCmd := &cobra.Command{ + Use: "create-group-rule", + Short: "创建规则", + Long: `创建分组规则,返回操作是否成功。 + +必填: + --name 规则名称 + --tag-code 标签编码 + --dept-type 部门类型 + +可选: + --auto-admin 是否自动设置管理员(true/false)`, + Example: ` dws college-contact group create-group-rule --name 毕业生分组 --tag-code T001 --dept-type college + dws college-contact group create-group-rule --name 毕业生分组 --tag-code T001 --dept-type college --auto-admin true -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + + // 必填 string + for _, pair := range []struct { + flag string + key string + }{ + {"name", "name"}, + {"tag-code", "tagCode"}, + {"dept-type", "deptType"}, + } { + v, _ := cmd.Flags().GetString(pair.flag) + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("--%s 为必填参数", pair.flag) + } + input[pair.key] = v + } + + // 可选 bool(使用 String flag 以避免 pflag 对 "--flag false" 的错误解析) + if cmd.Flags().Changed("auto-admin") { + aaRaw, _ := cmd.Flags().GetString("auto-admin") + autoAdmin, err := strconv.ParseBool(strings.TrimSpace(aaRaw)) + if err != nil { + return fmt.Errorf("--auto-admin 须为 true 或 false: %w", err) + } + input["autoAdmin"] = autoAdmin + } + + return callMCPToolOnServer("college-contact", "create_group_rule", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(createGroupRuleCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "create_group_rule", + CanonicalPath: "college-contact.create_group_rule", + CLIPath: "college-contact group create-group-rule", + PrimaryCLIPath: "college-contact group create-group-rule", + }, + Description: "创建规则", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "create_group_rule"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "创建规则", + UseWhen: []string{"需要创建规则时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact group create-group-rule --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "name", Property: "input.name", Required: boolPtr(true)}, + {Name: "tag-code", Property: "input.tagCode", Required: boolPtr(true)}, + {Name: "dept-type", Property: "input.deptType", Required: boolPtr(true)}, + {Name: "auto-admin", Property: "input.autoAdmin", Required: boolPtr(false)}, + }, + }, + }) + + createGroupRuleCmd.Flags().String("name", "", "规则名称(必填)") + createGroupRuleCmd.Flags().String("tag-code", "", "标签编码(必填)") + createGroupRuleCmd.Flags().String("dept-type", "", "部门类型(必填)") + createGroupRuleCmd.Flags().String("auto-admin", "", "是否自动设置管理员(true/false)") + + // ── delete-group-rule ──────────────────────────────────── + deleteGroupRuleCmd := &cobra.Command{ + Use: "delete-group-rule", + Short: "删除规则", + Long: `删除分组规则,返回操作是否成功。 +--rule-id 为必填。 + +注意:删除操作不可逆。非 --dry-run 预览时必须显式传入 --yes 才会真实执行, +未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact group delete-group-rule --rule-id 1 --dry-run + dws college-contact group delete-group-rule --rule-id 1 -y + dws college-contact group delete-group-rule --rule-id 1 -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + ruleIDRaw, _ := cmd.Flags().GetString("rule-id") + ruleIDRaw = strings.TrimSpace(ruleIDRaw) + if ruleIDRaw == "" { + return fmt.Errorf("--rule-id 为必填参数") + } + ruleID, err := strconv.ParseInt(ruleIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--rule-id 须为整数: %w", err) + } + + if err := collegeContactConfirmDestructive(cmd, "删除规则"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "delete_group_rule", map[string]any{ + "input": map[string]any{"ruleId": ruleID}, + }) + }, + } + + DeclareLeafMetadata(deleteGroupRuleCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "delete_group_rule", + CanonicalPath: "college-contact.delete_group_rule", + CLIPath: "college-contact group delete-group-rule", + PrimaryCLIPath: "college-contact group delete-group-rule", + }, + Description: "删除规则", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "delete_group_rule"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "删除规则", + UseWhen: []string{"需要删除规则时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact group delete-group-rule --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "rule-id", Property: "input.ruleId", Required: boolPtr(true)}, + }, + }, + }) + + deleteGroupRuleCmd.Flags().String("rule-id", "", "规则 ID(必填)") + + // ── enable-group-rule ──────────────────────────────────── + enableGroupRuleCmd := &cobra.Command{ + Use: "enable-group-rule", + Short: "启用规则", + Long: `启用分组规则,返回操作是否成功。 +--rule-id 为必填。`, + Example: ` dws college-contact group enable-group-rule --rule-id 1 + dws college-contact group enable-group-rule --rule-id 1 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + ruleIDRaw, _ := cmd.Flags().GetString("rule-id") + ruleIDRaw = strings.TrimSpace(ruleIDRaw) + if ruleIDRaw == "" { + return fmt.Errorf("--rule-id 为必填参数") + } + ruleID, err := strconv.ParseInt(ruleIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--rule-id 须为整数: %w", err) + } + + return callMCPToolOnServer("college-contact", "enable_group_rule", map[string]any{ + "input": map[string]any{"ruleId": ruleID}, + }) + }, + } + + DeclareLeafMetadata(enableGroupRuleCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "enable_group_rule", + CanonicalPath: "college-contact.enable_group_rule", + CLIPath: "college-contact group enable-group-rule", + PrimaryCLIPath: "college-contact group enable-group-rule", + }, + Description: "启用规则", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "enable_group_rule"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "启用规则", + UseWhen: []string{"需要启用规则时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact group enable-group-rule --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "rule-id", Property: "input.ruleId", Required: boolPtr(true)}, + }, + }, + }) + + enableGroupRuleCmd.Flags().String("rule-id", "", "规则 ID(必填)") + + // ── disable-group-rule ─────────────────────────────────── + disableGroupRuleCmd := &cobra.Command{ + Use: "disable-group-rule", + Short: "停用规则", + Long: `停用分组规则,返回操作是否成功。 +--rule-id 为必填。`, + Example: ` dws college-contact group disable-group-rule --rule-id 1 + dws college-contact group disable-group-rule --rule-id 1 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + ruleIDRaw, _ := cmd.Flags().GetString("rule-id") + ruleIDRaw = strings.TrimSpace(ruleIDRaw) + if ruleIDRaw == "" { + return fmt.Errorf("--rule-id 为必填参数") + } + ruleID, err := strconv.ParseInt(ruleIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("--rule-id 须为整数: %w", err) + } + + return callMCPToolOnServer("college-contact", "disable_group_rule", map[string]any{ + "input": map[string]any{"ruleId": ruleID}, + }) + }, + } + + DeclareLeafMetadata(disableGroupRuleCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "disable_group_rule", + CanonicalPath: "college-contact.disable_group_rule", + CLIPath: "college-contact group disable-group-rule", + PrimaryCLIPath: "college-contact group disable-group-rule", + }, + Description: "停用规则", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "disable_group_rule"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "停用规则", + UseWhen: []string{"需要停用规则时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact group disable-group-rule --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "rule-id", Property: "input.ruleId", Required: boolPtr(true)}, + }, + }, + }) + + disableGroupRuleCmd.Flags().String("rule-id", "", "规则 ID(必填)") + + // ── set-group-rule-schedule ────────────────────────────── + setGroupRuleScheduleCmd := &cobra.Command{ + Use: "set-group-rule-schedule", + Short: "设置规则调度", + Long: `设置规则调度 cron 表达式,返回操作是否成功。 + +可选: + --cron cron 表达式`, + Example: ` dws college-contact group set-group-rule-schedule --cron "0 0 2 * * ?" + dws college-contact group set-group-rule-schedule --cron "0 0 2 * * ?" -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if v, _ := cmd.Flags().GetString("cron"); strings.TrimSpace(v) != "" { + input["cron"] = strings.TrimSpace(v) + } + + return callMCPToolOnServer("college-contact", "set_group_rule_schedule", map[string]any{ + "input": input, + }) + }, + } + + DeclareLeafMetadata(setGroupRuleScheduleCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "set_group_rule_schedule", + CanonicalPath: "college-contact.set_group_rule_schedule", + CLIPath: "college-contact group set-group-rule-schedule", + PrimaryCLIPath: "college-contact group set-group-rule-schedule", + }, + Description: "设置规则调度", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "set_group_rule_schedule"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "设置规则调度", + UseWhen: []string{"需要设置规则调度时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact group set-group-rule-schedule --help"}, + }, + Parameters: []contract.ParamDecl{ + {Name: "cron", Property: "input.cron", Required: boolPtr(false)}, + }, + }, + }) + + setGroupRuleScheduleCmd.Flags().String("cron", "", "cron 表达式") + + // ── execute-group-rule ─────────────────────────────────── + executeGroupRuleCmd := &cobra.Command{ + Use: "execute-group-rule", + Short: "立即执行规则", + Long: `立即执行分组规则(按规则对成员进行分组、建群等批量变更),返回操作是否成功。 +无入参。 + +注意:执行会对成员产生批量变更,影响范围大。非 --dry-run 预览时必须显式传入 +--yes 才会真实执行,未传 --yes 直接拒绝;请在向用户展示操作摘要并获得确认后再追加 --yes。`, + Example: ` dws college-contact group execute-group-rule --dry-run + dws college-contact group execute-group-rule -y + dws college-contact group execute-group-rule -y -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := collegeContactConfirmDestructive(cmd, "立即执行规则"); err != nil { + return err + } + + return callMCPToolOnServer("college-contact", "execute_group_rule", map[string]any{ + "input": map[string]any{}, + }) + }, + } + + DeclareLeafMetadata(executeGroupRuleCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "college-contact", + Name: "execute_group_rule", + CanonicalPath: "college-contact.execute_group_rule", + CLIPath: "college-contact group execute-group-rule", + PrimaryCLIPath: "college-contact group execute-group-rule", + }, + Description: "立即执行规则", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "college-contact", RPCName: "execute_group_rule"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "立即执行规则", + UseWhen: []string{"需要立即执行规则时"}, + AvoidWhen: []string{"与高校通讯录无关的操作"}, + Examples: []string{"dws college-contact group execute-group-rule --help"}, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + groupCmd.AddCommand(queryGroupRuleCmd, getGroupRuleScheduleCmd, queryPreviewDataCmd, createGroupRuleCmd, deleteGroupRuleCmd, enableGroupRuleCmd, disableGroupRuleCmd, setGroupRuleScheduleCmd, executeGroupRuleCmd) + + alumniCmd.AddCommand(getAlumniDeptTreeCmd, getAlumniDeptInfoCmd, listAlumniCmd, queryAlumnusCmd, searchAlumniCmd, listUnacceptedAlumnusCmd, getAlumniGroupCmd, createAlumniDeptsCmd, updateAlumniDeptCmd, deleteAlumniDeptCmd, updateAlumniDeptManagersCmd, addAlumnusCmd, updateAlumnusCmd, removeAlumnusCmd, cancelAlumniInviteCmd, createAlumniGroupCmd, disbandAlumniGroupCmd, getAlumniOrgFromGraduateCmd, createAlumniOrgCmd, addAlumniOrgMainAdminsCmd) + graduateCmd.AddCommand(queryGraduateYearsCmd, queryGraduateDeptsCmd, queryGraduateSubDeptsCmd, queryPageGraduateUsersCmd, getTaskResultCmd, getAlumniOrgCmd, queryRestoreSubDeptsCmd, queryDeptDeletedEmpsCmd, searchGraduateCmd, commitGraduateCmd, allGraduateCmd, batchGraduateCmd, deleteAndGraduateCmd, batchDeletePendingCmd, batchUpdatePendingCmd, commitRestoreCmd) + + employeeCmd.AddCommand(getEmployeeDetailCmd, addEmployeeCmd, removeEmployeeCmd, changeEmployeeTypeCmd, changeEmployeeDeptCmd, sendActiveSmsCmd, listEmployeesCmd, listUnacceptedEmployeesCmd, listUnactiveEmployeesCmd, upgradeStatusCmd, startUpgradeCmd) + root.AddCommand(deptCmd, employeeCmd, alumniCmd, graduateCmd, groupCmd) + + return root +} + +// collegeContactConfirmDestructive 是不可逆操作(删除部门/移除员工)的安全门禁: +// - 全局 --dry-run 仅预览不执行,直接放行; +// - 显式传入全局 --yes/-y 视为已确认,放行; +// - 其余情况拒绝执行,避免误操作产生不可逆变更。 +// +// 全局 flag 由核心框架注册在 rootCmd 上,cobra 执行时会将 persistent flag +// 合并进叶子命令的 Flags();若 flag 不存在(如单测环境),GetBool 返回零值, +// 默认拒绝执行,保持安全兼底。 +func collegeContactConfirmDestructive(cmd *cobra.Command, action string) error { + if deps != nil && deps.Caller != nil && deps.Caller.DryRun() { + return nil + } + if yes, _ := cmd.Flags().GetBool("yes"); yes { + return nil + } + return fmt.Errorf("%s为不可逆操作:请先向用户展示操作摘要并获得明确同意,确认后追加 --yes 执行,或使用 --dry-run 预览", action) +} diff --git a/internal/helpers/college_contact_test.go b/internal/helpers/college_contact_test.go new file mode 100644 index 00000000..95e0a26f --- /dev/null +++ b/internal/helpers/college_contact_test.go @@ -0,0 +1,680 @@ +package helpers + +import ( + "io" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestCollegeContactCommand_Structure(t *testing.T) { + cmd := newCollegeContactCommand() + + if cmd.Name() != "college-contact" { + t.Errorf("expected name 'college-contact', got %q", cmd.Name()) + } + if !cmd.Hidden { + t.Error("extension root command should be Hidden") + } + + // 分组 → 叶子命令映射 + groups := map[string][]string{ + "dept": { + "get-standard-structure", "get-detail", "get-chain", "search", + "create", "update", "delete", "batch-update-type", "overview", + }, + "employee": { + "get-detail", "add", "remove", "change-type", "change-dept", + "send-active-sms", "list-employees", "list-unaccepted", + "list-unactive", "upgrade-status", "start-upgrade", + }, + "alumni": { + "get-dept-tree", "get-info", "list", "query", "search", "list-unaccepted", "get-group", "create-dept", "update-dept", "delete-dept", "update-managers", "add-alumnus", "update-alumnus", "remove-alumnus", "cancel-invite", "create-group", "disband-group", "get-alumni-org-from-graduate", "create-alumni-org", "add-alumni-org-main-admins", + }, + "graduate": { + "query-graduate-years", "query-graduate-depts", "query-graduate-sub-depts", "query-page-graduate-users", "get-task-result", "get-alumni-org", "query-restore-sub-depts", "query-dept-deleted-emps", "search-graduate", "commit-graduate", "all-graduate", "batch-graduate", "delete-and-graduate", "batch-delete-pending", "batch-update-pending", "commit-restore", + }, + "group": { + "query-group-rule", "get-group-rule-schedule", "query-preview-data", "create-group-rule", "delete-group-rule", "enable-group-rule", "disable-group-rule", "set-group-rule-schedule", "execute-group-rule", + }, + } + + for groupName, leaves := range groups { + var groupCmd *cobra.Command + for _, c := range cmd.Commands() { + if c.Name() == groupName { + groupCmd = c + break + } + } + if groupCmd == nil { + t.Fatalf("subcommand group %q not found", groupName) + } + for _, leaf := range leaves { + found := false + for _, c := range groupCmd.Commands() { + if c.Name() == leaf { + found = true + break + } + } + if !found { + t.Errorf("leaf command %q not found under %q", leaf, groupName) + } + } + } + + // stats 分组已移除 + for _, c := range cmd.Commands() { + if c.Name() == "stats" { + t.Error("subcommand group 'stats' should be removed") + } + } +} + +func TestCollegeContactCommand_FindPath(t *testing.T) { + root := &cobra.Command{Use: "dws"} + root.AddCommand(newCollegeContactCommand()) + + c, _, err := root.Find([]string{"college-contact", "dept", "get-standard-structure"}) + if err != nil { + t.Fatalf("command path not found: %v", err) + } + if c.Name() != "get-standard-structure" { + t.Errorf("expected leaf 'get-standard-structure', got %q", c.Name()) + } +} + +// newCollegeContactTestRoot 模拟真实运行时的根命令:核心框架在 rootCmd 上 +// 注册全局 persistent --yes flag,叶子命令通过合并后的 Flags() 读取。 +func newCollegeContactTestRoot() *cobra.Command { + root := &cobra.Command{Use: "dws"} + root.PersistentFlags().BoolP("yes", "y", false, "跳过确认提示") + root.AddCommand(newCollegeContactCommand()) + return root +} + +// runDestructiveLeaf 执行不可逆叶子命令并捕获 panic。 +// 单测环境未初始化 products 运行时依赖,若门禁放行后进入 +// CallMCPToolOnServer 会因 deps 为 nil 而 panic,据此区分 +// “被门禁拦截(返回错误)”与“已越过门禁到达 MCP 调用层(panic)”。 +func runDestructiveLeaf(t *testing.T, args ...string) (err error, panicked bool) { + t.Helper() + root := newCollegeContactTestRoot() + root.SetArgs(args) + defer func() { + if r := recover(); r != nil { + panicked = true + } + }() + err = root.Execute() + return err, false +} + +func TestCollegeContactDestructive_RejectedWithoutYes(t *testing.T) { + cases := [][]string{ + {"college-contact", "dept", "delete", "--dept-id", "12345"}, + {"college-contact", "employee", "remove", "--staff-ids", "S12345,S12346"}, + } + for _, args := range cases { + err, panicked := runDestructiveLeaf(t, args...) + if panicked { + t.Fatalf("%v: 未传 --yes 不应到达 MCP 调用层", args) + } + if err == nil { + t.Fatalf("%v: 未传 --yes 应拒绝执行", args) + } + if !strings.Contains(err.Error(), "--yes") { + t.Errorf("%v: 错误信息应提示 --yes,got: %v", args, err) + } + } +} + +func TestCollegeContactDestructive_ProceedsWithYes(t *testing.T) { + cases := [][]string{ + {"college-contact", "dept", "delete", "--dept-id", "12345", "--yes"}, + {"college-contact", "employee", "remove", "--staff-ids", "S12345", "--yes"}, + } + for _, args := range cases { + err, panicked := runDestructiveLeaf(t, args...) + if !panicked { + // 未 panic 意味着未到达 MCP 调用层;若返回的仍是门禁错误则为拦截失败 + if err != nil && strings.Contains(err.Error(), "不可逆操作") { + t.Fatalf("%v: 已传 --yes 仍被门禁拦截: %v", args, err) + } + } + } +} + +func TestCollegeContactConfirmDestructive_Helper(t *testing.T) { + // 无 yes flag(零值安全兼底)→ 拒绝 + bare := &cobra.Command{Use: "x"} + if err := collegeContactConfirmDestructive(bare, "删除部门"); err == nil { + t.Error("无 --yes 应拒绝执行") + } + + // yes=true → 放行 + withYes := &cobra.Command{Use: "x"} + withYes.Flags().Bool("yes", true, "") + if err := collegeContactConfirmDestructive(withYes, "删除部门"); err != nil { + t.Errorf("已传 --yes 应放行,got: %v", err) + } +} + +// withCollegeContactCaller installs a dry-run capture caller so happy-path +// command execution exercises each RunE up to the callMCPToolOnServer dispatch +// without requiring a live MCP transport. In dry-run mode destructive +// commands' confirm gate short-circuits to nil, so no --yes flag is needed. +func withCollegeContactCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{dryRun: true} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +// TestCollegeContactHappyPaths runs every leaf command with required flags only +// and with all optional flags populated, expecting a nil error (dry-run preview). +func TestCollegeContactHappyPaths(t *testing.T) { + withCollegeContactCaller(t) + + cases := [][]string{ + // ── dept ───────────────────────────────────────────── + {"dept", "get-standard-structure"}, + {"dept", "get-standard-structure", "--dept-id", "123", "--staff-id", "S1", "--keyword", "k", "--offset", "0", "--size", "20"}, + {"dept", "get-detail", "--dept-id", "123"}, + {"dept", "get-detail", "--dept-id", "123", "--staff-id", "S1", "--keyword", "k", "--offset", "0", "--size", "20"}, + {"dept", "get-chain", "--dept-id", "123"}, + {"dept", "get-chain", "--dept-id", "123", "--staff-id", "S1", "--keyword", "k", "--offset", "0", "--size", "20"}, + {"dept", "search", "--dept-id", "123", "--keyword", "k"}, + {"dept", "search", "--dept-id", "123", "--keyword", "k", "--staff-id", "S1", "--offset", "0", "--size", "20"}, + {"dept", "create", "--super-id", "100", "--stru-dept-id", "200", "--name", "X", "--dept-type", "college", "--create-dept-group", "true"}, + {"dept", "create", "--super-id", "100", "--stru-dept-id", "200", "--name", "X", "--dept-type", "college", "--create-dept-group", "false", "--dept-id", "5", "--dept-code", "C", "--brief", "b", "--phone", "p"}, + {"dept", "update", "--dept-id", "123", "--dept-type", "college"}, + {"dept", "update", "--dept-id", "123", "--dept-type", "college", "--stru-dept-id", "200", "--super-id", "100", "--create-dept-group", "true", "--name", "X", "--dept-code", "C", "--brief", "b", "--phone", "p"}, + {"dept", "delete", "--dept-id", "123"}, + {"dept", "batch-update-type", "--dept-ids", "100,200", "--target-dept-type", "college"}, + {"dept", "overview"}, + {"dept", "overview", "--dept-id", "123", "--staff-id", "S1", "--keyword", "k", "--offset", "0", "--size", "20"}, + + // ── employee ───────────────────────────────────────── + {"employee", "get-detail", "--staff-id", "S1"}, + {"employee", "get-detail", "--staff-id", "S1", "--dept-id", "1", "--main-dept-id", "2", "--target-dept-id", "3", "--offset", "0", "--size", "20", "--exclusive-account", "true", "--send-active-sms", "true", "--name", "n", "--mobile", "m", "--job-number", "j", "--emp-type", "college_student", "--login-id-type", "l", "--order-field", "job_number", "--ordering", "asc", "--staff-ids", "s1,s2"}, + {"employee", "add", "--emp-type", "college_student", "--main-dept-id", "100", "--exclusive-account", "true"}, + {"employee", "add", "--emp-type", "college_student", "--main-dept-id", "100", "--exclusive-account", "true", "--dept-id", "1", "--target-dept-id", "3", "--offset", "0", "--size", "20", "--send-active-sms", "false", "--staff-id", "S1", "--name", "n", "--mobile", "m", "--job-number", "j", "--login-id-type", "l", "--order-field", "f", "--ordering", "asc", "--staff-ids", "s1,s2"}, + {"employee", "remove", "--staff-ids", "S1"}, + {"employee", "remove", "--staff-ids", "S1", "--dept-id", "1", "--main-dept-id", "2", "--target-dept-id", "3", "--offset", "0", "--size", "20", "--exclusive-account", "true", "--send-active-sms", "true", "--staff-id", "x", "--name", "n", "--mobile", "m", "--job-number", "j", "--emp-type", "college_student", "--login-id-type", "l", "--order-field", "f", "--ordering", "asc"}, + {"employee", "change-type", "--staff-id", "S1", "--emp-type", "college_teacher"}, + {"employee", "change-type", "--staff-id", "S1", "--emp-type", "college_teacher", "--dept-id", "1", "--main-dept-id", "2", "--target-dept-id", "3", "--offset", "0", "--size", "20", "--exclusive-account", "true", "--send-active-sms", "true", "--name", "n", "--mobile", "m", "--job-number", "j", "--login-id-type", "l", "--order-field", "f", "--ordering", "asc", "--staff-ids", "s1,s2"}, + {"employee", "change-dept", "--staff-id", "S1", "--target-dept-id", "200"}, + {"employee", "change-dept", "--staff-id", "S1", "--target-dept-id", "200", "--dept-id", "1", "--main-dept-id", "2", "--offset", "0", "--size", "20", "--exclusive-account", "true", "--send-active-sms", "true", "--name", "n", "--mobile", "m", "--job-number", "j", "--emp-type", "college_student", "--login-id-type", "l", "--order-field", "f", "--ordering", "asc", "--staff-ids", "s1,s2"}, + {"employee", "send-active-sms", "--dept-id", "100"}, + {"employee", "send-active-sms", "--dept-id", "100", "--main-dept-id", "2", "--target-dept-id", "3", "--offset", "0", "--size", "20", "--exclusive-account", "true", "--send-active-sms", "true", "--staff-id", "x", "--name", "n", "--mobile", "m", "--job-number", "j", "--emp-type", "college_student", "--login-id-type", "l", "--order-field", "f", "--ordering", "asc", "--staff-ids", "s1,s2"}, + {"employee", "list-employees", "--dept-id", "123"}, + {"employee", "list-employees", "--dept-id", "123", "--main-dept-id", "2", "--target-dept-id", "3", "--offset", "0", "--size", "20", "--exclusive-account", "true", "--send-active-sms", "true", "--staff-id", "x", "--name", "n", "--mobile", "m", "--job-number", "j", "--login-id-type", "l", "--order-field", "f", "--ordering", "asc", "--staff-ids", "s1,s2"}, + {"employee", "list-unaccepted", "--dept-id", "123"}, + {"employee", "list-unaccepted", "--dept-id", "123", "--main-dept-id", "2", "--target-dept-id", "3", "--offset", "0", "--size", "20", "--exclusive-account", "true", "--send-active-sms", "true", "--staff-id", "x", "--name", "n", "--mobile", "m", "--job-number", "j", "--emp-type", "college_student", "--login-id-type", "l", "--order-field", "f", "--ordering", "asc", "--staff-ids", "s1,s2"}, + {"employee", "list-unactive", "--dept-id", "123"}, + {"employee", "list-unactive", "--dept-id", "123", "--main-dept-id", "2", "--target-dept-id", "3", "--offset", "0", "--size", "20", "--exclusive-account", "true", "--send-active-sms", "true", "--staff-id", "x", "--name", "n", "--mobile", "m", "--job-number", "j", "--login-id-type", "l", "--order-field", "f", "--ordering", "asc", "--staff-ids", "s1,s2"}, + {"employee", "upgrade-status"}, + {"employee", "upgrade-status", "--dept-id", "123", "--staff-id", "S1", "--keyword", "k", "--offset", "0", "--size", "20"}, + {"employee", "start-upgrade"}, + + // ── alumni ─────────────────────────────────────────── + {"alumni", "get-dept-tree", "--alumni-dept-id", "123"}, + {"alumni", "get-info", "--alumni-dept-id", "123"}, + {"alumni", "list", "--alumni-dept-id", "1", "--order-field", "dept_entry", "--ordering", "asc"}, + {"alumni", "list", "--alumni-dept-id", "1", "--order-field", "dept_entry", "--ordering", "asc", "--offset", "0", "--size", "20"}, + {"alumni", "query", "--staff-id", "S1"}, + {"alumni", "search", "--keyword", "x"}, + {"alumni", "search", "--keyword", "x", "--offset", "0", "--size", "20"}, + {"alumni", "list-unaccepted", "--alumni-dept-id", "1"}, + {"alumni", "list-unaccepted", "--alumni-dept-id", "1", "--offset", "0", "--size", "20"}, + {"alumni", "get-group", "--alumni-dept-id", "1"}, + {"alumni", "create-dept", "--alumni-dept-id", "1", "--dept-name", "D"}, + {"alumni", "update-dept", "--alumni-dept-id", "1", "--dept-name", "D"}, + {"alumni", "delete-dept", "--alumni-dept-id", "1"}, + {"alumni", "update-managers", "--alumni-dept-id", "1", "--admin-user-ids", "u1,u2"}, + {"alumni", "add-alumnus", "--name", "X", "--mobile", "138", "--dept-ids", "1,2"}, + {"alumni", "add-alumnus", "--name", "X", "--mobile", "138", "--dept-ids", "1,2", "--student-number", "2020", "--email", "e", "--intake", "2020", "--outtake", "2024"}, + {"alumni", "update-alumnus", "--staff-id", "S1", "--name", "X", "--dept-ids", "1,2"}, + {"alumni", "update-alumnus", "--staff-id", "S1", "--name", "X", "--dept-ids", "1,2", "--student-number", "2020", "--email", "e", "--intake", "2020", "--outtake", "2024"}, + {"alumni", "remove-alumnus", "--staff-id", "S1", "--alumni-dept-id", "1"}, + {"alumni", "cancel-invite", "--alumni-dept-id", "1", "--staff-ids", "s1,s2"}, + {"alumni", "create-group", "--alumni-dept-id", "1"}, + {"alumni", "disband-group", "--alumni-dept-id", "1"}, + {"alumni", "get-alumni-org-from-graduate"}, + {"alumni", "create-alumni-org", "--org-name", "O"}, + {"alumni", "add-alumni-org-main-admins", "--admin-user-ids", "u1,u2"}, + + // ── graduate ───────────────────────────────────────── + {"graduate", "query-graduate-years"}, + {"graduate", "query-graduate-depts", "--dept-id", "1"}, + {"graduate", "query-graduate-depts", "--dept-id", "1", "--graduate-year", "2026"}, + {"graduate", "query-graduate-sub-depts", "--dept-id", "1"}, + {"graduate", "query-page-graduate-users", "--dept-id", "1"}, + {"graduate", "query-page-graduate-users", "--dept-id", "1", "--graduate-year", "2026", "--offset", "0", "--size", "20"}, + {"graduate", "get-task-result", "--request-no", "r1"}, + {"graduate", "get-task-result", "--request-no", "r1", "--type", "GRADUATE"}, + {"graduate", "get-alumni-org"}, + {"graduate", "query-restore-sub-depts", "--dept-id", "1"}, + {"graduate", "query-dept-deleted-emps", "--dept-id", "1"}, + {"graduate", "query-dept-deleted-emps", "--dept-id", "1", "--offset", "0", "--size", "20"}, + {"graduate", "search-graduate", "--keyword", "x"}, + {"graduate", "search-graduate", "--keyword", "x", "--offset", "0", "--size", "20"}, + {"graduate", "commit-graduate", "--graduate-dept-ids", "1,2", "--graduate-year", "2026"}, + {"graduate", "commit-graduate", "--graduate-dept-ids", "1,2", "--graduate-year", "2026", "--request-no", "r1"}, + {"graduate", "all-graduate", "--graduate-year", "2026"}, + {"graduate", "all-graduate", "--graduate-year", "2026", "--request-no", "r1"}, + {"graduate", "batch-graduate", "--dept-id", "1", "--staff-ids", "s1,s2"}, + {"graduate", "delete-and-graduate", "--dept-id", "1", "--staff-ids", "s1,s2"}, + {"graduate", "batch-delete-pending", "--dept-id", "1", "--staff-ids", "s1,s2"}, + {"graduate", "batch-update-pending", "--dept-id", "1", "--staff-ids", "s1,s2", "--graduate-year", "2026"}, + {"graduate", "commit-restore", "--graduate-dept-ids", "1,2"}, + {"graduate", "commit-restore", "--graduate-dept-ids", "1,2", "--request-no", "r1"}, + + // ── group ──────────────────────────────────────────── + {"group", "query-group-rule"}, + {"group", "query-group-rule", "--name", "N", "--offset", "0", "--size", "20"}, + {"group", "get-group-rule-schedule"}, + {"group", "query-preview-data"}, + {"group", "query-preview-data", "--offset", "0", "--size", "20"}, + {"group", "create-group-rule", "--name", "X", "--tag-code", "T", "--dept-type", "college"}, + {"group", "create-group-rule", "--name", "X", "--tag-code", "T", "--dept-type", "college", "--auto-admin", "true"}, + {"group", "delete-group-rule", "--rule-id", "1"}, + {"group", "enable-group-rule", "--rule-id", "1"}, + {"group", "disable-group-rule", "--rule-id", "1"}, + {"group", "set-group-rule-schedule"}, + {"group", "set-group-rule-schedule", "--cron", "0 0 2 * * ?"}, + {"group", "execute-group-rule"}, + } + + for _, args := range cases { + root := newCollegeContactCommand() + if err := executeCommand(root, args...); err != nil { + t.Errorf("%v: expected nil error, got: %v", args, err) + } + } +} + +// TestCollegeContactValidationErrors exercises every validation-error branch: +// missing required flags, non-integer int flags, invalid bool flags, and +// empty-after-split CSV lists. Each case must return a non-nil error. +func TestCollegeContactValidationErrors(t *testing.T) { + withCollegeContactCaller(t) + + cases := [][]string{ + // ── dept ───────────────────────────────────────────── + {"dept", "get-standard-structure", "--dept-id", "abc"}, + {"dept", "get-standard-structure", "--offset", "abc"}, + {"dept", "get-standard-structure", "--size", "abc"}, + {"dept", "get-detail"}, + {"dept", "get-detail", "--dept-id", "abc"}, + {"dept", "get-detail", "--dept-id", "1", "--offset", "abc"}, + {"dept", "get-detail", "--dept-id", "1", "--size", "abc"}, + {"dept", "get-chain"}, + {"dept", "get-chain", "--dept-id", "abc"}, + {"dept", "get-chain", "--dept-id", "1", "--offset", "abc"}, + {"dept", "get-chain", "--dept-id", "1", "--size", "abc"}, + {"dept", "search"}, + {"dept", "search", "--dept-id", "abc", "--keyword", "k"}, + {"dept", "search", "--dept-id", "1"}, + {"dept", "search", "--dept-id", "1", "--keyword", "k", "--offset", "abc"}, + {"dept", "search", "--dept-id", "1", "--keyword", "k", "--size", "abc"}, + {"dept", "create"}, + {"dept", "create", "--super-id", "abc"}, + {"dept", "create", "--super-id", "100"}, + {"dept", "create", "--super-id", "100", "--stru-dept-id", "abc"}, + {"dept", "create", "--super-id", "100", "--stru-dept-id", "200"}, + {"dept", "create", "--super-id", "100", "--stru-dept-id", "200", "--name", "X"}, + {"dept", "create", "--super-id", "100", "--stru-dept-id", "200", "--name", "X", "--dept-type", "college"}, + {"dept", "create", "--super-id", "100", "--stru-dept-id", "200", "--name", "X", "--dept-type", "college", "--create-dept-group", "maybe"}, + {"dept", "create", "--super-id", "100", "--stru-dept-id", "200", "--name", "X", "--dept-type", "college", "--create-dept-group", "true", "--dept-id", "abc"}, + {"dept", "update"}, + {"dept", "update", "--dept-id", "abc"}, + {"dept", "update", "--dept-id", "1"}, + {"dept", "update", "--dept-id", "1", "--dept-type", "college", "--stru-dept-id", "abc"}, + {"dept", "update", "--dept-id", "1", "--dept-type", "college", "--super-id", "abc"}, + {"dept", "update", "--dept-id", "1", "--dept-type", "college", "--create-dept-group", "maybe"}, + {"dept", "delete"}, + {"dept", "delete", "--dept-id", "abc"}, + {"dept", "batch-update-type"}, + {"dept", "batch-update-type", "--dept-ids", "abc", "--target-dept-type", "college"}, + {"dept", "batch-update-type", "--dept-ids", ",,", "--target-dept-type", "college"}, + {"dept", "batch-update-type", "--dept-ids", "1,2"}, + {"dept", "overview", "--dept-id", "abc"}, + {"dept", "overview", "--offset", "abc"}, + {"dept", "overview", "--size", "abc"}, + + // ── employee ───────────────────────────────────────── + {"employee", "get-detail"}, + {"employee", "get-detail", "--staff-id", "S1", "--dept-id", "abc"}, + {"employee", "get-detail", "--staff-id", "S1", "--exclusive-account", "maybe"}, + {"employee", "add"}, + {"employee", "add", "--emp-type", "x"}, + {"employee", "add", "--emp-type", "x", "--main-dept-id", "abc"}, + {"employee", "add", "--emp-type", "x", "--main-dept-id", "100"}, + {"employee", "add", "--emp-type", "x", "--main-dept-id", "100", "--exclusive-account", "maybe"}, + {"employee", "add", "--emp-type", "x", "--main-dept-id", "100", "--exclusive-account", "true", "--dept-id", "abc"}, + {"employee", "add", "--emp-type", "x", "--main-dept-id", "100", "--exclusive-account", "true", "--send-active-sms", "maybe"}, + {"employee", "remove"}, + {"employee", "remove", "--staff-ids", ",,"}, + {"employee", "remove", "--staff-ids", "S1", "--dept-id", "abc"}, + {"employee", "remove", "--staff-ids", "S1", "--exclusive-account", "maybe"}, + {"employee", "change-type"}, + {"employee", "change-type", "--staff-id", "S1"}, + {"employee", "change-type", "--staff-id", "S1", "--emp-type", "t", "--dept-id", "abc"}, + {"employee", "change-type", "--staff-id", "S1", "--emp-type", "t", "--exclusive-account", "maybe"}, + {"employee", "change-dept"}, + {"employee", "change-dept", "--staff-id", "S1"}, + {"employee", "change-dept", "--staff-id", "S1", "--target-dept-id", "abc"}, + {"employee", "change-dept", "--staff-id", "S1", "--target-dept-id", "200", "--dept-id", "abc"}, + {"employee", "change-dept", "--staff-id", "S1", "--target-dept-id", "200", "--exclusive-account", "maybe"}, + {"employee", "send-active-sms"}, + {"employee", "send-active-sms", "--dept-id", "abc"}, + {"employee", "send-active-sms", "--dept-id", "1", "--main-dept-id", "abc"}, + {"employee", "send-active-sms", "--dept-id", "1", "--exclusive-account", "maybe"}, + {"employee", "list-employees"}, + {"employee", "list-employees", "--dept-id", "abc"}, + {"employee", "list-employees", "--dept-id", "1", "--main-dept-id", "abc"}, + {"employee", "list-employees", "--dept-id", "1", "--exclusive-account", "maybe"}, + {"employee", "list-unaccepted"}, + {"employee", "list-unaccepted", "--dept-id", "abc"}, + {"employee", "list-unaccepted", "--dept-id", "1", "--main-dept-id", "abc"}, + {"employee", "list-unaccepted", "--dept-id", "1", "--exclusive-account", "maybe"}, + {"employee", "list-unactive"}, + {"employee", "list-unactive", "--dept-id", "abc"}, + {"employee", "list-unactive", "--dept-id", "1", "--main-dept-id", "abc"}, + {"employee", "list-unactive", "--dept-id", "1", "--exclusive-account", "maybe"}, + {"employee", "upgrade-status", "--dept-id", "abc"}, + {"employee", "upgrade-status", "--offset", "abc"}, + {"employee", "upgrade-status", "--size", "abc"}, + + // ── alumni ─────────────────────────────────────────── + {"alumni", "get-dept-tree"}, + {"alumni", "get-dept-tree", "--alumni-dept-id", "abc"}, + {"alumni", "get-info"}, + {"alumni", "get-info", "--alumni-dept-id", "abc"}, + {"alumni", "list"}, + {"alumni", "list", "--alumni-dept-id", "abc", "--order-field", "f", "--ordering", "asc"}, + {"alumni", "list", "--alumni-dept-id", "1"}, + {"alumni", "list", "--alumni-dept-id", "1", "--order-field", "f"}, + {"alumni", "list", "--alumni-dept-id", "1", "--order-field", "f", "--ordering", "asc", "--offset", "abc"}, + {"alumni", "query"}, + {"alumni", "search"}, + {"alumni", "search", "--keyword", "x", "--offset", "abc"}, + {"alumni", "list-unaccepted"}, + {"alumni", "list-unaccepted", "--alumni-dept-id", "abc"}, + {"alumni", "list-unaccepted", "--alumni-dept-id", "1", "--offset", "abc"}, + {"alumni", "get-group"}, + {"alumni", "get-group", "--alumni-dept-id", "abc"}, + {"alumni", "create-dept"}, + {"alumni", "create-dept", "--alumni-dept-id", "abc", "--dept-name", "D"}, + {"alumni", "create-dept", "--alumni-dept-id", "1"}, + {"alumni", "update-dept"}, + {"alumni", "update-dept", "--alumni-dept-id", "abc", "--dept-name", "D"}, + {"alumni", "update-dept", "--alumni-dept-id", "1"}, + {"alumni", "delete-dept"}, + {"alumni", "delete-dept", "--alumni-dept-id", "abc"}, + {"alumni", "update-managers"}, + {"alumni", "update-managers", "--alumni-dept-id", "abc", "--admin-user-ids", "u"}, + {"alumni", "update-managers", "--alumni-dept-id", "1"}, + {"alumni", "update-managers", "--alumni-dept-id", "1", "--admin-user-ids", ",,"}, + {"alumni", "add-alumnus"}, + {"alumni", "add-alumnus", "--name", "X"}, + {"alumni", "add-alumnus", "--name", "X", "--mobile", "m"}, + {"alumni", "add-alumnus", "--name", "X", "--mobile", "m", "--dept-ids", "abc"}, + {"alumni", "add-alumnus", "--name", "X", "--mobile", "m", "--dept-ids", ",,"}, + {"alumni", "update-alumnus"}, + {"alumni", "update-alumnus", "--staff-id", "S1"}, + {"alumni", "update-alumnus", "--staff-id", "S1", "--name", "X"}, + {"alumni", "update-alumnus", "--staff-id", "S1", "--name", "X", "--dept-ids", "abc"}, + {"alumni", "update-alumnus", "--staff-id", "S1", "--name", "X", "--dept-ids", ",,"}, + {"alumni", "remove-alumnus"}, + {"alumni", "remove-alumnus", "--staff-id", "S1"}, + {"alumni", "remove-alumnus", "--staff-id", "S1", "--alumni-dept-id", "abc"}, + {"alumni", "cancel-invite"}, + {"alumni", "cancel-invite", "--alumni-dept-id", "abc", "--staff-ids", "s"}, + {"alumni", "cancel-invite", "--alumni-dept-id", "1"}, + {"alumni", "cancel-invite", "--alumni-dept-id", "1", "--staff-ids", ",,"}, + {"alumni", "create-group"}, + {"alumni", "create-group", "--alumni-dept-id", "abc"}, + {"alumni", "disband-group"}, + {"alumni", "disband-group", "--alumni-dept-id", "abc"}, + {"alumni", "create-alumni-org"}, + {"alumni", "add-alumni-org-main-admins"}, + {"alumni", "add-alumni-org-main-admins", "--admin-user-ids", ",,"}, + + // ── graduate ───────────────────────────────────────── + {"graduate", "query-graduate-depts"}, + {"graduate", "query-graduate-depts", "--dept-id", "abc"}, + {"graduate", "query-graduate-depts", "--dept-id", "1", "--graduate-year", "abc"}, + {"graduate", "query-graduate-sub-depts"}, + {"graduate", "query-graduate-sub-depts", "--dept-id", "abc"}, + {"graduate", "query-page-graduate-users"}, + {"graduate", "query-page-graduate-users", "--dept-id", "abc"}, + {"graduate", "query-page-graduate-users", "--dept-id", "1", "--offset", "abc"}, + {"graduate", "get-task-result"}, + {"graduate", "query-restore-sub-depts"}, + {"graduate", "query-restore-sub-depts", "--dept-id", "abc"}, + {"graduate", "query-dept-deleted-emps"}, + {"graduate", "query-dept-deleted-emps", "--dept-id", "abc"}, + {"graduate", "query-dept-deleted-emps", "--dept-id", "1", "--offset", "abc"}, + {"graduate", "search-graduate"}, + {"graduate", "search-graduate", "--keyword", "x", "--offset", "abc"}, + {"graduate", "commit-graduate"}, + {"graduate", "commit-graduate", "--graduate-dept-ids", "abc"}, + {"graduate", "commit-graduate", "--graduate-dept-ids", ",,"}, + {"graduate", "commit-graduate", "--graduate-dept-ids", "1,2"}, + {"graduate", "commit-graduate", "--graduate-dept-ids", "1,2", "--graduate-year", "abc"}, + {"graduate", "all-graduate"}, + {"graduate", "all-graduate", "--graduate-year", "abc"}, + {"graduate", "batch-graduate"}, + {"graduate", "batch-graduate", "--dept-id", "abc"}, + {"graduate", "batch-graduate", "--dept-id", "1"}, + {"graduate", "batch-graduate", "--dept-id", "1", "--staff-ids", ",,"}, + {"graduate", "delete-and-graduate"}, + {"graduate", "delete-and-graduate", "--dept-id", "abc"}, + {"graduate", "delete-and-graduate", "--dept-id", "1"}, + {"graduate", "delete-and-graduate", "--dept-id", "1", "--staff-ids", ",,"}, + {"graduate", "batch-delete-pending"}, + {"graduate", "batch-delete-pending", "--dept-id", "abc"}, + {"graduate", "batch-delete-pending", "--dept-id", "1"}, + {"graduate", "batch-delete-pending", "--dept-id", "1", "--staff-ids", ",,"}, + {"graduate", "batch-update-pending"}, + {"graduate", "batch-update-pending", "--dept-id", "abc"}, + {"graduate", "batch-update-pending", "--dept-id", "1"}, + {"graduate", "batch-update-pending", "--dept-id", "1", "--staff-ids", ",,"}, + {"graduate", "batch-update-pending", "--dept-id", "1", "--staff-ids", "s1"}, + {"graduate", "batch-update-pending", "--dept-id", "1", "--staff-ids", "s1", "--graduate-year", "abc"}, + {"graduate", "commit-restore"}, + {"graduate", "commit-restore", "--graduate-dept-ids", "abc"}, + {"graduate", "commit-restore", "--graduate-dept-ids", ",,"}, + + // ── group ──────────────────────────────────────────── + {"group", "query-group-rule", "--offset", "abc"}, + {"group", "query-group-rule", "--size", "abc"}, + {"group", "query-preview-data", "--offset", "abc"}, + {"group", "query-preview-data", "--size", "abc"}, + {"group", "create-group-rule"}, + {"group", "create-group-rule", "--name", "X"}, + {"group", "create-group-rule", "--name", "X", "--tag-code", "T"}, + {"group", "create-group-rule", "--name", "X", "--tag-code", "T", "--dept-type", "college", "--auto-admin", "maybe"}, + {"group", "delete-group-rule"}, + {"group", "delete-group-rule", "--rule-id", "abc"}, + {"group", "enable-group-rule"}, + {"group", "enable-group-rule", "--rule-id", "abc"}, + {"group", "disable-group-rule"}, + {"group", "disable-group-rule", "--rule-id", "abc"}, + } + + for _, args := range cases { + root := newCollegeContactCommand() + if err := executeCommand(root, args...); err == nil { + t.Errorf("%v: expected non-nil error, got nil", args) + } + } +} + +// TestCollegeContactDestructiveConfirmGate covers the `return err` branch of +// every destructive leaf command's confirm gate. A non-dry-run caller is +// installed (so collegeContactConfirmDestructive does not short-circuit) and +// each command is run with valid required flags but without --yes, so the +// confirm gate returns an error that the RunE closure propagates. +func TestCollegeContactDestructiveConfirmGate(t *testing.T) { + caller := &recruitCaptureCaller{dryRun: false} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + + cases := [][]string{ + {"college-contact", "alumni", "delete-dept", "--alumni-dept-id", "1"}, + {"college-contact", "alumni", "remove-alumnus", "--staff-id", "S1", "--alumni-dept-id", "1"}, + {"college-contact", "alumni", "cancel-invite", "--alumni-dept-id", "1", "--staff-ids", "s1,s2"}, + {"college-contact", "alumni", "disband-group", "--alumni-dept-id", "1"}, + {"college-contact", "graduate", "commit-graduate", "--graduate-dept-ids", "1,2", "--graduate-year", "2026"}, + {"college-contact", "graduate", "all-graduate", "--graduate-year", "2026"}, + {"college-contact", "graduate", "batch-graduate", "--dept-id", "1", "--staff-ids", "s1,s2"}, + {"college-contact", "graduate", "delete-and-graduate", "--dept-id", "1", "--staff-ids", "s1,s2"}, + {"college-contact", "graduate", "batch-delete-pending", "--dept-id", "1", "--staff-ids", "s1,s2"}, + {"college-contact", "graduate", "batch-update-pending", "--dept-id", "1", "--staff-ids", "s1,s2", "--graduate-year", "2026"}, + {"college-contact", "graduate", "commit-restore", "--graduate-dept-ids", "1,2"}, + {"college-contact", "group", "delete-group-rule", "--rule-id", "1"}, + {"college-contact", "group", "execute-group-rule"}, + } + + for _, args := range cases { + root := newCollegeContactTestRoot() + root.SetArgs(args) + err := root.Execute() + if err == nil { + t.Errorf("%v: expected confirm-gate error without --yes, got nil", args) + continue + } + if !strings.Contains(err.Error(), "不可逆操作") { + t.Errorf("%v: expected 不可逆操作 gate error, got: %v", args, err) + } + } +} + +// withCollegeContactDispatchCaller installs a non-dry-run capture caller so +// commands go through the full dispatch path (deps.Caller.CallTool) and we can +// verify the productID, tool name, and args passed to callMCPToolOnServer. +func withCollegeContactDispatchCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +// TestCollegeContactDispatch verifies that representative commands from each +// group dispatch to the correct MCP tool with the expected productID and args. +func TestCollegeContactDispatch(t *testing.T) { + type dispatchCase struct { + name string + args []string + wantTool string + wantProd string + checkArgs func(t *testing.T, args map[string]any) + } + + cases := []dispatchCase{ + { + name: "dept get-standard-structure", + args: []string{"college-contact", "dept", "get-standard-structure"}, + wantTool: "get_college_standard_structure", + wantProd: "college-contact", + }, + { + name: "dept get-detail", + args: []string{"college-contact", "dept", "get-detail", "--dept-id", "123"}, + wantTool: "get_college_dept_detail", + wantProd: "college-contact", + checkArgs: func(t *testing.T, args map[string]any) { + input := args["input"].(map[string]any) + if input["deptId"] != int64(123) { + t.Errorf("deptId = %v (%T), want int64(123)", input["deptId"], input["deptId"]) + } + }, + }, + { + name: "dept create", + args: []string{"college-contact", "dept", "create", "--super-id", "100", "--stru-dept-id", "200", "--name", "X", "--dept-type", "college", "--create-dept-group", "true"}, + wantTool: "create_college_contact_dept", + wantProd: "college-contact", + }, + { + name: "employee get-detail", + args: []string{"college-contact", "employee", "get-detail", "--staff-id", "S1"}, + wantTool: "get_employee_detail", + wantProd: "college-contact", + checkArgs: func(t *testing.T, args map[string]any) { + input := args["input"].(map[string]any) + if input["staffId"] != "S1" { + t.Errorf("staffId = %v, want S1", input["staffId"]) + } + }, + }, + { + name: "alumni get-dept-tree", + args: []string{"college-contact", "alumni", "get-dept-tree", "--alumni-dept-id", "123"}, + wantTool: "get_alumni_dept_tree", + wantProd: "college-contact", + checkArgs: func(t *testing.T, args map[string]any) { + input := args["input"].(map[string]any) + if input["alumniDeptId"] != int64(123) { + t.Errorf("alumniDeptId = %v (%T), want int64(123)", input["alumniDeptId"], input["alumniDeptId"]) + } + }, + }, + { + name: "graduate query-graduate-years", + args: []string{"college-contact", "graduate", "query-graduate-years"}, + wantTool: "query_graduate_years", + wantProd: "college-contact", + }, + { + name: "group query-group-rule", + args: []string{"college-contact", "group", "query-group-rule"}, + wantTool: "query_group_rule", + wantProd: "college-contact", + }, + { + name: "dept delete with --yes", + args: []string{"college-contact", "dept", "delete", "--dept-id", "123", "--yes"}, + wantTool: "delete_college_contact_dept", + wantProd: "college-contact", + checkArgs: func(t *testing.T, args map[string]any) { + input := args["input"].(map[string]any) + if input["deptId"] != int64(123) { + t.Errorf("deptId = %v (%T), want int64(123)", input["deptId"], input["deptId"]) + } + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + caller := withCollegeContactDispatchCaller(t) + + root := &cobra.Command{Use: "dws"} + root.PersistentFlags().BoolP("yes", "y", false, "跳过确认提示") + root.AddCommand(newCollegeContactCommand()) + root.SetArgs(tc.args) + + if err := root.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + if caller.productID != tc.wantProd { + t.Errorf("productID = %q, want %q", caller.productID, tc.wantProd) + } + if caller.tool != tc.wantTool { + t.Errorf("tool = %q, want %q", caller.tool, tc.wantTool) + } + if tc.checkArgs != nil { + tc.checkArgs(t, caller.args) + } + }) + } +} diff --git a/internal/helpers/edu_app.go b/internal/helpers/edu_app.go new file mode 100644 index 00000000..0117bd31 --- /dev/null +++ b/internal/helpers/edu_app.go @@ -0,0 +1,3546 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package helpers + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" +) + +// ────────────────────────────────────────────────────────── +// dws edu-app — 家校应用 +// 共 42 个工具,按 message / task / report / notice / circle / card / diploma / homework 分组 +// ────────────────────────────────────────────────────────── + +func newEduAppCommand() *cobra.Command { + contract.RegisterProductDecl(contract.ProductDecl{ + ID: "edu-app", + Selection: contract.ProductSelectionDecl{ + AgentSummary: "家校应用:AI消息总结、家校任务查询、成绩单管理、通知确认、班级圈动态、打卡管理、奖状颁发、作业管理", + UseWhen: []string{ + "用户要管理钉钉家校应用中的消息总结、任务、成绩单、通知、班级圈、打卡、奖状或作业。", + }, + AvoidWhen: []string{ + "家校通讯录用 edu-contact;班级群用 edu-group;家庭群用 edu-familygroup。", + }, + }, + }) + root := &cobra.Command{ + Use: "edu-app", + Short: "家校应用", + Long: `钉钉家校应用:AI消息总结、家校任务查询、成绩单管理、通知确认、奖状颁发、作业管理等。`, + Hidden: true, + RunE: groupRunE, + } + + // ════════════════════════════════════════════════════════════ + // message 子命令组 — AI 消息 + // ════════════════════════════════════════════════════════════ + + messageCmd := &cobra.Command{Use: "message", Short: "AI 消息管理", RunE: groupRunE} + + messageSummaryListCmd := &cobra.Command{ + Use: "summary-list", + Short: "获取AI消息总结列表", + Long: `查询指定群下用户的AI消息总结任务列表。 +--target-role 取值:guardian(家长)、student(学生)。 +--status 取值:0(未处理)、1(已处理)。`, + Example: ` dws edu-app message summary-list --class-id 12345 --cid cidxxx --target-role guardian --status 0 + dws edu-app message summary-list --class-id 12345 --cid cidxxx --target-role student --status 1 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + classID, err := eduAppRequiredIntFlag(cmd, "class-id") + if err != nil { + return err + } + cid, _ := cmd.Flags().GetString("cid") + cid = strings.TrimSpace(cid) + if cid == "" { + return fmt.Errorf("--cid 为必填参数") + } + targetRole, _ := cmd.Flags().GetString("target-role") + targetRole = strings.TrimSpace(targetRole) + if targetRole == "" { + return fmt.Errorf("--target-role 为必填参数") + } + statusStr, _ := cmd.Flags().GetString("status") + statusStr = strings.TrimSpace(statusStr) + if statusStr == "" { + return fmt.Errorf("--status 为必填参数") + } + status, err := strconv.ParseInt(statusStr, 10, 64) + if err != nil { + return fmt.Errorf("--status 须为整数: %w", err) + } + return callMCPToolOnServer("edu-app", "get_ai_message_summary_list", map[string]any{ + "input": map[string]any{ + "classId": classID, + "cid": cid, + "targetRole": targetRole, + "status": status, + }, + }) + }, + } + DeclareLeafMetadata(messageSummaryListCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "get_ai_message_summary_list", + CanonicalPath: "edu-app.get_ai_message_summary_list", + CLIPath: "edu-app message summary-list", + PrimaryCLIPath: "edu-app message summary-list", + }, + Description: "获取AI消息总结列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "get_ai_message_summary_list"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "获取AI消息总结列表", + UseWhen: []string{"需要查询指定群下用户的AI消息总结任务列表时"}, + AvoidWhen: []string{"查询家校任务用 task publish-list"}, + Examples: []string{ + "dws edu-app message summary-list --class-id 12345 --cid cidxxx --target-role guardian --status 0 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "cid", Property: "input.cid", Required: boolPtr(true)}, + {Name: "target-role", Property: "input.targetRole", Required: boolPtr(true)}, + {Name: "status", Property: "input.status", Required: boolPtr(true)}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // task 子命令组 — 家校任务 + // ════════════════════════════════════════════════════════════ + + taskCmd := &cobra.Command{Use: "task", Short: "家校任务管理", RunE: groupRunE} + + taskPublishListCmd := &cobra.Command{ + Use: "publish-list", + Short: "查询老师发布的任务列表", + Long: `查询当前老师已发布的家校任务列表,支持分页。 +--task-sources 可选值(逗号分隔):EDU_HOMEWORK(家校本)、EDU_CARD(打卡)、EDU_NOTICE(通知)、EDU_SR(成绩单)、EDU_DIPLOMA(奖状),不传时默认查询全部类型。`, + Example: ` dws edu-app task publish-list + dws edu-app task publish-list --limit 10 --need-statistic + dws edu-app task publish-list --task-sources EDU_HOMEWORK,EDU_NOTICE -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if v, _ := cmd.Flags().GetInt64("cursor"); cmd.Flags().Changed("cursor") { + input["cursor"] = v + } + if v, _ := cmd.Flags().GetInt64("limit"); cmd.Flags().Changed("limit") { + input["pageSize"] = v + } + if v, _ := cmd.Flags().GetBool("need-statistic"); v { + input["needStatistic"] = true + } + if raw, _ := cmd.Flags().GetString("task-sources"); strings.TrimSpace(raw) != "" { + input["taskSourceList"] = eduAppParseCSV(raw) + } + return callMCPToolOnServer("edu-app", "query_publish_task", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(taskPublishListCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_publish_task", + CanonicalPath: "edu-app.query_publish_task", + CLIPath: "edu-app task publish-list", + PrimaryCLIPath: "edu-app task publish-list", + }, + Description: "查询老师发布的任务列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_publish_task"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询老师发布的家校任务列表", + UseWhen: []string{"需要查询当前老师已发布的家校任务列表时"}, + AvoidWhen: []string{"查询全部任务用 task all-list;查询学生待办用 task student-list"}, + Examples: []string{ + "dws edu-app task publish-list --limit 10 --need-statistic --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "cursor", Property: "input.cursor"}, + {Name: "limit", Property: "input.pageSize"}, + {Name: "need-statistic", Property: "input.needStatistic"}, + {Name: "task-sources", Property: "input.taskSourceList"}, + }, + }, + }) + + taskAllListCmd := &cobra.Command{ + Use: "all-list", + Short: "查询全部家校任务列表", + Long: `查询组织内全部家校任务列表,支持分页,仅限老师角色调用。 +--task-sources 可选值(逗号分隔):EDU_HOMEWORK、EDU_CARD、EDU_NOTICE、EDU_SR、EDU_DIPLOMA,不传时默认查询全部类型。`, + Example: ` dws edu-app task all-list --biz-id 12345 + dws edu-app task all-list --biz-id 12345 --limit 10 --need-statistic -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + bizID, err := eduAppRequiredIntFlag(cmd, "biz-id") + if err != nil { + return err + } + input := map[string]any{"bizId": bizID} + if v, _ := cmd.Flags().GetInt64("cursor"); cmd.Flags().Changed("cursor") { + input["cursor"] = v + } + if v, _ := cmd.Flags().GetInt64("limit"); cmd.Flags().Changed("limit") { + input["pageSize"] = v + } + if v, _ := cmd.Flags().GetBool("need-statistic"); v { + input["needStatistic"] = true + } + if raw, _ := cmd.Flags().GetString("task-sources"); strings.TrimSpace(raw) != "" { + input["taskSourceList"] = eduAppParseCSV(raw) + } + return callMCPToolOnServer("edu-app", "query_all_task", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(taskAllListCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_all_task", + CanonicalPath: "edu-app.query_all_task", + CLIPath: "edu-app task all-list", + PrimaryCLIPath: "edu-app task all-list", + }, + Description: "查询全部家校任务列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_all_task"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询组织内全部家校任务列表", + UseWhen: []string{"需要查询组织内全部家校任务列表时"}, + AvoidWhen: []string{"查询老师发布的任务用 task publish-list;查询学生待办用 task student-list"}, + Examples: []string{ + "dws edu-app task all-list --biz-id 12345 --limit 10 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "biz-id", Property: "input.bizId", Required: boolPtr(true)}, + {Name: "cursor", Property: "input.cursor"}, + {Name: "limit", Property: "input.pageSize"}, + {Name: "need-statistic", Property: "input.needStatistic"}, + {Name: "task-sources", Property: "input.taskSourceList"}, + }, + }, + }) + + taskStudentListCmd := &cobra.Command{ + Use: "student-list", + Short: "查询学生待办任务列表", + Long: `查询多个学生的待办任务列表,支持分页,适用于家长/学生角色调用。 +--students 为 JSON 数组,每个元素需包含 userId 和 bizId(班级ID)。 +--query-all 为 false 时只查待办任务,为 true 时查询全部任务(含已完成)。 +--task-sources 可选值(逗号分隔):EDU_HOMEWORK、EDU_CARD、EDU_NOTICE、EDU_SR、EDU_DIPLOMA。`, + Example: ` dws edu-app task student-list --students '[{"userId":"uid1","bizId":"12345"}]' --query-all=false + dws edu-app task student-list --students '[{"userId":"uid1","bizId":"12345"},{"userId":"uid2","bizId":"67890"}]' --query-all=true -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + studentsRaw, _ := cmd.Flags().GetString("students") + studentsRaw = strings.TrimSpace(studentsRaw) + if studentsRaw == "" { + return fmt.Errorf("--students 为必填参数") + } + var students []map[string]any + if err := json.Unmarshal([]byte(studentsRaw), &students); err != nil { + return fmt.Errorf("--students JSON 格式错误: %w", err) + } + queryAll, _ := cmd.Flags().GetBool("query-all") + input := map[string]any{ + "students": students, + "queryAll": queryAll, + } + if v, _ := cmd.Flags().GetString("cursor"); strings.TrimSpace(v) != "" { + input["cursor"] = v + } + if v, _ := cmd.Flags().GetString("limit"); strings.TrimSpace(v) != "" { + input["pageSize"] = v + } + if raw, _ := cmd.Flags().GetString("task-sources"); strings.TrimSpace(raw) != "" { + input["taskSourceList"] = eduAppParseCSV(raw) + } + return callMCPToolOnServer("edu-app", "query_multi_user_sub_tasks", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(taskStudentListCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_multi_user_sub_tasks", + CanonicalPath: "edu-app.query_multi_user_sub_tasks", + CLIPath: "edu-app task student-list", + PrimaryCLIPath: "edu-app task student-list", + }, + Description: "查询学生待办任务列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_multi_user_sub_tasks"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询多个学生的待办任务列表", + UseWhen: []string{"需要查询学生的待办或全部家校任务时"}, + AvoidWhen: []string{"查询老师发布的任务用 task publish-list"}, + Examples: []string{ + "dws edu-app task student-list --students '[{\"userId\":\"uid1\",\"bizId\":\"12345\"}]' --query-all=false --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "students", Property: "input.students", Required: boolPtr(true)}, + {Name: "query-all", Property: "input.queryAll"}, + {Name: "cursor", Property: "input.cursor"}, + {Name: "limit", Property: "input.pageSize"}, + {Name: "task-sources", Property: "input.taskSourceList"}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // report 子命令组 — 成绩单 + // ════════════════════════════════════════════════════════════ + + reportCmd := &cobra.Command{Use: "report", Short: "成绩单管理", RunE: groupRunE} + + reportGetCmd := &cobra.Command{ + Use: "get", + Short: "获取成绩单列表", + Long: `按成绩单ID列表批量查询成绩单信息。`, + Example: ` dws edu-app report get --ids 1001,1002,1003 + dws edu-app report get --ids 1001 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + raw, _ := cmd.Flags().GetString("ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--ids 为必填参数") + } + ids, err := eduAppParseIntCSV(raw) + if err != nil { + return fmt.Errorf("--ids 须为逗号分隔的整数列表: %w", err) + } + return callMCPToolOnServer("edu-app", "get_report", map[string]any{ + "input": map[string]any{"schoolReportIdList": ids}, + }) + }, + } + DeclareLeafMetadata(reportGetCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "get_report", + CanonicalPath: "edu-app.get_report", + CLIPath: "edu-app report get", + PrimaryCLIPath: "edu-app report get", + }, + Description: "获取成绩单列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "get_report"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按成绩单ID列表批量查询成绩单信息", + UseWhen: []string{"需要按成绩单ID批量查询成绩单信息时"}, + AvoidWhen: []string{"查询老师创建的成绩单用 report by-teacher;查询学生成绩用 report by-student-detail"}, + Examples: []string{ + "dws edu-app report get --ids 1001,1002,1003 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "ids", Property: "input.schoolReportIdList", Required: boolPtr(true)}, + }, + }, + }) + + reportByTeacherCmd := &cobra.Command{ + Use: "by-teacher", + Short: "查询老师创建的成绩单列表", + Long: `查询指定老师创建的成绩单列表,支持分页。 +--status 取值:0(未发布)、1(已发布),不传则查询所有状态。`, + Example: ` dws edu-app report by-teacher + dws edu-app report by-teacher --status 1 --page 1 --limit 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + pageNumber, _ := cmd.Flags().GetInt64("page") + if pageNumber <= 0 { + pageNumber = 1 + } + input["pageNumber"] = pageNumber + pageSize, _ := cmd.Flags().GetInt64("limit") + if pageSize <= 0 { + pageSize = 20 + } + input["pageSize"] = pageSize + if cmd.Flags().Changed("status") { + statusVal, _ := cmd.Flags().GetInt64("status") + input["status"] = statusVal + } + return callMCPToolOnServer("edu-app", "list_report_by_teacher", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(reportByTeacherCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "list_report_by_teacher", + CanonicalPath: "edu-app.list_report_by_teacher", + CLIPath: "edu-app report by-teacher", + PrimaryCLIPath: "edu-app report by-teacher", + }, + Description: "查询老师创建的成绩单列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "list_report_by_teacher"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询老师创建的成绩单列表", + UseWhen: []string{"需要查询指定老师创建的成绩单列表时"}, + AvoidWhen: []string{"查询学生收到的成绩单用 report by-student-list"}, + Examples: []string{ + "dws edu-app report by-teacher --status 1 --page 1 --limit 20 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "page", Property: "input.pageNumber"}, + {Name: "limit", Property: "input.pageSize"}, + {Name: "status", Property: "input.status"}, + }, + }, + }) + + reportByClassCmd := &cobra.Command{ + Use: "by-class", + Short: "查询班级学生成绩明细", + Long: `按成绩单ID和班级ID查询该班级学生的成绩明细。 +返回学生成绩明细列表及阅读状态汇总统计(totalCount/readCount/unreadCount)。`, + Example: ` dws edu-app report by-class --report-id 1001 --class-id 12345 + dws edu-app report by-class --report-id 1001 --class-id 12345 --student-ids uid1,uid2 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + reportIDStr, _ := cmd.Flags().GetString("report-id") + reportIDStr = strings.TrimSpace(reportIDStr) + if reportIDStr == "" { + return fmt.Errorf("--report-id 为必填参数") + } + reportID, err := strconv.ParseInt(reportIDStr, 10, 64) + if err != nil { + return fmt.Errorf("--report-id 须为整数: %w", err) + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + input := map[string]any{ + "schoolReportId": reportID, + "classId": classID, + } + if raw, _ := cmd.Flags().GetString("student-ids"); strings.TrimSpace(raw) != "" { + input["studentIdList"] = eduAppParseCSV(raw) + } + return callMCPToolOnServer("edu-app", "query_detail_by_class", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(reportByClassCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_detail_by_class", + CanonicalPath: "edu-app.query_detail_by_class", + CLIPath: "edu-app report by-class", + PrimaryCLIPath: "edu-app report by-class", + }, + Description: "查询班级学生成绩明细", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_detail_by_class"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按成绩单ID和班级ID查询学生成绩明细", + UseWhen: []string{"需要按成绩单ID和班级ID查询班级学生的成绩明细时"}, + AvoidWhen: []string{"查询单个学生成绩用 report by-student-detail"}, + Examples: []string{ + "dws edu-app report by-class --report-id 1001 --class-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "report-id", Property: "input.schoolReportId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "student-ids", Property: "input.studentIdList"}, + }, + }, + }) + + reportByStudentListCmd := &cobra.Command{ + Use: "by-student-list", + Short: "查询学生收到的成绩单列表", + Long: `查询指定学生收到的已发布成绩单列表,支持分页。`, + Example: ` dws edu-app report by-student-list --class-id 12345 --student-id uid1 + dws edu-app report by-student-list --class-id 12345 --student-id uid1 --page 1 --limit 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + studentID, _ := cmd.Flags().GetString("student-id") + studentID = strings.TrimSpace(studentID) + if studentID == "" { + return fmt.Errorf("--student-id 为必填参数") + } + pageNumber, _ := cmd.Flags().GetInt64("page") + if pageNumber <= 0 { + pageNumber = 1 + } + pageSize, _ := cmd.Flags().GetInt64("limit") + if pageSize <= 0 { + pageSize = 20 + } + return callMCPToolOnServer("edu-app", "list_report_by_student", map[string]any{ + "input": map[string]any{ + "classId": classID, + "studentId": studentID, + "pageNumber": pageNumber, + "pageSize": pageSize, + }, + }) + }, + } + DeclareLeafMetadata(reportByStudentListCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "list_report_by_student", + CanonicalPath: "edu-app.list_report_by_student", + CLIPath: "edu-app report by-student-list", + PrimaryCLIPath: "edu-app report by-student-list", + }, + Description: "查询学生收到的成绩单列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "list_report_by_student"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询学生收到的已发布成绩单列表", + UseWhen: []string{"需要查询指定学生收到的成绩单列表时"}, + AvoidWhen: []string{"查询单个学生成绩明细用 report by-student-detail"}, + Examples: []string{ + "dws edu-app report by-student-list --class-id 12345 --student-id uid1 --page 1 --limit 20 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "student-id", Property: "input.studentId", Required: boolPtr(true)}, + {Name: "page", Property: "input.pageNumber"}, + {Name: "limit", Property: "input.pageSize"}, + }, + }, + }) + + reportByStudentDetailCmd := &cobra.Command{ + Use: "by-student-detail", + Short: "查询学生成绩明细", + Long: `按成绩单ID查询指定学生的成绩明细。 +返回该学生的成绩明细列表,包含各科成绩及已读状态(readStatus:1-未读、2-已读)。`, + Example: ` dws edu-app report by-student-detail --report-id 1001 --student-id uid1 --class-id 12345 + dws edu-app report by-student-detail --report-id 1001 --student-id uid1 --class-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + reportIDStr, _ := cmd.Flags().GetString("report-id") + reportIDStr = strings.TrimSpace(reportIDStr) + if reportIDStr == "" { + return fmt.Errorf("--report-id 为必填参数") + } + reportID, err := strconv.ParseInt(reportIDStr, 10, 64) + if err != nil { + return fmt.Errorf("--report-id 须为整数: %w", err) + } + studentID, _ := cmd.Flags().GetString("student-id") + studentID = strings.TrimSpace(studentID) + if studentID == "" { + return fmt.Errorf("--student-id 为必填参数") + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + return callMCPToolOnServer("edu-app", "query_detail_by_student", map[string]any{ + "input": map[string]any{ + "schoolReportId": reportID, + "studentId": studentID, + "classId": classID, + }, + }) + }, + } + DeclareLeafMetadata(reportByStudentDetailCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_detail_by_student", + CanonicalPath: "edu-app.query_detail_by_student", + CLIPath: "edu-app report by-student-detail", + PrimaryCLIPath: "edu-app report by-student-detail", + }, + Description: "查询学生成绩明细", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_detail_by_student"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按成绩单ID查询指定学生的成绩明细", + UseWhen: []string{"需要查询指定学生的成绩明细时"}, + AvoidWhen: []string{"查询班级整体成绩用 report by-class"}, + Examples: []string{ + "dws edu-app report by-student-detail --report-id 1001 --student-id uid1 --class-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "report-id", Property: "input.schoolReportId", Required: boolPtr(true)}, + {Name: "student-id", Property: "input.studentId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // notice 子命令组 — 通知 + // ════════════════════════════════════════════════════════════ + + noticeCmd := &cobra.Command{Use: "notice", Short: "通知管理", RunE: groupRunE} + + noticeConfirmCmd := &cobra.Command{ + Use: "confirm", + Short: "确认收到通知", + Long: `家长端确认收到通知。返回确认结果(confirmed: true 表示确认成功)。`, + Example: ` dws edu-app notice confirm --notice-id nid1 --student-id uid1 + dws edu-app notice confirm --notice-id nid1 --student-id uid1 --parent-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + noticeID, _ := cmd.Flags().GetString("notice-id") + noticeID = strings.TrimSpace(noticeID) + if noticeID == "" { + return fmt.Errorf("--notice-id 为必填参数") + } + studentID, _ := cmd.Flags().GetString("student-id") + studentID = strings.TrimSpace(studentID) + if studentID == "" { + return fmt.Errorf("--student-id 为必填参数") + } + input := map[string]any{ + "noticeId": noticeID, + "studentId": studentID, + } + if v, _ := cmd.Flags().GetString("device-id"); strings.TrimSpace(v) != "" { + input["deviceId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("parent-name"); strings.TrimSpace(v) != "" { + input["parentName"] = strings.TrimSpace(v) + } + if cmd.Flags().Changed("update-sign") { + updateSign, _ := cmd.Flags().GetBool("update-sign") + input["updateSign"] = updateSign + } + return callMCPToolOnServer("edu-app", "confirm_notice", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(noticeConfirmCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "confirm_notice", + CanonicalPath: "edu-app.confirm_notice", + CLIPath: "edu-app notice confirm", + PrimaryCLIPath: "edu-app notice confirm", + }, + Description: "确认收到通知", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "confirm_notice"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "家长端确认收到通知", + UseWhen: []string{"需要确认收到家校通知时"}, + AvoidWhen: []string{"查询通知详情用 notice get;查询确认状态用 notice confirm-status"}, + Examples: []string{ + "dws edu-app notice confirm --notice-id nid1 --student-id uid1 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "notice-id", Property: "input.noticeId", Required: boolPtr(true)}, + {Name: "student-id", Property: "input.studentId", Required: boolPtr(true)}, + {Name: "device-id", Property: "input.deviceId"}, + {Name: "parent-name", Property: "input.parentName"}, + {Name: "update-sign", Property: "input.updateSign"}, + }, + }, + }) + + noticeCreateCmd := &cobra.Command{ + Use: "create", + Short: "创建并发布通知", + Long: `创建并发布一条家校通知。 +--identifer 为幂等字段(必填),建议格式:orgId-staffId-UUID。 +--content 为通知内容(必填)。 +--class-ids、--class-names 为逗号分隔的班级列表,--class-selected-students 为 JSON 对象。`, + Example: ` dws edu-app notice create --identifer org1-staff1-uuid --content "明天放假" + dws edu-app notice create --identifer org1-staff1-uuid --title "放假通知" --content "明天放假" --class-ids 12345,67890 --target-role guardian --is-signed true -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + identifer, _ := cmd.Flags().GetString("identifer") + identifer = strings.TrimSpace(identifer) + if identifer == "" { + return fmt.Errorf("--identifer 为必填参数") + } + content, _ := cmd.Flags().GetString("content") + content = strings.TrimSpace(content) + if content == "" { + return fmt.Errorf("--content 为必填参数") + } + input := map[string]any{ + "identifer": identifer, + "content": content, + } + if v, _ := cmd.Flags().GetString("title"); strings.TrimSpace(v) != "" { + input["title"] = strings.TrimSpace(v) + } + if raw, _ := cmd.Flags().GetString("class-ids"); strings.TrimSpace(raw) != "" { + input["classIds"] = eduAppParseCSV(raw) + } + if raw, _ := cmd.Flags().GetString("class-names"); strings.TrimSpace(raw) != "" { + input["classNames"] = eduAppParseCSV(raw) + } + if raw, _ := cmd.Flags().GetString("class-selected-students"); strings.TrimSpace(raw) != "" { + var obj any + if err := json.Unmarshal([]byte(raw), &obj); err != nil { + return fmt.Errorf("--class-selected-students JSON 格式错误: %w", err) + } + input["classSelectedStudents"] = obj + } + if v, _ := cmd.Flags().GetString("type"); strings.TrimSpace(v) != "" { + input["type"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("scope"); strings.TrimSpace(v) != "" { + input["scope"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("target-role"); strings.TrimSpace(v) != "" { + input["targetRole"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("is-signed"); strings.TrimSpace(v) != "" { + input["isSigned"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("photo"); strings.TrimSpace(v) != "" { + input["photo"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("media"); strings.TrimSpace(v) != "" { + input["media"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("audio"); strings.TrimSpace(v) != "" { + input["audio"] = strings.TrimSpace(v) + } + if cmd.Flags().Changed("send-ding") { + v, _ := cmd.Flags().GetBool("send-ding") + input["sendDing"] = v + } + if v, _ := cmd.Flags().GetString("scheduled-release"); strings.TrimSpace(v) != "" { + input["scheduledRelease"] = strings.TrimSpace(v) + } + if cmd.Flags().Changed("notice-deadline") { + v, _ := cmd.Flags().GetInt64("notice-deadline") + input["noticeDeadline"] = v + } + if v, _ := cmd.Flags().GetString("notice-deadline-open"); strings.TrimSpace(v) != "" { + input["noticeDeadlineOpen"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("notice-deadline-setting"); strings.TrimSpace(v) != "" { + input["noticeDeadlineSetting"] = strings.TrimSpace(v) + } + if raw, _ := cmd.Flags().GetString("attributes"); strings.TrimSpace(raw) != "" { + var obj any + if err := json.Unmarshal([]byte(raw), &obj); err != nil { + return fmt.Errorf("--attributes JSON 格式错误: %w", err) + } + input["attributes"] = obj + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "create_notice", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(noticeCreateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "create_notice", + CanonicalPath: "edu-app.create_notice", + CLIPath: "edu-app notice create", + PrimaryCLIPath: "edu-app notice create", + }, + Description: "创建并发布通知", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "create_notice"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "创建并发布一条家校通知", + UseWhen: []string{"需要创建并发布家校通知时"}, + AvoidWhen: []string{"删除通知用 notice delete;查询通知用 notice get"}, + Examples: []string{ + "dws edu-app notice create --identifer org1-staff1-uuid --content \"明天放假\" --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "identifer", Property: "input.identifer", Required: boolPtr(true)}, + {Name: "content", Property: "input.content", Required: boolPtr(true)}, + {Name: "title", Property: "input.title"}, + {Name: "class-ids", Property: "input.classIds"}, + {Name: "class-names", Property: "input.classNames"}, + {Name: "class-selected-students", Property: "input.classSelectedStudents"}, + {Name: "type", Property: "input.type"}, + {Name: "scope", Property: "input.scope"}, + {Name: "target-role", Property: "input.targetRole"}, + {Name: "is-signed", Property: "input.isSigned"}, + {Name: "photo", Property: "input.photo"}, + {Name: "media", Property: "input.media"}, + {Name: "audio", Property: "input.audio"}, + {Name: "send-ding", Property: "input.sendDing"}, + {Name: "scheduled-release", Property: "input.scheduledRelease"}, + {Name: "notice-deadline", Property: "input.noticeDeadline"}, + {Name: "notice-deadline-open", Property: "input.noticeDeadlineOpen"}, + {Name: "notice-deadline-setting", Property: "input.noticeDeadlineSetting"}, + {Name: "attributes", Property: "input.attributes"}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + noticeDeleteCmd := &cobra.Command{ + Use: "delete", + Short: "删除通知", + Long: `删除指定的家校通知。 +--notice-id 为通知 ID(必填),--user-name 为可选参数。`, + Example: ` dws edu-app notice delete --notice-id 12345 + dws edu-app notice delete --notice-id 12345 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + noticeID, err := eduAppRequiredIntFlag(cmd, "notice-id") + if err != nil { + return err + } + input := map[string]any{ + "noticeId": noticeID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "delete_notice", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(noticeDeleteCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "delete_notice", + CanonicalPath: "edu-app.delete_notice", + CLIPath: "edu-app notice delete", + PrimaryCLIPath: "edu-app notice delete", + }, + Description: "删除通知", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "delete_notice"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "删除指定的家校通知", + UseWhen: []string{"需要删除指定的家校通知时"}, + AvoidWhen: []string{"查询通知详情用 notice get;创建通知用 notice create"}, + Examples: []string{ + "dws edu-app notice delete --notice-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "notice-id", Property: "input.noticeId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + noticeListByTeacherCmd := &cobra.Command{ + Use: "list-by-teacher", + Short: "查询老师发布的通知列表", + Long: `查询老师发布的家校通知列表,支持分页和筛选。 +所有参数均为可选,--class-id 按班级筛选,--type 按通知类型筛选,--status 按通知状态筛选。`, + Example: ` dws edu-app notice list-by-teacher + dws edu-app notice list-by-teacher --class-id 67890 --type SCHOOL --status FINISHED --page 1 --page-size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if v, _ := cmd.Flags().GetString("class-id"); strings.TrimSpace(v) != "" { + input["classId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("type"); strings.TrimSpace(v) != "" { + input["type"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("status"); strings.TrimSpace(v) != "" { + input["status"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + pageNumber, _ := cmd.Flags().GetInt64("page") + if pageNumber > 0 { + input["pageNumber"] = pageNumber + } else { + input["pageNumber"] = 1 + } + pageSize, _ := cmd.Flags().GetInt64("page-size") + if pageSize > 0 { + input["pageSize"] = pageSize + } else { + input["pageSize"] = 20 + } + return callMCPToolOnServer("edu-app", "list_notice_by_teacher", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(noticeListByTeacherCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "list_notice_by_teacher", + CanonicalPath: "edu-app.list_notice_by_teacher", + CLIPath: "edu-app notice list-by-teacher", + PrimaryCLIPath: "edu-app notice list-by-teacher", + }, + Description: "查询老师发布的通知列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "list_notice_by_teacher"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询老师发布的家校通知列表", + UseWhen: []string{"需要查询老师发布的家校通知列表时"}, + AvoidWhen: []string{"查询学生通知用 notice list-by-student"}, + Examples: []string{ + "dws edu-app notice list-by-teacher --class-id 67890 --page 1 --page-size 20 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-id", Property: "input.classId"}, + {Name: "type", Property: "input.type"}, + {Name: "status", Property: "input.status"}, + {Name: "user-name", Property: "input.userName"}, + {Name: "page", Property: "input.pageNumber"}, + {Name: "page-size", Property: "input.pageSize"}, + }, + }, + }) + + noticeGetCmd := &cobra.Command{ + Use: "get", + Short: "查询通知详情", + Long: `按通知 ID 查询家校通知详情。 +--notice-id 为通知 ID(必填),--user-name 为可选参数。`, + Example: ` dws edu-app notice get --notice-id 12345 + dws edu-app notice get --notice-id 12345 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + noticeID, err := eduAppRequiredIntFlag(cmd, "notice-id") + if err != nil { + return err + } + input := map[string]any{ + "noticeId": noticeID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "get_notice", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(noticeGetCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "get_notice", + CanonicalPath: "edu-app.get_notice", + CLIPath: "edu-app notice get", + PrimaryCLIPath: "edu-app notice get", + }, + Description: "查询通知详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "get_notice"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按通知ID查询家校通知详情", + UseWhen: []string{"需要查询指定通知的详情时"}, + AvoidWhen: []string{"查询通知列表用 notice list-by-teacher 或 notice list-by-student"}, + Examples: []string{ + "dws edu-app notice get --notice-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "notice-id", Property: "input.noticeId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + noticeConfirmStatusCmd := &cobra.Command{ + Use: "confirm-status", + Short: "查询通知确认状态", + Long: `按通知 ID 和班级 ID 查询通知的确认状态(已确认/未确认人数及明细)。 +--notice-id、--class-id 为必填参数,--status、--page、--page-size、--user-name 为可选参数。`, + Example: ` dws edu-app notice confirm-status --notice-id 12345 --class-id 67890 + dws edu-app notice confirm-status --notice-id 12345 --class-id 67890 --status CONFIRMED --page 1 --page-size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + noticeID, err := eduAppRequiredIntFlag(cmd, "notice-id") + if err != nil { + return err + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + input := map[string]any{ + "noticeId": noticeID, + "classId": classID, + } + if v, _ := cmd.Flags().GetString("status"); strings.TrimSpace(v) != "" { + input["status"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + pageNumber, _ := cmd.Flags().GetInt64("page") + if pageNumber > 0 { + input["pageNumber"] = pageNumber + } else { + input["pageNumber"] = 1 + } + pageSize, _ := cmd.Flags().GetInt64("page-size") + if pageSize > 0 { + input["pageSize"] = pageSize + } else { + input["pageSize"] = 20 + } + return callMCPToolOnServer("edu-app", "query_notice_confirm_status", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(noticeConfirmStatusCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_notice_confirm_status", + CanonicalPath: "edu-app.query_notice_confirm_status", + CLIPath: "edu-app notice confirm-status", + PrimaryCLIPath: "edu-app notice confirm-status", + }, + Description: "查询通知确认状态", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_notice_confirm_status"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按通知ID和班级ID查询通知确认状态", + UseWhen: []string{"需要查询通知的确认状态时"}, + AvoidWhen: []string{"确认通知用 notice confirm;查询通知详情用 notice get"}, + Examples: []string{ + "dws edu-app notice confirm-status --notice-id 12345 --class-id 67890 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "notice-id", Property: "input.noticeId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "status", Property: "input.status"}, + {Name: "user-name", Property: "input.userName"}, + {Name: "page", Property: "input.pageNumber"}, + {Name: "page-size", Property: "input.pageSize"}, + }, + }, + }) + + noticeListByStudentCmd := &cobra.Command{ + Use: "list-by-student", + Short: "查询学生通知列表", + Long: `按学生 ID 和班级 ID 查询学生收到的家校通知列表,支持分页和状态筛选。 +--student-id、--class-id 为必填参数,--status、--page、--page-size、--user-name 为可选参数。`, + Example: ` dws edu-app notice list-by-student --student-id uid1 --class-id 67890 + dws edu-app notice list-by-student --student-id uid1 --class-id 67890 --status FINISHED --page 1 --page-size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + studentID, _ := cmd.Flags().GetString("student-id") + studentID = strings.TrimSpace(studentID) + if studentID == "" { + return fmt.Errorf("--student-id 为必填参数") + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + input := map[string]any{ + "studentId": studentID, + "classId": classID, + } + if v, _ := cmd.Flags().GetString("status"); strings.TrimSpace(v) != "" { + input["status"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + pageNumber, _ := cmd.Flags().GetInt64("page") + if pageNumber > 0 { + input["pageNumber"] = pageNumber + } else { + input["pageNumber"] = 1 + } + pageSize, _ := cmd.Flags().GetInt64("page-size") + if pageSize > 0 { + input["pageSize"] = pageSize + } else { + input["pageSize"] = 20 + } + return callMCPToolOnServer("edu-app", "list_notice_by_student", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(noticeListByStudentCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "list_notice_by_student", + CanonicalPath: "edu-app.list_notice_by_student", + CLIPath: "edu-app notice list-by-student", + PrimaryCLIPath: "edu-app notice list-by-student", + }, + Description: "查询学生通知列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "list_notice_by_student"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按学生ID和班级ID查询学生收到的通知列表", + UseWhen: []string{"需要查询学生收到的家校通知列表时"}, + AvoidWhen: []string{"查询老师发布的通知用 notice list-by-teacher"}, + Examples: []string{ + "dws edu-app notice list-by-student --student-id uid1 --class-id 67890 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "student-id", Property: "input.studentId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "status", Property: "input.status"}, + {Name: "user-name", Property: "input.userName"}, + {Name: "page", Property: "input.pageNumber"}, + {Name: "page-size", Property: "input.pageSize"}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // circle 子命令组 — 班级圈 + // ════════════════════════════════════════════════════════════ + + circleCmd := &cobra.Command{Use: "circle", Short: "班级圈管理", RunE: groupRunE} + + circlePostsCmd := &cobra.Command{ + Use: "posts", + Short: "查询学生班级圈动态", + Long: `查询指定学生在班级圈中的动态(文字、图片等成长记录)。 +--target-role 取值:guardian(家长视角)、student(学生视角)。 +返回每条动态的文字内容、图片URL列表、发布者姓名、发布时间、评论数、点赞数等信息。`, + Example: ` dws edu-app circle posts --class-id 12345 --student-id uid1 --target-role guardian + dws edu-app circle posts --class-id 12345 --student-id uid1 --target-role student -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + studentID, _ := cmd.Flags().GetString("student-id") + studentID = strings.TrimSpace(studentID) + if studentID == "" { + return fmt.Errorf("--student-id 为必填参数") + } + targetRole, _ := cmd.Flags().GetString("target-role") + targetRole = strings.TrimSpace(targetRole) + if targetRole == "" { + return fmt.Errorf("--target-role 为必填参数") + } + return callMCPToolOnServer("edu-app", "query_student_circle_posts", map[string]any{ + "input": map[string]any{ + "classId": classID, + "studentId": studentID, + "targetRole": targetRole, + }, + }) + }, + } + DeclareLeafMetadata(circlePostsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_student_circle_posts", + CanonicalPath: "edu-app.query_student_circle_posts", + CLIPath: "edu-app circle posts", + PrimaryCLIPath: "edu-app circle posts", + }, + Description: "查询学生班级圈动态", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_student_circle_posts"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询学生在班级圈中的动态", + UseWhen: []string{"需要查询学生在班级圈中的成长记录动态时"}, + AvoidWhen: []string{"查询家校任务用 task;查询作业用 homework"}, + Examples: []string{ + "dws edu-app circle posts --class-id 12345 --student-id uid1 --target-role guardian --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "student-id", Property: "input.studentId", Required: boolPtr(true)}, + {Name: "target-role", Property: "input.targetRole", Required: boolPtr(true)}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // card 子命令组 — 打卡 + // ════════════════════════════════════════════════════════════ + + cardCmd := &cobra.Command{Use: "card", Short: "打卡管理", RunE: groupRunE} + + cardUpdateCmd := &cobra.Command{ + Use: "update", + Short: "修改打卡标题或内容", + Long: `修改指定打卡任务的标题或内容(仅允许修改这两项,其他字段忽略)。仅老师可调用且操作人必须为打卡创建者。 +--title 与 --content 至少传一个。 +--identifier 建议格式:orgId-staffId-UUID,用于幂等去重。`, + Example: ` dws edu-app card update --card-id 12345 --identifier org1-staff1-uuid --title "新标题" + dws edu-app card update --card-id 12345 --identifier org1-staff1-uuid --content "新内容" --should-send-update-msg true -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + cardID, err := eduAppRequiredIntFlag(cmd, "card-id") + if err != nil { + return err + } + identifier, _ := cmd.Flags().GetString("identifier") + identifier = strings.TrimSpace(identifier) + if identifier == "" { + return fmt.Errorf("--identifier 为必填参数") + } + title, _ := cmd.Flags().GetString("title") + title = strings.TrimSpace(title) + content, _ := cmd.Flags().GetString("content") + content = strings.TrimSpace(content) + if title == "" && content == "" { + return fmt.Errorf("--title 与 --content 至少传一个") + } + input := map[string]any{ + "cardId": cardID, + "identifier": identifier, + } + if title != "" { + input["title"] = title + } + if content != "" { + input["content"] = content + } + if cmd.Flags().Changed("should-send-update-msg") { + v, _ := cmd.Flags().GetBool("should-send-update-msg") + input["shouldSendUpdateMsg"] = v + } + return callMCPToolOnServer("edu-app", "update_card", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(cardUpdateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "update_card", + CanonicalPath: "edu-app.update_card", + CLIPath: "edu-app card update", + PrimaryCLIPath: "edu-app card update", + }, + Description: "修改打卡标题或内容", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "update_card"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "修改打卡任务的标题或内容", + UseWhen: []string{"需要修改打卡任务的标题或内容时"}, + AvoidWhen: []string{"结束打卡用 card end;查询打卡用 card list"}, + Examples: []string{ + "dws edu-app card update --card-id 12345 --identifier org1-staff1-uuid --title \"新标题\" --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "card-id", Property: "input.cardId", Required: boolPtr(true)}, + {Name: "identifier", Property: "input.identifier", Required: boolPtr(true)}, + {Name: "title", Property: "input.title"}, + {Name: "content", Property: "input.content"}, + {Name: "should-send-update-msg", Property: "input.shouldSendUpdateMsg"}, + }, + }, + }) + + cardEndCmd := &cobra.Command{ + Use: "end", + Short: "提前结束打卡任务", + Long: `提前结束指定打卡任务,仅老师可调用且操作人必须为打卡创建者。适用于老师在打卡周期未到期时手动终止打卡任务。`, + Example: ` dws edu-app card end --card-id 12345 + dws edu-app card end --card-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + cardID, err := eduAppRequiredIntFlag(cmd, "card-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-app", "end_card", map[string]any{ + "input": map[string]any{ + "cardId": cardID, + }, + }) + }, + } + DeclareLeafMetadata(cardEndCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "end_card", + CanonicalPath: "edu-app.end_card", + CLIPath: "edu-app card end", + PrimaryCLIPath: "edu-app card end", + }, + Description: "提前结束打卡任务", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "end_card"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "提前结束打卡任务", + UseWhen: []string{"需要提前结束指定打卡任务时"}, + AvoidWhen: []string{"修改打卡用 card update;查询打卡用 card list"}, + Examples: []string{ + "dws edu-app card end --card-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "card-id", Property: "input.cardId", Required: boolPtr(true)}, + }, + }, + }) + + cardListCmd := &cobra.Command{ + Use: "list", + Short: "查询打卡列表", + Long: `查询孩子或本人的全部打卡列表(含进行中与已完结),支持分页。 +角色由 uid 真实身份自动判断:学生查自己、家长查孩子,老师调用返回空列表。 +--status 取值:FINISH(已完结)、UNFINISH(进行中)。`, + Example: ` dws edu-app card list --status UNFINISH + dws edu-app card list --status FINISH --class-id 12345 --page 1 --limit 10 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + status, _ := cmd.Flags().GetString("status") + status = strings.TrimSpace(status) + if status == "" { + return fmt.Errorf("--status 为必填参数") + } + if status != "FINISH" && status != "UNFINISH" { + return fmt.Errorf("--status 取值必须为 FINISH 或 UNFINISH") + } + input := map[string]any{ + "status": status, + } + if cmd.Flags().Changed("class-id") { + v, _ := cmd.Flags().GetInt64("class-id") + input["classId"] = v + } + pageNo, _ := cmd.Flags().GetInt64("page") + if pageNo <= 0 { + pageNo = 1 + } + input["pageNo"] = pageNo + pageSize, _ := cmd.Flags().GetInt64("limit") + if pageSize <= 0 { + pageSize = 10 + } + input["pageSize"] = pageSize + return callMCPToolOnServer("edu-app", "get_card_list", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(cardListCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "get_card_list", + CanonicalPath: "edu-app.get_card_list", + CLIPath: "edu-app card list", + PrimaryCLIPath: "edu-app card list", + }, + Description: "查询打卡列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "get_card_list"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询打卡列表", + UseWhen: []string{"需要查询打卡列表时"}, + AvoidWhen: []string{"查询打卡完成情况用 card user-statistic;查询打卡详情用 card finish-info"}, + Examples: []string{ + "dws edu-app card list --status UNFINISH --page 1 --limit 10 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "status", Property: "input.status", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId"}, + {Name: "page", Property: "input.pageNo"}, + {Name: "limit", Property: "input.pageSize"}, + }, + }, + }) + + cardUserStatisticCmd := &cobra.Command{ + Use: "user-statistic", + Short: "查询班级打卡完成/未完成人员", + Long: `查询指定打卡任务下某班级的已完成或未完成人员列表,支持分页。 +仅老师/班主任可调用(角色由 uid 真实身份自动判断)。 +--finish 为 true 时查已完成人员,为 false 时查未完成人员。 +注意:--class-id 为 string 类型。`, + Example: ` dws edu-app card user-statistic --card-id 12345 --task-code code1 --class-id cid1 + dws edu-app card user-statistic --card-id 12345 --task-code code1 --class-id cid1 --finish --page 1 --limit 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + cardID, err := eduAppRequiredIntFlag(cmd, "card-id") + if err != nil { + return err + } + taskCode, _ := cmd.Flags().GetString("task-code") + taskCode = strings.TrimSpace(taskCode) + if taskCode == "" { + return fmt.Errorf("--task-code 为必填参数") + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + input := map[string]any{ + "cardId": cardID, + "taskCode": taskCode, + "classId": classID, + } + if cmd.Flags().Changed("finish") { + v, _ := cmd.Flags().GetBool("finish") + input["finish"] = v + } + pageNo, _ := cmd.Flags().GetInt64("page") + if pageNo <= 0 { + pageNo = 1 + } + input["pageNo"] = pageNo + pageSize, _ := cmd.Flags().GetInt64("limit") + if pageSize <= 0 { + pageSize = 10 + } + input["pageSize"] = pageSize + return callMCPToolOnServer("edu-app", "get_card_user_statistic", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(cardUserStatisticCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "get_card_user_statistic", + CanonicalPath: "edu-app.get_card_user_statistic", + CLIPath: "edu-app card user-statistic", + PrimaryCLIPath: "edu-app card user-statistic", + }, + Description: "查询班级打卡完成/未完成人员", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "get_card_user_statistic"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询班级打卡完成/未完成人员列表", + UseWhen: []string{"需要查询打卡任务下某班级的完成或未完成人员时"}, + AvoidWhen: []string{"查询打卡列表用 card list;查询打卡详情用 card finish-info"}, + Examples: []string{ + "dws edu-app card user-statistic --card-id 12345 --task-code code1 --class-id cid1 --finish --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "card-id", Property: "input.cardId", Required: boolPtr(true)}, + {Name: "task-code", Property: "input.taskCode", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "finish", Property: "input.finish"}, + {Name: "page", Property: "input.pageNo"}, + {Name: "limit", Property: "input.pageSize"}, + }, + }, + }) + + cardFinishInfoCmd := &cobra.Command{ + Use: "finish-info", + Short: "查询打卡详情及完成进度", + Long: `查询指定打卡任务的详情及完成进度。 +--target-role 取值:teacher(老师视角)、guardian(家长/学生视角),未传时按 uid 真实身份自动推断。 +当 targetRole 为 guardian 时,可通过 --student-id 指定查看某个孩子的进度。`, + Example: ` dws edu-app card finish-info --card-id 12345 --card-biz-id bid1 + dws edu-app card finish-info --card-id 12345 --card-biz-id bid1 --target-role guardian --student-id stu1 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + cardID, err := eduAppRequiredIntFlag(cmd, "card-id") + if err != nil { + return err + } + cardBizID, _ := cmd.Flags().GetString("card-biz-id") + cardBizID = strings.TrimSpace(cardBizID) + if cardBizID == "" { + return fmt.Errorf("--card-biz-id 为必填参数") + } + input := map[string]any{ + "cardId": cardID, + "cardBizId": cardBizID, + } + if v, _ := cmd.Flags().GetString("target-role"); strings.TrimSpace(v) != "" { + role := strings.TrimSpace(v) + if role != "teacher" && role != "guardian" && role != "student" && role != "headmaster" { + return fmt.Errorf("--target-role 取值必须为 teacher / guardian / student / headmaster") + } + input["targetRole"] = role + } + if v, _ := cmd.Flags().GetString("student-id"); strings.TrimSpace(v) != "" { + input["studentId"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "get_card_finish_info", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(cardFinishInfoCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "get_card_finish_info", + CanonicalPath: "edu-app.get_card_finish_info", + CLIPath: "edu-app card finish-info", + PrimaryCLIPath: "edu-app card finish-info", + }, + Description: "查询打卡详情及完成进度", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "get_card_finish_info"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询打卡任务详情及完成进度", + UseWhen: []string{"需要查询打卡任务的详情及完成进度时"}, + AvoidWhen: []string{"查询打卡列表用 card list;查询打卡人员统计用 card user-statistic"}, + Examples: []string{ + "dws edu-app card finish-info --card-id 12345 --card-biz-id bid1 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "card-id", Property: "input.cardId", Required: boolPtr(true)}, + {Name: "card-biz-id", Property: "input.cardBizId", Required: boolPtr(true)}, + {Name: "target-role", Property: "input.targetRole"}, + {Name: "student-id", Property: "input.studentId"}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // diploma 子命令组 — 奖状 + // ════════════════════════════════════════════════════════════ + + diplomaCmd := &cobra.Command{Use: "diploma", Short: "奖状管理", RunE: groupRunE} + + diplomaCreateCmd := &cobra.Command{ + Use: "create", + Short: "创建并颁发奖状", + Long: `创建并颁发一张奖状。 +--identifier 为幂等标识(必填),建议格式:orgId-staffId-UUID,重复提交同一 identifier 不会重复创建。 +--select-class 为选择的班级及学生 JSON 数组,--attributes 为扩展属性 JSON 对象。 +返回创建结果:success 表示是否创建成功,diplomaId 为新创建的奖状 ID。`, + Example: ` dws edu-app diploma create --identifier org1-staff1-uuid --content "期末三好学生" --user-name 张三 + dws edu-app diploma create --identifier org1-staff1-uuid --title "三好学生" --content "表现优秀" --user-name 张三 --unit-name "实验小学" --publish-time "2026-07-29" -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + identifier, _ := cmd.Flags().GetString("identifier") + identifier = strings.TrimSpace(identifier) + if identifier == "" { + return fmt.Errorf("--identifier 为必填参数") + } + content, _ := cmd.Flags().GetString("content") + content = strings.TrimSpace(content) + if content == "" { + return fmt.Errorf("--content 为必填参数") + } + userName, _ := cmd.Flags().GetString("user-name") + userName = strings.TrimSpace(userName) + if userName == "" { + return fmt.Errorf("--user-name 为必填参数") + } + input := map[string]any{ + "identifier": identifier, + "content": content, + "userName": userName, + } + if v, _ := cmd.Flags().GetString("title"); strings.TrimSpace(v) != "" { + input["title"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("unit-name"); strings.TrimSpace(v) != "" { + input["unitName"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("tag"); strings.TrimSpace(v) != "" { + input["tag"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("photo"); strings.TrimSpace(v) != "" { + input["photo"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("publish-time"); strings.TrimSpace(v) != "" { + input["publishTime"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("biz-code"); strings.TrimSpace(v) != "" { + input["bizCode"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("biz-category"); strings.TrimSpace(v) != "" { + input["bizCategory"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("msg-type"); strings.TrimSpace(v) != "" { + input["msgType"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("template-url"); strings.TrimSpace(v) != "" { + input["templateUrl"] = strings.TrimSpace(v) + } + if raw, _ := cmd.Flags().GetString("class-ids"); strings.TrimSpace(raw) != "" { + input["classIds"] = eduAppParseCSV(raw) + } + if raw, _ := cmd.Flags().GetString("select-class"); strings.TrimSpace(raw) != "" { + var selectClass []map[string]any + if err := json.Unmarshal([]byte(raw), &selectClass); err != nil { + return fmt.Errorf("--select-class JSON 格式错误: %w", err) + } + input["selectClass"] = selectClass + } + if raw, _ := cmd.Flags().GetString("attributes"); strings.TrimSpace(raw) != "" { + var attributes map[string]any + if err := json.Unmarshal([]byte(raw), &attributes); err != nil { + return fmt.Errorf("--attributes JSON 格式错误: %w", err) + } + input["attributes"] = attributes + } + return callMCPToolOnServer("edu-app", "create_diploma", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(diplomaCreateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "create_diploma", + CanonicalPath: "edu-app.create_diploma", + CLIPath: "edu-app diploma create", + PrimaryCLIPath: "edu-app diploma create", + }, + Description: "创建并颁发奖状", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "create_diploma"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "创建并颁发一张奖状", + UseWhen: []string{"需要创建并颁发奖状时"}, + AvoidWhen: []string{"查询奖状用 diploma get;删除奖状用 diploma delete"}, + Examples: []string{ + "dws edu-app diploma create --identifier org1-staff1-uuid --content \"期末三好学生\" --user-name 张三 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "identifier", Property: "input.identifier", Required: boolPtr(true)}, + {Name: "content", Property: "input.content", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName", Required: boolPtr(true)}, + {Name: "title", Property: "input.title"}, + {Name: "unit-name", Property: "input.unitName"}, + {Name: "tag", Property: "input.tag"}, + {Name: "photo", Property: "input.photo"}, + {Name: "publish-time", Property: "input.publishTime"}, + {Name: "biz-code", Property: "input.bizCode"}, + {Name: "biz-category", Property: "input.bizCategory"}, + {Name: "msg-type", Property: "input.msgType"}, + {Name: "template-url", Property: "input.templateUrl"}, + {Name: "class-ids", Property: "input.classIds"}, + {Name: "select-class", Property: "input.selectClass"}, + {Name: "attributes", Property: "input.attributes"}, + }, + }, + }) + + diplomaReadCmd := &cobra.Command{ + Use: "read", + Short: "标记奖状为已读", + Long: `标记指定奖状为已读状态。 +--diploma-id 为奖状 ID(必填),--class-id、--student-id、--user-name 为可选参数。`, + Example: ` dws edu-app diploma read --diploma-id 12345 + dws edu-app diploma read --diploma-id 12345 --class-id 67890 --student-id uid1 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + diplomaID, err := eduAppRequiredIntFlag(cmd, "diploma-id") + if err != nil { + return err + } + input := map[string]any{ + "diplomaId": diplomaID, + } + if v, _ := cmd.Flags().GetString("class-id"); strings.TrimSpace(v) != "" { + input["classId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("student-id"); strings.TrimSpace(v) != "" { + input["studentId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "read_diploma", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(diplomaReadCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "read_diploma", + CanonicalPath: "edu-app.read_diploma", + CLIPath: "edu-app diploma read", + PrimaryCLIPath: "edu-app diploma read", + }, + Description: "标记奖状为已读", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "read_diploma"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "标记指定奖状为已读状态", + UseWhen: []string{"需要标记奖状为已读时"}, + AvoidWhen: []string{"查询奖状详情用 diploma get;查询奖状统计用 diploma statistics"}, + Examples: []string{ + "dws edu-app diploma read --diploma-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "diploma-id", Property: "input.diplomaId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId"}, + {Name: "student-id", Property: "input.studentId"}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + diplomaListByTeacherCmd := &cobra.Command{ + Use: "list-by-teacher", + Short: "查询老师创建的奖状列表", + Long: `查询当前老师创建的奖状列表,支持分页。 +--status 为奖状状态筛选,--tag 为标签筛选,--user-name 为获奖人姓名筛选。`, + Example: ` dws edu-app diploma list-by-teacher + dws edu-app diploma list-by-teacher --page 1 --limit 20 --tag "三好学生" -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + pageNumber, _ := cmd.Flags().GetInt64("page") + if pageNumber <= 0 { + pageNumber = 1 + } + input["pageNumber"] = pageNumber + pageSize, _ := cmd.Flags().GetInt64("limit") + if pageSize <= 0 { + pageSize = 20 + } + input["pageSize"] = pageSize + if v, _ := cmd.Flags().GetString("status"); strings.TrimSpace(v) != "" { + input["status"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("tag"); strings.TrimSpace(v) != "" { + input["tag"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "list_diploma_by_teacher", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(diplomaListByTeacherCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "list_diploma_by_teacher", + CanonicalPath: "edu-app.list_diploma_by_teacher", + CLIPath: "edu-app diploma list-by-teacher", + PrimaryCLIPath: "edu-app diploma list-by-teacher", + }, + Description: "查询老师创建的奖状列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "list_diploma_by_teacher"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询老师创建的奖状列表", + UseWhen: []string{"需要查询老师创建的奖状列表时"}, + AvoidWhen: []string{"查询学生收到的奖状用 diploma list-by-student"}, + Examples: []string{ + "dws edu-app diploma list-by-teacher --page 1 --limit 20 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "page", Property: "input.pageNumber"}, + {Name: "limit", Property: "input.pageSize"}, + {Name: "status", Property: "input.status"}, + {Name: "tag", Property: "input.tag"}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + diplomaGetCmd := &cobra.Command{ + Use: "get", + Short: "查询奖状详情", + Long: `按奖状 ID 查询奖状详情信息。 +--diploma-id 为奖状 ID(必填),--user-name 为可选参数。`, + Example: ` dws edu-app diploma get --diploma-id 12345 + dws edu-app diploma get --diploma-id 12345 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + diplomaID, err := eduAppRequiredIntFlag(cmd, "diploma-id") + if err != nil { + return err + } + input := map[string]any{ + "diplomaId": diplomaID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "get_diploma", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(diplomaGetCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "get_diploma", + CanonicalPath: "edu-app.get_diploma", + CLIPath: "edu-app diploma get", + PrimaryCLIPath: "edu-app diploma get", + }, + Description: "查询奖状详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "get_diploma"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按奖状ID查询奖状详情", + UseWhen: []string{"需要查询指定奖状的详情时"}, + AvoidWhen: []string{"查询奖状列表用 diploma list-by-teacher 或 diploma list-by-student"}, + Examples: []string{ + "dws edu-app diploma get --diploma-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "diploma-id", Property: "input.diplomaId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + diplomaStatisticsCmd := &cobra.Command{ + Use: "statistics", + Short: "查询奖状阅读统计", + Long: `按奖状 ID 查询奖状的阅读统计数据(按班级维度)。 +返回每个班级的总人数、已读人数、未读人数。 +--diploma-id 为奖状 ID(必填),--user-name 为可选参数。`, + Example: ` dws edu-app diploma statistics --diploma-id 12345 + dws edu-app diploma statistics --diploma-id 12345 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + diplomaID, err := eduAppRequiredIntFlag(cmd, "diploma-id") + if err != nil { + return err + } + input := map[string]any{ + "diplomaId": diplomaID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "query_diploma_statistics", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(diplomaStatisticsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_diploma_statistics", + CanonicalPath: "edu-app.query_diploma_statistics", + CLIPath: "edu-app diploma statistics", + PrimaryCLIPath: "edu-app diploma statistics", + }, + Description: "查询奖状阅读统计", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_diploma_statistics"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按奖状ID查询奖状阅读统计数据", + UseWhen: []string{"需要查询奖状的阅读统计数据时"}, + AvoidWhen: []string{"查询奖状接收详情用 diploma detail"}, + Examples: []string{ + "dws edu-app diploma statistics --diploma-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "diploma-id", Property: "input.diplomaId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + diplomaDetailCmd := &cobra.Command{ + Use: "detail", + Short: "查询奖状接收详情", + Long: `按奖状 ID 查询奖状的接收详情列表。 +返回每个学生的接收状态、接收时间、查看时间等信息。 +--diploma-id 为奖状 ID(必填),--class-id、--user-name 为可选参数。`, + Example: ` dws edu-app diploma detail --diploma-id 12345 + dws edu-app diploma detail --diploma-id 12345 --class-id 67890 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + diplomaID, err := eduAppRequiredIntFlag(cmd, "diploma-id") + if err != nil { + return err + } + input := map[string]any{ + "diplomaId": diplomaID, + } + if v, _ := cmd.Flags().GetString("class-id"); strings.TrimSpace(v) != "" { + input["classId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "query_diploma_detail", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(diplomaDetailCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_diploma_detail", + CanonicalPath: "edu-app.query_diploma_detail", + CLIPath: "edu-app diploma detail", + PrimaryCLIPath: "edu-app diploma detail", + }, + Description: "查询奖状接收详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_diploma_detail"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按奖状ID查询奖状接收详情列表", + UseWhen: []string{"需要查询奖状的接收详情时"}, + AvoidWhen: []string{"查询奖状阅读统计用 diploma statistics;查询学生奖状详情用 diploma student-detail"}, + Examples: []string{ + "dws edu-app diploma detail --diploma-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "diploma-id", Property: "input.diplomaId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId"}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + diplomaListByStudentCmd := &cobra.Command{ + Use: "list-by-student", + Short: "查询学生收到的奖状列表", + Long: `查询指定学生收到的奖状列表,支持分页。 +--student-id 和 --class-id 为必填参数。`, + Example: ` dws edu-app diploma list-by-student --student-id uid1 --class-id 12345 + dws edu-app diploma list-by-student --student-id uid1 --class-id 12345 --page 1 --limit 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + studentID, _ := cmd.Flags().GetString("student-id") + studentID = strings.TrimSpace(studentID) + if studentID == "" { + return fmt.Errorf("--student-id 为必填参数") + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + input := map[string]any{ + "studentId": studentID, + "classId": classID, + } + pageNumber, _ := cmd.Flags().GetInt64("page") + if pageNumber <= 0 { + pageNumber = 1 + } + input["pageNumber"] = pageNumber + pageSize, _ := cmd.Flags().GetInt64("limit") + if pageSize <= 0 { + pageSize = 20 + } + input["pageSize"] = pageSize + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "list_diploma_by_student", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(diplomaListByStudentCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "list_diploma_by_student", + CanonicalPath: "edu-app.list_diploma_by_student", + CLIPath: "edu-app diploma list-by-student", + PrimaryCLIPath: "edu-app diploma list-by-student", + }, + Description: "查询学生收到的奖状列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "list_diploma_by_student"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询学生收到的奖状列表", + UseWhen: []string{"需要查询指定学生收到的奖状列表时"}, + AvoidWhen: []string{"查询老师创建的奖状用 diploma list-by-teacher"}, + Examples: []string{ + "dws edu-app diploma list-by-student --student-id uid1 --class-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "student-id", Property: "input.studentId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "page", Property: "input.pageNumber"}, + {Name: "limit", Property: "input.pageSize"}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + diplomaStudentDetailCmd := &cobra.Command{ + Use: "student-detail", + Short: "查询学生奖状接收详情", + Long: `按奖状 ID 和学生 ID 查询该学生的奖状接收详情。 +--diploma-id、--student-id、--class-id 为必填参数。`, + Example: ` dws edu-app diploma student-detail --diploma-id 12345 --student-id uid1 --class-id 67890 + dws edu-app diploma student-detail --diploma-id 12345 --student-id uid1 --class-id 67890 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + diplomaID, err := eduAppRequiredIntFlag(cmd, "diploma-id") + if err != nil { + return err + } + studentID, _ := cmd.Flags().GetString("student-id") + studentID = strings.TrimSpace(studentID) + if studentID == "" { + return fmt.Errorf("--student-id 为必填参数") + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + input := map[string]any{ + "diplomaId": diplomaID, + "studentId": studentID, + "classId": classID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "query_student_diploma_detail", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(diplomaStudentDetailCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_student_diploma_detail", + CanonicalPath: "edu-app.query_student_diploma_detail", + CLIPath: "edu-app diploma student-detail", + PrimaryCLIPath: "edu-app diploma student-detail", + }, + Description: "查询学生奖状接收详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_student_diploma_detail"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按奖状ID和学生ID查询学生奖状接收详情", + UseWhen: []string{"需要查询指定学生的奖状接收详情时"}, + AvoidWhen: []string{"查询奖状接收详情列表用 diploma detail"}, + Examples: []string{ + "dws edu-app diploma student-detail --diploma-id 12345 --student-id uid1 --class-id 67890 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "diploma-id", Property: "input.diplomaId", Required: boolPtr(true)}, + {Name: "student-id", Property: "input.studentId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // homework 子命令组 — 作业 + // ════════════════════════════════════════════════════════════ + + homeworkCmd := &cobra.Command{Use: "homework", Short: "作业管理", RunE: groupRunE} + + homeworkCreateCmd := &cobra.Command{ + Use: "create", + Short: "创建并发布作业", + Long: `创建并发布一份作业。 +--identifier 为幂等标识(必填),建议格式:orgId-staffId-UUID,重复提交同一 identifier 不会重复创建。 +--hw-content 为作业内容(必填),其余参数均为可选。`, + Example: ` dws edu-app homework create --identifier org1-staff1-uuid --hw-content "完成课后练习第三题" + dws edu-app homework create --identifier org1-staff1-uuid --hw-title "数学作业" --hw-content "完成练习册" --class-ids 12345,67890 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + identifier, _ := cmd.Flags().GetString("identifier") + identifier = strings.TrimSpace(identifier) + if identifier == "" { + return fmt.Errorf("--identifier 为必填参数") + } + hwContent, _ := cmd.Flags().GetString("hw-content") + hwContent = strings.TrimSpace(hwContent) + if hwContent == "" { + return fmt.Errorf("--hw-content 为必填参数") + } + input := map[string]any{ + "identifier": identifier, + "hwContent": hwContent, + } + if v, _ := cmd.Flags().GetString("hw-title"); strings.TrimSpace(v) != "" { + input["hwTitle"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("hw-photo"); strings.TrimSpace(v) != "" { + input["hwPhoto"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("hw-media"); strings.TrimSpace(v) != "" { + input["hwMedia"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("hw-video"); strings.TrimSpace(v) != "" { + input["hwVideo"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("class-ids"); strings.TrimSpace(v) != "" { + input["classIds"] = eduAppParseCSV(v) + } + if v, _ := cmd.Flags().GetString("class-names"); strings.TrimSpace(v) != "" { + input["classNames"] = eduAppParseCSV(v) + } + if v, _ := cmd.Flags().GetString("class-selected-students"); strings.TrimSpace(v) != "" { + var obj any + if err := json.Unmarshal([]byte(v), &obj); err != nil { + return fmt.Errorf("--class-selected-students JSON 格式错误: %w", err) + } + input["classSelectedStudents"] = obj + } + if v, _ := cmd.Flags().GetString("feedback"); strings.TrimSpace(v) != "" { + input["feedback"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("hw-deadline"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--hw-deadline 须为整数: %w", err) + } + input["hwDeadline"] = n + } + if v, _ := cmd.Flags().GetString("hw-deadline-open"); strings.TrimSpace(v) != "" { + input["hwDeadlineOpen"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("hw-deadline-setting"); strings.TrimSpace(v) != "" { + input["hwDeadlineSetting"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("submit-types"); strings.TrimSpace(v) != "" { + input["submitTypes"] = eduAppParseCSV(v) + } + if v, _ := cmd.Flags().GetString("hw-type"); strings.TrimSpace(v) != "" { + input["hwType"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("target-role"); strings.TrimSpace(v) != "" { + input["targetRole"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("publish-type"); strings.TrimSpace(v) != "" { + input["publishType"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("biz-code"); strings.TrimSpace(v) != "" { + input["bizCode"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("scheduled-release"); strings.TrimSpace(v) != "" { + input["scheduledRelease"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("task-plan-duration"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--task-plan-duration 须为整数: %w", err) + } + input["taskPlanDuration"] = n + } + if v, _ := cmd.Flags().GetString("attributes"); strings.TrimSpace(v) != "" { + var obj any + if err := json.Unmarshal([]byte(v), &obj); err != nil { + return fmt.Errorf("--attributes JSON 格式错误: %w", err) + } + input["attributes"] = obj + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "create_homework", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkCreateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "create_homework", + CanonicalPath: "edu-app.create_homework", + CLIPath: "edu-app homework create", + PrimaryCLIPath: "edu-app homework create", + }, + Description: "创建并发布作业", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "create_homework"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "创建并发布一份作业", + UseWhen: []string{"需要创建并发布作业时"}, + AvoidWhen: []string{"删除作业用 homework delete;查询作业用 homework get"}, + Examples: []string{ + "dws edu-app homework create --identifier org1-staff1-uuid --hw-content \"完成课后练习第三题\" --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "identifier", Property: "input.identifier", Required: boolPtr(true)}, + {Name: "hw-content", Property: "input.hwContent", Required: boolPtr(true)}, + {Name: "hw-title", Property: "input.hwTitle"}, + {Name: "hw-photo", Property: "input.hwPhoto"}, + {Name: "hw-media", Property: "input.hwMedia"}, + {Name: "hw-video", Property: "input.hwVideo"}, + {Name: "class-ids", Property: "input.classIds"}, + {Name: "class-names", Property: "input.classNames"}, + {Name: "class-selected-students", Property: "input.classSelectedStudents"}, + {Name: "feedback", Property: "input.feedback"}, + {Name: "hw-deadline", Property: "input.hwDeadline"}, + {Name: "hw-deadline-open", Property: "input.hwDeadlineOpen"}, + {Name: "hw-deadline-setting", Property: "input.hwDeadlineSetting"}, + {Name: "submit-types", Property: "input.submitTypes"}, + {Name: "hw-type", Property: "input.hwType"}, + {Name: "target-role", Property: "input.targetRole"}, + {Name: "publish-type", Property: "input.publishType"}, + {Name: "biz-code", Property: "input.bizCode"}, + {Name: "scheduled-release", Property: "input.scheduledRelease"}, + {Name: "task-plan-duration", Property: "input.taskPlanDuration"}, + {Name: "attributes", Property: "input.attributes"}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + homeworkDeleteCmd := &cobra.Command{ + Use: "delete", + Short: "删除作业", + Long: `删除指定的作业。 +返回删除结果:success 表示是否删除成功。`, + Example: ` dws edu-app homework delete --homework-id 12345 + dws edu-app homework delete --homework-id 12345 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + homeworkID, err := eduAppRequiredIntFlag(cmd, "homework-id") + if err != nil { + return err + } + input := map[string]any{ + "homeworkId": homeworkID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "delete_homework", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkDeleteCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "delete_homework", + CanonicalPath: "edu-app.delete_homework", + CLIPath: "edu-app homework delete", + PrimaryCLIPath: "edu-app homework delete", + }, + Description: "删除作业", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "delete_homework"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "删除指定的作业", + UseWhen: []string{"需要删除指定作业时"}, + AvoidWhen: []string{"查询作业详情用 homework get;创建作业用 homework create"}, + Examples: []string{ + "dws edu-app homework delete --homework-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "homework-id", Property: "input.homeworkId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + homeworkSubmitCmd := &cobra.Command{ + Use: "submit", + Short: "提交作业", + Long: `提交指定作业内容。 +--hw-content-detail-id 为作业内容详情 ID(必填),其余参数均为可选。`, + Example: ` dws edu-app homework submit --hw-content-detail-id 12345 + dws edu-app homework submit --hw-content-detail-id 12345 --homework-id 67890 --content "已完成" --photo "https://example.com/img.jpg" -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + hwContentDetailID, err := eduAppRequiredIntFlag(cmd, "hw-content-detail-id") + if err != nil { + return err + } + input := map[string]any{ + "hwContentDetailId": hwContentDetailID, + } + if v, _ := cmd.Flags().GetString("homework-id"); strings.TrimSpace(v) != "" { + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return fmt.Errorf("--homework-id 须为整数: %w", err) + } + input["homeworkId"] = n + } + if v, _ := cmd.Flags().GetString("student-id"); strings.TrimSpace(v) != "" { + input["studentId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("class-id"); strings.TrimSpace(v) != "" { + input["classId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("content"); strings.TrimSpace(v) != "" { + input["content"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("photo"); strings.TrimSpace(v) != "" { + input["photo"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("media"); strings.TrimSpace(v) != "" { + input["media"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("video"); strings.TrimSpace(v) != "" { + input["video"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "submit_homework", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkSubmitCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "submit_homework", + CanonicalPath: "edu-app.submit_homework", + CLIPath: "edu-app homework submit", + PrimaryCLIPath: "edu-app homework submit", + }, + Description: "提交作业", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "submit_homework"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "提交指定作业内容", + UseWhen: []string{"需要提交作业时"}, + AvoidWhen: []string{"查询作业详情用 homework get;创建作业用 homework create"}, + Examples: []string{ + "dws edu-app homework submit --hw-content-detail-id 12345 --content \"已完成\" --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "hw-content-detail-id", Property: "input.hwContentDetailId", Required: boolPtr(true)}, + {Name: "homework-id", Property: "input.homeworkId"}, + {Name: "student-id", Property: "input.studentId"}, + {Name: "class-id", Property: "input.classId"}, + {Name: "content", Property: "input.content"}, + {Name: "photo", Property: "input.photo"}, + {Name: "media", Property: "input.media"}, + {Name: "video", Property: "input.video"}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + homeworkGetCmd := &cobra.Command{ + Use: "get", + Short: "查询作业详情", + Long: `按作业 ID 查询作业详情。 +--homework-id 为作业 ID(必填),--user-name 为可选参数。`, + Example: ` dws edu-app homework get --homework-id 12345 + dws edu-app homework get --homework-id 12345 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + homeworkID, err := eduAppRequiredIntFlag(cmd, "homework-id") + if err != nil { + return err + } + input := map[string]any{ + "homeworkId": homeworkID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "get_homework", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkGetCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "get_homework", + CanonicalPath: "edu-app.get_homework", + CLIPath: "edu-app homework get", + PrimaryCLIPath: "edu-app homework get", + }, + Description: "查询作业详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "get_homework"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按作业ID查询作业详情", + UseWhen: []string{"需要查询指定作业的详情时"}, + AvoidWhen: []string{"查询作业列表用 homework list-by-teacher 或 homework list-by-student"}, + Examples: []string{ + "dws edu-app homework get --homework-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "homework-id", Property: "input.homeworkId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + homeworkClassByHomeworkCmd := &cobra.Command{ + Use: "class-by-homework", + Short: "查询作业的班级提交情况", + Long: `按作业 ID 查询作业的班级列表及学生提交明细。 +--homework-id 为作业 ID(必填),--class-id、--user-name 为可选参数。`, + Example: ` dws edu-app homework class-by-homework --homework-id 12345 + dws edu-app homework class-by-homework --homework-id 12345 --class-id 67890 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + homeworkID, err := eduAppRequiredIntFlag(cmd, "homework-id") + if err != nil { + return err + } + input := map[string]any{ + "homeworkId": homeworkID, + } + if v, _ := cmd.Flags().GetString("class-id"); strings.TrimSpace(v) != "" { + input["classId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "query_class_by_homework", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkClassByHomeworkCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_class_by_homework", + CanonicalPath: "edu-app.query_class_by_homework", + CLIPath: "edu-app homework class-by-homework", + PrimaryCLIPath: "edu-app homework class-by-homework", + }, + Description: "查询作业的班级提交情况", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_class_by_homework"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按作业ID查询作业的班级提交情况", + UseWhen: []string{"需要查询作业的班级列表及学生提交明细时"}, + AvoidWhen: []string{"查询班级作业详情用 homework class-detail"}, + Examples: []string{ + "dws edu-app homework class-by-homework --homework-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "homework-id", Property: "input.homeworkId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId"}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + homeworkClassDetailCmd := &cobra.Command{ + Use: "class-detail", + Short: "查询班级作业详情", + Long: `按作业 ID 和班级 ID 查询班级作业的提交明细。 +--homework-id、--class-id、--user-name 均为必填参数。`, + Example: ` dws edu-app homework class-detail --homework-id 12345 --class-id 67890 --user-name 张三 + dws edu-app homework class-detail --homework-id 12345 --class-id 67890 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + homeworkID, err := eduAppRequiredIntFlag(cmd, "homework-id") + if err != nil { + return err + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + userName, _ := cmd.Flags().GetString("user-name") + userName = strings.TrimSpace(userName) + if userName == "" { + return fmt.Errorf("--user-name 为必填参数") + } + input := map[string]any{ + "homeworkId": homeworkID, + "classId": classID, + "userName": userName, + } + return callMCPToolOnServer("edu-app", "query_class_homework_detail", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkClassDetailCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_class_homework_detail", + CanonicalPath: "edu-app.query_class_homework_detail", + CLIPath: "edu-app homework class-detail", + PrimaryCLIPath: "edu-app homework class-detail", + }, + Description: "查询班级作业详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_class_homework_detail"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按作业ID和班级ID查询班级作业提交明细", + UseWhen: []string{"需要查询班级作业的提交明细时"}, + AvoidWhen: []string{"查询作业班级列表用 homework class-by-homework"}, + Examples: []string{ + "dws edu-app homework class-detail --homework-id 12345 --class-id 67890 --user-name 张三 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "homework-id", Property: "input.homeworkId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName", Required: boolPtr(true)}, + }, + }, + }) + + homeworkSubmitStatisticsCmd := &cobra.Command{ + Use: "submit-statistics", + Short: "查询作业提交统计", + Long: `按作业 ID 和班级 ID 查询作业提交统计(已提交人数、已批改人数)。 +--homework-id、--class-id 为必填参数,--user-name 为可选参数。`, + Example: ` dws edu-app homework submit-statistics --homework-id 12345 --class-id 67890 + dws edu-app homework submit-statistics --homework-id 12345 --class-id 67890 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + homeworkID, err := eduAppRequiredIntFlag(cmd, "homework-id") + if err != nil { + return err + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + input := map[string]any{ + "homeworkId": homeworkID, + "classId": classID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "query_submit_statistics", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkSubmitStatisticsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_submit_statistics", + CanonicalPath: "edu-app.query_submit_statistics", + CLIPath: "edu-app homework submit-statistics", + PrimaryCLIPath: "edu-app homework submit-statistics", + }, + Description: "查询作业提交统计", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_submit_statistics"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按作业ID和班级ID查询作业提交统计", + UseWhen: []string{"需要查询作业提交统计时"}, + AvoidWhen: []string{"查询班级作业详情用 homework class-detail"}, + Examples: []string{ + "dws edu-app homework submit-statistics --homework-id 12345 --class-id 67890 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "homework-id", Property: "input.homeworkId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + homeworkListByStudentCmd := &cobra.Command{ + Use: "list-by-student", + Short: "查询学生作业列表", + Long: `按学生 ID 和班级 ID 查询学生收到的作业列表。 +--student-id、--class-id、--user-name 为必填参数,--status、--page、--page-size 为可选参数。`, + Example: ` dws edu-app homework list-by-student --student-id uid1 --class-id 67890 --user-name 张三 + dws edu-app homework list-by-student --student-id uid1 --class-id 67890 --user-name 张三 --status FINISHED --page 1 --page-size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + studentID, _ := cmd.Flags().GetString("student-id") + studentID = strings.TrimSpace(studentID) + if studentID == "" { + return fmt.Errorf("--student-id 为必填参数") + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + userName, _ := cmd.Flags().GetString("user-name") + userName = strings.TrimSpace(userName) + if userName == "" { + return fmt.Errorf("--user-name 为必填参数") + } + input := map[string]any{ + "studentId": studentID, + "classId": classID, + "userName": userName, + } + if v, _ := cmd.Flags().GetString("status"); strings.TrimSpace(v) != "" { + input["status"] = strings.TrimSpace(v) + } + pageNumber, _ := cmd.Flags().GetInt64("page") + if pageNumber > 0 { + input["pageNumber"] = pageNumber + } else { + input["pageNumber"] = 1 + } + pageSize, _ := cmd.Flags().GetInt64("page-size") + if pageSize > 0 { + input["pageSize"] = pageSize + } else { + input["pageSize"] = 20 + } + return callMCPToolOnServer("edu-app", "list_homework_by_student", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkListByStudentCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "list_homework_by_student", + CanonicalPath: "edu-app.list_homework_by_student", + CLIPath: "edu-app homework list-by-student", + PrimaryCLIPath: "edu-app homework list-by-student", + }, + Description: "查询学生作业列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "list_homework_by_student"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按学生ID和班级ID查询学生收到的作业列表", + UseWhen: []string{"需要查询学生收到的作业列表时"}, + AvoidWhen: []string{"查询老师作业列表用 homework list-by-teacher"}, + Examples: []string{ + "dws edu-app homework list-by-student --student-id uid1 --class-id 67890 --user-name 张三 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "student-id", Property: "input.studentId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName", Required: boolPtr(true)}, + {Name: "status", Property: "input.status"}, + {Name: "page", Property: "input.pageNumber"}, + {Name: "page-size", Property: "input.pageSize"}, + }, + }, + }) + + homeworkStudentDetailCmd := &cobra.Command{ + Use: "student-detail", + Short: "查询学生作业详情", + Long: `按作业 ID、学生 ID 和班级 ID 查询单个学生的作业提交明细。 +--homework-id、--student-id、--class-id 为必填参数,--user-name 为可选参数。`, + Example: ` dws edu-app homework student-detail --homework-id 12345 --student-id uid1 --class-id 67890 + dws edu-app homework student-detail --homework-id 12345 --student-id uid1 --class-id 67890 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + homeworkID, err := eduAppRequiredIntFlag(cmd, "homework-id") + if err != nil { + return err + } + studentID, _ := cmd.Flags().GetString("student-id") + studentID = strings.TrimSpace(studentID) + if studentID == "" { + return fmt.Errorf("--student-id 为必填参数") + } + classID, _ := cmd.Flags().GetString("class-id") + classID = strings.TrimSpace(classID) + if classID == "" { + return fmt.Errorf("--class-id 为必填参数") + } + input := map[string]any{ + "homeworkId": homeworkID, + "studentId": studentID, + "classId": classID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "query_student_homework_detail", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkStudentDetailCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "query_student_homework_detail", + CanonicalPath: "edu-app.query_student_homework_detail", + CLIPath: "edu-app homework student-detail", + PrimaryCLIPath: "edu-app homework student-detail", + }, + Description: "查询学生作业详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "query_student_homework_detail"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "按作业ID和学生ID查询学生作业提交明细", + UseWhen: []string{"需要查询单个学生的作业提交明细时"}, + AvoidWhen: []string{"查询班级作业详情用 homework class-detail"}, + Examples: []string{ + "dws edu-app homework student-detail --homework-id 12345 --student-id uid1 --class-id 67890 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "homework-id", Property: "input.homeworkId", Required: boolPtr(true)}, + {Name: "student-id", Property: "input.studentId", Required: boolPtr(true)}, + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + homeworkListByTeacherCmd := &cobra.Command{ + Use: "list-by-teacher", + Short: "查询老师作业列表", + Long: `查询老师发布的家校作业列表,支持分页和筛选。 +所有参数均为可选,--class-id 按班级筛选,--type 按作业类型筛选,--status 按作业状态筛选。`, + Example: ` dws edu-app homework list-by-teacher + dws edu-app homework list-by-teacher --class-id 67890 --type HOMEWORK --status FINISHED --page 1 --page-size 20 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if v, _ := cmd.Flags().GetString("class-id"); strings.TrimSpace(v) != "" { + input["classId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("type"); strings.TrimSpace(v) != "" { + input["type"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("status"); strings.TrimSpace(v) != "" { + input["status"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + pageNumber, _ := cmd.Flags().GetInt64("page") + if pageNumber > 0 { + input["pageNumber"] = pageNumber + } else { + input["pageNumber"] = 1 + } + pageSize, _ := cmd.Flags().GetInt64("page-size") + if pageSize > 0 { + input["pageSize"] = pageSize + } else { + input["pageSize"] = 20 + } + return callMCPToolOnServer("edu-app", "list_homework_by_teacher", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkListByTeacherCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "list_homework_by_teacher", + CanonicalPath: "edu-app.list_homework_by_teacher", + CLIPath: "edu-app homework list-by-teacher", + PrimaryCLIPath: "edu-app homework list-by-teacher", + }, + Description: "查询老师作业列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "list_homework_by_teacher"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询老师发布的家校作业列表", + UseWhen: []string{"需要查询老师发布的作业列表时"}, + AvoidWhen: []string{"查询学生作业列表用 homework list-by-student"}, + Examples: []string{ + "dws edu-app homework list-by-teacher --class-id 67890 --page 1 --page-size 20 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-id", Property: "input.classId"}, + {Name: "type", Property: "input.type"}, + {Name: "status", Property: "input.status"}, + {Name: "user-name", Property: "input.userName"}, + {Name: "page", Property: "input.pageNumber"}, + {Name: "page-size", Property: "input.pageSize"}, + }, + }, + }) + + homeworkCreateCommentCmd := &cobra.Command{ + Use: "create-comment", + Short: "创建作业评语", + Long: `为指定作业创建评语。 +--comment、--hw-content-detail-id 为必填参数,--homework-id、--student-id、--photo、--video、--media、--user-name 为可选参数。`, + Example: ` dws edu-app homework create-comment --comment "做得很好" --hw-content-detail-id 12345 + dws edu-app homework create-comment --comment "继续努力" --hw-content-detail-id 12345 --homework-id 67890 --student-id uid1 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + comment, _ := cmd.Flags().GetString("comment") + comment = strings.TrimSpace(comment) + if comment == "" { + return fmt.Errorf("--comment 为必填参数") + } + hwContentDetailID, err := eduAppRequiredIntFlag(cmd, "hw-content-detail-id") + if err != nil { + return err + } + input := map[string]any{ + "comment": comment, + "hwContentDetailId": hwContentDetailID, + } + if cmd.Flags().Changed("homework-id") { + v, _ := cmd.Flags().GetInt64("homework-id") + input["homeworkId"] = v + } + if v, _ := cmd.Flags().GetString("student-id"); strings.TrimSpace(v) != "" { + input["studentId"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("photo"); strings.TrimSpace(v) != "" { + input["photo"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("video"); strings.TrimSpace(v) != "" { + input["video"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("media"); strings.TrimSpace(v) != "" { + input["media"] = strings.TrimSpace(v) + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "create_comment", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(homeworkCreateCommentCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "create_comment", + CanonicalPath: "edu-app.create_comment", + CLIPath: "edu-app homework create-comment", + PrimaryCLIPath: "edu-app homework create-comment", + }, + Description: "创建作业评语", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "create_comment"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "为指定作业创建评语", + UseWhen: []string{"需要为作业创建评语时"}, + AvoidWhen: []string{"提交作业用 homework submit;查询作业用 homework get"}, + Examples: []string{ + "dws edu-app homework create-comment --comment \"做得很好\" --hw-content-detail-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "comment", Property: "input.comment", Required: boolPtr(true)}, + {Name: "hw-content-detail-id", Property: "input.hwContentDetailId", Required: boolPtr(true)}, + {Name: "homework-id", Property: "input.homeworkId"}, + {Name: "student-id", Property: "input.studentId"}, + {Name: "photo", Property: "input.photo"}, + {Name: "video", Property: "input.video"}, + {Name: "media", Property: "input.media"}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + diplomaDeleteCmd := &cobra.Command{ + Use: "delete", + Short: "删除奖状", + Long: `删除指定的奖状。 +返回删除结果:success 表示是否删除成功。`, + Example: ` dws edu-app diploma delete --diploma-id 12345 + dws edu-app diploma delete --diploma-id 12345 --user-name 张三 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + diplomaID, err := eduAppRequiredIntFlag(cmd, "diploma-id") + if err != nil { + return err + } + input := map[string]any{ + "diplomaId": diplomaID, + } + if v, _ := cmd.Flags().GetString("user-name"); strings.TrimSpace(v) != "" { + input["userName"] = strings.TrimSpace(v) + } + return callMCPToolOnServer("edu-app", "delete_diploma", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(diplomaDeleteCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-app", + Name: "delete_diploma", + CanonicalPath: "edu-app.delete_diploma", + CLIPath: "edu-app diploma delete", + PrimaryCLIPath: "edu-app diploma delete", + }, + Description: "删除奖状", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-app", RPCName: "delete_diploma"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "删除指定的奖状", + UseWhen: []string{"需要删除指定奖状时"}, + AvoidWhen: []string{"查询奖状详情用 diploma get;创建奖状用 diploma create"}, + Examples: []string{ + "dws edu-app diploma delete --diploma-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "diploma-id", Property: "input.diplomaId", Required: boolPtr(true)}, + {Name: "user-name", Property: "input.userName"}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // flags + 构建命令树 + // ════════════════════════════════════════════════════════════ + + // message flags + messageSummaryListCmd.Flags().String("class-id", "", "班级 ID(必填)") + messageSummaryListCmd.Flags().String("cid", "", "群会话 ID(必填)") + messageSummaryListCmd.Flags().String("target-role", "", "用户角色:guardian / student(必填)") + messageSummaryListCmd.Flags().String("status", "", "任务状态:0-未处理 1-已处理(必填)") + + // task flags + taskPublishListCmd.Flags().Int64("cursor", 0, "分页游标,首次不传") + taskPublishListCmd.Flags().Int64("limit", 0, "每页条数") + taskPublishListCmd.Flags().Bool("need-statistic", false, "是否返回任务完成统计") + taskPublishListCmd.Flags().String("task-sources", "", "任务来源,逗号分隔(EDU_HOMEWORK/EDU_CARD/EDU_NOTICE/EDU_SR/EDU_DIPLOMA)") + + taskAllListCmd.Flags().String("biz-id", "", "班级 ID(必填)") + taskAllListCmd.Flags().Int64("cursor", 0, "分页游标,首次不传") + taskAllListCmd.Flags().Int64("limit", 0, "每页条数") + taskAllListCmd.Flags().Bool("need-statistic", false, "是否返回任务完成统计") + taskAllListCmd.Flags().String("task-sources", "", "任务来源,逗号分隔") + + taskStudentListCmd.Flags().String("students", "", "学生列表 JSON 数组(必填)") + taskStudentListCmd.Flags().Bool("query-all", false, "是否查询全部(含已完成),默认 false") + taskStudentListCmd.Flags().String("cursor", "", "分页游标") + taskStudentListCmd.Flags().String("limit", "", "每页条数") + taskStudentListCmd.Flags().String("task-sources", "", "任务来源,逗号分隔") + + // report flags + reportGetCmd.Flags().String("ids", "", "成绩单 ID 列表,逗号分隔(必填)") + + reportByTeacherCmd.Flags().Int64("page", 1, "页码,从 1 开始") + reportByTeacherCmd.Flags().Int64("limit", 20, "每页大小") + reportByTeacherCmd.Flags().Int64("status", 0, "成绩单状态:0-未发布 1-已发布") + + reportByClassCmd.Flags().String("report-id", "", "成绩单 ID(必填)") + reportByClassCmd.Flags().String("class-id", "", "班级 ID(必填)") + reportByClassCmd.Flags().String("student-ids", "", "学生 userId 列表,逗号分隔(可选)") + + reportByStudentListCmd.Flags().String("class-id", "", "班级 ID(必填)") + reportByStudentListCmd.Flags().String("student-id", "", "学生 userId(必填)") + reportByStudentListCmd.Flags().Int64("page", 1, "页码,从 1 开始") + reportByStudentListCmd.Flags().Int64("limit", 20, "每页大小") + + reportByStudentDetailCmd.Flags().String("report-id", "", "成绩单 ID(必填)") + reportByStudentDetailCmd.Flags().String("student-id", "", "学生 userId(必填)") + reportByStudentDetailCmd.Flags().String("class-id", "", "班级 ID(必填)") + + // notice flags + noticeConfirmCmd.Flags().String("notice-id", "", "通知 ID(必填)") + noticeConfirmCmd.Flags().String("student-id", "", "学生 userId(必填)") + noticeConfirmCmd.Flags().String("device-id", "", "设备 ID(可选)") + noticeConfirmCmd.Flags().String("parent-name", "", "确认操作人名字(可选)") + noticeConfirmCmd.Flags().Bool("update-sign", false, "是否更新签名(可选)") + + noticeCreateCmd.Flags().String("identifer", "", "幂等字段,建议 orgId-staffId-UUID(必填)") + noticeCreateCmd.Flags().String("content", "", "通知内容(必填)") + noticeCreateCmd.Flags().String("title", "", "通知标题(可选)") + noticeCreateCmd.Flags().String("class-ids", "", "班级 ID 列表,逗号分隔(可选)") + noticeCreateCmd.Flags().String("class-names", "", "班级名称列表,逗号分隔(可选)") + noticeCreateCmd.Flags().String("class-selected-students", "", "班级被选中学生 JSON 对象(可选)") + noticeCreateCmd.Flags().String("type", "", "类型(可选)") + noticeCreateCmd.Flags().String("scope", "", "范围(可选)") + noticeCreateCmd.Flags().String("target-role", "", "目标角色(可选)") + noticeCreateCmd.Flags().String("is-signed", "", "是否签收(可选)") + noticeCreateCmd.Flags().String("photo", "", "图片 URL(可选)") + noticeCreateCmd.Flags().String("media", "", "媒体 URL(可选)") + noticeCreateCmd.Flags().String("audio", "", "音频 URL(可选)") + noticeCreateCmd.Flags().Bool("send-ding", false, "是否发送钉钉(可选)") + noticeCreateCmd.Flags().String("scheduled-release", "", "定时发送(可选)") + noticeCreateCmd.Flags().Int64("notice-deadline", 0, "通知截止时间戳(可选)") + noticeCreateCmd.Flags().String("notice-deadline-open", "", "是否开启通知截止时间(可选)") + noticeCreateCmd.Flags().String("notice-deadline-setting", "", "通知截止时间设置(可选)") + noticeCreateCmd.Flags().String("attributes", "", "扩展属性 JSON 对象(可选)") + noticeCreateCmd.Flags().String("user-name", "", "用户名(可选)") + + noticeDeleteCmd.Flags().String("notice-id", "", "通知 ID(必填)") + noticeDeleteCmd.Flags().String("user-name", "", "用户名(可选)") + + noticeListByTeacherCmd.Flags().String("class-id", "", "班级 ID(可选)") + noticeListByTeacherCmd.Flags().String("type", "", "通知类型(可选)") + noticeListByTeacherCmd.Flags().String("status", "", "通知状态筛选(可选)") + noticeListByTeacherCmd.Flags().String("user-name", "", "用户名(可选)") + noticeListByTeacherCmd.Flags().Int64("page", 1, "页码,从 1 开始") + noticeListByTeacherCmd.Flags().Int64("page-size", 20, "每页大小") + + noticeGetCmd.Flags().String("notice-id", "", "通知 ID(必填)") + noticeGetCmd.Flags().String("user-name", "", "用户名(可选)") + + noticeConfirmStatusCmd.Flags().String("notice-id", "", "通知 ID(必填)") + noticeConfirmStatusCmd.Flags().String("class-id", "", "班级 ID(必填)") + noticeConfirmStatusCmd.Flags().String("status", "", "确认状态筛选(可选)") + noticeConfirmStatusCmd.Flags().String("user-name", "", "用户名(可选)") + noticeConfirmStatusCmd.Flags().Int64("page", 1, "页码,从 1 开始") + noticeConfirmStatusCmd.Flags().Int64("page-size", 20, "每页大小") + + noticeListByStudentCmd.Flags().String("student-id", "", "学生 ID(必填)") + noticeListByStudentCmd.Flags().String("class-id", "", "班级 ID(必填)") + noticeListByStudentCmd.Flags().String("status", "", "通知状态筛选(可选)") + noticeListByStudentCmd.Flags().String("user-name", "", "用户名(可选)") + noticeListByStudentCmd.Flags().Int64("page", 1, "页码,从 1 开始") + noticeListByStudentCmd.Flags().Int64("page-size", 20, "每页大小") + + // circle flags + circlePostsCmd.Flags().String("class-id", "", "班级 ID(必填)") + circlePostsCmd.Flags().String("student-id", "", "学生 userId(必填)") + circlePostsCmd.Flags().String("target-role", "", "用户角色:guardian(家长)/ student(学生)(必填)") + + // card flags + cardUpdateCmd.Flags().String("card-id", "", "打卡卡片 ID(必填)") + cardUpdateCmd.Flags().String("identifier", "", "幂等标识,建议 orgId-staffId-UUID(必填)") + cardUpdateCmd.Flags().String("title", "", "修改后的标题(与 --content 至少传一个)") + cardUpdateCmd.Flags().String("content", "", "修改后的内容(与 --title 至少传一个)") + cardUpdateCmd.Flags().Bool("should-send-update-msg", false, "是否发送更新通知消息,默认 false") + + cardEndCmd.Flags().String("card-id", "", "打卡卡片 ID(必填)") + + cardListCmd.Flags().String("status", "", "打卡状态:FINISH-已完结 UNFINISH-进行中(必填)") + cardListCmd.Flags().Int64("class-id", 0, "班级 ID(可选)") + cardListCmd.Flags().Int64("page", 1, "页码,从 1 开始") + cardListCmd.Flags().Int64("limit", 10, "每页大小") + + cardUserStatisticCmd.Flags().String("class-id", "", "班级 ID(必填)") + cardUserStatisticCmd.Flags().String("task-code", "", "任务 code(必填)") + cardUserStatisticCmd.Flags().String("card-id", "", "打卡卡片 ID(必填)") + cardUserStatisticCmd.Flags().Bool("finish", false, "是否查询已完成人员,默认 false(查未完成)") + cardUserStatisticCmd.Flags().Int64("page", 1, "页码,从 1 开始") + cardUserStatisticCmd.Flags().Int64("limit", 10, "每页大小") + + cardFinishInfoCmd.Flags().String("card-id", "", "打卡卡片 ID(必填)") + cardFinishInfoCmd.Flags().String("card-biz-id", "", "打卡业务 ID(必填)") + cardFinishInfoCmd.Flags().String("target-role", "", "目标角色:teacher / guardian / student / headmaster(可选,未传按 uid 真实身份自动推断)") + cardFinishInfoCmd.Flags().String("student-id", "", "学生 ID(可选,targetRole 为 guardian 时用于指定查看某个孩子的进度)") + + // diploma flags + diplomaCreateCmd.Flags().String("identifier", "", "幂等标识,建议 orgId-staffId-UUID(必填)") + diplomaCreateCmd.Flags().String("content", "", "奖状内容(必填)") + diplomaCreateCmd.Flags().String("user-name", "", "获奖人用户名(必填)") + diplomaCreateCmd.Flags().String("title", "", "奖状标题(可选)") + diplomaCreateCmd.Flags().String("unit-name", "", "颁发单位名称(可选)") + diplomaCreateCmd.Flags().String("tag", "", "奖状标签(可选)") + diplomaCreateCmd.Flags().String("photo", "", "奖状图片 URL(可选)") + diplomaCreateCmd.Flags().String("publish-time", "", "颁发时间(可选)") + diplomaCreateCmd.Flags().String("biz-code", "", "业务编码(可选)") + diplomaCreateCmd.Flags().String("biz-category", "", "业务种类(可选)") + diplomaCreateCmd.Flags().String("msg-type", "", "消息类型(可选)") + diplomaCreateCmd.Flags().String("template-url", "", "模板 URL(可选)") + diplomaCreateCmd.Flags().String("class-ids", "", "班级 ID 列表,逗号分隔(可选)") + diplomaCreateCmd.Flags().String("select-class", "", "选择的班级及学生 JSON 数组(可选)") + diplomaCreateCmd.Flags().String("attributes", "", "扩展属性 JSON 对象(可选)") + + diplomaReadCmd.Flags().String("diploma-id", "", "奖状 ID(必填)") + diplomaReadCmd.Flags().String("class-id", "", "班级 ID(可选)") + diplomaReadCmd.Flags().String("student-id", "", "学生 ID(可选)") + diplomaReadCmd.Flags().String("user-name", "", "用户名(可选)") + + diplomaListByTeacherCmd.Flags().Int64("page", 1, "页码,从 1 开始") + diplomaListByTeacherCmd.Flags().Int64("limit", 20, "每页大小") + diplomaListByTeacherCmd.Flags().String("status", "", "奖状状态筛选(可选)") + diplomaListByTeacherCmd.Flags().String("tag", "", "标签筛选(可选)") + diplomaListByTeacherCmd.Flags().String("user-name", "", "获奖人用户名筛选(可选)") + + diplomaGetCmd.Flags().String("diploma-id", "", "奖状 ID(必填)") + diplomaGetCmd.Flags().String("user-name", "", "用户名(可选)") + + diplomaStatisticsCmd.Flags().String("diploma-id", "", "奖状 ID(必填)") + diplomaStatisticsCmd.Flags().String("user-name", "", "用户名(可选)") + + diplomaDetailCmd.Flags().String("diploma-id", "", "奖状 ID(必填)") + diplomaDetailCmd.Flags().String("class-id", "", "班级 ID(可选)") + diplomaDetailCmd.Flags().String("user-name", "", "用户名(可选)") + + diplomaListByStudentCmd.Flags().String("student-id", "", "学生 ID(必填)") + diplomaListByStudentCmd.Flags().String("class-id", "", "班级 ID(必填)") + diplomaListByStudentCmd.Flags().Int64("page", 1, "页码,从 1 开始") + diplomaListByStudentCmd.Flags().Int64("limit", 20, "每页大小") + diplomaListByStudentCmd.Flags().String("user-name", "", "用户名(可选)") + + diplomaStudentDetailCmd.Flags().String("diploma-id", "", "奖状 ID(必填)") + diplomaStudentDetailCmd.Flags().String("student-id", "", "学生 ID(必填)") + diplomaStudentDetailCmd.Flags().String("class-id", "", "班级 ID(必填)") + diplomaStudentDetailCmd.Flags().String("user-name", "", "用户名(可选)") + + homeworkCreateCmd.Flags().String("identifier", "", "幂等标识,建议 orgId-staffId-UUID(必填)") + homeworkCreateCmd.Flags().String("hw-content", "", "作业内容(必填)") + homeworkCreateCmd.Flags().String("hw-title", "", "作业标题(可选)") + homeworkCreateCmd.Flags().String("hw-photo", "", "作业图片 URL(可选)") + homeworkCreateCmd.Flags().String("hw-media", "", "作业视频 URL(可选)") + homeworkCreateCmd.Flags().String("hw-video", "", "作业录音 URL(可选)") + homeworkCreateCmd.Flags().String("class-ids", "", "班级 ID 列表,逗号分隔(可选)") + homeworkCreateCmd.Flags().String("class-names", "", "班级名称列表,逗号分隔(可选)") + homeworkCreateCmd.Flags().String("class-selected-students", "", "班级被选中学生 JSON 对象(可选)") + homeworkCreateCmd.Flags().String("feedback", "", "反馈(可选)") + homeworkCreateCmd.Flags().String("hw-deadline", "", "作业截止时间戳(可选)") + homeworkCreateCmd.Flags().String("hw-deadline-open", "", "是否开启作业截止时间(可选)") + homeworkCreateCmd.Flags().String("hw-deadline-setting", "", "作业截止时间设置(可选)") + homeworkCreateCmd.Flags().String("submit-types", "", "提交类型列表,逗号分隔(可选)") + homeworkCreateCmd.Flags().String("hw-type", "", "作业类型(可选)") + homeworkCreateCmd.Flags().String("target-role", "", "目标角色(可选)") + homeworkCreateCmd.Flags().String("publish-type", "", "发布类型(可选)") + homeworkCreateCmd.Flags().String("biz-code", "", "业务代码(可选)") + homeworkCreateCmd.Flags().String("scheduled-release", "", "定时发送(可选)") + homeworkCreateCmd.Flags().String("task-plan-duration", "", "计划任务时长(可选)") + homeworkCreateCmd.Flags().String("attributes", "", "扩展属性 JSON 对象(可选)") + homeworkCreateCmd.Flags().String("user-name", "", "用户名(可选)") + + homeworkDeleteCmd.Flags().String("homework-id", "", "作业 ID(必填)") + homeworkDeleteCmd.Flags().String("user-name", "", "用户名(可选)") + + homeworkSubmitCmd.Flags().String("hw-content-detail-id", "", "作业内容详情 ID(必填)") + homeworkSubmitCmd.Flags().String("homework-id", "", "作业 ID(可选)") + homeworkSubmitCmd.Flags().String("student-id", "", "学生 ID(可选)") + homeworkSubmitCmd.Flags().String("class-id", "", "班级 ID(可选)") + homeworkSubmitCmd.Flags().String("content", "", "提交内容(可选)") + homeworkSubmitCmd.Flags().String("photo", "", "提交图片 URL(可选)") + homeworkSubmitCmd.Flags().String("media", "", "提交视频 URL(可选)") + homeworkSubmitCmd.Flags().String("video", "", "提交录音 URL(可选)") + homeworkSubmitCmd.Flags().String("user-name", "", "用户名(可选)") + + homeworkGetCmd.Flags().String("homework-id", "", "作业 ID(必填)") + homeworkGetCmd.Flags().String("user-name", "", "用户名(可选)") + + homeworkClassByHomeworkCmd.Flags().String("homework-id", "", "作业 ID(必填)") + homeworkClassByHomeworkCmd.Flags().String("class-id", "", "班级 ID(可选)") + homeworkClassByHomeworkCmd.Flags().String("user-name", "", "用户名(可选)") + + homeworkClassDetailCmd.Flags().String("homework-id", "", "作业 ID(必填)") + homeworkClassDetailCmd.Flags().String("class-id", "", "班级 ID(必填)") + homeworkClassDetailCmd.Flags().String("user-name", "", "用户名(必填)") + + homeworkSubmitStatisticsCmd.Flags().String("homework-id", "", "作业 ID(必填)") + homeworkSubmitStatisticsCmd.Flags().String("class-id", "", "班级 ID(必填)") + homeworkSubmitStatisticsCmd.Flags().String("user-name", "", "用户名(可选)") + + homeworkListByStudentCmd.Flags().String("student-id", "", "学生 ID(必填)") + homeworkListByStudentCmd.Flags().String("class-id", "", "班级 ID(必填)") + homeworkListByStudentCmd.Flags().String("user-name", "", "用户名(必填)") + homeworkListByStudentCmd.Flags().String("status", "", "提交状态筛选(可选)") + homeworkListByStudentCmd.Flags().Int64("page", 1, "页码,从 1 开始") + homeworkListByStudentCmd.Flags().Int64("page-size", 20, "每页大小") + + homeworkStudentDetailCmd.Flags().String("homework-id", "", "作业 ID(必填)") + homeworkStudentDetailCmd.Flags().String("student-id", "", "学生 ID(必填)") + homeworkStudentDetailCmd.Flags().String("class-id", "", "班级 ID(必填)") + homeworkStudentDetailCmd.Flags().String("user-name", "", "用户名(可选)") + + homeworkListByTeacherCmd.Flags().String("class-id", "", "班级 ID(可选)") + homeworkListByTeacherCmd.Flags().String("type", "", "作业类型(可选)") + homeworkListByTeacherCmd.Flags().String("status", "", "作业状态筛选(可选)") + homeworkListByTeacherCmd.Flags().String("user-name", "", "用户名(可选)") + homeworkListByTeacherCmd.Flags().Int64("page", 1, "页码,从 1 开始") + homeworkListByTeacherCmd.Flags().Int64("page-size", 20, "每页大小") + + homeworkCreateCommentCmd.Flags().String("comment", "", "评语文字内容(必填)") + homeworkCreateCommentCmd.Flags().String("hw-content-detail-id", "", "作业内容详情 ID(必填)") + homeworkCreateCommentCmd.Flags().Int64("homework-id", 0, "作业 ID(可选)") + homeworkCreateCommentCmd.Flags().String("student-id", "", "学生 ID(可选)") + homeworkCreateCommentCmd.Flags().String("photo", "", "评语图片 URL(可选)") + homeworkCreateCommentCmd.Flags().String("video", "", "评语录音 URL(可选)") + homeworkCreateCommentCmd.Flags().String("media", "", "评语视频 URL(可选)") + homeworkCreateCommentCmd.Flags().String("user-name", "", "用户名(可选)") + + diplomaDeleteCmd.Flags().String("diploma-id", "", "奖状 ID(必填)") + diplomaDeleteCmd.Flags().String("user-name", "", "用户名(可选)") + + messageCmd.AddCommand(messageSummaryListCmd) + taskCmd.AddCommand(taskPublishListCmd, taskAllListCmd, taskStudentListCmd) + reportCmd.AddCommand(reportGetCmd, reportByTeacherCmd, reportByClassCmd, reportByStudentListCmd, reportByStudentDetailCmd) + noticeCmd.AddCommand(noticeConfirmCmd, noticeCreateCmd, noticeDeleteCmd, noticeListByTeacherCmd, noticeGetCmd, noticeConfirmStatusCmd, noticeListByStudentCmd) + circleCmd.AddCommand(circlePostsCmd) + cardCmd.AddCommand(cardUpdateCmd, cardEndCmd, cardListCmd, cardUserStatisticCmd, cardFinishInfoCmd) + homeworkCmd.AddCommand(homeworkCreateCmd, homeworkDeleteCmd, homeworkSubmitCmd, homeworkGetCmd, homeworkClassByHomeworkCmd, homeworkClassDetailCmd, homeworkSubmitStatisticsCmd, homeworkListByStudentCmd, homeworkStudentDetailCmd, homeworkListByTeacherCmd, homeworkCreateCommentCmd) + diplomaCmd.AddCommand(diplomaCreateCmd, diplomaReadCmd, diplomaListByTeacherCmd, diplomaGetCmd, diplomaStatisticsCmd, diplomaDetailCmd, diplomaListByStudentCmd, diplomaStudentDetailCmd, diplomaDeleteCmd) + + root.AddCommand(messageCmd, taskCmd, reportCmd, noticeCmd, circleCmd, cardCmd, diplomaCmd, homeworkCmd) + + return root +} + +// eduAppRequiredIntFlag extracts a required integer flag, returning an error +// if the flag is empty or not a valid integer. +func eduAppRequiredIntFlag(cmd *cobra.Command, name string) (int64, error) { + v, _ := cmd.Flags().GetString(name) + v = strings.TrimSpace(v) + if v == "" { + return 0, fmt.Errorf("--%s 为必填参数", name) + } + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return 0, fmt.Errorf("--%s 须为整数: %w", name, err) + } + return n, nil +} + +// eduAppParseCSV splits a comma-separated string into trimmed non-empty values. +func eduAppParseCSV(raw string) []string { + parts := strings.Split(raw, ",") + result := make([]string, 0, len(parts)) + for _, p := range parts { + v := strings.TrimSpace(p) + if v != "" { + result = append(result, v) + } + } + return result +} + +// eduAppParseIntCSV splits a comma-separated string into int64 values. +func eduAppParseIntCSV(raw string) ([]int64, error) { + parts := strings.Split(raw, ",") + result := make([]int64, 0, len(parts)) + for _, p := range parts { + v := strings.TrimSpace(p) + if v == "" { + continue + } + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid integer %q", v) + } + result = append(result, n) + } + return result, nil +} diff --git a/internal/helpers/edu_app_test.go b/internal/helpers/edu_app_test.go new file mode 100644 index 00000000..c12a83e4 --- /dev/null +++ b/internal/helpers/edu_app_test.go @@ -0,0 +1,400 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package helpers + +import ( + "io" + "strings" + "testing" +) + +func withEduAppCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{dryRun: true} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +func runEduApp(t *testing.T, args ...string) error { + t.Helper() + cmd := newEduAppCommand() + cmd.SetArgs(args) + return cmd.Execute() +} + +// TestEduAppHappyPathsFullFlags exercises each leaf command with every flag +// populated, so all optional-field branches and the dispatch line are covered. +func TestEduAppHappyPathsFullFlags(t *testing.T) { + cases := [][]string{ + {"message", "summary-list", "--class-id", "1", "--cid", "c", "--target-role", "guardian", "--status", "0"}, + + {"task", "publish-list", "--cursor", "5", "--limit", "10", "--need-statistic", "--task-sources", "EDU_HOMEWORK,EDU_NOTICE"}, + {"task", "all-list", "--biz-id", "1", "--cursor", "5", "--limit", "10", "--need-statistic", "--task-sources", "EDU_CARD"}, + {"task", "student-list", "--students", `[{"userId":"u1","bizId":"1"}]`, "--query-all", "--cursor", "c", "--limit", "10", "--task-sources", "EDU_SR"}, + + {"report", "get", "--ids", "1001,1002"}, + {"report", "by-teacher", "--page", "1", "--limit", "20", "--status", "1"}, + {"report", "by-class", "--report-id", "1001", "--class-id", "12345", "--student-ids", "u1,u2"}, + {"report", "by-student-list", "--class-id", "12345", "--student-id", "u1", "--page", "1", "--limit", "20"}, + {"report", "by-student-detail", "--report-id", "1001", "--student-id", "u1", "--class-id", "12345"}, + + {"notice", "confirm", "--notice-id", "n1", "--student-id", "u1", "--device-id", "d1", "--parent-name", "张三", "--update-sign"}, + {"notice", "create", "--identifer", "org1-staff1-uuid", "--content", "明天放假", "--title", "放假", + "--class-ids", "1,2", "--class-names", "一班,二班", "--class-selected-students", `{"1":["u1"]}`, + "--type", "SCHOOL", "--scope", "ALL", "--target-role", "guardian", "--is-signed", "true", + "--photo", "p", "--media", "m", "--audio", "a", "--send-ding", "--scheduled-release", "2026-07-29", + "--notice-deadline", "100", "--notice-deadline-open", "true", "--notice-deadline-setting", "s", + "--attributes", `{"k":"v"}`, "--user-name", "张三"}, + {"notice", "delete", "--notice-id", "12345", "--user-name", "张三"}, + {"notice", "list-by-teacher", "--class-id", "c", "--type", "SCHOOL", "--status", "FINISHED", "--user-name", "u", "--page", "1", "--page-size", "20"}, + {"notice", "get", "--notice-id", "12345", "--user-name", "张三"}, + {"notice", "confirm-status", "--notice-id", "12345", "--class-id", "c", "--status", "CONFIRMED", "--user-name", "u", "--page", "1", "--page-size", "20"}, + {"notice", "list-by-student", "--student-id", "u1", "--class-id", "c", "--status", "FINISHED", "--user-name", "u", "--page", "1", "--page-size", "20"}, + + {"circle", "posts", "--class-id", "12345", "--student-id", "u1", "--target-role", "guardian"}, + + {"card", "update", "--card-id", "1", "--identifier", "org1-staff1-uuid", "--title", "新标题", "--content", "新内容", "--should-send-update-msg"}, + {"card", "end", "--card-id", "1"}, + {"card", "list", "--status", "UNFINISH", "--class-id", "5", "--page", "1", "--limit", "10"}, + {"card", "user-statistic", "--card-id", "1", "--task-code", "code1", "--class-id", "cid1", "--finish", "--page", "1", "--limit", "20"}, + {"card", "finish-info", "--card-id", "1", "--card-biz-id", "bid1", "--target-role", "guardian", "--student-id", "stu1"}, + + {"diploma", "create", "--identifier", "org1-staff1-uuid", "--content", "期末三好学生", "--user-name", "张三", + "--title", "三好学生", "--unit-name", "实验小学", "--tag", "三好", "--photo", "p", "--publish-time", "2026-07-29", + "--biz-code", "bc", "--biz-category", "cat", "--msg-type", "mt", "--template-url", "tpl", + "--class-ids", "1,2", "--select-class", `[{"classId":"1"}]`, "--attributes", `{"k":"v"}`}, + {"diploma", "read", "--diploma-id", "1", "--class-id", "c", "--student-id", "u1", "--user-name", "张三"}, + {"diploma", "list-by-teacher", "--page", "1", "--limit", "20", "--status", "PUBLISHED", "--tag", "三好", "--user-name", "u"}, + {"diploma", "get", "--diploma-id", "1", "--user-name", "张三"}, + {"diploma", "statistics", "--diploma-id", "1", "--user-name", "张三"}, + {"diploma", "detail", "--diploma-id", "1", "--class-id", "c", "--user-name", "张三"}, + {"diploma", "list-by-student", "--student-id", "u1", "--class-id", "c", "--page", "1", "--limit", "20", "--user-name", "张三"}, + {"diploma", "student-detail", "--diploma-id", "1", "--student-id", "u1", "--class-id", "c", "--user-name", "张三"}, + {"diploma", "delete", "--diploma-id", "1", "--user-name", "张三"}, + + {"homework", "create", "--identifier", "org1-staff1-uuid", "--hw-content", "完成练习", + "--hw-title", "数学作业", "--hw-photo", "p", "--hw-media", "m", "--hw-video", "v", + "--class-ids", "1,2", "--class-names", "一班,二班", "--class-selected-students", `{"1":["u1"]}`, + "--feedback", "fb", "--hw-deadline", "100", "--hw-deadline-open", "true", "--hw-deadline-setting", "s", + "--submit-types", "TEXT,PHOTO", "--hw-type", "HOMEWORK", "--target-role", "guardian", "--publish-type", "NOW", + "--biz-code", "bc", "--scheduled-release", "2026-07-29", "--task-plan-duration", "5", "--attributes", `{"k":"v"}`, "--user-name", "张三"}, + {"homework", "delete", "--homework-id", "1", "--user-name", "张三"}, + {"homework", "submit", "--hw-content-detail-id", "1", "--homework-id", "2", "--student-id", "u1", "--class-id", "c", + "--content", "已完成", "--photo", "p", "--media", "m", "--video", "v", "--user-name", "张三"}, + {"homework", "get", "--homework-id", "1", "--user-name", "张三"}, + {"homework", "class-by-homework", "--homework-id", "1", "--class-id", "c", "--user-name", "张三"}, + {"homework", "class-detail", "--homework-id", "1", "--class-id", "c", "--user-name", "张三"}, + {"homework", "submit-statistics", "--homework-id", "1", "--class-id", "c", "--user-name", "张三"}, + {"homework", "list-by-student", "--student-id", "u1", "--class-id", "c", "--user-name", "张三", "--status", "FINISHED", "--page", "1", "--page-size", "20"}, + {"homework", "student-detail", "--homework-id", "1", "--student-id", "u1", "--class-id", "c", "--user-name", "张三"}, + {"homework", "list-by-teacher", "--class-id", "c", "--type", "HOMEWORK", "--status", "FINISHED", "--user-name", "u", "--page", "1", "--page-size", "20"}, + {"homework", "create-comment", "--comment", "做得很好", "--hw-content-detail-id", "1", "--homework-id", "2", + "--student-id", "u1", "--photo", "p", "--video", "v", "--media", "m", "--user-name", "张三"}, + } + for _, args := range cases { + t.Run(strings.Join(args, " "), func(t *testing.T) { + withEduAppCaller(t) + if err := runEduApp(t, args...); err != nil { + t.Fatalf("Execute(%v) = %v, want nil", args, err) + } + }) + } +} + +// TestEduAppZeroPagination drives the pagination defaulting branches (page<=0 / +// page-size<=0 / the >0 else arms) that the positive-value happy paths skip. +func TestEduAppZeroPagination(t *testing.T) { + cases := [][]string{ + {"report", "by-teacher", "--page", "0", "--limit", "0"}, + {"report", "by-student-list", "--class-id", "c", "--student-id", "u1", "--page", "0", "--limit", "0"}, + {"notice", "list-by-teacher", "--page", "0", "--page-size", "0"}, + {"notice", "confirm-status", "--notice-id", "1", "--class-id", "c", "--page", "0", "--page-size", "0"}, + {"notice", "list-by-student", "--student-id", "u1", "--class-id", "c", "--page", "0", "--page-size", "0"}, + {"card", "list", "--status", "FINISH", "--page", "0", "--limit", "0"}, + {"card", "user-statistic", "--card-id", "1", "--task-code", "code1", "--class-id", "cid1", "--page", "0", "--limit", "0"}, + {"diploma", "list-by-teacher", "--page", "0", "--limit", "0"}, + {"diploma", "list-by-student", "--student-id", "u1", "--class-id", "c", "--page", "0", "--limit", "0"}, + {"homework", "list-by-student", "--student-id", "u1", "--class-id", "c", "--user-name", "张三", "--page", "0", "--page-size", "0"}, + {"homework", "list-by-teacher", "--page", "0", "--page-size", "0"}, + } + for _, args := range cases { + t.Run(strings.Join(args, " "), func(t *testing.T) { + withEduAppCaller(t) + if err := runEduApp(t, args...); err != nil { + t.Fatalf("Execute(%v) = %v, want nil", args, err) + } + }) + } +} + +func TestEduAppErrorPaths(t *testing.T) { + cases := []struct { + name string + args []string + want string + }{ + {"summary-list missing class-id", []string{"message", "summary-list", "--cid", "c", "--target-role", "guardian", "--status", "0"}, "class-id"}, + {"summary-list non-int class-id", []string{"message", "summary-list", "--class-id", "abc", "--cid", "c", "--target-role", "guardian", "--status", "0"}, "整数"}, + {"summary-list missing cid", []string{"message", "summary-list", "--class-id", "1", "--target-role", "guardian", "--status", "0"}, "cid"}, + {"summary-list missing target-role", []string{"message", "summary-list", "--class-id", "1", "--cid", "c", "--status", "0"}, "target-role"}, + {"summary-list missing status", []string{"message", "summary-list", "--class-id", "1", "--cid", "c", "--target-role", "guardian"}, "status"}, + {"summary-list non-int status", []string{"message", "summary-list", "--class-id", "1", "--cid", "c", "--target-role", "guardian", "--status", "x"}, "status"}, + + {"all-list missing biz-id", []string{"task", "all-list"}, "biz-id"}, + {"student-list missing students", []string{"task", "student-list"}, "students"}, + {"student-list bad json", []string{"task", "student-list", "--students", "{"}, "JSON"}, + + {"report get missing ids", []string{"report", "get"}, "ids"}, + {"report get non-int ids", []string{"report", "get", "--ids", "abc"}, "整数"}, + {"report by-class missing report-id", []string{"report", "by-class", "--class-id", "c"}, "report-id"}, + {"report by-class non-int report-id", []string{"report", "by-class", "--report-id", "x", "--class-id", "c"}, "整数"}, + {"report by-class missing class-id", []string{"report", "by-class", "--report-id", "1"}, "class-id"}, + {"report by-student-list missing class-id", []string{"report", "by-student-list", "--student-id", "u1"}, "class-id"}, + {"report by-student-list missing student-id", []string{"report", "by-student-list", "--class-id", "c"}, "student-id"}, + {"report by-student-detail missing report-id", []string{"report", "by-student-detail", "--student-id", "u1", "--class-id", "c"}, "report-id"}, + {"report by-student-detail non-int report-id", []string{"report", "by-student-detail", "--report-id", "x", "--student-id", "u1", "--class-id", "c"}, "整数"}, + {"report by-student-detail missing student-id", []string{"report", "by-student-detail", "--report-id", "1", "--class-id", "c"}, "student-id"}, + {"report by-student-detail missing class-id", []string{"report", "by-student-detail", "--report-id", "1", "--student-id", "u1"}, "class-id"}, + + {"notice confirm missing notice-id", []string{"notice", "confirm", "--student-id", "u1"}, "notice-id"}, + {"notice confirm missing student-id", []string{"notice", "confirm", "--notice-id", "n1"}, "student-id"}, + {"notice create missing identifer", []string{"notice", "create", "--content", "c"}, "identifer"}, + {"notice create missing content", []string{"notice", "create", "--identifer", "x"}, "content"}, + {"notice create bad selected-students", []string{"notice", "create", "--identifer", "x", "--content", "c", "--class-selected-students", "{"}, "class-selected-students"}, + {"notice create bad attributes", []string{"notice", "create", "--identifer", "x", "--content", "c", "--attributes", "{"}, "attributes"}, + {"notice delete missing notice-id", []string{"notice", "delete"}, "notice-id"}, + {"notice delete non-int notice-id", []string{"notice", "delete", "--notice-id", "x"}, "整数"}, + {"notice get missing notice-id", []string{"notice", "get"}, "notice-id"}, + {"notice confirm-status missing notice-id", []string{"notice", "confirm-status", "--class-id", "c"}, "notice-id"}, + {"notice confirm-status missing class-id", []string{"notice", "confirm-status", "--notice-id", "1"}, "class-id"}, + {"notice list-by-student missing student-id", []string{"notice", "list-by-student", "--class-id", "c"}, "student-id"}, + {"notice list-by-student missing class-id", []string{"notice", "list-by-student", "--student-id", "u1"}, "class-id"}, + + {"circle posts missing class-id", []string{"circle", "posts", "--student-id", "u1", "--target-role", "guardian"}, "class-id"}, + {"circle posts missing student-id", []string{"circle", "posts", "--class-id", "c", "--target-role", "guardian"}, "student-id"}, + {"circle posts missing target-role", []string{"circle", "posts", "--class-id", "c", "--student-id", "u1"}, "target-role"}, + + {"card update missing card-id", []string{"card", "update", "--identifier", "i", "--title", "t"}, "card-id"}, + {"card update missing identifier", []string{"card", "update", "--card-id", "1", "--title", "t"}, "identifier"}, + {"card update no title no content", []string{"card", "update", "--card-id", "1", "--identifier", "i"}, "至少传一个"}, + {"card end missing card-id", []string{"card", "end"}, "card-id"}, + {"card list missing status", []string{"card", "list"}, "status"}, + {"card list invalid status", []string{"card", "list", "--status", "OTHER"}, "FINISH"}, + {"card user-statistic missing card-id", []string{"card", "user-statistic", "--task-code", "c", "--class-id", "c"}, "card-id"}, + {"card user-statistic missing task-code", []string{"card", "user-statistic", "--card-id", "1", "--class-id", "c"}, "task-code"}, + {"card user-statistic missing class-id", []string{"card", "user-statistic", "--card-id", "1", "--task-code", "c"}, "class-id"}, + {"card finish-info missing card-id", []string{"card", "finish-info", "--card-biz-id", "b"}, "card-id"}, + {"card finish-info missing card-biz-id", []string{"card", "finish-info", "--card-id", "1"}, "card-biz-id"}, + {"card finish-info invalid target-role", []string{"card", "finish-info", "--card-id", "1", "--card-biz-id", "b", "--target-role", "boss"}, "target-role"}, + + {"diploma create missing identifier", []string{"diploma", "create", "--content", "c", "--user-name", "u"}, "identifier"}, + {"diploma create missing content", []string{"diploma", "create", "--identifier", "i", "--user-name", "u"}, "content"}, + {"diploma create missing user-name", []string{"diploma", "create", "--identifier", "i", "--content", "c"}, "user-name"}, + {"diploma create bad select-class", []string{"diploma", "create", "--identifier", "i", "--content", "c", "--user-name", "u", "--select-class", "{"}, "select-class"}, + {"diploma create bad attributes", []string{"diploma", "create", "--identifier", "i", "--content", "c", "--user-name", "u", "--attributes", "{"}, "attributes"}, + {"diploma read missing diploma-id", []string{"diploma", "read"}, "diploma-id"}, + {"diploma get missing diploma-id", []string{"diploma", "get"}, "diploma-id"}, + {"diploma statistics missing diploma-id", []string{"diploma", "statistics"}, "diploma-id"}, + {"diploma detail missing diploma-id", []string{"diploma", "detail"}, "diploma-id"}, + {"diploma list-by-student missing student-id", []string{"diploma", "list-by-student", "--class-id", "c"}, "student-id"}, + {"diploma list-by-student missing class-id", []string{"diploma", "list-by-student", "--student-id", "u1"}, "class-id"}, + {"diploma student-detail missing diploma-id", []string{"diploma", "student-detail", "--student-id", "u1", "--class-id", "c"}, "diploma-id"}, + {"diploma student-detail missing student-id", []string{"diploma", "student-detail", "--diploma-id", "1", "--class-id", "c"}, "student-id"}, + {"diploma student-detail missing class-id", []string{"diploma", "student-detail", "--diploma-id", "1", "--student-id", "u1"}, "class-id"}, + {"diploma delete missing diploma-id", []string{"diploma", "delete"}, "diploma-id"}, + + {"homework create missing identifier", []string{"homework", "create", "--hw-content", "c"}, "identifier"}, + {"homework create missing hw-content", []string{"homework", "create", "--identifier", "i"}, "hw-content"}, + {"homework create bad hw-deadline", []string{"homework", "create", "--identifier", "i", "--hw-content", "c", "--hw-deadline", "x"}, "hw-deadline"}, + {"homework create bad task-plan-duration", []string{"homework", "create", "--identifier", "i", "--hw-content", "c", "--task-plan-duration", "x"}, "task-plan-duration"}, + {"homework create bad selected-students", []string{"homework", "create", "--identifier", "i", "--hw-content", "c", "--class-selected-students", "{"}, "class-selected-students"}, + {"homework create bad attributes", []string{"homework", "create", "--identifier", "i", "--hw-content", "c", "--attributes", "{"}, "attributes"}, + {"homework delete missing homework-id", []string{"homework", "delete"}, "homework-id"}, + {"homework submit missing detail-id", []string{"homework", "submit"}, "hw-content-detail-id"}, + {"homework submit bad homework-id", []string{"homework", "submit", "--hw-content-detail-id", "1", "--homework-id", "x"}, "homework-id"}, + {"homework get missing homework-id", []string{"homework", "get"}, "homework-id"}, + {"homework class-by-homework missing homework-id", []string{"homework", "class-by-homework"}, "homework-id"}, + {"homework class-detail missing homework-id", []string{"homework", "class-detail", "--class-id", "c", "--user-name", "u"}, "homework-id"}, + {"homework class-detail missing class-id", []string{"homework", "class-detail", "--homework-id", "1", "--user-name", "u"}, "class-id"}, + {"homework class-detail missing user-name", []string{"homework", "class-detail", "--homework-id", "1", "--class-id", "c"}, "user-name"}, + {"homework submit-statistics missing homework-id", []string{"homework", "submit-statistics", "--class-id", "c"}, "homework-id"}, + {"homework submit-statistics missing class-id", []string{"homework", "submit-statistics", "--homework-id", "1"}, "class-id"}, + {"homework list-by-student missing student-id", []string{"homework", "list-by-student", "--class-id", "c", "--user-name", "u"}, "student-id"}, + {"homework list-by-student missing class-id", []string{"homework", "list-by-student", "--student-id", "u1", "--user-name", "u"}, "class-id"}, + {"homework list-by-student missing user-name", []string{"homework", "list-by-student", "--student-id", "u1", "--class-id", "c"}, "user-name"}, + {"homework student-detail missing homework-id", []string{"homework", "student-detail", "--student-id", "u1", "--class-id", "c"}, "homework-id"}, + {"homework student-detail missing student-id", []string{"homework", "student-detail", "--homework-id", "1", "--class-id", "c"}, "student-id"}, + {"homework student-detail missing class-id", []string{"homework", "student-detail", "--homework-id", "1", "--student-id", "u1"}, "class-id"}, + {"homework create-comment missing comment", []string{"homework", "create-comment", "--hw-content-detail-id", "1"}, "comment"}, + {"homework create-comment missing detail-id", []string{"homework", "create-comment", "--comment", "cm"}, "hw-content-detail-id"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + withEduAppCaller(t) + err := runEduApp(t, tc.args...) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("Execute(%v) error = %v, want contains %q", tc.args, err, tc.want) + } + }) + } +} + +func TestEduAppParseHelpers(t *testing.T) { + if got := eduAppParseCSV(" a , , b "); len(got) != 2 || got[0] != "a" || got[1] != "b" { + t.Fatalf("eduAppParseCSV = %#v", got) + } + ids, err := eduAppParseIntCSV(" 1 , , 2 ") + if err != nil || len(ids) != 2 || ids[0] != 1 || ids[1] != 2 { + t.Fatalf("eduAppParseIntCSV = %#v, err = %v", ids, err) + } + if _, err := eduAppParseIntCSV("1,bad"); err == nil { + t.Fatalf("eduAppParseIntCSV invalid = nil error") + } +} + +func withEduAppDispatchCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +func TestEduAppDispatch(t *testing.T) { + t.Run("message summary-list dispatches get_ai_message_summary_list", func(t *testing.T) { + caller := withEduAppDispatchCaller(t) + cmd := newEduAppCommand() + cmd.SetArgs([]string{"message", "summary-list", "--class-id", "100", "--cid", "cidxxx", "--target-role", "guardian", "--status", "1"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-app" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-app") + } + if caller.tool != "get_ai_message_summary_list" { + t.Fatalf("tool = %q, want %q", caller.tool, "get_ai_message_summary_list") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] type = %T, want map[string]any", caller.args["input"]) + } + if input["classId"] != int64(100) { + t.Fatalf("classId = %v, want 100", input["classId"]) + } + if input["cid"] != "cidxxx" { + t.Fatalf("cid = %v, want %q", input["cid"], "cidxxx") + } + if input["targetRole"] != "guardian" { + t.Fatalf("targetRole = %v, want %q", input["targetRole"], "guardian") + } + if input["status"] != int64(1) { + t.Fatalf("status = %v, want 1", input["status"]) + } + }) + + t.Run("report get dispatches get_report", func(t *testing.T) { + caller := withEduAppDispatchCaller(t) + cmd := newEduAppCommand() + cmd.SetArgs([]string{"report", "get", "--ids", "1001,1002"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-app" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-app") + } + if caller.tool != "get_report" { + t.Fatalf("tool = %q, want %q", caller.tool, "get_report") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] type = %T, want map[string]any", caller.args["input"]) + } + ids, ok := input["schoolReportIdList"].([]int64) + if !ok || len(ids) != 2 || ids[0] != 1001 || ids[1] != 1002 { + t.Fatalf("schoolReportIdList = %v, want [1001 1002]", input["schoolReportIdList"]) + } + }) + + t.Run("circle posts dispatches query_student_circle_posts", func(t *testing.T) { + caller := withEduAppDispatchCaller(t) + cmd := newEduAppCommand() + cmd.SetArgs([]string{"circle", "posts", "--class-id", "12345", "--student-id", "stu1", "--target-role", "guardian"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-app" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-app") + } + if caller.tool != "query_student_circle_posts" { + t.Fatalf("tool = %q, want %q", caller.tool, "query_student_circle_posts") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] type = %T, want map[string]any", caller.args["input"]) + } + if input["classId"] != "12345" { + t.Fatalf("classId = %v, want %q", input["classId"], "12345") + } + if input["studentId"] != "stu1" { + t.Fatalf("studentId = %v, want %q", input["studentId"], "stu1") + } + if input["targetRole"] != "guardian" { + t.Fatalf("targetRole = %v, want %q", input["targetRole"], "guardian") + } + }) + + t.Run("card end dispatches end_card", func(t *testing.T) { + caller := withEduAppDispatchCaller(t) + cmd := newEduAppCommand() + cmd.SetArgs([]string{"card", "end", "--card-id", "999"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-app" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-app") + } + if caller.tool != "end_card" { + t.Fatalf("tool = %q, want %q", caller.tool, "end_card") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] type = %T, want map[string]any", caller.args["input"]) + } + if input["cardId"] != int64(999) { + t.Fatalf("cardId = %v, want 999", input["cardId"]) + } + }) + + t.Run("card update dispatches update_card", func(t *testing.T) { + caller := withEduAppDispatchCaller(t) + cmd := newEduAppCommand() + cmd.SetArgs([]string{"card", "update", "--card-id", "77", "--identifier", "org1-staff1-uuid", "--title", "新标题", "--should-send-update-msg"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-app" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-app") + } + if caller.tool != "update_card" { + t.Fatalf("tool = %q, want %q", caller.tool, "update_card") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] type = %T, want map[string]any", caller.args["input"]) + } + if input["cardId"] != int64(77) { + t.Fatalf("cardId = %v, want 77", input["cardId"]) + } + if input["identifier"] != "org1-staff1-uuid" { + t.Fatalf("identifier = %v, want %q", input["identifier"], "org1-staff1-uuid") + } + if input["title"] != "新标题" { + t.Fatalf("title = %v, want %q", input["title"], "新标题") + } + if input["shouldSendUpdateMsg"] != true { + t.Fatalf("shouldSendUpdateMsg = %v, want true", input["shouldSendUpdateMsg"]) + } + }) +} diff --git a/internal/helpers/edu_contact.go b/internal/helpers/edu_contact.go new file mode 100644 index 00000000..5548beee --- /dev/null +++ b/internal/helpers/edu_contact.go @@ -0,0 +1,1917 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package helpers + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" +) + +// ────────────────────────────────────────────────────────── +// dws edu-contact — 家校通讯录扩展 +// 共 16 个工具,按 school / class / family / teacher 分组 +// ────────────────────────────────────────────────────────── + +func newEduContactCommand() *cobra.Command { + contract.RegisterProductDecl(contract.ProductDecl{ + ID: "edu-contact", + Selection: contract.ProductSelectionDecl{ + AgentSummary: "家校通讯录:学校组织架构/班级/家庭关系/教师信息查询与管理", + UseWhen: []string{ + "用户要查询或管理钉钉家校通讯录,包括学校组织、班级、学生、教师、家长等信息。", + }, + AvoidWhen: []string{ + "家庭群用 edu-familygroup;班级师生群用 edu-group;家校应用/作业/打卡用 edu-app。", + }, + }, + }) + root := &cobra.Command{ + Use: "edu-contact", + Short: "家校通讯录", + Long: `钉钉家校通讯录扩展:查询学校组织/班级/家庭关系/教师信息等。`, + Hidden: true, + RunE: groupRunE, + } + + // ════════════════════════════════════════════════════════════ + // school 子命令组 — 学校/组织级查询 + // ════════════════════════════════════════════════════════════ + + schoolCmd := &cobra.Command{Use: "school", Short: "学校/组织管理", RunE: groupRunE} + + schoolRolesCmd := &cobra.Command{ + Use: "roles", + Short: "查询用户在组织内的身份", + Long: `查询当前用户在指定组织(学校)内拥有的角色身份。 +返回用户拥有的角色列表:teacher、headmaster、student、guardian、superAdmin、admin 等。`, + Example: ` dws edu-contact school roles + dws edu-contact school roles -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("edu-contact", "query_user_roles_in_org", nil) + }, + } + DeclareLeafMetadata(schoolRolesCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "query_user_roles_in_org", + CanonicalPath: "edu-contact.query_user_roles_in_org", + CLIPath: "edu-contact school roles", + PrimaryCLIPath: "edu-contact school roles", + }, + Description: "查询用户在组织内的身份", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "query_user_roles_in_org"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询用户在组织内的身份角色", + UseWhen: []string{"需要查询当前用户在学校组织内拥有哪些角色身份时"}, + AvoidWhen: []string{"查询班级内角色用 class user-role"}, + Examples: []string{ + "dws edu-contact school roles --format json", + }, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + schoolStructureCmd := &cobra.Command{ + Use: "structure", + Short: "查询学校组织架构", + Long: `查询指定学校的完整组织架构树。仅限管理员角色调用。`, + Example: ` dws edu-contact school structure + dws edu-contact school structure -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("edu-contact", "get_school_structure", nil) + }, + } + DeclareLeafMetadata(schoolStructureCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "get_school_structure", + CanonicalPath: "edu-contact.get_school_structure", + CLIPath: "edu-contact school structure", + PrimaryCLIPath: "edu-contact school structure", + }, + Description: "查询学校组织架构", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "get_school_structure"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询学校完整组织架构树", + UseWhen: []string{"需要查看学校的完整组织架构时"}, + AvoidWhen: []string{"查询学校班级列表用 school class-list"}, + Examples: []string{ + "dws edu-contact school structure --format json", + }, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + schoolPeriodsCmd := &cobra.Command{ + Use: "periods", + Short: "查询学校学段信息", + Long: `查询指定学校配置的所有学段信息,如小学、初中、高中等。所有已认证用户均可调用。`, + Example: ` dws edu-contact school periods + dws edu-contact school periods -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("edu-contact", "get_school_periods", nil) + }, + } + DeclareLeafMetadata(schoolPeriodsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "get_school_periods", + CanonicalPath: "edu-contact.get_school_periods", + CLIPath: "edu-contact school periods", + PrimaryCLIPath: "edu-contact school periods", + }, + Description: "查询学校学段信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "get_school_periods"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询学校配置的所有学段信息", + UseWhen: []string{"需要查询学校有哪些学段(小学/初中/高中等)时"}, + AvoidWhen: []string{"查询学校类型用 school type"}, + Examples: []string{ + "dws edu-contact school periods --format json", + }, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + schoolTypeCmd := &cobra.Command{ + Use: "type", + Short: "查询学校组织类型", + Long: `查询指定学校的教育组织类型。 +返回值:EDU_BASIC(基础教育/K12)、SELF_CLASS(自建班级)、UNIVERSITY(高校)、EDU_TRAINING(教培机构)。`, + Example: ` dws edu-contact school type + dws edu-contact school type -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("edu-contact", "get_edu_org_type", map[string]any{ + "input": "", + }) + }, + } + DeclareLeafMetadata(schoolTypeCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "get_edu_org_type", + CanonicalPath: "edu-contact.get_edu_org_type", + CLIPath: "edu-contact school type", + PrimaryCLIPath: "edu-contact school type", + }, + Description: "查询学校组织类型", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "get_edu_org_type"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询学校的教育组织类型", + UseWhen: []string{"需要查询学校属于哪种教育组织类型时"}, + AvoidWhen: []string{"查询学段信息用 school periods"}, + Examples: []string{ + "dws edu-contact school type --format json", + }, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + schoolStatsCmd := &cobra.Command{ + Use: "stats", + Short: "查询学校统计数据", + Long: `查询指定学校的统计数据。 +统计类型 --statistics-type 取值:1-学生、2-家长、4-班级;不指定则返回全部统计。 +仅限老师、班主任、管理员角色调用。`, + Example: ` dws edu-contact school stats + dws edu-contact school stats --statistics-type 1 + dws edu-contact school stats --statistics-type 4 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{} + if stStr, _ := cmd.Flags().GetString("statistics-type"); strings.TrimSpace(stStr) != "" { + st, err := strconv.ParseInt(strings.TrimSpace(stStr), 10, 64) + if err != nil { + return fmt.Errorf("--statistics-type 须为整数: %w", err) + } + input["statisticsType"] = st + } + return callMCPToolOnServer("edu-contact", "statistics_school", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(schoolStatsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "statistics_school", + CanonicalPath: "edu-contact.statistics_school", + CLIPath: "edu-contact school stats", + PrimaryCLIPath: "edu-contact school stats", + }, + Description: "查询学校统计数据", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "statistics_school"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询学校统计数据(学生/家长/班级数量)", + UseWhen: []string{"需要查询学校的学生数、家长数或班级数等统计信息时"}, + AvoidWhen: []string{"查询班级列表用 school class-list"}, + Examples: []string{ + "dws edu-contact school stats --statistics-type 1 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "statistics-type", Property: "input.statisticsType"}, + }, + }, + }) + + schoolClassListCmd := &cobra.Command{ + Use: "class-list", + Short: "查询学校所有班级列表", + Long: `查询指定学校下的所有班级列表。仅限老师、班主任、管理员角色调用。`, + Example: ` dws edu-contact school class-list + dws edu-contact school class-list -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("edu-contact", "get_org_class_list", nil) + }, + } + DeclareLeafMetadata(schoolClassListCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "get_org_class_list", + CanonicalPath: "edu-contact.get_org_class_list", + CLIPath: "edu-contact school class-list", + PrimaryCLIPath: "edu-contact school class-list", + }, + Description: "查询学校所有班级列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "get_org_class_list"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询学校下的所有班级列表", + UseWhen: []string{"需要获取学校下所有班级的列表时"}, + AvoidWhen: []string{"查询班级详情用 class detail"}, + Examples: []string{ + "dws edu-contact school class-list --format json", + }, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + // ════════════════════════════════════════════════════════════ + // class 子命令组 — 班级级查询 + // ════════════════════════════════════════════════════════════ + + classCmd := &cobra.Command{Use: "class", Short: "班级管理", RunE: groupRunE} + + classDetailCmd := &cobra.Command{ + Use: "detail", + Short: "查询班级详情", + Long: `查询指定班级的详细信息,包括班级名称、所属年级、学段、学生人数、班主任等。`, + Example: ` dws edu-contact class detail --dept-id 12345 + dws edu-contact class detail --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-contact", "get_class_detail", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(classDetailCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "get_class_detail", + CanonicalPath: "edu-contact.get_class_detail", + CLIPath: "edu-contact class detail", + PrimaryCLIPath: "edu-contact class detail", + }, + Description: "查询班级详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "get_class_detail"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询指定班级的详细信息", + UseWhen: []string{"需要查询某个班级的名称、年级、学段、学生人数、班主任等详情时"}, + AvoidWhen: []string{"查询班级学生列表用 class students"}, + Examples: []string{ + "dws edu-contact class detail --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + classStudentsCmd := &cobra.Command{ + Use: "students", + Short: "查询班级学生信息", + Long: `查询指定班级内的学生列表及班级详情。`, + Example: ` dws edu-contact class students --dept-id 12345 + dws edu-contact class students --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-contact", "query_classes_student_info", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(classStudentsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "query_classes_student_info", + CanonicalPath: "edu-contact.query_classes_student_info", + CLIPath: "edu-contact class students", + PrimaryCLIPath: "edu-contact class students", + }, + Description: "查询班级学生信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "query_classes_student_info"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询指定班级内的学生列表", + UseWhen: []string{"需要查询某个班级的学生列表及班级详情时"}, + AvoidWhen: []string{"查询班级老师用 class teachers"}, + Examples: []string{ + "dws edu-contact class students --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + classTeachersCmd := &cobra.Command{ + Use: "teachers", + Short: "查询班级的老师列表", + Long: `查询指定班级的所有老师信息,包括教授课程、是否为班主任等。`, + Example: ` dws edu-contact class teachers --dept-id 12345 + dws edu-contact class teachers --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-contact", "query_class_teachers", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(classTeachersCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "query_class_teachers", + CanonicalPath: "edu-contact.query_class_teachers", + CLIPath: "edu-contact class teachers", + PrimaryCLIPath: "edu-contact class teachers", + }, + Description: "查询班级的老师列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "query_class_teachers"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询指定班级的所有老师信息", + UseWhen: []string{"需要查询某个班级的老师列表(含课程、是否班主任)时"}, + AvoidWhen: []string{"查询班级学生用 class students"}, + Examples: []string{ + "dws edu-contact class teachers --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + classSameNameCmd := &cobra.Command{ + Use: "same-name", + Short: "查询班级内同名学生", + Long: `查询指定班级内是否存在同名学生。适用于点名、成绩录入等场景。`, + Example: ` dws edu-contact class same-name --dept-id 12345 + dws edu-contact class same-name --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-contact", "get_class_same_name_students", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(classSameNameCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "get_class_same_name_students", + CanonicalPath: "edu-contact.get_class_same_name_students", + CLIPath: "edu-contact class same-name", + PrimaryCLIPath: "edu-contact class same-name", + }, + Description: "查询班级内同名学生", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "get_class_same_name_students"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询班级内是否存在同名学生", + UseWhen: []string{"需要查询班级内是否有重名学生时"}, + AvoidWhen: []string{"查询班级学生列表用 class students"}, + Examples: []string{ + "dws edu-contact class same-name --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + classUserRoleCmd := &cobra.Command{ + Use: "user-role", + Short: "查询用户在班级内的角色", + Long: `查询当前用户在指定班级内的角色身份。`, + Example: ` dws edu-contact class user-role --dept-id 12345 + dws edu-contact class user-role --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-contact", "get_user_role_in_class", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(classUserRoleCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "get_user_role_in_class", + CanonicalPath: "edu-contact.get_user_role_in_class", + CLIPath: "edu-contact class user-role", + PrimaryCLIPath: "edu-contact class user-role", + }, + Description: "查询用户在班级内的角色", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "get_user_role_in_class"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询当前用户在指定班级内的角色身份", + UseWhen: []string{"需要查询用户在某个班级内的角色时"}, + AvoidWhen: []string{"查询用户在组织内角色用 school roles"}, + Examples: []string{ + "dws edu-contact class user-role --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + classSearchByNameCmd := &cobra.Command{ + Use: "search-by-name", + Short: "根据学生或家长姓名查询班级", + Long: `根据学生或家长姓名模糊搜索其所在的班级信息。 +--query-type 取值:student(学生)、guardian(家长)。仅限管理员角色调用。`, + Example: ` dws edu-contact class search-by-name --query-type student --name 张三 + dws edu-contact class search-by-name --query-type guardian --name 李四 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + queryType, _ := cmd.Flags().GetString("query-type") + queryType = strings.TrimSpace(queryType) + if queryType == "" { + return fmt.Errorf("--query-type 为必填参数") + } + name, _ := cmd.Flags().GetString("name") + name = strings.TrimSpace(name) + if name == "" { + return fmt.Errorf("--name 为必填参数") + } + return callMCPToolOnServer("edu-contact", "query_class_by_guardian_name", map[string]any{ + "input": map[string]any{ + "queryType": queryType, + "name": name, + }, + }) + }, + } + DeclareLeafMetadata(classSearchByNameCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "query_class_by_guardian_name", + CanonicalPath: "edu-contact.query_class_by_guardian_name", + CLIPath: "edu-contact class search-by-name", + PrimaryCLIPath: "edu-contact class search-by-name", + }, + Description: "根据学生或家长姓名查询班级", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "query_class_by_guardian_name"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "根据学生或家长姓名模糊搜索所在班级", + UseWhen: []string{"需要根据学生或家长的姓名查找其所在班级时"}, + AvoidWhen: []string{"根据老师姓名查班级用 class search-by-teacher"}, + Examples: []string{ + "dws edu-contact class search-by-name --query-type student --name 张三 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "query-type", Property: "input.queryType", Required: boolPtr(true)}, + {Name: "name", Property: "input.name", Required: boolPtr(true)}, + }, + }, + }) + + classHeadmasterCmd := &cobra.Command{ + Use: "headmaster", + Short: "根据班级名称查询班主任", + Long: `根据班级名称查询班主任信息。仅限管理员角色调用。`, + Example: ` dws edu-contact class headmaster --class-name 一年级1班 + dws edu-contact class headmaster --class-name 三年级2班 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + className, _ := cmd.Flags().GetString("class-name") + className = strings.TrimSpace(className) + if className == "" { + return fmt.Errorf("--class-name 为必填参数") + } + return callMCPToolOnServer("edu-contact", "query_headmaster_by_className", map[string]any{ + "input": map[string]any{"className": className}, + }) + }, + } + DeclareLeafMetadata(classHeadmasterCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "query_headmaster_by_className", + CanonicalPath: "edu-contact.query_headmaster_by_className", + CLIPath: "edu-contact class headmaster", + PrimaryCLIPath: "edu-contact class headmaster", + }, + Description: "根据班级名称查询班主任", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "query_headmaster_by_className"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "根据班级名称查询班主任信息", + UseWhen: []string{"需要通过班级名称查找班主任时"}, + AvoidWhen: []string{"查询班级所有老师用 class teachers"}, + Examples: []string{ + "dws edu-contact class headmaster --class-name 一年级1班 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-name", Property: "input.className", Required: boolPtr(true)}, + }, + }, + }) + + classSearchByTeacherCmd := &cobra.Command{ + Use: "search-by-teacher", + Short: "根据老师姓名查询班级", + Long: `根据教师姓名模糊搜索其任教的班级信息。 +通过通讯录模糊匹配教师,返回该教师所任教的班级列表及班级详情(班级ID、格式化班级名称等), +结果已按班级去重。仅限管理员角色调用。`, + Example: ` dws edu-contact class search-by-teacher --name 张老师 + dws edu-contact class search-by-teacher --name 李 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + name, _ := cmd.Flags().GetString("name") + name = strings.TrimSpace(name) + if name == "" { + return fmt.Errorf("--name 为必填参数") + } + return callMCPToolOnServer("edu-contact", "query_class_by_teacher_name", map[string]any{ + "input": map[string]any{ + "name": name, + }, + }) + }, + } + DeclareLeafMetadata(classSearchByTeacherCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "query_class_by_teacher_name", + CanonicalPath: "edu-contact.query_class_by_teacher_name", + CLIPath: "edu-contact class search-by-teacher", + PrimaryCLIPath: "edu-contact class search-by-teacher", + }, + Description: "根据老师姓名查询班级", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "query_class_by_teacher_name"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "根据教师姓名模糊搜索其任教的班级", + UseWhen: []string{"需要通过教师姓名查找其任教的班级时"}, + AvoidWhen: []string{"根据学生/家长姓名查班级用 class search-by-name"}, + Examples: []string{ + "dws edu-contact class search-by-teacher --name 张老师 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "name", Property: "input.name", Required: boolPtr(true)}, + }, + }, + }) + + classUpdateStudentCmd := &cobra.Command{ + Use: "update-student", + Short: "更新学生信息", + Long: `更新指定班级中某个学生的信息。 +可选更新学生姓名、学号及家长信息列表。 + +通过 --append-patriarch 控制家长更新模式: + - 完全更新家长信息(默认):不传 --append-patriarch 或 --append-patriarch=false,并传入完整的 --patriarchs JSON + - 增量添加新家长:--append-patriarch=true,仅传入需要新增的 --patriarchs JSON + - 不修改家长信息:--append-patriarch=true 且不传 --patriarchs + +--patriarchs JSON 格式示例: + [{"userId":"uid1","relation":"F","relationName":"父亲","phone":"13800138000"}] + relation 取值:F-父亲 M-母亲 GF-爷爷 GM-奶奶 O-其他 + +仅限管理员或班主任角色调用。`, + Example: ` # 仅更新学生姓名(不修改家长,需传 --append-patriarch 避免清空家长) + dws edu-contact class update-student --class-id 12345 --student-user-id userId1 --student-name 张三 --append-patriarch + # 仅更新学号 + dws edu-contact class update-student --class-id 12345 --student-user-id userId1 --student-number S001 --append-patriarch + # 全量更新家长信息(覆盖原有家长列表,不传 --append-patriarch) + dws edu-contact class update-student --class-id 12345 --student-user-id userId1 --patriarchs '[{"userId":"uid1","relation":"F","relationName":"父亲","phone":"13800138000"}]' + # 增量添加新家长(不影响已有家长) + dws edu-contact class update-student --class-id 12345 --student-user-id userId1 --patriarchs '[{"userId":"uid2","relation":"M","relationName":"母亲","phone":"13900139000"}]' --append-patriarch`, + RunE: func(cmd *cobra.Command, args []string) error { + classID, err := eduRequiredIntFlag(cmd, "class-id") + if err != nil { + return err + } + studentUserID, _ := cmd.Flags().GetString("student-user-id") + studentUserID = strings.TrimSpace(studentUserID) + if studentUserID == "" { + return fmt.Errorf("--student-user-id 为必填参数") + } + + input := map[string]any{ + "classId": classID, + "studentUserId": studentUserID, + } + + if studentName, _ := cmd.Flags().GetString("student-name"); strings.TrimSpace(studentName) != "" { + input["studentName"] = strings.TrimSpace(studentName) + } + if studentNumber, _ := cmd.Flags().GetString("student-number"); strings.TrimSpace(studentNumber) != "" { + input["studentNumber"] = strings.TrimSpace(studentNumber) + } + + appendPatriarch, _ := cmd.Flags().GetBool("append-patriarch") + input["appendPatriarch"] = appendPatriarch + + patriarchsJSON, _ := cmd.Flags().GetString("patriarchs") + if strings.TrimSpace(patriarchsJSON) != "" { + var patriarchs []map[string]any + if err := json.Unmarshal([]byte(patriarchsJSON), &patriarchs); err != nil { + return fmt.Errorf("--patriarchs JSON 格式错误: %w", err) + } + input["patriarchs"] = patriarchs + } + + return callMCPToolOnServer("edu-contact", "update_student", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(classUpdateStudentCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "update_student", + CanonicalPath: "edu-contact.update_student", + CLIPath: "edu-contact class update-student", + PrimaryCLIPath: "edu-contact class update-student", + }, + Description: "更新学生信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "update_student"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "更新指定班级中某个学生的姓名、学号或家长信息", + UseWhen: []string{"需要更新学生的姓名、学号或家长信息时"}, + AvoidWhen: []string{"修改学号用 class update-student-number;修改学生名称/家长关系用 class modify-student-info"}, + Examples: []string{ + "dws edu-contact class update-student --class-id 12345 --student-user-id userId1 --student-name 张三 --append-patriarch --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "student-user-id", Property: "input.studentUserId", Required: boolPtr(true)}, + {Name: "student-name", Property: "input.studentName"}, + {Name: "student-number", Property: "input.studentNumber"}, + {Name: "patriarchs", Property: "input.patriarchs"}, + {Name: "append-patriarch", Property: "input.appendPatriarch"}, + }, + }, + }) + + classAddStudentCmd := &cobra.Command{ + Use: "add-student", + Short: "添加班级学生", + Long: `向指定班级添加一名学生。 +必须提供学生手机号(--student-mobile)或至少一个家长手机号(通过 --mother/--father/--other-patriarchs 传入)。 +可选传入学生userId(已有学生时传入)、学号、虚拟账号ID。 +仅限管理员角色调用。 + +家长信息通过 --mother、--father、--other-patriarchs 传入 JSON 格式: + {"mobile":"13800138000","relation":"M","relationName":"母亲"} + relation 取值:F-父亲 M-母亲 GF-爷爷 GM-奶奶 O-其他 + +--other-patriarchs 为 JSON 数组,可传入多个其他家长。`, + Example: ` # 添加学生(学生手机号) + dws edu-contact class add-student --dept-id 12345 --student-name 张三 --student-mobile 13800138000 + # 添加学生并指定母亲信息(家长手机号) + dws edu-contact class add-student --dept-id 12345 --student-name 张三 --mother '{"mobile":"13800138000","relation":"M","relationName":"母亲"}' + # 添加学生并指定学号和父亲信息 + dws edu-contact class add-student --dept-id 12345 --student-name 张三 --student-number S001 --father '{"mobile":"13900139000","relation":"F","relationName":"父亲"}' + # 添加已有学生到班级 + dws edu-contact class add-student --dept-id 12345 --student-name 张三 --student-user-id existingUserId --student-mobile 13800138000`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + studentName, _ := cmd.Flags().GetString("student-name") + studentName = strings.TrimSpace(studentName) + if studentName == "" { + return fmt.Errorf("--student-name 为必填参数") + } + + input := map[string]any{ + "deptId": deptID, + "studentName": studentName, + } + + if studentUserID, _ := cmd.Flags().GetString("student-user-id"); strings.TrimSpace(studentUserID) != "" { + input["studentUserId"] = strings.TrimSpace(studentUserID) + } + if studentNumber, _ := cmd.Flags().GetString("student-number"); strings.TrimSpace(studentNumber) != "" { + input["studentNumber"] = strings.TrimSpace(studentNumber) + } + if virtualAccountID, _ := cmd.Flags().GetString("virtual-account-id"); strings.TrimSpace(virtualAccountID) != "" { + input["virtualAccountId"] = strings.TrimSpace(virtualAccountID) + } + + studentMobile, _ := cmd.Flags().GetString("student-mobile") + studentMobile = strings.TrimSpace(studentMobile) + if studentMobile != "" { + input["studentMobile"] = studentMobile + } + + hasMobile := studentMobile != "" + + if motherJSON, _ := cmd.Flags().GetString("mother"); strings.TrimSpace(motherJSON) != "" { + var mother map[string]any + if err := json.Unmarshal([]byte(motherJSON), &mother); err != nil { + return fmt.Errorf("--mother JSON 格式错误: %w", err) + } + input["mother"] = mother + hasMobile = true + } + if fatherJSON, _ := cmd.Flags().GetString("father"); strings.TrimSpace(fatherJSON) != "" { + var father map[string]any + if err := json.Unmarshal([]byte(fatherJSON), &father); err != nil { + return fmt.Errorf("--father JSON 格式错误: %w", err) + } + input["father"] = father + hasMobile = true + } + if otherJSON, _ := cmd.Flags().GetString("other-patriarchs"); strings.TrimSpace(otherJSON) != "" { + var others []map[string]any + if err := json.Unmarshal([]byte(otherJSON), &others); err != nil { + return fmt.Errorf("--other-patriarchs JSON 格式错误: %w", err) + } + input["otherPatriarchs"] = others + hasMobile = true + } + + if !hasMobile { + return fmt.Errorf("必须提供学生手机号(--student-mobile)或至少一个家长手机号(通过 --mother/--father/--other-patriarchs 传入)") + } + + return callMCPToolOnServer("edu-contact", "add_student", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(classAddStudentCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "add_student", + CanonicalPath: "edu-contact.add_student", + CLIPath: "edu-contact class add-student", + PrimaryCLIPath: "edu-contact class add-student", + }, + Description: "添加班级学生", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "add_student"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "向指定班级添加一名学生", + UseWhen: []string{"需要向班级添加新学生时"}, + AvoidWhen: []string{"添加非行政班学生用 class add-unofficial-student"}, + Examples: []string{ + "dws edu-contact class add-student --dept-id 12345 --student-name 张三 --student-mobile 13800138000 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "student-name", Property: "input.studentName", Required: boolPtr(true)}, + {Name: "student-mobile", Property: "input.studentMobile"}, + {Name: "student-user-id", Property: "input.studentUserId"}, + {Name: "student-number", Property: "input.studentNumber"}, + {Name: "virtual-account-id", Property: "input.virtualAccountId"}, + {Name: "mother", Property: "input.mother"}, + {Name: "father", Property: "input.father"}, + {Name: "other-patriarchs", Property: "input.otherPatriarchs"}, + }, + }, + }) + + classModifyStudentInfoCmd := &cobra.Command{ + Use: "modify-student-info", + Short: "修改学生信息", + Long: `修改指定班级中某个学生的名称或家长关系信息。 +可选修改学生名称和家长关系,两者至少填一项。 +修改家长关系时需同时传入家长 userId 和关系类型。 +操作有并发控制,同一用户短时间内不可重复操作。仅限班主任角色调用。 + +--relation 取值:父亲、母亲、爷爷、奶奶、哥哥、姐姐、其他`, + Example: ` # 修改学生名称 + dws edu-contact class modify-student-info --dept-id 12345 --target-user-id userId1 --nick 张三 + # 修改家长关系 + dws edu-contact class modify-student-info --dept-id 12345 --target-user-id userId1 --patriarch-user-id parentId1 --relation 父亲 + # 同时修改名称和家长关系 + dws edu-contact class modify-student-info --dept-id 12345 --target-user-id userId1 --nick 张三 --patriarch-user-id parentId1 --relation 母亲`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + targetUserID, _ := cmd.Flags().GetString("target-user-id") + targetUserID = strings.TrimSpace(targetUserID) + if targetUserID == "" { + return fmt.Errorf("--target-user-id 为必填参数") + } + + input := map[string]any{ + "deptId": deptID, + "targetUserId": targetUserID, + } + + nick, _ := cmd.Flags().GetString("nick") + nick = strings.TrimSpace(nick) + patriarchUserID, _ := cmd.Flags().GetString("patriarch-user-id") + patriarchUserID = strings.TrimSpace(patriarchUserID) + relation, _ := cmd.Flags().GetString("relation") + relation = strings.TrimSpace(relation) + + if nick == "" && patriarchUserID == "" { + return fmt.Errorf("--nick 和 --patriarch-user-id/--relation 至少填一项") + } + if nick != "" { + input["nick"] = nick + } + if patriarchUserID != "" { + if relation == "" { + return fmt.Errorf("修改家长关系时需同时传入 --relation") + } + input["patriarchUserId"] = patriarchUserID + input["relation"] = relation + } + + return callMCPToolOnServer("edu-contact", "modify_student_info", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(classModifyStudentInfoCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "modify_student_info", + CanonicalPath: "edu-contact.modify_student_info", + CLIPath: "edu-contact class modify-student-info", + PrimaryCLIPath: "edu-contact class modify-student-info", + }, + Description: "修改学生信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "modify_student_info"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "修改学生名称或家长关系信息", + UseWhen: []string{"需要修改学生名称或调整家长关系时"}, + AvoidWhen: []string{"更新学生姓名/学号/家长列表用 class update-student"}, + Examples: []string{ + "dws edu-contact class modify-student-info --dept-id 12345 --target-user-id userId1 --nick 张三 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "target-user-id", Property: "input.targetUserId", Required: boolPtr(true)}, + {Name: "nick", Property: "input.nick"}, + {Name: "patriarch-user-id", Property: "input.patriarchUserId"}, + {Name: "relation", Property: "input.relation"}, + }, + }, + }) + + classDeleteTeacherCmd := &cobra.Command{ + Use: "delete-teacher", + Short: "删除班级教师", + Long: `从指定班级中删除一名教师。 +返回操作是否成功。仅限管理员或班主任角色调用。`, + Example: ` dws edu-contact class delete-teacher --class-id 12345 --teacher-user-id userId1 + dws edu-contact class delete-teacher --class-id 12345 --teacher-user-id userId1 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + classID, err := eduRequiredIntFlag(cmd, "class-id") + if err != nil { + return err + } + teacherUserID, _ := cmd.Flags().GetString("teacher-user-id") + teacherUserID = strings.TrimSpace(teacherUserID) + if teacherUserID == "" { + return fmt.Errorf("--teacher-user-id 为必填参数") + } + return callMCPToolOnServer("edu-contact", "delete_teacher", map[string]any{ + "input": map[string]any{ + "classId": classID, + "teacherUserId": teacherUserID, + }, + }) + }, + } + DeclareLeafMetadata(classDeleteTeacherCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "delete_teacher", + CanonicalPath: "edu-contact.delete_teacher", + CLIPath: "edu-contact class delete-teacher", + PrimaryCLIPath: "edu-contact class delete-teacher", + }, + Description: "删除班级教师", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "delete_teacher"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "从指定班级中删除一名教师", + UseWhen: []string{"需要将某位教师从班级中移除时"}, + AvoidWhen: []string{"添加教师用 class add-teachers"}, + Examples: []string{ + "dws edu-contact class delete-teacher --class-id 12345 --teacher-user-id userId1 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "teacher-user-id", Property: "input.teacherUserId", Required: boolPtr(true)}, + }, + }, + }) + + classUpdateClassInfoCmd := &cobra.Command{ + Use: "update-info", + Short: "更新班级信息", + Long: `更新指定班级的基本信息。 +可选更新班级别名、预期学生人数及班级群名称。 +班级别名和群名称有长度限制且需通过内容安全校验。 +修改群名称时需同时传入群会话ID(--conversation-id)。 +仅限管理员或班主任角色调用。`, + Example: ` # 更新班级别名 + dws edu-contact class update-info --class-id 12345 --nick 火箭班 + # 更新预期学生人数 + dws edu-contact class update-info --class-id 12345 --expected-student-num 45 + # 更新班级群名称(需同时传 conversation-id) + dws edu-contact class update-info --class-id 12345 --group-name 一年级1班家长群 --conversation-id cid123456`, + RunE: func(cmd *cobra.Command, args []string) error { + classID, err := eduRequiredIntFlag(cmd, "class-id") + if err != nil { + return err + } + + input := map[string]any{ + "classId": classID, + } + + if nick, _ := cmd.Flags().GetString("nick"); strings.TrimSpace(nick) != "" { + input["nick"] = strings.TrimSpace(nick) + } + if expectedStr, _ := cmd.Flags().GetString("expected-student-num"); strings.TrimSpace(expectedStr) != "" { + expectedNum, err := strconv.ParseInt(strings.TrimSpace(expectedStr), 10, 64) + if err != nil { + return fmt.Errorf("--expected-student-num 须为整数: %w", err) + } + input["expectedStudentNum"] = expectedNum + } + + groupName, _ := cmd.Flags().GetString("group-name") + conversationID, _ := cmd.Flags().GetString("conversation-id") + groupName = strings.TrimSpace(groupName) + conversationID = strings.TrimSpace(conversationID) + if groupName != "" { + if conversationID == "" { + return fmt.Errorf("修改群名称时需同时传入 --conversation-id") + } + input["groupName"] = groupName + input["conversationId"] = conversationID + } + + return callMCPToolOnServer("edu-contact", "update_class_info", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(classUpdateClassInfoCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "update_class_info", + CanonicalPath: "edu-contact.update_class_info", + CLIPath: "edu-contact class update-info", + PrimaryCLIPath: "edu-contact class update-info", + }, + Description: "更新班级信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "update_class_info"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "更新班级别名、预期学生人数或班级群名称", + UseWhen: []string{"需要修改班级别名、预期人数或群名称时"}, + AvoidWhen: []string{"更新学生信息用 class update-student"}, + Examples: []string{ + "dws edu-contact class update-info --class-id 12345 --nick 火箭班 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "nick", Property: "input.nick"}, + {Name: "expected-student-num", Property: "input.expectedStudentNum"}, + {Name: "group-name", Property: "input.groupName"}, + {Name: "conversation-id", Property: "input.conversationId"}, + }, + }, + }) + + classUpdateStudentNumberCmd := &cobra.Command{ + Use: "update-student-number", + Short: "修改学生学号", + Long: `修改指定班级中某个学生的学号。 +学号有长度限制且需通过内容安全校验。仅限班主任或教师角色调用。`, + Example: ` dws edu-contact class update-student-number --class-id 12345 --student-user-id userId1 --student-number S001 + dws edu-contact class update-student-number --class-id 12345 --student-user-id userId1 --student-number S002 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + classID, err := eduRequiredIntFlag(cmd, "class-id") + if err != nil { + return err + } + studentUserID, _ := cmd.Flags().GetString("student-user-id") + studentUserID = strings.TrimSpace(studentUserID) + if studentUserID == "" { + return fmt.Errorf("--student-user-id 为必填参数") + } + studentNumber, _ := cmd.Flags().GetString("student-number") + studentNumber = strings.TrimSpace(studentNumber) + if studentNumber == "" { + return fmt.Errorf("--student-number 为必填参数") + } + return callMCPToolOnServer("edu-contact", "update_student_number", map[string]any{ + "input": map[string]any{ + "classId": classID, + "studentUserId": studentUserID, + "studentNumber": studentNumber, + }, + }) + }, + } + DeclareLeafMetadata(classUpdateStudentNumberCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "update_student_number", + CanonicalPath: "edu-contact.update_student_number", + CLIPath: "edu-contact class update-student-number", + PrimaryCLIPath: "edu-contact class update-student-number", + }, + Description: "修改学生学号", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "update_student_number"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "修改指定班级中某个学生的学号", + UseWhen: []string{"需要修改学生学号时"}, + AvoidWhen: []string{"更新学生姓名/家长用 class update-student"}, + Examples: []string{ + "dws edu-contact class update-student-number --class-id 12345 --student-user-id userId1 --student-number S001 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-id", Property: "input.classId", Required: boolPtr(true)}, + {Name: "student-user-id", Property: "input.studentUserId", Required: boolPtr(true)}, + {Name: "student-number", Property: "input.studentNumber", Required: boolPtr(true)}, + }, + }, + }) + + classAddUnofficialStudentCmd := &cobra.Command{ + Use: "add-unofficial-student", + Short: "添加非行政班学生", + Long: `向指定非行政班级批量添加学生。 +支持批量操作,学生数量不可超过上限。仅限管理员角色调用。`, + Example: ` dws edu-contact class add-unofficial-student --dept-id 12345 --student-staff-ids staffId1,staffId2 + dws edu-contact class add-unofficial-student --dept-id 12345 --student-staff-ids staffId1 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + staffIdsStr, _ := cmd.Flags().GetString("student-staff-ids") + staffIdsStr = strings.TrimSpace(staffIdsStr) + if staffIdsStr == "" { + return fmt.Errorf("--student-staff-ids 为必填参数") + } + var studentStaffIds []string + for _, sid := range strings.Split(staffIdsStr, ",") { + if s := strings.TrimSpace(sid); s != "" { + studentStaffIds = append(studentStaffIds, s) + } + } + if len(studentStaffIds) == 0 { + return fmt.Errorf("--student-staff-ids 不能为空") + } + return callMCPToolOnServer("edu-contact", "add_unofficial_class_student", map[string]any{ + "input": map[string]any{ + "deptId": deptID, + "studentStaffIds": studentStaffIds, + }, + }) + }, + } + DeclareLeafMetadata(classAddUnofficialStudentCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "add_unofficial_class_student", + CanonicalPath: "edu-contact.add_unofficial_class_student", + CLIPath: "edu-contact class add-unofficial-student", + PrimaryCLIPath: "edu-contact class add-unofficial-student", + }, + Description: "添加非行政班学生", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "add_unofficial_class_student"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "向非行政班级批量添加学生", + UseWhen: []string{"需要向非行政班级批量添加学生时"}, + AvoidWhen: []string{"添加行政班学生用 class add-student"}, + Examples: []string{ + "dws edu-contact class add-unofficial-student --dept-id 12345 --student-staff-ids staffId1,staffId2 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "student-staff-ids", Property: "input.studentStaffIds", Required: boolPtr(true)}, + }, + }, + }) + + classDeleteStudentsCmd := &cobra.Command{ + Use: "delete-students", + Short: "批量删除学生", + Long: `从指定班级中批量删除学生。 +返回操作是否成功。仅限管理员角色调用。`, + Example: ` dws edu-contact class delete-students --dept-id 12345 --student-user-ids userId1,userId2 + dws edu-contact class delete-students --dept-id 12345 --student-user-ids userId1 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + studentIdsStr, _ := cmd.Flags().GetString("student-user-ids") + studentIdsStr = strings.TrimSpace(studentIdsStr) + if studentIdsStr == "" { + return fmt.Errorf("--student-user-ids 为必填参数") + } + var studentUserIds []string + for _, uid := range strings.Split(studentIdsStr, ",") { + if u := strings.TrimSpace(uid); u != "" { + studentUserIds = append(studentUserIds, u) + } + } + if len(studentUserIds) == 0 { + return fmt.Errorf("--student-user-ids 不能为空") + } + return callMCPToolOnServer("edu-contact", "delete_students", map[string]any{ + "input": map[string]any{ + "deptId": deptID, + "studentUserIds": studentUserIds, + }, + }) + }, + } + DeclareLeafMetadata(classDeleteStudentsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "delete_students", + CanonicalPath: "edu-contact.delete_students", + CLIPath: "edu-contact class delete-students", + PrimaryCLIPath: "edu-contact class delete-students", + }, + Description: "批量删除学生", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "delete_students"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "从指定班级中批量删除学生", + UseWhen: []string{"需要从班级中批量移除学生时"}, + AvoidWhen: []string{"学生移班用 class move-student"}, + Examples: []string{ + "dws edu-contact class delete-students --dept-id 12345 --student-user-ids userId1,userId2 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "student-user-ids", Property: "input.studentUserIds", Required: boolPtr(true)}, + }, + }, + }) + + classUpdateStudentMobileCmd := &cobra.Command{ + Use: "update-student-mobile", + Short: "修改学生手机号", + Long: `修改指定班级中某个学生的手机号。 +手机号会进行格式校验。仅限管理员角色调用。`, + Example: ` dws edu-contact class update-student-mobile --dept-id 12345 --student-user-id userId1 --mobile 13800138000 + dws edu-contact class update-student-mobile --dept-id 12345 --student-user-id userId1 --mobile 13800138000 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + studentUserID, _ := cmd.Flags().GetString("student-user-id") + studentUserID = strings.TrimSpace(studentUserID) + if studentUserID == "" { + return fmt.Errorf("--student-user-id 为必填参数") + } + mobile, _ := cmd.Flags().GetString("mobile") + mobile = strings.TrimSpace(mobile) + if mobile == "" { + return fmt.Errorf("--mobile 为必填参数") + } + return callMCPToolOnServer("edu-contact", "update_student_mobile", map[string]any{ + "input": map[string]any{ + "deptId": deptID, + "studentUserId": studentUserID, + "mobile": mobile, + }, + }) + }, + } + DeclareLeafMetadata(classUpdateStudentMobileCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "update_student_mobile", + CanonicalPath: "edu-contact.update_student_mobile", + CLIPath: "edu-contact class update-student-mobile", + PrimaryCLIPath: "edu-contact class update-student-mobile", + }, + Description: "修改学生手机号", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "update_student_mobile"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "修改指定班级中某个学生的手机号", + UseWhen: []string{"需要修改学生手机号时"}, + AvoidWhen: []string{"更新学生姓名/学号用 class update-student"}, + Examples: []string{ + "dws edu-contact class update-student-mobile --dept-id 12345 --student-user-id userId1 --mobile 13800138000 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "student-user-id", Property: "input.studentUserId", Required: boolPtr(true)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(true)}, + }, + }, + }) + + classMoveStudentCmd := &cobra.Command{ + Use: "move-student", + Short: "学生移班", + Long: `将学生从一个班级转移到另一个班级。 +支持批量操作,学生数量不可超过上限。仅限管理员或班主任角色调用。`, + Example: ` dws edu-contact class move-student --student-user-ids userId1,userId2 --origin-class-id 12345 --target-class-id 67890 + dws edu-contact class move-student --student-user-ids userId1 --origin-class-id 12345 --target-class-id 67890 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + originClassID, err := eduRequiredIntFlag(cmd, "origin-class-id") + if err != nil { + return err + } + targetClassID, err := eduRequiredIntFlag(cmd, "target-class-id") + if err != nil { + return err + } + studentIdsStr, _ := cmd.Flags().GetString("student-user-ids") + studentIdsStr = strings.TrimSpace(studentIdsStr) + if studentIdsStr == "" { + return fmt.Errorf("--student-user-ids 为必填参数") + } + var studentUserIds []string + for _, uid := range strings.Split(studentIdsStr, ",") { + if u := strings.TrimSpace(uid); u != "" { + studentUserIds = append(studentUserIds, u) + } + } + if len(studentUserIds) == 0 { + return fmt.Errorf("--student-user-ids 不能为空") + } + return callMCPToolOnServer("edu-contact", "move_student", map[string]any{ + "input": map[string]any{ + "studentUserIds": studentUserIds, + "originClassId": originClassID, + "targetClassId": targetClassID, + }, + }) + }, + } + DeclareLeafMetadata(classMoveStudentCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "move_student", + CanonicalPath: "edu-contact.move_student", + CLIPath: "edu-contact class move-student", + PrimaryCLIPath: "edu-contact class move-student", + }, + Description: "学生移班", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "move_student"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "将学生从一个班级转移到另一个班级", + UseWhen: []string{"需要将学生从原班级转移到目标班级时"}, + AvoidWhen: []string{"删除学生用 class delete-students"}, + Examples: []string{ + "dws edu-contact class move-student --student-user-ids userId1 --origin-class-id 12345 --target-class-id 67890 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "student-user-ids", Property: "input.studentUserIds", Required: boolPtr(true)}, + {Name: "origin-class-id", Property: "input.originClassId", Required: boolPtr(true)}, + {Name: "target-class-id", Property: "input.targetClassId", Required: boolPtr(true)}, + }, + }, + }) + + classAddTeachersCmd := &cobra.Command{ + Use: "add-teachers", + Short: "批量添加班级教师", + Long: `批量将教师添加到指定班级。 +教师列表会自动去重,单次最多添加50名教师。仅限管理员角色调用。 + +--is-adviser 取值: + 0 = 普通老师(默认) + 1 = 班主任`, + Example: ` dws edu-contact class add-teachers --dept-id 12345 --teacher-user-ids userId1,userId2 + dws edu-contact class add-teachers --dept-id 12345 --teacher-user-ids userId1,userId2 --is-adviser 1 + dws edu-contact class add-teachers --dept-id 12345 --teacher-user-ids userId1 --is-adviser 0 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + teacherIdsStr, _ := cmd.Flags().GetString("teacher-user-ids") + teacherIdsStr = strings.TrimSpace(teacherIdsStr) + if teacherIdsStr == "" { + return fmt.Errorf("--teacher-user-ids 为必填参数") + } + var teacherUserIds []string + for _, uid := range strings.Split(teacherIdsStr, ",") { + if u := strings.TrimSpace(uid); u != "" { + teacherUserIds = append(teacherUserIds, u) + } + } + if len(teacherUserIds) == 0 { + return fmt.Errorf("--teacher-user-ids 不能为空") + } + if len(teacherUserIds) > 50 { + return fmt.Errorf("单次最多添加50名教师,当前 %d 名", len(teacherUserIds)) + } + isAdviserStr, _ := cmd.Flags().GetString("is-adviser") + isAdviser := int64(0) + if strings.TrimSpace(isAdviserStr) != "" { + isAdviser, err = strconv.ParseInt(strings.TrimSpace(isAdviserStr), 10, 64) + if err != nil || (isAdviser != 0 && isAdviser != 1) { + return fmt.Errorf("--is-adviser 须为 0(普通老师)或 1(班主任)") + } + } + return callMCPToolOnServer("edu-contact", "batch_add_class_teacher", map[string]any{ + "input": map[string]any{ + "deptId": deptID, + "teacherUserIds": teacherUserIds, + "isAdviser": isAdviser, + }, + }) + }, + } + DeclareLeafMetadata(classAddTeachersCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "batch_add_class_teacher", + CanonicalPath: "edu-contact.batch_add_class_teacher", + CLIPath: "edu-contact class add-teachers", + PrimaryCLIPath: "edu-contact class add-teachers", + }, + Description: "批量添加班级教师", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "batch_add_class_teacher"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "批量将教师添加到指定班级", + UseWhen: []string{"需要向班级批量添加教师时"}, + AvoidWhen: []string{"删除教师用 class delete-teacher"}, + Examples: []string{ + "dws edu-contact class add-teachers --dept-id 12345 --teacher-user-ids userId1,userId2 --is-adviser 1 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + {Name: "teacher-user-ids", Property: "input.teacherUserIds", Required: boolPtr(true)}, + {Name: "is-adviser", Property: "input.isAdviser"}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // family 子命令组 — 家庭关系查询 + // ════════════════════════════════════════════════════════════ + + familyCmd := &cobra.Command{Use: "family", Short: "家庭关系查询", RunE: groupRunE} + + familyChildrenCmd := &cobra.Command{ + Use: "children", + Short: "查询家长的孩子信息", + Long: `查询当前家长在指定组织(学校)内关联的所有孩子信息。仅限家长角色调用。`, + Example: ` dws edu-contact family children + dws edu-contact family children -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("edu-contact", "query_children_in_org", nil) + }, + } + DeclareLeafMetadata(familyChildrenCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "query_children_in_org", + CanonicalPath: "edu-contact.query_children_in_org", + CLIPath: "edu-contact family children", + PrimaryCLIPath: "edu-contact family children", + }, + Description: "查询家长的孩子信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "query_children_in_org"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询当前家长关联的所有孩子信息", + UseWhen: []string{"需要查询家长在学校内关联的孩子信息时"}, + AvoidWhen: []string{"查询学生的家长信息用 family parents"}, + Examples: []string{ + "dws edu-contact family children --format json", + }, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + familyParentsCmd := &cobra.Command{ + Use: "parents", + Short: "查询学生的家长信息", + Long: `查询当前学生在指定组织(学校)内关联的所有家长信息。`, + Example: ` dws edu-contact family parents + dws edu-contact family parents -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("edu-contact", "query_patriarch_in_org", nil) + }, + } + DeclareLeafMetadata(familyParentsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "query_patriarch_in_org", + CanonicalPath: "edu-contact.query_patriarch_in_org", + CLIPath: "edu-contact family parents", + PrimaryCLIPath: "edu-contact family parents", + }, + Description: "查询学生的家长信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "query_patriarch_in_org"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询当前学生关联的所有家长信息", + UseWhen: []string{"需要查询学生在学校内关联的家长信息时"}, + AvoidWhen: []string{"查询家长的孩子信息用 family children"}, + Examples: []string{ + "dws edu-contact family parents --format json", + }, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + // ════════════════════════════════════════════════════════════ + // teacher 子命令组 — 教师查询 + // ════════════════════════════════════════════════════════════ + + teacherCmd := &cobra.Command{Use: "teacher", Short: "教师管理", RunE: groupRunE} + + teacherClassesCmd := &cobra.Command{ + Use: "classes", + Short: "查询老师管理的班级列表", + Long: `查询当前老师或班主任在指定组织(学校)内管理的班级列表。`, + Example: ` dws edu-contact teacher classes + dws edu-contact teacher classes -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("edu-contact", "query_teacher_classes", nil) + }, + } + DeclareLeafMetadata(teacherClassesCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "query_teacher_classes", + CanonicalPath: "edu-contact.query_teacher_classes", + CLIPath: "edu-contact teacher classes", + PrimaryCLIPath: "edu-contact teacher classes", + }, + Description: "查询老师管理的班级列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "query_teacher_classes"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询当前老师管理的班级列表", + UseWhen: []string{"需要查询老师或班主任管理的班级列表时"}, + AvoidWhen: []string{"查询班级老师列表用 class teachers"}, + Examples: []string{ + "dws edu-contact teacher classes --format json", + }, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + teacherUpdateCourseCmd := &cobra.Command{ + Use: "update-course", + Short: "更新教师任教科目", + Long: `批量更新教师在各班级的任教科目信息。 +返回操作是否成功。仅限班主任或教师角色调用。 + +--teacher-class-infos JSON 格式示例: + [{"classId":12345,"courseCode":"c1","courseName":"语文"}] + +每项包含: + - classId: 班级ID + - courseCode: 科目编码 + - courseName: 科目名称`, + Example: ` dws edu-contact teacher update-course --teacher-class-infos '[{"classId":12345,"courseCode":"c1","courseName":"语文"}]' + dws edu-contact teacher update-course --teacher-class-infos '[{"classId":12345,"courseCode":"c1","courseName":"语文"},{"classId":67890,"courseCode":"c2","courseName":"数学"}]' -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + infosJSON, _ := cmd.Flags().GetString("teacher-class-infos") + infosJSON = strings.TrimSpace(infosJSON) + if infosJSON == "" { + return fmt.Errorf("--teacher-class-infos 为必填参数") + } + var teacherClassInfos []map[string]any + if err := json.Unmarshal([]byte(infosJSON), &teacherClassInfos); err != nil { + return fmt.Errorf("--teacher-class-infos JSON 格式错误: %w", err) + } + if len(teacherClassInfos) == 0 { + return fmt.Errorf("--teacher-class-infos 不能为空数组") + } + return callMCPToolOnServer("edu-contact", "update_teacher_course", map[string]any{ + "input": map[string]any{ + "teacherClassInfos": teacherClassInfos, + }, + }) + }, + } + DeclareLeafMetadata(teacherUpdateCourseCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-contact", + Name: "update_teacher_course", + CanonicalPath: "edu-contact.update_teacher_course", + CLIPath: "edu-contact teacher update-course", + PrimaryCLIPath: "edu-contact teacher update-course", + }, + Description: "更新教师任教科目", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-contact", RPCName: "update_teacher_course"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "批量更新教师在各班级的任教科目信息", + UseWhen: []string{"需要更新教师任教的科目信息时"}, + AvoidWhen: []string{"添加教师到班级用 class add-teachers"}, + Examples: []string{ + "dws edu-contact teacher update-course --teacher-class-infos '[{\"classId\":12345,\"courseCode\":\"c1\",\"courseName\":\"语文\"}]' --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "teacher-class-infos", Property: "input.teacherClassInfos", Required: boolPtr(true)}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // flags + 构建命令树 + // ════════════════════════════════════════════════════════════ + + // school flags + schoolStatsCmd.Flags().String("statistics-type", "", "统计类型:1-学生 2-家长 4-班级") + + // class flags + classDetailCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classStudentsCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classTeachersCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classSameNameCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classUserRoleCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classSearchByNameCmd.Flags().String("query-type", "", "查询类型(必填):student / guardian") + classSearchByNameCmd.Flags().String("name", "", "姓名(必填)") + classHeadmasterCmd.Flags().String("class-name", "", "班级名称(必填)") + classSearchByTeacherCmd.Flags().String("name", "", "教师姓名(必填)") + classUpdateStudentCmd.Flags().String("class-id", "", "班级 ID(必填)") + classUpdateStudentCmd.Flags().String("student-user-id", "", "学生 userId(必填)") + classUpdateStudentCmd.Flags().String("student-name", "", "学生姓名(可选)") + classUpdateStudentCmd.Flags().String("student-number", "", "学号(可选)") + classUpdateStudentCmd.Flags().String("patriarchs", "", "家长信息 JSON 数组(可选)") + classUpdateStudentCmd.Flags().Bool("append-patriarch", false, "家长更新模式:传入则增量添加,不传则全量更新(默认全量更新)") + classAddStudentCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classAddStudentCmd.Flags().String("student-name", "", "学生姓名(必填)") + classAddStudentCmd.Flags().String("student-mobile", "", "学生手机号(与家长手机号至少填一个)") + classAddStudentCmd.Flags().String("student-user-id", "", "学生 userId(可选,已有学生时传入)") + classAddStudentCmd.Flags().String("student-number", "", "学号(可选)") + classAddStudentCmd.Flags().String("virtual-account-id", "", "虚拟账号 ID(可选)") + classAddStudentCmd.Flags().String("mother", "", "母亲信息 JSON(可选)") + classAddStudentCmd.Flags().String("father", "", "父亲信息 JSON(可选)") + classAddStudentCmd.Flags().String("other-patriarchs", "", "其他家长信息 JSON 数组(可选)") + classModifyStudentInfoCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classModifyStudentInfoCmd.Flags().String("target-user-id", "", "学生 userId(必填)") + classModifyStudentInfoCmd.Flags().String("nick", "", "学生名称(可选)") + classModifyStudentInfoCmd.Flags().String("patriarch-user-id", "", "家长 userId(修改家长关系时必填)") + classModifyStudentInfoCmd.Flags().String("relation", "", "家长关系:父亲/母亲/爷爷/奶奶/哥哥/姐姐/其他(修改家长关系时必填)") + classDeleteTeacherCmd.Flags().String("class-id", "", "班级 ID(必填)") + classDeleteTeacherCmd.Flags().String("teacher-user-id", "", "教师 userId(必填)") + classUpdateClassInfoCmd.Flags().String("class-id", "", "班级 ID(必填)") + classUpdateClassInfoCmd.Flags().String("nick", "", "班级别名(可选)") + classUpdateClassInfoCmd.Flags().String("expected-student-num", "", "预期学生人数(可选)") + classUpdateClassInfoCmd.Flags().String("group-name", "", "班级群名称(可选,需同时传 --conversation-id)") + classUpdateClassInfoCmd.Flags().String("conversation-id", "", "群会话 ID(修改群名称时必填)") + classUpdateStudentNumberCmd.Flags().String("class-id", "", "班级 ID(必填)") + classUpdateStudentNumberCmd.Flags().String("student-user-id", "", "学生 userId(必填)") + classUpdateStudentNumberCmd.Flags().String("student-number", "", "新学号(必填)") + classAddUnofficialStudentCmd.Flags().String("dept-id", "", "非行政班级 ID(必填)") + classAddUnofficialStudentCmd.Flags().String("student-staff-ids", "", "学生 staffId 列表,逗号分隔(必填)") + classDeleteStudentsCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classDeleteStudentsCmd.Flags().String("student-user-ids", "", "学生 userId 列表,逗号分隔(必填)") + classUpdateStudentMobileCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classUpdateStudentMobileCmd.Flags().String("student-user-id", "", "学生 userId(必填)") + classUpdateStudentMobileCmd.Flags().String("mobile", "", "修改后的手机号(必填)") + classMoveStudentCmd.Flags().String("student-user-ids", "", "学生 userId 列表,逗号分隔(必填)") + classMoveStudentCmd.Flags().String("origin-class-id", "", "原班级 ID(必填)") + classMoveStudentCmd.Flags().String("target-class-id", "", "目标班级 ID(必填)") + classAddTeachersCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classAddTeachersCmd.Flags().String("teacher-user-ids", "", "教师 userId 列表,逗号分隔(必填,最多50个)") + classAddTeachersCmd.Flags().String("is-adviser", "0", "教师角色:0-普通老师 1-班主任(默认 0)") + + schoolCmd.AddCommand(schoolRolesCmd, schoolStructureCmd, schoolPeriodsCmd, schoolTypeCmd, schoolStatsCmd, schoolClassListCmd) + classCmd.AddCommand(classDetailCmd, classStudentsCmd, classTeachersCmd, classSameNameCmd, classUserRoleCmd, classSearchByNameCmd, classHeadmasterCmd, classSearchByTeacherCmd, classAddStudentCmd, classModifyStudentInfoCmd, classDeleteTeacherCmd, classUpdateClassInfoCmd, classUpdateStudentCmd, classUpdateStudentNumberCmd, classAddUnofficialStudentCmd, classDeleteStudentsCmd, classUpdateStudentMobileCmd, classMoveStudentCmd, classAddTeachersCmd) + familyCmd.AddCommand(familyChildrenCmd, familyParentsCmd) + teacherUpdateCourseCmd.Flags().String("teacher-class-infos", "", "教师班级课程信息 JSON 数组(必填)") + teacherCmd.AddCommand(teacherClassesCmd, teacherUpdateCourseCmd) + + root.AddCommand(schoolCmd, classCmd, familyCmd, teacherCmd) + + return root +} + +func eduRequiredIntFlag(cmd *cobra.Command, name string) (int64, error) { + v, _ := cmd.Flags().GetString(name) + v = strings.TrimSpace(v) + if v == "" { + return 0, fmt.Errorf("--%s 为必填参数", name) + } + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return 0, fmt.Errorf("--%s 须为整数: %w", name, err) + } + return n, nil +} diff --git a/internal/helpers/edu_contact_test.go b/internal/helpers/edu_contact_test.go new file mode 100644 index 00000000..c3b8e5c9 --- /dev/null +++ b/internal/helpers/edu_contact_test.go @@ -0,0 +1,583 @@ +package helpers + +import ( + "io" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func newTestEduContactRoot() *cobra.Command { + return newEduContactCommand() +} + +// ────────────────────────────────────────────────────────── +// 命令注册测试 — 验证所有子命令路径是否正确注册 +// ────────────────────────────────────────────────────────── + +func TestEduContactCommandTree(t *testing.T) { + root := newTestEduContactRoot() + + paths := [][]string{ + // school + {"school", "roles"}, + {"school", "structure"}, + {"school", "periods"}, + {"school", "type"}, + {"school", "stats"}, + {"school", "class-list"}, + // class — 原有 + {"class", "detail"}, + {"class", "students"}, + {"class", "teachers"}, + {"class", "same-name"}, + {"class", "user-role"}, + {"class", "search-by-name"}, + {"class", "headmaster"}, + // class — 新增 + {"class", "search-by-teacher"}, + {"class", "add-student"}, + {"class", "add-teachers"}, + {"class", "add-unofficial-student"}, + {"class", "delete-students"}, + {"class", "delete-teacher"}, + {"class", "modify-student-info"}, + {"class", "move-student"}, + {"class", "update-info"}, + {"class", "update-student"}, + {"class", "update-student-mobile"}, + {"class", "update-student-number"}, + // family + {"family", "children"}, + {"family", "parents"}, + // teacher + {"teacher", "classes"}, + {"teacher", "update-course"}, + } + + for _, path := range paths { + if _, _, err := root.Find(path); err != nil { + t.Errorf("command path %v not found: %v", path, err) + } + } +} + +// ────────────────────────────────────────────────────────── +// 参数校验测试 — 验证必填参数缺失时返回错误 +// ────────────────────────────────────────────────────────── + +func executeCommand(root *cobra.Command, args ...string) error { + root.SetArgs(args) + return root.Execute() +} + +func TestClassSearchByTeacher_MissingName(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "search-by-teacher") + if err == nil { + t.Fatal("expected error for missing --name, got nil") + } +} + +func TestClassAddTeachers_MissingDeptId(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "add-teachers", "--teacher-user-ids", "uid1") + if err == nil { + t.Fatal("expected error for missing --dept-id, got nil") + } +} + +func TestClassAddTeachers_MissingTeacherUserIds(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "add-teachers", "--dept-id", "12345") + if err == nil { + t.Fatal("expected error for missing --teacher-user-ids, got nil") + } +} + +func TestClassAddTeachers_InvalidIsAdviser(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "add-teachers", "--dept-id", "12345", "--teacher-user-ids", "uid1", "--is-adviser", "3") + if err == nil { + t.Fatal("expected error for invalid --is-adviser, got nil") + } +} + +func TestClassMoveStudent_MissingOriginClassId(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "move-student", "--student-user-ids", "uid1", "--target-class-id", "67890") + if err == nil { + t.Fatal("expected error for missing --origin-class-id, got nil") + } +} + +func TestClassMoveStudent_MissingStudentUserIds(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "move-student", "--origin-class-id", "12345", "--target-class-id", "67890") + if err == nil { + t.Fatal("expected error for missing --student-user-ids, got nil") + } +} + +func TestClassUpdateStudentMobile_MissingMobile(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "update-student-mobile", "--dept-id", "12345", "--student-user-id", "uid1") + if err == nil { + t.Fatal("expected error for missing --mobile, got nil") + } +} + +func TestClassDeleteStudents_MissingDeptId(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "delete-students", "--student-user-ids", "uid1") + if err == nil { + t.Fatal("expected error for missing --dept-id, got nil") + } +} + +func TestClassDeleteStudents_MissingStudentUserIds(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "delete-students", "--dept-id", "12345") + if err == nil { + t.Fatal("expected error for missing --student-user-ids, got nil") + } +} + +func TestClassAddUnofficialStudent_MissingDeptId(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "add-unofficial-student", "--student-staff-ids", "sid1") + if err == nil { + t.Fatal("expected error for missing --dept-id, got nil") + } +} + +func TestClassAddUnofficialStudent_MissingStaffIds(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "add-unofficial-student", "--dept-id", "12345") + if err == nil { + t.Fatal("expected error for missing --student-staff-ids, got nil") + } +} + +func TestClassUpdateStudent_MissingClassId(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "update-student", "--student-user-id", "uid1", "--student-name", "张三", "--append-patriarch") + if err == nil { + t.Fatal("expected error for missing --class-id, got nil") + } +} + +func TestClassUpdateStudent_MissingStudentUserId(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "update-student", "--class-id", "12345", "--student-name", "张三", "--append-patriarch") + if err == nil { + t.Fatal("expected error for missing --student-user-id, got nil") + } +} + +func TestClassUpdateStudent_InvalidPatriarchsJSON(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "update-student", "--class-id", "12345", "--student-user-id", "uid1", "--patriarchs", "invalid-json", "--append-patriarch") + if err == nil { + t.Fatal("expected error for invalid --patriarchs JSON, got nil") + } +} + +func TestClassUpdateStudentNumber_MissingStudentNumber(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "update-student-number", "--class-id", "12345", "--student-user-id", "uid1") + if err == nil { + t.Fatal("expected error for missing --student-number, got nil") + } +} + +func TestClassUpdateInfo_MissingClassId(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "update-info", "--nick", "火箭班") + if err == nil { + t.Fatal("expected error for missing --class-id, got nil") + } +} + +func TestClassUpdateInfo_GroupNameWithoutConversationId(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "update-info", "--class-id", "12345", "--group-name", "测试群") + if err == nil { + t.Fatal("expected error for --group-name without --conversation-id, got nil") + } +} + +func TestClassDeleteTeacher_MissingTeacherUserId(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "delete-teacher", "--class-id", "12345") + if err == nil { + t.Fatal("expected error for missing --teacher-user-id, got nil") + } +} + +func TestClassModifyStudentInfo_MissingBothNickAndPatriarch(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "modify-student-info", "--dept-id", "12345", "--target-user-id", "uid1") + if err == nil { + t.Fatal("expected error for missing both --nick and --patriarch-user-id, got nil") + } +} + +func TestClassModifyStudentInfo_PatriarchWithoutRelation(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "modify-student-info", "--dept-id", "12345", "--target-user-id", "uid1", "--patriarch-user-id", "pid1") + if err == nil { + t.Fatal("expected error for --patriarch-user-id without --relation, got nil") + } +} + +func TestClassAddStudent_MissingStudentName(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "add-student", "--dept-id", "12345", "--student-mobile", "13800138000") + if err == nil { + t.Fatal("expected error for missing --student-name, got nil") + } +} + +func TestClassAddStudent_MissingMobile(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "add-student", "--dept-id", "12345", "--student-name", "张三") + if err == nil { + t.Fatal("expected error for missing mobile (student or parent), got nil") + } +} + +func TestClassAddStudent_InvalidMotherJSON(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "class", "add-student", "--dept-id", "12345", "--student-name", "张三", "--mother", "bad-json") + if err == nil { + t.Fatal("expected error for invalid --mother JSON, got nil") + } +} + +func TestTeacherUpdateCourse_MissingTeacherClassInfos(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "teacher", "update-course") + if err == nil { + t.Fatal("expected error for missing --teacher-class-infos, got nil") + } +} + +func TestTeacherUpdateCourse_InvalidJSON(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "teacher", "update-course", "--teacher-class-infos", "not-json") + if err == nil { + t.Fatal("expected error for invalid --teacher-class-infos JSON, got nil") + } +} + +func TestTeacherUpdateCourse_EmptyArray(t *testing.T) { + root := newTestEduContactRoot() + err := executeCommand(root, "teacher", "update-course", "--teacher-class-infos", "[]") + if err == nil { + t.Fatal("expected error for empty --teacher-class-infos array, got nil") + } +} + +// ────────────────────────────────────────────────────────── +// Happy-path 测试 — 每个 leaf 命令的成功分支(经由 dry-run caller) +// 以及所有可选字段分支,用于把 changed-code 覆盖率补到 100%。 +// ────────────────────────────────────────────────────────── + +// withEduContactCaller installs a dry-run capture caller so happy-path command +// execution exercises each RunE up to the callMCPToolOnServer dispatch without +// requiring a live MCP transport. +func withEduContactCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{dryRun: true} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +func TestEduContactHappyPaths(t *testing.T) { + withEduContactCaller(t) + cases := [][]string{ + // school + {"school", "roles"}, + {"school", "structure"}, + {"school", "periods"}, + {"school", "type"}, + {"school", "stats"}, + {"school", "stats", "--statistics-type", "1"}, + {"school", "class-list"}, + // class — 读操作 + {"class", "detail", "--dept-id", "123"}, + {"class", "students", "--dept-id", "123"}, + {"class", "teachers", "--dept-id", "123"}, + {"class", "same-name", "--dept-id", "123"}, + {"class", "user-role", "--dept-id", "123"}, + {"class", "search-by-name", "--query-type", "student", "--name", "张三"}, + {"class", "headmaster", "--class-name", "一年级1班"}, + {"class", "search-by-teacher", "--name", "张老师"}, + // class — update-student 各分支 + {"class", "update-student", "--class-id", "123", "--student-user-id", "u1", + "--student-name", "张三", "--student-number", "S1", "--append-patriarch", + "--patriarchs", `[{"userId":"uid1","relation":"F"}]`}, + {"class", "update-student", "--class-id", "123", "--student-user-id", "u1"}, + // class — add-student 各手机号来源分支 + {"class", "add-student", "--dept-id", "123", "--student-name", "张三", + "--student-mobile", "13800138000", "--student-user-id", "u1", + "--student-number", "S1", "--virtual-account-id", "v1"}, + {"class", "add-student", "--dept-id", "123", "--student-name", "张三", + "--mother", `{"mobile":"13800138000","relation":"M"}`}, + {"class", "add-student", "--dept-id", "123", "--student-name", "张三", + "--father", `{"mobile":"13900139000","relation":"F"}`}, + {"class", "add-student", "--dept-id", "123", "--student-name", "张三", + "--other-patriarchs", `[{"mobile":"13700137000","relation":"O"}]`}, + // class — modify-student-info 两个分支 + {"class", "modify-student-info", "--dept-id", "123", "--target-user-id", "u1", "--nick", "张三"}, + {"class", "modify-student-info", "--dept-id", "123", "--target-user-id", "u1", + "--patriarch-user-id", "p1", "--relation", "父亲"}, + // class — delete-teacher + {"class", "delete-teacher", "--class-id", "123", "--teacher-user-id", "u1"}, + // class — update-info 三个可选分支 + {"class", "update-info", "--class-id", "123", "--nick", "火箭班"}, + {"class", "update-info", "--class-id", "123", "--expected-student-num", "45"}, + {"class", "update-info", "--class-id", "123", "--group-name", "家长群", "--conversation-id", "cid1"}, + // class — update-student-number + {"class", "update-student-number", "--class-id", "123", "--student-user-id", "u1", "--student-number", "S1"}, + // class — add-unofficial-student + {"class", "add-unofficial-student", "--dept-id", "123", "--student-staff-ids", "s1,s2"}, + // class — delete-students + {"class", "delete-students", "--dept-id", "123", "--student-user-ids", "u1,u2"}, + // class — update-student-mobile + {"class", "update-student-mobile", "--dept-id", "123", "--student-user-id", "u1", "--mobile", "13800138000"}, + // class — move-student + {"class", "move-student", "--student-user-ids", "u1,u2", "--origin-class-id", "123", "--target-class-id", "456"}, + // class — add-teachers 默认/班主任 + {"class", "add-teachers", "--dept-id", "123", "--teacher-user-ids", "u1,u2"}, + {"class", "add-teachers", "--dept-id", "123", "--teacher-user-ids", "u1", "--is-adviser", "1"}, + // family + {"family", "children"}, + {"family", "parents"}, + // teacher + {"teacher", "classes"}, + {"teacher", "update-course", "--teacher-class-infos", `[{"classId":123,"courseCode":"c1","courseName":"语文"}]`}, + } + for _, args := range cases { + root := newTestEduContactRoot() + if err := executeCommand(root, args...); err != nil { + t.Errorf("happy path %v returned error: %v", args, err) + } + } +} + +// TestEduContactErrorPathsRemaining covers validation branches not exercised by +// the existing error tests, ensuring 100% changed-code coverage. +func TestEduContactErrorPathsRemaining(t *testing.T) { + cases := []struct { + name string + args []string + }{ + // eduRequiredIntFlag:空值 + 非整数 + {"stats-invalid-type", []string{"school", "stats", "--statistics-type", "abc"}}, + {"detail-missing-dept", []string{"class", "detail"}}, + {"detail-invalid-dept", []string{"class", "detail", "--dept-id", "abc"}}, + {"students-missing-dept", []string{"class", "students"}}, + {"teachers-missing-dept", []string{"class", "teachers"}}, + {"samename-missing-dept", []string{"class", "same-name"}}, + {"userrole-missing-dept", []string{"class", "user-role"}}, + {"searchbyname-missing-querytype", []string{"class", "search-by-name", "--name", "张三"}}, + {"searchbyname-missing-name", []string{"class", "search-by-name", "--query-type", "student"}}, + {"headmaster-missing-classname", []string{"class", "headmaster"}}, + // update-student + {"updatestudent-missing-classid", []string{"class", "update-student", "--student-user-id", "u1"}}, + {"updatestudent-missing-userid", []string{"class", "update-student", "--class-id", "123"}}, + {"updatestudent-invalid-patriarchs", []string{"class", "update-student", "--class-id", "123", "--student-user-id", "u1", "--patriarchs", "not-json"}}, + // add-student + {"addstudent-missing-dept", []string{"class", "add-student", "--student-name", "张三"}}, + {"addstudent-missing-name", []string{"class", "add-student", "--dept-id", "123"}}, + {"addstudent-invalid-father", []string{"class", "add-student", "--dept-id", "123", "--student-name", "张三", "--father", "not-json"}}, + {"addstudent-invalid-other", []string{"class", "add-student", "--dept-id", "123", "--student-name", "张三", "--other-patriarchs", "not-json"}}, + {"addstudent-no-mobile", []string{"class", "add-student", "--dept-id", "123", "--student-name", "张三"}}, + // modify-student-info + {"modify-missing-dept", []string{"class", "modify-student-info", "--target-user-id", "u1", "--nick", "张三"}}, + {"modify-missing-target", []string{"class", "modify-student-info", "--dept-id", "123", "--nick", "张三"}}, + // delete-teacher + {"deleteteacher-missing-classid", []string{"class", "delete-teacher", "--teacher-user-id", "u1"}}, + // update-info + {"updateinfo-missing-classid", []string{"class", "update-info", "--nick", "火箭班"}}, + {"updateinfo-invalid-expected", []string{"class", "update-info", "--class-id", "123", "--expected-student-num", "abc"}}, + // update-student-number + {"usn-missing-classid", []string{"class", "update-student-number", "--student-user-id", "u1", "--student-number", "S1"}}, + {"usn-missing-userid", []string{"class", "update-student-number", "--class-id", "123", "--student-number", "S1"}}, + // add-unofficial-student + {"unofficial-missing-dept", []string{"class", "add-unofficial-student", "--student-staff-ids", "s1"}}, + {"unofficial-empty-staffids", []string{"class", "add-unofficial-student", "--dept-id", "123", "--student-staff-ids", ",,"}}, + // delete-students + {"deletestudents-missing-userids", []string{"class", "delete-students", "--dept-id", "123"}}, + {"deletestudents-empty-userids", []string{"class", "delete-students", "--dept-id", "123", "--student-user-ids", ",,"}}, + // update-student-mobile + {"usm-missing-dept", []string{"class", "update-student-mobile", "--student-user-id", "u1", "--mobile", "13800138000"}}, + {"usm-missing-userid", []string{"class", "update-student-mobile", "--dept-id", "123", "--mobile", "13800138000"}}, + // move-student + {"move-missing-target", []string{"class", "move-student", "--student-user-ids", "u1", "--origin-class-id", "123"}}, + {"move-empty-userids", []string{"class", "move-student", "--origin-class-id", "123", "--target-class-id", "456", "--student-user-ids", ",,"}}, + // add-teachers + {"addteachers-empty-userids", []string{"class", "add-teachers", "--dept-id", "123", "--teacher-user-ids", ",,"}}, + {"addteachers-too-many", []string{"class", "add-teachers", "--dept-id", "123", "--teacher-user-ids", strings.Repeat("u,", 51) + "u"}}, + } + for _, tc := range cases { + root := newTestEduContactRoot() + if err := executeCommand(root, tc.args...); err == nil { + t.Errorf("%s: expected error, got nil", tc.name) + } + } +} + +// ────────────────────────────────────────────────────────── +// Dispatch 验证测试 — 验证命令正确派发到 MCP Server +// ────────────────────────────────────────────────────────── + +// withEduContactDispatchCaller installs a non-dry-run capture caller so that +// callMCPToolOnServer goes through deps.Caller.CallTool and we can verify +// the dispatched productID, tool name, and args. +func withEduContactDispatchCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +func TestEduContactDispatch(t *testing.T) { + t.Run("class detail dispatches get_class_detail with deptId", func(t *testing.T) { + caller := withEduContactDispatchCaller(t) + root := newEduContactCommand() + root.SetArgs([]string{"class", "detail", "--dept-id", "123"}) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if caller.productID != "edu-contact" { + t.Errorf("productID = %q, want %q", caller.productID, "edu-contact") + } + if caller.tool != "get_class_detail" { + t.Errorf("tool = %q, want %q", caller.tool, "get_class_detail") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] is not map[string]any: %#v", caller.args) + } + if input["deptId"] != int64(123) { + t.Errorf("input[deptId] = %v (%T), want int64(123)", input["deptId"], input["deptId"]) + } + }) + + t.Run("class search-by-name dispatches query_class_by_guardian_name", func(t *testing.T) { + caller := withEduContactDispatchCaller(t) + root := newEduContactCommand() + root.SetArgs([]string{"class", "search-by-name", "--query-type", "student", "--name", "张三"}) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if caller.productID != "edu-contact" { + t.Errorf("productID = %q, want %q", caller.productID, "edu-contact") + } + if caller.tool != "query_class_by_guardian_name" { + t.Errorf("tool = %q, want %q", caller.tool, "query_class_by_guardian_name") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] is not map[string]any: %#v", caller.args) + } + if input["queryType"] != "student" { + t.Errorf("input[queryType] = %v, want %q", input["queryType"], "student") + } + if input["name"] != "张三" { + t.Errorf("input[name] = %v, want %q", input["name"], "张三") + } + }) + + t.Run("school stats dispatches statistics_school with statisticsType", func(t *testing.T) { + caller := withEduContactDispatchCaller(t) + root := newEduContactCommand() + root.SetArgs([]string{"school", "stats", "--statistics-type", "2"}) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if caller.productID != "edu-contact" { + t.Errorf("productID = %q, want %q", caller.productID, "edu-contact") + } + if caller.tool != "statistics_school" { + t.Errorf("tool = %q, want %q", caller.tool, "statistics_school") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] is not map[string]any: %#v", caller.args) + } + if input["statisticsType"] != int64(2) { + t.Errorf("input[statisticsType] = %v (%T), want int64(2)", input["statisticsType"], input["statisticsType"]) + } + }) + + t.Run("class add-teachers dispatches batch_add_class_teacher with list and isAdviser", func(t *testing.T) { + caller := withEduContactDispatchCaller(t) + root := newEduContactCommand() + root.SetArgs([]string{"class", "add-teachers", "--dept-id", "789", "--teacher-user-ids", "t1,t2", "--is-adviser", "1"}) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if caller.productID != "edu-contact" { + t.Errorf("productID = %q, want %q", caller.productID, "edu-contact") + } + if caller.tool != "batch_add_class_teacher" { + t.Errorf("tool = %q, want %q", caller.tool, "batch_add_class_teacher") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] is not map[string]any: %#v", caller.args) + } + if input["deptId"] != int64(789) { + t.Errorf("input[deptId] = %v (%T), want int64(789)", input["deptId"], input["deptId"]) + } + teacherUserIds, ok := input["teacherUserIds"].([]string) + if !ok { + t.Fatalf("input[teacherUserIds] is not []string: %#v", input["teacherUserIds"]) + } + if len(teacherUserIds) != 2 || teacherUserIds[0] != "t1" || teacherUserIds[1] != "t2" { + t.Errorf("input[teacherUserIds] = %v, want [t1 t2]", teacherUserIds) + } + if input["isAdviser"] != int64(1) { + t.Errorf("input[isAdviser] = %v (%T), want int64(1)", input["isAdviser"], input["isAdviser"]) + } + }) + + t.Run("class move-student dispatches move_student with list and two int flags", func(t *testing.T) { + caller := withEduContactDispatchCaller(t) + root := newEduContactCommand() + root.SetArgs([]string{"class", "move-student", "--student-user-ids", "u1,u2", "--origin-class-id", "100", "--target-class-id", "200"}) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if caller.productID != "edu-contact" { + t.Errorf("productID = %q, want %q", caller.productID, "edu-contact") + } + if caller.tool != "move_student" { + t.Errorf("tool = %q, want %q", caller.tool, "move_student") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] is not map[string]any: %#v", caller.args) + } + studentUserIds, ok := input["studentUserIds"].([]string) + if !ok { + t.Fatalf("input[studentUserIds] is not []string: %#v", input["studentUserIds"]) + } + if len(studentUserIds) != 2 || studentUserIds[0] != "u1" || studentUserIds[1] != "u2" { + t.Errorf("input[studentUserIds] = %v, want [u1 u2]", studentUserIds) + } + if input["originClassId"] != int64(100) { + t.Errorf("input[originClassId] = %v (%T), want int64(100)", input["originClassId"], input["originClassId"]) + } + if input["targetClassId"] != int64(200) { + t.Errorf("input[targetClassId] = %v (%T), want int64(200)", input["targetClassId"], input["targetClassId"]) + } + }) +} diff --git a/internal/helpers/edu_family_group.go b/internal/helpers/edu_family_group.go new file mode 100644 index 00000000..e655f5f9 --- /dev/null +++ b/internal/helpers/edu_family_group.go @@ -0,0 +1,605 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package helpers + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" +) + +// ────────────────────────────────────────────────────────── +// dws edu-familygroup — 家庭群管理 +// 共 6 个工具,按 group(读操作)/ manage(写操作)分组 +// 参考 wukong/extensions/vendors/dingtalk/eduFamilyGroup.go 迁移 +// ────────────────────────────────────────────────────────── + +func newEduFamilyGroupCommand() *cobra.Command { + contract.RegisterProductDecl(contract.ProductDecl{ + ID: "edu-familygroup", + Selection: contract.ProductSelectionDecl{ + AgentSummary: "家庭群查询/创建、孩子管理、家长邀请、学生应用权限控制", + UseWhen: []string{ + "用户要查询或管理钉钉家庭群、添加孩子、邀请家长或控制学生应用权限。", + }, + AvoidWhen: []string{ + "家校通讯录用 edu-contact;班级师生群用 edu-group;家校应用/作业/打卡用 edu-app。", + }, + }, + }) + root := &cobra.Command{ + Use: "edu-familygroup", + Short: "家庭群", + Long: `钉钉家庭群管理:家庭群查询/创建、孩子管理、家长邀请、学生应用权限控制等。`, + Hidden: true, + RunE: groupRunE, + } + + // ════════════════════════════════════════════════════════════ + // group 子命令组 — 家庭群读操作 + // ════════════════════════════════════════════════════════════ + + groupCmd := &cobra.Command{Use: "group", Short: "家庭群查询", RunE: groupRunE} + + groupCheckExistsCmd := &cobra.Command{ + Use: "check-exists", + Short: "检查家庭群是否存在", + Long: `根据传入的 uid 拉取该用户所有家庭组织,按家庭群名称匹配判断家庭群是否存在。 +仅当存在同名家庭且其群会话 cid 非空时,才认为家庭群存在,返回 true,否则返回 false。 +面向家长(GUARDIAN)角色。`, + Example: ` dws edu-familygroup group check-exists --uid 12345 --group-name "小明一家" + dws edu-familygroup group check-exists --uid 12345 --group-name "小明一家" -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + uid, err := eduFamilyGroupRequiredIntFlag(cmd, "uid") + if err != nil { + return err + } + groupName, err := eduFamilyGroupRequiredStringFlag(cmd, "group-name") + if err != nil { + return err + } + return callMCPToolOnServer("edu-familygroup", "check_family_group_exists", map[string]any{ + "input": map[string]any{"uid": uid, "groupName": groupName}, + }) + }, + } + DeclareLeafMetadata(groupCheckExistsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-familygroup", + Name: "check_family_group_exists", + CanonicalPath: "edu-familygroup.check_family_group_exists", + CLIPath: "edu-familygroup group check-exists", + PrimaryCLIPath: "edu-familygroup group check-exists", + }, + Description: "检查家庭群是否存在", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-familygroup", RPCName: "check_family_group_exists"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "检查家庭群是否存在", + UseWhen: []string{"需要判断指定用户名下是否存在同名家庭群时"}, + AvoidWhen: []string{"查询家庭成员信息用 list-children"}, + Examples: []string{ + "dws edu-familygroup group check-exists --uid 12345 --group-name \"小明一家\" --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "uid", Property: "input.uid", Required: boolPtr(true)}, + {Name: "group-name", Property: "input.groupName", Required: boolPtr(true)}, + }, + }, + }) + + groupListChildrenCmd := &cobra.Command{ + Use: "list-children", + Short: "查询家长绑定的孩子列表", + Long: `查询当前用户(uid)作为家长身份所在家庭中的所有孩子信息(不限家庭组织), +包含孩子基本信息及关联的学生号列表。底层按 uid 读扩散并完成家长身份校验,无孩子时返回空列表。 +面向家长(GUARDIAN)角色。`, + Example: ` dws edu-familygroup group list-children --uid 12345 + dws edu-familygroup group list-children --uid 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + uid, err := eduFamilyGroupRequiredIntFlag(cmd, "uid") + if err != nil { + return err + } + return callMCPToolOnServer("edu-familygroup", "listBoundChildren", map[string]any{ + "input": map[string]any{"uid": uid}, + }) + }, + } + DeclareLeafMetadata(groupListChildrenCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-familygroup", + Name: "listBoundChildren", + CanonicalPath: "edu-familygroup.listBoundChildren", + CLIPath: "edu-familygroup group list-children", + PrimaryCLIPath: "edu-familygroup group list-children", + }, + Description: "查询家长绑定的孩子列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-familygroup", RPCName: "listBoundChildren"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询家长绑定的孩子列表", + UseWhen: []string{"需要查询指定家长 uid 绑定的所有孩子及关联学生号信息时"}, + AvoidWhen: []string{"查看家庭群是否存在用 check-exists"}, + Examples: []string{ + "dws edu-familygroup group list-children --uid 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "uid", Property: "input.uid", Required: boolPtr(true)}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // manage 子命令组 — 家庭群写操作 + // ════════════════════════════════════════════════════════════ + + manageCmd := &cobra.Command{Use: "manage", Short: "家庭群管理", RunE: groupRunE} + + manageCreateCmd := &cobra.Command{ + Use: "create", + Short: "创建家庭群", + Long: `以 uid 作为创建人创建一个新的家庭,创建家庭时会同时创建家庭群,返回结果中携带群会话 cid。 +children 为 JSON 数组,每个元素包含 name(必填)、students(必填,含 corpId + staffId)、 +birthday / gender / nick / avatar / period / grade / mobile(均可选)。 +addGroup 为 JSON 对象,含 schoolCorpId / schoolStaffId / inviteDingtalkId / inviteId(均可选)。 +新建家庭场景下无需前置家长身份校验,创建人合法性由底层校验单元完成。`, + Example: ` dws edu-familygroup manage create --uid 12345 --children '[{"name":"小明","students":[{"corpId":"dingxxx","staffId":"stu001"}]}]' + dws edu-familygroup manage create --uid 12345 --children '[{"name":"小明","students":[{"corpId":"dingxxx","staffId":"stu001"}]}]' --source 1`, + RunE: func(cmd *cobra.Command, args []string) error { + uid, err := eduFamilyGroupRequiredIntFlag(cmd, "uid") + if err != nil { + return err + } + childrenRaw, _ := cmd.Flags().GetString("children") + if strings.TrimSpace(childrenRaw) == "" { + return fmt.Errorf("--children 为必填参数") + } + var children []any + if err := json.Unmarshal([]byte(childrenRaw), &children); err != nil { + return fmt.Errorf("--children 须为合法 JSON 数组: %w", err) + } + if err := eduFamilyGroupValidateChildren(children); err != nil { + return err + } + input := map[string]any{"uid": uid, "children": children} + if v, _ := cmd.Flags().GetString("add-group"); strings.TrimSpace(v) != "" { + var addGroup map[string]any + if err := json.Unmarshal([]byte(v), &addGroup); err != nil { + return fmt.Errorf("--add-group 须为合法 JSON 对象: %w", err) + } + if addGroup == nil { + return fmt.Errorf("--add-group 须为 JSON 对象,不能为 null") + } + input["addGroup"] = addGroup + } + if v, _ := cmd.Flags().GetInt("source"); cmd.Flags().Changed("source") { + input["source"] = v + } + return callMCPToolOnServer("edu-familygroup", "create_family_group", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(manageCreateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-familygroup", + Name: "create_family_group", + CanonicalPath: "edu-familygroup.create_family_group", + CLIPath: "edu-familygroup manage create", + PrimaryCLIPath: "edu-familygroup manage create", + }, + Description: "创建家庭群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-familygroup", RPCName: "create_family_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "创建家庭群并同步创建家庭组织", + UseWhen: []string{"需要以指定 uid 创建新的家庭群,同时注册孩子信息并生成群会话时"}, + AvoidWhen: []string{"已有家庭群要加孩子用 add-child"}, + Examples: []string{ + "dws edu-familygroup manage create --uid 12345 --children '[{\"name\":\"小明\",\"students\":[{\"corpId\":\"dingxxx\",\"staffId\":\"stu001\"}]}]' --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "uid", Property: "input.uid", Required: boolPtr(true)}, + {Name: "children", Property: "input.children", Required: boolPtr(true)}, + {Name: "add-group", Property: "input.addGroup"}, + {Name: "source", Property: "input.source"}, + }, + }, + }) + + manageInviteParentCmd := &cobra.Command{ + Use: "invite-parent", + Short: "短信邀请家长加入家庭群", + Long: `通过短信向指定手机号的家长发送家庭群邀请链接,家长点击链接后加入当前家庭组织及家庭群。 +返回 true 表示邀请短信已成功发送。仅家长(GUARDIAN)角色可调用。`, + Example: ` dws edu-familygroup manage invite-parent --org-id 12345 --uid 67890 --mobile 13800138000 + dws edu-familygroup manage invite-parent --org-id 12345 --uid 67890 --mobile 13800138000 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + orgID, err := eduFamilyGroupRequiredIntFlag(cmd, "org-id") + if err != nil { + return err + } + uid, err := eduFamilyGroupRequiredIntFlag(cmd, "uid") + if err != nil { + return err + } + mobile, err := eduFamilyGroupRequiredStringFlag(cmd, "mobile") + if err != nil { + return err + } + return callMCPToolOnServer("edu-familygroup", "invite_parent_to_familygroup", map[string]any{ + "input": map[string]any{"orgId": orgID, "uid": uid, "mobile": mobile}, + }) + }, + } + DeclareLeafMetadata(manageInviteParentCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-familygroup", + Name: "invite_parent_to_familygroup", + CanonicalPath: "edu-familygroup.invite_parent_to_familygroup", + CLIPath: "edu-familygroup manage invite-parent", + PrimaryCLIPath: "edu-familygroup manage invite-parent", + }, + Description: "短信邀请家长加入家庭群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-familygroup", RPCName: "invite_parent_to_familygroup"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "通过短信邀请家长加入家庭群", + UseWhen: []string{"需要向指定手机号发送家庭群邀请短信时"}, + AvoidWhen: []string{"添加孩子到家庭群用 add-child"}, + Examples: []string{ + "dws edu-familygroup manage invite-parent --org-id 12345 --uid 67890 --mobile 13800138000 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "org-id", Property: "input.orgId", Required: boolPtr(true)}, + {Name: "uid", Property: "input.uid", Required: boolPtr(true)}, + {Name: "mobile", Property: "input.mobile", Required: boolPtr(true)}, + }, + }, + }) + + manageAddChildCmd := &cobra.Command{ + Use: "add-child", + Short: "为家庭群添加孩子", + Long: `为指定家庭群添加孩子,支持三种方式(由底层自动路由): + - 仅传 --mobile:手机号邀请链路,该手机号对应的钉钉账号被邀请加入家庭群 + - 仅传 --students:直接生成学生号链路,选中学生后创建孩子并绑定关系 + - 同时传 --mobile + --students:mobile 优先,走手机号邀请链路 +mobile 与 students 至少传一个。 +students 为 JSON 数组,每个元素含 schoolOrgId(整数)和 studentStaffId(字符串),均必填。 +仅家长(GUARDIAN)角色可调用。`, + Example: ` dws edu-familygroup manage add-child --org-id 12345 --uid 67890 --name 小明 --mobile 13900139000 + dws edu-familygroup manage add-child --org-id 12345 --uid 67890 --name 小明 --students '[{"schoolOrgId":111,"studentStaffId":"stu001"}]'`, + RunE: func(cmd *cobra.Command, args []string) error { + orgID, err := eduFamilyGroupRequiredIntFlag(cmd, "org-id") + if err != nil { + return err + } + uid, err := eduFamilyGroupRequiredIntFlag(cmd, "uid") + if err != nil { + return err + } + name, err := eduFamilyGroupRequiredStringFlag(cmd, "name") + if err != nil { + return err + } + mobile, _ := cmd.Flags().GetString("mobile") + mobile = strings.TrimSpace(mobile) + studentsRaw, _ := cmd.Flags().GetString("students") + studentsRaw = strings.TrimSpace(studentsRaw) + if mobile == "" && studentsRaw == "" { + return fmt.Errorf("--mobile 与 --students 至少传一个") + } + input := map[string]any{"orgId": orgID, "uid": uid, "name": name} + if mobile != "" { + input["mobile"] = mobile + } + if studentsRaw != "" { + var students []any + if err := json.Unmarshal([]byte(studentsRaw), &students); err != nil { + return fmt.Errorf("--students 须为合法 JSON 数组: %w", err) + } + if err := eduFamilyGroupValidateStudents(students); err != nil { + return err + } + input["students"] = students + } + return callMCPToolOnServer("edu-familygroup", "add_child_to_family_group", map[string]any{ + "input": input, + }) + }, + } + DeclareLeafMetadata(manageAddChildCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-familygroup", + Name: "add_child_to_family_group", + CanonicalPath: "edu-familygroup.add_child_to_family_group", + CLIPath: "edu-familygroup manage add-child", + PrimaryCLIPath: "edu-familygroup manage add-child", + }, + Description: "为家庭群添加孩子", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-familygroup", RPCName: "add_child_to_family_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "为已有家庭群添加孩子", + UseWhen: []string{"需要向已有家庭群添加新孩子(通过手机号邀请或直接绑定学生号)时"}, + AvoidWhen: []string{"创建全新家庭群用 create"}, + Examples: []string{ + "dws edu-familygroup manage add-child --org-id 12345 --uid 67890 --name 小明 --mobile 13900139000 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "org-id", Property: "input.orgId", Required: boolPtr(true)}, + {Name: "uid", Property: "input.uid", Required: boolPtr(true)}, + {Name: "name", Property: "input.name", Required: boolPtr(true)}, + {Name: "mobile", Property: "input.mobile"}, + {Name: "students", Property: "input.students"}, + }, + }, + }) + + manageToggleAppCmd := &cobra.Command{ + Use: "toggle-app", + Short: "开启或关闭学生应用权限", + Long: `为指定学生号开启或关闭应用权限。 +支持的应用类型(--app-type): + - XIAOTIANDI:小天地(学生圈) + - LEARNING_VIDEO:学习视频 +直接覆写权限状态,天然幂等。仅家长(GUARDIAN)角色可调用。`, + Example: ` dws edu-familygroup manage toggle-app --org-id 12345 --uid 67890 --child-staff-id staff001 --app-type XIAOTIANDI --open true + dws edu-familygroup manage toggle-app --org-id 12345 --uid 67890 --child-staff-id staff001 --app-type LEARNING_VIDEO --open false`, + RunE: func(cmd *cobra.Command, args []string) error { + orgID, err := eduFamilyGroupRequiredIntFlag(cmd, "org-id") + if err != nil { + return err + } + uid, err := eduFamilyGroupRequiredIntFlag(cmd, "uid") + if err != nil { + return err + } + childStaffID, err := eduFamilyGroupRequiredStringFlag(cmd, "child-staff-id") + if err != nil { + return err + } + appType, err := eduFamilyGroupRequiredStringFlag(cmd, "app-type") + if err != nil { + return err + } + if appType != "XIAOTIANDI" && appType != "LEARNING_VIDEO" { + return fmt.Errorf("--app-type 须为 XIAOTIANDI 或 LEARNING_VIDEO") + } + openStr, err := eduFamilyGroupRequiredStringFlag(cmd, "open") + if err != nil { + return err + } + var open bool + switch strings.ToLower(openStr) { + case "true": + open = true + case "false": + open = false + default: + return fmt.Errorf("--open 须为 true 或 false") + } + return callMCPToolOnServer("edu-familygroup", "toggle_student_app", map[string]any{ + "input": map[string]any{ + "orgId": orgID, "uid": uid, "childStaffId": childStaffID, + "appType": appType, "open": open, + }, + }) + }, + } + DeclareLeafMetadata(manageToggleAppCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-familygroup", + Name: "toggle_student_app", + CanonicalPath: "edu-familygroup.toggle_student_app", + CLIPath: "edu-familygroup manage toggle-app", + PrimaryCLIPath: "edu-familygroup manage toggle-app", + }, + Description: "开启或关闭学生应用权限", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-familygroup", RPCName: "toggle_student_app"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "开启或关闭学生应用权限", + UseWhen: []string{"需要为指定学生号开启或关闭小天地/学习视频应用权限时"}, + AvoidWhen: []string{"管理家庭群成员用 add-child / invite-parent"}, + Examples: []string{ + "dws edu-familygroup manage toggle-app --org-id 12345 --uid 67890 --child-staff-id staff001 --app-type XIAOTIANDI --open true --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "org-id", Property: "input.orgId", Required: boolPtr(true)}, + {Name: "uid", Property: "input.uid", Required: boolPtr(true)}, + {Name: "child-staff-id", Property: "input.childStaffId", Required: boolPtr(true)}, + {Name: "app-type", Property: "input.appType", Required: boolPtr(true)}, + {Name: "open", Property: "input.open", Required: boolPtr(true)}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // flags + 构建命令树 + // ════════════════════════════════════════════════════════════ + + // group(读操作)flags + groupCheckExistsCmd.Flags().String("uid", "", "用户 uid(必填)") + groupCheckExistsCmd.Flags().String("group-name", "", "家庭群名称(必填)") + groupListChildrenCmd.Flags().String("uid", "", "家长 uid(必填)") + + // manage(写操作)flags + manageCreateCmd.Flags().String("uid", "", "创建人 uid(必填)") + manageCreateCmd.Flags().String("children", "", "孩子信息 JSON 数组(必填)") + manageCreateCmd.Flags().String("add-group", "", "同学群信息 JSON 对象(可选)") + manageCreateCmd.Flags().Int("source", 0, "渠道来源(可选)") + + manageInviteParentCmd.Flags().String("org-id", "", "家庭组织 ID(必填)") + manageInviteParentCmd.Flags().String("uid", "", "操作人 uid(必填)") + manageInviteParentCmd.Flags().String("mobile", "", "被邀请家长手机号(必填)") + + manageAddChildCmd.Flags().String("org-id", "", "家庭组织 ID(必填)") + manageAddChildCmd.Flags().String("uid", "", "操作人 uid(必填)") + manageAddChildCmd.Flags().String("name", "", "孩子姓名(必填)") + manageAddChildCmd.Flags().String("mobile", "", "孩子手机号(可选,与 --students 至少传一个)") + manageAddChildCmd.Flags().String("students", "", "待关联学生号 JSON 数组(可选,每项含 schoolOrgId + studentStaffId)") + + manageToggleAppCmd.Flags().String("org-id", "", "家庭组织 ID(必填)") + manageToggleAppCmd.Flags().String("uid", "", "家长 uid(必填)") + manageToggleAppCmd.Flags().String("child-staff-id", "", "孩子在家庭组织中的 staffId(必填)") + manageToggleAppCmd.Flags().String("app-type", "", "应用类型:XIAOTIANDI / LEARNING_VIDEO(必填)") + manageToggleAppCmd.Flags().String("open", "", "true=开启 / false=关闭(必填)") + + groupCmd.AddCommand(groupCheckExistsCmd, groupListChildrenCmd) + manageCmd.AddCommand(manageCreateCmd, manageInviteParentCmd, manageAddChildCmd, manageToggleAppCmd) + + root.AddCommand(groupCmd, manageCmd) + + return root +} + +// eduFamilyGroupValidateChildren validates the --children payload: the array +// must be non-empty, each child must carry a non-empty name and a non-empty +// students array, and each student must carry corpId + staffId. +func eduFamilyGroupValidateChildren(children []any) error { + if len(children) == 0 { + return fmt.Errorf("--children 不能为空数组,至少需包含一个孩子") + } + for i, c := range children { + child, ok := c.(map[string]any) + if !ok { + return fmt.Errorf("--children[%d] 须为 JSON 对象", i) + } + name, _ := child["name"].(string) + if strings.TrimSpace(name) == "" { + return fmt.Errorf("--children[%d].name 为必填字段", i) + } + students, ok := child["students"].([]any) + if !ok || len(students) == 0 { + return fmt.Errorf("--children[%d].students 为必填字段且不能为空数组", i) + } + for j, s := range students { + student, ok := s.(map[string]any) + if !ok { + return fmt.Errorf("--children[%d].students[%d] 须为 JSON 对象", i, j) + } + corpID, _ := student["corpId"].(string) + if strings.TrimSpace(corpID) == "" { + return fmt.Errorf("--children[%d].students[%d].corpId 为必填字段", i, j) + } + staffID, _ := student["staffId"].(string) + if strings.TrimSpace(staffID) == "" { + return fmt.Errorf("--children[%d].students[%d].staffId 为必填字段", i, j) + } + } + } + return nil +} + +// eduFamilyGroupValidateStudents validates the --students payload: the array +// must be non-empty and each element must carry a numeric schoolOrgId and a +// non-empty studentStaffId. +func eduFamilyGroupValidateStudents(students []any) error { + if len(students) == 0 { + return fmt.Errorf("--students 不能为空数组,至少需包含一个学生号") + } + for i, s := range students { + student, ok := s.(map[string]any) + if !ok { + return fmt.Errorf("--students[%d] 须为 JSON 对象", i) + } + switch student["schoolOrgId"].(type) { + case float64, int, int64, json.Number: + default: + return fmt.Errorf("--students[%d].schoolOrgId 为必填字段且须为整数", i) + } + staffID, _ := student["studentStaffId"].(string) + if strings.TrimSpace(staffID) == "" { + return fmt.Errorf("--students[%d].studentStaffId 为必填字段", i) + } + } + return nil +} + +// eduFamilyGroupRequiredIntFlag extracts a required integer flag, returning an +// error if the flag is empty or not a valid integer. +func eduFamilyGroupRequiredIntFlag(cmd *cobra.Command, name string) (int64, error) { + v, _ := cmd.Flags().GetString(name) + v = strings.TrimSpace(v) + if v == "" { + return 0, fmt.Errorf("--%s 为必填参数", name) + } + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return 0, fmt.Errorf("--%s 须为整数: %w", name, err) + } + return n, nil +} + +// eduFamilyGroupRequiredStringFlag extracts a required string flag. +func eduFamilyGroupRequiredStringFlag(cmd *cobra.Command, name string) (string, error) { + v, _ := cmd.Flags().GetString(name) + v = strings.TrimSpace(v) + if v == "" { + return "", fmt.Errorf("--%s 为必填参数", name) + } + return v, nil +} diff --git a/internal/helpers/edu_family_group_test.go b/internal/helpers/edu_family_group_test.go new file mode 100644 index 00000000..c4f5f73e --- /dev/null +++ b/internal/helpers/edu_family_group_test.go @@ -0,0 +1,286 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package helpers + +import ( + "io" + "strings" + "testing" +) + +func withEduFamilyGroupCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{dryRun: true} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +func TestEduFamilyGroupHappyPaths(t *testing.T) { + cases := [][]string{ + {"group", "check-exists", "--uid", "1", "--group-name", "小明一家"}, + {"group", "list-children", "--uid", "1"}, + {"manage", "create", "--uid", "1", + "--children", `[{"name":"小明","students":[{"corpId":"c","staffId":"s"}]}]`, + "--add-group", `{"schoolCorpId":"x"}`, "--source", "1"}, + {"manage", "invite-parent", "--org-id", "1", "--uid", "2", "--mobile", "13800138000"}, + {"manage", "add-child", "--org-id", "1", "--uid", "2", "--name", "小明", "--mobile", "13900139000"}, + {"manage", "add-child", "--org-id", "1", "--uid", "2", "--name", "小明", + "--students", `[{"schoolOrgId":111,"studentStaffId":"stu001"}]`}, + {"manage", "toggle-app", "--org-id", "1", "--uid", "2", "--child-staff-id", "s", + "--app-type", "XIAOTIANDI", "--open", "true"}, + {"manage", "toggle-app", "--org-id", "1", "--uid", "2", "--child-staff-id", "s", + "--app-type", "LEARNING_VIDEO", "--open", "false"}, + } + for _, args := range cases { + t.Run(strings.Join(args, " "), func(t *testing.T) { + withEduFamilyGroupCaller(t) + cmd := newEduFamilyGroupCommand() + cmd.SetArgs(args) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute(%v) = %v, want nil", args, err) + } + }) + } +} + +func TestEduFamilyGroupErrorPaths(t *testing.T) { + cases := []struct { + name string + args []string + want string + }{ + {"check-exists missing uid", []string{"group", "check-exists", "--group-name", "x"}, "uid"}, + {"check-exists non-int uid", []string{"group", "check-exists", "--uid", "abc", "--group-name", "x"}, "整数"}, + {"check-exists missing group-name", []string{"group", "check-exists", "--uid", "1"}, "group-name"}, + {"list-children missing uid", []string{"group", "list-children"}, "uid"}, + {"create missing uid", []string{"manage", "create", "--children", "[]"}, "uid"}, + {"create missing children", []string{"manage", "create", "--uid", "1"}, "children"}, + {"create children bad json", []string{"manage", "create", "--uid", "1", "--children", "{"}, "JSON"}, + {"create children empty array", []string{"manage", "create", "--uid", "1", "--children", "[]"}, "不能为空数组"}, + {"create add-group bad json", []string{"manage", "create", "--uid", "1", + "--children", `[{"name":"小明","students":[{"corpId":"c","staffId":"s"}]}]`, + "--add-group", "{"}, "add-group"}, + {"create add-group null", []string{"manage", "create", "--uid", "1", + "--children", `[{"name":"小明","students":[{"corpId":"c","staffId":"s"}]}]`, + "--add-group", "null"}, "null"}, + {"invite-parent missing org-id", []string{"manage", "invite-parent", "--uid", "2", "--mobile", "138"}, "org-id"}, + {"invite-parent missing uid", []string{"manage", "invite-parent", "--org-id", "1", "--mobile", "138"}, "uid"}, + {"invite-parent missing mobile", []string{"manage", "invite-parent", "--org-id", "1", "--uid", "2"}, "mobile"}, + {"add-child missing org-id", []string{"manage", "add-child", "--uid", "2", "--name", "x", "--mobile", "138"}, "org-id"}, + {"add-child missing uid", []string{"manage", "add-child", "--org-id", "1", "--name", "x", "--mobile", "138"}, "uid"}, + {"add-child missing name", []string{"manage", "add-child", "--org-id", "1", "--uid", "2", "--mobile", "138"}, "name"}, + {"add-child no mobile no students", []string{"manage", "add-child", "--org-id", "1", "--uid", "2", "--name", "x"}, "至少传一个"}, + {"add-child students bad json", []string{"manage", "add-child", "--org-id", "1", "--uid", "2", "--name", "x", "--students", "{"}, "students"}, + {"add-child students empty", []string{"manage", "add-child", "--org-id", "1", "--uid", "2", "--name", "x", "--students", "[]"}, "不能为空数组"}, + {"toggle-app missing org-id", []string{"manage", "toggle-app", "--uid", "2", "--child-staff-id", "s", "--app-type", "XIAOTIANDI", "--open", "true"}, "org-id"}, + {"toggle-app missing uid", []string{"manage", "toggle-app", "--org-id", "1", "--child-staff-id", "s", "--app-type", "XIAOTIANDI", "--open", "true"}, "uid"}, + {"toggle-app missing child-staff-id", []string{"manage", "toggle-app", "--org-id", "1", "--uid", "2", "--app-type", "XIAOTIANDI", "--open", "true"}, "child-staff-id"}, + {"toggle-app missing app-type", []string{"manage", "toggle-app", "--org-id", "1", "--uid", "2", "--child-staff-id", "s", "--open", "true"}, "app-type"}, + {"toggle-app invalid app-type", []string{"manage", "toggle-app", "--org-id", "1", "--uid", "2", "--child-staff-id", "s", "--app-type", "OTHER", "--open", "true"}, "XIAOTIANDI"}, + {"toggle-app missing open", []string{"manage", "toggle-app", "--org-id", "1", "--uid", "2", "--child-staff-id", "s", "--app-type", "XIAOTIANDI"}, "open"}, + {"toggle-app invalid open", []string{"manage", "toggle-app", "--org-id", "1", "--uid", "2", "--child-staff-id", "s", "--app-type", "XIAOTIANDI", "--open", "maybe"}, "true 或 false"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + withEduFamilyGroupCaller(t) + cmd := newEduFamilyGroupCommand() + cmd.SetArgs(tc.args) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("Execute(%v) error = %v, want contains %q", tc.args, err, tc.want) + } + }) + } +} + +func TestEduFamilyGroupValidateChildren(t *testing.T) { + valid := []any{map[string]any{ + "name": "小明", + "students": []any{map[string]any{"corpId": "c", "staffId": "s"}}, + }} + if err := eduFamilyGroupValidateChildren(valid); err != nil { + t.Fatalf("valid children error = %v", err) + } + cases := []struct { + name string + children []any + want string + }{ + {"empty", []any{}, "不能为空数组"}, + {"non-object", []any{1}, "须为 JSON 对象"}, + {"missing name", []any{map[string]any{"students": []any{map[string]any{"corpId": "c", "staffId": "s"}}}}, "name 为必填"}, + {"missing students", []any{map[string]any{"name": "x"}}, "students 为必填"}, + {"student non-object", []any{map[string]any{"name": "x", "students": []any{1}}}, "须为 JSON 对象"}, + {"missing corpId", []any{map[string]any{"name": "x", "students": []any{map[string]any{"staffId": "s"}}}}, "corpId 为必填"}, + {"missing staffId", []any{map[string]any{"name": "x", "students": []any{map[string]any{"corpId": "c"}}}}, "staffId 为必填"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if err := eduFamilyGroupValidateChildren(tc.children); err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("error = %v, want contains %q", err, tc.want) + } + }) + } +} + +func TestEduFamilyGroupValidateStudents(t *testing.T) { + valid := []any{map[string]any{"schoolOrgId": float64(111), "studentStaffId": "stu001"}} + if err := eduFamilyGroupValidateStudents(valid); err != nil { + t.Fatalf("valid students error = %v", err) + } + cases := []struct { + name string + students []any + want string + }{ + {"empty", []any{}, "不能为空数组"}, + {"non-object", []any{1}, "须为 JSON 对象"}, + {"missing schoolOrgId", []any{map[string]any{"studentStaffId": "s"}}, "schoolOrgId 为必填"}, + {"missing studentStaffId", []any{map[string]any{"schoolOrgId": float64(1)}}, "studentStaffId 为必填"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if err := eduFamilyGroupValidateStudents(tc.students); err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("error = %v, want contains %q", err, tc.want) + } + }) + } +} + +func withEduFamilyGroupDispatchCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{dryRun: false} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +func TestEduFamilyGroupDispatch(t *testing.T) { + t.Run("check-exists", func(t *testing.T) { + caller := withEduFamilyGroupDispatchCaller(t) + cmd := newEduFamilyGroupCommand() + cmd.SetArgs([]string{"group", "check-exists", "--uid", "123", "--group-name", "测试家庭"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-familygroup" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-familygroup") + } + if caller.tool != "check_family_group_exists" { + t.Fatalf("tool = %q, want %q", caller.tool, "check_family_group_exists") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map[string]any", caller.args["input"]) + } + if input["uid"] != int64(123) { + t.Fatalf("input[\"uid\"] = %#v, want int64(123)", input["uid"]) + } + if input["groupName"] != "测试家庭" { + t.Fatalf("input[\"groupName\"] = %#v, want %q", input["groupName"], "测试家庭") + } + }) + + t.Run("list-children", func(t *testing.T) { + caller := withEduFamilyGroupDispatchCaller(t) + cmd := newEduFamilyGroupCommand() + cmd.SetArgs([]string{"group", "list-children", "--uid", "456"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-familygroup" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-familygroup") + } + if caller.tool != "listBoundChildren" { + t.Fatalf("tool = %q, want %q", caller.tool, "listBoundChildren") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map[string]any", caller.args["input"]) + } + if input["uid"] != int64(456) { + t.Fatalf("input[\"uid\"] = %#v, want int64(456)", input["uid"]) + } + }) + + t.Run("create", func(t *testing.T) { + caller := withEduFamilyGroupDispatchCaller(t) + cmd := newEduFamilyGroupCommand() + cmd.SetArgs([]string{"manage", "create", "--uid", "789", + "--children", `[{"name":"小明","students":[{"corpId":"c","staffId":"s"}]}]`}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-familygroup" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-familygroup") + } + if caller.tool != "create_family_group" { + t.Fatalf("tool = %q, want %q", caller.tool, "create_family_group") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map[string]any", caller.args["input"]) + } + if input["uid"] != int64(789) { + t.Fatalf("input[\"uid\"] = %#v, want int64(789)", input["uid"]) + } + if input["children"] == nil { + t.Fatalf("input[\"children\"] is nil, want non-nil") + } + }) + + t.Run("invite-parent", func(t *testing.T) { + caller := withEduFamilyGroupDispatchCaller(t) + cmd := newEduFamilyGroupCommand() + cmd.SetArgs([]string{"manage", "invite-parent", "--org-id", "1", "--uid", "2", "--mobile", "13800138000"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-familygroup" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-familygroup") + } + if caller.tool != "invite_parent_to_familygroup" { + t.Fatalf("tool = %q, want %q", caller.tool, "invite_parent_to_familygroup") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map[string]any", caller.args["input"]) + } + if input["orgId"] != int64(1) { + t.Fatalf("input[\"orgId\"] = %#v, want int64(1)", input["orgId"]) + } + if input["uid"] != int64(2) { + t.Fatalf("input[\"uid\"] = %#v, want int64(2)", input["uid"]) + } + if input["mobile"] != "13800138000" { + t.Fatalf("input[\"mobile\"] = %#v, want %q", input["mobile"], "13800138000") + } + }) + + t.Run("toggle-app", func(t *testing.T) { + caller := withEduFamilyGroupDispatchCaller(t) + cmd := newEduFamilyGroupCommand() + cmd.SetArgs([]string{"manage", "toggle-app", "--org-id", "1", "--uid", "2", + "--child-staff-id", "s", "--app-type", "XIAOTIANDI", "--open", "true"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() = %v", err) + } + if caller.productID != "edu-familygroup" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-familygroup") + } + if caller.tool != "toggle_student_app" { + t.Fatalf("tool = %q, want %q", caller.tool, "toggle_student_app") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map[string]any", caller.args["input"]) + } + if input["appType"] != "XIAOTIANDI" { + t.Fatalf("input[\"appType\"] = %#v, want %q", input["appType"], "XIAOTIANDI") + } + if input["open"] != true { + t.Fatalf("input[\"open\"] = %#v, want true", input["open"]) + } + }) +} diff --git a/internal/helpers/edu_group.go b/internal/helpers/edu_group.go new file mode 100644 index 00000000..93d84317 --- /dev/null +++ b/internal/helpers/edu_group.go @@ -0,0 +1,846 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package helpers + +import ( + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" +) + +// ────────────────────────────────────────────────────────── +// dws edu-group — 家校群管理 +// 共 14 个工具,按 student-group / class-group / batch 分组 +// ────────────────────────────────────────────────────────── + +func newEduGroupCommand() *cobra.Command { + contract.RegisterProductDecl(contract.ProductDecl{ + ID: "edu-group", + Selection: contract.ProductSelectionDecl{ + AgentSummary: "师生群查询/创建/解散、班级群会话信息查询、批量操作", + UseWhen: []string{ + "用户要查询或管理钉钉师生群、班级群会话信息,或批量检查/创建师生群。", + }, + AvoidWhen: []string{ + "家庭群用 edu-familygroup;家校通讯录用 edu-contact;家校应用/作业/打卡用 edu-app。", + }, + }, + }) + root := &cobra.Command{ + Use: "edu-group", + Short: "家校群", + Long: `钉钉家校群管理:师生群查询/创建/解散、班级群会话信息查询、批量操作等。`, + Hidden: true, + RunE: groupRunE, + } + + // ════════════════════════════════════════════════════════════ + // student-group 子命令组 — 师生群管理 + // ════════════════════════════════════════════════════════════ + + studentGroupCmd := &cobra.Command{Use: "student-group", Short: "师生群管理", RunE: groupRunE} + + studentGroupInfoCmd := &cobra.Command{ + Use: "info", + Short: "查询班级师生群信息", + Long: `查询指定班级的师生群信息。返回师生群的群会话ID(cid)。管理员、班主任、老师角色可调用。`, + Example: ` dws edu-group student-group info --dept-id 12345 + dws edu-group student-group info --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "get_class_group_info", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(studentGroupInfoCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "get_class_group_info", + CanonicalPath: "edu-group.get_class_group_info", + CLIPath: "edu-group student-group info", + PrimaryCLIPath: "edu-group student-group info", + }, + Description: "查询班级师生群信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "get_class_group_info"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询班级师生群信息", + UseWhen: []string{"需要查询指定班级的师生群会话ID时"}, + AvoidWhen: []string{"查询班级群会话详情用 student-group conversation"}, + Examples: []string{ + "dws edu-group student-group info --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + studentGroupExistsCmd := &cobra.Command{ + Use: "exists", + Short: "检查组织是否已创建师生群", + Long: `检查指定组织下是否已创建师生群。返回是否存在师生群(true/false)。仅限管理员角色调用。`, + Example: ` dws edu-group student-group exists --dept-id 12345 + dws edu-group student-group exists --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "check_class_group_exists", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(studentGroupExistsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "check_class_group_exists", + CanonicalPath: "edu-group.check_class_group_exists", + CLIPath: "edu-group student-group exists", + PrimaryCLIPath: "edu-group student-group exists", + }, + Description: "检查组织是否已创建师生群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "check_class_group_exists"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "检查组织是否已创建师生群", + UseWhen: []string{"需要判断指定班级是否已创建师生群时"}, + AvoidWhen: []string{"查询师生群成员用 student-group members"}, + Examples: []string{ + "dws edu-group student-group exists --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + studentGroupMembersCmd := &cobra.Command{ + Use: "members", + Short: "查询师生群成员列表", + Long: `查询指定班级师生群的所有成员userId列表。管理员、班主任、老师角色可调用。`, + Example: ` dws edu-group student-group members --dept-id 12345 + dws edu-group student-group members --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "get_group_members", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(studentGroupMembersCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "get_group_members", + CanonicalPath: "edu-group.get_group_members", + CLIPath: "edu-group student-group members", + PrimaryCLIPath: "edu-group student-group members", + }, + Description: "查询师生群成员列表", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "get_group_members"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询师生群成员列表", + UseWhen: []string{"需要查询指定班级师生群的所有成员userId列表时"}, + AvoidWhen: []string{"判断用户是否在群中用 student-group is-in"}, + Examples: []string{ + "dws edu-group student-group members --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + studentGroupIsInCmd := &cobra.Command{ + Use: "is-in", + Short: "判断用户是否在师生群中", + Long: `判断当前用户是否在指定班级的师生群中。返回是否在群中(true/false)。管理员、班主任、老师角色可调用。`, + Example: ` dws edu-group student-group is-in --dept-id 12345 + dws edu-group student-group is-in --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "is_in_class_group", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(studentGroupIsInCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "is_in_class_group", + CanonicalPath: "edu-group.is_in_class_group", + CLIPath: "edu-group student-group is-in", + PrimaryCLIPath: "edu-group student-group is-in", + }, + Description: "判断用户是否在师生群中", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "is_in_class_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "判断用户是否在师生群中", + UseWhen: []string{"需要判断当前用户是否在指定班级师生群中时"}, + AvoidWhen: []string{"查询群成员列表用 student-group members"}, + Examples: []string{ + "dws edu-group student-group is-in --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + studentGroupConversationCmd := &cobra.Command{ + Use: "conversation", + Short: "查询班级群会话详情", + Long: `查询指定班级师生群的会话详情。 +返回群会话ID(cid)、群标题(title)、群成员数量(memberCount)和群图标URL(icon)。 +管理员、班主任、老师角色可调用。`, + Example: ` dws edu-group student-group conversation --dept-id 12345 + dws edu-group student-group conversation --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "get_group_conversation_info", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(studentGroupConversationCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "get_group_conversation_info", + CanonicalPath: "edu-group.get_group_conversation_info", + CLIPath: "edu-group student-group conversation", + PrimaryCLIPath: "edu-group student-group conversation", + }, + Description: "查询班级群会话详情", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "get_group_conversation_info"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "查询班级师生群会话详情", + UseWhen: []string{"需要查询指定班级师生群的会话ID、标题、成员数、图标时"}, + AvoidWhen: []string{"仅需群会话ID用 student-group info"}, + Examples: []string{ + "dws edu-group student-group conversation --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + studentGroupCreateCmd := &cobra.Command{ + Use: "create", + Short: "创建班级师生群", + Long: `为指定班级创建师生群。自动将班级的班主任设为群主,并拉入所有老师和学生。 +前提是班级必须已设置班主任。返回创建成功的群会话ID(cid)。仅限管理员或班主任角色调用。`, + Example: ` dws edu-group student-group create --dept-id 12345 + dws edu-group student-group create --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "create_class_group", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(studentGroupCreateCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "create_class_group", + CanonicalPath: "edu-group.create_class_group", + CLIPath: "edu-group student-group create", + PrimaryCLIPath: "edu-group student-group create", + }, + Description: "创建班级师生群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "create_class_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "为指定班级创建师生群", + UseWhen: []string{"需要为指定班级创建师生群,自动拉入班主任、老师和学生时"}, + AvoidWhen: []string{"批量创建师生群用 batch create-student-groups"}, + Examples: []string{ + "dws edu-group student-group create --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + studentGroupDisbandCmd := &cobra.Command{ + Use: "disband", + Short: "解散班级师生群", + Long: `解散指定班级的师生群,同时删除班级与群的关联关系。仅限管理员或班主任角色调用。`, + Example: ` dws edu-group student-group disband --dept-id 12345 + dws edu-group student-group disband --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "disband_class_group", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(studentGroupDisbandCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "destructive", Risk: "high", + Confirmation: "user_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "disband_class_group", + CanonicalPath: "edu-group.disband_class_group", + CLIPath: "edu-group student-group disband", + PrimaryCLIPath: "edu-group student-group disband", + }, + Description: "解散班级师生群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "disband_class_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "解散班级师生群并删除关联关系", + UseWhen: []string{"需要解散指定班级的师生群并删除班级与群的关联关系时"}, + AvoidWhen: []string{"查询师生群信息用 student-group info"}, + Examples: []string{ + "dws edu-group student-group disband --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // class-group 子命令组 — 班级群(家校群)会话管理 + // ════════════════════════════════════════════════════════════ + + classGroupCmd := &cobra.Command{Use: "class-group", Short: "班级群会话管理", RunE: groupRunE} + + classGroupConversationIDCmd := &cobra.Command{ + Use: "conversation-id", + Short: "获取班级群会话ID", + Long: `获取指定班级的班级群会话ID,可用于后续发送群消息等操作。管理员、班主任、老师角色可调用。`, + Example: ` dws edu-group class-group conversation-id --dept-id 12345 + dws edu-group class-group conversation-id --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "get_class_conversation_id", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(classGroupConversationIDCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "get_class_conversation_id", + CanonicalPath: "edu-group.get_class_conversation_id", + CLIPath: "edu-group class-group conversation-id", + PrimaryCLIPath: "edu-group class-group conversation-id", + }, + Description: "获取班级群会话ID", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "get_class_conversation_id"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "获取班级群会话ID", + UseWhen: []string{"需要获取指定班级的班级群会话ID以便后续发送群消息时"}, + AvoidWhen: []string{"需要完整群信息用 class-group conversation"}, + Examples: []string{ + "dws edu-group class-group conversation-id --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + classGroupConversationCmd := &cobra.Command{ + Use: "conversation", + Short: "获取班级群完整会话信息", + Long: `获取指定班级的班级群完整会话信息。 +返回班级群的会话ID(cid)、群标题(title)、群成员数量(memberCount)和群图标URL(icon)。 +管理员、班主任、老师角色可调用。`, + Example: ` dws edu-group class-group conversation --dept-id 12345 + dws edu-group class-group conversation --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "get_class_conversation", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(classGroupConversationCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "get_class_conversation", + CanonicalPath: "edu-group.get_class_conversation", + CLIPath: "edu-group class-group conversation", + PrimaryCLIPath: "edu-group class-group conversation", + }, + Description: "获取班级群完整会话信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "get_class_conversation"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "获取班级群完整会话信息", + UseWhen: []string{"需要查询指定班级的班级群会话ID、标题、成员数、图标时"}, + AvoidWhen: []string{"仅需会话ID用 class-group conversation-id"}, + Examples: []string{ + "dws edu-group class-group conversation --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + classGroupExistsCmd := &cobra.Command{ + Use: "exists", + Short: "检查班级群是否存在", + Long: `检查指定班级是否已创建班级群。返回班级群是否存在(true/false)。管理员、班主任、老师角色可调用。`, + Example: ` dws edu-group class-group exists --dept-id 12345 + dws edu-group class-group exists --dept-id 12345 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + deptID, err := eduGroupRequiredIntFlag(cmd, "dept-id") + if err != nil { + return err + } + return callMCPToolOnServer("edu-group", "check_class_conversation_exists", map[string]any{ + "input": map[string]any{"deptId": deptID}, + }) + }, + } + DeclareLeafMetadata(classGroupExistsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "check_class_conversation_exists", + CanonicalPath: "edu-group.check_class_conversation_exists", + CLIPath: "edu-group class-group exists", + PrimaryCLIPath: "edu-group class-group exists", + }, + Description: "检查班级群是否存在", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "check_class_conversation_exists"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "检查班级群是否存在", + UseWhen: []string{"需要判断指定班级是否已创建班级群时"}, + AvoidWhen: []string{"查询班级群会话信息用 class-group conversation"}, + Examples: []string{ + "dws edu-group class-group exists --dept-id 12345 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "dept-id", Property: "input.deptId", Required: boolPtr(true)}, + }, + }, + }) + + classGroupListByConversationIDsCmd := &cobra.Command{ + Use: "list-by-cids", + Short: "根据会话ID列表批量查询群信息", + Long: `根据群会话ID列表批量查询群的详细信息。 +返回群会话详情列表,每项包含会话ID(cid)、群标题(title)、群成员数量(memberCount)和群图标URL(icon)。 +管理员、班主任、老师角色可调用。`, + Example: ` dws edu-group class-group list-by-cids --conversation-ids cid1,cid2,cid3 + dws edu-group class-group list-by-cids --conversation-ids cid1,cid2 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + raw, _ := cmd.Flags().GetString("conversation-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--conversation-ids 为必填参数") + } + conversationIDs := eduGroupParseCSV(raw) + if len(conversationIDs) == 0 { + return fmt.Errorf("--conversation-ids 不能为空") + } + return callMCPToolOnServer("edu-group", "list_groups_by_conversation_ids", map[string]any{ + "input": map[string]any{"conversationIds": conversationIDs}, + }) + }, + } + DeclareLeafMetadata(classGroupListByConversationIDsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "list_groups_by_conversation_ids", + CanonicalPath: "edu-group.list_groups_by_conversation_ids", + CLIPath: "edu-group class-group list-by-cids", + PrimaryCLIPath: "edu-group class-group list-by-cids", + }, + Description: "根据会话ID列表批量查询群信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "list_groups_by_conversation_ids"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "根据会话ID列表批量查询群信息", + UseWhen: []string{"需要根据一组群会话ID批量查询群的详细信息时"}, + AvoidWhen: []string{"按班级ID批量查询用 batch get-class-groups"}, + Examples: []string{ + "dws edu-group class-group list-by-cids --conversation-ids cid1,cid2,cid3 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "conversation-ids", Property: "input.conversationIds", Required: boolPtr(true)}, + }, + }, + }) + + // ════════════════════════════════════════════════════════════ + // batch 子命令组 — 批量操作 + // ════════════════════════════════════════════════════════════ + + batchCmd := &cobra.Command{Use: "batch", Short: "批量操作", RunE: groupRunE} + + batchCheckClassGroupCmd := &cobra.Command{ + Use: "check-student-group", + Short: "批量检查班级是否已创建师生群", + Long: `批量检查多个班级是否已创建师生群。返回班级ID与群会话ID(cid)的映射关系。仅限管理员角色调用。`, + Example: ` dws edu-group batch check-student-group --class-ids 12345,67890 + dws edu-group batch check-student-group --class-ids 12345,67890 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + raw, _ := cmd.Flags().GetString("class-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--class-ids 为必填参数") + } + classIDs, err := eduGroupParseIntCSV(raw) + if err != nil { + return fmt.Errorf("--class-ids 须为逗号分隔的整数列表: %w", err) + } + return callMCPToolOnServer("edu-group", "batch_check_class_group", map[string]any{ + "input": map[string]any{"classIds": classIDs}, + }) + }, + } + DeclareLeafMetadata(batchCheckClassGroupCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "batch_check_class_group", + CanonicalPath: "edu-group.batch_check_class_group", + CLIPath: "edu-group batch check-student-group", + PrimaryCLIPath: "edu-group batch check-student-group", + }, + Description: "批量检查班级是否已创建师生群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "batch_check_class_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "批量检查班级是否已创建师生群", + UseWhen: []string{"需要批量检查多个班级是否已创建师生群时"}, + AvoidWhen: []string{"单个班级检查用 student-group exists"}, + Examples: []string{ + "dws edu-group batch check-student-group --class-ids 12345,67890 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-ids", Property: "input.classIds", Required: boolPtr(true)}, + }, + }, + }) + + batchGetClassConversationsCmd := &cobra.Command{ + Use: "get-class-groups", + Short: "批量获取班级群信息", + Long: `批量获取多个班级的班级群会话信息。返回班级ID与群会话信息的映射关系。仅限管理员角色调用。`, + Example: ` dws edu-group batch get-class-groups --class-ids 12345,67890 + dws edu-group batch get-class-groups --class-ids 12345,67890 -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + raw, _ := cmd.Flags().GetString("class-ids") + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("--class-ids 为必填参数") + } + classIDs := eduGroupParseCSV(raw) + if len(classIDs) == 0 { + return fmt.Errorf("--class-ids 不能为空") + } + return callMCPToolOnServer("edu-group", "batch_get_class_conversations", map[string]any{ + "input": map[string]any{"classIds": classIDs}, + }) + }, + } + DeclareLeafMetadata(batchGetClassConversationsCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "read", Risk: "low", + Confirmation: "not_required", Idempotency: "idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "batch_get_class_conversations", + CanonicalPath: "edu-group.batch_get_class_conversations", + CLIPath: "edu-group batch get-class-groups", + PrimaryCLIPath: "edu-group batch get-class-groups", + }, + Description: "批量获取班级群信息", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "batch_get_class_conversations"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "批量获取班级群会话信息", + UseWhen: []string{"需要批量获取多个班级的班级群会话信息时"}, + AvoidWhen: []string{"按会话ID批量查询用 class-group list-by-cids"}, + Examples: []string{ + "dws edu-group batch get-class-groups --class-ids 12345,67890 --format json", + }, + }, + Parameters: []contract.ParamDecl{ + {Name: "class-ids", Property: "input.classIds", Required: boolPtr(true)}, + }, + }, + }) + + batchCreateClassGroupCmd := &cobra.Command{ + Use: "create-student-groups", + Short: "批量创建师生群", + Long: `为组织下所有已设置班主任但尚未创建师生群的班级批量创建师生群。 +小学和幼儿园学段的班级不会创建师生群。仅限管理员角色调用。`, + Example: ` dws edu-group batch create-student-groups + dws edu-group batch create-student-groups -f json`, + RunE: func(cmd *cobra.Command, args []string) error { + return callMCPToolOnServer("edu-group", "batch_create_class_group", map[string]any{ + "input": map[string]any{}, + }) + }, + } + DeclareLeafMetadata(batchCreateClassGroupCmd, LeafSpec{ + Safety: contract.SafetySpec{ + Effect: "write", Risk: "medium", + Confirmation: "not_required", Idempotency: "non_idempotent", + }, + Contract: LeafContract{ + Identity: contract.ToolIdentitySpec{ + ProductID: "edu-group", + Name: "batch_create_class_group", + CanonicalPath: "edu-group.batch_create_class_group", + CLIPath: "edu-group batch create-student-groups", + PrimaryCLIPath: "edu-group batch create-student-groups", + }, + Description: "批量创建师生群", + Interface: &contract.InterfaceSpec{ + Mode: "mcp", + Availability: "available", + Ref: &contract.InterfaceRefSpec{ProductID: "edu-group", RPCName: "batch_create_class_group"}, + }, + Selection: contract.SelectionSpec{ + AgentSummary: "为组织下符合条件的班级批量创建师生群", + UseWhen: []string{"需要为组织下所有已设置班主任但尚未创建师生群的班级批量创建师生群时"}, + AvoidWhen: []string{"单个班级创建用 student-group create"}, + Examples: []string{ + "dws edu-group batch create-student-groups --format json", + }, + }, + Parameters: []contract.ParamDecl{}, + }, + }) + + // ════════════════════════════════════════════════════════════ + // flags + 构建命令树 + // ════════════════════════════════════════════════════════════ + + // student-group flags + studentGroupInfoCmd.Flags().String("dept-id", "", "班级 ID(必填)") + studentGroupExistsCmd.Flags().String("dept-id", "", "班级 ID(必填)") + studentGroupMembersCmd.Flags().String("dept-id", "", "班级 ID(必填)") + studentGroupIsInCmd.Flags().String("dept-id", "", "班级 ID(必填)") + studentGroupConversationCmd.Flags().String("dept-id", "", "班级 ID(必填)") + studentGroupCreateCmd.Flags().String("dept-id", "", "班级 ID(必填)") + studentGroupDisbandCmd.Flags().String("dept-id", "", "班级 ID(必填)") + + // class-group flags + classGroupConversationIDCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classGroupConversationCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classGroupExistsCmd.Flags().String("dept-id", "", "班级 ID(必填)") + classGroupListByConversationIDsCmd.Flags().String("conversation-ids", "", "群会话 ID 列表,逗号分隔(必填)") + + // batch flags + batchCheckClassGroupCmd.Flags().String("class-ids", "", "班级 ID 列表,逗号分隔(必填)") + batchGetClassConversationsCmd.Flags().String("class-ids", "", "班级 ID 列表,逗号分隔(必填)") + + studentGroupCmd.AddCommand( + studentGroupInfoCmd, studentGroupExistsCmd, studentGroupMembersCmd, + studentGroupIsInCmd, studentGroupConversationCmd, + studentGroupCreateCmd, studentGroupDisbandCmd, + ) + classGroupCmd.AddCommand( + classGroupConversationIDCmd, classGroupConversationCmd, + classGroupExistsCmd, classGroupListByConversationIDsCmd, + ) + batchCmd.AddCommand(batchCheckClassGroupCmd, batchGetClassConversationsCmd, batchCreateClassGroupCmd) + + root.AddCommand(studentGroupCmd, classGroupCmd, batchCmd) + + return root +} + +// eduGroupRequiredIntFlag extracts a required integer flag, returning an error +// if the flag is empty or not a valid integer. +func eduGroupRequiredIntFlag(cmd *cobra.Command, name string) (int64, error) { + v, _ := cmd.Flags().GetString(name) + v = strings.TrimSpace(v) + if v == "" { + return 0, fmt.Errorf("--%s 为必填参数", name) + } + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return 0, fmt.Errorf("--%s 须为整数: %w", name, err) + } + return n, nil +} + +// eduGroupParseCSV splits a comma-separated string into trimmed non-empty values. +func eduGroupParseCSV(raw string) []string { + parts := strings.Split(raw, ",") + result := make([]string, 0, len(parts)) + for _, p := range parts { + v := strings.TrimSpace(p) + if v != "" { + result = append(result, v) + } + } + return result +} + +// eduGroupParseIntCSV splits a comma-separated string into int64 values. +func eduGroupParseIntCSV(raw string) ([]int64, error) { + parts := strings.Split(raw, ",") + result := make([]int64, 0, len(parts)) + for _, p := range parts { + v := strings.TrimSpace(p) + if v == "" { + continue + } + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid integer %q", v) + } + result = append(result, n) + } + return result, nil +} diff --git a/internal/helpers/edu_group_test.go b/internal/helpers/edu_group_test.go new file mode 100644 index 00000000..c8f92095 --- /dev/null +++ b/internal/helpers/edu_group_test.go @@ -0,0 +1,281 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package helpers + +import ( + "io" + "strings" + "testing" +) + +// withEduGroupCaller installs a dry-run capture caller so happy-path command +// execution exercises each RunE up to the callMCPToolOnServer dispatch without +// requiring a live MCP transport. +func withEduGroupCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{dryRun: true} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +func TestEduGroupHappyPaths(t *testing.T) { + cases := [][]string{ + {"student-group", "info", "--dept-id", "123"}, + {"student-group", "exists", "--dept-id", "123"}, + {"student-group", "members", "--dept-id", "123"}, + {"student-group", "is-in", "--dept-id", "123"}, + {"student-group", "conversation", "--dept-id", "123"}, + {"student-group", "create", "--dept-id", "123"}, + {"student-group", "disband", "--dept-id", "123"}, + {"class-group", "conversation-id", "--dept-id", "123"}, + {"class-group", "conversation", "--dept-id", "123"}, + {"class-group", "exists", "--dept-id", "123"}, + {"class-group", "list-by-cids", "--conversation-ids", "cid1,cid2"}, + {"batch", "check-student-group", "--class-ids", "1,2"}, + {"batch", "get-class-groups", "--class-ids", "1,2"}, + {"batch", "create-student-groups"}, + } + for _, args := range cases { + t.Run(strings.Join(args, " "), func(t *testing.T) { + withEduGroupCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs(args) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute(%v) = %v, want nil", args, err) + } + }) + } +} + +func TestEduGroupMissingRequiredFlags(t *testing.T) { + // Each dept-id command must reject an absent --dept-id, covering its own + // error branch as well as the shared eduGroupRequiredIntFlag empty case. + deptIDCommands := [][]string{ + {"student-group", "info"}, + {"student-group", "exists"}, + {"student-group", "members"}, + {"student-group", "is-in"}, + {"student-group", "conversation"}, + {"student-group", "create"}, + {"student-group", "disband"}, + {"class-group", "conversation-id"}, + {"class-group", "conversation"}, + {"class-group", "exists"}, + } + for _, args := range deptIDCommands { + t.Run(strings.Join(args, " "), func(t *testing.T) { + withEduGroupCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs(args) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "dept-id") { + t.Fatalf("Execute(%v) error = %v, want dept-id required", args, err) + } + }) + } +} + +func TestEduGroupFlagValidation(t *testing.T) { + t.Run("non-integer dept-id", func(t *testing.T) { + withEduGroupCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"student-group", "info", "--dept-id", "abc"}) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "整数") { + t.Fatalf("non-integer dept-id error = %v", err) + } + }) + + t.Run("list-by-cids missing conversation-ids", func(t *testing.T) { + withEduGroupCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"class-group", "list-by-cids"}) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "conversation-ids") { + t.Fatalf("missing conversation-ids error = %v", err) + } + }) + + t.Run("list-by-cids only separators", func(t *testing.T) { + withEduGroupCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"class-group", "list-by-cids", "--conversation-ids", " , , "}) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "conversation-ids") { + t.Fatalf("empty conversation-ids error = %v", err) + } + }) + + t.Run("batch check missing class-ids", func(t *testing.T) { + withEduGroupCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"batch", "check-student-group"}) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "class-ids") { + t.Fatalf("missing class-ids error = %v", err) + } + }) + + t.Run("batch check invalid integer class-ids", func(t *testing.T) { + withEduGroupCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"batch", "check-student-group", "--class-ids", "x"}) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "整数") { + t.Fatalf("invalid integer class-ids error = %v", err) + } + }) + + t.Run("batch get missing class-ids", func(t *testing.T) { + withEduGroupCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"batch", "get-class-groups"}) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "class-ids") { + t.Fatalf("missing class-ids error = %v", err) + } + }) + + t.Run("batch get only separators", func(t *testing.T) { + withEduGroupCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"batch", "get-class-groups", "--class-ids", " , , "}) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "class-ids") { + t.Fatalf("empty class-ids error = %v", err) + } + }) +} + +func TestEduGroupParseHelpers(t *testing.T) { + if got := eduGroupParseCSV(" a , , b "); len(got) != 2 || got[0] != "a" || got[1] != "b" { + t.Fatalf("eduGroupParseCSV = %#v", got) + } + ids, err := eduGroupParseIntCSV(" 1 , , 2 ") + if err != nil || len(ids) != 2 || ids[0] != 1 || ids[1] != 2 { + t.Fatalf("eduGroupParseIntCSV = %#v, err = %v", ids, err) + } + if _, err := eduGroupParseIntCSV("1,bad"); err == nil { + t.Fatalf("eduGroupParseIntCSV invalid = nil error") + } +} + +// withEduGroupDispatchCaller installs a non-dry-run capture caller so commands +// exercise the full dispatch path through deps.Caller.CallTool. +func withEduGroupDispatchCaller(t *testing.T) *recruitCaptureCaller { + t.Helper() + caller := &recruitCaptureCaller{} + InitDepsForTest(t, caller) + deps.Out.w = io.Discard + return caller +} + +func TestEduGroupDispatch(t *testing.T) { + t.Run("student-group info dispatches get_class_group_info", func(t *testing.T) { + caller := withEduGroupDispatchCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"student-group", "info", "--dept-id", "123"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute error = %v", err) + } + if caller.productID != "edu-group" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-group") + } + if caller.tool != "get_class_group_info" { + t.Fatalf("tool = %q, want %q", caller.tool, "get_class_group_info") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map", caller.args["input"]) + } + if input["deptId"] != int64(123) { + t.Fatalf("input[\"deptId\"] = %#v, want int64(123)", input["deptId"]) + } + }) + + t.Run("student-group create dispatches create_class_group", func(t *testing.T) { + caller := withEduGroupDispatchCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"student-group", "create", "--dept-id", "456"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute error = %v", err) + } + if caller.productID != "edu-group" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-group") + } + if caller.tool != "create_class_group" { + t.Fatalf("tool = %q, want %q", caller.tool, "create_class_group") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map", caller.args["input"]) + } + if input["deptId"] != int64(456) { + t.Fatalf("input[\"deptId\"] = %#v, want int64(456)", input["deptId"]) + } + }) + + t.Run("student-group disband dispatches disband_class_group", func(t *testing.T) { + caller := withEduGroupDispatchCaller(t) + cmd := newEduGroupCommand() + cmd.PersistentFlags().Bool("yes", false, "") + cmd.PersistentFlags().Bool("dry-run", false, "") + cmd.SetArgs([]string{"student-group", "disband", "--dept-id", "789", "--yes"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute error = %v", err) + } + if caller.productID != "edu-group" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-group") + } + if caller.tool != "disband_class_group" { + t.Fatalf("tool = %q, want %q", caller.tool, "disband_class_group") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map", caller.args["input"]) + } + if input["deptId"] != int64(789) { + t.Fatalf("input[\"deptId\"] = %#v, want int64(789)", input["deptId"]) + } + }) + + t.Run("class-group list-by-cids dispatches list_groups_by_conversation_ids", func(t *testing.T) { + caller := withEduGroupDispatchCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"class-group", "list-by-cids", "--conversation-ids", "cid1,cid2,cid3"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute error = %v", err) + } + if caller.productID != "edu-group" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-group") + } + if caller.tool != "list_groups_by_conversation_ids" { + t.Fatalf("tool = %q, want %q", caller.tool, "list_groups_by_conversation_ids") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map", caller.args["input"]) + } + cids, ok := input["conversationIds"].([]string) + if !ok || len(cids) != 3 || cids[0] != "cid1" || cids[1] != "cid2" || cids[2] != "cid3" { + t.Fatalf("input[\"conversationIds\"] = %#v, want [cid1 cid2 cid3]", input["conversationIds"]) + } + }) + + t.Run("batch check-student-group dispatches batch_check_class_group", func(t *testing.T) { + caller := withEduGroupDispatchCaller(t) + cmd := newEduGroupCommand() + cmd.SetArgs([]string{"batch", "check-student-group", "--class-ids", "100,200"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute error = %v", err) + } + if caller.productID != "edu-group" { + t.Fatalf("productID = %q, want %q", caller.productID, "edu-group") + } + if caller.tool != "batch_check_class_group" { + t.Fatalf("tool = %q, want %q", caller.tool, "batch_check_class_group") + } + input, ok := caller.args["input"].(map[string]any) + if !ok { + t.Fatalf("args[\"input\"] = %#v, want map", caller.args["input"]) + } + classIDs, ok := input["classIds"].([]int64) + if !ok || len(classIDs) != 2 || classIDs[0] != 100 || classIDs[1] != 200 { + t.Fatalf("input[\"classIds\"] = %#v, want [100 200]", input["classIds"]) + } + }) +} diff --git a/internal/helpers/register_edu.go b/internal/helpers/register_edu.go new file mode 100644 index 00000000..f90f710f --- /dev/null +++ b/internal/helpers/register_edu.go @@ -0,0 +1,28 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package helpers + +import "github.com/spf13/cobra" + +// 家校/高校系列(家校通讯录、家校群、家校应用、家庭群、高校通讯录)是开源库 +// 显式维护的公开命令,不依赖生成式产品注册表(register_products.go)。 +// 其 MCP server 端点由运行时宿主按 serverID 解析。 +func init() { + eduCommands := []struct { + name string + buildFn func() *cobra.Command + }{ + {"edu-contact", newEduContactCommand}, + {"edu-group", newEduGroupCommand}, + {"edu-app", newEduAppCommand}, + {"edu-familygroup", newEduFamilyGroupCommand}, + {"college-contact", newCollegeContactCommand}, + } + for _, c := range eduCommands { + c := c + RegisterPublic(func() Handler { + return wukongHandler{name: c.name, buildFn: c.buildFn} + }) + } +} diff --git a/pkg/edition/default.go b/pkg/edition/default.go index 04e92642..4ca77c9b 100644 --- a/pkg/edition/default.go +++ b/pkg/edition/default.go @@ -66,6 +66,36 @@ func openSupplementServers() []ServerInfo { Endpoint: "https://mcp-gw.dingtalk.com/server/f69b54ada16c57b603c0e5e1c36f464ba73dcee28d64bb701ff2682c259c0cff", Prefixes: []string{"recruit", "job"}, }, + { + ID: "edu-contact", + Name: "家校通讯录", + Endpoint: "https://mcp-gw.dingtalk.com/server/d24759cc1c6e424e2de4e9901ea0202136e6707991ffc33b473878ec1cd688a2", + Prefixes: []string{"edu-contact", "edu"}, + }, + { + ID: "edu-group", + Name: "家校群", + Endpoint: "https://mcp-gw.dingtalk.com/server/14624b71ac9bc1a03b1b60e5b0403a48b346361f86cc9f555f98f89eb383875a", + Prefixes: []string{"edu-group"}, + }, + { + ID: "edu-app", + Name: "家校应用", + Endpoint: "https://mcp-gw.dingtalk.com/server/905eef591d16e2a1d95b235bcc780ce2fadb6ebe1f25648a279f8a2d97907a1e", + Prefixes: []string{"edu-app"}, + }, + { + ID: "edu-familygroup", + Name: "家庭群", + Endpoint: "https://mcp-gw.dingtalk.com/server/1cd153fb5296df340507c3e9ee20c938f9feeefec3147e9cc32317032f1a2944", + Prefixes: []string{"edu-familygroup"}, + }, + { + ID: "college-contact", + Name: "高校通讯录", + Endpoint: "https://mcp-gw.dingtalk.com/server/45bb310b388b9c39e0b80e08236782880cb51ad536e1292f9a40933c428a7474", + Prefixes: []string{"college-contact"}, + }, } } diff --git a/skills/content-qa/mono-multi-coverage.yaml b/skills/content-qa/mono-multi-coverage.yaml index 8dc8895a..2e471ad3 100644 --- a/skills/content-qa/mono-multi-coverage.yaml +++ b/skills/content-qa/mono-multi-coverage.yaml @@ -112,6 +112,26 @@ coverage: multi_skill: dingtalk-misc multi_refs: - references/recruit.md + - mono: edu-app + multi_skill: dingtalk-misc + multi_refs: + - references/edu-app.md + - mono: edu-contact + multi_skill: dingtalk-misc + multi_refs: + - references/edu-contact.md + - mono: edu-group + multi_skill: dingtalk-misc + multi_refs: + - references/edu-group.md + - mono: college-contact + multi_skill: dingtalk-misc + multi_refs: + - references/college-contact.md + - mono: edu-familygroup + multi_skill: dingtalk-misc + multi_refs: + - references/edu-familygroup.md omit_coverage: - mono: simple diff --git a/skills/mono/SKILL.md b/skills/mono/SKILL.md index 0ad95f22..7670952a 100644 --- a/skills/mono/SKILL.md +++ b/skills/mono/SKILL.md @@ -97,6 +97,11 @@ cli_version: ">=1.0.15" | `wiki` | 知识库:空间创建/详情/列表/搜索 + 成员管理 + 知识库动态查询 | [wiki.md](./references/products/wiki.md) | | `whiteboard` | 文档内嵌白板:读取 OpenNodes、追加节点、整页重建 | [whiteboard.md](./references/products/whiteboard.md) | | `recruit` | 钉钉招聘:查询职位列表、获取职位详情、创建职位 | [recruit.md](./references/products/recruit.md) | +| `edu-app` | 家校应用(教育版):家校任务、班级消息摘要等教育场景应用能力 | [edu-app.md](./references/products/edu-app.md) | +| `edu-contact` | 家校通讯录(教育版):学校组织架构、班级列表等教育场景通讯录能力 | [edu-contact.md](./references/products/edu-contact.md) | +| `edu-group` | 家校群(师生群):班级群/师生群的查询、创建、解散 | [edu-group.md](./references/products/edu-group.md) | +| `edu-familygroup` | 家庭群:家庭群查询/创建、孩子管理、家长邀请、学生应用权限控制 | [edu-familygroup.md](./references/products/edu-familygroup.md) | +| `college-contact` | 高校通讯录:高校组织架构/院系部门管理(查询/创建/更新/删除)/师生员工管理(查询/添加/移除/变更类型与部门/激活短信)/通讯录搜索/概览统计/升级 | [college-contact.md](./references/products/college-contact.md) | | `event` | 个人 IM/OA 事件:监听消息、群生命周期、审批任务与审批实例事件,NDJSON 输出(实时驱动 Agent)| [event.md](./references/products/event.md) | ## 意图判断决策树 @@ -166,6 +171,8 @@ cli_version: ">=1.0.15" | `oa` | `approval reject` | 拒绝待审批(需加明确理由) | | `todo` | `task delete` | 删除待办 | | `minutes` | `replace-text` | 全文批量替换转写与摘要 | +| `college-contact` | `dept delete` | 删除高校部门,不可恢复 | +| `college-contact` | `employee remove` | 移除高校员工,不可恢复 | ### 确认流程 ``` diff --git a/skills/mono/references/products/college-contact.md b/skills/mono/references/products/college-contact.md new file mode 100644 index 00000000..45622934 --- /dev/null +++ b/skills/mono/references/products/college-contact.md @@ -0,0 +1,135 @@ +# 高校通讯录 (college-contact) 命令参考 + +## 命令总览 + +### dept (部门管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `dept get-standard-structure` | 查询高校标准架构信息(组织 ID/行政架构部门 ID 映射) | 无 | +| `dept get-detail` | 查询部门详情 | `--dept-id` | +| `dept get-chain` | 查询部门链(根节点到当前部门) | `--dept-id` | +| `dept search` | 按关键词搜索通讯录(人员/部门/角色) | `--dept-id`, `--keyword` | +| `dept create` | 创建部门 | `--super-id`, `--stru-dept-id`, `--name`, `--dept-type`, `--create-dept-group` | +| `dept update` | 更新部门 | `--dept-id`, `--dept-type` | +| `dept delete` | 删除部门 ⚠️ | `--dept-id` | +| `dept batch-update-type` | 批量修改部门类型 | `--dept-ids`(逗号分隔), `--target-dept-type` | +| `dept overview` | 查询高校概览统计 | 无 | + +### employee (员工管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `employee get-detail` | 查询员工详情 | `--staff-id` | +| `employee add` | 添加员工(返回成功/失败数量及邮箱初始密码) | `--emp-type`, `--main-dept-id`, `--exclusive-account` | +| `employee remove` | 移除员工 ⚠️ | `--staff-ids`(逗号分隔) | +| `employee change-type` | 变更员工类型 | `--staff-id`, `--emp-type` | +| `employee change-dept` | 变更员工部门 | `--staff-id`, `--target-dept-id` | +| `employee send-active-sms` | 发送激活短信 | `--dept-id` | +| `employee list-employees` | 查询部门员工列表 | `--dept-id` | +| `employee list-unaccepted` | 查询未接受邀请的员工列表 | `--dept-id` | +| `employee list-unactive` | 查询未激活的员工列表 | `--dept-id` | +| `employee upgrade-status` | 查询高校通讯录升级状态 | 无 | +| `employee start-upgrade` | 启动高校通讯录升级 | 无 | + +### alumni (校友管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `alumni get-dept-tree` | 查询校友部门树 | `--alumni-dept-id` | +| `alumni get-info` | 查询校友部门详情 | `--alumni-dept-id` | +| `alumni list` | 查询校友列表 | `--alumni-dept-id`, `--order-field`, `--ordering` | +| `alumni query` | 查询单个校友详情 | `--staff-id` | +| `alumni search` | 搜索校友 | `--keyword` | +| `alumni list-unaccepted` | 查询未接受邀请的校友列表 | `--alumni-dept-id` | +| `alumni get-group` | 查询校友群信息 | `--alumni-dept-id` | +| `alumni create-dept` | 创建校友子部门 | `--alumni-dept-id`, `--dept-name` | +| `alumni update-dept` | 更新校友部门名称 | `--alumni-dept-id`, `--dept-name` | +| `alumni delete-dept` | 删除校友部门 ⚠️ | `--alumni-dept-id` | +| `alumni update-managers` | 设置校友部门负责人 | `--alumni-dept-id`, `--admin-user-ids`(逗号分隔) | +| `alumni add-alumnus` | 添加校友 | `--name`, `--mobile`, `--dept-ids`(逗号分隔) | +| `alumni update-alumnus` | 更新校友信息 | `--staff-id`, `--name`, `--dept-ids`(逗号分隔) | +| `alumni remove-alumnus` | 删除校友 ⚠️ | `--staff-id`, `--alumni-dept-id` | +| `alumni cancel-invite` | 取消校友邀请 ⚠️ | `--alumni-dept-id`, `--staff-ids`(逗号分隔) | +| `alumni create-group` | 创建校友群 | `--alumni-dept-id` | +| `alumni disband-group` | 解散校友群 ⚠️ | `--alumni-dept-id` | +| `alumni get-alumni-org-from-graduate` | 查询毕业生校友组织 | 无入参 | +| `alumni create-alumni-org` | 创建校友会组织 | `--org-name` | +| `alumni add-alumni-org-main-admins` | 添加校友会组织管理员 | `--admin-user-ids`(逗号分隔) | + +### graduate (毕业年级管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `graduate query-graduate-years` | 查询毕业年级列表 | 无入参 | +| `graduate query-graduate-depts` | 查询待毕业部门列表 | `--dept-id`, `--graduate-year`(可选) | +| `graduate query-graduate-sub-depts` | 查询毕业子部门列表 | `--dept-id` | +| `graduate query-page-graduate-users` | 分页查询待毕业学生列表 | `--dept-id`, `--graduate-year`/`--offset`/`--size`(可选) | +| `graduate get-task-result` | 查询异步任务执行结果 | `--request-no`, `--type`(可选) | +| `graduate get-alumni-org` | 查询校友组织信息 | 无入参 | +| `graduate query-restore-sub-depts` | 查询可恢复子部门列表 | `--dept-id` | +| `graduate query-dept-deleted-emps` | 查询部门可恢复员工列表 | `--dept-id`, `--offset`/`--size`(可选) | +| `graduate search-graduate` | 搜索毕业部门与员工 | `--keyword`, `--offset`/`--size`(可选) | +| `graduate commit-graduate` | 提交毕业 ⚠️ | `--graduate-dept-ids`(逗号分隔), `--graduate-year`, `--request-no`(可选) | +| `graduate all-graduate` | 全部毕业 ⚠️ | `--graduate-year`, `--request-no`(可选) | +| `graduate batch-graduate` | 批量毕业 ⚠️ | `--dept-id`, `--staff-ids`(逗号分隔) | +| `graduate delete-and-graduate` | 删除并毕业 ⚠️ | `--dept-id`, `--staff-ids`(逗号分隔) | +| `graduate batch-delete-pending` | 批量删除待毕业学生 ⚠️ | `--dept-id`, `--staff-ids`(逗号分隔) | +| `graduate batch-update-pending` | 批量更新待毕业学生 ⚠️ | `--dept-id`, `--staff-ids`(逗号分隔), `--graduate-year` | +| `graduate commit-restore` | 提交恢复 ⚠️ | `--graduate-dept-ids`(逗号分隔), `--request-no`(可选) | + +### group (规则管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `group query-group-rule` | 查询规则 | `--name`(可选), `--offset`(可选), `--size`(可选) | +| `group get-group-rule-schedule` | 查询规则调度 | 无参数 | +| `group query-preview-data` | 查询规则预览数据 | `--offset`(可选), `--size`(可选) | +| `group create-group-rule` | 创建规则 | `--name`, `--tag-code`, `--dept-type`, `--auto-admin`(可选,true/false) | +| `group delete-group-rule` | 删除规则 ⚠️ | `--rule-id` | +| `group enable-group-rule` | 启用规则 | `--rule-id` | +| `group disable-group-rule` | 停用规则 | `--rule-id` | +| `group set-group-rule-schedule` | 设置规则调度 | `--cron`(可选) | +| `group execute-group-rule` | 立即执行规则 ⚠️ | 无参数 | + +## 常用参数说明 + +- `--emp-type`:员工类型,取值 `college_student`(学生)/ `college_teacher`(教职工) +- `--dept-type`:部门类型(如 `contact_grade_dept` 年级 / `contact_class_dept` 班级 / `contact_major_dept` 专业) +- `--staff-id` 单个员工 staffId;`--staff-ids` 为逗号分隔的批量列表 +- 列表类命令支持 `--offset` / `--size` 分页与 `--order-field` / `--ordering`(asc/desc) 排序 +- `--exclusive-account`、`--create-dept-group`、`--send-active-sms` 为布尔参数(true/false) + +## 意图判断 + +用户说"高校架构/组织架构/学院/系/部门" → dept 子命令 +用户说"搜人/找某某老师/找某某同学" → `dept search` +用户说"师生/教职工/学生/员工/辅导员" → employee 子命令 +用户说"激活/邀请/未激活账号" → `employee list-unactive` / `list-unaccepted` / `send-active-sms` +用户说"通讯录升级" → `employee upgrade-status` / `start-upgrade` +用户说"校友/校友会/校友部门/添加校友" → alumni 子命令 +用户说"毕业年级/毕业年份/待毕业学生/毕业操作" → graduate 子命令 +用户说"群规则/建群规则/自动建群" → group 子命令 + +## 核心工作流 + +1. 查标准架构 → `dept get-standard-structure`(提取 deptId) +2. 查看部门详情 → `dept get-detail --dept-id ` +3. 查看部门员工 → `employee list-employees --dept-id ` +4. 查看员工详情 → `employee get-detail --staff-id ` + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `dept get-standard-structure` | `deptId` | dept/employee 子命令的 --dept-id | +| `employee list-employees` | `staffId` | get-detail/change-type/change-dept 的 --staff-id、remove 的 --staff-ids | +| `dept create` | `deptId` | update/delete 的 --dept-id | +| `alumni get-dept-tree` | `alumniDeptId` | alumni 子命令的 --alumni-dept-id | +| `alumni list` | `staffId` | update-alumnus/remove-alumnus 的 --staff-id | +| `graduate query-graduate-depts` | `deptId` | graduate 子命令的 --dept-id | +| `group query-group-rule` | `ruleId` | delete/enable/disable-group-rule 的 --rule-id | + +## 危险操作 + +- `dept delete`、`employee remove`、`alumni delete-dept`、`alumni remove-alumnus`、`alumni cancel-invite`、`alumni disband-group`、`graduate commit-graduate`、`graduate all-graduate`、`graduate batch-graduate`、`graduate delete-and-graduate`、`graduate batch-delete-pending`、`graduate batch-update-pending`、`graduate commit-restore`、`group delete-group-rule`、`group execute-group-rule` 不可逆:非 --dry-run 预览时必须显式传入 --yes 才会真实执行,未传 --yes 会直接拒绝。执行前必须向用户展示操作摘要并获得明确同意,确认后再追加 --yes。 diff --git a/skills/mono/references/products/edu-app.md b/skills/mono/references/products/edu-app.md new file mode 100644 index 00000000..22349119 --- /dev/null +++ b/skills/mono/references/products/edu-app.md @@ -0,0 +1,109 @@ +# 家校应用 (edu-app) 命令参考 + +## 命令总览 + +### message (消息管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `message summary-list` | 查询消息摘要列表 | `--class-id`, `--cid`, `--target-role`, `--status` | + +> `--target-role`: guardian(家长) / student(学生) +> `--status`: 0(未处理) / 1(已处理) + +### task (任务管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `task publish-list` | 查询发布的家校任务列表(仅老师) | 无(均可选) | +| `task all-list` | 查询全部家校任务列表(仅老师) | `--biz-id`(班级ID) | +| `task student-list` | 查询学生待办任务列表 | `--students`(JSON数组) | + +> `--task-sources` 可选值(逗号分隔): EDU_HOMEWORK, EDU_CARD, EDU_NOTICE, EDU_SR, EDU_DIPLOMA + +### report (成绩单管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `report get` | 获取成绩单列表 | `--ids`(逗号分隔整数) | +| `report by-teacher` | 查询老师创建的成绩单 | 无(均可选) | +| `report by-class` | 查询班级学生成绩明细 | `--report-id`, `--class-id` | +| `report by-student-list` | 查询学生收到的成绩单 | `--class-id`, `--student-id` | +| `report by-student-detail` | 查询学生成绩明细 | `--report-id`, `--student-id`, `--class-id` | + +### notice (通知管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `notice confirm` | 确认收到通知 | `--notice-id`, `--student-id` | + +### circle (班级圈) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `circle posts` | 查询学生班级圈动态 | `--class-id`, `--student-id`, `--target-role` | + +> `--target-role`: guardian(家长视角) / student(学生视角) +> 返回动态的文字内容、图片URL列表、发布者姓名、发布时间、评论数、点赞数等。 + +### card (打卡管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `card update` | 修改打卡任务的标题或内容(仅老师且为创建者) | `--card-id`, `--identifier`, (`--title` 或 `--content` 至少一个) | +| `card end` | 提前结束打卡任务(仅老师且为创建者) | `--card-id` | +| `card list` | 查询孩子/本人打卡列表(含进行中与已完结) | `--status` | +| `card user-statistic` | 查询班级已完成/未完成人员(仅老师/班主任) | `--card-id`, `--task-code`, `--class-id` | +| `card finish-info` | 查询打卡详情及完成进度 | `--card-id`, `--card-biz-id` | + +> `--status`: FINISH(已完结) / UNFINISH(进行中) +> `--identifier` 建议格式 `orgId-staffId-UUID`,用于幂等去重 +> `card finish-info --target-role`: teacher / headmaster / guardian / student,未传时按 uid 真实身份自动推断 +> `card finish-info` 当 `targetRole=guardian` 时,可传 `--student-id` 指定查看某个孩子的进度 + +## 意图判断 + +用户说"消息/消息摘要" → message summary-list +用户说"作业/任务/打卡/家校任务" → task 子命令 +用户说"成绩/成绩单" → report 子命令 +用户说"通知/确认通知" → notice confirm +用户说"班级圈/成长记录/学生动态" → circle posts +用户说"打卡/打卡任务/打卡完成情况/卡片完成情况" → card 子命令 + +关键区分: task(家校任务/作业) vs report(成绩单) +关键区分: circle(班级圈动态/成长记录) vs message(AI消息总结) + +## 核心工作流 + +### 老师场景 +1. 查看发布的任务 → `task publish-list --need-statistic -f json` +2. 查看某班全部任务 → `task all-list --biz-id ` +3. 查看成绩单 → `report by-teacher --status 1` +4. 查看班级成绩明细 → `report by-class --report-id --class-id ` +5. 修改打卡标题/内容 → `card update --card-id --identifier --title "新标题"` +6. 提前结束打卡 → `card end --card-id ` +7. 查看某班打卡完成情况 → `card user-statistic --card-id --task-code --class-id --finish` +8. 查看某班未打卡人员 → `card user-statistic --card-id --task-code --class-id ` +9. 查看某打卡完成进度 → `card finish-info --card-id --card-biz-id ` + +### 家长场景 +1. 查看孩子待办 → `task student-list --students '[{"userId":"","bizId":""}]'` +2. 确认通知 → `notice confirm --notice-id --student-id ` +3. 查看孩子班级圈动态 → `circle posts --class-id --student-id --target-role guardian` +4. 查看孩子进行中打卡 → `card list --status UNFINISH` +5. 查看孩子某打卡进度 → `card finish-info --card-id --card-biz-id ` + +### 学生场景 +1. 查看自己的班级圈动态 → `circle posts --class-id --student-id --target-role student` + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `edu-contact school class-list` | `deptId` | task all-list 的 --biz-id | +| `edu-contact class students` | `userId` | task student-list 的 students.userId | +| `edu-group class-group conversation-id` | `conversationId` | message summary-list 的 --cid | +| `report by-teacher` | `schoolReportId` | report get/by-class/by-student-detail 的 --report-id | +| `edu-contact family children` | `studentUserId`, `classId` | circle posts 的 --student-id, --class-id | +| `task publish-list` | `cardId` | card update/end/finish-info` 的 --card-id | +| `task publish-list` | `taskCode` | card user-statistic 的 --task-code | diff --git a/skills/mono/references/products/edu-contact.md b/skills/mono/references/products/edu-contact.md new file mode 100644 index 00000000..91291454 --- /dev/null +++ b/skills/mono/references/products/edu-contact.md @@ -0,0 +1,74 @@ +# 家校通讯录 (edu-contact) 命令参考 + +## 命令总览 + +### school (学校/组织管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `school roles` | 查询用户在组织内的身份 | 无 | +| `school structure` | 查询学校组织架构 | 无 | +| `school periods` | 查询学校学段信息 | 无 | +| `school type` | 查询学校组织类型 | 无 | +| `school stats` | 查询学校统计数据 | `--statistics-type`(可选) | +| `school class-list` | 查询学校所有班级列表 | 无 | + +### class (班级管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `class detail` | 查询班级详情 | `--dept-id` | +| `class students` | 查询班级学生信息 | `--dept-id` | +| `class teachers` | 查询班级老师列表 | `--dept-id` | +| `class same-name` | 查询班级内同名学生 | `--dept-id` | +| `class user-role` | 查询用户在班级内的角色 | `--dept-id` | +| `class search-by-name` | 根据姓名查询班级 | `--query-type`, `--name` | +| `class headmaster` | 根据班级名查询班主任 | `--class-name` | +| `class search-by-teacher` | 根据老师姓名查询班级 | `--name` | +| `class update-student` | 更新学生信息 | `--class-id`, `--student-user-id` | +| `class add-student` | 添加学生到班级 | `--dept-id`, `--student-name` | +| `class modify-student-info` | 修改学生信息 | `--dept-id`, `--target-user-id` | +| `class delete-teacher` | 删除班级教师 ⚠️ | `--class-id`, `--teacher-user-id` | +| `class update-info` | 更新班级信息 | `--class-id` | +| `class update-student-number` | 修改学生学号 | `--class-id`, `--student-user-id`, `--student-number` | +| `class add-unofficial-student` | 添加非行政班学生 | `--dept-id`, `--student-staff-ids` | +| `class delete-students` | 批量删除学生 ⚠️ | `--dept-id`, `--student-user-ids` | +| `class update-student-mobile` | 修改学生手机号 | `--dept-id`, `--student-user-id`, `--mobile` | +| `class move-student` | 学生移班 | `--student-user-ids`, `--origin-class-id`, `--target-class-id` | +| `class add-teachers` | 批量添加班级教师 | `--dept-id`, `--teacher-user-ids` | + +### family (家庭关系查询) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `family children` | 查询家长的孩子信息 | 无 | +| `family parents` | 查询学生的家长信息 | 无 | + +### teacher (教师管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `teacher classes` | 查询老师管理的班级列表 | 无 | +| `teacher update-course` | 更新教师任教科目 | `--teacher-class-infos`(JSON数组) | + +## 意图判断 + +用户说"学校/组织/学段/组织架构" → school 子命令 +用户说"班级/学生/教师/班主任" → class 子命令 +用户说"家长/孩子/家庭关系" → family 子命令 +用户说"任教/科目" → teacher update-course + +## 核心工作流 + +1. 查询角色 → `school roles` +2. 查看班级列表 → `school class-list`(提取 deptId) +3. 查看班级详情 → `class detail --dept-id ` +4. 查看学生列表 → `class students --dept-id ` + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `school class-list` | `deptId` | class 子命令的 --dept-id | +| `class students` | `userId` | update-student/delete-students 的 --student-user-id | +| `class teachers` | `userId` | delete-teacher 的 --teacher-user-id | diff --git a/skills/mono/references/products/edu-familygroup.md b/skills/mono/references/products/edu-familygroup.md new file mode 100644 index 00000000..82ec8631 --- /dev/null +++ b/skills/mono/references/products/edu-familygroup.md @@ -0,0 +1,68 @@ +# 家庭群 (edu-familygroup) 命令参考 + +## 命令总览 + +### group (家庭群查询) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `group check-exists` | 检查家庭群是否存在 | `--uid`, `--group-name` | +| `group list-children` | 查询家长绑定的孩子列表 | `--uid` | + +### manage (家庭群管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `manage create` | 创建家庭群 | `--uid`, `--children` | +| `manage invite-parent` | 短信邀请家长加入家庭群 | `--org-id`, `--uid`, `--mobile` | +| `manage add-child` | 为家庭群添加孩子 | `--org-id`, `--uid`, `--name`, (`--mobile` 或 `--students`) | +| `manage toggle-app` | 开启或关闭学生应用权限 | `--org-id`, `--uid`, `--child-staff-id`, `--app-type`, `--open` | + +## 意图判断 + +用户说"家庭群存在/有没有家庭群" → group check-exists +用户说"孩子列表/我的孩子" → group list-children +用户说"创建家庭/建群" → manage create +用户说"邀请家长/拉家长入群" → manage invite-parent +用户说"添加孩子/加娃" → manage add-child +用户说"应用权限/小天地/学习视频" → manage toggle-app + +## 核心工作流 + +1. 检查家庭群是否存在 → `group check-exists --uid --group-name ` +2. 如不存在,创建家庭群 → `manage create --uid --children ''` +3. 查看已绑定孩子 → `group list-children --uid ` +4. 邀请其他家长 → `manage invite-parent --org-id --uid --mobile ` +5. 添加孩子 → `manage add-child --org-id --uid --name --mobile ` +6. 管理应用权限 → `manage toggle-app --org-id --uid --child-staff-id --app-type XIAOTIANDI --open true` + +## 参数说明 + +### manage create --children 格式 + +```json +[{"name":"小明","students":[{"corpId":"dingxxx","staffId":"stu001"}]}] +``` + +每个孩子必填 name + students 数组(含 corpId、staffId),可选 birthday/gender/nick/avatar/period/grade/mobile。 + +### manage add-child --students 格式 + +```json +[{"schoolOrgId":111,"studentStaffId":"stu001"}] +``` + +每项必填 schoolOrgId(整数)+ studentStaffId(字符串)。 + +### manage toggle-app --app-type 可选值 + +- `XIAOTIANDI`:小天地(学生圈) +- `LEARNING_VIDEO`:学习视频 + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `manage create` | `orgId`, `cid` | invite-parent / add-child 的 --org-id | +| `group list-children` | 孩子 staffId | toggle-app 的 --child-staff-id | +| `edu-contact family list` | 家长 uid | 所有 edu-familygroup 命令的 --uid | diff --git a/skills/mono/references/products/edu-group.md b/skills/mono/references/products/edu-group.md new file mode 100644 index 00000000..794d24ca --- /dev/null +++ b/skills/mono/references/products/edu-group.md @@ -0,0 +1,55 @@ +# 家校群 (edu-group) 命令参考 + +## 命令总览 + +### student-group (师生群管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `student-group info` | 查询班级师生群信息 | `--dept-id` | +| `student-group exists` | 检查是否已创建师生群 | `--dept-id` | +| `student-group members` | 查询师生群成员列表 | `--dept-id` | +| `student-group is-in` | 判断用户是否在师生群中 | `--dept-id` | +| `student-group conversation` | 查询班级群会话详情 | `--dept-id` | +| `student-group create` | 创建班级师生群 | `--dept-id` | +| `student-group disband` | 解散班级师生群 ⚠️ | `--dept-id` | + +### class-group (班级群会话管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `class-group conversation-id` | 获取班级群会话ID | `--dept-id` | +| `class-group conversation` | 获取班级群完整会话信息 | `--dept-id` | +| `class-group exists` | 检查班级群是否存在 | `--dept-id` | +| `class-group list-by-cids` | 根据会话ID列表批量查询 | `--conversation-ids` | + +### batch (批量操作) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `batch check-student-group` | 批量检查是否已创建师生群 | `--class-ids` | +| `batch get-class-groups` | 批量获取班级群信息 | `--class-ids` | +| `batch create-student-groups` | 批量创建师生群 | 无 | + +## 意图判断 + +用户说"师生群/学生群" → student-group 子命令 +用户说"班级群/群会话" → class-group 子命令 +用户说"批量/一键操作" → batch 子命令 + +关键区分: student-group(师生群) vs class-group(班级群会话管理) + +## 核心工作流 + +1. 检查群是否存在 → `student-group exists --dept-id ` +2. 如不存在,创建 → `student-group create --dept-id ` +3. 查看成员 → `student-group members --dept-id ` +4. 获取会话ID → `class-group conversation-id --dept-id ` + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `edu-contact school class-list` | `deptId` | 所有 edu-group 命令的 --dept-id | +| `class-group conversation-id` | `conversationId` | edu-app 消息命令的 --cid | +| `batch check-student-group` | 未创建群的班级 | batch create-student-groups | diff --git a/skills/multi/dingtalk-misc/SKILL.md b/skills/multi/dingtalk-misc/SKILL.md index d4eed650..a1f697d5 100644 --- a/skills/multi/dingtalk-misc/SKILL.md +++ b/skills/multi/dingtalk-misc/SKILL.md @@ -1,6 +1,6 @@ --- name: dingtalk-misc -description: 长尾产品集合技能,覆盖低频钉钉产品:OA审批查询与处理/考勤/直播/DING紧急消息/开放平台应用管理/Agoal目标管理/日志日报周报/电子表格/开放平台文档搜索/文档内嵌白板/钉钉招聘/DWS技能市场安装/组织大脑Hrbrain/原生Markdown/PAT行为授权/多组织profile。Use when 用户提到上述任一产品,或查待审批/同意拒绝转交撤销审批/打卡/排班/OKR/日报周报/单元格读写/白板节点读写/招聘职位/JD/创建职位/搜索安装技能/开发者后台应用/人才池/员工档案/职业历程/绩效/原生.md文件/PAT授权/切换组织/跨组织/profile 等相关操作。未来审批任务或实例变化的实时监听不属于本 skill,应使用 dingtalk-event。命中后由本 skill 的「产品索引表」定位具体子产品和命令前缀,再按对应子产品说明执行。 +description: 长尾产品集合技能,覆盖低频钉钉产品:OA审批查询与处理/考勤/直播/DING紧急消息/开放平台应用管理/Agoal目标管理/日志日报周报/电子表格/开放平台文档搜索/文档内嵌白板/钉钉招聘/DWS技能市场安装/组织大脑Hrbrain/原生Markdown/PAT行为授权/多组织profile/家校应用/家校通讯录/家校群/高校通讯录。Use when 用户提到上述任一产品,或查待审批/同意拒绝转交撤销审批/打卡/排班/OKR/日报周报/单元格读写/白板节点读写/招聘职位/JD/创建职位/搜索安装技能/开发者后台应用/人才池/员工档案/职业历程/绩效/原生.md文件/PAT授权/切换组织/跨组织/profile/家校任务/班级消息摘要/学校组织架构/班级列表/师生群/班级群/高校院系部门/师生员工管理 等相关操作。未来审批任务或实例变化的实时监听不属于本 skill,应使用 dingtalk-event。命中后由本 skill 的「产品索引表」定位具体子产品和命令前缀,再按对应子产品说明执行。 metadata: cli_version: ">=0.2.14" category: product @@ -37,6 +37,11 @@ metadata: | 原生 Markdown / `.md` 原文 / 覆盖 Markdown / 局部替换 Markdown | 原生 `.md` 文件读取、创建、全量覆盖与局部替换 | `dws markdown` | [markdown.md](references/markdown.md) | | PAT 授权 / 行为权限 / scope 授权 / 一次性授权 / 会话授权 / 永久授权 / 授权浏览器策略 | PAT 行为授权与本地浏览器策略 | `dws pat` | [pat.md](references/pat.md) | | 切换组织 / 换组织 / 跨组织 / 多组织 / profile / 看登录了哪些组织 | 多组织 / profile 管理与跨组织取数 | `dws profile` / `dws auth` / `--profile` | [profile.md](references/profile.md) | +| 家校应用 / 班级消息摘要 / 家校任务(教育场景) | 家校应用(教育版) | `dws edu-app` | [edu-app.md](references/edu-app.md) | +| 家校通讯录 / 学校组织架构 / 班级列表(教育场景) | 家校通讯录(教育版) | `dws edu-contact` | [edu-contact.md](references/edu-contact.md) | +| 家校群 / 师生群 / 班级群(教育场景) | 家校群(师生群)查询、创建、解散 | `dws edu-group` | [edu-group.md](references/edu-group.md) | +| 家庭群 / 家长邀请 / 孩子管理 / 学生应用权限(教育场景) | 家庭群查询/创建、孩子管理、家长邀请 | `dws edu-familygroup` | [edu-familygroup.md](references/edu-familygroup.md) | +| 高校通讯录 / 高校组织架构 / 院系部门 / 师生员工管理(高校场景) | 高校通讯录:部门与师生员工管理、搜索、统计、升级 | `dws college-contact` | [college-contact.md](references/college-contact.md) | | 宜搭 / AI应用脚本 / 财务辅助脚本(未产品化) | **无**稳定命令面;仅仓库内辅助脚本 | (非默认路由) | [unsupported-scripts.md](references/unsupported-scripts.md) | ## 说明 diff --git a/skills/multi/dingtalk-misc/references/college-contact.md b/skills/multi/dingtalk-misc/references/college-contact.md new file mode 100644 index 00000000..45622934 --- /dev/null +++ b/skills/multi/dingtalk-misc/references/college-contact.md @@ -0,0 +1,135 @@ +# 高校通讯录 (college-contact) 命令参考 + +## 命令总览 + +### dept (部门管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `dept get-standard-structure` | 查询高校标准架构信息(组织 ID/行政架构部门 ID 映射) | 无 | +| `dept get-detail` | 查询部门详情 | `--dept-id` | +| `dept get-chain` | 查询部门链(根节点到当前部门) | `--dept-id` | +| `dept search` | 按关键词搜索通讯录(人员/部门/角色) | `--dept-id`, `--keyword` | +| `dept create` | 创建部门 | `--super-id`, `--stru-dept-id`, `--name`, `--dept-type`, `--create-dept-group` | +| `dept update` | 更新部门 | `--dept-id`, `--dept-type` | +| `dept delete` | 删除部门 ⚠️ | `--dept-id` | +| `dept batch-update-type` | 批量修改部门类型 | `--dept-ids`(逗号分隔), `--target-dept-type` | +| `dept overview` | 查询高校概览统计 | 无 | + +### employee (员工管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `employee get-detail` | 查询员工详情 | `--staff-id` | +| `employee add` | 添加员工(返回成功/失败数量及邮箱初始密码) | `--emp-type`, `--main-dept-id`, `--exclusive-account` | +| `employee remove` | 移除员工 ⚠️ | `--staff-ids`(逗号分隔) | +| `employee change-type` | 变更员工类型 | `--staff-id`, `--emp-type` | +| `employee change-dept` | 变更员工部门 | `--staff-id`, `--target-dept-id` | +| `employee send-active-sms` | 发送激活短信 | `--dept-id` | +| `employee list-employees` | 查询部门员工列表 | `--dept-id` | +| `employee list-unaccepted` | 查询未接受邀请的员工列表 | `--dept-id` | +| `employee list-unactive` | 查询未激活的员工列表 | `--dept-id` | +| `employee upgrade-status` | 查询高校通讯录升级状态 | 无 | +| `employee start-upgrade` | 启动高校通讯录升级 | 无 | + +### alumni (校友管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `alumni get-dept-tree` | 查询校友部门树 | `--alumni-dept-id` | +| `alumni get-info` | 查询校友部门详情 | `--alumni-dept-id` | +| `alumni list` | 查询校友列表 | `--alumni-dept-id`, `--order-field`, `--ordering` | +| `alumni query` | 查询单个校友详情 | `--staff-id` | +| `alumni search` | 搜索校友 | `--keyword` | +| `alumni list-unaccepted` | 查询未接受邀请的校友列表 | `--alumni-dept-id` | +| `alumni get-group` | 查询校友群信息 | `--alumni-dept-id` | +| `alumni create-dept` | 创建校友子部门 | `--alumni-dept-id`, `--dept-name` | +| `alumni update-dept` | 更新校友部门名称 | `--alumni-dept-id`, `--dept-name` | +| `alumni delete-dept` | 删除校友部门 ⚠️ | `--alumni-dept-id` | +| `alumni update-managers` | 设置校友部门负责人 | `--alumni-dept-id`, `--admin-user-ids`(逗号分隔) | +| `alumni add-alumnus` | 添加校友 | `--name`, `--mobile`, `--dept-ids`(逗号分隔) | +| `alumni update-alumnus` | 更新校友信息 | `--staff-id`, `--name`, `--dept-ids`(逗号分隔) | +| `alumni remove-alumnus` | 删除校友 ⚠️ | `--staff-id`, `--alumni-dept-id` | +| `alumni cancel-invite` | 取消校友邀请 ⚠️ | `--alumni-dept-id`, `--staff-ids`(逗号分隔) | +| `alumni create-group` | 创建校友群 | `--alumni-dept-id` | +| `alumni disband-group` | 解散校友群 ⚠️ | `--alumni-dept-id` | +| `alumni get-alumni-org-from-graduate` | 查询毕业生校友组织 | 无入参 | +| `alumni create-alumni-org` | 创建校友会组织 | `--org-name` | +| `alumni add-alumni-org-main-admins` | 添加校友会组织管理员 | `--admin-user-ids`(逗号分隔) | + +### graduate (毕业年级管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `graduate query-graduate-years` | 查询毕业年级列表 | 无入参 | +| `graduate query-graduate-depts` | 查询待毕业部门列表 | `--dept-id`, `--graduate-year`(可选) | +| `graduate query-graduate-sub-depts` | 查询毕业子部门列表 | `--dept-id` | +| `graduate query-page-graduate-users` | 分页查询待毕业学生列表 | `--dept-id`, `--graduate-year`/`--offset`/`--size`(可选) | +| `graduate get-task-result` | 查询异步任务执行结果 | `--request-no`, `--type`(可选) | +| `graduate get-alumni-org` | 查询校友组织信息 | 无入参 | +| `graduate query-restore-sub-depts` | 查询可恢复子部门列表 | `--dept-id` | +| `graduate query-dept-deleted-emps` | 查询部门可恢复员工列表 | `--dept-id`, `--offset`/`--size`(可选) | +| `graduate search-graduate` | 搜索毕业部门与员工 | `--keyword`, `--offset`/`--size`(可选) | +| `graduate commit-graduate` | 提交毕业 ⚠️ | `--graduate-dept-ids`(逗号分隔), `--graduate-year`, `--request-no`(可选) | +| `graduate all-graduate` | 全部毕业 ⚠️ | `--graduate-year`, `--request-no`(可选) | +| `graduate batch-graduate` | 批量毕业 ⚠️ | `--dept-id`, `--staff-ids`(逗号分隔) | +| `graduate delete-and-graduate` | 删除并毕业 ⚠️ | `--dept-id`, `--staff-ids`(逗号分隔) | +| `graduate batch-delete-pending` | 批量删除待毕业学生 ⚠️ | `--dept-id`, `--staff-ids`(逗号分隔) | +| `graduate batch-update-pending` | 批量更新待毕业学生 ⚠️ | `--dept-id`, `--staff-ids`(逗号分隔), `--graduate-year` | +| `graduate commit-restore` | 提交恢复 ⚠️ | `--graduate-dept-ids`(逗号分隔), `--request-no`(可选) | + +### group (规则管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `group query-group-rule` | 查询规则 | `--name`(可选), `--offset`(可选), `--size`(可选) | +| `group get-group-rule-schedule` | 查询规则调度 | 无参数 | +| `group query-preview-data` | 查询规则预览数据 | `--offset`(可选), `--size`(可选) | +| `group create-group-rule` | 创建规则 | `--name`, `--tag-code`, `--dept-type`, `--auto-admin`(可选,true/false) | +| `group delete-group-rule` | 删除规则 ⚠️ | `--rule-id` | +| `group enable-group-rule` | 启用规则 | `--rule-id` | +| `group disable-group-rule` | 停用规则 | `--rule-id` | +| `group set-group-rule-schedule` | 设置规则调度 | `--cron`(可选) | +| `group execute-group-rule` | 立即执行规则 ⚠️ | 无参数 | + +## 常用参数说明 + +- `--emp-type`:员工类型,取值 `college_student`(学生)/ `college_teacher`(教职工) +- `--dept-type`:部门类型(如 `contact_grade_dept` 年级 / `contact_class_dept` 班级 / `contact_major_dept` 专业) +- `--staff-id` 单个员工 staffId;`--staff-ids` 为逗号分隔的批量列表 +- 列表类命令支持 `--offset` / `--size` 分页与 `--order-field` / `--ordering`(asc/desc) 排序 +- `--exclusive-account`、`--create-dept-group`、`--send-active-sms` 为布尔参数(true/false) + +## 意图判断 + +用户说"高校架构/组织架构/学院/系/部门" → dept 子命令 +用户说"搜人/找某某老师/找某某同学" → `dept search` +用户说"师生/教职工/学生/员工/辅导员" → employee 子命令 +用户说"激活/邀请/未激活账号" → `employee list-unactive` / `list-unaccepted` / `send-active-sms` +用户说"通讯录升级" → `employee upgrade-status` / `start-upgrade` +用户说"校友/校友会/校友部门/添加校友" → alumni 子命令 +用户说"毕业年级/毕业年份/待毕业学生/毕业操作" → graduate 子命令 +用户说"群规则/建群规则/自动建群" → group 子命令 + +## 核心工作流 + +1. 查标准架构 → `dept get-standard-structure`(提取 deptId) +2. 查看部门详情 → `dept get-detail --dept-id ` +3. 查看部门员工 → `employee list-employees --dept-id ` +4. 查看员工详情 → `employee get-detail --staff-id ` + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `dept get-standard-structure` | `deptId` | dept/employee 子命令的 --dept-id | +| `employee list-employees` | `staffId` | get-detail/change-type/change-dept 的 --staff-id、remove 的 --staff-ids | +| `dept create` | `deptId` | update/delete 的 --dept-id | +| `alumni get-dept-tree` | `alumniDeptId` | alumni 子命令的 --alumni-dept-id | +| `alumni list` | `staffId` | update-alumnus/remove-alumnus 的 --staff-id | +| `graduate query-graduate-depts` | `deptId` | graduate 子命令的 --dept-id | +| `group query-group-rule` | `ruleId` | delete/enable/disable-group-rule 的 --rule-id | + +## 危险操作 + +- `dept delete`、`employee remove`、`alumni delete-dept`、`alumni remove-alumnus`、`alumni cancel-invite`、`alumni disband-group`、`graduate commit-graduate`、`graduate all-graduate`、`graduate batch-graduate`、`graduate delete-and-graduate`、`graduate batch-delete-pending`、`graduate batch-update-pending`、`graduate commit-restore`、`group delete-group-rule`、`group execute-group-rule` 不可逆:非 --dry-run 预览时必须显式传入 --yes 才会真实执行,未传 --yes 会直接拒绝。执行前必须向用户展示操作摘要并获得明确同意,确认后再追加 --yes。 diff --git a/skills/multi/dingtalk-misc/references/edu-app.md b/skills/multi/dingtalk-misc/references/edu-app.md new file mode 100644 index 00000000..256bffd7 --- /dev/null +++ b/skills/multi/dingtalk-misc/references/edu-app.md @@ -0,0 +1,115 @@ +# 家校应用 (edu-app) 命令参考 + +## 命令总览 + +### message (消息管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `message summary-list` | 查询消息摘要列表 | `--class-id`, `--cid`, `--target-role`, `--status` | + +> `--target-role`: guardian(家长) / student(学生) +> `--status`: 0(未处理) / 1(已处理) + +### task (任务管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `task publish-list` | 查询发布的家校任务列表(仅老师) | 无(均可选) | +| `task all-list` | 查询全部家校任务列表(仅老师) | `--biz-id`(班级ID) | +| `task student-list` | 查询学生待办任务列表 | `--students`(JSON数组) | + +> `--task-sources` 可选值(逗号分隔): EDU_HOMEWORK, EDU_CARD, EDU_NOTICE, EDU_SR, EDU_DIPLOMA + +### report (成绩单管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `report get` | 获取成绩单列表 | `--ids`(逗号分隔整数) | +| `report by-teacher` | 查询老师创建的成绩单 | 无(均可选) | +| `report by-class` | 查询班级学生成绩明细 | `--report-id`, `--class-id` | +| `report by-student-list` | 查询学生收到的成绩单 | `--class-id`, `--student-id` | +| `report by-student-detail` | 查询学生成绩明细 | `--report-id`, `--student-id`, `--class-id` | + +### notice (通知管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `notice confirm` | 确认收到通知 | `--notice-id`, `--student-id` | + +### circle (班级圈) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `circle posts` | 查询学生班级圈动态 | `--class-id`, `--student-id`, `--target-role` | + +> `--target-role`: guardian(家长视角) / student(学生视角) +> 返回动态的文字内容、图片URL列表、发布者姓名、发布时间、评论数、点赞数等。 + +### card (打卡管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `card update` | 修改打卡标题或内容(仅创建者) | `--card-id`, `--identifier`, `--title`/`--content` | +| `card end` | 提前结束打卡任务(仅创建者) | `--card-id` | +| `card list` | 查询打卡列表(学生/家长) | `--status`(FINISH/UNFINISH) | +| `card user-statistic` | 查询班级打卡完成/未完成人员(老师/班主任) | `--card-id`, `--task-code`, `--class-id` | +| `card finish-info` | 查询打卡详情及完成进度 | `--card-id`, `--card-biz-id` | + +> `card list --status`: FINISH(已完结) / UNFINISH(进行中) +> `card finish-info --target-role`: teacher / headmaster / guardian / student,未传时按 uid 真实身份自动推断 +> `card finish-info --student-id`: 当 targetRole 为 guardian 时,用于指定查看某个孩子的进度 + +## 意图判断 + +用户说"消息/消息摘要" → message summary-list +用户说"作业/任务/打卡/家校任务" → task 子命令 +用户说"成绩/成绩单" → report 子命令 +用户说"通知/确认通知" → notice confirm +用户说"班级圈/成长记录/学生动态" → circle posts +用户说"打卡/打卡列表/打卡进度/打卡统计/卡片完成情况" → card 子命令 + +关键区分: task(家校任务/作业) vs report(成绩单) +关键区分: circle(班级圈动态/成长记录) vs message(AI消息总结) + +## 核心工作流 + +### 老师场景 +1. 查看发布的任务 → `task publish-list --need-statistic -f json` +2. 查看某班全部任务 → `task all-list --biz-id ` +3. 查看成绩单 → `report by-teacher --status 1` +4. 查看班级成绩明细 → `report by-class --report-id --class-id ` + +### 家长场景 +1. 查看孩子待办 → `task student-list --students '[{"userId":"","bizId":""}]'` +2. 确认通知 → `notice confirm --notice-id --student-id ` +3. 查看孩子班级圈动态 → `circle posts --class-id --student-id --target-role guardian` + +### 学生场景 +1. 查看自己的班级圈动态 → `circle posts --class-id --student-id --target-role student` + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `edu-contact school class-list` | `deptId` | task all-list 的 --biz-id | +| `edu-contact class students` | `userId` | task student-list 的 students.userId | +| `edu-group class-group conversation-id` | `conversationId` | message summary-list 的 --cid | +| `report by-teacher` | `schoolReportId` | report get/by-class/by-student-detail 的 --report-id | +| `edu-contact family children` | `studentUserId`, `classId` | circle posts 的 --student-id, --class-id | + +--- + +## SKILL 摘要(原 dingtalk-edu-app/SKILL.md 正文) + +## 意图表 + +| 用户说 | 命令 | +|--------|------| +| "查班级消息摘要" | `dws edu-app message summary-list --class-id --cid --target-role guardian\|student --status 0\|1` | +| "查家校任务" | 见 [edu-app.md](./edu-app.md) `task` 章节 | + +## 跨产品协作 + +- 师生群本身 → 见本包 references/edu-group.md +- 家校通讯录 → 见本包 references/edu-contact.md diff --git a/skills/multi/dingtalk-misc/references/edu-contact.md b/skills/multi/dingtalk-misc/references/edu-contact.md new file mode 100644 index 00000000..5d9b4c52 --- /dev/null +++ b/skills/multi/dingtalk-misc/references/edu-contact.md @@ -0,0 +1,92 @@ +# 家校通讯录 (edu-contact) 命令参考 + +## 命令总览 + +### school (学校/组织管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `school roles` | 查询用户在组织内的身份 | 无 | +| `school structure` | 查询学校组织架构 | 无 | +| `school periods` | 查询学校学段信息 | 无 | +| `school type` | 查询学校组织类型 | 无 | +| `school stats` | 查询学校统计数据 | `--statistics-type`(可选) | +| `school class-list` | 查询学校所有班级列表 | 无 | + +### class (班级管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `class detail` | 查询班级详情 | `--dept-id` | +| `class students` | 查询班级学生信息 | `--dept-id` | +| `class teachers` | 查询班级老师列表 | `--dept-id` | +| `class same-name` | 查询班级内同名学生 | `--dept-id` | +| `class user-role` | 查询用户在班级内的角色 | `--dept-id` | +| `class search-by-name` | 根据姓名查询班级 | `--query-type`, `--name` | +| `class headmaster` | 根据班级名查询班主任 | `--class-name` | +| `class search-by-teacher` | 根据老师姓名查询班级 | `--name` | +| `class update-student` | 更新学生信息 | `--class-id`, `--student-user-id` | +| `class add-student` | 添加学生到班级 | `--dept-id`, `--student-name` | +| `class modify-student-info` | 修改学生信息 | `--dept-id`, `--target-user-id` | +| `class delete-teacher` | 删除班级教师 ⚠️ | `--class-id`, `--teacher-user-id` | +| `class update-info` | 更新班级信息 | `--class-id` | +| `class update-student-number` | 修改学生学号 | `--class-id`, `--student-user-id`, `--student-number` | +| `class add-unofficial-student` | 添加非行政班学生 | `--dept-id`, `--student-staff-ids` | +| `class delete-students` | 批量删除学生 ⚠️ | `--dept-id`, `--student-user-ids` | +| `class update-student-mobile` | 修改学生手机号 | `--dept-id`, `--student-user-id`, `--mobile` | +| `class move-student` | 学生移班 | `--student-user-ids`, `--origin-class-id`, `--target-class-id` | +| `class add-teachers` | 批量添加班级教师 | `--dept-id`, `--teacher-user-ids` | + +### family (家庭关系查询) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `family children` | 查询家长的孩子信息 | 无 | +| `family parents` | 查询学生的家长信息 | 无 | + +### teacher (教师管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `teacher classes` | 查询老师管理的班级列表 | 无 | +| `teacher update-course` | 更新教师任教科目 | `--teacher-class-infos`(JSON数组) | + +## 意图判断 + +用户说"学校/组织/学段/组织架构" → school 子命令 +用户说"班级/学生/教师/班主任" → class 子命令 +用户说"家长/孩子/家庭关系" → family 子命令 +用户说"任教/科目" → teacher update-course + +## 核心工作流 + +1. 查询角色 → `school roles` +2. 查看班级列表 → `school class-list`(提取 deptId) +3. 查看班级详情 → `class detail --dept-id ` +4. 查看学生列表 → `class students --dept-id ` + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `school class-list` | `deptId` | class 子命令的 --dept-id | +| `class students` | `userId` | update-student/delete-students 的 --student-user-id | +| `class teachers` | `userId` | delete-teacher 的 --teacher-user-id | + +--- + +## SKILL 摘要(原 dingtalk-edu-contact/SKILL.md 正文) + +## 意图表 + +| 用户说 | 命令 | +|--------|------| +| "我在学校的身份" | `dws edu-contact school roles` | +| "学校组织架构" | `dws edu-contact school structure` | +| "学校学段 / 类型" | `dws edu-contact school periods` / `school type` | +| "学校所有班级" | `dws edu-contact school class-list` | +| "学校统计" | `dws edu-contact school stats [--statistics-type ]` | + +## 跨产品协作 + +- 企业通讯录场景 → 切到 `dingtalk-contact` diff --git a/skills/multi/dingtalk-misc/references/edu-familygroup.md b/skills/multi/dingtalk-misc/references/edu-familygroup.md new file mode 100644 index 00000000..82ec8631 --- /dev/null +++ b/skills/multi/dingtalk-misc/references/edu-familygroup.md @@ -0,0 +1,68 @@ +# 家庭群 (edu-familygroup) 命令参考 + +## 命令总览 + +### group (家庭群查询) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `group check-exists` | 检查家庭群是否存在 | `--uid`, `--group-name` | +| `group list-children` | 查询家长绑定的孩子列表 | `--uid` | + +### manage (家庭群管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `manage create` | 创建家庭群 | `--uid`, `--children` | +| `manage invite-parent` | 短信邀请家长加入家庭群 | `--org-id`, `--uid`, `--mobile` | +| `manage add-child` | 为家庭群添加孩子 | `--org-id`, `--uid`, `--name`, (`--mobile` 或 `--students`) | +| `manage toggle-app` | 开启或关闭学生应用权限 | `--org-id`, `--uid`, `--child-staff-id`, `--app-type`, `--open` | + +## 意图判断 + +用户说"家庭群存在/有没有家庭群" → group check-exists +用户说"孩子列表/我的孩子" → group list-children +用户说"创建家庭/建群" → manage create +用户说"邀请家长/拉家长入群" → manage invite-parent +用户说"添加孩子/加娃" → manage add-child +用户说"应用权限/小天地/学习视频" → manage toggle-app + +## 核心工作流 + +1. 检查家庭群是否存在 → `group check-exists --uid --group-name ` +2. 如不存在,创建家庭群 → `manage create --uid --children ''` +3. 查看已绑定孩子 → `group list-children --uid ` +4. 邀请其他家长 → `manage invite-parent --org-id --uid --mobile ` +5. 添加孩子 → `manage add-child --org-id --uid --name --mobile ` +6. 管理应用权限 → `manage toggle-app --org-id --uid --child-staff-id --app-type XIAOTIANDI --open true` + +## 参数说明 + +### manage create --children 格式 + +```json +[{"name":"小明","students":[{"corpId":"dingxxx","staffId":"stu001"}]}] +``` + +每个孩子必填 name + students 数组(含 corpId、staffId),可选 birthday/gender/nick/avatar/period/grade/mobile。 + +### manage add-child --students 格式 + +```json +[{"schoolOrgId":111,"studentStaffId":"stu001"}] +``` + +每项必填 schoolOrgId(整数)+ studentStaffId(字符串)。 + +### manage toggle-app --app-type 可选值 + +- `XIAOTIANDI`:小天地(学生圈) +- `LEARNING_VIDEO`:学习视频 + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `manage create` | `orgId`, `cid` | invite-parent / add-child 的 --org-id | +| `group list-children` | 孩子 staffId | toggle-app 的 --child-staff-id | +| `edu-contact family list` | 家长 uid | 所有 edu-familygroup 命令的 --uid | diff --git a/skills/multi/dingtalk-misc/references/edu-group.md b/skills/multi/dingtalk-misc/references/edu-group.md new file mode 100644 index 00000000..056327a6 --- /dev/null +++ b/skills/multi/dingtalk-misc/references/edu-group.md @@ -0,0 +1,77 @@ +# 家校群 (edu-group) 命令参考 + +## 命令总览 + +### student-group (师生群管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `student-group info` | 查询班级师生群信息 | `--dept-id` | +| `student-group exists` | 检查是否已创建师生群 | `--dept-id` | +| `student-group members` | 查询师生群成员列表 | `--dept-id` | +| `student-group is-in` | 判断用户是否在师生群中 | `--dept-id` | +| `student-group conversation` | 查询班级群会话详情 | `--dept-id` | +| `student-group create` | 创建班级师生群 | `--dept-id` | +| `student-group disband` | 解散班级师生群 ⚠️ | `--dept-id` | + +### class-group (班级群会话管理) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `class-group conversation-id` | 获取班级群会话ID | `--dept-id` | +| `class-group conversation` | 获取班级群完整会话信息 | `--dept-id` | +| `class-group exists` | 检查班级群是否存在 | `--dept-id` | +| `class-group list-by-cids` | 根据会话ID列表批量查询 | `--conversation-ids` | + +### batch (批量操作) + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `batch check-student-group` | 批量检查是否已创建师生群 | `--class-ids` | +| `batch get-class-groups` | 批量获取班级群信息 | `--class-ids` | +| `batch create-student-groups` | 批量创建师生群 | 无 | + +## 意图判断 + +用户说"师生群/学生群" → student-group 子命令 +用户说"班级群/群会话" → class-group 子命令 +用户说"批量/一键操作" → batch 子命令 + +关键区分: student-group(师生群) vs class-group(班级群会话管理) + +## 核心工作流 + +1. 检查群是否存在 → `student-group exists --dept-id ` +2. 如不存在,创建 → `student-group create --dept-id ` +3. 查看成员 → `student-group members --dept-id ` +4. 获取会话ID → `class-group conversation-id --dept-id ` + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `edu-contact school class-list` | `deptId` | 所有 edu-group 命令的 --dept-id | +| `class-group conversation-id` | `conversationId` | edu-app 消息命令的 --cid | +| `batch check-student-group` | 未创建群的班级 | batch create-student-groups | + +--- + +## SKILL 摘要(原 dingtalk-edu-group/SKILL.md 正文) + +## 意图表 + +| 用户说 | 命令 | +|--------|------| +| "查师生群信息 / 是否已建" | `dws edu-group student-group info --dept-id ` / `exists --dept-id ` | +| "师生群成员" | `dws edu-group student-group members --dept-id ` | +| "建班级师生群" | `dws edu-group student-group create --dept-id ` | +| "解散班级师生群 ⚠️" | `dws edu-group student-group disband --dept-id `(需用户确认 `--yes`) | + +## 危险操作 + +`student-group disband` 不可逆,必须先向用户确认再加 `--yes`。 + +## 跨产品协作 + +- 班级列表 → 见本包 references/edu-contact.md(school class-list) +- 企业群 → 切到 `dingtalk-chat`