Files
dingtalk-workspace-cli/internal/helpers/chat.go
T

10087 lines
446 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helpers
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/targetresolver"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
func callProjectedChatMessages(cmd *cobra.Command, toolName string, args map[string]any, search bool) error {
if deps.Caller.DryRun() {
return callMCPToolOnServer("chat", toolName, args)
}
text, err := callMCPToolReturnTextOnServer(cmd.Context(), "chat", toolName, args)
if err != nil {
var cliErr *CLIError
if errors.As(err, &cliErr) && cliErr.Operation == "" {
withOperation := *cliErr
withOperation.Operation = "chat/" + toolName
return &withOperation
}
return err
}
if strings.TrimSpace(text) == "" {
return apperrors.NewAPI("MCP read tool returned no non-empty text content",
apperrors.WithOperation("chat/"+toolName),
apperrors.WithOrigin("mcp"),
apperrors.WithFailureStage("response_validation"),
apperrors.WithRetryable(true),
apperrors.WithReason("empty_tool_response"),
)
}
data := map[string]any{}
if err := unmarshalJSONUseNumber(text, &data); err != nil {
deps.Out.PrintRaw(text)
return nil
}
return writeCommandPayload(cmd, projectChatMessagesPayload(data, search))
}
func projectChatMessagesPayload(data map[string]any, search bool) map[string]any {
items := chatmsg.ListMessageItems(data)
if search {
items = chatmsg.SearchItems(data)
}
messages := make([]map[string]any, 0, len(items))
for _, item := range items {
projected := make(map[string]any, len(item)+8)
for key, value := range item {
projected[key] = value
}
for key, value := range chatmsg.ProjectMessageV1(item, true) {
if key == "messageId" || key == "text" {
projected[key] = value
continue
}
if _, exists := projected[key]; !exists {
projected[key] = value
}
}
if value, exists := item["openMessageId"]; exists {
projected["openMessageId"] = value
} else if value, exists := projected["messageId"]; exists {
projected["openMessageId"] = value
}
if value, exists := item["content"]; exists {
projected["content"] = value
} else if value, exists := projected["text"]; exists {
projected["content"] = value
}
messages = append(messages, projected)
}
payload := make(map[string]any, len(data)+1)
for key, value := range data {
payload[key] = value
}
payload["messages"] = messages
return payload
}
func resolveMessageForward(cmd *cobra.Command, defaultForward bool) (bool, error) {
forwardStr, _ := cmd.Flags().GetString("forward")
forward := forwardStr != "false"
if !cmd.Flags().Changed("direction") {
if !cmd.Flags().Changed("forward") {
return defaultForward, nil
}
return forward, nil
}
direction, _ := cmd.Flags().GetString("direction")
switch strings.TrimSpace(strings.ToLower(direction)) {
case "newer":
if cmd.Flags().Changed("forward") && !forward {
return false, fmt.Errorf("--direction newer conflicts with --forward=false")
}
return true, nil
case "older":
if cmd.Flags().Changed("forward") && forward {
return false, fmt.Errorf("--direction older conflicts with --forward=true")
}
return false, nil
case "":
return defaultForward, nil
default:
return false, fmt.Errorf("--direction must be newer or older")
}
}
func chatCompatibilityHintSubCmd(use, hint string) *cobra.Command {
command := hintSubCmd(use, hint)
// Legacy callers may still pass the old command's flags. Let the migration
// command consume them so Cobra reaches RunE and returns the replacement path.
command.DisableFlagParsing = true
return command
}
type nativeChatTargetReader struct{}
func (nativeChatTargetReader) CallMCPData(product, tool string, params map[string]any) (map[string]any, error) {
text, err := CallMCPReadToolTextOnServer(product, tool, params)
if err != nil {
return nil, err
}
if strings.TrimSpace(text) == "" {
return map[string]any{}, nil
}
var data map[string]any
if err := json.Unmarshal([]byte(text), &data); err != nil {
return nil, apperrors.NewInternal(fmt.Sprintf("解析 %s 返回失败: %v", tool, err))
}
return data, nil
}
func resolveNativeChatTarget(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if targetresolver.LooksLikeOpenConversationID(raw) {
return raw, nil
}
resolved, err := targetresolver.ResolveChat(nativeChatTargetReader{}, raw)
if err != nil {
return "", err
}
return resolved.Selected.OpenConversationID, nil
}
const (
maxConversationCategoryTitleRunes = 15
chatFavoritesMaxPageSize = 30
)
func validatedConversationCategoryTitle(raw string) (string, error) {
title := strings.TrimSpace(raw)
if title == "" {
return "", apperrors.NewValidation("--title 不能为空")
}
if utf8.RuneCountInString(title) > maxConversationCategoryTitleRunes {
return "", apperrors.NewValidation(fmt.Sprintf(
"--title 最多 %d 个字符", maxConversationCategoryTitleRunes))
}
return title, nil
}
func chatIntFlagOrFallback(cmd *cobra.Command, primary string, aliases ...string) int {
for _, alias := range aliases {
if f := cmd.Flags().Lookup(alias); f != nil && f.Changed {
v, _ := cmd.Flags().GetInt(alias)
return v
}
}
v, _ := cmd.Flags().GetInt(primary)
return v
}
const maxConversationScopedSearchPages = 40
func runConversationScopedPagedMessageSearch(
cmd *cobra.Command,
cfg PagedMCPCommandConfig,
scopeParam string,
conversationIDs []string,
) error {
conversationIDs = uniqueNonEmptyStrings(conversationIDs)
if len(conversationIDs) == 0 {
return RunPagedMCPCommand(cmd, cfg)
}
toolArgs, err := cfg.BuildArgs(cmd)
if err != nil {
return err
}
return runConversationScopedMessageSearchWithProjector(
cmd,
cfg.ServerID,
cfg.ToolName,
scopeParam,
toolArgs,
conversationIDs,
cfg.ProjectResult,
)
}
func runConversationScopedMessageSearch(
cmd *cobra.Command,
serverID, toolName, scopeParam string,
toolArgs map[string]any,
conversationIDs []string,
) error {
return runConversationScopedMessageSearchWithProjector(
cmd,
serverID,
toolName,
scopeParam,
toolArgs,
conversationIDs,
nil,
)
}
func runConversationScopedMessageSearchWithProjector(
cmd *cobra.Command,
serverID, toolName, scopeParam string,
toolArgs map[string]any,
conversationIDs []string,
projectResult func(map[string]any) map[string]any,
) error {
conversationIDs = uniqueNonEmptyStrings(conversationIDs)
if len(conversationIDs) == 0 {
return callMCPToolOnServer(serverID, toolName, toolArgs)
}
opts, err := readPagedCommandOptions(cmd)
if err != nil {
return err
}
if commandDryRun(cmd) {
return writeConversationScopedSearchPreview(cmd, serverID, toolName, scopeParam, toolArgs, conversationIDs, opts)
}
if err := validateNativeSearchConversationScope(conversationIDs); err != nil {
return err
}
// The downstream search currently treats invalid CID filters as absent and
// does not reliably return group-scoped hits. Keep every other filter, scan
// the global result stream with a hard page bound, and apply the validated
// conversation set locally.
scanArgs := cloneStringAnyMap(toolArgs)
delete(scanArgs, scopeParam)
pageSize := positiveSearchLimit(scanArgs["limit"], 100)
resultLimit := pageSize
pageLimit := maxConversationScopedSearchPages
if opts.pageAll {
resultLimit = opts.maxItems
pageLimit = opts.pageLimit
}
cursor := cleanSearchCursor(scanArgs["cursor"])
capacity := resultLimit
if capacity <= 0 {
capacity = pageSize
}
messages := make([]map[string]any, 0, capacity)
seenMessageIDs := map[string]struct{}{}
pagesFetched := 0
hasMore := false
truncatedWithinPage := false
var nextCursor any
for pagesFetched < pageLimit && (resultLimit == 0 || len(messages) < resultLimit) {
requestLimit := pageSize
if !opts.pageAll {
requestLimit = resultLimit - len(messages)
}
scanArgs["limit"] = requestLimit
scanArgs["cursor"] = cursor
text, err := CallMCPToolTextOnServer(serverID, toolName, scanArgs)
if err != nil {
return err
}
var data map[string]any
if err := unmarshalJSONUseNumber(text, &data); err != nil {
return apperrors.NewInternal(
fmt.Sprintf("解析 %s 返回失败: %v", toolName, err),
apperrors.WithReason("search_response_invalid"),
)
}
pagesFetched++
pageMessages := chatmsg.SearchItems(data)
pageMessages, unverifiableMessageIDs := chatmsg.FilterConversationScope(pageMessages, conversationIDs)
if len(unverifiableMessageIDs) > 0 {
return nativeSearchScopeUnverifiedError(conversationIDs, unverifiableMessageIDs)
}
for index, message := range pageMessages {
messageID := strings.TrimSpace(fmt.Sprint(chatmsg.MessageID(message)))
if messageID != "" && messageID != "<nil>" {
if _, exists := seenMessageIDs[messageID]; exists {
continue
}
seenMessageIDs[messageID] = struct{}{}
}
messages = append(messages, message)
if resultLimit > 0 && len(messages) == resultLimit {
truncatedWithinPage = index < len(pageMessages)-1
break
}
}
page := chatmsg.Pagination(data)
hasMoreValue, paginationKnown := page["hasMore"].(bool)
if !paginationKnown {
return apperrors.NewAPI(
"搜索服务未返回可靠的 hasMore,无法安全完成会话范围过滤",
apperrors.WithReason("search_conversation_scope_pagination_unknown"),
apperrors.WithRetryable(false),
)
}
hasMore = hasMoreValue || truncatedWithinPage
nextCursor = page["nextCursor"]
if !hasMore {
break
}
if truncatedWithinPage {
break
}
next := cleanSearchCursor(nextCursor)
if next == "" || next == cursor {
return apperrors.NewAPI(
"搜索服务声称仍有更多结果,但 nextCursor 缺失或未前进",
apperrors.WithReason("search_conversation_scope_cursor_stalled"),
apperrors.WithRetryable(false),
)
}
if resultLimit > 0 && len(messages) >= resultLimit {
break
}
cursor = next
if opts.pageAll && opts.delayMS > 0 {
if err := sleepPagedCommandDelay(cmd.Context(), time.Duration(opts.delayMS)*time.Millisecond); err != nil {
return err
}
}
}
result := map[string]any{
"conversationMessagesList": chatmsg.GroupSearchMessages(messages),
"hasMore": hasMore,
"complete": !hasMore,
"pagesFetched": pagesFetched,
}
if hasMore && nextCursor != nil && cleanSearchCursor(nextCursor) != "" {
result["nextCursor"] = nextCursor
}
payload := map[string]any{
"result": result,
"scope": map[string]any{
"requestedConversationIds": append([]string(nil), conversationIDs...),
"targetsValidated": true,
"filterApplied": true,
"filterMode": "client",
"resultsWithinScope": true,
"sourceComplete": !hasMore,
},
}
if opts.pageAll {
paging := map[string]any{
"truncated": hasMore,
"hasMore": hasMore,
"lastCursor": nextCursor,
"pages": pagesFetched,
"total": len(messages),
}
if truncatedWithinPage {
paging["truncatedWithinPage"] = true
paging["resumeCursorReliable"] = false
}
payload["paging"] = paging
}
if projectResult != nil {
payload = projectResult(payload)
}
return writeCommandPayload(cmd, payload)
}
func writeConversationScopedSearchPreview(
cmd *cobra.Command,
serverID, toolName, scopeParam string,
toolArgs map[string]any,
conversationIDs []string,
opts pagedCommandOptions,
) error {
plan := make([]map[string]any, 0, len(conversationIDs)+2)
for _, conversationID := range conversationIDs {
plan = append(plan, map[string]any{
"stage": "validate-conversation",
"product": "chat",
"tool": "get_conversation_info",
"arguments": map[string]any{
"openConversationId": conversationID,
},
})
}
scanArgs := cloneStringAnyMap(toolArgs)
delete(scanArgs, scopeParam)
pageLimit := maxConversationScopedSearchPages
maxItems := positiveSearchLimit(scanArgs["limit"], 100)
pageDelay := 0
if opts.pageAll {
pageLimit = opts.pageLimit
maxItems = opts.maxItems
pageDelay = opts.delayMS
}
plan = append(plan,
map[string]any{
"stage": "search-global",
"product": serverID,
"tool": toolName,
"arguments": scanArgs,
"pageAll": opts.pageAll,
"pageLimit": pageLimit,
"maxItems": maxItems,
"pageDelay": pageDelay,
},
map[string]any{
"stage": "filter-conversation-scope",
"requestedConversationIds": append([]string(nil), conversationIDs...),
"failClosed": true,
},
)
return writeCommandPayload(cmd, map[string]any{
"dry_run": true,
"executed": false,
"plan": plan,
})
}
func validateNativeSearchConversationScope(conversationIDs []string) error {
for _, conversationID := range conversationIDs {
_, err := CallMCPToolTextOnServer("chat", "get_conversation_info", map[string]any{
"openConversationId": conversationID,
})
if err == nil {
continue
}
return NormalizeSearchConversationScopeError(conversationID, err)
}
return nil
}
func chatMessageListAllArgs(cmd *cobra.Command) (map[string]any, error) {
startRaw, endRaw, err := defaultChatMessageListAllTimeRange(cmd, 24*time.Hour)
if err != nil {
return nil, err
}
startMs, err := parseISOTimeToMillis("start", startRaw)
if err != nil {
return nil, err
}
endMs, err := parseISOTimeToMillis("end", endRaw)
if err != nil {
return nil, err
}
if err := validateTimeRange(startMs, endMs); err != nil {
return nil, err
}
cursor, _ := cmd.Flags().GetString("cursor")
return map[string]any{
"startTime": startRaw,
"endTime": endRaw,
"limit": chatIntFlagOrFallback(cmd, "limit", "size"),
"cursor": cursor,
}, nil
}
func defaultChatMessageListAllTimeRange(cmd *cobra.Command, lookback time.Duration) (string, string, error) {
startRaw := mustGetFlag(cmd, "start")
endRaw := mustGetFlag(cmd, "end")
anchor := time.Now()
if endRaw == "" {
endRaw = formatChatMessageListAllTime(anchor.UnixMilli())
} else if startRaw == "" {
endMs, err := parseISOTimeToMillis("end", endRaw)
if err != nil {
return "", "", err
}
anchor = time.UnixMilli(endMs)
}
if startRaw == "" {
startRaw = formatChatMessageListAllTime(anchor.Add(-lookback).UnixMilli())
}
return startRaw, endRaw, nil
}
func chatMessageListBySenderArgs(cmd *cobra.Command) (map[string]any, error) {
senderUserID := flagOrFallback(cmd, "sender-user-id", "sender")
senderOpenDingTalkID, _ := cmd.Flags().GetString("sender-open-dingtalk-id")
if senderUserID != "" && senderOpenDingTalkID != "" {
return nil, fmt.Errorf("--sender-user-id and --sender-open-dingtalk-id are mutually exclusive, specify exactly one")
}
if senderUserID == "" && senderOpenDingTalkID == "" {
return nil, fmt.Errorf("--sender-user-id or --sender-open-dingtalk-id is required")
}
if senderOpenDingTalkID != "" {
if err := targetresolver.ValidateExplicitOpenDingTalkID("--sender-open-dingtalk-id", senderOpenDingTalkID); err != nil {
return nil, err
}
}
startRaw, endRaw, err := defaultChatMessageTimeRange(cmd, 7*24*time.Hour)
if err != nil {
return nil, err
}
startMs, err := parseISOTimeToMillis("start", startRaw)
if err != nil {
return nil, err
}
endMs, err := parseISOTimeToMillis("end", endRaw)
if err != nil {
return nil, err
}
if err := validateTimeRange(startMs, endMs); err != nil {
return nil, err
}
cursor, _ := cmd.Flags().GetString("cursor")
toolArgs := map[string]any{
"startTime": startMs,
"endTime": endMs,
"limit": chatIntFlagOrFallback(cmd, "limit", "size"),
"cursor": cursor,
}
if senderUserID != "" {
toolArgs["senderUserId"] = senderUserID
} else {
toolArgs["senderOpenDingTalkId"] = senderOpenDingTalkID
}
return toolArgs, nil
}
func chatMessageListMentionsArgs(cmd *cobra.Command) (map[string]any, error) {
startRaw, endRaw, err := defaultChatMessageTimeRange(cmd, 7*24*time.Hour)
if err != nil {
return nil, err
}
startMs, err := parseISOTimeToMillis("start", startRaw)
if err != nil {
return nil, err
}
endMs, err := parseISOTimeToMillis("end", endRaw)
if err != nil {
return nil, err
}
if err := validateTimeRange(startMs, endMs); err != nil {
return nil, err
}
cursor, _ := cmd.Flags().GetString("cursor")
toolArgs := map[string]any{
"startTime": startMs,
"endTime": endMs,
"limit": chatIntFlagOrFallback(cmd, "limit", "size"),
"cursor": cursor,
}
if groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat"); groupID != "" {
toolArgs["openConversationId"] = groupID
}
return toolArgs, nil
}
func chatMessageListFocusedArgs(cmd *cobra.Command) (map[string]any, error) {
toolArgs := map[string]any{}
if v, err := cmd.Flags().GetInt("limit"); err == nil && v > 0 {
toolArgs["limit"] = v
}
if v, _ := cmd.Flags().GetInt64("cursor"); v > 0 {
toolArgs["cursor"] = v
}
return toolArgs, nil
}
func chatMessageSearchArgs(cmd *cobra.Command) (map[string]any, error) {
if err := validateRequiredFlagWithAliases(cmd, "query", "keyword"); err != nil {
return nil, err
}
startRaw, endRaw, err := defaultChatMessageTimeRange(cmd, 7*24*time.Hour)
if err != nil {
return nil, err
}
startMs, err := parseISOTimeToMillis("start", startRaw)
if err != nil {
return nil, err
}
endMs, err := parseISOTimeToMillis("end", endRaw)
if err != nil {
return nil, err
}
if err := validateTimeRange(startMs, endMs); err != nil {
return nil, err
}
cursor, _ := cmd.Flags().GetString("cursor")
toolArgs := map[string]any{
"keyword": flagOrFallback(cmd, "query", "keyword"),
"startTime": startMs,
"endTime": endMs,
"limit": chatIntFlagOrFallback(cmd, "limit", "size"),
"cursor": cursor,
}
if groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat"); groupID != "" {
toolArgs["openConversationId"] = groupID
}
return toolArgs, nil
}
func defaultChatMessageTimeRange(cmd *cobra.Command, lookback time.Duration) (string, string, error) {
startRaw := mustGetFlag(cmd, "start")
endRaw := mustGetFlag(cmd, "end")
anchor := time.Now()
if endRaw == "" {
endRaw = anchor.Format(time.RFC3339)
} else if startRaw == "" {
endMs, err := parseISOTimeToMillis("end", endRaw)
if err != nil {
return "", "", err
}
anchor = time.UnixMilli(endMs)
}
if startRaw == "" {
startRaw = anchor.Add(-lookback).Format(time.RFC3339)
}
return startRaw, endRaw, nil
}
func formatChatMessageListAllTime(ms int64) string {
return time.UnixMilli(ms).In(shanghaiLocation()).Format("2006-01-02 15:04:05")
}
func defaultChatMessageListTime() string {
return time.Now().In(shanghaiLocation()).Format("2006-01-02 15:04:05")
}
func chatMessageSearchAdvancedArgs(cmd *cobra.Command) (map[string]any, error) {
toolArgs := map[string]any{}
if v := flagOrFallback(cmd, "query", "keyword"); v != "" {
toolArgs["keyword"] = v
}
if v := flagOrFallback(cmd, "users", "user", "userId"); v != "" {
appendChatIDArgs(toolArgs, parseCSVValues(v), "senderUserIds", "senderOpenDingTakIds")
}
if v := flagOrFallback(cmd, "sender-ids", "senders", "sender"); v != "" {
appendChatIDArgs(toolArgs, parseCSVValues(v), "senderUserIds", "senderOpenDingTakIds")
}
if v, _ := cmd.Flags().GetBool("at-me"); v {
toolArgs["atMe"] = true
}
if v, _ := cmd.Flags().GetString("at-ids"); v != "" {
appendChatIDArgs(toolArgs, parseCSVValues(v), "atUserIds", "atOpenDingTakIds")
}
appendChatConversationIDs(cmd, toolArgs)
if err := appendChatAdvancedFilters(cmd, toolArgs); err != nil {
return nil, err
}
return toolArgs, nil
}
func appendChatConversationIDs(cmd *cobra.Command, toolArgs map[string]any) {
convIds := flagOrFallback(cmd, "conversation-ids", "groups", "group")
if convIds == "" {
return
}
var ids []string
for _, s := range strings.Split(convIds, ",") {
if t := strings.TrimSpace(s); t != "" {
ids = append(ids, t)
}
}
if len(ids) > 0 {
toolArgs["openConversationIds"] = ids
}
}
func appendChatAdvancedFilters(cmd *cobra.Command, toolArgs map[string]any) error {
if v, _ := cmd.Flags().GetString("message-type"); v != "" {
toolArgs["messageType"] = v
}
if cmd.Flags().Changed("only-robot") {
toolArgs["onlyRobotMessages"], _ = cmd.Flags().GetBool("only-robot")
} else if cmd.Flags().Changed("only-robot-messages") {
toolArgs["onlyRobotMessages"], _ = cmd.Flags().GetBool("only-robot-messages")
}
if v := flagOrFallback(cmd, "conversation-type", "search-conv-type"); v != "" {
toolArgs["searchConvType"] = v
}
if v, _ := cmd.Flags().GetString("start"); v != "" {
ms, err := parseISOTimeToMillis("start", v)
if err != nil {
return err
}
toolArgs["startTime"] = ms
}
if v, _ := cmd.Flags().GetString("end"); v != "" {
ms, err := parseISOTimeToMillis("end", v)
if err != nil {
return err
}
toolArgs["endTime"] = ms
}
if v, _ := cmd.Flags().GetString("cursor"); v != "" {
toolArgs["cursor"] = v
}
if v := chatIntFlagOrFallback(cmd, "limit", "size"); v > 0 {
toolArgs["limit"] = v
}
return nil
}
func nativeSearchScopeUnverifiedError(conversationIDs, messageIDs []string) error {
return apperrors.NewAPI(
"搜索结果缺少 conversationId,无法证明会话过滤范围;已停止输出",
apperrors.WithReason("search_conversation_scope_unverified"),
apperrors.WithDetails(map[string]any{
"requestedConversationIds": conversationIDs,
"unverifiableMessageIds": messageIDs,
}),
apperrors.WithRetryable(false),
apperrors.WithHint("请保留 trace_id 并检查 IM 搜索服务是否返回 openConversationId"),
)
}
func uniqueNonEmptyStrings(values []string) []string {
out := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out
}
func positiveSearchLimit(value any, fallback int) int {
switch typed := value.(type) {
case int:
if typed > 0 {
return typed
}
case int64:
if typed > 0 {
return int(typed)
}
case json.Number:
if parsed, err := strconv.Atoi(typed.String()); err == nil && parsed > 0 {
return parsed
}
case float64:
if typed > 0 {
return int(typed)
}
}
return fallback
}
func cleanSearchCursor(value any) string {
if value == nil {
return ""
}
text := strings.TrimSpace(fmt.Sprint(value))
if text == "<nil>" || strings.EqualFold(text, "null") {
return ""
}
return text
}
func chatMessageListFavoritesArgs(cmd *cobra.Command) (map[string]any, error) {
cursor, _ := cmd.Flags().GetInt64("cursor")
if cursor < 0 {
return nil, apperrors.NewValidation("--cursor must be greater than or equal to 0")
}
size, _ := cmd.Flags().GetInt("size")
if size < 1 || size > chatFavoritesMaxPageSize {
return nil, apperrors.NewValidation("--size must be between 1 and 30")
}
return map[string]any{"cursor": cursor, "size": strconv.Itoa(size)}, nil
}
func pagedChatMessagesConfig(toolName string, build func(*cobra.Command) (map[string]any, error)) PagedMCPCommandConfig {
return PagedMCPCommandConfig{
ServerID: "chat",
ToolName: toolName,
ItemPath: "result.messages",
CursorPath: "result.nextCursor",
HasMorePath: "result.hasMore",
CursorArg: "cursor",
CursorKind: PagedCursorString,
BuildArgs: build,
Fallback: func(args map[string]any) error {
return callMCPTool(toolName, args)
},
}
}
func pagedChatConversationMessagesConfig(toolName string, build func(*cobra.Command) (map[string]any, error)) PagedMCPCommandConfig {
cfg := pagedChatMessagesConfig(toolName, build)
cfg.ItemPath = "result.conversationMessagesList"
cfg.AggregationMode = PagedAggregationConversationMessages
return cfg
}
func pagedProjectedChatSearchConfig(cmd *cobra.Command, toolName string, build func(*cobra.Command) (map[string]any, error)) PagedMCPCommandConfig {
cfg := pagedChatConversationMessagesConfig(toolName, build)
cfg.Fallback = func(args map[string]any) error {
return callProjectedChatMessages(cmd, toolName, args, true)
}
cfg.ProjectResult = func(payload map[string]any) map[string]any {
return projectChatMessagesPayload(payload, true)
}
return cfg
}
func pagedChatConversationMessagesOnServerConfig(serverID, toolName string, build func(*cobra.Command) (map[string]any, error)) PagedMCPCommandConfig {
cfg := pagedChatConversationMessagesConfig(toolName, build)
cfg.ServerID = serverID
cfg.Fallback = func(args map[string]any) error {
return callMCPToolOnServer(serverID, toolName, args)
}
return cfg
}
func pagedChatMessagesOnServerConfig(serverID, toolName string, build func(*cobra.Command) (map[string]any, error)) PagedMCPCommandConfig {
cfg := pagedChatMessagesConfig(toolName, build)
cfg.ServerID = serverID
cfg.Fallback = func(args map[string]any) error {
return callMCPToolOnServer(serverID, toolName, args)
}
return cfg
}
func pagedChatMessagesInt64Config(toolName string, build func(*cobra.Command) (map[string]any, error)) PagedMCPCommandConfig {
cfg := pagedChatMessagesConfig(toolName, build)
cfg.CursorKind = PagedCursorInt64
return cfg
}
func pagedMCPParamDecls() []contract.ParamDecl {
return []contract.ParamDecl{
{Name: "page-all", InterfaceType: "boolean"},
{Name: "page-limit", InterfaceType: "integer"},
{Name: "max-items", InterfaceType: "integer"},
{Name: "page-delay", InterfaceType: "integer"},
}
}
func runChatGroupSearch(cmd *cobra.Command, args []string) error {
keyword := flagOrFallback(cmd, "query", "keyword", "name", "group")
if len(args) == 1 {
if keyword != "" {
return apperrors.NewValidation("群搜索位置参数与 --query/--keyword 不能同时指定")
}
keyword = strings.TrimSpace(args[0])
}
if keyword == "" {
return apperrors.NewValidation("flag --query is required\n hint: dws chat search --query \"test\"")
}
limit := chatIntFlagOrFallback(cmd, "limit", "size")
cursor, _ := cmd.Flags().GetString("cursor")
toolArgs := map[string]any{
"keyword": keyword,
"limit": limit,
"cursor": cursor,
}
if v, _ := cmd.Flags().GetBool("exclude-muted"); v {
toolArgs["excludeMuted"] = true
}
return callMCPToolOnServer("im", "search_groups", toolArgs)
}
func newChatGroupSearchCommand(hidden bool) *cobra.Command {
cmd := &cobra.Command{
Use: "search [query]",
Short: "根据关键词搜索群聊",
Hidden: hidden,
Long: `根据关键词搜索群聊列表。分页参数 --limit(默认 20)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。
注意:
1. query 不要拆分得太细,应使用群名称中连续的核心词作为关键词(如群名"项目冲刺群"应搜"项目冲刺"而非拆成"项目"+"冲刺"分别搜索)。
2. 当搜索结果返回多个群聊时,应列出候选群让用户确认目标群聊,不要自行假定并直接进行后续操作。`,
Example: ` dws chat search --query "项目冲刺"
dws chat search "项目冲刺"
dws chat search --query "项目冲刺" --limit 20 --cursor 0`,
Args: cobra.MaximumNArgs(1),
RunE: runChatGroupSearch,
}
cmd.Flags().String("query", "", "搜索关键词 (必填)")
cmd.Flags().String("keyword", "", "--query 的别名")
_ = cmd.Flags().MarkHidden("keyword")
cmd.Flags().String("name", "", "--query 的兼容别名")
_ = cmd.Flags().MarkHidden("name")
cmd.Flags().String("group", "", "--query 的兼容别名")
_ = cmd.Flags().MarkHidden("group")
cmd.Flags().Int("limit", 20, "每页返回数量(默认 20)")
cmd.Flags().Int("size", 0, "--limit 的旧版别名")
_ = cmd.Flags().MarkHidden("size")
cmd.Flags().String("cursor", "0", "分页游标(默认 \"0\",翻页传 nextCursor)")
cmd.Flags().Bool("exclude-muted", false, "是否排除已设置免打扰的群聊(默认 false)")
return cmd
}
func runChatSearchCommon(cmd *cobra.Command, _ []string) error {
if err := validateRequiredFlags(cmd, "nicks"); err != nil {
return err
}
nicks := parseCSVValues(mustGetFlag(cmd, "nicks"))
limit := chatIntFlagOrFallback(cmd, "limit", "size")
cursor, _ := cmd.Flags().GetString("cursor")
matchMode, _ := cmd.Flags().GetString("match-mode")
toolArgs := map[string]any{
"nicks": nicks,
"matchMode": matchMode,
"limit": limit,
"cursor": cursor,
}
if v, _ := cmd.Flags().GetBool("exclude-muted"); v {
toolArgs["excludeMuted"] = true
}
return callMCPTool("search_common_groups", toolArgs)
}
// sanitizeTitleFromText derives a safe title from message text.
// When the user doesn't provide --title, the text content (which may contain
// URLs with percent-encoded characters like %3D, %26) is used as title.
// The title field has stricter validation on the server side (128 bytes max),
// so we strip or truncate content that is likely to be rejected.
func sanitizeTitleFromText(text string) (title string) {
const maxTitleBytes = 100
const maxTitleRunes = 30 // conservative: 30 CJK chars = 90 bytes, leaving room for "..."
const fallbackTitle = "消息"
if strings.TrimSpace(text) == "" {
return fallbackTitle
}
// If text contains a URL, use only the portion before the URL as title.
for _, prefix := range []string{"https://", "http://"} {
if idx := strings.Index(text, prefix); idx > 0 {
candidate := strings.TrimSpace(text[:idx])
if candidate != "" {
return truncateTitleToBytes(candidate, maxTitleBytes, maxTitleRunes)
}
}
}
// If the entire text is a URL, use a generic title.
if strings.HasPrefix(text, "https://") || strings.HasPrefix(text, "http://") {
return fallbackTitle
}
// Final fallback: if the result contains percent-encoded sequences
// that the server may reject, use a generic title.
if strings.Contains(text, "%") {
return fallbackTitle
}
// No URL — truncate to safe length.
return truncateTitleToBytes(text, maxTitleBytes, maxTitleRunes)
}
// truncateTitleToBytes truncates a title string ensuring it doesn't exceed
// maxBytes in UTF-8 encoding. It also limits by rune count for readability.
func truncateTitleToBytes(s string, maxBytes, maxRunes int) string {
runes := []rune(s)
if len(runes) > maxRunes {
runes = runes[:maxRunes]
}
result := string(runes)
// Ensure we don't exceed byte limit
for len(result) > maxBytes-3 { // reserve 3 bytes for "..."
runes = runes[:len(runes)-1]
result = string(runes)
}
if len([]rune(s)) > len(runes) {
return result + "..."
}
return result
}
// marshalJSONRaw serializes v to JSON without escaping <, >, &.
func marshalJSONRaw(v any) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, err
}
// Encode appends a trailing newline; trim it.
b := buf.Bytes()
if len(b) > 0 && b[len(b)-1] == '\n' {
b = b[:len(b)-1]
}
return b, nil
}
// parseCSVInt64 parses a comma-separated string of integers into []int64.
func parseCSVInt64(raw string) ([]int64, error) {
parts := strings.Split(raw, ",")
result := make([]int64, 0, len(parts))
for _, p := range parts {
v := strings.TrimSpace(p)
if v == "" {
continue
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid integer %q", v)
}
result = append(result, n)
}
if len(result) == 0 {
return nil, fmt.Errorf("at least one ID is required")
}
return result, nil
}
func isNumericUserID(value string) bool {
value = strings.TrimSpace(value)
if value == "" {
return false
}
for _, r := range value {
if r < '0' || r > '9' {
return false
}
}
return true
}
func isOpenDingTalkID(value string) bool {
return targetresolver.LooksLikeCurrentDOpenDingTalkID(value)
}
// webhookErrcodeFailure 解析自定义机器人 webhook 的响应,判定是否发送失败。
// webhook 失败时 HTTP 仍是 200 且返回 {errcode!=0, errmsg},需据 errcode 显式识别。
// errcode 可能是 JSON 数字或字符串,统一转字符串比较;缺 errcode 或为 0 视为成功。
func webhookErrcodeFailure(raw string) (code, msg string, failed bool) {
var m map[string]any
if json.Unmarshal([]byte(raw), &m) != nil {
return "", "", false
}
ec, ok := m["errcode"]
if !ok {
return "", "", false
}
code = strings.TrimSpace(fmt.Sprintf("%v", ec))
if code == "" || code == "0" || code == "0.0" {
return "", "", false
}
if v, ok := m["errmsg"].(string); ok {
msg = v
}
return code, msg, true
}
func splitChatIDValues(values []string) (userIDs []string, openDingTalkIDs []string) {
for _, raw := range values {
value := strings.TrimSpace(raw)
if value == "" {
continue
}
if isOpenDingTalkID(value) {
openDingTalkIDs = append(openDingTalkIDs, value)
} else {
userIDs = append(userIDs, value)
}
}
return userIDs, openDingTalkIDs
}
func appendStringSliceArg(args map[string]any, key string, values []string) {
if len(values) == 0 {
return
}
if existing, ok := args[key].([]string); ok {
args[key] = append(existing, values...)
return
}
args[key] = values
}
func appendChatIDArgs(args map[string]any, values []string, userIDKey, openDingTalkIDKey string) bool {
userIDs, openDingTalkIDs := splitChatIDValues(values)
appendStringSliceArg(args, userIDKey, userIDs)
appendStringSliceArg(args, openDingTalkIDKey, openDingTalkIDs)
return len(userIDs) > 0
}
// normalizeAtPlaceholders 统一文本中针对 ids 的 @ 占位符格式,消化 send 与 send-by-bot 之间的差异。
// wrapAngle=true:用户身份发消息(send),裸 @id 自动包成 <@id>;已有 <@id> 保持不变。
// wrapAngle=false:机器人发消息(send-by-bot),<@id> 自动剥离为 @id。
// 仅替换 ids 中实际声明的标识,避免误伤 markdown 中其他 <...> 内容。
func normalizeAtPlaceholders(text string, ids []string, wrapAngle bool) string {
const sentinelPrefix = "\x00DWS_AT_WRAPPED_"
const sentinelSuffix = "\x00"
for _, raw := range ids {
id := strings.TrimSpace(raw)
if id == "" {
continue
}
wrapped := "<@" + id + ">"
bare := "@" + id
if wrapAngle {
sentinel := sentinelPrefix + id + sentinelSuffix
text = strings.ReplaceAll(text, wrapped, sentinel)
text = strings.ReplaceAll(text, bare, wrapped)
text = strings.ReplaceAll(text, sentinel, wrapped)
} else {
text = strings.ReplaceAll(text, wrapped, bare)
}
}
return text
}
// NormalizeMessageMentions applies the placeholder convention required by the
// selected sender identity and ensures a declared @all is present in the body.
// Current-user messages use <@id>/<@all>; bot and webhook messages use
// @id/@mobile/@all.
func NormalizeMessageMentions(text string, ids []string, atAll, wrapAngle bool) string {
text = normalizeAtPlaceholders(text, ids, wrapAngle)
allPlaceholder := "@all"
if wrapAngle {
text = normalizeAtPlaceholders(text, []string{"all"}, true)
allPlaceholder = "<@all>"
} else {
text = strings.ReplaceAll(text, "<@all>", "@all")
}
if atAll && !containsMessageMention(text, allPlaceholder) {
text = allPlaceholder + " " + text
}
return text
}
// applyCurrentUserGroupMentions keeps the body placeholders and
// send_personal_message mention arguments aligned for send and reply.
func applyCurrentUserGroupMentions(params map[string]any, text, rawOpenIDs string, atAll bool) string {
var atOpenIDs []string
if rawOpenIDs != "" {
atOpenIDs = strings.Split(rawOpenIDs, ",")
}
if atAll && !strings.Contains(text, "<@all>") {
text = "<@all> " + text
}
text = normalizeAtPlaceholders(text, atOpenIDs, true)
if atAll {
params["atAll"] = true
}
if len(atOpenIDs) > 0 {
params["atOpenDingTalkIds"] = atOpenIDs
}
return text
}
func addMissingCurrentUserMentionPlaceholders(text, rawOpenIDs string) string {
if rawOpenIDs == "" {
return text
}
missing := make([]string, 0)
probeText := text
for _, id := range parseCSVValues(rawOpenIDs) {
placeholder := "<@" + id + ">"
if strings.Contains(probeText, placeholder) {
continue
}
missing = append(missing, placeholder)
probeText += placeholder
}
if len(missing) == 0 {
return text
}
prefix := strings.Join(missing, " ")
if strings.HasPrefix(text, "<@all> ") {
return "<@all> " + prefix + " " + strings.TrimPrefix(text, "<@all> ")
}
return prefix + " " + text
}
func containsMessageMention(text, placeholder string) bool {
if strings.HasPrefix(placeholder, "<") {
return strings.Contains(text, placeholder)
}
for searchFrom := 0; ; {
offset := strings.Index(text[searchFrom:], placeholder)
if offset < 0 {
return false
}
end := searchFrom + offset + len(placeholder)
if end == len(text) {
return true
}
next, _ := utf8.DecodeRuneInString(text[end:])
if !unicode.IsLetter(next) && !unicode.IsDigit(next) &&
next != '_' && next != '-' {
return true
}
searchFrom = end
}
}
func resolveOpenDingTalkID(ctx context.Context, value string) (string, error) {
ids, err := resolveOpenDingTalkIDs(ctx, []string{value})
if err != nil {
return "", err
}
if len(ids) == 0 || ids[0] == "" {
return "", fmt.Errorf("empty user identifier")
}
return ids[0], nil
}
func resolveOpenDingTalkIDs(ctx context.Context, values []string) ([]string, error) {
resolved := make([]string, len(values))
userIDs := make([]string, 0, len(values))
userIDIndexes := make([]int, 0, len(values))
seenUserIDs := make(map[string]bool)
for i, raw := range values {
value := strings.TrimSpace(raw)
if value == "" {
continue
}
if isOpenDingTalkID(value) {
resolved[i] = value
} else {
userIDIndexes = append(userIDIndexes, i)
if !seenUserIDs[value] {
seenUserIDs[value] = true
userIDs = append(userIDs, value)
}
}
}
if len(userIDs) == 0 {
return resolved, nil
}
openByUserID, err := lookupOpenDingTalkIDsByUserID(ctx, userIDs)
if err != nil {
return nil, err
}
for _, index := range userIDIndexes {
userID := strings.TrimSpace(values[index])
openDingTalkID := openByUserID[userID]
if openDingTalkID == "" {
return nil, fmt.Errorf("cannot resolve userId %q to openDingTalkId; hint: run dws contact user search --keyword \"姓名\" --format json and pass the returned openDingTalkId", userID)
}
resolved[index] = openDingTalkID
}
return resolved, nil
}
// groupOwnerOpenDingTalkID 分页拉取群成员,返回群主(memberRoleType==1)的 openDingtalkId。
// 找不到群主或群主字段为空时返回 ""(由调用方决定是否放行)。
func groupOwnerOpenDingTalkID(ctx context.Context, openConversationID string) (string, error) {
cursor := "0"
for page := 0; page < 50; page++ { // 防御性上限,避免异常分页导致死循环
raw, err := callMCPToolReturnText(ctx, "get_group_members", map[string]any{
"openconversation_id": openConversationID,
"cursor": cursor,
})
if err != nil {
return "", err
}
var body struct {
Result struct {
HasMore bool `json:"hasMore"`
NextCursor string `json:"nextCursor"`
Cursor string `json:"cursor"`
List []struct {
MemberRoleType int `json:"memberRoleType"`
OpenDingtalkID string `json:"openDingtalkId"`
} `json:"list"`
} `json:"result"`
}
if err := json.Unmarshal([]byte(raw), &body); err != nil {
return "", err
}
for _, m := range body.Result.List {
if m.MemberRoleType == 1 {
return m.OpenDingtalkID, nil
}
}
next := body.Result.NextCursor
if next == "" {
next = body.Result.Cursor
}
if !body.Result.HasMore || next == "" || next == cursor {
break
}
cursor = next
}
return "", nil
}
// guardGroupOwnerRemoval 拦截把群主移出群的操作:一个群只有一个群主,移出群主会产生
// 无群主的“孤儿群”。防护为尽力而为:群主信息查询失败或 userId→openDingTalkId 解析
// 失败时不阻塞正常移除路径(由服务端兜底)。
func guardGroupOwnerRemoval(ctx context.Context, openConversationID string, removeValues []string) error {
ownerOpenID, err := groupOwnerOpenDingTalkID(ctx, openConversationID)
if err != nil || ownerOpenID == "" {
return nil
}
ownerErr := fmt.Errorf(
"refusing to remove the group owner: 被移除列表包含群主,移出群主将导致群无群主(孤儿群)\n hint: 先执行 dws chat group transfer-owner --group %s --user <newOwnerUserId> 转让群主后再移除",
openConversationID,
)
userIDs, openDingTalkIDs := splitChatIDValues(removeValues)
for _, id := range openDingTalkIDs {
if id == ownerOpenID {
return ownerErr
}
}
if len(userIDs) > 0 {
if resolved, resolveErr := resolveOpenDingTalkIDs(ctx, userIDs); resolveErr == nil {
for _, id := range resolved {
if id == ownerOpenID {
return ownerErr
}
}
}
}
return nil
}
func lookupOpenDingTalkIDsByUserID(ctx context.Context, userIDs []string) (map[string]string, error) {
mapping := map[string]string{}
namesByUserID := map[string]string{}
// Step 1: Try contact service to get openDingTalkId and username by userId.
raw, err := callMCPToolReturnTextOnServer(ctx, "contact", "get_user_info_by_user_ids", map[string]any{
"user_id_list": userIDs,
})
if err == nil {
var body any
dec := json.NewDecoder(strings.NewReader(raw))
dec.UseNumber()
if err := dec.Decode(&body); err != nil {
return nil, fmt.Errorf("failed to parse contact user get response: %w", err)
}
collectContactUserMappings(body, mapping, namesByUserID)
}
if allUserIDsMapped(userIDs, mapping) {
return mapping, nil
}
// Step 2: For unmapped userIds, use aisearch with username (not userId) as keyword.
for _, userID := range userIDs {
if userID == "" || mapping[userID] != "" {
continue
}
keyword := namesByUserID[userID]
if keyword == "" {
// No username resolved from contact; fall through to keyword search below.
continue
}
_ = lookupOpenDingTalkIDsByAisearchPerson(ctx, keyword, mapping, namesByUserID)
}
if allUserIDsMapped(userIDs, mapping) {
return mapping, nil
}
// Step 3: Fallback - search contact by username or userId keyword.
for _, userID := range userIDs {
if mapping[userID] != "" {
continue
}
keywords := []string{}
if name := namesByUserID[userID]; name != "" {
keywords = append(keywords, name)
}
keywords = append(keywords, userID)
searched := map[string]bool{}
for _, keyword := range keywords {
if keyword == "" || searched[keyword] {
continue
}
searched[keyword] = true
searchRaw, err := callMCPToolReturnTextOnServer(ctx, "contact", "search_contact_by_key_word", map[string]any{
"keyword": keyword,
})
if err != nil {
return nil, err
}
var searchBody any
searchDec := json.NewDecoder(strings.NewReader(searchRaw))
searchDec.UseNumber()
if err := searchDec.Decode(&searchBody); err != nil {
return nil, fmt.Errorf("failed to parse contact user search response: %w", err)
}
collectContactUserMappings(searchBody, mapping, namesByUserID)
if mapping[userID] != "" {
break
}
}
}
return mapping, nil
}
func allUserIDsMapped(userIDs []string, mapping map[string]string) bool {
for _, userID := range userIDs {
if strings.TrimSpace(userID) != "" && mapping[userID] == "" {
return false
}
}
return true
}
func lookupOpenDingTalkIDsByAisearchPerson(ctx context.Context, keyword string, openByUserID map[string]string, nameByUserID map[string]string) error {
raw, err := callMCPToolReturnTextOnServer(ctx, "aisearch", "enterprise_person_search", map[string]any{
"keyword": keyword,
"dimension": []string{"all"},
})
if err != nil {
return err
}
var body any
dec := json.NewDecoder(strings.NewReader(raw))
dec.UseNumber()
if err := dec.Decode(&body); err != nil {
return fmt.Errorf("failed to parse aisearch person response: %w", err)
}
collectContactUserMappings(body, openByUserID, nameByUserID)
return nil
}
var (
chatUserIDJSONKeys = map[string]bool{
"userid": true,
"user_id": true,
"orguserid": true,
"org_user_id": true,
"uid": true,
"staffid": true,
"staff_id": true,
}
chatOpenDingTalkIDJSONKeys = map[string]bool{
"opendingtalkid": true,
"opendingtalk_id": true,
"open_dingtalk_id": true,
"opendingid": true,
}
chatUserNameJSONKeys = map[string]bool{
"name": true,
"username": true,
"user_name": true,
"orgusername": true,
"org_user_name": true,
"displayname": true,
"display_name": true,
"nick": true,
}
chatContactNestedUserKeys = []string{"orgEmployeeModel", "employee", "user", "profile", "staff"}
)
func collectContactUserMappings(value any, openByUserID map[string]string, nameByUserID map[string]string) {
switch typed := value.(type) {
case []any:
for _, item := range typed {
collectContactUserMappings(item, openByUserID, nameByUserID)
}
case map[string]any:
userID := stringForJSONKeys(typed, chatUserIDJSONKeys)
openDingTalkID := stringForJSONKeys(typed, chatOpenDingTalkIDJSONKeys)
userName := stringForJSONKeys(typed, chatUserNameJSONKeys)
if userID == "" {
userID = stringForNestedJSONKeys(typed, chatContactNestedUserKeys, chatUserIDJSONKeys)
}
if openDingTalkID == "" {
openDingTalkID = stringForNestedJSONKeys(typed, chatContactNestedUserKeys, chatOpenDingTalkIDJSONKeys)
}
if userName == "" {
userName = stringForNestedJSONKeys(typed, chatContactNestedUserKeys, chatUserNameJSONKeys)
}
if userID != "" && openDingTalkID != "" {
openByUserID[userID] = openDingTalkID
}
if userID != "" && userName != "" {
nameByUserID[userID] = userName
}
for _, item := range typed {
collectContactUserMappings(item, openByUserID, nameByUserID)
}
}
}
func stringForNestedJSONKeys(value map[string]any, nestedKeys []string, keys map[string]bool) string {
for _, nestedKey := range nestedKeys {
if nested, ok := value[nestedKey].(map[string]any); ok {
if found := stringForJSONKeys(nested, keys); found != "" {
return found
}
}
}
return ""
}
func stringForJSONKeys(value map[string]any, keys map[string]bool) string {
for key, raw := range value {
if !keys[strings.ToLower(key)] {
continue
}
if str := stringFromJSONScalar(raw); str != "" {
return str
}
}
return ""
}
func stringFromJSONScalar(value any) string {
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed)
case json.Number:
return typed.String()
case float64:
return strconv.FormatFloat(typed, 'f', -1, 64)
case float32:
return strconv.FormatFloat(float64(typed), 'f', -1, 32)
case int:
return strconv.Itoa(typed)
case int64:
return strconv.FormatInt(typed, 10)
case int32:
return strconv.FormatInt(int64(typed), 10)
default:
return ""
}
}
func buildConversationTargetArgs(cmd *cobra.Command) (map[string]any, error) {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
rawOpenDingTalkID, _ := cmd.Flags().GetString("open-dingtalk-id")
rawUserID := flagOrFallback(cmd, "user", "userId")
// All three params are passed through to the server; the server decides which to use.
userID := rawUserID
openDingTalkID := rawOpenDingTalkID
if openDingTalkID != "" && isNumericUserID(openDingTalkID) {
userID = openDingTalkID
openDingTalkID = ""
}
if userID != "" && !isNumericUserID(userID) {
openDingTalkID = userID
userID = ""
}
toolArgs := map[string]any{}
if groupID != "" {
toolArgs["openConversationId"] = groupID
}
if userID != "" {
toolArgs["userId"] = userID
}
if openDingTalkID != "" {
toolArgs["openDingTalkId"] = openDingTalkID
}
return toolArgs, nil
}
var chatValidGrantTypes = map[string]bool{
"once": true,
"session": true,
"timed": true,
"permanent": true,
}
func validateChatScope(scope string) error {
if !strings.HasPrefix(scope, "chat.") {
return fmt.Errorf("invalid scope %q, dws chat chmod only accepts chat.* scope", scope)
}
return nil
}
func buildChatGrantBaseArgs(cmd *cobra.Command, scope string) (map[string]any, error) {
grantType, _ := cmd.Flags().GetString("grant-type")
if !chatValidGrantTypes[grantType] {
return nil, fmt.Errorf("invalid --grant-type %q, must be one of: once, session, timed, permanent", grantType)
}
ttl, _ := cmd.Flags().GetString("ttl")
sessionID, _ := cmd.Flags().GetString("session-id")
if grantType == "timed" && ttl == "" {
return nil, fmt.Errorf("--ttl is required when --grant-type is timed")
}
if grantType == "session" && sessionID == "" {
return nil, fmt.Errorf("--session-id is required when --grant-type is session")
}
toolArgs := map[string]any{
"agentCode": mustGetFlag(cmd, "agentCode"),
"scope": scope,
"grantType": grantType,
}
if grantType == "timed" {
toolArgs["ttl"] = ttl
}
if sessionID != "" {
toolArgs["sessionId"] = sessionID
}
return toolArgs, nil
}
func buildChatChmodArgs(cmd *cobra.Command, scope string) (map[string]any, error) {
toolArgs, err := buildChatGrantBaseArgs(cmd, scope)
if err != nil {
return nil, err
}
if err := appendChatChmodParams(cmd, toolArgs); err != nil {
return nil, err
}
return toolArgs, nil
}
func buildChatCrossOrgDataAuthArgs(cmd *cobra.Command) (map[string]any, error) {
targetOrgID := strings.TrimSpace(mustGetFlag(cmd, "target-org-id"))
all, _ := cmd.Flags().GetBool("all")
if targetOrgID == "" && !all {
return nil, fmt.Errorf("--target-org-id or --all is required")
}
if targetOrgID != "" && all {
return nil, fmt.Errorf("--target-org-id and --all cannot be used together")
}
if all {
targetOrgID = "*"
}
toolArgs, err := buildChatGrantBaseArgs(cmd, "chat.data:cross-org")
if err != nil {
return nil, err
}
toolArgs["grantCategory"] = "data"
paramsJSON, _ := marshalJSONRaw(map[string]string{"targetOrgId": targetOrgID})
toolArgs["grantParams"] = string(paramsJSON)
return toolArgs, nil
}
func buildChatGroupShareInviteArgs(cmd *cobra.Command) (map[string]any, error) {
if err := validateRequiredFlags(cmd, "source"); err != nil {
return nil, err
}
target, _ := cmd.Flags().GetString("target")
receiver, _ := cmd.Flags().GetString("receiver")
if target == "" && receiver == "" {
return nil, fmt.Errorf("--target or --receiver is required")
}
if target != "" && receiver != "" {
return nil, fmt.Errorf("--target and --receiver are mutually exclusive")
}
toolArgs := map[string]any{
"sourceOpenConversationId": mustGetFlag(cmd, "source"),
}
if target != "" {
toolArgs["targetOpenConversationId"] = target
}
if receiver != "" {
toolArgs["receiverOpenDingTalkId"] = receiver
}
if v, _ := cmd.Flags().GetInt64("expires-seconds"); v > 0 || cmd.Flags().Changed("expires-seconds") {
toolArgs["expiresSeconds"] = v
}
if v, _ := cmd.Flags().GetString("uuid"); v != "" {
toolArgs["uuid"] = v
}
return toolArgs, nil
}
func appendChatChmodParams(cmd *cobra.Command, toolArgs map[string]any) error {
conversationID, _ := cmd.Flags().GetString("conversation-id")
openDingTalkID, _ := cmd.Flags().GetString("open-dingtalk-id")
userID, _ := cmd.Flags().GetString("user")
rawParams, _ := cmd.Flags().GetStringArray("permParam")
grantParams, err := parseChatChmodParams(rawParams)
if err != nil {
return err
}
specified := 0
for _, value := range []string{conversationID, openDingTalkID, userID} {
if strings.TrimSpace(value) != "" {
specified++
}
}
if specified > 1 {
return fmt.Errorf("--conversation-id, --open-dingtalk-id and --user are mutually exclusive")
}
if conversationID != "" {
putChatChmodParam(grantParams, "conversationId", conversationID)
putChatChmodParam(grantParams, "openConversationId", conversationID)
putChatChmodParam(grantParams, "openCid", conversationID)
} else if openDingTalkID != "" {
putChatChmodParam(grantParams, "openDingTalkId", openDingTalkID)
} else {
if userID != "" {
putChatChmodParam(grantParams, "targetUid", userID)
putChatChmodParam(grantParams, "receiverUid", userID)
}
}
if specified == 0 && len(grantParams) == 0 {
return fmt.Errorf("--conversation-id, --open-dingtalk-id, --user or --permParam is required")
}
paramsJSON, _ := marshalJSONRaw(grantParams)
toolArgs["grantParams"] = string(paramsJSON)
return nil
}
func parseChatChmodParams(values []string) (map[string]string, error) {
params := map[string]string{}
for _, raw := range values {
item := strings.TrimSpace(raw)
if item == "" {
continue
}
parts := strings.SplitN(item, "=", 2)
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" {
return nil, fmt.Errorf("--permParam must be key=value, got %q", raw)
}
params[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
return params, nil
}
func putChatChmodParam(params map[string]string, key, value string) {
if strings.TrimSpace(value) == "" {
return
}
if _, exists := params[key]; !exists {
params[key] = strings.TrimSpace(value)
}
}
func fileMD5Hex(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("failed to open file for md5: %w", err)
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", fmt.Errorf("failed to calculate md5: %w", err)
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func parseConversationFileUploadInfo(text string) (resourceURL, uploadKey string, headers map[string]string, err error) {
var data map[string]any
if err = unmarshalJSONUseNumber(text, &data); err != nil {
return "", "", nil, fmt.Errorf("failed to parse upload credentials JSON: %w", err)
}
if result, ok := data["result"].(map[string]any); ok {
data = result
}
resourceURL = firstStringField(data, "resourceUrl", "resourceURL", "url")
if resourceURL == "" {
if values, ok := data["resourceUrls"].([]any); ok && len(values) > 0 {
resourceURL = stringFromJSONScalar(values[0])
}
}
uploadKey = firstStringField(data, "uploadKey", "key")
if resourceURL == "" || uploadKey == "" {
return "", "", nil, fmt.Errorf("incomplete upload credentials: resourceUrl=%q, uploadKey=%q", resourceURL, uploadKey)
}
headers = map[string]string{}
for _, key := range []string{"headers", "ossHeaders"} {
if h, ok := data[key].(map[string]any); ok {
for name, value := range h {
if s := stringFromJSONScalar(value); s != "" {
headers[name] = s
}
}
}
}
return resourceURL, uploadKey, headers, nil
}
type conversationLocalFileMeta struct {
LocalPath string
FileName string
FileType string
ContentPath string
FileSize int64
MD5 string
}
var chatFileMD5 = fileMD5Hex
func buildConversationLocalFileMeta(filePath, fileName, md5Value string) (conversationLocalFileMeta, error) {
fi, err := os.Stat(filePath)
if err != nil {
return conversationLocalFileMeta{}, fmt.Errorf("cannot read file %s: %w", filePath, err)
}
if fi.IsDir() {
return conversationLocalFileMeta{}, fmt.Errorf("%s is a directory, not a file", filePath)
}
if fileName == "" {
fileName = filepath.Base(filePath)
}
fileType := strings.TrimPrefix(filepath.Ext(fileName), ".")
if md5Value == "" {
md5Value, err = chatFileMD5(filePath)
if err != nil {
return conversationLocalFileMeta{}, err
}
}
return conversationLocalFileMeta{
LocalPath: filePath,
FileName: fileName,
FileType: fileType,
ContentPath: "/" + fileName,
FileSize: fi.Size(),
MD5: md5Value,
}, nil
}
func uploadConversationLocalFile(ctx context.Context, targetArgs map[string]any, meta conversationLocalFileMeta, uuid string) (string, error) {
initArgs := cloneStringAnyMap(targetArgs)
initArgs["fileName"] = meta.FileName
initArgs["fileSize"] = meta.FileSize
initArgs["md5"] = meta.MD5
if uuid != "" {
initArgs["uuid"] = uuid
}
initText, err := callMCPToolReturnTextOnServer(ctx, "im", "init_conversation_file_upload", initArgs)
if err != nil {
return "", err
}
resourceURL, uploadKey, headers, err := parseConversationFileUploadInfo(initText)
if err != nil {
return "", err
}
if err := httpPutFile(ctx, resourceURL, headers, meta.LocalPath, meta.FileSize); err != nil {
return "", err
}
commitArgs := cloneStringAnyMap(targetArgs)
commitArgs["uploadKey"] = uploadKey
commitArgs["fileName"] = meta.FileName
commitArgs["fileSize"] = meta.FileSize
commitArgs["md5"] = meta.MD5
if uuid != "" {
commitArgs["uuid"] = uuid
}
return callMCPToolReturnTextOnServer(ctx, "im", "commit_conversation_file_upload", commitArgs)
}
func parseConversationFileDownloadURL(text string) (string, error) {
var data map[string]any
if err := unmarshalJSONUseNumber(text, &data); err != nil {
return "", fmt.Errorf("failed to parse uploaded file response JSON: %w", err)
}
if result, ok := data["result"].(map[string]any); ok {
data = result
}
downloadURL := firstStringField(data, "downloadUrl")
if downloadURL == "" {
return "", fmt.Errorf("uploaded file response missing downloadUrl")
}
return downloadURL, nil
}
func uploadRobotMessageLocalFile(
cmd *cobra.Command,
filePath string,
groupID string,
userIDs []string,
openDingTalkIDs []string,
) (string, error) {
uploadTarget := map[string]any{}
if groupID != "" {
uploadTarget["openConversationId"] = groupID
} else {
if len(userIDs)+len(openDingTalkIDs) != 1 {
return "", fmt.Errorf("--file-path requires exactly one --users or --open-dingtalk-ids recipient")
}
if len(userIDs) == 1 {
uploadTarget["userId"] = userIDs[0]
} else {
uploadTarget["openDingTalkId"] = openDingTalkIDs[0]
}
}
meta, err := buildConversationLocalFileMeta(filePath, "", "")
if err != nil {
return "", err
}
if deps.Caller.DryRun() {
deps.Out.PrintKeyValue("操作", "上传本地文件并由机器人发送")
deps.Out.PrintKeyValue("文件", meta.LocalPath)
deps.Out.PrintKeyValue("名称", meta.FileName)
deps.Out.PrintKeyValue("大小", fmt.Sprintf("%d bytes", meta.FileSize))
return "", nil
}
ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Minute)
defer cancel()
commitText, err := uploadConversationLocalFile(ctx, uploadTarget, meta, "")
if err != nil {
return "", err
}
return parseConversationFileDownloadURL(commitText)
}
func buildConversationFileContent(dentryID, spaceID int64, meta conversationLocalFileMeta) (string, error) {
content := struct {
DentryID int64 `json:"dentryId"`
SpaceID int64 `json:"spaceId"`
FileName string `json:"fileName"`
FileType string `json:"fileType"`
FilePath string `json:"filePath"`
FileSize int64 `json:"fileSize"`
}{
DentryID: dentryID,
SpaceID: spaceID,
FileName: meta.FileName,
FileType: meta.FileType,
FilePath: meta.ContentPath,
FileSize: meta.FileSize,
}
body, _ := marshalJSONRaw(content)
return string(body), nil
}
func parseConversationFileSendIDs(text string) (int64, int64, error) {
var data any
if err := unmarshalJSONUseNumber(text, &data); err != nil {
return 0, 0, fmt.Errorf("failed to parse uploaded file response JSON: %w", err)
}
dentryID, _ := findInt64Field(data, "dentryId", "dentryID")
spaceID, _ := findInt64Field(data, "spaceId", "spaceID")
if dentryID == 0 || spaceID == 0 {
return 0, 0, fmt.Errorf("uploaded file response missing dentryId or spaceId")
}
return dentryID, spaceID, nil
}
func findInt64Field(value any, keys ...string) (int64, bool) {
switch typed := value.(type) {
case map[string]any:
for _, key := range keys {
if raw, ok := typed[key]; ok {
if v, ok := int64FromJSONScalar(raw); ok {
return v, true
}
}
}
for _, raw := range typed {
if v, ok := findInt64Field(raw, keys...); ok {
return v, true
}
}
case []any:
for _, raw := range typed {
if v, ok := findInt64Field(raw, keys...); ok {
return v, true
}
}
case string:
trimmed := strings.TrimSpace(typed)
if strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[") {
var nested any
if unmarshalJSONUseNumber(trimmed, &nested) == nil {
return findInt64Field(nested, keys...)
}
}
}
return 0, false
}
func int64FromJSONScalar(value any) (int64, bool) {
switch typed := value.(type) {
case json.Number:
v, err := typed.Int64()
return v, err == nil
case float64:
return int64(typed), typed > 0
case int64:
return typed, true
case int:
return int64(typed), true
case string:
v, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
return v, err == nil
default:
return 0, false
}
}
func cloneStringAnyMap(src map[string]any) map[string]any {
dst := make(map[string]any, len(src))
for key, value := range src {
dst[key] = value
}
return dst
}
func unmarshalJSONUseNumber(text string, v any) error {
dec := json.NewDecoder(strings.NewReader(text))
dec.UseNumber()
return dec.Decode(v)
}
func nativeCardUpdateVerificationError(bizID string, verifyErr error) error {
reason := "streaming_card_update_unverified"
message := "服务端未返回卡片实际更新的证据;为避免假成功,CLI 已将本次操作判为失败"
hint := "请检查服务端是否返回 updated=true、affectedCount>0 或等价的明确更新结果"
switch {
case errors.Is(verifyErr, chatmsg.ErrCardUpdateNotApplied):
reason = "streaming_card_update_not_applied"
message = "服务端明确表示流式卡片没有被更新"
hint = "请确认 bizId 来自 send-card、当前账号有权限且卡片仍允许该状态转换"
case errors.Is(verifyErr, chatmsg.ErrCardUpdateBizIDDrift):
reason = "streaming_card_update_biz_id_mismatch"
message = "服务端返回的 bizId 与本次请求不一致;无法确认目标卡片已更新"
hint = "请保留 trace_id 并检查 update_streaming_card 的响应映射"
}
return apperrors.NewAPI(
message,
apperrors.WithOperation("update_streaming_card"),
apperrors.WithServerKey("im"),
apperrors.WithOrigin("client_postcondition"),
apperrors.WithFailureStage("verify_update_result"),
apperrors.WithExecutionStarted(true),
apperrors.WithRetryable(false),
apperrors.WithReason(reason),
apperrors.WithHint(hint),
apperrors.WithDetails(map[string]any{"bizId": bizID}),
apperrors.WithCause(verifyErr),
)
}
func firstStringField(data map[string]any, keys ...string) string {
for _, key := range keys {
if value, ok := data[key]; ok {
if s := stringFromJSONScalar(value); s != "" {
return s
}
}
}
return ""
}
// ──────────────────────────────────────────────────────────
// dws chat — 会话 / 群聊 / 消息
// ──────────────────────────────────────────────────────────
func newChatCommand() *cobra.Command {
// Product-level Agent routing Decl (migrated from selection/chat.json
// products.chat). Catalog assembly stamps provenance contract_final.
contract.RegisterProductDecl(contract.ProductDecl{
ID: "chat",
Selection: contract.ProductSelectionDecl{
AgentSummary: "管理钉钉会话、群聊、群成员、机器人、消息检索与发送",
UseWhen: []string{
"请求涉及群聊管理、聊天记录、消息发送、会话设置或群机器人",
},
AvoidWhen: []string{
"实时监听未来 IM 事件用 event +listen-im;邮件用 mail;开放平台应用/机器人建号发布用 dev;企业语义找人优先 aisearch person",
},
},
})
root := &cobra.Command{
Use: "chat",
Aliases: []string{"im"},
Short: "群聊 / 消息 / 机器人",
Long: `管理钉钉会话与群聊:创建群、搜索群、查看群成员、添加机器人到群、修改群名称、拉取/发送/收藏会话消息、机器人消息与 Webhook。`,
RunE: groupRunE,
}
chatChmodCmd := &cobra.Command{
Use: "chmod <scope>",
Short: "授予 chat 高风险操作权限",
Long: `授予指定命令参数维度的 chat 操作权限。
该命令用于触发悟空宿主应用的授权确认弹窗。chat.* scope 每次执行都需要用户在宿主 UI 中确认,模型无法静默绕过。
授权维度:
--permParam 授权原始业务参数,可重复传入 key=value,例如 --permParam openCid=xxx --permParam msgType=text
兼容目标选择(三选一):
--conversation-id 群聊 openConversationId
--open-dingtalk-id 单聊目标 openDingTalkId
--user 单聊目标 userId,由服务端按 Diamond 映射解析为授权维度`,
Example: ` dws chat chmod chat.message:send --agentCode agt-wukong-xxxx --grant-type timed --ttl 24h --permParam openCid=cidXXXXXXXXXX
dws chat chmod chat.message:send --agentCode agt-wukong-xxxx --grant-type timed --ttl 24h --permParam receiverUid=123456
dws chat chmod chat.group:destroy --agentCode agt-wukong-xxxx --grant-type once --permParam openCid=cidXXXXXXXXXX
dws chat chmod chat.message:send --agentCode agt-wukong-xxxx --grant-type timed --ttl 24h --conversation-id cidXXXXXXXXXX`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateChatScope(args[0]); err != nil {
return err
}
toolArgs, err := buildChatChmodArgs(cmd, args[0])
if err != nil {
return err
}
if !commandBoolFlag(cmd, "yes") {
return apperrors.NewValidation(
"授予 chat 高风险操作权限需要用户确认;获得用户确认后加 --yes 执行",
apperrors.WithReason("confirmation_required"),
apperrors.WithHint("先确认授权 scope、目标会话/用户和有效期;用户明确同意后以相同参数追加 --yes"),
apperrors.WithActions("确认授权 scope 和影响范围", "获得用户确认后使用 --yes 执行"),
)
}
return callMCPToolOnServer("im", "chat_permission_grant", toolArgs)
},
}
chatChmodCmd.Flags().String("agentCode", "wukong", "Agent 标识,默认 wukong")
chatChmodCmd.Flags().String("grant-type", "timed", "授权策略: once|session|timed|permanent")
chatChmodCmd.Flags().String("ttl", "24h", "timed 授权有效期,如 1h/4h/24h/7d")
chatChmodCmd.Flags().StringArray("permParam", nil, "授权原始业务参数,格式 key=value,可重复传入")
chatChmodCmd.Flags().String("conversation-id", "", "群聊 openConversationId")
chatChmodCmd.Flags().String("open-dingtalk-id", "", "单聊目标 openDingTalkId")
chatChmodCmd.Flags().String("user", "", "单聊目标 userId(与 --open-dingtalk-id 二选一)")
chatChmodCmd.Flags().String("session-id", "", "session 授权的会话标识")
chatChmodCmd.Flags().BoolP("yes", "y", false, "确认执行 chat 高风险授权操作")
DeclareLeafMetadata(chatChmodCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "chat_permission_grant",
CanonicalPath: "chat.chat_permission_grant",
CLIPath: "chat chmod",
PrimaryCLIPath: "chat chmod",
},
Description: "授予指定 chat scope 的高风险操作权限",
Positionals: []contract.RuntimeSchemaPositional{
{Index: 0, Name: "scope", Type: "string", Required: true, Description: "chat 授权 scope,如 chat.message:send"},
},
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "chat_permission_grant"},
},
Selection: contract.SelectionSpec{
AgentSummary: "授予指定 chat scope 的高风险操作权限",
UseWhen: []string{"需要在执行发送、撤回、群管理等 chat 高风险操作前申请授权时"},
AvoidWhen: []string{"只授予跨组织消息读取数据权限时使用 chat data-auth cross-org"},
Examples: []string{"dws chat chmod chat.message:send --agentCode wukong --grant-type timed --ttl 24h --conversation-id <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "agentCode", Property: "agentCode", Required: boolPtr(false)},
{Name: "conversation-id", Property: "grantParams.openCid", Required: boolPtr(false)},
{Name: "grant-type", Property: "grantType", Required: boolPtr(false), Enum: []string{"once", "session", "timed", "permanent"}},
{Name: "open-dingtalk-id", Property: "grantParams.openDingTalkId", Required: boolPtr(false)},
{Name: "permParam", Property: "grantParams", Required: boolPtr(false)},
{Name: "session-id", Property: "sessionId", Required: boolPtr(false), RequiredWhen: "grant-type is session"},
{Name: "ttl", Property: "ttl", Required: boolPtr(false), RequiredWhen: "grant-type is timed"},
{Name: "user", Property: "grantParams.userId", Required: boolPtr(false)},
},
},
})
chatDataAuthCmd := &cobra.Command{
Use: "data-auth",
Short: "授予 chat 数据读取权限",
Long: `授予 chat 数据读取权限。该命令用于跨组织消息拉取等数据访问场景,不用于发送、撤回、群管理等命令操作。`,
RunE: groupRunE,
}
chatDataAuthCrossOrgCmd := &cobra.Command{
Use: "cross-org",
Short: "授予跨组织 chat 数据访问权限",
Long: `授予跨组织 chat 数据访问权限。
该命令调用与 dws chat chmod 相同的授权工具,但固定使用数据授权类别:
scope: chat.data:cross-org
grantCategory: data
grantParams: {"targetOrgId":"<目标组织ID>"} 或 {"targetOrgId":"*"}`,
Example: ` dws chat data-auth cross-org --target-org-id 439446171
dws chat data-auth cross-org --target-org-id 439446171 --agentCode wukong --grant-type timed --ttl 24h
dws chat data-auth cross-org --all --grant-type timed --ttl 24h`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs, err := buildChatCrossOrgDataAuthArgs(cmd)
if err != nil {
return err
}
return callMCPToolOnServer("im", "chat_permission_grant", toolArgs)
},
}
chatDataAuthCrossOrgCmd.Flags().String("target-org-id", "", "目标组织 ID(与 --all 二选一)")
chatDataAuthCrossOrgCmd.Flags().Bool("all", false, "授权所有目标组织")
chatDataAuthCrossOrgCmd.Flags().String("agentCode", "wukong", "Agent 标识,默认 wukong")
chatDataAuthCrossOrgCmd.Flags().String("grant-type", "timed", "授权策略: once|session|timed|permanent")
chatDataAuthCrossOrgCmd.Flags().String("ttl", "24h", "timed 授权有效期,如 1h/4h/24h/7d")
chatDataAuthCrossOrgCmd.Flags().String("session-id", "", "session 授权的会话标识")
chatDataAuthCrossOrgCmd.Flags().BoolP("yes", "y", false, "确认执行跨组织 chat 数据授权")
DeclareLeafMetadata(chatDataAuthCrossOrgCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
Validate: func(cmd *cobra.Command, args []string) error {
_, err := buildChatCrossOrgDataAuthArgs(cmd)
return err
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "chat_permission_grant_cross_org_data",
CanonicalPath: "chat." + "chat_permission_grant_cross_org_data",
CLIPath: "chat data-auth cross-org",
PrimaryCLIPath: "chat data-auth cross-org",
},
Description: "授予跨组织 chat 数据访问权限",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "chat_permission_grant"},
},
Selection: contract.SelectionSpec{
AgentSummary: "授予跨组织 chat 数据访问权限",
UseWhen: []string{"需要为跨组织消息读取授予 chat.data:cross-org 数据权限时"},
AvoidWhen: []string{"发送、撤回或群管理授权应使用 chat chmod 指定操作 scope"},
Examples: []string{"dws chat data-auth cross-org --target-org-id 439446171"},
},
Parameters: []contract.ParamDecl{
{Name: "agentCode", Property: "agentCode", Required: boolPtr(false)},
{Name: "all", Property: "grantParams.targetOrgId", Required: boolPtr(false)},
{Name: "grant-type", Property: "grantType", Required: boolPtr(false), Enum: []string{"once", "session", "timed", "permanent"}},
{Name: "session-id", Property: "sessionId", Required: boolPtr(false), RequiredWhen: "grant-type is session"},
{Name: "target-org-id", Property: "grantParams.targetOrgId", Required: boolPtr(false)},
{Name: "ttl", Property: "ttl", Required: boolPtr(false), RequiredWhen: "grant-type is timed"},
},
},
})
chatDataAuthCmd.AddCommand(chatDataAuthCrossOrgCmd)
// ── group 子命令 ──────────────────────────────────────────
chatGroupCmd := &cobra.Command{Use: "group", Short: "群组管理", RunE: groupRunE}
chatGroupCreateCmd := &cobra.Command{
Use: "create",
Short: "创建群(支持内部群/外部群/普通群/话题圈)",
Long: `创建一个群聊,支持指定群名称、初始成员列表等参数。
可选择内部群/外部群/普通群/内部话题圈/外部话题圈/普通话题圈等多种类型群。
默认创建内部群。当选择内部群时如果所选成员非组织内成员会创建失败。`,
Example: ` dws chat group create --name "Q1 项目冲刺群" --users userId1,userId2,userId3
dws chat group create --name "外部合作群" --users userId1,userId2 --type EXTERNAL
dws chat group create --name "话题圈" --users userId1,userId2 --thread
# 查询 userId: dws contact user search --query "姓名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "name", "users"); err != nil {
return err
}
ctx := cmd.Context()
// 校验 --type 取值
groupType, _ := cmd.Flags().GetString("type")
groupType = strings.ToUpper(groupType)
switch groupType {
case "INTERNAL", "EXTERNAL", "NORMAL":
// valid
default:
return fmt.Errorf("invalid --type %q, supported: INTERNAL, EXTERNAL, NORMAL", groupType)
}
memberUserIds := parseCSVValues(mustGetFlag(cmd, "users"))
// 钉钉要求:群主(owner)必须是 useridlist 的成员之一。当前登录用户作为群主,须加入成员列表。
currentUserID, err := getCurrentUserID(ctx)
if err != nil {
return err
}
// 将当前用户置于首位(作为群主),避免重复添加
seen := map[string]bool{currentUserID: true}
allMembers := []string{currentUserID}
for _, uid := range memberUserIds {
if !seen[uid] {
seen[uid] = true
allMembers = append(allMembers, uid)
}
}
toolArgs := map[string]any{
"groupName": mustGetFlag(cmd, "name"),
"groupMembers": allMembers,
"groupType": groupType,
}
// 话题模式
thread, _ := cmd.Flags().GetBool("thread")
if thread {
toolArgs["convThreadEnabled"] = true
}
raw, err := callMCPToolReturnTextOnServer(ctx, "im", "create_group_conversation", toolArgs)
if err != nil {
return err
}
var resp map[string]any
if json.Unmarshal([]byte(raw), &resp) == nil {
if result, ok := resp["result"].(map[string]any); ok {
if v, exists := result["openCid"]; exists {
result["openConversationId"] = v
delete(result, "openCid")
}
delete(result, "cid")
}
return deps.Out.PrintJSON(resp)
}
deps.Out.PrintRaw(raw)
return nil
},
}
DeclareLeafMetadata(chatGroupCreateCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "create_group_conversation",
CanonicalPath: "chat.create_group_conversation",
CLIPath: "chat group create",
PrimaryCLIPath: "chat group create",
},
Description: "创建群聊并可指定群名与初始成员",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "create_group_conversation"},
},
Selection: contract.SelectionSpec{
AgentSummary: "创建群聊并可指定群名与初始成员",
UseWhen: []string{
"需要新建内部/外部群或话题圈,并带上初始成员",
"用户明确要建群且已给出群名与成员标识",
},
AvoidWhen: []string{
"已有群只需加人时使用 chat group members add",
"只是搜索已有群时使用 chat search",
},
Examples: []string{"dws chat group create --name \"Q1 项目冲刺群\" --users userId1,userId2,userId3"},
},
Parameters: []contract.ParamDecl{
{Name: "name", Property: "groupName"},
{Name: "thread", Property: "convThreadEnabled"},
{Name: "type", Property: "groupType"},
{Name: "users", Property: "groupMembers"},
},
},
})
chatSearchCmd := newChatGroupSearchCommand(false)
chatGroupSearchCompatibilityCmd := newChatGroupSearchCommand(true)
cli.AttachRuntimeSchema(
chatGroupSearchCompatibilityCmd,
"chat",
"search_groups",
"reviewed-compatibility:chat-group-search",
)
cli.AnnotateRuntimeCompatibilityEquivalence(
chatSearchCmd,
chatGroupSearchCompatibilityCmd,
cli.RuntimeCompatibilityEquivalence{
ID: "chat-group-search-compatibility-v1",
Reason: "Both leaves share the same constructor, flags, positional normalization, read-only search_groups transport, and result contract.",
Reviewed: true,
},
)
DeclareLeafMetadata(chatSearchCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "search_groups",
CanonicalPath: "chat.search_groups",
CLIPath: "chat search",
PrimaryCLIPath: "chat search",
Aliases: []string{"chat group search"},
},
Description: "按关键词搜索群聊并拿到 openConversationId",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: the CLI calls im/search_groups with a flat payload, while the pinned snapshot only contains the incompatible chat/search_groups_by_keyword contract.",
},
Selection: contract.SelectionSpec{
AgentSummary: "按关键词搜索群聊并拿到 openConversationId",
UseWhen: []string{"只知道群名关键词,需要定位群并提取 openConversationId"},
AvoidWhen: []string{
"已有群号时用 chat group get-by-group-id",
"查我创建/管理的群用 chat group list-my-groups",
"查与某人的共同群用 chat search-common",
},
Examples: []string{
"dws chat search --query \"项目冲刺\"",
"dws chat search --query \"项目冲刺\" --limit 20 --cursor 0",
},
},
Parameters: []contract.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
})
chatGroupMembersCmd := &cobra.Command{
Use: "members",
Short: "群成员管理",
Long: `查看群成员列表,分页查询指定群聊的成员。`,
Example: ` dws chat group members --id <openconversation_id>
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id"); err != nil {
return err
}
toolArgs := map[string]any{
"openconversation_id": mustGetFlag(cmd, "id"),
}
if v, _ := cmd.Flags().GetString("cursor"); v != "" {
toolArgs["cursor"] = v
}
return callMCPTool("get_group_members", toolArgs)
},
}
chatGroupMembersAddBotCmd := &cobra.Command{
Use: "add-bot",
Short: "将机器人添加到群中",
Long: `将自定义机器人添加到当前用户有管理权限的群聊中。如果没有权限则会报错。`,
Example: ` dws chat group members add-bot --robot-code <robot-code> --id <openconversation_id>
# 查询群 ID: dws chat search --query "群名"
# robot-code: $DINGTALK_CHAT_ROBOT_CODE`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "robot-code", "id"); err != nil {
return err
}
return callMCPToolOnServer("bot", "add_robot_to_group", map[string]any{
"robotCode": mustGetFlag(cmd, "robot-code"),
"openConversationId": mustGetFlag(cmd, "id"),
})
},
}
DeclareLeafMetadata(chatGroupMembersAddBotCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "add_robot_to_group",
CanonicalPath: "chat.add_robot_to_group",
CLIPath: "chat group members add-bot",
PrimaryCLIPath: "chat group members add-bot",
},
Description: "把企业机器人拉进我有管理权限的群",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "bot", RPCName: "add_robot_to_group"},
},
Selection: contract.SelectionSpec{
AgentSummary: "把企业机器人拉进我有管理权限的群",
UseWhen: []string{"需要把已有企业机器人加入指定群"},
AvoidWhen: []string{
"添加普通成员时使用 chat group members add",
"移除群内机器人时使用 chat group members remove-bot",
},
Examples: []string{"dws chat group members add-bot --robot-code <robot-code> --id <openconversation_id>"},
},
Parameters: []contract.ParamDecl{
{Name: "id", Property: "openConversationId"},
},
},
})
chatGroupRenameCmd := &cobra.Command{
Use: "rename",
Short: "更新群名称",
Example: ` dws chat group rename --id <openconversation_id> --name "新群名"
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id", "name"); err != nil {
return err
}
return callMCPTool("update_group_name", map[string]any{
"openconversation_id": mustGetFlag(cmd, "id"),
"group_name": mustGetFlag(cmd, "name"),
})
},
}
DeclareLeafMetadata(chatGroupRenameCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_group_name",
CanonicalPath: "chat.update_group_name",
CLIPath: "chat group rename",
PrimaryCLIPath: "chat group rename",
},
Description: "修改指定群聊的名称",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "update_group_name"},
},
Selection: contract.SelectionSpec{
AgentSummary: "修改指定群聊的名称",
UseWhen: []string{"需要给已有群聊重命名时"},
AvoidWhen: []string{"只修改个人可见备注时不要使用群名称更新"},
Examples: []string{"dws chat group rename --id <openConversationId> --name \"新群名\""},
},
Parameters: []contract.ParamDecl{
{Name: "id", Property: "openconversation_id"},
{Name: "name", Property: "group_name"},
},
},
})
chatGroupMemberAddCmd := &cobra.Command{
Use: "add",
Short: "添加群成员",
Long: `向指定群聊添加成员,需传入群 ID 与用户标识列表。支持 userId 和 openDingTalkId 混传。`,
Example: ` dws chat group members add --id <openconversation_id> --users userId1,userId2
dws chat group members add --id <openconversation_id> --users openDingTalkId1,openDingTalkId2
dws chat group members add --id <openconversation_id> --users userId1,openDingTalkId1
# 查询群 ID: dws chat search --query "群名"
# 查询 userId / openDingTalkId: dws contact user search --query "姓名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id", "users"); err != nil {
return err
}
toolArgs := map[string]any{
"openconversation_id": mustGetFlag(cmd, "id"),
}
appendChatIDArgs(toolArgs, parseCSVValues(mustGetFlag(cmd, "users")), "userId", "openDingtalkIds")
return callMCPTool("add_group_member", toolArgs)
},
}
DeclareLeafMetadata(chatGroupMemberAddCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "add_group_member",
CanonicalPath: "chat.add_group_member",
CLIPath: "chat group members add",
PrimaryCLIPath: "chat group members add",
},
Description: "向指定群聊添加成员",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "add_group_member"},
},
Selection: contract.SelectionSpec{
AgentSummary: "向指定群聊添加成员",
UseWhen: []string{"已知群 ID 和成员 ID,需要邀请成员入群时"},
AvoidWhen: []string{"添加机器人时使用 chat group members add-bot"},
Examples: []string{"dws chat group members add --id <openConversationId> --users userId1,userId2"},
},
Parameters: []contract.ParamDecl{
{Name: "id", Property: "openconversation_id"},
{Name: "users", Property: "userId"},
},
},
})
chatGroupMemberRemoveCmd := &cobra.Command{
Use: "remove",
Short: "移除群成员",
Long: `从指定群聊中移除成员,需传入群 ID 与待移除的用户 ID 列表。`,
Example: ` dws chat group members remove --id <openconversation_id> --users userId1,userId2
# 查询群 ID: dws chat search --query "群名"
# 查询 userId: dws contact user search --query "姓名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id", "users"); err != nil {
return err
}
groupID := mustGetFlag(cmd, "id")
removeValues := parseCSVValues(mustGetFlag(cmd, "users"))
// 群主防护:移出群主会产生无群主的孤儿群,先在客户端拦截。
if err := guardGroupOwnerRemoval(cmd.Context(), groupID, removeValues); err != nil {
return err
}
return callMCPTool("remove_group_member", map[string]any{
"openConversationId": groupID,
"userIdList": removeValues,
})
},
}
DeclareLeafMetadata(chatGroupMemberRemoveCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "remove_group_member",
CanonicalPath: "chat.remove_group_member",
CLIPath: "chat group members remove",
PrimaryCLIPath: "chat group members remove",
},
Description: "从指定群聊移除成员",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "remove_group_member"},
},
Selection: contract.SelectionSpec{
AgentSummary: "从指定群聊移除成员",
UseWhen: []string{"群管理员明确要移除一个或多个普通成员时"},
AvoidWhen: []string{"移除机器人时使用 chat group members remove-bot"},
Examples: []string{"dws chat group members remove --id <openConversationId> --users userId1,userId2"},
},
Parameters: []contract.ParamDecl{
{Name: "id", Property: "openConversationId"},
{Name: "users", Property: "userIdList"},
},
},
})
// ── message 子命令 ────────────────────────────────────────
chatMessageCmd := &cobra.Command{
Use: "message",
Short: "会话消息管理",
Long: `管理会话消息,包括拉取、发送、搜索、转发、钉住、收藏和撤回消息。`,
RunE: groupRunE,
}
chatMessageListCmd := &cobra.Command{
Use: "list",
Short: "拉取会话消息内容",
Long: `拉取指定群聊或单聊的会话消息内容。输出顶层 messages,稳定字段为 messageId 和 text;兼容保留 openMessageId 和 content。--group 指定群聊,--user 指定单聊用户(userId),--open-dingtalk-id 指定单聊用户(openDingTalkId),三者互斥。--time 可选,不传时默认上海时间当前时间并向旧消息拉取。推荐使用 --direction newer/older 控制时间方向:newer 表示从给定时间往现在拉,older 表示从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。引用回复消息会返回 quotedMessage 引用上下文;被引用的原消息是合并转发或图片时,对应的类型与内容也会随引用上下文返回。如果返回的会话消息中包含 openConvThreadId 字段,说明是话题消息,可以调用 dws chat message list-topic-replies 拉取话题回复消息列表,openConvThreadId 作为 topic-id 参数。`,
Example: ` dws chat message list --group <openconversation_id>
dws chat message list --group <openconversation_id> --time "2025-03-01 00:00:00"
dws chat message list --user <userId> --time "2025-03-01 00:00:00" --limit 50
dws chat message list --open-dingtalk-id <openDingTalkId> --time "2025-03-01 00:00:00" --limit 50
dws chat message list --group <openconversation_id> --time "2025-03-01 00:00:00" --direction older
dws chat message list --group <openconversation_id> --time "2025-03-01 00:00:00" --jq '.messages[] | {messageId, text}'
# 查询群 ID: dws chat search --query "群名"
# 查询 userId: dws contact user search --query "姓名"`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
userID, _ := cmd.Flags().GetString("user")
openDingTalkID, _ := cmd.Flags().GetString("open-dingtalk-id")
specified := 0
if groupID != "" {
specified++
}
if userID != "" {
specified++
}
if openDingTalkID != "" {
specified++
}
if specified > 1 {
return fmt.Errorf("--group, --user and --open-dingtalk-id are mutually exclusive, specify exactly one")
}
if specified == 0 {
return fmt.Errorf("--group, --user or --open-dingtalk-id is required")
}
if openDingTalkID != "" {
if err := targetresolver.ValidateExplicitOpenDingTalkID("--open-dingtalk-id", openDingTalkID); err != nil {
return err
}
}
if userID != "" && isOpenDingTalkID(userID) {
openDingTalkID = userID
userID = ""
}
timeVal := mustGetFlag(cmd, "time")
defaultForward := true
if timeVal == "" {
timeVal = defaultChatMessageListTime()
defaultForward = false
}
forward, err := resolveMessageForward(cmd, defaultForward)
if err != nil {
return err
}
if groupID != "" {
toolArgs := map[string]any{
"openconversation_id": groupID,
"time": timeVal,
"forward": forward,
}
if v := chatIntFlagOrFallback(cmd, "limit", "size"); v > 0 {
toolArgs["limit"] = v
}
return callProjectedChatMessages(cmd, "list_conversation_message_v2", toolArgs, false)
}
toolArgs := map[string]any{
"time": timeVal,
"forward": forward,
}
if userID != "" {
toolArgs["userId"] = userID
} else {
toolArgs["openDingTalkId"] = openDingTalkID
}
if v := chatIntFlagOrFallback(cmd, "limit", "size"); v > 0 {
toolArgs["limit"] = v
}
return callProjectedChatMessages(cmd, "list_individual_chat_message", toolArgs, false)
},
}
DeclareLeafMetadata(chatMessageListCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_conversation_message_v2",
CanonicalPath: "chat.list_conversation_message_v2",
CLIPath: "chat message list",
PrimaryCLIPath: "chat message list",
},
Description: "分页读取指定会话消息及其引用上下文",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "list_conversation_message_v2"},
},
Selection: contract.SelectionSpec{
AgentSummary: "分页读取指定会话消息及其引用上下文",
UseWhen: []string{"用户明确指定某个会话,并要读取消息或追溯引用回复中的原消息上下文时"},
AvoidWhen: []string{"跨全部会话按时间查询时使用 chat message list-all"},
Examples: []string{
"dws chat message list --group <openConversationId> --limit 50",
"dws chat message list --group <openConversationId> --time \"2026-07-01 00:00:00\" --limit 50 --jq '.messages[] | {messageId, text}'",
},
},
Parameters: []contract.ParamDecl{
{Name: "direction", Property: "forward"},
{Name: "group", Property: "openconversation_id"},
},
},
})
chatMessageListDirectCmd := &cobra.Command{
Use: "list-direct",
Short: "拉取单聊会话消息",
Hidden: true,
Long: `按对方 userId 或 openDingTalkId 拉取单聊会话消息。`,
Example: ` dws chat message list-direct --user <对方userId> --time "2026-04-01 00:00:00" --forward true --limit 50
dws chat message list-direct --open-dingtalk-id <openDingTalkId> --time "2026-04-01 00:00:00" --forward false --limit 20
# 查询 userId / openDingTalkId: dws contact user search --query "姓名"`,
RunE: func(cmd *cobra.Command, args []string) error {
userID, _ := cmd.Flags().GetString("user")
openDingTalkID, _ := cmd.Flags().GetString("open-dingtalk-id")
if userID != "" && openDingTalkID != "" {
return fmt.Errorf("--user and --open-dingtalk-id are mutually exclusive")
}
if userID == "" && openDingTalkID == "" {
return fmt.Errorf("--user or --open-dingtalk-id is required")
}
if openDingTalkID != "" {
if err := targetresolver.ValidateExplicitOpenDingTalkID("--open-dingtalk-id", openDingTalkID); err != nil {
return err
}
}
if userID != "" && isOpenDingTalkID(userID) {
openDingTalkID = userID
userID = ""
}
timeVal, _ := cmd.Flags().GetString("time")
defaultForward := true
if strings.TrimSpace(timeVal) == "" {
timeVal = defaultChatMessageListTime()
defaultForward = false
}
forward, err := resolveMessageForward(cmd, defaultForward)
if err != nil {
return err
}
toolArgs := map[string]any{
"time": timeVal,
"forward": forward,
}
if userID != "" {
toolArgs["userId"] = userID
} else {
toolArgs["openDingTalkId"] = openDingTalkID
}
if v, err := cmd.Flags().GetInt("limit"); err == nil && v > 0 {
toolArgs["limit"] = v
}
return callMCPTool("list_individual_chat_message", toolArgs)
},
}
DeclareLeafMetadata(chatMessageListDirectCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_individual_chat_message",
CanonicalPath: "chat.list_individual_chat_message",
CLIPath: "chat message list-direct",
PrimaryCLIPath: "chat message list-direct",
},
Description: "读取与指定用户的单聊消息记录",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "list_individual_chat_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "读取与指定用户的单聊消息记录",
UseWhen: []string{"用户明确要求查看和某人的一对一聊天时"},
AvoidWhen: []string{"跨群聊按发送者查询时使用 chat message list-by-sender"},
Examples: []string{"dws chat message list-direct --user <userId> --time \"2026-07-01 00:00:00\" --direction newer --limit 50"},
},
Parameters: []contract.ParamDecl{
{Name: "direction", Property: "forward"},
{Name: "open-dingtalk-id", Property: "openDingTalkId"},
{Name: "user", Property: "userId"},
},
},
})
chatMessageSendCmd := &cobra.Command{
Use: "send",
Short: "以当前用户身份发送消息(--group 群聊 / --user 或 --open-dingtalk-id 单聊)",
Long: `以当前用户身份发送消息。
⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。
目标选择(三选一,必填):
--group 群聊 openconversation_id
--user 单聊接收人 userId
--open-dingtalk-id 单聊接收人 openDingTalkId
纯文本 / Markdown 消息(默认):
无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。
图文混排时,公网图片 URL 需要写成 Markdown 图片语法:![图片标题](https://example.com/image.png),才会以内联图片展示。
如果省略开头的 !,例如 [图片标题](https://example.com/image.png),将按链接/URL 展示,不会渲染为图片。
返回值与后续操作:
发送后会返回 openTaskId。如需编辑或撤回刚发送的消息,使用
dws chat message query-send-status --open-task-id <openTaskId>
获取 openMessageId 和 openConversationId,无需再按消息内容反查。
本地图片 / 文件消息:
统一使用 --msg-type file --file-path <本地路径>。CLI 会完成上传并按 file 消息发送;
.png/.jpg 也会显示为可下载的文件附件,不会生成 mediaId 或渲染成内联图片。
旧版内联图片消息:
仅当上游已经提供有效 mediaId 时,使用 --msg-type image --media-id。
当前 CLI 不提供本地文件到 mediaId 的上传能力。`,
Example: ` dws chat message send --group <openconversation_id> "hello"
dws chat message send --user <userId> "请查收"
dws chat message send --open-dingtalk-id <openDingTalkId> "请查收"
dws chat message send --group <openconversation_id> --title "周报提醒" "请大家本周五前提交周报"
# 图文混排 Markdown:公网图片 URL 需要写成 ![图片标题](URL) 才会以内联图片展示
dws chat message send --group <openconversation_id> --text $'这是图文说明\n\n![这个是展示图片标题](https://down.dingtalk.com/media/lQLPM5jiBEiBNjswMLAKd_CTzm8eowpEWPT_7-cA_48_48.png)'
# 发送本地图片或文件(图片会作为可下载的 file 附件发送)
dws chat message send --group <openconversation_id> --msg-type file --file-path ./screenshot.png
dws chat message send --group <openconversation_id> --msg-type file --file-path ./report.pdf
# 发送本地音频/视频(audio/video 是 file 的语义别名)
dws chat message send --group <openconversation_id> --msg-type audio --file-path ./recording.mp3
dws chat message send --group <openconversation_id> --msg-type video --file-path ./demo.mp4
# 旧版内联图片:仅当上游已经持有有效 mediaId 时使用
dws chat message send --group <openconversation_id> --msg-type image --media-id <mediaId>
# 查询群 ID: dws chat search --query "群名"
# 查询用户 ID: dws contact user search --query "姓名"`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
userID, _ := cmd.Flags().GetString("user")
openDingTalkID, _ := cmd.Flags().GetString("open-dingtalk-id")
msgUuid := flagOrFallback(cmd, "idempotency-key", "uuid")
specified := 0
if groupID != "" {
specified++
}
if userID != "" {
specified++
}
if openDingTalkID != "" {
specified++
}
if specified > 1 {
return fmt.Errorf("--group, --user and --open-dingtalk-id are mutually exclusive, specify exactly one")
}
if specified == 0 {
return fmt.Errorf("--group, --user or --open-dingtalk-id is required")
}
if openDingTalkID != "" {
if err := targetresolver.ValidateExplicitOpenDingTalkID("--open-dingtalk-id", openDingTalkID); err != nil {
return err
}
}
if userID != "" && isOpenDingTalkID(userID) {
openDingTalkID = userID
userID = ""
}
// 数字 userId 尝试 lookup 转换为 openDingTalkId,让所有消息类型(文本/媒体)都走 openDingTalkId 路径。
// 真实后端的 send_personal_message 单聊路径稳定接受 receiverOpenDingTalkId;
// userId/uid 直传会被服务端判定为空,因此解析失败时直接返回明确错误。
if userID != "" {
resolved, err := resolveOpenDingTalkID(cmd.Context(), userID)
if err != nil {
return fmt.Errorf("cannot resolve --user %q to openDingTalkId: %w; pass --open-dingtalk-id instead", userID, err)
} else {
if commandBoolFlag(cmd, "debug") || commandBoolFlag(cmd, "verbose") {
fmt.Fprintf(os.Stderr, "[debug] resolved userID=%q to openDingTalkId=%q\n", userID, resolved)
}
openDingTalkID = resolved
userID = ""
}
}
if commandBoolFlag(cmd, "debug") || commandBoolFlag(cmd, "verbose") {
fmt.Fprintf(os.Stderr, "[debug] message send after normalization: groupID=%q userID=%q openDingTalkID=%q\n", groupID, userID, openDingTalkID)
}
mediaId, _ := cmd.Flags().GetString("media-id")
msgType, _ := cmd.Flags().GetString("msg-type")
clawType := ""
aiTag, _ := cmd.Flags().GetBool("ai-tag")
if aiTag {
clawType = edition.ClawType()
}
// ── 富媒体消息(image/audio/video/file) ──
// text/markdown 透传到下方的文本消息分支,避免模型填 --msg-type text 报 unsupported
if msgType == "text" || msgType == "markdown" {
msgType = ""
}
if msgType != "" {
var contentJSON string
serviceMsgType := msgType
switch msgType {
case "image":
if mediaId == "" {
return fmt.Errorf("--media-id is required for msgType=image")
}
contentJSON = fmt.Sprintf(`{"mediaId":"%s"}`, mediaId)
case "file", "audio", "video":
serviceMsgType = "file"
filePath, _ := cmd.Flags().GetString("file-path")
dentryId, _ := cmd.Flags().GetInt64("dentry-id")
spaceId, _ := cmd.Flags().GetInt64("space-id")
if (dentryId == 0) != (spaceId == 0) {
return fmt.Errorf("--dentry-id and --space-id must be specified together")
}
if filePath != "" {
meta, err := buildConversationLocalFileMeta(filePath, "", "")
if err == nil && dentryId != 0 && spaceId != 0 {
contentJSON, _ = buildConversationFileContent(dentryId, spaceId, meta)
} else if err == nil {
targetArgs, _ := buildConversationTargetArgs(cmd)
if deps.Caller.DryRun() {
deps.Out.PrintKeyValue("操作", "上传本地文件并发送 file 消息")
deps.Out.PrintKeyValue("文件", meta.LocalPath)
deps.Out.PrintKeyValue("名称", meta.FileName)
deps.Out.PrintKeyValue("大小", fmt.Sprintf("%d bytes", meta.FileSize))
return nil
}
ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Minute)
defer cancel()
commitText, err := uploadConversationLocalFile(ctx, targetArgs, meta, "")
if err != nil {
return err
}
dentryId, spaceId, err = parseConversationFileSendIDs(commitText)
if err != nil {
return err
}
contentJSON, _ = buildConversationFileContent(dentryId, spaceId, meta)
} else if dentryId == 0 || spaceId == 0 {
return fmt.Errorf("--file-path must be a readable local file, or pass legacy --dentry-id and --space-id: %w", err)
}
}
if contentJSON == "" {
fileName, _ := cmd.Flags().GetString("file-name")
fileType, _ := cmd.Flags().GetString("file-type")
fileSize, _ := cmd.Flags().GetInt64("file-size")
if dentryId == 0 || spaceId == 0 || fileName == "" {
return fmt.Errorf("readable local --file-path is required for msgType=file; legacy flags --dentry-id, --space-id, --file-name are still supported")
}
contentJSON = fmt.Sprintf(`{"dentryId":%d,"spaceId":%d,"fileName":"%s","fileType":"%s","filePath":"%s","fileSize":%d}`,
dentryId, spaceId, fileName, fileType, filePath, fileSize)
}
case "location":
latitude, _ := cmd.Flags().GetString("latitude")
longitude, _ := cmd.Flags().GetString("longitude")
locationName, _ := cmd.Flags().GetString("location-name")
mapThumbnailUrl, _ := cmd.Flags().GetString("map-thumbnail-url")
if latitude == "" || longitude == "" || locationName == "" || mapThumbnailUrl == "" {
return fmt.Errorf("--latitude, --longitude, --location-name, --map-thumbnail-url are all required for msgType=location")
}
contentJSON = fmt.Sprintf(`{"locationName":"%s","longitude":"%s","latitude":"%s","mapThumbnailUrl":"%s"}`, locationName, longitude, latitude, mapThumbnailUrl)
case "profile":
contactID, _ := cmd.Flags().GetString("contact-id")
if contactID == "" {
return fmt.Errorf("--contact-id is required for msgType=profile")
}
contentJSON = fmt.Sprintf(`{"openDingTalkId":"%s"}`, contactID)
default:
return fmt.Errorf("unsupported --msg-type: %s (supported: image, file, audio, video, location, profile)", msgType)
}
params := map[string]any{
"msgType": serviceMsgType,
"content": contentJSON,
"clawType": clawType,
}
if groupID != "" {
params["openConversationId"] = groupID
} else {
params["receiverOpenDingTalkId"] = openDingTalkID
}
if msgUuid != "" {
params["uuid"] = msgUuid
}
return callMCPTool("send_personal_message", params)
}
// ── 文本/Markdown 消息 ──
text := flagOrFallback(cmd, "text", "content", "body", "message", "markdown")
if text == "" && len(args) > 0 {
text = args[0]
}
if text == "" {
return fmt.Errorf("message content required (use --text or positional arg, or --media-id for image)")
}
title, _ := cmd.Flags().GetString("title")
if title == "" {
title = sanitizeTitleFromText(text)
}
if groupID != "" {
atAll, _ := cmd.Flags().GetBool("at-all")
atOpenIdsStr, _ := cmd.Flags().GetString("at-open-dingtalk-ids")
// 群聊统一走 openDingTalkId @ 人接口。
newParams := map[string]any{
"openConversationId": groupID,
"msgType": "markdown",
"clawType": clawType,
}
text = applyCurrentUserGroupMentions(newParams, text, atOpenIdsStr, atAll)
contentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
newParams["content"] = string(contentJSON)
if msgUuid != "" {
newParams["uuid"] = msgUuid
}
return callMCPTool("send_personal_message", newParams)
}
// 单聊:统一走 openDingTalkId
directContentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
newDirectParams := map[string]any{
"receiverOpenDingTalkId": openDingTalkID,
"msgType": "markdown",
"content": string(directContentJSON),
"clawType": clawType,
}
if msgUuid != "" {
newDirectParams["uuid"] = msgUuid
}
return callMCPTool("send_personal_message", newDirectParams)
},
}
DeclareLeafMetadata(chatMessageSendCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "send_personal_message",
CanonicalPath: "chat.send_personal_message",
CLIPath: "chat message send",
PrimaryCLIPath: "chat message send",
},
Description: "以当前用户身份发送群聊或单聊消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "send_personal_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "以当前用户身份发送群聊或单聊消息",
UseWhen: []string{"用户明确要以个人身份发送文本或媒体消息时;响应返回 openTaskId 后用 chat message query-send-status 确认投递并取得后续操作所需的消息 ID"},
AvoidWhen: []string{"机器人身份或 Webhook 发送应使用对应命令"},
Examples: []string{"dws chat message send --group <openConversationId> \"项目已更新\""},
},
Parameters: []contract.ParamDecl{
{Name: "ai-tag", Property: "clawType", InterfaceType: "string"},
{Name: "at-open-dingtalk-ids", Property: "atOpenDingTalkIds"},
{Name: "group", Property: "openConversationId"},
{Name: "idempotency-key", Property: "uuid"},
{Name: "open-dingtalk-id", Property: "receiverOpenDingTalkId"},
},
},
})
// send-by-bot: 群聊传 --group,单聊传 --users/--open-dingtalk-ids。
// Markdown 使用 --title/--text,图片使用 --msg-type image/--image-url,
// 文件使用 --msg-type file/--file-path,CLI 上传后发送。
chatMessageSendByBotCmd := &cobra.Command{
Use: "send-by-bot",
Short: "机器人发送消息(--group 群聊 / --users 单聊)",
Long: `群聊:传 --group 指定群;单聊:传 --users 或 --open-dingtalk-ids 指定用户列表,与 --group 只能选其一,不能同时指定。省略 --msg-type 时发送 Markdown;图片使用 --msg-type image --image-url;本地文件使用 --msg-type file --file-path,CLI 完成上传后发送。
⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。`,
Example: ` dws chat message send-by-bot --robot-code <robot-code> --group <openconversation_id> --title "日报" --text "## 今日完成..."
dws chat message send-by-bot --robot-code <robot-code> --group <openconversation_id> --msg-type image --image-url "https://example.com/image.png"
dws chat message send-by-bot --robot-code <robot-code> --group <openconversation_id> --msg-type file --file-path ./report.pdf
dws chat message send-by-bot --robot-code <robot-code> --users userId1,userId2 --title "提醒" --text "请提交周报"
dws chat message send-by-bot --robot-code <robot-code> --open-dingtalk-ids openDingtalkId1,openDingtalkId2 --title "提醒" --text "请提交周报"
dws chat message send-by-bot --robot-code <robot-code> --group <openconversation_id> --at-user-ids userId1,userId2 --title "提醒" --text "@userId1 @userId2 请查收本周报告"
dws chat message send-by-bot --robot-code <robot-code> --group <openconversation_id> --at-open-dingtalk-ids openDingtalkId1,openDingtalkId2 --title "提醒" --text "@openDingtalkId1 @openDingtalkId2 请查收本周报告"
dws chat message send-by-bot --robot-code <robot-code> --group <openconversation_id> --at-all --title "通知" --text "请所有人注意"
# 查询群 ID: dws chat search --query "群名"
# 查询 userId: dws contact user search --query "姓名"
# robot-code: $DINGTALK_CHAT_ROBOT_CODE`,
RunE: func(cmd *cobra.Command, args []string) error {
msgType := strings.TrimSpace(mustGetFlag(cmd, "msg-type"))
title := mustGetFlag(cmd, "title")
text := mustGetFlag(cmd, "text")
imageURL := strings.TrimSpace(mustGetFlag(cmd, "image-url"))
filePath := mustGetFlag(cmd, "file-path")
if err := validateRequiredFlags(cmd, "robot-code"); err != nil {
return err
}
if msgType == "" {
if imageURL != "" {
return fmt.Errorf("--msg-type image is required when using --image-url")
}
if filePath != "" {
return fmt.Errorf("--msg-type file is required when using --file-path")
}
msgType = "markdown"
}
switch msgType {
case "markdown":
if err := validateRequiredFlags(cmd, "title", "text"); err != nil {
return err
}
case "image":
if imageURL == "" {
return fmt.Errorf("--image-url is required for --msg-type image")
}
case "file":
if filePath == "" {
return fmt.Errorf("--file-path is required for --msg-type file")
}
default:
return fmt.Errorf("unsupported --msg-type %q, must be one of: markdown, image, file", msgType)
}
isMarkdownMessage := msgType == "markdown"
chatID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
usersStr, _ := cmd.Flags().GetString("users")
openDingtalkIdsStr, _ := cmd.Flags().GetString("open-dingtalk-ids")
hasDirectTarget := usersStr != "" || openDingtalkIdsStr != ""
if chatID != "" && hasDirectTarget {
return fmt.Errorf("--group and --users/--open-dingtalk-ids are mutually exclusive")
}
if chatID == "" && !hasDirectTarget {
return fmt.Errorf("--group or --users/--open-dingtalk-ids is required")
}
userIDs := splitCommaList(usersStr)
openDingTalkIDs := splitCommaList(openDingtalkIdsStr)
fileURL := ""
if msgType == "file" {
var err error
fileURL, err = uploadRobotMessageLocalFile(
cmd, filePath, chatID, userIDs, openDingTalkIDs)
if err != nil {
return err
}
if deps.Caller.DryRun() {
return nil
}
}
buildRobotMessageArgs := func(markdown string) map[string]any {
toolArgs := map[string]any{
"robotCode": mustGetFlag(cmd, "robot-code"),
}
switch msgType {
case "image":
toolArgs["photoURL"] = imageURL
case "file":
toolArgs["fileUrl"] = fileURL
default:
toolArgs["title"] = title
toolArgs["markdown"] = markdown
}
return toolArgs
}
if chatID != "" {
var atUserIds []string
if atUserIdsStr, _ := cmd.Flags().GetString("at-user-ids"); atUserIdsStr != "" {
for _, id := range strings.Split(atUserIdsStr, ",") {
if s := strings.TrimSpace(id); s != "" {
atUserIds = append(atUserIds, s)
}
}
}
var atOpenDingtalkIds []string
if atOpenDingtalkIdsStr, _ := cmd.Flags().GetString("at-open-dingtalk-ids"); atOpenDingtalkIdsStr != "" {
for _, id := range strings.Split(atOpenDingtalkIdsStr, ",") {
if s := strings.TrimSpace(id); s != "" {
atOpenDingtalkIds = append(atOpenDingtalkIds, s)
}
}
}
markdown := text
if isMarkdownMessage {
// 机器人发消息要求 @ 占位符为裸 @id;模型若写成 <@id> 会导致 @ 不生效,主动剥离尖括号
markdown = normalizeAtPlaceholders(markdown, atUserIds, false)
markdown = normalizeAtPlaceholders(markdown, atOpenDingtalkIds, false)
markdown = strings.ReplaceAll(markdown, "<@all>", "@all")
}
toolArgs := buildRobotMessageArgs(markdown)
switch msgType {
case "image":
toolArgs["msgKey"] = "sampleImageMsg"
case "file":
toolArgs["msgKey"] = "sampleDingtalkDriveFile"
default:
toolArgs["msgKey"] = "sampleMarkdownDX"
}
toolArgs["openConversationId"] = chatID
if len(atUserIds) > 0 {
toolArgs["atUserIds"] = atUserIds
}
if len(atOpenDingtalkIds) > 0 {
toolArgs["atOpendingtalkIds"] = atOpenDingtalkIds
}
if isAtAll, _ := cmd.Flags().GetBool("at-all"); isAtAll {
toolArgs["isAtAll"] = "true"
}
return callMCPToolOnServer("bot", "send_robot_group_message", toolArgs)
}
toolArgs := buildRobotMessageArgs(text)
switch msgType {
case "image":
toolArgs["msgType"] = "sampleImageMsg"
case "file":
toolArgs["msgType"] = "sampleDingtalkDriveFile"
default:
toolArgs["msgType"] = "sampleMarkdownDX"
}
if len(userIDs) > 0 {
toolArgs["userIds"] = userIDs
}
if len(openDingTalkIDs) > 0 {
toolArgs["openDingtalkIds"] = openDingTalkIDs
}
if isAtAll, _ := cmd.Flags().GetBool("at-all"); isAtAll {
toolArgs["isAtAll"] = "true"
}
return callMCPToolOnServer("bot", "batch_send_robot_msg_to_users", toolArgs)
},
}
DeclareLeafMetadata(chatMessageSendByBotCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "send_robot_message",
CanonicalPath: "chat.send_robot_message",
CLIPath: "chat message send-by-bot",
PrimaryCLIPath: "chat message send-by-bot",
},
Description: "以应用机器人身份发送 Markdown、图片或文件群消息或批量单聊",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "命令包含多个 RPC、条件分派或本地 HTTP/文件步骤,不能绑定为单一 interface_ref",
},
Selection: contract.SelectionSpec{
AgentSummary: "以应用机器人身份发送 Markdown、图片或文件群消息或批量单聊",
UseWhen: []string{"已有 robotCode 且需要机器人身份投递 Markdown、图片或文件时"},
AvoidWhen: []string{"个人身份发送或自定义 Webhook 告警不要使用"},
Examples: []string{"dws chat message send-by-bot --robot-code <robotCode> --group <openConversationId> --title \"日报\" --text \"今日进展\""},
},
// Keep title/text required_when out of Schema: the runtime switch above
// enforces Markdown inputs, while adding it breaks merge-base compatibility.
Parameters: []contract.ParamDecl{
{Name: "msg-type", RequiredWhen: "image-url or file-path is provided", Enum: []string{"markdown", "image", "file"}},
{Name: "image-url", RequiredWhen: "msg-type is image"},
{Name: "file-path", RequiredWhen: "msg-type is file"},
},
},
})
// recall-by-bot: 传 --group 为群聊撤回,不传为单聊撤回
chatMessageRecallByBotCmd := &cobra.Command{
Use: "recall-by-bot",
Short: "机器人撤回消息(--group 群聊 / 不传为单聊)",
Long: `群聊:传 --group 与 --keys;单聊:仅传 --keys。--keys 为发送时返回的 processQueryKey 列表,逗号分隔。`,
Example: ` dws chat message recall-by-bot --robot-code <robot-code> --group <openconversation_id> --keys <process-query-key>
dws chat message recall-by-bot --robot-code <robot-code> --keys key1,key2
# 查询群 ID: dws chat search --query "群名"
# robot-code: $DINGTALK_CHAT_ROBOT_CODE`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "robot-code", "keys"); err != nil {
return err
}
keysStr := mustGetFlag(cmd, "keys")
var processQueryKeys []string
for _, k := range strings.Split(keysStr, ",") {
if s := strings.TrimSpace(k); s != "" {
processQueryKeys = append(processQueryKeys, s)
}
}
chatID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
if chatID != "" {
return callMCPToolOnServer("bot", "recall_robot_group_message", map[string]any{
"robotCode": mustGetFlag(cmd, "robot-code"),
"openConversationId": chatID,
"processQueryKeys": processQueryKeys,
})
}
return callMCPToolOnServer("bot", "batch_recall_robot_users_msg", map[string]any{
"robotCode": mustGetFlag(cmd, "robot-code"),
"processQueryKeys": processQueryKeys,
})
},
}
DeclareLeafMetadata(chatMessageRecallByBotCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "recall_robot_message",
CanonicalPath: "chat.recall_robot_message",
CLIPath: "chat message recall-by-bot",
PrimaryCLIPath: "chat message recall-by-bot",
},
Description: "撤回指定机器人发送的消息",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "命令包含多个 RPC、条件分派或本地 HTTP/文件步骤,不能绑定为单一 interface_ref",
},
Selection: contract.SelectionSpec{
AgentSummary: "撤回指定机器人发送的消息",
UseWhen: []string{"持有 robotCode 和发送结果 key,需要撤回机器人消息时"},
AvoidWhen: []string{"个人身份消息撤回使用 chat message recall"},
Examples: []string{"dws chat message recall-by-bot --robot-code <robotCode> --group <openConversationId> --keys <processQueryKey>"},
},
},
})
chatMessageSendByWebhookCmd := &cobra.Command{
Use: "send-by-webhook",
Short: "自定义机器人 Webhook 发送群消息",
Long: `通过自定义机器人 Webhook 发送群消息。@ 人时需在 --text 中包含 @userId 或 @手机号,否则 @ 不生效。
⚠️ 重要:该接口会真实发送消息到目标群聊,不可用于测试或试探性调用。调用前必须确认消息内容无误。`,
Example: ` dws chat message send-by-webhook --token <webhook-token> --title "告警" --text "CPU 超 90%" --at-all
dws chat message send-by-webhook --token <webhook-token> --title "test" --text "hi @118785" --at-users 118785`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "token", "title", "text"); err != nil {
return err
}
toolArgs := map[string]any{
"robotToken": mustGetFlag(cmd, "token"),
"title": mustGetFlag(cmd, "title"),
"text": mustGetFlag(cmd, "text"),
}
if v, _ := cmd.Flags().GetBool("at-all"); v {
toolArgs["isAtAll"] = true
}
if v, _ := cmd.Flags().GetString("at-mobiles"); v != "" {
var mobiles []string
for _, m := range strings.Split(v, ",") {
if s := strings.TrimSpace(m); s != "" {
mobiles = append(mobiles, s)
}
}
toolArgs["atMobiles"] = mobiles
}
if v, _ := cmd.Flags().GetString("at-users"); v != "" {
var atUserIds []string
for _, u := range strings.Split(v, ",") {
if s := strings.TrimSpace(u); s != "" {
atUserIds = append(atUserIds, s)
}
}
toolArgs["atUserIds"] = atUserIds
}
// dry-run 只预览参数,不实际发送,交回标准调用路径。
if deps.Caller.DryRun() {
return callMCPToolOnServer("bot", "send_message_by_custom_robot", toolArgs)
}
// 自定义机器人 webhook 即使发送失败(如 errcode=300005 token 不存在)
// HTTP 仍是 200,会被包成 success:true。这里取原始响应显式识别 errcode,
// 非 0 时按失败返回,避免 agent 误判消息已发出。
raw, err := callMCPToolReturnTextOnServer(context.Background(), "bot", "send_message_by_custom_robot", toolArgs)
if err != nil {
return err
}
if code, msg, failed := webhookErrcodeFailure(raw); failed {
return &CLIError{
Code: CodeMCPToolError,
Message: fmt.Sprintf("自定义机器人 webhook 发送失败: errcode=%s errmsg=%s", code, msg),
Suggestion: "检查 --token 是否有效、机器人是否仍在群内、以及机器人安全设置(关键词/IP/加签)是否拦截",
}
}
var parsed any
if json.Unmarshal([]byte(raw), &parsed) == nil {
return deps.Out.PrintJSON(parsed)
}
deps.Out.PrintRaw(raw)
return nil
},
}
DeclareLeafMetadata(chatMessageSendByWebhookCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "send_message_by_custom_robot",
CanonicalPath: "chat.send_message_by_custom_robot",
CLIPath: "chat message send-by-webhook",
PrimaryCLIPath: "chat message send-by-webhook",
},
Description: "用自定义机器人 Webhook 向群发送消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "bot", RPCName: "send_message_by_custom_robot"},
},
Selection: contract.SelectionSpec{
AgentSummary: "用自定义机器人 Webhook 向群发送消息",
UseWhen: []string{"已有自定义机器人 webhook token,需要向群发告警或通知"},
AvoidWhen: []string{
"使用企业机器人 robot-code 发消息时用 chat message send-by-bot",
"以个人身份发消息时用 chat message send",
},
Examples: []string{"dws chat message send-by-webhook --token <webhook-token> --title \"告警\" --text \"CPU 超 90%\" --at-all"},
},
Parameters: []contract.ParamDecl{
{Name: "at-all", Property: "isAtAll"},
{Name: "at-users", Property: "atUserIds"},
{Name: "token", Property: "robotToken"},
},
},
})
chatMessageListTopicRepliesCmd := &cobra.Command{
Use: "list-topic-replies",
Short: "拉取群话题回复消息列表",
Long: `查询指定群聊中某条话题消息的全部回复。--group 指定群会话 ID,--topic-id 指定话题 ID(由 dws chat message list 返回)。`,
Example: ` dws chat message list-topic-replies --group <openconversation_id> --topic-id <topicId>
dws chat message list-topic-replies --group <openconversation_id> --topic-id <topicId> --time "2025-03-01 00:00:00" --limit 20
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "group", "conversation-id", "id", "chat"); err != nil {
return err
}
if err := validateRequiredFlags(cmd, "topic-id"); err != nil {
return err
}
toolArgs := map[string]any{
"openconversationId": flagOrFallback(cmd, "group", "conversation-id", "id", "chat"),
"topicId": mustGetFlag(cmd, "topic-id"),
}
if v, _ := cmd.Flags().GetString("time"); v != "" {
toolArgs["startTime"] = v
}
if v, err := cmd.Flags().GetInt("limit"); err == nil && v > 0 {
toolArgs["pageSize"] = v
}
forward, err := resolveMessageForward(cmd, false)
if err != nil {
return err
}
toolArgs["forward"] = forward
return callMCPTool("list_topic_replies", toolArgs)
},
}
DeclareLeafMetadata(chatMessageListTopicRepliesCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_topic_replies",
CanonicalPath: "chat.list_topic_replies",
CLIPath: "chat message list-topic-replies",
PrimaryCLIPath: "chat message list-topic-replies",
},
Description: "分页读取指定话题的回复",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "list_topic_replies"},
},
Selection: contract.SelectionSpec{
AgentSummary: "分页读取指定话题的回复",
UseWhen: []string{"已知话题 ID 并需要查看回复串时"},
AvoidWhen: []string{"读取普通会话消息时使用 chat message list"},
Examples: []string{"dws chat message list-topic-replies --group <openConversationId> --topic-id <topicId> --limit 50"},
},
Parameters: []contract.ParamDecl{
{Name: "direction", Property: "forward"},
{Name: "group", Property: "openconversationId"},
{Name: "limit", Property: "pageSize"},
{Name: "time", Property: "startTime"},
},
},
})
chatMessageListAllCmd := &cobra.Command{
Use: "list-all",
Short: "拉取指定时间范围内当前用户的所有会话消息",
Long: `分页拉取当前登录用户在指定时间范围内的所有会话消息。--start 和 --end 可选,不传时默认最近 1 天到当前时间,避免跨全部会话范围过大;--limit 指定每页数量,--cursor 传分页游标(首页传 0)。服务端按 cursor 分页返回,hasMore=true 时用返回的 nextCursor 值继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。如果当前账号没有消息搜索权益,CLI 会保留服务端返回的友好提示与开通入口;不要把权限错误解释为时间范围内没有消息。`,
Example: ` dws chat message list-all --start "2025-03-01 00:00:00" --end "2025-03-31 23:59:59" --limit 50
dws chat message list-all --start "2025-03-01 00:00:00" --end "2025-03-31 23:59:59" --limit 50 --cursor "abc123token"
dws chat message list-all --start "2025-03-01 00:00:00" --end "2025-03-31 23:59:59" --limit 100 --page-all --page-limit 20 --max-items 500 --page-delay 0`,
RunE: func(cmd *cobra.Command, args []string) error {
return RunPagedMCPCommand(cmd, pagedChatConversationMessagesConfig(
"search_messages_by_time_range", chatMessageListAllArgs))
},
}
DeclareLeafMetadata(chatMessageListAllCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "search_messages_by_time_range",
CanonicalPath: "chat.search_messages_by_time_range",
CLIPath: "chat message list-all",
PrimaryCLIPath: "chat message list-all",
},
Description: "按时间范围搜索跨会话消息并保留权益指引",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "search_messages_by_time_range"},
},
Selection: contract.SelectionSpec{
AgentSummary: "按时间范围搜索跨会话消息并保留权益指引",
UseWhen: []string{"需要汇总一段时间内所有可见会话消息时"},
AvoidWhen: []string{"已指定单个会话时优先使用 chat message list"},
Examples: []string{
"dws chat message list-all --start \"2026-07-01 00:00:00\" --end \"2026-07-02 00:00:00\" --limit 50",
"dws chat message list-all --start \"2026-07-01 00:00:00\" --end \"2026-07-02 00:00:00\" --limit 100 --page-all --page-limit 20",
},
},
Parameters: append([]contract.ParamDecl{
{Name: "end", Property: "endTime"},
{Name: "start", Property: "startTime"},
}, pagedMCPParamDecls()...),
},
})
chatMessageListBySenderCmd := &cobra.Command{
Use: "list-by-sender",
Short: "拉取指定发送者的消息(包含单聊和群聊)",
Long: `搜索特定人发送给我的消息,返回结果包含单聊和群聊标识。--sender-user-id 指定发送者 userId,--sender-open-dingtalk-id 指定发送者 openDingTalkId,二者互斥。--start 和 --end 可选,不传时默认最近 7 天到当前时间。分页参数 --limit(默认 50)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`,
Example: ` dws chat message list-by-sender --sender-user-id <userId> --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor 0
dws chat message list-by-sender --sender-open-dingtalk-id <openDingTalkId> --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor 0
dws chat message list-by-sender --sender-user-id <userId> --start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" --limit 20 --cursor 0
dws chat message list-by-sender --sender-open-dingtalk-id <openDingTalkId> --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor <nextCursor>
dws chat message list-by-sender --sender-user-id <userId> --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --page-all --page-limit 10 --page-delay 0
# 查询 userId: dws contact user search --query "姓名"
# 查询 openDingTalkId: dws contact user search --query "姓名"`,
RunE: func(cmd *cobra.Command, args []string) error {
return RunPagedMCPCommand(cmd, pagedChatConversationMessagesConfig(
"search_messages_by_sender", chatMessageListBySenderArgs))
},
}
DeclareLeafMetadata(chatMessageListBySenderCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "search_messages_by_sender",
CanonicalPath: "chat.search_messages_by_sender",
CLIPath: "chat message list-by-sender",
PrimaryCLIPath: "chat message list-by-sender",
},
Description: "按发送者和时间范围查询消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "search_messages_by_sender"},
},
Selection: contract.SelectionSpec{
AgentSummary: "按发送者和时间范围查询消息",
UseWhen: []string{"需要查某人发送过的消息且不限定单聊时"},
AvoidWhen: []string{"明确查询与某人的单聊记录时使用 chat message list-direct"},
Examples: []string{
"dws chat message list-by-sender --sender-user-id <userId> --start \"2026-07-01T00:00:00+08:00\" --end \"2026-07-02T00:00:00+08:00\" --limit 50",
"dws chat message list-by-sender --sender-user-id <userId> --start \"2026-07-01T00:00:00+08:00\" --end \"2026-07-02T00:00:00+08:00\" --limit 50 --page-all --page-limit 10",
},
},
Parameters: append([]contract.ParamDecl{
{Name: "end", Property: "endTime"},
{Name: "sender-open-dingtalk-id", Property: "senderOpenDingTalkId"},
{Name: "start", Property: "startTime"},
}, pagedMCPParamDecls()...),
},
})
chatMessageListMentionsCmd := &cobra.Command{
Use: "list-mentions",
Short: "拉取 @我 的消息",
Long: `搜索时间范围内 @我 的消息,可选指定群聊。--start 和 --end 可选,不传时默认最近 7 天到当前时间。返回结果包含单聊和群聊标识。分页参数 --limit(默认 50)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`,
Example: ` dws chat message list-mentions --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor 0
dws chat message list-mentions --start "2026-04-01T00:00:00+08:00" --end "2026-04-14T00:00:00+08:00" --limit 20 --cursor 0
dws chat message list-mentions --group <openconversation_id> --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor 0
dws chat message list-mentions --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor <nextCursor>
dws chat message list-mentions --group <openconversation_id> --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --page-all --max-items 200 --page-delay 0
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
return RunPagedMCPCommand(cmd, pagedChatConversationMessagesConfig(
"search_at_me_message", chatMessageListMentionsArgs))
},
}
DeclareLeafMetadata(chatMessageListMentionsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "search_at_me_message",
CanonicalPath: "chat.search_at_me_message",
CLIPath: "chat message list-mentions",
PrimaryCLIPath: "chat message list-mentions",
},
Description: "查询指定时间范围内提及当前用户的消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "search_at_me_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "查询指定时间范围内提及当前用户的消息",
UseWhen: []string{"需要找出 @我的消息和待关注事项时"},
AvoidWhen: []string{"查询全部消息时使用 chat message list-all"},
Examples: []string{
"dws chat message list-mentions --start \"2026-07-01T00:00:00+08:00\" --end \"2026-07-02T00:00:00+08:00\" --limit 50",
"dws chat message list-mentions --start \"2026-07-01T00:00:00+08:00\" --end \"2026-07-02T00:00:00+08:00\" --limit 50 --page-all --max-items 200",
},
},
Parameters: append([]contract.ParamDecl{
{Name: "end", Property: "endTime"},
{Name: "group", Property: "openConversationId"},
{Name: "start", Property: "startTime"},
}, pagedMCPParamDecls()...),
},
})
chatMessageListFocusedCmd := &cobra.Command{
Use: "list-focused",
Short: "拉取特别关注人的消息",
Long: `拉取当前用户特别关注人的消息。分页参数 --limit 指定每页数量,--cursor 传数字分页游标(首次不传或传 0)。返回结果中 hasMore=true 时用数字 nextCursor 作为下次 --cursor 继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并聚合 result.messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 精确截断返回条数,--page-delay 控制页间等待毫秒数。`,
Example: ` dws chat message list-focused --limit 50
dws chat message list-focused --limit 20 --cursor <nextCursor>
dws chat message list-focused --limit 50 --page-all --page-limit 10 --page-delay 0`,
RunE: func(cmd *cobra.Command, args []string) error {
return RunPagedMCPCommand(cmd, pagedChatMessagesInt64Config(
"list_special_focus_messages", chatMessageListFocusedArgs))
},
}
DeclareLeafMetadata(chatMessageListFocusedCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_special_focus_messages",
CanonicalPath: "chat.list_special_focus_messages",
CLIPath: "chat message list-focused",
PrimaryCLIPath: "chat message list-focused",
},
Description: "列出当前用户特别关注的消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "list_special_focus_messages"},
},
Selection: contract.SelectionSpec{
AgentSummary: "列出当前用户特别关注的消息",
UseWhen: []string{"需要查看特别关注或重点消息列表时"},
AvoidWhen: []string{"普通未读消息或提及消息使用对应专用命令"},
Examples: []string{
"dws chat message list-focused --limit 50",
"dws chat message list-focused --limit 50 --page-all --page-limit 10",
},
},
Parameters: pagedMCPParamDecls(),
},
})
chatMessageListTopConversationsCmd := &cobra.Command{
Use: "list-top-conversations",
Short: "拉取置顶会话列表",
Long: `拉取当前用户的置顶会话列表。分页参数 --limit 指定每页数量,--cursor 传分页游标(首次不传或传 0)。返回结果中 hasMore=true 时用 nextCursor 作为下次 --cursor 继续翻页。`,
Example: ` dws chat list-top-conversations --limit 1000
dws chat list-top-conversations --limit 1000 --cursor <nextCursor>`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs := map[string]any{}
if v, err := cmd.Flags().GetInt("limit"); err == nil && v > 0 {
toolArgs["limit"] = v
}
if v, _ := cmd.Flags().GetInt64("cursor"); v > 0 {
toolArgs["cursor"] = v
}
if v, _ := cmd.Flags().GetBool("exclude-muted"); v {
toolArgs["excludeMuted"] = true
}
return callMCPTool("list_top_conversations", toolArgs)
},
}
DeclareLeafMetadata(chatMessageListTopConversationsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_top_conversations",
CanonicalPath: "chat.list_top_conversations",
CLIPath: "chat list-top-conversations",
PrimaryCLIPath: "chat list-top-conversations",
},
Description: "列出当前用户置顶的会话",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "list_top_conversations"},
},
Selection: contract.SelectionSpec{
AgentSummary: "列出当前用户置顶的会话",
UseWhen: []string{"需要查看现有置顶会话清单时"},
AvoidWhen: []string{"设置或取消某个会话置顶时使用 chat set-top"},
Examples: []string{"dws chat list-top-conversations --limit 100"},
},
},
})
chatMessageListUnreadConversationsCmd := &cobra.Command{
Use: "list-unread-conversations",
Short: "获取未读会话列表",
Long: `获取当前用户有未读消息的会话列表,可选通过 --count 控制返回的会话条数。`,
Example: ` dws chat message list-unread-conversations
dws chat message list-unread-conversations --count 20`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs := map[string]any{}
if count, err := cmd.Flags().GetInt("count"); err == nil && count > 0 {
toolArgs["count"] = count
}
if v, _ := cmd.Flags().GetBool("exclude-muted"); v {
toolArgs["excludeMuted"] = true
}
return callMCPTool("unread_message_conversation_list", toolArgs)
},
}
DeclareLeafMetadata(chatMessageListUnreadConversationsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "unread_message_conversation_list",
CanonicalPath: "chat.unread_message_conversation_list",
CLIPath: "chat message list-unread-conversations",
PrimaryCLIPath: "chat message list-unread-conversations",
},
Description: "列出当前用户存在未读消息的会话",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "unread_message_conversation_list"},
},
Selection: contract.SelectionSpec{
AgentSummary: "列出当前用户存在未读消息的会话",
UseWhen: []string{"需要定位哪些会话还有未读内容时"},
AvoidWhen: []string{"查询某条消息的已读人员时使用 chat message read-status"},
Examples: []string{"dws chat message list-unread-conversations --count 50"},
},
},
})
chatMessageSearchCmd := &cobra.Command{
Use: "search",
Short: "按关键词搜索消息",
Long: `在当前用户的会话中按关键词搜索消息。输出顶层 messages,稳定字段为 messageId 和 text;兼容保留 openMessageId、content 和原始 result。--query 指定搜索关键词(必填)。可选 --group 限定搜索某个会话,不传则搜索所有会话。显式指定会话时,CLI 会先验证 CID,再扫描全局搜索流并在本地精确过滤,避免下层忽略非法 CID 或群聊 CID;默认最多扫描 40 页并返回至 --limit 条范围内消息。时间参数 --start/--end(ISO-8601)可选,不传时默认最近 7 天到当前时间。分页参数 --limit(默认 100)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。未指定会话时默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持默认行为。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`,
Example: ` dws chat message search --query "changefree" --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00" --limit 50 --cursor 0
dws chat message search --query "codereview" --group <openconversation_id> --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00" --limit 100 --cursor 0
dws chat message search --query "链接" --start "2026-04-15T00:00:00+08:00" --end "2026-04-16T00:00:00+08:00" --limit 100 --cursor <nextCursor>
dws chat message search --query "发布计划" --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00" --limit 100 --page-all --max-items 300 --page-delay 0
dws chat message search --query "发布计划" --start "2026-07-01T00:00:00+08:00" --end "2026-07-10T00:00:00+08:00" --jq '.messages[] | {messageId, text}'
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
return runConversationScopedPagedMessageSearch(
cmd,
pagedProjectedChatSearchConfig(cmd, "search_messages_by_keyword", chatMessageSearchArgs),
"openConversationId",
[]string{groupID},
)
},
}
DeclareLeafMetadata(chatMessageSearchCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "search_messages_by_keyword",
CanonicalPath: "chat.search_messages_by_keyword",
CLIPath: "chat message search",
PrimaryCLIPath: "chat message search",
},
Description: "按关键词和时间范围搜索消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "search_messages_by_keyword"},
},
Selection: contract.SelectionSpec{
AgentSummary: "按关键词和时间范围搜索消息",
UseWhen: []string{"需要用关键词查找消息且过滤条件较简单时"},
AvoidWhen: []string{"需要多会话、发送者或 @维度组合时使用 search-advanced"},
Examples: []string{
"dws chat message search --query \"发布计划\" --start \"2026-07-01T00:00:00+08:00\" --end \"2026-07-10T00:00:00+08:00\"",
"dws chat message search --query \"发布计划\" --start \"2026-07-01T00:00:00+08:00\" --end \"2026-07-10T00:00:00+08:00\" --page-all --max-items 300 --jq '.messages[] | {messageId, text}'",
},
},
Parameters: append([]contract.ParamDecl{
{Name: "end", Property: "endTime"},
{Name: "group", Property: "openConversationId"},
{Name: "query", Property: "keyword"},
{Name: "start", Property: "startTime"},
}, pagedMCPParamDecls()...),
},
})
// ── search-advanced:多维度搜索消息 ──────
chatMessageSearchAdvancedCmd := &cobra.Command{
Use: "search-advanced",
Short: "多维度搜索消息",
Long: `支持按关键词、发送者、@我、@指定人、指定会话、时间范围等多维度搜索消息。发送者 userId 使用 --user/--users;发送者或 @ 人的 openDingTalkId 使用 --sender-ids/--at-ids。显式指定会话时,CLI 会先验证 CID,再扫描全局搜索流并在本地精确过滤,避免下层忽略非法 CID 或群聊 CID;默认最多扫描 40 页并返回至 --limit 条范围内消息。所有参数均为可选,至少指定一个搜索条件。未指定会话时默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持默认行为。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`,
Example: ` dws chat message search-advanced --query "周报" --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00"
dws chat message search-advanced --user <userId> --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00"
dws chat message search-advanced --users <userId1>,<userId2> --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00"
dws chat message search-advanced --sender-ids <openDingTalkId1>,<openDingTalkId2> --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00"
dws chat message search-advanced --at-me --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00"
dws chat message search-advanced --at-ids <openDingTalkId1>,<openDingTalkId2> --conversation-ids <openConversationId1>,<openConversationId2> --limit 50 --cursor 0
dws chat message search-advanced --conversation-ids <单聊openConversationId> --query "合同" --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00"
dws chat message search-advanced --query "周报" --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00" --limit 100 --page-all --page-limit 20 --max-items 500
# 查询群 ID: dws chat search --query "群名"
# 查询单聊会话 ID: dws chat conversation-info --user <userId>
# 查询人员: dws contact user search --keyword "姓名" --format json`,
RunE: func(cmd *cobra.Command, args []string) error {
conversationIDs := parseCSVValues(flagOrFallback(cmd, "conversation-ids", "groups", "group"))
return runConversationScopedPagedMessageSearch(
cmd,
pagedChatConversationMessagesOnServerConfig("im", "search_messages", chatMessageSearchAdvancedArgs),
"openConversationIds",
conversationIDs,
)
},
}
DeclareLeafMetadata(chatMessageSearchAdvancedCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "search_messages",
CanonicalPath: "chat.search_messages",
CLIPath: "chat message search-advanced",
PrimaryCLIPath: "chat message search-advanced",
},
Description: "按时间、关键词、发送者、@ 或会话等多维度搜索消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "search_messages"},
},
Selection: contract.SelectionSpec{
AgentSummary: "按时间、关键词、发送者、@ 或会话等多维度搜索消息",
UseWhen: []string{"需要组合时间范围、关键词、发送者、@我/@某人、指定会话等条件搜消息"},
AvoidWhen: []string{
"只需按关键词在会话里搜时优先评估 chat message search",
"只需拉取某会话时间线时使用 chat message list",
"只需某人发给我的消息时使用 chat message list-by-sender",
},
Examples: []string{
"dws chat message search-advanced --query \"周报\" --start \"2026-04-01T00:00:00+08:00\" --end \"2026-04-15T00:00:00+08:00\"",
"dws chat message search-advanced --query \"周报\" --start \"2026-04-01T00:00:00+08:00\" --end \"2026-04-15T00:00:00+08:00\" --page-all --page-limit 20",
},
},
Parameters: append([]contract.ParamDecl{
{Name: "at-ids", Property: "atOpenDingTakIds"},
{Name: "conversation-ids", Property: "openConversationIds"},
{Name: "conversation-type", Property: "searchConvType"},
{Name: "end", Property: "endTime"},
{Name: "message-type", Property: "messageType"},
{Name: "only-robot", Property: "onlyRobotMessages"},
{Name: "query", Property: "keyword"},
{Name: "sender-ids", Property: "senderOpenDingTakIds"},
{Name: "start", Property: "startTime"},
}, pagedMCPParamDecls()...),
},
})
// ── query-send-status:查询消息发送状态(走 IM MCP)──────
chatMessageQuerySendStatusCmd := &cobra.Command{
Use: "query-send-status",
Aliases: []string{"send-status"},
Short: "查询消息发送状态",
Long: `查询以当前用户身份发送的消息的发送状态。需要传入 chat message send 返回的 openTaskId。
发送成功后,查询结果会返回 openMessageId 和 openConversationId,可直接作为
chat message edit 或 chat message recall 的 --msg-id 和 --conversation-id。
同一组 ID 也可继续用于 chat message read-status(消息参数为 --message-id);openTaskId 本身不是消息 ID。`,
Example: ` dws chat message query-send-status --open-task-id <openTaskId>
dws chat message recall --conversation-id <openConversationId> --msg-id <openMessageId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "open-task-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "query_message_send_status", map[string]any{
"openTaskId": mustGetFlag(cmd, "open-task-id"),
})
},
}
DeclareLeafMetadata(chatMessageQuerySendStatusCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "query_message_send_status",
CanonicalPath: "chat.query_message_send_status",
CLIPath: "chat message query-send-status",
PrimaryCLIPath: "chat message query-send-status",
Aliases: []string{"chat message send-status"},
},
Description: "查询异步消息发送任务的状态",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "query_message_send_status"},
},
Selection: contract.SelectionSpec{
AgentSummary: "查询异步消息发送任务的状态",
UseWhen: []string{"发送命令返回 openTaskId 后需要确认投递结果,或后续 edit/recall/read-status 需要先取得 openMessageId 和 openConversationId 时"},
AvoidWhen: []string{"没有 openTaskId、已经有消息 ID,或只需查历史消息内容时不要使用"},
Examples: []string{"dws chat message query-send-status --open-task-id <openTaskId>"},
},
},
})
// ── recall:撤回用户发送的消息(走 IM MCP)──────
chatMessageRecallCmd := &cobra.Command{
Use: "recall",
Short: "撤回用户发送的消息",
Long: `撤回当前用户发送的消息。需要指定会话 ID 和消息 ID。`,
Example: ` dws chat message recall --conversation-id <openConversationId> --msg-id <openMessageId>
# 发送后撤回:send -> query-send-status -> recall
dws chat message send --group <openConversationId> --text "待撤回的内容"
dws chat message query-send-status --open-task-id <上一步返回的openTaskId>
dws chat message recall --conversation-id <上一步返回的openConversationId> --msg-id <上一步返回的openMessageId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil {
return err
}
if err := validateRequiredFlags(cmd, "msg-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "recall_message", map[string]any{
"openConversationId": flagOrFallback(cmd, "conversation-id", "group", "id", "chat"),
"openMessageId": mustGetFlag(cmd, "msg-id"),
})
},
}
DeclareLeafMetadata(chatMessageRecallCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "recall_message",
CanonicalPath: "chat.recall_message",
CLIPath: "chat message recall",
PrimaryCLIPath: "chat message recall",
},
Description: "撤回当前用户已发送的单条消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "recall_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "撤回当前用户已发送的单条消息",
UseWhen: []string{"已知会话与消息 ID,需要撤回自己发出的那条消息"},
AvoidWhen: []string{
"撤回机器人消息时使用 chat message recall-by-bot",
"消息 ID 未确认时先用消息查询/搜索拿到 openMessageId",
},
Examples: []string{"dws chat message recall --conversation-id <openConversationId> --msg-id <openMessageId>"},
},
Parameters: []contract.ParamDecl{
{Name: "conversation-id", Property: "openConversationId"},
{Name: "msg-id", Property: "openMessageId"},
},
},
})
chatMessageEditCmd := &cobra.Command{
Use: "edit",
Short: "编辑消息",
Long: `编辑指定消息的内容。需要指定会话 ID 和消息 ID。
推荐使用 --text 和可选 --title,CLI 会按 Markdown 消息规则生成 content:{"title":"标题","text":"正文"}。
也可以直接使用 --content 传入完整 Markdown content JSON。--text 和 --content 二选一。`,
Example: ` dws chat message edit --conversation-id <openConversationId> --msg-id <openMessageId> --text "更新后的内容"
# 发送后编辑:send -> query-send-status -> edit
dws chat message send --group <openConversationId> --text "原始内容"
dws chat message query-send-status --open-task-id <上一步返回的openTaskId>
dws chat message edit --conversation-id <上一步返回的openConversationId> --msg-id <上一步返回的openMessageId> --text "更新后的内容"
dws chat message edit --group <openConversationId> --msg-id <openMessageId> --title "标题" --text "更新后的内容"
dws chat message edit --group <openConversationId> --msg-id <openMessageId> --text "<@all> 请查看" --at-all
dws chat message edit --group <openConversationId> --msg-id <openMessageId> --text "<@openDingTalkId1> 请查看" --at-open-dingtalk-ids <openDingTalkId1>
dws chat message edit --group <openConversationId> --msg-id <openMessageId> --content '{"title":"标题","text":"更新后的内容"}'`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil {
return err
}
if err := validateRequiredFlags(cmd, "msg-id"); err != nil {
return err
}
content, _ := cmd.Flags().GetString("content")
text, _ := cmd.Flags().GetString("text")
if content == "" && text == "" {
return fmt.Errorf("flag --text or --content is required")
}
if content != "" && text != "" {
return fmt.Errorf("--text and --content are mutually exclusive")
}
atAll, _ := cmd.Flags().GetBool("at-all")
atOpenIDs := parseCSVValues(mustGetFlag(cmd, "at-open-dingtalk-ids"))
if text != "" {
title := mustGetFlag(cmd, "title")
if title == "" {
title = sanitizeTitleFromText(text)
}
if atAll && !strings.Contains(text, "<@all>") {
text = "<@all> " + text
}
text = normalizeAtPlaceholders(text, atOpenIDs, true)
contentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
content = string(contentJSON)
}
toolArgs := map[string]any{
"openConversationId": flagOrFallback(cmd, "conversation-id", "group", "id", "chat"),
"openMessageId": mustGetFlag(cmd, "msg-id"),
"content": content,
}
if atAll {
toolArgs["atAll"] = true
}
if len(atOpenIDs) > 0 {
toolArgs["atOpenDingTalkIds"] = atOpenIDs
}
return callMCPToolOnServer("im", "edit_message", toolArgs)
},
}
DeclareLeafMetadata(chatMessageEditCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "edit_message",
CanonicalPath: "chat.edit_message",
CLIPath: "chat message edit",
PrimaryCLIPath: "chat message edit",
},
Description: "编辑当前用户已发送消息的 Markdown 内容",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: the executable CLI builds or accepts message content and calls im/edit_message, which is absent from the pinned MCP metadata snapshot.",
},
Selection: contract.SelectionSpec{
AgentSummary: "编辑当前用户已发送消息的 Markdown 内容",
UseWhen: []string{"已有会话 openConversationId 和消息 openMessageId,需要更正已发送消息的标题、正文或 @ 信息"},
AvoidWhen: []string{"发送新消息应使用 chat message send;撤回消息应使用 chat message recall"},
Examples: []string{"dws chat message edit --conversation-id <openConversationId> --msg-id <openMessageId> --text \"更新后的内容\""},
},
Parameters: []contract.ParamDecl{
{Name: "at-all", Property: "atAll", Required: boolPtr(false), InterfaceType: "boolean"},
{Name: "at-open-dingtalk-ids", Property: "atOpenDingTalkIds", Required: boolPtr(false), InterfaceType: "array"},
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "content", Property: "content", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(true)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "msg-id", Property: "openMessageId", Required: boolPtr(true)},
{Name: "text", Property: "text", Required: boolPtr(false)},
{Name: "title", Property: "title", Required: boolPtr(false)},
},
},
})
chatMessageReadStatusCmd := &cobra.Command{
Use: "read-status",
Short: "查询消息的已读/未读状态",
Long: `查询指定会话中消息的已读/未读状态(仅消息发送者可查询自己发出的消息)。--conversation-id 指定会话 openConversationId(群聊或单聊均可),--message-id 指定消息 ID(由 dws chat message list 返回的 openMessageId,必须是当前用户发送的消息)。目标用户 userId 使用 --user/--users;目标用户 openDingTalkId 使用 --target-open-dingtalk-ids;不传目标用户则返回所有接收者的状态。`,
Example: ` dws chat message read-status --conversation-id <openConversationId> --message-id <openMessageId>
dws chat message read-status --conversation-id <openConversationId> --message-id <openMessageId> --user userId1,userId2
dws chat message read-status --conversation-id <openConversationId> --message-id <openMessageId> --users userId1,userId2
dws chat message read-status --conversation-id <openConversationId> --message-id <openMessageId> --target-open-dingtalk-ids openDingTalkId1,openDingTalkId2
# 查询会话 ID: dws chat search --query "群名"
# 查询 openMessageId: dws chat message list --group <openConversationId> --time "2025-03-01 00:00:00"
# 查询人员: dws contact user search --keyword "姓名" --format json`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil {
return err
}
if err := validateRequiredFlags(cmd, "message-id"); err != nil {
return err
}
toolArgs := map[string]any{
"openConversationId": flagOrFallback(cmd, "conversation-id", "group", "id", "chat"),
"openMessageId": mustGetFlag(cmd, "message-id"),
}
if usersStr := flagOrFallback(cmd, "users", "user", "userId"); usersStr != "" {
appendChatIDArgs(toolArgs, parseCSVValues(usersStr), "targetUserIds", "targetOpenDingTalkIds")
}
if usersStr, _ := cmd.Flags().GetString("target-open-dingtalk-ids"); usersStr != "" {
appendChatIDArgs(toolArgs, parseCSVValues(usersStr), "targetUserIds", "targetOpenDingTalkIds")
}
return callMCPToolOnServer("im", "query_msg_read_status", toolArgs)
},
}
DeclareLeafMetadata(chatMessageReadStatusCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "query_msg_read_status",
CanonicalPath: "chat.query_msg_read_status",
CLIPath: "chat message read-status",
PrimaryCLIPath: "chat message read-status",
},
Description: "查询指定消息的已读状态和人员",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "query_msg_read_status"},
},
Selection: contract.SelectionSpec{
AgentSummary: "查询指定消息的已读状态和人员",
UseWhen: []string{"已知会话 ID 与消息 ID,需要核对阅读情况时"},
AvoidWhen: []string{"查看哪些会话未读时使用 chat message list-unread-conversations"},
Examples: []string{"dws chat message read-status --conversation-id <openConversationId> --message-id <openMessageId>"},
},
Parameters: []contract.ParamDecl{
{Name: "conversation-id", Property: "openConversationId"},
{Name: "message-id", Property: "openMessageId"},
{Name: "target-open-dingtalk-ids", Property: "targetOpenDingTalkIds"},
},
},
})
chatSearchCommonCmd := &cobra.Command{
Use: "search-common",
Short: "搜索共同群(查询指定人共同所在的群聊)",
Long: `根据昵称列表搜索共同群聊。--nicks 指定要搜索的人员昵称(逗号分隔,必填)。--match-mode 控制匹配模式:AND 表示所有人都在群里,OR 表示任一人在群里(默认 AND)。分页参数 --limit(默认 20)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。`,
Example: ` dws chat search-common --nicks "风雷,山乔" --limit 20 --cursor 0
dws chat search-common --nicks "天鸡,乐函" --match-mode OR --limit 20 --cursor 0
dws chat search-common --nicks "风雷,山乔,天鸡" --limit 10 --cursor <nextCursor>`,
RunE: runChatSearchCommon,
}
DeclareLeafMetadata(chatSearchCommonCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "search_common_groups",
CanonicalPath: "chat.search_common_groups",
CLIPath: "chat search-common",
PrimaryCLIPath: "chat search-common",
},
Description: "查询指定人员共同所在的群聊",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "search_common_groups"},
},
Selection: contract.SelectionSpec{
AgentSummary: "查询指定人员共同所在的群聊",
UseWhen: []string{"需要找两人或多人共同群聊时"},
AvoidWhen: []string{"按群名称搜索时使用 chat search"},
Examples: []string{"dws chat search-common --nicks \"张三,李四\" --match-mode all --limit 20"},
},
},
})
chatMessageSearchCommonCmd := &cobra.Command{
Use: "search-common",
Short: "搜索共同群",
Hidden: true,
RunE: runChatSearchCommon,
}
// ── bot 子命令 ────────────────────────────────────────────
chatBotCmd := &cobra.Command{Use: "bot", Short: "机器人管理", RunE: groupRunE}
chatBotSearchCmd := &cobra.Command{
Use: "search",
Short: "搜索【我自己创建】的机器人(仅本人创建的,不含他人/官方机器人)",
Long: `搜索【当前登录用户自己创建】的机器人,按 robotName 模糊匹配 + 页码分页。
如何在 search 与 find 之间选择:
- search:用户说"我创建的""我的""我自己的""我做的"机器人 → 用 search
- find :用户说"搜索机器人""找一个机器人""所有可用机器人""帮我找 XXX 机器人"
(不限范围,包含他人/官方)→ 用 find(dws chat bot find)
注意:search 没有 openDingTalkId,如果需要给机器人发单聊消息请用 find。`,
Example: ` # "搜一下我创建的机器人" / "我自己的机器人有哪些"
dws chat bot search --page 1
dws chat bot search --page 1 --size 10 --name "日报"`,
RunE: func(cmd *cobra.Command, args []string) error {
page, _ := cmd.Flags().GetInt("page")
toolArgs := map[string]any{
"currentPage": page,
}
size, _ := cmd.Flags().GetInt("size")
if size == 0 {
size, _ = cmd.Flags().GetInt("limit")
}
if size > 0 {
toolArgs["pageSize"] = size
}
if v, _ := cmd.Flags().GetString("name"); v != "" {
toolArgs["robotName"] = v
}
return callMCPToolOnServer("bot", "search_my_robots", toolArgs)
},
}
DeclareLeafMetadata(chatBotSearchCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "search_my_robots",
CanonicalPath: "chat.search_my_robots",
CLIPath: "chat bot search",
PrimaryCLIPath: "chat bot search",
},
Description: "搜索我创建的企业机器人并提取 robot-code",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "bot", RPCName: "search_my_robots"},
},
Selection: contract.SelectionSpec{
AgentSummary: "搜索我创建的企业机器人并提取 robot-code",
UseWhen: []string{"需要我自己创建的机器人及其 robot-code"},
AvoidWhen: []string{"搜索全部可用企业机器人时使用 chat bot find"},
Examples: []string{"dws chat bot search --page 1 --size 10 --name \"日报\""},
},
Parameters: []contract.ParamDecl{
{Name: "name", Property: "robotName"},
{Name: "page", Property: "currentPage"},
{Name: "size", Property: "pageSize"},
},
},
})
// group 子命令 flags
chatGroupCreateCmd.Flags().String("name", "", "群名称 (必填)")
_ = chatGroupCreateCmd.MarkFlagRequired("name")
chatGroupCreateCmd.Flags().String("users", "", "成员 userId 或 openDingTalkId(可混传),逗号分隔 (必填)")
_ = chatGroupCreateCmd.MarkFlagRequired("users")
chatGroupCreateCmd.Flags().String("type", "INTERNAL", "群类型: INTERNAL(内部群,默认)/EXTERNAL(外部群)/NORMAL(普通群)")
chatGroupCreateCmd.Flags().Bool("thread", false, "开启话题模式,将创建话题圈")
chatGroupMembersCmd.Flags().String("id", "", "群 ID / openconversation_id (必填)")
_ = chatGroupMembersCmd.MarkFlagRequired("id")
chatGroupMembersCmd.Flags().String("cursor", "", "分页游标,首次从 0 开始")
chatGroupMembersAddBotCmd.Flags().String("robot-code", "", "机器人 Code (必填)")
_ = chatGroupMembersAddBotCmd.MarkFlagRequired("robot-code")
chatGroupMembersAddBotCmd.Flags().String("id", "", "群聊 openConversationId (必填)")
_ = chatGroupMembersAddBotCmd.MarkFlagRequired("id")
chatGroupRenameCmd.Flags().String("id", "", "群 ID / openconversation_id (必填)")
_ = chatGroupRenameCmd.MarkFlagRequired("id")
chatGroupRenameCmd.Flags().String("name", "", "修改后的群名称 (必填)")
_ = chatGroupRenameCmd.MarkFlagRequired("name")
chatGroupMemberAddCmd.Flags().String("id", "", "群 ID / openconversation_id (必填)")
_ = chatGroupMemberAddCmd.MarkFlagRequired("id")
chatGroupMemberAddCmd.Flags().String("users", "", "要添加的用户 userId 或 openDingTalkId(可混传),逗号分隔 (必填)")
_ = chatGroupMemberAddCmd.MarkFlagRequired("users")
chatGroupMemberRemoveCmd.Flags().String("id", "", "群 ID / openconversation_id (必填)")
_ = chatGroupMemberRemoveCmd.MarkFlagRequired("id")
chatGroupMemberRemoveCmd.Flags().String("users", "", "要移除的用户 userId 列表,逗号分隔 (必填)")
_ = chatGroupMemberRemoveCmd.MarkFlagRequired("users")
chatGroupCmd.AddCommand(chatGroupCreateCmd, chatGroupMembersCmd, chatGroupRenameCmd)
chatGroupCmd.AddCommand(chatGroupSearchCompatibilityCmd)
chatGroupMembersCmd.AddCommand(chatGroupMemberAddCmd, chatGroupMemberRemoveCmd, chatGroupMembersAddBotCmd)
// message 子命令 flags
chatMessageListCmd.Flags().String("group", "", "群聊 openconversation_id(群聊时必填)")
chatMessageListCmd.Flags().String("user", "", "单聊用户 userId(单聊时与 --open-dingtalk-id 二选一)")
chatMessageListCmd.Flags().String("open-dingtalk-id", "", "单聊用户 openDingTalkId(单聊时与 --user 二选一,适用于无法获取 userId 的场景)")
chatMessageListCmd.Flags().String("time", "", "开始时间,格式: yyyy-MM-dd HH:mm:ss(可选,默认当前时间)")
chatMessageListCmd.Flags().String("direction", "", "时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(未传 --time 时默认 older)")
chatMessageListCmd.Flags().String("forward", "true", "true 等价 --direction newer,false 等价 --direction older(未传 --time 时默认 false)")
_ = chatMessageListCmd.Flags().MarkHidden("forward")
chatMessageListCmd.Flags().Int("limit", 0, "返回数量,不传则不限制")
chatMessageListCmd.Flags().Int("size", 0, "--limit 的旧版别名")
_ = chatMessageListCmd.Flags().MarkHidden("size")
chatMessageListDirectCmd.Flags().String("user", "", "对方 userId(同组织内同事,与 --open-dingtalk-id 二选一)")
chatMessageListDirectCmd.Flags().String("open-dingtalk-id", "", "对方 openDingTalkId(非同组织普通好友场景,与 --user 二选一)")
chatMessageListDirectCmd.Flags().String("time", "", "开始时间,格式 yyyy-MM-dd HH:mm:ss (必填)")
chatMessageListDirectCmd.Flags().String("direction", "", "时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉")
chatMessageListDirectCmd.Flags().String("forward", "true", "true 等价 --direction newer,false 等价 --direction older")
_ = chatMessageListDirectCmd.Flags().MarkHidden("forward")
chatMessageListDirectCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)")
chatMessageListDirectCmd.Flags().Int("size", 0, "--limit 的旧版别名")
_ = chatMessageListDirectCmd.Flags().MarkHidden("size")
chatMessageSendCmd.Flags().String("group", "", "群聊 openconversation_id(群聊时必填)")
chatMessageSendCmd.Flags().String("user", "", "单聊接收人 userId(单聊时与 --open-dingtalk-id 二选一)")
chatMessageSendCmd.Flags().String("open-dingtalk-id", "", "单聊接收人 openDingTalkId(单聊时与 --user 二选一)")
chatMessageSendCmd.Flags().String("title", "", "消息标题,显示在消息列表(可选,未指定时使用消息内容)")
// 别名注册: --text/--content/--body/--message/--markdown → 位置参数
chatMessageSendCmd.Flags().String("text", "", "消息内容(推荐方式,也可用位置参数传递。内容含换行/特殊字符时必须使用此 flag)")
chatMessageSendCmd.Flags().String("content", "", "--text 的别名")
chatMessageSendCmd.Flags().String("body", "", "--text 的别名")
chatMessageSendCmd.Flags().String("message", "", "--text 的别名")
chatMessageSendCmd.Flags().String("markdown", "", "--text 的别名")
// --text 不再隐藏,作为首选传参方式展示在 help 中
_ = chatMessageSendCmd.Flags().MarkHidden("content")
_ = chatMessageSendCmd.Flags().MarkHidden("body")
_ = chatMessageSendCmd.Flags().MarkHidden("message")
_ = chatMessageSendCmd.Flags().MarkHidden("markdown")
chatMessageSendCmd.Flags().Bool("at-all", false, "@所有人(仅群聊时生效,可选),设置时,消息内容中一定要包含对应的占位符<@all>")
chatMessageSendCmd.Flags().String("at-open-dingtalk-ids", "", "@指定成员的 openDingTalkId 列表,逗号分隔(仅群聊时生效,可选),设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2>")
chatMessageSendCmd.Flags().String("media-id", "", "上游已提供的图片 mediaId(仅旧版 msgType=image;CLI 不提供本地上传到 mediaId)")
chatMessageSendCmd.Flags().String("msg-type", "", "富媒体消息类型: image/file/audio/video/location/profile(本地图片/文件推荐 file --file-path;image 仅接受已有 mediaId)")
chatMessageSendCmd.Flags().String("latitude", "", "位置消息纬度(msgType=location 时必填)")
chatMessageSendCmd.Flags().String("longitude", "", "位置消息经度(msgType=location 时必填)")
chatMessageSendCmd.Flags().String("location-name", "", "位置消息地址名称(msgType=location 时必填)")
chatMessageSendCmd.Flags().String("map-thumbnail-url", "", "位置消息地图缩略图 mediaId,形如 @mediaId(msgType=location 时必填)")
chatMessageSendCmd.Flags().String("contact-id", "", "联系人名片 openDingTalkId(msgType=profile 时必填)")
chatMessageSendCmd.Flags().Int64("dentry-id", 0, "文件 dentryId(与 --space-id 成对传入时跳过自动上传)")
chatMessageSendCmd.Flags().Int64("space-id", 0, "空间 ID(与 --dentry-id 成对传入时跳过自动上传)")
chatMessageSendCmd.Flags().String("file-name", "", "文件名")
chatMessageSendCmd.Flags().String("file-type", "", "文件类型/扩展名")
chatMessageSendCmd.Flags().String("file-path", "", "本地文件路径(msgType=file/audio/video 时直接上传并按 file 消息发送)")
chatMessageSendCmd.Flags().Int64("file-size", 0, "文件大小,单位字节")
_ = chatMessageSendCmd.Flags().MarkHidden("dentry-id")
_ = chatMessageSendCmd.Flags().MarkHidden("space-id")
_ = chatMessageSendCmd.Flags().MarkHidden("file-name")
_ = chatMessageSendCmd.Flags().MarkHidden("file-type")
_ = chatMessageSendCmd.Flags().MarkHidden("file-size")
chatMessageSendCmd.Flags().Bool("ai-tag", true, "消息是否带 AI 发送角标(默认 true)")
corecmd.RegisterFlags(chatMessageSendCmd, []corecmd.FlagSpec{{
Name: "idempotency-key",
Usage: "幂等键,相同 key 在 24h 内不会重复发送(可选)",
Aliases: []string{"uuid"},
}})
cli.AttachRuntimeSchema(chatMessageSendCmd, "chat", "send_personal_message", "hardcoded:chat")
cli.AnnotateRuntimeConstraints(chatMessageSendCmd, cli.RuntimeSchemaConstraints{
MutuallyExclusive: [][]string{{"group", "user", "open-dingtalk-id"}},
RequireOneOf: [][]string{{"group", "user", "open-dingtalk-id"}},
})
cli.AnnotateRuntimePositionals(chatMessageSendCmd, contract.RuntimeSchemaPositional{
Name: "content",
Type: "string",
Description: "消息内容(也可使用 --text;富媒体消息可省略)",
Required: false,
Index: 0,
})
cli.AnnotateRuntimeFlagEnum(chatMessageSendCmd, "msg-type", "image", "file", "audio", "video")
cli.AnnotateRuntimeFlagFormat(chatMessageSendCmd, "file-path", "file-path")
chatMessageSendByBotCmd.Flags().String("robot-code", "", "机器人 Code (必填)")
_ = chatMessageSendByBotCmd.MarkFlagRequired("robot-code")
chatMessageSendByBotCmd.Flags().String("group", "", "群聊 openConversationId(群聊时必填)")
chatMessageSendByBotCmd.Flags().String("users", "", "用户 userId 列表,逗号分隔,最多20个(单聊时必填)")
chatMessageSendByBotCmd.Flags().String("msg-type", "", "消息类型: markdown/image/file(省略时为 markdown;图片使用 image --image-url;本地文件使用 file --file-path)")
chatMessageSendByBotCmd.Flags().String("title", "", "Markdown 消息标题(发送 Markdown 时必填)")
chatMessageSendByBotCmd.Flags().String("text", "", "Markdown 消息内容(发送 Markdown 时必填;稳定换行用空行,转义形式写 \\n\\n,不要只写 \\n)")
chatMessageSendByBotCmd.Flags().String("image-url", "", "公网图片 URL(msgType=image 时必填)")
chatMessageSendByBotCmd.Flags().String("file-path", "", "本地文件路径(msgType=file 时直接上传并按 file 消息发送)")
chatMessageSendByBotCmd.Flags().String("at-user-ids", "", "@指定成员的 userId 列表,逗号分隔(仅群聊时生效,可选),--text 中需包含 @userId 对应文本")
chatMessageSendByBotCmd.Flags().String("open-dingtalk-ids", "", "用户 openDingtalkId 列表,逗号分隔(单聊时可替代 --users,可选)")
chatMessageSendByBotCmd.Flags().String("at-open-dingtalk-ids", "", "@指定成员的 openDingtalkId 列表,逗号分隔(仅群聊时生效,可选)")
chatMessageSendByBotCmd.Flags().Bool("at-all", false, "@所有人(可选),服务端接收字符串 true/false")
cli.AnnotateRuntimeFlagFormat(chatMessageSendByBotCmd, "file-path", "file-path")
chatMessageRecallByBotCmd.Flags().String("robot-code", "", "机器人 Code (必填)")
_ = chatMessageRecallByBotCmd.MarkFlagRequired("robot-code")
chatMessageRecallByBotCmd.Flags().String("group", "", "群聊 openConversationId(群聊撤回时必填)")
chatMessageRecallByBotCmd.Flags().String("keys", "", "消息 processQueryKey 列表,逗号分隔 (必填)")
_ = chatMessageRecallByBotCmd.MarkFlagRequired("keys")
chatMessageSendByWebhookCmd.Flags().String("token", "", "Webhook Token (必填)")
_ = chatMessageSendByWebhookCmd.MarkFlagRequired("token")
chatMessageSendByWebhookCmd.Flags().String("title", "", "消息标题 (必填)")
_ = chatMessageSendByWebhookCmd.MarkFlagRequired("title")
chatMessageSendByWebhookCmd.Flags().String("text", "", "消息内容 (必填)")
_ = chatMessageSendByWebhookCmd.MarkFlagRequired("text")
chatMessageSendByWebhookCmd.Flags().Bool("at-all", false, "@ 所有人")
chatMessageSendByWebhookCmd.Flags().String("at-mobiles", "", "@ 指定手机号,逗号分隔")
chatMessageSendByWebhookCmd.Flags().String("at-users", "", "@ 指定用户,逗号分隔")
chatBotSearchCmd.Flags().Int("page", 1, "页码,从1开始")
chatBotSearchCmd.Flags().Int("size", 0, "每页条数 (默认50)")
chatBotSearchCmd.Flags().Int("limit", 0, "--size 的别名")
_ = chatBotSearchCmd.Flags().MarkHidden("limit")
chatBotSearchCmd.Flags().String("name", "", "按名称搜索")
chatMessageListTopicRepliesCmd.Flags().String("group", "", "群会话 openconversationId (必填)")
_ = chatMessageListTopicRepliesCmd.MarkFlagRequired("group")
chatMessageListTopicRepliesCmd.Flags().String("topic-id", "", "话题 ID,由 dws chat message list 返回 (必填)")
_ = chatMessageListTopicRepliesCmd.MarkFlagRequired("topic-id")
chatMessageListTopicRepliesCmd.Flags().String("time", "", "开始时间,格式: yyyy-MM-dd HH:mm:ss(可选)")
chatMessageListTopicRepliesCmd.Flags().Int("limit", 50, "返回数量(默认 50)")
chatMessageListTopicRepliesCmd.Flags().String("direction", "", "时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(推荐,默认 older)")
chatMessageListTopicRepliesCmd.Flags().String("forward", "false", "true 等价 --direction newer,false 等价 --direction older(默认 false)")
_ = chatMessageListTopicRepliesCmd.Flags().MarkHidden("forward")
chatMessageListAllCmd.Flags().String("start", "", "起始时间,格式: yyyy-MM-dd HH:mm:ss(可选,默认当前时间前 1 天)")
chatMessageListAllCmd.Flags().String("end", "", "结束时间,格式: yyyy-MM-dd HH:mm:ss(可选,默认当前时间)")
chatMessageListAllCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)")
chatMessageListAllCmd.Flags().Int("size", 0, "--limit 的旧版别名")
_ = chatMessageListAllCmd.Flags().MarkHidden("size")
chatMessageListAllCmd.Flags().String("cursor", "0", "分页游标(首页传 \"0\",后续从响应中获取)")
AddPagedMCPFlags(chatMessageListAllCmd)
// list-by-sender flags
chatMessageListBySenderCmd.Flags().String("sender-user-id", "", "发送者 userId(与 --sender-open-dingtalk-id 二选一)")
chatMessageListBySenderCmd.Flags().String("sender", "", "--sender-user-id 的旧版别名")
_ = chatMessageListBySenderCmd.Flags().MarkHidden("sender")
chatMessageListBySenderCmd.Flags().String("sender-open-dingtalk-id", "", "发送者 openDingTalkId(与 --sender-user-id 二选一,适用于无法获取 userId 的场景)")
chatMessageListBySenderCmd.Flags().String("user", "", "")
_ = chatMessageListBySenderCmd.Flags().MarkHidden("user")
chatMessageListBySenderCmd.Flags().String("start", "", "开始时间,ISO-8601 格式(可选,默认当前时间前 7 天)")
chatMessageListBySenderCmd.Flags().String("end", "", "结束时间,ISO-8601 格式(可选,默认当前时间)")
chatMessageListBySenderCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)")
chatMessageListBySenderCmd.Flags().Int("size", 0, "--limit 的旧版别名")
_ = chatMessageListBySenderCmd.Flags().MarkHidden("size")
chatMessageListBySenderCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\",翻页传 nextCursor)")
AddPagedMCPFlags(chatMessageListBySenderCmd)
// list-mentions flags
chatMessageListMentionsCmd.Flags().String("group", "", "群聊 openconversation_id(可选,不传则查全部)")
chatMessageListMentionsCmd.Flags().String("start", "", "开始时间,ISO-8601 格式(可选,默认当前时间前 7 天)")
chatMessageListMentionsCmd.Flags().String("end", "", "结束时间,ISO-8601 格式(可选,默认当前时间)")
chatMessageListMentionsCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)")
chatMessageListMentionsCmd.Flags().Int("size", 0, "--limit 的旧版别名")
_ = chatMessageListMentionsCmd.Flags().MarkHidden("size")
chatMessageListMentionsCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\",翻页传 nextCursor)")
AddPagedMCPFlags(chatMessageListMentionsCmd)
// list-focused flags
chatMessageListFocusedCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)")
chatMessageListFocusedCmd.Flags().Int64("cursor", 0, "分页游标(首次不传或传 0,翻页传 nextCursor)")
AddPagedMCPFlags(chatMessageListFocusedCmd)
// list-top-conversations flags
chatMessageListTopConversationsCmd.Flags().Int("limit", 1000, "每页返回数量(默认 1000)")
chatMessageListTopConversationsCmd.Flags().Int64("cursor", 0, "分页游标(首次不传或传 0,翻页传 nextCursor)")
chatMessageListTopConversationsCmd.Flags().Bool("exclude-muted", false, "是否排除已设置免打扰的会话(默认 false)")
chatMessageListUnreadConversationsCmd.Flags().Int("count", 0, "返回未读会话条数(可选,不传则使用服务端默认值)")
chatMessageListUnreadConversationsCmd.Flags().Bool("exclude-muted", false, "是否排除已设置免打扰的会话(默认 false)")
// message search flags
chatMessageSearchCmd.Flags().String("query", "", "搜索关键词 (必填)")
chatMessageSearchCmd.Flags().String("keyword", "", "--query 的别名")
_ = chatMessageSearchCmd.Flags().MarkHidden("keyword")
chatMessageSearchCmd.Flags().String("group", "", "群聊 openconversation_id(可选,不传则搜索所有会话)")
chatMessageSearchCmd.Flags().String("start", "", "开始时间,ISO-8601 格式(可选,默认当前时间前 7 天)")
chatMessageSearchCmd.Flags().String("end", "", "结束时间,ISO-8601 格式(可选,默认当前时间)")
chatMessageSearchCmd.Flags().Int("limit", 100, "每页返回数量(默认 100)")
chatMessageSearchCmd.Flags().Int("size", 0, "--limit 的旧版别名")
_ = chatMessageSearchCmd.Flags().MarkHidden("size")
chatMessageSearchCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\",翻页传 nextCursor)")
AddPagedMCPFlags(chatMessageSearchCmd)
// read-status flags (主 flag 为 --conversation-id,因为支持群聊和单聊)
chatMessageReadStatusCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,群聊或单聊均可)")
_ = chatMessageReadStatusCmd.MarkFlagRequired("conversation-id")
chatMessageReadStatusCmd.Flags().String("group", "", "--conversation-id 的别名")
_ = chatMessageReadStatusCmd.Flags().MarkHidden("group")
chatMessageReadStatusCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatMessageReadStatusCmd.Flags().MarkHidden("id")
chatMessageReadStatusCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatMessageReadStatusCmd.Flags().MarkHidden("chat")
chatMessageReadStatusCmd.Flags().String("message-id", "", "消息 openMessageId,由 chat message list 返回 (必填)")
_ = chatMessageReadStatusCmd.MarkFlagRequired("message-id")
chatMessageReadStatusCmd.Flags().String("user", "", "目标用户 userId,支持逗号分隔(可选,不传则查所有接收者)")
chatMessageReadStatusCmd.Flags().String("users", "", "目标用户 userId 列表,逗号分隔(可选,不传则查所有接收者)")
chatMessageReadStatusCmd.Flags().String("userId", "", "--user 的别名")
_ = chatMessageReadStatusCmd.Flags().MarkHidden("userId")
chatMessageReadStatusCmd.Flags().String("target-open-dingtalk-ids", "", "目标用户 openDingTalkId 列表,逗号分隔(可选,不传则查所有接收者)")
// search-common flags
chatSearchCommonCmd.Flags().String("nicks", "", "要搜索的昵称列表,逗号分隔 (必填)")
_ = chatSearchCommonCmd.MarkFlagRequired("nicks")
chatSearchCommonCmd.Flags().String("match-mode", "AND", "匹配模式:AND=所有人都在群里,OR=任一人在群里(默认 AND)")
chatSearchCommonCmd.Flags().Int("limit", 20, "每页返回数量(默认 20)")
chatSearchCommonCmd.Flags().Int("size", 0, "--limit 的旧版别名")
_ = chatSearchCommonCmd.Flags().MarkHidden("size")
chatSearchCommonCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\",翻页传 nextCursor)")
chatSearchCommonCmd.Flags().Bool("exclude-muted", false, "是否排除已设置免打扰的群聊(默认 false)")
chatMessageSearchCommonCmd.Flags().String("nicks", "", "要搜索的昵称列表,逗号分隔 (必填)")
_ = chatMessageSearchCommonCmd.MarkFlagRequired("nicks")
chatMessageSearchCommonCmd.Flags().String("match-mode", "AND", "匹配模式:AND=所有人都在群里,OR=任一人在群里(默认 AND)")
chatMessageSearchCommonCmd.Flags().Int("limit", 20, "每页返回数量(默认 20)")
chatMessageSearchCommonCmd.Flags().Int("size", 0, "")
_ = chatMessageSearchCommonCmd.Flags().MarkHidden("size")
chatMessageSearchCommonCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\",翻页传 nextCursor)")
chatMessageSearchCommonCmd.Flags().Bool("exclude-muted", false, "是否排除已设置免打扰的群聊(默认 false)")
chatMessageSearchCommonCmd.Flags().String("group", "", "")
_ = chatMessageSearchCommonCmd.Flags().MarkHidden("group")
// search-advanced flags
chatMessageSearchAdvancedCmd.Flags().String("query", "", "搜索关键词(可选)")
chatMessageSearchAdvancedCmd.Flags().String("keyword", "", "--query 的别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("keyword")
chatMessageSearchAdvancedCmd.Flags().String("user", "", "发送者 userId,支持逗号分隔(可选)")
chatMessageSearchAdvancedCmd.Flags().String("users", "", "发送者 userId 列表,逗号分隔(可选)")
chatMessageSearchAdvancedCmd.Flags().String("userId", "", "--user 的别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("userId")
chatMessageSearchAdvancedCmd.Flags().String("sender-ids", "", "发送者 openDingTalkId 列表,逗号分隔(可选)")
chatMessageSearchAdvancedCmd.Flags().String("senders", "", "--sender-ids 的旧版别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("senders")
chatMessageSearchAdvancedCmd.Flags().String("sender", "", "--sender-ids 的旧版别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("sender")
chatMessageSearchAdvancedCmd.Flags().Bool("at-me", false, "只搜索 @我 的消息(可选,默认 false)")
chatMessageSearchAdvancedCmd.Flags().String("at-ids", "", "@指定人的 openDingTalkId 列表,逗号分隔(可选)")
chatMessageSearchAdvancedCmd.Flags().String("conversation-ids", "", "会话 openConversationId 列表,逗号分隔(可选,群聊或单聊均可,不传则搜索所有会话)")
chatMessageSearchAdvancedCmd.Flags().String("groups", "", "--conversation-ids 的别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("groups")
chatMessageSearchAdvancedCmd.Flags().String("group", "", "")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("group")
chatMessageSearchAdvancedCmd.Flags().String("message-type", "", "下层消息类型过滤值(可选,以当前 IM Schema 支持值为准)")
chatMessageSearchAdvancedCmd.Flags().Bool("only-robot", false, "只搜索机器人消息(可选;显式传 false 时也会传给下层)")
chatMessageSearchAdvancedCmd.Flags().Bool("only-robot-messages", false, "--only-robot 的别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("only-robot-messages")
chatMessageSearchAdvancedCmd.Flags().String("conversation-type", "", "下层会话类型过滤值(可选,以当前 IM Schema 支持值为准)")
chatMessageSearchAdvancedCmd.Flags().String("search-conv-type", "", "--conversation-type 的别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("search-conv-type")
chatMessageSearchAdvancedCmd.Flags().String("start", "", "开始时间,ISO-8601 格式(可选)")
chatMessageSearchAdvancedCmd.Flags().String("end", "", "结束时间,ISO-8601 格式(可选)")
chatMessageSearchAdvancedCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\")")
chatMessageSearchAdvancedCmd.Flags().Int("limit", 100, "每页返回数量(默认 100)")
chatMessageSearchAdvancedCmd.Flags().Int("size", 0, "--limit 的旧版别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("size")
AddPagedMCPFlags(chatMessageSearchAdvancedCmd)
// query-send-status flags
chatMessageQuerySendStatusCmd.Flags().String("open-task-id", "", "消息发送任务 ID (必填)")
_ = chatMessageQuerySendStatusCmd.MarkFlagRequired("open-task-id")
// recall flags
chatMessageRecallCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填)")
chatMessageRecallCmd.Flags().String("group", "", "--conversation-id 的别名")
_ = chatMessageRecallCmd.Flags().MarkHidden("group")
chatMessageRecallCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatMessageRecallCmd.Flags().MarkHidden("id")
chatMessageRecallCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatMessageRecallCmd.Flags().MarkHidden("chat")
chatMessageRecallCmd.Flags().String("msg-id", "", "消息 openMessageId (必填)")
_ = chatMessageRecallCmd.MarkFlagRequired("msg-id")
// edit flags
chatMessageEditCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填)")
chatMessageEditCmd.Flags().String("group", "", "--conversation-id 的别名")
_ = chatMessageEditCmd.Flags().MarkHidden("group")
chatMessageEditCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatMessageEditCmd.Flags().MarkHidden("id")
chatMessageEditCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatMessageEditCmd.Flags().MarkHidden("chat")
chatMessageEditCmd.Flags().String("msg-id", "", "消息 openMessageId (必填)")
_ = chatMessageEditCmd.MarkFlagRequired("msg-id")
chatMessageEditCmd.Flags().String("text", "", "编辑后的 Markdown 正文;与 --content 二选一")
chatMessageEditCmd.Flags().String("title", "", "消息标题;配合 --text 使用,未传时从正文自动生成")
chatMessageEditCmd.Flags().String("content", "", "完整 Markdown content JSON;与 --text 二选一")
chatMessageEditCmd.Flags().Bool("at-all", false, "是否 @所有人;正文未包含 <@all> 时自动补到开头")
chatMessageEditCmd.Flags().String("at-open-dingtalk-ids", "", "@指定成员的 openDingTalkId 列表,逗号分隔")
cli.AnnotateRuntimeRequiredFlags(chatMessageEditCmd, "conversation-id")
cli.AnnotateRuntimeConstraints(chatMessageEditCmd, cli.RuntimeSchemaConstraints{
MutuallyExclusive: [][]string{{"text", "content"}},
RequireOneOf: [][]string{{"text", "content"}},
})
// 别名注册: --conversation-id/--id/--chat → --group (chat message 子命令)
groupAliasCmds := []*cobra.Command{
chatMessageListCmd, chatMessageSendCmd, chatMessageSendByBotCmd,
chatMessageRecallByBotCmd, chatMessageListTopicRepliesCmd, chatMessageListMentionsCmd,
chatMessageSearchCmd,
}
for _, c := range groupAliasCmds {
c.Flags().String("conversation-id", "", "--group 的别名")
_ = c.Flags().MarkHidden("conversation-id")
if c.Flags().Lookup("id") == nil {
c.Flags().String("id", "", "--group 的别名")
_ = c.Flags().MarkHidden("id")
}
c.Flags().String("chat", "", "--group 的别名")
_ = c.Flags().MarkHidden("chat")
}
// conversation-info: 获取会话基础信息
chatConversationInfoCmd := &cobra.Command{
Use: "conversation-info",
Short: "获取会话基础信息",
Long: `获取指定会话的基础信息。
发送本地文件消息请优先使用 dws chat message send --msg-type file --file-path <本地文件>,CLI 不再要求调用方获取或传递 spaceId。`,
Example: ` dws chat conversation-info --group <openConversationId> --format json
dws chat conversation-info --user <userId> --format json
dws chat conversation-info --open-dingtalk-id <openDingTalkId> --format json`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
rawOpenDingTalkID, _ := cmd.Flags().GetString("open-dingtalk-id")
rawUserID := flagOrFallback(cmd, "user", "userId")
specified := 0
for _, value := range []string{groupID, rawUserID, rawOpenDingTalkID} {
if value != "" {
specified++
}
}
if specified > 1 {
return fmt.Errorf("--group, --user and --open-dingtalk-id are mutually exclusive, specify exactly one")
}
if specified == 0 {
return fmt.Errorf("--group, --user or --open-dingtalk-id is required")
}
userID := rawUserID
openDingTalkID := rawOpenDingTalkID
if openDingTalkID != "" {
if err := targetresolver.ValidateExplicitOpenDingTalkID("--open-dingtalk-id", openDingTalkID); err != nil {
return err
}
}
if userID != "" && isOpenDingTalkID(userID) {
openDingTalkID = userID
userID = ""
}
toolArgs := map[string]any{}
if groupID != "" {
toolArgs["openConversationId"] = groupID
}
if userID != "" {
// 服务端 get_conversation_info 单聊只认 openDingTalkId(传 userId 键会被
// 忽略并报「openCid、cid、peerUid不能同时为空」)。这里把 --user 的 userId
// 先解析成 openDingTalkId 再走与 --open-dingtalk-id 相同的通路。
resolved, rerr := resolveOpenDingTalkID(context.Background(), userID)
if rerr != nil {
return rerr
}
toolArgs["openDingTalkId"] = resolved
}
if openDingTalkID != "" {
toolArgs["openDingTalkId"] = openDingTalkID
}
return callMCPToolOnServer("chat", "get_conversation_info", toolArgs)
},
}
DeclareLeafMetadata(chatConversationInfoCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "get_conversation_info",
CanonicalPath: "chat.get_conversation_info",
CLIPath: "chat conversation-info",
PrimaryCLIPath: "chat conversation-info",
},
Description: "获取群聊或单聊会话的详细信息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "get_conversation_info"},
},
Selection: contract.SelectionSpec{
AgentSummary: "获取群聊或单聊会话的详细信息",
UseWhen: []string{"已知群 ID 或用户标识并需要解析会话详情时"},
AvoidWhen: []string{"按群名查找会话时使用 chat search"},
Examples: []string{"dws chat conversation-info --group <openConversationId> --format json"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
{Name: "open-dingtalk-id", Property: "openDingTalkId"},
{Name: "user", Property: "openDingTalkId"},
},
},
})
chatConversationInfoCmd.Flags().String("group", "", "群聊 openConversationId(群聊时使用)")
chatConversationInfoCmd.Flags().String("conversation-id", "", "--group 的别名")
chatConversationInfoCmd.Flags().String("id", "", "--group 的别名")
chatConversationInfoCmd.Flags().String("chat", "", "--group 的别名")
_ = chatConversationInfoCmd.Flags().MarkHidden("conversation-id")
_ = chatConversationInfoCmd.Flags().MarkHidden("id")
_ = chatConversationInfoCmd.Flags().MarkHidden("chat")
chatConversationInfoCmd.Flags().String("user", "", "单聊对方 userId(单聊时使用)")
chatConversationInfoCmd.Flags().String("userId", "", "--user 的别名")
_ = chatConversationInfoCmd.Flags().MarkHidden("userId")
chatConversationInfoCmd.Flags().String("open-dingtalk-id", "", "单聊对方 openDingTalkId(单聊时使用)")
// ── file 子命令(会话文件上传,不暴露 spaceId)───────────────
chatFileCmd := &cobra.Command{
Use: "file",
Short: "会话文件上传(已下线)",
Hidden: true,
RunE: groupRunE,
}
chatFileUploadCmd := &cobra.Command{
Use: "upload",
Short: "上传本地文件或 URL 文件到会话文件空间(已下线)",
Hidden: true,
Long: `chat file upload 已下线,不再调用 chat/upload_conversation_file_by_url。
发送本地文件消息请改用 chat message send --msg-type file --file-path;该路径仍然可用,CLI 内部会完成本地文件上传和消息发送。`,
Example: ` dws chat message send --group <openConversationId> --msg-type file --file-path ./report.pdf --format json
dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file-path ./report.pdf --format json`,
RunE: func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("chat file upload 已下线;chat/upload_conversation_file_by_url 当前不可用。发送本地文件请改用: dws chat message send --msg-type file --file-path <本地路径>")
},
}
chatFileUploadCmd.Flags().String("group", "", "群聊 openConversationId(群聊时使用)")
chatFileUploadCmd.Flags().String("conversation-id", "", "--group 的别名")
_ = chatFileUploadCmd.Flags().MarkHidden("conversation-id")
chatFileUploadCmd.Flags().String("id", "", "--group 的别名")
_ = chatFileUploadCmd.Flags().MarkHidden("id")
chatFileUploadCmd.Flags().String("chat", "", "--group 的别名")
_ = chatFileUploadCmd.Flags().MarkHidden("chat")
chatFileUploadCmd.Flags().String("user", "", "单聊对方 userId(单聊时使用)")
chatFileUploadCmd.Flags().String("userId", "", "--user 的别名")
_ = chatFileUploadCmd.Flags().MarkHidden("userId")
chatFileUploadCmd.Flags().String("open-dingtalk-id", "", "单聊对方 openDingTalkId(单聊时使用)")
chatFileUploadCmd.Flags().String("file", "", "本地文件路径(与 --url 二选一)")
chatFileUploadCmd.Flags().String("url", "", "远程文件 URL(与 --file 二选一,服务端代传)")
chatFileUploadCmd.Flags().String("file-name", "", "文件名(可选,本地文件默认取文件名,URL 默认从 URL 推断)")
chatFileUploadCmd.Flags().String("md5", "", "文件 MD5(可选,本地文件不传时自动计算)")
chatFileUploadCmd.Flags().String("uuid", "", "幂等 UUID(可选)")
chatFileCmd.AddCommand(chatFileUploadCmd)
// ── category 子命令(会话分组,走 IM MCP)───────────────────
chatCategoryCmd := &cobra.Command{Use: "category", Short: "会话分组管理", RunE: groupRunE}
chatCategoryListCmd := &cobra.Command{
Use: "list",
Short: "获取用户自定义会话分组",
Example: ` dws chat category list
# 返回当前用户的所有自定义会话分组`,
RunE: func(cmd *cobra.Command, args []string) error {
return callMCPToolOnServer("im", "list_user_define_conv_categories", map[string]any{})
},
}
DeclareLeafMetadata(chatCategoryListCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_user_define_conv_categories",
CanonicalPath: "chat.list_user_define_conv_categories",
CLIPath: "chat category list",
PrimaryCLIPath: "chat category list",
},
Description: "列出当前用户的自定义会话分组",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_user_define_conv_categories"},
},
Selection: contract.SelectionSpec{
AgentSummary: "列出当前用户的自定义会话分组",
UseWhen: []string{"需要取得分组 ID 或浏览分组配置时"},
AvoidWhen: []string{"需要查看某个分组内会话时使用 chat category list-conversations"},
Examples: []string{"dws chat category list"},
},
},
})
chatCategoryConvsCmd := &cobra.Command{
Use: "list-conversations",
Short: "拉取指定自定义会话分组下的会话",
Example: ` dws chat category list-conversations --category-id <分组ID>
# 分组ID 可通过 dws chat category list 获取`,
RunE: func(cmd *cobra.Command, args []string) error {
categoryId, _ := cmd.Flags().GetInt("category-id")
toolArgs := map[string]any{
"categoryId": categoryId,
}
if v, _ := cmd.Flags().GetBool("exclude-muted"); v {
toolArgs["excludeMuted"] = true
}
return callMCPToolOnServer("im", "list_conversations_by_category", toolArgs)
},
}
DeclareLeafMetadata(chatCategoryConvsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_conversations_by_category",
CanonicalPath: "chat.list_conversations_by_category",
CLIPath: "chat category list-conversations",
PrimaryCLIPath: "chat category list-conversations",
},
Description: "列出指定会话分组中的会话",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_conversations_by_category"},
},
Selection: contract.SelectionSpec{
AgentSummary: "列出指定会话分组中的会话",
UseWhen: []string{"已知分组 ID 并需要查看其中会话时"},
AvoidWhen: []string{"只需列出分组本身时使用 chat category list"},
Examples: []string{"dws chat category list-conversations --category-id 123"},
},
},
})
chatCategoryCreateCmd := &cobra.Command{
Use: "create",
Short: "创建用户自定义会话分组",
Example: ` dws chat category create --title "工作群"
dws chat category create --title "项目组"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "title"); err != nil {
return err
}
title, err := validatedConversationCategoryTitle(mustGetFlag(cmd, "title"))
if err != nil {
return err
}
return callMCPToolOnServer("im", "create_conv_category", map[string]any{
"title": title,
})
},
}
DeclareLeafMetadata(chatCategoryCreateCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "create_conv_category",
CanonicalPath: "chat." + "create_conv_category",
CLIPath: "chat category create",
PrimaryCLIPath: "chat category create",
},
Description: "创建用户自定义会话分组",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "create_conv_category"},
},
Selection: contract.SelectionSpec{
AgentSummary: "创建用户自定义会话分组",
UseWhen: []string{"需要新建一个手工维护的会话分组时"},
AvoidWhen: []string{"需要按规则自动归集会话时使用 chat category create-smart"},
Examples: []string{"dws chat category create --title \"工作群\""},
},
Parameters: []contract.ParamDecl{
{Name: "title", Property: "title", Required: boolPtr(true)},
},
},
})
chatCategoryDeleteCmd := &cobra.Command{
Use: "delete",
Short: "删除用户自定义会话分组",
Long: "删除用户自定义会话分组。该操作不可逆;必须先获得用户确认,再追加 --yes 执行。",
Example: ` dws chat category delete --category-id <分组ID>
# 分组ID 可通过 dws chat category list 获取`,
RunE: func(cmd *cobra.Command, args []string) error {
categoryId, _ := cmd.Flags().GetInt64("category-id")
if categoryId == 0 {
return fmt.Errorf("flag --category-id is required")
}
if !commandBoolFlag(cmd, "yes") {
return apperrors.NewValidation(
"删除会话分组不可逆;获得用户确认后加 --yes 执行",
apperrors.WithReason("confirmation_required"),
apperrors.WithHint("先确认目标分组及影响范围;用户明确同意后以相同参数追加 --yes"),
apperrors.WithActions("确认目标会话分组", "获得用户确认后使用 --yes 执行"),
)
}
return callMCPToolOnServer("im", "delete_conv_category", map[string]any{
"categoryId": categoryId,
})
},
}
DeclareLeafMetadata(chatCategoryDeleteCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "destructive", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "delete_conv_category",
CanonicalPath: "chat." + "delete_conv_category",
CLIPath: "chat category delete",
PrimaryCLIPath: "chat category delete",
},
Description: "删除用户自定义会话分组",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "delete_conv_category"},
},
Selection: contract.SelectionSpec{
AgentSummary: "删除用户自定义会话分组",
UseWhen: []string{"用户明确要删除某个自定义会话分组,且已确认不影响会话本身"},
AvoidWhen: []string{"只是从分组移出某个会话时使用 chat category remove-conv"},
Examples: []string{"dws chat category delete --category-id 123"},
},
Parameters: []contract.ParamDecl{
{Name: "category-id", Property: "categoryId", Required: boolPtr(true)},
{Name: "yes", Property: "", Required: boolPtr(false)},
},
},
})
chatCategoryRenameCmd := &cobra.Command{
Use: "rename",
Short: "更新用户自定义会话分组的名称",
Example: ` dws chat category rename --category-id <分组ID> --title "新名称"
# 分组ID 可通过 dws chat category list 获取`,
RunE: func(cmd *cobra.Command, args []string) error {
categoryId, _ := cmd.Flags().GetInt64("category-id")
if categoryId == 0 {
return fmt.Errorf("flag --category-id is required")
}
if err := validateRequiredFlags(cmd, "title"); err != nil {
return err
}
title, err := validatedConversationCategoryTitle(mustGetFlag(cmd, "title"))
if err != nil {
return err
}
return callMCPToolOnServer("im", "rename_conv_category", map[string]any{
"categoryId": categoryId,
"title": title,
})
},
}
DeclareLeafMetadata(chatCategoryRenameCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "rename_conv_category",
CanonicalPath: "chat." + "rename_conv_category",
CLIPath: "chat category rename",
PrimaryCLIPath: "chat category rename",
},
Description: "更新用户自定义会话分组的名称",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "rename_conv_category"},
},
Selection: contract.SelectionSpec{
AgentSummary: "更新用户自定义会话分组的名称",
UseWhen: []string{"需要给已有自定义会话分组改名时"},
AvoidWhen: []string{"需要新建分组时使用 chat category create"},
Examples: []string{"dws chat category rename --category-id 123 --title \"新名称\""},
},
Parameters: []contract.ParamDecl{
{Name: "category-id", Property: "categoryId", Required: boolPtr(true)},
{Name: "title", Property: "title", Required: boolPtr(true)},
},
},
})
chatCategoryAddConvCmd := &cobra.Command{
Use: "add-conv",
Short: "将会话移动到指定的自定义分组中",
Long: `将某个会话添加到一批用户自定义会话分组中。需指定会话 openConversationId 和目标分组 ID 列表。`,
Example: ` dws chat category add-conv --group <openConversationId> --category-ids 123,456
# 分组ID 可通过 dws chat category list 获取
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id")
if groupID == "" {
return fmt.Errorf("flag --group is required")
}
if err := validateRequiredFlags(cmd, "category-ids"); err != nil {
return err
}
categoryIds, err := parseCSVInt64(mustGetFlag(cmd, "category-ids"))
if err != nil {
return fmt.Errorf("--category-ids: %w", err)
}
return callMCPToolOnServer("im", "add_conv_to_categories", map[string]any{
"openConversationId": groupID,
"categoryIds": categoryIds,
})
},
}
DeclareLeafMetadata(chatCategoryAddConvCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "add_conv_to_categories",
CanonicalPath: "chat." + "add_conv_to_categories",
CLIPath: "chat category add-conv",
PrimaryCLIPath: "chat category add-conv",
},
Description: "将会话加入一个或多个自定义会话分组",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "add_conv_to_categories"},
},
Selection: contract.SelectionSpec{
AgentSummary: "将会话加入一个或多个自定义会话分组",
UseWhen: []string{"已有会话 openConversationId 和目标 categoryId,需要把会话归入分组时"},
AvoidWhen: []string{"从分组移除会话时使用 chat category remove-conv"},
Examples: []string{"dws chat category add-conv --group <openConversationId> --category-ids 123,456"},
},
Parameters: []contract.ParamDecl{
{Name: "category-ids", Property: "categoryIds", Required: boolPtr(true), InterfaceType: "array"},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
chatCategoryRemoveConvCmd := &cobra.Command{
Use: "remove-conv",
Short: "将会话从指定的自定义分组中移出",
Long: `将某个会话从一批用户自定义会话分组中移出。需指定会话 openConversationId 和目标分组 ID 列表。`,
Example: ` dws chat category remove-conv --group <openConversationId> --category-ids 123,456
# 分组ID 可通过 dws chat category list 获取
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id")
if groupID == "" {
return fmt.Errorf("flag --group is required")
}
if err := validateRequiredFlags(cmd, "category-ids"); err != nil {
return err
}
categoryIds, err := parseCSVInt64(mustGetFlag(cmd, "category-ids"))
if err != nil {
return fmt.Errorf("--category-ids: %w", err)
}
return callMCPToolOnServer("im", "remove_conv_from_categories", map[string]any{
"openConversationId": groupID,
"categoryIds": categoryIds,
})
},
}
DeclareLeafMetadata(chatCategoryRemoveConvCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "remove_conv_from_categories",
CanonicalPath: "chat." + "remove_conv_from_categories",
CLIPath: "chat category remove-conv",
PrimaryCLIPath: "chat category remove-conv",
},
Description: "将会话从一个或多个自定义会话分组移出",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "remove_conv_from_categories"},
},
Selection: contract.SelectionSpec{
AgentSummary: "将会话从一个或多个自定义会话分组移出",
UseWhen: []string{"已有会话 openConversationId 和 categoryId,需要取消会话分组归属时"},
AvoidWhen: []string{"向分组加入会话时使用 chat category add-conv"},
Examples: []string{"dws chat category remove-conv --group <openConversationId> --category-ids 123,456"},
},
Parameters: []contract.ParamDecl{
{Name: "category-ids", Property: "categoryIds", Required: boolPtr(true), InterfaceType: "array"},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
chatCategoryListByConvCmd := &cobra.Command{
Use: "list-by-conv",
Short: "拉取指定会话所属的用户自定义会话分组",
Long: `拉取指定会话所属的用户自定义会话分组。需指定会话 openConversationId。`,
Example: ` dws chat category list-by-conv --group <openConversationId>
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id")
if groupID == "" {
return fmt.Errorf("flag --group is required")
}
return callMCPToolOnServer("im", "list_conv_categories_by_conv", map[string]any{
"openConversationId": groupID,
})
},
}
DeclareLeafMetadata(chatCategoryListByConvCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_conv_categories_by_conv",
CanonicalPath: "chat.list_conv_categories_by_conv",
CLIPath: "chat category list-by-conv",
PrimaryCLIPath: "chat category list-by-conv",
},
Description: "查询指定会话所属的自定义会话分组",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: the executable CLI maps a conversation locator to im/list_conv_categories_by_conv, which is absent from the pinned MCP metadata snapshot.",
},
Selection: contract.SelectionSpec{
AgentSummary: "查询指定会话所属的自定义会话分组",
UseWhen: []string{"已有会话 openConversationId,需要反查该会话被放入了哪些自定义分组"},
AvoidWhen: []string{"列出全部自定义分组应使用 chat category list;按 categoryId 查详情应使用 chat category batch-info"},
Examples: []string{"dws chat category list-by-conv --group <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
chatCategoryBatchInfoCmd := &cobra.Command{
Use: "batch-info",
Short: "批量拉取用户自定义会话分组信息",
Long: `根据分组 ID 列表批量拉取用户自定义会话分组信息。分组 ID 使用逗号分隔。`,
Example: ` dws chat category batch-info --category-ids 123,456
# 分组ID 可通过 dws chat category list 获取`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "category-ids"); err != nil {
return err
}
categoryIDs, err := parseCSVInt64(mustGetFlag(cmd, "category-ids"))
if err != nil {
return fmt.Errorf("--category-ids: %w", err)
}
return callMCPToolOnServer("im", "get_conv_categories_info", map[string]any{
"categoryIds": categoryIDs,
})
},
}
DeclareLeafMetadata(chatCategoryBatchInfoCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "get_conv_categories_info",
CanonicalPath: "chat.get_conv_categories_info",
CLIPath: "chat category batch-info",
PrimaryCLIPath: "chat category batch-info",
},
Description: "按分组 ID 批量获取自定义会话分组详情",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: the executable CLI parses category IDs and calls im/get_conv_categories_info, which is absent from the pinned MCP metadata snapshot.",
},
Selection: contract.SelectionSpec{
AgentSummary: "按分组 ID 批量获取自定义会话分组详情",
UseWhen: []string{"已经有一个或多个会话分组 categoryId,需要批量读取分组信息"},
AvoidWhen: []string{"不知道 categoryId 时先用 chat category list;按会话反查所属分组应使用 chat category list-by-conv"},
Examples: []string{"dws chat category batch-info --category-ids 123,456"},
},
Parameters: []contract.ParamDecl{
{Name: "category-ids", Property: "categoryIds", Required: boolPtr(true), InterfaceType: "array"},
},
},
})
// ── group get-by-group-id(走 IM MCP)─────────────────────────
chatGroupInfoByIdCmd := &cobra.Command{
Use: "get-by-group-id",
Short: "根据群号获取群聊信息",
Example: ` dws chat group get-by-group-id --group-id 12345678
# 群号为数字类型的群ID`,
RunE: func(cmd *cobra.Command, args []string) error {
groupId, _ := cmd.Flags().GetInt64("group-id")
return callMCPToolOnServer("im", "get_conv_info_by_group_id", map[string]any{
"groupId": groupId,
})
},
}
DeclareLeafMetadata(chatGroupInfoByIdCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "get_conv_info_by_group_id",
CanonicalPath: "chat.get_conv_info_by_group_id",
CLIPath: "chat group get-by-group-id",
PrimaryCLIPath: "chat group get-by-group-id",
},
Description: "把数字群号解析为群聊信息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "get_conv_info_by_group_id"},
},
Selection: contract.SelectionSpec{
AgentSummary: "把数字群号解析为群聊信息",
UseWhen: []string{"用户只提供数字群号且需要 openConversationId 时"},
AvoidWhen: []string{"已经持有 openConversationId 时直接使用目标命令"},
Examples: []string{"dws chat group get-by-group-id --group-id 12345678"},
},
},
})
// ── message 新增命令(批量查消息、emoji 表情、文字表情)─────
chatMessageListByIdsCmd := &cobra.Command{
Use: "list-by-ids",
Short: "根据消息 ID 批量查询消息",
Example: ` dws chat message list-by-ids --msg-ids msgId1,msgId2,msgId3
# 最多传 50 条消息 ID`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "msg-ids"); err != nil {
return err
}
msgIds := parseCSVValues(mustGetFlag(cmd, "msg-ids"))
if len(msgIds) > 50 {
return fmt.Errorf("--msg-ids 最多支持 50 条,当前 %d 条", len(msgIds))
}
return callMCPToolOnServer("im", "list_messages_by_ids", map[string]any{
"openMsgIds": msgIds,
})
},
}
DeclareLeafMetadata(chatMessageListByIdsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_messages_by_ids",
CanonicalPath: "chat.list_messages_by_ids",
CLIPath: "chat message list-by-ids",
PrimaryCLIPath: "chat message list-by-ids",
},
Description: "按消息 ID 批量获取消息详情",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_messages_by_ids"},
},
Selection: contract.SelectionSpec{
AgentSummary: "按消息 ID 批量获取消息详情",
UseWhen: []string{"已经持有一组 msgId 并需要精确取回消息时"},
AvoidWhen: []string{"只有关键词或时间范围时使用 search 或 list 命令"},
Examples: []string{"dws chat message list-by-ids --msg-ids msgId1,msgId2"},
},
Parameters: []contract.ParamDecl{
{Name: "msg-ids", Property: "openMsgIds"},
},
},
})
chatMessageListByIdsCmd.Flags().String("msg-ids", "", "消息 ID 列表,逗号分隔,最多 50 条 (必填)")
_ = chatMessageListByIdsCmd.MarkFlagRequired("msg-ids")
chatMessageAddEmojiCmd := &cobra.Command{
Use: "add-emoji",
Short: "对消息添加 emoji 表情回应",
Example: ` dws chat message add-emoji --conversation-id <openConversationId> --msg-id <openMsgId> --emoji "赞"
# 查询会话 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil {
return err
}
if err := validateRequiredFlags(cmd, "msg-id", "emoji"); err != nil {
return err
}
return callMCPToolOnServer("im", "add_emoji_reaction", map[string]any{
"openConversationId": flagOrFallback(cmd, "conversation-id", "group", "id", "chat"),
"openMsgId": mustGetFlag(cmd, "msg-id"),
"emojiName": mustGetFlag(cmd, "emoji"),
})
},
}
DeclareLeafMetadata(chatMessageAddEmojiCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "add_emoji_reaction",
CanonicalPath: "chat.add_emoji_reaction",
CLIPath: "chat message add-emoji",
PrimaryCLIPath: "chat message add-emoji",
},
Description: "给指定消息添加表情回应",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "add_emoji_reaction"},
},
Selection: contract.SelectionSpec{
AgentSummary: "给指定消息添加表情回应",
UseWhen: []string{"需要对已有消息添加一个 emoji reaction 时"},
AvoidWhen: []string{"发送文本消息或文字表情时不要使用"},
Examples: []string{"dws chat message add-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji \"赞\""},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "emoji", Property: "emojiName"},
{Name: "msg-id", Property: "openMsgId"},
},
},
})
chatMessageAddEmojiCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
chatMessageAddEmojiCmd.Flags().String("group", "", "--conversation-id 的别名")
chatMessageAddEmojiCmd.Flags().String("id", "", "--conversation-id 的别名")
chatMessageAddEmojiCmd.Flags().String("chat", "", "--conversation-id 的别名")
chatMessageAddEmojiCmd.Flags().String("msg-id", "", "消息 openMsgId (必填)")
chatMessageAddEmojiCmd.Flags().String("emoji", "", "emoji 表情名称 (必填)")
chatMessageRemoveEmojiCmd := &cobra.Command{
Use: "remove-emoji",
Short: "移除消息的 emoji 表情回应",
Example: ` dws chat message remove-emoji --conversation-id <openConversationId> --msg-id <openMsgId> --emoji "赞"
# 查询会话 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil {
return err
}
if err := validateRequiredFlags(cmd, "msg-id", "emoji"); err != nil {
return err
}
return callMCPToolOnServer("im", "remove_emoji_reaction", map[string]any{
"openConversationId": flagOrFallback(cmd, "conversation-id", "group", "id", "chat"),
"openMsgId": mustGetFlag(cmd, "msg-id"),
"emojiName": mustGetFlag(cmd, "emoji"),
})
},
}
DeclareLeafMetadata(chatMessageRemoveEmojiCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "remove_emoji_reaction",
CanonicalPath: "chat.remove_emoji_reaction",
CLIPath: "chat message remove-emoji",
PrimaryCLIPath: "chat message remove-emoji",
},
Description: "移除指定消息上的表情回应",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "remove_emoji_reaction"},
},
Selection: contract.SelectionSpec{
AgentSummary: "移除指定消息上的表情回应",
UseWhen: []string{"需要取消此前添加的 emoji reaction 时"},
AvoidWhen: []string{"移除文字表情时使用 chat message remove-text-emotion"},
Examples: []string{"dws chat message remove-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji \"赞\""},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "emoji", Property: "emojiName"},
{Name: "msg-id", Property: "openMsgId"},
},
},
})
chatMessageRemoveEmojiCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
chatMessageRemoveEmojiCmd.Flags().String("group", "", "--conversation-id 的别名")
chatMessageRemoveEmojiCmd.Flags().String("id", "", "--conversation-id 的别名")
chatMessageRemoveEmojiCmd.Flags().String("chat", "", "--conversation-id 的别名")
chatMessageRemoveEmojiCmd.Flags().String("msg-id", "", "消息 openMsgId (必填)")
chatMessageRemoveEmojiCmd.Flags().String("emoji", "", "emoji 表情名称 (必填)")
chatMessageAddTextEmotionCmd := &cobra.Command{
Use: "add-text-emotion",
Short: "对消息添加文字表情回应",
Example: ` dws chat message add-text-emotion --conversation-id <openConversationId> --msg-id <openMsgId> --emotion-id <emotionId> --emotion-name "赞" --text "nice" --background-id im_bg_5`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil {
return err
}
if err := validateRequiredFlags(cmd, "msg-id", "emotion-id", "emotion-name", "text", "background-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "add_text_emotion", map[string]any{
"openConversationId": flagOrFallback(cmd, "conversation-id", "group", "id", "chat"),
"openMsgId": mustGetFlag(cmd, "msg-id"),
"emotionId": mustGetFlag(cmd, "emotion-id"),
"emotionName": mustGetFlag(cmd, "emotion-name"),
"text": mustGetFlag(cmd, "text"),
"backgroundId": mustGetFlag(cmd, "background-id"),
})
},
}
DeclareLeafMetadata(chatMessageAddTextEmotionCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "add_text_emotion",
CanonicalPath: "chat.add_text_emotion",
CLIPath: "chat message add-text-emotion",
PrimaryCLIPath: "chat message add-text-emotion",
},
Description: "给指定消息添加已定义的文字表情",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "add_text_emotion"},
},
Selection: contract.SelectionSpec{
AgentSummary: "给指定消息添加已定义的文字表情",
UseWhen: []string{"已有文字表情定义并要附加到消息时"},
AvoidWhen: []string{"需要先创建文字表情资源时使用 chat message create-text-emotion"},
Examples: []string{"dws chat message add-text-emotion --conversation-id <openConversationId> --msg-id <openMessageId> --emotion-id <emotionId> --emotion-name \"赞\" --text \"nice\" --background-id im_bg_5"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "msg-id", Property: "openMsgId"},
},
},
})
chatMessageAddTextEmotionCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
chatMessageAddTextEmotionCmd.Flags().String("group", "", "--conversation-id 的别名")
chatMessageAddTextEmotionCmd.Flags().String("id", "", "--conversation-id 的别名")
chatMessageAddTextEmotionCmd.Flags().String("chat", "", "--conversation-id 的别名")
chatMessageAddTextEmotionCmd.Flags().String("msg-id", "", "消息 openMsgId (必填)")
chatMessageAddTextEmotionCmd.Flags().String("emotion-id", "", "表情 ID (必填,通过 create-text-emotion 获取)")
chatMessageAddTextEmotionCmd.Flags().String("emotion-name", "", "表情名称 (必填)")
chatMessageAddTextEmotionCmd.Flags().String("text", "", "文字内容 (必填)")
chatMessageAddTextEmotionCmd.Flags().String("background-id", "", "背景 ID (必填)")
chatMessageRemoveTextEmotionCmd := &cobra.Command{
Use: "remove-text-emotion",
Short: "移除消息的文字表情回应",
Example: ` dws chat message remove-text-emotion --conversation-id <openConversationId> --msg-id <openMsgId> --emotion-id <emotionId> --emotion-name "赞" --text "nice" --background-id <backgroundId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil {
return err
}
if err := validateRequiredFlags(cmd, "msg-id", "emotion-id", "emotion-name", "text", "background-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "remove_text_emotion", map[string]any{
"openConversationId": flagOrFallback(cmd, "conversation-id", "group", "id", "chat"),
"openMsgId": mustGetFlag(cmd, "msg-id"),
"emotionId": mustGetFlag(cmd, "emotion-id"),
"emotionName": mustGetFlag(cmd, "emotion-name"),
"text": mustGetFlag(cmd, "text"),
"backgroundId": mustGetFlag(cmd, "background-id"),
})
},
}
DeclareLeafMetadata(chatMessageRemoveTextEmotionCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "remove_text_emotion",
CanonicalPath: "chat.remove_text_emotion",
CLIPath: "chat message remove-text-emotion",
PrimaryCLIPath: "chat message remove-text-emotion",
},
Description: "移除指定消息上的文字表情回应",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "remove_text_emotion"},
},
Selection: contract.SelectionSpec{
AgentSummary: "移除指定消息上的文字表情回应",
UseWhen: []string{"需要取消已添加的文字表情时"},
AvoidWhen: []string{"移除普通 emoji reaction 时使用 chat message remove-emoji"},
Examples: []string{"dws chat message remove-text-emotion --conversation-id <openConversationId> --msg-id <openMessageId> --emotion-id <emotionId> --emotion-name \"赞\" --text \"nice\" --background-id im_bg_5"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "msg-id", Property: "openMsgId"},
},
},
})
chatMessageRemoveTextEmotionCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
chatMessageRemoveTextEmotionCmd.Flags().String("group", "", "--conversation-id 的别名")
chatMessageRemoveTextEmotionCmd.Flags().String("id", "", "--conversation-id 的别名")
chatMessageRemoveTextEmotionCmd.Flags().String("chat", "", "--conversation-id 的别名")
chatMessageRemoveTextEmotionCmd.Flags().String("msg-id", "", "消息 openMsgId (必填)")
chatMessageRemoveTextEmotionCmd.Flags().String("emotion-id", "", "表情 ID (必填)")
chatMessageRemoveTextEmotionCmd.Flags().String("emotion-name", "", "表情名称 (必填)")
chatMessageRemoveTextEmotionCmd.Flags().String("text", "", "文字内容 (必填)")
chatMessageRemoveTextEmotionCmd.Flags().String("background-id", "", "背景 ID (必填)")
chatMessageUpdateTextEmotionCmd := &cobra.Command{
Use: "update-text-emotion",
Short: "更新消息的文字表情回应",
Example: ` dws chat message update-text-emotion --conversation-id <openConversationId> --msg-id <openMsgId> --old-emotion-id <oldEmotionId> --emotion-id <emotionId> --emotion-name "赞" --text "nice" --background-id im_bg_5`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil {
return err
}
return callMCPToolOnServer("im", "update_text_emotion", map[string]any{
"openConversationId": flagOrFallback(cmd, "conversation-id", "group", "id", "chat"),
"openMsgId": mustGetFlag(cmd, "msg-id"),
"oldEmotionId": mustGetFlag(cmd, "old-emotion-id"),
"emotionId": mustGetFlag(cmd, "emotion-id"),
"emotionName": mustGetFlag(cmd, "emotion-name"),
"text": mustGetFlag(cmd, "text"),
"backgroundId": mustGetFlag(cmd, "background-id"),
})
},
}
DeclareLeafMetadata(chatMessageUpdateTextEmotionCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_text_emotion",
CanonicalPath: "chat.update_text_emotion",
CLIPath: "chat message update-text-emotion",
PrimaryCLIPath: "chat message update-text-emotion",
},
Description: "把消息上已有的文字表情原地替换为新的文字表情",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: the executable CLI calls im/update_text_emotion, which is proven by dws-wukong develop@30f13f02 but absent from the pinned MCP metadata snapshot; no pinned interface_ref can represent the command yet.",
},
Selection: contract.SelectionSpec{
AgentSummary: "把消息上已有的文字表情原地替换为新的文字表情",
UseWhen: []string{"需要更新消息的状态文字或表情,并避免先移除再添加造成闪烁和两次网络调用时"},
AvoidWhen: []string{
"消息上还没有文字表情时使用 chat message add-text-emotion",
"只需清除文字表情时使用 chat message remove-text-emotion",
},
Examples: []string{"dws chat message update-text-emotion --conversation-id <openConversationId> --msg-id <openMessageId> --old-emotion-id <oldEmotionId> --emotion-id <emotionId> --emotion-name \"处理中\" --text \"处理中 2 分钟\" --background-id im_bg_5"},
},
Parameters: []contract.ParamDecl{
{Name: "background-id", Property: "backgroundId", Required: boolPtr(true), Description: "新文字表情的背景 ID"},
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false), Description: "会话 openConversationId;与 --group、--id、--chat 四选一"},
{Name: "emotion-id", Property: "emotionId", Required: boolPtr(true), Description: "新的文字表情 ID,可通过 create-text-emotion 获取"},
{Name: "emotion-name", Property: "emotionName", Required: boolPtr(true), Description: "新的文字表情名称"},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "msg-id", Property: "openMsgId", Required: boolPtr(true), Description: "需要原地更新文字表情的消息 openMsgId"},
{Name: "old-emotion-id", Property: "oldEmotionId", Required: boolPtr(true), Description: "消息上当前文字表情的 emotionId"},
{Name: "text", Property: "text", Required: boolPtr(true), Description: "新的文字表情内容"},
},
},
})
chatMessageUpdateTextEmotionCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
chatMessageUpdateTextEmotionCmd.Flags().String("group", "", "--conversation-id 的别名")
chatMessageUpdateTextEmotionCmd.Flags().String("id", "", "--conversation-id 的别名")
chatMessageUpdateTextEmotionCmd.Flags().String("chat", "", "--conversation-id 的别名")
chatMessageUpdateTextEmotionCmd.Flags().String("msg-id", "", "消息 openMsgId (必填)")
chatMessageUpdateTextEmotionCmd.Flags().String("old-emotion-id", "", "待更新的原表情 ID (必填)")
chatMessageUpdateTextEmotionCmd.Flags().String("emotion-id", "", "新表情 ID (必填)")
chatMessageUpdateTextEmotionCmd.Flags().String("emotion-name", "", "新表情名称 (必填)")
chatMessageUpdateTextEmotionCmd.Flags().String("text", "", "新文字内容 (必填)")
chatMessageUpdateTextEmotionCmd.Flags().String("background-id", "", "新背景 ID (必填)")
chatMessageUpdateTextEmotionCmd.MarkFlagsOneRequired("conversation-id", "group", "id", "chat")
for _, name := range []string{"msg-id", "old-emotion-id", "emotion-id", "emotion-name", "text", "background-id"} {
_ = chatMessageUpdateTextEmotionCmd.MarkFlagRequired(name)
}
// ── 创建文字表情(获取 emotionId)──────────────────────
chatMessageCreateTextEmotionCmd := &cobra.Command{
Use: "create-text-emotion",
Short: "创建文字表情(获取 emotionId)",
Long: `创建一个新的文字表情模板。当内置表情(见 chat-emoji-list.md)中没有所需表情时,使用此命令创建并获取 emotionId,随后可用于 add-text-emotion。`,
Example: ` dws chat message create-text-emotion --emotion-name "赞" --text "nice"
dws chat message create-text-emotion --emotion-name "感谢" --text "感谢" --background-id im_bg_5`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "emotion-name", "text"); err != nil {
return err
}
params := map[string]any{
"emotionName": mustGetFlag(cmd, "emotion-name"),
"text": mustGetFlag(cmd, "text"),
}
if v, _ := cmd.Flags().GetString("background-id"); v != "" {
params["backgroundId"] = v
}
return callMCPToolOnServer("im", "create_text_emotion", params)
},
}
DeclareLeafMetadata(chatMessageCreateTextEmotionCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "create_text_emotion",
CanonicalPath: "chat.create_text_emotion",
CLIPath: "chat message create-text-emotion",
PrimaryCLIPath: "chat message create-text-emotion",
},
Description: "创建可用于消息回应的文字表情",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "create_text_emotion"},
},
Selection: contract.SelectionSpec{
AgentSummary: "创建可用于消息回应的文字表情",
UseWhen: []string{"需要定义新的文字表情资源时"},
AvoidWhen: []string{"已有 emotionId 并要回应消息时使用 chat message add-text-emotion"},
Examples: []string{"dws chat message create-text-emotion --emotion-name \"赞\" --text \"nice\" --background-id im_bg_5"},
},
},
})
chatMessageCreateTextEmotionCmd.Flags().String("emotion-name", "", "表情名称 (必填)")
_ = chatMessageCreateTextEmotionCmd.MarkFlagRequired("emotion-name")
chatMessageCreateTextEmotionCmd.Flags().String("text", "", "文字内容 (必填)")
_ = chatMessageCreateTextEmotionCmd.MarkFlagRequired("text")
chatMessageCreateTextEmotionCmd.Flags().String("background-id", "", "背景 ID(可选,不传则由服务端默认分配)")
// ── 流式卡片命令 ──────────────────────────────────────────
chatMessageSendCardCmd := &cobra.Command{
Use: "send-card",
Short: "创建并推送流式卡片",
Long: `向群聊或单聊创建并推送流式卡片。群聊传 --group,单聊传 --receiver,二者互斥。
群聊创建卡片时可通过 --at-open-dingtalk-ids @指定成员,或通过 --at-all @所有人。
创建时无需传入卡片内容,后续通过 update-card 更新内容。
注意:send-card 必须和 update-card 搭配使用。发送卡片后,使用返回的 bizId 调用 update-card 更新内容,
最后一次更新必须将 --flow-status 设为 3(finish),否则卡片会一直处于"生成中"的加载状态。
flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成(FINISH),4=执行中(EXECUTING),5=错误(ERROR)。`,
Example: ` dws chat message send-card --group <openConversationId>
dws chat message send-card --group <openConversationId> --at-open-dingtalk-ids <openDingTalkId>
dws chat message send-card --group <openConversationId> --at-all
dws chat message send-card --receiver <openDingTalkId>
# 查询群 ID: dws chat search --query "群名"
# 查询人员: dws contact user search --keyword "姓名" --format json`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
receiver, _ := cmd.Flags().GetString("receiver")
atOpenDingTalkIDs := uniqueNonEmptyStrings(parseCSVValues(mustGetFlag(cmd, "at-open-dingtalk-ids")))
atAll, _ := cmd.Flags().GetBool("at-all")
if groupID == "" && receiver == "" {
return fmt.Errorf("--group or --receiver is required")
}
if groupID != "" && receiver != "" {
return fmt.Errorf("--group and --receiver are mutually exclusive")
}
if groupID == "" && (len(atOpenDingTalkIDs) > 0 || atAll) {
return fmt.Errorf("--at-open-dingtalk-ids and --at-all are only supported with --group")
}
toolArgs := map[string]any{}
if groupID != "" {
toolArgs["openConversationId"] = groupID
if len(atOpenDingTalkIDs) > 0 {
toolArgs["atOpenDingTalkIds"] = atOpenDingTalkIDs
}
if atAll {
toolArgs["atAll"] = true
}
}
if receiver != "" {
resolved, err := resolveOpenDingTalkID(cmd.Context(), receiver)
if err != nil {
return err
}
toolArgs["receiverOpenDingTalkId"] = resolved
}
return callMCPToolOnServer("im", "create_and_send_card", toolArgs)
},
}
DeclareLeafMetadata(chatMessageSendCardCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "create_and_send_card",
CanonicalPath: "chat.create_and_send_card",
CLIPath: "chat message send-card",
PrimaryCLIPath: "chat message send-card",
},
Description: "创建并向群聊或单聊发送互动卡片;群聊创建时可 @成员或 @所有人",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "create_and_send_card"},
},
Selection: contract.SelectionSpec{
AgentSummary: "创建并向群聊或单聊发送互动卡片;群聊创建时可 @成员或 @所有人",
UseWhen: []string{"需要创建卡片且已准备接收会话或用户时;群聊创建可同时指定 @成员或 @所有人"},
AvoidWhen: []string{"只发送普通文本时使用 send 或 send-by-bot"},
Examples: []string{"dws chat message send-card --group <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "at-all", Property: "atAll", Required: boolPtr(false), InterfaceType: "boolean"},
{Name: "at-open-dingtalk-ids", Property: "atOpenDingTalkIds", Required: boolPtr(false), InterfaceType: "array"},
{Name: "group", Property: "openConversationId"},
{Name: "receiver", Property: "receiverOpenDingTalkId"},
},
},
})
chatMessageSendCardCmd.Flags().String("group", "", "群聊 openConversationId(群聊时必填,与 --receiver 互斥)")
chatMessageSendCardCmd.Flags().String("receiver", "", "单聊接收者 openDingTalkId(单聊时必填,与 --group 互斥)")
chatMessageSendCardCmd.Flags().String("at-open-dingtalk-ids", "", "群聊创建卡片时 @ 的 openDingTalkId 列表,逗号分隔(仅与 --group 一起使用)")
chatMessageSendCardCmd.Flags().Bool("at-all", false, "群聊创建卡片时 @ 所有人(仅与 --group 一起使用)")
chatMessageUpdateCardCmd := &cobra.Command{
Use: "update-card",
Short: "流式更新卡片内容",
Long: `更新已发送的流式卡片内容。--biz-id 为 send-card 返回的业务 ID,--flow-status 控制流式状态。
flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成(FINISH),4=执行中(EXECUTING),5=错误(ERROR)。
最后一次更新必须将 --flow-status 设为 3(finish),否则卡片会一直处于"生成中"的加载状态。`,
Example: ` dws chat message update-card --biz-id <bizId> --content "更新的卡片内容" --flow-status 2
dws chat message update-card --biz-id <bizId> --content "最终内容" --flow-status 3`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "biz-id", "content"); err != nil {
return err
}
if !cmd.Flags().Changed("flow-status") {
return fmt.Errorf("flag --flow-status is required")
}
bizID, err := chatmsg.NormalizeCardBizID(mustGetFlag(cmd, "biz-id"))
if err != nil {
return err
}
flowStatus, _ := cmd.Flags().GetInt("flow-status")
if flowStatus < 1 || flowStatus > 5 {
return fmt.Errorf("--flow-status 必须在 1-5 之间")
}
params := map[string]any{
"bizId": bizID,
"msgContent": mustGetFlag(cmd, "content"),
"flowStatus": flowStatus,
}
if commandDryRun(cmd) {
return writeCommandPayload(cmd, map[string]any{
"dry_run": true,
"executed": false,
"verified": false,
"action": map[string]any{
"product": "im",
"tool": "update_streaming_card",
"arguments": params,
},
})
}
text, err := CallMCPToolTextOnServer("im", "update_streaming_card", params)
if err != nil {
return err
}
var response map[string]any
if strings.TrimSpace(text) == "" {
response = map[string]any{}
} else if err := unmarshalJSONUseNumber(text, &response); err != nil {
return apperrors.NewInternal(
fmt.Sprintf("解析 update_streaming_card 返回失败: %v", err),
apperrors.WithReason("streaming_card_update_response_invalid"),
)
}
if _, err := chatmsg.VerifyStreamingCardUpdate(bizID, response); err != nil {
return nativeCardUpdateVerificationError(bizID, err)
}
return writeCommandPayload(cmd, response)
},
}
DeclareLeafMetadata(chatMessageUpdateCardCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
// The typed command is the atomic MCP surface and intentionally
// preserves its original no-extra-confirmation contract. The
// Agent-facing +messages-update-card shortcut owns the higher-level
// confirmation boundary.
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_streaming_card",
CanonicalPath: "chat.update_streaming_card",
CLIPath: "chat message update-card",
PrimaryCLIPath: "chat message update-card",
},
Description: "更新已发送流式卡片的内容和状态",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_streaming_card"},
},
Selection: contract.SelectionSpec{
AgentSummary: "更新已发送流式卡片的内容和状态",
UseWhen: []string{"需要直接调用底层原子更新,并由调用方自行管理确认与更新节奏时"},
AvoidWhen: []string{"面向 Agent 的默认快速通道使用 chat +messages-update-card;创建新卡片时使用 chat message send-card"},
Examples: []string{"dws chat message update-card --biz-id <bizId> --content \"处理完成\" --flow-status 2"},
},
Parameters: []contract.ParamDecl{
{Name: "biz-id", Property: "bizId"},
{Name: "content", Property: "msgContent"},
{Name: "flow-status", Property: "flowStatus"},
},
},
})
chatMessageUpdateCardCmd.Flags().String("biz-id", "", "卡片业务 ID (必填)")
_ = chatMessageUpdateCardCmd.MarkFlagRequired("biz-id")
chatMessageUpdateCardCmd.Flags().String("content", "", "卡片消息内容 (必填)")
_ = chatMessageUpdateCardCmd.MarkFlagRequired("content")
chatMessageUpdateCardCmd.Flags().Int("flow-status", 0, "流式状态 (必填)")
// ── download-media:下载消息中的媒体资源(走 IM MCP)──────
chatMessageDownloadMediaCmd := &cobra.Command{
Use: "download-media",
Short: "下载消息中的资源(图片/视频/语音等)到本地",
Long: `下载聊天消息中的图片、视频、语音等资源到本地文件。
本命令只支持聊天消息 mediaId 下载,不支持钉盘 fileId。
如需用 fileId 下载,请使用钉盘/drive 下载命令。
流程:
1. 根据 resourceType + resource-id + message-id + open-conversation-id 向服务端获取下载 URL
2. HTTP GET 下载文件到本地
--output 指定本地保存路径,可以是文件路径或目录。
如果指定目录,文件名从下载 URL 中自动推断。默认保存到当前目录。`,
Example: ` dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./download.bin
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./photo.jpg
# resource-id: 从 dws chat message list 返回的消息内容中获取 mediaId
# 不支持钉盘 fileId;fileId 下载请使用钉盘/drive 下载命令
# message-id: 从 dws chat message list 返回的 openMessageId
# open-conversation-id: 从 dws chat search 获取 openConversationId`,
PreRunE: func(cmd *cobra.Command, args []string) error {
// Cobra validates required flags after PreRunE. Copy a supplied alias
// into the canonical flag first so --message-id can remain a hard
// required fact in both the executable and Agent Schema contracts.
if cmd.Flags().Changed("message-id") {
return nil
}
alias := ""
switch {
case cmd.Flags().Changed("msg-id"):
alias = "msg-id"
case cmd.Flags().Changed("open-message-id"):
alias = "open-message-id"
default:
return nil
}
value, _ := cmd.Flags().GetString(alias) // registered string flags above
return cmd.Flags().Set("message-id", value)
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "type", "resource-id", "message-id", "open-conversation-id", "output"); err != nil {
return err
}
resourceType := mustGetFlag(cmd, "type")
resourceID := mustGetFlag(cmd, "resource-id")
conversationID := mustGetFlag(cmd, "open-conversation-id")
messageID := mustGetFlag(cmd, "message-id")
outputPath := mustGetFlag(cmd, "output")
jsonMode := deps.Caller.Format() == "json"
switch resourceType {
case "mediaId":
// current supported type
default:
return fmt.Errorf("unsupported resource type: %s (supported: mediaId)", resourceType)
}
if deps.Caller.DryRun() {
deps.Out.PrintKeyValue("操作", "下载消息资源")
deps.Out.PrintKeyValue("类型", resourceType)
deps.Out.PrintKeyValue("资源ID", resourceID)
deps.Out.PrintKeyValue("消息ID", messageID)
deps.Out.PrintKeyValue("会话ID", conversationID)
deps.Out.PrintKeyValue("输出", outputPath)
return nil
}
ctx := context.Background()
// Step 1: 获取下载 URL
if !jsonMode {
deps.Out.PrintInfo("[1/2] 获取资源下载链接...")
}
text, err := callMCPToolReturnTextOnServer(ctx, "im", "get_resource_download_url", map[string]any{
"resourceType": resourceType,
"resourceId": resourceID,
"openMessageId": messageID,
"openConversationId": conversationID,
})
if err != nil {
return err
}
resourceURL, dlHeaders, err := parseDownloadInfo(text)
if err != nil {
return err
}
// 解析输出路径:目录(已存在的目录,或以分隔符结尾的意图目录)→ 追加推断文件名。
fi, statErr := os.Stat(outputPath)
isDir := (statErr == nil && fi.IsDir()) ||
strings.HasSuffix(outputPath, string(os.PathSeparator)) ||
strings.HasSuffix(outputPath, "/")
if isDir {
filename := inferFilename(resourceURL)
outputPath = filepath.Join(outputPath, filename)
}
// 确保目标父目录存在,否则 httpGetFile 打开文件会因目录缺失失败。
if dir := filepath.Dir(outputPath); dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("创建输出目录失败: %w", err)
}
}
// Step 2: HTTP GET 下载文件
if !jsonMode {
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载资源到 %s ...", outputPath))
}
if err := httpGetFile(ctx, resourceURL, dlHeaders, outputPath); err != nil {
return err
}
if jsonMode {
return deps.Out.PrintJSONUnescaped(map[string]any{
"success": true,
"downloadUrl": resourceURL,
"output": outputPath,
})
}
deps.Out.PrintInfo(fmt.Sprintf("下载完成: %s", outputPath))
return nil
},
}
DeclareLeafMetadata(chatMessageDownloadMediaCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "download_media",
CanonicalPath: "chat.download_media",
CLIPath: "chat message download-media",
PrimaryCLIPath: "chat message download-media",
},
Description: "下载消息中的媒体资源到本地",
DryRun: &contract.DryRunSpec{PreviewKind: "plan", RemoteReads: false},
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "命令包含多个 RPC、条件分派或本地 HTTP/文件步骤,不能绑定为单一 interface_ref",
},
Selection: contract.SelectionSpec{
AgentSummary: "下载消息中的媒体资源到本地",
UseWhen: []string{"已知消息、会话和资源 ID,需要保存媒体文件时"},
AvoidWhen: []string{"只查看文本消息内容时使用对应消息查询命令", "资源是钉盘 fileId 时使用钉盘/drive 下载命令"},
Examples: []string{"dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ."},
},
},
})
// download-media flags
chatMessageDownloadMediaCmd.Flags().String("type", "", "资源类型: mediaId(必填;仅支持聊天消息 mediaId,不支持钉盘 fileId)")
_ = chatMessageDownloadMediaCmd.MarkFlagRequired("type")
chatMessageDownloadMediaCmd.Flags().String("resource-id", "", "资源 ID,mediaId 类型时为消息中的 mediaId 值(必填;不是钉盘 fileId)")
_ = chatMessageDownloadMediaCmd.MarkFlagRequired("resource-id")
chatMessageDownloadMediaCmd.Flags().String("open-conversation-id", "", "会话 openConversationId (必填)")
_ = chatMessageDownloadMediaCmd.MarkFlagRequired("open-conversation-id")
chatMessageDownloadMediaCmd.Flags().String("message-id", "", "消息 openMessageId (必填)")
_ = chatMessageDownloadMediaCmd.MarkFlagRequired("message-id")
// Hidden aliases: agents routinely pass --msg-id / --open-message-id since
// the message-list output exposes the field as openMessageId/msgId. Accept
// them transparently instead of failing with "unknown flag".
chatMessageDownloadMediaCmd.Flags().String("msg-id", "", "--message-id 的别名")
_ = chatMessageDownloadMediaCmd.Flags().MarkHidden("msg-id")
chatMessageDownloadMediaCmd.Flags().String("open-message-id", "", "--message-id 的别名")
_ = chatMessageDownloadMediaCmd.Flags().MarkHidden("open-message-id")
chatMessageDownloadMediaCmd.Flags().String("output", "", "本地保存路径,文件或目录 (必填)")
_ = chatMessageDownloadMediaCmd.MarkFlagRequired("output")
chatMessageCmd.AddCommand(chatMessageListCmd, chatMessageSendCmd, chatMessageSendByBotCmd, chatMessageRecallByBotCmd, chatMessageSendByWebhookCmd, chatMessageListTopicRepliesCmd, chatMessageListAllCmd, chatMessageListBySenderCmd, chatMessageListMentionsCmd, chatMessageListFocusedCmd, chatMessageListUnreadConversationsCmd, chatMessageSearchCmd, chatMessageListByIdsCmd, chatMessageAddEmojiCmd, chatMessageRemoveEmojiCmd, chatMessageAddTextEmotionCmd, chatMessageRemoveTextEmotionCmd, chatMessageUpdateTextEmotionCmd, chatMessageCreateTextEmotionCmd, chatMessageSearchAdvancedCmd, chatMessageQuerySendStatusCmd, chatMessageRecallCmd, chatMessageEditCmd, chatMessageReadStatusCmd, chatMessageSendCardCmd, chatMessageUpdateCardCmd, chatMessageDownloadMediaCmd)
chatBotCmd.AddCommand(chatBotSearchCmd)
chatCategoryCmd.AddCommand(chatCategoryListCmd, chatCategoryConvsCmd, chatCategoryCreateCmd, chatCategoryDeleteCmd, chatCategoryRenameCmd, chatCategoryAddConvCmd, chatCategoryRemoveConvCmd, chatCategoryListByConvCmd, chatCategoryBatchInfoCmd)
chatGroupCmd.AddCommand(chatGroupInfoByIdCmd)
// ── group 新增命令(群主转让、邀请链接、免打扰)──────────
chatGroupTransferOwnerCmd := &cobra.Command{
Use: "transfer-owner",
Short: "转让群主",
Example: ` dws chat group transfer-owner --group <openConversationId> --new-owner <openDingTalkId>
dws chat group transfer-owner --group <openConversationId> --user <userId>
# 查询群 ID: dws chat search --query "群名"
# 查询 openDingTalkId: dws contact user search --query "姓名"
# 查询 userId: dws contact user search --query "姓名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
newOwnerOpenDingTalkID, _ := cmd.Flags().GetString("new-owner")
newOwnerUserID := flagOrFallback(cmd, "user", "userId")
if newOwnerOpenDingTalkID != "" && newOwnerUserID != "" {
return fmt.Errorf("--new-owner and --user are mutually exclusive, specify exactly one")
}
newOwner := newOwnerOpenDingTalkID
if newOwner == "" {
newOwner = newOwnerUserID
}
if newOwner == "" {
return fmt.Errorf("flag --new-owner or --user is required")
}
if !isOpenDingTalkID(newOwner) {
return callMCPToolOnServer("im", "transfer_group_owner", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"newOwnerUid": newOwner,
})
}
return callMCPToolOnServer("im", "transfer_group_owner", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"newOwnerOpenDingTalkId": newOwner,
})
},
}
DeclareLeafMetadata(chatGroupTransferOwnerCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "transfer_group_owner",
CanonicalPath: "chat.transfer_group_owner",
CLIPath: "chat group transfer-owner",
PrimaryCLIPath: "chat group transfer-owner",
},
Description: "把群主身份转让给指定群成员",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "transfer_group_owner"},
},
Selection: contract.SelectionSpec{
AgentSummary: "把群主身份转让给指定群成员",
UseWhen: []string{"现群主明确指定新群主时"},
AvoidWhen: []string{"只是授予管理员权限时使用 chat group set-admin"},
Examples: []string{"dws chat group transfer-owner --group <openConversationId> --new-owner <openDingTalkId>"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
{Name: "new-owner", Property: "newOwnerOpenDingTalkId"},
},
},
})
chatGroupTransferOwnerCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupTransferOwnerCmd.MarkFlagRequired("group")
chatGroupTransferOwnerCmd.Flags().String("new-owner", "", "新群主 openDingTalkId")
chatGroupTransferOwnerCmd.Flags().String("user", "", "新群主 userId")
chatGroupTransferOwnerCmd.Flags().String("userId", "", "--user 的别名")
_ = chatGroupTransferOwnerCmd.Flags().MarkHidden("userId")
chatGroupInviteUrlCmd := &cobra.Command{
Use: "invite-url",
Short: "获取群邀请链接",
Long: `获取群聊邀请链接。可选 --expires-seconds 指定链接有效期(秒),0 表示永久有效,不传则使用服务端默认值。`,
Example: ` dws chat group invite-url --group <openConversationId>
dws chat group invite-url --group <openConversationId> --expires-seconds 86400
dws chat group invite-url --group <openConversationId> --expires-seconds 0
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
}
if v, _ := cmd.Flags().GetInt64("expires-seconds"); v >= 0 && cmd.Flags().Changed("expires-seconds") {
toolArgs["expiresSeconds"] = v
}
return callMCPToolOnServer("im", "get_group_invite_url", toolArgs)
},
}
DeclareLeafMetadata(chatGroupInviteUrlCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "get_group_invite_url",
CanonicalPath: "chat.get_group_invite_url",
CLIPath: "chat group invite-url",
PrimaryCLIPath: "chat group invite-url",
},
Description: "获取指定群聊的邀请链接",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "get_group_invite_url"},
},
Selection: contract.SelectionSpec{
AgentSummary: "获取指定群聊的邀请链接",
UseWhen: []string{"需要生成群邀请链接或设置有效期时"},
AvoidWhen: []string{"需要直接添加已知成员时使用 chat group members add"},
Examples: []string{"dws chat group invite-url --group <openConversationId> --expires-seconds 86400"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupInviteUrlCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupInviteUrlCmd.MarkFlagRequired("group")
chatGroupInviteUrlCmd.Flags().Int64("expires-seconds", 0, "链接有效期(秒),0 表示永久有效,不传使用服务端默认值")
chatMuteCmd := &cobra.Command{
Use: "mute",
Short: "会话消息免打扰",
Long: `开启或关闭会话消息免打扰(支持单聊和群聊)。默认开启免打扰,传 --off 则关闭免打扰。`,
Example: ` dws chat mute --conversation-id <openConversationId>
dws chat mute --conversation-id <openConversationId> --off
# 查询群 ID: dws chat search --query "群名"
# 查询单聊会话 ID: dws chat conversation-info --user <userId>`,
RunE: func(cmd *cobra.Command, args []string) error {
convID := flagOrFallback(cmd, "conversation-id", "id", "chat")
if convID == "" {
return fmt.Errorf("flag --conversation-id is required\n hint: dws chat mute --conversation-id <openConversationId>")
}
off, _ := cmd.Flags().GetBool("off")
return callMCPToolOnServer("im", "update_notification_off", map[string]any{
"openConversationId": convID,
"mute": !off,
})
},
}
DeclareLeafMetadata(chatMuteCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_notification_off",
CanonicalPath: "chat.update_notification_off",
CLIPath: "chat mute",
PrimaryCLIPath: "chat mute",
},
Description: "开启或关闭指定会话的免打扰",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_notification_off"},
},
Selection: contract.SelectionSpec{
AgentSummary: "开启或关闭指定会话的免打扰",
UseWhen: []string{"用户明确要静音或恢复某个会话通知时"},
AvoidWhen: []string{"群成员禁言属于发言权限,应使用 group-mute 命令"},
Examples: []string{"dws chat mute --conversation-id <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "off", Property: "mute", Required: boolPtr(false)},
},
},
})
chatMuteCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
chatMuteCmd.Flags().String("id", "", "--conversation-id 的别名")
chatMuteCmd.Flags().String("chat", "", "--conversation-id 的别名")
chatMuteCmd.Flags().Bool("off", false, "关闭免打扰(不传则开启免打扰)")
// ── group 新增命令(退群、更新群头像、更新群设置)──────────
chatGroupQuitCmd := &cobra.Command{
Use: "quit",
Short: "退出群聊",
Long: `当前用户退出指定群聊。退出后将无法查看群消息。`,
Example: ` dws chat group quit --group <openConversationId>
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
return callMCPToolOnServer("im", "quit_group", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
})
},
}
DeclareLeafMetadata(chatGroupQuitCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "quit_group",
CanonicalPath: "chat.quit_group",
CLIPath: "chat group quit",
PrimaryCLIPath: "chat group quit",
},
Description: "当前用户退出群聊,群本身保留",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "quit_group"},
},
Selection: contract.SelectionSpec{
AgentSummary: "当前用户退出群聊,群本身保留",
UseWhen: []string{"用户只要自己离开某个群,不需要解散群"},
AvoidWhen: []string{
"需要永久解散整个群时使用 chat group dismiss",
"需要踢出其他成员时使用 chat group members remove",
},
Examples: []string{"dws chat group quit --group <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupQuitCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupQuitCmd.MarkFlagRequired("group")
chatGroupUpdateIconCmd := &cobra.Command{
Use: "update-icon",
Short: "更新群头像",
Long: `更新指定群聊的群头像。需传入群 ID 和头像 mediaId。`,
Example: ` dws chat group update-icon --group <openConversationId> --icon-media-id <mediaId>
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "icon-media-id"); err != nil {
return err
}
iconMediaID := strings.TrimSpace(mustGetFlag(cmd, "icon-media-id"))
if iconMediaID == "" {
return fmt.Errorf("invalid --icon-media-id: mediaId 不能为空\n hint: 请使用上游媒体上传能力返回的有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传命令")
}
return callMCPToolOnServer("im", "update_group_icon", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"iconMediaId": iconMediaID,
})
},
}
DeclareLeafMetadata(chatGroupUpdateIconCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_group_icon",
CanonicalPath: "chat.update_group_icon",
CLIPath: "chat group update-icon",
PrimaryCLIPath: "chat group update-icon",
},
Description: "使用真实媒体 ID 更新群头像",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_group_icon"},
},
Selection: contract.SelectionSpec{
AgentSummary: "使用真实媒体 ID 更新群头像",
UseWhen: []string{"已有上传后的头像 mediaId 并要修改群头像时"},
AvoidWhen: []string{"没有真实可用 mediaId 时先完成媒体上传"},
Examples: []string{"dws chat group update-icon --group <openConversationId> --icon-media-id @mediaId"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupUpdateIconCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupUpdateIconCmd.MarkFlagRequired("group")
chatGroupUpdateIconCmd.Flags().String("icon-media-id", "", "群头像 mediaId (必填)")
_ = chatGroupUpdateIconCmd.MarkFlagRequired("icon-media-id")
chatGroupUpdateSettingsCmd := &cobra.Command{
Use: "update-settings",
Short: "更新群设置",
Long: `更新指定群聊的设置项。--setting-key 指定设置项,--status 指定值(0=关闭,1=开启)。
支持的 settingKey:
authority 仅群主和管理员可管理
joinValidation 入群许可
onlyAdminCanAtAll 仅群主和管理员可@所有人
searchable 群可被搜索
addFriendForbidden 禁止群内私聊
toolbarStatus 群快捷栏状态
pluginCustomizeVerify 仅群主和管理员可管理快捷栏
onlyAdminCanDING 谁可以在群内发DING
allMembersCanCreateMcsConf 谁可以在群里发起视频和语音会议
onlyAdminCanSetMsgTop 谁可以把群消息置顶
onlyAdminCanPinMsg 谁可以把群消息钉住
onlyAdminCanSendFile 谁可以上传文件、文件夹和钉盘文件
allMembersCanCreateCalendar 群成员日历可见性
groupEmailDisabled 群邮件组
groupRedEnvelopeSwitch 发红包
groupLiveAuthority 谁可以发起直播
groupBillAuthority 群收款开关`,
Example: ` dws chat group update-settings --group <openConversationId> --setting-key searchable --status 1
dws chat group update-settings --group <openConversationId> --setting-key onlyAdminCanAtAll --status 0
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "setting-key"); err != nil {
return err
}
if !cmd.Flags().Changed("status") {
return fmt.Errorf("flag --status is required (0=关闭, 1=开启)")
}
status, _ := cmd.Flags().GetInt("status")
return callMCPToolOnServer("im", "update_group_settings", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"settingKey": mustGetFlag(cmd, "setting-key"),
"status": status,
})
},
}
DeclareLeafMetadata(chatGroupUpdateSettingsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_group_settings",
CanonicalPath: "chat.update_group_settings",
CLIPath: "chat group update-settings",
PrimaryCLIPath: "chat group update-settings",
},
Description: "更新指定群聊的一项设置开关",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_group_settings"},
},
Selection: contract.SelectionSpec{
AgentSummary: "更新指定群聊的一项设置开关",
UseWhen: []string{"需要调整 searchable、入群验证或群权限等设置时"},
AvoidWhen: []string{"全员禁言和成员禁言使用专门的 mute 命令"},
Examples: []string{"dws chat group update-settings --group <openConversationId> --setting-key searchable --status 1"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupUpdateSettingsCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupUpdateSettingsCmd.MarkFlagRequired("group")
chatGroupUpdateSettingsCmd.Flags().String("setting-key", "", "群设置项 key (必填)")
_ = chatGroupUpdateSettingsCmd.MarkFlagRequired("setting-key")
chatGroupUpdateSettingsCmd.Flags().Int("status", 0, "设置值: 0=关闭, 1=开启 (必填)")
chatGroupCmd.AddCommand(chatGroupTransferOwnerCmd, chatGroupInviteUrlCmd, chatGroupQuitCmd, chatGroupUpdateIconCmd, chatGroupUpdateSettingsCmd)
// ── message reply: 引用回复消息 ──────────────────────────
chatMessageReplyCmd := &cobra.Command{
Use: "reply",
Short: "引用回复消息(支持单聊/群聊)",
Long: `以当前用户身份引用某条消息并回复。需要指定会话 ID、被引用消息 ID、原消息发送者 openDingTalkId,以及回复内容。群聊回复可通过 --at-open-dingtalk-ids @指定成员,或通过 --at-all @所有人;正文中的裸 @openDingTalkId 会自动规范化为 <@openDingTalkId>,缺少对应成员或 <@all> 占位符时会自动补齐。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>
(人员信息可通过 dws contact user search --keyword "姓名" --format json 获取)`,
Example: ` dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text "收到,马上处理"
dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text "请看一下" --at-open-dingtalk-ids <mentionedOpenDingTalkId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "conversation-id", "ref-msg-id", "ref-sender", "text"); err != nil {
return err
}
refSender := mustGetFlag(cmd, "ref-sender")
if !isOpenDingTalkID(refSender) {
resolved, err := resolveOpenDingTalkID(cmd.Context(), refSender)
if err != nil {
return err
}
refSender = resolved
}
clawType := ""
aiTag, _ := cmd.Flags().GetBool("ai-tag")
if aiTag {
clawType = edition.ClawType()
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "conversation-id"),
"msgType": "reply",
"clawType": clawType,
}
atAll, _ := cmd.Flags().GetBool("at-all")
atOpenIDs := mustGetFlag(cmd, "at-open-dingtalk-ids")
replyText := applyCurrentUserGroupMentions(
toolArgs,
mustGetFlag(cmd, "text"),
atOpenIDs,
atAll,
)
replyText = addMissingCurrentUserMentionPlaceholders(replyText, atOpenIDs)
replyContent := map[string]string{
"referenceOpenMessageId": mustGetFlag(cmd, "ref-msg-id"),
"srcMsgSendOpenDingTalkId": refSender,
"replyMsgType": "text",
"content": replyText,
}
contentJSON, _ := marshalJSONRaw(replyContent)
toolArgs["content"] = string(contentJSON)
if v, _ := cmd.Flags().GetString("uuid"); v != "" {
toolArgs["uuid"] = v
}
return callMCPTool("send_personal_message", toolArgs)
},
}
DeclareLeafMetadata(chatMessageReplyCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "reply_personal_message",
CanonicalPath: "chat.reply_personal_message",
CLIPath: "chat message reply",
PrimaryCLIPath: "chat message reply",
},
Description: "引用指定消息发送个人回复",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "send_personal_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "引用指定消息发送个人回复",
UseWhen: []string{"用户要针对某条已有消息进行引用回复时"},
AvoidWhen: []string{"无需引用上下文的普通消息使用 chat message send"},
Examples: []string{"dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text \"收到\""},
},
Parameters: []contract.ParamDecl{
{Name: "ai-tag", Property: "clawType", InterfaceType: "string"},
{Name: "at-all", Property: "atAll", Required: boolPtr(false), InterfaceType: "boolean"},
{Name: "at-open-dingtalk-ids", Property: "atOpenDingTalkIds", Required: boolPtr(false), InterfaceType: "array"},
{Name: "conversation-id", Property: "openConversationId"},
},
},
})
chatMessageReplyCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
_ = chatMessageReplyCmd.MarkFlagRequired("conversation-id")
chatMessageReplyCmd.Flags().String("ref-msg-id", "", "被引用的消息 openMessageId (必填)")
_ = chatMessageReplyCmd.MarkFlagRequired("ref-msg-id")
chatMessageReplyCmd.Flags().String("ref-sender", "", "被引用消息的发送者 openDingTalkId (必填)")
_ = chatMessageReplyCmd.MarkFlagRequired("ref-sender")
chatMessageReplyCmd.Flags().String("text", "", "回复内容 (必填)")
_ = chatMessageReplyCmd.MarkFlagRequired("text")
chatMessageReplyCmd.Flags().String("uuid", "", "幂等键(可选)")
chatMessageReplyCmd.Flags().Bool("ai-tag", true, "消息是否带 AI 发送角标(默认 true)")
chatMessageReplyCmd.Flags().Bool("at-all", false, "@所有人(仅群聊时生效;正文缺少 <@all> 时自动补齐)")
chatMessageReplyCmd.Flags().String("at-open-dingtalk-ids", "", "@指定成员的 openDingTalkId 列表,逗号分隔(仅群聊时生效;正文缺少对应 <@id> 时自动补齐,裸 @id 自动规范化)")
cli.AttachRuntimeSchema(chatMessageReplyCmd, "chat", "reply_personal_message", "hardcoded:chat")
// ── message forward: 转发单条消息 ────────────────────────
chatMessageForwardCmd := &cobra.Command{
Use: "forward",
Short: "转发单条消息(源/目标会话均支持单聊/群聊)",
Long: `将一条消息从源会话转发到目标会话。需要指定源会话 ID、源消息 ID、目标会话 ID。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>
(openDingTalkId 可通过 dws contact user search --query "姓名" 获取)`,
Example: ` dws chat message forward --src-conversation-id <srcOpenConversationId> --msg-id <srcOpenMessageId> --dest-conversation-id <destOpenConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "src-conversation-id", "msg-id", "dest-conversation-id"); err != nil {
return err
}
toolArgs := map[string]any{
"srcOpenCid": mustGetFlag(cmd, "src-conversation-id"),
"srcOpenMessageId": mustGetFlag(cmd, "msg-id"),
"destOpenCid": mustGetFlag(cmd, "dest-conversation-id"),
}
if v, _ := cmd.Flags().GetString("uuid"); v != "" {
toolArgs["uuid"] = v
}
return callMCPToolOnServer("im", "forward_message", toolArgs)
},
}
DeclareLeafMetadata(chatMessageForwardCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "forward_message",
CanonicalPath: "chat.forward_message",
CLIPath: "chat message forward",
PrimaryCLIPath: "chat message forward",
},
Description: "把一条已有消息转发到另一个会话",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "forward_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "把一条已有消息转发到另一个会话",
UseWhen: []string{"已知源消息与源、目标会话 ID 时"},
AvoidWhen: []string{"合并转发多条消息时使用 chat message combine-forward"},
Examples: []string{"dws chat message forward --src-conversation-id <srcConversationId> --msg-id <openMessageId> --dest-conversation-id <destConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "dest-conversation-id", Property: "destOpenCid"},
{Name: "msg-id", Property: "srcOpenMessageId"},
{Name: "src-conversation-id", Property: "srcOpenCid"},
},
},
})
chatMessageForwardCmd.Flags().String("src-conversation-id", "", "源会话 openConversationId (必填,支持单聊/群聊)")
_ = chatMessageForwardCmd.MarkFlagRequired("src-conversation-id")
chatMessageForwardCmd.Flags().String("msg-id", "", "源消息 openMessageId (必填)")
_ = chatMessageForwardCmd.MarkFlagRequired("msg-id")
chatMessageForwardCmd.Flags().String("dest-conversation-id", "", "目标会话 openConversationId (必填,支持单聊/群聊)")
_ = chatMessageForwardCmd.MarkFlagRequired("dest-conversation-id")
chatMessageForwardCmd.Flags().String("uuid", "", "幂等键(可选)")
// ── set-top: 会话置顶 ──────────────────────────────────
chatSetTopCmd := &cobra.Command{
Use: "set-top",
Short: "会话置顶 / 取消置顶(支持单聊/群聊)",
Long: `设置或取消会话置顶。默认设置置顶,传 --off 则取消置顶。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>
(openDingTalkId 可通过 dws contact user search --query "姓名" 获取)`,
Example: ` dws chat set-top --conversation-id <openConversationId>
dws chat set-top --conversation-id <openConversationId> --off`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "conversation-id"); err != nil {
return err
}
off, _ := cmd.Flags().GetBool("off")
return callMCPToolOnServer("im", "set_top_conversation", map[string]any{
"openConversationId": mustGetFlag(cmd, "conversation-id"),
"top": !off,
})
},
}
DeclareLeafMetadata(chatSetTopCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "set_top_conversation",
CanonicalPath: "chat.set_top_conversation",
CLIPath: "chat set-top",
PrimaryCLIPath: "chat set-top",
},
Description: "设置或取消指定会话置顶",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "set_top_conversation"},
},
Selection: contract.SelectionSpec{
AgentSummary: "设置或取消指定会话置顶",
UseWhen: []string{"需要改变某个会话在列表中的置顶状态时"},
AvoidWhen: []string{"只查看置顶清单时使用 chat list-top-conversations"},
Examples: []string{"dws chat set-top --conversation-id <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "off", Property: "top", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId"},
},
},
})
chatSetTopCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
_ = chatSetTopCmd.MarkFlagRequired("conversation-id")
chatSetTopCmd.Flags().Bool("off", false, "取消置顶(不传则设置置顶)")
// ── group get-mute-config: 查询群用户禁言配置 ───────────────
chatGroupGetMuteConfigCmd := &cobra.Command{
Use: "get-mute-config",
Short: "查询群用户禁言配置",
Long: `查询指定群的用户禁言配置,包括单独禁言黑名单、全员禁言白名单及相关操作时间。
返回的是原始配置记录,不等同于当前被禁言成员列表;全员禁言开关也不在本命令的返回范围内。`,
Example: ` dws chat group get-mute-config --group <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
return callMCPToolOnServer("im", "get_group_mute_config", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
})
},
}
DeclareLeafMetadata(chatGroupGetMuteConfigCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "get_group_mute_config",
CanonicalPath: "chat.get_group_mute_config",
CLIPath: "chat group get-mute-config",
PrimaryCLIPath: "chat group get-mute-config",
},
Description: "查询群用户禁言配置(禁言黑名单/全员禁言白名单)",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
},
Selection: contract.SelectionSpec{
AgentSummary: "查询群用户禁言配置(禁言黑名单/全员禁言白名单)",
UseWhen: []string{"用户说 看下群里谁被禁言/禁言配置"},
AvoidWhen: []string{"设置全员禁言用 chat group-mute;禁言个人用 chat group-mute-member"},
Examples: []string{"dws chat group get-mute-config --group <openConversationId> --format json"},
},
},
})
chatGroupGetMuteConfigCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
chatGroupCmd.AddCommand(chatGroupGetMuteConfigCmd)
// ── group-mute: 全员禁言 ───────────────────────────────
chatGroupMuteCmd := &cobra.Command{
Use: "group-mute",
Short: "全员禁言 / 取消全员禁言",
Long: `设置或取消群全员禁言。默认开启全员禁言,传 --off 则取消。`,
Example: ` dws chat group-mute --group <openConversationId>
dws chat group-mute --group <openConversationId> --off
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
if groupID == "" {
return fmt.Errorf("flag --group is required\n hint: dws chat group-mute --group <openConversationId>")
}
off, _ := cmd.Flags().GetBool("off")
return callMCPToolOnServer("im", "set_group_mute", map[string]any{
"openConversationId": groupID,
"mute": !off,
})
},
}
DeclareLeafMetadata(chatGroupMuteCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "set_group_mute",
CanonicalPath: "chat.set_group_mute",
CLIPath: "chat group-mute",
PrimaryCLIPath: "chat group-mute",
},
Description: "开启或关闭群聊全员禁言",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "set_group_mute"},
},
Selection: contract.SelectionSpec{
AgentSummary: "开启或关闭群聊全员禁言",
UseWhen: []string{"需要控制整个群的发言权限时"},
AvoidWhen: []string{"只禁言指定成员时使用 chat group-mute-member"},
Examples: []string{"dws chat group-mute --group <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "off", Property: "mute", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupMuteCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
chatGroupMuteCmd.Flags().String("conversation-id", "", "--group 的别名")
_ = chatGroupMuteCmd.Flags().MarkHidden("conversation-id")
chatGroupMuteCmd.Flags().String("id", "", "--group 的别名")
_ = chatGroupMuteCmd.Flags().MarkHidden("id")
chatGroupMuteCmd.Flags().String("chat", "", "--group 的别名")
_ = chatGroupMuteCmd.Flags().MarkHidden("chat")
chatGroupMuteCmd.Flags().Bool("off", false, "取消全员禁言(不传则开启禁言)")
// ── group-mute-member: 指定群成员禁言 ──────────────────
chatGroupMuteMemberCmd := &cobra.Command{
Use: "group-mute-member",
Short: "指定群成员禁言 / 取消禁言",
Long: `将指定群成员加入或移出禁言名单。
--mute-time 禁言时长(毫秒),仅支持 5min(300000) / 1h(3600000) / 1d(86400000) / 7d(604800000) / 30d(2592000000)。
默认加入禁言名单,传 --off 则移除。`,
Example: ` dws chat group-mute-member --group <openConversationId> --users userId1,userId2 --mute-time 3600000
dws chat group-mute-member --group <openConversationId> --user userId1 --mute-time 3600000
dws chat group-mute-member --group <openConversationId> --user userId1,userId2 --off
# 查询群 ID: dws chat search --query "群名"
# 查询人员: dws contact user search --keyword "姓名" --format json`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
if groupID == "" {
return fmt.Errorf("flag --group is required\n hint: dws chat group-mute-member --group <openConversationId> --user <userIds> --mute-time <ms>")
}
usersRaw := flagOrFallback(cmd, "users", "user", "userId")
if usersRaw == "" {
return fmt.Errorf("flag --users or --user is required")
}
userIDs, openDingTalkIDs := splitChatIDValues(parseCSVValues(usersRaw))
// 服务端 set_group_member_mute_list 的 uids(staffId)入参存在缺陷:
// 即使传了 uids 仍返回 "uids is required",而 openDingTalkIds 路径正常。
// 与 message send 一致:先把 userId 解析为 openDingTalkId;解析失败再降级透传 uids。
// Resolving userId to openDingTalkId is a remote preflight. A dry-run
// must preserve the supplied uids in its preview without calling MCP.
if len(userIDs) > 0 && !deps.Caller.DryRun() {
if resolved, err := resolveOpenDingTalkIDs(cmd.Context(), userIDs); err == nil {
openDingTalkIDs = append(openDingTalkIDs, resolved...)
userIDs = nil
}
}
off, _ := cmd.Flags().GetBool("off")
toolArgs := map[string]any{
"openConversationId": groupID,
"mute": !off,
}
if len(userIDs) > 0 {
toolArgs["uids"] = userIDs
}
if len(openDingTalkIDs) > 0 {
toolArgs["openDingTalkIds"] = openDingTalkIDs
}
if !off {
muteTime, _ := cmd.Flags().GetInt64("mute-time")
if muteTime <= 0 {
return fmt.Errorf("--mute-time is required when muting (supported: 300000/3600000/86400000/604800000/2592000000)")
}
toolArgs["muteTime"] = muteTime
}
return callMCPToolOnServer("im", "set_group_member_mute_list", toolArgs)
},
}
DeclareLeafMetadata(chatGroupMuteMemberCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "set_group_member_mute_list",
CanonicalPath: "chat.set_group_member_mute_list",
CLIPath: "chat group-mute-member",
PrimaryCLIPath: "chat group-mute-member",
},
Description: "禁言或解除禁言指定群成员",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "set_group_member_mute_list"},
},
Selection: contract.SelectionSpec{
AgentSummary: "禁言或解除禁言指定群成员",
UseWhen: []string{"需要按成员设置禁言时长或解除禁言时"},
AvoidWhen: []string{"需要全员禁言时使用 chat group-mute"},
Examples: []string{"dws chat group-mute-member --group <openConversationId> --users userId1,userId2 --mute-time 3600000"},
},
Parameters: []contract.ParamDecl{
{Name: "off", Property: "mute", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId"},
{Name: "users", Property: "openDingTalkIds"},
},
},
})
chatGroupMuteMemberCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
chatGroupMuteMemberCmd.Flags().String("conversation-id", "", "--group 的别名")
_ = chatGroupMuteMemberCmd.Flags().MarkHidden("conversation-id")
chatGroupMuteMemberCmd.Flags().String("id", "", "--group 的别名")
_ = chatGroupMuteMemberCmd.Flags().MarkHidden("id")
chatGroupMuteMemberCmd.Flags().String("chat", "", "--group 的别名")
_ = chatGroupMuteMemberCmd.Flags().MarkHidden("chat")
chatGroupMuteMemberCmd.Flags().String("users", "", "群成员 userId 列表,逗号分隔(批量)")
chatGroupMuteMemberCmd.Flags().String("user", "", "群成员 userId,支持逗号分隔")
chatGroupMuteMemberCmd.Flags().String("userId", "", "--user 的别名")
_ = chatGroupMuteMemberCmd.Flags().MarkHidden("userId")
chatGroupMuteMemberCmd.Flags().Int64("mute-time", 0, "禁言时长(毫秒),支持 300000/3600000/86400000/604800000/2592000000")
chatGroupMuteMemberCmd.Flags().Bool("off", false, "移出禁言名单(不传则加入禁言名单)")
// ── group set-admin: 设置群成员为管理员 ─────────────────
chatGroupSetAdminCmd := &cobra.Command{
Use: "set-admin",
Short: "设置 / 取消群管理员",
Long: `将指定群成员设置为管理员或取消管理员身份。默认设为管理员,传 --off 则取消。`,
Example: ` dws chat group set-admin --group <openConversationId> --users userId1,userId2
dws chat group set-admin --group <openConversationId> --user userId1
dws chat group set-admin --group <openConversationId> --user userId1 --off
# 查询群 ID: dws chat search --query "群名"
# 查询人员: dws contact user search --keyword "姓名" --format json`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
usersRaw := flagOrFallback(cmd, "users", "user", "userId")
if usersRaw == "" {
return fmt.Errorf("flag --users or --user is required")
}
userIDs, openDingTalkIDs := splitChatIDValues(parseCSVValues(usersRaw))
off, _ := cmd.Flags().GetBool("off")
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"admin": !off,
}
if len(userIDs) > 0 {
toolArgs["uids"] = userIDs
}
if len(openDingTalkIDs) > 0 {
toolArgs["openDingTalkIds"] = openDingTalkIDs
}
return callMCPToolOnServer("im", "update_conv_member_roles", toolArgs)
},
}
DeclareLeafMetadata(chatGroupSetAdminCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_conv_member_roles",
CanonicalPath: "chat.update_conv_member_roles",
CLIPath: "chat group set-admin",
PrimaryCLIPath: "chat group set-admin",
},
Description: "设置或取消群管理员角色",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_conv_member_roles"},
},
Selection: contract.SelectionSpec{
AgentSummary: "设置或取消群管理员角色",
UseWhen: []string{"需要变更指定成员的群管理员身份时"},
AvoidWhen: []string{"自定义业务角色应使用 chat group-role 系列命令"},
Examples: []string{"dws chat group set-admin --group <openConversationId> --users userId1,userId2"},
},
Parameters: []contract.ParamDecl{
{Name: "off", Property: "admin", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId"},
{Name: "users", Property: "openDingTalkIds"},
},
},
})
chatGroupSetAdminCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupSetAdminCmd.MarkFlagRequired("group")
chatGroupSetAdminCmd.Flags().String("users", "", "成员 userId 列表,逗号分隔(批量)")
chatGroupSetAdminCmd.Flags().String("user", "", "成员 userId,支持逗号分隔")
chatGroupSetAdminCmd.Flags().String("userId", "", "--user 的别名")
_ = chatGroupSetAdminCmd.Flags().MarkHidden("userId")
chatGroupSetAdminCmd.Flags().Bool("off", false, "取消管理员(不传则设为管理员)")
chatGroupCmd.AddCommand(chatGroupSetAdminCmd)
chatMessageCmd.AddCommand(chatMessageReplyCmd, chatMessageForwardCmd)
// info-by-id flags
chatGroupInfoByIdCmd.Flags().Int64("group-id", 0, "群号 (必填,数字类型)")
_ = chatGroupInfoByIdCmd.MarkFlagRequired("group-id")
// category conversations flags
chatCategoryConvsCmd.Flags().Int("category-id", 0, "会话分组 ID (必填)")
chatCategoryConvsCmd.Flags().Bool("exclude-muted", false, "是否排除已设置免打扰的会话(默认 false)")
_ = chatCategoryConvsCmd.MarkFlagRequired("category-id")
// category create flags
chatCategoryCreateCmd.Flags().String("title", "", "分组名称,最多 15 个字符 (必填)")
_ = chatCategoryCreateCmd.MarkFlagRequired("title")
// category delete flags
chatCategoryDeleteCmd.Flags().Int64("category-id", 0, "会话分组 ID (必填)")
// category rename flags
chatCategoryRenameCmd.Flags().Int64("category-id", 0, "会话分组 ID (必填)")
chatCategoryRenameCmd.Flags().String("title", "", "新的分组名称,最多 15 个字符 (必填)")
_ = chatCategoryRenameCmd.MarkFlagRequired("title")
// category add-conv flags
chatCategoryAddConvCmd.Flags().String("group", "", "会话 openConversationId (必填)")
chatCategoryAddConvCmd.Flags().String("conversation-id", "", "--group 的别名")
_ = chatCategoryAddConvCmd.Flags().MarkHidden("conversation-id")
chatCategoryAddConvCmd.Flags().String("id", "", "--group 的别名")
_ = chatCategoryAddConvCmd.Flags().MarkHidden("id")
chatCategoryAddConvCmd.Flags().String("category-ids", "", "目标分组 ID 列表,逗号分隔 (必填)")
_ = chatCategoryAddConvCmd.MarkFlagRequired("category-ids")
// category remove-conv flags
chatCategoryRemoveConvCmd.Flags().String("group", "", "会话 openConversationId (必填)")
chatCategoryRemoveConvCmd.Flags().String("conversation-id", "", "--group 的别名")
_ = chatCategoryRemoveConvCmd.Flags().MarkHidden("conversation-id")
chatCategoryRemoveConvCmd.Flags().String("id", "", "--group 的别名")
_ = chatCategoryRemoveConvCmd.Flags().MarkHidden("id")
chatCategoryRemoveConvCmd.Flags().String("category-ids", "", "目标分组 ID 列表,逗号分隔 (必填)")
_ = chatCategoryRemoveConvCmd.MarkFlagRequired("category-ids")
// category list-by-conv flags
chatCategoryListByConvCmd.Flags().String("group", "", "会话 openConversationId (必填)")
chatCategoryListByConvCmd.Flags().String("conversation-id", "", "--group 的别名")
_ = chatCategoryListByConvCmd.Flags().MarkHidden("conversation-id")
chatCategoryListByConvCmd.Flags().String("id", "", "--group 的别名")
_ = chatCategoryListByConvCmd.Flags().MarkHidden("id")
cli.AnnotateRuntimeRequiredFlags(chatCategoryListByConvCmd, "group")
// category batch-info flags
chatCategoryBatchInfoCmd.Flags().String("category-ids", "", "分组 ID 列表,逗号分隔 (必填)")
_ = chatCategoryBatchInfoCmd.MarkFlagRequired("category-ids")
// ── group-role 子命令(群身份管理)────────────────────────
chatGroupRoleCmd := &cobra.Command{Use: "group-role", Short: "群身份管理", RunE: groupRunE}
chatGroupRoleListCmd := &cobra.Command{
Use: "list",
Short: "拉取会话的群身份列表",
Example: ` dws chat group-role list --group <openConversationId>
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
groupID := flagOrFallback(cmd, "group", "conversation-id", "id")
if groupID == "" {
return fmt.Errorf("flag --group is required\n hint: dws chat group-role list --group <openConversationId>")
}
return callMCPToolOnServer("im", "list_custom_group_roles", map[string]any{
"openConversationId": groupID,
})
},
}
DeclareLeafMetadata(chatGroupRoleListCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_custom_group_roles",
CanonicalPath: "chat.list_custom_group_roles",
CLIPath: "chat group-role list",
PrimaryCLIPath: "chat group-role list",
},
Description: "列出群聊中的自定义角色",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_custom_group_roles"},
},
Selection: contract.SelectionSpec{
AgentSummary: "列出群聊中的自定义角色",
UseWhen: []string{"需要取得角色 ID 或查看角色定义时"},
AvoidWhen: []string{"查询某个成员已分配角色时使用 chat group-role query-user"},
Examples: []string{"dws chat group-role list --group <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupRoleListCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupRoleListCmd.MarkFlagRequired("group")
chatGroupRoleAddCmd := &cobra.Command{
Use: "add",
Short: "添加群身份",
Example: ` dws chat group-role add --group <openConversationId> --name "管理员"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "name"); err != nil {
return err
}
return callMCPToolOnServer("im", "add_custom_group_role", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"name": mustGetFlag(cmd, "name"),
})
},
}
DeclareLeafMetadata(chatGroupRoleAddCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "add_custom_group_role",
CanonicalPath: "chat.add_custom_group_role",
CLIPath: "chat group-role add",
PrimaryCLIPath: "chat group-role add",
},
Description: "在群聊中创建自定义角色",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "add_custom_group_role"},
},
Selection: contract.SelectionSpec{
AgentSummary: "在群聊中创建自定义角色",
UseWhen: []string{"需要新增可分配给群成员的业务角色时"},
AvoidWhen: []string{"设置系统管理员角色时使用 chat group set-admin"},
Examples: []string{"dws chat group-role add --group <openConversationId> --name \"值班负责人\""},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupRoleAddCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupRoleAddCmd.MarkFlagRequired("group")
chatGroupRoleAddCmd.Flags().String("name", "", "群身份名称 (必填)")
_ = chatGroupRoleAddCmd.MarkFlagRequired("name")
chatGroupRoleUpdateCmd := &cobra.Command{
Use: "update",
Short: "更新群身份名称",
Example: ` dws chat group-role update --group <openConversationId> --role-id <openRoleId> --name "新名称"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "role-id", "name"); err != nil {
return err
}
return callMCPToolOnServer("im", "update_custom_group_role", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"openRoleId": mustGetFlag(cmd, "role-id"),
"name": mustGetFlag(cmd, "name"),
})
},
}
DeclareLeafMetadata(chatGroupRoleUpdateCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_custom_group_role",
CanonicalPath: "chat.update_custom_group_role",
CLIPath: "chat group-role update",
PrimaryCLIPath: "chat group-role update",
},
Description: "更新群聊自定义角色的名称",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_custom_group_role"},
},
Selection: contract.SelectionSpec{
AgentSummary: "更新群聊自定义角色的名称",
UseWhen: []string{"已知角色 ID 并需要重命名该角色时"},
AvoidWhen: []string{"需要变更成员角色分配时使用 set-user 或 remove-user"},
Examples: []string{"dws chat group-role update --group <openConversationId> --role-id <openRoleId> --name \"新名称\""},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
{Name: "role-id", Property: "openRoleId"},
},
},
})
chatGroupRoleUpdateCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupRoleUpdateCmd.MarkFlagRequired("group")
chatGroupRoleUpdateCmd.Flags().String("role-id", "", "群身份 openRoleId,由 group-role list 返回 (必填)")
_ = chatGroupRoleUpdateCmd.MarkFlagRequired("role-id")
chatGroupRoleUpdateCmd.Flags().String("name", "", "群身份新名称 (必填)")
_ = chatGroupRoleUpdateCmd.MarkFlagRequired("name")
chatGroupRoleRemoveCmd := &cobra.Command{
Use: "remove",
Short: "删除群身份",
Example: ` dws chat group-role remove --group <openConversationId> --role-id <openRoleId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "role-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "remove_custom_group_role", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"openRoleId": mustGetFlag(cmd, "role-id"),
})
},
}
DeclareLeafMetadata(chatGroupRoleRemoveCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "remove_custom_group_role",
CanonicalPath: "chat.remove_custom_group_role",
CLIPath: "chat group-role remove",
PrimaryCLIPath: "chat group-role remove",
},
Description: "删除群聊中的自定义角色",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "remove_custom_group_role"},
},
Selection: contract.SelectionSpec{
AgentSummary: "删除群聊中的自定义角色",
UseWhen: []string{"明确要移除整个自定义角色定义时"},
AvoidWhen: []string{"只取消某个成员的角色时使用 chat group-role remove-user"},
Examples: []string{"dws chat group-role remove --group <openConversationId> --role-id <openRoleId>"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
{Name: "role-id", Property: "openRoleId"},
},
},
})
chatGroupRoleRemoveCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupRoleRemoveCmd.MarkFlagRequired("group")
chatGroupRoleRemoveCmd.Flags().String("role-id", "", "群身份 openRoleId,由 group-role list 返回 (必填)")
_ = chatGroupRoleRemoveCmd.MarkFlagRequired("role-id")
chatGroupRoleSetUserCmd := &cobra.Command{
Use: "set-user",
Short: "设置用户的群身份(覆盖该用户的全部群身份)",
Example: ` dws chat group-role set-user --group <openConversationId> --user <userId> --role-ids roleId1,roleId2
# 查询人员: dws contact user search --keyword "姓名" --format json
# 查询 role-id: dws chat group-role list --group <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "role-ids"); err != nil {
return err
}
if err := validateRequiredFlagWithAliases(cmd, "user", "userId"); err != nil {
return err
}
roleIDs := parseCSVValues(mustGetFlag(cmd, "role-ids"))
user := flagOrFallback(cmd, "user", "userId")
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"openRoleIds": roleIDs,
}
if isOpenDingTalkID(user) {
toolArgs["openDingTalkId"] = user
} else {
toolArgs["userId"] = user
}
return callMCPToolOnServer("im", "set_custom_user_roles", toolArgs)
},
}
DeclareLeafMetadata(chatGroupRoleSetUserCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "set_custom_user_roles",
CanonicalPath: "chat.set_custom_user_roles",
CLIPath: "chat group-role set-user",
PrimaryCLIPath: "chat group-role set-user",
},
Description: "为指定群成员设置自定义角色",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "set_custom_user_roles"},
},
Selection: contract.SelectionSpec{
AgentSummary: "为指定群成员设置自定义角色",
UseWhen: []string{"需要把一个或多个已有角色分配给成员时"},
AvoidWhen: []string{"创建新角色定义时使用 chat group-role add"},
Examples: []string{"dws chat group-role set-user --group <openConversationId> --user <userId> --role-ids roleId1,roleId2"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
{Name: "role-ids", Property: "openRoleIds"},
{Name: "user", Property: "openDingTalkId"},
},
},
})
chatGroupRoleSetUserCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupRoleSetUserCmd.MarkFlagRequired("group")
chatGroupRoleSetUserCmd.Flags().String("user", "", "用户 userId(必填)")
chatGroupRoleSetUserCmd.Flags().String("userId", "", "--user 的别名")
_ = chatGroupRoleSetUserCmd.Flags().MarkHidden("userId")
chatGroupRoleSetUserCmd.Flags().String("role-ids", "", "群身份 openRoleId 列表,逗号分隔 (必填),传空字符串则清除该用户所有群身份")
_ = chatGroupRoleSetUserCmd.MarkFlagRequired("role-ids")
chatGroupRoleRemoveUserCmd := &cobra.Command{
Use: "remove-user",
Short: "移除用户的指定群身份",
Example: ` dws chat group-role remove-user --group <openConversationId> --user <userId> --role-ids roleId1,roleId2`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "role-ids"); err != nil {
return err
}
if err := validateRequiredFlagWithAliases(cmd, "user", "userId"); err != nil {
return err
}
roleIDs := parseCSVValues(mustGetFlag(cmd, "role-ids"))
user := flagOrFallback(cmd, "user", "userId")
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"openRoleIds": roleIDs,
}
if isOpenDingTalkID(user) {
toolArgs["openDingTalkId"] = user
} else {
toolArgs["userId"] = user
}
return callMCPToolOnServer("im", "remove_custom_user_roles", toolArgs)
},
}
DeclareLeafMetadata(chatGroupRoleRemoveUserCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "remove_custom_user_roles",
CanonicalPath: "chat.remove_custom_user_roles",
CLIPath: "chat group-role remove-user",
PrimaryCLIPath: "chat group-role remove-user",
},
Description: "取消指定成员的一个或多个自定义角色",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "remove_custom_user_roles"},
},
Selection: contract.SelectionSpec{
AgentSummary: "取消指定成员的一个或多个自定义角色",
UseWhen: []string{"需要保留角色定义但解除成员角色时"},
AvoidWhen: []string{"删除角色定义本身时使用 chat group-role remove"},
Examples: []string{"dws chat group-role remove-user --group <openConversationId> --user <userId> --role-ids roleId1,roleId2"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
{Name: "role-ids", Property: "openRoleIds"},
{Name: "user", Property: "openDingTalkId"},
},
},
})
chatGroupRoleRemoveUserCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupRoleRemoveUserCmd.MarkFlagRequired("group")
chatGroupRoleRemoveUserCmd.Flags().String("user", "", "用户 userId(必填)")
chatGroupRoleRemoveUserCmd.Flags().String("userId", "", "--user 的别名")
_ = chatGroupRoleRemoveUserCmd.Flags().MarkHidden("userId")
chatGroupRoleRemoveUserCmd.Flags().String("role-ids", "", "要移除的群身份 openRoleId 列表,逗号分隔 (必填)")
_ = chatGroupRoleRemoveUserCmd.MarkFlagRequired("role-ids")
chatGroupRoleQueryUserCmd := &cobra.Command{
Use: "query-user",
Short: "查询群成员的群身份",
Example: ` dws chat group-role query-user --group <openConversationId> --user <userId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
if err := validateRequiredFlagWithAliases(cmd, "user", "userId"); err != nil {
return err
}
user := flagOrFallback(cmd, "user", "userId")
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
}
if isOpenDingTalkID(user) {
toolArgs["openDingTalkId"] = user
} else {
toolArgs["userId"] = user
}
return callMCPToolOnServer("im", "query_custom_user_roles", toolArgs)
},
}
DeclareLeafMetadata(chatGroupRoleQueryUserCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "query_custom_user_roles",
CanonicalPath: "chat.query_custom_user_roles",
CLIPath: "chat group-role query-user",
PrimaryCLIPath: "chat group-role query-user",
},
Description: "查询指定群成员的自定义角色",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "query_custom_user_roles"},
},
Selection: contract.SelectionSpec{
AgentSummary: "查询指定群成员的自定义角色",
UseWhen: []string{"需要核对某个成员在群内的业务角色时"},
AvoidWhen: []string{"列出全部角色定义时使用 chat group-role list"},
Examples: []string{"dws chat group-role query-user --group <openConversationId> --user <userId>"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
{Name: "user", Property: "openDingTalkId"},
},
},
})
chatGroupRoleQueryUserCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupRoleQueryUserCmd.MarkFlagRequired("group")
chatGroupRoleQueryUserCmd.Flags().String("user", "", "用户 userId(必填)")
chatGroupRoleQueryUserCmd.Flags().String("userId", "", "--user 的别名")
_ = chatGroupRoleQueryUserCmd.Flags().MarkHidden("userId")
chatGroupRoleCmd.AddCommand(
chatGroupRoleListCmd,
chatGroupRoleAddCmd,
chatGroupRoleUpdateCmd,
chatGroupRoleRemoveCmd,
chatGroupRoleSetUserCmd,
chatGroupRoleRemoveUserCmd,
chatGroupRoleQueryUserCmd,
)
// ── 群机器人 / 群解散 / 历史消息 / 合并转发(5.18 + 5.14 表)──
chatGroupBotsCmd := &cobra.Command{
Use: "bots",
Short: "查看群内所有机器人",
Long: `获取指定群聊中的所有机器人列表。`,
Example: ` dws chat group bots --group <openConversationId>
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
groupID, err := resolveNativeChatTarget(mustGetFlag(cmd, "group"))
if err != nil {
return err
}
return callMCPToolOnServer("bot", "list_group_bots", map[string]any{
"openConversationId": groupID,
})
},
}
DeclareLeafMetadata(chatGroupBotsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_group_bots",
CanonicalPath: "chat.list_group_bots",
CLIPath: "chat group bots",
PrimaryCLIPath: "chat group bots",
},
Description: "列出群内机器人",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "bot", RPCName: "list_group_bots"},
},
Selection: contract.SelectionSpec{
AgentSummary: "列出群内机器人",
UseWhen: []string{"需要查看某群已安装哪些机器人或提取 openBotId"},
AvoidWhen: []string{"搜索企业内机器人目录时使用 chat bot find"},
Examples: []string{"dws chat group bots --group <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupBotsCmd.Flags().String("group", "", "群聊 openConversationId 或需唯一解析的群名 (必填)")
_ = chatGroupBotsCmd.MarkFlagRequired("group")
chatGroupMembersRemoveBotCmd := &cobra.Command{
Use: "remove-bot",
Short: "从群内移除机器人",
Long: `将指定机器人从群聊中移除。需要群管理员或群主权限。`,
Example: ` dws chat group members remove-bot --id <openConversationId> --bot-id <openBotId>
# 查询群 ID: dws chat search --query "群名"
# 查询群内机器人: dws chat group bots --group <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id", "bot-id"); err != nil {
return err
}
return callMCPToolOnServer("bot", "remove_robot_in_group", map[string]any{
"openConversationId": mustGetFlag(cmd, "id"),
"openBotId": mustGetFlag(cmd, "bot-id"),
})
},
}
DeclareLeafMetadata(chatGroupMembersRemoveBotCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "remove_robot_in_group",
CanonicalPath: "chat.remove_robot_in_group",
CLIPath: "chat group members remove-bot",
PrimaryCLIPath: "chat group members remove-bot",
},
Description: "从群内移除指定机器人",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "bot", RPCName: "remove_robot_in_group"},
},
Selection: contract.SelectionSpec{
AgentSummary: "从群内移除指定机器人",
UseWhen: []string{"需要把某个机器人踢出指定群"},
AvoidWhen: []string{
"移除普通成员时使用 chat group members remove",
"解散整个群时使用 chat group dismiss",
},
Examples: []string{"dws chat group members remove-bot --id <openConversationId> --bot-id <openBotId>"},
},
Parameters: []contract.ParamDecl{
{Name: "bot-id", Property: "openBotId"},
{Name: "id", Property: "openConversationId"},
},
},
})
chatGroupMembersRemoveBotCmd.Flags().String("id", "", "群聊 openConversationId (必填)")
_ = chatGroupMembersRemoveBotCmd.MarkFlagRequired("id")
chatGroupMembersRemoveBotCmd.Flags().String("bot-id", "", "机器人 openBotId (必填)")
_ = chatGroupMembersRemoveBotCmd.MarkFlagRequired("bot-id")
chatBotFindCmd := &cobra.Command{
Use: "find",
Short: "搜索【全部可用】机器人(含他人/官方,额外返回 openDingTalkId 可发单聊)",
Long: `按关键词搜索当前用户可用的【全部】机器人(含他人创建、官方机器人),支持游标分页。
如何在 find 与 search 之间选择:
- find :用户说"搜索机器人""找一个机器人""所有可用机器人""帮我找 XXX 机器人"
(不限范围,包含他人/官方)→ 用 find
- search:用户说"我创建的""我的""我自己的""我做的"机器人 → 用 search(dws chat bot search)
返回字段差异(核心区分点):
- find 额外返回 openDingTalkId(可用于给机器人发单聊消息)
- search 没有 openDingTalkId
如果后续需要给机器人发单聊消息,必须用 find 拿 openDingTalkId。`,
Example: ` dws chat bot find --query "日报"
dws chat bot find --query "日报" --limit 20
# 拿到 openDingTalkId 后可用于给机器人发单聊消息`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "query", "keyword"); err != nil {
return err
}
toolArgs := map[string]any{
"keyword": flagOrFallback(cmd, "query", "keyword"),
}
if v, err := cmd.Flags().GetInt("limit"); err == nil && v > 0 {
toolArgs["limit"] = v
}
if v, _ := cmd.Flags().GetString("cursor"); v != "" {
toolArgs["cursor"] = v
}
return callMCPToolOnServer("bot", "search_bots", toolArgs)
},
}
DeclareLeafMetadata(chatBotFindCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "search_bots",
CanonicalPath: "chat.search_bots",
CLIPath: "chat bot find",
PrimaryCLIPath: "chat bot find",
},
Description: "按关键词搜索企业机器人并拿到 openDingTalkId",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "bot", RPCName: "search_bots"},
},
Selection: contract.SelectionSpec{
AgentSummary: "按关键词搜索企业机器人并拿到 openDingTalkId",
UseWhen: []string{"要找可用机器人并提取 openDingTalkId(例如后续单聊机器人)"},
AvoidWhen: []string{
"只查我创建的机器人时使用 chat bot search",
"已有 robot-code 直接发消息时不要先搜",
},
Examples: []string{"dws chat bot find --query \"日报\""},
},
Parameters: []contract.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
})
chatBotFindCmd.Flags().String("query", "", "搜索关键词 (必填)")
chatBotFindCmd.Flags().String("keyword", "", "--query 的别名")
_ = chatBotFindCmd.Flags().MarkHidden("keyword")
chatBotFindCmd.Flags().Int("limit", 20, "每页返回数量(默认 20)")
chatBotFindCmd.Flags().String("cursor", "", "分页游标(首次调用不传,翻页时传上次返回的 nextCursor)")
chatGroupDismissCmd := &cobra.Command{
Use: "dismiss",
Short: "解散群聊",
Long: `解散指定群聊。该操作不可逆,需要群主权限;必须先获得用户确认,再追加 --yes 执行。`,
Example: ` dws chat group dismiss --group <openConversationId>
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
return callMCPToolOnServer("im", "dismiss_group", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
})
},
}
DeclareLeafMetadata(chatGroupDismissCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "destructive", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "dismiss_group",
CanonicalPath: "chat.dismiss_group",
CLIPath: "chat group dismiss",
PrimaryCLIPath: "chat group dismiss",
},
Description: "永久解散指定群聊(不可恢复,仅群主)",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "dismiss_group"},
},
Selection: contract.SelectionSpec{
AgentSummary: "永久解散指定群聊(不可恢复,仅群主)",
UseWhen: []string{
"群主明确要求永久解散整个群,而不是自己退出",
"已确认 openConversationId,且用户接受群与历史消息一并消失",
},
AvoidWhen: []string{
"当前用户只想自己离开群时使用 chat group quit",
"仍需保留群供他人继续使用时不要解散",
"目标群未确认或用户未明确同意不可恢复后果时不要执行",
},
Examples: []string{"dws chat group dismiss --group <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupDismissCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupDismissCmd.MarkFlagRequired("group")
chatGroupSetHistoryCmd := &cobra.Command{
Use: "set-history",
Short: "设置新成员入群可查看历史消息选项",
Long: `设置新成员入群后可查看的历史消息范围。--option 取值:
FORBIDDEN 禁止查看历史消息
RECENT_100 可查看最近 100 条消息
ALL 可查看全部历史消息`,
Example: ` dws chat group set-history --group <openConversationId> --option RECENT_100
dws chat group set-history --group <openConversationId> --option FORBIDDEN
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "option"); err != nil {
return err
}
option := mustGetFlag(cmd, "option")
switch option {
case "FORBIDDEN", "RECENT_100", "ALL":
default:
return fmt.Errorf("--option must be one of FORBIDDEN | RECENT_100 | ALL, got %q", option)
}
return callMCPToolOnServer("im", "update_show_history_msg_option", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"option": option,
})
},
}
DeclareLeafMetadata(chatGroupSetHistoryCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_show_history_msg_option",
CanonicalPath: "chat.update_show_history_msg_option",
CLIPath: "chat group set-history",
PrimaryCLIPath: "chat group set-history",
},
Description: "设置新成员可见的群历史消息范围",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_show_history_msg_option"},
},
Selection: contract.SelectionSpec{
AgentSummary: "设置新成员可见的群历史消息范围",
UseWhen: []string{"需要调整新成员入群后的历史消息可见性时"},
AvoidWhen: []string{"普通消息查询或群设置的其他开关不要使用"},
Examples: []string{"dws chat group set-history --group <openConversationId> --option RECENT_100"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId"},
},
},
})
chatGroupSetHistoryCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupSetHistoryCmd.MarkFlagRequired("group")
chatGroupSetHistoryCmd.Flags().String("option", "", "可见范围: FORBIDDEN | RECENT_100 | ALL (必填)")
_ = chatGroupSetHistoryCmd.MarkFlagRequired("option")
chatMessageCombineForwardCmd := &cobra.Command{
Use: "combine-forward",
Short: "合并转发多条消息",
Long: `将多条消息合并后转发到目标会话。需要指定源会话 ID、源消息 ID 列表(逗号分隔)、目标会话 ID。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>
(openDingTalkId 可通过 dws contact user search --query "姓名" 获取)`,
Example: ` dws chat message combine-forward --src-conversation-id <srcOpenCid> --msg-ids <id1>,<id2>,<id3> --dest-conversation-id <destOpenCid>
dws chat message combine-forward --src-conversation-id <srcOpenCid> --msg-ids <id1>,<id2> --dest-conversation-id <destOpenCid> --uuid <idempotencyKey>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "src-conversation-id", "msg-ids", "dest-conversation-id"); err != nil {
return err
}
toolArgs := map[string]any{
"srcOpenCid": mustGetFlag(cmd, "src-conversation-id"),
"srcOpenMessageIds": parseCSVValues(mustGetFlag(cmd, "msg-ids")),
"destOpenCid": mustGetFlag(cmd, "dest-conversation-id"),
}
if v, _ := cmd.Flags().GetString("uuid"); v != "" {
toolArgs["uuid"] = v
}
return callMCPToolOnServer("im", "combine_forward_messages", toolArgs)
},
}
DeclareLeafMetadata(chatMessageCombineForwardCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "combine_forward_messages",
CanonicalPath: "chat.combine_forward_messages",
CLIPath: "chat message combine-forward",
PrimaryCLIPath: "chat message combine-forward",
},
Description: "把多条消息合并转发到目标会话",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "combine_forward_messages"},
},
Selection: contract.SelectionSpec{
AgentSummary: "把多条消息合并转发到目标会话",
UseWhen: []string{"需要保留多条源消息并作为合集转发时"},
AvoidWhen: []string{"只转发单条消息时使用 chat message forward"},
Examples: []string{"dws chat message combine-forward --src-conversation-id <srcConversationId> --msg-ids <id1>,<id2> --dest-conversation-id <destConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "dest-conversation-id", Property: "destOpenCid"},
{Name: "msg-ids", Property: "srcOpenMessageIds"},
{Name: "src-conversation-id", Property: "srcOpenCid"},
},
},
})
chatMessageCombineForwardCmd.Flags().String("src-conversation-id", "", "源会话 openConversationId (必填,支持单聊/群聊)")
_ = chatMessageCombineForwardCmd.MarkFlagRequired("src-conversation-id")
chatMessageCombineForwardCmd.Flags().String("msg-ids", "", "源消息 openMessageId 列表,逗号分隔 (必填)")
_ = chatMessageCombineForwardCmd.MarkFlagRequired("msg-ids")
chatMessageCombineForwardCmd.Flags().String("dest-conversation-id", "", "目标会话 openConversationId (必填,支持单聊/群聊)")
_ = chatMessageCombineForwardCmd.MarkFlagRequired("dest-conversation-id")
chatMessageCombineForwardCmd.Flags().String("uuid", "", "幂等键(可选)")
// ── message forward-topic: 转发话题消息 ─────────────────────
chatMessageForwardTopicCmd := &cobra.Command{
Use: "forward-topic",
Short: "转发话题消息到目标会话",
Long: `将一条话题消息从源会话转发到目标会话。需要指定源消息 ID、源会话 ID、话题 ID、目标会话 ID。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>
(openDingTalkId 可通过 dws contact user search --query "姓名" 获取)
话题 ID(srcOpenConvThreadId)格式为 "convThread" + 加密后的 convThreadId,
可通过 dws chat message list 返回的话题信息获取。`,
Example: ` dws chat message forward-topic --src-msg-id <srcOpenMessageId> --src-conversation-id <srcOpenConversationId> --src-thread-id <srcOpenConvThreadId> --dest-conversation-id <destOpenConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "src-msg-id", "src-conversation-id", "src-thread-id", "dest-conversation-id"); err != nil {
return err
}
toolArgs := map[string]any{
"srcOpenMessageId": mustGetFlag(cmd, "src-msg-id"),
"srcOpenConversationId": mustGetFlag(cmd, "src-conversation-id"),
"srcOpenConvThreadId": mustGetFlag(cmd, "src-thread-id"),
"destOpenConversationId": mustGetFlag(cmd, "dest-conversation-id"),
}
return callMCPToolOnServer("im", "forward_topic", toolArgs)
},
}
DeclareLeafMetadata(chatMessageForwardTopicCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "forward_topic",
CanonicalPath: "chat.forward_topic",
CLIPath: "chat message forward-topic",
PrimaryCLIPath: "chat message forward-topic",
},
Description: "把一个话题消息转发到目标会话",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "forward_topic"},
},
Selection: contract.SelectionSpec{
AgentSummary: "把一个话题消息转发到目标会话",
UseWhen: []string{"需要转发话题圈中的主题及上下文时"},
AvoidWhen: []string{"普通单条消息转发使用 chat message forward"},
Examples: []string{"dws chat message forward-topic --src-msg-id <openMessageId> --src-conversation-id <srcConversationId> --src-thread-id <threadId> --dest-conversation-id <destConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "dest-conversation-id", Property: "destOpenConversationId"},
{Name: "src-conversation-id", Property: "srcOpenConversationId"},
{Name: "src-msg-id", Property: "srcOpenMessageId"},
{Name: "src-thread-id", Property: "srcOpenConvThreadId"},
},
},
})
chatMessageForwardTopicCmd.Flags().String("src-msg-id", "", "源消息 openMessageId (必填,要转发的消息)")
_ = chatMessageForwardTopicCmd.MarkFlagRequired("src-msg-id")
chatMessageForwardTopicCmd.Flags().String("src-conversation-id", "", "源会话 openConversationId (必填,消息所在的会话)")
_ = chatMessageForwardTopicCmd.MarkFlagRequired("src-conversation-id")
chatMessageForwardTopicCmd.Flags().String("src-thread-id", "", "话题 ID (必填,格式: convThread + 加密后的convThreadId)")
_ = chatMessageForwardTopicCmd.MarkFlagRequired("src-thread-id")
chatMessageForwardTopicCmd.Flags().String("dest-conversation-id", "", "目标会话 openConversationId (必填,转发到的会话)")
_ = chatMessageForwardTopicCmd.MarkFlagRequired("dest-conversation-id")
// ── message pin: 钉住/取消钉住/拉取钉住消息 ──────────────
chatMessageSetPinCmd := &cobra.Command{
Use: "set-pin-msg",
Short: "钉住消息(Pin)",
Long: `将指定消息设置为钉住状态。需要指定会话 ID 和消息 ID。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>`,
Example: ` dws chat message set-pin-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "open-conversation-id", "msg-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "set_pin_message", map[string]any{
"openConversationId": mustGetFlag(cmd, "open-conversation-id"),
"openMessageId": mustGetFlag(cmd, "msg-id"),
})
},
}
DeclareLeafMetadata(chatMessageSetPinCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "set_pin_message",
CanonicalPath: "chat.set_pin_message",
CLIPath: "chat message set-pin-msg",
PrimaryCLIPath: "chat message set-pin-msg",
},
Description: "把指定消息设为会话置顶消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "set_pin_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "把指定消息设为会话置顶消息",
UseWhen: []string{"需要在会话中置顶一条已知消息时"},
AvoidWhen: []string{"取消置顶使用 chat message unset-pin-msg"},
Examples: []string{"dws chat message set-pin-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"},
},
Parameters: []contract.ParamDecl{
{Name: "msg-id", Property: "openMessageId"},
},
},
})
chatMessageSetPinCmd.Flags().String("open-conversation-id", "", "会话 openConversationId (必填,支持群聊/单聊)")
_ = chatMessageSetPinCmd.MarkFlagRequired("open-conversation-id")
chatMessageSetPinCmd.Flags().String("msg-id", "", "消息 openMessageId (必填)")
_ = chatMessageSetPinCmd.MarkFlagRequired("msg-id")
chatMessageUnsetPinCmd := &cobra.Command{
Use: "unset-pin-msg",
Short: "取消钉住消息(Unpin)",
Long: `取消指定消息的钉住状态。需要指定会话 ID 和消息 ID。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>`,
Example: ` dws chat message unset-pin-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "open-conversation-id", "msg-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "unset_pin_message", map[string]any{
"openConversationId": mustGetFlag(cmd, "open-conversation-id"),
"openMessageId": mustGetFlag(cmd, "msg-id"),
})
},
}
DeclareLeafMetadata(chatMessageUnsetPinCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "unset_pin_message",
CanonicalPath: "chat.unset_pin_message",
CLIPath: "chat message unset-pin-msg",
PrimaryCLIPath: "chat message unset-pin-msg",
},
Description: "取消指定消息的会话置顶",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "unset_pin_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "取消指定消息的会话置顶",
UseWhen: []string{"需要移除一条已知置顶消息时"},
AvoidWhen: []string{"新增置顶使用 chat message set-pin-msg"},
Examples: []string{"dws chat message unset-pin-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"},
},
Parameters: []contract.ParamDecl{
{Name: "msg-id", Property: "openMessageId"},
},
},
})
chatMessageUnsetPinCmd.Flags().String("open-conversation-id", "", "会话 openConversationId (必填,支持群聊/单聊)")
_ = chatMessageUnsetPinCmd.MarkFlagRequired("open-conversation-id")
chatMessageUnsetPinCmd.Flags().String("msg-id", "", "消息 openMessageId (必填)")
_ = chatMessageUnsetPinCmd.MarkFlagRequired("msg-id")
chatMessageListPinCmd := &cobra.Command{
Use: "list-pin-msg",
Short: "拉取会话中钉住的消息列表",
Long: `拉取指定会话中被钉住的消息列表,支持游标分页。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>`,
Example: ` dws chat message list-pin-msg --open-conversation-id <openConversationId>
dws chat message list-pin-msg --open-conversation-id <openConversationId> --size 50
dws chat message list-pin-msg --open-conversation-id <openConversationId> --cursor <nextCursor> --size 20`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "open-conversation-id"); err != nil {
return err
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "open-conversation-id"),
}
if v, _ := cmd.Flags().GetString("cursor"); v != "" {
toolArgs["cursor"] = v
}
if v, _ := cmd.Flags().GetInt("size"); v > 0 {
toolArgs["count"] = v
}
return callMCPToolOnServer("im", "list_pin_messages", toolArgs)
},
}
DeclareLeafMetadata(chatMessageListPinCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_pin_messages",
CanonicalPath: "chat.list_pin_messages",
CLIPath: "chat message list-pin-msg",
PrimaryCLIPath: "chat message list-pin-msg",
},
Description: "列出指定会话中的置顶消息",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_pin_messages"},
},
Selection: contract.SelectionSpec{
AgentSummary: "列出指定会话中的置顶消息",
UseWhen: []string{"需要查看群聊当前置顶的消息时"},
AvoidWhen: []string{"设置或取消置顶时使用 set-pin-msg 或 unset-pin-msg"},
Examples: []string{"dws chat message list-pin-msg --open-conversation-id <openConversationId> --size 50"},
},
Parameters: []contract.ParamDecl{
{Name: "size", Property: "count"},
},
},
})
chatMessageListPinCmd.Flags().String("open-conversation-id", "", "会话 openConversationId (必填,支持群聊/单聊)")
_ = chatMessageListPinCmd.MarkFlagRequired("open-conversation-id")
chatMessageListPinCmd.Flags().String("cursor", "", "分页游标(首次不传,翻页时传上次返回的 nextCursor)")
chatMessageListPinCmd.Flags().Int("size", 0, "一次拉取的消息数量(默认 20,最大 100)")
// ── message favorites: 收藏/取消收藏/查询收藏消息 ──────────
chatMessageAddFavoriteCmd := &cobra.Command{
Use: "add-favorite",
Short: "收藏指定消息",
Long: `收藏指定消息。消息 ID 和会话 ID 可从 chat message list 等消息查询命令的返回结果中获取。
该操作会修改当前用户的消息收藏状态。`,
Example: ` dws chat message add-favorite --open-message-id <openMessageId> --open-conversation-id <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "open-message-id", "open-conversation-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "add_message_favorite", map[string]any{
"openMessageId": mustGetFlag(cmd, "open-message-id"),
"openConversationId": mustGetFlag(cmd, "open-conversation-id"),
})
},
}
DeclareLeafMetadata(chatMessageAddFavoriteCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "add_message_favorite",
CanonicalPath: "chat.add_message_favorite",
CLIPath: "chat message add-favorite",
PrimaryCLIPath: "chat message add-favorite",
},
Description: "将指定会话中的一条消息加入当前用户的收藏。",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
},
Selection: contract.SelectionSpec{
AgentSummary: "将指定会话中的一条消息加入当前用户的收藏。",
UseWhen: []string{"用户明确要收藏一条已知消息,且已取得消息 ID 与所属会话 ID 时。"},
AvoidWhen: []string{"需要给消息添加表情、置顶消息或发送新消息时不要使用;本命令只修改当前用户的收藏状态。"},
Examples: []string{"dws chat message add-favorite --open-message-id MSG_ID --open-conversation-id CONVERSATION_ID"},
},
},
})
chatMessageAddFavoriteCmd.Flags().String("open-message-id", "", "消息 openMessageId (必填)")
_ = chatMessageAddFavoriteCmd.MarkFlagRequired("open-message-id")
chatMessageAddFavoriteCmd.Flags().String("open-conversation-id", "", "消息所在会话的 openConversationId (必填,支持群聊/单聊)")
_ = chatMessageAddFavoriteCmd.MarkFlagRequired("open-conversation-id")
chatMessageRemoveFavoriteCmd := &cobra.Command{
Use: "remove-favorite",
Short: "取消收藏指定消息",
Long: `取消收藏指定消息。消息 ID 和会话 ID 可从 chat message list 等消息查询命令的返回结果中获取。
该操作只移除当前用户的收藏标记,不会删除原消息。`,
Example: ` dws chat message remove-favorite --open-message-id <openMessageId> --open-conversation-id <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "open-message-id", "open-conversation-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "remove_message_favorite", map[string]any{
"openMessageId": mustGetFlag(cmd, "open-message-id"),
"openConversationId": mustGetFlag(cmd, "open-conversation-id"),
})
},
}
DeclareLeafMetadata(chatMessageRemoveFavoriteCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "remove_message_favorite",
CanonicalPath: "chat.remove_message_favorite",
CLIPath: "chat message remove-favorite",
PrimaryCLIPath: "chat message remove-favorite",
},
Description: "取消当前用户对指定消息的收藏标记,不删除原消息。",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
},
Selection: contract.SelectionSpec{
AgentSummary: "取消当前用户对指定消息的收藏标记,不删除原消息。",
UseWhen: []string{"用户明确要从个人收藏中移除一条已知消息时。"},
AvoidWhen: []string{"需要撤回或删除原消息、移除表情回应或取消消息置顶时不要使用。"},
Examples: []string{"dws chat message remove-favorite --open-message-id MSG_ID --open-conversation-id CONVERSATION_ID"},
},
},
})
chatMessageRemoveFavoriteCmd.Flags().String("open-message-id", "", "消息 openMessageId (必填)")
_ = chatMessageRemoveFavoriteCmd.MarkFlagRequired("open-message-id")
chatMessageRemoveFavoriteCmd.Flags().String("open-conversation-id", "", "消息所在会话的 openConversationId (必填,支持群聊/单聊)")
_ = chatMessageRemoveFavoriteCmd.MarkFlagRequired("open-conversation-id")
chatMessageListFavoritesCmd := &cobra.Command{
Use: "list-favorites",
Short: "查询收藏的消息列表",
Long: `查询当前用户收藏的消息列表,支持数字游标分页。
首次请求可省略分页参数,CLI 会按 Open 服务契约传 cursor=0、size="20"。
返回 hasMore=true 时,将数字 nextCursor 作为下一次的 --cursor。默认只读取单页;只有显式传 --page-all 才会自动翻页并聚合 result.items。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 精确截断返回条数,--page-delay 控制页间等待毫秒数。`,
Example: ` dws chat message list-favorites
dws chat message list-favorites --size 30
dws chat message list-favorites --cursor 20 --size 20
dws chat message list-favorites --size 20 --page-all --page-limit 10 --max-items 100 --page-delay 0`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := pagedChatMessagesOnServerConfig("im", "list_message_favorites", chatMessageListFavoritesArgs)
cfg.ItemPath = "result.items"
cfg.CursorKind = PagedCursorInt64
return RunPagedMCPCommand(cmd, cfg)
},
}
DeclareLeafMetadata(chatMessageListFavoritesCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_message_favorites",
CanonicalPath: "chat.list_message_favorites",
CLIPath: "chat message list-favorites",
PrimaryCLIPath: "chat message list-favorites",
},
Description: "分页查询当前用户收藏的消息列表。",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
},
Selection: contract.SelectionSpec{
AgentSummary: "分页查询当前用户收藏的消息列表。",
UseWhen: []string{"需要查看当前用户已经收藏的消息,或使用 nextCursor 继续翻页时。"},
AvoidWhen: []string{"需要搜索普通聊天记录、置顶消息或修改收藏状态时不要使用。"},
Examples: []string{
"dws chat message list-favorites --cursor 0 --size 20",
"dws chat message list-favorites --size 20 --page-all --page-limit 10",
},
},
Parameters: append([]contract.ParamDecl{
{Name: "size", InterfaceType: "string"},
}, pagedMCPParamDecls()...),
},
})
chatMessageListFavoritesCmd.Flags().Int64("cursor", 0, "数字分页游标(默认 0;翻页时传上次返回的 nextCursor)")
chatMessageListFavoritesCmd.Flags().Int("size", 20, "一次拉取的收藏数量(默认 20,范围 1-30)")
AddPagedMCPFlags(chatMessageListFavoritesCmd)
// ── group list-my-groups: 拉取我创建/管理的群 ──────────────
chatGroupListMyGroupsCmd := &cobra.Command{
Use: "list-my-groups",
Short: "拉取我创建/管理的群",
Long: `拉取当前用户作为群主或管理员的群列表。
可通过 --role 过滤角色:OWNER 仅群主、ADMIN 仅管理员,不传则返回全部。
可通过 --limit 限制返回数量,不传则返回所有符合条件的群。`,
Example: ` dws chat group list-my-groups
dws chat group list-my-groups --role OWNER
dws chat group list-my-groups --role ADMIN --limit 10`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs := map[string]any{}
if v, _ := cmd.Flags().GetString("role"); v != "" {
if v != "OWNER" && v != "ADMIN" {
return apperrors.NewValidation("--role must be one of OWNER or ADMIN")
}
toolArgs["roleFilter"] = v
}
if v, _ := cmd.Flags().GetInt("limit"); v > 0 {
toolArgs["limit"] = v
}
if v, _ := cmd.Flags().GetBool("exclude-muted"); v {
toolArgs["excludeMuted"] = true
}
return callMCPToolOnServer("im", "list_owned_or_admin_groups", toolArgs)
},
}
DeclareLeafMetadata(chatGroupListMyGroupsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_owned_or_admin_groups",
CanonicalPath: "chat.list_owned_or_admin_groups",
CLIPath: "chat group list-my-groups",
PrimaryCLIPath: "chat group list-my-groups",
},
Description: "列出当前用户创建或管理的群聊",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_owned_or_admin_groups"},
},
Selection: contract.SelectionSpec{
AgentSummary: "列出当前用户创建或管理的群聊",
UseWhen: []string{"需要按群主或管理员角色盘点群聊时"},
AvoidWhen: []string{"按名称搜索任意可见群时使用 chat search"},
Examples: []string{"dws chat group list-my-groups --role OWNER --limit 100"},
},
Parameters: []contract.ParamDecl{
{Name: "limit", Required: boolPtr(false)},
{Name: "role", Property: "roleFilter", Required: boolPtr(false)},
},
},
})
chatGroupListMyGroupsCmd.Flags().String("role", "", "角色过滤: OWNER(仅群主) / ADMIN(仅管理员),不传返回全部")
chatGroupListMyGroupsCmd.Flags().Int("limit", 0, "最多返回群数量,不传返回全部")
chatGroupListMyGroupsCmd.Flags().Bool("exclude-muted", false, "是否排除已设置免打扰的群聊(默认 false)")
// ── group list-all: 分页拉取我所有群列表 ───────────────────────
chatGroupListAllCmd := &cobra.Command{
Use: "list-all",
Short: "分页拉取我所有群列表",
Long: `分页获取当前用户加入的所有群聊列表。
支持分页,每次最多返回 200 个群。`,
Example: ` dws chat group list-all
dws chat group list-all --limit 50
dws chat group list-all --limit 100 --cursor <nextCursor>`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs := map[string]any{}
if v, _ := cmd.Flags().GetInt("limit"); v > 0 {
toolArgs["limit"] = v
}
if v, _ := cmd.Flags().GetString("cursor"); v != "" && v != "0" {
toolArgs["cursor"] = v
}
return callMCPToolOnServer("im", "list_my_groups_pagination", toolArgs)
},
}
chatGroupListAllCmd.Flags().Int("limit", 100, "每页返回数量(默认 100,最大 200)")
chatGroupListAllCmd.Flags().String("cursor", "", "分页游标(首次不传,翻页传返回的 nextCursor)")
DeclareLeafMetadata(chatGroupListAllCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_my_groups_pagination",
CanonicalPath: "chat.list_my_groups_pagination",
CLIPath: "chat group list-all",
PrimaryCLIPath: "chat group list-all",
},
Description: "分页获取当前用户加入的所有群聊列表",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_my_groups_pagination"},
},
Selection: contract.SelectionSpec{
AgentSummary: "分页获取当前用户加入的所有群聊列表",
UseWhen: []string{"需要完整翻页盘点当前用户加入的所有群聊时"},
AvoidWhen: []string{"只按群主/管理员角色列群时使用 chat group list-my-groups"},
Examples: []string{"dws chat group list-all --limit 100"},
},
Parameters: []contract.ParamDecl{
{Name: "cursor", Property: "cursor", Required: boolPtr(false)},
{Name: "limit", Property: "limit", Required: boolPtr(false)},
},
},
})
// ── group list-join-validations: 分页拉取入群验证记录 ─────────
chatGroupListJoinValidationsCmd := &cobra.Command{
Use: "list-join-validations",
Short: "分页拉取入群验证记录",
Long: `分页拉取当前用户的所有入群验证记录,包括自己被拒绝的记录以及作为审批者的记录。
支持分页,每页最多返回 50 条。`,
Example: ` dws chat group list-join-validations
dws chat group list-join-validations --limit 30
dws chat group list-join-validations --limit 20 --cursor <nextCursor>`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs := map[string]any{}
if v, _ := cmd.Flags().GetInt("limit"); v > 0 {
toolArgs["limit"] = v
}
if v, _ := cmd.Flags().GetString("cursor"); v != "" {
toolArgs["cursor"] = v
}
return callMCPToolOnServer("im", "list_apply_join_group_records", toolArgs)
},
}
chatGroupListJoinValidationsCmd.Flags().Int("limit", 20, "单页数量(默认 20,最大 50)")
chatGroupListJoinValidationsCmd.Flags().String("cursor", "", "分页游标(首次不传,翻页传返回的 nextCursor)")
DeclareLeafMetadata(chatGroupListJoinValidationsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_apply_join_group_records",
CanonicalPath: "chat.list_apply_join_group_records",
CLIPath: "chat group list-join-validations",
PrimaryCLIPath: "chat group list-join-validations",
},
Description: "分页拉取当前用户相关的入群验证记录",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_apply_join_group_records"},
},
Selection: contract.SelectionSpec{
AgentSummary: "分页拉取当前用户相关的入群验证记录",
UseWhen: []string{"需要查看待处理、被拒绝或已处理的入群申请记录时"},
AvoidWhen: []string{"需要审批某条记录时使用 chat group audit-join-validation"},
Examples: []string{"dws chat group list-join-validations --limit 20"},
},
Parameters: []contract.ParamDecl{
{Name: "cursor", Property: "cursor", Required: boolPtr(false)},
{Name: "limit", Property: "limit", Required: boolPtr(false)},
},
},
})
// ── group audit-join-validation: 审批入群验证 ─────────────────────────
chatGroupAuditJoinValidationCmd := &cobra.Command{
Use: "audit-join-validation",
Short: "审批入群验证(通过、拒绝、删除)",
Long: `审批入群验证。真机实测服务端仅接受 AuditApprove / AuditDelete,其余状态会被拒绝(unsupported audit status)。
status 可选值:
AuditApprove — 通过(可用)
AuditDelete — 删除(可用)
AuditIgnore — 忽略(服务端拒绝,不可用)
AuditRefuse — 拒绝(服务端拒绝,不可用)
AuditBlock — 拒绝且不再接受该用户的申请(服务端拒绝,不可用)`,
Example: ` dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <userId> --inviter <userId> --status AuditApprove
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <userId> --inviter <userId> --status AuditDelete --description "不符合入群条件"
# 查询入群验证记录: dws chat group list-join-validations`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "record-id", "applicant", "inviter", "status"); err != nil {
return err
}
recordID, err := strconv.ParseInt(mustGetFlag(cmd, "record-id"), 10, 64)
if err != nil {
return fmt.Errorf("--record-id must be a valid integer: %w", err)
}
status := mustGetFlag(cmd, "status")
if status != "AuditApprove" && status != "AuditDelete" {
return fmt.Errorf("unsupported audit status %q, must be one of: AuditApprove, AuditDelete", status)
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"applyRecordId": recordID,
"applicantUid": mustGetFlag(cmd, "applicant"),
"inviterUid": mustGetFlag(cmd, "inviter"),
"status": status,
}
if v, _ := cmd.Flags().GetString("description"); v != "" {
toolArgs["auditDescription"] = v
}
return callMCPToolOnServer("im", "audit_join_group", toolArgs)
},
}
chatGroupAuditJoinValidationCmd.Flags().String("group", "", "群 openConversationId (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("group")
chatGroupAuditJoinValidationCmd.Flags().String("record-id", "", "申请记录 ID (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("record-id")
chatGroupAuditJoinValidationCmd.Flags().String("status", "", "审批动作,真机仅 AuditApprove/AuditDelete 可用;AuditIgnore/AuditRefuse/AuditBlock 服务端拒绝 (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("status")
chatGroupAuditJoinValidationCmd.Flags().String("applicant", "", "申请人 userId (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("applicant")
chatGroupAuditJoinValidationCmd.Flags().String("inviter", "", "邀请人 userId (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("inviter")
chatGroupAuditJoinValidationCmd.Flags().String("description", "", "审批说明(可选)")
DeclareLeafMetadata(chatGroupAuditJoinValidationCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "audit_join_group",
CanonicalPath: "chat.audit_join_group",
CLIPath: "chat group audit-join-validation",
PrimaryCLIPath: "chat group audit-join-validation",
},
Description: "审批入群验证记录",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "audit_join_group"},
},
Selection: contract.SelectionSpec{
AgentSummary: "审批入群验证记录",
UseWhen: []string{"需要对已知入群申请记录执行通过或删除审批动作时"},
AvoidWhen: []string{"还没有 record-id 时先用 chat group list-join-validations 查询"},
Examples: []string{"dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <userId> --inviter <userId> --status AuditApprove"},
},
Parameters: []contract.ParamDecl{
{Name: "applicant", Property: "applicantUid", Required: boolPtr(true)},
{Name: "description", Property: "auditDescription", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "inviter", Property: "inviterUid", Required: boolPtr(true)},
{Name: "record-id", Property: "applyRecordId", Required: boolPtr(true), InterfaceType: "integer"},
{Name: "status", Property: "status", Required: boolPtr(true), Enum: []string{"AuditApprove", "AuditDelete"}},
},
},
})
// ── mark-unread: 标记会话为未读 ───────────────────────────
chatMarkUnreadCmd := &cobra.Command{
Use: "mark-unread",
Short: "标记会话为未读",
Long: `将指定会话标记为未读状态。支持群聊和单聊。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>`,
Example: ` dws chat mark-unread --conversation-id <openConversationId>
dws chat mark-unread --id <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
convID := flagOrFallback(cmd, "conversation-id", "id", "chat")
if convID == "" {
return fmt.Errorf("flag --conversation-id is required\n hint: dws chat mark-unread --conversation-id <openConversationId>")
}
return callMCPToolOnServer("im", "mark_conversation_unread", map[string]any{
"openConversationId": convID,
})
},
}
chatMarkUnreadCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持群聊/单聊)")
chatMarkUnreadCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatMarkUnreadCmd.Flags().MarkHidden("id")
chatMarkUnreadCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatMarkUnreadCmd.Flags().MarkHidden("chat")
DeclareLeafMetadata(chatMarkUnreadCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "mark_conversation_unread",
CanonicalPath: "chat." + "mark_conversation_unread",
CLIPath: "chat mark-unread",
PrimaryCLIPath: "chat mark-unread",
},
Description: "将指定会话标记为未读",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "mark_conversation_unread"},
},
Selection: contract.SelectionSpec{
AgentSummary: "将指定会话标记为未读",
UseWhen: []string{"用户明确要把某个群聊或单聊标记为未读时"},
AvoidWhen: []string{"需要标记某条消息及之前消息已读时使用 chat mark-read"},
Examples: []string{"dws chat mark-unread --conversation-id <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
// ── clear-red-point: 清除会话红点 ──────────────────────────
chatClearRedPointCmd := &cobra.Command{
Use: "clear-red-point",
Short: "清除会话红点",
Long: `清除指定会话的未读红点。支持群聊和单聊。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>`,
Example: ` dws chat clear-red-point --conversation-id <openConversationId>
dws chat clear-red-point --id <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
convID := flagOrFallback(cmd, "conversation-id", "id", "chat")
if convID == "" {
return fmt.Errorf("flag --conversation-id is required\n hint: dws chat clear-red-point --conversation-id <openConversationId>")
}
return callMCPToolOnServer("im", "clear_conversation_red_point", map[string]any{
"openConversationId": convID,
})
},
}
chatClearRedPointCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持群聊/单聊)")
chatClearRedPointCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatClearRedPointCmd.Flags().MarkHidden("id")
chatClearRedPointCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatClearRedPointCmd.Flags().MarkHidden("chat")
DeclareLeafMetadata(chatClearRedPointCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "clear_conversation_red_point",
CanonicalPath: "chat." + "clear_conversation_red_point",
CLIPath: "chat clear-red-point",
PrimaryCLIPath: "chat clear-red-point",
},
Description: "清除指定会话未读红点",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "clear_conversation_red_point"},
},
Selection: contract.SelectionSpec{
AgentSummary: "清除指定会话未读红点",
UseWhen: []string{"用户只想清掉某个会话的未读红点时"},
AvoidWhen: []string{"需要清除所有会话红点时使用 chat clear-all-red-point"},
Examples: []string{"dws chat clear-red-point --conversation-id <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
// ── clear-all-red-point: 红点清零(全部已读) ─────────────────
chatClearAllRedPointCmd := &cobra.Command{
Use: "clear-all-red-point",
Short: "清除所有会话红点(全部已读)",
Long: `一键清除当前用户所有会话的未读红点,等效于“全部已读”。`,
Example: ` dws chat clear-all-red-point`,
RunE: func(cmd *cobra.Command, args []string) error {
return callMCPToolOnServer("im", "clear_all_red_point", map[string]any{})
},
}
DeclareLeafMetadata(chatClearAllRedPointCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "clear_all_red_point",
CanonicalPath: "chat." + "clear_all_red_point",
CLIPath: "chat clear-all-red-point",
PrimaryCLIPath: "chat clear-all-red-point",
},
Description: "清除当前用户所有会话未读红点",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "clear_all_red_point"},
},
Selection: contract.SelectionSpec{
AgentSummary: "清除当前用户所有会话未读红点",
UseWhen: []string{"用户明确要求一键清除所有会话未读红点或全部已读时"},
AvoidWhen: []string{"只处理单个会话红点时使用 chat clear-red-point"},
Examples: []string{"dws chat clear-all-red-point"},
},
},
})
// ── list-all-conversations: 分页获取全部会话 ────────────────────
chatListAllConversationsCmd := &cobra.Command{
Use: "list-all-conversations",
Short: "分页获取当前用户的全部会话列表",
Long: `分页获取当前用户的全部会话列表(包含单聊和群聊)。--limit 指定每页数量(1-100,默认 100),--cursor 传分页游标(首次不传或传 0)。
返回 hasMore=true 时用 nextCursor 作为下次 --cursor 继续翻页。`,
Example: ` dws chat list-all-conversations
dws chat list-all-conversations --limit 50
dws chat list-all-conversations --limit 100 --cursor <nextCursor>
dws chat list-all-conversations --exclude-muted`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs := map[string]any{}
if v, err := cmd.Flags().GetInt("limit"); err == nil && v > 0 {
// 服务端每页上限 100,超过会被静默截断,这里显式拒绝以免误以为取全。
if v > 100 {
return fmt.Errorf("--limit 最大 100(服务端上限),got %d;如需取全部会话请配合 --cursor 翻页", v)
}
toolArgs["limit"] = v
}
if v, _ := cmd.Flags().GetInt64("cursor"); v > 0 {
toolArgs["cursor"] = v
}
if v, _ := cmd.Flags().GetBool("exclude-muted"); v {
toolArgs["excludeMuted"] = true
}
return callMCPToolOnServer("im", "list_all_conversations", toolArgs)
},
}
chatListAllConversationsCmd.Flags().Int("limit", 100, "每页数量(1-100,默认 100)")
chatListAllConversationsCmd.Flags().Int64("cursor", 0, "分页游标(首次不传或传 0,翻页传 nextCursor)")
chatListAllConversationsCmd.Flags().Bool("exclude-muted", false, "是否排除已免打扰会话(默认 false)")
DeclareLeafMetadata(chatListAllConversationsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_all_conversations",
CanonicalPath: "chat.list_all_conversations",
CLIPath: "chat list-all-conversations",
PrimaryCLIPath: "chat list-all-conversations",
},
Description: "分页获取当前用户的全部会话列表",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_all_conversations"},
},
Selection: contract.SelectionSpec{
AgentSummary: "分页获取当前用户的全部会话列表",
UseWhen: []string{"需要枚举当前用户全部单聊和群聊会话并按 cursor 翻页时"},
AvoidWhen: []string{"只搜索群聊时使用 chat search;只查某个会话详情时使用 chat conversation-info"},
Examples: []string{"dws chat list-all-conversations --limit 100"},
},
Parameters: []contract.ParamDecl{
{Name: "cursor", Property: "cursor", Required: boolPtr(false), InterfaceType: "integer"},
{Name: "exclude-muted", Property: "excludeMuted", Required: boolPtr(false), InterfaceType: "boolean"},
{Name: "limit", Property: "limit", Required: boolPtr(false), InterfaceType: "integer"},
},
},
})
// ── clear-messages: 清空会话聊天记录 ────────────────────────────
chatClearMessagesCmd := &cobra.Command{
Use: "clear-messages",
Short: "清空当前用户指定会话的聊天记录",
Long: `清空当前用户在指定会话中的聊天记录。仅清空当前用户视角的消息,不影响其他成员。该操作不可逆;必须先获得用户确认,再追加 --yes 执行。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>`,
Example: ` dws chat clear-messages --conversation-id <openConversationId>
dws chat clear-messages --id <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
convID := flagOrFallback(cmd, "conversation-id", "id", "chat")
if convID == "" {
return fmt.Errorf("flag --conversation-id is required\n hint: dws chat clear-messages --conversation-id <openConversationId>")
}
if !commandBoolFlag(cmd, "yes") {
return apperrors.NewValidation(
"清空会话聊天记录不可逆;获得用户确认后加 --yes 执行",
apperrors.WithReason("confirmation_required"),
apperrors.WithHint("先确认目标会话及影响范围;用户明确同意后以相同参数追加 --yes"),
apperrors.WithActions("确认目标会话", "获得用户确认后使用 --yes 执行"),
)
}
return callMCPToolOnServer("im", "clear_conversation_messages", map[string]any{
"openConversationId": convID,
})
},
}
chatClearMessagesCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持群聊/单聊)")
chatClearMessagesCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatClearMessagesCmd.Flags().MarkHidden("id")
chatClearMessagesCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatClearMessagesCmd.Flags().MarkHidden("chat")
DeclareLeafMetadata(chatClearMessagesCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "destructive", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "clear_conversation_messages",
CanonicalPath: "chat.clear_conversation_messages",
CLIPath: "chat clear-messages",
PrimaryCLIPath: "chat clear-messages",
},
Description: "清空当前用户指定会话中的聊天记录",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "clear_conversation_messages"},
},
Selection: contract.SelectionSpec{
AgentSummary: "清空当前用户指定会话中的聊天记录",
UseWhen: []string{"用户明确要求清空某个会话在当前用户视角的聊天记录,并已确认不可逆影响时"},
AvoidWhen: []string{"只清除未读红点时使用 chat clear-red-point 或 chat clear-all-red-point"},
Examples: []string{"dws chat clear-messages --conversation-id <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
// ── mark-read: 标记消息已读 ────────────────────────────────────
chatMarkReadCmd := &cobra.Command{
Use: "mark-read",
Short: "标记消息已读",
Long: `标记指定会话中某条消息为已读。该消息及之前的所有消息都会被标记为已读。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>
如何获取 openMessageId:
- dws chat message list --group <openConversationId> --time "2025-03-01 00:00:00"`,
Example: ` dws chat mark-read --conversation-id <openConversationId> --message-id <openMessageId>
dws chat mark-read --id <openConversationId> --message-id <openMessageId>`,
RunE: func(cmd *cobra.Command, args []string) error {
convID := flagOrFallback(cmd, "conversation-id", "id", "chat")
if convID == "" {
return fmt.Errorf("flag --conversation-id is required\n hint: dws chat mark-read --conversation-id <openConversationId> --message-id <openMessageId>")
}
if err := validateRequiredFlags(cmd, "message-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "mark_message_read", map[string]any{
"openConversationId": convID,
"openMessageId": mustGetFlag(cmd, "message-id"),
})
},
}
chatMarkReadCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持群聊/单聊)")
chatMarkReadCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatMarkReadCmd.Flags().MarkHidden("id")
chatMarkReadCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatMarkReadCmd.Flags().MarkHidden("chat")
chatMarkReadCmd.Flags().String("message-id", "", "消息 openMessageId (必填)")
_ = chatMarkReadCmd.MarkFlagRequired("message-id")
DeclareLeafMetadata(chatMarkReadCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "mark_message_read",
CanonicalPath: "chat." + "mark_message_read",
CLIPath: "chat mark-read",
PrimaryCLIPath: "chat mark-read",
},
Description: "将指定消息及之前消息标记为已读",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "mark_message_read"},
},
Selection: contract.SelectionSpec{
AgentSummary: "将指定消息及之前消息标记为已读",
UseWhen: []string{"已有会话和消息 ID,需要把该消息及之前消息标记已读时"},
AvoidWhen: []string{"只标记会话为未读时使用 chat mark-unread"},
Examples: []string{"dws chat mark-read --conversation-id <openConversationId> --message-id <openMessageId>"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "message-id", Property: "openMessageId", Required: boolPtr(true)},
},
},
})
// ── set-top-msg: 置顶某条消息 ────────────────────────────────
chatMessageSetTopMsgCmd := &cobra.Command{
Use: "set-top-msg",
Short: "置顶消息",
Long: `将指定消息设置为置顶状态。需要指定会话 ID 和消息 ID。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>`,
Example: ` dws chat message set-top-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "open-conversation-id", "msg-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "set_top_message", map[string]any{
"openConversationId": mustGetFlag(cmd, "open-conversation-id"),
"openMessageId": mustGetFlag(cmd, "msg-id"),
})
},
}
chatMessageSetTopMsgCmd.Flags().String("open-conversation-id", "", "会话 openConversationId (必填,支持群聊/单聊)")
_ = chatMessageSetTopMsgCmd.MarkFlagRequired("open-conversation-id")
chatMessageSetTopMsgCmd.Flags().String("msg-id", "", "消息 openMessageId (必填)")
_ = chatMessageSetTopMsgCmd.MarkFlagRequired("msg-id")
DeclareLeafMetadata(chatMessageSetTopMsgCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "set_top_message",
CanonicalPath: "chat." + "set_top_message",
CLIPath: "chat message set-top-msg",
PrimaryCLIPath: "chat message set-top-msg",
},
Description: "将指定会话消息设置为置顶",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "set_top_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "将指定会话消息设置为置顶",
UseWhen: []string{"需要把群聊或单聊中的某条消息置顶展示时"},
AvoidWhen: []string{"取消消息置顶时使用 chat message unset-top-msg"},
Examples: []string{"dws chat message set-top-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"},
},
Parameters: []contract.ParamDecl{
{Name: "msg-id", Property: "openMessageId", Required: boolPtr(true)},
{Name: "open-conversation-id", Property: "openConversationId", Required: boolPtr(true)},
},
},
})
// ── unset-top-msg: 取消置顶某条消息 ─────────────────────────
chatMessageUnsetTopMsgCmd := &cobra.Command{
Use: "unset-top-msg",
Short: "取消置顶消息",
Long: `取消指定消息的置顶状态。需要指定会话 ID 和消息 ID。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>`,
Example: ` dws chat message unset-top-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "open-conversation-id", "msg-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "unset_top_message", map[string]any{
"openConversationId": mustGetFlag(cmd, "open-conversation-id"),
"openMessageId": mustGetFlag(cmd, "msg-id"),
})
},
}
chatMessageUnsetTopMsgCmd.Flags().String("open-conversation-id", "", "会话 openConversationId (必填,支持群聊/单聊)")
_ = chatMessageUnsetTopMsgCmd.MarkFlagRequired("open-conversation-id")
chatMessageUnsetTopMsgCmd.Flags().String("msg-id", "", "消息 openMessageId (必填)")
_ = chatMessageUnsetTopMsgCmd.MarkFlagRequired("msg-id")
DeclareLeafMetadata(chatMessageUnsetTopMsgCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "unset_top_message",
CanonicalPath: "chat." + "unset_top_message",
CLIPath: "chat message unset-top-msg",
PrimaryCLIPath: "chat message unset-top-msg",
},
Description: "取消指定会话消息的置顶",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "unset_top_message"},
},
Selection: contract.SelectionSpec{
AgentSummary: "取消指定会话消息的置顶",
UseWhen: []string{"需要取消某条消息的置顶状态时"},
AvoidWhen: []string{"设置消息置顶时使用 chat message set-top-msg"},
Examples: []string{"dws chat message unset-top-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"},
},
Parameters: []contract.ParamDecl{
{Name: "msg-id", Property: "openMessageId", Required: boolPtr(true)},
{Name: "open-conversation-id", Property: "openConversationId", Required: boolPtr(true)},
},
},
})
// group 与 members 的子命令在下方(chatGroupCmd.AddCommand / chatGroupMembersCmd.AddCommand
// 的完整列表处)统一注册;此处不再重复 AddCommand,否则 --help 会重复列出。
// ── group update-nick: 设置用户在群内的群昵称 ──────────────
chatGroupUpdateNickCmd := &cobra.Command{
Use: "update-nick",
Short: "设置或清除用户在群内的群昵称",
Long: `设置当前用户在指定群聊内的个人群昵称。不传 --nick 时清除当前群昵称。`,
Example: ` dws chat group update-nick --group <openConversationId> --nick "我的群昵称"
dws chat group update-nick --group <openConversationId>
# 不传 --nick 表示清除群昵称
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
nick, _ := cmd.Flags().GetString("nick")
return callMCPToolOnServer("im", "update_group_nick", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"nick": nick,
})
},
}
DeclareLeafMetadata(chatGroupUpdateNickCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_group_nick",
CanonicalPath: "chat.update_group_nick",
CLIPath: "chat group update-nick",
PrimaryCLIPath: "chat group update-nick",
},
Description: "设置或清除当前用户在指定群内的昵称",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: the executable CLI maps nickname update or clear semantics to im/update_group_nick, which is absent from the pinned MCP metadata snapshot.",
},
Selection: contract.SelectionSpec{
AgentSummary: "设置或清除当前用户在指定群内的昵称",
UseWhen: []string{"用户要求修改自己的群昵称,或明确要求清除群昵称"},
AvoidWhen: []string{"修改群名称应使用 chat group rename;修改其他成员信息不应使用本命令"},
Examples: []string{
"dws chat group update-nick --group <openConversationId> --nick \"项目昵称\"",
"dws chat group update-nick --group <openConversationId>",
},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "nick", Property: "nick", Required: boolPtr(false)},
},
},
})
chatGroupUpdateNickCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupUpdateNickCmd.MarkFlagRequired("group")
chatGroupUpdateNickCmd.Flags().String("nick", "", "个人群昵称,不传则清除群昵称")
// ── group update-alias: 设置群备注 ──────────────────────────
chatGroupUpdateAliasCmd := &cobra.Command{
Use: "update-alias",
Short: "设置群备注",
Long: `设置当前用户对指定群聊的备注名称(仅自己可见)。`,
Example: ` dws chat group update-alias --group <openConversationId> --alias-title "项目A群"
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "alias-title"); err != nil {
return err
}
return callMCPToolOnServer("im", "update_user_group_alias", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"aliasTitle": mustGetFlag(cmd, "alias-title"),
})
},
}
chatGroupUpdateAliasCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupUpdateAliasCmd.MarkFlagRequired("group")
chatGroupUpdateAliasCmd.Flags().String("alias-title", "", "群备注标题 (必填)")
_ = chatGroupUpdateAliasCmd.MarkFlagRequired("alias-title")
DeclareLeafMetadata(chatGroupUpdateAliasCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_user_group_alias",
CanonicalPath: "chat.update_user_group_alias",
CLIPath: "chat group update-alias",
PrimaryCLIPath: "chat group update-alias",
},
Description: "设置当前用户可见的群备注名称",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_user_group_alias"},
},
Selection: contract.SelectionSpec{
AgentSummary: "设置当前用户可见的群备注名称",
UseWhen: []string{"用户要求给指定群设置仅自己可见的备注名时"},
AvoidWhen: []string{"修改群公开名称应使用 chat group rename"},
Examples: []string{"dws chat group update-alias --group <openConversationId> --alias-title \"项目A群\""},
},
Parameters: []contract.ParamDecl{
{Name: "alias-title", Property: "aliasTitle", Required: boolPtr(true)},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
},
},
})
// ── hide: 会话列表中隐藏会话 ────────────────────────────────
chatHideCmd := &cobra.Command{
Use: "hide",
Short: "会话列表中隐藏会话",
Long: `在会话列表中隐藏指定会话(支持单聊/群聊)。隐藏后会话不再显示在列表中,收到新消息时会重新出现。`,
Example: ` dws chat hide --conversation-id <openConversationId>
dws chat hide --id <openConversationId>
# 查询群 ID: dws chat search --query "群名"
# 查询单聊会话 ID: dws chat conversation-info --user <userId>`,
RunE: func(cmd *cobra.Command, args []string) error {
convID := flagOrFallback(cmd, "conversation-id", "id", "chat")
if convID == "" {
return fmt.Errorf("flag --conversation-id is required\n hint: dws chat hide --conversation-id <openConversationId>")
}
return callMCPToolOnServer("im", "hide_conversation", map[string]any{
"openConversationId": convID,
})
},
}
chatHideCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
chatHideCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatHideCmd.Flags().MarkHidden("id")
chatHideCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatHideCmd.Flags().MarkHidden("chat")
DeclareLeafMetadata(chatHideCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "hide_conversation",
CanonicalPath: "chat.hide_conversation",
CLIPath: "chat hide",
PrimaryCLIPath: "chat hide",
},
Description: "在会话列表中隐藏指定会话",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "hide_conversation"},
},
Selection: contract.SelectionSpec{
AgentSummary: "在会话列表中隐藏指定会话",
UseWhen: []string{"用户要求从当前会话列表隐藏某个单聊或群聊时"},
AvoidWhen: []string{"只想关闭通知提醒时使用 chat mute 或 chat mute-at-all"},
Examples: []string{"dws chat hide --conversation-id <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
// ── mute-at-all: 关闭/开启 @所有人通知 ─────────────────────
chatMuteAtAllCmd := &cobra.Command{
Use: "mute-at-all",
Short: "关闭/开启 @所有人消息提醒",
Long: `关闭或开启会话中 @所有人的消息通知。默认关闭通知,传 --off 则恢复接收通知。`,
Example: ` dws chat mute-at-all --conversation-id <openConversationId>
dws chat mute-at-all --conversation-id <openConversationId> --off
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
convID := flagOrFallback(cmd, "conversation-id", "id", "chat")
if convID == "" {
return fmt.Errorf("flag --conversation-id is required\n hint: dws chat mute-at-all --conversation-id <openConversationId>")
}
off, _ := cmd.Flags().GetBool("off")
return callMCPToolOnServer("im", "update_at_all_notification_off", map[string]any{
"openConversationId": convID,
"mute": !off,
})
},
}
chatMuteAtAllCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
chatMuteAtAllCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatMuteAtAllCmd.Flags().MarkHidden("id")
chatMuteAtAllCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatMuteAtAllCmd.Flags().MarkHidden("chat")
chatMuteAtAllCmd.Flags().Bool("off", false, "恢复接收 @所有人通知(不传则关闭通知)")
DeclareLeafMetadata(chatMuteAtAllCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_at_all_notification_off",
CanonicalPath: "chat.update_at_all_notification_off",
CLIPath: "chat mute-at-all",
PrimaryCLIPath: "chat mute-at-all",
},
Description: "关闭或恢复会话中的 @所有人消息提醒",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_at_all_notification_off"},
},
Selection: contract.SelectionSpec{
AgentSummary: "关闭或恢复会话中的 @所有人消息提醒",
UseWhen: []string{"用户要求关闭或恢复指定会话的 @所有人提醒时"},
AvoidWhen: []string{"关闭普通消息免打扰使用 chat mute;关闭红包提醒使用 chat mute-red-envelope"},
Examples: []string{"dws chat mute-at-all --conversation-id <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "off", Property: "mute", Required: boolPtr(false), InterfaceType: "boolean"},
},
},
})
// ── mute-red-envelope: 关闭/开启红包通知 ────────────────────
chatMuteRedEnvelopeCmd := &cobra.Command{
Use: "mute-red-envelope",
Short: "关闭/开启红包消息提醒",
Long: `关闭或开启会话中的红包消息通知。默认关闭通知,传 --off 则恢复接收通知。`,
Example: ` dws chat mute-red-envelope --conversation-id <openConversationId>
dws chat mute-red-envelope --conversation-id <openConversationId> --off
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
convID := flagOrFallback(cmd, "conversation-id", "id", "chat")
if convID == "" {
return fmt.Errorf("flag --conversation-id is required\n hint: dws chat mute-red-envelope --conversation-id <openConversationId>")
}
off, _ := cmd.Flags().GetBool("off")
return callMCPToolOnServer("im", "update_red_env_notification_off", map[string]any{
"openConversationId": convID,
"mute": !off,
})
},
}
chatMuteRedEnvelopeCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
chatMuteRedEnvelopeCmd.Flags().String("id", "", "--conversation-id 的别名")
_ = chatMuteRedEnvelopeCmd.Flags().MarkHidden("id")
chatMuteRedEnvelopeCmd.Flags().String("chat", "", "--conversation-id 的别名")
_ = chatMuteRedEnvelopeCmd.Flags().MarkHidden("chat")
chatMuteRedEnvelopeCmd.Flags().Bool("off", false, "恢复接收红包通知(不传则关闭通知)")
DeclareLeafMetadata(chatMuteRedEnvelopeCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "low",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "update_red_env_notification_off",
CanonicalPath: "chat.update_red_env_notification_off",
CLIPath: "chat mute-red-envelope",
PrimaryCLIPath: "chat mute-red-envelope",
},
Description: "关闭或恢复会话中的红包消息提醒",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "update_red_env_notification_off"},
},
Selection: contract.SelectionSpec{
AgentSummary: "关闭或恢复会话中的红包消息提醒",
UseWhen: []string{"用户要求关闭或恢复指定会话的红包提醒时"},
AvoidWhen: []string{"关闭 @所有人提醒使用 chat mute-at-all;关闭普通消息免打扰使用 chat mute"},
Examples: []string{"dws chat mute-red-envelope --conversation-id <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "off", Property: "mute", Required: boolPtr(false), InterfaceType: "boolean"},
},
},
})
// ── group members list-by-ids: 批量查看群成员详情 ───────────
chatGroupMembersListByIdsCmd := &cobra.Command{
Use: "list-by-ids",
Short: "根据成员 ID 批量查询群成员详情",
Long: `根据群 openConversationId 和成员 openDingTalkId 列表,批量查询群成员详情信息。`,
Example: ` dws chat group members list-by-ids --id <openConversationId> --users openDingTalkId1,openDingTalkId2
# 查询群 ID: dws chat search --query "群名"
# 查询 openDingTalkId: dws contact user search --query "姓名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id", "users"); err != nil {
return err
}
users := parseCSVValues(mustGetFlag(cmd, "users"))
return callMCPToolOnServer("im", "list_group_member_by_ids", map[string]any{
"openConversationId": mustGetFlag(cmd, "id"),
"memberOpenDingTalkIds": users,
})
},
}
chatGroupMembersListByIdsCmd.Flags().String("id", "", "群 ID / openConversationId (必填)")
_ = chatGroupMembersListByIdsCmd.MarkFlagRequired("id")
chatGroupMembersListByIdsCmd.Flags().String("users", "", "成员 openDingTalkId 列表,逗号分隔 (必填)")
_ = chatGroupMembersListByIdsCmd.MarkFlagRequired("users")
DeclareLeafMetadata(chatGroupMembersListByIdsCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_group_member_by_ids",
CanonicalPath: "chat.list_group_member_by_ids",
CLIPath: "chat group members list-by-ids",
PrimaryCLIPath: "chat group members list-by-ids",
},
Description: "按成员 openDingTalkId 批量查询群成员详情",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_group_member_by_ids"},
},
Selection: contract.SelectionSpec{
AgentSummary: "按成员 openDingTalkId 批量查询群成员详情",
UseWhen: []string{"已知群 ID 和一组成员 openDingTalkId,需要批量查看这些成员资料时"},
AvoidWhen: []string{"需要列出群内全部成员时使用 chat group members list"},
Examples: []string{"dws chat group members list-by-ids --id <openConversationId> --users openDingTalkId1,openDingTalkId2"},
},
Parameters: []contract.ParamDecl{
{Name: "id", Property: "openConversationId", Required: boolPtr(true)},
{Name: "users", Property: "memberOpenDingTalkIds", Required: boolPtr(true), InterfaceType: "array"},
},
},
})
// ── group notice: 群公告管理 ────────────────────────────────
chatGroupNoticeCmd := &cobra.Command{Use: "notice", Short: "群公告管理", RunE: groupRunE}
chatGroupNoticeCreateCmd := &cobra.Command{
Use: "create",
Short: "发布群公告",
Long: `在指定群聊中发布群公告,正文为 Markdown 格式。
支持标题、加粗、斜体、删除线、行内代码、链接、代码块、有序/无序/任务列表、表格、引用、分割线、图片、段落、换行。
定时发布:传 --run-at 指定执行时间点,ISO-8601 格式(建议带时区偏移,不带时按北京时区处理)。`,
Example: ` dws chat group notice create --group <openConversationId> --content "今晚 22 点系统维护,请提前保存工作内容"
dws chat group notice create --group <openConversationId> --content "# 重要通知\n请大家查收" --sticky --send-ding
dws chat group notice create --group <openConversationId> --content "明早九点例会" --run-at "2026-07-03T09:00:00+08:00"
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "content"); err != nil {
return err
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"content": mustGetFlag(cmd, "content"),
}
if v, _ := cmd.Flags().GetBool("sticky"); v {
toolArgs["sticky"] = true
}
if v, _ := cmd.Flags().GetBool("send-ding"); v {
toolArgs["sendDing"] = true
}
if v, _ := cmd.Flags().GetString("run-at"); v != "" {
toolArgs["scheduled"] = true
toolArgs["runAtText"] = v
}
return callMCPToolOnServer("im", "create_group_notice", toolArgs)
},
}
chatGroupNoticeCreateCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupNoticeCreateCmd.MarkFlagRequired("group")
chatGroupNoticeCreateCmd.Flags().String("content", "", "公告正文,Markdown 格式 (必填)")
_ = chatGroupNoticeCreateCmd.MarkFlagRequired("content")
chatGroupNoticeCreateCmd.Flags().Bool("sticky", false, "是否吊顶置顶(默认 false)")
chatGroupNoticeCreateCmd.Flags().Bool("send-ding", false, "是否发 DING 提醒(默认 false)")
chatGroupNoticeCreateCmd.Flags().String("run-at", "", "定时发布时间 ISO-8601(如 2026-07-03T09:00:00+08:00,传入则定时发布)")
DeclareLeafMetadata(chatGroupNoticeCreateCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "create_group_notice",
CanonicalPath: "chat." + "create_group_notice",
CLIPath: "chat group notice create",
PrimaryCLIPath: "chat group notice create",
},
Description: "在指定群聊中发布群公告",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "create_group_notice"},
},
Selection: contract.SelectionSpec{
AgentSummary: "在指定群聊中发布群公告",
UseWhen: []string{"需要在群聊里发布 Markdown 群公告,可选吊顶、DING 或定时发布时"},
AvoidWhen: []string{"修改已有群公告时使用 chat group notice edit"},
Examples: []string{"dws chat group notice create --group <openConversationId> --content \"今晚维护\""},
},
Parameters: []contract.ParamDecl{
{Name: "content", Property: "content", Required: boolPtr(true)},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "run-at", Property: "runAtText", Required: boolPtr(false)},
{Name: "send-ding", Property: "sendDing", Required: boolPtr(false)},
{Name: "sticky", Property: "sticky", Required: boolPtr(false)},
},
},
})
chatGroupNoticeEditCmd := &cobra.Command{
Use: "edit",
Short: "修改群公告",
Long: `修改指定群聊中的群公告,正文为 Markdown 格式,会整体替换原公告内容。`,
Example: ` dws chat group notice edit --group <openConversationId> --notice-id <dataId> --content "更新后的公告内容"
dws chat group notice edit --group <openConversationId> --notice-id <dataId> --content "更新后的公告内容" --sticky --send-ding
# 查询公告 ID: dws chat group notice list --group <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "notice-id", "content"); err != nil {
return err
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"dataId": mustGetFlag(cmd, "notice-id"),
"content": mustGetFlag(cmd, "content"),
}
if v, _ := cmd.Flags().GetBool("sticky"); v {
toolArgs["sticky"] = true
}
if v, _ := cmd.Flags().GetBool("send-ding"); v {
toolArgs["sendDing"] = true
}
return callMCPToolOnServer("im", "edit_group_notice", toolArgs)
},
}
chatGroupNoticeEditCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupNoticeEditCmd.MarkFlagRequired("group")
chatGroupNoticeEditCmd.Flags().String("notice-id", "", "群公告 dataId (必填)")
_ = chatGroupNoticeEditCmd.MarkFlagRequired("notice-id")
chatGroupNoticeEditCmd.Flags().String("content", "", "公告新正文,Markdown 格式 (必填)")
_ = chatGroupNoticeEditCmd.MarkFlagRequired("content")
chatGroupNoticeEditCmd.Flags().Bool("sticky", false, "是否吊顶置顶(不传按 false 处理)")
chatGroupNoticeEditCmd.Flags().Bool("send-ding", false, "是否发 DING 提醒(默认 false)")
DeclareLeafMetadata(chatGroupNoticeEditCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "edit_group_notice",
CanonicalPath: "chat." + "edit_group_notice",
CLIPath: "chat group notice edit",
PrimaryCLIPath: "chat group notice edit",
},
Description: "修改指定群聊中的群公告",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "edit_group_notice"},
},
Selection: contract.SelectionSpec{
AgentSummary: "修改指定群聊中的群公告",
UseWhen: []string{"已有公告 dataId,需要替换群公告正文或调整吊顶/DING 状态时"},
AvoidWhen: []string{"发布新公告时使用 chat group notice create"},
Examples: []string{"dws chat group notice edit --group <openConversationId> --notice-id <dataId> --content \"更新后的公告\""},
},
Parameters: []contract.ParamDecl{
{Name: "content", Property: "content", Required: boolPtr(true)},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "notice-id", Property: "dataId", Required: boolPtr(true)},
{Name: "send-ding", Property: "sendDing", Required: boolPtr(false)},
{Name: "sticky", Property: "sticky", Required: boolPtr(false)},
},
},
})
chatGroupNoticeGetCmd := &cobra.Command{
Use: "get",
Short: "查看群公告详情",
Long: `查看指定群公告的详情,包含正文摘要、吊顶状态、发布者、已读人数、点赞/评论数等信息。`,
Example: ` dws chat group notice get --group <openConversationId> --notice-id <dataId>
# 查询公告 ID: dws chat group notice list --group <openConversationId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "notice-id"); err != nil {
return err
}
return callMCPToolOnServer("im", "get_group_notice", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"dataId": mustGetFlag(cmd, "notice-id"),
})
},
}
chatGroupNoticeGetCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupNoticeGetCmd.MarkFlagRequired("group")
chatGroupNoticeGetCmd.Flags().String("notice-id", "", "群公告 dataId (必填)")
_ = chatGroupNoticeGetCmd.MarkFlagRequired("notice-id")
DeclareLeafMetadata(chatGroupNoticeGetCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "get_group_notice",
CanonicalPath: "chat." + "get_group_notice",
CLIPath: "chat group notice get",
PrimaryCLIPath: "chat group notice get",
},
Description: "查看指定群公告详情",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "get_group_notice"},
},
Selection: contract.SelectionSpec{
AgentSummary: "查看指定群公告详情",
UseWhen: []string{"已有群公告 dataId,需要查看公告详情、状态或互动统计时"},
AvoidWhen: []string{"不知道公告 ID 时先使用 chat group notice list"},
Examples: []string{"dws chat group notice get --group <openConversationId> --notice-id <dataId>"},
},
Parameters: []contract.ParamDecl{
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "notice-id", Property: "dataId", Required: boolPtr(true)},
},
},
})
chatGroupNoticeListCmd := &cobra.Command{
Use: "list",
Short: "查看群公告列表",
Long: `分页查看指定群聊的群公告列表。默认查询已发布公告,传 --scheduled 查询定时公告列表。
支持游标分页,hasMore=true 时用返回的 nextPageCursor 作为下次 --cursor。`,
Example: ` dws chat group notice list --group <openConversationId>
dws chat group notice list --group <openConversationId> --limit 20 --cursor <nextPageCursor>
dws chat group notice list --group <openConversationId> --scheduled
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
}
if v, _ := cmd.Flags().GetInt("limit"); v > 0 {
toolArgs["limit"] = v
}
if v, _ := cmd.Flags().GetString("cursor"); v != "" {
toolArgs["cursor"] = v
}
if v, _ := cmd.Flags().GetBool("scheduled"); v {
toolArgs["scheduled"] = true
}
return callMCPToolOnServer("im", "list_group_notices", toolArgs)
},
}
chatGroupNoticeListCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
_ = chatGroupNoticeListCmd.MarkFlagRequired("group")
chatGroupNoticeListCmd.Flags().Int("limit", 10, "每页返回数量(默认 10,最大 100)")
chatGroupNoticeListCmd.Flags().String("cursor", "", "分页游标(首次不传,翻页传返回的 nextPageCursor)")
chatGroupNoticeListCmd.Flags().Bool("scheduled", false, "是否查询定时公告列表(默认 false,查询已发布公告)")
DeclareLeafMetadata(chatGroupNoticeListCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_group_notices",
CanonicalPath: "chat." + "list_group_notices",
CLIPath: "chat group notice list",
PrimaryCLIPath: "chat group notice list",
},
Description: "分页查看指定群聊的群公告列表",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_group_notices"},
},
Selection: contract.SelectionSpec{
AgentSummary: "分页查看指定群聊的群公告列表",
UseWhen: []string{"需要列出群公告、查找公告 dataId 或翻页查看定时公告时"},
AvoidWhen: []string{"已知 dataId 并只看详情时使用 chat group notice get"},
Examples: []string{"dws chat group notice list --group <openConversationId> --limit 20"},
},
Parameters: []contract.ParamDecl{
{Name: "cursor", Property: "cursor", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "limit", Property: "limit", Required: boolPtr(false)},
{Name: "scheduled", Property: "scheduled", Required: boolPtr(false)},
},
},
})
chatGroupNoticeCmd.AddCommand(chatGroupNoticeCreateCmd, chatGroupNoticeEditCmd, chatGroupNoticeGetCmd, chatGroupNoticeListCmd)
chatGroupShareInviteCmd := &cobra.Command{
Use: "share-invite",
Short: "分享群聊链接到会话",
Long: `将指定群的邀请链接分享到另一个会话或单聊用户。--target 和 --receiver 二选一:--target 指定目标会话,--receiver 指定单聊用户。`,
Example: ` dws chat group share-invite --source <被分享群openConversationId> --target <目标会话openConversationId>
dws chat group share-invite --source <被分享群openConversationId> --receiver <接收者openDingTalkId>
dws chat group share-invite --source <openConversationId> --target <openConversationId> --expires-seconds 86400
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs, err := buildChatGroupShareInviteArgs(cmd)
if err != nil {
return err
}
return callMCPToolOnServer("im", "share_group_invite_url", toolArgs)
},
}
chatGroupShareInviteCmd.Flags().String("source", "", "被分享群的 openConversationId (必填)")
_ = chatGroupShareInviteCmd.MarkFlagRequired("source")
chatGroupShareInviteCmd.Flags().String("target", "", "接收分享消息的会话 openConversationId(与 --receiver 二选一)")
chatGroupShareInviteCmd.Flags().String("receiver", "", "接收分享消息的单聊用户 openDingTalkId(与 --target 二选一)")
chatGroupShareInviteCmd.Flags().Int64("expires-seconds", 0, "链接有效期(秒),0 表示永久有效,不传使用服务端默认值")
chatGroupShareInviteCmd.Flags().String("uuid", "", "消息幂等键(可选)")
chatGroupShareInviteCmd.Flags().BoolP("yes", "y", false, "确认分享群邀请链接")
DeclareLeafMetadata(chatGroupShareInviteCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "user_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "share_group_invite_url",
CanonicalPath: "chat.share_group_invite_url",
CLIPath: "chat group share-invite",
PrimaryCLIPath: "chat group share-invite",
},
Description: "将指定群聊的邀请链接分享到会话或单聊用户",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "share_group_invite_url"},
},
Selection: contract.SelectionSpec{
AgentSummary: "将指定群聊的邀请链接分享到会话或单聊用户",
UseWhen: []string{"用户要求把某个群的邀请链接发送给另一个会话或指定单聊用户时"},
AvoidWhen: []string{"只是获取群邀请链接时使用 chat group invite-url"},
Examples: []string{"dws chat group share-invite --source <openConversationId> --target <targetOpenConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "expires-seconds", Property: "expiresSeconds", Required: boolPtr(false), InterfaceType: "integer"},
{Name: "receiver", Property: "receiverOpenDingTalkId", Required: boolPtr(false)},
{Name: "source", Property: "sourceOpenConversationId", Required: boolPtr(true)},
{Name: "target", Property: "targetOpenConversationId", Required: boolPtr(false)},
{Name: "uuid", Property: "uuid", Required: boolPtr(false)},
},
},
})
chatGroupUpgradeToExternalCmd := &cobra.Command{
Use: "upgrade-to-external",
Short: "[危险] 将普通群升级为外部群",
Long: `[危险] 将已有普通群升级为外部群。适用于邀请外部联系人、开展跨组织协作,或保留原群会话并转换群类型的场景。
本命令升级已有普通群;新建外部群请使用 chat group create --type EXTERNAL。
该操作不可逆,仅群主可执行。正式执行必须通过 --yes 显式确认,可先使用 --dry-run 预览。`,
Example: ` dws chat group upgrade-to-external --group <openConversationId> --dry-run
dws chat group upgrade-to-external --group <openConversationId> --extension '{"source":"dws"}' --dry-run
# 查询群 ID: dws chat search --query "群名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group"); err != nil {
return err
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
}
if rawExtension := mustGetFlag(cmd, "extension"); rawExtension != "" {
var rawValues map[string]any
if err := json.Unmarshal([]byte(rawExtension), &rawValues); err != nil {
return fmt.Errorf("--extension must be a JSON object with string values: %w", err)
}
if rawValues == nil {
return fmt.Errorf("--extension must be a JSON object with string values")
}
extension := make(map[string]string, len(rawValues))
for key, value := range rawValues {
stringValue, ok := value.(string)
if !ok {
return fmt.Errorf("--extension value for %q must be a string, got %T", key, value)
}
extension[key] = stringValue
}
toolArgs["extension"] = extension
}
return callMCPToolOnServer("im", "upgrade_group_to_external", toolArgs)
},
}
DeclareLeafMetadata(chatGroupUpgradeToExternalCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "destructive", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "upgrade_group_to_external",
CanonicalPath: "chat.upgrade_group_to_external",
CLIPath: "chat group upgrade-to-external",
PrimaryCLIPath: "chat group upgrade-to-external",
},
Description: "不可逆地把已有普通群升级为外部群",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: the executable CLI validates an optional string map and calls im/upgrade_group_to_external, which is absent from the pinned MCP metadata snapshot.",
},
Selection: contract.SelectionSpec{
AgentSummary: "不可逆地把已有普通群升级为外部群",
UseWhen: []string{"群主明确要求保留现有会话并升级为可跨组织协作的外部群,且已确认不可逆影响"},
AvoidWhen: []string{"新建外部群应使用 chat group create --type EXTERNAL;未确认群主身份和不可逆影响时不要执行"},
Examples: []string{"dws chat group upgrade-to-external --group <openConversationId>"},
},
Parameters: []contract.ParamDecl{
{Name: "extension", Property: "extension", Required: boolPtr(false), InterfaceType: "object"},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
},
},
})
chatGroupUpgradeToExternalCmd.Flags().String("group", "", "待升级普通群的 openConversationId (必填)")
_ = chatGroupUpgradeToExternalCmd.MarkFlagRequired("group")
chatGroupUpgradeToExternalCmd.Flags().String("extension", "", `预留扩展字段 JSON 对象 (可选),如 '{"source":"dws"}'`)
chatCategoryCreateSmartCmd := &cobra.Command{
Use: "create-smart",
Short: "创建智能会话分组",
Long: `创建智能会话分组,可指定群名称关键词和群内成员 openDingTalkId 作为匹配规则。`,
Example: ` dws chat category create-smart --name "工作群"
dws chat category create-smart --name "项目组" --keywords "项目,开发"
dws chat category create-smart --name "团队群" --members openDingTalkId1,openDingTalkId2
dws chat category create-smart --name "重点群" --keywords "重点" --members openDingTalkId1`,
RunE: func(cmd *cobra.Command, args []string) error {
name := strings.TrimSpace(mustGetFlag(cmd, "name"))
if name == "" {
return apperrors.NewValidation("--name must not be blank")
}
toolArgs := map[string]any{
"categoryName": name,
}
if cmd.Flags().Changed("keywords") {
values := parseCSVValues(mustGetFlag(cmd, "keywords"))
if len(values) == 0 {
return apperrors.NewValidation("--keywords must contain at least one non-empty value")
}
toolArgs["groupNameKeywords"] = values
}
if cmd.Flags().Changed("members") {
values := parseCSVValues(mustGetFlag(cmd, "members"))
if len(values) == 0 {
return apperrors.NewValidation("--members must contain at least one non-empty value")
}
toolArgs["memberOpenDingTalkIds"] = values
}
return callMCPToolOnServer("im", "create_smart_conv_category", toolArgs)
},
}
DeclareLeafMetadata(chatCategoryCreateSmartCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "create_smart_conv_category",
CanonicalPath: "chat.create_smart_conv_category",
CLIPath: "chat category create-smart",
PrimaryCLIPath: "chat category create-smart",
},
Description: "按名称、成员或关键词创建智能会话分组",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
},
Selection: contract.SelectionSpec{
AgentSummary: "按名称、成员或关键词创建智能会话分组",
UseWhen: []string{"需要自动归集符合条件的会话时"},
AvoidWhen: []string{"只需查看现有分组或手工管理会话时不要使用"},
Examples: []string{"dws chat category create-smart --name \"项目群\" --keywords \"项目,交付\""},
},
Parameters: []contract.ParamDecl{
{Name: "keywords", Property: "groupNameKeywords", Required: boolPtr(false), InterfaceType: "array", Description: "群名称关键词列表,逗号分隔(可选)"},
{Name: "members", Property: "memberOpenDingTalkIds", Required: boolPtr(false), InterfaceType: "array", Description: "群内成员 openDingTalkId 列表,逗号分隔(可选)"},
{Name: "name", Property: "categoryName", Required: boolPtr(true), Description: "分组名称 (必填)"},
},
},
})
chatCategoryCreateSmartCmd.Flags().String("name", "", "分组名称 (必填)")
_ = chatCategoryCreateSmartCmd.MarkFlagRequired("name")
chatCategoryCreateSmartCmd.Flags().String("keywords", "", "群名称关键词列表,逗号分隔(可选)")
chatCategoryCreateSmartCmd.Flags().String("members", "", "群内成员 openDingTalkId 列表,逗号分隔(可选)")
chatMessageListEmotionRepliesCmd := &cobra.Command{
Use: "list-emotion-replies",
Short: "批量拉取消息的表情回复和文字回复",
Example: ` dws chat message list-emotion-replies --msg-ids msgId1,msgId2,msgId3
# 消息 ID 可通过 dws chat message list 获取`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "msg-ids"); err != nil {
return err
}
msgIds := parseCSVValues(mustGetFlag(cmd, "msg-ids"))
return callMCPToolOnServer("im", "list_message_emotion_replies", map[string]any{
"openMessageIds": msgIds,
})
},
}
chatMessageListEmotionRepliesCmd.Flags().String("msg-ids", "", "消息 ID 列表,逗号分隔 (必填)")
_ = chatMessageListEmotionRepliesCmd.MarkFlagRequired("msg-ids")
DeclareLeafMetadata(chatMessageListEmotionRepliesCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "list_message_emotion_replies",
CanonicalPath: "chat." + "list_message_emotion_replies",
CLIPath: "chat message list-emotion-replies",
PrimaryCLIPath: "chat message list-emotion-replies",
},
Description: "批量拉取消息的表情回复和文字回复",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "list_message_emotion_replies"},
},
Selection: contract.SelectionSpec{
AgentSummary: "批量拉取消息的表情回复和文字回复",
UseWhen: []string{"已有一批消息 ID,需要查看对应 emoji reaction 或文字表情回复时"},
AvoidWhen: []string{"需要读取消息正文时使用 chat message list-by-ids 或 message list"},
Examples: []string{"dws chat message list-emotion-replies --msg-ids msgId1,msgId2"},
},
Parameters: []contract.ParamDecl{
{Name: "msg-ids", Property: "openMessageIds", Required: boolPtr(true), InterfaceType: "array"},
},
},
})
supportedTranslateLanguages := map[string]bool{
"en_US": true, "zh_CN": true, "zh_TW": true, "zh_HK": true,
"ja_JP": true, "ko_KR": true, "vi_VN": true, "th_TH": true,
"id_ID": true, "ms_MY": true, "es_419": true, "fr_FR": true,
"pt_BR": true, "tr_TR": true, "ru_RU": true, "de_DE": true,
"hi_IN": true, "hu_HU": true, "pl_PL": true, "sv_SE": true,
"fi_FI": true, "cs_CZ": true, "ar_SA": true, "tl_PH": true,
"he_IL": true, "nl_NL": true, "lo_LA": true, "it_IT": true,
}
chatTextCmd := &cobra.Command{Use: "text", Short: "文本内容处理", RunE: groupRunE}
chatTextTranslateCmd := &cobra.Command{
Use: "translate",
Short: "翻译文本内容",
Long: `将指定文本翻译成目标语言。
支持的目标语言代码: en_US, zh_CN, zh_TW, zh_HK, ja_JP, ko_KR, vi_VN, th_TH,
id_ID, ms_MY, es_419, fr_FR, pt_BR, tr_TR, ru_RU, de_DE, hi_IN, hu_HU,
pl_PL, sv_SE, fi_FI, cs_CZ, ar_SA, tl_PH, he_IL, nl_NL, lo_LA, it_IT`,
Example: ` dws chat text translate --query "你好世界" --to en_US
dws chat text translate --query "Hello World" --to zh_CN
dws chat text translate --query "Bonjour" --to ja_JP`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "query", "to"); err != nil {
return err
}
toLang := mustGetFlag(cmd, "to")
if !supportedTranslateLanguages[toLang] {
return fmt.Errorf("unsupported target language: %s", toLang)
}
return callMCPToolOnServer("im", "translate", map[string]any{
"query": mustGetFlag(cmd, "query"),
"to": toLang,
})
},
}
chatTextTranslateCmd.Flags().String("query", "", "待翻译的文本内容 (必填)")
_ = chatTextTranslateCmd.MarkFlagRequired("query")
chatTextTranslateCmd.Flags().String("to", "en_US", "目标语言代码 (必填,默认 en_US)")
_ = chatTextTranslateCmd.MarkFlagRequired("to")
DeclareLeafMetadata(chatTextTranslateCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "translate",
CanonicalPath: "chat." + "translate",
CLIPath: "chat text translate",
PrimaryCLIPath: "chat text translate",
},
Description: "将指定文本翻译成目标语言",
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
Ref: &contract.InterfaceRefSpec{ProductID: "im", RPCName: "translate"},
},
Selection: contract.SelectionSpec{
AgentSummary: "将指定文本翻译成目标语言",
UseWhen: []string{"需要把一段文本翻译为指定目标语言代码时"},
AvoidWhen: []string{"需要发送翻译结果到聊天时先翻译再使用消息发送命令"},
Examples: []string{"dws chat text translate --query \"你好世界\" --to en_US"},
},
Parameters: []contract.ParamDecl{
{Name: "query", Property: "query", Required: boolPtr(true)},
{Name: "to", Property: "to", Required: boolPtr(true)},
},
},
})
chatTextCmd.AddCommand(chatTextTranslateCmd)
chatGroupCmd.AddCommand(chatGroupBotsCmd, chatGroupDismissCmd, chatGroupSetHistoryCmd, chatGroupListMyGroupsCmd, chatGroupUpdateNickCmd, chatGroupUpdateAliasCmd, chatGroupListAllCmd, chatGroupListJoinValidationsCmd, chatGroupAuditJoinValidationCmd, chatGroupNoticeCmd, chatGroupShareInviteCmd, chatGroupUpgradeToExternalCmd)
// ── chat group user-settings ──
chatGroupUserSettingsCmd := &cobra.Command{
Use: "user-settings",
Short: "批量查询或更新当前用户的群会话设置",
RunE: groupRunE,
}
chatGroupUserSettingsQueryCmd := &cobra.Command{
Use: "query",
Short: "批量查询当前用户的群会话设置",
Example: ` dws chat group user-settings query --groups cid1,cid2`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "groups"); err != nil {
return err
}
convIds := parseCSVValues(mustGetFlag(cmd, "groups"))
if len(convIds) == 0 {
return fmt.Errorf("--groups must not be empty")
}
if len(convIds) > 100 {
return fmt.Errorf("--groups batch size %d exceeds limit 100", len(convIds))
}
return callMCPToolOnServer("im", "batch_query_group_chat_settings", map[string]any{
"openConversationIds": convIds,
})
},
}
DeclareLeafMetadata(chatGroupUserSettingsQueryCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "batch_query_group_chat_settings",
CanonicalPath: "chat.batch_query_group_chat_settings",
CLIPath: "chat group user-settings query",
PrimaryCLIPath: "chat group user-settings query",
},
Description: "批量查询当前用户自己的群会话设置(置顶/免打扰/群昵称/群备注)",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
},
Selection: contract.SelectionSpec{
AgentSummary: "批量查询当前用户自己的群会话设置(置顶/免打扰/群昵称/群备注)",
UseWhen: []string{"用户说 看下这些群我的置顶和免打扰设置"},
AvoidWhen: []string{"管理员级群功能开关用 chat group update-settings"},
Examples: []string{"dws chat group user-settings query --groups cid1,cid2 --format json"},
},
},
})
chatGroupUserSettingsQueryCmd.Flags().String("groups", "", "群会话 openConversationId 列表,逗号分隔,最多 100 个 (必填)")
chatGroupUserSettingsSetCmd := &cobra.Command{
Use: "set",
Short: "批量更新当前用户的群会话设置",
Example: ` dws chat group user-settings set --items '[{"openConversationId":"cid1","top":true,"mute":false}]'`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "items"); err != nil {
return err
}
itemsJSON := mustGetFlag(cmd, "items")
var items []map[string]any
if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil {
return fmt.Errorf("--items JSON parse error: %w", err)
}
if len(items) == 0 {
return fmt.Errorf("--items must not be empty")
}
if len(items) > 100 {
return fmt.Errorf("--items batch size %d exceeds limit 100", len(items))
}
// 每项必须携带非空 openConversationId,缺失时 fail-fast,避免下发无效批量更新。
for i, item := range items {
cid, _ := item["openConversationId"].(string)
if strings.TrimSpace(cid) == "" {
return fmt.Errorf("--items[%d] 缺少非空 openConversationId", i)
}
}
return callMCPToolOnServer("im", "batch_update_group_chat_settings", map[string]any{
"items": items,
})
},
}
DeclareLeafMetadata(chatGroupUserSettingsSetCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "chat",
Name: "batch_update_group_chat_settings",
CanonicalPath: "chat.batch_update_group_chat_settings",
CLIPath: "chat group user-settings set",
PrimaryCLIPath: "chat group user-settings set",
},
Description: "批量更新当前用户自己的群会话设置(置顶/免打扰/群昵称/群备注)",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
},
Selection: contract.SelectionSpec{
AgentSummary: "批量更新当前用户自己的群会话设置(置顶/免打扰/群昵称/群备注)",
UseWhen: []string{"用户说 把这些群都设为免打扰/置顶"},
AvoidWhen: []string{"单个群昵称优先 chat group update-nick"},
Examples: []string{"dws chat group user-settings set --items '[{\"openConversationId\":\"cid1\",\"top\":true,\"mute\":false}]' --format json"},
},
},
})
chatGroupUserSettingsSetCmd.Flags().String("items", "", `JSON 数组,每项 {"openConversationId":"cid","top":bool,"mute":bool,"groupNick":"...","groupAlias":"..."} (必填)`)
chatGroupUserSettingsCmd.AddCommand(chatGroupUserSettingsQueryCmd, chatGroupUserSettingsSetCmd)
chatGroupCmd.AddCommand(chatGroupUserSettingsCmd)
chatGroupMembersCmd.AddCommand(chatGroupMembersRemoveBotCmd, chatGroupMembersListByIdsCmd)
chatBotCmd.AddCommand(chatBotFindCmd)
chatCategoryCmd.AddCommand(chatCategoryCreateSmartCmd)
chatMessageCmd.AddCommand(chatMessageListDirectCmd, chatMessageSearchCommonCmd, chatMessageCombineForwardCmd, chatMessageForwardTopicCmd, chatMessageSetPinCmd, chatMessageUnsetPinCmd, chatMessageListPinCmd, chatMessageAddFavoriteCmd, chatMessageRemoveFavoriteCmd, chatMessageListFavoritesCmd, chatMessageSetTopMsgCmd, chatMessageUnsetTopMsgCmd, chatMessageListEmotionRepliesCmd)
root.AddCommand(chatChmodCmd, chatDataAuthCmd, chatGroupCmd, chatSearchCmd, chatSearchCommonCmd, chatMessageCmd, chatFileCmd, newChatMediaGroup(), chatBotCmd, chatMessageListTopConversationsCmd, chatConversationInfoCmd, chatCategoryCmd, chatGroupRoleCmd, chatMuteCmd, chatSetTopCmd, chatGroupMuteCmd, chatGroupMuteMemberCmd, chatHideCmd, chatMuteAtAllCmd, chatMuteRedEnvelopeCmd, chatMarkUnreadCmd, chatClearRedPointCmd, chatClearAllRedPointCmd, chatListAllConversationsCmd, chatClearMessagesCmd, chatMarkReadCmd, chatTextCmd, newChatToolbarCommand())
// Keep the v1.0.56 command surface recognizable while directing callers to
// the supported nested commands. The chat root's "im" alias makes these
// compatibility hints available through both chat and im.
root.AddCommand(chatCompatibilityHintSubCmd("send", "use: dws chat message send"))
root.AddCommand(chatCompatibilityHintSubCmd("history", "use: dws chat message list --group <GROUP_OPEN_CONVERSATION_ID>"))
return root
}