From d63f7e58366d5473cfd4aadd1027ee6a28917fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:35:05 +0800 Subject: [PATCH] fix(devapp): robot create routes to async submit/poll (issue #35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The synchronous `create_dingtalk_robot` MCP action is broken server-side: it dispatches to a connect-engine action template (detailId G-ACT-... / versionId G-ACT-VER-...) that the engine can no longer find, returning PARAM_ERROR / "找不到执行动作" for every caller (the orgId in the error is hardcoded server-side, not the caller's). The async path (submit_robot_create_task + query_robot_create_result) still works. `dws devapp robot create` now submits the async task and polls the result until a terminal status, returning the same agentId/clientId/robotCode/ clientSecret payload. User experience is unchanged — one command still creates the robot. --dry-run previews the submit call. - runDevAppRobotCreate orchestrates submit + interval-based polling (pending set WAITING/PROCESSING/RUNNING/PENDING/INIT/DOING, max 24 polls). - Tests: create routing now expects submit; add poll-until-terminal and status-classification unit tests. - Docs/skills updated to reflect the new routing. --- docs/devapp-yulan-command-routing.md | 4 +- internal/helpers/devapp.go | 112 +++++++++++++++++- internal/helpers/devapp_test.go | 93 ++++++++++++++- skills/dingtalk-devapp/references/robot.md | 4 +- skills/mono/references/products/devapp.md | 6 +- .../multi/dingtalk-devapp/references/robot.md | 4 +- 6 files changed, 207 insertions(+), 16 deletions(-) diff --git a/docs/devapp-yulan-command-routing.md b/docs/devapp-yulan-command-routing.md index ced85339..c91dcf69 100644 --- a/docs/devapp-yulan-command-routing.md +++ b/docs/devapp-yulan-command-routing.md @@ -140,7 +140,7 @@ Do not send confirmation fields such as `confirmCreate`, `confirmUpdate`, `confi | Permission apply | `devapp permission add` | `apply_open_dev_app_permissions` | `OpenInnerAppPermissionFacade.apply` | Implemented | Hardcoded helper | | Permission remove | `devapp permission remove` | `remove_open_dev_app_permission` | `OpenInnerAppPermissionFacade.remove` | Implemented | Hardcoded helper | | Events | `devapp event list/config` | `list/config_open_dev_app_events` | Event facade pending | Pending | Spec only | -| Robot create | `devapp robot create/submit/result` | `create_dingtalk_robot`/`submit_robot_create_task`/`query_robot_create_result` | Implemented | Hardcoded helper | +| Robot create | `devapp robot create/submit/result` | `submit_robot_create_task`/`query_robot_create_result` (sync `create_dingtalk_robot` broken server-side, see issue #35) | Implemented | Hardcoded helper | | Robot config | `devapp robot get/config/update/enable/offline` | `get/create/update_open_dev_app_robot_config`, `enable/offline_open_dev_app_robot` | Implemented | Hardcoded helper | | Version | `devapp version create/list/get/check-approval/publish/status` | `create/list_open_dev_app_versions`, `get_open_dev_app_version_detail/status`, `publish_open_dev_app_version` | Implemented | Hardcoded helper | @@ -1030,7 +1030,7 @@ Implemented robot tools: | CLI | MCP tool | Notes | | --- | --- | --- | -| `devapp robot create` | `create_dingtalk_robot` | Synchronously create a new agent app + robot. | +| `devapp robot create` | `submit_robot_create_task` + `query_robot_create_result` | Create a new agent app + robot via the async submit/poll flow (the legacy sync `create_dingtalk_robot` action is broken server-side — see issue #35). | | `devapp robot submit` | `submit_robot_create_task` | Async submit; supports retry via original `taskId`. | | `devapp robot result` | `query_robot_create_result` | Poll async task by `taskId`. | | `devapp robot get` | `get_open_dev_app_robot_config` | Read robot config of an existing app. | diff --git a/internal/helpers/devapp.go b/internal/helpers/devapp.go index 62025e05..3212410d 100644 --- a/internal/helpers/devapp.go +++ b/internal/helpers/devapp.go @@ -17,6 +17,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" @@ -34,6 +35,12 @@ const ( devAppSecurityConfigTool = "update_app_security_config" // 机器人能力(op-app MCP 工具,硬编码不走服务发现)。 + // + // devAppRobotCreateTool(同步 create_dingtalk_robot)的服务端实现引用了一个 + // connect-engine 动作模板(detailId G-ACT-...),该模板当前在引擎里已找不到, + // 对所有调用方都返回 PARAM_ERROR / "找不到执行动作"(见 issue #35)。因此 + // `dws devapp robot create` 不再调用它,改用 submit + 轮询 result 的异步路径, + // 二者返回的最终结果一致(agentId/clientId/robotCode/clientSecret)。 devAppRobotCreateTool = "create_dingtalk_robot" devAppRobotSubmitTool = "submit_robot_create_task" devAppRobotResultTool = "query_robot_create_result" @@ -663,7 +670,7 @@ func newDevAppSecurityConfigCommand(runner executor.Runner) *cobra.Command { func newDevAppRobotCreateCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ Use: "create", - Short: "新建钉钉智能体机器人(一次性同步创建应用+机器人)", + Short: "新建钉钉智能体机器人(提交创建任务并等待完成)", Example: " dws devapp robot create --app-name 我的智能体 --robot-name 小助手 --desc \"处理审批问答\" --dry-run --format json", Args: cobra.NoArgs, DisableAutoGenTag: true, @@ -675,7 +682,7 @@ func newDevAppRobotCreateCommand(runner executor.Runner) *cobra.Command { if err != nil { return err } - return runDevAppTool(runner, cmd, devAppRobotCreateTool, params) + return runDevAppRobotCreate(runner, cmd, params) }, } registerDevAppRobotCreateFlags(cmd) @@ -841,6 +848,107 @@ func devAppRobotCreateParams(cmd *cobra.Command) (map[string]any, error) { return params, nil } +// runDevAppRobotCreate 实现 `dws devapp robot create`。 +// +// 同步工具 create_dingtalk_robot 的服务端动作模板已失效(issue #35),因此这里 +// 改走异步路径:submit_robot_create_task 拿 taskId,再按服务端给的 interval 轮询 +// query_robot_create_result 直到任务进入终态,最后输出与同步接口一致的结果 +// (agentId/clientId/robotCode/clientSecret)。用户感知仍是“一条命令建出机器人”。 +func runDevAppRobotCreate(runner executor.Runner, cmd *cobra.Command, params map[string]any) error { + // submit_robot_create_task 的 schema 把图标字段标为必填(空值时服务端用默认图标), + // 即使用户未提供也补空串占位。 + if _, ok := params["robotMediaId"]; !ok { + params["robotMediaId"] = "" + } + if _, ok := params["previewMediaId"]; !ok { + params["previewMediaId"] = "" + } + + // dry-run 直接预览将要提交的 submit 调用(这正是真实会发生的事)。 + if commandDryRun(cmd) { + return runDevAppTool(runner, cmd, devAppRobotSubmitTool, params) + } + + legacyPath := cobracmd.LegacyCommandPath(cmd) + submitInv := executor.NewHelperInvocation(legacyPath, devAppProduct, devAppRobotSubmitTool, params) + submitRes, err := runner.Run(cmd.Context(), submitInv) + if err != nil { + return err + } + + taskID := devAppRobotResultString(submitRes, "taskId") + if taskID == "" { + // 拿不到 taskId 说明服务端返回结构异常,原样回吐 submit 结果由用户判断。 + return writeCommandPayload(cmd, submitRes) + } + + last := submitRes + interval := devAppRobotResultInterval(submitRes) + const maxAttempts = 24 // 配合 interval(默认 5s)约 2 分钟内收敛 + for attempt := 0; attempt < maxAttempts; attempt++ { + if !devAppRobotStatusPending(devAppRobotResultString(last, "status")) { + break + } + select { + case <-cmd.Context().Done(): + return cmd.Context().Err() + case <-time.After(interval): + } + pollInv := executor.NewHelperInvocation(legacyPath, devAppProduct, devAppRobotResultTool, map[string]any{"taskId": taskID}) + pollRes, pollErr := runner.Run(cmd.Context(), pollInv) + if pollErr != nil { + return pollErr + } + last = pollRes + if next := devAppRobotResultInterval(pollRes); next > 0 { + interval = next + } + } + return writeCommandPayload(cmd, last) +} + +// devAppRobotResultBody 取出工具返回里的业务 result 对象 +// (形如 {"errorCode":null,"result":{...},"success":true} 中的 result)。 +func devAppRobotResultBody(res executor.Result) map[string]any { + content, ok := res.Response["content"].(map[string]any) + if !ok { + return nil + } + body, _ := content["result"].(map[string]any) + return body +} + +func devAppRobotResultString(res executor.Result, key string) string { + body := devAppRobotResultBody(res) + if body == nil { + return "" + } + value, _ := body[key].(string) + return strings.TrimSpace(value) +} + +// devAppRobotResultInterval 读取服务端建议的轮询间隔(秒),缺省 5 秒。 +func devAppRobotResultInterval(res executor.Result) time.Duration { + body := devAppRobotResultBody(res) + if body != nil { + if raw, ok := body["interval"].(float64); ok && raw > 0 { + return time.Duration(raw * float64(time.Second)) + } + } + return 5 * time.Second +} + +// devAppRobotStatusPending 判断创建任务是否仍在进行中(需要继续轮询)。 +// 已知终态:SUCCESS / APPROVAL_REQUIRED / FAILED / ERROR 等;未知或空状态保守按终态处理,避免死等。 +func devAppRobotStatusPending(status string) bool { + switch strings.ToUpper(strings.TrimSpace(status)) { + case "WAITING", "PROCESSING", "RUNNING", "PENDING", "INIT", "DOING": + return true + default: + return false + } +} + func registerDevAppRobotConfigFlags(cmd *cobra.Command) { cmd.Flags().String("name", "", "机器人名称") cmd.Flags().String("brief", "", "机器人简介") diff --git a/internal/helpers/devapp_test.go b/internal/helpers/devapp_test.go index b672d2e3..8bd77b10 100644 --- a/internal/helpers/devapp_test.go +++ b/internal/helpers/devapp_test.go @@ -129,13 +129,17 @@ func TestDevAppRobotCommandsBuildToolParams(t *testing.T) { wantParams map[string]any }{ { - name: "create sync", + // create 不再调用失效的同步 create_dingtalk_robot(issue #35), + // 改为提交异步 submit_robot_create_task 任务(并补图标占位),随后轮询结果。 + name: "create routes to async submit", args: []string{"robot", "create", "--app-name", "智能体", "--robot-name", "小助手", "--desc", "审批问答", "--yes"}, - wantTool: "create_dingtalk_robot", + wantTool: "submit_robot_create_task", wantParams: map[string]any{ - "appName": "智能体", - "robotName": "小助手", - "desc": "审批问答", + "appName": "智能体", + "robotName": "小助手", + "desc": "审批问答", + "robotMediaId": "", + "previewMediaId": "", }, }, { @@ -209,6 +213,85 @@ func TestDevAppRobotCommandsBuildToolParams(t *testing.T) { } } +// sequencedRobotRunner returns a scripted response per call so we can exercise +// the create → submit → poll-until-terminal loop without real network calls. +type sequencedRobotRunner struct { + responses []map[string]any + calls []executor.Invocation +} + +func (r *sequencedRobotRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) { + idx := len(r.calls) + r.calls = append(r.calls, invocation) + resp := map[string]any{} + if idx < len(r.responses) { + resp = r.responses[idx] + } else if len(r.responses) > 0 { + resp = r.responses[len(r.responses)-1] + } + invocation.Implemented = true + return executor.Result{Invocation: invocation, Response: resp}, nil +} + +func robotToolResponse(body map[string]any) map[string]any { + return map[string]any{"content": map[string]any{"result": body}} +} + +func TestDevAppRobotCreatePollsUntilTerminal(t *testing.T) { + runner := &sequencedRobotRunner{ + responses: []map[string]any{ + // submit_robot_create_task → task accepted, still pending. + robotToolResponse(map[string]any{"taskId": "t-1", "status": "WAITING", "interval": 0.01}), + // first poll → still pending. + robotToolResponse(map[string]any{"taskId": "t-1", "status": "PROCESSING", "interval": 0.01}), + // second poll → terminal, carries credentials. + robotToolResponse(map[string]any{"taskId": "t-1", "status": "APPROVAL_REQUIRED", "agentId": "4680653193", "robotCode": "ding-test"}), + }, + } + root := newDevAppTestRoot(runner) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"devapp", "robot", "create", "--app-name", "智能体", "--robot-name", "小助手", "--desc", "审批问答", "--yes"}) + + if err := root.Execute(); err != nil { + t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) + } + + if len(runner.calls) != 3 { + t.Fatalf("expected submit + 2 polls = 3 calls, got %d", len(runner.calls)) + } + if got := runner.calls[0].Tool; got != "submit_robot_create_task" { + t.Fatalf("first call tool = %q, want submit_robot_create_task", got) + } + for i := 1; i < 3; i++ { + if got := runner.calls[i].Tool; got != "query_robot_create_result" { + t.Fatalf("poll call %d tool = %q, want query_robot_create_result", i, got) + } + if got, _ := runner.calls[i].Params["taskId"].(string); got != "t-1" { + t.Fatalf("poll call %d taskId = %q, want t-1", i, got) + } + } + if !strings.Contains(out.String(), "APPROVAL_REQUIRED") || !strings.Contains(out.String(), "4680653193") { + t.Fatalf("final output missing terminal result, got:\n%s", out.String()) + } +} + +func TestDevAppRobotStatusPending(t *testing.T) { + pending := []string{"WAITING", "processing", " Running ", "PENDING", "INIT", "DOING"} + for _, s := range pending { + if !devAppRobotStatusPending(s) { + t.Errorf("status %q should be pending", s) + } + } + terminal := []string{"SUCCESS", "APPROVAL_REQUIRED", "FAILED", "ERROR", "", "unknown"} + for _, s := range terminal { + if devAppRobotStatusPending(s) { + t.Errorf("status %q should be terminal", s) + } + } +} + func TestDevAppVersionCommandsBuildToolParams(t *testing.T) { cases := []struct { name string diff --git a/skills/dingtalk-devapp/references/robot.md b/skills/dingtalk-devapp/references/robot.md index 11b51755..a50f2089 100644 --- a/skills/dingtalk-devapp/references/robot.md +++ b/skills/dingtalk-devapp/references/robot.md @@ -9,14 +9,14 @@ ## 一、新建智能体机器人 -### 同步创建 +### 一条命令创建(推荐) ```bash dws devapp robot create --app-name 我的智能体 --robot-name 小助手 --desc "处理审批问答" --dry-run --format json dws devapp robot create --app-name 我的智能体 --robot-name 小助手 --desc "处理审批问答" --yes --format json ``` -MCP tool: `create_dingtalk_robot`。成功返回 `agentId / robotCode / clientId / clientSecret`(凭据按敏感信息处理)。 +`create` 会自动提交异步创建任务并轮询直到完成,最终返回 `agentId / robotCode / clientId / clientSecret`(凭据按敏感信息处理)。底层走 `submit_robot_create_task` + `query_robot_create_result`(同步工具 `create_dingtalk_robot` 服务端动作已失效,见 issue #35,不再使用)。 | CLI | MCP | 必填 | 说明 | |-----|-----|------|------| diff --git a/skills/mono/references/products/devapp.md b/skills/mono/references/products/devapp.md index 726facd9..b546ca58 100644 --- a/skills/mono/references/products/devapp.md +++ b/skills/mono/references/products/devapp.md @@ -195,15 +195,15 @@ MCP tool: `update_app_security_config` ### 新建智能体机器人 ```bash -# 同步创建(返回 agentId/robotCode/clientId/clientSecret) +# 一条命令创建(自动提交+轮询,返回 agentId/robotCode/clientId/clientSecret) dws devapp robot create --app-name 我的智能体 --robot-name 小助手 --desc "处理审批问答" --dry-run --format json -# 异步创建 + 查询 +# 手动异步创建 + 查询(需要自己控制重试节奏时用) dws devapp robot submit --app-name 我的智能体 --robot-name 小助手 --desc "处理审批问答" --dry-run --format json dws devapp robot result --task-id TASK_ID --format json ``` -MCP tools: `create_dingtalk_robot` / `submit_robot_create_task` / `query_robot_create_result`。`submit` 失败可带原 `--task-id` 重试。 +MCP tools: `submit_robot_create_task` / `query_robot_create_result`。`create` 内部即走这两个工具(自动轮询直到终态);`submit` 失败可带原 `--task-id` 重试。同步工具 `create_dingtalk_robot` 服务端动作已失效(issue #35),不再使用。 异步创建任务状态: diff --git a/skills/multi/dingtalk-devapp/references/robot.md b/skills/multi/dingtalk-devapp/references/robot.md index 11b51755..a50f2089 100644 --- a/skills/multi/dingtalk-devapp/references/robot.md +++ b/skills/multi/dingtalk-devapp/references/robot.md @@ -9,14 +9,14 @@ ## 一、新建智能体机器人 -### 同步创建 +### 一条命令创建(推荐) ```bash dws devapp robot create --app-name 我的智能体 --robot-name 小助手 --desc "处理审批问答" --dry-run --format json dws devapp robot create --app-name 我的智能体 --robot-name 小助手 --desc "处理审批问答" --yes --format json ``` -MCP tool: `create_dingtalk_robot`。成功返回 `agentId / robotCode / clientId / clientSecret`(凭据按敏感信息处理)。 +`create` 会自动提交异步创建任务并轮询直到完成,最终返回 `agentId / robotCode / clientId / clientSecret`(凭据按敏感信息处理)。底层走 `submit_robot_create_task` + `query_robot_create_result`(同步工具 `create_dingtalk_robot` 服务端动作已失效,见 issue #35,不再使用)。 | CLI | MCP | 必填 | 说明 | |-----|-----|------|------|