From f2b2de89f4cc7db99ef7616a82fbeb366c4b19dd Mon Sep 17 00:00:00 2001 From: wxianfeng Date: Thu, 18 Jun 2026 18:34:12 +0800 Subject: [PATCH] fix cr problem --- docs/devapp-yulan-command-routing.md | 16 ++--- internal/app/runner.go | 36 ++++++++++- internal/app/runner_test.go | 14 +++++ internal/helpers/devapp.go | 20 ++++-- internal/helpers/devapp_pretty.go | 16 ++--- internal/helpers/devapp_pretty_test.go | 9 ++- internal/helpers/devapp_test.go | 26 ++++++++ internal/transport/diagnostics.go | 61 ++++++++++++++++--- internal/transport/diagnostics_test.go | 20 ++++++ skills/multi/dingtalk-dev/SKILL.md | 2 +- skills/multi/dingtalk-dev/references/event.md | 7 ++- skills/multi/dingtalk-dev/references/robot.md | 17 +++--- .../multi/dingtalk-dev/references/version.md | 14 +++-- .../multi/dingtalk-devapp/references/event.md | 27 ++++---- 14 files changed, 219 insertions(+), 66 deletions(-) diff --git a/docs/devapp-yulan-command-routing.md b/docs/devapp-yulan-command-routing.md index 90965651..110ede95 100644 --- a/docs/devapp-yulan-command-routing.md +++ b/docs/devapp-yulan-command-routing.md @@ -138,7 +138,7 @@ Do not send confirmation fields such as `confirmCreate`, `confirmUpdate`, `confi | Permission list | `devapp permission list/search/detail` | `list_dev_app_permissions` | `OpenInnerAppPermissionFacade.list` | Implemented | Hardcoded helper | | Permission apply | `devapp permission add` | `apply_dev_app_permissions` | `OpenInnerAppPermissionFacade.apply` | Implemented | Hardcoded helper | | Permission remove | `devapp permission remove` | `remove_dev_app_permissions` | `OpenInnerAppPermissionFacade.remove` | Implemented | Hardcoded helper | -| Events | `devapp event list/subscribe/unsubscribe` | `list_open_dev_app_events`, `subscribe/unsubscribe_open_dev_app_event` | Implemented | Hardcoded helper | +| Events | `devapp event list/subscribe/unsubscribe` | `list_dev_app_events`, `subscribe_dev_app_events`, `unsubscribe_dev_app_events` | Implemented | Hardcoded helper | | Robot create | `devapp robot create/submit/result` | `create_dingtalk_robot`/`submit_robot_create_task`/`query_robot_create_result` | Implemented | Hardcoded helper | | Robot config | `devapp robot get/config/enable/disable` | `get_extension_robot_config`, `set_extension_robot_config`, `enable_dev_app_robot`, `disable_dev_app_robot` | Implemented | Hardcoded helper | | Version | `devapp version create/list/get/check-approval/publish/status` | `create_dev_app_version`, `list_dev_app_versions`, `get_dev_app_version_detail`, `publish_dev_app_version`, `get_dev_app_version_status` | Implemented | Hardcoded helper | @@ -953,9 +953,9 @@ real model is "list subscribable events + subscribe/unsubscribe events by | CLI | MCP tool | Notes | | --- | --- | --- | -| `devapp event list` | `list_dev_app_events` | Args `unifiedAppId`; returns `pushType` + `events[]` (`eventCode`/`eventName`/`subscribed`). Read. | -| `devapp event subscribe` | `subscribe_dev_app_events` | Args `unifiedAppId` + `eventCodes`. Write (`--dry-run`/`--yes`). | -| `devapp event unsubscribe` | `unsubscribe_dev_app_events` | Args `unifiedAppId` + `eventCodes`. Write. | +| `dev app event list` | `list_dev_app_events` | Args `unifiedAppId/cursor/pageSize`; returns `pushType=STREAM` + `events[]` (`eventCode`/`eventName`/`subscribed`) + `hasMore/nextCursor/pageSize`. Read. | +| `dev app event subscribe` | `subscribe_dev_app_events` | Args `unifiedAppId` + required array `eventCodes`. Returns `success/operation/eventCodes/needsPublish/versionRequiredAction`; failures include `errorCode/errorMsg/reason/retryable/action` such as `STREAM_NOT_CONNECTED`. Write (`--dry-run`/`--yes`). | +| `dev app event unsubscribe` | `unsubscribe_dev_app_events` | Args `unifiedAppId` + required array `eventCodes`. Returns `success/operation/eventCodes/needsPublish/versionRequiredAction`; failures include `errorCode/errorMsg/reason/retryable/action`. Write. | Note: for gray unified apps, subscribe/unsubscribe are staged into version metadata and only take effect after `version publish`. The event callback URL @@ -969,8 +969,8 @@ Implemented version tools (verified against the `op-app` MCP `tools/list`): | `devapp version create` | `create_dev_app_version` | Save a new version from current config. Omit `version` by default so the server auto-increments from the latest released version; only pass `version` when the user explicitly requests a higher version. | | `devapp version list` | `list_dev_app_versions` | Cursor-paged list (`cursor`, `pageSize`; response `items/nextCursor/hasMore`). | | `devapp version get` | `get_dev_app_version_detail` | One version detail by `versionId`. | -| `devapp version check-approval` | `publish_dev_app_version` (`precheckOnly=true`) | Precheck approval requirement / approvers; does not publish. | -| `devapp version publish` | `publish_dev_app_version` (`precheckOnly=false`) | Publish; `confirmedSensitive` for high-sensitivity scopes, optional `approverUserId`. | +| `devapp version check-approval` | `publish_dev_app_version` (`precheckOnly=true`) | Precheck approval requirement / approvers via `requiresApproval/publishable/approvalMode/approvalCandidates`; does not publish. | +| `devapp version publish` | `publish_dev_app_version` (`precheckOnly=false`) | Publish or submit approval; inspect `published/approvalSubmitted/processId/versionStatus`. | | `devapp version status` | `get_dev_app_version_status` | Poll publish and approval state. | Note: the MCP `precheckOnly` field is the server-side approval precheck and maps to @@ -1086,7 +1086,7 @@ Yulan invocation examples: | "把应用图标换成 ICON_RESOURCE" | `dws devapp update --unified-app-id ... --icon ICON_RESOURCE --dry-run --format json` | Icon change is app base info update and must be guarded. | | "已确认删除这个开放平台应用" | `dws devapp delete --unified-app-id ... --yes --format json` | Destructive write only after unique app resolution and confirmation. | | "手机号权限要申请,虽然需要审核" | `dws devapp permission add --permissions Contact.User.mobile --dry-run/--yes` | `requiredApproval=true` is still appliable; approval moves to version publish. | -| "订阅通讯录用户增加事件" | `dws devapp event list ...` then `dws devapp event subscribe --event-codes user_add_org --dry-run --format json` | Pick eventCode from list, then subscribe; gray apps need a version publish to take effect. | +| "订阅通讯录用户增加事件" | `dws dev app event list ...` then `dws dev app event subscribe --event-codes user_add_org --dry-run --format json` | Pick eventCode from list, then subscribe; gray apps need a version publish to take effect. | | "保存版本并看审核状态" | `version create`, `check-approval`, `publish`, `status` | Version flow owns approver selection and publish audit state. | | "创建 MCP 服务并配置 HSF tool" | OpenDev MCP platform workflow, not `dws devapp create` | MCP connector/tool is a platform artifact, not an enterprise internal app. | @@ -1209,7 +1209,7 @@ Agent should normalize behavior without hiding raw backend fields. | `PERMISSION_ALREADY_AUTHED` | Add receives an already authorized scope | Report skipped scope and continue only for remaining unauth scopes. | | `PERMISSION_NOT_AUTHED` | Remove receives an unauthorized scope | Report that there is nothing to remove. | | `PERMISSION_NOT_EDITABLE` | `canEdit=false` or backend no-edit error | Show the no-edit reason; do not retry as a write. | -| `VERSION_APPROVAL_REQUIRED` | `check-approval` says approver is required | Ask for approver or use returned candidate when the user gave an explicit approver. | +| `requiresApproval=true` | `check-approval` says approval is required | Use `approvalMode/approvalCandidates`; ask for approver only in `SELECT_APPROVER` mode. | Raw `errorCode/errorMsg` must remain in JSON output or the Agent response so the backend owner can diagnose actual HSF failures. diff --git a/internal/app/runner.go b/internal/app/runner.go index e9b68daa..e13894d9 100644 --- a/internal/app/runner.go +++ b/internal/app/runner.go @@ -726,13 +726,23 @@ func resolveIdentityHeaders() map[string]string { // errors (success=false + errorCode/errorMsg) that are not flagged at the MCP // protocol level. Returns the error message, or "" if the response is OK. func detectBusinessError(content map[string]any) string { + return detectBusinessErrorAtDepth(content, 0) +} + +func detectBusinessErrorAtDepth(content map[string]any, depth int) string { + if content == nil || depth > 8 { + return "" + } success, ok := content["success"] if !ok { - return "" + return detectNestedBusinessError(content, depth) } b, ok := success.(bool) if !ok || b { - return "" + return detectNestedBusinessError(content, depth) + } + if nested := detectNestedBusinessError(content, depth); nested != "" { + return nested } if msg, ok := content["errorMsg"].(string); ok && strings.TrimSpace(msg) != "" { return strings.TrimSpace(msg) @@ -743,6 +753,28 @@ func detectBusinessError(content map[string]any) string { return "business error: success=false" } +func detectNestedBusinessError(content map[string]any, depth int) string { + for _, key := range []string{"content", "result", "data"} { + switch child := content[key].(type) { + case map[string]any: + if msg := detectBusinessErrorAtDepth(child, depth+1); msg != "" { + return msg + } + case []any: + for _, item := range child { + childMap, ok := item.(map[string]any) + if !ok { + continue + } + if msg := detectBusinessErrorAtDepth(childMap, depth+1); msg != "" { + return msg + } + } + } + } + return "" +} + // extractMCPErrorMessage builds an error message from a ToolCallResult with // isError=true. It extracts text from content blocks when available. func extractMCPErrorMessage(result transport.ToolCallResult) string { diff --git a/internal/app/runner_test.go b/internal/app/runner_test.go index 0803ff79..3aeafb4b 100644 --- a/internal/app/runner_test.go +++ b/internal/app/runner_test.go @@ -896,6 +896,20 @@ func jsonRPCToolName(req map[string]any) string { return name } +func TestDetectBusinessErrorNestedServiceResult(t *testing.T) { + content := map[string]any{ + "success": false, + "result": map[string]any{ + "success": false, + "errorCode": "ROBOT_NOT_FOUND", + "errorMsg": "robot info is not exist", + }, + } + if got := detectBusinessError(content); got != "robot info is not exist" { + t.Fatalf("detectBusinessError() = %q, want nested errorMsg", got) + } +} + func writeJSONRPCToolResult(t *testing.T, w http.ResponseWriter, req map[string]any, content map[string]any, isError bool) { t.Helper() _ = json.NewEncoder(w).Encode(map[string]any{ diff --git a/internal/helpers/devapp.go b/internal/helpers/devapp.go index 4778283f..3617f5f8 100644 --- a/internal/helpers/devapp.go +++ b/internal/helpers/devapp.go @@ -285,9 +285,11 @@ func newDevAppEventSubscribeCommand(runner executor.Runner) *cobra.Command { } params := map[string]any{"unifiedAppId": appID} // 服务端 subscribe/unsubscribe 工具收 eventCodes 数组(事件码列表)。 - if types := parseDevAppListFlag(cmd, "event-codes"); len(types) > 0 { - params["eventCodes"] = types + eventCodes, err := requiredDevAppEventCodes(cmd) + if err != nil { + return err } + params["eventCodes"] = eventCodes return runDevAppTool(runner, cmd, devAppEventSubscribeTool, params) }, } @@ -314,9 +316,11 @@ func newDevAppEventUnsubscribeCommand(runner executor.Runner) *cobra.Command { return err } params := map[string]any{"unifiedAppId": appID} - if types := parseDevAppListFlag(cmd, "event-codes"); len(types) > 0 { - params["eventCodes"] = types + eventCodes, err := requiredDevAppEventCodes(cmd) + if err != nil { + return err } + params["eventCodes"] = eventCodes return runDevAppTool(runner, cmd, devAppEventUnsubscribeTool, params) }, } @@ -1489,6 +1493,14 @@ func parseDevAppListFlag(cmd *cobra.Command, name string) []string { return splitDevAppList(raw) } +func requiredDevAppEventCodes(cmd *cobra.Command) ([]string, error) { + eventCodes := parseDevAppListFlag(cmd, "event-codes") + if len(eventCodes) == 0 { + return nil, apperrors.NewValidation("--event-codes 为必填") + } + return eventCodes, nil +} + func splitDevAppList(raw string) []string { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/helpers/devapp_pretty.go b/internal/helpers/devapp_pretty.go index 71f05928..37854818 100644 --- a/internal/helpers/devapp_pretty.go +++ b/internal/helpers/devapp_pretty.go @@ -22,9 +22,7 @@ import ( // devapp_pretty annotates --format pretty output with human-readable status // labels. Upstream payloads use a numeric enum for app status and the bare -// INIT/AUDIT/... enum for version status, and the same key `status` means -// different things on different tools — fine for agents reading JSON against -// the skill's enum tables, hostile to humans. JSON output is NEVER touched +// INIT/AUDIT/... enum for versionStatus. JSON output is NEVER touched // (transparency contract); only the pretty view gets `*Text` companions. var devAppStatusLabels = map[int]string{ @@ -50,8 +48,8 @@ var devAppToolStatusKind = map[string]string{ "create_dev_app": "version", // create/update 返回 versionStatus "update_dev_app": "version", "create_dev_app_version": "version", - "list_dev_app_version": "version", - "get_dev_app_version": "version", + "list_dev_app_versions": "version", + "get_dev_app_version_detail": "version", "get_dev_app_version_status": "version", "publish_dev_app_version": "version", } @@ -96,11 +94,9 @@ func devAppAnnotateMap(m map[string]any, kind string) { } return } - for _, key := range []string{"status", "versionStatus"} { - if s, ok := m[key].(string); ok { - if label, ok := devAppVersionStatusLabels[s]; ok { - m[key+"Text"] = label - } + if s, ok := m["versionStatus"].(string); ok { + if label, ok := devAppVersionStatusLabels[s]; ok { + m["versionStatusText"] = label } } } diff --git a/internal/helpers/devapp_pretty_test.go b/internal/helpers/devapp_pretty_test.go index c236cc81..85be0606 100644 --- a/internal/helpers/devapp_pretty_test.go +++ b/internal/helpers/devapp_pretty_test.go @@ -35,12 +35,15 @@ func TestDevAppPrettyAnnotateAppStatus(t *testing.T) { } func TestDevAppPrettyAnnotateVersionStatusKindSeparation(t *testing.T) { - // 版本工具的字符串枚举要注解 + // 版本工具只注解 versionStatus;旧 status 字段不再参与版本契约。 version := map[string]any{"status": "INIT", "versionStatus": "RELEASE"} - devAppPrettyAnnotate("get_dev_app_version", version) - if version["statusText"] != "未发布(有待发布变更)" || version["versionStatusText"] != "已发布生效" { + devAppPrettyAnnotate("get_dev_app_version_detail", version) + if version["versionStatusText"] != "已发布生效" { t.Fatalf("version annotations wrong: %#v", version) } + if _, exists := version["statusText"]; exists { + t.Fatalf("version status should not be annotated: %#v", version) + } // 应用工具的数字枚举不能套用版本语义;未登记的工具不动 unknown := map[string]any{"status": float64(1)} diff --git a/internal/helpers/devapp_test.go b/internal/helpers/devapp_test.go index 877c4480..0a8d988e 100644 --- a/internal/helpers/devapp_test.go +++ b/internal/helpers/devapp_test.go @@ -568,6 +568,32 @@ func TestDevAppEventSubscribeUsesEventCodes(t *testing.T) { } } +func TestDevAppEventSubscribeRequiresEventCodes(t *testing.T) { + for _, tc := range []struct { + name string + args []string + }{ + {"subscribe", []string{"dev", "app", "event", "subscribe", "--unified-app-id", "u-1", "--yes"}}, + {"unsubscribe", []string{"dev", "app", "event", "unsubscribe", "--unified-app-id", "u-1", "--yes"}}, + } { + t.Run(tc.name, func(t *testing.T) { + runner := &captureRunner{} + root := newDevAppTestRoot(runner) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs(tc.args) + err := root.Execute() + if err == nil || !strings.Contains(err.Error(), "--event-codes 为必填") { + t.Fatalf("Execute() error = %v, want --event-codes 为必填", err) + } + if runner.last.Tool != "" { + t.Fatalf("runner should not be called, got tool %q", runner.last.Tool) + } + }) + } +} + func TestDevAppWebappCommandsBuildParams(t *testing.T) { cases := []struct { name string diff --git a/internal/transport/diagnostics.go b/internal/transport/diagnostics.go index cd62a465..a50f5a07 100644 --- a/internal/transport/diagnostics.go +++ b/internal/transport/diagnostics.go @@ -16,6 +16,7 @@ package transport import ( "encoding/json" "net/http" + "strings" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" ) @@ -38,16 +39,11 @@ func ExtractServerDiagnostics(data json.RawMessage) apperrors.ServerDiagnostics if len(data) == 0 { return apperrors.ServerDiagnostics{} } - var fields serverDiagFields - if json.Unmarshal(data, &fields) != nil { + var content map[string]any + if json.Unmarshal(data, &content) != nil { return apperrors.ServerDiagnostics{} } - return apperrors.ServerDiagnostics{ - TraceID: coalesceStr(fields.TraceID, fields.TraceIDCamel), - ServerErrorCode: coalesceStr(fields.Code, fields.ErrorCode), - TechnicalDetail: fields.TechnicalDetail, - ServerRetryable: fields.Retryable, - } + return ExtractServerDiagnosticsFromMap(content) } // ExtractServerDiagnosticsFromMap parses server diagnostic fields from a @@ -58,7 +54,7 @@ func ExtractServerDiagnosticsFromMap(content map[string]any) apperrors.ServerDia } diag := apperrors.ServerDiagnostics{ TraceID: stringFromMap(content, "trace_id", "traceId"), - ServerErrorCode: stringFromMap(content, "code", "errorCode"), + ServerErrorCode: serverErrorCodeFromMap(content, 0), TechnicalDetail: stringFromMap(content, "technical_detail"), } if v, ok := content["retryable"].(bool); ok { @@ -102,3 +98,50 @@ func stringFromMap(m map[string]any, keys ...string) string { } return "" } + +func serverErrorCodeFromMap(content map[string]any, depth int) string { + if content == nil || depth > 8 { + return "" + } + direct := stringFromMap(content, "errorCode", "code", "server_error_code") + if isWrapperServerCode(direct) { + if nested := nestedServerErrorCode(content, depth); nested != "" { + return nested + } + } + if direct != "" { + return direct + } + return nestedServerErrorCode(content, depth) +} + +func nestedServerErrorCode(content map[string]any, depth int) string { + for _, key := range []string{"content", "result", "data"} { + switch child := content[key].(type) { + case map[string]any: + if code := serverErrorCodeFromMap(child, depth+1); code != "" { + return code + } + case []any: + for _, item := range child { + childMap, ok := item.(map[string]any) + if !ok { + continue + } + if code := serverErrorCodeFromMap(childMap, depth+1); code != "" { + return code + } + } + } + } + return "" +} + +func isWrapperServerCode(code string) bool { + switch strings.ToUpper(strings.TrimSpace(code)) { + case "ERROR", "BUSINESS_ERROR", "-1": + return true + default: + return false + } +} diff --git a/internal/transport/diagnostics_test.go b/internal/transport/diagnostics_test.go index 4d018454..2a25dd5f 100644 --- a/internal/transport/diagnostics_test.go +++ b/internal/transport/diagnostics_test.go @@ -76,6 +76,26 @@ func TestExtractServerDiagnosticsFromMap(t *testing.T) { } } +func TestExtractServerDiagnosticsFromMapNestedBusinessCode(t *testing.T) { + t.Parallel() + content := map[string]any{ + "trace_id": "trace-002", + "code": "BUSINESS_ERROR", + "result": map[string]any{ + "success": false, + "errorCode": "ROBOT_NOT_FOUND", + "errorMsg": "robot info is not exist", + }, + } + diag := ExtractServerDiagnosticsFromMap(content) + if diag.TraceID != "trace-002" { + t.Fatalf("TraceID = %q, want trace-002", diag.TraceID) + } + if diag.ServerErrorCode != "ROBOT_NOT_FOUND" { + t.Fatalf("ServerErrorCode = %q, want ROBOT_NOT_FOUND", diag.ServerErrorCode) + } +} + func TestExtractServerDiagnosticsFromMap_Empty(t *testing.T) { t.Parallel() diag := ExtractServerDiagnosticsFromMap(nil) diff --git a/skills/multi/dingtalk-dev/SKILL.md b/skills/multi/dingtalk-dev/SKILL.md index 527b3109..b98a84ab 100644 --- a/skills/multi/dingtalk-dev/SKILL.md +++ b/skills/multi/dingtalk-dev/SKILL.md @@ -109,4 +109,4 @@ metadata: ## Gotchas - 新应用 `version list` 返回空不等于无可发布内容:先 `version create`,用返回的 `versionId` 继续 check-approval/publish。 -- `robot info is not exist` 是「应用还没配过机器人」,走 `robot config` 首次创建,不是 `enable`。 +- `robotStatus=UNCONFIGURED` 是「应用还没配过机器人」,走 `robot config` 首次创建,不是 `enable`。 diff --git a/skills/multi/dingtalk-dev/references/event.md b/skills/multi/dingtalk-dev/references/event.md index c020d56a..08b14d4c 100644 --- a/skills/multi/dingtalk-dev/references/event.md +++ b/skills/multi/dingtalk-dev/references/event.md @@ -4,11 +4,12 @@ `dws dev app event list/subscribe/unsubscribe`,按 `--unified-app-id` 定位,订阅/退订用 `--event-codes`(逗号分隔,一次多个)。参数查 `dws schema dev.app.event.`。 -前置条件(业务规则):订阅/退订前必须先建立长连(`dev connect`,见 connect.md)。长连未在线时服务端报「长链接未在线」(errorCode -1),这是预期,不是故障——先建联再订阅。 - 规则: - 写操作先 `--dry-run` 预览,确认后 `--yes`。 - 一次可订阅多个事件码,共用同一回调。 - 事件码取值以开放平台文档为准;不确定走 `dws dev doc search`。 - 退订前先 `event list` 确认当前订阅,避免退不存在的。 -- 返回看 `events`(已订阅列表)和 `pushType`(推送方式)。 +- `event list` 支持 `--cursor/--page-size`,返回 `events/hasMore/nextCursor/pageSize`;翻页继续传 `nextCursor`。 +- 返回看 `events[].subscribed` 和 `pushType=STREAM`。 +- `subscribe/unsubscribe` 的 `--event-codes` 必填,返回 `success/operation/unifiedAppId/eventCodes/needsPublish/versionRequiredAction`;失败时补 `errorCode/errorMsg/reason/retryable/action`。 +- 如果服务端返回 `errorCode=STREAM_NOT_CONNECTED`、`reason=STREAM_NOT_CONNECTED`、`retryable=false`、`action=run connect`,先执行 `dev connect` 建联,再重试订阅。 diff --git a/skills/multi/dingtalk-dev/references/robot.md b/skills/multi/dingtalk-dev/references/robot.md index 4144a238..1aa9a877 100644 --- a/skills/multi/dingtalk-dev/references/robot.md +++ b/skills/multi/dingtalk-dev/references/robot.md @@ -11,13 +11,13 @@ ## 一、新建智能体机器人(异步建号) -`submit` 提交任务拿 `taskId`,`result --task-id ` 轮询。`submit` 返回 `taskId/status/expiresIn/interval/retryCount`,提交成功通常是 `WAITING`。失败重试:把上次 `taskId` 通过 `--task-id` 传回 `submit`,避免重复创建。只有 `result` 返回 `SUCCESS` 才能用返回的 `agentId/robotCode/clientId/clientSecret`。 +`submit` 提交任务拿 `taskId`,`result --task-id ` 轮询。`submit` 返回 `taskId/status/expiresInSeconds/intervalSeconds/retryCount/bindsUnifiedApp`,提交成功通常是 `WAITING`,且 `bindsUnifiedApp=false` 表示异步建号任务不挂到现有应用。失败重试:把上次 `taskId` 通过 `--task-id` 传回 `submit`,避免重复创建。只有 `result` 返回 `SUCCESS` 才能用返回的 `agentId/robotCode/clientId/clientSecret`。 异步任务状态: | status | 含义 | 下一步 | |--------|------|--------| -| `WAITING` | 创建中 | 按 `interval` 轮询 `robot result` | +| `WAITING` | 创建中 | 按 `intervalSeconds` 轮询 `robot result` | | `SUCCESS` | 创建完成 | 保存 `robotCode/clientId/clientSecret`,凭据按敏感处理 | | `APPROVAL_REQUIRED` | 创建编排需审批 | 不要重复建号;按返回信息或后台审批后再继续 | | `FAIL` | 创建失败 | 读 `errorCode/errorMsg/failReason`;可带原 `taskId` 重新 `submit` | @@ -25,22 +25,23 @@ ## 二、现有应用的机器人配置 -`robot get` 返回机器人基础信息、回调、模式、状态、技能列表;应用尚未配置机器人时后端返回 `robot info is not exist`。 +`robot get` 返回机器人基础信息、回调、模式、状态、技能列表;应用尚未配置机器人时返回空态 `robotStatus=UNCONFIGURED`,不是业务错误。 状态判断: -- `status=1`:OFFLINE,配置存在但停用/下线。 -- `status=2`:ONLINE,配置已生效;`robotCode` 可用于加群、机器人身份发消息或后续建联。 +- `robotStatus=UNCONFIGURED`:应用未配置机器人,走 `robot config`。 +- `robotStatus=OFFLINE`:配置存在但停用/下线,可走 `robot enable`。 +- `robotStatus=ONLINE`:配置已启用;`robotCode` 可用于加群、机器人身份发消息或后续建联。 +- `mode` 是字符串枚举:`HTTPS` / `STREAM` / `AISKILL`。 - `robot get` 返回 `success=true` 且含 `robotCode` = 配置已落库,不是异步等待态。 - ONLINE 只代表能力已开启。要让机器人自动处理消息,还需配 `--outgoing-url`/`--event-callback-url`,或用 `dev connect` 接本地 Agent(见 connect.md)。 -- 未配置机器人时不返回 `status`,而是业务错误 `robot info is not exist`;这时走 `robot config`,不是 `enable`。 -`config` 是 upsert:建或改都用它,不存在则建、存在则改,至少给一个配置字段。国际化字段(`--i18n-name` 等)传 JSON,如 `'{"en_US":"Bot"}'`。`enable` 是纯启用:只开启能力,不带配置字段(只传 `--unified-app-id`)。旧的独立 `update` 已并入 `config`。`config` 成功后回读 `robot get`:返回 `status=2` 就别再误判"待生效",只有 `status=1` 或要重新上架才调 `enable`。 +`config` 是 upsert:建或改都用它,不存在则建、存在则改,至少给一个配置字段。国际化字段(`--i18n-name` 等)传 JSON,如 `'{"en_US":"Bot"}'`。`enable` 是纯启用:只开启能力,不带配置字段(只传 `--unified-app-id`)。旧的独立 `update` 已并入 `config`。`config/enable/disable` 成功统一返回 `success/operation/unifiedAppId/robotCode/robotStatus/configured`;回读 `robot get` 看到 `robotStatus=ONLINE` 就别再误判"待生效"。 ## 错误处理 | 情况 | 处理 | |------|------| -| `robot info is not exist` | 应用未配置机器人,先用 `robot config` 创建 | +| `robotStatus=UNCONFIGURED` | 应用未配置机器人,先用 `robot config` 创建 | | 应用名重复 | `app-name` 企业内需唯一,换名 | | `ServiceResult.success=false` | 透传 `errorCode/errorMsg` | | 创建任务 `EXPIRED` | 任务过期,重新 `submit`(可带原 taskId) | diff --git a/skills/multi/dingtalk-dev/references/version.md b/skills/multi/dingtalk-dev/references/version.md index 932e57d7..af1c23b2 100644 --- a/skills/multi/dingtalk-dev/references/version.md +++ b/skills/multi/dingtalk-dev/references/version.md @@ -28,13 +28,15 @@ permission add(requiredApproval=true 写入版本变更) 区分两个"预检":`--dry-run` 是 CLI 层的预览不调上游;`check-approval` 是服务端查审批要求不发布。发布前建议先 `check-approval`。 -发布响应 `result`: +发布/预检不返回动作枚举 `result`,看结构化字段判断下一步: -| result | 含义 | 下一步 | -|--------|------|--------| -| `NOT_REQUIRED` | 不需审批;预检=可直接发布,真实发布=已直接上线 | 回读 `version status/get` 验证 `RELEASE` | -| `APPROVAL_REQUIRED` | 需审批,常见于 `check-approval` 或未指定审批人 | 看 `approvalMode/approvalCandidates`,让用户选审批人后再 `publish --approver-user-id` | -| `SUBMITTED` | 已提交审批 | 保存 `processId`,轮询 `version status` | +| 字段 | 含义 | 下一步 | +|------|------|--------| +| `requiresApproval=false` + `publishable=true` | 不需审批,`check-approval` 通过 | 可以执行 `version publish` | +| `requiresApproval=true` + `approvalMode=SELECT_APPROVER` | 需从候选人里选审批人 | 展示 `approvalCandidates`,让用户选后再 `publish --approver-user-id` | +| `requiresApproval=true` + `approvalMode=ENTERPRISE_SELF_BUILT` | 企业自建审核 | 不传 `--approver-user-id`,直接 `publish` 提交审批 | +| `published=true` | 本次 `publish` 已直接发布 | 回读 `version status/get` 验证 `versionStatus=RELEASE` | +| `approvalSubmitted=true` | 本次 `publish` 已提交审批 | 保存 `processId`,轮询 `version status` | 审批模式 `approvalMode`: diff --git a/skills/multi/dingtalk-devapp/references/event.md b/skills/multi/dingtalk-devapp/references/event.md index 67174586..107a80b9 100644 --- a/skills/multi/dingtalk-devapp/references/event.md +++ b/skills/multi/dingtalk-devapp/references/event.md @@ -7,10 +7,10 @@ ## 可订阅事件列表 ```bash -dws devapp event list --unified-app-id --format json +dws dev app event list --unified-app-id --page-size 20 --format json ``` -MCP tool: `list_dev_app_events`。返回 `pushType`(当前推送类型)和 `events[]`,每项含: +MCP tool: `list_dev_app_events`。返回 `pushType=STREAM`、`events[]`、`hasMore`、`nextCursor`、`pageSize`;`hasMore=true` 时下一页继续传 `--cursor `。`events[]` 每项含: | 字段 | 含义 | |------|------| @@ -23,26 +23,28 @@ MCP tool: `list_dev_app_events`。返回 `pushType`(当前推送类型)和 ` ## 订阅事件 ```bash -dws devapp event subscribe --unified-app-id --event-codes user_add_org,org_dept_modify --dry-run --format json -dws devapp event subscribe --unified-app-id --event-codes user_add_org,org_dept_modify --yes --format json +dws dev app event subscribe --unified-app-id --event-codes user_add_org,org_dept_modify --dry-run --format json +dws dev app event subscribe --unified-app-id --event-codes user_add_org,org_dept_modify --yes --format json ``` -MCP tool: `subscribe_dev_app_events`(入参 `unifiedAppId` + `eventCodes`,一次订阅多个事件码)。 +MCP tool: `subscribe_dev_app_events`(入参 `unifiedAppId` + 必填数组 `eventCodes`,一次订阅多个事件码)。成功返回 `success/operation=SUBSCRIBE/unifiedAppId/eventCodes/needsPublish/versionRequiredAction`;失败时补 `errorCode/errorMsg/reason/retryable/action`。 ## 退订事件 ```bash -dws devapp event unsubscribe --unified-app-id --event-codes user_add_org,org_dept_modify --dry-run --format json -dws devapp event unsubscribe --unified-app-id --event-codes user_add_org,org_dept_modify --yes --format json +dws dev app event unsubscribe --unified-app-id --event-codes user_add_org,org_dept_modify --dry-run --format json +dws dev app event unsubscribe --unified-app-id --event-codes user_add_org,org_dept_modify --yes --format json ``` -MCP tool: `unsubscribe_dev_app_events`(入参 `unifiedAppId` + `eventCodes`,一次退订多个事件码)。 +MCP tool: `unsubscribe_dev_app_events`(入参 `unifiedAppId` + 必填数组 `eventCodes`,一次退订多个事件码)。成功返回 `success/operation=UNSUBSCRIBE/unifiedAppId/eventCodes/needsPublish/versionRequiredAction`;失败时补 `errorCode/errorMsg/reason/retryable/action`。 ## 字段映射 | CLI | MCP | 必填 | 说明 | |-----|-----|------|------| | `--unified-app-id` | `unifiedAppId` | 是 | 统一应用 ID | +| `--cursor` | `cursor` | list 可选 | 服务端返回的下一页游标 | +| `--page-size` | `pageSize` | list 可选 | 单页数量 | | `--event-codes` | `eventCodes` | subscribe/unsubscribe 必填 | 事件码列表,逗号分隔,取自 `event list` | ## 灰度应用需发布版本生效(重要) @@ -57,21 +59,21 @@ event subscribe/unsubscribe 订阅变更写入版本元数据 → version status 轮询发布/审批状态 ``` -详见 [version.md](version.md)。 +详见版本发布文档。 ## 单步流程 ```text 1. 查看可订阅事件及当前状态 - dws devapp event list --unified-app-id --format json + dws dev app event list --unified-app-id --format json → 从返回里挑 eventCode 2. 订阅 - dws devapp event subscribe --unified-app-id --event-codes , --dry-run --format json + dws dev app event subscribe --unified-app-id --event-codes , --dry-run --format json → 确认后加 --yes 3. 验证 - dws devapp event list --unified-app-id --format json + dws dev app event list --unified-app-id --format json → 确认对应事件 subscribed=true(灰度应用需先发布版本) ``` @@ -81,4 +83,5 @@ event subscribe/unsubscribe 订阅变更写入版本元数据 |------|------| | `--event-codes` 缺失 | CLI 直接报错,先 `event list` 取事件码 | | 订阅后 `subscribed` 仍为 false | 灰度应用需先 `version publish` 发布版本 | +| `errorCode=STREAM_NOT_CONNECTED` / `reason=STREAM_NOT_CONNECTED` | 先执行 `dev connect` 建联,再重试订阅 | | `ServiceResult.success=false` | 透传 `errorCode/errorMsg` |