846 lines
33 KiB
Go
846 lines
33 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package mail provides declarative shortcuts for the DingTalk mail (邮箱) service:
|
|
// mailbox / message / draft / thread / folder / tag / user / attachment / template /
|
|
// contact / auto-reply / rule operations. Each shortcut maps 1:1 onto an MCP tool
|
|
// declared in internal/helpers/mail.go.
|
|
package mail
|
|
|
|
import (
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
|
)
|
|
|
|
// ── mailbox ────────────────────────────────────────────────
|
|
|
|
// MailboxList 查询当前用户可用的邮箱地址。
|
|
// ── message ────────────────────────────────────────────────
|
|
|
|
// Search 使用 KQL 查询表达式搜索邮件。
|
|
// Messages 列出指定文件夹中的邮件(底层按 folderId 查询)。
|
|
// Message 根据邮件 ID 获取邮件完整内容(含正文)。
|
|
// MessageVerify 根据 internetMessageId 查询邮件发送状态。
|
|
// Send 发送一封邮件到指定收件人。
|
|
// MessageMove 将多封邮件批量移动到目标文件夹。
|
|
// MessageDelete 批量删除指定邮件(移入已删除文件夹或永久删除)。
|
|
// MessageModify 批量修改邮件状态(标记已读/未读/添加标签/移除标签)。
|
|
// ── draft ──────────────────────────────────────────────────
|
|
|
|
// DraftCreate 创建一封邮件草稿并保存到草稿箱。
|
|
// DraftEdit 更新草稿箱中已有草稿的内容。
|
|
// DraftSend 将草稿箱中已有的草稿发送出去。
|
|
// ── thread ─────────────────────────────────────────────────
|
|
|
|
// ThreadList 列出指定邮箱文件夹下的邮件会话。
|
|
var ThreadList = shortcut.Shortcut{
|
|
Service: "mail",
|
|
Command: "+thread-list",
|
|
Product: "mail",
|
|
Description: "列出指定邮箱文件夹下的邮件会话(thread)",
|
|
Intent: "当你想按会话(同一往来主题的邮件串)而非单封邮件来浏览某个文件夹时使用;传入邮箱和文件夹 ID,可按时间范围和升降序筛选,返回会话列表及其 conversationId,供 +thread 查看详情。",
|
|
Risk: shortcut.RiskRead,
|
|
Safety: contract.SafetySpec{
|
|
Effect: "read", Risk: "low",
|
|
Confirmation: "not_required", Idempotency: "idempotent",
|
|
},
|
|
Contract: corecmd.ContractDecl{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "mail",
|
|
Name: "shortcut_thread_list",
|
|
CanonicalPath: "mail.shortcut_thread_list",
|
|
CLIPath: "mail +thread-list",
|
|
PrimaryCLIPath: "mail +thread-list",
|
|
},
|
|
Description: "列出指定邮箱文件夹下的邮件会话(thread)",
|
|
Interface: &contract.InterfaceSpec{
|
|
Mode: "composite",
|
|
Availability: "available",
|
|
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
|
},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "列出指定邮箱文件夹下的邮件会话(thread)",
|
|
UseWhen: []string{"当你想按会话(同一往来主题的邮件串)而非单封邮件来浏览某个文件夹时使用;传入邮箱和文件夹 ID,可按时间范围和升降序筛选,返回会话列表及其 conversationId,供 +thread 查看详情。"},
|
|
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
|
Examples: []string{"dws mail +thread-list --email user@company.com --folder 104 --limit 20"},
|
|
},
|
|
},
|
|
Flags: []shortcut.Flag{
|
|
{Name: "email", Type: shortcut.FlagString, Desc: "会话所属邮箱地址", Required: true},
|
|
{Name: "folder", Type: shortcut.FlagString, Desc: "邮件文件夹 ID(不是文件夹名称)", Required: true},
|
|
{Name: "limit", Type: shortcut.FlagInt, Default: "20", Desc: "本次列出的会话数,最大 100"},
|
|
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,首次请求可不传"},
|
|
{Name: "start", Type: shortcut.FlagString, Desc: "开始 UTC 时间,如 2024-01-01T00:00:00Z"},
|
|
{Name: "end", Type: shortcut.FlagString, Desc: "结束 UTC 时间,如 2024-12-31T23:59:59Z"},
|
|
{Name: "ascending", Type: shortcut.FlagBool, Desc: "是否按时间升序"},
|
|
},
|
|
Tips: []string{
|
|
`dws mail +thread-list --email user@company.com --folder 104 --limit 20`,
|
|
},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{
|
|
"email": rt.Str("email"),
|
|
"folderId": rt.Str("folder"),
|
|
"size": rt.Int("limit"),
|
|
}
|
|
if rt.Changed("cursor") {
|
|
params["cursor"] = rt.Str("cursor")
|
|
}
|
|
if rt.Changed("start") {
|
|
params["startTime"] = rt.Str("start")
|
|
}
|
|
if rt.Changed("end") {
|
|
params["endTime"] = rt.Str("end")
|
|
}
|
|
if rt.Changed("ascending") {
|
|
params["isAscending"] = rt.Bool("ascending")
|
|
}
|
|
data, err := rt.CallMCPData("mail", "list_mailbox_threads", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
threads := threadListProject(data)
|
|
return rt.Output(map[string]any{"count": len(threads), "threads": threads})
|
|
},
|
|
}
|
|
|
|
// threadListProject reshapes the raw list_mailbox_threads response into a clean
|
|
// {conversationId, subject, lastUpdated, isRead} thread list —
|
|
// clean output projection. Both the list container and per-item
|
|
// field names are probed defensively across candidate keys, so an empty/unknown
|
|
// shape yields an empty list rather than a crash or fabricated data.
|
|
func threadListProject(data map[string]any) []map[string]any {
|
|
raw := threadListResolveList(data)
|
|
out := make([]map[string]any, 0, len(raw))
|
|
for _, item := range raw {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
row := map[string]any{}
|
|
if v, ok := threadListFirst(m, "conversationId", "conversation_id", "id", "threadId"); ok {
|
|
row["conversationId"] = v
|
|
}
|
|
if v, ok := threadListFirst(m, "subject", "title", "topic"); ok {
|
|
row["subject"] = v
|
|
}
|
|
if v, ok := threadListFirst(m, "lastUpdated", "last_updated", "lastModifiedDateTime", "updateTime", "modifiedTime", "sentDateTime"); ok {
|
|
row["lastUpdated"] = v
|
|
}
|
|
if v, ok := threadListFirst(m, "isRead", "is_read", "read", "unread"); ok {
|
|
row["isRead"] = v
|
|
}
|
|
if len(row) > 0 {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// threadListResolveList locates the list payload inside the response, tolerating
|
|
// a bare top-level array container or nesting one level deeper.
|
|
func threadListResolveList(data map[string]any) []any {
|
|
for _, key := range []string{"result", "data", "list", "items", "threads", "conversations"} {
|
|
v, ok := data[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if arr, ok := v.([]any); ok {
|
|
return arr
|
|
}
|
|
if inner, ok := v.(map[string]any); ok {
|
|
for _, ik := range []string{"list", "items", "threads", "conversations", "result", "data"} {
|
|
if arr, ok := inner[ik].([]any); ok {
|
|
return arr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return []any{}
|
|
}
|
|
|
|
// threadListFirst returns the first present candidate key's value.
|
|
func threadListFirst(m map[string]any, keys ...string) (any, bool) {
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok {
|
|
return v, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// Thread 根据会话 ID 获取会话详情。
|
|
// ThreadUpdate 修改单个邮件会话的状态或标签。
|
|
// ThreadBatchUpdate 批量修改邮件会话的状态或标签。
|
|
// ThreadTrash 删除指定邮件会话(移入已删除文件夹,不可撤销)。
|
|
// ThreadBatchTrash 批量删除指定邮件会话(不可撤销)。
|
|
// ── folder ─────────────────────────────────────────────────
|
|
|
|
// FolderList 列举邮件文件夹。
|
|
var FolderList = shortcut.Shortcut{
|
|
Service: "mail",
|
|
Command: "+folder-list",
|
|
Product: "mail",
|
|
Description: "列出顶层文件夹或指定父文件夹下的子文件夹",
|
|
Intent: "当你需要了解某个邮箱有哪些文件夹、或要取得文件夹 ID 以便移动邮件、按文件夹列信/建规则时使用;传入邮箱(可选父文件夹 ID 查子级),返回文件夹列表及其 ID。",
|
|
Risk: shortcut.RiskRead,
|
|
Safety: contract.SafetySpec{
|
|
Effect: "read", Risk: "low",
|
|
Confirmation: "not_required", Idempotency: "idempotent",
|
|
},
|
|
Contract: corecmd.ContractDecl{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "mail",
|
|
Name: "shortcut_folder_list",
|
|
CanonicalPath: "mail.shortcut_folder_list",
|
|
CLIPath: "mail +folder-list",
|
|
PrimaryCLIPath: "mail +folder-list",
|
|
},
|
|
Description: "列出顶层文件夹或指定父文件夹下的子文件夹",
|
|
Interface: &contract.InterfaceSpec{
|
|
Mode: "composite",
|
|
Availability: "available",
|
|
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
|
},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "列出顶层文件夹或指定父文件夹下的子文件夹",
|
|
UseWhen: []string{"当你需要了解某个邮箱有哪些文件夹、或要取得文件夹 ID 以便移动邮件、按文件夹列信/建规则时使用;传入邮箱(可选父文件夹 ID 查子级),返回文件夹列表及其 ID。"},
|
|
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
|
Examples: []string{"dws mail +folder-list --email user@company.com"},
|
|
},
|
|
},
|
|
Flags: []shortcut.Flag{
|
|
{Name: "email", Type: shortcut.FlagString, Desc: "邮件所属邮箱地址", Required: true},
|
|
{Name: "folder", Type: shortcut.FlagString, Desc: "父文件夹 ID,不传则返回顶层文件夹"},
|
|
},
|
|
Tips: []string{
|
|
`dws mail +folder-list --email user@company.com`,
|
|
},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{
|
|
"email": rt.Str("email"),
|
|
}
|
|
if rt.Changed("folder") {
|
|
params["folderId"] = rt.Str("folder")
|
|
}
|
|
data, err := rt.CallMCPData("mail", "list_folders", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
folders := folderListProject(data)
|
|
return rt.Output(map[string]any{"count": len(folders), "folders": folders})
|
|
},
|
|
}
|
|
|
|
// folderListProject reshapes the raw list_folders response into a clean
|
|
// {id, name, parentId} folder list — clean output projection. Both
|
|
// the list container and per-item field names are probed defensively across
|
|
// candidate keys, so an empty/unknown shape yields an empty list rather than a
|
|
// crash or fabricated data.
|
|
func folderListProject(data map[string]any) []map[string]any {
|
|
raw := folderListResolveList(data)
|
|
out := make([]map[string]any, 0, len(raw))
|
|
for _, item := range raw {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
row := map[string]any{}
|
|
if v, ok := folderListFirst(m, "id", "folderId", "folder_id"); ok {
|
|
row["id"] = v
|
|
}
|
|
if v, ok := folderListFirst(m, "name", "folderName", "folder_name", "displayName"); ok {
|
|
row["name"] = v
|
|
}
|
|
if v, ok := folderListFirst(m, "parentId", "parent_id", "parentFolderId"); ok {
|
|
row["parentId"] = v
|
|
}
|
|
if len(row) > 0 {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// folderListResolveList locates the list payload inside the response, tolerating
|
|
// a bare top-level array container or nesting one level deeper.
|
|
func folderListResolveList(data map[string]any) []any {
|
|
for _, key := range []string{"result", "data", "list", "items", "folders"} {
|
|
v, ok := data[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if arr, ok := v.([]any); ok {
|
|
return arr
|
|
}
|
|
if inner, ok := v.(map[string]any); ok {
|
|
for _, ik := range []string{"list", "items", "folders", "result", "data"} {
|
|
if arr, ok := inner[ik].([]any); ok {
|
|
return arr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return []any{}
|
|
}
|
|
|
|
// folderListFirst returns the first present candidate key's value.
|
|
func folderListFirst(m map[string]any, keys ...string) (any, bool) {
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok {
|
|
return v, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// FolderCreate 创建邮件文件夹。
|
|
// FolderUpdate 更新邮件文件夹名称。
|
|
// FolderDelete 删除邮件文件夹。
|
|
// ── tag ────────────────────────────────────────────────────
|
|
|
|
// TagList 列举邮件标签。
|
|
var TagList = shortcut.Shortcut{
|
|
Service: "mail",
|
|
Command: "+tag-list",
|
|
Product: "mail",
|
|
Description: "列出指定邮箱下的所有邮件标签",
|
|
Intent: "当你要查看邮箱里有哪些标签、或需要取得标签 ID 以便给邮件/会话加标签时使用;传入邮箱地址,返回全部邮件标签及其 ID。",
|
|
Risk: shortcut.RiskRead,
|
|
Safety: contract.SafetySpec{
|
|
Effect: "read", Risk: "low",
|
|
Confirmation: "not_required", Idempotency: "idempotent",
|
|
},
|
|
Contract: corecmd.ContractDecl{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "mail",
|
|
Name: "shortcut_tag_list",
|
|
CanonicalPath: "mail.shortcut_tag_list",
|
|
CLIPath: "mail +tag-list",
|
|
PrimaryCLIPath: "mail +tag-list",
|
|
},
|
|
Description: "列出指定邮箱下的所有邮件标签",
|
|
Interface: &contract.InterfaceSpec{
|
|
Mode: "composite",
|
|
Availability: "available",
|
|
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
|
},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "列出指定邮箱下的所有邮件标签",
|
|
UseWhen: []string{"当你要查看邮箱里有哪些标签、或需要取得标签 ID 以便给邮件/会话加标签时使用;传入邮箱地址,返回全部邮件标签及其 ID。"},
|
|
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
|
Examples: []string{"dws mail +tag-list --email user@company.com"},
|
|
},
|
|
},
|
|
Flags: []shortcut.Flag{
|
|
{Name: "email", Type: shortcut.FlagString, Desc: "用户的邮箱地址", Required: true},
|
|
},
|
|
Tips: []string{
|
|
`dws mail +tag-list --email user@company.com`,
|
|
},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
data, err := rt.CallMCPData("mail", "list_tags", map[string]any{
|
|
"email": rt.Str("email"),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tags := tagListProject(data)
|
|
return rt.Output(map[string]any{"count": len(tags), "tags": tags})
|
|
},
|
|
}
|
|
|
|
// tagListProject reshapes the raw list_tags response into a clean
|
|
// {id, name, parentId} tag list — clean output projection. Both the
|
|
// list container and per-item field names are probed defensively across
|
|
// candidate keys, so an empty/unknown shape yields an empty list rather than a
|
|
// crash or fabricated data.
|
|
func tagListProject(data map[string]any) []map[string]any {
|
|
raw := tagListResolveList(data)
|
|
out := make([]map[string]any, 0, len(raw))
|
|
for _, item := range raw {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
row := map[string]any{}
|
|
if v, ok := tagListFirst(m, "id", "tagId", "tag_id"); ok {
|
|
row["id"] = v
|
|
}
|
|
if v, ok := tagListFirst(m, "name", "tagName", "tag_name", "displayName"); ok {
|
|
row["name"] = v
|
|
}
|
|
if v, ok := tagListFirst(m, "parentId", "parent_id"); ok {
|
|
row["parentId"] = v
|
|
}
|
|
if len(row) > 0 {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// tagListResolveList locates the list payload inside the response, tolerating a
|
|
// bare top-level array container or nesting one level deeper.
|
|
func tagListResolveList(data map[string]any) []any {
|
|
for _, key := range []string{"result", "data", "list", "items", "tags"} {
|
|
v, ok := data[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if arr, ok := v.([]any); ok {
|
|
return arr
|
|
}
|
|
if inner, ok := v.(map[string]any); ok {
|
|
for _, ik := range []string{"list", "items", "tags", "result", "data"} {
|
|
if arr, ok := inner[ik].([]any); ok {
|
|
return arr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return []any{}
|
|
}
|
|
|
|
// tagListFirst returns the first present candidate key's value.
|
|
func tagListFirst(m map[string]any, keys ...string) (any, bool) {
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok {
|
|
return v, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// TagCreate 创建邮件标签。
|
|
// TagUpdate 更新邮件标签名称。
|
|
// TagDelete 删除邮件标签。
|
|
// ── user ───────────────────────────────────────────────────
|
|
|
|
// UserSearch 按关键词或工号搜索邮箱用户。
|
|
var UserSearch = shortcut.Shortcut{
|
|
Service: "mail",
|
|
Command: "+user-search",
|
|
Product: "mail",
|
|
Description: "按关键词或工号搜索邮箱用户(仅企业邮箱)",
|
|
Intent: "当你只知道同事的姓名或工号、需要查出其企业邮箱地址以便发信或添加联系人时使用;提供关键词或工号(至少其一),返回匹配的企业邮箱用户列表。",
|
|
Risk: shortcut.RiskRead,
|
|
Safety: contract.SafetySpec{
|
|
Effect: "read", Risk: "low",
|
|
Confirmation: "not_required", Idempotency: "idempotent",
|
|
},
|
|
Contract: corecmd.ContractDecl{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "mail",
|
|
Name: "shortcut_user_search",
|
|
CanonicalPath: "mail.shortcut_user_search",
|
|
CLIPath: "mail +user-search",
|
|
PrimaryCLIPath: "mail +user-search",
|
|
},
|
|
Description: "按关键词或工号搜索邮箱用户(仅企业邮箱)",
|
|
Interface: &contract.InterfaceSpec{
|
|
Mode: "composite",
|
|
Availability: "available",
|
|
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
|
},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "按关键词或工号搜索邮箱用户(仅企业邮箱)",
|
|
UseWhen: []string{"当你只知道同事的姓名或工号、需要查出其企业邮箱地址以便发信或添加联系人时使用;提供关键词或工号(至少其一),返回匹配的企业邮箱用户列表。"},
|
|
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
|
Examples: []string{
|
|
"dws mail +user-search --keyword \"张三\"",
|
|
"dws mail +user-search --email user@company.com --employee-no \"E123456\"",
|
|
},
|
|
},
|
|
},
|
|
Flags: []shortcut.Flag{
|
|
{Name: "keyword", Type: shortcut.FlagString, Desc: "搜索关键词(未提供 --employee-no 时为必填)"},
|
|
{Name: "employee-no", Type: shortcut.FlagString, Desc: "按工号精确搜索"},
|
|
{Name: "email", Type: shortcut.FlagString, Desc: "搜索目标邮箱地址"},
|
|
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,取自响应中的 nextCursor"},
|
|
{Name: "limit", Type: shortcut.FlagString, Desc: "每页返回数量"},
|
|
},
|
|
Constraints: []shortcut.Constraint{
|
|
{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"keyword", "employee-no"}},
|
|
},
|
|
Tips: []string{
|
|
`dws mail +user-search --keyword "张三"`,
|
|
`dws mail +user-search --email user@company.com --employee-no "E123456"`,
|
|
},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{}
|
|
if rt.Str("keyword") != "" {
|
|
params["keyword"] = rt.Str("keyword")
|
|
}
|
|
if rt.Str("employee-no") != "" {
|
|
params["employeeNo"] = rt.Str("employee-no")
|
|
}
|
|
if rt.Changed("email") {
|
|
params["email"] = rt.Str("email")
|
|
}
|
|
if rt.Changed("cursor") {
|
|
params["cursor"] = rt.Str("cursor")
|
|
}
|
|
if rt.Changed("limit") {
|
|
params["size"] = rt.Str("limit")
|
|
}
|
|
data, err := rt.CallMCPData("mail", "search_mail_users", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
users := userSearchProject(data)
|
|
return rt.Output(map[string]any{"count": len(users), "users": users})
|
|
},
|
|
}
|
|
|
|
// userSearchProject reshapes the raw search_mail_users response into a clean
|
|
// {name, email, employeeNo, userId} user list — clean output projection.
|
|
// Both the list container and per-item field names are probed defensively
|
|
// across candidate keys, so an empty/unknown shape yields an empty list rather
|
|
// than a crash or fabricated data.
|
|
func userSearchProject(data map[string]any) []map[string]any {
|
|
raw := userSearchResolveList(data)
|
|
out := make([]map[string]any, 0, len(raw))
|
|
for _, item := range raw {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
row := map[string]any{}
|
|
if v, ok := userSearchFirst(m, "name", "userName", "displayName", "nickName"); ok {
|
|
row["name"] = v
|
|
}
|
|
if v, ok := userSearchFirst(m, "email", "mail", "emailAddress"); ok {
|
|
row["email"] = v
|
|
}
|
|
if v, ok := userSearchFirst(m, "employeeNo", "employee_no", "employeeNumber", "jobNumber"); ok {
|
|
row["employeeNo"] = v
|
|
}
|
|
if v, ok := userSearchFirst(m, "userId", "user_id", "id"); ok {
|
|
row["userId"] = v
|
|
}
|
|
if len(row) > 0 {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// userSearchResolveList locates the list payload inside the response, tolerating
|
|
// a bare top-level array container or nesting one level deeper.
|
|
func userSearchResolveList(data map[string]any) []any {
|
|
for _, key := range []string{"result", "data", "list", "items", "users"} {
|
|
v, ok := data[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if arr, ok := v.([]any); ok {
|
|
return arr
|
|
}
|
|
if inner, ok := v.(map[string]any); ok {
|
|
for _, ik := range []string{"list", "items", "users", "result", "data"} {
|
|
if arr, ok := inner[ik].([]any); ok {
|
|
return arr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return []any{}
|
|
}
|
|
|
|
// userSearchFirst returns the first present candidate key's value.
|
|
func userSearchFirst(m map[string]any, keys ...string) (any, bool) {
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok {
|
|
return v, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// ── attachment ─────────────────────────────────────────────
|
|
|
|
// AttachmentList 列举邮件附件。
|
|
// ── template ───────────────────────────────────────────────
|
|
|
|
// TemplateCreate 创建邮件模板。
|
|
// TemplateList 列举邮件模板。
|
|
var TemplateList = shortcut.Shortcut{
|
|
Service: "mail",
|
|
Command: "+template-list",
|
|
Product: "mail",
|
|
Description: "列出指定邮箱的所有邮件模板",
|
|
Intent: "当你想查看某个邮箱下已有哪些邮件模板、或需要取得模板 ID 以便查看详情或更新时使用;传入邮箱和每页数量,返回模板列表,支持分页。",
|
|
Risk: shortcut.RiskRead,
|
|
Safety: contract.SafetySpec{
|
|
Effect: "read", Risk: "low",
|
|
Confirmation: "not_required", Idempotency: "idempotent",
|
|
},
|
|
Contract: corecmd.ContractDecl{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "mail",
|
|
Name: "shortcut_template_list",
|
|
CanonicalPath: "mail.shortcut_template_list",
|
|
CLIPath: "mail +template-list",
|
|
PrimaryCLIPath: "mail +template-list",
|
|
},
|
|
Description: "列出指定邮箱的所有邮件模板",
|
|
Interface: &contract.InterfaceSpec{
|
|
Mode: "composite",
|
|
Availability: "available",
|
|
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
|
},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "列出指定邮箱的所有邮件模板",
|
|
UseWhen: []string{"当你想查看某个邮箱下已有哪些邮件模板、或需要取得模板 ID 以便查看详情或更新时使用;传入邮箱和每页数量,返回模板列表,支持分页。"},
|
|
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
|
Examples: []string{"dws mail +template-list --email user@company.com --limit 20"},
|
|
},
|
|
},
|
|
Flags: []shortcut.Flag{
|
|
{Name: "email", Type: shortcut.FlagString, Desc: "用户邮箱地址", Required: true},
|
|
{Name: "limit", Type: shortcut.FlagString, Desc: "每页返回数量", Required: true},
|
|
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,取自响应中的 nextCursor"},
|
|
},
|
|
Tips: []string{
|
|
`dws mail +template-list --email user@company.com --limit 20`,
|
|
},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{
|
|
"email": rt.Str("email"),
|
|
"size": rt.Str("limit"),
|
|
}
|
|
if rt.Changed("cursor") {
|
|
params["cursor"] = rt.Str("cursor")
|
|
}
|
|
data, err := rt.CallMCPData("mail", "list_user_message_templates", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
templates := templateListProject(data)
|
|
return rt.Output(map[string]any{"count": len(templates), "templates": templates})
|
|
},
|
|
}
|
|
|
|
// templateListProject reshapes the raw list_user_message_templates response into
|
|
// a clean {id, name, subject} template list — clean output projection.
|
|
// Both the list container and per-item field names are probed defensively
|
|
// across candidate keys, so an empty/unknown shape yields an empty list rather
|
|
// than a crash or fabricated data.
|
|
func templateListProject(data map[string]any) []map[string]any {
|
|
raw := templateListResolveList(data)
|
|
out := make([]map[string]any, 0, len(raw))
|
|
for _, item := range raw {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
row := map[string]any{}
|
|
if v, ok := templateListFirst(m, "id", "templateId", "template_id"); ok {
|
|
row["id"] = v
|
|
}
|
|
if v, ok := templateListFirst(m, "name", "templateName", "template_name", "displayName"); ok {
|
|
row["name"] = v
|
|
}
|
|
if v, ok := templateListFirst(m, "subject", "title"); ok {
|
|
row["subject"] = v
|
|
}
|
|
if len(row) > 0 {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// templateListResolveList locates the list payload inside the response,
|
|
// tolerating a bare top-level array container or nesting one level deeper.
|
|
func templateListResolveList(data map[string]any) []any {
|
|
for _, key := range []string{"result", "data", "list", "items", "templates"} {
|
|
v, ok := data[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if arr, ok := v.([]any); ok {
|
|
return arr
|
|
}
|
|
if inner, ok := v.(map[string]any); ok {
|
|
for _, ik := range []string{"list", "items", "templates", "result", "data"} {
|
|
if arr, ok := inner[ik].([]any); ok {
|
|
return arr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return []any{}
|
|
}
|
|
|
|
// templateListFirst returns the first present candidate key's value.
|
|
func templateListFirst(m map[string]any, keys ...string) (any, bool) {
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok {
|
|
return v, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// TemplateGet 获取邮件模板详情。
|
|
// TemplateUpdate 更新邮件模板。
|
|
// TemplateDelete 删除邮件模板。
|
|
// ── contact ────────────────────────────────────────────────
|
|
|
|
// ContactCreate 创建邮件联系人。
|
|
// ContactList 列举邮件联系人。
|
|
var ContactList = shortcut.Shortcut{
|
|
Service: "mail",
|
|
Command: "+contact-list",
|
|
Product: "mail",
|
|
Description: "列出指定邮箱的所有邮件联系人",
|
|
Intent: "当你想查看某邮箱通讯录里有哪些联系人、或需要取得联系人 ID 以便更新或删除时使用;传入邮箱和每页数量,返回联系人列表,支持分页。",
|
|
Risk: shortcut.RiskRead,
|
|
Safety: contract.SafetySpec{
|
|
Effect: "read", Risk: "low",
|
|
Confirmation: "not_required", Idempotency: "idempotent",
|
|
},
|
|
Contract: corecmd.ContractDecl{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "mail",
|
|
Name: "shortcut_contact_list",
|
|
CanonicalPath: "mail.shortcut_contact_list",
|
|
CLIPath: "mail +contact-list",
|
|
PrimaryCLIPath: "mail +contact-list",
|
|
},
|
|
Description: "列出指定邮箱的所有邮件联系人",
|
|
Interface: &contract.InterfaceSpec{
|
|
Mode: "composite",
|
|
Availability: "available",
|
|
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
|
},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "列出指定邮箱的所有邮件联系人",
|
|
UseWhen: []string{"当你想查看某邮箱通讯录里有哪些联系人、或需要取得联系人 ID 以便更新或删除时使用;传入邮箱和每页数量,返回联系人列表,支持分页。"},
|
|
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
|
Examples: []string{"dws mail +contact-list --email user@company.com --limit 20"},
|
|
},
|
|
},
|
|
Flags: []shortcut.Flag{
|
|
{Name: "email", Type: shortcut.FlagString, Desc: "用户邮箱地址", Required: true},
|
|
{Name: "limit", Type: shortcut.FlagString, Desc: "每页返回数量", Required: true},
|
|
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,取自响应中的 nextCursor"},
|
|
},
|
|
Tips: []string{
|
|
`dws mail +contact-list --email user@company.com --limit 20`,
|
|
},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{
|
|
"email": rt.Str("email"),
|
|
"size": rt.Str("limit"),
|
|
}
|
|
if rt.Changed("cursor") {
|
|
params["cursor"] = rt.Str("cursor")
|
|
}
|
|
data, err := rt.CallMCPData("mail", "list_user_mail_contacts", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
contacts := contactListProject(data)
|
|
return rt.Output(map[string]any{"count": len(contacts), "contacts": contacts})
|
|
},
|
|
}
|
|
|
|
// contactListProject reshapes the raw list_user_mail_contacts response into a
|
|
// clean {id, contactEmail, displayName} contact list — output-projection
|
|
// clean output projection. Both the list container and per-item field names are probed
|
|
// defensively across candidate keys, so an empty/unknown shape yields an empty
|
|
// list rather than a crash or fabricated data.
|
|
func contactListProject(data map[string]any) []map[string]any {
|
|
raw := contactListResolveList(data)
|
|
out := make([]map[string]any, 0, len(raw))
|
|
for _, item := range raw {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
row := map[string]any{}
|
|
if v, ok := contactListFirst(m, "id", "contactId", "contact_id"); ok {
|
|
row["id"] = v
|
|
}
|
|
if v, ok := contactListFirst(m, "contactEmail", "contact_email", "email"); ok {
|
|
row["contactEmail"] = v
|
|
}
|
|
if v, ok := contactListFirst(m, "displayName", "display_name", "name"); ok {
|
|
row["displayName"] = v
|
|
}
|
|
if len(row) > 0 {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// contactListResolveList locates the list payload inside the response,
|
|
// tolerating a bare top-level array container or nesting one level deeper.
|
|
func contactListResolveList(data map[string]any) []any {
|
|
for _, key := range []string{"result", "data", "list", "items", "contacts"} {
|
|
v, ok := data[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if arr, ok := v.([]any); ok {
|
|
return arr
|
|
}
|
|
if inner, ok := v.(map[string]any); ok {
|
|
for _, ik := range []string{"list", "items", "contacts", "result", "data"} {
|
|
if arr, ok := inner[ik].([]any); ok {
|
|
return arr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return []any{}
|
|
}
|
|
|
|
// contactListFirst returns the first present candidate key's value.
|
|
func contactListFirst(m map[string]any, keys ...string) (any, bool) {
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok {
|
|
return v, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// ContactUpdate 更新邮件联系人。
|
|
// ContactBatchDelete 批量删除邮件联系人。
|
|
// ── auto-reply ─────────────────────────────────────────────
|
|
|
|
// AutoReplyGet 获取用户的自动回复配置。
|
|
// ── rule ───────────────────────────────────────────────────
|
|
|
|
// RuleList 列出个人收信规则。
|
|
// RuleCreate 创建个人收信规则。
|
|
// RuleUpdate 更新个人收信规则。
|
|
// RuleDelete 删除个人收信规则。
|
|
// RuleAdjust 调整收信规则排序。
|
|
func init() {
|
|
shortcut.Register(
|
|
ThreadList,
|
|
FolderList,
|
|
TagList,
|
|
UserSearch,
|
|
TemplateList,
|
|
ContactList,
|
|
)
|
|
}
|