Files
dingtalk-workspace-cli/internal/helpers/minutes.go

961 lines
42 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helpers
import (
"fmt"
"strconv"
"strings"
"github.com/spf13/cobra"
)
// ──────────────────────────────────────────────────────────
// dws minutes — 听记
// ──────────────────────────────────────────────────────────
func newMinutesCommand() *cobra.Command {
minutesListCmd := &cobra.Command{Use: "list", Short: "听记列表", RunE: groupRunE}
minutesListMineCmd := &cobra.Command{
Use: "mine",
Short: "查询我创建的听记列表",
Long: `查询我创建的听记列表,支持分页,支持按关键字和时间范围筛选。`,
Example: ` dws minutes list mine
dws minutes list mine --limit 10
dws minutes list mine --limit 10 --cursor <token>
dws minutes list mine --query "周会"`,
RunE: func(cmd *cobra.Command, args []string) error {
return callListByKeywordRange(cmd, "created")
},
}
minutesListSharedCmd := &cobra.Command{
Use: "shared",
Short: "查询他人共享给我的听记列表",
Long: `查询他人共享给我的听记列表,支持分页,支持按关键字和时间范围筛选。`,
Example: ` dws minutes list shared
dws minutes list shared --limit 20
dws minutes list shared --limit 5 --cursor <token>`,
RunE: func(cmd *cobra.Command, args []string) error {
return callListByKeywordRange(cmd, "shared")
},
}
minutesListAllCmd := &cobra.Command{
Use: "all",
Short: "查询我有权限访问的所有听记列表",
Long: `查询我有权限访问的所有听记列表(包括我创建的、他人共享给我的等所有有权限的听记),支持按关键字和时间范围筛选。
时间范围和时间关键词为可选参数,不传则返回所有有权限的听记。
--limit 为可选参数,不传时默认返回 10 条。`,
Example: ` dws minutes list all
dws minutes list all --limit 20
dws minutes list all --query "周会" --limit 20
dws minutes list all --start "2026-03-01T00:00:00+08:00" --end "2026-03-20T23:59:59+08:00"
dws minutes list all --limit 10 --cursor <token>`,
RunE: func(cmd *cobra.Command, args []string) error {
return callListByKeywordRange(cmd, "noLimit")
},
}
minutesGetCmd := &cobra.Command{Use: "get", Short: "获取听记内容", RunE: groupRunE}
minutesGetInfoCmd := &cobra.Command{
Use: "info",
Short: "获取听记基础信息",
Long: `获取指定听记的基础元数据信息。
返回字段:创建人、开始时间、截止时间、听记标题、听记访问链接URL。`,
Example: ` dws minutes get info --id <taskUuid>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "id", "url", "task-uuid", "uuid"); err != nil {
return err
}
return callMCPTool("get_minutes_basic_info", map[string]any{
"taskUuid": flagOrFallback(cmd, "id", "url", "task-uuid", "uuid"),
})
},
}
minutesGetSummaryCmd := &cobra.Command{
Use: "summary",
Short: "获取听记 AI 摘要",
Long: `获取由 AI 对听记转写原文进行结构化提炼生成的摘要,返回 Markdown 格式。
内容涵盖会议主题、核心结论、关键讨论点等。`,
Example: ` dws minutes get summary --id <taskUuid>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "id", "url", "task-uuid", "uuid"); err != nil {
return err
}
return callMCPTool("get_minutes_ai_summary", map[string]any{
"taskUuid": flagOrFallback(cmd, "id", "url", "task-uuid", "uuid"),
})
},
}
minutesGetKeywordsCmd := &cobra.Command{
Use: "keywords",
Short: "获取听记关键字列表",
Example: ` dws minutes get keywords --id <taskUuid>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "id", "url", "task-uuid", "uuid"); err != nil {
return err
}
return callMCPTool("get_minutes_keywords", map[string]any{
"taskUuid": flagOrFallback(cmd, "id", "url", "task-uuid", "uuid"),
})
},
}
minutesGetTranscriptionCmd := &cobra.Command{
Use: "transcription",
Short: "获取听记语音转写原文",
Long: `获取指定听记的语音转写原文。
每条记录包含:发言人信息、转写文本、对应时间戳。
当用户明确要求查看或分析转写原文时,应默认拉取全部原文(自动翻页),
不需要用户手动指定"第一页"。如果用户意图不是专门看原文(如查列表、
看摘要),则不应主动调用此命令。
字符上限保护:循环拉取累积超过 12000 字符时,应暂停并询问用户
是否继续拉取后续分页内容。
--direction:
0 = 正序(时间递增,默认)
1 = 倒序(时间递减)`,
Example: ` dws minutes get transcription --id <taskUuid>
dws minutes get transcription --id <taskUuid> --direction 1`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "id", "url", "task-uuid", "uuid"); err != nil {
return err
}
toolArgs := map[string]any{
"taskUuid": flagOrFallback(cmd, "id", "url", "task-uuid", "uuid"),
"direction": mustGetFlag(cmd, "direction"),
}
if v := flagOrFallback(cmd, "cursor", "next-token"); v != "" {
toolArgs["nextToken"] = v
}
return callMCPTool("get_minutes_transcription", toolArgs)
},
}
minutesGetTodosCmd := &cobra.Command{
Use: "todos",
Short: "获取听记中提取的待办事项",
Long: `查询指定听记中由系统提取的待办事项列表。
每条记录包含:待办内容、待办唯一ID、参与人信息、待办时间。`,
Example: ` dws minutes get todos --id <taskUuid>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "id", "url", "task-uuid", "uuid"); err != nil {
return err
}
return callMCPTool("list_minutes_todos", map[string]any{
"taskUuid": flagOrFallback(cmd, "id", "url", "task-uuid", "uuid"),
})
},
}
// minutesGetAudioCmd — 对应 MCP 工具 query_minutes_audio_url
// 必填参数:taskUuid(--id 或 --url)
// 操作人需拥有该听记的"读"权限及以上才会返回音频/视频地址。
// 支持所有类型的听记(线上闪记、线下闪记、A1 硬件听记、上传文件听记等)。
// 以下场景不返回地址:听记已被删除、A1 无痕模式听记、临存过期的听记(媒体未准备好或临时存储已过期)。
// 注意:返回的 OSS 地址通常包含 & 等字符,使用 callMCPToolUnescaped 避免 JSON 转义。
minutesGetAudioCmd := &cobra.Command{
Use: "audio",
Short: "获取听记音频/视频地址",
Long: `查询听记的音频/视频文件地址(OSS 链接)。
操作人需拥有该听记的"读"权限及以上才会返回。
支持所有类型的听记(线上闪记、线下闪记、A1 硬件听记、上传文件听记等)。
以下场景不返回地址:
- 听记已被删除
- A1 无痕模式听记
- 临存过期的听记(媒体未准备好或临时存储已过期)`,
Example: ` dws minutes get audio --id <taskUuid>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "id", "url", "task-uuid", "uuid"); err != nil {
return err
}
return callMCPToolUnescaped("query_minutes_audio_url", map[string]any{
"taskUuid": flagOrFallback(cmd, "id", "url", "task-uuid", "uuid"),
})
},
}
minutesGetBatchCmd := &cobra.Command{
Use: "batch",
Short: "批量查询听记详情",
Long: `根据 taskUuid 列表批量查询听记详情。
返回字段:听记标题、时长、参与人列表、创建时间、taskUuid、听记状态。`,
Example: ` dws minutes get batch --ids uuid1,uuid2,uuid3`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "ids"); err != nil {
return err
}
return callMCPTool("batch_get_minutes_details", map[string]any{
"requestBody": map[string]any{
"taskUuids": parseCSVValues(mustGetFlag(cmd, "ids")),
},
})
},
}
minutesUpdateCmd := &cobra.Command{Use: "update", Short: "更新听记信息", RunE: groupRunE}
minutesUpdateTitleCmd := &cobra.Command{
Use: "title",
Short: "修改听记标题",
Example: ` dws minutes update title --id <taskUuid> --title "Q2 复盘会议"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id", "title"); err != nil {
return err
}
return callMCPTool("update_minutes_title", map[string]any{
"taskUuid": mustGetFlag(cmd, "id"),
"title": mustGetFlag(cmd, "title"),
})
},
}
// list subcommands — mine/shared/all 共享 callListByKeywordRange 链路
minutesListMineCmd.Flags().Float64("limit", 10, "每页数据条数 (默认 10)")
minutesListSharedCmd.Flags().Float64("limit", 10, "每页数据条数 (默认 10)")
minutesListAllCmd.Flags().Float64("limit", 10, "每页数据条数 (默认 10)")
for _, sub := range []*cobra.Command{minutesListMineCmd, minutesListSharedCmd, minutesListAllCmd} {
sub.Flags().Float64("max", 10, "--limit 的别名 (兼容旧版)")
_ = sub.Flags().MarkHidden("max")
sub.Flags().String("cursor", "", "分页 token (首页留空)")
sub.Flags().String("next-token", "", "--cursor 的别名 (兼容旧版)")
_ = sub.Flags().MarkHidden("next-token")
sub.Flags().String("offset", "", "[已废弃] 分页 offset,请使用 --cursor")
_ = sub.Flags().MarkHidden("offset")
sub.Flags().String("query", "", "关键字筛选 (可选)")
sub.Flags().String("keyword", "", "关键字筛选 (--query 的别名)")
_ = sub.Flags().MarkHidden("keyword")
sub.Flags().String("start", "", "开始时间 ISO-8601 (可选)")
sub.Flags().String("end", "", "结束时间 ISO-8601 (可选)")
}
minutesListCmd.AddCommand(minutesListMineCmd, minutesListSharedCmd, minutesListAllCmd)
for _, sub := range []*cobra.Command{
minutesGetInfoCmd, minutesGetSummaryCmd,
minutesGetKeywordsCmd, minutesGetTodosCmd,
minutesGetAudioCmd,
} {
sub.Flags().String("id", "", "听记 taskUuid (必填)")
sub.Flags().String("url", "", "--id 的别名")
_ = sub.Flags().MarkHidden("url")
sub.Flags().String("task-uuid", "", "--id 的别名 (兼容 OpenAPI 字段名)")
_ = sub.Flags().MarkHidden("task-uuid")
sub.Flags().String("uuid", "", "--id 的别名")
_ = sub.Flags().MarkHidden("uuid")
}
minutesGetTranscriptionCmd.Flags().String("id", "", "听记 taskUuid (必填)")
minutesGetTranscriptionCmd.Flags().String("url", "", "--id 的别名")
_ = minutesGetTranscriptionCmd.Flags().MarkHidden("url")
minutesGetTranscriptionCmd.Flags().String("task-uuid", "", "--id 的别名 (兼容 OpenAPI 字段名)")
_ = minutesGetTranscriptionCmd.Flags().MarkHidden("task-uuid")
minutesGetTranscriptionCmd.Flags().String("uuid", "", "--id 的别名")
_ = minutesGetTranscriptionCmd.Flags().MarkHidden("uuid")
minutesGetTranscriptionCmd.Flags().String("direction", "0", "排序方向: 0=正序, 1=倒序 (默认 0)")
minutesGetTranscriptionCmd.Flags().String("cursor", "", "分页 token (首页留空)")
minutesGetTranscriptionCmd.Flags().String("next-token", "", "--cursor 的别名 (兼容旧版)")
_ = minutesGetTranscriptionCmd.Flags().MarkHidden("next-token")
minutesGetBatchCmd.Flags().String("ids", "", "听记 taskUuid 列表,逗号分隔 (必填)")
minutesGetCmd.AddCommand(
minutesGetInfoCmd, minutesGetSummaryCmd, minutesGetKeywordsCmd,
minutesGetTranscriptionCmd, minutesGetTodosCmd, minutesGetBatchCmd,
minutesGetAudioCmd,
)
minutesUpdateTitleCmd.Flags().String("id", "", "听记 taskUuid (必填)")
minutesUpdateTitleCmd.Flags().String("title", "", "新标题 (必填)")
// listeningNoteCmdTool 是听记指令工具在 MCP 网关上的注册名。
// 网关侧该工具以中文名注册(title: minutes_cmd_start),旧英文名
// execute_listening_note_command 会返回 "PARAM_ERROR - 未找到指定工具"。
// 单个工具通过 cmd 参数覆盖 create/pause/resume/end 四种指令。
const listeningNoteCmdTool = "执行听记指令-发起AI听记录音"
minutesRecordCmd := &cobra.Command{Use: "record", Short: "控制听记录音", RunE: groupRunE}
minutesRecordStartCmd := &cobra.Command{
Use: "start",
Short: "发起听记(开始录音)",
Example: ` dws minutes record start
dws minutes record start --session-id <sessionId>`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs := map[string]any{"cmd": "create"}
if v := mustGetFlag(cmd, "session-id"); v != "" {
toolArgs["sessionId"] = v
}
return callMCPTool(listeningNoteCmdTool, toolArgs)
},
}
minutesRecordPauseCmd := &cobra.Command{
Use: "pause",
Short: "暂停听记录音",
Example: ` dws minutes record pause --id <taskUuid>
dws minutes record pause --id <taskUuid> --session-id <sessionId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id"); err != nil {
return err
}
toolArgs := map[string]any{
"cmd": "pause",
"uuid": mustGetFlag(cmd, "id"),
}
if v := mustGetFlag(cmd, "session-id"); v != "" {
toolArgs["sessionId"] = v
}
return callMCPTool(listeningNoteCmdTool, toolArgs)
},
}
minutesRecordResumeCmd := &cobra.Command{
Use: "resume",
Short: "恢复听记录音",
Example: ` dws minutes record resume --id <taskUuid>
dws minutes record resume --id <taskUuid> --session-id <sessionId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id"); err != nil {
return err
}
toolArgs := map[string]any{
"cmd": "resume",
"uuid": mustGetFlag(cmd, "id"),
}
if v := mustGetFlag(cmd, "session-id"); v != "" {
toolArgs["sessionId"] = v
}
return callMCPTool(listeningNoteCmdTool, toolArgs)
},
}
minutesRecordStopCmd := &cobra.Command{
Use: "stop",
Short: "结束听记录音",
Example: ` dws minutes record stop --id <taskUuid>
dws minutes record stop --id <taskUuid> --session-id <sessionId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id"); err != nil {
return err
}
toolArgs := map[string]any{
"cmd": "end",
"uuid": mustGetFlag(cmd, "id"),
}
if v := mustGetFlag(cmd, "session-id"); v != "" {
toolArgs["sessionId"] = v
}
return callMCPTool(listeningNoteCmdTool, toolArgs)
},
}
for _, sub := range []*cobra.Command{
minutesRecordStartCmd, minutesRecordPauseCmd, minutesRecordResumeCmd, minutesRecordStopCmd,
} {
sub.Flags().String("session-id", "", "AI 助理会话 ID (可选)")
}
for _, sub := range []*cobra.Command{
minutesRecordPauseCmd, minutesRecordResumeCmd, minutesRecordStopCmd,
} {
sub.Flags().String("id", "", "听记 taskUuid (必填)")
}
minutesRecordCmd.AddCommand(minutesRecordStartCmd, minutesRecordPauseCmd, minutesRecordResumeCmd, minutesRecordStopCmd)
// ── update summary ──────────────────────────────────────────
minutesUpdateSummaryCmd := &cobra.Command{
Use: "summary",
Short: "更新纪要内容",
Long: `用传入的摘要文本全量覆盖听记的纪要内容,不触发 AI 重新生成。
适用于用户手动编辑或 AI Agent 修改纪要的场景。
修改纪要的完整流程(读取 -> 修改 -> 校验 -> 写回):
1. 先调用 get summary 获取当前纪要原文
2. 修改时必须保留原文中所有 Markdown 图片,仅优化文本内容
3. 写回前执行 Markdown 格式校验,确保结构合理、可渲染
4. 调用 update summary 将修改后的完整内容写回听记`,
Example: ` dws minutes update summary --id <taskUuid> --content "新的纪要内容"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id", "content"); err != nil {
return err
}
return callMCPTool("update_minutes_summary", map[string]any{
"taskUuid": mustGetFlag(cmd, "id"),
"summaryText": mustGetFlag(cmd, "content"),
})
},
}
minutesUpdateSummaryCmd.Flags().String("id", "", "听记 taskUuid (必填)")
minutesUpdateSummaryCmd.Flags().String("content", "", "新的纪要内容 (必填)")
minutesUpdateCmd.AddCommand(minutesUpdateTitleCmd, minutesUpdateSummaryCmd)
// ── mind-graph 子组 ─────────────────────────────────────────
mindGraphCmd := &cobra.Command{Use: "mind-graph", Short: "思维导图管理", RunE: groupRunE}
mindGraphCreateCmd := &cobra.Command{
Use: "create",
Short: "创建思维导图",
Long: `触发创建听记思维导图任务。触发成功后,可通过 query_mind_graph_status 轮询任务状态。
状态:0=进行中,1=成功,2=失败。`,
Example: ` dws minutes mind-graph create --id <taskUuid>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "id", "url", "task-uuid", "uuid"); err != nil {
return err
}
return callMCPTool("create_mind_graph", map[string]any{
"taskUuid": flagOrFallback(cmd, "id", "url", "task-uuid", "uuid"),
})
},
}
mindGraphCreateCmd.Flags().String("id", "", "听记 taskUuid (必填)")
mindGraphCreateCmd.Flags().String("url", "", "--id 的别名")
_ = mindGraphCreateCmd.Flags().MarkHidden("url")
mindGraphCreateCmd.Flags().String("task-uuid", "", "--id 的别名 (兼容 OpenAPI 字段名)")
_ = mindGraphCreateCmd.Flags().MarkHidden("task-uuid")
mindGraphCreateCmd.Flags().String("uuid", "", "--id 的别名")
_ = mindGraphCreateCmd.Flags().MarkHidden("uuid")
mindGraphStatusCmd := &cobra.Command{
Use: "status",
Short: "查询思维导图状态",
Long: `查询指定听记的思维导图生成状态。
返回任务状态:0=进行中,1=成功,2=失败。如果没有返回任务状态,也视为成功。`,
Example: ` dws minutes mind-graph status --id <taskUuid>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlagWithAliases(cmd, "id", "url", "task-uuid", "uuid"); err != nil {
return err
}
return callMCPTool("query_mind_graph_status", map[string]any{
"taskUuid": flagOrFallback(cmd, "id", "url", "task-uuid", "uuid"),
})
},
}
mindGraphStatusCmd.Flags().String("id", "", "听记 taskUuid (必填)")
mindGraphStatusCmd.Flags().String("url", "", "--id 的别名")
_ = mindGraphStatusCmd.Flags().MarkHidden("url")
mindGraphStatusCmd.Flags().String("task-uuid", "", "--id 的别名 (兼容 OpenAPI 字段名)")
_ = mindGraphStatusCmd.Flags().MarkHidden("task-uuid")
mindGraphStatusCmd.Flags().String("uuid", "", "--id 的别名")
_ = mindGraphStatusCmd.Flags().MarkHidden("uuid")
mindGraphCmd.AddCommand(mindGraphCreateCmd, mindGraphStatusCmd)
// ── speaker 子组 ────────────────────────────────────────────
speakerCmd := &cobra.Command{Use: "speaker", Short: "发言人管理", RunE: groupRunE}
speakerReplaceCmd := &cobra.Command{
Use: "replace",
Short: "替换发言人",
Long: `批量替换听记转写中指定发言人,将源发言人(speakerNick)精确匹配的所有段落替换为目标发言人。
支持同时替换 nickName 和 subSpeakerNickname 两种匹配方式,并自动更新纪要、待办中的发言人信息。`,
Example: ` dws minutes speaker replace --id <taskUuid> --from "张三" --to "李四"
dws minutes speaker replace --id <taskUuid> --from "张三" --to "李四" --target-uid <uid>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id", "from", "to"); err != nil {
return err
}
toolArgs := map[string]any{
"taskUuid": mustGetFlag(cmd, "id"),
"speakerNick": mustGetFlag(cmd, "from"),
"targetNickName": mustGetFlag(cmd, "to"),
}
if v, _ := cmd.Flags().GetString("target-uid"); v != "" {
toolArgs["targetUid"] = v
}
return callMCPTool("replace_speaker", toolArgs)
},
}
speakerReplaceCmd.Flags().String("id", "", "听记 taskUuid (必填)")
speakerReplaceCmd.Flags().String("from", "", "源发言人昵称 (必填)")
speakerReplaceCmd.Flags().String("to", "", "目标发言人昵称 (必填)")
speakerReplaceCmd.Flags().String("target-uid", "", "目标发言人钉钉 UID (可选)")
// ── speaker summary 子组 ────────────────────────────────────
// 对应 MCP 工具 create_speaker_summary / get_speaker_summary
// 批量按听记维度汇总每位发言人的段落总结
speakerSummaryCmd := &cobra.Command{Use: "summary", Short: "发言人段落总结", RunE: groupRunE}
speakerSummaryCreateCmd := &cobra.Command{
Use: "create",
Short: "触发创建发言人段落总结任务",
Long: `触发创建发言人的段落总结任务,将听记中每位发言人的所有发言内容汇总总结。
触发后需调用 dws minutes speaker summary get 查询总结结果。
--ids 和 --task-uuids 等价,均可使用。`,
Example: ` dws minutes speaker summary create --ids <uuid1,uuid2>
dws minutes speaker summary create --task-uuids <uuid1,uuid2>`,
RunE: func(cmd *cobra.Command, args []string) error {
v := flagOrFallback(cmd, "ids", "task-uuids")
if v == "" {
return fmt.Errorf("flag --ids (or --task-uuids) is required")
}
return callMCPTool("create_speaker_summary", map[string]any{
"uuids": parseCSVValues(v),
})
},
}
speakerSummaryCreateCmd.Flags().String("ids", "", "听记 taskUuid 列表,逗号分隔 (必填)")
speakerSummaryCreateCmd.Flags().String("task-uuids", "", "--ids 的别名")
_ = speakerSummaryCreateCmd.Flags().MarkHidden("task-uuids")
speakerSummaryGetCmd := &cobra.Command{
Use: "get",
Short: "查询发言人段落总结结果",
Long: `查询发言人段落总结任务的结果,返回每位发言人的发言汇总。
需先调用 dws minutes speaker summary create 触发任务。
--ids 和 --task-uuids 等价,均可使用。`,
Example: ` dws minutes speaker summary get --ids <uuid1,uuid2>
dws minutes speaker summary get --task-uuids <uuid1,uuid2>`,
RunE: func(cmd *cobra.Command, args []string) error {
v := flagOrFallback(cmd, "ids", "task-uuids")
if v == "" {
return fmt.Errorf("flag --ids (or --task-uuids) is required")
}
return callMCPTool("get_speaker_summary", map[string]any{
"uuids": parseCSVValues(v),
})
},
}
speakerSummaryGetCmd.Flags().String("ids", "", "听记 taskUuid 列表,逗号分隔 (必填)")
speakerSummaryGetCmd.Flags().String("task-uuids", "", "--ids 的别名")
_ = speakerSummaryGetCmd.Flags().MarkHidden("task-uuids")
speakerSummaryCmd.AddCommand(speakerSummaryCreateCmd, speakerSummaryGetCmd)
speakerCmd.AddCommand(speakerReplaceCmd, speakerSummaryCmd)
// ── hot-word 子组 ───────────────────────────────────────────
hotWordCmd := &cobra.Command{Use: "hot-word", Short: "个人热词管理", RunE: groupRunE}
hotWordAddCmd := &cobra.Command{
Use: "add",
Short: "添加个人热词",
Long: `添加听记个人热词,用于优化语音识别中专有名词、人名等的识别准确率。
支持一次添加多个热词(逗号分隔),每个热词长度不超过 10 个汉字或 5 个英文单词。`,
Example: ` dws minutes hot-word add --words "钉钉"
dws minutes hot-word add --words "OKR,钉钉,Copilot"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "words"); err != nil {
return err
}
return callMCPTool("add_personal_hot_word", map[string]any{
"hotWordList": parseCSVValues(mustGetFlag(cmd, "words")),
})
},
}
hotWordAddCmd.Flags().String("words", "", "要添加的热词,多个用逗号分隔 (必填)")
hotWordListCmd := &cobra.Command{
Use: "list",
Short: "查询我的热词列表",
Long: `查询当前用户配置的所有听记热词列表。
无需传入额外参数,系统自动识别当前用户身份。
返回用户已添加的全部热词,适用于查看已有热词、去重检查等场景。`,
Example: ` dws minutes hot-word list`,
RunE: func(cmd *cobra.Command, args []string) error {
return callMCPTool("list_my_hotwords", map[string]any{})
},
}
hotWordCmd.AddCommand(hotWordAddCmd, hotWordListCmd)
// ── replace-text 命令 ───────────────────────────────────────
replaceTextCmd := &cobra.Command{
Use: "replace-text",
Short: "查找替换段落和纪要中匹配的文字",
Long: `把听记中所有出现的原文字替换为目标文字,包括转写段落和纪要摘要中出现的原文字都会被替换。
区分大小写,精确匹配。`,
Example: ` dws minutes replace-text --id <taskUuid> --search "旧文字" --replace "新文字"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "id", "search", "replace"); err != nil {
return err
}
return callMCPTool("replace_minutes_text", map[string]any{
"taskUuid": mustGetFlag(cmd, "id"),
"originalText": mustGetFlag(cmd, "search"),
"replacedText": mustGetFlag(cmd, "replace"),
})
},
}
replaceTextCmd.Flags().String("id", "", "听记 taskUuid (必填)")
replaceTextCmd.Flags().String("search", "", "要查找的文字 (必填)")
replaceTextCmd.Flags().String("replace", "", "替换为的新文字 (必填)")
// ── upload 子组 ─────────────────────────────────────────────
// 文件上传管理:通过预签名 URL 上传音视频文件并创建听记。
// 完整流程:create → HTTP PUT → complete,或 create → cancel 取消。
// 注意:upload 子组的所有命令均使用 callMCPToolUnescaped 输出 JSON,
// 避免 presignedUrl 中的 & 被 Go 标准库转义为 \u0026。
uploadCmd := &cobra.Command{Use: "upload", Short: "文件上传管理", RunE: groupRunE}
// upload create — 对应 MCP 工具 create_upload_session
// 必填参数:fileName(--file-name), fileSize(--file-size)
// 可选参数:title(--title), minutesOption 嵌套对象(--template-id, --input-language, --enable-message-card)
// 返回值包含 sessionId(后续 complete/cancel 使用)和 presignedUrl(HTTP PUT 上传目标)
uploadCreateCmd := &cobra.Command{
Use: "create",
Short: "创建文件上传会话",
Long: `创建文件上传会话,获取预签名上传URL。
调用方拿到 URL 后,直接用 HTTP PUT 将文件上传到该 URL。
必须与 complete 配合使用:
1. 调用 create 获取预签名上传 URL 和上传 ID
2. HTTP PUT 预签名上传 URL 上传文件(不带 HEADER)
3. 调用 complete 传入会话 ID`,
Example: ` dws minutes upload create --file-name "meeting.mp4" --file-size 102400
dws minutes upload create --file-name "meeting.mp4" --file-size 102400 --title "周会录音"
dws minutes upload create --file-name "meeting.mp4" --file-size 102400 --input-language "zh" --enable-message-card`,
RunE: func(cmd *cobra.Command, args []string) error {
// 校验必填参数 --file-name
if err := validateRequiredFlags(cmd, "file-name"); err != nil {
return err
}
// 校验必填参数 --file-size,必须为正整数
fileSize, _ := cmd.Flags().GetInt64("file-size")
if fileSize <= 0 {
return fmt.Errorf("flag --file-size is required and must be a positive integer")
}
// 构建 MCP 工具调用参数
toolArgs := map[string]any{
"fileName": mustGetFlag(cmd, "file-name"),
"fileSize": fileSize,
}
// 可选:听记标题,不传时服务端默认使用文件名去掉后缀
if v, _ := cmd.Flags().GetString("title"); v != "" {
toolArgs["title"] = v
}
// 构建可选的嵌套对象 minutesOption,仅在有子字段时才传入
minutesOption := map[string]any{}
if v, _ := cmd.Flags().GetString("template-id"); v != "" {
minutesOption["templateId"] = v
}
if v, _ := cmd.Flags().GetString("input-language"); v != "" {
minutesOption["inputLanguage"] = v
}
// enable-message-card 是 bool 类型,仅在用户显式传入时才加入参数
if cmd.Flags().Changed("enable-message-card") {
enableCard, _ := cmd.Flags().GetBool("enable-message-card")
minutesOption["enableMessageCard"] = enableCard
}
if len(minutesOption) > 0 {
toolArgs["minutesOption"] = minutesOption
}
// 使用 Unescaped 版本输出,保证 presignedUrl 中的 & 不被转义
return callMCPToolUnescaped("create_upload_session", toolArgs)
},
}
uploadCreateCmd.Flags().String("file-name", "", "文件名(含后缀),如 meeting.mp4 (必填)")
uploadCreateCmd.Flags().Int64("file-size", 0, "文件大小(字节)(必填)")
uploadCreateCmd.Flags().String("title", "", "听记标题,不传时默认使用文件名去掉后缀 (可选)")
uploadCreateCmd.Flags().String("template-id", "", "纪要生成使用的模板 ID (可选)")
uploadCreateCmd.Flags().String("input-language", "", "ASR 识别的源语言 (可选)")
uploadCreateCmd.Flags().Bool("enable-message-card", false, "是否推送闪记卡片消息 (可选)")
// upload complete — 对应 MCP 工具 complete_upload_session
// 必填参数:sessionId(--session-id),来自 create 返回值
// 幂等:同一 sessionId 重复调用直接返回已有任务,不会重复创建
uploadCompleteCmd := &cobra.Command{
Use: "complete",
Short: "完成文件上传并创建听记",
Long: `文件上传完成后,创建听记。
必须在 create 之后、预签名 URL 上传完成后调用。
调用流程:
1. dws minutes upload create ... → 获取 sessionId 和 presignedUrl
2. curl -X PUT "<presignedUrl>" -T "/path/to/file.mp4"
3. dws minutes upload complete --session-id <sessionId>
幂等:同一 sessionId 重复调用直接返回已有的任务,不会重复创建。`,
Example: ` dws minutes upload complete --session-id <sessionId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "session-id"); err != nil {
return err
}
return callMCPToolUnescaped("complete_upload_session", map[string]any{
"sessionId": mustGetFlag(cmd, "session-id"),
})
},
}
uploadCompleteCmd.Flags().String("session-id", "", "上传会话 ID,来自 create 返回的 sessionId (必填)")
// upload cancel — 对应 MCP 工具 cancel_upload_session
// 必填参数:sessionId(--session-id),来自 create 返回值
// 用于在上传前或上传失败后取消会话,释放服务端资源
uploadCancelCmd := &cobra.Command{
Use: "cancel",
Short: "取消文件上传会话",
Long: `取消 create 创建的上传会话,传入要取消的会话 ID。`,
Example: ` dws minutes upload cancel --session-id <sessionId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "session-id"); err != nil {
return err
}
return callMCPToolUnescaped("cancel_upload_session", map[string]any{
"sessionId": mustGetFlag(cmd, "session-id"),
})
},
}
uploadCancelCmd.Flags().String("session-id", "", "要取消的会话 sessionId (必填)")
// 注册 upload 子命令:create / complete / cancel
uploadCmd.AddCommand(uploadCreateCmd, uploadCompleteCmd, uploadCancelCmd)
// ── permission 子组 ─────────────────────────────────────────
// 听记成员权限管理:批量添加/移除成员及其权限。
// 对应 MCP 工具 add_member_permission / remove_member_permission。
permissionCmd := &cobra.Command{Use: "permission", Short: "听记成员权限管理", RunE: groupRunE}
// permission add — 对应 MCP 工具 add_member_permission
// 批量给多个听记增加成员,并设置成员的权限。
// 权限类型(--policy): 0=管理员, 1=所有者, 2=可编辑, 3=可查看/下载, 4=仅查看
// 必填参数:--ids(听记 taskUuid 列表)、--member-uids(成员钉钉 UID 列表)、--policy(权限类型)
// 可选参数:--cover(是否覆盖已有权限)、--sub-resources(权限子模块列表)
permissionAddCmd := &cobra.Command{
Use: "add",
Short: "批量添加听记成员并设置权限",
Long: `批量给多个听记增加成员,并设置成员的权限。
权限类型 (--policy):
0 = 管理员
1 = 所有者
2 = 可编辑
3 = 可查看/下载
4 = 仅查看
权限子模块 (--sub-resources,可选,逗号分隔):
OrigContent = 原始内容
Summary = 纪要
Analysis = 分析
Note = 笔记`,
Example: ` dws minutes permission add --ids <uuid1,uuid2> --member-uids 123456,789012 --policy 3
dws minutes permission add --ids <uuid> --member-uids 123456 --policy 2 --cover
dws minutes permission add --ids <uuid> --member-uids 123456 --policy 3 --sub-resources "OrigContent,Summary"`,
RunE: func(cmd *cobra.Command, args []string) error {
v := flagOrFallback(cmd, "ids", "uuids", "task-uuids")
if v == "" {
return fmt.Errorf("flag --ids (or --uuids / --task-uuids) is required")
}
if err := validateRequiredFlags(cmd, "member-uids", "policy"); err != nil {
return err
}
policyID, err := strconv.ParseInt(mustGetFlag(cmd, "policy"), 10, 64)
if err != nil || policyID < 0 || policyID > 4 {
return fmt.Errorf("flag --policy must be an integer between 0 and 4 (0=管理员, 1=所有者, 2=可编辑, 3=可查看/下载, 4=仅查看)")
}
memberUids := parseCSVValues(mustGetFlag(cmd, "member-uids"))
for _, uid := range memberUids {
if strings.TrimSpace(uid) == "" {
return fmt.Errorf("flag --member-uids contains empty UID")
}
}
toolArgs := map[string]any{
"uuids": parseCSVValues(v),
"policyId": float64(policyID),
"memberUids": memberUids,
}
if cmd.Flags().Changed("cover") {
cover, _ := cmd.Flags().GetBool("cover")
if cover {
toolArgs["coverPermission"] = "true"
} else {
toolArgs["coverPermission"] = "false"
}
}
if sv, _ := cmd.Flags().GetString("sub-resources"); sv != "" {
toolArgs["roleSubResourceIds"] = parseCSVValues(sv)
}
return callMCPTool("add_member_permission", toolArgs)
},
}
permissionAddCmd.Flags().String("ids", "", "听记 taskUuid 列表,逗号分隔 (必填)")
permissionAddCmd.Flags().String("uuids", "", "--ids 的别名")
_ = permissionAddCmd.Flags().MarkHidden("uuids")
permissionAddCmd.Flags().String("task-uuids", "", "--ids 的别名")
_ = permissionAddCmd.Flags().MarkHidden("task-uuids")
permissionAddCmd.Flags().String("member-uids", "", "成员钉钉 UID 列表,逗号分隔 (必填)")
permissionAddCmd.Flags().String("policy", "", "权限类型: 0=管理员, 1=所有者, 2=可编辑, 3=可查看/下载, 4=仅查看 (必填)")
permissionAddCmd.Flags().Bool("cover", false, "是否覆盖已有权限 (可选,默认 false)")
permissionAddCmd.Flags().String("sub-resources", "", "权限子模块,逗号分隔: OrigContent/Summary/Analysis/Note (可选)")
// permission remove — 对应 MCP 工具 remove_member_permission
// 批量移除多个听记的成员权限。
// 必填参数:--ids(听记 taskUuid 列表)、--member-uids(成员钉钉 UID 列表)
permissionRemoveCmd := &cobra.Command{
Use: "remove",
Short: "批量移除听记成员权限",
Long: `批量移除多个听记的成员权限。移除后,对应成员将失去对这些听记的访问权限。`,
Example: ` dws minutes permission remove --ids <uuid1,uuid2> --member-uids 123456,789012
dws minutes permission remove --ids <uuid> --member-uids 123456`,
RunE: func(cmd *cobra.Command, args []string) error {
v := flagOrFallback(cmd, "ids", "uuids", "task-uuids")
if v == "" {
return fmt.Errorf("flag --ids (or --uuids / --task-uuids) is required")
}
if err := validateRequiredFlags(cmd, "member-uids"); err != nil {
return err
}
memberUids := parseCSVValues(mustGetFlag(cmd, "member-uids"))
for _, uid := range memberUids {
if strings.TrimSpace(uid) == "" {
return fmt.Errorf("flag --member-uids contains empty UID")
}
}
return callMCPTool("remove_member_permission", map[string]any{
"uuids": parseCSVValues(v),
"memberUids": memberUids,
})
},
}
permissionRemoveCmd.Flags().String("ids", "", "听记 taskUuid 列表,逗号分隔 (必填)")
permissionRemoveCmd.Flags().String("uuids", "", "--ids 的别名")
_ = permissionRemoveCmd.Flags().MarkHidden("uuids")
permissionRemoveCmd.Flags().String("task-uuids", "", "--ids 的别名")
_ = permissionRemoveCmd.Flags().MarkHidden("task-uuids")
permissionRemoveCmd.Flags().String("member-uids", "", "成员钉钉 UID 列表,逗号分隔 (必填)")
permissionCmd.AddCommand(permissionAddCmd, permissionRemoveCmd)
// ── tag 子组 ────────────────────────────────────────────────
// 听记标签/分组管理:查询用户标签列表、按标签查询听记。
// 对应 MCP 工具 query_user_tag_list / query_minutes_by_tag_id。
// 标签/分组由用户在听记页面手动创建,此处仅提供查询能力。
tagCmd := &cobra.Command{Use: "tag", Short: "听记标签/分组管理", RunE: groupRunE}
// tag list — 对应 MCP 工具 query_user_tag_list
// 无需传入参数,系统自动识别当前用户身份。
// 返回用户在听记页面创建的所有标签/分组列表,每条记录包含 tagId 和标签名称。
tagListCmd := &cobra.Command{
Use: "list",
Short: "查询我的听记标签/分组列表",
Long: `查询当前用户的听记标签或分组列表。
标签/分组在听记页面手动创建,此命令用于查看已有标签。
无需传入额外参数,系统自动识别当前用户身份。
返回所有标签/分组的列表,每条记录包含 tagId 和标签名称。
获取到 tagId 后,可使用 dws minutes tag query --tag-id <tagId> 查询该标签下的听记列表。`,
Example: ` dws minutes tag list`,
RunE: func(cmd *cobra.Command, args []string) error {
return callMCPTool("query_user_tag_list", map[string]any{})
},
}
// tag query — 对应 MCP 工具 query_minutes_by_tag_id
// 根据用户的标签/分组 ID 查询该标签下的听记列表。
// 必填参数:tagId(--tag-id)
// 可选参数:maxResults(--limit), nextToken(--cursor)
// tagId 可通过 dws minutes tag list 获取。
tagQueryCmd := &cobra.Command{
Use: "query",
Short: "根据标签ID查询听记列表",
Long: `根据用户的标签或分组 ID 查询该标签下的听记列表。
标签/分组在听记页面手动创建,tagId 可通过 dws minutes tag list 获取。
支持分页查询,使用 --limit 控制每页数量,--cursor 传入分页 token。`,
Example: ` dws minutes tag query --tag-id <tagId>
dws minutes tag query --tag-id <tagId> --limit 20
dws minutes tag query --tag-id <tagId> --limit 10 --cursor <nextToken>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "tag-id"); err != nil {
return err
}
toolArgs := map[string]any{
"tagId": mustGetFlag(cmd, "tag-id"),
}
if cmd.Flags().Changed("limit") {
limit, _ := cmd.Flags().GetFloat64("limit")
toolArgs["maxResults"] = limit
}
if v := flagOrFallback(cmd, "cursor", "next-token"); v != "" {
toolArgs["nextToken"] = v
}
return callMCPTool("query_minutes_by_tag_id", toolArgs)
},
}
tagQueryCmd.Flags().String("tag-id", "", "标签/分组 ID,可通过 tag list 获取 (必填)")
tagQueryCmd.Flags().Float64("limit", 10, "每页数据条数 (默认 10)")
tagQueryCmd.Flags().String("cursor", "", "分页 token (首页留空)")
tagQueryCmd.Flags().String("next-token", "", "--cursor 的别名 (兼容旧版)")
_ = tagQueryCmd.Flags().MarkHidden("next-token")
tagCmd.AddCommand(tagListCmd, tagQueryCmd)
minutesCmd := &cobra.Command{
Use: "minutes",
Short: "AI 听记 / 会议纪要",
Long: `管理钉钉AI听记:查询列表、获取详情、摘要、转写、待办、关键字、音频地址、思维导图、发言人管理、文件上传、成员权限管理,以及修改标题和纪要内容。`,
RunE: groupRunE,
}
minutesCmd.AddCommand(minutesListCmd, minutesGetCmd, minutesUpdateCmd, minutesRecordCmd, mindGraphCmd, speakerCmd, hotWordCmd, replaceTextCmd, uploadCmd, permissionCmd, tagCmd)
return minutesCmd
}
// callListByKeywordRange 调用 list_by_keyword_and_time_range,
// mine/shared/all 统一入口,通过 belongingConditionId 区分(created / shared / noLimit)。
func callListByKeywordRange(cmd *cobra.Command, filterType string) error {
toolArgs := map[string]any{
"belongingConditionId": filterType,
}
limit, _ := cmd.Flags().GetFloat64("limit")
if !cmd.Flags().Changed("limit") {
if maxVal, _ := cmd.Flags().GetFloat64("max"); cmd.Flags().Changed("max") {
limit = maxVal
}
}
toolArgs["maxResults"] = limit
startStr, _ := cmd.Flags().GetString("start")
endStr, _ := cmd.Flags().GetString("end")
if startStr == "" && endStr == "" {
// no time range specified — omit time filters
} else {
var startMs, endMs int64
if startStr != "" {
var err error
startMs, err = parseISOTimeToMillis("start", startStr)
if err != nil {
return err
}
toolArgs["createTimeStart"] = float64(startMs)
}
if endStr != "" {
var err error
endMs, err = parseISOTimeToMillis("end", endStr)
if err != nil {
return err
}
toolArgs["createTimeEnd"] = float64(endMs)
}
if startStr != "" && endStr != "" {
if err := validateTimeRange(startMs, endMs); err != nil {
return err
}
}
}
if v := flagOrFallback(cmd, "query", "keyword"); v != "" {
toolArgs["keyword"] = v
}
if v := flagOrFallback(cmd, "cursor", "next-token", "offset"); v != "" {
toolArgs["nextToken"] = v
}
return callMCPToolOnServer("minutes", "list_by_keyword_and_time_range", toolArgs)
}