feat: add edu & college vendor extension commands

Add five hidden vendor extension commands for education scenarios:
- edu-contact: school/class/family/teacher contact management
- edu-group: student/class group lifecycle
- edu-app: homework, notices, report cards, diplomas, class circles
- edu-familygroup: family group management, child binding, app permissions
- college-contact: university dept/employee/alumni/graduate management

All route to dedicated MCP servers via callMCPToolOnServer with endpoints
registered in openSupplementServers (helper-only, not VisibleProducts).

Includes: helper implementations, dispatch tests, mono/multi skill docs,
coverage.yaml registration, context-budget compliance, and .changes fragment.
This commit is contained in:
听荷 committed 2026-08-24 13:30:32 +08:00
1 parent da6f867dfa
commit f3e98d55e8
26 files changed
+16180 -1

No files matched your search

@@ -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`.
File diff suppressed because it is too large. Load diff
+680
View File
@@ -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)
}
})
}
}
File diff suppressed because it is too large. Load diff
+400
View File
@@ -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"])
}
})
}
File diff suppressed because it is too large. Load diff
+583
View File
@@ -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"])
}
})
}
+605
View File
@@ -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
}
+286
View File
@@ -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"])
}
})
}
+846
View File
@@ -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
}
+281
View File
@@ -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"])
}
})
}
+28
View File
@@ -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}
})
}
}
+30
View File
@@ -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"},
},
}
}
@@ -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
+7
View File
@@ -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` | 移除高校员工,不可恢复 |
### 确认流程
```
@@ -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 <deptId>`
3. 查看部门员工 → `employee list-employees --dept-id <deptId>`
4. 查看员工详情 → `employee get-detail --staff-id <staffId>`
## 上下文传递表
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `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。
+109
View File
@@ -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 <classId>`
3. 查看成绩单 → `report by-teacher --status 1`
4. 查看班级成绩明细 → `report by-class --report-id <id> --class-id <classId>`
5. 修改打卡标题/内容 → `card update --card-id <cardId> --identifier <id> --title "新标题"`
6. 提前结束打卡 → `card end --card-id <cardId>`
7. 查看某班打卡完成情况 → `card user-statistic --card-id <cardId> --task-code <taskCode> --class-id <classId> --finish`
8. 查看某班未打卡人员 → `card user-statistic --card-id <cardId> --task-code <taskCode> --class-id <classId>`
9. 查看某打卡完成进度 → `card finish-info --card-id <cardId> --card-biz-id <cardBizId>`
### 家长场景
1. 查看孩子待办 → `task student-list --students '[{"userId":"<uid>","bizId":"<classId>"}]'`
2. 确认通知 → `notice confirm --notice-id <id> --student-id <uid>`
3. 查看孩子班级圈动态 → `circle posts --class-id <classId> --student-id <studentId> --target-role guardian`
4. 查看孩子进行中打卡 → `card list --status UNFINISH`
5. 查看孩子某打卡进度 → `card finish-info --card-id <cardId> --card-biz-id <cardBizId>`
### 学生场景
1. 查看自己的班级圈动态 → `circle posts --class-id <classId> --student-id <studentId> --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 |
@@ -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 <deptId>`
4. 查看学生列表 → `class students --dept-id <deptId>`
## 上下文传递表
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `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 |
@@ -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 <uid> --group-name <name>`
2. 如不存在,创建家庭群 → `manage create --uid <uid> --children '<json>'`
3. 查看已绑定孩子 → `group list-children --uid <uid>`
4. 邀请其他家长 → `manage invite-parent --org-id <orgId> --uid <uid> --mobile <phone>`
5. 添加孩子 → `manage add-child --org-id <orgId> --uid <uid> --name <name> --mobile <phone>`
6. 管理应用权限 → `manage toggle-app --org-id <orgId> --uid <uid> --child-staff-id <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 |
@@ -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 <deptId>`
2. 如不存在,创建 → `student-group create --dept-id <deptId>`
3. 查看成员 → `student-group members --dept-id <deptId>`
4. 获取会话ID → `class-group conversation-id --dept-id <deptId>`
## 上下文传递表
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `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 |
+6 -1
View File
@@ -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) |
## 说明
@@ -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 <deptId>`
3. 查看部门员工 → `employee list-employees --dept-id <deptId>`
4. 查看员工详情 → `employee get-detail --staff-id <staffId>`
## 上下文传递表
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `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。
@@ -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 <classId>`
3. 查看成绩单 → `report by-teacher --status 1`
4. 查看班级成绩明细 → `report by-class --report-id <id> --class-id <classId>`
### 家长场景
1. 查看孩子待办 → `task student-list --students '[{"userId":"<uid>","bizId":"<classId>"}]'`
2. 确认通知 → `notice confirm --notice-id <id> --student-id <uid>`
3. 查看孩子班级圈动态 → `circle posts --class-id <classId> --student-id <studentId> --target-role guardian`
### 学生场景
1. 查看自己的班级圈动态 → `circle posts --class-id <classId> --student-id <studentId> --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 <id> --cid <id> --target-role guardian\|student --status 0\|1` |
| "查家校任务" | 见 [edu-app.md](./edu-app.md) `task` 章节 |
## 跨产品协作
- 师生群本身 → 见本包 references/edu-group.md
- 家校通讯录 → 见本包 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 <deptId>`
4. 查看学生列表 → `class students --dept-id <deptId>`
## 上下文传递表
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `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 <t>]` |
## 跨产品协作
- 企业通讯录场景 → 切到 `dingtalk-contact`
@@ -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 <uid> --group-name <name>`
2. 如不存在,创建家庭群 → `manage create --uid <uid> --children '<json>'`
3. 查看已绑定孩子 → `group list-children --uid <uid>`
4. 邀请其他家长 → `manage invite-parent --org-id <orgId> --uid <uid> --mobile <phone>`
5. 添加孩子 → `manage add-child --org-id <orgId> --uid <uid> --name <name> --mobile <phone>`
6. 管理应用权限 → `manage toggle-app --org-id <orgId> --uid <uid> --child-staff-id <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 |
@@ -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 <deptId>`
2. 如不存在,创建 → `student-group create --dept-id <deptId>`
3. 查看成员 → `student-group members --dept-id <deptId>`
4. 获取会话ID → `class-group conversation-id --dept-id <deptId>`
## 上下文传递表
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `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 <id>` / `exists --dept-id <id>` |
| "师生群成员" | `dws edu-group student-group members --dept-id <id>` |
| "建班级师生群" | `dws edu-group student-group create --dept-id <id>` |
| "解散班级师生群 ⚠️" | `dws edu-group student-group disband --dept-id <id>`(需用户确认 `--yes`) |
## 危险操作
`student-group disband` 不可逆,必须先向用户确认再加 `--yes`。
## 跨产品协作
- 班级列表 → 见本包 references/edu-contact.md(school class-list)
- 企业群 → 切到 `dingtalk-chat`