chat

package
v1.0.63 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

Package chat provides declarative shortcuts for the DingTalk chat service (群聊 / 会话 / 消息 / 机器人). Tool names and parameter keys are copied verbatim from internal/helpers/chat.go, the single source of truth for the real MCP tools. Shortcuts route to the correct MCP server via each Shortcut.Product:

  • "chat" (default): tools invoked via the plain helper callMCPTool path
  • "im": tools invoked via callMCPToolOnServer("im", ...)
  • "bot": tools invoked via callMCPToolOnServer("bot", ...)

Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0 (the "License");

Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0 (the "License");

Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0 (the "License");

Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0 (the "License");

Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0 (the "License");

Index

Constants

This section is empty.

Variables

View Source
var ActiveConversations = shortcut.Shortcut{
	OutputRollout: output.RolloutUnifiedActive,
	Service:       "chat",
	Command:       "+recent-conversations",
	Product:       "chat",
	Description:   "列出指定时间以来有新消息的单聊和群聊",
	Intent:        "当你只需要知道最近 24 小时或指定时间范围内哪些单聊或群聊出现了新消息,而不需要读取消息正文时使用;省略 --start 时从有效 --end 往前推 24 小时,省略 --end 时固定为当前时间取整秒;CLI 自动翻页、按 openConversationId 去重,并返回会话名称、类型和时间窗内最新消息时间。",
	Risk:          shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID: "chat",

			Name:           "shortcut_active_conversations",
			CanonicalPath:  "chat.shortcut_active_conversations",
			CLIPath:        "chat +recent-conversations",
			PrimaryCLIPath: "chat +recent-conversations",
			Aliases:        []string{"chat +active-conversations"},
		},
		Description: "列出指定时间以来有新消息的单聊和群聊",
		Interface: &contract.InterfaceSpec{
			Mode:         contract.InterfaceModeComposite,
			Availability: contract.InterfaceAvailable,
			Reason:       "Reviewed Chat Shortcut composite: the CLI owns time normalization, cursor exhaustion, conversation deduplication, latest-message aggregation, stable projection, and completeness metadata over chat/search_messages_by_time_range.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "列出指定时间以来有新消息的单聊和群聊",
			UseWhen:      []string{"当你只需要知道最近 24 小时或指定时间范围内哪些单聊或群聊出现了新消息,而不需要读取消息正文时使用;省略 --start 时从有效 --end 往前推 24 小时,省略 --end 时固定为当前时间取整秒;CLI 自动翻页、按 openConversationId 去重,并返回会话名称、类型和时间窗内最新消息时间。"},
			AvoidWhen:    []string{"需要读取消息正文或按发送者、关键词、@对象筛选时使用 +search-msg;需要不区分是否有新消息的全部会话时使用 +conversation-list;监听未来消息时使用 event +listen-im"},
			Examples: []string{
				"dws chat +recent-conversations",
				"dws chat +recent-conversations --start \"2026-09-07T00:00:00+08:00\" --end \"2026-09-08T00:00:00+08:00\"",
			},
		},
		Parameters: []contract.ParamDecl{
			{Name: "start", Property: "startTime"},
			{Name: "end", Property: "endTime"},
			{Name: "limit", Property: "limit"},
			{Name: "cursor", Property: "cursor"},
		},
		Result: &contract.ResultSpec{
			Outcomes:   []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomePartialFailure, contract.ResultOutcomeFailure},
			DataSchema: activeConversationsResultSchema(),
			SensitivePaths: []string{
				"conversations.conversationId", "conversations.name",
				"succeeded.conversations.conversationId", "succeeded.conversations.name",
			},
		},
		Pagination: &contract.PaginationSpec{
			Kind:                  contract.PaginationKindCursor,
			CursorParameter:       "cursor",
			MetaPath:              contract.PaginationMetaPath,
			EndpointExhaustedPath: contract.PaginationExhaustedPath,
			NextTokenPath:         contract.PaginationNextTokenPath,
		},
	},
	Flags: []shortcut.Flag{
		{Name: "start", Type: shortcut.FlagString, Desc: "开始时间(包含),选填;省略时从有效 --end 往前推 24 小时,两个时间参数都省略时查询最近 24 小时;支持 RFC3339、YYYY-MM-DD HH:mm:ss 或 YYYY-MM-DD;查询时间边界仅支持整秒,拒绝非零小数秒;显式传入 --start 时不能为空白;--end 必须晚于 --start;使用非首页 --cursor 时必须显式复用原查询的 --start 和 --end"},
		{Name: "end", Type: shortcut.FlagString, Desc: "结束时间(不包含),格式同 --start;查询时间边界仅支持整秒,拒绝非零小数秒;不传时固定为本次查询当前时间向下取整秒,不包含当前未结束秒;有效整秒区间的 --end 必须晚于 --start;使用非首页 --cursor 时必须显式复用原查询的 --start 和 --end"},
		{Name: "limit", Type: shortcut.FlagInt, Default: strconv.Itoa(activeConversationsDefaultLimit), Desc: "底层每页消息数量;--limit 必须在 1-100 之间"},
		{Name: "cursor", Type: shortcut.FlagString, Default: "0", Desc: "续页游标;使用非首页 --cursor 时必须显式复用原查询的 --start 和 --end,不重新计算默认窗口;续页应保持同一 profile、--start、--end 和 --limit(结果 pageSize);续页批次不包含此前结果,complete 始终为 false"},
		{Name: "page-limit", Type: shortcut.FlagInt, Default: strconv.Itoa(activeConversationsDefaultPages), Desc: "自动分页安全上限;--page-limit 必须在 1-500 之间;达到上限仍有下一页时返回 complete=false 和 next_token"},
		{Name: "page-delay", Type: shortcut.FlagInt, Default: strconv.Itoa(activeConversationsDefaultDelay), Desc: "自动分页间隔毫秒数;--page-delay 必须在 0-60000 之间;默认 200,0 表示不等待;等待可取消"},
		{Name: "total-timeout", Type: shortcut.FlagInt, Default: strconv.Itoa(activeConversationsTotalTimeout), Desc: "本次执行的总查询预算(秒,含所有页、重试和分页等待);--total-timeout 必须在 1-3600 之间;默认 300;不同于全局 --timeout 的单请求上限;超时保留已验证页面"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintCustom, Flags: []string{"start"}, Description: "显式传入 --start 时不能为空白"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"start", "end"}, Description: "--end 必须晚于 --start"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"start", "end"}, Description: "查询时间边界仅支持整秒,拒绝非零小数秒"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"limit"}, Description: "--limit 必须在 1-100 之间"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"page-limit"}, Description: "--page-limit 必须在 1-500 之间"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"page-delay"}, Description: "--page-delay 必须在 0-60000 之间"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"cursor", "start", "end"}, Description: "使用非首页 --cursor 时必须显式复用原查询的 --start 和 --end"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"total-timeout"}, Description: "--total-timeout 必须在 1-3600 之间"},
	},
	Tips: []string{
		`dws chat +recent-conversations`,
		`dws chat +recent-conversations --start "2026-09-07 00:00:00" --end "2026-09-08 00:00:00"`,
		"分页中途失败返回 partial_failure(退出码 7);已验证页面聚合保存在 data.succeeded[0],失败页和原始诊断在 data.failed[0].error;先处理错误,再用 meta.pagination.next_token 和相同 profile、start/end/limit 手工续查并合并。",
		"会话 name 始终返回;下层未提供名称时 name 为空字符串、nameKnown=false。",
		"--cursor 续查不自动合并此前结果,也不能跨次校验查询绑定或检测游标循环;本命令不保存磁盘进度,进程强制终止后需重新查询。",
	},
	Validate: validateActiveConversations,
	Execute:  executeActiveConversations,
}

ActiveConversations returns the distinct direct and group conversations that contain at least one message in a requested time range. The lower API returns grouped message details, so this Shortcut owns cursor exhaustion, conversation deduplication, latest-time calculation, and completeness.

View Source
var BotFind = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+bot-find",
	Product:     "bot",
	Description: "搜索全部可用机器人(含他人/官方,返回 openDingTalkId 可发单聊)",
	Intent:      "当你想找到平台上任意可用机器人(含他人创建或官方助手,例如某个日报/审批机器人)以便与其发起单聊时使用;输入关键词,返回含 openDingTalkId 的机器人列表。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_bot_find",
			CanonicalPath:  "chat.shortcut_bot_find",
			CLIPath:        "chat +bot-find",
			PrimaryCLIPath: "chat +bot-find",
		},
		Description: "搜索全部可用机器人(含他人/官方,返回 openDingTalkId 可发单聊)",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "搜索全部可用机器人(含他人/官方,返回 openDingTalkId 可发单聊)",
			UseWhen:      []string{"当你想找到平台上任意可用机器人(含他人创建或官方助手,例如某个日报/审批机器人)以便与其发起单聊时使用;输入关键词,返回含 openDingTalkId 的机器人列表。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +bot-find --query \"日报\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词"},
		{Name: "keyword", Type: shortcut.FlagString, Desc: "--query 的别名", Hidden: true},
		{Name: "limit", Type: shortcut.FlagInt, Default: "20", Desc: "每页返回数量"},
		{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传 nextCursor"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"query", "keyword"}},
	},
	Tips: []string{`dws chat +bot-find --query "日报"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{"keyword": rt.StrFirst("query", "keyword")}
		if rt.Int("limit") > 0 {
			params["limit"] = rt.Int("limit")
		}
		if rt.Str("cursor") != "" {
			params["cursor"] = rt.Str("cursor")
		}
		data, err := rt.CallMCPData("bot", "search_bots", params)
		if err != nil {
			return err
		}
		bots := botFindProject(data)
		return rt.Output(map[string]any{"count": len(bots), "bots": bots})
	},
}

BotFind searches all available robots (search_bots, bot).

View Source
var BotSearch = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+bot-search",
	Product:     "bot",
	Description: "搜索当前用户自己创建的机器人",
	Intent:      "当你要管理或复用自己创建的机器人(比如查到其 robotCode 以便让它进群或发消息)时使用;按机器人名称模糊搜索,只返回当前用户名下创建的机器人列表。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_bot_search",
			CanonicalPath:  "chat.shortcut_bot_search",
			CLIPath:        "chat +bot-search",
			PrimaryCLIPath: "chat +bot-search",
		},
		Description: "搜索当前用户自己创建的机器人",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "搜索当前用户自己创建的机器人",
			UseWhen:      []string{"当你要管理或复用自己创建的机器人(比如查到其 robotCode 以便让它进群或发消息)时使用;按机器人名称模糊搜索,只返回当前用户名下创建的机器人列表。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +bot-search --page 1 --name \"日报\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "page", Type: shortcut.FlagInt, Default: "1", Desc: "页码"},
		{Name: "size", Type: shortcut.FlagInt, Desc: "每页数量"},
		{Name: "name", Type: shortcut.FlagString, Desc: "robotName 模糊匹配"},
	},
	Tips: []string{`dws chat +bot-search --page 1 --name "日报"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{"currentPage": rt.Int("page")}
		if rt.Int("size") > 0 {
			params["pageSize"] = rt.Int("size")
		}
		if rt.Str("name") != "" {
			params["robotName"] = rt.Str("name")
		}
		data, err := rt.CallMCPData("bot", "search_my_robots", params)
		if err != nil {
			return err
		}
		robots := botSearchProject(data)
		return rt.Output(map[string]any{"count": len(robots), "robots": robots})
	},
}

BotSearch searches robots created by the current user (search_my_robots, bot).

View Source
var CategoryAddConversation = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+category-add-conversation",
	Product:     "im",
	Description: "将会话移动到指定的自定义分组中",
	Intent:      "当你想把某个会话归入一个或多个自定义分组时使用;会实际把会话加入指定分组,需传会话 openConversationId 和目标分组 ID 列表。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "category-ids", Type: shortcut.FlagStringSlice, Desc: "目标分组 ID 列表", Required: true},
	},
	Tips: []string{`dws chat +category-add-conversation --group <openConversationId> --category-ids 123,456`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		ids, err := toInt64Slice(rt.StrSlice("category-ids"))
		if err != nil {
			return fmt.Errorf("--category-ids: %w", err)
		}
		return rt.CallMCP("add_conv_to_categories", map[string]any{
			"openConversationId": rt.Str("group"),
			"categoryIds":        ids,
		})
	},
}

CategoryAddConversation adds a conversation to categories (add_conv_to_categories, im).

View Source
var CategoryCreate = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+category-create",
	Product:     "im",
	Description: "创建用户自定义会话分组",
	Intent:      "当你想新建会话分组/分类容器来归类已有会话时使用;不是创建群聊/聊天群。会实际创建分类并返回 ID,需传最多 15 个字符的名称 --title。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_category_create",
			CanonicalPath:  "chat.shortcut_category_create",
			CLIPath:        "chat +category-create",
			PrimaryCLIPath: "chat +category-create",
		},
		Description: "创建用户自定义会话分组",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "创建用户自定义会话分组",
			UseWhen:      []string{"当你想新建会话分组/分类容器来归类已有会话时使用;不是创建群聊/聊天群。会实际创建分类并返回 ID,需传最多 15 个字符的名称 --title。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +category-create --title \"工作群\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "title", Type: shortcut.FlagString, Desc: "分组名称;去除首尾空白后必须非空,且最多 15 个字符", Required: true},
	},
	Constraints: []shortcut.Constraint{
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"title"},
			Description: "--title 去除首尾空白后必须非空,且最多 15 个字符",
		},
	},
	Tips:     []string{`dws chat +category-create --title "工作群"`},
	Validate: validateConversationCategoryTitle,
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("create_conv_category", map[string]any{
			"title": strings.TrimSpace(rt.Str("title")),
		})
	},
}

CategoryCreate creates a conversation category (create_conv_category, im).

View Source
var CategoryDelete = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+category-delete",
	Product:     "im",
	Description: "删除用户自定义会话分组",
	Intent:      "当你想删除某个自定义会话分组时使用;会实际删除分组(不影响其中会话本身),不可逆,需传 categoryId。",
	Risk:        shortcut.RiskHighWrite,
	Safety: contract.SafetySpec{
		Effect: "destructive", Risk: "high",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_category_delete",
			CanonicalPath:  "chat.shortcut_category_delete",
			CLIPath:        "chat +category-delete",
			PrimaryCLIPath: "chat +category-delete",
		},
		Description: "删除用户自定义会话分组",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "删除用户自定义会话分组",
			UseWhen:      []string{"当你想删除某个自定义会话分组时使用;会实际删除分组(不影响其中会话本身),不可逆,需传 categoryId。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +category-delete --category-id <分组ID>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "category-id", Type: shortcut.FlagInt, Desc: "会话分组 ID", Required: true},
	},
	Tips: []string{`dws chat +category-delete --category-id <分组ID>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("delete_conv_category", map[string]any{"categoryId": rt.Int("category-id")})
	},
}

CategoryDelete deletes a conversation category (delete_conv_category, im).

View Source
var CategoryList = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+category-list",
	Aliases:     []string{"+feed-group-list"},
	Product:     "im",
	Description: "获取用户自定义会话分组",
	Intent:      "当你想查看当前用户的会话分组/分类容器时使用;不是查看群聊/聊天群列表。只读返回分组及 categoryId,供后续按分类拉取或增删会话。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_category_list",
			CanonicalPath:  "chat.shortcut_category_list",
			CLIPath:        "chat +category-list",
			PrimaryCLIPath: "chat +category-list",
			Aliases:        []string{"chat +feed-group-list"},
		},
		Description: "获取用户自定义会话分组",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "获取用户自定义会话分组",
			UseWhen:      []string{"当你想查看当前用户的会话分组/分类容器时使用;不是查看群聊/聊天群列表。只读返回分组及 categoryId,供后续按分类拉取或增删会话。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +category-list"},
		},
	},
	Tips: []string{`dws chat +category-list`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		data, err := rt.CallMCPData("im", "list_user_define_conv_categories", map[string]any{})
		if err != nil {
			return err
		}
		categories, err := categoryListProject(data)
		if err != nil {
			return err
		}
		return rt.Output(map[string]any{"count": len(categories), "categories": categories})
	},
}

CategoryList lists user-defined conversation categories (list_user_define_conv_categories, im).

View Source
var CategoryListConversations = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+category-list-conversations",
	Aliases:     []string{"+feed-group-list-item"},
	Product:     "im",
	Description: "按稳定 categoryId 列出会话分组中的会话",
	Intent:      categoryListConversationsIntent,
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_category_list_conversations",
			CanonicalPath:  "chat.shortcut_category_list_conversations",
			CLIPath:        "chat +category-list-conversations",
			PrimaryCLIPath: "chat +category-list-conversations",
		},
		Description: "按稳定 categoryId 列出会话分组中的会话",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed category reader: it preserves the published stable-ID input while validating the collection shape and stable conversation identity. The bound lower interface has no continuation input, so an explicit collection without pagination signals is one complete response; any reported pagination signal is validated strictly.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "按稳定 categoryId 列出会话分组中的会话",
			UseWhen:      []string{categoryListConversationsIntent},
			AvoidWhen: []string{
				"只需列出分组本身时使用 chat +category-list;需要群成员或聊天消息时分别使用 chat +chat-members-list 或 chat +chat-messages",
			},
			Examples: []string{
				"dws chat +category-list-conversations --category-id <categoryId>",
			},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "category-id", Type: shortcut.FlagInt, Aliases: []string{"feed-group-id"}, Desc: "稳定会话分组 ID", Required: true},
		{Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "排除已免打扰会话"},
	},
	Tips: []string{
		`dws chat +category-list-conversations --category-id <分组ID>`,
	},
	Execute: executeCategoryListConversations,
}

CategoryListConversations lists conversations in a category (list_conversations_by_category, im).

View Source
var CategoryRemoveConversation = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+category-remove-conversation",
	Product:     "im",
	Description: "将会话从指定的自定义分组中移出",
	Intent:      "当你想把某个会话从指定自定义分组中移出时使用;会实际从分组移除该会话(不删除会话本身),需传会话 openConversationId 和分组 ID 列表。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "category-ids", Type: shortcut.FlagStringSlice, Desc: "目标分组 ID 列表", Required: true},
	},
	Tips: []string{`dws chat +category-remove-conversation --group <openConversationId> --category-ids 123,456`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		ids, err := toInt64Slice(rt.StrSlice("category-ids"))
		if err != nil {
			return fmt.Errorf("--category-ids: %w", err)
		}
		return rt.CallMCP("remove_conv_from_categories", map[string]any{
			"openConversationId": rt.Str("group"),
			"categoryIds":        ids,
		})
	},
}

CategoryRemoveConversation removes a conversation from categories (remove_conv_from_categories, im).

View Source
var CategoryRename = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+category-rename",
	Product:     "im",
	Description: "更新用户自定义会话分组的名称",
	Intent:      "当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和最多 15 个字符的新名称 --title。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_category_rename",
			CanonicalPath:  "chat.shortcut_category_rename",
			CLIPath:        "chat +category-rename",
			PrimaryCLIPath: "chat +category-rename",
		},
		Description: "更新用户自定义会话分组的名称",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "更新用户自定义会话分组的名称",
			UseWhen:      []string{"当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和最多 15 个字符的新名称 --title。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +category-rename --category-id <分组ID> --title \"新名称\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "category-id", Type: shortcut.FlagInt, Desc: "会话分组 ID", Required: true},
		{Name: "title", Type: shortcut.FlagString, Desc: "新的分组名称;去除首尾空白后必须非空,且最多 15 个字符", Required: true},
	},
	Constraints: []shortcut.Constraint{
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"title"},
			Description: "--title 去除首尾空白后必须非空,且最多 15 个字符",
		},
	},
	Tips:     []string{`dws chat +category-rename --category-id <分组ID> --title "新名称"`},
	Validate: validateConversationCategoryTitle,
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("rename_conv_category", map[string]any{
			"categoryId": rt.Int("category-id"),
			"title":      strings.TrimSpace(rt.Str("title")),
		})
	},
}

CategoryRename renames a conversation category (rename_conv_category, im).

View Source
var ChatAddBot = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-add-bot",
	Product:     "bot",
	Description: "将机器人添加到群中",
	Intent:      "当你想把某个机器人添加进群(比如让日报机器人进群播报)时使用;会实际把机器人加入群聊,需传机器人 robotCode 和群 openConversationId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code", Required: true},
		{Name: "id", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
	},
	Tips: []string{`dws chat +chat-add-bot --robot-code <robotCode> --id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("add_robot_to_group", map[string]any{
			"robotCode":          rt.Str("robot-code"),
			"openConversationId": rt.Str("id"),
		})
	},
}

ChatAddBot adds a custom robot to a group (add_robot_to_group, bot).

View Source
var ChatAuditJoin = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-audit-join",
	Product:     "im",
	Description: "审批入群验证(通过/拒绝/删除/忽略/拉黑)",
	Intent:      "当你要处理某条入群申请时使用;会实际执行通过/拒绝/删除/忽略/拉黑动作,需传群 openConversationId、recordId、申请人与邀请人 userId 及 status。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "record-id", Type: shortcut.FlagInt, Desc: "申请记录 ID", Required: true},
		{Name: "applicant", Type: shortcut.FlagString, Desc: "申请人 userId", Required: true},
		{Name: "inviter", Type: shortcut.FlagString, Desc: "邀请人 userId", Required: true},
		{Name: "status", Type: shortcut.FlagString, Desc: "审批动作", Required: true, Enum: []string{"AuditApprove", "AuditDelete", "AuditIgnore", "AuditRefuse", "AuditBlock"}},
		{Name: "description", Type: shortcut.FlagString, Desc: "审批说明"},
	},
	Tips: []string{`dws chat +chat-audit-join --group <openConversationId> --record-id 123 --applicant <userId> --inviter <userId> --status AuditApprove`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{
			"openConversationId": rt.Str("group"),
			"applyRecordId":      rt.Int("record-id"),
			"applicantUid":       rt.Str("applicant"),
			"inviterUid":         rt.Str("inviter"),
			"status":             rt.Str("status"),
		}
		if rt.Changed("description") {
			params["auditDescription"] = rt.Str("description")
		}
		return rt.CallMCP("audit_join_group", params)
	},
}

ChatAuditJoin audits a join-validation record (audit_join_group, im).

View Source
var ChatBots = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-bots",
	Product:     "bot",
	Description: "查看群内所有机器人",
	Intent:      "当你想查看某个群里已添加了哪些机器人时使用;--group 可传群 openConversationId 或群名,多命中会安全停止。只读返回机器人列表(含 openBotId,供后续移除)。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_bots",
			CanonicalPath:  "chat.shortcut_chat_bots",
			CLIPath:        "chat +chat-bots",
			PrimaryCLIPath: "chat +chat-bots",
		},
		Description: "查看群内所有机器人",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "查看群内所有机器人",
			UseWhen:      []string{"当你想查看某个群里已添加了哪些机器人时使用;--group 可传群 openConversationId 或群名,多命中会安全停止。只读返回机器人列表(含 openBotId,供后续移除)。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-bots --group <openConversationId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId;兼容直接传群名并唯一解析"},
		{Name: "chat-query", Type: shortcut.FlagString, Desc: "--group 的旧版自然名称入口", Hidden: true},
		{Name: "group-query", Type: shortcut.FlagString, Desc: "--chat-query 的兼容别名", Hidden: true},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"group", "chat-query", "group-query"}},
	},
	Tips: []string{
		`dws chat +chat-bots --group <openConversationId>`,
		`dws chat +chat-bots --group "项目群"`,
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		groupID, err := resolveStableOrNamedChat(rt)
		if err != nil {
			return err
		}
		data, err := rt.CallMCPData("bot", "list_group_bots", map[string]any{"openConversationId": groupID})
		if err != nil {
			return err
		}
		bots := chatBotsProject(data)
		return rt.Output(map[string]any{"count": len(bots), "bots": bots})
	},
}

ChatBots lists robots in a group (list_group_bots, bot).

View Source
var ChatCreate = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-create",
	Product:     "im",
	Description: "按成员和可选群主全量预检后创建一个钉钉群聊",
	Intent:      "当你要创建真实钉钉群聊/聊天群时使用;不是创建会话分组/分类容器。成员可传稳定 ID 或 --member-query 姓名,成员省略时仅当前用户;名称省略由服务端生成默认群名。群主默认当前用户,也可用 --owner-open-dingtalk-id 或 --owner-query 明确指定。所有自然身份会在唯一解析并去重后才执行一次创建,任一零命中或多命中都会整体停止。--bots 可在建群后逐个添加机器人(最多10个 robotCode);权限由添加接口校验,非原子操作,失败保留已建群并报告恢复上下文。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "name", Type: shortcut.FlagString, Desc: "群名称;省略或空白时不传 groupName,由服务端生成默认群名"},
		{Name: "users", Type: shortcut.FlagStringSlice, Desc: "初始成员 userId 或 openDingTalkId 列表"},
		{Name: "member-query", Type: shortcut.FlagStringSlice, Desc: "按姓名/花名唯一解析的初始成员,可逗号分隔或重复传入"},
		{Name: "owner-open-dingtalk-id", Type: shortcut.FlagString, Desc: "明确指定群主 openDingTalkId(与 --owner-query 互斥;省略时群主为当前用户)"},
		{Name: "owner-query", Type: shortcut.FlagString, Desc: "按姓名唯一解析群主 openDingTalkId(与 --owner-open-dingtalk-id 互斥)"},
		{Name: "type", Type: shortcut.FlagString, Default: "INTERNAL", Desc: "群类型", Enum: []string{"INTERNAL", "EXTERNAL", "NORMAL"}},
		{Name: "bots", Type: shortcut.FlagStringSlice, Desc: "初始机器人 robotCode 去重后最多10个;创建后逐个添加,权限失败保留已建群"},
		{Name: "thread", Type: shortcut.FlagBool, Desc: "创建为话题群"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintCustom, Flags: []string{"bots"}, Description: "初始机器人 robotCode 去重后最多10个;创建后逐个添加,权限失败保留已建群"},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"owner-open-dingtalk-id", "owner-query"}},
	},
	Tips: []string{
		`dws chat +chat-create --name "项目冲刺群" --users userId1,userId2`,
		`dws chat +chat-create --name "合作群" --member-query "张三,李四" --type EXTERNAL`,
	},
	Validate: func(rt *shortcut.RuntimeContext) error {
		if len(uniqueShortcutStrings(rt.StrSlice("bots"))) > 10 {
			return apperrors.NewValidation("--bots 去重后最多10个 robotCode")
		}
		return nil
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		if ownerOpenID := rt.Str("owner-open-dingtalk-id"); ownerOpenID != "" {
			if err := targetresolver.ValidateExplicitOpenDingTalkID("--owner-open-dingtalk-id", ownerOpenID); err != nil {
				return err
			}
		}
		resolvedMembers, err := targetresolver.ResolveUsers(
			rt,
			rt.StrSlice("member-query"),
			targetresolver.IdentityAny,
		)
		if err != nil {
			return err
		}
		ownerOpenID := rt.Str("owner-open-dingtalk-id")
		if query := rt.Str("owner-query"); query != "" {
			resolvedOwner, resolveErr := targetresolver.ResolveUser(
				rt, query, targetresolver.IdentityOpenDingTalkID)
			if resolveErr != nil {
				return resolveErr
			}
			ownerOpenID = resolvedOwner.Selected.OpenDingTalkID
		}
		members := make([]string, 0, len(rt.StrSlice("users"))+len(resolvedMembers)+1)
		if ownerOpenID != "" {
			members = append(members, ownerOpenID)
		} else {
			profile, profileErr := rt.CallMCPData("contact", "get_current_user_profile", nil)
			if profileErr != nil {
				return fmt.Errorf("读取当前用户以设置群主失败: %w", profileErr)
			}
			currentUserID := currentProfileUserID(profile)
			if currentUserID == "" {
				return apperrors.NewValidation("当前用户资料缺少 userId,无法保证群主属于初始成员列表")
			}
			members = append(members, currentUserID)
		}
		for _, member := range rt.StrSlice("users") {
			member = strings.TrimSpace(member)
			if member != "" {
				members = appendUniqueShortcutString(members, member)
			}
		}
		for _, resolved := range resolvedMembers {
			member := resolved.Selected.UserID
			if member == "" {
				member = resolved.Selected.OpenDingTalkID
			}
			members = appendUniqueShortcutString(members, member)
		}
		params := map[string]any{
			"groupMembers": members,
			"groupType":    rt.Str("type"),
		}

		if name := rt.Str("name"); strings.TrimSpace(name) != "" {
			params["groupName"] = name
		}
		if rt.Bool("thread") {
			params["convThreadEnabled"] = true
		}
		if ownerOpenID != "" {
			params["ownerOpenDingTalkId"] = ownerOpenID
		}
		if rt.DryRun() {
			if bots := uniqueShortcutStrings(rt.StrSlice("bots")); len(bots) > 0 {
				return rt.Output(map[string]any{"dry_run": true, "executed": false, "createArguments": params, "bots": bots, "addBotTarget": "created openConversationId", "atomic": false})
			}
			return rt.CallMCP("create_group_conversation", params)
		}
		data, err := rt.CallMCPWriteData("im", "create_group_conversation", params)
		if err != nil {
			return err
		}
		normalizeCreatedConversation(data)
		if bots := uniqueShortcutStrings(rt.StrSlice("bots")); len(bots) > 0 {
			return addCreatedGroupBots(rt, data, bots)
		}
		return rt.Output(data)
	},
}

ChatCreate creates a DingTalk group after resolving every natural member and the optional owner to stable DingTalk identities. Initial bots are added after creation; description, idempotency and visibility remain separate contracts.

View Source
var ChatDismiss = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-dismiss",
	Product:     "im",
	Description: "解散群聊(不可逆,需群主权限)",
	Intent:      "当你要彻底解散一个群时使用;会实际销毁群聊,不可逆且需群主权限,仅需传群 openConversationId,操作前务必确认。",
	Risk:        shortcut.RiskHighWrite,
	Safety: contract.SafetySpec{
		Effect: "destructive", Risk: "high",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_dismiss",
			CanonicalPath:  "chat.shortcut_chat_dismiss",
			CLIPath:        "chat +chat-dismiss",
			PrimaryCLIPath: "chat +chat-dismiss",
		},
		Description: "解散群聊(不可逆,需群主权限)",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "解散群聊(不可逆,需群主权限)",
			UseWhen:      []string{"当你要彻底解散一个群时使用;会实际销毁群聊,不可逆且需群主权限,仅需传群 openConversationId,操作前务必确认。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-dismiss --group <openConversationId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
	},
	Tips: []string{`dws chat +chat-dismiss --group <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("dismiss_group", map[string]any{"openConversationId": rt.Str("group")})
	},
}

ChatDismiss dismisses (destroys) a group (dismiss_group, im).

View Source
var ChatGetByID = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-get-by-id",
	Product:     "im",
	Description: "根据群号获取群聊信息",
	Intent:      "当你只知道群号(数字)、需要换取群 openConversationId 及群信息时使用;只读,需传 --group-id。",
	Risk:        shortcut.RiskRead,
	Flags: []shortcut.Flag{
		{Name: "group-id", Type: shortcut.FlagInt, Desc: "群号(数字类型)", Required: true},
	},
	Tips: []string{`dws chat +chat-get-by-id --group-id 12345678`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("get_conv_info_by_group_id", map[string]any{"groupId": rt.Int("group-id")})
	},
}

ChatGetByID looks up a group by numeric group id (get_conv_info_by_group_id, im).

View Source
var ChatInviteURL = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-invite-url",
	Product:     "im",
	Description: "获取群邀请链接",
	Intent:      "当你想拿到一条群邀请链接分享给别人加群时使用;--group 可传群 openConversationId 或群名,多命中会安全停止。可用 --expires-seconds 设置有效期(0 表示永久)。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_invite_url",
			CanonicalPath:  "chat.shortcut_chat_invite_url",
			CLIPath:        "chat +chat-invite-url",
			PrimaryCLIPath: "chat +chat-invite-url",
		},
		Description: "获取群邀请链接",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "获取群邀请链接",
			UseWhen:      []string{"当你想拿到一条群邀请链接分享给别人加群时使用;--group 可传群 openConversationId 或群名,多命中会安全停止。可用 --expires-seconds 设置有效期(0 表示永久)。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-invite-url --group <openConversationId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId;兼容直接传群名并唯一解析"},
		{Name: "chat-query", Type: shortcut.FlagString, Desc: "--group 的旧版自然名称入口", Hidden: true},
		{Name: "group-query", Type: shortcut.FlagString, Desc: "--chat-query 的兼容别名", Hidden: true},
		{Name: "expires-seconds", Type: shortcut.FlagInt, Desc: "链接有效期(秒),0 表示永久"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"group", "chat-query", "group-query"}},
	},
	Tips: []string{
		`dws chat +chat-invite-url --group <openConversationId>`,
		`dws chat +chat-invite-url --group "项目群"`,
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		groupID, err := resolveStableOrNamedChat(rt)
		if err != nil {
			return err
		}
		params := map[string]any{
			"openConversationId": groupID,
			"cid":                groupID,
		}
		if rt.Changed("expires-seconds") {
			params["expiresSeconds"] = rt.Int("expires-seconds")
		}
		return rt.CallMCP("get_group_invite_url", params)
	},
}

ChatInviteURL gets the group invite url (get_group_invite_url, im).

View Source
var ChatList = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-list",
	Product:     "im",
	Description: "分页列出当前用户加入的会话(默认群聊;可选包含单聊)",
	Intent: "当你要像 lark-cli +chat-list 一样列出当前用户加入的会话时使用;默认只返回群聊," +
		"--types 可追加 p2p(钉钉投影为 conversationType=direct)。" +
		"--exclude-muted 交给下层 excludeMuted;默认读取一页,明确要求全部时加 --page-all,合并并去重所有已读取页面后再执行 --types 过滤,结果公开完整性 ledger。" +
		"不支持 lark 的 sort/sort-type,也不模拟 bot 身份剥离 p2p。",
	Risk: shortcut.RiskRead,
	Flags: []shortcut.Flag{
		{Name: "sort", Type: shortcut.FlagString, Enum: []string{"create_time", "active_time"}, Desc: "对已取集合排序:创建升序或最近消息降序;完整性依赖原目录"},
		{Name: "types", Type: shortcut.FlagStringSlice, Desc: "会话类型只能包含 group 和/或 p2p;省略时默认只返回群聊"},
		{Name: "page-size", Type: shortcut.FlagInt, Default: "20", Desc: "每页数量,必须在 1-100 之间"},
		{Name: "limit", Type: shortcut.FlagInt, Desc: "--page-size 的别名,必须在 1-100 之间"},
		{Name: "page-token", Type: shortcut.FlagString, Desc: "分页游标;若提供则必须是非负整数"},
		{Name: "cursor", Type: shortcut.FlagInt, Desc: "--page-token 的整数别名"},
		{Name: "page-delay", Type: shortcut.FlagInt, Default: "0", Desc: "后续页间隔毫秒,0–60000"},
		{Name: "page-all", Type: shortcut.FlagBool, Desc: "自动读取全部会话分页;--page-limit 仅与 --page-all 一起使用且范围 1-500"},
		{Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
		{Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "排除已免打扰会话"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"page-size", "limit"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"page-token", "cursor"}},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"types"},
			Description: "只能包含 group 和/或 p2p",
		},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"page-size", "limit"},
			Description: "必须在 1-100 之间",
		},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"page-token"},
			Description: "若提供则必须是非负整数",
		},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
	},
	Tips: []string{
		`dws chat +chat-list`,
		`dws chat +chat-list --types group,p2p --exclude-muted --page-size 50`,
		`dws chat +chat-list --types group --page-size 100 --page-all --page-limit 20`,
	},
	Validate: validateChatList,
	Execute:  executeChatList,
}

ChatList is the lark-cli +chat-list alignment for DingTalk. It lists the current user's conversations through list_all_conversations, defaults to groups only (lark omit-types behavior), and applies --types after all fetched pages have been merged. Sort order and bot identity p2p stripping are intentionally omitted: DingTalk has no matching sort_type parameter here, and DWS shortcuts run as the signed-in user.

View Source
var ChatListAll = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-list-all",
	Product:     "im",
	Description: "分页拉取我加入的所有群列表",
	Intent:      "当你想遍历当前用户加入的所有群做统计或批量操作时使用;只读分页返回全部已加入的群列表。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_list_all",
			CanonicalPath:  "chat.shortcut_chat_list_all",
			CLIPath:        "chat +chat-list-all",
			PrimaryCLIPath: "chat +chat-list-all",
		},
		Description: "分页拉取我加入的所有群列表",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "分页拉取我加入的所有群列表",
			UseWhen:      []string{"当你想遍历当前用户加入的所有群做统计或批量操作时使用;只读分页返回全部已加入的群列表。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-list-all --limit 50"},
		},
	},
	Flags: append([]shortcut.Flag{
		{Name: "limit", Type: shortcut.FlagInt, Default: "100", Desc: "每页返回数量;--limit 必须在 1-200 之间"},
		{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传 nextCursor"},
		{Name: "page-all", Type: shortcut.FlagBool, Desc: "沿 nextCursor 自动读取全部已加入群;--page-limit 仅与 --page-all 一起使用且范围 1-500;--max-items/--page-delay 仅与 --page-all 一起使用;值必须大于等于 0"},
		{Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
	}, shortcut.AutoPageControlFlags()...),
	Constraints: append([]shortcut.Constraint{
		{Kind: shortcut.ConstraintCustom, Flags: []string{"limit"}, Description: "--limit 必须在 1-200 之间"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
	}, shortcut.AutoPageControlConstraints()...),
	Tips: []string{
		`dws chat +chat-list-all --limit 50`,
		`dws chat +chat-list-all --limit 200 --page-all --page-limit 50`,
	},
	Validate: validateChatListAll,
	Execute:  executeChatListAll,
}

ChatListAll paginates all groups the caller joined (list_my_groups_pagination, im).

View Source
var ChatListJoinRequests = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-list-join-requests",
	Product:     "im",
	Description: "分页拉取入群验证记录",
	Intent:      "当你作为群主/管理员想查看待处理的入群申请时使用;只读分页返回入群验证记录(含 recordId、申请人与邀请人 ID),供后续用 chat-audit-join 审批。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_list_join_requests",
			CanonicalPath:  "chat.shortcut_chat_list_join_requests",
			CLIPath:        "chat +chat-list-join-requests",
			PrimaryCLIPath: "chat +chat-list-join-requests",
		},
		Description: "分页拉取入群验证记录",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "分页拉取入群验证记录",
			UseWhen:      []string{"当你作为群主/管理员想查看待处理的入群申请时使用;只读分页返回入群验证记录(含 recordId、申请人与邀请人 ID),供后续用 chat-audit-join 审批。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-list-join-requests --limit 30"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "limit", Type: shortcut.FlagInt, Default: "20", Desc: "单页数量(最大 50)"},
		{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传 nextCursor"},
	},
	Tips: []string{`dws chat +chat-list-join-requests --limit 30`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{}
		if rt.Int("limit") > 0 {
			params["limit"] = rt.Int("limit")
		}
		if rt.Changed("cursor") {
			params["cursor"] = rt.Str("cursor")
		}
		return rt.CallMCP("list_apply_join_group_records", params)
	},
}

ChatListJoinRequests paginates join-validation records (list_apply_join_group_records, im).

View Source
var ChatListMine = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-list-mine",
	Product:     "im",
	Description: "拉取我创建/管理的群",
	Intent:      "当你想查看自己作为群主或管理员在管理哪些群时使用;只读分页返回,可用 --role OWNER/ADMIN 按角色过滤。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_list_mine",
			CanonicalPath:  "chat.shortcut_chat_list_mine",
			CLIPath:        "chat +chat-list-mine",
			PrimaryCLIPath: "chat +chat-list-mine",
		},
		Description: "拉取我创建/管理的群",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "拉取我创建/管理的群",
			UseWhen:      []string{"当你想查看自己作为群主或管理员在管理哪些群时使用;只读分页返回,可用 --role OWNER/ADMIN 按角色过滤。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-list-mine --role OWNER"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "role", Type: shortcut.FlagString, Desc: "角色过滤", Enum: []string{"OWNER", "ADMIN"}},
		{Name: "limit", Type: shortcut.FlagInt, Desc: "最多返回群数量,不传返回全部"},
		{Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "排除已设置免打扰的群聊"},
	},
	Tips: []string{`dws chat +chat-list-mine --role OWNER`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{}
		if rt.Changed("role") {
			params["roleFilter"] = rt.Str("role")
		}
		if rt.Int("limit") > 0 {
			params["limit"] = rt.Int("limit")
		}
		if rt.Bool("exclude-muted") {
			params["excludeMuted"] = true
		}
		data, err := rt.CallMCPData("im", "list_owned_or_admin_groups", params)
		if err != nil {
			return err
		}
		groups := chatListMineProject(data)
		payload := map[string]any{"count": len(groups), "groups": groups}
		chatmsg.ApplyPagination(payload, data)
		return rt.Output(payload)
	},
}

ChatListMine lists groups the caller owns/administers (list_owned_or_admin_groups, im).

View Source
var ChatMembersGet = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-members-get",
	Product:     "im",
	Description: "根据成员 openDingTalkId 批量查询群成员详情",
	Intent:      "当你已有若干成员的 openDingTalkId、需要批量获取他们在该群内的详情(群昵称、角色等)时使用;只读,需传群 openConversationId 和成员 openDingTalkId 列表。",
	Risk:        shortcut.RiskRead,
	Flags: []shortcut.Flag{
		{Name: "id", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "users", Type: shortcut.FlagStringSlice, Desc: "成员 openDingTalkId 列表", Required: true},
	},
	Tips: []string{`dws chat +chat-members-get --id <openConversationId> --users odid1,odid2`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		if err := validateExplicitOpenIDs("--users", rt.StrSlice("users")); err != nil {
			return err
		}
		return rt.CallMCP("list_group_member_by_ids", map[string]any{
			"openConversationId":    rt.Str("id"),
			"cid":                   rt.Str("id"),
			"memberOpenDingTalkIds": rt.StrSlice("users"),
		})
	},
}

ChatMembersList lists members of a group (get_group_members, chat server). ChatMembersGet batch-queries member detail by ids (list_group_member_by_ids, im).

View Source
var ChatMute = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-mute",
	Product:     "im",
	Description: "全员禁言 / 取消全员禁言",
	Intent:      "当你想对整个群开启或取消全员禁言时使用;会实际切换群的全员禁言状态,需传群 openConversationId,加 --off 取消禁言。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_mute",
			CanonicalPath:  "chat.shortcut_chat_mute",
			CLIPath:        "chat +chat-mute",
			PrimaryCLIPath: "chat +chat-mute",
		},
		Description: "全员禁言 / 取消全员禁言",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "全员禁言 / 取消全员禁言",
			UseWhen:      []string{"当你想对整个群开启或取消全员禁言时使用;会实际切换群的全员禁言状态,需传群 openConversationId,加 --off 取消禁言。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-mute --group <openConversationId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "off", Type: shortcut.FlagBool, Desc: "取消全员禁言(不传则开启禁言)"},
	},
	Tips: []string{`dws chat +chat-mute --group <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("set_group_mute", map[string]any{
			"openConversationId": rt.Str("group"),
			"mute":               !rt.Bool("off"),
		})
	},
}

ChatMute mutes/unmutes the whole group (set_group_mute, im).

View Source
var ChatMuteMember = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-mute-member",
	Product:     "im",
	Description: "指定群成员禁言 / 取消禁言",
	Intent:      "当你想只禁言或解禁群里的指定成员时使用;会实际把成员加入或移出禁言名单,需传群 openConversationId 和成员列表,禁言时还需 --mute-time(毫秒)。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "users", Type: shortcut.FlagStringSlice, Desc: "成员 userId 或 openDingTalkId 列表", Required: true},
		{Name: "mute-time", Type: shortcut.FlagInt, Desc: "禁言时长(毫秒),如 300000/3600000/86400000/604800000/2592000000"},
		{Name: "off", Type: shortcut.FlagBool, Desc: "移出禁言名单(不传则加入禁言名单)"},
	},
	Tips: []string{`dws chat +chat-mute-member --group <openConversationId> --users userId1 --mute-time 3600000`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		userIDs, openIDs := splitIDs(rt.StrSlice("users"))
		off := rt.Bool("off")
		if len(userIDs) > 0 {
			resolved, err := resolveMuteMemberOpenIDs(rt, rt.Str("group"), userIDs)
			if err != nil {
				return err
			}
			openIDs = append(openIDs, resolved...)
			userIDs = nil
		}
		deduplicatedOpenIDs := make([]string, 0, len(openIDs))
		for _, openID := range openIDs {
			deduplicatedOpenIDs = appendUniqueShortcutString(deduplicatedOpenIDs, openID)
		}
		openIDs = deduplicatedOpenIDs
		params := map[string]any{
			"openConversationId": rt.Str("group"),
			"cid":                rt.Str("group"),
			"mute":               !off,
		}
		if len(userIDs) > 0 {
			params["uids"] = userIDs
		}
		if len(openIDs) > 0 {
			params["openDingTalkIds"] = openIDs
		}
		if !off {
			if rt.Int("mute-time") <= 0 {
				return fmt.Errorf("--mute-time 为禁言时必填(毫秒)")
			}
			params["muteTime"] = rt.Int("mute-time")
		}
		return rt.CallMCP("set_group_member_mute_list", params)
	},
}

ChatMuteMember mutes/unmutes specific members (set_group_member_mute_list, im).

View Source
var ChatQuit = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-quit",
	Product:     "im",
	Description: "退出群聊",
	Intent:      "当你想让当前用户主动退出某个群时使用;会实际退群,需传群 openConversationId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
	},
	Tips: []string{`dws chat +chat-quit --group <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("quit_group", map[string]any{"openConversationId": rt.Str("group")})
	},
}

ChatQuit quits a group (quit_group, im).

View Source
var ChatRemoveBot = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-remove-bot",
	Product:     "bot",
	Description: "从群内移除机器人",
	Intent:      "当你想把某个机器人从群里移除时使用;会实际移除机器人,不可逆,需传群 openConversationId 和机器人 openBotId。",
	Risk:        shortcut.RiskHighWrite,
	Flags: []shortcut.Flag{
		{Name: "id", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "bot-id", Type: shortcut.FlagString, Desc: "机器人 openBotId", Required: true},
	},
	Tips: []string{`dws chat +chat-remove-bot --id <openConversationId> --bot-id <openBotId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("remove_robot_in_group", map[string]any{
			"openConversationId": rt.Str("id"),
			"openBotId":          rt.Str("bot-id"),
		})
	},
}

ChatRemoveBot removes a robot from a group (remove_robot_in_group, bot).

View Source
var ChatRoleAdd = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-role-add",
	Product:     "im",
	Description: "添加群身份",
	Intent:      "当你想在群里新增一个自定义群身份/头衔时使用;会实际创建群身份,--group 可传群名或 openConversationId,多命中时在写入前停止。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_role_add",
			CanonicalPath:  "chat.shortcut_chat_role_add",
			CLIPath:        "chat +chat-role-add",
			PrimaryCLIPath: "chat +chat-role-add",
		},
		Description: "添加群身份",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "添加群身份",
			UseWhen:      []string{"当你想在群里新增一个自定义群身份/头衔时使用;会实际创建群身份,--group 可传群名或 openConversationId,多命中时在写入前停止。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-role-add --group <openConversationId> --name \"管理员\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群名或 openConversationId;群名必须唯一匹配", Required: true},
		{Name: "name", Type: shortcut.FlagString, Desc: "群身份名称", Required: true},
	},
	Tips: []string{`dws chat +chat-role-add --group <openConversationId> --name "管理员"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		groupID, err := resolveStableOrNamedChat(rt)
		if err != nil {
			return err
		}
		err = rt.CallMCP("add_custom_group_role", map[string]any{
			"openConversationId": groupID,
			"name":               rt.Str("name"),
		})
		return convergeChatRoleError("add_custom_group_role", err)
	},
}

ChatRoleAdd adds a custom group role (add_custom_group_role, im).

View Source
var ChatRoleList = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-role-list",
	Product:     "im",
	Description: "拉取会话的群身份列表",
	Intent:      "当你想查看某群自定义的群身份(如'班长''值日')都有哪些时使用;--group 可传群名或 openConversationId,多命中会安全停止,只读返回群身份列表及 openRoleId。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_role_list",
			CanonicalPath:  "chat.shortcut_chat_role_list",
			CLIPath:        "chat +chat-role-list",
			PrimaryCLIPath: "chat +chat-role-list",
		},
		Description: "拉取会话的群身份列表",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "拉取会话的群身份列表",
			UseWhen:      []string{"当你想查看某群自定义的群身份(如'班长''值日')都有哪些时使用;--group 可传群名或 openConversationId,多命中会安全停止,只读返回群身份列表及 openRoleId。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-role-list --group <openConversationId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群名或 openConversationId;群名必须唯一匹配", Required: true},
	},
	Tips: []string{`dws chat +chat-role-list --group <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		groupID, err := resolveStableOrNamedChat(rt)
		if err != nil {
			return err
		}
		data, err := rt.CallMCPData("im", "list_custom_group_roles", map[string]any{"openConversationId": groupID})
		if err != nil {
			return convergeChatRoleError("list_custom_group_roles", err)
		}
		roles := chatRoleListProject(data)
		return rt.Output(map[string]any{"count": len(roles), "roles": roles})
	},
}

ChatRoleList lists custom group roles (list_custom_group_roles, im).

View Source
var ChatRoleQueryUser = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-role-query-user",
	Product:     "im",
	Description: "查询群成员的群身份",
	Intent:      "当你想查看某个群成员当前拥有哪些群身份时使用;只读,--group 可传群名或 openConversationId,用户可传 userId 或 openDingTalkId。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_role_query_user",
			CanonicalPath:  "chat.shortcut_chat_role_query_user",
			CLIPath:        "chat +chat-role-query-user",
			PrimaryCLIPath: "chat +chat-role-query-user",
		},
		Description: "查询群成员的群身份",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "查询群成员的群身份",
			UseWhen:      []string{"当你想查看某个群成员当前拥有哪些群身份时使用;只读,--group 可传群名或 openConversationId,用户可传 userId 或 openDingTalkId。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-role-query-user --group <openConversationId> --user <userId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群名或 openConversationId;群名必须唯一匹配", Required: true},
		{Name: "user", Type: shortcut.FlagString, Desc: "用户 userId 或 openDingTalkId", Required: true},
	},
	Tips: []string{`dws chat +chat-role-query-user --group <openConversationId> --user <userId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		groupID, err := resolveStableOrNamedChat(rt)
		if err != nil {
			return err
		}
		user := rt.Str("user")
		params := map[string]any{"openConversationId": groupID}
		if isOpenID(user) {
			params["openDingTalkId"] = user
		} else {
			params["userId"] = user
		}
		err = rt.CallMCP("query_custom_user_roles", params)
		return convergeChatRoleError("query_custom_user_roles", err)
	},
}

ChatRoleQueryUser queries a member's group roles (query_custom_user_roles, im).

View Source
var ChatRoleRemove = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-role-remove",
	Product:     "im",
	Description: "删除群身份",
	Intent:      "当你想删除某个自定义群身份时使用;会实际删除群身份且不可逆,--group 可传群名或 openConversationId,并需身份 openRoleId。",
	Risk:        shortcut.RiskHighWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群名或 openConversationId;群名必须唯一匹配", Required: true},
		{Name: "role-id", Type: shortcut.FlagString, Desc: "群身份 openRoleId", Required: true},
	},
	Tips: []string{`dws chat +chat-role-remove --group <openConversationId> --role-id <openRoleId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		groupID, err := resolveStableOrNamedChat(rt)
		if err != nil {
			return err
		}
		err = rt.CallMCP("remove_custom_group_role", map[string]any{
			"openConversationId": groupID,
			"openRoleId":         rt.Str("role-id"),
		})
		return convergeChatRoleError("remove_custom_group_role", err)
	},
}

ChatRoleRemove deletes a custom group role (remove_custom_group_role, im).

View Source
var ChatRoleRemoveUser = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-role-remove-user",
	Product:     "im",
	Description: "移除用户的指定群身份",
	Intent:      "当你只想撤销某成员的部分群身份、保留其余时使用;--group 可传群名或 openConversationId;会实际移除指定的群身份,需传用户和 openRoleId 列表。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群名或 openConversationId;群名必须唯一匹配", Required: true},
		{Name: "user", Type: shortcut.FlagString, Desc: "用户 userId 或 openDingTalkId", Required: true},
		{Name: "role-ids", Type: shortcut.FlagStringSlice, Desc: "要移除的群身份 openRoleId 列表;必须包含至少一个非空 openRoleId,且不能包含空值或仅含空白的元素", Required: true},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintCustom, Flags: []string{"role-ids"}, Description: "必须包含至少一个非空 openRoleId,且不能包含空值或仅含空白的元素"},
	},
	Tips: []string{`dws chat +chat-role-remove-user --group <openConversationId> --user <userId> --role-ids roleId1`},
	Validate: func(rt *shortcut.RuntimeContext) error {
		return validateChatRoleIDs(rt.StrSlice("role-ids"))
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		groupID, err := resolveStableOrNamedChat(rt)
		if err != nil {
			return err
		}
		user := rt.Str("user")
		return verifyChatRoleUserWrite(
			rt,
			"remove_custom_user_roles",
			groupID,
			user,
			normalizeChatRoleIDs(rt.StrSlice("role-ids")),
			true,
		)
	},
}

ChatRoleRemoveUser removes specific roles from a user (remove_custom_user_roles, im).

View Source
var ChatRoleSetUser = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-role-set-user",
	Product:     "im",
	Description: "设置用户的群身份(覆盖该用户的全部群身份)",
	Intent:      "当你想为某成员整体设定其在群内的身份时使用;--group 可传群名或 openConversationId;会实际覆盖该用户的全部群身份,必须传至少一个 openRoleId;只撤销指定身份时使用 +chat-role-remove-user。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_role_set_user",
			CanonicalPath:  "chat.shortcut_chat_role_set_user",
			CLIPath:        "chat +chat-role-set-user",
			PrimaryCLIPath: "chat +chat-role-set-user",
		},
		Description: "设置用户的群身份(覆盖该用户的全部群身份)",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "设置用户的群身份(覆盖该用户的全部群身份)",
			UseWhen:      []string{"当你想为某成员整体设定其在群内的身份时使用;--group 可传群名或 openConversationId;会实际覆盖该用户的全部群身份,必须传至少一个 openRoleId;只撤销指定身份时使用 +chat-role-remove-user。"},
			AvoidWhen:    []string{"只撤销成员的指定群身份时使用 +chat-role-remove-user;需要未公开的底层参数或不同执行语义时才用对应原子命令。"},
			Examples:     []string{"dws chat +chat-role-set-user --group <openConversationId> --user <userId> --role-ids roleId1,roleId2"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群名或 openConversationId;群名必须唯一匹配", Required: true},
		{Name: "user", Type: shortcut.FlagString, Desc: "用户 userId 或 openDingTalkId", Required: true},
		{Name: "role-ids", Type: shortcut.FlagStringSlice, Desc: "要整体设置的群身份 openRoleId 列表;必须包含至少一个非空 openRoleId,且不能包含空值或仅含空白的元素", Required: true},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintCustom, Flags: []string{"role-ids"}, Description: "必须包含至少一个非空 openRoleId,且不能包含空值或仅含空白的元素"},
	},
	Tips: []string{`dws chat +chat-role-set-user --group <openConversationId> --user <userId> --role-ids roleId1,roleId2`},
	Validate: func(rt *shortcut.RuntimeContext) error {
		return validateChatRoleIDs(rt.StrSlice("role-ids"))
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		roleIDs := normalizeChatRoleIDs(rt.StrSlice("role-ids"))
		groupID, err := resolveStableOrNamedChat(rt)
		if err != nil {
			return err
		}
		user := rt.Str("user")
		return verifyChatRoleUserWrite(rt, "set_custom_user_roles", groupID, user, roleIDs, false)
	},
}

ChatRoleSetUser overwrites a user's group roles (set_custom_user_roles, im).

View Source
var ChatRoleUpdate = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-role-update",
	Product:     "im",
	Description: "更新群身份名称",
	Intent:      "当你想重命名已有的群身份时使用;会实际更新身份名称,--group 可传群名或 openConversationId,并需身份 openRoleId 和新名称。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_role_update",
			CanonicalPath:  "chat.shortcut_chat_role_update",
			CLIPath:        "chat +chat-role-update",
			PrimaryCLIPath: "chat +chat-role-update",
		},
		Description: "更新群身份名称",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "更新群身份名称",
			UseWhen:      []string{"当你想重命名已有的群身份时使用;会实际更新身份名称,--group 可传群名或 openConversationId,并需身份 openRoleId 和新名称。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-role-update --group <openConversationId> --role-id <openRoleId> --name \"新名称\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群名或 openConversationId;群名必须唯一匹配", Required: true},
		{Name: "role-id", Type: shortcut.FlagString, Desc: "群身份 openRoleId", Required: true},
		{Name: "name", Type: shortcut.FlagString, Desc: "群身份新名称", Required: true},
	},
	Tips: []string{`dws chat +chat-role-update --group <openConversationId> --role-id <openRoleId> --name "新名称"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		groupID, err := resolveStableOrNamedChat(rt)
		if err != nil {
			return err
		}
		err = rt.CallMCP("update_custom_group_role", map[string]any{
			"openConversationId": groupID,
			"openRoleId":         rt.Str("role-id"),
			"name":               rt.Str("name"),
		})
		return convergeChatRoleError("update_custom_group_role", err)
	},
}

ChatRoleUpdate renames a custom group role (update_custom_group_role, im).

View Source
var ChatSearch = shortcut.Shortcut{
	Service:                  "chat",
	Command:                  "+chat-search",
	Aliases:                  []string{"+chat-group-search", "+search-group"},
	SinglePositionalAliasFor: "query",
	Product:                  "im",
	Description:              "按关键词分页搜索群聊,支持有界自动翻页和完整性检查",
	Intent:                   "当你只记得真实群聊/聊天群名称关键词、需要拿到群 openConversationId 时使用;不是搜索会话分组/分类名。默认读取一页,明确要求全部候选时加 --page-all,并用 --page-limit 保持有界。结果按 openConversationId 去重,并公开 complete、hasMore、nextCursor、stopReason 和 failures,避免把截断或失败结果误当完整候选集。",
	Risk:                     shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_search",
			CanonicalPath:  "chat.shortcut_chat_search",
			CLIPath:        "chat +chat-search",
			PrimaryCLIPath: "chat +chat-search",
		},
		Description: "按关键词分页搜索群聊,支持有界自动翻页和完整性检查",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "按关键词分页搜索群聊,支持有界自动翻页和完整性检查",
			UseWhen:      []string{"当你只记得真实群聊/聊天群名称关键词、需要拿到群 openConversationId 时使用;不是搜索会话分组/分类名。默认读取一页,明确要求全部候选时加 --page-all,并用 --page-limit 保持有界。结果按 openConversationId 去重,并公开 complete、hasMore、nextCursor、stopReason 和 failures,避免把截断或失败结果误当完整候选集。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-search --query \"项目冲刺\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "member-ids", Type: shortcut.FlagStringSlice, Desc: "候选群必须包含全部指定OpenID;最多核验100个候选,自动有界扫描"},
		{Name: "is-manager", Type: shortcut.FlagBool, Desc: "仅本人为群主或管理员的候选群;自动有界扫描"},
		{Name: "chat-modes", Type: shortcut.FlagStringSlice, Desc: "按已验证channel筛选group/topic;未知模式报错"},
		{Name: "sort", Type: shortcut.FlagString, Enum: []string{"create_time", "member_count"}, Desc: "已取候选按创建时间或成员数降序;不承诺全局完整"},
		{Name: "page-delay", Type: shortcut.FlagInt, Default: "0", Desc: "后续页间隔毫秒(0–60000)"},
		{Name: "query", Type: shortcut.FlagString, Desc: "群名称关键词"},
		{Name: "keyword", Type: shortcut.FlagString, Desc: "--query 的别名", Hidden: true},
		{Name: "limit", Type: shortcut.FlagInt, Default: "20", Desc: "每页返回数量;显式页大小必须在 1-100 之间"},
		{Name: "page-size", Type: shortcut.FlagInt, Desc: "--limit 的 Lark 对齐别名;显式页大小必须在 1-100 之间"},
		{Name: "size", Type: shortcut.FlagInt, Desc: "--limit 的旧版别名", Hidden: true},
		{Name: "cursor", Type: shortcut.FlagString, Default: "0", Desc: "分页游标,翻页传 nextCursor"},
		{Name: "page-token", Type: shortcut.FlagString, Desc: "--cursor 的 Lark 对齐别名"},
		{Name: "page-all", Type: shortcut.FlagBool, Desc: "自动读取全部群搜索分页;--page-limit 仅与 --page-all 一起使用且范围 1-500"},
		{Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
		{Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "排除已设置免打扰的群聊"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"query", "keyword"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"limit", "page-size", "size"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"cursor", "page-token"}},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"limit", "page-size"}, Description: "显式页大小必须在 1-100 之间"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
	},
	Tips: []string{
		`dws chat +chat-search --query "项目冲刺"`,
		`dws chat +chat-search --query "项目" --page-size 100 --page-all --page-limit 20`,
	},
	Validate: validateChatSearch,
	Execute:  executeChatSearch,
}

ChatSearch searches groups by keyword (search_groups on the im server).

View Source
var ChatSetAdmin = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-set-admin",
	Product:     "im",
	Description: "设置 / 取消群管理员",
	Intent:      "当你想把某些成员设为或取消群管理员时使用;会实际变更成员角色,需传群 openConversationId 和成员 userId/openDingTalkId 列表,加 --off 取消管理员。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_set_admin",
			CanonicalPath:  "chat.shortcut_chat_set_admin",
			CLIPath:        "chat +chat-set-admin",
			PrimaryCLIPath: "chat +chat-set-admin",
		},
		Description: "设置 / 取消群管理员",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "设置 / 取消群管理员",
			UseWhen:      []string{"当你想把某些成员设为或取消群管理员时使用;会实际变更成员角色,需传群 openConversationId 和成员 userId/openDingTalkId 列表,加 --off 取消管理员。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-set-admin --group <openConversationId> --users userId1,userId2"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "users", Type: shortcut.FlagStringSlice, Desc: "成员 userId 或 openDingTalkId 列表", Required: true},
		{Name: "off", Type: shortcut.FlagBool, Desc: "取消管理员(不传则设为管理员)"},
	},
	Tips: []string{`dws chat +chat-set-admin --group <openConversationId> --users userId1,userId2`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		userIDs, openIDs := splitIDs(rt.StrSlice("users"))
		params := map[string]any{
			"openConversationId": rt.Str("group"),
			"admin":              !rt.Bool("off"),
		}
		if len(userIDs) > 0 {
			params["uids"] = userIDs
		}
		if len(openIDs) > 0 {
			params["openDingTalkIds"] = openIDs
		}
		return rt.CallMCP("update_conv_member_roles", params)
	},
}

ChatSetAdmin sets/unsets group admins (update_conv_member_roles, im).

View Source
var ChatSetHistory = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-set-history",
	Product:     "im",
	Description: "设置新成员入群可查看历史消息范围",
	Intent:      "当你想控制新成员入群后能看到多少历史消息时使用;会实际修改群配置,需传群 openConversationId 和范围(FORBIDDEN 不可见 / RECENT_100 最近100条 / ALL 全部)。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_set_history",
			CanonicalPath:  "chat.shortcut_chat_set_history",
			CLIPath:        "chat +chat-set-history",
			PrimaryCLIPath: "chat +chat-set-history",
		},
		Description: "设置新成员入群可查看历史消息范围",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "设置新成员入群可查看历史消息范围",
			UseWhen:      []string{"当你想控制新成员入群后能看到多少历史消息时使用;会实际修改群配置,需传群 openConversationId 和范围(FORBIDDEN 不可见 / RECENT_100 最近100条 / ALL 全部)。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-set-history --group <openConversationId> --option RECENT_100"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "option", Type: shortcut.FlagString, Desc: "可见范围", Required: true, Enum: []string{"FORBIDDEN", "RECENT_100", "ALL"}},
	},
	Tips: []string{`dws chat +chat-set-history --group <openConversationId> --option RECENT_100`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("update_show_history_msg_option", map[string]any{
			"openConversationId": rt.Str("group"),
			"option":             rt.Str("option"),
		})
	},
}

ChatSetHistory sets new-member history visibility (update_show_history_msg_option, im).

View Source
var ChatTransferOwner = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-transfer-owner",
	Product:     "im",
	Description: "转让群主",
	Intent:      "当你要把群主身份转让给他人时使用;会实际变更群主(自己不再是群主),需传群 openConversationId 和新群主的 userId 或 openDingTalkId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "new-owner", Type: shortcut.FlagString, Desc: "新群主 userId 或 openDingTalkId", Required: true},
	},
	Tips: []string{`dws chat +chat-transfer-owner --group <openConversationId> --new-owner <openDingTalkId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		newOwner := rt.Str("new-owner")
		params := map[string]any{
			"openConversationId": rt.Str("group"),
			"cid":                rt.Str("group"),
		}
		if isOpenID(newOwner) {
			params["newOwnerOpenDingTalkId"] = newOwner
		} else {
			params["newOwnerUid"] = newOwner
		}
		return rt.CallMCP("transfer_group_owner", params)
	},
}

ChatMemberAdd adds members to a group (add_group_member, chat server). ChatMemberRemove removes members from a group (remove_group_member, chat server). ChatUpdateName renames a group (update_group_name, chat server). ChatTransferOwner transfers group ownership (transfer_group_owner, im).

View Source
var ChatUpdate = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-update",
	Aliases:     []string{"+chat-rename"},
	Product:     "chat",
	Description: "更新群名称(仅名称,不支持 description)",
	Intent:      "当你只需要修改群名称时使用;--group 可传群名或 openConversationId,群名必须唯一解析后才会写入。修改群 description、个人备注、群昵称或其他群设置时不要使用。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群名称或 openConversationId", Required: true},
		{Name: "name", Type: shortcut.FlagString, Desc: "新的群名称", Required: true},
	},
	Tips: []string{`dws chat +chat-update --group <群名或openConversationId> --name "新群名"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		resolved, err := targetresolver.ResolveChatTarget(rt, rt.Str("group"), "")
		if err != nil {
			return err
		}
		return rt.CallMCP("update_group_name", map[string]any{
			"openconversation_id": resolved.Selected.OpenConversationID,
			"group_name":          rt.Str("name"),
		})
	},
}

ChatUpdate intentionally supports only the group-name subset shared by DWS and lark-cli. A description flag is omitted because DWS has no such lower confirmed description write contract.

View Source
var ChatUpdateAlias = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-update-alias",
	Product:     "im",
	Description: "设置群备注(仅自己可见)",
	Intent:      "当你想给某个群设置仅自己可见的备注名以便区分同名群时使用;会实际保存本人对该群的备注,需传群 openConversationId 和备注标题。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_update_alias",
			CanonicalPath:  "chat.shortcut_chat_update_alias",
			CLIPath:        "chat +chat-update-alias",
			PrimaryCLIPath: "chat +chat-update-alias",
		},
		Description: "设置群备注(仅自己可见)",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "设置群备注(仅自己可见)",
			UseWhen:      []string{"当你想给某个群设置仅自己可见的备注名以便区分同名群时使用;会实际保存本人对该群的备注,需传群 openConversationId 和备注标题。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-update-alias --group <openConversationId> --alias-title \"项目A群\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "alias-title", Type: shortcut.FlagString, Desc: "群备注标题", Required: true},
	},
	Tips: []string{`dws chat +chat-update-alias --group <openConversationId> --alias-title "项目A群"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("update_user_group_alias", map[string]any{
			"openConversationId": rt.Str("group"),
			"aliasTitle":         rt.Str("alias-title"),
		})
	},
}

ChatUpdateAlias sets the caller's private alias for a group (update_user_group_alias, im).

View Source
var ChatUpdateIcon = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-update-icon",
	Product:     "im",
	Description: "更新群头像",
	Intent:      "当你想更换群头像时使用;会实际更新群头像,需传群 openConversationId 和已上传头像的 mediaId(以 @ 开头)。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "icon-media-id", Type: shortcut.FlagString, Desc: "群头像 mediaId(以 @ 开头)", Required: true},
	},
	Tips: []string{`dws chat +chat-update-icon --group <openConversationId> --icon-media-id <mediaId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("update_group_icon", map[string]any{
			"openConversationId": rt.Str("group"),
			"iconMediaId":        rt.Str("icon-media-id"),
		})
	},
}

ChatUpdateIcon updates the group icon (update_group_icon, im).

View Source
var ChatUpdateNick = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-update-nick",
	Product:     "im",
	Description: "设置当前用户在群内的群昵称",
	Intent:      "当你想设置当前用户在某个群里显示的群昵称时使用;会实际更新本人在该群的昵称,需传群 openConversationId 和昵称。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_chat_update_nick",
			CanonicalPath:  "chat.shortcut_chat_update_nick",
			CLIPath:        "chat +chat-update-nick",
			PrimaryCLIPath: "chat +chat-update-nick",
		},
		Description: "设置当前用户在群内的群昵称",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "设置当前用户在群内的群昵称",
			UseWhen:      []string{"当你想设置当前用户在某个群里显示的群昵称时使用;会实际更新本人在该群的昵称,需传群 openConversationId 和昵称。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +chat-update-nick --group <openConversationId> --nick \"我的群昵称\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "nick", Type: shortcut.FlagString, Desc: "个人群昵称", Required: true},
	},
	Tips: []string{`dws chat +chat-update-nick --group <openConversationId> --nick "我的群昵称"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("update_group_nick", map[string]any{
			"openConversationId": rt.Str("group"),
			"nick":               rt.Str("nick"),
		})
	},
}

ChatUpdateNick sets the caller's in-group nickname (update_group_nick, im).

View Source
var ChatUpdateSettings = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+chat-update-settings",
	Product:     "im",
	Description: "更新群设置(settingKey + status)",
	Intent:      "当你想调整群的某项开关设置(如是否可被搜索 searchable、是否仅管理员可@所有人 onlyAdminCanAtAll)时使用;会实际修改群设置,需传 settingKey 和 status(0关/1开)。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "setting-key", Type: shortcut.FlagString, Desc: "群设置项 key,如 searchable / onlyAdminCanAtAll", Required: true},
		{Name: "status", Type: shortcut.FlagInt, Desc: "设置值:0=关闭,1=开启", Required: true},
	},
	Tips: []string{`dws chat +chat-update-settings --group <openConversationId> --setting-key searchable --status 1`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("update_group_settings", map[string]any{
			"openConversationId": rt.Str("group"),
			"settingKey":         rt.Str("setting-key"),
			"status":             rt.Int("status"),
		})
	},
}

ChatUpdateSettings updates a group setting (update_group_settings, im).

View Source
var ConversationClearAllRedPoint = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-clear-all-red-point",
	Product:     "im",
	Description: "清除所有会话红点(全部已读)",
	Intent:      "当你想一键把全部会话标记为已读、清空所有红点时使用;会实际清除当前用户所有会话的红点,无需任何参数。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_conversation_clear_all_red_point",
			CanonicalPath:  "chat.shortcut_conversation_clear_all_red_point",
			CLIPath:        "chat +conversation-clear-all-red-point",
			PrimaryCLIPath: "chat +conversation-clear-all-red-point",
		},
		Description: "清除所有会话红点(全部已读)",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "清除所有会话红点(全部已读)",
			UseWhen:      []string{"当你想一键把全部会话标记为已读、清空所有红点时使用;会实际清除当前用户所有会话的红点,无需任何参数。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +conversation-clear-all-red-point"},
		},
	},
	Tips: []string{`dws chat +conversation-clear-all-red-point`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("clear_all_red_point", map[string]any{})
	},
}

ConversationClearAllRedPoint clears all red points (clear_all_red_point, im).

View Source
var ConversationClearMessages = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-clear-messages",
	Product:     "im",
	Description: "清空当前用户指定会话的聊天记录(仅本人视角,不可逆)",
	Intent:      "当你要清空自己在某个会话里的聊天记录时使用;仅影响本人视角,但会实际删除且不可逆,需传 openConversationId,务必谨慎操作。",
	Risk:        shortcut.RiskHighWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
	},
	Tips: []string{`dws chat +conversation-clear-messages --conversation-id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("clear_conversation_messages", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"cid":                rt.Str("conversation-id"),
		})
	},
}

ConversationClearMessages clears a conversation's chat records (clear_conversation_messages, im).

View Source
var ConversationClearRedPoint = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-clear-red-point",
	Product:     "im",
	Description: "清除会话红点",
	Intent:      "当你想消除某个会话上的未读红点(小圆点)时使用;会实际清除该会话红点,需传 openConversationId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
	},
	Tips: []string{`dws chat +conversation-clear-red-point --conversation-id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("clear_conversation_red_point", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"cid":                rt.Str("conversation-id"),
		})
	},
}

ConversationClearRedPoint clears a conversation red point (clear_conversation_red_point, im).

View Source
var ConversationHide = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-hide",
	Product:     "im",
	Description: "会话列表中隐藏会话(收到新消息会重新出现)",
	Intent:      "当你想把某个会话从会话列表中暂时隐藏、让列表更清爽时使用;会实际隐藏该会话(收到新消息会自动重新出现),需传 openConversationId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
	},
	Tips: []string{`dws chat +conversation-hide --conversation-id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("hide_conversation", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"cid":                rt.Str("conversation-id"),
		})
	},
}

ConversationHide hides a conversation from the list (hide_conversation, im).

View Source
var ConversationInfo = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-info",
	Product:     "chat",
	Description: "获取会话信息(群聊传 --group,单聊传 --open-dingtalk-id)",
	Intent:      "当你已有群 openConversationId 或单聊对方 openDingTalkId、需要查看该会话的名称/类型/成员数等基础信息时使用;只读,群聊传 --group、单聊传 --open-dingtalk-id 二选一。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_conversation_info",
			CanonicalPath:  "chat.shortcut_conversation_info",
			CLIPath:        "chat +conversation-info",
			PrimaryCLIPath: "chat +conversation-info",
		},
		Description: "获取会话信息(群聊传 --group,单聊传 --open-dingtalk-id)",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "获取会话信息(群聊传 --group,单聊传 --open-dingtalk-id)",
			UseWhen:      []string{"当你已有群 openConversationId 或单聊对方 openDingTalkId、需要查看该会话的名称/类型/成员数等基础信息时使用;只读,群聊传 --group、单聊传 --open-dingtalk-id 二选一。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +conversation-info --group <openConversationId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群聊 openConversationId"},
		{Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊对方 openDingTalkId"},
	},
	Tips: []string{`dws chat +conversation-info --group <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{}
		if rt.Str("group") != "" {
			params["openConversationId"] = rt.Str("group")
		}
		if rt.Str("open-dingtalk-id") != "" {
			if err := targetresolver.ValidateExplicitOpenDingTalkID("--open-dingtalk-id", rt.Str("open-dingtalk-id")); err != nil {
				return err
			}
			params["openDingTalkId"] = rt.Str("open-dingtalk-id")
		}
		if len(params) == 0 {
			return fmt.Errorf("--group 或 --open-dingtalk-id 必填其一")
		}
		return rt.CallMCP("get_conversation_info", params)
	},
}

ConversationInfo gets conversation info (get_conversation_info, chat server).

View Source
var ConversationList = shortcut.Shortcut{

	OutputRollout: output.RolloutDualValidate,
	Service:       "chat",
	Command:       "+conversation-list",
	Product:       "im",
	Description:   "分页或一键全量获取当前用户的会话列表(单聊+群聊)",
	Intent:        "当你想遍历当前用户的所有会话(单聊+群聊)做统计、清理或批量处理时使用;默认读取一页,明确要求全部时使用 --page-all,CLI 会按服务端每页上限自动翻页并公开完整性 ledger;可用 --exclude-muted 排除已免打扰会话。",
	Risk:          shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_conversation_list",
			CanonicalPath:  "chat.shortcut_conversation_list",
			CLIPath:        "chat +conversation-list",
			PrimaryCLIPath: "chat +conversation-list",
		},
		Description: "分页或一键全量获取当前用户的会话列表(单聊+群聊)",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "分页或一键全量获取当前用户的会话列表(单聊+群聊)",
			UseWhen:      []string{"当你想遍历当前用户的所有会话(单聊+群聊)做统计、清理或批量处理时使用;默认读取一页,明确要求全部时使用 --page-all,CLI 会按服务端每页上限自动翻页并公开完整性 ledger;可用 --exclude-muted 排除已免打扰会话。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +conversation-list --limit 50"},
		},
		Result: conversationDiscoveryResult(),
		Pagination: &contract.PaginationSpec{
			Kind:                  contract.PaginationKindCursor,
			CursorParameter:       "cursor",
			MetaPath:              contract.PaginationMetaPath,
			EndpointExhaustedPath: contract.PaginationExhaustedPath,
			NextTokenPath:         contract.PaginationNextTokenPath,
		},
	},
	Flags: append([]shortcut.Flag{
		{Name: "limit", Type: shortcut.FlagInt, Default: "100", Desc: "每页数量;--limit 必须在 1-100"},
		{Name: "cursor", Type: shortcut.FlagInt, Desc: "分页游标(首次不传或 0)"},
		{Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "排除已免打扰会话"},
		{Name: "page-all", Type: shortcut.FlagBool, Desc: "自动读取全部分页;--page-limit 仅与 --page-all 一起使用且范围 1-500;--max-items/--page-delay 仅与 --page-all 一起使用;值必须大于等于 0"},
		{Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
	}, shortcut.AutoPageControlFlags()...),
	Constraints: append([]shortcut.Constraint{
		{Kind: shortcut.ConstraintCustom, Flags: []string{"limit"}, Description: "--limit 必须在 1-100"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
	}, shortcut.AutoPageControlConstraints()...),
	Tips: []string{
		`dws chat +conversation-list --limit 50`,
		`dws chat +conversation-list --page-all --limit 100`,
	},
	Validate: func(rt *shortcut.RuntimeContext) error {
		if limit := rt.Int("limit"); limit < 1 || limit > 100 {
			return apperrors.NewValidation("--limit 必须在 1-100 之间;读取全部会话请使用 --page-all")
		}
		if !rt.Bool("page-all") && rt.Changed("page-limit") {
			return apperrors.NewValidation("--page-limit 仅与 --page-all 一起使用")
		}
		if pageLimit := rt.Int("page-limit"); pageLimit < 1 || pageLimit > 500 {
			return apperrors.NewValidation("--page-limit 必须在 1-500 之间")
		}
		if err := shortcut.ValidateAutoPageControls(rt); err != nil {
			return apperrors.NewValidation(err.Error())
		}
		return nil
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		cursor := int64(rt.Int("cursor"))
		pageLimit := 1
		if rt.Bool("page-all") {
			pageLimit = rt.Int("page-limit")
		}
		convs := make([]map[string]any, 0)
		seenConversations := map[string]bool{}
		seenCursors := map[int64]bool{cursor: true}
		pagesFetched := 0
		complete := false
		hasMore := false
		nextCursor := int64(0)
		stopReason := "source_complete"
		truncatedByPageLimit := false
		truncatedByResultLimit := false
		unsafeContinuation := false
		paginationKnown := true
		failures := make([]map[string]any, 0)
		var terminalCause error
		for pagesFetched < pageLimit {
			if pagesFetched > 0 {
				if err := shortcut.WaitAutoPageDelay(rt); err != nil {
					failures = append(failures, map[string]any{"stage": "conversation-page-delay", "cursor": cursor, "error": err.Error()})
					terminalCause = err
					stopReason = "delay_interrupted"
					break
				}
			}
			params := map[string]any{"limit": shortcut.AutoPageRequestSize(rt, rt.Int("limit"), len(convs))}
			if cursor > 0 {
				params["cursor"] = cursor
			}
			if rt.Bool("exclude-muted") {
				params["excludeMuted"] = true
			}
			data, err := rt.CallMCPData("im", "list_all_conversations", params)
			if err != nil {
				if pagesFetched == 0 {
					return err
				}
				failures = append(failures, map[string]any{"stage": "conversation-page", "cursor": cursor, "error": err.Error()})
				terminalCause = err
				stopReason = "read_failure"
				break
			}
			pagesFetched++
			overflowOnPage := false
			pageConversations, projectionFailures, projectionCause := conversationListProjectChecked(data)
			for _, conversation := range pageConversations {
				id := strings.TrimSpace(fmt.Sprint(conversation["openConversationId"]))
				if id != "" && id != "<nil>" {
					if seenConversations[id] {
						continue
					}
					seenConversations[id] = true
				}
				if maxItems := rt.Int("max-items"); maxItems > 0 && len(convs) >= maxItems {
					truncatedByResultLimit = true
					overflowOnPage = true
					continue
				}
				convs = append(convs, conversation)
			}
			if len(projectionFailures) > 0 {
				failures = append(failures, projectionFailures...)
				unsafeContinuation = true
			}
			page, paginationErr := chatListPagination(data)
			if paginationErr != nil {
				paginationKnown = false
				unsafeContinuation = true
				nextCursor = 0
				terminalCause = paginationErr
				failures = append(failures, map[string]any{
					"stage": "conversation-pagination",
					"error": paginationErr.Error(),
				})
				stopReason = "pagination_error"
				break
			}
			hasMoreValue, known := page["hasMore"].(bool)
			candidateCursor, cursorErr := conversationPaginationCursor(page["nextCursor"])
			hasMore = hasMoreValue
			if !known {
				paginationKnown = false
				paginationFailure := fmt.Errorf("下层未返回 hasMore,无法证明结果完整")
				if !unsafeContinuation && cursorErr == nil && candidateCursor > 0 && !seenCursors[candidateCursor] {
					hasMore = true
					nextCursor = candidateCursor
				} else {
					unsafeContinuation = true
					nextCursor = 0
				}
				failures = append(failures, map[string]any{
					"stage": "conversation-pagination",
					"error": paginationFailure.Error(),
				})
				terminalCause = paginationFailure
				stopReason = "pagination_error"
				break
			}
			if len(projectionFailures) > 0 {
				complete = false
				unsafeContinuation = true
				nextCursor = 0
				terminalCause = projectionCause
				stopReason = "projection_error"
				break
			}
			if overflowOnPage {
				paginationFailure := fmt.Errorf("下层返回条数超过请求的剩余额度,无法生成不跳项的安全续页游标")
				hasMore = true
				nextCursor = 0
				unsafeContinuation = true
				failures = append(failures, map[string]any{"stage": "conversation-pagination", "error": paginationFailure.Error()})
				terminalCause = paginationFailure
				stopReason = "pagination_error"
				break
			}
			if !hasMore {
				complete = true
				nextCursor = 0
				stopReason = "source_complete"
				break
			}
			if cursorErr != nil || candidateCursor <= 0 || seenCursors[candidateCursor] {
				paginationFailure := cursorErr
				if paginationFailure == nil {
					paginationFailure = fmt.Errorf("hasMore=true 但 nextCursor 缺失、无效或未前进")
				}
				unsafeContinuation = true
				nextCursor = 0
				failures = append(failures, map[string]any{"stage": "conversation-pagination", "error": paginationFailure.Error()})
				terminalCause = paginationFailure
				stopReason = "pagination_error"
				break
			}
			nextCursor = candidateCursor
			if !rt.Bool("page-all") {
				stopReason = "single_page"
				break
			}
			if maxItems := rt.Int("max-items"); maxItems > 0 && len(convs) >= maxItems {
				truncatedByResultLimit = true
				stopReason = "result_limit"
				break
			}
			seenCursors[nextCursor] = true
			cursor = nextCursor
		}
		if rt.Bool("page-all") && hasMore && pagesFetched == pageLimit && !truncatedByResultLimit && len(failures) == 0 {
			truncatedByPageLimit = true
			stopReason = "page_limit"
		}
		payload := map[string]any{
			"count":                  len(convs),
			"conversations":          convs,
			"pagesFetched":           pagesFetched,
			"complete":               complete,
			"hasMore":                hasMore,
			"nextCursor":             nextCursor,
			"paginationKnown":        paginationKnown,
			"stopReason":             stopReason,
			"truncatedByPageLimit":   truncatedByPageLimit,
			"truncatedByResultLimit": truncatedByResultLimit,
			"failedCount":            len(failures),
			"failures":               failures,
			"partial":                len(failures) > 0 && len(convs) > 0,
			"discoveryOnly":          true,
		}
		payload["nextActions"] = conversationDiscoveryNextActions(
			convs, hasMore, nextCursor, unsafeContinuation, rt.Int("limit"), rt.Bool("exclude-muted"),
		)
		chatmsg.ApplyTruncation(payload)
		if len(failures) > 0 {
			failureStage := "pagination"
			retryable := nextCursor > 0 && !unsafeContinuation
			if stopReason == "read_failure" {
				failureStage = "read"
			} else if stopReason == "delay_interrupted" {
				failureStage = "pagination_delay"
			} else if stopReason == "projection_error" {
				failureStage = "projection"
			}
			origin := "shortcut"
			if stopReason == "read_failure" {
				origin = "mcp_gateway"
			} else if stopReason == "delay_interrupted" {
				origin = "client"
			}
			incompleteErr := helpers.NewIncompleteResultError(
				fmt.Sprintf("会话列表分页未完成:成功读取 %d 页,存在 %d 个失败项", pagesFetched, len(failures)),
				terminalCause,
				retryable,
				apperrors.WithOperation("im/list_all_conversations"),
				apperrors.WithReason("conversation_list_incomplete"),
				apperrors.WithOrigin(origin),
				apperrors.WithFailureStage(failureStage),
				apperrors.WithExecutionStarted(true),
				apperrors.WithHint("请保留 details.partialResult 中已发现的会话,并根据其中的 failures 和 nextCursor 重试"),
				apperrors.WithDetails(map[string]any{
					"count":         len(convs),
					"failedCount":   len(failures),
					"stopReason":    stopReason,
					"nextCursor":    nextCursor,
					"partialResult": payload,
				}),
			)
			return rt.OutputIncomplete(payload, incompleteErr)
		}
		nextToken := ""
		if nextCursor > 0 {
			nextToken = strconv.FormatInt(nextCursor, 10)
		}
		pagination, paginationErr := newConversationResultPagination(paginationKnown && !hasMore, nextToken)
		if paginationErr != nil {
			return apperrors.NewInternal(
				"会话列表生成了不可发布的分页元数据",
				apperrors.WithOperation("im/list_all_conversations"),
				apperrors.WithReason("invalid_result_pagination"),
				apperrors.WithOrigin("shortcut"),
				apperrors.WithFailureStage("result_projection"),
				apperrors.WithExecutionStarted(pagesFetched > 0),
				apperrors.WithRetryable(false),
				apperrors.WithCause(paginationErr),
			)
		}
		pagination.Pages = pagesFetched
		pagination.Items = len(convs)
		return rt.OutputWithMeta(payload, &output.Meta{
			Count: output.NewCount(len(convs)), Pagination: pagination,
		})
	},
}

ConversationList paginates all conversations (list_all_conversations, im).

View Source
var ConversationListTop = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-list-top",
	Aliases:     []string{"+feed-shortcut-list"},
	Description: "拉取置顶会话列表,可只看群聊或单聊",
	Intent:      "当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,并把下层 singleChat 规范化为 conversationType=group|direct。可用 --type group 只看群聊、--type direct 只看单聊,或用 --exclude-muted 排除已免打扰会话。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_conversation_list_top",
			CanonicalPath:  "chat.shortcut_conversation_list_top",
			CLIPath:        "chat +conversation-list-top",
			PrimaryCLIPath: "chat +conversation-list-top",
			Aliases:        []string{"chat +feed-shortcut-list"},
		},
		Description: "拉取置顶会话列表,可只看群聊或单聊",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "拉取置顶会话列表,可只看群聊或单聊",
			UseWhen:      []string{"当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,并把下层 singleChat 规范化为 conversationType=group|direct。可用 --type group 只看群聊、--type direct 只看单聊,或用 --exclude-muted 排除已免打扰会话。"},
			AvoidWhen:    []string{"用户要查看群内被 Pin 的消息而不是侧边栏置顶会话时,改用 +messages-list-pin"},
			Examples: []string{
				"dws chat +conversation-list-top --type group --limit 1000",
				"dws chat +conversation-list-top --type direct --limit 1000",
			},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "limit", Type: shortcut.FlagInt, Desc: "每页数量"},
		{Name: "no-detail", Type: shortcut.FlagBool, Desc: "跳过会话详情补查(默认补查并精确核对ID)"},
		{Name: "cursor", Type: shortcut.FlagInt, Aliases: []string{"page-token"}, Desc: "分页游标(首次不传或 0)"},
		{Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "排除已免打扰会话"},
		{Name: "type", Type: shortcut.FlagString, Default: "all", Desc: "会话类型:all 全部 / group 群聊 / direct 单聊(当前页本地过滤)", Enum: []string{"all", "group", "direct"}},
	},
	Tips: []string{
		`dws chat +conversation-list-top --limit 1000`,
		`dws chat +conversation-list-top --type group --limit 1000`,
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{}
		if rt.Int("limit") > 0 {
			params["limit"] = rt.Int("limit")
		}
		if rt.IntFirst("cursor", "page-token") > 0 {
			params["cursor"] = rt.IntFirst("cursor", "page-token")
		}
		if rt.Bool("exclude-muted") {
			params["excludeMuted"] = true
		}
		data, err := rt.CallMCPData("chat", "list_top_conversations", params)
		if err != nil {
			return err
		}
		if _, err := StrictChatCollection(data, "conversations", "items", "list"); err != nil {
			return err
		}
		convs := conversationListTopProject(data)
		typeFilter := rt.Str("type")
		convs = conversationListTopFilter(convs, typeFilter)
		payload := map[string]any{
			"count":         len(convs),
			"requestedType": typeFilter,
			"conversations": convs,
		}
		chatmsg.ApplyPagination(payload, data)
		if !rt.Bool("no-detail") {
			if err := attachConversationDetails(rt, payload, convs); err != nil {
				return err
			}
		}
		return rt.Output(payload)
	},
}

ConversationListTop lists pinned conversations (list_top_conversations, chat server).

View Source
var ConversationMarkRead = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-mark-read",
	Product:     "im",
	Description: "标记消息已读(该消息及之前的消息都标记为已读)",
	Intent:      "当你想把某会话中某条消息及其之前的所有消息都标记为已读时使用;会实际更新已读位置,需传 openConversationId 和该消息 openMessageId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "message-id", Type: shortcut.FlagString, Desc: "消息 openMessageId", Required: true},
	},
	Tips: []string{`dws chat +conversation-mark-read --conversation-id <openConversationId> --message-id <openMessageId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("mark_message_read", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"openMessageId":      rt.Str("message-id"),
		})
	},
}

ConversationMarkRead marks a message read (mark_message_read, im).

View Source
var ConversationMarkUnread = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-mark-unread",
	Product:     "im",
	Description: "标记会话为未读",
	Intent:      "当你想把某个已读会话重新标记为未读(提醒自己稍后再处理)时使用;会实际改变该会话的未读状态,需传 openConversationId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
	},
	Tips: []string{`dws chat +conversation-mark-unread --conversation-id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("mark_conversation_unread", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"cid":                rt.Str("conversation-id"),
		})
	},
}

ConversationMarkUnread marks a conversation unread (mark_conversation_unread, im).

View Source
var ConversationMute = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-mute",
	Product:     "im",
	Description: "会话消息免打扰(支持单聊/群聊)",
	Intent:      "当你想对某个会话开启或关闭消息免打扰时使用;会实际更改该会话的免打扰设置,需传 openConversationId,加 --off 表示关闭免打扰。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "off", Type: shortcut.FlagBool, Desc: "关闭免打扰(不传则开启免打扰)"},
	},
	Tips: []string{`dws chat +conversation-mute --conversation-id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("update_notification_off", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"cid":                rt.Str("conversation-id"),
			"mute":               !rt.Bool("off"),
		})
	},
}

ConversationMute mutes/unmutes a conversation (update_notification_off, im).

View Source
var ConversationMuteAtAll = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-mute-at-all",
	Product:     "im",
	Description: "关闭/开启 @所有人消息提醒",
	Intent:      "当你已对某个会话开启消息免打扰,并希望额外关闭或恢复'@所有人'提醒时使用;这是免打扰的子开关,若尚未开启总免打扰,先执行 +conversation-mute,否则平台会返回 NotificationOffNotEnabled。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "off", Type: shortcut.FlagBool, Desc: "恢复接收 @所有人通知(不传则关闭通知)"},
	},
	Tips: []string{`dws chat +conversation-mute-at-all --conversation-id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("update_at_all_notification_off", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"mute":               !rt.Bool("off"),
		})
	},
}

ConversationMuteAtAll toggles @all notification (update_at_all_notification_off, im).

View Source
var ConversationMuteRedEnvelope = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-mute-red-envelope",
	Product:     "im",
	Description: "关闭/开启红包消息提醒",
	Intent:      "当你已对某个会话开启消息免打扰,并希望额外关闭或恢复红包提醒时使用;这是免打扰的子开关,若尚未开启总免打扰,或刚恢复过@所有人提醒,先执行 +conversation-mute,否则平台会返回 NotificationOffNotEnabled。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "off", Type: shortcut.FlagBool, Desc: "恢复接收红包通知(不传则关闭通知)"},
	},
	Tips: []string{`dws chat +conversation-mute-red-envelope --conversation-id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("update_red_env_notification_off", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"mute":               !rt.Bool("off"),
		})
	},
}

ConversationMuteRedEnvelope toggles red-envelope notification (update_red_env_notification_off, im).

View Source
var ConversationSetTop = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+conversation-set-top",
	Product:     "im",
	Description: "批量会话置顶 / 取消置顶(最多 10 个)",
	Intent:      "当你想把一个或多个单聊/群聊置顶到会话列表顶部、或取消置顶时使用;支持 1-10 个 openConversationId,逐项执行并返回成功/失败 ledger,某一项失败不阻断其余项。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "单个会话 openConversationId;会话 ID 去重后必须为 1-10 个"},
		{Name: "conversation-ids", Type: shortcut.FlagStringSlice, Desc: "多个会话 openConversationId;会话 ID 去重后必须为 1-10 个"},
		{Name: "off", Type: shortcut.FlagBool, Desc: "取消置顶(不传则设置置顶)"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"conversation-id", "conversation-ids"}},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"conversation-id", "conversation-ids"},
			Description: "会话 ID 去重后必须为 1-10 个",
		},
	},
	Tips: []string{
		`dws chat +conversation-set-top --conversation-id <openConversationId>`,
		`dws chat +conversation-set-top --conversation-ids <cid1>,<cid2> --off`,
	},
	Validate: func(rt *shortcut.RuntimeContext) error {
		ids := conversationSetTopIDs(rt)
		if len(ids) < 1 || len(ids) > 10 {
			return apperrors.NewValidation(fmt.Sprintf("会话 ID 去重后必须为 1-10 个,当前 %d 个", len(ids)))
		}
		return nil
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		ids := conversationSetTopIDs(rt)
		items := make([]shortcutBatchWrite, 0, len(ids))
		for _, id := range ids {
			items = append(items, shortcutBatchWrite{
				target: id,
				arguments: map[string]any{
					"openConversationId": id,
					"cid":                id,
					"top":                !rt.Bool("off"),
				},
			})
		}
		return executeShortcutBatchWrite(rt, "im", "set_top_conversation", items)
	},
}

ConversationSetTop sets/unsets a conversation top (set_top_conversation, im).

View Source
var FeedGroupQueryItem = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+feed-group-query-item",
	Product:     "im",
	Description: "在会话分组结果中按会话 ID 精确查询多项",
	Intent:      "当你已知一个钉钉会话分组 ID 和若干 openConversationId、想精确取回这些分组项时使用;先读取该分组,再按 ID 本地过滤并返回未找到清单。下层接口没有续页参数且未返回分页信号时,明确数组按单响应集合处理;若下层表明仍有后续页但未提供可执行游标,本命令会把缺失项标为 unresolved 并返回失败 ledger,不会误报 notFound。它不提供 Lark deleted_items 或服务端多 ID 查询语义。",
	Risk:        shortcut.RiskRead,
	Flags: []shortcut.Flag{
		{Name: "category-id", Type: shortcut.FlagInt, Aliases: []string{"feed-group-id"}, Desc: "钉钉会话分组 ID", Required: true},
		{Name: "conversation-ids", Type: shortcut.FlagStringSlice, Aliases: []string{"feed-id"}, Desc: "要精确查询的 openConversationId 列表(去重1–100项)", Required: true},
		{Name: "no-detail", Type: shortcut.FlagBool, Desc: "跳过精确命中会话的详情补查"},
		{Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "读取分组时排除已免打扰会话"},
	},
	Tips:    []string{`dws chat +feed-group-query-item --category-id <分组ID> --conversation-ids <openConversationId1>,<openConversationId2>`},
	Execute: executeExactFeedGroupQuery,
}

FeedGroupQueryItem is a client-side exact filter over a DingTalk conversation category. It does not claim Lark deleted-item or server-side multi-ID query semantics.

View Source
var FlagCancel = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+flag-cancel",
	Product:     "im",
	Description: "取消收藏一条或多条消息(最多 10 条)",
	Intent:      "当你要移除当前用户对同一会话中一条或多条消息的个人收藏标记时使用;逐项返回成功/失败 ledger,只影响 message favorite,不删除原消息,也不会修改 Pin 或会话置顶。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "message-id", Type: shortcut.FlagString, Desc: "单条消息 openMessageId;消息 ID 去重后必须为 1-10 条"},
		{Name: "message-ids", Type: shortcut.FlagStringSlice, Desc: "多条消息 openMessageId;消息 ID 去重后必须为 1-10 条"},
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "消息所在会话 openConversationId;省略从消息精确解析"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"message-id", "message-ids"}},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"message-id", "message-ids"},
			Description: "消息 ID 去重后必须为 1-10 条",
		},
	},
	Tips:     []string{`dws chat +flag-cancel --message-id <openMessageId> --conversation-id <openConversationId>`},
	Validate: validateFlagMessageIDs,
	Execute: func(rt *shortcut.RuntimeContext) error {
		return executeFlagBatch(rt, "remove_message_favorite")
	},
}
View Source
var FlagCreate = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+flag-create",
	Product:     "im",
	Description: "收藏一条或多条消息(最多 10 条)",
	Intent:      "当你要把同一会话中的一条或多条消息加入当前用户的个人收藏时使用;逐项返回成功/失败 ledger。这是消息 favorite,不是消息 Pin、会话置顶或 feed-layer thread flag。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "message-id", Type: shortcut.FlagString, Desc: "单条消息 openMessageId;消息 ID 去重后必须为 1-10 条"},
		{Name: "message-ids", Type: shortcut.FlagStringSlice, Desc: "多条消息 openMessageId;消息 ID 去重后必须为 1-10 条"},
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "消息所在会话 openConversationId;省略从消息精确解析"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"message-id", "message-ids"}},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"message-id", "message-ids"},
			Description: "消息 ID 去重后必须为 1-10 条",
		},
	},
	Tips:     []string{`dws chat +flag-create --message-id <openMessageId> --conversation-id <openConversationId>`},
	Validate: validateFlagMessageIDs,
	Execute: func(rt *shortcut.RuntimeContext) error {
		return executeFlagBatch(rt, "add_message_favorite")
	},
}

FlagCreate/FlagCancel/FlagList map Lark's message flag intent to DingTalk's message-favorite layer. They are explicitly not Pin or Feed Shortcut state.

View Source
var FlagList = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+flag-list",
	Product:     "im",
	Description: "分页查询当前用户收藏的消息,支持有界自动翻页",
	Intent:      "当你要查看当前用户的 DingTalk message favorite 列表时使用;默认读取一页,明确要求全部收藏时加 --page-all,并用 --page-limit 保持有界。底层实际使用数字 cursor,结果按 openMessageId 去重并公开 complete、hasMore、nextCursor、stopReason 和 failures;它不把 message favorite 与 Pin、会话置顶或 Lark feed-layer thread flag 混为一谈。",
	Risk:        shortcut.RiskRead,
	Flags: append([]shortcut.Flag{
		{Name: "no-enrich", Type: shortcut.FlagBool, Desc: "跳过收藏消息正文/资源补查"},
		{Name: "with-threads", Type: shortcut.FlagBool, Desc: "有界补查收藏消息Thread;每Thread10条,全查询最多50个Thread/500条回复;不可与no-enrich并用"},
		{Name: "no-reactions", Type: shortcut.FlagBool, Desc: "跳过收藏消息主动Reaction补查"},
		{Name: "page-size", Type: shortcut.FlagInt, Default: "20", Desc: "每页数量;下游真实上限为 30,显式页大小必须在 1-30 之间"},
		{Name: "size", Type: shortcut.FlagInt, Default: "20", Desc: "--page-size 的兼容别名;下游真实上限为 30,显式页大小必须在 1-30 之间"},
		{Name: "page-token", Type: shortcut.FlagString, Desc: "Lark 对齐的起始分页参数;起始 cursor 必须是非负整数"},
		{Name: "cursor", Type: shortcut.FlagInt, Default: "0", Desc: "钉钉数字分页游标;起始 cursor 必须是非负整数"},
		{Name: "page-all", Type: shortcut.FlagBool, Desc: "自动读取全部收藏分页;--page-limit 仅与 --page-all 一起使用且范围 1-500;--max-items/--page-delay 仅与 --page-all 一起使用;值必须大于等于 0"},
		{Name: "page-limit", Type: shortcut.FlagInt, Default: "20", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
	}, shortcut.AutoPageControlFlags()...),
	Constraints: append([]shortcut.Constraint{
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"with-threads", "no-enrich"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"page-size", "size"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"page-token", "cursor"}},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"page-size", "size"}, Description: "显式页大小必须在 1-30 之间"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"page-token", "cursor"}, Description: "起始 cursor 必须是非负整数"},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
	}, shortcut.AutoPageControlConstraints()...),
	Tips: []string{
		`dws chat +flag-list --cursor 0 --page-size 20`,
		`dws chat +flag-list --page-size 30 --page-all --page-limit 20`,
	},
	Validate: validateFlagList,
	Execute:  executeFlagList,
}
View Source
var MessagesAddEmoji = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-add-emoji",
	Product:     "im",
	Description: "对消息添加 emoji 表情回应",
	Intent:      "当你想给某条消息点一个 emoji 表情回应时使用;会实际添加表情回应,需传会话 openConversationId、消息 openMsgId 和表情名称。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMsgId", Required: true},
		{Name: "emoji", Type: shortcut.FlagString, Desc: "emoji 表情名称", Required: true},
	},
	Tips: []string{`dws chat +messages-add-emoji --conversation-id <openConversationId> --msg-id <openMsgId> --emoji "赞"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("add_emoji_reaction", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"openMsgId":          rt.Str("msg-id"),
			"emojiName":          rt.Str("emoji"),
		})
	},
}

MessagesAddEmoji adds an emoji reaction (add_emoji_reaction, im).

View Source
var MessagesAddTextEmotion = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-add-text-emotion",
	Product:     "im",
	Description: "对消息添加文字表情回应",
	Intent:      "当你想给某条消息添加自定义文字表情回应时使用;会实际添加文字表情,需传会话、消息 openMsgId 及由 create-text-emotion 得到的 emotionId 等参数。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMsgId", Required: true},
		{Name: "emotion-id", Type: shortcut.FlagString, Desc: "表情 ID(由 create-text-emotion 获取)", Required: true},
		{Name: "emotion-name", Type: shortcut.FlagString, Desc: "表情名称", Required: true},
		{Name: "text", Type: shortcut.FlagString, Desc: "文字内容", Required: true},
		{Name: "background-id", Type: shortcut.FlagString, Desc: "背景 ID", Required: true},
	},
	Tips: []string{`dws chat +messages-add-text-emotion --conversation-id <openConversationId> --msg-id <openMsgId> --emotion-id <id> --emotion-name "赞" --text "nice" --background-id im_bg_5`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("add_text_emotion", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"openMsgId":          rt.Str("msg-id"),
			"emotionId":          rt.Str("emotion-id"),
			"emotionName":        rt.Str("emotion-name"),
			"text":               rt.Str("text"),
			"backgroundId":       rt.Str("background-id"),
		})
	},
}

MessagesAddTextEmotion adds a text emotion reaction (add_text_emotion, im).

View Source
var MessagesBatchRecallByBot = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-batch-recall-by-bot",
	Product:     "bot",
	Description: "机器人撤回单聊消息",
	Intent:      "当你要批量撤回机器人此前发出的单聊消息时使用;会实际撤回机器人单聊消息,需传 robotCode 和发送时返回的 processQueryKey 列表。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code", Required: true},
		{Name: "keys", Type: shortcut.FlagStringSlice, Desc: "发送时返回的 processQueryKey 列表", Required: true},
	},
	Tips: []string{`dws chat +messages-batch-recall-by-bot --robot-code <robotCode> --keys key1,key2`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("batch_recall_robot_users_msg", map[string]any{
			"robotCode":        rt.Str("robot-code"),
			"processQueryKeys": rt.StrSlice("keys"),
		})
	},
}

MessagesBatchRecallByBot recalls robot single-chat messages (batch_recall_robot_users_msg, bot).

View Source
var MessagesBatchSendByBot = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-batch-send-by-bot",
	Product:     "bot",
	Description: "机器人批量向用户发送单聊 Markdown 消息",
	Intent:      "当你要用机器人给多个人分别发单聊 Markdown 消息(如批量提醒交周报)时使用;会实际批量发送单聊消息,需传 robotCode、接收人列表(userId 或 openDingTalkId)及标题正文。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code", Required: true},
		{Name: "title", Type: shortcut.FlagString, Desc: "消息标题", Required: true},
		{Name: "content", Type: shortcut.FlagString, Desc: "Markdown 正文", Required: true, Aliases: []string{"text"}},
		{Name: "users", Type: shortcut.FlagStringSlice, Desc: "接收人 userId 列表"},
		{Name: "open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "接收人 openDingTalkId 列表"},
		{Name: "at-all", Type: shortcut.FlagBool, Desc: "@ 所有人"},
	},
	Tips: []string{`dws chat +messages-batch-send-by-bot --robot-code <robotCode> --users userId1,userId2 --title "提醒" --content "请提交周报"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		if err := validateExplicitOpenIDs("--open-dingtalk-ids", rt.StrSlice("open-dingtalk-ids")); err != nil {
			return err
		}
		params := map[string]any{
			"robotCode": rt.Str("robot-code"),
			"title":     rt.Str("title"),
			"markdown":  rt.StrFirst("text", "content"),
		}
		if v := rt.StrSlice("users"); len(v) > 0 {
			params["userIds"] = v
		}
		if v := rt.StrSlice("open-dingtalk-ids"); len(v) > 0 {
			params["openDingtalkIds"] = v
		}
		if rt.Bool("at-all") {
			params["isAtAll"] = "true"
		}
		return rt.CallMCP("batch_send_robot_msg_to_users", params)
	},
}

MessagesBatchSendByBot sends single-chat messages via a robot to users (batch_send_robot_msg_to_users, bot).

View Source
var MessagesCombineForward = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-combine-forward",
	Product:     "im",
	Description: "合并转发多条消息",
	Intent:      "当你想把多条消息合并成一条转发到目标会话时使用;会实际合并转发,需传源会话、源消息 openMessageId 列表和目标会话 openConversationId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "src-conversation-id", Type: shortcut.FlagString, Desc: "源会话 openConversationId", Required: true},
		{Name: "msg-ids", Type: shortcut.FlagStringSlice, Desc: "源消息 openMessageId 列表", Required: true},
		{Name: "dest-conversation-id", Type: shortcut.FlagString, Desc: "目标会话 openConversationId", Required: true},
		{Name: "uuid", Type: shortcut.FlagString, Desc: "幂等键(可选)"},
	},
	Tips: []string{`dws chat +messages-combine-forward --src-conversation-id <srcCid> --msg-ids id1,id2 --dest-conversation-id <destCid>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{
			"srcOpenCid":        rt.Str("src-conversation-id"),
			"srcOpenMessageIds": rt.StrSlice("msg-ids"),
			"destOpenCid":       rt.Str("dest-conversation-id"),
		}
		if rt.Changed("uuid") {
			params["uuid"] = rt.Str("uuid")
		}
		return rt.CallMCP("combine_forward_messages", params)
	},
}

MessagesCombineForward merge-forwards multiple messages (combine_forward_messages, im).

View Source
var MessagesCreateTextEmotion = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-create-text-emotion",
	Product:     "im",
	Description: "创建文字表情(获取 emotionId)",
	Intent:      "当你要先创建一个文字表情模板(拿到 emotionId 供 add-text-emotion 使用)时使用;会实际创建文字表情,需传表情名称和文字内容。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "emotion-name", Type: shortcut.FlagString, Desc: "表情名称", Required: true},
		{Name: "text", Type: shortcut.FlagString, Desc: "文字内容", Required: true},
		{Name: "background-id", Type: shortcut.FlagString, Desc: "背景 ID(可选)"},
	},
	Tips: []string{`dws chat +messages-create-text-emotion --emotion-name "赞" --text "nice"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{
			"emotionName": rt.Str("emotion-name"),
			"text":        rt.Str("text"),
		}
		if rt.Changed("background-id") {
			params["backgroundId"] = rt.Str("background-id")
		}
		return rt.CallMCP("create_text_emotion", params)
	},
}

MessagesCreateTextEmotion creates a text emotion template (create_text_emotion, im).

View Source
var MessagesForward = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-forward",
	Product:     "im",
	Description: "转发单条消息",
	Intent:      "当你想把一条消息转发到另一个会话时使用;会实际转发消息,需传源会话 openConversationId、源消息 openMessageId 和目标会话 openConversationId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "src-conversation-id", Type: shortcut.FlagString, Desc: "源会话 openConversationId", Required: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "源消息 openMessageId", Required: true},
		{Name: "dest-conversation-id", Type: shortcut.FlagString, Desc: "目标会话 openConversationId", Required: true},
		{Name: "uuid", Type: shortcut.FlagString, Desc: "幂等键(可选)"},
	},
	Tips: []string{`dws chat +messages-forward --src-conversation-id <srcCid> --msg-id <msgId> --dest-conversation-id <destCid>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{
			"srcOpenCid":       rt.Str("src-conversation-id"),
			"srcOpenMessageId": rt.Str("msg-id"),
			"destOpenCid":      rt.Str("dest-conversation-id"),
		}
		if rt.Changed("uuid") {
			params["uuid"] = rt.Str("uuid")
		}
		return rt.CallMCP("forward_message", params)
	},
}

MessagesForward forwards one message (forward_message, im).

View Source
var MessagesForwardTopic = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-forward-topic",
	Product:     "im",
	Description: "转发话题消息到目标会话",
	Intent:      "当你要把某条群话题消息转发到目标会话时使用;会实际转发话题消息,需传源消息、源会话、话题 threadId 和目标会话 openConversationId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "src-msg-id", Type: shortcut.FlagString, Desc: "源消息 openMessageId", Required: true},
		{Name: "src-conversation-id", Type: shortcut.FlagString, Desc: "源会话 openConversationId", Required: true},
		{Name: "src-thread-id", Type: shortcut.FlagString, Desc: "话题 ID(convThread + 加密 convThreadId)", Required: true},
		{Name: "dest-conversation-id", Type: shortcut.FlagString, Desc: "目标会话 openConversationId", Required: true},
	},
	Tips: []string{`dws chat +messages-forward-topic --src-msg-id <msgId> --src-conversation-id <srcCid> --src-thread-id <threadId> --dest-conversation-id <destCid>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("forward_topic", map[string]any{
			"srcOpenMessageId":       rt.Str("src-msg-id"),
			"srcOpenConversationId":  rt.Str("src-conversation-id"),
			"srcOpenConvThreadId":    rt.Str("src-thread-id"),
			"destOpenConversationId": rt.Str("dest-conversation-id"),
		})
	},
}

MessagesForwardTopic forwards a topic message (forward_topic, im).

View Source
var MessagesList = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-list",
	Description: "拉取群聊会话消息",
	Intent:      "当你想按时间拉取某个群的历史聊天记录(做回顾或分析)时使用;只读,需传群 openConversationId 和起始时间,--forward 控制往新还是往旧方向翻。",
	Risk:        shortcut.RiskRead,
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId"},
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true},
		{Name: "id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true},
		{Name: "time", Type: shortcut.FlagString, Desc: "起始时间,如 \"2025-03-01 00:00:00\"", Required: true},
		{Name: "forward", Type: shortcut.FlagBool, Default: "true", Desc: "true=从给定时间往现在拉,false=往以前拉"},
		{Name: "limit", Type: shortcut.FlagInt, Desc: "每页返回数量;显式页大小必须大于 0"},
		{Name: "size", Type: shortcut.FlagInt, Desc: "--limit 的旧版别名;显式页大小必须大于 0", Hidden: true},
		{Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出消息 reaction(默认输出)"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id", "id"}},
	},
	Tips: []string{`dws chat +messages-list --group <openConversationId> --time "2025-03-01 00:00:00"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		group := rt.StrFirst("group", "conversation-id", "id")
		params := map[string]any{
			"openconversation_id": group,
			"time":                rt.Str("time"),
			"forward":             rt.Bool("forward"),
		}
		if limit := rt.IntFirst("limit", "size"); limit > 0 {
			params["limit"] = limit
		}
		data, err := rt.CallMCPData("chat", "list_conversation_message_v2", params)
		if err != nil {
			return err
		}
		rawMessages := listMessagesResolveMaps(data)
		decryptLedger := decryptMessageItemsIfRequested(rt, rawMessages)
		messages := projectMessageMapsWithReactions(rawMessages, !rt.Bool("no-reactions"))
		payload := map[string]any{"count": len(messages), "messages": messages}
		applyMessageDecryptLedger(payload, decryptLedger)
		direction := "older"
		if rt.Bool("forward") {
			direction = "newer"
		}
		chatmsg.ApplyMessagePagination(payload, data, listMessagesResolveMaps(data), direction)
		return rt.Output(payload)
	},
}

MessagesList pulls messages of a group (list_conversation_message_v2, chat server).

View Source
var MessagesListDirect = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-list-direct",
	Description: "拉取单聊会话消息",
	Intent:      "当你想按时间拉取与某人单聊的历史消息时使用;只读,需传对方 userId 或 openDingTalkId 及起始时间,--forward 控制翻页方向。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_list_direct",
			CanonicalPath:  "chat.shortcut_messages_list_direct",
			CLIPath:        "chat +messages-list-direct",
			PrimaryCLIPath: "chat +messages-list-direct",
		},
		Description: "拉取单聊会话消息",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "拉取单聊会话消息",
			UseWhen:      []string{"当你想按时间拉取与某人单聊的历史消息时使用;只读,需传对方 userId 或 openDingTalkId 及起始时间,--forward 控制翻页方向。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +messages-list-direct --user <userId> --time \"2025-03-01 00:00:00\""},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "user", Type: shortcut.FlagString, Desc: "对方 userId(与 --open-dingtalk-id 二选一)"},
		{Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "对方 openDingTalkId(与 --user 二选一)"},
		{Name: "time", Type: shortcut.FlagString, Desc: "起始时间,如 \"2025-03-01 00:00:00\"", Required: true},
		{Name: "forward", Type: shortcut.FlagBool, Default: "true", Desc: "true=往现在拉,false=往以前拉"},
		{Name: "limit", Type: shortcut.FlagInt, Desc: "每页返回数量;显式页大小必须大于 0"},
		{Name: "size", Type: shortcut.FlagInt, Desc: "--limit 的旧版别名;显式页大小必须大于 0", Hidden: true},
		{Name: "page-all", Type: shortcut.FlagBool, Desc: "沿毫秒级 nextCursor 自动读取全部单聊消息;--page-limit 仅与 --page-all 一起使用且范围 1-500"},
		{Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"},
		{Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出消息 reaction(默认输出)"},
	},
	Tips: []string{
		`dws chat +messages-list-direct --user <userId> --time "2025-03-01 00:00:00"`,
		`dws chat +messages-list-direct --open-dingtalk-id <openDingTalkId> --time "2025-03-01 00:00:00" --forward=false --page-all`,
	},
	Execute: executeMessagesListDirect,
}

MessagesListDirect pulls messages of a single chat (list_individual_chat_message, chat server).

View Source
var MessagesListPin = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-list-pin",
	Product:     "im",
	Description: "拉取会话中钉住的消息列表",
	Intent:      "当你想查看某会话里当前钉住的消息有哪些时使用;只读分页返回,需传会话 openConversationId。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_list_pin",
			CanonicalPath:  "chat.shortcut_messages_list_pin",
			CLIPath:        "chat +messages-list-pin",
			PrimaryCLIPath: "chat +messages-list-pin",
		},
		Description: "拉取会话中钉住的消息列表",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "拉取会话中钉住的消息列表",
			UseWhen:      []string{"当你想查看某会话里当前钉住的消息有哪些时使用;只读分页返回,需传会话 openConversationId。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +messages-list-pin --open-conversation-id <openConversationId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传 nextCursor"},
		{Name: "size", Type: shortcut.FlagInt, Desc: "一次拉取的消息数量(默认 20,最大 100)"},
	},
	Tips: []string{`dws chat +messages-list-pin --open-conversation-id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{
			"openConversationId": rt.Str("open-conversation-id"),
			"cid":                rt.Str("open-conversation-id"),
		}
		if rt.Changed("cursor") {
			params["cursor"] = rt.Str("cursor")
		}
		if rt.Int("size") > 0 {
			params["count"] = rt.Int("size")
		}
		data, err := rt.CallMCPData("im", "list_pin_messages", params)
		if err != nil {
			return err
		}
		pins := listPinProject(data)
		payload := map[string]any{"count": len(pins), "pins": pins}
		chatmsg.ApplyPagination(payload, data)
		return rt.Output(payload)
	},
}

MessagesListPin lists pinned messages (list_pin_messages, im).

View Source
var MessagesListUnreadConversations = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-list-unread-conversations",
	Description: "获取有未读消息的会话列表",
	Intent:      "当你想快速定位哪些会话还有未读消息时使用;只读返回有未读的会话列表,可用 --exclude-muted 排除已免打扰会话。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_list_unread_conversations",
			CanonicalPath:  "chat.shortcut_messages_list_unread_conversations",
			CLIPath:        "chat +messages-list-unread-conversations",
			PrimaryCLIPath: "chat +messages-list-unread-conversations",
		},
		Description: "获取有未读消息的会话列表",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "获取有未读消息的会话列表",
			UseWhen:      []string{"当你想快速定位哪些会话还有未读消息时使用;只读返回有未读的会话列表,可用 --exclude-muted 排除已免打扰会话。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +messages-list-unread-conversations --count 20"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "count", Type: shortcut.FlagInt, Desc: "返回的会话条数"},
		{Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "排除已免打扰会话"},
	},
	Tips: []string{`dws chat +messages-list-unread-conversations --count 20`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{}
		if rt.Int("count") > 0 {
			params["count"] = rt.Int("count")
		}
		if rt.Bool("exclude-muted") {
			params["excludeMuted"] = true
		}
		data, err := rt.CallMCPData("chat", "unread_message_conversation_list", params)
		if err != nil {
			return err
		}
		conversations := listUnreadConversationsProject(data)
		return rt.Output(map[string]any{"count": len(conversations), "conversations": conversations})
	},
}

MessagesListTopicReplies pulls topic replies (list_topic_replies, chat server). MessagesListAll pulls all messages in a time range (search_messages_by_time_range, chat server). MessagesListFocused pulls messages from specially-followed people (list_special_focus_messages, chat server). MessagesListUnreadConversations lists conversations with unread messages (unread_message_conversation_list, chat server).

View Source
var MessagesMget = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-mget",
	Product:     "im",
	Description: "根据消息 ID 批量查询消息(最多 50 条)",
	Intent:      "当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条;ID 参数错误会有界拆分隔离,其他错误停止拆分;默认主动补查 Reaction 和每个 Thread 最多 10 条回复,使用 complete、messagesComplete、enrichment 披露完整性。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger;本地下载路径受限于工作目录、默认不覆盖同名文件,按既有安全下载约定无需交互确认。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Parameters: []contract.ParamDecl{
			{Name: "msg-ids", Property: "msgIds"},
			{Name: "no-reactions", Property: "noReactions"},
			{Name: "no-threads", Property: "noThreads"},
		},
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_mget",
			CanonicalPath:  "chat.shortcut_messages_mget",
			CLIPath:        "chat +messages-mget",
			PrimaryCLIPath: "chat +messages-mget",
		},
		Description: "根据消息 ID 批量查询消息(最多 50 条)",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "根据消息 ID 批量查询消息(最多 50 条)",
			UseWhen:      []string{"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条;ID 参数错误会有界拆分隔离,其他错误停止拆分;默认主动补查 Reaction 和每个 Thread 最多 10 条回复,使用 complete、messagesComplete、enrichment 披露完整性。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger;本地下载路径受限于工作目录、默认不覆盖同名文件,按既有安全下载约定无需交互确认。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +messages-mget --msg-ids msgId1,msgId2"},
		},
	},
	Flags: append([]shortcut.Flag{
		{Name: "msg-ids", Type: shortcut.FlagStringSlice, Desc: "消息 openMsgId 列表,兼容 --message-ids / --message-id;--msg-ids 去重后必须包含 1-50 条消息 ID", Required: true, Aliases: []string{"message-ids", "message-id"}},
		{Name: "no-reactions", Type: shortcut.FlagBool, Desc: "跳过主动 Reaction 补查并不输出 Reaction(默认补查,每批最多 20 条)"},
		{Name: "no-threads", Type: shortcut.FlagBool, Desc: "跳过 Thread 回复补查(默认每个 Thread 最多 10 条、总计最多 500 条;需要完整回复请用 +thread-replies)"},
	}, MessageResourceDownloadFlags()...),
	Constraints: append([]shortcut.Constraint{
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"msg-ids"},
			Description: "--msg-ids 去重后必须包含 1-50 条消息 ID",
		},
	}, MessageResourceDownloadConstraints()...),
	Tips: []string{`dws chat +messages-mget --msg-ids msgId1,msgId2`},
	Validate: func(rt *shortcut.RuntimeContext) error {
		ids := mgetRequestedIDs(rt)
		if len(ids) < 1 || len(ids) > 50 {
			return fmt.Errorf("--msg-ids 去重后必须包含 1-50 条消息 ID,当前 %d 条", len(ids))
		}
		return ValidateMessageResourceDownload(rt)
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		ids := mgetRequestedIDs(rt)
		batch, err := readMessagesMget(rt, ids)
		if err != nil {
			return err
		}
		rawMessages := batch.messages
		var decryptLedger messageDecryptLedger
		enrichment := map[string]any{"complete": false, "enrichedCount": 0, "stopReason": "query_aborted"}
		threadViews := map[string]map[string]any{}
		resourceMessages := rawMessages
		if !batch.aborted {
			decryptLedger = decryptMessageItemsIfRequested(rt, rawMessages)
			enrichment, threadViews, resourceMessages = enrichMessagesMget(rt, rawMessages)
		}
		messages := projectMessageMapsWithReactions(rawMessages, !rt.Bool("no-reactions"))
		for i, raw := range rawMessages {
			if view, ok := threadViews[mgetID(chatmsg.MessageID(raw))]; ok {
				messages[i]["thread"] = view
			}
		}
		notFound := batch.missing
		failures := batch.failures
		payload := map[string]any{
			"contractVersion":    chatmsg.MessageListContractVersion,
			"requestedCount":     len(ids),
			"foundCount":         len(ids) - len(notFound),
			"notFoundCount":      len(notFound),
			"notFoundMessageIds": notFound,
			"messages":           messages,
			"complete":           len(notFound) == 0 && enrichment["complete"] == true,
			"messagesComplete":   len(notFound) == 0,
			"enrichment":         enrichment,
			"batchRequests":      batch.requests,
			"hasMore":            false,
			"nextCursor":         "",
			"paginationKnown":    true,
			"pagesFetched":       1,
			"enrichedCount":      enrichment["enrichedCount"],
			"failedCount":        len(failures),
			"failures":           failures,
		}
		applyMessageDecryptLedger(payload, decryptLedger)
		if len(decryptLedger.failures) > 0 {
			payload["complete"] = false
		}
		if rt.Bool("download-resources") && !batch.aborted {
			AttachMessageResourceDownloads(payload, DownloadMessageResources(rt, resourceMessages, ""))
		}
		return rt.Output(payload)
	},
}

MessagesMget batch-queries messages by id (list_messages_by_ids, im).

View Source
var MessagesQuerySendStatus = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-query-send-status",
	Aliases:     []string{"+messages-send-status"},
	Product:     "im",
	Description: "查询消息投递状态并衔接后续消息操作",
	Intent:      "当你发消息后拿到 openTaskId、想确认投递结果,或后续 edit/recall/read-status 需要取得 openMessageId 和 openConversationId 时使用;openTaskId 不是消息 ID。结果会保留下层响应,并追加版本化 messageRef 与结构化 nextActions。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_query_send_status",
			CanonicalPath:  "chat.shortcut_messages_query_send_status",
			CLIPath:        "chat +messages-query-send-status",
			PrimaryCLIPath: "chat +messages-query-send-status",
			Aliases:        []string{"chat +messages-send-status"},
		},
		Description: "查询消息投递状态并衔接后续消息操作",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "查询消息投递状态并衔接后续消息操作",
			UseWhen:      []string{"当你发消息后拿到 openTaskId、想确认投递结果,或后续 edit/recall/read-status 需要取得 openMessageId 和 openConversationId 时使用;openTaskId 不是消息 ID。结果会保留下层响应,并追加版本化 messageRef 与结构化 nextActions。"},
			AvoidWhen:    []string{"没有 openTaskId、已经有消息 ID,或只需查历史消息内容时不要使用"},
			Examples:     []string{"dws chat +messages-query-send-status --open-task-id <openTaskId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "open-task-id", Type: shortcut.FlagString, Desc: "发送消息时返回的 openTaskId", Required: true},
	},
	Tips: []string{`dws chat +messages-query-send-status --open-task-id <openTaskId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		taskID := rt.Str("open-task-id")
		data, err := rt.CallMCPData("im", "query_message_send_status", map[string]any{"openTaskId": taskID})
		if err != nil {
			return err
		}
		return rt.Output(chatmsg.ProjectMessageSendStatus(data, taskID))
	},
}

MessagesQuerySendStatus queries send status of a message (query_message_send_status, im).

View Source
var MessagesReadStatus = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-read-status",
	Aliases:     []string{"+message-read-users"},
	Product:     "im",
	Description: "查询消息的已读/未读状态",
	Intent:      "当你想知道自己发出的某条消息有哪些人已读/未读时使用;只读,按消息 ID 自动解析会话,显式会话必须与消息匹配,可指定目标成员列表。",
	Risk:        shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_read_status",
			CanonicalPath:  "chat.shortcut_messages_read_status",
			CLIPath:        "chat +messages-read-status",
			PrimaryCLIPath: "chat +messages-read-status",
			Aliases:        []string{"chat +message-read-users"},
		},
		Description: "查询消息的已读/未读状态",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "查询消息的已读/未读状态",
			UseWhen:      []string{"当你想知道自己发出的某条消息有哪些人已读/未读时使用;只读,按消息 ID 自动解析会话,显式会话必须与消息匹配,可指定目标成员列表。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +messages-read-status --conversation-id <openConversationId> --message-id <openMessageId>"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId"},
		{Name: "group", Type: shortcut.FlagString, Desc: "--conversation-id 的别名", Hidden: true},
		{Name: "id", Type: shortcut.FlagString, Desc: "--conversation-id 的别名", Hidden: true},
		{Name: "message-id", Type: shortcut.FlagString, Desc: "消息 openMessageId(当前用户发送的消息)", Required: true},
		{Name: "users", Type: shortcut.FlagStringSlice, Desc: "目标 userId 或 openDingTalkId 列表(不传返回全部接收者)"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"conversation-id", "group", "id"}},
	},
	Tips:    []string{`dws chat +messages-read-status --conversation-id <openConversationId> --message-id <openMessageId>`},
	Execute: executeOptimizedReadStatus,
}

MessagesReadStatus queries read/unread status of a message (query_msg_read_status, im).

View Source
var MessagesRecall = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-recall",
	Product:     "im",
	Description: "撤回当前用户发送的消息",
	Intent:      "当你想撤回当前用户刚发出的某条消息时使用;会实际撤回消息。推荐同时传会话 openConversationId 和消息 openMessageId;若只传一个消息 ID,CLI 会先只读查询消息详情并补齐会话 ID。兼容 --message-id/--message-ids 的单值写法。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId;省略时从消息详情解析"},
		{Name: "group", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
		{Name: "id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
		{Name: "chat", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMessageId;一次只能撤回一个消息 ID;--message-ids 仅接受单值"},
		{Name: "message-id", Type: shortcut.FlagString, Desc: "--msg-id 的兼容别名", Hidden: true},
		{Name: "message-ids", Type: shortcut.FlagStringSlice, Desc: "--msg-id 的兼容单值别名;不支持批量撤回", Hidden: true},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"msg-id", "message-id", "message-ids"}},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"msg-id"}, Description: "一次只能撤回一个消息 ID;--message-ids 仅接受单值"},
	},
	Tips: []string{`dws chat +messages-recall --conversation-id <openConversationId> --msg-id <openMessageId>`},
	Validate: func(rt *shortcut.RuntimeContext) error {
		messageIDs := uniqueShortcutStrings(append(
			[]string{rt.StrFirst("msg-id", "message-id")},
			rt.StrSlice("message-ids")...,
		))
		if len(messageIDs) != 1 {
			return apperrors.NewValidation("撤回一次只接受一个消息 ID;请通过 --msg-id 或单值 --message-ids 传入")
		}
		return nil
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		messageIDs := uniqueShortcutStrings(append(
			[]string{rt.StrFirst("msg-id", "message-id")},
			rt.StrSlice("message-ids")...,
		))
		messageID := messageIDs[0]
		conversationID := strings.TrimSpace(rt.StrFirst("conversation-id", "group", "id", "chat"))
		if conversationID == "" {
			data, err := rt.CallMCPData("im", "list_messages_by_ids", map[string]any{"openMsgIds": []string{messageID}})
			if err != nil {
				return err
			}
			messages := listMessagesResolveMaps(data)
			if len(messages) == 0 {
				return apperrors.NewValidation("无法根据消息 ID 查询到会话;请补充 --conversation-id")
			}
			conversationID = strings.TrimSpace(fmt.Sprint(chatmsg.ConversationID(messages[0])))
			if conversationID == "" || conversationID == "<nil>" {
				return apperrors.NewValidation("消息详情未返回会话 ID;请补充 --conversation-id")
			}
		}
		return rt.CallMCP("recall_message", map[string]any{
			"openConversationId": conversationID,
			"openMessageId":      messageID,
		})
	},
}

MessagesRecall recalls a message sent by the current user (recall_message, im).

View Source
var MessagesRecallByBot = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-recall-by-bot",
	Product:     "bot",
	Description: "机器人撤回群消息",
	Intent:      "当你要撤回机器人此前发到群里的消息时使用;会实际撤回机器人群消息,需传 robotCode、群 openConversationId 和发送时返回的 processQueryKey 列表。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code", Required: true},
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "keys", Type: shortcut.FlagStringSlice, Desc: "发送时返回的 processQueryKey 列表", Required: true},
	},
	Tips: []string{`dws chat +messages-recall-by-bot --robot-code <robotCode> --group <openConversationId> --keys key1,key2`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("recall_robot_group_message", map[string]any{
			"robotCode":          rt.Str("robot-code"),
			"openConversationId": rt.Str("group"),
			"processQueryKeys":   rt.StrSlice("keys"),
		})
	},
}

MessagesRecallByBot recalls a robot group message (recall_robot_group_message, bot).

View Source
var MessagesRemoveEmoji = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-remove-emoji",
	Product:     "im",
	Description: "移除消息的 emoji 表情回应",
	Intent:      "当你想取消此前给某条消息添加的 emoji 表情回应时使用;会实际移除表情回应,需传会话 openConversationId、消息 openMsgId 和表情名称。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMsgId", Required: true},
		{Name: "emoji", Type: shortcut.FlagString, Desc: "emoji 表情名称", Required: true},
	},
	Tips: []string{`dws chat +messages-remove-emoji --conversation-id <openConversationId> --msg-id <openMsgId> --emoji "赞"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("remove_emoji_reaction", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"openMsgId":          rt.Str("msg-id"),
			"emojiName":          rt.Str("emoji"),
		})
	},
}

MessagesRemoveEmoji removes an emoji reaction (remove_emoji_reaction, im).

View Source
var MessagesRemoveTextEmotion = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-remove-text-emotion",
	Product:     "im",
	Description: "移除消息的文字表情回应",
	Intent:      "当你想移除此前给某条消息添加的文字表情回应时使用;会实际移除文字表情,需传会话、消息 openMsgId 及对应的表情参数。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMsgId", Required: true},
		{Name: "emotion-id", Type: shortcut.FlagString, Desc: "表情 ID", Required: true},
		{Name: "emotion-name", Type: shortcut.FlagString, Desc: "表情名称", Required: true},
		{Name: "text", Type: shortcut.FlagString, Desc: "文字内容", Required: true},
		{Name: "background-id", Type: shortcut.FlagString, Desc: "背景 ID", Required: true},
	},
	Tips: []string{`dws chat +messages-remove-text-emotion --conversation-id <openConversationId> --msg-id <openMsgId> --emotion-id <id> --emotion-name "赞" --text "nice" --background-id im_bg_5`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("remove_text_emotion", map[string]any{
			"openConversationId": rt.Str("conversation-id"),
			"openMsgId":          rt.Str("msg-id"),
			"emotionId":          rt.Str("emotion-id"),
			"emotionName":        rt.Str("emotion-name"),
			"text":               rt.Str("text"),
			"backgroundId":       rt.Str("background-id"),
		})
	},
}

MessagesRemoveTextEmotion removes a text emotion reaction (remove_text_emotion, im).

View Source
var MessagesReply = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-reply",
	Product:     "chat",
	Description: "统一回复已有消息:个人群/单聊引用、个人 Thread 追加、Bot 群引用",
	Intent:      "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传原消息 ID,CLI 会只读定位会话和发送者;可显式传会话或 --ref-sender,所有模式均核对源消息身份。可用 --as bot 选择 Bot 群引用,--open-dingtalk-id 选择个人单聊,--reply-in-thread 追加到已核实的 Thread;所有模式先检查源消息 ID/会话归属;个人普通引用的 markdown/content 仍按纯文本解释,群聊可显式 @成员或 @所有人。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "as", Type: shortcut.FlagString, Default: "user", Enum: []string{"user", "bot"}, Aliases: []string{"identity"}, Desc: "回复身份,Bot 仅普通群文本/Markdown引用" + ";" + replyExtensionConstraint},
		{Name: "robot-code", Type: shortcut.FlagString, Desc: "Bot 回复所用 robotCode" + ";" + replyExtensionConstraint},
		{Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "个人单聊接收者;可省略 --group,自动读取源消息会话核实" + ";" + replyExtensionConstraint + ";" + replyTargetConstraint},
		{Name: "create-thread", Type: shortcut.FlagBool, Desc: "与reply-in-thread一起:仅源消息尚无Thread时先转换,再追加;转换后失败保留恢复ID"},
		{Name: "reply-in-thread", Type: shortcut.FlagBool, Desc: "直接追加到源消息所属 Thread,不生成引用消息;仅 user" + ";" + replyExtensionConstraint},
		{Name: "thread-id", Type: shortcut.FlagString, Desc: "可选 openConvThreadId,必须与源消息返回值一致;仅 reply-in-thread" + ";" + replyExtensionConstraint},

		{Name: "group", Type: shortcut.FlagString, Desc: "可选会话 openConversationId;" + replyTargetConstraint, Aliases: []string{"conversation-id"}},
		{Name: "ref-msg-id", Type: shortcut.FlagString, Desc: "被引用消息 openMessageId"},
		{Name: "message-id", Type: shortcut.FlagString, Desc: "--ref-msg-id 的 lark-cli 对齐别名"},
		{Name: "ref-sender", Type: shortcut.FlagString, Desc: "原消息发送者 openDingTalkId/userId(userId 通过通讯录搜索精确匹配;不传则自动读取)" + ";" + replyExtensionConstraint + ";" + replyTargetConstraint},
		{Name: "content", Type: shortcut.FlagString, Desc: "回复正文;普通引用中 content/markdown 均按纯文本解释,Bot群引用及已有Thread追加为Markdown;@仅群聊,可用 <@openDingTalkId> 定位提及", Required: true, Aliases: []string{"text", "markdown"}},
		{Name: "uuid", Type: shortcut.FlagString, Desc: "幂等键(可选)" + ";" + replyExtensionConstraint},
		{Name: "idempotency-key", Type: shortcut.FlagString, Desc: "--uuid 的 lark-cli 对齐别名" + ";" + replyExtensionConstraint},
		{Name: "at-open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "群回复中 @ 的 openDingTalkId 列表;自动补齐缺少的提及占位符;" + replyTargetConstraint},
		{Name: "at-all", Type: shortcut.FlagBool, Desc: "群回复中 @所有人;" + replyTargetConstraint},
		shortcut.AIMessageTagFlag(),
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintCustom, Flags: []string{"as", "robot-code", "open-dingtalk-id", "reply-in-thread", "thread-id", "ref-sender", "uuid", "idempotency-key"}, Description: replyExtensionConstraint},
		{Kind: shortcut.ConstraintCustom, Flags: []string{"group", "ref-sender", "open-dingtalk-id", "at-open-dingtalk-ids", "at-all"}, Description: replyTargetConstraint},
		{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"ref-msg-id", "message-id"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"uuid", "idempotency-key"}},
	},
	Tips:     []string{`dws chat +messages-reply --group <openConversationId> --message-id <openMessageId> --content "收到" --idempotency-key <key>`},
	Validate: validateReplyExtensions,
	Execute: func(rt *shortcut.RuntimeContext) error {
		target, err := resolveReplyTarget(rt)
		if err != nil {
			return err
		}
		if rt.StrFirst("identity", "as") == "bot" || rt.Bool("reply-in-thread") || rt.Str("open-dingtalk-id") != "" {
			return executeReplyExtensions(rt, target)
		}
		refSender := target.sender
		body := replyMentionBody(rt, true)
		content, _ := json.Marshal(map[string]string{
			"referenceOpenMessageId":   replyMessageID(rt),
			"srcMsgSendOpenDingTalkId": refSender,
			"replyMsgType":             "text",
			"content":                  body,
		})
		params := rt.AddAIMessageTag(map[string]any{
			"openConversationId": target.conversationID,
			"msgType":            "reply",
			"content":            string(content),
		})
		addReplyMentionParams(rt, params, false)
		if value := rt.StrFirst("idempotency-key", "uuid"); value != "" {
			params["uuid"] = value
		}
		if rt.DryRun() {
			return rt.Output(map[string]any{
				"contractVersion": "im.message-reply.v1",
				"dryRun":          true,
				"willSend":        false,
				"transport":       "chat/send_personal_message",
				"arguments":       params,
				"conversationId":  target.conversationID,
				"referencedMessage": map[string]any{
					"messageId":            replyMessageID(rt),
					"senderOpenDingTalkId": refSender,
				},
			})
		}
		data, err := rt.CallMCPWriteData("chat", "send_personal_message", params)
		if err != nil {
			return err
		}
		enrichReplyResult(data, rt, refSender)
		data["conversationId"] = target.conversationID
		return rt.Output(data)
	},
}
View Source
var MessagesResourceDownload = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-resource-download",
	Aliases:     []string{"+messages-resources-download"},
	Product:     "im",
	Description: "安全下载消息资源(图片/视频/语音/文件)到本地",
	Intent: "当你需要拿到消息里的实际图片、视频、语音或钉盘文件,而不只是资源 ID 时使用;" +
		"mediaId 用消息和会话身份换取下载地址,fileId 复用钉盘下载能力,再安全写入工作目录内的相对路径。" +
		"默认不覆盖已有文件,只有显式传 --overwrite 才覆盖;下载采用整文件临时落盘后原子发布,可选 --part-size 启用进程内分段恢复;不承诺跨进程断点续传。按既有安全本地下载约定无需交互确认。",
	Risk: shortcut.RiskRead,
	Flags: []shortcut.Flag{
		{Name: "type", Type: shortcut.FlagString, Default: "mediaId", Desc: "资源类型;媒体需message-id,可自动补会话;image/file从消息资源元数据解析ID类型;fileId可独立下载", Enum: []string{"mediaId", "fileId", "image", "file"}},
		{Name: "resource-id", Type: shortcut.FlagString, Aliases: []string{"file-key"}, Desc: "消息中的 mediaId 或 fileId", Required: true},
		{Name: "message-id", Type: shortcut.FlagString, Desc: "mediaId 所属消息的 openMessageId;媒体需message-id,可自动补会话;image/file从消息资源元数据解析ID类型;fileId可独立下载"},
		{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "mediaId 所属会话的 openConversationId;媒体需message-id,可自动补会话;image/file从消息资源元数据解析ID类型;fileId可独立下载"},
		{Name: "output", Type: shortcut.FlagString, Default: ".", Desc: "工作目录内的相对路径;不允许绝对路径或 .. 逃逸"},
		{Name: "part-size", Type: shortcut.FlagInt, Default: "0", Desc: "文件分段大小字节;0为整文件,启用范围4096–33554432,分段须有稳定ETag"},
		{Name: "retries", Type: shortcut.FlagInt, Default: "2", Desc: "分段下载瞬时故障最多重试次数(0–3);失败片段重新取URL且校验同一版本"},
		{Name: "retry-delay", Type: shortcut.FlagInt, Default: "200", Desc: "分段下载重试间隔毫秒(0–10000)"},
		{Name: "overwrite", Type: shortcut.FlagBool, Desc: "允许覆盖工作目录内已存在的目标文件(默认拒绝)"},
	},
	Constraints: []shortcut.Constraint{
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"output"},
			Description: "--output 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
		},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"type", "message-id", "open-conversation-id"},
			Description: "媒体需message-id,可自动补会话;image/file从消息资源元数据解析ID类型;fileId可独立下载",
		},
	},
	Tips: []string{
		`dws chat +messages-resource-download --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId>`,
		`dws chat +messages-resource-download --type fileId --resource-id <fileId> --output ./downloads/`,
		`dws chat +messages-resource-download --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/`,
	},
	Validate: func(rt *shortcut.RuntimeContext) error {
		if rt.Int("part-size") == 0 && (rt.Changed("retries") || rt.Changed("retry-delay")) {
			return apperrors.NewValidation("显式重试参数需要非零--part-size")
		}
		if n := rt.Int("part-size"); n != 0 && (n < 4096 || n > 33554432) {
			return apperrors.NewValidation("--part-size 必须为0或4096–33554432")
		}
		if n := rt.Int("retries"); n < 0 || n > 3 {
			return apperrors.NewValidation("--retries 必须为0–3")
		}
		if n := rt.Int("retry-delay"); n < 0 || n > 10000 {
			return apperrors.NewValidation("--retry-delay 必须为0–10000")
		}
		if err := validateResourceDownloadOutput(rt.Str("output")); err != nil {
			return err
		}
		if rt.Str("message-id") == "" && rt.Str("type") != "fileId" {
			return apperrors.NewValidation("媒体/内容类型下载需要 --message-id;会话可自动解析;独立文件使用--type fileId")
		}
		return nil
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		resourceType, messageID, conversationID, err := resolveDownloadIdentity(rt)
		if err != nil {
			return err
		}

		plan := map[string]any{
			"resourceType":       resourceType,
			"resourceId":         rt.StrFirst("resource-id", "file-key"),
			"messageId":          messageID,
			"openConversationId": conversationID,
			"output":             rt.Str("output"),
			"overwrite":          rt.Bool("overwrite"),
		}
		if rt.DryRun() {
			plan["dryRun"] = true
			plan["steps"] = []string{
				"resolve temporary download URL",
				"validate output path and no-clobber policy",
				"download to a temporary file",
				"atomically publish the completed file",
			}
			return rt.Output(plan)
		}

		data, err := resolveMessageResourceDownloadData(
			rt,
			resourceType,
			rt.StrFirst("resource-id", "file-key"),
			messageID,
			conversationID,
		)
		if err != nil {
			return err
		}
		resourceURL, headers, err := resourceDownloadInfo(data)
		if err != nil {
			return err
		}
		cwd, err := resourceGetwd()
		if err != nil {
			return apperrors.NewInternal(fmt.Sprintf("读取工作目录失败: %v", err))
		}
		requestedOutput := rt.Str("output")
		if filepath.Ext(requestedOutput) == "" && requestedOutput != "." && !strings.HasSuffix(requestedOutput, "/") {
			name := resourceDownloadPreferredName(data)
			if name == "" {
				if u, e := url.Parse(resourceURL); e == nil {
					name = pathpkg.Base(u.Path)
				}
			}
			if ext := filepath.Ext(name); ext != "" {
				if info, e := os.Stat(requestedOutput); e != nil || !info.IsDir() {
					requestedOutput += ext
				}
			}
		}
		destPath, relativePath, err := resolveResourceDownloadPath(
			cwd,
			requestedOutput,
			resourceURL,
			rt.Bool("overwrite"),
			resourceDownloadPreferredName(data),
		)
		if err != nil {
			return err
		}
		resolve := func() (string, map[string]string, error) {
			data, err := resolveMessageResourceDownloadData(rt, resourceType, rt.StrFirst("resource-id", "file-key"), messageID, conversationID)
			if err != nil {
				return "", nil, err
			}
			return resourceDownloadInfo(data)
		}
		size, err := downloadWithRecovery(rt.Command().Context(), nil, resourceURL, headers, destPath, rt.Bool("overwrite"), int64(rt.Int("part-size")), rt.Int("retries"), rt.Int("retry-delay"), resolve)

		if err != nil {
			return err
		}
		return rt.Output(map[string]any{
			"messageId":       messageID,
			"resourceId":      rt.StrFirst("resource-id", "file-key"),
			"resourceType":    resourceType,
			"localPath":       filepath.ToSlash(relativePath),
			"sizeBytes":       size,
			"messageVerified": messageID != "",
		})
	},
}

MessagesResourceDownload resolves a temporary IM resource URL and saves the bytes through a safe, atomic, no-clobber local-file workflow.

View Source
var MessagesResourceURL = shortcut.Shortcut{

	OutputRollout: output.RolloutDualValidate,
	Service:       "chat",
	Command:       "+messages-resource-url",
	Product:       "im",
	Description:   "获取消息资源(图片/视频/语音)下载链接",
	Intent:        "仅当调用方明确需要短时下载凭据、而不是资源文件本身时使用;返回地址及请求头可能包含签名,视为敏感信息,不复制到脚本、日志或最终答复。实际下载图片/视频/语音时使用 +messages-resource-download,由 CLI 在内部消费临时地址。",
	Risk:          shortcut.RiskRead,
	Safety: contract.SafetySpec{
		Effect: "read", Risk: "low",
		Confirmation: "not_required", Idempotency: "idempotent",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_resource_url",
			CanonicalPath:  "chat.shortcut_messages_resource_url",
			CLIPath:        "chat +messages-resource-url",
			PrimaryCLIPath: "chat +messages-resource-url",
		},
		Description: "获取短时有效的消息资源下载凭据",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       reviewedChatShortcutInterfaceReason,
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "获取短时有效的消息资源下载凭据",
			UseWhen:      []string{"调用方明确要求取得临时下载 URL 或请求头,并会按敏感凭据处理时"},
			AvoidWhen:    []string{"需要实际下载资源时使用 chat +messages-resource-download;不要把临时签名 URL 复制到 shell、脚本、日志或最终答复"},
			Examples:     []string{"dws chat +messages-resource-url --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId>"},
		},
		Result: messageResourceURLResult(),
	},
	Flags: []shortcut.Flag{
		{Name: "type", Type: shortcut.FlagString, Default: "mediaId", Desc: "资源类型", Enum: []string{"mediaId"}},
		{Name: "resource-id", Type: shortcut.FlagString, Desc: "资源 ID(消息中的 mediaId)", Required: true},
		{Name: "message-id", Type: shortcut.FlagString, Desc: "消息 openMessageId"},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "--message-id 的别名", Hidden: true},
		{Name: "open-message-id", Type: shortcut.FlagString, Desc: "--message-id 的别名", Hidden: true},
		{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
	},

	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"message-id", "msg-id", "open-message-id"}},
	},
	Tips: []string{`dws chat +messages-resource-url --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("get_resource_download_url", map[string]any{
			"resourceType":       rt.Str("type"),
			"resourceId":         rt.Str("resource-id"),
			"openMessageId":      rt.StrFirst("message-id", "msg-id", "open-message-id"),
			"openConversationId": rt.Str("open-conversation-id"),
		})
	},
}

MessagesResourceURL gets a message resource download url (get_resource_download_url, im).

View Source
var MessagesSend = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-send",
	Product:     "chat",
	Description: "按身份和目标统一发送消息,Bot 多群返回逐目标 ledger",
	Intent:      "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。Bot 可用 --groups/--groups-file 向最多 100 个稳定群 ID 发送文本或 Markdown,去重后返回 im.batch-write.v1 逐目标 ledger;webhook 目标由 token 所在群决定。user 另支持名片、群邀请和 A2UI 卡片创建;bot 支持 --image-url 图片及单目标 --file 上传,图片支持多目标 ledger;webhook 仍仅文本/Markdown。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_send",
			CanonicalPath:  "chat.shortcut_messages_send",
			CLIPath:        "chat +messages-send",
			PrimaryCLIPath: "chat +messages-send",
		},
		Description: "按身份和目标统一发送消息,Bot 多群返回逐目标 ledger",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports live-compatible contact search with exact userId matching, mediaId images, and the native init/upload/commit local-file flow.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "按身份和目标统一发送消息,Bot 多群返回逐目标 ledger",
			UseWhen:      []string{"当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。Bot 可用 --groups/--groups-file 向最多 100 个稳定群 ID 发送文本或 Markdown,去重后返回 im.batch-write.v1 逐目标 ledger;webhook 目标由 token 所在群决定。user 另支持名片、群邀请和 A2UI 卡片创建;bot 支持 --image-url 图片及单目标 --file 上传,图片支持多目标 ledger;webhook 仍仅文本/Markdown。"},
			AvoidWhen:    []string{"Bot 原生音视频、Webhook 富媒体及任意 Lark Card JSON 未接入;Thread 直接回复使用 +messages-reply --reply-in-thread,A2UI 创建可用 --a2ui-messages,更新仍用 chat message update-a2ui-card"},
			Examples: []string{
				"dws chat +messages-send --as user --chat-id <openConversationId> --markdown \"## 周报\" --idempotency-key <key>",
				"dws chat +messages-send --as user --user <userId> --msg-type file --file ./report.pdf",
			},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "identity", Type: shortcut.FlagString, Default: "user", Enum: []string{"user", "bot", "webhook"}, Desc: "发送身份;目标、凭据和幂等参数受发送身份能力矩阵约束"},
		{Name: "as", Type: shortcut.FlagString, Enum: []string{"user", "bot", "webhook"}, Desc: "--identity 的 lark-cli 对齐别名;受发送身份能力矩阵约束"},
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(user/bot 群聊);受发送身份能力矩阵约束"},
		{Name: "chat-id", Type: shortcut.FlagString, Desc: "--group 的 lark-cli 对齐别名;受发送身份能力矩阵约束"},
		{Name: "groups", Type: shortcut.FlagStringSlice, Desc: "多个群 openConversationId(仅 bot;受发送身份能力矩阵约束,逐群返回 typed ledger,最多 100 个)"},
		{Name: "groups-file", Type: shortcut.FlagString, Desc: "工作目录内相对文本文件(仅 bot;受发送身份能力矩阵约束),每行或逗号分隔一个群 openConversationId"},
		{Name: "chat-query", Type: shortcut.FlagString, Desc: "按群名解析唯一群聊(仅 user 的高级发送场景);受发送身份能力矩阵约束"},
		{Name: "user", Type: shortcut.FlagString, Desc: "单聊接收者 userId(user;包括 --dry-run 也会先通过通讯录搜索精确匹配 openDingTalkId);受发送身份能力矩阵约束"},
		{Name: "user-query", Type: shortcut.FlagString, Desc: "按姓名解析唯一 openDingTalkId(仅 user 的高级发送场景);受发送身份能力矩阵约束"},
		{Name: "open-dingtalk-id", Type: shortcut.FlagString, Aliases: []string{"user-id"}, Desc: "单聊接收者 openDingTalkId(user);受发送身份能力矩阵约束"},
		{Name: "users", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 userId(bot);受发送身份能力矩阵约束"},
		{Name: "open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 openDingTalkId(bot);受发送身份能力矩阵约束"},

		{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code(identity=bot 时使用);受发送身份能力矩阵约束"},
		{Name: "webhook-token", Type: shortcut.FlagString, Desc: "自定义机器人 Webhook token(identity=webhook 时使用);受发送身份能力矩阵约束"},
		{Name: "title", Type: shortcut.FlagString, Desc: "消息标题(不传则从正文生成)"},
		{Name: "text", Type: shortcut.FlagString, Desc: "纯文本正文"},
		{Name: "markdown", Type: shortcut.FlagString, Desc: "Markdown 正文"},
		{Name: "msg-type", Type: shortcut.FlagString, Enum: []string{"text", "markdown", "image", "file", "audio", "video", "profile", "share-chat", "a2ui"}, Desc: "内容类型;省略时根据正文、--media-id 或 --file 自动推断"},
		{Name: "media-id", Type: shortcut.FlagString, Desc: "已有图片 mediaId(仅 user 的 image)"},
		{Name: "file", Type: shortcut.FlagString, Desc: "工作目录内安全相对文件路径(user 的 file/audio/video;bot 仅单目标 file)"},
		{Name: "a2ui-messages", Type: shortcut.FlagString, Input: []string{"file", "stdin"}, Desc: "user 的 A2UI JSON 字符串数组;与正文/媒体互斥,不接受 Lark Card JSON"},
		{Name: "card-summary", Type: shortcut.FlagString, Desc: "A2UI 降级摘要;省略使用 A2UI 消息文本"},
		{Name: "biz-card-id", Type: shortcut.FlagString, Desc: "A2UI 业务标识,省略生成;不承诺消息幂等"},
		{Name: "request-id", Type: shortcut.FlagString, Desc: "A2UI 链路追踪标识,省略生成;不是幂等键"},
		{Name: "image-url", Type: shortcut.FlagString, Desc: "Bot 图片的 HTTP(S) URL(不接受已有 mediaId)"},
		{Name: "contact-id", Type: shortcut.FlagString, Desc: "user 发送名片的 openDingTalkId"},
		{Name: "share-chat-id", Type: shortcut.FlagString, Desc: "user 分享的源群 openConversationId;发送群邀请链接,不是 Lark share_chat 对象"},
		{Name: "expires-seconds", Type: shortcut.FlagInt, Default: "0", Desc: "群邀请有效期秒数,0 为永久;仅 share-chat 使用"},
		{Name: "file-path", Type: shortcut.FlagString, Desc: "--file 的兼容别名"},
		{Name: "uuid", Type: shortcut.FlagString, Desc: "幂等键(仅 user);受发送身份能力矩阵约束"},
		{Name: "idempotency-key", Type: shortcut.FlagString, Desc: "--uuid 的 lark-cli 对齐别名(仅 user);受发送身份能力矩阵约束"},
		{Name: "at-open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "@ 的 openDingTalkId(user/bot 群聊)"},
		{Name: "at-user-ids", Type: shortcut.FlagStringSlice, Desc: "@ 的 userId(bot/webhook)"},
		{Name: "at-mobiles", Type: shortcut.FlagStringSlice, Desc: "@ 的手机号(webhook)"},
		{Name: "at-all", Type: shortcut.FlagBool, Desc: "@所有人"},
		shortcut.AIMessageTagFlag(),
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"text", "markdown", "media-id", "file", "file-path", "image-url", "contact-id", "share-chat-id", "a2ui-messages"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"text", "markdown", "media-id", "file", "file-path", "image-url", "contact-id", "share-chat-id", "a2ui-messages"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"identity", "as"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"group", "chat-id"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"user", "open-dingtalk-id"}},
		{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"uuid", "idempotency-key"}},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"identity", "as", "group", "chat-id", "groups", "groups-file", "chat-query", "user", "user-query", "open-dingtalk-id", "users", "open-dingtalk-ids", "robot-code", "webhook-token", "uuid", "idempotency-key"},
			Description: "目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标,多群最多 100 个并逐项返回 ledger;webhook 必须指定 webhook-token;幂等键仅 user 支持",
		},
	},
	Tips: []string{
		`dws chat +messages-send --as user --chat-id <openConversationId> --markdown "## 周报" --idempotency-key <key>`,
		`dws chat +messages-send --as user --user <userId> --msg-type file --file ./report.pdf --idempotency-key <key>`,
		`dws chat +messages-send --as bot --robot-code <robotCode> --groups <openConversationId1>,<openConversationId2> --text "请提交周报"`,
	},
	Validate: validateMessagesSend,
	Execute:  executeMessagesSend,
}

MessagesSend is the identity-aware common sending entry point. The current user branch reuses the native message leaf's reviewed file-upload flow and existing-mediaId image path. Bot images and local files reuse the native robot transport; webhook remains limited to text/Markdown.

View Source
var MessagesSendByBot = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-send-by-bot",
	Product:     "bot",
	Description: "机器人向群聊发送 Markdown 消息",
	Intent:      "当你要用机器人向某群推送 Markdown 消息(如日报、告警播报)时使用;会实际以机器人身份发群消息,需传 robotCode、群 openConversationId、标题与正文,可 @人或 @所有人。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code", Required: true},
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
		{Name: "title", Type: shortcut.FlagString, Desc: "消息标题", Required: true},
		{Name: "content", Type: shortcut.FlagString, Desc: "Markdown 正文", Required: true, Aliases: []string{"text"}},
		{Name: "at-user-ids", Type: shortcut.FlagStringSlice, Desc: "@ 的 userId 列表"},
		{Name: "at-open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "@ 的 openDingTalkId 列表"},
		{Name: "at-all", Type: shortcut.FlagBool, Desc: "@ 所有人"},
	},
	Tips: []string{`dws chat +messages-send-by-bot --robot-code <robotCode> --group <openConversationId> --title "日报" --content "## 今日完成"`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		if err := validateExplicitOpenIDs("--at-open-dingtalk-ids", rt.StrSlice("at-open-dingtalk-ids")); err != nil {
			return err
		}
		params := map[string]any{
			"robotCode":          rt.Str("robot-code"),
			"openConversationId": rt.Str("group"),
			"title":              rt.Str("title"),
			"markdown":           rt.StrFirst("text", "content"),
		}
		if v := rt.StrSlice("at-user-ids"); len(v) > 0 {
			params["atUserIds"] = v
		}
		if v := rt.StrSlice("at-open-dingtalk-ids"); len(v) > 0 {
			params["atOpendingtalkIds"] = v
		}
		if rt.Bool("at-all") {
			params["isAtAll"] = "true"
		}
		return rt.CallMCP("send_robot_group_message", params)
	},
}

MessagesSend sends a text/markdown message as the current user (send_personal_message, chat server). Media/file variants are not covered. MessagesReply quote-replies a message (send_personal_message, chat server). MessagesSendByBot sends a group message via a robot (send_robot_group_message, bot).

View Source
var MessagesSendByWebhook = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-send-by-webhook",
	Product:     "bot",
	Description: "兼容旧入口的自定义机器人 Webhook 群消息发送",
	Intent:      "只有既有自动化明确依赖 +messages-send-by-webhook 兼容路径、暂时不能迁移统一身份入口时使用",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_send_by_webhook",
			CanonicalPath:  "chat.shortcut_messages_send_by_webhook",
			CLIPath:        "chat +messages-send-by-webhook",
			PrimaryCLIPath: "chat +messages-send-by-webhook",
		},
		Description: "兼容旧入口的自定义机器人 Webhook 群消息发送",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "兼容旧入口的自定义机器人 Webhook 群消息发送",
			UseWhen:      []string{"只有既有自动化明确依赖 +messages-send-by-webhook 兼容路径、暂时不能迁移统一身份入口时使用"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			Examples:     []string{"dws chat +messages-send-by-webhook --token <token> --title \"告警\" --content \"CPU 超 90%\" --at-all"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "token", Type: shortcut.FlagString, Desc: "Webhook token", Required: true},
		{Name: "title", Type: shortcut.FlagString, Desc: "消息标题", Required: true},
		{Name: "content", Type: shortcut.FlagString, Desc: "消息正文", Required: true, Aliases: []string{"text"}},
		{Name: "at-all", Type: shortcut.FlagBool, Desc: "@ 所有人"},
		{Name: "at-mobiles", Type: shortcut.FlagStringSlice, Desc: "@ 的手机号列表"},
		{Name: "at-users", Type: shortcut.FlagStringSlice, Desc: "@ 的 userId 列表"},
	},
	Tips: []string{`dws chat +messages-send-by-webhook --token <token> --title "告警" --content "CPU 超 90%" --at-all`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		params := map[string]any{
			"robotToken": rt.Str("token"),
			"title":      rt.Str("title"),
			"text":       rt.StrFirst("text", "content"),
		}
		if rt.Bool("at-all") {
			params["isAtAll"] = true
		}
		if v := rt.StrSlice("at-mobiles"); len(v) > 0 {
			params["atMobiles"] = v
		}
		if v := rt.StrSlice("at-users"); len(v) > 0 {
			params["atUserIds"] = v
		}
		return rt.CallMCP("send_message_by_custom_robot", params)
	},
}

MessagesSendByWebhook sends via a custom robot webhook (send_message_by_custom_robot, bot).

View Source
var MessagesSendCard = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-send-card",
	Product:     "im",
	Description: "创建流式卡片,可在同一次调用中写入内容并结束;群聊创建时可 @成员或 @所有人",
	Intent:      "当你要发送一张流式文本卡片时使用;群 openConversationId、单聊 userId、单聊 openDingTalkId 严格三选一,分别使用 --group、--receiver、--receiver-open-dingtalk-id。群聊可用 --at-open-dingtalk-ids 或 --at-all 在创建卡片时设置 @对象;同时传 --content 时,Runtime 会把创建响应中的 atTag 自动加在正文前,后续更新不重复传递 @参数。--receiver 始终按 userId 通过通讯录关键词搜索做精确匹配,即使值以 D/d 开头也不会猜成 openDingTalkId;已有 openDingTalkId 时必须用显式参数直传。userId 包括在 --dry-run 时也会先解析。只传目标时创建卡片并返回 bizId,供后续 messages-update-card 流式更新;同时传 --content 时会自动串联创建和更新,默认以 flowStatus=3 完成。当前只支持 streaming text,不支持 Card JSON 组件或 action callback。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_send_card",
			CanonicalPath:  "chat.shortcut_messages_send_card",
			CLIPath:        "chat +messages-send-card",
			PrimaryCLIPath: "chat +messages-send-card",
		},
		Description: "创建流式卡片,可在同一次调用中写入内容并结束;群聊创建时可 @成员或 @所有人",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed card lifecycle adapter: it can resolve a userId through contact search with exact matching, call create_and_send_card alone, or compose creation with update_streaming_card after extracting the returned bizId and atTag.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "创建流式卡片,可在同一次调用中写入内容并结束;群聊创建时可 @成员或 @所有人",
			UseWhen:      []string{"当你要发送一张流式文本卡片时使用;群 openConversationId、单聊 userId、单聊 openDingTalkId 严格三选一,分别使用 --group、--receiver、--receiver-open-dingtalk-id。群聊可用 --at-open-dingtalk-ids 或 --at-all 在创建卡片时设置 @对象;同时传 --content 时,Runtime 会把创建响应中的 atTag 自动加在正文前,后续更新不重复传递 @参数。--receiver 始终按 userId 通过通讯录关键词搜索做精确匹配,即使值以 D/d 开头也不会猜成 openDingTalkId;已有 openDingTalkId 时必须用显式参数直传。userId 包括在 --dry-run 时也会先解析。只传目标时创建卡片并返回 bizId,供后续 messages-update-card 流式更新;同时传 --content 时会自动串联创建和更新,默认以 flowStatus=3 完成。当前只支持 streaming text,不支持 Card JSON 组件或 action callback。"},
			AvoidWhen:    []string{"已有 bizId、只需要追加或更新现有卡片内容时使用 +messages-update-card"},
			Examples: []string{
				"dws chat +messages-send-card --group <openConversationId> --at-open-dingtalk-ids <openDingTalkId> --content \"任务已完成\"",
				"dws chat +messages-send-card --receiver <userId>",
			},
		},
		Parameters: []contract.ParamDecl{
			{Name: "receiver-open-dingtalk-id", Property: "receiverOpenDingTalkId"},
			{Name: "at-open-dingtalk-ids", Property: "atOpenDingTalkIds"},
			{Name: "at-all", Property: "atAll"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(与两个单聊接收者参数互斥);艾特参数仅支持群聊 --group"},
		{Name: "receiver", Type: shortcut.FlagString, Desc: "单聊接收者 userId(与 --group/--receiver-open-dingtalk-id 互斥);始终通过通讯录搜索精确匹配 openDingTalkId,包括 --dry-run 和 D/d 开头的 userId"},
		{Name: "receiver-open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊接收者 openDingTalkId(与 --group/--receiver 互斥);显式直传且不做通讯录解析"},
		{Name: "at-open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "群聊创建卡片时 @ 的 openDingTalkId 列表;仅随 create_and_send_card 发送;艾特参数仅支持群聊 --group"},
		{Name: "at-all", Type: shortcut.FlagBool, Desc: "群聊创建卡片时 @ 所有人;仅随 create_and_send_card 发送;艾特参数仅支持群聊 --group"},
		{Name: "content", Type: shortcut.FlagString, Desc: "创建后立即写入的卡片正文;群聊 @ 时 Runtime 自动前置 create 返回的 atTag;省略时仅创建并返回 bizId"},
		{Name: "flow-status", Type: shortcut.FlagInt, Default: "3", Desc: "自动更新状态:1处理中/2输入中/3完成/4执行中/5错误;--flow-status 必须在 1-5 之间,且显式指定时必须同时提供 --content"},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "receiver", "receiver-open-dingtalk-id"}},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"flow-status"},
			Description: "--flow-status 必须在 1-5 之间,且显式指定时必须同时提供 --content",
		},
		{
			Kind:        shortcut.ConstraintCustom,
			Flags:       []string{"group", "at-open-dingtalk-ids", "at-all"},
			Description: "艾特参数仅支持群聊 --group",
		},
	},
	Tips: []string{
		`dws chat +messages-send-card --group <openConversationId>`,
		`dws chat +messages-send-card --group <openConversationId> --at-open-dingtalk-ids <openDingTalkId> --content "任务已完成"`,
	},
	Validate: func(rt *shortcut.RuntimeContext) error {
		if receiverOpenID := rt.Str("receiver-open-dingtalk-id"); receiverOpenID != "" {
			if err := targetresolver.ValidateExplicitOpenDingTalkID("--receiver-open-dingtalk-id", receiverOpenID); err != nil {
				return err
			}
		}
		if err := validateExplicitOpenIDs("--at-open-dingtalk-ids", rt.StrSlice("at-open-dingtalk-ids")); err != nil {
			return err
		}
		if status := rt.Int("flow-status"); !validCardFlowStatus(status) {
			return fmt.Errorf("--flow-status 必须在 1-5 之间")
		}
		if rt.Changed("flow-status") && rt.Str("content") == "" {
			return fmt.Errorf("--flow-status 只有与 --content 一起使用才有意义")
		}
		if rt.Str("group") == "" && (len(uniqueShortcutStrings(rt.StrSlice("at-open-dingtalk-ids"))) > 0 || rt.Bool("at-all")) {
			return fmt.Errorf("--at-open-dingtalk-ids 和 --at-all 仅支持群聊 --group")
		}
		return nil
	},
	Execute: func(rt *shortcut.RuntimeContext) error {
		group := rt.Str("group")
		receiver := rt.Str("receiver")
		receiverOpenID := rt.Str("receiver-open-dingtalk-id")
		params := map[string]any{}
		mentionsRequested := false
		switch {
		case group != "":
			params["openConversationId"] = group
			if atOpenIDs := uniqueShortcutStrings(rt.StrSlice("at-open-dingtalk-ids")); len(atOpenIDs) > 0 {
				params["atOpenDingTalkIds"] = atOpenIDs
				mentionsRequested = true
			}
			if rt.Bool("at-all") {
				params["atAll"] = true
				mentionsRequested = true
			}
		case receiver != "":
			openID, err := resolveUserOpenDingTalkID(rt, receiver)
			if err != nil {
				return err
			}
			params["receiverOpenDingTalkId"] = openID
		default:
			params["receiverOpenDingTalkId"] = receiverOpenID
		}
		content := rt.Str("content")
		if content == "" {
			if rt.DryRun() {
				return rt.Output(map[string]any{
					"contractVersion": chatmsg.StreamingCardContractVersion,
					"dry_run":         true,
					"executed":        false,
					"preview_kind":    "plan",
					"actionCount":     1,
					"actions": []map[string]any{{
						"tool":      "create_and_send_card",
						"arguments": params,
					}},
				})
			}
			created, err := rt.CallMCPWriteData("im", "create_and_send_card", params)
			if err != nil {
				return err
			}
			bizID := findCardBizID(created)
			if bizID == "" {
				return cardCreateMissingBizIDError(created)
			}
			return rt.Output(chatmsg.ProjectStreamingCardReceipt(created, bizID))
		}
		status := rt.Int("flow-status")
		if rt.DryRun() {
			plannedContent := content
			if mentionsRequested {
				plannedContent = "<atTag from create_and_send_card>" + content
			}
			return rt.Output(map[string]any{
				"contractVersion": currentCardWorkflowContract.Version,
				"dry_run":         true,
				"executed":        false,
				"preview_kind":    "plan",
				"actionCount":     2,
				"failedCount":     0,
				"actions": []map[string]any{
					{
						"tool":      "create_and_send_card",
						"arguments": params,
					},
					{
						"tool": "update_streaming_card",
						"arguments": map[string]any{
							"bizId":      "<from create_and_send_card>",
							"msgContent": plannedContent,
							"flowStatus": status,
						},
					},
				},
			})
		}
		created, err := rt.CallMCPWriteData("im", "create_and_send_card", params)
		if err != nil {
			return err
		}
		bizID := findCardBizID(created)
		if bizID == "" {
			return fmt.Errorf("卡片已创建但下层未返回 bizId,无法自动更新;请检查 create_and_send_card 响应")
		}
		atTag := findCardAtTag(created)
		if mentionsRequested && atTag == "" {
			return fmt.Errorf("卡片已创建(bizId=%s),但下层未返回 atTag,无法保证请求的 @ 生效;未执行自动更新", bizID)
		}
		updated, err := rt.CallMCPWriteData("im", "update_streaming_card", map[string]any{
			"bizId":      bizID,
			"msgContent": atTag + content,
			"flowStatus": status,
		})
		if err != nil {
			return fmt.Errorf("卡片已创建(bizId=%s),但自动更新失败: %w", bizID, err)
		}
		verification, err := chatmsg.VerifyStreamingCardUpdate(bizID, updated)
		if err != nil {
			return fmt.Errorf("卡片已创建(bizId=%s),但自动更新结果不可信: %w", bizID, cardUpdateVerificationError(bizID, err))
		}
		payload := chatmsg.ProjectStreamingCardReceipt(created, bizID)
		payload["bizId"] = bizID
		payload["flowStatus"] = status
		payload["updated"] = updated
		payload["updateAccepted"] = verification.Accepted
		payload["updateVerified"] = verification.Verified
		payload["updateVerificationEvidence"] = verification.Evidence
		if verification.Accepted && !verification.Verified {
			payload["updateWarning"] = "服务端已接受卡片更新请求,但未返回可独立证明可见内容已更新的字段;不要重复执行相同更新"
		}
		return rt.Output(payload)
	},
}

MessagesSendCard creates and optionally completes a streaming card by composing create_and_send_card with update_streaming_card.

View Source
var MessagesSetPin = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-set-pin",
	Product:     "im",
	Description: "钉住消息(Pin)",
	Intent:      "当你想把某条消息钉在会话中(Pin)以便成员随时查看时使用;会实际钉住消息,需传会话 openConversationId 和消息 openMessageId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMessageId", Required: true},
	},
	Tips: []string{`dws chat +messages-set-pin --open-conversation-id <openConversationId> --msg-id <openMessageId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("set_pin_message", map[string]any{
			"openConversationId": rt.Str("open-conversation-id"),
			"cid":                rt.Str("open-conversation-id"),
			"openMessageId":      rt.Str("msg-id"),
		})
	},
}

MessagesSetPin pins a message (set_pin_message, im).

View Source
var MessagesSetTop = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-set-top",
	Product:     "im",
	Description: "置顶消息",
	Intent:      "当你想把某条消息置顶到会话顶部时使用;会实际置顶消息,需传会话 openConversationId 和消息 openMessageId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMessageId", Required: true},
	},
	Tips: []string{`dws chat +messages-set-top --open-conversation-id <openConversationId> --msg-id <openMessageId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("set_top_message", map[string]any{
			"openConversationId": rt.Str("open-conversation-id"),
			"openMessageId":      rt.Str("msg-id"),
		})
	},
}

MessagesSetTop pins a message to the top (set_top_message, im).

View Source
var MessagesUnsetPin = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-unset-pin",
	Product:     "im",
	Description: "取消钉住消息(Unpin)",
	Intent:      "当你想取消此前钉住的消息时使用;会实际取消 Pin,需传会话 openConversationId 和消息 openMessageId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMessageId", Required: true},
	},
	Tips: []string{`dws chat +messages-unset-pin --open-conversation-id <openConversationId> --msg-id <openMessageId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("unset_pin_message", map[string]any{
			"openConversationId": rt.Str("open-conversation-id"),
			"cid":                rt.Str("open-conversation-id"),
			"openMessageId":      rt.Str("msg-id"),
		})
	},
}

MessagesUnsetPin unpins a message (unset_pin_message, im).

View Source
var MessagesUnsetTop = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-unset-top",
	Product:     "im",
	Description: "取消置顶消息",
	Intent:      "当你想取消此前置顶的消息时使用;会实际取消置顶,需传会话 openConversationId 和消息 openMessageId。",
	Risk:        shortcut.RiskWrite,
	Flags: []shortcut.Flag{
		{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
		{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMessageId", Required: true},
	},
	Tips: []string{`dws chat +messages-unset-top --open-conversation-id <openConversationId> --msg-id <openMessageId>`},
	Execute: func(rt *shortcut.RuntimeContext) error {
		return rt.CallMCP("unset_top_message", map[string]any{
			"openConversationId": rt.Str("open-conversation-id"),
			"openMessageId":      rt.Str("msg-id"),
		})
	},
}

MessagesUnsetTop cancels a message top (unset_top_message, im).

View Source
var MessagesUpdateCard = shortcut.Shortcut{
	Service:     "chat",
	Command:     "+messages-update-card",
	Product:     "im",
	Description: "流式更新卡片内容(最后一次 --flow-status 应为 3)",
	Intent:      "当你要向已发送的流式文本卡片持续追加/更新内容时使用;会实际更新卡片,需传 send-card 返回的 bizId、新内容及 flowStatus 1-5(最后一次应为 3 表示完成)。当前不支持 Card JSON 组件或 action callback。",
	Risk:        shortcut.RiskWrite,
	Safety: contract.SafetySpec{
		Effect: "write", Risk: "medium",
		Confirmation: "user_required", Idempotency: "unknown",
	},
	Contract: corecmd.ContractDecl{
		Identity: contract.ToolIdentitySpec{
			ProductID:      "chat",
			Name:           "shortcut_messages_update_card",
			CanonicalPath:  "chat.shortcut_messages_update_card",
			CLIPath:        "chat +messages-update-card",
			PrimaryCLIPath: "chat +messages-update-card",
		},
		Description: "流式更新卡片内容(最后一次 --flow-status 应为 3)",
		Interface: &contract.InterfaceSpec{
			Mode:         "composite",
			Availability: "available",
			Reason:       "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
		},
		Selection: contract.SelectionSpec{
			AgentSummary: "流式更新卡片内容(最后一次 --flow-status 应为 3)",
			UseWhen:      []string{"当你要向已发送的流式文本卡片持续追加/更新内容时使用;会实际更新卡片,需传 send-card 返回的 bizId、新内容及 flowStatus 1-5(最后一次应为 3 表示完成)。当前不支持 Card JSON 组件或 action callback。"},
			AvoidWhen:    []string{"需要底层原始响应、未公开参数,或由调用方自行管理确认与更新节奏时,改用 chat message update-card"},
			Examples:     []string{"dws chat +messages-update-card --biz-id <bizId> --content \"内容\" --flow-status 3"},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "biz-id", Type: shortcut.FlagString, Desc: "send-card 返回的卡片业务 ID", Required: true},
		{Name: "content", Type: shortcut.FlagString, Desc: "卡片消息内容", Required: true},
		{Name: "flow-status", Type: shortcut.FlagInt, Desc: "流式状态 1处理中/2输入中/3完成/4执行中/5错误;--flow-status 必须在 1-5 之间", Required: true},
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintCustom, Flags: []string{"flow-status"}, Description: "--flow-status 必须在 1-5 之间"},
	},
	Tips: []string{`dws chat +messages-update-card --biz-id <bizId> --content "内容" --flow-status 3`},
	Validate: func(rt *shortcut.RuntimeContext) error {
		if _, err := chatmsg.NormalizeCardBizID(rt.Str("biz-id")); err != nil {
			return err
		}
		if !validCardFlowStatus(rt.Int("flow-status")) {
			return fmt.Errorf("--flow-status 必须在 1-5 之间")
		}
		return nil
	},
	Execute: func(rt *shortcut.RuntimeContext) error {

		bizID, _ := chatmsg.NormalizeCardBizID(rt.Str("biz-id"))
		params := map[string]any{
			"bizId":      bizID,
			"msgContent": rt.Str("content"),
			"flowStatus": rt.Int("flow-status"),
		}
		if rt.DryRun() {
			return rt.Output(map[string]any{
				"dry_run":  true,
				"executed": false,
				"verified": false,
				"action": map[string]any{
					"product":   "im",
					"tool":      "update_streaming_card",
					"arguments": params,
				},
			})
		}
		updated, err := rt.CallMCPWriteData("im", "update_streaming_card", params)
		if err != nil {
			return err
		}
		verification, err := chatmsg.VerifyStreamingCardUpdate(bizID, updated)
		if err != nil {
			return cardUpdateVerificationError(bizID, err)
		}
		return rt.Output(chatmsg.ProjectStreamingCardUpdate(updated, bizID, verification))
	},
}

MessagesUpdateCard streams updated card content (update_streaming_card, im).

Functions

func AttachMessageResourceDownloads added in v1.0.57

func AttachMessageResourceDownloads(payload, ledger map[string]any)

AttachMessageResourceDownloads publishes the download ledger and folds any resource failure into the task-level completeness contract without dropping successfully read messages or downloaded files.

func DownloadMessageResources added in v1.0.56

func DownloadMessageResources(
	rt *shortcut.RuntimeContext,
	messages []map[string]any,
	fallbackConversationID string,
) map[string]any

DownloadMessageResources downloads every unique message resource reference and returns a per-resource success/failure ledger. A fallback conversation ID lets group/thread list commands supply context when a mediaId item's lower response omits it; fileId resources route through the existing drive leaf.

func DownloadMessageResourcesWithCause added in v1.0.62

func DownloadMessageResourcesWithCause(
	rt *shortcut.RuntimeContext,
	messages []map[string]any,
	fallbackConversationID string,
) (map[string]any, error)

DownloadMessageResourcesWithCause returns the public ledger together with the first concrete failure that produced it. The cause stays out of business data and is used only by the typed incomplete-result wrapper.

func EnrichMessageDetails added in v1.0.62

func EnrichMessageDetails(rt *shortcut.RuntimeContext, messages []map[string]any) (map[string]any, map[string]map[string]any, []map[string]any)

EnrichMessageDetails reuses the exact-read bounded Thread/Reaction pipeline.

func EnrichMessageReactions added in v1.0.62

func EnrichMessageReactions(rt *shortcut.RuntimeContext, all []map[string]any) (requests int, enriched map[string]bool, failures []map[string]any)

EnrichMessageReactions is shared by exact reads, history, searches and Thread readers. It mutates only known messages and never turns a missing row into an empty reaction set.

func ExecuteResolvedUserMarkdown added in v1.0.57

func ExecuteResolvedUserMarkdown(
	rt *shortcut.RuntimeContext,
	target ResolvedUserMessageTarget,
	text string,
) error

ExecuteResolvedUserMarkdown lets narrow semantic shortcuts such as +dm and +send-to-group reuse the same target/content/AI-tag parameter builder while preserving their existing raw lower-response output contract.

func MessageResourceDownloadConstraints added in v1.0.56

func MessageResourceDownloadConstraints() []shortcut.Constraint

MessageResourceDownloadConstraints publishes the shared safe-output rule.

func MessageResourceDownloadFlags added in v1.0.56

func MessageResourceDownloadFlags() []shortcut.Flag

MessageResourceDownloadFlags returns the common opt-in resource workflow used by message list, search, mget, @me and thread-reading Shortcuts.

func OutputCheckedRead added in v1.0.62

func OutputCheckedRead(rt *shortcut.RuntimeContext, payload map[string]any) error

OutputCheckedRead preserves the command's business payload while making an unproven/partial result nonzero. Explicit continuation remains in the payload.

func ReadExactMessage added in v1.0.62

func ReadExactMessage(rt *shortcut.RuntimeContext, conversationID, messageID string) (map[string]any, error)

func StrictChatCollection added in v1.0.62

func StrictChatCollection(data map[string]any, keys ...string) ([]map[string]any, error)

StrictChatCollection accepts explicit collections only. An unrecognised successful response is not evidence of an empty business result.

func ValidateMessageExportOutput added in v1.0.57

func ValidateMessageExportOutput(output string) error

ValidateMessageExportOutput applies the same workspace-relative and no-symlink boundary used by message resource downloads. The export target is always a file; directory-shaped paths are rejected instead of inventing a name.

func ValidateMessageResourceDownload added in v1.0.56

func ValidateMessageResourceDownload(rt *shortcut.RuntimeContext) error

ValidateMessageResourceDownload validates the output path only when the caller opts into local writes.

func WriteMessageExportJSON added in v1.0.57

func WriteMessageExportJSON(output string, overwrite bool, payload any) (relativePath string, size int, err error)

WriteMessageExportJSON atomically publishes the exact structured message ledger. It defaults to no-clobber and never follows a symlink outside the current working directory.

Types

type CardFlowStatus added in v1.0.57

type CardFlowStatus struct {
	Value int
	Name  string
}

CardFlowStatus is one reviewed state accepted by DingTalk's streaming-card update transport.

type CardWorkflowContract added in v1.0.57

type CardWorkflowContract struct {
	Version           string
	Targets           []string
	ContentTypes      []string
	FlowStatuses      []CardFlowStatus
	CallbackSupported bool
}

CardWorkflowContract describes the real card subset exposed by the current Runtime. It intentionally does not claim Lark card JSON/component compilation or callback consumption, neither of which exists in the lower interface.

func CurrentCardWorkflowContract added in v1.0.57

func CurrentCardWorkflowContract() CardWorkflowContract

CurrentCardWorkflowContract returns a defensive copy for policy and docs.

type IMCapabilityBoundary added in v1.0.57

type IMCapabilityBoundary struct {
	Capability  string
	Supported   bool
	Alternative string
}

IMCapabilityBoundary makes unsupported Lark-parity requests explicit and testable instead of leaving them to prose or transport guessing.

func CurrentIMCapabilityBoundaries added in v1.0.57

func CurrentIMCapabilityBoundaries() []IMCapabilityBoundary

CurrentIMCapabilityBoundaries returns the reviewed positive/negative matrix.

type MessageIdentityCapability added in v1.0.57

type MessageIdentityCapability struct {
	Identity        string
	Targets         []string
	ContentTypes    []string
	NaturalTargets  []string
	MentionTargets  []string
	IdempotencyKeys bool
	BatchLedger     bool
}

MessageIdentityCapability is the reviewed Runtime capability descriptor for +messages-send. Validation and Skill drift checks consume this same typed source; it describes only behavior that the current lower transports expose.

func MessageIdentityCapabilities added in v1.0.57

func MessageIdentityCapabilities() []MessageIdentityCapability

MessageIdentityCapabilities returns defensive copies of the public matrix.

type ResolvedUserMessageTarget added in v1.0.57

type ResolvedUserMessageTarget struct {
	GroupID        string
	OpenDingTalkID string
}

ResolvedUserMessageTarget is the stable target accepted by the shared user send engine after natural-name resolution has completed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL