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:
@@ -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
@@ -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", "", "机器人简介")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 | 必填 | 说明 |
|
||||
|-----|-----|------|------|
|
||||
|
||||
@@ -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 | 必填 | 说明 |
|
||||
|-----|-----|------|------|
|
||||
|
||||
Reference in New Issue
Block a user