Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97390829dd | ||
|
|
7828089081 |
@@ -4,6 +4,48 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and this project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [1.0.15] - 2026-04-23
|
||||
|
||||
Compat layer gains **subcommand merging** under shared parents so multiple server entries can contribute into the same `dws <parent> <branch>` subtree without producing duplicate `--help` rows. Ships with a fresh auto-generated command index doc, a README sync to **159 commands across 13 products**, and a wide-ranging flag-naming cleanup that standardises CLI flags across chat, calendar, drive, minutes, contact, and devdoc commands.
|
||||
|
||||
### Added
|
||||
|
||||
- **`internal/compat` subcommand merging via `attachOrMerge`** — when two or more server entries attach to the same parent (e.g. `parent: "chat"`) and their `cli.command` collides with an existing subcommand in the parent's tree, the new subcommand's children are merged recursively into the existing one instead of creating a duplicate sibling. Leaf-name collisions resolve first-wins. Fixes the "double `group` / `message` rows in `dws chat --help`" symptom when bot capabilities are distributed across `chat.group.members` and `chat.message`.
|
||||
- **`docs/command-index.md`** — a single, English, auto-generated listing of every runtime command the `dws` CLI exposes under the pre environment (159 total). Each entry carries a description and a "when to use" column aimed at AI agents. Replaces the earlier `command-index.pre.*` / `command-index.full.*` ad-hoc snapshots.
|
||||
|
||||
### Changed
|
||||
|
||||
- **README Key Services table** (`README.md` + `README_zh.md`) fully synced to the shipped command surface:
|
||||
- `Chat`: 20 → **23** (bot capabilities merged in; new `list-all` / `list-focused` / `list-unread-conversations` / `conversation-info` exposed)
|
||||
- `Calendar`: 13 → **14**
|
||||
- `AI Tables`: 37 → **41** (chart / dashboard public-share config rows)
|
||||
- `Doc`: 16 → **21** (comment subtree + `file create`)
|
||||
- `Minutes`: 22 → **19** (single-tool `record`, `list query`, `list-by-keyword-range` pruned)
|
||||
- New `Drive` row (6 commands) — promoted out of "Coming soon"
|
||||
- `Workbench` row and standalone `Bot` row removed
|
||||
- Total revised to **159 commands across 13 products**
|
||||
- **Quick Start** expanded to 7 examples covering `doc`, `minutes`, `drive` in addition to `contact`, `calendar`, `todo`
|
||||
- **Coming soon** trimmed to 5: `mail`, `conference`, `aiapp`, `live`, `wiki`
|
||||
- **Reference & Docs** section now leads with a pointer to the new `docs/command-index.md`
|
||||
- **Flag naming cleanup** — CLI flags across chat, calendar, drive, minutes, contact, and devdoc have been standardised so the names users type match the product-skill documentation. Notable flags:
|
||||
- `dws contact user search` / `dws contact dept search` / `dws devdoc article search` now take `--query` (previously `--keyword`)
|
||||
- `dws chat message list` / `dws chat message search` / `dws chat message list-mentions` / `dws chat conversation-info` / `dws chat message send` now take `--group` for the target conversation (previously `--id`) and `--open-dingtalk-id` (previously `--open-id`)
|
||||
- `dws chat message list-by-sender` now takes `--sender-user-id` / `--sender-open-dingtalk-id` (previously `--user` / `--open-id`)
|
||||
- `dws chat message list-topic-replies` now takes `--group` / `--topic-id` / `--limit` / `--time` (previously `--id` / `--topic` / `--size` / `--start`)
|
||||
- `dws chat search-common` now takes `--match-mode` (previously `--mode`)
|
||||
- `dws drive list` now takes `--max` / `--thumbnail` (previously `--max-results` / `--with-thumbnail`)
|
||||
- `dws calendar event suggest` now takes `--users` / `--duration` / `--timezone` (previously `--attendee-user-ids` / `--duration-minutes` / `--time-zone`)
|
||||
- `dws minutes list mine` / `dws minutes list shared` now take `--max` (previously `--max-results`) and gain `--query` / `--start` / `--end`
|
||||
- `dws minutes list all` no longer exposes the legacy `--__scope__` internal alias
|
||||
- **Flag coverage additions** — `dws calendar event create` / `update` gain `--attendees`, `--open-dingtalk-ids`, `--timezone`; `dws chat message send` gains file-message flags (`--dentry-id`, `--file-name`, `--file-size`, `--file-type`, `--media-id`, `--msg-type`, `--space-id`) plus `--open-dingtalk-id` / `--user`; `dws chat message list` gains `--open-dingtalk-id` / `--user`; `dws aitable table delete` gains `--reason`; `dws calendar participant add` gains `--optional`; `dws todo task create` gains `--recurrence`.
|
||||
|
||||
### Tests
|
||||
|
||||
- 3 new unit tests in `internal/compat/dynamic_commands_test.go`:
|
||||
- `TestBuildDynamicCommands_ParentMergeSameName` — two servers with identical `command` + `parent` collapse into a single merged subcommand
|
||||
- `TestBuildDynamicCommands_ParentMergeRecursive` — recursive merge through nested groups (e.g. `chat.group.members`)
|
||||
- `TestBuildDynamicCommands_ParentMergeLeafCollision` — identical leaf paths resolve first-wins without producing duplicates
|
||||
|
||||
## [1.0.13] - 2026-04-22
|
||||
|
||||
IM / Messaging capability expansion: the `chat` (aka `im`) product surface grows from "group + bot messaging" into a full conversational layer — user-identity messaging, message reading & search, personal messages, topic replies, mentions, focused contacts, unread/top/common conversations, org-wide group creation, and first-class bot lifecycle.
|
||||
|
||||
@@ -185,12 +185,17 @@ Credentials are securely persisted after first login (Keychain). Subsequent runs
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
dws contact user search --keyword "engineering" # search contacts
|
||||
dws calendar event list # list calendar events
|
||||
dws contact user search --query "engineering" # search contacts
|
||||
dws calendar event list # list today's calendar events
|
||||
dws doc search --query "quarterly" # search DingTalk Docs
|
||||
dws minutes list mine # list AI meeting notes I created
|
||||
dws drive list # list DingTalk drive files
|
||||
dws todo task create --title "Quarterly report" --executors "<your-userId>" # create a todo (replace <your-userId>)
|
||||
dws todo task list --dry-run # preview without executing
|
||||
```
|
||||
|
||||
> **Full command list**: [`docs/command-index.md`](./docs/command-index.md) — all 159 commands with descriptions and when-to-use guidance.
|
||||
|
||||
## Using with Agents
|
||||
|
||||
dws is designed as an AI-native CLI. Complete [Installation](#installation) and [Getting Started](#getting-started) first, then configure your agent:
|
||||
@@ -350,26 +355,27 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \
|
||||
| Service | Command | Commands | Subcommands | Description |
|
||||
|---------|---------|:--------:|-------------|-------------|
|
||||
| Contact | `contact` | 6 | `user` `dept` | Search users by name/mobile, batch query, departments, current user profile |
|
||||
| Chat / IM | `chat` (alias `im`) | 20 | `message` `group` `search` `list-top-conversations` | User-identity send (group / 1-on-1 / open-dingtalk-id), Markdown + image, @mentions; read & search conversations (list, list-all, topic replies, by-sender, mentions, focused, unread, search, info, top / common groups); group CRUD + member management |
|
||||
| Bot | `chat bot` | 7 | `bot` `group` `message` `search` `create` `search-groups` | Bot create / search, search bot groups; bot-identity group & batch-1:1 messaging, Webhook, message recall; add bot to group |
|
||||
| Calendar | `calendar` | 13 | `event` `room` `participant` `busy` | Events CRUD, meeting room booking, free-busy query, participant management |
|
||||
| Chat / IM | `chat` (alias `im`) | 23 | `message` `group` `bot` `conversation-info` `search` `search-common` `list-top-conversations` | Messages (send / list / list-all / by-sender / mentions / focused / unread / topic replies / search), group CRUD + member management (incl. `add-bot`), bot-identity messaging (`send-by-bot` / `recall-by-bot` / `send-by-webhook`), conversation info, common groups lookup |
|
||||
| Calendar | `calendar` | 14 | `event` `room` `participant` `busy` | Events CRUD + suggested times + attachments, meeting room booking, free-busy query, participant management |
|
||||
| Todo | `todo` | 6 | `task` | Create, list, update, done, get detail, delete |
|
||||
| Approval | `oa` | 9 | `approval` | Approve/reject/revoke, pending tasks, initiated instances, process list |
|
||||
| Approval | `oa` | 9 | `approval` | Approve / reject / revoke, pending / initiated instances, process list, operation records |
|
||||
| Attendance | `attendance` | 4 | `record` `shift` `summary` `rules` | Clock-in records, shift schedules, attendance summary, group rules |
|
||||
| Ding | `ding` | 2 | `message` | Send/recall DING messages |
|
||||
| Ding | `ding` | 2 | `message` | Send / recall DING messages |
|
||||
| Report | `report` | 7 | `create` `list` `detail` `template` `stats` `sent` | Create reports, sent/received list, templates, statistics |
|
||||
| AITable | `aitable` | 37 | `base` `table` `record` `field` `attachment` `template` `chart` `dashboard` `export` `import` `view` | Full CRUD for bases/tables/records/fields; charts/dashboards; data import/export; views; templates |
|
||||
| Doc | `doc` | 16 | `search` `list` `info` `read` `create` `update` `upload` `download` `folder` `block` `comment` | Search, read, create/update documents; block-level editing; file upload/download; comments |
|
||||
| Minutes | `minutes` | 22 | `list` `get` `update` `record` `hot-word` `mind-graph` `replace-text` `speaker` `upload` | List/search AI meeting transcripts; summaries, transcriptions, todos, mind-maps; recording control; speaker management, hot-words, file upload |
|
||||
| Workbench | `workbench` | 2 | `app` | Batch query app details |
|
||||
| DevDoc | `devdoc` | 1 | `article` | Search platform docs and error codes |
|
||||
| AI Tables | `aitable` | 41 | `base` `table` `record` `field` `view` `dashboard` `chart` `import` `export` `attachment` `template` | Full CRUD for Bases / datasheets / records / fields / views; charts & dashboards with public-share configs; data import/export; attachments; templates |
|
||||
| Doc | `doc` | 21 | `search` `list` `info` `read` `create` `update` `upload` `download` `copy` `move` `rename` `file` `folder` `block` `comment` | Search / read / write docs, file & folder create, block-level editing, comments (list / create / reply / create-inline), upload / download |
|
||||
| Drive | `drive` | 6 | `list` `info` `download` `mkdir` `upload-info` `commit` | DingTalk drive file ops: list, info, download, create folders, two-phase upload |
|
||||
| Minutes | `minutes` | 19 | `list` `get` `update` `mind-graph` `speaker` `hot-word` `upload` | List AI meeting notes (mine / shared), details (info / summary / keywords / transcription / todos / batch), title/summary updates, mind map, speaker replace, hot-word, upload session |
|
||||
| DevDoc | `devdoc` | 1 | `article` | Search the DingTalk Open Platform documentation |
|
||||
|
||||
> 152 commands across 14 products. Run `dws --help` for the full list, or `dws <service> --help` for subcommands.
|
||||
> **159 commands across 13 products.** Full listing with descriptions and usage scenarios: [`docs/command-index.md`](./docs/command-index.md). Run `dws --help` for the top-level tree, or `dws <service> --help` for subcommands.
|
||||
|
||||
> **Note on `chat bot`**: bot capabilities (`send-by-bot` / `recall-by-bot` / `add-bot` / `send-by-webhook` / bot search) are merged into the relevant `chat` subtrees (e.g. `dws chat message send-by-bot`, `dws chat group members add-bot`) so the agent-facing command surface stays flat and discoverable. There is no longer a separate top-level `bot` product.
|
||||
|
||||
<details>
|
||||
<summary>Coming soon</summary>
|
||||
|
||||
`mail` (email) · `drive` (cloud drive) · `conference` (video) · `tb` (Teambition) · `aiapp` (AI apps) · `live` (streaming) · `skill` (marketplace)
|
||||
`mail` (email) · `conference` (video) · `aiapp` (AI apps) · `live` (streaming) · `wiki` (knowledge base)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -418,6 +424,7 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \
|
||||
|
||||
## Reference & Docs
|
||||
|
||||
- [Command Index](./docs/command-index.md) — every runtime command (159 total) with description and when-to-use guidance
|
||||
- [Reference](./docs/reference.md) — environment variables, exit codes, output formats, shell completion
|
||||
- [Architecture](./docs/architecture.md) — discovery-driven pipeline, IR, transport layer
|
||||
- [Changelog](./CHANGELOG.md) — release history and migration notes
|
||||
|
||||
+22
-13
@@ -185,12 +185,17 @@ dws auth login --client-id <your-app-key> --client-secret <your-app-secret>
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
dws contact user search --keyword "悟空" # 搜索联系人
|
||||
dws calendar event list # 查看日历日程
|
||||
dws contact user search --query "悟空" # 搜索联系人
|
||||
dws calendar event list # 查看今天的日程
|
||||
dws doc search --query "季度" # 搜索钉钉文档
|
||||
dws minutes list mine # 列出我创建的 AI 听记
|
||||
dws drive list # 列出钉盘文件
|
||||
dws todo task create --title "季度汇报" --executors "<your-userId>" # 创建待办(请替换为真实 userId)
|
||||
dws todo task list --dry-run # 预览操作但不执行
|
||||
```
|
||||
|
||||
> **完整命令列表**:[`docs/command-index.md`](./docs/command-index.md) — 全部 159 条命令,带描述和使用场景。
|
||||
|
||||
## 在 Agent 中使用
|
||||
|
||||
dws 是为 AI Agent 设计的 CLI 工具。请先完成[安装](#安装)和[开始使用](#开始使用),然后配置 Agent 环境:
|
||||
@@ -350,24 +355,27 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \
|
||||
| 服务 | 命令 | 命令数 | 子命令 | 描述 |
|
||||
|------|------|:------:|--------|------|
|
||||
| 通讯录 | `contact` | 6 | `user` `dept` | 按姓名/手机号搜索、批量查询、部门树、当前用户信息 |
|
||||
| 群聊 | `chat` | 10 | `message` `group` `search` | 群增删改查、成员管理、机器人消息、Webhook |
|
||||
| 机器人 | `chat bot` | 6 | `bot` `group` `message` `search` | 机器人创建/搜索、群聊/单聊消息、Webhook、消息撤回 |
|
||||
| 日历 | `calendar` | 13 | `event` `room` `participant` `busy` | 日程增删改查、会议室预订、闲忙查询、参与者管理 |
|
||||
| 群聊 | `chat`(别名 `im`)| 23 | `message` `group` `bot` `conversation-info` `search` `search-common` `list-top-conversations` | 消息(发送 / 列表 / list-all / 按发送者 / @我 / 关注 / 未读 / 话题回复 / 搜索)、群增删改 + 成员管理(含 `add-bot`)、机器人身份消息(`send-by-bot` / `recall-by-bot` / `send-by-webhook`)、会话信息查询、共同群聊 |
|
||||
| 日历 | `calendar` | 14 | `event` `room` `participant` `busy` | 日程 CRUD + 建议时间 + 附件、会议室预订、闲忙查询、参与者管理 |
|
||||
| 待办 | `todo` | 6 | `task` | 创建、列表、修改、完成、详情、删除 |
|
||||
| 审批 | `oa` | 9 | `approval` | 同意/拒绝/撤销、待我审批、我发起的、流程列表 |
|
||||
| 审批 | `oa` | 9 | `approval` | 同意 / 拒绝 / 撤销、待我审批 / 我发起的、流程列表、操作记录 |
|
||||
| 考勤 | `attendance` | 4 | `record` `shift` `summary` `rules` | 打卡记录、排班查询、考勤摘要、考勤组规则 |
|
||||
| DING | `ding` | 2 | `message` | 发送/撤回 DING 消息 |
|
||||
| 日志 | `report` | 7 | `create` `list` `detail` `template` `stats` `sent` | 创建日志、收发列表、模版、统计 |
|
||||
| 智能表格 | `aitable` | 20 | `base` `table` `record` `field` `attachment` `template` | 多维表/数据表/记录/字段全量 CRUD、模板 |
|
||||
| 工作台 | `workbench` | 2 | `app` | 批量查询应用详情 |
|
||||
| 开发者文档 | `devdoc` | 1 | `article` | 搜索开放平台文档与错误码 |
|
||||
| DING | `ding` | 2 | `message` | 发送 / 撤回 DING 消息 |
|
||||
| 日志 | `report` | 7 | `create` `list` `detail` `template` `stats` `sent` | 创建日志、收发列表、模版、详情、统计 |
|
||||
| AI 表格 | `aitable` | 41 | `base` `table` `record` `field` `view` `dashboard` `chart` `import` `export` `attachment` `template` | Base / 数据表 / 记录 / 字段 / 视图 全量 CRUD;图表 + 仪表盘(含分享配置);数据导入导出;附件;模板 |
|
||||
| 文档 | `doc` | 21 | `search` `list` `info` `read` `create` `update` `upload` `download` `copy` `move` `rename` `file` `folder` `block` `comment` | 搜索 / 读写文档、文件与文件夹创建、块级编辑、评论(list / create / reply / create-inline)、上传 / 下载 |
|
||||
| 钉盘 | `drive` | 6 | `list` `info` `download` `mkdir` `upload-info` `commit` | 钉盘文件操作:列表、详情、下载、创建文件夹、两阶段上传 |
|
||||
| AI 听记 | `minutes` | 19 | `list` `get` `update` `mind-graph` `speaker` `hot-word` `upload` | 听记列表(我创建 / 共享给我)、详情(info / summary / keywords / transcription / todos / batch)、标题/摘要更新、思维导图、发言人替换、热词、上传会话 |
|
||||
| 开发者文档 | `devdoc` | 1 | `article` | 搜索钉钉开放平台文档 |
|
||||
|
||||
> 12 个产品,86 个命令。运行 `dws --help` 查看完整列表,或 `dws <service> --help` 查看子命令。
|
||||
> **13 个产品,159 条命令。** 完整命令清单(带描述与使用场景):[`docs/command-index.md`](./docs/command-index.md)。运行 `dws --help` 查看顶层命令树,或 `dws <service> --help` 查看子命令。
|
||||
|
||||
> **关于 `chat bot`**:机器人能力(`send-by-bot` / `recall-by-bot` / `add-bot` / `send-by-webhook` / bot 搜索)已合并到对应的 `chat` 子树下(例如 `dws chat message send-by-bot`、`dws chat group members add-bot`),保持 agent 视角下的命令面扁平易发现。不再有独立的顶层 `bot` 产品。
|
||||
|
||||
<details>
|
||||
<summary>即将推出</summary>
|
||||
|
||||
`doc`(文档)· `mail`(邮箱)· `minutes`(AI 听记)· `drive`(钉盘)· `conference`(视频会议)· `tb`(Teambition)· `aiapp`(AI 应用)· `live`(直播)· `skill`(技能市场)
|
||||
`mail`(邮箱)· `conference`(视频会议)· `aiapp`(AI 应用)· `live`(直播)· `wiki`(知识库)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -418,6 +426,7 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \
|
||||
|
||||
## 参考与文档
|
||||
|
||||
- [命令索引](./docs/command-index.md) — 159 条运行时命令,带描述与使用场景
|
||||
- [参考手册](./docs/reference.md) — 环境变量、退出码、输出格式、Shell 补全
|
||||
- [架构设计](./docs/architecture.md) — 发现驱动管道、IR、Transport 层
|
||||
- [更新日志](./CHANGELOG.md) — 版本历史与迁移说明
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
# dws Command Index
|
||||
|
||||
Every runtime command the `dws` CLI exposes when loaded with the **pre** environment configuration.
|
||||
|
||||
- **Source**: `dws-wukong/envelope/channel/open/pre/config.json`
|
||||
- **Products**: 13
|
||||
- **Total commands**: 159
|
||||
- **Generated from**: `internal/compat.BuildDynamicCommands` rendering of the pre config — the same code path the CLI uses at runtime.
|
||||
|
||||
> Auto-generated. Edit `pre/config.json`, not this file.
|
||||
|
||||
## Global flags
|
||||
|
||||
Every command inherits these flags (documented here once, not repeated per command):
|
||||
|
||||
| Flag | Purpose |
|
||||
|---|---|
|
||||
| `--client-id` | Override OAuth client ID (DingTalk AppKey) |
|
||||
| `--client-secret` | Override OAuth client secret (DingTalk AppSecret) |
|
||||
| `--debug` | Enable debug logging |
|
||||
| `--dry-run` | Preview the request without executing |
|
||||
| `--fields` | Comma-separated output field projection |
|
||||
| `-f, --format` | Output format: `json` \| `table` \| `raw` (default `json`) |
|
||||
| `--jq` | jq expression applied to JSON output |
|
||||
| `--mock` | Return mock data (developer aid) |
|
||||
| `-o, --output` | Write output to a file |
|
||||
| `--timeout` | HTTP request timeout in seconds (default 30) |
|
||||
| `--token` | Override the configured API token |
|
||||
| `-v, --verbose` | Verbose logging |
|
||||
| `-y, --yes` | Skip confirmation prompts (AI-agent mode) |
|
||||
|
||||
## Contents
|
||||
|
||||
- [`dws aitable` — AI Tables](#dws-aitable) · 41 commands
|
||||
- [`dws attendance` — Attendance](#dws-attendance) · 4 commands
|
||||
- [`dws calendar` — Calendar](#dws-calendar) · 14 commands
|
||||
- [`dws chat` — Group Chat / IM](#dws-chat) · 23 commands
|
||||
- [`dws contact` — Contact Directory](#dws-contact) · 6 commands
|
||||
- [`dws devdoc` — Open Platform Docs](#dws-devdoc) · 1 commands
|
||||
- [`dws ding` — DING Messages](#dws-ding) · 2 commands
|
||||
- [`dws doc` — DingTalk Doc](#dws-doc) · 21 commands
|
||||
- [`dws drive` — DingTalk Drive](#dws-drive) · 6 commands
|
||||
- [`dws minutes` — AI Minutes](#dws-minutes) · 19 commands
|
||||
- [`dws oa` — OA Approval](#dws-oa) · 9 commands
|
||||
- [`dws report` — Reports](#dws-report) · 7 commands
|
||||
- [`dws todo` — Todo Tasks](#dws-todo) · 6 commands
|
||||
|
||||
## `dws aitable` — AI Tables
|
||||
|
||||
_AI-powered spreadsheet (Base) with datasheets, fields, records, views, dashboards, charts, import/export, attachments, and templates._
|
||||
|
||||
**41 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws aitable attachment upload` | Request an upload ticket for attaching a file to an AI table attachment-type field. Returns an upload URL and token the caller uses to stream the file. | When the agent needs to attach binary assets (images, PDFs, etc.) to records before creating or updating an attachment field value. |
|
||||
| `dws aitable base create` | Create a new AI table (Base) under the current user's workspace. Returns the newly-created Base ID. | When an agent needs to provision a fresh Base before populating datasheets, fields, and records. |
|
||||
| `dws aitable base delete` | Permanently delete an existing AI table (Base) by ID, removing all its datasheets, views, and records. | When the agent is cleaning up a Base that is no longer needed or was created for a one-off task. |
|
||||
| `dws aitable base get` | Retrieve metadata for a single AI table (Base), including name, owner, and structural summary. | When the agent needs to inspect a specific Base before performing further operations on it. |
|
||||
| `dws aitable base list` | List AI tables (Bases) accessible to the current user, paginated. | When the agent needs to enumerate the user's Bases to pick one by name or index. |
|
||||
| `dws aitable base search` | Search AI tables (Bases) the current user can access by keyword against the Base name. | When the agent knows a partial Base name and needs to resolve it to a Base ID. |
|
||||
| `dws aitable base update` | Update mutable properties of an AI table (Base), such as its name or icon. | When the agent needs to rename or rebrand an existing Base without touching its data. |
|
||||
| `dws aitable chart create` | Create a new chart inside a Base, bound to a datasheet and view with a given configuration. | When the agent is building analytics on top of a datasheet and needs to materialize a chart visualization. |
|
||||
| `dws aitable chart delete` | Delete a chart from a Base by chart ID. | When the agent needs to remove an obsolete or mistakenly-created chart. |
|
||||
| `dws aitable chart get` | Retrieve a chart's full configuration and metadata. | When the agent needs to inspect an existing chart to clone it or adjust its configuration. |
|
||||
| `dws aitable chart share get` | Retrieve the current public-sharing configuration of a chart, including share link and permissions. | When the agent needs to check whether a chart is already shared externally before issuing a link. |
|
||||
| `dws aitable chart share update` | Enable, disable, or update the public-sharing configuration of a chart. | When the agent needs to generate or revoke an external share link for a chart. |
|
||||
| `dws aitable chart update` | Update an existing chart's configuration (type, dimensions, metrics, style). | When the agent iterates on a chart's visualization after reviewing the initial result. |
|
||||
| `dws aitable chart widgets-example` | Return a reference JSON example of chart widget configuration accepted by chart create/update. | When the agent needs a schema template before composing chart configuration payloads. |
|
||||
| `dws aitable dashboard config-example` | Return a reference JSON example of dashboard configuration accepted by dashboard create/update. | When the agent needs a schema template before composing dashboard layout payloads. |
|
||||
| `dws aitable dashboard create` | Create a new dashboard inside a Base with a layout of chart widgets. | When the agent wants to group multiple charts into a single dashboard view for a report or overview page. |
|
||||
| `dws aitable dashboard delete` | Delete a dashboard from a Base by dashboard ID. | When the agent is removing an outdated dashboard. |
|
||||
| `dws aitable dashboard get` | Retrieve a dashboard's layout, widget list, and metadata. | When the agent needs to inspect a dashboard before updating it or cloning it. |
|
||||
| `dws aitable dashboard share get` | Retrieve the current public-sharing configuration of a dashboard. | When the agent needs to verify whether a dashboard has an active external share link. |
|
||||
| `dws aitable dashboard share update` | Enable, disable, or update the public-sharing configuration of a dashboard. | When the agent needs to generate or revoke an external share link for a dashboard. |
|
||||
| `dws aitable dashboard update` | Update an existing dashboard's layout, widgets, or metadata. | When the agent adds, removes, or rearranges charts on an existing dashboard. |
|
||||
| `dws aitable export data` | Export data from a datasheet (optionally scoped to a view) to a downloadable file such as Excel or CSV. | When the agent needs to hand off Base data to an external system or deliver it as an attachment. |
|
||||
| `dws aitable field create` | Create one or more fields in a datasheet with specified types and options. | When the agent is extending a datasheet's schema to capture new attributes. |
|
||||
| `dws aitable field delete` | Delete a field from a datasheet by field ID; all values in that column are removed. | When the agent is cleaning up unused or deprecated columns in a datasheet. |
|
||||
| `dws aitable field get` | Retrieve field definitions for a datasheet, including type, options, and order. | When the agent needs the field schema before constructing record payloads or queries. |
|
||||
| `dws aitable field update` | Update a field's name, type, or options in a datasheet. | When the agent needs to rename a column or change its type/options without recreating it. |
|
||||
| `dws aitable import data` | Import previously-uploaded data (e.g. Excel) into a datasheet as records, optionally creating fields. | When the agent is bulk-loading external data into a Base after a successful import upload. |
|
||||
| `dws aitable import upload` | Request an upload ticket for an import file (Excel/CSV) to be staged before calling import data. | When the agent needs to push a local dataset into a Base and must first stage the file. |
|
||||
| `dws aitable record create` | Insert one or more records into a datasheet with given field values. | When the agent needs to add new rows to a datasheet, individually or in batches. |
|
||||
| `dws aitable record delete` | Delete one or more records from a datasheet by record ID. | When the agent removes rows that are obsolete or were created in error. |
|
||||
| `dws aitable record query` | Query records from a datasheet with optional filters, sort, view scoping, and pagination. | When the agent needs to read row data to reason about it, render it, or feed it into downstream logic. |
|
||||
| `dws aitable record update` | Update field values on one or more existing records by record ID. | When the agent modifies specific row values after reading or computing new data. |
|
||||
| `dws aitable table create` | Create a new datasheet (table) inside a Base. | When the agent needs another table alongside existing ones in the same Base. |
|
||||
| `dws aitable table delete` | Delete a datasheet from a Base by table ID, removing all its records, views, and fields. | When the agent is disposing of a datasheet that is no longer needed. |
|
||||
| `dws aitable table get` | List datasheets within a Base, returning table IDs and names. | When the agent needs to resolve a table name to an ID inside a known Base. |
|
||||
| `dws aitable table update` | Update a datasheet's name or other metadata. | When the agent needs to rename a datasheet without altering its contents. |
|
||||
| `dws aitable template search` | Search the AI table template gallery by keyword. | When the agent needs to suggest or bootstrap from an existing Base template rather than building from scratch. |
|
||||
| `dws aitable view create` | Create a new view (grid, gallery, kanban, etc.) on a datasheet. | When the agent needs an alternate filtered/sorted presentation of the same datasheet data. |
|
||||
| `dws aitable view delete` | Delete a view from a datasheet by view ID. | When the agent is cleaning up unused views. |
|
||||
| `dws aitable view get` | Retrieve view definitions for a datasheet, including filter, sort, and visible-field configuration. | When the agent needs to understand or reuse a view's configuration before querying records through it. |
|
||||
| `dws aitable view update` | Update a view's name, filter, sort, grouping, or visible fields. | When the agent refines an existing view's configuration after inspection. |
|
||||
|
||||
## `dws attendance` — Attendance
|
||||
|
||||
_Attendance check-in records, shifts, and aggregate statistics._
|
||||
|
||||
**4 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws attendance record get` | Query a user's detailed clock-in/clock-out attendance records for a given time range. | When the agent needs to verify punctuality, pull attendance evidence, or build an attendance report for an individual. |
|
||||
| `dws attendance rules` | Query the attendance group the user belongs to along with its attendance rules (schedule, locations, shifts). | When the agent needs to know the user's expected work schedule or attendance policies before interpreting records. |
|
||||
| `dws attendance shift list` | Batch-query the assigned shifts for a set of employees over a date range. | When the agent needs to plan around team shifts or compile a shift-based roster. |
|
||||
| `dws attendance summary` | Retrieve an aggregated attendance summary for a single user (totals of late, early-leave, absence, overtime). | When the agent needs a quick attendance health check without pulling raw records. |
|
||||
|
||||
## `dws calendar` — Calendar
|
||||
|
||||
_Calendar events, participants, meeting rooms, and busy-status queries._
|
||||
|
||||
**14 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws calendar busy search` | Query the busy/free time windows of one or more users over a given range. | When the agent is scheduling a meeting and needs to find a slot where all attendees are free. |
|
||||
| `dws calendar event create` | Create a new calendar event on the user's calendar with title, time, attendees, and optional meeting room. | When the agent schedules a meeting or reminder on behalf of the user. |
|
||||
| `dws calendar event delete` | Delete an existing calendar event by event ID. | When the agent cancels a previously scheduled event. |
|
||||
| `dws calendar event get` | Retrieve the full details of a calendar event, including participants, location, and body. | When the agent needs to inspect an event before updating or referencing it. |
|
||||
| `dws calendar event list` | List calendar events on the user's calendar within a given time range. | When the agent needs an overview of the user's upcoming schedule or a day's agenda. |
|
||||
| `dws calendar event suggest` | Suggest candidate meeting time slots based on participants' busy/free data and constraints. | When the agent is coordinating a meeting and wants ranked time suggestions rather than raw busy data. |
|
||||
| `dws calendar event update` | Update an existing calendar event's fields such as time, title, participants, or location. | When the agent needs to reschedule or amend a previously created event. |
|
||||
| `dws calendar participant add` | Add one or more participants to an existing calendar event. | When the agent invites additional attendees after the event has been created. |
|
||||
| `dws calendar participant delete` | Remove one or more participants from an existing calendar event. | When the agent drops attendees who no longer need to join the event. |
|
||||
| `dws calendar participant list` | List current participants of a calendar event along with their response status. | When the agent needs to check who is attending before sending follow-up reminders. |
|
||||
| `dws calendar room add` | Book a specific meeting room onto an existing calendar event. | When the agent needs to attach a physical meeting room to an already-scheduled event. |
|
||||
| `dws calendar room delete` | Release a previously booked meeting room from a calendar event. | When the agent cancels or changes the room on an existing event. |
|
||||
| `dws calendar room list-groups` | List meeting room groups (usually by building or floor) available to the user. | When the agent is narrowing down rooms by location before running an availability search. |
|
||||
| `dws calendar room search` | Search meeting rooms by keyword within a group, optionally filtering to rooms free during a given window via `--available`. | When the agent needs to find a suitable room, typically free at a specific time, prior to booking. |
|
||||
|
||||
## `dws chat` — Group Chat / IM
|
||||
|
||||
_Group chats, conversations, messages, and robot/webhook integrations._
|
||||
|
||||
**23 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws chat bot search` | Search robots (bots) created by the current user by keyword. | When the agent needs to resolve one of its own bots by name to a robot code before sending bot messages. |
|
||||
| `dws chat conversation-info` | Retrieve basic metadata for a conversation (single chat or group chat) by conversation ID. | When the agent needs context about a conversation (name, type, member count) before operating on it. |
|
||||
| `dws chat group create` | Create a new internal group chat with a set of initial members. | When the agent needs to spin up a dedicated group for a new project, incident, or discussion thread. |
|
||||
| `dws chat group members` | List members of a group chat; can also be used against the current user to enumerate their groups' members. | When the agent needs the roster of a group before mentioning, removing, or auditing members. |
|
||||
| `dws chat group members add` | Add one or more users to an existing group chat. | When the agent expands a group to include additional participants. |
|
||||
| `dws chat group members add-bot` | Add a robot (bot) to an existing group chat so the bot can post messages there. | When the agent needs to enable bot-driven notifications in a group that does not yet contain the bot. |
|
||||
| `dws chat group members remove` | Remove one or more members from a group chat. | When the agent kicks users who should no longer have access to the group. |
|
||||
| `dws chat group rename` | Update the display name of a group chat. | When the agent is rebranding or clarifying the purpose of an existing group. |
|
||||
| `dws chat list-top-conversations` | Fetch the list of conversations the current user has pinned to the top of their chat list. | When the agent needs to prioritize the user's most important conversations in a summary or dashboard. |
|
||||
| `dws chat message list` | Pull the recent message history of a specific conversation (v2), paginated. | When the agent needs to read what has recently been said in a conversation to summarize or reason about it. |
|
||||
| `dws chat message list-all` | Search all messages across the current user's conversations within a time range. | When the agent needs to audit or summarize everything the user saw across chats in a window. |
|
||||
| `dws chat message list-by-sender` | Fetch messages authored by a specific sender across both single and group chats. | When the agent needs to pull everything a particular colleague said recently. |
|
||||
| `dws chat message list-focused` | Fetch messages from users the current user has marked as "special focus" (starred contacts). | When the agent builds a priority-inbox view highlighting messages from important people. |
|
||||
| `dws chat message list-mentions` | Fetch messages where the current user was @-mentioned. | When the agent wants to surface items that explicitly require the user's attention. |
|
||||
| `dws chat message list-topic-replies` | Pull replies under a specific group topic thread. | When the agent needs the conversation tree of a threaded discussion rather than the flat message list. |
|
||||
| `dws chat message list-unread-conversations` | Fetch the list of conversations that currently have unread messages for the user. | When the agent builds a "catch me up" triage view of what still needs reading. |
|
||||
| `dws chat message recall-by-bot` | Recall (retract) a message previously sent by a robot in a group chat. | When the agent sent a bot message in error or with incorrect content and needs to withdraw it. |
|
||||
| `dws chat message search` | Search messages by keyword across the user's conversations. | When the agent needs to locate a specific statement or link the user remembers from chat history. |
|
||||
| `dws chat message send` | Send a message into a group chat or single chat as the authenticated user. | When the agent needs to relay a response to a user or notify a group on behalf of the human operator. |
|
||||
| `dws chat message send-by-bot` | Send a group message as a specific robot (bot) the user owns. | When the agent posts automated notifications under a bot identity rather than as the user. |
|
||||
| `dws chat message send-by-webhook` | Send a group message via a custom-robot incoming webhook URL. | When the agent needs to post to a group using a webhook without requiring full bot-permission setup. |
|
||||
| `dws chat search` | Search group conversations the user belongs to by group name keyword. | When the agent needs to resolve a group name to a conversation ID. |
|
||||
| `dws chat search-common` | Find group chats the current user and a specified other user both belong to. | When the agent needs an existing shared channel to contact another user without creating a new group. |
|
||||
|
||||
## `dws contact` — Contact Directory
|
||||
|
||||
_Users, departments, and directory lookups._
|
||||
|
||||
**6 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws contact dept list-members` | List members of a specific department by department ID. | When the agent needs the roster of a department to target communication or build a team overview. |
|
||||
| `dws contact dept search` | Search departments in the organization's contact directory by keyword. | When the agent needs to resolve a department name to a department ID. |
|
||||
| `dws contact user get` | Batch-fetch detailed profile information for one or more users by user ID. | When the agent needs names, titles, emails, or departments for a known set of user IDs. |
|
||||
| `dws contact user get-self` | Retrieve the profile of the currently authenticated user. | When the agent needs to identify who it is acting on behalf of (user ID, name, org). |
|
||||
| `dws contact user search` | Search users in the contact directory by keyword (name, title, etc.). | When the agent needs to resolve a person's display name to a user ID. |
|
||||
| `dws contact user search-mobile` | Look up a user by mobile phone number. | When the agent has only a phone number and needs to find the corresponding DingTalk user. |
|
||||
|
||||
## `dws devdoc` — Open Platform Docs
|
||||
|
||||
_Search the DingTalk Open Platform documentation._
|
||||
|
||||
**1 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws devdoc article search` | Search the DingTalk Open Platform documentation by keyword. | When the agent needs authoritative API reference or guides to answer a developer question. |
|
||||
|
||||
## `dws ding` — DING Messages
|
||||
|
||||
_Send and recall DING messages (priority notifications)._
|
||||
|
||||
**2 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws ding message recall` | Recall (retract) a previously sent DING message. | When the agent sent a DING in error and must withdraw it before recipients act on it. |
|
||||
| `dws ding message send` | Send a DING message (high-priority notification) to one or more recipients via app/SMS/phone. | When the agent needs to page recipients with urgency beyond a normal chat message. |
|
||||
|
||||
## `dws doc` — DingTalk Doc
|
||||
|
||||
_DingTalk Doc: search, browse, read/write, upload/download, files, folders, blocks, comments._
|
||||
|
||||
**21 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws doc block delete` | Delete a block from a DingTalk Doc by block ID. | When the agent is editing a document and needs to remove a specific paragraph, table, or other block. |
|
||||
| `dws doc block insert` | Insert a new block (paragraph, table, image, etc.) into a DingTalk Doc at a given position. | When the agent is programmatically assembling or editing a document's content. |
|
||||
| `dws doc block list` | List the blocks of a DingTalk Doc with their IDs, types, and content. | When the agent needs the structured block tree of a doc before modifying specific blocks. |
|
||||
| `dws doc block update` | Update the content or properties of an existing block in a DingTalk Doc. | When the agent amends a specific paragraph or element without rewriting the whole document. |
|
||||
| `dws doc comment create` | Create a document-level comment on a DingTalk Doc. | When the agent leaves feedback or follow-up notes that apply to the entire document. |
|
||||
| `dws doc comment create-inline` | Create an inline (anchored) comment on a specific text range within a DingTalk Doc. | When the agent needs to attach feedback to a particular passage rather than the whole doc. |
|
||||
| `dws doc comment list` | List comments on a DingTalk Doc, including replies. | When the agent is reviewing outstanding feedback or summarizing comment threads. |
|
||||
| `dws doc comment reply` | Reply to an existing comment on a DingTalk Doc. | When the agent responds to a reviewer's comment inline rather than starting a new thread. |
|
||||
| `dws doc copy` | Copy an existing DingTalk Doc or file to a specified destination folder. | When the agent needs to duplicate a template document into a new location for reuse. |
|
||||
| `dws doc create` | Create a new DingTalk Doc (document type) in a target folder or knowledge base. | When the agent needs a fresh DingTalk Doc to write into. |
|
||||
| `dws doc download` | Download a DingTalk Doc or file to a local path. | When the agent needs the raw file locally for processing or attachment. |
|
||||
| `dws doc file create` | Create a new file node of a given type (doc, sheet, mind map, whiteboard, AI table, etc.) in a target folder. | When the agent provisions any non-plain-document file type inside DingTalk Docs. |
|
||||
| `dws doc folder create` | Create a new folder inside a DingTalk Docs knowledge base or drive location. | When the agent organizes output into a fresh folder before writing files into it. |
|
||||
| `dws doc info` | Retrieve metadata for a document or file (title, type, owner, path, permissions). | When the agent needs descriptive info about a node without fetching its full content. |
|
||||
| `dws doc list` | List the child nodes (files and subfolders) of a folder or knowledge base. | When the agent traverses the document hierarchy to find or enumerate items. |
|
||||
| `dws doc move` | Move a DingTalk Doc or file to a different folder location. | When the agent reorganizes document structure. |
|
||||
| `dws doc read` | Read the content of a DingTalk Doc as Markdown. | When the agent needs the document body as text for summarization, Q&A, or further editing. |
|
||||
| `dws doc rename` | Rename a DingTalk Doc or file. | When the agent needs to change a document's title without altering its contents or location. |
|
||||
| `dws doc search` | Search DingTalk Docs the user can access by keyword. | When the agent needs to locate a document by title or content before reading or editing it. |
|
||||
| `dws doc update` | Update the content of a DingTalk Doc (bulk content rewrite rather than block-level edit). | When the agent has freshly generated content and needs to overwrite a doc's body. |
|
||||
| `dws doc upload` | Obtain upload credentials and URL for uploading a local file as an attachment into DingTalk Docs or a knowledge base. | When the agent needs to stage a local file for attachment into the DingTalk Docs system. |
|
||||
|
||||
## `dws drive` — DingTalk Drive
|
||||
|
||||
_DingTalk Drive file and folder management._
|
||||
|
||||
**6 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws drive commit` | Commit a file upload to DingTalk Drive after the binary has been pushed to the presigned URL. | When the agent finalizes a Drive upload step; pairs with `drive upload-info`. |
|
||||
| `dws drive download` | Fetch a temporary download URL for a file stored in DingTalk Drive. | When the agent needs to retrieve a Drive-hosted file for local use or for handing to another service. |
|
||||
| `dws drive info` | Retrieve metadata for a file or folder in DingTalk Drive. | When the agent inspects a Drive node before downloading, moving, or listing around it. |
|
||||
| `dws drive list` | List the files and subfolders of a DingTalk Drive folder. | When the agent needs to enumerate Drive contents to find or pick items. |
|
||||
| `dws drive mkdir` | Create a new folder in DingTalk Drive. | When the agent organizes Drive output into a fresh folder before uploading files. |
|
||||
| `dws drive upload-info` | Obtain a presigned upload URL and token for pushing a local file into DingTalk Drive. | When the agent starts a Drive upload; pairs with `drive commit` to finalize. |
|
||||
|
||||
## `dws minutes` — AI Minutes
|
||||
|
||||
_AI meeting notes: listing, summary, todos, transcription, recording control, mind maps, speakers, hot words, uploads._
|
||||
|
||||
**19 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws minutes get batch` | Batch-fetch detailed metadata for multiple meeting notes (AI minutes) by ID. | When the agent needs to enrich a list of minutes IDs with titles, durations, and participants in one call. |
|
||||
| `dws minutes get info` | Retrieve basic metadata for a single meeting note (title, owner, time, duration, participants). | When the agent needs a header view of a specific meeting note. |
|
||||
| `dws minutes get keywords` | Retrieve the extracted keywords of a meeting note. | When the agent needs topical tags for a meeting without pulling the full transcript or summary. |
|
||||
| `dws minutes get summary` | Retrieve the AI-generated summary of a meeting note. | When the agent needs a concise recap of a meeting for reporting or follow-up. |
|
||||
| `dws minutes get todos` | Retrieve the action items (todos) extracted from a meeting note. | When the agent needs to convert meeting action items into tasks or follow up on commitments. |
|
||||
| `dws minutes get transcription` | Retrieve the raw speech-to-text transcription of a meeting note. | When the agent needs the full verbatim transcript for deep analysis or quoting. |
|
||||
| `dws minutes hot-word add` | Add a custom personal hot word to improve future speech-recognition accuracy on the user's minutes. | When the user has domain-specific jargon or proper nouns that the ASR model mistranscribes. |
|
||||
| `dws minutes list all` | List all meeting notes the user has access to, filterable by keyword and time range. | When the agent needs a broad search across the user's full minutes library. |
|
||||
| `dws minutes list mine` | List only the meeting notes the current user created. | When the agent scopes results to the user's own recordings rather than shared ones. |
|
||||
| `dws minutes list shared` | List meeting notes that have been shared with the current user by others. | When the agent wants to surface meetings the user is an invited viewer of. |
|
||||
| `dws minutes mind-graph create` | Generate a mind map from a meeting note asynchronously. | When the agent wants a structured mind-map visualization of a meeting's content. |
|
||||
| `dws minutes mind-graph status` | Query the generation status of a mind-map job and fetch the result when ready. | When the agent polls after `mind-graph create` to retrieve the finished mind map. |
|
||||
| `dws minutes replace-text` | Find and replace matching text across a meeting note's transcript paragraphs and summary. | When the agent corrects a systemic transcription mistake (e.g. wrong product name) throughout a note. |
|
||||
| `dws minutes speaker replace` | Reassign speaker labels in a meeting note (e.g. map "Speaker 1" to a specific user). | When the agent cleans up speaker diarization after automatic labels came out wrong. |
|
||||
| `dws minutes update summary` | Overwrite the summary content of a meeting note. | When the agent refines or replaces the AI-generated summary with a corrected or customized version. |
|
||||
| `dws minutes update title` | Update the title of a meeting note. | When the agent renames a meeting note for clarity before sharing or archiving. |
|
||||
| `dws minutes upload cancel` | Cancel an in-progress meeting-note file upload session. | When the agent aborts a multi-step upload due to user cancellation or upstream error. |
|
||||
| `dws minutes upload complete` | Complete an upload session and create a meeting note from the uploaded audio/video. | When the agent finalizes a minutes upload, triggering transcription and AI processing. |
|
||||
| `dws minutes upload create` | Create a file upload session for producing a meeting note from a local audio/video file. | When the agent begins uploading a recording to be turned into a meeting note. |
|
||||
|
||||
## `dws oa` — OA Approval
|
||||
|
||||
_OA approval workflows: list, approve, reject, revoke, records._
|
||||
|
||||
**9 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws oa approval approve` | Approve a pending approval process instance (task) as the current user. | When the agent acts on a pending approval the user has delegated it to handle. |
|
||||
| `dws oa approval detail` | Retrieve full details of an approval process instance, including form fields, attachments, and state. | When the agent needs to read the content of an approval ticket before deciding on it or summarizing it. |
|
||||
| `dws oa approval list-forms` | List approval process templates (forms) the current user is allowed to initiate. | When the agent needs to pick the right approval form before submitting a new request. |
|
||||
| `dws oa approval list-initiated` | List approval process instances the current user has initiated. | When the agent reviews the status of approvals the user submitted. |
|
||||
| `dws oa approval list-pending` | List approval process instances currently awaiting action from the current user. | When the agent surfaces "needs your approval" items in the user's inbox. |
|
||||
| `dws oa approval records` | Retrieve the operation history (who approved/commented/transferred, when) of an approval instance. | When the agent explains an approval's progression or audits who handled it. |
|
||||
| `dws oa approval reject` | Reject a pending approval process instance as the current user. | When the agent declines an approval on behalf of the user, optionally with a reason. |
|
||||
| `dws oa approval revoke` | Revoke an approval process instance previously initiated by the current user. | When the agent withdraws an approval request the user no longer wants to pursue. |
|
||||
| `dws oa approval tasks` | List pending approval task IDs assigned to the current user, used to drive approve/reject actions. | When the agent needs task IDs (not just instance IDs) before calling approve/reject. |
|
||||
|
||||
## `dws report` — Reports
|
||||
|
||||
_DingTalk Report feature: templates, entries, and statistics._
|
||||
|
||||
**7 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws report create` | Create a new report (DingTalk "Report" entry) based on a report template with filled-in content. | When the agent submits a daily/weekly report on behalf of the user. |
|
||||
| `dws report detail` | Retrieve the full details of a specific report entry, including fields and recipients. | When the agent needs to read a report's content for summarization or follow-up. |
|
||||
| `dws report list` | List reports the current user has received from others. | When the agent digests the user's incoming reports (e.g. team members' weeklies). |
|
||||
| `dws report sent` | List reports the current user has created and sent out. | When the agent reviews the user's own reporting history. |
|
||||
| `dws report stats` | Retrieve aggregated statistics for a report entry by ID (views, likes, comments, etc.). | When the agent measures engagement or reach of a report the user sent. |
|
||||
| `dws report template detail` | Retrieve the detailed schema of a report template by name, including required fields. | When the agent needs to know a template's field structure before calling `report create`. |
|
||||
| `dws report template list` | List the report templates the current user is allowed to use. | When the agent picks the correct report template (e.g. "weekly", "daily") before creating a report. |
|
||||
|
||||
## `dws todo` — Todo Tasks
|
||||
|
||||
_Personal todo task management._
|
||||
|
||||
**6 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws todo task create` | Create a personal todo item for the current user with title, due time, and optional executors. | When the agent captures an action item as a tracked todo in the user's DingTalk todo list. |
|
||||
| `dws todo task delete` | Delete a todo item by ID. | When the agent removes a todo that is no longer relevant. |
|
||||
| `dws todo task done` | Update the completion status of a todo's executor (mark done or undone). | When the agent marks an action item as completed after confirming the work is finished. |
|
||||
| `dws todo task get` | Retrieve the full details of a todo item by ID. | When the agent inspects a specific todo's content, due date, and executors. |
|
||||
| `dws todo task list` | List todos for the current user within the current organization. | When the agent surfaces the user's outstanding tasks or builds a daily focus list. |
|
||||
| `dws todo task update` | Update a todo's title, description, due time, or executors. | When the agent edits an existing todo after new information comes in. |
|
||||
|
||||
@@ -32,6 +32,7 @@ With `-f json`, error responses include structured payloads: `category`, `reason
|
||||
dws contact user search --keyword "Alice" -f table # Table (default, human-friendly / 表格,默认)
|
||||
dws contact user search --keyword "Alice" -f json # JSON (for agents and piping / 适合 agent)
|
||||
dws contact user search --keyword "Alice" -f raw # Raw API response / 原始响应
|
||||
dws schema -f pretty ding.send_ding_message # Pretty (ANSI-colored, schema-aware / 彩色分区,专为 schema 设计)
|
||||
```
|
||||
|
||||
## Dry Run / 试运行
|
||||
@@ -46,6 +47,46 @@ dws todo task list --dry-run # Preview MCP call without executing / 预览但
|
||||
dws contact user search --keyword "Alice" -o result.json
|
||||
```
|
||||
|
||||
## Schema Introspection / Schema 查询
|
||||
|
||||
`dws schema` 查询已发现的 MCP 产品和工具元数据。不带参数列出所有产品,带路径输出单个工具的完整 schema。
|
||||
|
||||
### 路径写法
|
||||
|
||||
```bash
|
||||
dws schema # 列出所有产品 + 工具名
|
||||
dws schema ding.send_ding_message # canonical: product.rpc_name
|
||||
dws schema ding.message.send # CLI 点路径: product.group.cli_name
|
||||
dws schema "ding message send" # CLI 空格路径(同上)
|
||||
dws schema --cli-path "ding message send" # 显式 flag(脚本友好,免转义)
|
||||
dws schema -f pretty ding.send_ding_message # ANSI 着色分区展示(人肉查看最舒服)
|
||||
```
|
||||
|
||||
Canonical 路径先匹配;落空后走 CLI 路径(product → group.. → cli_name)。
|
||||
|
||||
### 单工具输出字段
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `name` / `cli_name` / `canonical_path` | MCP RPC 名 / CLI 叶子名 / `product.rpc_name` |
|
||||
| `group` | CLI 父级 group 路径(dot-separated) |
|
||||
| `title` / `description` | 工具名/说明(overlay 优先) |
|
||||
| `parameters` / `required` | MCP 输入 JSON Schema 的 properties / required |
|
||||
| `output_schema` | MCP 输出 Schema(上游下发时才有) |
|
||||
| `sensitive` | 敏感写操作,需 `--yes` 确认 |
|
||||
| `annotations.destructive_hint` | 对齐 MCP 2025+ annotations,目前从 `sensitive` 映射 |
|
||||
| `flag_overlay[param]` | CLI 层对 MCP 参数的改写:`alias` / `transform` / `transform_args` / `env_default` / `default` / `hidden` |
|
||||
|
||||
**调试 `--flag` 行为的第一站**是 `flag_overlay` —— 比如 `--users 0232...` 能不能直接用,看 `receiverUserIdList.transform == "csv_to_array"` 即可判断。
|
||||
|
||||
### 筛选输出
|
||||
|
||||
```bash
|
||||
dws schema ding.send_ding_message --jq '.tool.flag_overlay' # 只看 overlay
|
||||
dws schema --jq '.products[] | {id, count: (.tools|length)}' # 各产品工具数
|
||||
dws schema aitable.delete_base --jq '.tool.annotations' # 敏感操作提示
|
||||
```
|
||||
|
||||
## Shell Completion / 自动补全
|
||||
|
||||
```bash
|
||||
|
||||
@@ -41,7 +41,7 @@ func bindPersistentFlags(cmd *cobra.Command, flags *GlobalFlags) {
|
||||
cmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "显示调试日志")
|
||||
cmd.PersistentFlags().BoolVar(&flags.DryRun, "dry-run", false, "预览操作内容,不实际执行")
|
||||
cmd.PersistentFlags().StringVar(&flags.Fields, "fields", "", "筛选输出字段 (逗号分隔, 如: name,id,status)")
|
||||
cmd.PersistentFlags().StringVarP(&flags.Format, "format", "f", "json", "输出格式: json|table|raw")
|
||||
cmd.PersistentFlags().StringVarP(&flags.Format, "format", "f", "json", "输出格式: json|table|raw|pretty")
|
||||
cmd.PersistentFlags().StringVar(&flags.JQ, "jq", "", "jq 表达式过滤输出 (如: '.items[] | .name')")
|
||||
cmd.PersistentFlags().BoolVar(&flags.Mock, "mock", false, "使用 Mock 数据 (开发调试用)")
|
||||
cmd.PersistentFlags().StringVarP(&flags.Output, "output", "o", "", "Write command output to a file")
|
||||
|
||||
+113
-18
@@ -104,22 +104,38 @@ func NewMCPCommand(ctx context.Context, loader CatalogLoader, runner executor.Ru
|
||||
}
|
||||
|
||||
func NewSchemaCommand(loader CatalogLoader) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "schema [product.tool]",
|
||||
cmd := &cobra.Command{
|
||||
Use: "schema [path]",
|
||||
Short: "查看 MCP 工具 Schema (产品列表 / 工具参数)",
|
||||
Long: `查看已发现的 MCP 产品和工具的 Schema 元数据。
|
||||
|
||||
不带参数时列出所有产品及其工具数量;带 product.tool 路径时
|
||||
输出该工具的完整输入 Schema(JSON Schema 格式)。
|
||||
不带参数时列出所有产品及其工具数量;带路径时输出该工具的完整
|
||||
输入 Schema(JSON Schema 格式)、输出 Schema、MCP 注解和 CLI
|
||||
层的 flag overlay(alias/transform/env_default)。
|
||||
|
||||
路径支持三种写法:
|
||||
product.rpc_name 规范路径 (e.g. ding.send_ding_message)
|
||||
product.group.cli_name CLI 点路径 (e.g. ding.message.send)
|
||||
"product group cli_name" CLI 空格/斜杠路径 (e.g. "ding message send")
|
||||
|
||||
示例:
|
||||
dws schema # 列出所有产品
|
||||
dws schema aitable.query_records # 查看 aitable query_records 的参数 Schema
|
||||
dws schema --fields id,tools # 只显示 id 和 tools 字段
|
||||
dws schema --jq '.products[].id' # 用 jq 提取所有产品 ID`,
|
||||
dws schema # 列出所有产品
|
||||
dws schema ding.send_ding_message # 规范路径
|
||||
dws schema "ding message send" # CLI 路径(空格)
|
||||
dws schema --cli-path "ding message send" # 同上,显式 flag(脚本友好)
|
||||
dws schema -f pretty ding.send_ding_message # ANSI 彩色分区展示
|
||||
dws schema --jq '.tool.flag_overlay' # 只看 CLI overlay`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cliPath, _ := cmd.Flags().GetString("cli-path")
|
||||
cliPath = strings.TrimSpace(cliPath)
|
||||
if cliPath != "" {
|
||||
if len(args) > 0 {
|
||||
return apperrors.NewValidation("--cli-path and positional argument are mutually exclusive")
|
||||
}
|
||||
args = []string{cliPath}
|
||||
}
|
||||
catalog, err := loader.Load(cmd.Context())
|
||||
if err != nil {
|
||||
var degraded *CatalogDegraded
|
||||
@@ -158,6 +174,8 @@ func NewSchemaCommand(loader CatalogLoader) *cobra.Command {
|
||||
)
|
||||
},
|
||||
}
|
||||
cmd.Flags().String("cli-path", "", "按 CLI 命令路径查询 (等同于位置参数,便于脚本使用无需转义)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func BuildFlagSpecs(schema map[string]any, hints map[string]ir.CLIFlagHint) []FlagSpec {
|
||||
@@ -664,7 +682,7 @@ func schemaPayload(catalog ir.Catalog, args []string) (map[string]any, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
product, tool, ok := catalog.FindTool(args[0])
|
||||
product, tool, ok := resolveSchemaPath(catalog, args[0])
|
||||
if !ok {
|
||||
return nil, apperrors.NewValidation(fmt.Sprintf("unknown canonical schema path %q", args[0]))
|
||||
}
|
||||
@@ -676,23 +694,100 @@ func schemaPayload(catalog ir.Catalog, args []string) (map[string]any, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// compactTool returns a lean representation of a tool for schema
|
||||
// output, keeping only the fields that AI agents and developers
|
||||
// need: name, description, parameters, and sensitivity flag.
|
||||
func compactTool(t ir.ToolDescriptor) map[string]any {
|
||||
tool := map[string]any{
|
||||
"name": t.RPCName,
|
||||
"title": t.Title,
|
||||
"description": t.Description,
|
||||
"sensitive": t.Sensitive,
|
||||
// resolveSchemaPath accepts three input forms and maps to (product, tool):
|
||||
// - "product.rpc_name" (canonical, e.g. "ding.send_ding_message")
|
||||
// - "product.cli_name" (single-level CLI path, e.g. "doc.create")
|
||||
// - CLI path with group ("ding message send" or "ding.message.send";
|
||||
// also accepts "/" and multiple whitespace between tokens)
|
||||
//
|
||||
// Canonical form is tried first so existing callers and scripts keep
|
||||
// working; only when that fails does the CLI-path resolver run.
|
||||
func resolveSchemaPath(catalog ir.Catalog, raw string) (ir.CanonicalProduct, ir.ToolDescriptor, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ir.CanonicalProduct{}, ir.ToolDescriptor{}, false
|
||||
}
|
||||
|
||||
if product, tool, ok := catalog.FindTool(raw); ok {
|
||||
return product, tool, true
|
||||
}
|
||||
|
||||
tokens := splitSchemaPathTokens(raw)
|
||||
if len(tokens) < 2 {
|
||||
return ir.CanonicalProduct{}, ir.ToolDescriptor{}, false
|
||||
}
|
||||
|
||||
productID := tokens[0]
|
||||
leaf := tokens[len(tokens)-1]
|
||||
groupPath := strings.Join(tokens[1:len(tokens)-1], ".")
|
||||
|
||||
product, ok := catalog.FindProduct(productID)
|
||||
if !ok {
|
||||
return ir.CanonicalProduct{}, ir.ToolDescriptor{}, false
|
||||
}
|
||||
|
||||
for _, tool := range product.Tools {
|
||||
if tool.CLIName != leaf {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(tool.Group) != groupPath {
|
||||
continue
|
||||
}
|
||||
return product, tool, true
|
||||
}
|
||||
return ir.CanonicalProduct{}, ir.ToolDescriptor{}, false
|
||||
}
|
||||
|
||||
// splitSchemaPathTokens splits a CLI path on dots, slashes, and
|
||||
// whitespace, returning only non-empty tokens. "ding message send",
|
||||
// "ding.message.send", and "ding/message/send" all yield the same
|
||||
// three tokens.
|
||||
func splitSchemaPathTokens(raw string) []string {
|
||||
fields := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == '.' || r == '/' || r == ' ' || r == '\t'
|
||||
})
|
||||
out := fields[:0]
|
||||
for _, f := range fields {
|
||||
if s := strings.TrimSpace(f); s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// compactTool returns a lean representation of a tool for schema
|
||||
// output, keeping the fields AI agents and scripts need: RPC + CLI
|
||||
// identity, input/output schema, sensitivity, MCP annotations, and the
|
||||
// CLI flag overlay (alias/transform/envDefault/default) that shapes
|
||||
// how raw MCP parameters appear on the command line.
|
||||
func compactTool(t ir.ToolDescriptor) map[string]any {
|
||||
tool := map[string]any{
|
||||
"name": t.RPCName,
|
||||
"cli_name": t.CLIName,
|
||||
"canonical_path": t.CanonicalPath,
|
||||
"title": t.Title,
|
||||
"description": t.Description,
|
||||
"sensitive": t.Sensitive,
|
||||
}
|
||||
|
||||
if strings.TrimSpace(t.Group) != "" {
|
||||
tool["group"] = t.Group
|
||||
}
|
||||
if props, ok := t.InputSchema["properties"]; ok {
|
||||
tool["parameters"] = props
|
||||
}
|
||||
if req := requiredFields(t.InputSchema); len(req) > 0 {
|
||||
tool["required"] = req
|
||||
}
|
||||
if len(t.OutputSchema) > 0 {
|
||||
tool["output_schema"] = t.OutputSchema
|
||||
}
|
||||
if t.Annotations != nil {
|
||||
tool["annotations"] = t.Annotations
|
||||
}
|
||||
if len(t.FlagOverlay) > 0 {
|
||||
tool["flag_overlay"] = t.FlagOverlay
|
||||
}
|
||||
|
||||
return tool
|
||||
}
|
||||
|
||||
@@ -121,6 +121,205 @@ func TestSchemaPayloadFindsTool(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactToolEmitsExtendedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
destructive := true
|
||||
tool := ir.ToolDescriptor{
|
||||
RPCName: "send_ding_message",
|
||||
CLIName: "send",
|
||||
Group: "message",
|
||||
CanonicalPath: "ding.send_ding_message",
|
||||
Title: "发送DING消息",
|
||||
Description: "desc",
|
||||
Sensitive: true,
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []any{"robotCode"},
|
||||
"properties": map[string]any{
|
||||
"robotCode": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
OutputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"openDingId": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
Annotations: &ir.ToolAnnotations{DestructiveHint: &destructive},
|
||||
FlagOverlay: map[string]ir.FlagOverlay{
|
||||
"receiverUserIdList": {Alias: "users", Transform: "csv_to_array"},
|
||||
},
|
||||
}
|
||||
|
||||
out := compactTool(tool)
|
||||
if out["name"] != "send_ding_message" {
|
||||
t.Errorf("name = %v", out["name"])
|
||||
}
|
||||
if out["cli_name"] != "send" {
|
||||
t.Errorf("cli_name = %v", out["cli_name"])
|
||||
}
|
||||
if out["canonical_path"] != "ding.send_ding_message" {
|
||||
t.Errorf("canonical_path = %v", out["canonical_path"])
|
||||
}
|
||||
if out["group"] != "message" {
|
||||
t.Errorf("group = %v", out["group"])
|
||||
}
|
||||
if _, ok := out["output_schema"]; !ok {
|
||||
t.Errorf("output_schema missing, keys = %v", keysOf(out))
|
||||
}
|
||||
if _, ok := out["annotations"]; !ok {
|
||||
t.Errorf("annotations missing, keys = %v", keysOf(out))
|
||||
}
|
||||
overlay, ok := out["flag_overlay"].(map[string]ir.FlagOverlay)
|
||||
if !ok {
|
||||
t.Fatalf("flag_overlay type = %T", out["flag_overlay"])
|
||||
}
|
||||
if overlay["receiverUserIdList"].Alias != "users" {
|
||||
t.Errorf("overlay alias = %q", overlay["receiverUserIdList"].Alias)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactToolOmitsEmptyExtras(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tool := ir.ToolDescriptor{
|
||||
RPCName: "list_documents",
|
||||
CLIName: "list",
|
||||
CanonicalPath: "doc.list_documents",
|
||||
InputSchema: map[string]any{"type": "object"},
|
||||
}
|
||||
out := compactTool(tool)
|
||||
for _, key := range []string{"output_schema", "annotations", "flag_overlay", "group"} {
|
||||
if _, has := out[key]; has {
|
||||
t.Errorf("key %q should be omitted when empty, got %#v", key, out[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaPayloadResolvesCLIPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
catalog := ir.Catalog{
|
||||
Products: []ir.CanonicalProduct{
|
||||
{
|
||||
ID: "ding",
|
||||
Tools: []ir.ToolDescriptor{
|
||||
{
|
||||
RPCName: "send_ding_message",
|
||||
CLIName: "send",
|
||||
Group: "message",
|
||||
CanonicalPath: "ding.send_ding_message",
|
||||
InputSchema: map[string]any{"type": "object"},
|
||||
},
|
||||
{
|
||||
RPCName: "recall_ding_message",
|
||||
CLIName: "recall",
|
||||
Group: "message",
|
||||
CanonicalPath: "ding.recall_ding_message",
|
||||
InputSchema: map[string]any{"type": "object"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
wantRPC string
|
||||
wantErr bool
|
||||
}{
|
||||
{"canonical rpc path", "ding.send_ding_message", "send_ding_message", false},
|
||||
{"dotted cli path", "ding.message.send", "send_ding_message", false},
|
||||
{"space cli path", "ding message send", "send_ding_message", false},
|
||||
{"slash cli path", "ding/message/recall", "recall_ding_message", false},
|
||||
{"unknown leaf", "ding message nope", "", true},
|
||||
{"unknown group", "ding random send", "", true},
|
||||
{"unknown product", "nope send", "", true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
payload, err := schemaPayload(catalog, []string{tc.input})
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("err = %v, wantErr = %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr {
|
||||
return
|
||||
}
|
||||
tool, ok := payload["tool"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("payload tool missing, got %#v", payload)
|
||||
}
|
||||
if tool["name"] != tc.wantRPC {
|
||||
t.Errorf("tool name = %v, want %s", tool["name"], tc.wantRPC)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaCommandCLIPathFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
loader := StaticLoader{Catalog: ir.Catalog{
|
||||
Products: []ir.CanonicalProduct{{
|
||||
ID: "ding",
|
||||
Tools: []ir.ToolDescriptor{{
|
||||
RPCName: "send_ding_message",
|
||||
CLIName: "send",
|
||||
Group: "message",
|
||||
CanonicalPath: "ding.send_ding_message",
|
||||
InputSchema: map[string]any{"type": "object"},
|
||||
}},
|
||||
}},
|
||||
}}
|
||||
|
||||
t.Run("resolves via --cli-path", func(t *testing.T) {
|
||||
cmd := NewSchemaCommand(loader)
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
cmd.SetArgs([]string{"--cli-path", "ding message send"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
tool, ok := payload["tool"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("tool missing: %#v", payload)
|
||||
}
|
||||
if tool["name"] != "send_ding_message" {
|
||||
t.Errorf("tool name = %v", tool["name"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects positional + flag collision", func(t *testing.T) {
|
||||
cmd := NewSchemaCommand(loader)
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
cmd.SetArgs([]string{"--cli-path", "ding message send", "ding.send_ding_message"})
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatalf("expected mutual-exclusion error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mutually exclusive") {
|
||||
t.Errorf("err = %v, want mutual-exclusion message", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func keysOf(m map[string]any) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestNewMCPCommandReturnsLoaderErrorForInvocations(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ func BuildDynamicCommands(servers []market.ServerDescriptor, runner executor.Run
|
||||
}
|
||||
for _, child := range children {
|
||||
if parent, ok := topLevel[child.parent]; ok {
|
||||
parent.AddCommand(child.cmd)
|
||||
attachOrMerge(parent, child.cmd)
|
||||
} else {
|
||||
// Parent not found among dynamic commands; emit as top-level.
|
||||
name := child.cmd.Name()
|
||||
@@ -389,6 +389,43 @@ func resolveNestedGroup(root *cobra.Command, groupPath string, registry map[stri
|
||||
return ensureNestedGroup(root, groupPath, groupPath, registry)
|
||||
}
|
||||
|
||||
// attachOrMerge adds child as a sub-command of parent. If parent already has a
|
||||
// sub-command with the same Name(), the two are merged recursively: child's
|
||||
// sub-commands are moved onto the existing one and child itself is discarded.
|
||||
// Leaf collisions (two commands with the same Name and no further children)
|
||||
// are resolved first-wins — the incoming one is dropped.
|
||||
//
|
||||
// This lets multiple server entries share a cli.command under the same parent,
|
||||
// e.g. bot-message (command="message", parent="chat") can contribute leaves
|
||||
// into the same "message" subtree already built from chat's own toolOverrides,
|
||||
// without creating a duplicate "message" sibling in chat's help output.
|
||||
func attachOrMerge(parent, child *cobra.Command) {
|
||||
existing := findSubcommand(parent, child.Name())
|
||||
if existing == nil {
|
||||
parent.AddCommand(child)
|
||||
return
|
||||
}
|
||||
// Snapshot child's sub-commands before we start moving them (RemoveCommand
|
||||
// mutates the slice we'd be iterating).
|
||||
subs := make([]*cobra.Command, len(child.Commands()))
|
||||
copy(subs, child.Commands())
|
||||
for _, sub := range subs {
|
||||
child.RemoveCommand(sub)
|
||||
attachOrMerge(existing, sub)
|
||||
}
|
||||
}
|
||||
|
||||
// findSubcommand returns the first sub-command of parent with the given name,
|
||||
// or nil if none match.
|
||||
func findSubcommand(parent *cobra.Command, name string) *cobra.Command {
|
||||
for _, sub := range parent.Commands() {
|
||||
if sub.Name() == name {
|
||||
return sub
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildOverrideBindings converts CLIToolOverride flags into FlagBindings and
|
||||
// constructs a Normalizer that applies transform rules.
|
||||
// Implements §2.1-§2.5 of the conversion rules.
|
||||
|
||||
@@ -16,6 +16,8 @@ package compat
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market"
|
||||
)
|
||||
@@ -143,3 +145,210 @@ func TestBuildDynamicCommands_NoParent(t *testing.T) {
|
||||
t.Fatalf("expected 2 top-level commands, got %d", len(cmds))
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildDynamicCommands_ParentMergeSameName covers the case where two
|
||||
// servers share the same cli.command + cli.parent. Instead of producing two
|
||||
// sibling subcommands with the same Name under the parent (which cobra allows
|
||||
// but `--help` renders as duplicate rows), the compat layer merges them into
|
||||
// a single subcommand whose children are the union of both sides.
|
||||
func TestBuildDynamicCommands_ParentMergeSameName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
servers := []market.ServerDescriptor{
|
||||
{
|
||||
Endpoint: "https://endpoint-chat",
|
||||
CLI: market.CLIOverlay{
|
||||
ID: "group-chat",
|
||||
Command: "chat",
|
||||
Groups: map[string]market.CLIGroupDef{
|
||||
"message": {Description: "会话消息管理"},
|
||||
},
|
||||
ToolOverrides: map[string]market.CLIToolOverride{
|
||||
"send_message_as_user": {CLIName: "send", Group: "message"},
|
||||
"list_conversation_message_v2": {CLIName: "list", Group: "message"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Second server contributes more leaves into the same "message"
|
||||
// namespace via command="message" + parent="chat".
|
||||
Endpoint: "https://endpoint-bot",
|
||||
CLI: market.CLIOverlay{
|
||||
ID: "bot-message",
|
||||
Command: "message",
|
||||
Parent: "chat",
|
||||
ToolOverrides: map[string]market.CLIToolOverride{
|
||||
"send_robot_group_message": {CLIName: "send-by-bot"},
|
||||
"send_message_by_custom_robot": {CLIName: "send-by-webhook"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cmds := BuildDynamicCommands(servers, executor.EchoRunner{}, nil)
|
||||
if len(cmds) != 1 || cmds[0].Name() != "chat" {
|
||||
t.Fatalf("expected single top-level 'chat', got %d cmds", len(cmds))
|
||||
}
|
||||
|
||||
// There must be exactly one child named "message" under chat, not two.
|
||||
var messageCmds []*cobra.Command
|
||||
for _, sub := range cmds[0].Commands() {
|
||||
if sub.Name() == "message" {
|
||||
messageCmds = append(messageCmds, sub)
|
||||
}
|
||||
}
|
||||
if len(messageCmds) != 1 {
|
||||
names := make([]string, 0, len(cmds[0].Commands()))
|
||||
for _, c := range cmds[0].Commands() {
|
||||
names = append(names, c.Name())
|
||||
}
|
||||
t.Fatalf("expected exactly one 'message' under chat, got %d — chat children: %v", len(messageCmds), names)
|
||||
}
|
||||
|
||||
// The merged message subcommand must contain all four leaves.
|
||||
want := map[string]bool{"send": false, "list": false, "send-by-bot": false, "send-by-webhook": false}
|
||||
for _, leaf := range messageCmds[0].Commands() {
|
||||
if _, ok := want[leaf.Name()]; ok {
|
||||
want[leaf.Name()] = true
|
||||
}
|
||||
}
|
||||
for leaf, seen := range want {
|
||||
if !seen {
|
||||
t.Errorf("expected 'chat message %s' after merge, missing", leaf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildDynamicCommands_ParentMergeRecursive covers a multi-level merge:
|
||||
// chat already has `group.members` (with add/remove), and a separate
|
||||
// bot-group server contributes `dws chat group members add-bot` via
|
||||
// command=group + parent=chat + groups.members. The "group" and "members"
|
||||
// nodes must each be merged, not duplicated.
|
||||
func TestBuildDynamicCommands_ParentMergeRecursive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
servers := []market.ServerDescriptor{
|
||||
{
|
||||
Endpoint: "https://endpoint-chat",
|
||||
CLI: market.CLIOverlay{
|
||||
ID: "group-chat",
|
||||
Command: "chat",
|
||||
Groups: map[string]market.CLIGroupDef{
|
||||
"group": {Description: "群组管理"},
|
||||
"group.members": {Description: "群成员管理"},
|
||||
},
|
||||
ToolOverrides: map[string]market.CLIToolOverride{
|
||||
"add_group_member": {CLIName: "add", Group: "group.members"},
|
||||
"remove_group_member": {CLIName: "remove", Group: "group.members"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Endpoint: "https://endpoint-bot",
|
||||
CLI: market.CLIOverlay{
|
||||
ID: "bot-group",
|
||||
Command: "group",
|
||||
Parent: "chat",
|
||||
Groups: map[string]market.CLIGroupDef{
|
||||
"members": {Description: "机器人群成员"},
|
||||
},
|
||||
ToolOverrides: map[string]market.CLIToolOverride{
|
||||
"add_robot_to_group": {CLIName: "add-bot", Group: "members"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cmds := BuildDynamicCommands(servers, executor.EchoRunner{}, nil)
|
||||
if len(cmds) != 1 || cmds[0].Name() != "chat" {
|
||||
t.Fatalf("expected single top-level 'chat', got %d", len(cmds))
|
||||
}
|
||||
|
||||
// One 'group' under chat.
|
||||
var groupCmds []*cobra.Command
|
||||
for _, sub := range cmds[0].Commands() {
|
||||
if sub.Name() == "group" {
|
||||
groupCmds = append(groupCmds, sub)
|
||||
}
|
||||
}
|
||||
if len(groupCmds) != 1 {
|
||||
t.Fatalf("expected single 'group' under chat, got %d", len(groupCmds))
|
||||
}
|
||||
|
||||
// One 'members' under chat.group.
|
||||
var membersCmds []*cobra.Command
|
||||
for _, sub := range groupCmds[0].Commands() {
|
||||
if sub.Name() == "members" {
|
||||
membersCmds = append(membersCmds, sub)
|
||||
}
|
||||
}
|
||||
if len(membersCmds) != 1 {
|
||||
t.Fatalf("expected single 'members' under chat.group, got %d", len(membersCmds))
|
||||
}
|
||||
|
||||
// The merged members subcommand must contain add, remove, add-bot.
|
||||
want := map[string]bool{"add": false, "remove": false, "add-bot": false}
|
||||
for _, leaf := range membersCmds[0].Commands() {
|
||||
if _, ok := want[leaf.Name()]; ok {
|
||||
want[leaf.Name()] = true
|
||||
}
|
||||
}
|
||||
for leaf, seen := range want {
|
||||
if !seen {
|
||||
t.Errorf("expected 'chat group members %s', missing", leaf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildDynamicCommands_ParentMergeLeafCollision verifies that when two
|
||||
// servers both produce the same leaf path (e.g. both try to register
|
||||
// `chat message send`), the first one wins and the second is silently
|
||||
// dropped rather than producing a duplicate cobra command.
|
||||
func TestBuildDynamicCommands_ParentMergeLeafCollision(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
servers := []market.ServerDescriptor{
|
||||
{
|
||||
Endpoint: "https://endpoint-chat",
|
||||
CLI: market.CLIOverlay{
|
||||
ID: "group-chat",
|
||||
Command: "chat",
|
||||
Groups: map[string]market.CLIGroupDef{"message": {Description: "消息"}},
|
||||
ToolOverrides: map[string]market.CLIToolOverride{
|
||||
"send_message_as_user": {CLIName: "send", Group: "message"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Endpoint: "https://endpoint-bot",
|
||||
CLI: market.CLIOverlay{
|
||||
ID: "bot-message",
|
||||
Command: "message",
|
||||
Parent: "chat",
|
||||
ToolOverrides: map[string]market.CLIToolOverride{
|
||||
// Intentional collision: same leaf name.
|
||||
"send_robot_group_message": {CLIName: "send"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cmds := BuildDynamicCommands(servers, executor.EchoRunner{}, nil)
|
||||
if len(cmds) != 1 {
|
||||
t.Fatalf("expected 1 top-level, got %d", len(cmds))
|
||||
}
|
||||
messageCmd := findSubcommand(cmds[0], "message")
|
||||
if messageCmd == nil {
|
||||
t.Fatal("expected 'message' subcommand under chat")
|
||||
}
|
||||
// Exactly one 'send' leaf.
|
||||
var sendCount int
|
||||
for _, leaf := range messageCmd.Commands() {
|
||||
if leaf.Name() == "send" {
|
||||
sendCount++
|
||||
}
|
||||
}
|
||||
if sendCount != 1 {
|
||||
t.Fatalf("expected exactly one 'send' leaf, got %d", sendCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
)
|
||||
|
||||
@@ -110,6 +112,18 @@ func transformCSVToArray(value any) (any, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// transformJSONParse parses a CLI string into a structured value so callers can
|
||||
// pass complex payloads (JSON arrays/objects) through a single flag.
|
||||
//
|
||||
// Two input dialects are accepted, in order:
|
||||
// 1. Strict JSON — `[{"fieldName":"x","type":"text"}]`
|
||||
// 2. YAML (flow) — `[{fieldName: x, type: text}]`
|
||||
//
|
||||
// YAML is a superset of JSON that permits unquoted keys and strings, which
|
||||
// dramatically reduces the need for shell-level escaping. Users can therefore
|
||||
// write `--fields '[{fieldName: 标题, type: text}]'` instead of piling quotes
|
||||
// around every token. The output shape is the same either way; downstream
|
||||
// consumers see the parsed Go value, not the original dialect.
|
||||
func transformJSONParse(value any) (any, error) {
|
||||
s, ok := toString(value)
|
||||
if !ok {
|
||||
@@ -119,11 +133,22 @@ func transformJSONParse(value any) (any, error) {
|
||||
if s == "" {
|
||||
return value, nil
|
||||
}
|
||||
// Strict JSON first — fast path and unambiguous type promotion (numbers
|
||||
// stay numbers, etc.).
|
||||
var parsed any
|
||||
if err := json.Unmarshal([]byte(s), &parsed); err != nil {
|
||||
return nil, apperrors.NewValidation(fmt.Sprintf("json_parse: invalid JSON: %v", err))
|
||||
if err := json.Unmarshal([]byte(s), &parsed); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
return parsed, nil
|
||||
// YAML (flow) fallback — accepts `{key: value}` without surrounding
|
||||
// quotes, which is the natural form when typing at a shell prompt.
|
||||
if err := yaml.Unmarshal([]byte(s), &parsed); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
return nil, apperrors.NewValidation(
|
||||
"json_parse: input is not valid JSON or YAML; " +
|
||||
"quote the whole value and use `[{key: value, ...}]` for ad-hoc input, " +
|
||||
"or pass `@path/to/file.json` to read from a file",
|
||||
)
|
||||
}
|
||||
|
||||
func transformEnumMap(value any, args map[string]any) (any, error) {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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 compat
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestJSONParse_StrictJSON covers the primary path: callers passing
|
||||
// canonical JSON (as generated programmatically or by agents).
|
||||
func TestJSONParse_StrictJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := `[{"fieldName":"title","type":"text"},{"fieldName":"count","type":"number"}]`
|
||||
got, err := ApplyTransform(input, "json_parse", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("strict JSON should parse, got err: %v", err)
|
||||
}
|
||||
arr, ok := got.([]any)
|
||||
if !ok || len(arr) != 2 {
|
||||
t.Fatalf("expected []any of length 2, got %T %v", got, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSONParse_YAMLFlowFallback is the motivating case: a user types an
|
||||
// ad-hoc JSON-shaped array without quoting every key and value. YAML flow
|
||||
// syntax accepts it and the parsed output is indistinguishable from the
|
||||
// strict-JSON equivalent.
|
||||
func TestJSONParse_YAMLFlowFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Intentionally unquoted keys, unquoted string values, and Chinese
|
||||
// identifiers — typical of what humans type at a shell.
|
||||
input := `[{fieldName: 标题, type: text}, {fieldName: 数量, type: number, config: {formatter: INT}}, {fieldName: 状态, type: singleSelect, config: {options: [{name: 待办}, {name: 进行中}, {name: 已完成}]}}, {fieldName: 已确认, type: checkbox}]`
|
||||
|
||||
got, err := ApplyTransform(input, "json_parse", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("YAML-flow input should parse, got err: %v", err)
|
||||
}
|
||||
arr, ok := got.([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected []any, got %T", got)
|
||||
}
|
||||
if len(arr) != 4 {
|
||||
t.Fatalf("expected 4 field definitions, got %d", len(arr))
|
||||
}
|
||||
|
||||
// Spot-check the third entry, which is the most deeply nested.
|
||||
third, ok := arr[2].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("arr[2] expected map[string]any, got %T", arr[2])
|
||||
}
|
||||
if third["fieldName"] != "状态" {
|
||||
t.Errorf("arr[2].fieldName: want 状态, got %v", third["fieldName"])
|
||||
}
|
||||
config, ok := third["config"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("arr[2].config expected map, got %T", third["config"])
|
||||
}
|
||||
options, ok := config["options"].([]any)
|
||||
if !ok || len(options) != 3 {
|
||||
t.Fatalf("arr[2].config.options: want 3 items, got %v", config["options"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSONParse_EmptyString preserves the legacy behaviour of returning the
|
||||
// original value untouched when the caller passes an empty / whitespace-only
|
||||
// string, matching how other transforms treat empty input.
|
||||
func TestJSONParse_EmptyString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []string{"", " ", "\n\t"}
|
||||
for _, in := range cases {
|
||||
got, err := ApplyTransform(in, "json_parse", nil)
|
||||
if err != nil {
|
||||
t.Errorf("empty input %q should not error: %v", in, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(got, in) {
|
||||
t.Errorf("empty input %q should pass through, got %v", in, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSONParse_NonString passes through non-string inputs (already-parsed
|
||||
// values flowing through the pipeline).
|
||||
func TestJSONParse_NonString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
preParsed := []any{map[string]any{"k": "v"}}
|
||||
got, err := ApplyTransform(preParsed, "json_parse", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("non-string should pass through: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, preParsed) {
|
||||
t.Errorf("non-string should pass through unchanged, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSONParse_InvalidInput verifies that genuine garbage is still rejected
|
||||
// with a user-facing validation error that nudges towards `@file` syntax.
|
||||
func TestJSONParse_InvalidInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Unterminated bracket — neither valid JSON nor valid YAML flow.
|
||||
_, err := ApplyTransform("[{fieldName:", "json_parse", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for malformed input")
|
||||
}
|
||||
if msg := err.Error(); msg == "" {
|
||||
t.Fatal("error message should be non-empty")
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func newChatMessageSendByBotCommand(runner executor.Runner) *cobra.Command {
|
||||
|
||||
invocation := executor.NewHelperInvocation(
|
||||
cobracmd.LegacyCommandPath(cmd),
|
||||
"chat",
|
||||
"bot",
|
||||
tool,
|
||||
params,
|
||||
)
|
||||
|
||||
@@ -43,4 +43,55 @@ func TestChatMessageSendByBotIgnoresLegacyRealBuildModeEnv(t *testing.T) {
|
||||
if got := runner.last.Params["robotCode"]; got != "robot-001" {
|
||||
t.Fatalf("robotCode = %#v, want robot-001", got)
|
||||
}
|
||||
if got := runner.last.CanonicalProduct; got != "bot" {
|
||||
t.Fatalf("CanonicalProduct = %q, want bot", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessageSendByBotRoutesToBotProduct(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantTool string
|
||||
}{
|
||||
{
|
||||
name: "single-chat",
|
||||
args: []string{
|
||||
"--users", "user-001",
|
||||
"--robot-code", "robot-001",
|
||||
"--title", "t",
|
||||
"--text", "x",
|
||||
},
|
||||
wantTool: "batch_send_robot_msg_to_users",
|
||||
},
|
||||
{
|
||||
name: "group-chat",
|
||||
args: []string{
|
||||
"--group", "cid-xyz",
|
||||
"--robot-code", "robot-001",
|
||||
"--title", "t",
|
||||
"--text", "x",
|
||||
},
|
||||
wantTool: "send_robot_group_message",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runner := &captureRunner{}
|
||||
cmd := newChatMessageSendByBotCommand(runner)
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&out)
|
||||
cmd.SetArgs(tc.args)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String())
|
||||
}
|
||||
if got := runner.last.CanonicalProduct; got != "bot" {
|
||||
t.Fatalf("CanonicalProduct = %q, want bot", got)
|
||||
}
|
||||
if got := runner.last.Tool; got != tc.wantTool {
|
||||
t.Fatalf("Tool = %q, want %q", got, tc.wantTool)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,29 @@ type CLIFlagHint struct {
|
||||
Alias string `json:"alias,omitempty"`
|
||||
}
|
||||
|
||||
// FlagOverlay carries CLI-layer transformation metadata for a single
|
||||
// MCP parameter: the flag alias the user types, the transform applied
|
||||
// before dispatch, env-var fallback, default value, and whether the flag
|
||||
// is hidden from help. Sourced from market.CLIToolOverride.Flags.
|
||||
type FlagOverlay struct {
|
||||
Alias string `json:"alias,omitempty"`
|
||||
Transform string `json:"transform,omitempty"`
|
||||
TransformArgs map[string]any `json:"transform_args,omitempty"`
|
||||
EnvDefault string `json:"env_default,omitempty"`
|
||||
Default string `json:"default,omitempty"`
|
||||
Hidden bool `json:"hidden,omitempty"`
|
||||
}
|
||||
|
||||
// ToolAnnotations mirrors MCP 2025+ tool annotations. All hints are
|
||||
// nullable: absence means "unknown", not "false". Populate only when the
|
||||
// source has a clear signal — don't guess.
|
||||
type ToolAnnotations struct {
|
||||
DestructiveHint *bool `json:"destructive_hint,omitempty"`
|
||||
ReadOnlyHint *bool `json:"read_only_hint,omitempty"`
|
||||
IdempotentHint *bool `json:"idempotent_hint,omitempty"`
|
||||
OpenWorldHint *bool `json:"open_world_hint,omitempty"`
|
||||
}
|
||||
|
||||
type CanonicalProduct struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
@@ -67,13 +90,16 @@ type CanonicalProduct struct {
|
||||
type ToolDescriptor struct {
|
||||
RPCName string `json:"rpc_name"`
|
||||
CLIName string `json:"cli_name,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
InputSchema map[string]any `json:"input_schema,omitempty"`
|
||||
OutputSchema map[string]any `json:"output_schema,omitempty"`
|
||||
Sensitive bool `json:"sensitive"`
|
||||
Annotations *ToolAnnotations `json:"annotations,omitempty"`
|
||||
Hidden bool `json:"hidden,omitempty"`
|
||||
FlagHints map[string]CLIFlagHint `json:"flag_hints,omitempty"`
|
||||
FlagOverlay map[string]FlagOverlay `json:"flag_overlay,omitempty"`
|
||||
SourceServerKey string `json:"source_server_key"`
|
||||
CanonicalPath string `json:"canonical_path"`
|
||||
}
|
||||
@@ -105,10 +131,58 @@ func BuildCatalog(runtimeServers []discovery.RuntimeServer) Catalog {
|
||||
sensitiveByTool := make(map[string]bool, len(runtimeServer.Server.CLI.Tools))
|
||||
hasSensitiveOverride := make(map[string]bool, len(runtimeServer.Server.CLI.Tools))
|
||||
toolCLIName := make(map[string]string, len(runtimeServer.Server.CLI.Tools))
|
||||
toolGroup := make(map[string]string)
|
||||
toolHidden := make(map[string]bool, len(runtimeServer.Server.CLI.Tools))
|
||||
toolTitleOverride := make(map[string]string, len(runtimeServer.Server.CLI.Tools))
|
||||
toolDescriptionOverride := make(map[string]string, len(runtimeServer.Server.CLI.Tools))
|
||||
toolFlagHints := make(map[string]map[string]CLIFlagHint, len(runtimeServer.Server.CLI.Tools))
|
||||
toolFlagOverlay := make(map[string]map[string]FlagOverlay)
|
||||
|
||||
// Merge richer market.CLIToolOverride data (group, cliName, flag
|
||||
// transforms/aliases/env-defaults) that the dynamic compat layer
|
||||
// already consumes but the IR previously dropped.
|
||||
for name, override := range runtimeServer.Server.CLI.ToolOverrides {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if cn := strings.TrimSpace(override.CLIName); cn != "" {
|
||||
toolCLIName[trimmed] = cn
|
||||
}
|
||||
if gp := strings.TrimSpace(override.Group); gp != "" {
|
||||
toolGroup[trimmed] = gp
|
||||
}
|
||||
if override.Hidden {
|
||||
toolHidden[trimmed] = true
|
||||
}
|
||||
if override.IsSensitive {
|
||||
sensitiveByTool[trimmed] = true
|
||||
hasSensitiveOverride[trimmed] = true
|
||||
}
|
||||
if desc := strings.TrimSpace(override.Description); desc != "" && toolDescriptionOverride[trimmed] == "" {
|
||||
toolDescriptionOverride[trimmed] = desc
|
||||
}
|
||||
if len(override.Flags) > 0 {
|
||||
overlay := make(map[string]FlagOverlay, len(override.Flags))
|
||||
for paramName, flagOverride := range override.Flags {
|
||||
paramTrim := strings.TrimSpace(paramName)
|
||||
if paramTrim == "" {
|
||||
continue
|
||||
}
|
||||
overlay[paramTrim] = FlagOverlay{
|
||||
Alias: strings.TrimSpace(flagOverride.Alias),
|
||||
Transform: strings.TrimSpace(flagOverride.Transform),
|
||||
TransformArgs: cloneTransformArgs(flagOverride.TransformArgs),
|
||||
EnvDefault: strings.TrimSpace(flagOverride.EnvDefault),
|
||||
Default: strings.TrimSpace(flagOverride.Default),
|
||||
Hidden: flagOverride.Hidden,
|
||||
}
|
||||
}
|
||||
if len(overlay) > 0 {
|
||||
toolFlagOverlay[trimmed] = overlay
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, cliTool := range runtimeServer.Server.CLI.Tools {
|
||||
name := strings.TrimSpace(cliTool.Name)
|
||||
if name == "" {
|
||||
@@ -164,13 +238,16 @@ func BuildCatalog(runtimeServers []discovery.RuntimeServer) Catalog {
|
||||
tools = append(tools, ToolDescriptor{
|
||||
RPCName: tool.Name,
|
||||
CLIName: cliName,
|
||||
Group: strings.TrimSpace(toolGroup[tool.Name]),
|
||||
Title: title,
|
||||
Description: description,
|
||||
InputSchema: cloneMap(tool.InputSchema),
|
||||
OutputSchema: cloneMap(tool.OutputSchema),
|
||||
Sensitive: sensitive,
|
||||
Annotations: deriveAnnotations(sensitive),
|
||||
Hidden: toolHidden[tool.Name],
|
||||
FlagHints: cloneFlagHints(toolFlagHints[tool.Name]),
|
||||
FlagOverlay: cloneFlagOverlay(toolFlagOverlay[tool.Name]),
|
||||
SourceServerKey: runtimeServer.Server.Key,
|
||||
CanonicalPath: fmt.Sprintf("%s.%s", productID, tool.Name),
|
||||
})
|
||||
@@ -363,3 +440,43 @@ func cloneFlagHints(value map[string]CLIFlagHint) map[string]CLIFlagHint {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneFlagOverlay(value map[string]FlagOverlay) map[string]FlagOverlay {
|
||||
if len(value) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]FlagOverlay, len(value))
|
||||
for key, overlay := range value {
|
||||
overlay.TransformArgs = cloneTransformArgs(overlay.TransformArgs)
|
||||
out[key] = overlay
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTransformArgs(value map[string]any) map[string]any {
|
||||
if len(value) == 0 {
|
||||
return nil
|
||||
}
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var cloned map[string]any
|
||||
if err := json.Unmarshal(data, &cloned); err != nil {
|
||||
return nil
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
// deriveAnnotations maps the coarse-grained Sensitive bool onto MCP 2025+
|
||||
// annotation hints. Only DestructiveHint is populated — the other hints
|
||||
// (read_only, idempotent, open_world) stay nil until the upstream tool
|
||||
// manifest advertises them explicitly. Guessing from name prefixes
|
||||
// ("list_", "get_") risks false signals for AI agents.
|
||||
func deriveAnnotations(sensitive bool) *ToolAnnotations {
|
||||
if !sensitive {
|
||||
return nil
|
||||
}
|
||||
destructive := true
|
||||
return &ToolAnnotations{DestructiveHint: &destructive}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,114 @@ func TestBuildCatalogFallsBackToRuntimeSensitiveMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCatalogCarriesToolOverrideGroupAndFlagOverlay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
catalog := BuildCatalog([]discovery.RuntimeServer{
|
||||
{
|
||||
Server: market.ServerDescriptor{
|
||||
Key: "ding-key",
|
||||
DisplayName: "DING",
|
||||
Endpoint: "https://example.com/server/ding",
|
||||
CLI: market.CLIOverlay{
|
||||
Command: "ding",
|
||||
ToolOverrides: map[string]market.CLIToolOverride{
|
||||
"send_ding_message": {
|
||||
CLIName: "send",
|
||||
Group: "message",
|
||||
IsSensitive: true,
|
||||
Flags: map[string]market.CLIFlagOverride{
|
||||
"receiverUserIdList": {
|
||||
Alias: "users",
|
||||
Transform: "csv_to_array",
|
||||
},
|
||||
"robotCode": {
|
||||
Alias: "robot-code",
|
||||
EnvDefault: "DINGTALK_DING_ROBOT_CODE",
|
||||
},
|
||||
"remindType": {
|
||||
Alias: "type",
|
||||
Transform: "enum_map",
|
||||
TransformArgs: map[string]any{
|
||||
"_default": float64(1),
|
||||
"app": float64(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Tools: []transport.ToolDescriptor{
|
||||
{Name: "send_ding_message", Title: "发送DING消息", InputSchema: map[string]any{"type": "object"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if len(catalog.Products) != 1 {
|
||||
t.Fatalf("products len = %d, want 1", len(catalog.Products))
|
||||
}
|
||||
product := catalog.Products[0]
|
||||
if product.ID != "ding" {
|
||||
t.Fatalf("product ID = %q, want ding", product.ID)
|
||||
}
|
||||
tool, ok := product.FindTool("send_ding_message")
|
||||
if !ok {
|
||||
t.Fatalf("tool not found")
|
||||
}
|
||||
if tool.CLIName != "send" {
|
||||
t.Fatalf("CLIName = %q, want send", tool.CLIName)
|
||||
}
|
||||
if tool.Group != "message" {
|
||||
t.Fatalf("Group = %q, want message", tool.Group)
|
||||
}
|
||||
if !tool.Sensitive {
|
||||
t.Fatalf("Sensitive = false, want true (from overlay)")
|
||||
}
|
||||
if tool.Annotations == nil || tool.Annotations.DestructiveHint == nil || !*tool.Annotations.DestructiveHint {
|
||||
t.Fatalf("DestructiveHint not propagated, got %#v", tool.Annotations)
|
||||
}
|
||||
if tool.Annotations.ReadOnlyHint != nil {
|
||||
t.Fatalf("ReadOnlyHint should be nil when source is unknown, got %#v", *tool.Annotations.ReadOnlyHint)
|
||||
}
|
||||
users := tool.FlagOverlay["receiverUserIdList"]
|
||||
if users.Alias != "users" || users.Transform != "csv_to_array" {
|
||||
t.Fatalf("receiverUserIdList overlay = %#v", users)
|
||||
}
|
||||
robot := tool.FlagOverlay["robotCode"]
|
||||
if robot.Alias != "robot-code" || robot.EnvDefault != "DINGTALK_DING_ROBOT_CODE" {
|
||||
t.Fatalf("robotCode overlay = %#v", robot)
|
||||
}
|
||||
remind := tool.FlagOverlay["remindType"]
|
||||
if remind.Transform != "enum_map" {
|
||||
t.Fatalf("remindType.Transform = %q, want enum_map", remind.Transform)
|
||||
}
|
||||
if v, _ := remind.TransformArgs["app"].(float64); v != 1 {
|
||||
t.Fatalf("remindType.TransformArgs[app] = %v, want 1", remind.TransformArgs["app"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCatalogLeavesAnnotationsNilForNonSensitive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
catalog := BuildCatalog([]discovery.RuntimeServer{
|
||||
{
|
||||
Server: market.ServerDescriptor{
|
||||
Key: "doc-key",
|
||||
DisplayName: "文档",
|
||||
Endpoint: "https://example.com/server/doc",
|
||||
},
|
||||
Tools: []transport.ToolDescriptor{
|
||||
{Name: "list_documents", InputSchema: map[string]any{"type": "object"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
tool := catalog.Products[0].Tools[0]
|
||||
if tool.Annotations != nil {
|
||||
t.Fatalf("Annotations = %#v, want nil for non-sensitive tool", tool.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCatalogConsumesCLIRouteMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -29,9 +29,10 @@ import (
|
||||
type Format string
|
||||
|
||||
const (
|
||||
FormatJSON Format = "json"
|
||||
FormatTable Format = "table"
|
||||
FormatRaw Format = "raw"
|
||||
FormatJSON Format = "json"
|
||||
FormatTable Format = "table"
|
||||
FormatRaw Format = "raw"
|
||||
FormatPretty Format = "pretty"
|
||||
)
|
||||
|
||||
var preferredListKeys = []string{"items", "results", "data", "list", "records", "tools", "servers", "products"}
|
||||
@@ -75,6 +76,8 @@ func Write(w io.Writer, format Format, payload any) error {
|
||||
return writeRaw(w, payload)
|
||||
case FormatTable:
|
||||
return writeTableish(w, payload)
|
||||
case FormatPretty:
|
||||
return writePretty(w, payload)
|
||||
default:
|
||||
return WriteJSON(w, payload)
|
||||
}
|
||||
@@ -123,6 +126,8 @@ func normalizeFormat(raw string, fallback Format) Format {
|
||||
return FormatRaw
|
||||
case string(FormatTable):
|
||||
return FormatTable
|
||||
case string(FormatPretty):
|
||||
return FormatPretty
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
// 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 output
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// writePretty renders the payload as ANSI-colored, human-readable text.
|
||||
// If the payload looks like a `dws schema` response (has `kind: "schema"`),
|
||||
// a schema-specific hierarchical renderer runs. Anything else falls back
|
||||
// to the table renderer so `--format pretty` is safe on any command.
|
||||
func writePretty(w io.Writer, payload any) error {
|
||||
normalized, err := normalizePayload(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m, ok := normalized.(map[string]any); ok {
|
||||
if kind, _ := m["kind"].(string); kind == "schema" {
|
||||
return writeSchemaPretty(w, m)
|
||||
}
|
||||
}
|
||||
return writeTableish(w, normalized)
|
||||
}
|
||||
|
||||
// writeSchemaPretty handles both the list-all shape (has `products`) and
|
||||
// the single-tool shape (has `tool`). Colours are on by default via fatih/color,
|
||||
// auto-disabled when stdout is not a TTY or when NO_COLOR is set.
|
||||
func writeSchemaPretty(w io.Writer, payload map[string]any) error {
|
||||
bold := color.New(color.Bold).SprintFunc()
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
red := color.New(color.FgRed, color.Bold).SprintFunc()
|
||||
dim := color.New(color.Faint).SprintFunc()
|
||||
|
||||
if tool, ok := payload["tool"].(map[string]any); ok {
|
||||
return writeSchemaToolPretty(w, payload, tool, bold, cyan, green, yellow, red, dim)
|
||||
}
|
||||
if products, ok := payload["products"].([]any); ok {
|
||||
return writeSchemaListPretty(w, payload, products, bold, cyan, dim)
|
||||
}
|
||||
|
||||
if degraded, _ := payload["degraded"].(bool); degraded {
|
||||
reason, _ := payload["reason"].(string)
|
||||
hint, _ := payload["hint"].(string)
|
||||
fmt.Fprintf(w, "%s discovery degraded: %s\n", red("!"), yellow(reason))
|
||||
if hint != "" {
|
||||
fmt.Fprintf(w, " %s %s\n", dim("hint:"), hint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return writeTableish(w, payload)
|
||||
}
|
||||
|
||||
func writeSchemaListPretty(
|
||||
w io.Writer,
|
||||
payload map[string]any,
|
||||
products []any,
|
||||
bold, cyan, dim func(...any) string,
|
||||
) error {
|
||||
count, _ := payload["count"].(float64)
|
||||
if count == 0 {
|
||||
count = float64(len(products))
|
||||
}
|
||||
fmt.Fprintf(w, "%s %s products discovered\n", bold("Catalog"), cyan(fmt.Sprintf("%d", int(count))))
|
||||
|
||||
for _, raw := range products {
|
||||
p, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, _ := p["id"].(string)
|
||||
name, _ := p["name"].(string)
|
||||
desc, _ := p["description"].(string)
|
||||
tools, _ := p["tools"].([]any)
|
||||
fmt.Fprintf(w, "\n%s %s %s\n", bold("▸"), bold(id), dim(name))
|
||||
if desc != "" && desc != name {
|
||||
fmt.Fprintf(w, " %s\n", dim(desc))
|
||||
}
|
||||
fmt.Fprintf(w, " %s %s\n", dim("tools:"), cyan(fmt.Sprintf("%d", len(tools))))
|
||||
shown := 0
|
||||
for _, t := range tools {
|
||||
tm, ok := t.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rpc, _ := tm["name"].(string)
|
||||
cli, _ := tm["cli_name"].(string)
|
||||
if cli == "" || cli == rpc {
|
||||
fmt.Fprintf(w, " - %s\n", rpc)
|
||||
} else {
|
||||
fmt.Fprintf(w, " - %s %s\n", rpc, dim("→ "+cli))
|
||||
}
|
||||
shown++
|
||||
if shown >= 6 && len(tools) > 8 {
|
||||
fmt.Fprintf(w, " %s\n", dim(fmt.Sprintf("… %d more", len(tools)-shown)))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeSchemaToolPretty(
|
||||
w io.Writer,
|
||||
payload map[string]any,
|
||||
tool map[string]any,
|
||||
bold, cyan, green, yellow, red, dim func(...any) string,
|
||||
) error {
|
||||
rpc, _ := tool["name"].(string)
|
||||
cli, _ := tool["cli_name"].(string)
|
||||
title, _ := tool["title"].(string)
|
||||
desc, _ := tool["description"].(string)
|
||||
group, _ := tool["group"].(string)
|
||||
canonical, _ := tool["canonical_path"].(string)
|
||||
|
||||
header := rpc
|
||||
if title != "" && title != rpc {
|
||||
header = fmt.Sprintf("%s %s", rpc, dim(title))
|
||||
}
|
||||
fmt.Fprintf(w, "%s %s\n", bold("Tool"), header)
|
||||
|
||||
var productID string
|
||||
if product, ok := payload["product"].(map[string]any); ok {
|
||||
pid, _ := product["id"].(string)
|
||||
pname, _ := product["name"].(string)
|
||||
productID = pid
|
||||
fmt.Fprintf(w, " %s %s %s\n", dim("product:"), pid, dim(pname))
|
||||
}
|
||||
if canonical != "" {
|
||||
fmt.Fprintf(w, " %s %s\n", dim("canonical:"), canonical)
|
||||
}
|
||||
if cli != "" {
|
||||
parts := []string{}
|
||||
if productID != "" {
|
||||
parts = append(parts, productID)
|
||||
}
|
||||
if group != "" {
|
||||
parts = append(parts, strings.Split(group, ".")...)
|
||||
}
|
||||
parts = append(parts, cli)
|
||||
fmt.Fprintf(w, " %s %s\n", dim("cli path:"), cyan(strings.Join(parts, " ")))
|
||||
}
|
||||
|
||||
// Sensitivity + annotations in one line.
|
||||
sensitive, _ := tool["sensitive"].(bool)
|
||||
if sensitive {
|
||||
fmt.Fprintf(w, " %s %s\n", dim("sensitive:"), red("yes (needs --yes)"))
|
||||
}
|
||||
if ann, ok := tool["annotations"].(map[string]any); ok && len(ann) > 0 {
|
||||
var parts []string
|
||||
for _, key := range sortedMapKeys(ann) {
|
||||
parts = append(parts, fmt.Sprintf("%s=%v", key, ann[key]))
|
||||
}
|
||||
fmt.Fprintf(w, " %s %s\n", dim("annotations:"), yellow(strings.Join(parts, " ")))
|
||||
}
|
||||
|
||||
if desc != "" && desc != title {
|
||||
fmt.Fprintln(w)
|
||||
for _, line := range strings.Split(strings.TrimSpace(desc), "\n") {
|
||||
fmt.Fprintf(w, " %s\n", dim(line))
|
||||
}
|
||||
}
|
||||
|
||||
// Parameters section.
|
||||
if params, ok := tool["parameters"].(map[string]any); ok && len(params) > 0 {
|
||||
required := map[string]bool{}
|
||||
if req, ok := tool["required"].([]any); ok {
|
||||
for _, r := range req {
|
||||
if s, ok := r.(string); ok {
|
||||
required[s] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
overlay := map[string]map[string]any{}
|
||||
if ov, ok := tool["flag_overlay"].(map[string]any); ok {
|
||||
for name, v := range ov {
|
||||
if m, ok := v.(map[string]any); ok {
|
||||
overlay[name] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintf(w, "%s\n", bold("Parameters"))
|
||||
for _, name := range sortedMapKeys(params) {
|
||||
prop, _ := params[name].(map[string]any)
|
||||
writeParamPretty(w, name, prop, required[name], overlay[name],
|
||||
bold, cyan, green, yellow, red, dim)
|
||||
}
|
||||
}
|
||||
|
||||
// Output schema, if any.
|
||||
if out, ok := tool["output_schema"].(map[string]any); ok && len(out) > 0 {
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintf(w, "%s\n", bold("Output schema"))
|
||||
b, _ := json.MarshalIndent(out, " ", " ")
|
||||
fmt.Fprintf(w, " %s\n", string(b))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeParamPretty(
|
||||
w io.Writer,
|
||||
name string,
|
||||
prop map[string]any,
|
||||
required bool,
|
||||
overlay map[string]any,
|
||||
bold, cyan, green, yellow, red, dim func(...any) string,
|
||||
) {
|
||||
typeStr := describeType(prop)
|
||||
marker := " "
|
||||
if required {
|
||||
marker = red("*")
|
||||
}
|
||||
|
||||
alias, _ := overlay["alias"].(string)
|
||||
line := fmt.Sprintf(" %s %s", marker, bold(name))
|
||||
if alias != "" && alias != name {
|
||||
line += fmt.Sprintf(" %s", cyan("--"+alias))
|
||||
}
|
||||
line += fmt.Sprintf(" %s", dim(typeStr))
|
||||
fmt.Fprintln(w, line)
|
||||
|
||||
if d, _ := prop["description"].(string); d != "" {
|
||||
for _, ln := range strings.Split(strings.TrimSpace(d), "\n") {
|
||||
fmt.Fprintf(w, " %s\n", dim(ln))
|
||||
}
|
||||
}
|
||||
|
||||
// enum values — render inline.
|
||||
if enum, ok := prop["enum"].([]any); ok && len(enum) > 0 {
|
||||
vals := make([]string, 0, len(enum))
|
||||
for _, v := range enum {
|
||||
vals = append(vals, fmt.Sprintf("%v", v))
|
||||
}
|
||||
fmt.Fprintf(w, " %s %s\n", dim("enum:"), green(strings.Join(vals, ", ")))
|
||||
}
|
||||
|
||||
// overlay — transform / env default / default.
|
||||
if transform, _ := overlay["transform"].(string); transform != "" {
|
||||
extras := transform
|
||||
if args, ok := overlay["transform_args"].(map[string]any); ok && len(args) > 0 {
|
||||
kvs := make([]string, 0, len(args))
|
||||
for _, k := range sortedMapKeys(args) {
|
||||
kvs = append(kvs, fmt.Sprintf("%s=%v", k, args[k]))
|
||||
}
|
||||
extras += "(" + strings.Join(kvs, ", ") + ")"
|
||||
}
|
||||
fmt.Fprintf(w, " %s %s\n", dim("transform:"), yellow(extras))
|
||||
}
|
||||
if env, _ := overlay["env_default"].(string); env != "" {
|
||||
fmt.Fprintf(w, " %s %s\n", dim("env default:"), green("$"+env))
|
||||
}
|
||||
if def, _ := overlay["default"].(string); def != "" {
|
||||
fmt.Fprintf(w, " %s %s\n", dim("default:"), green(def))
|
||||
}
|
||||
}
|
||||
|
||||
func describeType(prop map[string]any) string {
|
||||
t, _ := prop["type"].(string)
|
||||
if t == "array" {
|
||||
if items, ok := prop["items"].(map[string]any); ok {
|
||||
if inner, _ := items["type"].(string); inner != "" {
|
||||
return inner + "[]"
|
||||
}
|
||||
}
|
||||
return "array"
|
||||
}
|
||||
if t == "" {
|
||||
return "any"
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func sortedMapKeys(m map[string]any) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// 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 output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// forceNoColor disables ANSI codes for assertion simplicity. fatih/color
|
||||
// would already do this when writing to a non-TTY, but tests use bytes.Buffer
|
||||
// so we flip the flag explicitly.
|
||||
func forceNoColor(t *testing.T) {
|
||||
t.Helper()
|
||||
prev := color.NoColor
|
||||
color.NoColor = true
|
||||
t.Cleanup(func() { color.NoColor = prev })
|
||||
}
|
||||
|
||||
func TestPretty_SchemaListRendersProducts(t *testing.T) {
|
||||
forceNoColor(t)
|
||||
|
||||
payload := map[string]any{
|
||||
"kind": "schema",
|
||||
"count": 2,
|
||||
"products": []any{
|
||||
map[string]any{
|
||||
"id": "ding",
|
||||
"name": "DING消息",
|
||||
"description": "DING 消息 / 发送 / 撤回",
|
||||
"tools": []any{
|
||||
map[string]any{"name": "send_ding_message", "cli_name": "send"},
|
||||
map[string]any{"name": "recall_ding_message", "cli_name": "recall"},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"id": "doc",
|
||||
"name": "钉钉文档",
|
||||
"tools": []any{map[string]any{"name": "create_document", "cli_name": "create"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := writePretty(&buf, payload); err != nil {
|
||||
t.Fatalf("writePretty() error = %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
|
||||
wants := []string{
|
||||
"Catalog",
|
||||
"2 products discovered",
|
||||
"ding",
|
||||
"send_ding_message",
|
||||
"→ send",
|
||||
"recall_ding_message",
|
||||
"doc",
|
||||
"create_document",
|
||||
}
|
||||
for _, want := range wants {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("pretty list missing %q; got:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPretty_SchemaToolRendersAllSections(t *testing.T) {
|
||||
forceNoColor(t)
|
||||
|
||||
payload := map[string]any{
|
||||
"kind": "schema",
|
||||
"path": "ding.send_ding_message",
|
||||
"product": map[string]any{
|
||||
"id": "ding",
|
||||
"name": "DING消息",
|
||||
},
|
||||
"tool": map[string]any{
|
||||
"name": "send_ding_message",
|
||||
"cli_name": "send",
|
||||
"canonical_path": "ding.send_ding_message",
|
||||
"group": "message",
|
||||
"title": "发送DING消息",
|
||||
"description": "使用企业内机器人发送DING消息",
|
||||
"sensitive": true,
|
||||
"annotations": map[string]any{
|
||||
"destructive_hint": true,
|
||||
},
|
||||
"parameters": map[string]any{
|
||||
"robotCode": map[string]any{
|
||||
"type": "string",
|
||||
"description": "机器人Code",
|
||||
},
|
||||
"receiverUserIdList": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{"type": "string"},
|
||||
"description": "接收者用户ID列表",
|
||||
},
|
||||
"remindType": map[string]any{
|
||||
"type": "number",
|
||||
"description": "提醒类型",
|
||||
},
|
||||
},
|
||||
"required": []any{"robotCode", "receiverUserIdList", "remindType"},
|
||||
"flag_overlay": map[string]any{
|
||||
"receiverUserIdList": map[string]any{
|
||||
"alias": "users",
|
||||
"transform": "csv_to_array",
|
||||
},
|
||||
"robotCode": map[string]any{
|
||||
"alias": "robot-code",
|
||||
"env_default": "DINGTALK_DING_ROBOT_CODE",
|
||||
},
|
||||
"remindType": map[string]any{
|
||||
"alias": "type",
|
||||
"transform": "enum_map",
|
||||
"transform_args": map[string]any{
|
||||
"app": 1,
|
||||
"sms": 2,
|
||||
"call": 3,
|
||||
"_default": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := writePretty(&buf, payload); err != nil {
|
||||
t.Fatalf("writePretty() error = %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
|
||||
wants := []string{
|
||||
"Tool send_ding_message", // header
|
||||
"canonical:", // meta section
|
||||
"ding.send_ding_message",
|
||||
"cli path:",
|
||||
"ding message send",
|
||||
"sensitive:",
|
||||
"yes (needs --yes)",
|
||||
"annotations:",
|
||||
"destructive_hint=true",
|
||||
"Parameters",
|
||||
"robotCode",
|
||||
"--robot-code", // overlay alias rendered
|
||||
"env default:",
|
||||
"$DINGTALK_DING_ROBOT_CODE",
|
||||
"receiverUserIdList",
|
||||
"--users",
|
||||
"transform:",
|
||||
"csv_to_array",
|
||||
"string[]", // array<string> shorthand
|
||||
"remindType",
|
||||
"--type",
|
||||
"enum_map(", // with args inlined
|
||||
"app=1",
|
||||
}
|
||||
for _, want := range wants {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("pretty tool missing %q; full output:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
|
||||
// required params must show the red asterisk marker (even with color disabled
|
||||
// the literal '*' still appears).
|
||||
for _, req := range []string{"robotCode", "receiverUserIdList", "remindType"} {
|
||||
// format: " * <name>"
|
||||
if !strings.Contains(out, "* "+req) {
|
||||
t.Errorf("required marker missing for %q", req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPretty_NonSchemaFallsBackToTableish(t *testing.T) {
|
||||
forceNoColor(t)
|
||||
|
||||
// Payload that doesn't have kind="schema" should fall through to
|
||||
// the tableish renderer, not error or render a schema header.
|
||||
payload := map[string]any{"items": []any{
|
||||
map[string]any{"id": "a", "name": "Alice"},
|
||||
map[string]any{"id": "b", "name": "Bob"},
|
||||
}}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := writePretty(&buf, payload); err != nil {
|
||||
t.Fatalf("writePretty() error = %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
if strings.Contains(out, "Catalog") || strings.Contains(out, "Tool ") {
|
||||
t.Errorf("pretty wrongly ran schema renderer on non-schema payload:\n%s", out)
|
||||
}
|
||||
// tableish should produce header + rows
|
||||
for _, want := range []string{"id", "name", "Alice", "Bob"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("tableish fallback missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeFormat_RecognisesPretty(t *testing.T) {
|
||||
if got := normalizeFormat("pretty", FormatJSON); got != FormatPretty {
|
||||
t.Errorf("normalizeFormat(pretty) = %q, want %q", got, FormatPretty)
|
||||
}
|
||||
if got := normalizeFormat("PRETTY", FormatJSON); got != FormatPretty {
|
||||
t.Errorf("normalizeFormat(PRETTY) = %q, want %q", got, FormatPretty)
|
||||
}
|
||||
}
|
||||
+55
-21
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: dws
|
||||
description: 管理钉钉产品能力(AI表格/日历/通讯录/群聊与机器人/待办/审批/考勤/日志/DING消息/工作台/开放平台文档/钉钉文档/AI听记等)。当用户需要操作表格数据、管理日程会议、查询通讯录、管理群聊、机器人发消息、创建待办、提交审批、查看考勤、提交日报周报(钉钉日志模版)、读写钉钉文档、查询听记纪要时使用。
|
||||
cli_version: ">=1.0.6"
|
||||
description: 管理钉钉产品能力(AI表格/日历/通讯录/群聊与机器人/待办/审批/考勤/日志/DING消息/开放平台文档/钉钉文档/钉钉云盘/AI听记等)。当用户需要操作表格数据、管理日程会议、查询通讯录、管理群聊、机器人发消息、创建待办、提交审批、查看考勤、提交日报周报(钉钉日志模版)、读写钉钉文档、上传下载云盘文件、查询听记纪要时使用。
|
||||
cli_version: ">=1.0.15"
|
||||
---
|
||||
|
||||
# 钉钉全产品 Skill
|
||||
@@ -24,39 +24,41 @@ cli_version: ">=1.0.6"
|
||||
|
||||
| 产品 | 用途 | 参考文件 |
|
||||
|-------------------|------------------------------------------------------|----------------------------------------------------------------|
|
||||
| `aitable` | AI表格:Base/数据表/字段/记录/附件/模板搜索 | [aitable.md](./references/products/aitable.md) |
|
||||
| `approval` | 审批:审批表单/发起实例/审批/撤销 | [simple.md](./references/products/simple.md) |
|
||||
| `attendance` | 考勤:打卡记录/排班查询 | [attendance.md](./references/products/attendance.md) |
|
||||
| `calendar` | 日历:日程/参与者/会议室/闲忙查询 | [calendar.md](./references/products/calendar.md) |
|
||||
| `aitable` | AI表格:Base/数据表/字段/记录/视图/附件/图表/仪表盘/导入导出/模板搜索 | [aitable.md](./references/products/aitable.md) |
|
||||
| `attendance` | 考勤:打卡记录/排班查询/考勤规则/汇总统计 | [attendance.md](./references/products/attendance.md) |
|
||||
| `calendar` | 日历:日程/参与者/会议室/闲忙查询/时间建议 | [calendar.md](./references/products/calendar.md) |
|
||||
| `chat` | 群聊与机器人:搜索群/建群/群成员管理/改群名/机器人群发/单聊/撤回/Webhook/机器人搜索 | [chat.md](./references/products/chat.md) |
|
||||
| `contact` | 通讯录:用户查询(当前用户/搜索/详情)/部门查询(搜索/子部门/成员列表) | [contact.md](./references/products/contact.md) |
|
||||
| `devdoc` | 开放平台文档:搜索开发文档 | [simple.md](./references/products/simple.md) |
|
||||
| `contact` | 通讯录:用户查询(当前用户/搜索/详情/手机号)/部门查询(搜索/成员列表) | [contact.md](./references/products/contact.md) |
|
||||
| `devdoc` | 开放平台文档:搜索开发文档 | [devdoc.md](./references/products/devdoc.md) |
|
||||
| `ding` | DING消息:发送/撤回(应用内/短信/电话) | [ding.md](./references/products/ding.md) |
|
||||
| `doc` | 钉钉文档:搜索/浏览/读写/块级编辑/评论 | [doc.md](./references/products/doc.md) |
|
||||
| `minutes` | AI听记:听记列表/摘要/关键词/转写/待办/思维导图/发言人/热词 | [minutes.md](./references/products/minutes.md) |
|
||||
| `doc` | 钉钉文档:搜索/浏览/读写/块级编辑/评论/文件创建/复制/移动/重命名 | [doc.md](./references/products/doc.md) |
|
||||
| `drive` | 钉钉云盘:文件列表/元数据/文件夹/上传(两步)/下载 | [drive.md](./references/products/drive.md) |
|
||||
| `minutes` | AI听记:听记列表/摘要/关键词/转写/待办/思维导图/发言人/热词/上传 | [minutes.md](./references/products/minutes.md) |
|
||||
| `oa` | OA审批:待办/我发起的/表单模板/详情/审批流水/同意/拒绝/撤销 | [oa.md](./references/products/oa.md) |
|
||||
| `report` | 日志:按模版创建/收件箱/已发送/模版查看/详情/已读统计 | [report.md](./references/products/report.md) |
|
||||
| `todo` | 待办:创建(含优先级/截止时间)/查询/修改/标记完成/删除 | [todo.md](./references/products/todo.md) |
|
||||
| `workbench` | 工作台:应用管理 | [workbench.md](./references/products/workbench.md) |
|
||||
| `todo` | 待办:创建(含优先级/截止时间/循环)/查询/修改/标记完成/删除 | [todo.md](./references/products/todo.md) |
|
||||
|
||||
## 意图判断决策树
|
||||
|
||||
用户提到"表格/多维表/AI表格/记录/数据" → `aitable`
|
||||
用户提到"审批/请假/报销/出差/加班" → `oa`
|
||||
用户提到"表格/多维表/AI表格/记录/数据/视图/图表/仪表盘" → `aitable`
|
||||
用户提到"考勤/打卡/排班" → `attendance`
|
||||
用户提到"日程/日历/会议室/约会" → `calendar`
|
||||
用户提到"日程/日历/会议室/约会/时间建议" → `calendar`
|
||||
用户提到"群聊/建群/群成员/群管理/机器人发消息/Webhook/机器人群发/机器人单聊/通知" → `chat`
|
||||
用户提到"通讯录/同事/部门/组织架构" → `contact`
|
||||
用户提到"开发/API/调用错误 文档" → `devdoc`
|
||||
用户提到"DING/紧急消息/电话提醒" → `ding`
|
||||
用户提到"钉钉文档/云文档/知识库/读写文档/块级编辑/文档评论" → `doc`
|
||||
用户提到"钉钉文档/云文档/知识库/读写文档/块级编辑/文档评论/文档复制移动" → `doc`
|
||||
用户提到"云盘/文件存储/文件上传下载/文件夹" → `drive`
|
||||
用户提到"听记/AI听记/会议纪要/转写/摘要/思维导图/发言人/热词" → `minutes`
|
||||
用户提到"审批/请假/报销/出差/加班/同意/拒绝/撤销审批" → `oa`
|
||||
用户提到"日志/日报/周报/日志统计/写日报/提交周报/发日志/填日志" → `report`
|
||||
用户提到"待办/TODO/任务提醒" → `todo`
|
||||
用户提到"工作台/应用管理" → `workbench`
|
||||
用户提到"待办/TODO/任务提醒/循环待办" → `todo`
|
||||
|
||||
关键区分: aitable(数据表格) vs todo(待办任务)
|
||||
关键区分: report(钉钉日志/日报周报) vs todo(待办任务)
|
||||
关键区分: chat send-by-bot(机器人身份发消息) vs send-by-webhook(自定义机器人Webhook告警)
|
||||
关键区分: doc(钉钉文档/富文本协同) vs drive(钉钉云盘/二进制文件)
|
||||
关键区分: oa tasks(审批 taskId,审批/拒绝用) vs oa list-pending(收件箱 processInstanceId,查看用)
|
||||
|
||||
|
||||
> 更多易混淆场景见 [intent-guide.md](./references/intent-guide.md)
|
||||
@@ -68,14 +70,22 @@ cli_version: ">=1.0.6"
|
||||
| 产品 | 命令 | 说明 |
|
||||
|------|------|------|
|
||||
| `aitable` | `base delete` | 删除整个 AI 表格,含全部数据表和记录 |
|
||||
| `aitable` | `table delete` | 删除数据表(含全部字段/视图/记录) |
|
||||
| `aitable` | `field delete` | 删除字段(该列所有值同步清空) |
|
||||
| `aitable` | `view delete` | 删除视图 |
|
||||
| `aitable` | `record delete` | 删除记录(支持批量) |
|
||||
| `aitable` | `chart delete` / `dashboard delete` | 删除图表/仪表盘 |
|
||||
| `calendar` | `event delete` | 删除日程,所有参与者同步取消 |
|
||||
| `calendar` | `participant delete` | 移除日程参与者 |
|
||||
| `calendar` | `room delete` | 取消会议室预定 |
|
||||
| `chat` | `group members remove` | 移除群成员 |
|
||||
| `doc` | `delete` | 删除钉钉文档(不可恢复) |
|
||||
| `doc` | `block delete` | 删除文档块 |
|
||||
| `chat` | `message recall-by-bot` | 撤回机器人已发消息 |
|
||||
| `doc` | `block delete` | 删除文档块(不可恢复) |
|
||||
| `ding` | `message recall` | 撤回已发 DING 消息 |
|
||||
| `oa` | `approval revoke` | 撤销自己发起的审批实例 |
|
||||
| `oa` | `approval reject` | 拒绝待审批(需加明确理由) |
|
||||
| `todo` | `task delete` | 删除待办 |
|
||||
| `minutes` | `replace-text` | 全文批量替换转写与摘要 |
|
||||
|
||||
### 确认流程
|
||||
```
|
||||
@@ -92,6 +102,30 @@ Step 3 → 加 --yes 执行命令
|
||||
3. 精准产品映射:在完成前两步,意图已经清晰后,参考产品总览和意图判断决策树 来选择产品。
|
||||
4. 充分阅读产品参考文件,通过编写代码或直接调用指令实现用户意图。
|
||||
|
||||
## 命令发现(flag / 参数以 binary 为准)
|
||||
|
||||
产品参考文档(`references/products/*.md`)里的 flag 列表是**便于理解用途的参考**,不是权威契约。参数名称、默认值、必填约束随服务发现动态变化,**以下两个命令的输出才是调用的事实源**:
|
||||
|
||||
```bash
|
||||
# 1) 人读视图:看 Usage / Example / Flags
|
||||
dws <command-path> --help
|
||||
# 例:dws calendar event list --help
|
||||
|
||||
# 2) 机读视图:JSON Schema + flag 别名映射 + 必填字段
|
||||
dws schema # 列出所有产品及工具
|
||||
dws schema <product>.<canonical_name> # 规范路径(如 calendar.list_suggested_event_times)
|
||||
dws schema "<product> <group> <cli_name>" # CLI 路径(如 "calendar event list")
|
||||
dws schema <path> --jq '.tool.flag_overlay' # 只看 flag 别名
|
||||
dws schema <path> --jq '.tool.required' # 只看必填字段
|
||||
```
|
||||
|
||||
**何时用哪条路径:**
|
||||
- 只需看某个命令怎么调用 → `dws <cmd> --help`
|
||||
- 构造 `--params` / `--json` 时不确定字段类型、必填、别名 → `dws schema <path>`
|
||||
- 参考文档和 `--help` 冲突时 → **以 `--help` / `dws schema` 为准**,文档视为过期
|
||||
|
||||
`dws schema` 输出的 `flag_overlay[key].alias` 就是实际生效的 flag 名(如 `attendeeUserIds → --attendee-user-ids`);`parameters[key]` 是原始 JSON Schema;`required` 是必填字段数组;`sensitive: true` 表示写/删操作,须先向用户确认再加 `--yes`。
|
||||
|
||||
## 错误处理
|
||||
1. 遇到错误,加 `--verbose` 重试一次
|
||||
2. 若 stderr 出现 `RECOVERY_EVENT_ID=<event_id>`,优先按 [recovery-guide.md](./references/recovery-guide.md) 执行 recovery 闭环
|
||||
@@ -102,7 +136,7 @@ Step 3 → 加 --yes 执行命令
|
||||
|
||||
## 详细参考 (按需读取)
|
||||
|
||||
- [references/products/](./references/products/) — 各产品命令详细参考
|
||||
- [references/products/](./references/products/) — 各产品命令详细参考(flag 细节以 `--help` / `dws schema` 为准)
|
||||
- [references/intent-guide.md](./references/intent-guide.md) — 意图路由指南(易混淆场景对照)
|
||||
- [references/global-reference.md](./references/global-reference.md) — 全局标志、认证、输出格式
|
||||
- [references/field-rules.md](./references/field-rules.md) — AI表格字段类型规则
|
||||
|
||||
@@ -215,11 +215,12 @@ Example:
|
||||
dws aitable field update --base-id <BASE_ID> --table-id <TABLE_ID> --field-id <FIELD_ID> --name "新字段名"
|
||||
dws aitable field update --base-id <BASE_ID> --table-id <TABLE_ID> --field-id <FIELD_ID> --config '{"options":[{"name":"A"},{"name":"B"}]}'
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--config string 字段配置 JSON (不修改时省略)
|
||||
--field-id string Field ID (必填)
|
||||
--name string 新字段名称 (不修改时省略)
|
||||
--table-id string Table ID (必填)
|
||||
--ai-config string AI 字段配置 JSON(AI 字段类型专用,可选)
|
||||
--base-id string Base ID (必填)
|
||||
--config string 字段配置 JSON (不修改时省略)
|
||||
--field-id string Field ID (必填)
|
||||
--name string 新字段名称 (不修改时省略)
|
||||
--table-id string Table ID (必填)
|
||||
```
|
||||
|
||||
不可变更字段类型。更新 singleSelect/multipleSelect 的 options 时需传入完整列表,已有选项应回传原 id。
|
||||
@@ -362,6 +363,328 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
|
||||
> ⚠️ `uploadUrl` 有时效性(`expiresAt`),脚本会自动在获取后立即上传。
|
||||
|
||||
### view (视图管理)
|
||||
|
||||
视图是同一张数据表的筛选/排序/分组/可见字段组合的备用呈现。record query 可以通过视图收敛查询范围。
|
||||
|
||||
#### 获取视图
|
||||
```
|
||||
Usage:
|
||||
dws aitable view get [flags]
|
||||
Example:
|
||||
dws aitable view get --base-id <BASE_ID> --table-id <TABLE_ID>
|
||||
dws aitable view get --base-id <BASE_ID> --table-id <TABLE_ID> --view-ids viw1,viw2
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--table-id string Table ID (必填)
|
||||
--view-ids string View ID 列表,逗号分隔,单次最多 10 个
|
||||
```
|
||||
|
||||
返回视图配置(过滤、排序、可见字段、分组)。不传 view-ids 返回该表全部视图。
|
||||
|
||||
#### 创建视图
|
||||
```
|
||||
Usage:
|
||||
dws aitable view create [flags]
|
||||
Example:
|
||||
dws aitable view create --base-id <BASE_ID> --table-id <TABLE_ID> --name "进行中看板" --view-type kanban
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--config string 视图配置 JSON:过滤/排序/分组/可见字段(可选,创建后可再 update)
|
||||
--name string 视图名称 (必填)
|
||||
--table-id string Table ID (必填)
|
||||
--view-type string 视图类型:grid/gallery/kanban/gantt/calendar/form 等 (必填)
|
||||
```
|
||||
|
||||
> ⚠️ 视图类型参数是 `--view-type`,不是 `--type`。
|
||||
|
||||
#### 更新视图
|
||||
```
|
||||
Usage:
|
||||
dws aitable view update [flags]
|
||||
Example:
|
||||
dws aitable view update --base-id <BASE_ID> --table-id <TABLE_ID> --view-id <VIEW_ID> --name "新视图名"
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--config string 视图配置 JSON:过滤/排序/分组/可见字段
|
||||
--name string 新视图名
|
||||
--table-id string Table ID (必填)
|
||||
--view-id string 目标 View ID (必填)
|
||||
```
|
||||
|
||||
调整过滤、排序、分组、可见字段时使用。不重建视图即可替换配置。
|
||||
|
||||
#### 删除视图
|
||||
```
|
||||
Usage:
|
||||
dws aitable view delete [flags]
|
||||
Example:
|
||||
dws aitable view delete --base-id <BASE_ID> --table-id <TABLE_ID> --view-id <VIEW_ID> --yes
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--table-id string Table ID (必填)
|
||||
--view-id string 待删除 View ID (必填)
|
||||
```
|
||||
|
||||
不可逆。若是主视图或最后一个视图,删除会失败。
|
||||
|
||||
### import (数据导入)
|
||||
|
||||
把外部 Excel/CSV 批量写入某张数据表,按两步走:先 upload 拿 importId,再 data 触发导入。
|
||||
|
||||
#### 准备导入上传
|
||||
```
|
||||
Usage:
|
||||
dws aitable import upload [flags]
|
||||
Example:
|
||||
dws aitable import upload --base-id <BASE_ID> --file-name sales.xlsx --file-size 204800
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--file-name string 文件名,必须含扩展名 (必填)
|
||||
--file-size int 文件大小(字节),>0 (必填)
|
||||
```
|
||||
|
||||
> ⚠️ 参数是 `--file-size`,不是 `--size`。返回 uploadUrl(PUT 上传)与 importId。
|
||||
|
||||
#### 触发导入
|
||||
```
|
||||
Usage:
|
||||
dws aitable import data [flags]
|
||||
Example:
|
||||
dws aitable import data --import-id <IMPORT_ID> --timeout 60
|
||||
Flags:
|
||||
--import-id string import upload 返回的 importId (必填)
|
||||
--timeout int 等待导入完成的超时秒数,默认 30
|
||||
```
|
||||
|
||||
把上一步上传到暂存区的文件正式导入为记录;超时未完成时返回处理状态,按 importId 再次查询即可。
|
||||
|
||||
### dashboard (仪表盘)
|
||||
|
||||
仪表盘 = 多个图表的布局容器;图表绑定数据表/视图。创建仪表盘之前通常先查 `dashboard config-example`,拿到 JSON 骨架,再把图表塞进 `--config` 的布局里。
|
||||
|
||||
#### 仪表盘配置示例
|
||||
```
|
||||
Usage:
|
||||
dws aitable dashboard config-example [flags]
|
||||
Example:
|
||||
dws aitable dashboard config-example --format json
|
||||
```
|
||||
|
||||
返回仪表盘 `--config` 的 JSON 示例,供 create/update 复制裁剪。
|
||||
|
||||
#### 获取仪表盘
|
||||
```
|
||||
Usage:
|
||||
dws aitable dashboard get [flags]
|
||||
Example:
|
||||
dws aitable dashboard get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID>
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
```
|
||||
|
||||
返回布局和 `charts[].chartId`,可直接喂给 `chart get`。
|
||||
|
||||
#### 创建仪表盘
|
||||
```
|
||||
Usage:
|
||||
dws aitable dashboard create [flags]
|
||||
Example:
|
||||
dws aitable dashboard create --base-id <BASE_ID> --config '<JSON>'
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--config string 仪表盘配置 JSON(名称、布局、图表列表都在里面) (必填)
|
||||
```
|
||||
|
||||
> ⚠️ 没有独立的 `--name`;名称和布局一起放在 `--config` JSON 中。
|
||||
|
||||
#### 更新仪表盘
|
||||
```
|
||||
Usage:
|
||||
dws aitable dashboard update [flags]
|
||||
Example:
|
||||
dws aitable dashboard update --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --config '<JSON>'
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--config string 更新后的仪表盘配置 JSON (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
```
|
||||
|
||||
调整布局、增删图表一律改 `--config`。
|
||||
|
||||
#### 删除仪表盘
|
||||
```
|
||||
Usage:
|
||||
dws aitable dashboard delete [flags]
|
||||
Example:
|
||||
dws aitable dashboard delete --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --yes
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
--reason string 删除原因
|
||||
```
|
||||
|
||||
不可逆。
|
||||
|
||||
#### 查看仪表盘分享状态
|
||||
```
|
||||
Usage:
|
||||
dws aitable dashboard share get [flags]
|
||||
Example:
|
||||
dws aitable dashboard share get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID>
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
```
|
||||
|
||||
> ⚠️ 可能返回 404(资源不存在或未开通外链),按可重试错误处理,不要误判为参数拼错。
|
||||
|
||||
#### 更新仪表盘分享
|
||||
```
|
||||
Usage:
|
||||
dws aitable dashboard share update [flags]
|
||||
Example:
|
||||
dws aitable dashboard share update --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --enabled true
|
||||
Flags:
|
||||
--allow-back-to-doc 是否允许从分享页返回原文档(可选)
|
||||
--base-id string Base ID (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
--enabled 是否开启外链分享 (必填)
|
||||
--share-type string 分享类型(权限/可见范围等,可选)
|
||||
```
|
||||
|
||||
开启后返回 shareUrl;关闭后原链接失效。
|
||||
|
||||
### chart (图表)
|
||||
|
||||
图表挂在某个仪表盘下,绑定数据表(可进一步绑定视图)。新建前通常先查 `chart widgets-example`。
|
||||
|
||||
#### 图表组件示例
|
||||
```
|
||||
Usage:
|
||||
dws aitable chart widgets-example [flags]
|
||||
Example:
|
||||
dws aitable chart widgets-example --format json
|
||||
```
|
||||
|
||||
返回图表 widget 的 JSON 示例,供 `chart create/update --config` 参考。
|
||||
|
||||
#### 获取图表
|
||||
```
|
||||
Usage:
|
||||
dws aitable chart get [flags]
|
||||
Example:
|
||||
dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID>
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--chart-id string Chart ID (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
```
|
||||
|
||||
#### 创建图表
|
||||
```
|
||||
Usage:
|
||||
dws aitable chart create [flags]
|
||||
Example:
|
||||
dws aitable chart create --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --config '<JSON>'
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--config string 图表配置 JSON:数据源、维度、度量、样式 (必填)
|
||||
--dashboard-id string 挂载到的 Dashboard ID (必填)
|
||||
--layout string 图表布局 JSON(位置、尺寸,可选)
|
||||
```
|
||||
|
||||
> ⚠️ 名称和类型都在 `--config` JSON 里,没有独立 `--name` / `--type`。
|
||||
|
||||
#### 更新图表
|
||||
```
|
||||
Usage:
|
||||
dws aitable chart update [flags]
|
||||
Example:
|
||||
dws aitable chart update --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID> --config '<JSON>'
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--chart-id string Chart ID (必填)
|
||||
--config string 图表配置 JSON (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
--layout string 图表布局 JSON(位置、尺寸,可选)
|
||||
```
|
||||
|
||||
#### 删除图表
|
||||
```
|
||||
Usage:
|
||||
dws aitable chart delete [flags]
|
||||
Example:
|
||||
dws aitable chart delete --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID> --yes
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--chart-id string 待删除 Chart ID (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
--reason string 删除原因
|
||||
```
|
||||
|
||||
不可逆。
|
||||
|
||||
#### 查看图表分享状态
|
||||
```
|
||||
Usage:
|
||||
dws aitable chart share get [flags]
|
||||
Example:
|
||||
dws aitable chart share get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID>
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--chart-id string Chart ID (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
```
|
||||
|
||||
返回 `enabled` 与 `shareUrl`,用来判断是否已经对外分享。
|
||||
|
||||
#### 更新图表分享
|
||||
```
|
||||
Usage:
|
||||
dws aitable chart share update [flags]
|
||||
Example:
|
||||
dws aitable chart share update --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID> --enabled true
|
||||
Flags:
|
||||
--allow-back-to-doc 是否允许从分享页返回原文档(可选)
|
||||
--base-id string Base ID (必填)
|
||||
--chart-id string Chart ID (必填)
|
||||
--dashboard-id string Dashboard ID (必填)
|
||||
--enabled 是否开启外链分享 (必填)
|
||||
--share-type string 分享类型(权限/可见范围等,可选)
|
||||
```
|
||||
|
||||
### export (数据导出)
|
||||
|
||||
把数据表/视图/整张 Base 导出为 Excel/CSV,下发下载链接;常见是异步任务,首次调用可能只返回 `taskId`,需要按 `taskId` 继续轮询直到拿到 `downloadUrl`。
|
||||
|
||||
#### 导出数据
|
||||
```
|
||||
Usage:
|
||||
dws aitable export data [flags]
|
||||
Example:
|
||||
# 第一步:创建任务(按 scope 传必要参数)
|
||||
dws aitable export data --base-id <BASE_ID> --scope table --table-id <TABLE_ID> --format excel --timeout-ms 1000
|
||||
|
||||
# 第二步:拿 taskId 继续轮询,直到返回 downloadUrl
|
||||
dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 3000
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--scope string 导出范围:all / table / view
|
||||
--table-id string Table ID(scope=table/view 时必填)
|
||||
--view-id string View ID(scope=view 时必填)
|
||||
--format string 导出格式:excel / csv,默认 excel
|
||||
--task-id string 轮询已有任务的 taskId
|
||||
--timeout-ms int 单次调用服务端等待时间(毫秒)
|
||||
```
|
||||
|
||||
参数约束:
|
||||
|
||||
- `scope=all`:只需 `--base-id`
|
||||
- `scope=table`:必须同时传 `--table-id`
|
||||
- `scope=view`:必须同时传 `--table-id + --view-id`
|
||||
|
||||
### template (模板搜索)
|
||||
|
||||
#### 搜索模板
|
||||
@@ -392,6 +715,10 @@ dws aitable chart widgets-example --format json
|
||||
# 2) 先拿 dashboard,再拿 chart 详情
|
||||
dws aitable dashboard get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --format json
|
||||
dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID> --format json
|
||||
|
||||
# 3) 写入/更新都用 --config JSON(名称、布局、图表类型都在里面)
|
||||
dws aitable dashboard create --base-id <BASE_ID> --config '<JSON>' --format json
|
||||
dws aitable chart update --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID> --config '<JSON>' --format json
|
||||
```
|
||||
|
||||
要点:
|
||||
@@ -399,24 +726,20 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
||||
- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用。
|
||||
- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错。
|
||||
- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断。
|
||||
- dashboard/chart 的 create/update 只有 `--config`,没有独立的 `--name` / `--type`,名称与布局都在 JSON 里。
|
||||
|
||||
### 导出数据(两阶段轮询)
|
||||
|
||||
`export data` 常见为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。
|
||||
### 导入外部数据(两步走)
|
||||
|
||||
```bash
|
||||
# 第一步:创建任务(按 scope 传必要参数)
|
||||
dws aitable export data --base-id <BASE_ID> --scope table --table-id <TABLE_ID> --format excel --timeout-ms 1000
|
||||
# 1) 先上传文件拿 importId
|
||||
dws aitable import upload --base-id <BASE_ID> --file-name sales.xlsx --file-size 204800 --format json
|
||||
# 把本地文件 PUT 到返回的 uploadUrl(过期前必须上传完成)
|
||||
|
||||
# 第二步:拿 taskId 继续轮询,直到返回 downloadUrl
|
||||
dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 3000
|
||||
# 2) 触发导入
|
||||
dws aitable import data --import-id <IMPORT_ID> --timeout 60 --format json
|
||||
```
|
||||
|
||||
参数约束
|
||||
|
||||
- `scope=all`:只需 `base-id`
|
||||
- `scope=table`:必须 `table-id`
|
||||
- `scope=view`:必须同时 `table-id + view-id`
|
||||
要点:`--file-size` 单位字节,必须与实际文件一致;`--timeout` 单位秒,不是毫秒。
|
||||
|
||||
## 意图判断
|
||||
|
||||
@@ -446,9 +769,37 @@ dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 300
|
||||
- 修改/更新 → `record update`(需 recordId,先 `record query`)
|
||||
- 删除 → `record delete` [危险](需 recordId)
|
||||
|
||||
用户说"视图/筛选视图/看板/画廊":
|
||||
- 查看 → `view get`
|
||||
- 新建 → `view create`(`--view-type` 指定视图类型)
|
||||
- 修改(过滤/排序/分组/可见字段) → `view update`
|
||||
- 删除 → `view delete` [危险]
|
||||
|
||||
用户说"导入/上传 Excel/CSV":
|
||||
- 外部文件导入 → `import upload` → PUT 文件 → `import data`(两步)
|
||||
|
||||
用户说"导出/下载表格数据":
|
||||
- → `export data`(按 scope=all/table/view 传参,异步轮询)
|
||||
|
||||
用户说"仪表盘/dashboard":
|
||||
- 查看布局 → `dashboard get`
|
||||
- 查看模板 → `dashboard config-example`
|
||||
- 新建/修改布局 → `dashboard create` / `dashboard update`(配置都在 `--config` JSON)
|
||||
- 删除 → `dashboard delete` [危险]
|
||||
- 对外分享 → `dashboard share get` / `dashboard share update`
|
||||
|
||||
用户说"图表/chart":
|
||||
- 查看 → `chart get`(需要 dashboardId)
|
||||
- 查看组件模板 → `chart widgets-example`
|
||||
- 新建/修改 → `chart create` / `chart update`(配置都在 `--config` JSON)
|
||||
- 删除 → `chart delete` [危险]
|
||||
- 对外分享 → `chart share get` / `chart share update`
|
||||
|
||||
用户说"附件/文件字段" → `attachment upload`(不要用钉盘 drive)
|
||||
|
||||
用户说"模板" → `template search`
|
||||
|
||||
关键区分: base=表格文件, table=数据表, field=列, record=行
|
||||
关键区分: base=表格文件, table=数据表, field=列, record=行, view=视图, dashboard=仪表盘, chart=图表
|
||||
|
||||
## 核心工作流
|
||||
|
||||
@@ -476,15 +827,23 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
|------|-------------|------|
|
||||
| `base list/search` | `baseId` | 所有后续命令的 --base-id,拼接文档 URI |
|
||||
| `base create` | `baseId` | 后续命令 + 文档 URI |
|
||||
| `base get` | `tables[].tableId` | --table-id |
|
||||
| `table get` | `fields[].fieldId` | record 操作的 cells key, field get/update/delete |
|
||||
| `base get` | `tables[].tableId`、`dashboards[].dashboardId` | --table-id / --dashboard-id |
|
||||
| `table get` | `fields[].fieldId`、`views[].viewId` | record 操作的 cells key;field get/update/delete;view 操作 |
|
||||
| `view get` | `viewId` | record query 按视图收敛;view update/delete |
|
||||
| `record query` | `recordId` | record update/delete |
|
||||
| `dashboard get` | `charts[].chartId` | chart get/update/delete/share |
|
||||
| `import upload` | `importId`、`uploadUrl` | PUT 文件后调 import data |
|
||||
| `export data` (首次) | `taskId` | 下一轮 export data 轮询 |
|
||||
| `attachment upload` | `fileToken` | record create/update 写入 attachment 字段 |
|
||||
| `template search` | `templateId` | base create --template-id,拼接模板预览 URI |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有操作使用 ID(baseId/tableId/fieldId/recordId),不使用名称
|
||||
- 所有操作使用 ID(baseId/tableId/fieldId/recordId/viewId/dashboardId/chartId),不使用名称
|
||||
- records 的 cells key 是 fieldId,不是字段名称
|
||||
- dashboard/chart 的 create/update 只有 `--config`(JSON 内含名称/布局/类型),没有独立 `--name`
|
||||
- view 创建的类型参数是 `--view-type`,不是 `--type`
|
||||
- import upload 的文件大小参数是 `--file-size`,不是 `--size`
|
||||
|
||||
## `--filters` 筛选语法排错与使用规范(极易出错)
|
||||
|
||||
|
||||
@@ -30,11 +30,17 @@ Usage:
|
||||
Example:
|
||||
dws calendar event create --title "Q1 复盘会" \
|
||||
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00"
|
||||
dws calendar event create --title "Q1 复盘会" \
|
||||
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" \
|
||||
--attendees userId1,userId2 --timezone "Asia/Shanghai" --desc "Q1 复盘"
|
||||
Flags:
|
||||
--desc string 日程描述
|
||||
--end string 结束时间 ISO-8601 (必填)
|
||||
--start string 开始时间 ISO-8601 (必填)
|
||||
--title string 日程标题 (必填)
|
||||
--attendees string 参会人 userId 列表 (逗号分隔)
|
||||
--desc string 日程描述
|
||||
--end string 结束时间 ISO-8601 (必填)
|
||||
--open-dingtalk-ids string 参会人 openDingTalkId 列表 (逗号分隔)
|
||||
--start string 开始时间 ISO-8601 (必填)
|
||||
--timezone string 时区 (如 Asia/Shanghai)
|
||||
--title string 日程标题 (必填)
|
||||
```
|
||||
|
||||
### 修改日程
|
||||
@@ -43,11 +49,14 @@ Usage:
|
||||
dws calendar event update [flags]
|
||||
Example:
|
||||
dws calendar event update --id <EVENT_ID> --title "新标题"
|
||||
dws calendar event update --id <EVENT_ID> --desc "议程调整" --timezone "Asia/Shanghai"
|
||||
Flags:
|
||||
--end string 新结束时间
|
||||
--id string 日程 ID (必填)
|
||||
--start string 新开始时间
|
||||
--title string 新标题
|
||||
--desc string 新描述
|
||||
--end string 新结束时间
|
||||
--id string 日程 ID (必填)
|
||||
--start string 新开始时间
|
||||
--timezone string 时区 (如 Asia/Shanghai)
|
||||
--title string 新标题
|
||||
```
|
||||
|
||||
### 删除日程
|
||||
@@ -60,6 +69,23 @@ Flags:
|
||||
--id string 日程 ID (必填)
|
||||
```
|
||||
|
||||
### 推荐会议时间
|
||||
```
|
||||
Usage:
|
||||
dws calendar event suggest [flags]
|
||||
Example:
|
||||
dws calendar event suggest --users <USER_ID_1>,<USER_ID_2> \
|
||||
--start "2026-03-10T09:00:00+08:00" --end "2026-03-10T18:00:00+08:00" --duration 30
|
||||
Flags:
|
||||
--duration string 会议时长(分钟)
|
||||
--end string 候选时段结束时间 ISO-8601 (必填)
|
||||
--start string 候选时段开始时间 ISO-8601 (必填)
|
||||
--timezone string 时区 (如 Asia/Shanghai)
|
||||
--users string 参会人 userId 列表 (必填)
|
||||
```
|
||||
|
||||
基于参会人闲忙数据给出排名后的候选时段,比 `busy search` 直接给出原始忙闲更适合"帮我约"场景。
|
||||
|
||||
### 查看参与者
|
||||
```
|
||||
Usage:
|
||||
@@ -157,6 +183,7 @@ Flags:
|
||||
- 创建/约 → `event create`
|
||||
- 修改/改时间 → `event update`
|
||||
- 取消/删除 → `event delete`
|
||||
- 帮我约/找个大家都有空的时间 → `event suggest`
|
||||
|
||||
用户说"参会人/与会者":
|
||||
- 查看 → `participant list`
|
||||
@@ -170,7 +197,8 @@ Flags:
|
||||
- 分组 → `room list-groups`,取 groupId 后 `room search --group-id`
|
||||
|
||||
用户说"有空吗/忙不忙/闲忙":
|
||||
- 查询 → `busy search`
|
||||
- 要原始闲忙块 → `busy search`
|
||||
- 要直接给时段候选 → `event suggest`
|
||||
|
||||
## 核心工作流
|
||||
|
||||
|
||||
@@ -9,21 +9,20 @@
|
||||
| 子命令 | 用途 |
|
||||
|-------|------|
|
||||
| `group create` | 创建内部群 |
|
||||
| `group create-org` | 创建企业全员群 |
|
||||
| `group members list` | 查看群成员列表 |
|
||||
| `group members` | 查看群成员列表 |
|
||||
| `group members add` | 添加群成员 |
|
||||
| `group members remove` | 移除群成员(⚠️ 危险操作) |
|
||||
| `group members add-bot` | 添加机器人到群 |
|
||||
| `group rename` | 修改群名称 |
|
||||
| `search` | 搜索群会话 |
|
||||
| `search-common` | 搜索共同群 |
|
||||
| `conversation-info` | 获取会话基础信息(单聊/群聊) |
|
||||
|
||||
### message (会话消息管理)
|
||||
|
||||
| 子命令 | 用途 |
|
||||
|-------|------|
|
||||
| `message send` | 以当前用户身份发群消息或单聊消息 |
|
||||
| `message send-personal` | 发送个人消息(⚠️ 敏感操作) |
|
||||
| `message list` | 拉取群聊或单聊会话消息 |
|
||||
| `message list-all` | 按时间范围拉取当前用户所有会话消息 |
|
||||
| `message list-topic-replies` | 拉取群话题回复消息列表 |
|
||||
@@ -32,7 +31,6 @@
|
||||
| `message list-focused` | 拉取特别关注人的消息 |
|
||||
| `message list-unread-conversations` | 获取未读会话列表 |
|
||||
| `message search` | 按关键词搜索消息 |
|
||||
| `message info` | 获取会话信息 |
|
||||
| `message send-by-bot` | 机器人发消息(群聊或批量单聊) |
|
||||
| `message recall-by-bot` | 机器人撤回消息 |
|
||||
| `message send-by-webhook` | 自定义机器人 Webhook 发消息 |
|
||||
@@ -43,8 +41,6 @@
|
||||
| 子命令 | 用途 |
|
||||
|-------|------|
|
||||
| `bot search` | 搜索我的机器人 |
|
||||
| `bot create` | 创建企业机器人 |
|
||||
| `bot search-groups` | 搜索机器人所在群 |
|
||||
|
||||
---
|
||||
|
||||
@@ -64,31 +60,15 @@ Flags:
|
||||
|
||||
---
|
||||
|
||||
## group create-org — 创建企业全员群
|
||||
|
||||
创建面向企业组织的群,成员通过 userId 列表指定。
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws chat group create-org [flags]
|
||||
Example:
|
||||
dws chat group create-org --name "全员通知群" --users userId1,userId2
|
||||
Flags:
|
||||
--name string 群名称 (必填)
|
||||
--users string 成员 userId 列表,逗号分隔 (必填)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## group members list — 查看群成员列表
|
||||
## group members — 查看群成员列表
|
||||
|
||||
分页查询指定群聊的成员。
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws chat group members list [flags]
|
||||
dws chat group members [flags]
|
||||
Example:
|
||||
dws chat group members list --id <openconversation_id>
|
||||
dws chat group members --id <openconversation_id>
|
||||
Flags:
|
||||
--cursor string 分页游标,首次从 0 开始
|
||||
--id string 群 ID / openconversation_id (必填)
|
||||
@@ -227,6 +207,12 @@ Flags:
|
||||
--at-all @所有人(仅群聊时生效,可选,默认 false)
|
||||
--at-users string @指定成员的 userId 列表,逗号分隔(仅群聊时生效,可选)
|
||||
--media-id string 图片 mediaId(通过 dt_media_upload 工具上传获得,需从返回链接中去除 _宽_高.格式 后缀并加上 @ 前缀),指定后发送图片消息,不需要传文本内容
|
||||
--msg-type string 消息类型(可选,如 text/markdown/image/file;通常由 --text/--media-id/--dentry-id 自动推断)
|
||||
--dentry-id string 钉盘文件 dentryId(发送钉盘文件消息时使用,需配合 --space-id)
|
||||
--space-id string 钉盘空间 spaceId(与 --dentry-id 配合使用)
|
||||
--file-name string 文件消息的文件名
|
||||
--file-size string 文件消息的文件大小(字节)
|
||||
--file-type string 文件消息的文件类型(如 pdf / docx / xlsx 等)
|
||||
|
||||
注意:
|
||||
- --text 和位置参数二选一,--text 优先
|
||||
@@ -234,33 +220,7 @@ Flags:
|
||||
- --group 的别名: --id, --chat, --conversation-id (均可替代 --group)
|
||||
- --at-all 和 --at-users 仅在 --group 群聊时生效;当设置--at-all时,消息内容中一定要包含对应的占位符<@all>;当设置--at-users userId1,userId2时,消息内容中一定要包含对应格式的占位符<@userId1> <@userId2>
|
||||
- --media-id 指定图片 mediaId 时自动发送图片消息(msgType=image),不需要传 --text;图片单聊仅支持 --open-dingtalk-id,不支持 --user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## message send-personal — 发送个人消息
|
||||
|
||||
> ⚠️ 敏感操作:执行前必须向用户确认,同意后才加 `--yes`。
|
||||
|
||||
发送个人消息到指定会话或指定用户。支持 @指定人。
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws chat message send-personal [flags]
|
||||
Example:
|
||||
dws chat message send-personal --id <openConversationId> --content "你好" --type text
|
||||
dws chat message send-personal --open-id <openDingTalkId> --content "消息内容" --type text
|
||||
dws chat message send-personal --id <openConversationId> --content "内容" --at-all
|
||||
Flags:
|
||||
--content string 消息内容 (必填)
|
||||
--type string 消息类型,如 text、markdown (必填)
|
||||
--id string 群聊会话 ID openConversationId(与 --open-id 二选一)
|
||||
--open-id string 接收人 openDingTalkId(与 --id 二选一)
|
||||
--at-all @所有人(可选)
|
||||
--at-users string @指定人的 openDingTalkId 列表,逗号分隔(可选)
|
||||
|
||||
注意:
|
||||
- --id(群聊会话)和 --open-id(指定用户 openDingTalkId)二选一
|
||||
- 发送钉盘文件消息:传 --dentry-id + --space-id(必要时配合 --file-name / --file-size / --file-type),msg-type 自动推断为 file
|
||||
```
|
||||
|
||||
---
|
||||
@@ -457,22 +417,22 @@ Flags:
|
||||
|
||||
---
|
||||
|
||||
## message info — 获取会话信息
|
||||
## conversation-info — 获取会话基础信息
|
||||
|
||||
获取指定群聊或单聊会话的详情信息。
|
||||
按会话 ID 获取单聊或群聊的基础元数据(名称、类型、成员数等)。在对会话执行操作前用来确认上下文。
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws chat message info [flags]
|
||||
dws chat conversation-info [flags]
|
||||
Example:
|
||||
dws chat message info --id <openConversationId>
|
||||
dws chat message info --open-id <openDingTalkId>
|
||||
dws chat conversation-info --group <openConversationId>
|
||||
dws chat conversation-info --open-dingtalk-id <openDingTalkId>
|
||||
Flags:
|
||||
--id string 群聊会话 ID openConversationId(与 --open-id 二选一)
|
||||
--open-id string 用户 openDingTalkId(单聊时与 --id 二选一)
|
||||
--group string 群聊会话 ID openConversationId(与 --open-dingtalk-id 二选一)
|
||||
--open-dingtalk-id string 用户 openDingTalkId(单聊时与 --group 二选一)
|
||||
|
||||
注意:
|
||||
- --id(群聊)和 --open-id(单聊用户 openDingTalkId)二选一
|
||||
- --group(群聊 openConversationId)和 --open-dingtalk-id(单聊用户 openDingTalkId)二选一
|
||||
```
|
||||
|
||||
---
|
||||
@@ -515,37 +475,6 @@ Flags:
|
||||
|
||||
---
|
||||
|
||||
## bot create — 创建企业机器人
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws chat bot create [flags]
|
||||
Example:
|
||||
dws chat bot create --name "日报提醒机器人" --desc "负责每日日报提醒"
|
||||
Flags:
|
||||
--name string 机器人名称 (必填)
|
||||
--desc string 机器人描述(可选)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## bot search-groups — 搜索机器人所在群
|
||||
|
||||
搜索指定机器人已加入的群列表。
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws chat bot search-groups [flags]
|
||||
Example:
|
||||
dws chat bot search-groups --keyword "项目"
|
||||
dws chat bot search-groups --keyword "冲刺" --cursor <nextCursor>
|
||||
Flags:
|
||||
--keyword string 搜索关键词 (必填)
|
||||
--cursor string 分页游标(首页留空,翻页传返回的 cursor)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## message send-by-bot — 机器人发消息
|
||||
|
||||
群聊:传 --group 指定群;单聊:传 --users 指定用户列表,二者只能选其一,不能同时指定。--text 支持 Markdown。
|
||||
@@ -612,9 +541,8 @@ Flags:
|
||||
## 意图判断
|
||||
|
||||
用户说"建群/创建群聊" → `chat group create`
|
||||
用户说"创建企业全员群/组织群" → `chat group create-org`
|
||||
用户说"搜索群/找群" → `chat search`
|
||||
用户说"群成员/看群里有谁" → `chat group members list`
|
||||
用户说"群成员/看群里有谁" → `chat group members`
|
||||
用户说"拉人进群/加群成员" → `chat group members add`
|
||||
用户说"踢人/移除群成员" → `chat group members remove`
|
||||
用户说"加机器人到群" → `chat group members add-bot`
|
||||
@@ -626,7 +554,6 @@ Flags:
|
||||
用户说"未读消息会话/未读会话列表/我的未读会话" → `chat message list-unread-conversations`
|
||||
用户说"发群消息(以个人身份)" → `chat message send --group`
|
||||
用户说"发单聊消息(以个人身份)" → `chat message send --user`(有 userId 时)或 `chat message send --open-dingtalk-id`(有 openDingTalkId 时)
|
||||
用户说"发个人消息/个人通知" → `chat message send-personal`(⚠️ 敏感操作,需确认)
|
||||
用户说"机器人发消息/机器人群发" → `chat message send-by-bot`
|
||||
用户说"机器人撤回消息" → `chat message recall-by-bot`
|
||||
用户说"Webhook 发消息/告警消息" → `chat message send-by-webhook`
|
||||
@@ -634,12 +561,10 @@ Flags:
|
||||
用户说"所有消息/全部会话消息/拉取全部消息/时间范围内消息/我的消息/我今天的消息/查我的钉钉消息/最近的消息" → `chat message list-all`
|
||||
用户说"特别关注人的消息/关注的人的消息/星标联系人的消息" → `chat message list-focused`
|
||||
用户说"查看我的机器人" → `chat bot search`
|
||||
用户说"创建机器人" → `chat bot create`
|
||||
用户说"搜索消息/查找关键词/搜一下消息里的XX" → `chat message search`
|
||||
用户说"我和XX的共同群/我们都在哪些群/查共同群" → `chat search-common`
|
||||
用户说"置顶会话/置顶消息/我的置顶/查看置顶" → `chat list-top-conversations`
|
||||
用户说"获取会话信息/会话详情" → `chat message info`
|
||||
用户说"机器人在哪些群/机器人的群" → `chat bot search-groups`
|
||||
用户说"获取会话信息/会话详情/会话元数据" → `chat conversation-info`
|
||||
|
||||
关键区分:
|
||||
- `chat message list` — 拉取指定会话的消息(需指定 --group 或 --user),按时间点 + 方向翻页
|
||||
@@ -652,15 +577,13 @@ Flags:
|
||||
- `chat message list-focused` — 拉取特别关注人的消息,cursor 分页
|
||||
- `chat list-top-conversations` — 拉取置顶会话列表(用户询问"置顶会话"或"置顶消息"时路由到此),cursor 分页
|
||||
- `chat message send` — 以**当前用户**身份发消息(群聊或单聊),text 为位置参数;支持 --media-id 发送图片消息
|
||||
- `chat message send-personal` — 发送个人消息,支持通过 openConversationId 或 openDingTalkId 指定目标(⚠️ 敏感操作)
|
||||
- `chat message search` — 按关键词搜索消息内容(跨所有会话,可选指定群)
|
||||
- `chat search-common` — 搜索共同群,查询指定人共同所在的群聊(AND=所有人都在,OR=任一人在)
|
||||
- `chat message send-by-bot` — 以**机器人**身份发消息(群聊或单聊),text 为 --text flag
|
||||
- `chat message send-by-webhook` — 通过**自定义机器人 Webhook** 发群消息
|
||||
- `chat message recall-by-bot` — 通过机器人撤回已发送的消息
|
||||
- `chat message info` — 获取指定会话的详情信息
|
||||
- `chat bot create` — 创建新的企业机器人
|
||||
- `chat bot search-groups` — 搜索机器人所在群列表
|
||||
- `chat conversation-info` — 按会话 ID 获取单聊/群聊的基础元数据(名称、类型、成员数等)
|
||||
- `chat bot search` — 搜索当前用户名下的机器人,拿到 robotCode 用于 send-by-bot / recall-by-bot / group members add-bot
|
||||
|
||||
## 核心工作流
|
||||
|
||||
@@ -715,16 +638,18 @@ dws chat message recall-by-bot --robot-code <robot-code> --group <openconversati
|
||||
--keys <processQueryKey> --format json
|
||||
```
|
||||
|
||||
### 创建并使用机器人(完整流程)
|
||||
### 将已有机器人加入群并发消息(完整流程)
|
||||
|
||||
机器人需先在钉钉开放平台创建好,这里只做"找机器人 → 加入群 → 发消息"。
|
||||
|
||||
```bash
|
||||
# Step 1: 创建机器人
|
||||
dws chat bot create --name "项目提醒机器人" --desc "项目状态提醒" --format json
|
||||
# Step 1: 搜索我的机器人 — 提取 robotCode
|
||||
dws chat bot search --name "项目提醒" --format json
|
||||
|
||||
# Step 2: 搜索群 — 提取 openConversationId
|
||||
dws chat search --query "项目群" --format json
|
||||
|
||||
# Step 3: 将机器人添加到群
|
||||
# Step 3: 将机器人添加到群(需当前用户对该群有管理权限)
|
||||
dws chat group members add-bot --id <openConversationId> --robot-code <robotCode> --format json
|
||||
|
||||
# Step 4: 机器人发消息
|
||||
@@ -775,8 +700,8 @@ dws chat message send --group <openconversation_id> \
|
||||
| `aisearch person` | `userId` | message send 的 --user、--at-users、send-by-bot 的 --users、list-by-sender 的 --sender-user-id |
|
||||
| `aisearch person` → `contact user get` | `openDingTalkId` | list-by-sender 的 --sender-open-dingtalk-id、message send/list 的 --open-dingtalk-id |
|
||||
| `chat bot search` | `robotCode` | send-by-bot / recall-by-bot 的 --robot-code、group members add-bot 的 --robot-code |
|
||||
| `chat bot create` | `robotCode` | send-by-bot / recall-by-bot 的 --robot-code |
|
||||
| `chat message send-by-bot` | `processQueryKey` | recall-by-bot 的 --keys |
|
||||
| `chat conversation-info` | `openConversationId` / 成员信息 | 作为 message send/list 等后续操作的会话上下文确认 |
|
||||
| `chat message search` | `nextCursor` | 下次 message search 的 --cursor |
|
||||
| `chat search-common` | `openConversationId` | message send/list 等的 --group |
|
||||
| `drive download` | 下载链接 | message send 的 Markdown 图片/链接语法 |
|
||||
@@ -785,7 +710,6 @@ dws chat message send --group <openconversation_id> \
|
||||
|
||||
- `--group` 为群聊会话 ID (openconversation_id),可从群搜索或群聊信息中获取
|
||||
- `chat message send` 的 text 是位置参数(恰好 1 个),非 flag;群聊用 `--group`,单聊用 `--user`(userId)或 `--open-dingtalk-id`(openDingTalkId),三者互斥;`--at-all`、`--at-users` 仅在 `--group` 群聊时生效;发送图片消息用 `--media-id`
|
||||
- `chat message send-personal` 为敏感操作(isSensitive),执行前需用户明确确认
|
||||
- `chat message list-all` 的四个参数(--start、--end、--limit、--cursor)每次请求都必须传递;翻页时用响应中的 nextCursor 值作为下次 --cursor
|
||||
- `chat message list` 的 `--group`、`--user`、`--open-dingtalk-id` 三者互斥,必须且只能指定其一
|
||||
- `chat message list-by-sender` 不需要指定单聊/群聊,返回结果自带会话类型标识
|
||||
|
||||
@@ -17,9 +17,9 @@ Example:
|
||||
Usage:
|
||||
dws contact user search [flags]
|
||||
Example:
|
||||
dws contact user search --keyword "张三"
|
||||
dws contact user search --query "张三"
|
||||
Flags:
|
||||
--keyword string 搜索关键词 (必填)
|
||||
--query string 搜索关键词 (必填)
|
||||
```
|
||||
|
||||
#### 按手机号搜索用户
|
||||
@@ -49,9 +49,9 @@ Flags:
|
||||
Usage:
|
||||
dws contact dept search [flags]
|
||||
Example:
|
||||
dws contact dept search --keyword "技术部"
|
||||
dws contact dept search --query "技术部"
|
||||
Flags:
|
||||
--keyword string 搜索关键词 (必填)
|
||||
--query string 搜索关键词 (必填)
|
||||
```
|
||||
|
||||
#### 查看部门成员
|
||||
@@ -79,10 +79,10 @@ Flags:
|
||||
dws contact user get-self --format json
|
||||
|
||||
# 2. 按名字搜索同事 — 提取 userId
|
||||
dws contact user search --keyword "张三" --format json
|
||||
dws contact user search --query "张三" --format json
|
||||
|
||||
# 3. 查看部门结构 — 提取 deptId
|
||||
dws contact dept search --keyword "技术部" --format json
|
||||
dws contact dept search --query "技术部" --format json
|
||||
|
||||
# 4. 查看部门成员
|
||||
dws contact dept list-members --ids <deptId> --format json
|
||||
@@ -93,7 +93,6 @@ dws contact dept list-members --ids <deptId> --format json
|
||||
| 操作 | 提取 | 用于 |
|
||||
|------|------|------|
|
||||
| `user get-self/search` | `userId` | 其他产品中的 --users/--executor 参数 |
|
||||
| `user get-self/search` | `orgAuthEmail` | mail message send 的 --to/--cc (跨产品) |
|
||||
| `dept search` | `deptId` | dept list-members 的 --ids |
|
||||
|
||||
## 注意事项
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# 开放平台文档 (devdoc) 命令参考
|
||||
|
||||
搜索钉钉**开放平台**开发文档,用于回答开发者关于 OpenAPI、字段、错误码、接入指南、配额等技术问题。
|
||||
|
||||
## 命令总览
|
||||
|
||||
### 搜索开发文档
|
||||
```
|
||||
Usage:
|
||||
dws devdoc article search [flags]
|
||||
Example:
|
||||
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)
|
||||
```
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户问开放平台 API / 字段 / 错误码 / SDK / 鉴权 / 回调 / 配额相关的技术细节:
|
||||
- 走 `devdoc article search`,把用户问的关键短语作为 `--query`
|
||||
|
||||
关键区分:
|
||||
- devdoc(钉钉**开放平台**开发者文档,面向研发) vs doc(钉钉在线文档,面向普通用户内容)
|
||||
- devdoc 只做搜索,不做读取;命中条目返回标题、摘要、文档链接,由 Agent 引用链接或进一步浏览
|
||||
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
# 开发者问"OAuth2 怎么接"
|
||||
dws devdoc article search --query "OAuth2 接入" --format json
|
||||
|
||||
# 命中结果多时翻页
|
||||
dws devdoc article search --query "消息卡片" --page 2 --size 5 --format json
|
||||
|
||||
# 查错误码 / 字段含义
|
||||
dws devdoc article search --query "errcode 40078" --format json
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `--query` 必填;建议传用户原话里的关键名词(API 名、错误码、能力名),不要过度改写
|
||||
- 返回按相关性排序,默认 `--size 10`;要拿更多结果时先翻页,再考虑换关键词
|
||||
- 命中结果里的链接是钉钉开放平台公开文档,可直接给用户做参考
|
||||
- 不要把 devdoc 用来查业务数据(那是 aitable / doc / report 的事);devdoc 只查**官方开发者文档**
|
||||
@@ -80,6 +80,22 @@ Flags:
|
||||
--markdown string 文档初始 Markdown 内容
|
||||
```
|
||||
|
||||
### 创建其他类型文件 (表格/脑图/白板/多维表/画板)
|
||||
```
|
||||
Usage:
|
||||
dws doc file create [flags]
|
||||
Example:
|
||||
dws doc file create --name "项目周报" --type adoc
|
||||
dws doc file create --name "数据统计" --type axls --folder <FOLDER_ID>
|
||||
dws doc file create --name "思维导图" --type amind --workspace <WS_ID>
|
||||
dws doc file create --name "子文件夹" --type folder
|
||||
Flags:
|
||||
--name string 文件名称 (必填)
|
||||
--type string 文件类型 (必填): adoc=文档, axls=表格, appt=演示, adraw=白板, amind=脑图, able=多维表, folder=文件夹
|
||||
--folder string 目标文件夹 ID 或 URL
|
||||
--workspace string 目标知识库 ID 或 URL
|
||||
```
|
||||
|
||||
### 更新文档内容
|
||||
```
|
||||
Usage:
|
||||
@@ -135,6 +151,45 @@ Flags:
|
||||
--workspace string 目标知识库 ID
|
||||
```
|
||||
|
||||
### 复制文档/文件
|
||||
```
|
||||
Usage:
|
||||
dws doc copy [flags]
|
||||
Example:
|
||||
dws doc copy --node <DOC_ID> --folder <TARGET_FOLDER_ID>
|
||||
dws doc copy --node <DOC_ID> --workspace <TARGET_WS_ID>
|
||||
dws doc copy --node "https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>" --folder <FOLDER_ID>
|
||||
Flags:
|
||||
--node string 源文档/文件 ID 或 URL (必填)
|
||||
--folder string 目标文件夹 ID 或 URL
|
||||
--workspace string 目标知识库 ID 或 URL (不传 --folder 时复制到该知识库根目录)
|
||||
```
|
||||
|
||||
### 移动文档/文件
|
||||
```
|
||||
Usage:
|
||||
dws doc move [flags]
|
||||
Example:
|
||||
dws doc move --node <DOC_ID> --folder <TARGET_FOLDER_ID>
|
||||
dws doc move --node <DOC_ID> --workspace <TARGET_WS_ID>
|
||||
Flags:
|
||||
--node string 源文档/文件 ID 或 URL (必填)
|
||||
--folder string 目标文件夹 ID 或 URL
|
||||
--workspace string 目标知识库 ID 或 URL (不传 --folder 时移动到该知识库根目录)
|
||||
```
|
||||
|
||||
### 重命名文档/文件
|
||||
```
|
||||
Usage:
|
||||
dws doc rename [flags]
|
||||
Example:
|
||||
dws doc rename --node <DOC_ID> --name "新名称"
|
||||
dws doc rename --node "https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>" --name "项目周报 v2"
|
||||
Flags:
|
||||
--node string 文档/文件 ID 或 URL (必填)
|
||||
--name string 新名称 (必填)
|
||||
```
|
||||
|
||||
### 查询块元素
|
||||
```
|
||||
Usage:
|
||||
@@ -229,6 +284,24 @@ Flags:
|
||||
--mention string 被 @ 的用户 uid 列表,逗号分隔
|
||||
```
|
||||
|
||||
### 创建划词评论 (内联评论)
|
||||
```
|
||||
Usage:
|
||||
dws doc comment create-inline [flags]
|
||||
Example:
|
||||
dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 0 --end 10 --content "这里需要修改"
|
||||
dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 5 --end 20 --content "建议调整" --selected-text "被选中的原文"
|
||||
dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 0 --end 10 --content "请review" --mention uid1,uid2
|
||||
Flags:
|
||||
--node string 目标文档的标识,支持传入 URL 或 ID (必填)
|
||||
--block-id string 评论锚定所在的块 ID (必填,可通过 dws doc block list 获取)
|
||||
--start int 选中文本在块内的起始字符偏移量,从 0 开始 (必填)
|
||||
--end int 选中文本在块内的结束字符偏移量,必须大于 start (必填)
|
||||
--content string 评论的文字内容,纯文本 (必填)
|
||||
--selected-text string 选中文本内容,填写后评论列表会展示「引用原文:xxx」
|
||||
--mention string 被 @ 的用户 uid 列表,逗号分隔
|
||||
```
|
||||
|
||||
### 回复文档评论
|
||||
```
|
||||
Usage:
|
||||
@@ -282,12 +355,24 @@ Flags:
|
||||
- 元信息 → `info`
|
||||
|
||||
用户说"写文档/创建文档":
|
||||
- 新建 → `create`
|
||||
- 新建纯文档 (adoc) → `create`
|
||||
- 追加内容 → `update --mode append`
|
||||
- 覆盖替换 → `update --mode overwrite`
|
||||
|
||||
用户说"新建表格/脑图/白板/多维表/演示文稿":
|
||||
- 用 `file create --type` 指定类型 (axls/amind/adraw/able/appt/adraw)
|
||||
|
||||
用户说"建文件夹/新建目录":
|
||||
- 创建 → `folder create`
|
||||
- 创建 → `folder create` 或 `file create --type folder`
|
||||
|
||||
用户说"复制文档/拷贝一份":
|
||||
- 复制 → `copy` (需源 --node 和目标 --folder/--workspace)
|
||||
|
||||
用户说"移动文档/换个目录":
|
||||
- 移动 → `move` (需源 --node 和目标 --folder/--workspace)
|
||||
|
||||
用户说"改文档名字/重命名":
|
||||
- 改名 → `rename` (需 --node 和新 --name)
|
||||
|
||||
用户说"上传文件/传文件/上传到文档/上传到知识库":
|
||||
- 上传 → `upload`(需本地文件路径)
|
||||
@@ -390,7 +475,7 @@ dws doc block delete --node <DOC_ID> --block-id <BLOCK_ID> --yes
|
||||
# 1. 查看文档的所有评论
|
||||
dws doc comment list --node <DOC_ID> --format json
|
||||
|
||||
# 2. 在文档上创建评论
|
||||
# 2. 在文档上创建全文评论
|
||||
dws doc comment create --node <DOC_ID> --content "这里需要补充数据来源" --format json
|
||||
|
||||
# 3. 创建评论并 @ 相关人
|
||||
@@ -398,11 +483,34 @@ dws doc comment create --node <DOC_ID> --content "这里需要补充数据来源
|
||||
# 再将 userId 传入 --mention
|
||||
dws doc comment create --node <DOC_ID> --content "请确认这部分内容" --mention <userId1>,<userId2> --format json
|
||||
|
||||
# 4. 回复某条评论(commentKey 从 list 或 create 返回中获取)
|
||||
# 4. 对某段文字创建划词评论(需先 block list 拿 blockId 和字符偏移)
|
||||
dws doc block list --node <DOC_ID> --format json
|
||||
dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 0 --end 12 \
|
||||
--content "这里的数据要复核" --selected-text "被选中的原文片段" --format json
|
||||
|
||||
# 5. 回复某条评论(commentKey 从 list 或 create 返回中获取)
|
||||
dws doc comment reply --node <DOC_ID> --comment-key <COMMENT_KEY> --content "已修改" --format json
|
||||
|
||||
# 5. 用表情回复评论
|
||||
# 6. 用表情回复评论
|
||||
dws doc comment reply --node <DOC_ID> --comment-key <COMMENT_KEY> --content "比心" --emoji --format json
|
||||
|
||||
# ── 工作流 8: 创建非文档类型文件 ──
|
||||
|
||||
# 创建表格 / 脑图 / 白板 / 多维表 / 演示文稿
|
||||
dws doc file create --name "销售数据" --type axls --folder <FOLDER_ID> --format json
|
||||
dws doc file create --name "需求脑图" --type amind --workspace <WS_ID> --format json
|
||||
dws doc file create --name "Q1 立项会" --type appt --format json
|
||||
|
||||
# ── 工作流 9: 整理文档结构 (复制 / 移动 / 重命名) ──
|
||||
|
||||
# 1. 把模板复制到目标目录作为新工作件
|
||||
dws doc copy --node <TEMPLATE_DOC_ID> --folder <TARGET_FOLDER_ID> --format json
|
||||
|
||||
# 2. 把文档从个人空间挪到团队知识库
|
||||
dws doc move --node <DOC_ID> --workspace <WS_ID> --format json
|
||||
|
||||
# 3. 重命名
|
||||
dws doc rename --node <DOC_ID> --name "项目周报 v2" --format json
|
||||
```
|
||||
|
||||
## 上下文传递表
|
||||
@@ -418,8 +526,11 @@ dws doc comment reply --node <DOC_ID> --comment-key <COMMENT_KEY> --content "比
|
||||
| `upload` | `nodeId` / URL | 上传后文件的访问链接 |
|
||||
| `download` | 本地文件路径 | 下载后的文件保存位置 |
|
||||
| `comment list` | `commentList[].commentKey` | comment reply 的 --comment-key |
|
||||
| `comment create` | `commentKey` | comment reply 的 --comment-key |
|
||||
| `contact user search` | `userId` | comment create/reply 的 --mention |
|
||||
| `comment create` / `comment create-inline` | `commentKey` | comment reply 的 --comment-key |
|
||||
| `block list` | `blockId` + 文本内容 | comment create-inline 的 --block-id 及 --start/--end 计算 |
|
||||
| `contact user search` | `userId` | comment create / create-inline / reply 的 --mention |
|
||||
| `file create` | `nodeId` | 后续 read / update / block 操作的 --node(仅 adoc 支持 read/update,axls/amind 等类型用各自产品的命令) |
|
||||
| `copy` / `move` | 新 `nodeId`(copy)或原 nodeId(move) | 后续 read / info 等的 --node |
|
||||
|
||||
## nodeId 双格式说明
|
||||
|
||||
@@ -448,6 +559,10 @@ dws doc read --node "https://alidocs.dingtalk.com/i/nodes/9E05BDRVQePjzLkZt2p2vE
|
||||
- `upload` 支持上传任意类型文件 (PDF、Office、图片等) 到钉钉文档空间或知识库;`--convert` 可将 Office 文件转换为钉钉在线文档
|
||||
- `upload` 是三步自动完成的流程 (获取凭证 → OSS 上传 → 提交入库),无需手动分步操作
|
||||
- `download` 是两步自动完成的流程 (获取下载链接 → HTTP GET 下载),支持自动推断文件名;`--output` 可指定文件路径或目录
|
||||
- `create` 只能建"文档"(adoc);要建表格/脑图/白板/多维表/演示/文件夹,用 `file create --type`
|
||||
- `copy` 需要对源节点有"阅读"权限、对目标目录有"编辑"权限;`move` 需要对源节点有"管理"权限
|
||||
- `copy` / `move` 不传 `--folder` 时,`--workspace` 表示放到知识库根目录;两者都不传则回落到"我的文档"
|
||||
- `comment create` 是全文评论;`comment create-inline` 是划词评论,必须先 `block list` 拿到 `blockId` 并确定 `--start` / `--end` 偏移(按块内纯文本字符算,从 0 开始)
|
||||
|
||||
## 自动化脚本
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# 钉盘 (drive) 命令参考
|
||||
|
||||
钉盘 = DingTalk Drive,用于云端文件存储 / 上传 / 下载 / 目录管理。不是在线文档编辑;要编辑文档请用 [doc](./doc.md)。
|
||||
|
||||
## 命令总览
|
||||
|
||||
### 列出钉盘目录
|
||||
```
|
||||
Usage:
|
||||
dws drive list [flags]
|
||||
Example:
|
||||
dws drive list
|
||||
dws drive list --parent-id <FOLDER_ID>
|
||||
dws drive list --parent-id <FOLDER_ID> --max 20 --order-by modifiedTime --order desc
|
||||
Flags:
|
||||
--parent-id string 父目录 ID (不传则列根目录)
|
||||
--space-id string 钉盘空间 ID (一般无需指定)
|
||||
--max int 每页数量 (默认 20)
|
||||
--next-token string 分页游标 (从上次结果的 nextToken 获取)
|
||||
--order-by string 排序字段: name / createdTime / modifiedTime
|
||||
--order string 排序方向: asc / desc
|
||||
--thumbnail 是否返回缩略图链接
|
||||
```
|
||||
|
||||
### 获取文件元信息
|
||||
```
|
||||
Usage:
|
||||
dws drive info [flags]
|
||||
Example:
|
||||
dws drive info --file-id <FILE_ID>
|
||||
Flags:
|
||||
--file-id string 文件或文件夹 ID (必填)
|
||||
--space-id string 钉盘空间 ID (一般无需指定)
|
||||
```
|
||||
|
||||
### 创建文件夹
|
||||
```
|
||||
Usage:
|
||||
dws drive mkdir [flags]
|
||||
Example:
|
||||
dws drive mkdir --name "项目资料"
|
||||
dws drive mkdir --name "子目录" --parent-id <PARENT_FOLDER_ID>
|
||||
Flags:
|
||||
--name string 文件夹名称 (必填)
|
||||
--parent-id string 父目录 ID (不传则建在根目录)
|
||||
--space-id string 钉盘空间 ID (一般无需指定)
|
||||
```
|
||||
|
||||
### 获取下载临时链接
|
||||
```
|
||||
Usage:
|
||||
dws drive download [flags]
|
||||
Example:
|
||||
dws drive download --file-id <FILE_ID>
|
||||
Flags:
|
||||
--file-id string 文件 ID (必填)
|
||||
--space-id string 钉盘空间 ID (一般无需指定)
|
||||
```
|
||||
|
||||
### 获取上传凭证 (上传第一步)
|
||||
```
|
||||
Usage:
|
||||
dws drive upload-info [flags]
|
||||
Example:
|
||||
dws drive upload-info --file-name "report.pdf" --file-size 102400
|
||||
dws drive upload-info --file-name "slides.pptx" --file-size 512000 --parent-id <FOLDER_ID>
|
||||
Flags:
|
||||
--file-name string 文件名含后缀 (必填)
|
||||
--file-size int 文件大小,单位字节 (必填)
|
||||
--mime-type string MIME 类型 (可选,服务端会自动推断)
|
||||
--parent-id string 父目录 ID (不传则上传到根目录)
|
||||
--space-id string 钉盘空间 ID (一般无需指定)
|
||||
```
|
||||
|
||||
### 提交上传 (上传第三步)
|
||||
```
|
||||
Usage:
|
||||
dws drive commit [flags]
|
||||
Example:
|
||||
dws drive commit --file-name "report.pdf" --file-size 102400 --upload-id <UPLOAD_ID>
|
||||
Flags:
|
||||
--file-name string 文件名含后缀 (必填,须与 upload-info 一致)
|
||||
--file-size int 文件大小,单位字节 (必填,须与 upload-info 一致)
|
||||
--upload-id string upload-info 返回的 uploadId (必填)
|
||||
--parent-id string 父目录 ID (必填时须与 upload-info 一致)
|
||||
--space-id string 钉盘空间 ID (一般无需指定)
|
||||
--conflict-handler string 同名冲突策略: AUTO_RENAME / OVERWRITE / RETURN_DENTRY_IF_EXIST (默认 AUTO_RENAME)
|
||||
```
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户说"钉盘有什么文件/列钉盘/看钉盘目录" → `list`
|
||||
用户说"钉盘文件详情/文件信息" → `info` (需 fileId)
|
||||
用户说"新建钉盘目录/钉盘里建文件夹" → `mkdir`
|
||||
用户说"下载钉盘文件/把这个文件拿下来" → `download` 拿临时 URL,再由 Agent 自行发起 HTTP GET
|
||||
用户说"上传文件到钉盘/把本地文件传钉盘":
|
||||
- 三步走: `upload-info` → HTTP PUT 到预签名 URL → `commit`
|
||||
- **没有**一条聚合命令可以完成;必须完整走完三步
|
||||
|
||||
关键区分:
|
||||
- drive(钉盘云存储,面向文件二进制) vs doc(在线文档/知识库,面向富文本内容)
|
||||
- 把图片/文件发到群里一般走 drive 上传拿链接 → chat 发送 Markdown 链接 (见 [chat.md](./chat.md) 的 `drive → chat` 工作流)
|
||||
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
# ── 工作流 1: 浏览钉盘 ──
|
||||
|
||||
# 1. 看根目录
|
||||
dws drive list --format json
|
||||
|
||||
# 2. 进入子目录 (parentId 取自上一步的 dentryUuid)
|
||||
dws drive list --parent-id <FOLDER_ID> --format json
|
||||
|
||||
# 3. 看单个文件的元信息
|
||||
dws drive info --file-id <FILE_ID> --format json
|
||||
|
||||
# ── 工作流 2: 上传本地文件到钉盘 (三步不能省) ──
|
||||
|
||||
# Step 1: 拿上传凭证
|
||||
dws drive upload-info --file-name "report.pdf" --file-size 102400 --parent-id <FOLDER_ID> --format json
|
||||
# → 返回: resourceUrl (预签名 URL), headers, uploadId
|
||||
|
||||
# Step 2: Agent 自己发 HTTP PUT 把文件二进制推到 resourceUrl
|
||||
# 请求头必须携带返回的 headers 全部键值对;期望 HTTP 200
|
||||
curl -X PUT -T ./report.pdf -H "<header-from-step-1>: <value>" "<resourceUrl>"
|
||||
|
||||
# Step 3: 提交入库
|
||||
dws drive commit --file-name "report.pdf" --file-size 102400 --upload-id <UPLOAD_ID> \
|
||||
--parent-id <FOLDER_ID> --format json
|
||||
# → 返回: dentryUuid (= 后续 download/info 用的 fileId)
|
||||
|
||||
# ── 工作流 3: 下载钉盘文件到本地 (两步) ──
|
||||
|
||||
# Step 1: 拿临时下载 URL
|
||||
dws drive download --file-id <FILE_ID> --format json
|
||||
# → 返回: resourceUrl (带签名的临时下载 URL), expirationSeconds
|
||||
|
||||
# Step 2: Agent 自己发 HTTP GET 下载二进制
|
||||
curl -o ./report.pdf "<resourceUrl>"
|
||||
|
||||
# ── 工作流 4: 建目录后批量上传 ──
|
||||
|
||||
# 1. 建目录 → 拿 folderId
|
||||
dws drive mkdir --name "2026 Q1 归档" --format json
|
||||
|
||||
# 2. 再走 "工作流 2" 把每个文件上传到该目录
|
||||
```
|
||||
|
||||
## 上下文传递表
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `list` | `dentryUuid` (folder 类) | 下次 list 的 --parent-id、upload-info / commit 的 --parent-id、mkdir 的 --parent-id |
|
||||
| `list` | `dentryUuid` (file 类) | info / download 的 --file-id |
|
||||
| `list` | `nextToken` | 下次 list 的 --next-token |
|
||||
| `mkdir` | `dentryUuid` | 作为父目录传给 upload-info / commit 的 --parent-id |
|
||||
| `upload-info` | `resourceUrl` + `headers` | Agent 自行执行 HTTP PUT 上传二进制 |
|
||||
| `upload-info` | `uploadId` | commit 的 --upload-id |
|
||||
| `commit` | `dentryUuid` | download / info / chat message 发送图片链接的 --file-id |
|
||||
| `download` | `resourceUrl` | Agent 自行执行 HTTP GET 下载;也可作为 Markdown 图片/附件链接发送到 chat |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 上传是**三步**流程:`upload-info` → 客户端 HTTP PUT 到预签名 URL → `commit`。**没有**自动聚合命令,跳过任何一步都会失败
|
||||
- Step 2 的 HTTP PUT 必须把 upload-info 返回的 `headers` 全部回传,`Content-Type` 通常要留空;只有 PUT 返回 200 才能调 `commit`
|
||||
- 上传凭证 (`uploadId`) 有过期时间,拿到后尽快 commit;过期需重新调 `upload-info`
|
||||
- `download` 只返回临时 URL(几分钟级别有效期),不会把文件落地;要真正下载到本地必须再发 HTTP GET
|
||||
- `--parent-id` 在 upload-info / commit 中要保持一致,否则 commit 会报位置不匹配
|
||||
- `--conflict-handler` 默认 `AUTO_RENAME` (自动重命名);`OVERWRITE` 覆盖同名文件前必须和用户确认
|
||||
- `--space-id` 绝大多数场景不需要传;默认会用用户主钉盘空间
|
||||
- 文件名规则:头尾不能有空格;不能含 `*`、`"`、`<`、`>`、`|`、制表符;不能以 `.` 结尾
|
||||
|
||||
## 相关产品
|
||||
|
||||
- [doc](./doc.md) — 钉钉在线文档 / 知识库(文字、表格、脑图等富文本节点),和 drive 的裸文件存储不同
|
||||
- [chat](./chat.md) — 结合 drive 发送图片 / 附件消息到群聊(Markdown 链接语法)
|
||||
@@ -1,5 +1,7 @@
|
||||
# AI听记 (minutes) 命令参考
|
||||
|
||||
AI 听记:列表 / 详情 / 摘要 / 待办 / 文字稿 / 思维导图 / 发言人 / 热词 / 文件上传。
|
||||
|
||||
## 命令总览
|
||||
|
||||
### 查询我创建的听记列表
|
||||
@@ -143,53 +145,126 @@ Flags:
|
||||
--title string 新标题 (必填)
|
||||
```
|
||||
|
||||
### 发起听记(开始录音)
|
||||
### 覆盖听记 AI 摘要
|
||||
```
|
||||
Usage:
|
||||
dws minutes record start [flags]
|
||||
dws minutes update summary [flags]
|
||||
Example:
|
||||
dws minutes record start
|
||||
dws minutes record start --session-id <sessionId>
|
||||
dws minutes update summary --id <taskUuid> --content "修订后的摘要 Markdown"
|
||||
Flags:
|
||||
--session-id string AI 助理会话 ID (可选)
|
||||
--id string 听记 taskUuid (必填)
|
||||
--content string 新的摘要正文 (必填),会整体覆盖原摘要
|
||||
```
|
||||
|
||||
### 暂停听记录音
|
||||
将 AI 生成的摘要替换成定制版本,适合人工修订或按业务口径重写后回写。
|
||||
|
||||
### 全文替换听记文本
|
||||
```
|
||||
Usage:
|
||||
dws minutes record pause [flags]
|
||||
dws minutes replace-text [flags]
|
||||
Example:
|
||||
dws minutes record pause --id <taskUuid>
|
||||
dws minutes record pause --id <taskUuid> --session-id <sessionId>
|
||||
dws minutes replace-text --id <taskUuid> --search "小钉" --replace "DingTalk"
|
||||
Flags:
|
||||
--id string 听记 taskUuid (必填)
|
||||
--session-id string AI 助理会话 ID (可选)
|
||||
--id string 听记 taskUuid (必填)
|
||||
--search string 需要替换的原文 (必填)
|
||||
--replace string 替换后的文本 (必填)
|
||||
```
|
||||
|
||||
### 恢复听记录音
|
||||
在转写段落与摘要中一次性替换所有命中文本,适合纠正系统性的识别错误(如专有名词)。
|
||||
|
||||
### 替换发言人标签
|
||||
```
|
||||
Usage:
|
||||
dws minutes record resume [flags]
|
||||
dws minutes speaker replace [flags]
|
||||
Example:
|
||||
dws minutes record resume --id <taskUuid>
|
||||
dws minutes record resume --id <taskUuid> --session-id <sessionId>
|
||||
dws minutes speaker replace --id <taskUuid> --from "说话人1" --to "张三"
|
||||
dws minutes speaker replace --id <taskUuid> --from "说话人1" --to "张三" --target-uid <userId>
|
||||
Flags:
|
||||
--id string 听记 taskUuid (必填)
|
||||
--session-id string AI 助理会话 ID (可选)
|
||||
--id string 听记 taskUuid (必填)
|
||||
--from string 原发言人昵称,如 "说话人1" (必填)
|
||||
--to string 新发言人昵称 (必填)
|
||||
--target-uid string 绑定到指定钉钉 userId (可选)
|
||||
```
|
||||
|
||||
### 结束听记录音
|
||||
修正自动分离的发言人标签,可顺带把该说话人对应到真实用户。
|
||||
|
||||
### 添加个人热词
|
||||
```
|
||||
Usage:
|
||||
dws minutes record stop [flags]
|
||||
dws minutes hot-word add [flags]
|
||||
Example:
|
||||
dws minutes record stop --id <taskUuid>
|
||||
dws minutes record stop --id <taskUuid> --session-id <sessionId>
|
||||
dws minutes hot-word add --words "钉钉,悟空,DWS"
|
||||
Flags:
|
||||
--id string 听记 taskUuid (必填)
|
||||
--session-id string AI 助理会话 ID (可选)
|
||||
--words string 热词列表,逗号分隔 (必填)
|
||||
```
|
||||
|
||||
把专有名词/业务术语写入个人热词库,后续听记的语音识别会优先匹配,用来兜底高频误识别。
|
||||
|
||||
### 生成思维导图
|
||||
```
|
||||
Usage:
|
||||
dws minutes mind-graph create [flags]
|
||||
Example:
|
||||
dws minutes mind-graph create --id <taskUuid>
|
||||
Flags:
|
||||
--id string 听记 taskUuid (必填)
|
||||
```
|
||||
|
||||
异步任务:提交后返回 jobId,需要用 `mind-graph status` 轮询结果。
|
||||
|
||||
### 查询思维导图状态
|
||||
```
|
||||
Usage:
|
||||
dws minutes mind-graph status [flags]
|
||||
Example:
|
||||
dws minutes mind-graph status --id <taskUuid>
|
||||
Flags:
|
||||
--id string 听记 taskUuid (必填)
|
||||
```
|
||||
|
||||
轮询 `mind-graph create` 的生成任务,就绪后返回思维导图内容。
|
||||
|
||||
### 创建文件上传会话
|
||||
```
|
||||
Usage:
|
||||
dws minutes upload create [flags]
|
||||
Example:
|
||||
dws minutes upload create --file-name "会议录音.m4a" --file-size 10485760 --title "Q2 复盘"
|
||||
Flags:
|
||||
--file-name string 本地文件名 (必填)
|
||||
--file-size string 文件大小,单位字节 (必填)
|
||||
--title string 听记标题 (可选)
|
||||
--template-id string 摘要模板 ID (可选)
|
||||
--input-language string 语言,如 zh_CN / en_US (可选)
|
||||
--enable-message-card string 是否推送消息卡片: true/false (可选)
|
||||
```
|
||||
|
||||
三步上传的第 1 步:返回 sessionId 和预签名 URL,由调用方把本地音视频文件 HTTP PUT 到该 URL。
|
||||
|
||||
### 完成上传并生成听记
|
||||
```
|
||||
Usage:
|
||||
dws minutes upload complete [flags]
|
||||
Example:
|
||||
dws minutes upload complete --session-id <sessionId>
|
||||
Flags:
|
||||
--session-id string upload create 返回的 sessionId (必填)
|
||||
```
|
||||
|
||||
三步上传的第 3 步:通知服务端文件已上传完毕,触发转写与 AI 处理,返回新的 taskUuid。
|
||||
|
||||
### 取消上传
|
||||
```
|
||||
Usage:
|
||||
dws minutes upload cancel [flags]
|
||||
Example:
|
||||
dws minutes upload cancel --session-id <sessionId>
|
||||
Flags:
|
||||
--session-id string upload create 返回的 sessionId (必填)
|
||||
```
|
||||
|
||||
上传中途放弃或上游出错时释放会话,避免残留。
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户说"我的听记/我创建的听记" → `list mine`(可附加 `--query`、`--start`、`--end` 筛选)
|
||||
@@ -197,23 +272,23 @@ Flags:
|
||||
用户说"有权限的听记/我能访问的听记/所有听记" → `list all`(可附加 `--query`、`--start`、`--end` 筛选)
|
||||
用户说"某时间段内的听记/按时间查听记/按关键词查听记" → 根据所属范围选择 `list mine`/`list shared`/`list all`,附加 `--start`、`--end`、`--query` 参数
|
||||
用户说"听记详情/听记信息" → `get info`
|
||||
用户说"批量看听记/一次查多篇" → `get batch`(`--ids` 传逗号分隔 taskUuid)
|
||||
用户说"摘要/总结/会议纪要" → `get summary`
|
||||
用户说"关键字/关键词" → `get keywords`
|
||||
用户说"原文/转写/录音文字" → `get transcription`
|
||||
用户说"会议待办/听记待办" → `get todos`
|
||||
用户说"改听记标题/重命名听记" → `update title`
|
||||
用户说"发起听记/开始录音" → `record start`
|
||||
用户说"暂停听记/暂停录音" → `record pause`
|
||||
用户说"继续听记/恢复录音" → `record resume`
|
||||
用户说"结束听记/结束录音" → `record stop`
|
||||
用户说"改摘要/覆盖摘要/重写总结" → `update summary`
|
||||
用户说"全文替换/批量改转写里的错字/专有名词搞错了" → `replace-text`
|
||||
用户说"改发言人/把说话人1改成张三/认领发言人" → `speaker replace`
|
||||
用户说"加热词/让后续识别更准/把某个专业术语教给系统" → `hot-word add`
|
||||
用户说"生成思维导图/导图/脑图" → `mind-graph create`(异步),再 `mind-graph status` 轮询
|
||||
用户说"上传录音生成听记/把本地音视频变听记" → `upload create` → 本地 PUT → `upload complete`;放弃则 `upload cancel`
|
||||
用户传入听记 URL(如 `https://shanji.dingtalk.com/app/transcribes/xxx`),从 URL 提取 taskUuid,再执行对应的 get/update 操作
|
||||
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
# 0. 发起听记(开始录音)
|
||||
dws minutes record start --format json
|
||||
|
||||
# 1. 查看我的听记列表 — 提取 taskUuid
|
||||
dws minutes list mine --format json
|
||||
dws minutes list mine --max 10 --next-token <nextToken> --format json
|
||||
@@ -236,13 +311,28 @@ dws minutes get transcription --id <taskUuid> --format json
|
||||
# 4. 提取待办事项
|
||||
dws minutes get todos --id <taskUuid> --format json
|
||||
|
||||
# 5. 修改标题
|
||||
dws minutes update title --id <taskUuid> --title "新标题" --format json
|
||||
# 5. 批量补齐多个 taskUuid 的详情
|
||||
dws minutes get batch --ids <taskUuid1>,<taskUuid2>,<taskUuid3> --format json
|
||||
|
||||
# 6. 录音控制(基于 start 返回的 taskUuid)
|
||||
dws minutes record pause --id <taskUuid> --format json
|
||||
dws minutes record resume --id <taskUuid> --format json
|
||||
dws minutes record stop --id <taskUuid> --format json
|
||||
# 6. 修改标题 / 覆盖摘要
|
||||
dws minutes update title --id <taskUuid> --title "新标题" --format json
|
||||
dws minutes update summary --id <taskUuid> --content "修订后的摘要 Markdown" --format json
|
||||
|
||||
# 7. 修正转写错字 / 发言人标签 / 加热词
|
||||
dws minutes replace-text --id <taskUuid> --search "小钉" --replace "DingTalk" --format json
|
||||
dws minutes speaker replace --id <taskUuid> --from "说话人1" --to "张三" --format json
|
||||
dws minutes hot-word add --words "钉钉,悟空,DWS" --format json
|
||||
|
||||
# 8. 异步生成思维导图(create 拿 jobId,status 轮询)
|
||||
dws minutes mind-graph create --id <taskUuid> --format json
|
||||
dws minutes mind-graph status --id <taskUuid> --format json
|
||||
|
||||
# 9. 从本地音视频上传生成听记(三步走)
|
||||
dws minutes upload create --file-name "会议录音.m4a" --file-size 10485760 --title "Q2 复盘" --format json
|
||||
# → 拿到 sessionId 和预签名 URL,调用方自行 HTTP PUT 把文件推上去
|
||||
dws minutes upload complete --session-id <sessionId> --format json
|
||||
# 放弃上传
|
||||
dws minutes upload cancel --session-id <sessionId> --format json
|
||||
```
|
||||
|
||||
## 上下文传递表
|
||||
@@ -253,18 +343,23 @@ dws minutes record stop --id <taskUuid> --format json
|
||||
| `list shared` | `taskUuid`、`nextToken` | get/update 的 --id;翻页时 --next-token |
|
||||
| `list all` | `taskUuid`、`nextToken` | get/update 的 --id;翻页时 --next-token |
|
||||
| `get batch` | 各听记 `taskUuid` | 进一步查询详情 |
|
||||
| `upload create` | `sessionId`、预签名 URL | 本地 PUT 上传 + `upload complete` / `upload cancel` 的 --session-id |
|
||||
| `upload complete` | 新听记 `taskUuid` | 后续 get/update/mind-graph 的 --id |
|
||||
| `mind-graph create` | 异步 jobId(随 taskUuid 关联) | `mind-graph status` 的 --id 查进度与结果 |
|
||||
|
||||
## 注意事项
|
||||
- `taskUuid` 是听记的唯一标识,所有 get/update 操作均以此为入参
|
||||
- `record start` 对应 MCP 工具 `execute_listening_note_command` 的 `cmd=create`,通常会返回可继续控制录音的 `taskUuid/uuid`
|
||||
- `record pause` / `record resume` / `record stop` 对应 `cmd=pause/resume/end`,需要传入 `--id`(映射 MCP 入参 `uuid`)
|
||||
- `taskUuid` 是听记的唯一标识,所有 get/update/mind-graph/speaker/replace-text 操作均以此为入参
|
||||
- 如果用户传入听记 URL(格式: `https://shanji.dingtalk.com/app/transcribes/<taskUuid>`),直接从路径末段提取 taskUuid 作为 `--id` 参数,无需再调用 list 查询
|
||||
- `list mine`、`list shared`、`list all` 统一走 `list_by_keyword_and_time_range` 链路,通过 `belongingConditionId` 区分(`created` / `shared` / `noLimit`)
|
||||
- 三个 list 命令均支持 `--max`、`--next-token` 分页及 `--query`、`--start`、`--end` 筛选
|
||||
- `list mine`、`list shared`、`list all` 统一走 `list_by_keyword_and_time_range` 链路,通过 `belongingConditionId` 区分(`created` / `shared` / `noLimit`);三者均支持 `--max`、`--next-token` 分页及 `--query`、`--start`、`--end` 筛选
|
||||
- `list mine`、`list shared` 默认每页 20 条,`list all` 默认每页 10 条
|
||||
- `get summary` 返回 AI 生成的结构化 Markdown 摘要
|
||||
- `get info` 是单条查询;`get batch` 是批量补齐 ID 列表(`--ids` 逗号分隔),用在"有一串 taskUuid,想一次拿标题/时长/参与人"的场景
|
||||
- `get summary` 返回 AI 生成的结构化 Markdown 摘要;`update summary` 会整体覆盖原摘要,不做增量合并
|
||||
- `get transcription` 的 `--direction` 控制时间排序: 0=正序(默认), 1=倒序
|
||||
- `get batch` 支持一次查询多个听记,用逗号分隔 taskUuid
|
||||
- `replace-text` 是系统性纠错:命中即替换,整篇转写与摘要一起改,慎用模糊文本
|
||||
- `speaker replace` 只改"标签",不重跑说话人分离;`--target-uid` 可顺带把该说话人绑定到真实 userId
|
||||
- `hot-word add` 作用于**个人**热词库,只对本人后续的听记识别生效
|
||||
- `mind-graph create` 是异步:调用后立即返回,需要用 `mind-graph status --id <taskUuid>` 轮询到 ready 才能拿到图
|
||||
- 上传流程严格三步:`upload create`(拿 sessionId + 预签名 URL)→ 客户端自己 HTTP PUT 文件 → `upload complete`(触发转写);中途放弃必须用 `upload cancel` 释放会话,不要只丢 sessionId
|
||||
|
||||
## 自动化脚本
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# OA 审批 (oa) 命令参考
|
||||
|
||||
钉钉 OA 审批:待办审批列表 / 查看详情 / 同意 / 拒绝 / 撤销 / 流程记录 / 表单模板。
|
||||
|
||||
## 命令总览
|
||||
|
||||
### 查询待我处理的审批实例
|
||||
```
|
||||
Usage:
|
||||
dws oa approval list-pending [flags]
|
||||
Example:
|
||||
dws oa approval list-pending
|
||||
dws oa approval list-pending --page 1 --size 20
|
||||
dws oa approval list-pending --start "2026-03-01T00:00:00+08:00" --end "2026-03-31T23:59:59+08:00"
|
||||
Flags:
|
||||
--page string 页码 (默认 1)
|
||||
--size string 每页数量 (默认 10)
|
||||
--start string 提交开始时间 ISO-8601 (可选)
|
||||
--end string 提交结束时间 ISO-8601 (可选)
|
||||
```
|
||||
|
||||
"待办收件箱"视图:列出当前用户仍需动作的审批 `processInstanceId`。注意返回的是**实例 ID**,真正驱动 approve/reject 还需要用 `tasks` 再换成 `taskId`。
|
||||
|
||||
### 查询我发起的审批实例
|
||||
```
|
||||
Usage:
|
||||
dws oa approval list-initiated [flags]
|
||||
Example:
|
||||
dws oa approval list-initiated
|
||||
dws oa approval list-initiated --process-code <processCode> --max-results 20
|
||||
dws oa approval list-initiated --start "2026-03-01T00:00:00+08:00" --end "2026-03-31T23:59:59+08:00" --next-token <nextToken>
|
||||
Flags:
|
||||
--process-code string 按指定表单/流程模板过滤 (可选)
|
||||
--start string 发起开始时间 ISO-8601 (可选)
|
||||
--end string 发起结束时间 ISO-8601 (可选)
|
||||
--max-results string 每页条数 (可选)
|
||||
--next-token string 分页 token (首页留空)
|
||||
```
|
||||
|
||||
查询当前用户作为申请人发起的审批,用来查"我报销到哪一步了"、"我之前提的请假单"。
|
||||
|
||||
### 查询可发起的审批表单模板
|
||||
```
|
||||
Usage:
|
||||
dws oa approval list-forms [flags]
|
||||
Example:
|
||||
dws oa approval list-forms
|
||||
dws oa approval list-forms --cursor 0 --size 20
|
||||
Flags:
|
||||
--cursor string 分页游标,首次传 0 (默认 0)
|
||||
--size string 每页大小 (可选)
|
||||
```
|
||||
|
||||
列出当前用户被授权发起的审批表单(如"请假"、"报销"),返回 `processCode`,后续用于提交新的申请或按模板筛选历史实例。
|
||||
|
||||
### 获取审批实例详情
|
||||
```
|
||||
Usage:
|
||||
dws oa approval detail [flags]
|
||||
Example:
|
||||
dws oa approval detail --instance-id <processInstanceId>
|
||||
Flags:
|
||||
--instance-id string 审批实例 processInstanceId (必填)
|
||||
```
|
||||
|
||||
拉取单个实例的完整内容:表单字段、附件、当前状态、参与人。适合让 AI 读懂审批单再决定动作或生成摘要。
|
||||
|
||||
### 查询审批实例的操作记录
|
||||
```
|
||||
Usage:
|
||||
dws oa approval records [flags]
|
||||
Example:
|
||||
dws oa approval records --instance-id <processInstanceId>
|
||||
Flags:
|
||||
--instance-id string 审批实例 processInstanceId (必填)
|
||||
```
|
||||
|
||||
列出实例的流转历史(谁在什么时候同意/加签/转交/评论),用于复盘或解释"这单卡在哪"。
|
||||
|
||||
### 查询实例下待处理的任务
|
||||
```
|
||||
Usage:
|
||||
dws oa approval tasks [flags]
|
||||
Example:
|
||||
dws oa approval tasks --instance-id <processInstanceId>
|
||||
Flags:
|
||||
--instance-id string 审批实例 processInstanceId (必填)
|
||||
```
|
||||
|
||||
针对具体实例拿到当前用户名下的 `taskId`。approve / reject 要的是 `taskId`,不是 `processInstanceId`,这一步不能省。
|
||||
|
||||
### 同意审批
|
||||
```
|
||||
Usage:
|
||||
dws oa approval approve [flags]
|
||||
Example:
|
||||
dws oa approval approve --instance-id <processInstanceId> --task-id <taskId>
|
||||
dws oa approval approve --instance-id <processInstanceId> --task-id <taskId> --remark "同意,按流程推进"
|
||||
Flags:
|
||||
--instance-id string 审批实例 processInstanceId (必填)
|
||||
--task-id string 待办任务 taskId (必填),来自 approval tasks
|
||||
--remark string 审批意见 (可选)
|
||||
```
|
||||
|
||||
以当前用户身份同意某个审批任务。`--task-id` 必须是当前用户的待办 taskId,否则 MCP 会拒绝。
|
||||
|
||||
### 拒绝审批
|
||||
```
|
||||
Usage:
|
||||
dws oa approval reject [flags]
|
||||
Example:
|
||||
dws oa approval reject --instance-id <processInstanceId> --task-id <taskId> --remark "预算不足,下月再议"
|
||||
Flags:
|
||||
--instance-id string 审批实例 processInstanceId (必填)
|
||||
--task-id string 待办任务 taskId (必填),来自 approval tasks
|
||||
--remark string 拒绝理由 (可选,但强烈建议填)
|
||||
```
|
||||
|
||||
以当前用户身份驳回审批任务;拒绝后一般由 MCP 端决定流程是打回申请人还是整体终止。
|
||||
|
||||
### 撤回已发起的审批
|
||||
```
|
||||
Usage:
|
||||
dws oa approval revoke [flags]
|
||||
Example:
|
||||
dws oa approval revoke --instance-id <processInstanceId>
|
||||
dws oa approval revoke --instance-id <processInstanceId> --remark "金额填错,重新提交" --yes
|
||||
Flags:
|
||||
--instance-id string 审批实例 processInstanceId (必填)
|
||||
--remark string 撤回原因 (可选)
|
||||
```
|
||||
|
||||
只能撤销**自己发起且尚未终态**的实例。命令标了 `isSensitive`,AI 调用前建议与用户再确认一次,可配合全局 `--yes` 跳过确认。
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户说"我要审的单/待我处理的审批/审批收件箱" → `list-pending` 拿 `processInstanceId`
|
||||
用户说"这单我同意/批了它/通过" → `approval tasks` 换 `taskId` → `approval approve`
|
||||
用户说"这单不行/驳回/拒绝" → `approval tasks` 换 `taskId` → `approval reject --remark "<理由>"`
|
||||
用户说"这单详情/看看内容/这单在说什么" → `approval detail`
|
||||
用户说"这单卡在谁那/谁还没审/流程走到哪" → `approval records`
|
||||
用户说"我发起的审批/我的申请" → `list-initiated`(可用 `--process-code` 按模板筛)
|
||||
用户说"能发起哪些审批/有哪些表单" → `list-forms`
|
||||
用户说"撤回我的申请/我不提了" → `approval revoke`(仅限自己发起且未终态)
|
||||
|
||||
关键区分: oa(钉钉 OA 审批流程) vs todo(个人待办) vs report(日志汇报)
|
||||
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
# 1. 看我有哪些待审的单 — 提取 processInstanceId
|
||||
dws oa approval list-pending --page 1 --size 20 --format json
|
||||
|
||||
# 2. 看单子内容(表单字段 / 附件 / 当前状态)
|
||||
dws oa approval detail --instance-id <processInstanceId> --format json
|
||||
|
||||
# 3. 想动作(同意/拒绝)前,先拿 taskId
|
||||
dws oa approval tasks --instance-id <processInstanceId> --format json
|
||||
|
||||
# 4. 同意 / 拒绝(taskId 来自步骤 3)
|
||||
dws oa approval approve --instance-id <processInstanceId> --task-id <taskId> --remark "同意" --format json
|
||||
dws oa approval reject --instance-id <processInstanceId> --task-id <taskId> --remark "预算不足" --format json
|
||||
|
||||
# 5. 查看流程记录 — 谁何时做了什么
|
||||
dws oa approval records --instance-id <processInstanceId> --format json
|
||||
|
||||
# 6. 查我发起的审批 / 按模板过滤
|
||||
dws oa approval list-initiated --format json
|
||||
dws oa approval list-initiated --process-code <processCode> --format json
|
||||
|
||||
# 7. 能发起哪些表单
|
||||
dws oa approval list-forms --cursor 0 --size 20 --format json
|
||||
|
||||
# 8. 撤回我发起的单
|
||||
dws oa approval revoke --instance-id <processInstanceId> --remark "填错了" --yes --format json
|
||||
```
|
||||
|
||||
## 上下文传递表
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `list-pending` | `processInstanceId`、`nextToken` | detail / tasks / records 的 --instance-id |
|
||||
| `list-initiated` | `processInstanceId`、`nextToken` | detail / records / revoke 的 --instance-id |
|
||||
| `list-forms` | `processCode` | `list-initiated --process-code` 按模板过滤,或后续发起新审批 |
|
||||
| `approval tasks` | `taskId` | approve / reject 的 --task-id |
|
||||
| `approval detail` | 表单字段、参与人、当前状态 | 供 AI 决策是否批准、生成摘要 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `processInstanceId`(实例 ID)与 `taskId`(待办任务 ID)**不是一回事**,不能混用:
|
||||
- `list-pending` / `list-initiated` / `detail` / `records` / `revoke` 用 `processInstanceId`
|
||||
- `approve` / `reject` 用 `taskId`,必须先 `approval tasks --instance-id <processInstanceId>` 换一次
|
||||
- `list-pending` 是"需要我动作"视图;同一个实例如果流转到下一节点就会从这里消失
|
||||
- `list-initiated` 是"我发起的"视图;和 `list-pending` 是两套语义,不要互相替代
|
||||
- `--start` / `--end` 使用 ISO-8601 格式,内部会自动转毫秒时间戳
|
||||
- `revoke` 只能撤销**当前用户发起且未完结**的实例;已流转结束、已作废、或不是自己发起的都会失败
|
||||
- `approve` / `reject` 的 `--remark` 可选但强烈建议填,尤其是 `reject`——审批流下游会把理由展示给申请人
|
||||
- 所有敏感动作(`approve` / `reject` / `revoke`)在 AI 调用时应与用户二次确认,可用全局 `--yes` 跳过确认
|
||||
- `list-forms` 返回的 `processCode` 是未来发起新审批、或筛选历史实例的钩子;目前本命令集只覆盖审批的"处理侧",发起新审批需走业务方自己的流程
|
||||
@@ -84,6 +84,8 @@ Flags:
|
||||
--report-id string 日志 ID (必填)
|
||||
```
|
||||
|
||||
返回该日志的互动聚合数据:浏览数 (views)、点赞数 (likes)、评论数 (comments) 等,用于衡量一篇日志的阅读与反馈情况。
|
||||
|
||||
### 查询当前人创建的日志列表
|
||||
```
|
||||
Usage:
|
||||
@@ -106,7 +108,7 @@ Flags:
|
||||
|
||||
用户说"查日志/看日报" → `list` 获取列表,再 `detail`
|
||||
用户说"写日报/提交周报/发日志/填日志" → 先 `template list` / `template detail` 取 `templateId` 与各控件 `key`/`sort`/类型,拼 `--contents` JSON,再 `create`
|
||||
用户说"日志统计/已读统计" → `stats`
|
||||
用户说"日志数据/互动情况/多少人看了/点赞评论" → `stats`
|
||||
用户说"有什么日志模版" → `template list` 或 `template detail`
|
||||
用户说"我发过的日志/我创建的日志" → `sent`
|
||||
|
||||
@@ -133,7 +135,7 @@ dws report list --start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+0
|
||||
# 4. 查看日志详情
|
||||
dws report detail --report-id <reportId> --format json
|
||||
|
||||
# 5. 查看日志统计(已读/未读)
|
||||
# 5. 查看日志统计(浏览/点赞/评论聚合)
|
||||
dws report stats --report-id <reportId> --format json
|
||||
|
||||
# 6. 查看当前人创建的日志列表
|
||||
|
||||
@@ -9,11 +9,15 @@ Usage:
|
||||
Example:
|
||||
dws todo task create --title "修复线上Bug" --executors <USER_ID_1>,<USER_ID_2> --priority 40
|
||||
dws todo task create --title "提交报告" --executors <USER_ID> --due "2026-03-20T10:00:00+08:00"
|
||||
dws todo task create --title "每日站会" --executors <USER_ID> \
|
||||
--due "2026-03-20T10:00:00+08:00" \
|
||||
--recurrence $'DTSTART:20260320T020000Z\nRRULE:FREQ=DAILY;INTERVAL=1'
|
||||
Flags:
|
||||
--due string 截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)
|
||||
--executors string 执行者 userId 列表 (必填)
|
||||
--priority string 优先级: 10低/20普通/30较高/40紧急
|
||||
--title string 待办标题 (必填)
|
||||
--due string 截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)
|
||||
--executors string 执行者 userId 列表 (必填)
|
||||
--priority string 优先级: 10低/20普通/30较高/40紧急
|
||||
--recurrence string 循环规则 RFC 5545 (DTSTART+RRULE,仅在同时设置 --due 时生效)
|
||||
--title string 待办标题 (必填)
|
||||
```
|
||||
|
||||
### 查询待办列表
|
||||
@@ -122,6 +126,7 @@ dws todo task delete --task-id <taskId> --yes --format json
|
||||
|
||||
- 优先级值: 10=低, 20=普通, 30=较高, 40=紧急
|
||||
- `--due` 截止时间使用 ISO-8601 格式(如 2026-03-10T18:00:00+08:00)
|
||||
- `--recurrence` 为 RFC 5545 循环规则(`DTSTART:...\nRRULE:FREQ=DAILY;INTERVAL=1` 这种格式),必须与 `--due` 同时设置才生效
|
||||
- `task list` 的 `--status` 对应 MCP `get_user_todos_in_current_org` 的 `todoStatus` 参数
|
||||
- todo 是个人待办管理产品
|
||||
- `task update` 可同时修改标题/优先级/截止时间/完成状态
|
||||
|
||||
Reference in New Issue
Block a user