fix(devapp): robot create routes to async submit/poll (issue #35)

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.
This commit is contained in:
修雨
2026-06-16 09:35:05 +08:00
parent 1f21fdf7be
commit d63f7e5836
6 changed files with 207 additions and 16 deletions
+2 -2
View File
@@ -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. |
+110 -2
View File
@@ -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", "", "机器人简介")
+88 -5
View File
@@ -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
+2 -2
View File
@@ -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 | 必填 | 说明 |
|-----|-----|------|------|
+3 -3
View File
@@ -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),不再使用。
异步创建任务状态:
@@ -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 | 必填 | 说明 |
|-----|-----|------|------|