Compare commits

...
3 Commits
Author SHA1 Message Date
修雨 818b8b29e3 chore(changelog): add v1.0.50 release notes (#580)
Covers PR #575 (global --jq/--fields honored on product commands,
skill --dry-run preview, sheet batch-style JSON mode, skill docs
alignment) and the exported cmdutil leaf-merge / provenance helpers.
2026-07-08 14:49:25 +08:00
Ari 109ad13844 fix: honor global --jq/--fields on product commands; round-2 QA fixes (#575)
* fix: honor global --jq/--fields on product commands; round-2 QA fixes

Make the global --jq / --fields output filters actually work for the
product (MCP) commands. The helper Formatter used by every product
command ignored them, so they were silent no-ops there (they already
worked for `dws api`). Expose Fields()/JQ() on the ToolCaller interface
and apply the existing output.WriteFiltered path in the helper
Formatter's PrintJSON. The handful of bespoke utility commands
(auth/config/profile/...) still encode directly and are documented as
such.

Additional CLI fixes surfaced by the second real-machine QA pass:
- sheet write-image: emit clean JSON under --format json (suppress the
  progress lines that leaked onto stdout, same as media-upload/export)
- sheet range batch-set-style: under --format json, collect per-item
  results into a single JSON object instead of printing N separate ones
- chat download-media: create the output directory when missing and
  strip URL-encoded path separators from the inferred filename so the
  file actually lands instead of failing on a missing subdirectory
- pat chmod, aitable, sheet, chat, attendance: correct --help text
  (real scope names, non-existent subcommands, flag requiredness,
  alxs -> axls typo)

Helper scripts (mono and multi):
- minutes_extract_todos: parse dingtalkTodoList/actions (there is no
  todos key), so todos are no longer silently dropped
- sync the multi copies of chat_export_messages / chat_history_with_user
  (were crashing with AttributeError), minutes_list_parse /
  minutes_recent_summary, and calendar_free_slot_finder to the fixed
  mono versions

Skill docs (mono and multi): correct return-structure keys, flag names,
deprecated command routing (doc download -> drive download), enum values
and server-side limitations across products; update the global
reference to note --jq/--fields now apply to product commands.

* fix(skill): make skill setup --dry-run a no-op preview; doc/help fixups

skill setup ignored the global --dry-run flag and always wrote the skill
files (overwriting an existing install). Short-circuit into a preview
that lists the source, target dirs and selected sub-skills without
touching the filesystem.

Also correct a few doc/help mismatches found in the round-3 health check:
- attendance vacation balance/records quick-reference examples were
  missing the required --leave-code flag
- mail mailbox list --help described the returned field as "mailboxes"
  but the real field is "emailAccounts"

* docs: clarify --fields projects top-level/list keys, use --jq for nested

* docs: drop QA voice ("真机") and don't state env-specific quirks as absolute rules

The QA-driven doc/comment edits leaked test-process narration ("真机实测")
and this environment/account's quirks stated as universal rules into the
skill files, which are general-purpose instructions for any org/account.
Strip the "真机" narration everywhere; reword environment-specific findings
(PUBLIC sharing disabled by org policy, transient 1002, sender-open-dingtalk-id
behaviour) from absolute bans into conditional hints; keep genuinely
universal command behaviour, just without the QA voice.
2026-07-08 14:06:57 +08:00
修雨 91dfc8b926 fix: export command merge helpers 2026-07-08 10:47:31 +08:00
79 changed files with 1058 additions and 266 deletions
+15
View File
@@ -6,6 +6,21 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
## [Unreleased]
## [1.0.50] - 2026-07-08
This release fixes a long-standing gap where the global `--jq` / `--fields` output filters were silently ignored on product commands, lands a JSON-mode output path for the sheet batch-style command, and aligns the bundled skill surface with the real command semantics uncovered by the round-2 real-machine QA sweep.
### Fixed
- **Global `--jq` / `--fields` are honored on product commands** (#575) — `Formatter.PrintJSON` / `PrintJSONUnescaped` now route through `output.WriteFiltered` when either flag is set, so product commands accept the same filters that `dws api` has always supported. The tool-caller adapter exposes `Fields()` / `JQ()` so helpers can read the flags without re-parsing.
- **`skill setup --dry-run` is a no-op preview** (#575) — it now prints what would be written without touching the skill directory, the registry, or the agent config. Help text and docs are updated to match.
- **Skill docs alignment to the real command surface** (#575) — per-product references and the cross-product intent guide clarify that `--fields` projects top-level / list keys only (use `--jq` for nested paths); `minutes_extract_todos.py`, `calendar_free_slot_finder.py`, `chat_export_messages.py` / `chat_history_with_user.py`, and `contact_dept_members.py` are rewritten against the current response shapes; `aisearch` / `aitable` / `attendance` / `calendar` / `chat` / `contact` / `dev` / `doc` / `doc-comment` / `doc-file-ops` / `doc-list` / `doc-search` / `drive` / `mail` / `minutes` / `oa` / `sheet` / `sheet-export` / `url-patterns` / `best_practices/lite-recipes.md` / `global-reference.md` / `intent-guide.md` are re-synced; the QA voice ("真机" phrasing) and environment-specific quirks stated as absolute rules are removed from the docs.
### Changed
- **`sheet range batch-set-style` emits per-row JSON in JSON mode** (#575) — when `--format json` is set, each update is reported as `{index, sheetId, range, ok, error}` instead of only the final aggregate, so callers can programmatically track partial failures under `--continue-on-error`.
- **Command-merge helpers exported** — `pkg/cmdutil.LeafMerge*` and the provenance helpers are now public so downstream command trees can reuse the same merge semantics.
## [1.0.49] - 2026-07-08
This release lands a full real-machine QA sweep across the CLI, helper scripts, and skill docs (#572), and hardens the release pipeline so npm publishing can no longer be blocked by Gitee mirror issues (#570).
+4
View File
@@ -824,6 +824,10 @@ func (f *authLoginRecommendSequenceCaller) Format() string { return "table" }
func (f *authLoginRecommendSequenceCaller) DryRun() bool { return false }
func (f *authLoginRecommendSequenceCaller) Fields() string { return "" }
func (f *authLoginRecommendSequenceCaller) JQ() string { return "" }
func stringSliceArgEqual(got any, want []string) bool {
if got == nil {
return len(want) == 0
+13
View File
@@ -128,6 +128,19 @@ func runSkillSetup(cmd *cobra.Command, _ []string) error {
multiSkillNames = ensureMandatorySharedSkill(filtered, allMultiSkillNames)
}
// --dry-run:仅预览将安装的内容与目标目录,不写入任何文件、不弹确认。
if dryRun, _ := cmd.Flags().GetBool("dry-run"); dryRun {
fmt.Fprintf(out, "[DRY-RUN] 预览(不写入任何文件):mode=%s,来源 %s\n", mode, skillSrc)
fmt.Fprintln(out, "将安装到:")
for _, d := range dests {
fmt.Fprintf(out, " - %s\n", d)
}
if mode == skillSetupModeMulti && len(multiSkillNames) > 0 {
fmt.Fprintf(out, "子 skill:%s\n", strings.Join(multiSkillNames, ", "))
}
return nil
}
if !autoYes {
ok, err := confirmSkillSetup(out, mode, skillSrc, dests, multiSkillNames)
if err != nil {
+14
View File
@@ -54,6 +54,20 @@ func (a *toolCallerAdapter) DryRun() bool {
return a.flags != nil && a.flags.DryRun
}
func (a *toolCallerAdapter) Fields() string {
if a.flags != nil {
return a.flags.Fields
}
return ""
}
func (a *toolCallerAdapter) JQ() string {
if a.flags != nil {
return a.flags.JQ
}
return ""
}
func convertResult(r executor.Result) *edition.ToolResult {
resp := r.Response
if resp == nil {
+1 -1
View File
@@ -4290,7 +4290,7 @@ parentSectionId 为空串表示该节点在 Base 根目录下。
baseGetPrimaryDocIdCmd.Flags().String("table-id", "", "Table ID,可通过 list_tables 或 get_base 获取 (必填)")
baseGetPrimaryDocIdCmd.Flags().String("record-id", "", "记录 ID (必填)")
baseCopyCmd.Flags().String("base-id", "", "源 Base ID (必填)")
baseCopyCmd.Flags().String("target-folder-id", "", "目标文件夹 ID (必填), 默认为当前 Base 的父节点")
baseCopyCmd.Flags().String("target-folder-id", "", "目标文件夹 ID (必填, 不传会复制失败)")
baseCopyCmd.Flags().Bool("only-struct", false, "是否仅复制结构(不含数据),默认 false 表示完整复制")
baseCmd.AddCommand(
baseListCmd, baseSearchCmd, baseGetCmd,
+3 -3
View File
@@ -2333,7 +2333,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`,
子命令:
types 查询当前用户假期规则列表
save-type 创建或更新假期规则(仅支持无额度模式)
update-type 更新假期规则(仅支持无额度模式)
balance 查询指定员工假期余额
save-balance 更新员工假期余额
records 查询指定员工假期余额变更记录`,
@@ -3136,11 +3136,11 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`,
// vacation balance (get_leave_balance_quota)
vacationBalanceCmd.Flags().String("users", "", "目标员工 ID 列表,逗号分隔 (必填)")
vacationBalanceCmd.Flags().String("leave-code", "", "假期规则 code (选填,不传则查询所有假期)")
vacationBalanceCmd.Flags().String("leave-code", "", "假期规则 code (必填,服务端要求非空,不传返回 INVALID_PARAMS)")
// vacation records (get_leave_balance_records)
vacationRecordsCmd.Flags().String("user", "", "指定查询员工 ID (必填)")
vacationRecordsCmd.Flags().String("leave-code", "", "假期规则 code (选填,不传则查询所有假期)")
vacationRecordsCmd.Flags().String("leave-code", "", "假期规则 code (必填,服务端要求非空,不传返回 INVALID_PARAMS)")
vacationRecordsCmd.Flags().String("start", "", "查询开始日期,格式 YYYY-MM-DD (必填)")
vacationRecordsCmd.Flags().String("end", "", "查询结束日期,格式 YYYY-MM-DD (必填)")
+20 -11
View File
@@ -3069,7 +3069,7 @@ func newChatCommand() *cobra.Command {
chatMessageCreateTextEmotionCmd := &cobra.Command{
Use: "create-text-emotion",
Short: "创建文字表情(获取 emotionId)",
Long: `创建一个新的文字表情模板。当 list-emotions 中没有所需表情时,使用此命令创建并获取 emotionId,随后可用于 add-text-emotion。`,
Long: `创建一个新的文字表情模板。当内置表情(见 chat-emoji-list.md)中没有所需表情时,使用此命令创建并获取 emotionId,随后可用于 add-text-emotion。`,
Example: ` dws chat message create-text-emotion --emotion-name "赞" --text "nice"
dws chat message create-text-emotion --emotion-name "感谢" --text "感谢" --background-id im_bg_5`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -3228,12 +3228,21 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
return err
}
// 解析输出路径:如果是目录,从 URL 推断文件名
// 解析输出路径:目录(已存在的目录,或以分隔符结尾的意图目录)→ 追加推断文件名。
fi, statErr := os.Stat(outputPath)
if statErr == nil && fi.IsDir() {
isDir := (statErr == nil && fi.IsDir()) ||
strings.HasSuffix(outputPath, string(os.PathSeparator)) ||
strings.HasSuffix(outputPath, "/")
if isDir {
filename := inferFilename(resourceURL)
outputPath = filepath.Join(outputPath, filename)
}
// 确保目标父目录存在,否则 httpGetFile 打开文件会因目录缺失失败。
if dir := filepath.Dir(outputPath); dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("创建输出目录失败: %w", err)
}
}
// Step 2: HTTP GET 下载文件
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载资源到 %s ...", outputPath))
@@ -4316,16 +4325,16 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
chatGroupAuditJoinValidationCmd := &cobra.Command{
Use: "audit-join-validation",
Short: "审批入群验证(通过、拒绝、删除)",
Long: `审批入群验证。支持通过、拒绝、删除、忽略、拒绝并拉黑等操作。
Long: `审批入群验证。真机实测服务端仅接受 AuditApprove / AuditDelete,其余状态会被拒绝(unsupported audit status)。
status 可选值:
AuditApprove — 通过
AuditDelete — 删除
AuditIgnore — 忽略
AuditRefuse — 拒绝
AuditBlock — 拒绝且不再接受该用户的申请`,
AuditApprove — 通过(可用)
AuditDelete — 删除(可用)
AuditIgnore — 忽略(服务端拒绝,不可用)
AuditRefuse — 拒绝(服务端拒绝,不可用)
AuditBlock — 拒绝且不再接受该用户的申请(服务端拒绝,不可用)`,
Example: ` dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditApprove
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditRefuse --description "不符合入群条件"
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditDelete --description "不符合入群条件"
# 查询入群验证记录: dws chat group list-join-validations`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "record-id", "applicant", "inviter", "status"); err != nil {
@@ -4352,7 +4361,7 @@ status 可选值:
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("group")
chatGroupAuditJoinValidationCmd.Flags().String("record-id", "", "申请记录 ID (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("record-id")
chatGroupAuditJoinValidationCmd.Flags().String("status", "", "审批动作: AuditApprove/AuditDelete/AuditIgnore/AuditRefuse/AuditBlock (必填)")
chatGroupAuditJoinValidationCmd.Flags().String("status", "", "审批动作,真机仅 AuditApprove/AuditDelete 可用;AuditIgnore/AuditRefuse/AuditBlock 服务端拒绝 (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("status")
chatGroupAuditJoinValidationCmd.Flags().String("applicant", "", "申请人 openDingTalkId (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("applicant")
+5 -1
View File
@@ -400,7 +400,11 @@ func inferFilename(rawURL string) string {
if decoded, err := url.PathUnescape(name); err == nil {
name = decoded
}
if name != "" {
// 解码后可能含 %2F 还原出的路径分隔符(如 "ddmedia/xxx.png"),只取
// 末段 base 名,避免拼出调用方未创建的子目录导致写文件失败。
name = strings.ReplaceAll(name, "\\", "/")
name = filepath.Base(name)
if name != "" && name != "." && name != "/" {
return name
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ func newMailCommand() *cobra.Command {
Long: `查询当前用户绑定的所有邮箱地址。
返回字段:
mailboxes 邮箱列表,每条包含邮箱地址、账号类型、所属企业
emailAccounts 邮箱列表,每条包含邮箱地址(email)、账号类型(type)、所属企业(orgName)
错误说明:
domain.notFound 该用户的邮箱不是由钉钉邮箱托管,无法完成操作`,
+29
View File
@@ -6,8 +6,31 @@ import (
"io"
"os"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
)
// applyGlobalFilter applies the global --jq / --fields output filters to data
// when either is set. Returns handled=true when it wrote the (filtered) output,
// so callers skip their default JSON encoding. The filter helpers live in
// internal/output (the same ones used by `dws api`); the helper Formatter used
// by product commands previously ignored these flags, making them no-ops.
func (f *Formatter) applyGlobalFilter(data any) (handled bool, err error) {
if deps == nil || deps.Caller == nil {
return false, nil
}
jq := strings.TrimSpace(deps.Caller.JQ())
fields := strings.TrimSpace(deps.Caller.Fields())
if jq == "" && fields == "" {
return false, nil
}
format := output.Format(strings.TrimSpace(deps.Caller.Format()))
if format == "" {
format = output.FormatJSON
}
return true, output.WriteFiltered(f.w, format, data, fields, jq)
}
// Formatter provides output formatting compatible with the old Wukong CLI.
type Formatter struct {
w io.Writer
@@ -23,6 +46,9 @@ func NewFormatter() *Formatter {
// 转义为 \u0026、\u003c、\u003e。对于大多数 CLI 输出场景这是安全的默认行为。
// 如果返回值中包含 URL 等不应被转义的内容,请使用 PrintJSONUnescaped。
func (f *Formatter) PrintJSON(data any) error {
if handled, err := f.applyGlobalFilter(data); handled {
return err
}
enc := json.NewEncoder(f.w)
enc.SetIndent("", " ")
return enc.Encode(data)
@@ -38,6 +64,9 @@ func (f *Formatter) PrintJSON(data any) error {
//
// 影响范围:仅在调用方显式选择时生效,不影响全局 PrintJSON 的行为。
func (f *Formatter) PrintJSONUnescaped(data any) error {
if handled, err := f.applyGlobalFilter(data); handled {
return err
}
enc := json.NewEncoder(f.w)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
+1 -1
View File
@@ -268,7 +268,7 @@ func newExportCmd() *cobra.Command {
- 未指定:仅返回 downloadUrl,链接有时效性请尽快下载
支持范围:
仅支持钉钉在线电子表格(alxs)→ xlsx;
仅支持钉钉在线电子表格(axls)→ xlsx;
若需导出钉钉文字文档,请使用 dingtalkdoc 侧的导出工具。
权限要求:
+19 -7
View File
@@ -182,7 +182,13 @@ func runSheetWriteImage(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
deps.Out.PrintInfo(fmt.Sprintf("[1/3] 获取附件上传凭证 (%s, %d bytes)...", fileName, fileSize))
// json 模式下进度提示会污染 stdout(PrintInfo/PrintKeyValue 都写 stdout),
// 使 write_image 的 JSON 响应无法被单独解析。故 json 模式抑制进度。
jsonMode := deps.Caller.Format() == "json"
if !jsonMode {
deps.Out.PrintInfo(fmt.Sprintf("[1/3] 获取附件上传凭证 (%s, %d bytes)...", fileName, fileSize))
}
result, err := deps.Caller.CallTool(ctx, "doc", "get_doc_attachment_upload_info", map[string]any{
"nodeId": nodeID,
@@ -216,10 +222,11 @@ func runSheetWriteImage(cmd *cobra.Command, _ []string) error {
resourceURL, _ = credData["resourceUrl"].(string)
}
deps.Out.PrintKeyValue("resourceId", resourceID)
deps.Out.PrintKeyValue("resourceUrl", resourceURL)
deps.Out.PrintInfo("[2/3] 上传图片到 OSS...")
if !jsonMode {
deps.Out.PrintKeyValue("resourceId", resourceID)
deps.Out.PrintKeyValue("resourceUrl", resourceURL)
deps.Out.PrintInfo("[2/3] 上传图片到 OSS...")
}
ossHeaders := map[string]string{
"Content-Type": mimeType,
@@ -228,7 +235,9 @@ func runSheetWriteImage(cmd *cobra.Command, _ []string) error {
return err
}
deps.Out.PrintInfo("[3/3] 写入图片到表格单元格...")
if !jsonMode {
deps.Out.PrintInfo("[3/3] 写入图片到表格单元格...")
}
writeArgs := map[string]any{
"nodeId": nodeID,
@@ -244,10 +253,13 @@ func runSheetWriteImage(cmd *cobra.Command, _ []string) error {
writeArgs["height"] = h
}
if jsonMode {
// json 模式:write_image 的 JSON 响应就是唯一 stdout 输出。
return callMCPTool("write_image", writeArgs)
}
if err := callMCPTool("write_image", writeArgs); err != nil {
return err
}
deps.Out.PrintInfo(fmt.Sprintf("图片已写入表格: %s → %s (resourceId=%s)", fileName, rangeAddress, resourceID))
return nil
}
+40
View File
@@ -1,6 +1,7 @@
package helpers
import (
"context"
"encoding/json"
"fmt"
"os"
@@ -397,6 +398,9 @@ func newRangeBatchSetStyleCmd() *cobra.Command {
node := mustGetFlag(cmd, "node")
batchPath := mustGetFlag(cmd, "batch")
continueOnErr, _ := cmd.Flags().GetBool("continue-on-error")
jsonMode := deps.Caller.Format() == "json"
ctx := context.Background()
var jsonResults []any
data, err := os.ReadFile(batchPath)
if err != nil {
@@ -457,6 +461,34 @@ func newRangeBatchSetStyleCmd() *cobra.Command {
continue
}
fmt.Fprintf(os.Stderr, "[%d/%d] update_range sheet=%s range=%s\n", i+1, total, item.SheetID, item.Range)
if jsonMode {
text, cerr := callMCPToolReturnText(ctx, "update_range", toolArgs)
entry := map[string]any{"index": i + 1, "sheetId": item.SheetID, "range": item.Range}
if cerr != nil {
entry["ok"] = false
entry["error"] = cerr.Error()
jsonResults = append(jsonResults, entry)
cerr = fmt.Errorf("第 %d/%d 条 update_range 失败: %w", i+1, total, cerr)
fmt.Fprintln(os.Stderr, cerr)
failed++
if firstErr == nil {
firstErr = cerr
}
if !continueOnErr {
break
}
continue
}
var parsed any
if json.Unmarshal([]byte(text), &parsed) == nil {
entry["result"] = parsed
} else {
entry["result"] = text
}
entry["ok"] = true
jsonResults = append(jsonResults, entry)
continue
}
if err := callMCPTool("update_range", toolArgs); err != nil {
err = fmt.Errorf("第 %d/%d 条 update_range 失败: %w", i+1, total, err)
fmt.Fprintln(os.Stderr, err)
@@ -470,6 +502,14 @@ func newRangeBatchSetStyleCmd() *cobra.Command {
}
}
fmt.Fprintf(os.Stderr, "batch-set-style 完成:共 %d 条,失败 %d 条\n", total, failed)
if jsonMode {
_ = deps.Out.PrintJSON(map[string]any{
"total": total,
"failed": failed,
"results": jsonResults,
"success": failed == 0,
})
}
if failed > 0 && !continueOnErr {
return firstErr
}
+3 -3
View File
@@ -244,7 +244,7 @@ func newChmodCommand(c edition.ToolCaller) *cobra.Command {
Long: `授予指定 scope 的操作权限。
scope 格式: <product>.<entity>:<permission>
例: aitable.record:read chat.group:write calendar.event:read
例: aitable.record:query chat.message:list calendar.event:get
grantType 规则:
once 一次性,执行一次后自动失效
@@ -271,9 +271,9 @@ agentCode 配置:
}
return cobra.MinimumNArgs(1)(cmd, args)
},
Example: ` dws pat chmod aitable.record:read --grant-type session --session-id session-xxx
Example: ` dws pat chmod aitable.record:query --grant-type session --session-id session-xxx
dws pat chmod chat.message:list --grant-type once
dws pat chmod aitable.record:read aitable.record:write --grant-type permanent --yes
dws pat chmod aitable.record:query aitable.record:create --grant-type permanent --yes
dws pat chmod --product calendar --product aitable --grant-type once --dry-run --format json
dws pat chmod --products calendar,aitable --grant-type session --session-id session-xxx --yes
dws pat chmod --domain calendar --domain chat --grant-type once --yes
+18 -1
View File
@@ -67,6 +67,8 @@ func (f *fakeToolCaller) CallTool(_ context.Context, _ string, toolName string,
func (f *fakeToolCaller) Format() string { return "json" }
func (f *fakeToolCaller) DryRun() bool { return f.dryRun }
func (f *fakeToolCaller) Fields() string { return "" }
func (f *fakeToolCaller) JQ() string { return "" }
type recordedToolCall struct {
tool string
@@ -94,6 +96,8 @@ func (f *fallbackToolCaller) CallTool(_ context.Context, _ string, toolName stri
func (f *fallbackToolCaller) Format() string { return "json" }
func (f *fallbackToolCaller) DryRun() bool { return false }
func (f *fallbackToolCaller) Fields() string { return "" }
func (f *fallbackToolCaller) JQ() string { return "" }
type fallbackErrorToolCaller struct {
calls []recordedToolCall
@@ -198,6 +202,8 @@ func (f *fallbackPATContractErrorToolCaller) CallTool(_ context.Context, _ strin
func (f *fallbackPATContractErrorToolCaller) Format() string { return "json" }
func (f *fallbackPATContractErrorToolCaller) DryRun() bool { return false }
func (f *fallbackPATContractErrorToolCaller) Fields() string { return "" }
func (f *fallbackPATContractErrorToolCaller) JQ() string { return "" }
type sequenceToolCaller struct {
calls []recordedToolCall
@@ -227,6 +233,8 @@ func (s *sequenceToolCaller) CallTool(_ context.Context, _ string, toolName stri
func (s *sequenceToolCaller) Format() string { return "json" }
func (s *sequenceToolCaller) DryRun() bool { return s.dryRun }
func (s *sequenceToolCaller) Fields() string { return "" }
func (s *sequenceToolCaller) JQ() string { return "" }
func stringSliceArgEqual(got any, want []string) bool {
gotSlice, ok := got.([]string)
@@ -372,7 +380,7 @@ func TestPATHelpDocumentsBatchAuthorization(t *testing.T) {
"--dry-run 只返回授权计划",
"执行批量授权必须显式",
"由服务端默认兜底",
"aitable.record:read aitable.record:write --grant-type permanent --yes",
"aitable.record:query aitable.record:create --grant-type permanent --yes",
"dws pat chmod --products calendar,aitable",
"dws pat chmod --recommend --grant-type session",
} {
@@ -2020,3 +2028,12 @@ func TestResolveAgentCodeFromEnv(t *testing.T) {
code, src)
}
}
func (f *fallbackErrorToolCaller) Fields() string { return "" }
func (f *fallbackErrorToolCaller) JQ() string { return "" }
func (f *fallbackSchemaMismatchToolCaller) Fields() string { return "" }
func (f *fallbackSchemaMismatchToolCaller) JQ() string { return "" }
func (f *fallbackPermissionDeniedToolCaller) Fields() string { return "" }
func (f *fallbackPermissionDeniedToolCaller) JQ() string { return "" }
func (f *fallbackPATErrorToolCaller) Fields() string { return "" }
func (f *fallbackPATErrorToolCaller) JQ() string { return "" }
+90
View File
@@ -0,0 +1,90 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"log/slog"
"github.com/spf13/cobra"
)
// MergeHardcodedLeaves grafts leaves from hardcodedRoot onto dynamicRoot when
// the same-named path does not already exist. Groups recurse. On leaf
// conflicts, the dynamic side wins by default because the runtime envelope is
// authoritative; hardcoded commands are retained as fallback for paths the
// envelope does not declare.
//
// A hardcoded leaf or group can opt into replacement by carrying a strictly
// higher OverridePriority than the dynamic command at the same path.
//
// MergeHardcodedLeaves mutates dynamicRoot in place and returns it. Grafted
// commands are detached from hardcodedRoot so Cobra parent pointers remain
// correct.
func MergeHardcodedLeaves(dynamicRoot, hardcodedRoot *cobra.Command) *cobra.Command {
if dynamicRoot == nil || hardcodedRoot == nil {
return dynamicRoot
}
children := append([]*cobra.Command(nil), hardcodedRoot.Commands()...)
for _, hc := range children {
dyn := findChildByName(dynamicRoot, hc.Name())
switch {
case dyn == nil:
hardcodedRoot.RemoveCommand(hc)
dynamicRoot.AddCommand(hc)
case IsLeafCmd(hc) && IsLeafCmd(dyn):
if OverridePriority(hc) > OverridePriority(dyn) {
hardcodedRoot.RemoveCommand(hc)
dynamicRoot.RemoveCommand(dyn)
dynamicRoot.AddCommand(hc)
}
case !IsLeafCmd(hc) && !IsLeafCmd(dyn) && OverridePriority(hc) > OverridePriority(dyn):
hardcodedRoot.RemoveCommand(hc)
dynamicRoot.RemoveCommand(dyn)
dynamicRoot.AddCommand(hc)
case !IsLeafCmd(hc) && !IsLeafCmd(dyn):
MergeHardcodedLeaves(dyn, hc)
case IsLeafCmd(dyn) && !IsLeafCmd(hc) && OverridePriority(hc) > OverridePriority(dyn):
hardcodedRoot.RemoveCommand(hc)
dynamicRoot.RemoveCommand(dyn)
dynamicRoot.AddCommand(hc)
default:
slog.Warn("overlay: shape mismatch, keeping dynamic",
"name", hc.Name(),
"dynamicIsLeaf", IsLeafCmd(dyn),
"hardcodedIsLeaf", IsLeafCmd(hc))
}
}
return dynamicRoot
}
// IsLeafCmd reports whether cmd has no subcommands.
func IsLeafCmd(cmd *cobra.Command) bool {
if cmd == nil {
return false
}
return !cmd.HasSubCommands()
}
func findChildByName(parent *cobra.Command, name string) *cobra.Command {
if parent == nil {
return nil
}
for _, child := range parent.Commands() {
if child.Name() == name {
return child
}
}
return nil
}
+257
View File
@@ -0,0 +1,257 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"testing"
"github.com/spf13/cobra"
)
func newGroup(name string, children ...*cobra.Command) *cobra.Command {
cmd := &cobra.Command{Use: name}
cmd.AddCommand(children...)
return cmd
}
func newLeaf(name, tag string) *cobra.Command {
return &cobra.Command{Use: name, Short: tag}
}
func TestMergeHardcodedLeavesNilInputs(t *testing.T) {
t.Parallel()
if got := MergeHardcodedLeaves(nil, nil); got != nil {
t.Fatalf("expected nil, got %v", got)
}
dyn := newGroup("root")
if got := MergeHardcodedLeaves(dyn, nil); got != dyn {
t.Fatal("expected dynamic root to be returned unchanged")
}
hc := newGroup("root")
if got := MergeHardcodedLeaves(nil, hc); got != nil {
t.Fatal("expected nil when dynamic root is nil")
}
}
func TestMergeHardcodedLeavesGraftsUnknownLeaf(t *testing.T) {
t.Parallel()
dyn := newGroup("root", newLeaf("kept", "dynamic"))
hc := newGroup("root", newLeaf("extra", "hardcoded"))
MergeHardcodedLeaves(dyn, hc)
got := findChildByName(dyn, "extra")
if got == nil {
t.Fatal("expected extra leaf to be grafted")
}
if got.Short != "hardcoded" {
t.Fatalf("extra.Short = %q, want %q", got.Short, "hardcoded")
}
if findChildByName(hc, "extra") != nil {
t.Fatal("expected extra leaf to be detached from hardcoded root")
}
if got.Parent() != dyn {
t.Fatalf("grafted leaf parent = %v, want %v", got.Parent(), dyn)
}
}
func TestMergeHardcodedLeavesDynamicLeafWinsByDefault(t *testing.T) {
t.Parallel()
dyn := newGroup("root", newLeaf("shared", "dynamic"))
hc := newGroup("root", newLeaf("shared", "hardcoded"))
MergeHardcodedLeaves(dyn, hc)
got := findChildByName(dyn, "shared")
if got == nil {
t.Fatal("expected shared leaf to remain")
}
if got.Short != "dynamic" {
t.Fatalf("shared.Short = %q, want dynamic", got.Short)
}
if findChildByName(hc, "shared") == nil {
t.Fatal("expected hardcoded shared leaf to remain on donor root")
}
}
func TestMergeHardcodedLeavesHigherPriorityHardcodedLeafWins(t *testing.T) {
t.Parallel()
dynLeaf := newLeaf("shared", "dynamic")
dyn := newGroup("root", dynLeaf)
hcLeaf := newLeaf("shared", "hardcoded")
SetOverridePriority(hcLeaf, 100)
hc := newGroup("root", hcLeaf)
MergeHardcodedLeaves(dyn, hc)
got := findChildByName(dyn, "shared")
if got != hcLeaf {
t.Fatalf("expected hardcoded leaf to replace dynamic leaf, got %+v", got)
}
if findChildByName(hc, "shared") != nil {
t.Fatal("expected hardcoded leaf to be detached from donor root")
}
}
func TestMergeHardcodedLeavesEqualPriorityKeepsDynamic(t *testing.T) {
t.Parallel()
dynLeaf := newLeaf("shared", "dynamic")
SetOverridePriority(dynLeaf, 100)
dyn := newGroup("root", dynLeaf)
hcLeaf := newLeaf("shared", "hardcoded")
SetOverridePriority(hcLeaf, 100)
hc := newGroup("root", hcLeaf)
MergeHardcodedLeaves(dyn, hc)
got := findChildByName(dyn, "shared")
if got != dynLeaf {
t.Fatalf("equal priorities should keep dynamic leaf, got %+v", got)
}
}
func TestMergeHardcodedLeavesRecurseGroups(t *testing.T) {
t.Parallel()
dyn := newGroup("root",
newGroup("space",
newLeaf("list", "dynamic"),
),
)
hc := newGroup("root",
newGroup("space",
newLeaf("list", "hardcoded"),
newLeaf("create", "hardcoded"),
),
newLeaf("ping", "hardcoded"),
)
MergeHardcodedLeaves(dyn, hc)
space := findChildByName(dyn, "space")
if space == nil {
t.Fatal("expected space group")
}
if list := findChildByName(space, "list"); list == nil || list.Short != "dynamic" {
t.Fatalf("space.list should remain dynamic, got %+v", list)
}
if create := findChildByName(space, "create"); create == nil || create.Short != "hardcoded" {
t.Fatalf("space.create should be grafted, got %+v", create)
}
if ping := findChildByName(dyn, "ping"); ping == nil || ping.Short != "hardcoded" {
t.Fatalf("ping should be grafted, got %+v", ping)
}
}
func TestMergeHardcodedLeavesHigherPriorityGroupReplacesDynamicGroup(t *testing.T) {
t.Parallel()
dynGroup := newGroup("export", newLeaf("get", "dynamic"))
dyn := newGroup("root", dynGroup)
hcGroup := newGroup("export", newLeaf("get", "hardcoded"))
SetOverridePriority(hcGroup, 100)
hc := newGroup("root", hcGroup)
MergeHardcodedLeaves(dyn, hc)
got := findChildByName(dyn, "export")
if got != hcGroup {
t.Fatal("expected higher-priority hardcoded group to replace dynamic group")
}
if get := findChildByName(got, "get"); get == nil || get.Short != "hardcoded" {
t.Fatalf("expected hardcoded export.get after replacement, got %+v", get)
}
if findChildByName(hc, "export") != nil {
t.Fatal("expected hardcoded group to be detached from donor root")
}
}
func TestMergeHardcodedLeavesShapeMismatchKeepsDynamic(t *testing.T) {
t.Parallel()
dyn := newGroup("root",
newGroup("cmd", newLeaf("sub", "dynamic")),
)
hc := newGroup("root", newLeaf("cmd", "hardcoded"))
MergeHardcodedLeaves(dyn, hc)
cmd := findChildByName(dyn, "cmd")
if cmd == nil {
t.Fatal("expected dynamic cmd to remain")
}
if IsLeafCmd(cmd) {
t.Fatal("expected dynamic cmd to remain a group")
}
if findChildByName(cmd, "sub") == nil {
t.Fatal("expected cmd.sub to remain")
}
}
func TestMergeHardcodedLeavesHigherPriorityGroupReplacesDynamicLeaf(t *testing.T) {
t.Parallel()
dynLeaf := newLeaf("shared", "dynamic")
dyn := newGroup("root", dynLeaf)
hcGroup := newGroup("shared",
newLeaf("list", "hardcoded"),
newLeaf("add", "hardcoded"),
)
SetOverridePriority(hcGroup, 100)
hc := newGroup("root", hcGroup)
MergeHardcodedLeaves(dyn, hc)
got := findChildByName(dyn, "shared")
if got != hcGroup {
t.Fatal("expected higher-priority hardcoded group to replace dynamic leaf")
}
if findChildByName(got, "list") == nil {
t.Fatal("expected hardcoded subtree leaf list to be reachable")
}
if findChildByName(got, "add") == nil {
t.Fatal("expected hardcoded subtree leaf add to be reachable")
}
if findChildByName(hc, "shared") != nil {
t.Fatal("expected hardcoded group to be detached from donor root")
}
}
func TestMergeHardcodedLeavesEqualPriorityShapeMismatchKeepsDynamic(t *testing.T) {
t.Parallel()
dynLeaf := newLeaf("shared", "dynamic")
SetOverridePriority(dynLeaf, 100)
dyn := newGroup("root", dynLeaf)
hcGroup := newGroup("shared", newLeaf("list", "hardcoded"))
SetOverridePriority(hcGroup, 100)
hc := newGroup("root", hcGroup)
MergeHardcodedLeaves(dyn, hc)
got := findChildByName(dyn, "shared")
if got != dynLeaf {
t.Fatalf("equal priorities should keep dynamic leaf, got %+v", got)
}
}
func TestIsLeafCmd(t *testing.T) {
t.Parallel()
leaf := newLeaf("x", "")
group := newGroup("x", newLeaf("child", ""))
if !IsLeafCmd(leaf) {
t.Fatal("expected leaf to be leaf")
}
if IsLeafCmd(group) {
t.Fatal("expected group to not be leaf")
}
if IsLeafCmd(nil) {
t.Fatal("expected nil to not be leaf")
}
}
+38
View File
@@ -1,7 +1,45 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import "github.com/spf13/cobra"
// SourceAnnotation records where a command tree came from. Edition overlays
// use it to distinguish runtime-authored commands from helper fallbacks that
// happen to share the same top-level product name.
const SourceAnnotation = "dws.source"
// SourceEnvelope marks a command as authored by the runtime discovery envelope.
const SourceEnvelope = "envelope"
// MarkEnvelopeSource stamps cmd with runtime discovery provenance.
func MarkEnvelopeSource(cmd *cobra.Command) {
if cmd == nil {
return
}
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
cmd.Annotations[SourceAnnotation] = SourceEnvelope
}
// IsEnvelopeSourced reports whether cmd was authored by the runtime discovery
// envelope.
func IsEnvelopeSourced(cmd *cobra.Command) bool {
return cmd != nil && cmd.Annotations[SourceAnnotation] == SourceEnvelope
}
// KindAnnotation is the annotation key for marking command kinds.
const KindAnnotation = "dws.kind"
+45
View File
@@ -0,0 +1,45 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"testing"
"github.com/spf13/cobra"
)
func TestEnvelopeSourceProvenance(t *testing.T) {
t.Parallel()
if IsEnvelopeSourced(nil) {
t.Fatal("nil command should not be envelope sourced")
}
cmd := &cobra.Command{Use: "chat"}
if IsEnvelopeSourced(cmd) {
t.Fatal("unstamped command should not be envelope sourced")
}
MarkEnvelopeSource(cmd)
if !IsEnvelopeSourced(cmd) {
t.Fatal("stamped command should be envelope sourced")
}
if got := cmd.Annotations[SourceAnnotation]; got != SourceEnvelope {
t.Fatalf("SourceAnnotation = %q, want %q", got, SourceEnvelope)
}
}
func TestMarkEnvelopeSourceNilDoesNotPanic(t *testing.T) {
t.Parallel()
MarkEnvelopeSource(nil)
}
+4
View File
@@ -53,6 +53,10 @@ type ToolCaller interface {
Format() string
// DryRun returns true when --dry-run is active.
DryRun() bool
// Fields returns the global --fields output projection ("" if unset).
Fields() string
// JQ returns the global --jq output filter expression ("" if unset).
JQ() string
}
// RuntimeDefaultFn resolves a single runtimeDefault placeholder (e.g.
@@ -249,7 +249,7 @@ query 未提"听记"但任务产出依赖会议讨论内容时(报告/总结/
- 按文件夹:`folderId:2`(2=收件箱, 1=已发送, 5=草稿, 6=已删除)
- 按是否有附件:`hasAttachments:true`
- 组合:`from:alice AND subject:周报 AND date>2025-06-01T00:00:00Z`
3. 执行搜索:`mail message search --email <邮箱> --query "<KQL表达式>" --size 20`
3. 执行搜索:`mail message search --email <邮箱> --query "<KQL表达式>" --limit 20`
4. 查看详情(按需):`mail message get --email <邮箱> --id <messageId>`
### mail-send
+2 -2
View File
@@ -64,8 +64,8 @@ dws recovery finalize --event-id <event_id> --outcome recovered|failed|handoff -
| 标志 | 短名 | 说明 | 默认 |
|------|:---:|------|------|
| `--format` | `-f` | 输出格式: json / table / raw | json |
| `--jq` | | jq 表达式过滤输出 ⚠️**当前为 no-op,不生效**:仍返回完整 JSON,不要依赖它过滤,改在拿到 JSON 后自行处理 | 无 |
| `--fields` | | 筛选输出字段 ⚠️**当前为 no-op,不生效**:仍返回全量字段 | 无 |
| `--jq` | | jq 表达式过滤输出(如 `.result[].name`)。对产品命令(aitable/chat/mail/... 走 MCP 的)已生效;少数工具命令(auth/config/profile/doctor/schema 等)仍直接编码、暂不过滤 | 无 |
| `--fields` | | 筛选输出字段(逗号分隔)。按**顶层信封键**(data/result/success/status…)或**列表元素字段**投影;取 data 内的嵌套字段(如 baseName)请改用 `--jq '.data.baseName'`,`--fields baseName` 会因顶层无此键返回 `{}`。同 `--jq`:产品命令已生效,个别工具命令暂不生效 | 无 |
| `--verbose` | `-v` | 详细日志 | false |
| `--debug` | | 调试日志 | false |
| `--yes` | `-y` | 跳过确认提示 | false |
+2 -2
View File
@@ -68,8 +68,8 @@
| "改群昵称/设置群昵称/我在群里的名字" | 设置个人群昵称 | `chat group update-nick` | `chat group rename` | update-nick 改自己的群昵称;rename 改群名称 |
| "群备注/给群加备注/修改群备注" | 设置群备注 | `chat group update-alias` | `chat group rename` | update-alias 设置仅自己可见的备注;rename 改群名称全员可见 |
| "隐藏会话/隐藏群聊/隐藏对话" | 隐藏会话 | `chat hide` | `chat mute` | hide 隐藏会话不显示;mute 是免打扰但仍显示 |
| "关闭@所有人通知/屏蔽@all/不接收@所有人" | 关闭 @所有人提醒 | `chat mute-at-all` | `chat mute` | mute-at-all 仅屏蔽 @所有人;mute 是整个会话免打扰 |
| "关闭红包通知/屏蔽红包/不接收红包提醒" | 关闭红包提醒 | `chat mute-red-envelope` | `chat mute` | mute-red-envelope 仅屏蔽红包;mute 是整个会话免打扰 |
| "关闭@所有人通知/屏蔽@all/不接收@所有人" | 关闭 @所有人提醒 | `chat mute-at-all` | `chat mute` | mute-at-all 仅屏蔽 @所有人;mute 是整个会话免打扰。⚠️ 当前服务端稳定返回 1002「系统繁忙」,命令暂不可用,先告知用户到钉钉客户端设置 |
| "关闭红包通知/屏蔽红包/不接收红包提醒" | 关闭红包提醒 | `chat mute-red-envelope` | `chat mute` | mute-red-envelope 仅屏蔽红包;mute 是整个会话免打扰。⚠️ 同 mute-at-all,服务端稳定 1002,暂不可用 |
| "解散群/解散群聊" | 解散群聊 | `chat group dismiss` | `chat group quit` | dismiss 是群主解散整个群(不可逆);quit 是当前用户自己退群 |
| "新成员看历史/历史消息可见范围" | 设置新成员可见历史消息 | `chat group set-history` | `chat group update-settings` | set-history 控制新成员入群后可见历史消息范围;update-settings 是其他群功能开关 |
| "群里有哪些机器人/查看群机器人/列出群机器人" | 查看群内机器人列表 | `chat group bots` | `chat group members` | bots 只列机器人;members 列普通群成员 |
+1 -1
View File
@@ -98,7 +98,7 @@ Flags:
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `aisearch person` | `userId`(用户ID)、`title`(姓名) | 展示搜索结果、后续操作(发消息/建待办等) |
| `aisearch person` | `userId`(用户ID,= `meta.staffId`/`meta.jobNumber`)、`author`(真实姓名,= `meta.name`);`title` 是花名/显示名,不一定等于姓名 | 展示搜索结果、后续操作(发消息/建待办等) |
## 重名消歧
@@ -31,7 +31,7 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
| `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]"` |
| `dashboard share update` | 更新仪表盘分享配置 | `--base-id` `--dashboard-id` `--enabled` | `--enabled true` 开启,`--share-type` 用 `ORG`(ORG 回显 `shareType="[1]"`);部分组织禁用了 `PUBLIC` 公开分享;若报 `Illegal argument`,改用 `ORG`(组织内分享)。`--enabled false` 关闭 |
## chart 子命令
@@ -43,7 +43,7 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
| `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 share update` | 更新图表分享配置(`--enabled true/false`,开启配 `--share-type`,用 `ORG`;部分组织禁用了 `PUBLIC` 公开分享;报 `Illegal argument` 时改用 `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,否则服务端拒绝。
+15 -2
View File
@@ -89,6 +89,19 @@ Flags:
返回每条记录含:用户 ID、实际打卡时间、打卡地址、打卡经纬度、打卡类型(OnDuty/OffDuty)、定位方式(Map/Wifi/etc)。时间跨度不超过 1 个月。
### 查询个人某日考勤详情
```
Usage:
dws attendance record get [flags]
Example:
dws attendance record get --user 011769261608 --date 2026-03-08
Flags:
--user string 钉钉用户 ID (必填,别名 --users 亦可)
--date string 查询日期, 格式 YYYY-MM-DD (必填)
```
查询单个用户在**某一天**的考勤详情(区别于 `check record`/`check result` 的多人时间段批量查询)。返回 `result` 对象含 `isHasSchedule`(当日是否有排班)、`isRest`(是否休息日)、`isUnSigned`(是否未打卡)、`recordList`(打卡明细)、`approveList`(当日审批单)、`workOvertime`、`workTimeDesc` 等字段。`--user` 只接受**单个** userId;查 userId 用 `dws contact user search --query "姓名"`。
### 查询审批单(补卡/加班/请假/出差外出)
```
Usage:
@@ -1161,13 +1174,13 @@ dws attendance report query-leave --users userId1,userId2 \
dws attendance vacation types --format json
# 查看指定员工假期余额
dws attendance vacation balance --users userId1,userId2 --format json
dws attendance vacation balance --users userId1,userId2 --leave-code <假期类型code> --format json
# 查看指定员工某类假期余额
dws attendance vacation balance --users userId1 --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890 --format json
# 查看指定员工假期余额变更记录
dws attendance vacation records --user USER_ID --start 2026-04-01 --end 2026-04-22 --format json
dws attendance vacation records --user USER_ID --leave-code <假期类型code> --start 2026-04-01 --end 2026-04-22 --format json
# 更新假期规则名称
dws attendance vacation update-type --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
+3 -3
View File
@@ -5,7 +5,7 @@
- **二级子命令(必选其一)**:`event`(日程)、`attendee`(参会人)、`room`(会议室)、`busy`(闲忙)、`attachment`(日程附件)、`book`(我能看哪些日历本)、`acl`(我的日历共享给了谁)。`dws calendar` 后**必须**紧跟上述之一;**禁止**只执行 `dws calendar`(无子命令)。
- **个人日程 / 给自己留时间块 / 专注时段**:统一走 **`dws calendar event create`**。当前**没有**单独的 `personal schedule create` / `calendar create` 命令。
- **查日程列表**:`dws calendar event list --start "<ISO-8601>" --end "<ISO-8601>" --format json`,或优先使用脚本 `python scripts/calendar_today_agenda.py [today|tomorrow|week]`(见文末「自动化脚本」)。
- **查用户日历本列表**:`dws calendar book list`(返回主日历 `id == "primary"` 等)。**重要**可以查询他人共享给自己的日历本,根据日历本id可以进一步查询对方的日程信息。
- **查用户日历本列表**:`dws calendar book list`(返回字段为 `calendarId`,主日历 `calendarId == "primary"` 等;注意无 `id` 字段,按 `.id` 提取会取空)。**重要**可以查询他人共享给自己的日历本,根据日历本id可以进一步查询对方的日程信息。
- **CLI 不存在**独立的 `dws calendar list`;若误跑无子命令的 `dws calendar`,会打印整段 Usage,**切勿**将该段 help 当作工具结果再次塞进对话(会急剧增加 token 与首字延迟)。
- **必须**遵循指令说明进行调用。**绝对禁止**使用虚构指令,使用虚构参数。
@@ -618,7 +618,7 @@ dws calendar event list --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15
| `event respond` | 响应结果 | — |
| `room search` | `rooms[].roomId` | room add 的 --rooms 或 event create 的 --rooms |
| `room list-groups` | `groups[].groupId` | room search 的 --group-id |
| `book list` | `id`(如 `primary`) | event list/get 的 --calendar-id, book get/update 的 --id |
| `book list` | `calendarId`(如 `primary`) | event list/get 的 --calendar-id, book get/update 的 --id |
| `book get` | 日历详细信息 | — |
| `book search` | 匹配的日历列表 | book get/update 的 --id |
| `acl list` | `aclId` | acl delete 的 --acl-id |
@@ -640,7 +640,7 @@ dws calendar event list --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15
- **搜房无结果**:在符合早停/用户限定范围内,`room search`(含按分组逐组查)全部返回空或无空闲 → 应**直接向用户报错/说明失败**并结束订房;**禁止**假设 roomId、禁止无合法 `roomId` 时调用 `room add` / `event create --rooms` 试探、禁止用 `event get` 等绕路推断 roomId
- **评测 / 自动化断言**:凡涉及 `room add` / `event create --rooms` 的流程,`--rooms` 只能填上游 `room search`(或等价接口)返回 JSON 中的 **`rooms[].roomId`**;不得以会议室展示名、楼层文案或用户口语当作 `roomId`
- **附件**:`attachment add` 仅负责挂载,**不上传**文件;fileId 必须先通过钉盘流程取得;`--files` 多附件用 `<fileId>:<name>` 元素逗号分隔
- **日历本**:`book list` 返回的 `id` 才是合法 `calendarId`;如无明确说明,`event list` / `event get` 都不要带 `--calendar-id`,让接口默认走 primary 主日历
- **日历本**:`book list` 返回的 `calendarId` 字段才是合法日历 id(无 `id` 字段);如无明确说明,`event list` / `event get` 都不要带 `--calendar-id`,让接口默认走 primary 主日历
- **已弃用入参**:`--max-results`(event list)在新 MCP schema 中被移除;CLI 仍接受但**不会**透传到 MCP;模型生成命令时**禁止**继续使用
## 自动化脚本
+12 -12
View File
@@ -334,7 +334,7 @@ Flags:
#### 移除用户的指定群身份 — 从用户身上移除指定的群身份(不影响其他群身份)
> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。需要清除某人身份时,用 `chat group-role set-user --role-ids ""`(传空 role-ids 覆盖为无身份)兜底;但 set-user 会清掉该用户的**全部**身份,无法只移除其中一个。
> ⚠️ 当前不可用:该命令当前调用常失败(服务端返回 1002 系统繁忙),多为服务端侧限制。需要清除某人身份时,用 `chat group-role set-user --role-ids ""`(传空 role-ids 覆盖为无身份)兜底;但 set-user 会清掉该用户的**全部**身份,无法只移除其中一个。
```
Usage:
dws chat group-role remove-user [flags]
@@ -729,7 +729,7 @@ Example:
dws chat message list-by-sender --sender-open-dingtalk-id <openDingTalkId> --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor <nextCursor>
Flags:
--sender-user-id string 发送者 userId(与 --sender-open-dingtalk-id 二选一)
--sender-open-dingtalk-id string 发送者 openDingTalkId(与 --sender-user-id 二选一,适用于无法获取 userId 的场景)
--sender-open-dingtalk-id string 发送者 openDingTalkId(与 --sender-user-id 二选一);该路径当前返回 result:{}、success:null(疑似服务端问题),优先用 --sender-user-id
--start string 开始时间,ISO-8601 格式 (必填)
--end string 结束时间,ISO-8601 格式 (必填)
--limit int 每页返回数量(默认 50)
@@ -737,8 +737,8 @@ Flags:
注意:
- --sender-user-id 和 --sender-open-dingtalk-id 二者互斥,必须且只能指定其一:
- --sender-user-id 传 userId(企业内部应用常用)
- --sender-open-dingtalk-id 传 openDingTalkId(三方应用或跨组织场景常用,无法获取 userId 时使用)
- --sender-user-id 传 userId(企业内部应用常用,当前唯一可用路径)
- --sender-open-dingtalk-id 传 openDingTalkId(当前返回 result:{}、success:null(疑似服务端问题);即使只有 openDingTalkId,也建议先用 contact 换取 userId 后走 --sender-user-id)
- openDingTalkId 获取方式见下方「openDingTalkId 获取方式」小节
- 不需要指定单聊/群聊,返回结果自带会话类型标识
- 时间支持多种 ISO-8601 格式,如 "2026-03-10T00:00:00+08:00"、"2026-03-10 14:00:00"、"2026-03-10" 等
@@ -1025,20 +1025,20 @@ Flags:
Usage:
dws chat message download-media [flags]
Example:
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./photo.jpg
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/photo.jpg
Flags:
--type string 资源类型: mediaId (必填)
--resource-id string 资源 ID,mediaId 类型时为消息中的 mediaId 值 (必填)
--message-id string 消息 openMessageId (必填)
--open-conversation-id string 会话 openConversationId (必填)
--output string 本地保存路径,文件或目录 (必填)
--output string 本地保存路径 (必填),建议传完整文件路径(含文件名)
注意:
- resource-id 从 `dws chat message list` 返回的消息内容中获取 mediaId
- message-id 从 `dws chat message list` 返回的 openMessageId
- open-conversation-id 从 `dws chat search` 获取 openConversationId
- --output 如果指定目录,文件名会从下载 URL 中自动推断
- --output 建议传完整文件路径(含扩展名,如 ./photo.jpg);只传目录(如 ./downloads/)在旧版本会下载失败,请显式带上文件名
```
### search-common (搜索共同群)
@@ -1405,7 +1405,7 @@ Flags:
#### 关闭/开启 @所有人消息提醒 — 关闭或开启会话中 @所有人的消息通知
> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。
> ⚠️ 当前不可用:该命令当前调用常失败(服务端返回 1002 系统繁忙),多为服务端侧限制。命令本身参数合法,但调用不会生效。
```
Usage:
dws chat mute-at-all [flags]
@@ -1428,7 +1428,7 @@ Flags:
#### 关闭/开启红包消息提醒 — 关闭或开启会话中的红包消息通知
> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。
> ⚠️ 当前不可用:该命令当前调用常失败(服务端返回 1002 系统繁忙),多为服务端侧限制。命令本身参数合法,但调用不会生效。
```
Usage:
dws chat mute-red-envelope [flags]
@@ -1518,7 +1518,7 @@ Flags:
注意:
- 返回结果包含单聊和群聊,不区分会话类型
- --limit 范围 1-100,默认 100,上限 100;传入 >100 会报错拒绝,不会静默截断
- 分页当前不可用:真机 hasMore 恒为 false、nextCursor 恒为 null,本命令最多返回 100 条会话,无法用 --cursor 继续翻页取更多
- 分页当前不可用:当前 hasMore 恒为 false、nextCursor 恒为 null,本命令最多返回 100 条会话,无法用 --cursor 继续翻页取更多
- 与 list-top-conversations 的区别: 本命令返回全部会话(单聊+群聊),list-top-conversations 仅返回置顶会话
```
@@ -1608,7 +1608,7 @@ Flags:
#### 审批入群验证 — 通过、删除单个审核
真机当前仅 AuditApprove(通过)和 AuditDelete(删除)两个动作可用。
当前仅 AuditApprove(通过)和 AuditDelete(删除)两个动作可用。
```
Usage:
@@ -1626,7 +1626,7 @@ Flags:
--description string 审批说明(可选)
注意:
- status 真机仅支持 AuditApprove(通过) 和 AuditDelete(删除);AuditIgnore(忽略)、AuditRefuse(拒绝)、AuditBlock(拒绝且拉黑) 会被服务端拒绝报 unsupported audit status,属服务端限制
- status 仅支持 AuditApprove(通过) 和 AuditDelete(删除);AuditIgnore(忽略)、AuditRefuse(拒绝)、AuditBlock(拒绝且拉黑) 会被服务端拒绝报 unsupported audit status,属服务端限制
- record-id、applicant、inviter 可通过 dws chat group list-join-validations 查询获得
```
+2 -2
View File
@@ -213,7 +213,7 @@ Example:
Flags:
无
Notes:
- 无需参数,返回当前企业全部角色列表(labelId、labelName等)
- 无需参数,返回当前企业全部角色,按 `groupName` 分组,每组 `labels[]` 里每个角色是 `{labelId, name}`(字段名是 `name`,不是 `labelName`)
- 用于不知道准确角色名称时先浏览全部角色
- 典型场景:用户说“企业所有主管/查所有管理员/财务人员有哪些”→ 先 label list 浏览全部角色,匹配目标角色后 label list-members 获取成员
```
@@ -360,7 +360,7 @@ dws contact user dismission search --depts 123456,789012 --hide-retirement=false
| `user get-self/search` | `orgAuthEmail` | mail message send 的 --to/--cc (跨产品) |
| `user get-self/search` | `userId` | profile get 的 --staff-id |
| `user profile fields` | `fieldCode` | profile get 的 --fields |
| `label list` | `labelId` / `labelName` | `label get --names` 或 `label list-members --id` |
| `label list` | `labelId` / `name` | `label get --names` 或 `label list-members --id` |
| `label get` | `labelId` | `label list-members` 的 --id |
| `dept search/list-children` | `deptId` | dept get-info/list-children/list-members 的 --dept/--depts |
| `dept search/list-children` | `deptId` | dismission search 的 --depts |
+4 -7
View File
@@ -118,11 +118,8 @@ dws dev app robot disable --unified-app-id <unifiedAppId> --format json
## dev app credentials — 凭证
```bash
# 查询 clientId/clientSecret
# 查询 clientId/clientSecret(credentials 下只有 get 子命令)
dws dev app credentials get --unified-app-id <unifiedAppId> --format json
# 重置凭证(旧 secret 立即失效)
dws dev app credentials reset --unified-app-id <unifiedAppId> --yes --format json
```
---
@@ -147,8 +144,8 @@ dws dev connect --channel opencode \
dws dev connect --daemon \
--robot-client-id <clientId> --robot-client-secret <clientSecret>
# 查看/停止后台连接器
dws dev connect status --format json
# 查看/停止后台连接器(status/list 忽略全局 --format,出 JSON 用专属 --json)
dws dev connect status --json
dws dev connect stop
```
@@ -208,7 +205,7 @@ dws dev app version status --unified-app-id <unifiedAppId> --version-id
## 注意事项
- **`clientSecret` 只在 `robot result` 返回一次**,务必立即保存;遗失需走 `credentials reset` 重置
- **`clientSecret` 只在 `robot result` 返回一次**,务必立即保存;dws 只能 `credentials get` 读取,不支持重置,遗失后到开放平台控制台重置密钥
- 改配置后机器人不自动生效,需走 `version create → publish` 才上线
- `hermes`/`openclaw` 渠道走官方建联,`dws dev connect` 不代建机器人,会输出指引后退出
- 应用名在企业内唯一;`app list/get` 用 `--app-key` 过滤但不能定位单应用,定位单应用须用 `--unified-app-id`
+6 -6
View File
@@ -27,7 +27,7 @@ dws doc permission --help
### 搜索 / 遍历文件(已迁移,不再是 doc 子命令)
> **弃用提示**:`dws doc search` 和 `dws doc list` 已迁移到 `drive` / `wiki`。真机上这两条虽仍能跑,但每次都会打印弃用警告,请改用下面的命令,命令详情见 [drive.md](./drive.md)。
> **弃用提示**:`dws doc search` 和 `dws doc list` 已迁移到 `drive` / `wiki`。这两条虽仍能跑,但每次都会打印弃用警告,请改用下面的命令,命令详情见 [drive.md](./drive.md)。
| 旧命令(弃用) | 改用 | 场景 |
|--------------|------|------|
@@ -83,7 +83,7 @@ Flags:
### 创建其他类型文件 (表格/脑图/白板/多维表/画板/文件夹)
> **弃用提示**:`dws doc file create --type` 已弃用,真机每次执行都会打印 `deprecated, use 'dws wiki node create --type <type>'`。创建各类型文件节点改用 `dws wiki node create`(与 intent-guide 一致);创建普通文件夹用 `dws drive mkdir`。
> **弃用提示**:`dws doc file create --type` 已弃用,执行时会打印 `deprecated, use 'dws wiki node create --type <type>'`。创建各类型文件节点改用 `dws wiki node create`(与 intent-guide 一致);创建普通文件夹用 `dws drive mkdir`。
```
Usage:
@@ -133,7 +133,7 @@ Flags:
### 下载文件到本地(已迁移到 drive)
> **弃用提示**:`dws doc download` 已迁移到 `dws drive download`(真机执行 `doc download` 会打印弃用警告)。下载已有文件(PDF/图片/附件等非在线文档)改用:
> **弃用提示**:`dws doc download` 已迁移到 `dws drive download`(执行 `doc download` 会打印弃用警告)。下载已有文件(PDF/图片/附件等非在线文档)改用:
```
dws drive download --node <NODE_ID> --output ~/downloads/
@@ -261,11 +261,11 @@ Usage:
Example:
dws doc comment list --node <DOC_ID>
dws doc comment list --node <DOC_ID> --type inline --resolve-status unresolved
dws doc comment list --node <DOC_ID> --page-size 20 --next-token <TOKEN>
dws doc comment list --node <DOC_ID> --limit 20 --cursor <TOKEN>
Flags:
--node string 目标文档的标识,支持传入 URL 或 ID (必填)
--page-size int 每页返回的评论数量,默认 50,最大 50
--next-token string 分页游标,从上一次请求的返回结果中获取 (首次请求不传)
--limit int 每页返回的评论数量,默认 50,最大 50
--cursor string 分页游标,从上一次请求的返回结果中获取 (首次请求不传)
--type string 按评论类型过滤: global (全文评论) / inline (划词评论)
--resolve-status string 按解决状态过滤: resolved (已解决) / unresolved (未解决)
```
@@ -15,11 +15,11 @@ Usage:
Example:
dws doc comment list --node <DOC_ID>
dws doc comment list --node <DOC_ID> --type inline --resolve-status unresolved
dws doc comment list --node <DOC_ID> --page-size 20 --next-token <TOKEN>
dws doc comment list --node <DOC_ID> --limit 20 --cursor <TOKEN>
Flags:
--node string 目标文档的标识,支持传入 URL 或 ID (必填)
--page-size int 每页返回的评论数量
--next-token string 下一页 token,从上一次请求的返回结果中获取 (首次请求不传)
--limit int 每页返回的评论数量,默认 50,最大 50
--cursor string 分页游标,从上一次请求的返回结果中获取 (首次请求不传)
--type string 按评论类型过滤: global (全文评论) / inline (划词评论)
--resolve-status string 按解决状态过滤: resolved (已解决) / unresolved (未解决)
```
@@ -3,7 +3,7 @@
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令在真机上虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令:
> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令:
> - `doc download` → **`dws drive download`**(下载已有文件;在线文档导出 docx 仍走 `doc export`)
> - `doc folder create` → **`dws drive mkdir`**(个人空间/钉盘)或 **`dws wiki node create --workspace <id> --type folder`**(知识库内)
> - `doc upload` / `doc copy` / `doc move` / `doc rename` / `doc delete` 也在同一迁移方向(→ `drive`),命令用法以 `drive` 为准。
@@ -1,6 +1,6 @@
# doc list(已弃用 → drive list / wiki node list)
> **弃用提示**:`dws doc list` 已迁移,不再是 `doc` 子命令。真机上它虽仍能跑,但每次都会打印弃用警告:
> **弃用提示**:`dws doc list` 已迁移,不再是 `doc` 子命令。它虽仍能跑,但每次都会打印弃用警告:
> `'dws doc list' is deprecated, use 'dws drive list --workspace <workspaceId>' or 'dws wiki node list --workspace <workspaceId>'`。
> 遍历文件夹/知识库请改用下面的命令,命令详情见 [`../drive.md`](../drive.md)。
@@ -1,6 +1,6 @@
# doc search(已弃用 → drive search / wiki node search)
> **弃用提示**:`dws doc search` 已迁移,不再是 `doc` 子命令。真机上它虽仍能跑,但每次都会打印弃用警告:
> **弃用提示**:`dws doc search` 已迁移,不再是 `doc` 子命令。它虽仍能跑,但每次都会打印弃用警告:
> `'dws doc search' is deprecated, use 'dws drive search' or 'dws wiki node search --workspace <id>'`。
> 搜索文档/文件请改用下面的命令,命令详情见 [`../drive.md`](../drive.md)。
+1 -1
View File
@@ -514,7 +514,7 @@ dws drive copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --f
- 不传 `--space-id` 时默认使用「我的文件」空间
- 不传 `--folder` 时默认操作空间根目录
- `--folder` 只能使用父文件夹的 `dentryUuid`。不要把 `drive info` 返回的数字型 `dentryId` 当作父目录
- **`--limit` 最大值为 50**,禁止传入超过 50 的值(如 `--limit 100`)。用户要求超过 50 条时,应使用 `--limit 50` 配合 `--cursor` 分页查询
- **`--limit` 有效上限为 50**:CLI 不做本地校验,传 `--limit 100` 不会报错,但服务端每页最多返回 50 条,超出部分无效。用户要求超过 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`)
+9 -3
View File
@@ -214,7 +214,9 @@ Flags:
| 字段 | 类型 | 说明 |
|------|------|------|
| `message` | `object` | 邮件完整信息,包含主题、发件人、收件人、正文、附件等 |
| `message` | `object` | 邮件信息,含 subject/from/toRecipients/ccRecipients/markdownBody/conversationId/receivedDateTime/size 等 |
> **注意:** 即使邮件确有附件,`message get` 返回的 `message` 对象也**不含** `attachments`/`isRead`/`tags` 字段(实测 keys 仅上表所列这些)。要取附件请另用 `dws mail attachment list`。
### 发送邮件
```
@@ -1364,6 +1366,8 @@ Flags:
--actions string 规则动作 JSON 数组 (必填)
```
> **⚠️ 实测:** 尽管 `--help` 把 `--conditions` 标为(可选),服务端实际要求它**非空**——省略或传空会返回 `209 SYS_UNKNOWN_ERROR(1001)` 失败,帮助文本承诺的"命中所有邮件"无条件匹配**不可用**。create 请始终传入有效条件。
**返回字段:**
| 字段 | 类型 | 说明 |
@@ -1375,11 +1379,11 @@ Flags:
#### 更新收信规则
更新已有的收信规则。**除 `--conditions` 外所有参数均为必填**。
更新已有的收信规则。**所有参数(含 `--conditions`)均须传入有效值**——见下方警告。
> **建议工作流:** 先通过 `dws mail rule list` 获取当前规则的完整配置,再传入需要修改的字段值。
>
> `--conditions` 为空或不传表示命中所有邮件(无条件匹配)。`--actions` 格式同 create 命令。
> **⚠️ 实测:** `--help` 称 `--conditions` 为空即命中所有邮件,但该无条件匹配**不可用**——省略或传空会返回 `209 SYS_UNKNOWN_ERROR(1001)` 失败。update 请始终传入有效条件。`--actions` 格式同 create 命令。
```
Usage:
@@ -1399,6 +1403,8 @@ Flags:
--actions string 规则动作 JSON 数组 (必填)
```
> **⚠️ 实测:** 上方 `--conditions` 的"(可选,为空表示命中所有邮件)"是 `--help` 原文,但服务端实际要求它**非空**——省略或传空会返回 `209 SYS_UNKNOWN_ERROR(1001)` 失败。update 须传入有效条件。
**返回字段:**
| 字段 | 类型 | 说明 |
+5 -5
View File
@@ -26,7 +26,7 @@ dws minutes
│ ├── shared [--query] [--start] [--end] [--max] [--next-token] # 仅查询他人共享给我的听记(不含我自己创建的)
│ └── all [--query] [--start] [--end] [--max] [--next-token] # 查询我可访问的所有听记(= mine ∪ shared,覆盖面最广,默认 scope)
├── get <subcommand> # 获取听记详情(get 后必须跟子命令名)
│ ├── info --id <uuid> # 获取听记基础信息(标题、时长、参与人、状态等)
│ ├── info --id <uuid> # 获取听记基础信息(标题、时长、开始时间、taskUuid、访问链接;不含参会人)
│ ├── summary --id <uuid> # 获取听记 AI 摘要(结构化纪要)
│ ├── transcription --id <uuid> # 获取听记语音转写原文(逐字稿,支持分页)
│ ├── keywords --id <uuid> # 获取听记关键词
@@ -618,7 +618,7 @@ AI 基于已拉取的转写数据,**本地完成**聚类与摘要(无需新
> **铁律 2:严禁用 AI 摘要里的"参与人/参会人"字段判断某人是否参会**
> - AI 摘要中的"参与人"字段往往**只截取最显著的 1-2 个名字**(通常是创建人或发言最多的人),**不是完整参会人列表**
> - "AI 摘要参与人只有故愚 → 灵麦没参会" 是典型的错误推理链
> - 如果一定要列出参会人,应使用 `dws minutes get info` / `get batch` 返回的 `participants` 字段,而不是摘要文本里的描述
> - 如果一定要列出参会人,只能从 `get transcription` 的转写发言人(`speakerName`/`speakerId`)汇总——`get info` / `get batch` **均不返回** `participants` 结构化字段(get info 只返回 duration/startTime/taskUuid/title/url,get batch 只返回 creatorNick/creatorUnionId/status 等),别指望从它们拿参会名单
>
> **铁律 3:通讯录查询是 Step 4 的强制起点,不依赖 Step 3 是否成功**
> - Step 2 一旦返回"未命中真名标注",**Step 3 与 Step 4 ① 必须并发触发**,不要等 Step 3 失败再补查
@@ -667,7 +667,7 @@ Step 7: 引导用户替换发言人(调用 speaker replace 写回听记)
> - **未命中真名标注 ≠ 目标人物没参会**——绝大多数听记的发言人都是匿名编号,"未命中"是**默认场景**,恰恰是发言人识别功能要解决的问题
> - **不要在转写文本里 grep 目标人名**作为存在性判断——花名/真名通常不会出现在 TA 自己的发言里(参见铁律 1)
> - **不要把 `dws minutes get summary` 摘要文本里写到的"参与人"当作完整参会列表**——AI 摘要里的"参与人"字段只截取最显著的 1-2 个名字,不是完整名册(参见铁律 2)
> - 唯一能下"目标人物没参会"结论的场景是:`get info` / `get batch` 返回的结构化 `participants` 字段里**完整列出参会人且不含目标**,且 `dws contact user search` 也搜不到该人 → 才允许告知用户"该人不在参会列表"
> - 听记各接口都拿不到权威的完整参会名册:`get info` / `get batch` **均不返回** `participants` 字段,转写发言人列表又常是匿名编号。因此**几乎无法**仅凭听记数据下"目标人物没参会"的结论;只有当转写发言人已全部实名标注且不含目标、且 `dws contact user search` 也搜不到该人时,才可谨慎告知"未在可见发言人中找到该人"
##### Step 3: 转写原文内推断(优先)
@@ -813,7 +813,7 @@ Flags:
--ids string 听记 taskUuid 列表,逗号分隔 (必填)
```
返回字段: 听记标题、时长、参与人列表、创建时间、taskUuid、听记状态
返回字段: `result.minutesDetails` 数组,每项含 `title`(标题)、`durationMicros`(时长, 微秒)、`startTime`(开始时间, 毫秒)、`taskUuid`、`status`(状态)、`creatorNick`(创建人昵称)、`creatorUnionId`、`size`、`bizType`、`isDeleted`。**不返回参会人列表**。
### 修改听记标题
```
@@ -2299,7 +2299,7 @@ Step 2 **[禁止]** 看到全是匿名编号 → 没有继续走 Step 3-4,反
1. **"在转写里搜不到目标人名"绝不构成"没参会"的证据**:花名/真名通常不出现在 TA 自己的发言里,这是默认场景而非例外。99% 的听记发言人都是匿名编号——这正是发言人识别功能要解决的问题。
2. **AI 摘要的"参与人"字段是低保真信号,不能作为参会判断依据**:`get summary` 返回的是 AI 生成的自然语言摘要,里面提到的"参与人"通常只是最显著的 1-2 人;要拿完整参会列表,应使用 `get info` / `get batch` 返回的结构化 `participants` 字段。
2. **AI 摘要的"参与人"字段是低保真信号,不能作为参会判断依据**:`get summary` 返回的是 AI 生成的自然语言摘要,里面提到的"参与人"通常只是最显著的 1-2 人。而 `get info` / `get batch` **均不返回**结构化 `participants` 字段,无法从中拿完整参会列表;能拿到的只有 `get transcription` 的转写发言人(常为匿名编号)。
3. **`dws contact user search` 是 Step 4 ① 的必跑项,单次调用就能突破死局**:本案例的整个失败链路只要有 1 次通讯录查询就能立即扭转——拿到"木兰=王佳明,产品设计部"后,Step 5 的角色匹配就有了锚点,再也不会得出"没参会"的错误结论。
+4 -3
View File
@@ -314,7 +314,7 @@ Flags:
用户说"拒绝审批/驳回" → 先 `tasks` 获取 taskId,再 `reject`
用户说"撤回审批/取消审批" → `approval revoke`
用户说"审批记录/操作历史" → `approval records`
用户说"我发起的审批" → `approval list-initiated`(需 --process-code,可从 list-forms 或 detail 获取)
用户说"我发起的审批" → `approval list-initiated`(需 --process-code,可从 list-forms / search-forms / detail 获取)
用户说"有哪些审批表单/可见表单" → `approval list-forms`
用户说"我有哪些待审的任务" → `approval tasks`
用户说"我发起的审批单" -> `approval list-submitted`
@@ -351,7 +351,7 @@ dws oa approval records --instance-id <processInstanceId> --format json
# 7. 获取可见审批表单(得到 processCode)
dws oa approval list-forms --cursor 0 --limit 100 --format json
# 8. 查看自己发起的审批列表(--process-code 来自 list-forms 或 detail)
# 8. 查看自己发起的审批列表(--process-code 来自 list-forms / search-forms / detail)
dws oa approval list-initiated --process-code <code> \
--start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" \
--cursor 0 --limit 20 --format json
@@ -383,6 +383,7 @@ dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "userId1
| `tasks` | `taskId` | approve / reject / redirect-task 的 --task-id |
| `detail` | `processCode` | list-initiated 的 --process-code |
| `list-forms` | `processCode` | list-initiated 的 --process-code |
| `search-forms` | `processCode` | list-initiated 的 --process-code |
## 注意事项
@@ -391,7 +392,7 @@ dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "userId1
- `redirect-task` 的 `--to-actioner-id` 可通过 `dws contact user search` 获取目标用户 userId
- `revoke` 只能撤销自己发起的审批
- `--remark` 审批意见虽为可选,但建议填写以留存审批痕迹
- `list-initiated` 的 `--process-code` 可从 `list-forms` 或 `detail` 返回中提取
- `list-initiated` 的 `--process-code` 可从 `list-forms`、`search-forms` 或 `detail` 返回中提取。当 `list-forms` 返回 `processCodeList` 为空(`totalCount -1`)时,用 `search-forms --query <表单名>`(如 `--query 报销`)按名称精准拿 `processCode` 更稳
- `list-initiated` 的 `--start` / `--end` 区间有后端上限(约 120 天)。超过上限会返回误导性的 `business_error: 时间戳无效`(实为区间过长,不是时间格式问题)。跨度大时请拆成多段短区间分别查询
## 自动化脚本
+2 -2
View File
@@ -1581,7 +1581,7 @@ Flags:
- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试
- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试
**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
**限制**:仅支持钉钉在线电子表格(axls)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
### 表格模板(list / search / apply)
```
@@ -2067,7 +2067,7 @@ dws sheet export --node <NODE_ID> --output ./
- `filter-view delete-criteria` 仅清除指定列的条件,不会删除整个筛选视图。如需删除整个筛选视图,请使用 `filter-view delete`
- `filter-view delete-criteria` 如果指定列没有设置筛选条件,调用不会报错
- 筛选视图相关操作需要"可阅读"权限(list / info / list-criteria / get-criteria)或"可编辑"权限(create / update / delete / update-criteria / delete-criteria),不支持跨组织操作
- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
- ★ `export` 仅支持钉钉在线电子表格(axls)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令
- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可
- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试
@@ -56,7 +56,7 @@ Flags:
- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试
- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试
**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
**限制**:仅支持钉钉在线电子表格(axls)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
## 核心工作流
@@ -86,7 +86,7 @@ dws sheet export --node <NODE_ID> --output ./
## 注意事项
- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
- ★ `export` 仅支持钉钉在线电子表格(axls)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令
- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可
- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试
+7 -7
View File
@@ -90,14 +90,14 @@ Step 3 → 按下方路由规则映射到对应产品
| `contentType=ALIDOC`, `extension=adoc` | `doc` | 按 [doc.md](./products/doc.md) 操作 |
| `contentType=ALIDOC`, `extension=axls` | `sheet` | 按 [sheet.md](./products/sheet.md) 操作(仅 `axls` 在线电子表格) |
| `contentType=ALIDOC`, `extension=able` | `aitable` | 将 nodeId 作为 baseId,按 [aitable.md](./products/aitable.md) 操作 |
| `contentType=DOCUMENT`, `extension=xlsx` / `xls` / `xlsm` / `csv` | `doc` | 必须用 `dws doc download` 下载到本地处理,禁止走 `sheet`(非在线表格,sheet 命令无法操作) |
| `contentType≠ALIDOC`, `nodeType=file` | `doc` | 调用 `dws doc download` 下载,返回文件下载链接 |
| `contentType=DOCUMENT`, `extension=xlsx` / `xls` / `xlsm` / `csv` | `doc` | 必须用 `dws drive download` 下载到本地处理,禁止走 `sheet`(非在线表格,sheet 命令无法操作) |
| `contentType≠ALIDOC`, `nodeType=file` | `doc` | 调用 `dws drive download` 下载,返回文件下载链接 |
| `nodeType=folder` | `doc` | 调用 `dws doc list --folder <ID>` 列出指定文件夹直接子节点列表 |
| 以上均不匹配 | — | 告知用户当前暂不支持该类型 |
> axls vs xlsx 关键区分:
> - `axls`(钉钉在线电子表格,`contentType=ALIDOC`)→ 走 `sheet` 产品线(读/写/筛选/导出等服务端原子操作)
> - `xlsx` / `xls` / `xlsm` / `csv`(上传到文档空间的本地表格文件,`contentType=DOCUMENT`)→ 必须走 `dws doc download` 下载到本地后再解析处理,严禁错误路由到 `sheet` 产品线(sheet 命令只支持在线表格,调用 xlsx 节点会直接报错)
> - `xlsx` / `xls` / `xlsm` / `csv`(上传到文档空间的本地表格文件,`contentType=DOCUMENT`)→ 必须走 `dws drive download` 下载到本地后再解析处理,严禁错误路由到 `sheet` 产品线(sheet 命令只支持在线表格,调用 xlsx 节点会直接报错)
> - 用户想把在线表格导出为 xlsx 文件 → 开源 dws CLI 暂未暴露在线表格导出能力(旧动态 schema 曾包含 `submit_export_job` / `query_export_job`,但当前 cobra 未注册),需要在钉钉客户端手动导出 xlsx
### 示例
@@ -110,10 +110,10 @@ dws doc info --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
# 返回 contentType≠ALIDOC, extension=xlsx/xls/csv → 本地表格文件,必须下载处理(禁止走 sheet)
dws doc download --node "https://alidocs.dingtalk.com/i/nodes/xlsx456"
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/xlsx456"
# 返回 contentType≠ALIDOC, nodeType=file → 普通文件,下载
dws doc download --node "https://alidocs.dingtalk.com/i/nodes/def456"
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/def456"
# 返回 nodeType=folder → 文件夹,列出子节点
dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/ghi789" --format json
@@ -138,8 +138,8 @@ dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/ghi789" --format jso
| **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 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 *` | 不适用 |
| **xlsx / xls / xlsm / csv**(本地表格文件) | `drive download` → 本地用 xlsx skill 解析 | 不支持服务端写(先下载改本地再上传) | ⚠️ `doc delete`(节点删除) | 不需要(本身就是 xlsx) | ⚠️ `doc permission *` | 不适用 |
| **普通文件** (nodeType=file) | `drive download` | 不支持服务端写 | ⚠️ `doc delete` | 不需要 | ⚠️ `doc permission *` | 不适用 |
| **文件夹** (nodeType=folder) | `doc list --folder <URL>` | `doc create --folder <URL> ...` | ⚠️ `doc delete` | 不适用 | ⚠️ `doc permission *` | 不适用 |
| **分享短链** `/i/p/<short>` | `read_url` 兜底(外部工具) | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
@@ -100,7 +100,7 @@ def main():
event_id = None
if not args.dry_run and isinstance(result, dict):
# event create 真机返回 {result:{id,...}, success:true},id 在 result 内层,
# 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')
+3 -3
View File
@@ -70,7 +70,7 @@ def main():
print('未找到匹配部门')
sys.exit(1)
# dept search 真机返回顶层 deptList;兼容 result 包裹与历史 items/result 键。
# dept search 返回顶层 deptList;兼容 result 包裹与历史 items/result 键。
if isinstance(dept_data, list):
depts = dept_data
else:
@@ -104,7 +104,7 @@ def main():
print(' 无法获取成员列表')
continue
# list-members 真机返回 deptUserList;兼容 result 包裹与历史 userlist 键。
# list-members 返回 deptUserList;兼容 result 包裹与历史 userlist 键。
if isinstance(members_data, list):
members = members_data
else:
@@ -121,7 +121,7 @@ def main():
continue
for m in members:
# list-members 真机每项形如 {"userInfo": {"name":..., "userId":...}},
# list-members 每项形如 {"userInfo": {"name":..., "userId":...}},
# 成员字段嵌在 userInfo 下;兼容历史扁平结构。
info = m.get('userInfo', m)
name = info.get('name') or info.get('userName', '未知')
+54 -14
View File
@@ -44,6 +44,58 @@ def run_dws(
return None
def todos_from_payload(payload: Any) -> List[dict]:
"""解析 `minutes get todos` 返回为待办列表。
返回结构: result.dingtalkTodoList(对象数组, 含 title 等)与
result.actions(JSON 字符串数组, 每条形如
{"mark":[],"value":"..."}). 二者无 todos 键。
优先取 dingtalkTodoList; 为空时回退解析 actions。
"""
if isinstance(payload, dict):
inner = payload.get('result', payload)
else:
inner = payload
out: List[dict] = []
if isinstance(inner, dict):
ding_list = inner.get('dingtalkTodoList')
if isinstance(ding_list, list) and ding_list:
for t in ding_list:
if isinstance(t, dict):
content = t.get('title') or t.get('content') or ''
if content:
out.append({'content': str(content), '_raw': t})
if out:
return out
actions = inner.get('actions')
if isinstance(actions, list):
for a in actions:
content = ''
if isinstance(a, str):
text = a.strip()
if text.startswith('{'):
try:
parsed = json.loads(text)
content = parsed.get('value') or ''
except json.JSONDecodeError:
content = text
else:
content = text
elif isinstance(a, dict):
content = (a.get('value') or a.get('content')
or a.get('title') or '')
if content:
out.append({'content': str(content)})
elif isinstance(inner, list):
for t in inner:
if isinstance(t, dict):
content = (t.get('content') or t.get('text')
or t.get('title') or t.get('value') or '')
if content:
out.append({'content': str(content), '_raw': t})
return out
def main():
parser = argparse.ArgumentParser(
description='从听记中提取待办事项'
@@ -82,21 +134,9 @@ def main():
])
if not todos_data:
continue
if isinstance(todos_data, list):
items = todos_data
elif isinstance(todos_data, dict):
inner = todos_data.get('result', todos_data)
if isinstance(inner, dict):
items = inner.get('todos', [])
elif isinstance(inner, list):
items = inner
else:
items = []
else:
items = []
items = todos_from_payload(todos_data)
for t in items:
if isinstance(t, dict):
t['_source'] = title
t['_source'] = title
all_todos.extend(items)
print(f"\n📋 听记待办汇总")
@@ -98,7 +98,7 @@ Flags:
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `aisearch person` | `userId`(用户ID)、`title`(姓名) | 展示搜索结果、后续操作(发消息/建待办等) |
| `aisearch person` | `userId`(用户ID,= `meta.staffId`/`meta.jobNumber`)、`author`(真实姓名,= `meta.name`);`title` 是花名/显示名,不一定等于姓名 | 展示搜索结果、后续操作(发消息/建待办等) |
## 重名消歧
@@ -101,6 +101,17 @@
> **创建表单**有两种等价方式:`form create --name "..."`(推荐)或 `view create --view-type FormDesigner --name "..."`。
### workflow (自动化工作流) → 详见 [aitable-workflow.md](./aitable/aitable-workflow.md)
| 命令 | 用途 | 必填参数 | 路由提醒 |
|------|------|----------|----------|
| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` |
| `workflow get` | 获取单个工作流详情(含 flowSchema) | `--base-id` `--workflow-id` | `--workflow-id` 接受 list 里的 `flowId`(同值) |
| `workflow enable` | 启用工作流 | `--base-id` `--workflow-id` | 返回 `{enabled: true}` 是动作确认;要确认真启用看 list 的 `status` |
| `workflow disable` | 禁用工作流(高危) | `--base-id` `--workflow-id` `--yes` | 影响业务自动化,建议二次确认;status 变 STOP |
> **当前不支持通过 CLI 新建/修改/删除工作流**,请去 AI 表格 Web 端(数据表页面 → 自动化)配置。
### dashboard & chart → 详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)
| 命令 | 用途 |
@@ -130,6 +141,35 @@
|------|------|----------|
| `template search` | 搜索模板 | `--query` |
### advperm (高级权限/自定义角色) → 详见 [aitable-advperm.md](./aitable/aitable-advperm.md)
| 命令 | 用途 | 必填参数 | 路由提醒 |
|------|------|----------|----------|
| `advperm enable` | 开启 Base 高级权限总开关 | `--base-id` | 不开启时角色规则不生效 |
| `advperm disable` | 关闭 Base 高级权限总开关(高危) | `--base-id` `--yes` | 关闭后全员回退默认权限 |
| `advperm role-list` | 列出 Base 下所有角色 | `--base-id` | 同时返回自定义角色和系统角色;`roleType == "custom"` 是自定义,前缀 `system_` 是系统角色 |
| `advperm role-get` | 获取单角色完整配置 | `--base-id` `--role-id` | 含 subRoles 与字段/行级规则 |
| `advperm role-create` | 创建自定义角色 | `--base-id` `--name` | 可选 `--sub-roles` 同时指定子角色权限规则 |
| `advperm role-update` | 增量更新自定义角色(PATCH) | `--base-id` `--role-id` | 未传字段不变;`--sub-roles` 按 (targetId,targetType) 合并 |
| `advperm role-delete` | 删除自定义角色 | `--base-id` `--role-id` `--yes` | 不可逆;系统角色禁删;**调用者必须是该 AI 表格的管理员/Owner**,非管理员会得到 401 AUTH_ERROR |
> 所有写命令(enable/disable/role-create/role-update/role-delete)需要 Base 管理员权限;非管理员只能调 `role-list` / `role-get`(只读)。
> "角色 ↔ 成员"绑定当前 CLI 不支持,仍需在 AI 表格 Web 端 → Base 设置 → 高级权限面板手动完成。
### section (文件夹与节点管理)
用于在 Base 的导航树中组织 table / dashboard / 表单视图 / 文档等节点(类似文件夹)。操作前建议先用 `section list-nodes` 拿到 nodeId / sectionId 与父级关系。
| 命令 | 用途 | 必填参数 | 路由提醒 |
|------|------|----------|----------|
| `section list-nodes` | 列出 Base 下全部节点 | `--base-id` | 返回 `{nodeId, nodeType, parentSectionId, name?}`;是 move-node / reorder 的前置定位命令 |
| `section list-empty` | 列出空文件夹 | `--base-id` | 返回 `{sectionId, name, parentSectionId}`,用于清理导航树 |
| `section create` | 创建文件夹 | `--base-id` `--name` | `--parent-section-id` 不传/空串=根目录;`--index` 指定位置 |
| `section rename` | 重命名文件夹 | `--base-id` `--section-id` `--new-name` | — |
| `section delete` | 删除文件夹 | `--base-id` `--section-id` | 不可逆;建议先 list-empty 确认为空 |
| `section reorder` | 调整文件夹顺序 | `--base-id` `--section-id` `--target-index` | 仅当前父级下调序;跨父级用 move-node |
| `section move-node` | 移动节点(跨父级) | `--base-id` `--node-id` `--new-parent-section-id` | `--new-parent-section-id ""`=移到根;`--target-index` 调全局位置 |
## 意图判断
用户说"表格/多维表/AI表格":
@@ -31,7 +31,7 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
| `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]"` |
| `dashboard share update` | 更新仪表盘分享配置 | `--base-id` `--dashboard-id` `--enabled` | `--enabled true` 开启,`--share-type` 用 `ORG`(ORG 回显 `shareType="[1]"`);部分组织禁用了 `PUBLIC` 公开分享;若报 `Illegal argument`,改用 `ORG`(组织内分享)。`--enabled false` 关闭 |
## chart 子命令
@@ -43,7 +43,7 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
| `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 share update` | 更新图表分享配置(`--enabled true/false`,开启配 `--share-type`,用 `ORG`;部分组织禁用了 `PUBLIC` 公开分享;报 `Illegal argument` 时改用 `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,否则服务端拒绝。
+1 -1
View File
@@ -39,7 +39,7 @@ metadata:
## 评测高频硬约束
- 当前 dws 已注册全部考勤子命令组(`record` / `check` / `approve` / `shift` / `schedule` / `class` / `adjustment` / `overtime` / `group` / `summary` / `rules` / `selfsetting` / `globalsetting` / `vacation` / `checkin` / `report` / `boss-check`),查询与写操作大多可直接调用后端。**不要再以"开源版只读/不支持写操作"为由拒答。** 创建班次、导入排班、加人入考勤组、保存个人规则设置、设置假期余额等写操作真机可执行,但必须先展示参数摘要并二次确认,再追加 `--yes`(或 `--user-say-yes`)执行;不要在未确认时直接写、也不要伪装成功。个别命令受权限/数据影响返回空或权限错误(如 `report` 系列仅管理员),如实说明即可。
- 当前 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。
@@ -55,6 +55,19 @@ Flags:
返回每条记录含:用户 ID、实际打卡时间、打卡地址、打卡经纬度、打卡类型(OnDuty/OffDuty)、定位方式(Map/Wifi/etc)。时间跨度不超过 1 个月。
### 查询个人某日考勤详情
```
Usage:
dws attendance record get [flags]
Example:
dws attendance record get --user 011769261608 --date 2026-03-08
Flags:
--user string 钉钉用户 ID (必填,别名 --users 亦可)
--date string 查询日期, 格式 YYYY-MM-DD (必填)
```
查询单个用户在**某一天**的考勤详情(区别于 `check record`/`check result` 的多人时间段批量查询)。返回 `result` 对象含 `isHasSchedule`(当日是否有排班)、`isRest`(是否休息日)、`isUnSigned`(是否未打卡)、`recordList`(打卡明细)、`approveList`(当日审批单)、`workOvertime`、`workTimeDesc` 等字段。`--user` 只接受**单个** userId;查 userId 用 `dws contact user search --query "姓名"`。
### 查询审批单
```
Usage:
@@ -1115,13 +1128,13 @@ dws attendance report query-leave --users userId1,userId2 \
dws attendance vacation types --format json
# 查看指定员工假期余额
dws attendance vacation balance --users userId1,userId2 --format json
dws attendance vacation balance --users userId1,userId2 --leave-code <假期类型code> --format json
# 查看指定员工某类假期余额
dws attendance vacation balance --users userId1 --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890 --format json
# 查看指定员工假期余额变更记录
dws attendance vacation records --user USER_ID --start 2026-04-01 --end 2026-04-22 --format json
dws attendance vacation records --user USER_ID --leave-code <假期类型code> --start 2026-04-01 --end 2026-04-22 --format json
# 更新假期规则名称
dws attendance vacation update-type --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
@@ -5,7 +5,7 @@
- **二级子命令(必选其一)**:`event`(日程)、`attendee`(参会人)、`room`(会议室)、`busy`(闲忙)、`attachment`(日程附件)、`book`(用户日历列表)。`dws calendar` 后**必须**紧跟上述之一;**禁止**只执行 `dws calendar`(无子命令)。
- **个人日程 / 给自己留时间块 / 专注时段**:统一走 **`dws calendar event create`**。当前**没有**单独的 `personal schedule create` / `calendar create` 命令。
- **查日程列表**:`dws calendar event list --start "<ISO-8601>" --end "<ISO-8601>" --format json`,或优先使用脚本 `python scripts/calendar_today_agenda.py [today|tomorrow|week]`(见文末「自动化脚本」)。
- **查用户日历本列表**:`dws calendar book list`(返回主日历 `id == "primary"` 等)。**重要**可以查询他人共享给自己的日历本,根据日历本id可以进一步查询对方的日程信息。
- **查用户日历本列表**:`dws calendar book list`(返回字段为 `calendarId`,主日历 `calendarId == "primary"` 等;注意无 `id` 字段,按 `.id` 提取会取空)。**重要**可以查询他人共享给自己的日历本,根据日历本id可以进一步查询对方的日程信息。
- **CLI 不存在**独立的 `dws calendar list`;若误跑无子命令的 `dws calendar`,会打印整段 Usage,**切勿**将该段 help 当作工具结果再次塞进对话(会急剧增加 token 与首字延迟)。
- **必须**遵循指令说明进行调用。**绝对禁止**使用虚构指令,使用虚构参数。
@@ -511,7 +511,7 @@ dws calendar event list --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15
| `event respond` | 响应结果 | — |
| `room search` | `rooms[].roomId` | room add 的 --rooms 或 event create 的 --rooms |
| `room list-groups` | `groups[].groupId` | room search 的 --group-id |
| `book list` | `id`(如 `primary`) | event list/get 的 --calendar-id |
| `book list` | `calendarId`(如 `primary`) | event list/get 的 --calendar-id |
| 钉盘上传 | 文件 `fileId` | attachment add 的 --files `<fileId>:<name>` |
## 注意事项
@@ -529,7 +529,7 @@ dws calendar event list --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15
- **搜房无结果**:在符合早停/用户限定范围内,`room search`(含按分组逐组查)全部返回空或无空闲 → 应**直接向用户报错/说明失败**并结束订房;**禁止**假设 roomId、禁止无合法 `roomId` 时调用 `room add` / `event create --rooms` 试探、禁止用 `event get` 等绕路推断 roomId
- **评测 / 自动化断言**:凡涉及 `room add` / `event create --rooms` 的流程,`--rooms` 只能填上游 `room search`(或等价接口)返回 JSON 中的 **`rooms[].roomId`**;不得以会议室展示名、楼层文案或用户口语当作 `roomId`
- **附件**:`attachment add` 仅负责挂载,**不上传**文件;fileId 必须先通过钉盘流程取得;`--files` 多附件用 `<fileId>:<name>` 元素逗号分隔
- **日历本**:`book list` 返回的 `id` 才是合法 `calendarId`;如无明确说明,`event list` / `event get` 都不要带 `--calendar-id`,让接口默认走 primary 主日历
- **日历本**:`book list` 返回的 `calendarId` 字段才是合法日历 id(无 `id` 字段);如无明确说明,`event list` / `event get` 都不要带 `--calendar-id`,让接口默认走 primary 主日历
- **已弃用入参**:`--max-results`(event list)在新 MCP schema 中被移除;CLI 仍接受但**不会**透传到 MCP;模型生成命令时**禁止**继续使用
## 自动化脚本
@@ -51,30 +51,51 @@ def fmt_iso(dt: datetime) -> str:
return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00')
def _extract_time_str(value: Any) -> str:
"""时间字段兼容字符串与 {dateTime, date} 对象两种形态"""
if isinstance(value, dict):
return value.get('dateTime') or value.get('date') or ''
return value or ''
def parse_busy_intervals(
data: Any,
) -> List[Tuple[datetime, datetime]]:
intervals = []
if not data:
return intervals
items = []
# 兼容 {result: [...]} 包裹结构
if isinstance(data, dict) and 'result' in data:
data = data['result']
rows = []
if isinstance(data, list):
items = data
rows = data
elif isinstance(data, dict):
inner = data.get('result', data)
if isinstance(inner, list):
items = inner
elif isinstance(inner, dict):
for user_data in inner.values():
if isinstance(user_data, list):
items.extend(user_data)
elif isinstance(user_data, dict):
items.extend(
user_data.get('busyTimes', [])
)
rows = list(data.values())
items = []
for row in rows:
if isinstance(row, list):
items.extend(row)
elif isinstance(row, dict):
# 新结构: {userId, scheduleItems: [{start, end, status}]}
if isinstance(row.get('scheduleItems'), list):
items.extend(row['scheduleItems'])
elif isinstance(row.get('busyTimes'), list):
items.extend(row['busyTimes'])
elif row.get('startTime') or row.get('start'):
items.append(row)
for item in items:
start_str = item.get('startTime') or item.get('start', '')
end_str = item.get('endTime') or item.get('end', '')
if not isinstance(item, dict):
continue
# FREE 时段不算忙
if str(item.get('status') or '').upper() == 'FREE':
continue
start_str = _extract_time_str(
item.get('startTime') or item.get('start')
)
end_str = _extract_time_str(
item.get('endTime') or item.get('end')
)
if not start_str or not end_str:
continue
for fmt in (
@@ -172,6 +193,16 @@ def main():
if args.dry_run:
return
# 查询失败必须中止, 否则会把"查不到"当成"全天空闲"给出危险结论
if data is None:
print('错误:忙闲查询失败,无法给出空闲时段结论', file=sys.stderr)
sys.exit(2)
if isinstance(data, dict) and data.get('success') is False:
print(f"错误:忙闲查询失败: "
f"{data.get('errorMsg') or data.get('errorCode') or data}",
file=sys.stderr)
sys.exit(2)
busy = parse_busy_intervals(data)
free = find_free_slots(day_start, day_end, busy, args.duration)
@@ -181,6 +212,7 @@ def main():
print(f" 会议时长: {args.duration} 分钟")
print(f" 工作时间: {args.start_hour}:00 ~ "
f"{args.end_hour}:00")
print(f" 已识别忙时段: {len(busy)} 个")
print('=' * 50)
if not free:
+12 -12
View File
@@ -336,7 +336,7 @@ Flags:
#### 移除用户的指定群身份 — 从用户身上移除指定的群身份(不影响其他群身份)
> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。需要清除某人身份时,用 `chat group-role set-user --role-ids ""`(传空 role-ids 覆盖为无身份)兜底;但 set-user 会清掉该用户的**全部**身份,无法只移除其中一个。
> ⚠️ 当前不可用:该命令当前调用常失败(服务端返回 1002 系统繁忙),多为服务端侧限制。需要清除某人身份时,用 `chat group-role set-user --role-ids ""`(传空 role-ids 覆盖为无身份)兜底;但 set-user 会清掉该用户的**全部**身份,无法只移除其中一个。
```
Usage:
dws chat group-role remove-user [flags]
@@ -694,7 +694,7 @@ Example:
dws chat message list-by-sender --sender-open-dingtalk-id <openDingTalkId> --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor <nextCursor>
Flags:
--sender-user-id string 发送者 userId(与 --sender-open-dingtalk-id 二选一)
--sender-open-dingtalk-id string 发送者 openDingTalkId(与 --sender-user-id 二选一,适用于无法获取 userId 的场景)
--sender-open-dingtalk-id string 发送者 openDingTalkId(与 --sender-user-id 二选一);该路径当前返回 result:{}、success:null(疑似服务端问题),优先用 --sender-user-id
--start string 开始时间,ISO-8601 格式 (必填)
--end string 结束时间,ISO-8601 格式 (必填)
--limit int 每页返回数量(默认 50)
@@ -702,8 +702,8 @@ Flags:
注意:
- --sender-user-id 和 --sender-open-dingtalk-id 二者互斥,必须且只能指定其一:
- --sender-user-id 传 userId(企业内部应用常用)
- --sender-open-dingtalk-id 传 openDingTalkId(三方应用或跨组织场景常用,无法获取 userId 时使用)
- --sender-user-id 传 userId(企业内部应用常用,当前唯一可用路径)
- --sender-open-dingtalk-id 传 openDingTalkId(当前返回 result:{}、success:null(疑似服务端问题);即使只有 openDingTalkId,也建议先用 contact 换取 userId 后走 --sender-user-id)
- openDingTalkId 获取方式见下方「openDingTalkId 获取方式」小节
- 不需要指定单聊/群聊,返回结果自带会话类型标识
- 时间支持多种 ISO-8601 格式,如 "2026-03-10T00:00:00+08:00"、"2026-03-10 14:00:00"、"2026-03-10" 等
@@ -990,20 +990,20 @@ Flags:
Usage:
dws chat message download-media [flags]
Example:
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./photo.jpg
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/photo.jpg
Flags:
--type string 资源类型: mediaId (必填)
--resource-id string 资源 ID,mediaId 类型时为消息中的 mediaId 值 (必填)
--message-id string 消息 openMessageId (必填)
--open-conversation-id string 会话 openConversationId (必填)
--output string 本地保存路径,文件或目录 (必填)
--output string 本地保存路径 (必填),建议传完整文件路径(含文件名)
注意:
- resource-id 从 `dws chat message list` 返回的消息内容中获取 mediaId
- message-id 从 `dws chat message list` 返回的 openMessageId
- open-conversation-id 从 `dws chat search` 获取 openConversationId
- --output 如果指定目录,文件名会从下载 URL 中自动推断
- --output 建议传完整文件路径(含扩展名,如 ./photo.jpg);只传目录(如 ./downloads/)在旧版本会下载失败,请显式带上文件名
```
### search-common (搜索共同群)
@@ -1387,7 +1387,7 @@ Flags:
#### 关闭/开启 @所有人消息提醒 — 关闭或开启会话中 @所有人的消息通知
> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。
> ⚠️ 当前不可用:该命令当前调用常失败(服务端返回 1002 系统繁忙),多为服务端侧限制。命令本身参数合法,但调用不会生效。
```
Usage:
dws chat mute-at-all [flags]
@@ -1405,7 +1405,7 @@ Flags:
#### 关闭/开启红包消息提醒 — 关闭或开启会话中的红包消息通知
> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。
> ⚠️ 当前不可用:该命令当前调用常失败(服务端返回 1002 系统繁忙),多为服务端侧限制。命令本身参数合法,但调用不会生效。
```
Usage:
dws chat mute-red-envelope [flags]
@@ -1487,7 +1487,7 @@ Flags:
注意:
- 返回结果包含单聊和群聊,不区分会话类型
- --limit 范围 1-100,默认 100,上限 100;传入 >100 会报错拒绝,不会静默截断
- 分页当前不可用:真机 hasMore 恒为 false、nextCursor 恒为 null,本命令最多返回 100 条会话,无法用 --cursor 继续翻页取更多
- 分页当前不可用:当前 hasMore 恒为 false、nextCursor 恒为 null,本命令最多返回 100 条会话,无法用 --cursor 继续翻页取更多
- 与 list-top-conversations 的区别: 本命令返回全部会话(单聊+群聊),list-top-conversations 仅返回置顶会话
```
@@ -1574,7 +1574,7 @@ Flags:
#### 审批入群验证 — 通过、删除单个审核
真机当前仅 AuditApprove(通过)和 AuditDelete(删除)两个动作可用。
当前仅 AuditApprove(通过)和 AuditDelete(删除)两个动作可用。
```
Usage:
dws chat group audit-join-validation [flags]
@@ -1591,7 +1591,7 @@ Flags:
--description string 审批说明(可选)
注意:
- status 真机仅支持 AuditApprove(通过) 和 AuditDelete(删除);AuditIgnore(忽略)、AuditRefuse(拒绝)、AuditBlock(拒绝且拉黑) 会被服务端拒绝报 unsupported audit status,属服务端限制
- status 仅支持 AuditApprove(通过) 和 AuditDelete(删除);AuditIgnore(忽略)、AuditRefuse(拒绝)、AuditBlock(拒绝且拉黑) 会被服务端拒绝报 unsupported audit status,属服务端限制
- record-id、applicant、inviter 可通过 dws chat group list-join-validations 查询获得
```
@@ -135,12 +135,21 @@ def main():
if not data:
break
# 兼容两种结构: 顶层 messages / {result: {messages, hasMore}}
if isinstance(data, list):
page_msgs = data
has_more = False
else:
page_msgs = data.get('messages', data.get('result', []))
has_more = data.get('hasMore', False)
container = data
inner = data.get('result')
if isinstance(inner, dict):
container = inner
page_msgs = container.get('messages')
if page_msgs is None and isinstance(inner, list):
page_msgs = inner
if not isinstance(page_msgs, list):
page_msgs = []
has_more = bool(container.get('hasMore', False))
if not page_msgs:
break
@@ -153,7 +162,9 @@ def main():
break
last_msg = page_msgs[-1]
boundary_time = last_msg.get('createAt') or last_msg.get('time', '')
boundary_time = (last_msg.get('createTime')
or last_msg.get('createAt')
or last_msg.get('time', ''))
if not boundary_time or boundary_time == current_time:
break
current_time = boundary_time
@@ -163,28 +174,18 @@ def main():
print('未拉取到消息')
return
if isinstance(data, list):
messages = data
elif isinstance(data, dict):
inner = data.get('result', data)
if isinstance(inner, dict):
messages = inner.get('messages', inner.get('records', []))
elif isinstance(inner, list):
messages = inner
else:
messages = []
else:
messages = []
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(all_messages, f, ensure_ascii=False, indent=2)
print(f" ✓ 已导出 {len(all_messages)} 条消息到 {args.output}")
else:
for m in all_messages:
sender = m.get('senderNick') or m.get('sender', '未知')
text = m.get('text') or m.get('content', '')
time_str = m.get('createAt') or m.get('time', '')
# 实际字段: sender/createTime/content (兼容旧字段名)
sender = (m.get('sender') or m.get('senderNick')
or m.get('senderOpenDingTalkId', '未知'))
text = m.get('content') or m.get('text', '')
time_str = (m.get('createTime') or m.get('createAt')
or m.get('time', ''))
print(f" [{time_str}] {sender}: {text[:80]}")
print(f"\n合计: {len(all_messages)} 条消息 ({page} 页)")
@@ -135,12 +135,21 @@ def main():
if not data:
break
# 兼容两种结构: 顶层 messages / {result: {messages, hasMore}}
if isinstance(data, list):
page_msgs = data
has_more = False
else:
page_msgs = data.get('messages', data.get('result', []))
has_more = data.get('hasMore', False)
container = data
inner = data.get('result')
if isinstance(inner, dict):
container = inner
page_msgs = container.get('messages')
if page_msgs is None and isinstance(inner, list):
page_msgs = inner
if not isinstance(page_msgs, list):
page_msgs = []
has_more = bool(container.get('hasMore', False))
if not page_msgs:
break
@@ -153,7 +162,9 @@ def main():
break
last_msg = page_msgs[-1]
boundary_time = last_msg.get('createAt') or last_msg.get('time', '')
boundary_time = (last_msg.get('createTime')
or last_msg.get('createAt')
or last_msg.get('time', ''))
if not boundary_time or boundary_time == current_time:
break
current_time = boundary_time
@@ -164,28 +175,18 @@ def main():
return
# 3. 输出结果
if isinstance(data, list):
messages = data
elif isinstance(data, dict):
inner = data.get('result', data)
if isinstance(inner, dict):
messages = inner.get('messages', inner.get('records', []))
elif isinstance(inner, list):
messages = inner
else:
messages = []
else:
messages = []
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(all_messages, f, ensure_ascii=False, indent=2)
print(f" ✓ 已导出 {len(all_messages)} 条消息到 {args.output}")
else:
for m in all_messages:
sender = m.get('senderNick') or m.get('sender', '未知')
text = m.get('text') or m.get('content', '')
time_str = m.get('createAt') or m.get('time', '')
# 实际字段: sender/createTime/content (兼容旧字段名)
sender = (m.get('sender') or m.get('senderNick')
or m.get('senderOpenDingTalkId', '未知'))
text = m.get('content') or m.get('text', '')
time_str = (m.get('createTime') or m.get('createAt')
or m.get('time', ''))
print(f" [{time_str}] {sender}: {text[:80]}")
print(f"\n合计: {len(all_messages)} 条消息 ({page} 页)")
+1 -1
View File
@@ -31,7 +31,7 @@ metadata:
| "按 userId 查详情" | `dws contact user get --ids <userId1>,<userId2>,...`(多个并行) |
| "按部门名拉成员" | `python scripts/contact_dept_members.py --query "<部门名>"` |
| "搜部门" | `dws contact dept search --query "<关键词>"` |
| "部门成员列表" | `dws contact dept list-members --ids <deptId>` |
| "部门成员列表" | `dws contact dept list-members --depts <deptId>` |
| "离职员工/离职名单/已离职" | `dws contact user dismission search`(可加 `--name` / `--start` + `--end` / `--depts`) |
| "花名册/员工档案/学历/银行卡/合同" | `dws contact user profile get --staff-id <STAFF_ID>`(先 `profile fields` 查字段) |
@@ -6,7 +6,7 @@
## 专用规则(#8 非 lite 步骤必守)
- **角色类查人优先 label**:用户说"角色为XX的员工/XX角色的员工/XX角色的人员""所有主管/主管理员/财务/HR/总经理"等角色类型人员时,**优先** `contact label list` 获取全部角色 → 匹配目标角色 → `contact label list-members --id <labelId>`;若用户明确指定了角色名称(如"角色为总经理"),则先用 `contact label get --names <XX>` 精确匹配,**若精确匹配无结果,降级 `label list` 模糊匹配**(如用户说"管理员"可匹配到"主管理员"和"子管理员")。
- **脚本优先**:按部门拉成员**优先** `python scripts/contact_dept_members.py --query "<部门名>"`(`--dry-run` / `--format json`);失败再 `dept search` → `dept list-members --ids`。
- **脚本优先**:按部门拉成员**优先** `python scripts/contact_dept_members.py --query "<部门名>"`(`--dry-run` / `--format json`);失败再 `dept search` → `dept list-members --depts`。
- **详情链路**:用户要子部门、职位、联系方式、汇报关系等,在 `user search` 之后**必须**再 `contact user get --ids <userId>`;禁止仅用 search 的浅表字段交差。
- **`user get` 后部门仍空**:不得过早结束或只建议用户去 App;须在 CLI 能力内尝试 **用户点名的部门** `dept search` + `dept list-members` 等与 `userId` 交叉核对,再结构化汇总「返回中有哪些字段 / 哪些为空及可能原因」。
- **多命中**:`user search` 或 `dept search` 多条时须列候选(姓名、title、部门线索)请用户确认,禁止默认猜一人。具体消歧流程:
@@ -35,16 +35,16 @@
| `search-user-by-mobile` | 1. `contact user search-mobile --mobile "<手机号>"` → 按需 `contact user get --ids <userId>` |
| `lookup-dept-id` | 1. `contact dept search --query "<部门关键词>"` → 回显 `deptId`(多命中须消歧) |
| `list-subdepts` | 1. 已有父 `deptId` → `contact dept list-children --id <父deptId>` 直接取直属子部门列表<br>2. 只有部门名 → 先 `lookup-dept-id` 取 `deptId`,再 `list-children` |
| `list-dept-members` | 1. **优先** `python scripts/contact_dept_members.py --query "<部门名>"`<br>2. 备选:`lookup-dept-id` → `contact dept list-members --ids <deptId>`<br>3. 若要每人档案字段:对 `userId` 批量 `contact user get --ids …` |
| `list-multi-dept-members` | 1. 对每个部门名 `contact dept search --query "<名>"` → 各 `deptId`<br>2. `contact dept list-members --ids <id1>,<id2>,...`(多部门并行/批量见 conventions)<br>3. 需要档案再 `contact user get --ids …` |
| `verify-user-dept` | 1. `contact dept search --query "<部门名>"` → `deptId`<br>2. `contact dept list-members --ids <deptId>` 中匹配姓名;或先 `search-user` lite 再 `user get` 核对部门字段 |
| `list-dept-members` | 1. **优先** `python scripts/contact_dept_members.py --query "<部门名>"`<br>2. 备选:`lookup-dept-id` → `contact dept list-members --depts <deptId>`<br>3. 若要每人档案字段:对 `userId` 批量 `contact user get --ids …` |
| `list-multi-dept-members` | 1. 对每个部门名 `contact dept search --query "<名>"` → 各 `deptId`<br>2. `contact dept list-members --depts <id1>,<id2>,...`(多部门并行/批量见 conventions)<br>3. 需要档案再 `contact user get --ids …` |
| `verify-user-dept` | 1. `contact dept search --query "<部门名>"` → `deptId`<br>2. `contact dept list-members --depts <deptId>` 中匹配姓名;或先 `search-user` lite 再 `user get` 核对部门字段 |
## Full / 多步组合
| Recipe | 行动指南(固定路线) |
|--------|---------------------|
| explore-subdepts-and-members | 1. 取父部门 `deptId`:用户给了 ID 直接用;只给名字则 `contact dept search --query "<父部门名>"` → 父 `deptId`(多命中先消歧)<br>2. **优先** `contact dept list-children --id <父deptId>` 拿到全部直属子 `deptId` 列表;若用户只点名了部分子部门,则改为对每个子部门名 `contact dept search --query "<子部门名>"`<br>3. 对子 `deptId`:`contact dept list-members --ids <id1>,<id2>,...`(多部门按 conventions **并行/批量**)<br>4. 若还要成员详情:汇总 `userId` → `contact user get --ids …`(≤30 条/批,超出分批 + 用户确认) |
| explore-subdepts-and-members | 1. 取父部门 `deptId`:用户给了 ID 直接用;只给名字则 `contact dept search --query "<父部门名>"` → 父 `deptId`(多命中先消歧)<br>2. **优先** `contact dept list-children --id <父deptId>` 拿到全部直属子 `deptId` 列表;若用户只点名了部分子部门,则改为对每个子部门名 `contact dept search --query "<子部门名>"`<br>3. 对子 `deptId`:`contact dept list-members --depts <id1>,<id2>,...`(多部门按 conventions **并行/批量**)<br>4. 若还要成员详情:汇总 `userId` → `contact user get --ids …`(≤30 条/批,超出分批 + 用户确认) |
| verify-user-in-dept | 同速查表 `verify-user-dept`;多轮对话中用户追加「是否在某部门」时叠加本路线 |
| cross-level-dept-members | 1. `contact dept search --query "<父部门关键词>"` → 父 `deptId`<br>2. **优先** `contact dept list-children --id <父deptId>` 枚举全部直属子 `deptId`;用户已点名的子部门则用 `dept search` 精确命中<br>3. 需要逐层下钻时,对上一步拿到的子 `deptId` 继续 `dept list-children` 递归(注意控制深度,避免一次拉太多)<br>4. `contact dept list-members --ids <id1,id2,…>` → 按需 `user get` |
| cross-level-dept-members | 1. `contact dept search --query "<父部门关键词>"` → 父 `deptId`<br>2. **优先** `contact dept list-children --id <父deptId>` 枚举全部直属子 `deptId`;用户已点名的子部门则用 `dept search` 精确命中<br>3. 需要逐层下钻时,对上一步拿到的子 `deptId` 继续 `dept list-children` 递归(注意控制深度,避免一次拉太多)<br>4. `contact dept list-members --depts <id1,id2,…>` → 按需 `user get` |
| user-detail-organization | 1. `contact user search --query "<关键词>"` → `userId`(多结果先消歧)<br>2. **必须** `contact user get --ids <userId>`<br>3. 若用户同时给出部门语境:叠加 `verify-user-dept` |
| batch-users-by-keyword | 1. `contact user search --query "<职位或技能关键词>"` → 多条<br>2. 提取 `userId`(≤30)→ `contact user get --ids …`<br>3. 结果过多时汇总或请用户收窄 |
@@ -211,7 +211,7 @@ Example:
Flags:
无
Notes:
- 无需参数,返回当前企业全部角色列表(labelId、labelName等)
- 无需参数,返回当前企业全部角色,按 `groupName` 分组,每组 `labels[]` 里每个角色是 `{labelId, name}`(字段名是 `name`,不是 `labelName`)
- 用于不知道准确角色名称时先浏览全部角色
- 典型场景:用户说“企业所有主管/查所有管理员/财务人员有哪些”→ 先 label list 浏览全部角色,匹配目标角色后 label list-members 获取成员
```
@@ -358,7 +358,7 @@ dws contact user dismission search --depts 123456,789012 --hide-retirement=false
| `user get-self/search` | `orgAuthEmail` | mail message send 的 --to/--cc (跨产品) |
| `user get-self/search` | `userId` | profile get 的 --staff-id |
| `user profile fields` | `fieldCode` | profile get 的 --fields |
| `label list` | `labelId` / `labelName` | `label get --names` 或 `label list-members --id` |
| `label list` | `labelId` / `name` | `label get --names` 或 `label list-members --id` |
| `label get` | `labelId` | `label list-members` 的 --id |
| `dept search/list-children` | `deptId` | dept get-info/list-children/list-members 的 --dept/--depts |
| `dept search/list-children` | `deptId` | dismission search 的 --depts |
@@ -75,7 +75,7 @@ def main():
elif isinstance(dept_data, dict):
inner = dept_data.get('result', dept_data)
if isinstance(inner, dict):
# dept search 真机返回顶层 deptList;兼容历史 items/depts 键。
# dept search 返回顶层 deptList;兼容历史 items/depts 键。
depts = (inner.get('deptList')
or inner.get('items')
or inner.get('depts')
@@ -114,7 +114,7 @@ def main():
elif isinstance(members_data, dict):
inner = members_data.get('result', members_data)
if isinstance(inner, dict):
# list-members 真机返回 deptUserList;兼容历史 userlist/list 键。
# list-members 返回 deptUserList;兼容历史 userlist/list 键。
members = (inner.get('deptUserList')
or inner.get('userlist')
or inner.get('list')
@@ -130,7 +130,7 @@ def main():
continue
for m in members:
# list-members 真机每项形如 {"userInfo": {"name":..., "userId":...}},
# list-members 每项形如 {"userInfo": {"name":..., "userId":...}},
# 成员字段嵌在 userInfo 下;兼容历史扁平结构。
info = m.get('userInfo', m)
name = info.get('name') or info.get('userName', '未知')
@@ -14,14 +14,14 @@
- 批量或全量订阅前,先把候选 `eventCode` 列给用户确认,再 `--dry-run` → `--yes`。
- 返回看 `events[].subscribed` 和 `pushType=STREAM`(事件走 Stream 长连推送;connect 与事件订阅的关系见下方「Stream 长连」)。
- `subscribe/unsubscribe` 的 `--event-codes` 必填,返回 `success/operation/unifiedAppId/eventCodes/needsPublish/versionRequiredAction`;失败时补 `errorCode/errorMsg/reason/retryable/action`。
- 如果服务端返回 `errorCode=STREAM_NOT_CONNECTED`、`reason=STREAM_NOT_CONNECTED`、`retryable=false`、`action=run connect`,先执行 `dev connect` 建联,再重试订阅。
- 如果订阅失败、返回提示长链接未在线(是泛化错误:`reason=business_error`、`message` 含「长链接未在线」、`server_error_code=-1`;没有 STREAM_NOT_CONNECTED 这类结构化错误码,也没有 action 字段),先执行 `dev connect` 建联,再重试订阅。
## Stream 长连:connect 与事件订阅的关系
钉钉一个应用就一条 Stream 长连(WebSocket),上面同时承载机器人消息、事件、卡片回调等多种 topic。connect 和 event 在这条长连上分工不同:
- `dev connect`:用应用凭证把这条长连建起来并保活,但只注册了机器人消息处理(收 @机器人 → 转发本地 agent),**不消费事件**。
- `event subscribe/unsubscribe`:只是**配置**操作(配应用订阅哪些事件码),自己不建长连、不收事件;服务端要求应用的 Stream 长连已在线,否则返回 `STREAM_NOT_CONNECTED`。
- `event subscribe/unsubscribe`:只是**配置**操作(配应用订阅哪些事件码),自己不建长连、不收事件;服务端要求应用的 Stream 长连已在线,否则报错「长链接未在线」(泛化 business_error,不是结构化错误码)。
- 两者唯一关联:先 `dev connect` 把长连建在线,`event subscribe` 才能成功——connect 负责「让长连在线」,subscribe 负责「配置订阅」。
- 当前限制:connect 的长连不处理事件,dws 也没有「收/消费事件」的运行时命令,event 只到「配置订阅」为止。订阅成功后真正的事件推送 dws 暂不消费——要消费得用注册了事件 handler 的 SDK 自接(事件结构走 `dws devdoc article search --query` 查)。
@@ -90,5 +90,5 @@ event subscribe/unsubscribe 订阅变更写入版本元数据
|------|------|
| `--event-codes` 缺失 | CLI 直接报错,先 `event list` 取事件码 |
| 订阅后 `subscribed` 仍为 false | 灰度应用需先 `version publish` 发布版本 |
| `errorCode=STREAM_NOT_CONNECTED` / `reason=STREAM_NOT_CONNECTED` | 先执行 `dev connect` 建联,再重试订阅 |
| 返回提示「长链接未在线」(泛化错误 `reason=business_error`、`server_error_code=-1`,无 STREAM_NOT_CONNECTED 码) | 先执行 `dev connect` 建联,再重试订阅 |
| `ServiceResult.success=false` | 透传 `errorCode/errorMsg` |
@@ -7,15 +7,16 @@
### 搜索开发文档
```
Usage:
dws devdoc article search [flags]
dws devdoc article search [keyword] [flags]
Example:
dws devdoc article search "MCP" # 关键词可作位置参数,等价于 --query
dws devdoc article search --query "OAuth2 接入"
dws devdoc article search --query "消息卡片" --page 2 --size 5
dws devdoc article search --query "机器人" --size 10
Flags:
--query string 搜索关键词 (必填)
--page int 分页页码 (从 1 开始,默认 1)
--size int 分页大小 (默认 10)
--query string 搜索关键词 (必填;也可用位置参数 [keyword] 传)
--page string 分页页码 (从 1 开始,default "1")
--size string 分页大小 (default "10")
```
## 意图判断
+6 -6
View File
@@ -27,7 +27,7 @@ dws doc permission --help
### 搜索 / 遍历文件(已迁移,不再是 doc 子命令)
> **弃用提示**:`dws doc search` 和 `dws doc list` 已迁移到 `drive` / `wiki`。真机上这两条虽仍能跑,但每次都会打印弃用警告,请改用下面的命令,命令详情见 dingtalk-drive skill。
> **弃用提示**:`dws doc search` 和 `dws doc list` 已迁移到 `drive` / `wiki`。这两条虽仍能跑,但每次都会打印弃用警告,请改用下面的命令,命令详情见 dingtalk-drive skill。
| 旧命令(弃用) | 改用 | 场景 |
|--------------|------|------|
@@ -83,7 +83,7 @@ Flags:
### 创建其他类型文件 (表格/脑图/白板/多维表/画板/文件夹)
> **弃用提示**:`dws doc file create --type` 已弃用,真机每次执行都会打印 `deprecated, use 'dws wiki node create --type <type>'`。创建各类型文件节点改用 `dws wiki node create`(与 intent-guide 一致);创建普通文件夹用 `dws drive mkdir`。
> **弃用提示**:`dws doc file create --type` 已弃用,执行时会打印 `deprecated, use 'dws wiki node create --type <type>'`。创建各类型文件节点改用 `dws wiki node create`(与 intent-guide 一致);创建普通文件夹用 `dws drive mkdir`。
```
Usage:
@@ -133,7 +133,7 @@ Flags:
### 下载文件到本地(已迁移到 drive)
> **弃用提示**:`dws doc download` 已迁移到 `dws drive download`(真机执行 `doc download` 会打印弃用警告)。下载已有文件(PDF/图片/附件等非在线文档)改用:
> **弃用提示**:`dws doc download` 已迁移到 `dws drive download`(执行 `doc download` 会打印弃用警告)。下载已有文件(PDF/图片/附件等非在线文档)改用:
```
dws drive download --node <NODE_ID> --output ~/downloads/
@@ -261,11 +261,11 @@ Usage:
Example:
dws doc comment list --node <DOC_ID>
dws doc comment list --node <DOC_ID> --type inline --resolve-status unresolved
dws doc comment list --node <DOC_ID> --page-size 20 --next-token <TOKEN>
dws doc comment list --node <DOC_ID> --limit 20 --cursor <TOKEN>
Flags:
--node string 目标文档的标识,支持传入 URL 或 ID (必填)
--page-size int 每页返回的评论数量,默认 50,最大 50
--next-token string 分页游标,从上一次请求的返回结果中获取 (首次请求不传)
--limit int 每页返回的评论数量,默认 50,最大 50
--cursor string 分页游标,从上一次请求的返回结果中获取 (首次请求不传)
--type string 按评论类型过滤: global (全文评论) / inline (划词评论)
--resolve-status string 按解决状态过滤: resolved (已解决) / unresolved (未解决)
```
@@ -3,7 +3,7 @@
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令在真机上虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令:
> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令:
> - `doc download` → **`dws drive download`**(下载已有文件;在线文档导出 docx 仍走 `doc export`)
> - `doc folder create` → **`dws drive mkdir`**(个人空间/钉盘)或 **`dws wiki node create --workspace <id> --type folder`**(知识库内)
> - `doc upload` / `doc copy` / `doc move` / `doc rename` / `doc delete` 也在同一迁移方向(→ `drive`),命令用法以 `drive` 为准。
@@ -1,6 +1,6 @@
# doc list(已弃用 → drive list / wiki node list)
> **弃用提示**:`dws doc list` 已迁移,不再是 `doc` 子命令。真机上它虽仍能跑,但每次都会打印弃用警告:
> **弃用提示**:`dws doc list` 已迁移,不再是 `doc` 子命令。它虽仍能跑,但每次都会打印弃用警告:
> `'dws doc list' is deprecated, use 'dws drive list --workspace <workspaceId>' or 'dws wiki node list --workspace <workspaceId>'`。
> 遍历文件夹/知识库请切到 `dingtalk-drive` skill,用下面的命令。
@@ -1,6 +1,6 @@
# doc search(已弃用 → drive search / wiki node search)
> **弃用提示**:`dws doc search` 已迁移,不再是 `doc` 子命令。真机上它虽仍能跑,但每次都会打印弃用警告:
> **弃用提示**:`dws doc search` 已迁移,不再是 `doc` 子命令。它虽仍能跑,但每次都会打印弃用警告:
> `'dws doc search' is deprecated, use 'dws drive search' or 'dws wiki node search --workspace <id>'`。
> 搜索文档/文件请切到 `dingtalk-drive` skill,用下面的命令。
@@ -90,14 +90,14 @@ Step 3 → 按下方路由规则映射到对应产品
| `contentType=ALIDOC`, `extension=adoc` | `doc` | 按 [doc.md](./products/doc.md) 操作 |
| `contentType=ALIDOC`, `extension=axls` | `sheet` | 按 [sheet.md](./products/sheet.md) 操作(仅 `axls` 在线电子表格) |
| `contentType=ALIDOC`, `extension=able` | `aitable` | 将 nodeId 作为 baseId,按 [aitable.md](./products/aitable.md) 操作 |
| `contentType=DOCUMENT`, `extension=xlsx` / `xls` / `xlsm` / `csv` | `doc` | 必须用 `dws doc download` 下载到本地处理,禁止走 `sheet`(非在线表格,sheet 命令无法操作) |
| `contentType≠ALIDOC`, `nodeType=file` | `doc` | 调用 `dws doc download` 下载,返回文件下载链接 |
| `contentType=DOCUMENT`, `extension=xlsx` / `xls` / `xlsm` / `csv` | `doc` | 必须用 `dws drive download` 下载到本地处理,禁止走 `sheet`(非在线表格,sheet 命令无法操作) |
| `contentType≠ALIDOC`, `nodeType=file` | `doc` | 调用 `dws drive download` 下载,返回文件下载链接 |
| `nodeType=folder` | `doc` | 调用 `dws doc list --folder <ID>` 列出指定文件夹直接子节点列表 |
| 以上均不匹配 | — | 告知用户当前暂不支持该类型 |
> axls vs xlsx 关键区分:
> - `axls`(钉钉在线电子表格,`contentType=ALIDOC`)→ 走 `sheet` 产品线(读/写/筛选/导出等服务端原子操作)
> - `xlsx` / `xls` / `xlsm` / `csv`(上传到文档空间的本地表格文件,`contentType=DOCUMENT`)→ 必须走 `dws doc download` 下载到本地后再解析处理,严禁错误路由到 `sheet` 产品线(sheet 命令只支持在线表格,调用 xlsx 节点会直接报错)
> - `xlsx` / `xls` / `xlsm` / `csv`(上传到文档空间的本地表格文件,`contentType=DOCUMENT`)→ 必须走 `dws drive download` 下载到本地后再解析处理,严禁错误路由到 `sheet` 产品线(sheet 命令只支持在线表格,调用 xlsx 节点会直接报错)
> - 用户想把在线表格导出为 xlsx 文件 → 用 `dws sheet export --node <表格ID或URL>`(单命令一站式:提交导出任务→轮询→取 downloadUrl,加 `--output ./x.xlsx` 可直接落盘)
### 示例
@@ -110,10 +110,10 @@ dws doc info --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
# 返回 contentType≠ALIDOC, extension=xlsx/xls/csv → 本地表格文件,必须下载处理(禁止走 sheet)
dws doc download --node "https://alidocs.dingtalk.com/i/nodes/xlsx456"
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/xlsx456"
# 返回 contentType≠ALIDOC, nodeType=file → 普通文件,下载
dws doc download --node "https://alidocs.dingtalk.com/i/nodes/def456"
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/def456"
# 返回 nodeType=folder → 文件夹,列出子节点
dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/ghi789" --format json
@@ -138,8 +138,8 @@ dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/ghi789" --format jso
| **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 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 *` | 不适用 |
| **xlsx / xls / xlsm / csv**(本地表格文件) | `drive download` → 本地用 xlsx skill 解析 | 不支持服务端写(先下载改本地再上传) | ⚠️ `doc delete`(节点删除) | 不需要(本身就是 xlsx) | ⚠️ `doc permission *` | 不适用 |
| **普通文件** (nodeType=file) | `drive download` | 不支持服务端写 | ⚠️ `doc delete` | 不需要 | ⚠️ `doc permission *` | 不适用 |
| **文件夹** (nodeType=folder) | `doc list --folder <URL>` | `doc create --folder <URL> ...` | ⚠️ `doc delete` | 不适用 | ⚠️ `doc permission *` | 不适用 |
| **分享短链** `/i/p/<short>` | `read_url` 兜底(外部工具) | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
@@ -514,7 +514,7 @@ dws drive copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --f
- 不传 `--space-id` 时默认使用「我的文件」空间
- 不传 `--folder` 时默认操作空间根目录
- `--folder` 只能使用父文件夹的 `dentryUuid`。不要把 `drive info` 返回的数字型 `dentryId` 当作父目录
- **`--limit` 最大值为 50**,禁止传入超过 50 的值(如 `--limit 100`)。用户要求超过 50 条时,应使用 `--limit 50` 配合 `--cursor` 分页查询
- **`--limit` 有效上限为 50**:CLI 不做本地校验,传 `--limit 100` 不会报错,但服务端每页最多返回 50 条,超出部分无效。用户要求超过 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`)
@@ -214,7 +214,9 @@ Flags:
| 字段 | 类型 | 说明 |
|------|------|------|
| `message` | `object` | 邮件完整信息,包含主题、发件人、收件人、正文、附件等 |
| `message` | `object` | 邮件信息,含 subject/from/toRecipients/ccRecipients/markdownBody/conversationId/receivedDateTime/size 等 |
> **注意:** 即使邮件确有附件,`message get` 返回的 `message` 对象也**不含** `attachments`/`isRead`/`tags` 字段(实测 keys 仅上表所列这些)。要取附件请另用 `dws mail attachment list`。
### 发送邮件
```
@@ -1364,6 +1366,8 @@ Flags:
--actions string 规则动作 JSON 数组 (必填)
```
> **⚠️ 实测:** 尽管 `--help` 把 `--conditions` 标为(可选),服务端实际要求它**非空**——省略或传空会返回 `209 SYS_UNKNOWN_ERROR(1001)` 失败,帮助文本承诺的"命中所有邮件"无条件匹配**不可用**。create 请始终传入有效条件。
**返回字段:**
| 字段 | 类型 | 说明 |
@@ -1375,11 +1379,11 @@ Flags:
#### 更新收信规则
更新已有的收信规则。**除 `--conditions` 外所有参数均为必填**。
更新已有的收信规则。**所有参数(含 `--conditions`)均须传入有效值**——见下方警告。
> **建议工作流:** 先通过 `dws mail rule list` 获取当前规则的完整配置,再传入需要修改的字段值。
>
> `--conditions` 为空或不传表示命中所有邮件(无条件匹配)。`--actions` 格式同 create 命令。
> **⚠️ 实测:** `--help` 称 `--conditions` 为空即命中所有邮件,但该无条件匹配**不可用**——省略或传空会返回 `209 SYS_UNKNOWN_ERROR(1001)` 失败。update 请始终传入有效条件。`--actions` 格式同 create 命令。
```
Usage:
@@ -1399,6 +1403,8 @@ Flags:
--actions string 规则动作 JSON 数组 (必填)
```
> **⚠️ 实测:** 上方 `--conditions` 的"(可选,为空表示命中所有邮件)"是 `--help` 原文,但服务端实际要求它**非空**——省略或传空会返回 `209 SYS_UNKNOWN_ERROR(1001)` 失败。update 须传入有效条件。
**返回字段:**
| 字段 | 类型 | 说明 |
@@ -8,4 +8,4 @@
| meeting-followup | 1. **提取待办优先**:`python scripts/minutes_extract_todos.py [--id <taskUuid>] [--max N]` → 获取行动项列表 无脚本时按「多源并行采集」(见 dws-shared 的 conventions.md)执行 2. 提取行动项 3. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId` → `todo task create --title "<行动项>" --executors <userId>`(每条行动项;批量时通过 `dingtalk-todo` skill:`python scripts/todo_batch_create.py todos.json`,或循环 `dws todo task create`) 4. `chat message send --group <openConversationId> --text "<通知内容>"` 通知已创建 |
| share-minutes | **拉摘要优先**:`python scripts/minutes_recent_summary.py [--max N] [--output summary.md]` → 获取近期听记摘要 备选手动:1. `minutes list mine` → 取 `taskUuid` 2. 用户选定 3. `minutes get summary --id <taskUuid>` → 取摘要 4. 单聊:`contact user search --query "<姓名>"` → 取 `openDingTalkId` → `chat message send --open-dingtalk-id <openDingTalkId> --text "<摘要内容>"`(推荐);群聊:`--group <openConversationId> --text "<摘要内容>"` 发送。仅当无法获取 openDingTalkId 时才用 `--user <userId>`(备选) |
| browse-minutes | 1. `minutes list all --query "<关键词>" --max <N>` → 取全量 `taskUuid`(翻页直至无更多;无主题筛选用 `minutes list mine`) 2. **详情/元数据优先** `minutes get batch --ids <uuid1,uuid2,...>`(仅 API 上限时拆多批);**摘要专项**且无 batch 字段时再用并行 `get summary` 或 `**python scripts/minutes_recent_summary.py --max <N>`** 3. 汇总展示;**同一用户诉求下避免**「半截 list → 再 list 剩余」「拆 4 条一批 summary 连跑多轮」等无必要拆分 |
| minutes-detail | 1. 确定 `taskUuid`:用户已提供 → 直接用;未提供 → `minutes list mine --max 5` 让用户选 2. 并行拉取四维信息:`minutes get info --id <taskUuid>` `&` `minutes get summary --id <taskUuid>` `&` `minutes get keywords --id <taskUuid>` `&` `minutes get todos --id <taskUuid>` `& wait` 3. 整合输出:基础信息(标题、时间、参与人)→ AI 摘要 → 关键字 → 行动项/待办 4.(可选)用户要求看原文 → `minutes get transcription --id <taskUuid>` |
| minutes-detail | 1. 确定 `taskUuid`:用户已提供 → 直接用;未提供 → `minutes list mine --max 5` 让用户选 2. 依次拉取四维信息(**逐条串行执行,禁止用 `&` 后台与 `wait`——违反 Shell 安全铁律,Windows cmd 下会命令错乱**):`minutes get info --id <taskUuid>`、`minutes get summary --id <taskUuid>`、`minutes get keywords --id <taskUuid>`、`minutes get todos --id <taskUuid>` 3. 整合输出:基础信息(标题、时长、开始时间;get info 不返回参会人)→ AI 摘要 → 关键字 → 行动项/待办 4.(可选)用户要求看原文 → `minutes get transcription --id <taskUuid>` |
@@ -26,7 +26,7 @@ dws minutes
│ ├── shared [--query] [--start] [--end] [--max] [--next-token] # 仅查询他人共享给我的听记(不含我自己创建的)
│ └── all [--query] [--start] [--end] [--max] [--next-token] # 查询我可访问的所有听记(= mine ∪ shared,覆盖面最广,默认 scope)
├── get <subcommand> # 获取听记详情(get 后必须跟子命令名)
│ ├── info --id <uuid> # 获取听记基础信息(标题、时长、参与人、状态等)
│ ├── info --id <uuid> # 获取听记基础信息(标题、时长、开始时间、taskUuid、访问链接;不含参会人)
│ ├── summary --id <uuid> # 获取听记 AI 摘要(结构化纪要)
│ ├── transcription --id <uuid> # 获取听记语音转写原文(逐字稿,支持分页)
│ ├── keywords --id <uuid> # 获取听记关键词
@@ -615,7 +615,7 @@ AI 基于已拉取的转写数据,**本地完成**聚类与摘要(无需新
> **铁律 2:严禁用 AI 摘要里的"参与人/参会人"字段判断某人是否参会**
> - AI 摘要中的"参与人"字段往往**只截取最显著的 1-2 个名字**(通常是创建人或发言最多的人),**不是完整参会人列表**
> - "AI 摘要参与人只有故愚 → 灵麦没参会" 是典型的错误推理链
> - 如果一定要列出参会人,应使用 `dws minutes get info` / `get batch` 返回的 `participants` 字段,而不是摘要文本里的描述
> - 如果一定要列出参会人,只能从 `get transcription` 的转写发言人(`speakerName`/`speakerId`)汇总——`get info` / `get batch` **均不返回** `participants` 结构化字段(get info 只返回 duration/startTime/taskUuid/title/url,get batch 只返回 creatorNick/creatorUnionId/status 等),别指望从它们拿参会名单
>
> **铁律 3:通讯录查询是 Step 4 的强制起点,不依赖 Step 3 是否成功**
> - Step 2 一旦返回"未命中真名标注",**Step 3 与 Step 4 ① 必须并发触发**,不要等 Step 3 失败再补查
@@ -664,7 +664,7 @@ Step 7: 引导用户替换发言人(调用 speaker replace 写回听记)
> - **未命中真名标注 ≠ 目标人物没参会**——绝大多数听记的发言人都是匿名编号,"未命中"是**默认场景**,恰恰是发言人识别功能要解决的问题
> - **不要在转写文本里 grep 目标人名**作为存在性判断——花名/真名通常不会出现在 TA 自己的发言里(参见铁律 1)
> - **不要把 `dws minutes get summary` 摘要文本里写到的"参与人"当作完整参会列表**——AI 摘要里的"参与人"字段只截取最显著的 1-2 个名字,不是完整名册(参见铁律 2)
> - 唯一能下"目标人物没参会"结论的场景是:`get info` / `get batch` 返回的结构化 `participants` 字段里**完整列出参会人且不含目标**,且 `dws contact user search` 也搜不到该人 → 才允许告知用户"该人不在参会列表"
> - 听记各接口都拿不到权威的完整参会名册:`get info` / `get batch` **均不返回** `participants` 字段,转写发言人列表又常是匿名编号。因此**几乎无法**仅凭听记数据下"目标人物没参会"的结论;只有当转写发言人已全部实名标注且不含目标、且 `dws contact user search` 也搜不到该人时,才可谨慎告知"未在可见发言人中找到该人"
##### Step 3: 转写原文内推断(优先)
@@ -810,7 +810,7 @@ Flags:
--ids string 听记 taskUuid 列表,逗号分隔 (必填)
```
返回字段: 听记标题、时长、参与人列表、创建时间、taskUuid、听记状态
返回字段: `result.minutesDetails` 数组,每项含 `title`(标题)、`durationMicros`(时长, 微秒)、`startTime`(开始时间, 毫秒)、`taskUuid`、`status`(状态)、`creatorNick`(创建人昵称)、`creatorUnionId`、`size`、`bizType`、`isDeleted`。**不返回参会人列表**。
### 修改听记标题
```
@@ -2239,7 +2239,7 @@ Step 2 **[禁止]** 看到全是匿名编号 → 没有继续走 Step 3-4,反
1. **"在转写里搜不到目标人名"绝不构成"没参会"的证据**:花名/真名通常不出现在 TA 自己的发言里,这是默认场景而非例外。99% 的听记发言人都是匿名编号——这正是发言人识别功能要解决的问题。
2. **AI 摘要的"参与人"字段是低保真信号,不能作为参会判断依据**:`get summary` 返回的是 AI 生成的自然语言摘要,里面提到的"参与人"通常只是最显著的 1-2 人;要拿完整参会列表,应使用 `get info` / `get batch` 返回的结构化 `participants` 字段。
2. **AI 摘要的"参与人"字段是低保真信号,不能作为参会判断依据**:`get summary` 返回的是 AI 生成的自然语言摘要,里面提到的"参与人"通常只是最显著的 1-2 人。而 `get info` / `get batch` **均不返回**结构化 `participants` 字段,无法从中拿完整参会列表;能拿到的只有 `get transcription` 的转写发言人(常为匿名编号)。
3. **`dws contact user search` 是 Step 4 ① 的必跑项,单次调用就能突破死局**:本案例的整个失败链路只要有 1 次通讯录查询就能立即扭转——拿到"木兰=王佳明,产品设计部"后,Step 5 的角色匹配就有了锚点,再也不会得出"没参会"的错误结论。
@@ -44,6 +44,58 @@ def run_dws(
return None
def todos_from_payload(payload: Any) -> List[dict]:
"""解析 `minutes get todos` 返回为待办列表。
返回结构: result.dingtalkTodoList(对象数组, 含 title 等)与
result.actions(JSON 字符串数组, 每条形如
{"mark":[],"value":"..."}). 二者无 todos 键。
优先取 dingtalkTodoList; 为空时回退解析 actions。
"""
if isinstance(payload, dict):
inner = payload.get('result', payload)
else:
inner = payload
out: List[dict] = []
if isinstance(inner, dict):
ding_list = inner.get('dingtalkTodoList')
if isinstance(ding_list, list) and ding_list:
for t in ding_list:
if isinstance(t, dict):
content = t.get('title') or t.get('content') or ''
if content:
out.append({'content': str(content), '_raw': t})
if out:
return out
actions = inner.get('actions')
if isinstance(actions, list):
for a in actions:
content = ''
if isinstance(a, str):
text = a.strip()
if text.startswith('{'):
try:
parsed = json.loads(text)
content = parsed.get('value') or ''
except json.JSONDecodeError:
content = text
else:
content = text
elif isinstance(a, dict):
content = (a.get('value') or a.get('content')
or a.get('title') or '')
if content:
out.append({'content': str(content)})
elif isinstance(inner, list):
for t in inner:
if isinstance(t, dict):
content = (t.get('content') or t.get('text')
or t.get('title') or t.get('value') or '')
if content:
out.append({'content': str(content), '_raw': t})
return out
def main():
parser = argparse.ArgumentParser(
description='从听记中提取待办事项'
@@ -82,21 +134,9 @@ def main():
])
if not todos_data:
continue
if isinstance(todos_data, list):
items = todos_data
elif isinstance(todos_data, dict):
inner = todos_data.get('result', todos_data)
if isinstance(inner, dict):
items = inner.get('todos', [])
elif isinstance(inner, list):
items = inner
else:
items = []
else:
items = []
items = todos_from_payload(todos_data)
for t in items:
if isinstance(t, dict):
t['_source'] = title
t['_source'] = title
all_todos.extend(items)
print(f"\n📋 听记待办汇总")
@@ -17,7 +17,7 @@ def _unwrap_rows(payload: Any) -> List[Any]:
return value
if isinstance(value, dict):
for inner_key in (
'items', 'list', 'records', 'minutes',
'itemList', 'items', 'list', 'records', 'minutes',
):
inner = value.get(inner_key)
if isinstance(inner, list):
@@ -30,7 +30,8 @@ def uuid_title_pairs_from_payload(payload: Any) -> List[Tuple[str, str]]:
out: List[Tuple[str, str]] = []
for item in _unwrap_rows(payload):
if isinstance(item, dict):
uuid = item.get('taskUuid') or item.get('id') or item.get('task_uuid')
uuid = (item.get('taskUuid') or item.get('uuid')
or item.get('id') or item.get('task_uuid'))
if not uuid:
continue
title = item.get('title') or item.get('name') or '无标题'
@@ -53,6 +54,7 @@ def uuid_title_pairs_from_payload(payload: Any) -> List[Tuple[str, str]]:
continue
uuid = (
parsed.get('taskUuid')
or parsed.get('uuid')
or parsed.get('id')
or parsed.get('task_uuid')
)
@@ -93,9 +93,16 @@ def main():
if isinstance(summary_data, str):
summary_text = summary_data
elif isinstance(summary_data, dict):
summary_text = (summary_data.get('summary')
or summary_data.get('content')
or json.dumps(summary_data,
# 兼容 {result: {fullSummary}} 包裹结构
inner = summary_data.get('result')
container = (inner if isinstance(inner, dict)
else summary_data)
summary_text = (container.get('fullSummary')
or container.get('summary')
or container.get('content')
or (inner if isinstance(inner, str)
else '')
or json.dumps(container,
ensure_ascii=False))
output_lines.append(f"## {i}. {title}\n")
+4 -3
View File
@@ -314,7 +314,7 @@ Flags:
用户说"拒绝审批/驳回" → 先 `tasks` 获取 taskId,再 `reject`
用户说"撤回审批/取消审批" → `approval revoke`
用户说"审批记录/操作历史" → `approval records`
用户说"我发起的审批" → `approval list-initiated`(需 --process-code,可从 list-forms 或 detail 获取)
用户说"我发起的审批" → `approval list-initiated`(需 --process-code,可从 list-forms / search-forms / detail 获取)
用户说"有哪些审批表单/可见表单" → `approval list-forms`
用户说"我有哪些待审的任务" → `approval tasks`
用户说"我发起的审批单" -> `approval list-submitted`
@@ -351,7 +351,7 @@ dws oa approval records --instance-id <processInstanceId> --format json
# 7. 获取可见审批表单(得到 processCode)
dws oa approval list-forms --cursor 0 --limit 100 --format json
# 8. 查看自己发起的审批列表(--process-code 来自 list-forms 或 detail)
# 8. 查看自己发起的审批列表(--process-code 来自 list-forms / search-forms / detail)
dws oa approval list-initiated --process-code <code> \
--start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" \
--cursor 0 --limit 20 --format json
@@ -383,6 +383,7 @@ dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "userId1
| `tasks` | `taskId` | approve / reject / redirect-task 的 --task-id |
| `detail` | `processCode` | list-initiated 的 --process-code |
| `list-forms` | `processCode` | list-initiated 的 --process-code |
| `search-forms` | `processCode` | list-initiated 的 --process-code |
## 注意事项
@@ -391,7 +392,7 @@ dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "userId1
- `redirect-task` 的 `--to-actioner-id` 可通过 `dws contact user search` 获取目标用户 userId
- `revoke` 只能撤销自己发起的审批
- `--remark` 审批意见虽为可选,但建议填写以留存审批痕迹
- `list-initiated` 的 `--process-code` 可从 `list-forms` 或 `detail` 返回中提取
- `list-initiated` 的 `--process-code` 可从 `list-forms`、`search-forms` 或 `detail` 返回中提取。当 `list-forms` 返回 `processCodeList` 为空(`totalCount -1`)时,用 `search-forms --query <表单名>`(如 `--query 报销`)按名称精准拿 `processCode` 更稳
- `list-initiated` 的 `--start` / `--end` 区间有后端上限(约 120 天)。超过上限会返回误导性的 `business_error: 时间戳无效`(实为区间过长,不是时间格式问题)。跨度大时请拆成多段短区间分别查询
## 自动化脚本
@@ -1581,7 +1581,7 @@ Flags:
- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试
- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试
**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
**限制**:仅支持钉钉在线电子表格(axls)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
### 表格模板(list / search / apply)
```
@@ -2067,7 +2067,7 @@ dws sheet export --node <NODE_ID> --output ./
- `filter-view delete-criteria` 仅清除指定列的条件,不会删除整个筛选视图。如需删除整个筛选视图,请使用 `filter-view delete`
- `filter-view delete-criteria` 如果指定列没有设置筛选条件,调用不会报错
- 筛选视图相关操作需要"可阅读"权限(list / info / list-criteria / get-criteria)或"可编辑"权限(create / update / delete / update-criteria / delete-criteria),不支持跨组织操作
- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
- ★ `export` 仅支持钉钉在线电子表格(axls)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令
- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可
- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试
@@ -56,7 +56,7 @@ Flags:
- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试
- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试
**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
**限制**:仅支持钉钉在线电子表格(axls)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
## 核心工作流
@@ -86,7 +86,7 @@ dws sheet export --node <NODE_ID> --output ./
## 注意事项
- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
- ★ `export` 仅支持钉钉在线电子表格(axls)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令
- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可
- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试
+3 -3
View File
@@ -33,10 +33,10 @@ metadata:
## 环境
- `DWS_SKILL_API_HOST` 覆盖技能 API 地址(默认 `https://aihub.dingtalk.com`)
- `DWS_SKILL_API_HOST` 覆盖技能 API 地址(默认 `https://mcp.dingtalk.com`)
## 兼容提示
- `dws skill find` → 用 `dws skill search --query <关键词>`
- `--scopes` 已废弃(真机打 `Flag --scopes has been deprecated, 请使用 --source 替代` 警告)→ 用 `--source`
- 前置:`skill search` 依赖企业开通技能市场;未开通时真机报「当前企业暂未开放此功能」,属企业级开关,非命令写法问题
- `--scopes` 已废弃(会打印 `Flag --scopes has been deprecated, 请使用 --source 替代` 警告)→ 用 `--source`
- 前置:`skill search` 依赖企业开通技能市场;未开通时报「当前企业暂未开放此功能」,属企业级开关,非命令写法问题
@@ -48,8 +48,8 @@ Flags:
--source string 查询范围,空格分隔。备选值:DingtalkMarket(钉钉市场)、OrgInternal(企业内部)。为空默认查市场技能
```
> `--scopes` 已废弃(真机会打 `Flag --scopes has been deprecated, 请使用 --source 替代` 警告),一律用 `--source`。
> 前置:`skill search` 依赖企业开通技能市场;未开通时真机报「当前企业暂未开放此功能」,这是企业级开关,非命令写法问题。
> `--scopes` 已废弃(会打印 `Flag --scopes has been deprecated, 请使用 --source 替代` 警告),一律用 `--source`。
> 前置:`skill search` 依赖企业开通技能市场;未开通时报「当前企业暂未开放此功能」,这是企业级开关,非命令写法问题。
返回字段:
- `skillId` — 技能 ID(后续 `install` 需要)