Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,21 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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.
|
||||
|
||||
+73
-11
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -202,6 +202,28 @@ func isOpenDingTalkID(value string) bool {
|
||||
return len(value) > 0 && (value[0] == 'D' || value[0] == 'd')
|
||||
}
|
||||
|
||||
// webhookErrcodeFailure 解析自定义机器人 webhook 的响应,判定是否发送失败。
|
||||
// webhook 失败时 HTTP 仍是 200 且返回 {errcode!=0, errmsg},需据 errcode 显式识别。
|
||||
// errcode 可能是 JSON 数字或字符串,统一转字符串比较;缺 errcode 或为 0 视为成功。
|
||||
func webhookErrcodeFailure(raw string) (code, msg string, failed bool) {
|
||||
var m map[string]any
|
||||
if json.Unmarshal([]byte(raw), &m) != nil {
|
||||
return "", "", false
|
||||
}
|
||||
ec, ok := m["errcode"]
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
code = strings.TrimSpace(fmt.Sprintf("%v", ec))
|
||||
if code == "" || code == "0" || code == "0.0" {
|
||||
return "", "", false
|
||||
}
|
||||
if v, ok := m["errmsg"].(string); ok {
|
||||
msg = v
|
||||
}
|
||||
return code, msg, true
|
||||
}
|
||||
|
||||
func splitChatIDValues(values []string) (userIDs []string, openDingTalkIDs []string) {
|
||||
for _, raw := range values {
|
||||
value := strings.TrimSpace(raw)
|
||||
@@ -1841,7 +1863,30 @@ func newChatCommand() *cobra.Command {
|
||||
}
|
||||
toolArgs["atUserIds"] = atUserIds
|
||||
}
|
||||
return callMCPToolOnServer("bot", "send_message_by_custom_robot", toolArgs)
|
||||
// dry-run 只预览参数,不实际发送,交回标准调用路径。
|
||||
if deps.Caller.DryRun() {
|
||||
return callMCPToolOnServer("bot", "send_message_by_custom_robot", toolArgs)
|
||||
}
|
||||
// 自定义机器人 webhook 即使发送失败(如 errcode=300005 token 不存在)
|
||||
// HTTP 仍是 200,会被包成 success:true。这里取原始响应显式识别 errcode,
|
||||
// 非 0 时按失败返回,避免 agent 误判消息已发出。
|
||||
raw, err := callMCPToolReturnTextOnServer(context.Background(), "bot", "send_message_by_custom_robot", toolArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if code, msg, failed := webhookErrcodeFailure(raw); failed {
|
||||
return &CLIError{
|
||||
Code: CodeMCPToolError,
|
||||
Message: fmt.Sprintf("自定义机器人 webhook 发送失败: errcode=%s errmsg=%s", code, msg),
|
||||
Suggestion: "检查 --token 是否有效、机器人是否仍在群内、以及机器人安全设置(关键词/IP/加签)是否拦截",
|
||||
}
|
||||
}
|
||||
var parsed any
|
||||
if json.Unmarshal([]byte(raw), &parsed) == nil {
|
||||
return deps.Out.PrintJSON(parsed)
|
||||
}
|
||||
deps.Out.PrintRaw(raw)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2666,7 +2711,14 @@ func newChatCommand() *cobra.Command {
|
||||
toolArgs["openConversationId"] = groupID
|
||||
}
|
||||
if userID != "" {
|
||||
toolArgs["userId"] = userID
|
||||
// 服务端 get_conversation_info 单聊只认 openDingTalkId(传 userId 键会被
|
||||
// 忽略并报「openCid、cid、peerUid不能同时为空」)。这里把 --user 的 userId
|
||||
// 先解析成 openDingTalkId 再走与 --open-dingtalk-id 相同的通路。
|
||||
resolved, rerr := resolveOpenDingTalkID(context.Background(), userID)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
toolArgs["openDingTalkId"] = resolved
|
||||
}
|
||||
if openDingTalkID != "" {
|
||||
toolArgs["openDingTalkId"] = openDingTalkID
|
||||
@@ -2675,6 +2727,12 @@ func newChatCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
chatConversationInfoCmd.Flags().String("group", "", "群聊 openConversationId(群聊时使用)")
|
||||
chatConversationInfoCmd.Flags().String("conversation-id", "", "--group 的别名")
|
||||
chatConversationInfoCmd.Flags().String("id", "", "--group 的别名")
|
||||
chatConversationInfoCmd.Flags().String("chat", "", "--group 的别名")
|
||||
_ = chatConversationInfoCmd.Flags().MarkHidden("conversation-id")
|
||||
_ = chatConversationInfoCmd.Flags().MarkHidden("id")
|
||||
_ = chatConversationInfoCmd.Flags().MarkHidden("chat")
|
||||
chatConversationInfoCmd.Flags().String("user", "", "单聊对方 userId(单聊时使用)")
|
||||
chatConversationInfoCmd.Flags().String("userId", "", "--user 的别名")
|
||||
_ = chatConversationInfoCmd.Flags().MarkHidden("userId")
|
||||
@@ -4375,7 +4433,7 @@ status 可选值:
|
||||
chatListAllConversationsCmd := &cobra.Command{
|
||||
Use: "list-all-conversations",
|
||||
Short: "分页获取当前用户的全部会话列表",
|
||||
Long: `分页获取当前用户的全部会话列表(包含单聊和群聊)。--limit 指定每页数量(最大 100),--cursor 传分页游标(首次不传或传 0)。
|
||||
Long: `分页获取当前用户的全部会话列表(包含单聊和群聊)。--limit 指定每页数量(1-100,默认 100),--cursor 传分页游标(首次不传或传 0)。
|
||||
返回 hasMore=true 时用 nextCursor 作为下次 --cursor 继续翻页。`,
|
||||
Example: ` dws chat list-all-conversations
|
||||
dws chat list-all-conversations --limit 50
|
||||
@@ -4384,6 +4442,10 @@ status 可选值:
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
toolArgs := map[string]any{}
|
||||
if v, err := cmd.Flags().GetInt("limit"); err == nil && v > 0 {
|
||||
// 服务端每页上限 100,超过会被静默截断,这里显式拒绝以免误以为取全。
|
||||
if v > 100 {
|
||||
return fmt.Errorf("--limit 最大 100(服务端上限),got %d;如需取全部会话请配合 --cursor 翻页", v)
|
||||
}
|
||||
toolArgs["limit"] = v
|
||||
}
|
||||
if v, _ := cmd.Flags().GetInt64("cursor"); v > 0 {
|
||||
@@ -4395,7 +4457,7 @@ status 可选值:
|
||||
return callMCPToolOnServer("im", "list_all_conversations", toolArgs)
|
||||
},
|
||||
}
|
||||
chatListAllConversationsCmd.Flags().Int("limit", 1000, "每页数量(默认 1000)")
|
||||
chatListAllConversationsCmd.Flags().Int("limit", 100, "每页数量(1-100,默认 100)")
|
||||
chatListAllConversationsCmd.Flags().Int64("cursor", 0, "分页游标(首次不传或传 0,翻页传 nextCursor)")
|
||||
chatListAllConversationsCmd.Flags().Bool("exclude-muted", false, "是否排除已免打扰会话(默认 false)")
|
||||
|
||||
@@ -4515,8 +4577,8 @@ status 可选值:
|
||||
chatMessageUnsetTopMsgCmd.Flags().String("msg-id", "", "消息 openMessageId (必填)")
|
||||
_ = chatMessageUnsetTopMsgCmd.MarkFlagRequired("msg-id")
|
||||
|
||||
chatGroupCmd.AddCommand(chatGroupBotsCmd, chatGroupDismissCmd, chatGroupSetHistoryCmd, chatGroupListMyGroupsCmd)
|
||||
chatGroupMembersCmd.AddCommand(chatGroupMembersRemoveBotCmd)
|
||||
// group 与 members 的子命令在下方(chatGroupCmd.AddCommand / chatGroupMembersCmd.AddCommand
|
||||
// 的完整列表处)统一注册;此处不再重复 AddCommand,否则 --help 会重复列出。
|
||||
// ── group update-nick: 设置用户在群内的群昵称 ──────────────
|
||||
|
||||
chatGroupUpdateNickCmd := &cobra.Command{
|
||||
|
||||
@@ -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 列表,逗号分隔 (必填)")
|
||||
|
||||
@@ -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 分钟)仍未完成,请稍后再试")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
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 "<内容>"` |
|
||||
|
||||
@@ -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 表达式过滤输出 ⚠️**当前为 no-op,不生效**:仍返回完整 JSON,不要依赖它过滤,改在拿到 JSON 后自行处理 | 无 |
|
||||
| `--fields` | | 筛选输出字段 ⚠️**当前为 no-op,不生效**:仍返回全量字段 | 无 |
|
||||
| `--verbose` | `-v` | 详细日志 | false |
|
||||
| `--debug` | | 调试日志 | false |
|
||||
| `--yes` | `-y` | 跳过确认提示 | false |
|
||||
|
||||
@@ -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` 代替** —— 该命令组只能查/审/撤已存在的审批单,考勤业务审批单走考勤自己的逻辑便于区分。
|
||||
|
||||
|
||||
@@ -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 PUBLIC/ORG`)/ `--enabled false` 关闭;ORG 回显 `shareType="[1]"` |
|
||||
|
||||
## chart 子命令
|
||||
|
||||
| 命令 | 用途 | 必填参数 |
|
||||
|------|------|----------|
|
||||
| `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` |
|
||||
| `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` |
|
||||
| `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` |
|
||||
| `chart widgets-example` | 查看图表 widgets 配置模板 | 无 |
|
||||
| `chart share get` | 获取图表分享配置 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart share update` | 更新图表分享配置(`--enabled true/false`,开启配 `--share-type PUBLIC/ORG`) | `--base-id` `--dashboard-id` `--chart-id` `--enabled` |
|
||||
|
||||
> `chart create` 的 `--layout` 是**必填**(12 列网格布局,如 `{"x":0,"y":0,"w":6,"h":4}`);不传本地校验直接拒。`chart update` 的 `--config` 也**必填**——即便只想改 layout,也要带完整 config,否则服务端拒绝。
|
||||
|
||||
## 配置获取流程
|
||||
|
||||
|
||||
@@ -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 的命令均适用**
|
||||
>
|
||||
@@ -143,21 +143,20 @@ Usage:
|
||||
dws attendance schedule import [flags]
|
||||
Example:
|
||||
dws attendance schedule import --group-id 123456 \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \
|
||||
--yes
|
||||
Flags:
|
||||
--group-id string 考勤组(必填,传入考勤组ID)
|
||||
--schedules string 排班记录 JSON 数组(必填)
|
||||
--yes 跳过确认提示
|
||||
--group-id string 考勤组(必填,传入考勤组ID);--help 主名为 --groupId,--group-id 为别名
|
||||
--schedules string 排班记录 JSON 数组(必填);--help 主名为 --scheduleVOS,--schedules 为别名
|
||||
--yes 跳过确认提示;--help 主名为 --user-say-yes,--yes 为别名
|
||||
```
|
||||
|
||||
为排班制考勤组导入排班记录。`--schedules` 为 JSON 数组,每条记录包含:
|
||||
- `userId`: 员工ID
|
||||
- `classId`: 班次ID
|
||||
- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22
|
||||
- `checkBeginTime`: 开始打卡时间
|
||||
- `checkEndTime`: 结束打卡时间
|
||||
- `isRest`: 是否休息日 Y/N(可选)
|
||||
- `userId`: 员工ID(必填)
|
||||
- `classId`: 班次ID(必填)
|
||||
- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22(必填)
|
||||
- `isRest`: 是否休息日 Y/N(**必填**,服务端要求传入)
|
||||
- `checkBeginTime` / `checkEndTime`: 开始/结束打卡时间(可传,但当前不会进入后端 payload,实际打卡时段以 `classId` 对应班次为准)
|
||||
|
||||
#### AI 调用 `schedule import` 的二次确认流程
|
||||
|
||||
@@ -305,7 +304,7 @@ Flags:
|
||||
--adjustment-id int 补卡规则主键 ID (必填)
|
||||
```
|
||||
|
||||
根据补卡规则主键 ID 查询对应的补卡规则详情。主键 ID 可从 `adjustment search` 返回结果中提取,也有可能来源于用户手动输入。**注意:已被删除或被更新覆盖的补卡规则无法查询到。**
|
||||
**注意:本命令当前拿不到有效的补卡规则详情,不要依赖它。** 服务端对任意 `--adjustment-id`(含不存在的 ID)都返回同一个 `{"success":true,"有效期类型":"..."}`,不返回规则明细,也无法据此判断规则是否存在;且 `adjustment search` 返回的默认补卡规则 `entityVO.id` 为 `null`,`search → get` 取 id 的链路走不通。补卡规则内容请直接看 `adjustment search` 的返回结果。
|
||||
|
||||
### 分页查询加班规则,支持按名称搜素
|
||||
```
|
||||
@@ -580,12 +579,17 @@ Flags:
|
||||
Usage:
|
||||
dws attendance summary [flags]
|
||||
Example:
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00"
|
||||
dws attendance summary --user USER_ID --date 2026-03-12 --stats-type week
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type month
|
||||
Flags:
|
||||
--date string 工作日期, 格式 yyyy-MM-dd HH:mm:ss (必填)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
--date string 查询日期, 格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss (必填)
|
||||
--stats-type string 统计类型: week 周统计 / month 月统计 (必填)
|
||||
--tag-name string 标签名称 (可选)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
```
|
||||
|
||||
`summary` 必须同时传 `--user`、`--date`、`--stats-type`,缺一即报错(如 C0002)。`--stats-type` 只能是 `week`(周统计)或 `month`(月统计)。
|
||||
|
||||
### 查询考勤组与考勤规则
|
||||
```
|
||||
Usage:
|
||||
@@ -847,10 +851,10 @@ Example:
|
||||
dws attendance vacation balance --users userId1,userId2 --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
||||
Flags:
|
||||
--users string 目标员工 ID 列表, 逗号分隔 (必填)
|
||||
--leave-code string 假期规则 code (选填,不传则查询所有假期规则余额)
|
||||
--leave-code string 假期规则 code (必填,服务端要求非空,不传返回 INVALID_PARAMS)
|
||||
```
|
||||
|
||||
调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取;不传 `--leave-code` 时查询所有假期规则余额。认证信息(corpId、opUserId)由系统自动注入。
|
||||
调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取。**注意:`--help` 虽标"选填",但服务端要求 `leaveCode` 非空,实际必填;不传会返回 `INVALID_PARAMS`(corpId、opUserId、leaveCode、targetUserIds 不能为空)。** 若要一次查所有假期规则余额,必须走 [attendance-vacation.md](./attendance-vacation.md) 工作流脚本逐个规则遍历。认证信息(corpId、opUserId)由系统自动注入。
|
||||
|
||||
如用户需要“所有假期规则余额 / 导出假期余额列表 / 所有假期规则余额 Excel / 按截图样式导出假期余额”,必须先读取 [attendance-vacation.md](./attendance-vacation.md),再按其中工作流调用脚本生成 Excel。
|
||||
|
||||
@@ -867,7 +871,7 @@ Flags:
|
||||
--end string 查询结束日期, 格式 YYYY-MM-DD (必填)
|
||||
```
|
||||
|
||||
调用 MCP 工具 get_leave_balance_records 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。
|
||||
调用 MCP 工具 get_leave_balance_records_v2 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 必填,可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。
|
||||
|
||||
### 更新假期规则(写场景接口,必须走二次确认流程)
|
||||
|
||||
@@ -1002,7 +1006,7 @@ Usage:
|
||||
dws attendance checkin records [flags]
|
||||
Example:
|
||||
dws attendance checkin records \
|
||||
--operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00"
|
||||
--operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00"
|
||||
Flags:
|
||||
--end string 结束时间, 格式 yyyy-MM-dd HH:mm:ss(必填)
|
||||
--operator-corp-id string 操作者企业 ID(必填)
|
||||
@@ -1032,7 +1036,7 @@ Flags:
|
||||
用户说"班次详情/某个班次的具体信息" → `class search --name "..."`(search 直出,直接返回详情)。`class get` 仅在需要按已知 classId 精确查询时使用
|
||||
用户说"更新班次/修改班次/班次改名/修改上下班时间" → `class update`
|
||||
用户说"补卡规则/补卡设置" → `adjustment search`(返回结果已包含全量属性,无需再调 get)
|
||||
用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出)。`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用
|
||||
用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出,已含全量属性)。**不要用 `adjustment get`**:当前服务端对任意 id 都只返回"有效期类型"、拿不到规则明细
|
||||
用户说"加班规则/加班设置/加班计算" → `overtime search`(返回结果已包含全量属性,无需再调 get)
|
||||
用户说"加班规则详情/某条加班规则的具体信息" → `overtime search --name "..."`(search 直出)。如需查已删除/被覆盖的历史记录 → `overtime get`
|
||||
用户说"考勤组列表/有哪些考勤组" → `group search`
|
||||
@@ -1068,7 +1072,7 @@ Flags:
|
||||
```bash
|
||||
# 导入排班记录
|
||||
dws attendance schedule import --group-id 123456 \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \
|
||||
--yes --format json
|
||||
|
||||
# 获取排班记录 — 禁止直接调用,必须走 attendance-schedule.md 排班查询导出工作流
|
||||
@@ -1123,8 +1127,8 @@ dws attendance group update --group-id 123456 --group-vo '{"positions":[{"title"
|
||||
dws attendance group create --name "研发考勤组" --type FIXED --group-vo '{"defaultClassId":1170996821,"workDayClassList":[0,1170996821,0,0,0,0,0]}' --timeout 10 --format json
|
||||
dws attendance group create --name "自由工时分组" --type NONE --timeout 10 --format json
|
||||
|
||||
# 查看考勤统计摘要
|
||||
dws attendance summary --user <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
|
||||
@@ -1202,7 +1206,7 @@ dws attendance vacation save-balance --target user001 \
|
||||
--num 8 --reason "绩效奖励发放3天" --format json
|
||||
|
||||
# 查询签到记录
|
||||
dws attendance checkin records --operator-staff-id op001 --staff-ids user001,user002 \
|
||||
dws attendance checkin records --operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 \
|
||||
--start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00" --format json
|
||||
```
|
||||
|
||||
@@ -1233,7 +1237,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use
|
||||
- `class get` 的 `--class-id` 必填,班次 ID 可从 `class search` 结果中提取
|
||||
- `class search` 返回结果已包含全量属性,无需再调用 `class get`;`class get` 仅在需要按已知 classId 精确查询时使用
|
||||
- `class update` 的 `--class-id` 必填,其余均可选,仅需对要修改的字段赋值,未传字段会自动从已有配置补充;由于保存班次耗时较久,建议加 `--timeout 10`
|
||||
- `adjustment search` 返回结果已包含全量属性,无需再调用 `adjustment get`;`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用
|
||||
- `adjustment search` 返回结果已包含全量属性;`adjustment get` 当前服务端对任意 id 都只返回"有效期类型"、无规则明细,不可用,补卡规则详情一律看 `adjustment search` 返回
|
||||
- `overtime search` 返回结果已包含全量属性,无需再调用 `overtime get`;`overtime get` 仅在需要按已知 overtimeId 查询时使用(包括已删除/被覆盖的历史记录)
|
||||
- `adjustment search` / `overtime search` 分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20
|
||||
- `group search` 的分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20
|
||||
@@ -1242,7 +1246,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use
|
||||
- `group update` 的 --group-id 必填,其余均可选,至少需指定一个修改项;仅需对要修改的字段赋値,未传字段会从已有配置自动补充;修改打卡地址/wifi/蓝牙等复杂子对象时用 `--group-vo` 传入完整 JSON;`--group-vo` 与单字段 flag 同时传入时单字段 flag 优先级更高
|
||||
- `group create` 的 `--name` 和 `--type` 必填,`--type` 必须为 FIXED/TURN/NONE 之一;type=FIXED 时 `--group-vo` 必须包含 `workDayClassList`(非空)和 `defaultClassId`(非 null);由于保存考勤组耗时较久,建议加 `--timeout 10`
|
||||
- `group filtered-get` 的 `--group-id` 必填,`--member/--position/--wifi/--bles` 均可选,默认 false。**返回结果中如含成员 userId 列表,必须调用 `dws contact user get --ids <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 +1259,8 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use
|
||||
- 用户 ID 需从 `contact user get-self` 或 `aisearch person` 获取
|
||||
- 考勤组 ID 需从 `rules` 命令返回结果中获取
|
||||
- `vacation types` 无需任何参数,认证信息自动注入
|
||||
- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 选填,可通过 `vacation types` 获取
|
||||
- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 选填
|
||||
- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 服务端要求非空、实际必填(`--help` 标"选填"不准),不传返回 `INVALID_PARAMS`,可通过 `vacation types` 获取
|
||||
- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 必填(不传无法查询),底层 MCP 工具为 `get_leave_balance_records_v2`
|
||||
- `vacation balance` 和 `vacation records` 的认证参数(corpId、opUserId)由系统自动注入,无需手动传入
|
||||
- `vacation update-type` 的 `--leave-code` 必填;其他字段均为可选,但至少需传一个更新字段
|
||||
- `vacation update-type` 的 `--visibility-rules` 为 JSON 数组字符串,格式:`[{"type":"dept","visible":["1","2","3"]}]`,type 可取值 staff/label/dept
|
||||
|
||||
@@ -64,6 +64,7 @@ dws calendar room add [flags]
|
||||
dws calendar room delete [flags]
|
||||
```
|
||||
> room是会议室,用于线下开会场景。
|
||||
> **组织限制**:部分企业使用自建会议室系统,未接入钉钉会议室能力。此时 `room search` / `room list-groups` 会返回业务错误 `400056`("所选组织不支持预定钉钉会议室")。这是组织级配置限制,不是命令用法问题;应直接告知用户该组织需在钉钉客户端手动预订会议室,不要重试或换参。
|
||||
|
||||
### busy 相关三级子命令
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
#### 拉取指定时间范围内当前用户的所有会话消息 — 分页拉取当前登录用户在指定时间范围内的所有会话消息
|
||||
@@ -734,8 +798,10 @@ Usage:
|
||||
Example:
|
||||
dws chat message list-unread-conversations
|
||||
dws chat message list-unread-conversations --count 20
|
||||
dws chat message list-unread-conversations --exclude-muted
|
||||
Flags:
|
||||
--count int 返回未读会话条数(可选)
|
||||
--count int 返回未读会话条数(可选)
|
||||
--exclude-muted 是否排除已设置免打扰的会话(默认 false)
|
||||
```
|
||||
|
||||
#### 查询消息的已读/未读状态
|
||||
@@ -942,6 +1008,7 @@ Example:
|
||||
Flags:
|
||||
--limit int 每页返回数量(默认 1000)
|
||||
--cursor int64 分页游标(首次不传或传 0,翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已设置免打扰的会话(默认 false)
|
||||
|
||||
注意:
|
||||
- 用户询问"置顶会话"时,直接调用此命令返回置顶会话列表即可
|
||||
@@ -991,6 +1058,7 @@ Flags:
|
||||
--match-mode string 匹配模式:AND=所有人都在群里,OR=任一人在群里(默认 AND)
|
||||
--limit int 每页返回数量(默认 20)
|
||||
--cursor string 分页游标(默认 "0",翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已设置免打扰的群聊(默认 false)
|
||||
|
||||
注意:
|
||||
- --nicks 传人员昵称(花名),逗号分隔,如 "风雷,山乔"
|
||||
@@ -1022,6 +1090,76 @@ Flags:
|
||||
- 上传到共享空间的文件对方才能打开,上传到个人空间的文件对方无法访问
|
||||
```
|
||||
|
||||
#### 引用回复消息 — 引用某条消息并回复文字(单聊/群聊均可)
|
||||
```
|
||||
Usage:
|
||||
dws chat message reply [flags]
|
||||
Example:
|
||||
dws chat message reply --conversation-id <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`
|
||||
|
||||
@@ -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
|
||||
@@ -186,8 +187,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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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)。
|
||||
|
||||
### 复制文档/文件
|
||||
```
|
||||
@@ -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 写入」)。
|
||||
|
||||
@@ -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` 参数值。
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
### 查看邮件完整内容
|
||||
@@ -222,17 +222,17 @@ Usage:
|
||||
dws mail message send [flags]
|
||||
Example:
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "周报" --body "本周完成任务A和任务B"
|
||||
--subject "周报" --content "本周完成任务A和任务B"
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "周报" --body "见附件" --attachment ./report.pdf
|
||||
--subject "周报" --content "见附件" --attachment ./report.pdf
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "周报" --body "见附件" --attachment ./a.pdf --attachment ./b.xlsx
|
||||
--subject "周报" --content "见附件" --attachment ./a.pdf --attachment ./b.xlsx
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "图表周报" --body "图表如下:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
--subject "图表周报" --content "图表如下:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
dws mail message send --from user@company.com --to colleague@company.com \
|
||||
--subject "带图文档" --body "见附件,图表:[inline:img.png]" --attachment ./doc.pdf --inline-attachment ./img.png
|
||||
--subject "带图文档" --content "见附件,图表:[inline:img.png]" --attachment ./doc.pdf --inline-attachment ./img.png
|
||||
Flags:
|
||||
--body string 邮件正文 (必填)
|
||||
--content string 邮件正文 (必填)
|
||||
--cc string 抄送人列表
|
||||
--from string 发件人邮箱 (必填),别名: --sender
|
||||
--subject string 邮件标题 (必填)
|
||||
@@ -256,7 +256,7 @@ Flags:
|
||||
|
||||
- 仅支持图片类型:`jpg` / `jpeg` / `png` / `gif` / `webp` / `bmp` / `svg`
|
||||
- CLI 自动生成 contentId,格式:`inline-{文件名(不含扩展名)}-{序号}@alimail.com`,例:`inline-chart-1@alimail.com`
|
||||
- 在 `--body` 中使用占位符 `[inline:文件名]` 引用图片,CLI 自动替换为 `<img src="cid:...">` 标签
|
||||
- 在 `--content` 中使用占位符 `[inline:文件名]` 引用图片,CLI 自动替换为 `<img src="cid:...">` 标签
|
||||
- 若 body 中没有对应占位符,内联图片会自动追加到正文末尾
|
||||
- 非图片类型(PDF、视频、音频等)请改用 `--attachment`
|
||||
|
||||
@@ -266,13 +266,13 @@ Usage:
|
||||
dws mail folder list [flags]
|
||||
Example:
|
||||
dws mail folder list --email user@company.com
|
||||
dws mail folder list --email user@company.com --folder-id <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 +351,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -374,8 +373,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -401,7 +399,7 @@ Flags:
|
||||
|------|------|------|
|
||||
| `id` | `string` | 附件唯一标识 |
|
||||
| `name` | `string` | 附件文件名 |
|
||||
| `contentType` | `string` | 附件 MIME 类型 |
|
||||
| `isInline` | `bool` | 是否为内联附件(`true`=正文内联图片,`false`=普通附件) |
|
||||
| `size` | `int` | 附件大小(字节) |
|
||||
|
||||
### 下载邮件附件
|
||||
@@ -521,8 +519,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -546,8 +543,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -627,8 +623,10 @@ Flags:
|
||||
| `senders` | `List[{email, name}]` | 会话发件人列表 |
|
||||
| `isRead` | `boolean` | 会话是否已读(全部已读/未读) |
|
||||
| `priority` | `string` | 会话重要性,取会话内邮件最高优先级(`PRY_HIGH` / `PRY_NORMAL`) |
|
||||
| `flag` | `string` | 会话标识,取会话内最近邮件的标识(`FLAG_NONE` / `FLAG_REPLY` / `FLAG_FORWARD`) |
|
||||
| `hasAttachments` | `boolean` | 会话是否包含附件(不含 inline 资源) |
|
||||
| `messages` | `List[]` | 会话内的邮件列表;默认即返回,但各邮件字段(正文、收件人等)多为 null,需在 `--select` 中额外指定才有值 |
|
||||
|
||||
> **注意:** `thread get` 返回的 `conversation` **不含 `flag` 字段**(`flag` 仅出现在 `thread list` 的会话项中)。会话标识请从 `thread list` 获取。
|
||||
|
||||
### 修改邮件会话状态
|
||||
|
||||
@@ -660,8 +658,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -686,8 +683,7 @@ Flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -749,13 +745,13 @@ Usage:
|
||||
dws mail message reply [flags]
|
||||
Example:
|
||||
dws mail message reply --from user@company.com --id <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 +777,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 +815,7 @@ Flags:
|
||||
--to string 转发收件人列表(可选)
|
||||
--id string 要转发的邮件 ID (必填)
|
||||
--subject string 转发邮件标题(可选)
|
||||
--body string 转发附言(可选)
|
||||
--content string 转发附言(可选)
|
||||
--attachment stringArray 附件文件路径,可多次指定 (可选)
|
||||
--inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选)
|
||||
```
|
||||
@@ -922,7 +918,9 @@ Flags:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `message` | `object` | 邮件完整信息 |
|
||||
| `sendStatus` | `string` | 发送状态,取值见下表 |
|
||||
| `message.sendStatus` | `string` | 发送状态,**嵌在 `message` 对象内部**(非顶层字段),取值见下表 |
|
||||
|
||||
> **注意:** 顶层只有 `message` 和 `success` 两个字段,`sendStatus` 位于 `message.sendStatus`,不是与 `message` 平级的顶层字段。
|
||||
|
||||
**`sendStatus` 取值说明:**
|
||||
|
||||
@@ -941,57 +939,57 @@ Usage:
|
||||
dws mail draft create [flags]
|
||||
Example:
|
||||
dws mail draft create --from user@company.com --to colleague@company.com \
|
||||
--subject "草稿标题" --body "草稿正文"
|
||||
--subject "草稿标题" --content "草稿正文"
|
||||
dws mail draft create --from user@company.com --subject "草稿标题"
|
||||
dws mail draft create --from user@company.com --subject "带附件草稿" \
|
||||
--body "见附件" --attachment ./report.pdf
|
||||
--content "见附件" --attachment ./report.pdf
|
||||
dws mail draft create --from user@company.com --subject "带图片草稿" \
|
||||
--body "图表:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
--content "图表:[inline:chart.png]" --inline-attachment ./chart.png
|
||||
Flags:
|
||||
--from string 发件人邮箱 (必填),别名: --sender
|
||||
--subject string 邮件标题 (必填)
|
||||
--to string 收件人列表(可选,有确定收件人时才传)
|
||||
--cc string 抄送人列表(可选,有确定抄送人时才传)
|
||||
--body string 邮件正文(可选,有正文内容时才传)
|
||||
--content string 邮件正文(可选,有正文内容时才传)
|
||||
--attachment stringArray 附件文件路径,可多次指定 (可选)
|
||||
--inline-attachment stringArray 内联图片路径,可多次指定,cid 自动生成 (可选)
|
||||
```
|
||||
|
||||
> **注意:** `--to`、`--cc`、`--body` 均为可选参数,**仅在用户明确提供对应信息时才传入**。若用户未指定收件人,不要传 `--to ""`(空字符串)。
|
||||
> **注意:** `--to`、`--cc`、`--content` 均为可选参数,**仅在用户明确提供对应信息时才传入**。若用户未指定收件人,不要传 `--to ""`(空字符串)。
|
||||
|
||||
**附件说明:**
|
||||
|
||||
指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿创建和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--body` 中使用 `[inline:文件名]` 占位符)。
|
||||
指定 `--attachment` 或 `--inline-attachment` 时,CLI 自动完成草稿创建和附件上传,**草稿保留在草稿箱,不会发送**。内联图片用法同 `message send`(`--content` 中使用 `[inline:文件名]` 占位符)。
|
||||
|
||||
**返回字段:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `messageId` | `string` | 新建草稿的邮件 ID |
|
||||
| `result.message.id` | `string` | 新建草稿的邮件 ID(**嵌在 `result.message` 内**,无顶层 `messageId` 字段);`result.message` 还含 `internetMessageId`、`folderId`(草稿箱为 `5`)、`subject`、`from` 等 |
|
||||
|
||||
### 更新草稿
|
||||
```
|
||||
Usage:
|
||||
dws mail draft update [flags]
|
||||
Example:
|
||||
dws mail draft update --from user@company.com --id <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 +1011,14 @@ Usage:
|
||||
Example:
|
||||
dws mail user search --keyword "张三"
|
||||
dws mail user search --email user@company.com --keyword "张三"
|
||||
dws mail user search --email user@company.com --keyword "alice" --size 10
|
||||
dws mail user search --email user@company.com --keyword "alice" --limit 10
|
||||
dws mail user search --email user@company.com --keyword "alice" --cursor <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 +1068,10 @@ Flags:
|
||||
```
|
||||
|
||||
> **草稿模板说明:** 传入 `--is-draft` 创建的模板为草稿模板,草稿模板支持后续通过 `template update` 修改内容。**非草稿模板创建后不可修改**(`template update` 仅对草稿模板有效)。
|
||||
>
|
||||
> **草稿模板不出现在 `template list`(重要):** 实测 `template list` **只返回非草稿模板**;草稿模板(`--is-draft`)不在列表里,只能用 `template get --id <模板ID>` 直接获取。因此**不要用 `template list` 是否出现来判断草稿模板是否创建成功**——`template create` 返回 `success:true` 即已创建。
|
||||
>
|
||||
> **模板 ID 前缀区分类型:** `template create` 返回的 `id` 前缀标识类型——非草稿模板为 `1:0:` 开头(如 `1:0:8bbeea56-...`),草稿模板为 `0:0:` 开头(如 `0:0:e3e2d134-...`)。
|
||||
|
||||
### 列举邮件模板
|
||||
```
|
||||
@@ -1241,7 +1244,7 @@ Flags:
|
||||
| `rules[].enabled` | bool | 是否启用 |
|
||||
| `rules[].conditions` | List[] | 规则条件列表 |
|
||||
| `rules[].actions` | List[] | 规则动作列表 |
|
||||
| `rules[].order` | int | 规则排序 |
|
||||
| `rules[].order` | null | 排序字段;实测存量与新建规则该字段**恒为 `null`**,不返回有效排序值,不要依赖它判断规则顺序 |
|
||||
|
||||
#### 创建收信规则
|
||||
|
||||
@@ -1366,10 +1369,10 @@ Flags:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 创建是否成功 |
|
||||
| `errorCode` | string | 错误码 |
|
||||
| `errorMsg` | string | 错误消息 |
|
||||
| `id` | string | 新建规则 ID |
|
||||
|
||||
> 实测 `rule create` 成功仅返回 `{id, success}`,**不返回** `errorCode` / `errorMsg`。
|
||||
|
||||
#### 更新收信规则
|
||||
|
||||
更新已有的收信规则。**除 `--conditions` 外所有参数均为必填**。
|
||||
@@ -1401,9 +1404,9 @@ Flags:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 更新是否成功 |
|
||||
| `errorCode` | string | 错误码 |
|
||||
| `errorMsg` | string | 错误信息 |
|
||||
| `result` | object | 更新结果 |
|
||||
| `result` | object | 更新结果(实测为空对象 `{}`) |
|
||||
|
||||
> 实测 `rule update` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。
|
||||
|
||||
#### 删除收信规则
|
||||
|
||||
@@ -1424,9 +1427,9 @@ Flags:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 删除是否成功 |
|
||||
| `errorCode` | string | 错误码 |
|
||||
| `errorMsg` | string | 错误信息 |
|
||||
| `result` | object | 删除结果 |
|
||||
| `result` | object | 删除结果(实测为空对象 `{}`) |
|
||||
|
||||
> 实测 `rule delete` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。
|
||||
|
||||
#### 调整收信规则排序
|
||||
|
||||
@@ -1449,9 +1452,9 @@ Flags:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 调整是否成功 |
|
||||
| `errorCode` | string | 错误码 |
|
||||
| `errorMsg` | string | 错误消息 |
|
||||
| `result` | object | 调整结果 |
|
||||
| `result` | object | 调整结果(实测为空对象 `{}`) |
|
||||
|
||||
> 实测 `rule adjust` 成功仅返回 `{result:{}, success}`,**不返回** `errorCode` / `errorMsg`。
|
||||
|
||||
## 通用错误说明
|
||||
|
||||
@@ -1559,29 +1562,29 @@ dws mail thread batch-trash --email user@company.com --ids <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 +1597,7 @@ dws mail attachment download --email user@company.com \
|
||||
# 5. 获取邮件所属会话详情(thread)
|
||||
# 步骤 5.1:先通过 message search 或 message get 获取邮件中的 conversationId
|
||||
dws mail message search --email user@company.com \
|
||||
--query "subject:\"周报\"" --size 5 --format json
|
||||
--query "subject:\"周报\"" --limit 5 --format json
|
||||
# 从返回的邮件列表中提取 conversationId 字段
|
||||
|
||||
# 步骤 5.2:用 conversationId 获取会话详情
|
||||
@@ -1637,8 +1640,8 @@ dws mail thread get --email user@company.com --id <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`**
|
||||
>
|
||||
@@ -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)字段**。
|
||||
|
||||
**发言人列表输出规范(查询详情后必须执行)**:
|
||||
|
||||
|
||||
@@ -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` 获取。
|
||||
|
||||
### 对审批任务进行加签
|
||||
|
||||
@@ -320,8 +321,8 @@ Flags:
|
||||
用户说"我审批/处理过的审批单" -> `approval list-executed`
|
||||
用户说"抄送我的审批单" -> `approval list-cc`
|
||||
用户说"转交审批/转交任务" → `approval redirect-task`(需 --task-id 和 --to-actioner-id)
|
||||
用户说"评论审批/添加评论/写评论" → `approval oa-comments`(需 --instance-id 和 --text)
|
||||
用户说"抄送审批/添加抄送人" → `approval oa-cc-noticer`(需 --instance-id 和 --user-list)
|
||||
用户说"评论审批/添加评论/写评论" → `approval oa-comments`(需 --instance-id 和 --content)
|
||||
用户说"抄送审批/添加抄送人" → `approval oa-cc-noticer`(需 --instance-id 和 --users)
|
||||
|
||||
## 核心工作流
|
||||
|
||||
@@ -348,12 +349,12 @@ dws oa approval revoke --instance-id <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)
|
||||
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
|
||||
```
|
||||
|
||||
## 上下文传递表
|
||||
@@ -391,6 +392,7 @@ dws oa approval oa-cc-noticer --instance-id <processInstanceId> --user-list "use
|
||||
- `revoke` 只能撤销自己发起的审批
|
||||
- `--remark` 审批意见虽为可选,但建议填写以留存审批痕迹
|
||||
- `list-initiated` 的 `--process-code` 可从 `list-forms` 或 `detail` 返回中提取
|
||||
- `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,10 +1572,10 @@ 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`**,告知用户稍后再试
|
||||
@@ -1510,6 +1583,29 @@ Flags:
|
||||
|
||||
**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。
|
||||
|
||||
### 表格模板(list / search / apply)
|
||||
```
|
||||
Usage:
|
||||
dws sheet template list [flags] # 列出可用模板(--cursor / --limit 分页)
|
||||
dws sheet template search [flags] # 按关键词搜索模板
|
||||
dws sheet template apply [flags] # 应用模板创建新表格文档
|
||||
|
||||
template search Flags:
|
||||
--query string 搜索关键词 (必填)
|
||||
--source string 模板来源: MY(我的模版,默认) / PUBLIC(公开模版)
|
||||
--cursor string 分页游标
|
||||
--limit int 返回数量上限
|
||||
|
||||
template apply Flags:
|
||||
--template-id string 模板 ID (必填,来自 list/search)
|
||||
--name string 新表格文档名称 (可选)
|
||||
--folder string 目标文件夹 ID (可选,UUID 或 URL)
|
||||
--workspace string 知识库 ID (可选)
|
||||
```
|
||||
|
||||
- 用户说"用模板建表 / 有哪些模板 / 找个 XX 模板"时走 template 系列
|
||||
- 典型链路:`template search --query "..."` 或 `template list` 拿到 `templateId` → `template apply --template-id <ID> --name "..."` 创建新表格,返回新文档的 `nodeId`
|
||||
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
@@ -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` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复
|
||||
|
||||
@@ -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,10 +47,10 @@ 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`**,告知用户稍后再试
|
||||
@@ -82,7 +82,7 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `export` | `downloadUrl`(未指定 --output)/ `导出完成: <path>`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 |
|
||||
| `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 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 "<姓名>"`)|
|
||||
|
||||
## 相关产品
|
||||
|
||||
|
||||
@@ -136,8 +136,8 @@ dws doc list --folder "https://alidocs.dingtalk.com/i/nodes/ghi789" --format jso
|
||||
| extension / contentType | 读取 | 写入 | 删除 | 导出 | 权限 | 媒体 |
|
||||
|-------------------------|------|------|------|------|------|------|
|
||||
| **adoc**(在线文档) | `doc read` | `doc update` / `doc block update` | ⚠️ `doc delete` | ⚠️ `doc export` (→ docx) | ⚠️ `doc permission *` | ⚠️ `doc media download/insert` |
|
||||
| **axls**(在线电子表格) | `sheet range read` / `sheet list` | `sheet range write` / `sheet append` | ⚠️ `doc delete`(节点删除) | `sheet submit_export_job` + `sheet query_export_job`(待吴淼 W-01 收敛为单命令 `sheet export`) | ⚠️ `doc permission *`(节点级,跨产品) | 不适用 |
|
||||
| **able**(在线多维表) | `aitable base get` / `aitable record query` | `aitable record create/update` | ⚠️ `doc delete`(节点删除)或 `aitable base delete --yes` | `aitable export data --output ./x.xlsx` | ⚠️ `doc permission *`(节点级) | `aitable attachment upload-file` |
|
||||
| **axls**(在线电子表格) | `sheet range read` / `sheet list` | `sheet range update` / `sheet append` | ⚠️ `doc delete`(节点删除) | `sheet export`(单命令一站式:提交→轮询→下载,可选 `--output` 落盘) | ⚠️ `doc permission *`(节点级,跨产品) | 不适用 |
|
||||
| **able**(在线多维表) | `aitable base get` / `aitable record query` | `aitable record create/update` | ⚠️ `doc delete`(节点删除)或 `aitable base delete --yes` | `aitable export data --scope all --format excel`(取 downloadUrl,`--output` 不落盘) | ⚠️ `doc permission *`(节点级) | `aitable attachment upload` |
|
||||
| **xlsx / xls / xlsm / csv**(本地表格文件) | `doc download` → 本地用 xlsx skill 解析 | 不支持服务端写(先下载改本地再上传) | ⚠️ `doc delete`(节点删除) | 不需要(本身就是 xlsx) | ⚠️ `doc permission *` | 不适用 |
|
||||
| **普通文件** (nodeType=file) | `doc download` | 不支持服务端写 | ⚠️ `doc delete` | 不需要 | ⚠️ `doc permission *` | 不适用 |
|
||||
| **文件夹** (nodeType=folder) | `doc list --folder <URL>` | `doc create --folder <URL> ...` | ⚠️ `doc delete` | 不适用 | ⚠️ `doc permission *` | 不适用 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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,21 @@
|
||||
>
|
||||
> 不支持 `formInfo`、`requiredFields`、`conditionalRules` 等 FormDesigner 高级配置,这些 key 会被服务端忽略。
|
||||
|
||||
### form (表单管理) → 详见 [aitable-form.md](./aitable/aitable-form.md)
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `form list` | 列出表单视图 | `--base-id` `--table-id` | 每条含 viewId/name;新建表单无 title 且 createdAt=0,改过后才有 |
|
||||
| `form get` | 按 viewId 取单个表单详情 | `--base-id` `--table-id` `--view-id` | 客户端按 viewId 过滤,`data` 即该表单对象;viewId 不存在报 not found |
|
||||
| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | 等价 `view create --view-type FormDesigner` |
|
||||
| `form update` | 更新表单配置 | `--base-id` `--table-id` `--view-id` | title/name/description 至少一项 |
|
||||
| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` | 不可逆 |
|
||||
| `form field list/update/hide` | 表单字段管理 | — | 详情见子文档 |
|
||||
| `form questions create/delete` | 题目管理(=field create/delete) | — | 详情见子文档 |
|
||||
| `form share get/update` | 表单分享配置 | — | 详情见子文档 |
|
||||
|
||||
> **创建表单**有两种等价方式:`form create --name "..."`(推荐)或 `view create --view-type FormDesigner --name "..."`。
|
||||
|
||||
### dashboard & chart → 详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)
|
||||
|
||||
| 命令 | 用途 |
|
||||
@@ -130,6 +158,8 @@
|
||||
- 修改/更新 → `record update`(读 [aitable-record-update.md](./aitable/aitable-record-update.md))
|
||||
- 删除 → `record delete`
|
||||
|
||||
用户说"表单/问卷/form/收集信息" → 读 [aitable-form.md](./aitable/aitable-form.md)
|
||||
|
||||
用户说"筛选/过滤/filter" → 读 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md)
|
||||
|
||||
用户说"统计/分析/聚合/TOP N/全量" → 读 [aitable-data-analysis-sop.md](./aitable/aitable-data-analysis-sop.md)
|
||||
|
||||
@@ -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 PUBLIC/ORG`)/ `--enabled false` 关闭;ORG 回显 `shareType="[1]"` |
|
||||
|
||||
## chart 子命令
|
||||
|
||||
| 命令 | 用途 | 必填参数 |
|
||||
|------|------|----------|
|
||||
| `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` |
|
||||
| `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` |
|
||||
| `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` |
|
||||
| `chart widgets-example` | 查看图表 widgets 配置模板 | 无 |
|
||||
| `chart share get` | 获取图表分享配置 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart share update` | 更新图表分享配置(`--enabled true/false`,开启配 `--share-type PUBLIC/ORG`) | `--base-id` `--dashboard-id` `--chart-id` `--enabled` |
|
||||
|
||||
> `chart create` 的 `--layout` 是**必填**(12 列网格布局,如 `{"x":0,"y":0,"w":6,"h":4}`);不传本地校验直接拒。`chart update` 的 `--config` 也**必填**——即便只想改 layout,也要带完整 config,否则服务端拒绝。
|
||||
|
||||
## 配置获取流程
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: dingtalk-attendance
|
||||
description: 钉钉考勤(只读)。Use when 用户说 考勤/打卡记录/查打卡/查班次/考勤汇总/考勤规则/出勤情况。命令前缀:dws attendance。开源版仅支持只读查询,不支持创建班次、导入排班、修改考勤组等写操作。
|
||||
description: 钉钉考勤。Use when 用户说 考勤/打卡记录/查打卡/查班次/考勤汇总/考勤规则/出勤情况/请假加班补卡/排班/考勤组/假期余额。命令前缀:dws attendance。支持查询与写操作(班次、排班、考勤组、个人规则设置、假期规则/余额等),写操作需二次确认后再加 --yes。
|
||||
cli_version: ">=0.2.14"
|
||||
metadata:
|
||||
category: product
|
||||
@@ -30,11 +30,16 @@ metadata:
|
||||
| "查我 / 某人 某天的打卡" | `dws attendance record get --user <userId> --date <YYYY-MM-DD>` |
|
||||
| "查考勤组 / 考勤规则 / 打卡范围" | `dws attendance rules --date <YYYY-MM-DD>` |
|
||||
| "查班次 / 排班 / 谁今天上什么班" | `dws attendance shift list --users <userId1,userId2> --start <YYYY-MM-DD> --end <YYYY-MM-DD>` |
|
||||
| "考勤统计 / 周月汇总 / 出勤天数" | `dws attendance summary --user <userId> --date "<yyyy-MM-dd HH:mm:ss>" --stats-type week\|month` |
|
||||
| "考勤统计 / 周月汇总 / 出勤天数" | `dws attendance summary --user <userId> --date <YYYY-MM-DD> --stats-type week\|month` |
|
||||
| "请假 / 加班 / 补卡 / 出差 记录或提交链接" | `dws attendance approve list` / `dws attendance approve templates --type <类型>` |
|
||||
| "班次 / 补卡规则 / 加班规则 / 考勤组 查询与修改" | `class` / `adjustment` / `overtime` / `group` 子命令 |
|
||||
| "排班 / 假期规则 / 假期余额 / 签到 / 报表" | `schedule` / `vacation` / `checkin` / `report` 子命令 |
|
||||
|
||||
> 完整命令集(含写操作与参数)见 [references/attendance.md](references/attendance.md)。
|
||||
|
||||
## 评测高频硬约束
|
||||
|
||||
- 开源版考勤只有 `record / rules / shift / summary` 四个只读命令;用户提到创建班次、导入排班、加人入考勤组等写操作时,直接告知"开源版不支持",不要伪装成功。
|
||||
- 当前 dws 已注册全部考勤子命令组(`record` / `check` / `approve` / `shift` / `schedule` / `class` / `adjustment` / `overtime` / `group` / `summary` / `rules` / `selfsetting` / `globalsetting` / `vacation` / `checkin` / `report` / `boss-check`),查询与写操作大多可直接调用后端。**不要再以"开源版只读/不支持写操作"为由拒答。** 创建班次、导入排班、加人入考勤组、保存个人规则设置、设置假期余额等写操作真机可执行,但必须先展示参数摘要并二次确认,再追加 `--yes`(或 `--user-say-yes`)执行;不要在未确认时直接写、也不要伪装成功。个别命令受权限/数据影响返回空或权限错误(如 `report` 系列仅管理员),如实说明即可。
|
||||
- 查询迟到/缺勤名单时,空打卡结果不等于"没人迟到"。必须结合排班、`NotSigned`、`Absenteeism`、无记录人员分别说明;数据缺失要标为"无记录/无法判断",不要归为正常。
|
||||
- 做部门 Top N 排名时,用户要求前 N 名就必须输出 N 个部门;无打卡记录或无可计算数据的部门按 0 或"无数据"保留在排名中,不能只输出有数据的少数部门。
|
||||
- `summary` 必须同时传 `--user`、`--date`、`--stats-type`(week/month),缺一返回 C0002。
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
# 考勤报表导出参考 (attendance-report)
|
||||
|
||||
> 本文档由 `attendance.md` 路由调用。当用户提到"考勤报表"、"导出考勤"、"出勤汇总"、"考勤明细"、"迟到早退统计"、"全员考勤数据"、"某月考勤统计"、"考勤表格"、"考勤 Excel" 时,应阅读本文档执行。
|
||||
> 不适用于:个人单日打卡查询(用 `attendance check record`)、班次查询(用 `attendance schedule get`)、假期余额(用 `vacation balance`)、审批进度(用 `oa`)。
|
||||
|
||||
## 强制门禁(必须先读完本文档才能执行)
|
||||
|
||||
**任何调用 `attendance_report_detail.py` / `attendance_report_monthly.py` / `attendance_report_daily.py` 的请求,都必须经过本文档定义的工作流,严禁绕过本文档直接拼脚本命令执行。**
|
||||
|
||||
违反将出现以下任一问题:
|
||||
1. 未按"阶段 1"做人员解析 → `--users` 传入部门 ID 而非员工 userId,脚本虽内置回退但会浪费一次失败的接口调用
|
||||
2. 未按"阶段 0"判断报表类型 → 用户说"汇总"被理解成"明细",导致输出粒度错误
|
||||
3. 未按"列选择"判断是否传 `--column-keywords` → 用户要"迟到情况报表"被输出成全字段默认报表
|
||||
4. 未按"错误处理"规则处理无权限错误(errorCode 6001「无权限操作」/ `AUTH_ERROR`)/ `HSF_ILLEGALPARAMS` → 把环境错误当成业务错误反馈给用户
|
||||
5. 未按"阶段 4"返回结果 → 把 Excel 内容贴在对话里,或者裸 userId 直接输出
|
||||
|
||||
**执行前自检(必须能在心中回答)**:
|
||||
- [ ] 报表类型是?(明细 / 月度汇总 / 每日统计)
|
||||
- [ ] 人员列表的来源是?(`aisearch person` 还是 `contact dept list-members`?)
|
||||
- [ ] 列选择方式是?(预设报表关键词 / 自定义 `--column-keywords` / 默认列集合)
|
||||
- [ ] 报错时如何向用户解释?
|
||||
|
||||
如果上述任何一项答不出,**回到本文档对应章节重新阅读**,禁止凭记忆/想象组装命令。
|
||||
|
||||
**前提**:当前用户必须是钉钉管理员,否则 report 系列接口返回业务权限错误(`server_error_code` 为 `AUTH_ERROR`、errorCode `6001`「无权限操作」),**不是 HTTP 403**。提示需要管理员权限,不要重试。
|
||||
|
||||
## 核心原则
|
||||
|
||||
Agent 解析用户意图(报表类型、人员范围、时间范围、关注维度),获取 userId 列表后,**直接调用对应的 Python 脚本 CLI 生成 Excel**。
|
||||
- **脚本自包含**:数据查询(分批、分段、翻页)、字段解析、聚合计算、Excel 生成全部由脚本内部完成,Agent 不参与数据查询和计算
|
||||
- **月度汇总 / 每日统计**:脚本内部调用 `report columns` + `report query-data`
|
||||
- **明细**:脚本内部调用 `check result` + `check record`(数据源不同)
|
||||
- 列选择是独立维度:用户未指定关注维度时脚本使用内置默认字段;用户指定了关注维度时 Agent 通过 `--column-keywords` 参数传给脚本
|
||||
|
||||
## 严格禁止 (NEVER DO)
|
||||
|
||||
- 禁止凭历史记忆复用 userId 等任何 ID,必须从当次命令返回值中提取
|
||||
- 禁止用大模型口算/目测做考勤数据聚合(求和、计数、分组),必须通过 Python 脚本完成
|
||||
- 禁止 Agent 直接调用 `report query-data` / `report columns` / `check result` / `check record`,这些由脚本内部自动完成
|
||||
- 禁止 `dws` 命令缺省 `--format json`(Agent 仅在阶段 1 获取人员时直接调用 dws 命令)
|
||||
- 禁止编造任何字段值或用户姓名
|
||||
- 禁止直接输出裸 userId,脚本已内置 userId → 姓名转换
|
||||
|
||||
## 严格要求 (MUST DO)
|
||||
|
||||
- 所有 `dws` 命令必须携带 `--format json`
|
||||
- 时间参数 `--start` / `--end` 格式必须为 `yyyy-MM-dd HH:mm:ss`
|
||||
- 字段 ID 与字段名的映射必须从 `report columns` 实时建立,禁止硬编码
|
||||
- 任何接口失败(含无权限错误 6001/`AUTH_ERROR`)必须向用户清晰报错,禁止静默吞掉
|
||||
|
||||
## 涉及工具
|
||||
|
||||
| 工具 | 用途 | 安全等级 |
|
||||
|------|------|---------|
|
||||
| `dws attendance report columns` | 获取当前管理员可见的考勤字段清单(字段 ID → 字段名) | 只读 |
|
||||
| `dws attendance report query-data` | 按字段查询考勤数据(≤20 人/次,≤32 天/次) | 只读 |
|
||||
| `dws attendance report query-leave` | 按假期名称查询假期数据(≤20 人/次,≤32 天/次),月度汇总/每日统计的"请假"列由脚本自动调用 | 只读 |
|
||||
| `dws attendance approve list` | 查询审批单记录(考勤记录报表专用),支持类型:leave/trip/out/patch | 只读 |
|
||||
| `dws oa approval detail` | 获取审批单详情(考勤记录报表专用),解析 formValueVOS / extValue | 只读 |
|
||||
| `dws attendance check result` | 查询打卡结果(≤100 人/次,≤1 月,明细报表专用) | 只读 |
|
||||
| `dws attendance check record` | 查询打卡流水(≤1 月,明细报表专用) | 只读 |
|
||||
| `dws aisearch person` | 按姓名搜索用户获取 userId(搜人首选) | 只读 |
|
||||
| `dws contact user get` | 批量查询 userId → 用户信息(姓名/部门/工号/职位) | 只读 |
|
||||
| `dws contact dept search` | 搜索部门获取 deptId | 只读 |
|
||||
| `dws contact dept list-members` | 获取部门成员 userId 列表 | 只读 |
|
||||
|
||||
## 意图判断
|
||||
|
||||
### 报表类型(五选一)
|
||||
|
||||
| 用户说 | 报表类型 | 映射脚本 |
|
||||
|--------|---------|---------|
|
||||
| "导出研发部 3 月份的考勤明细" / "每条打卡记录" / "明细" / "原始记录" | 明细 | `attendance_report_detail.py` |
|
||||
| "生成研发部 3 月考勤汇总" / 用户明确说"月度汇总" / 用户未指明类型 | **月度汇总(默认)** | `attendance_report_monthly.py` |
|
||||
| "导出研发部 3 月每天的出勤情况" / "按天统计" / "每日" / 用户明确说"每日统计" | 每日统计 | `attendance_report_daily.py` |
|
||||
| "导出研发部 4 月的请假记录" / "补卡记录" / "出差记录" / "外出记录" / "xx记录" | 考勤记录 | `attendance_report_record.py` |
|
||||
| "导出签到记录" / "签到报表" / "签到数据导出" / "签到明细" / "外勤签到" | 签到报表 | `attendance_report_checkin.py` |
|
||||
|
||||
> **默认报表类型**:用户未指明报表类型时,**默认走月度汇总**,事后告知"已按月度汇总输出,如需明细/每日统计/考勤记录请告知"。
|
||||
>
|
||||
> **考勤记录 vs 其他报表**:当用户明确提到"请假记录"/"补卡记录"/"出差记录"/"外出记录"时,走考勤记录报表(数据源为审批单)。而"请假报表"/"出差时长统计"等走月度汇总(数据源为 report query-data)。区别在于:考勤记录导出的是**审批单维度的原始数据**(含审批单状态、每天明细),月度汇总导出的是**按人按月聚合后的统计数据**。
|
||||
|
||||
### 列选择(独立维度,与报表类型正交)
|
||||
|
||||
> **"报表类型"与"列选择"是两个独立维度,需分别判断。**
|
||||
> 例如用户说"帮我出一份加班报表":报表类型未指明 → 默认月度汇总;列选择命中"加班报表" → 使用加班预设关键词。
|
||||
> 例如用户说"帮我出每日的异常报表":报表类型命中"每日" → 每日统计脚本;列选择命中"异常报表" → 使用异常预设关键词。
|
||||
> 预设报表**不会改变报表类型的判断逻辑**,报表类型始终按下方「报表类型(三选一)」规则判断。
|
||||
|
||||
| 用户说 | 列选择方式 |
|
||||
|--------|-----------|
|
||||
| "帮我出一份考勤报表" / "导出考勤" / 未提及特定关注维度 | 不传 `--column-keywords`,使用脚本内置**默认列集合** |
|
||||
| "加班报表" / "加班统计" / "加班时长报表" | 传 `--column-keywords`,使用下方「加班报表预设关键词」 |
|
||||
| "请假报表" / "请假出差报表" / "请假外出统计" | 传 `--column-keywords`,使用下方「请假报表预设关键词」 |
|
||||
| "异常报表" / "异常考勤" / "迟到早退报表" / "缺卡报表" | 传 `--column-keywords`,使用下方「异常报表预设关键词」 |
|
||||
| 提及了其他自定义关注维度(如"工作时长报表") | 传 `--column-keywords`,由 Agent 自行拼接关键词 |
|
||||
|
||||
> **预设报表优先级**:当用户提到的关键词同时命中"预设报表"和一般自定义维度时,**优先使用预设报表的完整关键词列表**,确保列不遗漏。
|
||||
|
||||
### 易混淆场景
|
||||
|
||||
| 用户说 | 应路由到 |
|
||||
|--------|---------|
|
||||
| "今天打卡了吗" | `dws attendance record get`(单次查询,非报表) |
|
||||
| "帮我排班" | [attendance-schedule.md](./attendance-schedule.md) 排班工作流(排班,非报表) |
|
||||
| "我的假期还剩多少" | `dws attendance vacation balance`(单次查询) |
|
||||
| "帮我请假" | `dws oa`(审批流程,非报表) |
|
||||
| "我这个月的考勤怎么样" | `dws attendance summary`(个人统计,非报表) |
|
||||
|
||||
## 工作流
|
||||
|
||||
### 阶段 0: 参数解析与确认查询范围
|
||||
|
||||
1. **解析用户输入**(两个独立维度):
|
||||
- **报表类型**(四选一):明细 / 月度汇总(默认) / 每日统计 / 考勤记录
|
||||
- **列选择**(独立维度):预定义列集合(默认) / 用户指定维度筛选(考勤记录不适用)
|
||||
- **人员维度**:指定员工 / 某个部门 / 多个部门(暂不支持全公司查询)
|
||||
- **时间维度**:本周 / 本月 / 自定义时间段
|
||||
- **记录子类型**(仅考勤记录):leave(请假) / trip(出差) / out(外出) / patch(补卡)
|
||||
2. **缺失信息处理**:
|
||||
- **报表类型** 缺失 → 默认走月度汇总(不追问),事后告知
|
||||
- **列选择**:用户提及了特定关注维度 → 传 `--column-keywords`;未提及 → 使用脚本内置默认字段集
|
||||
- **用户范围 / 时间范围** 缺失 → 追问,禁止猜测
|
||||
|
||||
### 阶段 1: 获取完整人员列表
|
||||
|
||||
**场景 A — 指定员工姓名**:
|
||||
```bash
|
||||
dws aisearch person --keyword "<员工姓名>" --dimension name --format json
|
||||
```
|
||||
|
||||
**场景 B — 按部门查询**:
|
||||
```bash
|
||||
dws contact dept search --query "<部门名>" --format json
|
||||
dws contact dept list-members --ids <deptId> --format json
|
||||
```
|
||||
|
||||
**场景 C — 多个部门**: 对每个部门分别执行 B,汇总去重。
|
||||
|
||||
**场景 D — 全公司**: 暂不支持,引导用户指定部门。
|
||||
|
||||
**场景 E — 用户已给 userId 列表**: 直接跳过本步。
|
||||
|
||||
### 阶段 2: 列选择(决定脚本参数)
|
||||
|
||||
> Agent 不需要手动调用 `report columns`,字段获取由脚本内部完成。Agent 只需根据用户意图决定是否传 `--column-keywords` 参数。
|
||||
|
||||
**判断顺序**(优先级从高到低):
|
||||
|
||||
1. **明细报表** → 列固定,不支持 `--column-keywords`
|
||||
2. **用户提到预设报表关键词** → 传 `--column-keywords`,使用本文档「预设报表列集合」中定义的完整关键词列表:
|
||||
- "加班报表" / "加班统计" / "加班时长" → 使用「加班报表预设关键词」
|
||||
- "请假报表" / "请假出差" / "外出统计" → 使用「请假报表预设关键词」
|
||||
- "异常报表" / "迟到早退" / "缺卡报表" / "异常考勤" → 使用「异常报表预设关键词」
|
||||
3. **用户提及了其他自定义关注维度** → 传 `--column-keywords "..."`
|
||||
4. **用户未提及特定关注维度** → 不传 `--column-keywords`,脚本使用内置默认字段集
|
||||
|
||||
### 阶段 3: 调用脚本生成 Excel
|
||||
|
||||
#### 月度汇总 / 每日统计
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_monthly.py \
|
||||
--users <userId1>,<userId2>,... \
|
||||
--start "<yyyy-MM-dd>" \
|
||||
--end "<yyyy-MM-dd>" \
|
||||
[--column-keywords "出勤天数,迟到次数,迟到时长,..."] \
|
||||
[--out 月度汇总_研发部_202604.xlsx]
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `--users`(必填):逗号分隔的 userId 列表
|
||||
- `--start`(必填):开始日期,支持 `YYYY-MM-DD` 或 `YYYY-MM-DD HH:mm:ss`
|
||||
- `--end`(必填):结束日期,同上
|
||||
- `--column-keywords`(可选):逗号分隔的字段名关键词。不传则使用脚本内置默认字段集。预设报表(加班/请假/异常)也通过本参数传入对应的预设关键词列表
|
||||
- `--out`(可选):输出文件名,不传则自动生成
|
||||
|
||||
脚本内部自动处理:`report columns` 获取字段清单 → 按预设/关键词匹配 → `report query-data` 分批分段查询 → `contact user get` 姓名映射 → 聚合计算 → 生成 Excel
|
||||
|
||||
#### 明细
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_detail.py \
|
||||
--users <userId1>,<userId2>,... \
|
||||
--start "<yyyy-MM-dd>" \
|
||||
--end "<yyyy-MM-dd>" \
|
||||
[--out 考勤明细_研发部_202604.xlsx]
|
||||
```
|
||||
|
||||
- **没有 `--column-keywords`**,明细列固定
|
||||
- 数据来源不同:`check result` + `check record`(非 `report query-data`)
|
||||
- 分批限制:≤100 人/次(而非 20 人)
|
||||
|
||||
#### 考勤记录
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_record.py \
|
||||
--type <leave|trip|out|patch> \
|
||||
--users <userId1>,<userId2>,... \
|
||||
--start "<yyyy-MM-dd>" \
|
||||
--end "<yyyy-MM-dd>" \
|
||||
[--out 请假记录_研发部_202604.xlsx]
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `--type`(必填):记录类型,支持 `leave`(请假) / `trip`(出差) / `out`(外出) / `patch`(补卡)
|
||||
- `--users`(必填):逗号分隔的 userId 列表
|
||||
- `--start`(必填):开始日期 `YYYY-MM-DD`
|
||||
- `--end`(必填):结束日期 `YYYY-MM-DD`
|
||||
- `--out`(可选):输出文件名,不传则自动生成
|
||||
- **没有 `--column-keywords`**,列由 `--type` 决定
|
||||
|
||||
脚本内部自动处理:`attendance approve list` 获取审批摘要 → `oa approval detail` 获取详情 → 解析 DDHolidayField / extValue → 按天拆行 → `contact user get` 姓名映射 → 生成 Excel
|
||||
|
||||
**记录类型选择规则**(Agent 需从用户意图中判断):
|
||||
|
||||
| 用户说 | --type 值 |
|
||||
|--------|----------|
|
||||
| "请假记录" / "年假记录" / "调休记录" / "病假记录" | `leave` |
|
||||
| "出差记录" | `trip` |
|
||||
| "外出记录" | `out` |
|
||||
| "补卡记录" | `patch` |
|
||||
|
||||
#### 签到报表
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_checkin.py \
|
||||
--users <userId1>,<userId2>,... \
|
||||
--start "<yyyy-MM-dd>" \
|
||||
--end "<yyyy-MM-dd>" \
|
||||
[--out 签到报表_研发部_20260401_20260407.xlsx]
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `--users`(必填):逗号分隔的 userId 列表
|
||||
- `--start`(必填):开始日期 `YYYY-MM-DD` 或 `YYYY-MM-DD HH:mm:ss`
|
||||
- `--end`(必填):结束日期,同上
|
||||
- `--out`(可选):输出文件名,不传则自动生成
|
||||
- **没有 `--column-keywords`**,签到报表列固定
|
||||
|
||||
脚本内部自动处理:`attendance checkin records` 分批分段查询(每批 50 人,每段 7 天)→ `contact user get` 姓名/部门映射 → 时间戳转日期+时间 → 图片列展开(最多 9 张)→ 生成 Excel
|
||||
|
||||
> **注意**:签到接口时间限制为 7 天(不同于考勤报表的 32 天),脚本会自动按 7 天分段查询。
|
||||
|
||||
#### 脚本执行注意事项
|
||||
|
||||
- 脚本依赖 `openpyxl`,若未安装需先 `pip install openpyxl`
|
||||
- 脚本摘要输出到 stdout,进度日志输出到 stderr
|
||||
- 首次调试可加 `--inspect` 参数查看首条记录原始结构(`attendance_report_detail.py` / `attendance_report_monthly.py` / `attendance_report_daily.py` / `attendance_report_checkin.py` 支持;**`attendance_report_record.py` 无 `--inspect` 参数**)
|
||||
- 脚本执行失败(exit ≠ 0)时,stderr 中有具体错误信息
|
||||
|
||||
### 阶段 4: 返回结果给用户
|
||||
|
||||
- 将脚本 stdout 输出的摘要信息原样转告用户
|
||||
- 如果脚本输出 warning,原样转告用户
|
||||
- 如果走的是默认月度汇总,追加:"已按月度汇总输出,如需明细/每日统计请告知"
|
||||
- **不要把 Excel 内容贴在对话里**,只给路径和摘要
|
||||
|
||||
## 输出文件结构
|
||||
|
||||
### 月度汇总(双 sheet,自动生成)
|
||||
|
||||
`attendance_report_monthly.py` 输出的 Excel 文件包含 **2 个 sheet**:
|
||||
|
||||
| Sheet 名 | 布局 | 用途 |
|
||||
|---------|------|------|
|
||||
| `月度汇总` | 每人 1 行,列为基础信息 + 聚合字段 + 请假展开 + 考勤结果按天展开 | 整月数据汇总速览 |
|
||||
| `日历表` | 每人 3 行(班次名称/考勤结果/工作时长),列为基础信息 + 指标 + 1日~N日 | 钉钉日历视图,逐日查看 |
|
||||
|
||||
**日历表结构示意**:
|
||||
|
||||
| 姓名 | 考勤组 | 部门 | 指标 | 1日 | 2日 | ... | 30日 |
|
||||
|------|--------|------|------|------|------|------|------|
|
||||
| 张三 | 研发组 | 技术部 | 班次名称 | 早班 | 早班 | ... | 休息 |
|
||||
| | | | 考勤结果 | 正常 | 迟到 | ... | — |
|
||||
| | | | 工作时长 | 8 | 7.5 | ... | 0 |
|
||||
| 李四 | 研发组 | 技术部 | 班次名称 | 晚班 | 晚班 | ... | 早班 |
|
||||
| ... | ... | ... | ... | ... | ... | ... | ... |
|
||||
|
||||
- 基础列(姓名/考勤组/部门)已纵向 3 行合并
|
||||
- 日历表的 3 个指标字段(`班次名称`/`考勤结果`/`工作时长`)由脚本**强制**追加到 `report query-data` 查询字段中(即使用户的 `--column-keywords` 没包含),确保日历表非空
|
||||
- 日历表数据来源与月度汇总相同(同一次 `report query-data` 调用),不会增加接口次数
|
||||
|
||||
## 预定义列集合
|
||||
|
||||
### 月度汇总(3 个基础信息列 + 18 个考勤数据列)
|
||||
|
||||
**基础信息列**(脚本自动从 `contact user get` 和原始记录中提取):
|
||||
|
||||
| 序号 | 字段名称 | 数据来源 |
|
||||
|------|---------|---------|
|
||||
| 1 | 姓名 | `contact user get --ids` |
|
||||
| 2 | 考勤组 | `attendance group search` + `filtered-get --member` 反向映射 |
|
||||
| 3 | 部门 | `contact user get --ids` |
|
||||
|
||||
**考勤数据列**(从 `report columns` 中按名称精确匹配):
|
||||
|
||||
| 序号 | 字段名称 |
|
||||
|------|---------|
|
||||
| 4 | 出勤天数 |
|
||||
| 5 | 休息天数 |
|
||||
| 6 | 工作时长 |
|
||||
| 7 | 迟到次数 |
|
||||
| 8 | 迟到时长 |
|
||||
| 9 | 严重迟到次数 |
|
||||
| 10 | 严重迟到时长 |
|
||||
| 11 | 旷工迟到次数 |
|
||||
| 12 | 早退次数 |
|
||||
| 13 | 早退时长 |
|
||||
| 14 | 上班缺卡次数 |
|
||||
| 15 | 下班缺卡次数 |
|
||||
| 16 | 旷工天数 |
|
||||
| 17 | 出差时长 |
|
||||
| 18 | 外出时长 |
|
||||
| 19 | 请假(按假期类型展开为 4 列:`请假-事假`、`请假-调休`、`请假-病假`、`请假-年假`,值为月度求和;数据由脚本通过 `report query-leave` 单独查询) |
|
||||
| 20 | 加班-审批单统计 |
|
||||
| 21 | 考勤结果(按天展开为多列:1日/2日/.../31日,每列显示当天考勤状态) |
|
||||
|
||||
### 每日统计(4 个基础信息列 + 31 个考勤数据列)
|
||||
|
||||
**基础信息列**:
|
||||
|
||||
| 序号 | 字段名称 | 数据来源 |
|
||||
|------|---------|---------|
|
||||
| 1 | 姓名 | `contact user get --ids` |
|
||||
| 2 | 考勤组 | `attendance group search` + `filtered-get --member` 反向映射 |
|
||||
| 3 | 部门 | `contact user get --ids` |
|
||||
| 4 | 日期 | 查询日期 |
|
||||
|
||||
**考勤数据列**:
|
||||
|
||||
| 序号 | 字段名称 |
|
||||
|------|---------|
|
||||
| 5 | 班次 |
|
||||
| 6 | 上班1打卡时间 |
|
||||
| 7 | 上班1打卡结果 |
|
||||
| 8 | 下班1打卡时间 |
|
||||
| 9 | 下班1打卡结果 |
|
||||
| 10 | 上班2打卡时间 |
|
||||
| 11 | 上班2打卡结果 |
|
||||
| 12 | 下班2打卡时间 |
|
||||
| 13 | 下班2打卡结果 |
|
||||
| 14 | 上班3打卡时间 |
|
||||
| 15 | 上班3打卡结果 |
|
||||
| 16 | 下班3打卡时间 |
|
||||
| 17 | 下班3打卡结果 |
|
||||
| 18 | 关联的审批单 |
|
||||
| 19 | 出勤天数 |
|
||||
| 20 | 休息天数 |
|
||||
| 21 | 工作时长 |
|
||||
| 22 | 迟到次数 |
|
||||
| 23 | 迟到时长 |
|
||||
| 24 | 严重迟到次数 |
|
||||
| 25 | 严重迟到时长 |
|
||||
| 26 | 旷工迟到次数 |
|
||||
| 27 | 早退次数 |
|
||||
| 28 | 早退时长 |
|
||||
| 29 | 上班缺卡次数 |
|
||||
| 30 | 下班缺卡次数 |
|
||||
| 31 | 旷工天数 |
|
||||
| 32 | 出差时长 |
|
||||
| 33 | 外出时长 |
|
||||
| 34 | 请假(按假期类型展开为 4 列:`请假-事假`、`请假-调休`、`请假-病假`、`请假-年假`;数据由脚本通过 `report query-leave` 单独查询) |
|
||||
| 35 | 加班-审批单统计 |
|
||||
|
||||
### 预设报表:加班报表
|
||||
|
||||
3 个基础信息列(姓名/考勤组/部门)+ 以下考勤数据列:
|
||||
|
||||
> 原始配置中的 TITLE_COLUMN(如"加班时长(转调休)")为分组标题,脚本不支持父子列结构,已打平为叶子字段。
|
||||
|
||||
| 序号 | 字段名称 | 说明 |
|
||||
|------|---------|------|
|
||||
| 4 | 加班-审批单统计 | — |
|
||||
| 5 | 加班总时长 | — |
|
||||
| 6 | 考勤结果 | 按天展开 |
|
||||
|
||||
**对应 `--column-keywords`**:`加班-审批单统计,加班总时长,考勤结果`
|
||||
|
||||
### 预设报表:请假报表
|
||||
|
||||
3 个基础信息列(姓名/考勤组/部门)+ 以下考勤数据列:
|
||||
|
||||
| 序号 | 字段名称 | 说明 |
|
||||
|------|---------|------|
|
||||
| 4 | 请假 | 按假期类型自动展开(事假/调休/病假/年假等),数据由脚本通过 `report query-leave` 单独查询 |
|
||||
| 5 | 出差时长 | — |
|
||||
| 6 | 外出时长 | — |
|
||||
| 7 | 考勤结果 | 按天展开 |
|
||||
|
||||
**对应 `--column-keywords`**:`请假,出差时长,外出时长,考勤结果`
|
||||
|
||||
### 预设报表:异常报表
|
||||
|
||||
3 个基础信息列(姓名/考勤组/部门)+ 以下考勤数据列:
|
||||
|
||||
> 原始配置中的 TITLE_COLUMN(如"迟到"、"早退"、"缺卡")为分组标题,脚本不支持父子列结构,已打平为叶子字段。
|
||||
|
||||
| 序号 | 字段名称 | 说明 |
|
||||
|------|---------|------|
|
||||
| 4 | 迟到次数 | 原属分组「迟到」 |
|
||||
| 5 | 迟到时长 | 同上 |
|
||||
| 6 | 严重迟到次数 | 同上 |
|
||||
| 7 | 严重迟到时长 | 同上 |
|
||||
| 8 | 旷工迟到次数 | 同上 |
|
||||
| 9 | 早退次数 | 原属分组「早退」 |
|
||||
| 10 | 早退时长 | 同上 |
|
||||
| 11 | 上班缺卡次数 | 原属分组「缺卡」 |
|
||||
| 12 | 下班缺卡次数 | 同上 |
|
||||
| 13 | 旷工天数 | — |
|
||||
| 14 | 考勤结果 | 按天展开 |
|
||||
|
||||
**对应 `--column-keywords`**:`迟到次数,迟到时长,严重迟到次数,严重迟到时长,旷工迟到次数,早退次数,早退时长,上班缺卡次数,下班缺卡次数,旷工天数,考勤结果`
|
||||
|
||||
### 明细(3 个基础信息列 + 10 个打卡字段列)
|
||||
|
||||
**基础信息列**:
|
||||
|
||||
| 序号 | 字段名称 | 数据来源 |
|
||||
|------|---------|---------|
|
||||
| 1 | 姓名 | `contact user get --ids` |
|
||||
| 2 | 考勤组 | `attendance group search` + `filtered-get --member` 反向映射 |
|
||||
| 3 | 部门 | `contact user get --ids` |
|
||||
|
||||
**打卡字段列**(以打卡流水为主表,每条流水一行;通过打卡时间关联 `check result` 获取考勤时间和打卡结果):
|
||||
|
||||
| 序号 | 字段名称 | 数据来源 |
|
||||
|------|---------|---------|
|
||||
| 4 | 考勤日期 | `check record` |
|
||||
| 5 | 考勤时间 | `check result`(班次规定的上/下班时间,按打卡时间关联) |
|
||||
| 6 | 打卡时间 | `check record`(实际打卡时间) |
|
||||
| 7 | 打卡结果 | `check result`(正常/迟到/早退/缺卡等,按打卡时间关联) |
|
||||
| 8 | 打卡地址 | `check record` |
|
||||
| 9 | 打卡备注 | `check record` |
|
||||
| 10 | 异常打卡原因 | `check record` |
|
||||
| 11 | 打卡图片 | `check record` |
|
||||
| 12 | 打卡设备 | `check record` |
|
||||
| 13 | 管理员修改备注 | `check record` |
|
||||
|
||||
### 签到报表(3 个基础信息列 + 11 个签到字段列 + 9 个图片列)
|
||||
|
||||
**基础信息列**:
|
||||
|
||||
| 序号 | 字段名称 | 数据来源 |
|
||||
|------|---------|---------|
|
||||
| 1 | 姓名 | `contact user get --ids` |
|
||||
| 2 | 部门 | `contact user get --ids` |
|
||||
| 3 | 完整部门 | `contact user get --ids` |
|
||||
|
||||
**签到字段列**(每条签到记录一行,数据来源均为 `attendance checkin records`):
|
||||
|
||||
| 序号 | 字段名称 | 数据来源 |
|
||||
|------|---------|---------|
|
||||
| 4 | 签到来源 | `checkinType` |
|
||||
| 5 | 日期 | `timestamp`(毫秒时间戳转日期) |
|
||||
| 6 | 时间 | `timestamp`(毫秒时间戳转时间) |
|
||||
| 7 | 经度 | `longitude` |
|
||||
| 8 | 纬度 | `latitude` |
|
||||
| 9 | 地点 | `place` |
|
||||
| 10 | 详细地址 | `detailPlace` |
|
||||
| 11 | 拜访客户 | `customers` |
|
||||
| 12 | 客户部门名称 | 预留(签到接口暂无此字段) |
|
||||
| 13 | 工作内容 | `remark` |
|
||||
| 14 | 手机标识 | `mobileId` |
|
||||
|
||||
**图片列**(从 `imageList` 数组展开,最多 9 列):
|
||||
|
||||
| 序号 | 字段名称 |
|
||||
|------|---------|
|
||||
| 15 | 图片1 |
|
||||
| 16 | 图片2 |
|
||||
| ... | ... |
|
||||
| 23 | 图片9 |
|
||||
|
||||
## 分批查询规则(脚本内部自动处理)
|
||||
|
||||
| 维度 | 限制 | 脚本自动处理方式 |
|
||||
|------|------|---------|
|
||||
| 人数超限(月度/每日) | `query-data` 最多 20 人/次 | 自动按 5 人一批分批 |
|
||||
| 人数超限(明细) | `check result` 最多 100 人/次 | 自动按 100 人一批分批 |
|
||||
| 人数超限(签到) | `checkin records` 最多 100 人/次 | 自动按 50 人一批分批 |
|
||||
| 时间超限(月度/每日) | `--start` 到 `--end` 不超过 32 天 | 自动按月分段 |
|
||||
| 时间超限(明细) | `--start` 到 `--end` 不超过 1 个月 | 自动按月分段 |
|
||||
| 时间超限(签到) | `--start` 到 `--end` 不超过 7 天 | 自动按 7 天分段 |
|
||||
| 分页(明细打卡结果) | `check result` 单次最多 1000 条 | 自动翻页 |
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误 | 原因 | 处理方式 |
|
||||
|------|------|---------|
|
||||
| 权限错误(errorCode 6001「无权限操作」/ `AUTH_ERROR`,非 HTTP 403) | 当前账号非管理员 | 提示需要管理员权限,不要重试 |
|
||||
| userId 无效 | 用户 ID 错误或已离职 | 脚本跳过并在摘要中标注 |
|
||||
| 时间区间超长 | 接口可能性能不佳 | 提示"超过 1 年的数据建议分阶段导出" |
|
||||
| openpyxl 未安装 | 环境缺包 | 输出 `pip install openpyxl` 安装提示 |
|
||||
| 脚本执行失败 | 接口异常/配置问题 | 将 stderr 错误信息转告用户,可加 `--inspect` 重试(`attendance_report_record.py` 不支持 `--inspect`) |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例 1: 团队月度汇总(默认)
|
||||
**用户说**: "帮我生成研发组 4 月的考勤报表"
|
||||
|
||||
```bash
|
||||
# 1. 获取部门成员
|
||||
dws contact dept search --query "研发组" --format json
|
||||
dws contact dept list-members --ids <deptId> --format json
|
||||
|
||||
# 2. 调用脚本(默认月度汇总,不传 --column-keywords)
|
||||
python scripts/attendance_report_monthly.py \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01" --end "2026-04-30"
|
||||
```
|
||||
|
||||
### 示例 2: 加班报表(预设)
|
||||
**用户说**: "帮我出一份研发组 4 月的加班报表"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_monthly.py \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01" --end "2026-04-30" \
|
||||
--column-keywords "加班-审批单统计,加班总时长,考勤结果"
|
||||
```
|
||||
|
||||
### 示例 3: 请假报表(预设)
|
||||
**用户说**: "帮我导出研发组 4 月的请假出差情况"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_monthly.py \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01" --end "2026-04-30" \
|
||||
--column-keywords "请假,出差时长,外出时长,考勤结果"
|
||||
```
|
||||
|
||||
### 示例 4: 异常报表(预设)
|
||||
**用户说**: "帮我出研发组 4 月的异常考勤报表"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_monthly.py \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01" --end "2026-04-30" \
|
||||
--column-keywords "迟到次数,迟到时长,严重迟到次数,严重迟到时长,旷工迟到次数,早退次数,早退时长,上班缺卡次数,下班缺卡次数,旷工天数,考勤结果"
|
||||
```
|
||||
|
||||
### 示例 5: 自定义维度筛选
|
||||
**用户说**: "帮我出一份研发组 4 月的工作时长报表"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_monthly.py \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01" --end "2026-04-30" \
|
||||
--column-keywords "工作时长"
|
||||
```
|
||||
|
||||
### 示例 6: 每日统计
|
||||
**用户说**: "帮我出一份研发组 4 月每天的出勤情况"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_daily.py \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01" --end "2026-04-30"
|
||||
```
|
||||
|
||||
### 示例 7: 明细报表
|
||||
**用户说**: "帮我导出研发组 4 月的考勤明细"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_detail.py \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01" --end "2026-04-30"
|
||||
```
|
||||
|
||||
### 示例 8: 请假记录
|
||||
**用户说**: "帮我导出研发组 4 月的请假记录"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_record.py \
|
||||
--type leave \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01" --end "2026-04-30"
|
||||
```
|
||||
|
||||
### 示例 9: 出差记录
|
||||
**用户说**: "帮我导出研发组 4 月的出差记录"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_record.py \
|
||||
--type trip \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01" --end "2026-04-30"
|
||||
```
|
||||
|
||||
### 示例 10: 补卡记录
|
||||
**用户说**: "帮我导出研发组 5 月的补卡记录"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_record.py \
|
||||
--type patch \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-05-01" --end "2026-05-31"
|
||||
```
|
||||
|
||||
### 示例 11: 签到报表
|
||||
**用户说**: "帮我导出研发组上周的签到记录"
|
||||
|
||||
```bash
|
||||
python scripts/attendance_report_checkin.py \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-05-26" --end "2026-06-01"
|
||||
```
|
||||
|
||||
## 配套脚本
|
||||
|
||||
| 脚本 | 报表类型 | 数据来源 | CLI 参数 |
|
||||
|------|---------|---------|---------|
|
||||
| [attendance_report_detail.py](../../scripts/attendance_report_detail.py) | 明细 | `check result` + `check record` | `--users --start --end [--out]` |
|
||||
| [attendance_report_monthly.py](../../scripts/attendance_report_monthly.py) | 月度汇总(默认) | `report columns` + `report query-data` | `--users --start --end [--column-keywords] [--out]` |
|
||||
| [attendance_report_daily.py](../../scripts/attendance_report_daily.py) | 每日统计 | `report columns` + `report query-data` | `--users --start --end [--column-keywords] [--out]` |
|
||||
| [attendance_report_record.py](../../scripts/attendance_report_record.py) | 考勤记录 | `attendance approve list` + `oa approval detail` | `--type --users --start --end [--out]` |
|
||||
| [attendance_report_checkin.py](../../scripts/attendance_report_checkin.py) | 签到报表 | `attendance checkin records` | `--users --start --end [--out]` |
|
||||
| [attendance_report_common.py](../../scripts/attendance_report_common.py) | 公共模块(不可单独执行) | — | — |
|
||||
@@ -0,0 +1,590 @@
|
||||
# 考勤排班操作参考 (attendance-schedule)
|
||||
|
||||
> 本文档由 `attendance.md` 路由调用。覆盖两类排班操作:
|
||||
> 1. **排班导入**(写操作):当用户提到"排班"、"导入排班"、"安排班次"、"设置排班"、"调班"、"换班"、"排休"时
|
||||
> 2. **排班查询导出**(只读操作):当用户提到"查看排班"、"排班表"、"导出排班"、"排班记录"时
|
||||
>
|
||||
> 不适用于:班次定义查询(用 `attendance class search`)、考勤组配置(用 `attendance group get`)。
|
||||
|
||||
## 强制门禁(必须先读完本文档才能执行)
|
||||
|
||||
**任何排班操作都必须经过本文档定义的工作流,严禁绕过本文档直接调用 `dws attendance schedule import` 命令。**
|
||||
|
||||
违反将出现以下任一问题:
|
||||
1. 未按"阶段 1"确认考勤组 → 把固定班制考勤组当排班制操作,接口报错
|
||||
2. 未按"阶段 3"校验班次 → 传入不属于该考勤组的班次 ID,导致排班数据错乱
|
||||
3. 未按"阶段 4"回显确认 → 用户未看到排班内容就直接执行,排错了无法回退
|
||||
4. 未按"阶段 2"解析人员 → 传入错误的 userId,导致排班到错误的人
|
||||
5. 未经用户确认就执行排班 → 排班是写操作,一旦执行就会覆盖原有排班
|
||||
|
||||
**执行前自检(必须能在心中回答)**:
|
||||
- [ ] 考勤组是排班制(TURN)吗?
|
||||
- [ ] 员工都属于该考勤组吗?
|
||||
- [ ] 班次都属于该考勤组可用的班次吗?
|
||||
- [ ] 用户已经确认了排班内容吗?
|
||||
|
||||
如果上述任何一项答不出,**回到本文档对应章节重新阅读**,禁止凭记忆/想象组装命令。
|
||||
|
||||
**前提**:当前用户必须是钉钉考勤管理员,否则排班接口返回权限错误。
|
||||
|
||||
## 业务约束(必须深刻理解)
|
||||
|
||||
> **这两条约束是排班的根基,贯穿整个工作流的每一步。**
|
||||
|
||||
1. **用户只能属于一个考勤组**:每个员工有且只有一个考勤组,不存在"选择考勤组"的场景。直接通过 `dws attendance rules` 查询即可唯一确定。
|
||||
2. **排班只能排考勤组关联的班次**:考勤组绑定了固定的班次列表(`shiftVOList`),排班时只能从这些班次中选择,不能使用企业其他考勤组的班次,更不能编造班次。
|
||||
|
||||
**由此推导出的执行顺序**:必须先查清考勤组和它关联的班次,再去收集日期、人员等其他参数。
|
||||
|
||||
## 核心原则
|
||||
|
||||
Agent 解析用户意图(考勤组、员工、日期范围、班次安排),完成校验后,**调用 Python 脚本执行排班**。
|
||||
- **先查后排**:任何排班操作的第一步都是查询考勤组及其关联班次,拿到真实数据后再进行后续参数收集
|
||||
- **脚本自包含**:考勤组校验、班次校验、员工校验、回显确认、调用排班 API 全部由脚本内部完成
|
||||
- **Agent 职责**:先查考勤组和关联班次,再解析用户意图、获取必要的 ID(员工 userId)、组装脚本参数
|
||||
- **脚本职责**:二次校验、回显排班表格、等待用户确认、执行排班、输出结果摘要
|
||||
- 排班是**写操作**,必须经过回显确认后才能执行
|
||||
|
||||
## 严格禁止 (NEVER DO)
|
||||
|
||||
- **禁止直接调用 `dws attendance schedule import`**,必须通过脚本执行
|
||||
- 禁止凭历史记忆复用任何 ID(考勤组 ID、班次 ID、userId),必须从当次命令返回值中提取
|
||||
- 禁止在未确认考勤组类型为排班制(TURN)的情况下执行排班
|
||||
- 禁止在班次未经校验的情况下执行排班
|
||||
- 禁止跳过用户确认直接执行排班
|
||||
- **禁止在未向用户展示完整排班明细表格(含每天的班次名称)的情况下弹出确认卡片**。用户必须先看到"谁、哪天、上什么班"才能做出确认决策
|
||||
- 禁止编造任何字段值或用户姓名
|
||||
- 禁止直接输出裸 userId,脚本已内置 userId → 姓名转换
|
||||
- **禁止直接输出裸 classId 数字**,必须展示班次名称(如"早班"),用户不理解 classId 是什么
|
||||
|
||||
## 严格要求 (MUST DO)
|
||||
|
||||
- 所有 `dws` 命令必须携带 `--format json`
|
||||
- 必须先确认考勤组类型为 TURN(排班制),否则拒绝执行
|
||||
- 必须通过考勤组详情获取绑定班次列表,校验班次 ID 属于该考勤组
|
||||
- 必须在执行排班前向用户回显排班内容并获得确认
|
||||
- 任何接口失败必须向用户清晰报错,禁止静默吞掉
|
||||
|
||||
## 涉及工具
|
||||
|
||||
| 工具 | 用途 | 安全等级 |
|
||||
|------|------|---------|
|
||||
| `dws attendance group search` | 搜索考勤组(按名称/类型) | 只读 |
|
||||
| `dws attendance group get` | 查询考勤组全量信息(含绑定班次列表) | 只读 |
|
||||
| `dws attendance group filtered-get` | 查询考勤组详情(成员列表) | 只读 |
|
||||
| `dws attendance class search` | 查询班次列表(ID→名称映射) | 只读 |
|
||||
| `dws attendance class get` | 查询班次详情 | 只读 |
|
||||
| `dws attendance schedule import` | 导入排班记录(**仅由脚本内部调用**) | 写操作(危险) |
|
||||
| `dws attendance schedule get` | 查询现有排班记录 | 只读 |
|
||||
| `dws aisearch person` | 按姓名搜索用户获取 userId | 只读 |
|
||||
| `dws contact user get` | 批量查询 userId → 用户信息 | 只读 |
|
||||
| `dws contact dept search` | 搜索部门获取 deptId | 只读 |
|
||||
| `dws contact dept list-members` | 获取部门成员 userId 列表 | 只读 |
|
||||
|
||||
## 意图判断
|
||||
|
||||
### 排班操作类型
|
||||
|
||||
| 用户说 | 操作类型 | 处理方式 |
|
||||
|--------|---------|---------|
|
||||
| "帮我给研发组排下周的班" / "排班" / "安排班次" | 批量排班 | 走本文档工作流 |
|
||||
| "帮我把张三下周一改成早班" / "调班" / "换班" | 单人调班 | 走本文档工作流(单人模式) |
|
||||
| "帮我把李四下周三排休" | 排休 | 走本文档工作流(isRest=Y) |
|
||||
|
||||
### 易混淆场景
|
||||
|
||||
| 用户说 | 应路由到 |
|
||||
|--------|---------|
|
||||
| "查看下周的排班" / "排班表" / "导出排班" / "导出排班表" / "XX考勤组的排班" | **本文档「排班查询导出工作流」**(走脚本) |
|
||||
| "有哪些班次" / "班次列表" | `dws attendance class search`(查询班次定义) |
|
||||
| "我属于哪个考勤组" | `dws attendance rules`(查询考勤规则) |
|
||||
| "导出考勤报表" / "导出考勤" / "考勤明细" / "出勤汇总" (**不含"排班"二字**) | `attendance-report.md`(报表 skill) |
|
||||
|
||||
> **关键区分**:"导出排班表" ≠ "导出考勤报表"。判断标准:句中含"排班"→ 本文档;不含"排班"且说的是"考勤报表/考勤数据/出勤统计" → `attendance-report.md`。
|
||||
|
||||
## 工作流
|
||||
|
||||
### 阶段 0: 先查考勤组和关联班次,再收集缺失参数
|
||||
|
||||
> **核心逻辑:先查后问。** 用户只属于一个考勤组,排班只能排该考勤组关联的班次。所以第一步永远是查清考勤组和它的班次,拿到真实数据后再向用户收集其他信息。
|
||||
|
||||
**步骤 0a — 查询考勤组(必须最先执行)**:
|
||||
|
||||
```bash
|
||||
# 自动获取当前用户的考勤组(用户只属于一个考勤组,无需选择)
|
||||
dws attendance rules --date <今天日期> --format json
|
||||
# → 从返回中提取 groupId
|
||||
```
|
||||
|
||||
如果是给指定员工排班,先查该员工的 userId,再查其考勤组。
|
||||
|
||||
**步骤 0b — 查询考勤组详情和关联班次(必须在 ask_question 之前完成)**:
|
||||
|
||||
```bash
|
||||
# 获取考勤组详情(含绑定的班次列表)
|
||||
dws attendance group get --group-id <groupId> --format json
|
||||
# → 校验 groupVO.type 必须为 TURN(排班制)
|
||||
# → 从 groupVO.shiftVOList 提取关联的班次
|
||||
```
|
||||
|
||||
**提取结果**:
|
||||
- 考勤组名称:`groupVO.name`
|
||||
- 考勤组类型:`groupVO.type`(必须为 TURN)
|
||||
- 关联班次列表:`groupVO.shiftVOList[].shiftSetting.{shiftId, shiftName}`
|
||||
|
||||
**步骤 0c — 收集缺失参数(ask_question 卡片交互)**:
|
||||
|
||||
拿到考勤组和关联班次后,再向用户收集缺失的参数。排班所需的四个参数:
|
||||
- **考勤组**:已在 0a 自动获取,无需询问
|
||||
- **班次**:已在 0b 获取关联班次列表,展示给用户选择
|
||||
- **员工范围**(必填):指定员工姓名 / 部门 / 考勤组全员 / "给我排班"
|
||||
- **日期范围**(必填):具体日期 / 日期范围(如"下周"、"5月19日到5月23日")
|
||||
|
||||
**只收集真正缺失的参数**,用户已经提供的不要重复询问。将缺失参数**合并到一次 `ask_question` 调用中**。
|
||||
|
||||
示例:用户说"帮我排班",Agent 应先自动查询考勤组和关联班次(步骤 0a + 0b),然后只询问日期范围和班次:
|
||||
|
||||
```
|
||||
// ===== 步骤 0a + 0b 已完成,此时你已经拿到了以下真实数据 =====
|
||||
// groupId = 实际的考勤组ID
|
||||
// groupName = 实际的考勤组名称
|
||||
// shiftVOList = 考勤组关联的班次列表(来自 dws attendance group get 的返回)
|
||||
|
||||
// 从 shiftVOList 构建班次选项(伪代码):
|
||||
shiftOptions = []
|
||||
for each shift in groupVO.shiftVOList:
|
||||
shiftOptions.append({ id: String(shift.shiftSetting.shiftId), label: shift.shiftSetting.shiftName })
|
||||
shiftOptions.append({ id: "rest", label: "排休" })
|
||||
|
||||
// 如果考勤组只关联了一个班次,直接使用,不需要询问用户
|
||||
if shiftVOList.length == 1:
|
||||
selectedShift = shiftVOList[0] // 自动选定,跳过班次选择
|
||||
|
||||
// 构建日期选项(必须填入实际计算的日期):
|
||||
todayStr = 当天日期(YYYY-MM-DD)
|
||||
thisWeekEnd = 本周日日期
|
||||
nextWeekStart = 下周一日期
|
||||
nextWeekEnd = 下周日日期
|
||||
|
||||
ask_question({
|
||||
title: "排班参数确认",
|
||||
questions: [
|
||||
{
|
||||
id: "date_range",
|
||||
prompt: "请选择排班日期范围",
|
||||
options: [
|
||||
{ id: "this_week", label: "本周剩余时间(" + todayStr + "-" + thisWeekEnd + ")" },
|
||||
{ id: "next_week", label: "下周(" + nextWeekStart + "-" + nextWeekEnd + ")" },
|
||||
{ id: "custom", label: "自定义日期范围" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "shift",
|
||||
prompt: "请选择班次(以下为您考勤组「" + groupName + "」关联的班次)",
|
||||
options: shiftOptions // 直接使用上面从 shiftVOList 动态构建的选项,严禁替换为任何硬编码值
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**⚠️ 班次选项严禁硬编码**:上述伪代码中的 `shiftOptions` 必须在运行时从 `shiftVOList` 动态生成。禁止在 `ask_question` 中写入任何固定的班次名称(如"早班"、"晚班"、"正常班"、"全部排XX班"等)。如果你发现自己在 options 里手写班次名,说明你做错了。
|
||||
|
||||
**动态选项原则(严格执行)**:
|
||||
- 班次选项**必须且只能**来自考勤组的 `shiftVOList`,**严禁编造任何班次名称**(如"正常班"、"早班"、"晚班"、"全部排XX班"等都是编造)
|
||||
- 每个班次选项的 `id` 必须是 `shiftVOList` 中的真实 `shiftId`,`label` 必须是真实的 `shiftName`
|
||||
- 如果 `shiftVOList` 为空,降级从 `groupVO.classIds` + `class search` 按 ID 精确查询(不是全局搜索)
|
||||
- **只收集缺失的参数**:用户已经提供的参数不要重复询问
|
||||
- **如果考勤组只有一个班次,直接使用该班次,不需要询问用户选择**
|
||||
- **禁止用 `class search` 全局搜索来给用户展示班次选项**——全局班次列表包含不属于该考勤组的班次,用户选了也会被校验拒绝
|
||||
|
||||
### 阶段 1: 确认考勤组(必须为排班制)
|
||||
|
||||
> **业务事实:用户只属于一个考勤组。** 不存在"选考勤组"的场景,直接通过 `dws attendance rules` 自动获取即可。只有在用户明确指定了一个考勤组名称时,才用 `group search` 按名称确认。
|
||||
|
||||
**方式 A — 自动获取(默认方式,适用于绝大多数场景)**:
|
||||
```bash
|
||||
# 用户只属于一个考勤组,直接查询即可确定
|
||||
dws attendance rules --date <今天日期> --format json
|
||||
# → 返回中提取 groupId,然后用 group get 获取详情
|
||||
dws attendance group get --group-id <groupId> --format json
|
||||
```
|
||||
|
||||
**方式 B — 用户明确指定考勤组名称时**:
|
||||
```bash
|
||||
dws attendance group search --query "<考勤组名称>" --type TURN --format json
|
||||
```
|
||||
|
||||
**校验规则**:
|
||||
1. 考勤组类型必须为 **TURN(排班制)**,如果是 FIXED(固定班制)或 NONE(自由工时),拒绝并提示"该考勤组不是排班制,无法进行排班操作"
|
||||
2. 确认考勤组后,**必须立即获取其关联班次**(`groupVO.shiftVOList`),后续所有班次选项都从这里取
|
||||
|
||||
**提取信息**:考勤组 ID(`groupId`)、考勤组名称(`name`)、**关联班次列表(`shiftVOList`)**
|
||||
|
||||
### 阶段 2: 获取员工列表
|
||||
|
||||
**场景 A — 指定员工姓名**:
|
||||
```bash
|
||||
dws aisearch person --keyword "<员工姓名>" --dimension name --format json
|
||||
```
|
||||
|
||||
**场景 B — 按部门查询**:
|
||||
```bash
|
||||
dws contact dept search --query "<部门名>" --format json
|
||||
dws contact dept list-members --ids <deptId> --format json
|
||||
```
|
||||
|
||||
**场景 C — 考勤组全员**:
|
||||
```bash
|
||||
dws attendance group filtered-get --group-id <groupId> --member --format json
|
||||
```
|
||||
|
||||
**场景 D — 用户已给 userId 列表**:直接跳过本步。
|
||||
|
||||
### 阶段 3: 校验班次(必须是考勤组关联的班次)
|
||||
|
||||
> **业务约束:排班只能排考勤组关联的班次。** 阶段 0/1 已经通过 `dws attendance group get` 拿到了 `shiftVOList`,本阶段直接使用该数据校验,**不需要也不应该再调用 `class search` 全局搜索**。
|
||||
|
||||
**班次数据来源**(已在阶段 0 或阶段 1 获取):
|
||||
- `groupVO.shiftVOList[].shiftSetting.shiftId` — 班次 ID
|
||||
- `groupVO.shiftVOList[].shiftSetting.shiftName` — 班次名称
|
||||
|
||||
**禁止调用 `dws attendance class search` 全局搜索班次**——全局搜索会返回不属于该考勤组的班次,即使用户指定了班次名称,也必须在 `shiftVOList` 中匹配,而不是全局搜索。
|
||||
|
||||
**校验规则**:
|
||||
1. 用户在 `ask_question` 卡片中选择的班次,其 `shiftId` 必须存在于 `shiftVOList` 中(卡片选项本身就是从 `shiftVOList` 构建的,所以天然满足)
|
||||
2. 如果用户通过自然语言指定了班次名称(如"排早班"),必须在 `shiftVOList` 中**按名称模糊匹配**,找到对应的 `shiftId`
|
||||
3. 如果用户指定的班次不在 `shiftVOList` 中,**必须拒绝**,并列出该考勤组关联的全部班次让用户重新选择
|
||||
4. 仅当 `shiftVOList` 为空时,才降级从 `groupVO.classIds` + `dws attendance class get` 按 ID 精确查询(仍然不是全局搜索)
|
||||
|
||||
**提取信息**:班次 ID(`classId` / `shiftId`)、班次名称(`shiftName`)
|
||||
|
||||
### 阶段 4: 回显排班内容并使用 ask_question 卡片确认
|
||||
|
||||
> **[硬性门禁]** 必须先用普通文本向用户展示**完整的排班明细表格**(包含每个人、每天、具体班次名称),用户看到排班明细后,才能弹出 `ask_question` 确认卡片。
|
||||
> **禁止在用户还不知道"谁、哪天、上什么班"的情况下就弹确认卡片**——这等于让用户盲签,体验极差。
|
||||
|
||||
**步骤 4a — 展示排班明细(必须在确认卡片之前)**:
|
||||
|
||||
在调用 `ask_question` 之前,**必须先**用普通文本向用户展示排班内容。表格中**必须包含班次名称**(如"早班 09:00-18:00"),不能只展示 classId 数字:
|
||||
|
||||
```
|
||||
排班预览
|
||||
|
||||
考勤组: <考勤组名称>(ID: <groupId>)
|
||||
排班日期: <startDate> ~ <endDate>
|
||||
|
||||
| 员工姓名 | 日期 | 星期 | 班次 | 是否排休 |
|
||||
|---------|------|------|------|---------|
|
||||
| 张三 | 2026-05-19 | 周一 | 早班 09:00-18:00 | 否 |
|
||||
| 张三 | 2026-05-20 | 周二 | 早班 09:00-18:00 | 否 |
|
||||
| 张三 | 2026-05-21 | 周三 | 排休 | 是 |
|
||||
| 李四 | 2026-05-19 | 周一 | 晚班 18:00-02:00 | 否 |
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
共 <N> 条排班记录
|
||||
```
|
||||
|
||||
**自查清单(展示表格前必须确认)**:
|
||||
- [ ] 表格中有员工姓名(不是 userId)
|
||||
- [ ] 表格中有具体日期和星期几
|
||||
- [ ] 表格中有班次名称(不是 classId 数字)
|
||||
- [ ] 排休的记录标注了"排休"
|
||||
- [ ] 表格涵盖了所有待排班的员工和日期
|
||||
|
||||
**步骤 4b — 使用 `ask_question` 卡片确认(必须在展示表格之后)**:
|
||||
|
||||
```
|
||||
ask_question({
|
||||
title: "排班执行确认",
|
||||
questions: [
|
||||
{
|
||||
id: "confirm_execute",
|
||||
prompt: "以上排班将覆盖所选日期的现有排班记录,确认执行吗?",
|
||||
options: [
|
||||
{ id: "yes", label: "确认执行" },
|
||||
{ id: "no", label: "取消" }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**处理用户选择**:
|
||||
- 用户选择 **"确认执行"** → 进入阶段 5
|
||||
- 用户选择 **"取消"** → 终止流程,提示"已取消排班操作"
|
||||
|
||||
### 阶段 5: 调用脚本执行排班
|
||||
|
||||
```bash
|
||||
python scripts/attendance_schedule_import.py \
|
||||
--group-id <groupId> \
|
||||
--schedules '<JSON数组>' \
|
||||
--confirm
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `--group-id`(必填):考勤组 ID
|
||||
- `--schedules`(必填):排班记录 JSON 数组,每条记录包含 `userId`、`workDate`、`classId`、`isRest`
|
||||
- `--confirm`(必填):表示用户已确认,脚本收到此标志才会执行排班
|
||||
|
||||
脚本内部自动处理:
|
||||
1. 二次校验考勤组类型(必须为 TURN)
|
||||
2. 从考勤组详情提取绑定班次,二次校验班次 ID 属于该考勤组
|
||||
3. 格式化 workDate 为 `yyyy-MM-dd HH:mm:ss`
|
||||
4. 调用 `dws attendance schedule import` 执行排班
|
||||
5. 输出执行结果摘要(含全部排班明细)
|
||||
|
||||
### 阶段 6: 返回结果给用户
|
||||
|
||||
- 将脚本 stdout 输出的摘要信息原样转告用户
|
||||
- 如果脚本输出 warning,原样转告用户
|
||||
- 如果执行失败,将 stderr 错误信息转告用户
|
||||
|
||||
## 排班记录 JSON 格式
|
||||
|
||||
每条排班记录的字段说明:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `userId` | string | 是 | 员工的 userId |
|
||||
| `workDate` | string | 是 | 排班日期,格式 YYYY-MM-DD |
|
||||
| `classId` | int | 是 | 班次 ID(从 `class search` 获取) |
|
||||
| `isRest` | string | 是 | 是否排休,`Y`=排休 / `N`=正常上班 |
|
||||
|
||||
排休时 `classId` 传 0,`isRest` 传 `Y`。
|
||||
|
||||
## API 返回结构注意事项(Agent 必读)
|
||||
|
||||
> 以下是实际执行中多次踩坑的关键数据结构说明。**禁止凭直觉假设字段在顶层**,必须按本节描述的嵌套路径提取。
|
||||
|
||||
### `dws attendance group get` 返回结构
|
||||
|
||||
```
|
||||
run_dws 解包后的结构(unwrap_result 去掉 success/result 包装后):
|
||||
{
|
||||
"groupVO": { ← 关键!type/name/classIds 等字段在这一层
|
||||
"type": "TURN", ← 考勤组类型
|
||||
"name": "研发组",
|
||||
"classIds": [1290384739, ...], ← 绑定的班次 ID 列表
|
||||
"shiftVOList": [ ← 排班制特有,班次详情
|
||||
{
|
||||
"shiftSetting": {
|
||||
"shiftId": 1290384739, ← 班次 ID(与 classIds 对应)
|
||||
"shiftName": "早班 09:00-18:00"
|
||||
}
|
||||
}
|
||||
],
|
||||
"selectedClass": [...], ← 部分环境使用此字段
|
||||
...
|
||||
},
|
||||
...其他顶层字段...
|
||||
}
|
||||
```
|
||||
|
||||
**提取规则**:
|
||||
- 考勤组类型:`result["groupVO"]["type"]`
|
||||
- 考勤组名称:`result["groupVO"]["name"]`
|
||||
- 绑定班次 ID 列表:`result["groupVO"]["classIds"]`
|
||||
- 班次名称:`result["groupVO"]["shiftVOList"][N]["shiftSetting"]["shiftName"]`
|
||||
- **禁止从 result 顶层直接取 type/name/classIds,那里没有这些字段**
|
||||
|
||||
### `dws attendance class search` 返回结构
|
||||
|
||||
```
|
||||
run_dws 解包后可能为以下之一:
|
||||
1. 直接 list[dict]: [{id, name, ...}, ...]
|
||||
2. {"data": [...]} 或 {"items": [...]} 或 {"classList": [...]}
|
||||
```
|
||||
|
||||
**注意**:如果 `class search` 返回 0 条记录,不一定是错误——可能是当前账号没有班次管理权限。此时从 `group get` 的 `shiftVOList` 中也可获取班次名称。
|
||||
|
||||
### `dws aisearch person` 搜索同名问题
|
||||
|
||||
同一个姓名可能返回**多个不同 userId**(如主管理员账号 vs 子管理员账号)。必须通过以下方式确认正确的 userId:
|
||||
1. 检查目标考勤组的成员列表:`dws attendance group filtered-get --group-id <id> --member`
|
||||
2. 取成员列表中存在的那个 userId
|
||||
|
||||
**禁止直接取搜索结果的第一条 userId,必须与考勤组成员列表交叉验证。**
|
||||
|
||||
### `dws contact user get` 可能的权限错误
|
||||
|
||||
`resolve_user_names`(userId→姓名转换)可能遇到 `SECURITY_CHECK_INVOKE_FAILED` 错误。这只影响**展示层**,不影响排班数据的正确性:
|
||||
- 脚本已内置降级处理:权限失败时直接使用 userId 替代姓名
|
||||
- **不要因为姓名获取失败就中止排班流程**
|
||||
|
||||
### Agent 常见错误模式(严禁)
|
||||
|
||||
| 错误做法 | 正确做法 |
|
||||
|------------|------------|
|
||||
| `result.get("type")` 从顶层取类型 | `result["groupVO"]["type"]` |
|
||||
| `result.get("classIds")` 从顶层取班次 | `result["groupVO"]["classIds"]` 或 `result["groupVO"]["shiftVOList"]` |
|
||||
| 用 `python3 -c "..."` inline 脚本解析 JSON | 调用已有的 Python 脚本(`attendance_schedule_import.py`) |
|
||||
| 人名搜到多个结果直接取第一个 | 与考勤组成员列表交叉验证 |
|
||||
| 姓名获取失败就中止流程 | 降级用 userId 展示,继续执行排班 |
|
||||
| 直接调用 `dws attendance schedule import` | 必须通过 `attendance_schedule_import.py` 脚本 |
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误 | 原因 | 处理方式 |
|
||||
|------|------|---------|
|
||||
| 权限错误(403) | 当前账号非管理员 | 提示需要管理员权限,不要重试 |
|
||||
| 考勤组不是排班制 | 考勤组类型为 FIXED 或 NONE | 提示"该考勤组不是排班制,无法排班" |
|
||||
| 班次不在可用列表中 | classId 无效 | 列出可用班次让用户重新选择 |
|
||||
| userId 无效 | 用户 ID 错误或已离职 | 提示具体哪个用户无效 |
|
||||
| 脚本执行失败 | 接口异常/配置问题 | 将 stderr 错误信息转告用户 |
|
||||
| SECURITY_CHECK_INVOKE_FAILED | userId→姓名转换权限不足 | 仅影响展示,降级用 userId,不中止流程 |
|
||||
| class search 返回空列表 | 账号无班次管理权限 | 从 `group get` 的 `shiftVOList` 提取班次名称 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例 1: 给指定员工排班
|
||||
**用户说**: "帮我给张三下周一到周五排早班,考勤组是研发组"
|
||||
|
||||
```bash
|
||||
# 1. 确认考勤组并获取关联班次(先查后排)
|
||||
dws attendance group search --query "研发组" --type TURN --format json
|
||||
# → 拿到 groupId
|
||||
dws attendance group get --group-id <groupId> --format json
|
||||
# → 从 groupVO.shiftVOList 获取关联班次列表
|
||||
# → 在 shiftVOList 中匹配"早班",拿到对应的 shiftId 作为 classId
|
||||
# ⚠️ 禁止用 class search 全局搜索班次
|
||||
|
||||
# 2. 获取员工 userId
|
||||
dws aisearch person --keyword "张三" --dimension name --format json
|
||||
|
||||
# 3. 回显确认(Agent 向用户展示排班表格)
|
||||
# ... 用户确认 ...
|
||||
|
||||
# 4. 调用脚本执行
|
||||
python scripts/attendance_schedule_import.py \
|
||||
--group-id 123456 \
|
||||
--schedules '[{"userId":"user001","workDate":"2026-05-19","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-20","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-21","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-22","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-23","classId":789,"isRest":"N"}]' \
|
||||
--confirm
|
||||
```
|
||||
|
||||
### 示例 2: 给员工排休
|
||||
**用户说**: "帮我把李四下周三排休"
|
||||
|
||||
```bash
|
||||
# 1. 先查考勤组和关联班次(即使排休也需要确认考勤组)
|
||||
dws attendance rules --date 2026-05-15 --format json
|
||||
# → 拿到 groupId
|
||||
dws attendance group get --group-id <groupId> --format json
|
||||
# → 确认是排班制(TURN)
|
||||
|
||||
# 2. 获取员工 userId
|
||||
dws aisearch person --keyword "李四" --dimension name --format json
|
||||
|
||||
# 3. 回显确认 → 用户确认 → 执行
|
||||
python scripts/attendance_schedule_import.py \
|
||||
--group-id 123456 \
|
||||
--schedules '[{"userId":"user002","workDate":"2026-05-21","classId":0,"isRest":"Y"}]' \
|
||||
--confirm
|
||||
```
|
||||
|
||||
### 示例 3: 部门批量排班
|
||||
**用户说**: "帮我给研发部全员下周排早班"
|
||||
|
||||
```bash
|
||||
# 1. 先查考勤组并获取关联班次(先查后排)
|
||||
dws attendance rules --date 2026-05-15 --format json
|
||||
# → 拿到 groupId
|
||||
dws attendance group get --group-id <groupId> --format json
|
||||
# → 从 groupVO.shiftVOList 中匹配"早班",拿到 shiftId
|
||||
# ⚠️ 禁止用 class search 全局搜索班次
|
||||
|
||||
# 2. 获取部门成员
|
||||
dws contact dept search --query "研发部" --format json
|
||||
dws contact dept list-members --ids <deptId> --format json
|
||||
|
||||
# 3. 回显确认 → 用户确认 → 执行
|
||||
python scripts/attendance_schedule_import.py \
|
||||
--group-id 123456 \
|
||||
--schedules '[...]' \
|
||||
--confirm
|
||||
```
|
||||
|
||||
## 配套脚本
|
||||
|
||||
| 脚本 | 用途 | CLI 参数 |
|
||||
|------|------|---------|
|
||||
| [attendance_schedule_import.py](../../scripts/attendance_schedule_import.py) | 排班导入(含校验、回显、执行) | `--group-id --schedules --confirm` |
|
||||
| [attendance_schedule_export.py](../../scripts/attendance_schedule_export.py) | 排班查询导出(分批查询、排班表 Excel) | `--users --start --end [--output]` |
|
||||
|
||||
---
|
||||
|
||||
## 排班查询导出工作流
|
||||
|
||||
> 当用户提到"查看排班"、"排班表"、"导出排班"、"排班记录"时,走此工作流。
|
||||
> **禁止直接调用 `dws attendance schedule get`**,必须通过脚本执行,脚本自动处理分批、姓名转换、班次名称转换、排班表格式输出。
|
||||
|
||||
### 查询导出 — 阶段 1: 参数收集
|
||||
|
||||
1. **员工范围**(必填):需获取 userId 列表
|
||||
- 指定员工姓名 → `dws aisearch person` 获取 userId
|
||||
- 指定部门 → `dws contact dept search` + `dws contact dept list-members`
|
||||
- 指定考勤组全员 → `dws attendance group filtered-get --member`
|
||||
- 用户已给 userId 列表 → 直接使用
|
||||
2. **日期范围**(必填):开始日期 ~ 结束日期(YYYY-MM-DD)
|
||||
- 用户说"下周" → 计算下周一到周日
|
||||
- 用户说"本月" → 计算本月 1 日到月末
|
||||
- 任何缺失信息必须追问
|
||||
|
||||
### 查询导出 — 阶段 2: 调用脚本
|
||||
|
||||
```bash
|
||||
python scripts/attendance_schedule_export.py \
|
||||
--users <userId1,userId2,...> \
|
||||
--start <YYYY-MM-DD> \
|
||||
--end <YYYY-MM-DD> \
|
||||
[--output <output_path.xlsx>]
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `--users`(必填):userId 列表,逗号分隔
|
||||
- `--start`(必填):开始日期,格式 YYYY-MM-DD
|
||||
- `--end`(必填):结束日期,格式 YYYY-MM-DD
|
||||
- `--output`(可选):输出文件路径,默认 `attendance_schedule_<start>_<end>.xlsx`
|
||||
|
||||
脚本内部自动处理:
|
||||
1. **分批查询**:超过 20 人自动分批调用 `dws attendance schedule get`
|
||||
2. **班次名称转换**:classId → className(优先从记录中提取,缺失时回退 class search)
|
||||
3. **姓名转换**:userId → 员工姓名
|
||||
4. **排班表格式**:日历表(行=员工,列=日期,单元格=班次名称)
|
||||
5. **Excel 输出**:钉钉风格美化排版
|
||||
|
||||
### 查询导出 — 阶段 3: 返回结果给用户
|
||||
|
||||
- 将脚本 stdout 输出的摘要信息(人数、日期、记录数、预览表格)原样转告用户
|
||||
- 提醒用户完整排班表已导出到 Excel 文件
|
||||
- 如果执行失败,将 stderr 错误信息转告用户
|
||||
|
||||
### 查询导出示例
|
||||
|
||||
**用户说**: "帮我导出研发组下周的排班表"
|
||||
|
||||
```bash
|
||||
# 1. 获取考勤组成员
|
||||
dws attendance group search --query "研发组" --format json
|
||||
dws attendance group filtered-get --group-id <groupId> --member --format json
|
||||
|
||||
# 2. 调用脚本导出
|
||||
python scripts/attendance_schedule_export.py \
|
||||
--users user001,user002,user003 \
|
||||
--start 2026-05-18 \
|
||||
--end 2026-05-24
|
||||
```
|
||||
|
||||
**用户说**: "帮我查看张三和李四本月的排班"
|
||||
|
||||
```bash
|
||||
# 1. 获取 userId
|
||||
dws aisearch person --keyword "张三" --dimension name --format json
|
||||
dws aisearch person --keyword "李四" --dimension name --format json
|
||||
|
||||
# 2. 调用脚本导出
|
||||
python scripts/attendance_schedule_export.py \
|
||||
--users user001,user002 \
|
||||
--start 2026-05-01 \
|
||||
--end 2026-05-31
|
||||
```
|
||||
@@ -0,0 +1,140 @@
|
||||
# 假期余额导出参考 (attendance-vacation)
|
||||
|
||||
> 本文档由 `attendance.md` 路由调用。当用户提到“导出假期余额”、“假期余额列表”、“所有假期规则余额”、“假期余额 Excel”、“年假/病假/调休余额导出”等诉求时,必须先阅读本文档,再调用配套脚本。
|
||||
|
||||
## 强制门禁
|
||||
|
||||
**任何调用 `attendance_vacation_balance.py` 的请求,都必须经过本文档定义的工作流,禁止只凭脚本路径或 `--help` 自行拼命令。**
|
||||
|
||||
执行前必须确认:
|
||||
- 人员范围:指定员工 / 部门 / 多部门;缺失时必须追问
|
||||
- 导出范围:默认导出所有假期规则余额;用户指定“年假/病假/调休”等时才传 `--leave-keywords`
|
||||
- 输出形式:生成 Excel,不在对话中粘贴完整表格
|
||||
|
||||
## 核心原则
|
||||
|
||||
Agent 只负责解析人员范围并获取 userId 列表;假期规则查询、`leaveCode` 解析、假期规则单位解析、余额查询、字段解析、用户信息补齐、Excel 生成都由脚本完成。
|
||||
|
||||
`--leave-keywords` 是 `attendance_vacation_balance.py` 的脚本入参,只用于按假期名称筛选导出列;**不是** `dws attendance vacation balance` 的入参。`vacation balance` 当前只支持批量 `--users` 和单个 `--leave-code`,因此脚本必须先获取假期规则列表,再按每个匹配到的 `leaveCode` 分别查询余额。
|
||||
|
||||
脚本输出结构参考钉钉假期余额列表:
|
||||
- 每名员工一行
|
||||
- 基础列固定:`姓名`、`部门`、`入职时间`、`首次工作时间`
|
||||
- 假期规则横向动态展开为多列,表头必须携带 `vacation types` 返回的规则单位,例如 `年假(天)`、`病假(天)`、`调休(小时)`
|
||||
- 特殊值统一展示为 `不限制余额`、`不适用`、`未设置`
|
||||
- 当某个假期规则 `leaveCode` 查询余额时接口返回“假期类型没有余额”类业务错误,表示该规则不限制余额,脚本应为该规则列填充 `不限制余额`
|
||||
- 当余额记录中返回 `visible=false`,表示该员工不适用该假期规则,脚本应为该员工 + 该规则单元格填充 `不适用`
|
||||
- 当接口返回“员工未设置入职时间”或“员工未设置首次参加工作时间”类业务错误,表示该假期规则依赖员工时间字段且当前员工缺失配置,脚本应为该员工 + 该规则单元格填充 `不适用`
|
||||
- 当 `vacation types` 返回假期规则 `source=external`,表示该规则由开放接口写入;若这类外部规则调用余额接口失败且不是权限错误,脚本不应阻断导出,应为该员工 + 该规则单元格填充 `外部规则暂无余额,需通过接口初始化更新余额`
|
||||
|
||||
## 严格禁止 (NEVER DO)
|
||||
|
||||
- 禁止凭历史记忆复用 userId、deptId、leaveCode
|
||||
- 禁止 Agent 手工汇总、转置或目测假期余额
|
||||
- 禁止 Agent 自行只查单个 `leave-code` 再声称是“所有假期规则余额”;如需导出所有假期规则余额,必须交由脚本按假期规则列表逐个 `leaveCode` 查询并汇总
|
||||
- 禁止直接输出裸 userId;脚本会通过 `contact user get` 转换姓名和部门
|
||||
- 禁止把 Excel 明细内容完整贴在对话里,只返回路径和摘要
|
||||
|
||||
## 严格要求 (MUST DO)
|
||||
|
||||
- 所有 `dws` 命令必须携带 `--format json`
|
||||
- 假期规则名称`--leave-keywords`与假期规则`leaveCode`的映射必须从 `vacation types` 实时建立,禁止硬编码
|
||||
- 假期规则单位必须从 `vacation types` 实时读取,优先使用 `leaveViewUnit` 等展示单位字段,并在 Excel 表头中展示为 `假期名称(单位)`
|
||||
- 假期规则来源必须从 `vacation types` 实时读取;当 `source=external` 时按外部接口写入规则处理
|
||||
- 任何接口失败必须向用户清晰报错,禁止静默吞掉
|
||||
- `vacation balance` 返回“假期类型没有余额”时不作为致命错误处理,应按该假期规则 `leaveCode` 生成 `不限制余额`
|
||||
- `vacation balance` 返回员工维度 `visible=false` 时不作为查询失败处理,应按该员工 + 假期规则 `leaveCode` 生成 `不适用`
|
||||
- `vacation balance` 返回“员工未设置入职时间”或“员工未设置首次参加工作时间”时不作为查询失败处理,应按该员工 + 假期规则 `leaveCode` 生成 `不适用`
|
||||
- 对 `source=external` 的外部假期规则,`vacation balance` 查询失败且不是权限错误时不作为导出失败处理,应按该员工 + 假期规则 `leaveCode` 生成 `外部规则暂无余额,需通过接口初始化更新余额`
|
||||
|
||||
## 涉及工具
|
||||
|
||||
| 工具 | 用途 | 安全等级 |
|
||||
|------|------|---------|
|
||||
| `dws attendance vacation types` | 获取当前可见的假期规则清单,用于建立假期名称/关键词 → `leaveCode` 的映射,读取 `leaveViewUnit` 等规则单位、`source` 规则来源,并决定 Excel 假期列顺序 | 只读 |
|
||||
| `dws attendance vacation balance` | 按批量 `--users` + 单个 `--leave-code` 查询员工假期余额;由脚本按匹配到的 `leaveCode` 逐个调用 | 只读 |
|
||||
| `dws aisearch person` | 按姓名搜索用户获取 userId(搜人首选) | 只读 |
|
||||
| `dws contact user get` | 批量查询 userId → 用户信息(姓名/部门/入职时间等),用于 Excel 基础列补齐 | 只读 |
|
||||
| `dws contact dept search` | 搜索部门获取 deptId | 只读 |
|
||||
| `dws contact dept list-members` | 获取部门成员 userId 列表 | 只读 |
|
||||
|
||||
## 工作流
|
||||
|
||||
### 阶段 0:参数解析
|
||||
|
||||
1. 识别人员范围:
|
||||
- 指定员工姓名:用 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 获取 userId
|
||||
- 指定部门:用 `dws contact dept search --query "<部门名>" --format json`,再用 `dws contact dept list-members --ids <deptId> --format json` 获取成员
|
||||
- 已提供 userId:直接使用
|
||||
2. 识别假期列范围:
|
||||
- 未指定假期类型:导出所有假期规则余额,不传 `--leave-keywords`
|
||||
- 指定假期类型:传 `--leave-keywords "年假,病假"`
|
||||
|
||||
### 阶段 1: 获取完整人员列表
|
||||
|
||||
**场景 A — 指定员工姓名**:
|
||||
```bash
|
||||
dws aisearch person --keyword "<员工姓名>" --dimension name --format json
|
||||
```
|
||||
|
||||
**场景 B — 按部门查询**:
|
||||
```bash
|
||||
dws contact dept search --query "<部门名>" --format json
|
||||
dws contact dept list-members --ids <deptId> --format json
|
||||
```
|
||||
|
||||
**场景 C — 多个部门**: 对每个部门分别执行 B,汇总去重。
|
||||
|
||||
**场景 D — 全公司**: 暂不支持,引导用户指定部门。
|
||||
|
||||
**场景 E — 用户已给 userId 列表**: 直接跳过本步。
|
||||
|
||||
### 阶段 2:调用脚本生成 Excel
|
||||
|
||||
```bash
|
||||
python scripts/attendance_vacation_balance.py \
|
||||
--users <userId1>,<userId2>,... \
|
||||
[--leave-keywords "年假,病假,调休"] \
|
||||
[--out 假期余额列表.xlsx]
|
||||
```
|
||||
|
||||
脚本内部自动执行:
|
||||
1. `dws attendance vacation types --format json` 获取假期规则列表、`leaveCode`、展示单位、规则来源 `source` 和列顺序
|
||||
2. 对匹配到的每个假期规则,调用 `dws attendance vacation balance --users <批量用户> --leave-code <单个leaveCode> --format json` 查询余额;`vacation balance` 不支持 `--leave-keywords`
|
||||
3. 处理特殊业务返回:
|
||||
- 返回“假期类型没有余额”类业务错误:该 `leaveCode` 对应列填充 `不限制余额`
|
||||
- 返回员工维度 `visible=false`:该员工 + 该 `leaveCode` 单元格填充 `不适用`
|
||||
- 返回“员工未设置入职时间”或“员工未设置首次参加工作时间”:该员工 + 该 `leaveCode` 单元格填充 `不适用`
|
||||
- `source=external` 的外部假期规则查询失败且不是权限错误:该员工 + 该 `leaveCode` 单元格填充 `外部规则暂无余额,需通过接口初始化更新余额`
|
||||
4. `dws contact user get --ids <批量用户> --format json` 获取姓名、部门等信息
|
||||
5. 生成 Excel:`attendance_vacation_balance_<yyyyMMdd_HHmmss>.xlsx`
|
||||
|
||||
### 阶段 3:返回结果
|
||||
|
||||
向用户返回脚本 stdout 摘要即可,必须包含:
|
||||
- 输出文件路径
|
||||
- 员工数量
|
||||
- 假期规则列数
|
||||
- 如传了 `--leave-keywords`,说明筛选关键词
|
||||
|
||||
不要粘贴 Excel 全量内容。
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误 | 处理方式 |
|
||||
|------|---------|
|
||||
| 权限不足 | 提示当前账号无权查询目标员工假期余额,需管理员或管理范围权限 |
|
||||
| 人员范围缺失 | 追问员工或部门,禁止猜测 |
|
||||
| 无假期规则 | 提示未匹配到假期规则,建议先执行 `dws attendance vacation types --format json` 验证 |
|
||||
| 假期类型没有余额 | 不作为失败返回;按对应假期规则 `leaveCode` 填充 `不限制余额` |
|
||||
| `visible=false` | 不作为失败返回;按对应员工 + 假期规则 `leaveCode` 填充 `不适用` |
|
||||
| 员工未设置入职时间 / 首次参加工作时间 | 不作为失败返回;说明该规则依赖员工时间字段,按对应员工 + 假期规则 `leaveCode` 填充 `不适用` |
|
||||
| 外部假期规则查询失败 | 当假期规则 `source=external` 且失败不是权限错误时,不作为导出失败;按对应员工 + 假期规则 `leaveCode` 填充 `外部规则暂无余额,需通过接口初始化更新余额` |
|
||||
| openpyxl 缺失 | 提示执行 `pip install openpyxl` |
|
||||
| 接口返回结构不确定 | 使用脚本 `--inspect` 重新执行一次,查看首条原始结构 |
|
||||
|
||||
## 配套脚本
|
||||
|
||||
| 脚本 | 场景 | CLI 参数 |
|
||||
|------|------|---------|
|
||||
| [attendance_vacation_balance.py](../../scripts/attendance_vacation_balance.py) | 假期余额列表 Excel 导出;脚本按假期名称关键词筛选规则,并逐个 `leaveCode` 调用余额查询 | `--users [--leave-keywords] [--out] [--inspect]` |
|
||||
@@ -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 的命令均适用**
|
||||
>
|
||||
@@ -97,21 +97,20 @@ Usage:
|
||||
dws attendance schedule import [flags]
|
||||
Example:
|
||||
dws attendance schedule import --group-id 123456 \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \
|
||||
--yes
|
||||
Flags:
|
||||
--group-id string 考勤组(必填,传入考勤组ID)
|
||||
--schedules string 排班记录 JSON 数组(必填)
|
||||
--yes 跳过确认提示
|
||||
--group-id string 考勤组(必填,传入考勤组ID);--help 主名为 --groupId,--group-id 为别名
|
||||
--schedules string 排班记录 JSON 数组(必填);--help 主名为 --scheduleVOS,--schedules 为别名
|
||||
--yes 跳过确认提示;--help 主名为 --user-say-yes,--yes 为别名
|
||||
```
|
||||
|
||||
为排班制考勤组导入排班记录。`--schedules` 为 JSON 数组,每条记录包含:
|
||||
- `userId`: 员工ID
|
||||
- `classId`: 班次ID
|
||||
- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22
|
||||
- `checkBeginTime`: 开始打卡时间
|
||||
- `checkEndTime`: 结束打卡时间
|
||||
- `isRest`: 是否休息日 Y/N(可选)
|
||||
- `userId`: 员工ID(必填)
|
||||
- `classId`: 班次ID(必填)
|
||||
- `workDate`: 工作日期(YYYY-MM-DD),如 2026-04-22(必填)
|
||||
- `isRest`: 是否休息日 Y/N(**必填**,服务端要求传入)
|
||||
- `checkBeginTime` / `checkEndTime`: 开始/结束打卡时间(可传,但当前不会进入后端 payload,实际打卡时段以 `classId` 对应班次为准)
|
||||
|
||||
#### AI 调用 `schedule import` 的二次确认流程
|
||||
|
||||
@@ -259,7 +258,7 @@ Flags:
|
||||
--adjustment-id int 补卡规则主键 ID (必填)
|
||||
```
|
||||
|
||||
根据补卡规则主键 ID 查询对应的补卡规则详情。主键 ID 可从 `adjustment search` 返回结果中提取,也有可能来源于用户手动输入。**注意:已被删除或被更新覆盖的补卡规则无法查询到。**
|
||||
**注意:本命令当前拿不到有效的补卡规则详情,不要依赖它。** 服务端对任意 `--adjustment-id`(含不存在的 ID)都返回同一个 `{"success":true,"有效期类型":"..."}`,不返回规则明细,也无法据此判断规则是否存在;且 `adjustment search` 返回的默认补卡规则 `entityVO.id` 为 `null`,`search → get` 取 id 的链路走不通。补卡规则内容请直接看 `adjustment search` 的返回结果。
|
||||
|
||||
### 分页查询加班规则,支持按名称搜素
|
||||
```
|
||||
@@ -534,12 +533,17 @@ Flags:
|
||||
Usage:
|
||||
dws attendance summary [flags]
|
||||
Example:
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00"
|
||||
dws attendance summary --user USER_ID --date 2026-03-12 --stats-type week
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type month
|
||||
Flags:
|
||||
--date string 工作日期, 格式 yyyy-MM-dd HH:mm:ss (必填)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
--date string 查询日期, 格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss (必填)
|
||||
--stats-type string 统计类型: week 周统计 / month 月统计 (必填)
|
||||
--tag-name string 标签名称 (可选)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
```
|
||||
|
||||
`summary` 必须同时传 `--user`、`--date`、`--stats-type`,缺一即报错(如 C0002)。`--stats-type` 只能是 `week`(周统计)或 `month`(月统计)。
|
||||
|
||||
### 查询考勤组与考勤规则
|
||||
```
|
||||
Usage:
|
||||
@@ -801,10 +805,10 @@ Example:
|
||||
dws attendance vacation balance --users userId1,userId2 --leave-code a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
||||
Flags:
|
||||
--users string 目标员工 ID 列表, 逗号分隔 (必填)
|
||||
--leave-code string 假期规则 code (选填,不传则查询所有假期规则余额)
|
||||
--leave-code string 假期规则 code (必填,服务端要求非空,不传返回 INVALID_PARAMS)
|
||||
```
|
||||
|
||||
调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取;不传 `--leave-code` 时查询所有假期规则余额。认证信息(corpId、opUserId)由系统自动注入。
|
||||
调用 MCP 工具 get_leave_balance_quota 查询指定员工的假期余额。例如:查询某员工年假还剩多少、病假额度等。`--leave-code` 可通过 `vacation types` 获取。**注意:`--help` 虽标"选填",但服务端要求 `leaveCode` 非空,实际必填;不传会返回 `INVALID_PARAMS`(corpId、opUserId、leaveCode、targetUserIds 不能为空)。** 若要一次查所有假期规则余额,必须走 [attendance-vacation.md](./attendance-vacation.md) 工作流脚本逐个规则遍历。认证信息(corpId、opUserId)由系统自动注入。
|
||||
|
||||
如用户需要“所有假期规则余额 / 导出假期余额列表 / 所有假期规则余额 Excel / 按截图样式导出假期余额”,必须先读取 [attendance-vacation.md](./attendance-vacation.md),再按其中工作流调用脚本生成 Excel。
|
||||
|
||||
@@ -821,7 +825,7 @@ Flags:
|
||||
--end string 查询结束日期, 格式 YYYY-MM-DD (必填)
|
||||
```
|
||||
|
||||
调用 MCP 工具 get_leave_balance_records 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。
|
||||
调用 MCP 工具 get_leave_balance_records_v2 查询指定员工的假期余额变更记录。例如:查询某员工年假变更历史、请假扣减记录等。`--leave-code` 必填,可通过 `vacation types` 获取。认证信息(corpId、opUserId)由系统自动注入。
|
||||
|
||||
### 更新假期规则(写场景接口,必须走二次确认流程)
|
||||
|
||||
@@ -956,7 +960,7 @@ Usage:
|
||||
dws attendance checkin records [flags]
|
||||
Example:
|
||||
dws attendance checkin records \
|
||||
--operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00"
|
||||
--operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 --start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00"
|
||||
Flags:
|
||||
--end string 结束时间, 格式 yyyy-MM-dd HH:mm:ss(必填)
|
||||
--operator-corp-id string 操作者企业 ID(必填)
|
||||
@@ -986,7 +990,7 @@ Flags:
|
||||
用户说"班次详情/某个班次的具体信息" → `class search --name "..."`(search 直出,直接返回详情)。`class get` 仅在需要按已知 classId 精确查询时使用
|
||||
用户说"更新班次/修改班次/班次改名/修改上下班时间" → `class update`
|
||||
用户说"补卡规则/补卡设置" → `adjustment search`(返回结果已包含全量属性,无需再调 get)
|
||||
用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出)。`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用
|
||||
用户说"补卡规则详情/某条补卡规则的具体信息" → `adjustment search --name "..."`(search 直出,已含全量属性)。**不要用 `adjustment get`**:当前服务端对任意 id 都只返回"有效期类型"、拿不到规则明细
|
||||
用户说"加班规则/加班设置/加班计算" → `overtime search`(返回结果已包含全量属性,无需再调 get)
|
||||
用户说"加班规则详情/某条加班规则的具体信息" → `overtime search --name "..."`(search 直出)。如需查已删除/被覆盖的历史记录 → `overtime get`
|
||||
用户说"考勤组列表/有哪些考勤组" → `group search`
|
||||
@@ -1022,7 +1026,7 @@ Flags:
|
||||
```bash
|
||||
# 导入排班记录
|
||||
dws attendance schedule import --group-id 123456 \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","checkBeginTime":"09:00","checkEndTime":"18:00"}]' \
|
||||
--schedules '[{"userId":"user001","classId":123,"workDate":"2026-04-22","isRest":"N"}]' \
|
||||
--yes --format json
|
||||
|
||||
# 获取排班记录 — 禁止直接调用,必须走 attendance-schedule.md 排班查询导出工作流
|
||||
@@ -1078,7 +1082,7 @@ dws attendance group create --name "研发考勤组" --type FIXED --group-vo '{"
|
||||
dws attendance group create --name "自由工时分组" --type NONE --timeout 10 --format json
|
||||
|
||||
# 查看考勤统计摘要
|
||||
dws attendance summary --user <USER_ID> --date "2026-03-12 15:00:00" --format json
|
||||
dws attendance summary --user <USER_ID> --date 2026-03-12 --stats-type week --format json
|
||||
|
||||
# 查看考勤组和规则
|
||||
dws attendance rules --date 2026-03-14 --format json
|
||||
@@ -1156,7 +1160,7 @@ dws attendance vacation save-balance --target user001 \
|
||||
--num 8 --reason "绩效奖励发放3天" --format json
|
||||
|
||||
# 查询签到记录
|
||||
dws attendance checkin records --operator-staff-id op001 --staff-ids user001,user002 \
|
||||
dws attendance checkin records --operator-corp-id corp001 --operator-staff-id op001 --staff-ids user001,user002 \
|
||||
--start "2026-04-01 00:00:00" --end "2026-04-07 00:00:00" --format json
|
||||
```
|
||||
|
||||
@@ -1187,7 +1191,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use
|
||||
- `class get` 的 `--class-id` 必填,班次 ID 可从 `class search` 结果中提取
|
||||
- `class search` 返回结果已包含全量属性,无需再调用 `class get`;`class get` 仅在需要按已知 classId 精确查询时使用
|
||||
- `class update` 的 `--class-id` 必填,其余均可选,仅需对要修改的字段赋值,未传字段会自动从已有配置补充;由于保存班次耗时较久,建议加 `--timeout 10`
|
||||
- `adjustment search` 返回结果已包含全量属性,无需再调用 `adjustment get`;`adjustment get` 仅在需要按已知 adjustmentId 精确查询时使用
|
||||
- `adjustment search` 返回结果已包含全量属性;`adjustment get` 当前服务端对任意 id 都只返回"有效期类型"、无规则明细,不可用,补卡规则详情一律看 `adjustment search` 返回
|
||||
- `overtime search` 返回结果已包含全量属性,无需再调用 `overtime get`;`overtime get` 仅在需要按已知 overtimeId 查询时使用(包括已删除/被覆盖的历史记录)
|
||||
- `adjustment search` / `overtime search` 分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20
|
||||
- `group search` 的分页字段为 `--page` 和 `--limit`,不传时自动使用默认值 1 / 20
|
||||
@@ -1196,7 +1200,7 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use
|
||||
- `group update` 的 --group-id 必填,其余均可选,至少需指定一个修改项;仅需对要修改的字段赋値,未传字段会从已有配置自动补充;修改打卡地址/wifi/蓝牙等复杂子对象时用 `--group-vo` 传入完整 JSON;`--group-vo` 与单字段 flag 同时传入时单字段 flag 优先级更高
|
||||
- `group create` 的 `--name` 和 `--type` 必填,`--type` 必须为 FIXED/TURN/NONE 之一;type=FIXED 时 `--group-vo` 必须包含 `workDayClassList`(非空)和 `defaultClassId`(非 null);由于保存考勤组耗时较久,建议加 `--timeout 10`
|
||||
- `group filtered-get` 的 `--group-id` 必填,`--member/--position/--wifi/--bles` 均可选,默认 false。**返回结果中如含成员 userId 列表,必须调用 `dws contact user get --ids <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
|
||||
@@ -1209,8 +1213,8 @@ dws attendance checkin records --operator-staff-id op001 --staff-ids user001,use
|
||||
- 用户 ID 需从 `contact user get-self` 或 `aisearch person` 获取
|
||||
- 考勤组 ID 需从 `rules` 命令返回结果中获取
|
||||
- `vacation types` 无需任何参数,认证信息自动注入
|
||||
- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 选填,可通过 `vacation types` 获取
|
||||
- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 选填
|
||||
- `vacation balance` 的 `--users` 为目标员工 ID 列表,逗号分隔;`--leave-code` 服务端要求非空、实际必填(`--help` 标"选填"不准),不传返回 `INVALID_PARAMS`,可通过 `vacation types` 获取
|
||||
- `vacation records` 的 `--start/--end` 使用 YYYY-MM-DD 格式,CLI 自动转换为毫秒时间戳;`--leave-code` 必填(不传无法查询),底层 MCP 工具为 `get_leave_balance_records_v2`
|
||||
- `vacation balance` 和 `vacation records` 的认证参数(corpId、opUserId)由系统自动注入,无需手动传入
|
||||
- `vacation update-type` 的 `--leave-code` 必填;其他字段均为可选,但至少需传一个更新字段
|
||||
- `vacation update-type` 的 `--visibility-rules` 为 JSON 数组字符串,格式:`[{"type":"dept","visible":["1","2","3"]}]`,type 可取值 staff/label/dept
|
||||
|
||||
@@ -48,7 +48,25 @@ def get_my_user_id(dry_run: bool = False) -> Optional[str]:
|
||||
return '<MY_USER_ID>'
|
||||
if not data or not isinstance(data, dict):
|
||||
return None
|
||||
return data.get('userId') or data.get('userid')
|
||||
# 兼容两种结构:
|
||||
# 1) 顶层直接给 userId
|
||||
# 2) {result: [{orgEmployeeModel: {userId}}]} 包裹
|
||||
uid = data.get('userId') or data.get('userid')
|
||||
if uid:
|
||||
return uid
|
||||
inner = data.get('result')
|
||||
if isinstance(inner, dict):
|
||||
inner = [inner]
|
||||
if isinstance(inner, list):
|
||||
for item in inner:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
emp = item.get('orgEmployeeModel')
|
||||
if isinstance(emp, dict) and emp.get('userId'):
|
||||
return emp['userId']
|
||||
if item.get('userId'):
|
||||
return item['userId']
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
考勤报表导出 — 签到记录粒度
|
||||
|
||||
[AI Agent 强制门禁] 调用本脚本前必须先阅读:
|
||||
dingtalk-workspace/references/products/attendance-report.md
|
||||
|
||||
本脚本是"签到报表导出工作流"的执行末端,工作流完整定义在 attendance-report.md。
|
||||
|
||||
[严禁] 仅凭本脚本 docstring 或 --help 输出就直接拼命令执行。
|
||||
|
||||
导出签到报表:每条签到记录一行,包含签到详情(地点、经纬度、拜访客户、图片等)。
|
||||
|
||||
前置依赖:
|
||||
pip install openpyxl
|
||||
|
||||
用法:
|
||||
python attendance_report_checkin.py \
|
||||
--users userId1,userId2,... \
|
||||
--start "2026-04-01 00:00:00" \
|
||||
--end "2026-04-07 23:59:59" \
|
||||
[--out 签到报表_研发部_20260401_20260407.xlsx]
|
||||
[--inspect]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
# ── 前置依赖检查(在任何 dws 调用之前就检测,避免查完数据才报错)───────
|
||||
_missing_deps: list[str] = []
|
||||
try:
|
||||
import openpyxl as _openpyxl_check # noqa: F401
|
||||
except ImportError:
|
||||
_missing_deps.append("openpyxl")
|
||||
try:
|
||||
import requests as _requests_check # noqa: F401
|
||||
except ImportError:
|
||||
_missing_deps.append("requests")
|
||||
try:
|
||||
from PIL import Image as _pil_check # noqa: F401
|
||||
except ImportError:
|
||||
_missing_deps.append("Pillow")
|
||||
|
||||
if _missing_deps:
|
||||
print(
|
||||
f"[ERROR] 缺少以下依赖:{', '.join(_missing_deps)}\n"
|
||||
f" 请先安装:pip install {' '.join(_missing_deps)}\n"
|
||||
"安装后重新执行本脚本。\n"
|
||||
"(签到报表需要 openpyxl 生成 Excel、requests + Pillow 下载并嵌入签到图片)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
import attendance_report_common as cmn
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 自动获取当前认证的 operator 信息
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _get_operator_context() -> tuple[str, str]:
|
||||
"""
|
||||
从 `dws auth status --format json` 自动获取当前认证的 corp_id 和 user_id。
|
||||
|
||||
签到接口 (checkin records) 必须传 --operator-corp-id 和 --operator-staff-id,
|
||||
这两个值来自 dws 的认证上下文(即 `dws auth status` 返回的 corp_id / user_id),
|
||||
而非 `dws contact user get-self` 返回的长格式 userId。
|
||||
|
||||
Returns:
|
||||
(operator_corp_id, operator_staff_id) 元组
|
||||
|
||||
Raises:
|
||||
SystemExit: 未登录或无法获取认证信息时直接退出
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["dws", "auth", "status", "--format", "json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
cmn.error("未找到 dws 命令,请确认 dws CLI 已安装并在 PATH 中")
|
||||
sys.exit(2)
|
||||
except subprocess.TimeoutExpired:
|
||||
cmn.error("dws auth status 超时,请检查网络或重新登录(dws auth login)")
|
||||
sys.exit(2)
|
||||
|
||||
if result.returncode != 0:
|
||||
cmn.error(
|
||||
"获取认证信息失败,请确保已执行 dws auth login 完成登录。\n"
|
||||
f" 错误详情:{(result.stderr or result.stdout or '').strip()}"
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
try:
|
||||
auth_data = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
cmn.error(f"dws auth status 返回非 JSON:{result.stdout[:200]!r}")
|
||||
sys.exit(2)
|
||||
|
||||
corp_id = auth_data.get("corp_id") or auth_data.get("corpId") or ""
|
||||
user_id = auth_data.get("user_id") or auth_data.get("userId") or ""
|
||||
|
||||
if not corp_id or not user_id:
|
||||
cmn.error(
|
||||
"无法从认证信息中提取 corp_id / user_id,请重新登录:\n"
|
||||
" dws auth login\n"
|
||||
f" 当前返回:{json.dumps(auth_data, ensure_ascii=False)[:300]}"
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
cmn.log(f"[auth] 已获取 operator 信息:corp_id={corp_id}, user_id={user_id}")
|
||||
return str(corp_id), str(user_id)
|
||||
|
||||
|
||||
# 签到接口限制:开始到结束最多 7 天
|
||||
MAX_DAYS_PER_CHECKIN_SLICE = 7
|
||||
|
||||
# 签到接口限制:每次最多查 100 人(与 check record 一致)
|
||||
MAX_USERS_PER_CHECKIN_BATCH = 50
|
||||
|
||||
# 最多支持 9 张图片列
|
||||
MAX_IMAGE_COLUMNS = 9
|
||||
|
||||
# 报表表头(与用户要求严格对齐)
|
||||
REPORT_HEADERS = [
|
||||
"姓名", "部门", "完整部门",
|
||||
"日期", "时间",
|
||||
"经度", "纬度", "地点", "详细地址",
|
||||
"拜访客户", "客户部门名称", "工作内容",
|
||||
"手机标识",
|
||||
] + [f"图片{i}" for i in range(1, MAX_IMAGE_COLUMNS + 1)]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"导出签到报表 — 签到记录粒度。"
|
||||
"[强制] AI Agent 必须先读 references/products/attendance-report.md 再调用本脚本。"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--users", required=True,
|
||||
help="userId 列表,逗号分隔(必填)")
|
||||
parser.add_argument("--start", required=True,
|
||||
help='开始时间,YYYY-MM-DD 或 "YYYY-MM-DD HH:mm:ss"(必填)')
|
||||
parser.add_argument("--end", required=True,
|
||||
help='结束时间,YYYY-MM-DD 或 "YYYY-MM-DD HH:mm:ss"(必填)')
|
||||
parser.add_argument("--out", default="",
|
||||
help="输出 xlsx 文件名;不传则按规范自动生成")
|
||||
parser.add_argument("--inspect", action="store_true",
|
||||
help="首次跑时打印首条记录原始结构(用于核对真实字段)")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 签到接口时间切片(7 天一段)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def slice_checkin_date_range(
|
||||
start: datetime, end: datetime,
|
||||
) -> list[cmn.DateSlice]:
|
||||
"""将日期范围按 7 天一段切片(签到接口限制开始到结束最多 7 天)。"""
|
||||
slices: list[cmn.DateSlice] = []
|
||||
current = start
|
||||
while current <= end:
|
||||
slice_end = min(current + timedelta(days=MAX_DAYS_PER_CHECKIN_SLICE - 1), end)
|
||||
# 确保 slice_end 的时间部分是当天最后一秒
|
||||
slice_end = slice_end.replace(hour=23, minute=59, second=59)
|
||||
if slice_end > end:
|
||||
slice_end = end
|
||||
slices.append(cmn.DateSlice(
|
||||
start=current,
|
||||
end=slice_end,
|
||||
))
|
||||
current = slice_end.replace(hour=0, minute=0, second=0) + timedelta(days=1)
|
||||
return slices
|
||||
|
||||
|
||||
def chunk_checkin_users(user_ids: list[str]) -> list[list[str]]:
|
||||
"""将用户列表按 MAX_USERS_PER_CHECKIN_BATCH 分批。"""
|
||||
batches: list[list[str]] = []
|
||||
for i in range(0, len(user_ids), MAX_USERS_PER_CHECKIN_BATCH):
|
||||
batches.append(user_ids[i:i + MAX_USERS_PER_CHECKIN_BATCH])
|
||||
return batches
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 签到数据查询
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def query_checkin_batch(
|
||||
user_batch: list[str],
|
||||
date_slice: cmn.DateSlice,
|
||||
operator_corp_id: str,
|
||||
operator_staff_id: str,
|
||||
stats: cmn.CallStats,
|
||||
*,
|
||||
inspect: bool = False,
|
||||
inspected_flag: list[bool] | None = None,
|
||||
) -> list[dict]:
|
||||
"""查询一批用户在一个时间片内的签到记录。"""
|
||||
cmn.log(
|
||||
f"[checkin] users={len(user_batch)} "
|
||||
f"slice={date_slice.label}"
|
||||
)
|
||||
try:
|
||||
payload = cmn.run_dws([
|
||||
"attendance", "checkin", "records",
|
||||
"--operator-corp-id", operator_corp_id,
|
||||
"--operator-staff-id", operator_staff_id,
|
||||
"--staff-ids", ",".join(user_batch),
|
||||
"--start", date_slice.start_str,
|
||||
"--end", date_slice.end_str,
|
||||
])
|
||||
stats.total_dws_calls += 1
|
||||
except cmn.DwsCallError as exc:
|
||||
stats.total_dws_calls += 1
|
||||
stats.failed_calls += 1
|
||||
if exc.is_permission_error:
|
||||
cmn.error(
|
||||
"权限错误:当前账号无管理员权限,无法导出签到报表。\n"
|
||||
"请联系考勤管理员或换号重试。"
|
||||
)
|
||||
raise SystemExit(2) from exc
|
||||
err_msg = str(exc)
|
||||
if "missing required flag" in err_msg.lower():
|
||||
cmn.error(
|
||||
"签到接口调用失败:缺少必需参数。\n"
|
||||
"请确保已执行 dws auth login 完成登录,以便自动获取 operator 参数。\n"
|
||||
f"当前 operator: corp_id={operator_corp_id}, staff_id={operator_staff_id}\n"
|
||||
f"原始错误:{err_msg}"
|
||||
)
|
||||
raise SystemExit(2) from exc
|
||||
stats.add_warning(f"[checkin failed] {date_slice.label}: {exc}")
|
||||
return []
|
||||
|
||||
records = cmn.extract_records(payload)
|
||||
if inspect and records and inspected_flag is not None and not inspected_flag[0]:
|
||||
cmn.dump_first_record_for_inspection(records, "checkin-records")
|
||||
inspected_flag[0] = True
|
||||
return records
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 签到记录 → 报表行转换
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _format_timestamp(timestamp_value: Any) -> tuple[str, str]:
|
||||
"""
|
||||
将签到时间戳转换为 (日期字符串, 时间字符串)。
|
||||
|
||||
签到接口的 timestamp 为毫秒时间戳。
|
||||
"""
|
||||
if timestamp_value is None:
|
||||
return "", ""
|
||||
try:
|
||||
ts = float(timestamp_value)
|
||||
# 判断是毫秒还是秒级时间戳
|
||||
if ts > 1_000_000_000_000:
|
||||
ts = ts / 1000
|
||||
dt = datetime.fromtimestamp(ts)
|
||||
return dt.strftime("%Y-%m-%d"), dt.strftime("%H:%M:%S")
|
||||
except (ValueError, TypeError, OSError, OverflowError):
|
||||
return str(timestamp_value), ""
|
||||
|
||||
|
||||
def transform_records_to_rows(
|
||||
records: list[dict],
|
||||
user_info_map: dict[str, cmn.UserInfo],
|
||||
) -> list[list[Any]]:
|
||||
"""将签到原始记录转换为报表行(与 REPORT_HEADERS 对齐)。"""
|
||||
rows: list[list[Any]] = []
|
||||
for record in records:
|
||||
uid = cmn._first_nonempty(record, ("userId", "userid", "user_id"))
|
||||
uid_str = str(uid) if uid is not None else ""
|
||||
info = user_info_map.get(uid_str, cmn.UserInfo(name=uid_str))
|
||||
|
||||
# 姓名:优先用 resolve_user_info 的结果,回退到接口返回的 name
|
||||
name = info.name or record.get("name", uid_str)
|
||||
dept_name = info.dept_name
|
||||
# 完整部门:暂用 dept_name(如需更完整的路径可后续扩展)
|
||||
full_dept = dept_name
|
||||
|
||||
# 日期与时间
|
||||
date_str, time_str = _format_timestamp(record.get("timestamp"))
|
||||
|
||||
# 经纬度
|
||||
longitude = record.get("longitude", "")
|
||||
latitude = record.get("latitude", "")
|
||||
|
||||
# 地点
|
||||
place = record.get("place", "")
|
||||
detail_place = record.get("detailPlace", "")
|
||||
|
||||
# 拜访客户 & 客户部门名称
|
||||
customers = record.get("customers", "")
|
||||
# 签到接口暂无客户部门名称字段,预留空值
|
||||
customer_dept = ""
|
||||
|
||||
# 工作内容(备注)
|
||||
remark = record.get("remark", "")
|
||||
|
||||
# 手机标识
|
||||
mobile_id = record.get("mobileId", "")
|
||||
|
||||
# 图片列(最多 9 张)
|
||||
image_list = record.get("imageList") or []
|
||||
if isinstance(image_list, str):
|
||||
# 兼容接口可能返回逗号分隔的字符串
|
||||
image_list = [img.strip() for img in image_list.split(",") if img.strip()]
|
||||
image_cells = []
|
||||
for i in range(MAX_IMAGE_COLUMNS):
|
||||
if i < len(image_list):
|
||||
image_cells.append(image_list[i])
|
||||
else:
|
||||
image_cells.append("")
|
||||
|
||||
row = [
|
||||
name, dept_name, full_dept,
|
||||
date_str, time_str,
|
||||
longitude, latitude, place, detail_place,
|
||||
customers, customer_dept, remark,
|
||||
mobile_id,
|
||||
] + image_cells
|
||||
rows.append(row)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# main
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
raw_ids = [u.strip() for u in args.users.split(",") if u.strip()]
|
||||
if not raw_ids:
|
||||
cmn.error("--users 不能为空")
|
||||
return 2
|
||||
|
||||
# 自动识别部门ID并展开为员工userId
|
||||
user_ids = cmn.resolve_users_from_input(raw_ids)
|
||||
if not user_ids:
|
||||
cmn.error("未能解析出任何有效的员工userId")
|
||||
return 2
|
||||
cmn.log(f"[users] 最终用户列表:{len(user_ids)} 人")
|
||||
|
||||
try:
|
||||
start = cmn.parse_datetime_arg(args.start, end_of_day=False)
|
||||
end = cmn.parse_datetime_arg(args.end, end_of_day=True)
|
||||
except ValueError as exc:
|
||||
cmn.error(str(exc))
|
||||
return 2
|
||||
|
||||
if end < start:
|
||||
cmn.error(f"--end ({end}) 早于 --start ({start})")
|
||||
return 2
|
||||
|
||||
# 获取当前认证的 operator 信息(签到接口必需)
|
||||
operator_corp_id, operator_staff_id = _get_operator_context()
|
||||
|
||||
# 获取用户基础信息(姓名、部门)
|
||||
cmn.log(f"[users] 获取 {len(user_ids)} 个用户基础信息")
|
||||
user_info_map = cmn.resolve_user_info(user_ids)
|
||||
|
||||
# 分批分段查询签到记录
|
||||
user_batches = chunk_checkin_users(user_ids)
|
||||
date_slices = slice_checkin_date_range(start, end)
|
||||
stats = cmn.CallStats(
|
||||
user_batches=len(user_batches),
|
||||
date_slices=len(date_slices),
|
||||
)
|
||||
cmn.log(
|
||||
f"[plan] 共 {len(user_batches)} 批 × {len(date_slices)} 个时间片 "
|
||||
f"= {len(user_batches) * len(date_slices)} 次接口调用"
|
||||
)
|
||||
|
||||
inspected_flag = [False]
|
||||
all_records: list[dict] = []
|
||||
for batch_idx, batch in enumerate(user_batches, start=1):
|
||||
for slice_idx, date_slice in enumerate(date_slices, start=1):
|
||||
cmn.log(
|
||||
f"[batch {batch_idx}/{len(user_batches)}] "
|
||||
f"[slice {slice_idx}/{len(date_slices)}]"
|
||||
)
|
||||
records = query_checkin_batch(
|
||||
batch, date_slice,
|
||||
operator_corp_id, operator_staff_id,
|
||||
stats,
|
||||
inspect=args.inspect,
|
||||
inspected_flag=inspected_flag,
|
||||
)
|
||||
all_records.extend(records)
|
||||
|
||||
if not all_records:
|
||||
stats.add_warning("查询完成,但未得到任何签到记录")
|
||||
|
||||
# 转换为报表行
|
||||
rows = transform_records_to_rows(all_records, user_info_map)
|
||||
|
||||
# 按日期时间排序(日期列索引=3,时间列索引=4)
|
||||
rows.sort(key=lambda r: (r[3] or "", r[4] or ""))
|
||||
|
||||
# 生成 Excel
|
||||
out_name = args.out or cmn.build_output_filename(start, end, suffix="checkin")
|
||||
title = (
|
||||
f"签到报表 统计日期:{start.strftime(cmn.DATE_FMT)} "
|
||||
f"至 {end.strftime(cmn.DATE_FMT)}"
|
||||
)
|
||||
subtitle = f"报表生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
||||
|
||||
# 图片列名列表(图片1~图片9),让 write_excel_multi_sheets 自动将 URL 嵌入为缩略图
|
||||
image_column_names = [f"图片{i}" for i in range(1, MAX_IMAGE_COLUMNS + 1)]
|
||||
|
||||
checkin_sheet = {
|
||||
"name": "签到记录",
|
||||
"headers": REPORT_HEADERS,
|
||||
"rows": rows,
|
||||
"title": title,
|
||||
"subtitle": subtitle,
|
||||
"image_columns": image_column_names,
|
||||
"image_size": (60, 60),
|
||||
}
|
||||
|
||||
try:
|
||||
cmn.write_excel_multi_sheets(out_name, [checkin_sheet])
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
cmn.error(str(exc))
|
||||
return 1
|
||||
|
||||
cmn.print_summary(
|
||||
granularity_label="签到报表",
|
||||
out_path=out_name,
|
||||
user_count=len(user_ids),
|
||||
column_names=[h for h in REPORT_HEADERS],
|
||||
start=start,
|
||||
end=end,
|
||||
rows_count=len(rows),
|
||||
stats=stats,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,947 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
考勤记录报表导出脚本 — 补卡/出差/外出/请假
|
||||
|
||||
属于考勤报表导出体系,和 attendance_report_detail.py / attendance_report_monthly.py 平级。
|
||||
Agent 负责意图判断和人员获取,本脚本自包含:数据查询 → 解析 → Excel 生成。
|
||||
|
||||
数据链路:
|
||||
1. dws attendance approve list --users <ids> --types <type> --start --end
|
||||
→ 获取审批单摘要(含 originId = processInstanceId)
|
||||
2. dws oa approval detail --instance-id <originId>
|
||||
→ 获取审批单完整表单字段(extValue / detailList)
|
||||
3. 解析 formValueVOS 中的 DDHolidayField / extValue → 按天拆分行
|
||||
4. write_excel 输出
|
||||
|
||||
用法:
|
||||
python attendance_report_record.py --type leave --users <userId1,userId2> --start 2026-04-01 --end 2026-04-30
|
||||
python attendance_report_record.py --type trip --users <userId1,userId2> --start 2026-04-01 --end 2026-04-30
|
||||
python attendance_report_record.py --type out --users <userId1> --start 2026-05-01 --end 2026-05-31
|
||||
python attendance_report_record.py --type patch --users <userId1> --start 2026-05-01 --end 2026-05-31
|
||||
|
||||
支持类型: leave(请假), trip(出差), out(外出), patch(补卡)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from attendance_report_common import (
|
||||
run_dws,
|
||||
write_excel,
|
||||
resolve_user_names,
|
||||
resolve_user_info,
|
||||
UserInfo,
|
||||
log,
|
||||
warn,
|
||||
error,
|
||||
DwsCallError,
|
||||
DATE_FMT,
|
||||
)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 常量
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
SUPPORTED_TYPES = ("leave", "trip", "out", "patch")
|
||||
|
||||
COLUMNS: dict[str, list[str]] = {
|
||||
"leave": ["姓名", "考勤组", "部门", "工号", "职位", "假期类型", "请假时间",
|
||||
"请假时长(小时)", "请假时长(天)", "关联审批单", "审批单状态"],
|
||||
"trip": ["姓名", "考勤组", "部门", "工号", "职位", "出差时间",
|
||||
"出差时长", "出差单位", "关联审批单", "审批单状态"],
|
||||
"out": ["姓名", "考勤组", "部门", "工号", "职位", "外出申请时间",
|
||||
"外出时长(小时)", "外出时长(天)", "关联审批单", "审批单状态"],
|
||||
"patch": ["姓名", "考勤组", "部门", "工号", "职位", "考勤日期", "考勤时间",
|
||||
"原打卡时间", "原考勤状态", "补卡时间", "补卡结果", "关联审批单", "审批单状态"],
|
||||
}
|
||||
|
||||
SHEET_NAMES: dict[str, str] = {
|
||||
"leave": "请假记录",
|
||||
"trip": "出差记录",
|
||||
"out": "外出记录",
|
||||
"patch": "补卡记录",
|
||||
}
|
||||
|
||||
STATUS_MAP: dict[str, dict[str, str]] = {
|
||||
"COMPLETED": {"agree": "审批通过", "refuse": "已拒绝"},
|
||||
"RUNNING": {"": "审批中"},
|
||||
"TERMINATED": {"": "已撤销"},
|
||||
}
|
||||
|
||||
APPROVE_LIST_BATCH_SIZE = 50 # attendance approve list 单次最多用户数
|
||||
|
||||
# 审批详情页 URL 模板
|
||||
# 内层:aflow 审批详情页
|
||||
_AFLOW_URL_TEMPLATE = (
|
||||
"https://aflow.dingtalk.com/dingtalk/mobile/homepage.htm"
|
||||
"?corpid={corp_id}&dd_share=false&showmenu=true&back=native"
|
||||
"#/approval?procInstId={instance_id}"
|
||||
)
|
||||
# 外层:dingtalk schema 协议,在钉钉客户端侧边面板打开
|
||||
_DINGTALK_SCHEMA_TEMPLATE = (
|
||||
"dingtalk://dingtalkclient/action/openapp"
|
||||
"?corpid={corp_id}&container_type=slide_panel&app_id=-4"
|
||||
"&&redirect_url={encoded_url}"
|
||||
)
|
||||
|
||||
|
||||
class HyperlinkCell:
|
||||
"""标记单元格为超链接:Excel 中显示 label 文本,点击跳转到 url。"""
|
||||
|
||||
__slots__ = ("label", "url")
|
||||
|
||||
def __init__(self, label: str, url: str):
|
||||
self.label = label
|
||||
self.url = url
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.label
|
||||
|
||||
|
||||
def build_approve_url(corp_id: str, instance_id: str) -> str:
|
||||
"""
|
||||
构建审批单跳转链接(dingtalk:// schema)。
|
||||
|
||||
结构:外层 dingtalk schema 打开钉钉侧边面板,内部 redirect 到 aflow 审批详情页。
|
||||
"""
|
||||
from urllib.parse import quote
|
||||
|
||||
inner_url = _AFLOW_URL_TEMPLATE.format(corp_id=corp_id, instance_id=instance_id)
|
||||
encoded_url = quote(inner_url, safe="")
|
||||
return _DINGTALK_SCHEMA_TEMPLATE.format(corp_id=corp_id, encoded_url=encoded_url)
|
||||
|
||||
|
||||
def build_approve_cell(corp_id: str, instance_id: str, title: str = "") -> HyperlinkCell | str:
|
||||
"""
|
||||
构建"关联审批单"列的单元格值。
|
||||
|
||||
如果有 corp_id 和 instance_id,返回 HyperlinkCell(Excel 中为可点击链接)。
|
||||
否则返回纯文本。
|
||||
"""
|
||||
if not instance_id:
|
||||
return ""
|
||||
label = title or instance_id
|
||||
if not corp_id:
|
||||
return label
|
||||
url = build_approve_url(corp_id, instance_id)
|
||||
return HyperlinkCell(label=label, url=url)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 工具函数
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def normalize_am_pm(text: str) -> str:
|
||||
"""将时间文本中的 AM/PM 替换为 上午/下午。"""
|
||||
return text.replace(" PM", " 下午").replace(" AM", " 上午")
|
||||
|
||||
|
||||
def format_status(status: str, result: str) -> str:
|
||||
"""将 status + processInstanceResult 转为中文状态。"""
|
||||
status_upper = (status or "").upper()
|
||||
result_lower = (result or "").lower()
|
||||
group = STATUS_MAP.get(status_upper, {})
|
||||
return group.get(result_lower, group.get("", f"{status}/{result}"))
|
||||
|
||||
|
||||
def ms_to_datetime(ms: int | float | None) -> datetime | None:
|
||||
"""毫秒时间戳转 datetime。"""
|
||||
if not ms:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromtimestamp(int(ms) / 1000)
|
||||
except (OSError, ValueError, OverflowError):
|
||||
return None
|
||||
|
||||
|
||||
def ms_to_time_str(ms: int | float | None) -> str:
|
||||
"""毫秒时间戳转 HH:MM。"""
|
||||
dt = ms_to_datetime(ms)
|
||||
return dt.strftime("%H:%M") if dt else ""
|
||||
|
||||
|
||||
def ms_to_date_str(ms: int | float | None) -> str:
|
||||
"""毫秒时间戳转 YYYY-MM-DD。"""
|
||||
dt = ms_to_datetime(ms)
|
||||
return dt.strftime(DATE_FMT) if dt else ""
|
||||
|
||||
|
||||
def format_day_type(detail: dict) -> str:
|
||||
"""从 detailList 单条判断日历类型。"""
|
||||
day_type = detail.get("dayType", "")
|
||||
is_rest = detail.get("isRest", False)
|
||||
if day_type == "workDay" or (not is_rest and not day_type):
|
||||
return "工作日"
|
||||
if day_type == "restDay" or is_rest:
|
||||
return "休息日"
|
||||
if day_type == "holiday":
|
||||
return "节假日"
|
||||
return day_type or ("休息日" if is_rest else "工作日")
|
||||
|
||||
|
||||
def format_class_time(detail: dict) -> str:
|
||||
"""从 detailList 单条提取上下班时间。"""
|
||||
class_info = detail.get("classInfo", {})
|
||||
sections = class_info.get("sections", []) if class_info else []
|
||||
if not sections:
|
||||
return "未排班"
|
||||
section = sections[0]
|
||||
start_time = ms_to_time_str(section.get("startTime"))
|
||||
end_time = ms_to_time_str(section.get("endTime"))
|
||||
if start_time and end_time:
|
||||
return f"{start_time} ~ {end_time}"
|
||||
return "未排班"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 数据查询
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# 钉钉接口把"外出"和"出差"都归类到 trip(bizType=2),out 类型查不到数据。
|
||||
# 脚本通过 tagName 区分:tagName="出差" → trip,tagName="外出" → out。
|
||||
_API_TYPE_MAP: dict[str, str] = {
|
||||
"leave": "leave",
|
||||
"trip": "trip",
|
||||
"out": "trip", # 外出也用 trip 查询,再按 tagName 过滤
|
||||
"patch": "patch",
|
||||
}
|
||||
|
||||
_TAG_FILTER: dict[str, str | None] = {
|
||||
"leave": None,
|
||||
"trip": "出差",
|
||||
"out": "外出",
|
||||
"patch": None,
|
||||
}
|
||||
|
||||
|
||||
def fetch_approve_list(user_ids: list[str], record_type: str, start: str, end: str) -> list[dict]:
|
||||
"""
|
||||
分批调用 dws attendance approve list 获取审批单摘要。
|
||||
|
||||
返回列表中每条包含: userId, tagName, duration, durationUnit, beginTime, endTime, originId。
|
||||
对于 out 类型,实际用 trip 查询接口,再按 tagName="外出" 过滤;
|
||||
对于 trip 类型,按 tagName="出差" 过滤(排除外出记录)。
|
||||
"""
|
||||
api_type = _API_TYPE_MAP.get(record_type, record_type)
|
||||
tag_filter = _TAG_FILTER.get(record_type)
|
||||
|
||||
all_records: list[dict] = []
|
||||
for i in range(0, len(user_ids), APPROVE_LIST_BATCH_SIZE):
|
||||
batch = user_ids[i:i + APPROVE_LIST_BATCH_SIZE]
|
||||
users_str = ",".join(batch)
|
||||
try:
|
||||
result = run_dws([
|
||||
"attendance", "approve", "list",
|
||||
"--users", users_str,
|
||||
"--types", api_type,
|
||||
"--start", start,
|
||||
"--end", end,
|
||||
])
|
||||
records: list[dict] = []
|
||||
if isinstance(result, list):
|
||||
records = result
|
||||
elif isinstance(result, dict):
|
||||
records = result.get("approveList", result.get("list", []))
|
||||
if not isinstance(records, list):
|
||||
records = []
|
||||
# 按 tagName 过滤
|
||||
if tag_filter:
|
||||
records = [r for r in records if r.get("tagName") == tag_filter]
|
||||
all_records.extend(records)
|
||||
except DwsCallError as e:
|
||||
warn(f"查询审批列表失败(batch {i // APPROVE_LIST_BATCH_SIZE + 1}): {e}")
|
||||
return all_records
|
||||
|
||||
|
||||
def fetch_detail(instance_id: str) -> dict | None:
|
||||
"""调用 dws oa approval detail 获取审批单完整详情。"""
|
||||
try:
|
||||
result = run_dws([
|
||||
"oa", "approval", "detail",
|
||||
"--instance-id", instance_id,
|
||||
])
|
||||
return result if isinstance(result, dict) else None
|
||||
except DwsCallError as e:
|
||||
warn(f"获取审批详情失败({instance_id[:20]}...): {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 解析器
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def find_holiday_field(form_values: list[dict]) -> dict | None:
|
||||
"""从 formValueVOS 中查找 DDHolidayField 组件。"""
|
||||
for fv in form_values:
|
||||
if fv.get("componentType") == "DDHolidayField":
|
||||
return fv
|
||||
return None
|
||||
|
||||
|
||||
def parse_ext_value(field_data: dict) -> dict:
|
||||
"""解析字段的 extValue JSON 字符串。"""
|
||||
ext_str = field_data.get("extValue") or ""
|
||||
if not ext_str:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(ext_str)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return {}
|
||||
|
||||
|
||||
def parse_leave_detail(detail: dict, name_map: dict[str, str], *,
|
||||
user_info_map: dict[str, "UserInfo"] | None = None,
|
||||
group_map: dict[str, str] | None = None,
|
||||
corp_id: str = "",
|
||||
) -> list[list[str]]:
|
||||
"""解析请假审批单。"""
|
||||
form_values = detail.get("formValueVOS", [])
|
||||
user_id = detail.get("originatorUserid", "")
|
||||
dept_name = detail.get("originatorDeptName", "")
|
||||
instance_id = detail.get("processInstanceId", "")
|
||||
status = format_status(detail.get("status", ""), detail.get("processInstanceResult", ""))
|
||||
|
||||
# 用户基础信息
|
||||
info = (user_info_map or {}).get(user_id)
|
||||
user_name = info.name if info else name_map.get(user_id, user_id)
|
||||
dept = info.dept_name if info and info.dept_name else dept_name
|
||||
job_number = info.job_number if info else ""
|
||||
title = info.title if info else ""
|
||||
group_name = (group_map or {}).get(user_id, "")
|
||||
approve_cell = build_approve_cell(corp_id, instance_id, f"{user_name}提交的请假审批单")
|
||||
|
||||
holiday_field = find_holiday_field(form_values)
|
||||
if not holiday_field:
|
||||
return [[user_name, group_name, dept, job_number, title,
|
||||
"", "", "", "", approve_cell, status]]
|
||||
|
||||
# value: ["开始时间","结束时间",天数,"单位","假期类型","请假类型"]
|
||||
value_str = holiday_field.get("value", "")
|
||||
leave_type = ""
|
||||
leave_time = ""
|
||||
try:
|
||||
value_arr = json.loads(value_str)
|
||||
if isinstance(value_arr, list) and len(value_arr) >= 2:
|
||||
leave_time = normalize_am_pm(f"{value_arr[0]} ~ {value_arr[1]}")
|
||||
if len(value_arr) > 4:
|
||||
leave_type = str(value_arr[4])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
leave_time = normalize_am_pm(value_str)
|
||||
|
||||
ext = parse_ext_value(holiday_field)
|
||||
duration_day = str(ext.get("durationInDay", ""))
|
||||
duration_hour = str(ext.get("durationInHour", ""))
|
||||
|
||||
return [[user_name, group_name, dept, job_number, title,
|
||||
leave_type, leave_time, duration_hour, duration_day,
|
||||
approve_cell, status]]
|
||||
|
||||
|
||||
def _extract_time_duration_from_fields(form_values: list[dict]) -> tuple[str, str, str, str]:
|
||||
"""
|
||||
从独立表单字段中提取时间范围和时长。
|
||||
|
||||
适用于外出/出差表单的非 DDHolidayField 结构:
|
||||
- startTime (DDDateField) + finishTime (DDDateField) → 时间范围
|
||||
- duration (NumberField) → extValue 中含 durationInDay / durationInHour
|
||||
|
||||
Returns: (time_range, duration_hour, duration_day, ext_from_duration)
|
||||
"""
|
||||
start_time = ""
|
||||
end_time = ""
|
||||
duration_hour = ""
|
||||
duration_day = ""
|
||||
|
||||
for fv in form_values:
|
||||
biz_alias = (fv.get("bizAlias") or "").lower()
|
||||
name = (fv.get("name") or "").lower()
|
||||
value = fv.get("value") or ""
|
||||
|
||||
# 开始时间
|
||||
if biz_alias in ("starttime", "start_time") or "开始时间" in name:
|
||||
if value and not start_time:
|
||||
start_time = value
|
||||
# 结束时间
|
||||
if biz_alias in ("finishtime", "finish_time", "endtime", "end_time") or "结束时间" in name:
|
||||
if value and not end_time:
|
||||
end_time = value
|
||||
# 时长字段 — extValue 中有 durationInDay / durationInHour
|
||||
if biz_alias == "duration" or "时长" in name:
|
||||
ext = parse_ext_value(fv)
|
||||
if ext:
|
||||
duration_day = str(ext.get("durationInDay", ""))
|
||||
duration_hour = str(ext.get("durationInHour", ""))
|
||||
|
||||
time_range = ""
|
||||
if start_time and end_time:
|
||||
time_range = f"{start_time} ~ {end_time}"
|
||||
elif start_time:
|
||||
time_range = start_time
|
||||
|
||||
return time_range, duration_hour, duration_day
|
||||
|
||||
|
||||
def parse_out_detail(detail: dict, name_map: dict[str, str], *,
|
||||
user_info_map: dict[str, "UserInfo"] | None = None,
|
||||
group_map: dict[str, str] | None = None,
|
||||
corp_id: str = "",
|
||||
) -> list[list[str]]:
|
||||
"""解析外出审批单。兼容 DDHolidayField 和独立字段两种表单结构。"""
|
||||
form_values = detail.get("formValueVOS", [])
|
||||
user_id = detail.get("originatorUserid", "")
|
||||
dept_name = detail.get("originatorDeptName", "")
|
||||
instance_id = detail.get("processInstanceId", "")
|
||||
status = format_status(detail.get("status", ""), detail.get("processInstanceResult", ""))
|
||||
|
||||
# 用户基础信息
|
||||
info = (user_info_map or {}).get(user_id)
|
||||
user_name = info.name if info else name_map.get(user_id, user_id)
|
||||
dept = info.dept_name if info and info.dept_name else dept_name
|
||||
job_number = info.job_number if info else ""
|
||||
title = info.title if info else ""
|
||||
group_name = (group_map or {}).get(user_id, "")
|
||||
approve_cell = build_approve_cell(corp_id, instance_id, f"{user_name}提交的外出审批单")
|
||||
|
||||
# 优先尝试 DDHolidayField
|
||||
holiday_field = find_holiday_field(form_values)
|
||||
if holiday_field:
|
||||
value_str = holiday_field.get("value", "")
|
||||
time_range = ""
|
||||
try:
|
||||
value_arr = json.loads(value_str)
|
||||
if isinstance(value_arr, list) and len(value_arr) >= 2:
|
||||
time_range = normalize_am_pm(f"{value_arr[0]} ~ {value_arr[1]}")
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
time_range = normalize_am_pm(value_str)
|
||||
ext = parse_ext_value(holiday_field)
|
||||
duration_day = str(ext.get("durationInDay", ""))
|
||||
duration_hour = str(ext.get("durationInHour", ""))
|
||||
else:
|
||||
# 回退: 从独立字段提取
|
||||
time_range, duration_hour, duration_day = _extract_time_duration_from_fields(form_values)
|
||||
|
||||
return [[user_name, group_name, dept, job_number, title,
|
||||
time_range, duration_hour, duration_day,
|
||||
approve_cell, status]]
|
||||
|
||||
|
||||
def parse_trip_from_approve_record(
|
||||
record: dict,
|
||||
name_map: dict[str, str],
|
||||
*,
|
||||
user_info_map: dict[str, "UserInfo"] | None = None,
|
||||
group_map: dict[str, str] | None = None,
|
||||
corp_id: str = "",
|
||||
) -> list[str]:
|
||||
"""
|
||||
直接从 attendance approve list 的记录中解析出差行。
|
||||
|
||||
不依赖 oa approval detail(该接口对出差单存在 saNode 类型冲突 bug),
|
||||
仅使用 approve list 返回的 beginTime/endTime/duration/durationUnit/originId。
|
||||
"""
|
||||
user_id = record.get("userId", "")
|
||||
info = (user_info_map or {}).get(user_id)
|
||||
user_name = info.name if info else name_map.get(user_id, user_id)
|
||||
dept = info.dept_name if info else ""
|
||||
job_number = info.job_number if info else ""
|
||||
title = info.title if info else ""
|
||||
group_name = (group_map or {}).get(user_id, "")
|
||||
|
||||
begin_ms = record.get("beginTime")
|
||||
end_ms = record.get("endTime")
|
||||
begin_str = ms_to_date_str(begin_ms) if begin_ms else ""
|
||||
end_str = ms_to_date_str(end_ms) if end_ms else ""
|
||||
time_range = f"{begin_str} ~ {end_str}" if begin_str and end_str else begin_str or end_str
|
||||
|
||||
duration = record.get("duration", "")
|
||||
duration_unit = record.get("durationUnit", "DAY")
|
||||
unit_str = "天" if duration_unit == "DAY" else "小时"
|
||||
|
||||
instance_id = record.get("originId", "")
|
||||
effective_corp_id = corp_id or record.get("corpId", "")
|
||||
approve_cell = build_approve_cell(effective_corp_id, instance_id, f"{user_name}提交的出差审批单")
|
||||
|
||||
# approve list 没有审批状态,有 gmtFinished 说明已完结,视为审批通过
|
||||
status = "审批通过" if record.get("gmtFinished") else "审批中"
|
||||
|
||||
return [user_name, group_name, dept, job_number, title,
|
||||
time_range, str(duration), unit_str, approve_cell, status]
|
||||
|
||||
|
||||
def fetch_check_results(user_ids: list[str], start: str, end: str) -> dict[str, list[dict]]:
|
||||
"""
|
||||
批量查询打卡结果,返回 {userId: [records...]} 映射。
|
||||
|
||||
每条 record 含: workDate, timeResult, planCheckTime, userCheckTime 等。
|
||||
"""
|
||||
result_map: dict[str, list[dict]] = {}
|
||||
batch_size = 50
|
||||
for i in range(0, len(user_ids), batch_size):
|
||||
batch = user_ids[i:i + batch_size]
|
||||
try:
|
||||
result = run_dws([
|
||||
"attendance", "check", "result",
|
||||
"--users", ",".join(batch),
|
||||
"--start", start,
|
||||
"--end", end,
|
||||
])
|
||||
records = []
|
||||
if isinstance(result, list):
|
||||
records = result
|
||||
elif isinstance(result, dict):
|
||||
records = result.get("result", result.get("list", []))
|
||||
if not isinstance(records, list):
|
||||
records = []
|
||||
for rec in records:
|
||||
uid = rec.get("userId", "")
|
||||
if uid:
|
||||
result_map.setdefault(uid, []).append(rec)
|
||||
except DwsCallError as e:
|
||||
warn(f"查询打卡结果失败(batch {i // batch_size + 1}): {e}")
|
||||
return result_map
|
||||
|
||||
|
||||
def fetch_user_group_map(user_ids: list[str]) -> dict[str, str]:
|
||||
"""
|
||||
查询考勤组列表并建立 userId → 考勤组名称映射。
|
||||
|
||||
流程:先 group search 拿到所有考勤组 ID+名称,
|
||||
再对有成员的考勤组调用 filtered-get --member 获取成员列表。
|
||||
"""
|
||||
group_map: dict[str, str] = {}
|
||||
user_id_set = set(user_ids)
|
||||
|
||||
try:
|
||||
result = run_dws(["attendance", "group", "search"])
|
||||
items: list[dict] = []
|
||||
if isinstance(result, list):
|
||||
items = result
|
||||
elif isinstance(result, dict):
|
||||
# 适配 {items: [...]} 或 {result: {items: [...]}}
|
||||
inner = result.get("items", result.get("result", result))
|
||||
if isinstance(inner, dict):
|
||||
items = inner.get("items", [])
|
||||
elif isinstance(inner, list):
|
||||
items = inner
|
||||
|
||||
for g in items:
|
||||
group_name = g.get("name", g.get("groupName", ""))
|
||||
group_id = g.get("id", g.get("groupId", ""))
|
||||
member_count = g.get("memberCount", 0)
|
||||
|
||||
if not group_id or not group_name or not member_count:
|
||||
continue
|
||||
|
||||
# 调用 filtered-get 获取成员 userId 列表
|
||||
try:
|
||||
detail = run_dws([
|
||||
"attendance", "group", "filtered-get",
|
||||
"--group-id", str(group_id), "--member",
|
||||
])
|
||||
member_users: list[str] = []
|
||||
if isinstance(detail, dict):
|
||||
member_users = detail.get("memberUsers", [])
|
||||
if not isinstance(member_users, list):
|
||||
member_users = []
|
||||
for uid in member_users:
|
||||
uid_str = str(uid)
|
||||
if uid_str in user_id_set:
|
||||
group_map[uid_str] = group_name
|
||||
except DwsCallError:
|
||||
pass
|
||||
|
||||
except DwsCallError as e:
|
||||
warn(f"查询考勤组失败: {e}")
|
||||
return group_map
|
||||
|
||||
|
||||
CHECK_TIME_RESULT_MAP = {
|
||||
"Normal": "正常",
|
||||
"Late": "迟到",
|
||||
"Early": "早退",
|
||||
"Absenteeism": "旷工",
|
||||
"NotSigned": "未打卡",
|
||||
"SeriousLate": "严重迟到",
|
||||
}
|
||||
|
||||
|
||||
def parse_patch_detail(detail: dict, name_map: dict[str, str], *,
|
||||
user_info_map: dict[str, "UserInfo"] | None = None,
|
||||
group_map: dict[str, str] | None = None,
|
||||
check_result_map: dict[str, list[dict]] | None = None,
|
||||
corp_id: str = "",
|
||||
) -> list[list[str]]:
|
||||
"""解析补卡审批单,输出完整列。"""
|
||||
form_values = detail.get("formValueVOS", [])
|
||||
user_id = detail.get("originatorUserid", "")
|
||||
dept_name = detail.get("originatorDeptName", "")
|
||||
instance_id = detail.get("processInstanceId", "")
|
||||
status = format_status(detail.get("status", ""), detail.get("processInstanceResult", ""))
|
||||
|
||||
# 用户基础信息
|
||||
info = (user_info_map or {}).get(user_id)
|
||||
user_name = info.name if info else name_map.get(user_id, user_id)
|
||||
dept = info.dept_name if info and info.dept_name else dept_name
|
||||
job_number = info.job_number if info else ""
|
||||
title = info.title if info else ""
|
||||
group_name = (group_map or {}).get(user_id, "")
|
||||
approve_cell = build_approve_cell(corp_id, instance_id, f"{user_name}提交的补卡审批单")
|
||||
|
||||
# 从表单解析补卡时间和原因
|
||||
patch_time = ""
|
||||
patch_reason = ""
|
||||
work_date = ""
|
||||
check_time_str = ""
|
||||
ext_data: dict = {}
|
||||
|
||||
for fv in form_values:
|
||||
comp_type = fv.get("componentType", "") or ""
|
||||
biz_alias = (fv.get("bizAlias") or "").lower()
|
||||
name = fv.get("name") or ""
|
||||
value = fv.get("value") or ""
|
||||
|
||||
if comp_type == "DDDateField" or "checktime" in biz_alias or "补卡时间" in name:
|
||||
if value and not patch_time:
|
||||
patch_time = value
|
||||
# 解析 extValue 获取考勤日期等
|
||||
ext = parse_ext_value(fv)
|
||||
if ext and not ext_data:
|
||||
ext_data = ext
|
||||
if "reason" in biz_alias or "原因" in name or "事由" in name or "理由" in name:
|
||||
if value and not patch_reason:
|
||||
patch_reason = value
|
||||
|
||||
# 从 extValue 提取考勤日期、考勤时间、原考勤状态
|
||||
plan_tip = ""
|
||||
plan_text = ""
|
||||
if ext_data:
|
||||
work_date_ms = ext_data.get("workDate")
|
||||
if work_date_ms:
|
||||
work_date = ms_to_date_str(work_date_ms)
|
||||
plan_tip = ext_data.get("planTip", "")
|
||||
plan_text = ext_data.get("planText", "")
|
||||
|
||||
# 从 planText / planTip 提取考勤时间(目标格式:YYYY-MM-DD HH:MM)
|
||||
# planText 格式: "2026-04-25,星期六,195固定班次,上班时间09:00"
|
||||
# planTip 格式: "周六上班(04.25 09:00) 缺卡" / "周一上班(04.27 09:00) 缺卡"
|
||||
import re
|
||||
plan_time_hhmm = ""
|
||||
|
||||
# 优先从 planTip 提取(更可靠,含具体日期和时间)
|
||||
# planTip 格式: "周三下班(03.05 01:00) 缺卡"
|
||||
plan_date_from_tip = "" # MM.DD → 用于跨日场景
|
||||
if plan_tip:
|
||||
# 匹配 "(MM.DD HH:MM)" 格式
|
||||
tip_match = re.search(r"\((\d{2})\.(\d{2})\s+(\d{2}:\d{2})\)", plan_tip)
|
||||
if tip_match:
|
||||
plan_date_from_tip = f"{tip_match.group(1)}-{tip_match.group(2)}" # "03-05"
|
||||
plan_time_hhmm = tip_match.group(3)
|
||||
|
||||
# 回退:从 planText 中提取
|
||||
if not plan_time_hhmm and plan_text:
|
||||
# 匹配 "上班时间HH:MM" 或 "下班时间HH:MM" 或 "时间HH:MM"
|
||||
time_match = re.search(r"时间(\d{2}:\d{2})", plan_text)
|
||||
if time_match:
|
||||
plan_time_hhmm = time_match.group(1)
|
||||
|
||||
# 最后回退:任意 HH:MM 格式
|
||||
if not plan_time_hhmm:
|
||||
for source in (plan_tip, plan_text):
|
||||
if source:
|
||||
fallback_match = re.search(r"(\d{2}:\d{2})", source)
|
||||
if fallback_match:
|
||||
plan_time_hhmm = fallback_match.group(1)
|
||||
break
|
||||
|
||||
# 拼接考勤时间:优先使用 planTip 中解析的完整日期(处理跨日班次)
|
||||
if plan_date_from_tip and plan_time_hhmm and work_date:
|
||||
# 用 work_date 的年份 + planTip 中的 MM-DD + HH:MM
|
||||
year = work_date[:4]
|
||||
check_time_str = f"{year}-{plan_date_from_tip} {plan_time_hhmm}"
|
||||
elif work_date and plan_time_hhmm:
|
||||
check_time_str = f"{work_date} {plan_time_hhmm}"
|
||||
elif plan_time_hhmm:
|
||||
check_time_str = plan_time_hhmm
|
||||
elif plan_text:
|
||||
check_time_str = plan_text
|
||||
elif plan_tip:
|
||||
check_time_str = plan_tip
|
||||
|
||||
# 如果 work_date 为空,从 patch_time 中提取日期
|
||||
if not work_date and patch_time:
|
||||
work_date = patch_time[:10] if len(patch_time) >= 10 else ""
|
||||
|
||||
# 从 planTip / planText 提取原考勤状态
|
||||
# planTip 格式: "周六上班(04.25 09:00) 缺卡" / "Thursday ( 04.23 ) Adjust"
|
||||
# planText 格式: "2026-04-25,星期六,195固定班次,上班时间09:00" 或 "周一上班(04.27 09:00) 缺卡"
|
||||
original_check_time = ""
|
||||
original_status = ""
|
||||
|
||||
tip_status_map = {
|
||||
"缺卡": "缺卡", "未打卡": "未打卡",
|
||||
"迟到": "迟到", "早退": "早退",
|
||||
"旷工": "旷工", "正常": "正常",
|
||||
"NotSigned": "未打卡", "Adjust": "已调整",
|
||||
}
|
||||
# 优先从 planTip 提取,回退到 planText
|
||||
for source in (plan_tip, plan_text):
|
||||
if source:
|
||||
for keyword, label in tip_status_map.items():
|
||||
if keyword in source:
|
||||
original_status = label
|
||||
break
|
||||
if original_status:
|
||||
break
|
||||
|
||||
# 回退: 尝试从 check result 接口获取(如果有数据)
|
||||
if check_result_map and user_id in check_result_map:
|
||||
for rec in check_result_map[user_id]:
|
||||
rec_date = rec.get("workDate", "")
|
||||
if isinstance(rec_date, (int, float)):
|
||||
rec_date = ms_to_date_str(rec_date)
|
||||
if rec_date == work_date:
|
||||
user_check_ms = rec.get("userCheckTime")
|
||||
if user_check_ms:
|
||||
dt = ms_to_datetime(user_check_ms)
|
||||
original_check_time = dt.strftime("%Y-%m-%d %H:%M") if dt else ""
|
||||
time_result = rec.get("timeResult", "")
|
||||
if time_result:
|
||||
original_status = CHECK_TIME_RESULT_MAP.get(time_result, time_result)
|
||||
break
|
||||
|
||||
# 补卡结果:审批通过 → 补卡成功
|
||||
patch_result = ""
|
||||
if status == "审批通过":
|
||||
patch_result = "补卡成功"
|
||||
elif status == "已拒绝":
|
||||
patch_result = "补卡失败"
|
||||
elif status == "审批中":
|
||||
patch_result = "待审批"
|
||||
elif status == "已撤销":
|
||||
patch_result = "已撤销"
|
||||
|
||||
return [[user_name, group_name, dept, job_number, title, work_date, check_time_str,
|
||||
original_check_time, original_status, patch_time, patch_result,
|
||||
approve_cell, status]]
|
||||
|
||||
|
||||
PARSERS = {
|
||||
"leave": parse_leave_detail,
|
||||
"out": parse_out_detail,
|
||||
"patch": parse_patch_detail,
|
||||
}
|
||||
|
||||
# 需要额外用户信息(考勤组/工号/职位)的类型
|
||||
_TYPES_NEED_USER_INFO = {"leave", "out", "patch", "trip"}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 主流程
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="考勤记录报表导出(补卡/出差/外出/请假)",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--type", required=True, choices=SUPPORTED_TYPES,
|
||||
help="记录类型: leave(请假)/trip(出差)/out(外出)/patch(补卡)")
|
||||
parser.add_argument("--users", required=True,
|
||||
help="用户 ID 列表,逗号分隔(由 Agent 从人员获取阶段提供)")
|
||||
parser.add_argument("--start", required=True,
|
||||
help="开始日期 YYYY-MM-DD")
|
||||
parser.add_argument("--end", required=True,
|
||||
help="结束日期 YYYY-MM-DD")
|
||||
parser.add_argument("--out", default="",
|
||||
help="输出文件路径(不传则自动生成)")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
record_type: str = args.type
|
||||
user_ids = [u.strip() for u in args.users.split(",") if u.strip()]
|
||||
start_date: str = args.start
|
||||
end_date: str = args.end
|
||||
|
||||
if not user_ids:
|
||||
error("--users 不能为空")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
datetime.strptime(start_date, DATE_FMT)
|
||||
datetime.strptime(end_date, DATE_FMT)
|
||||
except ValueError:
|
||||
error("日期格式错误,请使用 YYYY-MM-DD")
|
||||
sys.exit(1)
|
||||
|
||||
sheet_name = SHEET_NAMES[record_type]
|
||||
log(f"开始导出{sheet_name}:{len(user_ids)} 人,{start_date} ~ {end_date}")
|
||||
|
||||
# ── Step 1: 获取审批单列表 ──
|
||||
log("步骤 1/4:查询审批单列表...")
|
||||
approve_records = fetch_approve_list(user_ids, record_type, start_date, end_date)
|
||||
log(f" 获取到 {len(approve_records)} 条审批记录")
|
||||
|
||||
if not approve_records:
|
||||
log("未查询到任何记录")
|
||||
print(f"{sheet_name}:0 条记录,无需生成文件")
|
||||
sys.exit(0)
|
||||
|
||||
# 从 approve list 记录中提取 corpId(用于构建审批单跳转链接)
|
||||
corp_id = ""
|
||||
for r in approve_records:
|
||||
if r.get("corpId"):
|
||||
corp_id = r["corpId"]
|
||||
break
|
||||
|
||||
# ── Step 2: 去重提取 instanceId ──
|
||||
instance_ids = list(dict.fromkeys(
|
||||
r.get("originId", "") for r in approve_records if r.get("originId")
|
||||
))
|
||||
log(f"步骤 2/4:共 {len(instance_ids)} 个审批实例")
|
||||
|
||||
# ── Step 3: 解析用户信息 ──
|
||||
log("步骤 3/4:解析用户信息...")
|
||||
name_map = resolve_user_names(user_ids)
|
||||
|
||||
user_info_map: dict[str, UserInfo] | None = None
|
||||
group_map: dict[str, str] | None = None
|
||||
check_result_map: dict[str, list[dict]] | None = None
|
||||
|
||||
if record_type in _TYPES_NEED_USER_INFO:
|
||||
log(" 获取用户完整信息(工号/职位)...")
|
||||
user_info_map = resolve_user_info(user_ids)
|
||||
log(" 查询考勤组映射...")
|
||||
group_map = fetch_user_group_map(user_ids)
|
||||
|
||||
if record_type == "patch":
|
||||
log(" 查询原打卡结果...")
|
||||
check_result_map = fetch_check_results(user_ids, start_date, end_date)
|
||||
|
||||
all_rows: list[list[str]] = []
|
||||
|
||||
if record_type == "trip":
|
||||
# 出差记录直接从 approve list 数据生成,不调用 oa approval detail
|
||||
# (oa approval detail 对出差单存在 saNode result 字段类型冲突 bug)
|
||||
log("步骤 4/4:从审批列表解析出差记录...")
|
||||
for record in approve_records:
|
||||
row = parse_trip_from_approve_record(
|
||||
record, name_map,
|
||||
user_info_map=user_info_map,
|
||||
group_map=group_map,
|
||||
corp_id=corp_id,
|
||||
)
|
||||
all_rows.append(row)
|
||||
else:
|
||||
log("步骤 4/4:查询审批详情并解析...")
|
||||
for idx, instance_id in enumerate(instance_ids):
|
||||
if (idx + 1) % 10 == 0:
|
||||
log(f" 进度: {idx + 1}/{len(instance_ids)}")
|
||||
|
||||
detail = fetch_detail(instance_id)
|
||||
if not detail:
|
||||
continue
|
||||
|
||||
# 补充新发现的用户
|
||||
originator = detail.get("originatorUserid", "")
|
||||
if originator and originator not in name_map:
|
||||
extra = resolve_user_names([originator])
|
||||
name_map.update(extra)
|
||||
if originator and user_info_map and originator not in user_info_map:
|
||||
extra_info = resolve_user_info([originator])
|
||||
user_info_map.update(extra_info)
|
||||
|
||||
if record_type == "patch":
|
||||
rows = parse_patch_detail(
|
||||
detail, name_map,
|
||||
user_info_map=user_info_map,
|
||||
group_map=group_map,
|
||||
check_result_map=check_result_map,
|
||||
corp_id=corp_id,
|
||||
)
|
||||
elif record_type == "leave":
|
||||
rows = parse_leave_detail(
|
||||
detail, name_map,
|
||||
user_info_map=user_info_map,
|
||||
group_map=group_map,
|
||||
corp_id=corp_id,
|
||||
)
|
||||
elif record_type == "out":
|
||||
rows = parse_out_detail(
|
||||
detail, name_map,
|
||||
user_info_map=user_info_map,
|
||||
group_map=group_map,
|
||||
corp_id=corp_id,
|
||||
)
|
||||
else:
|
||||
rows = PARSERS[record_type](detail, name_map)
|
||||
all_rows.extend(rows)
|
||||
|
||||
log(f" 解析完成,共 {len(all_rows)} 行")
|
||||
|
||||
if not all_rows:
|
||||
log("无有效数据行")
|
||||
print(f"{sheet_name}:解析后 0 行有效数据,无需生成文件")
|
||||
sys.exit(0)
|
||||
|
||||
# ── 写入 Excel ──
|
||||
out_path = args.out or f"attendance_report_record_{record_type}_{start_date}_{end_date}.xlsx"
|
||||
headers = COLUMNS[record_type]
|
||||
title = f"{sheet_name} 统计日期:{start_date} 至 {end_date}"
|
||||
subtitle = f"报表生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
||||
|
||||
# 将 HyperlinkCell 转为纯文本供 write_excel 写入,之后再补超链接
|
||||
plain_rows = []
|
||||
hyperlink_cells: list[tuple[int, int, str]] = [] # (row_offset, col_idx, url)
|
||||
for row_offset, row in enumerate(all_rows):
|
||||
plain_row = []
|
||||
for col_idx, cell in enumerate(row):
|
||||
if isinstance(cell, HyperlinkCell):
|
||||
plain_row.append(cell.label)
|
||||
hyperlink_cells.append((row_offset, col_idx, cell.url))
|
||||
else:
|
||||
plain_row.append(cell)
|
||||
plain_rows.append(plain_row)
|
||||
|
||||
write_excel(
|
||||
out_path,
|
||||
headers,
|
||||
plain_rows,
|
||||
sheet_name=sheet_name,
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
)
|
||||
|
||||
# 补充超链接
|
||||
if hyperlink_cells:
|
||||
from openpyxl import load_workbook
|
||||
from openpyxl.styles import Font
|
||||
|
||||
wb = load_workbook(out_path)
|
||||
ws = wb.active
|
||||
# 计算标题行偏移:title + subtitle + header
|
||||
title_row_count = (1 if title else 0) + (1 if subtitle else 0)
|
||||
first_data_row = title_row_count + 2 # +1 for header, +1 for 1-indexed
|
||||
|
||||
link_font = Font(color="0563C1", underline="single")
|
||||
for row_offset, col_idx, url in hyperlink_cells:
|
||||
cell = ws.cell(row=first_data_row + row_offset, column=col_idx + 1)
|
||||
cell.hyperlink = url
|
||||
cell.font = link_font
|
||||
wb.save(out_path)
|
||||
|
||||
abs_path = os.path.abspath(out_path)
|
||||
log(f"✅ 导出完成: {abs_path}")
|
||||
print(f"{sheet_name}导出完成:{abs_path}({len(all_rows)} 行,{len(instance_ids)} 个审批单)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -30,8 +30,8 @@ metadata:
|
||||
| "今天 / 明天 / 本周日程" | `python scripts/calendar_today_agenda.py [today\|tomorrow\|week]` |
|
||||
| "约会议(含参会人 + 会议室)" | `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起>" --end "<止>" [--users <ids>] [--book-room]` |
|
||||
| "多人共同空闲" | `python scripts/calendar_free_slot_finder.py --users <ids> --date <yyyy-MM-dd>` |
|
||||
| "查闲忙" | `dws calendar event list --start "<ISO>" --end "<ISO>"` |
|
||||
| "加参会人" / "订房" / "取消" | `dws calendar participant add` / `room add` / `event delete` |
|
||||
| "查闲忙" | `dws calendar busy search --users <id> --start "<ISO>" --end "<ISO>"` |
|
||||
| "加参会人" / "订房" / "取消" | `dws calendar attendee add` / `room add` / `event delete` |
|
||||
|
||||
## 执行硬约束
|
||||
|
||||
@@ -39,8 +39,8 @@ metadata:
|
||||
- 用户明确说"帮我订一个空闲会议室"时,`room search` 返回可用会议室后直接选择第一个可预订且不需要自定义审批的 `roomId` 执行 `room add`;不要把选择权抛回用户导致任务停住。
|
||||
- 已有日程订房:`dws calendar room search --start ... --end ... --format json` → `dws calendar room add --event <EVENT_ID> --rooms <ROOM_ID> --format json` → `event get` 或 `room/busy` 验证。
|
||||
- 换会议室:先 `room delete --event <EVENT_ID> --rooms <OLD_ROOM_ID>`,再 `room add --event <EVENT_ID> --rooms <NEW_ROOM_ID>`,最后回查;不要只更新 `--location`。
|
||||
- 参会人变化用 `participant add/delete`,日程描述变化用 `event update --desc`,删除日程用 `event delete --id`。用户当前消息已明确要求删除/取消时可直接执行;否则先确认。
|
||||
- 脚本失败或参数不完整时,立即降级到明确的 `dws calendar event/participant/room` 命令,不要停在"我要查看用法"。
|
||||
- 参会人变化用 `attendee add/delete`,日程描述变化用 `event update --desc`,删除日程用 `event delete --id`。用户当前消息已明确要求删除/取消时可直接执行;否则先确认。
|
||||
- 脚本失败或参数不完整时,立即降级到明确的 `dws calendar event/attendee/room` 命令,不要停在"我要查看用法"。
|
||||
- 所有 dws 命令带 `--format json`;查询时间必须显式 `--start` / `--end`。
|
||||
|
||||
## 跨产品协作
|
||||
|
||||
@@ -52,5 +52,5 @@
|
||||
|
||||
| Recipe | 行动指南(固定路线) |
|
||||
| ------------------ | ------------------- |
|
||||
| schedule-meeting | **见上文「两准则」**、**「搜房失败硬门禁」**。**未给时段且仅说「发起/开个会」**→ 不走本 recipe;当前 CLI 不支持实时视频会议,告知用户请在钉钉客户端操作。**未给时段但有预约意图**("安排""约""定"等词):追问具体开始/结束时间。**已有时段后**,按固定顺序执行:1. `dws calendar event create` 建日程;2. 有参会人则 `dws calendar participant add`;3. 再处理会议室。**无明确会议室范围**:可直接 `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起始>" --end "<结束>" [--users <userIds>] [--book-room] [--dry-run]`。**有明确范围(某楼/层)**:先 `dws calendar room list-groups`,锁定该地点**最相关的承载 group**;若只有一个地点,`--room-group-id` 应只传这个最相关 group,**不要**把同楼内多个楼层 group 打包传入碰运气。只有用户明确给出多个允许地点时,才把这些 `group-id` 一并传给 `python scripts/calendar_schedule_meeting.py ... --book-room --room-group-id "<id1,id2,...>"`。**用户点名具体会议室**:须手工 `dws calendar room search --start "<ISO>" --end "<ISO>" --group-id <GROUP_ID> [--available] --format json`(**无** `--query`),在 JSON 中匹配名称取 **`rooms[].roomId` 唯一真值** → `dws calendar room add --event <eventId> --rooms <roomId>`;**不得**把用户输入的会议室名当 `roomId`。**一旦连续 2 次空结果 / 任意一次 `roomId invalid`**:**必须回读本节并立即收束判断**;若整园/限定范围内搜尽仍无 roomId 或无空房 → **下一条消息必须直接向用户汇报失败结论**;否则只能向用户确认是否放宽范围/改时间。**禁止**假设 roomId、禁止无 ID 调用 `room add`、禁止用日程详情绕路、禁止继续猜测 Mock/测试环境。细则见「会议室搜索早停」。 |
|
||||
| schedule-meeting | **见上文「两准则」**、**「搜房失败硬门禁」**。**未给时段且仅说「发起/开个会」**→ 不走本 recipe;当前 CLI 不支持实时视频会议,告知用户请在钉钉客户端操作。**未给时段但有预约意图**("安排""约""定"等词):追问具体开始/结束时间。**已有时段后**,按固定顺序执行:1. `dws calendar event create` 建日程;2. 有参会人则 `dws calendar attendee add`;3. 再处理会议室。**无明确会议室范围**:可直接 `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起始>" --end "<结束>" [--users <userIds>] [--book-room] [--dry-run]`。**有明确范围(某楼/层)**:先 `dws calendar room list-groups`,锁定该地点**最相关的承载 group**;若只有一个地点,`--room-group-id` 应只传这个最相关 group,**不要**把同楼内多个楼层 group 打包传入碰运气。只有用户明确给出多个允许地点时,才把这些 `group-id` 一并传给 `python scripts/calendar_schedule_meeting.py ... --book-room --room-group-id "<id1,id2,...>"`。**用户点名具体会议室**:须手工 `dws calendar room search --start "<ISO>" --end "<ISO>" --group-id <GROUP_ID> [--available] --format json`(**无** `--query`),在 JSON 中匹配名称取 **`rooms[].roomId` 唯一真值** → `dws calendar room add --event <eventId> --rooms <roomId>`;**不得**把用户输入的会议室名当 `roomId`。**一旦连续 2 次空结果 / 任意一次 `roomId invalid`**:**必须回读本节并立即收束判断**;若整园/限定范围内搜尽仍无 roomId 或无空房 → **下一条消息必须直接向用户汇报失败结论**;否则只能向用户确认是否放宽范围/改时间。**禁止**假设 roomId、禁止无 ID 调用 `room add`、禁止用日程详情绕路、禁止继续猜测 Mock/测试环境。细则见「会议室搜索早停」。 |
|
||||
| reschedule-meeting | 1. `calendar event list --start "<起始ISO>" --end "<结束ISO>"` → 取 `eventId` 2. `calendar event update --id <eventId> --start "<新起始ISO>" --end "<新结束ISO>"` 更新时间 3. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message send --group <openConversationId> --text "<变更通知>"` 通知变更 |
|
||||
|
||||
@@ -61,6 +61,7 @@ dws calendar room add [flags]
|
||||
dws calendar room delete [flags]
|
||||
```
|
||||
> room是会议室,用于线下开会场景。
|
||||
> **组织限制**:部分企业使用自建会议室系统,未接入钉钉会议室能力。此时 `room search` / `room list-groups` 会返回业务错误 `400056`("所选组织不支持预定钉钉会议室")。这是组织级配置限制,不是命令用法问题;应直接告知用户该组织需在钉钉客户端手动预订会议室,不要重试或换参。
|
||||
|
||||
### busy 相关三级子命令
|
||||
```
|
||||
|
||||
@@ -31,12 +31,14 @@ metadata:
|
||||
| "发到XX群" | `dws chat search --query "<群名>"` → `dws chat message send --group <openConversationId> --title "<标题>" --text "<内容>"` |
|
||||
| "建群" / "拉人进群" | `dws chat group create` / `dws chat group members add` |
|
||||
| "改群名" / "踢人" | `dws chat group rename` / `dws chat group members remove --yes`(踢人不可逆,确认目标后加 --yes;踢群主会被 CLI 拦截,需先 `transfer-owner`)|
|
||||
| "@我消息" / "查群聊记录" | `dws chat message list` |
|
||||
| "@我消息" | `dws chat message list-mentions` |
|
||||
| "查群聊记录" | `dws chat message list` |
|
||||
| "用机器人发消息" | `dws chat message send-by-bot --robot-code <code> --group <id> --title "<标题>" --text "<内容>"` |
|
||||
| "Webhook 推一条" | `dws chat message send-by-webhook --token <token> --title "<标题>" --text "<内容>"` |
|
||||
| "撤回机器人消息" | `dws chat message recall-by-bot --robot-code <code> --group <openConversationId> --keys <processQueryKey>`(只能撤回机器人发的;撤回普通用户消息开源 dws v1.0.30 暂不支持)|
|
||||
| "撤回我发的消息" | `dws chat message recall`(撤回当前用户发送的消息)|
|
||||
| "撤回机器人消息" | `dws chat message recall-by-bot --robot-code <code> --group <openConversationId> --keys <processQueryKey>`(撤回机器人发的)|
|
||||
|
||||
> **注**:v1.0.30 起 `chat message send / send-by-bot / send-by-webhook` 全部强制 `--title` 必填(单聊群聊都要)。
|
||||
> **注**:`chat message send` 的 `--title` 可选(不传时用正文首行作标题);`send-by-bot` / `send-by-webhook` 的 `--title` 必填。
|
||||
|
||||
## 跨产品协作
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
| Recipe | 行动指南(固定路线) |
|
||||
|--------|-------------------|
|
||||
| query-group-chat | **优先**:`python scripts/chat_export_messages.py --query "<群名>" --time "<yyyy-MM-dd HH:mm:ss>" [--no-forward] [--limit N] [--output messages.json]`(自动搜群+翻页+导出)<br>备选:1. `chat search --query "<群名>"` → 取 `openConversationId`<br>2. `chat message list --group <openConversationId> --time "<yyyy-MM-dd HH:mm:ss>"` → 取消息列表<br>3. **翻页**:`hasMore=true` 时取本页最后 `createTime` 作为下次 `--time`,重复至 `hasMore=false`<br>4. `--forward=false` 拉给定时间**之前**的消息<br>5. 合并全部消息后总结 |
|
||||
| query-group-chat | **优先**:`python scripts/chat_export_messages.py --query "<群名>" --time "<yyyy-MM-dd HH:mm:ss>" [--no-forward] [--limit N] [--output messages.json]`(自动搜群+翻页+导出)<br>备选:1. `chat search --query "<群名>"` → 取 `openConversationId`<br>2. `chat message list --group <openConversationId> --time "<yyyy-MM-dd HH:mm:ss>"` → 取消息列表<br>3. **翻页**:`hasMore=true` 时取本页最后 `createTime` 作为下次 `--time`,重复至 `hasMore=false`<br>4. `--direction older` 拉给定时间**之前**的消息(`newer` 拉之后)<br>5. 合并全部消息后总结 |
|
||||
| query-private-chat | **优先**:`python scripts/chat_history_with_user.py --name "<姓名>" --time "<yyyy-MM-dd HH:mm:ss>" [--no-forward] [--limit N] [--output messages.json]`(自动搜人+翻页+导出)<br>备选:1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`<br>2. `chat message list-direct --user <userId> --time "<yyyy-MM-dd HH:mm:ss>"` → 取消息列表<br>3. **翻页**:同 query-group-chat<br>4. 合并全部消息后总结 |
|
||||
| escalate-ding | 三级升级:<br>1. `ding message send --robot-code <robotCode> --type app --users <userId> --content "<内容>"`(必填项见 `dingtalk-ding/references/ding.md`)<br>2. `chat message send --group <openConversationId> --text "<内容>"` 群里提醒(可选 `--title` / `@` 见 [chat.md](./chat.md))<br>3. `todo task create --title "<标题>" --executors <userId> --priority 40` 建紧急待办<br>前置:`aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`;`chat search --query "<群名>"` → 取 `openConversationId` |
|
||||
| send-by-bot | **多群批量优先**:`python scripts/bot_broadcast.py --robot-code <robotCode> --chats <id1>,<id2> --title "<标题>" --text "<内容>"`<br>单群:1. `chat bot search` → 取 `robotCode`<br>2. `chat search --query "<群名>"` → 取 `openConversationId`<br>3. `chat message send-by-bot --robot-code <robotCode> --group <openConversationId> --title "<标题>" --text "<内容>"` |
|
||||
|
||||
@@ -74,6 +74,19 @@ Flags:
|
||||
--bot-id string 机器人 openBotId (必填)
|
||||
```
|
||||
|
||||
#### 根据成员 ID 批量查询群成员详情 — 传入成员 openDingTalkId 列表批量查询
|
||||
```
|
||||
Usage:
|
||||
dws chat group members list-by-ids [flags]
|
||||
Example:
|
||||
dws chat group members list-by-ids --id <openConversationId> --users openDingTalkId1,openDingTalkId2
|
||||
# 查询群 ID: dws chat search --query "群名"
|
||||
# 查询 openDingTalkId: dws contact user search --query "姓名"
|
||||
Flags:
|
||||
--id string 群 ID / openConversationId (必填)
|
||||
--users string 成员 openDingTalkId 列表,逗号分隔 (必填)
|
||||
```
|
||||
|
||||
#### 更新群名称
|
||||
```
|
||||
Usage:
|
||||
@@ -137,6 +150,7 @@ Example:
|
||||
Flags:
|
||||
--group string 群聊 openConversationId (必填)
|
||||
```
|
||||
> ⚠️ 唯一群主 quit 会直接成功,产生无主群,之后对该群做 dismiss / members 等管理操作会报 listRoles null(11056) 无法管理。与 `chat group members remove` 移除群主有本地拦截不同,quit 无此防护。作为唯一群主想退群,应先 `chat group transfer-owner` 转让群主,或 `chat group dismiss` 解散群。
|
||||
|
||||
#### 更新群头像 — 更新指定群聊的群头像
|
||||
```
|
||||
@@ -151,6 +165,7 @@ Flags:
|
||||
```
|
||||
|
||||
> `--icon-media-id` 必须是 `@` 开头的媒体 ID(如 dt_media_upload 返回值),非法格式会本地报错。
|
||||
> ⚠️ 本地格式校验只查前缀。格式合法但不真实存在的 mediaId 服务端仍会静默返回成功,头像并不会真正更新。务必用 `dt_media_upload` / `chat media upload` 上传真实图片拿到的 mediaId。
|
||||
|
||||
#### 更新群设置 — 更新指定群聊的设置项
|
||||
|
||||
@@ -175,6 +190,30 @@ Flags:
|
||||
--status int 设置值: 0=关闭, 1=开启 (必填)
|
||||
```
|
||||
|
||||
#### 设置群备注 — 给群设置只有自己可见的备注标题
|
||||
```
|
||||
Usage:
|
||||
dws chat group update-alias [flags]
|
||||
Example:
|
||||
dws chat group update-alias --group <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:
|
||||
@@ -296,6 +335,8 @@ Flags:
|
||||
```
|
||||
|
||||
#### 移除用户的指定群身份 — 从用户身上移除指定的群身份(不影响其他群身份)
|
||||
|
||||
> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。需要清除某人身份时,用 `chat group-role set-user --role-ids ""`(传空 role-ids 覆盖为无身份)兜底;但 set-user 会清掉该用户的**全部**身份,无法只移除其中一个。
|
||||
```
|
||||
Usage:
|
||||
dws chat group-role remove-user [flags]
|
||||
@@ -338,6 +379,7 @@ Flags:
|
||||
--query string 搜索关键词 (必填)
|
||||
--limit int 每页返回数量(默认 20)
|
||||
--cursor string 分页游标(默认 "0",翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已设置免打扰的群聊(默认 false)
|
||||
```
|
||||
|
||||
### data-auth (数据授权)
|
||||
@@ -373,7 +415,7 @@ Flags:
|
||||
|
||||
#### 拉取会话消息内容 — 拉取指定群聊或单聊的会话消息内容
|
||||
|
||||
--group 指定群聊,--user 指定单聊用户(通过 userId),--open-dingtalk-id 指定单聊用户(通过 openDingTalkId),三者互斥。默认拉取给定时间之后的消息,--forward=false 拉之前的。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。
|
||||
--group 指定群聊,--user 指定单聊用户(通过 userId),--open-dingtalk-id 指定单聊用户(通过 openDingTalkId),三者互斥。用 --direction 控制时间方向:newer=从给定时间往现在拉,older=从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。
|
||||
```
|
||||
Usage:
|
||||
dws chat message list [flags]
|
||||
@@ -381,9 +423,9 @@ Example:
|
||||
dws chat message list --group <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 (必填)
|
||||
@@ -427,7 +469,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>
|
||||
@@ -455,13 +497,14 @@ Flags:
|
||||
--file-path string 文件路径(msgType=file 时必填)
|
||||
--file-size int64 文件大小,单位字节(msgType=file 时必填)
|
||||
--uuid string 幂等 UUID,相同 uuid 在 24h 内不会重复发送(可选)
|
||||
--ai-tag 消息是否带 AI 发送角标(可选,默认 true)
|
||||
|
||||
注意:
|
||||
- --text 和位置参数二选一,--text 优先
|
||||
- --group、--user、--open-dingtalk-id 三者互斥,只需指定其一:群聊用 --group,单聊用 --user 或 --open-dingtalk-id
|
||||
- 纯文本/Markdown 单聊发送时 `--user` 和 `--open-dingtalk-id` 都可用;传 `--user` 时直接走 userId 发送能力
|
||||
- --group 的别名: --id, --chat, --conversation-id (均可替代 --group)
|
||||
- --at-all 和 --at-open-dingtalk-ids 仅在 --group 群聊时生效,单聊时无效;当设置--at-all时,消息内容中一定要包含对应的占位符@all;当设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2>
|
||||
- --at-all 和 --at-open-dingtalk-ids 仅在 --group 群聊时生效,单聊时无效;当设置--at-all时,消息内容中一定要包含对应的占位符<@all>;当设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2>
|
||||
- **换行符**:消息内容按 Markdown 渲染,换行有两层要求,缺一不可:
|
||||
1. 必须使用**真实换行符**(Unicode `U+000A`),而非字面量字符串 `\n`(反斜杠 + 字母 n)。程序或大模型构造参数时,须确保已正确反转义;否则全部内容会渲染在同一行
|
||||
2. Markdown 规范下**单个换行不产生换行效果**。需要换行时请使用:段落分隔(连续两个真实换行符 `\n\n`)、行尾两个空格 + 真实换行符(硬换行 `<br>`),或直接写 HTML 的 `<br>` 标签
|
||||
@@ -492,6 +535,7 @@ Flags:
|
||||
注意:
|
||||
- openTaskId 由 `dws chat message send` 发送消息成功后返回
|
||||
- 用于确认消息是否已成功发送或获取发送失败的原因
|
||||
- 返回结果中含发送成功消息的 openMessageId,可用于后续 recall(撤回)、read-status(查已读)等命令
|
||||
```
|
||||
|
||||
#### 撤回消息 — 撤回当前用户自己发出的消息
|
||||
@@ -610,7 +654,7 @@ Flags:
|
||||
--topic-id string 话题 ID,由 dws chat message list 返回 (必填)
|
||||
--time string 开始时间,格式: yyyy-MM-dd HH:mm:ss(可选)
|
||||
--limit int 返回数量(默认 50)
|
||||
--forward true=从老往新,false=从新往老(默认 false)
|
||||
--direction string 时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(推荐,默认 older)
|
||||
```
|
||||
|
||||
#### 拉取指定时间范围内当前用户的所有会话消息 — 分页拉取当前登录用户在指定时间范围内的所有会话消息
|
||||
@@ -719,8 +763,10 @@ Usage:
|
||||
Example:
|
||||
dws chat message list-unread-conversations
|
||||
dws chat message list-unread-conversations --count 20
|
||||
dws chat message list-unread-conversations --exclude-muted
|
||||
Flags:
|
||||
--count int 返回未读会话条数(可选)
|
||||
--count int 返回未读会话条数(可选)
|
||||
--exclude-muted 是否排除已设置免打扰的会话(默认 false)
|
||||
```
|
||||
|
||||
#### 查询消息的已读/未读状态
|
||||
@@ -927,6 +973,7 @@ Example:
|
||||
Flags:
|
||||
--limit int 每页返回数量(默认 1000)
|
||||
--cursor int64 分页游标(首次不传或传 0,翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已设置免打扰的会话(默认 false)
|
||||
|
||||
注意:
|
||||
- 用户询问"置顶会话"时,直接调用此命令返回置顶会话列表即可
|
||||
@@ -976,6 +1023,7 @@ Flags:
|
||||
--match-mode string 匹配模式:AND=所有人都在群里,OR=任一人在群里(默认 AND)
|
||||
--limit int 每页返回数量(默认 20)
|
||||
--cursor string 分页游标(默认 "0",翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已设置免打扰的群聊(默认 false)
|
||||
|
||||
注意:
|
||||
- --nicks 传人员昵称(花名),逗号分隔,如 "风雷,山乔"
|
||||
@@ -1007,6 +1055,76 @@ Flags:
|
||||
- 上传到共享空间的文件对方才能打开,上传到个人空间的文件对方无法访问
|
||||
```
|
||||
|
||||
#### 引用回复消息 — 引用某条消息并回复文字(单聊/群聊均可)
|
||||
```
|
||||
Usage:
|
||||
dws chat message reply [flags]
|
||||
Example:
|
||||
dws chat message reply --conversation-id <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:
|
||||
@@ -1091,10 +1209,10 @@ Flags:
|
||||
--size int 每页条数 (默认 50),别名: --limit
|
||||
```
|
||||
|
||||
#### 搜索【全部可用】机器人 — 含他人创建/官方机器人,额外返回 openDingTalkId
|
||||
#### 搜索【全部可用】机器人 — 含他人创建/官方机器人,额外返回机器人 openDingTalkId
|
||||
|
||||
范围: 当前用户可用的全部机器人(含他人创建、官方机器人)。
|
||||
返回字段: 额外返回 openDingTalkId(可用于给机器人发单聊消息),search 没有此字段。
|
||||
返回字段: 结果在 `result.bots[]` 中,每项含 `botOpenDingTalkId`(机器人的 openDingTalkId,用于给机器人发单聊消息)和 `name`。注意字段名是 `botOpenDingTalkId`,不是 `openDingTalkId`;search 没有此字段。
|
||||
典型触发词: "搜索机器人""找一个机器人""帮我找 XXX 机器人""所有可用机器人""查机器人"。
|
||||
|
||||
```
|
||||
@@ -1119,7 +1237,7 @@ search 与 find 选择指南:
|
||||
| 维度 | `chat bot search` | `chat bot find` |
|
||||
|------|-------------------|-----------------|
|
||||
| 范围 | 仅我创建的机器人 | 全部可用机器人(含他人/官方) |
|
||||
| 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) |
|
||||
| 额外返回机器人 openDingTalkId | 无 | 有,字段名 `botOpenDingTalkId`(可用于给机器人发单聊消息) |
|
||||
| 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" |
|
||||
|
||||
### category (会话分组管理)
|
||||
@@ -1139,9 +1257,70 @@ Usage:
|
||||
dws chat category list-conversations [flags]
|
||||
Example:
|
||||
dws chat category list-conversations --category-id <分组ID>
|
||||
dws chat category list-conversations --category-id <分组ID> --exclude-muted
|
||||
# 分组ID 可通过 dws chat category list 获取
|
||||
Flags:
|
||||
--category-id int 会话分组 ID (必填)
|
||||
--exclude-muted 是否排除已设置免打扰的会话(默认 false)
|
||||
```
|
||||
|
||||
#### 创建会话分组
|
||||
```
|
||||
Usage:
|
||||
dws chat category create [flags]
|
||||
Example:
|
||||
dws chat category create --title "工作群"
|
||||
Flags:
|
||||
--title string 分组名称 (必填)
|
||||
```
|
||||
|
||||
#### 删除会话分组
|
||||
```
|
||||
Usage:
|
||||
dws chat category delete [flags]
|
||||
Example:
|
||||
dws chat category delete --category-id <分组ID>
|
||||
# 分组ID 可通过 dws chat category list 获取
|
||||
Flags:
|
||||
--category-id int 会话分组 ID (必填)
|
||||
```
|
||||
|
||||
#### 重命名会话分组
|
||||
```
|
||||
Usage:
|
||||
dws chat category rename [flags]
|
||||
Example:
|
||||
dws chat category rename --category-id <分组ID> --title "新名称"
|
||||
# 分组ID 可通过 dws chat category list 获取
|
||||
Flags:
|
||||
--category-id int 会话分组 ID (必填)
|
||||
--title string 新的分组名称 (必填)
|
||||
```
|
||||
|
||||
#### 将会话加入分组
|
||||
```
|
||||
Usage:
|
||||
dws chat category add-conv [flags]
|
||||
Example:
|
||||
dws chat category add-conv --group <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 (会话免打扰)
|
||||
@@ -1166,6 +1345,256 @@ Flags:
|
||||
- 支持单聊和群聊,openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取
|
||||
```
|
||||
|
||||
### media (上传媒体获取 mediaId)
|
||||
|
||||
#### 上传图片/媒体获取 mediaId — 用于 chat message send --msg-type image 等
|
||||
|
||||
⚠️ 前置条件:本命令需要应用凭证。必须已通过 `dws auth login --client-id <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
|
||||
```
|
||||
|
||||
### hide (隐藏会话)
|
||||
|
||||
#### 隐藏会话 — 在会话列表中隐藏指定会话(支持单聊/群聊),收到新消息时会重新出现
|
||||
```
|
||||
Usage:
|
||||
dws chat hide [flags]
|
||||
Example:
|
||||
dws chat hide --conversation-id <openConversationId>
|
||||
dws chat hide --id <openConversationId>
|
||||
Flags:
|
||||
--conversation-id string 会话 openConversationId (必填,支持单聊/群聊)
|
||||
--id string --conversation-id 的别名
|
||||
--chat string --conversation-id 的别名
|
||||
|
||||
注意:
|
||||
- 隐藏后会话不再显示在列表中,收到新消息时会重新出现
|
||||
- openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取
|
||||
```
|
||||
|
||||
### mute-at-all (关闭@所有人通知)
|
||||
|
||||
#### 关闭/开启 @所有人消息提醒 — 关闭或开启会话中 @所有人的消息通知
|
||||
|
||||
> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。
|
||||
```
|
||||
Usage:
|
||||
dws chat mute-at-all [flags]
|
||||
Example:
|
||||
dws chat mute-at-all --conversation-id <openConversationId>
|
||||
dws chat mute-at-all --conversation-id <openConversationId> --off
|
||||
Flags:
|
||||
--conversation-id string 会话 openConversationId (必填,支持单聊/群聊)
|
||||
--id string --conversation-id 的别名
|
||||
--chat string --conversation-id 的别名
|
||||
--off 恢复接收 @所有人通知(不传则关闭通知)
|
||||
```
|
||||
|
||||
### mute-red-envelope (关闭红包通知)
|
||||
|
||||
#### 关闭/开启红包消息提醒 — 关闭或开启会话中的红包消息通知
|
||||
|
||||
> ⚠️ 当前不可用:真机服务端对该命令恒返回 1002(系统繁忙),历史回归至今未修,属服务端问题。命令本身参数合法,但调用不会生效。
|
||||
```
|
||||
Usage:
|
||||
dws chat mute-red-envelope [flags]
|
||||
Example:
|
||||
dws chat mute-red-envelope --conversation-id <openConversationId>
|
||||
dws chat mute-red-envelope --conversation-id <openConversationId> --off
|
||||
Flags:
|
||||
--conversation-id string 会话 openConversationId (必填,支持单聊/群聊)
|
||||
--id string --conversation-id 的别名
|
||||
--chat string --conversation-id 的别名
|
||||
--off 恢复接收红包通知(不传则关闭通知)
|
||||
```
|
||||
|
||||
### mark-unread (标记会话为未读)
|
||||
|
||||
#### 标记会话为未读 — 将指定会话标记为未读状态
|
||||
```
|
||||
Usage:
|
||||
dws chat mark-unread [flags]
|
||||
Example:
|
||||
dws chat mark-unread --conversation-id <openConversationId>
|
||||
dws chat mark-unread --id <openConversationId>
|
||||
Flags:
|
||||
--conversation-id string 会话 openConversationId (必填,支持群聊/单聊)
|
||||
--id string --conversation-id 的别名
|
||||
--chat string --conversation-id 的别名
|
||||
|
||||
注意:
|
||||
- openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取
|
||||
- API 返回成功,但该未读状态在 API 侧观察不到:list-all-conversations 返回的 unreadPoint 不会随之变化,只有钉钉客户端 UI 上能看到未读标记
|
||||
```
|
||||
|
||||
### clear-red-point (清除会话红点)
|
||||
|
||||
#### 清除会话红点 — 清除指定会话的未读红点
|
||||
```
|
||||
Usage:
|
||||
dws chat clear-red-point [flags]
|
||||
Example:
|
||||
dws chat clear-red-point --conversation-id <openConversationId>
|
||||
dws chat clear-red-point --id <openConversationId>
|
||||
Flags:
|
||||
--conversation-id string 会话 openConversationId (必填,支持群聊/单聊)
|
||||
--id string --conversation-id 的别名
|
||||
--chat string --conversation-id 的别名
|
||||
|
||||
注意:
|
||||
- 清除红点后该会话不再显示未读标记
|
||||
```
|
||||
|
||||
### clear-all-red-point (红点清零)
|
||||
|
||||
#### 清除所有会话红点 — 一键全部已读
|
||||
```
|
||||
Usage:
|
||||
dws chat clear-all-red-point
|
||||
Example:
|
||||
dws chat clear-all-red-point
|
||||
|
||||
注意:
|
||||
- 无需任何参数,直接清除当前用户所有会话的未读红点,等效于"全部已读"
|
||||
```
|
||||
|
||||
### list-all-conversations (全部会话列表)
|
||||
|
||||
#### 分页获取全部会话列表 — 获取当前用户的所有会话
|
||||
```
|
||||
Usage:
|
||||
dws chat list-all-conversations [flags]
|
||||
Example:
|
||||
dws chat list-all-conversations
|
||||
dws chat list-all-conversations --limit 50
|
||||
dws chat list-all-conversations --exclude-muted
|
||||
Flags:
|
||||
--limit int 每页数量(1-100,默认 100);传 >100 会被明确拒绝
|
||||
--cursor int 分页游标(首次不传或传 0,翻页传 nextCursor)
|
||||
--exclude-muted 是否排除已免打扰会话(默认 false)
|
||||
|
||||
注意:
|
||||
- 返回结果包含单聊和群聊,不区分会话类型
|
||||
- --limit 范围 1-100,默认 100,上限 100;传入 >100 会报错拒绝,不会静默截断
|
||||
- 分页当前不可用:真机 hasMore 恒为 false、nextCursor 恒为 null,本命令最多返回 100 条会话,无法用 --cursor 继续翻页取更多
|
||||
- 与 list-top-conversations 的区别: 本命令返回全部会话(单聊+群聊),list-top-conversations 仅返回置顶会话
|
||||
```
|
||||
|
||||
### clear-messages (清空会话聊天记录)
|
||||
|
||||
#### 清空会话聊天记录 — 清空当前用户指定会话的消息
|
||||
```
|
||||
Usage:
|
||||
dws chat clear-messages [flags]
|
||||
Example:
|
||||
dws chat clear-messages --conversation-id <openConversationId>
|
||||
dws chat clear-messages --id <openConversationId>
|
||||
Flags:
|
||||
--conversation-id string 会话 openConversationId (必填,支持群聊/单聊)
|
||||
--id string --conversation-id 的别名
|
||||
--chat string --conversation-id 的别名
|
||||
|
||||
注意:
|
||||
- 仅清空当前用户视角的消息,不影响其他成员
|
||||
```
|
||||
|
||||
### mark-read (标记消息已读)
|
||||
|
||||
#### 标记消息已读 — 将指定消息及之前的消息标记为已读
|
||||
```
|
||||
Usage:
|
||||
dws chat mark-read [flags]
|
||||
Example:
|
||||
dws chat mark-read --conversation-id <openConversationId> --message-id <openMessageId>
|
||||
dws chat mark-read --id <openConversationId> --message-id <openMessageId>
|
||||
Flags:
|
||||
--conversation-id string 会话 openConversationId (必填,支持群聊/单聊)
|
||||
--id string --conversation-id 的别名
|
||||
--chat string --conversation-id 的别名
|
||||
--message-id string 消息 openMessageId (必填)
|
||||
|
||||
注意:
|
||||
- 标记该消息及之前的所有消息为已读
|
||||
- openMessageId 可通过 chat message list 获取
|
||||
```
|
||||
|
||||
### group list-all (分页拉取所有群)
|
||||
|
||||
#### 分页拉取我所有群列表 — 获取当前用户加入的所有群聊
|
||||
```
|
||||
Usage:
|
||||
dws chat group list-all [flags]
|
||||
Example:
|
||||
dws chat group list-all
|
||||
dws chat group list-all --limit 50
|
||||
dws chat group list-all --limit 100 --cursor <nextCursor>
|
||||
Flags:
|
||||
--limit int 每页返回数量(默认 100,最大 200)
|
||||
--cursor string 分页游标(首次不传,翻页传返回的 nextCursor)
|
||||
|
||||
注意:
|
||||
- 与 chat group list-my-groups 区别: list-all 返回用户加入的所有群;list-my-groups 仅返回用户作为群主/管理员的群
|
||||
- 分页: hasMore=true 时用返回的 nextCursor 作为下次 --cursor
|
||||
- ⚠️ 存在同步盲区:新建群后较长时间(实测 15 分钟后全量翻页仍查不到)内不会出现在 list-all 里;而 chat group list-my-groups / chat search 能立即查到。要确认刚建的群,用 list-my-groups 或 search,别依赖 list-all
|
||||
```
|
||||
|
||||
### group list-join-validations (分页拉取入群验证记录)
|
||||
|
||||
#### 分页拉取入群验证记录 — 获取当前用户的所有入群验证记录
|
||||
|
||||
包括自己被拒绝的记录以及作为审批者的记录。
|
||||
```
|
||||
Usage:
|
||||
dws chat group list-join-validations [flags]
|
||||
Example:
|
||||
dws chat group list-join-validations
|
||||
dws chat group list-join-validations --limit 30
|
||||
dws chat group list-join-validations --limit 20 --cursor <nextCursor>
|
||||
Flags:
|
||||
--limit int 单页数量(默认 20,最大 50)
|
||||
--cursor string 分页游标(首次不传,翻页传返回的 nextCursor)
|
||||
|
||||
注意:
|
||||
- 分页: hasMore=true 时用返回的 nextCursor 作为下次 --cursor
|
||||
- cursor 首次拉取不传或传 null 时从当前时间开始拉
|
||||
```
|
||||
|
||||
### group audit-join-validation (审批入群验证)
|
||||
|
||||
#### 审批入群验证 — 通过、删除单个审核
|
||||
|
||||
真机当前仅 AuditApprove(通过)和 AuditDelete(删除)两个动作可用。
|
||||
```
|
||||
Usage:
|
||||
dws chat group audit-join-validation [flags]
|
||||
Example:
|
||||
dws chat group audit-join-validation --group <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 AuditDelete
|
||||
# 查询入群验证记录: dws chat group list-join-validations
|
||||
Flags:
|
||||
--group string 群 openConversationId (必填)
|
||||
--record-id string 申请记录 ID (必填)
|
||||
--applicant string 申请人 openDingTalkId (必填)
|
||||
--inviter string 邀请人 openDingTalkId (必填)
|
||||
--status string 审批动作: AuditApprove(通过) / AuditDelete(删除) (必填)
|
||||
--description string 审批说明(可选)
|
||||
|
||||
注意:
|
||||
- status 真机仅支持 AuditApprove(通过) 和 AuditDelete(删除);AuditIgnore(忽略)、AuditRefuse(拒绝)、AuditBlock(拒绝且拉黑) 会被服务端拒绝报 unsupported audit status,属服务端限制
|
||||
- record-id、applicant、inviter 可通过 dws chat group list-join-validations 查询获得
|
||||
```
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户说"我特别关注的人最近发了什么消息/关注的人最近聊了啥/星标联系人最近的动态" → `chat message list-focused`(零参数一行命令)
|
||||
@@ -1188,6 +1617,9 @@ Flags:
|
||||
用户说"踢人/移除群成员" → `chat group members remove`
|
||||
用户说"加机器人到群" → `chat group members add-bot`
|
||||
用户说"改群名" → `chat group rename`
|
||||
用户说"设置群备注/给群加备注" → `chat group update-alias`
|
||||
用户说"改我在群里的昵称/设置群昵称" → `chat group update-nick`
|
||||
用户说"批量查群成员信息/按ID查群成员" → `chat group members list-by-ids`
|
||||
用户说"聊天记录/会话消息/拉取会话" → `chat message list`
|
||||
用户说"某人发给我的消息/指定发送者/某人的消息" → `chat message list-by-sender`(用户未明确说"单聊"时优先使用,跨单聊/群聊)
|
||||
用户说"拉取和某人的单聊记录/单聊消息" → `chat message list --user`(用户明确说"单聊"时使用)
|
||||
@@ -1211,6 +1643,11 @@ Flags:
|
||||
用户说"置顶会话/置顶消息/我的置顶/查看置顶" → `chat list-top-conversations`
|
||||
用户说"查看会话分组/自定义分组" → `chat category list`
|
||||
用户说"某个分组下的会话/分组会话列表" → `chat category list-conversations`
|
||||
用户说"新建会话分组/创建分组" → `chat category create`
|
||||
用户说"删除会话分组" → `chat category delete`
|
||||
用户说"重命名分组/改分组名" → `chat category rename`
|
||||
用户说"把会话加入分组/会话归类到分组" → `chat category add-conv`
|
||||
用户说"把会话移出分组/从分组移除会话" → `chat category remove-conv`
|
||||
用户说"根据群号查群信息/群号查群/群号转openConversationId" → `chat group get-by-group-id`(当用户发消息时只提供了群号,用此工具将群号转为 openConversationId,再调用发消息接口)
|
||||
用户说"查看群身份/群的自定义身份列表" → `chat group-role list`
|
||||
用户说"创建/添加群身份" → `chat group-role add`
|
||||
@@ -1228,12 +1665,30 @@ Flags:
|
||||
用户说"取消文字表情回应/移除文字表情" → `chat message remove-text-emotion`
|
||||
用户说"创建文字表情/新建文字表情" → `chat message create-text-emotion`
|
||||
用户说"免打扰/消息免打扰/静音/开启免打扰/关闭免打扰" → `chat mute`
|
||||
用户说"隐藏会话/隐藏群聊/隐藏对话" → `chat hide`
|
||||
用户说"关闭@所有人通知/屏蔽@所有人/不接收@all" → `chat mute-at-all`(当前服务端 1002 不可用)
|
||||
用户说"开启@所有人通知/恢复@所有人提醒" → `chat mute-at-all --off`(当前服务端 1002 不可用)
|
||||
用户说"关闭红包通知/屏蔽红包/不接收红包提醒" → `chat mute-red-envelope`(当前服务端 1002 不可用)
|
||||
用户说"开启红包通知/恢复红包提醒" → `chat mute-red-envelope --off`(当前服务端 1002 不可用)
|
||||
用户说"标记会话未读/标为未读" → `chat mark-unread`
|
||||
用户说"标记已读/把消息标成已读" → `chat mark-read`
|
||||
用户说"清除红点/去掉某个会话的未读红点" → `chat clear-red-point`
|
||||
用户说"全部已读/一键清除红点/红点清零" → `chat clear-all-red-point`
|
||||
用户说"我的所有会话/全部会话列表" → `chat list-all-conversations`
|
||||
用户说"清空聊天记录/清空会话消息" → `chat clear-messages`
|
||||
用户说"我加入的所有群/我的全部群列表" → `chat group list-all`
|
||||
用户说"入群验证记录/谁申请进群" → `chat group list-join-validations`
|
||||
用户说"审批入群/通过入群申请/删除入群申请" → `chat group audit-join-validation`(仅 AuditApprove/AuditDelete 可用)
|
||||
用户说"引用回复/回复消息/引用消息回复" → `chat message reply`
|
||||
用户说"转发消息/转发一条消息/把消息转发到另一个群" → `chat message forward`
|
||||
用户说"合并转发/批量转发/合并转发多条消息" → `chat message combine-forward`
|
||||
用户说"转发话题/转发话题消息" → `chat message forward-topic`
|
||||
用户说"置顶消息/把消息置顶" → `chat message set-top-msg`
|
||||
用户说"取消置顶消息/撤销消息置顶" → `chat message unset-top-msg`
|
||||
用户说"上传图片拿mediaId/上传媒体" → `chat media upload`
|
||||
用户说"群机器人列表/群里有哪些机器人/查看群机器人" → `chat group bots`
|
||||
用户说"从群里移除机器人/踢出机器人" → `chat group members remove-bot`
|
||||
用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 openDingTalkId 可发单聊)
|
||||
用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 botOpenDingTalkId 可发单聊)
|
||||
用户说"给机器人发单聊/给机器人发消息/跟机器人聊天" → 必须先 `chat bot find`(拿 openDingTalkId)→ 再 `chat message send --open-dingtalk-id`(search 没有 openDingTalkId,无法发单聊)
|
||||
用户说"我创建的机器人/我的机器人/我自己的机器人/查看我的机器人" → `chat bot search`(仅我创建的机器人,无 openDingTalkId)
|
||||
用户说"解散群/解散群聊" → `chat group dismiss`
|
||||
@@ -1474,7 +1929,7 @@ Flags:
|
||||
| `aisearch person` | `userId` | message send 的 --user、send-by-bot 的 --users、send-by-bot 的 --at-user-ids、list-by-sender 的 --sender-user-id |
|
||||
| `aisearch person` → `contact user get` | `openDingTalkId` | message send 的 --at-open-dingtalk-ids、--open-dingtalk-id、send-by-bot 的 --open-dingtalk-ids、send-by-bot 的 --at-open-dingtalk-ids、list-by-sender 的 --sender-open-dingtalk-id、message list 的 --open-dingtalk-id |
|
||||
| `chat bot search` | `robotCode` | send-by-bot / recall-by-bot 的 --robot-code(仅我创建的机器人,无 openDingTalkId) |
|
||||
| `chat bot find` | `openDingTalkId` | 给机器人发单聊消息(全部可用机器人,额外返回 openDingTalkId) |
|
||||
| `chat bot find` | `botOpenDingTalkId` | 给机器人发单聊消息(send --open-dingtalk-id;字段名是 botOpenDingTalkId,非 openDingTalkId) |
|
||||
| `chat message send-by-bot` | `processQueryKey` | recall-by-bot 的 --keys |
|
||||
| `chat message send` | `openTaskId` | query-send-status 的 --open-task-id |
|
||||
| `chat message list` | `openMessageId` | recall 的 --msg-id |
|
||||
@@ -1516,7 +1971,7 @@ Flags:
|
||||
- `chat search-common` 搜索共同群,`--nicks` 传人员昵称(逗号分隔),`--match-mode` AND/OR 控制匹配逻辑,分页用 `--limit`(默认 20)/`--cursor`
|
||||
- `chat list-top-conversations` 拉取置顶会话列表,分页用 `--limit`(默认 1000)/`--cursor`;用户询问"置顶会话"或"置顶消息"时均路由到此命令
|
||||
- `--user` 和 `--open-dingtalk-id` 本质上都是发起单聊操作,只是用户标识格式不同:userId 为企业内部应用常用标识,openDingTalkId 为三方应用或跨组织场景下的用户标识,服务端对两种 ID 的解析逻辑不同
|
||||
- `--time` 格式: `yyyy-MM-dd HH:mm:ss`,为拉取消息的起始时间点;`--forward` 控制方向(默认 true,拉给定时间之后的消息),`--limit` 控制数量
|
||||
- `--time` 格式: `yyyy-MM-dd HH:mm:ss`,为拉取消息的起始时间点;`--direction` 控制方向(newer=从给定时间往现在拉,older=从给定时间往以前拉),`--limit` 控制数量
|
||||
- `chat search` 挂在 `chat` 下(非 `chat group` 下),路径为 `dws chat search`
|
||||
- `send-by-bot` 群聊传 `--group`,单聊传 `--users` 或 `--open-dingtalk-ids`,与 `--group` 互斥且必选其一;群聊时可选 `--at-user-ids` @指定成员(传 userId 列表)或 `--at-open-dingtalk-ids` @指定成员(传 openDingtalkId 列表),content 中需包含对应 @标识;`--at-all` @所有人;群聊场景如果返回"机器人不存在"错误,需先通过 `chat group members add-bot --group <openConversationId> --robot-code <robot-code>` 将机器人邀请进群后再发送
|
||||
- `recall-by-bot` 群聊传 `--group` + `--keys`,单聊仅传 `--keys`(不传 `--group` 即为单聊撤回)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user