// Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package smart import ( "encoding/json" "fmt" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" todoshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/todo" ) // AssignMulti: create ONE personal todo and assign it to SEVERAL people at once, // addressing every executor by NAME instead of by userId. // // Steps: split the --to name CSV, resolve each name to a unique userId // (resolveUser, which never guesses — it errors on unknown/ambiguous names), // collect ALL resolution errors first and abort if any name failed, so we never // create a half-assigned todo. Then create the todo once via create_personal_todo // with every resolved userId in executorIds. Replaces the manual dance of // `contact user search` per name → copy each userId → // `todo task create --executors id1,id2,... --title ...`. // // tool name + params (create_personal_todo, PersonalTodoCreateVO.subject / // .executorIds) are copied verbatim from the todo helper's `task create` call // site. // // dws todo +assign-multi --to "张三,李四,王五" --task "周五前提交排期" var AssignMulti = shortcut.Shortcut{ OutputRollout: output.RolloutUnifiedActive, Service: "todo", Command: "+assign-multi", Product: "todo", Description: "把一条待办按姓名一次性指派给多个人(自动把每个姓名解析成 userId)", Intent: "当你想把同一条待办同时指派给好几个同事、但手上只有他们的姓名而不是 userId 时使用;" + "内部会把 --to 里的每个姓名逐个解析成唯一 userId,只要有任何一个姓名查不到或者重名有歧义," + "就把这些问题一次性汇总报错、并且完全不创建待办(不会建出只指派了一半人的残缺待办)。" + "全部姓名都解析成功后,才用这些 userId 一次性创建这条待办并指派给所有人。会真实创建一条新的待办。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "todo", Name: "shortcut_assign_multi", CanonicalPath: "todo.shortcut_assign_multi", CLIPath: "todo +assign-multi", PrimaryCLIPath: "todo +assign-multi", }, Description: "把一条待办按姓名一次性指派给多个人(自动把每个姓名解析成 userId)", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"已验证的多人指派待办","properties":{"taskId":{"type":"string","description":"新待办稳定 taskId"},"subject":{"type":"string","description":"待办标题"},"executors":{"type":"array","description":"已解析执行人摘要","items":{"type":"string"}},"count":{"type":"integer","description":"执行人数"},"verified":{"type":"boolean","description":"是否完成详情读回核验"}},"required":["taskId","subject","executors","count","verified"],"additionalProperties":false}`)}, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "把一条待办按姓名一次性指派给多个人(自动把每个姓名解析成 userId)", UseWhen: []string{"当你想把同一条待办同时指派给好几个同事、但手上只有他们的姓名而不是 userId 时使用;内部会把 --to 里的每个姓名逐个解析成唯一 userId,只要有任何一个姓名查不到或者重名有歧义,就把这些问题一次性汇总报错、并且完全不创建待办(不会建出只指派了一半人的残缺待办)。全部姓名都解析成功后,才用这些 userId 一次性创建这条待办并指派给所有人。会真实创建一条新的待办。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws todo +assign-multi --to \"张三,李四\" --task \"周五前提交排期\""}, }, }, Flags: []shortcut.Flag{ {Name: "to", Type: shortcut.FlagStringSlice, Desc: "执行人姓名/花名,逗号分隔(如 张三,李四)", Required: true}, {Name: "task", Type: shortcut.FlagString, Desc: "待办标题", Required: true}, }, Tips: []string{ `dws todo +assign-multi --to "张三,李四" --task "周五前提交排期"`, }, Execute: func(rt *shortcut.RuntimeContext) error { task := strings.TrimSpace(rt.Str("task")) if task == "" { return apperrors.NewValidation("--task 不能为空(待办标题)") } // Normalize the --to name list, dropping blanks. var names []string for _, n := range rt.StrSlice("to") { if n = strings.TrimSpace(n); n != "" { names = append(names, n) } } if len(names) == 0 { return apperrors.NewValidation("--to 不能为空,请至少提供一个执行人姓名") } // Resolve every name up front. Collect all failures and abort before any // write, so we never create a todo assigned to only some of the people. var ( executorIDs []string resolved []string failures []string ) for _, name := range names { user, err := resolveUser(rt, name) if err != nil { failures = append(failures, fmt.Sprintf("%s: %s", name, err.Error())) continue } executorIDs = append(executorIDs, user.userID) resolved = append(resolved, fmt.Sprintf("%s(%s)", user.name, user.userID)) } if len(failures) > 0 { return apperrors.NewValidation(fmt.Sprintf( "以下执行人姓名无法唯一解析,已中止、未创建任何待办:\n%s", strings.Join(failures, "\n"))) } if len(executorIDs) == 0 { return apperrors.NewValidation("没有解析出任何有效的执行人 userId,已中止") } // Create the todo once, assigning all resolved executors. Params mirror the // todo helper's `task create` call site verbatim. params := map[string]any{ "PersonalTodoCreateVO": map[string]any{ "subject": task, "executorIds": executorIDs, }, } if rt.DryRun() { return rt.Output(map[string]any{"dryRun": true, "executed": false, "subject": task, "count": len(executorIDs)}) } data, err := rt.CallMCPWriteDataStrict("todo", "create_personal_todo", params) if err != nil { return err } taskID, _, err := todoshortcut.VerifyCreatedTodo(rt, data, "todo/create_personal_todo", task) if err != nil { return err } return rt.Output(map[string]any{ "taskId": taskID, "subject": task, "executors": resolved, "count": len(executorIDs), "verified": true, }) }, } func init() { shortcut.Register(AssignMulti) }