fix cr problem

This commit is contained in:
wxianfeng
2026-06-18 18:34:12 +08:00
parent 6067906c55
commit f2b2de89f4
14 changed files with 219 additions and 66 deletions
+8 -8
View File
@@ -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.
+34 -2
View File
@@ -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 {
+14
View File
@@ -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{
+16 -4
View File
@@ -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 == "" {
+6 -10
View File
@@ -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
}
}
}
+6 -3
View File
@@ -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)}
+26
View File
@@ -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
+52 -9
View File
@@ -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
}
}
+20
View File
@@ -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)
+1 -1
View File
@@ -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`。
@@ -4,11 +4,12 @@
`dws dev app event list/subscribe/unsubscribe`,按 `--unified-app-id` 定位,订阅/退订用 `--event-codes`(逗号分隔,一次多个)。参数查 `dws schema dev.app.event.<method>`。
前置条件(业务规则):订阅/退订前必须先建立长连(`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` 建联,再重试订阅。
@@ -11,13 +11,13 @@
## 一、新建智能体机器人(异步建号)
`submit` 提交任务拿 `taskId`,`result --task-id <taskId>` 轮询。`submit` 返回 `taskId/status/expiresIn/interval/retryCount`,提交成功通常是 `WAITING`。失败重试:把上次 `taskId` 通过 `--task-id` 传回 `submit`,避免重复创建。只有 `result` 返回 `SUCCESS` 才能用返回的 `agentId/robotCode/clientId/clientSecret`。
`submit` 提交任务拿 `taskId`,`result --task-id <taskId>` 轮询。`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) |
@@ -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`:
@@ -7,10 +7,10 @@
## 可订阅事件列表
```bash
dws devapp event list --unified-app-id <unifiedAppId> --format json
dws dev app event list --unified-app-id <unifiedAppId> --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 <nextCursor>`。`events[]` 每项含:
| 字段 | 含义 |
|------|------|
@@ -23,26 +23,28 @@ MCP tool: `list_dev_app_events`。返回 `pushType`(当前推送类型)和 `
## 订阅事件
```bash
dws devapp event subscribe --unified-app-id <unifiedAppId> --event-codes user_add_org,org_dept_modify --dry-run --format json
dws devapp event subscribe --unified-app-id <unifiedAppId> --event-codes user_add_org,org_dept_modify --yes --format json
dws dev app event subscribe --unified-app-id <unifiedAppId> --event-codes user_add_org,org_dept_modify --dry-run --format json
dws dev app event subscribe --unified-app-id <unifiedAppId> --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 <unifiedAppId> --event-codes user_add_org,org_dept_modify --dry-run --format json
dws devapp event unsubscribe --unified-app-id <unifiedAppId> --event-codes user_add_org,org_dept_modify --yes --format json
dws dev app event unsubscribe --unified-app-id <unifiedAppId> --event-codes user_add_org,org_dept_modify --dry-run --format json
dws dev app event unsubscribe --unified-app-id <unifiedAppId> --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 <ID> --format json
dws dev app event list --unified-app-id <ID> --format json
→ 从返回里挑 eventCode
2. 订阅
dws devapp event subscribe --unified-app-id <ID> --event-codes <CODE1>,<CODE2> --dry-run --format json
dws dev app event subscribe --unified-app-id <ID> --event-codes <CODE1>,<CODE2> --dry-run --format json
→ 确认后加 --yes
3. 验证
dws devapp event list --unified-app-id <ID> --format json
dws dev app event list --unified-app-id <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` |