feat: 新增招聘职位管理 to#85340676

This commit is contained in:
恋川 committed 2026-08-13 10:26:03 +08:00
1 parent 5fed80fc0f
commit 19e19bcd2b
13 files changed
+632 -8

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
category: Added
---
- **招聘职位管理** (#976) — 新增招聘职位列表、详情查询和职位创建命令。
+284
View File
@@ -0,0 +1,284 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package helpers
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
)
const (
recruitServerID = "recruit"
recruitCreateJobTool = "create_job"
recruitGetJobTool = "get_job_detail"
recruitListJobsTool = "list_jobs"
)
var recruitDryRun = &contract.DryRunSpec{
PreviewKind: contract.DryRunPreviewInvocation,
RemoteReads: false,
}
func newRecruitCommand() *cobra.Command {
contract.RegisterProductDecl(contract.ProductDecl{
ID: "recruit",
Selection: contract.ProductSelectionDecl{
AgentSummary: "查询和创建钉钉招聘职位",
UseWhen: []string{"需要查询招聘职位列表、查看职位详情或创建职位时"},
AvoidWhen: []string{"查询企业员工与组织架构使用 contact;查询人才池、职业历程或绩效使用 hrbrain"},
},
})
root := &cobra.Command{
Use: "recruit",
Short: "钉钉招聘",
Long: "查询和创建钉钉招聘中的职位信息。",
RunE: groupRunE,
}
job := &cobra.Command{
Use: "job",
Short: "招聘职位管理",
RunE: groupRunE,
}
job.AddCommand(
newRecruitJobListCommand(),
newRecruitJobGetCommand(),
newRecruitJobCreateCommand(),
)
root.AddCommand(job)
return root
}
func newRecruitJobListCommand() *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "list",
Short: "查询招聘职位列表",
Long: "按职位 ID、关键词、状态、创建人、职位性质等条件分页查询招聘职位。",
Example: " dws recruit job list --keyword Java --status open --size 20 --format json\n dws recruit job list --job-ids JOB_ID_1,JOB_ID_2 --format json",
Server: recruitServerID,
Tool: recruitListJobsTool,
Safety: recruitSafetyRead(),
Flags: []LeafFlag{
{Name: "job-ids", Usage: "职位 ID,多个值用逗号分隔", Kind: LeafStringSlice, Bind: "jobIds"},
{Name: "required-edu", Usage: "学历要求枚举值", Kind: LeafInt, Bind: "requiredEdu"},
{Name: "status", Usage: "职位状态:draft/open/invalid/closed,多个值用逗号分隔", Bind: "statusList", Trim: true, OmitEmpty: true, Transform: transformRecruitStatuses},
{Name: "job-nature", Usage: "职位性质", Bind: "jobNature", Trim: true, OmitEmpty: true},
{Name: "campus", Usage: "是否为校园招聘", Kind: LeafBool, Bind: "campus"},
{Name: "start-modified-time", Usage: "修改时间范围起点", Bind: "startModifiedTime", Trim: true, OmitEmpty: true},
{Name: "end-modified-time", Usage: "修改时间范围终点", Bind: "endModifiedTime", Trim: true, OmitEmpty: true},
{Name: "creator-user-ids", Usage: "创建人 userId,多个值用逗号分隔", Kind: LeafStringSlice, Bind: "creatorUserIds"},
{Name: "keyword", Usage: "职位搜索关键词", Bind: "keyword", Trim: true, OmitEmpty: true},
{Name: "category", Usage: "职位分类", Bind: "category", Trim: true, OmitEmpty: true},
{Name: "cursor", Usage: "分页游标;首次查询不传,翻页时传返回的 nextCursor", Kind: LeafInt, Bind: "cursor"},
{Name: "size", Usage: "分页大小,默认 20", Kind: LeafInt, Bind: "size", ArgDefault: "20"},
},
Validate: validateRecruitList,
Call: runRecruitJobList,
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{ProductID: "recruit", Name: recruitListJobsTool, CanonicalPath: "recruit.list_jobs", CLIPath: "recruit job list", PrimaryCLIPath: "recruit job list"},
Description: "按条件分页查询招聘职位",
DryRun: recruitDryRun,
Interface: recruitMCPInterface(recruitListJobsTool),
Selection: contract.SelectionSpec{
AgentSummary: "按关键词、状态、创建人等条件分页查询招聘职位",
UseWhen: []string{"需要查找职位、筛选在招职位或取得 jobId 时"},
AvoidWhen: []string{"已经持有明确 jobId 且需要完整详情时使用 recruit job get"},
Examples: []string{`dws recruit job list --keyword "Java" --status open --size 20 --format json`},
},
Parameters: recruitListParamDecls(),
},
})
}
func runRecruitJobList(_ *cobra.Command, tool string, args map[string]any) error {
params := map[string]any{"param": map[string]any{}, "size": args["size"]}
query := params["param"].(map[string]any)
for key, value := range args {
switch key {
case "cursor":
params[key] = value
case "size":
default:
query[key] = value
}
}
return callMCPToolOnServer(recruitServerID, tool, params)
}
func newRecruitJobGetCommand() *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "get",
Short: "查询招聘职位详情",
Long: "根据职位 ID 查询招聘职位详情。",
Example: " dws recruit job get --job-id JOB_ID --format json",
Server: recruitServerID,
Tool: recruitGetJobTool,
Safety: recruitSafetyRead(),
Flags: []LeafFlag{{
Name: "job-id", Usage: "职位 ID(必填)", Bind: "jobId", Trim: true,
Required: true, RequiredHint: "--job-id 为必填",
}},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{ProductID: "recruit", Name: recruitGetJobTool, CanonicalPath: "recruit.get_job_detail", CLIPath: "recruit job get", PrimaryCLIPath: "recruit job get"},
Description: "根据职位 ID 查询招聘职位详情",
DryRun: recruitDryRun,
Interface: recruitMCPInterface(recruitGetJobTool),
Selection: contract.SelectionSpec{
AgentSummary: "获取指定招聘职位的完整信息",
UseWhen: []string{"已经持有明确 jobId,需要查看职位详情时"},
AvoidWhen: []string{"不知道 jobId 时先使用 recruit job list 查询"},
Examples: []string{"dws recruit job get --job-id <JOB_ID> --format json"},
},
Parameters: []contract.ParamDecl{{Name: "job-id", Property: "jobId", Required: boolPtr(true), InterfaceType: "string"}},
},
})
}
func newRecruitJobCreateCommand() *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "create",
Short: "创建招聘职位",
Long: "从 JSON 文件读取职位信息并创建招聘职位。该操作会写入远端招聘系统,执行前必须确认。",
Example: " dws recruit job create --from ./job.json --dry-run --format json\n dws recruit job create --from ./job.json --format json",
Server: recruitServerID,
Tool: recruitCreateJobTool,
Safety: recruitSafetyCreate(),
Flags: []LeafFlag{{
Name: "from", Usage: "职位 JSON 文件路径(必填)", Bind: "atsAddJobParam", Trim: true,
Required: true, RequiredHint: "--from 为必填", Transform: loadRecruitJobFile,
}},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{ProductID: "recruit", Name: recruitCreateJobTool, CanonicalPath: "recruit.create_job", CLIPath: "recruit job create", PrimaryCLIPath: "recruit job create"},
Description: "从 JSON 文件创建招聘职位",
DryRun: recruitDryRun,
Interface: recruitMCPInterface(recruitCreateJobTool),
Selection: contract.SelectionSpec{
AgentSummary: "使用结构化 JSON 创建招聘职位",
UseWhen: []string{"用户明确要求新建职位,并已准备或同意生成职位 JSON 时;文件必须包含 name、description、jobNature、requiredEdu、minSalary、maxSalary"},
AvoidWhen: []string{"仅查询职位时使用 recruit job list 或 recruit job get"},
Examples: []string{"dws recruit job create --from ./job.json --dry-run --format json"},
},
Parameters: []contract.ParamDecl{{Name: "from", Property: "atsAddJobParam", Required: boolPtr(true), InterfaceType: "object", Description: "职位 JSON 文件;CLI 读取后作为 atsAddJobParam 对象发送"}},
},
})
}
func recruitListParamDecls() []contract.ParamDecl {
return []contract.ParamDecl{
{Name: "job-ids", Property: "param.jobIds", InterfaceType: "array"},
{Name: "required-edu", Property: "param.requiredEdu", InterfaceType: "number"},
{Name: "status", Property: "param.statusList", InterfaceType: "array", Enum: []string{"draft", "open", "invalid", "closed"}},
{Name: "job-nature", Property: "param.jobNature", InterfaceType: "string"},
{Name: "campus", Property: "param.campus", InterfaceType: "boolean"},
{Name: "start-modified-time", Property: "param.startModifiedTime", InterfaceType: "string"},
{Name: "end-modified-time", Property: "param.endModifiedTime", InterfaceType: "string"},
{Name: "creator-user-ids", Property: "param.creatorUserIds", InterfaceType: "array"},
{Name: "keyword", Property: "param.keyword", InterfaceType: "string"},
{Name: "category", Property: "param.category", InterfaceType: "string"},
{Name: "cursor", Property: "cursor", InterfaceType: "number"},
{Name: "size", Property: "size", Required: boolPtr(true), InterfaceType: "number"},
}
}
func recruitMCPInterface(tool string) *contract.InterfaceSpec {
return &contract.InterfaceSpec{
Mode: contract.InterfaceModeMCP,
Availability: contract.InterfaceAvailable,
Ref: &contract.InterfaceRefSpec{ProductID: recruitServerID, RPCName: tool},
}
}
func recruitSafetyRead() contract.SafetySpec {
return contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}
}
func recruitSafetyCreate() contract.SafetySpec {
return contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "non_idempotent"}
}
func transformRecruitStatuses(raw string) (any, error) {
values := strings.Split(raw, ",")
statuses := make([]int, 0, len(values))
seen := make(map[int]bool, len(values))
lookup := map[string]int{"draft": 0, "open": 1, "invalid": 2, "closed": 3}
for _, value := range values {
name := strings.ToLower(strings.TrimSpace(value))
if name == "" {
continue
}
status, ok := lookup[name]
if !ok {
return nil, apperrors.NewValidation(fmt.Sprintf("--status 不支持 %q,可选 draft/open/invalid/closed", value))
}
if !seen[status] {
statuses = append(statuses, status)
seen[status] = true
}
}
return statuses, nil
}
func validateRecruitList(cmd *cobra.Command, _ []string) error {
if cursor, _ := cmd.Flags().GetInt("cursor"); cursor < 0 {
return apperrors.NewValidation("--cursor 必须大于或等于 0")
}
size, _ := cmd.Flags().GetInt("size")
if cmd.Flags().Changed("size") && (size < 1 || size > 100) {
return apperrors.NewValidation("--size 必须在 1 到 100 之间")
}
return nil
}
func loadRecruitJobFile(path string) (any, error) {
data, err := os.ReadFile(strings.TrimSpace(path))
if err != nil {
return nil, fmt.Errorf("读取职位 JSON 失败: %w", err)
}
var job map[string]any
if err := json.Unmarshal(data, &job); err != nil {
return nil, apperrors.NewValidation(fmt.Sprintf("职位文件不是有效的 JSON 对象: %v", err))
}
if job == nil {
return nil, apperrors.NewValidation("职位 JSON 顶层必须是对象")
}
if err := validateRecruitJob(job); err != nil {
return nil, err
}
return job, nil
}
func validateRecruitJob(job map[string]any) error {
for _, name := range []string{"name", "description", "jobNature", "requiredEdu", "minSalary", "maxSalary"} {
value, ok := job[name]
if !ok || value == nil || (isString(value) && strings.TrimSpace(value.(string)) == "") {
return apperrors.NewValidation(fmt.Sprintf("职位 JSON 缺少必填字段 %s", name))
}
}
for _, name := range []string{"name", "description", "jobNature"} {
if !isString(job[name]) {
return apperrors.NewValidation(fmt.Sprintf("职位 JSON 字段 %s 必须是字符串", name))
}
}
for _, name := range []string{"requiredEdu", "minSalary", "maxSalary"} {
if _, ok := job[name].(float64); !ok {
return apperrors.NewValidation(fmt.Sprintf("职位 JSON 字段 %s 必须是数字", name))
}
}
if job["minSalary"].(float64) > job["maxSalary"].(float64) {
return apperrors.NewValidation("职位 JSON 中 minSalary 不能大于 maxSalary")
}
return nil
}
func isString(value any) bool {
_, ok := value.(string)
return ok
}
+207
View File
@@ -0,0 +1,207 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package helpers
import (
"context"
"encoding/json"
stderrors "errors"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type recruitCapturedCall struct {
productID string
tool string
args map[string]any
}
type recruitCaptureCaller struct {
productID string
tool string
args map[string]any
calls []recruitCapturedCall
}
func (c *recruitCaptureCaller) CallTool(_ context.Context, productID, tool string, args map[string]any) (*edition.ToolResult, error) {
c.productID = productID
c.tool = tool
c.args = args
c.calls = append(c.calls, recruitCapturedCall{productID: productID, tool: tool, args: args})
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
}
func (c *recruitCaptureCaller) Format() string { return "json" }
func (c *recruitCaptureCaller) DryRun() bool { return false }
func (c *recruitCaptureCaller) Fields() string { return "" }
func (c *recruitCaptureCaller) JQ() string { return "" }
func withRecruitCaller(t *testing.T) *recruitCaptureCaller {
t.Helper()
caller := &recruitCaptureCaller{}
InitDepsForTest(t, caller)
deps.Out.w = io.Discard
return caller
}
func TestRecruitJobListWrapsQueryParameters(t *testing.T) {
caller := withRecruitCaller(t)
cmd := newRecruitJobListCommand()
cmd.SetArgs([]string{
"--keyword", "Java", "--status", "open,draft", "--creator-user-ids", "u1,u2",
"--campus=false", "--cursor", "10", "--size", "30",
})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if caller.productID != recruitServerID || caller.tool != recruitListJobsTool {
t.Fatalf("dispatch = %s/%s, want %s/%s", caller.productID, caller.tool, recruitServerID, recruitListJobsTool)
}
if caller.args["cursor"] != 10 || caller.args["size"] != 30 {
t.Fatalf("pagination args = %#v", caller.args)
}
param, ok := caller.args["param"].(map[string]any)
if !ok {
t.Fatalf("param = %#v, want object", caller.args["param"])
}
if param["keyword"] != "Java" || param["campus"] != false {
t.Fatalf("param = %#v", param)
}
statuses, ok := param["statusList"].([]int)
if !ok || len(statuses) != 2 || statuses[0] != 1 || statuses[1] != 0 {
t.Fatalf("statusList = %#v", param["statusList"])
}
creators, ok := param["creatorUserIds"].([]string)
if !ok || len(creators) != 2 || creators[0] != "u1" || creators[1] != "u2" {
t.Fatalf("creatorUserIds = %#v", param["creatorUserIds"])
}
}
func TestRecruitJobListDefaultsAndValidation(t *testing.T) {
caller := withRecruitCaller(t)
cmd := newRecruitJobListCommand()
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if caller.args["size"] != 20 {
t.Fatalf("size = %#v, want 20", caller.args["size"])
}
if _, ok := caller.args["cursor"]; ok {
t.Fatalf("first page unexpectedly sent cursor: %#v", caller.args)
}
badStatus := newRecruitJobListCommand()
badStatus.SetArgs([]string{"--status", "unknown"})
if err := badStatus.Execute(); err == nil || !strings.Contains(err.Error(), "draft/open/invalid/closed") {
t.Fatalf("invalid status error = %v", err)
}
badSize := newRecruitJobListCommand()
badSize.SetArgs([]string{"--size", "101"})
if err := badSize.Execute(); err == nil || !strings.Contains(err.Error(), "1 到 100") {
t.Fatalf("invalid size error = %v", err)
}
}
func TestRecruitJobGetDispatchesJobID(t *testing.T) {
caller := withRecruitCaller(t)
cmd := newRecruitJobGetCommand()
cmd.SetArgs([]string{"--job-id", "job-1"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if caller.productID != recruitServerID || caller.tool != recruitGetJobTool || caller.args["jobId"] != "job-1" {
t.Fatalf("captured call = %s/%s %#v", caller.productID, caller.tool, caller.args)
}
}
func writeRecruitJobFixture(t *testing.T) (string, map[string]any) {
t.Helper()
path := filepath.Join(t.TempDir(), "job.json")
job := map[string]any{
"name": "Java 工程师", "description": "服务端开发", "jobNature": "FULL_TIME",
"requiredEdu": 1, "minSalary": 20000, "maxSalary": 35000,
}
data, err := json.Marshal(job)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
return path, job
}
func TestRecruitJobCreateRequiresConfirmationBeforeRemoteCall(t *testing.T) {
caller := withRecruitCaller(t)
path, _ := writeRecruitJobFixture(t)
cmd := newRecruitJobCreateCommand()
cmd.Flags().Bool("yes", false, "确认创建")
cmd.SetIn(strings.NewReader(""))
cmd.SetArgs([]string{"--from", path})
err := cmd.Execute()
var appErr *apperrors.Error
if !stderrors.As(err, &appErr) || appErr.Reason != "confirmation_required" {
t.Fatalf("create without confirmation error = %T %v, want confirmation_required", err, err)
}
if len(caller.calls) != 0 {
t.Fatalf("MCP calls before confirmation = %d: %#v, want none", len(caller.calls), caller.calls)
}
}
func TestRecruitJobCreateCallsRemoteOnceWhenConfirmed(t *testing.T) {
caller := withRecruitCaller(t)
path, _ := writeRecruitJobFixture(t)
cmd := newRecruitJobCreateCommand()
cmd.Flags().Bool("yes", false, "确认创建")
cmd.SetArgs([]string{"--from", path, "--yes"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if len(caller.calls) != 1 {
t.Fatalf("MCP calls after confirmation = %d: %#v, want exactly one", len(caller.calls), caller.calls)
}
call := caller.calls[0]
if call.productID != recruitServerID || call.tool != recruitCreateJobTool {
t.Fatalf("dispatch = %s/%s, want %s/%s", call.productID, call.tool, recruitServerID, recruitCreateJobTool)
}
wantArgs := map[string]any{"atsAddJobParam": map[string]any{
"name": "Java 工程师", "description": "服务端开发", "jobNature": "FULL_TIME",
"requiredEdu": float64(1), "minSalary": float64(20000), "maxSalary": float64(35000),
}}
if !reflect.DeepEqual(call.args, wantArgs) {
t.Fatalf("args = %#v, want %#v", call.args, wantArgs)
}
}
func TestRecruitCreateValidatesJobFile(t *testing.T) {
withRecruitCaller(t)
badPath := filepath.Join(t.TempDir(), "bad-job.json")
if err := os.WriteFile(badPath, []byte(`{"name":"缺字段"}`), 0o600); err != nil {
t.Fatal(err)
}
bad := newRecruitJobCreateCommand()
bad.Flags().Bool("yes", false, "确认创建")
bad.SetArgs([]string{"--from", badPath, "--yes"})
if err := bad.Execute(); err == nil || !strings.Contains(err.Error(), "description") {
t.Fatalf("missing field error = %v", err)
}
}
func TestRecruitCommandTree(t *testing.T) {
root := newRecruitCommand()
for _, path := range [][]string{{"job", "list"}, {"job", "get"}, {"job", "create"}} {
cmd, _, err := root.Find(path)
if err != nil || cmd == root || cmd.Name() != path[len(path)-1] {
t.Fatalf("Find(%v) = %v, %v", path, cmd, err)
}
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ func TestCrossPlatformCoveragePublicProductCommandsBuildCompleteUniqueTrees(t *t
for _, want := range []string{
"agoal", "aisearch", "aitable", "attendance", "calendar", "chat",
"contact", "devdoc", "ding", "doc", "drive", "live", "mail",
"markdown", "minutes", "oa", "report", "sheet", "todo", "wiki", "whiteboard",
"markdown", "minutes", "oa", "recruit", "report", "sheet", "todo", "wiki", "whiteboard",
} {
if !seenProducts[want] {
t.Errorf("public product %q was not registered", want)
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package helpers
// 招聘是开源库显式维护的公开命令,不依赖生成式产品注册表。
func init() {
RegisterPublic(func() Handler {
return wukongHandler{name: "recruit", buildFn: newRecruitCommand}
})
}
+11 -5
View File
@@ -43,11 +43,11 @@ func defaultHooks() *Hooks {
}
}
// openSupplementServers returns helper-only MCP endpoints owned by the open
// CLI. They are callable by explicit server ID but are deliberately excluded
// from VisibleProducts, so no top-level product command is generated. They
// stay separate from syncdata.StaticServers because only explicitly wired CLI
// helpers may call them; they are not public MCP product surfaces.
// openSupplementServers returns explicitly wired MCP endpoints owned by the
// open CLI. They are callable by explicit server ID but are deliberately
// excluded from VisibleProducts, so this hook never generates a top-level
// product command. A public command using one of these endpoints must register
// its Cobra tree explicitly via helpers.RegisterPublic.
func openSupplementServers() []ServerInfo {
return []ServerInfo{
{
@@ -60,6 +60,12 @@ func openSupplementServers() []ServerInfo {
Name: "钉钉白板",
Endpoint: "https://mcp-gw.dingtalk.com/server/whiteboard",
},
{
ID: "recruit",
Name: "钉钉招聘",
Endpoint: "https://mcp-gw.dingtalk.com/server/f69b54ada16c57b603c0e5e1c36f464ba73dcee28d64bb701ff2682c259c0cff",
Prefixes: []string{"recruit", "job"},
},
}
}
+8
View File
@@ -107,7 +107,12 @@ func TestOpenSupplementServersIncludesMCPMeta(t *testing.T) {
servers := openSupplementServers()
foundMCPMeta := false
foundWhiteboard := false
foundRecruit := false
for _, server := range servers {
if server.ID == "recruit" {
foundRecruit = server.Endpoint == "https://mcp-gw.dingtalk.com/server/f69b54ada16c57b603c0e5e1c36f464ba73dcee28d64bb701ff2682c259c0cff" &&
len(server.Prefixes) == 2 && server.Prefixes[0] == "recruit" && server.Prefixes[1] == "job"
}
if server.ID == "whiteboard" {
foundWhiteboard = server.Endpoint == "https://mcp-gw.dingtalk.com/server/whiteboard"
}
@@ -128,4 +133,7 @@ func TestOpenSupplementServersIncludesMCPMeta(t *testing.T) {
if !foundWhiteboard {
t.Fatal("openSupplementServers() missing helper-only whiteboard endpoint")
}
if !foundRecruit {
t.Fatal("openSupplementServers() missing explicitly wired recruit endpoint")
}
}
@@ -108,6 +108,10 @@ coverage:
multi_skill: dingtalk-misc
multi_refs:
- references/whiteboard.md
- mono: recruit
multi_skill: dingtalk-misc
multi_refs:
- references/recruit.md
omit_coverage:
- mono: simple
+2
View File
@@ -94,6 +94,7 @@ cli_version: ">=1.0.15"
| `todo` | 待办:创建(含优先级/截止时间/循环)/查询/修改/标记完成/删除 | [todo.md](./references/products/todo.md) |
| `wiki` | 知识库:空间创建/详情/列表/搜索 + 成员管理 + 知识库动态查询 | [wiki.md](./references/products/wiki.md) |
| `whiteboard` | 文档内嵌白板:读取 OpenNodes、追加节点、整页重建 | [whiteboard.md](./references/products/whiteboard.md) |
| `recruit` | 钉钉招聘:查询职位列表、获取职位详情、创建职位 | [recruit.md](./references/products/recruit.md) |
| `event` | 个人 IM/OA 事件:监听消息、群生命周期、审批任务与审批实例事件,NDJSON 输出(实时驱动 Agent)| [event.md](./references/products/event.md) |
## 意图判断决策树
@@ -121,6 +122,7 @@ cli_version: ">=1.0.15"
用户提到"待办/TODO/任务提醒/循环待办" → `todo`
用户提到"创建知识库/知识库列表/搜索知识库空间/wiki/团队空间/知识库成员管理/我的文档个人空间" → `wiki`
用户提到"文档内嵌白板/画布/OpenNodes/白板节点/连接线/整页重建白板" → `whiteboard`;创建空白板卡片先走 `doc whiteboard insert`
用户提到"招聘/职位/JD/在招职位/创建职位/职位详情" → `recruit`
用户提到"监听有人@我/监听单聊或群消息/监听所有单聊或群消息/监听某人发送的消息/监听消息已读/监听消息撤回/监听消息贴表情或表情回应/订阅个人 IM 事件/实时接收钉钉事件/监听并自动回复消息/驱动 Agent 处理消息" → `event +listen-im`;群成员加入/退出、群改名/解散或明确原始 EventKey/Filter DSL → `event consume`
用户提到"监听待我审批的任务/监听审批任务创建、完成或转交/监听审批单发起或终止/监听我发起的审批完成/监听审批实例完成/订阅 OA 事件/event consume user_oa_approval_*" → `event consume`
@@ -0,0 +1,29 @@
# 钉钉招聘
`dws recruit` 提供招聘职位列表、职位详情和职位创建能力。组织、业务标识和当前
操作人由登录身份及 MCP Connector 注入,不要要求用户提供或猜测身份字段。
## 查询
```bash
dws recruit job list --keyword "Java" --status open --size 20 --format json
dws recruit job get --job-id JOB_ID --format json
```
列表状态支持 `draft`、`open`、`invalid`、`closed`。首页不传 `--cursor`;
`hasMore=true` 时把响应 `nextCursor` 回填。查询详情前必须从真实列表结果取得
`jobId`。
## 创建
创建是非幂等写操作。先用 `--dry-run` 检查;实际执行必须经过用户明确确认,并
交由 CLI 的确认流程处理:
```bash
dws recruit job create --from ./job.json --dry-run --format json
dws recruit job create --from ./job.json --format json
```
JSON 必填字段为 `name`、`description`、`jobNature`、`requiredEdu`、
`minSalary`、`maxSalary`。后三者为数字,且最低薪资不得大于最高薪资。文件只写
职位对象,不要添加 MCP 信封字段 `atsAddJobParam`。
+2 -1
View File
@@ -1,6 +1,6 @@
---
name: dingtalk-misc
description: 长尾产品集合技能,覆盖低频钉钉产品:OA审批查询与处理/考勤/直播/DING紧急消息/开放平台应用管理/Agoal目标管理/日志日报周报/电子表格/开放平台文档搜索/文档内嵌白板/DWS技能市场安装/组织大脑Hrbrain/原生Markdown/PAT行为授权/多组织profile。Use when 用户提到上述任一产品,或查待审批/同意拒绝转交撤销审批/打卡/排班/OKR/日报周报/单元格读写/白板节点读写/搜索安装技能/开发者后台应用/人才池/员工档案/职业历程/绩效/原生.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
@@ -31,6 +31,7 @@ metadata:
| 电子表格 / 工作表 / 单元格读写 / 公式 / 超链接 / 浮动图片 | 电子表格创建/读写/公式/超链接/浮动图片/导出 | `dws sheet` | [sheet.md](references/sheet.md) |
| 开放平台文档 / API文档 / 接口文档 / 接口报错 | 开放平台开发文档搜索 | `dws devdoc` | [devdoc.md](references/devdoc.md) |
| 白板 / 画布 / OpenNodes / 白板节点 | 读取和更新钉钉文档中的内嵌白板 | `dws whiteboard` | [whiteboard.md](references/whiteboard.md) |
| 招聘 / 职位 / JD / 在招职位 / 创建职位 / 职位详情 | 钉钉招聘职位的查询、详情与创建 | `dws recruit` | [recruit.md](references/recruit.md) |
| 搜索技能 / 找技能 / 安装技能 / 技能市场 / 安装 DWS mono 或 multi skill | DWS 技能市场搜索、下载、安装与内置技能部署 | `dws skill` | [skill.md](references/skill.md) |
| 人才池 / 储备干部池 / 员工档案 / 职业历程 / 绩效记录 / 员工标签 / 组织大脑 / 人才搜索 | 组织大脑:人才池、员工档案专项模块与结构化人才搜索 | `dws hrbrain` | [hrbrain.md](references/hrbrain.md) |
| 原生 Markdown / `.md` 原文 / 覆盖 Markdown / 局部替换 Markdown | 原生 `.md` 文件读取、创建、全量覆盖与局部替换 | `dws markdown` | [markdown.md](references/markdown.md) |
@@ -0,0 +1,64 @@
# 钉钉招聘
`dws recruit` 查询和创建钉钉招聘职位。当前公开命令只覆盖职位列表、职位详情和
职位创建;候选人、面试、Offer、职位修改、开放或关闭尚未作为稳定命令发布。
所有命令都应加 `--format json`。组织、业务标识和当前操作人由登录身份及 MCP
Connector 注入,不要要求用户提供或猜测 `corpId`、`bizCode`、`opUserId`。
## 查询职位列表
```bash
dws recruit job list --format json
dws recruit job list --keyword "Java" --status open --size 20 --format json
dws recruit job list --job-ids JOB_ID_1,JOB_ID_2 --format json
```
可选筛选包括 `--job-ids`、`--required-edu`、`--status`、`--job-nature`、
`--campus`、`--start-modified-time`、`--end-modified-time`、
`--creator-user-ids`、`--keyword`、`--category`。状态接受:
- `draft`:草稿
- `open`:招聘中
- `invalid`:已失效
- `closed`:已关闭/完成
`--size` 默认 20,范围 1–100。首页不传 `--cursor`;响应 `hasMore=true` 时,
把 `nextCursor` 回填到下一次查询。
## 查询职位详情
先从列表结果取得真实 `jobId`,不得编造:
```bash
dws recruit job get --job-id JOB_ID --format json
```
## 创建职位
创建是非幂等远端写入。先准备只包含职位对象的 UTF-8 JSON 文件,并用
`--dry-run` 检查完整调用;向用户展示职位名称、性质、薪资和负责人,取得明确
确认后,交由 CLI 的确认流程执行实际创建。不要在存储示例中加入确认绕过参数。
```bash
dws recruit job create --from ./job.json --dry-run --format json
dws recruit job create --from ./job.json --format json
```
最小 `job.json`:
```json
{
"name": "Java 开发工程师",
"description": "负责服务端系统开发",
"jobNature": "FULL_TIME",
"requiredEdu": 1,
"minSalary": 20000,
"maxSalary": 35000
}
```
六个字段均为必填;`requiredEdu`、`minSalary`、`maxSalary` 必须为数字,且
`minSalary` 不得大于 `maxSalary`。可选字段以当前
`dws recruit job create --help` 和 leaf Schema 为准;不要把 MCP 信封字段
`atsAddJobParam` 写进文件,CLI 会自动包装。
@@ -22,7 +22,7 @@ func TestRootHelpHidesRecoveredModules(t *testing.T) {
// All of these commands should be hidden because they are not in
// the dynamic server discovery product list.
for _, module := range []string{
"aisearch", "contract", "recruit",
"aisearch", "contract",
"chat", "drive", "minutes", "mail", "credit",
"credit-risk", "finance", "message", "notify", "doc",
} {
@@ -30,4 +30,7 @@ func TestRootHelpHidesRecoveredModules(t *testing.T) {
t.Fatalf("root help should not show hidden module %q:\n%s", module, got)
}
}
if !strings.Contains(got, "● recruit ") {
t.Fatalf("root help should show explicitly registered recruit module:\n%s", got)
}
}