Compare commits

..
Author SHA1 Message Date
chichuan d3f62193e7 Merge pull request #859 from DingTalk-Real-AI/codex/changelog-v1.0.56
docs: seal v1.0.56 changelog
2026-08-04 10:05:35 +08:00
chichuan f26df04679 docs: seal v1.0.56 changelog 2026-08-04 10:00:32 +08:00
github-actions[bot] d02b03436d chore: update beta formula for v1.0.56-beta.4 [skip ci] 2026-08-04 01:55:53 +00:00
chichuan bc7b96ba5f Merge pull request #858 from DingTalk-Real-AI/codex/changelog-v1.0.56-beta.4
docs: seal v1.0.56-beta.4 changelog
2026-08-04 09:46:33 +08:00
chichuan 9539c887f6 docs: seal v1.0.56-beta.4 changelog 2026-08-04 09:35:44 +08:00
github-actions[bot] 6607f44724 Merge pull request #852 from AlwaysLee/feat/center-protocol-transfer
feat: multipart download engine with checkpoint resume and credential refresh
2026-08-04 09:29:17 +08:00
半圭 837a96fe3d fix: show friendly message on Ctrl+C instead of internal error JSON
When user interrupts multipart download with Ctrl+C, display a helpful
message indicating checkpoint is saved and download can be resumed,
instead of returning an internal error with context.Canceled.
2026-08-04 08:58:17 +08:00
半圭 fdcd44f9e3 fix: handle Ctrl+C (SIGINT) gracefully during drive download
- Replace context.Background() with cmd.Context() in download and
  download-version commands so SIGINT propagates to download goroutines
- Enables graceful interruption of multipart downloads via Ctrl+C
2026-08-03 23:21:59 +08:00
半圭 34c0c86a59 feat: center protocol upload/download refactoring with multipart download
- Add multipart download engine (drive_transfer.go) with Range probe,
  resume support, and credential auto-refresh on 401/403
- Add --part-size, --parallel, --no-resume flags to drive download and
  download-version commands
- Replace httpGetFile with driveTransferDownload for chunked parallel
  downloads in download and download-version commands
- Replace uploadToDrive credential parsing with driveUploadPut
  (transparent header pass-through, retry on 401/403)
- Add typed httpStatusError for non-2xx HTTP responses in doc.go
- Add comprehensive unit tests (32 cases) for drive_transfer
- Update drive reference documentation with multipart download behavior
- Add E2E test for multipart download (auto-test/, gitignored)

CR: 28984991
2026-08-03 23:21:59 +08:00
github-actions[bot] b2cbca2762 chore: update beta formula for v1.0.56-beta.3 [skip ci] 2026-08-03 12:55:19 +00:00
chichuan 9ce95db08e Merge pull request #855 from DingTalk-Real-AI/codex/changelog-v1.0.56-beta.3
docs: seal v1.0.56-beta.3 changelog
2026-08-03 20:39:44 +08:00
chichuan e99c20a0a1 docs: seal v1.0.56-beta.3 changelog 2026-08-03 20:34:17 +08:00
github-actions[bot] 96bfae079a Merge pull request #846 from wxianfeng/fix/event-unix-socket-tmpdir
fix(event): use secure Unix bus runtime directory
2026-08-03 16:04:41 +08:00
wxianfeng bb18cdba3b Merge upstream/main into fix/event-unix-socket-tmpdir 2026-08-03 15:45:38 +08:00
wxianfeng 015a1f85ca fix(event): satisfy platform coverage gate 2026-08-03 15:42:17 +08:00
github-actions[bot] 8854e0d1d4 Merge pull request #851 from abucraft/codex/aitable-workflow-docs
feat: add aitable workflow edit example command
2026-08-03 07:01:27 +00:00
镜玄 22862508b8 feat: add aitable workflow edit example command 2026-08-03 14:47:59 +08:00
wxianfeng 5459bcc524 fix(event): secure Unix bus runtime directory 2026-08-03 11:40:01 +08:00
wxianfeng 029c665029 fix(event): place Unix bus sockets in temp dir 2026-07-31 18:01:34 +08:00
155 changed files with 11284 additions and 35638 deletions
+43
View File
@@ -6,6 +6,49 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
## [Unreleased]
## [1.0.56] - 2026-08-04
This stable release promotes the fully delivered `v1.0.56-beta.4` baseline.
It includes PR #852's resilient multipart Drive download implementation,
together with the v1.0.56 beta-line command, Schema, Skill, and runtime
improvements already validated through the prerelease channel.
### Added
- **Resilient multipart Drive downloads** (#852) — `drive download` and
`drive download-version` support parallel chunk transfer, Range probing,
fingerprint-validated checkpoint resume, automatic 401/403 credential
refresh, and graceful interruption with checkpoint preservation.
## [1.0.56-beta.4] - 2026-08-04
This beta adds PR #852 on top of v1.0.56-beta.3. It makes Drive downloads
resilient for large files through parallel transfer, validated resumable
checkpoints, and automatic credential refresh.
### Added
- **Multipart Drive downloads** (#852) — adds `--part-size`, `--parallel`, and
`--no-resume` to `drive download` and `drive download-version`. Files above
the part-size threshold use a Range probe and parallel chunks, resume from a
fingerprint-validated checkpoint, refresh credentials on 401/403, and keep
the checkpoint when Ctrl+C interrupts a transfer.
## [1.0.56-beta.3] - 2026-08-03
This beta adds PRs #846 and #851 on top of v1.0.56-beta.2. It adds a
service-provided Aitable workflow-editing reference command and makes local
event-bus IPC reliable on shared filesystems by placing Unix sockets in a
validated private runtime directory.
### Added
- **Aitable workflow editing reference** (#851) — adds `dws aitable workflow edit-example`, a parameter-free read command that returns the service-provided workflow editing documentation and `workflow-dsl/v1` examples through `aitable/edit_workflow_example`.
### Fixed
- **Event bus sockets on shared filesystems** (#846) — Unix event buses now place their local IPC socket in a private per-user runtime directory (`XDG_RUNTIME_DIR` when available, otherwise a `0700` per-UID directory under the system temporary directory) while retaining locks, metadata, logs, and subscription state in the configured Workdir. Listener and dial paths validate directory ownership and permissions before use. This prevents `dws event consume` from failing with `bind: errno 524` when `~/.dws` is hosted on NFS, CSI, FUSE, or another filesystem that does not support Unix Domain Sockets without exposing the socket directly in a shared `/tmp` root. When `XDG_RUNTIME_DIR` is unavailable, the per-UID directory name is deterministic: ownership validation prevents endpoint hijacking, but another local user can pre-create the directory to deny service; multi-user deployments should provide a private `XDG_RUNTIME_DIR`.
## [1.0.56-beta.2] - 2026-07-30
This beta adds PRs #831 and #835 on top of v1.0.56-beta.1. It separates
+11 -11
View File
@@ -1,33 +1,33 @@
class DingtalkWorkspaceCliBeta < Formula
desc "Automate DingTalk workspace tasks from the terminal (beta channel)"
homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
version "1.0.56-beta.2"
version "1.0.56-beta.4"
license "Apache-2.0"
keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-darwin-arm64.tar.gz"
sha256 "19b52b5427dbf24acfeb1da16a93e6edfd0443389a778abbbfd381ff8f656139"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-darwin-arm64.tar.gz"
sha256 "f1f9b6394137edbd0b08d632aab34e92a0f3f81d80107a47de1bec9b384f0515"
else
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-darwin-amd64.tar.gz"
sha256 "621da52d04f391234d160a0522d70c1fb2e36da59102ef201a9a3a11b706b3e8"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-darwin-amd64.tar.gz"
sha256 "cd3c64d20723c420e2490405d0bf8eecfd7e2b8fc352f63f23de5847a1d38f55"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-linux-arm64.tar.gz"
sha256 "4ba956463f4b583c1727f58026a6bc1fb23d27537083e3bafd541a05ab15b48b"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-linux-arm64.tar.gz"
sha256 "910918d88074534e680a2e320d3cb364ad092e96b9c422f9e75d11c9c0815dd8"
else
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-linux-amd64.tar.gz"
sha256 "a8156ec5b89355faf8c08a65d9c088f89a7b411a418fb4b488f3d472efc79670"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-linux-amd64.tar.gz"
sha256 "172fe0d84443be953d0c6f2c2433540e4b972fbe7776cff1417ec9c73723552b"
end
end
resource "skills" do
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-skills.zip"
sha256 "92c71fdeade88b3b76a00cb74ebd3223cc4110c7ee151d36d782537613129967"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-skills.zip"
sha256 "a3457befe858cbf3fe85848428b630bfd3a5f626256ed6b49415267948915152"
end
def install
-226
View File
@@ -1,226 +0,0 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
stderrors "errors"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
)
func TestChatGroupCreateMembersGuidanceUsesGenericExample(t *testing.T) {
root := NewRootCommand()
cmd := mustFindCommand(t, root, "chat", "group", "create")
err := enrichChatWorkbookError(cmd, stderrors.New("unknown flag: --members"))
var typed *apperrors.Error
if !stderrors.As(err, &typed) {
t.Fatalf("error = %T, want *errors.Error", err)
}
joined := strings.Join(typed.Examples, "\n")
for _, unwanted := range []string{"V2评审小组", "489149", "550582"} {
if strings.Contains(joined, unwanted) {
t.Fatalf("example is fitted to evaluation data %q: %s", unwanted, joined)
}
}
for _, want := range []string{"<群名称>", "<userId1>,<userId2>"} {
if !strings.Contains(joined, want) {
t.Fatalf("generic example missing %q: %s", want, joined)
}
}
}
func TestChatHintPathsAreReportedAsUnknownSubcommandsBeforeFlagParsing(t *testing.T) {
for _, tc := range []struct {
name string
args []string
message string
flag string
}{
{name: "group search", args: []string{"chat", "group", "search", "--query", "1", "--format", "json"}, message: "chat group 下不存在 search 子命令", flag: "--query"},
{name: "send", args: []string{"chat", "send", "--group", "cid", "--text", "hi", "--format", "json"}, message: "chat 下不存在 send 子命令", flag: "--group"},
{name: "history", args: []string{"chat", "history", "--group", "cid", "--time", "2026-08-03 10:00:00", "--format", "json"}, message: "chat 下不存在 history 子命令", flag: "--group"},
} {
t.Run(tc.name, func(t *testing.T) {
err := validateChatWorkbookRawArgs(tc.args)
var typed *apperrors.Error
if !stderrors.As(err, &typed) {
t.Fatalf("error = %T, want *errors.Error", err)
}
if typed.Message != tc.message || typed.Reason != "unknown_subcommand" {
t.Fatalf("guidance = %#v", typed)
}
if strings.Contains(typed.Message, tc.flag) || strings.Contains(typed.Reason, "unknown flag") {
t.Fatalf("subcommand error was misreported as a flag error: %#v", typed)
}
})
}
root := NewRootCommand()
chat := mustFindCommand(t, root, "chat")
for _, child := range chat.Commands() {
if child.Name() == "send" || child.Name() == "history" {
t.Fatalf("chat %s must not be registered as a hint subcommand", child.Name())
}
}
group := mustFindCommand(t, root, "chat", "group")
for _, child := range group.Commands() {
if child.Name() == "search" {
t.Fatal("chat group search must not be registered as a hint subcommand")
}
}
}
func TestValidateChatWorkbookRawArgs(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
args []string
want string
}{
{
name: "members group flag",
args: []string{"chat", "group", "members", "list", "--group", "cid-demo", "--format", "json"},
want: "群成员列表命令路径或群参数不正确",
},
{
name: "rename group flag",
args: []string{"chat", "group", "rename", "--group=cid-demo", "--name", "新群名"},
want: "群重命名命令不支持 --group",
},
{
name: "image local path",
args: []string{"chat", "message", "send", "--group", "cid-demo", "--msg-type", "image", "--file-path", "/tmp/x.png"},
want: "image 消息不能直接使用 --file-path",
},
{
name: "unsupported message type",
args: []string{"chat", "message", "send", "--group", "cid-demo", "--msg-type=sticker"},
want: "不支持指定的 --msg-type:sticker",
},
{
name: "numeric group id required",
args: []string{"chat", "group", "get-by-group-id", "--group-id", "cid-demo"},
want: "--group-id 必须是数字群号",
},
{
name: "file media id conflict",
args: []string{"chat", "message", "send", "--group", "cid-demo", "--msg-type", "file", "--media-id", "media"},
want: "文件消息不能使用 --media-id",
},
{
name: "silent text media conflict",
args: []string{"chat", "message", "send", "--group", "cid-demo", "--media-id", "media", "--text", "file.pdf"},
want: "检测到 --media-id,但没有指定媒体消息类型",
},
{
name: "dismiss numeric group id",
args: []string{"chat", "group", "dismiss", "--group", "12345678"},
want: "解散群命令需要 openConversationId,不是数字群号",
},
} {
t.Run(tc.name, func(t *testing.T) {
err := validateChatWorkbookRawArgs(tc.args)
var typed *apperrors.Error
if !stderrors.As(err, &typed) {
t.Fatalf("error = %T, want *errors.Error", err)
}
if typed.Message != tc.want || len(typed.Actions) == 0 || len(typed.Examples) == 0 {
t.Fatalf("guidance = %#v", typed)
}
})
}
if err := validateChatWorkbookRawArgs([]string{"chat", "group", "rename", "--id", "cid-demo"}); err != nil {
t.Fatalf("canonical rename args rejected: %v", err)
}
}
func TestChatWorkbookHelpGuidanceCoverage(t *testing.T) {
t.Parallel()
for _, path := range []string{
"chat group members",
"chat group members add",
"chat group members remove",
"chat group members add-bot",
"chat group members remove-bot",
"chat group members list-by-ids",
"chat group create",
"chat group rename",
"chat message list",
"chat message search",
"chat message search-advanced",
"chat message list-all",
"chat message list-by-sender",
} {
guide, ok := chatWorkbookHelpGuidance[path]
if !ok || guide.reason == "" || guide.action == "" || guide.example == "" {
t.Fatalf("incomplete help guidance for %q: %#v", path, guide)
}
}
}
func TestRawArgsFlagValue(t *testing.T) {
t.Parallel()
if got := rawArgsFlagValue([]string{"--msg-type", "image"}, "msg-type"); got != "image" {
t.Fatalf("separate value = %q", got)
}
if got := rawArgsFlagValue([]string{"--msg-type=file"}, "msg-type"); got != "file" {
t.Fatalf("equals value = %q", got)
}
if got := rawArgsFlagValue([]string{"--text", "hello"}, "msg-type"); got != "" {
t.Fatalf("missing value = %q", got)
}
}
func TestRawArgsRequestJSON(t *testing.T) {
t.Parallel()
for _, args := range [][]string{
{"chat", "search", "--format", "json"},
{"chat", "search", "--format=json"},
{"chat", "search", "-f", "JSON"},
{"chat", "search", "-f=json"},
} {
if !rawArgsRequestJSON(args) {
t.Fatalf("rawArgsRequestJSON(%v) = false", args)
}
}
}
func TestSuppressJSONDeprecationPreamble(t *testing.T) {
t.Parallel()
root := NewRootCommand()
cmd := mustFindCommand(t, root, "chat", "media", "upload")
if cmd.Deprecated == "" {
t.Fatal("fixture command is not deprecated")
}
suppressJSONDeprecationPreamble(root, []string{"chat", "media", "upload", "--format", "json"})
if cmd.Deprecated != "" {
t.Fatalf("JSON execution kept deprecation preamble: %q", cmd.Deprecated)
}
plainRoot := NewRootCommand()
plain := mustFindCommand(t, plainRoot, "chat", "media", "upload")
suppressJSONDeprecationPreamble(plainRoot, []string{"chat", "media", "upload"})
if plain.Deprecated == "" {
t.Fatal("human execution unexpectedly removed deprecation metadata")
}
}
-483
View File
@@ -24,7 +24,6 @@ import (
"os/signal"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"syscall"
@@ -113,16 +112,6 @@ func Execute() (exitCode int) {
root := rootNewRootCommandWithEngine(ctx, engine)
timing.Record("cmd_init", time.Since(initStart))
if err := validateChatWorkbookRawArgs(os.Args[1:]); err != nil {
if rawArgsRequestJSON(os.Args[1:]) {
_ = apperrors.PrintJSON(os.Stderr, err)
} else {
_ = apperrors.PrintHumanAt(os.Stderr, err, resolveVerbosity(root))
}
return apperrors.ExitCode(err)
}
suppressJSONDeprecationPreamble(root, os.Args[1:])
// Run PreParse handlers on raw argv before Cobra parses flags.
// This corrects model-generated errors like --userId → --user-id
// and --limit100 → --limit 100.
@@ -138,7 +127,6 @@ func Execute() (exitCode int) {
executed = root
}
err = rewordRequiredFlagError(err)
err = enrichChatWorkbookError(executed, err)
if isUnknownCommandError(err) {
executed.SetOut(os.Stderr)
_ = executed.Help()
@@ -153,474 +141,6 @@ func Execute() (exitCode int) {
return 0
}
func suppressJSONDeprecationPreamble(root *cobra.Command, args []string) {
if root == nil || !rawArgsRequestJSON(args) || len(args) < 3 {
return
}
if args[0] != "chat" || args[1] != "media" || args[2] != "upload" {
return
}
if cmd, _, err := root.Find([]string{"chat", "media", "upload"}); err == nil && cmd != nil {
cmd.Deprecated = ""
}
}
func validateChatWorkbookRawArgs(args []string) error {
path := strings.Join(args, " ")
switch {
case len(args) >= 3 && args[0] == "chat" && args[1] == "group" && args[2] == "search":
return apperrors.NewValidation(
"chat group 下不存在 search 子命令",
apperrors.WithReason("unknown_subcommand"),
apperrors.WithActions("群聊搜索使用 dws chat search,而不是 dws chat group search", "移除路径中的 group 后重试"),
apperrors.WithExamples(`dws chat search --query <群名关键词> --format json`),
)
case len(args) >= 2 && args[0] == "chat" && args[1] == "send":
return apperrors.NewValidation(
"chat 下不存在 send 子命令",
apperrors.WithReason("unknown_subcommand"),
apperrors.WithActions("发送消息使用 dws chat message send,而不是 dws chat send", "在路径中补充 message 后重试"),
apperrors.WithExamples(`dws chat message send --group <openConversationId> --text <消息正文> --format json`),
)
case len(args) >= 2 && args[0] == "chat" && args[1] == "history":
return apperrors.NewValidation(
"chat 下不存在 history 子命令",
apperrors.WithReason("unknown_subcommand"),
apperrors.WithActions("查询会话消息使用 dws chat message list,而不是 dws chat history", "改用 message list 并按帮助补充目标和时间参数"),
apperrors.WithExamples(`dws chat message list --group <openConversationId> --time <YYYY-MM-DD HH:mm:ss> --format json`),
)
case strings.HasPrefix(path, "chat message send ") && rawArgsFlagValue(args, "msg-type") == "file" &&
rawArgsContainFlag(args, "media-id"):
return apperrors.NewValidation(
"文件消息不能使用 --media-id",
apperrors.WithReason("PDF、DOCX、XLSX 和本地图片等文件通过 --file-path 上传发送;mediaId 仅用于已有媒体标识的 image 消息"),
apperrors.WithActions("移除 --media-id", "补充 --file-path 并保留 --msg-type file"),
apperrors.WithExamples(`dws chat message send --group <openConversationId> --msg-type file --file-path ./report.pdf --format json`),
)
case strings.HasPrefix(path, "chat message send ") && rawArgsContainFlag(args, "media-id") &&
rawArgsFlagValue(args, "msg-type") == "":
return apperrors.NewValidation(
"检测到 --media-id,但没有指定媒体消息类型",
apperrors.WithReason("未指定 --msg-type 时命令会进入文本分支,可能把文件名当成普通文字发送"),
apperrors.WithActions("已有图片 mediaId 时补充 --msg-type image", "发送 PDF/DOCX/XLSX 时移除 --media-id,改用 --msg-type file --file-path"),
apperrors.WithExamples(`dws chat message send --group <openConversationId> --msg-type image --media-id <mediaId> --format json`, `dws chat message send --group <openConversationId> --msg-type file --file-path ./thesis.pdf --format json`),
)
case strings.HasPrefix(path, "chat message send ") && rawArgsFlagValue(args, "msg-type") == "image" &&
rawArgsContainFlag(args, "file-path") && !rawArgsContainFlag(args, "media-id"):
filePath := rawArgsFlagValue(args, "file-path")
return apperrors.NewValidation(
"image 消息不能直接使用 --file-path",
apperrors.WithReason("msg-type=image 只接受已有 mediaId;本地图片路径不能自动转换为 mediaId"),
apperrors.WithActions("发送本地图片时改用 --msg-type file", "保留原路径并通过 --file-path 发送为文件附件"),
apperrors.WithExamples(fmt.Sprintf(`dws chat message send --group <openConversationId> --msg-type file --file-path %q --format json`, filePath)),
)
case strings.HasPrefix(path, "chat message send ") && rawArgsFlagValue(args, "msg-type") == "image" &&
!rawArgsContainFlag(args, "media-id"):
return apperrors.NewValidation(
"图片消息缺少 --media-id",
apperrors.WithReason("msg-type=image 只接受上游已经获得的有效 mediaId,不能把本地文件名当作 mediaId"),
apperrors.WithActions("已有 mediaId 时补充 --media-id", "发送本地图片时改用 --msg-type file --file-path"),
apperrors.WithExamples(`dws chat message send --group <openConversationId> --msg-type image --media-id <mediaId> --format json`, `dws chat message send --group <openConversationId> --msg-type file --file-path ./image.png --format json`),
)
case strings.HasPrefix(path, "chat message send "):
msgType := rawArgsFlagValue(args, "msg-type")
switch msgType {
case "", "text", "markdown", "image", "file", "audio", "video", "location", "profile":
default:
return apperrors.NewValidation(
"不支持指定的 --msg-type:"+msgType,
apperrors.WithReason("当前命令不支持 sticker/card 等消息类型;文本或 Markdown 消息无需传 --msg-type"),
apperrors.WithActions("文本消息移除 --msg-type 并使用 --text", "媒体消息使用 image、file、audio、video、location 或 profile"),
apperrors.WithExamples(`dws chat message send --group <openConversationId> --text "hi" --format json`),
)
}
case strings.HasPrefix(path, "chat group get-by-group-id "):
value := rawArgsFlagValue(args, "group-id")
if value != "" {
if _, err := strconv.ParseInt(value, 10, 64); err != nil {
return apperrors.NewValidation(
"--group-id 必须是数字群号",
apperrors.WithReason("cid 开头的值是 openConversationId,不是 get-by-group-id 所需的数字群号"),
apperrors.WithActions("如果已有 openConversationId,请改用接受 --group 的群查询命令", "只有拿到数字群号时才调用 get-by-group-id"),
apperrors.WithExamples(`dws chat group get-by-group-id --group-id 12345678 --format json`),
)
}
}
case strings.HasPrefix(path, "chat group dismiss ") && rawArgsContainFlag(args, "group"):
value := rawArgsFlagValue(args, "group")
if _, err := strconv.ParseInt(value, 10, 64); err == nil {
return apperrors.NewValidation(
"解散群命令需要 openConversationId,不是数字群号",
apperrors.WithReason("--group 应传 cid 开头或服务端返回的 openConversationId;数字群号只用于 get-by-group-id"),
apperrors.WithActions("先通过 chat search 获取 openConversationId", "确认目标群及不可逆影响后再执行解散"),
apperrors.WithExamples(`dws chat group dismiss --group <openConversationId> --format json`),
)
}
case strings.HasPrefix(path, "chat group members ") && rawArgsContainFlag(args, "group"):
return apperrors.NewValidation(
"群成员列表命令路径或群参数不正确",
apperrors.WithReason("群成员列表的可执行命令是 chat group members,群 ID 参数名为 --id;不存在 members list --group 这一组合"),
apperrors.WithActions("移除多余的 list 子命令", "将 --group 改为 --id"),
apperrors.WithExamples(`dws chat group members --id <openConversationId> --format json`),
)
case strings.HasPrefix(path, "chat group rename ") && rawArgsContainFlag(args, "group"):
return apperrors.NewValidation(
"群重命名命令不支持 --group",
apperrors.WithReason("chat group rename 使用 --id 接收群 openConversationId,而不是 --group"),
apperrors.WithActions("将 --group 改为 --id", "群 ID 不确定时先用 chat search 查询"),
apperrors.WithExamples(`dws chat group rename --id <openConversationId> --name "新群名" --format json`),
)
}
return nil
}
func rawArgsContainFlag(args []string, name string) bool {
prefix := "--" + name
for _, arg := range args {
if arg == prefix || strings.HasPrefix(arg, prefix+"=") {
return true
}
}
return false
}
func rawArgsFlagValue(args []string, name string) string {
prefix := "--" + name
for i, arg := range args {
if strings.HasPrefix(arg, prefix+"=") {
return strings.TrimPrefix(arg, prefix+"=")
}
if arg == prefix && i+1 < len(args) {
return args[i+1]
}
}
return ""
}
func rawArgsRequestJSON(args []string) bool {
for i, arg := range args {
if arg == "--format=json" || arg == "-f=json" {
return true
}
if (arg == "--format" || arg == "-f") && i+1 < len(args) && strings.EqualFold(args[i+1], "json") {
return true
}
}
return false
}
type chatWorkbookGuidance struct {
message string
reason string
actions []string
examples []string
}
var chatRequiredGuidance = map[string]chatWorkbookGuidance{
"chat message send-by-webhook": {
"Webhook 发送参数不完整",
"Webhook 消息必须同时提供机器人地址中的 access_token、标题和正文;不能降级为普通群消息",
[]string{"从自定义机器人 Webhook 地址提取 token", "同时补齐 --title 和 --text,并确保包含机器人安全关键词"},
[]string{`dws chat message send-by-webhook --token <access_token> --title "dws测试通知" --text "dws测试:评测结果已出" --format json`},
},
"chat group rename": {
"群重命名缺少群 ID 或新名称", "--id 必须是群 openConversationId,--name 是新的群名称",
[]string{"先用 chat search 获取群 openConversationId", "同时提供 --id 和 --name"},
[]string{`dws chat group rename --id <openConversationId> --name "新群名" --format json`},
},
"chat group dismiss": {
"解散群缺少目标群 ID", "解散群不可逆且需要群主权限,--group 必须是 openConversationId",
[]string{"先确认目标群和影响范围", "获取 openConversationId 后再执行,并按运行时要求确认"},
[]string{`dws chat group dismiss --group <openConversationId> --format json`},
},
"chat group quit": {
"退出群缺少目标群 ID", "quit 表示当前用户退出群聊,不会解散整个群;--group 必须是 openConversationId",
[]string{"确认你要退出而不是解散群", "先获取目标群 openConversationId"},
[]string{`dws chat group quit --group <openConversationId> --format json`},
},
"chat group set-admin": {
"设置群管理员参数不完整", "需要目标群以及一个或多个成员;默认设为管理员,--off 表示取消管理员",
[]string{"补充 --group", "通过 --user 或 --users 指定成员,取消管理员时增加 --off"},
[]string{`dws chat group set-admin --group <openConversationId> --users <userId1>,<userId2> --format json`},
},
"chat group transfer-owner": {
"转让群主参数不完整", "--group 指定群,--new-owner 使用 openDingTalkId,--user 使用 userId",
[]string{"补充群 openConversationId", "在 --new-owner 和 --user 中选择一个新群主标识"},
[]string{`dws chat group transfer-owner --group <openConversationId> --new-owner <openDingTalkId> --format json`},
},
"chat group update-nick": {
"修改本人群昵称参数不完整", "update-nick 只修改当前登录用户在指定群里的昵称,需要群 ID 和新昵称",
[]string{"补充 --group openConversationId", "补充新的昵称参数"},
[]string{`dws chat group update-nick --group <openConversationId> --nick "新昵称" --format json`},
},
"chat group update-icon": {
"更新群头像参数不完整", "需要群 openConversationId 和上游已经获得的有效图片 mediaId",
[]string{"补充 --group", "从上游媒体能力获取 mediaId 后传入 --icon-media-id"},
[]string{`dws chat group update-icon --group <openConversationId> --icon-media-id <mediaId> --format json`},
},
"chat group share-invite": {
"分享群邀请参数不完整", "--source 是被分享群,--target 是接收分享的会话,--receiver 是接收分享的单聊用户",
[]string{"补充 --source", "在 --target 和 --receiver 中选择一个接收目标"},
[]string{`dws chat group share-invite --source <源群ID> --target <目标会话ID> --format json`},
},
"chat message reply": {
"引用回复参数不完整", "会话 ID、原消息 ID、原发送者和回复正文必须来自或对应同一条原消息",
[]string{"先拉取目标消息", "补齐 conversation-id、ref-msg-id、ref-sender 和 text"},
[]string{`dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text "收到" --format json`},
},
"chat message forward": {
"转发消息参数不完整", "消息 ID 必须属于源会话,并需要明确源会话和目标会话",
[]string{"先从源会话拉取真实消息 ID", "确认 src 和 dest 没有写反"},
[]string{`dws chat message forward --src-conversation-id <源会话ID> --msg-id <openMessageId> --dest-conversation-id <目标会话ID> --format json`},
},
"chat message recall": {
"撤回消息参数不完整", "用户消息撤回需要会话 ID 和本人发送的消息 ID;机器人消息应使用 recall-by-bot",
[]string{"确认消息由当前用户发送", "补齐 conversation-id 和 msg-id"},
[]string{`dws chat message recall --conversation-id <openConversationId> --msg-id <openMessageId> --format json`},
},
"chat message read-status": {
"查询消息已读状态参数不完整", "只能查询当前用户发出消息的已读状态,需要会话和消息标识",
[]string{"补齐会话和消息 ID", "人员筛选时区分 userId 与 openDingTalkId"},
[]string{`dws chat message read-status --conversation-id <openConversationId> --message-id <openMessageId> --format json`},
},
"chat message list-by-ids": {
"缺少消息 ID 列表", "--msg-ids 使用逗号分隔的真实 openMessageId,单次最多 50 条",
[]string{"先拉取真实消息 ID", "将不超过 50 条 ID 用逗号连接"},
[]string{`dws chat message list-by-ids --msg-ids <id1>,<id2> --format json`},
},
"chat message download-media": {
"媒体下载参数不完整", "type、resource-id、message-id、open-conversation-id 和 output 必须完整,且资源与消息来自同一条消息",
[]string{"先拉取目标媒体消息", "从同一条消息取得资源、消息和会话标识"},
[]string{`dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/ --format json`},
},
"chat message add-emoji": {
"添加表情回应参数不完整", "需要真实会话 ID、消息 ID 和 emoji 名称",
[]string{"先拉取目标消息", "补齐 conversation-id、msg-id 和 emoji"},
[]string{`dws chat message add-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji "赞" --format json`},
},
"chat message remove-emoji": {
"移除表情回应参数不完整", "只能移除当前用户已添加的同名回应,需要会话、消息和 emoji 名称完全匹配",
[]string{"确认当前用户添加过该回应", "补齐 conversation-id、msg-id 和 emoji"},
[]string{`dws chat message remove-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji "赞" --format json`},
},
"chat message list-by-sender": {
"按发送者查询参数不完整", "必须提供开始时间以及发送者 userId/openDingTalkId 二选一,可选 end 和 cursor",
[]string{"补充 --start", "在 sender-user-id 和 sender-open-dingtalk-id 中选择一个"},
[]string{`dws chat message list-by-sender --sender-user-id <userId> --start "2026-07-14T00:00:00+08:00" --format json`},
},
"chat message query-send-status": {
"缺少发送任务 ID", "--open-task-id 来自 message send 返回的 openTaskId,不是消息 ID",
[]string{"先执行 message send", "从发送结果读取 openTaskId"},
[]string{`dws chat message query-send-status --open-task-id <openTaskId> --format json`},
},
}
func enrichChatWorkbookError(cmd *cobra.Command, err error) error {
if cmd == nil || err == nil {
return err
}
path := cmd.CommandPath()
if fields := strings.Fields(path); len(fields) > 1 {
path = strings.Join(fields[1:], " ")
}
message := err.Error()
var guide chatWorkbookGuidance
switch {
case path == "chat message send" &&
(strings.Contains(message, "unknown flag: --at-user-ids") ||
strings.Contains(message, "unknown flag: --at-users") ||
strings.Contains(message, "unknown flag: --mention")):
guide = chatWorkbookGuidance{
"群消息 @成员参数不正确",
"当前用户身份发送群消息时使用 --at-open-dingtalk-ids,参数值必须是成员的 openDingTalkId;--at-user-ids、--at-users、--mention 均不是有效参数",
[]string{"先查询目标成员的 openDingTalkId", "改用 --at-open-dingtalk-ids,并在正文中写入 <@openDingTalkId>"},
[]string{`dws chat message send --group <openConversationId> --at-open-dingtalk-ids <openDingTalkId> --text "<@openDingTalkId> 请关注" --format json`},
}
case path == "chat media upload":
guide = chatWorkbookGuidance{
"chat media upload 已下线",
"当前 CLI 不再通过该命令把本地文件转换为 mediaId,本地图片和文件统一由 message send 的 file 路径上传并发送",
[]string{"发送本地图片或文件时使用 --msg-type file --file-path", "只有上游已提供 mediaId 时才使用 --msg-type image --media-id"},
[]string{`dws chat message send --group <openConversationId> --msg-type file --file-path ./image.png --format json`},
}
case path == "chat group members" && strings.Contains(message, "unknown flag: --group"):
guide = chatWorkbookGuidance{
"群成员列表命令路径或群参数不正确",
"群成员列表的可执行命令是 chat group members,群 ID 参数名为 --id;不存在 members list --group 这一组合",
[]string{"移除多余的 list 子命令", "将 --group 改为 --id"},
[]string{`dws chat group members --id <openConversationId> --format json`},
}
case path == "chat group rename" && strings.Contains(message, "unknown flag: --group"):
guide = chatWorkbookGuidance{
"群重命名命令不支持 --group",
"chat group rename 使用 --id 接收群 openConversationId,而不是 --group",
[]string{"将 --group 改为 --id", "群 ID 不确定时先用 chat search 查询"},
[]string{`dws chat group rename --id <openConversationId> --name "新群名" --format json`},
}
case path == "chat group create" && strings.Contains(message, "unknown flag: --members"):
guide = chatWorkbookGuidance{
"建群命令不支持 --members",
"chat group create 使用 --users 接收逗号分隔的成员 userId;--members 是其他命令的参数名",
[]string{"将 --members 改为 --users", "成员标识不确定时先查询 userId"},
[]string{`dws chat group create --name "<群名称>" --users <userId1>,<userId2> --format json`},
}
case path == "chat group bots" && strings.Contains(message, "unknown flag: --id"):
guide = chatWorkbookGuidance{
"群机器人列表命令不支持 --id",
"chat group bots 使用 --group 接收群 openConversationId;该参数名与 members、rename 命令不同",
[]string{"将 --id 改为 --group", "群 ID 不确定时先用 chat search 查询"},
[]string{`dws chat group bots --group <openConversationId> --format json`},
}
case path == "chat message list-mentions" && strings.Contains(message, "required flag"):
guide = chatWorkbookGuidance{
"缺少必填参数:--start、--end",
"查询 @我 消息必须同时提供 ISO-8601 格式的开始和结束时间;只提供分页参数不能确定查询范围",
[]string{"同时补充 --start 和 --end,不要逐个参数反复试错", "按本地时区设置明确的查询时间窗"},
[]string{`dws chat message list-mentions --start "2026-07-23T00:00:00+08:00" --end "2026-07-30T23:59:59+08:00" --limit 50 --format json`},
}
case path == "chat message send" && strings.Contains(message, "--group, --user or --open-dingtalk-id is required"):
guide = chatWorkbookGuidance{
"缺少消息接收目标",
"发送消息必须在 --group、--user、--open-dingtalk-id 中选择且只选择一个接收目标",
[]string{"发群消息时先查询并传入群 openConversationId", "发单聊时先查询并传入 userId 或 openDingTalkId"},
[]string{`dws chat message send --group <openConversationId> --text "评测消息" --format json`, `dws chat message send --open-dingtalk-id <openDingTalkId> --text "评测消息" --format json`},
}
case path == "chat search" && strings.Contains(message, "query"):
guide = chatWorkbookGuidance{
"缺少群聊搜索关键词:--query",
"群聊搜索需要关键词才能定位候选群,不能使用空查询",
[]string{"使用 --query 传入群名称或名称片段", "从结果中读取 openConversationId 供后续群命令使用"},
[]string{`dws chat search --query "项目群" --format json`},
}
case path == "chat message search-advanced":
guide = chatWorkbookGuidance{
"高级消息搜索至少需要一个搜索条件",
"空条件搜索无法限定目标消息,必须提供关键词、人员、@我状态或会话范围中的至少一种",
[]string{"按内容搜索时传入 --query", "也可通过 --user、--at-me 或 --conversation-ids 缩小范围"},
[]string{`dws chat message search-advanced --query "评审" --format json`},
}
case path == "chat message search" && strings.Contains(message, "required flag"):
guide = chatWorkbookGuidance{
"关键词消息搜索缺少完整查询条件",
"关键词消息搜索需要 --query、--start 和 --end;当前命令没有提供完整的关键词和时间范围",
[]string{"补充搜索关键词", "同时提供 ISO-8601 格式的开始和结束时间"},
[]string{`dws chat message search --query "评审" --start "2026-07-23T00:00:00+08:00" --end "2026-07-30T23:59:59+08:00" --format json`},
}
case path == "chat message list-all" && strings.Contains(message, "required flag"):
guide = chatWorkbookGuidance{
"跨会话消息查询缺少时间范围",
"拉取全部会话消息必须使用 --start 和 --end 限定范围,避免无边界查询历史消息",
[]string{"同时补充 --start 和 --end", "结果存在 hasMore 时使用 nextCursor 继续翻页"},
[]string{`dws chat message list-all --start "2026-07-23T00:00:00+08:00" --end "2026-07-30T23:59:59+08:00" --limit 50 --format json`},
}
case path == "chat message list-topic-replies" && strings.Contains(message, "topic-id"):
guide = chatWorkbookGuidance{
"缺少话题定位参数:--topic-id",
"topic-id 不能臆造,必须来自同一群聊消息列表中目标话题消息的 openConvThreadId",
[]string{"先执行 chat message list 拉取目标群消息", "从目标话题消息读取 openConvThreadId 并作为 --topic-id"},
[]string{`dws chat message list --group <openConversationId> --time "2026-07-30 23:59:59" --direction older --format json`, `dws chat message list-topic-replies --group <openConversationId> --topic-id <openConvThreadId> --limit 50 --format json`},
}
case path == "chat message send-by-bot" && strings.Contains(message, "required flag"):
guide = chatWorkbookGuidance{
"机器人发送消息缺少必填参数",
"机器人发送需要 robotCode、标题、正文以及群聊或单聊目标,当前参数不完整",
[]string{"补充 --robot-code 和 --title", "通过 --group 或用户参数指定接收目标"},
[]string{`dws chat message send-by-bot --robot-code <robotCode> --group <openConversationId> --title "通知" --text "hello" --format json`},
}
case path == "chat message recall-by-bot" && strings.Contains(message, "required flag"):
guide = chatWorkbookGuidance{
"机器人撤回消息缺少 robotCode 或 processQueryKey",
"--keys 的 processQueryKey 来自机器人发送消息的返回结果,不能凭空构造",
[]string{"补充发送该消息的 --robot-code", "从发送结果读取 processQueryKey 并传给 --keys"},
[]string{`dws chat message recall-by-bot --robot-code <robotCode> --group <openConversationId> --keys <processQueryKey> --format json`},
}
case path == "chat message list" && strings.Contains(message, "required flag") && strings.Contains(message, "--time"):
guide = chatWorkbookGuidance{
"拉取会话消息缺少时间锚点:--time",
"消息列表按时间向前或向后拉取,必须提供一个明确的时间锚点",
[]string{"补充格式为 YYYY-MM-DD HH:mm:ss 的 --time", "使用 --direction older 或 newer 明确查询方向"},
[]string{`dws chat message list --group <openConversationId> --time "2026-07-30 10:00:00" --direction older --format json`},
}
case path == "chat message list" && strings.Contains(message, "--group, --user or --open-dingtalk-id is required"):
guide = chatWorkbookGuidance{
"拉取消息时缺少会话目标",
"必须在群聊 openConversationId、单聊 userId、单聊 openDingTalkId 中选择且只选择一个目标",
[]string{"群聊先用 chat search 获取 openConversationId", "单聊先查询人员标识,再传 --user 或 --open-dingtalk-id"},
[]string{`dws chat message list --group <openConversationId> --time "2026-07-15 10:00:00" --format json`},
}
case path == "chat message send" && strings.Contains(message, "media-id is required"):
guide = chatWorkbookGuidance{
"图片消息缺少 --media-id",
"msg-type=image 只接受上游已经获得的有效 mediaId,不能把本地文件名当作 mediaId",
[]string{"已有 mediaId 时补充 --media-id", "发送本地图片时改用 --msg-type file --file-path"},
[]string{`dws chat message send --group <openConversationId> --msg-type image --media-id <mediaId> --format json`, `dws chat message send --group <openConversationId> --msg-type file --file-path ./image.png --format json`},
}
case path == "chat message send" && strings.Contains(message, "readable local --file-path is required"):
guide = chatWorkbookGuidance{
"文件消息缺少可读的本地文件",
"file、audio、video 消息需要可读的 --file-path;旧版 dentry 参数则必须成组提供",
[]string{"优先传入当前机器上可读的 --file-path", "使用旧参数时同时提供 dentry-id、space-id 和 file-name"},
[]string{`dws chat message send --group <openConversationId> --msg-type file --file-path ./report.pdf --format json`},
}
case path == "chat message send" && strings.Contains(message, "--file-path must be a readable local file"):
guide = chatWorkbookGuidance{
"--file-path 指向的文件不可读",
"指定路径不存在、不是普通文件或当前进程没有读取权限,因此无法上传并发送",
[]string{"检查路径拼写并确认文件存在", "改用当前用户可读取的绝对路径或工作目录相对路径"},
[]string{`dws chat message send --group <openConversationId> --msg-type file --file-path ./report.pdf --format json`},
}
case path == "chat message send" && strings.Contains(message, "unsupported --msg-type"):
guide = chatWorkbookGuidance{
"不支持指定的 --msg-type",
"card 不是当前命令支持的消息类型;文本或 Markdown 消息无需传 --msg-type",
[]string{"文本消息移除 --msg-type 并使用 --text", "媒体消息仅使用 image、file、audio、video、location 或 profile"},
[]string{`dws chat message send --group <openConversationId> --text "消息正文" --format json`},
}
case path == "chat message send" && strings.Contains(message, "message content required"):
guide = chatWorkbookGuidance{
"群消息缺少正文内容",
"未提供 --text 或位置参数,同时也没有选择需要专用参数的媒体消息类型",
[]string{"发送文字时补充 --text", "发送文件时使用 --msg-type file --file-path"},
[]string{`dws chat message send --group <openConversationId> --text "消息正文" --format json`},
}
}
if guide.message == "" {
if required, ok := chatRequiredGuidance[path]; ok &&
(strings.Contains(message, "required") || strings.Contains(message, "缺少")) {
guide = required
} else if strings.HasPrefix(path, "chat ") &&
(strings.Contains(message, "required") ||
strings.Contains(message, "invalid") ||
strings.Contains(message, "unsupported") ||
strings.Contains(message, "unknown flag") ||
strings.Contains(message, "must be")) {
example := fmt.Sprintf("dws %s --help", path)
if meta, ok := cli.ResolveMeta(path); ok && len(meta.Selection.Examples) > 0 {
example = meta.Selection.Examples[0]
if !strings.Contains(example, "--format") {
example += " --format json"
}
}
guide = chatWorkbookGuidance{
"Chat 命令参数校验失败",
message,
[]string{"根据错误补齐或修正参数", fmt.Sprintf("运行 dws %s --help 核对当前命令参数", path)},
[]string{example},
}
} else {
return err
}
}
return apperrors.NewValidation(
guide.message,
apperrors.WithReason(guide.reason),
apperrors.WithHint(guide.actions[0]),
apperrors.WithActions(guide.actions...),
apperrors.WithExamples(guide.examples...),
apperrors.WithCause(err),
)
}
// newPreParseValidationError keeps pipeline handler identity in internal logs
// while exposing only the underlying parameter-domain error to CLI users.
func newPreParseValidationError(err error) error {
@@ -700,9 +220,6 @@ func flagErrorWithSuggestions(cmd *cobra.Command, err error) error {
apperrors.WithAvailableFlags(cmdutil.VisibleFlagNames(cmd)...),
)
}
if enriched := enrichChatWorkbookError(cmd, err); enriched != err {
return enriched
}
// Common flag aliases and suggestions
suggestions := map[string]string{
-141
View File
@@ -43,153 +43,12 @@ func configureRootHelp(root *cobra.Command) {
if cmd != root {
defaultHelpFunc(cmd, args)
cli.RenderSafetyAnnotation(cmd)
renderChatAgentSelectionHint(cmd)
return
}
renderRootHelp(root)
})
}
type chatHelpGuidance struct {
reason string
action string
example string
}
var chatWorkbookHelpGuidance = map[string]chatHelpGuidance{
"chat group members": {
"群成员列表固定使用 --id 传群 openConversationId,不使用消息命令的 --group。",
"先查群 ID,再直接执行 members;不要追加多余的 list 子命令。",
`dws chat group members --id <openConversationId> --format json`,
},
"chat group members add": {
"添加群成员固定使用 --id 指定群、--users 指定成员。",
"先查询群 ID 和成员 userId/openDingTalkId,再执行添加。",
`dws chat group members add --id <openConversationId> --users <userId1>,<userId2> --format json`,
},
"chat group members remove": {
"移除群成员使用 --id 和 --users,且不能移除群主。",
"先确认成员和不可逆影响,检查群主身份后再执行。",
`dws chat group members remove --id <openConversationId> --users <userId> --format json`,
},
"chat group members add-bot": {
"添加机器人属于群成员管理,群参数沿用 --id,并需要 robot-code。",
"确认机器人编码和目标群后执行。",
`dws chat group members add-bot --id <openConversationId> --robot-code <robotCode> --format json`,
},
"chat group members remove-bot": {
"移除机器人固定使用 --id 指定群、--bot-id 指定群内机器人。",
"先列出群机器人取得 openBotId,再执行移除。",
`dws chat group members remove-bot --id <openConversationId> --bot-id <openBotId> --format json`,
},
"chat group members list-by-ids": {
"批量查询成员详情使用 --id + --users,users 为成员标识列表。",
"确认目标群和成员 ID 后再查询。",
`dws chat group members list-by-ids --id <openConversationId> --users <openDingTalkId1>,<openDingTalkId2> --format json`,
},
"chat group create": {
"建群使用 --users;创建结果中的群 ID 可继续传给 members add 和 rename。",
"先准备成员 userId,创建后保存返回的 openConversationId。",
`dws chat group create --name "项目群" --users <userId1>,<userId2> --format json`,
},
"chat group rename": {
"群改名只使用 --id + --name,不能使用 --group。",
"先通过 chat search 获取 openConversationId。",
`dws chat group rename --id <openConversationId> --name "新群名" --format json`,
},
"chat message list": {
"message list 按会话和时间拉取消息,不执行服务端关键词搜索。",
"按关键词查找时改用 message search;拉历史时提供会话和 time。",
`dws chat message list --group <openConversationId> --time "2026-07-30 23:59:59" --direction older --format json`,
},
"chat message search": {
"关键词审计应使用服务端搜索,并同时提供 query、start、end。",
"不要用 message list 拉全量后人工筛选。",
`dws chat message search --query "评审" --start "2026-07-01T00:00:00+08:00" --end "2026-07-31T23:59:59+08:00" --format json`,
},
"chat message search-advanced": {
"简单关键词优先 message search;只有组合人员、@、会话等条件时才使用 search-advanced。",
"至少提供一个真实搜索条件,分页参数不算搜索条件。",
`dws chat message search-advanced --query "评审" --conversation-ids <openConversationId> --format json`,
},
"chat message list-all": {
"list-all 按时间跨会话拉取消息,不执行关键词匹配。",
"需要关键词时改用 message search,并始终限制时间范围。",
`dws chat message list-all --start "2026-07-01T00:00:00+08:00" --end "2026-07-31T23:59:59+08:00" --format json`,
},
"chat message list-by-sender": {
"list-by-sender 的核心条件是发送者;核心条件是关键词时应使用 message search。",
"提供发送者 ID 和开始时间,按 nextCursor 翻页。",
`dws chat message list-by-sender --sender-user-id <userId> --start "2026-07-01T00:00:00+08:00" --format json`,
},
}
func renderChatWorkbookHelpGuidance(cmd *cobra.Command) {
if cmd == nil {
return
}
path := strings.TrimSpace(strings.TrimPrefix(cmd.CommandPath(), cmd.Root().Name()+" "))
guide, ok := chatWorkbookHelpGuidance[path]
if !ok {
meta, metaOK := cli.ResolveMeta(path)
if !metaOK || meta.Identity.ProductID != "chat" {
return
}
reason := meta.Selection.AgentSummary
if reason == "" {
reason = "执行前需要确认该 Chat 命令的适用场景、必填参数和安全边界。"
}
action := "根据帮助正文补齐必填参数,并在实际执行时增加 --format json。"
if len(meta.Selection.UseWhen) > 0 {
action = meta.Selection.UseWhen[0]
}
example := "dws " + path + " --format json"
if len(meta.Selection.Examples) > 0 {
example = meta.Selection.Examples[0]
if !strings.Contains(example, "--format") {
example += " --format json"
}
}
guide = chatHelpGuidance{reason: reason, action: action, example: example}
}
w := cmd.ErrOrStderr()
_, _ = fmt.Fprintln(w, "错误信息:当前为执行前 guidance,不是运行失败")
_, _ = fmt.Fprintln(w, "原因:"+guide.reason)
_, _ = fmt.Fprintln(w, "建议操作:")
_, _ = fmt.Fprintln(w, "1. "+guide.action)
_, _ = fmt.Fprintln(w, "示例:")
_, _ = fmt.Fprintln(w, "1. "+guide.example)
}
// renderChatAgentSelectionHint exposes the reviewed Chat selection contract in
// command help without reintroducing a second product-local guidance map.
// Selection prose remains authored in schema_hints/selection/chat.json and is
// consumed through the repository-wide ResolveMeta API.
func renderChatAgentSelectionHint(cmd *cobra.Command) {
cliPath := strings.TrimSpace(strings.TrimPrefix(cmd.CommandPath(), cmd.Root().Name()+" "))
meta, ok := cli.ResolveMeta(cliPath)
if !ok || meta.Identity.ProductID != "chat" {
return
}
selection := meta.Selection
w := cmd.OutOrStdout()
_, _ = fmt.Fprintln(w, "Agent guidance:")
if selection.AgentSummary != "" {
_, _ = fmt.Fprintf(w, " Outcome: %s\n", selection.AgentSummary)
}
for _, scenario := range selection.UseWhen {
_, _ = fmt.Fprintf(w, " Use when: %s\n", scenario)
}
for _, scenario := range selection.AvoidWhen {
_, _ = fmt.Fprintf(w, " Avoid when: %s\n", scenario)
}
for _, example := range selection.Examples {
_, _ = fmt.Fprintf(w, " Example: %s\n", example)
}
_, _ = fmt.Fprintln(w, " Output: Agent execution should add --format json.")
}
func renderRootHelp(root *cobra.Command) {
services := visibleMCPRootCommands(root)
utilities := visibleUtilityRootCommands(root)
-71
View File
@@ -69,77 +69,6 @@ func TestCalendarEventCreateHelpKeepsRoomsStringMetavar(t *testing.T) {
}
}
func TestChatAgentGuidanceRendersOnlyOnStdout(t *testing.T) {
cmd := NewRootCommand()
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs([]string{"chat", "clear-messages", "--help"})
if err := cmd.Execute(); err != nil {
t.Fatalf("chat clear-messages --help: %v\nstdout:\n%s\nstderr:\n%s", err, stdout.String(), stderr.String())
}
for _, want := range []string{"Agent guidance:", "Outcome:", "Use when:", "Avoid when:", "Example:", "Output:"} {
if !strings.Contains(stdout.String(), want) {
t.Fatalf("chat help stdout missing %q:\n%s", want, stdout.String())
}
}
if got := strings.TrimSpace(stderr.String()); got != "" {
t.Fatalf("chat help wrote guidance or warnings to stderr:\n%s", got)
}
}
func TestCandidateAlignmentChatGuidance(t *testing.T) {
tests := []struct {
args []string
wants []string
}{
{[]string{"chat", "message", "query-send-status", "--help"}, []string{"sendStatus=SUCCESS", "FAILED"}},
{[]string{"chat", "+messages-query-send-status", "--help"}, []string{"sendStatus=SUCCESS", "openTaskId"}},
{[]string{"chat", "message", "set-pin-msg", "--help"}, []string{"openTaskId", "set-top-msg", "chat set-top"}},
{[]string{"chat", "message", "unset-pin-msg", "--help"}, []string{"复用", "unset-top-msg"}},
{[]string{"chat", "message", "add-emoji", "--help"}, []string{"同一条真实消息", "openTaskId"}},
{[]string{"chat", "message", "remove-emoji", "--help"}, []string{"复用", "表情名称"}},
{[]string{"chat", "group", "members", "remove", "--help"}, []string{"群主", "转让群主"}},
{[]string{"chat", "group", "update-icon", "--help"}, []string{"dentryId", "能力边界"}},
{[]string{"chat", "group", "update-settings", "--help"}, []string{"群级设置", "user-settings set"}},
{[]string{"chat", "group", "user-settings", "query", "--help"}, []string{"当前用户视角", "保存原值"}},
{[]string{"chat", "group", "user-settings", "set", "--help"}, []string{"再次 query", "真实值恢复"}},
}
for _, tc := range tests {
cmd := NewRootCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
if err := cmd.Execute(); err != nil {
t.Fatalf("%v: %v", tc.args, err)
}
for _, want := range tc.wants {
if !strings.Contains(stdout.String(), want) {
t.Fatalf("%v help missing %q:\n%s", tc.args, want, stdout.String())
}
}
}
}
func TestCandidateAlignmentDriveUploadSchemaGuidance(t *testing.T) {
cmd := NewRootCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"schema", "drive.upload", "--format", "json"})
if err := cmd.Execute(); err != nil {
t.Fatalf("drive.upload schema: %v", err)
}
for _, want := range []string{"暂时不要发送", "不会发送聊天消息"} {
if !strings.Contains(stdout.String(), want) {
t.Fatalf("drive.upload schema missing %q:\n%s", want, stdout.String())
}
}
}
func TestRootKeepsMainBranchChatCompatibilityCommands(t *testing.T) {
root := NewRootCommand()
listDirect := mustFindCommand(t, root, "chat", "message", "list-direct")
+2 -2
View File
@@ -742,7 +742,7 @@ func (r *runtimeRunner) executeInvocation(ctx context.Context, endpoint string,
apperrors.WithOperation("tools/call"),
apperrors.WithReason("mcp_tool_error"),
apperrors.WithServerKey(invocation.CanonicalProduct),
apperrors.WithHint(apperrors.SuggestBusinessHint(callResult.Content)),
apperrors.WithHint("MCP tool returned a business error; check tool parameters and refer to skill documentation."),
apperrors.WithServerDiag(diag),
)
// PAT scope error in business response: offer human-readable output and retry
@@ -767,7 +767,7 @@ func (r *runtimeRunner) executeInvocation(ctx context.Context, endpoint string,
apperrors.WithOperation("tools/call"),
apperrors.WithReason("business_error"),
apperrors.WithServerKey(invocation.CanonicalProduct),
apperrors.WithHint(apperrors.SuggestBusinessHint(callResult.Content)),
apperrors.WithHint("The API returned a business-level error. Check required parameters and values."),
apperrors.WithServerDiag(diag),
)
}
@@ -106,7 +106,7 @@ func TestEmbeddedShortcutProgressiveQueriesReturnCompleteContracts(t *testing.T)
product := executeShortcutSchemaQuery(t, "chat")
productPayload, _ := product["product"].(map[string]any)
if got, want := int(product["count"].(float64)), 159; got != want {
if got, want := int(product["count"].(float64)), 129; got != want {
t.Fatalf("schema chat count = %d, want %d", got, want)
}
summaries := schemaContractObjectSlice(productPayload["tools"])
+16 -6
View File
@@ -131,9 +131,12 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat +chat-bots",
Aliases: map[string]string{
"chat": "group",
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat +chat-dismiss",
@@ -148,9 +151,12 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat +chat-invite-url",
Aliases: map[string]string{
"chat": "group",
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat +chat-mute",
@@ -307,7 +313,11 @@ var generatedParamAliases = []ParamAliasEntry{
},
{
CLIPath: "chat +messages-mget",
Blocked: []string{"msg-id", "open-message-id", "ref-msg-id", "src-msg-id"},
Aliases: map[string]string{
"message-ids": "msg-ids",
"open-message-ids": "msg-ids",
},
Blocked: []string{"message-id", "msg-id", "open-message-id", "ref-msg-id", "src-msg-id"},
},
{
CLIPath: "chat +messages-read-status",
@@ -1099,7 +1109,7 @@ var generatedParamAliases = []ParamAliasEntry{
"open-conversation-ids": "conversation-ids",
"user-ids": "users",
},
Blocked: []string{"at-user-ids", "chat-id", "group-id", "group-ids", "open-conversation-id", "staff-id", "uid", "userid"},
Blocked: []string{"at-user-ids", "chat-id", "conversation-id", "group-id", "group-ids", "open-conversation-id", "staff-id", "uid", "userid"},
},
{
CLIPath: "chat message send",
+346 -99
View File
@@ -264,7 +264,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"confirmation": "not_required",
"effect": "read",
@@ -308,7 +308,7 @@
},
"avoid_when": {
"value": [
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -317,7 +317,7 @@
"candidates": [
{
"value": [
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -525,7 +525,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"confirmation": "not_required",
"effect": "read",
@@ -568,7 +568,7 @@
},
"avoid_when": {
"value": [
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -577,7 +577,7 @@
"candidates": [
{
"value": [
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -1815,7 +1815,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"confirmation": "not_required",
"effect": "read",
@@ -1858,7 +1858,7 @@
},
"avoid_when": {
"value": [
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -1867,7 +1867,7 @@
"candidates": [
{
"value": [
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -3624,7 +3624,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"confirmation": "not_required",
"effect": "read",
@@ -3667,7 +3667,7 @@
},
"avoid_when": {
"value": [
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -3676,7 +3676,7 @@
"candidates": [
{
"value": [
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4659,7 +4659,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"confirmation": "not_required",
"effect": "read",
@@ -4702,7 +4702,7 @@
},
"avoid_when": {
"value": [
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4711,7 +4711,7 @@
"candidates": [
{
"value": [
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4877,7 +4877,7 @@
},
"use_when": {
"value": [
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4886,7 +4886,7 @@
"candidates": [
{
"value": [
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4909,7 +4909,7 @@
"internal/cli/schema_hints/selection/aitable.json"
],
"use_when": [
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
]
},
"aitable +resolve-table": {
@@ -4917,7 +4917,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"confirmation": "not_required",
"effect": "read",
@@ -4960,7 +4960,7 @@
},
"avoid_when": {
"value": [
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4969,7 +4969,7 @@
"candidates": [
{
"value": [
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -5135,7 +5135,7 @@
},
"use_when": {
"value": [
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -5144,7 +5144,7 @@
"candidates": [
{
"value": [
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -5167,7 +5167,7 @@
"internal/cli/schema_hints/selection/aitable.json"
],
"use_when": [
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
]
},
"aitable +role-list": {
@@ -5949,7 +5949,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"confirmation": "not_required",
"effect": "read",
@@ -5993,7 +5993,7 @@
},
"avoid_when": {
"value": [
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -6002,7 +6002,7 @@
"candidates": [
{
"value": [
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11191,11 +11191,11 @@
]
},
"aitable base search": {
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
],
"confirmation": "not_required",
"effect": "read",
@@ -11205,14 +11205,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -11244,7 +11244,7 @@
},
"avoid_when": {
"value": [
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11253,7 +11253,7 @@
"candidates": [
{
"value": [
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11407,7 +11407,7 @@
},
"use_when": {
"value": [
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11416,7 +11416,7 @@
"candidates": [
{
"value": [
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11441,12 +11441,10 @@
"internal/cli/schema_hints/metadata/aitable.json",
"internal/cli/schema_hints/selection/aitable.json",
"internal/cli/schema_mcp_metadata.json#tools.aitable.base_search",
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
"skills/mono/references/products/aitable.md"
],
"use_when": [
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
]
},
"aitable base update": {
@@ -16701,11 +16699,11 @@
]
},
"aitable field get": {
"agent_summary": "底层获取字段完整类型与 config。",
"agent_summary": "获取字段完整配置。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
],
"confirmation": "not_required",
"effect": "read",
@@ -16715,14 +16713,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "底层获取字段完整类型与 config。",
"value": "获取字段完整配置。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "底层获取字段完整类型与 config。",
"value": "获取字段完整配置。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -16754,7 +16752,7 @@
},
"avoid_when": {
"value": [
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -16763,7 +16761,7 @@
"candidates": [
{
"value": [
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -16915,7 +16913,7 @@
},
"use_when": {
"value": [
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
"需要字段类型/config(选项、公式等)详情时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -16924,7 +16922,7 @@
"candidates": [
{
"value": [
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
"需要字段类型/config(选项、公式等)详情时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -16952,12 +16950,10 @@
"internal/cli/schema_hints/selection/aitable.json",
"internal/cli/schema_mcp_metadata.json#tools.aitable.field_get",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-field.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-field.md"
"skills/mono/references/products/aitable/aitable-field.md"
],
"use_when": [
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
"需要字段类型/config(选项、公式等)详情时"
]
},
"aitable field list": {
@@ -22343,11 +22339,11 @@
]
},
"aitable record create": {
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
],
"confirmation": "not_required",
"effect": "write",
@@ -22357,14 +22353,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"value": "新增记录(cells 的 key 必须是 fieldId)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"value": "新增记录(cells 的 key 必须是 fieldId)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -22396,7 +22392,7 @@
},
"avoid_when": {
"value": [
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -22405,7 +22401,7 @@
"candidates": [
{
"value": [
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -22557,7 +22553,7 @@
},
"use_when": {
"value": [
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
"需要插入新行数据时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -22566,7 +22562,7 @@
"candidates": [
{
"value": [
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
"需要插入新行数据时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -22596,12 +22592,10 @@
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-attachment.md",
"skills/mono/references/products/aitable/aitable-record-create.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md"
"skills/mono/references/products/aitable/aitable-record-create.md"
],
"use_when": [
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
"需要插入新行数据时"
]
},
"aitable record delete": {
@@ -23935,11 +23929,11 @@
]
},
"aitable record query": {
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"confirmation": "not_required",
"effect": "read",
@@ -23949,14 +23943,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -23988,7 +23982,7 @@
},
"avoid_when": {
"value": [
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -23997,7 +23991,7 @@
"candidates": [
{
"value": [
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24157,7 +24151,7 @@
},
"use_when": {
"value": [
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
"查看、筛选、全文搜索或遍历记录时的主入口"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24166,7 +24160,7 @@
"candidates": [
{
"value": [
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
"查看、筛选、全文搜索或遍历记录时的主入口"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24195,12 +24189,10 @@
"internal/cli/schema_mcp_metadata.json#tools.aitable.query_records",
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-record-query.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md"
"skills/mono/references/products/aitable/aitable-record-query.md"
],
"use_when": [
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
"查看、筛选、全文搜索或遍历记录时的主入口"
]
},
"aitable record query-empty": {
@@ -24719,11 +24711,11 @@
]
},
"aitable record update": {
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"confirmation": "not_required",
"effect": "write",
@@ -24733,14 +24725,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -24772,7 +24764,7 @@
},
"avoid_when": {
"value": [
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24781,7 +24773,7 @@
"candidates": [
{
"value": [
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24933,7 +24925,7 @@
},
"use_when": {
"value": [
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
"需要修改已有记录若干字段时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24942,7 +24934,7 @@
"candidates": [
{
"value": [
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
"需要修改已有记录若干字段时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24971,12 +24963,10 @@
"internal/cli/schema_mcp_metadata.json#tools.aitable.record_update",
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-attachment.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md"
"skills/mono/references/products/aitable/aitable-attachment.md"
],
"use_when": [
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
"需要修改已有记录若干字段时"
]
},
"aitable record upsert": {
@@ -27872,11 +27862,11 @@
]
},
"aitable table get": {
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
"agent_summary": "获取数据表结构(字段+视图目录)。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
],
"confirmation": "not_required",
"effect": "read",
@@ -27886,14 +27876,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "底层获取数据表结构、精简字段目录与视图目录。",
"value": "获取数据表结构(字段+视图目录)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "底层获取数据表结构、精简字段目录与视图目录。",
"value": "获取数据表结构(字段+视图目录)。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -27925,7 +27915,7 @@
},
"avoid_when": {
"value": [
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -27934,7 +27924,7 @@
"candidates": [
{
"value": [
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -28086,7 +28076,7 @@
},
"use_when": {
"value": [
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -28095,7 +28085,7 @@
"candidates": [
{
"value": [
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -28125,12 +28115,10 @@
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-advperm.md",
"skills/mono/references/products/aitable/aitable-primary-doc.md",
"skills/mono/references/products/aitable/aitable-record-create.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
"skills/mono/references/products/aitable/aitable-record-create.md"
],
"use_when": [
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
]
},
"aitable table list": {
@@ -37087,6 +37075,265 @@
"用户明确要求停止某自动化工作流时"
]
},
"aitable workflow edit-example": {
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws aitable workflow edit-example"
],
"field_provenance": {
"agent_summary": {
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"candidates": [
{
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"avoid_when": {
"value": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"candidates": [
{
"value": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"effect": {
"value": "read",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "read",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"examples": {
"value": [
"dws aitable workflow edit-example"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"candidates": [
{
"value": [
"dws aitable workflow edit-example"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"interface_mode": {
"value": "composite",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "composite",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"interface_reason": {
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"interface_ref": {
"value": null,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"candidates": [
{
"value": null,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "final interface mode composite forbids a direct MCP interface_ref"
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
},
"risk": {
"value": "low",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "low",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"use_when": {
"value": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"candidates": [
{
"value": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "composite",
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"reviewed": true,
"risk": "low",
"source_refs": [
"cobra-help:dws aitable workflow edit-example --help",
"internal/cli/schema_command_registry.json#aitable.workflow_edit_example",
"internal/cli/schema_hints/metadata/aitable.json",
"internal/cli/schema_hints/selection/aitable.json",
"mcp-contract:aitable/edit_workflow_example",
"skills/mono/references/products/aitable/aitable-workflow.md"
],
"use_when": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
]
},
"aitable workflow enable": {
"agent_summary": "启用工作流。",
"agent_summary_source": "dws-agent-selection/aitable",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12 -21
View File
@@ -10478,10 +10478,8 @@
"agent_summary_source": "dws-agent-selection/drive",
"availability": "available",
"avoid_when": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -10498,14 +10496,14 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件",
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10527,31 +10525,27 @@
},
"avoid_when": {
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10607,7 +10601,7 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
@@ -10617,7 +10611,7 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10710,7 +10704,7 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10739,26 +10733,24 @@
"use_when": {
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
}
]
}
@@ -10782,8 +10774,7 @@
],
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
},
+21 -22
View File
@@ -1,19 +1,19 @@
{
"version": 1,
"source_hash": "sha256:f239237a9b87fa5a95a0520b2e2f2a112e117b273de8418763ba0ecd8652b317",
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
"source_hash": "sha256:f3d5e5d6ca3c613b1d004d68eca89679d0f8cffe882d7fed0fbb1bcef078fdc9",
"surface_hash": "sha256:41044fe1b6723564c40d684381ac6d6f23c4dfd58f9769350f02f223f02fa894",
"coverage": {
"surface_products": 26,
"products_with_metadata": 26,
"surface_tools": 875,
"tools_with_metadata": 875,
"tools_with_agent_summary": 875,
"tools_with_use_when": 875,
"tools_with_avoid_when": 875,
"tools_with_examples": 875,
"tools_with_interface_mode": 875,
"unmatched_skill_tools": 97,
"unreviewed_skill_tools": 12
"surface_tools": 846,
"tools_with_metadata": 846,
"tools_with_agent_summary": 846,
"tools_with_use_when": 846,
"tools_with_avoid_when": 846,
"tools_with_examples": 846,
"tools_with_interface_mode": 846,
"unmatched_skill_tools": 122,
"unreviewed_skill_tools": 11
},
"products": {
"aisearch": {
@@ -88,20 +88,20 @@
]
},
"aitable": {
"agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
"agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
"agent_summary_source": "dws-agent-selection/aitable",
"avoid_when": [
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
],
"field_provenance": {
"agent_summary": {
"value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
"value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"candidates": [
{
"value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
"value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true
@@ -110,7 +110,7 @@
},
"avoid_when": {
"value": [
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -118,7 +118,7 @@
"candidates": [
{
"value": [
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -128,7 +128,7 @@
},
"use_when": {
"value": [
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -136,7 +136,7 @@
"candidates": [
{
"value": [
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -153,11 +153,10 @@
"internal/cli/schema_hints/selection/aitable.json",
"skills/mono/SKILL.md",
"skills/mono/references/intent-guide.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
"skills/mono/references/products/aitable.md"
],
"use_when": [
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
]
},
"attendance": {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+407 -99
View File
@@ -8704,15 +8704,13 @@
"internal/cli/schema_hints/metadata/aitable.json",
"internal/cli/schema_hints/selection/aitable.json",
"internal/cli/schema_mcp_metadata.json#tools.aitable.base_search",
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
"skills/mono/references/products/aitable.md"
],
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
],
"canonical_path": "aitable.base_search",
"cli_name": "search",
@@ -8733,14 +8731,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。"
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。"
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。"
},
"availability": {
"candidates": [
@@ -8772,7 +8770,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
]
}
],
@@ -8781,7 +8779,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
]
},
"canonical_path": {
@@ -9011,7 +9009,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
]
}
],
@@ -9020,7 +9018,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
]
}
},
@@ -9239,7 +9237,7 @@
"source": "reviewed_command_registry",
"title": "搜索 AI 表格",
"use_when": [
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
]
},
"aitable.base_update": {
@@ -23547,15 +23545,13 @@
"internal/cli/schema_hints/selection/aitable.json",
"internal/cli/schema_mcp_metadata.json#tools.aitable.field_get",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-field.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-field.md"
"skills/mono/references/products/aitable/aitable-field.md"
],
"agent_summary": "底层获取字段完整类型与 config。",
"agent_summary": "获取字段完整配置。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
],
"canonical_path": "aitable.field_get",
"cli_name": "get",
@@ -23576,14 +23572,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "底层获取字段完整类型与 config。"
"value": "获取字段完整配置。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "底层获取字段完整类型与 config。"
"value": "获取字段完整配置。"
},
"availability": {
"candidates": [
@@ -23615,7 +23611,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
]
}
],
@@ -23624,7 +23620,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
]
},
"canonical_path": {
@@ -23852,7 +23848,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
"需要字段类型/config(选项、公式等)详情时"
]
}
],
@@ -23861,7 +23857,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
"需要字段类型/config(选项、公式等)详情时"
]
}
},
@@ -24205,7 +24201,7 @@
"source": "reviewed_command_registry",
"title": "获取字段详情",
"use_when": [
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
"需要字段类型/config(选项、公式等)详情时"
]
},
"aitable.field_list": {
@@ -38123,18 +38119,16 @@
"internal/cli/schema_mcp_metadata.json#tools.aitable.query_records",
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-record-query.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md"
"skills/mono/references/products/aitable/aitable-record-query.md"
],
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"agent_summary_source": "dws-agent-selection/aitable",
"aliases": [
"aitable record list"
],
"availability": "available",
"avoid_when": [
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"canonical_path": "aitable.query_records",
"cli_name": "query",
@@ -38155,14 +38149,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。"
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。"
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。"
},
"availability": {
"candidates": [
@@ -38194,7 +38188,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
]
}
],
@@ -38203,7 +38197,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
]
},
"canonical_path": {
@@ -38453,7 +38447,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
"查看、筛选、全文搜索或遍历记录时的主入口"
]
}
],
@@ -38462,7 +38456,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
"查看、筛选、全文搜索或遍历记录时的主入口"
]
}
},
@@ -39705,7 +39699,7 @@
"source": "reviewed_command_registry",
"title": "获取行记录",
"use_when": [
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
"查看、筛选、全文搜索或遍历记录时的主入口"
]
},
"aitable.record_batch_update": {
@@ -40403,15 +40397,13 @@
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-attachment.md",
"skills/mono/references/products/aitable/aitable-record-create.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md"
"skills/mono/references/products/aitable/aitable-record-create.md"
],
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
],
"canonical_path": "aitable.record_create",
"cli_name": "create",
@@ -40432,14 +40424,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。"
"value": "新增记录(cells 的 key 必须是 fieldId)。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。"
"value": "新增记录(cells 的 key 必须是 fieldId)。"
},
"availability": {
"candidates": [
@@ -40471,7 +40463,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
]
}
],
@@ -40480,7 +40472,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
]
},
"canonical_path": {
@@ -40708,7 +40700,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
"需要插入新行数据时"
]
}
],
@@ -40717,7 +40709,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
"需要插入新行数据时"
]
}
},
@@ -41164,7 +41156,7 @@
"source": "reviewed_command_registry",
"title": "新增记录",
"use_when": [
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
"需要插入新行数据时"
]
},
"aitable.record_delete": {
@@ -46557,15 +46549,13 @@
"internal/cli/schema_mcp_metadata.json#tools.aitable.record_update",
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-attachment.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md"
"skills/mono/references/products/aitable/aitable-attachment.md"
],
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"canonical_path": "aitable.record_update",
"cli_name": "update",
@@ -46586,14 +46576,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。"
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。"
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。"
},
"availability": {
"candidates": [
@@ -46625,7 +46615,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
]
}
],
@@ -46634,7 +46624,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
]
},
"canonical_path": {
@@ -46862,7 +46852,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
"需要修改已有记录若干字段时"
]
}
],
@@ -46871,7 +46861,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
"需要修改已有记录若干字段时"
]
}
},
@@ -47318,7 +47308,7 @@
"source": "reviewed_command_registry",
"title": "更新记录",
"use_when": [
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
"需要修改已有记录若干字段时"
]
},
"aitable.record_upsert": {
@@ -53457,7 +53447,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"canonical_path": "aitable.shortcut_base_list",
"cli_name": "+base-list",
@@ -53512,7 +53502,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
}
],
@@ -53521,7 +53511,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
},
"canonical_path": {
@@ -53953,7 +53943,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"canonical_path": "aitable.shortcut_base_search",
"cli_name": "+base-search",
@@ -54007,7 +53997,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
}
],
@@ -54016,7 +54006,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
},
"canonical_path": {
@@ -56206,7 +56196,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"canonical_path": "aitable.shortcut_field_get",
"cli_name": "+field-get",
@@ -56260,7 +56250,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
}
],
@@ -56269,7 +56259,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
},
"canonical_path": {
@@ -60326,7 +60316,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"canonical_path": "aitable.shortcut_record_query",
"cli_name": "+record-query",
@@ -60380,7 +60370,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
}
],
@@ -60389,7 +60379,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
},
"canonical_path": {
@@ -63495,7 +63485,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"canonical_path": "aitable.shortcut_resolve_base",
"cli_name": "+resolve-base",
@@ -63549,7 +63539,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
}
],
@@ -63558,7 +63548,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
},
"canonical_path": {
@@ -63768,7 +63758,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
]
}
],
@@ -63777,7 +63767,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
]
}
},
@@ -63894,7 +63884,7 @@
"source": "reviewed_command_registry",
"title": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)",
"use_when": [
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
]
},
"aitable.shortcut_resolve_table": {
@@ -63910,7 +63900,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"canonical_path": "aitable.shortcut_resolve_table",
"cli_name": "+resolve-table",
@@ -63964,7 +63954,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
}
],
@@ -63973,7 +63963,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
},
"canonical_path": {
@@ -64183,7 +64173,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
]
}
],
@@ -64192,7 +64182,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
]
}
},
@@ -64405,7 +64395,7 @@
"source": "reviewed_command_registry",
"title": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)",
"use_when": [
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
]
},
"aitable.shortcut_role_list": {
@@ -65666,7 +65656,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"canonical_path": "aitable.shortcut_table_get",
"cli_name": "+table-get",
@@ -65721,7 +65711,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
}
],
@@ -65730,7 +65720,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
]
},
"canonical_path": {
@@ -70532,15 +70522,13 @@
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-advperm.md",
"skills/mono/references/products/aitable/aitable-primary-doc.md",
"skills/mono/references/products/aitable/aitable-record-create.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
"skills/mono/references/products/aitable/aitable-record-create.md"
],
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
"agent_summary": "获取数据表结构(字段+视图目录)。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
],
"canonical_path": "aitable.table_get",
"cli_name": "get",
@@ -70561,14 +70549,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "底层获取数据表结构、精简字段目录与视图目录。"
"value": "获取数据表结构(字段+视图目录)。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "底层获取数据表结构、精简字段目录与视图目录。"
"value": "获取数据表结构(字段+视图目录)。"
},
"availability": {
"candidates": [
@@ -70600,7 +70588,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
]
}
],
@@ -70609,7 +70597,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
]
},
"canonical_path": {
@@ -70837,7 +70825,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
]
}
],
@@ -70846,7 +70834,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
]
}
},
@@ -71093,7 +71081,7 @@
"source": "reviewed_command_registry",
"title": "获取数据表",
"use_when": [
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
]
},
"aitable.table_list": {
@@ -97789,6 +97777,326 @@
"用户明确要求停止某自动化工作流时"
]
},
"aitable.workflow_edit_example": {
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
"cobra-help:dws aitable workflow edit-example --help",
"internal/cli/schema_command_registry.json#aitable.workflow_edit_example",
"internal/cli/schema_hints/metadata/aitable.json",
"internal/cli/schema_hints/selection/aitable.json",
"mcp-contract:aitable/edit_workflow_example",
"skills/mono/references/products/aitable/aitable-workflow.md"
],
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"canonical_path": "aitable.workflow_edit_example",
"cli_name": "edit-example",
"cli_path": "aitable workflow edit-example",
"confirmation": "not_required",
"description": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。",
"display": "AI 表格操作",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws aitable workflow edit-example"
],
"field_provenance": {
"agent_summary": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。"
},
"availability": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "available"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "available"
},
"avoid_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
]
},
"canonical_path": {
"candidates": [
{
"precedence": "command_registry",
"selected": true,
"source": "reviewed_command_registry",
"source_ref": "aitable workflow edit-example",
"value": "aitable.workflow_edit_example"
}
],
"precedence": "command_registry",
"resolution": "registry_identity",
"source": "reviewed_command_registry",
"source_ref": "aitable workflow edit-example",
"value": "aitable.workflow_edit_example"
},
"confirmation": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "not_required"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "not_required"
},
"description": {
"candidates": [
{
"precedence": "cobra_help",
"selected": true,
"source": "cobra_help",
"value": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。"
}
],
"precedence": "cobra_help",
"resolution": "highest_precedence",
"source": "cobra_help",
"value": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。"
},
"effect": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "read"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "read"
},
"examples": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"dws aitable workflow edit-example"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"dws aitable workflow edit-example"
]
},
"idempotency": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "idempotent"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "idempotent"
},
"interface_mode": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "composite"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "composite"
},
"interface_reason": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command."
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command."
},
"interface_ref": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": null
}
],
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": null
},
"reviewed": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": true
},
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": false,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": true
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": true
},
"risk": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "low"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "low"
},
"title": {
"candidates": [
{
"precedence": "cobra_help",
"selected": true,
"source": "cobra_help",
"value": "获取工作流编辑文档与示例"
}
],
"precedence": "cobra_help",
"resolution": "highest_precedence",
"source": "cobra_help",
"value": "获取工作流编辑文档与示例"
},
"use_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
]
}
},
"group": "workflow",
"has_parameters": false,
"idempotency": "idempotent",
"interface_mode": "composite",
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"is_alias": false,
"name": "workflow_edit_example",
"parameter_count": 0,
"parameters": {},
"path": "aitable.workflow_edit_example",
"primary_cli_path": "aitable workflow edit-example",
"product_id": "aitable",
"reviewed": true,
"risk": "low",
"source": "reviewed_command_registry",
"title": "获取工作流编辑文档与示例",
"use_when": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
]
},
"aitable.workflow_enable": {
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+588 -23
View File
@@ -5312,8 +5312,99 @@
"is_alias": false,
"metadata_source": "embedded-mcp-metadata",
"name": "download_file",
"parameter_count": 3,
"parameter_count": 6,
"parameters": {
"no-resume": {
"description": "关闭断点续传 (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "关闭断点续传 (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "关闭断点续传 (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "CLI-only transfer-layer flag controlling download resume behaviour; not an MCP interface parameter.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "noResume"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "CLI-only transfer-layer flag controlling download resume behaviour; not an MCP interface parameter.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": false
}
],
"precedence": "default",
"resolution": "fallback",
"source": "default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "boolean"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "boolean"
}
},
"required": false,
"type": "boolean"
},
"node": {
"description": "文件 ID (dentryUuid) (必填)",
"field_provenance": {
@@ -5520,6 +5611,202 @@
"required": true,
"type": "string"
},
"parallel": {
"default": "4",
"description": "分片下载并发数,范围 1-8 (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "分片下载并发数,范围 1-8 (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "分片下载并发数,范围 1-8 (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "CLI-only transfer-layer flag controlling parallel chunk downloads; not an MCP interface parameter.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "parallel"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "CLI-only transfer-layer flag controlling parallel chunk downloads; not an MCP interface parameter.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "integer"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "integer"
}
},
"required": false,
"type": "integer"
},
"part-size": {
"default": "16MB",
"description": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "CLI-only transfer-layer flag controlling download chunk size; not an MCP interface parameter.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "partSize"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "CLI-only transfer-layer flag controlling download chunk size; not an MCP interface parameter.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "string"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "string"
}
},
"required": false,
"type": "string"
},
"space-id": {
"description": "文件所属空间 ID (可选)",
"field_provenance": {
@@ -5937,8 +6224,99 @@
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"is_alias": false,
"name": "download_file_version",
"parameter_count": 3,
"parameter_count": 6,
"parameters": {
"no-resume": {
"description": "关闭断点续传 (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "关闭断点续传 (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "关闭断点续传 (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --no-resume is a CLI wrapper input and does not publish a direct interface property.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "noResume"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --no-resume is a CLI wrapper input and does not publish a direct interface property.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": false
}
],
"precedence": "default",
"resolution": "fallback",
"source": "default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "boolean"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "boolean"
}
},
"required": false,
"type": "boolean"
},
"node": {
"description": "文件 ID (dentryUuid) 或 URL (必填)",
"field_provenance": {
@@ -6133,6 +6511,202 @@
"required": true,
"type": "string"
},
"parallel": {
"default": "4",
"description": "分片下载并发数,范围 1-8 (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "分片下载并发数,范围 1-8 (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "分片下载并发数,范围 1-8 (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --parallel is a CLI wrapper input and does not publish a direct interface property.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "parallel"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --parallel is a CLI wrapper input and does not publish a direct interface property.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "integer"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "integer"
}
},
"required": false,
"type": "integer"
},
"part-size": {
"default": "16MB",
"description": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --part-size is a CLI wrapper input and does not publish a direct interface property.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "partSize"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --part-size is a CLI wrapper input and does not publish a direct interface property.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "string"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "string"
}
},
"required": false,
"type": "string"
},
"version": {
"description": "历史版本号 (必填,正整数,从 drive list --versions 获取)",
"field_provenance": {
@@ -27242,10 +27816,8 @@
"agent_summary_source": "dws-agent-selection/drive",
"availability": "available",
"avoid_when": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -27269,7 +27841,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件"
@@ -27277,7 +27849,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件"
},
@@ -27301,14 +27873,12 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
]
@@ -27316,13 +27886,11 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
]
@@ -27435,7 +28003,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
@@ -27446,7 +28014,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"dws drive upload --file ./report.pdf --format json",
@@ -27534,7 +28102,7 @@
},
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"selected": false,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": true
@@ -27586,25 +28154,23 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
@@ -28327,8 +28893,7 @@
"title": "上传本地文件到钉盘或文档空间",
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
@@ -79,6 +79,36 @@
"calendar acl add",
"calendar acl delete",
"calendar book update",
"chat category add-conv",
"chat category create",
"chat category delete",
"chat category remove-conv",
"chat category rename",
"chat chmod",
"chat clear-all-red-point",
"chat clear-messages",
"chat clear-red-point",
"chat data-auth cross-org",
"chat group audit-join-validation",
"chat group list-all",
"chat group list-join-validations",
"chat group members list-by-ids",
"chat group notice create",
"chat group notice edit",
"chat group notice get",
"chat group notice list",
"chat group share-invite",
"chat group update-alias",
"chat hide",
"chat list-all-conversations",
"chat mark-read",
"chat mark-unread",
"chat message list-emotion-replies",
"chat message set-top-msg",
"chat message unset-top-msg",
"chat mute-at-all",
"chat mute-red-envelope",
"chat text translate",
"contact label get",
"contact label list",
"contact label list-members",
@@ -452,6 +452,10 @@
"canonical_path": "aitable.workflow_disable",
"cli_path": "aitable workflow disable"
},
{
"canonical_path": "aitable.workflow_edit_example",
"cli_path": "aitable workflow edit-example"
},
{
"canonical_path": "aitable.workflow_enable",
"cli_path": "aitable workflow enable"
@@ -1,378 +1,6 @@
{
"id": "chat",
"tools": [
{
"canonical_path": "chat.add_conv_to_categories",
"cli_path": "chat category add-conv"
},
{
"canonical_path": "chat.add_custom_group_role",
"cli_path": "chat group-role add"
},
{
"canonical_path": "chat.add_emoji_reaction",
"cli_path": "chat message add-emoji"
},
{
"canonical_path": "chat.add_group_member",
"cli_path": "chat group members add"
},
{
"canonical_path": "chat.add_message_favorite",
"cli_path": "chat message add-favorite"
},
{
"canonical_path": "chat.add_robot_to_group",
"cli_path": "chat group members add-bot"
},
{
"canonical_path": "chat.add_text_emotion",
"cli_path": "chat message add-text-emotion"
},
{
"canonical_path": "chat.audit_join_group",
"cli_path": "chat group audit-join-validation"
},
{
"canonical_path": "chat.batch_query_group_chat_settings",
"cli_path": "chat group user-settings query"
},
{
"canonical_path": "chat.batch_update_group_chat_settings",
"cli_path": "chat group user-settings set"
},
{
"canonical_path": "chat.clear_all_red_point",
"cli_path": "chat clear-all-red-point"
},
{
"canonical_path": "chat.clear_conversation_messages",
"cli_path": "chat clear-messages"
},
{
"canonical_path": "chat.clear_conversation_red_point",
"cli_path": "chat clear-red-point"
},
{
"canonical_path": "chat.combine_forward_messages",
"cli_path": "chat message combine-forward"
},
{
"canonical_path": "chat.create_and_send_card",
"cli_path": "chat message send-card"
},
{
"canonical_path": "chat.create_conv_category",
"cli_path": "chat category create"
},
{
"canonical_path": "chat.create_group_conversation",
"cli_path": "chat group create"
},
{
"canonical_path": "chat.create_group_notice",
"cli_path": "chat group notice create"
},
{
"canonical_path": "chat.create_smart_conv_category",
"cli_path": "chat category create-smart"
},
{
"canonical_path": "chat.create_text_emotion",
"cli_path": "chat message create-text-emotion"
},
{
"canonical_path": "chat.delete_conv_category",
"cli_path": "chat category delete"
},
{
"canonical_path": "chat.dismiss_group",
"cli_path": "chat group dismiss"
},
{
"canonical_path": "chat.download_media",
"cli_path": "chat message download-media"
},
{
"canonical_path": "chat.edit_group_notice",
"cli_path": "chat group notice edit"
},
{
"canonical_path": "chat.edit_message",
"cli_path": "chat message edit"
},
{
"canonical_path": "chat.forward_message",
"cli_path": "chat message forward"
},
{
"canonical_path": "chat.forward_topic",
"cli_path": "chat message forward-topic"
},
{
"canonical_path": "chat.get_conv_categories_info",
"cli_path": "chat category batch-info"
},
{
"canonical_path": "chat.get_conv_info_by_group_id",
"cli_path": "chat group get-by-group-id"
},
{
"canonical_path": "chat.get_conversation_info",
"cli_path": "chat conversation-info"
},
{
"canonical_path": "chat.get_group_invite_url",
"cli_path": "chat group invite-url"
},
{
"canonical_path": "chat.get_group_mute_config",
"cli_path": "chat group get-mute-config"
},
{
"canonical_path": "chat.get_group_notice",
"cli_path": "chat group notice get"
},
{
"canonical_path": "chat.grant_cross_org_data_access",
"cli_path": "chat data-auth cross-org"
},
{
"canonical_path": "chat.grant_permission",
"cli_path": "chat chmod"
},
{
"canonical_path": "chat.hide_conversation",
"cli_path": "chat hide"
},
{
"canonical_path": "chat.list_all_conversations",
"cli_path": "chat list-all-conversations"
},
{
"canonical_path": "chat.list_apply_join_group_records",
"cli_path": "chat group list-join-validations"
},
{
"canonical_path": "chat.list_conv_categories_by_conv",
"cli_path": "chat category list-by-conv"
},
{
"canonical_path": "chat.list_conversation_message_v2",
"cli_path": "chat message list"
},
{
"canonical_path": "chat.list_conversations_by_category",
"cli_path": "chat category list-conversations"
},
{
"canonical_path": "chat.list_custom_group_roles",
"cli_path": "chat group-role list"
},
{
"canonical_path": "chat.list_group_bots",
"cli_path": "chat group bots"
},
{
"canonical_path": "chat.list_group_member_by_ids",
"cli_path": "chat group members list-by-ids"
},
{
"canonical_path": "chat.list_group_notices",
"cli_path": "chat group notice list"
},
{
"canonical_path": "chat.list_individual_chat_message",
"cli_path": "chat message list-direct"
},
{
"canonical_path": "chat.list_message_emotion_replies",
"cli_path": "chat message list-emotion-replies"
},
{
"canonical_path": "chat.list_message_favorites",
"cli_path": "chat message list-favorites"
},
{
"canonical_path": "chat.list_messages_by_ids",
"cli_path": "chat message list-by-ids"
},
{
"canonical_path": "chat.list_my_groups_pagination",
"cli_path": "chat group list-all"
},
{
"canonical_path": "chat.list_owned_or_admin_groups",
"cli_path": "chat group list-my-groups"
},
{
"canonical_path": "chat.list_pin_messages",
"cli_path": "chat message list-pin-msg"
},
{
"canonical_path": "chat.list_special_focus_messages",
"cli_path": "chat message list-focused"
},
{
"canonical_path": "chat.list_top_conversations",
"cli_path": "chat list-top-conversations"
},
{
"canonical_path": "chat.list_topic_replies",
"cli_path": "chat message list-topic-replies"
},
{
"canonical_path": "chat.list_user_define_conv_categories",
"cli_path": "chat category list"
},
{
"canonical_path": "chat.mark_conversation_unread",
"cli_path": "chat mark-unread"
},
{
"canonical_path": "chat.mark_message_read",
"cli_path": "chat mark-read"
},
{
"canonical_path": "chat.query_custom_user_roles",
"cli_path": "chat group-role query-user"
},
{
"canonical_path": "chat.query_message_send_status",
"cli_path": "chat message query-send-status"
},
{
"canonical_path": "chat.query_msg_read_status",
"cli_path": "chat message read-status"
},
{
"canonical_path": "chat.quit_group",
"cli_path": "chat group quit"
},
{
"canonical_path": "chat.recall_message",
"cli_path": "chat message recall"
},
{
"canonical_path": "chat.recall_robot_message",
"cli_path": "chat message recall-by-bot"
},
{
"canonical_path": "chat.remove_conv_from_categories",
"cli_path": "chat category remove-conv"
},
{
"canonical_path": "chat.remove_custom_group_role",
"cli_path": "chat group-role remove"
},
{
"canonical_path": "chat.remove_custom_user_roles",
"cli_path": "chat group-role remove-user"
},
{
"canonical_path": "chat.remove_emoji_reaction",
"cli_path": "chat message remove-emoji"
},
{
"canonical_path": "chat.remove_group_member",
"cli_path": "chat group members remove"
},
{
"canonical_path": "chat.remove_message_favorite",
"cli_path": "chat message remove-favorite"
},
{
"canonical_path": "chat.remove_robot_in_group",
"cli_path": "chat group members remove-bot"
},
{
"canonical_path": "chat.remove_text_emotion",
"cli_path": "chat message remove-text-emotion"
},
{
"canonical_path": "chat.rename_conv_category",
"cli_path": "chat category rename"
},
{
"canonical_path": "chat.reply_personal_message",
"cli_path": "chat message reply"
},
{
"canonical_path": "chat.search_at_me_message",
"cli_path": "chat message list-mentions"
},
{
"canonical_path": "chat.search_bots",
"cli_path": "chat bot find"
},
{
"canonical_path": "chat.search_common_groups",
"cli_path": "chat search-common"
},
{
"canonical_path": "chat.search_groups",
"cli_path": "chat search"
},
{
"canonical_path": "chat.search_messages",
"cli_path": "chat message search-advanced"
},
{
"canonical_path": "chat.search_messages_by_keyword",
"cli_path": "chat message search"
},
{
"canonical_path": "chat.search_messages_by_sender",
"cli_path": "chat message list-by-sender"
},
{
"canonical_path": "chat.search_messages_by_time_range",
"cli_path": "chat message list-all"
},
{
"canonical_path": "chat.search_my_robots",
"cli_path": "chat bot search"
},
{
"canonical_path": "chat.send_message_by_custom_robot",
"cli_path": "chat message send-by-webhook"
},
{
"canonical_path": "chat.send_personal_message",
"cli_path": "chat message send"
},
{
"canonical_path": "chat.send_robot_message",
"cli_path": "chat message send-by-bot"
},
{
"canonical_path": "chat.set_custom_user_roles",
"cli_path": "chat group-role set-user"
},
{
"canonical_path": "chat.set_group_member_mute_list",
"cli_path": "chat group-mute-member"
},
{
"canonical_path": "chat.set_group_mute",
"cli_path": "chat group-mute"
},
{
"canonical_path": "chat.set_pin_message",
"cli_path": "chat message set-pin-msg"
},
{
"canonical_path": "chat.set_top_conversation",
"cli_path": "chat set-top"
},
{
"canonical_path": "chat.set_top_message",
"cli_path": "chat message set-top-msg"
},
{
"canonical_path": "chat.share_group_invite_url",
"cli_path": "chat group share-invite"
},
{
"canonical_path": "chat.shortcut_at_me",
"cli_path": "chat +at-me"
@@ -429,10 +57,6 @@
"canonical_path": "chat.shortcut_chat_list_mine",
"cli_path": "chat +chat-list-mine"
},
{
"canonical_path": "chat.shortcut_chat_messages",
"cli_path": "chat +chat-messages"
},
{
"canonical_path": "chat.shortcut_chat_mute",
"cli_path": "chat +chat-mute"
@@ -525,18 +149,10 @@
"canonical_path": "chat.shortcut_messages_read_status",
"cli_path": "chat +messages-read-status"
},
{
"canonical_path": "chat.shortcut_messages_send",
"cli_path": "chat +messages-send"
},
{
"canonical_path": "chat.shortcut_messages_send_by_webhook",
"cli_path": "chat +messages-send-by-webhook"
},
{
"canonical_path": "chat.shortcut_messages_send_card",
"cli_path": "chat +messages-send-card"
},
{
"canonical_path": "chat.shortcut_messages_update_card",
"cli_path": "chat +messages-update-card"
@@ -545,62 +161,114 @@
"canonical_path": "chat.shortcut_my_groups",
"cli_path": "chat +my-groups"
},
{
"canonical_path": "chat.shortcut_search_msg",
"cli_path": "chat +search-msg"
},
{
"canonical_path": "chat.shortcut_send_to_group",
"cli_path": "chat +send-to-group"
},
{
"canonical_path": "chat.shortcut_thread_replies",
"cli_path": "chat +thread-replies"
},
{
"canonical_path": "chat.shortcut_unread_chats",
"cli_path": "chat +unread-chats"
},
{
"canonical_path": "chat.transfer_group_owner",
"cli_path": "chat group transfer-owner"
"canonical_path": "chat.search_bots",
"cli_path": "chat bot find"
},
{
"canonical_path": "chat.translate",
"cli_path": "chat text translate"
"canonical_path": "chat.search_my_robots",
"cli_path": "chat bot search"
},
{
"canonical_path": "chat.unread_message_conversation_list",
"cli_path": "chat message list-unread-conversations"
"canonical_path": "chat.get_conv_categories_info",
"cli_path": "chat category batch-info"
},
{
"canonical_path": "chat.unset_pin_message",
"cli_path": "chat message unset-pin-msg"
"canonical_path": "chat.create_smart_conv_category",
"cli_path": "chat category create-smart"
},
{
"canonical_path": "chat.unset_top_message",
"cli_path": "chat message unset-top-msg"
"canonical_path": "chat.list_user_define_conv_categories",
"cli_path": "chat category list"
},
{
"canonical_path": "chat.update_at_all_notification_off",
"cli_path": "chat mute-at-all"
"canonical_path": "chat.list_conv_categories_by_conv",
"cli_path": "chat category list-by-conv"
},
{
"canonical_path": "chat.list_conversations_by_category",
"cli_path": "chat category list-conversations"
},
{
"canonical_path": "chat.get_conversation_info",
"cli_path": "chat conversation-info"
},
{
"canonical_path": "chat.list_group_bots",
"cli_path": "chat group bots"
},
{
"canonical_path": "chat.create_group_conversation",
"cli_path": "chat group create"
},
{
"canonical_path": "chat.dismiss_group",
"cli_path": "chat group dismiss"
},
{
"canonical_path": "chat.get_conv_info_by_group_id",
"cli_path": "chat group get-by-group-id"
},
{
"canonical_path": "chat.get_group_mute_config",
"cli_path": "chat group get-mute-config"
},
{
"canonical_path": "chat.get_group_invite_url",
"cli_path": "chat group invite-url"
},
{
"canonical_path": "chat.list_owned_or_admin_groups",
"cli_path": "chat group list-my-groups"
},
{
"canonical_path": "chat.add_group_member",
"cli_path": "chat group members add"
},
{
"canonical_path": "chat.add_robot_to_group",
"cli_path": "chat group members add-bot"
},
{
"canonical_path": "chat.remove_group_member",
"cli_path": "chat group members remove"
},
{
"canonical_path": "chat.remove_robot_in_group",
"cli_path": "chat group members remove-bot"
},
{
"canonical_path": "chat.quit_group",
"cli_path": "chat group quit"
},
{
"canonical_path": "chat.update_group_name",
"cli_path": "chat group rename"
},
{
"canonical_path": "chat.update_conv_member_roles",
"cli_path": "chat group set-admin"
},
{
"canonical_path": "chat.update_custom_group_role",
"cli_path": "chat group-role update"
"canonical_path": "chat.update_show_history_msg_option",
"cli_path": "chat group set-history"
},
{
"canonical_path": "chat.transfer_group_owner",
"cli_path": "chat group transfer-owner"
},
{
"canonical_path": "chat.update_group_icon",
"cli_path": "chat group update-icon"
},
{
"canonical_path": "chat.update_group_name",
"cli_path": "chat group rename"
},
{
"canonical_path": "chat.update_group_nick",
"cli_path": "chat group update-nick"
@@ -610,16 +278,200 @@
"cli_path": "chat group update-settings"
},
{
"canonical_path": "chat.update_notification_off",
"cli_path": "chat mute"
"canonical_path": "chat.upgrade_group_to_external",
"cli_path": "chat group upgrade-to-external"
},
{
"canonical_path": "chat.update_red_env_notification_off",
"cli_path": "chat mute-red-envelope"
"canonical_path": "chat.batch_query_group_chat_settings",
"cli_path": "chat group user-settings query"
},
{
"canonical_path": "chat.update_show_history_msg_option",
"cli_path": "chat group set-history"
"canonical_path": "chat.batch_update_group_chat_settings",
"cli_path": "chat group user-settings set"
},
{
"canonical_path": "chat.set_group_mute",
"cli_path": "chat group-mute"
},
{
"canonical_path": "chat.set_group_member_mute_list",
"cli_path": "chat group-mute-member"
},
{
"canonical_path": "chat.add_custom_group_role",
"cli_path": "chat group-role add"
},
{
"canonical_path": "chat.list_custom_group_roles",
"cli_path": "chat group-role list"
},
{
"canonical_path": "chat.query_custom_user_roles",
"cli_path": "chat group-role query-user"
},
{
"canonical_path": "chat.remove_custom_group_role",
"cli_path": "chat group-role remove"
},
{
"canonical_path": "chat.remove_custom_user_roles",
"cli_path": "chat group-role remove-user"
},
{
"canonical_path": "chat.set_custom_user_roles",
"cli_path": "chat group-role set-user"
},
{
"canonical_path": "chat.update_custom_group_role",
"cli_path": "chat group-role update"
},
{
"canonical_path": "chat.list_top_conversations",
"cli_path": "chat list-top-conversations"
},
{
"canonical_path": "chat.add_emoji_reaction",
"cli_path": "chat message add-emoji"
},
{
"canonical_path": "chat.add_message_favorite",
"cli_path": "chat message add-favorite"
},
{
"canonical_path": "chat.add_text_emotion",
"cli_path": "chat message add-text-emotion"
},
{
"canonical_path": "chat.combine_forward_messages",
"cli_path": "chat message combine-forward"
},
{
"canonical_path": "chat.create_text_emotion",
"cli_path": "chat message create-text-emotion"
},
{
"canonical_path": "chat.download_media",
"cli_path": "chat message download-media"
},
{
"canonical_path": "chat.edit_message",
"cli_path": "chat message edit"
},
{
"canonical_path": "chat.forward_message",
"cli_path": "chat message forward"
},
{
"canonical_path": "chat.forward_topic",
"cli_path": "chat message forward-topic"
},
{
"canonical_path": "chat.list_conversation_message_v2",
"cli_path": "chat message list"
},
{
"canonical_path": "chat.search_messages_by_time_range",
"cli_path": "chat message list-all"
},
{
"canonical_path": "chat.list_messages_by_ids",
"cli_path": "chat message list-by-ids"
},
{
"canonical_path": "chat.search_messages_by_sender",
"cli_path": "chat message list-by-sender"
},
{
"canonical_path": "chat.list_individual_chat_message",
"cli_path": "chat message list-direct"
},
{
"canonical_path": "chat.list_message_favorites",
"cli_path": "chat message list-favorites"
},
{
"canonical_path": "chat.list_special_focus_messages",
"cli_path": "chat message list-focused"
},
{
"canonical_path": "chat.search_at_me_message",
"cli_path": "chat message list-mentions"
},
{
"canonical_path": "chat.list_pin_messages",
"cli_path": "chat message list-pin-msg"
},
{
"canonical_path": "chat.list_topic_replies",
"cli_path": "chat message list-topic-replies"
},
{
"canonical_path": "chat.unread_message_conversation_list",
"cli_path": "chat message list-unread-conversations"
},
{
"canonical_path": "chat.query_message_send_status",
"cli_path": "chat message query-send-status"
},
{
"canonical_path": "chat.query_msg_read_status",
"cli_path": "chat message read-status"
},
{
"canonical_path": "chat.recall_message",
"cli_path": "chat message recall"
},
{
"canonical_path": "chat.recall_robot_message",
"cli_path": "chat message recall-by-bot"
},
{
"canonical_path": "chat.remove_emoji_reaction",
"cli_path": "chat message remove-emoji"
},
{
"canonical_path": "chat.remove_message_favorite",
"cli_path": "chat message remove-favorite"
},
{
"canonical_path": "chat.remove_text_emotion",
"cli_path": "chat message remove-text-emotion"
},
{
"canonical_path": "chat.reply_personal_message",
"cli_path": "chat message reply"
},
{
"canonical_path": "chat.search_messages_by_keyword",
"cli_path": "chat message search"
},
{
"canonical_path": "chat.search_messages",
"cli_path": "chat message search-advanced"
},
{
"canonical_path": "chat.send_personal_message",
"cli_path": "chat message send"
},
{
"canonical_path": "chat.send_robot_message",
"cli_path": "chat message send-by-bot"
},
{
"canonical_path": "chat.send_message_by_custom_robot",
"cli_path": "chat message send-by-webhook"
},
{
"canonical_path": "chat.create_and_send_card",
"cli_path": "chat message send-card"
},
{
"canonical_path": "chat.set_pin_message",
"cli_path": "chat message set-pin-msg"
},
{
"canonical_path": "chat.unset_pin_message",
"cli_path": "chat message unset-pin-msg"
},
{
"canonical_path": "chat.update_streaming_card",
@@ -630,12 +482,40 @@
"cli_path": "chat message update-text-emotion"
},
{
"canonical_path": "chat.update_user_group_alias",
"cli_path": "chat group update-alias"
"canonical_path": "chat.update_notification_off",
"cli_path": "chat mute"
},
{
"canonical_path": "chat.upgrade_group_to_external",
"cli_path": "chat group upgrade-to-external"
"canonical_path": "chat.search_groups",
"cli_path": "chat search"
},
{
"canonical_path": "chat.search_common_groups",
"cli_path": "chat search-common"
},
{
"canonical_path": "chat.set_top_conversation",
"cli_path": "chat set-top"
},
{
"canonical_path": "chat.shortcut_chat_messages",
"cli_path": "chat +chat-messages"
},
{
"canonical_path": "chat.shortcut_messages_send",
"cli_path": "chat +messages-send"
},
{
"canonical_path": "chat.shortcut_messages_send_card",
"cli_path": "chat +messages-send-card"
},
{
"canonical_path": "chat.shortcut_search_msg",
"cli_path": "chat +search-msg"
},
{
"canonical_path": "chat.shortcut_thread_replies",
"cli_path": "chat +thread-replies"
}
]
}
@@ -999,6 +999,19 @@
"reviewed": true,
"runtime_gate": "confirm_delete"
},
"aitable.workflow_edit_example": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"reviewed": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"cli_path": "aitable workflow edit-example",
"runtime_gate": "none"
},
"aitable.workflow_enable": {
"interface_ref": {
"product_id": "aitable-helper",
@@ -801,396 +801,6 @@
"cli_path": "chat group upgrade-to-external",
"runtime_gate": "confirm_dangerous"
},
"chat.add_conv_to_categories": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI owns validation and calls a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native category command with its real Cobra contract; runtime has no confirmation gate.",
"cli_path": "chat category add-conv",
"runtime_gate": "none"
},
"chat.create_conv_category": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI owns title validation and calls a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native category command and preserve the runtime 15-character title validation; runtime has no confirmation gate.",
"cli_path": "chat category create",
"runtime_gate": "none"
},
"chat.delete_conv_category": {
"effect": "destructive",
"risk": "high",
"confirmation": "user_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote category-delete helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native category deletion command with its explicit typed --yes confirmation gate.",
"cli_path": "chat category delete",
"runtime_gate": "typed_yes"
},
"chat.remove_conv_from_categories": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI owns validation and calls a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native category membership command with its real Cobra contract; runtime has no confirmation gate.",
"cli_path": "chat category remove-conv",
"runtime_gate": "none"
},
"chat.rename_conv_category": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI owns title validation and calls a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native category rename command and preserve the runtime 15-character title validation; runtime has no confirmation gate.",
"cli_path": "chat category rename",
"runtime_gate": "none"
},
"chat.grant_permission": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed permission adapter: the executable CLI normalizes grant scope and parameters before calling chat_permission_grant.",
"reviewed": true,
"review_reason": "Publish the documented chat permission grant surface without inventing a runtime confirmation gate.",
"cli_path": "chat chmod",
"runtime_gate": "none"
},
"chat.clear_all_red_point": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native red-point command; runtime has no confirmation gate.",
"cli_path": "chat clear-all-red-point",
"runtime_gate": "none"
},
"chat.clear_conversation_messages": {
"effect": "destructive",
"risk": "high",
"confirmation": "user_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote conversation-clear helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native clear-messages command with its explicit typed --yes confirmation gate.",
"cli_path": "chat clear-messages",
"runtime_gate": "typed_yes"
},
"chat.clear_conversation_red_point": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native red-point command; runtime has no confirmation gate.",
"cli_path": "chat clear-red-point",
"runtime_gate": "none"
},
"chat.grant_cross_org_data_access": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed cross-organization permission adapter: the executable CLI assembles scoped chat_permission_grant arguments.",
"reviewed": true,
"review_reason": "Publish the documented cross-organization data authorization command without inventing a runtime confirmation gate.",
"cli_path": "chat data-auth cross-org",
"runtime_gate": "none"
},
"chat.audit_join_group": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI validates supported audit statuses before calling a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented join-audit command and preserve the runtime AuditApprove/AuditDelete restriction.",
"cli_path": "chat group audit-join-validation",
"runtime_gate": "none"
},
"chat.list_my_groups_pagination": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a paginated remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native paginated group-list command.",
"cli_path": "chat group list-all",
"runtime_gate": "none"
},
"chat.list_apply_join_group_records": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a paginated remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native join-validation list command.",
"cli_path": "chat group list-join-validations",
"runtime_gate": "none"
},
"chat.list_group_member_by_ids": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote member lookup helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native batch group-member lookup command.",
"cli_path": "chat group members list-by-ids",
"runtime_gate": "none"
},
"chat.create_group_notice": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI assembles notice scheduling and notification arguments for a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native group notice creation command; runtime has no confirmation gate.",
"cli_path": "chat group notice create",
"runtime_gate": "none"
},
"chat.edit_group_notice": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI assembles notice update arguments for a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native group notice edit command; runtime has no confirmation gate.",
"cli_path": "chat group notice edit",
"runtime_gate": "none"
},
"chat.get_group_notice": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI validates notice IDs before calling a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native group notice detail command.",
"cli_path": "chat group notice get",
"runtime_gate": "none"
},
"chat.list_group_notices": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a paginated remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native group notice list command.",
"cli_path": "chat group notice list",
"runtime_gate": "none"
},
"chat.share_group_invite_url": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI enforces target/receiver exclusivity before calling a remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native invite-sharing command; runtime has no confirmation gate.",
"cli_path": "chat group share-invite",
"runtime_gate": "none"
},
"chat.update_user_group_alias": {
"effect": "write",
"risk": "low",
"confirmation": "not_required",
"idempotency": "unknown",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote personal-alias helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native personal group-alias command.",
"cli_path": "chat group update-alias",
"runtime_gate": "none"
},
"chat.hide_conversation": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote conversation visibility helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native hide-conversation command.",
"cli_path": "chat hide",
"runtime_gate": "none"
},
"chat.list_all_conversations": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a paginated remote helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native all-conversations list command.",
"cli_path": "chat list-all-conversations",
"runtime_gate": "none"
},
"chat.mark_message_read": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote read-state helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native mark-read command.",
"cli_path": "chat mark-read",
"runtime_gate": "none"
},
"chat.mark_conversation_unread": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote read-state helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native mark-unread command.",
"cli_path": "chat mark-unread",
"runtime_gate": "none"
},
"chat.list_message_emotion_replies": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote emotion-reply helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native batch emotion reply list command.",
"cli_path": "chat message list-emotion-replies",
"runtime_gate": "none"
},
"chat.set_top_message": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote message-top helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native set-top-message command.",
"cli_path": "chat message set-top-msg",
"runtime_gate": "none"
},
"chat.unset_top_message": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote message-top helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native unset-top-message command.",
"cli_path": "chat message unset-top-msg",
"runtime_gate": "none"
},
"chat.update_at_all_notification_off": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote notification preference helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native @all notification preference command.",
"cli_path": "chat mute-at-all",
"runtime_gate": "none"
},
"chat.update_red_env_notification_off": {
"effect": "write",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote notification preference helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native red-envelope notification preference command.",
"cli_path": "chat mute-red-envelope",
"runtime_gate": "none"
},
"chat.translate": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned IM adapter: the executable CLI calls a remote translation helper absent from the pinned MCP snapshot.",
"reviewed": true,
"review_reason": "Publish the documented native chat text translation command.",
"cli_path": "chat text translate",
"runtime_gate": "none"
},
"chat.shortcut_bot_search": {
"effect": "read",
"risk": "low",
@@ -7,7 +7,7 @@
"channel": "open-source"
},
"coverage": {
"source_tools": 875,
"source_tools": 846,
"matched_tools": 71
},
"tools": {
@@ -289,12 +289,12 @@
]
},
"aitable.base_search": {
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"use_when": [
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
],
"avoid_when": [
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
],
"examples": [
"dws aitable base search --query \"项目\""
@@ -304,8 +304,7 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable base search --help",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md",
"skills/mono/references/products/aitable.md",
"dws-schema-live:aitable.search_bases"
]
},
@@ -711,12 +710,12 @@
]
},
"aitable.field_get": {
"agent_summary": "底层获取字段完整类型与 config。",
"agent_summary": "获取字段完整配置。",
"use_when": [
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
"需要字段类型/config(选项、公式等)详情时"
],
"avoid_when": [
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
],
"examples": [
"dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID>"
@@ -726,8 +725,7 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable field get --help",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-field.md",
"skills/mono/references/products/aitable.md",
"dws-schema-live:aitable.get_fields"
]
},
@@ -1112,12 +1110,12 @@
]
},
"aitable.query_records": {
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"use_when": [
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
"查看、筛选、全文搜索或遍历记录时的主入口"
],
"avoid_when": [
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"examples": [
"dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID>"
@@ -1127,8 +1125,7 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable record query --help",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md",
"skills/mono/references/products/aitable.md",
"dws-schema-live:aitable.query_records"
]
},
@@ -1153,12 +1150,12 @@
]
},
"aitable.record_create": {
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
"use_when": [
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
"需要插入新行数据时"
],
"avoid_when": [
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
],
"examples": [
"dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> --records '[{\"cells\":{\"fldXXX\":\"值\"}}]'"
@@ -1168,8 +1165,7 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable record create --help",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md",
"skills/mono/references/products/aitable.md",
"dws-schema-live:aitable.create_records"
]
},
@@ -1314,12 +1310,12 @@
]
},
"aitable.record_update": {
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"use_when": [
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
"需要修改已有记录若干字段时"
],
"avoid_when": [
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"examples": [
"dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> --records '[{\"recordId\":\"recXXX\",\"cells\":{\"fldYYY\":\"新值\"}}]'"
@@ -1329,8 +1325,7 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable record update --help",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md",
"skills/mono/references/products/aitable.md",
"dws-schema-live:aitable.update_records"
]
},
@@ -1555,12 +1550,12 @@
]
},
"aitable.table_get": {
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
"agent_summary": "获取数据表结构(字段+视图目录)。",
"use_when": [
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
],
"avoid_when": [
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
],
"examples": [
"dws aitable table get --base-id BASE_ID"
@@ -1570,8 +1565,7 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable table get --help",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md",
"skills/mono/references/products/aitable.md",
"dws-schema-live:aitable.get_tables"
]
},
@@ -2256,6 +2250,26 @@
"dws-schema-live:none (helper/composite; Skill+Cobra)"
]
},
"aitable.workflow_edit_example": {
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"use_when": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
],
"avoid_when": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"examples": [
"dws aitable workflow edit-example"
],
"reviewed": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source_refs": [
"internal/cli/schema_command_registry.json#aitable.workflow_edit_example",
"cobra-help:dws aitable workflow edit-example --help",
"skills/mono/references/products/aitable/aitable-workflow.md",
"mcp-contract:aitable/edit_workflow_example"
]
},
"aitable.workflow_enable": {
"agent_summary": "启用工作流。",
"use_when": [
@@ -2341,7 +2355,7 @@
"当你不知道具体 baseId、想先浏览自己最近用过或可访问的 AI 表格清单以便定位目标时使用;支持游标分页,返回 Base 列表及其 baseId。"
],
"avoid_when": [
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws aitable +base-list",
@@ -2361,7 +2375,7 @@
"当你知道某个 AI 表格的名字或部分关键词、想直接定位到它并拿到 baseId 时使用;输入名称关键词,返回匹配的 Base 列表。"
],
"avoid_when": [
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws aitable +base-search --query \"项目管理\""
@@ -2399,7 +2413,7 @@
"当你已进入某个 Base、需要了解其中某些数据表有哪些字段(拿 fieldId)、有哪些视图(拿 viewId)以便读写数据时使用;批量返回表信息、字段目录和视图目录。"
],
"avoid_when": [
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws aitable +table-get --base-id BASE_ID",
@@ -2419,7 +2433,7 @@
"当你需要查看字段的完整类型配置(如单选选项、关联表设置、AI 配置)以便正确写入数据或改配置时使用;批量返回字段详情。"
],
"avoid_when": [
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws aitable +field-get --base-id B --table-id T"
@@ -2438,7 +2452,7 @@
"当你要读取表格里的行数据——按 recordId 精确取、按结构化条件筛选、按关键词全文搜索或分页遍历时使用;返回匹配记录及其单元格值。"
],
"avoid_when": [
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws aitable +record-query --base-id B --table-id T --query \"关键词\" --limit 50"
@@ -2855,10 +2869,10 @@
"aitable.shortcut_resolve_base": {
"agent_summary": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)",
"use_when": [
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
],
"avoid_when": [
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws aitable +resolve-base --name 项目管理"
@@ -2874,10 +2888,10 @@
"aitable.shortcut_resolve_table": {
"agent_summary": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)",
"use_when": [
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
],
"avoid_when": [
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws aitable +resolve-table --base B --name 任务"
@@ -2893,19 +2907,18 @@
},
"products": {
"aitable": {
"agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
"agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
"use_when": [
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
],
"avoid_when": [
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
],
"reviewed": true,
"review_reason": "人工结合多产品边界、根 Skill 的渐进加载顺序、真实 Cobra/Shortcut 路径与 MCP 语义审阅产品级选择边界。",
"review_reason": "人工结合实时 MCP/Skill/Cobra 审阅产品级选择边界。",
"source_refs": [
"internal/cli/schema_command_registry.json",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md",
"skills/mono/references/products/aitable.md",
"dws-schema-live:product-index"
]
}
+42 -640
View File
@@ -29,12 +29,12 @@
]
},
"chat.add_emoji_reaction": {
"agent_summary": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应",
"agent_summary": "给指定消息添加表情回应",
"use_when": [
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
"需要对已有消息添加一个 emoji reaction 时"
],
"avoid_when": [
"发送文本消息或文字表情时不要使用;不要把 openTaskId 当作 msg-id"
"发送文本消息或文字表情时不要使用"
],
"examples": [
"dws chat message add-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji \"赞\""
@@ -150,12 +150,12 @@
]
},
"chat.create_and_send_card": {
"agent_summary": "创建并向群聊或单聊发送通用流式卡片",
"agent_summary": "创建并向群聊或单聊发送互动卡片",
"use_when": [
"需要通用流式卡片交互且已准备接收会话或用户时"
"需要卡片式交互且已准备接收会话或用户时"
],
"avoid_when": [
"只发送普通文本时使用 send 或 send-by-bot;转发现有审批、日历、待办等原生产品卡片时应先取得真实 openMessageId,再使用 message forward"
"只发送普通文本时使用 send 或 send-by-bot"
],
"examples": [
"dws chat message send-card --group <openConversationId>"
@@ -263,7 +263,7 @@
"已知消息、会话和资源 ID,需要保存媒体文件时"
],
"avoid_when": [
"只查看文本消息内容时使用对应消息查询命令;本命令不是发送附件的前置步骤,本地文件发送直接使用 message send --msg-type file --file-path"
"只查看文本消息内容时使用对应消息查询命令"
],
"examples": [
"dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ."
@@ -280,10 +280,10 @@
"chat.forward_message": {
"agent_summary": "把一条已有消息转发到另一个会话",
"use_when": [
"已知同一源会话中的真实 openMessageId 与源、目标会话 ID,需要保留原消息或原生产品卡片时"
"已知源消息与源、目标会话 ID 时"
],
"avoid_when": [
"合并转发多条消息时使用 chat message combine-forward;OA 实例 ID、日历事件 ID、待办任务 ID 等产品对象 ID 不能代替消息 ID"
"合并转发多条消息时使用 chat message combine-forward"
],
"examples": [
"dws chat message forward --src-conversation-id <srcConversationId> --msg-id <openMessageId> --dest-conversation-id <destConversationId>"
@@ -340,7 +340,7 @@
"chat.get_conversation_info": {
"agent_summary": "获取群聊或单聊会话的详细信息",
"use_when": [
"已知群 ID 或单聊用户标识并需要解析会话详情时;--group、--user、--open-dingtalk-id 只能选择一个"
"已知群 ID 或用户标识并需要解析会话详情时"
],
"avoid_when": [
"按群名查找会话时使用 chat search"
@@ -383,7 +383,7 @@
"用户明确指定某个会话,并要读取消息或追溯引用回复中的原消息上下文时"
],
"avoid_when": [
"跨全部会话按时间查询时使用 chat message list-all;关键词搜索或审计应使用 message search,不要拉最近消息后在本地筛选"
"跨全部会话按时间查询时使用 chat message list-all"
],
"examples": [
"dws chat message list --group <openConversationId> --time \"2026-07-01 00:00:00\" --limit 50"
@@ -658,12 +658,12 @@
]
},
"chat.query_message_send_status": {
"agent_summary": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功",
"agent_summary": "查询异步消息发送任务的状态",
"use_when": [
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
"发送命令返回 openTaskId 后需要确认投递结果时"
],
"avoid_when": [
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
"没有 openTaskId 或只需查消息内容时不要使用"
],
"examples": [
"dws chat message query-send-status --open-task-id <openTaskId>"
@@ -802,12 +802,12 @@
]
},
"chat.remove_emoji_reaction": {
"agent_summary": "从同一条真实消息移除此前添加的同名表情回应",
"agent_summary": "移除指定消息上的表情回应",
"use_when": [
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
"需要取消此前添加的 emoji reaction 时"
],
"avoid_when": [
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
"移除文字表情时使用 chat message remove-text-emotion"
],
"examples": [
"dws chat message remove-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji \"赞\""
@@ -824,10 +824,10 @@
"chat.remove_group_member": {
"agent_summary": "从指定群聊移除成员",
"use_when": [
"群管理员明确要移除一个或多个普通成员时;清理临时群只传本次加入的普通成员"
"群管理员明确要移除一个或多个普通成员时"
],
"avoid_when": [
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
"移除机器人时使用 chat group members remove-bot"
],
"examples": [
"dws chat group members remove --id <openConversationId> --users userId1,userId2"
@@ -905,7 +905,7 @@
"chat.reply_personal_message": {
"agent_summary": "引用指定消息发送个人回复",
"use_when": [
"用户要针对某条已有消息进行引用回复,且 conversationId、openMessageId、senderOpenDingTalkId 来自同一条消息时"
"用户要针对某条已有消息进行引用回复时"
],
"avoid_when": [
"无需引用上下文的普通消息使用 chat message send"
@@ -1138,10 +1138,10 @@
"chat.send_personal_message": {
"agent_summary": "以当前用户身份发送群聊或单聊消息",
"use_when": [
"用户明确要以个人身份发送消息时;文本用 --text,图片仅在已有有效 mediaId 时用 --msg-type image --media-id,本地 PDF/DOCX/XLSX 等文件用 --msg-type file --file-path"
"用户明确要以个人身份发送文本或媒体消息时"
],
"avoid_when": [
"机器人身份或 Webhook 发送应使用对应命令;发送审批、日历或待办摘要不会创建对应产品对象"
"机器人身份或 Webhook 发送应使用对应命令"
],
"examples": [
"dws chat message send --group <openConversationId> \"项目已更新\""
@@ -1236,12 +1236,12 @@
]
},
"chat.set_pin_message": {
"agent_summary": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin",
"agent_summary": "把指定消息设为会话置顶消息",
"use_when": [
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
"需要在会话中置顶一条已知消息时"
],
"avoid_when": [
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
"取消置顶使用 chat message unset-pin-msg"
],
"examples": [
"dws chat message set-pin-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"
@@ -1316,12 +1316,12 @@
]
},
"chat.unset_pin_message": {
"agent_summary": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin",
"agent_summary": "取消指定消息的会话置顶",
"use_when": [
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
"需要移除一条已知置顶消息时"
],
"avoid_when": [
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
"新增置顶使用 chat message set-pin-msg"
],
"examples": [
"dws chat message unset-pin-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"
@@ -1378,10 +1378,10 @@
"chat.update_group_icon": {
"agent_summary": "使用真实媒体 ID 更新群头像",
"use_when": [
"已有可用于群头像接口的真实 mediaId 并要修改群头像时"
"已有上传后的头像 mediaId 并要修改群头像时"
],
"avoid_when": [
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 mediaId 的能力,缺少 mediaId 时应如实报告能力边界"
"没有真实可用 mediaId 时先完成媒体上传"
],
"examples": [
"dws chat group update-icon --group <openConversationId> --icon-media-id @mediaId"
@@ -1398,7 +1398,7 @@
"chat.update_group_name": {
"agent_summary": "修改指定群聊的名称",
"use_when": [
"需要把已有群聊改成用户指定的准确名称时;名称不得擅自加前缀、截断或改写,复合任务中重命名后继续完成后续步骤"
"需要给已有群聊重命名时"
],
"avoid_when": [
"只修改个人可见备注时不要使用群名称更新"
@@ -1418,10 +1418,10 @@
"chat.update_group_settings": {
"agent_summary": "更新指定群聊的一项设置开关",
"use_when": [
"需要调整 searchable、入群验证、@所有人权限等群级设置时;修改后用群信息或设置查询核对终态"
"需要调整 searchable、入群验证或群权限等设置时"
],
"avoid_when": [
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
"全员禁言和成员禁言使用专门的 mute 命令"
],
"examples": [
"dws chat group update-settings --group <openConversationId> --setting-key searchable --status 1"
@@ -1478,10 +1478,10 @@
"chat.update_streaming_card": {
"agent_summary": "更新已发送流式卡片的内容和状态",
"use_when": [
"已有 message send-card 真实返回的 bizId,并需要追加内容或结束流式输出时;最后一次更新使用完成状态"
"已有 bizId 并需要追加内容或结束流式输出时"
],
"avoid_when": [
"创建新卡片时使用 chat message send-card;openMessageId、OA 实例 ID、日历事件 ID或待办任务 ID 不能代替 bizId"
"创建新卡片时使用 chat message send-card"
],
"examples": [
"dws chat message update-card --biz-id <bizId> --content \"处理完成\" --flow-status 2"
@@ -1596,604 +1596,6 @@
"live-dws-schema:chat.upgrade_group_to_external#FAILED"
]
},
"chat.add_conv_to_categories": {
"agent_summary": "把一个会话加入一个或多个现有自定义分组",
"use_when": [
"已从 category list 取得真实 categoryId,并要把当前账号可访问的会话归入这些分组时"
],
"avoid_when": [
"创建新分组使用 category create;移出分组使用 category remove-conv"
],
"examples": [
"dws chat category add-conv --group <openConversationId> --category-ids 123,456"
],
"reviewed": true,
"review_reason": "依据 nanrun 树形路由与当前 Cobra/多维表参考复核会话分组写入路径;要求使用真实 ID 并在复合任务中继续完成后续步骤。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.add_conv_to_categories",
"cobra-help:dws chat category add-conv",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.create_conv_category": {
"agent_summary": "创建用户自定义会话分组",
"use_when": [
"用户明确要新建手工管理的会话分组,且名称不超过 15 个字符时"
],
"avoid_when": [
"按关键词或成员自动归类应使用 category create-smart;不得静默截断、缩写或改写用户名称"
],
"examples": [
"dws chat category create --title \"项目群\""
],
"reviewed": true,
"review_reason": "迁移 nanrun 的分组标题和复合任务 guidance 到 reviewed selection;保持当前 runtime 的 15 字符校验。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.create_conv_category",
"cobra-help:dws chat category create",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.delete_conv_category": {
"agent_summary": "删除指定的用户自定义会话分组",
"use_when": [
"用户明确要删除已知 categoryId 的自定义分组时"
],
"avoid_when": [
"只想把会话移出分组时使用 category remove-conv;目标不明确时不要删除"
],
"examples": [
"dws chat category delete --category-id 123"
],
"reviewed": true,
"review_reason": "依据当前原生命令与 nanrun 树形 category 分支补齐删除能力的选择边界。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.delete_conv_category",
"cobra-help:dws chat category delete",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.remove_conv_from_categories": {
"agent_summary": "把一个会话从一个或多个自定义分组移出",
"use_when": [
"已知会话和真实 categoryId,需要解除现有分组归属时"
],
"avoid_when": [
"删除整个分组使用 category delete;加入分组使用 category add-conv"
],
"examples": [
"dws chat category remove-conv --group <openConversationId> --category-ids 123,456"
],
"reviewed": true,
"review_reason": "依据当前原生命令与 nanrun 树形 category 分支补齐会话移出分组的选择边界。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.remove_conv_from_categories",
"cobra-help:dws chat category remove-conv",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.rename_conv_category": {
"agent_summary": "重命名用户自定义会话分组",
"use_when": [
"已知 categoryId,且用户给出了不超过 15 个字符的新名称时"
],
"avoid_when": [
"不得为绕过长度限制而截断、缩写或改写;创建新分组使用 category create"
],
"examples": [
"dws chat category rename --category-id 123 --title \"新名称\""
],
"reviewed": true,
"review_reason": "迁移 nanrun 的分组标题和复合任务 guidance 到 reviewed selection;保持当前 runtime 的 15 字符校验。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.rename_conv_category",
"cobra-help:dws chat category rename",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.grant_permission": {
"agent_summary": "为指定 chat scope 和业务参数发起高风险操作授权",
"use_when": [
"目标 chat 操作因行为授权缺失而需要按 scope、目标和时效发起授权时"
],
"avoid_when": [
"不要把它当作实际发送、撤回或群管理命令;跨组织数据读取授权使用 data-auth cross-org"
],
"examples": [
"dws chat chmod chat.message:send --grant-type timed --ttl 24h --permParam openCid=<openConversationId>"
],
"reviewed": true,
"review_reason": "依据当前授权 Cobra、dws-shared 安全规则和树形 data-auth/chmod 分支补齐 Agent 选路。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.grant_permission",
"cobra-help:dws chat chmod",
"skills/multi/dingtalk-chat/SKILL.md"
]
},
"chat.clear_all_red_point": {
"agent_summary": "清除当前用户所有会话的未读红点",
"use_when": [
"用户明确要求全部会话一键已读或清除所有红点时"
],
"avoid_when": [
"只处理一个会话时使用 clear-red-point;本命令不会删除聊天记录"
],
"examples": [
"dws chat clear-all-red-point"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 和 nanrun 会话状态树补齐全局红点清零能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.clear_all_red_point",
"cobra-help:dws chat clear-all-red-point",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.clear_conversation_messages": {
"agent_summary": "清空当前用户在指定会话中的聊天记录",
"use_when": [
"用户明确要求清空某个已知会话的本地聊天记录时"
],
"avoid_when": [
"仅清除未读红点使用 clear-red-point;未确认真实 openConversationId 时不要执行"
],
"examples": [
"dws chat clear-messages --conversation-id <openConversationId>"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 和 nanrun 会话状态树补齐清空会话消息的严格目标边界。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.clear_conversation_messages",
"cobra-help:dws chat clear-messages",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.clear_conversation_red_point": {
"agent_summary": "清除指定会话的未读红点",
"use_when": [
"用户只要求把某个已知会话标为已读或清除红点时"
],
"avoid_when": [
"全部会话一键已读使用 clear-all-red-point;本命令不会删除消息"
],
"examples": [
"dws chat clear-red-point --conversation-id <openConversationId>"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 和 nanrun 会话状态树补齐单会话红点清理能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.clear_conversation_red_point",
"cobra-help:dws chat clear-red-point",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.grant_cross_org_data_access": {
"agent_summary": "发起 chat 跨组织数据读取授权",
"use_when": [
"跨组织拉取聊天数据因 data scope 缺失,需要为目标组织或全部组织授权时"
],
"avoid_when": [
"发送、撤回和群管理授权使用 chat chmod;普通同组织查询不要预先授权"
],
"examples": [
"dws chat data-auth cross-org --target-org-id 439446171"
],
"reviewed": true,
"review_reason": "依据当前授权 Cobra、dws-shared 跨组织规则和 nanrun data-auth 分支补齐 Agent 选路。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.grant_cross_org_data_access",
"cobra-help:dws chat data-auth cross-org",
"skills/multi/dingtalk-chat/SKILL.md"
]
},
"chat.audit_join_group": {
"agent_summary": "审批一条群聊入群验证记录",
"use_when": [
"已从 list-join-validations 取得真实记录、申请人和邀请人 ID,需要执行 AuditApprove 或 AuditDelete 时"
],
"avoid_when": [
"列出申请使用 list-join-validations;服务端不支持 AuditIgnore、AuditRefuse 或 AuditBlock"
],
"examples": [
"dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <userId> --inviter <userId> --status AuditApprove"
],
"reviewed": true,
"review_reason": "依据 nanrun 的状态 hint 和当前 runtime 支持范围补齐入群审核选择语义。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.audit_join_group",
"cobra-help:dws chat group audit-join-validation",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.list_my_groups_pagination": {
"agent_summary": "分页拉取当前用户加入的全部群聊",
"use_when": [
"需要完整分页列出我加入的所有群,并沿用 nextCursor 继续读取时"
],
"avoid_when": [
"只查我创建或管理的群使用 group list-my-groups;按关键词找群使用 chat search"
],
"examples": [
"dws chat group list-all --limit 100"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun group 树补齐分页群列表能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.list_my_groups_pagination",
"cobra-help:dws chat group list-all",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.list_apply_join_group_records": {
"agent_summary": "分页拉取当前用户相关的入群验证记录",
"use_when": [
"需要查看待处理或历史入群申请,并取得后续审核所需记录 ID 时"
],
"avoid_when": [
"执行审批使用 audit-join-validation;普通群成员列表使用 group members"
],
"examples": [
"dws chat group list-join-validations --limit 20"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun group 树补齐入群验证列表能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.list_apply_join_group_records",
"cobra-help:dws chat group list-join-validations",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.list_group_member_by_ids": {
"agent_summary": "按成员 openDingTalkId 批量查询群成员详情",
"use_when": [
"已知群 openConversationId 和一组成员 openDingTalkId,需要批量取成员详情时"
],
"avoid_when": [
"列出整个群成员直接使用 group members --id;不要臆造 members list 子命令"
],
"examples": [
"dws chat group members list-by-ids --id <openConversationId> --users openDingTalkId1,openDingTalkId2"
],
"reviewed": true,
"review_reason": "迁移 nanrun 的群成员 flag guidance 到 reviewed selection,并固定 --id/--users 参数边界。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.list_group_member_by_ids",
"cobra-help:dws chat group members list-by-ids",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.create_group_notice": {
"agent_summary": "在指定群聊发布即时或定时群公告",
"use_when": [
"用户明确要发布群公告,并已给出群和 Markdown 正文时"
],
"avoid_when": [
"普通聊天消息使用 message send;修改已有公告使用 group notice edit"
],
"examples": [
"dws chat group notice create --group <openConversationId> --content \"今晚 22 点系统维护\""
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun notice 子树补齐群公告创建能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.create_group_notice",
"cobra-help:dws chat group notice create",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.edit_group_notice": {
"agent_summary": "整体替换指定群公告的正文与可选状态",
"use_when": [
"已从公告列表取得真实 dataId,需要修改现有群公告时"
],
"avoid_when": [
"发布新公告使用 group notice create;只查看内容使用 group notice get"
],
"examples": [
"dws chat group notice edit --group <openConversationId> --notice-id <dataId> --content \"更新后的公告\""
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun notice 子树补齐群公告编辑能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.edit_group_notice",
"cobra-help:dws chat group notice edit",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.get_group_notice": {
"agent_summary": "读取指定群公告的详情",
"use_when": [
"已知群和真实公告 dataId,需要查看正文、发布者或统计信息时"
],
"avoid_when": [
"不知道 dataId 时先用 group notice list;不要使用占位符公告 ID"
],
"examples": [
"dws chat group notice get --group <openConversationId> --notice-id <dataId>"
],
"reviewed": true,
"review_reason": "依据 nanrun 的真实 noticeId hint 和当前 Cobra 补齐公告详情选择语义。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.get_group_notice",
"cobra-help:dws chat group notice get",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.list_group_notices": {
"agent_summary": "分页读取指定群的已发布或定时公告",
"use_when": [
"需要列出群公告并取得后续详情或编辑使用的 dataId 时"
],
"avoid_when": [
"读取单条已知公告使用 group notice get;普通群消息使用 message list"
],
"examples": [
"dws chat group notice list --group <openConversationId> --limit 20"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun notice 子树补齐群公告列表能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.list_group_notices",
"cobra-help:dws chat group notice list",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.share_group_invite_url": {
"agent_summary": "把一个群的邀请链接分享到目标会话或单聊用户",
"use_when": [
"当前用户已加入源群,且要把邀请链接发给一个目标会话或一个接收人时"
],
"avoid_when": [
"只获取邀请链接使用 group invite-url;--target 与 --receiver 只能选择一个"
],
"examples": [
"dws chat group share-invite --source <sourceConversationId> --target <targetConversationId>"
],
"reviewed": true,
"review_reason": "迁移 nanrun 的 target/receiver 互斥 hint 与源群边界到 reviewed selection。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.share_group_invite_url",
"cobra-help:dws chat group share-invite",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.update_user_group_alias": {
"agent_summary": "设置仅当前用户可见的群备注",
"use_when": [
"用户要修改自己看到的群备注,而不是修改群的公开名称时"
],
"avoid_when": [
"修改所有成员看到的群名使用 group rename;修改自己群昵称使用 group update-nick"
],
"examples": [
"dws chat group update-alias --group <openConversationId> --alias-title \"客户项目群\""
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun group 树补齐群备注和群名/群昵称消歧。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.update_user_group_alias",
"cobra-help:dws chat group update-alias",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.hide_conversation": {
"agent_summary": "从当前用户的会话列表隐藏指定会话",
"use_when": [
"用户明确要隐藏一个已知会话,接受收到新消息后可能再次出现时"
],
"avoid_when": [
"免打扰使用 chat mute;退出群聊使用 group quit;本命令不会删除消息"
],
"examples": [
"dws chat hide --conversation-id <openConversationId>"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun 会话状态树补齐隐藏会话能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.hide_conversation",
"cobra-help:dws chat hide",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.list_all_conversations": {
"agent_summary": "分页获取当前用户的全部单聊和群聊会话",
"use_when": [
"需要完整枚举会话并沿用 nextCursor 翻页时"
],
"avoid_when": [
"只看置顶会话使用 list-top-conversations;只看群聊使用 group list-all"
],
"examples": [
"dws chat list-all-conversations --limit 50"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun 会话树补齐全部会话列表能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.list_all_conversations",
"cobra-help:dws chat list-all-conversations",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.mark_message_read": {
"agent_summary": "把指定消息及之前的消息标记为已读",
"use_when": [
"已知同一会话中的真实 openMessageId,需要推进已读位置时"
],
"avoid_when": [
"只清除会话红点使用 clear-red-point;查询他人是否已读使用 message read-status"
],
"examples": [
"dws chat mark-read --conversation-id <openConversationId> --message-id <openMessageId>"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun 会话状态树补齐消息已读写入能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.mark_message_read",
"cobra-help:dws chat mark-read",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.mark_conversation_unread": {
"agent_summary": "把指定会话标记为未读",
"use_when": [
"用户要稍后处理某个已知会话并将其重新标为未读时"
],
"avoid_when": [
"清除红点使用 clear-red-point;查询未读会话使用 message list-unread-conversations"
],
"examples": [
"dws chat mark-unread --conversation-id <openConversationId>"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun 会话状态树补齐标记未读能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.mark_conversation_unread",
"cobra-help:dws chat mark-unread",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.list_message_emotion_replies": {
"agent_summary": "批量读取多条消息的 emoji 与文字表情回应",
"use_when": [
"已从消息查询取得一组真实 openMessageId,需要汇总回应信息时"
],
"avoid_when": [
"添加或移除回应使用 add/remove-emoji 或 add/remove-text-emotion;不要传占位符 ID"
],
"examples": [
"dws chat message list-emotion-replies --msg-ids <openMessageId1>,<openMessageId2>"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun message 树补齐批量消息回应读取能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.list_message_emotion_replies",
"cobra-help:dws chat message list-emotion-replies",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.set_top_message": {
"agent_summary": "把指定消息置顶到会话顶部",
"use_when": [
"已从目标会话取得真实且同源的 openMessageId,需要设置消息置顶时"
],
"avoid_when": [
"置顶整个会话使用 chat set-top;钉住消息使用 message set-pin-msg"
],
"examples": [
"dws chat message set-top-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"
],
"reviewed": true,
"review_reason": "迁移 nanrun 的真实消息 ID guidance 到 reviewed selection,并与会话置顶和 Pin 消歧。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.set_top_message",
"cobra-help:dws chat message set-top-msg",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.unset_top_message": {
"agent_summary": "取消指定会话中的消息置顶",
"use_when": [
"已知同源的会话和真实 openMessageId,需要取消消息置顶时"
],
"avoid_when": [
"取消会话置顶使用 chat set-top --off;取消 Pin 使用 message unset-pin-msg"
],
"examples": [
"dws chat message unset-top-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun message 树补齐取消消息置顶的选择边界。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.unset_top_message",
"cobra-help:dws chat message unset-top-msg",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.update_at_all_notification_off": {
"agent_summary": "关闭或恢复指定会话的 @所有人通知",
"use_when": [
"用户只想调整某个会话的 @所有人提醒偏好时"
],
"avoid_when": [
"关闭全部会话通知使用 chat mute;红包通知偏好使用 mute-red-envelope"
],
"examples": [
"dws chat mute-at-all --conversation-id <openConversationId>"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun 会话通知树补齐 @all 通知偏好能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.update_at_all_notification_off",
"cobra-help:dws chat mute-at-all",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.update_red_env_notification_off": {
"agent_summary": "关闭或恢复指定会话的红包通知",
"use_when": [
"用户只想调整某个会话的红包消息提醒偏好时"
],
"avoid_when": [
"关闭全部会话通知使用 chat mute;@所有人通知偏好使用 mute-at-all"
],
"examples": [
"dws chat mute-red-envelope --conversation-id <openConversationId>"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun 会话通知树补齐红包通知偏好能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.update_red_env_notification_off",
"cobra-help:dws chat mute-red-envelope",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.translate": {
"agent_summary": "把指定聊天文本翻译成目标语言",
"use_when": [
"用户给出文本并明确要求翻译为支持的语言代码时"
],
"avoid_when": [
"翻译文档或文件内容应使用对应产品能力;本命令只处理传入文本"
],
"examples": [
"dws chat text translate --query \"你好世界\" --to en_US"
],
"reviewed": true,
"review_reason": "依据当前 Cobra 与 nanrun text 分支补齐聊天文本翻译能力。",
"source_refs": [
"internal/cli/schema_command_registry/products/chat.json#chat.translate",
"cobra-help:dws chat text translate",
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat.md"
]
},
"chat.shortcut_bot_search": {
"agent_summary": "搜索当前用户自己创建的机器人",
"use_when": [
@@ -2845,12 +2247,12 @@
]
},
"chat.shortcut_messages_query_send_status": {
"agent_summary": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功",
"agent_summary": "查询消息发送状态",
"use_when": [
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws chat +messages-query-send-status --open-task-id <openTaskId>"
@@ -3142,10 +2544,10 @@
"chat.batch_query_group_chat_settings": {
"agent_summary": "批量查询当前用户自己的群会话设置(置顶/免打扰/群昵称/群备注)",
"use_when": [
"用户说看下这些群中我自己的置顶和免打扰设置;修改前先查询并保存原值用于恢复"
"用户说 看下这些群我的置顶和免打扰设置"
],
"avoid_when": [
"管理员级群功能开关用 chat group update-settings;查询结果是当前用户视角,不代表群级设置"
"管理员级群功能开关用 chat group update-settings"
],
"examples": [
"dws chat group user-settings query --groups cid1,cid2 --format json"
@@ -3161,10 +2563,10 @@
"chat.batch_update_group_chat_settings": {
"agent_summary": "批量更新当前用户自己的群会话设置(置顶/免打扰/群昵称/群备注)",
"use_when": [
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
"用户说 把这些群都设为免打扰/置顶"
],
"avoid_when": [
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
"单个群昵称优先 chat group update-nick"
],
"examples": [
"dws chat group user-settings set --items '[{\"openConversationId\":\"cid1\",\"top\":true,\"mute\":false}]' --format json"
+48 -52
View File
@@ -82,22 +82,22 @@
]
},
"doc.create_document": {
"agent_summary": "在默认根目录、文档文件夹或知识库根创建带可选初始内容的 adoc",
"agent_summary": "创建一篇新的在线文档",
"use_when": [
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
"用户要新建一篇文字在线文档(adoc),可空文档或带初始 Markdown 时",
"创建到指定文件夹 --folder、知识库根 --workspace,或默认「我的文档」根目录时"
],
"avoid_when": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type <type>(勿用 doc create)",
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert"
],
"examples": [
"dws doc create --name \"项目周报\" --format json",
"dws doc create --name \"Q1 总结\" --content-file ./q1.md --workspace <WORKSPACE_ID> --format json"
"dws doc create --name \"Q1 总结\" --content \"# Q1 总结\" --folder <FOLDER_ID> --format json"
],
"reviewed": true,
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=doc.create_document",
"cobra-help:dws doc create",
@@ -295,12 +295,12 @@
"doc.get_document_content": {
"agent_summary": "读取完整文档内容,或按 outline/range/section/tags 获取 JSONML fragment",
"use_when": [
"已由 drive info 确认 extension=adoc,用户要读取正文(Markdown)时",
"用户提供已知 adoc nodeId/URL,且要读取内容或抽取指定章节时",
"用户要读取钉钉在线文字文档(adoc)正文(Markdown)时",
"用户直接粘贴文档 URL 且无其他指令时(默认读内容)",
"只需标题大纲、指定块区间/单块或特定 JSONML tags 时使用 --content-format jsonml 与 --scope"
],
"avoid_when": [
"原始 alidocs URL 类型未知或目标不是 adoc 时先用 drive info 探测并路由;表格/多维表/普通文件不要用本命令",
"非 adoc(表格/多维表/普通文件)不要用本命令;先 doc info 再路由",
"要元信息用 doc info;要块结构用 doc block list",
"Markdown 为有损投影:保形复制模板请用 doc copy,不要 read→create"
],
@@ -309,7 +309,7 @@
"dws doc read --node <DOC_ID> --content-format jsonml --scope outline --max-depth 3"
],
"reviewed": true,
"review_reason": "人工对齐 doc Skill 与 doc-info reference 的 extension 预检边界,并保留 Markdown/JSONML scope 的真实 Cobra 契约。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=doc.get_document_content",
"cobra-help:dws doc read",
@@ -321,23 +321,22 @@
]
},
"doc.get_document_info": {
"agent_summary": "在已确认是 ALIDOC 后读取文档专属元信息",
"agent_summary": "获取文档元信息(标题/类型/创建者/权限等)",
"use_when": [
"drive info 已确认是 ALIDOC,用户还要标题、创建者、权限或 docUrl 等文档专属元信息时",
"创建响应只有 nodeId、缺少 docUrl,需要补查文档链接时"
"用户要查看文档/节点元信息(标题、类型、创建者、权限)时",
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时"
],
"avoid_when": [
"原始 alidocs URL 的类型探测、extension 路由或可靠 fileSize 使用 dws drive info;不要先猜是文档",
"已确认是 adoc 且只要正文时改用 dws doc read",
"已确认是 adoc 且只要正文改用 dws doc read",
"只要目录列表改用 dws drive list / wiki node list",
"普通文件、电子表格或 AI 表格不使用本命令"
"需要可靠文件大小 fileSize 时改用 dws drive info;文档元信息接口可能不返回大小"
],
"examples": [
"dws doc info --node <DOC_ID> --format json",
"dws doc info --node \"https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>\" --format json"
],
"reviewed": true,
"review_reason": "人工对齐 drive info 统一类型探测入口与 doc info 的文档专属补查职责;不改变 node 参数或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=doc.get_document_info",
"cobra-help:dws doc info",
@@ -372,11 +371,10 @@
"doc.insert_document_block": {
"agent_summary": "向文档插入块元素",
"use_when": [
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
],
"avoid_when": [
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"删块用 block delete;改已有块用 block update"
],
@@ -385,7 +383,7 @@
"dws doc block insert --node <DOC_ID> --heading \"二级标题\" --level 2 --format json"
],
"reviewed": true,
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=doc.insert_document_block",
"cobra-help:dws doc block insert",
@@ -422,7 +420,7 @@
"doc.list_document_blocks": {
"agent_summary": "查询文档一级块元素列表",
"use_when": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
],
"avoid_when": [
"只要全文 Markdown 用 doc read",
@@ -433,7 +431,7 @@
"dws doc block list --node <DOC_ID> --start-index 0 --end-index 5 --format json"
],
"reviewed": true,
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=doc.list_document_blocks",
"cobra-help:dws doc block list",
@@ -721,18 +719,17 @@
"doc.update_comment": {
"agent_summary": "更新指定文档评论的文字内容和可选 @用户/@群。",
"use_when": [
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
],
"avoid_when": [
"删除评论用 delete;回复用 reply",
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
"删除评论用 delete;回复用 reply"
],
"examples": [
"dws doc comment update --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"已按最新数据修正\" --format json",
"dws doc comment update --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"请群内确认\" --mentioned-open-conversation-id <openConversationId>"
],
"reviewed": true,
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.update_comment",
"cobra-help:dws doc comment update --help",
@@ -742,24 +739,23 @@
]
},
"doc.update_document": {
"agent_summary": "用原生自动分片管道追加或整篇覆盖 adoc 内容",
"agent_summary": "更新文档内容(追加 / 覆盖;覆盖需 --yes)",
"use_when": [
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户要向已有 adoc 追加内容时用 --mode append(更安全)",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
],
"avoid_when": [
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
"创建新文档用 doc create,不要用 update 冒充创建"
],
"examples": [
"dws doc update --node <DOC_ID> --content \"# 追加内容\" --mode append --format json",
"dws doc update --node <DOC_ID> --content-file ./body.md --mode overwrite --dry-run"
],
"reviewed": true,
"review_reason": "人工对齐 Runtime 的 10000 字符自动分片、append/overwrite 动态门禁与根 Skill 的写后回读流程;不改变参数和安全事实。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=doc.update_document",
"cobra-help:dws doc update",
@@ -909,14 +905,14 @@
"当你只记得文档的标题或主题词、需要先定位到某篇钉钉文档拿到它的 nodeId/URL 以便后续阅读或编辑时使用;可按关键词、扩展名、创建/访问时间、创建者等条件过滤,不传关键词则返回最近访问的文档,返回匹配的文档列表。"
],
"avoid_when": [
"只有一个关键词且只要紧凑标题/URL/type/token 投影时优先 +find-doc;目标已给 nodeId/URL 时不要再搜索"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws doc +search --query \"会议纪要\"",
"dws doc +search --extensions pdf,docx"
],
"reviewed": true,
"review_reason": "人工区分 +search 的丰富过滤/最近访问能力与 +find-doc 的单关键词紧凑投影,避免两个同源搜索 Shortcut 互相争抢。",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_search",
"cobra-help:dws doc +search",
@@ -929,14 +925,14 @@
"当你已知某个文档文件夹或知识库的 ID、想浏览它下面直接包含的文档与子文件夹(不递归深层)以便逐层导航时使用;输入 folder 或 workspace,返回该层级的子节点列表。"
],
"avoid_when": [
"全局或钉盘目录浏览优先 drive list,知识库节点树优先 wiki node list;本 Shortcut 只列已知 doc folder/workspace 的直接子节点"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws doc +list --folder DOC_FOLDER_NODE_ID",
"dws doc +list --workspace WS_ID --limit 20"
],
"reviewed": true,
"review_reason": "人工对齐 doc/drive/wiki 产品边界:保留已知文档文件夹/知识库的直接子节点投影,不替代常规目录管理。",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_list",
"cobra-help:dws doc +list",
@@ -949,13 +945,13 @@
"当你想保留原件、在另一个文件夹或知识库里生成一份文档/文件副本(例如以某篇文档为模板另存)时使用;输入源 node 与目标 folder/workspace,会实际创建一个副本。"
],
"avoid_when": [
"常规文件管理优先 drive copy;要搬走原件用 move;复制后必须从真实返回取副本 nodeId,禁止继续编辑源文档"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws doc +copy --node DOC_ID --folder TARGET_FOLDER_NODE_ID"
],
"reviewed": true,
"review_reason": "人工对齐保形复制、真实副本 ID 与 drive 文件管理边界;保留 Runtime user_required 门禁。",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_copy",
"cobra-help:dws doc +copy",
@@ -968,13 +964,13 @@
"当你要整理文档归属、把某篇文档/文件从当前位置挪到另一个文件夹或知识库(原位置不再保留)时使用;输入 node 与目标 folder/workspace,会实际改变文件的存放位置。"
],
"avoid_when": [
"常规文件管理优先 drive move;要保留原位置副本用 copy;目标位置不明确或未确认时不要移动"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws doc +move --node DOC_ID --folder TARGET_FOLDER_NODE_ID"
],
"reviewed": true,
"review_reason": "人工对齐 move 的原位置消失语义、目标位置确认与 drive 文件管理边界;保留 Runtime user_required 门禁。",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_move",
"cobra-help:dws doc +move",
@@ -1045,13 +1041,13 @@
"当你想把在线文档导出成 docx/markdown/pdf 文件(例如离线保存或外发)时使用;这是异步任务的第一步,输入 node 与 export-format 提交导出,返回 jobId,随后用 +export-get 轮询结果。"
],
"avoid_when": [
"用户要直接拿到本地 docx/markdown/pdf 文件时使用一体化 dws doc export;不要默认手工编排 submit/get 轮询"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws doc +export-submit --node DOC_ID --export-format markdown"
],
"reviewed": true,
"review_reason": "人工对齐 atomic doc export 的一体化提交/轮询/下载路径,将 +export-submit 限定为明确异步控制场景。",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_export_submit",
"cobra-help:dws doc +export-submit",
@@ -1121,13 +1117,13 @@
"当文档被误改、你想把它整体恢复到某个历史版本时使用;先用 +version-list 找到目标版本号,再输入 node 与 version,会实际把文档内容覆盖回该版本,属于高风险写操作,需谨慎确认。"
],
"avoid_when": [
"只查看历史用 +version-list,只保存当前快照用 +version-save;版本号未核实、用户未确认或只需改单块时不要回滚"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws doc +version-revert --node DOC_ID --version 3"
],
"reviewed": true,
"review_reason": "人工对齐 version list → 明确确认 → revert → 回读的 ID 与高风险边界;不改变 typed_yes 安全门禁。",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_version_revert",
"cobra-help:dws doc +version-revert",
@@ -1178,14 +1174,14 @@
"当你只想往一篇钉钉文档的最后面补一段文字、又不想动原有内容时使用;内部用文档更新的“追加(append)”模式,把你给的文本安全地拼到文档末尾,不需要你先去查文档块列表、算末尾位置或手工拼块结构。会真实写入文档内容。"
],
"avoid_when": [
"长、多行、表格或文件内容使用 doc update --mode append --content-file;指定位置或富结构使用 block insert;不要跳过 user_required 确认"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws doc +doc-append --doc DOC_ID --text \"补充说明:本方案已评审通过。\"",
"dws doc +doc-append --doc \"https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>\" --text \"追加一行备注\""
],
"reviewed": true,
"review_reason": "人工将 Shortcut 限定为短纯文本末尾追加,并对齐 user_required 与写后回读;长内容交给原生自动分片管道。",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_doc_append",
"cobra-help:dws doc +doc-append",
@@ -1198,14 +1194,14 @@
"当你只记得云文档标题或内容里的某个关键词,想快速按关键词找到匹配的文档、拿到它的标题、URL、类型和 token 以便后续查看或编辑,却不想拿到一大坨原始字段时使用;内部调用云文档的 search_documents 工具,把 --query 作为搜索关键词(keyword),可选地用 --limit 限制返回条数(pageSize),再在本地把每条命中结果精简为「标题、URL、类型、token」四个字段后打印。这是纯只读操作,只做搜索与本地投影,不会创建、修改或删除任何文档;未命中时提示「没搜到文档」。"
],
"avoid_when": [
"需要最近访问、extension/时间/创建者等组合过滤时使用 +search;目标已给 nodeId/URL 时直接进入 info/read,不再搜索"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws doc +find-doc --query 季度汇报",
"dws doc +find-doc --query 合同 --limit 10"
],
"reviewed": true,
"review_reason": "人工将 +find-doc 定位为高频单关键词紧凑投影,并与 +search 的丰富过滤能力做互斥路由。",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_find_doc",
"cobra-help:dws doc +find-doc",
@@ -1218,13 +1214,13 @@
"当你手上已经有一个文档链接、想直接私信发给某个人而不必先查 userId 时使用;内部先按姓名搜通讯录解析出唯一用户,再用 openDingTalkId 把链接拼成一条 Markdown 消息发出去,姓名匹配到多人时会列出候选让你区分。只发链接、不读取或改动文档本身,会真实发出消息。"
],
"avoid_when": [
"同名人员未消歧、缺少真实文档 URL 或用户未确认时不要发送;本命令不授予文档权限,授权应走 drive permission"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
],
"examples": [
"dws doc +share-doc --to 张三 --url https://docs.dingtalk.com/xxx --note \"帮忙过一下\""
],
"reviewed": true,
"review_reason": "人工对齐人员消歧、真实 docUrl、user_required 消息发送门禁与文档权限边界。",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_share_doc",
"cobra-help:dws doc +share-doc",
@@ -910,15 +910,12 @@
"agent_summary": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件",
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
],
"avoid_when": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -927,7 +924,7 @@
"dws drive upload --file ./README.md --node <dentryUuid> --format json"
],
"reviewed": true,
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=drive.upload",
"cobra-help:dws drive upload",
@@ -1790,9 +1790,15 @@
"drive.apply_permission --reason": "Reviewed unpinned adapter: drive.apply_permission has no singular pinned interface_ref; --reason is a CLI wrapper input and does not publish a direct interface property.",
"drive.apply_permission --role": "Reviewed unpinned adapter: drive.apply_permission has no singular pinned interface_ref; --role is a CLI wrapper input and does not publish a direct interface property.",
"drive.apply_permission --users": "Reviewed unpinned adapter: drive.apply_permission has no singular pinned interface_ref; --users is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file --no-resume": "CLI-only transfer-layer flag controlling download resume behaviour; not an MCP interface parameter.",
"drive.download_file --output": "local output path",
"drive.download_file --parallel": "CLI-only transfer-layer flag controlling parallel chunk downloads; not an MCP interface parameter.",
"drive.download_file --part-size": "CLI-only transfer-layer flag controlling download chunk size; not an MCP interface parameter.",
"drive.download_file_version --no-resume": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --no-resume is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --node": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --node is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --output": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --output is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --parallel": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --parallel is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --part-size": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --part-size is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --version": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --version is a CLI wrapper input and does not publish a direct interface property.",
"drive.get_cover --node": "Reviewed unpinned adapter: drive.get_cover has no singular pinned interface_ref; --node is a CLI wrapper input and does not publish a direct interface property.",
"drive.get_star_list --content-types": "Reviewed unpinned adapter: drive.get_star_list has no singular pinned interface_ref; --content-types is a CLI wrapper input and does not publish a direct interface property.",
-264
View File
@@ -207,270 +207,6 @@ func TestCrossPlatformCoveragePATURLAndMutationCoverageEdges(t *testing.T) {
}
}
func TestCrossPlatformCoveragePATClassification(t *testing.T) {
patErr := &PATError{RawJSON: `{"code":"PAT_NO_PERMISSION"}`}
if patErr.Error() != patErr.RawJSON || patErr.RawStderr() != patErr.RawJSON || patErr.ExitCode() != ExitCodePermission {
t.Fatalf("PATError contract changed: %#v", patErr)
}
if !IsPATError(patErr) || IsPATError(stderrors.New("plain")) {
t.Fatal("PAT error classification changed")
}
if !IsPATNoPermissionCode("PAT_NO_PERMISSION") || IsPATNoPermissionCode("UNKNOWN") {
t.Fatal("PAT permission code classification changed")
}
if code, ok := lookupCodeIn(map[string]any{
"code": 1,
"errorCode": "PAT_NO_PERMISSION",
}, patNoPermissionCodes); !ok || code != "PAT_NO_PERMISSION" {
t.Fatalf("lookupCodeIn fallback = %q, %v", code, ok)
}
if code, ok := lookupCodeIn(map[string]any{"code": "UNKNOWN"}, patNoPermissionCodes); ok || code != "" {
t.Fatalf("lookupCodeIn unknown = %q, %v", code, ok)
}
for _, tc := range []struct {
body map[string]any
want string
ok bool
}{
{body: map[string]any{"code": "PAT_NO_PERMISSION"}, want: "PAT_NO_PERMISSION", ok: true},
{body: map[string]any{"error_code": "PAT_SCOPE_AUTH_REQUIRED"}, want: "PAT_SCOPE_AUTH_REQUIRED", ok: true},
{body: map[string]any{"code": "UNKNOWN"}},
} {
code, ok := getPATErrorCode(tc.body)
if code != tc.want || ok != tc.ok {
t.Fatalf("getPATErrorCode(%v) = %q, %v", tc.body, code, ok)
}
}
if code, ok := getDWSGatewayErrorCode(map[string]any{"errorCode": "DWS_AUTH_SERVICE_FAILED"}); !ok || code != "DWS_AUTH_SERVICE_FAILED" {
t.Fatalf("gateway code = %q, %v", code, ok)
}
if !isNotLoggedInError(map[string]any{
"error": 1,
"message": "Missing service_id or access_key",
}) {
t.Fatal("missing-login response was not recognized")
}
if isNotLoggedInError(map[string]any{"message": "other"}) {
t.Fatal("ordinary response was recognized as missing login")
}
for _, body := range []map[string]any{
{"error": "failure"},
{"success": false},
{"success": "FALSE"},
} {
if !isBusinessError(body) {
t.Fatalf("business error was not recognized: %#v", body)
}
}
for _, body := range []map[string]any{
{},
{"success": true},
{"success": "true"},
} {
if isBusinessError(body) {
t.Fatalf("successful response was classified as a business error: %#v", body)
}
}
if err := ClassifyToolResultContent(map[string]any{"code": "DWS_SERVICE_UNAUTHORIZED"}); err == nil {
t.Fatal("gateway tool result was not classified")
}
if err := ClassifyToolResultContent(map[string]any{"code": "PAT_BATCH_AUTH_PENDING"}); !IsPATError(err) {
t.Fatalf("PAT tool result = %T %v", err, err)
}
if err := ClassifyToolResultContent(map[string]any{"success": true}); err != nil {
t.Fatalf("successful tool result = %v", err)
}
responseCases := []struct {
name string
text string
kind string
}{
{name: "invalid json", text: "not-json", kind: "nil"},
{name: "gateway", text: `{"code":"DWS_AUTH_SERVICE_FAILED"}`, kind: "error"},
{name: "not logged in", text: `{"message":"Missing service_id or access_key"}`, kind: "error"},
{name: "pat", text: `{"code":"PAT_NO_PERMISSION"}`, kind: "pat"},
{name: "business", text: `{"success":false,"message":"参数错误"}`, kind: "error"},
{name: "success", text: `{"success":true}`, kind: "nil"},
}
for _, tc := range responseCases {
t.Run(tc.name, func(t *testing.T) {
err := ClassifyMCPResponseText(tc.text)
switch tc.kind {
case "nil":
if err != nil {
t.Fatalf("ClassifyMCPResponseText() = %v", err)
}
case "pat":
if !IsPATError(err) {
t.Fatalf("ClassifyMCPResponseText() = %T %v", err, err)
}
default:
if err == nil {
t.Fatal("ClassifyMCPResponseText() returned nil")
}
}
})
}
if !strings.Contains(authExpiredHint(), "auth login") || !strings.Contains(notLoggedInHint(), "auth login") {
t.Fatal("authentication recovery hints lost the login command")
}
if got := ClassifyPatAuthCheck(map[string]any{"code": "PAT_LOW_RISK_NO_PERMISSION"}); got == nil {
t.Fatal("ClassifyPatAuthCheck() returned nil")
}
if got := ClassifyPatAuthCheck(map[string]any{"code": "UNKNOWN"}); got != nil {
t.Fatalf("ClassifyPatAuthCheck() = %#v", got)
}
wrapped := stderrors.Join(stderrors.New("outer"), patErr)
if got := AsPatAuthCheckError(wrapped); got != patErr {
t.Fatalf("AsPatAuthCheckError() = %#v", got)
}
if got := AsPatAuthCheckError(stderrors.New("plain")); got != nil {
t.Fatalf("AsPatAuthCheckError() = %#v", got)
}
}
func TestSuggestBusinessHintChatRecovery(t *testing.T) {
tests := []struct {
name string
body map[string]any
want string
}{
{
name: "missing group role context",
body: map[string]any{
"error": map[string]any{"code": "IM_ERROR", "message": "listRoles null"},
"summary": "context",
"code": "TOP_LEVEL",
},
want: "list-my-groups",
},
{name: "legacy open id spelling", body: map[string]any{"message": "OpendId is not in conversation"}, want: "实际加入"},
{name: "open id outside conversation", body: map[string]any{"message": "OpenId is not in conversation"}, want: "实际加入"},
{name: "operator outside source group", body: map[string]any{"message": "The operator is not in this group chat"}, want: "源群"},
{name: "missing invitation receiver", body: map[string]any{"message": "targetOpenConversationId和receiverUid不能同时为空"}, want: "--receiver"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := SuggestBusinessHint(tc.body); !strings.Contains(got, tc.want) {
t.Errorf("SuggestBusinessHint(%v) = %q, want containing %q", tc.body, got, tc.want)
}
})
}
}
func TestCrossPlatformCoveragePATSerializationAndPolicyEdges(t *testing.T) {
oldHost := hostControlProvider
oldBrowser := patBrowserProvider
t.Cleanup(func() {
SetHostControlProvider(oldHost)
SetPATOpenBrowserProvider(oldBrowser)
})
SetHostControlProvider(func() string { return "" })
if block := HostControlBlock(); block != nil {
t.Fatalf("empty host provider returned %#v", block)
}
SetHostControlProvider(func() string { return "codex" })
SetPATOpenBrowserProvider(func() bool { return false })
out := map[string]any{
"data": map[string]any{
"authUrl": " https://example.test/fe/old#%2FpersonalAuthorization%3FflowId%3Df%26userCode%3Du ",
"callbacks": map[string]any{"owner": "cli"},
},
}
ApplyHostMutations(out)
data := out["data"].(map[string]any)
if data["openBrowser"] != false || data["hostControl"] == nil || data["uri"] == "" {
t.Fatalf("host mutations = %#v", data)
}
if _, ok := data["authUrl"]; ok {
t.Fatalf("authUrl alias was not removed: %#v", data)
}
if _, ok := data["callbacks"]; ok {
t.Fatalf("legacy callbacks were not removed: %#v", data)
}
rawPolicy := cleanPATJSON(map[string]any{
"message": "organization denied",
"scope": "chat.read",
}, "PAT_ORG_POLICY_DENIED")
var policyPayload map[string]any
if err := json.Unmarshal([]byte(rawPolicy), &policyPayload); err != nil {
t.Fatalf("decode policy PAT JSON: %v", err)
}
policyData := policyPayload["data"].(map[string]any)
for key, want := range map[string]any{
"policy": "OPEN_SOURCE_ORG_SCOPE_FORBIDDEN",
"message": "organization denied",
"action": "contact_org_admin",
"openBrowser": false,
"retryable": false,
} {
if got := policyData[key]; got != want {
t.Fatalf("policy data %s = %#v, want %#v", key, got, want)
}
}
if !strings.Contains(policyData["hint"].(string), "organization denied") {
t.Fatalf("policy hint = %#v", policyData["hint"])
}
rawDefault := cleanPATJSON(map[string]any{}, "PAT_ORG_POLICY_DENIED")
var defaultPayload map[string]any
if err := json.Unmarshal([]byte(rawDefault), &defaultPayload); err != nil {
t.Fatalf("decode default policy PAT JSON: %v", err)
}
defaultData := defaultPayload["data"].(map[string]any)
if !strings.Contains(defaultData["hint"].(string), "组织策略") {
t.Fatalf("default policy hint = %#v", defaultData["hint"])
}
prepopulated := map[string]any{"data": map[string]any{
"policy": "CUSTOM",
"message": "existing",
"hint": "existing hint",
}}
applyOrgPolicyDeniedHint(prepopulated, map[string]any{"message": "ignored"})
prepopulatedData := prepopulated["data"].(map[string]any)
if prepopulatedData["policy"] != "CUSTOM" ||
prepopulatedData["message"] != "existing" ||
prepopulatedData["hint"] != "existing hint" {
t.Fatalf("prepopulated policy fields changed: %#v", prepopulatedData)
}
if got := stringValue(map[string]any{
"number": 1,
"blank": " ",
"value": " kept ",
}, "number", "blank", "value"); got != "kept" {
t.Fatalf("stringValue fallback = %q", got)
}
if got := stringValue(map[string]any{"blank": " "}, "blank", "missing"); got != "" {
t.Fatalf("stringValue empty = %q", got)
}
cleaned := stripClassFields(map[string]any{
"class": "top",
"items": []any{
map[string]any{"class": "nested", "keep": "yes"},
"scalar",
},
}).(map[string]any)
if _, ok := cleaned["class"]; ok {
t.Fatalf("top-level class was retained: %#v", cleaned)
}
items := cleaned["items"].([]any)
nested := items[0].(map[string]any)
if _, ok := nested["class"]; ok || nested["keep"] != "yes" || items[1] != "scalar" {
t.Fatalf("nested class cleanup = %#v", cleaned)
}
}
func mustParseURLForTest(t *testing.T, raw string) *url.URL {
t.Helper()
parsed, err := url.Parse(raw)
+14 -53
View File
@@ -42,24 +42,23 @@ const (
// Error is the structured repository-local error model for the Go rewrite.
type Error struct {
Category Category
Message string
Operation string
ServerKey string
Retryable bool
Category Category
Message string
Operation string
ServerKey string
Retryable bool
RetryableSet bool
RetryAfterSeconds *int64
NextRetryAt *time.Time
Reason string
Hint string
Actions []string
Examples []string
AvailableFlags []string
Snapshot string
RPCCode int `json:"rpc_code,omitempty"`
RPCData json.RawMessage `json:"rpc_data,omitempty"`
ServerDiag ServerDiagnostics `json:"-"`
Cause error `json:"-"`
Reason string
Hint string
Actions []string
AvailableFlags []string
Snapshot string
RPCCode int `json:"rpc_code,omitempty"`
RPCData json.RawMessage `json:"rpc_data,omitempty"`
ServerDiag ServerDiagnostics `json:"-"`
Cause error `json:"-"`
}
func (e *Error) Error() string {
@@ -170,22 +169,6 @@ func WithActions(actions ...string) Option {
}
}
// WithExamples records copyable command examples for recovery.
func WithExamples(examples ...string) Option {
return func(err *Error) {
out := make([]string, 0, len(examples))
for _, example := range examples {
if strings.TrimSpace(example) == "" {
continue
}
out = append(out, example)
}
if len(out) > 0 {
err.Examples = out
}
}
}
// WithAvailableFlags records visible local flag names for agent recovery.
func WithAvailableFlags(names ...string) Option {
return func(err *Error) {
@@ -326,12 +309,7 @@ func PrintJSON(w io.Writer, err error) error {
}
if len(typed.Actions) > 0 {
errorPayload["actions"] = typed.Actions
errorPayload["suggested_actions"] = typed.Actions
}
if len(typed.Examples) > 0 {
errorPayload["examples"] = typed.Examples
}
errorPayload["error_message"] = typed.Message
if len(typed.AvailableFlags) > 0 {
errorPayload["available_flags"] = typed.AvailableFlags
}
@@ -410,23 +388,6 @@ func PrintHumanAt(w io.Writer, err error, v Verbosity) error {
return writeErr
}
if len(typed.Examples) > 0 {
lines := []string{
"错误信息:" + typed.Message,
"原因:" + typed.Reason,
"建议操作:",
}
for i, action := range typed.Actions {
lines = append(lines, fmt.Sprintf("%d. %s", i+1, action))
}
lines = append(lines, "示例:")
for i, example := range typed.Examples {
lines = append(lines, fmt.Sprintf("%d. %s", i+1, example))
}
_, writeErr := fmt.Fprintln(w, strings.Join(lines, "\n"))
return writeErr
}
// Line 1: Error summary
lines := []string{
fmt.Sprintf("%s %s", tui.StateMark("error"), tui.Danger(fmt.Sprintf("Error: [%s] %s", strings.ToUpper(string(typed.Category)), typed.Message))),
-40
View File
@@ -192,46 +192,6 @@ func TestPrintJSON_AvailableFlags(t *testing.T) {
}
}
func TestPrintJSON_FourPartGuidance(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintJSON(&b, NewValidation(
"缺少参数",
WithReason("必须提供时间范围"),
WithActions("补充开始时间", "补充结束时间"),
WithExamples(`dws chat message list-all --start "..." --end "..." --format json`),
)); err != nil {
t.Fatalf("PrintJSON() error = %v", err)
}
got := b.String()
for _, want := range []string{`"error_message"`, `"reason"`, `"suggested_actions"`, `"examples"`} {
if !strings.Contains(got, want) {
t.Fatalf("four-part JSON output missing %s: %q", want, got)
}
}
}
func TestPrintHuman_FourPartGuidance(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintHuman(&b, NewValidation(
"缺少参数",
WithReason("必须提供时间范围"),
WithActions("补充开始和结束时间"),
WithExamples(`dws chat message list-all --start "..." --end "..."`),
)); err != nil {
t.Fatalf("PrintHuman() error = %v", err)
}
got := b.String()
for _, want := range []string{"错误信息:", "原因:", "建议操作:", "示例:"} {
if !strings.Contains(got, want) {
t.Fatalf("four-part human output missing %q: %q", want, got)
}
}
}
func TestPrintHuman(t *testing.T) {
t.Parallel()
-28
View File
@@ -308,13 +308,6 @@ func notLoggedInHint() string {
return "请先登录:dws auth login"
}
// SuggestBusinessHint returns an actionable recovery hint for a parsed MCP
// business-error payload. Runtime callers share this entry point so product
// helpers and the generic runner do not drift.
func SuggestBusinessHint(body map[string]any) string {
return suggestForBusinessErrorText(body)
}
func suggestForBusinessErrorText(body map[string]any) string {
msg := ""
if v, ok := body["errorMsg"].(string); ok {
@@ -323,19 +316,6 @@ func suggestForBusinessErrorText(body map[string]any) string {
msg = v
} else if v, ok := body["error"].(string); ok {
msg = v
} else if nested, ok := body["error"].(map[string]any); ok {
if code, ok := nested["code"].(string); ok {
msg = code
}
if message, ok := nested["message"].(string); ok {
msg = strings.TrimSpace(msg + " " + message)
}
}
if summary, ok := body["summary"].(string); ok {
msg = strings.TrimSpace(msg + " " + summary)
}
if code, ok := body["code"].(string); ok {
msg = strings.TrimSpace(msg + " " + code)
}
switch {
case strings.Contains(msg, "搜索内容不能为空"):
@@ -346,14 +326,6 @@ func suggestForBusinessErrorText(body map[string]any) string {
return "API rate limit exceeded, wait a moment and retry"
case strings.Contains(msg, "参数错误") || strings.Contains(msg, "param error"):
return "Check input parameters. Use --help for available flags"
case strings.Contains(msg, "listRoles null"):
return "当前群的群身份或权限上下文不可用。请先用 dws chat group list-my-groups --format json 选择当前账号实际加入或管理的群,再核对群成员与权限。"
case strings.Contains(msg, "OpendId is not in conversation") || strings.Contains(msg, "OpenId is not in conversation"):
return "当前账号不在该会话中。请先用 dws chat group list-my-groups --format json 选择实际加入的群,并重新获取该会话中的真实 OpendId。"
case strings.Contains(msg, "The operator is not in this group chat"):
return "当前操作者不在源群中。请重新选择当前账号已加入的群,或先完成入群;不要只替换接收方后重复原命令。"
case strings.Contains(msg, "targetOpenConversationId和receiverUid不能同时为空"):
return "分享群邀请链接必须提供接收目标:群到群使用 --target,群到人使用 --receiver;同时确认 --source 是当前操作者已加入的源群。"
default:
return "MCP tool returned a business error; check parameters and refer to skill documentation."
}
+3 -1
View File
@@ -25,7 +25,9 @@ import (
)
// MetaFileName is the on-disk name of the bus metadata file. It lives
// alongside bus.lock and bus.sock inside the bus working directory.
// alongside bus.lock inside the bus working directory. Unix bus sockets live
// in a private per-user runtime directory so shared config filesystems do not
// need socket support.
const MetaFileName = "bus.meta"
// Meta is the JSON document written once at bus startup. Its primary
+3 -3
View File
@@ -30,9 +30,9 @@ import (
type SpawnFunc func(SpawnConfig) (pid int, err error)
// DiscoverConfig describes one discover attempt. WorkDir holds bus.lock and
// usually (on Unix) bus.sock — see dwsevent.IPCEndpoint for the short-path
// fallback when WorkDir is too deep; the caller must mkdir it with
// pkg/config.DirPerm beforehand.
// persistent bus metadata; Unix sockets live in a private per-user runtime
// directory so WorkDir may reside on a shared filesystem without socket
// support. The caller must mkdir WorkDir with pkg/config.DirPerm beforehand.
type DiscoverConfig struct {
WorkDir string
IPCEndpoint string
+2 -2
View File
@@ -69,8 +69,8 @@ type BusEntry struct {
// IPCEndpoint returns the IPC endpoint for this entry. Delegates to
// dwsevent.IPCEndpoint so status/stop dial exactly where consume and the
// bus daemon bound (including the short-path fallback when WorkDir is too
// deep for sun_path).
// bus daemon bound (a private per-user runtime path on Unix and a named pipe
// on Windows).
func (e BusEntry) IPCEndpoint() string {
hash := e.ClientIDHash
if e.IdentityHash != "" {
+36 -14
View File
@@ -17,8 +17,16 @@ import (
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
)
const eventRuntimeDirPrefix = "dws-event-"
func currentUserID() string {
return strconv.Itoa(os.Geteuid())
}
// MaxUnixSocketPath returns the longest Unix socket path accepted by
// bind/connect on this OS (Go rejects longer names with EINVAL before
// the syscall). sockaddr_un.sun_path is 104 bytes on darwin and the
@@ -35,17 +43,19 @@ func maxUnixSocketPath(goos string) int {
}
// IPCEndpoint returns the bus IPC endpoint for one identity: a Named Pipe
// name on Windows, otherwise bus.sock inside workDir.
// name on Windows, otherwise a deterministic Unix socket under a private
// per-user runtime directory.
//
// The canonical Unix location is <workDir>/bus.sock, but workDir derives
// from the config dir, which can be arbitrarily deep (e.g. dwssb sandboxes
// use ~/.dwssb/sandboxes/<name>/config/...). When the canonical path would
// exceed the OS sun_path limit, the socket falls back to a short
// deterministic path under os.TempDir keyed by a hash of workDir, so every
// process (consume parent, forked _bus child, status/stop tooling) that
// derives the endpoint from the same workDir agrees on the location.
// bus.lock / bus.meta / bus.log always stay in workDir — only the socket
// moves.
// Unix sockets must live on a local filesystem that supports bind(2).
// Config directories may reside on NFS, CSI, FUSE, or other shared mounts
// that reject Unix socket creation with ENOTSUPP. On Unix, the endpoint uses
// XDG_RUNTIME_DIR when it is absolute and short enough; otherwise it falls
// back to a per-UID directory under os.TempDir. The transport creates and
// validates that directory as owner-only before listening or dialing. The
// socket name is keyed by a hash of workDir so every process (consume parent,
// forked _bus child, status/stop tooling) that derives the endpoint from the
// same workDir agrees on the location. bus.lock / bus.meta / bus.log always
// stay in workDir.
//
// This is the single source of truth for endpoint derivation; the cobra
// layer and busctl must not re-implement the shape.
@@ -60,9 +70,21 @@ func ipcEndpointForOS(goos, workDir, editionName string, sourceKind SourceKind,
if goos == "windows" {
return `\\.\pipe\dws-event-` + editionName + "-" + string(sourceKind) + "-" + identityHash
}
sock := filepath.Join(workDir, "bus.sock")
if len(sock) <= maxUnixSocketPath(goos) {
return sock
return unixSocketEndpoint(goos, workDir, strings.TrimSpace(os.Getenv("XDG_RUNTIME_DIR")), os.TempDir())
}
func unixSocketEndpoint(goos, workDir, runtimeDir, tempDir string) string {
socketName := "dws-evt-" + IdentityHash(workDir) + ".sock"
userDirName := eventRuntimeDirPrefix + currentUserID()
if filepath.IsAbs(runtimeDir) {
candidate := filepath.Join(runtimeDir, userDirName, socketName)
if len(candidate) <= maxUnixSocketPath(goos) {
return candidate
}
}
return filepath.Join(os.TempDir(), "dws-evt-"+IdentityHash(workDir)+".sock")
fallback := filepath.Join(tempDir, userDirName, socketName)
if len(fallback) <= maxUnixSocketPath(goos) {
return fallback
}
return filepath.Join("/tmp", userDirName, socketName)
}
+19 -1
View File
@@ -14,6 +14,7 @@
package event
import (
"os"
"path/filepath"
"strings"
"testing"
@@ -33,10 +34,27 @@ func TestCrossPlatformCoverageEndpointPortableCoverageEdges(t *testing.T) {
if got := ipcEndpointForOS("windows", "ignored", "open", "", "hash"); got != `\\.\pipe\dws-event-open-app_stream-hash` {
t.Fatalf("Windows endpoint = %q", got)
}
if got := ipcEndpointForOS("darwin", "short", "open", SourceKindPersonalStream, "hash"); got != filepath.Join("short", "bus.sock") {
runtimeRoot := filepath.VolumeName(os.TempDir()) + string(filepath.Separator)
runtimeDir := filepath.Join(runtimeRoot, "dws-xdg-runtime")
t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
workDir := "portable-xdg-workdir"
wantXDG := filepath.Join(runtimeDir, eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got := ipcEndpointForOS("linux", workDir, "open", SourceKindPersonalStream, "hash"); got != wantXDG {
t.Fatalf("XDG Unix endpoint = %q, want %q", got, wantXDG)
}
t.Setenv("XDG_RUNTIME_DIR", "")
if got := ipcEndpointForOS("darwin", "short", "open", SourceKindPersonalStream, "hash"); got != filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash("short")+".sock") {
t.Fatalf("short Unix endpoint = %q", got)
}
if got := ipcEndpointForOS("darwin", strings.Repeat("x", 200), "open", SourceKindAppStream, "hash"); !strings.Contains(got, "dws-evt-") {
t.Fatalf("long Unix endpoint = %q", got)
}
longTempDir := filepath.Join(string(filepath.Separator), strings.Repeat("long-temp-root", 20))
wantShortFallback := filepath.Join("/tmp", eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got := unixSocketEndpoint("darwin", workDir, "", longTempDir); got != wantShortFallback {
t.Fatalf("overlong temp endpoint = %q, want %q", got, wantShortFallback)
}
}
+57 -5
View File
@@ -23,6 +23,7 @@ import (
)
func TestEndpointPlatformVariants(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "")
if maxUnixSocketPath("linux") != 107 || maxUnixSocketPath("darwin") != 103 {
t.Fatal("Unix socket limits changed")
}
@@ -31,7 +32,7 @@ func TestEndpointPlatformVariants(t *testing.T) {
t.Fatalf("Windows pipe = %q", pipe)
}
short := ipcEndpointForOS("darwin", "/tmp/events", "open", SourceKindPersonalStream, "hash")
if short != filepath.Join("/tmp/events", "bus.sock") {
if short != filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash("/tmp/events")+".sock") {
t.Fatalf("short Unix endpoint = %q", short)
}
long := ipcEndpointForOS("darwin", "/"+strings.Repeat("deep/", 40), "open", SourceKindAppStream, "hash")
@@ -40,16 +41,40 @@ func TestEndpointPlatformVariants(t *testing.T) {
}
}
func TestIPCEndpointShortWorkDirUsesCanonicalPath(t *testing.T) {
workDir := "/tmp/dws/events/open/app_stream/aabbccdd00112233"
func TestIPCEndpointUsesXDGUserRuntimeDir(t *testing.T) {
tempRoot, err := filepath.EvalSymlinks("/tmp")
if err != nil {
t.Fatalf("EvalSymlinks: %v", err)
}
runtimeDir, err := os.MkdirTemp(tempRoot, "dws-xdg-")
if err != nil {
t.Fatalf("MkdirTemp: %v", err)
}
t.Cleanup(func() { _ = os.RemoveAll(runtimeDir) })
t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
workDir := "/shared/events/open/app_stream/aabbccdd00112233"
got := IPCEndpoint(workDir, "open", SourceKindAppStream, "aabbccdd00112233")
want := filepath.Join(workDir, "bus.sock")
want := filepath.Join(runtimeDir, eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got != want {
t.Fatalf("IPCEndpoint = %q, want %q", got, want)
}
}
func TestIPCEndpointLongWorkDirFallsBackUnderTempDir(t *testing.T) {
func TestIPCEndpointWithoutXDGUsesPerUserLocalTempDir(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "")
workDir := "/tmp/dws/events/open/app_stream/aabbccdd00112233"
got := IPCEndpoint(workDir, "open", SourceKindAppStream, "aabbccdd00112233")
want := filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got != want {
t.Fatalf("IPCEndpoint = %q, want %q", got, want)
}
if strings.HasPrefix(got, workDir) {
t.Fatalf("IPCEndpoint = %q, want endpoint outside workDir", got)
}
}
func TestIPCEndpointLongWorkDirUsesLocalTempDir(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "")
// Mirrors the dwssb sandbox layout that produced a 111-byte socket
// path — over macOS's 103-byte usable sun_path budget.
workDir := "/Users/zhengyubai/.dwssb/sandboxes/event-subscribe/config/events/open/personal_stream/3928ce0fb4860a52"
@@ -66,6 +91,7 @@ func TestIPCEndpointLongWorkDirFallsBackUnderTempDir(t *testing.T) {
}
func TestIPCEndpointFallbackIsDeterministicPerWorkDir(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "")
long := strings.Repeat("x", 120)
a := IPCEndpoint("/base/"+long+"/one", "open", SourceKindPersonalStream, "hash")
b := IPCEndpoint("/base/"+long+"/one", "open", SourceKindPersonalStream, "hash")
@@ -77,3 +103,29 @@ func TestIPCEndpointFallbackIsDeterministicPerWorkDir(t *testing.T) {
t.Fatalf("different workDirs collided on endpoint %q", a)
}
}
func TestIPCEndpointLongXDGPathFallsBackToTempDir(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "/"+strings.Repeat("runtime/", 30))
workDir := "/shared/events/open/personal_stream/aabbccdd00112233"
got := ipcEndpointForOS("linux", workDir, "open", SourceKindPersonalStream, "hash")
wantPrefix := filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID()) + string(filepath.Separator)
if !strings.HasPrefix(got, wantPrefix) {
t.Fatalf("IPCEndpoint = %q, want fallback under %q", got, wantPrefix)
}
if len(got) > maxUnixSocketPath("linux") {
t.Fatalf("fallback path still too long: %d > %d (%q)", len(got), maxUnixSocketPath("linux"), got)
}
}
func TestIPCEndpointLongTempDirUsesShortSystemFallback(t *testing.T) {
workDir := "/shared/events/open/personal_stream/aabbccdd00112233"
longTempDir := "/" + strings.Repeat("long-temp-root/", 20)
got := unixSocketEndpoint("linux", workDir, "", longTempDir)
want := filepath.Join("/tmp", eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got != want {
t.Fatalf("IPCEndpoint = %q, want short fallback %q", got, want)
}
if len(got) > maxUnixSocketPath("linux") {
t.Fatalf("short fallback path too long: %d > %d (%q)", len(got), maxUnixSocketPath("linux"), got)
}
}
@@ -73,6 +73,83 @@ func TestCrossPlatformCoverageUnixListenErrorCoverage(t *testing.T) {
}
}
func TestCrossPlatformCoverageUnixSocketDirectoryErrorCoverage(t *testing.T) {
oldLstat, oldRuntimeStat, oldMkdir := lstatSocketPath, statSocketRuntimeRoot, mkdirSocketDir
t.Cleanup(func() {
lstatSocketPath, statSocketRuntimeRoot, mkdirSocketDir = oldLstat, oldRuntimeStat, oldMkdir
})
wantErr := errors.New("synthetic socket directory failure")
if err := ensureSocketDir("relative/bus.sock", true); err == nil || !strings.Contains(err.Error(), "must be absolute") {
t.Fatalf("relative socket path error = %v", err)
}
root := shortSecureTempDir(t)
missingRootPath := filepath.Join(root, "missing-root", "dws-event-test", "bus.sock")
if err := ensureSocketDir(missingRootPath, false); err == nil || !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing runtime root error = %v", err)
}
rootInfo, err := oldLstat(root)
if err != nil {
t.Fatalf("lstat secure root: %v", err)
}
lstatSocketPath = func(path string) (os.FileInfo, error) {
if path == "/tmp" {
return fileInfoWithMode{FileInfo: rootInfo, mode: os.ModeSymlink | 0o777}, nil
}
return oldLstat(path)
}
statSocketRuntimeRoot = func(path string) (os.FileInfo, error) {
if path == "/tmp" {
return nil, wantErr
}
return oldRuntimeStat(path)
}
if err := ensureSocketDir("/tmp/dws-event-coverage/bus.sock", false); !errors.Is(err, wantErr) {
t.Fatalf("runtime root resolution error = %v", err)
}
lstatSocketPath, statSocketRuntimeRoot = oldLstat, oldRuntimeStat
rootFile := filepath.Join(root, "runtime-root-file")
if err := os.WriteFile(rootFile, []byte("not a directory"), 0o600); err != nil {
t.Fatalf("write runtime root file: %v", err)
}
if err := ensureSocketDir(filepath.Join(rootFile, "dws-event-test", "bus.sock"), false); err == nil || !strings.Contains(err.Error(), "runtime root is not a directory") {
t.Fatalf("non-directory runtime root error = %v", err)
}
mkdirSocketDir = func(string, os.FileMode) error { return wantErr }
if err := ensureSocketDir(filepath.Join(root, "mkdir-failure", "bus.sock"), true); !errors.Is(err, wantErr) {
t.Fatalf("socket directory creation error = %v", err)
}
mkdirSocketDir = oldMkdir
if err := ensureSocketDir(filepath.Join(root, "missing-socket-dir", "bus.sock"), false); err == nil || !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing socket directory error = %v", err)
}
withoutOwner := fileInfoWithoutOwner{FileInfo: rootInfo}
if err := validateSocketRuntimeRoot(root, withoutOwner, uint32(os.Geteuid())); err == nil || !strings.Contains(err.Error(), "owner") {
t.Fatalf("runtime root owner error = %v", err)
}
if err := validatePrivateSocketDir(root, withoutOwner, uint32(os.Geteuid())); err == nil || !strings.Contains(err.Error(), "owner") {
t.Fatalf("socket directory owner error = %v", err)
}
}
type fileInfoWithMode struct {
os.FileInfo
mode os.FileMode
}
func (f fileInfoWithMode) Mode() os.FileMode { return f.mode }
func (f fileInfoWithMode) IsDir() bool { return f.mode.IsDir() }
type fileInfoWithoutOwner struct{ os.FileInfo }
func (fileInfoWithoutOwner) Sys() any { return struct{}{} }
type stubNetListener struct {
close func() error
}
+98 -5
View File
@@ -16,9 +16,12 @@
package transport
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"syscall"
dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/config"
@@ -30,10 +33,13 @@ type unixListener struct {
}
var (
statSocket = os.Stat
removeSocket = os.Remove
listenUnix = net.Listen
chmodSocket = os.Chmod
statSocket = os.Stat
removeSocket = os.Remove
listenUnix = net.Listen
chmodSocket = os.Chmod
lstatSocketPath = os.Lstat
statSocketRuntimeRoot = os.Stat
mkdirSocketDir = os.Mkdir
)
func (u *unixListener) Accept() (net.Conn, error) { return u.l.Accept() }
@@ -56,11 +62,95 @@ func checkSocketPath(path string) error {
return nil
}
// ensureSocketDir makes the socket's immediate parent an owner-only
// directory and rejects unsafe pre-existing paths. The parent of that
// directory must itself either be private to the effective user (for
// XDG_RUNTIME_DIR and macOS temporary roots) or sticky (for Linux /tmp), so
// another user cannot rename the private directory out from under us.
func ensureSocketDir(path string, create bool) error {
if !filepath.IsAbs(path) {
return fmt.Errorf("transport: unix socket path must be absolute: %s", path)
}
dir := filepath.Dir(path)
root := filepath.Dir(dir)
rootInfo, err := lstatSocketPath(root)
if err != nil {
return fmt.Errorf("transport: inspect socket runtime root %s: %w", root, err)
}
// macOS exposes the system /tmp as a root-owned symlink to /private/tmp.
// Follow only that well-known alias, then apply the same ownership/sticky
// validation to its target. Arbitrary runtime-root symlinks remain rejected.
if rootInfo.Mode()&os.ModeSymlink != 0 && filepath.Clean(root) == "/tmp" {
rootInfo, err = statSocketRuntimeRoot(root)
if err != nil {
return fmt.Errorf("transport: resolve socket runtime root %s: %w", root, err)
}
}
if err := validateSocketRuntimeRoot(root, rootInfo, uint32(os.Geteuid())); err != nil {
return err
}
if create {
if err := mkdirSocketDir(dir, config.DirPerm); err != nil && !errors.Is(err, os.ErrExist) {
return fmt.Errorf("transport: create socket directory %s: %w", dir, err)
}
}
dirInfo, err := lstatSocketPath(dir)
if err != nil {
return fmt.Errorf("transport: inspect socket directory %s: %w", dir, err)
}
return validatePrivateSocketDir(dir, dirInfo, uint32(os.Geteuid()))
}
func validateSocketRuntimeRoot(path string, info os.FileInfo, effectiveUID uint32) error {
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("transport: socket runtime root is not a directory: %s", path)
}
owner, err := fileOwnerUID(info)
if err != nil {
return fmt.Errorf("transport: inspect socket runtime root owner %s: %w", path, err)
}
privateOwnerRoot := owner == effectiveUID && info.Mode().Perm()&0o022 == 0
stickyRoot := info.Mode()&os.ModeSticky != 0
if !privateOwnerRoot && !stickyRoot {
return fmt.Errorf("transport: socket runtime root is neither private nor sticky: %s", path)
}
return nil
}
func validatePrivateSocketDir(path string, info os.FileInfo, effectiveUID uint32) error {
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("transport: socket directory is not a directory: %s", path)
}
owner, err := fileOwnerUID(info)
if err != nil {
return fmt.Errorf("transport: inspect socket directory owner %s: %w", path, err)
}
if owner != effectiveUID {
return fmt.Errorf("transport: socket directory %s is owned by uid %d, want %d", path, owner, effectiveUID)
}
if perm := info.Mode().Perm(); perm != config.DirPerm {
return fmt.Errorf("transport: socket directory %s has permissions %04o, want %04o", path, perm, config.DirPerm)
}
return nil
}
func fileOwnerUID(info os.FileInfo) (uint32, error) {
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return 0, errors.New("stat result does not expose an owner uid")
}
return stat.Uid, nil
}
func listen(path string) (Listener, error) {
if err := checkSocketPath(path); err != nil {
return nil, err
}
// Stale socket cleanup. Caller holds bus.lock so this is race-safe.
if err := ensureSocketDir(path, true); err != nil {
return nil, err
}
// The private per-user parent excludes other users. The caller's bus.lock
// serializes stale-socket cleanup for processes using the same WorkDir.
if _, err := statSocket(path); err == nil {
if err := removeSocket(path); err != nil {
return nil, fmt.Errorf("transport: remove stale socket %s: %w", path, err)
@@ -82,5 +172,8 @@ func dial(path string) (net.Conn, error) {
if err := checkSocketPath(path); err != nil {
return nil, err
}
if err := ensureSocketDir(path, false); err != nil {
return nil, err
}
return net.Dial("unix", path)
}
+138 -5
View File
@@ -21,12 +21,32 @@ import (
"net"
"os"
"path/filepath"
"strings"
"sync"
"testing"
dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event"
)
func shortSecureTempDir(t *testing.T) string {
t.Helper()
tempRoot, err := filepath.EvalSymlinks("/tmp")
if err != nil {
t.Fatalf("EvalSymlinks: %v", err)
}
dir, err := os.MkdirTemp(tempRoot, "dws-et-")
if err != nil {
t.Fatalf("MkdirTemp: %v", err)
}
if err := os.Chmod(dir, 0o700); err != nil {
t.Fatalf("chmod temp dir: %v", err)
}
t.Cleanup(func() { _ = os.RemoveAll(dir) })
return dir
}
func TestListen_DialRoundtrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "bus.sock")
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
l, err := Listen(path)
if err != nil {
t.Fatalf("Listen: %v", err)
@@ -87,7 +107,7 @@ func TestListen_DialRoundtrip(t *testing.T) {
}
func TestListen_StaleSocketCleanup(t *testing.T) {
path := filepath.Join(t.TempDir(), "bus.sock")
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
// Pre-create a stale file at path (not a valid socket).
if err := os.WriteFile(path, []byte("stale"), 0o600); err != nil {
t.Fatalf("pre-create: %v", err)
@@ -99,8 +119,121 @@ func TestListen_StaleSocketCleanup(t *testing.T) {
defer l.Close()
}
func TestCrossPlatformCoverageListenCreatesPrivateSocketDirectory(t *testing.T) {
dir := filepath.Join(shortSecureTempDir(t), "dws-event-test")
path := filepath.Join(dir, "bus.sock")
l, err := Listen(path)
if err != nil {
t.Fatalf("Listen: %v", err)
}
defer l.Close()
st, err := os.Stat(dir)
if err != nil {
t.Fatalf("stat socket directory: %v", err)
}
if mode := st.Mode().Perm(); mode != 0o700 {
t.Fatalf("socket directory mode = %04o, want 0700", mode)
}
}
func TestCrossPlatformCoverageListenRejectsWorldAccessibleSocketDirectory(t *testing.T) {
dir := filepath.Join(shortSecureTempDir(t), "dws-event-test")
if err := os.Mkdir(dir, 0o700); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.Chmod(dir, 0o777); err != nil {
t.Fatalf("chmod: %v", err)
}
if _, err := Listen(filepath.Join(dir, "bus.sock")); err == nil || !strings.Contains(err.Error(), "want 0700") {
t.Fatalf("Listen error = %v, want 0700 directory rejection", err)
}
}
func TestCrossPlatformCoverageDialRejectsWorldAccessibleSocketDirectory(t *testing.T) {
dir := filepath.Join(shortSecureTempDir(t), "dws-event-test")
if err := os.Mkdir(dir, 0o700); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.Chmod(dir, 0o777); err != nil {
t.Fatalf("chmod: %v", err)
}
if _, err := Dial(filepath.Join(dir, "bus.sock")); err == nil || !strings.Contains(err.Error(), "want 0700") {
t.Fatalf("Dial error = %v, want 0700 directory rejection", err)
}
}
func TestCrossPlatformCoverageListenRejectsSymlinkSocketDirectory(t *testing.T) {
root := shortSecureTempDir(t)
target := filepath.Join(root, "target")
if err := os.Mkdir(target, 0o700); err != nil {
t.Fatalf("mkdir target: %v", err)
}
link := filepath.Join(root, "dws-event-test")
if err := os.Symlink(target, link); err != nil {
t.Fatalf("symlink: %v", err)
}
if _, err := Listen(filepath.Join(link, "bus.sock")); err == nil || !strings.Contains(err.Error(), "not a directory") {
t.Fatalf("Listen error = %v, want symlink directory rejection", err)
}
}
func TestCrossPlatformCoverageValidatePrivateSocketDirRejectsDifferentOwner(t *testing.T) {
dir := shortSecureTempDir(t)
st, err := os.Lstat(dir)
if err != nil {
t.Fatalf("lstat: %v", err)
}
otherUID := uint32(os.Geteuid() + 1)
if err := validatePrivateSocketDir(dir, st, otherUID); err == nil || !strings.Contains(err.Error(), "is owned by uid") {
t.Fatalf("validatePrivateSocketDir error = %v, want owner mismatch", err)
}
}
func TestCrossPlatformCoverageListenRejectsUntrustedRuntimeRoot(t *testing.T) {
root := filepath.Join(shortSecureTempDir(t), "untrusted")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatalf("mkdir root: %v", err)
}
if err := os.Chmod(root, 0o777); err != nil {
t.Fatalf("chmod root: %v", err)
}
dir := filepath.Join(root, "dws-event-test")
if err := os.Mkdir(dir, 0o700); err != nil {
t.Fatalf("mkdir socket dir: %v", err)
}
if _, err := Listen(filepath.Join(dir, "bus.sock")); err == nil || !strings.Contains(err.Error(), "neither private nor sticky") {
t.Fatalf("Listen error = %v, want untrusted runtime root rejection", err)
}
}
func TestCrossPlatformCoverageListenSharedWorkDirUsesLocalSecureRuntimeEndpoint(t *testing.T) {
root := shortSecureTempDir(t)
runtimeDir := filepath.Join(root, "runtime")
if err := os.Mkdir(runtimeDir, 0o700); err != nil {
t.Fatalf("mkdir runtime: %v", err)
}
t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
sharedWorkDir := filepath.Join(root, "simulated-nfs", "events", "open", "personal_stream", "identity")
endpoint := dwsevent.IPCEndpoint(sharedWorkDir, "open", dwsevent.SourceKindPersonalStream, "identity")
if strings.HasPrefix(endpoint, sharedWorkDir) {
t.Fatalf("endpoint = %q, want socket outside shared WorkDir %q", endpoint, sharedWorkDir)
}
l, err := Listen(endpoint)
if err != nil {
t.Fatalf("Listen on local runtime endpoint: %v", err)
}
defer l.Close()
if mode, err := os.Stat(filepath.Dir(endpoint)); err != nil {
t.Fatalf("stat runtime socket directory: %v", err)
} else if mode.Mode().Perm() != 0o700 {
t.Fatalf("runtime socket directory mode = %04o, want 0700", mode.Mode().Perm())
}
}
func TestListen_CloseUnlinksSocket(t *testing.T) {
path := filepath.Join(t.TempDir(), "bus.sock")
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
l, err := Listen(path)
if err != nil {
t.Fatalf("Listen: %v", err)
@@ -114,7 +247,7 @@ func TestListen_CloseUnlinksSocket(t *testing.T) {
}
func TestDial_NoServerReturnsError(t *testing.T) {
path := filepath.Join(t.TempDir(), "nonexistent.sock")
path := filepath.Join(shortSecureTempDir(t), "nonexistent.sock")
if _, err := Dial(path); err == nil {
t.Fatal("Dial to nonexistent socket should error")
}
@@ -124,7 +257,7 @@ func TestDial_NoServerReturnsError(t *testing.T) {
// surfaces as io.EOF to the server's Reader — the EOF signal is what bus
// uses to unregister dead consumers (plan invariant #5).
func TestReader_HandlesPeerCloseEOF(t *testing.T) {
path := filepath.Join(t.TempDir(), "bus.sock")
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
l, err := Listen(path)
if err != nil {
t.Fatalf("Listen: %v", err)
+13 -1
View File
@@ -853,6 +853,7 @@ func newAitableCommand() *cobra.Command {
dws aitable form [list|delete|update] 表单管理
dws aitable form field [list|update|hide] 表单字段管理
dws aitable form share [get|update|notify] 表单分享管理
dws aitable workflow [edit-example|create|update|enable|disable|get|list] 自动化工作流管理
dws aitable dashboard [get|create|update|delete|config-example] 仪表盘管理
dws aitable chart [get|create|update|delete|widgets-example] 图表管理
dws aitable export data 数据导出
@@ -3260,6 +3261,17 @@ valid=false 仍表示 DSL 校验或发布未通过,必须读取 issues 修正
},
}
workflowEditExampleCmd := &cobra.Command{
Use: "edit-example",
Short: "获取工作流编辑文档与示例",
Long: `返回服务端提供的 AI 表格工作流编辑文档与示例。
可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。`,
Example: ` dws aitable workflow edit-example`,
RunE: func(cmd *cobra.Command, args []string) error {
return callAitableTool("edit_workflow_example", map[string]any{})
},
}
workflowUpdateCmd := &cobra.Command{
Use: "update",
Short: "更新并发布已有自动化工作流",
@@ -4856,7 +4868,7 @@ parentSectionId 为空串表示该节点在 Base 根目录下。
workflowListCmd.Flags().Int("limit", 0, "分页大小 [1, 100],不传走服务端默认 20")
workflowListCmd.Flags().Int("offset", 0, "分页偏移量,>= 0,不传走服务端默认 0")
workflowCmd.AddCommand(
workflowCreateCmd, workflowUpdateCmd,
workflowEditExampleCmd, workflowCreateCmd, workflowUpdateCmd,
workflowEnableCmd, workflowDisableCmd,
workflowGetCmd, workflowListCmd,
)
@@ -94,6 +94,23 @@ func TestAitableWorkflowCreateMapsDSLWithoutRetry(t *testing.T) {
}
}
func TestAitableWorkflowEditExampleMapsEmptyArguments(t *testing.T) {
caller, err := runAitableWorkflowCommand(t, nil, "edit-example")
if err != nil {
t.Fatalf("workflow edit-example returned error: %v", err)
}
if len(caller.calls) != 1 {
t.Fatalf("tool call count = %d, want 1", len(caller.calls))
}
call := caller.calls[0]
if call.productID != "aitable" || call.toolName != "edit_workflow_example" {
t.Fatalf("tool call = %s/%s, want aitable/edit_workflow_example", call.productID, call.toolName)
}
if len(call.args) != 0 {
t.Fatalf("tool args = %#v, want empty arguments", call.args)
}
}
func TestAitableWorkflowUpdateReadsDSLFile(t *testing.T) {
path := t.TempDir() + "/workflow.json"
if err := os.WriteFile(path, []byte(`{"version":"workflow-dsl/v1","name":"updated"}`), 0o600); err != nil {
+22 -259
View File
@@ -56,20 +56,11 @@ const maxConversationCategoryTitleRunes = 15
func validatedConversationCategoryTitle(raw string) (string, error) {
title := strings.TrimSpace(raw)
if title == "" {
return "", apperrors.NewValidation(
"--title 不能为空",
apperrors.WithReason("invalid_category_title"),
apperrors.WithHint("请提供 1 到 15 个字符的分组标题,并保持用户指定原文。"),
apperrors.WithActions("补充非空 --title", "运行当前命令 --help 查看示例"),
)
return "", apperrors.NewValidation("--title 不能为空")
}
if count := utf8.RuneCountInString(title); count > maxConversationCategoryTitleRunes {
return "", apperrors.NewValidation(
fmt.Sprintf("--title 当前 %d 个字符,最多 %d 个字符", count, maxConversationCategoryTitleRunes),
apperrors.WithReason("category_title_too_long"),
apperrors.WithHint("不得静默截断、缩写或改写用户指定名称;请让用户提供合法标题后重试。"),
apperrors.WithActions("请用户将标题缩短到 15 个字符以内", "使用用户确认后的标题原文重试"),
)
if utf8.RuneCountInString(title) > maxConversationCategoryTitleRunes {
return "", apperrors.NewValidation(fmt.Sprintf(
"--title 最多 %d 个字符", maxConversationCategoryTitleRunes))
}
return title, nil
}
@@ -340,25 +331,6 @@ func containsMessageMention(text, placeholder string) bool {
}
}
func chatGuidanceError(message, reason string, actions, examples []string) error {
return apperrors.NewValidation(
message,
apperrors.WithReason(reason),
apperrors.WithActions(actions...),
apperrors.WithExamples(examples...),
)
}
func isLikelyPlaceholderID(value string) bool {
normalized := strings.ToLower(strings.TrimSpace(value))
return normalized == "" ||
normalized == "0" ||
strings.Contains(normalized, "placeholder") ||
strings.HasPrefix(normalized, "test_") ||
strings.HasPrefix(normalized, "test-") ||
strings.HasPrefix(normalized, "<")
}
func resolveOpenDingTalkID(ctx context.Context, value string) (string, error) {
ids, err := resolveOpenDingTalkIDs(ctx, []string{value})
if err != nil {
@@ -462,12 +434,9 @@ func guardGroupOwnerRemoval(ctx context.Context, openConversationID string, remo
if err != nil || ownerOpenID == "" {
return nil
}
ownerErr := apperrors.NewValidation(
"被移除列表包含群主,不能直接移出群主",
apperrors.WithReason("group_owner_in_remove_list"),
apperrors.WithHint("清理临时群成员时应从 --users 中移除群主,只移除本次加入的普通成员;只有用户明确要求变更群主时才单独执行 transfer-owner。"),
apperrors.WithActions("从 --users 中移除群主后重试", "若用户明确要求转让群主,先确认新群主再单独执行 transfer-owner"),
apperrors.WithExamples(fmt.Sprintf("dws chat group members remove --id %s --users <普通成员userId列表> --format json", openConversationID)),
ownerErr := fmt.Errorf(
"refusing to remove the group owner: 被移除列表包含群主,移出群主将导致群无群主(孤儿群)\n hint: 先执行 dws chat group transfer-owner --group %s --user <newOwnerUserId> 转让群主后再移除",
openConversationID,
)
userIDs, openDingTalkIDs := splitChatIDValues(removeValues)
for _, id := range openDingTalkIDs {
@@ -1712,27 +1681,7 @@ func newChatCommand() *cobra.Command {
if atAll && !strings.Contains(text, "<@all>") {
text = "<@all> " + text
}
var addedMentions []string
for _, rawID := range atOpenIds {
id := strings.TrimSpace(rawID)
if id == "" {
continue
}
wrapped := "<@" + id + ">"
if !containsMessageMention(text, wrapped) && !containsMessageMention(text, "@"+id) {
addedMentions = append(addedMentions, wrapped)
}
}
if len(addedMentions) > 0 {
text = strings.Join(addedMentions, " ") + " " + text
fmt.Fprintf(
os.Stderr,
"错误信息:检测到 --at-open-dingtalk-ids,但正文缺少对应 @ 占位符;CLI 已自动补齐\n原因:钉钉群消息只有正文包含 <@openDingTalkId> 时才会真正展示 @ 提醒\n建议操作:\n1. 后续命令请在 --text 中显式写入对应占位符\n示例:\n1. dws chat message send --group <openConversationId> --at-open-dingtalk-ids %s --text %q --format json\n",
atOpenIdsStr,
text,
)
}
// 用户身份发消息要求 @ 占位符为 <@openDingTalkId>;模型若写成裸 @id 自动补全,已有 <@id> 保持不变
// 用户身份发消息要求 @ 占位符为 <@openDingTalkId>;模型若写成裸 @id 自动补全,已有 <@id> 不变
text = normalizeAtPlaceholders(text, atOpenIds, true)
// 群聊统一走 openDingTalkId @ 人接口。
contentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
@@ -2042,12 +1991,7 @@ func newChatCommand() *cobra.Command {
return fmt.Errorf("--sender-user-id and --sender-open-dingtalk-id are mutually exclusive, specify exactly one")
}
if senderUserID == "" && senderOpenDingTalkID == "" {
return chatGuidanceError(
"缺少消息发送者标识",
"list-by-sender 查询的是指定对方发送的消息,必须提供对方的 userId 或 openDingTalkId",
[]string{"使用 --sender-user-id 传入对方 userId", "或使用 --sender-open-dingtalk-id 传入对方 openDingTalkId"},
[]string{`dws chat message list-by-sender --sender-user-id <对方userId> --start "2026-07-14T00:00:00+08:00" --format json`},
)
return fmt.Errorf("--sender-user-id or --sender-open-dingtalk-id is required")
}
startMs, err := parseISOTimeToMillis("start", mustGetFlag(cmd, "start"))
if err != nil {
@@ -2238,25 +2182,6 @@ func newChatCommand() *cobra.Command {
# 查询单聊会话 ID: dws chat conversation-info --user <userId>
# 查询人员: dws contact user search --keyword "姓名" --format json`,
RunE: func(cmd *cobra.Command, args []string) error {
hasCondition := false
for _, name := range []string{
"query", "keyword", "user", "users", "userId", "sender-ids", "senders", "sender",
"at-ids", "conversation-ids", "conversation-id", "groups", "group", "message-type",
"conversation-type", "search-conv-type", "start", "end",
} {
if value, _ := cmd.Flags().GetString(name); strings.TrimSpace(value) != "" {
hasCondition = true
break
}
}
if !hasCondition {
atMe, _ := cmd.Flags().GetBool("at-me")
hasCondition = atMe || cmd.Flags().Changed("only-robot") || cmd.Flags().Changed("only-robot-messages")
}
if !hasCondition {
return apperrors.NewValidation("at least one search condition is required")
}
toolArgs := map[string]any{}
// The CLI primary is --query; the IM MCP field is still named "keyword".
@@ -2294,8 +2219,6 @@ func newChatCommand() *cobra.Command {
convIds := ""
if v, _ := cmd.Flags().GetString("conversation-ids"); v != "" {
convIds = v
} else if v, _ := cmd.Flags().GetString("conversation-id"); v != "" {
convIds = v
} else if v, _ := cmd.Flags().GetString("groups"); v != "" {
convIds = v
} else if v, _ := cmd.Flags().GetString("group"); v != "" {
@@ -2586,6 +2509,7 @@ func newChatCommand() *cobra.Command {
_ = chatGroupMemberRemoveCmd.MarkFlagRequired("users")
chatGroupCmd.AddCommand(chatGroupCreateCmd, chatGroupMembersCmd, chatGroupRenameCmd)
chatGroupCmd.AddCommand(hintSubCmd("search", "use: dws chat search --query <关键词>"))
chatGroupMembersCmd.AddCommand(chatGroupMemberAddCmd, chatGroupMemberRemoveCmd, chatGroupMembersAddBotCmd)
// message 子命令 flags
@@ -2820,8 +2744,6 @@ func newChatCommand() *cobra.Command {
chatMessageSearchAdvancedCmd.Flags().Bool("at-me", false, "只搜索 @我 的消息(可选,默认 false)")
chatMessageSearchAdvancedCmd.Flags().String("at-ids", "", "@指定人的 openDingTalkId 列表,逗号分隔(可选)")
chatMessageSearchAdvancedCmd.Flags().String("conversation-ids", "", "会话 openConversationId 列表,逗号分隔(可选,群聊或单聊均可,不传则搜索所有会话)")
chatMessageSearchAdvancedCmd.Flags().String("conversation-id", "", "--conversation-ids 的单值兼容别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("conversation-id")
chatMessageSearchAdvancedCmd.Flags().String("groups", "", "--conversation-ids 的别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("groups")
chatMessageSearchAdvancedCmd.Flags().String("group", "", "")
@@ -3054,7 +2976,6 @@ func newChatCommand() *cobra.Command {
chatCategoryDeleteCmd := &cobra.Command{
Use: "delete",
Short: "删除用户自定义会话分组",
Long: "删除用户自定义会话分组。该操作不可逆;必须先获得用户确认,再追加 --yes 执行。",
Example: ` dws chat category delete --category-id <分组ID>
# 分组ID 可通过 dws chat category list 获取`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -3062,14 +2983,6 @@ func newChatCommand() *cobra.Command {
if categoryId == 0 {
return fmt.Errorf("flag --category-id is required")
}
if !commandBoolFlag(cmd, "yes") {
return apperrors.NewValidation(
"删除会话分组不可逆;获得用户确认后加 --yes 执行",
apperrors.WithReason("confirmation_required"),
apperrors.WithHint("先确认目标分组及影响范围;用户明确同意后以相同参数追加 --yes"),
apperrors.WithActions("确认目标会话分组", "获得用户确认后使用 --yes 执行"),
)
}
return callMCPToolOnServer("im", "delete_conv_category", map[string]any{
"categoryId": categoryId,
})
@@ -3145,16 +3058,6 @@ func newChatCommand() *cobra.Command {
if err != nil {
return fmt.Errorf("--category-ids: %w", err)
}
for _, categoryID := range categoryIds {
if categoryID > 0 && categoryID < 1000 {
return chatGuidanceError(
"会话分组 ID 看起来仍是示例占位值",
"--category-ids 必须来自 chat category list 返回的真实分组 ID;123、456 等短示例值不能直接用于移出操作",
[]string{"先查询当前用户的会话分组", "从结果读取真实 categoryId 后再执行移出"},
[]string{`dws chat category list --format json`, `dws chat category remove-conv --group <openConversationId> --category-ids <categoryId> --format json`},
)
}
}
return callMCPToolOnServer("im", "remove_conv_from_categories", map[string]any{
"openConversationId": groupID,
"categoryIds": categoryIds,
@@ -3226,16 +3129,6 @@ func newChatCommand() *cobra.Command {
return err
}
msgIds := parseCSVValues(mustGetFlag(cmd, "msg-ids"))
for _, msgID := range msgIds {
if isLikelyPlaceholderID(msgID) {
return chatGuidanceError(
"消息 ID 仍是占位符,无法查询真实消息",
"--msg-ids 必须来自消息列表返回的真实 openMsgId,test_msg_id_placeholder 等示例值不会命中消息",
[]string{"先执行 chat message list 获取目标消息", "从结果读取 openMsgId 后替换占位符"},
[]string{`dws chat message list-by-ids --msg-ids <openMsgId> --format json`},
)
}
}
if len(msgIds) > 50 {
return fmt.Errorf("--msg-ids 最多支持 50 条,当前 %d 条", len(msgIds))
}
@@ -3259,14 +3152,6 @@ func newChatCommand() *cobra.Command {
if err := validateRequiredFlags(cmd, "msg-id", "emoji"); err != nil {
return err
}
if msgID := mustGetFlag(cmd, "msg-id"); isLikelyPlaceholderID(msgID) {
return chatGuidanceError(
"消息 ID 仍是占位符,无法添加表情回应",
"--msg-id 必须是目标消息真实的 openMsgId,不能使用测试占位符",
[]string{"先拉取目标会话消息", "读取目标消息的 openMsgId 后重试"},
[]string{`dws chat message add-emoji --conversation-id <openConversationId> --msg-id <openMsgId> --emoji "赞" --format json`},
)
}
return callMCPToolOnServer("im", "add_emoji_reaction", map[string]any{
"openConversationId": flagOrFallback(cmd, "conversation-id", "group", "id", "chat"),
"openMsgId": mustGetFlag(cmd, "msg-id"),
@@ -3549,14 +3434,6 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
conversationID := mustGetFlag(cmd, "open-conversation-id")
messageID := mustGetFlag(cmd, "message-id")
outputPath := mustGetFlag(cmd, "output")
if isLikelyPlaceholderID(resourceID) || isLikelyPlaceholderID(messageID) {
return chatGuidanceError(
"媒体资源参数仍包含占位符",
"--resource-id 和 --message-id 必须来自同一条真实消息,不能使用 test-media 或 test_msg_id_placeholder",
[]string{"先拉取包含媒体的目标消息", "从同一条消息读取 mediaId、openMessageId 和 openConversationId"},
[]string{`dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/ --format json`},
)
}
switch resourceType {
case "mediaId":
@@ -3669,12 +3546,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
newOwner = newOwnerUserID
}
if newOwner == "" {
return chatGuidanceError(
"缺少新群主标识",
"--new-owner 接收新群主 openDingTalkId,--user 接收新群主 userId,二者必须选择一个",
[]string{"先查询新群主的人员标识", "使用 --new-owner 或 --user 之一"},
[]string{`dws chat group transfer-owner --group <openConversationId> --new-owner <openDingTalkId> --format json`},
)
return fmt.Errorf("flag --new-owner or --user is required")
}
if !isOpenDingTalkID(newOwner) {
return callMCPToolOnServer("im", "transfer_group_owner", map[string]any{
@@ -3824,36 +3696,9 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
return fmt.Errorf("flag --status is required (0=关闭, 1=开启)")
}
status, _ := cmd.Flags().GetInt("status")
settingKey := mustGetFlag(cmd, "setting-key")
validSettingKeys := map[string]bool{
"authority": true, "joinValidation": true, "onlyAdminCanAtAll": true,
"searchable": true, "addFriendForbidden": true, "toolbarStatus": true,
"pluginCustomizeVerify": true, "onlyAdminCanDING": true,
"allMembersCanCreateMcsConf": true, "onlyAdminCanSetMsgTop": true,
"onlyAdminCanPinMsg": true, "onlyAdminCanSendFile": true,
"allMembersCanCreateCalendar": true, "groupEmailDisabled": true,
"groupRedEnvelopeSwitch": true, "groupLiveAuthority": true,
"groupBillAuthority": true,
}
if !validSettingKeys[settingKey] {
return chatGuidanceError(
"不支持的群设置项:"+settingKey,
"--setting-key 必须使用当前接口支持的精确枚举值,on 不是设置项名称",
[]string{"从 --help 列表选择合法 setting-key", "开启或关闭通过 --status 1/0 表达"},
[]string{`dws chat group update-settings --group <openConversationId> --setting-key searchable --status 1 --format json`},
)
}
if status != 0 && status != 1 {
return chatGuidanceError(
"群设置值只能是 0 或 1",
"--status 表示开关状态:0=关闭,1=开启;其他整数不会被服务端接受",
[]string{"关闭设置时传 --status 0", "开启设置时传 --status 1"},
[]string{`dws chat group update-settings --group <openConversationId> --setting-key searchable --status 1 --format json`},
)
}
return callMCPToolOnServer("im", "update_group_settings", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"settingKey": settingKey,
"settingKey": mustGetFlag(cmd, "setting-key"),
"status": status,
})
},
@@ -3942,14 +3787,6 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
if err := validateRequiredFlags(cmd, "src-conversation-id", "msg-id", "dest-conversation-id"); err != nil {
return err
}
if msgID := mustGetFlag(cmd, "msg-id"); isLikelyPlaceholderID(msgID) {
return chatGuidanceError(
"转发消息 ID 仍是占位符",
"--msg-id 必须是源会话中真实消息的 openMessageId",
[]string{"先拉取源会话消息", "确认消息属于 --src-conversation-id 后读取 openMessageId"},
[]string{`dws chat message forward --src-conversation-id <源会话ID> --msg-id <openMessageId> --dest-conversation-id <目标会话ID> --format json`},
)
}
toolArgs := map[string]any{
"srcOpenCid": mustGetFlag(cmd, "src-conversation-id"),
"srcOpenMessageId": mustGetFlag(cmd, "msg-id"),
@@ -4094,21 +3931,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
if !off {
muteTime, _ := cmd.Flags().GetInt64("mute-time")
if muteTime <= 0 {
return chatGuidanceError(
"禁言时必须提供 --mute-time",
"--mute-time 的单位是毫秒,仅支持 5 分钟、1 小时、1 天、7 天或 30 天对应的固定值",
[]string{"选择支持的毫秒值之一", "取消禁言时改用 --off"},
[]string{`dws chat group-mute-member --group <openConversationId> --user <userId> --mute-time 300000 --format json`},
)
}
validMuteTimes := map[int64]bool{300000: true, 3600000: true, 86400000: true, 604800000: true, 2592000000: true}
if !validMuteTimes[muteTime] {
return chatGuidanceError(
"不支持的禁言时长",
"--mute-time 使用毫秒且只支持 300000、3600000、86400000、604800000、2592000000;300 表示的时间不在支持范围内",
[]string{"5 分钟使用 300000", "从支持的五档时长中选择"},
[]string{`dws chat group-mute-member --group <openConversationId> --user <userId> --mute-time 300000 --format json`},
)
return fmt.Errorf("--mute-time is required when muting (supported: 300000/3600000/86400000/604800000/2592000000)")
}
toolArgs["muteTime"] = muteTime
}
@@ -4275,14 +4098,6 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
if err := validateRequiredFlags(cmd, "group", "role-id", "name"); err != nil {
return err
}
if roleID := mustGetFlag(cmd, "role-id"); isLikelyPlaceholderID(roleID) {
return chatGuidanceError(
"群身份 ID 仍是占位符",
"--role-id 必须来自 chat group-role list 返回的真实 openRoleId,0 不是有效群身份 ID",
[]string{"先列出目标群的群身份", "从结果读取 openRoleId 后重试"},
[]string{`dws chat group-role list --group <openConversationId> --format json`, `dws chat group-role update --group <openConversationId> --role-id <openRoleId> --name "新名称" --format json`},
)
}
return callMCPToolOnServer("im", "update_custom_group_role", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"openRoleId": mustGetFlag(cmd, "role-id"),
@@ -4577,28 +4392,9 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
if err := validateRequiredFlags(cmd, "src-conversation-id", "msg-ids", "dest-conversation-id"); err != nil {
return err
}
msgIDs := parseCSVValues(mustGetFlag(cmd, "msg-ids"))
if len(msgIDs) < 2 {
return chatGuidanceError(
"合并转发至少需要两条消息",
"--msg-ids 只有一条消息时不构成合并转发;单条消息应使用 message forward",
[]string{"提供至少两个来自同一源会话的 openMessageId", "只有一条时改用 message forward"},
[]string{`dws chat message combine-forward --src-conversation-id <源会话ID> --msg-ids <id1>,<id2> --dest-conversation-id <目标会话ID> --format json`},
)
}
for _, msgID := range msgIDs {
if isLikelyPlaceholderID(msgID) {
return chatGuidanceError(
"合并转发消息列表包含占位符",
"--msg-ids 必须全部是源会话中的真实 openMessageId",
[]string{"先拉取源会话消息", "选择至少两个真实消息 ID"},
[]string{`dws chat message combine-forward --src-conversation-id <源会话ID> --msg-ids <id1>,<id2> --dest-conversation-id <目标会话ID> --format json`},
)
}
}
toolArgs := map[string]any{
"srcOpenCid": mustGetFlag(cmd, "src-conversation-id"),
"srcOpenMessageIds": msgIDs,
"srcOpenMessageIds": parseCSVValues(mustGetFlag(cmd, "msg-ids")),
"destOpenCid": mustGetFlag(cmd, "dest-conversation-id"),
}
if v, _ := cmd.Flags().GetString("uuid"); v != "" {
@@ -4634,14 +4430,6 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
if err := validateRequiredFlags(cmd, "src-msg-id", "src-conversation-id", "src-thread-id", "dest-conversation-id"); err != nil {
return err
}
if msgID := mustGetFlag(cmd, "src-msg-id"); isLikelyPlaceholderID(msgID) {
return chatGuidanceError(
"话题转发的源消息 ID 仍是占位符",
"--src-msg-id 必须来自源话题中的真实 openMessageId,并与源会话和 thread-id 对应",
[]string{"先拉取源会话的话题消息", "从同一条消息读取 openMessageId 和 openConvThreadId"},
[]string{`dws chat message forward-topic --src-msg-id <openMessageId> --src-conversation-id <源会话ID> --src-thread-id <openConvThreadId> --dest-conversation-id <目标会话ID> --format json`},
)
}
toolArgs := map[string]any{
"srcOpenMessageId": mustGetFlag(cmd, "src-msg-id"),
"srcOpenConversationId": mustGetFlag(cmd, "src-conversation-id"),
@@ -4920,21 +4708,12 @@ status 可选值:
if err != nil {
return fmt.Errorf("--record-id must be a valid integer: %w", err)
}
status := mustGetFlag(cmd, "status")
if status != "AuditApprove" && status != "AuditDelete" {
return chatGuidanceError(
"不支持的入群审批状态:"+status,
"当前服务端仅接受大小写完全一致的 AuditApprove 或 AuditDelete;AuditRefuse 和 approve 均不可用",
[]string{"通过申请使用 AuditApprove", "拒绝或删除申请使用 AuditDelete,并可补充 --description"},
[]string{`dws chat group audit-join-validation --group <openConversationId> --record-id <recordId> --applicant <userId> --inviter <userId> --status AuditApprove --format json`},
)
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"applyRecordId": recordID,
"applicantUid": mustGetFlag(cmd, "applicant"),
"inviterUid": mustGetFlag(cmd, "inviter"),
"status": status,
"status": mustGetFlag(cmd, "status"),
}
if v, _ := cmd.Flags().GetString("description"); v != "" {
toolArgs["auditDescription"] = v
@@ -5060,7 +4839,7 @@ status 可选值:
chatClearMessagesCmd := &cobra.Command{
Use: "clear-messages",
Short: "清空当前用户指定会话的聊天记录",
Long: `清空当前用户在指定会话中的聊天记录。仅清空当前用户视角的消息,不影响其他成员。该操作不可逆;必须先获得用户确认,再追加 --yes 执行。
Long: `清空当前用户在指定会话中的聊天记录。仅清空当前用户视角的消息,不影响其他成员。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
@@ -5072,14 +4851,6 @@ status 可选值:
if convID == "" {
return fmt.Errorf("flag --conversation-id is required\n hint: dws chat clear-messages --conversation-id <openConversationId>")
}
if !commandBoolFlag(cmd, "yes") {
return apperrors.NewValidation(
"清空会话聊天记录不可逆;获得用户确认后加 --yes 执行",
apperrors.WithReason("confirmation_required"),
apperrors.WithHint("先确认目标会话及影响范围;用户明确同意后以相同参数追加 --yes"),
apperrors.WithActions("确认目标会话", "获得用户确认后使用 --yes 执行"),
)
}
return callMCPToolOnServer("im", "clear_conversation_messages", map[string]any{
"openConversationId": convID,
})
@@ -5422,14 +5193,6 @@ status 可选值:
if err := validateRequiredFlags(cmd, "group", "notice-id"); err != nil {
return err
}
if noticeID := mustGetFlag(cmd, "notice-id"); isLikelyPlaceholderID(noticeID) {
return chatGuidanceError(
"群公告 ID 仍是占位符",
"--notice-id 必须来自 chat group notice list 返回的真实 dataId,0 不是有效公告 ID",
[]string{"先查询目标群公告列表", "从结果读取 dataId 后重试;查询操作不需要 --dry-run"},
[]string{`dws chat group notice list --group <openConversationId> --format json`, `dws chat group notice get --group <openConversationId> --notice-id <dataId> --format json`},
)
}
return callMCPToolOnServer("im", "get_group_notice", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"dataId": mustGetFlag(cmd, "notice-id"),
@@ -5491,12 +5254,7 @@ status 可选值:
target, _ := cmd.Flags().GetString("target")
receiver, _ := cmd.Flags().GetString("receiver")
if target == "" && receiver == "" {
return chatGuidanceError(
"缺少群邀请链接的接收目标",
"--target 表示接收分享的目标会话,--receiver 表示接收分享的单聊用户,二者必须选择一个",
[]string{"分享到群或会话时使用 --target", "分享到个人时使用 --receiver openDingTalkId"},
[]string{`dws chat group share-invite --source <源群ID> --target <目标会话ID> --format json`},
)
return fmt.Errorf("--target or --receiver is required")
}
if target != "" && receiver != "" {
return fmt.Errorf("--target and --receiver are mutually exclusive")
@@ -5742,5 +5500,10 @@ pl_PL, sv_SE, fi_FI, cs_CZ, ar_SA, tl_PH, he_IL, nl_NL, lo_LA, it_IT`,
root.AddCommand(chatChmodCmd, chatDataAuthCmd, chatGroupCmd, chatSearchCmd, chatSearchCommonCmd, chatMessageCmd, chatFileCmd, newChatMediaGroup(), chatBotCmd, chatMessageListTopConversationsCmd, chatConversationInfoCmd, chatCategoryCmd, chatGroupRoleCmd, chatMuteCmd, chatSetTopCmd, chatGroupMuteCmd, chatGroupMuteMemberCmd, chatHideCmd, chatMuteAtAllCmd, chatMuteRedEnvelopeCmd, chatMarkUnreadCmd, chatClearRedPointCmd, chatClearAllRedPointCmd, chatListAllConversationsCmd, chatClearMessagesCmd, chatMarkReadCmd, chatTextCmd)
// hint: dws chat send → dws chat message send
root.AddCommand(hintSubCmd("send", "use: dws chat message send"))
// hint: dws chat history → dws chat message list
root.AddCommand(hintSubCmd("history", "use: dws chat message list --group <GROUP_OPEN_CONVERSATION_ID>"))
return root
}
+1 -22
View File
@@ -10,7 +10,6 @@ import (
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
)
@@ -115,20 +114,6 @@ func TestCrossPlatformCoverageChatDirectionAndScalarCoverage(t *testing.T) {
for _, wrap := range []bool{true, false} {
_ = normalizeAtPlaceholders("hello @u1 <@u2>", []string{"", "u1", "u2"}, wrap)
}
for _, tc := range []struct {
value string
want bool
}{
{value: "0", want: true},
{value: "test_msg_id_placeholder", want: true},
{value: "test-media", want: true},
{value: "<openMsgId>", want: true},
{value: "real-message-id", want: false},
} {
if got := isLikelyPlaceholderID(tc.value); got != tc.want {
t.Fatalf("isLikelyPlaceholderID(%q) = %v, want %v", tc.value, got, tc.want)
}
}
if got := NormalizeMessageMentions("hello @u1", []string{"u1"}, true, true); got != "<@all> hello <@u1>" {
t.Fatalf("current-user mention normalization = %q", got)
}
@@ -407,13 +392,7 @@ func TestCrossPlatformCoverageGuardGroupOwnerRemovalCoverage(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
caller := &scriptedToolCaller{steps: tc.steps}
installScriptedCaller(t, caller)
err := guardGroupOwnerRemoval(context.Background(), "group", tc.remove)
if tc.name == "owner-open" {
var typed *apperrors.Error
if !errors.As(err, &typed) || typed.Reason != "group_owner_in_remove_list" || !strings.Contains(typed.Hint, "从 --users 中移除群主") || strings.Contains(typed.Hint, "先执行 dws chat group transfer-owner") {
t.Fatalf("owner removal hint = %v", err)
}
}
_ = guardGroupOwnerRemoval(context.Background(), "group", tc.remove)
})
}
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{}`}}}
+2 -8
View File
@@ -63,14 +63,8 @@ func newChatMediaUploadCommand() *cobra.Command {
func chatMediaUploadDownlineError() error {
return apperrors.NewValidation(
"chat media upload 已下线,当前 CLI 不提供本地文件到 mediaId 的上传能力。"+
" 本地图片或文件请改用: "+chatMediaUploadReplacement+
"chat media upload 已下线,当前 CLI 不提供本地文件到 mediaId 的上传能力。" +
" 本地图片或文件请改用: " + chatMediaUploadReplacement +
";已有 mediaId 时可使用 dws chat message send --msg-type image --media-id <mediaId>。",
apperrors.WithReason("chat_media_upload_retired"),
apperrors.WithHint("本地图片、PDF、DOCX、XLSX 等统一通过 message send --msg-type file --file-path 发送;不要改用 drive upload。"),
apperrors.WithActions(
"本地文件:dws chat message send --msg-type file --file-path <本地路径>",
"已有图片 mediaId:dws chat message send --msg-type image --media-id <mediaId>",
),
)
}
-34
View File
@@ -16,40 +16,6 @@ import (
"github.com/spf13/cobra"
)
func TestChatConversationDestructiveCommandsRequireConfirmation(t *testing.T) {
for _, args := range [][]string{
{"category", "delete", "--category-id", "42"},
{"clear-messages", "--conversation-id", "cid-1"},
} {
caller := &guardedMutationCaller{}
err := executeGuardedMutationCommand(t, caller, newChatCommand, args...)
if err == nil || !strings.Contains(err.Error(), "确认") {
t.Fatalf("chat %v error = %v, want confirmation error", args, err)
}
if len(caller.calls) != 0 {
t.Fatalf("chat %v made tool calls before confirmation: %#v", args, caller.calls)
}
}
}
func TestChatConversationDestructiveCommandsRunAfterConfirmation(t *testing.T) {
for _, tc := range []struct {
args []string
toolName string
}{
{[]string{"category", "delete", "--category-id", "42", "--yes"}, "delete_conv_category"},
{[]string{"clear-messages", "--conversation-id", "cid-1", "--yes"}, "clear_conversation_messages"},
} {
caller := &guardedMutationCaller{}
if err := executeGuardedMutationCommand(t, caller, newChatCommand, tc.args...); err != nil {
t.Fatalf("chat %v error = %v", tc.args, err)
}
if len(caller.calls) != 1 || caller.calls[0].toolName != tc.toolName {
t.Fatalf("chat %v calls = %#v, want one %s call", tc.args, caller.calls, tc.toolName)
}
}
}
type contractDefectCaller struct {
dryRun bool
calls []guardedMutationCall
+54 -2
View File
@@ -276,7 +276,8 @@ func defaultHTTPPutFile(ctx context.Context, url string, headers map[string]stri
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("OSS upload failed: HTTP %d: %s", resp.StatusCode, string(body))
// typed httpStatusError 供上层按 401/403 分支重取凭证
return fmt.Errorf("OSS upload failed: %w", &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)})
}
return nil
@@ -434,7 +435,8 @@ func defaultHTTPGetFile(ctx context.Context, url string, headers map[string]stri
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
// typed httpStatusError 供上层按 401/403 分支重取凭证
return &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)}
}
outFile, err := docCreateDestination(destPath)
@@ -1070,6 +1072,9 @@ func newDocCommand() *cobra.Command {
})
}
if md != "" {
if name, ok := toolArgs["name"].(string); ok && name != "" {
md = stripDuplicateTitle(md, name)
}
toolArgs["markdown"] = md
}
if md != "" {
@@ -3215,6 +3220,53 @@ func pollDocExportJob(ctx context.Context, jobID string) (downloadURL string, er
return "", fmt.Errorf("导出任务超时:已轮询 %d 次仍在处理中 (jobId=%s),请稍后使用 dws doc export get --job-id %s 手动查询", maxPolls, jobID, jobID)
}
// stripDuplicateTitle removes the leading H1 heading from markdown content
// when it matches the document name (set via --name). This prevents the title
// from appearing twice: once as document metadata and once in the body.
func stripDuplicateTitle(markdown, name string) string {
trimmed := strings.TrimLeft(markdown, " \t\n\r")
if !strings.HasPrefix(trimmed, "# ") {
return markdown
}
newlineIdx := strings.Index(trimmed, "\n")
var headingRaw string
if newlineIdx < 0 {
headingRaw = trimmed[2:]
} else {
headingRaw = trimmed[2:newlineIdx]
}
if normalizeHeadingText(headingRaw) != normalizeHeadingText(name) {
return markdown
}
if newlineIdx < 0 {
return ""
}
rest := trimmed[newlineIdx+1:]
rest = strings.TrimLeft(rest, "\n")
return rest
}
// normalizeHeadingText strips trailing ATX hashes, inline markdown formatting
// markers, then returns a lowercased, trimmed string for comparison.
func normalizeHeadingText(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
if i := strings.LastIndexByte(s, ' '); i >= 0 {
suffix := s[i+1:]
if len(suffix) > 0 && strings.Trim(suffix, "#") == "" {
s = strings.TrimSpace(s[:i])
}
}
for _, m := range []string{"**", "__", "~~", "*", "_", "`"} {
s = strings.ReplaceAll(s, m, "")
}
return strings.TrimSpace(strings.ToLower(s))
}
// parseCommentMentionIds splits a comma-separated string of user IDs into a slice.
func parseCommentMentionIds(raw string) []string {
parts := strings.Split(raw, ",")
@@ -295,6 +295,12 @@ func TestCrossPlatformCoverageDocCreateUpdateAndBlockCommandEdges(t *testing.T)
})
}
for _, value := range []string{"plain", "# Other\nbody", "# Name", "# **Name** ###\n\nbody"} {
_ = stripDuplicateTitle(value, "Name")
}
for _, value := range []string{"", " Name ### ", "**Bold**", "__Under__ ~~Strike~~ `Code`"} {
_ = normalizeHeadingText(value)
}
for _, name := range []string{"file.pdf", "file.md", "file.unknown"} {
_ = inferMimeType(name)
}
@@ -1,74 +0,0 @@
package helpers
import (
"context"
"io"
"os"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type docCreateRecordingCall struct {
tool string
args map[string]any
}
type docCreateRecordingCaller struct {
calls []docCreateRecordingCall
}
func (c *docCreateRecordingCaller) CallTool(_ context.Context, _ string, tool string, args map[string]any) (*edition.ToolResult, error) {
copied := make(map[string]any, len(args))
for key, value := range args {
copied[key] = value
}
c.calls = append(c.calls, docCreateRecordingCall{tool: tool, args: copied})
return textToolResult(`{"nodeId":"node-1","success":true}`), nil
}
func (*docCreateRecordingCaller) Format() string { return "json" }
func (*docCreateRecordingCaller) DryRun() bool { return false }
func (*docCreateRecordingCaller) Fields() string { return "" }
func (*docCreateRecordingCaller) JQ() string { return "" }
func TestDocCreatePreservesExplicitLeadingH1MatchingName(t *testing.T) {
oldArgs := os.Args
os.Args = []string{"dws", "doc"}
t.Cleanup(func() { os.Args = oldArgs })
for _, content := range []string{
"# 需求清单",
"# 需求清单\n\n以上需求已与产品确认",
} {
t.Run(content, func(t *testing.T) {
previous := deps
caller := &docCreateRecordingCaller{}
InitDeps(caller)
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
t.Cleanup(func() { deps = previous })
root := newDocCommand()
root.SilenceErrors = true
root.SilenceUsage = true
root.SetOut(io.Discard)
root.SetErr(io.Discard)
root.SetArgs([]string{"create", "--name", "需求清单", "--content", content})
if err := root.ExecuteContext(context.Background()); err != nil {
t.Fatal(err)
}
if len(caller.calls) != 1 {
t.Fatalf("tool calls = %#v, want one create_document call", caller.calls)
}
call := caller.calls[0]
if call.tool != "create_document" {
t.Fatalf("tool = %q, want create_document", call.tool)
}
if got := call.args["markdown"]; got != content {
t.Fatalf("markdown = %#v, want exact explicit body H1 %#v", got, content)
}
})
}
}
+136 -14
View File
@@ -476,7 +476,8 @@ func newDriveCommand() *cobra.Command {
--output 指定本地保存路径,可以是文件路径或目录。
如果指定目录,文件名从下载 URL 中自动推断。`,
Example: ` dws drive download --node <dentryUuid> --output ./report.pdf
dws drive download --node <dentryUuid> --output ~/downloads/`,
dws drive download --node <dentryUuid> --output ~/downloads/
dws drive download --node <dentryUuid> --output ./big.zip --part-size 32MB --parallel 8`,
RunE: func(cmd *cobra.Command, args []string) error {
fileID := flagOrFallback(cmd, "node", "file-id")
if fileID == "" {
@@ -492,6 +493,15 @@ func newDriveCommand() *cobra.Command {
argsMap["spaceId"] = v
}
// fail-fast:分片下载参数校验
dlOpts, err := driveDownloadOptionsFromFlags(cmd)
if err != nil {
return err
}
dlOpts.logf = func(format string, a ...any) {
deps.Out.PrintInfo(fmt.Sprintf(format, a...))
}
if deps.Caller.DryRun() {
deps.Out.PrintKeyValue("操作", "下载钉盘文件")
deps.Out.PrintKeyValue("文件ID", fileID)
@@ -499,7 +509,7 @@ func newDriveCommand() *cobra.Command {
return nil
}
ctx := context.Background()
ctx := cmd.Context()
// Step 1: 获取下载 URL 和签名请求头
deps.Out.PrintInfo("[1/2] 获取下载链接...")
@@ -508,7 +518,7 @@ func newDriveCommand() *cobra.Command {
return err
}
resourceURL, dlHeaders, err := parseDownloadInfo(text)
resourceURL, dlHeaders, err := parseDriveDownloadInfo(text)
if err != nil {
return err
}
@@ -523,9 +533,34 @@ func newDriveCommand() *cobra.Command {
outputPath = filepath.Join(outputPath, filename)
}
// Step 2: HTTP GET 下载文件
// Step 2: 分片下载(自动分派 + 401/403 凭证刷新重试)
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载文件到 %s ...", outputPath))
if err := httpGetFile(ctx, resourceURL, dlHeaders, outputPath); err != nil {
dlOpts.knownSize = parseDownloadFileSize(text)
dlOpts.nodeID = fileID
dlOpts.version = parseDownloadFileVersion(text)
fetchCred := func(fctx context.Context) (string, map[string]string, int, error) {
t, ferr := callMCPToolReturnText(fctx, "download_file", argsMap)
if ferr != nil {
return "", nil, 0, ferr
}
u, h, perr := parseDriveDownloadInfo(t)
if perr != nil {
return "", nil, 0, perr
}
return u, h, parseDownloadFileVersion(t), nil
}
if err := driveTransferDownload(ctx, fetchCred, resourceURL, dlHeaders, outputPath, dlOpts); err != nil {
if errors.Is(err, context.Canceled) || ctx.Err() == context.Canceled {
partSize, _ := cmd.Flags().GetString("part-size")
noResume, _ := cmd.Flags().GetBool("no-resume")
if partSize != "" && !noResume {
fmt.Fprintf(cmd.ErrOrStderr(), "\n[INFO] 下载中断,已保存断点(可重新执行相同命令续传)\n")
} else {
fmt.Fprintf(cmd.ErrOrStderr(), "\n[INFO] 下载中断\n")
}
cmd.SilenceErrors = true
return err
}
return err
}
@@ -564,6 +599,15 @@ func newDriveCommand() *cobra.Command {
return fmt.Errorf("flag --output is required")
}
// fail-fast:分片下载参数校验
dlOpts, err := driveDownloadOptionsFromFlags(cmd)
if err != nil {
return err
}
dlOpts.logf = func(format string, a ...any) {
deps.Out.PrintInfo(fmt.Sprintf(format, a...))
}
if deps.Caller.DryRun() {
deps.Out.PrintKeyValue("操作", "下载文件历史版本")
deps.Out.PrintKeyValue("节点ID", fileID)
@@ -572,16 +616,17 @@ func newDriveCommand() *cobra.Command {
return nil
}
ctx := context.Background()
ctx := cmd.Context()
deps.Out.PrintInfo("[1/2] 获取历史版本下载链接...")
text, err := callMCPToolReturnTextOnServer(ctx, "drive", "download_file_version", map[string]any{
dlArgsMap := map[string]any{
"nodeId": fileID,
"version": versionNum,
})
}
text, err := callMCPToolReturnTextOnServer(ctx, "drive", "download_file_version", dlArgsMap)
if err != nil {
return err
}
resourceURL, dlHeaders, err := parseDownloadInfo(text)
resourceURL, dlHeaders, err := parseDriveDownloadInfo(text)
if err != nil {
return err
}
@@ -593,7 +638,32 @@ func newDriveCommand() *cobra.Command {
outputPath = filepath.Join(outputPath, filename)
}
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载文件到 %s ...", outputPath))
if err := httpGetFile(ctx, resourceURL, dlHeaders, outputPath); err != nil {
dlOpts.knownSize = parseDownloadFileSize(text)
dlOpts.nodeID = fileID
dlOpts.version = versionNum
fetchCred := func(fctx context.Context) (string, map[string]string, int, error) {
t, ferr := callMCPToolReturnTextOnServer(fctx, "drive", "download_file_version", dlArgsMap)
if ferr != nil {
return "", nil, 0, ferr
}
u, h, perr := parseDriveDownloadInfo(t)
if perr != nil {
return "", nil, 0, perr
}
return u, h, parseDownloadFileVersion(t), nil
}
if err := driveTransferDownload(ctx, fetchCred, resourceURL, dlHeaders, outputPath, dlOpts); err != nil {
if errors.Is(err, context.Canceled) || ctx.Err() == context.Canceled {
partSize, _ := cmd.Flags().GetString("part-size")
noResume, _ := cmd.Flags().GetBool("no-resume")
if partSize != "" && !noResume {
fmt.Fprintf(cmd.ErrOrStderr(), "\n[INFO] 下载中断,已保存断点(可重新执行相同命令续传)\n")
} else {
fmt.Fprintf(cmd.ErrOrStderr(), "\n[INFO] 下载中断\n")
}
cmd.SilenceErrors = true
return err
}
return err
}
deps.Out.PrintInfo(fmt.Sprintf("下载完成: %s", outputPath))
@@ -709,10 +779,16 @@ func newDriveCommand() *cobra.Command {
driveDownloadCmd.Flags().String("node", "", "文件 ID (dentryUuid) (必填)")
driveDownloadCmd.Flags().String("space-id", "", "文件所属空间 ID (可选)")
driveDownloadCmd.Flags().String("output", "", "本地保存路径 (文件路径或目录,必填)")
driveDownloadCmd.Flags().String("part-size", "16MB", "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)")
driveDownloadCmd.Flags().Int("parallel", 4, "分片下载并发数,范围 1-8 (可选)")
driveDownloadCmd.Flags().Bool("no-resume", false, "关闭断点续传 (可选)")
driveDownloadVersionCmd.Flags().String("node", "", "文件 ID (dentryUuid) 或 URL (必填)")
driveDownloadVersionCmd.Flags().Int("version", 0, "历史版本号 (必填,正整数,从 drive list --versions 获取)")
driveDownloadVersionCmd.Flags().String("output", "", "本地保存路径 (文件路径或目录,必填)")
driveDownloadVersionCmd.Flags().String("part-size", "16MB", "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)")
driveDownloadVersionCmd.Flags().Int("parallel", 4, "分片下载并发数,范围 1-8 (可选)")
driveDownloadVersionCmd.Flags().Bool("no-resume", false, "关闭断点续传 (可选)")
for _, alias := range []string{"url", "id", "node-id", "doc-id", "file-id"} {
driveDownloadVersionCmd.Flags().String(alias, "", "")
_ = driveDownloadVersionCmd.Flags().MarkHidden(alias)
@@ -2006,13 +2082,13 @@ func uploadToDrive(ctx context.Context, filePath, fileName string, fileSize int6
if err != nil {
return err
}
resourceURL, uploadID, headers, err := parseDriveUploadInfo(text)
// HTTP PUT 上传文件(OSS 与中心协议同路径;headers 透传,401/403 重取凭证重试一次)
uploadID, err := driveUploadPut(ctx, text, func(rctx context.Context) (string, error) {
return callMCPToolReturnTextOnServer(rctx, "drive", "get_upload_info", step1Args)
}, filePath, fileSize)
if err != nil {
return err
}
if err := httpPutFile(ctx, resourceURL, headers, filePath, fileSize); err != nil {
return err
}
commitArgs := map[string]any{
"fileName": fileName,
@@ -2219,3 +2295,49 @@ func isPermissionCLIError(err error) bool {
var patErr *PATError
return errors.As(err, &patErr)
}
// parseDriveDownloadInfo 从 drive 的 download_file 返回里取下载 URL 与请求头。
// drive 返回形如 {"result":{"downloadUrl":"https://..."}};OSS 预签名 URL 自带签名参数、
// 无额外请求头;中心协议(httpToCenterWithToken)返回的 headers 含 dentry-token,
// 需原样透传。对历史字段名(resourceUrl / resourceUrls[].url)做 fallback。
func parseDriveDownloadInfo(text string) (string, map[string]string, error) {
var data map[string]any
if err := json.Unmarshal([]byte(text), &data); err != nil {
return "", nil, fmt.Errorf("解析 download_file 返回失败: %w", err)
}
if result, ok := data["result"].(map[string]any); ok {
data = result
}
headers := make(map[string]string)
if h, ok := data["headers"].(map[string]any); ok {
for k, v := range h {
if s, ok := v.(string); ok {
headers[k] = s
}
}
}
dlURL, _ := data["downloadUrl"].(string)
if dlURL == "" {
dlURL, _ = data["resourceUrl"].(string)
}
if dlURL == "" {
if arr, ok := data["resourceUrls"].([]any); ok && len(arr) > 0 {
if first, ok := arr[0].(map[string]any); ok {
dlURL, _ = first["url"].(string)
if h, ok := first["headers"].(map[string]any); ok {
for k, v := range h {
if s, ok := v.(string); ok {
headers[k] = s
}
}
}
}
}
}
if dlURL == "" {
return "", nil, fmt.Errorf("download_file 未返回下载链接(downloadUrl 为空)")
}
return dlURL, headers, nil
}
+847
View File
@@ -0,0 +1,847 @@
package helpers
// ──────────────────────────────────────────────────────────
// drive 传输增强:中心协议(token 化凭证)+ 分片下载(Range)
//
// 下载:文件大小 ≥ 2×part-size 时自动分片并发下载(对齐 aws s3 cp /
// ossutil 惯例),断点续传默认开启(<dest>.dwspart 临时文件 + checkpoint
// 元信息),服务端不支持 Range 时自动回退整流;401/403(凭证过期)自动
// 重新调用 MCP 取新凭证后续传。
// 上传:中心协议(uploadType=httpToCenterWithToken)与 OSS 走同一 PUT
// 路径(URL 服务端拼好、headers 透传、客户端零追加),401/403 重取凭证
// 重试一次;服务端超限错误补充可读提示。
// ──────────────────────────────────────────────────────────
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/spf13/cobra"
)
const (
driveDownloadDefaultPartSize = 16 * 1024 * 1024 // --part-size 默认 16MB
driveDownloadMinPartSize = 1 * 1024 * 1024
driveDownloadMaxPartSize = 1024 * 1024 * 1024
driveDownloadDefaultParallel = 4
driveDownloadMaxParallel = 8
// 单分片常规失败指数退避重试次数(不含 401/403 凭证刷新)。
driveDownloadPartRetries = 3
// 单分片 401/403 凭证刷新重试上限(防止无效凭证死循环)。
driveDownloadPartAuthRetries = 2
drivePartFileSuffix = ".dwspart"
drivePartMetaSuffix = ".dwspart.meta"
driveCheckpointVersion = 1
uploadTypeCenterToken = "httpToCenterWithToken"
driveTransferBodyErrCap = 2048 // 错误响应 body 截断长度
)
// driveRangeClient 分片下载/探测专用 HTTP 客户端。
// 整流路径仍走可注入的 httpGetFile,保持既有测试注入点不变。
var driveRangeClient = &http.Client{Timeout: 10 * time.Minute}
// errCredentialRefreshVersionUnknown 凭证刷新后无法验证文件版本一致性(双方之一
// version=0),激进策略要求清空已完成分片从头下载。
var errCredentialRefreshVersionUnknown = errors.New("凭证刷新后无法验证文件版本一致性,需从头下载")
// Testable OS operation hooks (package-level for coverage injection).
var (
driveJsonMarshal = json.Marshal
driveOsRename = os.Rename
driveFileTruncate = (*os.File).Truncate
driveFileSync = (*os.File).Sync
driveFileStat = (*os.File).Stat
)
// ──────────────────────────────────────────────────────────
// HTTP 状态错误
// ──────────────────────────────────────────────────────────
// httpStatusError 表示非 2xx 的 HTTP 响应,供上层按状态码分支(401/403 重取凭证等)。
type httpStatusError struct {
StatusCode int
Body string
}
func (e *httpStatusError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Body)
}
// isAuthStatusError 判断错误是否为 401/403(凭证过期/无效)。
// 兼容 typed httpStatusError 与文本形态(测试注入或历史包装的 "HTTP 401/403" 错误)。
func isAuthStatusError(err error) bool {
if err == nil {
return false
}
var se *httpStatusError
if errors.As(err, &se) {
return se.StatusCode == http.StatusUnauthorized || se.StatusCode == http.StatusForbidden
}
msg := err.Error()
return strings.Contains(msg, "HTTP 401") || strings.Contains(msg, "HTTP 403")
}
// ──────────────────────────────────────────────────────────
// 参数解析
// ──────────────────────────────────────────────────────────
// parsePartSize 解析 --part-size 的人类可读值(16MB、512KB、1GB;纯数字按字节)。
func parsePartSize(s string) (int64, error) {
v := strings.TrimSpace(strings.ToUpper(s))
if v == "" {
return 0, fmt.Errorf("--part-size 不能为空(示例: 16MB、512KB、1GB)")
}
unit := int64(1)
switch {
case strings.HasSuffix(v, "GB"):
unit, v = 1<<30, strings.TrimSuffix(v, "GB")
case strings.HasSuffix(v, "MB"):
unit, v = 1<<20, strings.TrimSuffix(v, "MB")
case strings.HasSuffix(v, "KB"):
unit, v = 1<<10, strings.TrimSuffix(v, "KB")
case strings.HasSuffix(v, "G"):
unit, v = 1<<30, strings.TrimSuffix(v, "G")
case strings.HasSuffix(v, "M"):
unit, v = 1<<20, strings.TrimSuffix(v, "M")
case strings.HasSuffix(v, "K"):
unit, v = 1<<10, strings.TrimSuffix(v, "K")
case strings.HasSuffix(v, "B"):
v = strings.TrimSuffix(v, "B")
}
n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
if err != nil || n <= 0 {
return 0, fmt.Errorf("--part-size 格式非法: %q(示例: 16MB、512KB、1GB)", s)
}
size := n * unit
if size < driveDownloadMinPartSize || size > driveDownloadMaxPartSize {
return 0, fmt.Errorf("--part-size 取值范围 %s - %s,当前值: %s",
formatByteSize(driveDownloadMinPartSize), formatByteSize(driveDownloadMaxPartSize), s)
}
return size, nil
}
func formatByteSize(n int64) string {
switch {
case n >= 1<<30 && n%(1<<30) == 0:
return fmt.Sprintf("%dGB", n/(1<<30))
case n >= 1<<20 && n%(1<<20) == 0:
return fmt.Sprintf("%dMB", n/(1<<20))
case n >= 1<<10 && n%(1<<10) == 0:
return fmt.Sprintf("%dKB", n/(1<<10))
default:
return fmt.Sprintf("%dB", n)
}
}
// driveDownloadOptions 分片下载选项(由 drive download 的 flag 解析而来)。
type driveDownloadOptions struct {
partSize int64
parallel int
resume bool
knownSize int64 // MCP 返回的 fileSize;未知(0)或小于阈值时直接整流
nodeID string // 节点唯一标识(dentryUuid),用于生成 checkpoint 指纹
version int // 文件版本号;0 表示最新版
logf func(format string, args ...any)
}
// driveDownloadOptionsFromFlags 解析并校验 --part-size / --parallel / --no-resume。
func driveDownloadOptionsFromFlags(cmd *cobra.Command) (driveDownloadOptions, error) {
raw, _ := cmd.Flags().GetString("part-size")
partSize, err := parsePartSize(raw)
if err != nil {
return driveDownloadOptions{}, err
}
parallel, _ := cmd.Flags().GetInt("parallel")
if parallel < 1 || parallel > driveDownloadMaxParallel {
return driveDownloadOptions{}, fmt.Errorf("--parallel 取值范围 1-%d,当前值: %d", driveDownloadMaxParallel, parallel)
}
noResume, _ := cmd.Flags().GetBool("no-resume")
return driveDownloadOptions{partSize: partSize, parallel: parallel, resume: !noResume}, nil
}
// parseDownloadFileSize 从 download_file 返回中提取 fileSize(缺失/非法返回 0)。
func parseDownloadFileSize(text string) int64 {
var data map[string]any
if json.Unmarshal([]byte(text), &data) != nil {
return 0
}
if r, ok := data["result"].(map[string]any); ok {
data = r
}
switch v := data["fileSize"].(type) {
case float64:
return int64(v)
case string:
n, _ := strconv.ParseInt(v, 10, 64)
return n
}
return 0
}
// parseDownloadFileVersion 从 MCP download_file 响应中提取文件当前版本号。
// 返回 0 表示未获取到(兼容旧版 MCP 不返回 version 的场景)。
func parseDownloadFileVersion(text string) int {
var data map[string]any
if json.Unmarshal([]byte(text), &data) != nil {
return 0
}
if r, ok := data["result"].(map[string]any); ok {
data = r
}
switch v := data["version"].(type) {
case float64:
return int(v)
case string:
n, _ := strconv.Atoi(v)
return n
}
return 0
}
// ──────────────────────────────────────────────────────────
// 凭证状态(分片过程共享,401/403 时 single-flight 刷新)
// ──────────────────────────────────────────────────────────
// driveCredentialFetcher 重新调用 MCP 获取下载 URL + headers(含 dentry-token)。
type driveCredentialFetcher func(ctx context.Context) (url string, headers map[string]string, version int, err error)
type driveCredentialState struct {
mu sync.Mutex
fetch driveCredentialFetcher
url string
headers map[string]string
gen int
initialVersion int // 首次获取的文件版本号;0 表示未知(兼容旧 MCP)
}
func (cs *driveCredentialState) current() (string, map[string]string, int) {
cs.mu.Lock()
defer cs.mu.Unlock()
return cs.url, cs.headers, cs.gen
}
// refresh 重取凭证。仅当调用方持有的 generation 仍是最新时才真正重取
// (其他并发分片已刷新过则直接复用新凭证,避免重复 MCP 调用)。
func (cs *driveCredentialState) refresh(ctx context.Context, gen int) error {
cs.mu.Lock()
defer cs.mu.Unlock()
if cs.gen > gen {
return nil // 已被其他分片刷新
}
if cs.fetch == nil {
return fmt.Errorf("下载凭证已过期且无法自动刷新")
}
url, headers, version, err := cs.fetch(ctx)
if err != nil {
return err
}
// 版本校验
if cs.initialVersion > 0 && version > 0 {
if version != cs.initialVersion {
// 双方版本已知且不同 → 文件已被覆盖,终止下载防止数据不一致
return fmt.Errorf("下载凭证刷新后文件版本已变更(%d → %d),终止下载以防数据不一致", cs.initialVersion, version)
}
// 版本一致,正常继续
} else {
// 激进策略:版本不可验证(至少一方为 0),更新凭证但返回特殊错误
// 让上层清空已完成分片从头下载
cs.url, cs.headers = url, headers
cs.gen++
return errCredentialRefreshVersionUnknown
}
cs.url, cs.headers = url, headers
cs.gen++
return nil
}
// ──────────────────────────────────────────────────────────
// 下载入口:整流 / 分片自动分派
// ──────────────────────────────────────────────────────────
// driveTransferDownload 下载入口:按文件大小自动选择整流或分片下载。
// - 文件大小未知(MCP 未返回 fileSize)或 < 2×partSize → 直接整流
// (复用可注入的 httpGetFile,保持存量行为与测试注入边界不变,
// 401/403 时重取凭证重试一次);
// - 已知大小 ≥ 2×partSize → 首请求以 Range: bytes=0-0 探测:206 且解析出
// 总长 → 分片下载;服务端返回 200(不支持 Range)→ 直接消费该响应整流落盘。
func driveTransferDownload(ctx context.Context, fetch driveCredentialFetcher, rawURL string, headers map[string]string, destPath string, opts driveDownloadOptions) error {
if opts.partSize <= 0 {
opts.partSize = driveDownloadDefaultPartSize
}
if opts.parallel <= 0 {
opts.parallel = driveDownloadDefaultParallel
}
threshold := 2 * opts.partSize
if opts.knownSize < threshold {
// 含 knownSize==0(大小未知):不发起额外探测请求,保持存量整流行为
return downloadSingleWithAuthRetry(ctx, fetch, rawURL, headers, destPath)
}
creds := &driveCredentialState{fetch: fetch, url: rawURL, headers: headers, initialVersion: opts.version}
totalSize, fullResp, err := probeRangeSupport(ctx, creds)
if err != nil {
return err
}
if fullResp != nil {
// 服务端不支持 Range(忽略探测请求头返回 200 全量流):直接消费落盘
defer fullResp.Body.Close()
return writeStreamToFile(fullResp.Body, destPath)
}
curURL, curHeaders, _ := creds.current()
if totalSize <= 0 || totalSize < threshold {
// 总长未知(Content-Range 异常)或小于阈值:整流下载
return downloadSingleWithAuthRetry(ctx, func(fctx context.Context) (string, map[string]string, int, error) {
if fetch == nil {
return "", nil, 0, fmt.Errorf("下载凭证已过期且无法自动刷新")
}
return fetch(fctx)
}, curURL, curHeaders, destPath)
}
return downloadRangedParts(ctx, creds, destPath, totalSize, opts)
}
// downloadSingleWithAuthRetry 整流下载;401/403(凭证过期)时重新调用 MCP
// 获取新 URL+token 重试一次,仍失败走既有错误路径。
func downloadSingleWithAuthRetry(ctx context.Context, fetch driveCredentialFetcher, urlStr string, headers map[string]string, destPath string) error {
err := httpGetFile(ctx, urlStr, headers, destPath)
if err == nil || !isAuthStatusError(err) || fetch == nil {
return err
}
newURL, newHeaders, _, ferr := fetch(ctx)
if ferr != nil {
return err
}
return httpGetFile(ctx, newURL, newHeaders, destPath)
}
// probeRangeSupport 发送 Range: bytes=0-0 探测请求验证 206/Content-Range。
// 返回 (totalSize, nil, nil) 表示支持 Range 且已知总长;
// 返回 (0, resp, nil) 表示服务端忽略 Range 返回 200 全量响应(调用方直接消费);
// 401/403 时刷新凭证重试一次。
func probeRangeSupport(ctx context.Context, creds *driveCredentialState) (int64, *http.Response, error) {
for attempt := 0; ; attempt++ {
urlStr, headers, gen := creds.current()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return 0, nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
req.Header.Set("Range", "bytes=0-0")
resp, err := driveRangeClient.Do(req)
if err != nil {
return 0, nil, err
}
switch {
case resp.StatusCode == http.StatusPartialContent:
total, perr := parseContentRangeTotal(resp.Header.Get("Content-Range"))
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if perr != nil {
return 0, nil, nil // Content-Range 异常:总长未知,回退整流
}
return total, nil, nil
case resp.StatusCode == http.StatusOK:
return 0, resp, nil
case resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden:
body, _ := io.ReadAll(io.LimitReader(resp.Body, driveTransferBodyErrCap))
resp.Body.Close()
if attempt > 0 {
return 0, nil, fmt.Errorf("下载凭证刷新后仍鉴权失败")
}
if rerr := creds.refresh(ctx, gen); rerr != nil && !errors.Is(rerr, errCredentialRefreshVersionUnknown) {
return 0, nil, fmt.Errorf("重新获取下载凭证失败: %w (原错误: %v)",
rerr, &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)})
}
// errCredentialRefreshVersionUnknown 在探测阶段无需处理(尚无已完成分片),
// 凭证已更新,循环继续用新凭证重试探测。
default:
body, _ := io.ReadAll(io.LimitReader(resp.Body, driveTransferBodyErrCap))
resp.Body.Close()
return 0, nil, &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)}
}
}
}
// parseContentRangeTotal 从 "bytes 0-0/12345" 解析总长;"*" 视为未知。
func parseContentRangeTotal(cr string) (int64, error) {
idx := strings.LastIndex(cr, "/")
if idx < 0 || idx == len(cr)-1 {
return 0, fmt.Errorf("非法 Content-Range: %q", cr)
}
totalStr := cr[idx+1:]
if totalStr == "*" {
return 0, fmt.Errorf("Content-Range 总长未知: %q", cr)
}
total, err := strconv.ParseInt(totalStr, 10, 64)
if err != nil || total <= 0 {
return 0, fmt.Errorf("非法 Content-Range 总长: %q", cr)
}
return total, nil
}
// parseContentRange 解析 "bytes <start>-<end>/<total>" 格式的 Content-Range 头。
// 返回值 start、end 为字节偏移(闭区间),total 为文件总长("*" 视为 -1)。
func parseContentRange(header string) (start, end, total int64, err error) {
if header == "" {
return 0, 0, 0, fmt.Errorf("Content-Range 为空")
}
// 去掉 "bytes " 前缀
const prefix = "bytes "
if !strings.HasPrefix(header, prefix) {
return 0, 0, 0, fmt.Errorf("非法 Content-Range 前缀: %q", header)
}
rest := header[len(prefix):] // e.g. "0-1048575/104857600"
// 按 "/" 分割范围与总长
slashIdx := strings.LastIndex(rest, "/")
if slashIdx < 0 || slashIdx == len(rest)-1 {
return 0, 0, 0, fmt.Errorf("非法 Content-Range 格式: %q", header)
}
rangePart := rest[:slashIdx] // "0-1048575"
totalPart := rest[slashIdx+1:] // "104857600" 或 "*"
// 解析 total
if totalPart == "*" {
total = -1
} else {
total, err = strconv.ParseInt(totalPart, 10, 64)
if err != nil || total <= 0 {
return 0, 0, 0, fmt.Errorf("非法 Content-Range 总长: %q", header)
}
}
// 按 "-" 分割 start 和 end
dashIdx := strings.Index(rangePart, "-")
if dashIdx < 0 {
return 0, 0, 0, fmt.Errorf("非法 Content-Range 区间: %q", header)
}
start, err = strconv.ParseInt(rangePart[:dashIdx], 10, 64)
if err != nil || start < 0 {
return 0, 0, 0, fmt.Errorf("非法 Content-Range start: %q", header)
}
end, err = strconv.ParseInt(rangePart[dashIdx+1:], 10, 64)
if err != nil || end < start {
return 0, 0, 0, fmt.Errorf("非法 Content-Range end: %q", header)
}
return start, end, total, nil
}
func writeStreamToFile(r io.Reader, destPath string) error {
out, err := os.Create(destPath)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, r); err != nil {
return err
}
return nil
}
// ──────────────────────────────────────────────────────────
// 分片切分与 checkpoint
// ──────────────────────────────────────────────────────────
type driveDownloadPart struct {
index int
offset int64
length int64
}
// splitDownloadParts 按 partSize 等长切片,末片为余量。
func splitDownloadParts(totalSize, partSize int64) []driveDownloadPart {
if totalSize <= 0 || partSize <= 0 {
return nil
}
count := int((totalSize + partSize - 1) / partSize)
parts := make([]driveDownloadPart, 0, count)
for i := 0; i < count; i++ {
offset := int64(i) * partSize
length := partSize
if offset+length > totalSize {
length = totalSize - offset
}
parts = append(parts, driveDownloadPart{index: i, offset: offset, length: length})
}
return parts
}
// driveDownloadCheckpoint 断点续传元信息,随每个分片完成原子落盘。
type driveDownloadCheckpoint struct {
Version int `json:"version"`
Fingerprint string `json:"fingerprint"`
TotalSize int64 `json:"totalSize"`
PartSize int64 `json:"partSize"`
Completed []bool `json:"completed"`
}
// driveDownloadFingerprint 基于节点 ID + 版本号 + 文件总长 + 资源 URL 计算指纹。
// version>0 时只取 URL path(重签名不影响 checkpoint 复用);version==0(最新版)时
// 取完整 path+query——中心协议相同 path 可能对应不同实际版本,query 中的签名/token
// 标识了具体资源快照,防止错误复用旧 checkpoint。
// resourceURL 为空时不影响其他字段的指纹计算(安全降级)。
func driveDownloadFingerprint(nodeID string, version int, totalSize int64, resourceURL string) string {
urlComponent := ""
if resourceURL != "" {
if u, err := url.Parse(resourceURL); err == nil && u != nil {
if version == 0 {
// 最新版:含 query 以区分不同签名(不同实际版本)
urlComponent = u.RequestURI()
} else {
// 指定版本:只取 path,重签名不应废弃 checkpoint
urlComponent = u.Path
}
}
}
sum := sha256.Sum256([]byte(fmt.Sprintf("%s|%d|%d|%s", nodeID, version, totalSize, urlComponent)))
return hex.EncodeToString(sum[:])
}
// loadDriveDownloadCheckpoint 读取并校验 checkpoint;任一字段不匹配返回 nil(从头下载)。
func loadDriveDownloadCheckpoint(metaPath, fingerprint string, totalSize, partSize int64, partCount int) *driveDownloadCheckpoint {
data, err := os.ReadFile(metaPath)
if err != nil {
return nil
}
var cp driveDownloadCheckpoint
if err := json.Unmarshal(data, &cp); err != nil {
return nil
}
if cp.Version != driveCheckpointVersion || cp.Fingerprint != fingerprint ||
cp.TotalSize != totalSize || cp.PartSize != partSize || len(cp.Completed) != partCount {
return nil
}
return &cp
}
// save 原子写入(临时文件 + rename),避免中断产生半截 checkpoint。
func (cp *driveDownloadCheckpoint) save(metaPath string) error {
data, err := driveJsonMarshal(cp)
if err != nil {
return err
}
tmp := metaPath + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return err
}
return driveOsRename(tmp, metaPath)
}
// ──────────────────────────────────────────────────────────
// 分片下载引擎
// ──────────────────────────────────────────────────────────
// downloadRangedParts 并发分片下载到 <dest>.dwspart,全部完成后校验总长并
// 原子重命名为 destPath、清理 checkpoint;中途失败保留分片产物供断点续传。
func downloadRangedParts(ctx context.Context, creds *driveCredentialState, destPath string, totalSize int64, opts driveDownloadOptions) error {
parts := splitDownloadParts(totalSize, opts.partSize)
partPath := destPath + drivePartFileSuffix
metaPath := destPath + drivePartMetaSuffix
// 取首次凭证 URL 用于指纹计算,防止同大小文件覆盖后 checkpoint 错误复用
initialURL, _, _ := creds.current()
fingerprint := driveDownloadFingerprint(opts.nodeID, opts.version, totalSize, initialURL)
var cp *driveDownloadCheckpoint
if opts.resume {
cp = loadDriveDownloadCheckpoint(metaPath, fingerprint, totalSize, opts.partSize, len(parts))
// 分片数据文件缺失或长度不符时 checkpoint 作废,从头下载
if cp != nil {
if fi, err := os.Stat(partPath); err != nil || fi.Size() != totalSize {
cp = nil
}
}
} else {
// --no-resume:清理历史断点产物,从头下载且不写 checkpoint
_ = os.Remove(partPath)
_ = os.Remove(metaPath)
}
if cp == nil {
cp = &driveDownloadCheckpoint{
Version: driveCheckpointVersion,
Fingerprint: fingerprint,
TotalSize: totalSize,
PartSize: opts.partSize,
Completed: make([]bool, len(parts)),
}
}
f, err := os.OpenFile(partPath, os.O_RDWR|os.O_CREATE, 0o644)
if err != nil {
return fmt.Errorf("创建分片临时文件失败: %w", err)
}
if err := driveFileTruncate(f, totalSize); err != nil {
f.Close()
return fmt.Errorf("预分配分片临时文件失败: %w", err)
}
remaining := 0
for _, p := range parts {
if !cp.Completed[p.index] {
remaining++
}
}
if opts.logf != nil {
if remaining < len(parts) {
opts.logf("断点续传: 共 %d 分片(%s/片,并发 %d),已完成 %d,续传 %d",
len(parts), formatByteSize(opts.partSize), opts.parallel, len(parts)-remaining, remaining)
} else {
opts.logf("分片下载: 共 %d 分片(%s/片,并发 %d)", len(parts), formatByteSize(opts.partSize), opts.parallel)
}
}
runCtx, cancel := context.WithCancel(ctx)
defer cancel()
var (
mu sync.Mutex // 保护 cp 与 checkpoint 落盘
wg sync.WaitGroup
errOnce sync.Once
firstErr error
)
fail := func(err error) {
errOnce.Do(func() {
firstErr = err
cancel()
})
}
jobs := make(chan driveDownloadPart)
workers := opts.parallel
if workers > remaining {
workers = remaining
}
if workers < 1 {
workers = 1
}
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for part := range jobs {
if runCtx.Err() != nil {
return
}
if err := downloadOnePart(runCtx, creds, f, part, totalSize); err != nil {
fail(fmt.Errorf("分片 %d/%d 下载失败: %w", part.index+1, len(parts), err))
return
}
mu.Lock()
cp.Completed[part.index] = true
var saveErr error
if opts.resume {
saveErr = cp.save(metaPath)
}
mu.Unlock()
if saveErr != nil {
fail(fmt.Errorf("写入下载断点信息失败: %w", saveErr))
return
}
}
}()
}
dispatch:
for _, part := range parts {
if cp.Completed[part.index] {
continue
}
select {
case jobs <- part:
case <-runCtx.Done():
break dispatch
}
}
close(jobs)
wg.Wait()
if firstErr != nil {
f.Close()
if errors.Is(firstErr, errCredentialRefreshVersionUnknown) {
// 激进策略:版本不可验证,清空 checkpoint 和分片文件防止错误续传;
// 用户重跑时将自然从头下载。
_ = os.Remove(metaPath)
_ = os.Remove(partPath)
}
return firstErr // 其他错误保留 .dwspart 与 checkpoint,重跑同一命令自动续传
}
if err := driveFileSync(f); err != nil {
f.Close()
return err
}
fi, statErr := driveFileStat(f)
f.Close()
if statErr != nil {
return statErr
}
if fi.Size() != totalSize {
return fmt.Errorf("下载完成但文件长度不符: got %d, want %d", fi.Size(), totalSize)
}
if err := driveOsRename(partPath, destPath); err != nil {
return fmt.Errorf("重命名下载文件失败: %w", err)
}
_ = os.Remove(metaPath)
return nil
}
// downloadOnePart 下载单个分片:常规失败指数退避重试 driveDownloadPartRetries 次;
// 401/403 触发凭证 single-flight 刷新(不计入常规重试,上限 driveDownloadPartAuthRetries),
// 刷新后用新凭证续传,不重下其他已完成分片。
func downloadOnePart(ctx context.Context, creds *driveCredentialState, f *os.File, part driveDownloadPart, totalSize int64) error {
attempt := 0
authRetries := 0
backoff := 500 * time.Millisecond
for {
urlStr, headers, gen := creds.current()
err := fetchRangeInto(ctx, urlStr, headers, f, part, totalSize)
if err == nil {
return nil
}
if ctx.Err() != nil {
return err
}
if isAuthStatusError(err) && authRetries < driveDownloadPartAuthRetries {
authRetries++
if rerr := creds.refresh(ctx, gen); rerr != nil {
return fmt.Errorf("重新获取下载凭证失败: %w (原错误: %v)", rerr, err)
}
continue // 凭证刷新不计常规重试、不退避
}
attempt++
if attempt > driveDownloadPartRetries {
return err
}
select {
case <-time.After(backoff):
case <-ctx.Done():
return err
}
backoff *= 2
}
}
// fetchRangeInto 拉取 [offset, offset+length) 区间并写入文件对应偏移。
func fetchRangeInto(ctx context.Context, urlStr string, headers map[string]string, f *os.File, part driveDownloadPart, expectedTotal int64) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return err
}
for k, v := range headers {
req.Header.Set(k, v)
}
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", part.offset, part.offset+part.length-1))
resp, err := driveRangeClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusPartialContent {
body, _ := io.ReadAll(io.LimitReader(resp.Body, driveTransferBodyErrCap))
return &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)}
}
// 校验 Content-Range 响应区间与请求分片一致,防止代理/服务端返回错位数据
cr := resp.Header.Get("Content-Range")
if cr == "" {
return fmt.Errorf("分片响应缺少 Content-Range 头,无法验证数据偏移一致性")
}
{
crStart, crEnd, crTotal, crErr := parseContentRange(cr)
if crErr != nil {
return fmt.Errorf("Content-Range 解析失败: %w", crErr)
}
wantStart := part.offset
wantEnd := part.offset + part.length - 1
if crStart != wantStart || crEnd != wantEnd {
return fmt.Errorf("Content-Range 区间不匹配: 响应 %d-%d, 期望 %d-%d",
crStart, crEnd, wantStart, wantEnd)
}
if crTotal > 0 && expectedTotal > 0 && crTotal != expectedTotal {
return fmt.Errorf("Content-Range 总长不匹配: 响应 %d, 期望 %d", crTotal, expectedTotal)
}
}
n, err := io.Copy(io.NewOffsetWriter(f, part.offset), io.LimitReader(resp.Body, part.length))
if err != nil {
return err
}
if n != part.length {
return fmt.Errorf("分片长度不符: got %d, want %d", n, part.length)
}
return nil
}
// ──────────────────────────────────────────────────────────
// 上传:中心协议识别 + 401/403 重试 + 超限可读提示
// ──────────────────────────────────────────────────────────
// parseDriveUploadType 提取 get_upload_info 返回中的 uploadType(可选字段)。
// "httpToCenterWithToken" 表示中心协议;存量 OSS 返回无该字段,返回空串。
func parseDriveUploadType(text string) string {
var data map[string]any
if json.Unmarshal([]byte(text), &data) != nil {
return ""
}
if r, ok := data["result"].(map[string]any); ok {
data = r
}
t, _ := data["uploadType"].(string)
return t
}
// decorateUploadSizeError 为服务端上传超限错误补充可读提示。
// 不做本地文件大小上限校验(上限为服务端动态权益值,本地硬编码会漂移)。
func decorateUploadSizeError(err error, uploadType string) error {
var se *httpStatusError
if !errors.As(err, &se) {
return err
}
if se.StatusCode == http.StatusRequestEntityTooLarge ||
(uploadType == uploadTypeCenterToken && likelySizeLimitBody(se.Body)) {
return fmt.Errorf("%w\n提示: 文件大小可能超出空间容量或上传上限,请确认文件大小、清理空间或联系管理员调整容量后重试", err)
}
return err
}
func likelySizeLimitBody(body string) bool {
b := strings.ToLower(body)
if strings.Contains(b, "超限") || strings.Contains(b, "超出") || strings.Contains(b, "容量") {
return true
}
return strings.Contains(b, "size") && (strings.Contains(b, "limit") || strings.Contains(b, "exceed") || strings.Contains(b, "over"))
}
// driveUploadPut 解析上传凭证并执行 HTTP PUT;401/403(token 过期)时通过 refetch
// 重新获取凭证重试一次。返回最终生效凭证的 uploadId(凭证刷新后以新值为准)。
// 中心协议(uploadType=httpToCenterWithToken)与 OSS 走同一路径:resourceUrl 为
// 服务端拼好的完整 PUT URL(客户端零追加),headers(含 dentry-token)原样透传。
func driveUploadPut(ctx context.Context, credText string, refetch func(context.Context) (string, error), filePath string, fileSize int64) (string, error) {
resourceURL, uploadID, headers, err := parseDriveUploadInfo(credText)
if err != nil {
return "", err
}
uploadType := parseDriveUploadType(credText)
putErr := httpPutFile(ctx, resourceURL, headers, filePath, fileSize)
if putErr != nil && isAuthStatusError(putErr) && refetch != nil {
text2, rerr := refetch(ctx)
if rerr == nil {
if url2, id2, headers2, perr := parseDriveUploadInfo(text2); perr == nil {
uploadID = id2
uploadType = parseDriveUploadType(text2)
putErr = httpPutFile(ctx, url2, headers2, filePath, fileSize)
}
}
}
if putErr != nil {
return "", decorateUploadSizeError(putErr, uploadType)
}
return uploadID, nil
}
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -14,7 +14,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
@@ -670,7 +669,15 @@ func getDWSGatewayErrorCode(errBody map[string]any) (string, bool) {
// suggestForBusinessError returns a user-facing suggestion for known business
// error patterns in a parsed JSON body, or "" if no specific suggestion applies.
func suggestForBusinessError(body map[string]any) string {
return apperrors.SuggestBusinessHint(body)
msg := ""
if v, ok := body["errorMsg"].(string); ok {
msg = v
} else if v, ok := body["message"].(string); ok {
msg = v
} else if v, ok := body["error"].(string); ok {
msg = v
}
return suggestForBusinessErrorText(msg)
}
// confirmDelete is a convenience wrapper around cmdutil.ConfirmDelete that
+5 -17
View File
@@ -62,18 +62,13 @@ var ConversationSetTop = shortcut.Shortcut{
Flags: []shortcut.Flag{
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "单个会话 openConversationId"},
{Name: "conversation-ids", Type: shortcut.FlagStringSlice, Desc: "多个会话 openConversationId(最多 10 个)"},
{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
{Name: "chat-id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
{Name: "chat-ids", Type: shortcut.FlagStringSlice, Desc: "--conversation-ids 的兼容别名", Hidden: true},
{Name: "off", Type: shortcut.FlagBool, Desc: "取消置顶(不传则设置置顶)"},
{Name: "top", Type: shortcut.FlagBool, Default: "true", Desc: "置顶状态兼容参数", Hidden: true},
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"conversation-id", "conversation-ids", "open-conversation-id", "chat-id", "chat-ids"}},
{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"off", "top"}},
{Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"conversation-id", "conversation-ids"}},
{
Kind: shortcut.ConstraintCustom,
Flags: []string{"conversation-id", "conversation-ids", "open-conversation-id", "chat-id", "chat-ids"},
Flags: []string{"conversation-id", "conversation-ids"},
Description: "会话 ID 去重后必须为 1-10 个",
},
},
@@ -90,10 +85,6 @@ var ConversationSetTop = shortcut.Shortcut{
},
Execute: func(rt *shortcut.RuntimeContext) error {
ids := conversationSetTopIDs(rt)
top := !rt.Bool("off")
if rt.Changed("top") {
top = rt.Bool("top")
}
items := make([]shortcutBatchWrite, 0, len(ids))
for _, id := range ids {
items = append(items, shortcutBatchWrite{
@@ -101,7 +92,7 @@ var ConversationSetTop = shortcut.Shortcut{
arguments: map[string]any{
"openConversationId": id,
"cid": id,
"top": top,
"top": !rt.Bool("off"),
},
})
}
@@ -111,11 +102,8 @@ var ConversationSetTop = shortcut.Shortcut{
func conversationSetTopIDs(rt *shortcut.RuntimeContext) []string {
values := append([]string{}, rt.StrSlice("conversation-ids")...)
values = append(values, rt.StrSlice("chat-ids")...)
for _, name := range []string{"conversation-id", "open-conversation-id", "chat-id"} {
if value := rt.Str(name); value != "" {
values = append(values, value)
}
if value := rt.Str("conversation-id"); value != "" {
values = append(values, value)
}
return uniqueShortcutStrings(values)
}
+10 -37
View File
@@ -69,25 +69,15 @@ var ChatMembersGet = shortcut.Shortcut{
Intent: "当你已有若干成员的 openDingTalkId、需要批量获取他们在该群内的详情(群昵称、角色等)时使用;只读,需传群 openConversationId 和成员 openDingTalkId 列表。",
Risk: shortcut.RiskRead,
Flags: []shortcut.Flag{
{Name: "id", Type: shortcut.FlagString, Desc: "群 openConversationId(必填)"},
{Name: "group", Type: shortcut.FlagString, Desc: "--id 的兼容别名", Hidden: true},
{Name: "chat-id", Type: shortcut.FlagString, Desc: "--id 的兼容别名", Hidden: true},
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "--id 的兼容别名", Hidden: true},
{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "--id 的兼容别名", Hidden: true},
{Name: "users", Type: shortcut.FlagStringSlice, Desc: "成员 openDingTalkId 列表(必填)"},
{Name: "open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "--users 的兼容别名", Hidden: true},
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"id", "group", "chat-id", "conversation-id", "open-conversation-id"}},
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"users", "open-dingtalk-ids"}},
{Name: "id", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
{Name: "users", Type: shortcut.FlagStringSlice, Desc: "成员 openDingTalkId 列表", Required: true},
},
Tips: []string{`dws chat +chat-members-get --id <openConversationId> --users odid1,odid2`},
Execute: func(rt *shortcut.RuntimeContext) error {
conversationID := rt.StrFirst("id", "group", "chat-id", "conversation-id", "open-conversation-id")
return rt.CallMCP("list_group_member_by_ids", map[string]any{
"openConversationId": conversationID,
"cid": conversationID,
"memberOpenDingTalkIds": rt.StrSliceFirst("users", "open-dingtalk-ids"),
"openConversationId": rt.Str("id"),
"cid": rt.Str("id"),
"memberOpenDingTalkIds": rt.StrSlice("users"),
})
},
}
@@ -132,22 +122,14 @@ var ChatInviteURL = shortcut.Shortcut{
Intent: "当你想拿到一条群邀请链接分享给别人加群时使用;只读生成链接,需传群 openConversationId,可用 --expires-seconds 设置有效期(0 表示永久)。",
Risk: shortcut.RiskRead,
Flags: []shortcut.Flag{
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(必填)"},
{Name: "id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "chat-id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
{Name: "expires-seconds", Type: shortcut.FlagInt, Desc: "链接有效期(秒),0 表示永久"},
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "id", "chat-id", "conversation-id", "open-conversation-id"}},
},
Tips: []string{`dws chat +chat-invite-url --group <openConversationId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
conversationID := rt.StrFirst("group", "id", "chat-id", "conversation-id", "open-conversation-id")
params := map[string]any{
"openConversationId": conversationID,
"cid": conversationID,
"openConversationId": rt.Str("group"),
"cid": rt.Str("group"),
}
if rt.Changed("expires-seconds") {
params["expiresSeconds"] = rt.Int("expires-seconds")
@@ -570,20 +552,11 @@ var ChatBots = shortcut.Shortcut{
Intent: "当你想查看某个群里已添加了哪些机器人时使用;需传群 openConversationId,只读返回群内机器人列表(含 openBotId,供后续移除)。",
Risk: shortcut.RiskRead,
Flags: []shortcut.Flag{
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(必填)"},
{Name: "id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "chat-id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "id", "chat-id", "conversation-id", "open-conversation-id"}},
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
},
Tips: []string{`dws chat +chat-bots --group <openConversationId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
data, err := rt.CallMCPData("bot", "list_group_bots", map[string]any{
"openConversationId": rt.StrFirst("group", "id", "chat-id", "conversation-id", "open-conversation-id"),
})
data, err := rt.CallMCPData("bot", "list_group_bots", map[string]any{"openConversationId": rt.Str("group")})
if err != nil {
return err
}
+10 -31
View File
@@ -147,23 +147,14 @@ var MessagesRecall = shortcut.Shortcut{
Intent: "当你想撤回当前用户刚发出的某条消息时使用;会实际撤回消息,需传会话 openConversationId 和消息 openMessageId。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId(必填)"},
{Name: "group", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
{Name: "chat-id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMessageId(必填)"},
{Name: "message-id", Type: shortcut.FlagString, Desc: "--msg-id 的兼容别名", Hidden: true},
{Name: "open-message-id", Type: shortcut.FlagString, Desc: "--msg-id 的兼容别名", Hidden: true},
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"conversation-id", "group", "chat-id", "open-conversation-id"}},
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"msg-id", "message-id", "open-message-id"}},
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMessageId", Required: true},
},
Tips: []string{`dws chat +messages-recall --conversation-id <openConversationId> --msg-id <openMessageId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("recall_message", map[string]any{
"openConversationId": rt.StrFirst("conversation-id", "group", "chat-id", "open-conversation-id"),
"openMessageId": rt.StrFirst("msg-id", "message-id", "open-message-id"),
"openConversationId": rt.Str("conversation-id"),
"openMessageId": rt.Str("msg-id"),
})
},
}
@@ -538,30 +529,26 @@ var MessagesMget = shortcut.Shortcut{
Intent: "当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger;本地下载路径受限于工作目录、默认不覆盖同名文件,按既有安全下载约定无需交互确认。",
Risk: shortcut.RiskRead,
Flags: append([]shortcut.Flag{
{Name: "msg-ids", Type: shortcut.FlagStringSlice, Desc: "消息 openMsgId 列表;--msg-ids 去重后必须包含 1-50 条消息 ID(必填)"},
{Name: "message-id", Type: shortcut.FlagStringSlice, Desc: "--msg-ids 的兼容别名", Hidden: true},
{Name: "message-ids", Type: shortcut.FlagStringSlice, Desc: "--msg-ids 的兼容别名", Hidden: true},
{Name: "open-message-ids", Type: shortcut.FlagStringSlice, Desc: "--msg-ids 的兼容别名", Hidden: true},
{Name: "msg-ids", Type: shortcut.FlagStringSlice, Desc: "消息 openMsgId 列表;--msg-ids 去重后必须包含 1-50 条消息 ID", Required: true},
{Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出消息 reaction(默认输出)"},
}, MessageResourceDownloadFlags()...),
Constraints: append([]shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"msg-ids", "message-id", "message-ids", "open-message-ids"}},
{
Kind: shortcut.ConstraintCustom,
Flags: []string{"msg-ids", "message-id", "message-ids", "open-message-ids"},
Flags: []string{"msg-ids"},
Description: "--msg-ids 去重后必须包含 1-50 条消息 ID",
},
}, MessageResourceDownloadConstraints()...),
Tips: []string{`dws chat +messages-mget --msg-ids msgId1,msgId2`},
Validate: func(rt *shortcut.RuntimeContext) error {
ids := messageMgetIDs(rt)
ids := uniqueShortcutStrings(rt.StrSlice("msg-ids"))
if len(ids) < 1 || len(ids) > 50 {
return fmt.Errorf("--msg-ids 去重后必须包含 1-50 条消息 ID,当前 %d 条", len(ids))
}
return ValidateMessageResourceDownload(rt)
},
Execute: func(rt *shortcut.RuntimeContext) error {
ids := messageMgetIDs(rt)
ids := uniqueShortcutStrings(rt.StrSlice("msg-ids"))
data, err := rt.CallMCPData("im", "list_messages_by_ids", map[string]any{"openMsgIds": ids})
if err != nil {
return err
@@ -594,10 +581,6 @@ var MessagesMget = shortcut.Shortcut{
},
}
func messageMgetIDs(rt *shortcut.RuntimeContext) []string {
return uniqueShortcutStrings(rt.StrSliceFirst("msg-ids", "message-id", "message-ids", "open-message-ids"))
}
// MessageResourceDownloadFlags returns the common opt-in resource workflow used
// by message list, search, mget, @me and thread-reading Shortcuts.
func MessageResourceDownloadFlags() []shortcut.Flag {
@@ -850,15 +833,11 @@ var MessagesQuerySendStatus = shortcut.Shortcut{
Intent: "当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。",
Risk: shortcut.RiskRead,
Flags: []shortcut.Flag{
{Name: "open-task-id", Type: shortcut.FlagString, Desc: "发送消息时返回的 openTaskId(必填)"},
{Name: "task-id", Type: shortcut.FlagString, Desc: "--open-task-id 的兼容别名", Hidden: true},
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"open-task-id", "task-id"}},
{Name: "open-task-id", Type: shortcut.FlagString, Desc: "发送消息时返回的 openTaskId", Required: true},
},
Tips: []string{`dws chat +messages-query-send-status --open-task-id <openTaskId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("query_message_send_status", map[string]any{"openTaskId": rt.StrFirst("open-task-id", "task-id")})
return rt.CallMCP("query_message_send_status", map[string]any{"openTaskId": rt.Str("open-task-id")})
},
}
@@ -129,72 +129,6 @@ func TestCrossPlatformCoverageCompatibilityAliases(t *testing.T) {
wantTool: "query_msg_read_status",
wantArgs: map[string]any{"openConversationId": "cid-1"},
},
{
name: "chat update id and title aliases",
argv: []string{"chat", "+chat-update", "--open-conversation-id", "cid-1", "--title", "新群名", "--yes"},
wantProduct: "chat",
wantTool: "update_group_name",
wantArgs: map[string]any{"openconversation_id": "cid-1", "group_name": "新群名"},
},
{
name: "member get id and users aliases",
argv: []string{"chat", "+chat-members-get", "--conversation-id", "cid-1", "--open-dingtalk-ids", "D1,D2"},
wantProduct: "im",
wantTool: "list_group_member_by_ids",
wantArgs: map[string]any{
"openConversationId": "cid-1",
"memberOpenDingTalkIds": []string{"D1", "D2"},
},
},
{
name: "invite url id alias",
argv: []string{"chat", "+chat-invite-url", "--chat-id", "cid-1"},
wantProduct: "im",
wantTool: "get_group_invite_url",
wantArgs: map[string]any{"openConversationId": "cid-1"},
},
{
name: "chat bots id alias",
argv: []string{"chat", "+chat-bots", "--id", "cid-1"},
wantProduct: "bot",
wantTool: "list_group_bots",
wantArgs: map[string]any{"openConversationId": "cid-1"},
},
{
name: "recall message aliases",
argv: []string{"chat", "+messages-recall", "--chat-id", "cid-1", "--message-id", "msg-1", "--yes"},
wantProduct: "im",
wantTool: "recall_message",
wantArgs: map[string]any{"openConversationId": "cid-1", "openMessageId": "msg-1"},
},
{
name: "message mget message id alias",
argv: []string{"chat", "+messages-mget", "--message-id", "msg-1,msg-2"},
wantProduct: "im",
wantTool: "list_messages_by_ids",
wantArgs: map[string]any{"openMsgIds": []string{"msg-1", "msg-2"}},
},
{
name: "send status task id alias",
argv: []string{"chat", "+messages-query-send-status", "--task-id", "task-1"},
wantProduct: "im",
wantTool: "query_message_send_status",
wantArgs: map[string]any{"openTaskId": "task-1"},
},
{
name: "conversation top aliases",
argv: []string{"chat", "+conversation-set-top", "--open-conversation-id", "cid-1", "--top=false", "--yes"},
wantProduct: "im",
wantTool: "set_top_conversation",
wantArgs: map[string]any{"openConversationId": "cid-1", "top": false},
},
{
name: "favorite list limit alias",
argv: []string{"chat", "+flag-list", "--limit", "7"},
wantProduct: "im",
wantTool: "list_message_favorites",
wantArgs: map[string]any{"size": "7"},
},
{
name: "at all mute matches live MCP schema",
argv: []string{"chat", "+conversation-mute-at-all", "--conversation-id", "cid-1", "--yes"},
@@ -256,7 +190,7 @@ func TestCrossPlatformCoverageCompatibilityAliases(t *testing.T) {
t.Fatalf("call = %s/%s, want %s/%s", fake.product, fake.tool, tc.wantProduct, tc.wantTool)
}
for key, want := range tc.wantArgs {
if got := fake.args[key]; !reflect.DeepEqual(got, want) {
if got := fake.args[key]; got != want {
t.Errorf("%s = %#v, want %#v", key, got, want)
}
}
+6 -18
View File
@@ -119,24 +119,14 @@ var ChatUpdate = shortcut.Shortcut{
Intent: "当你只需要修改群名称时使用;这是 lark-cli +chat-update 的诚实子集,只接受群 openConversationId 和新名称。修改群 description、个人备注、群昵称或其他群设置时不要使用。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(必填)"},
{Name: "id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "chat-id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "--group 的兼容别名", Hidden: true},
{Name: "name", Type: shortcut.FlagString, Desc: "新的群名称(必填)"},
{Name: "title", Type: shortcut.FlagString, Desc: "--name 的兼容别名", Hidden: true},
{Name: "new-title", Type: shortcut.FlagString, Desc: "--name 的兼容别名", Hidden: true},
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "id", "chat-id", "conversation-id", "open-conversation-id"}},
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"name", "title", "new-title"}},
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
{Name: "name", Type: shortcut.FlagString, Desc: "新的群名称", Required: true},
},
Tips: []string{`dws chat +chat-update --group <openConversationId> --name "新群名"`},
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("update_group_name", map[string]any{
"openconversation_id": rt.StrFirst("group", "id", "chat-id", "conversation-id", "open-conversation-id"),
"group_name": rt.StrFirst("name", "title", "new-title"),
"openconversation_id": rt.Str("group"),
"group_name": rt.Str("name"),
})
},
}
@@ -401,8 +391,6 @@ var FlagList = shortcut.Shortcut{
Flags: []shortcut.Flag{
{Name: "cursor", Type: shortcut.FlagInt, Default: "0", Desc: "数字分页游标,首次传 0"},
{Name: "size", Type: shortcut.FlagInt, Default: "20", Desc: "每页数量,范围 1-100"},
{Name: "limit", Type: shortcut.FlagInt, Desc: "--size 的兼容别名", Hidden: true},
{Name: "max", Type: shortcut.FlagInt, Desc: "--size 的兼容别名", Hidden: true},
},
Constraints: []shortcut.Constraint{{
Kind: shortcut.ConstraintCustom,
@@ -414,7 +402,7 @@ var FlagList = shortcut.Shortcut{
if rt.Int("cursor") < 0 {
return apperrors.NewValidation("--cursor 必须大于等于 0")
}
if size := rt.IntFirst("size", "limit", "max"); size < 1 || size > 100 {
if size := rt.Int("size"); size < 1 || size > 100 {
return apperrors.NewValidation("--size 必须在 1-100 之间")
}
return nil
@@ -422,7 +410,7 @@ var FlagList = shortcut.Shortcut{
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("list_message_favorites", map[string]any{
"cursor": rt.Int("cursor"),
"size": strconv.Itoa(rt.IntFirst("size", "limit", "max")),
"size": strconv.Itoa(rt.Int("size")),
})
},
}
+1 -30
View File
@@ -96,17 +96,6 @@ func (rt *RuntimeContext) StrSlice(name string) []string {
return v
}
// StrSliceFirst returns the first non-empty string-slice value across a
// primary flag and its compatibility aliases.
func (rt *RuntimeContext) StrSliceFirst(names ...string) []string {
for _, name := range names {
if v := rt.StrSlice(name); hasNonEmptyString(v) {
return v
}
}
return nil
}
// Changed reports whether the user explicitly set the flag on the command line.
func (rt *RuntimeContext) Changed(name string) bool {
f := rt.cmd.Flags().Lookup(name)
@@ -369,27 +358,9 @@ func shortcutLongHelp(s Shortcut) string {
if len(s.Constraints) == 0 {
return long
}
publicFlags := make(map[string]bool, len(s.Flags))
for _, flag := range s.Flags {
publicFlags[flag.Name] = !flag.Hidden
}
lines := make([]string, 0, len(s.Constraints))
for _, constraint := range s.Constraints {
visible := constraint
visible.Flags = make([]string, 0, len(constraint.Flags))
for _, flag := range constraint.Flags {
if publicFlags[flag] {
visible.Flags = append(visible.Flags, flag)
}
}
if len(visible.Flags) == 0 ||
(visible.Kind != ConstraintCustom && len(visible.Flags) < 2) {
continue
}
lines = append(lines, " - "+constraintHelp(visible))
}
if len(lines) == 0 {
return long
lines = append(lines, " - "+constraintHelp(constraint))
}
return long + "\n\n参数约束:\n" + strings.Join(lines, "\n")
}
-15
View File
@@ -16,7 +16,6 @@ package shortcut
import (
"bytes"
"os"
"reflect"
"strings"
"testing"
@@ -138,7 +137,6 @@ func TestCrossPlatformCoverageSchemaConstraintCollapsesHiddenAliases(t *testing.
cmd := mount(Shortcut{
Service: "chat",
Command: "+search",
Intent: "搜索消息",
Flags: []Flag{
{Name: "query", Type: FlagString},
{Name: "keyword", Type: FlagString, Hidden: true},
@@ -167,11 +165,6 @@ func TestCrossPlatformCoverageSchemaConstraintCollapsesHiddenAliases(t *testing.
if raw := cmd.Annotations["dws.schema.constraints"]; raw != "" {
t.Fatalf("hidden compatibility alias leaked into public Schema constraints: %q", raw)
}
for _, hidden := range []string{"--keyword", "--legacy-id"} {
if strings.Contains(cmd.Long, hidden) {
t.Fatalf("hidden compatibility alias leaked into long help: %q", cmd.Long)
}
}
}
func TestCrossPlatformCoverageAliasAndAIMessageTagHelpers(t *testing.T) {
@@ -186,8 +179,6 @@ func TestCrossPlatformCoverageAliasAndAIMessageTagHelpers(t *testing.T) {
Flags: []Flag{
{Name: "query", Type: FlagString},
{Name: "keyword", Type: FlagString},
{Name: "ids", Type: FlagStringSlice},
{Name: "legacy-ids", Type: FlagStringSlice},
{Name: "limit", Type: FlagInt, Default: "20"},
{Name: "size", Type: FlagInt},
aiTag,
@@ -202,12 +193,6 @@ func TestCrossPlatformCoverageAliasAndAIMessageTagHelpers(t *testing.T) {
if got := rt.StrFirst("query", "keyword"); got != "树莓派" {
t.Fatalf("StrFirst() = %q, want 树莓派", got)
}
if err := cmd.Flags().Set("legacy-ids", "a,b"); err != nil {
t.Fatal(err)
}
if got := rt.StrSliceFirst("ids", "legacy-ids"); !reflect.DeepEqual(got, []string{"a", "b"}) {
t.Fatalf("StrSliceFirst() = %#v, want [a b]", got)
}
if got := rt.IntFirst("limit", "size"); got != 20 {
t.Fatalf("IntFirst() default = %d, want 20", got)
}
+1 -18
View File
@@ -65,7 +65,7 @@ var AtMe = shortcut.Shortcut{
`dws chat +at-me`,
`dws chat +at-me --days 3`,
},
Validate: validateAtMe,
Validate: chatshortcut.ValidateMessageResourceDownload,
Execute: func(rt *shortcut.RuntimeContext) error {
// Step 1 — look-back window [now-Nd, now] in epoch millis. days defaults
// to 7; guard against non-positive overrides so the window stays sane.
@@ -109,23 +109,6 @@ var AtMe = shortcut.Shortcut{
},
}
func validateAtMe(rt *shortcut.RuntimeContext) error {
if err := chatshortcut.ValidateMessageResourceDownload(rt); err != nil {
return err
}
days := rt.Int("days")
if days <= 0 {
return localChatOptionError("invalid_lookback_window", "+at-me 的 --days 必须大于 0", "--days")
}
if days > 3650 {
return localChatOptionError("lookback_window_too_large", "+at-me 的 --days 超出支持范围 1-3650", "--days")
}
if rt.Int("limit") <= 0 {
return localChatOptionError("invalid_page_size", "+at-me 的 --limit 必须大于 0", "--limit")
}
return nil
}
// atMeMessageItems locates the message list inside a search_at_me_message
// response, probing common container keys at the top level and nested under
// "result". Returns nil when no list is found.
@@ -22,22 +22,6 @@ func TestPreferExactGroupMatches(t *testing.T) {
}
}
func TestLooksLikeOpenConversationID(t *testing.T) {
for _, value := range []string{
"cidayZx5r0T+UiMi6NrO1048A==",
"cidr0zKX5dvj/c0wDO+wupqyg==",
} {
if !looksLikeOpenConversationID(value) {
t.Errorf("looksLikeOpenConversationID(%q) = false", value)
}
}
for _, value := range []string{"项目冲刺", "cid项目群", "city project"} {
if looksLikeOpenConversationID(value) {
t.Errorf("looksLikeOpenConversationID(%q) = true", value)
}
}
}
func TestResolveMemberTypes(t *testing.T) {
users, bots, err := resolveMemberTypes(nil)
if err != nil || !users || !bots {
+4 -48
View File
@@ -17,7 +17,6 @@ import (
"strings"
"time"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
chatshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
@@ -67,10 +66,8 @@ var ChatMessages = shortcut.Shortcut{
{Name: "user", Type: shortcut.FlagString, Desc: "单聊对方的 userId,与 --group 互斥"},
{Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊对方的 openDingTalkId,与 --group/--user 互斥"},
{Name: "time", Type: shortcut.FlagString, Desc: "时间边界,如 \"2025-03-01 00:00:00\";省略时从当前时间向前读取最近消息"},
{Name: "before", Type: shortcut.FlagString, Desc: "--time <值> --direction older 的兼容写法", Hidden: true},
{Name: "limit", Type: shortcut.FlagInt, Desc: "每页拉取的消息条数(可选)"},
{Name: "size", Type: shortcut.FlagInt, Desc: "--limit 的旧版别名", Hidden: true},
{Name: "page-all", Type: shortcut.FlagBool, Desc: "暂不支持;请按返回游标显式翻页", Hidden: true},
{Name: "direction", Type: shortcut.FlagString, Enum: []string{"newer", "older"}, Desc: "时间方向 newer/older;省略时为 older,从时间边界向前读取"},
{Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出消息 reaction(默认输出)"},
}, chatshortcut.MessageResourceDownloadFlags()...),
@@ -82,7 +79,7 @@ var ChatMessages = shortcut.Shortcut{
`dws chat +chat-messages --user <userId> --time "2025-03-01 00:00:00" --limit 50`,
`dws chat +chat-messages --group <openconversation_id> --direction older`,
},
Validate: validateChatMessages,
Validate: chatshortcut.ValidateMessageResourceDownload,
Execute: func(rt *shortcut.RuntimeContext) error {
// Step 1 — build params and pick the right tool. Param keys
// (openconversation_id / userId / time / forward / limit) match the MCP server
@@ -91,8 +88,8 @@ var ChatMessages = shortcut.Shortcut{
params := map[string]any{}
fallbackConversationID := ""
if boundary := rt.StrFirst("time", "before"); boundary != "" {
params["time"] = boundary
if rt.Changed("time") && rt.Str("time") != "" {
params["time"] = rt.Str("time")
} else {
params["time"] = formatDingTalkMessageBoundary(time.Now())
}
@@ -102,9 +99,7 @@ var ChatMessages = shortcut.Shortcut{
// direction newer/older maps to the tools' boolean `forward` param
// (newer -> forward=true, older -> forward=false), matching chat.go's
// resolveMessageForward.
if rt.Changed("before") {
params["forward"] = false
} else if rt.Changed("direction") {
if rt.Changed("direction") {
switch strings.TrimSpace(strings.ToLower(rt.Str("direction"))) {
case "newer":
params["forward"] = true
@@ -153,45 +148,6 @@ var ChatMessages = shortcut.Shortcut{
},
}
func validateChatMessages(rt *shortcut.RuntimeContext) error {
if err := chatshortcut.ValidateMessageResourceDownload(rt); err != nil {
return err
}
if rt.Changed("time") && rt.Changed("before") {
return localChatOptionError("conflicting_time_options", "+chat-messages 的 --time 与 --before 不能同时使用", "--time", "--before")
}
if rt.Changed("before") && rt.Changed("direction") && strings.ToLower(rt.Str("direction")) != "older" {
return localChatOptionError("incompatible_time_direction", "+chat-messages 的 --before 不能与 --direction newer 同时使用", "--before", "--direction")
}
if groupID := rt.StrFirst("group", "conversation-id", "id"); groupID != "" && looksLikeHumanGroupName(groupID) {
flag := changedConversationIDFlag(rt)
return localChatOptionError("group_name_used_as_conversation_id", "+chat-messages 的 "+flag+" 需要 openConversationId,当前值像群名", flag)
}
if (rt.Changed("limit") && rt.Int("limit") <= 0) || (rt.Changed("size") && rt.Int("size") <= 0) {
flag := "--limit"
if rt.Changed("size") {
flag = "--size"
}
return localChatOptionError("invalid_page_size", "+chat-messages 的 "+flag+" 必须大于 0", flag)
}
if boundary := rt.StrFirst("time", "before"); boundary != "" && !validChatTime(boundary) {
flag := "--time"
if rt.Changed("before") {
flag = "--before"
}
return localChatOptionError("invalid_time_boundary", "+chat-messages 的 "+flag+" 格式无效", flag)
}
if rt.Bool("page-all") {
return apperrors.NewValidation(
"+chat-messages 暂不支持 --page-all",
apperrors.WithReason("page_all_not_supported"),
apperrors.WithActions("先执行单页查询,再使用返回的游标继续翻页"),
apperrors.WithExamples(`dws chat +chat-messages --group <openConversationId> --time "2026-07-30 16:51:39" --direction older --format json`),
)
}
return nil
}
// chatMessageItems defensively unwraps the message list from the response,
// tolerating the common container keys and one level of nesting under a
// "result"/"data" wrapper.
@@ -218,20 +218,6 @@ func TestCrossPlatformCoverageCompatibilityAliases(t *testing.T) {
wantTool: "search_messages",
wantArgs: map[string]any{"openConversationIds": []string{"cid-1"}, "keyword": "树莓派"},
},
{
name: "dm name alias",
argv: []string{"chat", "+dm", "--name", "张三", "--text", "你好", "--yes"},
wantProduct: "chat",
wantTool: "send_personal_message",
wantArgs: map[string]any{"receiverOpenDingTalkId": "open1"},
},
{
name: "chat members list id alias",
argv: []string{"chat", "+chat-members-list", "--chat-id", "cid-1", "--member-types", "user"},
wantProduct: "chat",
wantTool: "get_group_members",
wantArgs: map[string]any{"openconversation_id": "cid-1"},
},
}
for _, tc := range tests {
@@ -260,117 +246,3 @@ func TestCrossPlatformCoverageCompatibilityAliases(t *testing.T) {
})
}
}
func TestSendToGroupRejectsNonNameInputsBeforeMCP(t *testing.T) {
tests := []struct {
name string
argv []string
want string
}{
{
name: "conversation id used as group name",
argv: []string{"chat", "+send-to-group", "--group", "cidayZx5r0T+UiMi6NrO1048A==", "--text", "你好", "--yes"},
want: "只接受群名关键词",
},
{
name: "at all belongs to messages send",
argv: []string{"chat", "+send-to-group", "--group", "项目冲刺", "--text", "你好", "--at-all", "--yes"},
want: "--at-all",
},
{
name: "idempotency belongs to messages send",
argv: []string{"chat", "+send-to-group", "--group", "项目冲刺", "--text", "你好", "--idempotency-key", "case-1", "--yes"},
want: "--idempotency-key",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tc.argv)
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("Execute() error = %v, want %q", err, tc.want)
}
if len(fake.calls) != 0 {
t.Fatalf("MCP calls = %#v, want none", fake.calls)
}
})
}
}
func TestChatMessagesBeforeAliasAndPageAllGuard(t *testing.T) {
t.Run("before maps to time and older", func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-messages", "--group", "cid-1", "--before", "2026-07-30 16:51:39"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
call := fake.calls[len(fake.calls)-1]
if call.args["time"] != "2026-07-30 16:51:39" || call.args["forward"] != false {
t.Fatalf("args = %#v", call.args)
}
})
t.Run("page all is blocked before MCP", func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-messages", "--group", "cid-1", "--page-all"})
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), "暂不支持 --page-all") {
t.Fatalf("Execute() error = %v", err)
}
if len(fake.calls) != 0 {
t.Fatalf("MCP calls = %#v, want none", fake.calls)
}
})
}
func TestRelatedChatOptionsRejectBeforeMCP(t *testing.T) {
tests := []struct {
name string
argv []string
want string
}{
{"group members rejects cid as name", []string{"chat", "+group-members", "--group", "cidayZx5r0T+UiMi6NrO1048A=="}, "--group"},
{"members list rejects cid as name", []string{"chat", "+chat-members-list", "--group", "cidayZx5r0T+UiMi6NrO1048A=="}, "--group"},
{"members list rejects name as cid", []string{"chat", "+chat-members-list", "--conversation-id", "测试群"}, "--conversation-id"},
{"members list reports id alias", []string{"chat", "+chat-members-list", "--id", "测试群"}, "--id"},
{"members list rejects unknown member type", []string{"chat", "+chat-members-list", "--group", "测试群", "--member-types", "admin"}, "--member-types"},
{"unread chats rejects nonpositive count", []string{"chat", "+unread-chats", "--count", "0"}, "--count"},
{"messages rejects time and before", []string{"chat", "+chat-messages", "--group", "cid", "--time", "2026-08-01", "--before", "2026-08-02"}, "--time"},
{"messages rejects before newer", []string{"chat", "+chat-messages", "--group", "cid", "--before", "2026-08-02", "--direction", "newer"}, "--direction"},
{"messages rejects group name as cid", []string{"chat", "+chat-messages", "--group", "测试群"}, "--group"},
{"messages rejects nonpositive limit", []string{"chat", "+chat-messages", "--group", "cid", "--limit", "0"}, "--limit"},
{"messages reports size alias", []string{"chat", "+chat-messages", "--group", "cid", "--size", "0"}, "--size"},
{"messages rejects invalid time", []string{"chat", "+chat-messages", "--group", "cid", "--time", "yesterday-ish"}, "--time"},
{"messages reports before alias", []string{"chat", "+chat-messages", "--group", "cid", "--before", "yesterday-ish"}, "--before"},
{"messages reports conversation id alias", []string{"chat", "+chat-messages", "--conversation-id", "测试群"}, "--conversation-id"},
{"thread replies rejects group name as cid", []string{"chat", "+thread-replies", "--group", "测试群", "--thread-id", "thread-1"}, "--group"},
{"thread replies rejects nonpositive limit", []string{"chat", "+thread-replies", "--group", "cid", "--thread-id", "thread-1", "--limit", "0"}, "--limit"},
{"thread replies rejects invalid time", []string{"chat", "+thread-replies", "--group", "cid", "--thread-id", "thread-1", "--time", "last-week"}, "--time"},
{"at me rejects nonpositive days", []string{"chat", "+at-me", "--days", "0"}, "--days"},
{"at me rejects oversized days", []string{"chat", "+at-me", "--days", "3651"}, "--days"},
{"at me rejects nonpositive limit", []string{"chat", "+at-me", "--limit", "0"}, "--limit"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tc.argv)
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("Execute() error = %v, want %q", err, tc.want)
}
if len(fake.calls) != 0 {
t.Fatalf("MCP calls = %#v, want none", fake.calls)
}
})
}
}
+2 -7
View File
@@ -40,21 +40,16 @@ var DM = shortcut.Shortcut{
"内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
{Name: "to", Type: shortcut.FlagString, Desc: "收件人姓名/花名(必填)"},
{Name: "name", Type: shortcut.FlagString, Desc: "--to 的兼容别名", Hidden: true},
{Name: "keyword", Type: shortcut.FlagString, Desc: "--to 的兼容别名", Hidden: true},
{Name: "to", Type: shortcut.FlagString, Desc: "收件人姓名/花名", Required: true},
{Name: "text", Type: shortcut.FlagString, Desc: "消息内容(支持 Markdown)", Required: true},
shortcut.AIMessageTagFlag(),
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"to", "name", "keyword"}},
},
Tips: []string{`dws chat +dm --to 张三 --text "周报发我一下"`},
Execute: func(rt *shortcut.RuntimeContext) error {
text := rt.Str("text")
// Step 1 — resolve the recipient name to a unique userId.
user, err := resolveOpenDingTalkUser(rt, rt.StrFirst("to", "name", "keyword"))
user, err := resolveOpenDingTalkUser(rt, rt.Str("to"))
if err != nil {
return err
}
+2 -31
View File
@@ -47,12 +47,6 @@ var GroupMembers = shortcut.Shortcut{
{Name: "group", Type: shortcut.FlagString, Desc: "群名称(搜群关键词,用群名里连续的核心词)", Required: true},
},
Tips: []string{`dws chat +group-members --group 项目冲刺`},
Validate: func(rt *shortcut.RuntimeContext) error {
if looksLikeOpenConversationID(rt.Str("group")) {
return localChatOptionError("group_name_expected", "+group-members 的 --group 需要群名,当前值像 openConversationId", "--group")
}
return nil
},
Execute: func(rt *shortcut.RuntimeContext) error {
groupName := rt.Str("group")
@@ -115,21 +109,17 @@ var ChatMembersList = shortcut.Shortcut{
Flags: []shortcut.Flag{
{Name: "group", Type: shortcut.FlagString, Desc: "群名称(与 --conversation-id 二选一)"},
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "群 openConversationId(与 --group 二选一)"},
{Name: "id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
{Name: "chat-id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
{Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true},
{Name: "member-types", Type: shortcut.FlagStringSlice, Desc: "成员类型:user,bot;不传则同时返回"},
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id", "id", "chat-id", "open-conversation-id"}},
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id"}},
},
Tips: []string{
`dws chat +chat-members-list --group "项目冲刺"`,
`dws chat +chat-members-list --conversation-id <openConversationId> --member-types user,bot`,
},
Validate: validateChatMembersList,
Execute: func(rt *shortcut.RuntimeContext) error {
groupID := strings.TrimSpace(rt.StrFirst("conversation-id", "id", "chat-id", "open-conversation-id"))
groupID := strings.TrimSpace(rt.Str("conversation-id"))
groupName := strings.TrimSpace(rt.Str("group"))
if groupID == "" {
resolved, err := resolveGroupName(rt, groupName)
@@ -201,25 +191,6 @@ var ChatMembersList = shortcut.Shortcut{
},
}
func validateChatMembersList(rt *shortcut.RuntimeContext) error {
groupName := strings.TrimSpace(rt.Str("group"))
groupID := strings.TrimSpace(rt.StrFirst("conversation-id", "id", "chat-id", "open-conversation-id"))
if groupName != "" && looksLikeOpenConversationID(groupName) {
return localChatOptionError("conversation_id_used_as_group_name", "+chat-members-list 的 --group 需要群名,当前值像 openConversationId", "--group")
}
if groupID != "" && looksLikeHumanGroupName(groupID) {
flag := changedConversationIDFlag(rt)
return localChatOptionError("group_name_used_as_conversation_id", "+chat-members-list 的 "+flag+" 需要 openConversationId,当前值像群名", flag)
}
if rt.Changed("member-types") {
users, bots, err := resolveMemberTypes(rt.StrSlice("member-types"))
if err != nil || (!users && !bots) {
return localChatOptionError("invalid_member_types", "+chat-members-list 的 --member-types 包含不支持的值", "--member-types")
}
}
return nil
}
func resolveGroupName(rt *shortcut.RuntimeContext, groupName string) (string, error) {
data, err := rt.CallMCPData("im", "search_groups", map[string]any{
"keyword": groupName,
@@ -1,67 +0,0 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package smart
import (
"strings"
"time"
"unicode"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func looksLikeOpenConversationID(value string) bool {
value = strings.TrimSpace(value)
if !strings.HasPrefix(strings.ToLower(value), "cid") {
return false
}
return len(value) >= 24 || strings.ContainsAny(value, "/+=")
}
func looksLikeHumanGroupName(value string) bool {
value = strings.TrimSpace(value)
if value == "" || looksLikeOpenConversationID(value) || strings.EqualFold(value, "cid") {
return false
}
for _, r := range value {
if unicode.Is(unicode.Han, r) || unicode.IsSpace(r) {
return true
}
}
return false
}
func validChatTime(value string) bool {
value = strings.TrimSpace(value)
for _, layout := range []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"} {
if _, err := time.Parse(layout, value); err == nil {
return true
}
}
return false
}
func changedConversationIDFlag(rt *shortcut.RuntimeContext) string {
for _, name := range []string{"group", "conversation-id", "id", "chat-id", "open-conversation-id"} {
if rt.Changed(name) {
return "--" + name
}
}
return "会话 ID 参数"
}
func localChatOptionError(reason, message string, flags ...string) error {
flagText := strings.Join(flags, "、")
action := "修正参数后重试,或查看当前命令帮助"
if flagText != "" {
action = "检查 " + flagText + " 后重试,或查看当前命令帮助"
}
return apperrors.NewValidation(
message,
apperrors.WithReason(reason),
apperrors.WithActions(action),
apperrors.WithExamples("dws chat --help"),
)
}
+1 -33
View File
@@ -45,12 +45,9 @@ var SendToGroup = shortcut.Shortcut{
Flags: []shortcut.Flag{
{Name: "group", Type: shortcut.FlagString, Desc: "群名称(搜群关键词,用群名里连续的核心词)", Required: true},
{Name: "text", Type: shortcut.FlagString, Desc: "消息内容(支持 Markdown)", Required: true},
{Name: "at-all", Type: shortcut.FlagBool, Desc: "不支持;需要 @所有人时改用 +messages-send", Hidden: true},
{Name: "idempotency-key", Type: shortcut.FlagString, Desc: "不支持;需要幂等键时改用 +messages-send", Hidden: true},
shortcut.AIMessageTagFlag(),
},
Tips: []string{`dws chat +send-to-group --group 项目冲刺 --text "今天 5 点前提交进度"`},
Validate: validateSendToGroup,
Tips: []string{`dws chat +send-to-group --group 项目冲刺 --text "今天 5 点前提交进度"`},
Execute: func(rt *shortcut.RuntimeContext) error {
groupName := rt.Str("group")
text := rt.Str("text")
@@ -85,35 +82,6 @@ var SendToGroup = shortcut.Shortcut{
},
}
func validateSendToGroup(rt *shortcut.RuntimeContext) error {
group := rt.Str("group")
if looksLikeOpenConversationID(group) {
return apperrors.NewValidation(
"+send-to-group 的 --group 只接受群名关键词,不能传 openConversationId",
apperrors.WithReason("conversation_id_used_as_group_name"),
apperrors.WithActions("已有群 openConversationId 时改用 chat +messages-send --chat-id"),
apperrors.WithExamples(`dws chat +messages-send --chat-id <openConversationId> --text "消息内容" --format json`),
)
}
if rt.Changed("at-all") || rt.Changed("idempotency-key") {
unsupported := make([]string, 0, 2)
if rt.Changed("at-all") {
unsupported = append(unsupported, "--at-all")
}
if rt.Changed("idempotency-key") {
unsupported = append(unsupported, "--idempotency-key")
}
flagText := strings.Join(unsupported, "、")
return apperrors.NewValidation(
"+send-to-group 不支持当前使用的发送选项:"+flagText,
apperrors.WithReason("unsupported_send_to_group_option"),
apperrors.WithActions("移除 "+flagText+",或改用支持该选项的消息发送命令"),
apperrors.WithExamples(`dws chat +messages-send --help`),
)
}
return nil
}
// sendGroupMatch is a single group candidate resolved from a name search.
type sendGroupMatch struct {
id string
+1 -19
View File
@@ -14,8 +14,6 @@
package smart
import (
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
chatshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
@@ -65,7 +63,7 @@ var ThreadReplies = shortcut.Shortcut{
`dws chat +thread-replies --group <openconversationId> --thread-id <threadId>`,
`dws chat +thread-replies --group <openconversationId> --thread-id <threadId> --time "2025-03-01 00:00:00" --limit 20`,
},
Validate: validateThreadReplies,
Validate: chatshortcut.ValidateMessageResourceDownload,
Execute: func(rt *shortcut.RuntimeContext) error {
// Step 1 — fetch the topic replies. Param keys (openconversationId /
// topicId / startTime / pageSize) are copied verbatim from chat.go's
@@ -107,22 +105,6 @@ var ThreadReplies = shortcut.Shortcut{
},
}
func validateThreadReplies(rt *shortcut.RuntimeContext) error {
if err := chatshortcut.ValidateMessageResourceDownload(rt); err != nil {
return err
}
if looksLikeHumanGroupName(rt.Str("group")) {
return localChatOptionError("group_name_used_as_conversation_id", "+thread-replies 的 --group 需要 openConversationId,当前值像群名", "--group")
}
if rt.Changed("limit") && rt.Int("limit") <= 0 {
return localChatOptionError("invalid_page_size", "+thread-replies 的 --limit 必须大于 0", "--limit")
}
if value := strings.TrimSpace(rt.Str("time")); value != "" && !validChatTime(value) {
return localChatOptionError("invalid_time_boundary", "+thread-replies 的 --time 格式无效", "--time")
}
return nil
}
// threadReplyItems defensively unwraps the reply list from the response,
// tolerating the common container keys and one level of nesting under a
// "result"/"data" wrapper.
-6
View File
@@ -59,12 +59,6 @@ var UnreadChats = shortcut.Shortcut{
`dws chat +unread-chats --count 20`,
`dws chat +unread-chats --exclude-muted`,
},
Validate: func(rt *shortcut.RuntimeContext) error {
if rt.Changed("count") && rt.Int("count") <= 0 {
return localChatOptionError("invalid_page_size", "+unread-chats 的 --count 必须大于 0", "--count")
}
return nil
},
Execute: func(rt *shortcut.RuntimeContext) error {
// Build params exactly like chatMessageListUnreadConversationsCmd: count is
// only sent when > 0, excludeMuted only when true.
+3 -32
View File
@@ -28,7 +28,7 @@ import (
// NewShortcutCommand builds the `dws shortcut` management command tree:
//
// dws shortcut list [--service x] [--compact] # list built-in shortcuts
// dws shortcut list [--service x] # list built-in shortcuts
// dws shortcut stats [--top N] # high-frequency usage aggregation
// dws shortcut stats --purge # clear the usage log
func NewShortcutCommand() *cobra.Command {
@@ -51,7 +51,6 @@ func newListCommand() *cobra.Command {
RunE: func(cmd *cobra.Command, _ []string) error {
svc, _ := cmd.Flags().GetString("service")
includeHidden, _ := cmd.Flags().GetBool("all")
compact, _ := cmd.Flags().GetBool("compact")
rows := make([]shortcutListRow, 0)
for _, s := range shortcut.All() {
if svc != "" && s.Service != svc {
@@ -62,37 +61,19 @@ func newListCommand() *cobra.Command {
}
rows = append(rows, newShortcutListRow(s))
}
payload := map[string]any{
return output.WriteCommandPayload(cmd, map[string]any{
"catalog": "shortcut",
"runtime_schema": true,
"count": len(rows),
"shortcuts": rows,
}
if compact {
compactRows := make([]shortcutCompactListRow, 0, len(rows))
for _, row := range rows {
compactRows = append(compactRows, row.compact())
}
payload["compact"] = true
payload["shortcuts"] = compactRows
}
return output.WriteCommandPayload(cmd, payload, output.FormatJSON)
}, output.FormatJSON)
},
}
cmd.Flags().String("service", "", "只列出指定服务的 shortcut")
cmd.Flags().Bool("all", false, "包含当前未进入公开 Catalog、但仍可直接调用的 shortcut")
cmd.Flags().Bool("compact", false, "仅返回发现和安全决策所需字段;选中后再查 leaf Schema 或完整 catalog")
return cmd
}
type shortcutCompactListRow struct {
CLIPath string `json:"cli_path"`
Risk string `json:"risk"`
Confirmation string `json:"confirmation"`
Description string `json:"description"`
Intent string `json:"intent,omitempty"`
}
type shortcutListRow struct {
Service string `json:"service"`
Command string `json:"command"`
@@ -113,16 +94,6 @@ type shortcutListRow struct {
Reviewed bool `json:"reviewed"`
}
func (r shortcutListRow) compact() shortcutCompactListRow {
return shortcutCompactListRow{
CLIPath: r.CLIPath,
Risk: r.Risk,
Confirmation: r.Confirmation,
Description: r.Description,
Intent: r.Intent,
}
}
func newShortcutListRow(s shortcut.Shortcut) shortcutListRow {
product := s.Product
if product == "" {
-51
View File
@@ -75,57 +75,6 @@ func TestCrossPlatformCoverageShortcutListFiltersHiddenAndService(t *testing.T)
}
}
func TestCrossPlatformCoverageShortcutListCompactPublishesDiscoveryContract(t *testing.T) {
shortcut.Register(shortcut.Shortcut{
Service: "chat",
Command: "+messages-send",
Description: "发送一条消息",
Intent: "用户要求发送消息",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
{Name: "text", Required: true},
},
Tips: []string{"dws chat +messages-send --text hello"},
})
cmd := newListCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetArgs([]string{"--service", "chat", "--compact"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
var payload struct {
Compact bool `json:"compact"`
Count int `json:"count"`
Shortcuts []map[string]any `json:"shortcuts"`
}
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if !payload.Compact || payload.Count != 1 || len(payload.Shortcuts) != 1 {
t.Fatalf("unexpected compact payload: %#v", payload)
}
row := payload.Shortcuts[0]
for key, want := range map[string]any{
"cli_path": "chat +messages-send",
"description": "发送一条消息",
"intent": "用户要求发送消息",
"risk": "write",
"confirmation": "user_required",
} {
if got := row[key]; got != want {
t.Fatalf("compact row %s = %#v, want %#v", key, got, want)
}
}
for _, omitted := range []string{"flags", "constraints", "examples", "primary", "reviewed"} {
if _, ok := row[omitted]; ok {
t.Fatalf("compact row unexpectedly contains %q: %#v", omitted, row)
}
}
}
func TestCrossPlatformCoverageShortcutListRowPublishesCompleteContract(t *testing.T) {
row := newShortcutListRow(shortcut.Shortcut{
Service: "chat",
+5 -22
View File
@@ -50,17 +50,12 @@ PRODUCT_START = "<!-- VISIBLE_SHORTCUTS_START -->"
PRODUCT_END = "<!-- VISIBLE_SHORTCUTS_END -->"
# Large, high-frequency product skills should route known intents directly and
# keep their full shortcut inventory in the Runtime Shortcut Catalog. Curated
# entries may additionally be queried through leaf Schema. Add services here
# only after verifying that the product skill has its own reviewed routing
# keep their full shortcut inventory in Runtime Catalog/Schema. Add services
# here only after verifying that the product skill has its own reviewed routing
# section and intent table; compacting a sparse skill without an alternative
# route would make its shortcuts harder to discover.
COMPACT_PRODUCT_SERVICES = {"aitable", "chat", "doc"}
COMPACT_PRODUCT_SERVICES = {"chat"}
# These compact products have every public Shortcut curated into Runtime
# Schema. Keep this separate from COMPACT_PRODUCT_SERVICES because Chat still
# has reviewed exclusions pending curation.
FULLY_CURATED_COMPACT_PRODUCT_SERVICES = {"doc"}
def md_escape(value: Any) -> str:
text = str(value or "")
@@ -140,24 +135,12 @@ def product_section(service: str, rows: list[dict[str, Any]]) -> str:
def compact_product_section(service: str, rows: list[dict[str, Any]]) -> str:
if service in FULLY_CURATED_COMPACT_PRODUCT_SERVICES:
inventory = (
f"`{md_escape(service)}` 当前有 {len(rows)} 条公开 Shortcut,"
"已全部进入 Runtime Schema。完整清单保留在 Runtime Shortcut "
"Catalog,根 Skill 不重复展开;单条参数与安全契约按需查询 leaf Schema。"
)
else:
inventory = (
f"`{md_escape(service)}` 当前有 {len(rows)} 条公开 shortcut。"
"完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation "
"的子集可通过 leaf Schema 查询。高频产品根 Skill 不重复展开完整清单。"
)
return f"""{PRODUCT_START}
## Shortcut 发现(按需)
{inventory}已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
`{md_escape(service)}` 当前有 {len(rows)} 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service {md_escape(service)} --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service {md_escape(service)} --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
{PRODUCT_END}"""
+2 -48
View File
@@ -7,12 +7,8 @@ cd "$ROOT"
python3 scripts/gen_skill_shortcut_sections.py --check
chat_skill="skills/multi/dingtalk-chat/SKILL.md"
aitable_skill="skills/multi/dingtalk-aitable/SKILL.md"
doc_skill="skills/multi/dingtalk-doc/SKILL.md"
mono_skill="skills/mono/SKILL.md"
chat_max_bytes=14000
aitable_max_bytes=11000
doc_max_bytes=9500
chat_bytes="$(wc -c < "$chat_skill" | tr -d ' ')"
if [ "$chat_bytes" -gt "$chat_max_bytes" ]; then
@@ -21,25 +17,11 @@ if [ "$chat_bytes" -gt "$chat_max_bytes" ]; then
exit 1
fi
aitable_bytes="$(wc -c < "$aitable_skill" | tr -d ' ')"
if [ "$aitable_bytes" -gt "$aitable_max_bytes" ]; then
printf '%s\n' \
"skill context budget exceeded: $aitable_skill is ${aitable_bytes} bytes (max ${aitable_max_bytes})" >&2
exit 1
fi
doc_bytes="$(wc -c < "$doc_skill" | tr -d ' ')"
if [ "$doc_bytes" -gt "$doc_max_bytes" ]; then
printf '%s\n' \
"skill context budget exceeded: $doc_skill is ${doc_bytes} bytes (max ${doc_max_bytes})" >&2
exit 1
fi
shortcut_rows="$(
awk '
/<!-- VISIBLE_SHORTCUTS_START -->/ { in_block = 1; next }
/<!-- VISIBLE_SHORTCUTS_END -->/ { in_block = 0 }
in_block && /^\|[[:space:]]*`/ { count++ }
in_block && /^\| `dws chat \+/ { count++ }
END { print count + 0 }
' "$chat_skill"
)"
@@ -49,34 +31,6 @@ if [ "$shortcut_rows" -ne 0 ]; then
exit 1
fi
aitable_shortcut_rows="$(
awk '
/<!-- VISIBLE_SHORTCUTS_START -->/ { in_block = 1; next }
/<!-- VISIBLE_SHORTCUTS_END -->/ { in_block = 0 }
in_block && /^\|[[:space:]]*`/ { count++ }
END { print count + 0 }
' "$aitable_skill"
)"
if [ "$aitable_shortcut_rows" -ne 0 ]; then
printf '%s\n' \
"skill context budget exceeded: $aitable_skill re-expanded $aitable_shortcut_rows shortcut rows" >&2
exit 1
fi
doc_shortcut_rows="$(
awk '
/<!-- VISIBLE_SHORTCUTS_START -->/ { in_block = 1; next }
/<!-- VISIBLE_SHORTCUTS_END -->/ { in_block = 0 }
in_block && /^\|[[:space:]]*`/ { count++ }
END { print count + 0 }
' "$doc_skill"
)"
if [ "$doc_shortcut_rows" -ne 0 ]; then
printf '%s\n' \
"skill context budget exceeded: $doc_skill re-expanded $doc_shortcut_rows shortcut rows" >&2
exit 1
fi
if grep -Fq "充分阅读产品参考文件" "$mono_skill"; then
printf '%s\n' \
"skill context budget regression: $mono_skill requires full product-reference loading" >&2
@@ -84,4 +38,4 @@ if grep -Fq "充分阅读产品参考文件" "$mono_skill"; then
fi
printf '%s\n' \
"skill context budget: ok (chat_bytes=$chat_bytes max=$chat_max_bytes shortcut_rows=$shortcut_rows; aitable_bytes=$aitable_bytes max=$aitable_max_bytes shortcut_rows=$aitable_shortcut_rows; doc_bytes=$doc_bytes max=$doc_max_bytes shortcut_rows=$doc_shortcut_rows)"
"skill context budget: ok (chat_bytes=$chat_bytes max=$chat_max_bytes shortcut_rows=$shortcut_rows)"
+4 -5
View File
@@ -1,8 +1,7 @@
---
name: dws
description: 管理钉钉产品能力(AI表格/AI搜问/日历/通讯录/群聊与机器人/待办/审批/考勤/日志/DING消息/开放平台文档/钉钉文档/钉钉云盘/原生Markdown文件/AI听记/邮箱/在线电子表格/知识库等)。当用户需要操作表格数据、管理日程会议、模糊找人/查谁负责某事项、查询通讯录、管理群聊、机器人发消息、创建待办、提交审批、查看考勤、提交日报周报(钉钉日志模版)、读写钉钉文档、上传下载云盘文件、读取或修改原生.md文件、查询听记纪要、收发邮件、读写在线电子表格(axls)、管理钉钉知识库,或订阅个人 IM 事件、实时监听群成员加入、群成员退出、群改名和群解散时使用。
metadata:
cli_version: ">=1.0.15"
cli_version: ">=1.0.15"
---
# 钉钉全产品 Skill
@@ -22,15 +21,15 @@ metadata:
- 危险操作必须先向用户确认,用户同意后才加 `--yes` 执行
- 单次批量操作不超过 30 条记录
- 所有命令必须**严格遵循**对应产品参考文档里面规定的参数格式(如:如果有参数值,则参数和参数值之间至少用一个空格隔开)
- **CLI 路径必须独立可用**:不要假定用户环境安装了 Python。优先使用 `dws` 原生命令或 Shortcut;[scripts/](./scripts/) 仅在对应运行时已确认可用且能明显简化翻页、轮询或批量操作时作为可选加速项。脚本不可用时直接执行同场景的原生命令流程,不得把缺少 Python 当成能力阻塞
- **脚本优先**:[scripts/](./scripts/) 下的 `python scripts/<name>.py` 已封装翻页/轮询/批量逻辑,遇到对应场景(如 AI 表格批量导入导出、AI 应用创建轮询、文档创建后写内容、钉盘目录树等)**优先调用脚本**而非手写多步命令。脚本均支持 `--dry-run` 预览、`--format json` 输出,失败时回退到手动步骤
- **实时个人消息事件例外**:用户要监听消息、订阅事件、自动回复消息或事件驱动 Agent 时,必须走 `dws event consume ... --flatten` 长连接,不要写脚本轮询消息历史
## Shortcut 与原子命令的使用原则
`shortcut` 是对常用操作的高层封装,适合优先承担用户意图;产品参考文档和本 skill 负责判断意图、风险、跨产品流程和复杂参数,CLI 帮助负责声明当前版本真正可调用的命令。
- 先按产品参考、意图表和 recipe 路由。精确 recipe 或对应解释器(如 `python3`)已可用的专用脚本优先;解释器不可用时直接跳过脚本,不得阻塞任务。否则用户意图可由可见 shortcut 满足时,优先使用 `dws <service> +<verb> ... --format json`,不要手写等价的多步原子命令。
- 公开内建 shortcut 以 `dws shortcut list --service <service> --format json` 为动态 catalog;已进入 Runtime Schema 的 leaf 用 `dws schema --cli-path "<service> +<verb>" --format json` 读取 Agent 选择、参数、跨参数约束、risk/confirmation 与接口语义。若 leaf 暂未进入 Schema,则使用 catalog 中同一 `cli_path` 的完整契约。
- 先按产品参考、意图表和 recipe 路由。存在精确覆盖场景的专用脚本/recipe 时继续遵循“脚本优先”;否则用户意图可由可见 shortcut 满足时,优先使用 `dws <service> +<verb> ... --format json`,不要手写等价的多步原子命令。
- 公开内建 shortcut 同时进入 Runtime Schema。用 `dws schema --cli-path "<service> +<verb>" --format json` 读取 Agent 选择、参数、跨参数约束、risk/confirmation 与接口语义;`dws shortcut list --service <service> --format json` 只作为轻量批量发现入口。
- 真正组装参数前用叶子帮助 `dws <service> +<verb> --help` 核对当前 Cobra 接受的 flags。父级 `dws <service> --help` 只能发现子命令,不能替代叶子参数帮助。
- shortcut catalog 中 `confirmation=user_required` 时,必须先获得用户确认,确认后才加 `--yes`;`not_required` 不额外确认。
- 如果 shortcut 不在 help / list 中,改用产品参考里的原子命令、脚本或标准流程;不要猜测未展示的 `+` 命令。
@@ -4,10 +4,10 @@
| Recipe | 行动指南(固定路线) |
|--------|-------------------|
| query-group-chat | 1. `chat search --query "<群名>"` → 取 `openConversationId`<br>2. `chat message list --group <openConversationId> --time "<yyyy-MM-dd HH:mm:ss>" --direction newer --format json`<br>3. **翻页**:`hasMore=true` 时取本页最后一条消息的 `createTime` 作为下一次 `--time`,保持原 `--direction`,禁止自造 cursor/time;直到 `hasMore=false`<br>4. `--direction older` 拉给定时间**之前**的消息<br>5. 合并结果;用户要求导出时由 Agent 将结构化输出写入目标文件 |
| query-private-chat | 1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId` / `openDingTalkId`<br>2. `chat message list --user <userId> --time "<yyyy-MM-dd HH:mm:ss>" --direction newer --format json`;跨组织时可用 `--open-dingtalk-id`<br>3. **翻页**:同 query-group-chat<br>4. 合并结果后总结或按用户要求保存 |
| query-group-chat | **优先**:`chat_export_messages.py`(开源版未引入;可手动用 `dws chat message list` 翻页后写入文件)(自动搜群+翻页+导出)<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-private-chat | **优先**:`chat_history_with_user.py`(开源版未引入;可手动用 `dws chat search` + `dws chat message list` 组合)(自动搜人+翻页+导出)<br>备选:1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`<br>2. `chat message list --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 "<内容>"`(必填项见 [ding.md](../products/ding.md))<br>2. `chat message send --group <openConversationId> --text "<内容>"` 群里提醒(可选 `--title` / `@` 见 [chat.md](../products/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 | 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 "<内容>"`<br>4. 多群时先一次性确认全部目标与内容,再对每个已确认群逐一执行同一命令;汇总每群成功/失败结果 |
| forward-message | 1. `chat search --query "<群名>"` → 取 `openConversationId` → `chat +chat-messages --group <openConversationId> --time "<起始时间>"` 拉源消息<br>2. `contact user search --query "<姓名>"` → 取 `openDingTalkId`(推荐);或 `chat search --query "<群名>"` → 取目标 `openConversationId`<br>3. `chat +messages-send --as user --open-dingtalk-id <openDingTalkId> --text "<内容>"`(推荐)或 `--chat-id <openConversationId>` 发送。仅当无法获取 openDingTalkId 时才用 `--user <userId>`(备选) |
| send-by-bot | **多群批量优先**:`bot_broadcast.py`(开源版未引入;可手动用 `dws chat message send-by-bot` 多次调用)<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 "<内容>"` |
| forward-message | 1. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message list --group <openConversationId> --time "<起始时间>"` 拉源消息<br>2. `contact user search --query "<姓名>"` → 取 `openDingTalkId`(推荐);或 `chat search --query "<群名>"` → 取目标 `openConversationId`<br>3. `chat message send --open-dingtalk-id <openDingTalkId> --text "<内容>"`(推荐)或 `--group <openConversationId> --text "<内容>"` 发送。仅当无法获取 openDingTalkId 时才用 `--user <userId>`(备选) |
| search-common-group | `chat search-common --nicks "<昵称1>,<昵称2>" --limit 20 --cursor 0`(`--match-mode AND`=全在/`OR`=任一在,翻页:`hasMore=true` 时用 `nextCursor`)<br>用户说"我和XX的共同群" → nicks 包含"我"时,需先 `contact user get-self` 取自己昵称再拼接 |
| focus-messages | **零参数一行命令**:`chat message list-focused --limit 50`(拉特别关注人发的消息聚合)<br>触发 query:`"我特别关注的人最近发了什么消息"`、`"关注的人最近聊了啥"`、`"星标联系人最近的动态"`<br>**强消歧**:query 含动词【发/说/聊/讲】或名词【消息/聊天/动态】 → **必须**走本命令,**不要**先去拉 `contact relation list-my-followings`;仅当用户终点是"人员列表"(如"我关注了谁")才走 `relation list-my-followings`(详见 [contact.md](../products/contact.md#意图判断) 易混淆硬规则)<br>翻页:`hasMore=true` 时用 `nextCursor` 作为下次 `--cursor`<br>按人精控(可选):先 `contact relation list-my-followings` 取 `openDingTalkId`,再 `chat message list-by-sender --sender-open-dingtalk-id <openDingTalkId> --start <ISO> --end <ISO>` |
@@ -2,13 +2,6 @@
> 通用规范见 [_common/conventions.md](_common/conventions.md)。
## 显式工作流与事实保真
- 用户点名的 `create → list → insert/append/update` 是可观察命令链,必须保持顺序逐项执行;create 只承载明确的初始正文。有序列表块必须验证回读结构中的 `list.isOrdered=true`。
- `--name` 不替代用户显式要求的正文 H1;新建资源返回 ID 后,同一请求的指代绑定该新资源,禁止搜索同名旧资源替换。
- Word/Excel 需要“在线编辑/直接在线改”时使用 `doc import`,普通 `drive upload` 只保留原文件。
- 汇总时保留证据强度:验证数量不等于通过数量,整理问题不等于根因分析。任一步骤返回 `null`/空结果或回查不一致时只能报告部分完成。
| Recipe | 行动指南(固定路线) |
|--------|-------------------|
| write-doc | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行<br>2. **先把内容写入临时文件**(Linux/Mac `/tmp/<name>.md`,Windows `%TEMP%\<name>.md`)—— 含多行/表格/长文本必须走文件,不要把 markdown 直接作为命令行字符串<br>3. **单步创建**(< 200KB):`doc create --name "<文档名>" --content-file <tmp> [--folder <DOC_FOLDER_NODE_ID>] [--workspace <WS_ID>]`(`--folder` 只传文档文件夹 nodeId / alidocs 文件夹 URL,不传数字 dentryId)<br>4. **超长兜底**(> 200KB):**必须先向用户提示截断风险**(详见下方「分块 append 截断风险提示」),用户确认后再执行:`doc create --name "<文档名>" [--folder/--workspace]` → `nodeId` → 按段落切 ≤200KB 片段(不断表格) → 每片 `doc update --node <nodeId> --content-file <part> --mode append`<br>5. **回读校验**(必须):所有写入完成后,执行 `doc read --node <nodeId>` 回读文档,校验关键标题/段落是否完整写入(详见下方「doc update 回读校验规范」)<br>备选(仅短内容 <2KB 且无换行/表格):`doc create --name "..." --content "..."` |
@@ -105,6 +105,7 @@
| 命令 | 用途 | 必填参数 | 路由提醒 |
|------|------|----------|----------|
| `workflow edit-example` | 获取编辑文档与 DSL 示例 | 无 | create/update 前优先调用,内容由服务端提供 |
| `workflow create` | 创建并发布工作流 | `--base-id` `--dsl` | `--dsl` 为完整 workflow-dsl/v1;非幂等,不自动重试 |
| `workflow update` | 更新并发布工作流 | `--base-id` `--workflow-id` `--dsl` | 全量替换,先 get 留底;检查 `data.valid/issues` |
| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` |
@@ -7,6 +7,7 @@
| 命令 | 用途 |
|------|------|
| `workflow edit-example` | 获取工作流编辑文档与 workflow-dsl/v1 示例 |
| `workflow create` | 创建并发布自动化工作流 |
| `workflow update` | 更新并发布已有自动化工作流 |
| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 |
@@ -14,11 +15,11 @@
| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) |
| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) |
> 所有子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
> `workflow edit-example` 无参数;其他子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
## DSL 入参格式与最小 Demo
`workflow create/update` 的 `--dsl` 接收完整的 `workflow-dsl/v1` JSON object,不是局部 patch。支持内联 JSON、`@文件路径` 或 `-` 从 stdin 读取。
先运行 `workflow edit-example` 获取服务端提供的最新编辑文档和示例。`workflow create/update` 的 `--dsl` 接收完整的 `workflow-dsl/v1` JSON object,不是局部 patch。支持内联 JSON、`@文件路径` 或 `-` 从 stdin 读取。
复杂工作流应先用 `table get` / `field get` / `view list` 确认真实 `sheetId`、`fieldId`、`viewId`,并检查所有 `next`、`loopEntry`、branch `to` 和 ref。下面是一个不依赖数据表字段的最小定时消息工作流:
@@ -53,6 +54,14 @@ create 和 update 都必须同时满足 `status=success`、`data.valid=true`、`
## 命令详情
### workflow edit-example — 获取编辑文档与示例
```bash
dws aitable workflow edit-example --format json
```
该命令无业务参数,调用 `aitable/edit_workflow_example` 返回服务端提供的工作流编辑文档和示例。创建或更新复杂工作流前优先调用它,避免依赖可能过期的本地 DSL 结构。
### workflow create — 创建并发布工作流
```bash
+14 -28
View File
@@ -4,26 +4,24 @@
## Shortcut 优先路由
精确脚本 / recipe 未覆盖时,常见 Agent 意图优先使用公开 `+` Shortcut;下面的原子命令章节保留给需要特定原始返回结构、兼容参数或 Shortcut 未覆盖字段的场景。先尝试 `dws shortcut list --service chat --compact --format json` 动态发现;当前 CLI 报 `unknown flag: --compact` 时,立即去掉 `--compact` 重试。选中后读取 `dws schema --cli-path "<cli_path>" --format json`;若 leaf 暂未进入 Schema,则以完整 Catalog 中同一 `cli_path` 的契约为准,最后用 `dws <cli_path> --help` 核对真实 flags。
常见 Agent 意图优先使用公开 `+` Shortcut;下面的原子命令章节保留给需要特定原始返回结构、兼容参数或 Shortcut 未覆盖字段的场景。执行前用 `dws schema --cli-path "chat +<shortcut>" --format json` 读取最终参数、约束和确认语义。
| 意图 | 首选 |
|---|---|
| 以 current-user / bot / webhook 身份发消息 | `dws chat +messages-send --as <identity> ...` |
| 拉取单个群聊或单聊的消息 | `dws chat +chat-messages ...` |
| 按关键词、发送者、@对象、会话、类型或时间组合搜索 | `dws chat +search-msg ...` |
| 查询 @ 我的消息 | `dws chat +at-me ...` |
| 查询 @我的消息 | `dws chat +at-me ...` |
| 根据消息 ID 批量取详情与 reaction | `dws chat +messages-mget ...` |
| 读取已知 thread/topic 的全部回复 | `dws chat +thread-replies ...` |
| 下载单个 mediaId/fileId | `dws chat +messages-resource-download ...` |
| 创建并按需立即更新流式卡片 | `dws chat +messages-send-card ...` |
- `+messages-send` 只暴露下层真实支持的身份能力:user 支持文本/Markdown、已有 mediaId 图片、本地文件和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。
- `+messages-send` 自动规范化并补齐对应身份的 @ 占位符。声明 `--at-*` / `--at-all` 即可,不要为统一 Shortcut 手工拼 `@10`。
- `+messages-send` 会自动规范化并补齐 @ 占位符。user 使用 `<@id>` / `<@all>`;bot/webhook 使用 `@id` / `@手机号` / `@all`。声明 `--at-*` / `--at-all` 即可,不要为统一 Shortcut 手工拼 `@10`。
- `+search-msg --page-all` 连续翻页并默认按消息 ID 批量富化;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger。
- `+at-me`、`+chat-messages`、`+messages-mget`、`+search-msg`、`+thread-replies` 可用 `--download-resources` 下载资源。引用、回复、合并转发中的资源使用结果 `resourceRefs` 自带的子消息 `messageId`;仅当子消息缺会话 ID 时继承父消息 `openConversationId`。
- 上述五个查询 Shortcut 与 `+messages-resource-download` 都是 `read/not_required`,不要添加 `--yes`。下载只允许工作目录内相对路径、默认不覆盖并原子落盘;需要覆盖时必须由用户显式传 `--overwrite`。
- 下载器只接受经审查的钉钉与公网 OSS HTTPS 地址并逐跳校验重定向;跨主机时不会转发下层提供的请求头。新官方域名被拒绝时记录错误中的 host 供审查,不要放宽为任意 HTTPS。
- `+messages-send-card` 的目标为 `--group`、`--receiver`、`--receiver-open-dingtalk-id` 三选一;传 `--content` 时创建并立即更新,不传时返回 `bizId` 供 `message update-card` 使用。
- 上述五个查询 Shortcut 与 `+messages-resource-download` 都沿用安全本地下载的 `read/not_required` 契约,不应添加 `--yes` 或触发交互确认。下载只允许工作目录内相对路径、默认不覆盖并原子落盘;需要覆盖时必须由用户显式传 `--overwrite`。
- 下载器仅接受经审查的钉钉与公网 OSS HTTPS 地址并逐跳校验重定向;跨主机时不会转发下层提供的请求头。新官方域名被拒绝时记录错误中的 host 供审查,不要放宽为任意 HTTPS。
### group (群组管理)
@@ -1171,10 +1169,6 @@ Flags:
#### 下载消息中的资源(图片/视频/语音等)到本地
读取消息时优先在 `+at-me`、`+chat-messages`、`+messages-mget`、`+search-msg`、`+thread-replies` 上加 `--download-resources --output-dir ./downloads`。只下载单个已知资源时优先用 `+messages-resource-download`:mediaId 必须同时传 `--message-id` 和 `--open-conversation-id`,fileId 不需要消息上下文。输出必须是工作目录内相对路径且不能包含 `..`;默认不覆盖,用户明确要求时才传 `--overwrite`。该 Shortcut 暂无 leaf Schema 时按完整 Catalog + 叶子 Help 核对。
以下原子命令保留给需要原始接口行为的场景。
下载聊天消息中的图片、视频、语音等资源到本地文件。流程:先获取下载 URL,再 HTTP GET 下载。
```
Usage:
@@ -1545,12 +1539,10 @@ Flags:
Usage:
dws chat category delete [flags]
Example:
# 先向用户确认,确认后执行
dws chat category delete --category-id <分组ID> --yes
dws chat category delete --category-id <分组ID>
# 分组ID 可通过 dws chat category list 获取
Flags:
--category-id int 会话分组 ID (必填)
--yes 跳过运行时确认;只能在用户明确确认后传入
```
#### 重命名会话分组
@@ -1779,18 +1771,15 @@ Flags:
Usage:
dws chat clear-messages [flags]
Example:
# 先向用户说明目标会话和影响范围,确认后执行
dws chat clear-messages --conversation-id <openConversationId> --yes
dws chat clear-messages --id <openConversationId> --yes
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 的别名
--yes 跳过运行时确认;只能在用户明确确认后传入
注意:
- 仅清空当前用户视角的消息,不影响其他成员
- 仍属于高风险操作;未确认时不得传 --yes 或执行
- openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取
```
@@ -2212,7 +2201,7 @@ dws chat message send --group <openConversationId> --msg-type image --media-id "
群聊传 --group,单聊传 --receiver,二者互斥。
**注意:本节原子 send-card 必须和 update-card 搭配使用。** 创建卡片时无需传入内容,后续通过 update-card 更新内容,最后一次更新必须将 --flow-status 设为 3(finish),否则卡片会一直处于"生成中"的加载状态。若使用 `+messages-send-card --content ...`,Shortcut 会在创建后立即完成一次更新;不传 `--content` 时仍返回 `bizId` 供本节 update-card 使用。
**注意:send-card 必须和 update-card 搭配使用。** 创建卡片时无需传入内容,后续通过 update-card 更新内容,最后一次更新必须将 --flow-status 设为 3(finish),否则卡片会一直处于"生成中"的加载状态。
flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成(FINISH),4=执行中(EXECUTING),5=错误(ERROR)。
```
Usage:
@@ -2331,15 +2320,12 @@ Flags:
- `chat group-mute-member` 指定群成员禁言,需传 --group、--user/--users(userId,逗号分隔)、--mute-time(毫秒,仅禁言时必填,支持 300000/3600000/86400000/604800000/2592000000),传 --off 解除禁言;CLI 会自动把 userId 解析成 openDingTalkId 再调用,直接传 userId 即可;禁言群主会被服务端拒绝
- `chat group set-admin` 设置/取消群管理员,需传 --group(openConversationId)、--user/--users(userId,逗号分隔),默认设为管理员,传 --off 取消
## 可选自动化脚本
## 自动化脚本
这些脚本仅用于已确认安装 Python 3 的环境,不是默认或唯一执行路径;无
Python 环境时直接使用上文的 `dws` Shortcut / 原子命令。
| 脚本 | 可选场景 | 用法 |
|------|----------|------|
| [chat_export_messages.py](../../scripts/chat_export_messages.py) | 导出群聊消息到 JSON 文件 | `python3 scripts/chat_export_messages.py --query "项目冲刺" --time "2026-03-10 00:00:00"` |
| [chat_history_with_user.py](../../scripts/chat_history_with_user.py) | 查询与某人的单聊聊天记录 | `python3 scripts/chat_history_with_user.py --name "张三" --time "2026-03-10 00:00:00"` |
| 脚本 | 场景 | 用法 |
|------|------|------|
| [chat_export_messages.py](../../scripts/chat_export_messages.py) | 导出群聊消息到 JSON 文件 | `python chat_export_messages.py --query "项目冲刺" --time "2026-03-10 00:00:00"` |
| [chat_history_with_user.py](../../scripts/chat_history_with_user.py) | 查询与某人的单聊聊天记录 | `python chat_history_with_user.py --name "张三" --time "2026-03-10 00:00:00"` |
## 相关产品
+7 -11
View File
@@ -665,8 +665,8 @@ Flags:
- 知识库内 → `dws wiki node create --workspace <WS_ID> --type folder`(`doc folder create` / `doc file create --type folder` 已弃用)
用户说"上传文件/传文件/上传到文档/上传到知识库":
- 仅保留原始文件用于存储/下载 → `drive upload`(需本地文件路径)
- 用户明确要求“在线编辑/大家直接在线改/转在线文档” → `doc import --file <本地路径>`;不得用普通 upload 的成功响应宣称可在线编辑
- 上传 → `upload`(需本地文件路径)
- 上传并转换 → `upload --convert`
用户说"导入文件/导入为在线文档/导入 Word/导入 Excel/导入 xmind/导入 Markdown/把本地文件转在线文档":
- 导入并转换为在线文档 → `doc import --file <本地路径>`
@@ -742,8 +742,8 @@ Flags:
关键区分: doc(文档编辑/阅读) vs aitable(数据表格操作) vs drive(钉盘文件管理)
用户说"上传文件/传文件/上传到文档/上传到知识库":
- 仅保留原始文件用于存储/下载 → `drive upload`(需本地文件路径)
- 要转换为可在线编辑文档 → `doc import --file <本地路径>`,导入后验证在线类型与目标文件夹
- 上传 → `upload`(需本地文件路径)
- 上传并转换 → `upload --convert`
用户说"下载文件/导出文件/下载到本地":
- 下载 → `download`(需文件节点 ID 或 URL)
@@ -1050,18 +1050,14 @@ EOF
- `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);要建表格/脑图/白板/多维表/演示,用 `dws wiki node create --workspace <id> --type <type>`(`doc file create` 已弃用);建普通文件夹用 `dws drive mkdir`
- `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;只有用户未指定块操作的纯文本追加才建议 `update --mode append`。用户点名 list/insert/update/append 时必须逐项真实调用,不得折叠进 create
- `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;简单内容追加建议用 `update --mode append`
- `block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型 (table, callout 等) 使用 `--element` JSON
- 用户要求“有序列表块”时必须写真实列表结构(JSONML `p.list.isOrdered=true` 或等价 orderedList element),普通 Markdown/数字前缀段落不算完成
- `--content` 参数中的换行必须使用**真实换行符**(即实际的换行字符,Unicode `U+000A`),而不是字面量字符串 `\n`(反斜杠加字母 n)。在通过程序或大模型构造此参数时,请确保字符串在发送前已正确反转义。如果传入的是两个字符的字面量 `\n`,所有内容将渲染在同一行,导致标题、段落和表格格式全部错乱。**含多行/表格/长文本时优先用 `--content-file path.md` 或 `--content -`(stdin),不经过 shell escape,换行和表格都保持原样**(详见下方「长 Markdown 写入」)。
- 块类型包括: paragraph, heading, blockquote, callout, columns, orderedList, unorderedList, table, sheet, attachment, slot
- 关键区分: doc(文档内容级操作) vs wiki(知识库空间级管理) vs aitable(数据表格操作) vs drive(钉盘文件管理)
- wiki 是知识库容器,doc 是知识库中的文档内容;需要 `workspaceId` 时,先用 `dws wiki space list/search` 获取,再传给 doc 的 `--workspace` 参数
- `drive upload` / `doc upload` 是普通文件存储路径;用户要求 Word/Excel “在线编辑/直接在线改”时硬路由到 `doc import`,并验证导入后的在线类型和文件夹。只有用户明确同时要原文件与在线版时才分别 upload + import
- 同一请求中新建、复制或导入返回的 `nodeId` 必须绑定后续“这篇/刚才那篇/上次那篇”;禁止搜索同名旧资源覆盖绑定
- `--name` 只是文档外壳标题,不能替代用户显式要求的正文 H1;用户说“正文先起一级标题”时必须写入或插入真实 H1
- 汇总只能保持用户事实强度:“验证 12 条”不等于“12 条全部通过”,“整理问题清单”不等于“输出根因分析”
- 写操作响应为 `null`/空对象或回查未变化时,该步骤失败;必须报告部分完成,禁止用其他成功步骤把整体说成“全部完成”
- `doc upload vs drive upload`:用户提到"知识库/文档空间/workspace" → `doc upload`;提到"钉盘/网盘/我的文件" → `drive upload`;未明确目标时默认 `drive upload`
- `upload` 支持上传任意类型文件 (PDF、Office、图片等) 到钉钉文档空间或知识库;`--convert` 可将 Office 文件转换为钉钉在线文档
- `upload` 是三步自动完成的流程 (获取凭证 → OSS 上传 → 提交入库),无需手动分步操作
- `download` 是两步自动完成的流程 (获取下载链接 → HTTP GET 下载),支持自动推断文件名;`--output` 可指定文件路径或目录
- `media insert` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 插入附件块到文档),无需手动分步操作
@@ -1,11 +1,15 @@
# doc block(块级精细编辑:list / insert / update / delete)
> 本文件自包含简单 list/insert/update/delete 契约,不要递归预读路由或 style reference。只有实际构造复杂 JSONML 节点时,才读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);字段仍不确定时再查 [`doc-jsonml-schema.md`](./format/doc-jsonml-schema.md)。整篇 overwrite 或纯文本 append 才转读 [`doc-update.md`](./doc-update.md)。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、JSONML validator 行为)
> 3. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — JSONML 范例(含 callout / 分栏 / 表格 / 标题等节点的完整命令)
> 4. [`./format/doc-jsonml-schema.md`](./format/doc-jsonml-schema.md) — JSONML 节点结构字段定义
>
> **同任务常配合**:[`doc-update.md`](./doc-update.md)(整篇 overwrite / 末尾追加纯文本)/ [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)(JSONML 复制范例)
> **改写已有文档优先 JSONML**:保真度最高、callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。详见 [`./style/doc-update-workflow.md` §1.3 编辑形态优先级](./style/doc-update-workflow.md)。
> **显式块操作不可折叠**:用户说“先 create,再 list/insert/update/append”时按原顺序真实调用;不能因为最终正文相似,就把后续块操作合并进 create 或一次 Markdown 写入。
---
## doc block list(查询块元素)
@@ -169,8 +173,7 @@ dws doc block delete --node DOC_ID --block-id UUID
- **块类型**:paragraph、heading、blockquote、callout、columns、orderedList、unorderedList、table、sheet、attachment、slot。
- **快捷 vs --element**:`block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型(table、callout、columns 等)使用 `--element` JSON 或 `--content-format jsonml`。
- **有序列表块**:用户明确要求 ordered list / 有序列表块时,必须用 JSONML `p` 节点的 `list.isOrdered=true`(同一 `listId`;仅首项设 `start:1`)或等价原生 orderedList element;带 `1.` 前缀的普通段落、普通 Markdown 或一次 create 不满足要求。
- **简单内容追加**:用户只说追加纯文本且不强调块操作时可用 [`./doc-update.md`](./doc-update.md) `--mode append`;用户明确说 block insert / 插入段落 / 插入标题 / 插入列表块时必须走 block insert。
- **简单内容追加**:建议用 [`./doc-update.md`](./doc-update.md) `--mode append`,不必走 block insert。
- **JSONML validator**(写入端默认行为):
- 裸字符串、缺 uuid 等结构错误会被 validator 抦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。
- `--fix-jsonml` 开启 JSON 语法修复,推荐 agent 调用。
@@ -238,12 +241,6 @@ dws doc block list --node <DOC_ID> --content-format jsonml --block-id <UUID>
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"新段落"]]]'
# 插入有序列表块(3 项共用 listId,仅首项有 start)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p",{"uuid":"ol1","list":{"listId":"actions","level":0,"isOrdered":true,"start":1}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"第一项"]]]'
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p",{"uuid":"ol2","list":{"listId":"actions","level":0,"isOrdered":true}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"第二项"]]]'
# 插入 callout(colorBlocks)
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["container",{"uuid":"co1","subType":"colorBlocks","metadata":{"bgcolor":"#FDE2E0","border":"#F5C2C7"}},["p",{"uuid":"co1p1"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"高风险操作,先备份"]]]]'
@@ -1,6 +1,9 @@
# doc comment(文档评论:list / create / reply / update / delete / create-inline)
> 本文件自包含评论命令契约。仅在需要 mention 时查询真实 userId/openConversationId;仅在划词评论尚无 blockId 与 paragraph 文本时读取 [`doc-block.md`](./doc-block.md) 并执行 block list。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
>
> **同任务常配合**:`dws contact user search`(查 `--mention` 用 userId)/ `dws chat search`(查群用 openConversationId)/ [`doc-block.md`](./doc-block.md)(划词评论必须先取 blockId 与 paragraph 文本)
---
@@ -128,7 +131,6 @@ Flags:
- 划词评论的 `--start` / `--end` 是块内文本字符偏移量,从 0 开始;通过 [`./doc-block.md`](./doc-block.md) `block list` 取 `paragraph.text` 后人工或脚本计算。
- `reply` 加 `--emoji` 时 `--content` 填表情名称(如 `比心`、`赞`),不是文字内容。
- `reply --emoji` 不能同时 @群。
- `comment create/reply/update/delete` 的退出码 0 不等于业务成功。响应为 `null`、空对象或缺少可核验字段时,立即执行 `comment list` 回查目标 `commentKey`。若 update 后正文仍是旧值,必须判定“更新未生效”;即使其他步骤成功或评论随后被删除,也只能报告部分完成,禁止写“全部完成”。
## 上下文传递
@@ -1,6 +1,11 @@
# doc create(创建文档)
> 本文件自包含普通 Markdown 创建契约,不要递归预读 `doc.md`、style 或 update reference。仅当用户要求复杂版式并实际选择 JSONML 时,读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);需要文档骨架建议时才读取对应 style 章节。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-create-workflow.md`](./style/doc-create-workflow.md) — 创建工作流(标题、位置、骨架、回读校验)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范(草稿元素清单、骨架样板)
> 4. [`./doc-update.md` §内容写入管道](./doc-update.md#内容写入管道createupdate-共用) — 长内容自动分片、`--content-file` vs `--content` 选择
> 5. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
## 创建路由前置判断(必看)
@@ -35,7 +40,7 @@ Flags:
## 关键说明
- **标题优先级**:`--name` 是文档外壳标题,默认可视作 H1;但它不能替代用户显式要求的正文一级标题。用户说“正文写 `# ...`”“正文先起个一级标题”时,必须在初始内容中保留该 `#` H1;用户未要求正文 H1 时,正文默认从 `##` 开始以避免重复。
- **`--name` 是 H1**:正文从 `##` 开始;正文内不要再写 `#` 一级标题(除非确需且已说明动机)。
- 不传 `--folder` 和 `--workspace` 时,默认创建在「我的文档」根目录。
- `--folder` 仅接受文档文件夹 `nodeId` / `dentryUuid` / alidocs 文件夹 URL;**禁止**传入 drive `dentryId`、`parentId`、`spaceId` 这类纯数字 ID。
- 输入方式选择见 [`./doc-update.md` §内容写入管道](./doc-update.md#内容写入管道createupdate-共用)(与 update 共用)。短文本字面量可 `--content`,多行/表格/特殊字符必须 `--content-file` 或 `--content -`。
@@ -49,12 +54,6 @@ Flags:
| `docUrl` | 最终交付给用户的链接;缺失时用 [`./doc-info.md`](./doc-info.md) 补查 |
| `chunksWritten` | 判断是否触发自动分片;> 1 时重点检查章节顺序 |
同一请求后续出现“这篇/刚才那篇/上次那篇”时,直接续用本次 create 返回的 `nodeId`;禁止先搜索同名文档再把后续操作指向旧节点。
## 显式操作序列
用户点名 `block list`、插入、追加、更新等后续动作时,必须按原顺序逐项执行。`doc create` 只写用户指定的初始内容,不能为了减少调用把后续标题、列表或段落提前塞进 create。例:`创建 → 查看块结构 → 末尾插入段落` 必须真实执行 create、block list、block insert 三步。
## 回读验收(必读)
CLI **不会**自动回读校验。**每次创建后**都必须执行 `doc read --node <nodeId>` 校验关键标题、段落首句、表格表头是否完整。详见 [`./style/doc-create-workflow.md` «回读验收»](./style/doc-create-workflow.md)。
@@ -1,6 +1,7 @@
# doc export(在线文档导出为 docx)
> 本文件自包含 export 契约。已有当前请求返回的 adoc nodeId 时直接导出;目标类型未知时只执行一次 info 检查,不要递归读取 `doc.md`。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> **路由前置判断**:用户说「下载/导出」时**必须**先用 [`./doc-info.md`](./doc-info.md) `info --node <ID> --format json` 查 `contentType`:
> - `contentType` 为 `ALIDOC`(在线文档)→ **必须用 `export`**,禁止用 `download`
@@ -44,7 +45,6 @@ Flags:
## 关键说明
- 同一请求中刚执行 create/copy/import 并紧接着说“这篇/刚才那篇/上次那篇”时,`--node` 必须使用该写操作真实返回的新 `nodeId`;不得预先搜索同名文档,也不得用搜索结果中的旧节点替换它。
- `export` 是一体化命令,一条命令自动完成提交→轮询→下载,**无需手动编排轮询**。CLI 内部使用渐进式退避轮询(最多约 5 分钟)。
- `export` 超时或中断后,CLI 会输出 `jobId`,可用 `dws doc export get --job-id <jobId>` 手动查询任务状态。
- `export` 当前仅支持钉钉在线文档(alidocs,`contentType=ALIDOC`)导出为 `docx`,**在线表格导出请使用其他命令**。
@@ -1,6 +1,7 @@
# doc 文件操作(upload / download / copy / move / rename / delete + folder create)
> **按需使用**:本文件自包含弃用命令的兼容说明,不要求先读总路由;优先按下方提示改用 `drive` / `wiki`。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令:
> - `doc download` → **`dws drive download`**(下载已有文件;在线文档导出 docx 仍走 `doc export`)
@@ -31,7 +32,6 @@ Flags:
- `upload` 是三步自动完成的流程(获取凭证 → OSS 上传 → 提交入库),无需手动分步操作。
- 支持上传任意类型文件(PDF、Office、图片等)到钉钉文档空间或知识库。
- `--convert` 可将 Office 文件转换为钉钉在线文档。
- **在线编辑硬路由**:用户说“大家直接在线改/上传后在线编辑/转成钉钉文档”时使用 [`./doc-import.md`](./doc-import.md) `doc import`,并回查在线类型;普通 `doc/drive upload` 只用于保留文件,不能据此承诺可在线编辑。
- **`doc upload` vs `drive upload`**:用户提到「知识库 / 文档空间 / workspace」→ `doc upload`;提到「钉盘 / 网盘 / 我的文件」→ `drive upload`;未明确目标时默认 `drive upload`。
- 与 [`./doc-media.md`](./doc-media.md) `media insert` 的区别:`upload` 上传到文档空间作为**独立文件**;`media insert` 作为**附件块插入到文档正文中**。
@@ -6,8 +6,6 @@
不要先读取文件内容再调用 `doc create` 或 `doc update`。`doc import` 会按文件格式走导入任务,保留更完整的原始结构。
> **在线编辑硬路由**:用户说“上传后在线编辑/大家直接在线改/转成钉钉文档”时必须使用 `doc import`。`drive upload` 只保留原始 `.docx/.xlsx/...` 普通文件,不能据此宣称已可在线编辑。若用户明确要同时保留原文件和在线版,才先 `drive upload`,再单独 `doc import`,并分别验证两个返回节点。
## 命令
```bash
@@ -40,7 +38,6 @@ dws doc import get --task-id <TASK_ID> --format json
3. 执行 `dws doc import --file ... --format json`。
4. 正常情况下 CLI 会自动提交、上传并轮询导入任务。
5. 如果命令超时或中断,从输出中提取 `taskId`,再执行 `dws doc import get --task-id <TASK_ID> --format json`。
6. 用返回的 `documentUrl`/`nodeId` 执行 `drive info` 或 `doc info`,确认在线类型和目标文件夹;验证通过后才能说“可直接在线编辑”。
## 上下文传递
@@ -1,6 +1,8 @@
# doc info(获取文档元信息 + URL 解析)
> 本文件自包含已知 nodeId/URL 的 info 契约。只有原始 alidocs URL 类型仍不明确时,才读取 [`url-patterns.md`](../../url-patterns.md);不要递归读取 `doc.md`。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`../../url-patterns.md`](../../url-patterns.md) — 仅当用户原始 `alidocs` URL 需要 probe 时
>
> **同任务常配合**:`dws drive search` / `dws wiki node search`(先定位 nodeId)/ [`doc-read.md`](./doc-read.md)(确认是 ALIDOC 后读正文)
@@ -1,6 +1,7 @@
# doc media(附件 / 图片:download / insert)
> 本文件自包含 media insert/download 契约。nodeId、文件路径或 resourceId 已知时直接执行;只在需要相对块定位且 blockId 未知时读取 [`doc-block.md`](./doc-block.md)。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> ⚠️ **图片插入硬规则**:
> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws drive download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert`
@@ -1,6 +1,7 @@
# doc permission(文档权限:add / update / list)
> **按需使用**:本文件自包含文档节点权限命令;不要求先读总路由。知识库整体成员权限改读 `dingtalk-wiki`。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> **关键区分**:
> - "把**某篇文档**授权给某人" → `doc permission add`(节点级,包括「我的文档」下的文档都支持)
@@ -1,6 +1,10 @@
# doc read(读取文档内容)
> 本文件自包含普通 read 契约。用户已给当前 adoc nodeId/URL 时直接读取;类型未知时才先执行 info。选择 JSONML 只为获取结构,不要求预读 cookbook;实际构造 JSONML 写入时再按需加载。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-info.md`](./doc-info.md)(先解析 URL,确认 contentType=ALIDOC、extension=adoc)/ [`doc-update.md`](./doc-update.md)(读后改写)/ [`doc-block.md`](./doc-block.md)(块级精修前先读结构)
## 命令格式
@@ -1,6 +1,12 @@
# doc update(更新文档内容)
> 本文件自包含普通 append/overwrite 契约,不要递归预读路由或 style reference。纯文本 append 可直接执行;overwrite 先 read/dry-run/确认。只有保真改写或复杂 JSONML 才读取 [`doc-update-workflow.md`](./style/doc-update-workflow.md) 与 cookbook;单块修改改用 [`doc-block.md`](./doc-block.md)。
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、分片 append、回读验收)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范
> 4. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-read.md`](./doc-read.md)(改写前必读,jsonml 模式拿当前结构;担心被并发覆盖时再取 revision)/ [`doc-block.md`](./doc-block.md)(单 block 改写优先;本命令更适合追加 / 整篇 overwrite)
## 命令格式
@@ -4,9 +4,11 @@
> 改写已有文档见 [doc-update-workflow.md](./doc-update-workflow.md)。排版规范见 [doc-style-guideline.md](./doc-style-guideline.md)。
## 按需使用
## 前置必读
普通 Markdown 创建不需要先读本文件或 style guideline。只有用户要求设计文档骨架或复杂版式时,才查看下方对应章节;实际选择 JSONML 后再读取 cookbook。需要按文档类型选骨架时,按需读取 [doc-style-guideline.md](./doc-style-guideline.md) 的对应一节,不要通读。
> **同时读取 [doc-style-guideline.md](./doc-style-guideline.md):**
> - **§2.0 类型判断决策表** → 锁定文档类型(决策型 / 执行型 / 说明型 / 知识沉淀型)和骨架
> - **§1 硬规则** → 全程生效(`--name` 已是 H1、不编造 URL、Markdown 草稿不写 callout 等)
### 关键词速查(用户意图 → 起稿路径)
@@ -43,7 +45,7 @@
| 项目 | 要求 |
|------|------|
| 标题 | 用 `--name` 传入;用户显式要求正文 H1 时原样保留,未要求时正文默认从 H2 开始 |
| 标题 | 用 `--name` 传入;正文不要再重复同名一级标题 |
| 位置 | 默认创建到我的文档;指定目录时只接受文档文件夹 `nodeId` 或 alidocs 文件夹 URL |
| 正文 | 多行、表格、代码块、特殊字符或长度 >= 2KB 时必须写入 UTF-8 临时 `.md` 文件 |
| 格式 | 按 §JSONML 起稿判定 决定起稿路径:命中 JSONML 起稿条件时**直接用 JSONML 构造**(跳过 markdown);未命中时用 Markdown 起稿,创建后按 [doc-update-workflow.md](./doc-update-workflow.md) 精修 |
@@ -224,7 +226,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
> **脚手架策略警示**:Markdown 无法表达分栏/callout/色彩表头,拉回的 JSONML 只有纯文本骨架。精修阶段不是“在现有结构上加色”,而是“参照 RFC/Spec 重组结构”。
> **JSONML 条件加载**:仅在确定使用 JSONML 后读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md);其中“决策型文档骨架范例”可直接改写。
> **MUST READ**:动手写 JSONML 前,必须先用 Read 工具读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md) — 其中 §决策型文档骨架范例 有可直接复制修改的完整模板。
> 节点类型和属性的权威定义见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。
### ⚠️ JSONML 降级约束
@@ -272,7 +274,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
```
- 根节点固定 `"root"`(不是 `"body"`)
- 用户未要求正文 H1 时,JSONML 从 `h2` 开始;用户明确要求“正文一级标题/插入一级标题”时必须构造 `h1`
- `--name` 已是 H1,JSONML 从 `h2` 开始
- 表格结构是 `table → tr → tc`(无 `th`/`td`)
- 分栏是 `table` + `"sr": true`,`tc` 建议设 `fill` 背景色
- 有序列表:仅第一项设 `"start": 1`,后续项不设 `start`(系统自动递增)
@@ -313,7 +315,7 @@ dws doc read --node <nodeId> --content-format jsonml --output /tmp/<name>-readba
- 只使用用户已提供或对话中已确认的正文素材。
- 如果正文素材不足,先补齐文档目标、受众、章节和缺口;不要在本文中临时扩展跨产品采集流程。
- **先按 [doc-style-guideline.md §2.0 类型判断决策表](./doc-style-guideline.md) 确定文档类型,再用对应类型的骨架样板(§2.1 决策型 / §2.2 执行型 / §2.3 说明型 / §2.4 知识沉淀型)**。不要套通用三段式。
- **`--name` 是外壳标题,不覆盖显式正文 H1**:用户未要求正文一级标题时从 `##` 开始;用户明确给出 `# ...` 或要求“先起一级标题”时,正文必须保留该 H1。
- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。
- 摘要、bullet、引用块、callout 等元素的使用边界以 style-guideline §3-§7 为准。
- 同类信息保持一致:风险、状态、行动项各用一种元素 + 一种视觉语义(style-guideline §1.2 / §5)。
- 临时文件必须保留真实换行,不能把换行写成字面量 `\n`。
@@ -25,7 +25,7 @@
## 一、硬规则
1. **用户显式正文 H1 优先**:`--name` 是文档外壳标题;用户未要求正文一级标题时从 `##` 开始。用户明确说“正文写 `# ...` / 先起一级标题 / 插入一级标题”时必须保留或插入真实 H1,不得用 `--name` 代替
1. **`--name` 是 H1**:正文从 `##` 开始;正文内不写 `#`(除非确需正文内再造一级 H1 并说明动机)
2. **同类信息同表达**:风险、状态、行动项、证据,每类只用一种元素 + 一种视觉语义(见 §5)
3. **Markdown 草稿阶段只用稳定元素**:标题、段落、列表、checklist、表格、代码块;callout / 分栏 / 附件 / 复杂嵌套留到创建后用 `doc block insert` / `doc media insert` 精修
4. **引用块只用于原文**:用户原话、会议摘录、外部材料原文;不许包装作者自己的结论
@@ -209,7 +209,7 @@
### 4.1 标题与段落
- 用户未要求正文 H1 时从 `##` 开始;用户明确要求正文 H1 时按原文使用 `#` 或 heading level 1
- 正文从 `##` 开始(H1 已被 `--name` 占用)
- 标题层级 ≤ 4 层(§7)
- 单段过长先拆段,再考虑换元素
@@ -217,7 +217,6 @@
- 普通列表:并列要点
- 有序列表:顺序步骤
- 用户明确要求“有序列表块”时必须使用真实列表结构;JSONML 为带 `list.isOrdered=true` 的多个 `p` 节点,不能只写带数字前缀的普通段落或以整篇 Markdown 代替显式 block insert
- checklist:待办状态(含 `- [ ]` / `- [x]`)
列表项里开始出现"负责人 / 截止时间 / 状态"这类字段时,改用表格。
+9
View File
@@ -202,14 +202,23 @@ Usage:
Example:
dws drive download --node <dentryUuid> --output ./report.pdf
dws drive download --node <dentryUuid> --output ~/downloads/
dws drive download --node <dentryUuid> --output ./big.zip --part-size 32MB --parallel 8
Flags:
--node string 文件 ID (dentryUuid) (必填)
--output string 本地保存路径 (必填),可以是文件路径或目录;如果指定目录,文件名从下载 URL 中自动推断
--space-id string 文件所属空间 ID (可选)
--part-size string 分片下载的分片大小,支持 KB/MB/GB 单位,范围 1MB-1GB (默认 16MB)
--parallel int 分片下载并发数,范围 1-8 (默认 4)
--no-resume 关闭断点续传,忽略历史下载进度从头下载 (默认开启续传)
```
> **注意**:`--output` 是必填参数,不传会报错。
> **大文件分片下载**:
> - 大文件自动分片并发下载,小文件整流下载,行为对用户透明,无需任何额外操作。
> - 断点续传默认开启:下载中断后重跑同一命令会自动跳过已完成部分继续下载(`<目标文件>.dwspart` 为临时进度文件,下载完成后自动清理);不需要续传时加 `--no-resume`。
> - 下载凭证过期会自动刷新并继续下载,已完成的部分不会重下;单个分片失败会自动重试,无需手动处理。
### 创建文件夹
```
+10 -15
View File
@@ -3,19 +3,19 @@
用机器人向多个群批量发送相同消息(如日报提醒)
用法:
python3 scripts/bot_broadcast.py \
python bot_broadcast.py \
--robot-code <ROBOT_CODE> \
--chats "conv_id1,conv_id2,conv_id3" \
--title "日报提醒" \
--text "请大家今天下班前提交日报"
python3 scripts/bot_broadcast.py \
python bot_broadcast.py \
--robot-code <ROBOT_CODE> \
--chats-file groups.txt \
--title "周会通知" \
--text "明天下午3点周会"
python3 scripts/bot_broadcast.py --dry-run ...
python bot_broadcast.py --dry-run ...
"""
import sys
@@ -40,19 +40,14 @@ def run_dws(
if result.returncode != 0:
print(f" ✗ 错误:{result.stderr.strip()}")
return None
data = json.loads(result.stdout)
if isinstance(data, dict) and data.get('success') is False:
detail = data.get('errorMsg') or data.get('message') or '未知错误'
print(f" ✗ 业务调用失败:{detail}")
return None
return data
return json.loads(result.stdout)
except (subprocess.TimeoutExpired, json.JSONDecodeError,
FileNotFoundError) as e:
print(f" ✗ 错误:{e}")
return None
def run(argv: Optional[List[str]] = None) -> int:
def main():
parser = argparse.ArgumentParser(
description='向多个群批量发送机器人消息'
)
@@ -69,7 +64,7 @@ def run(argv: Optional[List[str]] = None) -> int:
'--text', required=True, help='消息内容 Markdown'
)
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args(argv)
args = parser.parse_args()
chat_ids: List[str] = []
if args.chats:
@@ -79,13 +74,13 @@ def run(argv: Optional[List[str]] = None) -> int:
p = Path(args.chats_file)
if not p.exists():
print(f"错误:文件不存在: {p}")
return 1
sys.exit(1)
chat_ids = [line.strip() for line in
p.read_text(encoding='utf-8').splitlines()
if line.strip() and not line.startswith('#')]
if not chat_ids:
print('错误:需要 --chats 或 --chats-file')
return 1
sys.exit(1)
print(f"📢 批量发送消息到 {len(chat_ids)} 个群")
print(f" 标题: {args.title}")
@@ -110,8 +105,8 @@ def run(argv: Optional[List[str]] = None) -> int:
fail += 1
print(f"\n完成: 成功 {success}, 失败 {fail}")
return 0 if fail == 0 else 1
sys.exit(0 if fail == 0 else 1)
if __name__ == '__main__':
sys.exit(run())
main()

Some files were not shown because too many files have changed in this diff Show More