Files

206 lines
9.1 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
// Package ding registers strict declarative shortcuts for DingTalk DING.
package ding
import (
"strconv"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
var List = shortcut.Shortcut{
Service: "ding", Command: "+list", Product: "im",
Description: "查询 DING 消息列表",
Intent: "需要查看 DING 历史、取得稳定 openDingId,或按 ALL、UNREAD、SEND、NEW_COMMENT、DELETED 类型读取一页时使用。",
Risk: shortcut.RiskRead,
Safety: dingReadSafety(),
OutputRollout: output.RolloutUnifiedActive,
Contract: dingContract(
"+list", "查询 DING 消息列表",
"需要查看 DING 历史、取得稳定 openDingId,或按 ALL、UNREAD、SEND、NEW_COMMENT、DELETED 类型读取一页时使用。",
true, dingListResult(), dingPagination(),
[]contract.ParamDecl{{Name: "cursor", Property: "cursor"}, {Name: "type", Property: "type"}},
"dws ding +list --type ALL", "dws ding +list --type DELETED",
),
Flags: []shortcut.Flag{
{Name: "cursor", Type: shortcut.FlagInt, Desc: "分页游标,首次不传或传 0"},
{Name: "type", Type: shortcut.FlagString, Default: "ALL", Desc: "DING 类型:ALL、UNREAD、SEND、NEW_COMMENT 或 DELETED"},
},
Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"cursor"}, Description: "--cursor 不能小于 0,续页响应必须返回严格前进的正整数 nextCursor"}},
Tips: []string{"dws ding +list --type ALL", "dws ding +list --type DELETED"},
Validate: func(rt *shortcut.RuntimeContext) error {
if rt.Int("cursor") < 0 {
return apperrors.NewValidation("--cursor 不能小于 0")
}
return nil
},
Execute: func(rt *shortcut.RuntimeContext) error {
params := map[string]any{"type": rt.Str("type")}
if rt.Changed("cursor") {
params["cursor"] = rt.Int("cursor")
}
data, err := rt.CallMCPData("im", "list_ding_messages", params)
if err != nil {
return err
}
messages, page, err := dingProjectMessages(data, "im/list_ding_messages")
if err != nil {
return err
}
if page.HasMore {
current := int64(rt.Int("cursor"))
next, parseErr := strconv.ParseInt(page.Next, 10, 64)
if parseErr != nil || next <= current {
return dingResponseError("im/list_ding_messages", "stalled_cursor", "nextCursor 没有严格前进")
}
}
return outputDingPage(rt, messages, page)
},
}
var ReceiverStatus = shortcut.Shortcut{
Service: "ding", Command: "+receiver-status", Product: "im",
Description: "查询 DING 消息接收人已读状态",
Intent: "已经从 +list 取得稳定 openDingId,需要确认该 DING 的接收状态时使用;这是精确 ID 查询,不是消息搜索。",
Risk: shortcut.RiskRead,
Safety: dingReadSafety(),
OutputRollout: output.RolloutUnifiedActive,
Contract: dingContract(
"+receiver-status", "查询 DING 消息接收人已读状态",
"已经从 +list 取得稳定 openDingId,需要确认该 DING 的接收状态时使用;这是精确 ID 查询,不是消息搜索。",
true, dingReceiverResult(), nil,
[]contract.ParamDecl{{Name: "ding-id", Property: "dingId"}},
"dws ding +receiver-status --ding-id <DING_ID>",
),
Flags: []shortcut.Flag{{Name: "ding-id", Type: shortcut.FlagString, Desc: "openDingId", Required: true}},
Tips: []string{"dws ding +receiver-status --ding-id <DING_ID>"},
Execute: func(rt *shortcut.RuntimeContext) error {
const operation = "im/list_ding_receiver_status"
data, err := rt.CallMCPData("im", "list_ding_receiver_status", map[string]any{"openDingId": rt.Str("ding-id")})
if err != nil {
return err
}
receivers, err := dingProjectReceivers(data, operation, rt.Str("ding-id"))
if err != nil {
return err
}
payload := map[string]any{"count": len(receivers), "receivers": receivers}
if !output.UsesUnifiedResult(rt.Command()) {
return rt.Output(payload)
}
meta := &output.Meta{Count: output.NewCount(len(receivers))}
return output.StoreResult(rt.Command().Context(), output.Success(payload, output.WithMeta(meta)))
},
}
var SendPersonal = compatibilityDingWrite(
"+send-personal", "以本人身份发送 DING 给指定人",
"仅为历史 CLI 兼容保留:明确确认后按原有参数发送;因接收人稳定身份与撤回终态尚不可验证,不进入 Agent 公共发现。",
shortcut.RiskWrite, false,
[]shortcut.Flag{
{Name: "users", Type: shortcut.FlagStringSlice, Desc: "接收人 openDingTalkId 列表 (CSV)", Required: true},
{Name: "content", Type: shortcut.FlagString, Desc: "消息内容", Required: true},
{Name: "type", Type: shortcut.FlagString, Default: "app", Desc: "提醒方式:app、sms 或 call"},
{Name: "uuid", Type: shortcut.FlagString, Desc: "幂等键"},
},
[]contract.ParamDecl{
{Name: "users", Property: "users", InterfaceType: "array"},
{Name: "content", Property: "content"}, {Name: "type", Property: "type"}, {Name: "uuid", Property: "uuid"},
},
"dws ding +send-personal --users <VALUES> --content <CONTENT>",
func(rt *shortcut.RuntimeContext) error {
remindType, err := dingPersonalRemindType(rt.Str("type"))
if err != nil {
return err
}
params := map[string]any{
"receiverOpenDingTalkIds": rt.StrSlice("users"),
"content": rt.Str("content"),
"remindType": remindType,
}
if rt.Changed("uuid") {
params["uuid"] = rt.Str("uuid")
}
return rt.CallMCP("send_personal_ding", params)
},
)
var SendByMessage = unavailableDingWrite(
"+send-by-message", "针对某条消息发起 DING 提醒",
"需要把指定聊天消息转成应用内、短信或电话 DING 时使用;对应 lark-cli 的三种 urgent 任务,但接收人稳定身份与精确撤回终态仍缺失。",
shortcut.RiskWrite, false,
[]shortcut.Flag{
{Name: "group", Type: shortcut.FlagString, Desc: "openConversationId", Required: true},
{Name: "message-id", Type: shortcut.FlagString, Desc: "openMessageId", Required: true},
{Name: "users", Type: shortcut.FlagStringSlice, Desc: "接收人 openDingTalkId 列表 (CSV)", Required: true},
{Name: "type", Type: shortcut.FlagString, Default: "app", Desc: "提醒方式", Enum: []string{"app", "sms", "call"}},
{Name: "uuid", Type: shortcut.FlagString, Desc: "幂等键"},
},
[]contract.ParamDecl{
{Name: "group", Property: "openConversationId"}, {Name: "message-id", Property: "openMessageId"},
{Name: "users", Property: "receiverOpenDingTalkIds", InterfaceType: "array"},
{Name: "type", Property: "remindType"}, {Name: "uuid", Property: "uuid"},
},
"dws ding +send-by-message --group <GROUP_ID> --message-id <MESSAGE_ID> --users <VALUES>",
)
var RecallPersonal = compatibilityDingWrite(
"+recall-personal", "撤回本人发起的 DING",
"仅为历史 CLI 兼容保留:明确确认后按稳定 openDingId 撤回;因终态与残留通知尚不可查询验证,不进入 Agent 公共发现。",
shortcut.RiskHighWrite, true,
[]shortcut.Flag{{Name: "id", Type: shortcut.FlagString, Desc: "openDingId", Required: true}},
[]contract.ParamDecl{{Name: "id", Property: "id"}},
"dws ding +recall-personal --id <DING_ID>",
func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("recall_personal_ding", map[string]any{"openDingId": rt.Str("id")})
},
)
func compatibilityDingWrite(command, description, intent string, risk shortcut.Risk, destructive bool, flags []shortcut.Flag, params []contract.ParamDecl, example string, execute func(*shortcut.RuntimeContext) error) shortcut.Shortcut {
declaration := dingContract(command, description, intent, true, nil, nil, params, example)
declaration.Interface.Reason = dingCompatibilityWriteReason
return shortcut.Shortcut{
Service: "ding", Command: command, Product: "im",
Description: description, Intent: intent, Risk: risk,
Safety: dingWriteSafety(destructive), OutputRollout: output.RolloutUnifiedActive,
Contract: declaration,
Flags: flags, Tips: []string{example}, Execute: execute,
}
}
func unavailableDingWrite(command, description, intent string, risk shortcut.Risk, destructive bool, flags []shortcut.Flag, params []contract.ParamDecl, example string) shortcut.Shortcut {
return shortcut.Shortcut{
Service: "ding", Command: command, Product: "im",
Description: description, Intent: intent, Risk: risk,
Safety: dingWriteSafety(destructive), OutputRollout: output.RolloutUnifiedActive,
Contract: dingContract(command, description, intent, false, nil, nil, params, example),
Flags: flags, Tips: []string{example},
Execute: func(*shortcut.RuntimeContext) error {
return dingUnavailable("ding/" + command)
},
}
}
func dingPersonalRemindType(value string) (string, error) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "app":
return "APP", nil
case "sms":
return "SMS", nil
case "call":
return "PHONE", nil
default:
return "", apperrors.NewValidation("--type 必须是 app、sms 或 call")
}
}
func init() {
shortcut.Register(List, ReceiverStatus, SendPersonal, SendByMessage, RecallPersonal)
}