From a65d6f23ec7995c7a2aff44abed14f4b7871a7ed Mon Sep 17 00:00:00 2001 From: qinze Date: Tue, 7 Jul 2026 23:26:33 +0800 Subject: [PATCH] fix: resolve real-machine QA findings across CLI, scripts and skill docs Fix CLI command bugs surfaced by full real-machine QA: - aitable: make chart/dashboard share update --enabled a string flag so "--enabled false" disables instead of silently enabling (bool flag + space-syntax help example inverted the action); clarify chart update requires --config; make form get filter by view-id client-side so it returns a single form; drop inline // comments from chart JSON examples - chat: resolve conversation-info --user to openDingTalkId, register --id/--conversation-id/--chat aliases; cap list-all-conversations --limit at 100 and reject larger values instead of silent truncation; detect webhook errcode failures instead of wrapping them as success; remove duplicate group/members subcommand registration in help - contact: register --dept/--depts as the primary dept flags to match the RunE parsing (were only registered as --id/--ids) - sheet: emit clean JSON for media-upload and export under --format json, suppressing progress lines that leaked onto stdout - wiki: correct node create --type enum (drop unsupported asheet, add axls/able/appt/adraw/amind) - ding: default message list --type to ALL since the server rejects an empty type Fix helper scripts (mono and multi): - aitable import/export flag names and tableId length regex - mail search --limit, contact dept response keys and userInfo nesting - attendance_my_record whoami compatibility, calendar_schedule_meeting event id unwrapping, drive_tree_list recursion via fileId, report scripts migrated off deprecated report list/detail Sync skill docs (mono and multi) to real-machine behavior across all products: command indexes, flag names, enums, return-structure keys, and cross-product intent routing; annotate genuinely server-side limitations and the no-op global --jq/--fields flags. --- internal/helpers/aitable.go | 84 +- internal/helpers/chat.go | 74 +- internal/helpers/contact.go | 14 +- internal/helpers/ding.go | 14 +- internal/helpers/sheet_chart.go | 6 +- internal/helpers/sheet_export.go | 54 +- internal/helpers/sheet_media.go | 26 +- internal/helpers/wiki.go | 13 +- skills/mono/SKILL.md | 5 +- .../mono/references/best_practices/02-task.md | 8 +- .../best_practices/06-data-analytics.md | 2 +- .../mono/references/best_practices/09-mail.md | 14 +- .../references/best_practices/lite-recipes.md | 8 +- skills/mono/references/capability-limits.md | 2 +- skills/mono/references/global-reference.md | 4 +- skills/mono/references/intent-guide.md | 11 +- skills/mono/references/products/aitable.md | 24 +- .../products/aitable/aitable-attachment.md | 6 +- .../aitable/aitable-dashboard-chart.md | 15 +- .../products/aitable/aitable-export-import.md | 14 +- .../products/aitable/aitable-form.md | 8 +- .../products/aitable/aitable-primary-doc.md | 4 +- .../aitable/aitable-record-history.md | 25 +- .../products/aitable/aitable-view-config.md | 18 +- .../products/aitable/aitable-view-extras.md | 4 +- .../references/products/attendance-report.md | 12 +- skills/mono/references/products/attendance.md | 60 +- skills/mono/references/products/calendar.md | 1 + skills/mono/references/products/chat.md | 259 +++- skills/mono/references/products/contact.md | 15 +- skills/mono/references/products/dev.md | 11 +- skills/mono/references/products/ding.md | 10 +- skills/mono/references/products/doc.md | 193 ++- .../references/products/doc/doc-file-ops.md | 7 +- .../mono/references/products/doc/doc-list.md | 60 +- .../references/products/doc/doc-search.md | 82 +- skills/mono/references/products/drive.md | 2 + skills/mono/references/products/mail.md | 153 +-- skills/mono/references/products/minutes.md | 18 +- skills/mono/references/products/oa.md | 38 +- skills/mono/references/products/report.md | 10 +- skills/mono/references/products/sheet.md | 236 ++-- .../references/products/sheet/sheet-chart.md | 3 + .../sheet/sheet-conditional-format.md | 7 +- .../products/sheet/sheet-dropdown.md | 8 +- .../references/products/sheet/sheet-export.md | 10 +- .../references/products/sheet/sheet-filter.md | 6 +- .../products/sheet/sheet-media-image.md | 6 +- .../products/sheet/sheet-range-operations.md | 4 +- .../products/sheet/sheet-style-format.md | 4 +- .../products/sheet/sheet-workbook.md | 18 +- .../products/sheet/sheet-write-data.md | 4 +- skills/mono/references/products/todo.md | 44 +- skills/mono/references/products/wiki.md | 11 +- skills/mono/references/url-patterns.md | 4 +- .../mono/scripts/aitable_export_via_task.py | 12 +- .../mono/scripts/aitable_import_via_task.py | 6 +- skills/mono/scripts/bulk_add_fields.py | 2 +- .../mono/scripts/calendar_schedule_meeting.py | 6 +- skills/mono/scripts/contact_dept_members.py | 49 +- skills/mono/scripts/drive_tree_list.py | 37 +- skills/mono/scripts/import_records.py | 2 +- skills/mono/scripts/mail_unread_summary.py | 2 +- skills/mono/scripts/report_inbox_today.py | 150 ++- skills/mono/scripts/report_received_today.py | 164 +-- .../references/06-data-analytics.md | 2 +- .../dingtalk-aitable/references/aitable.md | 36 +- .../references/aitable/aitable-attachment.md | 6 +- .../aitable/aitable-dashboard-chart.md | 15 +- .../aitable/aitable-export-import.md | 14 +- .../references/aitable/aitable-form.md | 8 +- .../references/aitable/aitable-primary-doc.md | 4 +- .../aitable/aitable-record-history.md | 25 +- .../references/aitable/aitable-view-config.md | 18 +- .../references/aitable/aitable-view-extras.md | 4 +- .../scripts/aitable_export_via_task.py | 12 +- .../scripts/aitable_import_via_task.py | 6 +- .../scripts/bulk_add_fields.py | 2 +- .../scripts/import_records.py | 2 +- skills/multi/dingtalk-attendance/SKILL.md | 11 +- .../references/attendance-report.md | 619 ++++++++++ .../references/attendance-schedule.md | 590 +++++++++ .../references/attendance-vacation.md | 140 +++ .../references/attendance.md | 58 +- .../scripts/attendance_my_record.py | 20 +- .../scripts/attendance_report_checkin.py | 452 +++++++ .../scripts/attendance_report_record.py | 947 +++++++++++++++ skills/multi/dingtalk-calendar/SKILL.md | 8 +- .../references/03-meeting.md | 2 +- .../dingtalk-calendar/references/calendar.md | 1 + skills/multi/dingtalk-chat/SKILL.md | 8 +- .../dingtalk-chat/references/01-messaging.md | 2 +- skills/multi/dingtalk-chat/references/chat.md | 481 +++++++- .../dingtalk-contact/references/contact.md | 15 +- .../scripts/contact_dept_members.py | 34 +- skills/multi/dingtalk-dev/SKILL.md | 4 +- skills/multi/dingtalk-dev/references/event.md | 4 +- skills/multi/dingtalk-ding/references/ding.md | 6 +- skills/multi/dingtalk-doc/SKILL.md | 7 +- skills/multi/dingtalk-doc/references/doc.md | 193 ++- .../references/doc/doc-file-ops.md | 7 +- .../dingtalk-doc/references/doc/doc-list.md | 60 +- .../dingtalk-doc/references/doc/doc-search.md | 82 +- .../dingtalk-doc/references/url-patterns.md | 6 +- skills/multi/dingtalk-drive/SKILL.md | 29 +- .../multi/dingtalk-drive/references/drive.md | 542 ++++++--- .../dingtalk-drive/scripts/drive_tree_list.py | 37 +- skills/multi/dingtalk-mail/SKILL.md | 2 +- .../multi/dingtalk-mail/references/09-mail.md | 43 +- skills/multi/dingtalk-mail/references/mail.md | 1054 ++++++++++++++++- .../scripts/mail_send_with_cc.py | 2 +- .../scripts/mail_unread_summary.py | 2 +- .../dingtalk-minutes/references/minutes.md | 18 +- skills/multi/dingtalk-oa/references/oa.md | 159 ++- skills/multi/dingtalk-report/SKILL.md | 18 +- .../references/05-reporting.md | 8 +- .../dingtalk-report/references/report.md | 10 +- .../scripts/report_received_today.py | 164 +-- skills/multi/dingtalk-sheet/SKILL.md | 2 +- .../multi/dingtalk-sheet/references/sheet.md | 236 ++-- .../sheet/sheet-batch-operations.md | 246 ++++ .../references/sheet/sheet-chart.md | 354 ++++++ .../sheet/sheet-conditional-format.md | 7 +- .../references/sheet/sheet-dropdown.md | 8 +- .../references/sheet/sheet-export.md | 10 +- .../references/sheet/sheet-filter.md | 6 +- .../references/sheet/sheet-media-image.md | 6 +- .../sheet/sheet-range-operations.md | 4 +- .../references/sheet/sheet-style-format.md | 4 +- .../references/sheet/sheet-workbook.md | 18 +- .../references/sheet/sheet-write-data.md | 4 +- skills/multi/dingtalk-skill/SKILL.md | 4 +- .../multi/dingtalk-skill/references/skill.md | 9 +- skills/multi/dingtalk-todo/SKILL.md | 2 +- .../multi/dingtalk-todo/references/02-task.md | 8 +- skills/multi/dingtalk-todo/references/todo.md | 83 +- skills/multi/dingtalk-wiki/SKILL.md | 31 +- skills/multi/dingtalk-wiki/references/wiki.md | 257 +++- 138 files changed, 7767 insertions(+), 1714 deletions(-) create mode 100644 skills/multi/dingtalk-attendance/references/attendance-report.md create mode 100644 skills/multi/dingtalk-attendance/references/attendance-schedule.md create mode 100644 skills/multi/dingtalk-attendance/references/attendance-vacation.md create mode 100644 skills/multi/dingtalk-attendance/scripts/attendance_report_checkin.py create mode 100644 skills/multi/dingtalk-attendance/scripts/attendance_report_record.py create mode 100644 skills/multi/dingtalk-sheet/references/sheet/sheet-batch-operations.md create mode 100644 skills/multi/dingtalk-sheet/references/sheet/sheet-chart.md diff --git a/internal/helpers/aitable.go b/internal/helpers/aitable.go index e246cdfa..05e25e4a 100644 --- a/internal/helpers/aitable.go +++ b/internal/helpers/aitable.go @@ -5,12 +5,26 @@ import ( "encoding/json" "fmt" "os" + "strconv" "strings" "time" "github.com/spf13/cobra" ) +// parseBoolFlag reads a string flag and parses it as a boolean, accepting the +// natural `--flag true` / `--flag false` space syntax. Registering these +// switches as String (not Bool) flags avoids cobra swallowing `false` as a +// positional arg — which silently inverted `--enabled false` into enable. +func parseBoolFlag(cmd *cobra.Command, name string) (bool, error) { + raw := strings.TrimSpace(mustGetFlag(cmd, name)) + v, err := strconv.ParseBool(raw) + if err != nil { + return false, fmt.Errorf("--%s 只接受 true 或 false,got %q", name, raw) + } + return v, nil +} + // ────────────────────────────────────────────────────────── // dws aitable — AI 表格 // 中文 tool name 映射: CLI 英文命令 → MCP 中文 tool name @@ -617,6 +631,30 @@ func printViewSubBlock(block any) error { return deps.Out.PrintJSON(envelope) } +// findFormViewByID 从 list_form_views 响应里递归定位 viewId 匹配的表单对象。 +// 服务端的 viewIds 过滤参数当前不生效(返回全表所有表单),故 form get 在客户端 +// 侧按 viewId 精确筛出单条,避免退化成等价 form list。 +func findFormViewByID(node any, viewID string) (map[string]any, bool) { + switch v := node.(type) { + case map[string]any: + if id, _ := v["viewId"].(string); id == viewID { + return v, true + } + for _, val := range v { + if found, ok := findFormViewByID(val, viewID); ok { + return found, true + } + } + case []any: + for _, el := range v { + if found, ok := findFormViewByID(el, viewID); ok { + return found, true + } + } + } + return nil, false +} + // collectStringFlag 把 cmd 上 flagName 的字符串值(若非空)以 jsonKey 写入 out map。 func collectStringFlag(cmd *cobra.Command, flagName, jsonKey string, out map[string]any) { if v, _ := cmd.Flags().GetString(flagName); v != "" { @@ -2888,7 +2926,8 @@ locked 为 true 表示视图已锁定,false 表示未锁定。`, Use: "get", Short: "获取单个表单视图详情", Long: `按 viewId 获取单个表单视图的元信息(name/title/createdAt/shareFormUuid)。 -内部基于 list_form_views 过滤实现,等价于 form list 后筛选指定 viewId。`, +内部拉取 list_form_views 后在客户端按 viewId 精确筛出单条(服务端的 viewIds +过滤参数当前不生效,会返回全表所有表单,故在此侧过滤)。data 即命中的表单对象。`, Example: ` dws aitable form get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "table-id", "view-id"); err != nil { @@ -2898,11 +2937,30 @@ locked 为 true 表示视图已锁定,false 表示未锁定。`, if err != nil { return err } - return callAitableHelperTool("list_form_views", map[string]any{ + viewID := mustGetFlag(cmd, "view-id") + raw, err := callMCPToolReturnTextOnServer(context.Background(), "aitable-helper", "list_form_views", map[string]any{ "baseId": baseID, "tableId": mustGetFlag(cmd, "table-id"), - "viewIds": []string{mustGetFlag(cmd, "view-id")}, }) + if err != nil { + return err + } + var parsed any + if e := json.Unmarshal([]byte(raw), &parsed); e != nil { + return &CLIError{ + Code: CodeMCPToolError, + Message: fmt.Sprintf("list_form_views response is not valid JSON: %v", e), + } + } + form, ok := findFormViewByID(parsed, viewID) + if !ok { + return &CLIError{ + Code: CodeMCPToolError, + Message: fmt.Sprintf("form view %s not found in table", viewID), + Suggestion: "用 dws aitable form list --base-id --table-id 查看可用 viewId 列表", + } + } + return printViewSubBlock(form) }, } @@ -3439,10 +3497,10 @@ layout 数组里每项含图表的新位置(row/col/width/height)。`, Example: ` dws aitable dashboard share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --enabled true --share-type PUBLIC dws aitable dashboard share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --enabled false`, RunE: func(cmd *cobra.Command, args []string) error { - if err := validateRequiredFlags(cmd, "dashboard-id"); err != nil { + if err := validateRequiredFlags(cmd, "dashboard-id", "enabled"); err != nil { return err } - enabled, err := cmd.Flags().GetBool("enabled") + enabled, err := parseBoolFlag(cmd, "enabled") if err != nil { return err } @@ -3542,11 +3600,15 @@ layout 数组里每项含图表的新位置(row/col/width/height)。`, Use: "update", Short: "更新图表", Long: `更新指定 chart 的配置或布局。 -调用前建议先调用 chart widgets-example 了解 --config 入参结构和要求。`, +--config 为必填参数,即使只想改布局也要带上完整的图表配置(服务端会拒绝缺 config 的请求, +空对象 {} 同样被拒,至少要包含 chartName)。--layout 可选,仅调整位置/大小时追加。 +调用前建议先调用 chart widgets-example 了解 --config 入参结构和要求, +或先用 chart get 拿到当前 config 再改。`, Example: ` dws aitable chart update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID \ --config '{"chartName":"新柱图名",...}' + # 只改布局也必须带 --config(用 chart get 拿到当前 config 原样回传): dws aitable chart update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID \ - --layout '{"x":0,"y":4,"w":12,"h":4}' + --config '{"chartName":"柱图",...}' --layout '{"x":0,"y":4,"w":12,"h":4}' # 查询 chartId: dws aitable dashboard get --base-id --dashboard-id `, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "dashboard-id", "chart-id", "config"); err != nil { @@ -3634,10 +3696,10 @@ layout 数组里每项含图表的新位置(row/col/width/height)。`, Example: ` dws aitable chart share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID --enabled true --share-type ORG dws aitable chart share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID --enabled false`, RunE: func(cmd *cobra.Command, args []string) error { - if err := validateRequiredFlags(cmd, "dashboard-id", "chart-id"); err != nil { + if err := validateRequiredFlags(cmd, "dashboard-id", "chart-id", "enabled"); err != nil { return err } - enabled, err := cmd.Flags().GetBool("enabled") + enabled, err := parseBoolFlag(cmd, "enabled") if err != nil { return err } @@ -4658,7 +4720,7 @@ parentSectionId 为空串表示该节点在 Base 根目录下。 dashboardShareGetCmd.Flags().String("dashboard-id", "", "目标 Dashboard ID (必填)") dashboardShareUpdateCmd.Flags().String("base-id", "", "所属 Base ID (必填)") dashboardShareUpdateCmd.Flags().String("dashboard-id", "", "目标 Dashboard ID (必填)") - dashboardShareUpdateCmd.Flags().Bool("enabled", false, "分享开关:true 开启,false 关闭") + dashboardShareUpdateCmd.Flags().String("enabled", "", "分享开关:true 开启,false 关闭 (必填)") dashboardShareUpdateCmd.Flags().String("share-type", "", "分享类型:PUBLIC 或 ORG(enabled=true 时生效)") dashboardShareUpdateCmd.Flags().Bool("allow-back-to-doc", false, "是否允许从分享页返回源 AI 表格(仅在显式传参时生效)") dashboardShareCmd.AddCommand(dashboardShareGetCmd, dashboardShareUpdateCmd) @@ -4692,7 +4754,7 @@ parentSectionId 为空串表示该节点在 Base 根目录下。 chartShareUpdateCmd.Flags().String("base-id", "", "所属 Base ID (必填)") chartShareUpdateCmd.Flags().String("dashboard-id", "", "所属 Dashboard ID (必填)") chartShareUpdateCmd.Flags().String("chart-id", "", "目标 Chart ID (必填)") - chartShareUpdateCmd.Flags().Bool("enabled", false, "分享开关:true 开启,false 关闭") + chartShareUpdateCmd.Flags().String("enabled", "", "分享开关:true 开启,false 关闭 (必填)") chartShareUpdateCmd.Flags().String("share-type", "", "分享类型:PUBLIC 或 ORG(enabled=true 时生效)") chartShareUpdateCmd.Flags().Bool("allow-back-to-doc", false, "是否允许从分享页返回源 AI 表格(仅在显式传参时生效)") chartShareCmd.AddCommand(chartShareGetCmd, chartShareUpdateCmd) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 4af8ed5c..8cb9ff1c 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -202,6 +202,28 @@ func isOpenDingTalkID(value string) bool { 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) @@ -1841,7 +1863,30 @@ func newChatCommand() *cobra.Command { } toolArgs["atUserIds"] = atUserIds } - return callMCPToolOnServer("bot", "send_message_by_custom_robot", toolArgs) + // 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 }, } @@ -2666,7 +2711,14 @@ func newChatCommand() *cobra.Command { toolArgs["openConversationId"] = groupID } if userID != "" { - toolArgs["userId"] = 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 @@ -2675,6 +2727,12 @@ func newChatCommand() *cobra.Command { }, } 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") @@ -4375,7 +4433,7 @@ status 可选值: chatListAllConversationsCmd := &cobra.Command{ Use: "list-all-conversations", Short: "分页获取当前用户的全部会话列表", - Long: `分页获取当前用户的全部会话列表(包含单聊和群聊)。--limit 指定每页数量(最大 100),--cursor 传分页游标(首次不传或传 0)。 + Long: `分页获取当前用户的全部会话列表(包含单聊和群聊)。--limit 指定每页数量(1-100,默认 100),--cursor 传分页游标(首次不传或传 0)。 返回 hasMore=true 时用 nextCursor 作为下次 --cursor 继续翻页。`, Example: ` dws chat list-all-conversations dws chat list-all-conversations --limit 50 @@ -4384,6 +4442,10 @@ status 可选值: 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 { @@ -4395,7 +4457,7 @@ status 可选值: return callMCPToolOnServer("im", "list_all_conversations", toolArgs) }, } - chatListAllConversationsCmd.Flags().Int("limit", 1000, "每页数量(默认 1000)") + chatListAllConversationsCmd.Flags().Int("limit", 100, "每页数量(1-100,默认 100)") chatListAllConversationsCmd.Flags().Int64("cursor", 0, "分页游标(首次不传或传 0,翻页传 nextCursor)") chatListAllConversationsCmd.Flags().Bool("exclude-muted", false, "是否排除已免打扰会话(默认 false)") @@ -4515,8 +4577,8 @@ status 可选值: chatMessageUnsetTopMsgCmd.Flags().String("msg-id", "", "消息 openMessageId (必填)") _ = chatMessageUnsetTopMsgCmd.MarkFlagRequired("msg-id") - chatGroupCmd.AddCommand(chatGroupBotsCmd, chatGroupDismissCmd, chatGroupSetHistoryCmd, chatGroupListMyGroupsCmd) - chatGroupMembersCmd.AddCommand(chatGroupMembersRemoveBotCmd) + // group 与 members 的子命令在下方(chatGroupCmd.AddCommand / chatGroupMembersCmd.AddCommand + // 的完整列表处)统一注册;此处不再重复 AddCommand,否则 --help 会重复列出。 // ── group update-nick: 设置用户在群内的群昵称 ────────────── chatGroupUpdateNickCmd := &cobra.Command{ diff --git a/internal/helpers/contact.go b/internal/helpers/contact.go index f21a08ac..3a173690 100644 --- a/internal/helpers/contact.go +++ b/internal/helpers/contact.go @@ -582,9 +582,11 @@ contact user profile fields 获取可用字段列表。 contactDeptSearchCmd.Flags().String("name", "", "--query 的别名") _ = contactDeptSearchCmd.Flags().MarkHidden("keyword") _ = contactDeptSearchCmd.Flags().MarkHidden("name") - contactDeptGetInfoCmd.Flags().String("id", "", "部门 ID (必填)") - contactDeptListChildrenCmd.Flags().String("id", "", "部门 ID (必填)") - contactDeptListMembersCmd.Flags().String("ids", "", "部门 ID 列表 (必填)") + // 主 flag 与 RunE 读取保持一致:get-info / list-children 用 --dept,list-members 用 --depts。 + // 历史上主 flag 曾误注册为 --id/--ids,导致 RunE 读的 --dept/--depts 未注册、命令行传入报 unknown flag。 + contactDeptGetInfoCmd.Flags().String("dept", "", "部门 ID (必填)") + contactDeptListChildrenCmd.Flags().String("dept", "", "部门 ID (必填)") + contactDeptListMembersCmd.Flags().String("depts", "", "部门 ID 列表 (必填)") // dept 系列命令统一接受 --id / --ids / --dept-id / --dept-ids 别名(集中注册避免逐命令重复写)。 // camelCase --deptId / --deptIds 由 RegisterCamelCaseAliases 自动派生,无需手写。 @@ -593,9 +595,9 @@ contact user profile fields 获取可用字段列表。 aliases []string } for _, s := range []deptIDAliasSpec{ - {contactDeptGetInfoCmd, []string{"dept-id", "ids", "dept-ids"}}, - {contactDeptListChildrenCmd, []string{"ids", "dept-id", "dept-ids"}}, - {contactDeptListMembersCmd, []string{"id", "dept-id", "dept-ids"}}, + {contactDeptGetInfoCmd, []string{"id", "dept-id", "ids", "dept-ids"}}, + {contactDeptListChildrenCmd, []string{"id", "ids", "dept-id", "dept-ids"}}, + {contactDeptListMembersCmd, []string{"ids", "id", "dept-id", "dept-ids"}}, } { for _, name := range s.aliases { if s.cmd.Flags().Lookup(name) != nil { diff --git a/internal/helpers/ding.go b/internal/helpers/ding.go index 98a8043b..59fd75f0 100644 --- a/internal/helpers/ding.go +++ b/internal/helpers/ding.go @@ -93,8 +93,9 @@ func newDingCommand() *cobra.Command { Use: "list", Short: "查询 DING 消息历史", Long: `查询当前用户的 DING 消息列表,支持按类型过滤。 ---type 支持: ALL(全部)、UNREAD(未读)、SEND(已发)、NEW_COMMENT(新评论)、DELETED(已删除)。`, - Example: ` dws ding message list +--type 支持: ALL(全部)、UNREAD(未读)、SEND(已发)、NEW_COMMENT(新评论)、DELETED(已删除)。 +--type 为服务端必填字段,空值会报「type不能为空」;不传时 CLI 默认按 ALL 查询。`, + Example: ` dws ding message list # 默认 --type ALL dws ding message list --type UNREAD dws ding message list --type SEND --cursor 10`, RunE: func(cmd *cobra.Command, args []string) error { @@ -102,9 +103,12 @@ func newDingCommand() *cobra.Command { if v, _ := cmd.Flags().GetInt64("cursor"); v > 0 { toolArgs["cursor"] = v } - if v, _ := cmd.Flags().GetString("type"); v != "" { - toolArgs["type"] = v + // type 是服务端必填,空值会报错;不传或传空时兜底为 ALL。 + t, _ := cmd.Flags().GetString("type") + if t == "" { + t = "ALL" } + toolArgs["type"] = t return callMCPToolOnServer("im", "list_ding_messages", toolArgs) }, } @@ -210,7 +214,7 @@ func newDingCommand() *cobra.Command { dingMessageRecallCmd.Flags().String("robot-code", "", "机器人 ID (必填,或设 DINGTALK_DING_ROBOT_CODE)") dingMessageRecallCmd.Flags().String("id", "", "DING 消息 ID (必填)") dingMessageListCmd.Flags().Int64("cursor", 0, "分页游标(首次传 0,翻页传返回的 nextCursor)") - dingMessageListCmd.Flags().String("type", "", "消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED(可选,不传返回全部)") + dingMessageListCmd.Flags().String("type", "ALL", "消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED(必填,服务端不接受空值;默认 ALL 全部)") dingMessageReceiverStatusCmd.Flags().String("ding-id", "", "DING 消息 openDingId (必填)") _ = dingMessageReceiverStatusCmd.MarkFlagRequired("ding-id") dingMessageSendPersonalCmd.Flags().String("users", "", "接收者 openDingTalkId 列表,逗号分隔 (必填)") diff --git a/internal/helpers/sheet_chart.go b/internal/helpers/sheet_chart.go index ccbac8aa..8ebfb2f5 100644 --- a/internal/helpers/sheet_chart.go +++ b/internal/helpers/sheet_chart.go @@ -172,7 +172,7 @@ position.col 使用列字母表示法(如 "A"、"AA"),不支持数字形 创建成功后返回新图表的完整信息,包含系统分配的 chart-id。`, Example: ` # 创建柱形图 dws sheet chart create --node NODE_ID --sheet-id SHEET_ID --properties '{ - "position": {"row": 12, "col": "A"}, // col 使用字母 + "position": {"row": 12, "col": "A"}, "dimensions": {"width": 600, "height": 400}, "chart": { "type": "column", @@ -187,7 +187,7 @@ position.col 使用列字母表示法(如 "A"、"AA"),不支持数字形 # 创建饼图 dws sheet chart create --node NODE_ID --sheet-id SHEET_ID --properties '{ - "position": {"row": 0, "col": "F"}, // col 使用字母 + "position": {"row": 0, "col": "F"}, "dimensions": {"width": 500, "height": 400}, "chart": { "type": "pie", @@ -243,7 +243,7 @@ position.col 使用列字母表示法(如 "A"、"AA"),不支持数字形 # 修改后整体回写 dws sheet chart update --node NODE_ID --sheet-id SHEET_ID --chart-id CHART_ID --properties '{ - "position": {"row": 12, "col": "A"}, // col 使用字母 + "position": {"row": 12, "col": "A"}, "dimensions": {"width": 800, "height": 500}, "chart": { "type": "line", diff --git a/internal/helpers/sheet_export.go b/internal/helpers/sheet_export.go index 91b7af56..e66c9fe9 100644 --- a/internal/helpers/sheet_export.go +++ b/internal/helpers/sheet_export.go @@ -31,8 +31,14 @@ func runSheetExport(cmd *cobra.Command, _ []string) error { ctx := context.Background() + // json 模式下进度提示会污染 stdout(PrintInfo/PrintKeyValue 都写 stdout), + // 使得 agent 无法按 JSON 解析。故 json 模式抑制进度、末尾统一输出结果 JSON。 + jsonMode := deps.Caller.Format() == "json" + // Step 1: submit export job - deps.Out.PrintInfo("[1/3] 提交表格导出任务 (xlsx)...") + if !jsonMode { + deps.Out.PrintInfo("[1/3] 提交表格导出任务 (xlsx)...") + } submitText, err := callMCPToolReturnText(ctx, "submit_export_job", map[string]any{ "nodeId": nodeID, "exportFormat": "xlsx", @@ -44,10 +50,11 @@ func runSheetExport(cmd *cobra.Command, _ []string) error { if err != nil { return err } - deps.Out.PrintInfo(fmt.Sprintf("导出任务已提交: jobId=%s", jobID)) - - // Step 2: progressive backoff polling - deps.Out.PrintInfo("[2/3] 轮询任务状态(渐进式退避,最多 30 次约 5 分钟)...") + if !jsonMode { + deps.Out.PrintInfo(fmt.Sprintf("导出任务已提交: jobId=%s", jobID)) + // Step 2: progressive backoff polling + deps.Out.PrintInfo("[2/3] 轮询任务状态(渐进式退避,最多 30 次约 5 分钟)...") + } downloadURL, err := pollSheetExportJob(ctx, jobID) if err != nil { return err @@ -55,6 +62,13 @@ func runSheetExport(cmd *cobra.Command, _ []string) error { // No output path: print the downloadUrl and exit if outputPath == "" { + if jsonMode { + return deps.Out.PrintJSON(map[string]any{ + "success": true, + "jobId": jobID, + "downloadUrl": downloadURL, + }) + } deps.Out.PrintKeyValue("jobId", jobID) deps.Out.PrintKeyValue("downloadUrl", downloadURL) deps.Out.PrintInfo("导出完成。downloadUrl 具有时效性,请尽快下载。") @@ -71,10 +85,20 @@ func runSheetExport(cmd *cobra.Command, _ []string) error { outputPath = filepath.Join(outputPath, filename) } - deps.Out.PrintInfo(fmt.Sprintf("[3/3] 下载 xlsx 到 %s ...", outputPath)) + if !jsonMode { + deps.Out.PrintInfo(fmt.Sprintf("[3/3] 下载 xlsx 到 %s ...", outputPath)) + } if err := httpGetFile(ctx, downloadURL, map[string]string{}, outputPath); err != nil { return fmt.Errorf("下载 xlsx 失败: %w", err) } + if jsonMode { + return deps.Out.PrintJSON(map[string]any{ + "success": true, + "jobId": jobID, + "outputPath": outputPath, + "downloadUrl": downloadURL, + }) + } deps.Out.PrintInfo(fmt.Sprintf("导出完成: %s", outputPath)) return nil } @@ -124,6 +148,8 @@ func exportPollIntervals() []time.Duration { // pollExportJob polls query_export_job per the progressive backoff schedule // until the job completes successfully, fails, or the 30-attempt cap is hit. func pollSheetExportJob(ctx context.Context, jobID string) (string, error) { + // json 模式下轮询进度也要抑制,否则 [INFO] 行会混进 stdout 破坏纯 JSON 输出。 + quiet := deps.Caller.Format() == "json" intervals := exportPollIntervals() for i, wait := range intervals { select { @@ -136,7 +162,9 @@ func pollSheetExportJob(ctx context.Context, jobID string) (string, error) { "jobId": jobID, }) if err != nil { - deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 查询失败,将继续轮询: %v", i+1, err)) + if !quiet { + deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 查询失败,将继续轮询: %v", i+1, err)) + } continue } @@ -149,7 +177,9 @@ func pollSheetExportJob(ctx context.Context, jobID string) (string, error) { normStatus := strings.ToUpper(strings.TrimSpace(status)) switch normStatus { case "SUCCESS": - deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: SUCCESS", i+1)) + if !quiet { + deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: SUCCESS", i+1)) + } if downloadURL == "" { return "", fmt.Errorf("任务成功但未返回 downloadUrl") } @@ -160,9 +190,13 @@ func pollSheetExportJob(ctx context.Context, jobID string) (string, error) { } return "", fmt.Errorf("%s", message) case "PROCESSING", "RUNNING", "DOING", "PENDING", "": - deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: PROCESSING", i+1)) + if !quiet { + deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: PROCESSING", i+1)) + } default: - deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: %s", i+1, status)) + if !quiet { + deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: %s", i+1, status)) + } } } return "", fmt.Errorf("导出任务超时:已轮询 30 次(约 5 分钟)仍未完成,请稍后再试") diff --git a/internal/helpers/sheet_media.go b/internal/helpers/sheet_media.go index 13f391d0..1c31baca 100644 --- a/internal/helpers/sheet_media.go +++ b/internal/helpers/sheet_media.go @@ -57,7 +57,13 @@ func runSheetMediaUpload(cmd *cobra.Command, _ []string) error { ctx := context.Background() - deps.Out.PrintInfo(fmt.Sprintf("[1/2] 获取附件上传凭证 (%s, %d bytes)...", fileName, fileSize)) + // json 模式下进度提示会污染 stdout(PrintInfo/PrintKeyValue 都写 stdout), + // 使得 agent 无法按 JSON 解析。故 json 模式抑制进度、末尾统一输出结果 JSON。 + jsonMode := deps.Caller.Format() == "json" + + if !jsonMode { + deps.Out.PrintInfo(fmt.Sprintf("[1/2] 获取附件上传凭证 (%s, %d bytes)...", fileName, fileSize)) + } result, err := deps.Caller.CallTool(ctx, "doc", "get_doc_attachment_upload_info", map[string]any{ "nodeId": nodeID, @@ -91,10 +97,11 @@ func runSheetMediaUpload(cmd *cobra.Command, _ []string) error { resourceURL, _ = credData["resourceUrl"].(string) } - deps.Out.PrintKeyValue("resourceId", resourceID) - deps.Out.PrintKeyValue("resourceUrl", resourceURL) - - deps.Out.PrintInfo("[2/2] 上传文件到 OSS...") + if !jsonMode { + deps.Out.PrintKeyValue("resourceId", resourceID) + deps.Out.PrintKeyValue("resourceUrl", resourceURL) + deps.Out.PrintInfo("[2/2] 上传文件到 OSS...") + } ossHeaders := map[string]string{ "Content-Type": mimeType, @@ -103,6 +110,15 @@ func runSheetMediaUpload(cmd *cobra.Command, _ []string) error { return err } + if jsonMode { + return deps.Out.PrintJSON(map[string]any{ + "success": true, + "resourceId": resourceID, + "resourceUrl": resourceURL, + "fileName": fileName, + "fileSize": fileSize, + }) + } deps.Out.PrintInfo(fmt.Sprintf("附件已上传: %s (resourceId=%s)", fileName, resourceID)) return nil } diff --git a/internal/helpers/wiki.go b/internal/helpers/wiki.go index cd3646d5..89b15833 100644 --- a/internal/helpers/wiki.go +++ b/internal/helpers/wiki.go @@ -580,16 +580,19 @@ ORG 类型授权不会出现在查询结果中。`, Short: "在知识库中创建节点", Long: `在指定知识库中创建文档、文件夹或其他类型的节点。 -通过 --type 指定节点类型: +通过 --type 指定节点类型(服务端支持以下值,asheet 不被支持): adoc 在线文档 (默认) - asheet 在线表格 - folder 文件夹 axls 在线电子表格 + able 多维表 + appt 在线演示 + adraw 白板/画板 + amind 脑图 + folder 文件夹 通过 --folder 指定父节点,不传则创建在知识库根目录。`, Example: ` dws wiki node create --workspace --name "新文档" dws wiki node create --workspace --name "方案目录" --type folder - dws wiki node create --workspace --name "数据表" --type asheet --folder `, + dws wiki node create --workspace --name "数据表" --type axls --folder `, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { @@ -616,7 +619,7 @@ ORG 类型授权不会出现在查询结果中。`, } nodeCreateCmd.Flags().String("workspace", "", "知识库 ID (必填)") nodeCreateCmd.Flags().String("name", "", "节点名称 (必填)") - nodeCreateCmd.Flags().String("type", "adoc", "节点类型: adoc / asheet / folder / axls") + nodeCreateCmd.Flags().String("type", "adoc", "节点类型: adoc / axls / able / appt / adraw / amind / folder(asheet 不支持)") nodeCreateCmd.Flags().String("folder", "", "父节点 nodeId (选填,不传则在根目录创建)") nodeCopyCmd := &cobra.Command{ diff --git a/skills/mono/SKILL.md b/skills/mono/SKILL.md index da1d2772..3d8d4832 100644 --- a/skills/mono/SKILL.md +++ b/skills/mono/SKILL.md @@ -43,7 +43,7 @@ cli_version: ">=1.0.15" | `oa` | OA审批:待处理/详情/同意/拒绝/撤销/记录/已发起/任务/转交/评论/抄送 | [oa.md](./references/products/oa.md) | | `report` | 日志:按模版创建/收件箱/已发送/模版查看/详情/已读统计 | [report.md](./references/products/report.md) | | `mail` | 邮箱:邮箱地址查询/邮件搜索(KQL)/邮件详情/发送邮件 | [mail.md](./references/products/mail.md) | -| `sheet` | 在线电子表格(axls):工作表 CRUD/区域读写/CSV 批量写入/行列增删/合并/查找替换/筛选视图/全局筛选/排序/下拉列表/浮动图片/导出(两步) | [sheet.md](./references/products/sheet.md) | +| `sheet` | 在线电子表格(axls):工作表 CRUD/区域读写/CSV 批量写入/行列增删/合并/查找替换/筛选视图/全局筛选/排序/下拉列表/条件格式/浮动图片/浮动图表/模板/导出 xlsx(单命令一站式) | [sheet.md](./references/products/sheet.md) | | `todo` | 待办:创建(含优先级/截止时间/循环)/查询/修改/标记完成/删除 | [todo.md](./references/products/todo.md) | | `wiki` | 知识库:空间创建/详情/列表/搜索 + 成员管理 | [wiki.md](./references/products/wiki.md) | @@ -130,7 +130,8 @@ dws --help # helper-only schema 查询(如 dev.*),普通产品命令不要依赖 schema 推断参数 dws schema "dev app create" -dws schema "dev app create" --jq '.tool.required' +# 注:--jq 对 schema 输出无效(不过滤,仍返回完整对象);schema 结构里必填标在 +# .parameters.<字段>.required,没有 .tool 键。要看必填字段自行读 .parameters 即可。 ``` **何时用哪条路径:** diff --git a/skills/mono/references/best_practices/02-task.md b/skills/mono/references/best_practices/02-task.md index 5c16b015..82aaf106 100644 --- a/skills/mono/references/best_practices/02-task.md +++ b/skills/mono/references/best_practices/02-task.md @@ -6,9 +6,9 @@ | Recipe | 步骤(命令均须 `--format json`,下略) | |--------|----------------------------------------| -| `create-priority-todo` | 1. 确定执行者(同 [SKILL.md](../../SKILL.md) 中 `create-todo` 步骤 1)
2. `todo task create --title "<标题>" --executors [,...] --priority <10/20/30/40>`(可选 `--due "<截止ISO>"`;10低/20普通/30较高/40紧急)→ 取 `todoTaskId` | -| `create-recurring-todo` | 1. 确定执行者(同 `create-todo` 步骤 1)
2. `todo task create --title "<标题>" --executors --due "<首次截止ISO>" --recurrence "DTSTART:\nRRULE:FREQ=DAILY;INTERVAL=1"`(`--due` 必填;仅支持按天循环,见 [todo.md](../products/todo.md))→ 取 `todoTaskId` | -| `reschedule-todo` | 1. `todo task list --status false` → 取 `todoTaskId`
2. `todo task update --task-id --due "<新截止时间>"` | +| `create-priority-todo` | 1. 确定执行者(同 [SKILL.md](../../SKILL.md) 中 `create-todo` 步骤 1)
2. `todo task create --title "<标题>" --executors [,...] --priority <10/20/30/40>`(可选 `--due "<截止ISO>"`;10低/20普通/30较高/40紧急)→ 取 `taskId` | +| `create-recurring-todo` | 1. 确定执行者(同 `create-todo` 步骤 1)
2. `todo task create --title "<标题>" --executors --due "<首次截止ISO>" --recurrence "DTSTART:\nRRULE:FREQ=DAILY;INTERVAL=1"`(`--due` 必填;仅支持按天循环,见 [todo.md](../products/todo.md))→ 取 `taskId` | +| `reschedule-todo` | 1. `todo task list --status false` → 取 `taskId`
2. `todo task update --task-id --due "<新截止时间>"` | ## Full / 组合(固定路线) @@ -16,4 +16,4 @@ |--------|---------------------| | generate-progress-report | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行
2. 交叉比对各源数据
3. `doc create --name "<报告名>" --content "<报告内容>"` | | batch-create-todo | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行 → 从结果提取任务条目
2. 每条:`aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`
3. **优先**:将待办写入 `todos.json`(格式见 [todo_batch_create.py](../../scripts/todo_batch_create.py)),执行 `python scripts/todo_batch_create.py todos.json`
备选:逐条 `todo task create --title "<标题>" --executors ` → 汇总回显
**单批超 30 条须用户确认** | -| assign-and-notify | 1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`
2. `todo task create --title "<标题>" --executors ` → 取 `todoTaskId`
3. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message send --group --text "<通知内容>"` 通知 | +| assign-and-notify | 1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`
2. `todo task create --title "<标题>" --executors ` → 取 `taskId`
3. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message send --group --text "<通知内容>"` 通知 | diff --git a/skills/mono/references/best_practices/06-data-analytics.md b/skills/mono/references/best_practices/06-data-analytics.md index 6b089723..3fd9b0bc 100644 --- a/skills/mono/references/best_practices/06-data-analytics.md +++ b/skills/mono/references/best_practices/06-data-analytics.md @@ -6,7 +6,7 @@ |--------|-------------------| | read-aitable | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`
2. `aitable field get --base-id --table-id ` → 取 `fieldId`
3. **取记录(按场景分流)**:
  • 数据统计/分析/全量汇总 → `aitable record query --base-id --table-id --all`(自动翻页,**禁止凭单页数据做统计**)
  • 大表保险 → 加 `--page-limit 100`(默认 50 页/5000 条,0 = 无限制)
  • 单纯预览前几条 → `aitable record query --base-id --table-id --limit 30`(不加 --all)
  • 筛选时 `--filters` 格式见 [aitable-filter-sort.md](../products/aitable/aitable-filter-sort.md)
4. **检查输出契约**:`hasMore=true` 时数据被截断,必须用 `--cursor ` 续拉;`partial=true` 时表示中途某页失败(保留已拉数据,可重试)
5. 总结数据 | | generate-data-report | 1. 同 read-aitable 步骤 1-3(**必须用 --all 防漏数据**)
2. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行 → 补充背景
3. `doc create --name "<报告名>" --content "<分析报告>"` | -| create-aitable-record | **写入路径分流**(关键决策):
  • **追加到已有表**(场景:把这批数据加到「成员表」)→ `python scripts/import_records.py data.csv\|data.json [batch_size]`(走 `record create`,需要列名匹配字段名)
  • **新建表**(场景:把这个 Excel 变成多维表)→ `python scripts/aitable_import_via_task.py ` 或 `dws aitable import upload --base-id --file ./x.xlsx` + `dws aitable import data --import-id `(CLI 已内置 OSS PUT 清空 Content-Type + 同步轮询,**禁止自己写 PUT**)
  • **单条/少量手动**:1. `aitable base search` → `baseId`/`tableId`;2. `aitable field get` → `fieldId` 与类型;3. `aitable record create --records '[{"cells":{"":"值"}}]'` | +| create-aitable-record | **写入路径分流**(关键决策):
  • **追加到已有表**(场景:把这批数据加到「成员表」)→ `python scripts/import_records.py data.csv\|data.json [batch_size]`(走 `record create`,需要列名匹配字段名)
  • **新建表**(场景:把这个 Excel 变成多维表)→ `python scripts/aitable_import_via_task.py `(脚本内置 prepare→OSS PUT→import data 全流程和正确的头处理,**禁止自己写 PUT**)。注意 `aitable import upload` 没有 `--file` flag、也不代做 PUT,只准备导入;能一站式完成的是上面的脚本
  • **单条/少量手动**:1. `aitable base search` → `baseId`/`tableId`;2. `aitable field get` → `fieldId` 与类型;3. `aitable record create --records '[{"cells":{"":"值"}}]'` | | update-aitable-record | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`
2. **取目标 record**:
  • 已知少量 recordId → `aitable record query --record-ids `
  • 按条件批量改 → `aitable record query --base-id --table-id --filters '' --all`(**用 --all 防止漏改**)
3. **先展示让用户确认要改的 record 列表**
4. `aitable record update --base-id --table-id --records '[{"recordId":"","cells":{...}}]'`(单次 ≤30 条) | | search-aitable-template | 1. `aitable template search --query "<关键词>"` → 取 `templateId`
2. 用户选定
3. `aitable base create --name "<表格名>" --template-id ` → 取 `baseId` | | export-aitable-to-xlsx | 1. `aitable base search --query "<表格名>"` → 取 `baseId`
2. **按场景选 scope**:
  • 全表+附件 → `aitable export data --base-id --scope all --export-format excel_and_attachment --output ./.xlsx`
  • 单表(仅 xlsx)→ `--scope table --table-id --export-format excel`
  • 单视图 → `--scope view --table-id --view-id `
3. CLI 内置渐进式退避轮询 + 自动落盘,**不要自己写 GET downloadUrl**
4. 大表超时(默认 5 分钟):加 `--timeout-sec 900` 或拿到 `taskId` 后 `aitable export data --task-id --output ./.xlsx` 续等
5. 与悟空脚本路径并存:复杂场景(多 base 批量 / 按视图组合)请用 `python scripts/aitable_export_via_task.py --scope all\|table\|view` | diff --git a/skills/mono/references/best_practices/09-mail.md b/skills/mono/references/best_practices/09-mail.md index eef111d8..9ab1f495 100644 --- a/skills/mono/references/best_practices/09-mail.md +++ b/skills/mono/references/best_practices/09-mail.md @@ -24,22 +24,22 @@ | Recipe | 步骤 | |--------|------| | `mail-get` | `mail message get --email <邮箱> --id ` → 查看邮件完整内容(含正文) | -| `mail-folder-list` | `mail folder list --email <邮箱>` → 列举文件夹;`--folder-id ` 查子文件夹 | +| `mail-folder-list` | `mail folder list --email <邮箱>` → 列举文件夹;`--folder ` 查子文件夹 | | `mail-tag-list` | `mail tag list --email <邮箱>` → 列举邮件标签 | | `mail-thread-get` | `mail thread get --email <邮箱> --id ` → 获取会话(邮件线程)详情 | | `mail-attachment-list` | `mail attachment list --email <邮箱> --id ` → 列举指定邮件的附件 | | `mail-attachment-download` | 1. `mail attachment list --email <邮箱> --id ` → 取附件 `id` 和 `name`
2. `mail attachment download --email <邮箱> --message-id --attachment-id --name <文件名>` | | `mail-batch-move` | `mail message batch-move --email <邮箱> --ids --folder `(常用 folderId: 2=收件箱, 6=已删除) | | `mail-batch-delete` | `mail message batch-delete --email <邮箱> --ids --yes`(**危险操作,须先确认**) | -| `mail-draft-create` | `mail draft create --from <邮箱> --subject "<标题>"` → 取 `messageId`(可选 `--to`、`--body`、`--cc`) | -| `mail-draft-update` | `mail draft update --from <邮箱> --id --subject "<新标题>"`(可选 `--body`、`--to`、`--cc`) | +| `mail-draft-create` | `mail draft create --from <邮箱> --subject "<标题>"` → 取草稿 ID `result.message.id`(可选 `--to`、`--content`、`--cc`) | +| `mail-draft-update` | `mail draft update --from <邮箱> --id --subject "<新标题>"`(可选 `--content`、`--to`、`--cc`) | | `mail-draft-send` | `mail draft send --from <邮箱> --id ` | ## Full / 多步组合 | Recipe | 行动指南(固定路线) | |--------|---------------------| -| search-and-download-attachment | 1. `mail mailbox list` → 取邮箱
2. `mail message search --email <邮箱> --query "" --size 20` → 取 `messageId` 列表
3. 对每封邮件执行 `mail attachment list --email <邮箱> --id ` → 列出附件取 `id` 和 `name`
4. 对每个附件逐个执行 `mail attachment download --email <邮箱> --message-id --attachment-id --name <文件名>`(**仅支持逐个下载,不存在批量下载命令**) | -| search-reply-forward | 1. `mail mailbox list` → 取邮箱
2. `mail message search --email <邮箱> --query "" --size 10` → 取 `messageId`
3. 展示搜索结果供用户选择
4. 按用户指示执行 reply / reply-all / forward(参见 lite `mail-reply-forward`) | -| batch-mail-cleanup | 1. `mail mailbox list` → 取邮箱
2. `mail message search --email <邮箱> --query "" --size 100` → 取多个 `messageId`
3. 展示列表供用户确认
4. `mail message batch-move --email <邮箱> --ids --folder 6 ` 移到已删除;或 `batch-delete` 永久删除 | -| send-to-person-by-name | 1. `mail mailbox list` → 取发件邮箱
2. 走「查找他人邮箱地址」三路并发查询获取收件人邮箱(见 [mail.md](../products/mail.md))
3. `mail message send --from <发件邮箱> --to <收件邮箱> --subject "<标题>" --body "<内容>"` | +| search-and-download-attachment | 1. `mail mailbox list` → 取邮箱
2. `mail message search --email <邮箱> --query "" --limit 20` → 取 `messageId` 列表
3. 对每封邮件执行 `mail attachment list --email <邮箱> --id ` → 列出附件取 `id` 和 `name`
4. 对每个附件逐个执行 `mail attachment download --email <邮箱> --message-id --attachment-id --name <文件名>`(**仅支持逐个下载,不存在批量下载命令**) | +| search-reply-forward | 1. `mail mailbox list` → 取邮箱
2. `mail message search --email <邮箱> --query "" --limit 10` → 取 `messageId`
3. 展示搜索结果供用户选择
4. 按用户指示执行 reply / reply-all / forward(参见 lite `mail-reply-forward`) | +| batch-mail-cleanup | 1. `mail mailbox list` → 取邮箱(返回字段是 `emailAccounts`)
2. `mail message search --email <邮箱> --query "" --limit 100` → 取多个 `messageId`
3. 展示列表供用户确认
4. `mail message batch-move --email <邮箱> --ids --folder 6 ` 或 `batch-delete` **都是移入「已删除」文件夹,并非物理永久删除**(对已在已删除文件夹的邮件再执行返回 success 但无效;CLI 无永久删除路径,需在客户端手动清空) | +| send-to-person-by-name | 1. `mail mailbox list` → 取发件邮箱
2. 走「查找他人邮箱地址」三路并发查询获取收件人邮箱(见 [mail.md](../products/mail.md))
3. `mail message send --from <发件邮箱> --to <收件邮箱> --subject "<标题>" --content "<内容>"` | diff --git a/skills/mono/references/best_practices/lite-recipes.md b/skills/mono/references/best_practices/lite-recipes.md index 3e6dc162..f83c086b 100644 --- a/skills/mono/references/best_practices/lite-recipes.md +++ b/skills/mono/references/best_practices/lite-recipes.md @@ -258,7 +258,7 @@ query 未提"听记"但任务产出依赖会议讨论内容时(报告/总结/ 1. 获取邮箱地址:`mail mailbox list` → 取用户邮箱作为 `--from`。 2. 确定收件人:用户直接提供邮箱地址 → 直接使用;用户提供姓名 → 走「查找他人邮箱地址」三路并发流程(见 [mail.md](../products/mail.md))。 -3. 发送:`mail message send --from <发件邮箱> --to <收件邮箱> --subject "<主题>" --body "<正文>"`(可选 `--cc`、`--attachment`、`--inline-attachment`)。 +3. 发送:`mail message send --from <发件邮箱> --to <收件邮箱> --subject "<主题>" --content "<正文>"`(正文的规范 flag 是 `--content`,`--body` 是隐藏别名;可选 `--cc`、`--attachment`、`--inline-attachment`)。 ### mail-reply-forward @@ -267,6 +267,6 @@ query 未提"听记"但任务产出依赖会议讨论内容时(报告/总结/ 1. 获取邮箱地址:`mail mailbox list` → 取用户邮箱。 2. 定位原始邮件:若用户未提供 messageId → 先用 `mail-search` 搜索定位。 3. 执行: - - 回复:`mail message reply --from <邮箱> --id `(可选 `--to`、`--subject`、`--body`) - - 回复全部:`mail message reply-all --from <邮箱> --id `(可选 `--to`、`--subject`、`--body`) - - 转发:`mail message forward --from <邮箱> --to <收件邮箱> --id `(可选 `--subject`、`--body`) + - 回复:`mail message reply --from <邮箱> --id `(可选 `--to`、`--subject`、`--content`;正文 flag 规范名 `--content`,`--body` 为别名) + - 回复全部:`mail message reply-all --from <邮箱> --id `(可选 `--to`、`--subject`、`--content`) + - 转发:`mail message forward --from <邮箱> --to <收件邮箱> --id `(可选 `--subject`、`--content`) diff --git a/skills/mono/references/capability-limits.md b/skills/mono/references/capability-limits.md index 0a0717e8..993c014e 100644 --- a/skills/mono/references/capability-limits.md +++ b/skills/mono/references/capability-limits.md @@ -22,7 +22,7 @@ | 不支持的操作 | 说明 | |------------|------| | 创建公式/查找引用等高级字段类型 | 部分高级字段类型暂不支持 API 创建 | -| 自己 PUT 文件时 Content-Type 不为空 | OSS 签名机制要求 PUT 请求的 `Content-Type` 头**必须清空**,否则返回 `SignatureDoesNotMatch` / HTTP 403。这**不是 dws 限制**,是阿里云 OSS 行为。解决:`dws aitable import upload --file ./x.xlsx` 已内置正确处理;自己写 `curl` 时必须传 `-H "Content-Type:"`(注意冒号后是空值) | +| 自己 PUT 文件到 OSS 时 Content-Type 处理 | OSS 签名对 PUT 请求的 `Content-Type` 头有严格要求,处理不当会 `SignatureDoesNotMatch` / HTTP 403。这**不是 dws 限制**,是阿里云 OSS 行为。解决:直接用 `python scripts/aitable_import_via_task.py `,脚本已内置 prepare→PUT→import data 全流程和正确的头处理,**不要自己写 curl PUT**。(注意:`aitable import upload` 命令没有 `--file` flag、也不代做 PUT,只准备导入;能自动 PUT 的是上面的脚本。) | ## minutes diff --git a/skills/mono/references/global-reference.md b/skills/mono/references/global-reference.md index edfc7374..7c5605c3 100644 --- a/skills/mono/references/global-reference.md +++ b/skills/mono/references/global-reference.md @@ -64,8 +64,8 @@ dws recovery finalize --event-id --outcome recovered|failed|handoff - | 标志 | 短名 | 说明 | 默认 | |------|:---:|------|------| | `--format` | `-f` | 输出格式: json / table / raw | json | -| `--jq` | | jq 表达式过滤输出 (如: `.items[] \| .name`) | 无 | -| `--fields` | | 筛选输出字段 (逗号分隔, 如: name,id,status) | 无 | +| `--jq` | | jq 表达式过滤输出 ⚠️**当前为 no-op,不生效**:仍返回完整 JSON,不要依赖它过滤,改在拿到 JSON 后自行处理 | 无 | +| `--fields` | | 筛选输出字段 ⚠️**当前为 no-op,不生效**:仍返回全量字段 | 无 | | `--verbose` | `-v` | 详细日志 | false | | `--debug` | | 调试日志 | false | | `--yes` | `-y` | 跳过确认提示 | false | diff --git a/skills/mono/references/intent-guide.md b/skills/mono/references/intent-guide.md index 532434e0..7bd2b5c8 100644 --- a/skills/mono/references/intent-guide.md +++ b/skills/mono/references/intent-guide.md @@ -160,7 +160,7 @@ alidocs 链接表面长得一样(`https://alidocs.dingtalk.com/i/nodes/{id}` 用 `sheet` 的场景(axls,钉钉在线电子表格): - `dws doc info --node ` 返回 `contentType=ALIDOC` + `extension=axls` - 用户在钉钉文档空间直接"新建电子表格"得到的节点 -- 所有 sheet 子命令(`list` / `range read` / `range write` / `export` 等)仅服务这类节点 +- 所有 sheet 子命令(`list` / `range read` / `range update` / `export` 等)仅服务这类节点 用 `dws drive download` 的场景(xlsx / xls / xlsm / csv 本地表格文件): - `dws doc info --node ` 返回 `contentType=DOCUMENT` + `extension=xlsx` / `xls` / `xlsm` / `csv` @@ -273,8 +273,9 @@ alidocs 链接表面长得一样(`https://alidocs.dingtalk.com/i/nodes/{id}` - 已有 userId 时直接使用 `--user`;已有 openDingTalkId 时使用 `--open-dingtalk-id` - 纯文本/Markdown 单聊传 `--user` 时直接走 userId 发送能力,不需要先手动查询 openDingTalkId - 富媒体消息(image/file)单聊优先使用 `--open-dingtalk-id`;传 `--user` 时 CLI 会尝试解析为 openDingTalkId 后发送 -- "发张图片/截图/语音/视频/文件到群里" / "发张图给某某" — **统一一条命令**:`dws chat message send ... --msg-type file --file-path <本地路径>`,CLI 内部自动上传并发送,**任意扩展名(png/jpg/pdf/mp4/zip…)都走这条** -- "发图片+文字说明" — 不要硬塞进一条命令;先发文件消息再补一条 `--text "..."` 即可 +- "发文件/语音/视频到群里" — `dws chat message send ... --msg-type file --file-path <本地路径>`,CLI 内部自动上传并发送(png/jpg/pdf/mp4/zip… 任意扩展名都走这条,但**都作为「文件」消息**发出,接收方看到的是可下载的文件条目) +- "发张图片/截图(要在聊天里内联渲染成图,不是文件)" — 走图片消息链路:先 `dt_media_upload` 拿 mediaId,再 `dws chat message send ... --msg-type image --media-id `。**注意**:用 `--msg-type file` 发 .png 只会显示为[文件](fileId),不会渲染成图片;要图片效果必须走 `--msg-type image` +- "发图片+文字说明" — 不要硬塞进一条命令;先发图片/文件消息再补一条 `--text "..."` 即可 ```bash dws chat message send --group --msg-type file --file-path ./screenshot.png --format json @@ -345,7 +346,7 @@ dws chat message send --open-dingtalk-id --msg-type file --file **用 `oa approval` 的场景**(通用 OA 审批中心): - "我的待审批 / 待办审批 / 已审批记录" — `oa approval list-pending` - "某审批单详情 / 同意或驳回审批 / 撤销我发起的审批" — `oa approval detail/approve/reject/revoke` -- "查业务审批记录 / 审批转交 / 添加评论与抄送" — `oa approval records/transfer/comment/cc` +- "查业务审批记录 / 审批转交 / 添加评论与抄送" — `oa approval records`(操作记录)/ `redirect-task`(转交)/ `oa-comments`(评论)/ `oa-cc-noticer`(抄送) - 用户提到"报销 / 采购 / 用印 / 合同 等非考勤类审批" **判断关键**: @@ -353,7 +354,7 @@ dws chat message send --open-dingtalk-id --msg-type file --file - 不限于考勤业务、面向"我上下游的审批任务"表述 → `oa approval` **提交审批单的边界**: -- 提交考勤审批单走 `attendance approve templates --type leave|overtime|repair-check`(当前仅支持请假/加班/补卡三类模板;出差/外出的提交模板暂不支持,查询仍可用 `approve list --types trip` 覆盖),命令会返回审批表单的 submitUrl 跳转链接,由用户点击链接跳转到钉钉客户端的提交页面完成填写与提交。**展示链接时必须用 Markdown 可点击格式 `[表单名称](submitUrl)`,不要裸露 URL**。 +- 提交考勤审批单走 `attendance approve templates --type leave|overtime|repair-check|travel`(请假/加班/补卡/出差外出,出差外出用 `--type travel`,返回 approveType=TRAVEL/OUT 的模板;具体哪几类有模板取决于组织配置,返回空列表即该组织未配该类),命令会返回审批表单的 submitUrl 跳转链接,由用户点击链接跳转到钉钉客户端的提交页面完成填写与提交。**展示链接时必须用 Markdown 可点击格式 `[表单名称](submitUrl)`,不要裸露 URL**。 - 提交诉求的辅助查询:可用假期余额走 `attendance vacation balance`、历史已提交记录走 `attendance approve list`。 - 任何场景下都**不要误用 `oa approval` 代替** —— 该命令组只能查/审/撤已存在的审批单,考勤业务审批单走考勤自己的逻辑便于区分。 diff --git a/skills/mono/references/products/aitable.md b/skills/mono/references/products/aitable.md index 66945081..482265d0 100644 --- a/skills/mono/references/products/aitable.md +++ b/skills/mono/references/products/aitable.md @@ -19,8 +19,10 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `base list` | 列出最近访问的 Base | — | 仅返回最近访问过的,优先用 `base search` | -| `base search` | 按名称搜索 Base | `--query` | 关键词 ≥2 字符 | +| `base search` | 按名称搜索 Base(别名 `aitable search`) | — | `--query` help 标必填但实际可省略:不传时返回最近访问的 Base 列表。`--keyword` 是 `--query` 的隐藏别名,同义 | | `base get` | 获取 Base 信息(含 tables 列表) | `--base-id` | 用户给 URL 时提取末尾 ID | +| `base copy` | 复制整个 Base 到目标文件夹 | `--base-id` `--target-folder-id` | 默认全量复制;`--only-struct` 仅复制结构不含数据 | +| `base get-primary-doc-id` | 获取某记录的主键文档 ID | `--base-id` `--table-id` `--record-id` | 等价 `record primary-doc-get` 的取 ID 视角 | | `base create` | 创建 Base | `--name` | 创建后直接用返回的 baseId | | `base update` | 更新 Base 名称 | `--base-id` `--name` | — | | `base delete` | 删除 Base | `--base-id` | 不可逆 | @@ -30,7 +32,8 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `table get` | 获取表结构(字段+视图目录) | `--base-id` | 不传 `--table-ids` 返回全部表 | -| `table create` | 创建数据表 | `--base-id` `--name` `--fields` | fields 为 JSON 数组,至少 1 个 | +| `table list` | 获取数据表(`table get` 的别名) | `--base-id` | 与 `table get` 等价 | +| `table create` | 创建数据表 | `--base-id` `--name` | `--fields` 为 JSON 数组;**可传空数组 `[]`**(默认值即 `[]`),此时服务端自动补一个名为"标题"的 primaryDoc 首列;单次最多 15 个字段 | | `table update` | 重命名表 | `--base-id` `--table-id` `--name` | — | | `table delete` | 删除表 | `--base-id` `--table-id` | 不可逆 | @@ -39,19 +42,29 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `field get` | 获取字段完整配置 | `--base-id` `--table-id` | 按需展开少量字段 | +| `field list` | 获取字段信息(`field get` 的别名) | `--base-id` `--table-id` | 与 `field get` 等价 | | `field create` | 创建字段 | `--base-id` `--table-id` + (`--name --type` 或 `--fields`) | 支持单字段/批量模式 | | `field update` | 更新字段名/配置 | `--base-id` `--table-id` `--field-id` | 不可变更字段类型 | | `field delete` | 删除字段 | `--base-id` `--table-id` `--field-id` | 不可逆 | +| `field search-options` | 搜索单选/多选字段的选项 | `--base-id` `--table-id` `--field-id` | 仅 singleSelect/multipleSelect;`--keyword` 模糊过滤,不传返回全部 | ### record (记录管理) | 命令 | 用途 | 必读 reference | 路由提醒 | |------|------|----------------|----------| -| `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference | +| `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference;`--query`(隐藏别名 `--keyword`)全文搜索 | +| `record list` | 获取记录(`record query` 的别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 与 `record query` 等价 | | `record get` | 按 ID 取记录(`record query --record-ids` 的窄别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 已知 recordId 时首选;必填 `--record-ids`(单次最多 100 条);未暴露 filters/sort/query/cursor/limit | +| `record query-empty` | 查询完全没填用户字段的空行 | — | `--base-id` `--table-id`;`--limit` 扫描预算 [1,100],`--cursor` 翻页 | | `record create` | 新增记录 | [aitable-record-create.md](./aitable/aitable-record-create.md) | cells key 必须是 fieldId 不是字段名;单次最多 100 条 | | `record update` | 更新记录 | [aitable-record-update.md](./aitable/aitable-record-update.md) | 需先 query 拿 recordId;只传需改字段;**没有** `--record-id` `--cells` flag | +| `record batch-update` | 把同一份 cells 批量应用到多条记录 | [aitable-record-update.md](./aitable/aitable-record-update.md) | `--record-ids`(≤100)+ `--cells` 共享 patch | +| `record upsert` | 批量创建或更新(有 recordId 走更新,无则创建) | [aitable-record-upsert.md](./aitable/aitable-record-upsert.md) | `--records`/`--records-file`;单次最多 100 条 | | `record delete` | 删除记录 | [aitable-record-delete.md](./aitable/aitable-record-delete.md) | 不可逆,需先 query 确认 | +| `record share-url` | 批量获取记录分享链接 | [aitable-record-share.md](./aitable/aitable-record-share.md) | `--record-ids`(逗号分隔,单次最多 20 条) | +| `record history-list` | 查询单条记录的变更历史 | [aitable-record-history.md](./aitable/aitable-record-history.md) | `--record-id` 单条;`--offset`/`--limit`(≤50) | +| `record primary-doc-get` | 查询记录的主键文档 nodeId | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 无文档时返回 `no record` 错误 | +| `record primary-doc-create` | 为记录创建主键文档 | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 幂等;`--field-id` 须 primaryDoc 类型 | ### view (视图管理) @@ -264,8 +277,9 @@ dws aitable chart get --base-id --dashboard-id --chart- 要点: - `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用。 -- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错。 -- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断。 +- `dashboard share get` 可能返回 `404`(`retryable:true`,从未分享甚至刚开启分享后立即查都可能 404),按可重试错误处理,不要误判为参数拼错;行为不稳定,别当"是否已分享"的唯一判据。 +- `chart share get` 稳定返回 `success + data`(含 `enabled`),从未分享时 `enabled=false`,不会 404。 +- `dashboard share update` 开 ORG 分享后 `shareType` 回显 `"[1]"`(服务端已知问题);`chart share update` 正确回显 `ORG`。详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)。 ### 导出数据(两阶段轮询) diff --git a/skills/mono/references/products/aitable/aitable-attachment.md b/skills/mono/references/products/aitable/aitable-attachment.md index 1648fc87..48ae3306 100644 --- a/skills/mono/references/products/aitable/aitable-attachment.md +++ b/skills/mono/references/products/aitable/aitable-attachment.md @@ -38,8 +38,10 @@ dws aitable record create --base-id --table-id \ dws aitable attachment upload --base-id --file-name report.pdf --size 204800 --format json # → 返回 uploadUrl、fileToken -# 2. PUT 上传(Content-Type 留空) -curl -X PUT "" -H "Content-Type:" --data-binary @report.pdf +# 2. PUT 上传(Content-Type 必须与文件类型一致,不能留空) +# 留空或用 curl 默认的 application/x-www-form-urlencoded 都会被 OSS 拒为 403 +curl -X PUT "" -H "Content-Type: application/pdf" --data-binary @report.pdf +# 例:.txt → text/plain,.png → image/png,.xlsx → application/vnd.openxmlformats-officedocument.spreadsheetml.sheet # 3. 写入记录 dws aitable record update --base-id --table-id \ diff --git a/skills/mono/references/products/aitable/aitable-dashboard-chart.md b/skills/mono/references/products/aitable/aitable-dashboard-chart.md index ec96fb4f..f904158a 100644 --- a/skills/mono/references/products/aitable/aitable-dashboard-chart.md +++ b/skills/mono/references/products/aitable/aitable-dashboard-chart.md @@ -15,8 +15,10 @@ dws aitable chart get --base-id --dashboard-id --chart- ## 要点 - `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用 -- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错 -- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断 +- `dashboard share get` 可能返回 `404`(`retryable:true`)——从未分享、甚至刚开启分享后立即查都可能 404;`data` 为 `{}`。按可重试错误处理,不要误判为参数拼错。它有时也会返回 `success + data`(如关闭分享之后),行为不稳定,别用它当"是否已分享"的唯一判据。 +- `chart share get` 稳定返回 `success + data`(含 `enabled` 等),可用于分享状态判断;从未分享时 `enabled=false`,不会 404。 +- ⚠️ **`dashboard share update` 开启 ORG 分享后返回的 `shareType` 是 `"[1]"`(未映射回 `ORG`,服务端已知问题)**;`chart share update --share-type ORG` 则正确返回 `shareType="ORG"`。判断 dashboard 是否 ORG 分享时对 `"[1]"` 做兼容。 +- `chart share update` / `dashboard share update` 的 `--enabled` 是字符串 flag:`--enabled false`(空格)和 `--enabled=false` 都能正确关闭分享。 ## dashboard 子命令 @@ -25,18 +27,25 @@ dws aitable chart get --base-id --dashboard-id --chart- | `dashboard get` | 获取仪表盘详情(含 charts 列表) | `--base-id` `--dashboard-id` | — | | `dashboard create` | 创建仪表盘 | `--base-id` + (`--config` 或 `--name`) | `--name` 简化版创建空看板;`--config` 传完整 JSON | | `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config` 或 `--name`) | `--name` 仅改名;`--config` 更新完整配置 | +| `dashboard arrange` | 自动重排仪表盘图表布局 | `--base-id` `--dashboard-id` | 让服务端重新排布 charts 位置 | | `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` `--yes` | — | | `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 | +| `dashboard share get` | 获取仪表盘分享配置 | `--base-id` `--dashboard-id` | 可能 404,见上方要点 | +| `dashboard share update` | 更新仪表盘分享配置 | `--base-id` `--dashboard-id` `--enabled` | `--enabled true` 开启(配 `--share-type PUBLIC/ORG`)/ `--enabled false` 关闭;ORG 回显 `shareType="[1]"` | ## chart 子命令 | 命令 | 用途 | 必填参数 | |------|------|----------| | `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` | -| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` | +| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` | | `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` | | `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` | | `chart widgets-example` | 查看图表 widgets 配置模板 | 无 | +| `chart share get` | 获取图表分享配置 | `--base-id` `--dashboard-id` `--chart-id` | +| `chart share update` | 更新图表分享配置(`--enabled true/false`,开启配 `--share-type PUBLIC/ORG`) | `--base-id` `--dashboard-id` `--chart-id` `--enabled` | + +> `chart create` 的 `--layout` 是**必填**(12 列网格布局,如 `{"x":0,"y":0,"w":6,"h":4}`);不传本地校验直接拒。`chart update` 的 `--config` 也**必填**——即便只想改 layout,也要带完整 config,否则服务端拒绝。 ## 配置获取流程 diff --git a/skills/mono/references/products/aitable/aitable-export-import.md b/skills/mono/references/products/aitable/aitable-export-import.md index 05e83466..fd4ea89b 100644 --- a/skills/mono/references/products/aitable/aitable-export-import.md +++ b/skills/mono/references/products/aitable/aitable-export-import.md @@ -118,23 +118,25 @@ dws aitable import data --import-id --table-id --format js 用户给文件让导入 AI 表格时,路径选择决定成败: +> ⚠️ **`import upload` 没有 `--file` flag**(传了会报 unknown flag)。它只申请上传凭证,必填 `--file-name` + `--file-size`,拿到 `uploadUrl` 后要**自己 curl PUT 上传文件**(Content-Type 留空,见上文三步流程),再 `import data`。想省事直接用 `aitable_import_via_task.py` 脚本,它把这三步包好了。 + | 用户原话 | 链路 | 命令 / 脚本 | 行为 | |---------|------|------------|------| -| "把这个 Excel 导入到 AI 表格"(无指定目标表) | **文件导入任务** | `python scripts/aitable_import_via_task.py ` 或 `dws aitable import upload --base-id B --file ./x.xlsx` + `dws aitable import data --import-id ` | 服务端解析文件,**新建数据表**,自动识别表头 | +| "把这个 Excel 导入到 AI 表格"(无指定目标表) | **文件导入任务** | `python scripts/aitable_import_via_task.py `(推荐)或手动三步 `import upload --file-name x.xlsx --file-size <字节>` → curl PUT → `import data --import-id ` | 服务端解析文件,**新建数据表**,自动识别表头 | | "把这个 Excel 导入新表 / 自动建表" | 同上 | 同上 | 同上 | | "把这批记录追加到已有的『成员表』里" | **记录批量写入** | `python scripts/import_records.py ` | 走 `record create`,**写入已有 tableId**,需要字段名匹配 | -| "Excel 列名和表字段对不上但要追加" | 文件导入 + 追加 + 字段映射 | `dws aitable import upload --base-id B --file ./x.xlsx` → `dws aitable import data --import-id --table-id --field-mapping '{"目标":"源"}'` | 服务端按映射追加 | +| "Excel 列名和表字段对不上但要追加" | 文件导入 + 追加 + 字段映射 | 三步导入后 `import data --import-id --table-id --field-mapping '{"目标":"源"}'` | 服务端按映射追加 | ## 大表 / 长任务超时续等 -默认整体轮询超时 5 分钟。大表导出/导入超时后命令会返回 `taskId` / `importId`,用同命令带 ID 续等: +单次等待窗口很短:`export data` 只有 `--timeout-ms`(默认且**上限 30000 = 30 秒**,没有 `--timeout-sec`,传了会报 unknown flag);`import data` 用 `--timeout`(秒,默认且推荐最大值 30)。窗口内没跑完,命令会返回 `taskId` / `importId`,用同命令带 ID 反复续等即可: ```bash -# 续等导出 -dws aitable export data --base-id --task-id --output ./out.xlsx +# 续等导出:拿到 downloadUrl 后再 curl 下载(见下方警告) +dws aitable export data --base-id --task-id --timeout-ms 30000 # 续等导入 dws aitable import data --import-id ``` -或一次性把 timeout 提到 15 分钟:`--timeout-sec 900`。 +> ⚠️ **`--output` 不会保存导出的 xlsx**:`--output` 是隐藏的全局 flag,作用是把命令的 **JSON 输出**写到文件,实测只生成一个 0 字节文件,不会下载导出内容。正确做法是从 `export data` 返回里取 `downloadUrl`,再 `curl -L "" -o out.xlsx` 下载。想省事直接用 `aitable_export_via_task.py` 脚本(它负责轮询 + 下载)。 diff --git a/skills/mono/references/products/aitable/aitable-form.md b/skills/mono/references/products/aitable/aitable-form.md index 2b126d8f..2aaedba6 100644 --- a/skills/mono/references/products/aitable/aitable-form.md +++ b/skills/mono/references/products/aitable/aitable-form.md @@ -50,8 +50,8 @@ dws aitable form share get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ | 命令 | 用途 | 必填参数 | 说明 | |------|------|----------|------| -| `form list` | 列出表单视图 | `--base-id` `--table-id` | 返回 viewId/name/title/createdAt | -| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 内部基于 list_form_views 过滤 | +| `form list` | 列出表单视图 | `--base-id` `--table-id` | 每条返回 viewId/name;新建表单**无 title 且 createdAt=0**,改过(form update)后才出现 title 和真实 createdAt | +| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 客户端按 viewId 过滤后 `data` **即该表单对象**(不是 formViews 数组);viewId 不存在返回 `form view ... not found` 错误 | | `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | viewType=FormDesigner | | `form update` | 更新表单 | `--base-id` `--table-id` `--view-id` | `--title`/`--name`(等价)和 `--description` 至少传一项;同时传 title/name 时 title 优先 | | `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` `--yes` | 不可逆 | @@ -115,6 +115,6 @@ dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VI ## 返回结构补充 -- `form list` 返回 `data.formViews[]`,**每条仅含** `viewId/name/title/createdAt`;`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。 -- `form get` 返回结构与 `form list` 完全一致(`data.formViews[]`),仅含一条记录(与请求 viewId 一致)。Agent 提取时仍走 `data.formViews[0]`。 +- `form list` 返回 `data.formViews[]`,**每条含** `viewId/name`(+ `createdAt`);**新建表单没有 `title` 字段且 `createdAt=0`**,只有在 `form update` 碰过之后才会出现 `title` 和真实 `createdAt`。`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。 +- `form get` 的 `data` **就是命中的那一条表单对象**(如 `{viewId, name, createdAt, title?, shareFormUuid?}`),不是 `formViews` 数组。Agent 直接读 `data.viewId` / `data.name` 即可,**不要**再取 `data.formViews[0]`。服务端的 viewIds 过滤参数当前不生效,CLI 在客户端按 viewId 精确筛出单条;传了不存在的 viewId 会返回 `form view not found in table` 错误。 - `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`。 diff --git a/skills/mono/references/products/aitable/aitable-primary-doc.md b/skills/mono/references/products/aitable/aitable-primary-doc.md index eefda277..8ed2198d 100644 --- a/skills/mono/references/products/aitable/aitable-primary-doc.md +++ b/skills/mono/references/products/aitable/aitable-primary-doc.md @@ -17,7 +17,9 @@ dws aitable record primary-doc-get --base-id BASE_ID --table-id TABLE_ID --recor - `--table-id`(必填):Table ID - `--record-id`(必填):Record ID -**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。若该记录尚未创建主键文档,`nodeId` 为 null。 +**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。 + +> **该记录尚未创建主键文档时**:不会返回 `nodeId: null`,而是 `status=error`、`data={}`,`error={code:"-1", message:"no record", type:"SYSTEM_ERROR", retryable:true}`。要判断"有没有主键文档",看是否命中这个 `no record` 错误,而不是判断 `nodeId` 是否为 null。需要文档时改用 `primary-doc-create`(幂等,已存在则直接返回)。 ### 创建主键文档 diff --git a/skills/mono/references/products/aitable/aitable-record-history.md b/skills/mono/references/products/aitable/aitable-record-history.md index f02d3d87..20154b98 100644 --- a/skills/mono/references/products/aitable/aitable-record-history.md +++ b/skills/mono/references/products/aitable/aitable-record-history.md @@ -25,12 +25,11 @@ dws aitable record history-list \ "data": { "histories": [ { - "type": "field_change", // 变更类型 - "action": "update", // 操作动作: create / update / delete - "newValue": "{\"...\":\"...\"}", // 变更后的值(JSON 字符串) - "oldValue": "{\"...\":\"...\"}", // 变更前的值(JSON 字符串) + "type": "row", // 变更类型,实测均为 "row"(行级变更) + "action": "updateRecords", // 操作动作:appendRow(新增行) / updateRecords(更新记录) + "newValue": "{\"fldX\":{\"dataType\":\"STRING\",\"value\":\"改后值\"}}", // 变更后的值(JSON 字符串,按 fieldId 组织) + "oldValue": "{\"fldX\":{\"dataType\":\"STRING\",\"value\":\"改前值\"}}", // 变更前的值(appendRow 新增行时无此字段) "operateTime": 1733123456789, // 操作时间(毫秒级时间戳) - "typeChangedFields": "{...}", // 类型变更的字段信息(JSON 字符串) "version": 7 // 版本号(单调递增) } ] @@ -38,14 +37,14 @@ dws aitable record history-list \ } ``` -`newValue` / `oldValue` / `typeChangedFields` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值。 +`newValue` / `oldValue` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值;解析后是 `{fieldId: {dataType, value}}` 结构。`appendRow`(新增行)事件没有 `oldValue`。实测返回里**没有** `typeChangedFields` 字段。 ## 字段含义速查 | 字段 | 用途 | |------|------| -| `type` | 高层分类:`record_create` / `field_change` / `record_delete` 等。先按 type 过滤大类。 | -| `action` | 三态:`create` / `update` / `delete`。比 type 粗,但便于按"动作"统计。 | +| `type` | 实测均为 `row`(行级变更),服务端未按 `record_create` / `field_change` 细分。 | +| `action` | 底层操作名:`appendRow`(新增行)/ `updateRecords`(更新记录)。按"动作"统计时以这两个值为准。 | | `version` | 单调递增整数;同一 record 越新值越大。**用作"上一条 vs 这一条"的稳定排序键**。 | | `operateTime` | 毫秒时间戳;可格式化成可读时间。多条同 version 的极端场景用 operateTime 兜底排序。 | @@ -74,20 +73,22 @@ dws aitable record history-list --base-id BASE --table-id TBL --record-id REC -- ```bash dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --format json \ - | jq '[.data.histories[] | select(.action == "update")][0].oldValue' + | jq '[.data.histories[] | select(.action == "updateRecords")][0].oldValue' ``` -### 4. 找出删除事件(如果存在 delete history) +### 4. 只看更新事件(排除新增行) ```bash dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \ - | jq '.data.histories[] | select(.action == "delete") | {version, operateTime}' + | jq '.data.histories[] | select(.action == "updateRecords") | {version, operateTime}' ``` +> 记录被 `record delete` 删除后,其历史不再返回(`histories` 为空数组),无法通过本命令回溯删除事件。 + ## 注意事项 - 一次只能查一条 record;如需批量审计多条记录请循环调用。 -- 仅返回**字段值变更**与**记录生命周期事件**;视图、字段定义、表结构变更不在此 history 里。 +- 仅返回**新增行**(appendRow)与**字段值变更**(updateRecords);记录被删除后其历史不再可查(返回空)。视图、字段定义、表结构变更不在此 history 里。 - 历史保留时长由 server 决定,过老的记录可能不再返回。 ## 与其他 record 命令的关系 diff --git a/skills/mono/references/products/aitable/aitable-view-config.md b/skills/mono/references/products/aitable/aitable-view-config.md index 6b22ec03..d2423374 100644 --- a/skills/mono/references/products/aitable/aitable-view-config.md +++ b/skills/mono/references/products/aitable/aitable-view-config.md @@ -79,21 +79,21 @@ dws aitable view update timebar --view-id GANTT_ID --official-holiday=true ### view update aggregate(仅 Grid) -值是 `map[fieldId]→AggregateAction string`;传 null 清除某个字段聚合。 +值是 `map[fieldId]→AggregateAction string`。**设置**聚合可用;**清除**聚合当前无效(见下方警告)。 | flag | 类型 | 说明 | |------|------|------| | `--field-id` | string | 配合 `--action` 设置**单字段**聚合 | | `--action` | string | `SUM`/`AVG`/`MAX`/`MIN`/`MEDIAN`/`RANGE`/`TOTAL`/`DISTINCT`/`EXIST`/`UN_EXIST`/`CHECKED`/`EARLIEST_DATE` 等(按字段类型可用) | -| `--clear-field-id` | string (CSV) | 一/多个字段 ID,清除其聚合 | +| `--clear-field-id` | string (CSV) | 一/多个字段 ID,本意是清除其聚合,但**当前服务端不支持清除,静默无效**(见下方警告) | | `--json` | JSON | 完整 aggregate map | ```bash dws aitable view update aggregate --view-id GRID_ID --field-id fldX --action SUM -dws aitable view update aggregate --view-id GRID_ID --clear-field-id fldA,fldB -dws aitable view update aggregate --view-id GRID_ID --json '{"fldX":"AVG","fldY":null}' ``` +> ⚠️ **当前无法清除已设置的聚合(服务端限制)**:`--clear-field-id fldA,fldB` 与 `--json '{"fldX":null}'` 两种清除写法都返回 `success`,但用 `view get aggregate` 复核会发现聚合**原样不动**——是静默无效,不是真的清掉。这是服务端没有清除语义所致,直至服务端修复前不要依赖它。改聚合方式可行(重新 `--action` 覆盖成别的),只是无法回到"无聚合"。 + ### view update field-widths(仅 Grid) | flag | 类型 | @@ -108,9 +108,11 @@ dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB ### view update visible-fields(通用) -整组替换可见字段列表与顺序。首列字段(primaryDoc)必须保留在数组第一位。 +整组替换可见字段列表与顺序,同时兼作**隐藏/显示**入口。首列字段(primaryDoc)必须保留在数组第一位。 -> ⚠️ 注意:服务端**只接受 reorder,不接受真"隐藏字段"**——如果传入的列表比当前 columns 短,缺失的字段不会被隐藏。需要真正隐藏字段请到 AI 表格 Web UI。 +> **传入的列表既定顺序又定可见性**:传一个比当前 columns 短的列表,缺失的字段会被真正隐藏(该字段在 `view list` 的 `custom.hiddenFields` 里变 `true`);再传回全量列表即可解除隐藏(`hiddenFields` 变 `false`)。 +> +> ⚠️ **查隐藏状态别看这里**:`view get visible-fields` 返回的数组**包含已隐藏字段**(列的完整顺序),看不出谁被隐藏。要确认隐藏状态,读 `view get`(view list)里该视图的 `custom.hiddenFields`(`{fieldId: true|false}`)。 | flag | 类型 | |------|------| @@ -118,7 +120,9 @@ dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB | `--json` | string 数组 JSON(与 `--field-ids` 同传时 `--json` 优先) | ```bash -dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA,fldB +# 只保留首列和 fldA,其余字段被隐藏 +dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA +# 传回全量列表解除隐藏 dws aitable view update visible-fields --view-id VIEW_ID --json '["fldPrimary","fldA","fldB"]' ``` diff --git a/skills/mono/references/products/aitable/aitable-view-extras.md b/skills/mono/references/products/aitable/aitable-view-extras.md index 23db8059..c9174158 100644 --- a/skills/mono/references/products/aitable/aitable-view-extras.md +++ b/skills/mono/references/products/aitable/aitable-view-extras.md @@ -54,7 +54,7 @@ dws aitable view update frozen-cols --view-id VIEW_ID --count 0 # 查询当前冻结列数 dws aitable view get frozen-cols --view-id VIEW_ID --format json -# → {"data": {..., "count": 1}} count 为 null 表示视图未显式设置 +# → {"data": {..., "count": 1}} 未显式设置时整个 count 键缺失(不是返回 null) ``` `--count` 必须 ≥ 0;负数会被拒绝。 @@ -69,7 +69,7 @@ dws aitable view update row-height --view-id VIEW_ID --cell-height 56 # 查询当前行高 dws aitable view get row-height --view-id VIEW_ID --format json -# → {"data": {..., "cellHeight": 56}} cellHeight 为 null 表示视图未显式设置(前端按 32 渲染) +# → {"data": {..., "cellHeight": 56}} 未显式设置时整个 cellHeight 键缺失(不是返回 null;前端按 32 渲染) ``` ## 数据高亮规则(条件填色,仅 Grid) diff --git a/skills/mono/references/products/attendance-report.md b/skills/mono/references/products/attendance-report.md index e845abf4..43d54547 100644 --- a/skills/mono/references/products/attendance-report.md +++ b/skills/mono/references/products/attendance-report.md @@ -11,7 +11,7 @@ 1. 未按"阶段 1"做人员解析 → `--users` 传入部门 ID 而非员工 userId,脚本虽内置回退但会浪费一次失败的接口调用 2. 未按"阶段 0"判断报表类型 → 用户说"汇总"被理解成"明细",导致输出粒度错误 3. 未按"列选择"判断是否传 `--column-keywords` → 用户要"迟到情况报表"被输出成全字段默认报表 -4. 未按"错误处理"规则处理 403 / `HSF_ILLEGALPARAMS` → 把环境错误当成业务错误反馈给用户 +4. 未按"错误处理"规则处理无权限错误(errorCode 6001「无权限操作」/ `AUTH_ERROR`)/ `HSF_ILLEGALPARAMS` → 把环境错误当成业务错误反馈给用户 5. 未按"阶段 4"返回结果 → 把 Excel 内容贴在对话里,或者裸 userId 直接输出 **执行前自检(必须能在心中回答)**: @@ -22,7 +22,7 @@ 如果上述任何一项答不出,**回到本文档对应章节重新阅读**,禁止凭记忆/想象组装命令。 -**前提**:当前用户必须是钉钉管理员,否则 report 系列接口返回 403 权限错误。 +**前提**:当前用户必须是钉钉管理员,否则 report 系列接口返回业务权限错误(`server_error_code` 为 `AUTH_ERROR`、errorCode `6001`「无权限操作」),**不是 HTTP 403**。提示需要管理员权限,不要重试。 ## 核心原则 @@ -46,7 +46,7 @@ Agent 解析用户意图(报表类型、人员范围、时间范围、关注 - 所有 `dws` 命令必须携带 `--format json` - 时间参数 `--start` / `--end` 格式必须为 `yyyy-MM-dd HH:mm:ss` - 字段 ID 与字段名的映射必须从 `report columns` 实时建立,禁止硬编码 -- 任何接口失败(含 403)必须向用户清晰报错,禁止静默吞掉 +- 任何接口失败(含无权限错误 6001/`AUTH_ERROR`)必须向用户清晰报错,禁止静默吞掉 ## 涉及工具 @@ -246,7 +246,7 @@ python scripts/attendance_report_checkin.py \ - 脚本依赖 `openpyxl`,若未安装需先 `pip install openpyxl` - 脚本摘要输出到 stdout,进度日志输出到 stderr -- 首次调试可加 `--inspect` 参数查看首条记录原始结构 +- 首次调试可加 `--inspect` 参数查看首条记录原始结构(`attendance_report_detail.py` / `attendance_report_monthly.py` / `attendance_report_daily.py` / `attendance_report_checkin.py` 支持;**`attendance_report_record.py` 无 `--inspect` 参数**) - 脚本执行失败(exit ≠ 0)时,stderr 中有具体错误信息 ### 阶段 4: 返回结果给用户 @@ -488,11 +488,11 @@ python scripts/attendance_report_checkin.py \ | 错误 | 原因 | 处理方式 | |------|------|---------| -| 权限错误(403) | 当前账号非管理员 | 提示需要管理员权限,不要重试 | +| 权限错误(errorCode 6001「无权限操作」/ `AUTH_ERROR`,非 HTTP 403) | 当前账号非管理员 | 提示需要管理员权限,不要重试 | | userId 无效 | 用户 ID 错误或已离职 | 脚本跳过并在摘要中标注 | | 时间区间超长 | 接口可能性能不佳 | 提示"超过 1 年的数据建议分阶段导出" | | openpyxl 未安装 | 环境缺包 | 输出 `pip install openpyxl` 安装提示 | -| 脚本执行失败 | 接口异常/配置问题 | 将 stderr 错误信息转告用户,可加 `--inspect` 重试 | +| 脚本执行失败 | 接口异常/配置问题 | 将 stderr 错误信息转告用户,可加 `--inspect` 重试(`attendance_report_record.py` 不支持 `--inspect`) | ## 使用示例 diff --git a/skills/mono/references/products/attendance.md b/skills/mono/references/products/attendance.md index c826339e..e4dbd069 100644 --- a/skills/mono/references/products/attendance.md +++ b/skills/mono/references/products/attendance.md @@ -1,6 +1,6 @@ # 考勤 (attendance) 命令参考 -> **【开源版命令可用性提示】** 当前开源 dws 已落地 P0 5 条命令:`attendance check result`、`attendance check record`、`attendance group search`、`attendance vacation balance`、`attendance vacation types`。文档中其余 P1 阶段命令(`class` / `overtime` / `adjustment` / `group settings` / `report` / `schedule` / `boss-check` 等共 28 条)暂未在当前开源二进制暴露,调用会返回 `unknown command`,将在后续批次落地。 +> **【命令可用性提示】** 当前 dws 已注册全部考勤子命令组(`record` / `check` / `approve` / `shift` / `schedule` / `class` / `adjustment` / `overtime` / `group` / `summary` / `rules` / `selfsetting` / `globalsetting` / `vacation` / `checkin` / `report` / `boss-check`)。查询与写操作大多可直接调用后端,不会再返回 `unknown command`,不要以"开源版不支持"为由拒答。个别命令返回受账号权限和组织数据影响:`report` 系列仅管理员可用;非管理员或数据为空时可能返回空列表或权限错误。执行前可用 `dws --help` 或 `--dry-run` 验证参数。 > **【必读】日期范围严格计算规则 — 所有含 --start/--end 或 --from/--to 的命令均适用** > @@ -143,21 +143,20 @@ Usage: dws attendance schedule import [flags] Example: dws attendance schedule import --group-id 123456 \ - --schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \ + --schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \ --yes Flags: - --group-id string 考勤组(必填,传入考勤组ID) - --schedules string 排班记录 JSON 数组(必填) - --yes 跳过确认提示 + --group-id string 考勤组(必填,传入考勤组ID);--help 主名为 --groupId,--group-id 为别名 + --schedules string 排班记录 JSON 数组(必填);--help 主名为 --scheduleVOS,--schedules 为别名 + --yes 跳过确认提示;--help 主名为 --user-say-yes,--yes 为别名 ``` 为排班制考勤组导入排班记录。`--schedules` 为 JSON 数组,每条记录包含: -- `userId`: 员工ID -- `classId`: 班次ID -- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22 -- `checkBeginTime`: 开始打卡时间 -- `checkEndTime`: 结束打卡时间 -- `isRest`: 是否休息日 Y/N(可选) +- `userId`: 员工ID(必填) +- `classId`: 班次ID(必填) +- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22(必填) +- `isRest`: 是否休息日 Y/N(**必填**,服务端要求传入) +- `checkBeginTime` / `checkEndTime`: 开始/结束打卡时间(可传,但当前不会进入后端 payload,实际打卡时段以 `classId` 对应班次为准) #### AI 调用 `schedule import` 的二次确认流程 @@ -305,7 +304,7 @@ Flags: --adjustment-id int 补卡规则主键 ID (必填) ``` -根据补卡规则主键 ID 查询对应的补卡规则详情。主键 ID 可从 `adjustment search` 返回结果中提取,也有可能来源于用户手动输入。**注意:已被删除或被更新覆盖的补卡规则无法查询到。** +**注意:本命令当前拿不到有效的补卡规则详情,不要依赖它。** 服务端对任意 `--adjustment-id`(含不存在的 ID)都返回同一个 `{"success":true,"有效期类型":"..."}`,不返回规则明细,也无法据此判断规则是否存在;且 `adjustment search` 返回的默认补卡规则 `entityVO.id` 为 `null`,`search → get` 取 id 的链路走不通。补卡规则内容请直接看 `adjustment search` 的返回结果。 ### 分页查询加班规则,支持按名称搜素 ``` @@ -580,12 +579,17 @@ Flags: Usage: dws attendance summary [flags] Example: - dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" + dws attendance summary --user USER_ID --date 2026-03-12 --stats-type week + dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type month Flags: - --date string 工作日期, 格式 yyyy-MM-dd HH:mm:ss (必填) - --user string 钉钉用户 ID (必填) + --date string 查询日期, 格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss (必填) + --stats-type string 统计类型: week 周统计 / month 月统计 (必填) + --tag-name string 标签名称 (可选) + --user string 钉钉用户 ID (必填) ``` +`summary` 必须同时传 `--user`、`--date`、`--stats-type`,缺一即报错(如 C0002)。`--stats-type` 只能是 `week`(周统计)或 `month`(月统计)。 + ### 查询考勤组与考勤规则 ``` Usage: @@ -847,10 +851,10 @@ Example: dws attendance vacation balance --users userId1,userId2 --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890 Flags: --users string 目标员工 ID 列表, 逗号分隔 (必填) - --leave-code string 假期规则 code (选填,不传则查询所有假期规则余额) + --leave-code string 假期规则 code (必填,服务端要求非空,不传返回 INVALID_PARAMS) ``` -调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取;不传 `--leave-code` 时查询所有假期规则余额。认证信息(corpId、opUserId)由系统自动注入。 +调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取。**注意:`--help` 虽标"选填",但服务端要求 `leaveCode` 非空,实际必填;不传会返回 `INVALID_PARAMS`(corpId、opUserId、leaveCode、targetUserIds 不能为空)。** 若要一次查所有假期规则余额,必须走 [attendance-vacation.md](./attendance-vacation.md) 工作流脚本逐个规则遍历。认证信息(corpId、opUserId)由系统自动注入。 如用户需要“所有假期规则余额 / 导出假期余额列表 / 所有假期规则余额 Excel / 按截图样式导出假期余额”,必须先读取 [attendance-vacation.md](./attendance-vacation.md),再按其中工作流调用脚本生成 Excel。 @@ -867,7 +871,7 @@ Flags: --end string 查询结束日期, 格式 YYYY-MM-DD (必填) ``` -调用 MCP 工具 get_leave_balance_records 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。 +调用 MCP 工具 get_leave_balance_records_v2 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 必填,可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。 ### 更新假期规则(写场景接口,必须走二次确认流程) @@ -1002,7 +1006,7 @@ Usage: dws attendance checkin records [flags] Example: dws attendance checkin records \ - --operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00" + --operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00" Flags: --end string 结束时间, 格式 yyyy-MM-dd HH:mm:ss(必填) --operator-corp-id string 操作者企业 ID(必填) @@ -1032,7 +1036,7 @@ Flags: 用户说"班次详情/某个班次的具体信息" → `class search --name "..."`(search 直出,直接返回详情)。`class get` 仅在需要按已知 classId 精确查询时使用 用户说"更新班次/修改班次/班次改名/修改上下班时间" → `class update` 用户说"补卡规则/补卡设置" → `adjustment search`(返回结果已包含全量属性,无需再调 get) -用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出)。`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用 +用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出,已含全量属性)。**不要用 `adjustment get`**:当前服务端对任意 id 都只返回"有效期类型"、拿不到规则明细 用户说"加班规则/加班设置/加班计算" → `overtime search`(返回结果已包含全量属性,无需再调 get) 用户说"加班规则详情/某条加班规则的具体信息" → `overtime search --name "..."`(search 直出)。如需查已删除/被覆盖的历史记录 → `overtime get` 用户说"考勤组列表/有哪些考勤组" → `group search` @@ -1068,7 +1072,7 @@ Flags: ```bash # 导入排班记录 dws attendance schedule import --group-id 123456 \ - --schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \ + --schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \ --yes --format json # 获取排班记录 — 禁止直接调用,必须走 attendance-schedule.md 排班查询导出工作流 @@ -1123,8 +1127,8 @@ dws attendance group update --group-id 123456 --group-vo '{"positions":[{"title" dws attendance group create --name "研发考勤组" --type FIXED --group-vo '{"defaultClassId":1170996821,"workDayClassList":[0,1170996821,0,0,0,0,0]}' --timeout 10 --format json dws attendance group create --name "自由工时分组" --type NONE --timeout 10 --format json -# 查看考勤统计摘要 -dws attendance summary --user --date "2026-03-12 15:00:00" --format json +# 查看考勤统计摘要(--stats-type 必填:week 周统计 / month 月统计) +dws attendance summary --user --date 2026-03-12 --stats-type week --format json # 查看考勤组和规则 dws attendance rules --date 2026-03-14 --format json @@ -1202,7 +1206,7 @@ dws attendance vacation save-balance --target user001 \ --num 8 --reason "绩效奖励发放3天" --format json # 查询签到记录 -dws attendance checkin records --operator-staff-id op001 --staff-ids user001,user002 \ +dws attendance checkin records --operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 \ --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00" --format json ``` @@ -1233,7 +1237,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use - `class get` 的 `--class-id` 必填,班次 ID 可从 `class search` 结果中提取 - `class search` 返回结果已包含全量属性,无需再调用 `class get`;`class get` 仅在需要按已知 classId 精确查询时使用 - `class update` 的 `--class-id` 必填,其余均可选,仅需对要修改的字段赋值,未传字段会自动从已有配置补充;由于保存班次耗时较久,建议加 `--timeout 10` -- `adjustment search` 返回结果已包含全量属性,无需再调用 `adjustment get`;`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用 +- `adjustment search` 返回结果已包含全量属性;`adjustment get` 当前服务端对任意 id 都只返回"有效期类型"、无规则明细,不可用,补卡规则详情一律看 `adjustment search` 返回 - `overtime search` 返回结果已包含全量属性,无需再调用 `overtime get`;`overtime get` 仅在需要按已知 overtimeId 查询时使用(包括已删除/被覆盖的历史记录) - `adjustment search` / `overtime search` 分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20 - `group search` 的分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20 @@ -1242,7 +1246,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use - `group update` 的 --group-id 必填,其余均可选,至少需指定一个修改项;仅需对要修改的字段赋値,未传字段会从已有配置自动补充;修改打卡地址/wifi/蓝牙等复杂子对象时用 `--group-vo` 传入完整 JSON;`--group-vo` 与单字段 flag 同时传入时单字段 flag 优先级更高 - `group create` 的 `--name` 和 `--type` 必填,`--type` 必须为 FIXED/TURN/NONE 之一;type=FIXED 时 `--group-vo` 必须包含 `workDayClassList`(非空)和 `defaultClassId`(非 null);由于保存考勤组耗时较久,建议加 `--timeout 10` - `group filtered-get` 的 `--group-id` 必填,`--member/--position/--wifi/--bles` 均可选,默认 false。**返回结果中如含成员 userId 列表,必须调用 `dws contact user get --ids ,,...`(支持逗号分隔传多个 ID),将 userId 转换为员工姓名后再输出;不得直接输出裸 userId。** -- `summary` 的 `--date` 格式: yyyy-MM-dd HH:mm:ss(如 `2026-03-12 15:00:00`) +- `summary` 必须同时传 `--user`、`--date`、`--stats-type`(week 周统计 / month 月统计),三者缺一即报错;`--date` 支持 `YYYY-MM-DD` 或 `yyyy-MM-dd HH:mm:ss`;`--tag-name` 可选 - `rules` 的 `--date` 支持 YYYY-MM-DD 或 yyyy-MM-dd HH:mm:ss 两种格式 - `selfsetting get/save` 的 `--setting-scene` 必须是 `checkRemind`、`fastCheck`、`checkResultNotify`、`lackRemind`、`personalAttendStatNotify`、`bossAttendStatNotify` 之一 - `selfsetting get/save` 的 MCP 入参 `userId` 为必填;CLI 的 `--user` 也必填,必须显式传入目标用户 ID @@ -1255,8 +1259,8 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use - 用户 ID 需从 `contact user get-self` 或 `aisearch person` 获取 - 考勤组 ID 需从 `rules` 命令返回结果中获取 - `vacation types` 无需任何参数,认证信息自动注入 -- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 选填,可通过 `vacation types` 获取 -- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 选填 +- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 服务端要求非空、实际必填(`--help` 标"选填"不准),不传返回 `INVALID_PARAMS`,可通过 `vacation types` 获取 +- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 必填(不传无法查询),底层 MCP 工具为 `get_leave_balance_records_v2` - `vacation balance` 和 `vacation records` 的认证参数(corpId、opUserId)由系统自动注入,无需手动传入 - `vacation update-type` 的 `--leave-code` 必填;其他字段均为可选,但至少需传一个更新字段 - `vacation update-type` 的 `--visibility-rules` 为 JSON 数组字符串,格式:`[{"type":"dept","visible":["1","2","3"]}]`,type 可取值 staff/label/dept diff --git a/skills/mono/references/products/calendar.md b/skills/mono/references/products/calendar.md index 5cdcf080..2eba38f0 100644 --- a/skills/mono/references/products/calendar.md +++ b/skills/mono/references/products/calendar.md @@ -64,6 +64,7 @@ dws calendar room add [flags] dws calendar room delete [flags] ``` > room是会议室,用于线下开会场景。 +> **组织限制**:部分企业使用自建会议室系统,未接入钉钉会议室能力。此时 `room search` / `room list-groups` 会返回业务错误 `400056`("所选组织不支持预定钉钉会议室")。这是组织级配置限制,不是命令用法问题;应直接告知用户该组织需在钉钉客户端手动预订会议室,不要重试或换参。 ### busy 相关三级子命令 ``` diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index 1ba96750..bed1c0da 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -73,6 +73,19 @@ Flags: --bot-id string 机器人 openBotId (必填) ``` +#### 根据成员 ID 批量查询群成员详情 — 传入成员 openDingTalkId 列表批量查询 +``` +Usage: + dws chat group members list-by-ids [flags] +Example: + dws chat group members list-by-ids --id --users openDingTalkId1,openDingTalkId2 + # 查询群 ID: dws chat search --query "群名" + # 查询 openDingTalkId: dws contact user search --query "姓名" +Flags: + --id string 群 ID / openConversationId (必填) + --users string 成员 openDingTalkId 列表,逗号分隔 (必填) +``` + #### 更新群名称 ``` Usage: @@ -136,6 +149,7 @@ Example: Flags: --group string 群聊 openConversationId (必填) ``` +> ⚠️ 唯一群主 quit 会直接成功,产生无主群,之后对该群做 dismiss / members 等管理操作会报 listRoles null(11056) 无法管理。与 `chat group members remove` 移除群主有本地拦截不同,quit 无此防护。作为唯一群主想退群,应先 `chat group transfer-owner` 转让群主,或 `chat group dismiss` 解散群。 #### 更新群头像 — 更新指定群聊的群头像 ``` @@ -149,6 +163,7 @@ Flags: --icon-media-id string 群头像 mediaId (必填) ``` > `--icon-media-id` 有本地格式校验:必须是 `@` 开头的媒体 ID(如 `dt_media_upload` 的返回值),非法格式会在本地直接报错。 +> ⚠️ 本地格式校验只查前缀。格式合法但不真实存在的 mediaId 服务端仍会静默返回成功,头像并不会真正更新。务必用 `dt_media_upload` / `chat media upload` 上传真实图片拿到的 mediaId。 #### 更新群设置 — 更新指定群聊的设置项 @@ -173,6 +188,30 @@ Flags: --status int 设置值: 0=关闭, 1=开启 (必填) ``` +#### 设置群备注 — 给群设置只有自己可见的备注标题 +``` +Usage: + dws chat group update-alias [flags] +Example: + dws chat group update-alias --group --alias-title "项目A群" + # 查询群 ID: dws chat search --query "群名" +Flags: + --group string 群聊 openConversationId (必填) + --alias-title string 群备注标题 (必填) +``` + +#### 设置我在群内的群昵称 — 设置当前用户在指定群里显示的昵称 +``` +Usage: + dws chat group update-nick [flags] +Example: + dws chat group update-nick --group --nick "我的群昵称" + # 查询群 ID: dws chat search --query "群名" +Flags: + --group string 群聊 openConversationId (必填) + --nick string 个人群昵称 (必填) +``` + #### 查看群内所有机器人 — 获取指定群聊中的所有机器人列表 ``` Usage: @@ -294,6 +333,8 @@ Flags: ``` #### 移除用户的指定群身份 — 从用户身上移除指定的群身份(不影响其他群身份) + +> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。需要清除某人身份时,用 `chat group-role set-user --role-ids ""`(传空 role-ids 覆盖为无身份)兜底;但 set-user 会清掉该用户的**全部**身份,无法只移除其中一个。 ``` Usage: dws chat group-role remove-user [flags] @@ -336,6 +377,7 @@ Flags: --query string 搜索关键词 (必填) --limit int 每页返回数量(默认 20) --cursor string 分页游标(默认 "0",翻页传 nextCursor) + --exclude-muted 是否排除已设置免打扰的群聊(默认 false) ``` ### data-auth (数据授权) @@ -371,7 +413,7 @@ Flags: #### 拉取会话消息内容 — 拉取指定群聊或单聊的会话消息内容 ---group 指定群聊,--user 指定单聊用户(通过 userId),--open-dingtalk-id 指定单聊用户(通过 openDingTalkId),三者互斥。默认拉取给定时间之后的消息,--forward=false 拉之前的。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。 +--group 指定群聊,--user 指定单聊用户(通过 userId),--open-dingtalk-id 指定单聊用户(通过 openDingTalkId),三者互斥。用 --direction 控制时间方向:newer=从给定时间往现在拉,older=从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。 ``` Usage: dws chat message list [flags] @@ -379,9 +421,9 @@ Example: dws chat message list --group --time "2025-03-01 00:00:00" dws chat message list --user --time "2025-03-01 00:00:00" --limit 50 dws chat message list --open-dingtalk-id --time "2025-03-01 00:00:00" --limit 50 - dws chat message list --group --time "2025-03-01 00:00:00" --forward=false + dws chat message list --group --time "2025-03-01 00:00:00" --direction older Flags: - --forward true=拉给定时间之后的消息,false=拉给定时间之前的消息 (default true) + --direction string 时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(推荐) --group string 群聊 openconversation_id(群聊时必填) --limit int 返回数量,不传则不限制 --time string 开始时间,格式: yyyy-MM-dd HH:mm:ss (必填) @@ -425,7 +467,7 @@ Example: dws chat message send --group --title "周报提醒" --text "请大家本周五前提交周报" # 幂等发送(24h 内相同 uuid 不重复投递) dws chat message send --group --text "hello" --uuid "unique-id-123" - dws chat message send --group --at-all "@all 请大家注意" + dws chat message send --group --at-all "<@all> 请大家注意" dws chat message send --group --at-open-dingtalk-ids openDingTalkId1,openDingTalkId2 "<@openDingTalkId1> <@openDingTalkId2> 请查收" # 发送图片 dws chat message send --group --msg-type image --media-id @@ -453,13 +495,14 @@ Flags: --file-path string 文件路径(msgType=file 时必填) --file-size int64 文件大小,单位字节(msgType=file 时必填) --uuid string 幂等 UUID,相同 uuid 在 24h 内不会重复发送(可选) + --ai-tag 消息是否带 AI 发送角标(可选,默认 true) 注意: - --text 和位置参数二选一,--text 优先 - --group、--user、--open-dingtalk-id 三者互斥,只需指定其一:群聊用 --group,单聊用 --user 或 --open-dingtalk-id - 纯文本/Markdown 单聊发送时 `--user` 和 `--open-dingtalk-id` 都可用;传 `--user` 时直接走 userId 发送能力 - --group 的别名: --id, --chat, --conversation-id (均可替代 --group) - - --at-all 和 --at-open-dingtalk-ids 仅在 --group 群聊时生效,单聊时无效;当设置--at-all时,消息内容中一定要包含对应的占位符@all;当设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2> + - --at-all 和 --at-open-dingtalk-ids 仅在 --group 群聊时生效,单聊时无效;当设置--at-all时,消息内容中一定要包含对应的占位符<@all>;当设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2> - **换行符**:消息内容按 Markdown 渲染,换行有两层要求,缺一不可: 1. 必须使用**真实换行符**(Unicode `U+000A`),而非字面量字符串 `\n`(反斜杠 + 字母 n)。程序或大模型构造参数时,须确保已正确反转义;否则全部内容会渲染在同一行 2. Markdown 规范下**单个换行不产生换行效果**。需要换行时请使用:段落分隔(连续两个真实换行符 `\n\n`)、行尾两个空格 + 真实换行符(硬换行 `
`),或直接写 HTML 的 `
` 标签 @@ -475,6 +518,26 @@ Flags: - 发送文字 + 文件混合消息时的完整流程:除了将文件以 Markdown 链接内嵌到文字消息中发送一条 md 消息外,还必须额外逐个发送独立的文件消息(--msg-type file),确保接收方可以直接下载原始文件。即:先发一条包含文字和文件链接的 md 消息,再对每个涉及的文件各发一条 --msg-type file 的文件消息 ``` +### media (上传媒体获取 mediaId) + +#### 上传图片/媒体获取 mediaId — 用于 chat message send --msg-type image 等 + +⚠️ 前置条件:本命令需要应用凭证。必须已通过 `dws auth login --client-id --client-secret ` 登录,或设置环境变量 `DWS_CLIENT_ID` / `DWS_CLIENT_SECRET`;否则报"缺少应用凭证"。这与其他 chat 命令走用户登录态不同。 +``` +Usage: + dws chat media upload [flags] +Example: + dws chat media upload --file ./screenshot.png + dws chat media upload --file ./photo.jpg --type image +Flags: + --file string 本地文件路径 (必填) + --type string 媒体类型: image/voice/video/file(默认 image) + +注意: + - 返回的 mediaId 可直接用于 chat message send --msg-type image --media-id + - 发图片+文字时,agent 侧一般用独立的 dt_media_upload 工具;本命令是 dws 内置的等价上传入口 +``` + ### file (会话文件上传,已下线) #### chat file upload 已下线 @@ -507,6 +570,7 @@ Flags: 注意: - openTaskId 由 `dws chat message send` 发送消息成功后返回 - 用于确认消息是否已成功发送或获取发送失败的原因 + - 返回结果中含发送成功消息的 openMessageId,可用于后续 recall(撤回)、read-status(查已读)等命令 ``` #### 撤回消息 — 撤回当前用户自己发出的消息 @@ -625,7 +689,7 @@ Flags: --topic-id string 话题 ID,由 dws chat message list 返回 (必填) --time string 开始时间,格式: yyyy-MM-dd HH:mm:ss(可选) --limit int 返回数量(默认 50) - --forward true=从老往新,false=从新往老(默认 false) + --direction string 时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(推荐,默认 older) ``` #### 拉取指定时间范围内当前用户的所有会话消息 — 分页拉取当前登录用户在指定时间范围内的所有会话消息 @@ -734,8 +798,10 @@ Usage: Example: dws chat message list-unread-conversations dws chat message list-unread-conversations --count 20 + dws chat message list-unread-conversations --exclude-muted Flags: - --count int 返回未读会话条数(可选) + --count int 返回未读会话条数(可选) + --exclude-muted 是否排除已设置免打扰的会话(默认 false) ``` #### 查询消息的已读/未读状态 @@ -942,6 +1008,7 @@ Example: Flags: --limit int 每页返回数量(默认 1000) --cursor int64 分页游标(首次不传或传 0,翻页传 nextCursor) + --exclude-muted 是否排除已设置免打扰的会话(默认 false) 注意: - 用户询问"置顶会话"时,直接调用此命令返回置顶会话列表即可 @@ -991,6 +1058,7 @@ Flags: --match-mode string 匹配模式:AND=所有人都在群里,OR=任一人在群里(默认 AND) --limit int 每页返回数量(默认 20) --cursor string 分页游标(默认 "0",翻页传 nextCursor) + --exclude-muted 是否排除已设置免打扰的群聊(默认 false) 注意: - --nicks 传人员昵称(花名),逗号分隔,如 "风雷,山乔" @@ -1022,6 +1090,76 @@ Flags: - 上传到共享空间的文件对方才能打开,上传到个人空间的文件对方无法访问 ``` +#### 引用回复消息 — 引用某条消息并回复文字(单聊/群聊均可) +``` +Usage: + dws chat message reply [flags] +Example: + dws chat message reply --conversation-id --ref-msg-id --ref-sender --text "收到,马上处理" + # 被引用消息的 openMessageId、发送者 openDingTalkId 通过 dws chat message list 获取 +Flags: + --conversation-id string 会话 openConversationId (必填,支持单聊/群聊) + --ref-msg-id string 被引用的消息 openMessageId (必填) + --ref-sender string 被引用消息的发送者 openDingTalkId (必填) + --text string 回复内容 (必填) + --ai-tag 消息是否带 AI 发送角标(可选,默认 true) + --uuid string 幂等键(可选) + +注意: + - 以当前用户身份引用回复,语义同 chat message send;目前回复类型仅支持 text +``` + +#### 转发单条消息 — 将一条消息从源会话转发到目标会话(源/目标均支持单聊/群聊) +``` +Usage: + dws chat message forward [flags] +Example: + dws chat message forward --src-conversation-id --msg-id --dest-conversation-id +Flags: + --src-conversation-id string 源会话 openConversationId (必填) + --msg-id string 源消息 openMessageId (必填) + --dest-conversation-id string 目标会话 openConversationId (必填) + --uuid string 幂等键(可选) + +注意: + - 与 combine-forward 区别: forward 转单条,combine-forward 合并多条为一条转发 +``` + +#### 转发话题消息 — 将一条话题消息转发到目标会话 +``` +Usage: + dws chat message forward-topic [flags] +Example: + dws chat message forward-topic --src-conversation-id --src-msg-id --src-thread-id --dest-conversation-id +Flags: + --src-conversation-id string 源会话 openConversationId (必填,消息所在的会话) + --src-msg-id string 源消息 openMessageId (必填,要转发的消息) + --src-thread-id string 话题 ID (必填,格式: convThread + 加密后的 convThreadId,即 message list 返回的 openConvThreadId) + --dest-conversation-id string 目标会话 openConversationId (必填,转发到的会话) +``` + +#### 置顶消息 — 将指定消息置顶到会话顶部 +``` +Usage: + dws chat message set-top-msg [flags] +Example: + dws chat message set-top-msg --open-conversation-id --msg-id +Flags: + --open-conversation-id string 会话 openConversationId (必填,支持群聊/单聊) + --msg-id string 消息 openMessageId (必填) +``` + +#### 取消置顶消息 — 取消会话顶部的置顶消息 +``` +Usage: + dws chat message unset-top-msg [flags] +Example: + dws chat message unset-top-msg --open-conversation-id --msg-id +Flags: + --open-conversation-id string 会话 openConversationId (必填,支持群聊/单聊) + --msg-id string 消息 openMessageId (必填) +``` + #### 合并转发多条消息 — 将多条消息合并后转发到目标会话(源/目标会话均支持单聊/群聊) ``` Usage: @@ -1106,10 +1244,10 @@ Flags: --size int 每页条数 (默认 50),别名: --limit ``` -#### 搜索【全部可用】机器人 — 含他人创建/官方机器人,额外返回 openDingTalkId +#### 搜索【全部可用】机器人 — 含他人创建/官方机器人,额外返回机器人 openDingTalkId 范围: 当前用户可用的全部机器人(含他人创建、官方机器人)。 -返回字段: 额外返回 openDingTalkId(可用于给机器人发单聊消息),search 没有此字段。 +返回字段: 结果在 `result.bots[]` 中,每项含 `botOpenDingTalkId`(机器人的 openDingTalkId,用于给机器人发单聊消息)和 `name`。注意字段名是 `botOpenDingTalkId`,不是 `openDingTalkId`;search 没有此字段。 典型触发词: "搜索机器人""找一个机器人""帮我找 XXX 机器人""所有可用机器人""查机器人"。 ``` @@ -1134,7 +1272,7 @@ search 与 find 选择指南: | 维度 | `chat bot search` | `chat bot find` | |------|-------------------|-----------------| | 范围 | 仅我创建的机器人 | 全部可用机器人(含他人/官方) | -| 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) | +| 额外返回机器人 openDingTalkId | 无 | 有,字段名 `botOpenDingTalkId`(可用于给机器人发单聊消息) | | 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" | ### category (会话分组管理) @@ -1154,9 +1292,70 @@ Usage: dws chat category list-conversations [flags] Example: dws chat category list-conversations --category-id <分组ID> + dws chat category list-conversations --category-id <分组ID> --exclude-muted # 分组ID 可通过 dws chat category list 获取 Flags: --category-id int 会话分组 ID (必填) + --exclude-muted 是否排除已设置免打扰的会话(默认 false) +``` + +#### 创建会话分组 +``` +Usage: + dws chat category create [flags] +Example: + dws chat category create --title "工作群" +Flags: + --title string 分组名称 (必填) +``` + +#### 删除会话分组 +``` +Usage: + dws chat category delete [flags] +Example: + dws chat category delete --category-id <分组ID> + # 分组ID 可通过 dws chat category list 获取 +Flags: + --category-id int 会话分组 ID (必填) +``` + +#### 重命名会话分组 +``` +Usage: + dws chat category rename [flags] +Example: + dws chat category rename --category-id <分组ID> --title "新名称" + # 分组ID 可通过 dws chat category list 获取 +Flags: + --category-id int 会话分组 ID (必填) + --title string 新的分组名称 (必填) +``` + +#### 将会话加入分组 +``` +Usage: + dws chat category add-conv [flags] +Example: + dws chat category add-conv --group --category-ids 123,456 + # 分组ID 可通过 dws chat category list 获取 + # 查询群 ID: dws chat search --query "群名" +Flags: + --group string 会话 openConversationId (必填) + --category-ids string 目标分组 ID 列表,逗号分隔 (必填) +``` + +#### 将会话移出分组 +``` +Usage: + dws chat category remove-conv [flags] +Example: + dws chat category remove-conv --group --category-ids 123,456 + # 分组ID 可通过 dws chat category list 获取 + # 查询群 ID: dws chat search --query "群名" +Flags: + --group string 会话 openConversationId (必填) + --category-ids string 目标分组 ID 列表,逗号分隔 (必填) ``` ### mute (会话免打扰) @@ -1205,6 +1404,8 @@ Flags: ### mute-at-all (关闭@所有人通知) #### 关闭/开启 @所有人消息提醒 — 关闭或开启会话中 @所有人的消息通知 + +> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。 ``` Usage: dws chat mute-at-all [flags] @@ -1226,6 +1427,8 @@ Flags: ### mute-red-envelope (关闭红包通知) #### 关闭/开启红包消息提醒 — 关闭或开启会话中的红包消息通知 + +> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。 ``` Usage: dws chat mute-red-envelope [flags] @@ -1260,7 +1463,7 @@ Flags: 注意: - 支持群聊和单聊,openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取 - - 标记未读后会话列表中会显示未读状态 + - API 返回成功,但该未读状态在 API 侧观察不到:`list-all-conversations` 返回的 unreadPoint 不会随之变化,只有钉钉客户端 UI 上能看到未读标记 ``` ### clear-red-point (清除会话红点) @@ -1308,13 +1511,14 @@ Example: dws chat list-all-conversations --limit 100 --cursor dws chat list-all-conversations --exclude-muted Flags: - --limit int 每页数量(默认 1000) + --limit int 每页数量(1-100,默认 100);传 >100 会被明确拒绝 --cursor int 分页游标(首次不传或传 0,翻页传 nextCursor) --exclude-muted 是否排除已免打扰会话(默认 false) 注意: - 返回结果包含单聊和群聊,不区分会话类型 - - 翻页: hasMore=true 时用返回的 nextCursor 作为下次 --cursor + - --limit 范围 1-100,默认 100,上限 100;传入 >100 会报错拒绝,不会静默截断 + - 分页当前不可用:真机 hasMore 恒为 false、nextCursor 恒为 null,本命令最多返回 100 条会话,无法用 --cursor 继续翻页取更多 - 与 list-top-conversations 的区别: 本命令返回全部会话(单聊+群聊),list-top-conversations 仅返回置顶会话 ``` @@ -1375,6 +1579,7 @@ Flags: 注意: - 与 `chat group list-my-groups` 区别: list-all 返回用户加入的所有群;list-my-groups 仅返回用户作为群主/管理员的群 - 分页: hasMore=true 时用返回的 nextCursor 作为下次 --cursor + - ⚠️ 存在同步盲区:新建群后较长时间(实测 15 分钟后全量翻页仍查不到)内不会出现在 list-all 里;而 `chat group list-my-groups` / `chat search` 能立即查到。要确认刚建的群,用 list-my-groups 或 search,别依赖 list-all ``` ### group list-join-validations (分页拉取入群验证记录) @@ -1401,27 +1606,27 @@ Flags: ### group audit-join-validation (审批入群验证) -#### 审批入群验证 — 通过、拒绝、删除单个审核 +#### 审批入群验证 — 通过、删除单个审核 -支持通过、拒绝、删除、忽略、拒绝并拉黑等操作。 +真机当前仅 AuditApprove(通过)和 AuditDelete(删除)两个动作可用。 ``` Usage: dws chat group audit-join-validation [flags] Example: dws chat group audit-join-validation --group --record-id 123456 --applicant --inviter --status AuditApprove - dws chat group audit-join-validation --group --record-id 123456 --applicant --inviter --status AuditRefuse --description "不符合入群条件" + dws chat group audit-join-validation --group --record-id 123456 --applicant --inviter --status AuditDelete # 查询入群验证记录: dws chat group list-join-validations Flags: --group string 群 openConversationId (必填) --record-id string 申请记录 ID (必填) --applicant string 申请人 openDingTalkId (必填) --inviter string 邀请人 openDingTalkId (必填) - --status string 审批动作: AuditApprove/AuditDelete/AuditIgnore/AuditRefuse/AuditBlock (必填) + --status string 审批动作: AuditApprove(通过) / AuditDelete(删除) (必填) --description string 审批说明(可选) 注意: - - status 可选值: AuditApprove(通过), AuditDelete(删除), AuditIgnore(忽略), AuditRefuse(拒绝), AuditBlock(拒绝且拉黑) + - status 真机仅支持 AuditApprove(通过) 和 AuditDelete(删除);AuditIgnore(忽略)、AuditRefuse(拒绝)、AuditBlock(拒绝且拉黑) 会被服务端拒绝报 unsupported audit status,属服务端限制 - record-id、applicant、inviter 可通过 dws chat group list-join-validations 查询获得 ``` @@ -1447,6 +1652,9 @@ Flags: 用户说"踢人/移除群成员" → `chat group members remove` 用户说"加机器人到群" → `chat group members add-bot` 用户说"改群名" → `chat group rename` +用户说"设置群备注/给群加备注" → `chat group update-alias` +用户说"改我在群里的昵称/设置群昵称" → `chat group update-nick` +用户说"批量查群成员信息/按ID查群成员" → `chat group members list-by-ids` 用户说"聊天记录/会话消息/拉取会话" → `chat message list` 用户说"某人发给我的消息/指定发送者/某人的消息" → `chat message list-by-sender`(用户未明确说"单聊"时优先使用,跨单聊/群聊) 用户说"拉取和某人的单聊记录/单聊消息" → `chat message list --user`(用户明确说"单聊"时使用) @@ -1470,6 +1678,11 @@ Flags: 用户说"置顶会话/置顶消息/我的置顶/查看置顶" → `chat list-top-conversations` 用户说"查看会话分组/自定义分组" → `chat category list` 用户说"某个分组下的会话/分组会话列表" → `chat category list-conversations` +用户说"新建会话分组/创建分组" → `chat category create` +用户说"删除会话分组" → `chat category delete` +用户说"重命名分组/改分组名" → `chat category rename` +用户说"把会话加入分组/会话归类到分组" → `chat category add-conv` +用户说"把会话移出分组/从分组移除会话" → `chat category remove-conv` 用户说"根据群号查群信息/群号查群/群号转openConversationId" → `chat group get-by-group-id`(当用户发消息时只提供了群号,用此工具将群号转为 openConversationId,再调用发消息接口) 用户说"查看群身份/群的自定义身份列表" → `chat group-role list` 用户说"创建/添加群身份" → `chat group-role add` @@ -1504,9 +1717,13 @@ Flags: 用户说"引用回复/回复消息/引用消息回复" → `chat message reply` 用户说"转发消息/转发一条消息/把消息转发到另一个群" → `chat message forward` 用户说"合并转发/批量转发/合并转发多条消息" → `chat message combine-forward` +用户说"转发话题/转发话题消息" → `chat message forward-topic` +用户说"置顶消息/把消息置顶" → `chat message set-top-msg` +用户说"取消置顶消息/撤销消息置顶" → `chat message unset-top-msg` +用户说"上传图片拿mediaId/上传媒体" → `chat media upload` 用户说"群机器人列表/群里有哪些机器人/查看群机器人" → `chat group bots` 用户说"从群里移除机器人/踢出机器人" → `chat group members remove-bot` -用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 openDingTalkId 可发单聊) +用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 botOpenDingTalkId 可发单聊) 用户说"给机器人发单聊/给机器人发消息/跟机器人聊天" → 必须先 `chat bot find`(拿 openDingTalkId)→ 再 `chat message send --open-dingtalk-id`(search 没有 openDingTalkId,无法发单聊) 用户说"我创建的机器人/我的机器人/我自己的机器人/查看我的机器人" → `chat bot search`(仅我创建的机器人,无 openDingTalkId) 用户说"解散群/解散群聊" → `chat group dismiss` @@ -1783,7 +2000,7 @@ Flags: | `aisearch person` | `userId` | message send 的 --user、send-by-bot 的 --users、send-by-bot 的 --at-user-ids、list-by-sender 的 --sender-user-id | | `aisearch person` → `contact user get` | `openDingTalkId` | message send 的 --at-open-dingtalk-ids、--open-dingtalk-id、send-by-bot 的 --open-dingtalk-ids、send-by-bot 的 --at-open-dingtalk-ids、list-by-sender 的 --sender-open-dingtalk-id、message list 的 --open-dingtalk-id | | `chat bot search` | `robotCode` | send-by-bot / recall-by-bot 的 --robot-code(仅我创建的机器人,无 openDingTalkId) | -| `chat bot find` | `openDingTalkId` | 给机器人发单聊消息(全部可用机器人,额外返回 openDingTalkId) | +| `chat bot find` | `botOpenDingTalkId` | 给机器人发单聊消息(send --open-dingtalk-id;字段名是 botOpenDingTalkId,非 openDingTalkId) | | `chat message send-by-bot` | `processQueryKey` | recall-by-bot 的 --keys | | `chat message send` | `openTaskId` | query-send-status 的 --open-task-id | | `chat message list` | `openMessageId` | recall 的 --msg-id | @@ -1826,7 +2043,7 @@ Flags: - `chat search-common` 搜索共同群,`--nicks` 传人员昵称(逗号分隔),`--match-mode` AND/OR 控制匹配逻辑,分页用 `--limit`(默认 20)/`--cursor` - `chat list-top-conversations` 拉取置顶会话列表,分页用 `--limit`(默认 1000)/`--cursor`;用户询问"置顶会话"或"置顶消息"时均路由到此命令 - `--user` 和 `--open-dingtalk-id` 本质上都是发起单聊操作,只是用户标识格式不同:userId 为企业内部应用常用标识,openDingTalkId 为三方应用或跨组织场景下的用户标识,服务端对两种 ID 的解析逻辑不同 -- `--time` 格式: `yyyy-MM-dd HH:mm:ss`,为拉取消息的起始时间点;`--forward` 控制方向(默认 true,拉给定时间之后的消息),`--limit` 控制数量 +- `--time` 格式: `yyyy-MM-dd HH:mm:ss`,为拉取消息的起始时间点;`--direction` 控制方向(newer=从给定时间往现在拉,older=从给定时间往以前拉),`--limit` 控制数量 - `chat search` 挂在 `chat` 下(非 `chat group` 下),路径为 `dws chat search` - `send-by-bot` 群聊传 `--group`,单聊传 `--users` 或 `--open-dingtalk-ids`,与 `--group` 互斥且必选其一;群聊时可选 `--at-user-ids` @指定成员(传 userId 列表)或 `--at-open-dingtalk-ids` @指定成员(传 openDingtalkId 列表),content 中需包含对应 @标识;`--at-all` @所有人;群聊场景如果返回"机器人不存在"错误,需先通过 `chat group members add-bot --group --robot-code ` 将机器人邀请进群后再发送 - `recall-by-bot` 群聊传 `--group` + `--keys`,单聊仅传 `--keys`(不传 `--group` 即为单聊撤回) diff --git a/skills/mono/references/products/contact.md b/skills/mono/references/products/contact.md index dbc7846e..74036dc8 100644 --- a/skills/mono/references/products/contact.md +++ b/skills/mono/references/products/contact.md @@ -38,9 +38,10 @@ Example: dws contact user search --query "张三" Flags: --query string 搜索关键词 (必填) -Returns: (列表,每项包含以下字段) +Returns: (顶层 `result` 为列表,每项包含以下字段) name string 成员姓名 nick string 成员昵称 + flowerName string 花名(无花名时为 null) userId string 成员 ID(仅同事关系时返回) title string 员工职位(仅同事关系时返回) openDingTalkId string 当前用户视角下的目标用户唯一标识,不可跨用户共享;可用于发消息等好友关系场景的操作 @@ -84,6 +85,8 @@ Flags: 查询花名册有权限的字段列表,根据当前用户查询花名册有权限的字段列表。认证信息(corpId、optUserId)由系统自动注入,无需手动传入。 +> **前置条件:需操作人具备花名册管理权限。** 无权限时返回业务错误 `操作人无花名册管理权限`(非命令写法问题,不要改参数)。 + #### 查询员工花名册字段信息(个人档案) ``` Usage: @@ -100,6 +103,8 @@ Flags: 花名册字段包含:试用/转正信息、个人/家庭信息、学历信息、银行卡/合同信息、紧急联系人和其他企业自定义信息。 > **与 `contact user get` 的区别**:`user get` 返回组织管理信息(部门、主管、管理员权限),`user profile get` 返回个人档案信息(学历、家庭、银行卡等)。 +> +> **前置条件:需操作人具备花名册管理权限。** 无权限时返回业务错误 `操作人无花名册管理权限`(非命令写法问题,不要改参数)。 ### dismission (离职员工) @@ -126,6 +131,8 @@ Flags: 查询离职员工列表,支持按员工姓名、离职日期范围、部门进行过滤。认证信息(corpId、optUserId)由系统自动注入,无需手动传入。 `--start` 和 `--end` 必须同时设置或同时不设置,不允许只传其中一个。 +> **前置条件:需操作人具备已离职人员花名册管理权限。** 无权限时返回业务错误 `操作人无已离职人员花名册管理权限`(非命令写法问题,不要改参数)。 + ### dept (部门查询) #### 搜索部门 @@ -136,6 +143,9 @@ Example: dws contact dept search --query "技术部" Flags: --query string 搜索关键词 (必填) +Returns: (顶层 key 为 `deptList`,非 `result`,为列表,每项包含以下字段) + deptId int 部门 ID + deptName string 部门名称(命中关键词会用 … 高亮包裹,需自行去标签) ``` #### 获取部门详情 @@ -180,6 +190,9 @@ Example: dws contact dept list-members --depts 1 # 根部门 Flags: --depts string 部门 ID 列表,逗号分隔 (必填) +Returns: (顶层 key 为 `deptUserList`,非 `result`,为列表,每项形如 { "userInfo": { "name": ..., "userId": ... } }) + userInfo.name string 成员姓名 + userInfo.userId string 成员 ID Notes: - **钉钉根部门 `deptId=1`**;查根部门直属成员用 `--depts 1` - 仅返回**本部门**直接成员,**不含下级部门**成员;需含下级请先 `dept list-children` 枚举子部门,再对子 deptId 分别/合并调用 `list-members` diff --git a/skills/mono/references/products/dev.md b/skills/mono/references/products/dev.md index 3009fa2e..b0a2a9ae 100644 --- a/skills/mono/references/products/dev.md +++ b/skills/mono/references/products/dev.md @@ -6,7 +6,7 @@ |--------|------| | `dev app` | 应用生命周期(创建/查询/更新/删除/凭证/权限/成员/安全/网页/机器人/**建号**/版本/事件订阅) | | `dev connect` | **建联**:把现成机器人接到当前本地 agent(起 Stream,不建号) | -| `dev doc` | 开放平台开发文档搜索(同 `dws devdoc`) | +| `dev doc` | 开放平台开发文档搜索入口(当前网关未注册该工具键,`dev doc search` 会报「未找到指定工具」不可用;文档搜索一律走 `dws devdoc article search --query <关键词>`) | > ⚠️ **关键区分**:`dws chat bot search/find` 只查询已有机器人(IM 视角);**创建/建号**机器人走 `dws dev app robot submit`;**建联**走 `dws dev connect`。"创建机器人"/"建联"一律走 `dev`,禁止走 `chat`。 @@ -75,8 +75,9 @@ dws dev app get --unified-app-id --format json # 创建应用 dws dev app create --name <名称> --desc <描述> --format json -# 更新应用信息 -dws dev app update --unified-app-id --name <新名称> --format json +# 更新应用信息(写操作:先 --dry-run 预览,确认后加 --yes 执行;不加 --yes 会被拦下) +dws dev app update --unified-app-id --name <新名称> --dry-run --format json +dws dev app update --unified-app-id --name <新名称> --yes --format json # 停用/启用应用 dws dev app disable --unified-app-id --yes --format json @@ -186,8 +187,8 @@ dws dev app event unsubscribe --unified-app-id --event-codes --format json -dws dev app permission add --unified-app-id --scope-code --format json -dws dev app permission remove --unified-app-id --scope-code --yes --format json +dws dev app permission add --unified-app-id --scope-values --format json +dws dev app permission remove --unified-app-id --scope-values --yes --format json ``` --- diff --git a/skills/mono/references/products/ding.md b/skills/mono/references/products/ding.md index fb53c856..1223a0fa 100644 --- a/skills/mono/references/products/ding.md +++ b/skills/mono/references/products/ding.md @@ -31,12 +31,12 @@ Flags: Usage: dws ding message list [flags] Example: - dws ding message list + dws ding message list --type ALL dws ding message list --type UNREAD dws ding message list --type SEND --cursor 10 Flags: --cursor int 分页游标 (首次传 0, 翻页传返回的 nextCursor) - --type string 消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED (可选, 不传返回全部) + --type string 消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED (默认 ALL; 服务端不接受空值, CLI 不传时会自动按 ALL 查询, 故裸跑 `ding message list` 即可) ``` ### 查看 DING 接收状态 @@ -45,7 +45,7 @@ Usage: dws ding message receiver-status [flags] Example: dws ding message receiver-status --ding-id - # 查询 dingId: dws ding message list + # 查询 dingId: dws ding message list --type ALL Flags: --ding-id string DING 消息 openDingId (必填) ``` @@ -76,13 +76,13 @@ Usage: dws ding message recall-personal [flags] Example: dws ding message recall-personal --id - # 查询 openDingId: dws ding message list + # 查询 openDingId: dws ding message list --type ALL Flags: --id string DING 消息 openDingId (必填) 注意: - 与 `ding message recall`(机器人身份)不同:recall-personal 以当前用户身份撤回,无需 --robot-code - - openDingId 可通过 `dws ding message list` 或 `send-personal` 返回值获取 + - openDingId 可通过 `dws ding message list --type ALL` 或 `send-personal` 返回值获取 ``` ### 消息转 DING — 将聊天消息转为 DING 通知发送给指定接收者 diff --git a/skills/mono/references/products/doc.md b/skills/mono/references/products/doc.md index 04d21a9d..913016ac 100644 --- a/skills/mono/references/products/doc.md +++ b/skills/mono/references/products/doc.md @@ -9,7 +9,7 @@ dws doc --help # 查看具体命令的完整参数说明 -dws doc list --help +dws doc read --help dws doc create --help dws doc block insert --help @@ -25,46 +25,19 @@ dws doc permission --help ## 命令总览 -### 搜索文档 -``` -Usage: - dws doc search [flags] -Example: - dws doc search --query "会议纪要" - dws doc search - dws doc search --extensions pdf,docx - dws doc search --query "方案" --created-from 1700000000000 --created-to 1710000000000 - dws doc search --creator-uids uid1,uid2 - dws doc search --workspace-ids wsId1,wsId2 -Flags: - --query string 搜索关键词 (不传则返回最近访问) - --extensions strings 按文件扩展名过滤,不含点号,逗号分隔 (如 pdf,docx,png)。支持的在线文档类型后缀名: adoc=文字, axls=表格, appt=演示文稿, awbd=白板, adraw=画板, amind=脑图, able=多维表格, aform=收集表 - --created-from int 创建时间起始 (毫秒时间戳,含) - --created-to int 创建时间截止 (毫秒时间戳,含) - --visited-from int 访问时间起始 (毫秒时间戳,含) - --visited-to int 访问时间截止 (毫秒时间戳,含) - --creator-uids strings 按创建者用户 ID 过滤,逗号分隔 - --editor-uids strings 按编辑者用户 ID 过滤,逗号分隔 - --mentioned-uids strings 按 @提及的用户 ID 过滤,逗号分隔 - --workspace-ids strings 按知识库 ID 过滤,支持知识库 URL,逗号分隔 - --page-size int 每页数量 (默认 10,最大 30) - --page-token string 分页游标 (从上次结果的 nextPageToken 获取) -``` +### 搜索 / 遍历文件(已迁移,不再是 doc 子命令) -### 遍历文件列表 -``` -Usage: - dws doc list [flags] -Example: - dws doc list - dws doc list --folder - dws doc list --workspace --page-size 20 -Flags: - --folder string 文件夹 ID 或 URL - --workspace string 知识库 ID - --page-size int 每页数量 (默认 50,最大 50) - --page-token string 分页游标 (从上次结果的 nextPageToken 获取) -``` +> **弃用提示**:`dws doc search` 和 `dws doc list` 已迁移到 `drive` / `wiki`。真机上这两条虽仍能跑,但每次都会打印弃用警告,请改用下面的命令,命令详情见 [drive.md](./drive.md)。 + +| 旧命令(弃用) | 改用 | 场景 | +|--------------|------|------| +| `dws doc search --query "<关键词>"` | `dws drive search --query "<关键词>"` | 全局搜文档/文件(跨钉盘+文档空间聚合) | +| `dws doc search --workspace-ids ` | `dws wiki node search --workspace --query "<关键词>"` | 指定知识库内搜索 | +| `dws doc list` | `dws drive list` | 遍历「我的文档」/钉盘根目录 | +| `dws doc list --folder ` | `dws drive list --folder ` | 遍历指定文件夹 | +| `dws doc list --workspace ` | `dws drive list --workspace ` 或 `dws wiki node list --workspace ` | 遍历知识库 | + +拿到 `nodeId` 后,`doc read` / `doc info` / `doc update` / `doc block` 等内容级命令照常用。 ### 获取文档元信息 ``` @@ -108,22 +81,26 @@ Flags: --content-file string 从文件读取初始 Markdown ``` -### 创建其他类型文件 (表格/脑图/白板/多维表/画板) +### 创建其他类型文件 (表格/脑图/白板/多维表/画板/文件夹) + +> **弃用提示**:`dws doc file create --type` 已弃用,真机每次执行都会打印 `deprecated, use 'dws wiki node create --type '`。创建各类型文件节点改用 `dws wiki node create`(与 intent-guide 一致);创建普通文件夹用 `dws drive mkdir`。 + ``` Usage: - dws doc file create [flags] + dws wiki node create [flags] Example: - dws doc file create --name "项目周报" --type adoc - dws doc file create --name "数据统计" --type axls --folder - dws doc file create --name "思维导图" --type amind --workspace - dws doc file create --name "子文件夹" --type folder + dws wiki node create --workspace --name "数据统计" --type axls + dws wiki node create --workspace --name "思维导图" --type amind + dws wiki node create --workspace --name "方案目录" --type folder --folder Flags: + --workspace string 目标知识库 ID (必填 — wiki node create 不支持不带 workspace 创建) --name string 文件名称 (必填) - --type string 文件类型 (必填): adoc=文档, axls=表格, appt=演示, adraw=白板, amind=脑图, able=多维表, folder=文件夹 - --folder string 目标文件夹 ID 或 URL - --workspace string 目标知识库 ID 或 URL + --type string 文件类型: adoc=文档, axls=表格, appt=演示, adraw=白板, amind=脑图, able=多维表, folder=文件夹 + --folder string 父节点 ID (选填) ``` +> 说明:`wiki node create` **必须**带 `--workspace`(知识库上下文),不能像旧 `doc file create` 那样在「我的文档」个人空间直接建。要在个人空间/钉盘建普通文件夹,用 `dws drive mkdir --name "<名称>"`(见 [drive.md](./drive.md))。 + ### 更新文档内容 ``` Usage: @@ -154,31 +131,24 @@ Flags: --convert 是否转换为钉钉在线文档 ``` -### 下载文件到本地 +### 下载文件到本地(已迁移到 drive) + +> **弃用提示**:`dws doc download` 已迁移到 `dws drive download`(真机执行 `doc download` 会打印弃用警告)。下载已有文件(PDF/图片/附件等非在线文档)改用: + ``` -Usage: - dws doc download [flags] -Example: - dws doc download --node - dws doc download --node --output ./report.pdf - dws doc download --node "https://alidocs.dingtalk.com/i/nodes/" --output ~/downloads/ -Flags: - --node string 文件节点 ID 或 URL (必填) - --output string 本地保存路径 (文件路径或目录,必填) +dws drive download --node --output ~/downloads/ +dws drive download --node --output ./report.pdf ``` -### 创建文件夹 -``` -Usage: - dws doc folder create [flags] -Example: - dws doc folder create --name "项目资料" - dws doc folder create --name "子文件夹" --folder -Flags: - --name string 文件夹名称 (必填) - --folder string 父文件夹 ID 或 URL - --workspace string 目标知识库 ID -``` +> 注意区分:**在线文档(ALIDOC)导出为 docx** 仍走 `doc export`(内容级命令,未迁移);`download` 只下载已有文件。命令详情见 [drive.md](./drive.md)。 + +### 创建文件夹(已迁移) + +> **弃用提示**:`dws doc folder create` 已弃用。创建文件夹改用: +> - 个人空间/钉盘 → `dws drive mkdir --name "<名称>" [--folder <父节点>]` +> - 知识库内 → `dws wiki node create --workspace --name "<名称>" --type folder` +> +> 命令详情见 [drive.md](./drive.md)。 ### 复制文档/文件 ``` @@ -459,7 +429,7 @@ CLI **不会**自动执行回读验证。**Agent 必须在文档写入完成后 Usage: dws doc delete [flags] Example: - dws doc delete --node --format json # 查询 nodeId: dws doc search --query "..." 或 dws doc list + dws doc delete --node --format json # 查询 nodeId: dws drive search --query "..." 或 dws drive list Flags: --node string 文档/文件 ID 或 URL (必填) ``` @@ -604,7 +574,7 @@ Flags: - `dentryUuid` 是 `alidocs` URL `/i/nodes/{dentryUuid}` 的最后一段,在 `doc` 场景中等价于可传入 CLI 的 `nodeId`;不要把它改写成数字 ID。 - `dentryId` 通常是纯数字,**不是** `doc` 的 `nodeId`,也不是 `doc --folder` 的目标文件夹 ID;不要把数字 `dentryId` 当作 `--node`、`--folder`,也不要当作隐藏兼容 alias(如 `--parent-id` / `--parent-folder-id`)的取值。 - 目标父文件夹的 canonical flag 是 `--folder `,目标知识库使用 `--workspace `。CLI 可能接受 `--parent-id` / `--parent-folder-id` 作为隐藏兼容 alias,但 skill 正文和示例应统一教 `--folder`,不要把 drive/chat 链路里的数字 parentId 搬到 doc 命令。 -- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `doc search` / `doc list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。 +- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `drive search` / `drive list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。 ### 处理流程 @@ -612,15 +582,15 @@ Flags: 用户输入含 alidocs.dingtalk.com URL → 若是用户直接提供的原始 URL,先按链接规范做 probe → 提取 DOC_ID(URL 路径最后一段) - → 结合用户意图选择命令(doc 默认 read,folder 默认 list,file 默认 download) + → 结合用户意图选择命令(doc 默认 read,folder 默认 drive list,file 默认 drive download) → 将 DOC_ID 传给 --node 参数 ``` ## 意图判断 用户说"找文档/搜文档/最近文档": -- 搜索 → `search` -- 浏览 → `list` +- 搜索 → `dws drive search`(全局)或 `dws wiki node search --workspace `(指定知识库) +- 浏览 → `dws drive list`(`doc search` / `doc list` 已弃用,见本文顶部「搜索 / 遍历文件」) 用户说"看文档/读内容/文档内容": - 读取 → `read` (需文档 ID 或 URL) @@ -637,27 +607,29 @@ Flags: > - 用户说"创建多维表格/新建 AI 表格/建 base/数据库表" → 走 [`dws aitable base create`](./aitable.md)(able 多维表格) 用户说"新建脑图/白板/多维表/演示文稿": -- 用 `file create --type` 指定类型 (axls/amind/adraw/able/appt/adraw) +- 用 `dws wiki node create --workspace --type ` 指定类型 (axls/amind/adraw/able/appt);`doc file create` 已弃用 +- 注意 `wiki node create` 必须带 `--workspace`(知识库上下文) 用户说"建文件夹/新建目录": -- 创建 → `folder create` 或 `file create --type folder` +- 个人空间/钉盘 → `dws drive mkdir --name "<名称>"` +- 知识库内 → `dws wiki node create --workspace --type folder`(`doc folder create` / `doc file create --type folder` 已弃用) 用户说"上传文件/传文件/上传到文档/上传到知识库": - 上传 → `upload`(需本地文件路径) - 上传并转换 → `upload --convert` 用户说"下载/导出/下载到本地/导出文档/导出为Word/导出为docx/把文档导出来": -- **必须先判断目标文件类型**,再决定走 `export` 还是 `download`: - - 在线文档(alidocs/adoc)→ **`export`**(格式转换后导出为 docx) - - 已有文件(PDF、图片、附件、视频等非在线文档)→ **`download`**(直接下载原始文件) +- **必须先判断目标文件类型**,再决定走 `doc export` 还是 `drive download`: + - 在线文档(alidocs/adoc)→ **`doc export`**(内容级命令,格式转换后导出为 docx,未迁移) + - 已有文件(PDF、图片、附件、视频等非在线文档)→ **`drive download`**(`doc download` 已弃用) - 判断方法: - 1. 用户明确说"导出文档"/"导出为 Word/docx" → 直接走 `export` - 2. 用户明确说"下载 PDF/图片/附件" → 直接走 `download` - 3. 不确定时,先用 `info --node ` 查询节点信息,根据 `contentType` 判断: - - `contentType` = `ALIDOC` → 走 `export` - - `contentType` = `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `download` + 1. 用户明确说"导出文档"/"导出为 Word/docx" → 直接走 `doc export` + 2. 用户明确说"下载 PDF/图片/附件" → 直接走 `drive download` + 3. 不确定时,先用 `doc info --node ` 查询节点信息,根据 `contentType` 判断: + - `contentType` = `ALIDOC` → 走 `doc export` + - `contentType` = `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `drive download` -> **严禁将"导出文档"直接路由到 `download`**。`download` 只能下载已有文件(原样下载),`export` 是将在线文档格式转换后导出为 docx,两者完全不同。 +> **严禁将"导出文档"直接路由到 `drive download`**。`drive download` 只能下载已有文件(原样下载),`doc export` 是将在线文档格式转换后导出为 docx,两者完全不同。 用户说"复制文档/拷贝一份/复制到": - 复制 → `copy` (需源 --node 和目标 --folder/--workspace) @@ -702,7 +674,7 @@ Flags: **用户直接粘贴文档 URL(无其他指令)**: - 默认 → `read`(读取文档内容) -- 如 URL 明显是文件夹 → `list`(列出文件夹内容) +- 如 URL 明显是文件夹 → `drive list`(列出文件夹内容;doc list 已弃用) **用户粘贴 URL + 附加指令**: - "帮我看看这个文档" → `read` @@ -728,7 +700,7 @@ Flags: **用户直接粘贴文档 URL(无其他指令)**: - 默认 → `read`(读取文档内容) -- 如 URL 明显是文件夹 → `list`(列出文件夹内容) +- 如 URL 明显是文件夹 → `drive list`(列出文件夹内容;doc list 已弃用) **用户粘贴 URL + 附加指令**: - "帮我看看这个文档" → `read` @@ -743,11 +715,11 @@ Flags: ```bash # ── 工作流 1: 浏览并阅读文档 ── -# 1. 浏览我的文档根目录 -dws doc list --format json +# 1. 浏览我的文档/钉盘根目录(doc list 已弃用 → drive list) +dws drive list --format json # 2. 浏览子文件夹 -dws doc list --folder --format json +dws drive list --folder --format json # 3. 获取文档元信息 (标题、类型、权限) dws doc info --node --format json @@ -757,8 +729,8 @@ dws doc read --node --format json # ── 工作流 2: 创建文档并写入内容 ── -# 1. (可选) 创建文件夹 — 提取 nodeId -dws doc folder create --name "项目资料" --format json +# 1. (可选) 创建文件夹 — 提取 nodeId(doc folder create 已弃用 → drive mkdir) +dws drive mkdir --name "项目资料" --format json # 2. 创建文档 — 提取 nodeId dws doc create --name "项目周报" --folder --format json @@ -782,18 +754,18 @@ dws doc upload --file ./slides.pptx --name "Q1汇报.pptx" --folder --convert # ── 工作流 5: 下载/导出文件到本地 ── -# 必须先用 info 判断文件类型,再决定用 download 还是 export: -# - contentType 为 ALIDOC(在线文档)→ 用 export -# - contentType 为 DOCUMENT/IMAGE/VIDEO 等(已有文件)→ 用 download +# 必须先用 info 判断文件类型,再决定用 drive download 还是 doc export: +# - contentType 为 ALIDOC(在线文档)→ 用 doc export +# - contentType 为 DOCUMENT/IMAGE/VIDEO 等(已有文件)→ 用 drive download # 步骤 1: 查询文件类型 dws doc info --node --format json # 根据返回的 contentType 字段判断: -# 如果是已有文件 (非 ALIDOC),用 download: -dws doc download --node --output ~/downloads/ +# 如果是已有文件 (非 ALIDOC),用 drive download(doc download 已弃用): +dws drive download --node --output ~/downloads/ -# 如果是在线文档 (ALIDOC),用 export: +# 如果是在线文档 (ALIDOC),用 doc export: dws doc export --node --output ~/downloads/ # ── 工作流 6: 上传附件并插入文档 ── @@ -842,10 +814,10 @@ dws doc block delete --node --block-id --yes # 获取 nodeId 的三种方式(按场景选择,无需全部执行): # 方式 A: 用户直接提供文档 URL — 直接传给 --node,无需额外查询 -# 方式 B: 搜索文档 — 从返回中提取 nodeId -dws doc search --query "项目周报" --format json -# 方式 C: 浏览文件夹 — 从返回中提取 nodeId -dws doc list --folder --format json +# 方式 B: 搜索文档 — 从返回中提取 nodeId(doc search 已弃用 → drive search) +dws drive search --query "项目周报" --format json +# 方式 C: 浏览文件夹 — 从返回中提取 nodeId(doc list 已弃用 → drive list) +dws drive list --folder --format json # 注意: 这里的 是文件夹 nodeId/dentryUuid 或文件夹 URL,不是数字 dentryId;示例统一使用 canonical --folder。 # 复制文档到指定文件夹(--node 支持 ID 或 URL) @@ -907,19 +879,16 @@ dws doc export --node --output ./exported.docx | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `list` | `nodes[].nodeId` | read / info / update / block 操作的 --node | -| `list` | folder 类型的 `nodeId` | list 的 --folder, create 的 --folder | -| `search` | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update 的 --node;创建时间与创建者信息 | +| `drive list`(原 `doc list`,已弃用) | `nodes[].nodeId` / folder 类型 `nodeId` | read / info / update / block 的 --node;folder 用作 `--folder` | +| `drive search`(原 `doc search`,已弃用) | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update 的 --node;创建时间与创建者信息 | | `create` | `nodeId` | update / block 操作的 --node | -| `folder create` | `nodeId` | create / list / upload 的 --folder | +| `drive mkdir`(原 `doc folder create`,已弃用) | `nodeId` | create 的 --folder | | `block list` | `blockId` | block insert 的 --ref-block, block update/delete 的 --block-id | -| `upload` | `nodeId` / URL | 上传后文件的访问链接 | -| `download` | 本地文件路径 | 下载后的文件保存位置 | | `comment list` | `commentList[].commentKey` | comment reply 的 --comment-key | | `comment create` / `comment create-inline` | `commentKey` | comment reply 的 --comment-key | | `block list` | `blockId` + 文本内容 | comment create-inline 的 --block-id 及 --start/--end 计算 | | `contact user search` | `userId` | comment create / create-inline / reply 的 --mention | -| `file create` | `nodeId` | 后续 read / update / block 操作的 --node(仅 adoc 支持 read/update,axls/amind 等类型用各自产品的命令) | +| `wiki node create`(原 `doc file create`,已弃用) | `nodeId` | 后续 read / update / block 操作的 --node(仅 adoc 支持 read/update,axls/amind 等类型用各自产品的命令) | | `copy` / `move` | 新 `nodeId`(copy)或原 nodeId(move) | 后续 read / info 等的 --node | ## nodeId 多格式说明 @@ -998,7 +967,7 @@ EOF - `read` 返回 Markdown 格式的文档内容,仅限有"下载"权限的文档 - `read` 返回的内容中,文档里的附件会以 OSS 临时下载链接形式给出(如 `https://alidocs2.oss-cn-zhangjiakou.aliyuncs.com/res/.../att/.ext?Expires=...`),该链接会过期。链接过期后,可从 URL 路径中提取 ``(即 `/att/` 后、扩展名前的 UUID 部分),然后使用 `media download --node --resource-id ` 重新获取下载链接 - `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录 -- `create` 只能建"文档"(adoc);要建表格/脑图/白板/多维表/演示/文件夹,用 `file create --type` +- `create` 只能建"文档"(adoc);要建表格/脑图/白板/多维表/演示,用 `dws wiki node create --workspace --type `(`doc file create` 已弃用);建普通文件夹用 `dws drive mkdir` - `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;简单内容追加建议用 `update --mode append` - `block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型 (table, callout 等) 使用 `--element` JSON - `--content` 参数中的换行必须使用**真实换行符**(即实际的换行字符,Unicode `U+000A`),而不是字面量字符串 `\n`(反斜杠加字母 n)。在通过程序或大模型构造此参数时,请确保字符串在发送前已正确反转义。如果传入的是两个字符的字面量 `\n`,所有内容将渲染在同一行,导致标题、段落和表格格式全部错乱。**含多行/表格/长文本时优先用 `--content-file path.md` 或 `--content -`(stdin),不经过 shell escape,换行和表格都保持原样**(详见下方「长 Markdown 写入」)。 diff --git a/skills/mono/references/products/doc/doc-file-ops.md b/skills/mono/references/products/doc/doc-file-ops.md index f375a2bd..cabaaa11 100644 --- a/skills/mono/references/products/doc/doc-file-ops.md +++ b/skills/mono/references/products/doc/doc-file-ops.md @@ -3,6 +3,11 @@ > **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: > 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 +> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令在真机上虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令: +> - `doc download` → **`dws drive download`**(下载已有文件;在线文档导出 docx 仍走 `doc export`) +> - `doc folder create` → **`dws drive mkdir`**(个人空间/钉盘)或 **`dws wiki node create --workspace --type folder`**(知识库内) +> - `doc upload` / `doc copy` / `doc move` / `doc rename` / `doc delete` 也在同一迁移方向(→ `drive`),命令用法以 `drive` 为准。 + --- ## doc upload(上传文件到钉钉文档/知识库) @@ -130,7 +135,7 @@ Flags: Usage: dws doc delete [flags] Example: - dws doc delete --node --format json # 查询 nodeId: dws doc search --query "..." 或 dws doc list + dws doc delete --node --format json # 查询 nodeId: dws drive search --query "..." 或 dws drive list Flags: --node string 文档/文件 ID 或 URL (必填) ``` diff --git a/skills/mono/references/products/doc/doc-list.md b/skills/mono/references/products/doc/doc-list.md index 0be541fe..21a62bb3 100644 --- a/skills/mono/references/products/doc/doc-list.md +++ b/skills/mono/references/products/doc/doc-list.md @@ -1,58 +1,24 @@ -# doc list(遍历文件列表) +# doc list(已弃用 → drive list / wiki node list) -> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: -> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 -> -> **同任务常配合**:[`doc-search.md`](./doc-search.md)(关键字检索更精准)/ [`doc-info.md`](./doc-info.md)(拿到 nodeId 后查元信息) +> **弃用提示**:`dws doc list` 已迁移,不再是 `doc` 子命令。真机上它虽仍能跑,但每次都会打印弃用警告: +> `'dws doc list' is deprecated, use 'dws drive list --workspace ' or 'dws wiki node list --workspace '`。 +> 遍历文件夹/知识库请改用下面的命令,命令详情见 [`../drive.md`](../drive.md)。 -## 命令格式 +## 改用什么 -``` -Usage: - dws doc list [flags] -Example: - dws doc list - dws doc list --folder - dws doc list --workspace --page-size 20 -Flags: - --folder string 文档文件夹 nodeId 或 alidocs 文件夹 URL;不要传 drive dentryId/parent-id 这类纯数字 ID - --workspace string 知识库 ID - --page-size int 每页数量 - --page-token string 分页 token (从上次结果的 nextPageToken 获取) -``` +| 旧命令(弃用) | 改用 | 场景 | +|--------------|------|------| +| `dws doc list` | `dws drive list` | 遍历「我的文档」/钉盘根目录 | +| `dws doc list --folder ` | `dws drive list --folder ` | 遍历指定文件夹 | +| `dws doc list --workspace ` | `dws drive list --workspace ` 或 `dws wiki node list --workspace ` | 遍历知识库 | -## 关键说明 - -- 不传任何 flag 时遍历"我的文档"根目录。 -- `--folder` 仅接受文档文件夹 `nodeId` / `dentryUuid` / alidocs 文件夹 URL;**禁止**传入 drive `dentryId`、`parentId`、`spaceId` 这类纯数字 ID。 -- 需翻页时使用 `nextPageToken` → `--page-token`。 +`drive list` 的完整参数(`--folder` / `--workspace` / `--limit` / `--cursor` / `--order-by` 等)见 [`../drive.md` §获取文件/文件夹列表](../drive.md)。 ## 上下文传递 -| 从返回中提取 | 用于 | -|-------------|------| -| `nodes[].nodeId` | [`doc-read.md`](./doc-read.md) / [`doc-info.md`](./doc-info.md) / [`doc-update.md`](./doc-update.md) / [`doc-file-ops.md`](./doc-file-ops.md) 等所有 `--node` 入参 | -| folder 类型的 `nodeId` | 当前 `list --folder` 递归遍历;[`doc-create.md`](./doc-create.md) / [`doc-file-ops.md`](./doc-file-ops.md) 的 `--folder` | - -## 常用模板 - -```bash -# 浏览"我的文档"根目录 -dws doc list --format json - -# 浏览指定文档文件夹(folder nodeId 或 alidocs 文件夹 URL) -dws doc list --folder --format json -dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/" --format json - -# 浏览指定知识库根目录(取出 workspaceId 后) -dws doc list --workspace --page-size 20 --format json - -# 翻页 -dws doc list --folder --page-token --format json -``` +从返回里取 `nodes[].nodeId`(folder 类型的 `nodeId` 可继续作 `--folder`),传给 `doc read` / `doc info` / `doc update` 等内容级命令的 `--node`。 ## 参考 -- [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令) -- [`./doc-search.md`](./doc-search.md)(关键字检索路径) +- [`../drive.md`](../drive.md)(列表 / 搜索 / 上传下载等文件管理命令的归属) - [`./doc-info.md`](./doc-info.md)(拿到 nodeId 后查元信息) diff --git a/skills/mono/references/products/doc/doc-search.md b/skills/mono/references/products/doc/doc-search.md index 845b9b22..3b259d53 100644 --- a/skills/mono/references/products/doc/doc-search.md +++ b/skills/mono/references/products/doc/doc-search.md @@ -1,80 +1,24 @@ -# doc search(搜索文档) +# doc search(已弃用 → drive search / wiki node search) -> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: -> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 -> -> **同任务常配合**:[`doc-list.md`](./doc-list.md)(目录遍历,互补于关键字搜索)/ [`doc-info.md`](./doc-info.md)(拿到 nodeId 后查元信息)/ [`doc-read.md`](./doc-read.md)(拿到 nodeId 后读取正文) +> **弃用提示**:`dws doc search` 已迁移,不再是 `doc` 子命令。真机上它虽仍能跑,但每次都会打印弃用警告: +> `'dws doc search' is deprecated, use 'dws drive search' or 'dws wiki node search --workspace '`。 +> 搜索文档/文件请改用下面的命令,命令详情见 [`../drive.md`](../drive.md)。 -## 命令格式 +## 改用什么 -``` -Usage: - dws doc search [flags] -Example: - dws doc search --query "会议纪要" - dws doc search - dws doc search --extensions pdf,docx - dws doc search --query "方案" --created-from 1700000000000 --created-to 1710000000000 - dws doc search --creator-uids uid1,uid2 - dws doc search --workspace-ids wsId1,wsId2 -Flags: - --query string 搜索关键词 (不传则返回最近访问) - --extensions strings 按文件扩展名过滤,不含点号,逗号分隔 (如 pdf,docx,png)。 - 钉钉在线文档: adoc(文字) axls(表格) appt(演示文稿) awbd(白板) adraw(画板) amind(脑图) able(多维表格) aform(收集表) - 常见附件: pdf docx doc xlsx xls pptx ppt csv txt md json xml zip rar png jpg jpeg gif mp4 mp3 - 以上仅为参考,extensions 为开放参数,服务端支持的扩展名不限于此。不确定文件后缀时,建议不传 --extensions 让搜索返回所有类型,再从结果中按文件名后缀筛选 - --created-from int 创建时间起始 (毫秒时间戳,含) - --created-to int 创建时间截止 (毫秒时间戳,含) - --visited-from int 访问时间起始 (毫秒时间戳,含) - --visited-to int 访问时间截止 (毫秒时间戳,含) - --creator-uids strings 按创建者用户 ID 过滤,逗号分隔 - --editor-uids strings 按编辑者用户 ID 过滤,逗号分隔 - --mentioned-uids strings 按 @提及的用户 ID 过滤,逗号分隔 - --workspace-ids strings 按知识库 ID 过滤,支持知识库 URL,逗号分隔 - --page-size int 每页数量 - --page-token string 分页 token (从上次结果的 nextPageToken 获取) -``` +| 旧命令(弃用) | 改用 | 场景 | +|--------------|------|------| +| `dws doc search --query "<关键词>"` | `dws drive search --query "<关键词>"` | 全局搜文档/文件(跨钉盘+文档空间聚合) | +| `dws doc search --extensions pdf,docx` | `dws drive search --query "..." --extensions pdf,docx` | 按扩展名过滤 | +| `dws doc search --workspace-ids ` | `dws wiki node search --workspace --query "<关键词>"` | 指定知识库内搜索 | -## 关键说明 - -- 不传 `--query` 时返回最近访问列表,适合"最近文档"类意图。 -- `--extensions` 是开放参数,传入服务端不识别的扩展名时不会报错(可能也搜不到);优先通过文件名后缀人工筛选。 -- 多个时间戳为毫秒时间戳,注意单位(不是秒)。 +`drive search` 的完整参数(`--query` / `--extensions` / `--created-from` / `--creator-uids` / `--limit` / `--cursor` 等)见 [`../drive.md` §搜索钉盘文件/文件夹/空间](../drive.md)。 ## 上下文传递 -| 从返回中提取 | 用于 | -|-------------|------| -| 文档 `nodeId` / URL | [`doc-read.md`](./doc-read.md) / [`doc-info.md`](./doc-info.md) / [`doc-update.md`](./doc-update.md) / [`doc-file-ops.md`](./doc-file-ops.md) 的 `--node` | -| `createTime` / `creatorUid` | 创建时间与创建者过滤的二次检索 | - -## 常用模板 - -```bash -# 关键字搜索(最常用) -dws doc search --query "项目周报" --format json - -# 仅最近访问(不传 --query) -dws doc search --format json - -# 按扩展名过滤(在线文档族 + 常见办公附件) -dws doc search --extensions adoc,axls,able,docx,xlsx,pdf - -# 按创建时间窗口(毫秒时间戳) -dws doc search --query "方案" --created-from 1700000000000 --created-to 1710000000000 - -# 按创建者过滤(多个 uid 逗号分隔) -dws doc search --creator-uids uid1,uid2 - -# 按知识库范围过滤(支持知识库 URL) -dws doc search --workspace-ids wsId1,wsId2 - -# 翻页 -dws doc search --query "周报" --page-size 30 --page-token -``` +拿到返回里的文档 `nodeId` / URL 后,`doc read` / `doc info` / `doc update` / `doc block` 等内容级命令照常用。 ## 参考 -- [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令) -- [`./doc-list.md`](./doc-list.md)(目录遍历替代路径) +- [`../drive.md`](../drive.md)(搜索 / 列表 / 上传下载等文件管理命令的归属) - [`./doc-info.md`](./doc-info.md)(URL → nodeId 提取) diff --git a/skills/mono/references/products/drive.md b/skills/mono/references/products/drive.md index a84e2a7c..30b6a3c8 100644 --- a/skills/mono/references/products/drive.md +++ b/skills/mono/references/products/drive.md @@ -389,6 +389,8 @@ Flags: --name string 新名称 (仅 rename 必填) ``` +> **rename 只传主名,不要带扩展名**:服务端会按文件原扩展名自动补一个后缀。若 `--name` 里已带扩展名(如 `报告.txt`),回读会变成双扩展名 `报告.txt.txt`。正确做法:`dws drive rename --node --name "报告"`(不含 `.txt`),系统自动补回 `报告.txt`。 + 权限要求:copy 需对源文档有"阅读"权限且对目标文件夹有"编辑"权限;move 需对源文档有"管理"权限且对目标文件夹有"编辑"权限;rename 需对文档有"编辑"权限。 > **字段选择**:`drive list` 返回中有 `dentryId`(数字格式)和 `fileId`(UUID 格式),**必须使用 `fileId`(UUID 格式)**作为 `--node` 和 `--folder` 参数值。 diff --git a/skills/mono/references/products/mail.md b/skills/mono/references/products/mail.md index c0d84259..4a822165 100644 --- a/skills/mono/references/products/mail.md +++ b/skills/mono/references/products/mail.md @@ -65,7 +65,7 @@ **默认选择策略:** 1. 调用 `dws mail mailbox list --format json` 获取当前用户的所有邮箱。 -2. 从返回的 `mailboxes` 中**优先选择企业邮箱**(账号类型为企业邮箱、域名非 `@dingtalk.com` 的邮箱),将其作为 `--email` / `--from` 的默认值。 +2. 从返回的 `emailAccounts` 中**优先选择企业邮箱**(账号类型为企业邮箱、域名非 `@dingtalk.com` 的邮箱),将其作为 `--email` / `--from` 的默认值。 3. 仅当用户在指令中**明确指定**「用我的个人邮箱」「用 dingtalk.com 邮箱」「用我的私人邮箱」等表述时,才选择个人邮箱(`@dingtalk.com` 域名)。 4. 若用户同时拥有多个企业邮箱(如分属多家公司),优先选择与当前会话上下文匹配的企业邮箱;若仍无法判断,向用户确认后再操作。 5. 若用户**仅拥有个人邮箱**(无企业邮箱),可直接使用个人邮箱,但需注意 `mail user search` 等仅企业邮箱可用的命令会因权限报错,需走「查找他人邮箱地址」章节的替代路径。 @@ -91,7 +91,7 @@ Example: | 字段 | 类型 | 说明 | |------|------|------| -| `mailboxes` | `List[]` | 邮箱列表,每条包含邮箱地址、账号类型、所属企业 | +| `emailAccounts` | `List[]` | 邮箱列表,每条包含 `email`(邮箱地址)、`orgName`(所属企业,个人邮箱为 null)、`type`(`ORG`=企业邮箱 / `PERSONAL`=个人邮箱) | ### 查找他人邮箱地址(通讯录查人) @@ -150,13 +150,13 @@ Flags: Usage: dws mail message search [flags] Example: - dws mail message search --email user@company.com --query "subject:\"周报\"" --size 20 - dws mail message search --email user@company.com --query "from:alice AND date>2025-06-01T00:00:00Z" --size 10 + dws mail message search --email user@company.com --query "subject:\"周报\"" --limit 20 + dws mail message search --email user@company.com --query "from:alice AND date>2025-06-01T00:00:00Z" --limit 10 Flags: --cursor string 邮件的起始偏移标识, 其值取自响应中的nextCursor字段。""表示从头开始 --email string 搜索目标邮箱地址 (必填) --query string KQL 查询表达式 (必填), 其中 date 格式需遵循 ISO8601 规范 - --size string 每页返回数量(最大限制 100, 默认 20),别名: --limit, --page-size + --limit string 每页返回数量(最大限制 100, 默认 20) ``` KQL 查询字段: date, size, tag, folderId, isRead, hasAttachments, subject, attachname, body, from, to @@ -194,9 +194,9 @@ KQL 查询字段: date, size, tag, folderId, isRead, hasAttachments, subject, at **翻页示例:** ```bash # 第一页 -dws mail message search --email user@company.com --query "folderId:2" --size 20 --format json +dws mail message search --email user@company.com --query "folderId:2" --limit 20 --format json # 取返回中的 nextCursor,传入下一次请求(nextCursor="$" 时停止) -dws mail message search --email user@company.com --query "folderId:2" --size 20 --cursor --format json +dws mail message search --email user@company.com --query "folderId:2" --limit 20 --cursor --format json ``` ### 查看邮件完整内容 @@ -222,17 +222,17 @@ Usage: dws mail message send [flags] Example: dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "本周完成任务A和任务B" + --subject "周报" --content "本周完成任务A和任务B" dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "见附件" --attachment ./report.pdf + --subject "周报" --content "见附件" --attachment ./report.pdf dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "见附件" --attachment ./a.pdf --attachment ./b.xlsx + --subject "周报" --content "见附件" --attachment ./a.pdf --attachment ./b.xlsx dws mail message send --from user@company.com --to colleague@company.com \ - --subject "图表周报" --body "图表如下:[inline:chart.png]" --inline-attachment ./chart.png + --subject "图表周报" --content "图表如下:[inline:chart.png]" --inline-attachment ./chart.png dws mail message send --from user@company.com --to colleague@company.com \ - --subject "带图文档" --body "见附件,图表:[inline:img.png]" --attachment ./doc.pdf --inline-attachment ./img.png + --subject "带图文档" --content "见附件,图表:[inline:img.png]" --attachment ./doc.pdf --inline-attachment ./img.png Flags: - --body string 邮件正文 (必填) + --content string 邮件正文 (必填) --cc string 抄送人列表 --from string 发件人邮箱 (必填),别名: --sender --subject string 邮件标题 (必填) @@ -256,7 +256,7 @@ Flags: - 仅支持图片类型:`jpg` / `jpeg` / `png` / `gif` / `webp` / `bmp` / `svg` - CLI 自动生成 contentId,格式:`inline-{文件名(不含扩展名)}-{序号}@alimail.com`,例:`inline-chart-1@alimail.com` -- 在 `--body` 中使用占位符 `[inline:文件名]` 引用图片,CLI 自动替换为 `` 标签 +- 在 `--content` 中使用占位符 `[inline:文件名]` 引用图片,CLI 自动替换为 `` 标签 - 若 body 中没有对应占位符,内联图片会自动追加到正文末尾 - 非图片类型(PDF、视频、音频等)请改用 `--attachment` @@ -266,13 +266,13 @@ Usage: dws mail folder list [flags] Example: dws mail folder list --email user@company.com - dws mail folder list --email user@company.com --folder-id + dws mail folder list --email user@company.com --folder Flags: - --email string 邮件所属邮箱地址 (必填) - --folder-id string 父文件夹唯一标识,不传则返回顶层文件夹 (可选) + --email string 邮件所属邮箱地址 (必填) + --folder string 父文件夹唯一标识,不传则返回顶层文件夹 (可选) ``` -不传 `--folder-id` 返回顶层文件夹列表;传入则返回该文件夹的子文件夹列表。 +不传 `--folder` 返回顶层文件夹列表;传入则返回该文件夹的子文件夹列表。 **返回字段(`folders` 数组):** @@ -351,8 +351,7 @@ Flags: ```json { - "success": true, - "result": {} + "success": true } ``` @@ -374,8 +373,7 @@ Flags: ```json { - "success": true, - "result": {} + "success": true } ``` @@ -401,7 +399,7 @@ Flags: |------|------|------| | `id` | `string` | 附件唯一标识 | | `name` | `string` | 附件文件名 | -| `contentType` | `string` | 附件 MIME 类型 | +| `isInline` | `bool` | 是否为内联附件(`true`=正文内联图片,`false`=普通附件) | | `size` | `int` | 附件大小(字节) | ### 下载邮件附件 @@ -521,8 +519,7 @@ Flags: ```json { - "success": true, - "result": {} + "success": true } ``` @@ -546,8 +543,7 @@ Flags: ```json { - "success": true, - "result": {} + "success": true } ``` @@ -627,8 +623,10 @@ Flags: | `senders` | `List[{email, name}]` | 会话发件人列表 | | `isRead` | `boolean` | 会话是否已读(全部已读/未读) | | `priority` | `string` | 会话重要性,取会话内邮件最高优先级(`PRY_HIGH` / `PRY_NORMAL`) | -| `flag` | `string` | 会话标识,取会话内最近邮件的标识(`FLAG_NONE` / `FLAG_REPLY` / `FLAG_FORWARD`) | | `hasAttachments` | `boolean` | 会话是否包含附件(不含 inline 资源) | +| `messages` | `List[]` | 会话内的邮件列表;默认即返回,但各邮件字段(正文、收件人等)多为 null,需在 `--select` 中额外指定才有值 | + +> **注意:** `thread get` 返回的 `conversation` **不含 `flag` 字段**(`flag` 仅出现在 `thread list` 的会话项中)。会话标识请从 `thread list` 获取。 ### 修改邮件会话状态 @@ -660,8 +658,7 @@ Flags: ```json { - "success": true, - "result": {} + "success": true } ``` @@ -686,8 +683,7 @@ Flags: ```json { - "success": true, - "result": {} + "success": true } ``` @@ -749,13 +745,13 @@ Usage: dws mail message reply [flags] Example: dws mail message reply --from user@company.com --id - dws mail message reply --from user@company.com --id --subject "Re: 周报" --body "已收到,谢谢!" + dws mail message reply --from user@company.com --id --subject "Re: 周报" --content "已收到,谢谢!" Flags: --from string 发件人邮箱 (必填),别名: --sender --to string 收件人列表(可选) --id string 要回复的邮件 ID (必填) --subject string 回复邮件标题(可选) - --body string 回复正文(可选) + --content string 回复正文(可选) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` @@ -781,13 +777,13 @@ Usage: dws mail message reply-all [flags] Example: dws mail message reply-all --from user@company.com --id - dws mail message reply-all --from user@company.com --id --subject "Re: 周报" --body "感谢大家的参与!" + dws mail message reply-all --from user@company.com --id --subject "Re: 周报" --content "感谢大家的参与!" Flags: --from string 发件人邮箱 (必填),别名: --sender --to string 收件人列表(可选,包含发件人及所有原始收件人) --id string 要回复的邮件 ID (必填) --subject string 回复邮件标题(可选) - --body string 回复正文(可选) + --content string 回复正文(可选) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` @@ -819,7 +815,7 @@ Flags: --to string 转发收件人列表(可选) --id string 要转发的邮件 ID (必填) --subject string 转发邮件标题(可选) - --body string 转发附言(可选) + --content string 转发附言(可选) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` @@ -922,7 +918,9 @@ Flags: | 字段 | 类型 | 说明 | |------|------|------| | `message` | `object` | 邮件完整信息 | -| `sendStatus` | `string` | 发送状态,取值见下表 | +| `message.sendStatus` | `string` | 发送状态,**嵌在 `message` 对象内部**(非顶层字段),取值见下表 | + +> **注意:** 顶层只有 `message` 和 `success` 两个字段,`sendStatus` 位于 `message.sendStatus`,不是与 `message` 平级的顶层字段。 **`sendStatus` 取值说明:** @@ -941,57 +939,57 @@ Usage: dws mail draft create [flags] Example: dws mail draft create --from user@company.com --to colleague@company.com \ - --subject "草稿标题" --body "草稿正文" + --subject "草稿标题" --content "草稿正文" dws mail draft create --from user@company.com --subject "草稿标题" dws mail draft create --from user@company.com --subject "带附件草稿" \ - --body "见附件" --attachment ./report.pdf + --content "见附件" --attachment ./report.pdf dws mail draft create --from user@company.com --subject "带图片草稿" \ - --body "图表:[inline:chart.png]" --inline-attachment ./chart.png + --content "图表:[inline:chart.png]" --inline-attachment ./chart.png Flags: --from string 发件人邮箱 (必填),别名: --sender --subject string 邮件标题 (必填) --to string 收件人列表(可选,有确定收件人时才传) --cc string 抄送人列表(可选,有确定抄送人时才传) - --body string 邮件正文(可选,有正文内容时才传) + --content string 邮件正文(可选,有正文内容时才传) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` -> **注意:** `--to`、`--cc`、`--body` 均为可选参数,**仅在用户明确提供对应信息时才传入**。若用户未指定收件人,不要传 `--to ""`(空字符串)。 +> **注意:** `--to`、`--cc`、`--content` 均为可选参数,**仅在用户明确提供对应信息时才传入**。若用户未指定收件人,不要传 `--to ""`(空字符串)。 **附件说明:** -指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿创建和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--body` 中使用 `[inline:文件名]` 占位符)。 +指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿创建和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--content` 中使用 `[inline:文件名]` 占位符)。 **返回字段:** | 字段 | 类型 | 说明 | |------|------|------| -| `messageId` | `string` | 新建草稿的邮件 ID | +| `result.message.id` | `string` | 新建草稿的邮件 ID(**嵌在 `result.message` 内**,无顶层 `messageId` 字段);`result.message` 还含 `internetMessageId`、`folderId`(草稿箱为 `5`)、`subject`、`from` 等 | ### 更新草稿 ``` Usage: dws mail draft update [flags] Example: - dws mail draft update --from user@company.com --id --subject "新标题" --body "新正文" - dws mail draft update --from user@company.com --id --body "见附件" --attachment ./report.pdf + dws mail draft update --from user@company.com --id --subject "新标题" --content "新正文" + dws mail draft update --from user@company.com --id --content "见附件" --attachment ./report.pdf dws mail draft update --from user@company.com --id \ - --body "图表:[inline:chart.png]" --inline-attachment ./chart.png + --content "图表:[inline:chart.png]" --inline-attachment ./chart.png Flags: --from string 发件人邮箱 (必填),别名: --sender --id string 草稿邮件 ID (必填) --to string 收件人列表(可选) --cc string 抄送人列表(可选) --subject string 邮件标题(可选) - --body string 邮件正文(可选) + --content string 邮件正文(可选) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` **附件说明:** -指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿更新和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--body` 中使用 `[inline:文件名]` 占位符)。 +指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿更新和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--content` 中使用 `[inline:文件名]` 占位符)。 ### 发送草稿 ``` @@ -1013,13 +1011,14 @@ Usage: Example: dws mail user search --keyword "张三" dws mail user search --email user@company.com --keyword "张三" - dws mail user search --email user@company.com --keyword "alice" --size 10 + dws mail user search --email user@company.com --keyword "alice" --limit 10 dws mail user search --email user@company.com --keyword "alice" --cursor Flags: - --email string 搜索目标邮箱地址 (可选) - --keyword string 搜索关键词 (必填) - --cursor string 分页游标,取自响应中的 nextCursor 字段(可选) - --size string 每页返回数量(可选) + --email string 搜索目标邮箱地址 (可选) + --keyword string 搜索关键词(未提供 --employee-no 时为必填) + --employee-no string 按工号搜索用户;提供此参数时 keyword 不再必填 (可选) + --cursor string 分页游标,取自响应中的 nextCursor 字段(可选) + --limit string 每页返回数量(可选) ``` > **重要区别:** @@ -1069,6 +1068,10 @@ Flags: ``` > **草稿模板说明:** 传入 `--is-draft` 创建的模板为草稿模板,草稿模板支持后续通过 `template update` 修改内容。**非草稿模板创建后不可修改**(`template update` 仅对草稿模板有效)。 +> +> **草稿模板不出现在 `template list`(重要):** 实测 `template list` **只返回非草稿模板**;草稿模板(`--is-draft`)不在列表里,只能用 `template get --id <模板ID>` 直接获取。因此**不要用 `template list` 是否出现来判断草稿模板是否创建成功**——`template create` 返回 `success:true` 即已创建。 +> +> **模板 ID 前缀区分类型:** `template create` 返回的 `id` 前缀标识类型——非草稿模板为 `1:0:` 开头(如 `1:0:8bbeea56-...`),草稿模板为 `0:0:` 开头(如 `0:0:e3e2d134-...`)。 ### 列举邮件模板 ``` @@ -1241,7 +1244,7 @@ Flags: | `rules[].enabled` | bool | 是否启用 | | `rules[].conditions` | List[] | 规则条件列表 | | `rules[].actions` | List[] | 规则动作列表 | -| `rules[].order` | int | 规则排序 | +| `rules[].order` | null | 排序字段;实测存量与新建规则该字段**恒为 `null`**,不返回有效排序值,不要依赖它判断规则顺序 | #### 创建收信规则 @@ -1366,10 +1369,10 @@ Flags: | 字段 | 类型 | 说明 | |------|------|------| | `success` | bool | 创建是否成功 | -| `errorCode` | string | 错误码 | -| `errorMsg` | string | 错误消息 | | `id` | string | 新建规则 ID | +> 实测 `rule create` 成功仅返回 `{id, success}`,**不返回** `errorCode` / `errorMsg`。 + #### 更新收信规则 更新已有的收信规则。**除 `--conditions` 外所有参数均为必填**。 @@ -1401,9 +1404,9 @@ Flags: | 字段 | 类型 | 说明 | |------|------|------| | `success` | bool | 更新是否成功 | -| `errorCode` | string | 错误码 | -| `errorMsg` | string | 错误信息 | -| `result` | object | 更新结果 | +| `result` | object | 更新结果(实测为空对象 `{}`) | + +> 实测 `rule update` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。 #### 删除收信规则 @@ -1424,9 +1427,9 @@ Flags: | 字段 | 类型 | 说明 | |------|------|------| | `success` | bool | 删除是否成功 | -| `errorCode` | string | 错误码 | -| `errorMsg` | string | 错误信息 | -| `result` | object | 删除结果 | +| `result` | object | 删除结果(实测为空对象 `{}`) | + +> 实测 `rule delete` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。 #### 调整收信规则排序 @@ -1449,9 +1452,9 @@ Flags: | 字段 | 类型 | 说明 | |------|------|------| | `success` | bool | 调整是否成功 | -| `errorCode` | string | 错误码 | -| `errorMsg` | string | 错误消息 | -| `result` | object | 调整结果 | +| `result` | object | 调整结果(实测为空对象 `{}`) | + +> 实测 `rule adjust` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。 ## 通用错误说明 @@ -1559,29 +1562,29 @@ dws mail thread batch-trash --email user@company.com --ids ,2025-06-01T00:00:00Z" --size 10 --format json + --query "subject:\"周报\" AND date>2025-06-01T00:00:00Z" --limit 10 --format json # 3. 查看邮件详情 dws mail message get --email user@company.com --id --format json # 4. 发送邮件(纯文本) dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "本周完成…" --format json + --subject "周报" --content "本周完成…" --format json # 4b. 发送带附件的邮件(自动编排:创建草稿→上传附件→发送草稿) dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "见附件" --attachment ./report.pdf --format json + --subject "周报" --content "见附件" --attachment ./report.pdf --format json # 4c. 发送带内联图片的邮件(正文自动转 HTML, 标签自动注入) dws mail message send --from user@company.com --to colleague@company.com \ - --subject "图表周报" --body "本周图表如下:[inline:chart.png]" \ + --subject "图表周报" --content "本周图表如下:[inline:chart.png]" \ --inline-attachment ./chart.png --format json # 5. 下载邮件附件到本地(每次只能下载一个附件,不支持批量下载) # 步骤 5.1:搜索匹配的邮件,获取 messageId 列表 # 示例:下载4月所有发票邮件的附件 dws mail message search --email user@company.com \ - --query "subject:发票 AND date>2025-04-01T00:00:00Z AND date<2025-05-01T00:00:00Z AND hasAttachments:true" --size 50 --format json + --query "subject:发票 AND date>2025-04-01T00:00:00Z AND date<2025-05-01T00:00:00Z AND hasAttachments:true" --limit 50 --format json # 步骤 5.2:对每封邮件,列出附件获取 attachmentId 和 name # (对搜索结果中的每封邮件都要执行一次) @@ -1594,7 +1597,7 @@ dws mail attachment download --email user@company.com \ # 5. 获取邮件所属会话详情(thread) # 步骤 5.1:先通过 message search 或 message get 获取邮件中的 conversationId dws mail message search --email user@company.com \ - --query "subject:\"周报\"" --size 5 --format json + --query "subject:\"周报\"" --limit 5 --format json # 从返回的邮件列表中提取 conversationId 字段 # 步骤 5.2:用 conversationId 获取会话详情 @@ -1637,8 +1640,8 @@ dws mail thread get --email user@company.com --id --select mess 3. `dws contact user search --keyword "名字"`,提取用户邮箱字段 若三路均无有效邮箱,必须 ask_human 请用户手动提供收件人邮箱,严禁臆测和假设 - `thread get` 无法直接通过邮箱地址查询会话列表,**必须先有 conversationId**;conversationId 来自 `message search` 或 `message get` 返回的邮件字段 `conversationId` -- `thread get` 默认不返回邮件列表,如需查看会话内所有邮件,需加 `--select messages`;如需同时返回多个可选字段,用英文逗号分隔,如 `--select messages,internetMessageId` -- `thread get` 返回的 `messages` 列表中,邮件正文(`body`)、收件人(`toRecipients`)等字段默认不包含,需在 `--select` 中额外指定 +- `thread get` 默认即返回会话内 `messages` 列表,但列表中每封邮件的正文(`body`)、收件人(`toRecipients`)等字段默认为 null;需在 `--select` 中额外指定才有值,如 `--select messages,internetMessageId`(多个字段用英文逗号分隔) +- `thread get` 返回的 `conversation` 不含 `flag` 字段,会话标识请从 `thread list` 获取 - `user search` 仅支持企业邮箱(非 `@dingtalk.com` 个人邮箱),使用个人邮箱将因无权限报错;搜到的用户邮箱(`email` 字段)可直接用于 `message send` 的 `--to`/`--cc` 参数 - `thread list --folder` 的值必须是文件夹 ID,不是文件夹显示名称;不知道文件夹 ID 时,先调用 `folder list` 查 `folders[].id` - `thread get/update/trash/batch-update/batch-trash` 使用的是会话 ID(conversationId),不是邮件 ID;会话 ID 可来自 `thread list` 的 `conversations[].id`,也可来自 `message search` 或 `message get` 返回的 `conversationId` diff --git a/skills/mono/references/products/minutes.md b/skills/mono/references/products/minutes.md index 0d83f20d..eef1999a 100644 --- a/skills/mono/references/products/minutes.md +++ b/skills/mono/references/products/minutes.md @@ -7,8 +7,8 @@ minutes 模块的命令是**两级或三级结构**,不同层级之间不能 > **默认 scope 规则**:`dws minutes list` 后**必须**跟 scope 子命令。若用户未明确指定查询范围,**一律默认补 `all`**(查询我可访问的所有听记 = 我创建的 + 他人共享给我的,覆盖面最广)。 > > **`list` 不带 scope 的陷阱(0519 P2 Golden Case 提炼)**: -> - 裸 `dws minutes list`(不跟 mine/shared/all)虽然不会报错,但返回结果**不完整**(仅返回最近少量条目,约 913 字节) -> - `dws minutes list all` 才能返回完整列表(约 3362 字节) +> - 裸 `dws minutes list`(不跟 mine/shared/all)**不会返回任何听记数据**——它只打印该命令的帮助/用法信息(Usage、Available Commands: all/mine/shared、Flags),等于什么都没查到 +> - 必须带 scope 子命令(`dws minutes list all` / `list mine` / `list shared`)才会真正返回听记列表 > - **AI 严禁使用裸 `dws minutes list`**,必须始终带 scope 子命令 > - 如果 LLM 不确定用哪个 scope,**一律用 `all`** > @@ -77,7 +77,7 @@ dws minutes | `dws minutes list all --end-time "..."` | 参数名是 `--end` 不是 `--end-time` | `dws minutes list all --end "2026-04-30"` | | `dws minutes list --page-size 10` | `--page-size` 不存在,分页用 `--max`;且 `list` 后缺 scope | `dws minutes list mine --max 10` | | `dws minutes list --date-range 2026-05-04 2026-05-10` | `--date-range` 不存在,时间范围用 `--start` + `--end` 两个参数;且 `list` 后缺 scope | `dws minutes list mine --start "2026-05-04T00:00:00+08:00" --end "2026-05-10T23:59:59+08:00"` | -| `dws minutes list mine --limit 10` | `--limit` 尚未注册(跨产品规约 Primary 为 `--limit`,但 minutes CLI 当前只接受 `--max`)。**待 alias 注册后两者等价** | `dws minutes list mine --max 10` | +| `dws minutes list mine --limit 10` | 不报错——`--limit` 已注册为 `--max` 的合法别名,两者完全等价(`--limit 3` 与 `--max 3` 返回相同条数) | `dws minutes list mine --max 10`(或 `--limit 10`,二选一即可) | | `dws minutes upload create --json '{"fileName":...}'` | `--json` 不存在,cli 不接受 JSON 作为输入格式 | `dws minutes upload create --file-name "xxx.mp3" --file-size 61565431` | | `dws minutes upload create -f json '{"fileName":...}'` | `-f json` / `--format json` 是**输出格式**控制,不是输入参数 | `dws minutes upload create --file-name "xxx.mp3" --file-size 61565431 --format json` | | `dws minutes get transcription --id \| head -c 2000` | Windows 沙箱无 `head` 命令,**严禁使用 shell 管道截断** | `dws minutes get transcription --id --format json`(由 AI 在内存中截断处理) | @@ -90,7 +90,7 @@ dws minutes | `dws minutes transcribe --url <听记url>` | `transcribe` 不是合法子命令,`--url` 参数也不存在。LLM 凭印象编造 | 先从 URL 提取 taskUuid(见「URL → taskUuid 自动提取规则」),再 `dws minutes get transcription --id --format json` | | `dws minutes get transcription --url <听记url>` | `--url` 参数不存在,`--id` 只接受纯 taskUuid | 同上:从 URL 提取 taskUuid 后用 `--id` | | `dws minutes summary --uuid ` | `summary` 不是顶层子命令(应在 `get` 下),且顶层不识别 `--uuid`。**0519 高频错误** | `dws minutes get summary --id ` | -| `dws minutes list`(不跟 scope) | `list` 后**必须**跟 scope(mine/shared/all)。裸 `list` 返回结果不完整且行为未文档化 | `dws minutes list all`(默认 scope) | +| `dws minutes list`(不跟 scope) | `list` 后**必须**跟 scope(mine/shared/all)。裸 `list` 只打印帮助信息,不返回任何听记数据 | `dws minutes list all`(默认 scope) | | `dws report inbox --format json`(无时间窗口) | `inbox` 是兼容入口,等价于 `inbox list`;仍必须带 `--start` / `--end` | `dws report inbox list --start "2026-05-06T00:00:00+08:00" --end "2026-05-13T23:59:59+08:00" --format json` | | `dws report inbox list --format json`(不带 --start) | inbox list **必须**带 `--start` / `--end` 参数,否则报 "flag --start is required" | `dws report inbox list --start "2026-05-06T00:00:00+08:00" --end "2026-05-13T23:59:59+08:00" --format json` | @@ -102,14 +102,14 @@ dws minutes > > | 语义 | 规约 Primary | minutes 当前 CLI 实际参数 | alias 注册状态 | 说明 | > |------|-------------|------------------------|--------------|------| -> | 单页大小 (Group 13) | `--limit` | `--max` | `--limit` 未注册 | minutes 用 `--max`,其他模块用 `--limit`。**跨模块切换时注意差异** | +> | 单页大小 (Group 13) | `--limit` | `--max`(`--limit` 为合法别名) | `--limit` 已注册 | minutes 推荐 `--max`,但 `--limit` 也可用,两者等价 | > | 续页标识 (Group 14) | `--cursor` | `--next-token` | `--next-token` 在 alias 池中 | minutes 用 `--next-token`,合法 alias | > | 搜索关键词 (Group 11) | `--query` | `--query` | 已对齐 | 无差异 | > | 起始时间 (Group 15) | `--start` | `--start` | 已对齐 | 无差异 | > | 结束时间 (Group 16) | `--end` | `--end` | 已对齐 | 无差异 | > > **跨模块参数名差异速查(防止混用):** -> - 分页大小:minutes 用 `--max`,aitable/calendar/chat/drive 用 `--limit` +> - 分页大小:minutes 推荐 `--max`(`--limit` 也是合法别名,等价),aitable/calendar/chat/drive 用 `--limit` > - 续页标识:minutes 用 `--next-token`,aitable/drive/doc 用 `--cursor` > - 起止时间:minutes/report 用 `--start`/`--end`,calendar 用 `--start-time`/`--end-time`(跨模块最高频错误) @@ -260,8 +260,8 @@ dws minutes > | `missing required flag(s): --session-id` | cancel 命令必须传 session-id | 从之前 upload create 的返回中提取 session-id;若丢失则暂无法取消 | > > **上传流程三步走**(AI 必须按此顺序执行,不可跳步): -> 1. `dws minutes upload create --file-name "xxx.mp3" --file-size <字节数> --format json` → 获取 `sessionId` + `uploadUrl` -> 2. 将文件通过 `uploadUrl` 直传(由客户端侧完成,AI 不介入) +> 1. `dws minutes upload create --file-name "xxx.mp3" --file-size <字节数> --format json` → 获取 `sessionId` + `presignedUrl` +> 2. 将文件通过 `presignedUrl` 直传(由客户端侧完成,AI 不介入) > 3. `dws minutes upload complete --session-id --format json` → 确认上传完成 > > **注意**:`--file-size` 单位是**字节**(不是 KB/MB),AI 不要帮用户估算大小,应让用户确认文件实际字节数。 @@ -383,7 +383,7 @@ Flags: --id string 听记 taskUuid (必填),取值逻辑参考 ## 注意事项 ``` -返回字段: 创建人、开始时间、截止时间、听记标题、听记访问链接URL +返回字段(result 内): `duration`(时长, 秒)、`startTime`(开始时间, 毫秒时间戳)、`endTime`(结束时间, 仅较长会议返回, 短录音无此字段)、`taskUuid`(听记 ID)、`title`(听记标题)、`url`(听记访问链接)。注意: **不返回创建人(creator)字段**。 **发言人列表输出规范(查询详情后必须执行)**: diff --git a/skills/mono/references/products/oa.md b/skills/mono/references/products/oa.md index de477154..c44c049e 100644 --- a/skills/mono/references/products/oa.md +++ b/skills/mono/references/products/oa.md @@ -12,7 +12,7 @@ Example: Flags: --end string 结束时间 ISO-8601 (如 2026-03-10T23:59:59+08:00) (必填) --page string 分页页码 (可选) - --size string 每页大小 (可选) + --limit string 每页大小 (可选) --start string 开始时间 ISO-8601 (如 2026-03-10T00:00:00+08:00) (必填) --query string 关键字搜索 (可选) ``` @@ -99,12 +99,12 @@ Flags: Usage: dws oa approval list-forms [flags] Example: - dws oa approval list-forms --cursor 0 --size 100 + dws oa approval list-forms --cursor 0 --limit 100 Flags: --cursor string 分页游标,首次传 0 (默认 "0") - --size string 每页大小,最大 100 (默认 "100") + --limit string 每页大小,最大 100 (默认 "100") ``` -MCP 工具: `list_user_visible_process`;参数: cursor, pageSize(对应 --cursor/--size)。返回结果含 processCode,可用于 list-initiated 的 --process-code。 +MCP 工具: `list_user_visible_process`;参数: cursor, pageSize(对应 --cursor/--limit)。返回结果含 processCode,可用于 list-initiated 的 --process-code。 ### 按关键字模糊搜索审批表单 ``` @@ -245,25 +245,26 @@ MCP 工具: `redirect_task`;参数: taskId, toActionerId, remark(对应 --ta Usage: dws oa approval oa-comments [flags] Example: - dws oa approval oa-comments --instance-id --text "同意,请尽快处理" + dws oa approval oa-comments --instance-id --content "同意,请尽快处理" Flags: --instance-id string 审批实例 ID (必填) - --text string 评论内容 (必填) + --content string 评论内容 (必填) ``` -MCP 工具: `dingflow_comments`;参数: processInstanceId, text(对应 --instance-id/--text)。processInstanceId 可通过 `list-pending` 或 `detail` 获取。 +MCP 工具: `dingflow_comments`;参数: processInstanceId, text(对应 --instance-id/--content)。processInstanceId 可通过 `list-pending` 或 `detail` 获取。 ### 对审批实例进行抄送 ``` Usage: dws oa approval oa-cc-noticer [flags] Example: - dws oa approval oa-cc-noticer --instance-id --user-list "68674200835816" - dws oa approval oa-cc-noticer --instance-id --user-list "userId1,userId2" + dws oa approval oa-cc-noticer --instance-id --users "68674200835816" + dws oa approval oa-cc-noticer --instance-id --users "userId1,userId2" --operator-id "123123" Flags: --instance-id string 审批实例 ID (必填) - --user-list string 抄送用户 ID 列表,多个用逗号分隔 (必填) + --users string 抄送用户 ID 列表,多个用逗号分隔 (必填) + --operator-id string 操作人 ID (可选) ``` -MCP 工具: `oa_cc_noticer`;参数: processInstanceId, userList(对应 --instance-id/--user-list)。processInstanceId 可通过 `list-pending` 或 `detail` 获取,抄送用户 ID 可通过 `dws contact user search` 获取。 +MCP 工具: `oa_cc_noticer`;参数: processInstanceId, userList(对应 --instance-id/--users)。processInstanceId 可通过 `list-pending` 或 `detail` 获取,抄送用户 ID 可通过 `dws contact user search` 获取。 ### 对审批任务进行加签 @@ -320,8 +321,8 @@ Flags: 用户说"我审批/处理过的审批单" -> `approval list-executed` 用户说"抄送我的审批单" -> `approval list-cc` 用户说"转交审批/转交任务" → `approval redirect-task`(需 --task-id 和 --to-actioner-id) -用户说"评论审批/添加评论/写评论" → `approval oa-comments`(需 --instance-id 和 --text) -用户说"抄送审批/添加抄送人" → `approval oa-cc-noticer`(需 --instance-id 和 --user-list) +用户说"评论审批/添加评论/写评论" → `approval oa-comments`(需 --instance-id 和 --content) +用户说"抄送审批/添加抄送人" → `approval oa-cc-noticer`(需 --instance-id 和 --users) ## 核心工作流 @@ -348,12 +349,12 @@ dws oa approval revoke --instance-id --remark "误发起" --format json dws oa approval records --instance-id --format json # 7. 获取可见审批表单(得到 processCode) -dws oa approval list-forms --cursor 0 --size 100 --format json +dws oa approval list-forms --cursor 0 --limit 100 --format json # 8. 查看自己发起的审批列表(--process-code 来自 list-forms 或 detail) dws oa approval list-initiated --process-code \ --start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" \ - --next-token 0 --max-results 20 --format json + --cursor 0 --limit 20 --format json # 9. 我处理过的审批单 dws oa approval list-executed --limit --page --query 关键词 --format json @@ -367,11 +368,11 @@ dws oa approval redirect-task --task-id --to-actioner-id --for dws oa approval redirect-task --task-id --to-actioner-id --remark "请帮忙处理" --format json # 13. 对审批实例添加评论(processInstanceId 来自 list-pending 或 detail) -dws oa approval oa-comments --instance-id --text "同意,请尽快处理" --format json +dws oa approval oa-comments --instance-id --content "同意,请尽快处理" --format json # 14. 对审批实例进行抄送(processInstanceId 来自 list-pending 或 detail) -dws oa approval oa-cc-noticer --instance-id --user-list "68674200835816" --format json -dws oa approval oa-cc-noticer --instance-id --user-list "userId1,userId2" --format json +dws oa approval oa-cc-noticer --instance-id --users "68674200835816" --format json +dws oa approval oa-cc-noticer --instance-id --users "userId1,userId2" --format json ``` ## 上下文传递表 @@ -391,6 +392,7 @@ dws oa approval oa-cc-noticer --instance-id --user-list "use - `revoke` 只能撤销自己发起的审批 - `--remark` 审批意见虽为可选,但建议填写以留存审批痕迹 - `list-initiated` 的 `--process-code` 可从 `list-forms` 或 `detail` 返回中提取 +- `list-initiated` 的 `--start` / `--end` 区间有后端上限(约 120 天)。超过上限会返回误导性的 `business_error: 时间戳无效`(实为区间过长,不是时间格式问题)。跨度大时请拆成多段短区间分别查询 ## 自动化脚本 diff --git a/skills/mono/references/products/report.md b/skills/mono/references/products/report.md index f89be131..6db1d12e 100644 --- a/skills/mono/references/products/report.md +++ b/skills/mono/references/products/report.md @@ -343,8 +343,8 @@ dws report outbox list --cursor 0 --size 20 --format json | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `template list` | template 名称(result.items[].report_template_name) | `template get` 的 --name | -| `template list` | `report_template_id` | `entry submit` 的 --template-id | +| `template list` | template 名称(`items[].report_template_name`;顶层键只有 `items` 和 `success`,**无 `result` 包裹**) | `template get` 的 --name | +| `template list` | `items[].report_template_id` | `entry submit` 的 --template-id | | `template get` | `result.report_template_fields[].field_name` / `field_sort` / `field_type` | 拼 `entry submit` 的 --contents JSON(按下表映射)| | `entry submit` | `reportId`、`dingtalkOpenMarkdownLink`、`dingtalkOpenUrl`(CLI 自动反查详情后追加)| final reply 优先直接使用 `dingtalkOpenMarkdownLink`;需要结构化展示时用 `dingtalkOpenLink.title` + `dingtalkOpenLink.url` | | `inbox list` / `outbox list` | `reportId` | `entry get` / `entry stats` 的 --report-id | @@ -376,11 +376,11 @@ dws report outbox list --cursor 0 --size 20 --format json | Code | ExitCode | 真实含义 | 建议动作 | |------|---------|---------|---------| | `INPUT_INVALID_JSON` | 3 | `--contents` 或 `--contents-file` 内容非合法 JSON | 检查 JSON 数组结构,每项必须是 object,含 `key`/`sort`/`content`/`contentType`/`type` 五个字段 | -| `INPUT_FILE_NOT_FOUND` | 3 | `--contents-file` 路径不存在 / sandbox OS 风格不匹配(macOS 路径在 Windows 沙箱)| 先确认 sandbox OS 与路径风格;改写到 `os.tmpdir()` 等可移植目录 | +| `INPUT_FILE_NOT_FOUND` | 3 | `--contents-file` 指向的文件不存在(路径必须在当前工作目录 CWD 之下)| 确认文件存在且位于 CWD 内;把 contents JSON 写到 CWD 下的相对路径再传 | +| `INPUT_INVALID_PATH` | 3 | `--contents-file` 路径越出当前工作目录(如 `/tmp`、`os.tmpdir()`、`../` 或指向目录外的符号链接)| **不要**用 `/tmp` / `os.tmpdir()` / `../`;把 contents JSON 写到当前工作目录之下再传 | | `INPUT_MISSING_PARAM` | 3 | `--template-id` / `--contents` 必填缺失 | 显式传值;从 `template list` 取合法 templateId | | `INPUT_TOO_LARGE` | 3 | contents JSON 超过 10MB 限制 | **不支持分批次提交**。需精简内容或拆分为多个独立日志分别提交 | -| `MCP_TOOL_ERROR` | 1 | 服务端业务错(含 `server_error_code: PARAM_ERROR`,覆盖 templateId 错 / 字段名错 / 字段值错 / contents 空等多种形态)| 查看 `server_error_code` / `technical_detail`;服务端不区分具体子错因,按提交链路重新走 `template list → template get → entry submit`;连续 ≥ 2 次仍失败必须停止重试,降级 final_reply | -| `RESOURCE_NOT_FOUND` | 1 | reportId / templateId 在服务端找不到 | 用 `list` 或 `template list` 重新获取 | +| `MCP_TOOL_ERROR` / `[UNCLASSIFIED] business error` | 1 | 服务端业务错,`success=false`、`code=1`。覆盖:submit 时 templateId 错 / 字段名错 / 字段值错 / contents 空(`server_error_code: PARAM_ERROR`);`entry get` / `entry stats` 传了不存在的 reportId(`server_error_code: BUSINESS_ERROR`,technical_detail 含 `report not exist`);`template get` 传了不存在的模版名(`server_error_code: PARAM_ERROR`)| 查看 `server_error_code` / `technical_detail`;reportId / 模版名找不到时先用 `inbox list` / `outbox list` / `template list` 重新取合法值,不要猜;submit 类错误按链路重走 `template list → template get → entry submit`,连续 ≥ 2 次仍失败必须停止重试,降级 final_reply | ## 何时停止重试 diff --git a/skills/mono/references/products/sheet.md b/skills/mono/references/products/sheet.md index edc0bdb3..567c4278 100644 --- a/skills/mono/references/products/sheet.md +++ b/skills/mono/references/products/sheet.md @@ -83,11 +83,20 @@ dws sheet filter-view --help | `sheet copy` | 复制工作表 | | `sheet range read` | 读取工作表数据(别名: range get) | | `sheet range update` | 更新指定区域内容(值/公式/超链接) | +| `sheet range clear` | 清除区域(值/格式/全部) | +| `sheet range sort` | 对区域排序 | +| `sheet range fill` | 自动填充区域 | +| `sheet range copy-to` | 复制区域到目标位置 | +| `sheet range move-to` | 移动区域到目标位置 | +| `sheet range batch-clear` | 批量清除多个区域(原子事务) | +| `sheet batch-update` | 批量执行多个写操作(原子事务) | +| `sheet csv-get` | 以 CSV 格式读取区域数据 | | `sheet range set-style` | 设置单元格样式 | | `sheet range batch-set-style` | 按配置文件批量设置样式 | | `sheet find` | 搜索单元格内容 | | `sheet append` | 在末尾追加数据行 | | `sheet csv-put` | 将 CSV 数据写入指定位置(纯值,自动扩容) | +| `sheet delete-sheet` | 删除工作表(不可逆,删除前必须确认) | | `sheet replace` | 全局查找替换文本 | | `sheet merge-cells` | 合并单元格 | | `sheet unmerge-cells` | 取消合并单元格 | @@ -122,9 +131,44 @@ dws sheet filter-view --help | `sheet filter-view info` | 获取单个筛选视图详情 | | `sheet filter-view list-criteria` | 列出筛选视图所有列条件 | | `sheet filter-view get-criteria` | 获取单列筛选条件详情 | +| `sheet cond-format list` | 获取条件格式规则 | +| `sheet cond-format create` | 创建条件格式规则 | +| `sheet cond-format update` | 更新条件格式规则 | +| `sheet cond-format delete` | 删除条件格式规则 | +| `sheet chart list` | 获取浮动图表 | +| `sheet chart create` | 创建浮动图表 | +| `sheet chart update` | 更新浮动图表 | +| `sheet chart delete` | 删除浮动图表 | +| `sheet template list` | 获取表格模板列表 | +| `sheet template search` | 搜索表格模板 | +| `sheet template apply` | 应用模板创建新表格文档 | > 不确定参数?对任意命令执行 `dws sheet <命令> --help` 查看完整用法。 +## 子文档索引(更多命令详细参考) + +本文档覆盖高频命令。以下命令的完整参数、示例与工作流在 `sheet/` 子目录,需要时按主题查阅: + +| 主题 | 子文档 | 覆盖命令 | +|------|--------|---------| +| 表格与工作表管理 | [sheet/sheet-workbook.md](./sheet/sheet-workbook.md) | create / list / info / new / update / copy / delete-sheet | +| 写入数据 | [sheet/sheet-write-data.md](./sheet/sheet-write-data.md) | range update(对象协议详解)/ append / csv-put | +| 读取数据 | [sheet/sheet-read-data.md](./sheet/sheet-read-data.md) | range read / csv-get | +| 区域操作 | [sheet/sheet-range-operations.md](./sheet/sheet-range-operations.md) | range clear / sort / fill / copy-to / move-to | +| 批量操作 | [sheet/sheet-batch-operations.md](./sheet/sheet-batch-operations.md) | range batch-clear / batch-update | +| 行列操作 | [sheet/sheet-dimension-operations.md](./sheet/sheet-dimension-operations.md) | insert / delete / update / move / add-dimension | +| 样式与合并 | [sheet/sheet-style-format.md](./sheet/sheet-style-format.md) | range set-style / batch-set-style / merge-cells / unmerge-cells | +| 条件格式 | [sheet/sheet-conditional-format.md](./sheet/sheet-conditional-format.md) | cond-format list / create / update / delete | +| 浮动图表 | [sheet/sheet-chart.md](./sheet/sheet-chart.md) | chart list / create / update / delete | +| 下拉列表 | [sheet/sheet-dropdown.md](./sheet/sheet-dropdown.md) | set / get / delete-dropdown | +| 媒体与图片 | [sheet/sheet-media-image.md](./sheet/sheet-media-image.md) | media-upload / write-image / 浮动图片 | +| 查找替换 | [sheet/sheet-search-replace.md](./sheet/sheet-search-replace.md) | find / replace | +| 全局筛选 | [sheet/sheet-filter.md](./sheet/sheet-filter.md) | filter get / create / delete / update / clear-criteria / sort | +| 筛选视图 | [sheet/sheet-filter-view.md](./sheet/sheet-filter-view.md) | filter-view 全系列 | +| 导出 | [sheet/sheet-export.md](./sheet/sheet-export.md) | export | + +> `template list/search/apply` 无独立子文档,直接执行 `dws sheet template <子命令> --help` 查看用法。 + ## 意图判断 ### 表格与工作表管理 @@ -194,7 +238,7 @@ dws sheet filter-view --help 用户说"删除行/删除列/删掉第几行/删掉某列/移除行/移除列": - 删除行或列 → `delete-dimension` -- 仅清空内容但保留行/列 → `range update --values` 写入空字符串 `""` +- 仅清空内容但保留行/列 → `range clear`(整片区域清除,比逐格写空更简洁) 用户说"隐藏行/隐藏列/显示行/显示列/设置行高/设置列宽/调整行高/调整列宽/行列属性": - 隐藏/显示行或列 → `update-dimension --hidden` / `--hidden=false` @@ -340,8 +384,9 @@ dws sheet filter-view --help 以下是最容易出错的规则,**必须严格遵守**: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) -- ★ **`range update` 维度校验(强制)**:`--values` / `--hyperlinks` 的行列数必须与 `--range` 完全一致。例如 `--range "A1:C3"` → `--values` 必须是 3×3 数组 -- ★ **`range update` 清空规范(强制)**:清空单元格用空字符串 `""`,禁止用 `null`(全 null 会被跳过无效果) +- ★ **`range update` 单元格协议(强制)**:`--values` 是二维 JSON 数组,**每个单元格必须是 object**(如 `{"type":"text","text":"张三"}`),不再支持裸值 `"张三"` / `90` / `null`。裸值会报错「不支持原始值……每个单元格必须是 object」。数字/布尔也写成字符串 object(如 `{"type":"text","text":"90"}`),服务端自动识别类型。超链接写在单元格 object 的 `hyperlink` 字段,**没有 `--hyperlinks` flag**(传了报 unknown flag) +- ★ **`range update` 维度校验(强制)**:`--values` 的行列数必须与 `--range` 完全一致。例如 `--range "A1:C3"` → `--values` 必须是 3×3 的 object 数组 +- ★ **`range update` 清空规范(强制)**:清空单个单元格用 `{"type":"text","text":""}`;清空整片区域用 `range clear`。跳过某格保留原值用 `{}` 空对象 - ★ **单次调用上限(强制)**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) - ★ **大批量纯值写入用 `csv-put` 不用 `range update`**:当写入纯值(无公式/超链接)且数据量较大时(>5 行或 >20 单元格),必须使用 `csv-put`。`csv-put` 接受 CSV 文本直接写入,无需构造二维 JSON 数组,支持自动扩容,更简洁高效。仅在需要写入公式、超链接、或仅更新少量单元格时才使用 `range update` - ★ **搜索用 `find` 不用 `range read`**:`find` 是服务端搜索,禁止用 `range read` 全量读取后客户端过滤 @@ -432,15 +477,17 @@ Example: Flags: --node string 表格文档 ID 或 URL (必填) --sheet-id string 工作表 ID 或名称 (必填) - --title string 新标题,最长 100 字符,不能包含 / \ ? * [ ] : + --name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] : + --title string --name 的别名(兼容) --index int 新位置(从 0 开始) --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 + --tab-color string 工作表标签颜色,Hex 如 #FF0000;传空字符串清除颜色 --frozen-row-count int 冻结行数,0 表示取消冻结 --frozen-column-count int 冻结列数,0 表示取消冻结 ``` -更新工作表标题、位置、隐藏状态、冻结行列。 -`--title` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 +更新工作表名称、位置、隐藏状态、标签颜色、冻结行列。 +`--name`(别名 `--title`)/ `--index` / `--hidden` / `--tab-color` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 注意: - 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 @@ -483,14 +530,19 @@ Example: # 使用 get 别名,与 read 等价 dws sheet range get --node --sheet-id --range "A1:D10" + + # 指定取值模式:原始值 / 公式文本 + dws sheet range read --node --value-render-option raw_value + dws sheet range read --node --value-render-option formula Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) - --range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据) + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) + --range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据) + --value-render-option string 取值模式: formatted_value(格式化展示值,默认) | raw_value(原始值) | formula(公式文本,无公式回退原始值) ``` **超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: -- 先通过 `info` 获取 `rowCount` / `lastNonEmptyRow` / `columnCount` 确定数据边界 +- 先通过 `info` 获取 `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` 确定数据边界(空表时这些字段为 null) - 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… - 避免不传 `--range` 直接读取整个大工作表 @@ -499,34 +551,50 @@ Flags: Usage: dws sheet range update [flags] Example: - # 写入值 + # 写入文本(每个单元格必须是 object) dws sheet range update --node --sheet-id --range "A1:B2" \ - --values '[["姓名","分数"],["张三",90]]' + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"分数"}],[{"type":"text","text":"张三"},{"type":"text","text":"90"}]]' - # 写入公式 + # 写入公式(text 以 = 开头识别为公式) dws sheet range update --node --sheet-id --range "C2" \ - --values '[["=A2&B2"]]' + --values '[[{"type":"text","text":"=A2&B2"}]]' - # 写入超链接 + # 写入单元格级超链接(写在 cell 的 hyperlink 字段,没有 --hyperlinks flag) dws sheet range update --node --sheet-id --range "A1" \ - --hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"钉钉"}]]' + --values '[[{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' - # 清空区域(使用空字符串 "") - dws sheet range update --node --sheet-id --range "A1:B3" \ - --values '[["",""],["",""],["",""]]' + # 只更新部分单元格:用 {} 空对象占位保留原值 + dws sheet range update --node --sheet-id --range "A1:B1" \ + --values '[[{"type":"text","text":"新值"},{}]]' + + # 清空单个单元格(text 为空字符串) + dws sheet range update --node --sheet-id --range "A1" \ + --values '[[{"type":"text","text":""}]]' + + # 清空整片区域请改用 range clear + dws sheet range clear --node --sheet-id --range "A1:B3" Flags: --node string 表格文档 ID (必填) --sheet-id string 工作表 ID 或名称 (必填) --range string 目标单元格区域地址,如 A1:B3 (必填) - --values string 单元格值,二维 JSON 数组 (与 --hyperlinks 至少传一项) - --hyperlinks string 超链接,二维 JSON 数组 (与 --values 至少传一项) + --values string 单元格内容,二维 JSON 数组 (必填);每个元素必须是 object ``` +**单元格对象协议(重要)**:`--values` 每个元素必须是 object,支持以下形态: +- `{"type":"text","text":"内容"}`:普通文本;`text` 以 `=` 开头识别为公式;`text` 为 `""` 清空该格;数字/布尔写成字符串(`{"type":"text","text":"100"}`),服务端自动识别类型 +- `{"type":"text","text":"内容","cellStyles":{...}}`:文本 + 整格样式(`fontWeight`/`fontColor`/`backgroundColor`/`numberFormat` 等) +- `{"type":"richText","texts":[...]}`:富文本,子项可为 text/link/attachment/image,片段样式写在子项 `style` +- `{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://..."}}`:整格超链接(`type` 可为 `path`/`sheet`/`range`,或 `{"type":"none"}` 清除) +- `{"dataValidation":{...}}`:写下拉/复选框数据验证 +- `{}`:空对象,跳过该格保留原值(只改部分单元格时占位用) + +不再支持裸值(`"张三"` / `90` / `null`),原样传入会报「不支持原始值……每个单元格必须是 object」。 + **单次调用建议**:行数 ≤ 1000,单元格总数(行×列)≤ 5000;超过时请拆分多次调用。 -**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式和超链接),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装二维 JSON 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、超链接(`--hyperlinks`)、或修改少量单元格时才使用 `range update`。 +**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式和超链接),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装 object 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、超链接、富文本、或修改少量单元格时才使用 `range update`。 -**范围职责**:`range update` 仅负责写入单元格的值与超链接,不接受任何样式参数。如需设置数字格式(百分比 / 货币 / 日期 / 文本等)请使用 `dws sheet range set-style --number-format <格式代码>`,可与其他样式参数同时传入。 +**范围职责**:`range update` 负责写入单元格的值、超链接、per-cell 样式与数据验证。批量刷整片区域的统一样式或数字格式(百分比 / 货币 / 日期 / 文本等)请使用 `dws sheet range set-style --number-format <格式代码>`。 ### 设置单元格样式 ``` @@ -662,9 +730,11 @@ Flags: Usage: dws sheet csv-put [flags] Example: + # 内联多行 CSV:必须用 $'...' 让 \n 变成真换行;普通单引号 '...\n...' 里的 \n 是字面量,会写成一行含字面 \n 的错误数据 dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv 'name,score\nAlice,95\nBob,87' + --csv $'name,score\nAlice,95\nBob,87' + # 多行数据推荐用 @文件,最稳妥 dws sheet csv-put --node --sheet-id --start-cell B2 \ --csv @data.csv --allow-overwrite @@ -750,7 +820,7 @@ Flags: 在钉钉表格指定工作表中,从指定位置起删除若干连续的行或列。 `--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`。 +删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range clear`(整片区域清除,比逐格写空更简洁)。 ### 更新指定范围行/列属性 ``` @@ -832,6 +902,8 @@ Flags: --mime-type string 文件 MIME 类型 (默认根据扩展名推断) ``` +`--format json` 输出规整 JSON:`{success, resourceId, resourceUrl, fileName, fileSize}`。`resourceUrl` 可用于 `create-float-image` 的 `--src`。 + ### 上传图片并写入表格单元格 ``` Usage: @@ -1083,7 +1155,8 @@ Flags: 查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 - **用途**:查看单元格已设置的下拉列表选项和配置。 - **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 +- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `multipleValues` 和 `colorValueMap`)。**判空以 `hasDropdown` 为准**:无下拉列表时 `hasDropdown` 为 false,但 `dataValidations` 里仍会带 1 个全 null 的幽灵条目,不要用数组长度判空。 +- **已知限制**:`options.multipleValues` 服务端恒返回 `null`(即使该下拉是用 `--multi-select` 建的),且不返回 `enableMultiSelect` 字段——**`get-dropdown` 读回无法判断下拉是否多选**。要判断是否多选,改用 `range read`:其 `cells[].dataValidation.enableMultiSelect` 字段准确(实测有效)。 ### 删除下拉列表 ``` @@ -1118,7 +1191,7 @@ Flags: - **用途**:查看当前工作表上是否存在全局筛选及其配置。 - **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 - **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 -- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。 +- **返回**:`range`(筛选范围,A1 表示法)、`id`(筛选 ID)和 `criteria`(各列条件对象,key 为列偏移量;无条件时为 `{}`)。如果未设置筛选,返回筛选信息为空。 ### 创建筛选 ``` @@ -1499,10 +1572,10 @@ Flags: - 第 21~30 次:每次间隔 15 秒 - **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 -**命令返回**: -- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载) -- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: ` -- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下 +**命令返回**(`--format json`,默认):输出规整 JSON `{success, jobId, downloadUrl[, outputPath]}`。轮询进度以 `[INFO] [N/30] 状态: ...` 打到 stderr,不污染 stdout 的 JSON。 +- `--output` 未指定:JSON 含 `jobId` + `downloadUrl`(链接有时效性,请尽快下载) +- `--output` 指定为文件路径:下载到该路径,JSON 额外含 `outputPath` +- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录,JSON 含 `outputPath` **失败处理(命令内部已处理,Agent 仅需转述)**: - MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 @@ -1510,6 +1583,29 @@ Flags: **限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 +### 表格模板(list / search / apply) +``` +Usage: + dws sheet template list [flags] # 列出可用模板(--cursor / --limit 分页) + dws sheet template search [flags] # 按关键词搜索模板 + dws sheet template apply [flags] # 应用模板创建新表格文档 + +template search Flags: + --query string 搜索关键词 (必填) + --source string 模板来源: MY(我的模版,默认) / PUBLIC(公开模版) + --cursor string 分页游标 + --limit int 返回数量上限 + +template apply Flags: + --template-id string 模板 ID (必填,来自 list/search) + --name string 新表格文档名称 (可选) + --folder string 目标文件夹 ID (可选,UUID 或 URL) + --workspace string 知识库 ID (可选) +``` + +- 用户说"用模板建表 / 有哪些模板 / 找个 XX 模板"时走 template 系列 +- 典型链路:`template search --query "..."` 或 `template list` 拿到 `templateId` → `template apply --template-id --name "..."` 创建新表格,返回新文档的 `nodeId` + ## 核心工作流 ```bash @@ -1521,12 +1617,12 @@ dws sheet create --name "销售数据" --format json # 2. 查看工作表列表 — 提取 sheetId dws sheet list --node --format json -# 3. 写入表头和数据 +# 3. 写入表头和数据(每个单元格必须是 object;数字也写成字符串) dws sheet range update --node --sheet-id --range "A1:C1" \ - --values '[["姓名","部门","销售额"]]' --format json + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json dws sheet range update --node --sheet-id --range "A2:C4" \ - --values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json + --values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json # ── 工作流 2: 读取已有表格数据 ── @@ -1547,26 +1643,26 @@ dws sheet range read --node --sheet-id --range "A1:D10" --f # 1. 新建工作表 dws sheet new --node --name "汇总" --format json -# 2. 在新工作表中写入汇总公式 +# 2. 在新工作表中写入汇总公式(公式写在 text,以 = 开头) dws sheet range update --node --sheet-id --range "A1:B1" \ - --values '[["指标","数值"]]' --format json + --values '[[{"type":"text","text":"指标"},{"type":"text","text":"数值"}]]' --format json dws sheet range update --node --sheet-id --range "A2:B2" \ - --values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json + --values '[[{"type":"text","text":"总销售额"},{"type":"text","text":"=SUM(Sheet1!C2:C100)"}]]' --format json # ── 工作流 4: 写入数据并设置样式 ── # 1. 写入数据 dws sheet range update --node --sheet-id --range "A1:C3" \ - --values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json + --values '[[{"type":"text","text":"商品"},{"type":"text","text":"单价"},{"type":"text","text":"数量"}],[{"type":"text","text":"苹果"},{"type":"text","text":"5.5"},{"type":"text","text":"100"}],[{"type":"text","text":"香蕉"},{"type":"text","text":"3.2"},{"type":"text","text":"200"}]]' --format json -# 2. 设置数字格式(人民币)——请走 set-style,不要放到 range update +# 2. 设置数字格式(人民币)——批量刷整列走 set-style dws sheet range set-style --node --sheet-id --range "B2:B3" \ --number-format "¥#,##0.00" --format json -# 3. 写入超链接 +# 3. 写入超链接(写在 cell 的 hyperlink 字段) dws sheet range update --node --sheet-id --range "D1" \ - --hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"详情"}]]' --format json + --values '[[{"type":"text","text":"详情","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' --format json # ── 工作流 5: 追加数据 ── @@ -1716,8 +1812,8 @@ dws sheet write-image --node --sheet-id --range C3:C3 --fil # 4. 完整流程: 创建表格 → 写表头 → 写入图片 dws sheet create --name "产品目录" -f json # 提取 nodeId 后: -dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json -dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[[{"type":"text","text":"产品名称"},{"type":"text","text":"产品图片"}]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[[{"type":"text","text":"MacBook Pro"}]]' -f json dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json ``` @@ -1789,7 +1885,7 @@ dws sheet export --node --output ./ | `create` | `nodeId` | list / info / new / range read / range update / find 的 --node | | `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | | `new` | 新工作表的 `sheetId` | range read / range update / find 的 --sheet-id | -| `info` | `rowCount` / `lastNonEmptyRow` | 确定数据范围、追加写入起始行 | +| `info` | `rowCount` / `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` / `mergedRanges` | 确定数据范围、追加写入起始行、判断合并结构(空表时 nonEmptyRange.* 为 null) | | `find` | `matchedCells` 中的 `a1Notation` | 定位目标单元格,用于 range read / range update | | `append` | `a1Notation` 追加数据所在范围 | 确认追加位置 | | `csv-put` | `a1Notation` 实际写入的单元格范围 | 确认写入位置和范围 | @@ -1820,7 +1916,7 @@ dws sheet export --node --output ./ | `filter-view delete-criteria` | `id` 筛选视图 ID | 确认条件清除完成 | | `filter-view list-criteria` | 所有列条件(按列偏移量为 key 的对象) | 了解当前视图已设置哪些列的条件 | | `filter-view get-criteria` | 指定列的条件详情(`filterType`、`conditions` 等) | 查看某列的具体筛选规则 | -| `export` | `downloadUrl`(未指定 --output)/ `导出完成: `(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | +| `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | ## nodeId 多格式说明 @@ -1830,26 +1926,28 @@ dws sheet export --node --output ./ ## values 参数格式说明 -`--values` 为二维 JSON 数组,第一维为行,第二维为列: -- 字符串值: `"文本"` -- 数字值: `100` 或 `3.14` -- 公式: `"=SUM(B2:B4)"`(以 `=` 开头的字符串自动识别为公式) -- 清空单元格: 统一使用空字符串 `""`(不要用 `null` 取代,null 不会保留原值且全 null 会被视为无效调用跳过) +`--values` 为二维 JSON 数组,第一维为行,第二维为列。**每个单元格必须是 object**,不支持裸值 `"文本"` / `100` / `null`: +- 文本: `{"type":"text","text":"文本"}` +- 数字/布尔: 写成字符串 `{"type":"text","text":"100"}`,服务端按内容自动识别为数字/布尔 +- 公式: `{"type":"text","text":"=SUM(B2:B4)"}`(`text` 以 `=` 开头识别为公式) +- 清空单个单元格: `{"type":"text","text":""}`;清空整片区域用 `range clear` +- 跳过某格保留原值: `{}` 空对象 +- 整格样式: `{"type":"text","text":"重要","cellStyles":{"fontWeight":"bold","fontColor":"#FF0000","numberFormat":"@"}}` +- 富文本: `{"type":"richText","texts":[...]}`(子项 text/link/attachment/image,片段样式写在子项 `style`) -维度必须与 `--range` 范围一致,例如 `--range "A1:B3"` 需要 3 行 2 列的数组。 +维度必须与 `--range` 范围一致,例如 `--range "A1:B3"` 需要 3 行 2 列的 object 数组。 -## hyperlinks 参数格式说明 +## 超链接写法说明 -`--hyperlinks` 为二维 JSON 数组,每个元素为对象或 null: -- `type`: 链接类型,可选 `path`(外部链接)、`sheet`(工作表跳转)、`range`(单元格跳转) -- `link`: 链接地址 -- `text`: 显示文本 - -与 `--values` 共存时,hyperlinks 优先级更高。 +**没有 `--hyperlinks` flag**(旧写法已废弃,传入报 unknown flag)。单元格级超链接写在 `--values` 里 cell object 的 `hyperlink` 字段: +- `{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}`:写外部链接 +- `hyperlink.type` 可为 `path`(外部链接)/ `sheet`(工作表跳转,link 为工作表名)/ `range`(区域跳转,link 为 `Sheet2!A1`) +- `{"hyperlink":{"type":"none"}}`:清除整格超链接(保留原值) +- 富文本片段内的链接写在 `richText` 子项的 `link` 字段,与整格 `hyperlink` 不同 ## number-format 常用值 -适用范围:`number-format` 仅在 `range set-style` / `range batch-set-style` 中接受(CLI 对应 `--number-format`,batch 配置文件对应 `numberFormat`);`range update` 不接受该参数。 +适用范围:`number-format` 在 `range set-style` / `range batch-set-style` 中作为 `--number-format` 参数;`range update` 没有 `--number-format` flag,但可在每个 cell object 的 `cellStyles.numberFormat` 里写同样的格式代码。批量刷整片区域优先用 `set-style`。 | 格式代码 | 说明 | 示例 | |----------|------|------| @@ -1867,20 +1965,20 @@ dws sheet export --node --output ./ > 标 ★ 的条目已在前文「关键注意事项」中列出,此处为完整说明。 - ★ `--sheet-id` 获取规范(强制):所有涉及 `--sheet-id` 参数的命令(`info` / `new` / `range read` / `range update` / `find` / `append` / `insert-dimension` / `delete-dimension` / `update-dimension` / `move-dimension` / `add-dimension` / `merge-cells` / `unmerge-cells` / `replace` / `write-image` / `set-dropdown` / `get-dropdown` / `delete-dropdown` / `filter-view *` 等),除非用户主动提供了工作表 ID 或工作表名称,否则在 `sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 -- ★ `range update` 维度校验(强制):调用 `range update` 写入 `--values` 或 `--hyperlinks` 时,必须严格校验二维 JSON 数组的行数与列数与 `--range` 指定的范围完全一致: - - 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 `[[v1,v2,v3],[v4,v5,v6],[v7,v8,v9]]` 这样 3×3 的数组 - - `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[v]]` - - 行数不足需要用空字符串补齐,列数不足需要补齐到每行相同长度;禁止出现各行列数不一致或与 `--range` 不匹配的情况,否则调用会直接报错 - - 同时传入 `--values` 和 `--hyperlinks` 时,两个二维数组的行列数都必须与 `--range` 严格一致 -- ★ `range update` 清空单元格规范(强制):如需清空单元格内容,统一使用空字符串 `""`。禁止使用 `null`:`null` 不会保留单元格原值,也不存在"选择性保留"场景;且若 `--values` 全部为 `null`,整体调用会被视为无效而跳过,无任何写入效果 +- ★ `range update` 单元格对象协议(强制):`--values` 每个单元格必须是 object(`{"type":"text","text":...}` / `{"type":"richText",...}` / `{}` 等),不支持裸值 `"张三"` / `90` / `null`,裸值报「不支持原始值……每个单元格必须是 object」。数字/布尔写成字符串 object,服务端自动识别类型。超链接写在 cell object 的 `hyperlink` 字段,没有 `--hyperlinks` flag +- ★ `range update` 维度校验(强制):`--values` 二维数组的行数与列数必须与 `--range` 完全一致: + - 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 3×3 的 object 数组 + - `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[{...}]]` + - 不改的格用 `{}` 空对象占位补齐;禁止各行列数不一致或与 `--range` 不匹配,否则报错 +- ★ `range update` 清空单元格规范(强制):清空单个单元格用 `{"type":"text","text":""}`(不是裸 `""`,也不是 `null`);清空整片区域用 `range clear`;跳过某格保留原值用 `{}` 空对象 - `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录 - `list` 返回所有工作表的 ID 和名称,是后续操作的必要前置步骤 - `info` 不传 `--sheet-id` 时默认返回第一个工作表的详情 - `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 - `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) - `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 获取边界后分批读取,避免不传 `--range` 直接读取整个大工作表 -- `range update` 的 `--values` 和 `--hyperlinks` 至少传入一项 -- `range update` 职责边界:`range update` 仅写入单元格的值与超链接,不接受任何样式参数(包括但不限于数字格式 / 背景色 / 字体 / 对齐方式等)。如需设置数字格式,请使用 `dws sheet range set-style --number-format <格式代码>`;批量场景走 `dws sheet range batch-set-style --batch `(配置文件中使用 `numberFormat` 字段)。不要在同一次 `range update` 调用里同时完成写值与格式设置 +- `range update` 只有 `--values` 一个内容参数(必填),超链接/样式/数据验证都写进 cell object 内部字段 +- `range update` 职责边界:可写值、超链接、per-cell 样式(`cellStyles`)与数据验证(`dataValidation`)。批量刷整片区域的统一样式或数字格式优先用 `dws sheet range set-style --number-format <格式代码>`;批量多区域走 `dws sheet range batch-set-style --batch `(配置文件用 `numberFormat` 字段) - ★ `range update` / `range set-style` / `range batch-set-style` 单次调用上限(强制):行数 ≤ 1000,单元格总数(行×列)建议≤ 5000(底层硬限 30000);超限请拆分多次调用。CLI 会在调用前做本地预校验,底层超 30000 会直接报错 - `range set-style` / `range batch-set-style` 的样式枚举按驼峰书写:`wordWrap` 取 `overflow`/`clip`/`autoWrap`,`fontWeight` 取 `bold`/`normal`,`hAlign` 取 `left`/`center`/`right`/`general`,`vAlign` 取 `top`/`middle`/`bottom`;背景色/字体颜色统一使用 `#RRGGBB` 格式 - `new` 创建工作表时,如名称与已有工作表重复,系统会自动重命名 @@ -1900,7 +1998,7 @@ dws sheet export --node --output ./ - `delete-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` - `delete-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` - `delete-dimension` 的 `--length` 最大为 5000 -- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`(参见《range update 清空单元格规范》) +- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range clear`(整片区域清除,比逐格写空更简洁) - `update-dimension` 批量更新连续行/列的显隐状态与行高/列宽 - `update-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` - `update-dimension` 的 `--start-index` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` @@ -1939,7 +2037,7 @@ dws sheet export --node --output ./ - `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false - `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 - ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 -- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空 +- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)、`id` 和 `criteria`(各列条件对象,无条件时为 `{}`)。无筛选时返回空 - `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 - `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 - `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 diff --git a/skills/mono/references/products/sheet/sheet-chart.md b/skills/mono/references/products/sheet/sheet-chart.md index 8aa7fee2..0295faae 100644 --- a/skills/mono/references/products/sheet/sheet-chart.md +++ b/skills/mono/references/products/sheet/sheet-chart.md @@ -55,6 +55,8 @@ Flags: --chart-id string 浮动图表 ID (可选,不传则返回全部) ``` +- **判空看 `totalCount` / `message`,不要看数组长度**:无图表时 `floatCharts` 数组里仍会带 1 个全 null 的幽灵元素(`chart.category`/`chart.series` 全 null,无 id),但 `totalCount` 为 0、`message` 为「Successfully retrieved 0 float chart(s).」。判断有没有图表以 `totalCount` 为准,或过滤掉没有 `id` 的元素。 + ### 创建浮动图表 ``` Usage: @@ -342,6 +344,7 @@ dws sheet chart update --node --sheet-id --chart-id --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - [强制] **创建后必须验证**:图表创建后必须调用 `chart list` 验证配置是否正确 +- ⚠️ **`chart list` 判空看 `totalCount` 不看数组长度**:无图表时 `floatCharts` 仍含 1 个全 null 幽灵元素(无 id),`totalCount` 为 0、`message` 说明为空。判断有没有图表以 `totalCount` 为准或过滤无 `id` 的元素 - [强制] **chart-id 禁止臆测**:必须通过 `chart list` 获取真实的图表 ID,不可编造 - **图表类型映射**:用户说"柱形图"用 `column`,"条形图"(横向)用 `bar`,"散点图"用 `scatter`,"雷达图"用 `radar`,"环形图"用 `doughnut` - **堆积类型后缀**:堆叠版本的图表在基本类型后加 `Stacked`(如 `columnStacked`、`lineStacked`、`areaStacked`) diff --git a/skills/mono/references/products/sheet/sheet-conditional-format.md b/skills/mono/references/products/sheet/sheet-conditional-format.md index b4b7532a..70bc77b7 100644 --- a/skills/mono/references/products/sheet/sheet-conditional-format.md +++ b/skills/mono/references/products/sheet/sheet-conditional-format.md @@ -65,8 +65,9 @@ **正确做法(两步走)**: ```bash # Step 1: 用 range update 在新列写判断公式(形成"是/否"辅助列) +# 每个单元格必须是 object,公式写在 text(以 = 开头);不支持裸字符串 dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "H2:H100" \ - --values '[["=IF(A2>B2,\"是\",\"否\")"],...]' + --values '[[{"type":"text","text":"=IF(A2>B2,\"是\",\"否\")"}],...]' # Step 2: 基于辅助列值做条件格式(用 formulaCondition 引用辅助列) dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID \ @@ -142,7 +143,8 @@ Flags: - **用途**:查看指定工作表中已有的条件格式规则,或获取单个规则的详情。 - **场景**:创建/更新/删除条件格式前后验证规则状态;获取 ruleId 供后续 update/delete 使用。 -- **返回**:rules 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。 +- **返回**:`rules` 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。 +- **判空要看 `message`,不要看数组长度**:无规则时 `rules` 数组里仍会带 1 个全 null 的幽灵元素(`colorScaleCondition`/`dataBarStyle`/`iconSetCondition` 全 null,无 id),`message` 会是「No conditional formatting rules found.」。该命令**没有 `totalCount` 字段**,判空以 message 为准,或过滤掉没有 `id` 的元素。 ### 创建条件格式规则 ``` @@ -269,3 +271,4 @@ Flags: - **辅助列+条件格式两步走**:用户明确要求"辅助列"时,必须按两步走(先建辅助列 → 再基于辅助列做条件格式),禁止直接用 `formulaCondition` 一步绕过 - **大数据量优势**:当数据量 > 1000 行时,条件格式是首选——它由服务端自身渲染,不需要逐行调用 `range set-style`,性能远优于静态样式写入 - **判断标准**:交付后 `cond-format list` 必须能返回该规则;否则视为违规 +- ⚠️ **`cond-format list` 判空看 `message` 不看数组长度**:无规则时 `rules` 仍含 1 个全 null 幽灵元素(无 id),`message` 为「No conditional formatting rules found.」。该命令无 `totalCount`,判空以 message 为准或过滤无 `id` 的元素 diff --git a/skills/mono/references/products/sheet/sheet-dropdown.md b/skills/mono/references/products/sheet/sheet-dropdown.md index 0b135aa4..173594e2 100644 --- a/skills/mono/references/products/sheet/sheet-dropdown.md +++ b/skills/mono/references/products/sheet/sheet-dropdown.md @@ -55,10 +55,11 @@ Flags: --range string 查询范围,A1 表示法,如 A1:A100 (必填) ``` -查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 +查询指定范围内的下拉列表配置信息,包括选项值和颜色。 - **用途**:查看单元格已设置的下拉列表选项和配置。 - **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 +- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `multipleValues` 和 `colorValueMap`)。**判空以 `hasDropdown` 为准**:范围内无下拉列表时 `hasDropdown` 为 false,但 `dataValidations` 里仍会带 1 个全 null 的幽灵条目,不要用数组长度判空。 +- ⚠️ **已知限制**:`options.multipleValues` 服务端恒返回 `null`(即使该下拉是用 `--multi-select` 建的),也不返回 `enableMultiSelect` 字段——**`get-dropdown` 读回无法判断该下拉是否多选**。要判断是否多选,改用 `range read`:它返回的 `cells[].dataValidation.enableMultiSelect` 字段是准确的(实测有效)。 ### 删除下拉列表 ``` @@ -90,5 +91,6 @@ Flags: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 -- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false +- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。判空以 `hasDropdown` 为准(无下拉时为 false,但 `dataValidations` 仍含 1 个全 null 幽灵条目) +- ⚠️ `get-dropdown` 的 `options.multipleValues` 恒为 `null`、无 `enableMultiSelect` 字段,读回无法判断是否多选;要判断是否多选改用 `range read`(其 `dataValidation.enableMultiSelect` 准确) - `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 diff --git a/skills/mono/references/products/sheet/sheet-export.md b/skills/mono/references/products/sheet/sheet-export.md index 359b8c16..f390efed 100644 --- a/skills/mono/references/products/sheet/sheet-export.md +++ b/skills/mono/references/products/sheet/sheet-export.md @@ -47,10 +47,10 @@ Flags: - 第 21~30 次:每次间隔 15 秒 - **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 -**命令返回**: -- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载) -- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: ` -- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下 +**命令返回**(`--format json`,默认):输出规整 JSON `{success, jobId, downloadUrl[, outputPath]}`。轮询进度以 `[INFO] [N/30] 状态: ...` 打到 stderr,不污染 stdout 的 JSON(可直接 `python3 -m json.tool` 解析 stdout)。 +- `--output` 未指定:JSON 含 `jobId` + `downloadUrl`(链接有时效性,请尽快下载) +- `--output` 指定为文件路径:下载到该路径,JSON 额外含 `outputPath` +- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名保存,JSON 含 `outputPath` **失败处理(命令内部已处理,Agent 仅需转述)**: - MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 @@ -82,7 +82,7 @@ dws sheet export --node --output ./ | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `export` | `downloadUrl`(未指定 --output)/ `导出完成: `(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | +| `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | ## 注意事项 diff --git a/skills/mono/references/products/sheet/sheet-filter.md b/skills/mono/references/products/sheet/sheet-filter.md index 0e65607b..36d69dc4 100644 --- a/skills/mono/references/products/sheet/sheet-filter.md +++ b/skills/mono/references/products/sheet/sheet-filter.md @@ -32,7 +32,7 @@ Flags: - **用途**:查看当前工作表上是否存在全局筛选及其配置。 - **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 - **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 -- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。 +- **返回**:`range`(筛选范围,A1 表示法)、`id`(筛选 ID)和 `criteria`(各列条件对象,key 为列偏移量;无条件时为 `{}`)。如果未设置筛选,返回筛选信息为空。 ### 创建筛选 ``` @@ -152,7 +152,7 @@ Flags: | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `filter get` | `range`(筛选范围)、`columnFilterCriteria`(各列条件) | 查看当前筛选配置,确认筛选是否存在 | +| `filter get` | `range`(筛选范围)、`id`、`criteria`(各列条件对象,无条件为 `{}`) | 查看当前筛选配置,确认筛选是否存在 | | `filter create` | 筛选创建成功的确认 | 确认筛选已建立,后续可通过 `filter update` 设置条件 | | `filter delete` | 删除成功的确认 | 确认筛选已删除 | | `filter update` | 更新成功的确认 | 确认条件已设置 | @@ -164,7 +164,7 @@ Flags: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 -- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空 +- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)、`id` 和 `criteria`(各列条件对象,无条件时为 `{}`)。无筛选时返回空 - `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 - `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 - `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 diff --git a/skills/mono/references/products/sheet/sheet-media-image.md b/skills/mono/references/products/sheet/sheet-media-image.md index 40cf3640..060c55c9 100644 --- a/skills/mono/references/products/sheet/sheet-media-image.md +++ b/skills/mono/references/products/sheet/sheet-media-image.md @@ -48,6 +48,8 @@ Flags: --mime-type string 文件 MIME 类型 (默认根据扩展名推断) ``` +`--format json` 输出规整 JSON:`{success, resourceId, resourceUrl, fileName, fileSize}`。`resourceUrl` 可用于 `create-float-image` 的 `--src`。 + ### 上传图片并写入表格单元格 ``` Usage: @@ -204,8 +206,8 @@ dws sheet write-image --node --sheet-id --range C3:C3 --fil # 4. 完整流程: 创建表格 → 写表头 → 写入图片 dws sheet create --name "产品目录" -f json # 提取 nodeId 后: -dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json -dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[[{"type":"text","text":"产品名称"},{"type":"text","text":"产品图片"}]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[[{"type":"text","text":"MacBook Pro"}]]' -f json dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json ``` diff --git a/skills/mono/references/products/sheet/sheet-range-operations.md b/skills/mono/references/products/sheet/sheet-range-operations.md index 36d92a03..af6c076d 100644 --- a/skills/mono/references/products/sheet/sheet-range-operations.md +++ b/skills/mono/references/products/sheet/sheet-range-operations.md @@ -82,9 +82,11 @@ Flags: --sheet-id string 工作表 ID 或名称 (必填) --source-range string 源数据范围,A1 表示法 (必填) --target-range string 目标填充范围,A1 表示法 (必填) - --fill-type string 填充类型: series(序列,默认) / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值) + --fill-type string 填充类型: 不传则自动检测 / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值) ``` +`--fill-type` **不传时自动检测**(根据源数据智能判断:数值序列递增、日期递增、文本复制等),这是填序列的默认行为,无需显式传值。`--help` 枚举的可选值只有 `copy` / `onlystyle` / `withoutstyle`;服务端另外也接受 `series`(强制按序列填充),但它不在 `--help` 列表里,一般用不到——要序列递增直接不传 `--fill-type` 即可。 + 目标范围须与源范围在行或列维度对齐(不支持对角填充)。 ### 复制区域 diff --git a/skills/mono/references/products/sheet/sheet-style-format.md b/skills/mono/references/products/sheet/sheet-style-format.md index 240e57fc..bfa4bc1f 100644 --- a/skills/mono/references/products/sheet/sheet-style-format.md +++ b/skills/mono/references/products/sheet/sheet-style-format.md @@ -219,9 +219,9 @@ Flags: ```bash # ── 工作流 4: 写入数据并设置样式 ── -# 1. 写入数据 +# 1. 写入数据(每个单元格必须是 object;数字也写成字符串) dws sheet range update --node --sheet-id --range "A1:C3" \ - --values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json + --values '[[{"type":"text","text":"商品"},{"type":"text","text":"单价"},{"type":"text","text":"数量"}],[{"type":"text","text":"苹果"},{"type":"text","text":"5.5"},{"type":"text","text":"100"}],[{"type":"text","text":"香蕉"},{"type":"text","text":"3.2"},{"type":"text","text":"200"}]]' --format json # 2. 设置数字格式(人民币) dws sheet range set-style --node --sheet-id --range "B2:B3" \ diff --git a/skills/mono/references/products/sheet/sheet-workbook.md b/skills/mono/references/products/sheet/sheet-workbook.md index 8233dbfe..be061b0f 100644 --- a/skills/mono/references/products/sheet/sheet-workbook.md +++ b/skills/mono/references/products/sheet/sheet-workbook.md @@ -108,14 +108,16 @@ Flags: --node string 表格文档 ID 或 URL (必填) --sheet-id string 工作表 ID 或名称 (必填) --name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] : + --title string --name 的别名(兼容) --index int 新位置(从 0 开始) --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 + --tab-color string 工作表标签颜色,Hex 如 #FF0000;传空字符串清除颜色 --frozen-row-count int 冻结行数,0 表示取消冻结 --frozen-column-count int 冻结列数,0 表示取消冻结 ``` -更新工作表名称、位置、隐藏状态、冻结行列。 -`--name` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 +更新工作表名称、位置、隐藏状态、标签颜色、冻结行列。 +`--name`(别名 `--title`)/ `--index` / `--hidden` / `--tab-color` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 注意: - 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 @@ -174,12 +176,12 @@ dws sheet create --name "销售数据" --format json # 2. 查看工作表列表 — 提取 sheetId dws sheet list --node --format json -# 3. 写入表头和数据 +# 3. 写入表头和数据(每个单元格必须是 object;数字也写成字符串) dws sheet range update --node --sheet-id --range "A1:C1" \ - --values '[["姓名","部门","销售额"]]' --format json + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json dws sheet range update --node --sheet-id --range "A2:C4" \ - --values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json + --values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json # ── 工作流 2: 读取已有表格数据 ── @@ -200,12 +202,12 @@ dws sheet range read --node --sheet-id --range "A1:D10" --f # 1. 新建工作表 dws sheet new --node --name "汇总" --format json -# 2. 在新工作表中写入汇总公式 +# 2. 在新工作表中写入汇总公式(公式写在 text,以 = 开头) dws sheet range update --node --sheet-id --range "A1:B1" \ - --values '[["指标","数值"]]' --format json + --values '[[{"type":"text","text":"指标"},{"type":"text","text":"数值"}]]' --format json dws sheet range update --node --sheet-id --range "A2:B2" \ - --values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json + --values '[[{"type":"text","text":"总销售额"},{"type":"text","text":"=SUM(Sheet1!C2:C100)"}]]' --format json ``` ## 上下文传递 diff --git a/skills/mono/references/products/sheet/sheet-write-data.md b/skills/mono/references/products/sheet/sheet-write-data.md index fc90feec..6b6f4451 100644 --- a/skills/mono/references/products/sheet/sheet-write-data.md +++ b/skills/mono/references/products/sheet/sheet-write-data.md @@ -100,9 +100,11 @@ Flags: Usage: dws sheet csv-put [flags] Example: + # 内联多行 CSV:必须用 $'...' 让 \n 变成真换行;普通单引号 '...\n...' 里的 \n 是字面量,会写成一行含字面 \n 的错误数据 dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv 'name,score\nAlice,95\nBob,87' + --csv $'name,score\nAlice,95\nBob,87' + # 多行数据推荐用 @文件,最稳妥 dws sheet csv-put --node --sheet-id --start-cell B2 \ --csv @data.csv --allow-overwrite diff --git a/skills/mono/references/products/todo.md b/skills/mono/references/products/todo.md index 4f3527cc..543ee18a 100644 --- a/skills/mono/references/products/todo.md +++ b/skills/mono/references/products/todo.md @@ -188,11 +188,12 @@ Flags: --task-id string 待办任务 ID (必填) ``` +返回结构:子待办在**顶层** `subTasks[]` 数组里(不在 `result` 下),每个元素含 `taskId`(子待办 ID)、`subject`、`priority` 等字段。取子待办的 `taskId` 用于后续操作。 +注意:`task get` 返回的 `result.todoDetailModel.subTodos[]` 里**没有 taskId 字段**,要拿子待办 ID 必须用本命令 `task list-sub`。 + ### 上传待办附件 -> ⚠️ 当前不可用:后端未注册该工具,调用报「未找到指定工具」。遇到此需求直接告知用户暂不支持,勿重试或变通。 - -> ⚠️ 重要:该接口会上传文件到附件,不可用于测试或试探性调用。调用前必须确认待办存在。 +> ⚠️ 重要:该接口会真实上传文件到附件,不可用于测试或试探性调用。调用前必须确认待办存在。 ``` Usage: @@ -204,9 +205,9 @@ Flags: --task-id string 待办任务 ID (必填) ``` -### 查询待办附件列表 +返回 `result.attachmentIds`(数组,如 `["6a4cffb79e2b520ed3600960"]`),即新上传附件的 attachmentId。 -> ⚠️ 当前不可用:后端未注册该工具,调用报「未找到指定工具」。遇到此需求直接告知用户暂不支持,勿重试或变通。 +### 查询待办附件列表 ``` Usage: @@ -217,9 +218,9 @@ Flags: --task-id string 待办任务 ID (必填) ``` -### 删除待办附件 +返回 `attachments[]`(顶层数组),每个元素含 `attachmentId`、`fileName`、`fileSize`;无附件时返回空数组。 -> ⚠️ 当前不可用:后端未注册该工具,调用报「未找到指定工具」。遇到此需求直接告知用户暂不支持,勿重试或变通。 +### 删除待办附件 > **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 @@ -233,7 +234,7 @@ Flags: --attachment-id string 待办附件 ID (必填) --task-id string 待办任务 ID (必填) ``` -附件 attachmentId 使用 `dws todo task list-attachment` 命令获取。 +附件 attachmentId 使用 `dws todo task list-attachment` 命令获取。删除后可再 `list-attachment` 复查,返回空数组即删除成功。 ### 添加待办提醒 ``` @@ -313,7 +314,7 @@ JSON 数组,每个元素为一条提醒规则,支持两种 `baseTime` 模式 ## 核心工作流 ```bash -# 1. 创建待办 — 提取 todoTaskId +# 1. 创建待办 — 从返回 result.taskId 提取任务 ID dws todo task create --title "修复线上Bug" --executors userId1,userId2 \ --priority 40 --due "2026-03-10T18:00:00+08:00" --format json @@ -367,18 +368,28 @@ dws todo task add-reminder --task-id --base-time customTime --reminder- dws todo task reset-reminder --task-id --format json # 17. 重置待办提醒(指定新规则) dws todo task reset-reminder --task-id --reminder-rules '' --format json + +# 18. 上传附件(真实上传,先确认待办存在)— 从返回 result.attachmentIds 取 attachmentId +dws todo task add-attachment --task-id --file-path /path/to/file.pdf --format json +# 19. 查询附件列表 — 从返回 attachments[].attachmentId 取 ID +dws todo task list-attachment --task-id --format json +# 20. 删除附件(用户确认后加 --yes;删完可 list-attachment 复查为空) +dws todo task remove-attachment --task-id --attachment-id --yes --format json + +# 21. 查询子待办 — 从顶层 subTasks[].taskId 取子待办 ID +dws todo task list-sub --task-id --format json ``` ## 上下文传递表 | 操作 | 从返回中提取 | 用于 | |------|-------------|---------------------------------------------| -| `task create` | `todoTaskId` | update/done/get/delete 的 --task-id | -| `task list` | `result[].id` | update/done/get/delete 的 --task-id | -| `task create` | `todoTaskId` | update/done/get/delete/comment 的 --task-id | -| `task list` | `result[].id` | update/done/get/delete/comment/add-executor/remove-executor/add-participant/remove-participant 的 --task-id | -| `task get` | `result.todoDetailModel.subTodos[]` | 获取子待办列表,提取子待办的 `taskId` 用于后续操作 | -| `comment list` | `result[].commentId` | `comment delete` 的 --comment-id | +| `task create` / `task create-sub` | `result.taskId` | update/done/get/delete/comment 的 --task-id | +| `task list` | `result.todoCards[].taskId` | update/done/get/delete/comment/add-executor/remove-executor/add-participant/remove-participant 的 --task-id | +| `task list-sub` | `subTasks[].taskId`(顶层数组,不在 `result` 下) | 子待办的后续操作 --task-id | +| `add-attachment` | `result.attachmentIds[]` | 新上传附件的 attachmentId | +| `list-attachment` | `attachments[].attachmentId`(顶层数组) | `remove-attachment` 的 --attachment-id | +| `comment list` | `result.comments[].id` | `comment delete` 的 --comment-id | ## 注意事项 @@ -398,6 +409,9 @@ dws todo task reset-reminder --task-id --reminder-rules ' 接口不支持游标分页,使用 `--limit` 一次性拉取。 +> ⚠️ **返回字段限制**:`member list` 每条只返回 `name` / `role` / `type` 三个字段,**不含 userId**(服务端不返回)。因此**无法**从 `member list` 拿到 userId 再去串联 `member update` / `member remove`。要对某人改角色 / 移除,需另行拿到其 userId(例如用 `dws contact user search --query "<姓名>"` 按姓名反查)。 + ### 列出知识库节点 ``` Usage: @@ -393,10 +395,13 @@ dws wiki member list --workspace --format json # ── 工作流: 移除知识库成员 ── -# 1. 查看当前成员 +# 1. 查看当前成员(只返回 name/role/type,拿不到 userId) dws wiki member list --workspace --format json -# 2. 移除成员 +# 2. 另行按姓名反查目标成员的 userId(member list 不返回 userId) +dws contact user search --query "<姓名>" --format json + +# 3. 移除成员 dws wiki member remove --workspace --users --format json # ── 工作流: 删除知识库 ── @@ -419,7 +424,7 @@ dws wiki space delete --workspace --format json | `node list` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | | `node search` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | | `node create` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | -| `member list` | `userId` | member update 的 --users / member remove 的 --users | +| `member list` | `name` / `role` / `type`(**不含 userId**)| 仅用于查看成员名单;**无法**从这里取 userId 去串联 member update/remove,需另行按姓名反查 userId(如 `dws contact user search --query "<姓名>"`)| ## 相关产品 diff --git a/skills/mono/references/url-patterns.md b/skills/mono/references/url-patterns.md index 4b206ad9..26cdb302 100644 --- a/skills/mono/references/url-patterns.md +++ b/skills/mono/references/url-patterns.md @@ -136,8 +136,8 @@ dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/ghi789" --format jso | extension / contentType | 读取 | 写入 | 删除 | 导出 | 权限 | 媒体 | |-------------------------|------|------|------|------|------|------| | **adoc**(在线文档) | `doc read` | `doc update` / `doc block update` | ⚠️ `doc delete` | ⚠️ `doc export` (→ docx) | ⚠️ `doc permission *` | ⚠️ `doc media download/insert` | -| **axls**(在线电子表格) | `sheet range read` / `sheet list` | `sheet range write` / `sheet append` | ⚠️ `doc delete`(节点删除) | `sheet submit_export_job` + `sheet query_export_job`(待吴淼 W-01 收敛为单命令 `sheet export`) | ⚠️ `doc permission *`(节点级,跨产品) | 不适用 | -| **able**(在线多维表) | `aitable base get` / `aitable record query` | `aitable record create/update` | ⚠️ `doc delete`(节点删除)或 `aitable base delete --yes` | `aitable export data --output ./x.xlsx` | ⚠️ `doc permission *`(节点级) | `aitable attachment upload-file` | +| **axls**(在线电子表格) | `sheet range read` / `sheet list` | `sheet range update` / `sheet append` | ⚠️ `doc delete`(节点删除) | `sheet export`(单命令一站式:提交→轮询→下载,可选 `--output` 落盘) | ⚠️ `doc permission *`(节点级,跨产品) | 不适用 | +| **able**(在线多维表) | `aitable base get` / `aitable record query` | `aitable record create/update` | ⚠️ `doc delete`(节点删除)或 `aitable base delete --yes` | `aitable export data --scope all --format excel`(取 downloadUrl,`--output` 不落盘) | ⚠️ `doc permission *`(节点级) | `aitable attachment upload` | | **xlsx / xls / xlsm / csv**(本地表格文件) | `doc download` → 本地用 xlsx skill 解析 | 不支持服务端写(先下载改本地再上传) | ⚠️ `doc delete`(节点删除) | 不需要(本身就是 xlsx) | ⚠️ `doc permission *` | 不适用 | | **普通文件** (nodeType=file) | `doc download` | 不支持服务端写 | ⚠️ `doc delete` | 不需要 | ⚠️ `doc permission *` | 不适用 | | **文件夹** (nodeType=folder) | `doc list --folder ` | `doc create --folder ...` | ⚠️ `doc delete` | 不适用 | ⚠️ `doc permission *` | 不适用 | diff --git a/skills/mono/scripts/aitable_export_via_task.py b/skills/mono/scripts/aitable_export_via_task.py index fdb20406..abee2023 100644 --- a/skills/mono/scripts/aitable_export_via_task.py +++ b/skills/mono/scripts/aitable_export_via_task.py @@ -88,10 +88,12 @@ def build_start_args(args: argparse.Namespace) -> list[str]: args.base_id, "--scope", args.scope, - "--export-format", + "--format", args.export_format, - "--timeout-sec", - str(args.timeout_sec), + # CLI 的 --timeout-ms 是单次等待上限(毫秒,最大 30000);脚本自身的 + # --timeout-sec 用于整体轮询/子进程超时,二者语义不同,不能混用。 + "--timeout-ms", + "30000", ] if args.table_id: cmd.extend(["--table-id", args.table_id]) @@ -152,8 +154,8 @@ def main() -> None: args.base_id, "--task-id", task_id, - "--timeout-sec", - str(args.timeout_sec), + "--timeout-ms", + "30000", ], timeout_sec=max(120, args.timeout_sec + 60), ) diff --git a/skills/mono/scripts/aitable_import_via_task.py b/skills/mono/scripts/aitable_import_via_task.py index f3139df2..566be970 100644 --- a/skills/mono/scripts/aitable_import_via_task.py +++ b/skills/mono/scripts/aitable_import_via_task.py @@ -140,8 +140,10 @@ def main() -> None: "data", "--import-id", import_id, - "--timeout-sec", - str(args.timeout_sec), + # import data 的 --timeout 单位是秒、最大 30;脚本的 --timeout-sec + # 是整体子进程预算,不能直接透传,这里用 CLI 允许的最大值。 + "--timeout", + "30", "--format", "json", ], diff --git a/skills/mono/scripts/bulk_add_fields.py b/skills/mono/scripts/bulk_add_fields.py index 98e0a9c7..78c5bdfc 100644 --- a/skills/mono/scripts/bulk_add_fields.py +++ b/skills/mono/scripts/bulk_add_fields.py @@ -29,7 +29,7 @@ JsonData = Union[List[Any], Dict[str, Any]] MAX_FILE_SIZE = 10 * 1024 * 1024 ALLOWED_FILE_EXTENSIONS = ['.json'] -RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$') +RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{6,128}$') ALLOWED_FIELD_TYPES = { 'text', 'number', 'singleSelect', 'multipleSelect', 'date', 'currency', 'user', 'department', 'group', 'progress', 'rating', 'checkbox', diff --git a/skills/mono/scripts/calendar_schedule_meeting.py b/skills/mono/scripts/calendar_schedule_meeting.py index 54dd920d..7de31a3c 100644 --- a/skills/mono/scripts/calendar_schedule_meeting.py +++ b/skills/mono/scripts/calendar_schedule_meeting.py @@ -100,7 +100,11 @@ def main(): event_id = None if not args.dry_run and isinstance(result, dict): - event_id = result.get('eventId') or result.get('id') + # event create 真机返回 {result:{id,...}, success:true},id 在 result 内层, + # 顶层取 id 恒 None 会导致参会人/订房支路被静默跳过。先解包 result 再取。 + inner = result.get('result') if isinstance(result.get('result'), dict) else result + event_id = (inner.get('eventId') or inner.get('id') + or result.get('eventId') or result.get('id')) print(f" ✓ 日程已创建" + (f" (eventId: {event_id})" if event_id else "")) diff --git a/skills/mono/scripts/contact_dept_members.py b/skills/mono/scripts/contact_dept_members.py index 9413656f..f0518de2 100644 --- a/skills/mono/scripts/contact_dept_members.py +++ b/skills/mono/scripts/contact_dept_members.py @@ -8,12 +8,20 @@ """ import sys +import re import json import subprocess import argparse from typing import List, Any, Optional +def strip_highlight(text: str) -> str: + """去除 dept search 返回名称中的 … 高亮标签。""" + if not isinstance(text, str): + return text + return re.sub(r'', '', text) + + def run_dws( args: List[str], dry_run: bool = False, ) -> Optional[Any]: @@ -62,15 +70,26 @@ def main(): print('未找到匹配部门') sys.exit(1) - depts = (dept_data if isinstance(dept_data, list) - else dept_data.get('result', dept_data.get('items', []))) + # dept search 真机返回顶层 deptList;兼容 result 包裹与历史 items/result 键。 + if isinstance(dept_data, list): + depts = dept_data + else: + inner = dept_data.get('result', dept_data) if isinstance(dept_data, dict) else {} + if not isinstance(inner, dict): + inner = dept_data if isinstance(dept_data, dict) else {} + depts = (inner.get('deptList') + or dept_data.get('deptList') + or dept_data.get('items') + or []) if not depts: print('未找到匹配部门') sys.exit(1) for dept in depts: dept_id = dept.get('id') or dept.get('deptId') - dept_name = dept.get('name') or dept.get('deptName', '未知') + dept_name = strip_highlight( + dept.get('name') or dept.get('deptName', '未知') + ) if not dept_id: continue @@ -85,17 +104,29 @@ def main(): print(' 无法获取成员列表') continue - members = (members_data if isinstance(members_data, list) - else members_data.get('result', - members_data.get('userlist', []))) + # list-members 真机返回 deptUserList;兼容 result 包裹与历史 userlist 键。 + if isinstance(members_data, list): + members = members_data + else: + m_inner = (members_data.get('result', members_data) + if isinstance(members_data, dict) else {}) + if not isinstance(m_inner, dict): + m_inner = members_data if isinstance(members_data, dict) else {} + members = (m_inner.get('deptUserList') + or members_data.get('deptUserList') + or members_data.get('userlist') + or []) if not members: print(' (暂无成员)') continue for m in members: - name = m.get('name') or m.get('userName', '未知') - title = m.get('title') or m.get('position', '') - uid = m.get('userId') or m.get('userid', '') + # list-members 真机每项形如 {"userInfo": {"name":..., "userId":...}}, + # 成员字段嵌在 userInfo 下;兼容历史扁平结构。 + info = m.get('userInfo', m) + name = info.get('name') or info.get('userName', '未知') + title = info.get('title') or info.get('position', '') + uid = info.get('userId') or info.get('userid', '') line = f" 👤 {name}" if title: line += f" ({title})" diff --git a/skills/mono/scripts/drive_tree_list.py b/skills/mono/scripts/drive_tree_list.py index a7e3439b..95ccb1c2 100644 --- a/skills/mono/scripts/drive_tree_list.py +++ b/skills/mono/scripts/drive_tree_list.py @@ -3,10 +3,16 @@ 递归列出钉盘目录树结构(可指定深度) 用法: - python drive_tree_list.py # 列出根目录 - python drive_tree_list.py --depth 2 # 递归 2 层 - python drive_tree_list.py --parent-id # 指定目录 + python drive_tree_list.py # 列出根目录 + python drive_tree_list.py --depth 2 # 递归 2 层 + python drive_tree_list.py --folder # 指定目录 (传 drive list 返回的 fileId) python drive_tree_list.py --dry-run + +说明: + `dws drive list` 返回的每个 item 有两个 ID: + - dentryId:纯数字串,`--folder` 不接受,别用它递归; + - fileId:字母数字串,即 CLI 所称 dentryUuid,`--folder` 只认它。 + 递归子目录必须用 fileId 作为 `--folder` 的值。 """ import sys @@ -38,13 +44,13 @@ def run_dws( def list_dir( - parent_id: str = '', dry_run: bool = False, + folder: str = '', dry_run: bool = False, ) -> list: cmd_args = [ - 'drive', 'list', '--max', '50', '--format', 'json', + 'drive', 'list', '--limit', '50', '--format', 'json', ] - if parent_id: - cmd_args.extend(['--parent-id', parent_id]) + if folder: + cmd_args.extend(['--folder', folder]) data = run_dws(cmd_args, dry_run=dry_run) if not data: return [] @@ -69,7 +75,7 @@ def print_tree( name = item.get('name') or item.get('fileName', '?') item_type = item.get('type') or item.get('dentryType', '') is_dir = str(item_type).lower() in ( - 'folder', 'directory', '1', 'FOLDER' + 'folder', 'directory', '1', ) icon = '📁' if is_dir else '📄' size_str = '' @@ -87,10 +93,10 @@ def print_tree( if is_dir and depth < max_depth: child_prefix = prefix + (' ' if is_last else '│ ') - dentry_id = (item.get('dentryUuid') - or item.get('id', '')) - if dentry_id: - children = list_dir(dentry_id, dry_run=dry_run) + # `--folder` 只认 fileId (dentryUuid),不认纯数字 dentryId + folder_id = item.get('fileId', '') + if folder_id: + children = list_dir(folder_id, dry_run=dry_run) print_tree( children, depth + 1, max_depth, child_prefix, dry_run, @@ -102,7 +108,8 @@ def main(): description='递归列出钉盘目录树' ) parser.add_argument( - '--parent-id', default='', help='起始目录 ID' + '--folder', default='', + help='起始目录 ID (传 drive list 返回的 fileId)', ) parser.add_argument( '--depth', type=int, default=1, @@ -112,10 +119,10 @@ def main(): args = parser.parse_args() args.depth = min(args.depth, 5) - root_name = args.parent_id or '我的文件' + root_name = args.folder or '我的文件' print(f"📁 {root_name}") - items = list_dir(args.parent_id, dry_run=args.dry_run) + items = list_dir(args.folder, dry_run=args.dry_run) if args.dry_run: return if not items: diff --git a/skills/mono/scripts/import_records.py b/skills/mono/scripts/import_records.py index 020b3cad..a76f78d2 100644 --- a/skills/mono/scripts/import_records.py +++ b/skills/mono/scripts/import_records.py @@ -28,7 +28,7 @@ RecordDict = Dict[str, str] MAX_FILE_SIZE = 50 * 1024 * 1024 ALLOWED_CSV_EXTENSIONS = ['.csv'] ALLOWED_JSON_EXTENSIONS = ['.json'] -RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$') +RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{6,128}$') MAX_RECORDS_PER_BATCH = 100 DEFAULT_BATCH_SIZE = 50 diff --git a/skills/mono/scripts/mail_unread_summary.py b/skills/mono/scripts/mail_unread_summary.py index 09d19624..5588db97 100644 --- a/skills/mono/scripts/mail_unread_summary.py +++ b/skills/mono/scripts/mail_unread_summary.py @@ -102,7 +102,7 @@ def main(): 'mail', 'message', 'search', '--email', email or '', '--query', kql, - '--size', str(args.size), + '--limit', str(args.size), '--format', 'json', ], dry_run=args.dry_run) diff --git a/skills/mono/scripts/report_inbox_today.py b/skills/mono/scripts/report_inbox_today.py index 05f59859..edda22b2 100644 --- a/skills/mono/scripts/report_inbox_today.py +++ b/skills/mono/scripts/report_inbox_today.py @@ -2,9 +2,14 @@ """ 查看今天收到的日志列表及详情 +基于 `dws report inbox list` + `dws report entry get`(旧的 report list / report detail +已废弃)。inbox list 返回的 result[] 使用中文展示键:日期 / 标题 / 发送人 / 状态 / 钉钉链接; +reportId 不在 result[] 里,只在 _internalDetailCommands[].command 中,按页与 result[] 同序对应。 + 用法: python report_inbox_today.py - python report_inbox_today.py --days 3 # 最近 3 天 + python report_inbox_today.py --days 3 # 最近 3 天 + python report_inbox_today.py --detail # 额外拉取每条正文 (entry get) python report_inbox_today.py --dry-run """ @@ -13,7 +18,7 @@ import json import subprocess import argparse from datetime import datetime, timedelta -from typing import List, Any, Optional +from typing import List, Any, Optional, Tuple def run_dws( @@ -37,77 +42,118 @@ def run_dws( return None -def to_iso(dt: datetime) -> str: - return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00') +def iso_start(dt: datetime) -> str: + return dt.strftime('%Y-%m-%dT00:00:00+08:00') + + +def iso_end(dt: datetime) -> str: + return dt.strftime('%Y-%m-%dT23:59:59+08:00') + + +def report_id_from_command(cmd: str) -> str: + parts = cmd.split() + if '--report-id' in parts: + i = parts.index('--report-id') + if i + 1 < len(parts): + return parts[i + 1] + return '' + + +def fetch_inbox( + start: str, end: str, dry_run: bool, +) -> List[Tuple[dict, str]]: + """按 cursor 翻页拉全 inbox;返回 (result_item, reportId) 列表。""" + pairs: List[Tuple[dict, str]] = [] + cursor = 0 + while True: + data = run_dws([ + 'report', 'inbox', 'list', + '--start', start, + '--end', end, + '--cursor', str(cursor), + '--size', '20', + '--format', 'json', + ], dry_run=dry_run) + if dry_run or not isinstance(data, dict): + return pairs + items = data.get('result') or [] + cmds = data.get('_internalDetailCommands') or [] + for idx, item in enumerate(items): + rid = '' + if idx < len(cmds): + rid = report_id_from_command( + cmds[idx].get('command', '') + ) + pairs.append((item, rid)) + if data.get('hasMore') and data.get('nextCursor') is not None: + cursor = data['nextCursor'] + else: + break + return pairs + + +def print_detail(rid: str) -> None: + detail = run_dws([ + 'report', 'entry', 'get', + '--report-id', rid, '--format', 'json', + ]) + if not isinstance(detail, dict): + return + result = detail.get('result') + if not isinstance(result, dict): + return + for c in (result.get('report_content') or [])[:3]: + key = c.get('key', '') + val = c.get('value', '') + if key and val: + print(f" {key}: {str(val).strip()[:60]}") def main(): - parser = argparse.ArgumentParser( - description='查看收到的日志' + parser = argparse.ArgumentParser(description='查看收到的日志') + parser.add_argument( + '--days', type=int, default=1, help='查询天数 (默认 1,即今天)' ) parser.add_argument( - '--days', type=int, default=1, help='查询天数 (默认 1)' + '--detail', action='store_true', help='额外拉取每条正文' ) parser.add_argument('--dry-run', action='store_true') args = parser.parse_args() now = datetime.now() - start = now - timedelta(days=args.days) - start = start.replace(hour=0, minute=0, second=0) + start_dt = now - timedelta(days=max(args.days - 1, 0)) + start = iso_start(start_dt) + end = iso_end(now) label = '今天' if args.days == 1 else f'最近 {args.days} 天' - print(f'📓 查看{label}收到的日志...\n') - - data = run_dws([ - 'report', 'list', - '--start', to_iso(start), - '--end', to_iso(now), - '--cursor', '0', - '--size', '20', - '--format', 'json', - ], dry_run=args.dry_run) + print(f'查看{label}收到的日志...\n') + pairs = fetch_inbox(start, end, args.dry_run) if args.dry_run: return - if not data: - print('未查到日志') + if not pairs: + print(' 暂无收到的日志') return - reports = (data if isinstance(data, list) - else data.get('result', data.get('reports', []))) - if not reports: - print(' ✅ 暂无收到的日志') - return - - print(f"📓 {label}日志 ({len(reports)} 条)") + print(f"{label}日志 ({len(pairs)} 条)") print('=' * 50) - for r in reports: - rid = r.get('reportId') or r.get('id', '') - creator = r.get('creatorName') or r.get('creator', '未知') - template = r.get('templateName') or r.get('template', '') - create_time = r.get('createTime', '') - if isinstance(create_time, (int, float)): - create_time = datetime.fromtimestamp( - create_time / 1000 - ).strftime('%Y-%m-%d %H:%M') + for item, rid in pairs: + title = item.get('标题') or '日志' + sender = item.get('发送人') or '未知' + date = item.get('日期') or '' + status = item.get('状态') or '' + link = item.get('钉钉链接') or '' - print(f"\n 📝 {template or '日志'} - {creator}") - print(f" 时间: {create_time}") - print(f" ID: {rid}") + print(f"\n {title} - {sender}") + print(f" 时间: {date}") + if status: + print(f" 状态: {status}") + if link: + print(f" 链接: {link}") - if rid: - detail = run_dws([ - 'report', 'detail', - '--report-id', rid, '--format', 'json', - ]) - if detail and isinstance(detail, dict): - contents = detail.get('contents', []) - for c in contents[:3]: - key = c.get('key') or c.get('title', '') - val = c.get('value') or c.get('content', '') - if key and val: - print(f" {key}: {str(val)[:60]}") + if args.detail and rid: + print_detail(rid) if __name__ == '__main__': diff --git a/skills/mono/scripts/report_received_today.py b/skills/mono/scripts/report_received_today.py index a08e8777..7fd87617 100644 --- a/skills/mono/scripts/report_received_today.py +++ b/skills/mono/scripts/report_received_today.py @@ -2,9 +2,14 @@ """ 查看今天收到的日志列表及详情 +基于 `dws report inbox list` + `dws report entry get`(旧的 report list / report detail +已废弃)。inbox list 返回的 result[] 使用中文展示键:日期 / 标题 / 发送人 / 状态 / 钉钉链接; +reportId 不在 result[] 里,只在 _internalDetailCommands[].command 中,按页与 result[] 同序对应。 + 用法: python report_received_today.py - python report_received_today.py --days 3 # 最近 3 天 + python report_received_today.py --days 3 # 最近 3 天 + python report_received_today.py --detail # 额外拉取每条正文 (entry get) python report_received_today.py --dry-run """ @@ -13,7 +18,7 @@ import json import subprocess import argparse from datetime import datetime, timedelta -from typing import List, Any, Optional +from typing import List, Any, Optional, Tuple def run_dws( @@ -37,91 +42,118 @@ def run_dws( return None -def to_iso(dt: datetime) -> str: - return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00') +def iso_start(dt: datetime) -> str: + return dt.strftime('%Y-%m-%dT00:00:00+08:00') + + +def iso_end(dt: datetime) -> str: + return dt.strftime('%Y-%m-%dT23:59:59+08:00') + + +def report_id_from_command(cmd: str) -> str: + parts = cmd.split() + if '--report-id' in parts: + i = parts.index('--report-id') + if i + 1 < len(parts): + return parts[i + 1] + return '' + + +def fetch_inbox( + start: str, end: str, dry_run: bool, +) -> List[Tuple[dict, str]]: + """按 cursor 翻页拉全 inbox;返回 (result_item, reportId) 列表。""" + pairs: List[Tuple[dict, str]] = [] + cursor = 0 + while True: + data = run_dws([ + 'report', 'inbox', 'list', + '--start', start, + '--end', end, + '--cursor', str(cursor), + '--size', '20', + '--format', 'json', + ], dry_run=dry_run) + if dry_run or not isinstance(data, dict): + return pairs + items = data.get('result') or [] + cmds = data.get('_internalDetailCommands') or [] + for idx, item in enumerate(items): + rid = '' + if idx < len(cmds): + rid = report_id_from_command( + cmds[idx].get('command', '') + ) + pairs.append((item, rid)) + if data.get('hasMore') and data.get('nextCursor') is not None: + cursor = data['nextCursor'] + else: + break + return pairs + + +def print_detail(rid: str) -> None: + detail = run_dws([ + 'report', 'entry', 'get', + '--report-id', rid, '--format', 'json', + ]) + if not isinstance(detail, dict): + return + result = detail.get('result') + if not isinstance(result, dict): + return + for c in (result.get('report_content') or [])[:3]: + key = c.get('key', '') + val = c.get('value', '') + if key and val: + print(f" {key}: {str(val).strip()[:60]}") def main(): - parser = argparse.ArgumentParser( - description='查看收到的日志' + parser = argparse.ArgumentParser(description='查看收到的日志') + parser.add_argument( + '--days', type=int, default=1, help='查询天数 (默认 1,即今天)' ) parser.add_argument( - '--days', type=int, default=1, help='查询天数 (默认 1)' + '--detail', action='store_true', help='额外拉取每条正文' ) parser.add_argument('--dry-run', action='store_true') args = parser.parse_args() now = datetime.now() - start = now - timedelta(days=args.days) - start = start.replace(hour=0, minute=0, second=0) + start_dt = now - timedelta(days=max(args.days - 1, 0)) + start = iso_start(start_dt) + end = iso_end(now) label = '今天' if args.days == 1 else f'最近 {args.days} 天' - print(f'📓 查看{label}收到的日志...\n') - - data = run_dws([ - 'report', 'list', - '--start', to_iso(start), - '--end', to_iso(now), - '--cursor', '0', - '--size', '20', - '--format', 'json', - ], dry_run=args.dry_run) + print(f'查看{label}收到的日志...\n') + pairs = fetch_inbox(start, end, args.dry_run) if args.dry_run: return - if not data: - print('未查到日志') + if not pairs: + print(' 暂无收到的日志') return - if isinstance(data, list): - reports = data - elif isinstance(data, dict): - inner = data.get('result', data) - if isinstance(inner, dict): - reports = inner.get('report_list', - inner.get('reports', [])) - elif isinstance(inner, list): - reports = inner - else: - reports = [] - else: - reports = [] - if not reports: - print(' ✅ 暂无收到的日志') - return - - print(f"📓 {label}日志 ({len(reports)} 条)") + print(f"{label}日志 ({len(pairs)} 条)") print('=' * 50) - for r in reports: - if not isinstance(r, dict): - print(f"\n 📝 {r}") - continue - rid = r.get('reportId') or r.get('id', '') - creator = r.get('creatorName') or r.get('creator', '未知') - template = r.get('templateName') or r.get('template', '') - create_time = r.get('createTime', '') - if isinstance(create_time, (int, float)): - create_time = datetime.fromtimestamp( - create_time / 1000 - ).strftime('%Y-%m-%d %H:%M') + for item, rid in pairs: + title = item.get('标题') or '日志' + sender = item.get('发送人') or '未知' + date = item.get('日期') or '' + status = item.get('状态') or '' + link = item.get('钉钉链接') or '' - print(f"\n 📝 {template or '日志'} - {creator}") - print(f" 时间: {create_time}") - print(f" ID: {rid}") + print(f"\n {title} - {sender}") + print(f" 时间: {date}") + if status: + print(f" 状态: {status}") + if link: + print(f" 链接: {link}") - if rid: - detail = run_dws([ - 'report', 'detail', - '--report-id', rid, '--format', 'json', - ]) - if detail and isinstance(detail, dict): - contents = detail.get('contents', []) - for c in contents[:3]: - key = c.get('key') or c.get('title', '') - val = c.get('value') or c.get('content', '') - if key and val: - print(f" {key}: {str(val)[:60]}") + if args.detail and rid: + print_detail(rid) if __name__ == '__main__': diff --git a/skills/multi/dingtalk-aitable/references/06-data-analytics.md b/skills/multi/dingtalk-aitable/references/06-data-analytics.md index 6b089723..3fd9b0bc 100644 --- a/skills/multi/dingtalk-aitable/references/06-data-analytics.md +++ b/skills/multi/dingtalk-aitable/references/06-data-analytics.md @@ -6,7 +6,7 @@ |--------|-------------------| | read-aitable | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`
2. `aitable field get --base-id --table-id ` → 取 `fieldId`
3. **取记录(按场景分流)**:
  • 数据统计/分析/全量汇总 → `aitable record query --base-id --table-id --all`(自动翻页,**禁止凭单页数据做统计**)
  • 大表保险 → 加 `--page-limit 100`(默认 50 页/5000 条,0 = 无限制)
  • 单纯预览前几条 → `aitable record query --base-id --table-id --limit 30`(不加 --all)
  • 筛选时 `--filters` 格式见 [aitable-filter-sort.md](../products/aitable/aitable-filter-sort.md)
4. **检查输出契约**:`hasMore=true` 时数据被截断,必须用 `--cursor ` 续拉;`partial=true` 时表示中途某页失败(保留已拉数据,可重试)
5. 总结数据 | | generate-data-report | 1. 同 read-aitable 步骤 1-3(**必须用 --all 防漏数据**)
2. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行 → 补充背景
3. `doc create --name "<报告名>" --content "<分析报告>"` | -| create-aitable-record | **写入路径分流**(关键决策):
  • **追加到已有表**(场景:把这批数据加到「成员表」)→ `python scripts/import_records.py data.csv\|data.json [batch_size]`(走 `record create`,需要列名匹配字段名)
  • **新建表**(场景:把这个 Excel 变成多维表)→ `python scripts/aitable_import_via_task.py ` 或 `dws aitable import upload --base-id --file ./x.xlsx` + `dws aitable import data --import-id `(CLI 已内置 OSS PUT 清空 Content-Type + 同步轮询,**禁止自己写 PUT**)
  • **单条/少量手动**:1. `aitable base search` → `baseId`/`tableId`;2. `aitable field get` → `fieldId` 与类型;3. `aitable record create --records '[{"cells":{"":"值"}}]'` | +| create-aitable-record | **写入路径分流**(关键决策):
  • **追加到已有表**(场景:把这批数据加到「成员表」)→ `python scripts/import_records.py data.csv\|data.json [batch_size]`(走 `record create`,需要列名匹配字段名)
  • **新建表**(场景:把这个 Excel 变成多维表)→ `python scripts/aitable_import_via_task.py `(脚本内置 prepare→OSS PUT→import data 全流程和正确的头处理,**禁止自己写 PUT**)。注意 `aitable import upload` 没有 `--file` flag、也不代做 PUT,只准备导入;能一站式完成的是上面的脚本
  • **单条/少量手动**:1. `aitable base search` → `baseId`/`tableId`;2. `aitable field get` → `fieldId` 与类型;3. `aitable record create --records '[{"cells":{"":"值"}}]'` | | update-aitable-record | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`
2. **取目标 record**:
  • 已知少量 recordId → `aitable record query --record-ids `
  • 按条件批量改 → `aitable record query --base-id --table-id --filters '' --all`(**用 --all 防止漏改**)
3. **先展示让用户确认要改的 record 列表**
4. `aitable record update --base-id --table-id --records '[{"recordId":"","cells":{...}}]'`(单次 ≤30 条) | | search-aitable-template | 1. `aitable template search --query "<关键词>"` → 取 `templateId`
2. 用户选定
3. `aitable base create --name "<表格名>" --template-id ` → 取 `baseId` | | export-aitable-to-xlsx | 1. `aitable base search --query "<表格名>"` → 取 `baseId`
2. **按场景选 scope**:
  • 全表+附件 → `aitable export data --base-id --scope all --export-format excel_and_attachment --output ./.xlsx`
  • 单表(仅 xlsx)→ `--scope table --table-id --export-format excel`
  • 单视图 → `--scope view --table-id --view-id `
3. CLI 内置渐进式退避轮询 + 自动落盘,**不要自己写 GET downloadUrl**
4. 大表超时(默认 5 分钟):加 `--timeout-sec 900` 或拿到 `taskId` 后 `aitable export data --task-id --output ./.xlsx` 续等
5. 与悟空脚本路径并存:复杂场景(多 base 批量 / 按视图组合)请用 `python scripts/aitable_export_via_task.py --scope all\|table\|view` | diff --git a/skills/multi/dingtalk-aitable/references/aitable.md b/skills/multi/dingtalk-aitable/references/aitable.md index 85e759fd..ac8859ba 100644 --- a/skills/multi/dingtalk-aitable/references/aitable.md +++ b/skills/multi/dingtalk-aitable/references/aitable.md @@ -19,8 +19,10 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `base list` | 列出最近访问的 Base | — | 仅返回最近访问过的,优先用 `base search` | -| `base search` | 按名称搜索 Base | `--query` | 关键词 ≥2 字符 | +| `base search` | 按名称搜索 Base(别名 `aitable search`) | — | `--query` help 标必填但实际可省略:不传时返回最近访问的 Base 列表。`--keyword` 是 `--query` 的隐藏别名,同义 | | `base get` | 获取 Base 信息(含 tables 列表) | `--base-id` | 用户给 URL 时提取末尾 ID | +| `base copy` | 复制整个 Base 到目标文件夹 | `--base-id` `--target-folder-id` | 默认全量复制;`--only-struct` 仅复制结构不含数据 | +| `base get-primary-doc-id` | 获取某记录的主键文档 ID | `--base-id` `--table-id` `--record-id` | 等价 `record primary-doc-get` 的取 ID 视角 | | `base create` | 创建 Base | `--name` | 创建后直接用返回的 baseId | | `base update` | 更新 Base 名称 | `--base-id` `--name` | — | | `base delete` | 删除 Base | `--base-id` | 不可逆 | @@ -30,7 +32,8 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `table get` | 获取表结构(字段+视图目录) | `--base-id` | 不传 `--table-ids` 返回全部表 | -| `table create` | 创建数据表 | `--base-id` `--name` `--fields` | fields 为 JSON 数组,至少 1 个 | +| `table list` | 获取数据表(`table get` 的别名) | `--base-id` | 与 `table get` 等价 | +| `table create` | 创建数据表 | `--base-id` `--name` | `--fields` 为 JSON 数组;**可传空数组 `[]`**(默认值即 `[]`),此时服务端自动补一个名为"标题"的 primaryDoc 首列;单次最多 15 个字段 | | `table update` | 重命名表 | `--base-id` `--table-id` `--name` | — | | `table delete` | 删除表 | `--base-id` `--table-id` | 不可逆 | @@ -39,19 +42,29 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `field get` | 获取字段完整配置 | `--base-id` `--table-id` | 按需展开少量字段 | +| `field list` | 获取字段信息(`field get` 的别名) | `--base-id` `--table-id` | 与 `field get` 等价 | | `field create` | 创建字段 | `--base-id` `--table-id` + (`--name --type` 或 `--fields`) | 支持单字段/批量模式 | | `field update` | 更新字段名/配置 | `--base-id` `--table-id` `--field-id` | 不可变更字段类型 | | `field delete` | 删除字段 | `--base-id` `--table-id` `--field-id` | 不可逆 | +| `field search-options` | 搜索单选/多选字段的选项 | `--base-id` `--table-id` `--field-id` | 仅 singleSelect/multipleSelect;`--keyword` 模糊过滤,不传返回全部 | ### record (记录管理) | 命令 | 用途 | 必读 reference | 路由提醒 | |------|------|----------------|----------| -| `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference | +| `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference;`--query`(隐藏别名 `--keyword`)全文搜索 | +| `record list` | 获取记录(`record query` 的别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 与 `record query` 等价 | | `record get` | 按 ID 取记录(`record query --record-ids` 的窄别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 已知 recordId 时首选;必填 `--record-ids`(单次最多 100 条);未暴露 filters/sort/query/cursor/limit | +| `record query-empty` | 查询完全没填用户字段的空行 | — | `--base-id` `--table-id`;`--limit` 扫描预算 [1,100],`--cursor` 翻页 | | `record create` | 新增记录 | [aitable-record-create.md](./aitable/aitable-record-create.md) | cells key 必须是 fieldId 不是字段名;单次最多 100 条 | | `record update` | 更新记录 | [aitable-record-update.md](./aitable/aitable-record-update.md) | 需先 query 拿 recordId;只传需改字段;**没有** `--record-id` `--cells` flag | +| `record batch-update` | 把同一份 cells 批量应用到多条记录 | [aitable-record-update.md](./aitable/aitable-record-update.md) | `--record-ids`(≤100)+ `--cells` 共享 patch | +| `record upsert` | 批量创建或更新(有 recordId 走更新,无则创建) | [aitable-record-upsert.md](./aitable/aitable-record-upsert.md) | `--records`/`--records-file`;单次最多 100 条 | | `record delete` | 删除记录 | [aitable-record-delete.md](./aitable/aitable-record-delete.md) | 不可逆,需先 query 确认 | +| `record share-url` | 批量获取记录分享链接 | [aitable-record-share.md](./aitable/aitable-record-share.md) | `--record-ids`(逗号分隔,单次最多 20 条) | +| `record history-list` | 查询单条记录的变更历史 | [aitable-record-history.md](./aitable/aitable-record-history.md) | `--record-id` 单条;`--offset`/`--limit`(≤50) | +| `record primary-doc-get` | 查询记录的主键文档 nodeId | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 无文档时返回 `no record` 错误 | +| `record primary-doc-create` | 为记录创建主键文档 | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 幂等;`--field-id` 须 primaryDoc 类型 | ### view (视图管理) @@ -73,6 +86,21 @@ > > 不支持 `formInfo`、`requiredFields`、`conditionalRules` 等 FormDesigner 高级配置,这些 key 会被服务端忽略。 +### form (表单管理) → 详见 [aitable-form.md](./aitable/aitable-form.md) + +| 命令 | 用途 | 必填参数 | 路由提醒 | +|------|------|----------|----------| +| `form list` | 列出表单视图 | `--base-id` `--table-id` | 每条含 viewId/name;新建表单无 title 且 createdAt=0,改过后才有 | +| `form get` | 按 viewId 取单个表单详情 | `--base-id` `--table-id` `--view-id` | 客户端按 viewId 过滤,`data` 即该表单对象;viewId 不存在报 not found | +| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | 等价 `view create --view-type FormDesigner` | +| `form update` | 更新表单配置 | `--base-id` `--table-id` `--view-id` | title/name/description 至少一项 | +| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` | 不可逆 | +| `form field list/update/hide` | 表单字段管理 | — | 详情见子文档 | +| `form questions create/delete` | 题目管理(=field create/delete) | — | 详情见子文档 | +| `form share get/update` | 表单分享配置 | — | 详情见子文档 | + +> **创建表单**有两种等价方式:`form create --name "..."`(推荐)或 `view create --view-type FormDesigner --name "..."`。 + ### dashboard & chart → 详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md) | 命令 | 用途 | @@ -130,6 +158,8 @@ - 修改/更新 → `record update`(读 [aitable-record-update.md](./aitable/aitable-record-update.md)) - 删除 → `record delete` +用户说"表单/问卷/form/收集信息" → 读 [aitable-form.md](./aitable/aitable-form.md) + 用户说"筛选/过滤/filter" → 读 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md) 用户说"统计/分析/聚合/TOP N/全量" → 读 [aitable-data-analysis-sop.md](./aitable/aitable-data-analysis-sop.md) diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md index 1648fc87..48ae3306 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md @@ -38,8 +38,10 @@ dws aitable record create --base-id --table-id \ dws aitable attachment upload --base-id --file-name report.pdf --size 204800 --format json # → 返回 uploadUrl、fileToken -# 2. PUT 上传(Content-Type 留空) -curl -X PUT "" -H "Content-Type:" --data-binary @report.pdf +# 2. PUT 上传(Content-Type 必须与文件类型一致,不能留空) +# 留空或用 curl 默认的 application/x-www-form-urlencoded 都会被 OSS 拒为 403 +curl -X PUT "" -H "Content-Type: application/pdf" --data-binary @report.pdf +# 例:.txt → text/plain,.png → image/png,.xlsx → application/vnd.openxmlformats-officedocument.spreadsheetml.sheet # 3. 写入记录 dws aitable record update --base-id --table-id \ diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md index ec96fb4f..f904158a 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md @@ -15,8 +15,10 @@ dws aitable chart get --base-id --dashboard-id --chart- ## 要点 - `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用 -- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错 -- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断 +- `dashboard share get` 可能返回 `404`(`retryable:true`)——从未分享、甚至刚开启分享后立即查都可能 404;`data` 为 `{}`。按可重试错误处理,不要误判为参数拼错。它有时也会返回 `success + data`(如关闭分享之后),行为不稳定,别用它当"是否已分享"的唯一判据。 +- `chart share get` 稳定返回 `success + data`(含 `enabled` 等),可用于分享状态判断;从未分享时 `enabled=false`,不会 404。 +- ⚠️ **`dashboard share update` 开启 ORG 分享后返回的 `shareType` 是 `"[1]"`(未映射回 `ORG`,服务端已知问题)**;`chart share update --share-type ORG` 则正确返回 `shareType="ORG"`。判断 dashboard 是否 ORG 分享时对 `"[1]"` 做兼容。 +- `chart share update` / `dashboard share update` 的 `--enabled` 是字符串 flag:`--enabled false`(空格)和 `--enabled=false` 都能正确关闭分享。 ## dashboard 子命令 @@ -25,18 +27,25 @@ dws aitable chart get --base-id --dashboard-id --chart- | `dashboard get` | 获取仪表盘详情(含 charts 列表) | `--base-id` `--dashboard-id` | — | | `dashboard create` | 创建仪表盘 | `--base-id` + (`--config` 或 `--name`) | `--name` 简化版创建空看板;`--config` 传完整 JSON | | `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config` 或 `--name`) | `--name` 仅改名;`--config` 更新完整配置 | +| `dashboard arrange` | 自动重排仪表盘图表布局 | `--base-id` `--dashboard-id` | 让服务端重新排布 charts 位置 | | `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` `--yes` | — | | `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 | +| `dashboard share get` | 获取仪表盘分享配置 | `--base-id` `--dashboard-id` | 可能 404,见上方要点 | +| `dashboard share update` | 更新仪表盘分享配置 | `--base-id` `--dashboard-id` `--enabled` | `--enabled true` 开启(配 `--share-type PUBLIC/ORG`)/ `--enabled false` 关闭;ORG 回显 `shareType="[1]"` | ## chart 子命令 | 命令 | 用途 | 必填参数 | |------|------|----------| | `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` | -| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` | +| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` | | `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` | | `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` | | `chart widgets-example` | 查看图表 widgets 配置模板 | 无 | +| `chart share get` | 获取图表分享配置 | `--base-id` `--dashboard-id` `--chart-id` | +| `chart share update` | 更新图表分享配置(`--enabled true/false`,开启配 `--share-type PUBLIC/ORG`) | `--base-id` `--dashboard-id` `--chart-id` `--enabled` | + +> `chart create` 的 `--layout` 是**必填**(12 列网格布局,如 `{"x":0,"y":0,"w":6,"h":4}`);不传本地校验直接拒。`chart update` 的 `--config` 也**必填**——即便只想改 layout,也要带完整 config,否则服务端拒绝。 ## 配置获取流程 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md index 05e83466..fd4ea89b 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md @@ -118,23 +118,25 @@ dws aitable import data --import-id --table-id --format js 用户给文件让导入 AI 表格时,路径选择决定成败: +> ⚠️ **`import upload` 没有 `--file` flag**(传了会报 unknown flag)。它只申请上传凭证,必填 `--file-name` + `--file-size`,拿到 `uploadUrl` 后要**自己 curl PUT 上传文件**(Content-Type 留空,见上文三步流程),再 `import data`。想省事直接用 `aitable_import_via_task.py` 脚本,它把这三步包好了。 + | 用户原话 | 链路 | 命令 / 脚本 | 行为 | |---------|------|------------|------| -| "把这个 Excel 导入到 AI 表格"(无指定目标表) | **文件导入任务** | `python scripts/aitable_import_via_task.py ` 或 `dws aitable import upload --base-id B --file ./x.xlsx` + `dws aitable import data --import-id ` | 服务端解析文件,**新建数据表**,自动识别表头 | +| "把这个 Excel 导入到 AI 表格"(无指定目标表) | **文件导入任务** | `python scripts/aitable_import_via_task.py `(推荐)或手动三步 `import upload --file-name x.xlsx --file-size <字节>` → curl PUT → `import data --import-id ` | 服务端解析文件,**新建数据表**,自动识别表头 | | "把这个 Excel 导入新表 / 自动建表" | 同上 | 同上 | 同上 | | "把这批记录追加到已有的『成员表』里" | **记录批量写入** | `python scripts/import_records.py ` | 走 `record create`,**写入已有 tableId**,需要字段名匹配 | -| "Excel 列名和表字段对不上但要追加" | 文件导入 + 追加 + 字段映射 | `dws aitable import upload --base-id B --file ./x.xlsx` → `dws aitable import data --import-id --table-id --field-mapping '{"目标":"源"}'` | 服务端按映射追加 | +| "Excel 列名和表字段对不上但要追加" | 文件导入 + 追加 + 字段映射 | 三步导入后 `import data --import-id --table-id --field-mapping '{"目标":"源"}'` | 服务端按映射追加 | ## 大表 / 长任务超时续等 -默认整体轮询超时 5 分钟。大表导出/导入超时后命令会返回 `taskId` / `importId`,用同命令带 ID 续等: +单次等待窗口很短:`export data` 只有 `--timeout-ms`(默认且**上限 30000 = 30 秒**,没有 `--timeout-sec`,传了会报 unknown flag);`import data` 用 `--timeout`(秒,默认且推荐最大值 30)。窗口内没跑完,命令会返回 `taskId` / `importId`,用同命令带 ID 反复续等即可: ```bash -# 续等导出 -dws aitable export data --base-id --task-id --output ./out.xlsx +# 续等导出:拿到 downloadUrl 后再 curl 下载(见下方警告) +dws aitable export data --base-id --task-id --timeout-ms 30000 # 续等导入 dws aitable import data --import-id ``` -或一次性把 timeout 提到 15 分钟:`--timeout-sec 900`。 +> ⚠️ **`--output` 不会保存导出的 xlsx**:`--output` 是隐藏的全局 flag,作用是把命令的 **JSON 输出**写到文件,实测只生成一个 0 字节文件,不会下载导出内容。正确做法是从 `export data` 返回里取 `downloadUrl`,再 `curl -L "" -o out.xlsx` 下载。想省事直接用 `aitable_export_via_task.py` 脚本(它负责轮询 + 下载)。 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-form.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-form.md index 2b126d8f..2aaedba6 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-form.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-form.md @@ -50,8 +50,8 @@ dws aitable form share get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ | 命令 | 用途 | 必填参数 | 说明 | |------|------|----------|------| -| `form list` | 列出表单视图 | `--base-id` `--table-id` | 返回 viewId/name/title/createdAt | -| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 内部基于 list_form_views 过滤 | +| `form list` | 列出表单视图 | `--base-id` `--table-id` | 每条返回 viewId/name;新建表单**无 title 且 createdAt=0**,改过(form update)后才出现 title 和真实 createdAt | +| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 客户端按 viewId 过滤后 `data` **即该表单对象**(不是 formViews 数组);viewId 不存在返回 `form view ... not found` 错误 | | `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | viewType=FormDesigner | | `form update` | 更新表单 | `--base-id` `--table-id` `--view-id` | `--title`/`--name`(等价)和 `--description` 至少传一项;同时传 title/name 时 title 优先 | | `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` `--yes` | 不可逆 | @@ -115,6 +115,6 @@ dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VI ## 返回结构补充 -- `form list` 返回 `data.formViews[]`,**每条仅含** `viewId/name/title/createdAt`;`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。 -- `form get` 返回结构与 `form list` 完全一致(`data.formViews[]`),仅含一条记录(与请求 viewId 一致)。Agent 提取时仍走 `data.formViews[0]`。 +- `form list` 返回 `data.formViews[]`,**每条含** `viewId/name`(+ `createdAt`);**新建表单没有 `title` 字段且 `createdAt=0`**,只有在 `form update` 碰过之后才会出现 `title` 和真实 `createdAt`。`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。 +- `form get` 的 `data` **就是命中的那一条表单对象**(如 `{viewId, name, createdAt, title?, shareFormUuid?}`),不是 `formViews` 数组。Agent 直接读 `data.viewId` / `data.name` 即可,**不要**再取 `data.formViews[0]`。服务端的 viewIds 过滤参数当前不生效,CLI 在客户端按 viewId 精确筛出单条;传了不存在的 viewId 会返回 `form view not found in table` 错误。 - `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`。 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-primary-doc.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-primary-doc.md index eefda277..8ed2198d 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-primary-doc.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-primary-doc.md @@ -17,7 +17,9 @@ dws aitable record primary-doc-get --base-id BASE_ID --table-id TABLE_ID --recor - `--table-id`(必填):Table ID - `--record-id`(必填):Record ID -**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。若该记录尚未创建主键文档,`nodeId` 为 null。 +**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。 + +> **该记录尚未创建主键文档时**:不会返回 `nodeId: null`,而是 `status=error`、`data={}`,`error={code:"-1", message:"no record", type:"SYSTEM_ERROR", retryable:true}`。要判断"有没有主键文档",看是否命中这个 `no record` 错误,而不是判断 `nodeId` 是否为 null。需要文档时改用 `primary-doc-create`(幂等,已存在则直接返回)。 ### 创建主键文档 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-history.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-history.md index f02d3d87..20154b98 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-history.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-history.md @@ -25,12 +25,11 @@ dws aitable record history-list \ "data": { "histories": [ { - "type": "field_change", // 变更类型 - "action": "update", // 操作动作: create / update / delete - "newValue": "{\"...\":\"...\"}", // 变更后的值(JSON 字符串) - "oldValue": "{\"...\":\"...\"}", // 变更前的值(JSON 字符串) + "type": "row", // 变更类型,实测均为 "row"(行级变更) + "action": "updateRecords", // 操作动作:appendRow(新增行) / updateRecords(更新记录) + "newValue": "{\"fldX\":{\"dataType\":\"STRING\",\"value\":\"改后值\"}}", // 变更后的值(JSON 字符串,按 fieldId 组织) + "oldValue": "{\"fldX\":{\"dataType\":\"STRING\",\"value\":\"改前值\"}}", // 变更前的值(appendRow 新增行时无此字段) "operateTime": 1733123456789, // 操作时间(毫秒级时间戳) - "typeChangedFields": "{...}", // 类型变更的字段信息(JSON 字符串) "version": 7 // 版本号(单调递增) } ] @@ -38,14 +37,14 @@ dws aitable record history-list \ } ``` -`newValue` / `oldValue` / `typeChangedFields` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值。 +`newValue` / `oldValue` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值;解析后是 `{fieldId: {dataType, value}}` 结构。`appendRow`(新增行)事件没有 `oldValue`。实测返回里**没有** `typeChangedFields` 字段。 ## 字段含义速查 | 字段 | 用途 | |------|------| -| `type` | 高层分类:`record_create` / `field_change` / `record_delete` 等。先按 type 过滤大类。 | -| `action` | 三态:`create` / `update` / `delete`。比 type 粗,但便于按"动作"统计。 | +| `type` | 实测均为 `row`(行级变更),服务端未按 `record_create` / `field_change` 细分。 | +| `action` | 底层操作名:`appendRow`(新增行)/ `updateRecords`(更新记录)。按"动作"统计时以这两个值为准。 | | `version` | 单调递增整数;同一 record 越新值越大。**用作"上一条 vs 这一条"的稳定排序键**。 | | `operateTime` | 毫秒时间戳;可格式化成可读时间。多条同 version 的极端场景用 operateTime 兜底排序。 | @@ -74,20 +73,22 @@ dws aitable record history-list --base-id BASE --table-id TBL --record-id REC -- ```bash dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --format json \ - | jq '[.data.histories[] | select(.action == "update")][0].oldValue' + | jq '[.data.histories[] | select(.action == "updateRecords")][0].oldValue' ``` -### 4. 找出删除事件(如果存在 delete history) +### 4. 只看更新事件(排除新增行) ```bash dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \ - | jq '.data.histories[] | select(.action == "delete") | {version, operateTime}' + | jq '.data.histories[] | select(.action == "updateRecords") | {version, operateTime}' ``` +> 记录被 `record delete` 删除后,其历史不再返回(`histories` 为空数组),无法通过本命令回溯删除事件。 + ## 注意事项 - 一次只能查一条 record;如需批量审计多条记录请循环调用。 -- 仅返回**字段值变更**与**记录生命周期事件**;视图、字段定义、表结构变更不在此 history 里。 +- 仅返回**新增行**(appendRow)与**字段值变更**(updateRecords);记录被删除后其历史不再可查(返回空)。视图、字段定义、表结构变更不在此 history 里。 - 历史保留时长由 server 决定,过老的记录可能不再返回。 ## 与其他 record 命令的关系 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-view-config.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-view-config.md index 6b22ec03..d2423374 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-view-config.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-view-config.md @@ -79,21 +79,21 @@ dws aitable view update timebar --view-id GANTT_ID --official-holiday=true ### view update aggregate(仅 Grid) -值是 `map[fieldId]→AggregateAction string`;传 null 清除某个字段聚合。 +值是 `map[fieldId]→AggregateAction string`。**设置**聚合可用;**清除**聚合当前无效(见下方警告)。 | flag | 类型 | 说明 | |------|------|------| | `--field-id` | string | 配合 `--action` 设置**单字段**聚合 | | `--action` | string | `SUM`/`AVG`/`MAX`/`MIN`/`MEDIAN`/`RANGE`/`TOTAL`/`DISTINCT`/`EXIST`/`UN_EXIST`/`CHECKED`/`EARLIEST_DATE` 等(按字段类型可用) | -| `--clear-field-id` | string (CSV) | 一/多个字段 ID,清除其聚合 | +| `--clear-field-id` | string (CSV) | 一/多个字段 ID,本意是清除其聚合,但**当前服务端不支持清除,静默无效**(见下方警告) | | `--json` | JSON | 完整 aggregate map | ```bash dws aitable view update aggregate --view-id GRID_ID --field-id fldX --action SUM -dws aitable view update aggregate --view-id GRID_ID --clear-field-id fldA,fldB -dws aitable view update aggregate --view-id GRID_ID --json '{"fldX":"AVG","fldY":null}' ``` +> ⚠️ **当前无法清除已设置的聚合(服务端限制)**:`--clear-field-id fldA,fldB` 与 `--json '{"fldX":null}'` 两种清除写法都返回 `success`,但用 `view get aggregate` 复核会发现聚合**原样不动**——是静默无效,不是真的清掉。这是服务端没有清除语义所致,直至服务端修复前不要依赖它。改聚合方式可行(重新 `--action` 覆盖成别的),只是无法回到"无聚合"。 + ### view update field-widths(仅 Grid) | flag | 类型 | @@ -108,9 +108,11 @@ dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB ### view update visible-fields(通用) -整组替换可见字段列表与顺序。首列字段(primaryDoc)必须保留在数组第一位。 +整组替换可见字段列表与顺序,同时兼作**隐藏/显示**入口。首列字段(primaryDoc)必须保留在数组第一位。 -> ⚠️ 注意:服务端**只接受 reorder,不接受真"隐藏字段"**——如果传入的列表比当前 columns 短,缺失的字段不会被隐藏。需要真正隐藏字段请到 AI 表格 Web UI。 +> **传入的列表既定顺序又定可见性**:传一个比当前 columns 短的列表,缺失的字段会被真正隐藏(该字段在 `view list` 的 `custom.hiddenFields` 里变 `true`);再传回全量列表即可解除隐藏(`hiddenFields` 变 `false`)。 +> +> ⚠️ **查隐藏状态别看这里**:`view get visible-fields` 返回的数组**包含已隐藏字段**(列的完整顺序),看不出谁被隐藏。要确认隐藏状态,读 `view get`(view list)里该视图的 `custom.hiddenFields`(`{fieldId: true|false}`)。 | flag | 类型 | |------|------| @@ -118,7 +120,9 @@ dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB | `--json` | string 数组 JSON(与 `--field-ids` 同传时 `--json` 优先) | ```bash -dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA,fldB +# 只保留首列和 fldA,其余字段被隐藏 +dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA +# 传回全量列表解除隐藏 dws aitable view update visible-fields --view-id VIEW_ID --json '["fldPrimary","fldA","fldB"]' ``` diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-view-extras.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-view-extras.md index 23db8059..c9174158 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-view-extras.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-view-extras.md @@ -54,7 +54,7 @@ dws aitable view update frozen-cols --view-id VIEW_ID --count 0 # 查询当前冻结列数 dws aitable view get frozen-cols --view-id VIEW_ID --format json -# → {"data": {..., "count": 1}} count 为 null 表示视图未显式设置 +# → {"data": {..., "count": 1}} 未显式设置时整个 count 键缺失(不是返回 null) ``` `--count` 必须 ≥ 0;负数会被拒绝。 @@ -69,7 +69,7 @@ dws aitable view update row-height --view-id VIEW_ID --cell-height 56 # 查询当前行高 dws aitable view get row-height --view-id VIEW_ID --format json -# → {"data": {..., "cellHeight": 56}} cellHeight 为 null 表示视图未显式设置(前端按 32 渲染) +# → {"data": {..., "cellHeight": 56}} 未显式设置时整个 cellHeight 键缺失(不是返回 null;前端按 32 渲染) ``` ## 数据高亮规则(条件填色,仅 Grid) diff --git a/skills/multi/dingtalk-aitable/scripts/aitable_export_via_task.py b/skills/multi/dingtalk-aitable/scripts/aitable_export_via_task.py index fdb20406..abee2023 100644 --- a/skills/multi/dingtalk-aitable/scripts/aitable_export_via_task.py +++ b/skills/multi/dingtalk-aitable/scripts/aitable_export_via_task.py @@ -88,10 +88,12 @@ def build_start_args(args: argparse.Namespace) -> list[str]: args.base_id, "--scope", args.scope, - "--export-format", + "--format", args.export_format, - "--timeout-sec", - str(args.timeout_sec), + # CLI 的 --timeout-ms 是单次等待上限(毫秒,最大 30000);脚本自身的 + # --timeout-sec 用于整体轮询/子进程超时,二者语义不同,不能混用。 + "--timeout-ms", + "30000", ] if args.table_id: cmd.extend(["--table-id", args.table_id]) @@ -152,8 +154,8 @@ def main() -> None: args.base_id, "--task-id", task_id, - "--timeout-sec", - str(args.timeout_sec), + "--timeout-ms", + "30000", ], timeout_sec=max(120, args.timeout_sec + 60), ) diff --git a/skills/multi/dingtalk-aitable/scripts/aitable_import_via_task.py b/skills/multi/dingtalk-aitable/scripts/aitable_import_via_task.py index f3139df2..566be970 100644 --- a/skills/multi/dingtalk-aitable/scripts/aitable_import_via_task.py +++ b/skills/multi/dingtalk-aitable/scripts/aitable_import_via_task.py @@ -140,8 +140,10 @@ def main() -> None: "data", "--import-id", import_id, - "--timeout-sec", - str(args.timeout_sec), + # import data 的 --timeout 单位是秒、最大 30;脚本的 --timeout-sec + # 是整体子进程预算,不能直接透传,这里用 CLI 允许的最大值。 + "--timeout", + "30", "--format", "json", ], diff --git a/skills/multi/dingtalk-aitable/scripts/bulk_add_fields.py b/skills/multi/dingtalk-aitable/scripts/bulk_add_fields.py index 98e0a9c7..78c5bdfc 100644 --- a/skills/multi/dingtalk-aitable/scripts/bulk_add_fields.py +++ b/skills/multi/dingtalk-aitable/scripts/bulk_add_fields.py @@ -29,7 +29,7 @@ JsonData = Union[List[Any], Dict[str, Any]] MAX_FILE_SIZE = 10 * 1024 * 1024 ALLOWED_FILE_EXTENSIONS = ['.json'] -RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$') +RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{6,128}$') ALLOWED_FIELD_TYPES = { 'text', 'number', 'singleSelect', 'multipleSelect', 'date', 'currency', 'user', 'department', 'group', 'progress', 'rating', 'checkbox', diff --git a/skills/multi/dingtalk-aitable/scripts/import_records.py b/skills/multi/dingtalk-aitable/scripts/import_records.py index 020b3cad..a76f78d2 100644 --- a/skills/multi/dingtalk-aitable/scripts/import_records.py +++ b/skills/multi/dingtalk-aitable/scripts/import_records.py @@ -28,7 +28,7 @@ RecordDict = Dict[str, str] MAX_FILE_SIZE = 50 * 1024 * 1024 ALLOWED_CSV_EXTENSIONS = ['.csv'] ALLOWED_JSON_EXTENSIONS = ['.json'] -RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$') +RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{6,128}$') MAX_RECORDS_PER_BATCH = 100 DEFAULT_BATCH_SIZE = 50 diff --git a/skills/multi/dingtalk-attendance/SKILL.md b/skills/multi/dingtalk-attendance/SKILL.md index c876ddb0..ec9545a1 100644 --- a/skills/multi/dingtalk-attendance/SKILL.md +++ b/skills/multi/dingtalk-attendance/SKILL.md @@ -1,6 +1,6 @@ --- name: dingtalk-attendance -description: 钉钉考勤(只读)。Use when 用户说 考勤/打卡记录/查打卡/查班次/考勤汇总/考勤规则/出勤情况。命令前缀:dws attendance。开源版仅支持只读查询,不支持创建班次、导入排班、修改考勤组等写操作。 +description: 钉钉考勤。Use when 用户说 考勤/打卡记录/查打卡/查班次/考勤汇总/考勤规则/出勤情况/请假加班补卡/排班/考勤组/假期余额。命令前缀:dws attendance。支持查询与写操作(班次、排班、考勤组、个人规则设置、假期规则/余额等),写操作需二次确认后再加 --yes。 cli_version: ">=0.2.14" metadata: category: product @@ -30,11 +30,16 @@ metadata: | "查我 / 某人 某天的打卡" | `dws attendance record get --user --date ` | | "查考勤组 / 考勤规则 / 打卡范围" | `dws attendance rules --date ` | | "查班次 / 排班 / 谁今天上什么班" | `dws attendance shift list --users --start --end ` | -| "考勤统计 / 周月汇总 / 出勤天数" | `dws attendance summary --user --date "" --stats-type week\|month` | +| "考勤统计 / 周月汇总 / 出勤天数" | `dws attendance summary --user --date --stats-type week\|month` | +| "请假 / 加班 / 补卡 / 出差 记录或提交链接" | `dws attendance approve list` / `dws attendance approve templates --type <类型>` | +| "班次 / 补卡规则 / 加班规则 / 考勤组 查询与修改" | `class` / `adjustment` / `overtime` / `group` 子命令 | +| "排班 / 假期规则 / 假期余额 / 签到 / 报表" | `schedule` / `vacation` / `checkin` / `report` 子命令 | + +> 完整命令集(含写操作与参数)见 [references/attendance.md](references/attendance.md)。 ## 评测高频硬约束 -- 开源版考勤只有 `record / rules / shift / summary` 四个只读命令;用户提到创建班次、导入排班、加人入考勤组等写操作时,直接告知"开源版不支持",不要伪装成功。 +- 当前 dws 已注册全部考勤子命令组(`record` / `check` / `approve` / `shift` / `schedule` / `class` / `adjustment` / `overtime` / `group` / `summary` / `rules` / `selfsetting` / `globalsetting` / `vacation` / `checkin` / `report` / `boss-check`),查询与写操作大多可直接调用后端。**不要再以"开源版只读/不支持写操作"为由拒答。** 创建班次、导入排班、加人入考勤组、保存个人规则设置、设置假期余额等写操作真机可执行,但必须先展示参数摘要并二次确认,再追加 `--yes`(或 `--user-say-yes`)执行;不要在未确认时直接写、也不要伪装成功。个别命令受权限/数据影响返回空或权限错误(如 `report` 系列仅管理员),如实说明即可。 - 查询迟到/缺勤名单时,空打卡结果不等于"没人迟到"。必须结合排班、`NotSigned`、`Absenteeism`、无记录人员分别说明;数据缺失要标为"无记录/无法判断",不要归为正常。 - 做部门 Top N 排名时,用户要求前 N 名就必须输出 N 个部门;无打卡记录或无可计算数据的部门按 0 或"无数据"保留在排名中,不能只输出有数据的少数部门。 - `summary` 必须同时传 `--user`、`--date`、`--stats-type`(week/month),缺一返回 C0002。 diff --git a/skills/multi/dingtalk-attendance/references/attendance-report.md b/skills/multi/dingtalk-attendance/references/attendance-report.md new file mode 100644 index 00000000..43d54547 --- /dev/null +++ b/skills/multi/dingtalk-attendance/references/attendance-report.md @@ -0,0 +1,619 @@ +# 考勤报表导出参考 (attendance-report) + +> 本文档由 `attendance.md` 路由调用。当用户提到"考勤报表"、"导出考勤"、"出勤汇总"、"考勤明细"、"迟到早退统计"、"全员考勤数据"、"某月考勤统计"、"考勤表格"、"考勤 Excel" 时,应阅读本文档执行。 +> 不适用于:个人单日打卡查询(用 `attendance check record`)、班次查询(用 `attendance schedule get`)、假期余额(用 `vacation balance`)、审批进度(用 `oa`)。 + +## 强制门禁(必须先读完本文档才能执行) + +**任何调用 `attendance_report_detail.py` / `attendance_report_monthly.py` / `attendance_report_daily.py` 的请求,都必须经过本文档定义的工作流,严禁绕过本文档直接拼脚本命令执行。** + +违反将出现以下任一问题: +1. 未按"阶段 1"做人员解析 → `--users` 传入部门 ID 而非员工 userId,脚本虽内置回退但会浪费一次失败的接口调用 +2. 未按"阶段 0"判断报表类型 → 用户说"汇总"被理解成"明细",导致输出粒度错误 +3. 未按"列选择"判断是否传 `--column-keywords` → 用户要"迟到情况报表"被输出成全字段默认报表 +4. 未按"错误处理"规则处理无权限错误(errorCode 6001「无权限操作」/ `AUTH_ERROR`)/ `HSF_ILLEGALPARAMS` → 把环境错误当成业务错误反馈给用户 +5. 未按"阶段 4"返回结果 → 把 Excel 内容贴在对话里,或者裸 userId 直接输出 + +**执行前自检(必须能在心中回答)**: +- [ ] 报表类型是?(明细 / 月度汇总 / 每日统计) +- [ ] 人员列表的来源是?(`aisearch person` 还是 `contact dept list-members`?) +- [ ] 列选择方式是?(预设报表关键词 / 自定义 `--column-keywords` / 默认列集合) +- [ ] 报错时如何向用户解释? + +如果上述任何一项答不出,**回到本文档对应章节重新阅读**,禁止凭记忆/想象组装命令。 + +**前提**:当前用户必须是钉钉管理员,否则 report 系列接口返回业务权限错误(`server_error_code` 为 `AUTH_ERROR`、errorCode `6001`「无权限操作」),**不是 HTTP 403**。提示需要管理员权限,不要重试。 + +## 核心原则 + +Agent 解析用户意图(报表类型、人员范围、时间范围、关注维度),获取 userId 列表后,**直接调用对应的 Python 脚本 CLI 生成 Excel**。 +- **脚本自包含**:数据查询(分批、分段、翻页)、字段解析、聚合计算、Excel 生成全部由脚本内部完成,Agent 不参与数据查询和计算 +- **月度汇总 / 每日统计**:脚本内部调用 `report columns` + `report query-data` +- **明细**:脚本内部调用 `check result` + `check record`(数据源不同) +- 列选择是独立维度:用户未指定关注维度时脚本使用内置默认字段;用户指定了关注维度时 Agent 通过 `--column-keywords` 参数传给脚本 + +## 严格禁止 (NEVER DO) + +- 禁止凭历史记忆复用 userId 等任何 ID,必须从当次命令返回值中提取 +- 禁止用大模型口算/目测做考勤数据聚合(求和、计数、分组),必须通过 Python 脚本完成 +- 禁止 Agent 直接调用 `report query-data` / `report columns` / `check result` / `check record`,这些由脚本内部自动完成 +- 禁止 `dws` 命令缺省 `--format json`(Agent 仅在阶段 1 获取人员时直接调用 dws 命令) +- 禁止编造任何字段值或用户姓名 +- 禁止直接输出裸 userId,脚本已内置 userId → 姓名转换 + +## 严格要求 (MUST DO) + +- 所有 `dws` 命令必须携带 `--format json` +- 时间参数 `--start` / `--end` 格式必须为 `yyyy-MM-dd HH:mm:ss` +- 字段 ID 与字段名的映射必须从 `report columns` 实时建立,禁止硬编码 +- 任何接口失败(含无权限错误 6001/`AUTH_ERROR`)必须向用户清晰报错,禁止静默吞掉 + +## 涉及工具 + +| 工具 | 用途 | 安全等级 | +|------|------|---------| +| `dws attendance report columns` | 获取当前管理员可见的考勤字段清单(字段 ID → 字段名) | 只读 | +| `dws attendance report query-data` | 按字段查询考勤数据(≤20 人/次,≤32 天/次) | 只读 | +| `dws attendance report query-leave` | 按假期名称查询假期数据(≤20 人/次,≤32 天/次),月度汇总/每日统计的"请假"列由脚本自动调用 | 只读 | +| `dws attendance approve list` | 查询审批单记录(考勤记录报表专用),支持类型:leave/trip/out/patch | 只读 | +| `dws oa approval detail` | 获取审批单详情(考勤记录报表专用),解析 formValueVOS / extValue | 只读 | +| `dws attendance check result` | 查询打卡结果(≤100 人/次,≤1 月,明细报表专用) | 只读 | +| `dws attendance check record` | 查询打卡流水(≤1 月,明细报表专用) | 只读 | +| `dws aisearch person` | 按姓名搜索用户获取 userId(搜人首选) | 只读 | +| `dws contact user get` | 批量查询 userId → 用户信息(姓名/部门/工号/职位) | 只读 | +| `dws contact dept search` | 搜索部门获取 deptId | 只读 | +| `dws contact dept list-members` | 获取部门成员 userId 列表 | 只读 | + +## 意图判断 + +### 报表类型(五选一) + +| 用户说 | 报表类型 | 映射脚本 | +|--------|---------|---------| +| "导出研发部 3 月份的考勤明细" / "每条打卡记录" / "明细" / "原始记录" | 明细 | `attendance_report_detail.py` | +| "生成研发部 3 月考勤汇总" / 用户明确说"月度汇总" / 用户未指明类型 | **月度汇总(默认)** | `attendance_report_monthly.py` | +| "导出研发部 3 月每天的出勤情况" / "按天统计" / "每日" / 用户明确说"每日统计" | 每日统计 | `attendance_report_daily.py` | +| "导出研发部 4 月的请假记录" / "补卡记录" / "出差记录" / "外出记录" / "xx记录" | 考勤记录 | `attendance_report_record.py` | +| "导出签到记录" / "签到报表" / "签到数据导出" / "签到明细" / "外勤签到" | 签到报表 | `attendance_report_checkin.py` | + +> **默认报表类型**:用户未指明报表类型时,**默认走月度汇总**,事后告知"已按月度汇总输出,如需明细/每日统计/考勤记录请告知"。 +> +> **考勤记录 vs 其他报表**:当用户明确提到"请假记录"/"补卡记录"/"出差记录"/"外出记录"时,走考勤记录报表(数据源为审批单)。而"请假报表"/"出差时长统计"等走月度汇总(数据源为 report query-data)。区别在于:考勤记录导出的是**审批单维度的原始数据**(含审批单状态、每天明细),月度汇总导出的是**按人按月聚合后的统计数据**。 + +### 列选择(独立维度,与报表类型正交) + +> **"报表类型"与"列选择"是两个独立维度,需分别判断。** +> 例如用户说"帮我出一份加班报表":报表类型未指明 → 默认月度汇总;列选择命中"加班报表" → 使用加班预设关键词。 +> 例如用户说"帮我出每日的异常报表":报表类型命中"每日" → 每日统计脚本;列选择命中"异常报表" → 使用异常预设关键词。 +> 预设报表**不会改变报表类型的判断逻辑**,报表类型始终按下方「报表类型(三选一)」规则判断。 + +| 用户说 | 列选择方式 | +|--------|-----------| +| "帮我出一份考勤报表" / "导出考勤" / 未提及特定关注维度 | 不传 `--column-keywords`,使用脚本内置**默认列集合** | +| "加班报表" / "加班统计" / "加班时长报表" | 传 `--column-keywords`,使用下方「加班报表预设关键词」 | +| "请假报表" / "请假出差报表" / "请假外出统计" | 传 `--column-keywords`,使用下方「请假报表预设关键词」 | +| "异常报表" / "异常考勤" / "迟到早退报表" / "缺卡报表" | 传 `--column-keywords`,使用下方「异常报表预设关键词」 | +| 提及了其他自定义关注维度(如"工作时长报表") | 传 `--column-keywords`,由 Agent 自行拼接关键词 | + +> **预设报表优先级**:当用户提到的关键词同时命中"预设报表"和一般自定义维度时,**优先使用预设报表的完整关键词列表**,确保列不遗漏。 + +### 易混淆场景 + +| 用户说 | 应路由到 | +|--------|---------| +| "今天打卡了吗" | `dws attendance record get`(单次查询,非报表) | +| "帮我排班" | [attendance-schedule.md](./attendance-schedule.md) 排班工作流(排班,非报表) | +| "我的假期还剩多少" | `dws attendance vacation balance`(单次查询) | +| "帮我请假" | `dws oa`(审批流程,非报表) | +| "我这个月的考勤怎么样" | `dws attendance summary`(个人统计,非报表) | + +## 工作流 + +### 阶段 0: 参数解析与确认查询范围 + +1. **解析用户输入**(两个独立维度): + - **报表类型**(四选一):明细 / 月度汇总(默认) / 每日统计 / 考勤记录 + - **列选择**(独立维度):预定义列集合(默认) / 用户指定维度筛选(考勤记录不适用) + - **人员维度**:指定员工 / 某个部门 / 多个部门(暂不支持全公司查询) + - **时间维度**:本周 / 本月 / 自定义时间段 + - **记录子类型**(仅考勤记录):leave(请假) / trip(出差) / out(外出) / patch(补卡) +2. **缺失信息处理**: + - **报表类型** 缺失 → 默认走月度汇总(不追问),事后告知 + - **列选择**:用户提及了特定关注维度 → 传 `--column-keywords`;未提及 → 使用脚本内置默认字段集 + - **用户范围 / 时间范围** 缺失 → 追问,禁止猜测 + +### 阶段 1: 获取完整人员列表 + +**场景 A — 指定员工姓名**: +```bash +dws aisearch person --keyword "<员工姓名>" --dimension name --format json +``` + +**场景 B — 按部门查询**: +```bash +dws contact dept search --query "<部门名>" --format json +dws contact dept list-members --ids --format json +``` + +**场景 C — 多个部门**: 对每个部门分别执行 B,汇总去重。 + +**场景 D — 全公司**: 暂不支持,引导用户指定部门。 + +**场景 E — 用户已给 userId 列表**: 直接跳过本步。 + +### 阶段 2: 列选择(决定脚本参数) + +> Agent 不需要手动调用 `report columns`,字段获取由脚本内部完成。Agent 只需根据用户意图决定是否传 `--column-keywords` 参数。 + +**判断顺序**(优先级从高到低): + +1. **明细报表** → 列固定,不支持 `--column-keywords` +2. **用户提到预设报表关键词** → 传 `--column-keywords`,使用本文档「预设报表列集合」中定义的完整关键词列表: + - "加班报表" / "加班统计" / "加班时长" → 使用「加班报表预设关键词」 + - "请假报表" / "请假出差" / "外出统计" → 使用「请假报表预设关键词」 + - "异常报表" / "迟到早退" / "缺卡报表" / "异常考勤" → 使用「异常报表预设关键词」 +3. **用户提及了其他自定义关注维度** → 传 `--column-keywords "..."` +4. **用户未提及特定关注维度** → 不传 `--column-keywords`,脚本使用内置默认字段集 + +### 阶段 3: 调用脚本生成 Excel + +#### 月度汇总 / 每日统计 + +```bash +python scripts/attendance_report_monthly.py \ + --users ,,... \ + --start "" \ + --end "" \ + [--column-keywords "出勤天数,迟到次数,迟到时长,..."] \ + [--out 月度汇总_研发部_202604.xlsx] +``` + +参数说明: +- `--users`(必填):逗号分隔的 userId 列表 +- `--start`(必填):开始日期,支持 `YYYY-MM-DD` 或 `YYYY-MM-DD HH:mm:ss` +- `--end`(必填):结束日期,同上 +- `--column-keywords`(可选):逗号分隔的字段名关键词。不传则使用脚本内置默认字段集。预设报表(加班/请假/异常)也通过本参数传入对应的预设关键词列表 +- `--out`(可选):输出文件名,不传则自动生成 + +脚本内部自动处理:`report columns` 获取字段清单 → 按预设/关键词匹配 → `report query-data` 分批分段查询 → `contact user get` 姓名映射 → 聚合计算 → 生成 Excel + +#### 明细 + +```bash +python scripts/attendance_report_detail.py \ + --users ,,... \ + --start "" \ + --end "" \ + [--out 考勤明细_研发部_202604.xlsx] +``` + +- **没有 `--column-keywords`**,明细列固定 +- 数据来源不同:`check result` + `check record`(非 `report query-data`) +- 分批限制:≤100 人/次(而非 20 人) + +#### 考勤记录 + +```bash +python scripts/attendance_report_record.py \ + --type \ + --users ,,... \ + --start "" \ + --end "" \ + [--out 请假记录_研发部_202604.xlsx] +``` + +参数说明: +- `--type`(必填):记录类型,支持 `leave`(请假) / `trip`(出差) / `out`(外出) / `patch`(补卡) +- `--users`(必填):逗号分隔的 userId 列表 +- `--start`(必填):开始日期 `YYYY-MM-DD` +- `--end`(必填):结束日期 `YYYY-MM-DD` +- `--out`(可选):输出文件名,不传则自动生成 +- **没有 `--column-keywords`**,列由 `--type` 决定 + +脚本内部自动处理:`attendance approve list` 获取审批摘要 → `oa approval detail` 获取详情 → 解析 DDHolidayField / extValue → 按天拆行 → `contact user get` 姓名映射 → 生成 Excel + +**记录类型选择规则**(Agent 需从用户意图中判断): + +| 用户说 | --type 值 | +|--------|----------| +| "请假记录" / "年假记录" / "调休记录" / "病假记录" | `leave` | +| "出差记录" | `trip` | +| "外出记录" | `out` | +| "补卡记录" | `patch` | + +#### 签到报表 + +```bash +python scripts/attendance_report_checkin.py \ + --users ,,... \ + --start "" \ + --end "" \ + [--out 签到报表_研发部_20260401_20260407.xlsx] +``` + +参数说明: +- `--users`(必填):逗号分隔的 userId 列表 +- `--start`(必填):开始日期 `YYYY-MM-DD` 或 `YYYY-MM-DD HH:mm:ss` +- `--end`(必填):结束日期,同上 +- `--out`(可选):输出文件名,不传则自动生成 +- **没有 `--column-keywords`**,签到报表列固定 + +脚本内部自动处理:`attendance checkin records` 分批分段查询(每批 50 人,每段 7 天)→ `contact user get` 姓名/部门映射 → 时间戳转日期+时间 → 图片列展开(最多 9 张)→ 生成 Excel + +> **注意**:签到接口时间限制为 7 天(不同于考勤报表的 32 天),脚本会自动按 7 天分段查询。 + +#### 脚本执行注意事项 + +- 脚本依赖 `openpyxl`,若未安装需先 `pip install openpyxl` +- 脚本摘要输出到 stdout,进度日志输出到 stderr +- 首次调试可加 `--inspect` 参数查看首条记录原始结构(`attendance_report_detail.py` / `attendance_report_monthly.py` / `attendance_report_daily.py` / `attendance_report_checkin.py` 支持;**`attendance_report_record.py` 无 `--inspect` 参数**) +- 脚本执行失败(exit ≠ 0)时,stderr 中有具体错误信息 + +### 阶段 4: 返回结果给用户 + +- 将脚本 stdout 输出的摘要信息原样转告用户 +- 如果脚本输出 warning,原样转告用户 +- 如果走的是默认月度汇总,追加:"已按月度汇总输出,如需明细/每日统计请告知" +- **不要把 Excel 内容贴在对话里**,只给路径和摘要 + +## 输出文件结构 + +### 月度汇总(双 sheet,自动生成) + +`attendance_report_monthly.py` 输出的 Excel 文件包含 **2 个 sheet**: + +| Sheet 名 | 布局 | 用途 | +|---------|------|------| +| `月度汇总` | 每人 1 行,列为基础信息 + 聚合字段 + 请假展开 + 考勤结果按天展开 | 整月数据汇总速览 | +| `日历表` | 每人 3 行(班次名称/考勤结果/工作时长),列为基础信息 + 指标 + 1日~N日 | 钉钉日历视图,逐日查看 | + +**日历表结构示意**: + +| 姓名 | 考勤组 | 部门 | 指标 | 1日 | 2日 | ... | 30日 | +|------|--------|------|------|------|------|------|------| +| 张三 | 研发组 | 技术部 | 班次名称 | 早班 | 早班 | ... | 休息 | +| | | | 考勤结果 | 正常 | 迟到 | ... | — | +| | | | 工作时长 | 8 | 7.5 | ... | 0 | +| 李四 | 研发组 | 技术部 | 班次名称 | 晚班 | 晚班 | ... | 早班 | +| ... | ... | ... | ... | ... | ... | ... | ... | + +- 基础列(姓名/考勤组/部门)已纵向 3 行合并 +- 日历表的 3 个指标字段(`班次名称`/`考勤结果`/`工作时长`)由脚本**强制**追加到 `report query-data` 查询字段中(即使用户的 `--column-keywords` 没包含),确保日历表非空 +- 日历表数据来源与月度汇总相同(同一次 `report query-data` 调用),不会增加接口次数 + +## 预定义列集合 + +### 月度汇总(3 个基础信息列 + 18 个考勤数据列) + +**基础信息列**(脚本自动从 `contact user get` 和原始记录中提取): + +| 序号 | 字段名称 | 数据来源 | +|------|---------|---------| +| 1 | 姓名 | `contact user get --ids` | +| 2 | 考勤组 | `attendance group search` + `filtered-get --member` 反向映射 | +| 3 | 部门 | `contact user get --ids` | + +**考勤数据列**(从 `report columns` 中按名称精确匹配): + +| 序号 | 字段名称 | +|------|---------| +| 4 | 出勤天数 | +| 5 | 休息天数 | +| 6 | 工作时长 | +| 7 | 迟到次数 | +| 8 | 迟到时长 | +| 9 | 严重迟到次数 | +| 10 | 严重迟到时长 | +| 11 | 旷工迟到次数 | +| 12 | 早退次数 | +| 13 | 早退时长 | +| 14 | 上班缺卡次数 | +| 15 | 下班缺卡次数 | +| 16 | 旷工天数 | +| 17 | 出差时长 | +| 18 | 外出时长 | +| 19 | 请假(按假期类型展开为 4 列:`请假-事假`、`请假-调休`、`请假-病假`、`请假-年假`,值为月度求和;数据由脚本通过 `report query-leave` 单独查询) | +| 20 | 加班-审批单统计 | +| 21 | 考勤结果(按天展开为多列:1日/2日/.../31日,每列显示当天考勤状态) | + +### 每日统计(4 个基础信息列 + 31 个考勤数据列) + +**基础信息列**: + +| 序号 | 字段名称 | 数据来源 | +|------|---------|---------| +| 1 | 姓名 | `contact user get --ids` | +| 2 | 考勤组 | `attendance group search` + `filtered-get --member` 反向映射 | +| 3 | 部门 | `contact user get --ids` | +| 4 | 日期 | 查询日期 | + +**考勤数据列**: + +| 序号 | 字段名称 | +|------|---------| +| 5 | 班次 | +| 6 | 上班1打卡时间 | +| 7 | 上班1打卡结果 | +| 8 | 下班1打卡时间 | +| 9 | 下班1打卡结果 | +| 10 | 上班2打卡时间 | +| 11 | 上班2打卡结果 | +| 12 | 下班2打卡时间 | +| 13 | 下班2打卡结果 | +| 14 | 上班3打卡时间 | +| 15 | 上班3打卡结果 | +| 16 | 下班3打卡时间 | +| 17 | 下班3打卡结果 | +| 18 | 关联的审批单 | +| 19 | 出勤天数 | +| 20 | 休息天数 | +| 21 | 工作时长 | +| 22 | 迟到次数 | +| 23 | 迟到时长 | +| 24 | 严重迟到次数 | +| 25 | 严重迟到时长 | +| 26 | 旷工迟到次数 | +| 27 | 早退次数 | +| 28 | 早退时长 | +| 29 | 上班缺卡次数 | +| 30 | 下班缺卡次数 | +| 31 | 旷工天数 | +| 32 | 出差时长 | +| 33 | 外出时长 | +| 34 | 请假(按假期类型展开为 4 列:`请假-事假`、`请假-调休`、`请假-病假`、`请假-年假`;数据由脚本通过 `report query-leave` 单独查询) | +| 35 | 加班-审批单统计 | + +### 预设报表:加班报表 + +3 个基础信息列(姓名/考勤组/部门)+ 以下考勤数据列: + +> 原始配置中的 TITLE_COLUMN(如"加班时长(转调休)")为分组标题,脚本不支持父子列结构,已打平为叶子字段。 + +| 序号 | 字段名称 | 说明 | +|------|---------|------| +| 4 | 加班-审批单统计 | — | +| 5 | 加班总时长 | — | +| 6 | 考勤结果 | 按天展开 | + +**对应 `--column-keywords`**:`加班-审批单统计,加班总时长,考勤结果` + +### 预设报表:请假报表 + +3 个基础信息列(姓名/考勤组/部门)+ 以下考勤数据列: + +| 序号 | 字段名称 | 说明 | +|------|---------|------| +| 4 | 请假 | 按假期类型自动展开(事假/调休/病假/年假等),数据由脚本通过 `report query-leave` 单独查询 | +| 5 | 出差时长 | — | +| 6 | 外出时长 | — | +| 7 | 考勤结果 | 按天展开 | + +**对应 `--column-keywords`**:`请假,出差时长,外出时长,考勤结果` + +### 预设报表:异常报表 + +3 个基础信息列(姓名/考勤组/部门)+ 以下考勤数据列: + +> 原始配置中的 TITLE_COLUMN(如"迟到"、"早退"、"缺卡")为分组标题,脚本不支持父子列结构,已打平为叶子字段。 + +| 序号 | 字段名称 | 说明 | +|------|---------|------| +| 4 | 迟到次数 | 原属分组「迟到」 | +| 5 | 迟到时长 | 同上 | +| 6 | 严重迟到次数 | 同上 | +| 7 | 严重迟到时长 | 同上 | +| 8 | 旷工迟到次数 | 同上 | +| 9 | 早退次数 | 原属分组「早退」 | +| 10 | 早退时长 | 同上 | +| 11 | 上班缺卡次数 | 原属分组「缺卡」 | +| 12 | 下班缺卡次数 | 同上 | +| 13 | 旷工天数 | — | +| 14 | 考勤结果 | 按天展开 | + +**对应 `--column-keywords`**:`迟到次数,迟到时长,严重迟到次数,严重迟到时长,旷工迟到次数,早退次数,早退时长,上班缺卡次数,下班缺卡次数,旷工天数,考勤结果` + +### 明细(3 个基础信息列 + 10 个打卡字段列) + +**基础信息列**: + +| 序号 | 字段名称 | 数据来源 | +|------|---------|---------| +| 1 | 姓名 | `contact user get --ids` | +| 2 | 考勤组 | `attendance group search` + `filtered-get --member` 反向映射 | +| 3 | 部门 | `contact user get --ids` | + +**打卡字段列**(以打卡流水为主表,每条流水一行;通过打卡时间关联 `check result` 获取考勤时间和打卡结果): + +| 序号 | 字段名称 | 数据来源 | +|------|---------|---------| +| 4 | 考勤日期 | `check record` | +| 5 | 考勤时间 | `check result`(班次规定的上/下班时间,按打卡时间关联) | +| 6 | 打卡时间 | `check record`(实际打卡时间) | +| 7 | 打卡结果 | `check result`(正常/迟到/早退/缺卡等,按打卡时间关联) | +| 8 | 打卡地址 | `check record` | +| 9 | 打卡备注 | `check record` | +| 10 | 异常打卡原因 | `check record` | +| 11 | 打卡图片 | `check record` | +| 12 | 打卡设备 | `check record` | +| 13 | 管理员修改备注 | `check record` | + +### 签到报表(3 个基础信息列 + 11 个签到字段列 + 9 个图片列) + +**基础信息列**: + +| 序号 | 字段名称 | 数据来源 | +|------|---------|---------| +| 1 | 姓名 | `contact user get --ids` | +| 2 | 部门 | `contact user get --ids` | +| 3 | 完整部门 | `contact user get --ids` | + +**签到字段列**(每条签到记录一行,数据来源均为 `attendance checkin records`): + +| 序号 | 字段名称 | 数据来源 | +|------|---------|---------| +| 4 | 签到来源 | `checkinType` | +| 5 | 日期 | `timestamp`(毫秒时间戳转日期) | +| 6 | 时间 | `timestamp`(毫秒时间戳转时间) | +| 7 | 经度 | `longitude` | +| 8 | 纬度 | `latitude` | +| 9 | 地点 | `place` | +| 10 | 详细地址 | `detailPlace` | +| 11 | 拜访客户 | `customers` | +| 12 | 客户部门名称 | 预留(签到接口暂无此字段) | +| 13 | 工作内容 | `remark` | +| 14 | 手机标识 | `mobileId` | + +**图片列**(从 `imageList` 数组展开,最多 9 列): + +| 序号 | 字段名称 | +|------|---------| +| 15 | 图片1 | +| 16 | 图片2 | +| ... | ... | +| 23 | 图片9 | + +## 分批查询规则(脚本内部自动处理) + +| 维度 | 限制 | 脚本自动处理方式 | +|------|------|---------| +| 人数超限(月度/每日) | `query-data` 最多 20 人/次 | 自动按 5 人一批分批 | +| 人数超限(明细) | `check result` 最多 100 人/次 | 自动按 100 人一批分批 | +| 人数超限(签到) | `checkin records` 最多 100 人/次 | 自动按 50 人一批分批 | +| 时间超限(月度/每日) | `--start` 到 `--end` 不超过 32 天 | 自动按月分段 | +| 时间超限(明细) | `--start` 到 `--end` 不超过 1 个月 | 自动按月分段 | +| 时间超限(签到) | `--start` 到 `--end` 不超过 7 天 | 自动按 7 天分段 | +| 分页(明细打卡结果) | `check result` 单次最多 1000 条 | 自动翻页 | + +## 错误处理 + +| 错误 | 原因 | 处理方式 | +|------|------|---------| +| 权限错误(errorCode 6001「无权限操作」/ `AUTH_ERROR`,非 HTTP 403) | 当前账号非管理员 | 提示需要管理员权限,不要重试 | +| userId 无效 | 用户 ID 错误或已离职 | 脚本跳过并在摘要中标注 | +| 时间区间超长 | 接口可能性能不佳 | 提示"超过 1 年的数据建议分阶段导出" | +| openpyxl 未安装 | 环境缺包 | 输出 `pip install openpyxl` 安装提示 | +| 脚本执行失败 | 接口异常/配置问题 | 将 stderr 错误信息转告用户,可加 `--inspect` 重试(`attendance_report_record.py` 不支持 `--inspect`) | + +## 使用示例 + +### 示例 1: 团队月度汇总(默认) +**用户说**: "帮我生成研发组 4 月的考勤报表" + +```bash +# 1. 获取部门成员 +dws contact dept search --query "研发组" --format json +dws contact dept list-members --ids --format json + +# 2. 调用脚本(默认月度汇总,不传 --column-keywords) +python scripts/attendance_report_monthly.py \ + --users userId1,userId2,... \ + --start "2026-04-01" --end "2026-04-30" +``` + +### 示例 2: 加班报表(预设) +**用户说**: "帮我出一份研发组 4 月的加班报表" + +```bash +python scripts/attendance_report_monthly.py \ + --users userId1,userId2,... \ + --start "2026-04-01" --end "2026-04-30" \ + --column-keywords "加班-审批单统计,加班总时长,考勤结果" +``` + +### 示例 3: 请假报表(预设) +**用户说**: "帮我导出研发组 4 月的请假出差情况" + +```bash +python scripts/attendance_report_monthly.py \ + --users userId1,userId2,... \ + --start "2026-04-01" --end "2026-04-30" \ + --column-keywords "请假,出差时长,外出时长,考勤结果" +``` + +### 示例 4: 异常报表(预设) +**用户说**: "帮我出研发组 4 月的异常考勤报表" + +```bash +python scripts/attendance_report_monthly.py \ + --users userId1,userId2,... \ + --start "2026-04-01" --end "2026-04-30" \ + --column-keywords "迟到次数,迟到时长,严重迟到次数,严重迟到时长,旷工迟到次数,早退次数,早退时长,上班缺卡次数,下班缺卡次数,旷工天数,考勤结果" +``` + +### 示例 5: 自定义维度筛选 +**用户说**: "帮我出一份研发组 4 月的工作时长报表" + +```bash +python scripts/attendance_report_monthly.py \ + --users userId1,userId2,... \ + --start "2026-04-01" --end "2026-04-30" \ + --column-keywords "工作时长" +``` + +### 示例 6: 每日统计 +**用户说**: "帮我出一份研发组 4 月每天的出勤情况" + +```bash +python scripts/attendance_report_daily.py \ + --users userId1,userId2,... \ + --start "2026-04-01" --end "2026-04-30" +``` + +### 示例 7: 明细报表 +**用户说**: "帮我导出研发组 4 月的考勤明细" + +```bash +python scripts/attendance_report_detail.py \ + --users userId1,userId2,... \ + --start "2026-04-01" --end "2026-04-30" +``` + +### 示例 8: 请假记录 +**用户说**: "帮我导出研发组 4 月的请假记录" + +```bash +python scripts/attendance_report_record.py \ + --type leave \ + --users userId1,userId2,... \ + --start "2026-04-01" --end "2026-04-30" +``` + +### 示例 9: 出差记录 +**用户说**: "帮我导出研发组 4 月的出差记录" + +```bash +python scripts/attendance_report_record.py \ + --type trip \ + --users userId1,userId2,... \ + --start "2026-04-01" --end "2026-04-30" +``` + +### 示例 10: 补卡记录 +**用户说**: "帮我导出研发组 5 月的补卡记录" + +```bash +python scripts/attendance_report_record.py \ + --type patch \ + --users userId1,userId2,... \ + --start "2026-05-01" --end "2026-05-31" +``` + +### 示例 11: 签到报表 +**用户说**: "帮我导出研发组上周的签到记录" + +```bash +python scripts/attendance_report_checkin.py \ + --users userId1,userId2,... \ + --start "2026-05-26" --end "2026-06-01" +``` + +## 配套脚本 + +| 脚本 | 报表类型 | 数据来源 | CLI 参数 | +|------|---------|---------|---------| +| [attendance_report_detail.py](../../scripts/attendance_report_detail.py) | 明细 | `check result` + `check record` | `--users --start --end [--out]` | +| [attendance_report_monthly.py](../../scripts/attendance_report_monthly.py) | 月度汇总(默认) | `report columns` + `report query-data` | `--users --start --end [--column-keywords] [--out]` | +| [attendance_report_daily.py](../../scripts/attendance_report_daily.py) | 每日统计 | `report columns` + `report query-data` | `--users --start --end [--column-keywords] [--out]` | +| [attendance_report_record.py](../../scripts/attendance_report_record.py) | 考勤记录 | `attendance approve list` + `oa approval detail` | `--type --users --start --end [--out]` | +| [attendance_report_checkin.py](../../scripts/attendance_report_checkin.py) | 签到报表 | `attendance checkin records` | `--users --start --end [--out]` | +| [attendance_report_common.py](../../scripts/attendance_report_common.py) | 公共模块(不可单独执行) | — | — | diff --git a/skills/multi/dingtalk-attendance/references/attendance-schedule.md b/skills/multi/dingtalk-attendance/references/attendance-schedule.md new file mode 100644 index 00000000..05bcc8a3 --- /dev/null +++ b/skills/multi/dingtalk-attendance/references/attendance-schedule.md @@ -0,0 +1,590 @@ +# 考勤排班操作参考 (attendance-schedule) + +> 本文档由 `attendance.md` 路由调用。覆盖两类排班操作: +> 1. **排班导入**(写操作):当用户提到"排班"、"导入排班"、"安排班次"、"设置排班"、"调班"、"换班"、"排休"时 +> 2. **排班查询导出**(只读操作):当用户提到"查看排班"、"排班表"、"导出排班"、"排班记录"时 +> +> 不适用于:班次定义查询(用 `attendance class search`)、考勤组配置(用 `attendance group get`)。 + +## 强制门禁(必须先读完本文档才能执行) + +**任何排班操作都必须经过本文档定义的工作流,严禁绕过本文档直接调用 `dws attendance schedule import` 命令。** + +违反将出现以下任一问题: +1. 未按"阶段 1"确认考勤组 → 把固定班制考勤组当排班制操作,接口报错 +2. 未按"阶段 3"校验班次 → 传入不属于该考勤组的班次 ID,导致排班数据错乱 +3. 未按"阶段 4"回显确认 → 用户未看到排班内容就直接执行,排错了无法回退 +4. 未按"阶段 2"解析人员 → 传入错误的 userId,导致排班到错误的人 +5. 未经用户确认就执行排班 → 排班是写操作,一旦执行就会覆盖原有排班 + +**执行前自检(必须能在心中回答)**: +- [ ] 考勤组是排班制(TURN)吗? +- [ ] 员工都属于该考勤组吗? +- [ ] 班次都属于该考勤组可用的班次吗? +- [ ] 用户已经确认了排班内容吗? + +如果上述任何一项答不出,**回到本文档对应章节重新阅读**,禁止凭记忆/想象组装命令。 + +**前提**:当前用户必须是钉钉考勤管理员,否则排班接口返回权限错误。 + +## 业务约束(必须深刻理解) + +> **这两条约束是排班的根基,贯穿整个工作流的每一步。** + +1. **用户只能属于一个考勤组**:每个员工有且只有一个考勤组,不存在"选择考勤组"的场景。直接通过 `dws attendance rules` 查询即可唯一确定。 +2. **排班只能排考勤组关联的班次**:考勤组绑定了固定的班次列表(`shiftVOList`),排班时只能从这些班次中选择,不能使用企业其他考勤组的班次,更不能编造班次。 + +**由此推导出的执行顺序**:必须先查清考勤组和它关联的班次,再去收集日期、人员等其他参数。 + +## 核心原则 + +Agent 解析用户意图(考勤组、员工、日期范围、班次安排),完成校验后,**调用 Python 脚本执行排班**。 +- **先查后排**:任何排班操作的第一步都是查询考勤组及其关联班次,拿到真实数据后再进行后续参数收集 +- **脚本自包含**:考勤组校验、班次校验、员工校验、回显确认、调用排班 API 全部由脚本内部完成 +- **Agent 职责**:先查考勤组和关联班次,再解析用户意图、获取必要的 ID(员工 userId)、组装脚本参数 +- **脚本职责**:二次校验、回显排班表格、等待用户确认、执行排班、输出结果摘要 +- 排班是**写操作**,必须经过回显确认后才能执行 + +## 严格禁止 (NEVER DO) + +- **禁止直接调用 `dws attendance schedule import`**,必须通过脚本执行 +- 禁止凭历史记忆复用任何 ID(考勤组 ID、班次 ID、userId),必须从当次命令返回值中提取 +- 禁止在未确认考勤组类型为排班制(TURN)的情况下执行排班 +- 禁止在班次未经校验的情况下执行排班 +- 禁止跳过用户确认直接执行排班 +- **禁止在未向用户展示完整排班明细表格(含每天的班次名称)的情况下弹出确认卡片**。用户必须先看到"谁、哪天、上什么班"才能做出确认决策 +- 禁止编造任何字段值或用户姓名 +- 禁止直接输出裸 userId,脚本已内置 userId → 姓名转换 +- **禁止直接输出裸 classId 数字**,必须展示班次名称(如"早班"),用户不理解 classId 是什么 + +## 严格要求 (MUST DO) + +- 所有 `dws` 命令必须携带 `--format json` +- 必须先确认考勤组类型为 TURN(排班制),否则拒绝执行 +- 必须通过考勤组详情获取绑定班次列表,校验班次 ID 属于该考勤组 +- 必须在执行排班前向用户回显排班内容并获得确认 +- 任何接口失败必须向用户清晰报错,禁止静默吞掉 + +## 涉及工具 + +| 工具 | 用途 | 安全等级 | +|------|------|---------| +| `dws attendance group search` | 搜索考勤组(按名称/类型) | 只读 | +| `dws attendance group get` | 查询考勤组全量信息(含绑定班次列表) | 只读 | +| `dws attendance group filtered-get` | 查询考勤组详情(成员列表) | 只读 | +| `dws attendance class search` | 查询班次列表(ID→名称映射) | 只读 | +| `dws attendance class get` | 查询班次详情 | 只读 | +| `dws attendance schedule import` | 导入排班记录(**仅由脚本内部调用**) | 写操作(危险) | +| `dws attendance schedule get` | 查询现有排班记录 | 只读 | +| `dws aisearch person` | 按姓名搜索用户获取 userId | 只读 | +| `dws contact user get` | 批量查询 userId → 用户信息 | 只读 | +| `dws contact dept search` | 搜索部门获取 deptId | 只读 | +| `dws contact dept list-members` | 获取部门成员 userId 列表 | 只读 | + +## 意图判断 + +### 排班操作类型 + +| 用户说 | 操作类型 | 处理方式 | +|--------|---------|---------| +| "帮我给研发组排下周的班" / "排班" / "安排班次" | 批量排班 | 走本文档工作流 | +| "帮我把张三下周一改成早班" / "调班" / "换班" | 单人调班 | 走本文档工作流(单人模式) | +| "帮我把李四下周三排休" | 排休 | 走本文档工作流(isRest=Y) | + +### 易混淆场景 + +| 用户说 | 应路由到 | +|--------|---------| +| "查看下周的排班" / "排班表" / "导出排班" / "导出排班表" / "XX考勤组的排班" | **本文档「排班查询导出工作流」**(走脚本) | +| "有哪些班次" / "班次列表" | `dws attendance class search`(查询班次定义) | +| "我属于哪个考勤组" | `dws attendance rules`(查询考勤规则) | +| "导出考勤报表" / "导出考勤" / "考勤明细" / "出勤汇总" (**不含"排班"二字**) | `attendance-report.md`(报表 skill) | + +> **关键区分**:"导出排班表" ≠ "导出考勤报表"。判断标准:句中含"排班"→ 本文档;不含"排班"且说的是"考勤报表/考勤数据/出勤统计" → `attendance-report.md`。 + +## 工作流 + +### 阶段 0: 先查考勤组和关联班次,再收集缺失参数 + +> **核心逻辑:先查后问。** 用户只属于一个考勤组,排班只能排该考勤组关联的班次。所以第一步永远是查清考勤组和它的班次,拿到真实数据后再向用户收集其他信息。 + +**步骤 0a — 查询考勤组(必须最先执行)**: + +```bash +# 自动获取当前用户的考勤组(用户只属于一个考勤组,无需选择) +dws attendance rules --date <今天日期> --format json +# → 从返回中提取 groupId +``` + +如果是给指定员工排班,先查该员工的 userId,再查其考勤组。 + +**步骤 0b — 查询考勤组详情和关联班次(必须在 ask_question 之前完成)**: + +```bash +# 获取考勤组详情(含绑定的班次列表) +dws attendance group get --group-id --format json +# → 校验 groupVO.type 必须为 TURN(排班制) +# → 从 groupVO.shiftVOList 提取关联的班次 +``` + +**提取结果**: +- 考勤组名称:`groupVO.name` +- 考勤组类型:`groupVO.type`(必须为 TURN) +- 关联班次列表:`groupVO.shiftVOList[].shiftSetting.{shiftId, shiftName}` + +**步骤 0c — 收集缺失参数(ask_question 卡片交互)**: + +拿到考勤组和关联班次后,再向用户收集缺失的参数。排班所需的四个参数: +- **考勤组**:已在 0a 自动获取,无需询问 +- **班次**:已在 0b 获取关联班次列表,展示给用户选择 +- **员工范围**(必填):指定员工姓名 / 部门 / 考勤组全员 / "给我排班" +- **日期范围**(必填):具体日期 / 日期范围(如"下周"、"5月19日到5月23日") + +**只收集真正缺失的参数**,用户已经提供的不要重复询问。将缺失参数**合并到一次 `ask_question` 调用中**。 + +示例:用户说"帮我排班",Agent 应先自动查询考勤组和关联班次(步骤 0a + 0b),然后只询问日期范围和班次: + + ``` + // ===== 步骤 0a + 0b 已完成,此时你已经拿到了以下真实数据 ===== + // groupId = 实际的考勤组ID + // groupName = 实际的考勤组名称 + // shiftVOList = 考勤组关联的班次列表(来自 dws attendance group get 的返回) + + // 从 shiftVOList 构建班次选项(伪代码): + shiftOptions = [] + for each shift in groupVO.shiftVOList: + shiftOptions.append({ id: String(shift.shiftSetting.shiftId), label: shift.shiftSetting.shiftName }) + shiftOptions.append({ id: "rest", label: "排休" }) + + // 如果考勤组只关联了一个班次,直接使用,不需要询问用户 + if shiftVOList.length == 1: + selectedShift = shiftVOList[0] // 自动选定,跳过班次选择 + + // 构建日期选项(必须填入实际计算的日期): + todayStr = 当天日期(YYYY-MM-DD) + thisWeekEnd = 本周日日期 + nextWeekStart = 下周一日期 + nextWeekEnd = 下周日日期 + + ask_question({ + title: "排班参数确认", + questions: [ + { + id: "date_range", + prompt: "请选择排班日期范围", + options: [ + { id: "this_week", label: "本周剩余时间(" + todayStr + "-" + thisWeekEnd + ")" }, + { id: "next_week", label: "下周(" + nextWeekStart + "-" + nextWeekEnd + ")" }, + { id: "custom", label: "自定义日期范围" } + ] + }, + { + id: "shift", + prompt: "请选择班次(以下为您考勤组「" + groupName + "」关联的班次)", + options: shiftOptions // 直接使用上面从 shiftVOList 动态构建的选项,严禁替换为任何硬编码值 + } + ] + }) + ``` + + **⚠️ 班次选项严禁硬编码**:上述伪代码中的 `shiftOptions` 必须在运行时从 `shiftVOList` 动态生成。禁止在 `ask_question` 中写入任何固定的班次名称(如"早班"、"晚班"、"正常班"、"全部排XX班"等)。如果你发现自己在 options 里手写班次名,说明你做错了。 + + **动态选项原则(严格执行)**: + - 班次选项**必须且只能**来自考勤组的 `shiftVOList`,**严禁编造任何班次名称**(如"正常班"、"早班"、"晚班"、"全部排XX班"等都是编造) + - 每个班次选项的 `id` 必须是 `shiftVOList` 中的真实 `shiftId`,`label` 必须是真实的 `shiftName` + - 如果 `shiftVOList` 为空,降级从 `groupVO.classIds` + `class search` 按 ID 精确查询(不是全局搜索) + - **只收集缺失的参数**:用户已经提供的参数不要重复询问 + - **如果考勤组只有一个班次,直接使用该班次,不需要询问用户选择** + - **禁止用 `class search` 全局搜索来给用户展示班次选项**——全局班次列表包含不属于该考勤组的班次,用户选了也会被校验拒绝 + +### 阶段 1: 确认考勤组(必须为排班制) + +> **业务事实:用户只属于一个考勤组。** 不存在"选考勤组"的场景,直接通过 `dws attendance rules` 自动获取即可。只有在用户明确指定了一个考勤组名称时,才用 `group search` 按名称确认。 + +**方式 A — 自动获取(默认方式,适用于绝大多数场景)**: +```bash +# 用户只属于一个考勤组,直接查询即可确定 +dws attendance rules --date <今天日期> --format json +# → 返回中提取 groupId,然后用 group get 获取详情 +dws attendance group get --group-id --format json +``` + +**方式 B — 用户明确指定考勤组名称时**: +```bash +dws attendance group search --query "<考勤组名称>" --type TURN --format json +``` + +**校验规则**: +1. 考勤组类型必须为 **TURN(排班制)**,如果是 FIXED(固定班制)或 NONE(自由工时),拒绝并提示"该考勤组不是排班制,无法进行排班操作" +2. 确认考勤组后,**必须立即获取其关联班次**(`groupVO.shiftVOList`),后续所有班次选项都从这里取 + +**提取信息**:考勤组 ID(`groupId`)、考勤组名称(`name`)、**关联班次列表(`shiftVOList`)** + +### 阶段 2: 获取员工列表 + +**场景 A — 指定员工姓名**: +```bash +dws aisearch person --keyword "<员工姓名>" --dimension name --format json +``` + +**场景 B — 按部门查询**: +```bash +dws contact dept search --query "<部门名>" --format json +dws contact dept list-members --ids --format json +``` + +**场景 C — 考勤组全员**: +```bash +dws attendance group filtered-get --group-id --member --format json +``` + +**场景 D — 用户已给 userId 列表**:直接跳过本步。 + +### 阶段 3: 校验班次(必须是考勤组关联的班次) + +> **业务约束:排班只能排考勤组关联的班次。** 阶段 0/1 已经通过 `dws attendance group get` 拿到了 `shiftVOList`,本阶段直接使用该数据校验,**不需要也不应该再调用 `class search` 全局搜索**。 + +**班次数据来源**(已在阶段 0 或阶段 1 获取): +- `groupVO.shiftVOList[].shiftSetting.shiftId` — 班次 ID +- `groupVO.shiftVOList[].shiftSetting.shiftName` — 班次名称 + +**禁止调用 `dws attendance class search` 全局搜索班次**——全局搜索会返回不属于该考勤组的班次,即使用户指定了班次名称,也必须在 `shiftVOList` 中匹配,而不是全局搜索。 + +**校验规则**: +1. 用户在 `ask_question` 卡片中选择的班次,其 `shiftId` 必须存在于 `shiftVOList` 中(卡片选项本身就是从 `shiftVOList` 构建的,所以天然满足) +2. 如果用户通过自然语言指定了班次名称(如"排早班"),必须在 `shiftVOList` 中**按名称模糊匹配**,找到对应的 `shiftId` +3. 如果用户指定的班次不在 `shiftVOList` 中,**必须拒绝**,并列出该考勤组关联的全部班次让用户重新选择 +4. 仅当 `shiftVOList` 为空时,才降级从 `groupVO.classIds` + `dws attendance class get` 按 ID 精确查询(仍然不是全局搜索) + +**提取信息**:班次 ID(`classId` / `shiftId`)、班次名称(`shiftName`) + +### 阶段 4: 回显排班内容并使用 ask_question 卡片确认 + +> **[硬性门禁]** 必须先用普通文本向用户展示**完整的排班明细表格**(包含每个人、每天、具体班次名称),用户看到排班明细后,才能弹出 `ask_question` 确认卡片。 +> **禁止在用户还不知道"谁、哪天、上什么班"的情况下就弹确认卡片**——这等于让用户盲签,体验极差。 + +**步骤 4a — 展示排班明细(必须在确认卡片之前)**: + +在调用 `ask_question` 之前,**必须先**用普通文本向用户展示排班内容。表格中**必须包含班次名称**(如"早班 09:00-18:00"),不能只展示 classId 数字: + +``` +排班预览 + +考勤组: <考勤组名称>(ID: ) +排班日期: ~ + +| 员工姓名 | 日期 | 星期 | 班次 | 是否排休 | +|---------|------|------|------|---------| +| 张三 | 2026-05-19 | 周一 | 早班 09:00-18:00 | 否 | +| 张三 | 2026-05-20 | 周二 | 早班 09:00-18:00 | 否 | +| 张三 | 2026-05-21 | 周三 | 排休 | 是 | +| 李四 | 2026-05-19 | 周一 | 晚班 18:00-02:00 | 否 | +| ... | ... | ... | ... | ... | + +共 条排班记录 +``` + +**自查清单(展示表格前必须确认)**: +- [ ] 表格中有员工姓名(不是 userId) +- [ ] 表格中有具体日期和星期几 +- [ ] 表格中有班次名称(不是 classId 数字) +- [ ] 排休的记录标注了"排休" +- [ ] 表格涵盖了所有待排班的员工和日期 + +**步骤 4b — 使用 `ask_question` 卡片确认(必须在展示表格之后)**: + +``` +ask_question({ + title: "排班执行确认", + questions: [ + { + id: "confirm_execute", + prompt: "以上排班将覆盖所选日期的现有排班记录,确认执行吗?", + options: [ + { id: "yes", label: "确认执行" }, + { id: "no", label: "取消" } + ] + } + ] +}) +``` + +**处理用户选择**: +- 用户选择 **"确认执行"** → 进入阶段 5 +- 用户选择 **"取消"** → 终止流程,提示"已取消排班操作" + +### 阶段 5: 调用脚本执行排班 + +```bash +python scripts/attendance_schedule_import.py \ + --group-id \ + --schedules '' \ + --confirm +``` + +参数说明: +- `--group-id`(必填):考勤组 ID +- `--schedules`(必填):排班记录 JSON 数组,每条记录包含 `userId`、`workDate`、`classId`、`isRest` +- `--confirm`(必填):表示用户已确认,脚本收到此标志才会执行排班 + +脚本内部自动处理: +1. 二次校验考勤组类型(必须为 TURN) +2. 从考勤组详情提取绑定班次,二次校验班次 ID 属于该考勤组 +3. 格式化 workDate 为 `yyyy-MM-dd HH:mm:ss` +4. 调用 `dws attendance schedule import` 执行排班 +5. 输出执行结果摘要(含全部排班明细) + +### 阶段 6: 返回结果给用户 + +- 将脚本 stdout 输出的摘要信息原样转告用户 +- 如果脚本输出 warning,原样转告用户 +- 如果执行失败,将 stderr 错误信息转告用户 + +## 排班记录 JSON 格式 + +每条排班记录的字段说明: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `userId` | string | 是 | 员工的 userId | +| `workDate` | string | 是 | 排班日期,格式 YYYY-MM-DD | +| `classId` | int | 是 | 班次 ID(从 `class search` 获取) | +| `isRest` | string | 是 | 是否排休,`Y`=排休 / `N`=正常上班 | + +排休时 `classId` 传 0,`isRest` 传 `Y`。 + +## API 返回结构注意事项(Agent 必读) + +> 以下是实际执行中多次踩坑的关键数据结构说明。**禁止凭直觉假设字段在顶层**,必须按本节描述的嵌套路径提取。 + +### `dws attendance group get` 返回结构 + +``` +run_dws 解包后的结构(unwrap_result 去掉 success/result 包装后): +{ + "groupVO": { ← 关键!type/name/classIds 等字段在这一层 + "type": "TURN", ← 考勤组类型 + "name": "研发组", + "classIds": [1290384739, ...], ← 绑定的班次 ID 列表 + "shiftVOList": [ ← 排班制特有,班次详情 + { + "shiftSetting": { + "shiftId": 1290384739, ← 班次 ID(与 classIds 对应) + "shiftName": "早班 09:00-18:00" + } + } + ], + "selectedClass": [...], ← 部分环境使用此字段 + ... + }, + ...其他顶层字段... +} +``` + +**提取规则**: +- 考勤组类型:`result["groupVO"]["type"]` +- 考勤组名称:`result["groupVO"]["name"]` +- 绑定班次 ID 列表:`result["groupVO"]["classIds"]` +- 班次名称:`result["groupVO"]["shiftVOList"][N]["shiftSetting"]["shiftName"]` +- **禁止从 result 顶层直接取 type/name/classIds,那里没有这些字段** + +### `dws attendance class search` 返回结构 + +``` +run_dws 解包后可能为以下之一: +1. 直接 list[dict]: [{id, name, ...}, ...] +2. {"data": [...]} 或 {"items": [...]} 或 {"classList": [...]} +``` + +**注意**:如果 `class search` 返回 0 条记录,不一定是错误——可能是当前账号没有班次管理权限。此时从 `group get` 的 `shiftVOList` 中也可获取班次名称。 + +### `dws aisearch person` 搜索同名问题 + +同一个姓名可能返回**多个不同 userId**(如主管理员账号 vs 子管理员账号)。必须通过以下方式确认正确的 userId: +1. 检查目标考勤组的成员列表:`dws attendance group filtered-get --group-id --member` +2. 取成员列表中存在的那个 userId + +**禁止直接取搜索结果的第一条 userId,必须与考勤组成员列表交叉验证。** + +### `dws contact user get` 可能的权限错误 + +`resolve_user_names`(userId→姓名转换)可能遇到 `SECURITY_CHECK_INVOKE_FAILED` 错误。这只影响**展示层**,不影响排班数据的正确性: +- 脚本已内置降级处理:权限失败时直接使用 userId 替代姓名 +- **不要因为姓名获取失败就中止排班流程** + +### Agent 常见错误模式(严禁) + +| 错误做法 | 正确做法 | +|------------|------------| +| `result.get("type")` 从顶层取类型 | `result["groupVO"]["type"]` | +| `result.get("classIds")` 从顶层取班次 | `result["groupVO"]["classIds"]` 或 `result["groupVO"]["shiftVOList"]` | +| 用 `python3 -c "..."` inline 脚本解析 JSON | 调用已有的 Python 脚本(`attendance_schedule_import.py`) | +| 人名搜到多个结果直接取第一个 | 与考勤组成员列表交叉验证 | +| 姓名获取失败就中止流程 | 降级用 userId 展示,继续执行排班 | +| 直接调用 `dws attendance schedule import` | 必须通过 `attendance_schedule_import.py` 脚本 | + +## 错误处理 + +| 错误 | 原因 | 处理方式 | +|------|------|---------| +| 权限错误(403) | 当前账号非管理员 | 提示需要管理员权限,不要重试 | +| 考勤组不是排班制 | 考勤组类型为 FIXED 或 NONE | 提示"该考勤组不是排班制,无法排班" | +| 班次不在可用列表中 | classId 无效 | 列出可用班次让用户重新选择 | +| userId 无效 | 用户 ID 错误或已离职 | 提示具体哪个用户无效 | +| 脚本执行失败 | 接口异常/配置问题 | 将 stderr 错误信息转告用户 | +| SECURITY_CHECK_INVOKE_FAILED | userId→姓名转换权限不足 | 仅影响展示,降级用 userId,不中止流程 | +| class search 返回空列表 | 账号无班次管理权限 | 从 `group get` 的 `shiftVOList` 提取班次名称 | + +## 使用示例 + +### 示例 1: 给指定员工排班 +**用户说**: "帮我给张三下周一到周五排早班,考勤组是研发组" + +```bash +# 1. 确认考勤组并获取关联班次(先查后排) +dws attendance group search --query "研发组" --type TURN --format json +# → 拿到 groupId +dws attendance group get --group-id --format json +# → 从 groupVO.shiftVOList 获取关联班次列表 +# → 在 shiftVOList 中匹配"早班",拿到对应的 shiftId 作为 classId +# ⚠️ 禁止用 class search 全局搜索班次 + +# 2. 获取员工 userId +dws aisearch person --keyword "张三" --dimension name --format json + +# 3. 回显确认(Agent 向用户展示排班表格) +# ... 用户确认 ... + +# 4. 调用脚本执行 +python scripts/attendance_schedule_import.py \ + --group-id 123456 \ + --schedules '[{"userId":"user001","workDate":"2026-05-19","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-20","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-21","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-22","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-23","classId":789,"isRest":"N"}]' \ + --confirm +``` + +### 示例 2: 给员工排休 +**用户说**: "帮我把李四下周三排休" + +```bash +# 1. 先查考勤组和关联班次(即使排休也需要确认考勤组) +dws attendance rules --date 2026-05-15 --format json +# → 拿到 groupId +dws attendance group get --group-id --format json +# → 确认是排班制(TURN) + +# 2. 获取员工 userId +dws aisearch person --keyword "李四" --dimension name --format json + +# 3. 回显确认 → 用户确认 → 执行 +python scripts/attendance_schedule_import.py \ + --group-id 123456 \ + --schedules '[{"userId":"user002","workDate":"2026-05-21","classId":0,"isRest":"Y"}]' \ + --confirm +``` + +### 示例 3: 部门批量排班 +**用户说**: "帮我给研发部全员下周排早班" + +```bash +# 1. 先查考勤组并获取关联班次(先查后排) +dws attendance rules --date 2026-05-15 --format json +# → 拿到 groupId +dws attendance group get --group-id --format json +# → 从 groupVO.shiftVOList 中匹配"早班",拿到 shiftId +# ⚠️ 禁止用 class search 全局搜索班次 + +# 2. 获取部门成员 +dws contact dept search --query "研发部" --format json +dws contact dept list-members --ids --format json + +# 3. 回显确认 → 用户确认 → 执行 +python scripts/attendance_schedule_import.py \ + --group-id 123456 \ + --schedules '[...]' \ + --confirm +``` + +## 配套脚本 + +| 脚本 | 用途 | CLI 参数 | +|------|------|---------| +| [attendance_schedule_import.py](../../scripts/attendance_schedule_import.py) | 排班导入(含校验、回显、执行) | `--group-id --schedules --confirm` | +| [attendance_schedule_export.py](../../scripts/attendance_schedule_export.py) | 排班查询导出(分批查询、排班表 Excel) | `--users --start --end [--output]` | + +--- + +## 排班查询导出工作流 + +> 当用户提到"查看排班"、"排班表"、"导出排班"、"排班记录"时,走此工作流。 +> **禁止直接调用 `dws attendance schedule get`**,必须通过脚本执行,脚本自动处理分批、姓名转换、班次名称转换、排班表格式输出。 + +### 查询导出 — 阶段 1: 参数收集 + +1. **员工范围**(必填):需获取 userId 列表 + - 指定员工姓名 → `dws aisearch person` 获取 userId + - 指定部门 → `dws contact dept search` + `dws contact dept list-members` + - 指定考勤组全员 → `dws attendance group filtered-get --member` + - 用户已给 userId 列表 → 直接使用 +2. **日期范围**(必填):开始日期 ~ 结束日期(YYYY-MM-DD) + - 用户说"下周" → 计算下周一到周日 + - 用户说"本月" → 计算本月 1 日到月末 + - 任何缺失信息必须追问 + +### 查询导出 — 阶段 2: 调用脚本 + +```bash +python scripts/attendance_schedule_export.py \ + --users \ + --start \ + --end \ + [--output ] +``` + +参数说明: +- `--users`(必填):userId 列表,逗号分隔 +- `--start`(必填):开始日期,格式 YYYY-MM-DD +- `--end`(必填):结束日期,格式 YYYY-MM-DD +- `--output`(可选):输出文件路径,默认 `attendance_schedule__.xlsx` + +脚本内部自动处理: +1. **分批查询**:超过 20 人自动分批调用 `dws attendance schedule get` +2. **班次名称转换**:classId → className(优先从记录中提取,缺失时回退 class search) +3. **姓名转换**:userId → 员工姓名 +4. **排班表格式**:日历表(行=员工,列=日期,单元格=班次名称) +5. **Excel 输出**:钉钉风格美化排版 + +### 查询导出 — 阶段 3: 返回结果给用户 + +- 将脚本 stdout 输出的摘要信息(人数、日期、记录数、预览表格)原样转告用户 +- 提醒用户完整排班表已导出到 Excel 文件 +- 如果执行失败,将 stderr 错误信息转告用户 + +### 查询导出示例 + +**用户说**: "帮我导出研发组下周的排班表" + +```bash +# 1. 获取考勤组成员 +dws attendance group search --query "研发组" --format json +dws attendance group filtered-get --group-id --member --format json + +# 2. 调用脚本导出 +python scripts/attendance_schedule_export.py \ + --users user001,user002,user003 \ + --start 2026-05-18 \ + --end 2026-05-24 +``` + +**用户说**: "帮我查看张三和李四本月的排班" + +```bash +# 1. 获取 userId +dws aisearch person --keyword "张三" --dimension name --format json +dws aisearch person --keyword "李四" --dimension name --format json + +# 2. 调用脚本导出 +python scripts/attendance_schedule_export.py \ + --users user001,user002 \ + --start 2026-05-01 \ + --end 2026-05-31 +``` diff --git a/skills/multi/dingtalk-attendance/references/attendance-vacation.md b/skills/multi/dingtalk-attendance/references/attendance-vacation.md new file mode 100644 index 00000000..6c59e739 --- /dev/null +++ b/skills/multi/dingtalk-attendance/references/attendance-vacation.md @@ -0,0 +1,140 @@ +# 假期余额导出参考 (attendance-vacation) + +> 本文档由 `attendance.md` 路由调用。当用户提到“导出假期余额”、“假期余额列表”、“所有假期规则余额”、“假期余额 Excel”、“年假/病假/调休余额导出”等诉求时,必须先阅读本文档,再调用配套脚本。 + +## 强制门禁 + +**任何调用 `attendance_vacation_balance.py` 的请求,都必须经过本文档定义的工作流,禁止只凭脚本路径或 `--help` 自行拼命令。** + +执行前必须确认: +- 人员范围:指定员工 / 部门 / 多部门;缺失时必须追问 +- 导出范围:默认导出所有假期规则余额;用户指定“年假/病假/调休”等时才传 `--leave-keywords` +- 输出形式:生成 Excel,不在对话中粘贴完整表格 + +## 核心原则 + +Agent 只负责解析人员范围并获取 userId 列表;假期规则查询、`leaveCode` 解析、假期规则单位解析、余额查询、字段解析、用户信息补齐、Excel 生成都由脚本完成。 + +`--leave-keywords` 是 `attendance_vacation_balance.py` 的脚本入参,只用于按假期名称筛选导出列;**不是** `dws attendance vacation balance` 的入参。`vacation balance` 当前只支持批量 `--users` 和单个 `--leave-code`,因此脚本必须先获取假期规则列表,再按每个匹配到的 `leaveCode` 分别查询余额。 + +脚本输出结构参考钉钉假期余额列表: +- 每名员工一行 +- 基础列固定:`姓名`、`部门`、`入职时间`、`首次工作时间` +- 假期规则横向动态展开为多列,表头必须携带 `vacation types` 返回的规则单位,例如 `年假(天)`、`病假(天)`、`调休(小时)` +- 特殊值统一展示为 `不限制余额`、`不适用`、`未设置` +- 当某个假期规则 `leaveCode` 查询余额时接口返回“假期类型没有余额”类业务错误,表示该规则不限制余额,脚本应为该规则列填充 `不限制余额` +- 当余额记录中返回 `visible=false`,表示该员工不适用该假期规则,脚本应为该员工 + 该规则单元格填充 `不适用` +- 当接口返回“员工未设置入职时间”或“员工未设置首次参加工作时间”类业务错误,表示该假期规则依赖员工时间字段且当前员工缺失配置,脚本应为该员工 + 该规则单元格填充 `不适用` +- 当 `vacation types` 返回假期规则 `source=external`,表示该规则由开放接口写入;若这类外部规则调用余额接口失败且不是权限错误,脚本不应阻断导出,应为该员工 + 该规则单元格填充 `外部规则暂无余额,需通过接口初始化更新余额` + +## 严格禁止 (NEVER DO) + +- 禁止凭历史记忆复用 userId、deptId、leaveCode +- 禁止 Agent 手工汇总、转置或目测假期余额 +- 禁止 Agent 自行只查单个 `leave-code` 再声称是“所有假期规则余额”;如需导出所有假期规则余额,必须交由脚本按假期规则列表逐个 `leaveCode` 查询并汇总 +- 禁止直接输出裸 userId;脚本会通过 `contact user get` 转换姓名和部门 +- 禁止把 Excel 明细内容完整贴在对话里,只返回路径和摘要 + +## 严格要求 (MUST DO) + +- 所有 `dws` 命令必须携带 `--format json` +- 假期规则名称`--leave-keywords`与假期规则`leaveCode`的映射必须从 `vacation types` 实时建立,禁止硬编码 +- 假期规则单位必须从 `vacation types` 实时读取,优先使用 `leaveViewUnit` 等展示单位字段,并在 Excel 表头中展示为 `假期名称(单位)` +- 假期规则来源必须从 `vacation types` 实时读取;当 `source=external` 时按外部接口写入规则处理 +- 任何接口失败必须向用户清晰报错,禁止静默吞掉 +- `vacation balance` 返回“假期类型没有余额”时不作为致命错误处理,应按该假期规则 `leaveCode` 生成 `不限制余额` +- `vacation balance` 返回员工维度 `visible=false` 时不作为查询失败处理,应按该员工 + 假期规则 `leaveCode` 生成 `不适用` +- `vacation balance` 返回“员工未设置入职时间”或“员工未设置首次参加工作时间”时不作为查询失败处理,应按该员工 + 假期规则 `leaveCode` 生成 `不适用` +- 对 `source=external` 的外部假期规则,`vacation balance` 查询失败且不是权限错误时不作为导出失败处理,应按该员工 + 假期规则 `leaveCode` 生成 `外部规则暂无余额,需通过接口初始化更新余额` + +## 涉及工具 + +| 工具 | 用途 | 安全等级 | +|------|------|---------| +| `dws attendance vacation types` | 获取当前可见的假期规则清单,用于建立假期名称/关键词 → `leaveCode` 的映射,读取 `leaveViewUnit` 等规则单位、`source` 规则来源,并决定 Excel 假期列顺序 | 只读 | +| `dws attendance vacation balance` | 按批量 `--users` + 单个 `--leave-code` 查询员工假期余额;由脚本按匹配到的 `leaveCode` 逐个调用 | 只读 | +| `dws aisearch person` | 按姓名搜索用户获取 userId(搜人首选) | 只读 | +| `dws contact user get` | 批量查询 userId → 用户信息(姓名/部门/入职时间等),用于 Excel 基础列补齐 | 只读 | +| `dws contact dept search` | 搜索部门获取 deptId | 只读 | +| `dws contact dept list-members` | 获取部门成员 userId 列表 | 只读 | + +## 工作流 + +### 阶段 0:参数解析 + +1. 识别人员范围: + - 指定员工姓名:用 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 获取 userId + - 指定部门:用 `dws contact dept search --query "<部门名>" --format json`,再用 `dws contact dept list-members --ids --format json` 获取成员 + - 已提供 userId:直接使用 +2. 识别假期列范围: + - 未指定假期类型:导出所有假期规则余额,不传 `--leave-keywords` + - 指定假期类型:传 `--leave-keywords "年假,病假"` + +### 阶段 1: 获取完整人员列表 + +**场景 A — 指定员工姓名**: +```bash +dws aisearch person --keyword "<员工姓名>" --dimension name --format json +``` + +**场景 B — 按部门查询**: +```bash +dws contact dept search --query "<部门名>" --format json +dws contact dept list-members --ids --format json +``` + +**场景 C — 多个部门**: 对每个部门分别执行 B,汇总去重。 + +**场景 D — 全公司**: 暂不支持,引导用户指定部门。 + +**场景 E — 用户已给 userId 列表**: 直接跳过本步。 + +### 阶段 2:调用脚本生成 Excel + +```bash +python scripts/attendance_vacation_balance.py \ + --users ,,... \ + [--leave-keywords "年假,病假,调休"] \ + [--out 假期余额列表.xlsx] +``` + +脚本内部自动执行: +1. `dws attendance vacation types --format json` 获取假期规则列表、`leaveCode`、展示单位、规则来源 `source` 和列顺序 +2. 对匹配到的每个假期规则,调用 `dws attendance vacation balance --users <批量用户> --leave-code <单个leaveCode> --format json` 查询余额;`vacation balance` 不支持 `--leave-keywords` +3. 处理特殊业务返回: + - 返回“假期类型没有余额”类业务错误:该 `leaveCode` 对应列填充 `不限制余额` + - 返回员工维度 `visible=false`:该员工 + 该 `leaveCode` 单元格填充 `不适用` + - 返回“员工未设置入职时间”或“员工未设置首次参加工作时间”:该员工 + 该 `leaveCode` 单元格填充 `不适用` + - `source=external` 的外部假期规则查询失败且不是权限错误:该员工 + 该 `leaveCode` 单元格填充 `外部规则暂无余额,需通过接口初始化更新余额` +4. `dws contact user get --ids <批量用户> --format json` 获取姓名、部门等信息 +5. 生成 Excel:`attendance_vacation_balance_.xlsx` + +### 阶段 3:返回结果 + +向用户返回脚本 stdout 摘要即可,必须包含: +- 输出文件路径 +- 员工数量 +- 假期规则列数 +- 如传了 `--leave-keywords`,说明筛选关键词 + +不要粘贴 Excel 全量内容。 + +## 错误处理 + +| 错误 | 处理方式 | +|------|---------| +| 权限不足 | 提示当前账号无权查询目标员工假期余额,需管理员或管理范围权限 | +| 人员范围缺失 | 追问员工或部门,禁止猜测 | +| 无假期规则 | 提示未匹配到假期规则,建议先执行 `dws attendance vacation types --format json` 验证 | +| 假期类型没有余额 | 不作为失败返回;按对应假期规则 `leaveCode` 填充 `不限制余额` | +| `visible=false` | 不作为失败返回;按对应员工 + 假期规则 `leaveCode` 填充 `不适用` | +| 员工未设置入职时间 / 首次参加工作时间 | 不作为失败返回;说明该规则依赖员工时间字段,按对应员工 + 假期规则 `leaveCode` 填充 `不适用` | +| 外部假期规则查询失败 | 当假期规则 `source=external` 且失败不是权限错误时,不作为导出失败;按对应员工 + 假期规则 `leaveCode` 填充 `外部规则暂无余额,需通过接口初始化更新余额` | +| openpyxl 缺失 | 提示执行 `pip install openpyxl` | +| 接口返回结构不确定 | 使用脚本 `--inspect` 重新执行一次,查看首条原始结构 | + +## 配套脚本 + +| 脚本 | 场景 | CLI 参数 | +|------|------|---------| +| [attendance_vacation_balance.py](../../scripts/attendance_vacation_balance.py) | 假期余额列表 Excel 导出;脚本按假期名称关键词筛选规则,并逐个 `leaveCode` 调用余额查询 | `--users [--leave-keywords] [--out] [--inspect]` | diff --git a/skills/multi/dingtalk-attendance/references/attendance.md b/skills/multi/dingtalk-attendance/references/attendance.md index dc6bd394..12ac86ab 100644 --- a/skills/multi/dingtalk-attendance/references/attendance.md +++ b/skills/multi/dingtalk-attendance/references/attendance.md @@ -1,6 +1,6 @@ # 考勤 (attendance) 命令参考 -> **【开源版命令可用性提示】** 当前开源 dws 已落地 P0 5 条命令:`attendance check result`、`attendance check record`、`attendance group search`、`attendance vacation balance`、`attendance vacation types`。文档中其余 P1 阶段命令(`class` / `overtime` / `adjustment` / `group settings` / `report` / `schedule` / `boss-check` 等共 28 条)暂未在当前开源二进制暴露,调用会返回 `unknown command`,将在后续批次落地。 +> **【命令可用性提示】** 当前 dws 已注册全部考勤子命令组(`record` / `check` / `approve` / `shift` / `schedule` / `class` / `adjustment` / `overtime` / `group` / `summary` / `rules` / `selfsetting` / `globalsetting` / `vacation` / `checkin` / `report` / `boss-check`)。查询与写操作大多可直接调用后端,不会再返回 `unknown command`,不要以"开源版不支持"为由拒答。个别命令返回受账号权限和组织数据影响:`report` 系列仅管理员可用;非管理员或数据为空时可能返回空列表或权限错误。执行前可用 `dws --help` 或 `--dry-run` 验证参数。 > **【必读】日期范围严格计算规则 — 所有含 --start/--end 或 --from/--to 的命令均适用** > @@ -97,21 +97,20 @@ Usage: dws attendance schedule import [flags] Example: dws attendance schedule import --group-id 123456 \ - --schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \ + --schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \ --yes Flags: - --group-id string 考勤组(必填,传入考勤组ID) - --schedules string 排班记录 JSON 数组(必填) - --yes 跳过确认提示 + --group-id string 考勤组(必填,传入考勤组ID);--help 主名为 --groupId,--group-id 为别名 + --schedules string 排班记录 JSON 数组(必填);--help 主名为 --scheduleVOS,--schedules 为别名 + --yes 跳过确认提示;--help 主名为 --user-say-yes,--yes 为别名 ``` 为排班制考勤组导入排班记录。`--schedules` 为 JSON 数组,每条记录包含: -- `userId`: 员工ID -- `classId`: 班次ID -- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22 -- `checkBeginTime`: 开始打卡时间 -- `checkEndTime`: 结束打卡时间 -- `isRest`: 是否休息日 Y/N(可选) +- `userId`: 员工ID(必填) +- `classId`: 班次ID(必填) +- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22(必填) +- `isRest`: 是否休息日 Y/N(**必填**,服务端要求传入) +- `checkBeginTime` / `checkEndTime`: 开始/结束打卡时间(可传,但当前不会进入后端 payload,实际打卡时段以 `classId` 对应班次为准) #### AI 调用 `schedule import` 的二次确认流程 @@ -259,7 +258,7 @@ Flags: --adjustment-id int 补卡规则主键 ID (必填) ``` -根据补卡规则主键 ID 查询对应的补卡规则详情。主键 ID 可从 `adjustment search` 返回结果中提取,也有可能来源于用户手动输入。**注意:已被删除或被更新覆盖的补卡规则无法查询到。** +**注意:本命令当前拿不到有效的补卡规则详情,不要依赖它。** 服务端对任意 `--adjustment-id`(含不存在的 ID)都返回同一个 `{"success":true,"有效期类型":"..."}`,不返回规则明细,也无法据此判断规则是否存在;且 `adjustment search` 返回的默认补卡规则 `entityVO.id` 为 `null`,`search → get` 取 id 的链路走不通。补卡规则内容请直接看 `adjustment search` 的返回结果。 ### 分页查询加班规则,支持按名称搜素 ``` @@ -534,12 +533,17 @@ Flags: Usage: dws attendance summary [flags] Example: - dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" + dws attendance summary --user USER_ID --date 2026-03-12 --stats-type week + dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type month Flags: - --date string 工作日期, 格式 yyyy-MM-dd HH:mm:ss (必填) - --user string 钉钉用户 ID (必填) + --date string 查询日期, 格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss (必填) + --stats-type string 统计类型: week 周统计 / month 月统计 (必填) + --tag-name string 标签名称 (可选) + --user string 钉钉用户 ID (必填) ``` +`summary` 必须同时传 `--user`、`--date`、`--stats-type`,缺一即报错(如 C0002)。`--stats-type` 只能是 `week`(周统计)或 `month`(月统计)。 + ### 查询考勤组与考勤规则 ``` Usage: @@ -801,10 +805,10 @@ Example: dws attendance vacation balance --users userId1,userId2 --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890 Flags: --users string 目标员工 ID 列表, 逗号分隔 (必填) - --leave-code string 假期规则 code (选填,不传则查询所有假期规则余额) + --leave-code string 假期规则 code (必填,服务端要求非空,不传返回 INVALID_PARAMS) ``` -调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取;不传 `--leave-code` 时查询所有假期规则余额。认证信息(corpId、opUserId)由系统自动注入。 +调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取。**注意:`--help` 虽标"选填",但服务端要求 `leaveCode` 非空,实际必填;不传会返回 `INVALID_PARAMS`(corpId、opUserId、leaveCode、targetUserIds 不能为空)。** 若要一次查所有假期规则余额,必须走 [attendance-vacation.md](./attendance-vacation.md) 工作流脚本逐个规则遍历。认证信息(corpId、opUserId)由系统自动注入。 如用户需要“所有假期规则余额 / 导出假期余额列表 / 所有假期规则余额 Excel / 按截图样式导出假期余额”,必须先读取 [attendance-vacation.md](./attendance-vacation.md),再按其中工作流调用脚本生成 Excel。 @@ -821,7 +825,7 @@ Flags: --end string 查询结束日期, 格式 YYYY-MM-DD (必填) ``` -调用 MCP 工具 get_leave_balance_records 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。 +调用 MCP 工具 get_leave_balance_records_v2 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 必填,可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。 ### 更新假期规则(写场景接口,必须走二次确认流程) @@ -956,7 +960,7 @@ Usage: dws attendance checkin records [flags] Example: dws attendance checkin records \ - --operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00" + --operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00" Flags: --end string 结束时间, 格式 yyyy-MM-dd HH:mm:ss(必填) --operator-corp-id string 操作者企业 ID(必填) @@ -986,7 +990,7 @@ Flags: 用户说"班次详情/某个班次的具体信息" → `class search --name "..."`(search 直出,直接返回详情)。`class get` 仅在需要按已知 classId 精确查询时使用 用户说"更新班次/修改班次/班次改名/修改上下班时间" → `class update` 用户说"补卡规则/补卡设置" → `adjustment search`(返回结果已包含全量属性,无需再调 get) -用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出)。`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用 +用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出,已含全量属性)。**不要用 `adjustment get`**:当前服务端对任意 id 都只返回"有效期类型"、拿不到规则明细 用户说"加班规则/加班设置/加班计算" → `overtime search`(返回结果已包含全量属性,无需再调 get) 用户说"加班规则详情/某条加班规则的具体信息" → `overtime search --name "..."`(search 直出)。如需查已删除/被覆盖的历史记录 → `overtime get` 用户说"考勤组列表/有哪些考勤组" → `group search` @@ -1022,7 +1026,7 @@ Flags: ```bash # 导入排班记录 dws attendance schedule import --group-id 123456 \ - --schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \ + --schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \ --yes --format json # 获取排班记录 — 禁止直接调用,必须走 attendance-schedule.md 排班查询导出工作流 @@ -1078,7 +1082,7 @@ dws attendance group create --name "研发考勤组" --type FIXED --group-vo '{" dws attendance group create --name "自由工时分组" --type NONE --timeout 10 --format json # 查看考勤统计摘要 -dws attendance summary --user --date "2026-03-12 15:00:00" --format json +dws attendance summary --user --date 2026-03-12 --stats-type week --format json # 查看考勤组和规则 dws attendance rules --date 2026-03-14 --format json @@ -1156,7 +1160,7 @@ dws attendance vacation save-balance --target user001 \ --num 8 --reason "绩效奖励发放3天" --format json # 查询签到记录 -dws attendance checkin records --operator-staff-id op001 --staff-ids user001,user002 \ +dws attendance checkin records --operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 \ --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00" --format json ``` @@ -1187,7 +1191,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use - `class get` 的 `--class-id` 必填,班次 ID 可从 `class search` 结果中提取 - `class search` 返回结果已包含全量属性,无需再调用 `class get`;`class get` 仅在需要按已知 classId 精确查询时使用 - `class update` 的 `--class-id` 必填,其余均可选,仅需对要修改的字段赋值,未传字段会自动从已有配置补充;由于保存班次耗时较久,建议加 `--timeout 10` -- `adjustment search` 返回结果已包含全量属性,无需再调用 `adjustment get`;`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用 +- `adjustment search` 返回结果已包含全量属性;`adjustment get` 当前服务端对任意 id 都只返回"有效期类型"、无规则明细,不可用,补卡规则详情一律看 `adjustment search` 返回 - `overtime search` 返回结果已包含全量属性,无需再调用 `overtime get`;`overtime get` 仅在需要按已知 overtimeId 查询时使用(包括已删除/被覆盖的历史记录) - `adjustment search` / `overtime search` 分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20 - `group search` 的分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20 @@ -1196,7 +1200,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use - `group update` 的 --group-id 必填,其余均可选,至少需指定一个修改项;仅需对要修改的字段赋値,未传字段会从已有配置自动补充;修改打卡地址/wifi/蓝牙等复杂子对象时用 `--group-vo` 传入完整 JSON;`--group-vo` 与单字段 flag 同时传入时单字段 flag 优先级更高 - `group create` 的 `--name` 和 `--type` 必填,`--type` 必须为 FIXED/TURN/NONE 之一;type=FIXED 时 `--group-vo` 必须包含 `workDayClassList`(非空)和 `defaultClassId`(非 null);由于保存考勤组耗时较久,建议加 `--timeout 10` - `group filtered-get` 的 `--group-id` 必填,`--member/--position/--wifi/--bles` 均可选,默认 false。**返回结果中如含成员 userId 列表,必须调用 `dws contact user get --ids ,,...`(支持逗号分隔传多个 ID),将 userId 转换为员工姓名后再输出;不得直接输出裸 userId。** -- `summary` 的 `--date` 格式: yyyy-MM-dd HH:mm:ss(如 `2026-03-12 15:00:00`) +- `summary` 必须同时传 `--user`、`--date`、`--stats-type`(week 周统计 / month 月统计),三者缺一即报错;`--date` 支持 `YYYY-MM-DD` 或 `yyyy-MM-dd HH:mm:ss`;`--tag-name` 可选 - `rules` 的 `--date` 支持 YYYY-MM-DD 或 yyyy-MM-dd HH:mm:ss 两种格式 - `selfsetting get/save` 的 `--setting-scene` 必须是 `checkRemind`、`fastCheck`、`checkResultNotify`、`lackRemind`、`personalAttendStatNotify`、`bossAttendStatNotify` 之一 - `selfsetting get/save` 的 MCP 入参 `userId` 为必填;CLI 的 `--user` 也必填,必须显式传入目标用户 ID @@ -1209,8 +1213,8 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use - 用户 ID 需从 `contact user get-self` 或 `aisearch person` 获取 - 考勤组 ID 需从 `rules` 命令返回结果中获取 - `vacation types` 无需任何参数,认证信息自动注入 -- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 选填,可通过 `vacation types` 获取 -- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 选填 +- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 服务端要求非空、实际必填(`--help` 标"选填"不准),不传返回 `INVALID_PARAMS`,可通过 `vacation types` 获取 +- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 必填(不传无法查询),底层 MCP 工具为 `get_leave_balance_records_v2` - `vacation balance` 和 `vacation records` 的认证参数(corpId、opUserId)由系统自动注入,无需手动传入 - `vacation update-type` 的 `--leave-code` 必填;其他字段均为可选,但至少需传一个更新字段 - `vacation update-type` 的 `--visibility-rules` 为 JSON 数组字符串,格式:`[{"type":"dept","visible":["1","2","3"]}]`,type 可取值 staff/label/dept diff --git a/skills/multi/dingtalk-attendance/scripts/attendance_my_record.py b/skills/multi/dingtalk-attendance/scripts/attendance_my_record.py index 89898507..88f6cd97 100644 --- a/skills/multi/dingtalk-attendance/scripts/attendance_my_record.py +++ b/skills/multi/dingtalk-attendance/scripts/attendance_my_record.py @@ -48,7 +48,25 @@ def get_my_user_id(dry_run: bool = False) -> Optional[str]: return '' if not data or not isinstance(data, dict): return None - return data.get('userId') or data.get('userid') + # 兼容两种结构: + # 1) 顶层直接给 userId + # 2) {result: [{orgEmployeeModel: {userId}}]} 包裹 + uid = data.get('userId') or data.get('userid') + if uid: + return uid + inner = data.get('result') + if isinstance(inner, dict): + inner = [inner] + if isinstance(inner, list): + for item in inner: + if not isinstance(item, dict): + continue + emp = item.get('orgEmployeeModel') + if isinstance(emp, dict) and emp.get('userId'): + return emp['userId'] + if item.get('userId'): + return item['userId'] + return None def main(): diff --git a/skills/multi/dingtalk-attendance/scripts/attendance_report_checkin.py b/skills/multi/dingtalk-attendance/scripts/attendance_report_checkin.py new file mode 100644 index 00000000..6b362c7c --- /dev/null +++ b/skills/multi/dingtalk-attendance/scripts/attendance_report_checkin.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python3 +""" +考勤报表导出 — 签到记录粒度 + +[AI Agent 强制门禁] 调用本脚本前必须先阅读: + dingtalk-workspace/references/products/attendance-report.md + + 本脚本是"签到报表导出工作流"的执行末端,工作流完整定义在 attendance-report.md。 + + [严禁] 仅凭本脚本 docstring 或 --help 输出就直接拼命令执行。 + +导出签到报表:每条签到记录一行,包含签到详情(地点、经纬度、拜访客户、图片等)。 + +前置依赖: + pip install openpyxl + +用法: + python attendance_report_checkin.py \ + --users userId1,userId2,... \ + --start "2026-04-01 00:00:00" \ + --end "2026-04-07 23:59:59" \ + [--out 签到报表_研发部_20260401_20260407.xlsx] + [--inspect] +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from datetime import datetime, timedelta +from typing import Any + +# ── 前置依赖检查(在任何 dws 调用之前就检测,避免查完数据才报错)─────── +_missing_deps: list[str] = [] +try: + import openpyxl as _openpyxl_check # noqa: F401 +except ImportError: + _missing_deps.append("openpyxl") +try: + import requests as _requests_check # noqa: F401 +except ImportError: + _missing_deps.append("requests") +try: + from PIL import Image as _pil_check # noqa: F401 +except ImportError: + _missing_deps.append("Pillow") + +if _missing_deps: + print( + f"[ERROR] 缺少以下依赖:{', '.join(_missing_deps)}\n" + f" 请先安装:pip install {' '.join(_missing_deps)}\n" + "安装后重新执行本脚本。\n" + "(签到报表需要 openpyxl 生成 Excel、requests + Pillow 下载并嵌入签到图片)", + file=sys.stderr, + ) + sys.exit(2) + +import attendance_report_common as cmn + +# ───────────────────────────────────────────────────────────────────────────── +# 自动获取当前认证的 operator 信息 +# ───────────────────────────────────────────────────────────────────────────── + +def _get_operator_context() -> tuple[str, str]: + """ + 从 `dws auth status --format json` 自动获取当前认证的 corp_id 和 user_id。 + + 签到接口 (checkin records) 必须传 --operator-corp-id 和 --operator-staff-id, + 这两个值来自 dws 的认证上下文(即 `dws auth status` 返回的 corp_id / user_id), + 而非 `dws contact user get-self` 返回的长格式 userId。 + + Returns: + (operator_corp_id, operator_staff_id) 元组 + + Raises: + SystemExit: 未登录或无法获取认证信息时直接退出 + """ + try: + result = subprocess.run( + ["dws", "auth", "status", "--format", "json"], + capture_output=True, + text=True, + timeout=30, + ) + except FileNotFoundError: + cmn.error("未找到 dws 命令,请确认 dws CLI 已安装并在 PATH 中") + sys.exit(2) + except subprocess.TimeoutExpired: + cmn.error("dws auth status 超时,请检查网络或重新登录(dws auth login)") + sys.exit(2) + + if result.returncode != 0: + cmn.error( + "获取认证信息失败,请确保已执行 dws auth login 完成登录。\n" + f" 错误详情:{(result.stderr or result.stdout or '').strip()}" + ) + sys.exit(2) + + try: + auth_data = json.loads(result.stdout) + except json.JSONDecodeError: + cmn.error(f"dws auth status 返回非 JSON:{result.stdout[:200]!r}") + sys.exit(2) + + corp_id = auth_data.get("corp_id") or auth_data.get("corpId") or "" + user_id = auth_data.get("user_id") or auth_data.get("userId") or "" + + if not corp_id or not user_id: + cmn.error( + "无法从认证信息中提取 corp_id / user_id,请重新登录:\n" + " dws auth login\n" + f" 当前返回:{json.dumps(auth_data, ensure_ascii=False)[:300]}" + ) + sys.exit(2) + + cmn.log(f"[auth] 已获取 operator 信息:corp_id={corp_id}, user_id={user_id}") + return str(corp_id), str(user_id) + + +# 签到接口限制:开始到结束最多 7 天 +MAX_DAYS_PER_CHECKIN_SLICE = 7 + +# 签到接口限制:每次最多查 100 人(与 check record 一致) +MAX_USERS_PER_CHECKIN_BATCH = 50 + +# 最多支持 9 张图片列 +MAX_IMAGE_COLUMNS = 9 + +# 报表表头(与用户要求严格对齐) +REPORT_HEADERS = [ + "姓名", "部门", "完整部门", + "日期", "时间", + "经度", "纬度", "地点", "详细地址", + "拜访客户", "客户部门名称", "工作内容", + "手机标识", +] + [f"图片{i}" for i in range(1, MAX_IMAGE_COLUMNS + 1)] + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "导出签到报表 — 签到记录粒度。" + "[强制] AI Agent 必须先读 references/products/attendance-report.md 再调用本脚本。" + ), + ) + parser.add_argument("--users", required=True, + help="userId 列表,逗号分隔(必填)") + parser.add_argument("--start", required=True, + help='开始时间,YYYY-MM-DD 或 "YYYY-MM-DD HH:mm:ss"(必填)') + parser.add_argument("--end", required=True, + help='结束时间,YYYY-MM-DD 或 "YYYY-MM-DD HH:mm:ss"(必填)') + parser.add_argument("--out", default="", + help="输出 xlsx 文件名;不传则按规范自动生成") + parser.add_argument("--inspect", action="store_true", + help="首次跑时打印首条记录原始结构(用于核对真实字段)") + return parser.parse_args() + + +# ───────────────────────────────────────────────────────────────────────────── +# 签到接口时间切片(7 天一段) +# ───────────────────────────────────────────────────────────────────────────── + +def slice_checkin_date_range( + start: datetime, end: datetime, +) -> list[cmn.DateSlice]: + """将日期范围按 7 天一段切片(签到接口限制开始到结束最多 7 天)。""" + slices: list[cmn.DateSlice] = [] + current = start + while current <= end: + slice_end = min(current + timedelta(days=MAX_DAYS_PER_CHECKIN_SLICE - 1), end) + # 确保 slice_end 的时间部分是当天最后一秒 + slice_end = slice_end.replace(hour=23, minute=59, second=59) + if slice_end > end: + slice_end = end + slices.append(cmn.DateSlice( + start=current, + end=slice_end, + )) + current = slice_end.replace(hour=0, minute=0, second=0) + timedelta(days=1) + return slices + + +def chunk_checkin_users(user_ids: list[str]) -> list[list[str]]: + """将用户列表按 MAX_USERS_PER_CHECKIN_BATCH 分批。""" + batches: list[list[str]] = [] + for i in range(0, len(user_ids), MAX_USERS_PER_CHECKIN_BATCH): + batches.append(user_ids[i:i + MAX_USERS_PER_CHECKIN_BATCH]) + return batches + + +# ───────────────────────────────────────────────────────────────────────────── +# 签到数据查询 +# ───────────────────────────────────────────────────────────────────────────── + +def query_checkin_batch( + user_batch: list[str], + date_slice: cmn.DateSlice, + operator_corp_id: str, + operator_staff_id: str, + stats: cmn.CallStats, + *, + inspect: bool = False, + inspected_flag: list[bool] | None = None, +) -> list[dict]: + """查询一批用户在一个时间片内的签到记录。""" + cmn.log( + f"[checkin] users={len(user_batch)} " + f"slice={date_slice.label}" + ) + try: + payload = cmn.run_dws([ + "attendance", "checkin", "records", + "--operator-corp-id", operator_corp_id, + "--operator-staff-id", operator_staff_id, + "--staff-ids", ",".join(user_batch), + "--start", date_slice.start_str, + "--end", date_slice.end_str, + ]) + stats.total_dws_calls += 1 + except cmn.DwsCallError as exc: + stats.total_dws_calls += 1 + stats.failed_calls += 1 + if exc.is_permission_error: + cmn.error( + "权限错误:当前账号无管理员权限,无法导出签到报表。\n" + "请联系考勤管理员或换号重试。" + ) + raise SystemExit(2) from exc + err_msg = str(exc) + if "missing required flag" in err_msg.lower(): + cmn.error( + "签到接口调用失败:缺少必需参数。\n" + "请确保已执行 dws auth login 完成登录,以便自动获取 operator 参数。\n" + f"当前 operator: corp_id={operator_corp_id}, staff_id={operator_staff_id}\n" + f"原始错误:{err_msg}" + ) + raise SystemExit(2) from exc + stats.add_warning(f"[checkin failed] {date_slice.label}: {exc}") + return [] + + records = cmn.extract_records(payload) + if inspect and records and inspected_flag is not None and not inspected_flag[0]: + cmn.dump_first_record_for_inspection(records, "checkin-records") + inspected_flag[0] = True + return records + + +# ───────────────────────────────────────────────────────────────────────────── +# 签到记录 → 报表行转换 +# ───────────────────────────────────────────────────────────────────────────── + +def _format_timestamp(timestamp_value: Any) -> tuple[str, str]: + """ + 将签到时间戳转换为 (日期字符串, 时间字符串)。 + + 签到接口的 timestamp 为毫秒时间戳。 + """ + if timestamp_value is None: + return "", "" + try: + ts = float(timestamp_value) + # 判断是毫秒还是秒级时间戳 + if ts > 1_000_000_000_000: + ts = ts / 1000 + dt = datetime.fromtimestamp(ts) + return dt.strftime("%Y-%m-%d"), dt.strftime("%H:%M:%S") + except (ValueError, TypeError, OSError, OverflowError): + return str(timestamp_value), "" + + +def transform_records_to_rows( + records: list[dict], + user_info_map: dict[str, cmn.UserInfo], +) -> list[list[Any]]: + """将签到原始记录转换为报表行(与 REPORT_HEADERS 对齐)。""" + rows: list[list[Any]] = [] + for record in records: + uid = cmn._first_nonempty(record, ("userId", "userid", "user_id")) + uid_str = str(uid) if uid is not None else "" + info = user_info_map.get(uid_str, cmn.UserInfo(name=uid_str)) + + # 姓名:优先用 resolve_user_info 的结果,回退到接口返回的 name + name = info.name or record.get("name", uid_str) + dept_name = info.dept_name + # 完整部门:暂用 dept_name(如需更完整的路径可后续扩展) + full_dept = dept_name + + # 日期与时间 + date_str, time_str = _format_timestamp(record.get("timestamp")) + + # 经纬度 + longitude = record.get("longitude", "") + latitude = record.get("latitude", "") + + # 地点 + place = record.get("place", "") + detail_place = record.get("detailPlace", "") + + # 拜访客户 & 客户部门名称 + customers = record.get("customers", "") + # 签到接口暂无客户部门名称字段,预留空值 + customer_dept = "" + + # 工作内容(备注) + remark = record.get("remark", "") + + # 手机标识 + mobile_id = record.get("mobileId", "") + + # 图片列(最多 9 张) + image_list = record.get("imageList") or [] + if isinstance(image_list, str): + # 兼容接口可能返回逗号分隔的字符串 + image_list = [img.strip() for img in image_list.split(",") if img.strip()] + image_cells = [] + for i in range(MAX_IMAGE_COLUMNS): + if i < len(image_list): + image_cells.append(image_list[i]) + else: + image_cells.append("") + + row = [ + name, dept_name, full_dept, + date_str, time_str, + longitude, latitude, place, detail_place, + customers, customer_dept, remark, + mobile_id, + ] + image_cells + rows.append(row) + + return rows + + +# ───────────────────────────────────────────────────────────────────────────── +# main +# ───────────────────────────────────────────────────────────────────────────── + +def main() -> int: + args = parse_args() + + raw_ids = [u.strip() for u in args.users.split(",") if u.strip()] + if not raw_ids: + cmn.error("--users 不能为空") + return 2 + + # 自动识别部门ID并展开为员工userId + user_ids = cmn.resolve_users_from_input(raw_ids) + if not user_ids: + cmn.error("未能解析出任何有效的员工userId") + return 2 + cmn.log(f"[users] 最终用户列表:{len(user_ids)} 人") + + try: + start = cmn.parse_datetime_arg(args.start, end_of_day=False) + end = cmn.parse_datetime_arg(args.end, end_of_day=True) + except ValueError as exc: + cmn.error(str(exc)) + return 2 + + if end < start: + cmn.error(f"--end ({end}) 早于 --start ({start})") + return 2 + + # 获取当前认证的 operator 信息(签到接口必需) + operator_corp_id, operator_staff_id = _get_operator_context() + + # 获取用户基础信息(姓名、部门) + cmn.log(f"[users] 获取 {len(user_ids)} 个用户基础信息") + user_info_map = cmn.resolve_user_info(user_ids) + + # 分批分段查询签到记录 + user_batches = chunk_checkin_users(user_ids) + date_slices = slice_checkin_date_range(start, end) + stats = cmn.CallStats( + user_batches=len(user_batches), + date_slices=len(date_slices), + ) + cmn.log( + f"[plan] 共 {len(user_batches)} 批 × {len(date_slices)} 个时间片 " + f"= {len(user_batches) * len(date_slices)} 次接口调用" + ) + + inspected_flag = [False] + all_records: list[dict] = [] + for batch_idx, batch in enumerate(user_batches, start=1): + for slice_idx, date_slice in enumerate(date_slices, start=1): + cmn.log( + f"[batch {batch_idx}/{len(user_batches)}] " + f"[slice {slice_idx}/{len(date_slices)}]" + ) + records = query_checkin_batch( + batch, date_slice, + operator_corp_id, operator_staff_id, + stats, + inspect=args.inspect, + inspected_flag=inspected_flag, + ) + all_records.extend(records) + + if not all_records: + stats.add_warning("查询完成,但未得到任何签到记录") + + # 转换为报表行 + rows = transform_records_to_rows(all_records, user_info_map) + + # 按日期时间排序(日期列索引=3,时间列索引=4) + rows.sort(key=lambda r: (r[3] or "", r[4] or "")) + + # 生成 Excel + out_name = args.out or cmn.build_output_filename(start, end, suffix="checkin") + title = ( + f"签到报表 统计日期:{start.strftime(cmn.DATE_FMT)} " + f"至 {end.strftime(cmn.DATE_FMT)}" + ) + subtitle = f"报表生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}" + + # 图片列名列表(图片1~图片9),让 write_excel_multi_sheets 自动将 URL 嵌入为缩略图 + image_column_names = [f"图片{i}" for i in range(1, MAX_IMAGE_COLUMNS + 1)] + + checkin_sheet = { + "name": "签到记录", + "headers": REPORT_HEADERS, + "rows": rows, + "title": title, + "subtitle": subtitle, + "image_columns": image_column_names, + "image_size": (60, 60), + } + + try: + cmn.write_excel_multi_sheets(out_name, [checkin_sheet]) + except (RuntimeError, ValueError) as exc: + cmn.error(str(exc)) + return 1 + + cmn.print_summary( + granularity_label="签到报表", + out_path=out_name, + user_count=len(user_ids), + column_names=[h for h in REPORT_HEADERS], + start=start, + end=end, + rows_count=len(rows), + stats=stats, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/multi/dingtalk-attendance/scripts/attendance_report_record.py b/skills/multi/dingtalk-attendance/scripts/attendance_report_record.py new file mode 100644 index 00000000..84c57647 --- /dev/null +++ b/skills/multi/dingtalk-attendance/scripts/attendance_report_record.py @@ -0,0 +1,947 @@ +#!/usr/bin/env python3 +""" +考勤记录报表导出脚本 — 补卡/出差/外出/请假 + +属于考勤报表导出体系,和 attendance_report_detail.py / attendance_report_monthly.py 平级。 +Agent 负责意图判断和人员获取,本脚本自包含:数据查询 → 解析 → Excel 生成。 + +数据链路: + 1. dws attendance approve list --users --types --start --end + → 获取审批单摘要(含 originId = processInstanceId) + 2. dws oa approval detail --instance-id + → 获取审批单完整表单字段(extValue / detailList) + 3. 解析 formValueVOS 中的 DDHolidayField / extValue → 按天拆分行 + 4. write_excel 输出 + +用法: + python attendance_report_record.py --type leave --users --start 2026-04-01 --end 2026-04-30 + python attendance_report_record.py --type trip --users --start 2026-04-01 --end 2026-04-30 + python attendance_report_record.py --type out --users --start 2026-05-01 --end 2026-05-31 + python attendance_report_record.py --type patch --users --start 2026-05-01 --end 2026-05-31 + +支持类型: leave(请假), trip(出差), out(外出), patch(补卡) +""" + +from __future__ import annotations + +import argparse +import json +import sys +import os +from datetime import datetime +from typing import Any + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from attendance_report_common import ( + run_dws, + write_excel, + resolve_user_names, + resolve_user_info, + UserInfo, + log, + warn, + error, + DwsCallError, + DATE_FMT, +) + +# ───────────────────────────────────────────────────────────────────────────── +# 常量 +# ───────────────────────────────────────────────────────────────────────────── + +SUPPORTED_TYPES = ("leave", "trip", "out", "patch") + +COLUMNS: dict[str, list[str]] = { + "leave": ["姓名", "考勤组", "部门", "工号", "职位", "假期类型", "请假时间", + "请假时长(小时)", "请假时长(天)", "关联审批单", "审批单状态"], + "trip": ["姓名", "考勤组", "部门", "工号", "职位", "出差时间", + "出差时长", "出差单位", "关联审批单", "审批单状态"], + "out": ["姓名", "考勤组", "部门", "工号", "职位", "外出申请时间", + "外出时长(小时)", "外出时长(天)", "关联审批单", "审批单状态"], + "patch": ["姓名", "考勤组", "部门", "工号", "职位", "考勤日期", "考勤时间", + "原打卡时间", "原考勤状态", "补卡时间", "补卡结果", "关联审批单", "审批单状态"], +} + +SHEET_NAMES: dict[str, str] = { + "leave": "请假记录", + "trip": "出差记录", + "out": "外出记录", + "patch": "补卡记录", +} + +STATUS_MAP: dict[str, dict[str, str]] = { + "COMPLETED": {"agree": "审批通过", "refuse": "已拒绝"}, + "RUNNING": {"": "审批中"}, + "TERMINATED": {"": "已撤销"}, +} + +APPROVE_LIST_BATCH_SIZE = 50 # attendance approve list 单次最多用户数 + +# 审批详情页 URL 模板 +# 内层:aflow 审批详情页 +_AFLOW_URL_TEMPLATE = ( + "https://aflow.dingtalk.com/dingtalk/mobile/homepage.htm" + "?corpid={corp_id}&dd_share=false&showmenu=true&back=native" + "#/approval?procInstId={instance_id}" +) +# 外层:dingtalk schema 协议,在钉钉客户端侧边面板打开 +_DINGTALK_SCHEMA_TEMPLATE = ( + "dingtalk://dingtalkclient/action/openapp" + "?corpid={corp_id}&container_type=slide_panel&app_id=-4" + "&&redirect_url={encoded_url}" +) + + +class HyperlinkCell: + """标记单元格为超链接:Excel 中显示 label 文本,点击跳转到 url。""" + + __slots__ = ("label", "url") + + def __init__(self, label: str, url: str): + self.label = label + self.url = url + + def __str__(self) -> str: + return self.label + + +def build_approve_url(corp_id: str, instance_id: str) -> str: + """ + 构建审批单跳转链接(dingtalk:// schema)。 + + 结构:外层 dingtalk schema 打开钉钉侧边面板,内部 redirect 到 aflow 审批详情页。 + """ + from urllib.parse import quote + + inner_url = _AFLOW_URL_TEMPLATE.format(corp_id=corp_id, instance_id=instance_id) + encoded_url = quote(inner_url, safe="") + return _DINGTALK_SCHEMA_TEMPLATE.format(corp_id=corp_id, encoded_url=encoded_url) + + +def build_approve_cell(corp_id: str, instance_id: str, title: str = "") -> HyperlinkCell | str: + """ + 构建"关联审批单"列的单元格值。 + + 如果有 corp_id 和 instance_id,返回 HyperlinkCell(Excel 中为可点击链接)。 + 否则返回纯文本。 + """ + if not instance_id: + return "" + label = title or instance_id + if not corp_id: + return label + url = build_approve_url(corp_id, instance_id) + return HyperlinkCell(label=label, url=url) + + +# ───────────────────────────────────────────────────────────────────────────── +# 工具函数 +# ───────────────────────────────────────────────────────────────────────────── + +def normalize_am_pm(text: str) -> str: + """将时间文本中的 AM/PM 替换为 上午/下午。""" + return text.replace(" PM", " 下午").replace(" AM", " 上午") + + +def format_status(status: str, result: str) -> str: + """将 status + processInstanceResult 转为中文状态。""" + status_upper = (status or "").upper() + result_lower = (result or "").lower() + group = STATUS_MAP.get(status_upper, {}) + return group.get(result_lower, group.get("", f"{status}/{result}")) + + +def ms_to_datetime(ms: int | float | None) -> datetime | None: + """毫秒时间戳转 datetime。""" + if not ms: + return None + try: + return datetime.fromtimestamp(int(ms) / 1000) + except (OSError, ValueError, OverflowError): + return None + + +def ms_to_time_str(ms: int | float | None) -> str: + """毫秒时间戳转 HH:MM。""" + dt = ms_to_datetime(ms) + return dt.strftime("%H:%M") if dt else "" + + +def ms_to_date_str(ms: int | float | None) -> str: + """毫秒时间戳转 YYYY-MM-DD。""" + dt = ms_to_datetime(ms) + return dt.strftime(DATE_FMT) if dt else "" + + +def format_day_type(detail: dict) -> str: + """从 detailList 单条判断日历类型。""" + day_type = detail.get("dayType", "") + is_rest = detail.get("isRest", False) + if day_type == "workDay" or (not is_rest and not day_type): + return "工作日" + if day_type == "restDay" or is_rest: + return "休息日" + if day_type == "holiday": + return "节假日" + return day_type or ("休息日" if is_rest else "工作日") + + +def format_class_time(detail: dict) -> str: + """从 detailList 单条提取上下班时间。""" + class_info = detail.get("classInfo", {}) + sections = class_info.get("sections", []) if class_info else [] + if not sections: + return "未排班" + section = sections[0] + start_time = ms_to_time_str(section.get("startTime")) + end_time = ms_to_time_str(section.get("endTime")) + if start_time and end_time: + return f"{start_time} ~ {end_time}" + return "未排班" + + +# ───────────────────────────────────────────────────────────────────────────── +# 数据查询 +# ───────────────────────────────────────────────────────────────────────────── + +# 钉钉接口把"外出"和"出差"都归类到 trip(bizType=2),out 类型查不到数据。 +# 脚本通过 tagName 区分:tagName="出差" → trip,tagName="外出" → out。 +_API_TYPE_MAP: dict[str, str] = { + "leave": "leave", + "trip": "trip", + "out": "trip", # 外出也用 trip 查询,再按 tagName 过滤 + "patch": "patch", +} + +_TAG_FILTER: dict[str, str | None] = { + "leave": None, + "trip": "出差", + "out": "外出", + "patch": None, +} + + +def fetch_approve_list(user_ids: list[str], record_type: str, start: str, end: str) -> list[dict]: + """ + 分批调用 dws attendance approve list 获取审批单摘要。 + + 返回列表中每条包含: userId, tagName, duration, durationUnit, beginTime, endTime, originId。 + 对于 out 类型,实际用 trip 查询接口,再按 tagName="外出" 过滤; + 对于 trip 类型,按 tagName="出差" 过滤(排除外出记录)。 + """ + api_type = _API_TYPE_MAP.get(record_type, record_type) + tag_filter = _TAG_FILTER.get(record_type) + + all_records: list[dict] = [] + for i in range(0, len(user_ids), APPROVE_LIST_BATCH_SIZE): + batch = user_ids[i:i + APPROVE_LIST_BATCH_SIZE] + users_str = ",".join(batch) + try: + result = run_dws([ + "attendance", "approve", "list", + "--users", users_str, + "--types", api_type, + "--start", start, + "--end", end, + ]) + records: list[dict] = [] + if isinstance(result, list): + records = result + elif isinstance(result, dict): + records = result.get("approveList", result.get("list", [])) + if not isinstance(records, list): + records = [] + # 按 tagName 过滤 + if tag_filter: + records = [r for r in records if r.get("tagName") == tag_filter] + all_records.extend(records) + except DwsCallError as e: + warn(f"查询审批列表失败(batch {i // APPROVE_LIST_BATCH_SIZE + 1}): {e}") + return all_records + + +def fetch_detail(instance_id: str) -> dict | None: + """调用 dws oa approval detail 获取审批单完整详情。""" + try: + result = run_dws([ + "oa", "approval", "detail", + "--instance-id", instance_id, + ]) + return result if isinstance(result, dict) else None + except DwsCallError as e: + warn(f"获取审批详情失败({instance_id[:20]}...): {e}") + return None + + +# ───────────────────────────────────────────────────────────────────────────── +# 解析器 +# ───────────────────────────────────────────────────────────────────────────── + +def find_holiday_field(form_values: list[dict]) -> dict | None: + """从 formValueVOS 中查找 DDHolidayField 组件。""" + for fv in form_values: + if fv.get("componentType") == "DDHolidayField": + return fv + return None + + +def parse_ext_value(field_data: dict) -> dict: + """解析字段的 extValue JSON 字符串。""" + ext_str = field_data.get("extValue") or "" + if not ext_str: + return {} + try: + return json.loads(ext_str) + except (json.JSONDecodeError, TypeError): + return {} + + +def parse_leave_detail(detail: dict, name_map: dict[str, str], *, + user_info_map: dict[str, "UserInfo"] | None = None, + group_map: dict[str, str] | None = None, + corp_id: str = "", + ) -> list[list[str]]: + """解析请假审批单。""" + form_values = detail.get("formValueVOS", []) + user_id = detail.get("originatorUserid", "") + dept_name = detail.get("originatorDeptName", "") + instance_id = detail.get("processInstanceId", "") + status = format_status(detail.get("status", ""), detail.get("processInstanceResult", "")) + + # 用户基础信息 + info = (user_info_map or {}).get(user_id) + user_name = info.name if info else name_map.get(user_id, user_id) + dept = info.dept_name if info and info.dept_name else dept_name + job_number = info.job_number if info else "" + title = info.title if info else "" + group_name = (group_map or {}).get(user_id, "") + approve_cell = build_approve_cell(corp_id, instance_id, f"{user_name}提交的请假审批单") + + holiday_field = find_holiday_field(form_values) + if not holiday_field: + return [[user_name, group_name, dept, job_number, title, + "", "", "", "", approve_cell, status]] + + # value: ["开始时间","结束时间",天数,"单位","假期类型","请假类型"] + value_str = holiday_field.get("value", "") + leave_type = "" + leave_time = "" + try: + value_arr = json.loads(value_str) + if isinstance(value_arr, list) and len(value_arr) >= 2: + leave_time = normalize_am_pm(f"{value_arr[0]} ~ {value_arr[1]}") + if len(value_arr) > 4: + leave_type = str(value_arr[4]) + except (json.JSONDecodeError, TypeError): + leave_time = normalize_am_pm(value_str) + + ext = parse_ext_value(holiday_field) + duration_day = str(ext.get("durationInDay", "")) + duration_hour = str(ext.get("durationInHour", "")) + + return [[user_name, group_name, dept, job_number, title, + leave_type, leave_time, duration_hour, duration_day, + approve_cell, status]] + + +def _extract_time_duration_from_fields(form_values: list[dict]) -> tuple[str, str, str, str]: + """ + 从独立表单字段中提取时间范围和时长。 + + 适用于外出/出差表单的非 DDHolidayField 结构: + - startTime (DDDateField) + finishTime (DDDateField) → 时间范围 + - duration (NumberField) → extValue 中含 durationInDay / durationInHour + + Returns: (time_range, duration_hour, duration_day, ext_from_duration) + """ + start_time = "" + end_time = "" + duration_hour = "" + duration_day = "" + + for fv in form_values: + biz_alias = (fv.get("bizAlias") or "").lower() + name = (fv.get("name") or "").lower() + value = fv.get("value") or "" + + # 开始时间 + if biz_alias in ("starttime", "start_time") or "开始时间" in name: + if value and not start_time: + start_time = value + # 结束时间 + if biz_alias in ("finishtime", "finish_time", "endtime", "end_time") or "结束时间" in name: + if value and not end_time: + end_time = value + # 时长字段 — extValue 中有 durationInDay / durationInHour + if biz_alias == "duration" or "时长" in name: + ext = parse_ext_value(fv) + if ext: + duration_day = str(ext.get("durationInDay", "")) + duration_hour = str(ext.get("durationInHour", "")) + + time_range = "" + if start_time and end_time: + time_range = f"{start_time} ~ {end_time}" + elif start_time: + time_range = start_time + + return time_range, duration_hour, duration_day + + +def parse_out_detail(detail: dict, name_map: dict[str, str], *, + user_info_map: dict[str, "UserInfo"] | None = None, + group_map: dict[str, str] | None = None, + corp_id: str = "", + ) -> list[list[str]]: + """解析外出审批单。兼容 DDHolidayField 和独立字段两种表单结构。""" + form_values = detail.get("formValueVOS", []) + user_id = detail.get("originatorUserid", "") + dept_name = detail.get("originatorDeptName", "") + instance_id = detail.get("processInstanceId", "") + status = format_status(detail.get("status", ""), detail.get("processInstanceResult", "")) + + # 用户基础信息 + info = (user_info_map or {}).get(user_id) + user_name = info.name if info else name_map.get(user_id, user_id) + dept = info.dept_name if info and info.dept_name else dept_name + job_number = info.job_number if info else "" + title = info.title if info else "" + group_name = (group_map or {}).get(user_id, "") + approve_cell = build_approve_cell(corp_id, instance_id, f"{user_name}提交的外出审批单") + + # 优先尝试 DDHolidayField + holiday_field = find_holiday_field(form_values) + if holiday_field: + value_str = holiday_field.get("value", "") + time_range = "" + try: + value_arr = json.loads(value_str) + if isinstance(value_arr, list) and len(value_arr) >= 2: + time_range = normalize_am_pm(f"{value_arr[0]} ~ {value_arr[1]}") + except (json.JSONDecodeError, TypeError): + time_range = normalize_am_pm(value_str) + ext = parse_ext_value(holiday_field) + duration_day = str(ext.get("durationInDay", "")) + duration_hour = str(ext.get("durationInHour", "")) + else: + # 回退: 从独立字段提取 + time_range, duration_hour, duration_day = _extract_time_duration_from_fields(form_values) + + return [[user_name, group_name, dept, job_number, title, + time_range, duration_hour, duration_day, + approve_cell, status]] + + +def parse_trip_from_approve_record( + record: dict, + name_map: dict[str, str], + *, + user_info_map: dict[str, "UserInfo"] | None = None, + group_map: dict[str, str] | None = None, + corp_id: str = "", +) -> list[str]: + """ + 直接从 attendance approve list 的记录中解析出差行。 + + 不依赖 oa approval detail(该接口对出差单存在 saNode 类型冲突 bug), + 仅使用 approve list 返回的 beginTime/endTime/duration/durationUnit/originId。 + """ + user_id = record.get("userId", "") + info = (user_info_map or {}).get(user_id) + user_name = info.name if info else name_map.get(user_id, user_id) + dept = info.dept_name if info else "" + job_number = info.job_number if info else "" + title = info.title if info else "" + group_name = (group_map or {}).get(user_id, "") + + begin_ms = record.get("beginTime") + end_ms = record.get("endTime") + begin_str = ms_to_date_str(begin_ms) if begin_ms else "" + end_str = ms_to_date_str(end_ms) if end_ms else "" + time_range = f"{begin_str} ~ {end_str}" if begin_str and end_str else begin_str or end_str + + duration = record.get("duration", "") + duration_unit = record.get("durationUnit", "DAY") + unit_str = "天" if duration_unit == "DAY" else "小时" + + instance_id = record.get("originId", "") + effective_corp_id = corp_id or record.get("corpId", "") + approve_cell = build_approve_cell(effective_corp_id, instance_id, f"{user_name}提交的出差审批单") + + # approve list 没有审批状态,有 gmtFinished 说明已完结,视为审批通过 + status = "审批通过" if record.get("gmtFinished") else "审批中" + + return [user_name, group_name, dept, job_number, title, + time_range, str(duration), unit_str, approve_cell, status] + + +def fetch_check_results(user_ids: list[str], start: str, end: str) -> dict[str, list[dict]]: + """ + 批量查询打卡结果,返回 {userId: [records...]} 映射。 + + 每条 record 含: workDate, timeResult, planCheckTime, userCheckTime 等。 + """ + result_map: dict[str, list[dict]] = {} + batch_size = 50 + for i in range(0, len(user_ids), batch_size): + batch = user_ids[i:i + batch_size] + try: + result = run_dws([ + "attendance", "check", "result", + "--users", ",".join(batch), + "--start", start, + "--end", end, + ]) + records = [] + if isinstance(result, list): + records = result + elif isinstance(result, dict): + records = result.get("result", result.get("list", [])) + if not isinstance(records, list): + records = [] + for rec in records: + uid = rec.get("userId", "") + if uid: + result_map.setdefault(uid, []).append(rec) + except DwsCallError as e: + warn(f"查询打卡结果失败(batch {i // batch_size + 1}): {e}") + return result_map + + +def fetch_user_group_map(user_ids: list[str]) -> dict[str, str]: + """ + 查询考勤组列表并建立 userId → 考勤组名称映射。 + + 流程:先 group search 拿到所有考勤组 ID+名称, + 再对有成员的考勤组调用 filtered-get --member 获取成员列表。 + """ + group_map: dict[str, str] = {} + user_id_set = set(user_ids) + + try: + result = run_dws(["attendance", "group", "search"]) + items: list[dict] = [] + if isinstance(result, list): + items = result + elif isinstance(result, dict): + # 适配 {items: [...]} 或 {result: {items: [...]}} + inner = result.get("items", result.get("result", result)) + if isinstance(inner, dict): + items = inner.get("items", []) + elif isinstance(inner, list): + items = inner + + for g in items: + group_name = g.get("name", g.get("groupName", "")) + group_id = g.get("id", g.get("groupId", "")) + member_count = g.get("memberCount", 0) + + if not group_id or not group_name or not member_count: + continue + + # 调用 filtered-get 获取成员 userId 列表 + try: + detail = run_dws([ + "attendance", "group", "filtered-get", + "--group-id", str(group_id), "--member", + ]) + member_users: list[str] = [] + if isinstance(detail, dict): + member_users = detail.get("memberUsers", []) + if not isinstance(member_users, list): + member_users = [] + for uid in member_users: + uid_str = str(uid) + if uid_str in user_id_set: + group_map[uid_str] = group_name + except DwsCallError: + pass + + except DwsCallError as e: + warn(f"查询考勤组失败: {e}") + return group_map + + +CHECK_TIME_RESULT_MAP = { + "Normal": "正常", + "Late": "迟到", + "Early": "早退", + "Absenteeism": "旷工", + "NotSigned": "未打卡", + "SeriousLate": "严重迟到", +} + + +def parse_patch_detail(detail: dict, name_map: dict[str, str], *, + user_info_map: dict[str, "UserInfo"] | None = None, + group_map: dict[str, str] | None = None, + check_result_map: dict[str, list[dict]] | None = None, + corp_id: str = "", + ) -> list[list[str]]: + """解析补卡审批单,输出完整列。""" + form_values = detail.get("formValueVOS", []) + user_id = detail.get("originatorUserid", "") + dept_name = detail.get("originatorDeptName", "") + instance_id = detail.get("processInstanceId", "") + status = format_status(detail.get("status", ""), detail.get("processInstanceResult", "")) + + # 用户基础信息 + info = (user_info_map or {}).get(user_id) + user_name = info.name if info else name_map.get(user_id, user_id) + dept = info.dept_name if info and info.dept_name else dept_name + job_number = info.job_number if info else "" + title = info.title if info else "" + group_name = (group_map or {}).get(user_id, "") + approve_cell = build_approve_cell(corp_id, instance_id, f"{user_name}提交的补卡审批单") + + # 从表单解析补卡时间和原因 + patch_time = "" + patch_reason = "" + work_date = "" + check_time_str = "" + ext_data: dict = {} + + for fv in form_values: + comp_type = fv.get("componentType", "") or "" + biz_alias = (fv.get("bizAlias") or "").lower() + name = fv.get("name") or "" + value = fv.get("value") or "" + + if comp_type == "DDDateField" or "checktime" in biz_alias or "补卡时间" in name: + if value and not patch_time: + patch_time = value + # 解析 extValue 获取考勤日期等 + ext = parse_ext_value(fv) + if ext and not ext_data: + ext_data = ext + if "reason" in biz_alias or "原因" in name or "事由" in name or "理由" in name: + if value and not patch_reason: + patch_reason = value + + # 从 extValue 提取考勤日期、考勤时间、原考勤状态 + plan_tip = "" + plan_text = "" + if ext_data: + work_date_ms = ext_data.get("workDate") + if work_date_ms: + work_date = ms_to_date_str(work_date_ms) + plan_tip = ext_data.get("planTip", "") + plan_text = ext_data.get("planText", "") + + # 从 planText / planTip 提取考勤时间(目标格式:YYYY-MM-DD HH:MM) + # planText 格式: "2026-04-25,星期六,195固定班次,上班时间09:00" + # planTip 格式: "周六上班(04.25 09:00) 缺卡" / "周一上班(04.27 09:00) 缺卡" + import re + plan_time_hhmm = "" + + # 优先从 planTip 提取(更可靠,含具体日期和时间) + # planTip 格式: "周三下班(03.05 01:00) 缺卡" + plan_date_from_tip = "" # MM.DD → 用于跨日场景 + if plan_tip: + # 匹配 "(MM.DD HH:MM)" 格式 + tip_match = re.search(r"\((\d{2})\.(\d{2})\s+(\d{2}:\d{2})\)", plan_tip) + if tip_match: + plan_date_from_tip = f"{tip_match.group(1)}-{tip_match.group(2)}" # "03-05" + plan_time_hhmm = tip_match.group(3) + + # 回退:从 planText 中提取 + if not plan_time_hhmm and plan_text: + # 匹配 "上班时间HH:MM" 或 "下班时间HH:MM" 或 "时间HH:MM" + time_match = re.search(r"时间(\d{2}:\d{2})", plan_text) + if time_match: + plan_time_hhmm = time_match.group(1) + + # 最后回退:任意 HH:MM 格式 + if not plan_time_hhmm: + for source in (plan_tip, plan_text): + if source: + fallback_match = re.search(r"(\d{2}:\d{2})", source) + if fallback_match: + plan_time_hhmm = fallback_match.group(1) + break + + # 拼接考勤时间:优先使用 planTip 中解析的完整日期(处理跨日班次) + if plan_date_from_tip and plan_time_hhmm and work_date: + # 用 work_date 的年份 + planTip 中的 MM-DD + HH:MM + year = work_date[:4] + check_time_str = f"{year}-{plan_date_from_tip} {plan_time_hhmm}" + elif work_date and plan_time_hhmm: + check_time_str = f"{work_date} {plan_time_hhmm}" + elif plan_time_hhmm: + check_time_str = plan_time_hhmm + elif plan_text: + check_time_str = plan_text + elif plan_tip: + check_time_str = plan_tip + + # 如果 work_date 为空,从 patch_time 中提取日期 + if not work_date and patch_time: + work_date = patch_time[:10] if len(patch_time) >= 10 else "" + + # 从 planTip / planText 提取原考勤状态 + # planTip 格式: "周六上班(04.25 09:00) 缺卡" / "Thursday ( 04.23 ) Adjust" + # planText 格式: "2026-04-25,星期六,195固定班次,上班时间09:00" 或 "周一上班(04.27 09:00) 缺卡" + original_check_time = "" + original_status = "" + + tip_status_map = { + "缺卡": "缺卡", "未打卡": "未打卡", + "迟到": "迟到", "早退": "早退", + "旷工": "旷工", "正常": "正常", + "NotSigned": "未打卡", "Adjust": "已调整", + } + # 优先从 planTip 提取,回退到 planText + for source in (plan_tip, plan_text): + if source: + for keyword, label in tip_status_map.items(): + if keyword in source: + original_status = label + break + if original_status: + break + + # 回退: 尝试从 check result 接口获取(如果有数据) + if check_result_map and user_id in check_result_map: + for rec in check_result_map[user_id]: + rec_date = rec.get("workDate", "") + if isinstance(rec_date, (int, float)): + rec_date = ms_to_date_str(rec_date) + if rec_date == work_date: + user_check_ms = rec.get("userCheckTime") + if user_check_ms: + dt = ms_to_datetime(user_check_ms) + original_check_time = dt.strftime("%Y-%m-%d %H:%M") if dt else "" + time_result = rec.get("timeResult", "") + if time_result: + original_status = CHECK_TIME_RESULT_MAP.get(time_result, time_result) + break + + # 补卡结果:审批通过 → 补卡成功 + patch_result = "" + if status == "审批通过": + patch_result = "补卡成功" + elif status == "已拒绝": + patch_result = "补卡失败" + elif status == "审批中": + patch_result = "待审批" + elif status == "已撤销": + patch_result = "已撤销" + + return [[user_name, group_name, dept, job_number, title, work_date, check_time_str, + original_check_time, original_status, patch_time, patch_result, + approve_cell, status]] + + +PARSERS = { + "leave": parse_leave_detail, + "out": parse_out_detail, + "patch": parse_patch_detail, +} + +# 需要额外用户信息(考勤组/工号/职位)的类型 +_TYPES_NEED_USER_INFO = {"leave", "out", "patch", "trip"} + + +# ───────────────────────────────────────────────────────────────────────────── +# 主流程 +# ───────────────────────────────────────────────────────────────────────────── + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="考勤记录报表导出(补卡/出差/外出/请假)", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("--type", required=True, choices=SUPPORTED_TYPES, + help="记录类型: leave(请假)/trip(出差)/out(外出)/patch(补卡)") + parser.add_argument("--users", required=True, + help="用户 ID 列表,逗号分隔(由 Agent 从人员获取阶段提供)") + parser.add_argument("--start", required=True, + help="开始日期 YYYY-MM-DD") + parser.add_argument("--end", required=True, + help="结束日期 YYYY-MM-DD") + parser.add_argument("--out", default="", + help="输出文件路径(不传则自动生成)") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + record_type: str = args.type + user_ids = [u.strip() for u in args.users.split(",") if u.strip()] + start_date: str = args.start + end_date: str = args.end + + if not user_ids: + error("--users 不能为空") + sys.exit(1) + + try: + datetime.strptime(start_date, DATE_FMT) + datetime.strptime(end_date, DATE_FMT) + except ValueError: + error("日期格式错误,请使用 YYYY-MM-DD") + sys.exit(1) + + sheet_name = SHEET_NAMES[record_type] + log(f"开始导出{sheet_name}:{len(user_ids)} 人,{start_date} ~ {end_date}") + + # ── Step 1: 获取审批单列表 ── + log("步骤 1/4:查询审批单列表...") + approve_records = fetch_approve_list(user_ids, record_type, start_date, end_date) + log(f" 获取到 {len(approve_records)} 条审批记录") + + if not approve_records: + log("未查询到任何记录") + print(f"{sheet_name}:0 条记录,无需生成文件") + sys.exit(0) + + # 从 approve list 记录中提取 corpId(用于构建审批单跳转链接) + corp_id = "" + for r in approve_records: + if r.get("corpId"): + corp_id = r["corpId"] + break + + # ── Step 2: 去重提取 instanceId ── + instance_ids = list(dict.fromkeys( + r.get("originId", "") for r in approve_records if r.get("originId") + )) + log(f"步骤 2/4:共 {len(instance_ids)} 个审批实例") + + # ── Step 3: 解析用户信息 ── + log("步骤 3/4:解析用户信息...") + name_map = resolve_user_names(user_ids) + + user_info_map: dict[str, UserInfo] | None = None + group_map: dict[str, str] | None = None + check_result_map: dict[str, list[dict]] | None = None + + if record_type in _TYPES_NEED_USER_INFO: + log(" 获取用户完整信息(工号/职位)...") + user_info_map = resolve_user_info(user_ids) + log(" 查询考勤组映射...") + group_map = fetch_user_group_map(user_ids) + + if record_type == "patch": + log(" 查询原打卡结果...") + check_result_map = fetch_check_results(user_ids, start_date, end_date) + + all_rows: list[list[str]] = [] + + if record_type == "trip": + # 出差记录直接从 approve list 数据生成,不调用 oa approval detail + # (oa approval detail 对出差单存在 saNode result 字段类型冲突 bug) + log("步骤 4/4:从审批列表解析出差记录...") + for record in approve_records: + row = parse_trip_from_approve_record( + record, name_map, + user_info_map=user_info_map, + group_map=group_map, + corp_id=corp_id, + ) + all_rows.append(row) + else: + log("步骤 4/4:查询审批详情并解析...") + for idx, instance_id in enumerate(instance_ids): + if (idx + 1) % 10 == 0: + log(f" 进度: {idx + 1}/{len(instance_ids)}") + + detail = fetch_detail(instance_id) + if not detail: + continue + + # 补充新发现的用户 + originator = detail.get("originatorUserid", "") + if originator and originator not in name_map: + extra = resolve_user_names([originator]) + name_map.update(extra) + if originator and user_info_map and originator not in user_info_map: + extra_info = resolve_user_info([originator]) + user_info_map.update(extra_info) + + if record_type == "patch": + rows = parse_patch_detail( + detail, name_map, + user_info_map=user_info_map, + group_map=group_map, + check_result_map=check_result_map, + corp_id=corp_id, + ) + elif record_type == "leave": + rows = parse_leave_detail( + detail, name_map, + user_info_map=user_info_map, + group_map=group_map, + corp_id=corp_id, + ) + elif record_type == "out": + rows = parse_out_detail( + detail, name_map, + user_info_map=user_info_map, + group_map=group_map, + corp_id=corp_id, + ) + else: + rows = PARSERS[record_type](detail, name_map) + all_rows.extend(rows) + + log(f" 解析完成,共 {len(all_rows)} 行") + + if not all_rows: + log("无有效数据行") + print(f"{sheet_name}:解析后 0 行有效数据,无需生成文件") + sys.exit(0) + + # ── 写入 Excel ── + out_path = args.out or f"attendance_report_record_{record_type}_{start_date}_{end_date}.xlsx" + headers = COLUMNS[record_type] + title = f"{sheet_name} 统计日期:{start_date} 至 {end_date}" + subtitle = f"报表生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}" + + # 将 HyperlinkCell 转为纯文本供 write_excel 写入,之后再补超链接 + plain_rows = [] + hyperlink_cells: list[tuple[int, int, str]] = [] # (row_offset, col_idx, url) + for row_offset, row in enumerate(all_rows): + plain_row = [] + for col_idx, cell in enumerate(row): + if isinstance(cell, HyperlinkCell): + plain_row.append(cell.label) + hyperlink_cells.append((row_offset, col_idx, cell.url)) + else: + plain_row.append(cell) + plain_rows.append(plain_row) + + write_excel( + out_path, + headers, + plain_rows, + sheet_name=sheet_name, + title=title, + subtitle=subtitle, + ) + + # 补充超链接 + if hyperlink_cells: + from openpyxl import load_workbook + from openpyxl.styles import Font + + wb = load_workbook(out_path) + ws = wb.active + # 计算标题行偏移:title + subtitle + header + title_row_count = (1 if title else 0) + (1 if subtitle else 0) + first_data_row = title_row_count + 2 # +1 for header, +1 for 1-indexed + + link_font = Font(color="0563C1", underline="single") + for row_offset, col_idx, url in hyperlink_cells: + cell = ws.cell(row=first_data_row + row_offset, column=col_idx + 1) + cell.hyperlink = url + cell.font = link_font + wb.save(out_path) + + abs_path = os.path.abspath(out_path) + log(f"✅ 导出完成: {abs_path}") + print(f"{sheet_name}导出完成:{abs_path}({len(all_rows)} 行,{len(instance_ids)} 个审批单)") + + +if __name__ == "__main__": + main() diff --git a/skills/multi/dingtalk-calendar/SKILL.md b/skills/multi/dingtalk-calendar/SKILL.md index dce49c4c..1348e3dd 100644 --- a/skills/multi/dingtalk-calendar/SKILL.md +++ b/skills/multi/dingtalk-calendar/SKILL.md @@ -30,8 +30,8 @@ metadata: | "今天 / 明天 / 本周日程" | `python scripts/calendar_today_agenda.py [today\|tomorrow\|week]` | | "约会议(含参会人 + 会议室)" | `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起>" --end "<止>" [--users ] [--book-room]` | | "多人共同空闲" | `python scripts/calendar_free_slot_finder.py --users --date ` | -| "查闲忙" | `dws calendar event list --start "" --end ""` | -| "加参会人" / "订房" / "取消" | `dws calendar participant add` / `room add` / `event delete` | +| "查闲忙" | `dws calendar busy search --users --start "" --end ""` | +| "加参会人" / "订房" / "取消" | `dws calendar attendee add` / `room add` / `event delete` | ## 执行硬约束 @@ -39,8 +39,8 @@ metadata: - 用户明确说"帮我订一个空闲会议室"时,`room search` 返回可用会议室后直接选择第一个可预订且不需要自定义审批的 `roomId` 执行 `room add`;不要把选择权抛回用户导致任务停住。 - 已有日程订房:`dws calendar room search --start ... --end ... --format json` → `dws calendar room add --event --rooms --format json` → `event get` 或 `room/busy` 验证。 - 换会议室:先 `room delete --event --rooms `,再 `room add --event --rooms `,最后回查;不要只更新 `--location`。 -- 参会人变化用 `participant add/delete`,日程描述变化用 `event update --desc`,删除日程用 `event delete --id`。用户当前消息已明确要求删除/取消时可直接执行;否则先确认。 -- 脚本失败或参数不完整时,立即降级到明确的 `dws calendar event/participant/room` 命令,不要停在"我要查看用法"。 +- 参会人变化用 `attendee add/delete`,日程描述变化用 `event update --desc`,删除日程用 `event delete --id`。用户当前消息已明确要求删除/取消时可直接执行;否则先确认。 +- 脚本失败或参数不完整时,立即降级到明确的 `dws calendar event/attendee/room` 命令,不要停在"我要查看用法"。 - 所有 dws 命令带 `--format json`;查询时间必须显式 `--start` / `--end`。 ## 跨产品协作 diff --git a/skills/multi/dingtalk-calendar/references/03-meeting.md b/skills/multi/dingtalk-calendar/references/03-meeting.md index 3a36f53b..6479ed36 100644 --- a/skills/multi/dingtalk-calendar/references/03-meeting.md +++ b/skills/multi/dingtalk-calendar/references/03-meeting.md @@ -52,5 +52,5 @@ | Recipe | 行动指南(固定路线) | | ------------------ | ------------------- | -| schedule-meeting | **见上文「两准则」**、**「搜房失败硬门禁」**。**未给时段且仅说「发起/开个会」**→ 不走本 recipe;当前 CLI 不支持实时视频会议,告知用户请在钉钉客户端操作。**未给时段但有预约意图**("安排""约""定"等词):追问具体开始/结束时间。**已有时段后**,按固定顺序执行:1. `dws calendar event create` 建日程;2. 有参会人则 `dws calendar participant add`;3. 再处理会议室。**无明确会议室范围**:可直接 `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起始>" --end "<结束>" [--users ] [--book-room] [--dry-run]`。**有明确范围(某楼/层)**:先 `dws calendar room list-groups`,锁定该地点**最相关的承载 group**;若只有一个地点,`--room-group-id` 应只传这个最相关 group,**不要**把同楼内多个楼层 group 打包传入碰运气。只有用户明确给出多个允许地点时,才把这些 `group-id` 一并传给 `python scripts/calendar_schedule_meeting.py ... --book-room --room-group-id ""`。**用户点名具体会议室**:须手工 `dws calendar room search --start "" --end "" --group-id [--available] --format json`(**无** `--query`),在 JSON 中匹配名称取 **`rooms[].roomId` 唯一真值** → `dws calendar room add --event --rooms `;**不得**把用户输入的会议室名当 `roomId`。**一旦连续 2 次空结果 / 任意一次 `roomId invalid`**:**必须回读本节并立即收束判断**;若整园/限定范围内搜尽仍无 roomId 或无空房 → **下一条消息必须直接向用户汇报失败结论**;否则只能向用户确认是否放宽范围/改时间。**禁止**假设 roomId、禁止无 ID 调用 `room add`、禁止用日程详情绕路、禁止继续猜测 Mock/测试环境。细则见「会议室搜索早停」。 | +| schedule-meeting | **见上文「两准则」**、**「搜房失败硬门禁」**。**未给时段且仅说「发起/开个会」**→ 不走本 recipe;当前 CLI 不支持实时视频会议,告知用户请在钉钉客户端操作。**未给时段但有预约意图**("安排""约""定"等词):追问具体开始/结束时间。**已有时段后**,按固定顺序执行:1. `dws calendar event create` 建日程;2. 有参会人则 `dws calendar attendee add`;3. 再处理会议室。**无明确会议室范围**:可直接 `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起始>" --end "<结束>" [--users ] [--book-room] [--dry-run]`。**有明确范围(某楼/层)**:先 `dws calendar room list-groups`,锁定该地点**最相关的承载 group**;若只有一个地点,`--room-group-id` 应只传这个最相关 group,**不要**把同楼内多个楼层 group 打包传入碰运气。只有用户明确给出多个允许地点时,才把这些 `group-id` 一并传给 `python scripts/calendar_schedule_meeting.py ... --book-room --room-group-id ""`。**用户点名具体会议室**:须手工 `dws calendar room search --start "" --end "" --group-id [--available] --format json`(**无** `--query`),在 JSON 中匹配名称取 **`rooms[].roomId` 唯一真值** → `dws calendar room add --event --rooms `;**不得**把用户输入的会议室名当 `roomId`。**一旦连续 2 次空结果 / 任意一次 `roomId invalid`**:**必须回读本节并立即收束判断**;若整园/限定范围内搜尽仍无 roomId 或无空房 → **下一条消息必须直接向用户汇报失败结论**;否则只能向用户确认是否放宽范围/改时间。**禁止**假设 roomId、禁止无 ID 调用 `room add`、禁止用日程详情绕路、禁止继续猜测 Mock/测试环境。细则见「会议室搜索早停」。 | | reschedule-meeting | 1. `calendar event list --start "<起始ISO>" --end "<结束ISO>"` → 取 `eventId` 2. `calendar event update --id --start "<新起始ISO>" --end "<新结束ISO>"` 更新时间 3. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message send --group --text "<变更通知>"` 通知变更 | diff --git a/skills/multi/dingtalk-calendar/references/calendar.md b/skills/multi/dingtalk-calendar/references/calendar.md index db219135..6c7fe58e 100644 --- a/skills/multi/dingtalk-calendar/references/calendar.md +++ b/skills/multi/dingtalk-calendar/references/calendar.md @@ -61,6 +61,7 @@ dws calendar room add [flags] dws calendar room delete [flags] ``` > room是会议室,用于线下开会场景。 +> **组织限制**:部分企业使用自建会议室系统,未接入钉钉会议室能力。此时 `room search` / `room list-groups` 会返回业务错误 `400056`("所选组织不支持预定钉钉会议室")。这是组织级配置限制,不是命令用法问题;应直接告知用户该组织需在钉钉客户端手动预订会议室,不要重试或换参。 ### busy 相关三级子命令 ``` diff --git a/skills/multi/dingtalk-chat/SKILL.md b/skills/multi/dingtalk-chat/SKILL.md index 198d308e..d0ad4621 100644 --- a/skills/multi/dingtalk-chat/SKILL.md +++ b/skills/multi/dingtalk-chat/SKILL.md @@ -31,12 +31,14 @@ metadata: | "发到XX群" | `dws chat search --query "<群名>"` → `dws chat message send --group --title "<标题>" --text "<内容>"` | | "建群" / "拉人进群" | `dws chat group create` / `dws chat group members add` | | "改群名" / "踢人" | `dws chat group rename` / `dws chat group members remove --yes`(踢人不可逆,确认目标后加 --yes;踢群主会被 CLI 拦截,需先 `transfer-owner`)| -| "@我消息" / "查群聊记录" | `dws chat message list` | +| "@我消息" | `dws chat message list-mentions` | +| "查群聊记录" | `dws chat message list` | | "用机器人发消息" | `dws chat message send-by-bot --robot-code --group --title "<标题>" --text "<内容>"` | | "Webhook 推一条" | `dws chat message send-by-webhook --token --title "<标题>" --text "<内容>"` | -| "撤回机器人消息" | `dws chat message recall-by-bot --robot-code --group --keys `(只能撤回机器人发的;撤回普通用户消息开源 dws v1.0.30 暂不支持)| +| "撤回我发的消息" | `dws chat message recall`(撤回当前用户发送的消息)| +| "撤回机器人消息" | `dws chat message recall-by-bot --robot-code --group --keys `(撤回机器人发的)| -> **注**:v1.0.30 起 `chat message send / send-by-bot / send-by-webhook` 全部强制 `--title` 必填(单聊群聊都要)。 +> **注**:`chat message send` 的 `--title` 可选(不传时用正文首行作标题);`send-by-bot` / `send-by-webhook` 的 `--title` 必填。 ## 跨产品协作 diff --git a/skills/multi/dingtalk-chat/references/01-messaging.md b/skills/multi/dingtalk-chat/references/01-messaging.md index 7932c5a8..dc8c4db5 100644 --- a/skills/multi/dingtalk-chat/references/01-messaging.md +++ b/skills/multi/dingtalk-chat/references/01-messaging.md @@ -6,7 +6,7 @@ | Recipe | 行动指南(固定路线) | |--------|-------------------| -| query-group-chat | **优先**:`python scripts/chat_export_messages.py --query "<群名>" --time "" [--no-forward] [--limit N] [--output messages.json]`(自动搜群+翻页+导出)
备选:1. `chat search --query "<群名>"` → 取 `openConversationId`
2. `chat message list --group --time ""` → 取消息列表
3. **翻页**:`hasMore=true` 时取本页最后 `createTime` 作为下次 `--time`,重复至 `hasMore=false`
4. `--forward=false` 拉给定时间**之前**的消息
5. 合并全部消息后总结 | +| query-group-chat | **优先**:`python scripts/chat_export_messages.py --query "<群名>" --time "" [--no-forward] [--limit N] [--output messages.json]`(自动搜群+翻页+导出)
备选:1. `chat search --query "<群名>"` → 取 `openConversationId`
2. `chat message list --group --time ""` → 取消息列表
3. **翻页**:`hasMore=true` 时取本页最后 `createTime` 作为下次 `--time`,重复至 `hasMore=false`
4. `--direction older` 拉给定时间**之前**的消息(`newer` 拉之后)
5. 合并全部消息后总结 | | query-private-chat | **优先**:`python scripts/chat_history_with_user.py --name "<姓名>" --time "" [--no-forward] [--limit N] [--output messages.json]`(自动搜人+翻页+导出)
备选:1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`
2. `chat message list-direct --user --time ""` → 取消息列表
3. **翻页**:同 query-group-chat
4. 合并全部消息后总结 | | escalate-ding | 三级升级:
1. `ding message send --robot-code --type app --users --content "<内容>"`(必填项见 `dingtalk-ding/references/ding.md`)
2. `chat message send --group --text "<内容>"` 群里提醒(可选 `--title` / `@` 见 [chat.md](./chat.md))
3. `todo task create --title "<标题>" --executors --priority 40` 建紧急待办
前置:`aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`;`chat search --query "<群名>"` → 取 `openConversationId` | | send-by-bot | **多群批量优先**:`python scripts/bot_broadcast.py --robot-code --chats , --title "<标题>" --text "<内容>"`
单群:1. `chat bot search` → 取 `robotCode`
2. `chat search --query "<群名>"` → 取 `openConversationId`
3. `chat message send-by-bot --robot-code --group --title "<标题>" --text "<内容>"` | diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index 51875f6d..7a56247a 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -74,6 +74,19 @@ Flags: --bot-id string 机器人 openBotId (必填) ``` +#### 根据成员 ID 批量查询群成员详情 — 传入成员 openDingTalkId 列表批量查询 +``` +Usage: + dws chat group members list-by-ids [flags] +Example: + dws chat group members list-by-ids --id --users openDingTalkId1,openDingTalkId2 + # 查询群 ID: dws chat search --query "群名" + # 查询 openDingTalkId: dws contact user search --query "姓名" +Flags: + --id string 群 ID / openConversationId (必填) + --users string 成员 openDingTalkId 列表,逗号分隔 (必填) +``` + #### 更新群名称 ``` Usage: @@ -137,6 +150,7 @@ Example: Flags: --group string 群聊 openConversationId (必填) ``` +> ⚠️ 唯一群主 quit 会直接成功,产生无主群,之后对该群做 dismiss / members 等管理操作会报 listRoles null(11056) 无法管理。与 `chat group members remove` 移除群主有本地拦截不同,quit 无此防护。作为唯一群主想退群,应先 `chat group transfer-owner` 转让群主,或 `chat group dismiss` 解散群。 #### 更新群头像 — 更新指定群聊的群头像 ``` @@ -151,6 +165,7 @@ Flags: ``` > `--icon-media-id` 必须是 `@` 开头的媒体 ID(如 dt_media_upload 返回值),非法格式会本地报错。 +> ⚠️ 本地格式校验只查前缀。格式合法但不真实存在的 mediaId 服务端仍会静默返回成功,头像并不会真正更新。务必用 `dt_media_upload` / `chat media upload` 上传真实图片拿到的 mediaId。 #### 更新群设置 — 更新指定群聊的设置项 @@ -175,6 +190,30 @@ Flags: --status int 设置值: 0=关闭, 1=开启 (必填) ``` +#### 设置群备注 — 给群设置只有自己可见的备注标题 +``` +Usage: + dws chat group update-alias [flags] +Example: + dws chat group update-alias --group --alias-title "项目A群" + # 查询群 ID: dws chat search --query "群名" +Flags: + --group string 群聊 openConversationId (必填) + --alias-title string 群备注标题 (必填) +``` + +#### 设置我在群内的群昵称 — 设置当前用户在指定群里显示的昵称 +``` +Usage: + dws chat group update-nick [flags] +Example: + dws chat group update-nick --group --nick "我的群昵称" + # 查询群 ID: dws chat search --query "群名" +Flags: + --group string 群聊 openConversationId (必填) + --nick string 个人群昵称 (必填) +``` + #### 查看群内所有机器人 — 获取指定群聊中的所有机器人列表 ``` Usage: @@ -296,6 +335,8 @@ Flags: ``` #### 移除用户的指定群身份 — 从用户身上移除指定的群身份(不影响其他群身份) + +> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。需要清除某人身份时,用 `chat group-role set-user --role-ids ""`(传空 role-ids 覆盖为无身份)兜底;但 set-user 会清掉该用户的**全部**身份,无法只移除其中一个。 ``` Usage: dws chat group-role remove-user [flags] @@ -338,6 +379,7 @@ Flags: --query string 搜索关键词 (必填) --limit int 每页返回数量(默认 20) --cursor string 分页游标(默认 "0",翻页传 nextCursor) + --exclude-muted 是否排除已设置免打扰的群聊(默认 false) ``` ### data-auth (数据授权) @@ -373,7 +415,7 @@ Flags: #### 拉取会话消息内容 — 拉取指定群聊或单聊的会话消息内容 ---group 指定群聊,--user 指定单聊用户(通过 userId),--open-dingtalk-id 指定单聊用户(通过 openDingTalkId),三者互斥。默认拉取给定时间之后的消息,--forward=false 拉之前的。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。 +--group 指定群聊,--user 指定单聊用户(通过 userId),--open-dingtalk-id 指定单聊用户(通过 openDingTalkId),三者互斥。用 --direction 控制时间方向:newer=从给定时间往现在拉,older=从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。 ``` Usage: dws chat message list [flags] @@ -381,9 +423,9 @@ Example: dws chat message list --group --time "2025-03-01 00:00:00" dws chat message list --user --time "2025-03-01 00:00:00" --limit 50 dws chat message list --open-dingtalk-id --time "2025-03-01 00:00:00" --limit 50 - dws chat message list --group --time "2025-03-01 00:00:00" --forward=false + dws chat message list --group --time "2025-03-01 00:00:00" --direction older Flags: - --forward true=拉给定时间之后的消息,false=拉给定时间之前的消息 (default true) + --direction string 时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(推荐) --group string 群聊 openconversation_id(群聊时必填) --limit int 返回数量,不传则不限制 --time string 开始时间,格式: yyyy-MM-dd HH:mm:ss (必填) @@ -427,7 +469,7 @@ Example: dws chat message send --group --title "周报提醒" --text "请大家本周五前提交周报" # 幂等发送(24h 内相同 uuid 不重复投递) dws chat message send --group --text "hello" --uuid "unique-id-123" - dws chat message send --group --at-all "@all 请大家注意" + dws chat message send --group --at-all "<@all> 请大家注意" dws chat message send --group --at-open-dingtalk-ids openDingTalkId1,openDingTalkId2 "<@openDingTalkId1> <@openDingTalkId2> 请查收" # 发送图片 dws chat message send --group --msg-type image --media-id @@ -455,13 +497,14 @@ Flags: --file-path string 文件路径(msgType=file 时必填) --file-size int64 文件大小,单位字节(msgType=file 时必填) --uuid string 幂等 UUID,相同 uuid 在 24h 内不会重复发送(可选) + --ai-tag 消息是否带 AI 发送角标(可选,默认 true) 注意: - --text 和位置参数二选一,--text 优先 - --group、--user、--open-dingtalk-id 三者互斥,只需指定其一:群聊用 --group,单聊用 --user 或 --open-dingtalk-id - 纯文本/Markdown 单聊发送时 `--user` 和 `--open-dingtalk-id` 都可用;传 `--user` 时直接走 userId 发送能力 - --group 的别名: --id, --chat, --conversation-id (均可替代 --group) - - --at-all 和 --at-open-dingtalk-ids 仅在 --group 群聊时生效,单聊时无效;当设置--at-all时,消息内容中一定要包含对应的占位符@all;当设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2> + - --at-all 和 --at-open-dingtalk-ids 仅在 --group 群聊时生效,单聊时无效;当设置--at-all时,消息内容中一定要包含对应的占位符<@all>;当设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2> - **换行符**:消息内容按 Markdown 渲染,换行有两层要求,缺一不可: 1. 必须使用**真实换行符**(Unicode `U+000A`),而非字面量字符串 `\n`(反斜杠 + 字母 n)。程序或大模型构造参数时,须确保已正确反转义;否则全部内容会渲染在同一行 2. Markdown 规范下**单个换行不产生换行效果**。需要换行时请使用:段落分隔(连续两个真实换行符 `\n\n`)、行尾两个空格 + 真实换行符(硬换行 `
`),或直接写 HTML 的 `
` 标签 @@ -492,6 +535,7 @@ Flags: 注意: - openTaskId 由 `dws chat message send` 发送消息成功后返回 - 用于确认消息是否已成功发送或获取发送失败的原因 + - 返回结果中含发送成功消息的 openMessageId,可用于后续 recall(撤回)、read-status(查已读)等命令 ``` #### 撤回消息 — 撤回当前用户自己发出的消息 @@ -610,7 +654,7 @@ Flags: --topic-id string 话题 ID,由 dws chat message list 返回 (必填) --time string 开始时间,格式: yyyy-MM-dd HH:mm:ss(可选) --limit int 返回数量(默认 50) - --forward true=从老往新,false=从新往老(默认 false) + --direction string 时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(推荐,默认 older) ``` #### 拉取指定时间范围内当前用户的所有会话消息 — 分页拉取当前登录用户在指定时间范围内的所有会话消息 @@ -719,8 +763,10 @@ Usage: Example: dws chat message list-unread-conversations dws chat message list-unread-conversations --count 20 + dws chat message list-unread-conversations --exclude-muted Flags: - --count int 返回未读会话条数(可选) + --count int 返回未读会话条数(可选) + --exclude-muted 是否排除已设置免打扰的会话(默认 false) ``` #### 查询消息的已读/未读状态 @@ -927,6 +973,7 @@ Example: Flags: --limit int 每页返回数量(默认 1000) --cursor int64 分页游标(首次不传或传 0,翻页传 nextCursor) + --exclude-muted 是否排除已设置免打扰的会话(默认 false) 注意: - 用户询问"置顶会话"时,直接调用此命令返回置顶会话列表即可 @@ -976,6 +1023,7 @@ Flags: --match-mode string 匹配模式:AND=所有人都在群里,OR=任一人在群里(默认 AND) --limit int 每页返回数量(默认 20) --cursor string 分页游标(默认 "0",翻页传 nextCursor) + --exclude-muted 是否排除已设置免打扰的群聊(默认 false) 注意: - --nicks 传人员昵称(花名),逗号分隔,如 "风雷,山乔" @@ -1007,6 +1055,76 @@ Flags: - 上传到共享空间的文件对方才能打开,上传到个人空间的文件对方无法访问 ``` +#### 引用回复消息 — 引用某条消息并回复文字(单聊/群聊均可) +``` +Usage: + dws chat message reply [flags] +Example: + dws chat message reply --conversation-id --ref-msg-id --ref-sender --text "收到,马上处理" + # 被引用消息的 openMessageId、发送者 openDingTalkId 通过 dws chat message list 获取 +Flags: + --conversation-id string 会话 openConversationId (必填,支持单聊/群聊) + --ref-msg-id string 被引用的消息 openMessageId (必填) + --ref-sender string 被引用消息的发送者 openDingTalkId (必填) + --text string 回复内容 (必填) + --ai-tag 消息是否带 AI 发送角标(可选,默认 true) + --uuid string 幂等键(可选) + +注意: + - 以当前用户身份引用回复,语义同 chat message send;目前回复类型仅支持 text +``` + +#### 转发单条消息 — 将一条消息从源会话转发到目标会话(源/目标均支持单聊/群聊) +``` +Usage: + dws chat message forward [flags] +Example: + dws chat message forward --src-conversation-id --msg-id --dest-conversation-id +Flags: + --src-conversation-id string 源会话 openConversationId (必填) + --msg-id string 源消息 openMessageId (必填) + --dest-conversation-id string 目标会话 openConversationId (必填) + --uuid string 幂等键(可选) + +注意: + - 与 combine-forward 区别: forward 转单条,combine-forward 合并多条为一条转发 +``` + +#### 转发话题消息 — 将一条话题消息转发到目标会话 +``` +Usage: + dws chat message forward-topic [flags] +Example: + dws chat message forward-topic --src-conversation-id --src-msg-id --src-thread-id --dest-conversation-id +Flags: + --src-conversation-id string 源会话 openConversationId (必填,消息所在的会话) + --src-msg-id string 源消息 openMessageId (必填,要转发的消息) + --src-thread-id string 话题 ID (必填,格式: convThread + 加密后的 convThreadId,即 message list 返回的 openConvThreadId) + --dest-conversation-id string 目标会话 openConversationId (必填,转发到的会话) +``` + +#### 置顶消息 — 将指定消息置顶到会话顶部 +``` +Usage: + dws chat message set-top-msg [flags] +Example: + dws chat message set-top-msg --open-conversation-id --msg-id +Flags: + --open-conversation-id string 会话 openConversationId (必填,支持群聊/单聊) + --msg-id string 消息 openMessageId (必填) +``` + +#### 取消置顶消息 — 取消会话顶部的置顶消息 +``` +Usage: + dws chat message unset-top-msg [flags] +Example: + dws chat message unset-top-msg --open-conversation-id --msg-id +Flags: + --open-conversation-id string 会话 openConversationId (必填,支持群聊/单聊) + --msg-id string 消息 openMessageId (必填) +``` + #### 合并转发多条消息 — 将多条消息合并后转发到目标会话(源/目标会话均支持单聊/群聊) ``` Usage: @@ -1091,10 +1209,10 @@ Flags: --size int 每页条数 (默认 50),别名: --limit ``` -#### 搜索【全部可用】机器人 — 含他人创建/官方机器人,额外返回 openDingTalkId +#### 搜索【全部可用】机器人 — 含他人创建/官方机器人,额外返回机器人 openDingTalkId 范围: 当前用户可用的全部机器人(含他人创建、官方机器人)。 -返回字段: 额外返回 openDingTalkId(可用于给机器人发单聊消息),search 没有此字段。 +返回字段: 结果在 `result.bots[]` 中,每项含 `botOpenDingTalkId`(机器人的 openDingTalkId,用于给机器人发单聊消息)和 `name`。注意字段名是 `botOpenDingTalkId`,不是 `openDingTalkId`;search 没有此字段。 典型触发词: "搜索机器人""找一个机器人""帮我找 XXX 机器人""所有可用机器人""查机器人"。 ``` @@ -1119,7 +1237,7 @@ search 与 find 选择指南: | 维度 | `chat bot search` | `chat bot find` | |------|-------------------|-----------------| | 范围 | 仅我创建的机器人 | 全部可用机器人(含他人/官方) | -| 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) | +| 额外返回机器人 openDingTalkId | 无 | 有,字段名 `botOpenDingTalkId`(可用于给机器人发单聊消息) | | 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" | ### category (会话分组管理) @@ -1139,9 +1257,70 @@ Usage: dws chat category list-conversations [flags] Example: dws chat category list-conversations --category-id <分组ID> + dws chat category list-conversations --category-id <分组ID> --exclude-muted # 分组ID 可通过 dws chat category list 获取 Flags: --category-id int 会话分组 ID (必填) + --exclude-muted 是否排除已设置免打扰的会话(默认 false) +``` + +#### 创建会话分组 +``` +Usage: + dws chat category create [flags] +Example: + dws chat category create --title "工作群" +Flags: + --title string 分组名称 (必填) +``` + +#### 删除会话分组 +``` +Usage: + dws chat category delete [flags] +Example: + dws chat category delete --category-id <分组ID> + # 分组ID 可通过 dws chat category list 获取 +Flags: + --category-id int 会话分组 ID (必填) +``` + +#### 重命名会话分组 +``` +Usage: + dws chat category rename [flags] +Example: + dws chat category rename --category-id <分组ID> --title "新名称" + # 分组ID 可通过 dws chat category list 获取 +Flags: + --category-id int 会话分组 ID (必填) + --title string 新的分组名称 (必填) +``` + +#### 将会话加入分组 +``` +Usage: + dws chat category add-conv [flags] +Example: + dws chat category add-conv --group --category-ids 123,456 + # 分组ID 可通过 dws chat category list 获取 + # 查询群 ID: dws chat search --query "群名" +Flags: + --group string 会话 openConversationId (必填) + --category-ids string 目标分组 ID 列表,逗号分隔 (必填) +``` + +#### 将会话移出分组 +``` +Usage: + dws chat category remove-conv [flags] +Example: + dws chat category remove-conv --group --category-ids 123,456 + # 分组ID 可通过 dws chat category list 获取 + # 查询群 ID: dws chat search --query "群名" +Flags: + --group string 会话 openConversationId (必填) + --category-ids string 目标分组 ID 列表,逗号分隔 (必填) ``` ### mute (会话免打扰) @@ -1166,6 +1345,256 @@ Flags: - 支持单聊和群聊,openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取 ``` +### media (上传媒体获取 mediaId) + +#### 上传图片/媒体获取 mediaId — 用于 chat message send --msg-type image 等 + +⚠️ 前置条件:本命令需要应用凭证。必须已通过 `dws auth login --client-id --client-secret ` 登录,或设置环境变量 `DWS_CLIENT_ID` / `DWS_CLIENT_SECRET`;否则报"缺少应用凭证"。这与其他 chat 命令走用户登录态不同。 +``` +Usage: + dws chat media upload [flags] +Example: + dws chat media upload --file ./screenshot.png + dws chat media upload --file ./photo.jpg --type image +Flags: + --file string 本地文件路径 (必填) + --type string 媒体类型: image/voice/video/file(默认 image) + +注意: + - 返回的 mediaId 可直接用于 chat message send --msg-type image --media-id +``` + +### hide (隐藏会话) + +#### 隐藏会话 — 在会话列表中隐藏指定会话(支持单聊/群聊),收到新消息时会重新出现 +``` +Usage: + dws chat hide [flags] +Example: + dws chat hide --conversation-id + dws chat hide --id +Flags: + --conversation-id string 会话 openConversationId (必填,支持单聊/群聊) + --id string --conversation-id 的别名 + --chat string --conversation-id 的别名 + +注意: + - 隐藏后会话不再显示在列表中,收到新消息时会重新出现 + - openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取 +``` + +### mute-at-all (关闭@所有人通知) + +#### 关闭/开启 @所有人消息提醒 — 关闭或开启会话中 @所有人的消息通知 + +> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。 +``` +Usage: + dws chat mute-at-all [flags] +Example: + dws chat mute-at-all --conversation-id + dws chat mute-at-all --conversation-id --off +Flags: + --conversation-id string 会话 openConversationId (必填,支持单聊/群聊) + --id string --conversation-id 的别名 + --chat string --conversation-id 的别名 + --off 恢复接收 @所有人通知(不传则关闭通知) +``` + +### mute-red-envelope (关闭红包通知) + +#### 关闭/开启红包消息提醒 — 关闭或开启会话中的红包消息通知 + +> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。 +``` +Usage: + dws chat mute-red-envelope [flags] +Example: + dws chat mute-red-envelope --conversation-id + dws chat mute-red-envelope --conversation-id --off +Flags: + --conversation-id string 会话 openConversationId (必填,支持单聊/群聊) + --id string --conversation-id 的别名 + --chat string --conversation-id 的别名 + --off 恢复接收红包通知(不传则关闭通知) +``` + +### mark-unread (标记会话为未读) + +#### 标记会话为未读 — 将指定会话标记为未读状态 +``` +Usage: + dws chat mark-unread [flags] +Example: + dws chat mark-unread --conversation-id + dws chat mark-unread --id +Flags: + --conversation-id string 会话 openConversationId (必填,支持群聊/单聊) + --id string --conversation-id 的别名 + --chat string --conversation-id 的别名 + +注意: + - openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取 + - API 返回成功,但该未读状态在 API 侧观察不到:list-all-conversations 返回的 unreadPoint 不会随之变化,只有钉钉客户端 UI 上能看到未读标记 +``` + +### clear-red-point (清除会话红点) + +#### 清除会话红点 — 清除指定会话的未读红点 +``` +Usage: + dws chat clear-red-point [flags] +Example: + dws chat clear-red-point --conversation-id + dws chat clear-red-point --id +Flags: + --conversation-id string 会话 openConversationId (必填,支持群聊/单聊) + --id string --conversation-id 的别名 + --chat string --conversation-id 的别名 + +注意: + - 清除红点后该会话不再显示未读标记 +``` + +### clear-all-red-point (红点清零) + +#### 清除所有会话红点 — 一键全部已读 +``` +Usage: + dws chat clear-all-red-point +Example: + dws chat clear-all-red-point + +注意: + - 无需任何参数,直接清除当前用户所有会话的未读红点,等效于"全部已读" +``` + +### list-all-conversations (全部会话列表) + +#### 分页获取全部会话列表 — 获取当前用户的所有会话 +``` +Usage: + dws chat list-all-conversations [flags] +Example: + dws chat list-all-conversations + dws chat list-all-conversations --limit 50 + dws chat list-all-conversations --exclude-muted +Flags: + --limit int 每页数量(1-100,默认 100);传 >100 会被明确拒绝 + --cursor int 分页游标(首次不传或传 0,翻页传 nextCursor) + --exclude-muted 是否排除已免打扰会话(默认 false) + +注意: + - 返回结果包含单聊和群聊,不区分会话类型 + - --limit 范围 1-100,默认 100,上限 100;传入 >100 会报错拒绝,不会静默截断 + - 分页当前不可用:真机 hasMore 恒为 false、nextCursor 恒为 null,本命令最多返回 100 条会话,无法用 --cursor 继续翻页取更多 + - 与 list-top-conversations 的区别: 本命令返回全部会话(单聊+群聊),list-top-conversations 仅返回置顶会话 +``` + +### clear-messages (清空会话聊天记录) + +#### 清空会话聊天记录 — 清空当前用户指定会话的消息 +``` +Usage: + dws chat clear-messages [flags] +Example: + dws chat clear-messages --conversation-id + dws chat clear-messages --id +Flags: + --conversation-id string 会话 openConversationId (必填,支持群聊/单聊) + --id string --conversation-id 的别名 + --chat string --conversation-id 的别名 + +注意: + - 仅清空当前用户视角的消息,不影响其他成员 +``` + +### mark-read (标记消息已读) + +#### 标记消息已读 — 将指定消息及之前的消息标记为已读 +``` +Usage: + dws chat mark-read [flags] +Example: + dws chat mark-read --conversation-id --message-id + dws chat mark-read --id --message-id +Flags: + --conversation-id string 会话 openConversationId (必填,支持群聊/单聊) + --id string --conversation-id 的别名 + --chat string --conversation-id 的别名 + --message-id string 消息 openMessageId (必填) + +注意: + - 标记该消息及之前的所有消息为已读 + - openMessageId 可通过 chat message list 获取 +``` + +### group list-all (分页拉取所有群) + +#### 分页拉取我所有群列表 — 获取当前用户加入的所有群聊 +``` +Usage: + dws chat group list-all [flags] +Example: + dws chat group list-all + dws chat group list-all --limit 50 + dws chat group list-all --limit 100 --cursor +Flags: + --limit int 每页返回数量(默认 100,最大 200) + --cursor string 分页游标(首次不传,翻页传返回的 nextCursor) + +注意: + - 与 chat group list-my-groups 区别: list-all 返回用户加入的所有群;list-my-groups 仅返回用户作为群主/管理员的群 + - 分页: hasMore=true 时用返回的 nextCursor 作为下次 --cursor + - ⚠️ 存在同步盲区:新建群后较长时间(实测 15 分钟后全量翻页仍查不到)内不会出现在 list-all 里;而 chat group list-my-groups / chat search 能立即查到。要确认刚建的群,用 list-my-groups 或 search,别依赖 list-all +``` + +### group list-join-validations (分页拉取入群验证记录) + +#### 分页拉取入群验证记录 — 获取当前用户的所有入群验证记录 + +包括自己被拒绝的记录以及作为审批者的记录。 +``` +Usage: + dws chat group list-join-validations [flags] +Example: + dws chat group list-join-validations + dws chat group list-join-validations --limit 30 + dws chat group list-join-validations --limit 20 --cursor +Flags: + --limit int 单页数量(默认 20,最大 50) + --cursor string 分页游标(首次不传,翻页传返回的 nextCursor) + +注意: + - 分页: hasMore=true 时用返回的 nextCursor 作为下次 --cursor + - cursor 首次拉取不传或传 null 时从当前时间开始拉 +``` + +### group audit-join-validation (审批入群验证) + +#### 审批入群验证 — 通过、删除单个审核 + +真机当前仅 AuditApprove(通过)和 AuditDelete(删除)两个动作可用。 +``` +Usage: + dws chat group audit-join-validation [flags] +Example: + dws chat group audit-join-validation --group --record-id 123456 --applicant --inviter --status AuditApprove + dws chat group audit-join-validation --group --record-id 123456 --applicant --inviter --status AuditDelete + # 查询入群验证记录: dws chat group list-join-validations +Flags: + --group string 群 openConversationId (必填) + --record-id string 申请记录 ID (必填) + --applicant string 申请人 openDingTalkId (必填) + --inviter string 邀请人 openDingTalkId (必填) + --status string 审批动作: AuditApprove(通过) / AuditDelete(删除) (必填) + --description string 审批说明(可选) + +注意: + - status 真机仅支持 AuditApprove(通过) 和 AuditDelete(删除);AuditIgnore(忽略)、AuditRefuse(拒绝)、AuditBlock(拒绝且拉黑) 会被服务端拒绝报 unsupported audit status,属服务端限制 + - record-id、applicant、inviter 可通过 dws chat group list-join-validations 查询获得 +``` + ## 意图判断 用户说"我特别关注的人最近发了什么消息/关注的人最近聊了啥/星标联系人最近的动态" → `chat message list-focused`(零参数一行命令) @@ -1188,6 +1617,9 @@ Flags: 用户说"踢人/移除群成员" → `chat group members remove` 用户说"加机器人到群" → `chat group members add-bot` 用户说"改群名" → `chat group rename` +用户说"设置群备注/给群加备注" → `chat group update-alias` +用户说"改我在群里的昵称/设置群昵称" → `chat group update-nick` +用户说"批量查群成员信息/按ID查群成员" → `chat group members list-by-ids` 用户说"聊天记录/会话消息/拉取会话" → `chat message list` 用户说"某人发给我的消息/指定发送者/某人的消息" → `chat message list-by-sender`(用户未明确说"单聊"时优先使用,跨单聊/群聊) 用户说"拉取和某人的单聊记录/单聊消息" → `chat message list --user`(用户明确说"单聊"时使用) @@ -1211,6 +1643,11 @@ Flags: 用户说"置顶会话/置顶消息/我的置顶/查看置顶" → `chat list-top-conversations` 用户说"查看会话分组/自定义分组" → `chat category list` 用户说"某个分组下的会话/分组会话列表" → `chat category list-conversations` +用户说"新建会话分组/创建分组" → `chat category create` +用户说"删除会话分组" → `chat category delete` +用户说"重命名分组/改分组名" → `chat category rename` +用户说"把会话加入分组/会话归类到分组" → `chat category add-conv` +用户说"把会话移出分组/从分组移除会话" → `chat category remove-conv` 用户说"根据群号查群信息/群号查群/群号转openConversationId" → `chat group get-by-group-id`(当用户发消息时只提供了群号,用此工具将群号转为 openConversationId,再调用发消息接口) 用户说"查看群身份/群的自定义身份列表" → `chat group-role list` 用户说"创建/添加群身份" → `chat group-role add` @@ -1228,12 +1665,30 @@ Flags: 用户说"取消文字表情回应/移除文字表情" → `chat message remove-text-emotion` 用户说"创建文字表情/新建文字表情" → `chat message create-text-emotion` 用户说"免打扰/消息免打扰/静音/开启免打扰/关闭免打扰" → `chat mute` +用户说"隐藏会话/隐藏群聊/隐藏对话" → `chat hide` +用户说"关闭@所有人通知/屏蔽@所有人/不接收@all" → `chat mute-at-all`(当前服务端 1002 不可用) +用户说"开启@所有人通知/恢复@所有人提醒" → `chat mute-at-all --off`(当前服务端 1002 不可用) +用户说"关闭红包通知/屏蔽红包/不接收红包提醒" → `chat mute-red-envelope`(当前服务端 1002 不可用) +用户说"开启红包通知/恢复红包提醒" → `chat mute-red-envelope --off`(当前服务端 1002 不可用) +用户说"标记会话未读/标为未读" → `chat mark-unread` +用户说"标记已读/把消息标成已读" → `chat mark-read` +用户说"清除红点/去掉某个会话的未读红点" → `chat clear-red-point` +用户说"全部已读/一键清除红点/红点清零" → `chat clear-all-red-point` +用户说"我的所有会话/全部会话列表" → `chat list-all-conversations` +用户说"清空聊天记录/清空会话消息" → `chat clear-messages` +用户说"我加入的所有群/我的全部群列表" → `chat group list-all` +用户说"入群验证记录/谁申请进群" → `chat group list-join-validations` +用户说"审批入群/通过入群申请/删除入群申请" → `chat group audit-join-validation`(仅 AuditApprove/AuditDelete 可用) 用户说"引用回复/回复消息/引用消息回复" → `chat message reply` 用户说"转发消息/转发一条消息/把消息转发到另一个群" → `chat message forward` 用户说"合并转发/批量转发/合并转发多条消息" → `chat message combine-forward` +用户说"转发话题/转发话题消息" → `chat message forward-topic` +用户说"置顶消息/把消息置顶" → `chat message set-top-msg` +用户说"取消置顶消息/撤销消息置顶" → `chat message unset-top-msg` +用户说"上传图片拿mediaId/上传媒体" → `chat media upload` 用户说"群机器人列表/群里有哪些机器人/查看群机器人" → `chat group bots` 用户说"从群里移除机器人/踢出机器人" → `chat group members remove-bot` -用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 openDingTalkId 可发单聊) +用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 botOpenDingTalkId 可发单聊) 用户说"给机器人发单聊/给机器人发消息/跟机器人聊天" → 必须先 `chat bot find`(拿 openDingTalkId)→ 再 `chat message send --open-dingtalk-id`(search 没有 openDingTalkId,无法发单聊) 用户说"我创建的机器人/我的机器人/我自己的机器人/查看我的机器人" → `chat bot search`(仅我创建的机器人,无 openDingTalkId) 用户说"解散群/解散群聊" → `chat group dismiss` @@ -1474,7 +1929,7 @@ Flags: | `aisearch person` | `userId` | message send 的 --user、send-by-bot 的 --users、send-by-bot 的 --at-user-ids、list-by-sender 的 --sender-user-id | | `aisearch person` → `contact user get` | `openDingTalkId` | message send 的 --at-open-dingtalk-ids、--open-dingtalk-id、send-by-bot 的 --open-dingtalk-ids、send-by-bot 的 --at-open-dingtalk-ids、list-by-sender 的 --sender-open-dingtalk-id、message list 的 --open-dingtalk-id | | `chat bot search` | `robotCode` | send-by-bot / recall-by-bot 的 --robot-code(仅我创建的机器人,无 openDingTalkId) | -| `chat bot find` | `openDingTalkId` | 给机器人发单聊消息(全部可用机器人,额外返回 openDingTalkId) | +| `chat bot find` | `botOpenDingTalkId` | 给机器人发单聊消息(send --open-dingtalk-id;字段名是 botOpenDingTalkId,非 openDingTalkId) | | `chat message send-by-bot` | `processQueryKey` | recall-by-bot 的 --keys | | `chat message send` | `openTaskId` | query-send-status 的 --open-task-id | | `chat message list` | `openMessageId` | recall 的 --msg-id | @@ -1516,7 +1971,7 @@ Flags: - `chat search-common` 搜索共同群,`--nicks` 传人员昵称(逗号分隔),`--match-mode` AND/OR 控制匹配逻辑,分页用 `--limit`(默认 20)/`--cursor` - `chat list-top-conversations` 拉取置顶会话列表,分页用 `--limit`(默认 1000)/`--cursor`;用户询问"置顶会话"或"置顶消息"时均路由到此命令 - `--user` 和 `--open-dingtalk-id` 本质上都是发起单聊操作,只是用户标识格式不同:userId 为企业内部应用常用标识,openDingTalkId 为三方应用或跨组织场景下的用户标识,服务端对两种 ID 的解析逻辑不同 -- `--time` 格式: `yyyy-MM-dd HH:mm:ss`,为拉取消息的起始时间点;`--forward` 控制方向(默认 true,拉给定时间之后的消息),`--limit` 控制数量 +- `--time` 格式: `yyyy-MM-dd HH:mm:ss`,为拉取消息的起始时间点;`--direction` 控制方向(newer=从给定时间往现在拉,older=从给定时间往以前拉),`--limit` 控制数量 - `chat search` 挂在 `chat` 下(非 `chat group` 下),路径为 `dws chat search` - `send-by-bot` 群聊传 `--group`,单聊传 `--users` 或 `--open-dingtalk-ids`,与 `--group` 互斥且必选其一;群聊时可选 `--at-user-ids` @指定成员(传 userId 列表)或 `--at-open-dingtalk-ids` @指定成员(传 openDingtalkId 列表),content 中需包含对应 @标识;`--at-all` @所有人;群聊场景如果返回"机器人不存在"错误,需先通过 `chat group members add-bot --group --robot-code ` 将机器人邀请进群后再发送 - `recall-by-bot` 群聊传 `--group` + `--keys`,单聊仅传 `--keys`(不传 `--group` 即为单聊撤回) diff --git a/skills/multi/dingtalk-contact/references/contact.md b/skills/multi/dingtalk-contact/references/contact.md index bd7fbdbe..e5573f7a 100644 --- a/skills/multi/dingtalk-contact/references/contact.md +++ b/skills/multi/dingtalk-contact/references/contact.md @@ -36,9 +36,10 @@ Example: dws contact user search --query "张三" Flags: --query string 搜索关键词 (必填) -Returns: (列表,每项包含以下字段) +Returns: (顶层 `result` 为列表,每项包含以下字段) name string 成员姓名 nick string 成员昵称 + flowerName string 花名(无花名时为 null) userId string 成员 ID(仅同事关系时返回) title string 员工职位(仅同事关系时返回) openDingTalkId string 当前用户视角下的目标用户唯一标识,不可跨用户共享;可用于发消息等好友关系场景的操作 @@ -82,6 +83,8 @@ Flags: 查询花名册有权限的字段列表,根据当前用户查询花名册有权限的字段列表。认证信息(corpId、optUserId)由系统自动注入,无需手动传入。 +> **前置条件:需操作人具备花名册管理权限。** 无权限时返回业务错误 `操作人无花名册管理权限`(非命令写法问题,不要改参数)。 + #### 查询员工花名册字段信息(个人档案) ``` Usage: @@ -98,6 +101,8 @@ Flags: 花名册字段包含:试用/转正信息、个人/家庭信息、学历信息、银行卡/合同信息、紧急联系人和其他企业自定义信息。 > **与 `contact user get` 的区别**:`user get` 返回组织管理信息(部门、主管、管理员权限),`user profile get` 返回个人档案信息(学历、家庭、银行卡等)。 +> +> **前置条件:需操作人具备花名册管理权限。** 无权限时返回业务错误 `操作人无花名册管理权限`(非命令写法问题,不要改参数)。 ### dismission (离职员工) @@ -124,6 +129,8 @@ Flags: 查询离职员工列表,支持按员工姓名、离职日期范围、部门进行过滤。认证信息(corpId、optUserId)由系统自动注入,无需手动传入。 `--start` 和 `--end` 必须同时设置或同时不设置,不允许只传其中一个。 +> **前置条件:需操作人具备已离职人员花名册管理权限。** 无权限时返回业务错误 `操作人无已离职人员花名册管理权限`(非命令写法问题,不要改参数)。 + ### dept (部门查询) #### 搜索部门 @@ -134,6 +141,9 @@ Example: dws contact dept search --query "技术部" Flags: --query string 搜索关键词 (必填) +Returns: (顶层 key 为 `deptList`,非 `result`,为列表,每项包含以下字段) + deptId int 部门 ID + deptName string 部门名称(命中关键词会用 … 高亮包裹,需自行去标签) ``` #### 获取部门详情 @@ -178,6 +188,9 @@ Example: dws contact dept list-members --depts 1 # 根部门 Flags: --depts string 部门 ID 列表,逗号分隔 (必填) +Returns: (顶层 key 为 `deptUserList`,非 `result`,为列表,每项形如 { "userInfo": { "name": ..., "userId": ... } }) + userInfo.name string 成员姓名 + userInfo.userId string 成员 ID Notes: - **钉钉根部门 `deptId=1`**;查根部门直属成员用 `--depts 1` - 仅返回**本部门**直接成员,**不含下级部门**成员;需含下级请先 `dept list-children` 枚举子部门,再对子 deptId 分别/合并调用 `list-members` diff --git a/skills/multi/dingtalk-contact/scripts/contact_dept_members.py b/skills/multi/dingtalk-contact/scripts/contact_dept_members.py index 519afacf..a743e657 100644 --- a/skills/multi/dingtalk-contact/scripts/contact_dept_members.py +++ b/skills/multi/dingtalk-contact/scripts/contact_dept_members.py @@ -8,12 +8,20 @@ """ import sys +import re import json import subprocess import argparse from typing import List, Any, Optional +def strip_highlight(text: str) -> str: + """去除 dept search 返回名称中的 … 高亮标签。""" + if not isinstance(text, str): + return text + return re.sub(r'', '', text) + + def run_dws( args: List[str], dry_run: bool = False, ) -> Optional[Any]: @@ -67,7 +75,11 @@ def main(): elif isinstance(dept_data, dict): inner = dept_data.get('result', dept_data) if isinstance(inner, dict): - depts = inner.get('items', inner.get('depts', [])) + # dept search 真机返回顶层 deptList;兼容历史 items/depts 键。 + depts = (inner.get('deptList') + or inner.get('items') + or inner.get('depts') + or []) elif isinstance(inner, list): depts = inner else: @@ -80,7 +92,9 @@ def main(): for dept in depts: dept_id = dept.get('id') or dept.get('deptId') - dept_name = dept.get('name') or dept.get('deptName', '未知') + dept_name = strip_highlight( + dept.get('name') or dept.get('deptName', '未知') + ) if not dept_id: continue @@ -100,8 +114,11 @@ def main(): elif isinstance(members_data, dict): inner = members_data.get('result', members_data) if isinstance(inner, dict): - members = inner.get('userlist', - inner.get('list', [])) + # list-members 真机返回 deptUserList;兼容历史 userlist/list 键。 + members = (inner.get('deptUserList') + or inner.get('userlist') + or inner.get('list') + or []) elif isinstance(inner, list): members = inner else: @@ -113,9 +130,12 @@ def main(): continue for m in members: - name = m.get('name') or m.get('userName', '未知') - title = m.get('title') or m.get('position', '') - uid = m.get('userId') or m.get('userid', '') + # list-members 真机每项形如 {"userInfo": {"name":..., "userId":...}}, + # 成员字段嵌在 userInfo 下;兼容历史扁平结构。 + info = m.get('userInfo', m) + name = info.get('name') or info.get('userName', '未知') + title = info.get('title') or info.get('position', '') + uid = info.get('userId') or info.get('userid', '') line = f" 👤 {name}" if title: line += f" ({title})" diff --git a/skills/multi/dingtalk-dev/SKILL.md b/skills/multi/dingtalk-dev/SKILL.md index 0cdddf60..ccf5ba88 100644 --- a/skills/multi/dingtalk-dev/SKILL.md +++ b/skills/multi/dingtalk-dev/SKILL.md @@ -84,13 +84,13 @@ metadata: 6. 选审批人时优先原样展示 `approvalPromptText`(成品文案);需结构化时读 `approvalOptions[].label`;只有都缺时才用原始 `approvalCandidates` 的 `name(userId: xxx)` 自己拼标签。 ### 通用出参约定(跨所有命令) -- 游标分页(list / permission list / version list / event list / doc search):首次不传 `--cursor`,出参带 `nextCursor`(空=到底)原样回传续翻;`hasMore == nextCursor 非空`。cursor 是上游不透明令牌,不要自己解析或构造,也不要跨命令复用。 +- 游标分页(list / permission list / version list / event list / `devdoc article search`):首次不传 `--cursor`,出参带 `nextCursor`(空=到底)原样回传续翻;`hasMore == nextCursor 非空`。cursor 是上游不透明令牌,不要自己解析或构造,也不要跨命令复用。 - 批量聚合:`permission remove` 出参是 `{removed, removedScopeValues, rejectedScopeValues, success, message}`,逐条看 `removedScopeValues`/`rejectedScopeValues` 判断每个权限点成败。 - pretty:`--format pretty` 会在应用/版本状态字段旁附 `*Text` 可读标签(如 `appStatusText`);JSON 格式不附,以原始字段为准。 - 失败:`ServiceResult.success=false` 原样透传 `errorCode/errorMsg`,不编造解释,解读走下方文档 RAG。 ## 开放平台文档 RAG / 错误码排查 -- dev 命令执行中,只要用户问开放平台 API、接口参数、字段含义、权限点、回调、SDK、配额、错误码,或命令返回上游 OpenAPI/SDK 错误,必须先用 `dws dev doc search --keyword "<关键词>" --format json` 做官方文档 RAG。 +- dev 命令执行中,只要用户问开放平台 API、接口参数、字段含义、权限点、回调、SDK、配额、错误码,或命令返回上游 OpenAPI/SDK 错误,必须先用 `dws devdoc article search --query "<关键词>" --format json` 做官方文档 RAG(`dev doc search` 当前网关未注册该工具键,会报「未找到指定工具」,一律走 `devdoc article search`;flag 是 `--query` 不是 `--keyword`)。 - 业务错误(`ServiceResult.success=false`)原样透传 `errorCode/errorMsg`,不要编造解释;需要解读错误含义时走 devdoc RAG。 - 查询词优先保留原始 API 名、能力名、权限点、完整错误码和 message;首轮形如 `errcode `,无结果再换 `<产品/场景> <错误码>`、`<接口名> 参数`。 - 本地 CLI 错误(如 `unknown command` / `unknown flag` / 认证 / recovery)仍按 root `dws` / `dws-shared` 的错误处理执行;`devdoc` 用于开放平台业务错误码和接口语义排查。 diff --git a/skills/multi/dingtalk-dev/references/event.md b/skills/multi/dingtalk-dev/references/event.md index 38694d18..fcbe663b 100644 --- a/skills/multi/dingtalk-dev/references/event.md +++ b/skills/multi/dingtalk-dev/references/event.md @@ -23,13 +23,13 @@ - `dev connect`:用应用凭证把这条长连建起来并保活,但只注册了机器人消息处理(收 @机器人 → 转发本地 agent),**不消费事件**。 - `event subscribe/unsubscribe`:只是**配置**操作(配应用订阅哪些事件码),自己不建长连、不收事件;服务端要求应用的 Stream 长连已在线,否则返回 `STREAM_NOT_CONNECTED`。 - 两者唯一关联:先 `dev connect` 把长连建在线,`event subscribe` 才能成功——connect 负责「让长连在线」,subscribe 负责「配置订阅」。 -- 当前限制:connect 的长连不处理事件,dws 也没有「收/消费事件」的运行时命令,event 只到「配置订阅」为止。订阅成功后真正的事件推送 dws 暂不消费——要消费得用注册了事件 handler 的 SDK 自接(事件结构走 `dev doc search` 查)。 +- 当前限制:connect 的长连不处理事件,dws 也没有「收/消费事件」的运行时命令,event 只到「配置订阅」为止。订阅成功后真正的事件推送 dws 暂不消费——要消费得用注册了事件 handler 的 SDK 自接(事件结构走 `dws devdoc article search --query` 查)。 ## Stream 长连怎么建的 `dev connect` 内部用 dingtalk-stream-sdk-go,凭 `clientId/clientSecret` 建一条 WebSocket 长连并保活——底层网关握手、ticket、加解密都由 SDK 封装,agent 跑 `dev connect` 即可,不用碰这些。 -dws 当前只消费机器人消息、不消费事件。要自己写事件消费程序补这个 gap 时,SDK 用法以官方文档为准(走 `dev doc search`,或看 github.com/open-dingtalk/dingtalk-stream-sdk-go)——注意事件用 `RegisterAllEventHandler`、connect 用的机器人是 `RegisterChatBotCallbackRouter`,两套别混;版本/接口会变,不在这里固化。 +dws 当前只消费机器人消息、不消费事件。要自己写事件消费程序补这个 gap 时,SDK 用法以官方文档为准(走 `dws devdoc article search --query`,或看 github.com/open-dingtalk/dingtalk-stream-sdk-go)——注意事件用 `RegisterAllEventHandler`、connect 用的机器人是 `RegisterChatBotCallbackRouter`,两套别混;版本/接口会变,不在这里固化。 ## 发现命令 diff --git a/skills/multi/dingtalk-ding/references/ding.md b/skills/multi/dingtalk-ding/references/ding.md index 28cc5beb..5f62c730 100644 --- a/skills/multi/dingtalk-ding/references/ding.md +++ b/skills/multi/dingtalk-ding/references/ding.md @@ -31,12 +31,12 @@ Flags: Usage: dws ding message list [flags] Example: - dws ding message list + dws ding message list --type ALL dws ding message list --type UNREAD dws ding message list --type SEND --cursor 10 Flags: --cursor int 分页游标 (首次传 0, 翻页传返回的 nextCursor) - --type string 消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED (可选, 不传返回全部) + --type string 消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED (默认 ALL; 服务端不接受空值, CLI 不传时会自动按 ALL 查询, 故裸跑 `ding message list` 即可) ``` ### 查看 DING 接收状态 @@ -45,7 +45,7 @@ Usage: dws ding message receiver-status [flags] Example: dws ding message receiver-status --ding-id - # 查询 dingId: dws ding message list + # 查询 dingId: dws ding message list --type ALL Flags: --ding-id string DING 消息 openDingId (必填) ``` diff --git a/skills/multi/dingtalk-doc/SKILL.md b/skills/multi/dingtalk-doc/SKILL.md index 2d0b50ef..3bba1780 100644 --- a/skills/multi/dingtalk-doc/SKILL.md +++ b/skills/multi/dingtalk-doc/SKILL.md @@ -42,19 +42,20 @@ metadata: |--------|------| | "创建文档(短内容)" | `dws doc create --name "<标题>" --content "<内容>"` | | "创建+写入(长内容自动分块)" | `python scripts/doc_create_and_write.py --name "<标题>" --content "<内容>" [--mode append\|overwrite]` | -| "搜文档" | `dws doc search --query "<关键词>"` | +| "搜文档"(全局) | `dws drive search --query "<关键词>"`(`doc search` 已弃用;切到 `dingtalk-drive`) | +| "在某知识库里搜文档" | `dws wiki node search --workspace --query "<关键词>"` | | "读文档内容" | `dws doc read --node ` | | "更新文档内容 / 分块追加" | `dws doc update --node --content "<分块>" --mode append` | | "删除块" | `dws doc block delete`(需用户确认) | ## 评测/多步文档短路径 -- 知识库「评测记录」下按日期文件夹执行:`dws wiki space search --keyword "评测记录" --format json` → `dws doc list --workspace --format json` → 找 `评测-doc-YYYYMMDD`;不存在则 `dws doc folder create --name "评测-doc-YYYYMMDD" --workspace --format json`。 +- 知识库「评测记录」下按日期文件夹执行:`dws wiki space search --keyword "评测记录" --format json` → `dws wiki node list --workspace --format json`(或 `dws drive list --workspace `;`doc list` 已弃用)→ 找 `评测-doc-YYYYMMDD`;不存在则 `dws wiki node create --workspace --name "评测-doc-YYYYMMDD" --type folder --format json`(`doc folder create` 已弃用)。 - 在目标文件夹创建文字文档:`dws doc create --name "<标题>" --folder --content-file --format json`。拿到 `nodeId` 后立即回读。 - 块级编辑固定顺序:`doc block list --node ` → 选 `blockId` → `doc block insert/update/delete` → `doc block list` 验证。删除块必须已有用户明确删除意图或二次确认。 - 插入引用块、代码块、表格、分栏、附件、图片时,优先读 [doc.md](references/doc.md) 对应小节,不要只停在"准备查看 help"。说出"我将插入..."后必须立即执行对应 terminal 调用。 - 用户要求多个子文档/附件/块操作时,按 checklist 串行完成;最后一条 assistant 消息不能停在"接下来我要...",必须有实际工具调用或明确失败原因。 -- 用户说"下载文件"时用 `doc download --node ... --output `;用户说"导出在线文档为 docx"时用 `doc export --node ... --output `。 +- 用户说"下载文件"(已有文件)时用 `dws drive download --node ... --output `(`doc download` 已弃用,切到 `dingtalk-drive`);用户说"导出在线文档为 docx"时用 `doc export --node ... --output `(内容级命令,未迁移)。 - 所有 dws 命令带 `--format json`;仅参数不确定时查 `--help`,不要把完整 help 当成最终结果。 ## 危险操作 diff --git a/skills/multi/dingtalk-doc/references/doc.md b/skills/multi/dingtalk-doc/references/doc.md index 04d21a9d..f21ce032 100644 --- a/skills/multi/dingtalk-doc/references/doc.md +++ b/skills/multi/dingtalk-doc/references/doc.md @@ -9,7 +9,7 @@ dws doc --help # 查看具体命令的完整参数说明 -dws doc list --help +dws doc read --help dws doc create --help dws doc block insert --help @@ -25,46 +25,19 @@ dws doc permission --help ## 命令总览 -### 搜索文档 -``` -Usage: - dws doc search [flags] -Example: - dws doc search --query "会议纪要" - dws doc search - dws doc search --extensions pdf,docx - dws doc search --query "方案" --created-from 1700000000000 --created-to 1710000000000 - dws doc search --creator-uids uid1,uid2 - dws doc search --workspace-ids wsId1,wsId2 -Flags: - --query string 搜索关键词 (不传则返回最近访问) - --extensions strings 按文件扩展名过滤,不含点号,逗号分隔 (如 pdf,docx,png)。支持的在线文档类型后缀名: adoc=文字, axls=表格, appt=演示文稿, awbd=白板, adraw=画板, amind=脑图, able=多维表格, aform=收集表 - --created-from int 创建时间起始 (毫秒时间戳,含) - --created-to int 创建时间截止 (毫秒时间戳,含) - --visited-from int 访问时间起始 (毫秒时间戳,含) - --visited-to int 访问时间截止 (毫秒时间戳,含) - --creator-uids strings 按创建者用户 ID 过滤,逗号分隔 - --editor-uids strings 按编辑者用户 ID 过滤,逗号分隔 - --mentioned-uids strings 按 @提及的用户 ID 过滤,逗号分隔 - --workspace-ids strings 按知识库 ID 过滤,支持知识库 URL,逗号分隔 - --page-size int 每页数量 (默认 10,最大 30) - --page-token string 分页游标 (从上次结果的 nextPageToken 获取) -``` +### 搜索 / 遍历文件(已迁移,不再是 doc 子命令) -### 遍历文件列表 -``` -Usage: - dws doc list [flags] -Example: - dws doc list - dws doc list --folder - dws doc list --workspace --page-size 20 -Flags: - --folder string 文件夹 ID 或 URL - --workspace string 知识库 ID - --page-size int 每页数量 (默认 50,最大 50) - --page-token string 分页游标 (从上次结果的 nextPageToken 获取) -``` +> **弃用提示**:`dws doc search` 和 `dws doc list` 已迁移到 `drive` / `wiki`。真机上这两条虽仍能跑,但每次都会打印弃用警告,请改用下面的命令,命令详情见 dingtalk-drive skill。 + +| 旧命令(弃用) | 改用 | 场景 | +|--------------|------|------| +| `dws doc search --query "<关键词>"` | `dws drive search --query "<关键词>"` | 全局搜文档/文件(跨钉盘+文档空间聚合) | +| `dws doc search --workspace-ids ` | `dws wiki node search --workspace --query "<关键词>"` | 指定知识库内搜索 | +| `dws doc list` | `dws drive list` | 遍历「我的文档」/钉盘根目录 | +| `dws doc list --folder ` | `dws drive list --folder ` | 遍历指定文件夹 | +| `dws doc list --workspace ` | `dws drive list --workspace ` 或 `dws wiki node list --workspace ` | 遍历知识库 | + +拿到 `nodeId` 后,`doc read` / `doc info` / `doc update` / `doc block` 等内容级命令照常用。 ### 获取文档元信息 ``` @@ -108,22 +81,26 @@ Flags: --content-file string 从文件读取初始 Markdown ``` -### 创建其他类型文件 (表格/脑图/白板/多维表/画板) +### 创建其他类型文件 (表格/脑图/白板/多维表/画板/文件夹) + +> **弃用提示**:`dws doc file create --type` 已弃用,真机每次执行都会打印 `deprecated, use 'dws wiki node create --type '`。创建各类型文件节点改用 `dws wiki node create`(与 intent-guide 一致);创建普通文件夹用 `dws drive mkdir`。 + ``` Usage: - dws doc file create [flags] + dws wiki node create [flags] Example: - dws doc file create --name "项目周报" --type adoc - dws doc file create --name "数据统计" --type axls --folder - dws doc file create --name "思维导图" --type amind --workspace - dws doc file create --name "子文件夹" --type folder + dws wiki node create --workspace --name "数据统计" --type axls + dws wiki node create --workspace --name "思维导图" --type amind + dws wiki node create --workspace --name "方案目录" --type folder --folder Flags: + --workspace string 目标知识库 ID (必填 — wiki node create 不支持不带 workspace 创建) --name string 文件名称 (必填) - --type string 文件类型 (必填): adoc=文档, axls=表格, appt=演示, adraw=白板, amind=脑图, able=多维表, folder=文件夹 - --folder string 目标文件夹 ID 或 URL - --workspace string 目标知识库 ID 或 URL + --type string 文件类型: adoc=文档, axls=表格, appt=演示, adraw=白板, amind=脑图, able=多维表, folder=文件夹 + --folder string 父节点 ID (选填) ``` +> 说明:`wiki node create` **必须**带 `--workspace`(知识库上下文),不能像旧 `doc file create` 那样在「我的文档」个人空间直接建。要在个人空间/钉盘建普通文件夹,用 `dws drive mkdir --name "<名称>"`(见 dingtalk-drive skill)。 + ### 更新文档内容 ``` Usage: @@ -154,31 +131,24 @@ Flags: --convert 是否转换为钉钉在线文档 ``` -### 下载文件到本地 +### 下载文件到本地(已迁移到 drive) + +> **弃用提示**:`dws doc download` 已迁移到 `dws drive download`(真机执行 `doc download` 会打印弃用警告)。下载已有文件(PDF/图片/附件等非在线文档)改用: + ``` -Usage: - dws doc download [flags] -Example: - dws doc download --node - dws doc download --node --output ./report.pdf - dws doc download --node "https://alidocs.dingtalk.com/i/nodes/" --output ~/downloads/ -Flags: - --node string 文件节点 ID 或 URL (必填) - --output string 本地保存路径 (文件路径或目录,必填) +dws drive download --node --output ~/downloads/ +dws drive download --node --output ./report.pdf ``` -### 创建文件夹 -``` -Usage: - dws doc folder create [flags] -Example: - dws doc folder create --name "项目资料" - dws doc folder create --name "子文件夹" --folder -Flags: - --name string 文件夹名称 (必填) - --folder string 父文件夹 ID 或 URL - --workspace string 目标知识库 ID -``` +> 注意区分:**在线文档(ALIDOC)导出为 docx** 仍走 `doc export`(内容级命令,未迁移);`download` 只下载已有文件。命令详情见 dingtalk-drive skill。 + +### 创建文件夹(已迁移) + +> **弃用提示**:`dws doc folder create` 已弃用。创建文件夹改用: +> - 个人空间/钉盘 → `dws drive mkdir --name "<名称>" [--folder <父节点>]` +> - 知识库内 → `dws wiki node create --workspace --name "<名称>" --type folder` +> +> 命令详情见 dingtalk-drive skill。 ### 复制文档/文件 ``` @@ -459,7 +429,7 @@ CLI **不会**自动执行回读验证。**Agent 必须在文档写入完成后 Usage: dws doc delete [flags] Example: - dws doc delete --node --format json # 查询 nodeId: dws doc search --query "..." 或 dws doc list + dws doc delete --node --format json # 查询 nodeId: dws drive search --query "..." 或 dws drive list Flags: --node string 文档/文件 ID 或 URL (必填) ``` @@ -604,7 +574,7 @@ Flags: - `dentryUuid` 是 `alidocs` URL `/i/nodes/{dentryUuid}` 的最后一段,在 `doc` 场景中等价于可传入 CLI 的 `nodeId`;不要把它改写成数字 ID。 - `dentryId` 通常是纯数字,**不是** `doc` 的 `nodeId`,也不是 `doc --folder` 的目标文件夹 ID;不要把数字 `dentryId` 当作 `--node`、`--folder`,也不要当作隐藏兼容 alias(如 `--parent-id` / `--parent-folder-id`)的取值。 - 目标父文件夹的 canonical flag 是 `--folder `,目标知识库使用 `--workspace `。CLI 可能接受 `--parent-id` / `--parent-folder-id` 作为隐藏兼容 alias,但 skill 正文和示例应统一教 `--folder`,不要把 drive/chat 链路里的数字 parentId 搬到 doc 命令。 -- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `doc search` / `doc list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。 +- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `drive search` / `drive list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。 ### 处理流程 @@ -612,15 +582,15 @@ Flags: 用户输入含 alidocs.dingtalk.com URL → 若是用户直接提供的原始 URL,先按链接规范做 probe → 提取 DOC_ID(URL 路径最后一段) - → 结合用户意图选择命令(doc 默认 read,folder 默认 list,file 默认 download) + → 结合用户意图选择命令(doc 默认 read,folder 默认 drive list,file 默认 drive download) → 将 DOC_ID 传给 --node 参数 ``` ## 意图判断 用户说"找文档/搜文档/最近文档": -- 搜索 → `search` -- 浏览 → `list` +- 搜索 → `dws drive search`(全局)或 `dws wiki node search --workspace `(指定知识库) +- 浏览 → `dws drive list`(`doc search` / `doc list` 已弃用,见本文顶部「搜索 / 遍历文件」) 用户说"看文档/读内容/文档内容": - 读取 → `read` (需文档 ID 或 URL) @@ -637,27 +607,29 @@ Flags: > - 用户说"创建多维表格/新建 AI 表格/建 base/数据库表" → 走 [`dws aitable base create`](./aitable.md)(able 多维表格) 用户说"新建脑图/白板/多维表/演示文稿": -- 用 `file create --type` 指定类型 (axls/amind/adraw/able/appt/adraw) +- 用 `dws wiki node create --workspace --type ` 指定类型 (axls/amind/adraw/able/appt);`doc file create` 已弃用 +- 注意 `wiki node create` 必须带 `--workspace`(知识库上下文) 用户说"建文件夹/新建目录": -- 创建 → `folder create` 或 `file create --type folder` +- 个人空间/钉盘 → `dws drive mkdir --name "<名称>"` +- 知识库内 → `dws wiki node create --workspace --type folder`(`doc folder create` / `doc file create --type folder` 已弃用) 用户说"上传文件/传文件/上传到文档/上传到知识库": - 上传 → `upload`(需本地文件路径) - 上传并转换 → `upload --convert` 用户说"下载/导出/下载到本地/导出文档/导出为Word/导出为docx/把文档导出来": -- **必须先判断目标文件类型**,再决定走 `export` 还是 `download`: - - 在线文档(alidocs/adoc)→ **`export`**(格式转换后导出为 docx) - - 已有文件(PDF、图片、附件、视频等非在线文档)→ **`download`**(直接下载原始文件) +- **必须先判断目标文件类型**,再决定走 `doc export` 还是 `drive download`: + - 在线文档(alidocs/adoc)→ **`doc export`**(内容级命令,格式转换后导出为 docx,未迁移) + - 已有文件(PDF、图片、附件、视频等非在线文档)→ **`drive download`**(`doc download` 已弃用) - 判断方法: - 1. 用户明确说"导出文档"/"导出为 Word/docx" → 直接走 `export` - 2. 用户明确说"下载 PDF/图片/附件" → 直接走 `download` - 3. 不确定时,先用 `info --node ` 查询节点信息,根据 `contentType` 判断: - - `contentType` = `ALIDOC` → 走 `export` - - `contentType` = `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `download` + 1. 用户明确说"导出文档"/"导出为 Word/docx" → 直接走 `doc export` + 2. 用户明确说"下载 PDF/图片/附件" → 直接走 `drive download` + 3. 不确定时,先用 `doc info --node ` 查询节点信息,根据 `contentType` 判断: + - `contentType` = `ALIDOC` → 走 `doc export` + - `contentType` = `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `drive download` -> **严禁将"导出文档"直接路由到 `download`**。`download` 只能下载已有文件(原样下载),`export` 是将在线文档格式转换后导出为 docx,两者完全不同。 +> **严禁将"导出文档"直接路由到 `drive download`**。`drive download` 只能下载已有文件(原样下载),`doc export` 是将在线文档格式转换后导出为 docx,两者完全不同。 用户说"复制文档/拷贝一份/复制到": - 复制 → `copy` (需源 --node 和目标 --folder/--workspace) @@ -702,7 +674,7 @@ Flags: **用户直接粘贴文档 URL(无其他指令)**: - 默认 → `read`(读取文档内容) -- 如 URL 明显是文件夹 → `list`(列出文件夹内容) +- 如 URL 明显是文件夹 → `drive list`(列出文件夹内容;doc list 已弃用) **用户粘贴 URL + 附加指令**: - "帮我看看这个文档" → `read` @@ -728,7 +700,7 @@ Flags: **用户直接粘贴文档 URL(无其他指令)**: - 默认 → `read`(读取文档内容) -- 如 URL 明显是文件夹 → `list`(列出文件夹内容) +- 如 URL 明显是文件夹 → `drive list`(列出文件夹内容;doc list 已弃用) **用户粘贴 URL + 附加指令**: - "帮我看看这个文档" → `read` @@ -743,11 +715,11 @@ Flags: ```bash # ── 工作流 1: 浏览并阅读文档 ── -# 1. 浏览我的文档根目录 -dws doc list --format json +# 1. 浏览我的文档/钉盘根目录(doc list 已弃用 → drive list) +dws drive list --format json # 2. 浏览子文件夹 -dws doc list --folder --format json +dws drive list --folder --format json # 3. 获取文档元信息 (标题、类型、权限) dws doc info --node --format json @@ -757,8 +729,8 @@ dws doc read --node --format json # ── 工作流 2: 创建文档并写入内容 ── -# 1. (可选) 创建文件夹 — 提取 nodeId -dws doc folder create --name "项目资料" --format json +# 1. (可选) 创建文件夹 — 提取 nodeId(doc folder create 已弃用 → drive mkdir) +dws drive mkdir --name "项目资料" --format json # 2. 创建文档 — 提取 nodeId dws doc create --name "项目周报" --folder --format json @@ -782,18 +754,18 @@ dws doc upload --file ./slides.pptx --name "Q1汇报.pptx" --folder --convert # ── 工作流 5: 下载/导出文件到本地 ── -# 必须先用 info 判断文件类型,再决定用 download 还是 export: -# - contentType 为 ALIDOC(在线文档)→ 用 export -# - contentType 为 DOCUMENT/IMAGE/VIDEO 等(已有文件)→ 用 download +# 必须先用 info 判断文件类型,再决定用 drive download 还是 doc export: +# - contentType 为 ALIDOC(在线文档)→ 用 doc export +# - contentType 为 DOCUMENT/IMAGE/VIDEO 等(已有文件)→ 用 drive download # 步骤 1: 查询文件类型 dws doc info --node --format json # 根据返回的 contentType 字段判断: -# 如果是已有文件 (非 ALIDOC),用 download: -dws doc download --node --output ~/downloads/ +# 如果是已有文件 (非 ALIDOC),用 drive download(doc download 已弃用): +dws drive download --node --output ~/downloads/ -# 如果是在线文档 (ALIDOC),用 export: +# 如果是在线文档 (ALIDOC),用 doc export: dws doc export --node --output ~/downloads/ # ── 工作流 6: 上传附件并插入文档 ── @@ -842,10 +814,10 @@ dws doc block delete --node --block-id --yes # 获取 nodeId 的三种方式(按场景选择,无需全部执行): # 方式 A: 用户直接提供文档 URL — 直接传给 --node,无需额外查询 -# 方式 B: 搜索文档 — 从返回中提取 nodeId -dws doc search --query "项目周报" --format json -# 方式 C: 浏览文件夹 — 从返回中提取 nodeId -dws doc list --folder --format json +# 方式 B: 搜索文档 — 从返回中提取 nodeId(doc search 已弃用 → drive search) +dws drive search --query "项目周报" --format json +# 方式 C: 浏览文件夹 — 从返回中提取 nodeId(doc list 已弃用 → drive list) +dws drive list --folder --format json # 注意: 这里的 是文件夹 nodeId/dentryUuid 或文件夹 URL,不是数字 dentryId;示例统一使用 canonical --folder。 # 复制文档到指定文件夹(--node 支持 ID 或 URL) @@ -907,19 +879,16 @@ dws doc export --node --output ./exported.docx | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `list` | `nodes[].nodeId` | read / info / update / block 操作的 --node | -| `list` | folder 类型的 `nodeId` | list 的 --folder, create 的 --folder | -| `search` | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update 的 --node;创建时间与创建者信息 | +| `drive list`(原 `doc list`,已弃用) | `nodes[].nodeId` / folder 类型 `nodeId` | read / info / update / block 的 --node;folder 用作 `--folder` | +| `drive search`(原 `doc search`,已弃用) | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update 的 --node;创建时间与创建者信息 | | `create` | `nodeId` | update / block 操作的 --node | -| `folder create` | `nodeId` | create / list / upload 的 --folder | +| `drive mkdir`(原 `doc folder create`,已弃用) | `nodeId` | create 的 --folder | | `block list` | `blockId` | block insert 的 --ref-block, block update/delete 的 --block-id | -| `upload` | `nodeId` / URL | 上传后文件的访问链接 | -| `download` | 本地文件路径 | 下载后的文件保存位置 | | `comment list` | `commentList[].commentKey` | comment reply 的 --comment-key | | `comment create` / `comment create-inline` | `commentKey` | comment reply 的 --comment-key | | `block list` | `blockId` + 文本内容 | comment create-inline 的 --block-id 及 --start/--end 计算 | | `contact user search` | `userId` | comment create / create-inline / reply 的 --mention | -| `file create` | `nodeId` | 后续 read / update / block 操作的 --node(仅 adoc 支持 read/update,axls/amind 等类型用各自产品的命令) | +| `wiki node create`(原 `doc file create`,已弃用) | `nodeId` | 后续 read / update / block 操作的 --node(仅 adoc 支持 read/update,axls/amind 等类型用各自产品的命令) | | `copy` / `move` | 新 `nodeId`(copy)或原 nodeId(move) | 后续 read / info 等的 --node | ## nodeId 多格式说明 @@ -998,7 +967,7 @@ EOF - `read` 返回 Markdown 格式的文档内容,仅限有"下载"权限的文档 - `read` 返回的内容中,文档里的附件会以 OSS 临时下载链接形式给出(如 `https://alidocs2.oss-cn-zhangjiakou.aliyuncs.com/res/.../att/.ext?Expires=...`),该链接会过期。链接过期后,可从 URL 路径中提取 ``(即 `/att/` 后、扩展名前的 UUID 部分),然后使用 `media download --node --resource-id ` 重新获取下载链接 - `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录 -- `create` 只能建"文档"(adoc);要建表格/脑图/白板/多维表/演示/文件夹,用 `file create --type` +- `create` 只能建"文档"(adoc);要建表格/脑图/白板/多维表/演示,用 `dws wiki node create --workspace --type `(`doc file create` 已弃用);建普通文件夹用 `dws drive mkdir` - `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;简单内容追加建议用 `update --mode append` - `block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型 (table, callout 等) 使用 `--element` JSON - `--content` 参数中的换行必须使用**真实换行符**(即实际的换行字符,Unicode `U+000A`),而不是字面量字符串 `\n`(反斜杠加字母 n)。在通过程序或大模型构造此参数时,请确保字符串在发送前已正确反转义。如果传入的是两个字符的字面量 `\n`,所有内容将渲染在同一行,导致标题、段落和表格格式全部错乱。**含多行/表格/长文本时优先用 `--content-file path.md` 或 `--content -`(stdin),不经过 shell escape,换行和表格都保持原样**(详见下方「长 Markdown 写入」)。 diff --git a/skills/multi/dingtalk-doc/references/doc/doc-file-ops.md b/skills/multi/dingtalk-doc/references/doc/doc-file-ops.md index f375a2bd..cabaaa11 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-file-ops.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-file-ops.md @@ -3,6 +3,11 @@ > **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: > 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 +> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令在真机上虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令: +> - `doc download` → **`dws drive download`**(下载已有文件;在线文档导出 docx 仍走 `doc export`) +> - `doc folder create` → **`dws drive mkdir`**(个人空间/钉盘)或 **`dws wiki node create --workspace --type folder`**(知识库内) +> - `doc upload` / `doc copy` / `doc move` / `doc rename` / `doc delete` 也在同一迁移方向(→ `drive`),命令用法以 `drive` 为准。 + --- ## doc upload(上传文件到钉钉文档/知识库) @@ -130,7 +135,7 @@ Flags: Usage: dws doc delete [flags] Example: - dws doc delete --node --format json # 查询 nodeId: dws doc search --query "..." 或 dws doc list + dws doc delete --node --format json # 查询 nodeId: dws drive search --query "..." 或 dws drive list Flags: --node string 文档/文件 ID 或 URL (必填) ``` diff --git a/skills/multi/dingtalk-doc/references/doc/doc-list.md b/skills/multi/dingtalk-doc/references/doc/doc-list.md index a05a6293..17566a70 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-list.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-list.md @@ -1,58 +1,24 @@ -# doc list(遍历文件列表) +# doc list(已弃用 → drive list / wiki node list) -> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: -> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 -> -> **同任务常配合**:[`doc-search.md`](./doc-search.md)(关键字检索更精准)/ [`doc-info.md`](./doc-info.md)(拿到 nodeId 后查元信息) +> **弃用提示**:`dws doc list` 已迁移,不再是 `doc` 子命令。真机上它虽仍能跑,但每次都会打印弃用警告: +> `'dws doc list' is deprecated, use 'dws drive list --workspace ' or 'dws wiki node list --workspace '`。 +> 遍历文件夹/知识库请切到 `dingtalk-drive` skill,用下面的命令。 -## 命令格式 +## 改用什么 -``` -Usage: - dws doc list [flags] -Example: - dws doc list - dws doc list --folder - dws doc list --workspace --limit 20 -Flags: - --folder string 文档文件夹 nodeId 或 alidocs 文件夹 URL;不要传 drive dentryId/parent-id 这类纯数字 ID - --workspace string 知识库 ID - --limit int 每页数量 (默认 50,最大 50) - --cursor string 分页游标 (从上次结果的 nextPageToken 获取) -``` +| 旧命令(弃用) | 改用 | 场景 | +|--------------|------|------| +| `dws doc list` | `dws drive list` | 遍历「我的文档」/钉盘根目录 | +| `dws doc list --folder ` | `dws drive list --folder ` | 遍历指定文件夹 | +| `dws doc list --workspace ` | `dws drive list --workspace ` 或 `dws wiki node list --workspace ` | 遍历知识库 | -## 关键说明 - -- 不传任何 flag 时遍历"我的文档"根目录。 -- `--folder` 仅接受文档文件夹 `nodeId` / `dentryUuid` / alidocs 文件夹 URL;**禁止**传入 drive `dentryId`、`parentId`、`spaceId` 这类纯数字 ID。 -- 单页最多 50 条,需翻页时使用 `nextPageToken` → `--cursor`。 +`drive list` 的完整参数(`--folder` / `--workspace` / `--limit` / `--cursor` / `--order-by` 等)见 `dingtalk-drive` skill。 ## 上下文传递 -| 从返回中提取 | 用于 | -|-------------|------| -| `nodes[].nodeId` | [`doc-read.md`](./doc-read.md) / [`doc-info.md`](./doc-info.md) / [`doc-update.md`](./doc-update.md) / [`doc-file-ops.md`](./doc-file-ops.md) 等所有 `--node` 入参 | -| folder 类型的 `nodeId` | 当前 `list --folder` 递归遍历;[`doc-create.md`](./doc-create.md) / [`doc-file-ops.md`](./doc-file-ops.md) 的 `--folder` | - -## 常用模板 - -```bash -# 浏览"我的文档"根目录 -dws doc list --format json - -# 浏览指定文档文件夹(folder nodeId 或 alidocs 文件夹 URL) -dws doc list --folder --format json -dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/" --format json - -# 浏览指定知识库根目录(取出 workspaceId 后) -dws doc list --workspace --limit 20 --format json - -# 翻页 -dws doc list --folder --cursor --format json -``` +从返回里取 `nodes[].nodeId`(folder 类型的 `nodeId` 可继续作 `--folder`),传给 `doc read` / `doc info` / `doc update` 等内容级命令的 `--node`。 ## 参考 -- [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令) -- [`./doc-search.md`](./doc-search.md)(关键字检索路径) +- `dingtalk-drive` skill(列表 / 搜索 / 上传下载等文件管理命令的归属) - [`./doc-info.md`](./doc-info.md)(拿到 nodeId 后查元信息) diff --git a/skills/multi/dingtalk-doc/references/doc/doc-search.md b/skills/multi/dingtalk-doc/references/doc/doc-search.md index ea6aa4ca..7977007c 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-search.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-search.md @@ -1,80 +1,24 @@ -# doc search(搜索文档) +# doc search(已弃用 → drive search / wiki node search) -> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: -> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 -> -> **同任务常配合**:[`doc-list.md`](./doc-list.md)(目录遍历,互补于关键字搜索)/ [`doc-info.md`](./doc-info.md)(拿到 nodeId 后查元信息)/ [`doc-read.md`](./doc-read.md)(拿到 nodeId 后读取正文) +> **弃用提示**:`dws doc search` 已迁移,不再是 `doc` 子命令。真机上它虽仍能跑,但每次都会打印弃用警告: +> `'dws doc search' is deprecated, use 'dws drive search' or 'dws wiki node search --workspace '`。 +> 搜索文档/文件请切到 `dingtalk-drive` skill,用下面的命令。 -## 命令格式 +## 改用什么 -``` -Usage: - dws doc search [flags] -Example: - dws doc search --query "会议纪要" - dws doc search - dws doc search --extensions pdf,docx - dws doc search --query "方案" --created-from 1700000000000 --created-to 1710000000000 - dws doc search --creator-uids uid1,uid2 - dws doc search --workspace-ids wsId1,wsId2 -Flags: - --query string 搜索关键词 (不传则返回最近访问) - --extensions strings 按文件扩展名过滤,不含点号,逗号分隔 (如 pdf,docx,png)。 - 钉钉在线文档: adoc(文字) axls(表格) appt(演示文稿) awbd(白板) adraw(画板) amind(脑图) able(多维表格) aform(收集表) - 常见附件: pdf docx doc xlsx xls pptx ppt csv txt md json xml zip rar png jpg jpeg gif mp4 mp3 - 以上仅为参考,extensions 为开放参数,服务端支持的扩展名不限于此。不确定文件后缀时,建议不传 --extensions 让搜索返回所有类型,再从结果中按文件名后缀筛选 - --created-from int 创建时间起始 (毫秒时间戳,含) - --created-to int 创建时间截止 (毫秒时间戳,含) - --visited-from int 访问时间起始 (毫秒时间戳,含) - --visited-to int 访问时间截止 (毫秒时间戳,含) - --creator-uids strings 按创建者用户 ID 过滤,逗号分隔 - --editor-uids strings 按编辑者用户 ID 过滤,逗号分隔 - --mentioned-uids strings 按 @提及的用户 ID 过滤,逗号分隔 - --workspace-ids strings 按知识库 ID 过滤,支持知识库 URL,逗号分隔 - --limit int 每页数量 (默认 10,最大 30) - --cursor string 分页游标 (从上次结果的 nextPageToken 获取) -``` +| 旧命令(弃用) | 改用 | 场景 | +|--------------|------|------| +| `dws doc search --query "<关键词>"` | `dws drive search --query "<关键词>"` | 全局搜文档/文件(跨钉盘+文档空间聚合) | +| `dws doc search --extensions pdf,docx` | `dws drive search --query "..." --extensions pdf,docx` | 按扩展名过滤 | +| `dws doc search --workspace-ids ` | `dws wiki node search --workspace --query "<关键词>"` | 指定知识库内搜索 | -## 关键说明 - -- 不传 `--query` 时返回最近访问列表,适合"最近文档"类意图。 -- `--extensions` 是开放参数,传入服务端不识别的扩展名时不会报错(可能也搜不到);优先通过文件名后缀人工筛选。 -- 多个时间戳为毫秒时间戳,注意单位(不是秒)。 +`drive search` 的完整参数(`--query` / `--extensions` / `--created-from` / `--creator-uids` / `--limit` / `--cursor` 等)见 `dingtalk-drive` skill。 ## 上下文传递 -| 从返回中提取 | 用于 | -|-------------|------| -| 文档 `nodeId` / URL | [`doc-read.md`](./doc-read.md) / [`doc-info.md`](./doc-info.md) / [`doc-update.md`](./doc-update.md) / [`doc-file-ops.md`](./doc-file-ops.md) 的 `--node` | -| `createTime` / `creatorUid` | 创建时间与创建者过滤的二次检索 | - -## 常用模板 - -```bash -# 关键字搜索(最常用) -dws doc search --query "项目周报" --format json - -# 仅最近访问(不传 --query) -dws doc search --format json - -# 按扩展名过滤(在线文档族 + 常见办公附件) -dws doc search --extensions adoc,axls,able,docx,xlsx,pdf - -# 按创建时间窗口(毫秒时间戳) -dws doc search --query "方案" --created-from 1700000000000 --created-to 1710000000000 - -# 按创建者过滤(多个 uid 逗号分隔) -dws doc search --creator-uids uid1,uid2 - -# 按知识库范围过滤(支持知识库 URL) -dws doc search --workspace-ids wsId1,wsId2 - -# 翻页 -dws doc search --query "周报" --limit 30 --cursor -``` +拿到返回里的文档 `nodeId` / URL 后,`doc read` / `doc info` / `doc update` / `doc block` 等内容级命令照常用。 ## 参考 -- [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令) -- [`./doc-list.md`](./doc-list.md)(目录遍历替代路径) +- `dingtalk-drive` skill(搜索 / 列表 / 上传下载等文件管理命令的归属) - [`./doc-info.md`](./doc-info.md)(URL → nodeId 提取) diff --git a/skills/multi/dingtalk-doc/references/url-patterns.md b/skills/multi/dingtalk-doc/references/url-patterns.md index 4b206ad9..c20a5a25 100644 --- a/skills/multi/dingtalk-doc/references/url-patterns.md +++ b/skills/multi/dingtalk-doc/references/url-patterns.md @@ -98,7 +98,7 @@ Step 3 → 按下方路由规则映射到对应产品 > axls vs xlsx 关键区分: > - `axls`(钉钉在线电子表格,`contentType=ALIDOC`)→ 走 `sheet` 产品线(读/写/筛选/导出等服务端原子操作) > - `xlsx` / `xls` / `xlsm` / `csv`(上传到文档空间的本地表格文件,`contentType=DOCUMENT`)→ 必须走 `dws doc download` 下载到本地后再解析处理,严禁错误路由到 `sheet` 产品线(sheet 命令只支持在线表格,调用 xlsx 节点会直接报错) -> - 用户想把在线表格导出为 xlsx 文件 → 开源 dws CLI 暂未暴露在线表格导出能力(旧动态 schema 曾包含 `submit_export_job` / `query_export_job`,但当前 cobra 未注册),需要在钉钉客户端手动导出 xlsx +> - 用户想把在线表格导出为 xlsx 文件 → 用 `dws sheet export --node <表格ID或URL>`(单命令一站式:提交导出任务→轮询→取 downloadUrl,加 `--output ./x.xlsx` 可直接落盘) ### 示例 @@ -136,8 +136,8 @@ dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/ghi789" --format jso | extension / contentType | 读取 | 写入 | 删除 | 导出 | 权限 | 媒体 | |-------------------------|------|------|------|------|------|------| | **adoc**(在线文档) | `doc read` | `doc update` / `doc block update` | ⚠️ `doc delete` | ⚠️ `doc export` (→ docx) | ⚠️ `doc permission *` | ⚠️ `doc media download/insert` | -| **axls**(在线电子表格) | `sheet range read` / `sheet list` | `sheet range write` / `sheet append` | ⚠️ `doc delete`(节点删除) | `sheet submit_export_job` + `sheet query_export_job`(待吴淼 W-01 收敛为单命令 `sheet export`) | ⚠️ `doc permission *`(节点级,跨产品) | 不适用 | -| **able**(在线多维表) | `aitable base get` / `aitable record query` | `aitable record create/update` | ⚠️ `doc delete`(节点删除)或 `aitable base delete --yes` | `aitable export data --output ./x.xlsx` | ⚠️ `doc permission *`(节点级) | `aitable attachment upload-file` | +| **axls**(在线电子表格) | `sheet range read` / `sheet list` | `sheet range update` / `sheet append` | ⚠️ `doc delete`(节点删除) | `sheet export`(单命令一站式:提交→轮询→下载,可选 `--output` 落盘) | ⚠️ `doc permission *`(节点级,跨产品) | 不适用 | +| **able**(在线多维表) | `aitable base get` / `aitable record query` | `aitable record create/update` | ⚠️ `doc delete`(节点删除)或 `aitable base delete --yes` | `aitable export data --scope all --format excel`(取 downloadUrl,`--output` 不落盘) | ⚠️ `doc permission *`(节点级) | `aitable attachment upload` | | **xlsx / xls / xlsm / csv**(本地表格文件) | `doc download` → 本地用 xlsx skill 解析 | 不支持服务端写(先下载改本地再上传) | ⚠️ `doc delete`(节点删除) | 不需要(本身就是 xlsx) | ⚠️ `doc permission *` | 不适用 | | **普通文件** (nodeType=file) | `doc download` | 不支持服务端写 | ⚠️ `doc delete` | 不需要 | ⚠️ `doc permission *` | 不适用 | | **文件夹** (nodeType=folder) | `doc list --folder ` | `doc create --folder ...` | ⚠️ `doc delete` | 不适用 | ⚠️ `doc permission *` | 不适用 | diff --git a/skills/multi/dingtalk-drive/SKILL.md b/skills/multi/dingtalk-drive/SKILL.md index e1fa4cd9..e5812381 100644 --- a/skills/multi/dingtalk-drive/SKILL.md +++ b/skills/multi/dingtalk-drive/SKILL.md @@ -27,23 +27,28 @@ metadata: | 用户说 | 命令 | |--------|------| -| "看钉盘空间 / 团队文件 / 有哪些 space" | `dws drive list-spaces` | -| "看钉盘文件 / 文件夹列表" | `dws drive list --space-id [--parent-id ]` | +| "看钉盘文件 / 文件夹列表" | `dws drive list --limit 20 [--folder ]` | +| "按名字搜文件(不知道在哪)" | `dws drive search --query "<关键词>"` | +| "看钉盘空间 / 团队文件 / 有哪些 space" | `dws wiki space list --type orgSpace`(`drive list-spaces` 已 deprecated) | +| "最近访问 / 最近编辑的文档" | `dws drive recent` | | "钉盘目录树" | `python scripts/drive_tree_list.py --depth 2` | -| "查文件元数据" | `dws drive info --space-id --file-id ` | -| "下载文件" | `dws drive download --space-id --file-id --output ` | -| "上传本地文件(首选一键)" | `dws drive upload --file ./report.pdf [--folder ]` | -| "上传文件(手动三步)" | `dws drive upload-info --space-id --file-name <名> --file-size [--parent-id ]` → 客户端 HTTP PUT → `dws drive commit --space-id --upload-id --file-name <名> --file-size [--parent-id ]` | -| "建文件夹" | `dws drive mkdir --space-id --name "<名称>" [--parent-id ]` | -| "删除文件 / 移到回收站(需确认)" | `dws drive delete --file-id --yes` | +| "查文件元数据" | `dws drive info --node ` | +| "下载文件" | `dws drive download --node --output ` | +| "上传本地文件" | `dws drive upload --file ./report.pdf [--folder ]` | +| "建文件夹" | `dws drive mkdir --name "<名称>" [--folder ]` | +| "复制 / 移动 / 重命名" | `dws drive copy` / `move` / `rename --node --name "<主名>"` | +| "删除文件 / 移到回收站(需确认)" | `dws drive delete --node --yes` | +| "回收站 / 还原" | `dws drive recycle list` / `recycle restore --id ` | +| "公开 / 取消公开 / 查公开状态" | `dws drive publish set` / `unset` / `get --node ` | ## 评测高频硬约束 -- 查找文件不要只看根目录后放弃;根目录没命中时,进入最相关的评测/目标文件夹继续 `drive list --space-id --parent-id `,必要时用目录树脚本递归到合理深度。 -- `drive list` 默认 `--max 20`,评测里保守使用 `--max 50` 以内并处理 `nextToken` 翻页;不要因为参数边界报错反复重试。 -- `dws drive` 当前没有 search 子命令,按目录递归 `drive list`;命中后必须 `drive info --space-id --file-id --format json` 回读元数据。 +- 找文件优先用 `drive search --query "<关键词>"`(不知道位置时全局搜);只有需要逐层浏览时才用 `drive list`。命中后必须 `drive info --node --format json` 回读元数据。 +- **ID 字段选择**:`drive list` 返回同时有 `dentryId`(纯数字)和 `fileId`(UUID 格式)。所有 `--node` 和 `--folder` 参数**必须用 `fileId`**,纯数字 `dentryId` 会被拒绝。 +- `drive list` 默认 `--limit 20`,最大 50;要更多用 `--cursor` 翻页,不要因参数边界报错反复重试。 +- `rename` 的 `--name` **只传主名,不带扩展名**;服务端会按原扩展名自动补后缀,带了扩展名会变成双扩展名(如 `报告.txt` → `报告.txt.txt`)。 - `drive download` 需要 `--output` 指定本地保存路径或目录;不要省略必填输出位置。 -- 删除、覆盖、移动等破坏性操作必须确认;上传(upload-info + commit 两步)、创建文件夹、下载后要读回或列目录验证。 +- 删除、覆盖、移动、公开(publish set/unset)等破坏性操作必须先确认;上传、创建文件夹、下载后要读回或列目录验证。 - 所有 `dws drive` 命令加 `--format json`。 ## 跨产品协作 diff --git a/skills/multi/dingtalk-drive/references/drive.md b/skills/multi/dingtalk-drive/references/drive.md index b13f4d59..30b6a3c8 100644 --- a/skills/multi/dingtalk-drive/references/drive.md +++ b/skills/multi/dingtalk-drive/references/drive.md @@ -12,6 +12,7 @@ dws drive --help # 查看具体命令的完整参数说明 dws drive list --help +dws drive search --help dws drive upload --help dws drive download --help ``` @@ -23,53 +24,44 @@ dws drive download --help ## 命令总览 -### 列出钉盘空间 -``` -Usage: - dws drive list-spaces [flags] -Example: - dws drive list-spaces --format json - dws drive list-spaces --space-type orgSpace --max 50 --format json -Flags: - --max int 每页数量 (默认 20) - --next-token string 分页游标 (从上次结果的 nextToken 获取) - --space-type string 空间类型过滤 -``` +### 获取文件/文件夹列表 -> 适用场景:复制/移动文件到「我的文件」或团队空间根目录时,先取 `rootFolderId`;或者枚举用户可访问的团队空间。 -> 注意:由当前二进制静态注册;如果当前版本未暴露或后端工具未注册,调用前可用 `dws drive list-spaces --help` / `--dry-run` 验证。 - -### 列出钉盘目录 ``` Usage: dws drive list [flags] Example: - dws drive list - dws drive list --parent-id - dws drive list --parent-id --max 20 --order-by modifiedTime --order desc + dws drive list --limit 20 + dws drive list --limit 20 --folder --order-by name --order asc Flags: - --parent-id string 父目录 ID (不传则列根目录) - --space-id string 钉盘空间 ID (一般无需指定) - --max int 每页数量 (默认 20) - --next-token string 分页游标 (从上次结果的 nextToken 获取) - --order-by string 排序字段: name / createdTime / modifiedTime - --order string 排序方向: asc / desc - --thumbnail 是否返回缩略图链接 + --limit int 每页返回数量,默认 20,最大 50 (可选) + --cursor string 分页游标,首次不传 (可选) + --order string 排序方向: asc|desc,默认 desc (可选,仅钉盘) + --order-by string 排序字段: createTime|modifyTime|name (可选,仅钉盘) + --folder string 父节点 ID (dentryUuid),不传则列出空间根目录 (可选) + --space-id string 钉盘空间 ID (纯数字),不传则使用「我的文件」对应 spaceId (可选) + --workspace string 文档空间/知识库 ID (加密 string 或 URL),传入则路由到文档空间 (可选) + --thumbnail 是否返回缩略图信息 (可选,仅钉盘) ``` +> 统一入口:默认列钉盘空间(`--space-id` 纯数字);传 `--workspace` 时路由到文档空间/知识库列表。 + ### 获取钉盘空间列表 +> **Deprecated**:推荐改用 `dws wiki space list --type orgSpace` / `--type mySpace`(见 [wiki.md](./wiki.md))。本命令仍可用,仅作兼容保留。 +> 适用场景:复制/移动文件到「我的文件」或团队空间根目录时,先取 `rootFolderId`;或者枚举用户可访问的团队空间。 + ``` Usage: dws drive list-spaces [flags] Example: - dws drive list-spaces - dws drive list-spaces --space-type mySpace - dws drive list-spaces --space-type orgSpace --max 20 --next-token + dws wiki space list --type orgSpace # 推荐 + dws wiki space list --type mySpace # 推荐 + dws drive list-spaces # deprecated + dws drive list-spaces --space-type orgSpace --limit 20 --cursor Flags: --space-type string 空间类型: orgSpace=企业空间(默认), mySpace=我的文件 (可选) - --max int 每页返回数量 (默认 20),仅 spaceType 为 orgSpace 时有效 - --next-token string 分页游标,仅企业空间支持分页 (可选) + --limit int 每页返回数量 (默认 20,最大 50),仅 spaceType 为 orgSpace 时有效 + --cursor string 分页游标,仅企业空间支持分页 (可选) ``` spaceType 筛选规则: @@ -79,32 +71,89 @@ spaceType 筛选规则: 返回字段说明: - `spaceId` — 空间 ID,用于 `list`/`info`/`upload` 等命令的 `--space-id` - `spaceName` — 空间名称(如"全员文件夹"、"我的文件") -- `rootFolderId` — 空间根目录的 dentryUuid,可作为 `doc copy/move` 的 `--folder` 参数 +- `rootFolderId` — 空间根目录的 dentryUuid,可作为 `drive copy/move` 的 `--folder` 参数 - `spaceType` — 空间类型(如 `orgSpace`) - `nextToken` — 若不为空,表示还有更多空间可查询(仅企业空间) -### 获取文件元信息 +### 搜索钉盘文件/文件夹/空间 + +按关键词全局搜索文件,默认同时搜索钉盘和文档空间,合并返回结果。不同于 `list`(需要明确的 spaceId/folder 逐层遍历),`search` 用于不知道具体位置、只记得名称/关键词的场景。 + +``` +Usage: + dws drive search [flags] +Example: + dws drive search --query "季度汇报" + dws drive search --query "合同" --target file --extensions pdf,docx + dws drive search --query "项目" --target space + dws drive search --query "方案" --created-from 1700000000000 --created-to 1710000000000 + dws drive search --query "周报" --creator-uids 012345 + dws drive search --query "报告" --limit 30 --cursor +Flags: + --query string 搜索关键词 (必填) + --target string 搜索范围: all(默认,聚合钉盘+文档空间) | file(仅钉盘文件) | space(仅钉盘空间) (可选) + --file-types strings 按文件内容类型过滤,逗号分隔: alidoc,document,image,video,audio,archive (仅 target=file/all 生效) + --extensions strings 按文件扩展名过滤,不含点号,逗号分隔 (如 pdf,docx,adoc) + --creator-uids strings 按创建者用户 ID 过滤,逗号分隔 + --created-from int 创建时间起始 (毫秒时间戳,含) + --created-to int 创建时间截止 (毫秒时间戳,含) + --modified-from int 修改时间起始 (毫秒时间戳,含) + --modified-to int 修改时间截止 (毫秒时间戳,含) + --limit int 每页返回数量(默认 10,最大 30) + --cursor string 分页游标,从上次返回的 nextCursor 获取 (可选) +``` + +搜索范围 (`--target`) 选择规则: +- `all`(默认):同时搜钉盘文件与文档空间,聚合返回 — 不确定目标位置时使用 +- `file`:只搜钉盘文件 / 文件夹,支持 `--file-types` / `--extensions` 过滤 — 明确是找钉盘文件时使用 +- `space`:只搜钉盘团队空间 — 明确知道空间名、需快速定位空间 spaceId/rootFolderId 时使用 + +结果中 `source` 字段区分来源:`drive` / `doc`。如果需要在某个知识库内搜索,请使用 `dws wiki node search --workspace `。 + +> **提示**:结果按相关性排序,首页未命中时优先调整关键词 / 补充 `--file-types`/`--extensions` 缩小范围 / 加上时间范围,而非反复翻页。 + +### 获取最近访问/编辑的文档列表 + +``` +Usage: + dws drive recent [flags] +Example: + dws drive recent + dws drive recent --operate-type 1 + dws drive recent --creator-type 1 --limit 10 + dws drive recent --file-types 0,1 --operate-type 0 +Flags: + --file-types ints 按文档类型过滤,逗号分隔 (参考 RecentAccessType 枚举) (可选) + --operate-type ints 按操作类型过滤: 0=最近访问(默认), 1=最近编辑; 不传默认仅返回最近访问(0) (可选) + --creator-type int 按创建人过滤: 0=全部(默认), 1=我创建, 2=他人创建 (可选) + --org-ids ints 按资源所属组织 ID 过滤,逗号分隔 (可选) + --limit int 每页数量 (默认 20,最大 20) (可选) + --cursor string 分页游标,从上次返回的 nextCursor 获取 (可选) +``` + +返回字段说明: +- `recentItems[]` — 最近访问/编辑的文档列表 + - `nodeId` — 文档节点 ID,可用于 `doc read/info/update` 的 `--node` + - `name` — 文档名称 + - `contentType` — 内容类型(如 ALIDOC) + - `extension` — 扩展名(如 adoc、axls、able) + - `docUrl` — 文档在线访问 URL + - `operateType` — 操作类型:LAUNCH=访问,EDIT=编辑 + - `accessTime` — 最近访问时间 + - `createTime` / `updateTime` — 创建/更新时间 +- `nextCursor` — 翻页游标,传入 `--cursor` 获取下一页 +- `hasMore` — 是否还有更多数据 + +### 获取文件元数据信息 + ``` Usage: dws drive info [flags] Example: - dws drive info --file-id + dws drive info --node Flags: - --file-id string 文件或文件夹 ID (必填) - --space-id string 钉盘空间 ID (一般无需指定) -``` - -### 创建文件夹 -``` -Usage: - dws drive mkdir [flags] -Example: - dws drive mkdir --name "项目资料" - dws drive mkdir --name "子目录" --parent-id -Flags: - --name string 文件夹名称 (必填) - --parent-id string 父目录 ID (不传则建在根目录) - --space-id string 钉盘空间 ID (一般无需指定) + --node string 节点 ID (dentryUuid) (必填) + --space-id string 节点所属空间 ID (可选) ``` ### 文件内容获取路由规则 @@ -114,69 +163,49 @@ Flags: | extension | 文件类型 | 操作 | 命令 | |-----------|---------|------|------| -| adoc | 在线文档 | 在线获取 Markdown 内容 | `dws doc read --node ` | -| axls | 在线表格 | 在线读取表格数据 | `dws sheet range read` | +| adoc | 在线文档 | 在线获取 Markdown 内容 | `dws doc read --node ` | +| axls | 在线表格 | 在线读取表格数据 | `dws sheet list` → `dws sheet range read` | | able | 多维表格 | 在线查询记录 | `dws aitable base get` → `dws aitable record query` | -| 其他(pdf/docx/txt/png 等) | 普通文件 | **不支持在线分析**,需用户主动下载后本地查看 | `dws drive download --file-id --output ` | - -### 删除文件/文件夹到回收站 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws drive delete [flags] -Example: - dws drive delete --file-id --format json # 查询 fileId: dws drive list -Flags: - --file-id string 文件/文件夹 ID (dentryUuid),即 drive list 返回的 fileId (必填) -``` - -注意:`--file-id` 使用的是 `drive list` 返回结果中的 `fileId` 字段(即 `dentryUuid`),**不是** `dentryId` 字段。 +| 其他(pdf/docx/txt/png 等) | 普通文件 | **不支持在线分析**,需用户主动下载后本地查看 | `dws drive download --node --output ` | ### 下载钉盘文件到本地 + +下载流程一步到位:获取下载 URL → HTTP GET 下载文件二进制内容到本地。 + ``` Usage: dws drive download [flags] Example: - dws drive download --file-id --output ./report.pdf + dws drive download --node --output ./report.pdf + dws drive download --node --output ~/downloads/ Flags: - --file-id string 文件 ID (必填) - --output string 本地保存路径 (文件路径或目录,必填) - --space-id string 钉盘空间 ID (一般无需指定) + --node string 文件 ID (dentryUuid) (必填) + --output string 本地保存路径 (必填),可以是文件路径或目录;如果指定目录,文件名从下载 URL 中自动推断 + --space-id string 文件所属空间 ID (可选) ``` -### 获取上传凭证 (上传第一步) +> **注意**:`--output` 是必填参数,不传会报错。 + +### 创建文件夹 + ``` Usage: - dws drive upload-info [flags] + dws drive mkdir [flags] Example: - dws drive upload-info --file-name "report.pdf" --file-size 102400 - dws drive upload-info --file-name "slides.pptx" --file-size 512000 --parent-id + dws drive mkdir --name "项目资料" + dws drive mkdir --name "子目录" --folder Flags: - --file-name string 文件名含后缀 (必填) - --file-size int 文件大小,单位字节 (必填) - --mime-type string MIME 类型 (可选,服务端会自动推断) - --parent-id string 父目录 ID (不传则上传到根目录) - --space-id string 钉盘空间 ID (一般无需指定) + --name string 文件夹名称,最长 50 字符 (必填) + --folder string 父节点 ID (dentryUuid),不传则在空间根目录下创建 (可选) + --space-id string 目标空间 ID,不传则使用「我的文件」 (可选) ``` -### 提交上传 (上传第三步) -``` -Usage: - dws drive commit [flags] -Example: - dws drive commit --file-name "report.pdf" --file-size 102400 --upload-id -Flags: - --file-name string 文件名含后缀 (必填,须与 upload-info 一致) - --file-size int 文件大小,单位字节 (必填,须与 upload-info 一致) - --upload-id string upload-info 返回的 uploadId (必填) - --parent-id string 父目录 ID (必填时须与 upload-info 一致) - --space-id string 钉盘空间 ID (一般无需指定) - --conflict-handler string 同名冲突策略: AUTO_RENAME / OVERWRITE / RETURN_DENTRY_IF_EXIST (默认 AUTO_RENAME) -``` +> `mkdir` 在钉盘空间创建文件夹;要在文档空间/知识库中创建文件夹,用 `dws wiki node create --type folder --workspace `(见 [wiki.md](./wiki.md))。 + +### 上传本地文件到钉盘 + +> **注意:** 上传文件首选 `dws drive upload` 一条命令(内部自动完成三步流程),不要手动走 `upload-info` + `curl` + `commit` 三步。 -### 一键上传 (三步合成) ``` Usage: dws drive upload [flags] @@ -186,15 +215,48 @@ Example: dws drive upload --file ./data.xlsx --folder Flags: --file string 本地文件路径 (必填) - --file-name string 文件显示名称 (默认使用本地文件名) - --folder string 父节点 ID (dentryUuid),不传则上传到空间根目录 - --space-id string 目标空间 ID,不传则使用「我的文件」 - --mime-type string 文件 MIME 类型,不传则自动推断 + --file-name string 文件显示名称 (默认使用文件名) + --folder string 父节点 ID (dentryUuid),不传则上传到空间根目录 (可选) + --space-id string 目标钉盘空间 ID,不传则使用「我的文件」 (可选) + --workspace string 目标知识库 ID,传入时路由到文档空间上传 (可选) + --convert 是否转换为钉钉在线文档 (仅文档空间上传时生效) + --mime-type string 文件 MIME 类型,不传则自动推断 (可选) ``` -> 客户端胶水命令:内部自动完成 `upload-info` → HTTP PUT 到 OSS → `commit_upload` 三步。 -> 适合大多数本地文件上传场景,无需 Agent 自己发 HTTP PUT。 -> 注意:`--folder` 接受 `dentryUuid`(UUID 格式),不要传纯数字 `dentryId`。 +`upload` 命令内部自动完成三步流程(获取凭证 → OSS PUT → 提交入库),无需手动分步操作。上传到知识库/文档空间时加 `--workspace` 参数。 + +### 获取上传凭证 (手动三步·仅特殊场景) + +> 仅当需要自定义流式上传、无法使用 `upload` 一条命令时才走手动三步:`upload-info` → HTTP PUT → `commit`。 + +``` +Usage: + dws drive upload-info [flags] +Example: + dws drive upload-info --file-name "report.pdf" --file-size 102400 + dws drive upload-info --file-name "slides.pptx" --file-size 512000 --folder +Flags: + --file-name string 文件名含后缀 (必填) + --file-size int 文件大小,单位字节 (必填) + --mime-type string MIME 类型 (可选,服务端会自动推断) + --folder string 父节点 ID (dentryUuid),不传则上传到空间根目录 (可选) + --space-id string 目标空间 ID,不传则使用「我的文件」 (可选) +``` + +### 提交上传 (手动三步·第三步) + +``` +Usage: + dws drive commit [flags] +Example: + dws drive commit --file-name "report.pdf" --file-size 102400 --upload-id +Flags: + --file-name string 文件名含后缀 (必填,须与 upload-info 一致) + --file-size int 文件大小,单位字节 (必填,须与 upload-info 一致) + --upload-id string upload-info 返回的 uploadId (必填) + --folder string 父节点 ID (dentryUuid),须与 upload-info 一致 (可选) + --space-id string 空间 ID,不传则使用「我的文件」 (可选) +``` ### 删除文件/文件夹到回收站 @@ -204,95 +266,205 @@ Flags: Usage: dws drive delete [flags] Example: - dws drive delete --file-id --yes --format json + dws drive delete --node --format json # 查询 fileId: dws drive list Flags: - --file-id string 文件或文件夹 nodeId(dentryUuid 格式,必填) + --node string 文件/文件夹 ID (dentryUuid),即 drive list 返回的 fileId (必填) Global Flags: --yes 跳过二次确认 (危险操作,建议先与用户确认) ``` > 由当前二进制静态注册(路由到 doc 服务的 `delete_document` tool);如果当前版本未暴露,调用前可用 `dws drive delete --help` 验证。 -> 删除是软删除(进回收站),但仍需用户明确确认;不要在自动化脚本里默认带 `--yes`。 +注意:`--node` 使用的是 `drive list` 返回结果中的 `fileId` 字段(即 `dentryUuid`),**不是** `dentryId` 字段。 +删除是软删除(进回收站),但仍需用户明确确认;不要在自动化脚本里默认带 `--yes`。 + +### 查看回收站文件列表 + +``` +Usage: + dws drive recycle list [flags] +Example: + dws drive recycle list + dws drive recycle list --space-id 12345 --limit 10 +Flags: + --space-id string 钉盘空间 ID (选填,不传则返回所有空间) + --limit int 返回条数上限 (默认 20,最大 50) + --cursor string 分页游标 (选填) +``` + +### 还原回收站文件 + +``` +Usage: + dws drive recycle restore [flags] +Example: + dws drive recycle restore --id +Flags: + --id string 回收项 ID (必填,从 recycle list 获取) +``` + +> **注意**:还原操作可能是异步的(返回 `async=true` 和 `taskId`)。 ## 意图判断 -用户说"钉盘有什么空间/团队文件/列空间" → `list-spaces` -用户说"钉盘有什么文件/列钉盘/看钉盘目录" → `list` -用户说"钉盘文件详情/文件信息" → `info` (需 fileId) -用户说"新建钉盘目录/钉盘里建文件夹" → `mkdir` -用户说"下载钉盘文件/把这个文件拿下来" → `download` 指定 `--output` 保存到本地 -用户说"上传文件到钉盘/把本地文件传钉盘": -- **首选 `upload`**:一条命令自动完成三步(适合本地文件直接上传) -- 也可手动三步:`upload-info` → HTTP PUT 到预签名 URL → `commit`(适合自定义流式上传,或不想在客户端做 HTTP PUT 的场景) -用户说"删除钉盘文件/移到回收站" → `delete`(不可逆,必须确认) +用户说"我的文件/钉盘/网盘/云盘" → `list` +用户说"最近访问/最近打开/最近编辑/最近文档" → `recent`(默认仅最近访问,`--operate-type 1` 仅最近编辑,`--operate-type 0,1` 全部) +用户说"钉盘空间/团队文件/有哪些空间/空间列表/团队文件列表" → `wiki space list --type orgSpace`(`drive list-spaces` 已 deprecated) +用户说"搜索钉盘文件/钉盘里找个文件/查找某个钉盘文件/钉盘中搜索" → `search` +用户说"文件详情/文件信息" → `info` +用户说"下载文件" → `download` 指定 `--output` 保存到本地 +用户说"新建文件夹/创建目录" → `mkdir`(钉盘空间)/ `wiki node create --type folder`(文档空间) +用户说"上传文件/传文件到钉盘" → `upload`(首选此命令,自动完成三步流程) +用户说"复制文件/移动文件/搬到/移到" → `copy` / `move` +用户说"重命名/改名" → `rename` +用户说"删除文件/删除文件夹/移到回收站" → `delete`(危险操作,需确认) +用户说"回收站/查看回收站/回收站列表/回收站里有什么" → `recycle list` +用户说"恢复文件/还原删除的文件/从回收站恢复/还原回收站文件" → `recycle restore` +用户说"给文档授权/分享权限" → `permission add` +用户说"公开文件/互联网公开/设置公开/让互联网所有人可访问" → `publish set` +用户说"关闭公开/取消公开/取消互联网访问" → `publish unset` +用户说"查看公开状态/是否公开/发布状态" → `publish get` -关键区分: -- drive(钉盘云存储,面向文件二进制) vs doc(在线文档/知识库,面向富文本内容) -- 把图片/文件发到群里一般走 drive 上传拿链接 → chat 发送 Markdown 链接 (见 [chat.md](./chat.md) 的 `drive → chat` 工作流) +关键区分: drive(文件管理) vs doc(文档内容读写) vs wiki(空间管理) -**drive upload vs doc upload**: 用户提到"钉盘/网盘/我的文件"→ drive 三步式;提到"知识库/文档空间/workspace"→ `doc upload`;未明确目标时默认走 drive +**drive search vs wiki node search**: 用户提到"钉盘/网盘/我的文件里搜" → `drive search`;提到"知识库/文档空间/workspace 里搜" → `wiki node search`;未明确目标时 `drive search`(全局聚合搜索)。 -**钉盘文件复制/移动**: drive 本身没有 copy/move 命令,需使用 `dws doc copy` / `dws doc move` 实现(详见下方「复制/移动钉盘文件」工作流) +**drive upload vs doc upload**: 文件上传统一走 `drive upload`。上传到知识库/文档空间时加 `--workspace` 参数。 + +**drive permission vs wiki member**: "给某篇文档/文件授权" → `drive permission add`(节点级);"给某个知识库整体加成员" → `wiki member add`(空间级) + +**创建在线文档/表格/脑图**: drive 不支持创建文件,需走 `wiki node create --type `(创建空节点)或 `doc create`(创建并写入内容)。 + +**导出文档/导出为Word**: 导出是内容层操作,走 `doc export`,不属于 drive。 + +把图片/文件发到群里一般直接用 `chat message send --msg-type file --file-path <本地路径>`(见 [chat.md](./chat.md)),无需先经 drive 上传。 ## 核心工作流 ```bash -# ── 工作流 1: 浏览钉盘 ── +# 1. 浏览「我的文件」根目录 +dws drive list --limit 20 --format json -# 1. 看根目录 -dws drive list --format json +# 2. 进入子目录 — 提取 dentryUuid 作为 folder +dws drive list --limit 20 --folder --format json -# 2. 进入子目录 (parentId 取自上一步的 dentryUuid) -dws drive list --parent-id --format json +# 3. 查看文件元数据 +dws drive info --node --format json -# 3. 看单个文件的元信息 -dws drive info --file-id --format json +# 4. 下载文件到本地 +dws drive download --node --output /tmp/ --format json -# ── 工作流 2: 上传本地文件到钉盘 (三步不能省) ── +# 5. 创建文件夹 +dws drive mkdir --name "项目资料" --format json -# Step 1: 拿上传凭证 -dws drive upload-info --file-name "report.pdf" --file-size 102400 --parent-id --format json -# → 返回: resourceUrl (预签名 URL), headers, uploadId +# 6. 上传文件(首选 upload 命令,自动完成三步流程) +dws drive upload --file ./报告.pdf --format json +dws drive upload --file ./报告.pdf --folder --format json -# Step 2: Agent 自己发 HTTP PUT 把文件二进制推到 resourceUrl -# 请求头必须携带返回的 headers 全部键值对;期望 HTTP 200 -curl -X PUT -T ./report.pdf -H ": " "" +# 7. 删除文件/文件夹到回收站(危险操作:必须先向用户确认,用户同意后才加 --yes 执行) +# 正确流程:1.向用户展示"即将删除「文件名」到回收站" → 2.等用户确认 → 3.执行下面命令 +dws drive delete --node --yes --format json -# Step 3: 提交入库 -dws drive commit --file-name "report.pdf" --file-size 102400 --upload-id \ - --parent-id --format json -# → 返回: dentryUuid (= 后续 download/info 用的 fileId) - -# ── 工作流 3: 下载钉盘文件到本地 ── -dws drive download --file-id --output ./report.pdf --format json - -# ── 工作流 4: 建目录后批量上传 ── - -# 1. 建目录 → 拿 folderId -dws drive mkdir --name "2026 Q1 归档" --format json - -# 2. 再走 "工作流 2" 把每个文件上传到该目录 +# 8. 查看回收站并还原文件 +dws drive recycle list --format json +dws drive recycle restore --id --format json ``` -## 复制/移动钉盘文件 +## 文档空间管理命令 -钉盘本身没有 copy/move 命令,需使用 `dws doc copy` / `dws doc move` 实现跨场景搬运。 +> 以下命令操作的是**文档空间**(知识库 / 我的文档),底层路由到 doc 服务。 +> 与钉盘命令(list / mkdir / upload 等)的区别:钉盘命令操作钉盘空间(spaceId 纯数字),文档空间命令操作知识库/我的文档(workspaceId 加密 string)。 -> **关键陷阱:字段选择** -> `drive list` 返回中有 `dentryId`(数字格式,如 `220335325118`)和 `fileId`(UUID 格式,如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)两个字段。 -> **必须使用 `fileId`(UUID 格式)**作为 `--node` 和 `--folder` 的参数值。 -> **禁止使用 `dentryId`(数字格式)**,传入数字格式会导致命令失败。 +### 复制/移动/重命名文件 -> **能力限制**:钉盘场域下,仅支持将文件复制/移动到文件夹下,**不支持文档下嵌套文档**。 +``` +Usage: + dws drive copy --node [--folder ] [--workspace ] + dws drive move --node [--folder ] [--workspace ] + dws drive rename --node --name "新名称" +Flags: + --node string 文档/文件 ID 或 URL (必填) + --folder string 目标文件夹 nodeId + --workspace string 目标知识库 ID + --name string 新名称 (仅 rename 必填) +``` + +> **rename 只传主名,不要带扩展名**:服务端会按文件原扩展名自动补一个后缀。若 `--name` 里已带扩展名(如 `报告.txt`),回读会变成双扩展名 `报告.txt.txt`。正确做法:`dws drive rename --node --name "报告"`(不含 `.txt`),系统自动补回 `报告.txt`。 + +权限要求:copy 需对源文档有"阅读"权限且对目标文件夹有"编辑"权限;move 需对源文档有"管理"权限且对目标文件夹有"编辑"权限;rename 需对文档有"编辑"权限。 + +> **字段选择**:`drive list` 返回中有 `dentryId`(数字格式)和 `fileId`(UUID 格式),**必须使用 `fileId`(UUID 格式)**作为 `--node` 和 `--folder` 参数值。 + +### 创建文件夹(文档空间) + +drive 没有独立的文档空间建文件夹命令,在知识库/文档空间中创建文件夹走: + +```bash +dws wiki node create --type folder --name "文件夹名" --workspace --format json +``` + +详见 [wiki.md](./wiki.md)。 + +### 权限管理(文档节点级) + +> 仅适用于文档空间节点,不适用于钉盘文件。 + +``` +Usage: + dws drive permission add --node --users uid1,uid2 --role READER + dws drive permission update --node --users uid1 --role EDITOR + dws drive permission list --node + dws drive permission remove --node --users uid1 +Flags: + --node string 目标节点 ID 或 URL (必填) + --users string 用户 userId 列表,逗号分隔 (add/update/remove 必填) + --role string 角色: MANAGER / EDITOR / DOWNLOADER / READER (add/update 必填) + --workspace string 知识库 ID (选填) + --limit int 返回成员数上限 (仅 list,默认 30,最大 200) + --filter-role string 按角色过滤: OWNER / MANAGER / EDITOR / DOWNLOADER / READER (仅 list) +``` + +### 文件互联网公开发布 + +管理文件的互联网公开发布状态。公开后任何人通过链接即可访问,无需登录钉钉。操作者需要是文件的管理员或拥有者。 + +> **`publish set` 和 `publish unset` 为 [危险] 操作,执行前需要向用户确认。确认后传入 `--yes` 跳过交互式确认。** + +``` +Usage: + dws drive publish set --node [--permission READER|DOWNLOADER|EDITOR] + dws drive publish unset --node + dws drive publish get --node +Example: + dws drive publish set --node + dws drive publish set --node --permission READER + dws drive publish get --node + dws drive publish unset --node +Flags: + --node string 目标文件 ID (dentryUuid) 或 URL (必填) + --permission string 公开后的权限: READER(仅可查看) / DOWNLOADER(可查看和下载,默认) / EDITOR(可编辑),仅 set 有效 +``` + +子命令说明: +- `publish set` — [危险] 设置文件为互联网公开,可选指定公开权限 +- `publish unset` — [危险] 关闭文件互联网公开 +- `publish get` — 查询文件当前的公开发布状态 + +返回字段说明: +- `published` — true=已公开,false=未公开 +- `publishPermission` — 当前公开权限(READER/DOWNLOADER/EDITOR) +- `pendingApproval` — true=已提交审批待生效,false/null=无需审批或已直接生效 +- `docUrl` — 文件访问链接 + +> **注意**:`drive export` 不存在。导出仅对自研文档 (adoc) 有意义,属于内容层操作,应使用 `doc export`。 ### 目标位置参数规则 | 目标位置 | 参数传递方式 | 前置步骤 | |---------|-----------|---------| -| 未指定目标(默认) | `--folder ` | 先 `dws drive list-spaces --space-type mySpace` 获取「我的文件」的 `rootFolderId` | +| 未指定目标(默认) | `--folder ` | 先 `dws wiki space list --type mySpace` 获取「我的文件」的 `rootFolderId` | | 知识库空间根目录 | `--workspace ` | 无需额外步骤,直接传入 workspaceId | -| 钉盘 space 根目录 | `--folder ` | 先 `dws drive list-spaces` 获取目标 space 的 `rootFolderId` | +| 钉盘 space 根目录 | `--folder ` | 先 `dws wiki space list --type orgSpace` 获取目标 space 的 `rootFolderId` | | 钉盘 space 下的子文件夹 | `--folder ` | 先 `dws drive list --space-id ` 逐层浏览,获取目标文件夹的 `fileId`(dentryUuid 格式) | ### 工作流示例 @@ -300,48 +472,53 @@ dws drive mkdir --name "2026 Q1 归档" --format json ```bash # ── 场景默认: 用户未指定目标位置 → 复制/移动到「我的文件」根目录 ── dws drive list --space-id --format json # 获取源文件 dentryUuid -dws drive list-spaces --space-type mySpace --format json # 获取「我的文件」rootFolderId -dws doc copy --node <源文件dentryUuid> --folder <我的文件rootFolderId> --format json +dws wiki space list --type mySpace --format json # 获取「我的文件」rootFolderId +dws drive copy --node <源文件dentryUuid> --folder <我的文件rootFolderId> --format json # ── 场景 A: 复制钉盘文件到知识库空间根目录 ── -dws drive list --space-id --format json -dws doc copy --node <源文件dentryUuid> --workspace --format json +dws drive copy --node <源文件dentryUuid> --workspace --format json # ── 场景 B: 移动钉盘文件到另一个钉盘 space 根目录 ── -dws drive list --space-id --format json -dws drive list-spaces --format json -dws doc move --node <源文件dentryUuid> --folder <目标space的rootFolderId> --format json +dws wiki space list --type orgSpace --format json +dws drive move --node <源文件dentryUuid> --folder <目标space的rootFolderId> --format json # 注意:移动到其他 space 时只传 --folder,不传 --workspace # ── 场景 C: 复制钉盘文件到钉盘 space 下的子文件夹 ── -dws drive list --space-id --format json dws drive list --space-id --format json -dws drive list --space-id --parent-id <父文件夹dentryUuid> --format json # 目标深层级时逐层浏览 -dws doc copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --format json +dws drive copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --format json ``` ## 上下文传递表 | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `list` | `dentryUuid` (folder 类) | 下次 list 的 --parent-id、upload-info / commit 的 --parent-id、mkdir 的 --parent-id | -| `list` | `dentryUuid` (file 类) | info / download 的 --file-id | -| `list` | `nextToken` | 下次 list 的 --next-token | -| `mkdir` | `dentryUuid` | 作为父目录传给 upload-info / commit 的 --parent-id | -| `upload-info` | `resourceUrl` + `headers` | Agent 自行执行 HTTP PUT 上传二进制 | -| `upload-info` | `uploadId` | commit 的 --upload-id | -| `commit` | `dentryUuid` | download / info / chat message 发送图片链接的 --file-id | -| `download` | `--output` 指定的路径 | 本地文件落地位置 | +| `list` | **`fileId`**(UUID 格式,注意:不是 `dentryId`) | info / download / delete 的 --node;list / mkdir 的 --folder;`drive copy/move` 的 --node 或 --folder | +| `list` | `spaceId` | info / download / mkdir / upload 的 --space-id | +| `list` | `nextCursor` | 下次 list 的 --cursor | +| `list-spaces` / `wiki space list` | `rootFolderId` | `drive copy/move` 的 --folder(复制/移动到钉盘 space 根目录时) | +| `list-spaces` / `wiki space list` | `spaceId` | list / info / download / mkdir / upload 的 --space-id | +| `search` | **`fileId`**(文件/文件夹结果) | info / download / delete 的 --node;list 的 --folder | +| `search` | `spaceId` / `rootFolderId`(空间结果) | list 的 --space-id;`drive copy/move` 的 --folder | +| `search` | `nextCursor` | search 的 --cursor(翻页) | +| `mkdir` | `fileId`(UUID 格式) | list / upload 的 --folder | +| `upload` | `dentryUuid` | download / info 的 --node | +| `recycle list` | `id`(回收项 ID) | recycle restore 的 --id | +| `recycle list` | `name`(原始文件名) | 供用户确认还原目标 | +| `recent` | `recentItems[].nodeId` / `docUrl` | doc read / info / update / block 操作的 --node | +| `recent` | `nextCursor` | recent 的 --cursor(翻页) | + +> **重要**:`drive list` 返回结果中同时包含 `dentryId` 和 `fileId` 两个字段。所有需要传 `--node` 的命令(info / download / delete)必须使用 `fileId`(即 dentryUuid),**不要使用** `dentryId`。 ## 注意事项 -- 上传两条路径:**首选** `upload` 一条命令搞定(内部走三步);如果需要自己控制 HTTP PUT,可走 `upload-info` → 客户端 HTTP PUT → `commit` 三步 -- 用三步流程时,Step 2 的 HTTP PUT 必须把 upload-info 返回的 `headers` 全部回传,`Content-Type` 通常要留空;只有 PUT 返回 200 才能调 `commit` -- 上传凭证 (`uploadId`) 有过期时间,拿到后尽快 commit;过期需重新调 `upload-info` +- 不传 `--space-id` 时默认使用「我的文件」空间 +- 不传 `--folder` 时默认操作空间根目录 +- `--folder` 只能使用父文件夹的 `dentryUuid`。不要把 `drive info` 返回的数字型 `dentryId` 当作父目录 +- **`--limit` 最大值为 50**,禁止传入超过 50 的值(如 `--limit 100`)。用户要求超过 50 条时,应使用 `--limit 50` 配合 `--cursor` 分页查询 +- `--order-by` 支持: `createTime`、`modifyTime`、`name` +- **上传文件首选 `dws drive upload` 命令**;手动三步(`upload-info` → HTTP PUT → `commit`)仅用于自定义流式上传等特殊场景。手动三步时 HTTP PUT 必须把 upload-info 返回的 `headers` 全部回传,`Content-Type` 通常要留空;PUT 返回 200 后才能调 `commit`;`uploadId` 有过期时间,过期需重新 `upload-info`;`--folder` 在 upload-info / commit 中要保持一致 +- `--file-name` 必须包含扩展名(如 `report.pdf`) - `download` 需要指定 `--output`,CLI 会把文件保存到本地路径或目录 -- `--parent-id` 在 upload-info / commit 中要保持一致,否则 commit 会报位置不匹配 -- `--conflict-handler` 默认 `AUTO_RENAME` (自动重命名);`OVERWRITE` 覆盖同名文件前必须和用户确认 -- `--space-id` 绝大多数场景不需要传;默认会用用户主钉盘空间 - 文件名规则:头尾不能有空格;不能含 `*`、`"`、`<`、`>`、`|`、制表符;不能以 `.` 结尾 ## 自动化脚本 @@ -352,5 +529,6 @@ dws doc copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --for ## 相关产品 -- [doc](./doc.md) — 钉钉在线文档 / 知识库(文字、表格、脑图等富文本节点),和 drive 的裸文件存储不同 -- [chat](./chat.md) — 结合 drive 发送图片 / 附件消息到群聊(Markdown 链接语法) +- [doc](./doc.md) — 文档内容读写(Markdown/块级编辑/导出),不是文件存储 +- [wiki](./wiki.md) — 知识库/空间管理层(空间列表、节点创建、空间内搜索、成员管理) +- [chat](./chat.md) — 发送图片/文件消息用 `chat message send --msg-type file --file-path` diff --git a/skills/multi/dingtalk-drive/scripts/drive_tree_list.py b/skills/multi/dingtalk-drive/scripts/drive_tree_list.py index a7e3439b..95ccb1c2 100644 --- a/skills/multi/dingtalk-drive/scripts/drive_tree_list.py +++ b/skills/multi/dingtalk-drive/scripts/drive_tree_list.py @@ -3,10 +3,16 @@ 递归列出钉盘目录树结构(可指定深度) 用法: - python drive_tree_list.py # 列出根目录 - python drive_tree_list.py --depth 2 # 递归 2 层 - python drive_tree_list.py --parent-id # 指定目录 + python drive_tree_list.py # 列出根目录 + python drive_tree_list.py --depth 2 # 递归 2 层 + python drive_tree_list.py --folder # 指定目录 (传 drive list 返回的 fileId) python drive_tree_list.py --dry-run + +说明: + `dws drive list` 返回的每个 item 有两个 ID: + - dentryId:纯数字串,`--folder` 不接受,别用它递归; + - fileId:字母数字串,即 CLI 所称 dentryUuid,`--folder` 只认它。 + 递归子目录必须用 fileId 作为 `--folder` 的值。 """ import sys @@ -38,13 +44,13 @@ def run_dws( def list_dir( - parent_id: str = '', dry_run: bool = False, + folder: str = '', dry_run: bool = False, ) -> list: cmd_args = [ - 'drive', 'list', '--max', '50', '--format', 'json', + 'drive', 'list', '--limit', '50', '--format', 'json', ] - if parent_id: - cmd_args.extend(['--parent-id', parent_id]) + if folder: + cmd_args.extend(['--folder', folder]) data = run_dws(cmd_args, dry_run=dry_run) if not data: return [] @@ -69,7 +75,7 @@ def print_tree( name = item.get('name') or item.get('fileName', '?') item_type = item.get('type') or item.get('dentryType', '') is_dir = str(item_type).lower() in ( - 'folder', 'directory', '1', 'FOLDER' + 'folder', 'directory', '1', ) icon = '📁' if is_dir else '📄' size_str = '' @@ -87,10 +93,10 @@ def print_tree( if is_dir and depth < max_depth: child_prefix = prefix + (' ' if is_last else '│ ') - dentry_id = (item.get('dentryUuid') - or item.get('id', '')) - if dentry_id: - children = list_dir(dentry_id, dry_run=dry_run) + # `--folder` 只认 fileId (dentryUuid),不认纯数字 dentryId + folder_id = item.get('fileId', '') + if folder_id: + children = list_dir(folder_id, dry_run=dry_run) print_tree( children, depth + 1, max_depth, child_prefix, dry_run, @@ -102,7 +108,8 @@ def main(): description='递归列出钉盘目录树' ) parser.add_argument( - '--parent-id', default='', help='起始目录 ID' + '--folder', default='', + help='起始目录 ID (传 drive list 返回的 fileId)', ) parser.add_argument( '--depth', type=int, default=1, @@ -112,10 +119,10 @@ def main(): args = parser.parse_args() args.depth = min(args.depth, 5) - root_name = args.parent_id or '我的文件' + root_name = args.folder or '我的文件' print(f"📁 {root_name}") - items = list_dir(args.parent_id, dry_run=args.dry_run) + items = list_dir(args.folder, dry_run=args.dry_run) if args.dry_run: return if not items: diff --git a/skills/multi/dingtalk-mail/SKILL.md b/skills/multi/dingtalk-mail/SKILL.md index 47c39b2b..2478996b 100644 --- a/skills/multi/dingtalk-mail/SKILL.md +++ b/skills/multi/dingtalk-mail/SKILL.md @@ -27,7 +27,7 @@ metadata: | 用户说 | 命令 | |--------|------| -| "发邮件给 a@b.com" | `dws mail message send --from <自己邮箱> --to a@b.com --subject "<标题>" --body "<正文>"` | +| "发邮件给 a@b.com" | `dws mail message send --from <自己邮箱> --to a@b.com --subject "<标题>" --content "<正文>"`(正文规范 flag 是 `--content`,`--body` 为隐藏别名) | | "今天未读邮件" | `python scripts/mail_unread_summary.py` | | "带抄送发送" | `python scripts/mail_send_with_cc.py --to a@b.com --cc c@d.com --subject "<标题>" --body "<正文>"` | diff --git a/skills/multi/dingtalk-mail/references/09-mail.md b/skills/multi/dingtalk-mail/references/09-mail.md index 952ca887..5ac7a50f 100644 --- a/skills/multi/dingtalk-mail/references/09-mail.md +++ b/skills/multi/dingtalk-mail/references/09-mail.md @@ -3,33 +3,54 @@ > **SKILL.md** 中 #9 内联 4 条 **lite**:`mail-list-mailbox`、`mail-search`、`mail-send`、`mail-reply-forward`,见 `dws-shared/references/best_practices/_common/lite-recipes.md`。下列 recipe、专用规则与消歧请在命中 #9 且**超出**上述 lite 时阅读本文。 > 产品命令见 [mail.md](./mail.md)。通用批量/并行见 `dws-shared/references/best_practices/_common/conventions.md`。 -> **能力范围**:当前 `dws mail` 只实现 `mailbox list` 与 `message search / get / send` 四条命令;其余 `reply` / `reply-all` / `forward` / `batch-*` / `draft *` / `folder *` / `attachment *` / `tag *` / `thread *` / `user search` 等命令**均未实现**,禁止编造。上面的 `mail-reply-forward` lite 触发场景("回复邮件 / 转发邮件")需要回退到引导用户在钉钉客户端手动操作。 - ## 专用规则(#9 非 lite 步骤必守) - **KQL 语法强制**:邮件搜索的查询条件**只能**通过 `--query` 参数以 KQL 语法传入(如 `subject:周报`),**禁止臆造** `--subject`、`--sender`、`--from-address` 等不存在的 flag。详见 [mail.md](./mail.md) 中 KQL 查询字段说明。 -- **邮箱地址前置**:所有邮件命令需要 `--email` 或 `--from` 参数,执行前**必须**先通过 `mail mailbox list` 获取当前用户邮箱(返回字段 `emailAccounts`),禁止猜测邮箱地址。 -- **查找他人邮箱**:需要获取某人邮箱地址时,**不要用 `mailbox list`**(只返回自己的),必须走两路并发查询流程(见 [mail.md](./mail.md) 中「查找他人邮箱地址」章节)。 -- **未实现命令禁止编造**:回复 / 转发 / 批量移动 / 批量删除 / 草稿 / 文件夹 / 附件 / 标签 / 会话线程 / mail user search 等命令当前都没实现,遇到相关诉求要么走 search/get/send 能覆盖的子集,要么向用户说明并引导到钉钉客户端。 +- **邮箱地址前置**:大部分邮件命令需要 `--email` 或 `--from` 参数,执行前**必须**先通过 `mail mailbox list` 获取当前用户邮箱(返回字段是 `emailAccounts`),禁止猜测邮箱地址。 +- **查找他人邮箱**:需要获取某人邮箱地址时,**不要用 `mailbox list`**(只返回自己的),必须走三路并发查询流程(见 [mail.md](./mail.md) 中「查找他人邮箱地址」章节)。 +- **附件下载三步走**:先 `message search` 搜索邮件获取 messageId,再 `attachment list` 获取附件 ID 和文件名,最后逐个 `attachment download` 下载。**不存在 `download_batch` / `download_all` 等批量下载命令,禁止编造**。 +- **危险操作确认**:`batch-delete` 执行前必须向用户确认,同意后加 `--yes`。 ## 与其他场景消歧 -- **"给某人发邮件"**(只知姓名不知邮箱)→ 先走「查找他人邮箱地址」两路并发,再 `mail-send`。 -- **"找某人邮箱"**(终点是获取邮箱地址)→ 两路并发查询,不走 `mail-search`。 +- **"给某人发邮件"**(只知姓名不知邮箱)→ 先走「查找他人邮箱地址」三路并发,再 `mail-send`。 +- **"找某人邮箱"**(终点是获取邮箱地址)→ 三路并发查询,不走 `mail-search`。 - **"搜某人发的邮件"**(终点是邮件内容)→ `mail-search`,KQL 用 `from:xxx`。 - **"催+邮件"** → `mail-send` 发催促邮件,不是 #1 消息。 - **"邮件+待办"** → 先 `mail-search` 找邮件内容,再走 #2 创建待办。 -- **"回复 / 转发 / 删除 / 移动 / 草稿 / 附件 / 标签 / 会话线程"** → 当前 CLI 不支持,引导用户在钉钉客户端处理,不要编造命令。 ## Recipe 速查(本表步骤,非 SKILL lite) | Recipe | 步骤 | |--------|------| -| `mail-get` | `mail message get --email <邮箱> --id ` → 查看邮件完整内容(含 `message.markdownBody` 正文) | +| `mail-get` | `mail message get --email <邮箱> --id ` → 查看邮件完整内容(含正文) | +| `mail-message-list` | `mail message list --email <邮箱>` → 列出文件夹中的邮件(默认收件箱);`--folder-id ` 指定文件夹、`--limit ` 分页 | +| `mail-folder-list` | `mail folder list --email <邮箱>` → 列举文件夹;`--folder ` 查子文件夹 | +| `mail-folder-manage` | `mail folder create/update/delete`:创建/重命名/删除文件夹(先 `folder list` 取 `folders[].id`) | +| `mail-tag-list` | `mail tag list --email <邮箱>` → 列举邮件标签 | +| `mail-tag-manage` | `mail tag create/update/delete`:创建/重命名/删除标签(先 `tag list` 取 `tags[].id`,系统标签不可改删) | +| `mail-thread-get` | `mail thread get --email <邮箱> --id ` → 获取会话(邮件线程)详情 | +| `mail-thread-list` | `mail thread list --email <邮箱> --folder --limit ` → 列出文件夹下的会话 | +| `mail-thread-manage` | `mail thread update/batch-update`(标记已读未读/加移标签)、`thread trash/batch-trash`(移入已删除,须 `--yes`) | +| `mail-attachment-list` | `mail attachment list --email <邮箱> --id ` → 列举指定邮件的附件 | +| `mail-attachment-download` | 1. `mail attachment list --email <邮箱> --id ` → 取附件 `id` 和 `name`
2. `mail attachment download --email <邮箱> --message-id --attachment-id --name <文件名>` | +| `mail-batch-move` | `mail message batch-move --email <邮箱> --ids --folder `(常用 folderId: 2=收件箱, 6=已删除) | +| `mail-batch-delete` | `mail message batch-delete --email <邮箱> --ids `(**危险操作,须先确认**;仅移入已删除文件夹,非物理永久删除) | +| `mail-batch-update` | `mail message batch-update --email <邮箱> --ids --action markRead/markUnread/addTags/removeTags`(加移标签需 `--tags`) | +| `mail-verify` | `mail message verify --email <邮箱> --internet-message-id ` → 查发送状态(`sendStatus` 在 `message.sendStatus`) | +| `mail-draft-create` | `mail draft create --from <邮箱> --subject "<标题>"` → 取草稿 ID `result.message.id`(可选 `--to`、`--content`、`--cc`) | +| `mail-draft-update` | `mail draft update --from <邮箱> --id --subject "<新标题>"`(可选 `--content`、`--to`、`--cc`) | +| `mail-draft-send` | `mail draft send --from <邮箱> --id ` | +| `mail-template` | `mail template create/list/get/update/delete`(草稿模板 `--is-draft` 不出现在 list,只能 get;非草稿模板不可 update) | +| `mail-contact` | `mail contact create/list/update/batch-delete`:管理个人联系人(非通讯录搜人,搜人用 `user search`) | +| `mail-auto-reply-get` | `mail auto-reply get --email <邮箱>` → 获取自动回复配置 | +| `mail-rule` | `mail rule list/create/update/delete/adjust`:管理个人收信规则 | ## Full / 多步组合 | Recipe | 行动指南(固定路线) | |--------|---------------------| -| send-to-person-by-name | 1. `mail mailbox list` → 从 `emailAccounts` 取发件邮箱
2. 走「查找他人邮箱地址」两路并发查询获取收件人邮箱(见 [mail.md](./mail.md))
3. `mail message send --from <发件邮箱> --to <收件邮箱> --subject "<标题>" --body "<内容>"` | -| search-then-read | 1. `mail mailbox list` → 取邮箱
2. `mail message search --email <邮箱> --query "" --size 20` → 取 `messageId`
3. 对感兴趣的一封执行 `mail message get --email <邮箱> --id ` 读取正文 | +| search-and-download-attachment | 1. `mail mailbox list` → 从 `emailAccounts` 取邮箱
2. `mail message search --email <邮箱> --query "" --limit 20` → 取 `messageId` 列表
3. 对每封邮件执行 `mail attachment list --email <邮箱> --id ` → 列出附件取 `id` 和 `name`
4. 对每个附件逐个执行 `mail attachment download --email <邮箱> --message-id --attachment-id --name <文件名>`(**仅支持逐个下载,不存在批量下载命令**) | +| search-reply-forward | 1. `mail mailbox list` → 取邮箱
2. `mail message search --email <邮箱> --query "" --limit 10` → 取 `messageId`
3. 展示搜索结果供用户选择
4. 按用户指示执行 reply / reply-all / forward(参见 lite `mail-reply-forward`) | +| batch-mail-cleanup | 1. `mail mailbox list` → 从 `emailAccounts` 取邮箱
2. `mail message search --email <邮箱> --query "" --limit 100` → 取多个 `messageId`
3. 展示列表供用户确认
4. `mail message batch-move --email <邮箱> --ids --folder 6 ` 或 `batch-delete` **都是移入「已删除」文件夹,并非物理永久删除**(对已在已删除文件夹的邮件再执行返回 success 但无效;CLI 无永久删除路径,需在客户端手动清空) | +| send-to-person-by-name | 1. `mail mailbox list` → 取发件邮箱
2. 走「查找他人邮箱地址」三路并发查询获取收件人邮箱(见 [mail.md](./mail.md))
3. `mail message send --from <发件邮箱> --to <收件邮箱> --subject "<标题>" --content "<内容>"` | diff --git a/skills/multi/dingtalk-mail/references/mail.md b/skills/multi/dingtalk-mail/references/mail.md index ca3ad80b..4a822165 100644 --- a/skills/multi/dingtalk-mail/references/mail.md +++ b/skills/multi/dingtalk-mail/references/mail.md @@ -5,6 +5,7 @@ | 命令 | 功能简述 | |------|----------| | `dws mail mailbox list` | 查询**当前用户自己**的可用邮箱列表 | +| `dws mail message list` | 列出指定文件夹中的邮件(默认收件箱) | | `dws mail message search` | 搜索邮件(KQL 语法,按主题/发件人/日期等) | | `dws mail message get` | 查看邮件完整内容(含正文) | | `dws mail message send` | 发送邮件(支持附件/内联图片) | @@ -13,15 +14,43 @@ | `dws mail message forward` | 转发邮件(支持附件/内联图片) | | `dws mail message batch-move` | 批量移动邮件到指定文件夹 | | `dws mail message batch-delete` | 批量删除邮件 | +| `dws mail message batch-update` | 批量修改邮件状态(标记已读/未读/添加标签/移除标签) | +| `dws mail message verify` | 根据 internetMessageId 查询邮件发送状态 | | `dws mail draft create` | 创建草稿(保留在草稿箱,不发送) | | `dws mail draft update` | 更新草稿内容(保留在草稿箱,不发送) | | `dws mail draft send` | 发送草稿箱中已有的草稿 | | `dws mail folder list` | 列举邮件文件夹 | +| `dws mail folder create` | 创建邮件文件夹 | +| `dws mail folder delete` | 删除邮件文件夹 | +| `dws mail folder update` | 更新邮件文件夹名称 | | `dws mail attachment list` | 列举指定邮件的所有附件 | | `dws mail attachment download` | 下载邮件附件到本地(**仅支持逐个下载,不支持批量下载**) | | `dws mail tag list` | 列举邮件标签 | +| `dws mail tag create` | 创建邮件标签 | +| `dws mail tag delete` | 删除邮件标签 | +| `dws mail tag update` | 更新邮件标签名称 | +| `dws mail thread list` | 列出指定邮箱、指定文件夹下的邮件会话 | | `dws mail thread get` | 获取会话详情 | +| `dws mail thread update` | 修改单个邮件会话的状态或标签(标记已读/未读/添加标签/移除标签) | +| `dws mail thread batch-update` | 批量修改邮件会话的状态或标签(单次最多 100 个) | +| `dws mail thread trash` | 将单个邮件会话移动到已删除文件夹(不会永久删除) | +| `dws mail thread batch-trash` | 将多个邮件会话批量移动到已删除文件夹(单次最多 100 个,不会永久删除) | | `dws mail user search` | 搜索通讯录用户(**按姓名查他人邮箱**,不是搜邮件) | +| `dws mail template create` | 创建邮件模板 | +| `dws mail template list` | 列举邮件模板 | +| `dws mail template get` | 获取邮件模板详情 | +| `dws mail template update` | 更新邮件模板 | +| `dws mail template delete` | 删除邮件模板 | +| `dws mail contact create` | 创建个人邮件联系人(添加到自己的联系人列表) | +| `dws mail contact list` | 列举个人邮件联系人(查看自己保存的联系人,**不是搜索通讯录用户**) | +| `dws mail contact update` | 更新个人邮件联系人信息 | +| `dws mail contact batch-delete` | 批量删除个人邮件联系人 | +| `dws mail auto-reply get` | 获取用户的自动回复配置 | +| `dws mail rule list` | 列出个人收信规则 | +| `dws mail rule create` | 创建个人收信规则 | +| `dws mail rule update` | 更新个人收信规则 | +| `dws mail rule delete` | 删除个人收信规则 | +| `dws mail rule adjust` | 调整收信规则排序 | > **查找他人邮箱**(如「获取严龙的邮箱」)→ **不要用 `mailbox list`**,应走三路并发查询,详见「查找他人邮箱地址」章节。 @@ -36,7 +65,7 @@ **默认选择策略:** 1. 调用 `dws mail mailbox list --format json` 获取当前用户的所有邮箱。 -2. 从返回的 `mailboxes` 中**优先选择企业邮箱**(账号类型为企业邮箱、域名非 `@dingtalk.com` 的邮箱),将其作为 `--email` / `--from` 的默认值。 +2. 从返回的 `emailAccounts` 中**优先选择企业邮箱**(账号类型为企业邮箱、域名非 `@dingtalk.com` 的邮箱),将其作为 `--email` / `--from` 的默认值。 3. 仅当用户在指令中**明确指定**「用我的个人邮箱」「用 dingtalk.com 邮箱」「用我的私人邮箱」等表述时,才选择个人邮箱(`@dingtalk.com` 域名)。 4. 若用户同时拥有多个企业邮箱(如分属多家公司),优先选择与当前会话上下文匹配的企业邮箱;若仍无法判断,向用户确认后再操作。 5. 若用户**仅拥有个人邮箱**(无企业邮箱),可直接使用个人邮箱,但需注意 `mail user search` 等仅企业邮箱可用的命令会因权限报错,需走「查找他人邮箱地址」章节的替代路径。 @@ -62,7 +91,7 @@ Example: | 字段 | 类型 | 说明 | |------|------|------| -| `mailboxes` | `List[]` | 邮箱列表,每条包含邮箱地址、账号类型、所属企业 | +| `emailAccounts` | `List[]` | 邮箱列表,每条包含 `email`(邮箱地址)、`orgName`(所属企业,个人邮箱为 null)、`type`(`ORG`=企业邮箱 / `PERSONAL`=个人邮箱) | ### 查找他人邮箱地址(通讯录查人) @@ -91,18 +120,43 @@ dws contact user search --keyword "姓名" --format json 若三路均无有效邮箱,必须 `ask_human` 请用户手动提供,**严禁臆测**。 +### 列出文件夹中的邮件 +> **注意:** `message list` 用于按文件夹列出邮件;若需根据主题/发件人/日期等条件精确搜索,请使用 `message search`。 +``` +Usage: + dws mail message list [flags] +Example: + dws mail message list --email user@company.com + dws mail message list --email user@company.com --folder-id 1 + dws mail message list --email user@company.com --folder-id 2 --limit 50 + dws mail message list --email user@company.com --cursor +Flags: + --email string 邮件所属邮箱地址 (必填) + --folder-id string 文件夹 ID(1=已发送, 2=收件箱, 3=垃圾邮件, 5=草稿, 6=已删除),默认为收件箱 + --limit string 每页返回数量(最大限制 100, 默认 20) + --cursor string 邮件的起始偏移标识, 其值取自响应中的nextCursor字段。""表示从头开始 +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `messages` | `List[]` | 邮件列表,每条包含邮件 ID 及元信息(不含正文) | +| `total` | `int32` | 符合条件的总邮件数 | +| `nextCursor` | `string` | 下一页游标,传入 `--cursor` 翻页;值为 `$` 表示已到达列表尾部 | + ### 搜索邮件 (KQL 语法) ``` Usage: dws mail message search [flags] Example: - dws mail message search --email user@company.com --query "subject:\"周报\"" --size 20 - dws mail message search --email user@company.com --query "from:alice AND date>2025-06-01T00:00:00Z" --size 10 + dws mail message search --email user@company.com --query "subject:\"周报\"" --limit 20 + dws mail message search --email user@company.com --query "from:alice AND date>2025-06-01T00:00:00Z" --limit 10 Flags: --cursor string 邮件的起始偏移标识, 其值取自响应中的nextCursor字段。""表示从头开始 --email string 搜索目标邮箱地址 (必填) --query string KQL 查询表达式 (必填), 其中 date 格式需遵循 ISO8601 规范 - --size string 每页返回数量(最大限制 100, 默认 20),别名: --limit, --page-size + --limit string 每页返回数量(最大限制 100, 默认 20) ``` KQL 查询字段: date, size, tag, folderId, isRead, hasAttachments, subject, attachname, body, from, to @@ -140,9 +194,9 @@ KQL 查询字段: date, size, tag, folderId, isRead, hasAttachments, subject, at **翻页示例:** ```bash # 第一页 -dws mail message search --email user@company.com --query "folderId:2" --size 20 --format json +dws mail message search --email user@company.com --query "folderId:2" --limit 20 --format json # 取返回中的 nextCursor,传入下一次请求(nextCursor="$" 时停止) -dws mail message search --email user@company.com --query "folderId:2" --size 20 --cursor --format json +dws mail message search --email user@company.com --query "folderId:2" --limit 20 --cursor --format json ``` ### 查看邮件完整内容 @@ -168,17 +222,17 @@ Usage: dws mail message send [flags] Example: dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "本周完成任务A和任务B" + --subject "周报" --content "本周完成任务A和任务B" dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "见附件" --attachment ./report.pdf + --subject "周报" --content "见附件" --attachment ./report.pdf dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "见附件" --attachment ./a.pdf --attachment ./b.xlsx + --subject "周报" --content "见附件" --attachment ./a.pdf --attachment ./b.xlsx dws mail message send --from user@company.com --to colleague@company.com \ - --subject "图表周报" --body "图表如下:[inline:chart.png]" --inline-attachment ./chart.png + --subject "图表周报" --content "图表如下:[inline:chart.png]" --inline-attachment ./chart.png dws mail message send --from user@company.com --to colleague@company.com \ - --subject "带图文档" --body "见附件,图表:[inline:img.png]" --attachment ./doc.pdf --inline-attachment ./img.png + --subject "带图文档" --content "见附件,图表:[inline:img.png]" --attachment ./doc.pdf --inline-attachment ./img.png Flags: - --body string 邮件正文 (必填) + --content string 邮件正文 (必填) --cc string 抄送人列表 --from string 发件人邮箱 (必填),别名: --sender --subject string 邮件标题 (必填) @@ -202,7 +256,7 @@ Flags: - 仅支持图片类型:`jpg` / `jpeg` / `png` / `gif` / `webp` / `bmp` / `svg` - CLI 自动生成 contentId,格式:`inline-{文件名(不含扩展名)}-{序号}@alimail.com`,例:`inline-chart-1@alimail.com` -- 在 `--body` 中使用占位符 `[inline:文件名]` 引用图片,CLI 自动替换为 `` 标签 +- 在 `--content` 中使用占位符 `[inline:文件名]` 引用图片,CLI 自动替换为 `` 标签 - 若 body 中没有对应占位符,内联图片会自动追加到正文末尾 - 非图片类型(PDF、视频、音频等)请改用 `--attachment` @@ -212,13 +266,13 @@ Usage: dws mail folder list [flags] Example: dws mail folder list --email user@company.com - dws mail folder list --email user@company.com --folder-id + dws mail folder list --email user@company.com --folder Flags: - --email string 邮件所属邮箱地址 (必填) - --folder-id string 父文件夹唯一标识,不传则返回顶层文件夹 (可选) + --email string 邮件所属邮箱地址 (必填) + --folder string 父文件夹唯一标识,不传则返回顶层文件夹 (可选) ``` -不传 `--folder-id` 返回顶层文件夹列表;传入则返回该文件夹的子文件夹列表。 +不传 `--folder` 返回顶层文件夹列表;传入则返回该文件夹的子文件夹列表。 **返回字段(`folders` 数组):** @@ -231,6 +285,98 @@ Flags: | `totalItemCount` | `int` | 邮件总数 | | `unreadItemCount` | `int` | 未读邮件数量 | +### 创建邮件文件夹 +``` +Usage: + dws mail folder create [flags] +Example: + dws mail folder create --email user@company.com --name "项目资料" + dws mail folder create --email user@company.com --name "子文件夹" --folder +Flags: + --email string 邮件所属邮箱地址 (必填) + --name string 新建邮件文件夹名称 (必填) + --folder string 父文件夹 ID,不传则创建顶层文件夹 (可选) +``` + +不传 `--folder` 创建顶层文件夹;传入 `--folder` 时创建指定父文件夹下的子文件夹。 + +> **重要:** `--folder` 必须填写父文件夹 ID,不是文件夹名称。父文件夹 ID 来自 `dws mail folder list --email <邮箱>` 返回的 `folders[].id`。 + +**返回 JSON:** + +```json +{ + "success": true, + "result": { + "folder": { + "id": "104", + "displayName": "项目资料", + "parentFolderId": "0", + "childFolderCount": 0, + "totalItemCount": 0, + "unreadItemCount": 0, + "extensions": {} + } + } +} +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `success` | `boolean` | 是否创建成功 | +| `result.folder.id` | `string` | 文件夹唯一标识 | +| `result.folder.displayName` | `string` | 文件夹显示名称 | +| `result.folder.parentFolderId` | `string` | 父文件夹 ID | +| `result.folder.childFolderCount` | `number` | 子文件夹数量 | +| `result.folder.totalItemCount` | `number` | 邮件总数 | +| `result.folder.unreadItemCount` | `number` | 未读邮件数量 | +| `result.folder.extensions` | `object` | 文件夹扩展信息 | + +### 删除邮件文件夹 +``` +Usage: + dws mail folder delete [flags] +Example: + dws mail folder delete --email user@company.com --id +Flags: + --email string 邮件所属邮箱地址 (必填) + --id string 要删除的邮件文件夹 ID (必填) +``` + +`--id` 必须填写要删除的文件夹 ID,不是文件夹名称。文件夹 ID 来自 `dws mail folder list --email <邮箱>` 返回的 `folders[].id`,或来自 `folder create` 返回的 `result.folder.id`。 + +**返回 JSON:** + +```json +{ + "success": true +} +``` + +### 更新邮件文件夹 +``` +Usage: + dws mail folder update [flags] +Example: + dws mail folder update --email user@company.com --id --name "新文件夹名" +Flags: + --email string 邮件所属邮箱地址 (必填) + --id string 要更新的邮件文件夹 ID (必填) + --name string 更新后的邮件文件夹名称 (必填) +``` + +`--id` 必须填写要更新的文件夹 ID,不是文件夹名称;`--name` 是更新后的文件夹名称。若用户只给出原文件夹名称,必须先调用 `folder list` 找到对应 `folders[].id`,再执行 update。 + +**返回 JSON:** + +```json +{ + "success": true +} +``` + ### 列举邮件附件 > **重要:** 不存在 `attachment download_batch` / `download_all` 等批量下载命令。如需下载多封邮件的所有附件,必须按以下流程逐个下载:1) `message search` 搜索邮件获取 messageId 列表 → 2) 对每封邮件 `attachment list` 获取 attachmentId + name → 3) 对每个附件逐个调用 `attachment download`。 @@ -253,7 +399,7 @@ Flags: |------|------|------| | `id` | `string` | 附件唯一标识 | | `name` | `string` | 附件文件名 | -| `contentType` | `string` | 附件 MIME 类型 | +| `isInline` | `bool` | 是否为内联附件(`true`=正文内联图片,`false`=普通附件) | | `size` | `int` | 附件大小(字节) | ### 下载邮件附件 @@ -307,6 +453,152 @@ Flags: | `totalItemCount` | `int` | 标签下邮件总数 | | `unreadItemCount` | `int` | 标签下未读邮件数量 | +### 创建邮件标签 +``` +Usage: + dws mail tag create [flags] +Example: + dws mail tag create --email user@company.com --name "项目资料" + dws mail tag create --email user@company.com --name "子标签" --parent-id +Flags: + --email string 用户的邮箱地址 (必填) + --name string 新建邮件标签名称 (必填) + --parent-id string 父标签 ID,不传则创建顶层标签 (可选) +``` + +不传 `--parent-id` 创建顶层标签;传入 `--parent-id` 时创建指定父标签下的子标签。 + +> **重要:** `--parent-id` 必须填写父标签 ID,不是标签名称。父标签 ID 来自 `dws mail tag list --email <邮箱>` 返回的 `tags[].id`。 + +**返回 JSON:** + +```json +{ + "success": true, + "result": { + "tag": { + "id": "tag-001", + "name": "项目资料", + "parentId": "0", + "totalItemCount": 0, + "unreadItemCount": 0, + "extensions": {} + } + } +} +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `success` | `boolean` | 是否创建成功 | +| `result.tag.id` | `string` | 标签唯一标识 | +| `result.tag.name` | `string` | 标签显示名称 | +| `result.tag.parentId` | `string` | 父标签 ID | +| `result.tag.totalItemCount` | `number` | 标签下邮件总数 | +| `result.tag.unreadItemCount` | `number` | 标签下未读邮件数量 | +| `result.tag.extensions` | `object` | 标签扩展信息 | + +### 删除邮件标签 +``` +Usage: + dws mail tag delete [flags] +Example: + dws mail tag delete --email user@company.com --id +Flags: + --email string 用户的邮箱地址 (必填) + --id string 要删除的邮件标签 ID (必填) +``` + +`--id` 必须填写要删除的标签 ID,不是标签名称。标签 ID 来自 `dws mail tag list --email <邮箱>` 返回的 `tags[].id`,或来自 `tag create` 返回的 `result.tag.id`。 + +只能删除用户自定义标签,系统标签不能删除。 + +**返回 JSON:** + +```json +{ + "success": true +} +``` + +### 更新邮件标签 +``` +Usage: + dws mail tag update [flags] +Example: + dws mail tag update --email user@company.com --id --name "新标签名" +Flags: + --email string 用户的邮箱地址 (必填) + --id string 要更新的邮件标签 ID (必填) + --name string 更新后的邮件标签名称 (必填) +``` + +`--id` 必须填写要更新的标签 ID,不是标签名称;`--name` 是更新后的标签名称。若用户只给出原标签名称,必须先调用 `tag list` 找到对应 `tags[].id`,再执行 update。 + +只能更新用户自定义标签,系统标签不能更新。 + +**返回 JSON:** + +```json +{ + "success": true +} +``` + +### 列出邮件会话 + +``` +Usage: + dws mail thread list [flags] +Example: + dws mail thread list --email user@company.com --folder --limit 10 + dws mail thread list --email user@company.com --folder 104 --limit 20 --cursor +Flags: + --email string 会话所属邮箱地址 (必填) + --folder string 邮件文件夹 ID,不是文件夹名称 (必填) + --limit int 本次列出的会话数,最大 100 (必填) + --cursor string 分页游标,首次请求可不传 (可选) + --start string 开始 UTC 时间字符串,如 2024-01-01T00:00:00Z (可选) + --end string 结束 UTC 时间字符串,如 2024-12-31T23:59:59Z (可选) + --ascending 是否按时间升序;不传由服务端默认排序 (可选) +``` + +`--folder` 必须填写文件夹 ID,不是文件夹名称。若用户只给出“收件箱/已删除/某个自定义文件夹”这类名称,必须先调用 `folder list` 找到对应 `folders[].id`,再执行 `thread list`。 + +**返回 JSON:** + +```json +{ + "success": true, + "result": { + "conversations": [ + { + "id": "conversationId", + "subject": "会话主题", + "summary": "会话摘要", + "lastModifiedDateTime": "2024-02-06T01:05:07Z", + "messageCount": 1, + "tags": [], + "senders": [ + { + "email": "sender@example.com", + "name": "发件人" + } + ], + "isRead": true, + "priority": "PRY_NORMAL", + "flag": "FLAG_NONE", + "hasAttachments": false + } + ], + "nextCursor": "", + "hasMore": false + } +} +``` + ### 获取会话详情 ``` Usage: @@ -331,21 +623,135 @@ Flags: | `senders` | `List[{email, name}]` | 会话发件人列表 | | `isRead` | `boolean` | 会话是否已读(全部已读/未读) | | `priority` | `string` | 会话重要性,取会话内邮件最高优先级(`PRY_HIGH` / `PRY_NORMAL`) | -| `flag` | `string` | 会话标识,取会话内最近邮件的标识(`FLAG_NONE` / `FLAG_REPLY` / `FLAG_FORWARD`) | | `hasAttachments` | `boolean` | 会话是否包含附件(不含 inline 资源) | +| `messages` | `List[]` | 会话内的邮件列表;默认即返回,但各邮件字段(正文、收件人等)多为 null,需在 `--select` 中额外指定才有值 | + +> **注意:** `thread get` 返回的 `conversation` **不含 `flag` 字段**(`flag` 仅出现在 `thread list` 的会话项中)。会话标识请从 `thread list` 获取。 + +### 修改邮件会话状态 + +``` +Usage: + dws mail thread update [flags] +Example: + dws mail thread update --email user@company.com --id --action markRead + dws mail thread update --email user@company.com --id --action addTags --tag-ids 1,2 +Flags: + --email string 会话所属邮箱地址 (必填) + --id string 会话唯一标识 conversationId (必填) + --action string 操作类型:markRead、markUnread、addTags、removeTags (必填) + --tag-ids string 标签 ID 列表,多个用英文逗号分隔;addTags/removeTags 时必填 (可选) +``` + +`--id` 必须填写会话 ID,不是邮件 ID。会话 ID 可通过 `thread list` 的 `conversations[].id` 获取。 + +支持的 `--action`: + +| action | 说明 | 是否需要 `--tag-ids` | +|--------|------|----------------------| +| `markRead` | 标记会话为已读 | 否 | +| `markUnread` | 标记会话为未读 | 否 | +| `addTags` | 给会话增加标签 | 是 | +| `removeTags` | 从会话移除标签 | 是 | + +成功时返回: + +```json +{ + "success": true +} +``` + +### 批量修改邮件会话状态 + +``` +Usage: + dws mail thread batch-update [flags] +Example: + dws mail thread batch-update --email user@company.com --ids , --action markUnread + dws mail thread batch-update --email user@company.com --ids , --action removeTags --tag-ids 11 +Flags: + --email string 会话所属邮箱地址 (必填) + --ids string 会话 ID 列表,多个用英文逗号分隔,最多 100 个 (必填) + --action string 操作类型:markRead、markUnread、addTags、removeTags (必填) + --tag-ids string 标签 ID 列表,多个用英文逗号分隔;addTags/removeTags 时必填 (可选) +``` + +`--ids` 必须填写会话 ID 列表,不是邮件 ID 列表,最多 100 个。 + +成功时返回: + +```json +{ + "success": true +} +``` + +### [危险] 删除邮件会话 + +``` +Usage: + dws mail thread trash [flags] +Example: + dws mail thread trash --email user@company.com --id --yes +Flags: + --email string 会话所属邮箱地址 (必填) + --id string 要删除的会话 ID (必填) + --yes 确认执行此危险操作 (必填) +``` + +> ⚠️ **危险操作**:此命令会将邮件会话移动到已删除文件夹。建议先通过 `thread get` 确认目标会话后再执行。 + +将指定邮件会话移动到已删除文件夹,不会永久删除邮件。`--id` 必须填写会话 ID,不是邮件 ID。执行前需向用户确认,确认后传入 `--yes` 执行。 + +成功时返回: + +```json +{ + "success": true, + "result": {} +} +``` + +### [危险] 批量删除邮件会话 + +``` +Usage: + dws mail thread batch-trash [flags] +Example: + dws mail thread batch-trash --email user@company.com --ids , --yes +Flags: + --email string 会话所属邮箱地址 (必填) + --ids string 要删除的会话 ID 列表,多个用英文逗号分隔,最多 100 个 (必填) + --yes 确认执行此危险操作 (必填) +``` + +> ⚠️ **危险操作**:此命令会批量将邮件会话移动到已删除文件夹。建议先通过 `thread list` 确认目标会话后再执行。 + +将指定邮件会话批量移动到已删除文件夹,单次最多 100 个会话。不会永久删除邮件。`--ids` 必须填写会话 ID 列表,不是邮件 ID 列表。执行前需向用户确认,确认后传入 `--yes` 执行。 + +成功时返回: + +```json +{ + "success": true, + "result": {} +} +``` + ### 回复邮件 ``` Usage: dws mail message reply [flags] Example: dws mail message reply --from user@company.com --id - dws mail message reply --from user@company.com --id --subject "Re: 周报" --body "已收到,谢谢!" + dws mail message reply --from user@company.com --id --subject "Re: 周报" --content "已收到,谢谢!" Flags: --from string 发件人邮箱 (必填),别名: --sender --to string 收件人列表(可选) --id string 要回复的邮件 ID (必填) --subject string 回复邮件标题(可选) - --body string 回复正文(可选) + --content string 回复正文(可选) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` @@ -371,13 +777,13 @@ Usage: dws mail message reply-all [flags] Example: dws mail message reply-all --from user@company.com --id - dws mail message reply-all --from user@company.com --id --subject "Re: 周报" --body "感谢大家的参与!" + dws mail message reply-all --from user@company.com --id --subject "Re: 周报" --content "感谢大家的参与!" Flags: --from string 发件人邮箱 (必填),别名: --sender --to string 收件人列表(可选,包含发件人及所有原始收件人) --id string 要回复的邮件 ID (必填) --subject string 回复邮件标题(可选) - --body string 回复正文(可选) + --content string 回复正文(可选) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` @@ -409,7 +815,7 @@ Flags: --to string 转发收件人列表(可选) --id string 要转发的邮件 ID (必填) --subject string 转发邮件标题(可选) - --body string 转发附言(可选) + --content string 转发附言(可选) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` @@ -455,63 +861,135 @@ Flags: --ids string 要删除的邮件 ID 列表,逗号分隔 (必填) ``` +### 批量修改邮件状态 + +批量修改邮件的已读状态或标签,通过 `--action` 指定操作类型。 + +**支持的操作类型(--action):** + +| action | 说明 | 需要的额外参数 | +|--------|------|---------------| +| `markRead` | 标记邮件为已读 | 无 | +| `markUnread` | 标记邮件为未读 | 无 | +| `addTags` | 给邮件增加标签 | `--tags`(标签 ID 列表,必填) | +| `removeTags` | 从邮件移除标签 | `--tags`(标签 ID 列表,必填) | + +**常用标签 ID:** + +| 标签 ID | 名称 | 图标 | +|---------|------|------| +| `1` | 跟进事项 | 小红旗 | +| `2` | 完成事项 | 绿色小勾 | +| `11` | 重要 | 星标 | + +``` +Usage: + dws mail message batch-update [flags] +Example: + dws mail message batch-update --email user@company.com --ids , --action markRead + dws mail message batch-update --email user@company.com --ids , --action addTags --tags 1,2 + dws mail message batch-update --email user@company.com --ids , --action removeTags --tags 11 +Flags: + --email string 邮件所属邮箱地址 (必填) + --ids string 要修改的邮件 ID 列表,逗号分隔 (必填) + --action string 操作类型: markRead/markUnread/addTags/removeTags (必填) + --tags string 标签 ID 列表,逗号分隔 (action 为 addTags/removeTags 时必填) +``` + +> **查询标签 ID:** 使用 `dws mail tag list --email <邮箱>` 可查看所有可用标签及其 ID。 + +### 查询邮件发送状态 +``` +Usage: + dws mail message verify [flags] +Example: + dws mail message verify --email user@company.com --internet-message-id +Flags: + --email string 邮件所属邮箱地址 (必填) + --internet-message-id string 邮件的 internetMessageId (必填),取自发送类命令返回值 +``` + +根据 `internetMessageId` 查询某封邮件当前的发送投递状态。 + +> **internetMessageId 来源:** `message send` / `draft send` / `message reply` / `message reply-all` / `message forward` 等发送类命令的返回值中均会带 `internetMessageId` 字段,可直接传入此命令查询发送结果。 + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `message` | `object` | 邮件完整信息 | +| `message.sendStatus` | `string` | 发送状态,**嵌在 `message` 对象内部**(非顶层字段),取值见下表 | + +> **注意:** 顶层只有 `message` 和 `success` 两个字段,`sendStatus` 位于 `message.sendStatus`,不是与 `message` 平级的顶层字段。 + +**`sendStatus` 取值说明:** + +| 值 | 含义 | +|------|------| +| `none` | 未发送 | +| `posting` | 投递中 | +| `partial_success` | 部分成功(部分收件人投递成功) | +| `success` | 发送成功 | +| `failed` | 发送失败 | +| `unknown` | 未知状态 | + ### 创建草稿 ``` Usage: dws mail draft create [flags] Example: dws mail draft create --from user@company.com --to colleague@company.com \ - --subject "草稿标题" --body "草稿正文" + --subject "草稿标题" --content "草稿正文" dws mail draft create --from user@company.com --subject "草稿标题" dws mail draft create --from user@company.com --subject "带附件草稿" \ - --body "见附件" --attachment ./report.pdf + --content "见附件" --attachment ./report.pdf dws mail draft create --from user@company.com --subject "带图片草稿" \ - --body "图表:[inline:chart.png]" --inline-attachment ./chart.png + --content "图表:[inline:chart.png]" --inline-attachment ./chart.png Flags: --from string 发件人邮箱 (必填),别名: --sender --subject string 邮件标题 (必填) --to string 收件人列表(可选,有确定收件人时才传) --cc string 抄送人列表(可选,有确定抄送人时才传) - --body string 邮件正文(可选,有正文内容时才传) + --content string 邮件正文(可选,有正文内容时才传) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` -> **注意:** `--to`、`--cc`、`--body` 均为可选参数,**仅在用户明确提供对应信息时才传入**。若用户未指定收件人,不要传 `--to ""`(空字符串)。 +> **注意:** `--to`、`--cc`、`--content` 均为可选参数,**仅在用户明确提供对应信息时才传入**。若用户未指定收件人,不要传 `--to ""`(空字符串)。 **附件说明:** -指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿创建和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--body` 中使用 `[inline:文件名]` 占位符)。 +指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿创建和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--content` 中使用 `[inline:文件名]` 占位符)。 **返回字段:** | 字段 | 类型 | 说明 | |------|------|------| -| `messageId` | `string` | 新建草稿的邮件 ID | +| `result.message.id` | `string` | 新建草稿的邮件 ID(**嵌在 `result.message` 内**,无顶层 `messageId` 字段);`result.message` 还含 `internetMessageId`、`folderId`(草稿箱为 `5`)、`subject`、`from` 等 | ### 更新草稿 ``` Usage: dws mail draft update [flags] Example: - dws mail draft update --from user@company.com --id --subject "新标题" --body "新正文" - dws mail draft update --from user@company.com --id --body "见附件" --attachment ./report.pdf + dws mail draft update --from user@company.com --id --subject "新标题" --content "新正文" + dws mail draft update --from user@company.com --id --content "见附件" --attachment ./report.pdf dws mail draft update --from user@company.com --id \ - --body "图表:[inline:chart.png]" --inline-attachment ./chart.png + --content "图表:[inline:chart.png]" --inline-attachment ./chart.png Flags: --from string 发件人邮箱 (必填),别名: --sender --id string 草稿邮件 ID (必填) --to string 收件人列表(可选) --cc string 抄送人列表(可选) --subject string 邮件标题(可选) - --body string 邮件正文(可选) + --content string 邮件正文(可选) --attachment stringArray 附件文件路径,可多次指定 (可选) --inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选) ``` **附件说明:** -指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿更新和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--body` 中使用 `[inline:文件名]` 占位符)。 +指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿更新和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--content` 中使用 `[inline:文件名]` 占位符)。 ### 发送草稿 ``` @@ -533,13 +1011,14 @@ Usage: Example: dws mail user search --keyword "张三" dws mail user search --email user@company.com --keyword "张三" - dws mail user search --email user@company.com --keyword "alice" --size 10 + dws mail user search --email user@company.com --keyword "alice" --limit 10 dws mail user search --email user@company.com --keyword "alice" --cursor Flags: - --email string 搜索目标邮箱地址 (可选) - --keyword string 搜索关键词 (必填) - --cursor string 分页游标,取自响应中的 nextCursor 字段(可选) - --size string 每页返回数量(可选) + --email string 搜索目标邮箱地址 (可选) + --keyword string 搜索关键词(未提供 --employee-no 时为必填) + --employee-no string 按工号搜索用户;提供此参数时 keyword 不再必填 (可选) + --cursor string 分页游标,取自响应中的 nextCursor 字段(可选) + --limit string 每页返回数量(可选) ``` > **重要区别:** @@ -570,6 +1049,413 @@ Flags: | `jobTitle` | `string` | 职位 | | `workLocation` | `string` | 工作地 | +### 创建邮件模板 +``` +Usage: + dws mail template create [flags] +Example: + dws mail template create --email user@company.com --name "周报模板" --subject "周报" --content "本周工作总结..." + dws mail template create --email user@company.com --from user@company.com --name "通知模板" --subject "通知" --content "..." --to a@x.com,b@x.com --cc c@x.com +Flags: + --email string 用户邮箱地址 (必填) + --from string 模板发件人邮箱 (可选) + --subject string 模板邮件标题 (必填) + --content string 模板邮件正文 (必填) + --name string 模板名称 (必填) + --to string 模板收件人列表,逗号分隔 (可选) + --cc string 模板抄送人列表,逗号分隔 (可选) + --is-draft 是否为草稿模板 (可选,默认 false) +``` + +> **草稿模板说明:** 传入 `--is-draft` 创建的模板为草稿模板,草稿模板支持后续通过 `template update` 修改内容。**非草稿模板创建后不可修改**(`template update` 仅对草稿模板有效)。 +> +> **草稿模板不出现在 `template list`(重要):** 实测 `template list` **只返回非草稿模板**;草稿模板(`--is-draft`)不在列表里,只能用 `template get --id <模板ID>` 直接获取。因此**不要用 `template list` 是否出现来判断草稿模板是否创建成功**——`template create` 返回 `success:true` 即已创建。 +> +> **模板 ID 前缀区分类型:** `template create` 返回的 `id` 前缀标识类型——非草稿模板为 `1:0:` 开头(如 `1:0:8bbeea56-...`),草稿模板为 `0:0:` 开头(如 `0:0:e3e2d134-...`)。 + +### 列举邮件模板 +``` +Usage: + dws mail template list [flags] +Example: + dws mail template list --email user@company.com --limit 20 + dws mail template list --email user@company.com --limit 20 --cursor +Flags: + --email string 用户邮箱地址 (必填) + --cursor string 分页游标,取自响应中的 nextCursor 字段 (可选) + --limit string 每页返回数量 (必填) +``` + +### 获取邮件模板详情 +``` +Usage: + dws mail template get [flags] +Example: + dws mail template get --email user@company.com --id +Flags: + --email string 用户邮箱地址 (必填) + --id string 模板唯一标识 (必填) +``` + +### 更新邮件模板 + +> ⚠️ 服务端限制:仅草稿模板(创建时带 `--is-draft`)可更新;非草稿模板更新会返回 1001 Invalid parameter。需要修改非草稿模板时,建议删除后用 `--is-draft` 重建。 + +> **重要限制:** `template update` **仅对草稿模板有效**。只有通过 `template create --is-draft` 创建的草稿模板才支持更新,非草稿模板调用 update 会返回 `Invalid parameter` 错误。 + +``` +Usage: + dws mail template update [flags] +Example: + dws mail template update --email user@company.com --id --subject "新标题" --content "新正文" + dws mail template update --email user@company.com --id --name "新模板名" +Flags: + --email string 用户邮箱地址 (必填) + --id string 模板唯一标识 (必填,必须是草稿模板的 ID) + --from string 模板发件人邮箱 (可选) + --subject string 模板邮件标题 (可选) + --content string 模板邮件正文 (可选) + --name string 模板名称 (可选) + --to string 模板收件人列表,逗号分隔 (可选) + --cc string 模板抄送人列表,逗号分隔 (可选) +``` + +### 删除邮件模板 +``` +Usage: + dws mail template delete [flags] +Example: + dws mail template delete --email user@company.com --id +Flags: + --email string 用户邮箱地址 (必填) + --id string 模板唯一标识 (必填) +``` + +> **特殊字符注意:** `--contact-id` 等 ID 参数的值可能包含 `$`、`!` 等 shell 特殊字符。在终端手动执行时,**必须用单引号**包裹这类参数值(如 `--contact-id '101_0:DzzzzyJqO10$---.hp5uBuR'`),双引号会导致 `$` 被 shell 变量展开,使 ID 值被篡改从而报错。通过 MCP 协议(JSON 传参)调用时无此问题。 + +### 创建邮件联系人 +``` +Usage: + dws mail contact create [flags] +Example: + dws mail contact create --email user@company.com --contact-email colleague@company.com --display-name "张三" + dws mail contact create --email user@company.com --contact-email colleague@company.com --first-name "三" --last-name "张" +Flags: + --email string 用户邮箱地址 (必填) + --contact-email string 联系人邮箱地址 (必填) + --first-name string 联系人名 (可选) + --middle-name string 联系人中间名 (可选) + --last-name string 联系人姓 (可选) + --display-name string 联系人显示名称 (可选) +``` + +### 列举邮件联系人 +``` +Usage: + dws mail contact list [flags] +Example: + dws mail contact list --email user@company.com --limit 20 + dws mail contact list --email user@company.com --limit 20 --cursor +Flags: + --email string 用户邮箱地址 (必填) + --cursor string 分页游标,取自响应中的 nextCursor 字段 (可选) + --limit string 每页返回数量 (必填) +``` + +> **重要区别:** `contact list` 列举的是**个人联系人**(用户自己保存/创建的联系人列表),不需要关键词;搜索**企业通讯录用户**(按姓名找人查邮箱)请用 `user search`。 + +### 更新邮件联系人 +``` +Usage: + dws mail contact update [flags] +Example: + dws mail contact update --email user@company.com --contact-id --display-name "李四" + dws mail contact update --email user@company.com --contact-id --contact-email new@company.com --first-name "四" --last-name "李" +Flags: + --email string 用户邮箱地址 (必填) + --contact-id string 联系人唯一标识 (必填) + --contact-email string 联系人邮箱地址 (可选) + --first-name string 联系人名 (可选) + --middle-name string 联系人中间名 (可选) + --last-name string 联系人姓 (可选) + --display-name string 联系人显示名称 (可选) +``` + +### 批量删除邮件联系人 + +> **特殊字符注意:** `--contact-ids` 的值可能包含 `$`、`!` 等 shell 特殊字符。在终端手动执行时,**必须用单引号**包裹这类参数值(如 `--contact-ids '101_0:DzzzzyJqO10$---.hp5uBuR'`),双引号会导致 `$` 被 shell 变量展开,使 ID 值被篡改从而报错。通过 MCP 协议(JSON 传参)调用时无此问题。 + +``` +Usage: + dws mail contact batch-delete [flags] +Example: + dws mail contact batch-delete --email user@company.com --contact-ids , +Flags: + --email string 用户邮箱地址 (必填) + --contact-ids string 要删除的联系人 ID 列表,逗号分隔 (必填) +``` + +### 获取自动回复配置 + +获取当前用户的邮件自动回复配置,包括是否启用、生效时间、回复范围和回复内容。 + +``` +Usage: + dws mail auto-reply get [flags] +Example: + dws mail auto-reply get --email user@company.com +Flags: + --email string 用户的邮箱地址 (必填) +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `enabled` | bool | 是否启用自动回复 (true=启用, false=禁用) | +| `startTime` | string | 自动回复开始时间 | +| `endTime` | string | 自动回复结束时间 | +| `scope` | string | 回复范围: "contact"(仅联系人) 或 "all"(所有人) | +| `content` | string | 自动回复内容 | + +### 收信规则管理 + +#### 列出收信规则 + +列出当前用户的所有收信规则,包括规则名称、启用状态、条件、动作和排序。 + +``` +Usage: + dws mail rule list [flags] +Example: + dws mail rule list --email user@company.com +Flags: + --email string 用户的邮箱地址 (必填) +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `total` | int | 规则总数 | +| `rules` | List[] | 规则列表 | +| `rules[].id` | string | 规则 ID | +| `rules[].name` | string | 规则名称 | +| `rules[].enabled` | bool | 是否启用 | +| `rules[].conditions` | List[] | 规则条件列表 | +| `rules[].actions` | List[] | 规则动作列表 | +| `rules[].order` | null | 排序字段;实测存量与新建规则该字段**恒为 `null`**,不返回有效排序值,不要依赖它判断规则顺序 | + +#### 创建收信规则 + +创建一条新的收信规则。支持设置规则名称、启用状态、匹配条件和执行动作。 + +> **`--conditions` 和 `--actions`** 为 JSON 数组字符串。 + +**界面与参数对应关系:** + +| 界面元素 | CLI 参数 / JSON 字段 | 说明 | +|----------|---------------------|------| +| 规则名称 | `--name` | 必填,规则的显示名称 | +| 如果满足以下「全部」条件 | `--conditions` | 多个条件之间为 **AND** 关系(即所有条件都满足才触发) | +| ├ 对象下拉(发件人) | `object: "from"` | 条件匹配的对象,可选值见下方 | +| ├ 操作下拉(包含) | `operation: "include"` | 匹配方式,可选值见下方 | +| └ 关键词输入框 | `keyword: "a@test.com"` | 匹配的具体值 | +| 执行以下操作 | `--actions` | 条件满足后执行的动作列表 | +| ├ 动作下拉(移动到文件夹) | `action: "ActSavetoFolder"` | 动作类型,可选值见下方 | +| └ 参数选择(收件箱) | `parameters: ["2"]` | 动作的参数,如目标文件夹 ID | + +**条件逻辑说明:** + +- `--conditions` 数组中的多个条件之间为 **AND(且)** 关系,即所有条件都满足才触发规则 +- 同一个条件对象(如 `from`)内部的 `or` 数组中多个表达式之间为 **OR(或)** 关系 +- 同一个 `and` 数组中的多个子条件之间为 **AND(且)** 关系 + +**条件对象 (object) 与合法操作类型 (operation) 组合:** + +| object | 合法 operation | 说明 | +|--------|---------------|------| +| `from` | `include`(包含), `exclude`(不包含), `oneof`(是联系人之一), `noneof`(不是联系人之一) | 匹配发件人地址或名称 | +| `to` | `include`(包含), `exclude`(不包含), `oneof`(是联系人之一), `noneof`(不是联系人之一) | 匹配收件人地址或名称 | +| `subject` | `include`(包含), `exclude`(不包含) | 匹配邮件主题 | +| `attachment` | `exist`(是否存在附件) | keyword="1" 表示有附件,keyword="0" 表示无附件 | +| `x-aliyun-size` | `greater`(大于), `less`(小于) | 邮件大小,单位为 **字节(Bytes)**(1KB=1024, 1MB=1048576);可组合使用表示范围区间 | + +**操作类型 (operation) 详细说明:** + +| 值 | 界面显示 | 适用 object | 说明 | +|----|---------|------------|------| +| `include` | 包含 | from, to, subject | 字段包含关键词 | +| `exclude` | 不包含 | from, to, subject | 字段不包含关键词 | +| `oneof` | 是联系人之一 | from, to | 字段值在给定联系人列表中 | +| `noneof` | 不是联系人之一 | from, to | 字段值不在给定联系人列表中 | +| `greater` | 大于 | x-aliyun-size | 数值大于阈值,单位字节(Bytes) | +| `less` | 小于 | x-aliyun-size | 数值小于阈值,单位字节(Bytes) | +| `exist` | 存在 | attachment | keyword="1" 表示有附件,keyword="0" 表示无附件 | + +**动作类型 (action) 可选值:** + +| 值 | 界面显示 | parameters 说明 | 前置依赖 | +|----|---------|----------------|----------| +| `ActSavetoFolder` | 移动到文件夹 | 目标文件夹 ID,如 `["2"]`(2=收件箱) | 需先通过 `dws mail folder list` 获取文件夹 ID | +| `ActFlagMail` | 标记标签 | 标签 ID 列表,逗号分隔,如 `["102,11,1"]` | 需先通过 `dws mail tag list` 获取标签 ID | +| `ActFlagMail2` | 标记已读 | `"asread"`(标记已读),服务端仅支持标记已读,不支持标记未读 | 无 | +| `ActReply` | 自动回复 | 回复内容文本,如 `["感谢您的来信"]` | 无 | + +**条件 JSON 结构说明:** + +每个条件由 `object`(匹配对象)和 `or`(OR 表达式列表)组成,`or` 内嵌 `and`(AND 条件列表)。 + +| 字段 | 说明 | +|------|------| +| `object` | 条件对象,取值及合法 operation 见上方组合表 | +| `or` | OR 表达式列表,同一 object 下多个 or 项之间为 **OR** 关系 | +| `and` | AND 条件列表,同一 or 项内多个 and 子条件之间为 **AND** 关系 | +| `operation` | 操作类型,必须与 object 合法组合(见上方组合表) | +| `keyword` | 关键词/阈值;attachment+exist 时 "1"=有附件/"0"=无附件;x-aliyun-size 时单位为字节(Bytes),如 1KB=1024, 1MB=1048576 | +| `ignoreCase` | 是否忽略大小写(布尔值,仅 from/to/subject + include/exclude 时需要) | + +**完整 conditions JSON 示例:** + +```json +[ + {"object":"from","or":[ + {"and":[{"operation":"oneof","keyword":"a@test.com","ignoreCase":true}]}, + {"and":[{"operation":"oneof","keyword":"b@test.com","ignoreCase":true}]} + ]}, + {"object":"subject","or":[{"and":[{"operation":"include","keyword":"报告","ignoreCase":true}]}]}, + {"object":"attachment","or":[{"and":[{"operation":"exist","keyword":"1"}]}]}, + {"object":"x-aliyun-size","or":[{"and":[{"operation":"greater","keyword":"1024"},{"operation":"less","keyword":"10240"}]}]} +] +``` + +> 上例表示:发件人是 a@test.com **或** b@test.com **且** 主题包含"报告" **且** 有附件 **且** 大小在 1KB(1024字节)~10KB(10240字节) 之间。 +> +> **同一 object 下匹配多个值的 OR 写法:** 在 `or` 数组中放多个 `and` 项(每个 `and` 对应一个匹配值),而非在一个 `and` 中放多个条件。例如上方 `from` 条件中,两个邮箱地址分别作为独立的 `and` 项放在 `or` 数组中,表示"满足任一即可"。 + +**完整 actions JSON 示例:** + +```json +[ + {"action":"ActSavetoFolder","parameters":["2"]}, + {"action":"ActFlagMail","parameters":["102,11,1"]}, + {"action":"ActFlagMail2","parameters":["asread"]}, + {"action":"ActReply","parameters":["感谢您的来信,我将尽快回复"]} +] +``` + +> **注意:** 使用 `ActSavetoFolder` 前需先通过 `dws mail folder list` 获取文件夹 ID;使用 `ActFlagMail` 前需先通过 `dws mail tag list` 获取标签 ID。 + +``` +Usage: + dws mail rule create [flags] +Example: + dws mail rule create --email user@company.com --name "VIP邮件标记" --enabled true \ + --conditions '[{"object":"from","or":[{"and":[{"operation":"include","keyword":"vip@company.com","ignoreCase":true}]}]}]' \ + --actions '[{"action":"ActFlagMail2","parameters":["asread"]}]' + dws mail rule create --email user@company.com --name "大附件归档" --enabled true \ + --conditions '[{"object":"x-aliyun-size","or":[{"and":[{"operation":"greater","keyword":"10485760"}]}]}]' \ + --actions '[{"action":"ActSavetoFolder","parameters":["6"]}]' +Flags: + --email string 用户的邮箱地址 (必填) + --name string 规则名称 (必填) + --enabled string 是否启用: true/false (必填) + --conditions string 规则条件 JSON 数组 (可选) + --actions string 规则动作 JSON 数组 (必填) +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `success` | bool | 创建是否成功 | +| `id` | string | 新建规则 ID | + +> 实测 `rule create` 成功仅返回 `{id, success}`,**不返回** `errorCode` / `errorMsg`。 + +#### 更新收信规则 + +更新已有的收信规则。**除 `--conditions` 外所有参数均为必填**。 + +> **建议工作流:** 先通过 `dws mail rule list` 获取当前规则的完整配置,再传入需要修改的字段值。 +> +> `--conditions` 为空或不传表示命中所有邮件(无条件匹配)。`--actions` 格式同 create 命令。 + +``` +Usage: + dws mail rule update [flags] +Example: + dws mail rule update --email user@company.com --id --name "新规则名" --enabled true \ + --actions '[{"action":"ActSavetoFolder","parameters":["6"]}]' + dws mail rule update --email user@company.com --id --name "全量归档" --enabled false \ + --conditions '[{"object":"subject","or":[{"and":[{"operation":"include","keyword":"报告","ignoreCase":true}]}]}]' \ + --actions '[{"action":"ActSavetoFolder","parameters":["6"]}]' +Flags: + --email string 用户的邮箱地址 (必填) + --id string 规则 ID (必填) + --name string 规则名称 (必填) + --enabled string 是否启用: true/false (必填) + --conditions string 规则条件 JSON 数组 (可选,为空表示命中所有邮件) + --actions string 规则动作 JSON 数组 (必填) +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `success` | bool | 更新是否成功 | +| `result` | object | 更新结果(实测为空对象 `{}`) | + +> 实测 `rule update` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。 + +#### 删除收信规则 + +删除指定的收信规则。 + +``` +Usage: + dws mail rule delete [flags] +Example: + dws mail rule delete --email user@company.com --id +Flags: + --email string 用户的邮箱地址 (必填) + --id string 规则 ID (必填) +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `success` | bool | 删除是否成功 | +| `result` | object | 删除结果(实测为空对象 `{}`) | + +> 实测 `rule delete` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。 + +#### 调整收信规则排序 + +调整指定收信规则的排序位置,向上(up)或向下(down)移动。 + +``` +Usage: + dws mail rule adjust [flags] +Example: + dws mail rule adjust --email user@company.com --id --direction up + dws mail rule adjust --email user@company.com --id --direction down +Flags: + --email string 用户的邮箱地址 (必填) + --id string 规则 ID (必填) + --direction string 调整方向: up/down (必填) +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `success` | bool | 调整是否成功 | +| `result` | object | 调整结果(实测为空对象 `{}`) | + +> 实测 `rule adjust` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。 + ## 通用错误说明 以下错误适用于所有 mail 命令。 @@ -596,6 +1482,20 @@ Flags: 用户说"查看附件/邮件附件/有什么附件" → 先 `message search` 获取 messageId,再 `attachment list` 用户说"下载附件/保存附件/把附件存到本地/把所有附件下载到..." → 先 `message search` 获取 messageId,再 `attachment list` 获取 attachmentId 和 name,最后逐个 `attachment download`(**不支持批量下载,不存在 download_batch/download_all 命令,必须逐个下载**) 用户说"把XX邮件的所有附件都下载" / "批量下载附件" / "下载4月所有发票邮件的附件" → **不存在批量下载命令**,必须按以下流程循环执行:1) `message search` 搜索匹配邮件获取 messageId 列表 → 2) 对每封邮件 `attachment list` 获取 attachmentId + name → 3) 对每个附件逐个调用 `attachment download`。不要编造 `download_batch` / `download_all` / `batch_download` 等不存在的命令 +用户说"创建邮件文件夹/新建邮箱文件夹/新建邮件目录" → `folder create` +用户说"在某个邮件文件夹下创建子文件夹" → 先 `folder list` 找到父文件夹 ID,再 `folder create --folder `;禁止把父文件夹名称直接填给 `--folder` +用户说"删除邮件文件夹/删除邮箱文件夹/删除邮件目录" → 先确认要删除的文件夹 ID;如果用户只给名称,先 `folder list` 找到 `folders[].id`,再 `folder delete --id ` +用户说"重命名邮件文件夹/修改邮箱文件夹名称/更新邮件目录名称" → 先确认要更新的文件夹 ID;如果用户只给原名称,先 `folder list` 找到 `folders[].id`,再 `folder update --id --name <新名称>` +用户说"查看邮件标签/列出邮箱标签/查看邮箱 label" → `tag list` +用户说"创建邮件标签/新建邮箱标签/新增 label" → `tag create` +用户说"在某个邮件标签下创建子标签" → 先 `tag list` 找到父标签 ID,再 `tag create --parent-id `;禁止把父标签名称直接填给 `--parent-id` +用户说"删除邮件标签/删除邮箱标签/删除 label" → 先确认要删除的标签 ID;如果用户只给名称,先 `tag list` 找到 `tags[].id`,再 `tag delete --id ` +用户说"重命名邮件标签/修改邮箱标签名称/更新 label 名称" → 先确认要更新的标签 ID;如果用户只给原名称,先 `tag list` 找到 `tags[].id`,再 `tag update --id --name <新名称>` +用户说"列出邮件会话/查看会话列表/查看某个文件夹里的邮件会话" → 先确认邮箱地址和文件夹 ID;如果只有文件夹名称,先 `folder list` 找到 `folders[].id`,再 `thread list --folder ` +用户说"标记会话已读/未读/给会话加标签/移除会话标签" → 用 `thread update`;如果是多条会话,用 `thread batch-update`;标签操作必须先有标签 ID +用户说"删除会话/把会话放入已删除/批量删除会话" → 单条用 `thread trash`,多条用 `thread batch-trash`;传入的是会话 ID,不是邮件 ID +用户说"批量标记邮件已读/未读/给邮件加标签/移除邮件标签" → `message batch-update --action markRead/markUnread/addTags/removeTags`(操作对象是邮件 ID;会话级操作用 `thread update/batch-update`) +用户说"邮件发出去了吗/查邮件发送状态/确认邮件是否发送成功/邮件投递结果" → 用发送类命令返回的 `internetMessageId`,调用 `message verify` 查询 `sendStatus` 用户说"查看会话/获取会话/看这封邮件的会话" → 先 `message search` 或 `message get` 获取邮件中的 `conversationId`,再 `thread get` 用户说"搜索/查找/联系 邮箱用户/联系人/某人的邮箱地址" → `user search`(搜索通讯录人员,不是搜邮件内容) 用户说"发送草稿/把草稿发出去/发这封草稿" → 先 `message search --query "folderId:5"` 找到草稿 messageId,再 `draft send` @@ -612,6 +1512,13 @@ Flags: - **严禁在用户未明确指定使用个人邮箱时,默认选择 `@dingtalk.com` 个人邮箱作为 `--email` / `--from`**;默认必须从 `mailbox list` 中挑选企业邮箱 - **涉及带附件的邮件操作时,严禁上传到钉钉媒体存储(media upload)**;必须使用对应命令的 `--attachment` / `--inline-attachment` 参数,由 CLI 内部完成附件处理 - **严禁编造不存在的批量下载命令**(如 `attachment download_batch`、`attachment download_all`、`attachment batch_download` 等)。下载附件只有 `attachment download` 一条命令,每次只能下载一个附件;需要批量下载时必须循环调用 +- **严禁把文件夹名称当作 `folder create --folder` 的值**;`--folder` 只能填父文件夹 ID,父文件夹 ID 必须来自 `folder list` 的 `folders[].id` +- **严禁把文件夹名称当作 `folder delete/update --id` 的值**;`--id` 只能填要操作的文件夹 ID,文件夹 ID 必须来自 `folder list` 的 `folders[].id` 或 `folder create` 的 `result.folder.id` +- **严禁把标签名称当作 `tag create --parent-id` 的值**;`--parent-id` 只能填父标签 ID,父标签 ID 必须来自 `tag list` 的 `tags[].id` +- **严禁把标签名称当作 `tag delete/update --id` 的值**;`--id` 只能填要操作的标签 ID,标签 ID 必须来自 `tag list` 的 `tags[].id` 或 `tag create` 的 `result.tag.id` +- **严禁更新或删除系统标签**;`tag update/delete` 只适用于用户自定义标签 +- **严禁把文件夹名称当作 `thread list --folder` 的值**;`--folder` 只能填文件夹 ID,文件夹 ID 必须来自 `folder list` 的 `folders[].id` +- **严禁把邮件 ID 当作 `thread get/update/trash/batch-update/batch-trash` 的会话 ID**;这些命令需要 conversationId,可来自 `thread list` 的 `conversations[].id` 或邮件结果中的 `conversationId` ## 核心工作流 @@ -619,31 +1526,65 @@ Flags: # 1. 查看可用邮箱 — 提取邮箱地址 dws mail mailbox list --format json +# 1b. 创建顶层邮件文件夹 +dws mail folder create --email user@company.com --name "项目资料" --format json + +# 1c. 创建子文件夹 — 先通过 folder list 获取父文件夹 id,再传给 --folder +dws mail folder list --email user@company.com --format json +dws mail folder create --email user@company.com --name "子文件夹" --folder --format json + +# 1d. 更新或删除邮件文件夹 — 先通过 folder list 获取目标文件夹 id,再传给 --id +dws mail folder list --email user@company.com --format json +dws mail folder update --email user@company.com --id --name "新文件夹名" --format json +dws mail folder delete --email user@company.com --id --format json + +# 1e. 创建顶层邮件标签 +dws mail tag create --email user@company.com --name "项目资料" --format json + +# 1f. 创建子标签 — 先通过 tag list 获取父标签 id,再传给 --parent-id +dws mail tag list --email user@company.com --format json +dws mail tag create --email user@company.com --name "子标签" --parent-id --format json + +# 1g. 更新或删除邮件标签 — 先通过 tag list 获取目标标签 id,再传给 --id +dws mail tag list --email user@company.com --format json +dws mail tag update --email user@company.com --id --name "新标签名" --format json +dws mail tag delete --email user@company.com --id --format json + +# 1h. 列出文件夹中的邮件会话 — 先通过 folder list 获取文件夹 id,再传给 --folder +dws mail folder list --email user@company.com --format json +dws mail thread list --email user@company.com --folder --limit 10 --format json + +# 1i. 修改或删除邮件会话 — 先通过 thread list 获取 conversationId +dws mail thread update --email user@company.com --id --action markRead --format json +dws mail thread batch-update --email user@company.com --ids , --action markUnread --format json +dws mail thread trash --email user@company.com --id --yes --format json +dws mail thread batch-trash --email user@company.com --ids , --yes --format json + # 2. 搜索邮件 — 提取 messageId dws mail message search --email user@company.com \ - --query "subject:\"周报\" AND date>2025-06-01T00:00:00Z" --size 10 --format json + --query "subject:\"周报\" AND date>2025-06-01T00:00:00Z" --limit 10 --format json # 3. 查看邮件详情 dws mail message get --email user@company.com --id --format json # 4. 发送邮件(纯文本) dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "本周完成…" --format json + --subject "周报" --content "本周完成…" --format json # 4b. 发送带附件的邮件(自动编排:创建草稿→上传附件→发送草稿) dws mail message send --from user@company.com --to colleague@company.com \ - --subject "周报" --body "见附件" --attachment ./report.pdf --format json + --subject "周报" --content "见附件" --attachment ./report.pdf --format json # 4c. 发送带内联图片的邮件(正文自动转 HTML, 标签自动注入) dws mail message send --from user@company.com --to colleague@company.com \ - --subject "图表周报" --body "本周图表如下:[inline:chart.png]" \ + --subject "图表周报" --content "本周图表如下:[inline:chart.png]" \ --inline-attachment ./chart.png --format json # 5. 下载邮件附件到本地(每次只能下载一个附件,不支持批量下载) # 步骤 5.1:搜索匹配的邮件,获取 messageId 列表 # 示例:下载4月所有发票邮件的附件 dws mail message search --email user@company.com \ - --query "subject:发票 AND date>2025-04-01T00:00:00Z AND date<2025-05-01T00:00:00Z AND hasAttachments:true" --size 50 --format json + --query "subject:发票 AND date>2025-04-01T00:00:00Z AND date<2025-05-01T00:00:00Z AND hasAttachments:true" --limit 50 --format json # 步骤 5.2:对每封邮件,列出附件获取 attachmentId 和 name # (对搜索结果中的每封邮件都要执行一次) @@ -656,7 +1597,7 @@ dws mail attachment download --email user@company.com \ # 5. 获取邮件所属会话详情(thread) # 步骤 5.1:先通过 message search 或 message get 获取邮件中的 conversationId dws mail message search --email user@company.com \ - --query "subject:\"周报\"" --size 5 --format json + --query "subject:\"周报\"" --limit 5 --format json # 从返回的邮件列表中提取 conversationId 字段 # 步骤 5.2:用 conversationId 获取会话详情 @@ -676,8 +1617,16 @@ dws mail thread get --email user@company.com --id --select mess | `message search` | `messageId` | attachment list 的 --id | | `attachment list` | `attachments[].id` / `attachments[].name` | attachment download 的 --attachment-id / --name | | `message get` | `conversationId` | thread get 的 --id | +| `folder list` | `folders[].id` | folder create 的 --folder;folder delete/update 的 --id | +| `folder create` | `result.folder.id` | 后续创建子文件夹或移动邮件时作为 --folder;更新/删除该文件夹时作为 --id | +| `folder list` | `folders[].id` | thread list 的 --folder | +| `thread list` | `conversations[].id` | thread get/update/trash/batch-update/batch-trash 的 --id/--ids | +| `tag list` | `tags[].id` | message batch-update 的 --tags;thread update/batch-update 的 --tag-ids | +| `tag list` | `tags[].id` | tag create 的 --parent-id;tag delete/update 的 --id | +| `tag create` | `result.tag.id` | 后续创建子标签时作为 --parent-id;更新/删除该标签时作为 --id | | `aisearch person` → `contact user get` / `contact user search` / `mail user search` | 用户邮箱 (orgAuthEmail / email) | message send 的 --to/--cc(三路并发,取先到结果) | | `user search` | 用户邮箱 (email) | message send 的 --to/--cc | +| `message send` / `draft send` / `message reply` / `message reply-all` / `message forward` | `internetMessageId` | `message verify` 的 --internet-message-id | ## 注意事项 @@ -691,6 +1640,11 @@ dws mail thread get --email user@company.com --id --select mess 3. `dws contact user search --keyword "名字"`,提取用户邮箱字段 若三路均无有效邮箱,必须 ask_human 请用户手动提供收件人邮箱,严禁臆测和假设 - `thread get` 无法直接通过邮箱地址查询会话列表,**必须先有 conversationId**;conversationId 来自 `message search` 或 `message get` 返回的邮件字段 `conversationId` -- `thread get` 默认不返回邮件列表,如需查看会话内所有邮件,需加 `--select messages`;如需同时返回多个可选字段,用英文逗号分隔,如 `--select messages,internetMessageId` -- `thread get` 返回的 `messages` 列表中,邮件正文(`body`)、收件人(`toRecipients`)等字段默认不包含,需在 `--select` 中额外指定 +- `thread get` 默认即返回会话内 `messages` 列表,但列表中每封邮件的正文(`body`)、收件人(`toRecipients`)等字段默认为 null;需在 `--select` 中额外指定才有值,如 `--select messages,internetMessageId`(多个字段用英文逗号分隔) +- `thread get` 返回的 `conversation` 不含 `flag` 字段,会话标识请从 `thread list` 获取 - `user search` 仅支持企业邮箱(非 `@dingtalk.com` 个人邮箱),使用个人邮箱将因无权限报错;搜到的用户邮箱(`email` 字段)可直接用于 `message send` 的 `--to`/`--cc` 参数 +- `thread list --folder` 的值必须是文件夹 ID,不是文件夹显示名称;不知道文件夹 ID 时,先调用 `folder list` 查 `folders[].id` +- `thread get/update/trash/batch-update/batch-trash` 使用的是会话 ID(conversationId),不是邮件 ID;会话 ID 可来自 `thread list` 的 `conversations[].id`,也可来自 `message search` 或 `message get` 返回的 `conversationId` +- `thread update` / `thread batch-update` 仅支持 `markRead`、`markUnread`、`addTags`、`removeTags`;标签操作必须传 `--tag-ids` +- `folder create --folder` 的值必须是父文件夹 ID,不是文件夹显示名称;不知道父文件夹 ID 时,先调用 `folder list` 查 `folders[].id` +- `folder delete/update --id` 的值必须是目标文件夹 ID,不是文件夹显示名称;不知道目标文件夹 ID 时,先调用 `folder list` 查 `folders[].id` diff --git a/skills/multi/dingtalk-mail/scripts/mail_send_with_cc.py b/skills/multi/dingtalk-mail/scripts/mail_send_with_cc.py index 2b29a6f5..b8ad2a31 100644 --- a/skills/multi/dingtalk-mail/scripts/mail_send_with_cc.py +++ b/skills/multi/dingtalk-mail/scripts/mail_send_with_cc.py @@ -121,7 +121,7 @@ def main(): '--from', from_email or '', '--to', args.to, '--subject', args.subject, - '--body', args.body, + '--content', args.body, '--format', 'json', ] if args.cc: diff --git a/skills/multi/dingtalk-mail/scripts/mail_unread_summary.py b/skills/multi/dingtalk-mail/scripts/mail_unread_summary.py index 09d19624..5588db97 100644 --- a/skills/multi/dingtalk-mail/scripts/mail_unread_summary.py +++ b/skills/multi/dingtalk-mail/scripts/mail_unread_summary.py @@ -102,7 +102,7 @@ def main(): 'mail', 'message', 'search', '--email', email or '', '--query', kql, - '--size', str(args.size), + '--limit', str(args.size), '--format', 'json', ], dry_run=args.dry_run) diff --git a/skills/multi/dingtalk-minutes/references/minutes.md b/skills/multi/dingtalk-minutes/references/minutes.md index 438e07ad..5f9f71b9 100644 --- a/skills/multi/dingtalk-minutes/references/minutes.md +++ b/skills/multi/dingtalk-minutes/references/minutes.md @@ -7,8 +7,8 @@ minutes 模块的命令是**两级或三级结构**,不同层级之间不能 > **默认 scope 规则**:`dws minutes list` 后**必须**跟 scope 子命令。若用户未明确指定查询范围,**一律默认补 `all`**(查询我可访问的所有听记 = 我创建的 + 他人共享给我的,覆盖面最广)。 > > **`list` 不带 scope 的陷阱(0519 P2 Golden Case 提炼)**: -> - 裸 `dws minutes list`(不跟 mine/shared/all)虽然不会报错,但返回结果**不完整**(仅返回最近少量条目,约 913 字节) -> - `dws minutes list all` 才能返回完整列表(约 3362 字节) +> - 裸 `dws minutes list`(不跟 mine/shared/all)**不会返回任何听记数据**——它只打印该命令的帮助/用法信息(Usage、Available Commands: all/mine/shared、Flags),等于什么都没查到 +> - 必须带 scope 子命令(`dws minutes list all` / `list mine` / `list shared`)才会真正返回听记列表 > - **AI 严禁使用裸 `dws minutes list`**,必须始终带 scope 子命令 > - 如果 LLM 不确定用哪个 scope,**一律用 `all`** > @@ -77,7 +77,7 @@ dws minutes | `dws minutes list all --end-time "..."` | 参数名是 `--end` 不是 `--end-time` | `dws minutes list all --end "2026-04-30"` | | `dws minutes list --page-size 10` | `--page-size` 不存在,分页用 `--max`;且 `list` 后缺 scope | `dws minutes list mine --max 10` | | `dws minutes list --date-range 2026-05-04 2026-05-10` | `--date-range` 不存在,时间范围用 `--start` + `--end` 两个参数;且 `list` 后缺 scope | `dws minutes list mine --start "2026-05-04T00:00:00+08:00" --end "2026-05-10T23:59:59+08:00"` | -| `dws minutes list mine --limit 10` | `--limit` 尚未注册(跨产品规约 Primary 为 `--limit`,但 minutes CLI 当前只接受 `--max`)。**待 alias 注册后两者等价** | `dws minutes list mine --max 10` | +| `dws minutes list mine --limit 10` | 不报错——`--limit` 已注册为 `--max` 的合法别名,两者完全等价(`--limit 3` 与 `--max 3` 返回相同条数) | `dws minutes list mine --max 10`(或 `--limit 10`,二选一即可) | | `dws minutes upload create --json '{"fileName":...}'` | `--json` 不存在,cli 不接受 JSON 作为输入格式 | `dws minutes upload create --file-name "xxx.mp3" --file-size 61565431` | | `dws minutes upload create -f json '{"fileName":...}'` | `-f json` / `--format json` 是**输出格式**控制,不是输入参数 | `dws minutes upload create --file-name "xxx.mp3" --file-size 61565431 --format json` | | `dws minutes get transcription --id \| head -c 2000` | Windows 沙箱无 `head` 命令,**严禁使用 shell 管道截断** | `dws minutes get transcription --id --format json`(由 AI 在内存中截断处理) | @@ -90,7 +90,7 @@ dws minutes | `dws minutes transcribe --url <听记url>` | `transcribe` 不是合法子命令,`--url` 参数也不存在。LLM 凭印象编造 | 先从 URL 提取 taskUuid(见「URL → taskUuid 自动提取规则」),再 `dws minutes get transcription --id --format json` | | `dws minutes get transcription --url <听记url>` | `--url` 参数不存在,`--id` 只接受纯 taskUuid | 同上:从 URL 提取 taskUuid 后用 `--id` | | `dws minutes summary --uuid ` | `summary` 不是顶层子命令(应在 `get` 下),且顶层不识别 `--uuid`。**0519 高频错误** | `dws minutes get summary --id ` | -| `dws minutes list`(不跟 scope) | `list` 后**必须**跟 scope(mine/shared/all)。裸 `list` 返回结果不完整且行为未文档化 | `dws minutes list all`(默认 scope) | +| `dws minutes list`(不跟 scope) | `list` 后**必须**跟 scope(mine/shared/all)。裸 `list` 只打印帮助信息,不返回任何听记数据 | `dws minutes list all`(默认 scope) | | `dws report inbox --format json`(无时间窗口) | `inbox` 是兼容入口,等价于 `inbox list`;仍必须带 `--start` / `--end` | `dws report inbox list --start "2026-05-06T00:00:00+08:00" --end "2026-05-13T23:59:59+08:00" --format json` | | `dws report inbox list --format json`(不带 --start) | inbox list **必须**带 `--start` / `--end` 参数,否则报 "flag --start is required" | `dws report inbox list --start "2026-05-06T00:00:00+08:00" --end "2026-05-13T23:59:59+08:00" --format json` | @@ -102,14 +102,14 @@ dws minutes > > | 语义 | 规约 Primary | minutes 当前 CLI 实际参数 | alias 注册状态 | 说明 | > |------|-------------|------------------------|--------------|------| -> | 单页大小 (Group 13) | `--limit` | `--max` | `--limit` 未注册 | minutes 用 `--max`,其他模块用 `--limit`。**跨模块切换时注意差异** | +> | 单页大小 (Group 13) | `--limit` | `--max`(`--limit` 为合法别名) | `--limit` 已注册 | minutes 推荐 `--max`,但 `--limit` 也可用,两者等价 | > | 续页标识 (Group 14) | `--cursor` | `--next-token` | `--next-token` 在 alias 池中 | minutes 用 `--next-token`,合法 alias | > | 搜索关键词 (Group 11) | `--query` | `--query` | 已对齐 | 无差异 | > | 起始时间 (Group 15) | `--start` | `--start` | 已对齐 | 无差异 | > | 结束时间 (Group 16) | `--end` | `--end` | 已对齐 | 无差异 | > > **跨模块参数名差异速查(防止混用):** -> - 分页大小:minutes 用 `--max`,aitable/calendar/chat/drive 用 `--limit` +> - 分页大小:minutes 推荐 `--max`(`--limit` 也是合法别名,等价),aitable/calendar/chat/drive 用 `--limit` > - 续页标识:minutes 用 `--next-token`,aitable/drive/doc 用 `--cursor` > - 起止时间:minutes/report 用 `--start`/`--end`,calendar 用 `--start-time`/`--end-time`(跨模块最高频错误) @@ -260,8 +260,8 @@ dws minutes > | `missing required flag(s): --session-id` | cancel 命令必须传 session-id | 从之前 upload create 的返回中提取 session-id;若丢失则暂无法取消 | > > **上传流程三步走**(AI 必须按此顺序执行,不可跳步): -> 1. `dws minutes upload create --file-name "xxx.mp3" --file-size <字节数> --format json` → 获取 `sessionId` + `uploadUrl` -> 2. 将文件通过 `uploadUrl` 直传(由客户端侧完成,AI 不介入) +> 1. `dws minutes upload create --file-name "xxx.mp3" --file-size <字节数> --format json` → 获取 `sessionId` + `presignedUrl` +> 2. 将文件通过 `presignedUrl` 直传(由客户端侧完成,AI 不介入) > 3. `dws minutes upload complete --session-id --format json` → 确认上传完成 > > **注意**:`--file-size` 单位是**字节**(不是 KB/MB),AI 不要帮用户估算大小,应让用户确认文件实际字节数。 @@ -380,7 +380,7 @@ Flags: --id string 听记 taskUuid (必填),取值逻辑参考 ## 注意事项 ``` -返回字段: 创建人、开始时间、截止时间、听记标题、听记访问链接URL +返回字段(result 内): `duration`(时长, 秒)、`startTime`(开始时间, 毫秒时间戳)、`endTime`(结束时间, 仅较长会议返回, 短录音无此字段)、`taskUuid`(听记 ID)、`title`(听记标题)、`url`(听记访问链接)。注意: **不返回创建人(creator)字段**。 **发言人列表输出规范(查询详情后必须执行)**: diff --git a/skills/multi/dingtalk-oa/references/oa.md b/skills/multi/dingtalk-oa/references/oa.md index e9984282..c44c049e 100644 --- a/skills/multi/dingtalk-oa/references/oa.md +++ b/skills/multi/dingtalk-oa/references/oa.md @@ -12,7 +12,7 @@ Example: Flags: --end string 结束时间 ISO-8601 (如 2026-03-10T23:59:59+08:00) (必填) --page string 分页页码 (可选) - --size string 每页大小 (可选) + --limit string 每页大小 (可选) --start string 开始时间 ISO-8601 (如 2026-03-10T00:00:00+08:00) (必填) --query string 关键字搜索 (可选) ``` @@ -80,16 +80,16 @@ Flags: --instance-id string 审批实例 ID (必填) ``` -### 查询已发起的审批实例列表 +### 查询我已发起的审批实例记录 ``` Usage: dws oa approval list-initiated [flags] Example: - dws oa approval list-initiated --process-code --start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" --next-token 0 --max-results 20 + dws oa approval list-initiated --process-code --start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" --cursor 0 --limit 20 Flags: --end string 结束时间 ISO-8601 (如 2026-03-10T23:59:59+08:00) (必填) - --max-results string 每页大小,最大 20 (必填) - --next-token string 分页游标,首次传 0 (必填) + --limit string 每页大小,最大 20 (必填) + --cursor string 分页游标,首次传 0 (必填) --process-code string 表单 processCode (必填) --start string 开始时间 ISO-8601 (如 2026-03-10T00:00:00+08:00) (必填) ``` @@ -99,12 +99,86 @@ Flags: Usage: dws oa approval list-forms [flags] Example: - dws oa approval list-forms --cursor 0 --size 100 + dws oa approval list-forms --cursor 0 --limit 100 Flags: --cursor string 分页游标,首次传 0 (默认 "0") - --size string 每页大小,最大 100 (默认 "100") + --limit string 每页大小,最大 100 (默认 "100") ``` -MCP 工具: `list_user_visible_process`;参数: cursor, pageSize(对应 --cursor/--size)。返回结果含 processCode,可用于 list-initiated 的 --process-code。 +MCP 工具: `list_user_visible_process`;参数: cursor, pageSize(对应 --cursor/--limit)。返回结果含 processCode,可用于 list-initiated 的 --process-code。 + +### 按关键字模糊搜索审批表单 +``` +Usage: + dws oa approval search-forms [flags] +Example: + dws oa approval search-forms --query AI + dws oa approval search-forms --query 报销 +Flags: + --query string 关键字,匹配 processCode 或表单名称 (必填) +``` + +### 获取审批任务的被催办人 userId + +> **催办必须两步串联:** ① `ding-info` 获取被催办人 `userId` → ② `ding message send` 发送催办消息。禁止跳过第一步直接猜测 userId。 + +``` +Usage: + dws oa approval ding-info [flags] +Example: + dws oa approval ding-info --task-id +Flags: + --task-id string 审批任务 ID (必填),来自 list-pending 或 tasks +``` + +返回值字段: +- `userId` — 被催办人用户 ID(必取),作为 `ding message send` 的 `--users` 入参,多个以逗号拼接 + +**不返回** robotCode 和 content,需由 agent 自行处理: +- `--robot-code`:优先取环境变量 `$DINGTALK_DING_ROBOT_CODE`;若无则向用户确认 +- `--content`:由 agent 根据审批上下文撰写催办文案,建议格式 `"请尽快审批《{表单名}》(提交人:{发起人},提交时间:{时间})"` +- `--users`:取本接口返回的 `userId` + +**叮消息类型(`--type`):** +- 默认不发 `--type` → 应用内 DING 提醒(免费,推荐) +- `--type sms` → 短信提醒(有成本,需向用户确认) +- `--type call` → 电话提醒(有成本,需向用户确认) + +**完整催办流程:** +```bash +# Step 1: 获取被催办人 userId +dws oa approval ding-info --task-id --format json +# Step 2: 发送催办消息(robotCode 优先走环境变量,content 由 agent 撰写) +dws ding message send --robot-code $DINGTALK_DING_ROBOT_CODE --users --content "请尽快审批《XXX》" --format json +# Step 3 (可选): 如需短信/电话提醒,加 --type sms 或 --type call +dws ding message send --robot-code $DINGTALK_DING_ROBOT_CODE --users --content "请尽快审批《XXX》" --type sms --format json +``` + +### 获取任务可回退的节点信息 + +> **IMPORTANT:** 退回任务前**必须先调用此命令**获取可回退节点列表,从中提取 `activityId` 和 `revertAction` 作为 `revert-task` 的入参。若无返回值,明确告知用户"当前任务无可回退节点"。 + +``` +Usage: + dws oa approval revert-activities [flags] +Example: + dws oa approval revert-activities --task-id +Flags: + --task-id string 审批任务 ID (必填) +``` + +返回字段说明: +- `instRevertActivities[]` — 可回退的节点列表 + - `activityId` — 节点 ID,即 `revert-task` 的 `--target-activity-id` + - `activityName` — 节点名称(如"发起人"、"审批人"),用于向用户展示 + - `revertAction` — 退回方式,即 `revert-task` 的 `--action` + - `REVERT_FOR_RESUBMIT` → 退回到发起人重交(此时 `activityId` 为 `sid-startevent`) + - `REVERT_FOR_APPROVAL` → 退回到某审批节点重新审批 + - `activityActioners[]` — 该节点的审批人列表 + - `actualActioners[]` — 该节点的实际处理人列表 + - `approvalIndex` — 审批节点序号(仅审批节点有) + - `actType` — 审批类型(如 `one_by_one` 依次审批) + +**无返回值处理:** 若 `instRevertActivities` 为空或不存在,必须明确告知用户"当前任务无可回退节点",不得继续执行退回操作。 ### 查询待我审批的任务 ID ``` @@ -171,25 +245,65 @@ MCP 工具: `redirect_task`;参数: taskId, toActionerId, remark(对应 --ta Usage: dws oa approval oa-comments [flags] Example: - dws oa approval oa-comments --instance-id --text "同意,请尽快处理" + dws oa approval oa-comments --instance-id --content "同意,请尽快处理" Flags: --instance-id string 审批实例 ID (必填) - --text string 评论内容 (必填) + --content string 评论内容 (必填) ``` -MCP 工具: `dingflow_comments`;参数: processInstanceId, text(对应 --instance-id/--text)。processInstanceId 可通过 `list-pending` 或 `detail` 获取。 +MCP 工具: `dingflow_comments`;参数: processInstanceId, text(对应 --instance-id/--content)。processInstanceId 可通过 `list-pending` 或 `detail` 获取。 ### 对审批实例进行抄送 ``` Usage: dws oa approval oa-cc-noticer [flags] Example: - dws oa approval oa-cc-noticer --instance-id --user-list "68674200835816" - dws oa approval oa-cc-noticer --instance-id --user-list "userId1,userId2" + dws oa approval oa-cc-noticer --instance-id --users "68674200835816" + dws oa approval oa-cc-noticer --instance-id --users "userId1,userId2" --operator-id "123123" Flags: --instance-id string 审批实例 ID (必填) - --user-list string 抄送用户 ID 列表,多个用逗号分隔 (必填) + --users string 抄送用户 ID 列表,多个用逗号分隔 (必填) + --operator-id string 操作人 ID (可选) +``` +MCP 工具: `oa_cc_noticer`;参数: processInstanceId, userList(对应 --instance-id/--users)。processInstanceId 可通过 `list-pending` 或 `detail` 获取,抄送用户 ID 可通过 `dws contact user search` 获取。 + +### 对审批任务进行加签 + +> **CAUTION:** 加签操作不可撤回 — 执行前必须向用户确认加签类型、被加签人和激活方式。 +``` +Usage: + dws oa approval append-task [flags] +Example: + dws oa approval append-task --instance-id --task-id --type before --appender-user-ids "userId1,userId2" --activate-type ALL --agree-all true + dws oa approval append-task --instance-id --task-id --type after --appender-user-ids "userId1" --activate-type ONE_BY_ONE --agree-all false +Flags: + --instance-id string 审批实例 ID (必填) + --task-id string 审批任务 ID (必填) + --type string 加签类型:before(前加签),after(后加签),Parallel(并加签)(必填) + --appender-user-ids string 被加签用户 ID 列表,多个用逗号分隔 (必填) + --activate-type string 任务激活类型:ALL(或签),ONE_BY_ONE(依次审批)(必填) + --agree-all string 是否需要全部同意 (必填) 是 true 否 false +``` + +### 退回审批任务 + +> **CAUTION:** 退回操作不可撤回 — 执行前必须向用户确认退回方式及目标节点。 +> **前置步骤:** 必须先调用 `revert-activities` 获取可回退节点列表,从中提取 `activityId` 和 `revertAction`。若无返回值,明确告知用户"当前任务无可回退节点"。 + +``` +Usage: + dws oa approval revert-task [flags] +Example: + # 退回到发起人(targetActivityId 固定传 sid-startevent) + dws oa approval revert-task --instance-id --task-id --target-activity-id sid-startevent --action REVERT_FOR_RESUBMIT --remark "补充说明后重提" + # 退回到某个审批节点(targetActivityId 从 revert-activities 返回中获取 activityId) + dws oa approval revert-task --instance-id --task-id --target-activity-id --action REVERT_FOR_APPROVAL --remark "重新审批" +Flags: + --instance-id string 审批实例 ID (必填) + --task-id string 审批任务 ID (必填) + --target-activity-id string 退回到的节点 ID;退回发起人时固定传 sid-startevent (必填) + --action string 退回方式:REVERT_FOR_APPROVAL(退回到审批人)/ REVERT_FOR_RESUBMIT(退回到发起人)(必填) + --remark string 退回说明 (可选) ``` -MCP 工具: `oa_cc_noticer`;参数: processInstanceId, userList(对应 --instance-id/--user-list)。processInstanceId 可通过 `list-pending` 或 `detail` 获取,抄送用户 ID 可通过 `dws contact user search` 获取。 ## 意图判断 @@ -207,8 +321,8 @@ MCP 工具: `oa_cc_noticer`;参数: processInstanceId, userList(对应 --ins 用户说"我审批/处理过的审批单" -> `approval list-executed` 用户说"抄送我的审批单" -> `approval list-cc` 用户说"转交审批/转交任务" → `approval redirect-task`(需 --task-id 和 --to-actioner-id) -用户说"评论审批/添加评论/写评论" → `approval oa-comments`(需 --instance-id 和 --text) -用户说"抄送审批/添加抄送人" → `approval oa-cc-noticer`(需 --instance-id 和 --user-list) +用户说"评论审批/添加评论/写评论" → `approval oa-comments`(需 --instance-id 和 --content) +用户说"抄送审批/添加抄送人" → `approval oa-cc-noticer`(需 --instance-id 和 --users) ## 核心工作流 @@ -235,12 +349,12 @@ dws oa approval revoke --instance-id --remark "误发起" --format json dws oa approval records --instance-id --format json # 7. 获取可见审批表单(得到 processCode) -dws oa approval list-forms --cursor 0 --size 100 --format json +dws oa approval list-forms --cursor 0 --limit 100 --format json # 8. 查看自己发起的审批列表(--process-code 来自 list-forms 或 detail) dws oa approval list-initiated --process-code \ --start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" \ - --next-token 0 --max-results 20 --format json + --cursor 0 --limit 20 --format json # 9. 我处理过的审批单 dws oa approval list-executed --limit --page --query 关键词 --format json @@ -254,11 +368,11 @@ dws oa approval redirect-task --task-id --to-actioner-id --for dws oa approval redirect-task --task-id --to-actioner-id --remark "请帮忙处理" --format json # 13. 对审批实例添加评论(processInstanceId 来自 list-pending 或 detail) -dws oa approval oa-comments --instance-id --text "同意,请尽快处理" --format json +dws oa approval oa-comments --instance-id --content "同意,请尽快处理" --format json # 14. 对审批实例进行抄送(processInstanceId 来自 list-pending 或 detail) -dws oa approval oa-cc-noticer --instance-id --user-list "68674200835816" --format json -dws oa approval oa-cc-noticer --instance-id --user-list "userId1,userId2" --format json +dws oa approval oa-cc-noticer --instance-id --users "68674200835816" --format json +dws oa approval oa-cc-noticer --instance-id --users "userId1,userId2" --format json ``` ## 上下文传递表 @@ -278,6 +392,7 @@ dws oa approval oa-cc-noticer --instance-id --user-list "use - `revoke` 只能撤销自己发起的审批 - `--remark` 审批意见虽为可选,但建议填写以留存审批痕迹 - `list-initiated` 的 `--process-code` 可从 `list-forms` 或 `detail` 返回中提取 +- `list-initiated` 的 `--start` / `--end` 区间有后端上限(约 120 天)。超过上限会返回误导性的 `business_error: 时间戳无效`(实为区间过长,不是时间格式问题)。跨度大时请拆成多段短区间分别查询 ## 自动化脚本 diff --git a/skills/multi/dingtalk-report/SKILL.md b/skills/multi/dingtalk-report/SKILL.md index bfe05a1c..c71cd0a9 100644 --- a/skills/multi/dingtalk-report/SKILL.md +++ b/skills/multi/dingtalk-report/SKILL.md @@ -28,17 +28,21 @@ metadata: | 用户说 | 命令 | |--------|------| | "今天收到的日志" | `python scripts/report_received_today.py` | -| "看日志模版" | `dws report template list` → `dws report template detail --name "<模版名>"` | -| "提交日报 / 周报(按模版)" | `dws report create --template-id --contents '[...]' | -| "我已发送的日志" | `dws report sent --start --end ` | -| "日志已读统计" | `dws report stats --report-id ` | +| "看日志模版" | `dws report template list` → `dws report template get --name "<模版名>"` | +| "提交日报 / 周报(按模版)" | `dws report entry submit --template-id --contents-file ` | +| "我收到的日志" | `dws report inbox list --start --end --cursor 0 --size 20` | +| "我已发送的日志" | `dws report outbox list --cursor 0 --size 20` | +| "查看某条日志正文" | `dws report entry get --report-id ` | +| "日志已读统计" | `dws report entry stats --report-id ` | | "生成日报 / 周报 / 月报 / 主题报告" | 见 [05-reporting.md](references/05-reporting.md) recipe | +> 已废弃的旧命令 `report list` / `report detail` / `report sent` / `report stats` / `report create` / `report template detail` 均标 [deprecated],请一律改用上表的 `inbox list` / `outbox list` / `entry get` / `entry stats` / `entry submit` / `template get`。 + ## 日志查询硬约束 -- 查“收到的日志”必须用 `dws report list --start "" --end "" --cursor 0 --size 20 --format json`,并把“今天 / 最近 30 天”等时间词先展开成完整 ISO 起止时间。 -- 列表返回后,后续 `detail` / `stats` 必须复用同一个 `reportId`;不要重新挑选、猜测或改用标题。 -- 用户要正文时用 `dws report detail --report-id `;用户要已读/统计时用 `dws report stats --report-id `。 +- 查“收到的日志”必须用 `dws report inbox list --start "" --end "" --cursor 0 --size 20 --format json`,并把“今天 / 最近 30 天”等时间词先展开成完整 ISO 起止时间;查“我发出的日志”用 `dws report outbox list --cursor 0 --size 20 --format json`。 +- 列表返回后,后续 `entry get` / `entry stats` 必须复用同一个 `reportId`(在返回的 `_internalDetailCommands[].command` 里);不要重新挑选、猜测或改用标题。 +- 用户要正文时用 `dws report entry get --report-id `;用户要已读/统计时用 `dws report entry stats --report-id `。 ## 跨产品协作 diff --git a/skills/multi/dingtalk-report/references/05-reporting.md b/skills/multi/dingtalk-report/references/05-reporting.md index 4f8c3e96..f1e0e971 100644 --- a/skills/multi/dingtalk-report/references/05-reporting.md +++ b/skills/multi/dingtalk-report/references/05-reporting.md @@ -8,7 +8,7 @@ | query 中是否含强信号 | 选哪个 recipe | 默认值 | |---------------------|---------------|--------| -| 含「钉钉日志 / OA 周报 / 我的钉钉日志 / 日报模板 / 周报模板 / 提交日志 / 填模版」 | `submit-report`(走 dws report create) | — | +| 含「钉钉日志 / OA 周报 / 我的钉钉日志 / 日报模板 / 周报模板 / 提交日志 / 填模版」 | `submit-report`(走 dws report entry submit) | — | | 含「在线文档 / 写一篇文档 / 整理成文档 / 文档保存」 | `generate-*-report`(走 dws doc create) | — | | **无强信号**(如"写日报"、"写周报"、"整理本周工作")| `generate-*-report`(走 dws doc create) | 默认 | @@ -21,9 +21,9 @@ | Recipe | 行动指南(固定路线) | |--------|-------------------| -| query-report | **0. 前置判定**:query 含「查日志 / 看日志 / 我发过的日志 / 收到的日志 / 日志详情」且语义指向钉钉日志 OA 应用?是 → 直接走 `dws report`;否 → 先按 doc/report 分歧澄清
1. 用户说「我发过 / 我创建」→ `report sent --cursor 0 --size 20 --format json`;用户说「收到 / 别人发给我」→ `report list --start "" --end "" --cursor 0 --size 20 --format json`(注意:`dws report inbox` 当前 CLI 未实现,禁止编造)
2. 时间 flag 只允许 `--start` / `--end`,禁止 `--start-date` / `--end-date` / `--date`;不要只传裸日期,必须展开完整 ISO;不要先查 `help`,不要预先登录;只有命令返回认证错误时才处理认证
3. 从列表返回中取 `reportId` 留给内部后续调用;如果用户已直接提供 `reportId`,跳过列表
4. 面向用户展示列表时必须基于 `result[]` 拼 Markdown 表:`日期 | 标题 | 发送人 | 状态 | 钉钉链接`;每条 `result[]` 都带这五个中文字段;不要把日志 ID 作为主列;已读状态字段缺失则不展示,不要编造
5. 用户要看正文时再执行 `report detail --report-id --format json`;用户要统计 / 已读情况时执行 `report stats --report-id --format json`
**不要把 list/sent 当正文接口**;查询正文必须补 `detail` | +| query-report | **0. 前置判定**:query 含「查日志 / 看日志 / 我发过的日志 / 收到的日志 / 日志详情」且语义指向钉钉日志 OA 应用?是 → 直接走 `dws report`;否 → 先按 doc/report 分歧澄清
1. 用户说「我发过 / 我创建」→ `report outbox list --cursor 0 --size 20 --format json`;用户说「收到 / 别人发给我」→ `report inbox list --start "" --end "" --cursor 0 --size 20 --format json`
2. 时间 flag 只允许 `--start` / `--end`,禁止 `--start-date` / `--end-date` / `--date`;不要只传裸日期,必须展开完整 ISO;不要先查 `help`,不要预先登录;只有命令返回认证错误时才处理认证
3. 从列表返回中取 `reportId`(在 `_internalDetailCommands[].command` 里)留给内部后续调用;如果用户已直接提供 `reportId`,跳过列表
4. 面向用户展示列表时直接原样输出返回里的 `agentDisplayMarkdown`(表头 `日期 | 标题 | 发送人 | 状态 | 钉钉链接`),或基于 `result[]` 的五个中文字段拼表;不要把日志 ID 作为主列;已读状态字段缺失则不展示,不要编造
5. 用户要看正文时再执行 `report entry get --report-id --format json`;用户要统计 / 已读情况时执行 `report entry stats --report-id --format json`
**不要把 list 当正文接口**;查询正文必须补 `entry get` | | generate-daily-report | 1. 按「多源并行采集」(见 `dws-shared/references/best_practices/_common/conventions.md`)执行(时间=今日)
2. 交叉汇总并把日报内容写入临时文件 `.md`(UTF-8,真实换行)
3. **创建文档**:`doc create --name "<日报名>" --content-file .md`(> 200KB 按 `dingtalk-doc/references/04-document.md` 的 write-doc 兜底走 create 空 → 循环 update) | | generate-weekly-report | 1. 按「多源并行采集」(见 `dws-shared/references/best_practices/_common/conventions.md`)执行(时间=本周)
2. 交叉对比并把周报内容写入临时文件 `.md`
3. **创建文档**:`doc create --name "<周报名>" --content-file .md`(兜底同上) | -| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)
1. 按「多源并行采集」(见 `dws-shared/references/best_practices/_common/conventions.md`)执行(时间=当日)
2. `report template list --format json` → 取 `report_template_id`
3. `report template detail --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`
4. **把 contents 写入临时文件**(避免 shell 引号问题):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`
5. `report create --template-id --contents '[...]' (或 echo "$json" | ... --contents -) --format json` → CLI 会在创建成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接
6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 create 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report detail --report-id --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`
**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 create;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link | -| generate-monthly-report | 1. 按「多源并行采集」(见 `dws-shared/references/best_practices/_common/conventions.md`)执行(时间=当月)
2. `report sent --start "<月初ISO>" --end "<月末ISO>"` → 取当月已提交日志
3. 按周分段归纳并把月报内容写入临时文件 `.md`
4. **创建文档**:`doc create --name "<月报名>" --content-file .md`(兜底同上) | +| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)
1. 按「多源并行采集」(见 `dws-shared/references/best_practices/_common/conventions.md`)执行(时间=当日)
2. `report template list --format json` → 取 `report_template_id`(顶层 `items[]` 下,无 `result` 包裹)
3. `report template get --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`
4. **把 contents 写入当前工作目录下的临时文件**(避免 shell 引号问题;路径必须在 CWD 之下,不能用 /tmp):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`
5. `report entry submit --template-id --contents-file (或 echo "$json" | ... --contents -)--format json` → CLI 会在提交成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接
6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 submit 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report entry get --report-id --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`
**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 submit;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link | +| generate-monthly-report | 1. 按「多源并行采集」(见 `dws-shared/references/best_practices/_common/conventions.md`)执行(时间=当月)
2. `report outbox list --start "<月初ISO>" --end "<月末ISO>" --cursor 0 --size 20 --format json`(单次跨度 ≤ 20 天,跨月需分窗滚动)→ 取当月已提交日志
3. 按周分段归纳并把月报内容写入临时文件 `.md`
4. **创建文档**:`doc create --name "<月报名>" --content-file .md`(兜底同上) | | generate-topic-report | 1. 提取主题关键词;推断时间范围("最近"默认近 30 天)
2. 按「多源并行采集」(见 `dws-shared/references/best_practices/_common/conventions.md`)执行
3. 按时间线排列,交叉归纳核心结论/决策/行动项/未解决问题/演进脉络,并把内容写入临时文件 `.md`
4. **创建文档**:`doc create --name "<报告名>" --content-file .md`(兜底同上) | diff --git a/skills/multi/dingtalk-report/references/report.md b/skills/multi/dingtalk-report/references/report.md index a1edb8ab..c4c6e37a 100644 --- a/skills/multi/dingtalk-report/references/report.md +++ b/skills/multi/dingtalk-report/references/report.md @@ -343,8 +343,8 @@ dws report outbox list --cursor 0 --size 20 --format json | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `template list` | template 名称(result.items[].report_template_name) | `template get` 的 --name | -| `template list` | `report_template_id` | `entry submit` 的 --template-id | +| `template list` | template 名称(`items[].report_template_name`;顶层键只有 `items` 和 `success`,**无 `result` 包裹**) | `template get` 的 --name | +| `template list` | `items[].report_template_id` | `entry submit` 的 --template-id | | `template get` | `result.report_template_fields[].field_name` / `field_sort` / `field_type` | 拼 `entry submit` 的 --contents JSON(按下表映射)| | `entry submit` | `reportId`、`dingtalkOpenMarkdownLink`、`dingtalkOpenUrl`(CLI 自动反查详情后追加)| final reply 优先直接使用 `dingtalkOpenMarkdownLink`;需要结构化展示时用 `dingtalkOpenLink.title` + `dingtalkOpenLink.url` | | `inbox list` / `outbox list` | `reportId` | `entry get` / `entry stats` 的 --report-id | @@ -376,11 +376,11 @@ dws report outbox list --cursor 0 --size 20 --format json | Code | ExitCode | 真实含义 | 建议动作 | |------|---------|---------|---------| | `INPUT_INVALID_JSON` | 3 | `--contents` 或 `--contents-file` 内容非合法 JSON | 检查 JSON 数组结构,每项必须是 object,含 `key`/`sort`/`content`/`contentType`/`type` 五个字段 | -| `INPUT_FILE_NOT_FOUND` | 3 | `--contents-file` 路径不存在 / sandbox OS 风格不匹配(macOS 路径在 Windows 沙箱)| 先确认 sandbox OS 与路径风格;改写到 `os.tmpdir()` 等可移植目录 | +| `INPUT_FILE_NOT_FOUND` | 3 | `--contents-file` 指向的文件不存在(路径必须在当前工作目录 CWD 之下)| 确认文件存在且位于 CWD 内;把 contents JSON 写到 CWD 下的相对路径再传 | +| `INPUT_INVALID_PATH` | 3 | `--contents-file` 路径越出当前工作目录(如 `/tmp`、`os.tmpdir()`、`../` 或指向目录外的符号链接)| **不要**用 `/tmp` / `os.tmpdir()` / `../`;把 contents JSON 写到当前工作目录之下再传 | | `INPUT_MISSING_PARAM` | 3 | `--template-id` / `--contents` 必填缺失 | 显式传值;从 `template list` 取合法 templateId | | `INPUT_TOO_LARGE` | 3 | contents JSON 超过 10MB 限制 | **不支持分批次提交**。需精简内容或拆分为多个独立日志分别提交 | -| `MCP_TOOL_ERROR` | 1 | 服务端业务错(含 `server_error_code: PARAM_ERROR`,覆盖 templateId 错 / 字段名错 / 字段值错 / contents 空等多种形态)| 查看 `server_error_code` / `technical_detail`;服务端不区分具体子错因,按提交链路重新走 `template list → template get → entry submit`;连续 ≥ 2 次仍失败必须停止重试,降级 final_reply | -| `RESOURCE_NOT_FOUND` | 1 | reportId / templateId 在服务端找不到 | 用 `list` 或 `template list` 重新获取 | +| `MCP_TOOL_ERROR` / `[UNCLASSIFIED] business error` | 1 | 服务端业务错,`success=false`、`code=1`。覆盖:submit 时 templateId 错 / 字段名错 / 字段值错 / contents 空(`server_error_code: PARAM_ERROR`);`entry get` / `entry stats` 传了不存在的 reportId(`server_error_code: BUSINESS_ERROR`,technical_detail 含 `report not exist`);`template get` 传了不存在的模版名(`server_error_code: PARAM_ERROR`)| 查看 `server_error_code` / `technical_detail`;reportId / 模版名找不到时先用 `inbox list` / `outbox list` / `template list` 重新取合法值,不要猜;submit 类错误按链路重走 `template list → template get → entry submit`,连续 ≥ 2 次仍失败必须停止重试,降级 final_reply | ## 何时停止重试 diff --git a/skills/multi/dingtalk-report/scripts/report_received_today.py b/skills/multi/dingtalk-report/scripts/report_received_today.py index a08e8777..7fd87617 100644 --- a/skills/multi/dingtalk-report/scripts/report_received_today.py +++ b/skills/multi/dingtalk-report/scripts/report_received_today.py @@ -2,9 +2,14 @@ """ 查看今天收到的日志列表及详情 +基于 `dws report inbox list` + `dws report entry get`(旧的 report list / report detail +已废弃)。inbox list 返回的 result[] 使用中文展示键:日期 / 标题 / 发送人 / 状态 / 钉钉链接; +reportId 不在 result[] 里,只在 _internalDetailCommands[].command 中,按页与 result[] 同序对应。 + 用法: python report_received_today.py - python report_received_today.py --days 3 # 最近 3 天 + python report_received_today.py --days 3 # 最近 3 天 + python report_received_today.py --detail # 额外拉取每条正文 (entry get) python report_received_today.py --dry-run """ @@ -13,7 +18,7 @@ import json import subprocess import argparse from datetime import datetime, timedelta -from typing import List, Any, Optional +from typing import List, Any, Optional, Tuple def run_dws( @@ -37,91 +42,118 @@ def run_dws( return None -def to_iso(dt: datetime) -> str: - return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00') +def iso_start(dt: datetime) -> str: + return dt.strftime('%Y-%m-%dT00:00:00+08:00') + + +def iso_end(dt: datetime) -> str: + return dt.strftime('%Y-%m-%dT23:59:59+08:00') + + +def report_id_from_command(cmd: str) -> str: + parts = cmd.split() + if '--report-id' in parts: + i = parts.index('--report-id') + if i + 1 < len(parts): + return parts[i + 1] + return '' + + +def fetch_inbox( + start: str, end: str, dry_run: bool, +) -> List[Tuple[dict, str]]: + """按 cursor 翻页拉全 inbox;返回 (result_item, reportId) 列表。""" + pairs: List[Tuple[dict, str]] = [] + cursor = 0 + while True: + data = run_dws([ + 'report', 'inbox', 'list', + '--start', start, + '--end', end, + '--cursor', str(cursor), + '--size', '20', + '--format', 'json', + ], dry_run=dry_run) + if dry_run or not isinstance(data, dict): + return pairs + items = data.get('result') or [] + cmds = data.get('_internalDetailCommands') or [] + for idx, item in enumerate(items): + rid = '' + if idx < len(cmds): + rid = report_id_from_command( + cmds[idx].get('command', '') + ) + pairs.append((item, rid)) + if data.get('hasMore') and data.get('nextCursor') is not None: + cursor = data['nextCursor'] + else: + break + return pairs + + +def print_detail(rid: str) -> None: + detail = run_dws([ + 'report', 'entry', 'get', + '--report-id', rid, '--format', 'json', + ]) + if not isinstance(detail, dict): + return + result = detail.get('result') + if not isinstance(result, dict): + return + for c in (result.get('report_content') or [])[:3]: + key = c.get('key', '') + val = c.get('value', '') + if key and val: + print(f" {key}: {str(val).strip()[:60]}") def main(): - parser = argparse.ArgumentParser( - description='查看收到的日志' + parser = argparse.ArgumentParser(description='查看收到的日志') + parser.add_argument( + '--days', type=int, default=1, help='查询天数 (默认 1,即今天)' ) parser.add_argument( - '--days', type=int, default=1, help='查询天数 (默认 1)' + '--detail', action='store_true', help='额外拉取每条正文' ) parser.add_argument('--dry-run', action='store_true') args = parser.parse_args() now = datetime.now() - start = now - timedelta(days=args.days) - start = start.replace(hour=0, minute=0, second=0) + start_dt = now - timedelta(days=max(args.days - 1, 0)) + start = iso_start(start_dt) + end = iso_end(now) label = '今天' if args.days == 1 else f'最近 {args.days} 天' - print(f'📓 查看{label}收到的日志...\n') - - data = run_dws([ - 'report', 'list', - '--start', to_iso(start), - '--end', to_iso(now), - '--cursor', '0', - '--size', '20', - '--format', 'json', - ], dry_run=args.dry_run) + print(f'查看{label}收到的日志...\n') + pairs = fetch_inbox(start, end, args.dry_run) if args.dry_run: return - if not data: - print('未查到日志') + if not pairs: + print(' 暂无收到的日志') return - if isinstance(data, list): - reports = data - elif isinstance(data, dict): - inner = data.get('result', data) - if isinstance(inner, dict): - reports = inner.get('report_list', - inner.get('reports', [])) - elif isinstance(inner, list): - reports = inner - else: - reports = [] - else: - reports = [] - if not reports: - print(' ✅ 暂无收到的日志') - return - - print(f"📓 {label}日志 ({len(reports)} 条)") + print(f"{label}日志 ({len(pairs)} 条)") print('=' * 50) - for r in reports: - if not isinstance(r, dict): - print(f"\n 📝 {r}") - continue - rid = r.get('reportId') or r.get('id', '') - creator = r.get('creatorName') or r.get('creator', '未知') - template = r.get('templateName') or r.get('template', '') - create_time = r.get('createTime', '') - if isinstance(create_time, (int, float)): - create_time = datetime.fromtimestamp( - create_time / 1000 - ).strftime('%Y-%m-%d %H:%M') + for item, rid in pairs: + title = item.get('标题') or '日志' + sender = item.get('发送人') or '未知' + date = item.get('日期') or '' + status = item.get('状态') or '' + link = item.get('钉钉链接') or '' - print(f"\n 📝 {template or '日志'} - {creator}") - print(f" 时间: {create_time}") - print(f" ID: {rid}") + print(f"\n {title} - {sender}") + print(f" 时间: {date}") + if status: + print(f" 状态: {status}") + if link: + print(f" 链接: {link}") - if rid: - detail = run_dws([ - 'report', 'detail', - '--report-id', rid, '--format', 'json', - ]) - if detail and isinstance(detail, dict): - contents = detail.get('contents', []) - for c in contents[:3]: - key = c.get('key') or c.get('title', '') - val = c.get('value') or c.get('content', '') - if key and val: - print(f" {key}: {str(val)[:60]}") + if args.detail and rid: + print_detail(rid) if __name__ == '__main__': diff --git a/skills/multi/dingtalk-sheet/SKILL.md b/skills/multi/dingtalk-sheet/SKILL.md index 95980afc..2aad7a62 100644 --- a/skills/multi/dingtalk-sheet/SKILL.md +++ b/skills/multi/dingtalk-sheet/SKILL.md @@ -37,7 +37,7 @@ metadata: | "写入单元格" | `dws sheet range update --node --sheet-id --range A1:B2 --values '[[..]]'` | | "追加一行" | `dws sheet append --node --sheet-id --values '[[..]]'` | | "查找 / 替换" | `dws sheet find --node --sheet-id --find "<关键词>"` / `dws sheet replace --node --sheet-id --find "<旧值>" --replacement "<新值>"` | -| "插入图片到单元格" | `dws sheet write-image --node --sheet-id --range A1 --resource-id `(或 `--resource-url `) | +| "插入图片到单元格" | `dws sheet write-image --node --sheet-id --range A1:A1 --file <本地图片路径>`(CLI 自动上传本地图片并写入单元格;没有 --resource-id/--resource-url 这两个 flag) | ## URL 与 ID 前置 diff --git a/skills/multi/dingtalk-sheet/references/sheet.md b/skills/multi/dingtalk-sheet/references/sheet.md index edc0bdb3..567c4278 100644 --- a/skills/multi/dingtalk-sheet/references/sheet.md +++ b/skills/multi/dingtalk-sheet/references/sheet.md @@ -83,11 +83,20 @@ dws sheet filter-view --help | `sheet copy` | 复制工作表 | | `sheet range read` | 读取工作表数据(别名: range get) | | `sheet range update` | 更新指定区域内容(值/公式/超链接) | +| `sheet range clear` | 清除区域(值/格式/全部) | +| `sheet range sort` | 对区域排序 | +| `sheet range fill` | 自动填充区域 | +| `sheet range copy-to` | 复制区域到目标位置 | +| `sheet range move-to` | 移动区域到目标位置 | +| `sheet range batch-clear` | 批量清除多个区域(原子事务) | +| `sheet batch-update` | 批量执行多个写操作(原子事务) | +| `sheet csv-get` | 以 CSV 格式读取区域数据 | | `sheet range set-style` | 设置单元格样式 | | `sheet range batch-set-style` | 按配置文件批量设置样式 | | `sheet find` | 搜索单元格内容 | | `sheet append` | 在末尾追加数据行 | | `sheet csv-put` | 将 CSV 数据写入指定位置(纯值,自动扩容) | +| `sheet delete-sheet` | 删除工作表(不可逆,删除前必须确认) | | `sheet replace` | 全局查找替换文本 | | `sheet merge-cells` | 合并单元格 | | `sheet unmerge-cells` | 取消合并单元格 | @@ -122,9 +131,44 @@ dws sheet filter-view --help | `sheet filter-view info` | 获取单个筛选视图详情 | | `sheet filter-view list-criteria` | 列出筛选视图所有列条件 | | `sheet filter-view get-criteria` | 获取单列筛选条件详情 | +| `sheet cond-format list` | 获取条件格式规则 | +| `sheet cond-format create` | 创建条件格式规则 | +| `sheet cond-format update` | 更新条件格式规则 | +| `sheet cond-format delete` | 删除条件格式规则 | +| `sheet chart list` | 获取浮动图表 | +| `sheet chart create` | 创建浮动图表 | +| `sheet chart update` | 更新浮动图表 | +| `sheet chart delete` | 删除浮动图表 | +| `sheet template list` | 获取表格模板列表 | +| `sheet template search` | 搜索表格模板 | +| `sheet template apply` | 应用模板创建新表格文档 | > 不确定参数?对任意命令执行 `dws sheet <命令> --help` 查看完整用法。 +## 子文档索引(更多命令详细参考) + +本文档覆盖高频命令。以下命令的完整参数、示例与工作流在 `sheet/` 子目录,需要时按主题查阅: + +| 主题 | 子文档 | 覆盖命令 | +|------|--------|---------| +| 表格与工作表管理 | [sheet/sheet-workbook.md](./sheet/sheet-workbook.md) | create / list / info / new / update / copy / delete-sheet | +| 写入数据 | [sheet/sheet-write-data.md](./sheet/sheet-write-data.md) | range update(对象协议详解)/ append / csv-put | +| 读取数据 | [sheet/sheet-read-data.md](./sheet/sheet-read-data.md) | range read / csv-get | +| 区域操作 | [sheet/sheet-range-operations.md](./sheet/sheet-range-operations.md) | range clear / sort / fill / copy-to / move-to | +| 批量操作 | [sheet/sheet-batch-operations.md](./sheet/sheet-batch-operations.md) | range batch-clear / batch-update | +| 行列操作 | [sheet/sheet-dimension-operations.md](./sheet/sheet-dimension-operations.md) | insert / delete / update / move / add-dimension | +| 样式与合并 | [sheet/sheet-style-format.md](./sheet/sheet-style-format.md) | range set-style / batch-set-style / merge-cells / unmerge-cells | +| 条件格式 | [sheet/sheet-conditional-format.md](./sheet/sheet-conditional-format.md) | cond-format list / create / update / delete | +| 浮动图表 | [sheet/sheet-chart.md](./sheet/sheet-chart.md) | chart list / create / update / delete | +| 下拉列表 | [sheet/sheet-dropdown.md](./sheet/sheet-dropdown.md) | set / get / delete-dropdown | +| 媒体与图片 | [sheet/sheet-media-image.md](./sheet/sheet-media-image.md) | media-upload / write-image / 浮动图片 | +| 查找替换 | [sheet/sheet-search-replace.md](./sheet/sheet-search-replace.md) | find / replace | +| 全局筛选 | [sheet/sheet-filter.md](./sheet/sheet-filter.md) | filter get / create / delete / update / clear-criteria / sort | +| 筛选视图 | [sheet/sheet-filter-view.md](./sheet/sheet-filter-view.md) | filter-view 全系列 | +| 导出 | [sheet/sheet-export.md](./sheet/sheet-export.md) | export | + +> `template list/search/apply` 无独立子文档,直接执行 `dws sheet template <子命令> --help` 查看用法。 + ## 意图判断 ### 表格与工作表管理 @@ -194,7 +238,7 @@ dws sheet filter-view --help 用户说"删除行/删除列/删掉第几行/删掉某列/移除行/移除列": - 删除行或列 → `delete-dimension` -- 仅清空内容但保留行/列 → `range update --values` 写入空字符串 `""` +- 仅清空内容但保留行/列 → `range clear`(整片区域清除,比逐格写空更简洁) 用户说"隐藏行/隐藏列/显示行/显示列/设置行高/设置列宽/调整行高/调整列宽/行列属性": - 隐藏/显示行或列 → `update-dimension --hidden` / `--hidden=false` @@ -340,8 +384,9 @@ dws sheet filter-view --help 以下是最容易出错的规则,**必须严格遵守**: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) -- ★ **`range update` 维度校验(强制)**:`--values` / `--hyperlinks` 的行列数必须与 `--range` 完全一致。例如 `--range "A1:C3"` → `--values` 必须是 3×3 数组 -- ★ **`range update` 清空规范(强制)**:清空单元格用空字符串 `""`,禁止用 `null`(全 null 会被跳过无效果) +- ★ **`range update` 单元格协议(强制)**:`--values` 是二维 JSON 数组,**每个单元格必须是 object**(如 `{"type":"text","text":"张三"}`),不再支持裸值 `"张三"` / `90` / `null`。裸值会报错「不支持原始值……每个单元格必须是 object」。数字/布尔也写成字符串 object(如 `{"type":"text","text":"90"}`),服务端自动识别类型。超链接写在单元格 object 的 `hyperlink` 字段,**没有 `--hyperlinks` flag**(传了报 unknown flag) +- ★ **`range update` 维度校验(强制)**:`--values` 的行列数必须与 `--range` 完全一致。例如 `--range "A1:C3"` → `--values` 必须是 3×3 的 object 数组 +- ★ **`range update` 清空规范(强制)**:清空单个单元格用 `{"type":"text","text":""}`;清空整片区域用 `range clear`。跳过某格保留原值用 `{}` 空对象 - ★ **单次调用上限(强制)**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) - ★ **大批量纯值写入用 `csv-put` 不用 `range update`**:当写入纯值(无公式/超链接)且数据量较大时(>5 行或 >20 单元格),必须使用 `csv-put`。`csv-put` 接受 CSV 文本直接写入,无需构造二维 JSON 数组,支持自动扩容,更简洁高效。仅在需要写入公式、超链接、或仅更新少量单元格时才使用 `range update` - ★ **搜索用 `find` 不用 `range read`**:`find` 是服务端搜索,禁止用 `range read` 全量读取后客户端过滤 @@ -432,15 +477,17 @@ Example: Flags: --node string 表格文档 ID 或 URL (必填) --sheet-id string 工作表 ID 或名称 (必填) - --title string 新标题,最长 100 字符,不能包含 / \ ? * [ ] : + --name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] : + --title string --name 的别名(兼容) --index int 新位置(从 0 开始) --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 + --tab-color string 工作表标签颜色,Hex 如 #FF0000;传空字符串清除颜色 --frozen-row-count int 冻结行数,0 表示取消冻结 --frozen-column-count int 冻结列数,0 表示取消冻结 ``` -更新工作表标题、位置、隐藏状态、冻结行列。 -`--title` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 +更新工作表名称、位置、隐藏状态、标签颜色、冻结行列。 +`--name`(别名 `--title`)/ `--index` / `--hidden` / `--tab-color` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 注意: - 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 @@ -483,14 +530,19 @@ Example: # 使用 get 别名,与 read 等价 dws sheet range get --node --sheet-id --range "A1:D10" + + # 指定取值模式:原始值 / 公式文本 + dws sheet range read --node --value-render-option raw_value + dws sheet range read --node --value-render-option formula Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) - --range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据) + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) + --range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据) + --value-render-option string 取值模式: formatted_value(格式化展示值,默认) | raw_value(原始值) | formula(公式文本,无公式回退原始值) ``` **超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: -- 先通过 `info` 获取 `rowCount` / `lastNonEmptyRow` / `columnCount` 确定数据边界 +- 先通过 `info` 获取 `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` 确定数据边界(空表时这些字段为 null) - 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… - 避免不传 `--range` 直接读取整个大工作表 @@ -499,34 +551,50 @@ Flags: Usage: dws sheet range update [flags] Example: - # 写入值 + # 写入文本(每个单元格必须是 object) dws sheet range update --node --sheet-id --range "A1:B2" \ - --values '[["姓名","分数"],["张三",90]]' + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"分数"}],[{"type":"text","text":"张三"},{"type":"text","text":"90"}]]' - # 写入公式 + # 写入公式(text 以 = 开头识别为公式) dws sheet range update --node --sheet-id --range "C2" \ - --values '[["=A2&B2"]]' + --values '[[{"type":"text","text":"=A2&B2"}]]' - # 写入超链接 + # 写入单元格级超链接(写在 cell 的 hyperlink 字段,没有 --hyperlinks flag) dws sheet range update --node --sheet-id --range "A1" \ - --hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"钉钉"}]]' + --values '[[{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' - # 清空区域(使用空字符串 "") - dws sheet range update --node --sheet-id --range "A1:B3" \ - --values '[["",""],["",""],["",""]]' + # 只更新部分单元格:用 {} 空对象占位保留原值 + dws sheet range update --node --sheet-id --range "A1:B1" \ + --values '[[{"type":"text","text":"新值"},{}]]' + + # 清空单个单元格(text 为空字符串) + dws sheet range update --node --sheet-id --range "A1" \ + --values '[[{"type":"text","text":""}]]' + + # 清空整片区域请改用 range clear + dws sheet range clear --node --sheet-id --range "A1:B3" Flags: --node string 表格文档 ID (必填) --sheet-id string 工作表 ID 或名称 (必填) --range string 目标单元格区域地址,如 A1:B3 (必填) - --values string 单元格值,二维 JSON 数组 (与 --hyperlinks 至少传一项) - --hyperlinks string 超链接,二维 JSON 数组 (与 --values 至少传一项) + --values string 单元格内容,二维 JSON 数组 (必填);每个元素必须是 object ``` +**单元格对象协议(重要)**:`--values` 每个元素必须是 object,支持以下形态: +- `{"type":"text","text":"内容"}`:普通文本;`text` 以 `=` 开头识别为公式;`text` 为 `""` 清空该格;数字/布尔写成字符串(`{"type":"text","text":"100"}`),服务端自动识别类型 +- `{"type":"text","text":"内容","cellStyles":{...}}`:文本 + 整格样式(`fontWeight`/`fontColor`/`backgroundColor`/`numberFormat` 等) +- `{"type":"richText","texts":[...]}`:富文本,子项可为 text/link/attachment/image,片段样式写在子项 `style` +- `{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://..."}}`:整格超链接(`type` 可为 `path`/`sheet`/`range`,或 `{"type":"none"}` 清除) +- `{"dataValidation":{...}}`:写下拉/复选框数据验证 +- `{}`:空对象,跳过该格保留原值(只改部分单元格时占位用) + +不再支持裸值(`"张三"` / `90` / `null`),原样传入会报「不支持原始值……每个单元格必须是 object」。 + **单次调用建议**:行数 ≤ 1000,单元格总数(行×列)≤ 5000;超过时请拆分多次调用。 -**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式和超链接),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装二维 JSON 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、超链接(`--hyperlinks`)、或修改少量单元格时才使用 `range update`。 +**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式和超链接),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装 object 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、超链接、富文本、或修改少量单元格时才使用 `range update`。 -**范围职责**:`range update` 仅负责写入单元格的值与超链接,不接受任何样式参数。如需设置数字格式(百分比 / 货币 / 日期 / 文本等)请使用 `dws sheet range set-style --number-format <格式代码>`,可与其他样式参数同时传入。 +**范围职责**:`range update` 负责写入单元格的值、超链接、per-cell 样式与数据验证。批量刷整片区域的统一样式或数字格式(百分比 / 货币 / 日期 / 文本等)请使用 `dws sheet range set-style --number-format <格式代码>`。 ### 设置单元格样式 ``` @@ -662,9 +730,11 @@ Flags: Usage: dws sheet csv-put [flags] Example: + # 内联多行 CSV:必须用 $'...' 让 \n 变成真换行;普通单引号 '...\n...' 里的 \n 是字面量,会写成一行含字面 \n 的错误数据 dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv 'name,score\nAlice,95\nBob,87' + --csv $'name,score\nAlice,95\nBob,87' + # 多行数据推荐用 @文件,最稳妥 dws sheet csv-put --node --sheet-id --start-cell B2 \ --csv @data.csv --allow-overwrite @@ -750,7 +820,7 @@ Flags: 在钉钉表格指定工作表中,从指定位置起删除若干连续的行或列。 `--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`。 +删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range clear`(整片区域清除,比逐格写空更简洁)。 ### 更新指定范围行/列属性 ``` @@ -832,6 +902,8 @@ Flags: --mime-type string 文件 MIME 类型 (默认根据扩展名推断) ``` +`--format json` 输出规整 JSON:`{success, resourceId, resourceUrl, fileName, fileSize}`。`resourceUrl` 可用于 `create-float-image` 的 `--src`。 + ### 上传图片并写入表格单元格 ``` Usage: @@ -1083,7 +1155,8 @@ Flags: 查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 - **用途**:查看单元格已设置的下拉列表选项和配置。 - **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 +- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `multipleValues` 和 `colorValueMap`)。**判空以 `hasDropdown` 为准**:无下拉列表时 `hasDropdown` 为 false,但 `dataValidations` 里仍会带 1 个全 null 的幽灵条目,不要用数组长度判空。 +- **已知限制**:`options.multipleValues` 服务端恒返回 `null`(即使该下拉是用 `--multi-select` 建的),且不返回 `enableMultiSelect` 字段——**`get-dropdown` 读回无法判断下拉是否多选**。要判断是否多选,改用 `range read`:其 `cells[].dataValidation.enableMultiSelect` 字段准确(实测有效)。 ### 删除下拉列表 ``` @@ -1118,7 +1191,7 @@ Flags: - **用途**:查看当前工作表上是否存在全局筛选及其配置。 - **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 - **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 -- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。 +- **返回**:`range`(筛选范围,A1 表示法)、`id`(筛选 ID)和 `criteria`(各列条件对象,key 为列偏移量;无条件时为 `{}`)。如果未设置筛选,返回筛选信息为空。 ### 创建筛选 ``` @@ -1499,10 +1572,10 @@ Flags: - 第 21~30 次:每次间隔 15 秒 - **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 -**命令返回**: -- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载) -- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: ` -- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下 +**命令返回**(`--format json`,默认):输出规整 JSON `{success, jobId, downloadUrl[, outputPath]}`。轮询进度以 `[INFO] [N/30] 状态: ...` 打到 stderr,不污染 stdout 的 JSON。 +- `--output` 未指定:JSON 含 `jobId` + `downloadUrl`(链接有时效性,请尽快下载) +- `--output` 指定为文件路径:下载到该路径,JSON 额外含 `outputPath` +- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录,JSON 含 `outputPath` **失败处理(命令内部已处理,Agent 仅需转述)**: - MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 @@ -1510,6 +1583,29 @@ Flags: **限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 +### 表格模板(list / search / apply) +``` +Usage: + dws sheet template list [flags] # 列出可用模板(--cursor / --limit 分页) + dws sheet template search [flags] # 按关键词搜索模板 + dws sheet template apply [flags] # 应用模板创建新表格文档 + +template search Flags: + --query string 搜索关键词 (必填) + --source string 模板来源: MY(我的模版,默认) / PUBLIC(公开模版) + --cursor string 分页游标 + --limit int 返回数量上限 + +template apply Flags: + --template-id string 模板 ID (必填,来自 list/search) + --name string 新表格文档名称 (可选) + --folder string 目标文件夹 ID (可选,UUID 或 URL) + --workspace string 知识库 ID (可选) +``` + +- 用户说"用模板建表 / 有哪些模板 / 找个 XX 模板"时走 template 系列 +- 典型链路:`template search --query "..."` 或 `template list` 拿到 `templateId` → `template apply --template-id --name "..."` 创建新表格,返回新文档的 `nodeId` + ## 核心工作流 ```bash @@ -1521,12 +1617,12 @@ dws sheet create --name "销售数据" --format json # 2. 查看工作表列表 — 提取 sheetId dws sheet list --node --format json -# 3. 写入表头和数据 +# 3. 写入表头和数据(每个单元格必须是 object;数字也写成字符串) dws sheet range update --node --sheet-id --range "A1:C1" \ - --values '[["姓名","部门","销售额"]]' --format json + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json dws sheet range update --node --sheet-id --range "A2:C4" \ - --values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json + --values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json # ── 工作流 2: 读取已有表格数据 ── @@ -1547,26 +1643,26 @@ dws sheet range read --node --sheet-id --range "A1:D10" --f # 1. 新建工作表 dws sheet new --node --name "汇总" --format json -# 2. 在新工作表中写入汇总公式 +# 2. 在新工作表中写入汇总公式(公式写在 text,以 = 开头) dws sheet range update --node --sheet-id --range "A1:B1" \ - --values '[["指标","数值"]]' --format json + --values '[[{"type":"text","text":"指标"},{"type":"text","text":"数值"}]]' --format json dws sheet range update --node --sheet-id --range "A2:B2" \ - --values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json + --values '[[{"type":"text","text":"总销售额"},{"type":"text","text":"=SUM(Sheet1!C2:C100)"}]]' --format json # ── 工作流 4: 写入数据并设置样式 ── # 1. 写入数据 dws sheet range update --node --sheet-id --range "A1:C3" \ - --values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json + --values '[[{"type":"text","text":"商品"},{"type":"text","text":"单价"},{"type":"text","text":"数量"}],[{"type":"text","text":"苹果"},{"type":"text","text":"5.5"},{"type":"text","text":"100"}],[{"type":"text","text":"香蕉"},{"type":"text","text":"3.2"},{"type":"text","text":"200"}]]' --format json -# 2. 设置数字格式(人民币)——请走 set-style,不要放到 range update +# 2. 设置数字格式(人民币)——批量刷整列走 set-style dws sheet range set-style --node --sheet-id --range "B2:B3" \ --number-format "¥#,##0.00" --format json -# 3. 写入超链接 +# 3. 写入超链接(写在 cell 的 hyperlink 字段) dws sheet range update --node --sheet-id --range "D1" \ - --hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"详情"}]]' --format json + --values '[[{"type":"text","text":"详情","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' --format json # ── 工作流 5: 追加数据 ── @@ -1716,8 +1812,8 @@ dws sheet write-image --node --sheet-id --range C3:C3 --fil # 4. 完整流程: 创建表格 → 写表头 → 写入图片 dws sheet create --name "产品目录" -f json # 提取 nodeId 后: -dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json -dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[[{"type":"text","text":"产品名称"},{"type":"text","text":"产品图片"}]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[[{"type":"text","text":"MacBook Pro"}]]' -f json dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json ``` @@ -1789,7 +1885,7 @@ dws sheet export --node --output ./ | `create` | `nodeId` | list / info / new / range read / range update / find 的 --node | | `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | | `new` | 新工作表的 `sheetId` | range read / range update / find 的 --sheet-id | -| `info` | `rowCount` / `lastNonEmptyRow` | 确定数据范围、追加写入起始行 | +| `info` | `rowCount` / `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` / `mergedRanges` | 确定数据范围、追加写入起始行、判断合并结构(空表时 nonEmptyRange.* 为 null) | | `find` | `matchedCells` 中的 `a1Notation` | 定位目标单元格,用于 range read / range update | | `append` | `a1Notation` 追加数据所在范围 | 确认追加位置 | | `csv-put` | `a1Notation` 实际写入的单元格范围 | 确认写入位置和范围 | @@ -1820,7 +1916,7 @@ dws sheet export --node --output ./ | `filter-view delete-criteria` | `id` 筛选视图 ID | 确认条件清除完成 | | `filter-view list-criteria` | 所有列条件(按列偏移量为 key 的对象) | 了解当前视图已设置哪些列的条件 | | `filter-view get-criteria` | 指定列的条件详情(`filterType`、`conditions` 等) | 查看某列的具体筛选规则 | -| `export` | `downloadUrl`(未指定 --output)/ `导出完成: `(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | +| `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | ## nodeId 多格式说明 @@ -1830,26 +1926,28 @@ dws sheet export --node --output ./ ## values 参数格式说明 -`--values` 为二维 JSON 数组,第一维为行,第二维为列: -- 字符串值: `"文本"` -- 数字值: `100` 或 `3.14` -- 公式: `"=SUM(B2:B4)"`(以 `=` 开头的字符串自动识别为公式) -- 清空单元格: 统一使用空字符串 `""`(不要用 `null` 取代,null 不会保留原值且全 null 会被视为无效调用跳过) +`--values` 为二维 JSON 数组,第一维为行,第二维为列。**每个单元格必须是 object**,不支持裸值 `"文本"` / `100` / `null`: +- 文本: `{"type":"text","text":"文本"}` +- 数字/布尔: 写成字符串 `{"type":"text","text":"100"}`,服务端按内容自动识别为数字/布尔 +- 公式: `{"type":"text","text":"=SUM(B2:B4)"}`(`text` 以 `=` 开头识别为公式) +- 清空单个单元格: `{"type":"text","text":""}`;清空整片区域用 `range clear` +- 跳过某格保留原值: `{}` 空对象 +- 整格样式: `{"type":"text","text":"重要","cellStyles":{"fontWeight":"bold","fontColor":"#FF0000","numberFormat":"@"}}` +- 富文本: `{"type":"richText","texts":[...]}`(子项 text/link/attachment/image,片段样式写在子项 `style`) -维度必须与 `--range` 范围一致,例如 `--range "A1:B3"` 需要 3 行 2 列的数组。 +维度必须与 `--range` 范围一致,例如 `--range "A1:B3"` 需要 3 行 2 列的 object 数组。 -## hyperlinks 参数格式说明 +## 超链接写法说明 -`--hyperlinks` 为二维 JSON 数组,每个元素为对象或 null: -- `type`: 链接类型,可选 `path`(外部链接)、`sheet`(工作表跳转)、`range`(单元格跳转) -- `link`: 链接地址 -- `text`: 显示文本 - -与 `--values` 共存时,hyperlinks 优先级更高。 +**没有 `--hyperlinks` flag**(旧写法已废弃,传入报 unknown flag)。单元格级超链接写在 `--values` 里 cell object 的 `hyperlink` 字段: +- `{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}`:写外部链接 +- `hyperlink.type` 可为 `path`(外部链接)/ `sheet`(工作表跳转,link 为工作表名)/ `range`(区域跳转,link 为 `Sheet2!A1`) +- `{"hyperlink":{"type":"none"}}`:清除整格超链接(保留原值) +- 富文本片段内的链接写在 `richText` 子项的 `link` 字段,与整格 `hyperlink` 不同 ## number-format 常用值 -适用范围:`number-format` 仅在 `range set-style` / `range batch-set-style` 中接受(CLI 对应 `--number-format`,batch 配置文件对应 `numberFormat`);`range update` 不接受该参数。 +适用范围:`number-format` 在 `range set-style` / `range batch-set-style` 中作为 `--number-format` 参数;`range update` 没有 `--number-format` flag,但可在每个 cell object 的 `cellStyles.numberFormat` 里写同样的格式代码。批量刷整片区域优先用 `set-style`。 | 格式代码 | 说明 | 示例 | |----------|------|------| @@ -1867,20 +1965,20 @@ dws sheet export --node --output ./ > 标 ★ 的条目已在前文「关键注意事项」中列出,此处为完整说明。 - ★ `--sheet-id` 获取规范(强制):所有涉及 `--sheet-id` 参数的命令(`info` / `new` / `range read` / `range update` / `find` / `append` / `insert-dimension` / `delete-dimension` / `update-dimension` / `move-dimension` / `add-dimension` / `merge-cells` / `unmerge-cells` / `replace` / `write-image` / `set-dropdown` / `get-dropdown` / `delete-dropdown` / `filter-view *` 等),除非用户主动提供了工作表 ID 或工作表名称,否则在 `sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 -- ★ `range update` 维度校验(强制):调用 `range update` 写入 `--values` 或 `--hyperlinks` 时,必须严格校验二维 JSON 数组的行数与列数与 `--range` 指定的范围完全一致: - - 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 `[[v1,v2,v3],[v4,v5,v6],[v7,v8,v9]]` 这样 3×3 的数组 - - `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[v]]` - - 行数不足需要用空字符串补齐,列数不足需要补齐到每行相同长度;禁止出现各行列数不一致或与 `--range` 不匹配的情况,否则调用会直接报错 - - 同时传入 `--values` 和 `--hyperlinks` 时,两个二维数组的行列数都必须与 `--range` 严格一致 -- ★ `range update` 清空单元格规范(强制):如需清空单元格内容,统一使用空字符串 `""`。禁止使用 `null`:`null` 不会保留单元格原值,也不存在"选择性保留"场景;且若 `--values` 全部为 `null`,整体调用会被视为无效而跳过,无任何写入效果 +- ★ `range update` 单元格对象协议(强制):`--values` 每个单元格必须是 object(`{"type":"text","text":...}` / `{"type":"richText",...}` / `{}` 等),不支持裸值 `"张三"` / `90` / `null`,裸值报「不支持原始值……每个单元格必须是 object」。数字/布尔写成字符串 object,服务端自动识别类型。超链接写在 cell object 的 `hyperlink` 字段,没有 `--hyperlinks` flag +- ★ `range update` 维度校验(强制):`--values` 二维数组的行数与列数必须与 `--range` 完全一致: + - 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 3×3 的 object 数组 + - `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[{...}]]` + - 不改的格用 `{}` 空对象占位补齐;禁止各行列数不一致或与 `--range` 不匹配,否则报错 +- ★ `range update` 清空单元格规范(强制):清空单个单元格用 `{"type":"text","text":""}`(不是裸 `""`,也不是 `null`);清空整片区域用 `range clear`;跳过某格保留原值用 `{}` 空对象 - `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录 - `list` 返回所有工作表的 ID 和名称,是后续操作的必要前置步骤 - `info` 不传 `--sheet-id` 时默认返回第一个工作表的详情 - `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 - `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) - `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 获取边界后分批读取,避免不传 `--range` 直接读取整个大工作表 -- `range update` 的 `--values` 和 `--hyperlinks` 至少传入一项 -- `range update` 职责边界:`range update` 仅写入单元格的值与超链接,不接受任何样式参数(包括但不限于数字格式 / 背景色 / 字体 / 对齐方式等)。如需设置数字格式,请使用 `dws sheet range set-style --number-format <格式代码>`;批量场景走 `dws sheet range batch-set-style --batch `(配置文件中使用 `numberFormat` 字段)。不要在同一次 `range update` 调用里同时完成写值与格式设置 +- `range update` 只有 `--values` 一个内容参数(必填),超链接/样式/数据验证都写进 cell object 内部字段 +- `range update` 职责边界:可写值、超链接、per-cell 样式(`cellStyles`)与数据验证(`dataValidation`)。批量刷整片区域的统一样式或数字格式优先用 `dws sheet range set-style --number-format <格式代码>`;批量多区域走 `dws sheet range batch-set-style --batch `(配置文件用 `numberFormat` 字段) - ★ `range update` / `range set-style` / `range batch-set-style` 单次调用上限(强制):行数 ≤ 1000,单元格总数(行×列)建议≤ 5000(底层硬限 30000);超限请拆分多次调用。CLI 会在调用前做本地预校验,底层超 30000 会直接报错 - `range set-style` / `range batch-set-style` 的样式枚举按驼峰书写:`wordWrap` 取 `overflow`/`clip`/`autoWrap`,`fontWeight` 取 `bold`/`normal`,`hAlign` 取 `left`/`center`/`right`/`general`,`vAlign` 取 `top`/`middle`/`bottom`;背景色/字体颜色统一使用 `#RRGGBB` 格式 - `new` 创建工作表时,如名称与已有工作表重复,系统会自动重命名 @@ -1900,7 +1998,7 @@ dws sheet export --node --output ./ - `delete-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` - `delete-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` - `delete-dimension` 的 `--length` 最大为 5000 -- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`(参见《range update 清空单元格规范》) +- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range clear`(整片区域清除,比逐格写空更简洁) - `update-dimension` 批量更新连续行/列的显隐状态与行高/列宽 - `update-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` - `update-dimension` 的 `--start-index` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` @@ -1939,7 +2037,7 @@ dws sheet export --node --output ./ - `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false - `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 - ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 -- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空 +- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)、`id` 和 `criteria`(各列条件对象,无条件时为 `{}`)。无筛选时返回空 - `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 - `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 - `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-batch-operations.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-batch-operations.md new file mode 100644 index 00000000..ce79f719 --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-batch-operations.md @@ -0,0 +1,246 @@ +# 批量操作 + +## 写入边界 + 回读校验 + +`batch-update` 把多次写入打包成单次请求,但每个子操作仍受编辑类任务硬性默认规则约束: + +1. **目标 range 必须落在用户授权范围内**:除用户明示要修改的区域外,子操作禁止扩张到无关单元格 / 列 / 工作表。规划 range 时先确认每个子操作的边界。 +2. **批次完成后必须回读校验**:整个 `batch-update` 执行成功后,用 `range read` 或 `csv-get` 抽样回读受影响区域,至少校验 3-5 个代表性单元格(首 / 中 / 末),与本地预先计算的预期值对照。 +3. **预期条数前置断言**:涉及"批量填充 N 行"或"对 M 个区域分别写入"时,先把 N、M 硬编码进代码,回读后断言实际等于预期;不一致就再发一轮 `batch-update` 补齐,禁止交付半成品。 + +## 使用场景 + +用户说"批量清除/一次清除多个区域/清空多个 sheet 的数据": +- 批量清除多个区域 → `range batch-clear` +- 每个区域必须带工作表前缀(如 "Sheet1!A1:B3") +- 原子事务:任一区域清除失败则整批回滚 +- 请勿用多次 `range clear` 逐个调用,非原子,中途失败会留下半成品 + +用户说"批量操作/一次执行多个写操作/原子批量/先清除再写入": +- 多个写操作打包 → `batch-update` +- 支持混合操作(清除 + 写入 + 合并 + 调行列尺寸等) +- 原子事务:任一操作失败则整批回滚 +- 传 `--continue-on-error` 可切换为宽松模式(失败继续) + +**何时推荐使用 `batch-update`**: +- 需要对**多个**不同区域执行 `merge-cells` / `unmerge-cells` 时(如按分组合并多列相同内容) +- 需要对**多个**不同区域执行 `update-dimension` 时(如统一调整多列列宽或多行行高,含 hidden + pixelSize) +- 需要先插入行列再写入数据时(`add-dimension` + `range update` / `csv-put`) +- 需要对多个区域执行不同写入操作时(多次 `range update` + `range clear` 等组合) + +当同一工具需要对多个区域重复调用时,**推荐**改用 `batch-update` 合并为单次请求——`batch-update` 是原子提交(要么全成功要么整批回滚);逐个调用非原子,中途失败会留下半成品。 + +**不可放进 `--operations` 的操作**(强行写入会被校验拒或行为未定义): +- `range read` / `csv-get`(只读操作,不属于写入) +- `range sort` / `range move-to`(尚未支持,后续版本补充) +- `write-image` / `media-upload` / `create-float-image` / `update-float-image`(需本地上传或依赖前置上传句柄) +- `set-style` / `range batch-set-style`(自身已是批量入口,不可再嵌套) +- `find` / `replace`(尚未支持,后续版本补充) +- `export`(异步轮询操作) +- `sheet list` / `sheet info`(只读操作) + +这些操作需在 `batch-update` 之外单独调用。 + +## 命令详细参考 + +### 批量清除区域 +``` +Usage: + dws sheet range batch-clear [flags] +Example: + dws sheet range batch-clear --node --ranges '["Sheet1!A1:B3","Sheet2!C1:D5"]' + dws sheet range batch-clear --node --ranges '["Sheet1!A1:Z1000"]' --type all + dws sheet range batch-clear --node --ranges '["Sheet1!A2:A100","Sheet1!C2:C100"]' --type format +Flags: + --node string 表格文档 ID 或 URL (必填) + --ranges string 目标区域 JSON 数组,每项带 sheet 前缀 (必填) + --type string 清除类型: content(仅值,默认) / format(仅格式) / all(全部) +Notes: + - 原子事务:任一区域清除失败则整批回滚 + - 每个 --ranges 项必须包含工作表前缀(格式: "SheetName!A1:B3") + - 不同区域可以属于不同工作表 +``` + +### 批量更新(原子事务) +``` +Usage: + dws sheet batch-update [flags] +Example: + dws sheet batch-update --node --operations '[ + {"toolName":"range clear","input":{"sheet-id":"Sheet1","range":"A1:B3","type":"content"}}, + {"toolName":"range update","input":{"sheet-id":"Sheet1","range":"A1","values":[[{"type":"text","text":"hello"}]]}}, + {"toolName":"merge-cells","input":{"sheet-id":"Sheet1","range":"A1:B1","merge-type":"mergeAll"}}, + {"toolName":"update-dimension","input":{"sheet-id":"Sheet1","dimension":"ROWS","start-index":"1","length":1,"pixel-size":40}} + ]' + dws sheet batch-update --node --continue-on-error --operations '[...]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --operations string 操作数组 JSON (必填,每项 {toolName, input}) + --continue-on-error 遇失败继续执行(默认 false,严格事务) +toolName 用 CLI 命令名,input 的键用 CLI flag 名去掉 --: + range clear / range update / merge-cells / unmerge-cells / update-dimension + range fill / range copy-to / add-dimension / delete-dimension / move-dimension + set-dropdown / delete-dropdown / csv-put / delete-float-image +Notes: + - 默认严格事务模式:任一子操作失败 → 整批回滚到初始状态 + - --continue-on-error: 宽松模式,遇失败继续执行后续操作(已执行的子操作不回滚) + - operations 最多 20 条 + - 当需要对多个区域执行相同清除时,优先使用 `range batch-clear`(更简洁) + - 典型场景:先插入行列再写入数据、先清除再写入、批量合并+调整行高列宽 +``` + +### 子操作定位规则 + +> **`--node` 只在顶层传一次**,每个子操作的 `input` 不含 `node` 字段; +> **子表定位 `sheet-id` 写进各自 `input`**,不同子操作可指向不同工作表。 + +```jsonc +// 正确:node 在顶层,sheet-id 在各自 input +[ + {"toolName":"range clear","input":{"sheet-id":"Sheet1","range":"A1:B3"}}, + {"toolName":"range update","input":{"sheet-id":"Sheet2","range":"A1","values":[[...]]}} // 不同工作表 +] +``` + +## 校验与预览 + +### Validate 阶段 + +CLI 在发送请求前执行以下本地校验(不消耗网络请求): + +| 校验项 | 失败行为 | 示例 | +|--------|---------|------| +| `--node` / `--operations` 必填 | `missing required flag(s)` | 不传 `--node` | +| `--operations` 必须合法 JSON | `--operations JSON 解析失败` | `--operations 'not json'` | +| `--operations` 必须非空数组 | `--operations 不能为空数组` | `--operations '[]'` | +| 每项必须是 object | `operations[N] 不是 object` | `--operations '["string"]'` | +| `toolName` 必须是支持的 CLI 命令名 | `unsupported toolName "xxx"` | 传 `"batch-update"`(禁止嵌套)或拼写错误 | +| 禁止嵌套 `batch-update` | 同上(`batch-update` 不在 dispatch 表中,自动拦截) | `toolName: "batch-update"` | + +> `input` 内的字段(如 `sheet-id`、`range`、`values` 等)由服务端校验,CLI 不提前拦截——传入空值或缺失必填字段时,请求会到达服务端再返回错误。 + +### DryRun 阶段 + +加 `--dry-run` 预览翻译后的完整 MCP 请求,不实际执行任何操作: + +```bash +dws sheet batch-update --node NODE_ID --dry-run --operations '[...]' +``` + +DryRun 输出内容: +- `Tool: batch_update`(最终调用的 MCP 工具名) +- `Arguments:` 完整 JSON(含翻译后的 `operations` 数组,展示每个子操作的 MCP toolName + 参数名 + 参数值) +- 若传了 `--continue-on-error`,输出 JSON 中会包含 `"continueOnError": true` + +DryRun 适用场景: +- 确认 CLI 命令名 → MCP toolName 的翻译是否正确 +- 确认参数名转换(如 `--range` → `rangeAddress`)是否符合预期 +- 检查 `csv-put` 的 `@filepath` 是否被正确解析为文件内容 + +### --continue-on-error 行为 + +| 模式 | flag | 子操作失败时 | 返回格式 | +|------|------|------------|---------| +| 严格事务(默认) | 不传 | 整批回滚到初始状态,后续子操作不执行 | 顶层 `error` 对象 | +| 宽松模式 | `--continue-on-error` | 已执行的子操作不回滚,继续执行后续子操作 | `results` 数组 | + +**严格事务失败的错误信息**由服务端(flex-table-app)生成,包含以下内容: +- 失败操作索引:`operations[N]`(0-based,即 `--operations` JSON 数组中的下标) +- 回滚告知:"All previously executed operations have been rolled back." +- 失败原因:原始错误码和错误消息 + +示例: +``` +Batch update failed at operations[1] (0-based index). +All previously executed operations have been rolled back. +Cause: The requested resource was not found by the identifier 'NonExistentSheet'... +``` + +**宽松模式下**,返回 `results` 数组,逐个标注每个子操作的成功/失败状态: +- 成功的子操作:`success: true` + `message` +- 失败的子操作:`success: false` + `errorCode` + `errorMsg` +- 顶层 `success: true`(batch 本身完成执行,只是部分子操作失败) + +## CLI 命令名 → 原子命令参考 + +`toolName` 使用 CLI 命令名,`input` 的键使用 CLI flag 名(去掉 `--`),与原子命令文档完全一致。各命令的参数详情请查阅对应参考文档或执行 `dws sheet <命令> --help`。 + +| CLI 命令名 | 参考文档 | +|----------|---------| +| `range clear` | [sheet-range-operations](./sheet-range-operations.md) | +| `range update` | [sheet-write-data](./sheet-write-data.md) | +| `merge-cells` | [sheet-style-format](./sheet-style-format.md) | +| `unmerge-cells` | [sheet-style-format](./sheet-style-format.md) | +| `update-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `range fill` | [sheet-range-operations](./sheet-range-operations.md) | +| `range copy-to` | [sheet-range-operations](./sheet-range-operations.md) | +| `add-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `delete-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `move-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `set-dropdown` | [sheet-dropdown](./sheet-dropdown.md) | +| `delete-dropdown` | [sheet-dropdown](./sheet-dropdown.md) | +| `csv-put` | [sheet-write-data](./sheet-write-data.md) | +| `delete-float-image` | [sheet-media-image](./sheet-media-image.md) | + +## 典型组合场景 + +### 插列 + 写表头 + 整列回填 + +一次原子提交,不要拆成多次独立调用。批量回填同一列**只需一次** `range update`(range 写整列范围、values 写 N×1 矩阵),不需要逐行循环。 + +```jsonc +// 在 C 列前插入新列 → 写表头 C1 → 回填 C2:C100 共 99 行 +[ + {"toolName":"add-dimension", + "input":{"sheet-id":"...","dimension":"COLUMNS","length":1}}, + {"toolName":"range update", + "input":{"sheet-id":"...","range":"C1:C100", + "values":[[{"type":"text","text":"score"}],[{"type":"text","text":"95"}],[{"type":"text","text":"87"}]/* ... 97 more rows ... */]}} +] +``` + +### 先清除再写入 + +清除目标区域残留数据后写入新数据,保证目标区域干净。 + +```jsonc +[ + {"toolName":"range clear", + "input":{"sheet-id":"...","range":"A1:D50","type":"all"}}, + {"toolName":"range update", + "input":{"sheet-id":"...","range":"A1:D50", + "values":[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"金额"},{"type":"text","text":"日期"}],/* ... */]}} +] +``` + +### 批量合并 + 调整行高列宽 + +统一多列合并和尺寸调整,原子提交避免中途失败留下不一致格式。 + +```jsonc +[ + {"toolName":"merge-cells", + "input":{"sheet-id":"...","range":"A1:B1","merge-type":"mergeAll"}}, + {"toolName":"merge-cells", + "input":{"sheet-id":"...","range":"C1:D1","merge-type":"mergeAll"}}, + {"toolName":"update-dimension", + "input":{"sheet-id":"...","dimension":"ROWS","start-index":"1","length":1,"pixel-size":40}}, + {"toolName":"update-dimension", + "input":{"sheet-id":"...","dimension":"COLUMNS","start-index":"A","length":4,"pixel-size":120}} +] +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `list` | 工作表的 `sheetId` | batch-clear 的 --ranges sheet 前缀 / batch-update 子操作 input 的 sheet-id | +| `batch-update` | 各子操作的执行结果 | 回读校验(range read / csv-get) | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- ★ **需要对多个区域执行相同清除操作时,用 `range batch-clear`**:一次原子请求清除多个区域(可跨工作表),失败时整批回滚 +- ★ **需要组合多个不同写操作(清除+写入等)时,用 `batch-update`**:原子事务,任一操作失败则整批回滚,避免留下半成品 +- ★ **批次完成后必须回读校验**:用 `range read` 或 `csv-get` 抽样回读受影响区域,至少校验 3-5 个代表性单元格(首 / 中 / 末),与预期值对照 +- ★ **`batch-update` 不支持嵌套**:`--operations` 中的 `toolName` 必须是原子操作,不可再嵌套 `batch-update` diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-chart.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-chart.md new file mode 100644 index 00000000..0295faae --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-chart.md @@ -0,0 +1,354 @@ +# 浮动图表 (chart) + +## 真对象硬约束 + +当用户要求"画个图 / 数据可视化 / 趋势图 / 对比图 / 占比图"时,**必须**通过 `chart create` 创建真实的图表对象。**禁止**用本地脚本调 matplotlib / seaborn 生成图片再插入到表格代替——静态图片无法随源数据更新,且失去交互能力。判断标准:交付后 `chart list` 必须能返回该对象。 + +## 使用场景 + +读写浮动图表对象。本 reference 覆盖 4 个命令: + +| 操作需求 | 使用命令 | 说明 | +|---------|---------|------| +| 查看已有图表 | `chart list` | 获取图表的类型、数据源和样式配置 | +| 创建图表 | `chart create` | 创建浮动图表对象 | +| 更新图表 | `chart update` | 更新图表配置(PUT 语义整体覆盖) | +| 删除图表 | `chart delete` | 删除浮动图表 | + +典型工作流:先读取现有图表了解配置 → 执行创建/更新/删除 → 再次读取验证结果。 + +## 需求→图表类型映射(创建前必查) + +| 用户说 | 图表类型 | 备注 | +|--------|---------|------| +| "占比"、"比例"、"各XX占多少" | 饼图(pie) | 单维度占比首选 | +| "环形图"、"空心饼图" | 环形图(doughnut) | 与 pie 类似但中心空心 | +| "对比"、"各XX的YY" | 柱形图(column) | 多类别数值对比 | +| "堆叠柱"、"堆积" | 堆积柱形图(columnStacked) | 纵向堆叠 | +| "条形"、"横向对比" | 条形图(bar) | 横向柱形 | +| "横向堆叠" | 堆积条形图(barStacked) | 横向堆叠 | +| "趋势"、"变化"、"走势" | 折线图(line) | 时间序列首选 | +| "堆叠折线" | 堆积折线图(lineStacked) | 堆叠折线 | +| "面积图" | 面积图(area) | 填充折线下方区域 | +| "堆叠面积" | 堆积面积图(areaStacked) | 堆叠面积 | +| "百分比面积" | 百分比堆积面积图(areaPercentStacked) | 占比式面积 | +| "散点图"、"相关性" | 散点图(scatter) | 两变量相关性 | +| "雷达图"、"多维度" | 雷达图(radar) | 多维度对比 | + +**多图表需求**:当用户同时提到多种分析(如"统计占比 + 对比数量"),必须创建多个图表,每个对应一种类型,不要只做一个。 + +## 命令详细参考 + +### 获取浮动图表 +``` +Usage: + dws sheet chart list [flags] +Example: + # 列出所有浮动图表 + dws sheet chart list --node NODE_ID --sheet-id SHEET_ID + + # 获取单个图表详情 + dws sheet chart list --node NODE_ID --sheet-id SHEET_ID --chart-id CHART_ID +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --chart-id string 浮动图表 ID (可选,不传则返回全部) +``` + +- **判空看 `totalCount` / `message`,不要看数组长度**:无图表时 `floatCharts` 数组里仍会带 1 个全 null 的幽灵元素(`chart.category`/`chart.series` 全 null,无 id),但 `totalCount` 为 0、`message` 为「Successfully retrieved 0 float chart(s).」。判断有没有图表以 `totalCount` 为准,或过滤掉没有 `id` 的元素。 + +### 创建浮动图表 +``` +Usage: + dws sheet chart create [flags] +Example: + dws sheet chart create --node NODE_ID --sheet-id SHEET_ID --properties '{ + "position": {"row": 12, "col": "A"}, + "dimensions": {"width": 600, "height": 400}, + "chart": { + "type": "column", + "series": [{"name": "B1", "value": ["B2:B10"]}], + "category": ["A2:A10"], + "title": {"show": true, "text": "销售数据"} + } + }' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --properties string 图表完整配置 JSON (必填,含 position/dimensions/chart) +``` + +### 更新浮动图表 +``` +Usage: + dws sheet chart update [flags] +Example: + dws sheet chart update --node NODE_ID --sheet-id SHEET_ID --chart-id CHART_ID --properties '{ + "position": {"row": 12, "col": "A"}, + "dimensions": {"width": 800, "height": 500}, + "chart": { + "type": "line", + "series": [{"name": "B1", "value": ["B2:B20"]}], + "category": ["A2:A20"], + "title": {"show": true, "text": "月度趋势(更新)"} + } + }' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --chart-id string 浮动图表 ID (必填,可通过 chart list 获取) + --properties string 图表完整配置 JSON (必填,PUT 语义整体覆盖) +``` + +### 删除浮动图表 +``` +Usage: + dws sheet chart delete [flags] +Example: + dws sheet chart delete --node NODE_ID --sheet-id SHEET_ID --chart-id CHART_ID +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --chart-id string 浮动图表 ID (必填,可通过 chart list 获取) +``` + +> [强制] 危险操作:删除不可恢复。必须先向用户展示操作摘要并获得明确同意,用户同意后才加 `--yes` 执行。 + +## `--properties` JSON Schema 速查 + +**顶层字段**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `position` | object | 是 | 锚点位置,详见下方 position 字段表 | +| `offset` | object | 否 | 偏移量,详见下方 offset 字段表 | +| `dimensions` | object | 是 | 尺寸,详见下方 dimensions 字段表 | +| `chart` | object | 是 | 图表配置,详见下方 chart 字段表 | + +**position 字段**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `row` | number | 是 | 锚点行索引(0-based),如 0 表示第一行 | +| `col` | string | 是 | 锚点列。使用字母表示法(`"A"`=第1列, `"B"`=第2列, `"Z"`=第26列, `"AA"`=第27列),不支持数字形式 | + +**offset 字段**(可选,不传时默认 0): + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `offsetX` | number | 否 | 相对锚点的水平偏移量,单位像素,默认 0 | +| `offsetY` | number | 否 | 相对锚点的垂直偏移量,单位像素,默认 0 | + +**dimensions 字段**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `width` | number | 是 | 图表宽度,单位像素,正整数 | +| `height` | number | 是 | 图表高度,单位像素,正整数 | + +**chart 字段**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `type` | string | 是 | 图表类型,枚举值:`column` / `bar` / `columnStacked` / `barStacked` / `line` / `lineStacked` / `area` / `areaStacked` / `areaPercentStacked` / `pie` / `doughnut` / `scatter` / `radar` | +| `series` | object[] | 是 | 数据系列数组,至少一项,详见下方 series 字段表 | +| `category` | string[] | 否 | 分类轴(X轴)数据区域,A1 表示法字符串数组(如 `["A2:A10"]`) | +| `title` | object | 否 | 图表标题配置,详见下方 title 字段表 | +| `legend` | object | 否 | 图例配置,详见下方 legend 字段表 | +| `catAx` | object | 否 | 分类轴(X轴)样式配置,详见下方轴配置表 | +| `valAx` | object | 否 | 值轴(Y轴)样式配置,详见下方轴配置表 | + +**series 数组项**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `name` | string | 否 | 系列名称的单元格引用,A1 表示法(如 `"B1"`)。不传时图例显示为"系列1"等默认名 | +| `value` | string[] | 是 | 系列数据区域,A1 表示法字符串数组(如 `["B2:B10"]`)。每个元素为一段连续区域 | + +**title 字段**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `show` | boolean | 否 | 是否显示标题,默认 false | +| `text` | string | 否 | 标题文本内容 | + +**legend 字段**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `show` | boolean | 否 | 是否显示图例,默认 true | +| `pos` | string | 否 | 图例位置,枚举值:`t`(顶部)/ `b`(底部)/ `l`(左侧)/ `r`(右侧)/ `none`(隐藏) | + +**catAx / valAx 轴配置**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `show` | boolean | 否 | 是否显示该轴,默认 true | +| `pos` | string | 否 | 轴位置,枚举值:`l`(左)/ `t`(上)/ `b`(下)/ `r`(右) | +| `titleConfig` | object | 否 | 轴标题配置:`{ show: boolean, title: string }`。show 控制是否显示轴标题,title 为轴标题文本 | +| `axisMin` | number \| null | 否 | 轴最小值,传 null 或不传表示自动计算 | +| `axisMax` | number \| null | 否 | 轴最大值,传 null 或不传表示自动计算 | +| `splitLine` | boolean | 否 | 是否显示网格线,默认 true | +| `minorSplitLine` | boolean | 否 | 是否显示次网格线,默认 false | +| `axisLabel` | boolean | 否 | 是否显示轴标签(刻度文字),默认 true | +| `axisLine` | boolean | 否 | 是否显示轴线,默认 true | + +## A1 区域引用规则 + +- `series[].value`、`series[].name`、`category` 均使用 A1 表示法字符串 +- 格式:`"A2:B10"`(默认使用 `--sheet-id` 对应的工作表) +- 跨 sheet 引用:`"SHEET_ID!A2:B10"`(前缀为工作表 ID) +- 单元格引用:`"B1"`(等同于 `"B1:B1"`) +- MCP 内部自动将 A1 引用转换为 ISheetRange 结构,CLI 直接透传即可 + +## 图表位置选择(创建前必做) + +凭感觉挑行列号可能越界或遮挡数据。按以下四步走: + +1. **查尺寸**:`dws sheet info --node --sheet-id ` 获取工作表行列数信息。 +2. **估跨度**:默认单元格约 **100 px 宽 × 25 px 高**,`needCols = ceil(width/100)`,`needRows = ceil(height/25)`。 +3. **校验**:`position.row + needRows ≤ rowCount` 且 `col + needCols ≤ columnCount`。 +4. **不够就先扩表**:先调 `insert-dimension` 扩行/列,再 create。 + +**图表落点禁止压在已有数据矩形内**——必须落在数据区**右侧或下方的空白**,否则图表浮层会遮挡原始数据。 + +**示例**:数据在 A1:E20,放 600×400 图 → `needCols=6, needRows=16` +- [正确] `{"row": 22, "col": "A"}` — 放数据下方 +- [正确] `{"row": 0, "col": "G"}` — 放数据右侧 +- [错误] `{"row": 5, "col": "B"}` — 压在数据区域上 + +## 核心工作流 + +```bash +# ── 工作流 1: 创建柱形图 ── + +# 1. 先查 sheetId +dws sheet list --node -f json + +# 2. 查看数据范围确认列对应关系 +dws sheet csv-get --node --sheet-id --range "A1:E5" + +# 3. 创建柱形图 +dws sheet chart create --node --sheet-id --properties '{ + "position": {"row": 22, "col": "A"}, + "dimensions": {"width": 600, "height": 400}, + "chart": { + "type": "column", + "series": [ + {"name": "B1", "value": ["B2:B10"]}, + {"name": "C1", "value": ["C2:C10"]} + ], + "category": ["A2:A10"], + "title": {"show": true, "text": "各月销售额对比"}, + "legend": {"show": true, "pos": "t"} + } +}' + +# 4. 验证创建结果 +dws sheet chart list --node --sheet-id + +# ── 工作流 2: 创建折线图 ── + +dws sheet chart create --node --sheet-id --properties '{ + "position": {"row": 22, "col": "H"}, + "dimensions": {"width": 600, "height": 400}, + "chart": { + "type": "line", + "series": [ + {"name": "B1", "value": ["B2:B13"]}, + {"name": "C1", "value": ["C2:C13"]} + ], + "category": ["A2:A13"], + "title": {"show": true, "text": "年度趋势"}, + "legend": {"show": true, "pos": "b"} + } +}' + +# ── 工作流 3: 创建饼图 ── + +dws sheet chart create --node --sheet-id --properties '{ + "position": {"row": 0, "col": "G"}, + "dimensions": {"width": 500, "height": 400}, + "chart": { + "type": "pie", + "series": [{"value": ["B2:B6"]}], + "category": ["A2:A6"], + "title": {"show": true, "text": "各部门人数占比"} + } +}' + +# ── 工作流 4: 多图表组合分析 ── +# 当用户需要"柱+线"等多种图表组合分析时,分别创建对应类型的图表: + +# 创建柱形图 +dws sheet chart create --node --sheet-id --properties '{ + "position": {"row": 40, "col": "A"}, + "dimensions": {"width": 700, "height": 450}, + "chart": { + "type": "column", + "series": [{"name": "B1", "value": ["B2:B10"]}], + "category": ["A2:A10"], + "title": {"show": true, "text": "销售额"}, + "legend": {"show": true, "pos": "t"} + } +}' + +# 创建折线图 +dws sheet chart create --node --sheet-id --properties '{ + "position": {"row": 40, "col": "J"}, + "dimensions": {"width": 700, "height": 450}, + "chart": { + "type": "line", + "series": [{"name": "C1", "value": ["C2:C10"]}], + "category": ["A2:A10"], + "title": {"show": true, "text": "增长率"}, + "legend": {"show": true, "pos": "t"} + } +}' +``` + +## Update 三步法(PUT 语义) + +`chart update` 是 PUT 语义(整体覆盖),不是 PATCH。必须按三步操作: + +1. `chart list --chart-id ` 获取完整配置 +2. 在获取的配置基础上**局部**修改要改的字段,其余保持不变 +3. 把**完整配置**整体回写到 `--properties` + +> 关键:**不能只提交局部配置**,否则未传字段会被还原为默认值。 + +```bash +# 1. 获取现有配置 +dws sheet chart list --node --sheet-id --chart-id -f json +# → 得到完整的 properties 结构(position/dimensions/chart) + +# 2. 基于返回结果修改(如改标题 + 调整尺寸),构造 --properties JSON + +# 3. 整体回写 +dws sheet chart update --node --sheet-id --chart-id \ + --properties '<完整修改后的 JSON>' +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `chart list` | `floatCharts[].id` | 后续 update / delete 的 `--chart-id` | +| `chart list --chart-id` | 完整的 properties(position/dimensions/chart) | update 时作为基础配置修改后回写 | +| `chart create` | `floatChart.id` | 后续 update / delete 的 `--chart-id` | +| `chart update` | 更新后的完整信息 | 确认更新结果 | +| `chart delete` | `message` | 确认删除完成 | +| `sheet list` | 工作表的 `sheetId` | 所有 chart 命令的 `--sheet-id` | + +## 注意事项 + +- [强制] **`--sheet-id` 获取规范**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- [强制] **创建后必须验证**:图表创建后必须调用 `chart list` 验证配置是否正确 +- ⚠️ **`chart list` 判空看 `totalCount` 不看数组长度**:无图表时 `floatCharts` 仍含 1 个全 null 幽灵元素(无 id),`totalCount` 为 0、`message` 说明为空。判断有没有图表以 `totalCount` 为准或过滤无 `id` 的元素 +- [强制] **chart-id 禁止臆测**:必须通过 `chart list` 获取真实的图表 ID,不可编造 +- **图表类型映射**:用户说"柱形图"用 `column`,"条形图"(横向)用 `bar`,"散点图"用 `scatter`,"雷达图"用 `radar`,"环形图"用 `doughnut` +- **堆积类型后缀**:堆叠版本的图表在基本类型后加 `Stacked`(如 `columnStacked`、`lineStacked`、`areaStacked`) +- **位置不得压在数据区上**:图表浮层会遮挡原始数据,必须放在空白区域 +- **多图表需求**:多次调用 `chart create`,不要试图用一个图表包含所有分析 +- **跨 sheet 引用数据**:在 A1 区域前加 `SHEET_ID!` 前缀,如 `"sheetXXX!A2:B10"` +- **大 JSON 用 @file**:`--properties` 支持 `@文件路径` 读取本地 JSON 文件 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-conditional-format.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-conditional-format.md index b4b7532a..70bc77b7 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-conditional-format.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-conditional-format.md @@ -65,8 +65,9 @@ **正确做法(两步走)**: ```bash # Step 1: 用 range update 在新列写判断公式(形成"是/否"辅助列) +# 每个单元格必须是 object,公式写在 text(以 = 开头);不支持裸字符串 dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "H2:H100" \ - --values '[["=IF(A2>B2,\"是\",\"否\")"],...]' + --values '[[{"type":"text","text":"=IF(A2>B2,\"是\",\"否\")"}],...]' # Step 2: 基于辅助列值做条件格式(用 formulaCondition 引用辅助列) dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID \ @@ -142,7 +143,8 @@ Flags: - **用途**:查看指定工作表中已有的条件格式规则,或获取单个规则的详情。 - **场景**:创建/更新/删除条件格式前后验证规则状态;获取 ruleId 供后续 update/delete 使用。 -- **返回**:rules 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。 +- **返回**:`rules` 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。 +- **判空要看 `message`,不要看数组长度**:无规则时 `rules` 数组里仍会带 1 个全 null 的幽灵元素(`colorScaleCondition`/`dataBarStyle`/`iconSetCondition` 全 null,无 id),`message` 会是「No conditional formatting rules found.」。该命令**没有 `totalCount` 字段**,判空以 message 为准,或过滤掉没有 `id` 的元素。 ### 创建条件格式规则 ``` @@ -269,3 +271,4 @@ Flags: - **辅助列+条件格式两步走**:用户明确要求"辅助列"时,必须按两步走(先建辅助列 → 再基于辅助列做条件格式),禁止直接用 `formulaCondition` 一步绕过 - **大数据量优势**:当数据量 > 1000 行时,条件格式是首选——它由服务端自身渲染,不需要逐行调用 `range set-style`,性能远优于静态样式写入 - **判断标准**:交付后 `cond-format list` 必须能返回该规则;否则视为违规 +- ⚠️ **`cond-format list` 判空看 `message` 不看数组长度**:无规则时 `rules` 仍含 1 个全 null 幽灵元素(无 id),`message` 为「No conditional formatting rules found.」。该命令无 `totalCount`,判空以 message 为准或过滤无 `id` 的元素 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-dropdown.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-dropdown.md index 0b135aa4..173594e2 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-dropdown.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-dropdown.md @@ -55,10 +55,11 @@ Flags: --range string 查询范围,A1 表示法,如 A1:A100 (必填) ``` -查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 +查询指定范围内的下拉列表配置信息,包括选项值和颜色。 - **用途**:查看单元格已设置的下拉列表选项和配置。 - **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 +- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `multipleValues` 和 `colorValueMap`)。**判空以 `hasDropdown` 为准**:范围内无下拉列表时 `hasDropdown` 为 false,但 `dataValidations` 里仍会带 1 个全 null 的幽灵条目,不要用数组长度判空。 +- ⚠️ **已知限制**:`options.multipleValues` 服务端恒返回 `null`(即使该下拉是用 `--multi-select` 建的),也不返回 `enableMultiSelect` 字段——**`get-dropdown` 读回无法判断该下拉是否多选**。要判断是否多选,改用 `range read`:它返回的 `cells[].dataValidation.enableMultiSelect` 字段是准确的(实测有效)。 ### 删除下拉列表 ``` @@ -90,5 +91,6 @@ Flags: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 -- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false +- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。判空以 `hasDropdown` 为准(无下拉时为 false,但 `dataValidations` 仍含 1 个全 null 幽灵条目) +- ⚠️ `get-dropdown` 的 `options.multipleValues` 恒为 `null`、无 `enableMultiSelect` 字段,读回无法判断是否多选;要判断是否多选改用 `range read`(其 `dataValidation.enableMultiSelect` 准确) - `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-export.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-export.md index 359b8c16..f390efed 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-export.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-export.md @@ -47,10 +47,10 @@ Flags: - 第 21~30 次:每次间隔 15 秒 - **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 -**命令返回**: -- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载) -- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: ` -- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下 +**命令返回**(`--format json`,默认):输出规整 JSON `{success, jobId, downloadUrl[, outputPath]}`。轮询进度以 `[INFO] [N/30] 状态: ...` 打到 stderr,不污染 stdout 的 JSON(可直接 `python3 -m json.tool` 解析 stdout)。 +- `--output` 未指定:JSON 含 `jobId` + `downloadUrl`(链接有时效性,请尽快下载) +- `--output` 指定为文件路径:下载到该路径,JSON 额外含 `outputPath` +- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名保存,JSON 含 `outputPath` **失败处理(命令内部已处理,Agent 仅需转述)**: - MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 @@ -82,7 +82,7 @@ dws sheet export --node --output ./ | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `export` | `downloadUrl`(未指定 --output)/ `导出完成: `(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | +| `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | ## 注意事项 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-filter.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-filter.md index 0e65607b..36d69dc4 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-filter.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-filter.md @@ -32,7 +32,7 @@ Flags: - **用途**:查看当前工作表上是否存在全局筛选及其配置。 - **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 - **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 -- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。 +- **返回**:`range`(筛选范围,A1 表示法)、`id`(筛选 ID)和 `criteria`(各列条件对象,key 为列偏移量;无条件时为 `{}`)。如果未设置筛选,返回筛选信息为空。 ### 创建筛选 ``` @@ -152,7 +152,7 @@ Flags: | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `filter get` | `range`(筛选范围)、`columnFilterCriteria`(各列条件) | 查看当前筛选配置,确认筛选是否存在 | +| `filter get` | `range`(筛选范围)、`id`、`criteria`(各列条件对象,无条件为 `{}`) | 查看当前筛选配置,确认筛选是否存在 | | `filter create` | 筛选创建成功的确认 | 确认筛选已建立,后续可通过 `filter update` 设置条件 | | `filter delete` | 删除成功的确认 | 确认筛选已删除 | | `filter update` | 更新成功的确认 | 确认条件已设置 | @@ -164,7 +164,7 @@ Flags: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 -- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空 +- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)、`id` 和 `criteria`(各列条件对象,无条件时为 `{}`)。无筛选时返回空 - `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 - `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 - `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-media-image.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-media-image.md index 40cf3640..060c55c9 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-media-image.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-media-image.md @@ -48,6 +48,8 @@ Flags: --mime-type string 文件 MIME 类型 (默认根据扩展名推断) ``` +`--format json` 输出规整 JSON:`{success, resourceId, resourceUrl, fileName, fileSize}`。`resourceUrl` 可用于 `create-float-image` 的 `--src`。 + ### 上传图片并写入表格单元格 ``` Usage: @@ -204,8 +206,8 @@ dws sheet write-image --node --sheet-id --range C3:C3 --fil # 4. 完整流程: 创建表格 → 写表头 → 写入图片 dws sheet create --name "产品目录" -f json # 提取 nodeId 后: -dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json -dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[[{"type":"text","text":"产品名称"},{"type":"text","text":"产品图片"}]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[[{"type":"text","text":"MacBook Pro"}]]' -f json dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json ``` diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-range-operations.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-range-operations.md index 36d92a03..af6c076d 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-range-operations.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-range-operations.md @@ -82,9 +82,11 @@ Flags: --sheet-id string 工作表 ID 或名称 (必填) --source-range string 源数据范围,A1 表示法 (必填) --target-range string 目标填充范围,A1 表示法 (必填) - --fill-type string 填充类型: series(序列,默认) / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值) + --fill-type string 填充类型: 不传则自动检测 / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值) ``` +`--fill-type` **不传时自动检测**(根据源数据智能判断:数值序列递增、日期递增、文本复制等),这是填序列的默认行为,无需显式传值。`--help` 枚举的可选值只有 `copy` / `onlystyle` / `withoutstyle`;服务端另外也接受 `series`(强制按序列填充),但它不在 `--help` 列表里,一般用不到——要序列递增直接不传 `--fill-type` 即可。 + 目标范围须与源范围在行或列维度对齐(不支持对角填充)。 ### 复制区域 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-style-format.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-style-format.md index 240e57fc..bfa4bc1f 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-style-format.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-style-format.md @@ -219,9 +219,9 @@ Flags: ```bash # ── 工作流 4: 写入数据并设置样式 ── -# 1. 写入数据 +# 1. 写入数据(每个单元格必须是 object;数字也写成字符串) dws sheet range update --node --sheet-id --range "A1:C3" \ - --values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json + --values '[[{"type":"text","text":"商品"},{"type":"text","text":"单价"},{"type":"text","text":"数量"}],[{"type":"text","text":"苹果"},{"type":"text","text":"5.5"},{"type":"text","text":"100"}],[{"type":"text","text":"香蕉"},{"type":"text","text":"3.2"},{"type":"text","text":"200"}]]' --format json # 2. 设置数字格式(人民币) dws sheet range set-style --node --sheet-id --range "B2:B3" \ diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-workbook.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-workbook.md index 8233dbfe..be061b0f 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-workbook.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-workbook.md @@ -108,14 +108,16 @@ Flags: --node string 表格文档 ID 或 URL (必填) --sheet-id string 工作表 ID 或名称 (必填) --name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] : + --title string --name 的别名(兼容) --index int 新位置(从 0 开始) --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 + --tab-color string 工作表标签颜色,Hex 如 #FF0000;传空字符串清除颜色 --frozen-row-count int 冻结行数,0 表示取消冻结 --frozen-column-count int 冻结列数,0 表示取消冻结 ``` -更新工作表名称、位置、隐藏状态、冻结行列。 -`--name` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 +更新工作表名称、位置、隐藏状态、标签颜色、冻结行列。 +`--name`(别名 `--title`)/ `--index` / `--hidden` / `--tab-color` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 注意: - 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 @@ -174,12 +176,12 @@ dws sheet create --name "销售数据" --format json # 2. 查看工作表列表 — 提取 sheetId dws sheet list --node --format json -# 3. 写入表头和数据 +# 3. 写入表头和数据(每个单元格必须是 object;数字也写成字符串) dws sheet range update --node --sheet-id --range "A1:C1" \ - --values '[["姓名","部门","销售额"]]' --format json + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json dws sheet range update --node --sheet-id --range "A2:C4" \ - --values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json + --values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json # ── 工作流 2: 读取已有表格数据 ── @@ -200,12 +202,12 @@ dws sheet range read --node --sheet-id --range "A1:D10" --f # 1. 新建工作表 dws sheet new --node --name "汇总" --format json -# 2. 在新工作表中写入汇总公式 +# 2. 在新工作表中写入汇总公式(公式写在 text,以 = 开头) dws sheet range update --node --sheet-id --range "A1:B1" \ - --values '[["指标","数值"]]' --format json + --values '[[{"type":"text","text":"指标"},{"type":"text","text":"数值"}]]' --format json dws sheet range update --node --sheet-id --range "A2:B2" \ - --values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json + --values '[[{"type":"text","text":"总销售额"},{"type":"text","text":"=SUM(Sheet1!C2:C100)"}]]' --format json ``` ## 上下文传递 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-write-data.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-write-data.md index fc90feec..6b6f4451 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-write-data.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-write-data.md @@ -100,9 +100,11 @@ Flags: Usage: dws sheet csv-put [flags] Example: + # 内联多行 CSV:必须用 $'...' 让 \n 变成真换行;普通单引号 '...\n...' 里的 \n 是字面量,会写成一行含字面 \n 的错误数据 dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv 'name,score\nAlice,95\nBob,87' + --csv $'name,score\nAlice,95\nBob,87' + # 多行数据推荐用 @文件,最稳妥 dws sheet csv-put --node --sheet-id --start-cell B2 \ --csv @data.csv --allow-overwrite diff --git a/skills/multi/dingtalk-skill/SKILL.md b/skills/multi/dingtalk-skill/SKILL.md index 3a3d79a7..6a69c8fa 100644 --- a/skills/multi/dingtalk-skill/SKILL.md +++ b/skills/multi/dingtalk-skill/SKILL.md @@ -27,7 +27,7 @@ metadata: | 用户说 | 命令 | |--------|------| -| "搜索技能 / 找技能" | `dws skill search --query "<关键词>" [--scopes "DingtalkMarket OrgInternal"]` | +| "搜索技能 / 找技能" | `dws skill search --query "<关键词>" [--source "DingtalkMarket OrgInternal"]` | | "下载技能包到本地临时目录" | `dws skill get --skill-id ` | | "安装技能到 Agent 目录" | `dws skill install `(target: claude / cursor / codex / opencode / qoder / .) | @@ -38,3 +38,5 @@ metadata: ## 兼容提示 - `dws skill find` → 用 `dws skill search --query <关键词>` +- `--scopes` 已废弃(真机打 `Flag --scopes has been deprecated, 请使用 --source 替代` 警告)→ 用 `--source` +- 前置:`skill search` 依赖企业开通技能市场;未开通时真机报「当前企业暂未开放此功能」,属企业级开关,非命令写法问题 diff --git a/skills/multi/dingtalk-skill/references/skill.md b/skills/multi/dingtalk-skill/references/skill.md index fe918397..c1e47f55 100644 --- a/skills/multi/dingtalk-skill/references/skill.md +++ b/skills/multi/dingtalk-skill/references/skill.md @@ -41,13 +41,16 @@ Usage: dws skill search [flags] Example: dws skill search --query "周报" - dws skill search --query "日报" --scopes "OrgInternal" - dws skill search --query "日报" --scopes "DingtalkMarket OrgInternal" + dws skill search --query "日报" --source "OrgInternal" + dws skill search --query "日报" --source "DingtalkMarket OrgInternal" Flags: --query string 搜索关键词 (必填) - --scopes string 查询范围,空格分隔。备选值:DingtalkMarket(钉钉市场)、OrgInternal(企业内部)。为空默认查市场技能 + --source string 查询范围,空格分隔。备选值:DingtalkMarket(钉钉市场)、OrgInternal(企业内部)。为空默认查市场技能 ``` +> `--scopes` 已废弃(真机会打 `Flag --scopes has been deprecated, 请使用 --source 替代` 警告),一律用 `--source`。 +> 前置:`skill search` 依赖企业开通技能市场;未开通时真机报「当前企业暂未开放此功能」,这是企业级开关,非命令写法问题。 + 返回字段: - `skillId` — 技能 ID(后续 `install` 需要) - `name` — 技能唯一标识(SKILL.md 的 name) diff --git a/skills/multi/dingtalk-todo/SKILL.md b/skills/multi/dingtalk-todo/SKILL.md index 0fc38887..9e850bfd 100644 --- a/skills/multi/dingtalk-todo/SKILL.md +++ b/skills/multi/dingtalk-todo/SKILL.md @@ -46,7 +46,7 @@ metadata: - `--id` / `--ids` 是隐藏兼容别名,文档和生成命令统一写 `--task-id`,减少模型漂移。 - 优先级映射:低=10,普通=20,较高/高/重要=30,紧急/最高/P0/马上处理=40;不要把"较高"写成 40。 - 截止时间必须是 ISO-8601。相对日期按当前日期计算;例如周五说"下周二"就是紧接下一个自然周的周二,不要再加一周。 -- 附件命令 `task add-attachment` / `list-attachment` / `remove-attachment` 服务端无对应工具,当前不可用,不要生成。 +- 附件命令 `task add-attachment` / `list-attachment` / `remove-attachment` 均可用:`add-attachment --task-id --file-path <本地文件>`(真实上传,先确认待办存在,返回 `result.attachmentIds`);`list-attachment --task-id `(返回顶层 `attachments[].attachmentId`);`remove-attachment --task-id --attachment-id --yes`(不可逆,先确认)。 - 创建、标记完成、重开、删除后必须 `task get` 或对应 `task list --status ...` 验证,不要只凭创建返回或口头计划结束。 - 所有 dws 命令带 `--format json`。 diff --git a/skills/multi/dingtalk-todo/references/02-task.md b/skills/multi/dingtalk-todo/references/02-task.md index 55b881bd..7daaf1fc 100644 --- a/skills/multi/dingtalk-todo/references/02-task.md +++ b/skills/multi/dingtalk-todo/references/02-task.md @@ -6,9 +6,9 @@ | Recipe | 步骤(命令均须 `--format json`,下略) | |--------|----------------------------------------| -| `create-priority-todo` | 1. 确定执行者(同 [SKILL.md](../SKILL.md) 中 `create-todo` 步骤 1)
2. `todo task create --title "<标题>" --executors [,...] --priority <10/20/30/40>`(可选 `--due "<截止ISO>"`;10低/20普通/30较高/40紧急)→ 取 `todoTaskId` | -| `create-recurring-todo` | 1. 确定执行者(同 `create-todo` 步骤 1)
2. `todo task create --title "<标题>" --executors --due "<首次截止ISO>" --recurrence "DTSTART:\nRRULE:FREQ=DAILY;INTERVAL=1"`(`--due` 必填;仅支持按天循环,见 [todo.md](./todo.md))→ 取 `todoTaskId` | -| `reschedule-todo` | 1. `todo task list --status false` → 取 `todoTaskId`
2. `todo task update --task-id --due "<新截止时间>"` | +| `create-priority-todo` | 1. 确定执行者(同 [SKILL.md](../SKILL.md) 中 `create-todo` 步骤 1)
2. `todo task create --title "<标题>" --executors [,...] --priority <10/20/30/40>`(可选 `--due "<截止ISO>"`;10低/20普通/30较高/40紧急)→ 取 `taskId` | +| `create-recurring-todo` | 1. 确定执行者(同 `create-todo` 步骤 1)
2. `todo task create --title "<标题>" --executors --due "<首次截止ISO>" --recurrence "DTSTART:\nRRULE:FREQ=DAILY;INTERVAL=1"`(`--due` 必填;仅支持按天循环,见 [todo.md](./todo.md))→ 取 `taskId` | +| `reschedule-todo` | 1. `todo task list --status false` → 取 `taskId`
2. `todo task update --task-id --due "<新截止时间>"` | ## Full / 组合(固定路线) @@ -16,4 +16,4 @@ |--------|---------------------| | generate-progress-report | 1. 按「多源并行采集」(见 `dws-shared/references/best_practices/_common/conventions.md`)执行
2. 交叉比对各源数据
3. `doc create --name "<报告名>" --content "<报告内容>"` | | batch-create-todo | 1. 按「多源并行采集」(见 `dws-shared/references/best_practices/_common/conventions.md`)执行 → 从结果提取任务条目
2. 每条:`aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`
3. **优先**:将待办写入 `todos.json`(格式见 [todo_batch_create.py](../scripts/todo_batch_create.py)),执行 `python scripts/todo_batch_create.py todos.json`
备选:逐条 `todo task create --title "<标题>" --executors ` → 汇总回显
**单批超 30 条须用户确认** | -| assign-and-notify | 1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`
2. `todo task create --title "<标题>" --executors ` → 取 `todoTaskId`
3. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message send --group --text "<通知内容>"` 通知 | +| assign-and-notify | 1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`
2. `todo task create --title "<标题>" --executors ` → 取 `taskId`
3. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message send --group --text "<通知内容>"` 通知 | diff --git a/skills/multi/dingtalk-todo/references/todo.md b/skills/multi/dingtalk-todo/references/todo.md index 653946ff..759c05be 100644 --- a/skills/multi/dingtalk-todo/references/todo.md +++ b/skills/multi/dingtalk-todo/references/todo.md @@ -178,6 +178,63 @@ Flags: --task-id string 待办任务 ID (必填) ``` +### 查询子待办列表 +``` +Usage: + dws todo task list-sub [flags] +Example: + dws todo task list-sub --task-id +Flags: + --task-id string 待办任务 ID (必填) +``` + +返回结构:子待办在**顶层** `subTasks[]` 数组里(不在 `result` 下),每个元素含 `taskId`(子待办 ID)、`subject`、`priority` 等。取子待办的 `taskId` 用于后续操作。`task get` 的 `result.todoDetailModel.subTodos[]` 没有 taskId 字段,取子待办 ID 必须用本命令。 + +### 上传待办附件 + +> ⚠️ 重要:该接口会真实上传文件到附件,不可用于测试或试探性调用。调用前必须确认待办存在。 + +``` +Usage: + dws todo task add-attachment [flags] +Example: + dws todo task add-attachment --task-id --file-path /path/to/file.pdf +Flags: + --file-path string 本地文件路径 (必填) + --task-id string 待办任务 ID (必填) +``` + +返回 `result.attachmentIds`(数组,如 `["6a4cffb79e2b520ed3600960"]`)。 + +### 查询待办附件列表 + +``` +Usage: + dws todo task list-attachment [flags] +Example: + dws todo task list-attachment --task-id +Flags: + --task-id string 待办任务 ID (必填) +``` + +返回顶层 `attachments[]`,每个元素含 `attachmentId`、`fileName`、`fileSize`;无附件时为空数组。 + +### 删除待办附件 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws todo task remove-attachment [flags] +Example: + dws todo task remove-attachment --task-id --attachment-id + dws todo task remove-attachment --task-id --attachment-id --yes +Flags: + --attachment-id string 待办附件 ID (必填) + --task-id string 待办任务 ID (必填) +``` +附件 attachmentId 使用 `dws todo task list-attachment` 命令获取(顶层 `attachments[].attachmentId`)。删除后可再 `list-attachment` 复查,返回空数组即成功。 + ### 添加待办提醒 ``` Usage: @@ -256,7 +313,7 @@ JSON 数组,每个元素为一条提醒规则,支持两种 `baseTime` 模式 ## 核心工作流 ```bash -# 1. 创建待办 — 提取 todoTaskId +# 1. 创建待办 — 从返回 result.taskId 提取任务 ID dws todo task create --title "修复线上Bug" --executors userId1,userId2 \ --priority 40 --due "2026-03-10T18:00:00+08:00" --format json @@ -310,18 +367,27 @@ dws todo task add-reminder --task-id --base-time customTime --reminder- dws todo task reset-reminder --task-id --format json # 17. 重置待办提醒(指定新规则) dws todo task reset-reminder --task-id --reminder-rules '' --format json + +# 18. 上传附件(真实上传,先确认待办存在)— 从返回 result.attachmentIds 取 attachmentId +dws todo task add-attachment --task-id --file-path /path/to/file.pdf --format json +# 19. 查询附件列表 — 从顶层 attachments[].attachmentId 取 ID +dws todo task list-attachment --task-id --format json +# 20. 删除附件(用户确认后加 --yes;删完可 list-attachment 复查为空) +dws todo task remove-attachment --task-id --attachment-id --yes --format json +# 21. 查询子待办 — 从顶层 subTasks[].taskId 取子待办 ID +dws todo task list-sub --task-id --format json ``` ## 上下文传递表 | 操作 | 从返回中提取 | 用于 | |------|-------------|---------------------------------------------| -| `task create` | `todoTaskId` | update/done/get/delete 的 --task-id | -| `task list` | `result[].id` | update/done/get/delete 的 --task-id | -| `task create` | `todoTaskId` | update/done/get/delete/comment 的 --task-id | -| `task list` | `result[].id` | update/done/get/delete/comment/add-executor/remove-executor/add-participant/remove-participant 的 --task-id | -| `task get` | `result.todoDetailModel.subTodos[]` | 获取子待办列表,提取子待办的 `taskId` 用于后续操作 | -| `comment list` | `result[].commentId` | `comment delete` 的 --comment-id | +| `task create` / `task create-sub` | `result.taskId` | update/done/get/delete/comment 的 --task-id | +| `task list` | `result.todoCards[].taskId` | update/done/get/delete/comment/add-executor/remove-executor/add-participant/remove-participant 的 --task-id | +| `task list-sub` | `subTasks[].taskId`(顶层数组,不在 `result` 下) | 子待办的后续操作 --task-id | +| `add-attachment` | `result.attachmentIds[]` | 新上传附件的 attachmentId | +| `list-attachment` | `attachments[].attachmentId`(顶层数组) | `remove-attachment` 的 --attachment-id | +| `comment list` | `result.comments[].id` | `comment delete` 的 --comment-id | ## 注意事项 @@ -341,6 +407,9 @@ dws todo task reset-reminder --task-id --reminder-rules '" [--description "<描述>"]` | -| "搜索知识库空间" | `dws wiki space search --keyword "<关键词>" [--limit <1-20>]` | +| "创建知识库" | `dws wiki space create --name "<名称>" [--desc "<描述>"]` | +| "查看知识库详情" | `dws wiki space get --workspace ` | +| "搜索知识库空间" | `dws wiki space search --query "<关键词>" [--limit <1-20>]` | | "我的文档 / 个人知识库" | `dws wiki space list --type myWikiSpace` | | "列出组织知识库" | `dws wiki space list [--type orgWikiSpace] [--limit <1-50>]` | +| "删除知识库" | `dws wiki space delete --workspace `(不可逆,先确认)| +| "知识库里有哪些文档/浏览内容" | `dws wiki node list --workspace [--folder ]` | +| "在知识库里创建文档/文件夹" | `dws wiki node create --workspace --name "<名>" [--type folder\|axls\|...]` | +| "空间内搜文档" | `dws wiki node search --workspace --query "<关键词>"` | +| "复制 / 移动库内节点" | `dws wiki node copy` / `dws wiki node move --workspace --node [--folder ]` | +| "删除库内节点" | `dws wiki node delete --workspace --node `(不可逆,先确认)| +| "把知识库分享给某人 / 加成员" | `dws wiki member add --workspace --users --role EDITOR` | +| "改成员角色" | `dws wiki member update --workspace --users --role ` | +| "移除知识库成员" | `dws wiki member remove --workspace --users ` | +| "查看知识库成员" | `dws wiki member list --workspace ` | ## 评测高频硬约束 -- `space search` 用 `--keyword`,不要用 `--query`;`search` 支持 `--type myWikiSpace` 查询个人知识库,但按类型列出空间优先走 `space list --type myWikiSpace/orgWikiSpace`。 -- 用户说"我的文档/个人空间/my workspace"时优先用 `dws wiki space list --type myWikiSpace --format json`。 -- 用户给空关键词时,不要构造空 `--keyword ""`;若语义是我的文档则走 `space list --type myWikiSpace`,否则请用户补关键词。 -- 搜到空间后复用返回的 `workspaceId/id`,知识库内具体文档的创建、搜索、读写切到 `dingtalk-doc`,不要在 `wiki` 下编造 doc 子命令。 +- `space search` / `node search` 关键词 flag 是 `--query`(`--keyword` 是遗留别名,仍可跑通,但优先用 `--query`);`space create --desc`、`space list --cursor` 同理(`--description` / `--page-token` 为旧别名)。 +- 按类型列出空间优先走 `space list --type myWikiSpace/orgWikiSpace`;用户说"我的文档/个人空间"时用 `dws wiki space list --type myWikiSpace --format json`。 +- 用户给空关键词时,不要构造空 `--query ""`;若语义是我的文档则走 `space list --type myWikiSpace`,否则请用户补关键词。 +- `node create --type` 服务端支持 `adoc / axls / able / appt / adraw / amind / folder`;**`asheet` 不支持**,创建在线表格用 `axls`。 +- `member` 的 `--users` 是复数、逗号分隔;`--role` 大小写不敏感(`editor` 与 `EDITOR` 都行)。`member list` 只返回 `name/role/type`,**不含 userId**,拿不到 userId 去串联 update/remove,需用 `dws contact user search --query "<姓名>"` 反查。 +- 知识库内节点的**内容读写**(读取/编辑文档正文)切到 `dingtalk-doc`(先 `node list`/`node search` 拿 nodeId,再 `dws doc read --node `);不要在 `wiki` 下编造 doc 子命令。 - 所有 `dws wiki` 命令加 `--format json`。 ## 跨产品协作 -- 知识库内具体文档读写 → 切到 `dingtalk-doc` -- 文件存储 → 切到 `dingtalk-drive` +- 知识库内文档内容读写 → 切到 `dingtalk-doc` +- 文件存储 / 全局搜索 → 切到 `dingtalk-drive` diff --git a/skills/multi/dingtalk-wiki/references/wiki.md b/skills/multi/dingtalk-wiki/references/wiki.md index f6bc47df..eacfb661 100644 --- a/skills/multi/dingtalk-wiki/references/wiki.md +++ b/skills/multi/dingtalk-wiki/references/wiki.md @@ -14,6 +14,7 @@ dws wiki member add --help # 查看子命令组下的所有命令 dws wiki space --help +dws wiki node --help dws wiki member --help ``` @@ -22,6 +23,8 @@ dws wiki member --help - 报错 "unknown flag" 时 → `--help` 确认正确的 flag 名称 - 不确定某个功能是否存在时 → `dws wiki --help` 查看命令列表 +`dws wiki` 有三个命令族:`space`(知识库容器)、`node`(知识库内节点:文档/文件夹/表格等)、`member`(容器级成员)。 + ## 命令总览 ### 创建知识库 @@ -30,11 +33,11 @@ Usage: dws wiki space create [flags] Example: dws wiki space create --name "产品文档库" --format json - dws wiki space create --name "技术方案" --description "团队技术方案归档" --format json + dws wiki space create --name "技术方案" --desc "团队技术方案归档" --format json Flags: - --name string 知识库名称 (必填,不超过 100 字符) - --description string 知识库描述 (选填,不超过 500 字符) - --icon string 知识库图标标识 (选填) + --name string 知识库名称 (必填,不超过 100 字符) + --desc string 知识库描述 (选填,不超过 500 字符) + --icon string 知识库图标标识 (选填) ``` ### 查看知识库详情 @@ -60,9 +63,9 @@ Example: dws wiki space list --type myWikiSpace --format json dws wiki space list --type orgWikiSpace --limit 50 --format json Flags: - --type string 知识库类型: myWikiSpace / orgWikiSpace (默认 orgWikiSpace) - --limit string 每页数量 1-50 (默认 20) - --page-token string 分页游标 (首页留空) + --type string 空间类型: orgWikiSpace(默认) / myWikiSpace / orgSpace(钉盘企业空间) / mySpace(钉盘我的文件) + --limit string 每页数量 1-50 (默认 20) + --cursor string 分页游标 (首页留空) ``` - `myWikiSpace`:返回当前用户的「我的文档」个人空间(固定 1 条,不支持分页) @@ -73,66 +76,182 @@ Flags: Usage: dws wiki space search [flags] Example: - dws wiki space search --keyword "产品文档" --format json - dws wiki space search --keyword "技术方案" --limit 20 --format json + dws wiki space search --query "产品文档" --format json + dws wiki space search --query "技术方案" --limit 20 --format json dws wiki space list --type myWikiSpace --format json Flags: - --keyword string 搜索关键词 (--type myWikiSpace 时可省略) - --type string 知识库类型: myWikiSpace 时直接返回「我的文档」,省略则搜索组织知识库 - --limit string 返回数量 1-20 (默认 10) + --query string 搜索关键词 (搜索组织知识库时必填) + --type string 知识库类型: myWikiSpace 时直接返回「我的文档」,省略则搜索组织知识库 + --limit string 返回数量 1-20 (默认 10) ``` -当 `--type myWikiSpace` 时,忽略 `--keyword`,直接返回「我的文档」个人空间。 +当 `--type myWikiSpace` 时,忽略 `--query`,直接返回「我的文档」个人空间。 + +### 删除知识库 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws wiki space delete [flags] +Example: + dws wiki space delete --workspace +Flags: + --workspace string 知识库 ID 或 URL (必填) +``` + +将指定知识库移入回收站。操作者必须具备知识库的 OWNER 角色。 + +### 列出知识库节点 +``` +Usage: + dws wiki node list [flags] +Example: + dws wiki node list --workspace --format json + dws wiki node list --workspace --folder --format json +Flags: + --workspace string 知识库 ID (必填) + --folder string 父节点 nodeId (选填,不传则列出根目录) + --limit int 每页数量 (默认 50,最大 50) + --cursor string 分页游标 +``` + +### 在知识库中创建节点 +``` +Usage: + dws wiki node create [flags] +Example: + dws wiki node create --workspace --name "新文档" --format json + dws wiki node create --workspace --name "方案目录" --type folder --format json + dws wiki node create --workspace --name "数据表" --type axls --folder --format json +Flags: + --workspace string 知识库 ID (必填) + --name string 节点名称 (必填) + --type string 节点类型 (默认 adoc),服务端支持: adoc(在线文档) / axls(在线表格) / able(多维表) / appt(在线演示) / adraw(白板) / amind(脑图) / folder(文件夹) + --folder string 父节点 nodeId (选填,不传则在根目录创建) +``` + +> ⚠️ **type 枚举**:服务端支持 `adoc` / `axls` / `able` / `appt` / `adraw` / `amind` / `folder` 七种;**`asheet` 不支持**,传了会被后端拒绝(报「不支持的文件类型 asheet」)。创建在线表格请用 `axls`。 + +### 复制知识库节点 +``` +Usage: + dws wiki node copy [flags] +Example: + dws wiki node copy --workspace --node --format json + dws wiki node copy --workspace --node --folder --format json +Flags: + --workspace string 知识库 ID (必填) + --node string 源节点 ID (必填) + --folder string 目标文件夹 nodeId (选填) +``` + +### 移动知识库节点 +``` +Usage: + dws wiki node move [flags] +Example: + dws wiki node move --workspace --node --folder --format json +Flags: + --workspace string 知识库 ID (必填) + --node string 源节点 ID (必填) + --folder string 目标文件夹 nodeId (选填) +``` + +### 删除知识库节点 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws wiki node delete [flags] +Example: + dws wiki node delete --workspace --node +Flags: + --workspace string 知识库 ID (必填,用于权限校验) + --node string 节点 ID (必填) +``` + +将知识库中的节点移入回收站。权限要求: 对节点有「管理」权限。 + +### 在知识库中搜索节点 +``` +Usage: + dws wiki node search [flags] +Example: + dws wiki node search --workspace --query "方案" --format json + dws wiki node search --workspace --query "设计" --extensions adoc,axls --format json +Flags: + --workspace string 知识库 ID (必填) + --query string 搜索关键词 (必填) + --extensions strings 按文件扩展名过滤,逗号分隔 (如 adoc,axls,pdf) (选填) + --limit int 每页数量 (默认 10,最大 30) (选填) + --cursor string 分页游标 (选填) +``` + +在指定知识库空间内搜索节点(需 `--workspace`)。全局聚合搜索走 `dingtalk-drive` 的 `drive search`。 ### 添加知识库成员(容器级授权) ``` Usage: dws wiki member add [flags] Example: - dws wiki member add --workspace --user uid1 --role READER - dws wiki member add --workspace --user uid1,uid2 --role EDITOR - dws wiki member add --workspace "https://alidocs.dingtalk.com/i/spaces//overview" --user uid1 --role MANAGER + dws wiki member add --workspace --users uid1 --role READER + dws wiki member add --workspace --users uid1,uid2 --role EDITOR Flags: - --workspace string 目标知识库 ID 或 URL (必填) - --user string 被加入的用户 userId 列表,逗号分隔 (必填,单次最多 30 个) - --role string 授予的角色 (必填,大小写敏感,必须全大写): MANAGER (管理者) / EDITOR (可编辑) / DOWNLOADER (可下载) / READER (可阅读) + --workspace string 目标知识库 ID 或 URL (必填) + --users string 被加入的用户 userId 列表,逗号分隔 (必填,单次最多 30 个) + --role string 授予的角色 (必填,大小写不敏感): MANAGER / EDITOR / DOWNLOADER / READER ``` > **❗ 重要约束**: -> - 仅支持 USER 类型。 -> - 角色枚举严格大写:MANAGER / EDITOR / DOWNLOADER / READER(OWNER 不可通过此接口添加,知识库创建者默认为所有者)。 +> - 仅支持 USER 类型;角色枚举 MANAGER / EDITOR / DOWNLOADER / READER(OWNER 不可通过此接口添加,知识库创建者默认为所有者)。 +> - `--role` **大小写不敏感**,`editor` 与 `EDITOR` 都能通过校验(CLI 会归一化成大写)。 > - 操作者需具备知识库的 OWNER 或 MANAGER 权限。 -> - 「我的文档」(myWikiSpace) 是个人空间,**不支持容器级成员管理**;后端会直接拒绝。如果你的目标只是把某篇文档分享给别人,节点级权限授权在开源 dws v1.0.30 暂不支持,请在钉钉客户端文档中通过「分享」设置。 +> - 「我的文档」(myWikiSpace) 是个人空间,**不支持容器级成员管理**;后端会直接拒绝。节点级权限授权在开源 dws 暂不支持,请在钉钉客户端文档中通过「分享」设置。 ### 修改知识库成员角色 ``` Usage: dws wiki member update [flags] Example: - dws wiki member update --workspace --user uid1 --role EDITOR - dws wiki member update --workspace --user uid1,uid2 --role READER + dws wiki member update --workspace --users uid1 --role EDITOR Flags: - --workspace string 目标知识库 ID 或 URL (必填) - --user string 目标用户 userId 列表,逗号分隔 (必填,单次最多 30 个) - --role string 新角色 (必填,大小写敏感,必须全大写): MANAGER / EDITOR / DOWNLOADER / READER + --workspace string 目标知识库 ID 或 URL (必填) + --users string 目标用户 userId 列表,逗号分隔 (必填,单次最多 30 个) + --role string 新角色 (必填,大小写不敏感): MANAGER / EDITOR / DOWNLOADER / READER ``` +### 移除知识库成员 +``` +Usage: + dws wiki member remove [flags] +Example: + dws wiki member remove --workspace --users uid1 +Flags: + --workspace string 目标知识库 ID 或 URL (必填) + --users string 被移除的用户 userId 列表,逗号分隔 (必填,单次最多 30 个) +``` + +> OWNER 角色不可通过此接口移除;操作者需具备 OWNER 或 MANAGER 权限。 + ### 列出知识库成员 ``` Usage: dws wiki member list [flags] Example: dws wiki member list --workspace - dws wiki member list --workspace --limit 100 dws wiki member list --workspace --filter-role EDITOR Flags: --workspace string 目标知识库 ID 或 URL (必填) - --limit int 返回成员数上限 - --filter-role string 按角色过滤,逗号分隔: MANAGER / EDITOR / DOWNLOADER / READER (选填) + --limit int 返回成员数上限,默认 30,最大 200 + --filter-role string 按角色过滤,逗号分隔: OWNER / MANAGER / EDITOR / DOWNLOADER / READER (选填) ``` > 接口不支持游标分页,使用 `--limit` 一次性拉取。 +> ⚠️ **返回字段限制**:`member list` 每条只返回 `name` / `role` / `type` 三个字段,**不含 userId**(服务端不返回)。因此**无法**从 `member list` 拿到 userId 再去串联 `member update` / `member remove`。要对某人改角色 / 移除,需另行拿到其 userId(例如用 `dws contact user search --query "<姓名>"` 按姓名反查)。 + ## 意图判断 - 用户说"创建知识库/新建知识库" → `space create` @@ -140,21 +259,25 @@ Flags: - 用户说"我的知识库/知识库列表/有哪些知识库" → `space list` - 用户说"搜索知识库/找知识库" → `space search` - 用户说"我的文档/个人空间" → `space list --type myWikiSpace` -- 用户说"把知识库分享给某人/给某人加入知识库/邀请进知识库" → `member add`(需 `--workspace` + `--user` + `--role`) +- 用户说"删除知识库/移除知识库" → `space delete`(需 `--workspace`) +- 用户说"知识库下的文件/知识库里有哪些文档/浏览知识库内容" → `node list`(需 `--workspace`) +- 用户说"在知识库里搜文档/空间内搜索" → `node search`(需 `--workspace` + `--query`) +- 用户说"在知识库里创建文档/新建文件夹" → `node create`(需 `--workspace` + `--name`) +- 用户说"复制/移动知识库里的文档" → `node copy` / `node move`(需 `--workspace` + `--node`) +- 用户说"删除知识库里的文档/节点" → `node delete`(需 `--workspace` + `--node`) +- 用户说"把知识库分享给某人/邀请进知识库" → `member add`(需 `--workspace` + `--users` + `--role`) - 用户说"修改某人在知识库的权限/调整成员角色" → `member update` +- 用户说"移除知识库成员/把某人从知识库移除" → `member remove`(需 `--workspace` + `--users`) - 用户说"知识库有哪些成员/查看知识库成员" → `member list` -> **跨产品路由(重要)**:`dws wiki` 只管知识库容器(space/member),**不提供查看知识库文件/文档的能力**。以下意图必须走 `dws doc`,不要在 wiki 下尝试 `node`/`file`/`list` 等子命令: -> - 用户说"知识库下的文件/知识库里有哪些文档/浏览知识库内容" → 先用 `dws wiki space list` 或 `space search` 拿到 `workspaceId`,再走 **`dws doc list --workspace `** -> - 用户说"读某个知识库里的某篇文档" → 先通过上面的方式找到 nodeId,再走 **`dws doc read --node `** -> - 用户说"在知识库里搜文档" → 走 **`dws doc search --workspace-ids `** -> - 用户说"在知识库里创建文档" → 走 **`dws doc create --workspace `** +> **跨产品路由**:`dws wiki node` 负责知识库内节点的**列出/创建/复制/移动/删除/搜索**(空间管理层);节点的**内容读写**(读取/编辑/块级操作)切到 `dingtalk-doc`: +> - 用户说"读某个知识库里的某篇文档" → 先用 `node list` / `node search` 拿到 `nodeId`,再切到 `dingtalk-doc` 用 `dws doc read --node ` +> - 用户说"搜文件"(不指定空间) → 切到 `dingtalk-drive` 用 `dws drive search`(全局聚合搜索) 关键区分: -- wiki(知识库空间级管理:创建/查询/列出/搜索/成员管理) vs doc(文档内容级操作:搜索/读写/编辑/节点级权限) -- wiki space(知识库容器) vs drive(钉盘文件存储/上传/下载) -- **wiki member**(容器级,授权整个知识库);节点级 `doc permission` 在开源 dws v1.0.30 不可用 - - 「我的文档」当前开源版本不能通过 CLI 改权限;在钉钉客户端用「分享」设置 +- **wiki node**(空间管理层:节点列出/创建/复制/移动/删除/搜索)vs **doc**(内容层:读写/编辑/块级/评论/导出)vs **drive**(存储层:上传/下载/全局搜索/权限) +- **wiki node search**(空间内搜索,需 `--workspace`)vs **drive search**(全局聚合搜索) +- **wiki member**(容器级,授权整个知识库);「我的文档」不支持容器级成员管理 ## 核心工作流 @@ -162,44 +285,50 @@ Flags: # 列出我有权访问的组织知识库 dws wiki space list --format json -# 获取「我的文档」个人空间 -dws wiki space list --type myWikiSpace --format json - # 搜索知识库 -dws wiki space search --keyword "产品" --format json - -# 搜索「我的文档」 -dws wiki space list --type myWikiSpace --format json +dws wiki space search --query "产品" --format json # 创建知识库 -dws wiki space create --name "新项目文档" --description "项目相关文档归档" --format json +dws wiki space create --name "新项目文档" --desc "项目相关文档归档" --format json -# 查看知识库详情 -dws wiki space get --workspace --format json +# ── 工作流: 浏览知识库内容 ── +dws wiki space list --format json # 1. 取 workspaceId +dws wiki node list --workspace --format json # 2. 列根目录节点 +dws wiki node list --workspace --folder --format json # 3. 进子目录 +# 4. 读取文档内容 → 切到 dingtalk-doc: dws doc read --node --format json + +# ── 工作流: 在知识库中创建文档/文件夹 ── +dws wiki node create --workspace --name "新方案" --format json +dws wiki node create --workspace --name "方案归档" --type folder --format json + +# ── 工作流: 在知识库中搜索 ── +dws wiki node search --workspace --query "方案" --format json # ── 工作流: 给知识库加成员 ── +dws wiki space list --format json # 1. 确认 workspaceId(不要 --type myWikiSpace) +dws wiki member add --workspace --users --role EDITOR --format json # 2. 加成员 +dws wiki member list --workspace --format json # 3. 查看当前成员 -# 1. 先确认知识库 ID(避免授权到「我的文档」) -dws wiki space list --format json # 注意:不要 --type myWikiSpace - -# 2. 添加成员 -dws wiki member add --workspace --user --role EDITOR --format json - -# 3. 查看当前成员 -dws wiki member list --workspace --format json +# ── 工作流: 移除知识库成员 ── +dws wiki member list --workspace --format json # 1. 查看成员(只返回 name/role/type,无 userId) +dws contact user search --query "<姓名>" --format json # 2. 按姓名反查 userId +dws wiki member remove --workspace --users --format json # 3. 移除 ``` ## 上下文传递表 | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `space create` | `workspaceId` | space get 的 --workspace / member add 的 --workspace | -| `space list` | `workspaceId` | space get 的 --workspace / member add 的 --workspace | -| `space search` | `workspaceId` | space get 的 --workspace / member add 的 --workspace | +| `space create` | `workspaceId` | node list / member add 的 --workspace | +| `space list` | `workspaceId` | node list / member add 的 --workspace | +| `space search` | `workspaceId` | node list / member add 的 --workspace | | `space get` | `spaceUrl` | 分享给用户 | -| `member list` | `userId` | member update 的 --user | +| `node list` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | +| `node search` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | +| `node create` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | +| `member list` | `name` / `role` / `type`(**不含 userId**)| 仅用于查看成员名单;**无法**从这里取 userId 去串联 member update/remove,需另行按姓名反查 userId(如 `dws contact user search --query "<姓名>"`)| ## 相关产品 -- [doc](./doc.md) — 文档内容级操作(搜索/读写/编辑文档、知识库内文档管理) -- [drive](./drive.md) — 钉盘文件存储/上传/下载 +- [doc](../../dingtalk-doc/references/doc.md) — 内容层:文档读写/编辑/块级操作/评论/导出 +- [drive](../../dingtalk-drive/references/drive.md) — 存储层:文件上传/下载/全局搜索/权限