Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
818b8b29e3 | ||
|
|
109ad13844 | ||
|
|
91dfc8b926 | ||
|
|
b794d802f2 | ||
|
|
e6c1dfe15c | ||
|
|
32d32cd827 | ||
|
|
a65d6f23ec | ||
|
|
a838ae75a7 |
@@ -0,0 +1,71 @@
|
||||
name: Publish npm release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release tag to publish to npm (e.g. v1.0.48)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish-npm:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download GitHub release assets
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
mkdir -p dist
|
||||
gh release download "${{ inputs.version }}" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--dir dist \
|
||||
--pattern 'dws-*' \
|
||||
--pattern 'checksums.txt' \
|
||||
--clobber
|
||||
ls -la dist
|
||||
|
||||
- name: Stage npm package
|
||||
run: |
|
||||
set -eu
|
||||
version="${{ inputs.version }}"
|
||||
semver="${version#v}"
|
||||
pkg_root="dist/npm/dingtalk-workspace-cli"
|
||||
rm -rf "$pkg_root"
|
||||
mkdir -p "$pkg_root/assets" "$pkg_root/bin"
|
||||
cp build/npm/install.js "$pkg_root/install.js"
|
||||
cp build/npm/bin/dws.js "$pkg_root/bin/dws.js"
|
||||
cp build/npm/README.md "$pkg_root/README.md"
|
||||
sed "s|__VERSION__|${semver}|g" build/npm/package.json.tmpl > "$pkg_root/package.json"
|
||||
cp dist/dws-* "$pkg_root/assets/"
|
||||
cp dist/checksums.txt "$pkg_root/assets/"
|
||||
test -f "$pkg_root/assets/dws-skills.zip"
|
||||
cat "$pkg_root/package.json"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Publish stable to npm
|
||||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(inputs.version, '-') }}
|
||||
working-directory: dist/npm/dingtalk-workspace-cli
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish prerelease to npm beta
|
||||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(inputs.version, '-') }}
|
||||
working-directory: dist/npm/dingtalk-workspace-cli
|
||||
run: npm publish --access public --tag beta
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -5,12 +5,18 @@ on:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
repair_npm_version:
|
||||
description: "Only publish an existing release to npm, e.g. v1.0.48"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }}
|
||||
runs-on: ubuntu-latest
|
||||
# 60 (not 30): mirroring every release asset to Gitee is slow; 30 min cut the
|
||||
# Gitee step off mid-upload on the v1.0.42 release. The Gitee step is now also
|
||||
@@ -76,17 +82,6 @@ jobs:
|
||||
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
|
||||
OSS_PREFIX: ${{ secrets.OSS_PREFIX }}
|
||||
|
||||
- name: Mirror release to Gitee (China)
|
||||
# 把 release 附件(二进制/校验和/skills 包)镜像到 Gitee release,供 install.sh
|
||||
# 的 DWS_GITEE_REPO 开关消费(仓库代码由 Gitee 仓库镜像功能自动同步,附件不在其内)。
|
||||
# 脚本自带门控:未配置 GITEE_TOKEN / GITEE_REPO 时优雅跳过,不影响海外发布。
|
||||
run: ./scripts/release/sync-to-gitee.sh
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
|
||||
GITEE_USER: ${{ secrets.GITEE_USER }}
|
||||
GITEE_REPO: ${{ secrets.GITEE_REPO }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -94,7 +89,8 @@ jobs:
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Publish stable to npm
|
||||
# 只有官方仓库发 npm;fork(dev 预览)没有 NPM_TOKEN,跳过以免红叉
|
||||
# 只有官方仓库发 npm;fork(dev 预览)没有 NPM_TOKEN,跳过以免红叉。
|
||||
# 必须在 Gitee mirror 前发布:Gitee 附件上传偶发长时间挂住,不能阻塞 npm/latest。
|
||||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(github.ref_name, '-') }}
|
||||
working-directory: dist/npm/dingtalk-workspace-cli
|
||||
run: npm publish --access public
|
||||
@@ -108,3 +104,76 @@ jobs:
|
||||
run: npm publish --access public --tag beta
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Mirror release to Gitee (China)
|
||||
# 把 release 附件(二进制/校验和/skills 包)镜像到 Gitee release,供 install.sh
|
||||
# 的 DWS_GITEE_REPO 开关消费(仓库代码由 Gitee 仓库镜像功能自动同步,附件不在其内)。
|
||||
# 默认关闭:国内 release 应由 Gitee 侧本地构建发布,避免 GitHub -> Gitee 跨境传大包卡住。
|
||||
# 仅在需要临时补救时设置 repo variable ENABLE_GITEE_UPLOAD_FALLBACK=true。
|
||||
if: ${{ vars.ENABLE_GITEE_UPLOAD_FALLBACK == 'true' }}
|
||||
timeout-minutes: 20
|
||||
run: ./scripts/release/sync-to-gitee.sh
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
|
||||
GITEE_USER: ${{ secrets.GITEE_USER }}
|
||||
GITEE_REPO: ${{ secrets.GITEE_REPO }}
|
||||
|
||||
repair-npm:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.repair_npm_version != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download GitHub release assets
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
mkdir -p dist
|
||||
gh release download "${{ inputs.repair_npm_version }}" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--dir dist \
|
||||
--pattern 'dws-*' \
|
||||
--pattern 'checksums.txt' \
|
||||
--clobber
|
||||
ls -la dist
|
||||
|
||||
- name: Stage npm package
|
||||
run: |
|
||||
set -eu
|
||||
version="${{ inputs.repair_npm_version }}"
|
||||
semver="${version#v}"
|
||||
pkg_root="dist/npm/dingtalk-workspace-cli"
|
||||
rm -rf "$pkg_root"
|
||||
mkdir -p "$pkg_root/assets" "$pkg_root/bin"
|
||||
cp build/npm/install.js "$pkg_root/install.js"
|
||||
cp build/npm/bin/dws.js "$pkg_root/bin/dws.js"
|
||||
cp build/npm/README.md "$pkg_root/README.md"
|
||||
sed "s|__VERSION__|${semver}|g" build/npm/package.json.tmpl > "$pkg_root/package.json"
|
||||
cp dist/dws-* "$pkg_root/assets/"
|
||||
cp dist/checksums.txt "$pkg_root/assets/"
|
||||
test -f "$pkg_root/assets/dws-skills.zip"
|
||||
cat "$pkg_root/package.json"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Publish stable to npm
|
||||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(inputs.repair_npm_version, '-') }}
|
||||
working-directory: dist/npm/dingtalk-workspace-cli
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish prerelease to npm beta
|
||||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(inputs.repair_npm_version, '-') }}
|
||||
working-directory: dist/npm/dingtalk-workspace-cli
|
||||
run: npm publish --access public --tag beta
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Gitee Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release tag to build on Gitee, e.g. v1.0.48"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install packaging tools
|
||||
run: |
|
||||
set -eu
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y zip unzip curl
|
||||
|
||||
- name: Install rcodesign
|
||||
run: |
|
||||
set -eu
|
||||
RCS_VERSION="0.27.0"
|
||||
curl -fsSL -o /tmp/rcodesign.tar.gz \
|
||||
"https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F${RCS_VERSION}/apple-codesign-${RCS_VERSION}-x86_64-unknown-linux-musl.tar.gz"
|
||||
mkdir -p /tmp/rcodesign
|
||||
tar -xzf /tmp/rcodesign.tar.gz -C /tmp/rcodesign --strip-components=1
|
||||
sudo install -m 0755 /tmp/rcodesign/rcodesign /usr/local/bin/rcodesign
|
||||
rcodesign --version
|
||||
|
||||
- name: Build and publish Gitee release
|
||||
env:
|
||||
VERSION: ${{ inputs.version || github.ref_name }}
|
||||
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
|
||||
GITEE_REPO: DingTalk-Real-AI/dingtalk-workspace-cli
|
||||
run: ./scripts/release/build-and-publish-gitee.sh
|
||||
@@ -6,6 +6,36 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.50] - 2026-07-08
|
||||
|
||||
This release fixes a long-standing gap where the global `--jq` / `--fields` output filters were silently ignored on product commands, lands a JSON-mode output path for the sheet batch-style command, and aligns the bundled skill surface with the real command semantics uncovered by the round-2 real-machine QA sweep.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Global `--jq` / `--fields` are honored on product commands** (#575) — `Formatter.PrintJSON` / `PrintJSONUnescaped` now route through `output.WriteFiltered` when either flag is set, so product commands accept the same filters that `dws api` has always supported. The tool-caller adapter exposes `Fields()` / `JQ()` so helpers can read the flags without re-parsing.
|
||||
- **`skill setup --dry-run` is a no-op preview** (#575) — it now prints what would be written without touching the skill directory, the registry, or the agent config. Help text and docs are updated to match.
|
||||
- **Skill docs alignment to the real command surface** (#575) — per-product references and the cross-product intent guide clarify that `--fields` projects top-level / list keys only (use `--jq` for nested paths); `minutes_extract_todos.py`, `calendar_free_slot_finder.py`, `chat_export_messages.py` / `chat_history_with_user.py`, and `contact_dept_members.py` are rewritten against the current response shapes; `aisearch` / `aitable` / `attendance` / `calendar` / `chat` / `contact` / `dev` / `doc` / `doc-comment` / `doc-file-ops` / `doc-list` / `doc-search` / `drive` / `mail` / `minutes` / `oa` / `sheet` / `sheet-export` / `url-patterns` / `best_practices/lite-recipes.md` / `global-reference.md` / `intent-guide.md` are re-synced; the QA voice ("真机" phrasing) and environment-specific quirks stated as absolute rules are removed from the docs.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`sheet range batch-set-style` emits per-row JSON in JSON mode** (#575) — when `--format json` is set, each update is reported as `{index, sheetId, range, ok, error}` instead of only the final aggregate, so callers can programmatically track partial failures under `--continue-on-error`.
|
||||
- **Command-merge helpers exported** — `pkg/cmdutil.LeafMerge*` and the provenance helpers are now public so downstream command trees can reuse the same merge semantics.
|
||||
|
||||
## [1.0.49] - 2026-07-08
|
||||
|
||||
This release lands a full real-machine QA sweep across the CLI, helper scripts, and skill docs (#572), and hardens the release pipeline so npm publishing can no longer be blocked by Gitee mirror issues (#570).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Real-machine QA fixes across CLI commands** (#572) — `aitable chart/dashboard share update --enabled` now takes a string so `--enabled false` disables; `chat conversation-info --user` resolves openDingTalkId and registers `--id/--conversation-id/--chat` aliases; `chat list-all-conversations --limit` is capped at 100 and rejects larger values; custom-robot webhook failures surface `errcode` instead of masquerading as success; `contact` registers `--dept/--depts` as the primary flags so the documented spelling actually works; `sheet media-upload` and `sheet export` emit clean JSON under `--format json` (progress lines no longer leak); `wiki node create --type` enum is corrected (drops unsupported `asheet`, adds `axls/able/appt/adraw/amind`); `ding message list --type` defaults to `ALL` since the server rejects empty type.
|
||||
- **Helper script fixes (mono and multi)** (#572) — aitable import/export flag names and the tableId regex (7-char default tables were rejected); mail search `--limit`, contact dept response keys (`deptList`/`deptUserList`) and `userInfo` nesting; `attendance_my_record` whoami compatibility; `calendar_schedule_meeting` event-id unwrapping; `drive_tree_list` recursion via `fileId`; report scripts migrated off the deprecated `report list`/`report detail`.
|
||||
- **Skill docs sync (mono and multi)** (#572) — command indexes, flag names, enums, return-structure keys and cross-product intent routing are re-aligned to real-machine behavior across all products. Genuinely server-side limitations (permission gates, org-level restrictions, unregistered tool keys) are annotated instead of code-patched, and the cross-cutting hazards (`success` always true, `--jq`/`--fields` currently no-op) are documented.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Release pipeline unblocks npm publish from Gitee mirror** (#570) — the Release workflow now publishes to npm before touching the Gitee mirror, so Gitee upload issues cannot block `npm/latest`. GitHub→Gitee attachment upload is disabled by default (unreliable from US runners) and only runs when `ENABLE_GITEE_UPLOAD_FALLBACK=true`; the legacy upload fallback path is guarded with timeout and retry so it fails fast when re-enabled.
|
||||
- **Repair modes for release republish** (#570) — the Release workflow gains a repair input and a standalone npm-only repair workflow, used to republish an existing release to npm without re-running the full pipeline.
|
||||
|
||||
## [1.0.48] - 2026-07-07
|
||||
|
||||
This release promotes the sealed **remove-discovery delivery** from the beta line to the stable `v1.0.48` package. It removes dynamic service discovery from the open-edition runtime, keeps legacy CLI compatibility aliases, syncs the open command/help/skill surface with the dws-wukong baseline, and includes the `dev connect` default-yolo behavior on the stable upgrade track.
|
||||
|
||||
@@ -824,6 +824,10 @@ func (f *authLoginRecommendSequenceCaller) Format() string { return "table" }
|
||||
|
||||
func (f *authLoginRecommendSequenceCaller) DryRun() bool { return false }
|
||||
|
||||
func (f *authLoginRecommendSequenceCaller) Fields() string { return "" }
|
||||
|
||||
func (f *authLoginRecommendSequenceCaller) JQ() string { return "" }
|
||||
|
||||
func stringSliceArgEqual(got any, want []string) bool {
|
||||
if got == nil {
|
||||
return len(want) == 0
|
||||
|
||||
@@ -128,6 +128,19 @@ func runSkillSetup(cmd *cobra.Command, _ []string) error {
|
||||
multiSkillNames = ensureMandatorySharedSkill(filtered, allMultiSkillNames)
|
||||
}
|
||||
|
||||
// --dry-run:仅预览将安装的内容与目标目录,不写入任何文件、不弹确认。
|
||||
if dryRun, _ := cmd.Flags().GetBool("dry-run"); dryRun {
|
||||
fmt.Fprintf(out, "[DRY-RUN] 预览(不写入任何文件):mode=%s,来源 %s\n", mode, skillSrc)
|
||||
fmt.Fprintln(out, "将安装到:")
|
||||
for _, d := range dests {
|
||||
fmt.Fprintf(out, " - %s\n", d)
|
||||
}
|
||||
if mode == skillSetupModeMulti && len(multiSkillNames) > 0 {
|
||||
fmt.Fprintf(out, "子 skill:%s\n", strings.Join(multiSkillNames, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !autoYes {
|
||||
ok, err := confirmSkillSetup(out, mode, skillSrc, dests, multiSkillNames)
|
||||
if err != nil {
|
||||
|
||||
@@ -54,6 +54,20 @@ func (a *toolCallerAdapter) DryRun() bool {
|
||||
return a.flags != nil && a.flags.DryRun
|
||||
}
|
||||
|
||||
func (a *toolCallerAdapter) Fields() string {
|
||||
if a.flags != nil {
|
||||
return a.flags.Fields
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *toolCallerAdapter) JQ() string {
|
||||
if a.flags != nil {
|
||||
return a.flags.JQ
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func convertResult(r executor.Result) *edition.ToolResult {
|
||||
resp := r.Response
|
||||
if resp == nil {
|
||||
|
||||
+74
-12
@@ -5,12 +5,26 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// parseBoolFlag reads a string flag and parses it as a boolean, accepting the
|
||||
// natural `--flag true` / `--flag false` space syntax. Registering these
|
||||
// switches as String (not Bool) flags avoids cobra swallowing `false` as a
|
||||
// positional arg — which silently inverted `--enabled false` into enable.
|
||||
func parseBoolFlag(cmd *cobra.Command, name string) (bool, error) {
|
||||
raw := strings.TrimSpace(mustGetFlag(cmd, name))
|
||||
v, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("--%s 只接受 true 或 false,got %q", name, raw)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// dws aitable — AI 表格
|
||||
// 中文 tool name 映射: CLI 英文命令 → MCP 中文 tool name
|
||||
@@ -617,6 +631,30 @@ func printViewSubBlock(block any) error {
|
||||
return deps.Out.PrintJSON(envelope)
|
||||
}
|
||||
|
||||
// findFormViewByID 从 list_form_views 响应里递归定位 viewId 匹配的表单对象。
|
||||
// 服务端的 viewIds 过滤参数当前不生效(返回全表所有表单),故 form get 在客户端
|
||||
// 侧按 viewId 精确筛出单条,避免退化成等价 form list。
|
||||
func findFormViewByID(node any, viewID string) (map[string]any, bool) {
|
||||
switch v := node.(type) {
|
||||
case map[string]any:
|
||||
if id, _ := v["viewId"].(string); id == viewID {
|
||||
return v, true
|
||||
}
|
||||
for _, val := range v {
|
||||
if found, ok := findFormViewByID(val, viewID); ok {
|
||||
return found, true
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, el := range v {
|
||||
if found, ok := findFormViewByID(el, viewID); ok {
|
||||
return found, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// collectStringFlag 把 cmd 上 flagName 的字符串值(若非空)以 jsonKey 写入 out map。
|
||||
func collectStringFlag(cmd *cobra.Command, flagName, jsonKey string, out map[string]any) {
|
||||
if v, _ := cmd.Flags().GetString(flagName); v != "" {
|
||||
@@ -2888,7 +2926,8 @@ locked 为 true 表示视图已锁定,false 表示未锁定。`,
|
||||
Use: "get",
|
||||
Short: "获取单个表单视图详情",
|
||||
Long: `按 viewId 获取单个表单视图的元信息(name/title/createdAt/shareFormUuid)。
|
||||
内部基于 list_form_views 过滤实现,等价于 form list 后筛选指定 viewId。`,
|
||||
内部拉取 list_form_views 后在客户端按 viewId 精确筛出单条(服务端的 viewIds
|
||||
过滤参数当前不生效,会返回全表所有表单,故在此侧过滤)。data 即命中的表单对象。`,
|
||||
Example: ` dws aitable form get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateRequiredFlags(cmd, "table-id", "view-id"); err != nil {
|
||||
@@ -2898,11 +2937,30 @@ locked 为 true 表示视图已锁定,false 表示未锁定。`,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return callAitableHelperTool("list_form_views", map[string]any{
|
||||
viewID := mustGetFlag(cmd, "view-id")
|
||||
raw, err := callMCPToolReturnTextOnServer(context.Background(), "aitable-helper", "list_form_views", map[string]any{
|
||||
"baseId": baseID,
|
||||
"tableId": mustGetFlag(cmd, "table-id"),
|
||||
"viewIds": []string{mustGetFlag(cmd, "view-id")},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var parsed any
|
||||
if e := json.Unmarshal([]byte(raw), &parsed); e != nil {
|
||||
return &CLIError{
|
||||
Code: CodeMCPToolError,
|
||||
Message: fmt.Sprintf("list_form_views response is not valid JSON: %v", e),
|
||||
}
|
||||
}
|
||||
form, ok := findFormViewByID(parsed, viewID)
|
||||
if !ok {
|
||||
return &CLIError{
|
||||
Code: CodeMCPToolError,
|
||||
Message: fmt.Sprintf("form view %s not found in table", viewID),
|
||||
Suggestion: "用 dws aitable form list --base-id <baseId> --table-id <tableId> 查看可用 viewId 列表",
|
||||
}
|
||||
}
|
||||
return printViewSubBlock(form)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3439,10 +3497,10 @@ layout 数组里每项含图表的新位置(row/col/width/height)。`,
|
||||
Example: ` dws aitable dashboard share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --enabled true --share-type PUBLIC
|
||||
dws aitable dashboard share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --enabled false`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateRequiredFlags(cmd, "dashboard-id"); err != nil {
|
||||
if err := validateRequiredFlags(cmd, "dashboard-id", "enabled"); err != nil {
|
||||
return err
|
||||
}
|
||||
enabled, err := cmd.Flags().GetBool("enabled")
|
||||
enabled, err := parseBoolFlag(cmd, "enabled")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -3542,11 +3600,15 @@ layout 数组里每项含图表的新位置(row/col/width/height)。`,
|
||||
Use: "update",
|
||||
Short: "更新图表",
|
||||
Long: `更新指定 chart 的配置或布局。
|
||||
调用前建议先调用 chart widgets-example 了解 --config 入参结构和要求。`,
|
||||
--config 为必填参数,即使只想改布局也要带上完整的图表配置(服务端会拒绝缺 config 的请求,
|
||||
空对象 {} 同样被拒,至少要包含 chartName)。--layout 可选,仅调整位置/大小时追加。
|
||||
调用前建议先调用 chart widgets-example 了解 --config 入参结构和要求,
|
||||
或先用 chart get 拿到当前 config 再改。`,
|
||||
Example: ` dws aitable chart update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID \
|
||||
--config '{"chartName":"新柱图名",...}'
|
||||
# 只改布局也必须带 --config(用 chart get 拿到当前 config 原样回传):
|
||||
dws aitable chart update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID \
|
||||
--layout '{"x":0,"y":4,"w":12,"h":4}'
|
||||
--config '{"chartName":"柱图",...}' --layout '{"x":0,"y":4,"w":12,"h":4}'
|
||||
# 查询 chartId: dws aitable dashboard get --base-id <baseId> --dashboard-id <dashboardId>`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateRequiredFlags(cmd, "dashboard-id", "chart-id", "config"); err != nil {
|
||||
@@ -3634,10 +3696,10 @@ layout 数组里每项含图表的新位置(row/col/width/height)。`,
|
||||
Example: ` dws aitable chart share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID --enabled true --share-type ORG
|
||||
dws aitable chart share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID --enabled false`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateRequiredFlags(cmd, "dashboard-id", "chart-id"); err != nil {
|
||||
if err := validateRequiredFlags(cmd, "dashboard-id", "chart-id", "enabled"); err != nil {
|
||||
return err
|
||||
}
|
||||
enabled, err := cmd.Flags().GetBool("enabled")
|
||||
enabled, err := parseBoolFlag(cmd, "enabled")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -4228,7 +4290,7 @@ parentSectionId 为空串表示该节点在 Base 根目录下。
|
||||
baseGetPrimaryDocIdCmd.Flags().String("table-id", "", "Table ID,可通过 list_tables 或 get_base 获取 (必填)")
|
||||
baseGetPrimaryDocIdCmd.Flags().String("record-id", "", "记录 ID (必填)")
|
||||
baseCopyCmd.Flags().String("base-id", "", "源 Base ID (必填)")
|
||||
baseCopyCmd.Flags().String("target-folder-id", "", "目标文件夹 ID (必填), 默认为当前 Base 的父节点")
|
||||
baseCopyCmd.Flags().String("target-folder-id", "", "目标文件夹 ID (必填, 不传会复制失败)")
|
||||
baseCopyCmd.Flags().Bool("only-struct", false, "是否仅复制结构(不含数据),默认 false 表示完整复制")
|
||||
baseCmd.AddCommand(
|
||||
baseListCmd, baseSearchCmd, baseGetCmd,
|
||||
@@ -4658,7 +4720,7 @@ parentSectionId 为空串表示该节点在 Base 根目录下。
|
||||
dashboardShareGetCmd.Flags().String("dashboard-id", "", "目标 Dashboard ID (必填)")
|
||||
dashboardShareUpdateCmd.Flags().String("base-id", "", "所属 Base ID (必填)")
|
||||
dashboardShareUpdateCmd.Flags().String("dashboard-id", "", "目标 Dashboard ID (必填)")
|
||||
dashboardShareUpdateCmd.Flags().Bool("enabled", false, "分享开关:true 开启,false 关闭")
|
||||
dashboardShareUpdateCmd.Flags().String("enabled", "", "分享开关:true 开启,false 关闭 (必填)")
|
||||
dashboardShareUpdateCmd.Flags().String("share-type", "", "分享类型:PUBLIC 或 ORG(enabled=true 时生效)")
|
||||
dashboardShareUpdateCmd.Flags().Bool("allow-back-to-doc", false, "是否允许从分享页返回源 AI 表格(仅在显式传参时生效)")
|
||||
dashboardShareCmd.AddCommand(dashboardShareGetCmd, dashboardShareUpdateCmd)
|
||||
@@ -4692,7 +4754,7 @@ parentSectionId 为空串表示该节点在 Base 根目录下。
|
||||
chartShareUpdateCmd.Flags().String("base-id", "", "所属 Base ID (必填)")
|
||||
chartShareUpdateCmd.Flags().String("dashboard-id", "", "所属 Dashboard ID (必填)")
|
||||
chartShareUpdateCmd.Flags().String("chart-id", "", "目标 Chart ID (必填)")
|
||||
chartShareUpdateCmd.Flags().Bool("enabled", false, "分享开关:true 开启,false 关闭")
|
||||
chartShareUpdateCmd.Flags().String("enabled", "", "分享开关:true 开启,false 关闭 (必填)")
|
||||
chartShareUpdateCmd.Flags().String("share-type", "", "分享类型:PUBLIC 或 ORG(enabled=true 时生效)")
|
||||
chartShareUpdateCmd.Flags().Bool("allow-back-to-doc", false, "是否允许从分享页返回源 AI 表格(仅在显式传参时生效)")
|
||||
chartShareCmd.AddCommand(chartShareGetCmd, chartShareUpdateCmd)
|
||||
|
||||
@@ -2333,7 +2333,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`,
|
||||
|
||||
子命令:
|
||||
types 查询当前用户假期规则列表
|
||||
save-type 创建或更新假期规则(仅支持无额度模式)
|
||||
update-type 更新假期规则(仅支持无额度模式)
|
||||
balance 查询指定员工假期余额
|
||||
save-balance 更新员工假期余额
|
||||
records 查询指定员工假期余额变更记录`,
|
||||
@@ -3136,11 +3136,11 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`,
|
||||
|
||||
// vacation balance (get_leave_balance_quota)
|
||||
vacationBalanceCmd.Flags().String("users", "", "目标员工 ID 列表,逗号分隔 (必填)")
|
||||
vacationBalanceCmd.Flags().String("leave-code", "", "假期规则 code (选填,不传则查询所有假期)")
|
||||
vacationBalanceCmd.Flags().String("leave-code", "", "假期规则 code (必填,服务端要求非空,不传返回 INVALID_PARAMS)")
|
||||
|
||||
// vacation records (get_leave_balance_records)
|
||||
vacationRecordsCmd.Flags().String("user", "", "指定查询员工 ID (必填)")
|
||||
vacationRecordsCmd.Flags().String("leave-code", "", "假期规则 code (选填,不传则查询所有假期)")
|
||||
vacationRecordsCmd.Flags().String("leave-code", "", "假期规则 code (必填,服务端要求非空,不传返回 INVALID_PARAMS)")
|
||||
vacationRecordsCmd.Flags().String("start", "", "查询开始日期,格式 YYYY-MM-DD (必填)")
|
||||
vacationRecordsCmd.Flags().String("end", "", "查询结束日期,格式 YYYY-MM-DD (必填)")
|
||||
|
||||
|
||||
+88
-17
@@ -202,6 +202,28 @@ func isOpenDingTalkID(value string) bool {
|
||||
return len(value) > 0 && (value[0] == 'D' || value[0] == 'd')
|
||||
}
|
||||
|
||||
// webhookErrcodeFailure 解析自定义机器人 webhook 的响应,判定是否发送失败。
|
||||
// webhook 失败时 HTTP 仍是 200 且返回 {errcode!=0, errmsg},需据 errcode 显式识别。
|
||||
// errcode 可能是 JSON 数字或字符串,统一转字符串比较;缺 errcode 或为 0 视为成功。
|
||||
func webhookErrcodeFailure(raw string) (code, msg string, failed bool) {
|
||||
var m map[string]any
|
||||
if json.Unmarshal([]byte(raw), &m) != nil {
|
||||
return "", "", false
|
||||
}
|
||||
ec, ok := m["errcode"]
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
code = strings.TrimSpace(fmt.Sprintf("%v", ec))
|
||||
if code == "" || code == "0" || code == "0.0" {
|
||||
return "", "", false
|
||||
}
|
||||
if v, ok := m["errmsg"].(string); ok {
|
||||
msg = v
|
||||
}
|
||||
return code, msg, true
|
||||
}
|
||||
|
||||
func splitChatIDValues(values []string) (userIDs []string, openDingTalkIDs []string) {
|
||||
for _, raw := range values {
|
||||
value := strings.TrimSpace(raw)
|
||||
@@ -1841,7 +1863,30 @@ func newChatCommand() *cobra.Command {
|
||||
}
|
||||
toolArgs["atUserIds"] = atUserIds
|
||||
}
|
||||
return callMCPToolOnServer("bot", "send_message_by_custom_robot", toolArgs)
|
||||
// dry-run 只预览参数,不实际发送,交回标准调用路径。
|
||||
if deps.Caller.DryRun() {
|
||||
return callMCPToolOnServer("bot", "send_message_by_custom_robot", toolArgs)
|
||||
}
|
||||
// 自定义机器人 webhook 即使发送失败(如 errcode=300005 token 不存在)
|
||||
// HTTP 仍是 200,会被包成 success:true。这里取原始响应显式识别 errcode,
|
||||
// 非 0 时按失败返回,避免 agent 误判消息已发出。
|
||||
raw, err := callMCPToolReturnTextOnServer(context.Background(), "bot", "send_message_by_custom_robot", toolArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if code, msg, failed := webhookErrcodeFailure(raw); failed {
|
||||
return &CLIError{
|
||||
Code: CodeMCPToolError,
|
||||
Message: fmt.Sprintf("自定义机器人 webhook 发送失败: errcode=%s errmsg=%s", code, msg),
|
||||
Suggestion: "检查 --token 是否有效、机器人是否仍在群内、以及机器人安全设置(关键词/IP/加签)是否拦截",
|
||||
}
|
||||
}
|
||||
var parsed any
|
||||
if json.Unmarshal([]byte(raw), &parsed) == nil {
|
||||
return deps.Out.PrintJSON(parsed)
|
||||
}
|
||||
deps.Out.PrintRaw(raw)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2666,7 +2711,14 @@ func newChatCommand() *cobra.Command {
|
||||
toolArgs["openConversationId"] = groupID
|
||||
}
|
||||
if userID != "" {
|
||||
toolArgs["userId"] = userID
|
||||
// 服务端 get_conversation_info 单聊只认 openDingTalkId(传 userId 键会被
|
||||
// 忽略并报「openCid、cid、peerUid不能同时为空」)。这里把 --user 的 userId
|
||||
// 先解析成 openDingTalkId 再走与 --open-dingtalk-id 相同的通路。
|
||||
resolved, rerr := resolveOpenDingTalkID(context.Background(), userID)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
toolArgs["openDingTalkId"] = resolved
|
||||
}
|
||||
if openDingTalkID != "" {
|
||||
toolArgs["openDingTalkId"] = openDingTalkID
|
||||
@@ -2675,6 +2727,12 @@ func newChatCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
chatConversationInfoCmd.Flags().String("group", "", "群聊 openConversationId(群聊时使用)")
|
||||
chatConversationInfoCmd.Flags().String("conversation-id", "", "--group 的别名")
|
||||
chatConversationInfoCmd.Flags().String("id", "", "--group 的别名")
|
||||
chatConversationInfoCmd.Flags().String("chat", "", "--group 的别名")
|
||||
_ = chatConversationInfoCmd.Flags().MarkHidden("conversation-id")
|
||||
_ = chatConversationInfoCmd.Flags().MarkHidden("id")
|
||||
_ = chatConversationInfoCmd.Flags().MarkHidden("chat")
|
||||
chatConversationInfoCmd.Flags().String("user", "", "单聊对方 userId(单聊时使用)")
|
||||
chatConversationInfoCmd.Flags().String("userId", "", "--user 的别名")
|
||||
_ = chatConversationInfoCmd.Flags().MarkHidden("userId")
|
||||
@@ -3011,7 +3069,7 @@ func newChatCommand() *cobra.Command {
|
||||
chatMessageCreateTextEmotionCmd := &cobra.Command{
|
||||
Use: "create-text-emotion",
|
||||
Short: "创建文字表情(获取 emotionId)",
|
||||
Long: `创建一个新的文字表情模板。当 list-emotions 中没有所需表情时,使用此命令创建并获取 emotionId,随后可用于 add-text-emotion。`,
|
||||
Long: `创建一个新的文字表情模板。当内置表情(见 chat-emoji-list.md)中没有所需表情时,使用此命令创建并获取 emotionId,随后可用于 add-text-emotion。`,
|
||||
Example: ` dws chat message create-text-emotion --emotion-name "赞" --text "nice"
|
||||
dws chat message create-text-emotion --emotion-name "感谢" --text "感谢" --background-id im_bg_5`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -3170,12 +3228,21 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析输出路径:如果是目录,从 URL 推断文件名
|
||||
// 解析输出路径:目录(已存在的目录,或以分隔符结尾的意图目录)→ 追加推断文件名。
|
||||
fi, statErr := os.Stat(outputPath)
|
||||
if statErr == nil && fi.IsDir() {
|
||||
isDir := (statErr == nil && fi.IsDir()) ||
|
||||
strings.HasSuffix(outputPath, string(os.PathSeparator)) ||
|
||||
strings.HasSuffix(outputPath, "/")
|
||||
if isDir {
|
||||
filename := inferFilename(resourceURL)
|
||||
outputPath = filepath.Join(outputPath, filename)
|
||||
}
|
||||
// 确保目标父目录存在,否则 httpGetFile 打开文件会因目录缺失失败。
|
||||
if dir := filepath.Dir(outputPath); dir != "" && dir != "." {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("创建输出目录失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: HTTP GET 下载文件
|
||||
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载资源到 %s ...", outputPath))
|
||||
@@ -4258,16 +4325,16 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
|
||||
chatGroupAuditJoinValidationCmd := &cobra.Command{
|
||||
Use: "audit-join-validation",
|
||||
Short: "审批入群验证(通过、拒绝、删除)",
|
||||
Long: `审批入群验证。支持通过、拒绝、删除、忽略、拒绝并拉黑等操作。
|
||||
Long: `审批入群验证。真机实测服务端仅接受 AuditApprove / AuditDelete,其余状态会被拒绝(unsupported audit status)。
|
||||
|
||||
status 可选值:
|
||||
AuditApprove — 通过
|
||||
AuditDelete — 删除
|
||||
AuditIgnore — 忽略
|
||||
AuditRefuse — 拒绝
|
||||
AuditBlock — 拒绝且不再接受该用户的申请`,
|
||||
AuditApprove — 通过(可用)
|
||||
AuditDelete — 删除(可用)
|
||||
AuditIgnore — 忽略(服务端拒绝,不可用)
|
||||
AuditRefuse — 拒绝(服务端拒绝,不可用)
|
||||
AuditBlock — 拒绝且不再接受该用户的申请(服务端拒绝,不可用)`,
|
||||
Example: ` dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditApprove
|
||||
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditRefuse --description "不符合入群条件"
|
||||
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditDelete --description "不符合入群条件"
|
||||
# 查询入群验证记录: dws chat group list-join-validations`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateRequiredFlags(cmd, "group", "record-id", "applicant", "inviter", "status"); err != nil {
|
||||
@@ -4294,7 +4361,7 @@ status 可选值:
|
||||
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("group")
|
||||
chatGroupAuditJoinValidationCmd.Flags().String("record-id", "", "申请记录 ID (必填)")
|
||||
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("record-id")
|
||||
chatGroupAuditJoinValidationCmd.Flags().String("status", "", "审批动作: AuditApprove/AuditDelete/AuditIgnore/AuditRefuse/AuditBlock (必填)")
|
||||
chatGroupAuditJoinValidationCmd.Flags().String("status", "", "审批动作,真机仅 AuditApprove/AuditDelete 可用;AuditIgnore/AuditRefuse/AuditBlock 服务端拒绝 (必填)")
|
||||
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("status")
|
||||
chatGroupAuditJoinValidationCmd.Flags().String("applicant", "", "申请人 openDingTalkId (必填)")
|
||||
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("applicant")
|
||||
@@ -4375,7 +4442,7 @@ status 可选值:
|
||||
chatListAllConversationsCmd := &cobra.Command{
|
||||
Use: "list-all-conversations",
|
||||
Short: "分页获取当前用户的全部会话列表",
|
||||
Long: `分页获取当前用户的全部会话列表(包含单聊和群聊)。--limit 指定每页数量(最大 100),--cursor 传分页游标(首次不传或传 0)。
|
||||
Long: `分页获取当前用户的全部会话列表(包含单聊和群聊)。--limit 指定每页数量(1-100,默认 100),--cursor 传分页游标(首次不传或传 0)。
|
||||
返回 hasMore=true 时用 nextCursor 作为下次 --cursor 继续翻页。`,
|
||||
Example: ` dws chat list-all-conversations
|
||||
dws chat list-all-conversations --limit 50
|
||||
@@ -4384,6 +4451,10 @@ status 可选值:
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
toolArgs := map[string]any{}
|
||||
if v, err := cmd.Flags().GetInt("limit"); err == nil && v > 0 {
|
||||
// 服务端每页上限 100,超过会被静默截断,这里显式拒绝以免误以为取全。
|
||||
if v > 100 {
|
||||
return fmt.Errorf("--limit 最大 100(服务端上限),got %d;如需取全部会话请配合 --cursor 翻页", v)
|
||||
}
|
||||
toolArgs["limit"] = v
|
||||
}
|
||||
if v, _ := cmd.Flags().GetInt64("cursor"); v > 0 {
|
||||
@@ -4395,7 +4466,7 @@ status 可选值:
|
||||
return callMCPToolOnServer("im", "list_all_conversations", toolArgs)
|
||||
},
|
||||
}
|
||||
chatListAllConversationsCmd.Flags().Int("limit", 1000, "每页数量(默认 1000)")
|
||||
chatListAllConversationsCmd.Flags().Int("limit", 100, "每页数量(1-100,默认 100)")
|
||||
chatListAllConversationsCmd.Flags().Int64("cursor", 0, "分页游标(首次不传或传 0,翻页传 nextCursor)")
|
||||
chatListAllConversationsCmd.Flags().Bool("exclude-muted", false, "是否排除已免打扰会话(默认 false)")
|
||||
|
||||
@@ -4515,8 +4586,8 @@ status 可选值:
|
||||
chatMessageUnsetTopMsgCmd.Flags().String("msg-id", "", "消息 openMessageId (必填)")
|
||||
_ = chatMessageUnsetTopMsgCmd.MarkFlagRequired("msg-id")
|
||||
|
||||
chatGroupCmd.AddCommand(chatGroupBotsCmd, chatGroupDismissCmd, chatGroupSetHistoryCmd, chatGroupListMyGroupsCmd)
|
||||
chatGroupMembersCmd.AddCommand(chatGroupMembersRemoveBotCmd)
|
||||
// group 与 members 的子命令在下方(chatGroupCmd.AddCommand / chatGroupMembersCmd.AddCommand
|
||||
// 的完整列表处)统一注册;此处不再重复 AddCommand,否则 --help 会重复列出。
|
||||
// ── group update-nick: 设置用户在群内的群昵称 ──────────────
|
||||
|
||||
chatGroupUpdateNickCmd := &cobra.Command{
|
||||
|
||||
@@ -582,9 +582,11 @@ contact user profile fields 获取可用字段列表。
|
||||
contactDeptSearchCmd.Flags().String("name", "", "--query 的别名")
|
||||
_ = contactDeptSearchCmd.Flags().MarkHidden("keyword")
|
||||
_ = contactDeptSearchCmd.Flags().MarkHidden("name")
|
||||
contactDeptGetInfoCmd.Flags().String("id", "", "部门 ID (必填)")
|
||||
contactDeptListChildrenCmd.Flags().String("id", "", "部门 ID (必填)")
|
||||
contactDeptListMembersCmd.Flags().String("ids", "", "部门 ID 列表 (必填)")
|
||||
// 主 flag 与 RunE 读取保持一致:get-info / list-children 用 --dept,list-members 用 --depts。
|
||||
// 历史上主 flag 曾误注册为 --id/--ids,导致 RunE 读的 --dept/--depts 未注册、命令行传入报 unknown flag。
|
||||
contactDeptGetInfoCmd.Flags().String("dept", "", "部门 ID (必填)")
|
||||
contactDeptListChildrenCmd.Flags().String("dept", "", "部门 ID (必填)")
|
||||
contactDeptListMembersCmd.Flags().String("depts", "", "部门 ID 列表 (必填)")
|
||||
|
||||
// dept 系列命令统一接受 --id / --ids / --dept-id / --dept-ids 别名(集中注册避免逐命令重复写)。
|
||||
// camelCase --deptId / --deptIds 由 RegisterCamelCaseAliases 自动派生,无需手写。
|
||||
@@ -593,9 +595,9 @@ contact user profile fields 获取可用字段列表。
|
||||
aliases []string
|
||||
}
|
||||
for _, s := range []deptIDAliasSpec{
|
||||
{contactDeptGetInfoCmd, []string{"dept-id", "ids", "dept-ids"}},
|
||||
{contactDeptListChildrenCmd, []string{"ids", "dept-id", "dept-ids"}},
|
||||
{contactDeptListMembersCmd, []string{"id", "dept-id", "dept-ids"}},
|
||||
{contactDeptGetInfoCmd, []string{"id", "dept-id", "ids", "dept-ids"}},
|
||||
{contactDeptListChildrenCmd, []string{"id", "ids", "dept-id", "dept-ids"}},
|
||||
{contactDeptListMembersCmd, []string{"ids", "id", "dept-id", "dept-ids"}},
|
||||
} {
|
||||
for _, name := range s.aliases {
|
||||
if s.cmd.Flags().Lookup(name) != nil {
|
||||
|
||||
@@ -93,8 +93,9 @@ func newDingCommand() *cobra.Command {
|
||||
Use: "list",
|
||||
Short: "查询 DING 消息历史",
|
||||
Long: `查询当前用户的 DING 消息列表,支持按类型过滤。
|
||||
--type 支持: ALL(全部)、UNREAD(未读)、SEND(已发)、NEW_COMMENT(新评论)、DELETED(已删除)。`,
|
||||
Example: ` dws ding message list
|
||||
--type 支持: ALL(全部)、UNREAD(未读)、SEND(已发)、NEW_COMMENT(新评论)、DELETED(已删除)。
|
||||
--type 为服务端必填字段,空值会报「type不能为空」;不传时 CLI 默认按 ALL 查询。`,
|
||||
Example: ` dws ding message list # 默认 --type ALL
|
||||
dws ding message list --type UNREAD
|
||||
dws ding message list --type SEND --cursor 10`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -102,9 +103,12 @@ func newDingCommand() *cobra.Command {
|
||||
if v, _ := cmd.Flags().GetInt64("cursor"); v > 0 {
|
||||
toolArgs["cursor"] = v
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("type"); v != "" {
|
||||
toolArgs["type"] = v
|
||||
// type 是服务端必填,空值会报错;不传或传空时兜底为 ALL。
|
||||
t, _ := cmd.Flags().GetString("type")
|
||||
if t == "" {
|
||||
t = "ALL"
|
||||
}
|
||||
toolArgs["type"] = t
|
||||
return callMCPToolOnServer("im", "list_ding_messages", toolArgs)
|
||||
},
|
||||
}
|
||||
@@ -210,7 +214,7 @@ func newDingCommand() *cobra.Command {
|
||||
dingMessageRecallCmd.Flags().String("robot-code", "", "机器人 ID (必填,或设 DINGTALK_DING_ROBOT_CODE)")
|
||||
dingMessageRecallCmd.Flags().String("id", "", "DING 消息 ID (必填)")
|
||||
dingMessageListCmd.Flags().Int64("cursor", 0, "分页游标(首次传 0,翻页传返回的 nextCursor)")
|
||||
dingMessageListCmd.Flags().String("type", "", "消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED(可选,不传返回全部)")
|
||||
dingMessageListCmd.Flags().String("type", "ALL", "消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED(必填,服务端不接受空值;默认 ALL 全部)")
|
||||
dingMessageReceiverStatusCmd.Flags().String("ding-id", "", "DING 消息 openDingId (必填)")
|
||||
_ = dingMessageReceiverStatusCmd.MarkFlagRequired("ding-id")
|
||||
dingMessageSendPersonalCmd.Flags().String("users", "", "接收者 openDingTalkId 列表,逗号分隔 (必填)")
|
||||
|
||||
@@ -400,7 +400,11 @@ func inferFilename(rawURL string) string {
|
||||
if decoded, err := url.PathUnescape(name); err == nil {
|
||||
name = decoded
|
||||
}
|
||||
if name != "" {
|
||||
// 解码后可能含 %2F 还原出的路径分隔符(如 "ddmedia/xxx.png"),只取
|
||||
// 末段 base 名,避免拼出调用方未创建的子目录导致写文件失败。
|
||||
name = strings.ReplaceAll(name, "\\", "/")
|
||||
name = filepath.Base(name)
|
||||
if name != "" && name != "." && name != "/" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func newMailCommand() *cobra.Command {
|
||||
Long: `查询当前用户绑定的所有邮箱地址。
|
||||
|
||||
返回字段:
|
||||
mailboxes 邮箱列表,每条包含邮箱地址、账号类型、所属企业
|
||||
emailAccounts 邮箱列表,每条包含邮箱地址(email)、账号类型(type)、所属企业(orgName)
|
||||
|
||||
错误说明:
|
||||
domain.notFound 该用户的邮箱不是由钉钉邮箱托管,无法完成操作`,
|
||||
|
||||
@@ -6,8 +6,31 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
)
|
||||
|
||||
// applyGlobalFilter applies the global --jq / --fields output filters to data
|
||||
// when either is set. Returns handled=true when it wrote the (filtered) output,
|
||||
// so callers skip their default JSON encoding. The filter helpers live in
|
||||
// internal/output (the same ones used by `dws api`); the helper Formatter used
|
||||
// by product commands previously ignored these flags, making them no-ops.
|
||||
func (f *Formatter) applyGlobalFilter(data any) (handled bool, err error) {
|
||||
if deps == nil || deps.Caller == nil {
|
||||
return false, nil
|
||||
}
|
||||
jq := strings.TrimSpace(deps.Caller.JQ())
|
||||
fields := strings.TrimSpace(deps.Caller.Fields())
|
||||
if jq == "" && fields == "" {
|
||||
return false, nil
|
||||
}
|
||||
format := output.Format(strings.TrimSpace(deps.Caller.Format()))
|
||||
if format == "" {
|
||||
format = output.FormatJSON
|
||||
}
|
||||
return true, output.WriteFiltered(f.w, format, data, fields, jq)
|
||||
}
|
||||
|
||||
// Formatter provides output formatting compatible with the old Wukong CLI.
|
||||
type Formatter struct {
|
||||
w io.Writer
|
||||
@@ -23,6 +46,9 @@ func NewFormatter() *Formatter {
|
||||
// 转义为 \u0026、\u003c、\u003e。对于大多数 CLI 输出场景这是安全的默认行为。
|
||||
// 如果返回值中包含 URL 等不应被转义的内容,请使用 PrintJSONUnescaped。
|
||||
func (f *Formatter) PrintJSON(data any) error {
|
||||
if handled, err := f.applyGlobalFilter(data); handled {
|
||||
return err
|
||||
}
|
||||
enc := json.NewEncoder(f.w)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(data)
|
||||
@@ -38,6 +64,9 @@ func (f *Formatter) PrintJSON(data any) error {
|
||||
//
|
||||
// 影响范围:仅在调用方显式选择时生效,不影响全局 PrintJSON 的行为。
|
||||
func (f *Formatter) PrintJSONUnescaped(data any) error {
|
||||
if handled, err := f.applyGlobalFilter(data); handled {
|
||||
return err
|
||||
}
|
||||
enc := json.NewEncoder(f.w)
|
||||
enc.SetIndent("", " ")
|
||||
enc.SetEscapeHTML(false)
|
||||
|
||||
@@ -172,7 +172,7 @@ position.col 使用列字母表示法(如 "A"、"AA"),不支持数字形
|
||||
创建成功后返回新图表的完整信息,包含系统分配的 chart-id。`,
|
||||
Example: ` # 创建柱形图
|
||||
dws sheet chart create --node NODE_ID --sheet-id SHEET_ID --properties '{
|
||||
"position": {"row": 12, "col": "A"}, // col 使用字母
|
||||
"position": {"row": 12, "col": "A"},
|
||||
"dimensions": {"width": 600, "height": 400},
|
||||
"chart": {
|
||||
"type": "column",
|
||||
@@ -187,7 +187,7 @@ position.col 使用列字母表示法(如 "A"、"AA"),不支持数字形
|
||||
|
||||
# 创建饼图
|
||||
dws sheet chart create --node NODE_ID --sheet-id SHEET_ID --properties '{
|
||||
"position": {"row": 0, "col": "F"}, // col 使用字母
|
||||
"position": {"row": 0, "col": "F"},
|
||||
"dimensions": {"width": 500, "height": 400},
|
||||
"chart": {
|
||||
"type": "pie",
|
||||
@@ -243,7 +243,7 @@ position.col 使用列字母表示法(如 "A"、"AA"),不支持数字形
|
||||
|
||||
# 修改后整体回写
|
||||
dws sheet chart update --node NODE_ID --sheet-id SHEET_ID --chart-id CHART_ID --properties '{
|
||||
"position": {"row": 12, "col": "A"}, // col 使用字母
|
||||
"position": {"row": 12, "col": "A"},
|
||||
"dimensions": {"width": 800, "height": 500},
|
||||
"chart": {
|
||||
"type": "line",
|
||||
|
||||
@@ -31,8 +31,14 @@ func runSheetExport(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// json 模式下进度提示会污染 stdout(PrintInfo/PrintKeyValue 都写 stdout),
|
||||
// 使得 agent 无法按 JSON 解析。故 json 模式抑制进度、末尾统一输出结果 JSON。
|
||||
jsonMode := deps.Caller.Format() == "json"
|
||||
|
||||
// Step 1: submit export job
|
||||
deps.Out.PrintInfo("[1/3] 提交表格导出任务 (xlsx)...")
|
||||
if !jsonMode {
|
||||
deps.Out.PrintInfo("[1/3] 提交表格导出任务 (xlsx)...")
|
||||
}
|
||||
submitText, err := callMCPToolReturnText(ctx, "submit_export_job", map[string]any{
|
||||
"nodeId": nodeID,
|
||||
"exportFormat": "xlsx",
|
||||
@@ -44,10 +50,11 @@ func runSheetExport(cmd *cobra.Command, _ []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deps.Out.PrintInfo(fmt.Sprintf("导出任务已提交: jobId=%s", jobID))
|
||||
|
||||
// Step 2: progressive backoff polling
|
||||
deps.Out.PrintInfo("[2/3] 轮询任务状态(渐进式退避,最多 30 次约 5 分钟)...")
|
||||
if !jsonMode {
|
||||
deps.Out.PrintInfo(fmt.Sprintf("导出任务已提交: jobId=%s", jobID))
|
||||
// Step 2: progressive backoff polling
|
||||
deps.Out.PrintInfo("[2/3] 轮询任务状态(渐进式退避,最多 30 次约 5 分钟)...")
|
||||
}
|
||||
downloadURL, err := pollSheetExportJob(ctx, jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -55,6 +62,13 @@ func runSheetExport(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
// No output path: print the downloadUrl and exit
|
||||
if outputPath == "" {
|
||||
if jsonMode {
|
||||
return deps.Out.PrintJSON(map[string]any{
|
||||
"success": true,
|
||||
"jobId": jobID,
|
||||
"downloadUrl": downloadURL,
|
||||
})
|
||||
}
|
||||
deps.Out.PrintKeyValue("jobId", jobID)
|
||||
deps.Out.PrintKeyValue("downloadUrl", downloadURL)
|
||||
deps.Out.PrintInfo("导出完成。downloadUrl 具有时效性,请尽快下载。")
|
||||
@@ -71,10 +85,20 @@ func runSheetExport(cmd *cobra.Command, _ []string) error {
|
||||
outputPath = filepath.Join(outputPath, filename)
|
||||
}
|
||||
|
||||
deps.Out.PrintInfo(fmt.Sprintf("[3/3] 下载 xlsx 到 %s ...", outputPath))
|
||||
if !jsonMode {
|
||||
deps.Out.PrintInfo(fmt.Sprintf("[3/3] 下载 xlsx 到 %s ...", outputPath))
|
||||
}
|
||||
if err := httpGetFile(ctx, downloadURL, map[string]string{}, outputPath); err != nil {
|
||||
return fmt.Errorf("下载 xlsx 失败: %w", err)
|
||||
}
|
||||
if jsonMode {
|
||||
return deps.Out.PrintJSON(map[string]any{
|
||||
"success": true,
|
||||
"jobId": jobID,
|
||||
"outputPath": outputPath,
|
||||
"downloadUrl": downloadURL,
|
||||
})
|
||||
}
|
||||
deps.Out.PrintInfo(fmt.Sprintf("导出完成: %s", outputPath))
|
||||
return nil
|
||||
}
|
||||
@@ -124,6 +148,8 @@ func exportPollIntervals() []time.Duration {
|
||||
// pollExportJob polls query_export_job per the progressive backoff schedule
|
||||
// until the job completes successfully, fails, or the 30-attempt cap is hit.
|
||||
func pollSheetExportJob(ctx context.Context, jobID string) (string, error) {
|
||||
// json 模式下轮询进度也要抑制,否则 [INFO] 行会混进 stdout 破坏纯 JSON 输出。
|
||||
quiet := deps.Caller.Format() == "json"
|
||||
intervals := exportPollIntervals()
|
||||
for i, wait := range intervals {
|
||||
select {
|
||||
@@ -136,7 +162,9 @@ func pollSheetExportJob(ctx context.Context, jobID string) (string, error) {
|
||||
"jobId": jobID,
|
||||
})
|
||||
if err != nil {
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 查询失败,将继续轮询: %v", i+1, err))
|
||||
if !quiet {
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 查询失败,将继续轮询: %v", i+1, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -149,7 +177,9 @@ func pollSheetExportJob(ctx context.Context, jobID string) (string, error) {
|
||||
normStatus := strings.ToUpper(strings.TrimSpace(status))
|
||||
switch normStatus {
|
||||
case "SUCCESS":
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: SUCCESS", i+1))
|
||||
if !quiet {
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: SUCCESS", i+1))
|
||||
}
|
||||
if downloadURL == "" {
|
||||
return "", fmt.Errorf("任务成功但未返回 downloadUrl")
|
||||
}
|
||||
@@ -160,9 +190,13 @@ func pollSheetExportJob(ctx context.Context, jobID string) (string, error) {
|
||||
}
|
||||
return "", fmt.Errorf("%s", message)
|
||||
case "PROCESSING", "RUNNING", "DOING", "PENDING", "":
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: PROCESSING", i+1))
|
||||
if !quiet {
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: PROCESSING", i+1))
|
||||
}
|
||||
default:
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: %s", i+1, status))
|
||||
if !quiet {
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" [%d/30] 状态: %s", i+1, status))
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("导出任务超时:已轮询 30 次(约 5 分钟)仍未完成,请稍后再试")
|
||||
@@ -234,7 +268,7 @@ func newExportCmd() *cobra.Command {
|
||||
- 未指定:仅返回 downloadUrl,链接有时效性请尽快下载
|
||||
|
||||
支持范围:
|
||||
仅支持钉钉在线电子表格(alxs)→ xlsx;
|
||||
仅支持钉钉在线电子表格(axls)→ xlsx;
|
||||
若需导出钉钉文字文档,请使用 dingtalkdoc 侧的导出工具。
|
||||
|
||||
权限要求:
|
||||
|
||||
@@ -57,7 +57,13 @@ func runSheetMediaUpload(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
deps.Out.PrintInfo(fmt.Sprintf("[1/2] 获取附件上传凭证 (%s, %d bytes)...", fileName, fileSize))
|
||||
// json 模式下进度提示会污染 stdout(PrintInfo/PrintKeyValue 都写 stdout),
|
||||
// 使得 agent 无法按 JSON 解析。故 json 模式抑制进度、末尾统一输出结果 JSON。
|
||||
jsonMode := deps.Caller.Format() == "json"
|
||||
|
||||
if !jsonMode {
|
||||
deps.Out.PrintInfo(fmt.Sprintf("[1/2] 获取附件上传凭证 (%s, %d bytes)...", fileName, fileSize))
|
||||
}
|
||||
|
||||
result, err := deps.Caller.CallTool(ctx, "doc", "get_doc_attachment_upload_info", map[string]any{
|
||||
"nodeId": nodeID,
|
||||
@@ -91,10 +97,11 @@ func runSheetMediaUpload(cmd *cobra.Command, _ []string) error {
|
||||
resourceURL, _ = credData["resourceUrl"].(string)
|
||||
}
|
||||
|
||||
deps.Out.PrintKeyValue("resourceId", resourceID)
|
||||
deps.Out.PrintKeyValue("resourceUrl", resourceURL)
|
||||
|
||||
deps.Out.PrintInfo("[2/2] 上传文件到 OSS...")
|
||||
if !jsonMode {
|
||||
deps.Out.PrintKeyValue("resourceId", resourceID)
|
||||
deps.Out.PrintKeyValue("resourceUrl", resourceURL)
|
||||
deps.Out.PrintInfo("[2/2] 上传文件到 OSS...")
|
||||
}
|
||||
|
||||
ossHeaders := map[string]string{
|
||||
"Content-Type": mimeType,
|
||||
@@ -103,6 +110,15 @@ func runSheetMediaUpload(cmd *cobra.Command, _ []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if jsonMode {
|
||||
return deps.Out.PrintJSON(map[string]any{
|
||||
"success": true,
|
||||
"resourceId": resourceID,
|
||||
"resourceUrl": resourceURL,
|
||||
"fileName": fileName,
|
||||
"fileSize": fileSize,
|
||||
})
|
||||
}
|
||||
deps.Out.PrintInfo(fmt.Sprintf("附件已上传: %s (resourceId=%s)", fileName, resourceID))
|
||||
return nil
|
||||
}
|
||||
@@ -166,7 +182,13 @@ func runSheetWriteImage(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
deps.Out.PrintInfo(fmt.Sprintf("[1/3] 获取附件上传凭证 (%s, %d bytes)...", fileName, fileSize))
|
||||
// json 模式下进度提示会污染 stdout(PrintInfo/PrintKeyValue 都写 stdout),
|
||||
// 使 write_image 的 JSON 响应无法被单独解析。故 json 模式抑制进度。
|
||||
jsonMode := deps.Caller.Format() == "json"
|
||||
|
||||
if !jsonMode {
|
||||
deps.Out.PrintInfo(fmt.Sprintf("[1/3] 获取附件上传凭证 (%s, %d bytes)...", fileName, fileSize))
|
||||
}
|
||||
|
||||
result, err := deps.Caller.CallTool(ctx, "doc", "get_doc_attachment_upload_info", map[string]any{
|
||||
"nodeId": nodeID,
|
||||
@@ -200,10 +222,11 @@ func runSheetWriteImage(cmd *cobra.Command, _ []string) error {
|
||||
resourceURL, _ = credData["resourceUrl"].(string)
|
||||
}
|
||||
|
||||
deps.Out.PrintKeyValue("resourceId", resourceID)
|
||||
deps.Out.PrintKeyValue("resourceUrl", resourceURL)
|
||||
|
||||
deps.Out.PrintInfo("[2/3] 上传图片到 OSS...")
|
||||
if !jsonMode {
|
||||
deps.Out.PrintKeyValue("resourceId", resourceID)
|
||||
deps.Out.PrintKeyValue("resourceUrl", resourceURL)
|
||||
deps.Out.PrintInfo("[2/3] 上传图片到 OSS...")
|
||||
}
|
||||
|
||||
ossHeaders := map[string]string{
|
||||
"Content-Type": mimeType,
|
||||
@@ -212,7 +235,9 @@ func runSheetWriteImage(cmd *cobra.Command, _ []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
deps.Out.PrintInfo("[3/3] 写入图片到表格单元格...")
|
||||
if !jsonMode {
|
||||
deps.Out.PrintInfo("[3/3] 写入图片到表格单元格...")
|
||||
}
|
||||
|
||||
writeArgs := map[string]any{
|
||||
"nodeId": nodeID,
|
||||
@@ -228,10 +253,13 @@ func runSheetWriteImage(cmd *cobra.Command, _ []string) error {
|
||||
writeArgs["height"] = h
|
||||
}
|
||||
|
||||
if jsonMode {
|
||||
// json 模式:write_image 的 JSON 响应就是唯一 stdout 输出。
|
||||
return callMCPTool("write_image", writeArgs)
|
||||
}
|
||||
if err := callMCPTool("write_image", writeArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deps.Out.PrintInfo(fmt.Sprintf("图片已写入表格: %s → %s (resourceId=%s)", fileName, rangeAddress, resourceID))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -397,6 +398,9 @@ func newRangeBatchSetStyleCmd() *cobra.Command {
|
||||
node := mustGetFlag(cmd, "node")
|
||||
batchPath := mustGetFlag(cmd, "batch")
|
||||
continueOnErr, _ := cmd.Flags().GetBool("continue-on-error")
|
||||
jsonMode := deps.Caller.Format() == "json"
|
||||
ctx := context.Background()
|
||||
var jsonResults []any
|
||||
|
||||
data, err := os.ReadFile(batchPath)
|
||||
if err != nil {
|
||||
@@ -457,6 +461,34 @@ func newRangeBatchSetStyleCmd() *cobra.Command {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[%d/%d] update_range sheet=%s range=%s\n", i+1, total, item.SheetID, item.Range)
|
||||
if jsonMode {
|
||||
text, cerr := callMCPToolReturnText(ctx, "update_range", toolArgs)
|
||||
entry := map[string]any{"index": i + 1, "sheetId": item.SheetID, "range": item.Range}
|
||||
if cerr != nil {
|
||||
entry["ok"] = false
|
||||
entry["error"] = cerr.Error()
|
||||
jsonResults = append(jsonResults, entry)
|
||||
cerr = fmt.Errorf("第 %d/%d 条 update_range 失败: %w", i+1, total, cerr)
|
||||
fmt.Fprintln(os.Stderr, cerr)
|
||||
failed++
|
||||
if firstErr == nil {
|
||||
firstErr = cerr
|
||||
}
|
||||
if !continueOnErr {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
var parsed any
|
||||
if json.Unmarshal([]byte(text), &parsed) == nil {
|
||||
entry["result"] = parsed
|
||||
} else {
|
||||
entry["result"] = text
|
||||
}
|
||||
entry["ok"] = true
|
||||
jsonResults = append(jsonResults, entry)
|
||||
continue
|
||||
}
|
||||
if err := callMCPTool("update_range", toolArgs); err != nil {
|
||||
err = fmt.Errorf("第 %d/%d 条 update_range 失败: %w", i+1, total, err)
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
@@ -470,6 +502,14 @@ func newRangeBatchSetStyleCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "batch-set-style 完成:共 %d 条,失败 %d 条\n", total, failed)
|
||||
if jsonMode {
|
||||
_ = deps.Out.PrintJSON(map[string]any{
|
||||
"total": total,
|
||||
"failed": failed,
|
||||
"results": jsonResults,
|
||||
"success": failed == 0,
|
||||
})
|
||||
}
|
||||
if failed > 0 && !continueOnErr {
|
||||
return firstErr
|
||||
}
|
||||
|
||||
@@ -580,16 +580,19 @@ ORG 类型授权不会出现在查询结果中。`,
|
||||
Short: "在知识库中创建节点",
|
||||
Long: `在指定知识库中创建文档、文件夹或其他类型的节点。
|
||||
|
||||
通过 --type 指定节点类型:
|
||||
通过 --type 指定节点类型(服务端支持以下值,asheet 不被支持):
|
||||
adoc 在线文档 (默认)
|
||||
asheet 在线表格
|
||||
folder 文件夹
|
||||
axls 在线电子表格
|
||||
able 多维表
|
||||
appt 在线演示
|
||||
adraw 白板/画板
|
||||
amind 脑图
|
||||
folder 文件夹
|
||||
|
||||
通过 --folder 指定父节点,不传则创建在知识库根目录。`,
|
||||
Example: ` dws wiki node create --workspace <workspaceId> --name "新文档"
|
||||
dws wiki node create --workspace <workspaceId> --name "方案目录" --type folder
|
||||
dws wiki node create --workspace <workspaceId> --name "数据表" --type asheet --folder <parentNodeId>`,
|
||||
dws wiki node create --workspace <workspaceId> --name "数据表" --type axls --folder <parentNodeId>`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id")
|
||||
if err != nil {
|
||||
@@ -616,7 +619,7 @@ ORG 类型授权不会出现在查询结果中。`,
|
||||
}
|
||||
nodeCreateCmd.Flags().String("workspace", "", "知识库 ID (必填)")
|
||||
nodeCreateCmd.Flags().String("name", "", "节点名称 (必填)")
|
||||
nodeCreateCmd.Flags().String("type", "adoc", "节点类型: adoc / asheet / folder / axls")
|
||||
nodeCreateCmd.Flags().String("type", "adoc", "节点类型: adoc / axls / able / appt / adraw / amind / folder(asheet 不支持)")
|
||||
nodeCreateCmd.Flags().String("folder", "", "父节点 nodeId (选填,不传则在根目录创建)")
|
||||
|
||||
nodeCopyCmd := &cobra.Command{
|
||||
|
||||
@@ -244,7 +244,7 @@ func newChmodCommand(c edition.ToolCaller) *cobra.Command {
|
||||
Long: `授予指定 scope 的操作权限。
|
||||
|
||||
scope 格式: <product>.<entity>:<permission>
|
||||
例: aitable.record:read chat.group:write calendar.event:read
|
||||
例: aitable.record:query chat.message:list calendar.event:get
|
||||
|
||||
grantType 规则:
|
||||
once 一次性,执行一次后自动失效
|
||||
@@ -271,9 +271,9 @@ agentCode 配置:
|
||||
}
|
||||
return cobra.MinimumNArgs(1)(cmd, args)
|
||||
},
|
||||
Example: ` dws pat chmod aitable.record:read --grant-type session --session-id session-xxx
|
||||
Example: ` dws pat chmod aitable.record:query --grant-type session --session-id session-xxx
|
||||
dws pat chmod chat.message:list --grant-type once
|
||||
dws pat chmod aitable.record:read aitable.record:write --grant-type permanent --yes
|
||||
dws pat chmod aitable.record:query aitable.record:create --grant-type permanent --yes
|
||||
dws pat chmod --product calendar --product aitable --grant-type once --dry-run --format json
|
||||
dws pat chmod --products calendar,aitable --grant-type session --session-id session-xxx --yes
|
||||
dws pat chmod --domain calendar --domain chat --grant-type once --yes
|
||||
|
||||
@@ -67,6 +67,8 @@ func (f *fakeToolCaller) CallTool(_ context.Context, _ string, toolName string,
|
||||
|
||||
func (f *fakeToolCaller) Format() string { return "json" }
|
||||
func (f *fakeToolCaller) DryRun() bool { return f.dryRun }
|
||||
func (f *fakeToolCaller) Fields() string { return "" }
|
||||
func (f *fakeToolCaller) JQ() string { return "" }
|
||||
|
||||
type recordedToolCall struct {
|
||||
tool string
|
||||
@@ -94,6 +96,8 @@ func (f *fallbackToolCaller) CallTool(_ context.Context, _ string, toolName stri
|
||||
|
||||
func (f *fallbackToolCaller) Format() string { return "json" }
|
||||
func (f *fallbackToolCaller) DryRun() bool { return false }
|
||||
func (f *fallbackToolCaller) Fields() string { return "" }
|
||||
func (f *fallbackToolCaller) JQ() string { return "" }
|
||||
|
||||
type fallbackErrorToolCaller struct {
|
||||
calls []recordedToolCall
|
||||
@@ -198,6 +202,8 @@ func (f *fallbackPATContractErrorToolCaller) CallTool(_ context.Context, _ strin
|
||||
|
||||
func (f *fallbackPATContractErrorToolCaller) Format() string { return "json" }
|
||||
func (f *fallbackPATContractErrorToolCaller) DryRun() bool { return false }
|
||||
func (f *fallbackPATContractErrorToolCaller) Fields() string { return "" }
|
||||
func (f *fallbackPATContractErrorToolCaller) JQ() string { return "" }
|
||||
|
||||
type sequenceToolCaller struct {
|
||||
calls []recordedToolCall
|
||||
@@ -227,6 +233,8 @@ func (s *sequenceToolCaller) CallTool(_ context.Context, _ string, toolName stri
|
||||
|
||||
func (s *sequenceToolCaller) Format() string { return "json" }
|
||||
func (s *sequenceToolCaller) DryRun() bool { return s.dryRun }
|
||||
func (s *sequenceToolCaller) Fields() string { return "" }
|
||||
func (s *sequenceToolCaller) JQ() string { return "" }
|
||||
|
||||
func stringSliceArgEqual(got any, want []string) bool {
|
||||
gotSlice, ok := got.([]string)
|
||||
@@ -372,7 +380,7 @@ func TestPATHelpDocumentsBatchAuthorization(t *testing.T) {
|
||||
"--dry-run 只返回授权计划",
|
||||
"执行批量授权必须显式",
|
||||
"由服务端默认兜底",
|
||||
"aitable.record:read aitable.record:write --grant-type permanent --yes",
|
||||
"aitable.record:query aitable.record:create --grant-type permanent --yes",
|
||||
"dws pat chmod --products calendar,aitable",
|
||||
"dws pat chmod --recommend --grant-type session",
|
||||
} {
|
||||
@@ -2020,3 +2028,12 @@ func TestResolveAgentCodeFromEnv(t *testing.T) {
|
||||
code, src)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fallbackErrorToolCaller) Fields() string { return "" }
|
||||
func (f *fallbackErrorToolCaller) JQ() string { return "" }
|
||||
func (f *fallbackSchemaMismatchToolCaller) Fields() string { return "" }
|
||||
func (f *fallbackSchemaMismatchToolCaller) JQ() string { return "" }
|
||||
func (f *fallbackPermissionDeniedToolCaller) Fields() string { return "" }
|
||||
func (f *fallbackPermissionDeniedToolCaller) JQ() string { return "" }
|
||||
func (f *fallbackPATErrorToolCaller) Fields() string { return "" }
|
||||
func (f *fallbackPATErrorToolCaller) JQ() string { return "" }
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmdutil
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// MergeHardcodedLeaves grafts leaves from hardcodedRoot onto dynamicRoot when
|
||||
// the same-named path does not already exist. Groups recurse. On leaf
|
||||
// conflicts, the dynamic side wins by default because the runtime envelope is
|
||||
// authoritative; hardcoded commands are retained as fallback for paths the
|
||||
// envelope does not declare.
|
||||
//
|
||||
// A hardcoded leaf or group can opt into replacement by carrying a strictly
|
||||
// higher OverridePriority than the dynamic command at the same path.
|
||||
//
|
||||
// MergeHardcodedLeaves mutates dynamicRoot in place and returns it. Grafted
|
||||
// commands are detached from hardcodedRoot so Cobra parent pointers remain
|
||||
// correct.
|
||||
func MergeHardcodedLeaves(dynamicRoot, hardcodedRoot *cobra.Command) *cobra.Command {
|
||||
if dynamicRoot == nil || hardcodedRoot == nil {
|
||||
return dynamicRoot
|
||||
}
|
||||
|
||||
children := append([]*cobra.Command(nil), hardcodedRoot.Commands()...)
|
||||
for _, hc := range children {
|
||||
dyn := findChildByName(dynamicRoot, hc.Name())
|
||||
switch {
|
||||
case dyn == nil:
|
||||
hardcodedRoot.RemoveCommand(hc)
|
||||
dynamicRoot.AddCommand(hc)
|
||||
case IsLeafCmd(hc) && IsLeafCmd(dyn):
|
||||
if OverridePriority(hc) > OverridePriority(dyn) {
|
||||
hardcodedRoot.RemoveCommand(hc)
|
||||
dynamicRoot.RemoveCommand(dyn)
|
||||
dynamicRoot.AddCommand(hc)
|
||||
}
|
||||
case !IsLeafCmd(hc) && !IsLeafCmd(dyn) && OverridePriority(hc) > OverridePriority(dyn):
|
||||
hardcodedRoot.RemoveCommand(hc)
|
||||
dynamicRoot.RemoveCommand(dyn)
|
||||
dynamicRoot.AddCommand(hc)
|
||||
case !IsLeafCmd(hc) && !IsLeafCmd(dyn):
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
case IsLeafCmd(dyn) && !IsLeafCmd(hc) && OverridePriority(hc) > OverridePriority(dyn):
|
||||
hardcodedRoot.RemoveCommand(hc)
|
||||
dynamicRoot.RemoveCommand(dyn)
|
||||
dynamicRoot.AddCommand(hc)
|
||||
default:
|
||||
slog.Warn("overlay: shape mismatch, keeping dynamic",
|
||||
"name", hc.Name(),
|
||||
"dynamicIsLeaf", IsLeafCmd(dyn),
|
||||
"hardcodedIsLeaf", IsLeafCmd(hc))
|
||||
}
|
||||
}
|
||||
return dynamicRoot
|
||||
}
|
||||
|
||||
// IsLeafCmd reports whether cmd has no subcommands.
|
||||
func IsLeafCmd(cmd *cobra.Command) bool {
|
||||
if cmd == nil {
|
||||
return false
|
||||
}
|
||||
return !cmd.HasSubCommands()
|
||||
}
|
||||
|
||||
func findChildByName(parent *cobra.Command, name string) *cobra.Command {
|
||||
if parent == nil {
|
||||
return nil
|
||||
}
|
||||
for _, child := range parent.Commands() {
|
||||
if child.Name() == name {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmdutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newGroup(name string, children ...*cobra.Command) *cobra.Command {
|
||||
cmd := &cobra.Command{Use: name}
|
||||
cmd.AddCommand(children...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newLeaf(name, tag string) *cobra.Command {
|
||||
return &cobra.Command{Use: name, Short: tag}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesNilInputs(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := MergeHardcodedLeaves(nil, nil); got != nil {
|
||||
t.Fatalf("expected nil, got %v", got)
|
||||
}
|
||||
dyn := newGroup("root")
|
||||
if got := MergeHardcodedLeaves(dyn, nil); got != dyn {
|
||||
t.Fatal("expected dynamic root to be returned unchanged")
|
||||
}
|
||||
hc := newGroup("root")
|
||||
if got := MergeHardcodedLeaves(nil, hc); got != nil {
|
||||
t.Fatal("expected nil when dynamic root is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesGraftsUnknownLeaf(t *testing.T) {
|
||||
t.Parallel()
|
||||
dyn := newGroup("root", newLeaf("kept", "dynamic"))
|
||||
hc := newGroup("root", newLeaf("extra", "hardcoded"))
|
||||
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
|
||||
got := findChildByName(dyn, "extra")
|
||||
if got == nil {
|
||||
t.Fatal("expected extra leaf to be grafted")
|
||||
}
|
||||
if got.Short != "hardcoded" {
|
||||
t.Fatalf("extra.Short = %q, want %q", got.Short, "hardcoded")
|
||||
}
|
||||
if findChildByName(hc, "extra") != nil {
|
||||
t.Fatal("expected extra leaf to be detached from hardcoded root")
|
||||
}
|
||||
if got.Parent() != dyn {
|
||||
t.Fatalf("grafted leaf parent = %v, want %v", got.Parent(), dyn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesDynamicLeafWinsByDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
dyn := newGroup("root", newLeaf("shared", "dynamic"))
|
||||
hc := newGroup("root", newLeaf("shared", "hardcoded"))
|
||||
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
|
||||
got := findChildByName(dyn, "shared")
|
||||
if got == nil {
|
||||
t.Fatal("expected shared leaf to remain")
|
||||
}
|
||||
if got.Short != "dynamic" {
|
||||
t.Fatalf("shared.Short = %q, want dynamic", got.Short)
|
||||
}
|
||||
if findChildByName(hc, "shared") == nil {
|
||||
t.Fatal("expected hardcoded shared leaf to remain on donor root")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesHigherPriorityHardcodedLeafWins(t *testing.T) {
|
||||
t.Parallel()
|
||||
dynLeaf := newLeaf("shared", "dynamic")
|
||||
dyn := newGroup("root", dynLeaf)
|
||||
hcLeaf := newLeaf("shared", "hardcoded")
|
||||
SetOverridePriority(hcLeaf, 100)
|
||||
hc := newGroup("root", hcLeaf)
|
||||
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
|
||||
got := findChildByName(dyn, "shared")
|
||||
if got != hcLeaf {
|
||||
t.Fatalf("expected hardcoded leaf to replace dynamic leaf, got %+v", got)
|
||||
}
|
||||
if findChildByName(hc, "shared") != nil {
|
||||
t.Fatal("expected hardcoded leaf to be detached from donor root")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesEqualPriorityKeepsDynamic(t *testing.T) {
|
||||
t.Parallel()
|
||||
dynLeaf := newLeaf("shared", "dynamic")
|
||||
SetOverridePriority(dynLeaf, 100)
|
||||
dyn := newGroup("root", dynLeaf)
|
||||
hcLeaf := newLeaf("shared", "hardcoded")
|
||||
SetOverridePriority(hcLeaf, 100)
|
||||
hc := newGroup("root", hcLeaf)
|
||||
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
|
||||
got := findChildByName(dyn, "shared")
|
||||
if got != dynLeaf {
|
||||
t.Fatalf("equal priorities should keep dynamic leaf, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesRecurseGroups(t *testing.T) {
|
||||
t.Parallel()
|
||||
dyn := newGroup("root",
|
||||
newGroup("space",
|
||||
newLeaf("list", "dynamic"),
|
||||
),
|
||||
)
|
||||
hc := newGroup("root",
|
||||
newGroup("space",
|
||||
newLeaf("list", "hardcoded"),
|
||||
newLeaf("create", "hardcoded"),
|
||||
),
|
||||
newLeaf("ping", "hardcoded"),
|
||||
)
|
||||
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
|
||||
space := findChildByName(dyn, "space")
|
||||
if space == nil {
|
||||
t.Fatal("expected space group")
|
||||
}
|
||||
if list := findChildByName(space, "list"); list == nil || list.Short != "dynamic" {
|
||||
t.Fatalf("space.list should remain dynamic, got %+v", list)
|
||||
}
|
||||
if create := findChildByName(space, "create"); create == nil || create.Short != "hardcoded" {
|
||||
t.Fatalf("space.create should be grafted, got %+v", create)
|
||||
}
|
||||
if ping := findChildByName(dyn, "ping"); ping == nil || ping.Short != "hardcoded" {
|
||||
t.Fatalf("ping should be grafted, got %+v", ping)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesHigherPriorityGroupReplacesDynamicGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
dynGroup := newGroup("export", newLeaf("get", "dynamic"))
|
||||
dyn := newGroup("root", dynGroup)
|
||||
hcGroup := newGroup("export", newLeaf("get", "hardcoded"))
|
||||
SetOverridePriority(hcGroup, 100)
|
||||
hc := newGroup("root", hcGroup)
|
||||
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
|
||||
got := findChildByName(dyn, "export")
|
||||
if got != hcGroup {
|
||||
t.Fatal("expected higher-priority hardcoded group to replace dynamic group")
|
||||
}
|
||||
if get := findChildByName(got, "get"); get == nil || get.Short != "hardcoded" {
|
||||
t.Fatalf("expected hardcoded export.get after replacement, got %+v", get)
|
||||
}
|
||||
if findChildByName(hc, "export") != nil {
|
||||
t.Fatal("expected hardcoded group to be detached from donor root")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesShapeMismatchKeepsDynamic(t *testing.T) {
|
||||
t.Parallel()
|
||||
dyn := newGroup("root",
|
||||
newGroup("cmd", newLeaf("sub", "dynamic")),
|
||||
)
|
||||
hc := newGroup("root", newLeaf("cmd", "hardcoded"))
|
||||
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
|
||||
cmd := findChildByName(dyn, "cmd")
|
||||
if cmd == nil {
|
||||
t.Fatal("expected dynamic cmd to remain")
|
||||
}
|
||||
if IsLeafCmd(cmd) {
|
||||
t.Fatal("expected dynamic cmd to remain a group")
|
||||
}
|
||||
if findChildByName(cmd, "sub") == nil {
|
||||
t.Fatal("expected cmd.sub to remain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesHigherPriorityGroupReplacesDynamicLeaf(t *testing.T) {
|
||||
t.Parallel()
|
||||
dynLeaf := newLeaf("shared", "dynamic")
|
||||
dyn := newGroup("root", dynLeaf)
|
||||
hcGroup := newGroup("shared",
|
||||
newLeaf("list", "hardcoded"),
|
||||
newLeaf("add", "hardcoded"),
|
||||
)
|
||||
SetOverridePriority(hcGroup, 100)
|
||||
hc := newGroup("root", hcGroup)
|
||||
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
|
||||
got := findChildByName(dyn, "shared")
|
||||
if got != hcGroup {
|
||||
t.Fatal("expected higher-priority hardcoded group to replace dynamic leaf")
|
||||
}
|
||||
if findChildByName(got, "list") == nil {
|
||||
t.Fatal("expected hardcoded subtree leaf list to be reachable")
|
||||
}
|
||||
if findChildByName(got, "add") == nil {
|
||||
t.Fatal("expected hardcoded subtree leaf add to be reachable")
|
||||
}
|
||||
if findChildByName(hc, "shared") != nil {
|
||||
t.Fatal("expected hardcoded group to be detached from donor root")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeHardcodedLeavesEqualPriorityShapeMismatchKeepsDynamic(t *testing.T) {
|
||||
t.Parallel()
|
||||
dynLeaf := newLeaf("shared", "dynamic")
|
||||
SetOverridePriority(dynLeaf, 100)
|
||||
dyn := newGroup("root", dynLeaf)
|
||||
hcGroup := newGroup("shared", newLeaf("list", "hardcoded"))
|
||||
SetOverridePriority(hcGroup, 100)
|
||||
hc := newGroup("root", hcGroup)
|
||||
|
||||
MergeHardcodedLeaves(dyn, hc)
|
||||
|
||||
got := findChildByName(dyn, "shared")
|
||||
if got != dynLeaf {
|
||||
t.Fatalf("equal priorities should keep dynamic leaf, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLeafCmd(t *testing.T) {
|
||||
t.Parallel()
|
||||
leaf := newLeaf("x", "")
|
||||
group := newGroup("x", newLeaf("child", ""))
|
||||
if !IsLeafCmd(leaf) {
|
||||
t.Fatal("expected leaf to be leaf")
|
||||
}
|
||||
if IsLeafCmd(group) {
|
||||
t.Fatal("expected group to not be leaf")
|
||||
}
|
||||
if IsLeafCmd(nil) {
|
||||
t.Fatal("expected nil to not be leaf")
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,45 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmdutil
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
// SourceAnnotation records where a command tree came from. Edition overlays
|
||||
// use it to distinguish runtime-authored commands from helper fallbacks that
|
||||
// happen to share the same top-level product name.
|
||||
const SourceAnnotation = "dws.source"
|
||||
|
||||
// SourceEnvelope marks a command as authored by the runtime discovery envelope.
|
||||
const SourceEnvelope = "envelope"
|
||||
|
||||
// MarkEnvelopeSource stamps cmd with runtime discovery provenance.
|
||||
func MarkEnvelopeSource(cmd *cobra.Command) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
cmd.Annotations[SourceAnnotation] = SourceEnvelope
|
||||
}
|
||||
|
||||
// IsEnvelopeSourced reports whether cmd was authored by the runtime discovery
|
||||
// envelope.
|
||||
func IsEnvelopeSourced(cmd *cobra.Command) bool {
|
||||
return cmd != nil && cmd.Annotations[SourceAnnotation] == SourceEnvelope
|
||||
}
|
||||
|
||||
// KindAnnotation is the annotation key for marking command kinds.
|
||||
const KindAnnotation = "dws.kind"
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmdutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestEnvelopeSourceProvenance(t *testing.T) {
|
||||
t.Parallel()
|
||||
if IsEnvelopeSourced(nil) {
|
||||
t.Fatal("nil command should not be envelope sourced")
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{Use: "chat"}
|
||||
if IsEnvelopeSourced(cmd) {
|
||||
t.Fatal("unstamped command should not be envelope sourced")
|
||||
}
|
||||
|
||||
MarkEnvelopeSource(cmd)
|
||||
if !IsEnvelopeSourced(cmd) {
|
||||
t.Fatal("stamped command should be envelope sourced")
|
||||
}
|
||||
if got := cmd.Annotations[SourceAnnotation]; got != SourceEnvelope {
|
||||
t.Fatalf("SourceAnnotation = %q, want %q", got, SourceEnvelope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkEnvelopeSourceNilDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
MarkEnvelopeSource(nil)
|
||||
}
|
||||
@@ -53,6 +53,10 @@ type ToolCaller interface {
|
||||
Format() string
|
||||
// DryRun returns true when --dry-run is active.
|
||||
DryRun() bool
|
||||
// Fields returns the global --fields output projection ("" if unset).
|
||||
Fields() string
|
||||
// JQ returns the global --jq output filter expression ("" if unset).
|
||||
JQ() string
|
||||
}
|
||||
|
||||
// RuntimeDefaultFn resolves a single runtimeDefault placeholder (e.g.
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build release artifacts locally in the current runner and publish them to Gitee.
|
||||
|
||||
set -eu
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
SCRIPT_ROOT="$ROOT"
|
||||
cd "$SCRIPT_ROOT"
|
||||
|
||||
VERSION="${VERSION:-$(git describe --tags --exact-match 2>/dev/null || true)}"
|
||||
[ -n "$VERSION" ] || {
|
||||
echo "error: VERSION is required or HEAD must be an exact release tag" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$VERSION" in
|
||||
v*) TAG="$VERSION"; SEMVER="${VERSION#v}" ;;
|
||||
*) TAG="v$VERSION"; SEMVER="$VERSION" ;;
|
||||
esac
|
||||
|
||||
git fetch --tags origin "refs/tags/${TAG}:refs/tags/${TAG}" >/dev/null 2>&1 || true
|
||||
target_commit="$(git rev-parse "${TAG}^{commit}" 2>/dev/null || true)"
|
||||
[ -n "$target_commit" ] || {
|
||||
echo "error: could not resolve tag ${TAG}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
current_commit="$(git rev-parse HEAD)"
|
||||
WORKDIR="$SCRIPT_ROOT"
|
||||
cleanup_worktree() {
|
||||
if [ "$WORKDIR" != "$SCRIPT_ROOT" ]; then
|
||||
git -C "$SCRIPT_ROOT" worktree remove --force "$WORKDIR" >/dev/null 2>&1 || rm -rf "$WORKDIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup_worktree EXIT
|
||||
|
||||
if [ "$current_commit" != "$target_commit" ]; then
|
||||
WORKDIR="$(mktemp -d)"
|
||||
rm -rf "$WORKDIR"
|
||||
git worktree add --detach "$WORKDIR" "$TAG"
|
||||
mkdir -p "$WORKDIR/scripts/release"
|
||||
cp "$SCRIPT_ROOT/scripts/release/publish-gitee-local.sh" "$WORKDIR/scripts/release/publish-gitee-local.sh"
|
||||
chmod +x "$WORKDIR/scripts/release/publish-gitee-local.sh"
|
||||
fi
|
||||
|
||||
cd "$WORKDIR"
|
||||
|
||||
echo "==> Building ${TAG} locally for Gitee"
|
||||
VERSION="$SEMVER" ./scripts/dev/build-all.sh
|
||||
DWS_PACKAGE_VERSION="$TAG" ./scripts/release/post-goreleaser.sh
|
||||
VERSION="$TAG" ./scripts/release/publish-gitee-local.sh
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publish locally-built release artifacts to the matching Gitee release.
|
||||
#
|
||||
# Intended to run inside Gitee Go after building artifacts in China. This avoids
|
||||
# the unreliable GitHub Actions -> Gitee cross-border upload path.
|
||||
|
||||
set -eu
|
||||
|
||||
DIST_DIR="${DIST_DIR:-dist}"
|
||||
GITEE_API="${GITEE_API:-https://gitee.com/api/v5}"
|
||||
GITEE_REPO="${GITEE_REPO:-DingTalk-Real-AI/dingtalk-workspace-cli}"
|
||||
GITEE_TOKEN="${GITEE_TOKEN:-${GITEE_ACCESS_TOKEN:-}}"
|
||||
GITEE_CURL_CONNECT_TIMEOUT="${GITEE_CURL_CONNECT_TIMEOUT:-15}"
|
||||
GITEE_CURL_MAX_TIME="${GITEE_CURL_MAX_TIME:-120}"
|
||||
GITEE_UPLOAD_MAX_TIME="${GITEE_UPLOAD_MAX_TIME:-300}"
|
||||
GITEE_UPLOAD_RETRIES="${GITEE_UPLOAD_RETRIES:-3}"
|
||||
GITEE_UPLOAD_RETRY_DELAY="${GITEE_UPLOAD_RETRY_DELAY:-10}"
|
||||
|
||||
err() {
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -n "$GITEE_TOKEN" ] || err "GITEE_TOKEN is required"
|
||||
[ -d "$DIST_DIR" ] || err "dist dir not found: $DIST_DIR"
|
||||
|
||||
VERSION="${VERSION:-$(git describe --tags --exact-match 2>/dev/null || true)}"
|
||||
[ -n "$VERSION" ] || err "VERSION is required or HEAD must be an exact tag"
|
||||
case "$VERSION" in
|
||||
v*) ;;
|
||||
*) VERSION="v$VERSION" ;;
|
||||
esac
|
||||
|
||||
OWNER="${GITEE_REPO%%/*}"
|
||||
NAME="${GITEE_REPO##*/}"
|
||||
base="${GITEE_API}/repos/${OWNER}/${NAME}"
|
||||
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then sha256sum ${1:+"$1"} | awk '{print $1}'
|
||||
else shasum -a 256 ${1:+"$1"} | awk '{print $1}'
|
||||
fi
|
||||
}
|
||||
|
||||
api_get() {
|
||||
curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "$@"
|
||||
}
|
||||
|
||||
echo "📦 Publishing local artifacts for ${VERSION} to Gitee ${GITEE_REPO}"
|
||||
|
||||
target_commit="$(git rev-parse "${VERSION}^{commit}" 2>/dev/null || git rev-parse HEAD)"
|
||||
|
||||
rel_json="$(api_get "${base}/releases/tags/${VERSION}?access_token=${GITEE_TOKEN}" 2>/dev/null || true)"
|
||||
release_id="$(printf '%s' "$rel_json" | grep -o '"id":[ ]*[0-9]*' | head -1 | grep -o '[0-9]*' || true)"
|
||||
|
||||
if [ -z "$release_id" ]; then
|
||||
echo " No Gitee release for ${VERSION} yet — creating it."
|
||||
rel_json="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \
|
||||
-X POST "${base}/releases" \
|
||||
-F "access_token=${GITEE_TOKEN}" \
|
||||
-F "tag_name=${VERSION}" \
|
||||
-F "name=${VERSION}" \
|
||||
-F "body=Gitee-local build of ${VERSION} for China users." \
|
||||
-F "target_commitish=${target_commit}" 2>/dev/null || true)"
|
||||
release_id="$(printf '%s' "$rel_json" | grep -o '"id":[ ]*[0-9]*' | head -1 | grep -o '[0-9]*' || true)"
|
||||
fi
|
||||
[ -n "$release_id" ] || err "could not get/create Gitee release for ${VERSION}. Response: ${rel_json}"
|
||||
echo " Gitee release id = ${release_id}"
|
||||
|
||||
assets_map="$(api_get "${base}/releases/${release_id}/attach_files?access_token=${GITEE_TOKEN}" 2>/dev/null \
|
||||
| python3 -c 'import json,sys
|
||||
try:
|
||||
data=json.load(sys.stdin)
|
||||
rows=data if isinstance(data,list) else data.get("attach_files",[])
|
||||
for a in rows:
|
||||
n=a.get("name",""); i=a.get("id",""); u=a.get("browser_download_url","")
|
||||
if n and i!="":
|
||||
print("%s\t%s\t%s" % (n, i, u))
|
||||
except Exception:
|
||||
pass' 2>/dev/null || true)"
|
||||
|
||||
gitee_attach() {
|
||||
file="$1"
|
||||
fn="$(basename "$file")"
|
||||
attempt=1
|
||||
while [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ]; do
|
||||
response="$(curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_UPLOAD_MAX_TIME" \
|
||||
--retry 2 --retry-delay 5 --retry-all-errors \
|
||||
-X POST "${base}/releases/${release_id}/attach_files" \
|
||||
-F "access_token=${GITEE_TOKEN}" -F "file=@${file}" 2>&1 || true)"
|
||||
if printf '%s' "$response" | grep -q '"browser_download_url"'; then
|
||||
return 0
|
||||
fi
|
||||
echo " ⚠ upload attempt ${attempt}/${GITEE_UPLOAD_RETRIES} failed for ${fn}: $(printf '%s' "$response" | head -c 240)" >&2
|
||||
attempt=$((attempt + 1))
|
||||
[ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ] && sleep "$GITEE_UPLOAD_RETRY_DELAY"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
gitee_delete() {
|
||||
curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \
|
||||
-X DELETE "${base}/releases/${release_id}/attach_files/${1}?access_token=${GITEE_TOKEN}" \
|
||||
>/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
uploaded=0
|
||||
replaced=0
|
||||
skipped=0
|
||||
failed=0
|
||||
for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt "$DIST_DIR"/dws-skills.zip; do
|
||||
[ -f "$f" ] || continue
|
||||
fn="$(basename "$f")"
|
||||
local_sha="$(sha256_of "$f")"
|
||||
ids="$(printf '%s\n' "$assets_map" | awk -F'\t' -v n="$fn" '$1==n {print $2}')"
|
||||
aurl="$(printf '%s\n' "$assets_map" | awk -F'\t' -v n="$fn" '$1==n {print $3; exit}')"
|
||||
count="$(printf '%s' "$ids" | grep -c . || true)"
|
||||
|
||||
if [ "$count" -eq 1 ]; then
|
||||
gitee_sha="$(api_get "$aurl" 2>/dev/null | sha256_of || true)"
|
||||
if [ "$gitee_sha" = "$local_sha" ]; then
|
||||
echo " ✓ ${fn} already correct on Gitee — skip"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
echo " ↻ ${fn} differs on Gitee — replacing"
|
||||
elif [ "$count" -gt 1 ]; then
|
||||
echo " ↻ ${fn} has ${count} copies on Gitee — replacing"
|
||||
else
|
||||
echo " ⬆ ${fn} (new)"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$ids" | while read -r aid; do
|
||||
[ -n "$aid" ] && gitee_delete "$aid"
|
||||
done
|
||||
if gitee_attach "$f"; then
|
||||
if [ "$count" -eq 0 ]; then uploaded=$((uploaded + 1)); else replaced=$((replaced + 1)); fi
|
||||
else
|
||||
echo " ❌ upload failed for ${fn}" >&2
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$failed" -eq 0 ] || err "Gitee publish finished with ${failed} failed upload(s)"
|
||||
echo "✅ Gitee release ${VERSION}: uploaded ${uploaded}, replaced ${replaced}, skipped ${skipped}"
|
||||
@@ -28,6 +28,11 @@ set -eu
|
||||
|
||||
DIST_DIR="${DIST_DIR:-dist}"
|
||||
GITEE_API="${GITEE_API:-https://gitee.com/api/v5}"
|
||||
GITEE_CURL_CONNECT_TIMEOUT="${GITEE_CURL_CONNECT_TIMEOUT:-15}"
|
||||
GITEE_CURL_MAX_TIME="${GITEE_CURL_MAX_TIME:-120}"
|
||||
GITEE_UPLOAD_MAX_TIME="${GITEE_UPLOAD_MAX_TIME:-300}"
|
||||
GITEE_UPLOAD_RETRIES="${GITEE_UPLOAD_RETRIES:-3}"
|
||||
GITEE_UPLOAD_RETRY_DELAY="${GITEE_UPLOAD_RETRY_DELAY:-10}"
|
||||
|
||||
missing=""
|
||||
[ -z "${GITEE_TOKEN:-}" ] && missing="$missing GITEE_TOKEN"
|
||||
@@ -119,7 +124,7 @@ echo " Gitee release id = ${release_id}"
|
||||
# detail (/releases/{id}) endpoint: the latter's "assets" array omits the attach
|
||||
# id, so DELETE /attach_files/{id} was previously called with an empty id and
|
||||
# silently no-op'd — leaving stale + duplicate darwin binaries on Gitee.
|
||||
assets_map="$(curl -fsSL "${base}/releases/${release_id}/attach_files?access_token=${GITEE_TOKEN}" 2>/dev/null \
|
||||
assets_map="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "${base}/releases/${release_id}/attach_files?access_token=${GITEE_TOKEN}" 2>/dev/null \
|
||||
| python3 -c 'import json,sys
|
||||
try:
|
||||
data=json.load(sys.stdin)
|
||||
@@ -137,13 +142,27 @@ sha256_of() { # sha256 of a file ($1) or, with no arg, of stdin
|
||||
}
|
||||
|
||||
gitee_attach() { # upload file $1; success when the response carries a download url
|
||||
printf '%s' "$(curl -fsSL -X POST "${base}/releases/${release_id}/attach_files" \
|
||||
-F "access_token=${GITEE_TOKEN}" -F "file=@${1}" 2>/dev/null || true)" \
|
||||
| grep -q '"browser_download_url"'
|
||||
file="$1"
|
||||
fn="$(basename "$file")"
|
||||
attempt=1
|
||||
while [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ]; do
|
||||
response="$(curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_UPLOAD_MAX_TIME" \
|
||||
--retry 2 --retry-delay 5 --retry-all-errors \
|
||||
-X POST "${base}/releases/${release_id}/attach_files" \
|
||||
-F "access_token=${GITEE_TOKEN}" -F "file=@${file}" 2>&1 || true)"
|
||||
if printf '%s' "$response" | grep -q '"browser_download_url"'; then
|
||||
return 0
|
||||
fi
|
||||
echo " ⚠ upload attempt ${attempt}/${GITEE_UPLOAD_RETRIES} failed for ${fn}: $(printf '%s' "$response" | head -c 240)" >&2
|
||||
attempt=$((attempt + 1))
|
||||
[ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ] && sleep "$GITEE_UPLOAD_RETRY_DELAY"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
gitee_delete() { # delete attachment by id $1
|
||||
curl -fsSL -X DELETE "${base}/releases/${release_id}/attach_files/${1}?access_token=${GITEE_TOKEN}" \
|
||||
curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \
|
||||
-X DELETE "${base}/releases/${release_id}/attach_files/${1}?access_token=${GITEE_TOKEN}" \
|
||||
>/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@@ -166,7 +185,7 @@ for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.tx
|
||||
fi
|
||||
|
||||
if [ "$count" -eq 1 ]; then
|
||||
gitee_sha="$(curl -fsSL "$aurl" 2>/dev/null | sha256_of || true)"
|
||||
gitee_sha="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "$aurl" 2>/dev/null | sha256_of || true)"
|
||||
if [ "$gitee_sha" = "$local_sha" ]; then
|
||||
echo " ✓ ${fn} already correct on Gitee — skip"
|
||||
skipped=$((skipped + 1))
|
||||
|
||||
@@ -43,7 +43,7 @@ cli_version: ">=1.0.15"
|
||||
| `oa` | OA审批:待处理/详情/同意/拒绝/撤销/记录/已发起/任务/转交/评论/抄送 | [oa.md](./references/products/oa.md) |
|
||||
| `report` | 日志:按模版创建/收件箱/已发送/模版查看/详情/已读统计 | [report.md](./references/products/report.md) |
|
||||
| `mail` | 邮箱:邮箱地址查询/邮件搜索(KQL)/邮件详情/发送邮件 | [mail.md](./references/products/mail.md) |
|
||||
| `sheet` | 在线电子表格(axls):工作表 CRUD/区域读写/CSV 批量写入/行列增删/合并/查找替换/筛选视图/全局筛选/排序/下拉列表/浮动图片/导出(两步) | [sheet.md](./references/products/sheet.md) |
|
||||
| `sheet` | 在线电子表格(axls):工作表 CRUD/区域读写/CSV 批量写入/行列增删/合并/查找替换/筛选视图/全局筛选/排序/下拉列表/条件格式/浮动图片/浮动图表/模板/导出 xlsx(单命令一站式) | [sheet.md](./references/products/sheet.md) |
|
||||
| `todo` | 待办:创建(含优先级/截止时间/循环)/查询/修改/标记完成/删除 | [todo.md](./references/products/todo.md) |
|
||||
| `wiki` | 知识库:空间创建/详情/列表/搜索 + 成员管理 | [wiki.md](./references/products/wiki.md) |
|
||||
|
||||
@@ -130,7 +130,8 @@ dws <command-path> --help
|
||||
|
||||
# helper-only schema 查询(如 dev.*),普通产品命令不要依赖 schema 推断参数
|
||||
dws schema "dev app create"
|
||||
dws schema "dev app create" --jq '.tool.required'
|
||||
# 注:--jq 对 schema 输出无效(不过滤,仍返回完整对象);schema 结构里必填标在
|
||||
# .parameters.<字段>.required,没有 .tool 键。要看必填字段自行读 .parameters 即可。
|
||||
```
|
||||
|
||||
**何时用哪条路径:**
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
| Recipe | 步骤(命令均须 `--format json`,下略) |
|
||||
|--------|----------------------------------------|
|
||||
| `create-priority-todo` | 1. 确定执行者(同 [SKILL.md](../../SKILL.md) 中 `create-todo` 步骤 1)<br>2. `todo task create --title "<标题>" --executors <userId>[,<userId2>...] --priority <10/20/30/40>`(可选 `--due "<截止ISO>"`;10低/20普通/30较高/40紧急)→ 取 `todoTaskId` |
|
||||
| `create-recurring-todo` | 1. 确定执行者(同 `create-todo` 步骤 1)<br>2. `todo task create --title "<标题>" --executors <userId> --due "<首次截止ISO>" --recurrence "DTSTART:<UTC时间>\nRRULE:FREQ=DAILY;INTERVAL=1"`(`--due` 必填;仅支持按天循环,见 [todo.md](../products/todo.md))→ 取 `todoTaskId` |
|
||||
| `reschedule-todo` | 1. `todo task list --status false` → 取 `todoTaskId`<br>2. `todo task update --task-id <todoTaskId> --due "<新截止时间>"` |
|
||||
| `create-priority-todo` | 1. 确定执行者(同 [SKILL.md](../../SKILL.md) 中 `create-todo` 步骤 1)<br>2. `todo task create --title "<标题>" --executors <userId>[,<userId2>...] --priority <10/20/30/40>`(可选 `--due "<截止ISO>"`;10低/20普通/30较高/40紧急)→ 取 `taskId` |
|
||||
| `create-recurring-todo` | 1. 确定执行者(同 `create-todo` 步骤 1)<br>2. `todo task create --title "<标题>" --executors <userId> --due "<首次截止ISO>" --recurrence "DTSTART:<UTC时间>\nRRULE:FREQ=DAILY;INTERVAL=1"`(`--due` 必填;仅支持按天循环,见 [todo.md](../products/todo.md))→ 取 `taskId` |
|
||||
| `reschedule-todo` | 1. `todo task list --status false` → 取 `taskId`<br>2. `todo task update --task-id <taskId> --due "<新截止时间>"` |
|
||||
|
||||
## Full / 组合(固定路线)
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
|--------|---------------------|
|
||||
| generate-progress-report | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行<br>2. 交叉比对各源数据<br>3. `doc create --name "<报告名>" --content "<报告内容>"` |
|
||||
| batch-create-todo | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行 → 从结果提取任务条目<br>2. 每条:`aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`<br>3. **优先**:将待办写入 `todos.json`(格式见 [todo_batch_create.py](../../scripts/todo_batch_create.py)),执行 `python scripts/todo_batch_create.py todos.json`<br>备选:逐条 `todo task create --title "<标题>" --executors <userId>` → 汇总回显<br>**单批超 30 条须用户确认** |
|
||||
| assign-and-notify | 1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`<br>2. `todo task create --title "<标题>" --executors <userId>` → 取 `todoTaskId`<br>3. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message send --group <openConversationId> --text "<通知内容>"` 通知 |
|
||||
| assign-and-notify | 1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`<br>2. `todo task create --title "<标题>" --executors <userId>` → 取 `taskId`<br>3. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message send --group <openConversationId> --text "<通知内容>"` 通知 |
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|--------|-------------------|
|
||||
| read-aitable | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable field get --base-id <baseId> --table-id <tableId>` → 取 `fieldId`<br>3. **取记录(按场景分流)**:<br> • 数据统计/分析/全量汇总 → `aitable record query --base-id <baseId> --table-id <tableId> --all`(自动翻页,**禁止凭单页数据做统计**)<br> • 大表保险 → 加 `--page-limit 100`(默认 50 页/5000 条,0 = 无限制)<br> • 单纯预览前几条 → `aitable record query --base-id <baseId> --table-id <tableId> --limit 30`(不加 --all)<br> • 筛选时 `--filters` 格式见 [aitable-filter-sort.md](../products/aitable/aitable-filter-sort.md)<br>4. **检查输出契约**:`hasMore=true` 时数据被截断,必须用 `--cursor <X>` 续拉;`partial=true` 时表示中途某页失败(保留已拉数据,可重试)<br>5. 总结数据 |
|
||||
| generate-data-report | 1. 同 read-aitable 步骤 1-3(**必须用 --all 防漏数据**)<br>2. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行 → 补充背景<br>3. `doc create --name "<报告名>" --content "<分析报告>"` |
|
||||
| create-aitable-record | **写入路径分流**(关键决策):<br> • **追加到已有表**(场景:把这批数据加到「成员表」)→ `python scripts/import_records.py <baseId> <tableId> data.csv\|data.json [batch_size]`(走 `record create`,需要列名匹配字段名)<br> • **新建表**(场景:把这个 Excel 变成多维表)→ `python scripts/aitable_import_via_task.py <baseId> <file>` 或 `dws aitable import upload --base-id <baseId> --file ./x.xlsx` + `dws aitable import data --import-id <ID>`(CLI 已内置 OSS PUT 清空 Content-Type + 同步轮询,**禁止自己写 PUT**)<br> • **单条/少量手动**:1. `aitable base search` → `baseId`/`tableId`;2. `aitable field get` → `fieldId` 与类型;3. `aitable record create --records '[{"cells":{"<fieldId>":"值"}}]'` |
|
||||
| create-aitable-record | **写入路径分流**(关键决策):<br> • **追加到已有表**(场景:把这批数据加到「成员表」)→ `python scripts/import_records.py <baseId> <tableId> data.csv\|data.json [batch_size]`(走 `record create`,需要列名匹配字段名)<br> • **新建表**(场景:把这个 Excel 变成多维表)→ `python scripts/aitable_import_via_task.py <baseId> <file>`(脚本内置 prepare→OSS PUT→import data 全流程和正确的头处理,**禁止自己写 PUT**)。注意 `aitable import upload` 没有 `--file` flag、也不代做 PUT,只准备导入;能一站式完成的是上面的脚本<br> • **单条/少量手动**:1. `aitable base search` → `baseId`/`tableId`;2. `aitable field get` → `fieldId` 与类型;3. `aitable record create --records '[{"cells":{"<fieldId>":"值"}}]'` |
|
||||
| update-aitable-record | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. **取目标 record**:<br> • 已知少量 recordId → `aitable record query --record-ids <ID1,ID2>`<br> • 按条件批量改 → `aitable record query --base-id <baseId> --table-id <tableId> --filters '<JSON>' --all`(**用 --all 防止漏改**)<br>3. **先展示让用户确认要改的 record 列表**<br>4. `aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<recordId>","cells":{...}}]'`(单次 ≤30 条) |
|
||||
| search-aitable-template | 1. `aitable template search --query "<关键词>"` → 取 `templateId`<br>2. 用户选定<br>3. `aitable base create --name "<表格名>" --template-id <templateId>` → 取 `baseId` |
|
||||
| export-aitable-to-xlsx | 1. `aitable base search --query "<表格名>"` → 取 `baseId`<br>2. **按场景选 scope**:<br> • 全表+附件 → `aitable export data --base-id <baseId> --scope all --export-format excel_and_attachment --output ./<name>.xlsx`<br> • 单表(仅 xlsx)→ `--scope table --table-id <tableId> --export-format excel`<br> • 单视图 → `--scope view --table-id <tableId> --view-id <viewId>`<br>3. CLI 内置渐进式退避轮询 + 自动落盘,**不要自己写 GET downloadUrl**<br>4. 大表超时(默认 5 分钟):加 `--timeout-sec 900` 或拿到 `taskId` 后 `aitable export data --task-id <ID> --output ./<name>.xlsx` 续等<br>5. 与悟空脚本路径并存:复杂场景(多 base 批量 / 按视图组合)请用 `python scripts/aitable_export_via_task.py <baseId> --scope all\|table\|view` |
|
||||
|
||||
@@ -24,22 +24,22 @@
|
||||
| Recipe | 步骤 |
|
||||
|--------|------|
|
||||
| `mail-get` | `mail message get --email <邮箱> --id <messageId>` → 查看邮件完整内容(含正文) |
|
||||
| `mail-folder-list` | `mail folder list --email <邮箱>` → 列举文件夹;`--folder-id <id>` 查子文件夹 |
|
||||
| `mail-folder-list` | `mail folder list --email <邮箱>` → 列举文件夹;`--folder <id>` 查子文件夹 |
|
||||
| `mail-tag-list` | `mail tag list --email <邮箱>` → 列举邮件标签 |
|
||||
| `mail-thread-get` | `mail thread get --email <邮箱> --id <conversationId>` → 获取会话(邮件线程)详情 |
|
||||
| `mail-attachment-list` | `mail attachment list --email <邮箱> --id <messageId>` → 列举指定邮件的附件 |
|
||||
| `mail-attachment-download` | 1. `mail attachment list --email <邮箱> --id <messageId>` → 取附件 `id` 和 `name`<br>2. `mail attachment download --email <邮箱> --message-id <messageId> --attachment-id <attachmentId> --name <文件名>` |
|
||||
| `mail-batch-move` | `mail message batch-move --email <邮箱> --ids <id1,id2,...> --folder <folderId>`(常用 folderId: 2=收件箱, 6=已删除) |
|
||||
| `mail-batch-delete` | `mail message batch-delete --email <邮箱> --ids <id1,id2,...> --yes`(**危险操作,须先确认**) |
|
||||
| `mail-draft-create` | `mail draft create --from <邮箱> --subject "<标题>"` → 取 `messageId`(可选 `--to`、`--body`、`--cc`) |
|
||||
| `mail-draft-update` | `mail draft update --from <邮箱> --id <draftId> --subject "<新标题>"`(可选 `--body`、`--to`、`--cc`) |
|
||||
| `mail-draft-create` | `mail draft create --from <邮箱> --subject "<标题>"` → 取草稿 ID `result.message.id`(可选 `--to`、`--content`、`--cc`) |
|
||||
| `mail-draft-update` | `mail draft update --from <邮箱> --id <draftId> --subject "<新标题>"`(可选 `--content`、`--to`、`--cc`) |
|
||||
| `mail-draft-send` | `mail draft send --from <邮箱> --id <draftId>` |
|
||||
|
||||
## Full / 多步组合
|
||||
|
||||
| Recipe | 行动指南(固定路线) |
|
||||
|--------|---------------------|
|
||||
| search-and-download-attachment | 1. `mail mailbox list` → 取邮箱<br>2. `mail message search --email <邮箱> --query "<KQL>" --size 20` → 取 `messageId` 列表<br>3. 对每封邮件执行 `mail attachment list --email <邮箱> --id <messageId>` → 列出附件取 `id` 和 `name`<br>4. 对每个附件逐个执行 `mail attachment download --email <邮箱> --message-id <messageId> --attachment-id <attachmentId> --name <文件名>`(**仅支持逐个下载,不存在批量下载命令**) |
|
||||
| search-reply-forward | 1. `mail mailbox list` → 取邮箱<br>2. `mail message search --email <邮箱> --query "<KQL>" --size 10` → 取 `messageId`<br>3. 展示搜索结果供用户选择<br>4. 按用户指示执行 reply / reply-all / forward(参见 lite `mail-reply-forward`) |
|
||||
| batch-mail-cleanup | 1. `mail mailbox list` → 取邮箱<br>2. `mail message search --email <邮箱> --query "<KQL>" --size 100` → 取多个 `messageId`<br>3. 展示列表供用户确认<br>4. `mail message batch-move --email <邮箱> --ids <id1,id2,...> --folder 6 ` 移到已删除;或 `batch-delete` 永久删除 |
|
||||
| send-to-person-by-name | 1. `mail mailbox list` → 取发件邮箱<br>2. 走「查找他人邮箱地址」三路并发查询获取收件人邮箱(见 [mail.md](../products/mail.md))<br>3. `mail message send --from <发件邮箱> --to <收件邮箱> --subject "<标题>" --body "<内容>"` |
|
||||
| search-and-download-attachment | 1. `mail mailbox list` → 取邮箱<br>2. `mail message search --email <邮箱> --query "<KQL>" --limit 20` → 取 `messageId` 列表<br>3. 对每封邮件执行 `mail attachment list --email <邮箱> --id <messageId>` → 列出附件取 `id` 和 `name`<br>4. 对每个附件逐个执行 `mail attachment download --email <邮箱> --message-id <messageId> --attachment-id <attachmentId> --name <文件名>`(**仅支持逐个下载,不存在批量下载命令**) |
|
||||
| search-reply-forward | 1. `mail mailbox list` → 取邮箱<br>2. `mail message search --email <邮箱> --query "<KQL>" --limit 10` → 取 `messageId`<br>3. 展示搜索结果供用户选择<br>4. 按用户指示执行 reply / reply-all / forward(参见 lite `mail-reply-forward`) |
|
||||
| batch-mail-cleanup | 1. `mail mailbox list` → 取邮箱(返回字段是 `emailAccounts`)<br>2. `mail message search --email <邮箱> --query "<KQL>" --limit 100` → 取多个 `messageId`<br>3. 展示列表供用户确认<br>4. `mail message batch-move --email <邮箱> --ids <id1,id2,...> --folder 6 ` 或 `batch-delete` **都是移入「已删除」文件夹,并非物理永久删除**(对已在已删除文件夹的邮件再执行返回 success 但无效;CLI 无永久删除路径,需在客户端手动清空) |
|
||||
| send-to-person-by-name | 1. `mail mailbox list` → 取发件邮箱<br>2. 走「查找他人邮箱地址」三路并发查询获取收件人邮箱(见 [mail.md](../products/mail.md))<br>3. `mail message send --from <发件邮箱> --to <收件邮箱> --subject "<标题>" --content "<内容>"` |
|
||||
|
||||
@@ -249,7 +249,7 @@ query 未提"听记"但任务产出依赖会议讨论内容时(报告/总结/
|
||||
- 按文件夹:`folderId:2`(2=收件箱, 1=已发送, 5=草稿, 6=已删除)
|
||||
- 按是否有附件:`hasAttachments:true`
|
||||
- 组合:`from:alice AND subject:周报 AND date>2025-06-01T00:00:00Z`
|
||||
3. 执行搜索:`mail message search --email <邮箱> --query "<KQL表达式>" --size 20`
|
||||
3. 执行搜索:`mail message search --email <邮箱> --query "<KQL表达式>" --limit 20`
|
||||
4. 查看详情(按需):`mail message get --email <邮箱> --id <messageId>`
|
||||
|
||||
### mail-send
|
||||
@@ -258,7 +258,7 @@ query 未提"听记"但任务产出依赖会议讨论内容时(报告/总结/
|
||||
|
||||
1. 获取邮箱地址:`mail mailbox list` → 取用户邮箱作为 `--from`。
|
||||
2. 确定收件人:用户直接提供邮箱地址 → 直接使用;用户提供姓名 → 走「查找他人邮箱地址」三路并发流程(见 [mail.md](../products/mail.md))。
|
||||
3. 发送:`mail message send --from <发件邮箱> --to <收件邮箱> --subject "<主题>" --body "<正文>"`(可选 `--cc`、`--attachment`、`--inline-attachment`)。
|
||||
3. 发送:`mail message send --from <发件邮箱> --to <收件邮箱> --subject "<主题>" --content "<正文>"`(正文的规范 flag 是 `--content`,`--body` 是隐藏别名;可选 `--cc`、`--attachment`、`--inline-attachment`)。
|
||||
|
||||
### mail-reply-forward
|
||||
|
||||
@@ -267,6 +267,6 @@ query 未提"听记"但任务产出依赖会议讨论内容时(报告/总结/
|
||||
1. 获取邮箱地址:`mail mailbox list` → 取用户邮箱。
|
||||
2. 定位原始邮件:若用户未提供 messageId → 先用 `mail-search` 搜索定位。
|
||||
3. 执行:
|
||||
- 回复:`mail message reply --from <邮箱> --id <messageId>`(可选 `--to`、`--subject`、`--body`)
|
||||
- 回复全部:`mail message reply-all --from <邮箱> --id <messageId>`(可选 `--to`、`--subject`、`--body`)
|
||||
- 转发:`mail message forward --from <邮箱> --to <收件邮箱> --id <messageId>`(可选 `--subject`、`--body`)
|
||||
- 回复:`mail message reply --from <邮箱> --id <messageId>`(可选 `--to`、`--subject`、`--content`;正文 flag 规范名 `--content`,`--body` 为别名)
|
||||
- 回复全部:`mail message reply-all --from <邮箱> --id <messageId>`(可选 `--to`、`--subject`、`--content`)
|
||||
- 转发:`mail message forward --from <邮箱> --to <收件邮箱> --id <messageId>`(可选 `--subject`、`--content`)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
| 不支持的操作 | 说明 |
|
||||
|------------|------|
|
||||
| 创建公式/查找引用等高级字段类型 | 部分高级字段类型暂不支持 API 创建 |
|
||||
| 自己 PUT 文件时 Content-Type 不为空 | OSS 签名机制要求 PUT 请求的 `Content-Type` 头**必须清空**,否则返回 `SignatureDoesNotMatch` / HTTP 403。这**不是 dws 限制**,是阿里云 OSS 行为。解决:`dws aitable import upload --file ./x.xlsx` 已内置正确处理;自己写 `curl` 时必须传 `-H "Content-Type:"`(注意冒号后是空值) |
|
||||
| 自己 PUT 文件到 OSS 时 Content-Type 处理 | OSS 签名对 PUT 请求的 `Content-Type` 头有严格要求,处理不当会 `SignatureDoesNotMatch` / HTTP 403。这**不是 dws 限制**,是阿里云 OSS 行为。解决:直接用 `python scripts/aitable_import_via_task.py <baseId> <file>`,脚本已内置 prepare→PUT→import data 全流程和正确的头处理,**不要自己写 curl PUT**。(注意:`aitable import upload` 命令没有 `--file` flag、也不代做 PUT,只准备导入;能自动 PUT 的是上面的脚本。) |
|
||||
|
||||
## minutes
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ dws recovery finalize --event-id <event_id> --outcome recovered|failed|handoff -
|
||||
| 标志 | 短名 | 说明 | 默认 |
|
||||
|------|:---:|------|------|
|
||||
| `--format` | `-f` | 输出格式: json / table / raw | json |
|
||||
| `--jq` | | jq 表达式过滤输出 (如: `.items[] \| .name`) | 无 |
|
||||
| `--fields` | | 筛选输出字段 (逗号分隔, 如: name,id,status) | 无 |
|
||||
| `--jq` | | jq 表达式过滤输出(如 `.result[].name`)。对产品命令(aitable/chat/mail/... 走 MCP 的)已生效;少数工具命令(auth/config/profile/doctor/schema 等)仍直接编码、暂不过滤 | 无 |
|
||||
| `--fields` | | 筛选输出字段(逗号分隔)。按**顶层信封键**(data/result/success/status…)或**列表元素字段**投影;取 data 内的嵌套字段(如 baseName)请改用 `--jq '.data.baseName'`,`--fields baseName` 会因顶层无此键返回 `{}`。同 `--jq`:产品命令已生效,个别工具命令暂不生效 | 无 |
|
||||
| `--verbose` | `-v` | 详细日志 | false |
|
||||
| `--debug` | | 调试日志 | false |
|
||||
| `--yes` | `-y` | 跳过确认提示 | false |
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
| "改群昵称/设置群昵称/我在群里的名字" | 设置个人群昵称 | `chat group update-nick` | `chat group rename` | update-nick 改自己的群昵称;rename 改群名称 |
|
||||
| "群备注/给群加备注/修改群备注" | 设置群备注 | `chat group update-alias` | `chat group rename` | update-alias 设置仅自己可见的备注;rename 改群名称全员可见 |
|
||||
| "隐藏会话/隐藏群聊/隐藏对话" | 隐藏会话 | `chat hide` | `chat mute` | hide 隐藏会话不显示;mute 是免打扰但仍显示 |
|
||||
| "关闭@所有人通知/屏蔽@all/不接收@所有人" | 关闭 @所有人提醒 | `chat mute-at-all` | `chat mute` | mute-at-all 仅屏蔽 @所有人;mute 是整个会话免打扰 |
|
||||
| "关闭红包通知/屏蔽红包/不接收红包提醒" | 关闭红包提醒 | `chat mute-red-envelope` | `chat mute` | mute-red-envelope 仅屏蔽红包;mute 是整个会话免打扰 |
|
||||
| "关闭@所有人通知/屏蔽@all/不接收@所有人" | 关闭 @所有人提醒 | `chat mute-at-all` | `chat mute` | mute-at-all 仅屏蔽 @所有人;mute 是整个会话免打扰。⚠️ 当前服务端稳定返回 1002「系统繁忙」,命令暂不可用,先告知用户到钉钉客户端设置 |
|
||||
| "关闭红包通知/屏蔽红包/不接收红包提醒" | 关闭红包提醒 | `chat mute-red-envelope` | `chat mute` | mute-red-envelope 仅屏蔽红包;mute 是整个会话免打扰。⚠️ 同 mute-at-all,服务端稳定 1002,暂不可用 |
|
||||
| "解散群/解散群聊" | 解散群聊 | `chat group dismiss` | `chat group quit` | dismiss 是群主解散整个群(不可逆);quit 是当前用户自己退群 |
|
||||
| "新成员看历史/历史消息可见范围" | 设置新成员可见历史消息 | `chat group set-history` | `chat group update-settings` | set-history 控制新成员入群后可见历史消息范围;update-settings 是其他群功能开关 |
|
||||
| "群里有哪些机器人/查看群机器人/列出群机器人" | 查看群内机器人列表 | `chat group bots` | `chat group members` | bots 只列机器人;members 列普通群成员 |
|
||||
@@ -160,7 +160,7 @@ alidocs 链接表面长得一样(`https://alidocs.dingtalk.com/i/nodes/{id}`
|
||||
用 `sheet` 的场景(axls,钉钉在线电子表格):
|
||||
- `dws doc info --node <URL>` 返回 `contentType=ALIDOC` + `extension=axls`
|
||||
- 用户在钉钉文档空间直接"新建电子表格"得到的节点
|
||||
- 所有 sheet 子命令(`list` / `range read` / `range write` / `export` 等)仅服务这类节点
|
||||
- 所有 sheet 子命令(`list` / `range read` / `range update` / `export` 等)仅服务这类节点
|
||||
|
||||
用 `dws drive download` 的场景(xlsx / xls / xlsm / csv 本地表格文件):
|
||||
- `dws doc info --node <URL>` 返回 `contentType=DOCUMENT` + `extension=xlsx` / `xls` / `xlsm` / `csv`
|
||||
@@ -273,8 +273,9 @@ alidocs 链接表面长得一样(`https://alidocs.dingtalk.com/i/nodes/{id}`
|
||||
- 已有 userId 时直接使用 `--user`;已有 openDingTalkId 时使用 `--open-dingtalk-id`
|
||||
- 纯文本/Markdown 单聊传 `--user` 时直接走 userId 发送能力,不需要先手动查询 openDingTalkId
|
||||
- 富媒体消息(image/file)单聊优先使用 `--open-dingtalk-id`;传 `--user` 时 CLI 会尝试解析为 openDingTalkId 后发送
|
||||
- "发张图片/截图/语音/视频/文件到群里" / "发张图给某某" — **统一一条命令**:`dws chat message send ... --msg-type file --file-path <本地路径>`,CLI 内部自动上传并发送,**任意扩展名(png/jpg/pdf/mp4/zip…)都走这条**
|
||||
- "发图片+文字说明" — 不要硬塞进一条命令;先发文件消息再补一条 `--text "..."` 即可
|
||||
- "发文件/语音/视频到群里" — `dws chat message send ... --msg-type file --file-path <本地路径>`,CLI 内部自动上传并发送(png/jpg/pdf/mp4/zip… 任意扩展名都走这条,但**都作为「文件」消息**发出,接收方看到的是可下载的文件条目)
|
||||
- "发张图片/截图(要在聊天里内联渲染成图,不是文件)" — 走图片消息链路:先 `dt_media_upload` 拿 mediaId,再 `dws chat message send ... --msg-type image --media-id <mediaId>`。**注意**:用 `--msg-type file` 发 .png 只会显示为[文件](fileId),不会渲染成图片;要图片效果必须走 `--msg-type image`
|
||||
- "发图片+文字说明" — 不要硬塞进一条命令;先发图片/文件消息再补一条 `--text "..."` 即可
|
||||
|
||||
```bash
|
||||
dws chat message send --group <openConversationId> --msg-type file --file-path ./screenshot.png --format json
|
||||
@@ -345,7 +346,7 @@ dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file
|
||||
**用 `oa approval` 的场景**(通用 OA 审批中心):
|
||||
- "我的待审批 / 待办审批 / 已审批记录" — `oa approval list-pending`
|
||||
- "某审批单详情 / 同意或驳回审批 / 撤销我发起的审批" — `oa approval detail/approve/reject/revoke`
|
||||
- "查业务审批记录 / 审批转交 / 添加评论与抄送" — `oa approval records/transfer/comment/cc`
|
||||
- "查业务审批记录 / 审批转交 / 添加评论与抄送" — `oa approval records`(操作记录)/ `redirect-task`(转交)/ `oa-comments`(评论)/ `oa-cc-noticer`(抄送)
|
||||
- 用户提到"报销 / 采购 / 用印 / 合同 等非考勤类审批"
|
||||
|
||||
**判断关键**:
|
||||
@@ -353,7 +354,7 @@ dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file
|
||||
- 不限于考勤业务、面向"我上下游的审批任务"表述 → `oa approval`
|
||||
|
||||
**提交审批单的边界**:
|
||||
- 提交考勤审批单走 `attendance approve templates --type leave|overtime|repair-check`(当前仅支持请假/加班/补卡三类模板;出差/外出的提交模板暂不支持,查询仍可用 `approve list --types trip` 覆盖),命令会返回审批表单的 submitUrl 跳转链接,由用户点击链接跳转到钉钉客户端的提交页面完成填写与提交。**展示链接时必须用 Markdown 可点击格式 `[表单名称](submitUrl)`,不要裸露 URL**。
|
||||
- 提交考勤审批单走 `attendance approve templates --type leave|overtime|repair-check|travel`(请假/加班/补卡/出差外出,出差外出用 `--type travel`,返回 approveType=TRAVEL/OUT 的模板;具体哪几类有模板取决于组织配置,返回空列表即该组织未配该类),命令会返回审批表单的 submitUrl 跳转链接,由用户点击链接跳转到钉钉客户端的提交页面完成填写与提交。**展示链接时必须用 Markdown 可点击格式 `[表单名称](submitUrl)`,不要裸露 URL**。
|
||||
- 提交诉求的辅助查询:可用假期余额走 `attendance vacation balance`、历史已提交记录走 `attendance approve list`。
|
||||
- 任何场景下都**不要误用 `oa approval` 代替** —— 该命令组只能查/审/撤已存在的审批单,考勤业务审批单走考勤自己的逻辑便于区分。
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ Flags:
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `aisearch person` | `userId`(用户ID)、`title`(姓名) | 展示搜索结果、后续操作(发消息/建待办等) |
|
||||
| `aisearch person` | `userId`(用户ID,= `meta.staffId`/`meta.jobNumber`)、`author`(真实姓名,= `meta.name`);`title` 是花名/显示名,不一定等于姓名 | 展示搜索结果、后续操作(发消息/建待办等) |
|
||||
|
||||
## 重名消歧
|
||||
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `base list` | 列出最近访问的 Base | — | 仅返回最近访问过的,优先用 `base search` |
|
||||
| `base search` | 按名称搜索 Base | `--query` | 关键词 ≥2 字符 |
|
||||
| `base search` | 按名称搜索 Base(别名 `aitable search`) | — | `--query` help 标必填但实际可省略:不传时返回最近访问的 Base 列表。`--keyword` 是 `--query` 的隐藏别名,同义 |
|
||||
| `base get` | 获取 Base 信息(含 tables 列表) | `--base-id` | 用户给 URL 时提取末尾 ID |
|
||||
| `base copy` | 复制整个 Base 到目标文件夹 | `--base-id` `--target-folder-id` | 默认全量复制;`--only-struct` 仅复制结构不含数据 |
|
||||
| `base get-primary-doc-id` | 获取某记录的主键文档 ID | `--base-id` `--table-id` `--record-id` | 等价 `record primary-doc-get` 的取 ID 视角 |
|
||||
| `base create` | 创建 Base | `--name` | 创建后直接用返回的 baseId |
|
||||
| `base update` | 更新 Base 名称 | `--base-id` `--name` | — |
|
||||
| `base delete` | 删除 Base | `--base-id` | 不可逆 |
|
||||
@@ -30,7 +32,8 @@
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `table get` | 获取表结构(字段+视图目录) | `--base-id` | 不传 `--table-ids` 返回全部表 |
|
||||
| `table create` | 创建数据表 | `--base-id` `--name` `--fields` | fields 为 JSON 数组,至少 1 个 |
|
||||
| `table list` | 获取数据表(`table get` 的别名) | `--base-id` | 与 `table get` 等价 |
|
||||
| `table create` | 创建数据表 | `--base-id` `--name` | `--fields` 为 JSON 数组;**可传空数组 `[]`**(默认值即 `[]`),此时服务端自动补一个名为"标题"的 primaryDoc 首列;单次最多 15 个字段 |
|
||||
| `table update` | 重命名表 | `--base-id` `--table-id` `--name` | — |
|
||||
| `table delete` | 删除表 | `--base-id` `--table-id` | 不可逆 |
|
||||
|
||||
@@ -39,19 +42,29 @@
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `field get` | 获取字段完整配置 | `--base-id` `--table-id` | 按需展开少量字段 |
|
||||
| `field list` | 获取字段信息(`field get` 的别名) | `--base-id` `--table-id` | 与 `field get` 等价 |
|
||||
| `field create` | 创建字段 | `--base-id` `--table-id` + (`--name --type` 或 `--fields`) | 支持单字段/批量模式 |
|
||||
| `field update` | 更新字段名/配置 | `--base-id` `--table-id` `--field-id` | 不可变更字段类型 |
|
||||
| `field delete` | 删除字段 | `--base-id` `--table-id` `--field-id` | 不可逆 |
|
||||
| `field search-options` | 搜索单选/多选字段的选项 | `--base-id` `--table-id` `--field-id` | 仅 singleSelect/multipleSelect;`--keyword` 模糊过滤,不传返回全部 |
|
||||
|
||||
### record (记录管理)
|
||||
|
||||
| 命令 | 用途 | 必读 reference | 路由提醒 |
|
||||
|------|------|----------------|----------|
|
||||
| `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference |
|
||||
| `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference;`--query`(隐藏别名 `--keyword`)全文搜索 |
|
||||
| `record list` | 获取记录(`record query` 的别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 与 `record query` 等价 |
|
||||
| `record get` | 按 ID 取记录(`record query --record-ids` 的窄别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 已知 recordId 时首选;必填 `--record-ids`(单次最多 100 条);未暴露 filters/sort/query/cursor/limit |
|
||||
| `record query-empty` | 查询完全没填用户字段的空行 | — | `--base-id` `--table-id`;`--limit` 扫描预算 [1,100],`--cursor` 翻页 |
|
||||
| `record create` | 新增记录 | [aitable-record-create.md](./aitable/aitable-record-create.md) | cells key 必须是 fieldId 不是字段名;单次最多 100 条 |
|
||||
| `record update` | 更新记录 | [aitable-record-update.md](./aitable/aitable-record-update.md) | 需先 query 拿 recordId;只传需改字段;**没有** `--record-id` `--cells` flag |
|
||||
| `record batch-update` | 把同一份 cells 批量应用到多条记录 | [aitable-record-update.md](./aitable/aitable-record-update.md) | `--record-ids`(≤100)+ `--cells` 共享 patch |
|
||||
| `record upsert` | 批量创建或更新(有 recordId 走更新,无则创建) | [aitable-record-upsert.md](./aitable/aitable-record-upsert.md) | `--records`/`--records-file`;单次最多 100 条 |
|
||||
| `record delete` | 删除记录 | [aitable-record-delete.md](./aitable/aitable-record-delete.md) | 不可逆,需先 query 确认 |
|
||||
| `record share-url` | 批量获取记录分享链接 | [aitable-record-share.md](./aitable/aitable-record-share.md) | `--record-ids`(逗号分隔,单次最多 20 条) |
|
||||
| `record history-list` | 查询单条记录的变更历史 | [aitable-record-history.md](./aitable/aitable-record-history.md) | `--record-id` 单条;`--offset`/`--limit`(≤50) |
|
||||
| `record primary-doc-get` | 查询记录的主键文档 nodeId | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 无文档时返回 `no record` 错误 |
|
||||
| `record primary-doc-create` | 为记录创建主键文档 | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 幂等;`--field-id` 须 primaryDoc 类型 |
|
||||
|
||||
### view (视图管理)
|
||||
|
||||
@@ -264,8 +277,9 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
||||
要点:
|
||||
|
||||
- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用。
|
||||
- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错。
|
||||
- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断。
|
||||
- `dashboard share get` 可能返回 `404`(`retryable:true`,从未分享甚至刚开启分享后立即查都可能 404),按可重试错误处理,不要误判为参数拼错;行为不稳定,别当"是否已分享"的唯一判据。
|
||||
- `chart share get` 稳定返回 `success + data`(含 `enabled`),从未分享时 `enabled=false`,不会 404。
|
||||
- `dashboard share update` 开 ORG 分享后 `shareType` 回显 `"[1]"`(服务端已知问题);`chart share update` 正确回显 `ORG`。详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)。
|
||||
|
||||
### 导出数据(两阶段轮询)
|
||||
|
||||
|
||||
@@ -38,8 +38,10 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
dws aitable attachment upload --base-id <BASE_ID> --file-name report.pdf --size 204800 --format json
|
||||
# → 返回 uploadUrl、fileToken
|
||||
|
||||
# 2. PUT 上传(Content-Type 留空)
|
||||
curl -X PUT "<uploadUrl>" -H "Content-Type:" --data-binary @report.pdf
|
||||
# 2. PUT 上传(Content-Type 必须与文件类型一致,不能留空)
|
||||
# 留空或用 curl 默认的 application/x-www-form-urlencoded 都会被 OSS 拒为 403
|
||||
curl -X PUT "<uploadUrl>" -H "Content-Type: application/pdf" --data-binary @report.pdf
|
||||
# 例:.txt → text/plain,.png → image/png,.xlsx → application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
|
||||
# 3. 写入记录
|
||||
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
|
||||
@@ -15,8 +15,10 @@ 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 share get` 可能返回 `404`(`retryable:true`)——从未分享、甚至刚开启分享后立即查都可能 404;`data` 为 `{}`。按可重试错误处理,不要误判为参数拼错。它有时也会返回 `success + data`(如关闭分享之后),行为不稳定,别用它当"是否已分享"的唯一判据。
|
||||
- `chart share get` 稳定返回 `success + data`(含 `enabled` 等),可用于分享状态判断;从未分享时 `enabled=false`,不会 404。
|
||||
- ⚠️ **`dashboard share update` 开启 ORG 分享后返回的 `shareType` 是 `"[1]"`(未映射回 `ORG`,服务端已知问题)**;`chart share update --share-type ORG` 则正确返回 `shareType="ORG"`。判断 dashboard 是否 ORG 分享时对 `"[1]"` 做兼容。
|
||||
- `chart share update` / `dashboard share update` 的 `--enabled` 是字符串 flag:`--enabled false`(空格)和 `--enabled=false` 都能正确关闭分享。
|
||||
|
||||
## dashboard 子命令
|
||||
|
||||
@@ -25,18 +27,25 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
||||
| `dashboard get` | 获取仪表盘详情(含 charts 列表) | `--base-id` `--dashboard-id` | — |
|
||||
| `dashboard create` | 创建仪表盘 | `--base-id` + (`--config` 或 `--name`) | `--name` 简化版创建空看板;`--config` 传完整 JSON |
|
||||
| `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config` 或 `--name`) | `--name` 仅改名;`--config` 更新完整配置 |
|
||||
| `dashboard arrange` | 自动重排仪表盘图表布局 | `--base-id` `--dashboard-id` | 让服务端重新排布 charts 位置 |
|
||||
| `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` `--yes` | — |
|
||||
| `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 |
|
||||
| `dashboard share get` | 获取仪表盘分享配置 | `--base-id` `--dashboard-id` | 可能 404,见上方要点 |
|
||||
| `dashboard share update` | 更新仪表盘分享配置 | `--base-id` `--dashboard-id` `--enabled` | `--enabled true` 开启,`--share-type` 用 `ORG`(ORG 回显 `shareType="[1]"`);部分组织禁用了 `PUBLIC` 公开分享;若报 `Illegal argument`,改用 `ORG`(组织内分享)。`--enabled false` 关闭 |
|
||||
|
||||
## chart 子命令
|
||||
|
||||
| 命令 | 用途 | 必填参数 |
|
||||
|------|------|----------|
|
||||
| `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` |
|
||||
| `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` |
|
||||
| `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` |
|
||||
| `chart widgets-example` | 查看图表 widgets 配置模板 | 无 |
|
||||
| `chart share get` | 获取图表分享配置 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart share update` | 更新图表分享配置(`--enabled true/false`,开启配 `--share-type`,用 `ORG`;部分组织禁用了 `PUBLIC` 公开分享;报 `Illegal argument` 时改用 `ORG`) | `--base-id` `--dashboard-id` `--chart-id` `--enabled` |
|
||||
|
||||
> `chart create` 的 `--layout` 是**必填**(12 列网格布局,如 `{"x":0,"y":0,"w":6,"h":4}`);不传本地校验直接拒。`chart update` 的 `--config` 也**必填**——即便只想改 layout,也要带完整 config,否则服务端拒绝。
|
||||
|
||||
## 配置获取流程
|
||||
|
||||
|
||||
@@ -118,23 +118,25 @@ dws aitable import data --import-id <importId> --table-id <TABLE_ID> --format js
|
||||
|
||||
用户给文件让导入 AI 表格时,路径选择决定成败:
|
||||
|
||||
> ⚠️ **`import upload` 没有 `--file` flag**(传了会报 unknown flag)。它只申请上传凭证,必填 `--file-name` + `--file-size`,拿到 `uploadUrl` 后要**自己 curl PUT 上传文件**(Content-Type 留空,见上文三步流程),再 `import data`。想省事直接用 `aitable_import_via_task.py` 脚本,它把这三步包好了。
|
||||
|
||||
| 用户原话 | 链路 | 命令 / 脚本 | 行为 |
|
||||
|---------|------|------------|------|
|
||||
| "把这个 Excel 导入到 AI 表格"(无指定目标表) | **文件导入任务** | `python scripts/aitable_import_via_task.py <baseId> <file>` 或 `dws aitable import upload --base-id B --file ./x.xlsx` + `dws aitable import data --import-id <ID>` | 服务端解析文件,**新建数据表**,自动识别表头 |
|
||||
| "把这个 Excel 导入到 AI 表格"(无指定目标表) | **文件导入任务** | `python scripts/aitable_import_via_task.py <baseId> <file>`(推荐)或手动三步 `import upload --file-name x.xlsx --file-size <字节>` → curl PUT → `import data --import-id <ID>` | 服务端解析文件,**新建数据表**,自动识别表头 |
|
||||
| "把这个 Excel 导入新表 / 自动建表" | 同上 | 同上 | 同上 |
|
||||
| "把这批记录追加到已有的『成员表』里" | **记录批量写入** | `python scripts/import_records.py <baseId> <tableId> <file>` | 走 `record create`,**写入已有 tableId**,需要字段名匹配 |
|
||||
| "Excel 列名和表字段对不上但要追加" | 文件导入 + 追加 + 字段映射 | `dws aitable import upload --base-id B --file ./x.xlsx` → `dws aitable import data --import-id <ID> --table-id <TBL> --field-mapping '{"目标":"源"}'` | 服务端按映射追加 |
|
||||
| "Excel 列名和表字段对不上但要追加" | 文件导入 + 追加 + 字段映射 | 三步导入后 `import data --import-id <ID> --table-id <TBL> --field-mapping '{"目标":"源"}'` | 服务端按映射追加 |
|
||||
|
||||
## 大表 / 长任务超时续等
|
||||
|
||||
默认整体轮询超时 5 分钟。大表导出/导入超时后命令会返回 `taskId` / `importId`,用同命令带 ID 续等:
|
||||
单次等待窗口很短:`export data` 只有 `--timeout-ms`(默认且**上限 30000 = 30 秒**,没有 `--timeout-sec`,传了会报 unknown flag);`import data` 用 `--timeout`(秒,默认且推荐最大值 30)。窗口内没跑完,命令会返回 `taskId` / `importId`,用同命令带 ID 反复续等即可:
|
||||
|
||||
```bash
|
||||
# 续等导出
|
||||
dws aitable export data --base-id <B> --task-id <ID> --output ./out.xlsx
|
||||
# 续等导出:拿到 downloadUrl 后再 curl 下载(见下方警告)
|
||||
dws aitable export data --base-id <B> --task-id <ID> --timeout-ms 30000
|
||||
|
||||
# 续等导入
|
||||
dws aitable import data --import-id <ID>
|
||||
```
|
||||
|
||||
或一次性把 timeout 提到 15 分钟:`--timeout-sec 900`。
|
||||
> ⚠️ **`--output` 不会保存导出的 xlsx**:`--output` 是隐藏的全局 flag,作用是把命令的 **JSON 输出**写到文件,实测只生成一个 0 字节文件,不会下载导出内容。正确做法是从 `export data` 返回里取 `downloadUrl`,再 `curl -L "<downloadUrl>" -o out.xlsx` 下载。想省事直接用 `aitable_export_via_task.py` 脚本(它负责轮询 + 下载)。
|
||||
|
||||
@@ -50,8 +50,8 @@ dws aitable form share get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| `form list` | 列出表单视图 | `--base-id` `--table-id` | 返回 viewId/name/title/createdAt |
|
||||
| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 内部基于 list_form_views 过滤 |
|
||||
| `form list` | 列出表单视图 | `--base-id` `--table-id` | 每条返回 viewId/name;新建表单**无 title 且 createdAt=0**,改过(form update)后才出现 title 和真实 createdAt |
|
||||
| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 客户端按 viewId 过滤后 `data` **即该表单对象**(不是 formViews 数组);viewId 不存在返回 `form view ... not found` 错误 |
|
||||
| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | viewType=FormDesigner |
|
||||
| `form update` | 更新表单 | `--base-id` `--table-id` `--view-id` | `--title`/`--name`(等价)和 `--description` 至少传一项;同时传 title/name 时 title 优先 |
|
||||
| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` `--yes` | 不可逆 |
|
||||
@@ -115,6 +115,6 @@ dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VI
|
||||
|
||||
## 返回结构补充
|
||||
|
||||
- `form list` 返回 `data.formViews[]`,**每条仅含** `viewId/name/title/createdAt`;`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。
|
||||
- `form get` 返回结构与 `form list` 完全一致(`data.formViews[]`),仅含一条记录(与请求 viewId 一致)。Agent 提取时仍走 `data.formViews[0]`。
|
||||
- `form list` 返回 `data.formViews[]`,**每条含** `viewId/name`(+ `createdAt`);**新建表单没有 `title` 字段且 `createdAt=0`**,只有在 `form update` 碰过之后才会出现 `title` 和真实 `createdAt`。`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。
|
||||
- `form get` 的 `data` **就是命中的那一条表单对象**(如 `{viewId, name, createdAt, title?, shareFormUuid?}`),不是 `formViews` 数组。Agent 直接读 `data.viewId` / `data.name` 即可,**不要**再取 `data.formViews[0]`。服务端的 viewIds 过滤参数当前不生效,CLI 在客户端按 viewId 精确筛出单条;传了不存在的 viewId 会返回 `form view <id> not found in table` 错误。
|
||||
- `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`。
|
||||
|
||||
@@ -17,7 +17,9 @@ dws aitable record primary-doc-get --base-id BASE_ID --table-id TABLE_ID --recor
|
||||
- `--table-id`(必填):Table ID
|
||||
- `--record-id`(必填):Record ID
|
||||
|
||||
**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。若该记录尚未创建主键文档,`nodeId` 为 null。
|
||||
**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。
|
||||
|
||||
> **该记录尚未创建主键文档时**:不会返回 `nodeId: null`,而是 `status=error`、`data={}`,`error={code:"-1", message:"no record", type:"SYSTEM_ERROR", retryable:true}`。要判断"有没有主键文档",看是否命中这个 `no record` 错误,而不是判断 `nodeId` 是否为 null。需要文档时改用 `primary-doc-create`(幂等,已存在则直接返回)。
|
||||
|
||||
### 创建主键文档
|
||||
|
||||
|
||||
@@ -25,12 +25,11 @@ dws aitable record history-list \
|
||||
"data": {
|
||||
"histories": [
|
||||
{
|
||||
"type": "field_change", // 变更类型
|
||||
"action": "update", // 操作动作: create / update / delete
|
||||
"newValue": "{\"...\":\"...\"}", // 变更后的值(JSON 字符串)
|
||||
"oldValue": "{\"...\":\"...\"}", // 变更前的值(JSON 字符串)
|
||||
"type": "row", // 变更类型,实测均为 "row"(行级变更)
|
||||
"action": "updateRecords", // 操作动作:appendRow(新增行) / updateRecords(更新记录)
|
||||
"newValue": "{\"fldX\":{\"dataType\":\"STRING\",\"value\":\"改后值\"}}", // 变更后的值(JSON 字符串,按 fieldId 组织)
|
||||
"oldValue": "{\"fldX\":{\"dataType\":\"STRING\",\"value\":\"改前值\"}}", // 变更前的值(appendRow 新增行时无此字段)
|
||||
"operateTime": 1733123456789, // 操作时间(毫秒级时间戳)
|
||||
"typeChangedFields": "{...}", // 类型变更的字段信息(JSON 字符串)
|
||||
"version": 7 // 版本号(单调递增)
|
||||
}
|
||||
]
|
||||
@@ -38,14 +37,14 @@ dws aitable record history-list \
|
||||
}
|
||||
```
|
||||
|
||||
`newValue` / `oldValue` / `typeChangedFields` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值。
|
||||
`newValue` / `oldValue` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值;解析后是 `{fieldId: {dataType, value}}` 结构。`appendRow`(新增行)事件没有 `oldValue`。实测返回里**没有** `typeChangedFields` 字段。
|
||||
|
||||
## 字段含义速查
|
||||
|
||||
| 字段 | 用途 |
|
||||
|------|------|
|
||||
| `type` | 高层分类:`record_create` / `field_change` / `record_delete` 等。先按 type 过滤大类。 |
|
||||
| `action` | 三态:`create` / `update` / `delete`。比 type 粗,但便于按"动作"统计。 |
|
||||
| `type` | 实测均为 `row`(行级变更),服务端未按 `record_create` / `field_change` 细分。 |
|
||||
| `action` | 底层操作名:`appendRow`(新增行)/ `updateRecords`(更新记录)。按"动作"统计时以这两个值为准。 |
|
||||
| `version` | 单调递增整数;同一 record 越新值越大。**用作"上一条 vs 这一条"的稳定排序键**。 |
|
||||
| `operateTime` | 毫秒时间戳;可格式化成可读时间。多条同 version 的极端场景用 operateTime 兜底排序。 |
|
||||
|
||||
@@ -74,20 +73,22 @@ dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --
|
||||
|
||||
```bash
|
||||
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --format json \
|
||||
| jq '[.data.histories[] | select(.action == "update")][0].oldValue'
|
||||
| jq '[.data.histories[] | select(.action == "updateRecords")][0].oldValue'
|
||||
```
|
||||
|
||||
### 4. 找出删除事件(如果存在 delete history)
|
||||
### 4. 只看更新事件(排除新增行)
|
||||
|
||||
```bash
|
||||
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \
|
||||
| jq '.data.histories[] | select(.action == "delete") | {version, operateTime}'
|
||||
| jq '.data.histories[] | select(.action == "updateRecords") | {version, operateTime}'
|
||||
```
|
||||
|
||||
> 记录被 `record delete` 删除后,其历史不再返回(`histories` 为空数组),无法通过本命令回溯删除事件。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 一次只能查一条 record;如需批量审计多条记录请循环调用。
|
||||
- 仅返回**字段值变更**与**记录生命周期事件**;视图、字段定义、表结构变更不在此 history 里。
|
||||
- 仅返回**新增行**(appendRow)与**字段值变更**(updateRecords);记录被删除后其历史不再可查(返回空)。视图、字段定义、表结构变更不在此 history 里。
|
||||
- 历史保留时长由 server 决定,过老的记录可能不再返回。
|
||||
|
||||
## 与其他 record 命令的关系
|
||||
|
||||
@@ -79,21 +79,21 @@ dws aitable view update timebar --view-id GANTT_ID --official-holiday=true
|
||||
|
||||
### view update aggregate(仅 Grid)
|
||||
|
||||
值是 `map[fieldId]→AggregateAction string`;传 null 清除某个字段聚合。
|
||||
值是 `map[fieldId]→AggregateAction string`。**设置**聚合可用;**清除**聚合当前无效(见下方警告)。
|
||||
|
||||
| flag | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `--field-id` | string | 配合 `--action` 设置**单字段**聚合 |
|
||||
| `--action` | string | `SUM`/`AVG`/`MAX`/`MIN`/`MEDIAN`/`RANGE`/`TOTAL`/`DISTINCT`/`EXIST`/`UN_EXIST`/`CHECKED`/`EARLIEST_DATE` 等(按字段类型可用) |
|
||||
| `--clear-field-id` | string (CSV) | 一/多个字段 ID,清除其聚合 |
|
||||
| `--clear-field-id` | string (CSV) | 一/多个字段 ID,本意是清除其聚合,但**当前服务端不支持清除,静默无效**(见下方警告) |
|
||||
| `--json` | JSON | 完整 aggregate map |
|
||||
|
||||
```bash
|
||||
dws aitable view update aggregate --view-id GRID_ID --field-id fldX --action SUM
|
||||
dws aitable view update aggregate --view-id GRID_ID --clear-field-id fldA,fldB
|
||||
dws aitable view update aggregate --view-id GRID_ID --json '{"fldX":"AVG","fldY":null}'
|
||||
```
|
||||
|
||||
> ⚠️ **当前无法清除已设置的聚合(服务端限制)**:`--clear-field-id fldA,fldB` 与 `--json '{"fldX":null}'` 两种清除写法都返回 `success`,但用 `view get aggregate` 复核会发现聚合**原样不动**——是静默无效,不是真的清掉。这是服务端没有清除语义所致,直至服务端修复前不要依赖它。改聚合方式可行(重新 `--action` 覆盖成别的),只是无法回到"无聚合"。
|
||||
|
||||
### view update field-widths(仅 Grid)
|
||||
|
||||
| flag | 类型 |
|
||||
@@ -108,9 +108,11 @@ dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB
|
||||
|
||||
### view update visible-fields(通用)
|
||||
|
||||
整组替换可见字段列表与顺序。首列字段(primaryDoc)必须保留在数组第一位。
|
||||
整组替换可见字段列表与顺序,同时兼作**隐藏/显示**入口。首列字段(primaryDoc)必须保留在数组第一位。
|
||||
|
||||
> ⚠️ 注意:服务端**只接受 reorder,不接受真"隐藏字段"**——如果传入的列表比当前 columns 短,缺失的字段不会被隐藏。需要真正隐藏字段请到 AI 表格 Web UI。
|
||||
> **传入的列表既定顺序又定可见性**:传一个比当前 columns 短的列表,缺失的字段会被真正隐藏(该字段在 `view list` 的 `custom.hiddenFields` 里变 `true`);再传回全量列表即可解除隐藏(`hiddenFields` 变 `false`)。
|
||||
>
|
||||
> ⚠️ **查隐藏状态别看这里**:`view get visible-fields` 返回的数组**包含已隐藏字段**(列的完整顺序),看不出谁被隐藏。要确认隐藏状态,读 `view get`(view list)里该视图的 `custom.hiddenFields`(`{fieldId: true|false}`)。
|
||||
|
||||
| flag | 类型 |
|
||||
|------|------|
|
||||
@@ -118,7 +120,9 @@ dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB
|
||||
| `--json` | string 数组 JSON(与 `--field-ids` 同传时 `--json` 优先) |
|
||||
|
||||
```bash
|
||||
dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA,fldB
|
||||
# 只保留首列和 fldA,其余字段被隐藏
|
||||
dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA
|
||||
# 传回全量列表解除隐藏
|
||||
dws aitable view update visible-fields --view-id VIEW_ID --json '["fldPrimary","fldA","fldB"]'
|
||||
```
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ dws aitable view update frozen-cols --view-id VIEW_ID --count 0
|
||||
|
||||
# 查询当前冻结列数
|
||||
dws aitable view get frozen-cols --view-id VIEW_ID --format json
|
||||
# → {"data": {..., "count": 1}} count 为 null 表示视图未显式设置
|
||||
# → {"data": {..., "count": 1}} 未显式设置时整个 count 键缺失(不是返回 null)
|
||||
```
|
||||
|
||||
`--count` 必须 ≥ 0;负数会被拒绝。
|
||||
@@ -69,7 +69,7 @@ dws aitable view update row-height --view-id VIEW_ID --cell-height 56
|
||||
|
||||
# 查询当前行高
|
||||
dws aitable view get row-height --view-id VIEW_ID --format json
|
||||
# → {"data": {..., "cellHeight": 56}} cellHeight 为 null 表示视图未显式设置(前端按 32 渲染)
|
||||
# → {"data": {..., "cellHeight": 56}} 未显式设置时整个 cellHeight 键缺失(不是返回 null;前端按 32 渲染)
|
||||
```
|
||||
|
||||
## 数据高亮规则(条件填色,仅 Grid)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
1. 未按"阶段 1"做人员解析 → `--users` 传入部门 ID 而非员工 userId,脚本虽内置回退但会浪费一次失败的接口调用
|
||||
2. 未按"阶段 0"判断报表类型 → 用户说"汇总"被理解成"明细",导致输出粒度错误
|
||||
3. 未按"列选择"判断是否传 `--column-keywords` → 用户要"迟到情况报表"被输出成全字段默认报表
|
||||
4. 未按"错误处理"规则处理 403 / `HSF_ILLEGALPARAMS` → 把环境错误当成业务错误反馈给用户
|
||||
4. 未按"错误处理"规则处理无权限错误(errorCode 6001「无权限操作」/ `AUTH_ERROR`)/ `HSF_ILLEGALPARAMS` → 把环境错误当成业务错误反馈给用户
|
||||
5. 未按"阶段 4"返回结果 → 把 Excel 内容贴在对话里,或者裸 userId 直接输出
|
||||
|
||||
**执行前自检(必须能在心中回答)**:
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
如果上述任何一项答不出,**回到本文档对应章节重新阅读**,禁止凭记忆/想象组装命令。
|
||||
|
||||
**前提**:当前用户必须是钉钉管理员,否则 report 系列接口返回 403 权限错误。
|
||||
**前提**:当前用户必须是钉钉管理员,否则 report 系列接口返回业务权限错误(`server_error_code` 为 `AUTH_ERROR`、errorCode `6001`「无权限操作」),**不是 HTTP 403**。提示需要管理员权限,不要重试。
|
||||
|
||||
## 核心原则
|
||||
|
||||
@@ -46,7 +46,7 @@ Agent 解析用户意图(报表类型、人员范围、时间范围、关注
|
||||
- 所有 `dws` 命令必须携带 `--format json`
|
||||
- 时间参数 `--start` / `--end` 格式必须为 `yyyy-MM-dd HH:mm:ss`
|
||||
- 字段 ID 与字段名的映射必须从 `report columns` 实时建立,禁止硬编码
|
||||
- 任何接口失败(含 403)必须向用户清晰报错,禁止静默吞掉
|
||||
- 任何接口失败(含无权限错误 6001/`AUTH_ERROR`)必须向用户清晰报错,禁止静默吞掉
|
||||
|
||||
## 涉及工具
|
||||
|
||||
@@ -246,7 +246,7 @@ python scripts/attendance_report_checkin.py \
|
||||
|
||||
- 脚本依赖 `openpyxl`,若未安装需先 `pip install openpyxl`
|
||||
- 脚本摘要输出到 stdout,进度日志输出到 stderr
|
||||
- 首次调试可加 `--inspect` 参数查看首条记录原始结构
|
||||
- 首次调试可加 `--inspect` 参数查看首条记录原始结构(`attendance_report_detail.py` / `attendance_report_monthly.py` / `attendance_report_daily.py` / `attendance_report_checkin.py` 支持;**`attendance_report_record.py` 无 `--inspect` 参数**)
|
||||
- 脚本执行失败(exit ≠ 0)时,stderr 中有具体错误信息
|
||||
|
||||
### 阶段 4: 返回结果给用户
|
||||
@@ -488,11 +488,11 @@ python scripts/attendance_report_checkin.py \
|
||||
|
||||
| 错误 | 原因 | 处理方式 |
|
||||
|------|------|---------|
|
||||
| 权限错误(403) | 当前账号非管理员 | 提示需要管理员权限,不要重试 |
|
||||
| 权限错误(errorCode 6001「无权限操作」/ `AUTH_ERROR`,非 HTTP 403) | 当前账号非管理员 | 提示需要管理员权限,不要重试 |
|
||||
| userId 无效 | 用户 ID 错误或已离职 | 脚本跳过并在摘要中标注 |
|
||||
| 时间区间超长 | 接口可能性能不佳 | 提示"超过 1 年的数据建议分阶段导出" |
|
||||
| openpyxl 未安装 | 环境缺包 | 输出 `pip install openpyxl` 安装提示 |
|
||||
| 脚本执行失败 | 接口异常/配置问题 | 将 stderr 错误信息转告用户,可加 `--inspect` 重试 |
|
||||
| 脚本执行失败 | 接口异常/配置问题 | 将 stderr 错误信息转告用户,可加 `--inspect` 重试(`attendance_report_record.py` 不支持 `--inspect`) |
|
||||
|
||||
## 使用示例
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 考勤 (attendance) 命令参考
|
||||
|
||||
> **【开源版命令可用性提示】** 当前开源 dws 已落地 P0 5 条命令:`attendance check result`、`attendance check record`、`attendance group search`、`attendance vacation balance`、`attendance vacation types`。文档中其余 P1 阶段命令(`class` / `overtime` / `adjustment` / `group settings` / `report` / `schedule` / `boss-check` 等共 28 条)暂未在当前开源二进制暴露,调用会返回 `unknown command`,将在后续批次落地。
|
||||
> **【命令可用性提示】** 当前 dws 已注册全部考勤子命令组(`record` / `check` / `approve` / `shift` / `schedule` / `class` / `adjustment` / `overtime` / `group` / `summary` / `rules` / `selfsetting` / `globalsetting` / `vacation` / `checkin` / `report` / `boss-check`)。查询与写操作大多可直接调用后端,不会再返回 `unknown command`,不要以"开源版不支持"为由拒答。个别命令返回受账号权限和组织数据影响:`report` 系列仅管理员可用;非管理员或数据为空时可能返回空列表或权限错误。执行前可用 `dws <cmd> --help` 或 `--dry-run` 验证参数。
|
||||
|
||||
> **【必读】日期范围严格计算规则 — 所有含 --start/--end 或 --from/--to 的命令均适用**
|
||||
>
|
||||
@@ -89,6 +89,19 @@ Flags:
|
||||
|
||||
返回每条记录含:用户 ID、实际打卡时间、打卡地址、打卡经纬度、打卡类型(OnDuty/OffDuty)、定位方式(Map/Wifi/etc)。时间跨度不超过 1 个月。
|
||||
|
||||
### 查询个人某日考勤详情
|
||||
```
|
||||
Usage:
|
||||
dws attendance record get [flags]
|
||||
Example:
|
||||
dws attendance record get --user 011769261608 --date 2026-03-08
|
||||
Flags:
|
||||
--user string 钉钉用户 ID (必填,别名 --users 亦可)
|
||||
--date string 查询日期, 格式 YYYY-MM-DD (必填)
|
||||
```
|
||||
|
||||
查询单个用户在**某一天**的考勤详情(区别于 `check record`/`check result` 的多人时间段批量查询)。返回 `result` 对象含 `isHasSchedule`(当日是否有排班)、`isRest`(是否休息日)、`isUnSigned`(是否未打卡)、`recordList`(打卡明细)、`approveList`(当日审批单)、`workOvertime`、`workTimeDesc` 等字段。`--user` 只接受**单个** userId;查 userId 用 `dws contact user search --query "姓名"`。
|
||||
|
||||
### 查询审批单(补卡/加班/请假/出差外出)
|
||||
```
|
||||
Usage:
|
||||
@@ -143,21 +156,20 @@ Usage:
|
||||
dws attendance schedule import [flags]
|
||||
Example:
|
||||
dws attendance schedule import --group-id 123456 \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \
|
||||
--yes
|
||||
Flags:
|
||||
--group-id string 考勤组(必填,传入考勤组ID)
|
||||
--schedules string 排班记录 JSON 数组(必填)
|
||||
--yes 跳过确认提示
|
||||
--group-id string 考勤组(必填,传入考勤组ID);--help 主名为 --groupId,--group-id 为别名
|
||||
--schedules string 排班记录 JSON 数组(必填);--help 主名为 --scheduleVOS,--schedules 为别名
|
||||
--yes 跳过确认提示;--help 主名为 --user-say-yes,--yes 为别名
|
||||
```
|
||||
|
||||
为排班制考勤组导入排班记录。`--schedules` 为 JSON 数组,每条记录包含:
|
||||
- `userId`: 员工ID
|
||||
- `classId`: 班次ID
|
||||
- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22
|
||||
- `checkBeginTime`: 开始打卡时间
|
||||
- `checkEndTime`: 结束打卡时间
|
||||
- `isRest`: 是否休息日 Y/N(可选)
|
||||
- `userId`: 员工ID(必填)
|
||||
- `classId`: 班次ID(必填)
|
||||
- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22(必填)
|
||||
- `isRest`: 是否休息日 Y/N(**必填**,服务端要求传入)
|
||||
- `checkBeginTime` / `checkEndTime`: 开始/结束打卡时间(可传,但当前不会进入后端 payload,实际打卡时段以 `classId` 对应班次为准)
|
||||
|
||||
#### AI 调用 `schedule import` 的二次确认流程
|
||||
|
||||
@@ -305,7 +317,7 @@ Flags:
|
||||
--adjustment-id int 补卡规则主键 ID (必填)
|
||||
```
|
||||
|
||||
根据补卡规则主键 ID 查询对应的补卡规则详情。主键 ID 可从 `adjustment search` 返回结果中提取,也有可能来源于用户手动输入。**注意:已被删除或被更新覆盖的补卡规则无法查询到。**
|
||||
**注意:本命令当前拿不到有效的补卡规则详情,不要依赖它。** 服务端对任意 `--adjustment-id`(含不存在的 ID)都返回同一个 `{"success":true,"有效期类型":"..."}`,不返回规则明细,也无法据此判断规则是否存在;且 `adjustment search` 返回的默认补卡规则 `entityVO.id` 为 `null`,`search → get` 取 id 的链路走不通。补卡规则内容请直接看 `adjustment search` 的返回结果。
|
||||
|
||||
### 分页查询加班规则,支持按名称搜素
|
||||
```
|
||||
@@ -580,12 +592,17 @@ Flags:
|
||||
Usage:
|
||||
dws attendance summary [flags]
|
||||
Example:
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00"
|
||||
dws attendance summary --user USER_ID --date 2026-03-12 --stats-type week
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type month
|
||||
Flags:
|
||||
--date string 工作日期, 格式 yyyy-MM-dd HH:mm:ss (必填)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
--date string 查询日期, 格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss (必填)
|
||||
--stats-type string 统计类型: week 周统计 / month 月统计 (必填)
|
||||
--tag-name string 标签名称 (可选)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
```
|
||||
|
||||
`summary` 必须同时传 `--user`、`--date`、`--stats-type`,缺一即报错(如 C0002)。`--stats-type` 只能是 `week`(周统计)或 `month`(月统计)。
|
||||
|
||||
### 查询考勤组与考勤规则
|
||||
```
|
||||
Usage:
|
||||
@@ -847,10 +864,10 @@ Example:
|
||||
dws attendance vacation balance --users userId1,userId2 --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
||||
Flags:
|
||||
--users string 目标员工 ID 列表, 逗号分隔 (必填)
|
||||
--leave-code string 假期规则 code (选填,不传则查询所有假期规则余额)
|
||||
--leave-code string 假期规则 code (必填,服务端要求非空,不传返回 INVALID_PARAMS)
|
||||
```
|
||||
|
||||
调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取;不传 `--leave-code` 时查询所有假期规则余额。认证信息(corpId、opUserId)由系统自动注入。
|
||||
调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取。**注意:`--help` 虽标"选填",但服务端要求 `leaveCode` 非空,实际必填;不传会返回 `INVALID_PARAMS`(corpId、opUserId、leaveCode、targetUserIds 不能为空)。** 若要一次查所有假期规则余额,必须走 [attendance-vacation.md](./attendance-vacation.md) 工作流脚本逐个规则遍历。认证信息(corpId、opUserId)由系统自动注入。
|
||||
|
||||
如用户需要“所有假期规则余额 / 导出假期余额列表 / 所有假期规则余额 Excel / 按截图样式导出假期余额”,必须先读取 [attendance-vacation.md](./attendance-vacation.md),再按其中工作流调用脚本生成 Excel。
|
||||
|
||||
@@ -867,7 +884,7 @@ Flags:
|
||||
--end string 查询结束日期, 格式 YYYY-MM-DD (必填)
|
||||
```
|
||||
|
||||
调用 MCP 工具 get_leave_balance_records 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。
|
||||
调用 MCP 工具 get_leave_balance_records_v2 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 必填,可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。
|
||||
|
||||
### 更新假期规则(写场景接口,必须走二次确认流程)
|
||||
|
||||
@@ -1002,7 +1019,7 @@ Usage:
|
||||
dws attendance checkin records [flags]
|
||||
Example:
|
||||
dws attendance checkin records \
|
||||
--operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00"
|
||||
--operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00"
|
||||
Flags:
|
||||
--end string 结束时间, 格式 yyyy-MM-dd HH:mm:ss(必填)
|
||||
--operator-corp-id string 操作者企业 ID(必填)
|
||||
@@ -1032,7 +1049,7 @@ Flags:
|
||||
用户说"班次详情/某个班次的具体信息" → `class search --name "..."`(search 直出,直接返回详情)。`class get` 仅在需要按已知 classId 精确查询时使用
|
||||
用户说"更新班次/修改班次/班次改名/修改上下班时间" → `class update`
|
||||
用户说"补卡规则/补卡设置" → `adjustment search`(返回结果已包含全量属性,无需再调 get)
|
||||
用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出)。`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用
|
||||
用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出,已含全量属性)。**不要用 `adjustment get`**:当前服务端对任意 id 都只返回"有效期类型"、拿不到规则明细
|
||||
用户说"加班规则/加班设置/加班计算" → `overtime search`(返回结果已包含全量属性,无需再调 get)
|
||||
用户说"加班规则详情/某条加班规则的具体信息" → `overtime search --name "..."`(search 直出)。如需查已删除/被覆盖的历史记录 → `overtime get`
|
||||
用户说"考勤组列表/有哪些考勤组" → `group search`
|
||||
@@ -1068,7 +1085,7 @@ Flags:
|
||||
```bash
|
||||
# 导入排班记录
|
||||
dws attendance schedule import --group-id 123456 \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \
|
||||
--yes --format json
|
||||
|
||||
# 获取排班记录 — 禁止直接调用,必须走 attendance-schedule.md 排班查询导出工作流
|
||||
@@ -1123,8 +1140,8 @@ dws attendance group update --group-id 123456 --group-vo '{"positions":[{"title"
|
||||
dws attendance group create --name "研发考勤组" --type FIXED --group-vo '{"defaultClassId":1170996821,"workDayClassList":[0,1170996821,0,0,0,0,0]}' --timeout 10 --format json
|
||||
dws attendance group create --name "自由工时分组" --type NONE --timeout 10 --format json
|
||||
|
||||
# 查看考勤统计摘要
|
||||
dws attendance summary --user <USER_ID> --date "2026-03-12 15:00:00" --format json
|
||||
# 查看考勤统计摘要(--stats-type 必填:week 周统计 / month 月统计)
|
||||
dws attendance summary --user <USER_ID> --date 2026-03-12 --stats-type week --format json
|
||||
|
||||
# 查看考勤组和规则
|
||||
dws attendance rules --date 2026-03-14 --format json
|
||||
@@ -1157,13 +1174,13 @@ dws attendance report query-leave --users userId1,userId2 \
|
||||
dws attendance vacation types --format json
|
||||
|
||||
# 查看指定员工假期余额
|
||||
dws attendance vacation balance --users userId1,userId2 --format json
|
||||
dws attendance vacation balance --users userId1,userId2 --leave-code <假期类型code> --format json
|
||||
|
||||
# 查看指定员工某类假期余额
|
||||
dws attendance vacation balance --users userId1 --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890 --format json
|
||||
|
||||
# 查看指定员工假期余额变更记录
|
||||
dws attendance vacation records --user USER_ID --start 2026-04-01 --end 2026-04-22 --format json
|
||||
dws attendance vacation records --user USER_ID --leave-code <假期类型code> --start 2026-04-01 --end 2026-04-22 --format json
|
||||
|
||||
# 更新假期规则名称
|
||||
dws attendance vacation update-type --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
|
||||
@@ -1202,7 +1219,7 @@ dws attendance vacation save-balance --target user001 \
|
||||
--num 8 --reason "绩效奖励发放3天" --format json
|
||||
|
||||
# 查询签到记录
|
||||
dws attendance checkin records --operator-staff-id op001 --staff-ids user001,user002 \
|
||||
dws attendance checkin records --operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 \
|
||||
--start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00" --format json
|
||||
```
|
||||
|
||||
@@ -1233,7 +1250,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use
|
||||
- `class get` 的 `--class-id` 必填,班次 ID 可从 `class search` 结果中提取
|
||||
- `class search` 返回结果已包含全量属性,无需再调用 `class get`;`class get` 仅在需要按已知 classId 精确查询时使用
|
||||
- `class update` 的 `--class-id` 必填,其余均可选,仅需对要修改的字段赋值,未传字段会自动从已有配置补充;由于保存班次耗时较久,建议加 `--timeout 10`
|
||||
- `adjustment search` 返回结果已包含全量属性,无需再调用 `adjustment get`;`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用
|
||||
- `adjustment search` 返回结果已包含全量属性;`adjustment get` 当前服务端对任意 id 都只返回"有效期类型"、无规则明细,不可用,补卡规则详情一律看 `adjustment search` 返回
|
||||
- `overtime search` 返回结果已包含全量属性,无需再调用 `overtime get`;`overtime get` 仅在需要按已知 overtimeId 查询时使用(包括已删除/被覆盖的历史记录)
|
||||
- `adjustment search` / `overtime search` 分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20
|
||||
- `group search` 的分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20
|
||||
@@ -1242,7 +1259,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use
|
||||
- `group update` 的 --group-id 必填,其余均可选,至少需指定一个修改项;仅需对要修改的字段赋値,未传字段会从已有配置自动补充;修改打卡地址/wifi/蓝牙等复杂子对象时用 `--group-vo` 传入完整 JSON;`--group-vo` 与单字段 flag 同时传入时单字段 flag 优先级更高
|
||||
- `group create` 的 `--name` 和 `--type` 必填,`--type` 必须为 FIXED/TURN/NONE 之一;type=FIXED 时 `--group-vo` 必须包含 `workDayClassList`(非空)和 `defaultClassId`(非 null);由于保存考勤组耗时较久,建议加 `--timeout 10`
|
||||
- `group filtered-get` 的 `--group-id` 必填,`--member/--position/--wifi/--bles` 均可选,默认 false。**返回结果中如含成员 userId 列表,必须调用 `dws contact user get --ids <userId1>,<userId2>,...`(支持逗号分隔传多个 ID),将 userId 转换为员工姓名后再输出;不得直接输出裸 userId。**
|
||||
- `summary` 的 `--date` 格式: yyyy-MM-dd HH:mm:ss(如 `2026-03-12 15:00:00`)
|
||||
- `summary` 必须同时传 `--user`、`--date`、`--stats-type`(week 周统计 / month 月统计),三者缺一即报错;`--date` 支持 `YYYY-MM-DD` 或 `yyyy-MM-dd HH:mm:ss`;`--tag-name` 可选
|
||||
- `rules` 的 `--date` 支持 YYYY-MM-DD 或 yyyy-MM-dd HH:mm:ss 两种格式
|
||||
- `selfsetting get/save` 的 `--setting-scene` 必须是 `checkRemind`、`fastCheck`、`checkResultNotify`、`lackRemind`、`personalAttendStatNotify`、`bossAttendStatNotify` 之一
|
||||
- `selfsetting get/save` 的 MCP 入参 `userId` 为必填;CLI 的 `--user` 也必填,必须显式传入目标用户 ID
|
||||
@@ -1255,8 +1272,8 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use
|
||||
- 用户 ID 需从 `contact user get-self` 或 `aisearch person` 获取
|
||||
- 考勤组 ID 需从 `rules` 命令返回结果中获取
|
||||
- `vacation types` 无需任何参数,认证信息自动注入
|
||||
- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 选填,可通过 `vacation types` 获取
|
||||
- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 选填
|
||||
- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 服务端要求非空、实际必填(`--help` 标"选填"不准),不传返回 `INVALID_PARAMS`,可通过 `vacation types` 获取
|
||||
- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 必填(不传无法查询),底层 MCP 工具为 `get_leave_balance_records_v2`
|
||||
- `vacation balance` 和 `vacation records` 的认证参数(corpId、opUserId)由系统自动注入,无需手动传入
|
||||
- `vacation update-type` 的 `--leave-code` 必填;其他字段均为可选,但至少需传一个更新字段
|
||||
- `vacation update-type` 的 `--visibility-rules` 为 JSON 数组字符串,格式:`[{"type":"dept","visible":["1","2","3"]}]`,type 可取值 staff/label/dept
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- **二级子命令(必选其一)**:`event`(日程)、`attendee`(参会人)、`room`(会议室)、`busy`(闲忙)、`attachment`(日程附件)、`book`(我能看哪些日历本)、`acl`(我的日历共享给了谁)。`dws calendar` 后**必须**紧跟上述之一;**禁止**只执行 `dws calendar`(无子命令)。
|
||||
- **个人日程 / 给自己留时间块 / 专注时段**:统一走 **`dws calendar event create`**。当前**没有**单独的 `personal schedule create` / `calendar create` 命令。
|
||||
- **查日程列表**:`dws calendar event list --start "<ISO-8601>" --end "<ISO-8601>" --format json`,或优先使用脚本 `python scripts/calendar_today_agenda.py [today|tomorrow|week]`(见文末「自动化脚本」)。
|
||||
- **查用户日历本列表**:`dws calendar book list`(返回主日历 `id == "primary"` 等)。**重要**可以查询他人共享给自己的日历本,根据日历本id可以进一步查询对方的日程信息。
|
||||
- **查用户日历本列表**:`dws calendar book list`(返回字段为 `calendarId`,主日历 `calendarId == "primary"` 等;注意无 `id` 字段,按 `.id` 提取会取空)。**重要**可以查询他人共享给自己的日历本,根据日历本id可以进一步查询对方的日程信息。
|
||||
- **CLI 不存在**独立的 `dws calendar list`;若误跑无子命令的 `dws calendar`,会打印整段 Usage,**切勿**将该段 help 当作工具结果再次塞进对话(会急剧增加 token 与首字延迟)。
|
||||
- **必须**遵循指令说明进行调用。**绝对禁止**使用虚构指令,使用虚构参数。
|
||||
|
||||
@@ -64,6 +64,7 @@ dws calendar room add [flags]
|
||||
dws calendar room delete [flags]
|
||||
```
|
||||
> room是会议室,用于线下开会场景。
|
||||
> **组织限制**:部分企业使用自建会议室系统,未接入钉钉会议室能力。此时 `room search` / `room list-groups` 会返回业务错误 `400056`("所选组织不支持预定钉钉会议室")。这是组织级配置限制,不是命令用法问题;应直接告知用户该组织需在钉钉客户端手动预订会议室,不要重试或换参。
|
||||
|
||||
### busy 相关三级子命令
|
||||
```
|
||||
@@ -617,7 +618,7 @@ dws calendar event list --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15
|
||||
| `event respond` | 响应结果 | — |
|
||||
| `room search` | `rooms[].roomId` | room add 的 --rooms 或 event create 的 --rooms |
|
||||
| `room list-groups` | `groups[].groupId` | room search 的 --group-id |
|
||||
| `book list` | `id`(如 `primary`) | event list/get 的 --calendar-id, book get/update 的 --id |
|
||||
| `book list` | `calendarId`(如 `primary`) | event list/get 的 --calendar-id, book get/update 的 --id |
|
||||
| `book get` | 日历详细信息 | — |
|
||||
| `book search` | 匹配的日历列表 | book get/update 的 --id |
|
||||
| `acl list` | `aclId` | acl delete 的 --acl-id |
|
||||
@@ -639,7 +640,7 @@ dws calendar event list --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15
|
||||
- **搜房无结果**:在符合早停/用户限定范围内,`room search`(含按分组逐组查)全部返回空或无空闲 → 应**直接向用户报错/说明失败**并结束订房;**禁止**假设 roomId、禁止无合法 `roomId` 时调用 `room add` / `event create --rooms` 试探、禁止用 `event get` 等绕路推断 roomId
|
||||
- **评测 / 自动化断言**:凡涉及 `room add` / `event create --rooms` 的流程,`--rooms` 只能填上游 `room search`(或等价接口)返回 JSON 中的 **`rooms[].roomId`**;不得以会议室展示名、楼层文案或用户口语当作 `roomId`
|
||||
- **附件**:`attachment add` 仅负责挂载,**不上传**文件;fileId 必须先通过钉盘流程取得;`--files` 多附件用 `<fileId>:<name>` 元素逗号分隔
|
||||
- **日历本**:`book list` 返回的 `id` 才是合法 `calendarId`;如无明确说明,`event list` / `event get` 都不要带 `--calendar-id`,让接口默认走 primary 主日历
|
||||
- **日历本**:`book list` 返回的 `calendarId` 字段才是合法日历 id(无 `id` 字段);如无明确说明,`event list` / `event get` 都不要带 `--calendar-id`,让接口默认走 primary 主日历
|
||||
- **已弃用入参**:`--max-results`(event list)在新 MCP schema 中被移除;CLI 仍接受但**不会**透传到 MCP;模型生成命令时**禁止**继续使用
|
||||
|
||||
## 自动化脚本
|
||||
|
||||
@@ -73,6 +73,19 @@ Flags:
|
||||
--bot-id string 机器人 openBotId (必填)
|
||||
```
|
||||
|
||||
#### 根据成员 ID 批量查询群成员详情 — 传入成员 openDingTalkId 列表批量查询
|
||||
```
|
||||
Usage:
|
||||
dws chat group members list-by-ids [flags]
|
||||
Example:
|
||||
dws chat group members list-by-ids --id <openConversationId> --users openDingTalkId1,openDingTalkId2
|
||||
# 查询群 ID: dws chat search --query "群名"
|
||||
# 查询 openDingTalkId: dws contact user search --query "姓名"
|
||||
Flags:
|
||||
--id string 群 ID / openConversationId (必填)
|
||||
--users string 成员 openDingTalkId 列表,逗号分隔 (必填)
|
||||
```
|
||||
|
||||
#### 更新群名称
|
||||
```
|
||||
Usage:
|
||||
@@ -136,6 +149,7 @@ Example:
|
||||
Flags:
|
||||
--group string 群聊 openConversationId (必填)
|
||||
```
|
||||
> ⚠️ 唯一群主 quit 会直接成功,产生无主群,之后对该群做 dismiss / members 等管理操作会报 listRoles null(11056) 无法管理。与 `chat group members remove` 移除群主有本地拦截不同,quit 无此防护。作为唯一群主想退群,应先 `chat group transfer-owner` 转让群主,或 `chat group dismiss` 解散群。
|
||||
|
||||
#### 更新群头像 — 更新指定群聊的群头像
|
||||
```
|
||||
@@ -149,6 +163,7 @@ Flags:
|
||||
--icon-media-id string 群头像 mediaId (必填)
|
||||
```
|
||||
> `--icon-media-id` 有本地格式校验:必须是 `@` 开头的媒体 ID(如 `dt_media_upload` 的返回值),非法格式会在本地直接报错。
|
||||
> ⚠️ 本地格式校验只查前缀。格式合法但不真实存在的 mediaId 服务端仍会静默返回成功,头像并不会真正更新。务必用 `dt_media_upload` / `chat media upload` 上传真实图片拿到的 mediaId。
|
||||
|
||||
#### 更新群设置 — 更新指定群聊的设置项
|
||||
|
||||
@@ -173,6 +188,30 @@ Flags:
|
||||
--status int 设置值: 0=关闭, 1=开启 (必填)
|
||||
```
|
||||
|
||||
#### 设置群备注 — 给群设置只有自己可见的备注标题
|
||||
```
|
||||
Usage:
|
||||
dws chat group update-alias [flags]
|
||||
Example:
|
||||
dws chat group update-alias --group <openConversationId> --alias-title "项目A群"
|
||||
# 查询群 ID: dws chat search --query "群名"
|
||||
Flags:
|
||||
--group string 群聊 openConversationId (必填)
|
||||
--alias-title string 群备注标题 (必填)
|
||||
```
|
||||
|
||||
#### 设置我在群内的群昵称 — 设置当前用户在指定群里显示的昵称
|
||||
```
|
||||
Usage:
|
||||
dws chat group update-nick [flags]
|
||||
Example:
|
||||
dws chat group update-nick --group <openConversationId> --nick "我的群昵称"
|
||||
# 查询群 ID: dws chat search --query "群名"
|
||||
Flags:
|
||||
--group string 群聊 openConversationId (必填)
|
||||
--nick string 个人群昵称 (必填)
|
||||
```
|
||||
|
||||
#### 查看群内所有机器人 — 获取指定群聊中的所有机器人列表
|
||||
```
|
||||
Usage:
|
||||
@@ -294,6 +333,8 @@ Flags:
|
||||
```
|
||||
|
||||
#### 移除用户的指定群身份 — 从用户身上移除指定的群身份(不影响其他群身份)
|
||||
|
||||
> ⚠️ 当前不可用:该命令当前调用常失败(服务端返回 1002 系统繁忙),多为服务端侧限制。需要清除某人身份时,用 `chat group-role set-user --role-ids ""`(传空 role-ids 覆盖为无身份)兜底;但 set-user 会清掉该用户的**全部**身份,无法只移除其中一个。
|
||||
```
|
||||
Usage:
|
||||
dws chat group-role remove-user [flags]
|
||||
@@ -336,6 +377,7 @@ Flags:
|
||||
--query string 搜索关键词 (必填)
|
||||
--limit int 每页返回数量(默认 20)
|
||||
--cursor string 分页游标(默认 "0",翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已设置免打扰的群聊(默认 false)
|
||||
```
|
||||
|
||||
### data-auth (数据授权)
|
||||
@@ -371,7 +413,7 @@ Flags:
|
||||
|
||||
#### 拉取会话消息内容 — 拉取指定群聊或单聊的会话消息内容
|
||||
|
||||
--group 指定群聊,--user 指定单聊用户(通过 userId),--open-dingtalk-id 指定单聊用户(通过 openDingTalkId),三者互斥。默认拉取给定时间之后的消息,--forward=false 拉之前的。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。
|
||||
--group 指定群聊,--user 指定单聊用户(通过 userId),--open-dingtalk-id 指定单聊用户(通过 openDingTalkId),三者互斥。用 --direction 控制时间方向:newer=从给定时间往现在拉,older=从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。
|
||||
```
|
||||
Usage:
|
||||
dws chat message list [flags]
|
||||
@@ -379,9 +421,9 @@ Example:
|
||||
dws chat message list --group <openconversation_id> --time "2025-03-01 00:00:00"
|
||||
dws chat message list --user <userId> --time "2025-03-01 00:00:00" --limit 50
|
||||
dws chat message list --open-dingtalk-id <openDingTalkId> --time "2025-03-01 00:00:00" --limit 50
|
||||
dws chat message list --group <openconversation_id> --time "2025-03-01 00:00:00" --forward=false
|
||||
dws chat message list --group <openconversation_id> --time "2025-03-01 00:00:00" --direction older
|
||||
Flags:
|
||||
--forward true=拉给定时间之后的消息,false=拉给定时间之前的消息 (default true)
|
||||
--direction string 时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(推荐)
|
||||
--group string 群聊 openconversation_id(群聊时必填)
|
||||
--limit int 返回数量,不传则不限制
|
||||
--time string 开始时间,格式: yyyy-MM-dd HH:mm:ss (必填)
|
||||
@@ -425,7 +467,7 @@ Example:
|
||||
dws chat message send --group <openconversation_id> --title "周报提醒" --text "请大家本周五前提交周报"
|
||||
# 幂等发送(24h 内相同 uuid 不重复投递)
|
||||
dws chat message send --group <openconversation_id> --text "hello" --uuid "unique-id-123"
|
||||
dws chat message send --group <openconversation_id> --at-all "@all 请大家注意"
|
||||
dws chat message send --group <openconversation_id> --at-all "<@all> 请大家注意"
|
||||
dws chat message send --group <openconversation_id> --at-open-dingtalk-ids openDingTalkId1,openDingTalkId2 "<@openDingTalkId1> <@openDingTalkId2> 请查收"
|
||||
# 发送图片
|
||||
dws chat message send --group <openconversation_id> --msg-type image --media-id <mediaId>
|
||||
@@ -453,13 +495,14 @@ Flags:
|
||||
--file-path string 文件路径(msgType=file 时必填)
|
||||
--file-size int64 文件大小,单位字节(msgType=file 时必填)
|
||||
--uuid string 幂等 UUID,相同 uuid 在 24h 内不会重复发送(可选)
|
||||
--ai-tag 消息是否带 AI 发送角标(可选,默认 true)
|
||||
|
||||
注意:
|
||||
- --text 和位置参数二选一,--text 优先
|
||||
- --group、--user、--open-dingtalk-id 三者互斥,只需指定其一:群聊用 --group,单聊用 --user 或 --open-dingtalk-id
|
||||
- 纯文本/Markdown 单聊发送时 `--user` 和 `--open-dingtalk-id` 都可用;传 `--user` 时直接走 userId 发送能力
|
||||
- --group 的别名: --id, --chat, --conversation-id (均可替代 --group)
|
||||
- --at-all 和 --at-open-dingtalk-ids 仅在 --group 群聊时生效,单聊时无效;当设置--at-all时,消息内容中一定要包含对应的占位符@all;当设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2>
|
||||
- --at-all 和 --at-open-dingtalk-ids 仅在 --group 群聊时生效,单聊时无效;当设置--at-all时,消息内容中一定要包含对应的占位符<@all>;当设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2>
|
||||
- **换行符**:消息内容按 Markdown 渲染,换行有两层要求,缺一不可:
|
||||
1. 必须使用**真实换行符**(Unicode `U+000A`),而非字面量字符串 `\n`(反斜杠 + 字母 n)。程序或大模型构造参数时,须确保已正确反转义;否则全部内容会渲染在同一行
|
||||
2. Markdown 规范下**单个换行不产生换行效果**。需要换行时请使用:段落分隔(连续两个真实换行符 `\n\n`)、行尾两个空格 + 真实换行符(硬换行 `<br>`),或直接写 HTML 的 `<br>` 标签
|
||||
@@ -475,6 +518,26 @@ Flags:
|
||||
- 发送文字 + 文件混合消息时的完整流程:除了将文件以 Markdown 链接内嵌到文字消息中发送一条 md 消息外,还必须额外逐个发送独立的文件消息(--msg-type file),确保接收方可以直接下载原始文件。即:先发一条包含文字和文件链接的 md 消息,再对每个涉及的文件各发一条 --msg-type file 的文件消息
|
||||
```
|
||||
|
||||
### media (上传媒体获取 mediaId)
|
||||
|
||||
#### 上传图片/媒体获取 mediaId — 用于 chat message send --msg-type image 等
|
||||
|
||||
⚠️ 前置条件:本命令需要应用凭证。必须已通过 `dws auth login --client-id <APP_KEY> --client-secret <APP_SECRET>` 登录,或设置环境变量 `DWS_CLIENT_ID` / `DWS_CLIENT_SECRET`;否则报"缺少应用凭证"。这与其他 chat 命令走用户登录态不同。
|
||||
```
|
||||
Usage:
|
||||
dws chat media upload [flags]
|
||||
Example:
|
||||
dws chat media upload --file ./screenshot.png
|
||||
dws chat media upload --file ./photo.jpg --type image
|
||||
Flags:
|
||||
--file string 本地文件路径 (必填)
|
||||
--type string 媒体类型: image/voice/video/file(默认 image)
|
||||
|
||||
注意:
|
||||
- 返回的 mediaId 可直接用于 chat message send --msg-type image --media-id
|
||||
- 发图片+文字时,agent 侧一般用独立的 dt_media_upload 工具;本命令是 dws 内置的等价上传入口
|
||||
```
|
||||
|
||||
### file (会话文件上传,已下线)
|
||||
|
||||
#### chat file upload 已下线
|
||||
@@ -507,6 +570,7 @@ Flags:
|
||||
注意:
|
||||
- openTaskId 由 `dws chat message send` 发送消息成功后返回
|
||||
- 用于确认消息是否已成功发送或获取发送失败的原因
|
||||
- 返回结果中含发送成功消息的 openMessageId,可用于后续 recall(撤回)、read-status(查已读)等命令
|
||||
```
|
||||
|
||||
#### 撤回消息 — 撤回当前用户自己发出的消息
|
||||
@@ -625,7 +689,7 @@ Flags:
|
||||
--topic-id string 话题 ID,由 dws chat message list 返回 (必填)
|
||||
--time string 开始时间,格式: yyyy-MM-dd HH:mm:ss(可选)
|
||||
--limit int 返回数量(默认 50)
|
||||
--forward true=从老往新,false=从新往老(默认 false)
|
||||
--direction string 时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(推荐,默认 older)
|
||||
```
|
||||
|
||||
#### 拉取指定时间范围内当前用户的所有会话消息 — 分页拉取当前登录用户在指定时间范围内的所有会话消息
|
||||
@@ -665,7 +729,7 @@ Example:
|
||||
dws chat message list-by-sender --sender-open-dingtalk-id <openDingTalkId> --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor <nextCursor>
|
||||
Flags:
|
||||
--sender-user-id string 发送者 userId(与 --sender-open-dingtalk-id 二选一)
|
||||
--sender-open-dingtalk-id string 发送者 openDingTalkId(与 --sender-user-id 二选一,适用于无法获取 userId 的场景)
|
||||
--sender-open-dingtalk-id string 发送者 openDingTalkId(与 --sender-user-id 二选一);该路径当前返回 result:{}、success:null(疑似服务端问题),优先用 --sender-user-id
|
||||
--start string 开始时间,ISO-8601 格式 (必填)
|
||||
--end string 结束时间,ISO-8601 格式 (必填)
|
||||
--limit int 每页返回数量(默认 50)
|
||||
@@ -673,8 +737,8 @@ Flags:
|
||||
|
||||
注意:
|
||||
- --sender-user-id 和 --sender-open-dingtalk-id 二者互斥,必须且只能指定其一:
|
||||
- --sender-user-id 传 userId(企业内部应用常用)
|
||||
- --sender-open-dingtalk-id 传 openDingTalkId(三方应用或跨组织场景常用,无法获取 userId 时使用)
|
||||
- --sender-user-id 传 userId(企业内部应用常用,当前唯一可用路径)
|
||||
- --sender-open-dingtalk-id 传 openDingTalkId(当前返回 result:{}、success:null(疑似服务端问题);即使只有 openDingTalkId,也建议先用 contact 换取 userId 后走 --sender-user-id)
|
||||
- openDingTalkId 获取方式见下方「openDingTalkId 获取方式」小节
|
||||
- 不需要指定单聊/群聊,返回结果自带会话类型标识
|
||||
- 时间支持多种 ISO-8601 格式,如 "2026-03-10T00:00:00+08:00"、"2026-03-10 14:00:00"、"2026-03-10" 等
|
||||
@@ -734,8 +798,10 @@ Usage:
|
||||
Example:
|
||||
dws chat message list-unread-conversations
|
||||
dws chat message list-unread-conversations --count 20
|
||||
dws chat message list-unread-conversations --exclude-muted
|
||||
Flags:
|
||||
--count int 返回未读会话条数(可选)
|
||||
--count int 返回未读会话条数(可选)
|
||||
--exclude-muted 是否排除已设置免打扰的会话(默认 false)
|
||||
```
|
||||
|
||||
#### 查询消息的已读/未读状态
|
||||
@@ -942,6 +1008,7 @@ Example:
|
||||
Flags:
|
||||
--limit int 每页返回数量(默认 1000)
|
||||
--cursor int64 分页游标(首次不传或传 0,翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已设置免打扰的会话(默认 false)
|
||||
|
||||
注意:
|
||||
- 用户询问"置顶会话"时,直接调用此命令返回置顶会话列表即可
|
||||
@@ -958,20 +1025,20 @@ Flags:
|
||||
Usage:
|
||||
dws chat message download-media [flags]
|
||||
Example:
|
||||
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/
|
||||
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./photo.jpg
|
||||
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/photo.jpg
|
||||
Flags:
|
||||
--type string 资源类型: mediaId (必填)
|
||||
--resource-id string 资源 ID,mediaId 类型时为消息中的 mediaId 值 (必填)
|
||||
--message-id string 消息 openMessageId (必填)
|
||||
--open-conversation-id string 会话 openConversationId (必填)
|
||||
--output string 本地保存路径,文件或目录 (必填)
|
||||
--output string 本地保存路径 (必填),建议传完整文件路径(含文件名)
|
||||
|
||||
注意:
|
||||
- resource-id 从 `dws chat message list` 返回的消息内容中获取 mediaId
|
||||
- message-id 从 `dws chat message list` 返回的 openMessageId
|
||||
- open-conversation-id 从 `dws chat search` 获取 openConversationId
|
||||
- --output 如果指定目录,文件名会从下载 URL 中自动推断
|
||||
- --output 建议传完整文件路径(含扩展名,如 ./photo.jpg);只传目录(如 ./downloads/)在旧版本会下载失败,请显式带上文件名
|
||||
```
|
||||
|
||||
### search-common (搜索共同群)
|
||||
@@ -991,6 +1058,7 @@ Flags:
|
||||
--match-mode string 匹配模式:AND=所有人都在群里,OR=任一人在群里(默认 AND)
|
||||
--limit int 每页返回数量(默认 20)
|
||||
--cursor string 分页游标(默认 "0",翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已设置免打扰的群聊(默认 false)
|
||||
|
||||
注意:
|
||||
- --nicks 传人员昵称(花名),逗号分隔,如 "风雷,山乔"
|
||||
@@ -1022,6 +1090,76 @@ Flags:
|
||||
- 上传到共享空间的文件对方才能打开,上传到个人空间的文件对方无法访问
|
||||
```
|
||||
|
||||
#### 引用回复消息 — 引用某条消息并回复文字(单聊/群聊均可)
|
||||
```
|
||||
Usage:
|
||||
dws chat message reply [flags]
|
||||
Example:
|
||||
dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text "收到,马上处理"
|
||||
# 被引用消息的 openMessageId、发送者 openDingTalkId 通过 dws chat message list 获取
|
||||
Flags:
|
||||
--conversation-id string 会话 openConversationId (必填,支持单聊/群聊)
|
||||
--ref-msg-id string 被引用的消息 openMessageId (必填)
|
||||
--ref-sender string 被引用消息的发送者 openDingTalkId (必填)
|
||||
--text string 回复内容 (必填)
|
||||
--ai-tag 消息是否带 AI 发送角标(可选,默认 true)
|
||||
--uuid string 幂等键(可选)
|
||||
|
||||
注意:
|
||||
- 以当前用户身份引用回复,语义同 chat message send;目前回复类型仅支持 text
|
||||
```
|
||||
|
||||
#### 转发单条消息 — 将一条消息从源会话转发到目标会话(源/目标均支持单聊/群聊)
|
||||
```
|
||||
Usage:
|
||||
dws chat message forward [flags]
|
||||
Example:
|
||||
dws chat message forward --src-conversation-id <srcOpenCid> --msg-id <openMessageId> --dest-conversation-id <destOpenCid>
|
||||
Flags:
|
||||
--src-conversation-id string 源会话 openConversationId (必填)
|
||||
--msg-id string 源消息 openMessageId (必填)
|
||||
--dest-conversation-id string 目标会话 openConversationId (必填)
|
||||
--uuid string 幂等键(可选)
|
||||
|
||||
注意:
|
||||
- 与 combine-forward 区别: forward 转单条,combine-forward 合并多条为一条转发
|
||||
```
|
||||
|
||||
#### 转发话题消息 — 将一条话题消息转发到目标会话
|
||||
```
|
||||
Usage:
|
||||
dws chat message forward-topic [flags]
|
||||
Example:
|
||||
dws chat message forward-topic --src-conversation-id <srcOpenCid> --src-msg-id <openMessageId> --src-thread-id <convThreadId> --dest-conversation-id <destOpenCid>
|
||||
Flags:
|
||||
--src-conversation-id string 源会话 openConversationId (必填,消息所在的会话)
|
||||
--src-msg-id string 源消息 openMessageId (必填,要转发的消息)
|
||||
--src-thread-id string 话题 ID (必填,格式: convThread + 加密后的 convThreadId,即 message list 返回的 openConvThreadId)
|
||||
--dest-conversation-id string 目标会话 openConversationId (必填,转发到的会话)
|
||||
```
|
||||
|
||||
#### 置顶消息 — 将指定消息置顶到会话顶部
|
||||
```
|
||||
Usage:
|
||||
dws chat message set-top-msg [flags]
|
||||
Example:
|
||||
dws chat message set-top-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>
|
||||
Flags:
|
||||
--open-conversation-id string 会话 openConversationId (必填,支持群聊/单聊)
|
||||
--msg-id string 消息 openMessageId (必填)
|
||||
```
|
||||
|
||||
#### 取消置顶消息 — 取消会话顶部的置顶消息
|
||||
```
|
||||
Usage:
|
||||
dws chat message unset-top-msg [flags]
|
||||
Example:
|
||||
dws chat message unset-top-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>
|
||||
Flags:
|
||||
--open-conversation-id string 会话 openConversationId (必填,支持群聊/单聊)
|
||||
--msg-id string 消息 openMessageId (必填)
|
||||
```
|
||||
|
||||
#### 合并转发多条消息 — 将多条消息合并后转发到目标会话(源/目标会话均支持单聊/群聊)
|
||||
```
|
||||
Usage:
|
||||
@@ -1106,10 +1244,10 @@ Flags:
|
||||
--size int 每页条数 (默认 50),别名: --limit
|
||||
```
|
||||
|
||||
#### 搜索【全部可用】机器人 — 含他人创建/官方机器人,额外返回 openDingTalkId
|
||||
#### 搜索【全部可用】机器人 — 含他人创建/官方机器人,额外返回机器人 openDingTalkId
|
||||
|
||||
范围: 当前用户可用的全部机器人(含他人创建、官方机器人)。
|
||||
返回字段: 额外返回 openDingTalkId(可用于给机器人发单聊消息),search 没有此字段。
|
||||
返回字段: 结果在 `result.bots[]` 中,每项含 `botOpenDingTalkId`(机器人的 openDingTalkId,用于给机器人发单聊消息)和 `name`。注意字段名是 `botOpenDingTalkId`,不是 `openDingTalkId`;search 没有此字段。
|
||||
典型触发词: "搜索机器人""找一个机器人""帮我找 XXX 机器人""所有可用机器人""查机器人"。
|
||||
|
||||
```
|
||||
@@ -1134,7 +1272,7 @@ search 与 find 选择指南:
|
||||
| 维度 | `chat bot search` | `chat bot find` |
|
||||
|------|-------------------|-----------------|
|
||||
| 范围 | 仅我创建的机器人 | 全部可用机器人(含他人/官方) |
|
||||
| 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) |
|
||||
| 额外返回机器人 openDingTalkId | 无 | 有,字段名 `botOpenDingTalkId`(可用于给机器人发单聊消息) |
|
||||
| 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" |
|
||||
|
||||
### category (会话分组管理)
|
||||
@@ -1154,9 +1292,70 @@ Usage:
|
||||
dws chat category list-conversations [flags]
|
||||
Example:
|
||||
dws chat category list-conversations --category-id <分组ID>
|
||||
dws chat category list-conversations --category-id <分组ID> --exclude-muted
|
||||
# 分组ID 可通过 dws chat category list 获取
|
||||
Flags:
|
||||
--category-id int 会话分组 ID (必填)
|
||||
--exclude-muted 是否排除已设置免打扰的会话(默认 false)
|
||||
```
|
||||
|
||||
#### 创建会话分组
|
||||
```
|
||||
Usage:
|
||||
dws chat category create [flags]
|
||||
Example:
|
||||
dws chat category create --title "工作群"
|
||||
Flags:
|
||||
--title string 分组名称 (必填)
|
||||
```
|
||||
|
||||
#### 删除会话分组
|
||||
```
|
||||
Usage:
|
||||
dws chat category delete [flags]
|
||||
Example:
|
||||
dws chat category delete --category-id <分组ID>
|
||||
# 分组ID 可通过 dws chat category list 获取
|
||||
Flags:
|
||||
--category-id int 会话分组 ID (必填)
|
||||
```
|
||||
|
||||
#### 重命名会话分组
|
||||
```
|
||||
Usage:
|
||||
dws chat category rename [flags]
|
||||
Example:
|
||||
dws chat category rename --category-id <分组ID> --title "新名称"
|
||||
# 分组ID 可通过 dws chat category list 获取
|
||||
Flags:
|
||||
--category-id int 会话分组 ID (必填)
|
||||
--title string 新的分组名称 (必填)
|
||||
```
|
||||
|
||||
#### 将会话加入分组
|
||||
```
|
||||
Usage:
|
||||
dws chat category add-conv [flags]
|
||||
Example:
|
||||
dws chat category add-conv --group <openConversationId> --category-ids 123,456
|
||||
# 分组ID 可通过 dws chat category list 获取
|
||||
# 查询群 ID: dws chat search --query "群名"
|
||||
Flags:
|
||||
--group string 会话 openConversationId (必填)
|
||||
--category-ids string 目标分组 ID 列表,逗号分隔 (必填)
|
||||
```
|
||||
|
||||
#### 将会话移出分组
|
||||
```
|
||||
Usage:
|
||||
dws chat category remove-conv [flags]
|
||||
Example:
|
||||
dws chat category remove-conv --group <openConversationId> --category-ids 123,456
|
||||
# 分组ID 可通过 dws chat category list 获取
|
||||
# 查询群 ID: dws chat search --query "群名"
|
||||
Flags:
|
||||
--group string 会话 openConversationId (必填)
|
||||
--category-ids string 目标分组 ID 列表,逗号分隔 (必填)
|
||||
```
|
||||
|
||||
### mute (会话免打扰)
|
||||
@@ -1205,6 +1404,8 @@ Flags:
|
||||
### mute-at-all (关闭@所有人通知)
|
||||
|
||||
#### 关闭/开启 @所有人消息提醒 — 关闭或开启会话中 @所有人的消息通知
|
||||
|
||||
> ⚠️ 当前不可用:该命令当前调用常失败(服务端返回 1002 系统繁忙),多为服务端侧限制。命令本身参数合法,但调用不会生效。
|
||||
```
|
||||
Usage:
|
||||
dws chat mute-at-all [flags]
|
||||
@@ -1226,6 +1427,8 @@ Flags:
|
||||
### mute-red-envelope (关闭红包通知)
|
||||
|
||||
#### 关闭/开启红包消息提醒 — 关闭或开启会话中的红包消息通知
|
||||
|
||||
> ⚠️ 当前不可用:该命令当前调用常失败(服务端返回 1002 系统繁忙),多为服务端侧限制。命令本身参数合法,但调用不会生效。
|
||||
```
|
||||
Usage:
|
||||
dws chat mute-red-envelope [flags]
|
||||
@@ -1260,7 +1463,7 @@ Flags:
|
||||
|
||||
注意:
|
||||
- 支持群聊和单聊,openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取
|
||||
- 标记未读后会话列表中会显示未读状态
|
||||
- API 返回成功,但该未读状态在 API 侧观察不到:`list-all-conversations` 返回的 unreadPoint 不会随之变化,只有钉钉客户端 UI 上能看到未读标记
|
||||
```
|
||||
|
||||
### clear-red-point (清除会话红点)
|
||||
@@ -1308,13 +1511,14 @@ Example:
|
||||
dws chat list-all-conversations --limit 100 --cursor <nextCursor>
|
||||
dws chat list-all-conversations --exclude-muted
|
||||
Flags:
|
||||
--limit int 每页数量(默认 1000)
|
||||
--limit int 每页数量(1-100,默认 100);传 >100 会被明确拒绝
|
||||
--cursor int 分页游标(首次不传或传 0,翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已免打扰会话(默认 false)
|
||||
|
||||
注意:
|
||||
- 返回结果包含单聊和群聊,不区分会话类型
|
||||
- 翻页: hasMore=true 时用返回的 nextCursor 作为下次 --cursor
|
||||
- --limit 范围 1-100,默认 100,上限 100;传入 >100 会报错拒绝,不会静默截断
|
||||
- 分页当前不可用:当前 hasMore 恒为 false、nextCursor 恒为 null,本命令最多返回 100 条会话,无法用 --cursor 继续翻页取更多
|
||||
- 与 list-top-conversations 的区别: 本命令返回全部会话(单聊+群聊),list-top-conversations 仅返回置顶会话
|
||||
```
|
||||
|
||||
@@ -1375,6 +1579,7 @@ Flags:
|
||||
注意:
|
||||
- 与 `chat group list-my-groups` 区别: list-all 返回用户加入的所有群;list-my-groups 仅返回用户作为群主/管理员的群
|
||||
- 分页: hasMore=true 时用返回的 nextCursor 作为下次 --cursor
|
||||
- ⚠️ 存在同步盲区:新建群后较长时间(实测 15 分钟后全量翻页仍查不到)内不会出现在 list-all 里;而 `chat group list-my-groups` / `chat search` 能立即查到。要确认刚建的群,用 list-my-groups 或 search,别依赖 list-all
|
||||
```
|
||||
|
||||
### group list-join-validations (分页拉取入群验证记录)
|
||||
@@ -1401,27 +1606,27 @@ Flags:
|
||||
|
||||
### group audit-join-validation (审批入群验证)
|
||||
|
||||
#### 审批入群验证 — 通过、拒绝、删除单个审核
|
||||
#### 审批入群验证 — 通过、删除单个审核
|
||||
|
||||
支持通过、拒绝、删除、忽略、拒绝并拉黑等操作。
|
||||
当前仅 AuditApprove(通过)和 AuditDelete(删除)两个动作可用。
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws chat group audit-join-validation [flags]
|
||||
Example:
|
||||
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditApprove
|
||||
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditRefuse --description "不符合入群条件"
|
||||
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditDelete
|
||||
# 查询入群验证记录: dws chat group list-join-validations
|
||||
Flags:
|
||||
--group string 群 openConversationId (必填)
|
||||
--record-id string 申请记录 ID (必填)
|
||||
--applicant string 申请人 openDingTalkId (必填)
|
||||
--inviter string 邀请人 openDingTalkId (必填)
|
||||
--status string 审批动作: AuditApprove/AuditDelete/AuditIgnore/AuditRefuse/AuditBlock (必填)
|
||||
--status string 审批动作: AuditApprove(通过) / AuditDelete(删除) (必填)
|
||||
--description string 审批说明(可选)
|
||||
|
||||
注意:
|
||||
- status 可选值: AuditApprove(通过), AuditDelete(删除), AuditIgnore(忽略), AuditRefuse(拒绝), AuditBlock(拒绝且拉黑)
|
||||
- status 仅支持 AuditApprove(通过) 和 AuditDelete(删除);AuditIgnore(忽略)、AuditRefuse(拒绝)、AuditBlock(拒绝且拉黑) 会被服务端拒绝报 unsupported audit status,属服务端限制
|
||||
- record-id、applicant、inviter 可通过 dws chat group list-join-validations 查询获得
|
||||
```
|
||||
|
||||
@@ -1447,6 +1652,9 @@ Flags:
|
||||
用户说"踢人/移除群成员" → `chat group members remove`
|
||||
用户说"加机器人到群" → `chat group members add-bot`
|
||||
用户说"改群名" → `chat group rename`
|
||||
用户说"设置群备注/给群加备注" → `chat group update-alias`
|
||||
用户说"改我在群里的昵称/设置群昵称" → `chat group update-nick`
|
||||
用户说"批量查群成员信息/按ID查群成员" → `chat group members list-by-ids`
|
||||
用户说"聊天记录/会话消息/拉取会话" → `chat message list`
|
||||
用户说"某人发给我的消息/指定发送者/某人的消息" → `chat message list-by-sender`(用户未明确说"单聊"时优先使用,跨单聊/群聊)
|
||||
用户说"拉取和某人的单聊记录/单聊消息" → `chat message list --user`(用户明确说"单聊"时使用)
|
||||
@@ -1470,6 +1678,11 @@ Flags:
|
||||
用户说"置顶会话/置顶消息/我的置顶/查看置顶" → `chat list-top-conversations`
|
||||
用户说"查看会话分组/自定义分组" → `chat category list`
|
||||
用户说"某个分组下的会话/分组会话列表" → `chat category list-conversations`
|
||||
用户说"新建会话分组/创建分组" → `chat category create`
|
||||
用户说"删除会话分组" → `chat category delete`
|
||||
用户说"重命名分组/改分组名" → `chat category rename`
|
||||
用户说"把会话加入分组/会话归类到分组" → `chat category add-conv`
|
||||
用户说"把会话移出分组/从分组移除会话" → `chat category remove-conv`
|
||||
用户说"根据群号查群信息/群号查群/群号转openConversationId" → `chat group get-by-group-id`(当用户发消息时只提供了群号,用此工具将群号转为 openConversationId,再调用发消息接口)
|
||||
用户说"查看群身份/群的自定义身份列表" → `chat group-role list`
|
||||
用户说"创建/添加群身份" → `chat group-role add`
|
||||
@@ -1504,9 +1717,13 @@ Flags:
|
||||
用户说"引用回复/回复消息/引用消息回复" → `chat message reply`
|
||||
用户说"转发消息/转发一条消息/把消息转发到另一个群" → `chat message forward`
|
||||
用户说"合并转发/批量转发/合并转发多条消息" → `chat message combine-forward`
|
||||
用户说"转发话题/转发话题消息" → `chat message forward-topic`
|
||||
用户说"置顶消息/把消息置顶" → `chat message set-top-msg`
|
||||
用户说"取消置顶消息/撤销消息置顶" → `chat message unset-top-msg`
|
||||
用户说"上传图片拿mediaId/上传媒体" → `chat media upload`
|
||||
用户说"群机器人列表/群里有哪些机器人/查看群机器人" → `chat group bots`
|
||||
用户说"从群里移除机器人/踢出机器人" → `chat group members remove-bot`
|
||||
用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 openDingTalkId 可发单聊)
|
||||
用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 botOpenDingTalkId 可发单聊)
|
||||
用户说"给机器人发单聊/给机器人发消息/跟机器人聊天" → 必须先 `chat bot find`(拿 openDingTalkId)→ 再 `chat message send --open-dingtalk-id`(search 没有 openDingTalkId,无法发单聊)
|
||||
用户说"我创建的机器人/我的机器人/我自己的机器人/查看我的机器人" → `chat bot search`(仅我创建的机器人,无 openDingTalkId)
|
||||
用户说"解散群/解散群聊" → `chat group dismiss`
|
||||
@@ -1783,7 +2000,7 @@ Flags:
|
||||
| `aisearch person` | `userId` | message send 的 --user、send-by-bot 的 --users、send-by-bot 的 --at-user-ids、list-by-sender 的 --sender-user-id |
|
||||
| `aisearch person` → `contact user get` | `openDingTalkId` | message send 的 --at-open-dingtalk-ids、--open-dingtalk-id、send-by-bot 的 --open-dingtalk-ids、send-by-bot 的 --at-open-dingtalk-ids、list-by-sender 的 --sender-open-dingtalk-id、message list 的 --open-dingtalk-id |
|
||||
| `chat bot search` | `robotCode` | send-by-bot / recall-by-bot 的 --robot-code(仅我创建的机器人,无 openDingTalkId) |
|
||||
| `chat bot find` | `openDingTalkId` | 给机器人发单聊消息(全部可用机器人,额外返回 openDingTalkId) |
|
||||
| `chat bot find` | `botOpenDingTalkId` | 给机器人发单聊消息(send --open-dingtalk-id;字段名是 botOpenDingTalkId,非 openDingTalkId) |
|
||||
| `chat message send-by-bot` | `processQueryKey` | recall-by-bot 的 --keys |
|
||||
| `chat message send` | `openTaskId` | query-send-status 的 --open-task-id |
|
||||
| `chat message list` | `openMessageId` | recall 的 --msg-id |
|
||||
@@ -1826,7 +2043,7 @@ Flags:
|
||||
- `chat search-common` 搜索共同群,`--nicks` 传人员昵称(逗号分隔),`--match-mode` AND/OR 控制匹配逻辑,分页用 `--limit`(默认 20)/`--cursor`
|
||||
- `chat list-top-conversations` 拉取置顶会话列表,分页用 `--limit`(默认 1000)/`--cursor`;用户询问"置顶会话"或"置顶消息"时均路由到此命令
|
||||
- `--user` 和 `--open-dingtalk-id` 本质上都是发起单聊操作,只是用户标识格式不同:userId 为企业内部应用常用标识,openDingTalkId 为三方应用或跨组织场景下的用户标识,服务端对两种 ID 的解析逻辑不同
|
||||
- `--time` 格式: `yyyy-MM-dd HH:mm:ss`,为拉取消息的起始时间点;`--forward` 控制方向(默认 true,拉给定时间之后的消息),`--limit` 控制数量
|
||||
- `--time` 格式: `yyyy-MM-dd HH:mm:ss`,为拉取消息的起始时间点;`--direction` 控制方向(newer=从给定时间往现在拉,older=从给定时间往以前拉),`--limit` 控制数量
|
||||
- `chat search` 挂在 `chat` 下(非 `chat group` 下),路径为 `dws chat search`
|
||||
- `send-by-bot` 群聊传 `--group`,单聊传 `--users` 或 `--open-dingtalk-ids`,与 `--group` 互斥且必选其一;群聊时可选 `--at-user-ids` @指定成员(传 userId 列表)或 `--at-open-dingtalk-ids` @指定成员(传 openDingtalkId 列表),content 中需包含对应 @标识;`--at-all` @所有人;群聊场景如果返回"机器人不存在"错误,需先通过 `chat group members add-bot --group <openConversationId> --robot-code <robot-code>` 将机器人邀请进群后再发送
|
||||
- `recall-by-bot` 群聊传 `--group` + `--keys`,单聊仅传 `--keys`(不传 `--group` 即为单聊撤回)
|
||||
|
||||
@@ -38,9 +38,10 @@ Example:
|
||||
dws contact user search --query "张三"
|
||||
Flags:
|
||||
--query string 搜索关键词 (必填)
|
||||
Returns: (列表,每项包含以下字段)
|
||||
Returns: (顶层 `result` 为列表,每项包含以下字段)
|
||||
name string 成员姓名
|
||||
nick string 成员昵称
|
||||
flowerName string 花名(无花名时为 null)
|
||||
userId string 成员 ID(仅同事关系时返回)
|
||||
title string 员工职位(仅同事关系时返回)
|
||||
openDingTalkId string 当前用户视角下的目标用户唯一标识,不可跨用户共享;可用于发消息等好友关系场景的操作
|
||||
@@ -84,6 +85,8 @@ Flags:
|
||||
|
||||
查询花名册有权限的字段列表,根据当前用户查询花名册有权限的字段列表。认证信息(corpId、optUserId)由系统自动注入,无需手动传入。
|
||||
|
||||
> **前置条件:需操作人具备花名册管理权限。** 无权限时返回业务错误 `操作人无花名册管理权限`(非命令写法问题,不要改参数)。
|
||||
|
||||
#### 查询员工花名册字段信息(个人档案)
|
||||
```
|
||||
Usage:
|
||||
@@ -100,6 +103,8 @@ Flags:
|
||||
花名册字段包含:试用/转正信息、个人/家庭信息、学历信息、银行卡/合同信息、紧急联系人和其他企业自定义信息。
|
||||
|
||||
> **与 `contact user get` 的区别**:`user get` 返回组织管理信息(部门、主管、管理员权限),`user profile get` 返回个人档案信息(学历、家庭、银行卡等)。
|
||||
>
|
||||
> **前置条件:需操作人具备花名册管理权限。** 无权限时返回业务错误 `操作人无花名册管理权限`(非命令写法问题,不要改参数)。
|
||||
|
||||
### dismission (离职员工)
|
||||
|
||||
@@ -126,6 +131,8 @@ Flags:
|
||||
查询离职员工列表,支持按员工姓名、离职日期范围、部门进行过滤。认证信息(corpId、optUserId)由系统自动注入,无需手动传入。
|
||||
`--start` 和 `--end` 必须同时设置或同时不设置,不允许只传其中一个。
|
||||
|
||||
> **前置条件:需操作人具备已离职人员花名册管理权限。** 无权限时返回业务错误 `操作人无已离职人员花名册管理权限`(非命令写法问题,不要改参数)。
|
||||
|
||||
### dept (部门查询)
|
||||
|
||||
#### 搜索部门
|
||||
@@ -136,6 +143,9 @@ Example:
|
||||
dws contact dept search --query "技术部"
|
||||
Flags:
|
||||
--query string 搜索关键词 (必填)
|
||||
Returns: (顶层 key 为 `deptList`,非 `result`,为列表,每项包含以下字段)
|
||||
deptId int 部门 ID
|
||||
deptName string 部门名称(命中关键词会用 <red>…</red> 高亮包裹,需自行去标签)
|
||||
```
|
||||
|
||||
#### 获取部门详情
|
||||
@@ -180,6 +190,9 @@ Example:
|
||||
dws contact dept list-members --depts 1 # 根部门
|
||||
Flags:
|
||||
--depts string 部门 ID 列表,逗号分隔 (必填)
|
||||
Returns: (顶层 key 为 `deptUserList`,非 `result`,为列表,每项形如 { "userInfo": { "name": ..., "userId": ... } })
|
||||
userInfo.name string 成员姓名
|
||||
userInfo.userId string 成员 ID
|
||||
Notes:
|
||||
- **钉钉根部门 `deptId=1`**;查根部门直属成员用 `--depts 1`
|
||||
- 仅返回**本部门**直接成员,**不含下级部门**成员;需含下级请先 `dept list-children` 枚举子部门,再对子 deptId 分别/合并调用 `list-members`
|
||||
@@ -200,7 +213,7 @@ Example:
|
||||
Flags:
|
||||
无
|
||||
Notes:
|
||||
- 无需参数,返回当前企业全部角色列表(labelId、labelName等)
|
||||
- 无需参数,返回当前企业全部角色,按 `groupName` 分组,每组 `labels[]` 里每个角色是 `{labelId, name}`(字段名是 `name`,不是 `labelName`)
|
||||
- 用于不知道准确角色名称时先浏览全部角色
|
||||
- 典型场景:用户说“企业所有主管/查所有管理员/财务人员有哪些”→ 先 label list 浏览全部角色,匹配目标角色后 label list-members 获取成员
|
||||
```
|
||||
@@ -347,7 +360,7 @@ dws contact user dismission search --depts 123456,789012 --hide-retirement=false
|
||||
| `user get-self/search` | `orgAuthEmail` | mail message send 的 --to/--cc (跨产品) |
|
||||
| `user get-self/search` | `userId` | profile get 的 --staff-id |
|
||||
| `user profile fields` | `fieldCode` | profile get 的 --fields |
|
||||
| `label list` | `labelId` / `labelName` | `label get --names` 或 `label list-members --id` |
|
||||
| `label list` | `labelId` / `name` | `label get --names` 或 `label list-members --id` |
|
||||
| `label get` | `labelId` | `label list-members` 的 --id |
|
||||
| `dept search/list-children` | `deptId` | dept get-info/list-children/list-members 的 --dept/--depts |
|
||||
| `dept search/list-children` | `deptId` | dismission search 的 --depts |
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|--------|------|
|
||||
| `dev app` | 应用生命周期(创建/查询/更新/删除/凭证/权限/成员/安全/网页/机器人/**建号**/版本/事件订阅) |
|
||||
| `dev connect` | **建联**:把现成机器人接到当前本地 agent(起 Stream,不建号) |
|
||||
| `dev doc` | 开放平台开发文档搜索(同 `dws devdoc`) |
|
||||
| `dev doc` | 开放平台开发文档搜索入口(当前网关未注册该工具键,`dev doc search` 会报「未找到指定工具」不可用;文档搜索一律走 `dws devdoc article search --query <关键词>`) |
|
||||
|
||||
> ⚠️ **关键区分**:`dws chat bot search/find` 只查询已有机器人(IM 视角);**创建/建号**机器人走 `dws dev app robot submit`;**建联**走 `dws dev connect`。"创建机器人"/"建联"一律走 `dev`,禁止走 `chat`。
|
||||
|
||||
@@ -75,8 +75,9 @@ dws dev app get --unified-app-id <unifiedAppId> --format json
|
||||
# 创建应用
|
||||
dws dev app create --name <名称> --desc <描述> --format json
|
||||
|
||||
# 更新应用信息
|
||||
dws dev app update --unified-app-id <unifiedAppId> --name <新名称> --format json
|
||||
# 更新应用信息(写操作:先 --dry-run 预览,确认后加 --yes 执行;不加 --yes 会被拦下)
|
||||
dws dev app update --unified-app-id <unifiedAppId> --name <新名称> --dry-run --format json
|
||||
dws dev app update --unified-app-id <unifiedAppId> --name <新名称> --yes --format json
|
||||
|
||||
# 停用/启用应用
|
||||
dws dev app disable --unified-app-id <unifiedAppId> --yes --format json
|
||||
@@ -117,11 +118,8 @@ dws dev app robot disable --unified-app-id <unifiedAppId> --format json
|
||||
## dev app credentials — 凭证
|
||||
|
||||
```bash
|
||||
# 查询 clientId/clientSecret
|
||||
# 查询 clientId/clientSecret(credentials 下只有 get 子命令)
|
||||
dws dev app credentials get --unified-app-id <unifiedAppId> --format json
|
||||
|
||||
# 重置凭证(旧 secret 立即失效)
|
||||
dws dev app credentials reset --unified-app-id <unifiedAppId> --yes --format json
|
||||
```
|
||||
|
||||
---
|
||||
@@ -146,8 +144,8 @@ dws dev connect --channel opencode \
|
||||
dws dev connect --daemon \
|
||||
--robot-client-id <clientId> --robot-client-secret <clientSecret>
|
||||
|
||||
# 查看/停止后台连接器
|
||||
dws dev connect status --format json
|
||||
# 查看/停止后台连接器(status/list 忽略全局 --format,出 JSON 用专属 --json)
|
||||
dws dev connect status --json
|
||||
dws dev connect stop
|
||||
```
|
||||
|
||||
@@ -186,8 +184,8 @@ dws dev app event unsubscribe --unified-app-id <unifiedAppId> --event-codes <cod
|
||||
|
||||
```bash
|
||||
dws dev app permission list --unified-app-id <unifiedAppId> --format json
|
||||
dws dev app permission add --unified-app-id <unifiedAppId> --scope-code <scopeValue> --format json
|
||||
dws dev app permission remove --unified-app-id <unifiedAppId> --scope-code <scopeValue> --yes --format json
|
||||
dws dev app permission add --unified-app-id <unifiedAppId> --scope-values <scopeValue> --format json
|
||||
dws dev app permission remove --unified-app-id <unifiedAppId> --scope-values <scopeValue> --yes --format json
|
||||
```
|
||||
|
||||
---
|
||||
@@ -207,7 +205,7 @@ dws dev app version status --unified-app-id <unifiedAppId> --version-id
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **`clientSecret` 只在 `robot result` 返回一次**,务必立即保存;遗失需走 `credentials reset` 重置
|
||||
- **`clientSecret` 只在 `robot result` 返回一次**,务必立即保存;dws 只能 `credentials get` 读取,不支持重置,遗失后到开放平台控制台重置密钥
|
||||
- 改配置后机器人不自动生效,需走 `version create → publish` 才上线
|
||||
- `hermes`/`openclaw` 渠道走官方建联,`dws dev connect` 不代建机器人,会输出指引后退出
|
||||
- 应用名在企业内唯一;`app list/get` 用 `--app-key` 过滤但不能定位单应用,定位单应用须用 `--unified-app-id`
|
||||
|
||||
@@ -31,12 +31,12 @@ Flags:
|
||||
Usage:
|
||||
dws ding message list [flags]
|
||||
Example:
|
||||
dws ding message list
|
||||
dws ding message list --type ALL
|
||||
dws ding message list --type UNREAD
|
||||
dws ding message list --type SEND --cursor 10
|
||||
Flags:
|
||||
--cursor int 分页游标 (首次传 0, 翻页传返回的 nextCursor)
|
||||
--type string 消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED (可选, 不传返回全部)
|
||||
--type string 消息类型: ALL / UNREAD / SEND / NEW_COMMENT / DELETED (默认 ALL; 服务端不接受空值, CLI 不传时会自动按 ALL 查询, 故裸跑 `ding message list` 即可)
|
||||
```
|
||||
|
||||
### 查看 DING 接收状态
|
||||
@@ -45,7 +45,7 @@ Usage:
|
||||
dws ding message receiver-status [flags]
|
||||
Example:
|
||||
dws ding message receiver-status --ding-id <OPEN_DING_ID>
|
||||
# 查询 dingId: dws ding message list
|
||||
# 查询 dingId: dws ding message list --type ALL
|
||||
Flags:
|
||||
--ding-id string DING 消息 openDingId (必填)
|
||||
```
|
||||
@@ -76,13 +76,13 @@ Usage:
|
||||
dws ding message recall-personal [flags]
|
||||
Example:
|
||||
dws ding message recall-personal --id <openDingId>
|
||||
# 查询 openDingId: dws ding message list
|
||||
# 查询 openDingId: dws ding message list --type ALL
|
||||
Flags:
|
||||
--id string DING 消息 openDingId (必填)
|
||||
|
||||
注意:
|
||||
- 与 `ding message recall`(机器人身份)不同:recall-personal 以当前用户身份撤回,无需 --robot-code
|
||||
- openDingId 可通过 `dws ding message list` 或 `send-personal` 返回值获取
|
||||
- openDingId 可通过 `dws ding message list --type ALL` 或 `send-personal` 返回值获取
|
||||
```
|
||||
|
||||
### 消息转 DING — 将聊天消息转为 DING 通知发送给指定接收者
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
dws doc --help
|
||||
|
||||
# 查看具体命令的完整参数说明
|
||||
dws doc list --help
|
||||
dws doc read --help
|
||||
dws doc create --help
|
||||
dws doc block insert --help
|
||||
|
||||
@@ -25,46 +25,19 @@ dws doc permission --help
|
||||
|
||||
## 命令总览
|
||||
|
||||
### 搜索文档
|
||||
```
|
||||
Usage:
|
||||
dws doc search [flags]
|
||||
Example:
|
||||
dws doc search --query "会议纪要"
|
||||
dws doc search
|
||||
dws doc search --extensions pdf,docx
|
||||
dws doc search --query "方案" --created-from 1700000000000 --created-to 1710000000000
|
||||
dws doc search --creator-uids uid1,uid2
|
||||
dws doc search --workspace-ids wsId1,wsId2
|
||||
Flags:
|
||||
--query string 搜索关键词 (不传则返回最近访问)
|
||||
--extensions strings 按文件扩展名过滤,不含点号,逗号分隔 (如 pdf,docx,png)。支持的在线文档类型后缀名: adoc=文字, axls=表格, appt=演示文稿, awbd=白板, adraw=画板, amind=脑图, able=多维表格, aform=收集表
|
||||
--created-from int 创建时间起始 (毫秒时间戳,含)
|
||||
--created-to int 创建时间截止 (毫秒时间戳,含)
|
||||
--visited-from int 访问时间起始 (毫秒时间戳,含)
|
||||
--visited-to int 访问时间截止 (毫秒时间戳,含)
|
||||
--creator-uids strings 按创建者用户 ID 过滤,逗号分隔
|
||||
--editor-uids strings 按编辑者用户 ID 过滤,逗号分隔
|
||||
--mentioned-uids strings 按 @提及的用户 ID 过滤,逗号分隔
|
||||
--workspace-ids strings 按知识库 ID 过滤,支持知识库 URL,逗号分隔
|
||||
--page-size int 每页数量 (默认 10,最大 30)
|
||||
--page-token string 分页游标 (从上次结果的 nextPageToken 获取)
|
||||
```
|
||||
### 搜索 / 遍历文件(已迁移,不再是 doc 子命令)
|
||||
|
||||
### 遍历文件列表
|
||||
```
|
||||
Usage:
|
||||
dws doc list [flags]
|
||||
Example:
|
||||
dws doc list
|
||||
dws doc list --folder <FOLDER_ID>
|
||||
dws doc list --workspace <WS_ID> --page-size 20
|
||||
Flags:
|
||||
--folder string 文件夹 ID 或 URL
|
||||
--workspace string 知识库 ID
|
||||
--page-size int 每页数量 (默认 50,最大 50)
|
||||
--page-token string 分页游标 (从上次结果的 nextPageToken 获取)
|
||||
```
|
||||
> **弃用提示**:`dws doc search` 和 `dws doc list` 已迁移到 `drive` / `wiki`。这两条虽仍能跑,但每次都会打印弃用警告,请改用下面的命令,命令详情见 [drive.md](./drive.md)。
|
||||
|
||||
| 旧命令(弃用) | 改用 | 场景 |
|
||||
|--------------|------|------|
|
||||
| `dws doc search --query "<关键词>"` | `dws drive search --query "<关键词>"` | 全局搜文档/文件(跨钉盘+文档空间聚合) |
|
||||
| `dws doc search --workspace-ids <id>` | `dws wiki node search --workspace <id> --query "<关键词>"` | 指定知识库内搜索 |
|
||||
| `dws doc list` | `dws drive list` | 遍历「我的文档」/钉盘根目录 |
|
||||
| `dws doc list --folder <id>` | `dws drive list --folder <id>` | 遍历指定文件夹 |
|
||||
| `dws doc list --workspace <id>` | `dws drive list --workspace <id>` 或 `dws wiki node list --workspace <id>` | 遍历知识库 |
|
||||
|
||||
拿到 `nodeId` 后,`doc read` / `doc info` / `doc update` / `doc block` 等内容级命令照常用。
|
||||
|
||||
### 获取文档元信息
|
||||
```
|
||||
@@ -108,22 +81,26 @@ Flags:
|
||||
--content-file string 从文件读取初始 Markdown
|
||||
```
|
||||
|
||||
### 创建其他类型文件 (表格/脑图/白板/多维表/画板)
|
||||
### 创建其他类型文件 (表格/脑图/白板/多维表/画板/文件夹)
|
||||
|
||||
> **弃用提示**:`dws doc file create --type` 已弃用,执行时会打印 `deprecated, use 'dws wiki node create --type <type>'`。创建各类型文件节点改用 `dws wiki node create`(与 intent-guide 一致);创建普通文件夹用 `dws drive mkdir`。
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws doc file create [flags]
|
||||
dws wiki node create [flags]
|
||||
Example:
|
||||
dws doc file create --name "项目周报" --type adoc
|
||||
dws doc file create --name "数据统计" --type axls --folder <FOLDER_ID>
|
||||
dws doc file create --name "思维导图" --type amind --workspace <WS_ID>
|
||||
dws doc file create --name "子文件夹" --type folder
|
||||
dws wiki node create --workspace <WS_ID> --name "数据统计" --type axls
|
||||
dws wiki node create --workspace <WS_ID> --name "思维导图" --type amind
|
||||
dws wiki node create --workspace <WS_ID> --name "方案目录" --type folder --folder <PARENT_NODE_ID>
|
||||
Flags:
|
||||
--workspace string 目标知识库 ID (必填 — wiki node create 不支持不带 workspace 创建)
|
||||
--name string 文件名称 (必填)
|
||||
--type string 文件类型 (必填): adoc=文档, axls=表格, appt=演示, adraw=白板, amind=脑图, able=多维表, folder=文件夹
|
||||
--folder string 目标文件夹 ID 或 URL
|
||||
--workspace string 目标知识库 ID 或 URL
|
||||
--type string 文件类型: adoc=文档, axls=表格, appt=演示, adraw=白板, amind=脑图, able=多维表, folder=文件夹
|
||||
--folder string 父节点 ID (选填)
|
||||
```
|
||||
|
||||
> 说明:`wiki node create` **必须**带 `--workspace`(知识库上下文),不能像旧 `doc file create` 那样在「我的文档」个人空间直接建。要在个人空间/钉盘建普通文件夹,用 `dws drive mkdir --name "<名称>"`(见 [drive.md](./drive.md))。
|
||||
|
||||
### 更新文档内容
|
||||
```
|
||||
Usage:
|
||||
@@ -154,31 +131,24 @@ Flags:
|
||||
--convert 是否转换为钉钉在线文档
|
||||
```
|
||||
|
||||
### 下载文件到本地
|
||||
### 下载文件到本地(已迁移到 drive)
|
||||
|
||||
> **弃用提示**:`dws doc download` 已迁移到 `dws drive download`(执行 `doc download` 会打印弃用警告)。下载已有文件(PDF/图片/附件等非在线文档)改用:
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws doc download [flags]
|
||||
Example:
|
||||
dws doc download --node <NODE_ID>
|
||||
dws doc download --node <NODE_ID> --output ./report.pdf
|
||||
dws doc download --node "https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>" --output ~/downloads/
|
||||
Flags:
|
||||
--node string 文件节点 ID 或 URL (必填)
|
||||
--output string 本地保存路径 (文件路径或目录,必填)
|
||||
dws drive download --node <NODE_ID> --output ~/downloads/
|
||||
dws drive download --node <NODE_ID> --output ./report.pdf
|
||||
```
|
||||
|
||||
### 创建文件夹
|
||||
```
|
||||
Usage:
|
||||
dws doc folder create [flags]
|
||||
Example:
|
||||
dws doc folder create --name "项目资料"
|
||||
dws doc folder create --name "子文件夹" --folder <PARENT_FOLDER_ID>
|
||||
Flags:
|
||||
--name string 文件夹名称 (必填)
|
||||
--folder string 父文件夹 ID 或 URL
|
||||
--workspace string 目标知识库 ID
|
||||
```
|
||||
> 注意区分:**在线文档(ALIDOC)导出为 docx** 仍走 `doc export`(内容级命令,未迁移);`download` 只下载已有文件。命令详情见 [drive.md](./drive.md)。
|
||||
|
||||
### 创建文件夹(已迁移)
|
||||
|
||||
> **弃用提示**:`dws doc folder create` 已弃用。创建文件夹改用:
|
||||
> - 个人空间/钉盘 → `dws drive mkdir --name "<名称>" [--folder <父节点>]`
|
||||
> - 知识库内 → `dws wiki node create --workspace <WS_ID> --name "<名称>" --type folder`
|
||||
>
|
||||
> 命令详情见 [drive.md](./drive.md)。
|
||||
|
||||
### 复制文档/文件
|
||||
```
|
||||
@@ -291,11 +261,11 @@ Usage:
|
||||
Example:
|
||||
dws doc comment list --node <DOC_ID>
|
||||
dws doc comment list --node <DOC_ID> --type inline --resolve-status unresolved
|
||||
dws doc comment list --node <DOC_ID> --page-size 20 --next-token <TOKEN>
|
||||
dws doc comment list --node <DOC_ID> --limit 20 --cursor <TOKEN>
|
||||
Flags:
|
||||
--node string 目标文档的标识,支持传入 URL 或 ID (必填)
|
||||
--page-size int 每页返回的评论数量,默认 50,最大 50
|
||||
--next-token string 分页游标,从上一次请求的返回结果中获取 (首次请求不传)
|
||||
--limit int 每页返回的评论数量,默认 50,最大 50
|
||||
--cursor string 分页游标,从上一次请求的返回结果中获取 (首次请求不传)
|
||||
--type string 按评论类型过滤: global (全文评论) / inline (划词评论)
|
||||
--resolve-status string 按解决状态过滤: resolved (已解决) / unresolved (未解决)
|
||||
```
|
||||
@@ -459,7 +429,7 @@ CLI **不会**自动执行回读验证。**Agent 必须在文档写入完成后
|
||||
Usage:
|
||||
dws doc delete [flags]
|
||||
Example:
|
||||
dws doc delete --node <DOC_ID> --format json # 查询 nodeId: dws doc search --query "..." 或 dws doc list
|
||||
dws doc delete --node <DOC_ID> --format json # 查询 nodeId: dws drive search --query "..." 或 dws drive list
|
||||
Flags:
|
||||
--node string 文档/文件 ID 或 URL (必填)
|
||||
```
|
||||
@@ -604,7 +574,7 @@ Flags:
|
||||
- `dentryUuid` 是 `alidocs` URL `/i/nodes/{dentryUuid}` 的最后一段,在 `doc` 场景中等价于可传入 CLI 的 `nodeId`;不要把它改写成数字 ID。
|
||||
- `dentryId` 通常是纯数字,**不是** `doc` 的 `nodeId`,也不是 `doc --folder` 的目标文件夹 ID;不要把数字 `dentryId` 当作 `--node`、`--folder`,也不要当作隐藏兼容 alias(如 `--parent-id` / `--parent-folder-id`)的取值。
|
||||
- 目标父文件夹的 canonical flag 是 `--folder <folderNodeId或folderUrl>`,目标知识库使用 `--workspace <workspaceId或workspaceUrl>`。CLI 可能接受 `--parent-id` / `--parent-folder-id` 作为隐藏兼容 alias,但 skill 正文和示例应统一教 `--folder`,不要把 drive/chat 链路里的数字 parentId 搬到 doc 命令。
|
||||
- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `doc search` / `doc list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。
|
||||
- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `drive search` / `drive list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。
|
||||
|
||||
### 处理流程
|
||||
|
||||
@@ -612,15 +582,15 @@ Flags:
|
||||
用户输入含 alidocs.dingtalk.com URL
|
||||
→ 若是用户直接提供的原始 URL,先按链接规范做 probe
|
||||
→ 提取 DOC_ID(URL 路径最后一段)
|
||||
→ 结合用户意图选择命令(doc 默认 read,folder 默认 list,file 默认 download)
|
||||
→ 结合用户意图选择命令(doc 默认 read,folder 默认 drive list,file 默认 drive download)
|
||||
→ 将 DOC_ID 传给 --node 参数
|
||||
```
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户说"找文档/搜文档/最近文档":
|
||||
- 搜索 → `search`
|
||||
- 浏览 → `list`
|
||||
- 搜索 → `dws drive search`(全局)或 `dws wiki node search --workspace <id>`(指定知识库)
|
||||
- 浏览 → `dws drive list`(`doc search` / `doc list` 已弃用,见本文顶部「搜索 / 遍历文件」)
|
||||
|
||||
用户说"看文档/读内容/文档内容":
|
||||
- 读取 → `read` (需文档 ID 或 URL)
|
||||
@@ -637,27 +607,29 @@ Flags:
|
||||
> - 用户说"创建多维表格/新建 AI 表格/建 base/数据库表" → 走 [`dws aitable base create`](./aitable.md)(able 多维表格)
|
||||
|
||||
用户说"新建脑图/白板/多维表/演示文稿":
|
||||
- 用 `file create --type` 指定类型 (axls/amind/adraw/able/appt/adraw)
|
||||
- 用 `dws wiki node create --workspace <WS_ID> --type <type>` 指定类型 (axls/amind/adraw/able/appt);`doc file create` 已弃用
|
||||
- 注意 `wiki node create` 必须带 `--workspace`(知识库上下文)
|
||||
|
||||
用户说"建文件夹/新建目录":
|
||||
- 创建 → `folder create` 或 `file create --type folder`
|
||||
- 个人空间/钉盘 → `dws drive mkdir --name "<名称>"`
|
||||
- 知识库内 → `dws wiki node create --workspace <WS_ID> --type folder`(`doc folder create` / `doc file create --type folder` 已弃用)
|
||||
|
||||
用户说"上传文件/传文件/上传到文档/上传到知识库":
|
||||
- 上传 → `upload`(需本地文件路径)
|
||||
- 上传并转换 → `upload --convert`
|
||||
|
||||
用户说"下载/导出/下载到本地/导出文档/导出为Word/导出为docx/把文档导出来":
|
||||
- **必须先判断目标文件类型**,再决定走 `export` 还是 `download`:
|
||||
- 在线文档(alidocs/adoc)→ **`export`**(格式转换后导出为 docx)
|
||||
- 已有文件(PDF、图片、附件、视频等非在线文档)→ **`download`**(直接下载原始文件)
|
||||
- **必须先判断目标文件类型**,再决定走 `doc export` 还是 `drive download`:
|
||||
- 在线文档(alidocs/adoc)→ **`doc export`**(内容级命令,格式转换后导出为 docx,未迁移)
|
||||
- 已有文件(PDF、图片、附件、视频等非在线文档)→ **`drive download`**(`doc download` 已弃用)
|
||||
- 判断方法:
|
||||
1. 用户明确说"导出文档"/"导出为 Word/docx" → 直接走 `export`
|
||||
2. 用户明确说"下载 PDF/图片/附件" → 直接走 `download`
|
||||
3. 不确定时,先用 `info --node <ID>` 查询节点信息,根据 `contentType` 判断:
|
||||
- `contentType` = `ALIDOC` → 走 `export`
|
||||
- `contentType` = `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `download`
|
||||
1. 用户明确说"导出文档"/"导出为 Word/docx" → 直接走 `doc export`
|
||||
2. 用户明确说"下载 PDF/图片/附件" → 直接走 `drive download`
|
||||
3. 不确定时,先用 `doc info --node <ID>` 查询节点信息,根据 `contentType` 判断:
|
||||
- `contentType` = `ALIDOC` → 走 `doc export`
|
||||
- `contentType` = `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `drive download`
|
||||
|
||||
> **严禁将"导出文档"直接路由到 `download`**。`download` 只能下载已有文件(原样下载),`export` 是将在线文档格式转换后导出为 docx,两者完全不同。
|
||||
> **严禁将"导出文档"直接路由到 `drive download`**。`drive download` 只能下载已有文件(原样下载),`doc export` 是将在线文档格式转换后导出为 docx,两者完全不同。
|
||||
|
||||
用户说"复制文档/拷贝一份/复制到":
|
||||
- 复制 → `copy` (需源 --node 和目标 --folder/--workspace)
|
||||
@@ -702,7 +674,7 @@ Flags:
|
||||
|
||||
**用户直接粘贴文档 URL(无其他指令)**:
|
||||
- 默认 → `read`(读取文档内容)
|
||||
- 如 URL 明显是文件夹 → `list`(列出文件夹内容)
|
||||
- 如 URL 明显是文件夹 → `drive list`(列出文件夹内容;doc list 已弃用)
|
||||
|
||||
**用户粘贴 URL + 附加指令**:
|
||||
- "帮我看看这个文档" → `read`
|
||||
@@ -728,7 +700,7 @@ Flags:
|
||||
|
||||
**用户直接粘贴文档 URL(无其他指令)**:
|
||||
- 默认 → `read`(读取文档内容)
|
||||
- 如 URL 明显是文件夹 → `list`(列出文件夹内容)
|
||||
- 如 URL 明显是文件夹 → `drive list`(列出文件夹内容;doc list 已弃用)
|
||||
|
||||
**用户粘贴 URL + 附加指令**:
|
||||
- "帮我看看这个文档" → `read`
|
||||
@@ -743,11 +715,11 @@ Flags:
|
||||
```bash
|
||||
# ── 工作流 1: 浏览并阅读文档 ──
|
||||
|
||||
# 1. 浏览我的文档根目录
|
||||
dws doc list --format json
|
||||
# 1. 浏览我的文档/钉盘根目录(doc list 已弃用 → drive list)
|
||||
dws drive list --format json
|
||||
|
||||
# 2. 浏览子文件夹
|
||||
dws doc list --folder <DOC_FOLDER_NODE_ID> --format json
|
||||
dws drive list --folder <DOC_FOLDER_NODE_ID> --format json
|
||||
|
||||
# 3. 获取文档元信息 (标题、类型、权限)
|
||||
dws doc info --node <DOC_ID> --format json
|
||||
@@ -757,8 +729,8 @@ dws doc read --node <DOC_ID> --format json
|
||||
|
||||
# ── 工作流 2: 创建文档并写入内容 ──
|
||||
|
||||
# 1. (可选) 创建文件夹 — 提取 nodeId
|
||||
dws doc folder create --name "项目资料" --format json
|
||||
# 1. (可选) 创建文件夹 — 提取 nodeId(doc folder create 已弃用 → drive mkdir)
|
||||
dws drive mkdir --name "项目资料" --format json
|
||||
|
||||
# 2. 创建文档 — 提取 nodeId
|
||||
dws doc create --name "项目周报" --folder <DOC_FOLDER_NODE_ID> --format json
|
||||
@@ -782,18 +754,18 @@ dws doc upload --file ./slides.pptx --name "Q1汇报.pptx" --folder <DOC_FOLDER_
|
||||
dws doc upload --file ./data.xlsx --workspace <WS_ID> --convert
|
||||
|
||||
# ── 工作流 5: 下载/导出文件到本地 ──
|
||||
# 必须先用 info 判断文件类型,再决定用 download 还是 export:
|
||||
# - contentType 为 ALIDOC(在线文档)→ 用 export
|
||||
# - contentType 为 DOCUMENT/IMAGE/VIDEO 等(已有文件)→ 用 download
|
||||
# 必须先用 info 判断文件类型,再决定用 drive download 还是 doc export:
|
||||
# - contentType 为 ALIDOC(在线文档)→ 用 doc export
|
||||
# - contentType 为 DOCUMENT/IMAGE/VIDEO 等(已有文件)→ 用 drive download
|
||||
|
||||
# 步骤 1: 查询文件类型
|
||||
dws doc info --node <NODE_ID> --format json
|
||||
# 根据返回的 contentType 字段判断:
|
||||
|
||||
# 如果是已有文件 (非 ALIDOC),用 download:
|
||||
dws doc download --node <NODE_ID> --output ~/downloads/
|
||||
# 如果是已有文件 (非 ALIDOC),用 drive download(doc download 已弃用):
|
||||
dws drive download --node <NODE_ID> --output ~/downloads/
|
||||
|
||||
# 如果是在线文档 (ALIDOC),用 export:
|
||||
# 如果是在线文档 (ALIDOC),用 doc export:
|
||||
dws doc export --node <NODE_ID> --output ~/downloads/
|
||||
|
||||
# ── 工作流 6: 上传附件并插入文档 ──
|
||||
@@ -842,10 +814,10 @@ dws doc block delete --node <DOC_ID> --block-id <BLOCK_ID> --yes
|
||||
|
||||
# 获取 nodeId 的三种方式(按场景选择,无需全部执行):
|
||||
# 方式 A: 用户直接提供文档 URL — 直接传给 --node,无需额外查询
|
||||
# 方式 B: 搜索文档 — 从返回中提取 nodeId
|
||||
dws doc search --query "项目周报" --format json
|
||||
# 方式 C: 浏览文件夹 — 从返回中提取 nodeId
|
||||
dws doc list --folder <DOC_FOLDER_NODE_ID> --format json
|
||||
# 方式 B: 搜索文档 — 从返回中提取 nodeId(doc search 已弃用 → drive search)
|
||||
dws drive search --query "项目周报" --format json
|
||||
# 方式 C: 浏览文件夹 — 从返回中提取 nodeId(doc list 已弃用 → drive list)
|
||||
dws drive list --folder <DOC_FOLDER_NODE_ID> --format json
|
||||
# 注意: 这里的 <DOC_FOLDER_NODE_ID> 是文件夹 nodeId/dentryUuid 或文件夹 URL,不是数字 dentryId;示例统一使用 canonical --folder。
|
||||
|
||||
# 复制文档到指定文件夹(--node 支持 ID 或 URL)
|
||||
@@ -907,19 +879,16 @@ dws doc export --node <DOC_ID_OR_URL> --output ./exported.docx
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `list` | `nodes[].nodeId` | read / info / update / block 操作的 --node |
|
||||
| `list` | folder 类型的 `nodeId` | list 的 --folder, create 的 --folder |
|
||||
| `search` | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update 的 --node;创建时间与创建者信息 |
|
||||
| `drive list`(原 `doc list`,已弃用) | `nodes[].nodeId` / folder 类型 `nodeId` | read / info / update / block 的 --node;folder 用作 `--folder` |
|
||||
| `drive search`(原 `doc search`,已弃用) | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update 的 --node;创建时间与创建者信息 |
|
||||
| `create` | `nodeId` | update / block 操作的 --node |
|
||||
| `folder create` | `nodeId` | create / list / upload 的 --folder |
|
||||
| `drive mkdir`(原 `doc folder create`,已弃用) | `nodeId` | create 的 --folder |
|
||||
| `block list` | `blockId` | block insert 的 --ref-block, block update/delete 的 --block-id |
|
||||
| `upload` | `nodeId` / URL | 上传后文件的访问链接 |
|
||||
| `download` | 本地文件路径 | 下载后的文件保存位置 |
|
||||
| `comment list` | `commentList[].commentKey` | comment reply 的 --comment-key |
|
||||
| `comment create` / `comment create-inline` | `commentKey` | comment reply 的 --comment-key |
|
||||
| `block list` | `blockId` + 文本内容 | comment create-inline 的 --block-id 及 --start/--end 计算 |
|
||||
| `contact user search` | `userId` | comment create / create-inline / reply 的 --mention |
|
||||
| `file create` | `nodeId` | 后续 read / update / block 操作的 --node(仅 adoc 支持 read/update,axls/amind 等类型用各自产品的命令) |
|
||||
| `wiki node create`(原 `doc file create`,已弃用) | `nodeId` | 后续 read / update / block 操作的 --node(仅 adoc 支持 read/update,axls/amind 等类型用各自产品的命令) |
|
||||
| `copy` / `move` | 新 `nodeId`(copy)或原 nodeId(move) | 后续 read / info 等的 --node |
|
||||
|
||||
## nodeId 多格式说明
|
||||
@@ -998,7 +967,7 @@ EOF
|
||||
- `read` 返回 Markdown 格式的文档内容,仅限有"下载"权限的文档
|
||||
- `read` 返回的内容中,文档里的附件会以 OSS 临时下载链接形式给出(如 `https://alidocs2.oss-cn-zhangjiakou.aliyuncs.com/res/.../att/<resourceId>.ext?Expires=...`),该链接会过期。链接过期后,可从 URL 路径中提取 `<resourceId>`(即 `/att/` 后、扩展名前的 UUID 部分),然后使用 `media download --node <DOC_ID> --resource-id <resourceId>` 重新获取下载链接
|
||||
- `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录
|
||||
- `create` 只能建"文档"(adoc);要建表格/脑图/白板/多维表/演示/文件夹,用 `file create --type`
|
||||
- `create` 只能建"文档"(adoc);要建表格/脑图/白板/多维表/演示,用 `dws wiki node create --workspace <id> --type <type>`(`doc file create` 已弃用);建普通文件夹用 `dws drive mkdir`
|
||||
- `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;简单内容追加建议用 `update --mode append`
|
||||
- `block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型 (table, callout 等) 使用 `--element` JSON
|
||||
- `--content` 参数中的换行必须使用**真实换行符**(即实际的换行字符,Unicode `U+000A`),而不是字面量字符串 `\n`(反斜杠加字母 n)。在通过程序或大模型构造此参数时,请确保字符串在发送前已正确反转义。如果传入的是两个字符的字面量 `\n`,所有内容将渲染在同一行,导致标题、段落和表格格式全部错乱。**含多行/表格/长文本时优先用 `--content-file path.md` 或 `--content -`(stdin),不经过 shell escape,换行和表格都保持原样**(详见下方「长 Markdown 写入」)。
|
||||
|
||||
@@ -15,11 +15,11 @@ Usage:
|
||||
Example:
|
||||
dws doc comment list --node <DOC_ID>
|
||||
dws doc comment list --node <DOC_ID> --type inline --resolve-status unresolved
|
||||
dws doc comment list --node <DOC_ID> --page-size 20 --next-token <TOKEN>
|
||||
dws doc comment list --node <DOC_ID> --limit 20 --cursor <TOKEN>
|
||||
Flags:
|
||||
--node string 目标文档的标识,支持传入 URL 或 ID (必填)
|
||||
--page-size int 每页返回的评论数量
|
||||
--next-token string 下一页 token,从上一次请求的返回结果中获取 (首次请求不传)
|
||||
--limit int 每页返回的评论数量,默认 50,最大 50
|
||||
--cursor string 分页游标,从上一次请求的返回结果中获取 (首次请求不传)
|
||||
--type string 按评论类型过滤: global (全文评论) / inline (划词评论)
|
||||
--resolve-status string 按解决状态过滤: resolved (已解决) / unresolved (未解决)
|
||||
```
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
|
||||
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
|
||||
|
||||
> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令:
|
||||
> - `doc download` → **`dws drive download`**(下载已有文件;在线文档导出 docx 仍走 `doc export`)
|
||||
> - `doc folder create` → **`dws drive mkdir`**(个人空间/钉盘)或 **`dws wiki node create --workspace <id> --type folder`**(知识库内)
|
||||
> - `doc upload` / `doc copy` / `doc move` / `doc rename` / `doc delete` 也在同一迁移方向(→ `drive`),命令用法以 `drive` 为准。
|
||||
|
||||
---
|
||||
|
||||
## doc upload(上传文件到钉钉文档/知识库)
|
||||
@@ -130,7 +135,7 @@ Flags:
|
||||
Usage:
|
||||
dws doc delete [flags]
|
||||
Example:
|
||||
dws doc delete --node <DOC_ID> --format json # 查询 nodeId: dws doc search --query "..." 或 dws doc list
|
||||
dws doc delete --node <DOC_ID> --format json # 查询 nodeId: dws drive search --query "..." 或 dws drive list
|
||||
Flags:
|
||||
--node string 文档/文件 ID 或 URL (必填)
|
||||
```
|
||||
|
||||
@@ -1,58 +1,24 @@
|
||||
# doc list(遍历文件列表)
|
||||
# doc list(已弃用 → drive list / wiki node list)
|
||||
|
||||
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
|
||||
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
|
||||
>
|
||||
> **同任务常配合**:[`doc-search.md`](./doc-search.md)(关键字检索更精准)/ [`doc-info.md`](./doc-info.md)(拿到 nodeId 后查元信息)
|
||||
> **弃用提示**:`dws doc list` 已迁移,不再是 `doc` 子命令。它虽仍能跑,但每次都会打印弃用警告:
|
||||
> `'dws doc list' is deprecated, use 'dws drive list --workspace <workspaceId>' or 'dws wiki node list --workspace <workspaceId>'`。
|
||||
> 遍历文件夹/知识库请改用下面的命令,命令详情见 [`../drive.md`](../drive.md)。
|
||||
|
||||
## 命令格式
|
||||
## 改用什么
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws doc list [flags]
|
||||
Example:
|
||||
dws doc list
|
||||
dws doc list --folder <DOC_FOLDER_NODE_ID>
|
||||
dws doc list --workspace <WS_ID> --page-size 20
|
||||
Flags:
|
||||
--folder string 文档文件夹 nodeId 或 alidocs 文件夹 URL;不要传 drive dentryId/parent-id 这类纯数字 ID
|
||||
--workspace string 知识库 ID
|
||||
--page-size int 每页数量
|
||||
--page-token string 分页 token (从上次结果的 nextPageToken 获取)
|
||||
```
|
||||
| 旧命令(弃用) | 改用 | 场景 |
|
||||
|--------------|------|------|
|
||||
| `dws doc list` | `dws drive list` | 遍历「我的文档」/钉盘根目录 |
|
||||
| `dws doc list --folder <id>` | `dws drive list --folder <id>` | 遍历指定文件夹 |
|
||||
| `dws doc list --workspace <id>` | `dws drive list --workspace <id>` 或 `dws wiki node list --workspace <id>` | 遍历知识库 |
|
||||
|
||||
## 关键说明
|
||||
|
||||
- 不传任何 flag 时遍历"我的文档"根目录。
|
||||
- `--folder` 仅接受文档文件夹 `nodeId` / `dentryUuid` / alidocs 文件夹 URL;**禁止**传入 drive `dentryId`、`parentId`、`spaceId` 这类纯数字 ID。
|
||||
- 需翻页时使用 `nextPageToken` → `--page-token`。
|
||||
`drive list` 的完整参数(`--folder` / `--workspace` / `--limit` / `--cursor` / `--order-by` 等)见 [`../drive.md` §获取文件/文件夹列表](../drive.md)。
|
||||
|
||||
## 上下文传递
|
||||
|
||||
| 从返回中提取 | 用于 |
|
||||
|-------------|------|
|
||||
| `nodes[].nodeId` | [`doc-read.md`](./doc-read.md) / [`doc-info.md`](./doc-info.md) / [`doc-update.md`](./doc-update.md) / [`doc-file-ops.md`](./doc-file-ops.md) 等所有 `--node` 入参 |
|
||||
| folder 类型的 `nodeId` | 当前 `list --folder` 递归遍历;[`doc-create.md`](./doc-create.md) / [`doc-file-ops.md`](./doc-file-ops.md) 的 `--folder` |
|
||||
|
||||
## 常用模板
|
||||
|
||||
```bash
|
||||
# 浏览"我的文档"根目录
|
||||
dws doc list --format json
|
||||
|
||||
# 浏览指定文档文件夹(folder nodeId 或 alidocs 文件夹 URL)
|
||||
dws doc list --folder <DOC_FOLDER_NODE_ID> --format json
|
||||
dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>" --format json
|
||||
|
||||
# 浏览指定知识库根目录(取出 workspaceId 后)
|
||||
dws doc list --workspace <WS_ID> --page-size 20 --format json
|
||||
|
||||
# 翻页
|
||||
dws doc list --folder <DOC_FOLDER_NODE_ID> --page-token <nextPageToken> --format json
|
||||
```
|
||||
从返回里取 `nodes[].nodeId`(folder 类型的 `nodeId` 可继续作 `--folder`),传给 `doc read` / `doc info` / `doc update` 等内容级命令的 `--node`。
|
||||
|
||||
## 参考
|
||||
|
||||
- [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令)
|
||||
- [`./doc-search.md`](./doc-search.md)(关键字检索路径)
|
||||
- [`../drive.md`](../drive.md)(列表 / 搜索 / 上传下载等文件管理命令的归属)
|
||||
- [`./doc-info.md`](./doc-info.md)(拿到 nodeId 后查元信息)
|
||||
|
||||
@@ -1,80 +1,24 @@
|
||||
# doc search(搜索文档)
|
||||
# doc search(已弃用 → drive search / wiki node search)
|
||||
|
||||
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
|
||||
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
|
||||
>
|
||||
> **同任务常配合**:[`doc-list.md`](./doc-list.md)(目录遍历,互补于关键字搜索)/ [`doc-info.md`](./doc-info.md)(拿到 nodeId 后查元信息)/ [`doc-read.md`](./doc-read.md)(拿到 nodeId 后读取正文)
|
||||
> **弃用提示**:`dws doc search` 已迁移,不再是 `doc` 子命令。它虽仍能跑,但每次都会打印弃用警告:
|
||||
> `'dws doc search' is deprecated, use 'dws drive search' or 'dws wiki node search --workspace <id>'`。
|
||||
> 搜索文档/文件请改用下面的命令,命令详情见 [`../drive.md`](../drive.md)。
|
||||
|
||||
## 命令格式
|
||||
## 改用什么
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws doc search [flags]
|
||||
Example:
|
||||
dws doc search --query "会议纪要"
|
||||
dws doc search
|
||||
dws doc search --extensions pdf,docx
|
||||
dws doc search --query "方案" --created-from 1700000000000 --created-to 1710000000000
|
||||
dws doc search --creator-uids uid1,uid2
|
||||
dws doc search --workspace-ids wsId1,wsId2
|
||||
Flags:
|
||||
--query string 搜索关键词 (不传则返回最近访问)
|
||||
--extensions strings 按文件扩展名过滤,不含点号,逗号分隔 (如 pdf,docx,png)。
|
||||
钉钉在线文档: adoc(文字) axls(表格) appt(演示文稿) awbd(白板) adraw(画板) amind(脑图) able(多维表格) aform(收集表)
|
||||
常见附件: pdf docx doc xlsx xls pptx ppt csv txt md json xml zip rar png jpg jpeg gif mp4 mp3
|
||||
以上仅为参考,extensions 为开放参数,服务端支持的扩展名不限于此。不确定文件后缀时,建议不传 --extensions 让搜索返回所有类型,再从结果中按文件名后缀筛选
|
||||
--created-from int 创建时间起始 (毫秒时间戳,含)
|
||||
--created-to int 创建时间截止 (毫秒时间戳,含)
|
||||
--visited-from int 访问时间起始 (毫秒时间戳,含)
|
||||
--visited-to int 访问时间截止 (毫秒时间戳,含)
|
||||
--creator-uids strings 按创建者用户 ID 过滤,逗号分隔
|
||||
--editor-uids strings 按编辑者用户 ID 过滤,逗号分隔
|
||||
--mentioned-uids strings 按 @提及的用户 ID 过滤,逗号分隔
|
||||
--workspace-ids strings 按知识库 ID 过滤,支持知识库 URL,逗号分隔
|
||||
--page-size int 每页数量
|
||||
--page-token string 分页 token (从上次结果的 nextPageToken 获取)
|
||||
```
|
||||
| 旧命令(弃用) | 改用 | 场景 |
|
||||
|--------------|------|------|
|
||||
| `dws doc search --query "<关键词>"` | `dws drive search --query "<关键词>"` | 全局搜文档/文件(跨钉盘+文档空间聚合) |
|
||||
| `dws doc search --extensions pdf,docx` | `dws drive search --query "..." --extensions pdf,docx` | 按扩展名过滤 |
|
||||
| `dws doc search --workspace-ids <id>` | `dws wiki node search --workspace <id> --query "<关键词>"` | 指定知识库内搜索 |
|
||||
|
||||
## 关键说明
|
||||
|
||||
- 不传 `--query` 时返回最近访问列表,适合"最近文档"类意图。
|
||||
- `--extensions` 是开放参数,传入服务端不识别的扩展名时不会报错(可能也搜不到);优先通过文件名后缀人工筛选。
|
||||
- 多个时间戳为毫秒时间戳,注意单位(不是秒)。
|
||||
`drive search` 的完整参数(`--query` / `--extensions` / `--created-from` / `--creator-uids` / `--limit` / `--cursor` 等)见 [`../drive.md` §搜索钉盘文件/文件夹/空间](../drive.md)。
|
||||
|
||||
## 上下文传递
|
||||
|
||||
| 从返回中提取 | 用于 |
|
||||
|-------------|------|
|
||||
| 文档 `nodeId` / URL | [`doc-read.md`](./doc-read.md) / [`doc-info.md`](./doc-info.md) / [`doc-update.md`](./doc-update.md) / [`doc-file-ops.md`](./doc-file-ops.md) 的 `--node` |
|
||||
| `createTime` / `creatorUid` | 创建时间与创建者过滤的二次检索 |
|
||||
|
||||
## 常用模板
|
||||
|
||||
```bash
|
||||
# 关键字搜索(最常用)
|
||||
dws doc search --query "项目周报" --format json
|
||||
|
||||
# 仅最近访问(不传 --query)
|
||||
dws doc search --format json
|
||||
|
||||
# 按扩展名过滤(在线文档族 + 常见办公附件)
|
||||
dws doc search --extensions adoc,axls,able,docx,xlsx,pdf
|
||||
|
||||
# 按创建时间窗口(毫秒时间戳)
|
||||
dws doc search --query "方案" --created-from 1700000000000 --created-to 1710000000000
|
||||
|
||||
# 按创建者过滤(多个 uid 逗号分隔)
|
||||
dws doc search --creator-uids uid1,uid2
|
||||
|
||||
# 按知识库范围过滤(支持知识库 URL)
|
||||
dws doc search --workspace-ids wsId1,wsId2
|
||||
|
||||
# 翻页
|
||||
dws doc search --query "周报" --page-size 30 --page-token <nextPageToken>
|
||||
```
|
||||
拿到返回里的文档 `nodeId` / URL 后,`doc read` / `doc info` / `doc update` / `doc block` 等内容级命令照常用。
|
||||
|
||||
## 参考
|
||||
|
||||
- [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令)
|
||||
- [`./doc-list.md`](./doc-list.md)(目录遍历替代路径)
|
||||
- [`../drive.md`](../drive.md)(搜索 / 列表 / 上传下载等文件管理命令的归属)
|
||||
- [`./doc-info.md`](./doc-info.md)(URL → nodeId 提取)
|
||||
|
||||
@@ -389,6 +389,8 @@ Flags:
|
||||
--name string 新名称 (仅 rename 必填)
|
||||
```
|
||||
|
||||
> **rename 只传主名,不要带扩展名**:服务端会按文件原扩展名自动补一个后缀。若 `--name` 里已带扩展名(如 `报告.txt`),回读会变成双扩展名 `报告.txt.txt`。正确做法:`dws drive rename --node <ID> --name "报告"`(不含 `.txt`),系统自动补回 `报告.txt`。
|
||||
|
||||
权限要求:copy 需对源文档有"阅读"权限且对目标文件夹有"编辑"权限;move 需对源文档有"管理"权限且对目标文件夹有"编辑"权限;rename 需对文档有"编辑"权限。
|
||||
|
||||
> **字段选择**:`drive list` 返回中有 `dentryId`(数字格式)和 `fileId`(UUID 格式),**必须使用 `fileId`(UUID 格式)**作为 `--node` 和 `--folder` 参数值。
|
||||
@@ -512,7 +514,7 @@ dws drive copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --f
|
||||
- 不传 `--space-id` 时默认使用「我的文件」空间
|
||||
- 不传 `--folder` 时默认操作空间根目录
|
||||
- `--folder` 只能使用父文件夹的 `dentryUuid`。不要把 `drive info` 返回的数字型 `dentryId` 当作父目录
|
||||
- **`--limit` 最大值为 50**,禁止传入超过 50 的值(如 `--limit 100`)。用户要求超过 50 条时,应使用 `--limit 50` 配合 `--cursor` 分页查询
|
||||
- **`--limit` 有效上限为 50**:CLI 不做本地校验,传 `--limit 100` 不会报错,但服务端每页最多返回 50 条,超出部分无效。用户要求超过 50 条时,应使用 `--limit 50` 配合 `--cursor` 分页查询
|
||||
- `--order-by` 支持: `createTime`、`modifyTime`、`name`
|
||||
- **上传文件首选 `dws drive upload` 命令**;手动三步(`upload-info` → HTTP PUT → `commit`)仅用于自定义流式上传等特殊场景。手动三步时 HTTP PUT 必须把 upload-info 返回的 `headers` 全部回传,`Content-Type` 通常要留空;PUT 返回 200 后才能调 `commit`;`uploadId` 有过期时间,过期需重新 `upload-info`;`--folder` 在 upload-info / commit 中要保持一致
|
||||
- `--file-name` 必须包含扩展名(如 `report.pdf`)
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
**默认选择策略:**
|
||||
|
||||
1. 调用 `dws mail mailbox list --format json` 获取当前用户的所有邮箱。
|
||||
2. 从返回的 `mailboxes` 中**优先选择企业邮箱**(账号类型为企业邮箱、域名非 `@dingtalk.com` 的邮箱),将其作为 `--email` / `--from` 的默认值。
|
||||
2. 从返回的 `emailAccounts` 中**优先选择企业邮箱**(账号类型为企业邮箱、域名非 `@dingtalk.com` 的邮箱),将其作为 `--email` / `--from` 的默认值。
|
||||
3. 仅当用户在指令中**明确指定**「用我的个人邮箱」「用 dingtalk.com 邮箱」「用我的私人邮箱」等表述时,才选择个人邮箱(`@dingtalk.com` 域名)。
|
||||
4. 若用户同时拥有多个企业邮箱(如分属多家公司),优先选择与当前会话上下文匹配的企业邮箱;若仍无法判断,向用户确认后再操作。
|
||||
5. 若用户**仅拥有个人邮箱**(无企业邮箱),可直接使用个人邮箱,但需注意 `mail user search` 等仅企业邮箱可用的命令会因权限报错,需走「查找他人邮箱地址」章节的替代路径。
|
||||
@@ -91,7 +91,7 @@ Example:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `mailboxes` | `List[]` | 邮箱列表,每条包含邮箱地址、账号类型、所属企业 |
|
||||
| `emailAccounts` | `List[]` | 邮箱列表,每条包含 `email`(邮箱地址)、`orgName`(所属企业,个人邮箱为 null)、`type`(`ORG`=企业邮箱 / `PERSONAL`=个人邮箱) |
|
||||
|
||||
### 查找他人邮箱地址(通讯录查人)
|
||||
|
||||
@@ -150,13 +150,13 @@ Flags:
|
||||
Usage:
|
||||
dws mail message search [flags]
|
||||
Example:
|
||||
dws mail message search --email user@company.com --query "subject:\"周报\"" --size 20
|
||||
dws mail message search --email user@company.com --query "from:alice AND date>2025-06-01T00:00:00Z" --size 10
|
||||
dws mail message search --email user@company.com --query "subject:\"周报\"" --limit 20
|
||||
dws mail message search --email user@company.com --query "from:alice AND date>2025-06-01T00:00:00Z" --limit 10
|
||||
Flags:
|
||||
--cursor string 邮件的起始偏移标识, 其值取自响应中的nextCursor字段。""表示从头开始
|
||||
--email string 搜索目标邮箱地址 (必填)
|
||||
--query string KQL 查询表达式 (必填), 其中 date 格式需遵循 ISO8601 规范
|
||||
--size string 每页返回数量(最大限制 100, 默认 20),别名: --limit, --page-size
|
||||
--limit string 每页返回数量(最大限制 100, 默认 20)
|
||||
```
|
||||
|
||||
KQL 查询字段: date, size, tag, folderId, isRead, hasAttachments, subject, attachname, body, from, to
|
||||
@@ -194,9 +194,9 @@ KQL 查询字段: date, size, tag, folderId, isRead, hasAttachments, subject, at
|
||||
**翻页示例:**
|
||||
```bash
|
||||
# 第一页
|
||||
dws mail message search --email user@company.com --query "folderId:2" --size 20 --format json
|
||||
dws mail message search --email user@company.com --query "folderId:2" --limit 20 --format json
|
||||
# 取返回中的 nextCursor,传入下一次请求(nextCursor="$" 时停止)
|
||||
dws mail message search --email user@company.com --query "folderId:2" --size 20 --cursor <nextCursor> --format json
|
||||
dws mail message search --email user@company.com --query "folderId:2" --limit 20 --cursor <nextCursor> --format json
|
||||
```
|
||||
|
||||
### 查看邮件完整内容
|
||||
@@ -214,7 +214,9 @@ Flags:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `message` | `object` | 邮件完整信息,包含主题、发件人、收件人、正文、附件等 |
|
||||
| `message` | `object` | 邮件信息,含 subject/from/toRecipients/ccRecipients/markdownBody/conversationId/receivedDateTime/size 等 |
|
||||
|
||||
> **注意:** 即使邮件确有附件,`message get` 返回的 `message` 对象也**不含** `attachments`/`isRead`/`tags` 字段(实测 keys 仅上表所列这些)。要取附件请另用 `dws mail attachment list`。
|
||||
|
||||
### 发送邮件
|
||||
```
|
||||
@@ -222,17 +224,17 @@ Usage:
|
||||
dws mail message send [flags]
|
||||
Example:
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "周报" --body "本周完成任务A和任务B"
|
||||
--subject "周报" --content "本周完成任务A和任务B"
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "周报" --body "见附件" --attachment ./report.pdf
|
||||
--subject "周报" --content "见附件" --attachment ./report.pdf
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "周报" --body "见附件" --attachment ./a.pdf --attachment ./b.xlsx
|
||||
--subject "周报" --content "见附件" --attachment ./a.pdf --attachment ./b.xlsx
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "图表周报" --body "图表如下:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
--subject "图表周报" --content "图表如下:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "带图文档" --body "见附件,图表:[inline:img.png]" --attachment ./doc.pdf --inline-attachment ./img.png
|
||||
--subject "带图文档" --content "见附件,图表:[inline:img.png]" --attachment ./doc.pdf --inline-attachment ./img.png
|
||||
Flags:
|
||||
--body string 邮件正文 (必填)
|
||||
--content string 邮件正文 (必填)
|
||||
--cc string 抄送人列表
|
||||
--from string 发件人邮箱 (必填),别名: --sender
|
||||
--subject string 邮件标题 (必填)
|
||||
@@ -256,7 +258,7 @@ Flags:
|
||||
|
||||
- 仅支持图片类型:`jpg` / `jpeg` / `png` / `gif` / `webp` / `bmp` / `svg`
|
||||
- CLI 自动生成 contentId,格式:`inline-{文件名(不含扩展名)}-{序号}@alimail.com`,例:`inline-chart-1@alimail.com`
|
||||
- 在 `--body` 中使用占位符 `[inline:文件名]` 引用图片,CLI 自动替换为 `<img src="cid:...">` 标签
|
||||
- 在 `--content` 中使用占位符 `[inline:文件名]` 引用图片,CLI 自动替换为 `<img src="cid:...">` 标签
|
||||
- 若 body 中没有对应占位符,内联图片会自动追加到正文末尾
|
||||
- 非图片类型(PDF、视频、音频等)请改用 `--attachment`
|
||||
|
||||
@@ -266,13 +268,13 @@ Usage:
|
||||
dws mail folder list [flags]
|
||||
Example:
|
||||
dws mail folder list --email user@company.com
|
||||
dws mail folder list --email user@company.com --folder-id <folderId>
|
||||
dws mail folder list --email user@company.com --folder <folderId>
|
||||
Flags:
|
||||
--email string 邮件所属邮箱地址 (必填)
|
||||
--folder-id string 父文件夹唯一标识,不传则返回顶层文件夹 (可选)
|
||||
--email string 邮件所属邮箱地址 (必填)
|
||||
--folder string 父文件夹唯一标识,不传则返回顶层文件夹 (可选)
|
||||
```
|
||||
|
||||
不传 `--folder-id` 返回顶层文件夹列表;传入则返回该文件夹的子文件夹列表。
|
||||
不传 `--folder` 返回顶层文件夹列表;传入则返回该文件夹的子文件夹列表。
|
||||
|
||||
**返回字段(`folders` 数组):**
|
||||
|
||||
@@ -351,8 +353,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -374,8 +375,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -401,7 +401,7 @@ Flags:
|
||||
|------|------|------|
|
||||
| `id` | `string` | 附件唯一标识 |
|
||||
| `name` | `string` | 附件文件名 |
|
||||
| `contentType` | `string` | 附件 MIME 类型 |
|
||||
| `isInline` | `bool` | 是否为内联附件(`true`=正文内联图片,`false`=普通附件) |
|
||||
| `size` | `int` | 附件大小(字节) |
|
||||
|
||||
### 下载邮件附件
|
||||
@@ -521,8 +521,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -546,8 +545,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -627,8 +625,10 @@ Flags:
|
||||
| `senders` | `List[{email, name}]` | 会话发件人列表 |
|
||||
| `isRead` | `boolean` | 会话是否已读(全部已读/未读) |
|
||||
| `priority` | `string` | 会话重要性,取会话内邮件最高优先级(`PRY_HIGH` / `PRY_NORMAL`) |
|
||||
| `flag` | `string` | 会话标识,取会话内最近邮件的标识(`FLAG_NONE` / `FLAG_REPLY` / `FLAG_FORWARD`) |
|
||||
| `hasAttachments` | `boolean` | 会话是否包含附件(不含 inline 资源) |
|
||||
| `messages` | `List[]` | 会话内的邮件列表;默认即返回,但各邮件字段(正文、收件人等)多为 null,需在 `--select` 中额外指定才有值 |
|
||||
|
||||
> **注意:** `thread get` 返回的 `conversation` **不含 `flag` 字段**(`flag` 仅出现在 `thread list` 的会话项中)。会话标识请从 `thread list` 获取。
|
||||
|
||||
### 修改邮件会话状态
|
||||
|
||||
@@ -660,8 +660,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -686,8 +685,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -749,13 +747,13 @@ Usage:
|
||||
dws mail message reply [flags]
|
||||
Example:
|
||||
dws mail message reply --from user@company.com --id <messageId>
|
||||
dws mail message reply --from user@company.com --id <messageId> --subject "Re: 周报" --body "已收到,谢谢!"
|
||||
dws mail message reply --from user@company.com --id <messageId> --subject "Re: 周报" --content "已收到,谢谢!"
|
||||
Flags:
|
||||
--from string 发件人邮箱 (必填),别名: --sender
|
||||
--to string 收件人列表(可选)
|
||||
--id string 要回复的邮件 ID (必填)
|
||||
--subject string 回复邮件标题(可选)
|
||||
--body string 回复正文(可选)
|
||||
--content string 回复正文(可选)
|
||||
--attachment stringArray 附件文件路径,可多次指定 (可选)
|
||||
--inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选)
|
||||
```
|
||||
@@ -781,13 +779,13 @@ Usage:
|
||||
dws mail message reply-all [flags]
|
||||
Example:
|
||||
dws mail message reply-all --from user@company.com --id <messageId>
|
||||
dws mail message reply-all --from user@company.com --id <messageId> --subject "Re: 周报" --body "感谢大家的参与!"
|
||||
dws mail message reply-all --from user@company.com --id <messageId> --subject "Re: 周报" --content "感谢大家的参与!"
|
||||
Flags:
|
||||
--from string 发件人邮箱 (必填),别名: --sender
|
||||
--to string 收件人列表(可选,包含发件人及所有原始收件人)
|
||||
--id string 要回复的邮件 ID (必填)
|
||||
--subject string 回复邮件标题(可选)
|
||||
--body string 回复正文(可选)
|
||||
--content string 回复正文(可选)
|
||||
--attachment stringArray 附件文件路径,可多次指定 (可选)
|
||||
--inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选)
|
||||
```
|
||||
@@ -819,7 +817,7 @@ Flags:
|
||||
--to string 转发收件人列表(可选)
|
||||
--id string 要转发的邮件 ID (必填)
|
||||
--subject string 转发邮件标题(可选)
|
||||
--body string 转发附言(可选)
|
||||
--content string 转发附言(可选)
|
||||
--attachment stringArray 附件文件路径,可多次指定 (可选)
|
||||
--inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选)
|
||||
```
|
||||
@@ -922,7 +920,9 @@ Flags:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `message` | `object` | 邮件完整信息 |
|
||||
| `sendStatus` | `string` | 发送状态,取值见下表 |
|
||||
| `message.sendStatus` | `string` | 发送状态,**嵌在 `message` 对象内部**(非顶层字段),取值见下表 |
|
||||
|
||||
> **注意:** 顶层只有 `message` 和 `success` 两个字段,`sendStatus` 位于 `message.sendStatus`,不是与 `message` 平级的顶层字段。
|
||||
|
||||
**`sendStatus` 取值说明:**
|
||||
|
||||
@@ -941,57 +941,57 @@ Usage:
|
||||
dws mail draft create [flags]
|
||||
Example:
|
||||
dws mail draft create --from user@company.com --to colleague@company.com \
|
||||
--subject "草稿标题" --body "草稿正文"
|
||||
--subject "草稿标题" --content "草稿正文"
|
||||
dws mail draft create --from user@company.com --subject "草稿标题"
|
||||
dws mail draft create --from user@company.com --subject "带附件草稿" \
|
||||
--body "见附件" --attachment ./report.pdf
|
||||
--content "见附件" --attachment ./report.pdf
|
||||
dws mail draft create --from user@company.com --subject "带图片草稿" \
|
||||
--body "图表:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
--content "图表:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
Flags:
|
||||
--from string 发件人邮箱 (必填),别名: --sender
|
||||
--subject string 邮件标题 (必填)
|
||||
--to string 收件人列表(可选,有确定收件人时才传)
|
||||
--cc string 抄送人列表(可选,有确定抄送人时才传)
|
||||
--body string 邮件正文(可选,有正文内容时才传)
|
||||
--content string 邮件正文(可选,有正文内容时才传)
|
||||
--attachment stringArray 附件文件路径,可多次指定 (可选)
|
||||
--inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选)
|
||||
```
|
||||
|
||||
> **注意:** `--to`、`--cc`、`--body` 均为可选参数,**仅在用户明确提供对应信息时才传入**。若用户未指定收件人,不要传 `--to ""`(空字符串)。
|
||||
> **注意:** `--to`、`--cc`、`--content` 均为可选参数,**仅在用户明确提供对应信息时才传入**。若用户未指定收件人,不要传 `--to ""`(空字符串)。
|
||||
|
||||
**附件说明:**
|
||||
|
||||
指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿创建和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--body` 中使用 `[inline:文件名]` 占位符)。
|
||||
指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿创建和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--content` 中使用 `[inline:文件名]` 占位符)。
|
||||
|
||||
**返回字段:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `messageId` | `string` | 新建草稿的邮件 ID |
|
||||
| `result.message.id` | `string` | 新建草稿的邮件 ID(**嵌在 `result.message` 内**,无顶层 `messageId` 字段);`result.message` 还含 `internetMessageId`、`folderId`(草稿箱为 `5`)、`subject`、`from` 等 |
|
||||
|
||||
### 更新草稿
|
||||
```
|
||||
Usage:
|
||||
dws mail draft update [flags]
|
||||
Example:
|
||||
dws mail draft update --from user@company.com --id <messageId> --subject "新标题" --body "新正文"
|
||||
dws mail draft update --from user@company.com --id <messageId> --body "见附件" --attachment ./report.pdf
|
||||
dws mail draft update --from user@company.com --id <messageId> --subject "新标题" --content "新正文"
|
||||
dws mail draft update --from user@company.com --id <messageId> --content "见附件" --attachment ./report.pdf
|
||||
dws mail draft update --from user@company.com --id <messageId> \
|
||||
--body "图表:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
--content "图表:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
Flags:
|
||||
--from string 发件人邮箱 (必填),别名: --sender
|
||||
--id string 草稿邮件 ID (必填)
|
||||
--to string 收件人列表(可选)
|
||||
--cc string 抄送人列表(可选)
|
||||
--subject string 邮件标题(可选)
|
||||
--body string 邮件正文(可选)
|
||||
--content string 邮件正文(可选)
|
||||
--attachment stringArray 附件文件路径,可多次指定 (可选)
|
||||
--inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选)
|
||||
```
|
||||
|
||||
**附件说明:**
|
||||
|
||||
指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿更新和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--body` 中使用 `[inline:文件名]` 占位符)。
|
||||
指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿更新和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--content` 中使用 `[inline:文件名]` 占位符)。
|
||||
|
||||
### 发送草稿
|
||||
```
|
||||
@@ -1013,13 +1013,14 @@ Usage:
|
||||
Example:
|
||||
dws mail user search --keyword "张三"
|
||||
dws mail user search --email user@company.com --keyword "张三"
|
||||
dws mail user search --email user@company.com --keyword "alice" --size 10
|
||||
dws mail user search --email user@company.com --keyword "alice" --limit 10
|
||||
dws mail user search --email user@company.com --keyword "alice" --cursor <nextCursor>
|
||||
Flags:
|
||||
--email string 搜索目标邮箱地址 (可选)
|
||||
--keyword string 搜索关键词 (必填)
|
||||
--cursor string 分页游标,取自响应中的 nextCursor 字段(可选)
|
||||
--size string 每页返回数量(可选)
|
||||
--email string 搜索目标邮箱地址 (可选)
|
||||
--keyword string 搜索关键词(未提供 --employee-no 时为必填)
|
||||
--employee-no string 按工号搜索用户;提供此参数时 keyword 不再必填 (可选)
|
||||
--cursor string 分页游标,取自响应中的 nextCursor 字段(可选)
|
||||
--limit string 每页返回数量(可选)
|
||||
```
|
||||
|
||||
> **重要区别:**
|
||||
@@ -1069,6 +1070,10 @@ Flags:
|
||||
```
|
||||
|
||||
> **草稿模板说明:** 传入 `--is-draft` 创建的模板为草稿模板,草稿模板支持后续通过 `template update` 修改内容。**非草稿模板创建后不可修改**(`template update` 仅对草稿模板有效)。
|
||||
>
|
||||
> **草稿模板不出现在 `template list`(重要):** 实测 `template list` **只返回非草稿模板**;草稿模板(`--is-draft`)不在列表里,只能用 `template get --id <模板ID>` 直接获取。因此**不要用 `template list` 是否出现来判断草稿模板是否创建成功**——`template create` 返回 `success:true` 即已创建。
|
||||
>
|
||||
> **模板 ID 前缀区分类型:** `template create` 返回的 `id` 前缀标识类型——非草稿模板为 `1:0:` 开头(如 `1:0:8bbeea56-...`),草稿模板为 `0:0:` 开头(如 `0:0:e3e2d134-...`)。
|
||||
|
||||
### 列举邮件模板
|
||||
```
|
||||
@@ -1241,7 +1246,7 @@ Flags:
|
||||
| `rules[].enabled` | bool | 是否启用 |
|
||||
| `rules[].conditions` | List[] | 规则条件列表 |
|
||||
| `rules[].actions` | List[] | 规则动作列表 |
|
||||
| `rules[].order` | int | 规则排序 |
|
||||
| `rules[].order` | null | 排序字段;实测存量与新建规则该字段**恒为 `null`**,不返回有效排序值,不要依赖它判断规则顺序 |
|
||||
|
||||
#### 创建收信规则
|
||||
|
||||
@@ -1361,22 +1366,24 @@ Flags:
|
||||
--actions string 规则动作 JSON 数组 (必填)
|
||||
```
|
||||
|
||||
> **⚠️ 实测:** 尽管 `--help` 把 `--conditions` 标为(可选),服务端实际要求它**非空**——省略或传空会返回 `209 SYS_UNKNOWN_ERROR(1001)` 失败,帮助文本承诺的"命中所有邮件"无条件匹配**不可用**。create 请始终传入有效条件。
|
||||
|
||||
**返回字段:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 创建是否成功 |
|
||||
| `errorCode` | string | 错误码 |
|
||||
| `errorMsg` | string | 错误消息 |
|
||||
| `id` | string | 新建规则 ID |
|
||||
|
||||
> 实测 `rule create` 成功仅返回 `{id, success}`,**不返回** `errorCode` / `errorMsg`。
|
||||
|
||||
#### 更新收信规则
|
||||
|
||||
更新已有的收信规则。**除 `--conditions` 外所有参数均为必填**。
|
||||
更新已有的收信规则。**所有参数(含 `--conditions`)均须传入有效值**——见下方警告。
|
||||
|
||||
> **建议工作流:** 先通过 `dws mail rule list` 获取当前规则的完整配置,再传入需要修改的字段值。
|
||||
>
|
||||
> `--conditions` 为空或不传表示命中所有邮件(无条件匹配)。`--actions` 格式同 create 命令。
|
||||
> **⚠️ 实测:** `--help` 称 `--conditions` 为空即命中所有邮件,但该无条件匹配**不可用**——省略或传空会返回 `209 SYS_UNKNOWN_ERROR(1001)` 失败。update 请始终传入有效条件。`--actions` 格式同 create 命令。
|
||||
|
||||
```
|
||||
Usage:
|
||||
@@ -1396,14 +1403,16 @@ Flags:
|
||||
--actions string 规则动作 JSON 数组 (必填)
|
||||
```
|
||||
|
||||
> **⚠️ 实测:** 上方 `--conditions` 的"(可选,为空表示命中所有邮件)"是 `--help` 原文,但服务端实际要求它**非空**——省略或传空会返回 `209 SYS_UNKNOWN_ERROR(1001)` 失败。update 须传入有效条件。
|
||||
|
||||
**返回字段:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 更新是否成功 |
|
||||
| `errorCode` | string | 错误码 |
|
||||
| `errorMsg` | string | 错误信息 |
|
||||
| `result` | object | 更新结果 |
|
||||
| `result` | object | 更新结果(实测为空对象 `{}`) |
|
||||
|
||||
> 实测 `rule update` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。
|
||||
|
||||
#### 删除收信规则
|
||||
|
||||
@@ -1424,9 +1433,9 @@ Flags:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 删除是否成功 |
|
||||
| `errorCode` | string | 错误码 |
|
||||
| `errorMsg` | string | 错误信息 |
|
||||
| `result` | object | 删除结果 |
|
||||
| `result` | object | 删除结果(实测为空对象 `{}`) |
|
||||
|
||||
> 实测 `rule delete` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。
|
||||
|
||||
#### 调整收信规则排序
|
||||
|
||||
@@ -1449,9 +1458,9 @@ Flags:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 调整是否成功 |
|
||||
| `errorCode` | string | 错误码 |
|
||||
| `errorMsg` | string | 错误消息 |
|
||||
| `result` | object | 调整结果 |
|
||||
| `result` | object | 调整结果(实测为空对象 `{}`) |
|
||||
|
||||
> 实测 `rule adjust` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。
|
||||
|
||||
## 通用错误说明
|
||||
|
||||
@@ -1559,29 +1568,29 @@ dws mail thread batch-trash --email user@company.com --ids <conversationId1>,<co
|
||||
|
||||
# 2. 搜索邮件 — 提取 messageId
|
||||
dws mail message search --email user@company.com \
|
||||
--query "subject:\"周报\" AND date>2025-06-01T00:00:00Z" --size 10 --format json
|
||||
--query "subject:\"周报\" AND date>2025-06-01T00:00:00Z" --limit 10 --format json
|
||||
|
||||
# 3. 查看邮件详情
|
||||
dws mail message get --email user@company.com --id <messageId> --format json
|
||||
|
||||
# 4. 发送邮件(纯文本)
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "周报" --body "本周完成…" --format json
|
||||
--subject "周报" --content "本周完成…" --format json
|
||||
|
||||
# 4b. 发送带附件的邮件(自动编排:创建草稿→上传附件→发送草稿)
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "周报" --body "见附件" --attachment ./report.pdf --format json
|
||||
--subject "周报" --content "见附件" --attachment ./report.pdf --format json
|
||||
|
||||
# 4c. 发送带内联图片的邮件(正文自动转 HTML,<img> 标签自动注入)
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "图表周报" --body "本周图表如下:[inline:chart.png]" \
|
||||
--subject "图表周报" --content "本周图表如下:[inline:chart.png]" \
|
||||
--inline-attachment ./chart.png --format json
|
||||
|
||||
# 5. 下载邮件附件到本地(每次只能下载一个附件,不支持批量下载)
|
||||
# 步骤 5.1:搜索匹配的邮件,获取 messageId 列表
|
||||
# 示例:下载4月所有发票邮件的附件
|
||||
dws mail message search --email user@company.com \
|
||||
--query "subject:发票 AND date>2025-04-01T00:00:00Z AND date<2025-05-01T00:00:00Z AND hasAttachments:true" --size 50 --format json
|
||||
--query "subject:发票 AND date>2025-04-01T00:00:00Z AND date<2025-05-01T00:00:00Z AND hasAttachments:true" --limit 50 --format json
|
||||
|
||||
# 步骤 5.2:对每封邮件,列出附件获取 attachmentId 和 name
|
||||
# (对搜索结果中的每封邮件都要执行一次)
|
||||
@@ -1594,7 +1603,7 @@ dws mail attachment download --email user@company.com \
|
||||
# 5. 获取邮件所属会话详情(thread)
|
||||
# 步骤 5.1:先通过 message search 或 message get 获取邮件中的 conversationId
|
||||
dws mail message search --email user@company.com \
|
||||
--query "subject:\"周报\"" --size 5 --format json
|
||||
--query "subject:\"周报\"" --limit 5 --format json
|
||||
# 从返回的邮件列表中提取 conversationId 字段
|
||||
|
||||
# 步骤 5.2:用 conversationId 获取会话详情
|
||||
@@ -1637,8 +1646,8 @@ dws mail thread get --email user@company.com --id <conversationId> --select mess
|
||||
3. `dws contact user search --keyword "名字"`,提取用户邮箱字段
|
||||
若三路均无有效邮箱,必须 ask_human 请用户手动提供收件人邮箱,严禁臆测和假设
|
||||
- `thread get` 无法直接通过邮箱地址查询会话列表,**必须先有 conversationId**;conversationId 来自 `message search` 或 `message get` 返回的邮件字段 `conversationId`
|
||||
- `thread get` 默认不返回邮件列表,如需查看会话内所有邮件,需加 `--select messages`;如需同时返回多个可选字段,用英文逗号分隔,如 `--select messages,internetMessageId`
|
||||
- `thread get` 返回的 `messages` 列表中,邮件正文(`body`)、收件人(`toRecipients`)等字段默认不包含,需在 `--select` 中额外指定
|
||||
- `thread get` 默认即返回会话内 `messages` 列表,但列表中每封邮件的正文(`body`)、收件人(`toRecipients`)等字段默认为 null;需在 `--select` 中额外指定才有值,如 `--select messages,internetMessageId`(多个字段用英文逗号分隔)
|
||||
- `thread get` 返回的 `conversation` 不含 `flag` 字段,会话标识请从 `thread list` 获取
|
||||
- `user search` 仅支持企业邮箱(非 `@dingtalk.com` 个人邮箱),使用个人邮箱将因无权限报错;搜到的用户邮箱(`email` 字段)可直接用于 `message send` 的 `--to`/`--cc` 参数
|
||||
- `thread list --folder` 的值必须是文件夹 ID,不是文件夹显示名称;不知道文件夹 ID 时,先调用 `folder list` 查 `folders[].id`
|
||||
- `thread get/update/trash/batch-update/batch-trash` 使用的是会话 ID(conversationId),不是邮件 ID;会话 ID 可来自 `thread list` 的 `conversations[].id`,也可来自 `message search` 或 `message get` 返回的 `conversationId`
|
||||
|
||||
@@ -7,8 +7,8 @@ minutes 模块的命令是**两级或三级结构**,不同层级之间不能
|
||||
> **默认 scope 规则**:`dws minutes list` 后**必须**跟 scope 子命令。若用户未明确指定查询范围,**一律默认补 `all`**(查询我可访问的所有听记 = 我创建的 + 他人共享给我的,覆盖面最广)。
|
||||
>
|
||||
> **`list` 不带 scope 的陷阱(0519 P2 Golden Case 提炼)**:
|
||||
> - 裸 `dws minutes list`(不跟 mine/shared/all)虽然不会报错,但返回结果**不完整**(仅返回最近少量条目,约 913 字节)
|
||||
> - `dws minutes list all` 才能返回完整列表(约 3362 字节)
|
||||
> - 裸 `dws minutes list`(不跟 mine/shared/all)**不会返回任何听记数据**——它只打印该命令的帮助/用法信息(Usage、Available Commands: all/mine/shared、Flags),等于什么都没查到
|
||||
> - 必须带 scope 子命令(`dws minutes list all` / `list mine` / `list shared`)才会真正返回听记列表
|
||||
> - **AI 严禁使用裸 `dws minutes list`**,必须始终带 scope 子命令
|
||||
> - 如果 LLM 不确定用哪个 scope,**一律用 `all`**
|
||||
>
|
||||
@@ -26,7 +26,7 @@ dws minutes
|
||||
│ ├── shared [--query] [--start] [--end] [--max] [--next-token] # 仅查询他人共享给我的听记(不含我自己创建的)
|
||||
│ └── all [--query] [--start] [--end] [--max] [--next-token] # 查询我可访问的所有听记(= mine ∪ shared,覆盖面最广,默认 scope)
|
||||
├── get <subcommand> # 获取听记详情(get 后必须跟子命令名)
|
||||
│ ├── info --id <uuid> # 获取听记基础信息(标题、时长、参与人、状态等)
|
||||
│ ├── info --id <uuid> # 获取听记基础信息(标题、时长、开始时间、taskUuid、访问链接;不含参会人)
|
||||
│ ├── summary --id <uuid> # 获取听记 AI 摘要(结构化纪要)
|
||||
│ ├── transcription --id <uuid> # 获取听记语音转写原文(逐字稿,支持分页)
|
||||
│ ├── keywords --id <uuid> # 获取听记关键词
|
||||
@@ -77,7 +77,7 @@ dws minutes
|
||||
| `dws minutes list all --end-time "..."` | 参数名是 `--end` 不是 `--end-time` | `dws minutes list all --end "2026-04-30"` |
|
||||
| `dws minutes list --page-size 10` | `--page-size` 不存在,分页用 `--max`;且 `list` 后缺 scope | `dws minutes list mine --max 10` |
|
||||
| `dws minutes list --date-range 2026-05-04 2026-05-10` | `--date-range` 不存在,时间范围用 `--start` + `--end` 两个参数;且 `list` 后缺 scope | `dws minutes list mine --start "2026-05-04T00:00:00+08:00" --end "2026-05-10T23:59:59+08:00"` |
|
||||
| `dws minutes list mine --limit 10` | `--limit` 尚未注册(跨产品规约 Primary 为 `--limit`,但 minutes CLI 当前只接受 `--max`)。**待 alias 注册后两者等价** | `dws minutes list mine --max 10` |
|
||||
| `dws minutes list mine --limit 10` | 不报错——`--limit` 已注册为 `--max` 的合法别名,两者完全等价(`--limit 3` 与 `--max 3` 返回相同条数) | `dws minutes list mine --max 10`(或 `--limit 10`,二选一即可) |
|
||||
| `dws minutes upload create --json '{"fileName":...}'` | `--json` 不存在,cli 不接受 JSON 作为输入格式 | `dws minutes upload create --file-name "xxx.mp3" --file-size 61565431` |
|
||||
| `dws minutes upload create -f json '{"fileName":...}'` | `-f json` / `--format json` 是**输出格式**控制,不是输入参数 | `dws minutes upload create --file-name "xxx.mp3" --file-size 61565431 --format json` |
|
||||
| `dws minutes get transcription --id <uuid> \| head -c 2000` | Windows 沙箱无 `head` 命令,**严禁使用 shell 管道截断** | `dws minutes get transcription --id <uuid> --format json`(由 AI 在内存中截断处理) |
|
||||
@@ -90,7 +90,7 @@ dws minutes
|
||||
| `dws minutes transcribe --url <听记url>` | `transcribe` 不是合法子命令,`--url` 参数也不存在。LLM 凭印象编造 | 先从 URL 提取 taskUuid(见「URL → taskUuid 自动提取规则」),再 `dws minutes get transcription --id <taskUuid> --format json` |
|
||||
| `dws minutes get transcription --url <听记url>` | `--url` 参数不存在,`--id` 只接受纯 taskUuid | 同上:从 URL 提取 taskUuid 后用 `--id` |
|
||||
| `dws minutes summary --uuid <uuid>` | `summary` 不是顶层子命令(应在 `get` 下),且顶层不识别 `--uuid`。**0519 高频错误** | `dws minutes get summary --id <uuid>` |
|
||||
| `dws minutes list`(不跟 scope) | `list` 后**必须**跟 scope(mine/shared/all)。裸 `list` 返回结果不完整且行为未文档化 | `dws minutes list all`(默认 scope) |
|
||||
| `dws minutes list`(不跟 scope) | `list` 后**必须**跟 scope(mine/shared/all)。裸 `list` 只打印帮助信息,不返回任何听记数据 | `dws minutes list all`(默认 scope) |
|
||||
| `dws report inbox --format json`(无时间窗口) | `inbox` 是兼容入口,等价于 `inbox list`;仍必须带 `--start` / `--end` | `dws report inbox list --start "2026-05-06T00:00:00+08:00" --end "2026-05-13T23:59:59+08:00" --format json` |
|
||||
| `dws report inbox list --format json`(不带 --start) | inbox list **必须**带 `--start` / `--end` 参数,否则报 "flag --start is required" | `dws report inbox list --start "2026-05-06T00:00:00+08:00" --end "2026-05-13T23:59:59+08:00" --format json` |
|
||||
|
||||
@@ -102,14 +102,14 @@ dws minutes
|
||||
>
|
||||
> | 语义 | 规约 Primary | minutes 当前 CLI 实际参数 | alias 注册状态 | 说明 |
|
||||
> |------|-------------|------------------------|--------------|------|
|
||||
> | 单页大小 (Group 13) | `--limit` | `--max` | `--limit` 未注册 | minutes 用 `--max`,其他模块用 `--limit`。**跨模块切换时注意差异** |
|
||||
> | 单页大小 (Group 13) | `--limit` | `--max`(`--limit` 为合法别名) | `--limit` 已注册 | minutes 推荐 `--max`,但 `--limit` 也可用,两者等价 |
|
||||
> | 续页标识 (Group 14) | `--cursor` | `--next-token` | `--next-token` 在 alias 池中 | minutes 用 `--next-token`,合法 alias |
|
||||
> | 搜索关键词 (Group 11) | `--query` | `--query` | 已对齐 | 无差异 |
|
||||
> | 起始时间 (Group 15) | `--start` | `--start` | 已对齐 | 无差异 |
|
||||
> | 结束时间 (Group 16) | `--end` | `--end` | 已对齐 | 无差异 |
|
||||
>
|
||||
> **跨模块参数名差异速查(防止混用):**
|
||||
> - 分页大小:minutes 用 `--max`,aitable/calendar/chat/drive 用 `--limit`
|
||||
> - 分页大小:minutes 推荐 `--max`(`--limit` 也是合法别名,等价),aitable/calendar/chat/drive 用 `--limit`
|
||||
> - 续页标识:minutes 用 `--next-token`,aitable/drive/doc 用 `--cursor`
|
||||
> - 起止时间:minutes/report 用 `--start`/`--end`,calendar 用 `--start-time`/`--end-time`(跨模块最高频错误)
|
||||
|
||||
@@ -260,8 +260,8 @@ dws minutes
|
||||
> | `missing required flag(s): --session-id` | cancel 命令必须传 session-id | 从之前 upload create 的返回中提取 session-id;若丢失则暂无法取消 |
|
||||
>
|
||||
> **上传流程三步走**(AI 必须按此顺序执行,不可跳步):
|
||||
> 1. `dws minutes upload create --file-name "xxx.mp3" --file-size <字节数> --format json` → 获取 `sessionId` + `uploadUrl`
|
||||
> 2. 将文件通过 `uploadUrl` 直传(由客户端侧完成,AI 不介入)
|
||||
> 1. `dws minutes upload create --file-name "xxx.mp3" --file-size <字节数> --format json` → 获取 `sessionId` + `presignedUrl`
|
||||
> 2. 将文件通过 `presignedUrl` 直传(由客户端侧完成,AI 不介入)
|
||||
> 3. `dws minutes upload complete --session-id <sid> --format json` → 确认上传完成
|
||||
>
|
||||
> **注意**:`--file-size` 单位是**字节**(不是 KB/MB),AI 不要帮用户估算大小,应让用户确认文件实际字节数。
|
||||
@@ -383,7 +383,7 @@ Flags:
|
||||
--id string 听记 taskUuid (必填),取值逻辑参考 ## 注意事项
|
||||
```
|
||||
|
||||
返回字段: 创建人、开始时间、截止时间、听记标题、听记访问链接URL
|
||||
返回字段(result 内): `duration`(时长, 秒)、`startTime`(开始时间, 毫秒时间戳)、`endTime`(结束时间, 仅较长会议返回, 短录音无此字段)、`taskUuid`(听记 ID)、`title`(听记标题)、`url`(听记访问链接)。注意: **不返回创建人(creator)字段**。
|
||||
|
||||
**发言人列表输出规范(查询详情后必须执行)**:
|
||||
|
||||
@@ -618,7 +618,7 @@ AI 基于已拉取的转写数据,**本地完成**聚类与摘要(无需新
|
||||
> **铁律 2:严禁用 AI 摘要里的"参与人/参会人"字段判断某人是否参会**
|
||||
> - AI 摘要中的"参与人"字段往往**只截取最显著的 1-2 个名字**(通常是创建人或发言最多的人),**不是完整参会人列表**
|
||||
> - "AI 摘要参与人只有故愚 → 灵麦没参会" 是典型的错误推理链
|
||||
> - 如果一定要列出参会人,应使用 `dws minutes get info` / `get batch` 返回的 `participants` 字段,而不是摘要文本里的描述
|
||||
> - 如果一定要列出参会人,只能从 `get transcription` 的转写发言人(`speakerName`/`speakerId`)汇总——`get info` / `get batch` **均不返回** `participants` 结构化字段(get info 只返回 duration/startTime/taskUuid/title/url,get batch 只返回 creatorNick/creatorUnionId/status 等),别指望从它们拿参会名单
|
||||
>
|
||||
> **铁律 3:通讯录查询是 Step 4 的强制起点,不依赖 Step 3 是否成功**
|
||||
> - Step 2 一旦返回"未命中真名标注",**Step 3 与 Step 4 ① 必须并发触发**,不要等 Step 3 失败再补查
|
||||
@@ -667,7 +667,7 @@ Step 7: 引导用户替换发言人(调用 speaker replace 写回听记)
|
||||
> - **未命中真名标注 ≠ 目标人物没参会**——绝大多数听记的发言人都是匿名编号,"未命中"是**默认场景**,恰恰是发言人识别功能要解决的问题
|
||||
> - **不要在转写文本里 grep 目标人名**作为存在性判断——花名/真名通常不会出现在 TA 自己的发言里(参见铁律 1)
|
||||
> - **不要把 `dws minutes get summary` 摘要文本里写到的"参与人"当作完整参会列表**——AI 摘要里的"参与人"字段只截取最显著的 1-2 个名字,不是完整名册(参见铁律 2)
|
||||
> - 唯一能下"目标人物没参会"结论的场景是:`get info` / `get batch` 返回的结构化 `participants` 字段里**完整列出参会人且不含目标**,且 `dws contact user search` 也搜不到该人 → 才允许告知用户"该人不在参会列表"
|
||||
> - 听记各接口都拿不到权威的完整参会名册:`get info` / `get batch` **均不返回** `participants` 字段,转写发言人列表又常是匿名编号。因此**几乎无法**仅凭听记数据下"目标人物没参会"的结论;只有当转写发言人已全部实名标注且不含目标、且 `dws contact user search` 也搜不到该人时,才可谨慎告知"未在可见发言人中找到该人"
|
||||
|
||||
##### Step 3: 转写原文内推断(优先)
|
||||
|
||||
@@ -813,7 +813,7 @@ Flags:
|
||||
--ids string 听记 taskUuid 列表,逗号分隔 (必填)
|
||||
```
|
||||
|
||||
返回字段: 听记标题、时长、参与人列表、创建时间、taskUuid、听记状态
|
||||
返回字段: `result.minutesDetails` 数组,每项含 `title`(标题)、`durationMicros`(时长, 微秒)、`startTime`(开始时间, 毫秒)、`taskUuid`、`status`(状态)、`creatorNick`(创建人昵称)、`creatorUnionId`、`size`、`bizType`、`isDeleted`。**不返回参会人列表**。
|
||||
|
||||
### 修改听记标题
|
||||
```
|
||||
@@ -2299,7 +2299,7 @@ Step 2 **[禁止]** 看到全是匿名编号 → 没有继续走 Step 3-4,反
|
||||
|
||||
1. **"在转写里搜不到目标人名"绝不构成"没参会"的证据**:花名/真名通常不出现在 TA 自己的发言里,这是默认场景而非例外。99% 的听记发言人都是匿名编号——这正是发言人识别功能要解决的问题。
|
||||
|
||||
2. **AI 摘要的"参与人"字段是低保真信号,不能作为参会判断依据**:`get summary` 返回的是 AI 生成的自然语言摘要,里面提到的"参与人"通常只是最显著的 1-2 人;要拿完整参会列表,应使用 `get info` / `get batch` 返回的结构化 `participants` 字段。
|
||||
2. **AI 摘要的"参与人"字段是低保真信号,不能作为参会判断依据**:`get summary` 返回的是 AI 生成的自然语言摘要,里面提到的"参与人"通常只是最显著的 1-2 人。而 `get info` / `get batch` **均不返回**结构化 `participants` 字段,无法从中拿完整参会列表;能拿到的只有 `get transcription` 的转写发言人(常为匿名编号)。
|
||||
|
||||
3. **`dws contact user search` 是 Step 4 ① 的必跑项,单次调用就能突破死局**:本案例的整个失败链路只要有 1 次通讯录查询就能立即扭转——拿到"木兰=王佳明,产品设计部"后,Step 5 的角色匹配就有了锚点,再也不会得出"没参会"的错误结论。
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Example:
|
||||
Flags:
|
||||
--end string 结束时间 ISO-8601 (如 2026-03-10T23:59:59+08:00) (必填)
|
||||
--page string 分页页码 (可选)
|
||||
--size string 每页大小 (可选)
|
||||
--limit string 每页大小 (可选)
|
||||
--start string 开始时间 ISO-8601 (如 2026-03-10T00:00:00+08:00) (必填)
|
||||
--query string 关键字搜索 (可选)
|
||||
```
|
||||
@@ -99,12 +99,12 @@ Flags:
|
||||
Usage:
|
||||
dws oa approval list-forms [flags]
|
||||
Example:
|
||||
dws oa approval list-forms --cursor 0 --size 100
|
||||
dws oa approval list-forms --cursor 0 --limit 100
|
||||
Flags:
|
||||
--cursor string 分页游标,首次传 0 (默认 "0")
|
||||
--size string 每页大小,最大 100 (默认 "100")
|
||||
--limit string 每页大小,最大 100 (默认 "100")
|
||||
```
|
||||
MCP 工具: `list_user_visible_process`;参数: cursor, pageSize(对应 --cursor/--size)。返回结果含 processCode,可用于 list-initiated 的 --process-code。
|
||||
MCP 工具: `list_user_visible_process`;参数: cursor, pageSize(对应 --cursor/--limit)。返回结果含 processCode,可用于 list-initiated 的 --process-code。
|
||||
|
||||
### 按关键字模糊搜索审批表单
|
||||
```
|
||||
@@ -245,25 +245,26 @@ MCP 工具: `redirect_task`;参数: taskId, toActionerId, remark(对应 --ta
|
||||
Usage:
|
||||
dws oa approval oa-comments [flags]
|
||||
Example:
|
||||
dws oa approval oa-comments --instance-id <processInstanceId> --text "同意,请尽快处理"
|
||||
dws oa approval oa-comments --instance-id <processInstanceId> --content "同意,请尽快处理"
|
||||
Flags:
|
||||
--instance-id string 审批实例 ID (必填)
|
||||
--text string 评论内容 (必填)
|
||||
--content string 评论内容 (必填)
|
||||
```
|
||||
MCP 工具: `dingflow_comments`;参数: processInstanceId, text(对应 --instance-id/--text)。processInstanceId 可通过 `list-pending` 或 `detail` 获取。
|
||||
MCP 工具: `dingflow_comments`;参数: processInstanceId, text(对应 --instance-id/--content)。processInstanceId 可通过 `list-pending` 或 `detail` 获取。
|
||||
|
||||
### 对审批实例进行抄送
|
||||
```
|
||||
Usage:
|
||||
dws oa approval oa-cc-noticer [flags]
|
||||
Example:
|
||||
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --user-list "68674200835816"
|
||||
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --user-list "userId1,userId2"
|
||||
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "68674200835816"
|
||||
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "userId1,userId2" --operator-id "123123"
|
||||
Flags:
|
||||
--instance-id string 审批实例 ID (必填)
|
||||
--user-list string 抄送用户 ID 列表,多个用逗号分隔 (必填)
|
||||
--users string 抄送用户 ID 列表,多个用逗号分隔 (必填)
|
||||
--operator-id string 操作人 ID (可选)
|
||||
```
|
||||
MCP 工具: `oa_cc_noticer`;参数: processInstanceId, userList(对应 --instance-id/--user-list)。processInstanceId 可通过 `list-pending` 或 `detail` 获取,抄送用户 ID 可通过 `dws contact user search` 获取。
|
||||
MCP 工具: `oa_cc_noticer`;参数: processInstanceId, userList(对应 --instance-id/--users)。processInstanceId 可通过 `list-pending` 或 `detail` 获取,抄送用户 ID 可通过 `dws contact user search` 获取。
|
||||
|
||||
### 对审批任务进行加签
|
||||
|
||||
@@ -313,15 +314,15 @@ Flags:
|
||||
用户说"拒绝审批/驳回" → 先 `tasks` 获取 taskId,再 `reject`
|
||||
用户说"撤回审批/取消审批" → `approval revoke`
|
||||
用户说"审批记录/操作历史" → `approval records`
|
||||
用户说"我发起的审批" → `approval list-initiated`(需 --process-code,可从 list-forms 或 detail 获取)
|
||||
用户说"我发起的审批" → `approval list-initiated`(需 --process-code,可从 list-forms / search-forms / detail 获取)
|
||||
用户说"有哪些审批表单/可见表单" → `approval list-forms`
|
||||
用户说"我有哪些待审的任务" → `approval tasks`
|
||||
用户说"我发起的审批单" -> `approval list-submitted`
|
||||
用户说"我审批/处理过的审批单" -> `approval list-executed`
|
||||
用户说"抄送我的审批单" -> `approval list-cc`
|
||||
用户说"转交审批/转交任务" → `approval redirect-task`(需 --task-id 和 --to-actioner-id)
|
||||
用户说"评论审批/添加评论/写评论" → `approval oa-comments`(需 --instance-id 和 --text)
|
||||
用户说"抄送审批/添加抄送人" → `approval oa-cc-noticer`(需 --instance-id 和 --user-list)
|
||||
用户说"评论审批/添加评论/写评论" → `approval oa-comments`(需 --instance-id 和 --content)
|
||||
用户说"抄送审批/添加抄送人" → `approval oa-cc-noticer`(需 --instance-id 和 --users)
|
||||
|
||||
## 核心工作流
|
||||
|
||||
@@ -348,12 +349,12 @@ dws oa approval revoke --instance-id <id> --remark "误发起" --format json
|
||||
dws oa approval records --instance-id <processInstanceId> --format json
|
||||
|
||||
# 7. 获取可见审批表单(得到 processCode)
|
||||
dws oa approval list-forms --cursor 0 --size 100 --format json
|
||||
dws oa approval list-forms --cursor 0 --limit 100 --format json
|
||||
|
||||
# 8. 查看自己发起的审批列表(--process-code 来自 list-forms 或 detail)
|
||||
# 8. 查看自己发起的审批列表(--process-code 来自 list-forms / search-forms / detail)
|
||||
dws oa approval list-initiated --process-code <code> \
|
||||
--start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" \
|
||||
--next-token 0 --max-results 20 --format json
|
||||
--cursor 0 --limit 20 --format json
|
||||
|
||||
# 9. 我处理过的审批单
|
||||
dws oa approval list-executed --limit <pageSize> --page <pageNumber> --query 关键词 --format json
|
||||
@@ -367,11 +368,11 @@ dws oa approval redirect-task --task-id <taskId> --to-actioner-id <userId> --for
|
||||
dws oa approval redirect-task --task-id <taskId> --to-actioner-id <userId> --remark "请帮忙处理" --format json
|
||||
|
||||
# 13. 对审批实例添加评论(processInstanceId 来自 list-pending 或 detail)
|
||||
dws oa approval oa-comments --instance-id <processInstanceId> --text "同意,请尽快处理" --format json
|
||||
dws oa approval oa-comments --instance-id <processInstanceId> --content "同意,请尽快处理" --format json
|
||||
|
||||
# 14. 对审批实例进行抄送(processInstanceId 来自 list-pending 或 detail)
|
||||
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --user-list "68674200835816" --format json
|
||||
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --user-list "userId1,userId2" --format json
|
||||
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "68674200835816" --format json
|
||||
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "userId1,userId2" --format json
|
||||
```
|
||||
|
||||
## 上下文传递表
|
||||
@@ -382,6 +383,7 @@ dws oa approval oa-cc-noticer --instance-id <processInstanceId> --user-list "use
|
||||
| `tasks` | `taskId` | approve / reject / redirect-task 的 --task-id |
|
||||
| `detail` | `processCode` | list-initiated 的 --process-code |
|
||||
| `list-forms` | `processCode` | list-initiated 的 --process-code |
|
||||
| `search-forms` | `processCode` | list-initiated 的 --process-code |
|
||||
|
||||
## 注意事项
|
||||
|
||||
@@ -390,7 +392,8 @@ dws oa approval oa-cc-noticer --instance-id <processInstanceId> --user-list "use
|
||||
- `redirect-task` 的 `--to-actioner-id` 可通过 `dws contact user search` 获取目标用户 userId
|
||||
- `revoke` 只能撤销自己发起的审批
|
||||
- `--remark` 审批意见虽为可选,但建议填写以留存审批痕迹
|
||||
- `list-initiated` 的 `--process-code` 可从 `list-forms` 或 `detail` 返回中提取
|
||||
- `list-initiated` 的 `--process-code` 可从 `list-forms`、`search-forms` 或 `detail` 返回中提取。当 `list-forms` 返回 `processCodeList` 为空(`totalCount -1`)时,用 `search-forms --query <表单名>`(如 `--query 报销`)按名称精准拿 `processCode` 更稳
|
||||
- `list-initiated` 的 `--start` / `--end` 区间有后端上限(约 120 天)。超过上限会返回误导性的 `business_error: 时间戳无效`(实为区间过长,不是时间格式问题)。跨度大时请拆成多段短区间分别查询
|
||||
|
||||
## 自动化脚本
|
||||
|
||||
|
||||
@@ -343,8 +343,8 @@ dws report outbox list --cursor 0 --size 20 --format json
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `template list` | template 名称(result.items[].report_template_name) | `template get` 的 --name |
|
||||
| `template list` | `report_template_id` | `entry submit` 的 --template-id |
|
||||
| `template list` | template 名称(`items[].report_template_name`;顶层键只有 `items` 和 `success`,**无 `result` 包裹**) | `template get` 的 --name |
|
||||
| `template list` | `items[].report_template_id` | `entry submit` 的 --template-id |
|
||||
| `template get` | `result.report_template_fields[].field_name` / `field_sort` / `field_type` | 拼 `entry submit` 的 --contents JSON(按下表映射)|
|
||||
| `entry submit` | `reportId`、`dingtalkOpenMarkdownLink`、`dingtalkOpenUrl`(CLI 自动反查详情后追加)| final reply 优先直接使用 `dingtalkOpenMarkdownLink`;需要结构化展示时用 `dingtalkOpenLink.title` + `dingtalkOpenLink.url` |
|
||||
| `inbox list` / `outbox list` | `reportId` | `entry get` / `entry stats` 的 --report-id |
|
||||
@@ -376,11 +376,11 @@ dws report outbox list --cursor 0 --size 20 --format json
|
||||
| Code | ExitCode | 真实含义 | 建议动作 |
|
||||
|------|---------|---------|---------|
|
||||
| `INPUT_INVALID_JSON` | 3 | `--contents` 或 `--contents-file` 内容非合法 JSON | 检查 JSON 数组结构,每项必须是 object,含 `key`/`sort`/`content`/`contentType`/`type` 五个字段 |
|
||||
| `INPUT_FILE_NOT_FOUND` | 3 | `--contents-file` 路径不存在 / sandbox OS 风格不匹配(macOS 路径在 Windows 沙箱)| 先确认 sandbox OS 与路径风格;改写到 `os.tmpdir()` 等可移植目录 |
|
||||
| `INPUT_FILE_NOT_FOUND` | 3 | `--contents-file` 指向的文件不存在(路径必须在当前工作目录 CWD 之下)| 确认文件存在且位于 CWD 内;把 contents JSON 写到 CWD 下的相对路径再传 |
|
||||
| `INPUT_INVALID_PATH` | 3 | `--contents-file` 路径越出当前工作目录(如 `/tmp`、`os.tmpdir()`、`../` 或指向目录外的符号链接)| **不要**用 `/tmp` / `os.tmpdir()` / `../`;把 contents JSON 写到当前工作目录之下再传 |
|
||||
| `INPUT_MISSING_PARAM` | 3 | `--template-id` / `--contents` 必填缺失 | 显式传值;从 `template list` 取合法 templateId |
|
||||
| `INPUT_TOO_LARGE` | 3 | contents JSON 超过 10MB 限制 | **不支持分批次提交**。需精简内容或拆分为多个独立日志分别提交 |
|
||||
| `MCP_TOOL_ERROR` | 1 | 服务端业务错(含 `server_error_code: PARAM_ERROR`,覆盖 templateId 错 / 字段名错 / 字段值错 / contents 空等多种形态)| 查看 `server_error_code` / `technical_detail`;服务端不区分具体子错因,按提交链路重新走 `template list → template get → entry submit`;连续 ≥ 2 次仍失败必须停止重试,降级 final_reply |
|
||||
| `RESOURCE_NOT_FOUND` | 1 | reportId / templateId 在服务端找不到 | 用 `list` 或 `template list` 重新获取 |
|
||||
| `MCP_TOOL_ERROR` / `[UNCLASSIFIED] business error` | 1 | 服务端业务错,`success=false`、`code=1`。覆盖:submit 时 templateId 错 / 字段名错 / 字段值错 / contents 空(`server_error_code: PARAM_ERROR`);`entry get` / `entry stats` 传了不存在的 reportId(`server_error_code: BUSINESS_ERROR`,technical_detail 含 `report not exist`);`template get` 传了不存在的模版名(`server_error_code: PARAM_ERROR`)| 查看 `server_error_code` / `technical_detail`;reportId / 模版名找不到时先用 `inbox list` / `outbox list` / `template list` 重新取合法值,不要猜;submit 类错误按链路重走 `template list → template get → entry submit`,连续 ≥ 2 次仍失败必须停止重试,降级 final_reply |
|
||||
|
||||
## 何时停止重试
|
||||
|
||||
|
||||
@@ -83,11 +83,20 @@ dws sheet filter-view --help
|
||||
| `sheet copy` | 复制工作表 |
|
||||
| `sheet range read` | 读取工作表数据(别名: range get) |
|
||||
| `sheet range update` | 更新指定区域内容(值/公式/超链接) |
|
||||
| `sheet range clear` | 清除区域(值/格式/全部) |
|
||||
| `sheet range sort` | 对区域排序 |
|
||||
| `sheet range fill` | 自动填充区域 |
|
||||
| `sheet range copy-to` | 复制区域到目标位置 |
|
||||
| `sheet range move-to` | 移动区域到目标位置 |
|
||||
| `sheet range batch-clear` | 批量清除多个区域(原子事务) |
|
||||
| `sheet batch-update` | 批量执行多个写操作(原子事务) |
|
||||
| `sheet csv-get` | 以 CSV 格式读取区域数据 |
|
||||
| `sheet range set-style` | 设置单元格样式 |
|
||||
| `sheet range batch-set-style` | 按配置文件批量设置样式 |
|
||||
| `sheet find` | 搜索单元格内容 |
|
||||
| `sheet append` | 在末尾追加数据行 |
|
||||
| `sheet csv-put` | 将 CSV 数据写入指定位置(纯值,自动扩容) |
|
||||
| `sheet delete-sheet` | 删除工作表(不可逆,删除前必须确认) |
|
||||
| `sheet replace` | 全局查找替换文本 |
|
||||
| `sheet merge-cells` | 合并单元格 |
|
||||
| `sheet unmerge-cells` | 取消合并单元格 |
|
||||
@@ -122,9 +131,44 @@ dws sheet filter-view --help
|
||||
| `sheet filter-view info` | 获取单个筛选视图详情 |
|
||||
| `sheet filter-view list-criteria` | 列出筛选视图所有列条件 |
|
||||
| `sheet filter-view get-criteria` | 获取单列筛选条件详情 |
|
||||
| `sheet cond-format list` | 获取条件格式规则 |
|
||||
| `sheet cond-format create` | 创建条件格式规则 |
|
||||
| `sheet cond-format update` | 更新条件格式规则 |
|
||||
| `sheet cond-format delete` | 删除条件格式规则 |
|
||||
| `sheet chart list` | 获取浮动图表 |
|
||||
| `sheet chart create` | 创建浮动图表 |
|
||||
| `sheet chart update` | 更新浮动图表 |
|
||||
| `sheet chart delete` | 删除浮动图表 |
|
||||
| `sheet template list` | 获取表格模板列表 |
|
||||
| `sheet template search` | 搜索表格模板 |
|
||||
| `sheet template apply` | 应用模板创建新表格文档 |
|
||||
|
||||
> 不确定参数?对任意命令执行 `dws sheet <命令> --help` 查看完整用法。
|
||||
|
||||
## 子文档索引(更多命令详细参考)
|
||||
|
||||
本文档覆盖高频命令。以下命令的完整参数、示例与工作流在 `sheet/` 子目录,需要时按主题查阅:
|
||||
|
||||
| 主题 | 子文档 | 覆盖命令 |
|
||||
|------|--------|---------|
|
||||
| 表格与工作表管理 | [sheet/sheet-workbook.md](./sheet/sheet-workbook.md) | create / list / info / new / update / copy / delete-sheet |
|
||||
| 写入数据 | [sheet/sheet-write-data.md](./sheet/sheet-write-data.md) | range update(对象协议详解)/ append / csv-put |
|
||||
| 读取数据 | [sheet/sheet-read-data.md](./sheet/sheet-read-data.md) | range read / csv-get |
|
||||
| 区域操作 | [sheet/sheet-range-operations.md](./sheet/sheet-range-operations.md) | range clear / sort / fill / copy-to / move-to |
|
||||
| 批量操作 | [sheet/sheet-batch-operations.md](./sheet/sheet-batch-operations.md) | range batch-clear / batch-update |
|
||||
| 行列操作 | [sheet/sheet-dimension-operations.md](./sheet/sheet-dimension-operations.md) | insert / delete / update / move / add-dimension |
|
||||
| 样式与合并 | [sheet/sheet-style-format.md](./sheet/sheet-style-format.md) | range set-style / batch-set-style / merge-cells / unmerge-cells |
|
||||
| 条件格式 | [sheet/sheet-conditional-format.md](./sheet/sheet-conditional-format.md) | cond-format list / create / update / delete |
|
||||
| 浮动图表 | [sheet/sheet-chart.md](./sheet/sheet-chart.md) | chart list / create / update / delete |
|
||||
| 下拉列表 | [sheet/sheet-dropdown.md](./sheet/sheet-dropdown.md) | set / get / delete-dropdown |
|
||||
| 媒体与图片 | [sheet/sheet-media-image.md](./sheet/sheet-media-image.md) | media-upload / write-image / 浮动图片 |
|
||||
| 查找替换 | [sheet/sheet-search-replace.md](./sheet/sheet-search-replace.md) | find / replace |
|
||||
| 全局筛选 | [sheet/sheet-filter.md](./sheet/sheet-filter.md) | filter get / create / delete / update / clear-criteria / sort |
|
||||
| 筛选视图 | [sheet/sheet-filter-view.md](./sheet/sheet-filter-view.md) | filter-view 全系列 |
|
||||
| 导出 | [sheet/sheet-export.md](./sheet/sheet-export.md) | export |
|
||||
|
||||
> `template list/search/apply` 无独立子文档,直接执行 `dws sheet template <子命令> --help` 查看用法。
|
||||
|
||||
## 意图判断
|
||||
|
||||
### 表格与工作表管理
|
||||
@@ -194,7 +238,7 @@ dws sheet filter-view --help
|
||||
|
||||
用户说"删除行/删除列/删掉第几行/删掉某列/移除行/移除列":
|
||||
- 删除行或列 → `delete-dimension`
|
||||
- 仅清空内容但保留行/列 → `range update --values` 写入空字符串 `""`
|
||||
- 仅清空内容但保留行/列 → `range clear`(整片区域清除,比逐格写空更简洁)
|
||||
|
||||
用户说"隐藏行/隐藏列/显示行/显示列/设置行高/设置列宽/调整行高/调整列宽/行列属性":
|
||||
- 隐藏/显示行或列 → `update-dimension --hidden` / `--hidden=false`
|
||||
@@ -340,8 +384,9 @@ dws sheet filter-view --help
|
||||
以下是最容易出错的规则,**必须严格遵守**:
|
||||
|
||||
- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node <NODE_ID> --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等)
|
||||
- ★ **`range update` 维度校验(强制)**:`--values` / `--hyperlinks` 的行列数必须与 `--range` 完全一致。例如 `--range "A1:C3"` → `--values` 必须是 3×3 数组
|
||||
- ★ **`range update` 清空规范(强制)**:清空单元格用空字符串 `""`,禁止用 `null`(全 null 会被跳过无效果)
|
||||
- ★ **`range update` 单元格协议(强制)**:`--values` 是二维 JSON 数组,**每个单元格必须是 object**(如 `{"type":"text","text":"张三"}`),不再支持裸值 `"张三"` / `90` / `null`。裸值会报错「不支持原始值……每个单元格必须是 object」。数字/布尔也写成字符串 object(如 `{"type":"text","text":"90"}`),服务端自动识别类型。超链接写在单元格 object 的 `hyperlink` 字段,**没有 `--hyperlinks` flag**(传了报 unknown flag)
|
||||
- ★ **`range update` 维度校验(强制)**:`--values` 的行列数必须与 `--range` 完全一致。例如 `--range "A1:C3"` → `--values` 必须是 3×3 的 object 数组
|
||||
- ★ **`range update` 清空规范(强制)**:清空单个单元格用 `{"type":"text","text":""}`;清空整片区域用 `range clear`。跳过某格保留原值用 `{}` 空对象
|
||||
- ★ **单次调用上限(强制)**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000)
|
||||
- ★ **大批量纯值写入用 `csv-put` 不用 `range update`**:当写入纯值(无公式/超链接)且数据量较大时(>5 行或 >20 单元格),必须使用 `csv-put`。`csv-put` 接受 CSV 文本直接写入,无需构造二维 JSON 数组,支持自动扩容,更简洁高效。仅在需要写入公式、超链接、或仅更新少量单元格时才使用 `range update`
|
||||
- ★ **搜索用 `find` 不用 `range read`**:`find` 是服务端搜索,禁止用 `range read` 全量读取后客户端过滤
|
||||
@@ -432,15 +477,17 @@ Example:
|
||||
Flags:
|
||||
--node string 表格文档 ID 或 URL (必填)
|
||||
--sheet-id string 工作表 ID 或名称 (必填)
|
||||
--title string 新标题,最长 100 字符,不能包含 / \ ? * [ ] :
|
||||
--name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] :
|
||||
--title string --name 的别名(兼容)
|
||||
--index int 新位置(从 0 开始)
|
||||
--hidden --hidden=true 隐藏,--hidden=false 取消隐藏
|
||||
--tab-color string 工作表标签颜色,Hex 如 #FF0000;传空字符串清除颜色
|
||||
--frozen-row-count int 冻结行数,0 表示取消冻结
|
||||
--frozen-column-count int 冻结列数,0 表示取消冻结
|
||||
```
|
||||
|
||||
更新工作表标题、位置、隐藏状态、冻结行列。
|
||||
`--title` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。
|
||||
更新工作表名称、位置、隐藏状态、标签颜色、冻结行列。
|
||||
`--name`(别名 `--title`)/ `--index` / `--hidden` / `--tab-color` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。
|
||||
|
||||
注意:
|
||||
- 至少需要保留一个可见的工作表,不能将所有工作表都隐藏
|
||||
@@ -483,14 +530,19 @@ Example:
|
||||
|
||||
# 使用 get 别名,与 read 等价
|
||||
dws sheet range get --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:D10"
|
||||
|
||||
# 指定取值模式:原始值 / 公式文本
|
||||
dws sheet range read --node <NODE_ID> --value-render-option raw_value
|
||||
dws sheet range read --node <NODE_ID> --value-render-option formula
|
||||
Flags:
|
||||
--node string 表格文档 ID 或 URL (必填)
|
||||
--sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表)
|
||||
--range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据)
|
||||
--node string 表格文档 ID 或 URL (必填)
|
||||
--sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表)
|
||||
--range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据)
|
||||
--value-render-option string 取值模式: formatted_value(格式化展示值,默认) | raw_value(原始值) | formula(公式文本,无公式回退原始值)
|
||||
```
|
||||
|
||||
**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略:
|
||||
- 先通过 `info` 获取 `rowCount` / `lastNonEmptyRow` / `columnCount` 确定数据边界
|
||||
- 先通过 `info` 获取 `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` 确定数据边界(空表时这些字段为 null)
|
||||
- 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` ……
|
||||
- 避免不传 `--range` 直接读取整个大工作表
|
||||
|
||||
@@ -499,34 +551,50 @@ Flags:
|
||||
Usage:
|
||||
dws sheet range update [flags]
|
||||
Example:
|
||||
# 写入值
|
||||
# 写入文本(每个单元格必须是 object)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:B2" \
|
||||
--values '[["姓名","分数"],["张三",90]]'
|
||||
--values '[[{"type":"text","text":"姓名"},{"type":"text","text":"分数"}],[{"type":"text","text":"张三"},{"type":"text","text":"90"}]]'
|
||||
|
||||
# 写入公式
|
||||
# 写入公式(text 以 = 开头识别为公式)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "C2" \
|
||||
--values '[["=A2&B2"]]'
|
||||
--values '[[{"type":"text","text":"=A2&B2"}]]'
|
||||
|
||||
# 写入超链接
|
||||
# 写入单元格级超链接(写在 cell 的 hyperlink 字段,没有 --hyperlinks flag)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1" \
|
||||
--hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"钉钉"}]]'
|
||||
--values '[[{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]'
|
||||
|
||||
# 清空区域(使用空字符串 "")
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:B3" \
|
||||
--values '[["",""],["",""],["",""]]'
|
||||
# 只更新部分单元格:用 {} 空对象占位保留原值
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:B1" \
|
||||
--values '[[{"type":"text","text":"新值"},{}]]'
|
||||
|
||||
# 清空单个单元格(text 为空字符串)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1" \
|
||||
--values '[[{"type":"text","text":""}]]'
|
||||
|
||||
# 清空整片区域请改用 range clear
|
||||
dws sheet range clear --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:B3"
|
||||
Flags:
|
||||
--node string 表格文档 ID (必填)
|
||||
--sheet-id string 工作表 ID 或名称 (必填)
|
||||
--range string 目标单元格区域地址,如 A1:B3 (必填)
|
||||
--values string 单元格值,二维 JSON 数组 (与 --hyperlinks 至少传一项)
|
||||
--hyperlinks string 超链接,二维 JSON 数组 (与 --values 至少传一项)
|
||||
--values string 单元格内容,二维 JSON 数组 (必填);每个元素必须是 object
|
||||
```
|
||||
|
||||
**单元格对象协议(重要)**:`--values` 每个元素必须是 object,支持以下形态:
|
||||
- `{"type":"text","text":"内容"}`:普通文本;`text` 以 `=` 开头识别为公式;`text` 为 `""` 清空该格;数字/布尔写成字符串(`{"type":"text","text":"100"}`),服务端自动识别类型
|
||||
- `{"type":"text","text":"内容","cellStyles":{...}}`:文本 + 整格样式(`fontWeight`/`fontColor`/`backgroundColor`/`numberFormat` 等)
|
||||
- `{"type":"richText","texts":[...]}`:富文本,子项可为 text/link/attachment/image,片段样式写在子项 `style`
|
||||
- `{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://..."}}`:整格超链接(`type` 可为 `path`/`sheet`/`range`,或 `{"type":"none"}` 清除)
|
||||
- `{"dataValidation":{...}}`:写下拉/复选框数据验证
|
||||
- `{}`:空对象,跳过该格保留原值(只改部分单元格时占位用)
|
||||
|
||||
不再支持裸值(`"张三"` / `90` / `null`),原样传入会报「不支持原始值……每个单元格必须是 object」。
|
||||
|
||||
**单次调用建议**:行数 ≤ 1000,单元格总数(行×列)≤ 5000;超过时请拆分多次调用。
|
||||
|
||||
**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式和超链接),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装二维 JSON 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、超链接(`--hyperlinks`)、或修改少量单元格时才使用 `range update`。
|
||||
**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式和超链接),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装 object 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、超链接、富文本、或修改少量单元格时才使用 `range update`。
|
||||
|
||||
**范围职责**:`range update` 仅负责写入单元格的值与超链接,不接受任何样式参数。如需设置数字格式(百分比 / 货币 / 日期 / 文本等)请使用 `dws sheet range set-style --number-format <格式代码>`,可与其他样式参数同时传入。
|
||||
**范围职责**:`range update` 负责写入单元格的值、超链接、per-cell 样式与数据验证。批量刷整片区域的统一样式或数字格式(百分比 / 货币 / 日期 / 文本等)请使用 `dws sheet range set-style --number-format <格式代码>`。
|
||||
|
||||
### 设置单元格样式
|
||||
```
|
||||
@@ -662,9 +730,11 @@ Flags:
|
||||
Usage:
|
||||
dws sheet csv-put [flags]
|
||||
Example:
|
||||
# 内联多行 CSV:必须用 $'...' 让 \n 变成真换行;普通单引号 '...\n...' 里的 \n 是字面量,会写成一行含字面 \n 的错误数据
|
||||
dws sheet csv-put --node <NODE_ID> --sheet-id <SHEET_ID> --start-cell A1 \
|
||||
--csv 'name,score\nAlice,95\nBob,87'
|
||||
--csv $'name,score\nAlice,95\nBob,87'
|
||||
|
||||
# 多行数据推荐用 @文件,最稳妥
|
||||
dws sheet csv-put --node <NODE_ID> --sheet-id <SHEET_ID> --start-cell B2 \
|
||||
--csv @data.csv --allow-overwrite
|
||||
|
||||
@@ -750,7 +820,7 @@ Flags:
|
||||
在钉钉表格指定工作表中,从指定位置起删除若干连续的行或列。
|
||||
`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。
|
||||
支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。
|
||||
删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`。
|
||||
删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range clear`(整片区域清除,比逐格写空更简洁)。
|
||||
|
||||
### 更新指定范围行/列属性
|
||||
```
|
||||
@@ -832,6 +902,8 @@ Flags:
|
||||
--mime-type string 文件 MIME 类型 (默认根据扩展名推断)
|
||||
```
|
||||
|
||||
`--format json` 输出规整 JSON:`{success, resourceId, resourceUrl, fileName, fileSize}`。`resourceUrl` 可用于 `create-float-image` 的 `--src`。
|
||||
|
||||
### 上传图片并写入表格单元格
|
||||
```
|
||||
Usage:
|
||||
@@ -1083,7 +1155,8 @@ Flags:
|
||||
查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。
|
||||
- **用途**:查看单元格已设置的下拉列表选项和配置。
|
||||
- **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。
|
||||
- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。
|
||||
- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `multipleValues` 和 `colorValueMap`)。**判空以 `hasDropdown` 为准**:无下拉列表时 `hasDropdown` 为 false,但 `dataValidations` 里仍会带 1 个全 null 的幽灵条目,不要用数组长度判空。
|
||||
- **已知限制**:`options.multipleValues` 服务端恒返回 `null`(即使该下拉是用 `--multi-select` 建的),且不返回 `enableMultiSelect` 字段——**`get-dropdown` 读回无法判断下拉是否多选**。要判断是否多选,改用 `range read`:其 `cells[].dataValidation.enableMultiSelect` 字段准确(实测有效)。
|
||||
|
||||
### 删除下拉列表
|
||||
```
|
||||
@@ -1118,7 +1191,7 @@ Flags:
|
||||
- **用途**:查看当前工作表上是否存在全局筛选及其配置。
|
||||
- **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。
|
||||
- **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。
|
||||
- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。
|
||||
- **返回**:`range`(筛选范围,A1 表示法)、`id`(筛选 ID)和 `criteria`(各列条件对象,key 为列偏移量;无条件时为 `{}`)。如果未设置筛选,返回筛选信息为空。
|
||||
|
||||
### 创建筛选
|
||||
```
|
||||
@@ -1499,16 +1572,39 @@ Flags:
|
||||
- 第 21~30 次:每次间隔 15 秒
|
||||
- **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误
|
||||
|
||||
**命令返回**:
|
||||
- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载)
|
||||
- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: <path>`
|
||||
- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下
|
||||
**命令返回**(`--format json`,默认):输出规整 JSON `{success, jobId, downloadUrl[, outputPath]}`。轮询进度以 `[INFO] [N/30] 状态: ...` 打到 stderr,不污染 stdout 的 JSON。
|
||||
- `--output` 未指定:JSON 含 `jobId` + `downloadUrl`(链接有时效性,请尽快下载)
|
||||
- `--output` 指定为文件路径:下载到该路径,JSON 额外含 `outputPath`
|
||||
- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录,JSON 含 `outputPath`
|
||||
|
||||
**失败处理(命令内部已处理,Agent 仅需转述)**:
|
||||
- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试
|
||||
- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试
|
||||
|
||||
**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
|
||||
**限制**:仅支持钉钉在线电子表格(axls)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
|
||||
|
||||
### 表格模板(list / search / apply)
|
||||
```
|
||||
Usage:
|
||||
dws sheet template list [flags] # 列出可用模板(--cursor / --limit 分页)
|
||||
dws sheet template search [flags] # 按关键词搜索模板
|
||||
dws sheet template apply [flags] # 应用模板创建新表格文档
|
||||
|
||||
template search Flags:
|
||||
--query string 搜索关键词 (必填)
|
||||
--source string 模板来源: MY(我的模版,默认) / PUBLIC(公开模版)
|
||||
--cursor string 分页游标
|
||||
--limit int 返回数量上限
|
||||
|
||||
template apply Flags:
|
||||
--template-id string 模板 ID (必填,来自 list/search)
|
||||
--name string 新表格文档名称 (可选)
|
||||
--folder string 目标文件夹 ID (可选,UUID 或 URL)
|
||||
--workspace string 知识库 ID (可选)
|
||||
```
|
||||
|
||||
- 用户说"用模板建表 / 有哪些模板 / 找个 XX 模板"时走 template 系列
|
||||
- 典型链路:`template search --query "..."` 或 `template list` 拿到 `templateId` → `template apply --template-id <ID> --name "..."` 创建新表格,返回新文档的 `nodeId`
|
||||
|
||||
## 核心工作流
|
||||
|
||||
@@ -1521,12 +1617,12 @@ dws sheet create --name "销售数据" --format json
|
||||
# 2. 查看工作表列表 — 提取 sheetId
|
||||
dws sheet list --node <NODE_ID> --format json
|
||||
|
||||
# 3. 写入表头和数据
|
||||
# 3. 写入表头和数据(每个单元格必须是 object;数字也写成字符串)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:C1" \
|
||||
--values '[["姓名","部门","销售额"]]' --format json
|
||||
--values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json
|
||||
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A2:C4" \
|
||||
--values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json
|
||||
--values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json
|
||||
|
||||
# ── 工作流 2: 读取已有表格数据 ──
|
||||
|
||||
@@ -1547,26 +1643,26 @@ dws sheet range read --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:D10" --f
|
||||
# 1. 新建工作表
|
||||
dws sheet new --node <NODE_ID> --name "汇总" --format json
|
||||
|
||||
# 2. 在新工作表中写入汇总公式
|
||||
# 2. 在新工作表中写入汇总公式(公式写在 text,以 = 开头)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <NEW_SHEET_ID> --range "A1:B1" \
|
||||
--values '[["指标","数值"]]' --format json
|
||||
--values '[[{"type":"text","text":"指标"},{"type":"text","text":"数值"}]]' --format json
|
||||
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <NEW_SHEET_ID> --range "A2:B2" \
|
||||
--values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json
|
||||
--values '[[{"type":"text","text":"总销售额"},{"type":"text","text":"=SUM(Sheet1!C2:C100)"}]]' --format json
|
||||
|
||||
# ── 工作流 4: 写入数据并设置样式 ──
|
||||
|
||||
# 1. 写入数据
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:C3" \
|
||||
--values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json
|
||||
--values '[[{"type":"text","text":"商品"},{"type":"text","text":"单价"},{"type":"text","text":"数量"}],[{"type":"text","text":"苹果"},{"type":"text","text":"5.5"},{"type":"text","text":"100"}],[{"type":"text","text":"香蕉"},{"type":"text","text":"3.2"},{"type":"text","text":"200"}]]' --format json
|
||||
|
||||
# 2. 设置数字格式(人民币)——请走 set-style,不要放到 range update
|
||||
# 2. 设置数字格式(人民币)——批量刷整列走 set-style
|
||||
dws sheet range set-style --node <NODE_ID> --sheet-id <SHEET_ID> --range "B2:B3" \
|
||||
--number-format "¥#,##0.00" --format json
|
||||
|
||||
# 3. 写入超链接
|
||||
# 3. 写入超链接(写在 cell 的 hyperlink 字段)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "D1" \
|
||||
--hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"详情"}]]' --format json
|
||||
--values '[[{"type":"text","text":"详情","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' --format json
|
||||
|
||||
# ── 工作流 5: 追加数据 ──
|
||||
|
||||
@@ -1716,8 +1812,8 @@ dws sheet write-image --node <NODE_ID> --sheet-id <SHEET_ID> --range C3:C3 --fil
|
||||
# 4. 完整流程: 创建表格 → 写表头 → 写入图片
|
||||
dws sheet create --name "产品目录" -f json
|
||||
# 提取 nodeId 后:
|
||||
dws sheet range update --node <NODE_ID> --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json
|
||||
dws sheet range update --node <NODE_ID> --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json
|
||||
dws sheet range update --node <NODE_ID> --sheet-id Sheet1 --range "A1:B1" --values '[[{"type":"text","text":"产品名称"},{"type":"text","text":"产品图片"}]]' -f json
|
||||
dws sheet range update --node <NODE_ID> --sheet-id Sheet1 --range "A2:A2" --values '[[{"type":"text","text":"MacBook Pro"}]]' -f json
|
||||
dws sheet write-image --node <NODE_ID> --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json
|
||||
```
|
||||
|
||||
@@ -1789,7 +1885,7 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
| `create` | `nodeId` | list / info / new / range read / range update / find 的 --node |
|
||||
| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id |
|
||||
| `new` | 新工作表的 `sheetId` | range read / range update / find 的 --sheet-id |
|
||||
| `info` | `rowCount` / `lastNonEmptyRow` | 确定数据范围、追加写入起始行 |
|
||||
| `info` | `rowCount` / `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` / `mergedRanges` | 确定数据范围、追加写入起始行、判断合并结构(空表时 nonEmptyRange.* 为 null) |
|
||||
| `find` | `matchedCells` 中的 `a1Notation` | 定位目标单元格,用于 range read / range update |
|
||||
| `append` | `a1Notation` 追加数据所在范围 | 确认追加位置 |
|
||||
| `csv-put` | `a1Notation` 实际写入的单元格范围 | 确认写入位置和范围 |
|
||||
@@ -1820,7 +1916,7 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
| `filter-view delete-criteria` | `id` 筛选视图 ID | 确认条件清除完成 |
|
||||
| `filter-view list-criteria` | 所有列条件(按列偏移量为 key 的对象) | 了解当前视图已设置哪些列的条件 |
|
||||
| `filter-view get-criteria` | 指定列的条件详情(`filterType`、`conditions` 等) | 查看某列的具体筛选规则 |
|
||||
| `export` | `downloadUrl`(未指定 --output)/ `导出完成: <path>`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 |
|
||||
| `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 |
|
||||
|
||||
## nodeId 多格式说明
|
||||
|
||||
@@ -1830,26 +1926,28 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
|
||||
## values 参数格式说明
|
||||
|
||||
`--values` 为二维 JSON 数组,第一维为行,第二维为列:
|
||||
- 字符串值: `"文本"`
|
||||
- 数字值: `100` 或 `3.14`
|
||||
- 公式: `"=SUM(B2:B4)"`(以 `=` 开头的字符串自动识别为公式)
|
||||
- 清空单元格: 统一使用空字符串 `""`(不要用 `null` 取代,null 不会保留原值且全 null 会被视为无效调用跳过)
|
||||
`--values` 为二维 JSON 数组,第一维为行,第二维为列。**每个单元格必须是 object**,不支持裸值 `"文本"` / `100` / `null`:
|
||||
- 文本: `{"type":"text","text":"文本"}`
|
||||
- 数字/布尔: 写成字符串 `{"type":"text","text":"100"}`,服务端按内容自动识别为数字/布尔
|
||||
- 公式: `{"type":"text","text":"=SUM(B2:B4)"}`(`text` 以 `=` 开头识别为公式)
|
||||
- 清空单个单元格: `{"type":"text","text":""}`;清空整片区域用 `range clear`
|
||||
- 跳过某格保留原值: `{}` 空对象
|
||||
- 整格样式: `{"type":"text","text":"重要","cellStyles":{"fontWeight":"bold","fontColor":"#FF0000","numberFormat":"@"}}`
|
||||
- 富文本: `{"type":"richText","texts":[...]}`(子项 text/link/attachment/image,片段样式写在子项 `style`)
|
||||
|
||||
维度必须与 `--range` 范围一致,例如 `--range "A1:B3"` 需要 3 行 2 列的数组。
|
||||
维度必须与 `--range` 范围一致,例如 `--range "A1:B3"` 需要 3 行 2 列的 object 数组。
|
||||
|
||||
## hyperlinks 参数格式说明
|
||||
## 超链接写法说明
|
||||
|
||||
`--hyperlinks` 为二维 JSON 数组,每个元素为对象或 null:
|
||||
- `type`: 链接类型,可选 `path`(外部链接)、`sheet`(工作表跳转)、`range`(单元格跳转)
|
||||
- `link`: 链接地址
|
||||
- `text`: 显示文本
|
||||
|
||||
与 `--values` 共存时,hyperlinks 优先级更高。
|
||||
**没有 `--hyperlinks` flag**(旧写法已废弃,传入报 unknown flag)。单元格级超链接写在 `--values` 里 cell object 的 `hyperlink` 字段:
|
||||
- `{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}`:写外部链接
|
||||
- `hyperlink.type` 可为 `path`(外部链接)/ `sheet`(工作表跳转,link 为工作表名)/ `range`(区域跳转,link 为 `Sheet2!A1`)
|
||||
- `{"hyperlink":{"type":"none"}}`:清除整格超链接(保留原值)
|
||||
- 富文本片段内的链接写在 `richText` 子项的 `link` 字段,与整格 `hyperlink` 不同
|
||||
|
||||
## number-format 常用值
|
||||
|
||||
适用范围:`number-format` 仅在 `range set-style` / `range batch-set-style` 中接受(CLI 对应 `--number-format`,batch 配置文件对应 `numberFormat`);`range update` 不接受该参数。
|
||||
适用范围:`number-format` 在 `range set-style` / `range batch-set-style` 中作为 `--number-format` 参数;`range update` 没有 `--number-format` flag,但可在每个 cell object 的 `cellStyles.numberFormat` 里写同样的格式代码。批量刷整片区域优先用 `set-style`。
|
||||
|
||||
| 格式代码 | 说明 | 示例 |
|
||||
|----------|------|------|
|
||||
@@ -1867,20 +1965,20 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
> 标 ★ 的条目已在前文「关键注意事项」中列出,此处为完整说明。
|
||||
|
||||
- ★ `--sheet-id` 获取规范(强制):所有涉及 `--sheet-id` 参数的命令(`info` / `new` / `range read` / `range update` / `find` / `append` / `insert-dimension` / `delete-dimension` / `update-dimension` / `move-dimension` / `add-dimension` / `merge-cells` / `unmerge-cells` / `replace` / `write-image` / `set-dropdown` / `get-dropdown` / `delete-dropdown` / `filter-view *` 等),除非用户主动提供了工作表 ID 或工作表名称,否则在 `sheetId` 未知时必须先通过 `dws sheet list --node <NODE_ID> --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败
|
||||
- ★ `range update` 维度校验(强制):调用 `range update` 写入 `--values` 或 `--hyperlinks` 时,必须严格校验二维 JSON 数组的行数与列数与 `--range` 指定的范围完全一致:
|
||||
- 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 `[[v1,v2,v3],[v4,v5,v6],[v7,v8,v9]]` 这样 3×3 的数组
|
||||
- `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[v]]`
|
||||
- 行数不足需要用空字符串补齐,列数不足需要补齐到每行相同长度;禁止出现各行列数不一致或与 `--range` 不匹配的情况,否则调用会直接报错
|
||||
- 同时传入 `--values` 和 `--hyperlinks` 时,两个二维数组的行列数都必须与 `--range` 严格一致
|
||||
- ★ `range update` 清空单元格规范(强制):如需清空单元格内容,统一使用空字符串 `""`。禁止使用 `null`:`null` 不会保留单元格原值,也不存在"选择性保留"场景;且若 `--values` 全部为 `null`,整体调用会被视为无效而跳过,无任何写入效果
|
||||
- ★ `range update` 单元格对象协议(强制):`--values` 每个单元格必须是 object(`{"type":"text","text":...}` / `{"type":"richText",...}` / `{}` 等),不支持裸值 `"张三"` / `90` / `null`,裸值报「不支持原始值……每个单元格必须是 object」。数字/布尔写成字符串 object,服务端自动识别类型。超链接写在 cell object 的 `hyperlink` 字段,没有 `--hyperlinks` flag
|
||||
- ★ `range update` 维度校验(强制):`--values` 二维数组的行数与列数必须与 `--range` 完全一致:
|
||||
- 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 3×3 的 object 数组
|
||||
- `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[{...}]]`
|
||||
- 不改的格用 `{}` 空对象占位补齐;禁止各行列数不一致或与 `--range` 不匹配,否则报错
|
||||
- ★ `range update` 清空单元格规范(强制):清空单个单元格用 `{"type":"text","text":""}`(不是裸 `""`,也不是 `null`);清空整片区域用 `range clear`;跳过某格保留原值用 `{}` 空对象
|
||||
- `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录
|
||||
- `list` 返回所有工作表的 ID 和名称,是后续操作的必要前置步骤
|
||||
- `info` 不传 `--sheet-id` 时默认返回第一个工作表的详情
|
||||
- `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据
|
||||
- `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`)
|
||||
- `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 获取边界后分批读取,避免不传 `--range` 直接读取整个大工作表
|
||||
- `range update` 的 `--values` 和 `--hyperlinks` 至少传入一项
|
||||
- `range update` 职责边界:`range update` 仅写入单元格的值与超链接,不接受任何样式参数(包括但不限于数字格式 / 背景色 / 字体 / 对齐方式等)。如需设置数字格式,请使用 `dws sheet range set-style --number-format <格式代码>`;批量场景走 `dws sheet range batch-set-style --batch <config.json>`(配置文件中使用 `numberFormat` 字段)。不要在同一次 `range update` 调用里同时完成写值与格式设置
|
||||
- `range update` 只有 `--values` 一个内容参数(必填),超链接/样式/数据验证都写进 cell object 内部字段
|
||||
- `range update` 职责边界:可写值、超链接、per-cell 样式(`cellStyles`)与数据验证(`dataValidation`)。批量刷整片区域的统一样式或数字格式优先用 `dws sheet range set-style --number-format <格式代码>`;批量多区域走 `dws sheet range batch-set-style --batch <config.json>`(配置文件用 `numberFormat` 字段)
|
||||
- ★ `range update` / `range set-style` / `range batch-set-style` 单次调用上限(强制):行数 ≤ 1000,单元格总数(行×列)建议≤ 5000(底层硬限 30000);超限请拆分多次调用。CLI 会在调用前做本地预校验,底层超 30000 会直接报错
|
||||
- `range set-style` / `range batch-set-style` 的样式枚举按驼峰书写:`wordWrap` 取 `overflow`/`clip`/`autoWrap`,`fontWeight` 取 `bold`/`normal`,`hAlign` 取 `left`/`center`/`right`/`general`,`vAlign` 取 `top`/`middle`/`bottom`;背景色/字体颜色统一使用 `#RRGGBB` 格式
|
||||
- `new` 创建工作表时,如名称与已有工作表重复,系统会自动重命名
|
||||
@@ -1900,7 +1998,7 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
- `delete-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS`
|
||||
- `delete-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`
|
||||
- `delete-dimension` 的 `--length` 最大为 5000
|
||||
- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`(参见《range update 清空单元格规范》)
|
||||
- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range clear`(整片区域清除,比逐格写空更简洁)
|
||||
- `update-dimension` 批量更新连续行/列的显隐状态与行高/列宽
|
||||
- `update-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS`
|
||||
- `update-dimension` 的 `--start-index` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`
|
||||
@@ -1939,7 +2037,7 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false
|
||||
- `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功
|
||||
- ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列
|
||||
- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空
|
||||
- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)、`id` 和 `criteria`(各列条件对象,无条件时为 `{}`)。无筛选时返回空
|
||||
- `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错
|
||||
- `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件
|
||||
- `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复
|
||||
@@ -1969,7 +2067,7 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
- `filter-view delete-criteria` 仅清除指定列的条件,不会删除整个筛选视图。如需删除整个筛选视图,请使用 `filter-view delete`
|
||||
- `filter-view delete-criteria` 如果指定列没有设置筛选条件,调用不会报错
|
||||
- 筛选视图相关操作需要"可阅读"权限(list / info / list-criteria / get-criteria)或"可编辑"权限(create / update / delete / update-criteria / delete-criteria),不支持跨组织操作
|
||||
- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
|
||||
- ★ `export` 仅支持钉钉在线电子表格(axls)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
|
||||
- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令
|
||||
- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可
|
||||
- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试
|
||||
|
||||
@@ -55,6 +55,8 @@ Flags:
|
||||
--chart-id string 浮动图表 ID (可选,不传则返回全部)
|
||||
```
|
||||
|
||||
- **判空看 `totalCount` / `message`,不要看数组长度**:无图表时 `floatCharts` 数组里仍会带 1 个全 null 的幽灵元素(`chart.category`/`chart.series` 全 null,无 id),但 `totalCount` 为 0、`message` 为「Successfully retrieved 0 float chart(s).」。判断有没有图表以 `totalCount` 为准,或过滤掉没有 `id` 的元素。
|
||||
|
||||
### 创建浮动图表
|
||||
```
|
||||
Usage:
|
||||
@@ -342,6 +344,7 @@ dws sheet chart update --node <NODE_ID> --sheet-id <SHEET_ID> --chart-id <CHART_
|
||||
|
||||
- [强制] **`--sheet-id` 获取规范**:`sheetId` 未知时必须先通过 `dws sheet list --node <NODE_ID> --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等)
|
||||
- [强制] **创建后必须验证**:图表创建后必须调用 `chart list` 验证配置是否正确
|
||||
- ⚠️ **`chart list` 判空看 `totalCount` 不看数组长度**:无图表时 `floatCharts` 仍含 1 个全 null 幽灵元素(无 id),`totalCount` 为 0、`message` 说明为空。判断有没有图表以 `totalCount` 为准或过滤无 `id` 的元素
|
||||
- [强制] **chart-id 禁止臆测**:必须通过 `chart list` 获取真实的图表 ID,不可编造
|
||||
- **图表类型映射**:用户说"柱形图"用 `column`,"条形图"(横向)用 `bar`,"散点图"用 `scatter`,"雷达图"用 `radar`,"环形图"用 `doughnut`
|
||||
- **堆积类型后缀**:堆叠版本的图表在基本类型后加 `Stacked`(如 `columnStacked`、`lineStacked`、`areaStacked`)
|
||||
|
||||
@@ -65,8 +65,9 @@
|
||||
**正确做法(两步走)**:
|
||||
```bash
|
||||
# Step 1: 用 range update 在新列写判断公式(形成"是/否"辅助列)
|
||||
# 每个单元格必须是 object,公式写在 text(以 = 开头);不支持裸字符串
|
||||
dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "H2:H100" \
|
||||
--values '[["=IF(A2>B2,\"是\",\"否\")"],...]'
|
||||
--values '[[{"type":"text","text":"=IF(A2>B2,\"是\",\"否\")"}],...]'
|
||||
|
||||
# Step 2: 基于辅助列值做条件格式(用 formulaCondition 引用辅助列)
|
||||
dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID \
|
||||
@@ -142,7 +143,8 @@ Flags:
|
||||
|
||||
- **用途**:查看指定工作表中已有的条件格式规则,或获取单个规则的详情。
|
||||
- **场景**:创建/更新/删除条件格式前后验证规则状态;获取 ruleId 供后续 update/delete 使用。
|
||||
- **返回**:rules 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。
|
||||
- **返回**:`rules` 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。
|
||||
- **判空要看 `message`,不要看数组长度**:无规则时 `rules` 数组里仍会带 1 个全 null 的幽灵元素(`colorScaleCondition`/`dataBarStyle`/`iconSetCondition` 全 null,无 id),`message` 会是「No conditional formatting rules found.」。该命令**没有 `totalCount` 字段**,判空以 message 为准,或过滤掉没有 `id` 的元素。
|
||||
|
||||
### 创建条件格式规则
|
||||
```
|
||||
@@ -269,3 +271,4 @@ Flags:
|
||||
- **辅助列+条件格式两步走**:用户明确要求"辅助列"时,必须按两步走(先建辅助列 → 再基于辅助列做条件格式),禁止直接用 `formulaCondition` 一步绕过
|
||||
- **大数据量优势**:当数据量 > 1000 行时,条件格式是首选——它由服务端自身渲染,不需要逐行调用 `range set-style`,性能远优于静态样式写入
|
||||
- **判断标准**:交付后 `cond-format list` 必须能返回该规则;否则视为违规
|
||||
- ⚠️ **`cond-format list` 判空看 `message` 不看数组长度**:无规则时 `rules` 仍含 1 个全 null 幽灵元素(无 id),`message` 为「No conditional formatting rules found.」。该命令无 `totalCount`,判空以 message 为准或过滤无 `id` 的元素
|
||||
|
||||
@@ -55,10 +55,11 @@ Flags:
|
||||
--range string 查询范围,A1 表示法,如 A1:A100 (必填)
|
||||
```
|
||||
|
||||
查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。
|
||||
查询指定范围内的下拉列表配置信息,包括选项值和颜色。
|
||||
- **用途**:查看单元格已设置的下拉列表选项和配置。
|
||||
- **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。
|
||||
- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。
|
||||
- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `multipleValues` 和 `colorValueMap`)。**判空以 `hasDropdown` 为准**:范围内无下拉列表时 `hasDropdown` 为 false,但 `dataValidations` 里仍会带 1 个全 null 的幽灵条目,不要用数组长度判空。
|
||||
- ⚠️ **已知限制**:`options.multipleValues` 服务端恒返回 `null`(即使该下拉是用 `--multi-select` 建的),也不返回 `enableMultiSelect` 字段——**`get-dropdown` 读回无法判断该下拉是否多选**。要判断是否多选,改用 `range read`:它返回的 `cells[].dataValidation.enableMultiSelect` 字段是准确的(实测有效)。
|
||||
|
||||
### 删除下拉列表
|
||||
```
|
||||
@@ -90,5 +91,6 @@ Flags:
|
||||
|
||||
- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node <NODE_ID> --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等)
|
||||
- `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖
|
||||
- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false
|
||||
- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。判空以 `hasDropdown` 为准(无下拉时为 false,但 `dataValidations` 仍含 1 个全 null 幽灵条目)
|
||||
- ⚠️ `get-dropdown` 的 `options.multipleValues` 恒为 `null`、无 `enableMultiSelect` 字段,读回无法判断是否多选;要判断是否多选改用 `range read`(其 `dataValidation.enableMultiSelect` 准确)
|
||||
- `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功
|
||||
|
||||
@@ -47,16 +47,16 @@ Flags:
|
||||
- 第 21~30 次:每次间隔 15 秒
|
||||
- **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误
|
||||
|
||||
**命令返回**:
|
||||
- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载)
|
||||
- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: <path>`
|
||||
- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下
|
||||
**命令返回**(`--format json`,默认):输出规整 JSON `{success, jobId, downloadUrl[, outputPath]}`。轮询进度以 `[INFO] [N/30] 状态: ...` 打到 stderr,不污染 stdout 的 JSON(可直接 `python3 -m json.tool` 解析 stdout)。
|
||||
- `--output` 未指定:JSON 含 `jobId` + `downloadUrl`(链接有时效性,请尽快下载)
|
||||
- `--output` 指定为文件路径:下载到该路径,JSON 额外含 `outputPath`
|
||||
- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名保存,JSON 含 `outputPath`
|
||||
|
||||
**失败处理(命令内部已处理,Agent 仅需转述)**:
|
||||
- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试
|
||||
- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试
|
||||
|
||||
**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
|
||||
**限制**:仅支持钉钉在线电子表格(axls)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
|
||||
|
||||
## 核心工作流
|
||||
|
||||
@@ -82,11 +82,11 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `export` | `downloadUrl`(未指定 --output)/ `导出完成: <path>`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 |
|
||||
| `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
|
||||
- ★ `export` 仅支持钉钉在线电子表格(axls)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal`
|
||||
- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令
|
||||
- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可
|
||||
- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试
|
||||
|
||||
@@ -32,7 +32,7 @@ Flags:
|
||||
- **用途**:查看当前工作表上是否存在全局筛选及其配置。
|
||||
- **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。
|
||||
- **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。
|
||||
- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。
|
||||
- **返回**:`range`(筛选范围,A1 表示法)、`id`(筛选 ID)和 `criteria`(各列条件对象,key 为列偏移量;无条件时为 `{}`)。如果未设置筛选,返回筛选信息为空。
|
||||
|
||||
### 创建筛选
|
||||
```
|
||||
@@ -152,7 +152,7 @@ Flags:
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `filter get` | `range`(筛选范围)、`columnFilterCriteria`(各列条件) | 查看当前筛选配置,确认筛选是否存在 |
|
||||
| `filter get` | `range`(筛选范围)、`id`、`criteria`(各列条件对象,无条件为 `{}`) | 查看当前筛选配置,确认筛选是否存在 |
|
||||
| `filter create` | 筛选创建成功的确认 | 确认筛选已建立,后续可通过 `filter update` 设置条件 |
|
||||
| `filter delete` | 删除成功的确认 | 确认筛选已删除 |
|
||||
| `filter update` | 更新成功的确认 | 确认条件已设置 |
|
||||
@@ -164,7 +164,7 @@ Flags:
|
||||
|
||||
- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node <NODE_ID> --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等)
|
||||
- ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列
|
||||
- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空
|
||||
- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)、`id` 和 `criteria`(各列条件对象,无条件时为 `{}`)。无筛选时返回空
|
||||
- `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错
|
||||
- `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件
|
||||
- `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复
|
||||
|
||||
@@ -48,6 +48,8 @@ Flags:
|
||||
--mime-type string 文件 MIME 类型 (默认根据扩展名推断)
|
||||
```
|
||||
|
||||
`--format json` 输出规整 JSON:`{success, resourceId, resourceUrl, fileName, fileSize}`。`resourceUrl` 可用于 `create-float-image` 的 `--src`。
|
||||
|
||||
### 上传图片并写入表格单元格
|
||||
```
|
||||
Usage:
|
||||
@@ -204,8 +206,8 @@ dws sheet write-image --node <NODE_ID> --sheet-id <SHEET_ID> --range C3:C3 --fil
|
||||
# 4. 完整流程: 创建表格 → 写表头 → 写入图片
|
||||
dws sheet create --name "产品目录" -f json
|
||||
# 提取 nodeId 后:
|
||||
dws sheet range update --node <NODE_ID> --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json
|
||||
dws sheet range update --node <NODE_ID> --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json
|
||||
dws sheet range update --node <NODE_ID> --sheet-id Sheet1 --range "A1:B1" --values '[[{"type":"text","text":"产品名称"},{"type":"text","text":"产品图片"}]]' -f json
|
||||
dws sheet range update --node <NODE_ID> --sheet-id Sheet1 --range "A2:A2" --values '[[{"type":"text","text":"MacBook Pro"}]]' -f json
|
||||
dws sheet write-image --node <NODE_ID> --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json
|
||||
```
|
||||
|
||||
|
||||
@@ -82,9 +82,11 @@ Flags:
|
||||
--sheet-id string 工作表 ID 或名称 (必填)
|
||||
--source-range string 源数据范围,A1 表示法 (必填)
|
||||
--target-range string 目标填充范围,A1 表示法 (必填)
|
||||
--fill-type string 填充类型: series(序列,默认) / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值)
|
||||
--fill-type string 填充类型: 不传则自动检测 / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值)
|
||||
```
|
||||
|
||||
`--fill-type` **不传时自动检测**(根据源数据智能判断:数值序列递增、日期递增、文本复制等),这是填序列的默认行为,无需显式传值。`--help` 枚举的可选值只有 `copy` / `onlystyle` / `withoutstyle`;服务端另外也接受 `series`(强制按序列填充),但它不在 `--help` 列表里,一般用不到——要序列递增直接不传 `--fill-type` 即可。
|
||||
|
||||
目标范围须与源范围在行或列维度对齐(不支持对角填充)。
|
||||
|
||||
### 复制区域
|
||||
|
||||
@@ -219,9 +219,9 @@ Flags:
|
||||
```bash
|
||||
# ── 工作流 4: 写入数据并设置样式 ──
|
||||
|
||||
# 1. 写入数据
|
||||
# 1. 写入数据(每个单元格必须是 object;数字也写成字符串)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:C3" \
|
||||
--values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json
|
||||
--values '[[{"type":"text","text":"商品"},{"type":"text","text":"单价"},{"type":"text","text":"数量"}],[{"type":"text","text":"苹果"},{"type":"text","text":"5.5"},{"type":"text","text":"100"}],[{"type":"text","text":"香蕉"},{"type":"text","text":"3.2"},{"type":"text","text":"200"}]]' --format json
|
||||
|
||||
# 2. 设置数字格式(人民币)
|
||||
dws sheet range set-style --node <NODE_ID> --sheet-id <SHEET_ID> --range "B2:B3" \
|
||||
|
||||
@@ -108,14 +108,16 @@ Flags:
|
||||
--node string 表格文档 ID 或 URL (必填)
|
||||
--sheet-id string 工作表 ID 或名称 (必填)
|
||||
--name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] :
|
||||
--title string --name 的别名(兼容)
|
||||
--index int 新位置(从 0 开始)
|
||||
--hidden --hidden=true 隐藏,--hidden=false 取消隐藏
|
||||
--tab-color string 工作表标签颜色,Hex 如 #FF0000;传空字符串清除颜色
|
||||
--frozen-row-count int 冻结行数,0 表示取消冻结
|
||||
--frozen-column-count int 冻结列数,0 表示取消冻结
|
||||
```
|
||||
|
||||
更新工作表名称、位置、隐藏状态、冻结行列。
|
||||
`--name` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。
|
||||
更新工作表名称、位置、隐藏状态、标签颜色、冻结行列。
|
||||
`--name`(别名 `--title`)/ `--index` / `--hidden` / `--tab-color` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。
|
||||
|
||||
注意:
|
||||
- 至少需要保留一个可见的工作表,不能将所有工作表都隐藏
|
||||
@@ -174,12 +176,12 @@ dws sheet create --name "销售数据" --format json
|
||||
# 2. 查看工作表列表 — 提取 sheetId
|
||||
dws sheet list --node <NODE_ID> --format json
|
||||
|
||||
# 3. 写入表头和数据
|
||||
# 3. 写入表头和数据(每个单元格必须是 object;数字也写成字符串)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:C1" \
|
||||
--values '[["姓名","部门","销售额"]]' --format json
|
||||
--values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json
|
||||
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range "A2:C4" \
|
||||
--values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json
|
||||
--values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json
|
||||
|
||||
# ── 工作流 2: 读取已有表格数据 ──
|
||||
|
||||
@@ -200,12 +202,12 @@ dws sheet range read --node <NODE_ID> --sheet-id <SHEET_ID> --range "A1:D10" --f
|
||||
# 1. 新建工作表
|
||||
dws sheet new --node <NODE_ID> --name "汇总" --format json
|
||||
|
||||
# 2. 在新工作表中写入汇总公式
|
||||
# 2. 在新工作表中写入汇总公式(公式写在 text,以 = 开头)
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <NEW_SHEET_ID> --range "A1:B1" \
|
||||
--values '[["指标","数值"]]' --format json
|
||||
--values '[[{"type":"text","text":"指标"},{"type":"text","text":"数值"}]]' --format json
|
||||
|
||||
dws sheet range update --node <NODE_ID> --sheet-id <NEW_SHEET_ID> --range "A2:B2" \
|
||||
--values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json
|
||||
--values '[[{"type":"text","text":"总销售额"},{"type":"text","text":"=SUM(Sheet1!C2:C100)"}]]' --format json
|
||||
```
|
||||
|
||||
## 上下文传递
|
||||
|
||||
@@ -100,9 +100,11 @@ Flags:
|
||||
Usage:
|
||||
dws sheet csv-put [flags]
|
||||
Example:
|
||||
# 内联多行 CSV:必须用 $'...' 让 \n 变成真换行;普通单引号 '...\n...' 里的 \n 是字面量,会写成一行含字面 \n 的错误数据
|
||||
dws sheet csv-put --node <NODE_ID> --sheet-id <SHEET_ID> --start-cell A1 \
|
||||
--csv 'name,score\nAlice,95\nBob,87'
|
||||
--csv $'name,score\nAlice,95\nBob,87'
|
||||
|
||||
# 多行数据推荐用 @文件,最稳妥
|
||||
dws sheet csv-put --node <NODE_ID> --sheet-id <SHEET_ID> --start-cell B2 \
|
||||
--csv @data.csv --allow-overwrite
|
||||
|
||||
|
||||
@@ -188,11 +188,12 @@ Flags:
|
||||
--task-id string 待办任务 ID (必填)
|
||||
```
|
||||
|
||||
返回结构:子待办在**顶层** `subTasks[]` 数组里(不在 `result` 下),每个元素含 `taskId`(子待办 ID)、`subject`、`priority` 等字段。取子待办的 `taskId` 用于后续操作。
|
||||
注意:`task get` 返回的 `result.todoDetailModel.subTodos[]` 里**没有 taskId 字段**,要拿子待办 ID 必须用本命令 `task list-sub`。
|
||||
|
||||
### 上传待办附件
|
||||
|
||||
> ⚠️ 当前不可用:后端未注册该工具,调用报「未找到指定工具」。遇到此需求直接告知用户暂不支持,勿重试或变通。
|
||||
|
||||
> ⚠️ 重要:该接口会上传文件到附件,不可用于测试或试探性调用。调用前必须确认待办存在。
|
||||
> ⚠️ 重要:该接口会真实上传文件到附件,不可用于测试或试探性调用。调用前必须确认待办存在。
|
||||
|
||||
```
|
||||
Usage:
|
||||
@@ -204,9 +205,9 @@ Flags:
|
||||
--task-id string 待办任务 ID (必填)
|
||||
```
|
||||
|
||||
### 查询待办附件列表
|
||||
返回 `result.attachmentIds`(数组,如 `["6a4cffb79e2b520ed3600960"]`),即新上传附件的 attachmentId。
|
||||
|
||||
> ⚠️ 当前不可用:后端未注册该工具,调用报「未找到指定工具」。遇到此需求直接告知用户暂不支持,勿重试或变通。
|
||||
### 查询待办附件列表
|
||||
|
||||
```
|
||||
Usage:
|
||||
@@ -217,9 +218,9 @@ Flags:
|
||||
--task-id string 待办任务 ID (必填)
|
||||
```
|
||||
|
||||
### 删除待办附件
|
||||
返回 `attachments[]`(顶层数组),每个元素含 `attachmentId`、`fileName`、`fileSize`;无附件时返回空数组。
|
||||
|
||||
> ⚠️ 当前不可用:后端未注册该工具,调用报「未找到指定工具」。遇到此需求直接告知用户暂不支持,勿重试或变通。
|
||||
### 删除待办附件
|
||||
|
||||
> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。
|
||||
|
||||
@@ -233,7 +234,7 @@ Flags:
|
||||
--attachment-id string 待办附件 ID (必填)
|
||||
--task-id string 待办任务 ID (必填)
|
||||
```
|
||||
附件 attachmentId 使用 `dws todo task list-attachment` 命令获取。
|
||||
附件 attachmentId 使用 `dws todo task list-attachment` 命令获取。删除后可再 `list-attachment` 复查,返回空数组即删除成功。
|
||||
|
||||
### 添加待办提醒
|
||||
```
|
||||
@@ -313,7 +314,7 @@ JSON 数组,每个元素为一条提醒规则,支持两种 `baseTime` 模式
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
# 1. 创建待办 — 提取 todoTaskId
|
||||
# 1. 创建待办 — 从返回 result.taskId 提取任务 ID
|
||||
dws todo task create --title "修复线上Bug" --executors userId1,userId2 \
|
||||
--priority 40 --due "2026-03-10T18:00:00+08:00" --format json
|
||||
|
||||
@@ -367,18 +368,28 @@ dws todo task add-reminder --task-id <taskId> --base-time customTime --reminder-
|
||||
dws todo task reset-reminder --task-id <taskId> --format json
|
||||
# 17. 重置待办提醒(指定新规则)
|
||||
dws todo task reset-reminder --task-id <taskId> --reminder-rules '<reminderRules>' --format json
|
||||
|
||||
# 18. 上传附件(真实上传,先确认待办存在)— 从返回 result.attachmentIds 取 attachmentId
|
||||
dws todo task add-attachment --task-id <taskId> --file-path /path/to/file.pdf --format json
|
||||
# 19. 查询附件列表 — 从返回 attachments[].attachmentId 取 ID
|
||||
dws todo task list-attachment --task-id <taskId> --format json
|
||||
# 20. 删除附件(用户确认后加 --yes;删完可 list-attachment 复查为空)
|
||||
dws todo task remove-attachment --task-id <taskId> --attachment-id <attachmentId> --yes --format json
|
||||
|
||||
# 21. 查询子待办 — 从顶层 subTasks[].taskId 取子待办 ID
|
||||
dws todo task list-sub --task-id <taskId> --format json
|
||||
```
|
||||
|
||||
## 上下文传递表
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|---------------------------------------------|
|
||||
| `task create` | `todoTaskId` | update/done/get/delete 的 --task-id |
|
||||
| `task list` | `result[].id` | update/done/get/delete 的 --task-id |
|
||||
| `task create` | `todoTaskId` | update/done/get/delete/comment 的 --task-id |
|
||||
| `task list` | `result[].id` | update/done/get/delete/comment/add-executor/remove-executor/add-participant/remove-participant 的 --task-id |
|
||||
| `task get` | `result.todoDetailModel.subTodos[]` | 获取子待办列表,提取子待办的 `taskId` 用于后续操作 |
|
||||
| `comment list` | `result[].commentId` | `comment delete` 的 --comment-id |
|
||||
| `task create` / `task create-sub` | `result.taskId` | update/done/get/delete/comment 的 --task-id |
|
||||
| `task list` | `result.todoCards[].taskId` | update/done/get/delete/comment/add-executor/remove-executor/add-participant/remove-participant 的 --task-id |
|
||||
| `task list-sub` | `subTasks[].taskId`(顶层数组,不在 `result` 下) | 子待办的后续操作 --task-id |
|
||||
| `add-attachment` | `result.attachmentIds[]` | 新上传附件的 attachmentId |
|
||||
| `list-attachment` | `attachments[].attachmentId`(顶层数组) | `remove-attachment` 的 --attachment-id |
|
||||
| `comment list` | `result.comments[].id` | `comment delete` 的 --comment-id |
|
||||
|
||||
## 注意事项
|
||||
|
||||
@@ -398,6 +409,9 @@ dws todo task reset-reminder --task-id <taskId> --reminder-rules '<reminderRules
|
||||
- 执行人 (executor) 与参与人 (participant) 的区别:执行人负责完成待办,参与人仅关注待办进度
|
||||
- `task add-reminder` 用于为待办添加提醒,`--base-time` 支持 `dueTime`(基于截止时间偏移,待办必须有截止时间)和 `customTime`(自定义时间戳)两种模式
|
||||
- `task reset-reminder` 用于重置待办提醒规则,不传 `--reminder-rules` 则清除所有提醒
|
||||
- `task add-attachment` / `list-attachment` / `remove-attachment` 三条附件命令均可用;`add-attachment` 会真实上传文件,勿用于试探性调用,先确认待办存在
|
||||
- 附件 ID 的取法:`add-attachment` 从 `result.attachmentIds[]` 取,`list-attachment` 从顶层 `attachments[].attachmentId` 取;`remove-attachment` 用 `--attachment-id` + `--yes`
|
||||
- 子待办 ID 只能从 `task list-sub` 的顶层 `subTasks[].taskId` 取;`task get` 的 `result.todoDetailModel.subTodos[]` 没有 taskId 字段
|
||||
|
||||
|
||||
## 自动化脚本
|
||||
|
||||
@@ -179,6 +179,8 @@ Flags:
|
||||
|
||||
> 接口不支持游标分页,使用 `--limit` 一次性拉取。
|
||||
|
||||
> ⚠️ **返回字段限制**:`member list` 每条只返回 `name` / `role` / `type` 三个字段,**不含 userId**(服务端不返回)。因此**无法**从 `member list` 拿到 userId 再去串联 `member update` / `member remove`。要对某人改角色 / 移除,需另行拿到其 userId(例如用 `dws contact user search --query "<姓名>"` 按姓名反查)。
|
||||
|
||||
### 列出知识库节点
|
||||
```
|
||||
Usage:
|
||||
@@ -393,10 +395,13 @@ dws wiki member list --workspace <WS_ID> --format json
|
||||
|
||||
# ── 工作流: 移除知识库成员 ──
|
||||
|
||||
# 1. 查看当前成员
|
||||
# 1. 查看当前成员(只返回 name/role/type,拿不到 userId)
|
||||
dws wiki member list --workspace <WS_ID> --format json
|
||||
|
||||
# 2. 移除成员
|
||||
# 2. 另行按姓名反查目标成员的 userId(member list 不返回 userId)
|
||||
dws contact user search --query "<姓名>" --format json
|
||||
|
||||
# 3. 移除成员
|
||||
dws wiki member remove --workspace <WS_ID> --users <UID> --format json
|
||||
|
||||
# ── 工作流: 删除知识库 ──
|
||||
@@ -419,7 +424,7 @@ dws wiki space delete --workspace <workspaceId> --format json
|
||||
| `node list` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
|
||||
| `node search` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
|
||||
| `node create` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
|
||||
| `member list` | `userId` | member update 的 --users / member remove 的 --users |
|
||||
| `member list` | `name` / `role` / `type`(**不含 userId**)| 仅用于查看成员名单;**无法**从这里取 userId 去串联 member update/remove,需另行按姓名反查 userId(如 `dws contact user search --query "<姓名>"`)|
|
||||
|
||||
## 相关产品
|
||||
|
||||
|
||||
@@ -90,14 +90,14 @@ Step 3 → 按下方路由规则映射到对应产品
|
||||
| `contentType=ALIDOC`, `extension=adoc` | `doc` | 按 [doc.md](./products/doc.md) 操作 |
|
||||
| `contentType=ALIDOC`, `extension=axls` | `sheet` | 按 [sheet.md](./products/sheet.md) 操作(仅 `axls` 在线电子表格) |
|
||||
| `contentType=ALIDOC`, `extension=able` | `aitable` | 将 nodeId 作为 baseId,按 [aitable.md](./products/aitable.md) 操作 |
|
||||
| `contentType=DOCUMENT`, `extension=xlsx` / `xls` / `xlsm` / `csv` | `doc` | 必须用 `dws doc download` 下载到本地处理,禁止走 `sheet`(非在线表格,sheet 命令无法操作) |
|
||||
| `contentType≠ALIDOC`, `nodeType=file` | `doc` | 调用 `dws doc download` 下载,返回文件下载链接 |
|
||||
| `contentType=DOCUMENT`, `extension=xlsx` / `xls` / `xlsm` / `csv` | `doc` | 必须用 `dws drive download` 下载到本地处理,禁止走 `sheet`(非在线表格,sheet 命令无法操作) |
|
||||
| `contentType≠ALIDOC`, `nodeType=file` | `doc` | 调用 `dws drive download` 下载,返回文件下载链接 |
|
||||
| `nodeType=folder` | `doc` | 调用 `dws doc list --folder <ID>` 列出指定文件夹直接子节点列表 |
|
||||
| 以上均不匹配 | — | 告知用户当前暂不支持该类型 |
|
||||
|
||||
> axls vs xlsx 关键区分:
|
||||
> - `axls`(钉钉在线电子表格,`contentType=ALIDOC`)→ 走 `sheet` 产品线(读/写/筛选/导出等服务端原子操作)
|
||||
> - `xlsx` / `xls` / `xlsm` / `csv`(上传到文档空间的本地表格文件,`contentType=DOCUMENT`)→ 必须走 `dws doc download` 下载到本地后再解析处理,严禁错误路由到 `sheet` 产品线(sheet 命令只支持在线表格,调用 xlsx 节点会直接报错)
|
||||
> - `xlsx` / `xls` / `xlsm` / `csv`(上传到文档空间的本地表格文件,`contentType=DOCUMENT`)→ 必须走 `dws drive download` 下载到本地后再解析处理,严禁错误路由到 `sheet` 产品线(sheet 命令只支持在线表格,调用 xlsx 节点会直接报错)
|
||||
> - 用户想把在线表格导出为 xlsx 文件 → 开源 dws CLI 暂未暴露在线表格导出能力(旧动态 schema 曾包含 `submit_export_job` / `query_export_job`,但当前 cobra 未注册),需要在钉钉客户端手动导出 xlsx
|
||||
|
||||
### 示例
|
||||
@@ -110,10 +110,10 @@ dws doc info --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
|
||||
dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
|
||||
|
||||
# 返回 contentType≠ALIDOC, extension=xlsx/xls/csv → 本地表格文件,必须下载处理(禁止走 sheet)
|
||||
dws doc download --node "https://alidocs.dingtalk.com/i/nodes/xlsx456"
|
||||
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/xlsx456"
|
||||
|
||||
# 返回 contentType≠ALIDOC, nodeType=file → 普通文件,下载
|
||||
dws doc download --node "https://alidocs.dingtalk.com/i/nodes/def456"
|
||||
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/def456"
|
||||
|
||||
# 返回 nodeType=folder → 文件夹,列出子节点
|
||||
dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/ghi789" --format json
|
||||
@@ -136,10 +136,10 @@ dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/ghi789" --format jso
|
||||
| extension / contentType | 读取 | 写入 | 删除 | 导出 | 权限 | 媒体 |
|
||||
|-------------------------|------|------|------|------|------|------|
|
||||
| **adoc**(在线文档) | `doc read` | `doc update` / `doc block update` | ⚠️ `doc delete` | ⚠️ `doc export` (→ docx) | ⚠️ `doc permission *` | ⚠️ `doc media download/insert` |
|
||||
| **axls**(在线电子表格) | `sheet range read` / `sheet list` | `sheet range write` / `sheet append` | ⚠️ `doc delete`(节点删除) | `sheet submit_export_job` + `sheet query_export_job`(待吴淼 W-01 收敛为单命令 `sheet export`) | ⚠️ `doc permission *`(节点级,跨产品) | 不适用 |
|
||||
| **able**(在线多维表) | `aitable base get` / `aitable record query` | `aitable record create/update` | ⚠️ `doc delete`(节点删除)或 `aitable base delete --yes` | `aitable export data --output ./x.xlsx` | ⚠️ `doc permission *`(节点级) | `aitable attachment upload-file` |
|
||||
| **xlsx / xls / xlsm / csv**(本地表格文件) | `doc download` → 本地用 xlsx skill 解析 | 不支持服务端写(先下载改本地再上传) | ⚠️ `doc delete`(节点删除) | 不需要(本身就是 xlsx) | ⚠️ `doc permission *` | 不适用 |
|
||||
| **普通文件** (nodeType=file) | `doc download` | 不支持服务端写 | ⚠️ `doc delete` | 不需要 | ⚠️ `doc permission *` | 不适用 |
|
||||
| **axls**(在线电子表格) | `sheet range read` / `sheet list` | `sheet range update` / `sheet append` | ⚠️ `doc delete`(节点删除) | `sheet export`(单命令一站式:提交→轮询→下载,可选 `--output` 落盘) | ⚠️ `doc permission *`(节点级,跨产品) | 不适用 |
|
||||
| **able**(在线多维表) | `aitable base get` / `aitable record query` | `aitable record create/update` | ⚠️ `doc delete`(节点删除)或 `aitable base delete --yes` | `aitable export data --scope all --format excel`(取 downloadUrl,`--output` 不落盘) | ⚠️ `doc permission *`(节点级) | `aitable attachment upload` |
|
||||
| **xlsx / xls / xlsm / csv**(本地表格文件) | `drive download` → 本地用 xlsx skill 解析 | 不支持服务端写(先下载改本地再上传) | ⚠️ `doc delete`(节点删除) | 不需要(本身就是 xlsx) | ⚠️ `doc permission *` | 不适用 |
|
||||
| **普通文件** (nodeType=file) | `drive download` | 不支持服务端写 | ⚠️ `doc delete` | 不需要 | ⚠️ `doc permission *` | 不适用 |
|
||||
| **文件夹** (nodeType=folder) | `doc list --folder <URL>` | `doc create --folder <URL> ...` | ⚠️ `doc delete` | 不适用 | ⚠️ `doc permission *` | 不适用 |
|
||||
| **分享短链** `/i/p/<short>` | `read_url` 兜底(外部工具) | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
|
||||
|
||||
|
||||
@@ -88,10 +88,12 @@ def build_start_args(args: argparse.Namespace) -> list[str]:
|
||||
args.base_id,
|
||||
"--scope",
|
||||
args.scope,
|
||||
"--export-format",
|
||||
"--format",
|
||||
args.export_format,
|
||||
"--timeout-sec",
|
||||
str(args.timeout_sec),
|
||||
# CLI 的 --timeout-ms 是单次等待上限(毫秒,最大 30000);脚本自身的
|
||||
# --timeout-sec 用于整体轮询/子进程超时,二者语义不同,不能混用。
|
||||
"--timeout-ms",
|
||||
"30000",
|
||||
]
|
||||
if args.table_id:
|
||||
cmd.extend(["--table-id", args.table_id])
|
||||
@@ -152,8 +154,8 @@ def main() -> None:
|
||||
args.base_id,
|
||||
"--task-id",
|
||||
task_id,
|
||||
"--timeout-sec",
|
||||
str(args.timeout_sec),
|
||||
"--timeout-ms",
|
||||
"30000",
|
||||
],
|
||||
timeout_sec=max(120, args.timeout_sec + 60),
|
||||
)
|
||||
|
||||
@@ -140,8 +140,10 @@ def main() -> None:
|
||||
"data",
|
||||
"--import-id",
|
||||
import_id,
|
||||
"--timeout-sec",
|
||||
str(args.timeout_sec),
|
||||
# import data 的 --timeout 单位是秒、最大 30;脚本的 --timeout-sec
|
||||
# 是整体子进程预算,不能直接透传,这里用 CLI 允许的最大值。
|
||||
"--timeout",
|
||||
"30",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
|
||||
@@ -29,7 +29,7 @@ JsonData = Union[List[Any], Dict[str, Any]]
|
||||
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024
|
||||
ALLOWED_FILE_EXTENSIONS = ['.json']
|
||||
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
|
||||
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{6,128}$')
|
||||
ALLOWED_FIELD_TYPES = {
|
||||
'text', 'number', 'singleSelect', 'multipleSelect', 'date', 'currency',
|
||||
'user', 'department', 'group', 'progress', 'rating', 'checkbox',
|
||||
|
||||
@@ -100,7 +100,11 @@ def main():
|
||||
|
||||
event_id = None
|
||||
if not args.dry_run and isinstance(result, dict):
|
||||
event_id = result.get('eventId') or result.get('id')
|
||||
# event create 返回 {result:{id,...}, success:true},id 在 result 内层,
|
||||
# 顶层取 id 恒 None 会导致参会人/订房支路被静默跳过。先解包 result 再取。
|
||||
inner = result.get('result') if isinstance(result.get('result'), dict) else result
|
||||
event_id = (inner.get('eventId') or inner.get('id')
|
||||
or result.get('eventId') or result.get('id'))
|
||||
print(f" ✓ 日程已创建" +
|
||||
(f" (eventId: {event_id})" if event_id else ""))
|
||||
|
||||
|
||||
@@ -8,12 +8,20 @@
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from typing import List, Any, Optional
|
||||
|
||||
|
||||
def strip_highlight(text: str) -> str:
|
||||
"""去除 dept search 返回名称中的 <red>…</red> 高亮标签。"""
|
||||
if not isinstance(text, str):
|
||||
return text
|
||||
return re.sub(r'</?red>', '', text)
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
@@ -62,15 +70,26 @@ def main():
|
||||
print('未找到匹配部门')
|
||||
sys.exit(1)
|
||||
|
||||
depts = (dept_data if isinstance(dept_data, list)
|
||||
else dept_data.get('result', dept_data.get('items', [])))
|
||||
# dept search 返回顶层 deptList;兼容 result 包裹与历史 items/result 键。
|
||||
if isinstance(dept_data, list):
|
||||
depts = dept_data
|
||||
else:
|
||||
inner = dept_data.get('result', dept_data) if isinstance(dept_data, dict) else {}
|
||||
if not isinstance(inner, dict):
|
||||
inner = dept_data if isinstance(dept_data, dict) else {}
|
||||
depts = (inner.get('deptList')
|
||||
or dept_data.get('deptList')
|
||||
or dept_data.get('items')
|
||||
or [])
|
||||
if not depts:
|
||||
print('未找到匹配部门')
|
||||
sys.exit(1)
|
||||
|
||||
for dept in depts:
|
||||
dept_id = dept.get('id') or dept.get('deptId')
|
||||
dept_name = dept.get('name') or dept.get('deptName', '未知')
|
||||
dept_name = strip_highlight(
|
||||
dept.get('name') or dept.get('deptName', '未知')
|
||||
)
|
||||
if not dept_id:
|
||||
continue
|
||||
|
||||
@@ -85,17 +104,29 @@ def main():
|
||||
print(' 无法获取成员列表')
|
||||
continue
|
||||
|
||||
members = (members_data if isinstance(members_data, list)
|
||||
else members_data.get('result',
|
||||
members_data.get('userlist', [])))
|
||||
# list-members 返回 deptUserList;兼容 result 包裹与历史 userlist 键。
|
||||
if isinstance(members_data, list):
|
||||
members = members_data
|
||||
else:
|
||||
m_inner = (members_data.get('result', members_data)
|
||||
if isinstance(members_data, dict) else {})
|
||||
if not isinstance(m_inner, dict):
|
||||
m_inner = members_data if isinstance(members_data, dict) else {}
|
||||
members = (m_inner.get('deptUserList')
|
||||
or members_data.get('deptUserList')
|
||||
or members_data.get('userlist')
|
||||
or [])
|
||||
if not members:
|
||||
print(' (暂无成员)')
|
||||
continue
|
||||
|
||||
for m in members:
|
||||
name = m.get('name') or m.get('userName', '未知')
|
||||
title = m.get('title') or m.get('position', '')
|
||||
uid = m.get('userId') or m.get('userid', '')
|
||||
# list-members 每项形如 {"userInfo": {"name":..., "userId":...}},
|
||||
# 成员字段嵌在 userInfo 下;兼容历史扁平结构。
|
||||
info = m.get('userInfo', m)
|
||||
name = info.get('name') or info.get('userName', '未知')
|
||||
title = info.get('title') or info.get('position', '')
|
||||
uid = info.get('userId') or info.get('userid', '')
|
||||
line = f" 👤 {name}"
|
||||
if title:
|
||||
line += f" ({title})"
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
递归列出钉盘目录树结构(可指定深度)
|
||||
|
||||
用法:
|
||||
python drive_tree_list.py # 列出根目录
|
||||
python drive_tree_list.py --depth 2 # 递归 2 层
|
||||
python drive_tree_list.py --parent-id <id> # 指定目录
|
||||
python drive_tree_list.py # 列出根目录
|
||||
python drive_tree_list.py --depth 2 # 递归 2 层
|
||||
python drive_tree_list.py --folder <id> # 指定目录 (传 drive list 返回的 fileId)
|
||||
python drive_tree_list.py --dry-run
|
||||
|
||||
说明:
|
||||
`dws drive list` 返回的每个 item 有两个 ID:
|
||||
- dentryId:纯数字串,`--folder` 不接受,别用它递归;
|
||||
- fileId:字母数字串,即 CLI 所称 dentryUuid,`--folder` 只认它。
|
||||
递归子目录必须用 fileId 作为 `--folder` 的值。
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -38,13 +44,13 @@ def run_dws(
|
||||
|
||||
|
||||
def list_dir(
|
||||
parent_id: str = '', dry_run: bool = False,
|
||||
folder: str = '', dry_run: bool = False,
|
||||
) -> list:
|
||||
cmd_args = [
|
||||
'drive', 'list', '--max', '50', '--format', 'json',
|
||||
'drive', 'list', '--limit', '50', '--format', 'json',
|
||||
]
|
||||
if parent_id:
|
||||
cmd_args.extend(['--parent-id', parent_id])
|
||||
if folder:
|
||||
cmd_args.extend(['--folder', folder])
|
||||
data = run_dws(cmd_args, dry_run=dry_run)
|
||||
if not data:
|
||||
return []
|
||||
@@ -69,7 +75,7 @@ def print_tree(
|
||||
name = item.get('name') or item.get('fileName', '?')
|
||||
item_type = item.get('type') or item.get('dentryType', '')
|
||||
is_dir = str(item_type).lower() in (
|
||||
'folder', 'directory', '1', 'FOLDER'
|
||||
'folder', 'directory', '1',
|
||||
)
|
||||
icon = '📁' if is_dir else '📄'
|
||||
size_str = ''
|
||||
@@ -87,10 +93,10 @@ def print_tree(
|
||||
|
||||
if is_dir and depth < max_depth:
|
||||
child_prefix = prefix + (' ' if is_last else '│ ')
|
||||
dentry_id = (item.get('dentryUuid')
|
||||
or item.get('id', ''))
|
||||
if dentry_id:
|
||||
children = list_dir(dentry_id, dry_run=dry_run)
|
||||
# `--folder` 只认 fileId (dentryUuid),不认纯数字 dentryId
|
||||
folder_id = item.get('fileId', '')
|
||||
if folder_id:
|
||||
children = list_dir(folder_id, dry_run=dry_run)
|
||||
print_tree(
|
||||
children, depth + 1, max_depth,
|
||||
child_prefix, dry_run,
|
||||
@@ -102,7 +108,8 @@ def main():
|
||||
description='递归列出钉盘目录树'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--parent-id', default='', help='起始目录 ID'
|
||||
'--folder', default='',
|
||||
help='起始目录 ID (传 drive list 返回的 fileId)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--depth', type=int, default=1,
|
||||
@@ -112,10 +119,10 @@ def main():
|
||||
args = parser.parse_args()
|
||||
args.depth = min(args.depth, 5)
|
||||
|
||||
root_name = args.parent_id or '我的文件'
|
||||
root_name = args.folder or '我的文件'
|
||||
print(f"📁 {root_name}")
|
||||
|
||||
items = list_dir(args.parent_id, dry_run=args.dry_run)
|
||||
items = list_dir(args.folder, dry_run=args.dry_run)
|
||||
if args.dry_run:
|
||||
return
|
||||
if not items:
|
||||
|
||||
@@ -28,7 +28,7 @@ RecordDict = Dict[str, str]
|
||||
MAX_FILE_SIZE = 50 * 1024 * 1024
|
||||
ALLOWED_CSV_EXTENSIONS = ['.csv']
|
||||
ALLOWED_JSON_EXTENSIONS = ['.json']
|
||||
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
|
||||
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{6,128}$')
|
||||
MAX_RECORDS_PER_BATCH = 100
|
||||
DEFAULT_BATCH_SIZE = 50
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ def main():
|
||||
'mail', 'message', 'search',
|
||||
'--email', email or '<MY_EMAIL>',
|
||||
'--query', kql,
|
||||
'--size', str(args.size),
|
||||
'--limit', str(args.size),
|
||||
'--format', 'json',
|
||||
], dry_run=args.dry_run)
|
||||
|
||||
|
||||
@@ -44,6 +44,58 @@ def run_dws(
|
||||
return None
|
||||
|
||||
|
||||
def todos_from_payload(payload: Any) -> List[dict]:
|
||||
"""解析 `minutes get todos` 返回为待办列表。
|
||||
|
||||
返回结构: result.dingtalkTodoList(对象数组, 含 title 等)与
|
||||
result.actions(JSON 字符串数组, 每条形如
|
||||
{"mark":[],"value":"..."}). 二者无 todos 键。
|
||||
优先取 dingtalkTodoList; 为空时回退解析 actions。
|
||||
"""
|
||||
if isinstance(payload, dict):
|
||||
inner = payload.get('result', payload)
|
||||
else:
|
||||
inner = payload
|
||||
out: List[dict] = []
|
||||
if isinstance(inner, dict):
|
||||
ding_list = inner.get('dingtalkTodoList')
|
||||
if isinstance(ding_list, list) and ding_list:
|
||||
for t in ding_list:
|
||||
if isinstance(t, dict):
|
||||
content = t.get('title') or t.get('content') or ''
|
||||
if content:
|
||||
out.append({'content': str(content), '_raw': t})
|
||||
if out:
|
||||
return out
|
||||
actions = inner.get('actions')
|
||||
if isinstance(actions, list):
|
||||
for a in actions:
|
||||
content = ''
|
||||
if isinstance(a, str):
|
||||
text = a.strip()
|
||||
if text.startswith('{'):
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
content = parsed.get('value') or ''
|
||||
except json.JSONDecodeError:
|
||||
content = text
|
||||
else:
|
||||
content = text
|
||||
elif isinstance(a, dict):
|
||||
content = (a.get('value') or a.get('content')
|
||||
or a.get('title') or '')
|
||||
if content:
|
||||
out.append({'content': str(content)})
|
||||
elif isinstance(inner, list):
|
||||
for t in inner:
|
||||
if isinstance(t, dict):
|
||||
content = (t.get('content') or t.get('text')
|
||||
or t.get('title') or t.get('value') or '')
|
||||
if content:
|
||||
out.append({'content': str(content), '_raw': t})
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='从听记中提取待办事项'
|
||||
@@ -82,21 +134,9 @@ def main():
|
||||
])
|
||||
if not todos_data:
|
||||
continue
|
||||
if isinstance(todos_data, list):
|
||||
items = todos_data
|
||||
elif isinstance(todos_data, dict):
|
||||
inner = todos_data.get('result', todos_data)
|
||||
if isinstance(inner, dict):
|
||||
items = inner.get('todos', [])
|
||||
elif isinstance(inner, list):
|
||||
items = inner
|
||||
else:
|
||||
items = []
|
||||
else:
|
||||
items = []
|
||||
items = todos_from_payload(todos_data)
|
||||
for t in items:
|
||||
if isinstance(t, dict):
|
||||
t['_source'] = title
|
||||
t['_source'] = title
|
||||
all_todos.extend(items)
|
||||
|
||||
print(f"\n📋 听记待办汇总")
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
"""
|
||||
查看今天收到的日志列表及详情
|
||||
|
||||
基于 `dws report inbox list` + `dws report entry get`(旧的 report list / report detail
|
||||
已废弃)。inbox list 返回的 result[] 使用中文展示键:日期 / 标题 / 发送人 / 状态 / 钉钉链接;
|
||||
reportId 不在 result[] 里,只在 _internalDetailCommands[].command 中,按页与 result[] 同序对应。
|
||||
|
||||
用法:
|
||||
python report_inbox_today.py
|
||||
python report_inbox_today.py --days 3 # 最近 3 天
|
||||
python report_inbox_today.py --days 3 # 最近 3 天
|
||||
python report_inbox_today.py --detail # 额外拉取每条正文 (entry get)
|
||||
python report_inbox_today.py --dry-run
|
||||
"""
|
||||
|
||||
@@ -13,7 +18,7 @@ import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Any, Optional
|
||||
from typing import List, Any, Optional, Tuple
|
||||
|
||||
|
||||
def run_dws(
|
||||
@@ -37,77 +42,118 @@ def run_dws(
|
||||
return None
|
||||
|
||||
|
||||
def to_iso(dt: datetime) -> str:
|
||||
return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00')
|
||||
def iso_start(dt: datetime) -> str:
|
||||
return dt.strftime('%Y-%m-%dT00:00:00+08:00')
|
||||
|
||||
|
||||
def iso_end(dt: datetime) -> str:
|
||||
return dt.strftime('%Y-%m-%dT23:59:59+08:00')
|
||||
|
||||
|
||||
def report_id_from_command(cmd: str) -> str:
|
||||
parts = cmd.split()
|
||||
if '--report-id' in parts:
|
||||
i = parts.index('--report-id')
|
||||
if i + 1 < len(parts):
|
||||
return parts[i + 1]
|
||||
return ''
|
||||
|
||||
|
||||
def fetch_inbox(
|
||||
start: str, end: str, dry_run: bool,
|
||||
) -> List[Tuple[dict, str]]:
|
||||
"""按 cursor 翻页拉全 inbox;返回 (result_item, reportId) 列表。"""
|
||||
pairs: List[Tuple[dict, str]] = []
|
||||
cursor = 0
|
||||
while True:
|
||||
data = run_dws([
|
||||
'report', 'inbox', 'list',
|
||||
'--start', start,
|
||||
'--end', end,
|
||||
'--cursor', str(cursor),
|
||||
'--size', '20',
|
||||
'--format', 'json',
|
||||
], dry_run=dry_run)
|
||||
if dry_run or not isinstance(data, dict):
|
||||
return pairs
|
||||
items = data.get('result') or []
|
||||
cmds = data.get('_internalDetailCommands') or []
|
||||
for idx, item in enumerate(items):
|
||||
rid = ''
|
||||
if idx < len(cmds):
|
||||
rid = report_id_from_command(
|
||||
cmds[idx].get('command', '')
|
||||
)
|
||||
pairs.append((item, rid))
|
||||
if data.get('hasMore') and data.get('nextCursor') is not None:
|
||||
cursor = data['nextCursor']
|
||||
else:
|
||||
break
|
||||
return pairs
|
||||
|
||||
|
||||
def print_detail(rid: str) -> None:
|
||||
detail = run_dws([
|
||||
'report', 'entry', 'get',
|
||||
'--report-id', rid, '--format', 'json',
|
||||
])
|
||||
if not isinstance(detail, dict):
|
||||
return
|
||||
result = detail.get('result')
|
||||
if not isinstance(result, dict):
|
||||
return
|
||||
for c in (result.get('report_content') or [])[:3]:
|
||||
key = c.get('key', '')
|
||||
val = c.get('value', '')
|
||||
if key and val:
|
||||
print(f" {key}: {str(val).strip()[:60]}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='查看收到的日志'
|
||||
parser = argparse.ArgumentParser(description='查看收到的日志')
|
||||
parser.add_argument(
|
||||
'--days', type=int, default=1, help='查询天数 (默认 1,即今天)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--days', type=int, default=1, help='查询天数 (默认 1)'
|
||||
'--detail', action='store_true', help='额外拉取每条正文'
|
||||
)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
now = datetime.now()
|
||||
start = now - timedelta(days=args.days)
|
||||
start = start.replace(hour=0, minute=0, second=0)
|
||||
start_dt = now - timedelta(days=max(args.days - 1, 0))
|
||||
start = iso_start(start_dt)
|
||||
end = iso_end(now)
|
||||
|
||||
label = '今天' if args.days == 1 else f'最近 {args.days} 天'
|
||||
print(f'📓 查看{label}收到的日志...\n')
|
||||
|
||||
data = run_dws([
|
||||
'report', 'list',
|
||||
'--start', to_iso(start),
|
||||
'--end', to_iso(now),
|
||||
'--cursor', '0',
|
||||
'--size', '20',
|
||||
'--format', 'json',
|
||||
], dry_run=args.dry_run)
|
||||
print(f'查看{label}收到的日志...\n')
|
||||
|
||||
pairs = fetch_inbox(start, end, args.dry_run)
|
||||
if args.dry_run:
|
||||
return
|
||||
if not data:
|
||||
print('未查到日志')
|
||||
if not pairs:
|
||||
print(' 暂无收到的日志')
|
||||
return
|
||||
|
||||
reports = (data if isinstance(data, list)
|
||||
else data.get('result', data.get('reports', [])))
|
||||
if not reports:
|
||||
print(' ✅ 暂无收到的日志')
|
||||
return
|
||||
|
||||
print(f"📓 {label}日志 ({len(reports)} 条)")
|
||||
print(f"{label}日志 ({len(pairs)} 条)")
|
||||
print('=' * 50)
|
||||
|
||||
for r in reports:
|
||||
rid = r.get('reportId') or r.get('id', '')
|
||||
creator = r.get('creatorName') or r.get('creator', '未知')
|
||||
template = r.get('templateName') or r.get('template', '')
|
||||
create_time = r.get('createTime', '')
|
||||
if isinstance(create_time, (int, float)):
|
||||
create_time = datetime.fromtimestamp(
|
||||
create_time / 1000
|
||||
).strftime('%Y-%m-%d %H:%M')
|
||||
for item, rid in pairs:
|
||||
title = item.get('标题') or '日志'
|
||||
sender = item.get('发送人') or '未知'
|
||||
date = item.get('日期') or ''
|
||||
status = item.get('状态') or ''
|
||||
link = item.get('钉钉链接') or ''
|
||||
|
||||
print(f"\n 📝 {template or '日志'} - {creator}")
|
||||
print(f" 时间: {create_time}")
|
||||
print(f" ID: {rid}")
|
||||
print(f"\n {title} - {sender}")
|
||||
print(f" 时间: {date}")
|
||||
if status:
|
||||
print(f" 状态: {status}")
|
||||
if link:
|
||||
print(f" 链接: {link}")
|
||||
|
||||
if rid:
|
||||
detail = run_dws([
|
||||
'report', 'detail',
|
||||
'--report-id', rid, '--format', 'json',
|
||||
])
|
||||
if detail and isinstance(detail, dict):
|
||||
contents = detail.get('contents', [])
|
||||
for c in contents[:3]:
|
||||
key = c.get('key') or c.get('title', '')
|
||||
val = c.get('value') or c.get('content', '')
|
||||
if key and val:
|
||||
print(f" {key}: {str(val)[:60]}")
|
||||
if args.detail and rid:
|
||||
print_detail(rid)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
"""
|
||||
查看今天收到的日志列表及详情
|
||||
|
||||
基于 `dws report inbox list` + `dws report entry get`(旧的 report list / report detail
|
||||
已废弃)。inbox list 返回的 result[] 使用中文展示键:日期 / 标题 / 发送人 / 状态 / 钉钉链接;
|
||||
reportId 不在 result[] 里,只在 _internalDetailCommands[].command 中,按页与 result[] 同序对应。
|
||||
|
||||
用法:
|
||||
python report_received_today.py
|
||||
python report_received_today.py --days 3 # 最近 3 天
|
||||
python report_received_today.py --days 3 # 最近 3 天
|
||||
python report_received_today.py --detail # 额外拉取每条正文 (entry get)
|
||||
python report_received_today.py --dry-run
|
||||
"""
|
||||
|
||||
@@ -13,7 +18,7 @@ import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Any, Optional
|
||||
from typing import List, Any, Optional, Tuple
|
||||
|
||||
|
||||
def run_dws(
|
||||
@@ -37,91 +42,118 @@ def run_dws(
|
||||
return None
|
||||
|
||||
|
||||
def to_iso(dt: datetime) -> str:
|
||||
return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00')
|
||||
def iso_start(dt: datetime) -> str:
|
||||
return dt.strftime('%Y-%m-%dT00:00:00+08:00')
|
||||
|
||||
|
||||
def iso_end(dt: datetime) -> str:
|
||||
return dt.strftime('%Y-%m-%dT23:59:59+08:00')
|
||||
|
||||
|
||||
def report_id_from_command(cmd: str) -> str:
|
||||
parts = cmd.split()
|
||||
if '--report-id' in parts:
|
||||
i = parts.index('--report-id')
|
||||
if i + 1 < len(parts):
|
||||
return parts[i + 1]
|
||||
return ''
|
||||
|
||||
|
||||
def fetch_inbox(
|
||||
start: str, end: str, dry_run: bool,
|
||||
) -> List[Tuple[dict, str]]:
|
||||
"""按 cursor 翻页拉全 inbox;返回 (result_item, reportId) 列表。"""
|
||||
pairs: List[Tuple[dict, str]] = []
|
||||
cursor = 0
|
||||
while True:
|
||||
data = run_dws([
|
||||
'report', 'inbox', 'list',
|
||||
'--start', start,
|
||||
'--end', end,
|
||||
'--cursor', str(cursor),
|
||||
'--size', '20',
|
||||
'--format', 'json',
|
||||
], dry_run=dry_run)
|
||||
if dry_run or not isinstance(data, dict):
|
||||
return pairs
|
||||
items = data.get('result') or []
|
||||
cmds = data.get('_internalDetailCommands') or []
|
||||
for idx, item in enumerate(items):
|
||||
rid = ''
|
||||
if idx < len(cmds):
|
||||
rid = report_id_from_command(
|
||||
cmds[idx].get('command', '')
|
||||
)
|
||||
pairs.append((item, rid))
|
||||
if data.get('hasMore') and data.get('nextCursor') is not None:
|
||||
cursor = data['nextCursor']
|
||||
else:
|
||||
break
|
||||
return pairs
|
||||
|
||||
|
||||
def print_detail(rid: str) -> None:
|
||||
detail = run_dws([
|
||||
'report', 'entry', 'get',
|
||||
'--report-id', rid, '--format', 'json',
|
||||
])
|
||||
if not isinstance(detail, dict):
|
||||
return
|
||||
result = detail.get('result')
|
||||
if not isinstance(result, dict):
|
||||
return
|
||||
for c in (result.get('report_content') or [])[:3]:
|
||||
key = c.get('key', '')
|
||||
val = c.get('value', '')
|
||||
if key and val:
|
||||
print(f" {key}: {str(val).strip()[:60]}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='查看收到的日志'
|
||||
parser = argparse.ArgumentParser(description='查看收到的日志')
|
||||
parser.add_argument(
|
||||
'--days', type=int, default=1, help='查询天数 (默认 1,即今天)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--days', type=int, default=1, help='查询天数 (默认 1)'
|
||||
'--detail', action='store_true', help='额外拉取每条正文'
|
||||
)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
now = datetime.now()
|
||||
start = now - timedelta(days=args.days)
|
||||
start = start.replace(hour=0, minute=0, second=0)
|
||||
start_dt = now - timedelta(days=max(args.days - 1, 0))
|
||||
start = iso_start(start_dt)
|
||||
end = iso_end(now)
|
||||
|
||||
label = '今天' if args.days == 1 else f'最近 {args.days} 天'
|
||||
print(f'📓 查看{label}收到的日志...\n')
|
||||
|
||||
data = run_dws([
|
||||
'report', 'list',
|
||||
'--start', to_iso(start),
|
||||
'--end', to_iso(now),
|
||||
'--cursor', '0',
|
||||
'--size', '20',
|
||||
'--format', 'json',
|
||||
], dry_run=args.dry_run)
|
||||
print(f'查看{label}收到的日志...\n')
|
||||
|
||||
pairs = fetch_inbox(start, end, args.dry_run)
|
||||
if args.dry_run:
|
||||
return
|
||||
if not data:
|
||||
print('未查到日志')
|
||||
if not pairs:
|
||||
print(' 暂无收到的日志')
|
||||
return
|
||||
|
||||
if isinstance(data, list):
|
||||
reports = data
|
||||
elif isinstance(data, dict):
|
||||
inner = data.get('result', data)
|
||||
if isinstance(inner, dict):
|
||||
reports = inner.get('report_list',
|
||||
inner.get('reports', []))
|
||||
elif isinstance(inner, list):
|
||||
reports = inner
|
||||
else:
|
||||
reports = []
|
||||
else:
|
||||
reports = []
|
||||
if not reports:
|
||||
print(' ✅ 暂无收到的日志')
|
||||
return
|
||||
|
||||
print(f"📓 {label}日志 ({len(reports)} 条)")
|
||||
print(f"{label}日志 ({len(pairs)} 条)")
|
||||
print('=' * 50)
|
||||
|
||||
for r in reports:
|
||||
if not isinstance(r, dict):
|
||||
print(f"\n 📝 {r}")
|
||||
continue
|
||||
rid = r.get('reportId') or r.get('id', '')
|
||||
creator = r.get('creatorName') or r.get('creator', '未知')
|
||||
template = r.get('templateName') or r.get('template', '')
|
||||
create_time = r.get('createTime', '')
|
||||
if isinstance(create_time, (int, float)):
|
||||
create_time = datetime.fromtimestamp(
|
||||
create_time / 1000
|
||||
).strftime('%Y-%m-%d %H:%M')
|
||||
for item, rid in pairs:
|
||||
title = item.get('标题') or '日志'
|
||||
sender = item.get('发送人') or '未知'
|
||||
date = item.get('日期') or ''
|
||||
status = item.get('状态') or ''
|
||||
link = item.get('钉钉链接') or ''
|
||||
|
||||
print(f"\n 📝 {template or '日志'} - {creator}")
|
||||
print(f" 时间: {create_time}")
|
||||
print(f" ID: {rid}")
|
||||
print(f"\n {title} - {sender}")
|
||||
print(f" 时间: {date}")
|
||||
if status:
|
||||
print(f" 状态: {status}")
|
||||
if link:
|
||||
print(f" 链接: {link}")
|
||||
|
||||
if rid:
|
||||
detail = run_dws([
|
||||
'report', 'detail',
|
||||
'--report-id', rid, '--format', 'json',
|
||||
])
|
||||
if detail and isinstance(detail, dict):
|
||||
contents = detail.get('contents', [])
|
||||
for c in contents[:3]:
|
||||
key = c.get('key') or c.get('title', '')
|
||||
val = c.get('value') or c.get('content', '')
|
||||
if key and val:
|
||||
print(f" {key}: {str(val)[:60]}")
|
||||
if args.detail and rid:
|
||||
print_detail(rid)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -98,7 +98,7 @@ Flags:
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `aisearch person` | `userId`(用户ID)、`title`(姓名) | 展示搜索结果、后续操作(发消息/建待办等) |
|
||||
| `aisearch person` | `userId`(用户ID,= `meta.staffId`/`meta.jobNumber`)、`author`(真实姓名,= `meta.name`);`title` 是花名/显示名,不一定等于姓名 | 展示搜索结果、后续操作(发消息/建待办等) |
|
||||
|
||||
## 重名消歧
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|--------|-------------------|
|
||||
| read-aitable | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable field get --base-id <baseId> --table-id <tableId>` → 取 `fieldId`<br>3. **取记录(按场景分流)**:<br> • 数据统计/分析/全量汇总 → `aitable record query --base-id <baseId> --table-id <tableId> --all`(自动翻页,**禁止凭单页数据做统计**)<br> • 大表保险 → 加 `--page-limit 100`(默认 50 页/5000 条,0 = 无限制)<br> • 单纯预览前几条 → `aitable record query --base-id <baseId> --table-id <tableId> --limit 30`(不加 --all)<br> • 筛选时 `--filters` 格式见 [aitable-filter-sort.md](../products/aitable/aitable-filter-sort.md)<br>4. **检查输出契约**:`hasMore=true` 时数据被截断,必须用 `--cursor <X>` 续拉;`partial=true` 时表示中途某页失败(保留已拉数据,可重试)<br>5. 总结数据 |
|
||||
| generate-data-report | 1. 同 read-aitable 步骤 1-3(**必须用 --all 防漏数据**)<br>2. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行 → 补充背景<br>3. `doc create --name "<报告名>" --content "<分析报告>"` |
|
||||
| create-aitable-record | **写入路径分流**(关键决策):<br> • **追加到已有表**(场景:把这批数据加到「成员表」)→ `python scripts/import_records.py <baseId> <tableId> data.csv\|data.json [batch_size]`(走 `record create`,需要列名匹配字段名)<br> • **新建表**(场景:把这个 Excel 变成多维表)→ `python scripts/aitable_import_via_task.py <baseId> <file>` 或 `dws aitable import upload --base-id <baseId> --file ./x.xlsx` + `dws aitable import data --import-id <ID>`(CLI 已内置 OSS PUT 清空 Content-Type + 同步轮询,**禁止自己写 PUT**)<br> • **单条/少量手动**:1. `aitable base search` → `baseId`/`tableId`;2. `aitable field get` → `fieldId` 与类型;3. `aitable record create --records '[{"cells":{"<fieldId>":"值"}}]'` |
|
||||
| create-aitable-record | **写入路径分流**(关键决策):<br> • **追加到已有表**(场景:把这批数据加到「成员表」)→ `python scripts/import_records.py <baseId> <tableId> data.csv\|data.json [batch_size]`(走 `record create`,需要列名匹配字段名)<br> • **新建表**(场景:把这个 Excel 变成多维表)→ `python scripts/aitable_import_via_task.py <baseId> <file>`(脚本内置 prepare→OSS PUT→import data 全流程和正确的头处理,**禁止自己写 PUT**)。注意 `aitable import upload` 没有 `--file` flag、也不代做 PUT,只准备导入;能一站式完成的是上面的脚本<br> • **单条/少量手动**:1. `aitable base search` → `baseId`/`tableId`;2. `aitable field get` → `fieldId` 与类型;3. `aitable record create --records '[{"cells":{"<fieldId>":"值"}}]'` |
|
||||
| update-aitable-record | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. **取目标 record**:<br> • 已知少量 recordId → `aitable record query --record-ids <ID1,ID2>`<br> • 按条件批量改 → `aitable record query --base-id <baseId> --table-id <tableId> --filters '<JSON>' --all`(**用 --all 防止漏改**)<br>3. **先展示让用户确认要改的 record 列表**<br>4. `aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<recordId>","cells":{...}}]'`(单次 ≤30 条) |
|
||||
| search-aitable-template | 1. `aitable template search --query "<关键词>"` → 取 `templateId`<br>2. 用户选定<br>3. `aitable base create --name "<表格名>" --template-id <templateId>` → 取 `baseId` |
|
||||
| export-aitable-to-xlsx | 1. `aitable base search --query "<表格名>"` → 取 `baseId`<br>2. **按场景选 scope**:<br> • 全表+附件 → `aitable export data --base-id <baseId> --scope all --export-format excel_and_attachment --output ./<name>.xlsx`<br> • 单表(仅 xlsx)→ `--scope table --table-id <tableId> --export-format excel`<br> • 单视图 → `--scope view --table-id <tableId> --view-id <viewId>`<br>3. CLI 内置渐进式退避轮询 + 自动落盘,**不要自己写 GET downloadUrl**<br>4. 大表超时(默认 5 分钟):加 `--timeout-sec 900` 或拿到 `taskId` 后 `aitable export data --task-id <ID> --output ./<name>.xlsx` 续等<br>5. 与悟空脚本路径并存:复杂场景(多 base 批量 / 按视图组合)请用 `python scripts/aitable_export_via_task.py <baseId> --scope all\|table\|view` |
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `base list` | 列出最近访问的 Base | — | 仅返回最近访问过的,优先用 `base search` |
|
||||
| `base search` | 按名称搜索 Base | `--query` | 关键词 ≥2 字符 |
|
||||
| `base search` | 按名称搜索 Base(别名 `aitable search`) | — | `--query` help 标必填但实际可省略:不传时返回最近访问的 Base 列表。`--keyword` 是 `--query` 的隐藏别名,同义 |
|
||||
| `base get` | 获取 Base 信息(含 tables 列表) | `--base-id` | 用户给 URL 时提取末尾 ID |
|
||||
| `base copy` | 复制整个 Base 到目标文件夹 | `--base-id` `--target-folder-id` | 默认全量复制;`--only-struct` 仅复制结构不含数据 |
|
||||
| `base get-primary-doc-id` | 获取某记录的主键文档 ID | `--base-id` `--table-id` `--record-id` | 等价 `record primary-doc-get` 的取 ID 视角 |
|
||||
| `base create` | 创建 Base | `--name` | 创建后直接用返回的 baseId |
|
||||
| `base update` | 更新 Base 名称 | `--base-id` `--name` | — |
|
||||
| `base delete` | 删除 Base | `--base-id` | 不可逆 |
|
||||
@@ -30,7 +32,8 @@
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `table get` | 获取表结构(字段+视图目录) | `--base-id` | 不传 `--table-ids` 返回全部表 |
|
||||
| `table create` | 创建数据表 | `--base-id` `--name` `--fields` | fields 为 JSON 数组,至少 1 个 |
|
||||
| `table list` | 获取数据表(`table get` 的别名) | `--base-id` | 与 `table get` 等价 |
|
||||
| `table create` | 创建数据表 | `--base-id` `--name` | `--fields` 为 JSON 数组;**可传空数组 `[]`**(默认值即 `[]`),此时服务端自动补一个名为"标题"的 primaryDoc 首列;单次最多 15 个字段 |
|
||||
| `table update` | 重命名表 | `--base-id` `--table-id` `--name` | — |
|
||||
| `table delete` | 删除表 | `--base-id` `--table-id` | 不可逆 |
|
||||
|
||||
@@ -39,19 +42,29 @@
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `field get` | 获取字段完整配置 | `--base-id` `--table-id` | 按需展开少量字段 |
|
||||
| `field list` | 获取字段信息(`field get` 的别名) | `--base-id` `--table-id` | 与 `field get` 等价 |
|
||||
| `field create` | 创建字段 | `--base-id` `--table-id` + (`--name --type` 或 `--fields`) | 支持单字段/批量模式 |
|
||||
| `field update` | 更新字段名/配置 | `--base-id` `--table-id` `--field-id` | 不可变更字段类型 |
|
||||
| `field delete` | 删除字段 | `--base-id` `--table-id` `--field-id` | 不可逆 |
|
||||
| `field search-options` | 搜索单选/多选字段的选项 | `--base-id` `--table-id` `--field-id` | 仅 singleSelect/multipleSelect;`--keyword` 模糊过滤,不传返回全部 |
|
||||
|
||||
### record (记录管理)
|
||||
|
||||
| 命令 | 用途 | 必读 reference | 路由提醒 |
|
||||
|------|------|----------------|----------|
|
||||
| `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference |
|
||||
| `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference;`--query`(隐藏别名 `--keyword`)全文搜索 |
|
||||
| `record list` | 获取记录(`record query` 的别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 与 `record query` 等价 |
|
||||
| `record get` | 按 ID 取记录(`record query --record-ids` 的窄别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 已知 recordId 时首选;必填 `--record-ids`(单次最多 100 条);未暴露 filters/sort/query/cursor/limit |
|
||||
| `record query-empty` | 查询完全没填用户字段的空行 | — | `--base-id` `--table-id`;`--limit` 扫描预算 [1,100],`--cursor` 翻页 |
|
||||
| `record create` | 新增记录 | [aitable-record-create.md](./aitable/aitable-record-create.md) | cells key 必须是 fieldId 不是字段名;单次最多 100 条 |
|
||||
| `record update` | 更新记录 | [aitable-record-update.md](./aitable/aitable-record-update.md) | 需先 query 拿 recordId;只传需改字段;**没有** `--record-id` `--cells` flag |
|
||||
| `record batch-update` | 把同一份 cells 批量应用到多条记录 | [aitable-record-update.md](./aitable/aitable-record-update.md) | `--record-ids`(≤100)+ `--cells` 共享 patch |
|
||||
| `record upsert` | 批量创建或更新(有 recordId 走更新,无则创建) | [aitable-record-upsert.md](./aitable/aitable-record-upsert.md) | `--records`/`--records-file`;单次最多 100 条 |
|
||||
| `record delete` | 删除记录 | [aitable-record-delete.md](./aitable/aitable-record-delete.md) | 不可逆,需先 query 确认 |
|
||||
| `record share-url` | 批量获取记录分享链接 | [aitable-record-share.md](./aitable/aitable-record-share.md) | `--record-ids`(逗号分隔,单次最多 20 条) |
|
||||
| `record history-list` | 查询单条记录的变更历史 | [aitable-record-history.md](./aitable/aitable-record-history.md) | `--record-id` 单条;`--offset`/`--limit`(≤50) |
|
||||
| `record primary-doc-get` | 查询记录的主键文档 nodeId | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 无文档时返回 `no record` 错误 |
|
||||
| `record primary-doc-create` | 为记录创建主键文档 | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 幂等;`--field-id` 须 primaryDoc 类型 |
|
||||
|
||||
### view (视图管理)
|
||||
|
||||
@@ -73,6 +86,32 @@
|
||||
>
|
||||
> 不支持 `formInfo`、`requiredFields`、`conditionalRules` 等 FormDesigner 高级配置,这些 key 会被服务端忽略。
|
||||
|
||||
### form (表单管理) → 详见 [aitable-form.md](./aitable/aitable-form.md)
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `form list` | 列出表单视图 | `--base-id` `--table-id` | 每条含 viewId/name;新建表单无 title 且 createdAt=0,改过后才有 |
|
||||
| `form get` | 按 viewId 取单个表单详情 | `--base-id` `--table-id` `--view-id` | 客户端按 viewId 过滤,`data` 即该表单对象;viewId 不存在报 not found |
|
||||
| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | 等价 `view create --view-type FormDesigner` |
|
||||
| `form update` | 更新表单配置 | `--base-id` `--table-id` `--view-id` | title/name/description 至少一项 |
|
||||
| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` | 不可逆 |
|
||||
| `form field list/update/hide` | 表单字段管理 | — | 详情见子文档 |
|
||||
| `form questions create/delete` | 题目管理(=field create/delete) | — | 详情见子文档 |
|
||||
| `form share get/update` | 表单分享配置 | — | 详情见子文档 |
|
||||
|
||||
> **创建表单**有两种等价方式:`form create --name "..."`(推荐)或 `view create --view-type FormDesigner --name "..."`。
|
||||
|
||||
### workflow (自动化工作流) → 详见 [aitable-workflow.md](./aitable/aitable-workflow.md)
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` |
|
||||
| `workflow get` | 获取单个工作流详情(含 flowSchema) | `--base-id` `--workflow-id` | `--workflow-id` 接受 list 里的 `flowId`(同值) |
|
||||
| `workflow enable` | 启用工作流 | `--base-id` `--workflow-id` | 返回 `{enabled: true}` 是动作确认;要确认真启用看 list 的 `status` |
|
||||
| `workflow disable` | 禁用工作流(高危) | `--base-id` `--workflow-id` `--yes` | 影响业务自动化,建议二次确认;status 变 STOP |
|
||||
|
||||
> **当前不支持通过 CLI 新建/修改/删除工作流**,请去 AI 表格 Web 端(数据表页面 → 自动化)配置。
|
||||
|
||||
### dashboard & chart → 详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)
|
||||
|
||||
| 命令 | 用途 |
|
||||
@@ -102,6 +141,35 @@
|
||||
|------|------|----------|
|
||||
| `template search` | 搜索模板 | `--query` |
|
||||
|
||||
### advperm (高级权限/自定义角色) → 详见 [aitable-advperm.md](./aitable/aitable-advperm.md)
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `advperm enable` | 开启 Base 高级权限总开关 | `--base-id` | 不开启时角色规则不生效 |
|
||||
| `advperm disable` | 关闭 Base 高级权限总开关(高危) | `--base-id` `--yes` | 关闭后全员回退默认权限 |
|
||||
| `advperm role-list` | 列出 Base 下所有角色 | `--base-id` | 同时返回自定义角色和系统角色;`roleType == "custom"` 是自定义,前缀 `system_` 是系统角色 |
|
||||
| `advperm role-get` | 获取单角色完整配置 | `--base-id` `--role-id` | 含 subRoles 与字段/行级规则 |
|
||||
| `advperm role-create` | 创建自定义角色 | `--base-id` `--name` | 可选 `--sub-roles` 同时指定子角色权限规则 |
|
||||
| `advperm role-update` | 增量更新自定义角色(PATCH) | `--base-id` `--role-id` | 未传字段不变;`--sub-roles` 按 (targetId,targetType) 合并 |
|
||||
| `advperm role-delete` | 删除自定义角色 | `--base-id` `--role-id` `--yes` | 不可逆;系统角色禁删;**调用者必须是该 AI 表格的管理员/Owner**,非管理员会得到 401 AUTH_ERROR |
|
||||
|
||||
> 所有写命令(enable/disable/role-create/role-update/role-delete)需要 Base 管理员权限;非管理员只能调 `role-list` / `role-get`(只读)。
|
||||
> "角色 ↔ 成员"绑定当前 CLI 不支持,仍需在 AI 表格 Web 端 → Base 设置 → 高级权限面板手动完成。
|
||||
|
||||
### section (文件夹与节点管理)
|
||||
|
||||
用于在 Base 的导航树中组织 table / dashboard / 表单视图 / 文档等节点(类似文件夹)。操作前建议先用 `section list-nodes` 拿到 nodeId / sectionId 与父级关系。
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `section list-nodes` | 列出 Base 下全部节点 | `--base-id` | 返回 `{nodeId, nodeType, parentSectionId, name?}`;是 move-node / reorder 的前置定位命令 |
|
||||
| `section list-empty` | 列出空文件夹 | `--base-id` | 返回 `{sectionId, name, parentSectionId}`,用于清理导航树 |
|
||||
| `section create` | 创建文件夹 | `--base-id` `--name` | `--parent-section-id` 不传/空串=根目录;`--index` 指定位置 |
|
||||
| `section rename` | 重命名文件夹 | `--base-id` `--section-id` `--new-name` | — |
|
||||
| `section delete` | 删除文件夹 | `--base-id` `--section-id` | 不可逆;建议先 list-empty 确认为空 |
|
||||
| `section reorder` | 调整文件夹顺序 | `--base-id` `--section-id` `--target-index` | 仅当前父级下调序;跨父级用 move-node |
|
||||
| `section move-node` | 移动节点(跨父级) | `--base-id` `--node-id` `--new-parent-section-id` | `--new-parent-section-id ""`=移到根;`--target-index` 调全局位置 |
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户说"表格/多维表/AI表格":
|
||||
@@ -130,6 +198,8 @@
|
||||
- 修改/更新 → `record update`(读 [aitable-record-update.md](./aitable/aitable-record-update.md))
|
||||
- 删除 → `record delete`
|
||||
|
||||
用户说"表单/问卷/form/收集信息" → 读 [aitable-form.md](./aitable/aitable-form.md)
|
||||
|
||||
用户说"筛选/过滤/filter" → 读 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md)
|
||||
|
||||
用户说"统计/分析/聚合/TOP N/全量" → 读 [aitable-data-analysis-sop.md](./aitable/aitable-data-analysis-sop.md)
|
||||
|
||||
@@ -38,8 +38,10 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
dws aitable attachment upload --base-id <BASE_ID> --file-name report.pdf --size 204800 --format json
|
||||
# → 返回 uploadUrl、fileToken
|
||||
|
||||
# 2. PUT 上传(Content-Type 留空)
|
||||
curl -X PUT "<uploadUrl>" -H "Content-Type:" --data-binary @report.pdf
|
||||
# 2. PUT 上传(Content-Type 必须与文件类型一致,不能留空)
|
||||
# 留空或用 curl 默认的 application/x-www-form-urlencoded 都会被 OSS 拒为 403
|
||||
curl -X PUT "<uploadUrl>" -H "Content-Type: application/pdf" --data-binary @report.pdf
|
||||
# 例:.txt → text/plain,.png → image/png,.xlsx → application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
|
||||
# 3. 写入记录
|
||||
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
|
||||
@@ -15,8 +15,10 @@ 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 share get` 可能返回 `404`(`retryable:true`)——从未分享、甚至刚开启分享后立即查都可能 404;`data` 为 `{}`。按可重试错误处理,不要误判为参数拼错。它有时也会返回 `success + data`(如关闭分享之后),行为不稳定,别用它当"是否已分享"的唯一判据。
|
||||
- `chart share get` 稳定返回 `success + data`(含 `enabled` 等),可用于分享状态判断;从未分享时 `enabled=false`,不会 404。
|
||||
- ⚠️ **`dashboard share update` 开启 ORG 分享后返回的 `shareType` 是 `"[1]"`(未映射回 `ORG`,服务端已知问题)**;`chart share update --share-type ORG` 则正确返回 `shareType="ORG"`。判断 dashboard 是否 ORG 分享时对 `"[1]"` 做兼容。
|
||||
- `chart share update` / `dashboard share update` 的 `--enabled` 是字符串 flag:`--enabled false`(空格)和 `--enabled=false` 都能正确关闭分享。
|
||||
|
||||
## dashboard 子命令
|
||||
|
||||
@@ -25,18 +27,25 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
||||
| `dashboard get` | 获取仪表盘详情(含 charts 列表) | `--base-id` `--dashboard-id` | — |
|
||||
| `dashboard create` | 创建仪表盘 | `--base-id` + (`--config` 或 `--name`) | `--name` 简化版创建空看板;`--config` 传完整 JSON |
|
||||
| `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config` 或 `--name`) | `--name` 仅改名;`--config` 更新完整配置 |
|
||||
| `dashboard arrange` | 自动重排仪表盘图表布局 | `--base-id` `--dashboard-id` | 让服务端重新排布 charts 位置 |
|
||||
| `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` `--yes` | — |
|
||||
| `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 |
|
||||
| `dashboard share get` | 获取仪表盘分享配置 | `--base-id` `--dashboard-id` | 可能 404,见上方要点 |
|
||||
| `dashboard share update` | 更新仪表盘分享配置 | `--base-id` `--dashboard-id` `--enabled` | `--enabled true` 开启,`--share-type` 用 `ORG`(ORG 回显 `shareType="[1]"`);部分组织禁用了 `PUBLIC` 公开分享;若报 `Illegal argument`,改用 `ORG`(组织内分享)。`--enabled false` 关闭 |
|
||||
|
||||
## chart 子命令
|
||||
|
||||
| 命令 | 用途 | 必填参数 |
|
||||
|------|------|----------|
|
||||
| `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` |
|
||||
| `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` |
|
||||
| `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` |
|
||||
| `chart widgets-example` | 查看图表 widgets 配置模板 | 无 |
|
||||
| `chart share get` | 获取图表分享配置 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart share update` | 更新图表分享配置(`--enabled true/false`,开启配 `--share-type`,用 `ORG`;部分组织禁用了 `PUBLIC` 公开分享;报 `Illegal argument` 时改用 `ORG`) | `--base-id` `--dashboard-id` `--chart-id` `--enabled` |
|
||||
|
||||
> `chart create` 的 `--layout` 是**必填**(12 列网格布局,如 `{"x":0,"y":0,"w":6,"h":4}`);不传本地校验直接拒。`chart update` 的 `--config` 也**必填**——即便只想改 layout,也要带完整 config,否则服务端拒绝。
|
||||
|
||||
## 配置获取流程
|
||||
|
||||
|
||||
@@ -118,23 +118,25 @@ dws aitable import data --import-id <importId> --table-id <TABLE_ID> --format js
|
||||
|
||||
用户给文件让导入 AI 表格时,路径选择决定成败:
|
||||
|
||||
> ⚠️ **`import upload` 没有 `--file` flag**(传了会报 unknown flag)。它只申请上传凭证,必填 `--file-name` + `--file-size`,拿到 `uploadUrl` 后要**自己 curl PUT 上传文件**(Content-Type 留空,见上文三步流程),再 `import data`。想省事直接用 `aitable_import_via_task.py` 脚本,它把这三步包好了。
|
||||
|
||||
| 用户原话 | 链路 | 命令 / 脚本 | 行为 |
|
||||
|---------|------|------------|------|
|
||||
| "把这个 Excel 导入到 AI 表格"(无指定目标表) | **文件导入任务** | `python scripts/aitable_import_via_task.py <baseId> <file>` 或 `dws aitable import upload --base-id B --file ./x.xlsx` + `dws aitable import data --import-id <ID>` | 服务端解析文件,**新建数据表**,自动识别表头 |
|
||||
| "把这个 Excel 导入到 AI 表格"(无指定目标表) | **文件导入任务** | `python scripts/aitable_import_via_task.py <baseId> <file>`(推荐)或手动三步 `import upload --file-name x.xlsx --file-size <字节>` → curl PUT → `import data --import-id <ID>` | 服务端解析文件,**新建数据表**,自动识别表头 |
|
||||
| "把这个 Excel 导入新表 / 自动建表" | 同上 | 同上 | 同上 |
|
||||
| "把这批记录追加到已有的『成员表』里" | **记录批量写入** | `python scripts/import_records.py <baseId> <tableId> <file>` | 走 `record create`,**写入已有 tableId**,需要字段名匹配 |
|
||||
| "Excel 列名和表字段对不上但要追加" | 文件导入 + 追加 + 字段映射 | `dws aitable import upload --base-id B --file ./x.xlsx` → `dws aitable import data --import-id <ID> --table-id <TBL> --field-mapping '{"目标":"源"}'` | 服务端按映射追加 |
|
||||
| "Excel 列名和表字段对不上但要追加" | 文件导入 + 追加 + 字段映射 | 三步导入后 `import data --import-id <ID> --table-id <TBL> --field-mapping '{"目标":"源"}'` | 服务端按映射追加 |
|
||||
|
||||
## 大表 / 长任务超时续等
|
||||
|
||||
默认整体轮询超时 5 分钟。大表导出/导入超时后命令会返回 `taskId` / `importId`,用同命令带 ID 续等:
|
||||
单次等待窗口很短:`export data` 只有 `--timeout-ms`(默认且**上限 30000 = 30 秒**,没有 `--timeout-sec`,传了会报 unknown flag);`import data` 用 `--timeout`(秒,默认且推荐最大值 30)。窗口内没跑完,命令会返回 `taskId` / `importId`,用同命令带 ID 反复续等即可:
|
||||
|
||||
```bash
|
||||
# 续等导出
|
||||
dws aitable export data --base-id <B> --task-id <ID> --output ./out.xlsx
|
||||
# 续等导出:拿到 downloadUrl 后再 curl 下载(见下方警告)
|
||||
dws aitable export data --base-id <B> --task-id <ID> --timeout-ms 30000
|
||||
|
||||
# 续等导入
|
||||
dws aitable import data --import-id <ID>
|
||||
```
|
||||
|
||||
或一次性把 timeout 提到 15 分钟:`--timeout-sec 900`。
|
||||
> ⚠️ **`--output` 不会保存导出的 xlsx**:`--output` 是隐藏的全局 flag,作用是把命令的 **JSON 输出**写到文件,实测只生成一个 0 字节文件,不会下载导出内容。正确做法是从 `export data` 返回里取 `downloadUrl`,再 `curl -L "<downloadUrl>" -o out.xlsx` 下载。想省事直接用 `aitable_export_via_task.py` 脚本(它负责轮询 + 下载)。
|
||||
|
||||
@@ -50,8 +50,8 @@ dws aitable form share get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| `form list` | 列出表单视图 | `--base-id` `--table-id` | 返回 viewId/name/title/createdAt |
|
||||
| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 内部基于 list_form_views 过滤 |
|
||||
| `form list` | 列出表单视图 | `--base-id` `--table-id` | 每条返回 viewId/name;新建表单**无 title 且 createdAt=0**,改过(form update)后才出现 title 和真实 createdAt |
|
||||
| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 客户端按 viewId 过滤后 `data` **即该表单对象**(不是 formViews 数组);viewId 不存在返回 `form view ... not found` 错误 |
|
||||
| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | viewType=FormDesigner |
|
||||
| `form update` | 更新表单 | `--base-id` `--table-id` `--view-id` | `--title`/`--name`(等价)和 `--description` 至少传一项;同时传 title/name 时 title 优先 |
|
||||
| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` `--yes` | 不可逆 |
|
||||
@@ -115,6 +115,6 @@ dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VI
|
||||
|
||||
## 返回结构补充
|
||||
|
||||
- `form list` 返回 `data.formViews[]`,**每条仅含** `viewId/name/title/createdAt`;`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。
|
||||
- `form get` 返回结构与 `form list` 完全一致(`data.formViews[]`),仅含一条记录(与请求 viewId 一致)。Agent 提取时仍走 `data.formViews[0]`。
|
||||
- `form list` 返回 `data.formViews[]`,**每条含** `viewId/name`(+ `createdAt`);**新建表单没有 `title` 字段且 `createdAt=0`**,只有在 `form update` 碰过之后才会出现 `title` 和真实 `createdAt`。`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。
|
||||
- `form get` 的 `data` **就是命中的那一条表单对象**(如 `{viewId, name, createdAt, title?, shareFormUuid?}`),不是 `formViews` 数组。Agent 直接读 `data.viewId` / `data.name` 即可,**不要**再取 `data.formViews[0]`。服务端的 viewIds 过滤参数当前不生效,CLI 在客户端按 viewId 精确筛出单条;传了不存在的 viewId 会返回 `form view <id> not found in table` 错误。
|
||||
- `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`。
|
||||
|
||||
@@ -17,7 +17,9 @@ dws aitable record primary-doc-get --base-id BASE_ID --table-id TABLE_ID --recor
|
||||
- `--table-id`(必填):Table ID
|
||||
- `--record-id`(必填):Record ID
|
||||
|
||||
**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。若该记录尚未创建主键文档,`nodeId` 为 null。
|
||||
**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。
|
||||
|
||||
> **该记录尚未创建主键文档时**:不会返回 `nodeId: null`,而是 `status=error`、`data={}`,`error={code:"-1", message:"no record", type:"SYSTEM_ERROR", retryable:true}`。要判断"有没有主键文档",看是否命中这个 `no record` 错误,而不是判断 `nodeId` 是否为 null。需要文档时改用 `primary-doc-create`(幂等,已存在则直接返回)。
|
||||
|
||||
### 创建主键文档
|
||||
|
||||
|
||||
@@ -25,12 +25,11 @@ dws aitable record history-list \
|
||||
"data": {
|
||||
"histories": [
|
||||
{
|
||||
"type": "field_change", // 变更类型
|
||||
"action": "update", // 操作动作: create / update / delete
|
||||
"newValue": "{\"...\":\"...\"}", // 变更后的值(JSON 字符串)
|
||||
"oldValue": "{\"...\":\"...\"}", // 变更前的值(JSON 字符串)
|
||||
"type": "row", // 变更类型,实测均为 "row"(行级变更)
|
||||
"action": "updateRecords", // 操作动作:appendRow(新增行) / updateRecords(更新记录)
|
||||
"newValue": "{\"fldX\":{\"dataType\":\"STRING\",\"value\":\"改后值\"}}", // 变更后的值(JSON 字符串,按 fieldId 组织)
|
||||
"oldValue": "{\"fldX\":{\"dataType\":\"STRING\",\"value\":\"改前值\"}}", // 变更前的值(appendRow 新增行时无此字段)
|
||||
"operateTime": 1733123456789, // 操作时间(毫秒级时间戳)
|
||||
"typeChangedFields": "{...}", // 类型变更的字段信息(JSON 字符串)
|
||||
"version": 7 // 版本号(单调递增)
|
||||
}
|
||||
]
|
||||
@@ -38,14 +37,14 @@ dws aitable record history-list \
|
||||
}
|
||||
```
|
||||
|
||||
`newValue` / `oldValue` / `typeChangedFields` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值。
|
||||
`newValue` / `oldValue` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值;解析后是 `{fieldId: {dataType, value}}` 结构。`appendRow`(新增行)事件没有 `oldValue`。实测返回里**没有** `typeChangedFields` 字段。
|
||||
|
||||
## 字段含义速查
|
||||
|
||||
| 字段 | 用途 |
|
||||
|------|------|
|
||||
| `type` | 高层分类:`record_create` / `field_change` / `record_delete` 等。先按 type 过滤大类。 |
|
||||
| `action` | 三态:`create` / `update` / `delete`。比 type 粗,但便于按"动作"统计。 |
|
||||
| `type` | 实测均为 `row`(行级变更),服务端未按 `record_create` / `field_change` 细分。 |
|
||||
| `action` | 底层操作名:`appendRow`(新增行)/ `updateRecords`(更新记录)。按"动作"统计时以这两个值为准。 |
|
||||
| `version` | 单调递增整数;同一 record 越新值越大。**用作"上一条 vs 这一条"的稳定排序键**。 |
|
||||
| `operateTime` | 毫秒时间戳;可格式化成可读时间。多条同 version 的极端场景用 operateTime 兜底排序。 |
|
||||
|
||||
@@ -74,20 +73,22 @@ dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --
|
||||
|
||||
```bash
|
||||
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --format json \
|
||||
| jq '[.data.histories[] | select(.action == "update")][0].oldValue'
|
||||
| jq '[.data.histories[] | select(.action == "updateRecords")][0].oldValue'
|
||||
```
|
||||
|
||||
### 4. 找出删除事件(如果存在 delete history)
|
||||
### 4. 只看更新事件(排除新增行)
|
||||
|
||||
```bash
|
||||
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \
|
||||
| jq '.data.histories[] | select(.action == "delete") | {version, operateTime}'
|
||||
| jq '.data.histories[] | select(.action == "updateRecords") | {version, operateTime}'
|
||||
```
|
||||
|
||||
> 记录被 `record delete` 删除后,其历史不再返回(`histories` 为空数组),无法通过本命令回溯删除事件。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 一次只能查一条 record;如需批量审计多条记录请循环调用。
|
||||
- 仅返回**字段值变更**与**记录生命周期事件**;视图、字段定义、表结构变更不在此 history 里。
|
||||
- 仅返回**新增行**(appendRow)与**字段值变更**(updateRecords);记录被删除后其历史不再可查(返回空)。视图、字段定义、表结构变更不在此 history 里。
|
||||
- 历史保留时长由 server 决定,过老的记录可能不再返回。
|
||||
|
||||
## 与其他 record 命令的关系
|
||||
|
||||
@@ -79,21 +79,21 @@ dws aitable view update timebar --view-id GANTT_ID --official-holiday=true
|
||||
|
||||
### view update aggregate(仅 Grid)
|
||||
|
||||
值是 `map[fieldId]→AggregateAction string`;传 null 清除某个字段聚合。
|
||||
值是 `map[fieldId]→AggregateAction string`。**设置**聚合可用;**清除**聚合当前无效(见下方警告)。
|
||||
|
||||
| flag | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `--field-id` | string | 配合 `--action` 设置**单字段**聚合 |
|
||||
| `--action` | string | `SUM`/`AVG`/`MAX`/`MIN`/`MEDIAN`/`RANGE`/`TOTAL`/`DISTINCT`/`EXIST`/`UN_EXIST`/`CHECKED`/`EARLIEST_DATE` 等(按字段类型可用) |
|
||||
| `--clear-field-id` | string (CSV) | 一/多个字段 ID,清除其聚合 |
|
||||
| `--clear-field-id` | string (CSV) | 一/多个字段 ID,本意是清除其聚合,但**当前服务端不支持清除,静默无效**(见下方警告) |
|
||||
| `--json` | JSON | 完整 aggregate map |
|
||||
|
||||
```bash
|
||||
dws aitable view update aggregate --view-id GRID_ID --field-id fldX --action SUM
|
||||
dws aitable view update aggregate --view-id GRID_ID --clear-field-id fldA,fldB
|
||||
dws aitable view update aggregate --view-id GRID_ID --json '{"fldX":"AVG","fldY":null}'
|
||||
```
|
||||
|
||||
> ⚠️ **当前无法清除已设置的聚合(服务端限制)**:`--clear-field-id fldA,fldB` 与 `--json '{"fldX":null}'` 两种清除写法都返回 `success`,但用 `view get aggregate` 复核会发现聚合**原样不动**——是静默无效,不是真的清掉。这是服务端没有清除语义所致,直至服务端修复前不要依赖它。改聚合方式可行(重新 `--action` 覆盖成别的),只是无法回到"无聚合"。
|
||||
|
||||
### view update field-widths(仅 Grid)
|
||||
|
||||
| flag | 类型 |
|
||||
@@ -108,9 +108,11 @@ dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB
|
||||
|
||||
### view update visible-fields(通用)
|
||||
|
||||
整组替换可见字段列表与顺序。首列字段(primaryDoc)必须保留在数组第一位。
|
||||
整组替换可见字段列表与顺序,同时兼作**隐藏/显示**入口。首列字段(primaryDoc)必须保留在数组第一位。
|
||||
|
||||
> ⚠️ 注意:服务端**只接受 reorder,不接受真"隐藏字段"**——如果传入的列表比当前 columns 短,缺失的字段不会被隐藏。需要真正隐藏字段请到 AI 表格 Web UI。
|
||||
> **传入的列表既定顺序又定可见性**:传一个比当前 columns 短的列表,缺失的字段会被真正隐藏(该字段在 `view list` 的 `custom.hiddenFields` 里变 `true`);再传回全量列表即可解除隐藏(`hiddenFields` 变 `false`)。
|
||||
>
|
||||
> ⚠️ **查隐藏状态别看这里**:`view get visible-fields` 返回的数组**包含已隐藏字段**(列的完整顺序),看不出谁被隐藏。要确认隐藏状态,读 `view get`(view list)里该视图的 `custom.hiddenFields`(`{fieldId: true|false}`)。
|
||||
|
||||
| flag | 类型 |
|
||||
|------|------|
|
||||
@@ -118,7 +120,9 @@ dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB
|
||||
| `--json` | string 数组 JSON(与 `--field-ids` 同传时 `--json` 优先) |
|
||||
|
||||
```bash
|
||||
dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA,fldB
|
||||
# 只保留首列和 fldA,其余字段被隐藏
|
||||
dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA
|
||||
# 传回全量列表解除隐藏
|
||||
dws aitable view update visible-fields --view-id VIEW_ID --json '["fldPrimary","fldA","fldB"]'
|
||||
```
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user