Files

2784 lines
119 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
import (
"encoding/json"
"fmt"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/spf13/cobra"
)
const (
devAppProduct = "devapp"
// 工具名 = 服务端 op-app 网关**实际注册**的名字(已用 tools 真实联调逐个核对)。
// 注意:服务端这批命名本身并不统一——前缀 dev_app 与 extension 混用、list 用复数、
// permission 用 apply 而 member 用 add、robot 建号流程保留旧名(submit_robot_create_task/
// query_robot_create_result)。CLI 这里**对齐服务端现状以跑通联调**,不在 CLI 做映射;
// 命名统一是服务端待办,详见 obsidian《dev 命令树 - 服务端 MCP 待改造清单》。
// 集中声明、调用点不写字面量,避免拼写漂移。
//
// 应用主体 + 子资源(凭证/网页/权限)。
devAppListTool = "list_dev_app"
devAppGetTool = "get_dev_app"
devAppCreateTool = "create_dev_app"
devAppUpdateTool = "update_dev_app"
devAppDeleteTool = "delete_dev_app"
devAppEnableTool = "enable_dev_app"
devAppDisableTool = "disable_dev_app"
devAppCredentialsGetTool = "get_dev_app_credentials"
devAppWebappGetTool = "get_extension_webapp_config"
devAppWebappSetTool = "set_extension_webapp_config"
devAppPermissionListTool = "list_dev_app_permissions"
devAppPermissionAddTool = "apply_dev_app_permissions"
devAppPermissionRmTool = "remove_dev_app_permissions"
devAppMemberListTool = "list_dev_app_members"
devAppMemberAddTool = "add_dev_app_members"
devAppMemberRemoveTool = "remove_dev_app_members"
devAppSecurityConfigTool = "update_dev_app_security_config"
// 机器人能力(op-app MCP 工具,硬编码不走服务发现)。
devAppRobotSubmitTool = "submit_robot_create_task"
devAppRobotResultTool = "query_robot_create_result"
devAppRobotConfigGetTool = "get_extension_robot_config"
// 上游待合并:create/update 两个 tool 合成一个 upsert(建/改判断在服务端)。
// 见 docs/upstream-todo.md。上游上线前 CLI 调此名待联调。
devAppRobotConfigUpsertTool = "set_extension_robot_config"
devAppRobotEnableTool = "enable_dev_app_robot"
devAppRobotOfflineTool = "disable_dev_app_robot"
// 事件订阅能力(op-app MCP 工具,服务端新增)。
devAppEventListTool = "list_dev_app_events"
devAppEventSubscribeTool = "subscribe_dev_app_events"
devAppEventUnsubscribeTool = "unsubscribe_dev_app_events"
// 版本发布能力(op-app MCP 工具,硬编码不走服务发现)。
devAppVersionCreateTool = "create_dev_app_version"
devAppVersionListTool = "list_dev_app_versions"
devAppVersionDetailTool = "get_dev_app_version_detail"
devAppVersionPublishTool = "publish_dev_app_version"
devAppVersionStatusTool = "get_dev_app_version_status"
)
func devAppPaginatedItemsResult(description, itemDescription string) *contract.ResultSpec {
schema, _ := json.Marshal(map[string]any{
"type": "object",
"description": description,
"additionalProperties": true,
"properties": map[string]any{
"items": map[string]any{
"type": "array",
"description": itemDescription + ";分页控制信息只读取 meta.pagination",
"items": map[string]any{
"type": "object",
"additionalProperties": true,
},
},
},
"required": []string{"items"},
})
return &contract.ResultSpec{
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
DataSchema: schema,
}
}
func devAppCursorPagination() *contract.PaginationSpec {
return &contract.PaginationSpec{
Kind: contract.PaginationKindCursor,
CursorParameter: "cursor",
}
}
// newDevAppCommand builds the `app` subtree of `dws dev`. The cobra path is
// `dws dev app ...` while the MCP product id stays "devapp" — the id is a
// backend contract (SupplementServers/StaticServers injection key and the
// pinned op-app endpoint), decoupled from the user-facing command name.
func newDevAppCommand(runner executor.Runner) *cobra.Command {
// Product-level Agent routing Decl (migrated from selection/devapp.json
// products.devapp). Catalog assembly stamps provenance contract_final.
contract.RegisterProductDecl(contract.ProductDecl{
ID: "devapp",
Selection: contract.ProductSelectionDecl{
AgentSummary: "管理钉钉开放平台企业内部应用、成员、权限、机器人、事件与版本",
UseWhen: []string{
"请求涉及企业内部应用的查询、创建、配置、成员权限、机器人、事件订阅或版本管理",
},
AvoidWhen: []string{
"个人 IM/OA 实时事件监听使用 event;开放平台接口文档搜索使用 devdoc;普通钉钉业务数据使用对应产品命令",
},
},
})
root := &cobra.Command{
Use: "app",
Short: "开放平台应用",
Long: "管理开放平台开发者应用:查询、详情、创建、更新、启停、删除、权限、网页应用、成员、安全配置、机器人、版本发布和事件订阅。",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
newGroupCommand(root)
webapp := &cobra.Command{
Use: "webapp",
Short: "开放平台网页应用配置",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
newGroupCommand(webapp)
webapp.AddCommand(
newDevAppWebappGetCommand(runner),
newDevAppWebappConfigCommand(runner),
)
permission := &cobra.Command{
Use: "permission",
Short: "开放平台应用权限",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
newGroupCommand(permission)
permission.AddCommand(
newDevAppPermissionListCommand(runner),
newDevAppPermissionAddCommand(runner),
newDevAppPermissionRemoveCommand(runner),
)
credentials := &cobra.Command{
Use: "credentials",
Short: "开放平台应用凭证",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
newGroupCommand(credentials)
credentials.AddCommand(newDevAppCredentialsGetCommand(runner))
member := &cobra.Command{
Use: "member",
Short: "开放平台应用成员管理",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
newGroupCommand(member)
member.AddCommand(
newDevAppMemberListCommand(runner),
newDevAppMemberAddCommand(runner),
newDevAppMemberRemoveCommand(runner),
)
security := &cobra.Command{
Use: "security",
Short: "开放平台应用安全设置",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
newGroupCommand(security)
security.AddCommand(newDevAppSecurityConfigCommand(runner))
robot := &cobra.Command{
Use: "robot",
Short: "开放平台应用机器人能力",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
newGroupCommand(robot)
robot.AddCommand(
newDevAppRobotSubmitCommand(runner),
newDevAppRobotResultCommand(runner),
newDevAppRobotConfigGetCommand(runner),
newDevAppRobotConfigCommand(runner),
newDevAppRobotEnableCommand(runner),
newDevAppRobotOfflineCommand(runner),
)
version := &cobra.Command{
Use: "version",
Short: "开放平台应用版本发布",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
newGroupCommand(version)
version.AddCommand(
newDevAppVersionCreateCommand(runner),
newDevAppVersionListCommand(runner),
newDevAppVersionGetCommand(runner),
newDevAppVersionCheckApprovalCommand(runner),
newDevAppVersionPublishCommand(runner),
newDevAppVersionStatusCommand(runner),
)
event := &cobra.Command{
Use: "event",
Short: "开放平台应用事件订阅",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
newGroupCommand(event)
event.AddCommand(
newDevAppEventListCommand(runner),
newDevAppEventSubscribeCommand(runner),
newDevAppEventUnsubscribeCommand(runner),
)
root.AddCommand(
newDevAppListCommand(runner),
newDevAppGetCommand(runner),
newDevAppCreateCommand(runner),
newDevAppUpdateCommand(runner),
newDevAppDeleteCommand(runner),
newDevAppDisableCommand(runner),
newDevAppEnableCommand(runner),
credentials,
webapp,
permission,
member,
security,
robot,
version,
event,
)
return root
}
// ---------------------------------------------------------------------------
// 事件订阅能力(服务端新增 list/subscribe/unsubscribe)
// ---------------------------------------------------------------------------
func newDevAppEventListCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "list",
Short: "查询应用已订阅的事件列表",
Example: " dws dev app event list --unified-app-id UNIFIED_APP_ID --page-size 20 --format json",
Tool: devAppEventListTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "keyword", Usage: "事件搜索关键词,支持按事件码或事件名称模糊匹配", Bind: "keyword", Trim: true, OmitEmpty: true},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "list_dev_app_events",
CanonicalPath: "dev.list_dev_app_events",
CLIPath: "dev app event list",
PrimaryCLIPath: "dev app event list",
},
Description: "查询应用已订阅的事件列表",
DryRun: devAppDryRun,
Result: devAppPaginatedItemsResult("当前页应用订阅事件查询结果", "当前页事件记录"),
Pagination: devAppCursorPagination(),
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "列出或搜索应用可订阅的事件",
UseWhen: []string{"需要查事件码、事件名称或当前事件列表时"},
AvoidWhen: []string{"订阅或退订应用回调事件使用对应写命令;监听当前用户个人 IM/OA 事件使用 event"},
Examples: []string{`dws dev app event list --unified-app-id <unifiedAppId> --keyword "审批" --page-size 20`},
},
},
Call: devAppCallCursor(runner),
PostMount: devAppMetaCursor(devAppEventListTool),
})
}
func newDevAppEventSubscribeCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "subscribe",
Short: "订阅应用事件回调",
Example: " dws dev app event subscribe --unified-app-id UNIFIED_APP_ID --event-codes bpms_task_change --dry-run --format json",
Tool: devAppEventSubscribeTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "event-codes", Usage: "事件码,多个用逗号或分号分隔", Bind: "eventCodes", Trim: true, Required: true, RequiredHint: "--event-codes 为必填", Transform: transformDevAppListParam},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "subscribe_dev_app_events",
CanonicalPath: "dev.subscribe_dev_app_events",
CLIPath: "dev app event subscribe",
PrimaryCLIPath: "dev app event subscribe",
},
Description: "订阅应用事件回调",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "为应用订阅指定事件码",
UseWhen: []string{"已确认事件码并需要新增事件订阅时"},
AvoidWhen: []string{"查询事件码或已有订阅时先使用 dev app event list;个人 IM/OA 事件长连接监听使用 event"},
Examples: []string{"dws dev app event subscribe --unified-app-id <unifiedAppId> --event-codes bpms_task_change --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppEventSubscribeTool),
})
}
func newDevAppEventUnsubscribeCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "unsubscribe",
Short: "取消订阅应用事件",
Example: " dws dev app event unsubscribe --unified-app-id UNIFIED_APP_ID --event-codes bpms_task_change --dry-run --format json",
Tool: devAppEventUnsubscribeTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "event-codes", Usage: "事件码,多个用逗号或分号分隔", Bind: "eventCodes", Trim: true, Required: true, RequiredHint: "--event-codes 为必填", Transform: transformDevAppListParam},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "unsubscribe_dev_app_events",
CanonicalPath: "dev.unsubscribe_dev_app_events",
CLIPath: "dev app event unsubscribe",
PrimaryCLIPath: "dev app event unsubscribe",
},
Description: "取消订阅应用事件",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "取消应用的指定事件订阅",
UseWhen: []string{"需要停止接收一个或多个已订阅事件时"},
AvoidWhen: []string{"只是查看应用事件订阅时使用 dev app event list;停止个人事件监听使用 event stop"},
Examples: []string{"dws dev app event unsubscribe --unified-app-id <unifiedAppId> --event-codes bpms_task_change --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppEventUnsubscribeTool),
})
}
func newDevAppListCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "list",
Short: "查询开放平台企业内部应用列表",
Example: " dws dev app list --name DemoApp --page-size 20 --format json",
Tool: devAppListTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "name", Usage: "应用名称关键词", Bind: "name", Trim: true, OmitEmpty: true, Aliases: []string{"keyword"}},
{Name: "app-key", Usage: "按 appKey/clientId 过滤", Bind: "appKey", Trim: true, OmitEmpty: true},
{Name: "app-group-id", Usage: "应用分组 ID", Kind: LeafInt, Bind: "appGroupId"},
{Name: "creator", Usage: "创建人名称关键词", Bind: "creator", Trim: true, OmitEmpty: true},
{Name: "robot-name", Usage: "机器人名称关键词", Bind: "robotName", Trim: true, OmitEmpty: true},
{Name: "develop-type", Usage: "开发类型枚举;不确定时不要传", Kind: LeafInt, Bind: "developType"},
{Name: "filter-cool-app", Usage: "酷应用过滤枚举;不确定时不要传", Kind: LeafInt, Bind: "filterCoolApp"},
{Name: "sort-type", Usage: "排序字段,如 gmt_modified", Bind: "sortType", Trim: true, OmitEmpty: true},
{Name: "sort-order", Usage: "排序方向 asc 或 desc", Bind: "sortOrder", Trim: true, OmitEmpty: true},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "list_dev_app",
CanonicalPath: "dev.list_dev_app",
CLIPath: "dev app list",
PrimaryCLIPath: "dev app list",
},
Description: "查询开放平台企业内部应用列表",
DryRun: devAppDryRun,
Result: &contract.ResultSpec{
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeFailure, contract.ResultOutcomeSuccess},
DataSchema: json.RawMessage(`{"type":"object","description":"当前页应用查询结果","properties":{"items":{"type":"array","description":"当前页应用记录","items":{"type":"object","properties":{"unifiedAppId":{"type":"string","description":"开放平台统一应用 ID"},"name":{"type":"string","description":"应用名称"},"appKey":{"type":"string","description":"应用 AppKey"}},"additionalProperties":true}}},"required":["items"],"additionalProperties":true}`),
},
Pagination: devAppCursorPagination(),
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "按条件分页查询开放平台应用",
UseWhen: []string{"需要按名称、创建人或应用键筛选应用时"},
AvoidWhen: []string{"已经持有明确 unifiedAppId 时使用 dev app get"},
Examples: []string{`dws dev app list --name "DemoApp" --page-size 20 --format json`},
},
},
Call: devAppCallCursor(runner),
PostMount: devAppMetaCursor(devAppListTool),
})
}
func newDevAppGetCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "get",
Short: "查询开放平台企业内部应用详情",
Example: " dws dev app get --unified-app-id UNIFIED_APP_ID --format json\n dws dev app get --app-key APP_KEY --format json",
Tool: devAppGetTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(与 --app-key 二选一)", Bind: "unifiedAppId", Trim: true, OmitEmpty: true},
{Name: "app-key", Usage: "按 appKey/clientId 查询应用详情(与 --unified-app-id 二选一)", Bind: "appKey", Trim: true, OmitEmpty: true},
},
// 二选一走 Validate 而非类型化 Constraints:发布 constraints 会改变已
// 交付的 Schema 契约(merge-base 为 null),本 PR 承诺零契约变更。
// 声明化发布留给独立的契约变更 PR。
Validate: func(cmd *cobra.Command, args []string) error {
if devAppStringFlag(cmd, "unified-app-id") == "" && devAppStringFlag(cmd, "app-key") == "" {
return apperrors.NewValidation("请传入 --unified-app-id 或 --app-key")
}
return nil
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "get_dev_app",
CanonicalPath: "dev.get_dev_app",
CLIPath: "dev app get",
PrimaryCLIPath: "dev app get",
},
Description: "查询开放平台企业内部应用详情",
DryRun: devAppDryRun,
Result: &contract.ResultSpec{
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
DataSchema: json.RawMessage(`{"type":"object","description":"开放平台企业内部应用详情","properties":{"unifiedAppId":{"type":"string","description":"开放平台统一应用 ID"},"name":{"type":"string","description":"应用名称"},"appKey":{"type":"string","description":"应用 AppKey"},"agentId":{"description":"应用 Agent ID;具体类型由服务端返回"},"status":{"description":"应用当前状态;具体类型由服务端返回"}},"required":["unifiedAppId"],"additionalProperties":true}`),
},
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "获取指定开放平台应用详情",
UseWhen: []string{"已知 unifiedAppId 或 appKey 并需要核对应用配置或状态时"},
AvoidWhen: []string{"需要搜索或分页浏览多个应用时使用 dev app list"},
Examples: []string{
"dws dev app get --unified-app-id <unifiedAppId> --format json",
"dws dev app get --app-key <appKey> --format json",
},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppGetTool),
})
}
func newDevAppCreateCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "create",
Short: "创建开放平台企业内部应用",
Example: " dws dev app create --name DemoApp --desc 内部应用 --dry-run --format json",
Tool: devAppCreateTool,
Safety: devAppSafetyHighWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "name", Usage: "应用名称 (必填)", Bind: "name", Trim: true, Required: true, RequiredHint: "--name 为必填"},
{Name: "desc", Usage: "应用描述", Bind: "desc", Trim: true, OmitEmpty: true},
{Name: "icon-media-id", Usage: "应用图标 mediaId", Bind: "iconMediaId", Trim: true, OmitEmpty: true},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "create_dev_app",
CanonicalPath: "dev.create_dev_app",
CLIPath: "dev app create",
PrimaryCLIPath: "dev app create",
},
Description: "创建开放平台企业内部应用",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "创建钉钉开放平台应用并返回 unifiedAppId/凭证信息",
UseWhen: []string{"需要新建企业内部应用或三方个人应用并拿到 unifiedAppId"},
AvoidWhen: []string{"应用已存在只需改信息时用 dev app update", "只查文档时用 devdoc"},
Examples: []string{`dws dev app create --name "我的 AI 机器人" --desc "接 opencode" --dry-run --format json`},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppCreateTool),
})
}
func newDevAppUpdateCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "update",
Short: "修改开放平台企业内部应用基础信息",
Long: "修改开放平台企业内部应用基础信息:名称、描述或图标至少提供一项。",
Example: " dws dev app update --unified-app-id UNIFIED_APP_ID --name DemoApp2 --dry-run --format json",
Tool: devAppUpdateTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "name", Usage: "新的应用名称", Bind: "name", Trim: true, OmitEmpty: true},
{Name: "desc", Usage: "新的应用描述", Bind: "desc", Trim: true, OmitEmpty: true},
{Name: "icon-media-id", Usage: "新的应用图标 mediaId", Bind: "iconMediaId", Trim: true, OmitEmpty: true},
},
// 至少一项走 Validate 而非类型化 Constraints:零契约变更(见 get 命令注释)。
Validate: func(cmd *cobra.Command, args []string) error {
if devAppStringFlag(cmd, "name") == "" && devAppStringFlag(cmd, "desc") == "" &&
devAppStringFlag(cmd, "icon-media-id") == "" {
return apperrors.NewValidation("至少提供一项待更新字段:--name、--desc 或 --icon-media-id")
}
return nil
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "update_dev_app",
CanonicalPath: "dev.update_dev_app",
CLIPath: "dev app update",
PrimaryCLIPath: "dev app update",
},
Description: "修改开放平台企业内部应用基础信息",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "更新开放平台应用名称/描述/图标等基础信息",
UseWhen: []string{"已有 unifiedAppId,需要修改应用基础信息"},
AvoidWhen: []string{"创建新应用用 create;停用/启用用 disable/enable;改安全配置用 security config"},
Examples: []string{"dws dev app update --unified-app-id <unifiedAppId> --name <新名称> --dry-run --format json"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppUpdateTool),
})
}
func newDevAppCredentialsGetCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "get",
Short: "读取开放平台应用凭证",
Example: " dws dev app credentials get --unified-app-id UNIFIED_APP_ID --format json",
Tool: devAppCredentialsGetTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "get_dev_app_credentials",
CanonicalPath: "dev.get_dev_app_credentials",
CLIPath: "dev app credentials get",
PrimaryCLIPath: "dev app credentials get",
},
Description: "读取开放平台应用凭证",
DryRun: devAppDryRun,
Result: &contract.ResultSpec{
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
DataSchema: json.RawMessage(`{"type":"object","description":"开放平台应用凭证;敏感字段必须经过统一脱敏策略","properties":{"clientId":{"type":"string","description":"OAuth 客户端 ID"},"clientSecret":{"type":"string","description":"OAuth 客户端密钥"},"appKey":{"type":"string","description":"应用 AppKey"},"appSecret":{"type":"string","description":"应用 AppSecret"}},"additionalProperties":true}`),
SensitivePaths: []string{"appSecret", "clientSecret"},
},
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "读取指定应用的客户端凭证",
UseWhen: []string{"已知 unifiedAppId 且需要 clientId 或 clientSecret 时"},
AvoidWhen: []string{"不要把凭证内容用于普通应用详情查询或写入日志"},
Examples: []string{"dws dev app credentials get --unified-app-id <unifiedAppId> --format json"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppCredentialsGetTool),
})
}
func newDevAppDisableCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "disable",
Short: "停用开放平台企业内部应用",
Tool: devAppDisableTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "disable_dev_app",
CanonicalPath: "dev.disable_dev_app",
CLIPath: "dev app disable",
PrimaryCLIPath: "dev app disable",
},
Description: "停用开放平台企业内部应用",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "停用指定开放平台应用",
UseWhen: []string{"需要让应用暂时不可用但保留配置时"},
AvoidWhen: []string{"永久删除应用使用 dev app delete"},
Examples: []string{"dws dev app disable --unified-app-id <unifiedAppId> --dry-run --format json"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppDisableTool),
})
}
func newDevAppEnableCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "enable",
Short: "启用开放平台企业内部应用",
Tool: devAppEnableTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "enable_dev_app",
CanonicalPath: "dev.enable_dev_app",
CLIPath: "dev app enable",
PrimaryCLIPath: "dev app enable",
},
Description: "启用开放平台企业内部应用",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "启用指定开放平台应用",
UseWhen: []string{"需要恢复一个已停用应用时"},
AvoidWhen: []string{"启用应用内机器人能力使用 dev app robot enable"},
Examples: []string{"dws dev app enable --unified-app-id <unifiedAppId> --dry-run --format json"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppEnableTool),
})
}
// newDevAppDeleteCommand is delete with a danger tier: deleting an app is
// irreversible, so beyond the high-write confirmation it requires
// --confirm-name to match the located app's real name. This guards against
// "located the wrong app and deleted it" — the agent must first know the name
// (via `get`/dry-run) before it can delete. The match is verified client-side
// (a `get` then compare), standard practice for destructive CLI ops
// (gh repo delete, gcloud).
//
// LeafSpec+RunE:confirm-name 的 get-then-compare 多步二次确认属多步编排,
// 由自定义 RunE 承载;框架仍按同一 SafetySpec 执行确认门并发布 Schema。
func newDevAppDeleteCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "delete",
Short: "删除开放平台企业内部应用(不可逆,需 --confirm-name 二次确认)",
Example: " dws dev app delete --unified-app-id UNIFIED_APP_ID --confirm-name 应用名 --yes --format json",
Tool: devAppDeleteTool,
Safety: devAppSafetyDestructive(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "confirm-name", Usage: "二次确认:必须与被删应用的名称一致(不可逆操作的防误删)", Bind: "confirmName", Trim: true, OmitEmpty: true},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "delete_dev_app",
CanonicalPath: "dev.delete_dev_app",
CLIPath: "dev app delete",
PrimaryCLIPath: "dev app delete",
},
Description: "删除开放平台企业内部应用(不可逆,需 --confirm-name 二次确认)",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "删除开放平台企业内部应用(不可恢复)",
UseWhen: []string{"用户明确要求永久删除应用,且接受不可恢复后果"},
AvoidWhen: []string{"只需临时下架时用 dev app disable", "用户未确认应用名/影响范围时不要删除"},
Examples: []string{"dws dev app delete --unified-app-id <unified-app-id> --dry-run --format json"},
},
},
RunE: func(cmd *cobra.Command, args []string) error {
appID, err := requiredDevAppUnifiedID(cmd)
if err != nil {
return err
}
params := map[string]any{"unifiedAppId": appID}
// Dry-run previews the delete without requiring confirmation —
// the agent uses it (or `get`) to read the app name first.
if commandDryRun(cmd) {
return runDevAppTool(runner, cmd, devAppDeleteTool, params)
}
// Real execution: require --confirm-name and verify it matches.
confirmName := devAppStringFlag(cmd, "confirm-name")
if confirmName == "" {
return apperrors.NewValidation("删除不可逆,需二次确认:先用 `dev app get` 看应用名,再加 --confirm-name=<应用名>")
}
actualName, err := devAppFetchAppName(runner, cmd, params)
if err != nil {
return err
}
// 读不到应用名时 fail-closed:不可逆删除不能在无法校验 --confirm-name
// 的情况下放行,否则二次确认形同虚设。
if actualName == "" {
return apperrors.NewValidation("无法读取应用名以校验 --confirm-name,已中止删除;请确认 --unified-app-id 正确,或先用 --dry-run / `dev app get` 预览")
}
if confirmName != actualName {
return apperrors.NewValidation(fmt.Sprintf("名称不匹配:--confirm-name=%q 但定位到的应用名是 %q,已中止删除", confirmName, actualName))
}
return runDevAppTool(runner, cmd, devAppDeleteTool, params)
},
PostMount: devAppMeta(devAppDeleteTool),
})
}
// devAppFetchAppName resolves the located app's name via get_dev_app
// so delete can verify --confirm-name. Returns "" if the name can't be found;
// the caller treats "" as fail-closed (aborts the irreversible delete) rather
// than silently proceeding.
func devAppFetchAppName(runner executor.Runner, cmd *cobra.Command, locator map[string]any) (string, error) {
inv := executor.NewHelperInvocation(
cobracmd.LegacyCommandPath(cmd),
devAppProduct,
devAppGetTool,
locator,
)
result, err := runner.Run(cmd.Context(), inv)
if err != nil {
return "", err
}
// get_dev_app 返回的应用名字段是 name(credentials 才用 appName);
// 取 name、appName 兜底,否则 delete 永远读不到名、二次确认必然 fail-closed。
if name := devAppExtractString(result.Response, "name"); name != "" {
return name, nil
}
return devAppExtractString(result.Response, "appName"), nil
}
// devAppExtractString descends the helper response (content → result) and reads
// a string field. Returns "" if absent.
func devAppExtractString(response map[string]any, key string) string {
node := response
if inner, ok := node["content"].(map[string]any); ok {
node = inner
}
if inner, ok := node["result"].(map[string]any); ok {
node = inner
}
if v, ok := node[key].(string); ok {
return v
}
return ""
}
func newDevAppWebappGetCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "get",
Short: "查询网页应用配置",
Example: " dws dev app webapp get --unified-app-id UNIFIED_APP_ID --format json",
Tool: devAppWebappGetTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "get_extension_webapp_config",
CanonicalPath: "dev.get_extension_webapp_config",
CLIPath: "dev app webapp get",
PrimaryCLIPath: "dev app webapp get",
},
Description: "查询网页应用配置",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "获取指定应用的网页入口配置",
UseWhen: []string{"需要核对应用 H5 或 PC 首页地址时"},
AvoidWhen: []string{"修改网页入口时使用 dev app webapp config"},
Examples: []string{"dws dev app webapp get --unified-app-id <unifiedAppId> --format json"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppWebappGetTool),
})
}
func newDevAppWebappConfigCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "config",
Short: "配置网页应用能力",
Example: " dws dev app webapp config --unified-app-id UNIFIED_APP_ID --homepage-url https://example.com --dry-run --format json",
Tool: devAppWebappSetTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "h5-page-type", Usage: "网页应用生效端/页面类型", Bind: "h5PageType", Trim: true, OmitEmpty: true},
{Name: "homepage-url", Usage: "移动端首页地址", Bind: "homepageUrl", Trim: true, OmitEmpty: true},
{Name: "pc-homepage-url", Usage: "PC 端首页地址", Bind: "pcHomepageUrl", Trim: true, OmitEmpty: true},
{Name: "omp-url", Usage: "管理后台地址", Bind: "ompUrl", Trim: true, OmitEmpty: true},
},
// 至少一项走 Validate 而非类型化 Constraints:零契约变更(见 get 命令注释)。
Validate: func(cmd *cobra.Command, args []string) error {
if devAppStringFlag(cmd, "h5-page-type") == "" && devAppStringFlag(cmd, "homepage-url") == "" &&
devAppStringFlag(cmd, "pc-homepage-url") == "" && devAppStringFlag(cmd, "omp-url") == "" {
return apperrors.NewValidation("至少提供一项网页应用配置:--h5-page-type、--homepage-url、--pc-homepage-url 或 --omp-url")
}
return nil
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "set_extension_webapp_config",
CanonicalPath: "dev.set_extension_webapp_config",
CLIPath: "dev app webapp config",
PrimaryCLIPath: "dev app webapp config",
},
Description: "配置网页应用能力",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "创建或更新应用网页入口配置",
UseWhen: []string{"需要设置 H5、PC 首页或 OMP 地址时"},
AvoidWhen: []string{"只查看当前网页配置时使用 dev app webapp get"},
Examples: []string{"dws dev app webapp config --unified-app-id <unifiedAppId> --homepage-url https://example.com --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppWebappSetTool),
})
}
func newDevAppPermissionListCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "list",
Short: "查询开放平台应用权限列表",
Example: " dws dev app permission list --unified-app-id UNIFIED_APP_ID --keyword 通讯录 --page-size 20 --format json",
Tool: devAppPermissionListTool,
Safety: devAppSafetyRead(),
// 命令级别名 "search" 由 PostMount 设回(LeafSpec 无 Command Aliases 字段)。
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "keyword", Usage: "权限名、权限点、接口名关键词", Bind: "keyword", Trim: true, OmitEmpty: true},
{Name: "scope-value", Usage: "精确权限点 scopeValue", Bind: "scopeValue", Trim: true, OmitEmpty: true},
{Name: "auth-status", Usage: "权限状态:ALL、AUTHED、UNAUTHED", Default: "ALL", Bind: "authStatus", Trim: true, OmitEmpty: true, Transform: func(raw string) (any, error) { return strings.ToUpper(raw), nil }},
{Name: "scope-type", Usage: "权限一级类型:APP 或 SNS", Bind: "scopeType", Trim: true, OmitEmpty: true, Transform: func(raw string) (any, error) { return strings.ToUpper(raw), nil }},
{Name: "api-status", Usage: "开发者后台 apiStatus 过滤", Bind: "apiStatus", Trim: true, OmitEmpty: true},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "list_dev_app_permissions",
CanonicalPath: "dev.list_dev_app_permissions",
CLIPath: "dev app permission list",
PrimaryCLIPath: "dev app permission list",
Aliases: []string{"dev app permission search"},
},
Description: "查询开放平台应用权限列表",
DryRun: devAppDryRun,
Result: devAppPaginatedItemsResult("当前页开放平台应用权限查询结果", "当前页权限记录"),
Pagination: devAppCursorPagination(),
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "查询应用权限及其授权状态",
UseWhen: []string{"需要按关键词、范围或状态查权限时"},
AvoidWhen: []string{"新增或移除权限使用对应 permission 写命令"},
Examples: []string{`dws dev app permission list --unified-app-id <unifiedAppId> --keyword "通讯录" --page-size 20`},
},
},
Call: devAppCallCursor(runner),
PostMount: func(cmd *cobra.Command) {
cmd.Aliases = []string{"search"}
devAppMetaCursor(devAppPermissionListTool)(cmd)
},
})
}
func newDevAppPermissionAddCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "add",
Short: "申请开放平台应用权限点",
Example: " dws dev app permission add --unified-app-id UNIFIED_APP_ID --scope-values Contact.User.mobile --dry-run --format json",
Tool: devAppPermissionAddTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "scope-values", Usage: "权限点 scopeValue,多个用逗号或分号分隔", Bind: "scopeValues", Trim: true, Required: true, RequiredHint: "--scope-values 为必填", Transform: transformDevAppScopeValues},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "apply_dev_app_permissions",
CanonicalPath: "dev.apply_dev_app_permissions",
CLIPath: "dev app permission add",
PrimaryCLIPath: "dev app permission add",
},
Description: "申请开放平台应用权限点",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "为应用申请一个或多个接口权限",
UseWhen: []string{"已确认 scopeValue 并需要新增权限时"},
AvoidWhen: []string{"查找或核对已有权限时使用 dev app permission list"},
Examples: []string{"dws dev app permission add --unified-app-id <unifiedAppId> --scope-values Contact.User.mobile --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppPermissionAddTool),
})
}
func newDevAppPermissionRemoveCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "remove",
Short: "取消开放平台应用权限点",
Example: " dws dev app permission remove --unified-app-id UNIFIED_APP_ID --scope-values Contact.User.mobile,qyapi_robot_sendmsg --dry-run --format json",
Tool: devAppPermissionRmTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "scope-values", Usage: "待取消权限点 scopeValue,多个用逗号或分号分隔", Bind: "scopeValues", Trim: true, Required: true, RequiredHint: "--scope-values 为必填", Transform: transformDevAppScopeValues},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "remove_dev_app_permissions",
CanonicalPath: "dev.remove_dev_app_permissions",
CLIPath: "dev app permission remove",
PrimaryCLIPath: "dev app permission remove",
},
Description: "取消开放平台应用权限点",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "移除应用的一个或多个接口权限",
UseWhen: []string{"需要撤销已知 scopeValue 的权限时"},
AvoidWhen: []string{"只查看权限状态时使用 dev app permission list"},
Examples: []string{"dws dev app permission remove --unified-app-id <unifiedAppId> --scope-values Contact.User.mobile --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppPermissionRmTool),
})
}
func newDevAppMemberListCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "list",
Short: "查询开放平台应用成员",
Example: " dws dev app member list --unified-app-id <unifiedAppId>",
Tool: devAppMemberListTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "list_dev_app_members",
CanonicalPath: "dev.list_dev_app_members",
CLIPath: "dev app member list",
PrimaryCLIPath: "dev app member list",
},
Description: "查询开放平台应用成员",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "列出指定应用的协作成员",
UseWhen: []string{"需要查看应用开发者和管理员清单时"},
AvoidWhen: []string{"新增或移除成员使用对应 member 写命令"},
Examples: []string{"dws dev app member list --unified-app-id <unifiedAppId>"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppMemberListTool),
})
}
func newDevAppMemberAddCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "add",
Short: "添加开放平台应用成员",
Example: " dws dev app member add --unified-app-id <unifiedAppId> --user-ids userId1,userId2 --member-type DEVELOPER --dry-run",
Tool: devAppMemberAddTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "user-ids", Usage: "成员 userId 列表,多个用逗号分隔 (必填)", Bind: "userIds", Trim: true, Required: true, RequiredHint: "--user-ids 为必填", Aliases: []string{"member-user-ids"}, Transform: transformDevAppListParam},
{Name: "member-type", Usage: "成员类型,如 DEVELOPER (必填)", Bind: "memberType", Trim: true, Required: true, RequiredHint: "--member-type 为必填"},
},
// 纯分隔符输入(如 ",")通过 Required 但解析为空:保持旧版「至少包含一个」
// 拦截。member-user-ids 是 user-ids 的注册别名,需一并检查。
Validate: func(cmd *cobra.Command, args []string) error {
if len(parseDevAppListFlag(cmd, "user-ids")) == 0 && len(parseDevAppListFlag(cmd, "member-user-ids")) == 0 {
return apperrors.NewValidation("--user-ids 至少包含一个 userId")
}
return nil
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "add_dev_app_members",
CanonicalPath: "dev.add_dev_app_members",
CLIPath: "dev app member add",
PrimaryCLIPath: "dev app member add",
},
Description: "添加开放平台应用成员",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "向应用添加开发者或管理员成员",
UseWhen: []string{"需要授予指定用户应用协作角色时"},
AvoidWhen: []string{"只查看现有成员时使用 dev app member list"},
Examples: []string{"dws dev app member add --unified-app-id <unifiedAppId> --user-ids userId1,userId2 --member-type DEVELOPER --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppMemberAddTool),
})
}
func newDevAppMemberRemoveCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "remove",
Short: "移除开放平台应用成员",
Example: " dws dev app member remove --unified-app-id <unifiedAppId> --user-ids userId1,userId2 --member-type DEVELOPER --dry-run",
Tool: devAppMemberRemoveTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "user-ids", Usage: "成员 userId 列表,多个用逗号分隔 (必填)", Bind: "userIds", Trim: true, Required: true, RequiredHint: "--user-ids 为必填", Aliases: []string{"member-user-ids"}, Transform: transformDevAppListParam},
{Name: "member-type", Usage: "成员类型,如 DEVELOPER (必填)", Bind: "memberType", Trim: true, Required: true, RequiredHint: "--member-type 为必填"},
},
// 纯分隔符输入(如 ",")通过 Required 但解析为空:保持旧版「至少包含一个」
// 拦截。member-user-ids 是 user-ids 的注册别名,需一并检查。
Validate: func(cmd *cobra.Command, args []string) error {
if len(parseDevAppListFlag(cmd, "user-ids")) == 0 && len(parseDevAppListFlag(cmd, "member-user-ids")) == 0 {
return apperrors.NewValidation("--user-ids 至少包含一个 userId")
}
return nil
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "remove_dev_app_members",
CanonicalPath: "dev.remove_dev_app_members",
CLIPath: "dev app member remove",
PrimaryCLIPath: "dev app member remove",
},
Description: "移除开放平台应用成员",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "从应用移除开发者或管理员成员",
UseWhen: []string{"需要撤销指定用户的应用协作角色时"},
AvoidWhen: []string{"删除应用本身或只查看成员时不要使用"},
Examples: []string{"dws dev app member remove --unified-app-id <unifiedAppId> --user-ids userId1,userId2 --member-type DEVELOPER --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppMemberRemoveTool),
})
}
func newDevAppSecurityConfigCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "config",
Short: "更新开放平台应用安全配置",
Example: " dws dev app security config --unified-app-id <unifiedAppId> " +
"--ip-whitelist 192.0.2.10 --redirect-urls https://callback.example.invalid/callback --sso-urls https://sso.example.invalid/sso --dry-run",
Tool: devAppSecurityConfigTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "ip-whitelist", Usage: "出口 IP 白名单,多个用逗号或分号分隔(整组覆盖,非追加)", Bind: "ipWhitelist", Trim: true, Transform: transformDevAppListParam},
{Name: "redirect-urls", Usage: "登录重定向 URL,多个用逗号或分号分隔(整组覆盖,非追加)", Bind: "redirectUrls", Trim: true, Transform: transformDevAppListParam},
{Name: "sso-urls", Usage: "端内免登地址,多个用逗号或分号分隔(整组覆盖,非追加)", Bind: "ssoUrls", Trim: true, Transform: transformDevAppListParam},
},
// 至少一项走 Validate 而非类型化 Constraints:零契约变更(见 get 命令注释)。
Validate: func(cmd *cobra.Command, args []string) error {
if len(parseDevAppListFlag(cmd, "ip-whitelist")) == 0 &&
len(parseDevAppListFlag(cmd, "redirect-urls")) == 0 &&
len(parseDevAppListFlag(cmd, "sso-urls")) == 0 {
return apperrors.NewValidation("至少提供一项安全配置:--ip-whitelist、--redirect-urls 或 --sso-urls")
}
return nil
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "update_dev_app_security_config",
CanonicalPath: "dev.update_dev_app_security_config",
CLIPath: "dev app security config",
PrimaryCLIPath: "dev app security config",
},
Description: "更新开放平台应用安全配置",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "更新应用 IP 白名单、回调和单点登录地址",
UseWhen: []string{"需要调整应用安全相关 URL 或白名单时"},
AvoidWhen: []string{"普通应用名称和描述更新使用 dev app update"},
Examples: []string{"dws dev app security config --unified-app-id <unifiedAppId> --ip-whitelist 192.0.2.10 --redirect-urls https://callback.example.invalid/callback --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppSecurityConfigTool),
})
}
// ---------------------------------------------------------------------------
// 机器人能力
// ---------------------------------------------------------------------------
// LeafSpec+RunE:devAppRobotCreateParams 的 icon/preview 空串占位与失败重试
// taskId 编排由自定义 RunE 承载;框架按 SafetySpec 执行确认门,必填仍由
// devAppRobotCreateParams 报错,Required 标记仅用于 Schema/help 投影。
func newDevAppRobotSubmitCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "submit",
Short: "异步提交钉钉智能体机器人创建任务(支持失败重试)",
Example: " dws dev app robot submit --name 我的智能体 --robot-name 小助手 --desc \"处理审批问答\" --dry-run --format json",
Tool: devAppRobotSubmitTool,
Safety: devAppSafetyHighWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "name", Usage: "智能体应用名称,长度 2-20,企业内唯一 (必填)", Bind: "name", Trim: true, Required: true, RequiredHint: "--name 为必填"},
{Name: "robot-name", Usage: "承载机器人名称,用于客户端展示 (必填)", Bind: "robotName", Trim: true, Required: true, RequiredHint: "--robot-name 为必填"},
{Name: "desc", Usage: "机器人功能描述,不超过 200 字 (必填)", Bind: "desc", Trim: true, Required: true, RequiredHint: "--desc 为必填"},
{Name: "icon-media-id", Usage: "机器人图标 mediaId;为空时使用默认图标", Bind: "iconMediaId", Trim: true, OmitEmpty: true},
{Name: "preview-media-id", Usage: "机器人预览图 mediaId;为空时复用图标", Bind: "previewMediaId", Trim: true, OmitEmpty: true},
{Name: "task-id", Usage: "失败重试时传入原 taskId;为空时服务端自动生成", Bind: "taskId", Trim: true, OmitEmpty: true},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "submit_robot_create_task",
CanonicalPath: "dev.submit_robot_create_task",
CLIPath: "dev app robot submit",
PrimaryCLIPath: "dev app robot submit",
},
Description: "异步提交钉钉智能体机器人创建任务(支持失败重试)",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "提交机器人建号异步任务",
UseWhen: []string{"需要创建新的机器人账号并取得 taskId 时"},
AvoidWhen: []string{"已有任务应先用 robot result 轮询,不能重复提交"},
Examples: []string{`dws dev app robot submit --name "我的智能体" --robot-name "小助手" --desc "处理审批问答" --dry-run`},
},
},
RunE: func(cmd *cobra.Command, args []string) error {
params, err := devAppRobotCreateParams(cmd)
if err != nil {
return err
}
// submit_robot_create 的 schema 把图标字段标为必填(空值时服务端用默认图标),
// 因此即使用户未提供也补空串占位。
if _, ok := params["iconMediaId"]; !ok {
params["iconMediaId"] = ""
}
if _, ok := params["previewMediaId"]; !ok {
params["previewMediaId"] = ""
}
devAppPutString(params, "taskId", devAppStringFlag(cmd, "task-id"))
return runDevAppTool(runner, cmd, devAppRobotSubmitTool, params)
},
PostMount: devAppMeta(devAppRobotSubmitTool),
})
}
func newDevAppRobotResultCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "result",
Short: "查询机器人异步创建任务结果",
Example: " dws dev app robot result --task-id TASK_ID --format json",
Tool: devAppRobotResultTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "task-id", Usage: "提交创建任务时返回的 taskId (必填)", Bind: "taskId", Trim: true, Required: true, RequiredHint: "--task-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "query_robot_create_result",
CanonicalPath: "dev.query_robot_create_result",
CLIPath: "dev app robot result",
PrimaryCLIPath: "dev app robot result",
},
Description: "查询机器人异步创建任务结果",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "查询机器人建号异步任务结果",
UseWhen: []string{"已有 taskId,需要轮询 WAITING、SUCCESS 或审批状态时"},
AvoidWhen: []string{"没有建号任务时先使用 dev app robot submit"},
Examples: []string{"dws dev app robot result --task-id <taskId> --format json"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppRobotResultTool),
})
}
func newDevAppRobotConfigGetCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "get",
Short: "查询现有应用的机器人配置",
Example: " dws dev app robot get --unified-app-id UNIFIED_APP_ID --format json",
Tool: devAppRobotConfigGetTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "get_extension_robot_config",
CanonicalPath: "dev.get_extension_robot_config",
CLIPath: "dev app robot get",
PrimaryCLIPath: "dev app robot get",
},
Description: "查询现有应用的机器人配置",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "获取指定应用的机器人配置和状态",
UseWhen: []string{"需要判断机器人是否未配置、离线或在线时"},
AvoidWhen: []string{"查询机器人建号异步任务使用 dev app robot result"},
Examples: []string{"dws dev app robot get --unified-app-id <unifiedAppId> --format json"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppRobotConfigGetTool),
})
}
// newDevAppRobotConfigCommand is the upsert command for an app's robot config:
// one command for both "首次创建" and "更新" — the create-vs-update decision is
// the upstream tool's job, not the CLI's (see docs/upstream-todo.md, where the
// old create/update tools merge into one `set_dev_app_robot_config`).
// `enable` (pure enable, no config fields) is a separate command.
//
// LeafSpec+RunE:devAppRobotConfigParams 的 mode enum 校验、Bool Changed 语义、
// skills 列表、i18n JSON 解析与至少一项计数由自定义 RunE 承载;框架按
// SafetySpec 执行确认门并发布 Schema。
func newDevAppRobotConfigCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "config",
Short: "创建或更新现有应用的机器人配置(upsert)",
Example: " dws dev app robot config --unified-app-id UNIFIED_APP_ID --name 小助手 --brief 审批助手 --dry-run --format json",
Tool: devAppRobotConfigUpsertTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "name", Usage: "机器人名称", Bind: "name", Trim: true, OmitEmpty: true},
{Name: "brief", Usage: "机器人简介", Bind: "brief", Trim: true, OmitEmpty: true},
{Name: "desc", Usage: "机器人描述", Bind: "desc", Trim: true, OmitEmpty: true},
{Name: "icon-media-id", Usage: "机器人图标 mediaId", Bind: "iconMediaId", Trim: true, OmitEmpty: true},
{Name: "outgoing-url", Usage: "消息回调地址", Bind: "outgoingUrl", Trim: true, OmitEmpty: true},
{Name: "event-callback-url", Usage: "事件回调地址", Bind: "eventCallbackUrl", Trim: true, OmitEmpty: true},
{Name: "mode", Usage: "机器人模式:HTTPS / STREAM / AISKILL", Bind: "mode", Trim: true, OmitEmpty: true},
{Name: "skills", Usage: "技能列表,多个用逗号或分号分隔", Bind: "skills", Trim: true, OmitEmpty: true},
{Name: "add-scope", Usage: "是否自动添加机器人相关权限", Kind: LeafBool, Bind: "addScope", OmitEmpty: true},
{Name: "disable-ssl-verify", Usage: "回调地址是否关闭 SSL 校验", Kind: LeafBool, Bind: "disableSSLVerify", OmitEmpty: true},
{Name: "i18n-name", Usage: "机器人名称国际化 JSON,如 '{\"en_US\":\"Bot\"}'", Bind: "i18nName", Trim: true, OmitEmpty: true},
{Name: "i18n-brief", Usage: "机器人简介国际化 JSON", Bind: "i18nBrief", Trim: true, OmitEmpty: true},
{Name: "i18n-description", Usage: "机器人描述国际化 JSON", Bind: "i18nDescription", Trim: true, OmitEmpty: true},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "set_extension_robot_config",
CanonicalPath: "dev.set_extension_robot_config",
CLIPath: "dev app robot config",
PrimaryCLIPath: "dev app robot config",
},
Description: "创建或更新现有应用的机器人配置(upsert)",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "创建或更新应用的机器人能力配置",
UseWhen: []string{"已有 unifiedAppId 并需要配置机器人名称、回调或技能时"},
AvoidWhen: []string{"机器人建号使用 robot submit,本地建联使用 dev connect"},
Examples: []string{`dws dev app robot config --unified-app-id <unifiedAppId> --name "小助手" --brief "审批助手" --dry-run`},
},
},
RunE: func(cmd *cobra.Command, args []string) error {
appID, err := requiredDevAppUnifiedID(cmd)
if err != nil {
return err
}
params, updates, err := devAppRobotConfigParams(cmd, appID)
if err != nil {
return err
}
if updates == 0 {
return apperrors.NewValidation("至少提供一项机器人配置字段,如 --name、--brief、--desc、--icon-media-id、--outgoing-url、--event-callback-url、--mode、--skills")
}
return runDevAppTool(runner, cmd, devAppRobotConfigUpsertTool, params)
},
PostMount: devAppMeta(devAppRobotConfigUpsertTool),
})
}
// newDevAppRobotEnableCommand enables an app's robot capability. Unlike config,
// it needs no config fields — pure enable, only the app locator.
func newDevAppRobotEnableCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "enable",
Short: "启用现有应用机器人能力(纯启用,无需配置字段)",
Example: " dws dev app robot enable --unified-app-id UNIFIED_APP_ID --dry-run --format json",
Tool: devAppRobotEnableTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "enable_dev_app_robot",
CanonicalPath: "dev.enable_dev_app_robot",
CLIPath: "dev app robot enable",
PrimaryCLIPath: "dev app robot enable",
},
Description: "启用现有应用机器人能力(纯启用,无需配置字段)",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "启用指定应用的机器人能力",
UseWhen: []string{"机器人配置存在但处于 OFFLINE 时"},
AvoidWhen: []string{"建立本地 Stream 连接使用 dev connect"},
Examples: []string{"dws dev app robot enable --unified-app-id <unifiedAppId> --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppRobotEnableTool),
})
}
func newDevAppRobotOfflineCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "disable",
Short: "停用现有应用的机器人能力",
Example: " dws dev app robot disable --unified-app-id UNIFIED_APP_ID --dry-run --format json",
Tool: devAppRobotOfflineTool,
Safety: devAppSafetyWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "disable_dev_app_robot",
CanonicalPath: "dev.disable_dev_app_robot",
CLIPath: "dev app robot disable",
PrimaryCLIPath: "dev app robot disable",
},
Description: "停用现有应用的机器人能力",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "停用指定应用的机器人能力",
UseWhen: []string{"需要暂时让应用机器人离线时"},
AvoidWhen: []string{"停用整个应用使用 dev app disable"},
Examples: []string{"dws dev app robot disable --unified-app-id <unifiedAppId> --dry-run"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppRobotOfflineTool),
})
}
func devAppRobotCreateParams(cmd *cobra.Command) (map[string]any, error) {
name := devAppStringFlag(cmd, "name")
if name == "" {
return nil, apperrors.NewValidation("--name 为必填")
}
robotName := devAppStringFlag(cmd, "robot-name")
if robotName == "" {
return nil, apperrors.NewValidation("--robot-name 为必填")
}
desc := devAppStringFlag(cmd, "desc")
if desc == "" {
return nil, apperrors.NewValidation("--desc 为必填")
}
params := map[string]any{
"name": name,
"robotName": robotName,
"desc": desc,
}
devAppPutString(params, "iconMediaId", devAppStringFlag(cmd, "icon-media-id"))
devAppPutString(params, "previewMediaId", devAppStringFlag(cmd, "preview-media-id"))
return params, nil
}
func devAppRobotConfigParams(cmd *cobra.Command, appID string) (map[string]any, int, error) {
params := map[string]any{"unifiedAppId": appID}
updates := 0
setString := func(key, flag string) {
if v := devAppStringFlag(cmd, flag); v != "" {
params[key] = v
updates++
}
}
setString("name", "name")
setString("brief", "brief")
setString("desc", "desc")
setString("iconMediaId", "icon-media-id")
setString("outgoingUrl", "outgoing-url")
setString("eventCallbackUrl", "event-callback-url")
if cmd.Flags().Changed("mode") {
mode := strings.ToUpper(strings.TrimSpace(devAppStringFlag(cmd, "mode")))
switch mode {
case "HTTPS", "STREAM", "AISKILL":
params["mode"] = mode
default:
return nil, 0, apperrors.NewValidation("--mode 仅支持 HTTPS、STREAM、AISKILL")
}
updates++
}
if cmd.Flags().Changed("add-scope") {
value, _ := cmd.Flags().GetBool("add-scope")
params["addScope"] = value
updates++
}
if cmd.Flags().Changed("disable-ssl-verify") {
value, _ := cmd.Flags().GetBool("disable-ssl-verify")
params["disableSSLVerify"] = value
updates++
}
if values := parseDevAppListFlag(cmd, "skills"); len(values) > 0 {
params["skills"] = values
updates++
}
for _, item := range []struct{ key, flag string }{
{"i18nName", "i18n-name"},
{"i18nBrief", "i18n-brief"},
{"i18nDescription", "i18n-description"},
} {
raw := devAppStringFlag(cmd, item.flag)
if raw == "" {
continue
}
parsed := map[string]any{}
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
return nil, 0, apperrors.NewValidation(fmt.Sprintf("--%s 必须是合法 JSON 对象:%v", item.flag, err))
}
params[item.key] = parsed
updates++
}
return params, updates, nil
}
// ---------------------------------------------------------------------------
// 版本发布能力
// ---------------------------------------------------------------------------
func newDevAppVersionCreateCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "create",
Short: "基于当前配置创建应用新版本",
Example: " dws dev app version create --unified-app-id UNIFIED_APP_ID --desc \"新增机器人能力\" --dry-run --format json",
Tool: devAppVersionCreateTool,
Safety: devAppSafetyHighWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "version", Usage: "高级可选:显式版本号,如 1.0.1;默认不传,由服务端基于最新已发布版本自动递增", Bind: "version", Trim: true, OmitEmpty: true},
{Name: "desc", Usage: "版本描述", Bind: "desc", Trim: true, OmitEmpty: true},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "create_dev_app_version",
CanonicalPath: "dev.create_dev_app_version",
CLIPath: "dev app version create",
PrimaryCLIPath: "dev app version create",
},
Description: "基于当前配置创建应用新版本",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "为应用当前配置创建待发布版本",
UseWhen: []string{"配置变更完成后需要生成版本进入发布流程时"},
AvoidWhen: []string{"只是查看已有版本时使用 dev app version list"},
Examples: []string{`dws dev app version create --unified-app-id <unifiedAppId> --desc "新增机器人能力" --dry-run`},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppVersionCreateTool),
})
}
func newDevAppVersionListCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "list",
Short: "分页查询应用版本列表",
Example: " dws dev app version list --unified-app-id UNIFIED_APP_ID --page-size 20 --format json",
Tool: devAppVersionListTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "list_dev_app_versions",
CanonicalPath: "dev.list_dev_app_versions",
CLIPath: "dev app version list",
PrimaryCLIPath: "dev app version list",
},
Description: "分页查询应用版本列表",
DryRun: devAppDryRun,
Result: devAppPaginatedItemsResult("当前页开放平台应用版本查询结果", "当前页版本记录"),
Pagination: devAppCursorPagination(),
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "分页列出应用的历史和待发布版本",
UseWhen: []string{"需要查找 versionId 或浏览版本记录时"},
AvoidWhen: []string{"已知版本并需详情或状态时使用 get 或 status"},
Examples: []string{"dws dev app version list --unified-app-id <unifiedAppId> --page-size 20"},
},
},
Call: devAppCallCursor(runner),
PostMount: devAppMetaCursor(devAppVersionListTool),
})
}
func newDevAppVersionGetCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "get",
Short: "查询指定版本详情",
Example: " dws dev app version get --unified-app-id UNIFIED_APP_ID --version-id VERSION_ID --format json",
Tool: devAppVersionDetailTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "version-id", Usage: "版本 ID (必填)", Bind: "versionId", Trim: true, Required: true, RequiredHint: "--version-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "get_dev_app_version_detail",
CanonicalPath: "dev.get_dev_app_version_detail",
CLIPath: "dev app version get",
PrimaryCLIPath: "dev app version get",
},
Description: "查询指定版本详情",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "获取指定应用版本的详细内容",
UseWhen: []string{"已知 versionId 并需要核对版本配置时"},
AvoidWhen: []string{"查看发布进度时使用 dev app version status"},
Examples: []string{"dws dev app version get --unified-app-id <unifiedAppId> --version-id <versionId>"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppVersionDetailTool),
})
}
func newDevAppVersionCheckApprovalCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "check-approval",
Short: "预检版本发布是否需要审批(不实际发布)",
Example: " dws dev app version check-approval --unified-app-id UNIFIED_APP_ID --version-id VERSION_ID --format json",
Tool: devAppVersionPublishTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "version-id", Usage: "版本 ID (必填)", Bind: "versionId", Trim: true, Required: true, RequiredHint: "--version-id 为必填"},
},
ConstParams: map[string]any{"precheckOnly": true},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev", Name: "version_check_approval", CanonicalPath: "dev.version_check_approval",
CLIPath: "dev app version check-approval", PrimaryCLIPath: "dev app version check-approval",
},
Description: "预检版本发布是否需要审批,不执行发布",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "预检应用版本的审批要求和候选审批人",
UseWhen: []string{"发布版本前确认是否需要审批及候选审批人"},
AvoidWhen: []string{"实际发布版本时使用 dev app version publish"},
Examples: []string{"dws dev app version check-approval --unified-app-id <unifiedAppId> --version-id <versionId>"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppVersionPublishTool),
})
}
func newDevAppVersionPublishCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "publish",
Short: "发布指定版本(含高敏权限需 --confirmed-sensitive)",
Example: " dws dev app version publish --unified-app-id UNIFIED_APP_ID --version-id VERSION_ID --dry-run --format json",
Tool: devAppVersionPublishTool,
Safety: devAppSafetyHighWrite(),
// devapp 旧版写守卫为 guard-first:确认门先于参数校验。
ConfirmFirst: true,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "version-id", Usage: "版本 ID (必填)", Bind: "versionId", Trim: true, Required: true, RequiredHint: "--version-id 为必填"},
{Name: "approver-user-id", Usage: "灰度选人模式下指定审批人 userId", Bind: "approverUserId", Trim: true, OmitEmpty: true},
{Name: "confirmed-sensitive", Usage: "确认发布包含高敏权限的版本", Kind: LeafBool, Bind: "confirmedSensitive"},
},
ConstParams: map[string]any{"precheckOnly": false},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "publish_dev_app_version",
CanonicalPath: "dev.publish_dev_app_version",
CLIPath: "dev app version publish",
PrimaryCLIPath: "dev app version publish",
},
Description: "发布指定版本(含高敏权限需 --confirmed-sensitive)",
DryRun: devAppDryRun,
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "发布开放平台应用指定版本(可先预检)",
UseWhen: []string{"版本已创建,需要预检或正式发布到线上"},
AvoidWhen: []string{"还没有 versionId 时先 version create / list / get", "含高敏权限但尚未确认时不要正式发布"},
Examples: []string{"dws dev app version publish --unified-app-id <unifiedAppId> --version-id <versionId> --dry-run --format json"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppVersionPublishTool),
})
}
func newDevAppVersionStatusCommand(runner executor.Runner) *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "status",
Short: "查询版本发布/审批状态",
Example: " dws dev app version status --unified-app-id UNIFIED_APP_ID --version-id VERSION_ID --format json",
Tool: devAppVersionStatusTool,
Safety: devAppSafetyRead(),
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true, Required: true, RequiredHint: "--unified-app-id 为必填"},
{Name: "version-id", Usage: "版本 ID (必填)", Bind: "versionId", Trim: true, Required: true, RequiredHint: "--version-id 为必填"},
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "dev",
Name: "get_dev_app_version_status",
CanonicalPath: "dev.get_dev_app_version_status",
CLIPath: "dev app version status",
PrimaryCLIPath: "dev app version status",
},
Description: "查询版本发布/审批状态",
DryRun: devAppDryRun,
Result: &contract.ResultSpec{
Outcomes: []contract.ResultOutcome{contract.ResultOutcomePending, contract.ResultOutcomeFailure, contract.ResultOutcomeSuccess},
DataSchema: json.RawMessage(`{"type":"object","description":"应用版本发布或审批状态","properties":{"unifiedAppId":{"type":"string","description":"开放平台统一应用 ID"},"versionId":{"type":"string","description":"应用版本 ID"},"status":{"type":"string","description":"归一化版本状态"},"versionStatus":{"type":"string","description":"服务端版本状态"},"approvalStatus":{"type":"string","description":"版本审批状态"},"nextCommand":{"type":"string","description":"状态未终结时可执行的下一条命令"}},"required":["versionId"],"additionalProperties":true}`),
},
Interface: devAppCompositeInterface(),
Selection: contract.SelectionSpec{
AgentSummary: "查询指定应用版本的发布或审批状态",
UseWhen: []string{"需要判断版本是否已发布、审核中或受阻时"},
AvoidWhen: []string{"需要版本配置详情时使用 dev app version get"},
Examples: []string{"dws dev app version status --unified-app-id <unifiedAppId> --version-id <versionId>"},
},
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppVersionStatusTool),
})
}
func requiredDevAppUnifiedID(cmd *cobra.Command) (string, error) {
appID := devAppStringFlag(cmd, "unified-app-id")
if appID == "" {
return "", apperrors.NewValidation("--unified-app-id 为必填")
}
return appID, nil
}
// annotateDevAppTool tags a leaf command with the MCP tool name it invokes, so
// `dws schema dev.app...` can resolve command → tool → live op-app schema
// without re-deriving the mapping. The annotation is the single source of truth
// for the schema renderer (see internal/cli/dev_schema.go).
func annotateDevAppTool(cmd *cobra.Command, tool string) *cobra.Command {
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
cmd.Annotations["mcp-tool"] = tool
cmd.Annotations["mcp-source"] = "op-app"
return cmd
}
// devAppLeafMeta 是 devapp 叶子命令统一的 PostMount 收尾:设置 NoArgs /
// DisableAutoGenTag,并调用 preferLegacyLeaf + annotateDevAppTool。供迁移到
// LeafSpec 的命令在 LeafSpec.PostMount 里复用,保持与手写版逐字等价。
func devAppLeafMeta(cmd *cobra.Command, tool string) {
cmd.Args = cobra.NoArgs
cmd.DisableAutoGenTag = true
preferLegacyLeaf(cmd)
annotateDevAppTool(cmd, tool)
// This leaf has migrated to the unified result framework. Consumers keep using --format;
// the active contract is a release property, not an Agent-selected flag.
output.SetCommandRollout(cmd, output.RolloutUnifiedActive)
}
// devAppCall 返回统一派发闭包(替代各命令重复的 Call: runDevAppTool 透传)。
// 参数装配由 Flags/ConstParams 完成;本闭包只负责执行与响应处理。
func devAppCall(runner executor.Runner) func(*cobra.Command, string, map[string]any) error {
return func(cmd *cobra.Command, tool string, params map[string]any) error {
return runDevAppTool(runner, cmd, tool, params)
}
}
// devAppMeta 返回纯收尾 PostMount 闭包(无额外 flag 的命令用)。
func devAppMeta(tool string) func(*cobra.Command) {
return func(cmd *cobra.Command) { devAppLeafMeta(cmd, tool) }
}
// devAppCompositeInterfaceReason 是 devapp 全树共用的评审 interface 说明(非 pin
// MCP 元数据的远程适配器),作为 contract.InterfaceSpec.Reason 的最终发布值。
const devAppCompositeInterfaceReason = "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command."
// devapp 的 SafetySpec 直接对齐 Agent Runtime Schema。函数每次返回一个值
// 副本,避免共享可变状态;四个字段彼此独立,不从 effect 或 risk 推导。
func devAppSafetyRead() contract.SafetySpec {
return contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
}
}
func devAppSafetyWrite() contract.SafetySpec {
return contract.SafetySpec{
Effect: "write", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
}
}
func devAppSafetyHighWrite() contract.SafetySpec {
return contract.SafetySpec{
Effect: "write", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
}
}
func devAppSafetyDestructive() contract.SafetySpec {
return contract.SafetySpec{
Effect: "destructive", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
}
}
// devAppDryRun 是 devapp 全树共用的 dry_run 最终声明:本地拼装调用预览
// (invocation preview),dry-run 不发起任何远端读。
var devAppDryRun = &contract.DryRunSpec{
PreviewKind: "invocation",
RemoteReads: false,
}
// devAppCompositeInterface 是 devapp 全树共用的 Interface 最终声明。
func devAppCompositeInterface() *contract.InterfaceSpec {
return &contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: devAppCompositeInterfaceReason}
}
func runDevAppTool(runner executor.Runner, cmd *cobra.Command, tool string, params map[string]any) error {
invocation := executor.NewHelperInvocation(
cobracmd.LegacyCommandPath(cmd),
devAppProduct,
tool,
params,
)
invocation.DryRun = commandDryRun(cmd)
result, err := runner.Run(cmd.Context(), invocation)
if err != nil {
return err
}
// The requested tool is the local contract authority. Do not let an empty or
// stale runner echo disable tool-specific fail-closed projection rules.
result.Invocation.Tool = tool
// Unwrap the ServiceResult envelope and apply per-tool response fixes before
// rendering, so agents read the inner payload directly and pretty-annotation
// walks the already-normalized content.
result = normalizeDevAppToolResult(tool, normalizeDevAppServiceResult(result))
if devAppPrettyWanted(cmd) {
devAppPrettyAnnotate(tool, result.Response)
}
return writeDevAppEnvelope(cmd, result)
}
func devAppCommandResult(result executor.Result) output.CommandResult {
data := devAppEnvelopeData(result)
if content, ok := data.(map[string]any); ok {
if partial := devAppMultiProfileResult(content); partial != nil {
return partial
}
if failure := devAppFailureResult(content); failure != nil {
return failure
}
// check-approval reports what a later publish would require; it does not
// itself accept an asynchronous operation.
precheckOnly, _ := result.Invocation.Params["precheckOnly"].(bool)
if !precheckOnly {
if pending := devAppPendingResult(content); pending != nil {
return pending
}
}
}
successOptions := make([]output.ResultOption, 0, 2)
if result.Invocation.DryRun {
successOptions = append(successOptions, output.WithDryRun())
}
if meta, err := devAppPaginationMeta(data); err != nil {
return output.Failure(&output.ErrorInfo{
Type: "api", Subtype: "pagination_inconsistent", Message: err.Error(),
Hint: "保留原始响应并停止翻页;不要把当前页当作完整结果。",
})
} else if meta != nil {
successOptions = append(successOptions, output.WithMeta(meta))
return output.Success(devAppDataWithoutPagination(data), successOptions...)
} else if devAppToolRequiresPagination(result.Invocation.Tool) && !result.Invocation.DryRun {
// A dry-run payload is a completed local invocation preview, not a
// server list response. Only real responses must prove pagination.
return output.Failure(&output.ErrorInfo{
Type: "api",
Subtype: "pagination_inconsistent",
Message: "declared paginated response is missing hasMore and nextCursor",
Hint: "保留原始响应并停止翻页;不要把当前页当作完整结果。",
})
}
return output.Success(data, successOptions...)
}
func devAppToolRequiresPagination(tool string) bool {
switch strings.TrimSpace(tool) {
case devAppListTool, devAppPermissionListTool, devAppEventListTool, devAppVersionListTool:
return true
default:
return false
}
}
// DevAppCommandResultFromPayload is the shared dingtalk-dev outcome mapper for
// the native `dev ...` tree and the existing `devapp +...` shortcut tree. Both
// entry points must classify the same upstream payload into the same unified
// outcome; only command routing and projection differ.
func DevAppCommandResultFromPayload(tool string, payload any, dryRun bool) output.CommandResult {
response := map[string]any{"content": payload}
if object, ok := payload.(map[string]any); ok {
if _, wrapped := object["content"]; wrapped {
response = object
}
}
result := executor.Result{
Invocation: executor.Invocation{
Implemented: true,
Kind: "helper_invocation",
DryRun: dryRun,
Tool: tool,
},
Response: response,
}
result = normalizeDevAppServiceResult(result)
if strings.TrimSpace(tool) != "" {
result = normalizeDevAppToolResult(tool, result)
}
return devAppCommandResult(result)
}
// writeDevRolloutResult is the gradual migration seam shared by dingtalk-dev.
// The operation is executed exactly once; only the renderer changes. Legacy
// remains active only for commands that have not advanced to unified_active.
func writeDevRolloutResult(cmd *cobra.Command, result output.CommandResult, legacy *output.Envelope, fallback output.Format) error {
if output.UsesUnifiedResult(cmd) {
return output.StoreResult(cmd.Context(), result)
}
if output.CommandRollout(cmd) == output.RolloutDualValidate {
// Shadow-build/validate the unified result without a second business invocation and
// without changing stdout. Metrics can be added around this seam later.
if err := output.ValidateResult(result); err != nil {
return err
}
}
return output.WriteEnvelope(cmd, legacy, fallback)
}
// writeDevAppEnvelope 是 dev app 全树的统一信封出口(统一输出 dev 域试点,
// 队列 Phase F)。成功 → ok:true/outcome:success + data;--dry-run →
// ok:true/outcome:success + dry_run:true:dry-run 是已完成的预演,不是异步未终态。
// exit 0,参数非法仍走 validation 报错——错误路径继续由 apperrors 通道承载)。
// json(默认)输出完整信封(唯一 JSON 契约);其余 format 渲染业务数据。
// 复用 internal/output 的权威 Envelope 类型与 WriteEnvelope 出口函数。
func writeDevAppEnvelope(cmd *cobra.Command, result executor.Result) error {
env := &output.Envelope{
OK: true,
Outcome: output.OutcomeSuccess,
Data: devAppEnvelopeData(result),
}
if result.Invocation.DryRun {
env.DryRun = true
} else {
env.Meta, _ = devAppPaginationMeta(env.Data)
}
return writeDevRolloutResult(cmd, devAppCommandResult(result), env, output.FormatJSON)
}
// devAppEnvelopeData 从工具调用结果中提取业务载荷(L2):已实现的
// helper/compat 调用把载荷放在 response.content 下;其余形态(如 dry-run
// 预演的 Result 整体)原样透传,与 output.unwrapCompatRuntimePayload 的
// 解包规则保持一致,保证 data 即既有消费方看到的载荷。
func devAppEnvelopeData(result executor.Result) any {
if result.Invocation.Implemented {
switch result.Invocation.Kind {
case "compat_invocation", "helper_invocation":
if content, ok := result.Response["content"]; ok {
return content
}
}
}
return result
}
// devAppPaginationMeta 把列表载荷里的 cursor 分页字段投影到 meta.pagination
// (契约规范 §3:分页元数据挂 meta 层)。CLI 只观察服务端
// 返回的 hasMore/nextCursor,不做合成。hasMore=true 且带 nextCursor →
// endpoint_exhausted:false + next_token(可续跑);hasMore=false →
// endpoint_exhausted:true。hasMore=true 却无 cursor 时不产出分页元数据,
// 避免违反「endpoint_exhausted:false 必须携带 next_token」。统一结果通过
// devAppDataWithoutPagination 从 data 剥离源控制字段;legacy renderer 不变。
func devAppPaginationMeta(payload any) (*output.Meta, error) {
m, ok := payload.(map[string]any)
if !ok {
return nil, nil
}
rawHasMore, hasFlag := m["hasMore"]
hasMore, hasMoreBool := rawHasMore.(bool)
if hasFlag && !hasMoreBool {
return nil, fmt.Errorf("pagination hasMore must be a JSON boolean")
}
cursor := ""
rawCursor, hasCursor := m["nextCursor"]
if hasCursor {
value, stringOK := rawCursor.(string)
if !stringOK {
return nil, fmt.Errorf("pagination nextCursor must be a JSON string")
}
cursor = strings.TrimSpace(value)
}
if !hasFlag && !hasCursor {
return nil, nil
}
pg := &output.Pagination{}
switch {
case hasMore && cursor != "":
pg.EndpointExhausted = false
pg.NextToken = cursor
case hasFlag && hasMore:
return nil, fmt.Errorf("pagination hasMore=true is missing nextCursor")
case hasFlag && !hasMore:
// DingTalk may echo a terminal cursor even when hasMore=false. The
// boolean is the authoritative exhaustion signal; never expose that
// non-resumable cursor as meta.pagination.next_token.
pg.EndpointExhausted = true
case !hasFlag && hasCursor && cursor != "":
pg.EndpointExhausted = false
pg.NextToken = cursor
default:
return nil, fmt.Errorf("pagination nextCursor is empty without an exhaustion signal")
}
return &output.Meta{Pagination: pg}, nil
}
func devAppDataWithoutPagination(payload any) any {
object, ok := payload.(map[string]any)
if !ok {
return payload
}
data := make(map[string]any, len(object))
for key, value := range object {
switch key {
case "hasMore", "nextCursor":
continue
default:
data[key] = value
}
}
return data
}
func devAppMultiProfileResult(content map[string]any) output.CommandResult {
if !devAppContentBool(content, "multiProfile") {
return nil
}
profiles, ok := content["profiles"].([]any)
if !ok || len(profiles) == 0 {
return nil
}
succeeded := make([]any, 0, len(profiles))
failed := make([]output.PartialFailedEntry, 0)
unknown := make([]output.PartialUnknownEntry, 0)
for i, raw := range profiles {
entry, ok := raw.(map[string]any)
if !ok {
unknown = append(unknown, output.PartialUnknownEntry{
ID: fmt.Sprintf("profile-%d", i+1),
Reason: "profile result is malformed; terminal state cannot be confirmed",
})
continue
}
id := devAppFirstContentString(entry, "selector", "profile")
if id == "" {
id = fmt.Sprintf("profile-%d", i+1)
}
if devAppContentBool(entry, "ok") {
preserved := make(map[string]any, len(entry)+1)
for key, value := range entry {
preserved[key] = value
}
if _, exists := preserved["id"]; !exists {
preserved["id"] = id
}
succeeded = append(succeeded, preserved)
continue
}
errorInfo := &output.ErrorInfo{Type: "api", Message: "profile execution failed"}
if errorMap, ok := entry["error"].(map[string]any); ok {
errorInfo = devAppErrorInfo(errorMap, "profile execution failed")
if category := devAppFirstContentString(errorMap, "type", "category"); category != "" {
errorInfo.Type = devAppWireErrorType(category)
}
errorInfo.Subtype = devAppFirstContentString(errorMap, "subtype", "reason")
errorInfo.Stage = devAppFirstContentString(errorMap, "stage")
errorInfo.Origin = devAppFirstContentString(errorMap, "origin")
errorInfo.Operation = devAppFirstContentString(errorMap, "operation")
errorInfo.RequestID = devAppFirstContentString(errorMap, "request_id", "requestId")
errorInfo.TraceID = devAppFirstContentString(errorMap, "trace_id", "traceId")
errorInfo.Hint = devAppFirstContentString(errorMap, "hint")
if retryable, present := errorMap["retryable"].(bool); present {
errorInfo.Retryable = retryable
}
if executionStarted, present := errorMap["execution_started"].(bool); present {
errorInfo.ExecutionStarted = &executionStarted
}
if actions, present := errorMap["actions"].([]string); present {
errorInfo.Actions = append([]string(nil), actions...)
} else if rawActions, present := errorMap["actions"].([]any); present {
for _, rawAction := range rawActions {
if action, ok := rawAction.(string); ok {
errorInfo.Actions = append(errorInfo.Actions, action)
}
}
}
if details, present := errorMap["details"].(map[string]any); present {
errorInfo.Details = details
}
}
failed = append(failed, output.PartialFailedEntry{ID: id, Error: errorInfo})
}
if len(failed) == 0 && len(unknown) == 0 {
return nil
}
if len(succeeded) == 0 {
details := make([]any, 0, len(failed)+len(unknown))
for _, entry := range failed {
details = append(details, map[string]any{"id": entry.ID, "error": entry.Error})
}
for _, entry := range unknown {
details = append(details, map[string]any{"id": entry.ID, "unknown_reason": entry.Reason})
}
return output.Failure(&output.ErrorInfo{
Type: "api",
Message: "no selected profile has a confirmed success",
Details: map[string]any{"profiles": details},
})
}
partial, err := output.NewPartialData(len(profiles), succeeded, failed, unknown)
if err != nil {
return output.Failure(&output.ErrorInfo{Type: "internal", Message: err.Error()})
}
return output.Partial(partial)
}
func devAppFailureResult(content map[string]any) output.CommandResult {
status := strings.ToUpper(devAppFirstContentString(content, "status", "taskStatus", "versionStatus", "processStatus"))
if rawSuccess, present := content["success"]; present {
success, isBool := rawSuccess.(bool)
if !isBool {
return output.Failure(&output.ErrorInfo{
Type: "api",
Subtype: "invalid_success_type",
Message: "dev response success field must be a JSON boolean",
Hint: "写操作先核查目标状态;读取操作保留脱敏响应证据后排查上游。",
Operation: "devapp.response_projection",
})
}
if !success {
return output.Failure(devAppErrorInfo(content, "dev operation failed"))
}
}
if status == "FAIL" || status == "FAILED" || status == "EXPIRED" {
return output.Failure(devAppErrorInfo(content, "dev operation "+strings.ToLower(status)))
}
return nil
}
func devAppErrorInfo(content map[string]any, fallback string) *output.ErrorInfo {
message := devAppFirstContentString(content, "errorMsg", "errorMessage", "message")
if message == "" {
message = fallback
}
info := &output.ErrorInfo{Type: "api", Message: message}
if code, ok := content["errorCode"]; ok {
info.UpstreamCode = code
} else if code, ok := content["code"]; ok {
info.UpstreamCode = code
}
return info
}
func devAppWireErrorType(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "api", "auth", "validation", "permission", "discovery", "internal":
return strings.ToLower(strings.TrimSpace(raw))
case "authorization", "forbidden":
return "permission"
default:
// Upstream transport labels such as network/timeout are subtypes of an
// API operation failure, not new top-level Agent branch keys.
return "api"
}
}
func devAppPendingResult(content map[string]any) output.CommandResult {
state := strings.ToUpper(devAppFirstContentString(content,
"completionState", "status", "taskStatus", "versionStatus", "processStatus", "approvalStatus"))
nonTerminal := !devAppContentBool(content, "terminal") && (devAppContentBool(content, "mustContinue") || devAppContentBool(content, "mustAskUser"))
approvalPending := devAppContentBool(content, "approvalSubmitted") && !devAppContentBool(content, "published")
isPendingState := state == "WAITING" || state == "PENDING" || state == "PROCESSING" || state == "AUDIT" ||
state == "UNDER_REVIEW" || strings.HasPrefix(state, "WAITING_") || strings.HasPrefix(state, "BLOCKED_BY_")
if !isPendingState && !nonTerminal && !approvalPending {
return nil
}
if state == "" {
state = "WAITING_FOR_ACTION"
}
id := devAppFirstContentString(content, "taskId", "versionId", "unifiedAppId", "requestId")
if id == "" {
return output.Failure(&output.ErrorInfo{Type: "internal", Message: "non-terminal dev response is missing an operation identifier"})
}
next := devAppFirstNextCommand(content)
if next == "" {
next = devAppRecoveryCommand(content)
}
if next == "" {
return output.Failure(&output.ErrorInfo{Type: "internal", Message: "non-terminal dev response is missing a recovery command"})
}
return output.Pending(content, &output.OperationInfo{ID: id, State: strings.ToLower(state), NextCommand: next})
}
func devAppRecoveryCommand(content map[string]any) string {
if taskID := devAppFirstContentString(content, "taskId"); taskID != "" {
return fmt.Sprintf("dws dev app robot result --task-id %s --format json", taskID)
}
appID := devAppFirstContentString(content, "unifiedAppId")
versionID := devAppFirstContentString(content, "versionId")
if appID != "" && versionID != "" {
return fmt.Sprintf("dws dev app version status --unified-app-id %s --version-id %s --format json", appID, versionID)
}
return ""
}
func devAppFirstNextCommand(content map[string]any) string {
steps, ok := content["nextSteps"].([]map[string]any)
if ok {
for _, step := range steps {
if command := devAppFirstContentString(step, "command", "dryRunCommand"); command != "" {
return command
}
}
}
if rawSteps, ok := content["nextSteps"].([]any); ok {
for _, raw := range rawSteps {
if step, ok := raw.(map[string]any); ok {
if command := devAppFirstContentString(step, "command", "dryRunCommand"); command != "" {
return command
}
}
}
}
return ""
}
// normalizeDevAppServiceResult unwraps the op-app ServiceResult envelope
// ({content:{success:true, result:{...}}}) down to its inner result, so a
// successful tool call renders its payload directly instead of the wrapper.
func normalizeDevAppServiceResult(result executor.Result) executor.Result {
content, ok := result.Response["content"].(map[string]any)
if !ok {
return result
}
if success, ok := content["success"].(bool); !ok || !success {
return result
}
value, ok := content["result"]
if !ok || value == nil {
return result
}
result.Response["content"] = value
return result
}
// normalizeDevAppToolResult applies per-tool response shape fixes: flatten
// remove-permission's removedScopeValues to a string array, stamp explicit
// lifecycle booleans, and enrich async robot creation results with next steps.
func normalizeDevAppToolResult(tool string, result executor.Result) executor.Result {
content, ok := result.Response["content"].(map[string]any)
if !ok {
return result
}
switch tool {
case devAppPermissionRmTool:
normalizeDevAppScopeValueArray(content, "removedScopeValues")
case devAppDisableTool:
if _, ok := content["disabled"]; !ok {
content["disabled"] = true
}
case devAppEnableTool:
if _, ok := content["enabled"]; !ok {
content["enabled"] = true
}
case devAppVersionPublishTool:
normalizeDevAppVersionApproval(content)
case devAppRobotResultTool:
normalizeDevAppRobotResult(content)
}
return result
}
func normalizeDevAppVersionApproval(content map[string]any) {
candidates, ok := content["approvalCandidates"].([]any)
if !ok || len(candidates) == 0 {
return
}
options := make([]map[string]any, 0, len(candidates))
for i, raw := range candidates {
candidate, ok := raw.(map[string]any)
if !ok {
continue
}
userID := devAppFirstContentString(candidate, "userId", "userID", "userid", "staffId")
name := devAppFirstContentString(candidate, "name", "userName", "displayName", "nick", "nickName")
mainAdmin := devAppContentBool(candidate, "mainAdmin")
label := devAppApprovalCandidateLabel(name, userID, mainAdmin)
if label == "" {
label = fmt.Sprintf("候选审批人 %d", i+1)
}
option := map[string]any{
"index": i + 1,
"key": devAppOptionKey(i),
"label": label,
"name": name,
"userId": userID,
"mainAdmin": mainAdmin,
}
options = append(options, option)
}
if len(options) == 0 {
return
}
content["approvalOptions"] = options
approvalMode := strings.ToUpper(devAppContentString(content, "approvalMode"))
if approvalMode != "SELECT_APPROVER" {
return
}
unifiedAppID := devAppContentString(content, "unifiedAppId")
if unifiedAppID == "" {
unifiedAppID = "<unifiedAppId>"
}
versionID := devAppContentString(content, "versionId")
if versionID == "" {
versionID = "<versionId>"
}
// 预渲染一段"原样照抄即可"的审批人列表:序号复用 approvalOptions[].key
// (A-Z 后转数字),label 已是「姓名(userId: xxx)」。agent 直接展示
// approvalPromptText 即可,无需自己遍历 approvalOptions——此前有 agent 误把
// approvalOptions 当成 [{options:[...]}]、取空后只回退显示 userId,姓名全丢。
title := fmt.Sprintf("版本发布需要审批,请选择一位审批人(共 %d 位):", len(options))
var promptBuilder strings.Builder
promptBuilder.WriteString(title)
for _, opt := range options {
key, _ := opt["key"].(string)
label, _ := opt["label"].(string)
promptBuilder.WriteString(fmt.Sprintf("\n%s. %s", key, label))
}
promptText := promptBuilder.String()
content["completionState"] = "WAITING_FOR_APPROVER_SELECTION"
content["actionRequired"] = "select_approver"
content["mustAskUser"] = true
content["requiresUserInput"] = true
content["terminal"] = false
content["approvalPromptText"] = promptText
content["message"] = "版本发布需要选择审批人;请原样展示 approvalPromptText 的完整内容,等待用户选择,不要只显示 userId、不要自行截取、不能默认取第一个"
content["nextSteps"] = []map[string]any{
{
"id": "select_approver",
"blocking": true,
"requiresUserInput": true,
"doneWhen": "用户从 approvalOptions 中选择一位审批人,得到对应 userId",
},
devAppNextStep(devAppStep{
ID: "publish_version",
Command: fmt.Sprintf("dws dev app version publish --unified-app-id %s --version-id %s --approver-user-id <selectedUserId> --format json", unifiedAppID, versionID),
DryRunCommand: fmt.Sprintf("dws dev app version publish --unified-app-id %s --version-id %s --approver-user-id <selectedUserId> --dry-run --format json", unifiedAppID, versionID),
DoneWhen: "approvalSubmitted=true、versionStatus=AUDIT 或 processStatus=UNDER_REVIEW 表示已提交审批;published=true 表示已发布",
Blocking: true,
}),
}
}
func devAppApprovalCandidateLabel(name, userID string, mainAdmin bool) string {
label := strings.TrimSpace(name)
switch {
case label != "" && userID != "":
label = fmt.Sprintf("%s(userId: %s)", label, userID)
case label == "" && userID != "":
label = "userId: " + userID
}
if label != "" && mainAdmin {
label += "(主管理员)"
}
return label
}
func devAppOptionKey(index int) string {
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
if index >= 0 && index < len(letters) {
return string(letters[index])
}
return fmt.Sprintf("%d", index+1)
}
func normalizeDevAppRobotResult(content map[string]any) {
status := strings.ToUpper(devAppContentString(content, "status"))
if status == "" {
return
}
taskID := devAppContentString(content, "taskId")
clientID := devAppFirstContentString(content, "clientId", "appKey")
clientSecret := devAppFirstContentString(content, "clientSecret", "appSecret")
unifiedAppID := devAppContentString(content, "unifiedAppId")
localConnectReady := clientID != "" && clientSecret != ""
lifecycle := map[string]any{
"status": status,
"localConnectReady": false,
"localOnlyReady": false,
"publicUseReady": false,
"requiresVersionPublish": false,
"robotTaskDone": false,
"overallComplete": false,
}
var steps []map[string]any
switch status {
case "WAITING":
lifecycle["phase"] = "creating"
lifecycle["completionGate"] = "robot_result"
if interval := content["intervalSeconds"]; interval != nil {
lifecycle["retryAfterSeconds"] = interval
}
steps = append(steps, devAppRobotPollStep(taskID))
case "SUCCESS", "APPROVAL_REQUIRED":
lifecycle["phase"] = "created_pending_publish"
lifecycle["localConnectReady"] = localConnectReady
lifecycle["localOnlyReady"] = localConnectReady
lifecycle["requiresVersionPublish"] = true
lifecycle["robotTaskDone"] = true
if unifiedAppID == "" {
lifecycle["completionGate"] = "provide_unified_app_id"
lifecycle["blockingStepIds"] = []string{"provide_unified_app_id"}
steps = append(steps, devAppRobotProvideUnifiedAppIDStep())
devAppMarkMissingUnifiedAppIDBlocked(content)
} else {
lifecycle["completionGate"] = "version_publish"
lifecycle["blockingStepIds"] = devAppRobotPublishStepIDs()
steps = append(steps, devAppRobotPublishSteps(unifiedAppID)...)
devAppMarkVersionPublishBlocked(content)
}
if localConnectReady {
steps = append(steps, devAppRobotConnectStep(clientID, unifiedAppID))
}
case "FAIL":
lifecycle["phase"] = "failed"
lifecycle["robotTaskDone"] = true
lifecycle["completionGate"] = "retry_robot_submit"
steps = append(steps, devAppRobotRetryStep(taskID, true))
case "EXPIRED":
lifecycle["phase"] = "expired"
lifecycle["robotTaskDone"] = true
lifecycle["completionGate"] = "retry_robot_submit"
steps = append(steps, devAppRobotRetryStep(taskID, false))
default:
lifecycle["phase"] = "unknown"
}
content["lifecycle"] = lifecycle
if len(steps) > 0 {
content["nextSteps"] = steps
}
}
func devAppMarkVersionPublishBlocked(content map[string]any) {
content["completionState"] = "BLOCKED_BY_VERSION_PUBLISH"
content["mustContinue"] = true
content["actionRequired"] = "submit_version_publish"
content["message"] = "本地建联可用,但线上发布/审批未完成;必须继续执行 blocking nextSteps"
content["terminal"] = false
}
func devAppMarkMissingUnifiedAppIDBlocked(content map[string]any) {
content["completionState"] = "BLOCKED_BY_MISSING_UNIFIED_APP_ID"
content["mustContinue"] = true
content["mustAskUser"] = true
content["actionRequired"] = "provide_unified_app_id"
content["message"] = "缺少明确来源的 unifiedAppId,不能用 clientId/appKey 反查后写版本;请提供 dev app create 或 robot result 返回的 unifiedAppId"
content["terminal"] = false
}
func devAppRobotPublishSteps(appID string) []map[string]any {
steps := []map[string]any{
devAppNextStep(devAppStep{
ID: "create_version",
Command: fmt.Sprintf("dws dev app version create --unified-app-id %s --desc \"发布机器人能力\" --format json", appID),
DryRunCommand: fmt.Sprintf("dws dev app version create --unified-app-id %s --desc \"发布机器人能力\" --dry-run --format json", appID),
DoneWhen: "返回 versionId",
Blocking: true,
}),
devAppNextStep(devAppStep{
ID: "check_approval",
Command: fmt.Sprintf("dws dev app version check-approval --unified-app-id %s --version-id <versionId> --format json", appID),
DoneWhen: "返回 requiresApproval、approvalMode、approvalCandidates 等审批信息",
Blocking: true,
}),
devAppNextStep(devAppStep{
ID: "publish_version",
Command: fmt.Sprintf("dws dev app version publish --unified-app-id %s --version-id <versionId> --format json", appID),
DryRunCommand: fmt.Sprintf("dws dev app version publish --unified-app-id %s --version-id <versionId> --dry-run --format json", appID),
DoneWhen: "published=true 表示已发布;approvalSubmitted=true、versionStatus=AUDIT 或 processStatus=UNDER_REVIEW 表示已提交审批;SELECT_APPROVER 时必须先让用户从 approvalCandidates 选择审批人后追加 --approver-user-id",
RequiresUserInput: true,
Blocking: true,
}),
devAppNextStep(devAppStep{
ID: "wait_release",
Command: fmt.Sprintf("dws dev app version status --unified-app-id %s --version-id <versionId> --format json", appID),
DoneWhen: "versionStatus=RELEASE 表示已生效;versionStatus=AUDIT 或 processStatus=UNDER_REVIEW 表示已提交审批,等待审批通过",
Blocking: true,
}),
}
return steps
}
func devAppRobotPublishStepIDs() []string {
return []string{"create_version", "check_approval", "publish_version", "wait_release"}
}
func devAppRobotProvideUnifiedAppIDStep() map[string]any {
return map[string]any{
"id": "provide_unified_app_id",
"blocking": true,
"requiresUserInput": true,
"doneWhen": "用户提供 dev app create 或 robot result 返回的明确 unifiedAppId;不能用 clientId/appKey 自动反查后继续写版本",
}
}
func devAppRobotPollStep(taskID string) map[string]any {
if taskID == "" {
taskID = "<taskId>"
}
return devAppNextStep(devAppStep{
ID: "poll_robot_result",
Command: fmt.Sprintf("dws dev app robot result --task-id %s --format json", taskID),
DoneWhen: "status 变为 SUCCESS、APPROVAL_REQUIRED、FAIL 或 EXPIRED",
Blocking: true,
})
}
func devAppRobotRetryStep(taskID string, reuseTaskID bool) map[string]any {
taskIDFlag := ""
if reuseTaskID {
if taskID == "" {
taskID = "<taskId>"
}
taskIDFlag = " --task-id " + taskID
}
return devAppNextStep(devAppStep{
ID: "retry_robot_submit",
Command: fmt.Sprintf("dws dev app robot submit --name <name> --robot-name <robotName> --desc <desc>%s --format json", taskIDFlag),
DryRunCommand: fmt.Sprintf("dws dev app robot submit --name <name> --robot-name <robotName> --desc <desc>%s --dry-run --format json", taskIDFlag),
DoneWhen: "返回新的 WAITING taskId;FAIL 场景优先复用原 taskId,EXPIRED 场景重新提交",
Blocking: true,
})
}
// devAppRobotConnectStep advertises the local-debug connect command. The
// preferred form is `--unified-app-id`, which reuses `dev app credentials get`
// to fetch clientSecret at runtime — the secret never appears in argv, so it
// stays hidden from `ps` / journald / shell history. Only when unifiedAppID is
// unavailable do we fall back to `--robot-client-id`, and even then we point
// the caller at the safe path in doneWhen instead of hardcoding a
// clientSecret placeholder into the command string.
func devAppRobotConnectStep(clientID, unifiedAppID string) map[string]any {
var command, doneWhen string
if unifiedAppID != "" {
command = fmt.Sprintf("dws dev connect --unified-app-id %s --format json", unifiedAppID)
doneWhen = "本地 Stream 建联成功,进程保持运行;密钥由 credentials get 后台取回,命令行不出现 clientSecret"
} else {
if clientID == "" {
clientID = "<clientId>"
}
command = fmt.Sprintf("dws dev connect --robot-client-id %s --format json", clientID)
doneWhen = "本地 Stream 建联成功;建议改用 --unified-app-id <uappid>,避免 clientSecret 出现在命令行被 ps 看到"
}
step := devAppNextStep(devAppStep{
ID: "connect_local",
Command: command,
DoneWhen: doneWhen,
})
step["sensitiveFields"] = []string{"clientSecret"}
step["optional"] = true
step["scope"] = "local_debug_only"
return step
}
// devAppStep describes one nextSteps entry. Using named fields keeps call sites
// self-documenting instead of relying on a trailing pair of positional bools.
type devAppStep struct {
ID string
Command string
DryRunCommand string
DoneWhen string
RequiresUserInput bool
Blocking bool
}
func devAppNextStep(step devAppStep) map[string]any {
out := map[string]any{
"id": step.ID,
"requiresUserInput": step.RequiresUserInput,
"blocking": step.Blocking,
"doneWhen": step.DoneWhen,
}
if step.Command != "" {
out["command"] = step.Command
}
if step.DryRunCommand != "" {
out["dryRunCommand"] = step.DryRunCommand
}
return out
}
func devAppFirstContentString(content map[string]any, keys ...string) string {
for _, key := range keys {
if value := devAppContentString(content, key); value != "" {
return value
}
}
return ""
}
func devAppContentString(content map[string]any, key string) string {
value, ok := content[key]
if !ok || value == nil {
return ""
}
if text, ok := value.(string); ok {
return strings.TrimSpace(text)
}
return strings.TrimSpace(fmt.Sprint(value))
}
func devAppContentBool(content map[string]any, key string) bool {
value, ok := content[key]
if !ok || value == nil {
return false
}
switch v := value.(type) {
case bool:
return v
case string:
return strings.EqualFold(strings.TrimSpace(v), "true")
default:
return strings.EqualFold(strings.TrimSpace(fmt.Sprint(v)), "true")
}
}
// normalizeDevAppScopeValueArray rewrites an array of scope objects (or strings)
// into a flat string array of scopeValues, leaving the field untouched if any
// element is an unexpected shape.
func normalizeDevAppScopeValueArray(content map[string]any, key string) {
values, ok := content[key].([]any)
if !ok {
return
}
out := make([]string, 0, len(values))
for _, value := range values {
switch typed := value.(type) {
case string:
if typed != "" {
out = append(out, typed)
}
case map[string]any:
if scopeValue, _ := typed["scopeValue"].(string); scopeValue != "" {
out = append(out, scopeValue)
}
}
}
if len(out) == len(values) {
content[key] = out
}
}
func parseDevAppListFlag(cmd *cobra.Command, name string) []string {
raw, _ := cmd.Flags().GetString(name)
return splitDevAppList(raw)
}
func splitDevAppList(raw string) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
raw = strings.ReplaceAll(raw, ";", ",")
parts := strings.Split(raw, ",")
values := make([]string, 0, len(parts))
for _, part := range parts {
if value := strings.TrimSpace(part); value != "" {
values = append(values, value)
}
}
return values
}
// transformDevAppListParam splits a comma/semicolon list for LeafFlag.Transform.
// Empty input (including separator-only) returns nil so optional flags omit the
// key from toolArgs. Required flags that collapse to empty are rejected by
// corecmd.BuildArgs after transform.
func transformDevAppListParam(raw string) (any, error) {
values := splitDevAppList(raw)
if len(values) == 0 {
return nil, nil
}
return values, nil
}
// transformDevAppScopeValues preserves the double-split semantics (each
// comma-separated token may itself be a list).
func transformDevAppScopeValues(raw string) (any, error) {
values := splitDevAppList(raw)
out := make([]string, 0, len(values))
for _, value := range values {
for _, part := range splitDevAppList(value) {
if part != "" {
out = append(out, part)
}
}
}
if len(out) == 0 {
return nil, nil
}
return out, nil
}
// 应用定位:写操作统一只用 --unified-app-id;dev app get 额外支持只读 --app-key。
// --name 定位已下线(列表搜索的 --name/--app-key 是过滤参数、不在此列)。
// 写叶确认门已声明化为 SafetySpec.Confirmation=user_required。
func devAppStringFlag(cmd *cobra.Command, name string) string {
value, _ := cmd.Flags().GetString(name)
return strings.TrimSpace(value)
}
func devAppIntFlag(cmd *cobra.Command, name string) int {
value, _ := cmd.Flags().GetInt(name)
return value
}
func devAppFlagOrFallback(cmd *cobra.Command, primary, fallback string) string {
if value := devAppStringFlag(cmd, primary); value != "" {
return value
}
return devAppStringFlag(cmd, fallback)
}
func devAppPutString(params map[string]any, key, value string) {
if value != "" {
params[key] = value
}
}
func devAppPutInt(params map[string]any, key string, value int) {
if value != 0 {
params[key] = value
}
}