5112 lines
231 KiB
Go
5112 lines
231 KiB
Go
package helpers
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
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 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
|
||
}
|
||
|
||
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 = "消息"
|
||
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
title = 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
|
||
}
|
||
|
||
// dingTalkMediaIDPattern 匹配钉钉媒体 ID(上传接口返回),固定以 @ 开头,
|
||
// 后跟 base64 风格字符(如 @lADPfake、@lQDPM4DCjE1Mk_nNBInNBImw...)。
|
||
var dingTalkMediaIDPattern = regexp.MustCompile(`^@[A-Za-z0-9_\-+/=.]{4,}$`)
|
||
|
||
func isOpenDingTalkID(value string) bool {
|
||
value = strings.TrimSpace(value)
|
||
return len(value) > 0 && (value[0] == 'D' || value[0] == 'd')
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
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, err := cmd.Flags().GetBool("all")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
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, err := marshalJSONRaw(map[string]string{"targetOrgId": targetOrgID})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
toolArgs["grantParams"] = string(paramsJSON)
|
||
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, err := marshalJSONRaw(grantParams)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
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
|
||
}
|
||
|
||
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 = fileMD5Hex(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 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, err := marshalJSONRaw(content)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
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 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 ""
|
||
}
|
||
|
||
func printMCPText(text string) error {
|
||
if deps.Caller.Format() == "json" {
|
||
var parsed any
|
||
if err := json.Unmarshal([]byte(text), &parsed); err == nil {
|
||
return deps.Out.PrintJSONUnescaped(parsed)
|
||
}
|
||
}
|
||
deps.Out.PrintRaw(text)
|
||
return nil
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// dws chat — 会话 / 群聊 / 消息
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func newChatCommand() *cobra.Command {
|
||
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
|
||
}
|
||
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 授权的会话标识")
|
||
|
||
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 授权的会话标识")
|
||
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
|
||
},
|
||
}
|
||
|
||
chatSearchCmd := &cobra.Command{
|
||
Use: "search",
|
||
Short: "根据关键词搜索群聊",
|
||
Long: `根据关键词搜索群聊列表。分页参数 --limit(默认 20)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。
|
||
|
||
注意:
|
||
1. query 不要拆分得太细,应使用群名称中连续的核心词作为关键词(如群名"项目冲刺群"应搜"项目冲刺"而非拆成"项目"+"冲刺"分别搜索)。
|
||
2. 当搜索结果返回多个群聊时,应列出候选群让用户确认目标群聊,不要自行假定并直接进行后续操作。`,
|
||
Example: ` dws chat search --query "项目冲刺"
|
||
dws chat search --query "项目冲刺" --limit 20 --cursor 0`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
keyword := flagOrFallback(cmd, "query", "keyword")
|
||
if keyword == "" {
|
||
return fmt.Errorf("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)
|
||
},
|
||
}
|
||
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
|
||
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)
|
||
},
|
||
}
|
||
|
||
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,
|
||
})
|
||
},
|
||
}
|
||
|
||
// ── message 子命令 ────────────────────────────────────────
|
||
|
||
chatMessageCmd := &cobra.Command{
|
||
Use: "message",
|
||
Short: "会话消息管理",
|
||
Long: `管理会话消息,包括拉取、发送、搜索、转发、钉住、收藏和撤回消息。`,
|
||
RunE: groupRunE,
|
||
}
|
||
|
||
chatMessageListCmd := &cobra.Command{
|
||
Use: "list",
|
||
Short: "拉取会话消息内容",
|
||
Long: `拉取指定群聊或单聊的会话消息内容。--group 指定群聊,--user 指定单聊用户(userId),--open-dingtalk-id 指定单聊用户(openDingTalkId),三者互斥。推荐使用 --direction newer/older 控制时间方向:newer 表示从给定时间往现在拉,older 表示从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。如果返回的会话消息中包含 openConvThreadId 字段,说明是话题消息,可以调用 dws chat message list-topic-replies 拉取话题回复消息列表,openConvThreadId 作为 topic-id 参数。`,
|
||
Example: ` 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
|
||
# 查询群 ID: dws chat search --query "群名"
|
||
# 查询 userId: dws contact user search --query "姓名"`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "time"); err != nil {
|
||
return err
|
||
}
|
||
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 userID != "" && isOpenDingTalkID(userID) {
|
||
openDingTalkID = userID
|
||
userID = ""
|
||
}
|
||
forward, err := resolveMessageForward(cmd, true)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
timeVal := mustGetFlag(cmd, "time")
|
||
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 callMCPTool("list_conversation_message_v2", toolArgs)
|
||
}
|
||
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 callMCPTool("list_individual_chat_message", toolArgs)
|
||
},
|
||
}
|
||
|
||
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 userID != "" && isOpenDingTalkID(userID) {
|
||
openDingTalkID = userID
|
||
userID = ""
|
||
}
|
||
timeVal, _ := cmd.Flags().GetString("time")
|
||
defaultForward := true
|
||
if strings.TrimSpace(timeVal) == "" {
|
||
timeVal = time.Now().Format("2006-01-02 15:04:05")
|
||
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)
|
||
},
|
||
}
|
||
|
||
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 作为消息标题。
|
||
|
||
富媒体消息(通过 --msg-type 指定类型):
|
||
image — 发送图片:--msg-type image --media-id(通过 dt_media_upload 上传获得)
|
||
file/audio/video — 发送文件、音频、视频:传本地 --file-path,CLI 会上传后按 file 消息发送`,
|
||
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 "周报提醒" "请大家本周五前提交周报"
|
||
# 发送图片
|
||
dws chat message send --group <openconversation_id> --msg-type image --media-id <mediaId>
|
||
# 发送本地文件/音频/视频(audio/video 是 file 的语义别名)
|
||
dws chat message send --group <openconversation_id> --msg-type file --file-path ./report.pdf
|
||
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
|
||
# 查询群 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, _ := cmd.Flags().GetString("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 userID != "" && isOpenDingTalkID(userID) {
|
||
openDingTalkID = userID
|
||
userID = ""
|
||
}
|
||
// 数字 userId 尝试 lookup 转换为 openDingTalkId,让所有消息类型(文本/媒体)都走 openDingTalkId 路径
|
||
// lookup 失败时降级保留 userID,由下游用 receiverUserId 发送
|
||
if userID != "" {
|
||
resolved, err := resolveOpenDingTalkID(cmd.Context(), userID)
|
||
if err != nil {
|
||
if commandBoolFlag(cmd, "debug") || commandBoolFlag(cmd, "verbose") {
|
||
fmt.Fprintf(os.Stderr, "[debug] resolveOpenDingTalkID(%q) failed: %v, falling back to receiverUserId\n", 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, err = buildConversationFileContent(dentryId, spaceId, meta)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
} else if err == nil {
|
||
targetArgs, err := buildConversationTargetArgs(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
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, err = buildConversationFileContent(dentryId, spaceId, meta)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
} 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)
|
||
}
|
||
default:
|
||
return fmt.Errorf("unsupported --msg-type: %s (supported: image, file, audio, video)", msgType)
|
||
}
|
||
|
||
params := map[string]any{
|
||
"msgType": serviceMsgType,
|
||
"content": contentJSON,
|
||
"clawType": clawType,
|
||
}
|
||
if groupID != "" {
|
||
params["openConversationId"] = groupID
|
||
} else if openDingTalkID != "" {
|
||
params["receiverOpenDingTalkId"] = openDingTalkID
|
||
} else if userID != "" {
|
||
params["receiverUserId"] = userID
|
||
} else {
|
||
return fmt.Errorf("--group, --user or --open-dingtalk-id is required for media messages")
|
||
}
|
||
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")
|
||
var atOpenIds []string
|
||
if atOpenIdsStr != "" {
|
||
atOpenIds = strings.Split(atOpenIdsStr, ",")
|
||
}
|
||
if atAll && !strings.Contains(text, "<@all>") {
|
||
text = "<@all> " + text
|
||
}
|
||
// 用户身份发消息要求 @ 占位符为 <@openDingTalkId>;模型若写成裸 @id 自动补全,已有 <@id> 不变
|
||
text = normalizeAtPlaceholders(text, atOpenIds, true)
|
||
// 群聊统一走 openDingTalkId @ 人接口。
|
||
contentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
|
||
newParams := map[string]any{
|
||
"openConversationId": groupID,
|
||
"msgType": "markdown",
|
||
"content": string(contentJSON),
|
||
"clawType": clawType,
|
||
}
|
||
if atAll {
|
||
newParams["atAll"] = true
|
||
}
|
||
if len(atOpenIds) > 0 {
|
||
newParams["atOpenDingTalkIds"] = atOpenIds
|
||
}
|
||
if msgUuid != "" {
|
||
newParams["uuid"] = msgUuid
|
||
}
|
||
return callMCPTool("send_personal_message", newParams)
|
||
}
|
||
if userID != "" {
|
||
directContentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
|
||
directMsgParams := map[string]any{
|
||
"receiverUserId": userID,
|
||
"msgType": "markdown",
|
||
"content": string(directContentJSON),
|
||
"clawType": clawType,
|
||
}
|
||
if msgUuid != "" {
|
||
directMsgParams["uuid"] = msgUuid
|
||
}
|
||
return callMCPTool("send_personal_message", directMsgParams)
|
||
}
|
||
// 单聊:统一走 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)
|
||
},
|
||
}
|
||
|
||
// send-by-bot: 群聊传 --group,单聊传 --users/--open-dingtalk-ids;--text 支持 Markdown
|
||
chatMessageSendByBotCmd := &cobra.Command{
|
||
Use: "send-by-bot",
|
||
Short: "机器人发送消息(--group 群聊 / --users 单聊)",
|
||
Long: `群聊:传 --group 指定群;单聊:传 --users 或 --open-dingtalk-ids 指定用户列表,与 --group 只能选其一,不能同时指定。--text 支持 Markdown。
|
||
|
||
⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。`,
|
||
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> --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 {
|
||
if err := validateRequiredFlags(cmd, "robot-code", "title", "text"); err != nil {
|
||
return err
|
||
}
|
||
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")
|
||
}
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
// 机器人发消息要求 @ 占位符为裸 @id;模型若写成 <@id> 会导致 @ 不生效,主动剥离尖括号
|
||
markdown := mustGetFlag(cmd, "text")
|
||
markdown = normalizeAtPlaceholders(markdown, atUserIds, false)
|
||
markdown = normalizeAtPlaceholders(markdown, atOpenDingtalkIds, false)
|
||
markdown = strings.ReplaceAll(markdown, "<@all>", "@all")
|
||
toolArgs := map[string]any{
|
||
"robotCode": mustGetFlag(cmd, "robot-code"),
|
||
"openConversationId": chatID,
|
||
"title": mustGetFlag(cmd, "title"),
|
||
"markdown": markdown,
|
||
}
|
||
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 := map[string]any{
|
||
"robotCode": mustGetFlag(cmd, "robot-code"),
|
||
"title": mustGetFlag(cmd, "title"),
|
||
"markdown": mustGetFlag(cmd, "text"),
|
||
}
|
||
if usersStr != "" {
|
||
var userIds []string
|
||
for _, u := range strings.Split(usersStr, ",") {
|
||
if s := strings.TrimSpace(u); s != "" {
|
||
userIds = append(userIds, s)
|
||
}
|
||
}
|
||
toolArgs["userIds"] = userIds
|
||
}
|
||
if openDingtalkIdsStr != "" {
|
||
var openDingtalkIds []string
|
||
for _, id := range strings.Split(openDingtalkIdsStr, ",") {
|
||
if s := strings.TrimSpace(id); s != "" {
|
||
openDingtalkIds = append(openDingtalkIds, s)
|
||
}
|
||
}
|
||
toolArgs["openDingtalkIds"] = openDingtalkIds
|
||
}
|
||
if isAtAll, _ := cmd.Flags().GetBool("at-all"); isAtAll {
|
||
toolArgs["isAtAll"] = "true"
|
||
}
|
||
return callMCPToolOnServer("bot", "batch_send_robot_msg_to_users", toolArgs)
|
||
},
|
||
}
|
||
|
||
// 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,
|
||
})
|
||
},
|
||
}
|
||
|
||
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
|
||
},
|
||
}
|
||
|
||
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)
|
||
},
|
||
}
|
||
|
||
chatMessageListAllCmd := &cobra.Command{
|
||
Use: "list-all",
|
||
Short: "拉取指定时间范围内当前用户的所有会话消息",
|
||
Long: `分页拉取当前登录用户在指定时间范围内的所有会话消息。--start 和 --end 限定时间范围,--limit 指定每页数量,--cursor 传分页游标(首页传 0)。服务端按 cursor 分页返回,hasMore=true 时用返回的 nextCursor 值继续翻页。`,
|
||
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"`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "start", "end"); err != nil {
|
||
return err
|
||
}
|
||
limit := chatIntFlagOrFallback(cmd, "limit", "size")
|
||
cursor, _ := cmd.Flags().GetString("cursor")
|
||
toolArgs := map[string]any{
|
||
"startTime": mustGetFlag(cmd, "start"),
|
||
"endTime": mustGetFlag(cmd, "end"),
|
||
"limit": limit,
|
||
"cursor": cursor,
|
||
}
|
||
return callMCPTool("search_messages_by_time_range", toolArgs)
|
||
},
|
||
}
|
||
|
||
chatMessageListBySenderCmd := &cobra.Command{
|
||
Use: "list-by-sender",
|
||
Short: "拉取指定发送者的消息(包含单聊和群聊)",
|
||
Long: `搜索特定人发送给我的消息,返回结果包含单聊和群聊标识。--sender-user-id 指定发送者 userId,--sender-open-dingtalk-id 指定发送者 openDingTalkId,二者互斥。分页参数 --limit(默认 50)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。`,
|
||
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>
|
||
# 查询 userId: dws contact user search --query "姓名"
|
||
# 查询 openDingTalkId: dws contact user search --query "姓名"`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "start"); err != nil {
|
||
return err
|
||
}
|
||
senderUserID := flagOrFallback(cmd, "sender-user-id", "sender")
|
||
senderOpenDingTalkID, _ := cmd.Flags().GetString("sender-open-dingtalk-id")
|
||
if senderUserID != "" && senderOpenDingTalkID != "" {
|
||
return fmt.Errorf("--sender-user-id and --sender-open-dingtalk-id are mutually exclusive, specify exactly one")
|
||
}
|
||
if senderUserID == "" && senderOpenDingTalkID == "" {
|
||
return fmt.Errorf("--sender-user-id or --sender-open-dingtalk-id is required")
|
||
}
|
||
startMs, err := parseISOTimeToMillis("start", mustGetFlag(cmd, "start"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
endRaw, _ := cmd.Flags().GetString("end")
|
||
if strings.TrimSpace(endRaw) == "" {
|
||
endRaw = time.Now().Format(time.RFC3339)
|
||
}
|
||
endMs, err := parseISOTimeToMillis("end", endRaw)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := validateTimeRange(startMs, endMs); err != nil {
|
||
return err
|
||
}
|
||
limit := chatIntFlagOrFallback(cmd, "limit", "size")
|
||
cursor, _ := cmd.Flags().GetString("cursor")
|
||
toolArgs := map[string]any{
|
||
"startTime": startMs,
|
||
"endTime": endMs,
|
||
"limit": limit,
|
||
"cursor": cursor,
|
||
}
|
||
if senderUserID != "" {
|
||
toolArgs["senderUserId"] = senderUserID
|
||
} else {
|
||
toolArgs["senderOpenDingTalkId"] = senderOpenDingTalkID
|
||
}
|
||
return callMCPTool("search_messages_by_sender", toolArgs)
|
||
},
|
||
}
|
||
|
||
chatMessageListMentionsCmd := &cobra.Command{
|
||
Use: "list-mentions",
|
||
Short: "拉取 @我 的消息",
|
||
Long: `搜索时间范围内 @我 的消息,可选指定群聊。返回结果包含单聊和群聊标识。分页参数 --limit(默认 50)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。`,
|
||
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>
|
||
# 查询群 ID: dws chat search --query "群名"`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "start", "end"); err != nil {
|
||
return err
|
||
}
|
||
startMs, err := parseISOTimeToMillis("start", mustGetFlag(cmd, "start"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
endMs, err := parseISOTimeToMillis("end", mustGetFlag(cmd, "end"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := validateTimeRange(startMs, endMs); err != nil {
|
||
return err
|
||
}
|
||
limit := chatIntFlagOrFallback(cmd, "limit", "size")
|
||
cursor, _ := cmd.Flags().GetString("cursor")
|
||
toolArgs := map[string]any{
|
||
"startTime": startMs,
|
||
"endTime": endMs,
|
||
"limit": limit,
|
||
"cursor": cursor,
|
||
}
|
||
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
|
||
if groupID != "" {
|
||
toolArgs["openConversationId"] = groupID
|
||
}
|
||
return callMCPTool("search_at_me_message", toolArgs)
|
||
},
|
||
}
|
||
|
||
chatMessageListFocusedCmd := &cobra.Command{
|
||
Use: "list-focused",
|
||
Short: "拉取特别关注人的消息",
|
||
Long: `拉取当前用户特别关注人的消息。分页参数 --limit 指定每页数量,--cursor 传分页游标(首次不传或传 0)。返回结果中 hasMore=true 时用 nextCursor 作为下次 --cursor 继续翻页。`,
|
||
Example: ` dws chat message list-focused --limit 50
|
||
dws chat message list-focused --limit 20 --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
|
||
}
|
||
return callMCPTool("list_special_focus_messages", toolArgs)
|
||
},
|
||
}
|
||
|
||
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)
|
||
},
|
||
}
|
||
|
||
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)
|
||
},
|
||
}
|
||
|
||
chatMessageSearchCmd := &cobra.Command{
|
||
Use: "search",
|
||
Short: "按关键词搜索消息",
|
||
Long: `在当前用户的会话中按关键词搜索消息。--query 指定搜索关键词(必填)。可选 --group 限定搜索某个会话,不传则搜索所有会话。时间参数 --start/--end(ISO-8601)限定搜索时间范围。分页参数 --limit(默认 100)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。`,
|
||
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>
|
||
# 查询群 ID: dws chat search --query "群名"`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlagWithAliases(cmd, "query", "keyword"); err != nil {
|
||
return err
|
||
}
|
||
if err := validateRequiredFlags(cmd, "start", "end"); err != nil {
|
||
return err
|
||
}
|
||
startMs, err := parseISOTimeToMillis("start", mustGetFlag(cmd, "start"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
endMs, err := parseISOTimeToMillis("end", mustGetFlag(cmd, "end"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := validateTimeRange(startMs, endMs); err != nil {
|
||
return err
|
||
}
|
||
limit := chatIntFlagOrFallback(cmd, "limit", "size")
|
||
cursor, _ := cmd.Flags().GetString("cursor")
|
||
toolArgs := map[string]any{
|
||
"keyword": flagOrFallback(cmd, "query", "keyword"),
|
||
"startTime": startMs,
|
||
"endTime": endMs,
|
||
"limit": limit,
|
||
"cursor": cursor,
|
||
}
|
||
groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat")
|
||
if groupID != "" {
|
||
toolArgs["openConversationId"] = groupID
|
||
}
|
||
return callMCPTool("search_messages_by_keyword", toolArgs)
|
||
},
|
||
}
|
||
|
||
// ── search-advanced:多维度搜索消息 ──────
|
||
|
||
chatMessageSearchAdvancedCmd := &cobra.Command{
|
||
Use: "search-advanced",
|
||
Short: "多维度搜索消息",
|
||
Long: `支持按关键词、发送者、@我、@指定人、指定会话、时间范围等多维度搜索消息。发送者 userId 使用 --user/--users;发送者或 @ 人的 openDingTalkId 使用 --sender-ids/--at-ids。所有参数均为可选,至少指定一个搜索条件。`,
|
||
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"
|
||
# 查询群 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 {
|
||
toolArgs := map[string]any{}
|
||
|
||
// The CLI primary is --query; the IM MCP field is still named "keyword".
|
||
if v := flagOrFallback(cmd, "query", "keyword"); v != "" {
|
||
toolArgs["keyword"] = v
|
||
}
|
||
|
||
// --user/--userId are the preferred userId inputs for sender filtering.
|
||
if v := flagOrFallback(cmd, "users", "user", "userId"); v != "" {
|
||
appendChatIDArgs(toolArgs, parseCSVValues(v), "senderUserIds", "senderOpenDingTakIds")
|
||
}
|
||
|
||
// sender-ids -> senderOpenDingTakIds / senderUserIds(注意:MCP 入参字段名缺少字母 l)
|
||
if v := flagOrFallback(cmd, "sender-ids", "senders", "sender"); v != "" {
|
||
ids := parseCSVValues(v)
|
||
if len(ids) > 0 {
|
||
appendChatIDArgs(toolArgs, ids, "senderUserIds", "senderOpenDingTakIds")
|
||
}
|
||
}
|
||
|
||
// at-me
|
||
if v, _ := cmd.Flags().GetBool("at-me"); v {
|
||
toolArgs["atMe"] = true
|
||
}
|
||
|
||
// at-ids -> atOpenDingTakIds / atUserIds(注意:MCP 入参字段名缺少字母 l)
|
||
if v, _ := cmd.Flags().GetString("at-ids"); v != "" {
|
||
ids := parseCSVValues(v)
|
||
if len(ids) > 0 {
|
||
appendChatIDArgs(toolArgs, ids, "atUserIds", "atOpenDingTakIds")
|
||
}
|
||
}
|
||
|
||
// conversation-ids / groups / group -> openConversationIds
|
||
convIds := ""
|
||
if v, _ := cmd.Flags().GetString("conversation-ids"); v != "" {
|
||
convIds = v
|
||
} else if v, _ := cmd.Flags().GetString("groups"); v != "" {
|
||
convIds = v
|
||
} else if v, _ := cmd.Flags().GetString("group"); v != "" {
|
||
convIds = v
|
||
}
|
||
if convIds != "" {
|
||
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
|
||
}
|
||
}
|
||
|
||
// start -> startTime (ISO-8601 to milliseconds)
|
||
if v, _ := cmd.Flags().GetString("start"); v != "" {
|
||
ms, err := parseISOTimeToMillis("start", v)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
toolArgs["startTime"] = ms
|
||
}
|
||
|
||
// end -> endTime (ISO-8601 to milliseconds)
|
||
if v, _ := cmd.Flags().GetString("end"); v != "" {
|
||
ms, err := parseISOTimeToMillis("end", v)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
toolArgs["endTime"] = ms
|
||
}
|
||
|
||
// cursor
|
||
if v, _ := cmd.Flags().GetString("cursor"); v != "" {
|
||
toolArgs["cursor"] = v
|
||
}
|
||
|
||
// limit
|
||
if v := chatIntFlagOrFallback(cmd, "limit", "size"); v > 0 {
|
||
toolArgs["limit"] = v
|
||
}
|
||
|
||
return callMCPToolOnServer("im", "search_messages", toolArgs)
|
||
},
|
||
}
|
||
|
||
// ── query-send-status:查询消息发送状态(走 IM MCP)──────
|
||
|
||
chatMessageQuerySendStatusCmd := &cobra.Command{
|
||
Use: "query-send-status",
|
||
Short: "查询消息发送状态",
|
||
Long: `查询以当前用户身份发送的消息的发送状态。需要传入发送消息时返回的 openTaskId。`,
|
||
Example: ` dws chat message query-send-status --open-task-id <openTaskId>
|
||
# openTaskId 由 dws chat message send 返回`,
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
|
||
// ── recall:撤回用户发送的消息(走 IM MCP)──────
|
||
|
||
chatMessageRecallCmd := &cobra.Command{
|
||
Use: "recall",
|
||
Short: "撤回用户发送的消息",
|
||
Long: `撤回当前用户发送的消息。需要指定会话 ID 和消息 ID。`,
|
||
Example: ` dws chat message recall --conversation-id <openConversationId> --msg-id <openMessageId>
|
||
# 查询会话 ID: dws chat search --query "群名"
|
||
# 消息 ID 可通过 dws chat message list 获取`,
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
|
||
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)
|
||
},
|
||
}
|
||
|
||
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,
|
||
}
|
||
|
||
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)
|
||
},
|
||
}
|
||
|
||
// 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, "开启话题模式,将创建话题圈")
|
||
|
||
chatSearchCmd.Flags().String("query", "", "搜索关键词 (必填)")
|
||
chatSearchCmd.Flags().String("keyword", "", "--query 的别名")
|
||
_ = chatSearchCmd.Flags().MarkHidden("keyword")
|
||
chatSearchCmd.Flags().Int("limit", 20, "每页返回数量(默认 20)")
|
||
chatSearchCmd.Flags().Int("size", 0, "--limit 的旧版别名")
|
||
_ = chatSearchCmd.Flags().MarkHidden("size")
|
||
chatSearchCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\",翻页传 nextCursor)")
|
||
chatSearchCmd.Flags().Bool("exclude-muted", false, "是否排除已设置免打扰的群聊(默认 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(hintSubCmd("search", "use: dws chat search --query <关键词>"))
|
||
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=从给定时间往以前拉(推荐)")
|
||
chatMessageListCmd.Flags().String("forward", "true", "true 等价 --direction newer,false 等价 --direction older")
|
||
_ = 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(通过 dt_media_upload 上传后用 extract_media_id.py 提取,仅 msgType=image)")
|
||
chatMessageSendCmd.Flags().String("msg-type", "", "富媒体消息类型: image/file/audio/video(audio/video 是 file 别名;纯文本/Markdown 无需指定,直接传内容即可)")
|
||
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 时可直接上传发送)")
|
||
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)")
|
||
chatMessageSendCmd.Flags().String("uuid", "", "幂等 UUID,相同 uuid 在 24h 内不会重复发送(可选)")
|
||
|
||
chatMessageSendByBotCmd.Flags().String("robot-code", "", "机器人 Code (必填)")
|
||
_ = chatMessageSendByBotCmd.MarkFlagRequired("robot-code")
|
||
chatMessageSendByBotCmd.Flags().String("group", "", "群聊 openConversationId(群聊时必填)")
|
||
chatMessageSendByBotCmd.Flags().String("users", "", "用户 userId 列表,逗号分隔,最多20个(单聊时必填)")
|
||
chatMessageSendByBotCmd.Flags().String("title", "", "消息标题 (必填)")
|
||
_ = chatMessageSendByBotCmd.MarkFlagRequired("title")
|
||
chatMessageSendByBotCmd.Flags().String("text", "", "消息内容 Markdown (必填)")
|
||
_ = chatMessageSendByBotCmd.MarkFlagRequired("text")
|
||
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")
|
||
|
||
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 (必填)")
|
||
_ = chatMessageListAllCmd.MarkFlagRequired("start")
|
||
chatMessageListAllCmd.Flags().String("end", "", "结束时间,格式: yyyy-MM-dd HH:mm:ss (必填)")
|
||
_ = chatMessageListAllCmd.MarkFlagRequired("end")
|
||
chatMessageListAllCmd.Flags().Int("limit", 50, "每页返回数量 (必填,默认 50)")
|
||
chatMessageListAllCmd.Flags().Int("size", 0, "--limit 的旧版别名")
|
||
_ = chatMessageListAllCmd.Flags().MarkHidden("size")
|
||
chatMessageListAllCmd.Flags().String("cursor", "0", "分页游标(首页传 \"0\",后续从响应中获取)")
|
||
|
||
// 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 格式 (必填)")
|
||
_ = chatMessageListBySenderCmd.MarkFlagRequired("start")
|
||
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)")
|
||
|
||
// list-mentions flags
|
||
chatMessageListMentionsCmd.Flags().String("group", "", "群聊 openconversation_id(可选,不传则查全部)")
|
||
chatMessageListMentionsCmd.Flags().String("start", "", "开始时间,ISO-8601 格式 (必填)")
|
||
_ = chatMessageListMentionsCmd.MarkFlagRequired("start")
|
||
chatMessageListMentionsCmd.Flags().String("end", "", "结束时间,ISO-8601 格式 (必填)")
|
||
_ = chatMessageListMentionsCmd.MarkFlagRequired("end")
|
||
chatMessageListMentionsCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)")
|
||
chatMessageListMentionsCmd.Flags().Int("size", 0, "--limit 的旧版别名")
|
||
_ = chatMessageListMentionsCmd.Flags().MarkHidden("size")
|
||
chatMessageListMentionsCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\",翻页传 nextCursor)")
|
||
|
||
// list-focused flags
|
||
chatMessageListFocusedCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)")
|
||
chatMessageListFocusedCmd.Flags().Int64("cursor", 0, "分页游标(首次不传或传 0,翻页传 nextCursor)")
|
||
|
||
// 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 格式 (必填)")
|
||
_ = chatMessageSearchCmd.MarkFlagRequired("start")
|
||
chatMessageSearchCmd.Flags().String("end", "", "结束时间,ISO-8601 格式 (必填)")
|
||
_ = chatMessageSearchCmd.MarkFlagRequired("end")
|
||
chatMessageSearchCmd.Flags().Int("limit", 100, "每页返回数量(默认 100)")
|
||
chatMessageSearchCmd.Flags().Int("size", 0, "--limit 的旧版别名")
|
||
_ = chatMessageSearchCmd.Flags().MarkHidden("size")
|
||
chatMessageSearchCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\",翻页传 nextCursor)")
|
||
|
||
// 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("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")
|
||
|
||
// 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")
|
||
|
||
// 别名注册: --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 != "" && !isOpenDingTalkID(openDingTalkID) {
|
||
userID = openDingTalkID
|
||
openDingTalkID = ""
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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{})
|
||
},
|
||
}
|
||
|
||
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)
|
||
},
|
||
}
|
||
|
||
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
|
||
}
|
||
return callMCPToolOnServer("im", "create_conv_category", map[string]any{
|
||
"title": mustGetFlag(cmd, "title"),
|
||
})
|
||
},
|
||
}
|
||
|
||
chatCategoryDeleteCmd := &cobra.Command{
|
||
Use: "delete",
|
||
Short: "删除用户自定义会话分组",
|
||
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")
|
||
}
|
||
return callMCPToolOnServer("im", "delete_conv_category", map[string]any{
|
||
"categoryId": categoryId,
|
||
})
|
||
},
|
||
}
|
||
|
||
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
|
||
}
|
||
return callMCPToolOnServer("im", "rename_conv_category", map[string]any{
|
||
"categoryId": categoryId,
|
||
"title": mustGetFlag(cmd, "title"),
|
||
})
|
||
},
|
||
}
|
||
|
||
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,
|
||
})
|
||
},
|
||
}
|
||
|
||
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,
|
||
})
|
||
},
|
||
}
|
||
|
||
// ── 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,
|
||
})
|
||
},
|
||
}
|
||
|
||
// ── 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,
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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 (必填)")
|
||
|
||
// ── 创建文字表情(获取 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)
|
||
},
|
||
}
|
||
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,二者互斥。
|
||
创建时无需传入卡片内容,后续通过 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 --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")
|
||
if groupID == "" && receiver == "" {
|
||
return fmt.Errorf("--group or --receiver is required")
|
||
}
|
||
if groupID != "" && receiver != "" {
|
||
return fmt.Errorf("--group and --receiver are mutually exclusive")
|
||
}
|
||
toolArgs := map[string]any{}
|
||
if groupID != "" {
|
||
toolArgs["openConversationId"] = groupID
|
||
}
|
||
if receiver != "" {
|
||
resolved, err := resolveOpenDingTalkID(cmd.Context(), receiver)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
toolArgs["receiverOpenDingTalkId"] = resolved
|
||
}
|
||
return callMCPToolOnServer("im", "create_and_send_card", toolArgs)
|
||
},
|
||
}
|
||
chatMessageSendCardCmd.Flags().String("group", "", "群聊 openConversationId(群聊时必填,与 --receiver 互斥)")
|
||
chatMessageSendCardCmd.Flags().String("receiver", "", "单聊接收者 openDingTalkId(单聊时必填,与 --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")
|
||
}
|
||
flowStatus, _ := cmd.Flags().GetInt("flow-status")
|
||
return callMCPToolOnServer("im", "update_streaming_card", map[string]any{
|
||
"bizId": mustGetFlag(cmd, "biz-id"),
|
||
"msgContent": mustGetFlag(cmd, "content"),
|
||
"flowStatus": 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: `下载聊天消息中的图片、视频、语音等资源到本地文件。
|
||
|
||
流程:
|
||
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>
|
||
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
|
||
# message-id: 从 dws chat message list 返回的 openMessageId
|
||
# open-conversation-id: 从 dws chat search 获取 openConversationId`,
|
||
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")
|
||
|
||
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
|
||
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 下载文件
|
||
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载资源到 %s ...", outputPath))
|
||
if err := httpGetFile(ctx, resourceURL, dlHeaders, outputPath); err != nil {
|
||
return err
|
||
}
|
||
|
||
deps.Out.PrintInfo(fmt.Sprintf("下载完成: %s", outputPath))
|
||
return nil
|
||
},
|
||
}
|
||
|
||
// download-media flags
|
||
chatMessageDownloadMediaCmd.Flags().String("type", "", "资源类型: mediaId (必填)")
|
||
_ = chatMessageDownloadMediaCmd.MarkFlagRequired("type")
|
||
chatMessageDownloadMediaCmd.Flags().String("resource-id", "", "资源 ID,mediaId 类型时为消息中的 mediaId 值 (必填)")
|
||
_ = 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")
|
||
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, chatMessageCreateTextEmotionCmd, chatMessageSearchAdvancedCmd, chatMessageQuerySendStatusCmd, chatMessageRecallCmd, chatMessageReadStatusCmd, chatMessageSendCardCmd, chatMessageUpdateCardCmd, chatMessageDownloadMediaCmd)
|
||
chatBotCmd.AddCommand(chatBotSearchCmd)
|
||
chatCategoryCmd.AddCommand(chatCategoryListCmd, chatCategoryConvsCmd, chatCategoryCreateCmd, chatCategoryDeleteCmd, chatCategoryRenameCmd, chatCategoryAddConvCmd, chatCategoryRemoveConvCmd)
|
||
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,
|
||
})
|
||
},
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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,
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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"))
|
||
// 客户端预校验:钉钉媒体 ID 由上传接口(如 dt_media_upload)返回,
|
||
// 固定以 @ 开头(形如 @lADP.../@lQDP...)。明显非法的值本地直接报错。
|
||
if !dingTalkMediaIDPattern.MatchString(iconMediaID) {
|
||
return fmt.Errorf("invalid --icon-media-id %q: 钉钉媒体 ID 应以 @ 开头(如 @lADP...)\n hint: 先通过媒体上传命令(dt_media_upload)上传图片,使用返回的 mediaId", iconMediaID)
|
||
}
|
||
return callMCPToolOnServer("im", "update_group_icon", map[string]any{
|
||
"openConversationId": mustGetFlag(cmd, "group"),
|
||
"iconMediaId": iconMediaID,
|
||
})
|
||
},
|
||
}
|
||
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,
|
||
})
|
||
},
|
||
}
|
||
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,以及回复内容。
|
||
|
||
如何获取 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 "收到,马上处理"`,
|
||
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
|
||
}
|
||
replyContent := map[string]string{
|
||
"referenceOpenMessageId": mustGetFlag(cmd, "ref-msg-id"),
|
||
"srcMsgSendOpenDingTalkId": refSender,
|
||
"replyMsgType": "text",
|
||
"content": mustGetFlag(cmd, "text"),
|
||
}
|
||
contentJSON, err := marshalJSONRaw(replyContent)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to marshal reply content: %w", err)
|
||
}
|
||
clawType := ""
|
||
aiTag, _ := cmd.Flags().GetBool("ai-tag")
|
||
if aiTag {
|
||
clawType = edition.ClawType()
|
||
}
|
||
toolArgs := map[string]any{
|
||
"openConversationId": mustGetFlag(cmd, "conversation-id"),
|
||
"msgType": "reply",
|
||
"content": string(contentJSON),
|
||
"clawType": clawType,
|
||
}
|
||
if v, _ := cmd.Flags().GetString("uuid"); v != "" {
|
||
toolArgs["uuid"] = v
|
||
}
|
||
return callMCPTool("send_personal_message", toolArgs)
|
||
},
|
||
}
|
||
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)")
|
||
|
||
// ── 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)
|
||
},
|
||
}
|
||
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,
|
||
})
|
||
},
|
||
}
|
||
chatSetTopCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
|
||
_ = chatSetTopCmd.MarkFlagRequired("conversation-id")
|
||
chatSetTopCmd.Flags().Bool("off", false, "取消置顶(不传则设置置顶)")
|
||
|
||
// ── 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,
|
||
})
|
||
},
|
||
}
|
||
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。
|
||
if len(userIDs) > 0 {
|
||
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)
|
||
},
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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", "", "分组名称 (必填)")
|
||
_ = 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", "", "新的分组名称 (必填)")
|
||
_ = 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")
|
||
|
||
// ── 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,
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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
|
||
}
|
||
return callMCPToolOnServer("bot", "list_group_bots", map[string]any{
|
||
"openConversationId": mustGetFlag(cmd, "group"),
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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: `解散指定群聊。该操作不可逆,需要群主权限。`,
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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,
|
||
})
|
||
},
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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)
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
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。`,
|
||
Example: ` dws chat message list-favorites
|
||
dws chat message list-favorites --size 50
|
||
dws chat message list-favorites --cursor 20 --size 20`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
cursor, _ := cmd.Flags().GetInt64("cursor")
|
||
if cursor < 0 {
|
||
return apperrors.NewValidation("--cursor must be greater than or equal to 0")
|
||
}
|
||
size, _ := cmd.Flags().GetInt("size")
|
||
if size < 1 || size > 100 {
|
||
return apperrors.NewValidation("--size must be between 1 and 100")
|
||
}
|
||
return callMCPToolOnServer("im", "list_message_favorites", map[string]any{
|
||
"cursor": cursor,
|
||
"size": strconv.Itoa(size),
|
||
})
|
||
},
|
||
}
|
||
chatMessageListFavoritesCmd.Flags().Int64("cursor", 0, "数字分页游标(默认 0;翻页时传上次返回的 nextCursor)")
|
||
chatMessageListFavoritesCmd.Flags().Int("size", 20, "一次拉取的收藏数量(默认 20,范围 1-100)")
|
||
|
||
// ── 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 != "" {
|
||
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)
|
||
},
|
||
}
|
||
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)")
|
||
|
||
// ── 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)")
|
||
|
||
// ── 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 <openDingTalkId> --inviter <openDingTalkId> --status AuditApprove
|
||
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --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)
|
||
}
|
||
toolArgs := map[string]any{
|
||
"openConversationId": mustGetFlag(cmd, "group"),
|
||
"applyRecordId": recordID,
|
||
"applicantOpenDingTalkId": mustGetFlag(cmd, "applicant"),
|
||
"inviterOpenDingTalkId": mustGetFlag(cmd, "inviter"),
|
||
"status": mustGetFlag(cmd, "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", "", "申请人 openDingTalkId (必填)")
|
||
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("applicant")
|
||
chatGroupAuditJoinValidationCmd.Flags().String("inviter", "", "邀请人 openDingTalkId (必填)")
|
||
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("inviter")
|
||
chatGroupAuditJoinValidationCmd.Flags().String("description", "", "审批说明(可选)")
|
||
|
||
// ── 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")
|
||
|
||
// ── 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")
|
||
|
||
// ── 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{})
|
||
},
|
||
}
|
||
|
||
// ── 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)")
|
||
|
||
// ── clear-messages: 清空会话聊天记录 ────────────────────────────
|
||
|
||
chatClearMessagesCmd := &cobra.Command{
|
||
Use: "clear-messages",
|
||
Short: "清空当前用户指定会话的聊天记录",
|
||
Long: `清空当前用户在指定会话中的聊天记录。仅清空当前用户视角的消息,不影响其他成员。
|
||
|
||
如何获取 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>")
|
||
}
|
||
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")
|
||
|
||
// ── 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")
|
||
|
||
// ── 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")
|
||
|
||
// ── 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")
|
||
|
||
// group 与 members 的子命令在下方(chatGroupCmd.AddCommand / chatGroupMembersCmd.AddCommand
|
||
// 的完整列表处)统一注册;此处不再重复 AddCommand,否则 --help 会重复列出。
|
||
// ── group update-nick: 设置用户在群内的群昵称 ──────────────
|
||
|
||
chatGroupUpdateNickCmd := &cobra.Command{
|
||
Use: "update-nick",
|
||
Short: "设置用户在群内的群昵称",
|
||
Long: `设置当前用户在指定群聊内的个人群昵称。`,
|
||
Example: ` 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", "nick"); err != nil {
|
||
return err
|
||
}
|
||
return callMCPToolOnServer("im", "update_group_nick", map[string]any{
|
||
"openConversationId": mustGetFlag(cmd, "group"),
|
||
"nick": mustGetFlag(cmd, "nick"),
|
||
})
|
||
},
|
||
}
|
||
chatGroupUpdateNickCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
|
||
_ = chatGroupUpdateNickCmd.MarkFlagRequired("group")
|
||
chatGroupUpdateNickCmd.Flags().String("nick", "", "个人群昵称 (必填)")
|
||
_ = chatGroupUpdateNickCmd.MarkFlagRequired("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")
|
||
|
||
// ── 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")
|
||
|
||
// ── 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, "恢复接收 @所有人通知(不传则关闭通知)")
|
||
|
||
// ── 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, "恢复接收红包通知(不传则关闭通知)")
|
||
|
||
// ── 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")
|
||
|
||
// ── 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,传入则定时发布)")
|
||
|
||
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)")
|
||
|
||
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")
|
||
|
||
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,查询已发布公告)")
|
||
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 {
|
||
if err := validateRequiredFlags(cmd, "source"); err != nil {
|
||
return err
|
||
}
|
||
target, _ := cmd.Flags().GetString("target")
|
||
receiver, _ := cmd.Flags().GetString("receiver")
|
||
if target == "" && receiver == "" {
|
||
return fmt.Errorf("--target or --receiver is required")
|
||
}
|
||
if target != "" && receiver != "" {
|
||
return 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 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", "", "消息幂等键(可选)")
|
||
|
||
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)
|
||
},
|
||
}
|
||
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")
|
||
|
||
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")
|
||
chatTextCmd.AddCommand(chatTextTranslateCmd)
|
||
|
||
chatGroupCmd.AddCommand(chatGroupBotsCmd, chatGroupDismissCmd, chatGroupSetHistoryCmd, chatGroupListMyGroupsCmd, chatGroupUpdateNickCmd, chatGroupUpdateAliasCmd, chatGroupListAllCmd, chatGroupListJoinValidationsCmd, chatGroupAuditJoinValidationCmd, chatGroupNoticeCmd, chatGroupShareInviteCmd)
|
||
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)
|
||
|
||
// hint: dws chat send → dws chat message send
|
||
root.AddCommand(hintSubCmd("send", "use: dws chat message send"))
|
||
// hint: dws chat history → dws chat message list
|
||
root.AddCommand(hintSubCmd("history", "use: dws chat message list --group <GROUP_OPEN_CONVERSATION_ID>"))
|
||
|
||
return root
|
||
}
|