Compare commits
35
Commits
b2
...
codex/aitable-run4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a30ff648f4 | ||
|
|
e924d11db6 | ||
|
|
7c1edadd4d | ||
|
|
2699d2cfbe | ||
|
|
798def7337 | ||
|
|
5af3f0a8ba | ||
|
|
36803ac9bb | ||
|
|
31cec9efa7 | ||
|
|
88f5a5a0e8 | ||
|
|
9dd8e232bb | ||
|
|
7094e5eceb | ||
|
|
e909f1083b | ||
|
|
6182169b3e | ||
|
|
ab2fba868b | ||
|
|
796cee3d95 | ||
|
|
32c82e1246 | ||
|
|
fe2f64dc43 | ||
|
|
32c9109c71 | ||
|
|
7a42c83d3a | ||
|
|
8b1564eff4 | ||
|
|
b8b5583440 | ||
|
|
8f8ba3f290 | ||
|
|
fe856a8f38 | ||
|
|
583b453abf | ||
|
|
eef94425e2 | ||
|
|
e17ffe5bff | ||
|
|
e83e3a3e2c | ||
|
|
5323129e5e | ||
|
|
014dea52f0 | ||
|
|
20c2ff98c2 | ||
|
|
c7ea642aef | ||
|
|
b5af90c089 | ||
|
|
a5ee9dffe2 | ||
|
|
7179928c75 | ||
|
|
8e48ede81a |
@@ -0,0 +1,226 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -112,6 +113,16 @@ 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.
|
||||
@@ -127,6 +138,7 @@ func Execute() (exitCode int) {
|
||||
executed = root
|
||||
}
|
||||
err = rewordRequiredFlagError(err)
|
||||
err = enrichChatWorkbookError(executed, err)
|
||||
if isUnknownCommandError(err) {
|
||||
executed.SetOut(os.Stderr)
|
||||
_ = executed.Help()
|
||||
@@ -141,6 +153,474 @@ 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 {
|
||||
@@ -220,6 +700,9 @@ 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{
|
||||
|
||||
@@ -43,12 +43,153 @@ 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)
|
||||
|
||||
@@ -69,6 +69,77 @@ 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")
|
||||
|
||||
@@ -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("MCP tool returned a business error; check tool parameters and refer to skill documentation."),
|
||||
apperrors.WithHint(apperrors.SuggestBusinessHint(callResult.Content)),
|
||||
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("The API returned a business-level error. Check required parameters and values."),
|
||||
apperrors.WithHint(apperrors.SuggestBusinessHint(callResult.Content)),
|
||||
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)), 129; got != want {
|
||||
if got, want := int(product["count"].(float64)), 159; got != want {
|
||||
t.Fatalf("schema chat count = %d, want %d", got, want)
|
||||
}
|
||||
summaries := schemaContractObjectSlice(productPayload["tools"])
|
||||
|
||||
@@ -131,12 +131,9 @@ var generatedParamAliases = []ParamAliasEntry{
|
||||
{
|
||||
CLIPath: "chat +chat-bots",
|
||||
Aliases: map[string]string{
|
||||
"chat": "group",
|
||||
"chat-id": "group",
|
||||
"conversation-id": "group",
|
||||
"open-conversation-id": "group",
|
||||
"chat": "group",
|
||||
},
|
||||
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
|
||||
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
|
||||
},
|
||||
{
|
||||
CLIPath: "chat +chat-dismiss",
|
||||
@@ -151,12 +148,9 @@ var generatedParamAliases = []ParamAliasEntry{
|
||||
{
|
||||
CLIPath: "chat +chat-invite-url",
|
||||
Aliases: map[string]string{
|
||||
"chat": "group",
|
||||
"chat-id": "group",
|
||||
"conversation-id": "group",
|
||||
"open-conversation-id": "group",
|
||||
"chat": "group",
|
||||
},
|
||||
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
|
||||
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
|
||||
},
|
||||
{
|
||||
CLIPath: "chat +chat-mute",
|
||||
@@ -313,11 +307,7 @@ var generatedParamAliases = []ParamAliasEntry{
|
||||
},
|
||||
{
|
||||
CLIPath: "chat +messages-mget",
|
||||
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"},
|
||||
Blocked: []string{"msg-id", "open-message-id", "ref-msg-id", "src-msg-id"},
|
||||
},
|
||||
{
|
||||
CLIPath: "chat +messages-read-status",
|
||||
@@ -1109,7 +1099,7 @@ var generatedParamAliases = []ParamAliasEntry{
|
||||
"open-conversation-ids": "conversation-ids",
|
||||
"user-ids": "users",
|
||||
},
|
||||
Blocked: []string{"at-user-ids", "chat-id", "conversation-id", "group-id", "group-ids", "open-conversation-id", "staff-id", "uid", "userid"},
|
||||
Blocked: []string{"at-user-ids", "chat-id", "group-id", "group-ids", "open-conversation-id", "staff-id", "uid", "userid"},
|
||||
},
|
||||
{
|
||||
CLIPath: "chat message send",
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -308,7 +308,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -317,7 +317,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
|
||||
],
|
||||
"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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -568,7 +568,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -577,7 +577,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
|
||||
],
|
||||
"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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -1858,7 +1858,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -1867,7 +1867,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
|
||||
],
|
||||
"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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -3667,7 +3667,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -3676,7 +3676,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
|
||||
],
|
||||
"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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -4702,7 +4702,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -4711,7 +4711,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -4877,7 +4877,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
|
||||
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -4886,7 +4886,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
|
||||
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
|
||||
],
|
||||
"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 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
|
||||
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
|
||||
]
|
||||
},
|
||||
"aitable +resolve-table": {
|
||||
@@ -4917,7 +4917,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -4960,7 +4960,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -4969,7 +4969,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -5135,7 +5135,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
|
||||
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -5144,7 +5144,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
|
||||
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 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": [
|
||||
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
|
||||
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
|
||||
]
|
||||
},
|
||||
"aitable +role-list": {
|
||||
@@ -5949,7 +5949,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -5993,7 +5993,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -6002,7 +6002,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11191,11 +11191,11 @@
|
||||
]
|
||||
},
|
||||
"aitable base search": {
|
||||
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
|
||||
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
|
||||
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -11205,14 +11205,14 @@
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
|
||||
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
|
||||
"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(优先于仅最近访问的 list)。",
|
||||
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
@@ -11244,7 +11244,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
|
||||
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11253,7 +11253,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
|
||||
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11407,7 +11407,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
|
||||
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11416,7 +11416,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
|
||||
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11441,10 +11441,12 @@
|
||||
"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/mono/references/products/aitable.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable.md"
|
||||
],
|
||||
"use_when": [
|
||||
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
|
||||
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
|
||||
]
|
||||
},
|
||||
"aitable base update": {
|
||||
@@ -16699,11 +16701,11 @@
|
||||
]
|
||||
},
|
||||
"aitable field get": {
|
||||
"agent_summary": "获取字段完整配置。",
|
||||
"agent_summary": "底层获取字段完整类型与 config。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
|
||||
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -16713,14 +16715,14 @@
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "获取字段完整配置。",
|
||||
"value": "底层获取字段完整类型与 config。",
|
||||
"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": "底层获取字段完整类型与 config。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
@@ -16752,7 +16754,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
|
||||
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -16761,7 +16763,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
|
||||
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -16913,7 +16915,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"需要字段类型/config(选项、公式等)详情时"
|
||||
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -16922,7 +16924,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要字段类型/config(选项、公式等)详情时"
|
||||
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -16950,10 +16952,12 @@
|
||||
"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/mono/references/products/aitable/aitable-field.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-field.md"
|
||||
],
|
||||
"use_when": [
|
||||
"需要字段类型/config(选项、公式等)详情时"
|
||||
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
|
||||
]
|
||||
},
|
||||
"aitable field list": {
|
||||
@@ -22339,11 +22343,11 @@
|
||||
]
|
||||
},
|
||||
"aitable record create": {
|
||||
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
|
||||
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
|
||||
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
@@ -22353,14 +22357,14 @@
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "新增记录(cells 的 key 必须是 fieldId)。",
|
||||
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
|
||||
"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": "新增记录(cells 的 key 必须是 fieldId)。",
|
||||
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
@@ -22392,7 +22396,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
|
||||
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -22401,7 +22405,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
|
||||
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -22553,7 +22557,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"需要插入新行数据时"
|
||||
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -22562,7 +22566,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要插入新行数据时"
|
||||
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -22592,10 +22596,12 @@
|
||||
"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/mono/references/products/aitable/aitable-record-create.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md"
|
||||
],
|
||||
"use_when": [
|
||||
"需要插入新行数据时"
|
||||
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
|
||||
]
|
||||
},
|
||||
"aitable record delete": {
|
||||
@@ -23929,11 +23935,11 @@
|
||||
]
|
||||
},
|
||||
"aitable record query": {
|
||||
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
|
||||
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -23943,14 +23949,14 @@
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
|
||||
"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": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
|
||||
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
@@ -23982,7 +23988,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -23991,7 +23997,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -24151,7 +24157,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"查看、筛选、全文搜索或遍历记录时的主入口"
|
||||
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -24160,7 +24166,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"查看、筛选、全文搜索或遍历记录时的主入口"
|
||||
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -24189,10 +24195,12 @@
|
||||
"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/mono/references/products/aitable/aitable-record-query.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md"
|
||||
],
|
||||
"use_when": [
|
||||
"查看、筛选、全文搜索或遍历记录时的主入口"
|
||||
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
|
||||
]
|
||||
},
|
||||
"aitable record query-empty": {
|
||||
@@ -24711,11 +24719,11 @@
|
||||
]
|
||||
},
|
||||
"aitable record update": {
|
||||
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
|
||||
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
@@ -24725,14 +24733,14 @@
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
|
||||
"value": "更新已有记录字段;先 query 拿 recordId,使用 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": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
|
||||
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
@@ -24764,7 +24772,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -24773,7 +24781,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -24925,7 +24933,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"需要修改已有记录若干字段时"
|
||||
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -24934,7 +24942,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要修改已有记录若干字段时"
|
||||
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -24963,10 +24971,12 @@
|
||||
"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/mono/references/products/aitable/aitable-attachment.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md"
|
||||
],
|
||||
"use_when": [
|
||||
"需要修改已有记录若干字段时"
|
||||
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
|
||||
]
|
||||
},
|
||||
"aitable record upsert": {
|
||||
@@ -27862,11 +27872,11 @@
|
||||
]
|
||||
},
|
||||
"aitable table get": {
|
||||
"agent_summary": "获取数据表结构(字段+视图目录)。",
|
||||
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
|
||||
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -27876,14 +27886,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,
|
||||
@@ -27915,7 +27925,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
|
||||
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -27924,7 +27934,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
|
||||
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -28076,7 +28086,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
|
||||
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -28085,7 +28095,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
|
||||
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -28115,10 +28125,12 @@
|
||||
"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/mono/references/products/aitable/aitable-record-create.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable.md"
|
||||
],
|
||||
"use_when": [
|
||||
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
|
||||
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
|
||||
]
|
||||
},
|
||||
"aitable table list": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10478,8 +10478,10 @@
|
||||
"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 或目标根目录"
|
||||
],
|
||||
@@ -10496,14 +10498,14 @@
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件",
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -10525,27 +10527,31 @@
|
||||
},
|
||||
"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": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"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": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -10601,7 +10607,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
@@ -10611,7 +10617,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -10704,7 +10710,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -10733,24 +10739,26 @@
|
||||
"use_when": {
|
||||
"value": [
|
||||
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
|
||||
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
|
||||
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
|
||||
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
|
||||
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
|
||||
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
|
||||
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
|
||||
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
|
||||
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10774,7 +10782,8 @@
|
||||
],
|
||||
"use_when": [
|
||||
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
|
||||
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
|
||||
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
|
||||
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
|
||||
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613",
|
||||
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
|
||||
"source_hash": "sha256:f239237a9b87fa5a95a0520b2e2f2a112e117b273de8418763ba0ecd8652b317",
|
||||
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
|
||||
"coverage": {
|
||||
"surface_products": 26,
|
||||
"products_with_metadata": 26,
|
||||
"surface_tools": 845,
|
||||
"tools_with_metadata": 845,
|
||||
"tools_with_agent_summary": 845,
|
||||
"tools_with_use_when": 845,
|
||||
"tools_with_avoid_when": 845,
|
||||
"tools_with_examples": 845,
|
||||
"tools_with_interface_mode": 845,
|
||||
"unmatched_skill_tools": 122,
|
||||
"unreviewed_skill_tools": 11
|
||||
"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
|
||||
},
|
||||
"products": {
|
||||
"aisearch": {
|
||||
@@ -88,20 +88,20 @@
|
||||
]
|
||||
},
|
||||
"aitable": {
|
||||
"agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
|
||||
"agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"avoid_when": [
|
||||
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
|
||||
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
|
||||
"value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
|
||||
"value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true
|
||||
@@ -110,7 +110,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
|
||||
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -118,7 +118,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
|
||||
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -128,7 +128,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
|
||||
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -136,7 +136,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
|
||||
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -153,10 +153,11 @@
|
||||
"internal/cli/schema_hints/selection/aitable.json",
|
||||
"skills/mono/SKILL.md",
|
||||
"skills/mono/references/intent-guide.md",
|
||||
"skills/mono/references/products/aitable.md"
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable.md"
|
||||
],
|
||||
"use_when": [
|
||||
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
|
||||
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
|
||||
]
|
||||
},
|
||||
"attendance": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8704,13 +8704,15 @@
|
||||
"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/mono/references/products/aitable.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable.md"
|
||||
],
|
||||
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
|
||||
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
|
||||
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
|
||||
],
|
||||
"canonical_path": "aitable.base_search",
|
||||
"cli_name": "search",
|
||||
@@ -8731,14 +8733,14 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。"
|
||||
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。"
|
||||
}
|
||||
],
|
||||
"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(优先于仅最近访问的 list)。"
|
||||
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
@@ -8770,7 +8772,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
|
||||
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -8779,7 +8781,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
|
||||
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -9009,7 +9011,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
|
||||
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -9018,7 +9020,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
|
||||
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -9237,7 +9239,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "搜索 AI 表格",
|
||||
"use_when": [
|
||||
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
|
||||
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
|
||||
]
|
||||
},
|
||||
"aitable.base_update": {
|
||||
@@ -23545,13 +23547,15 @@
|
||||
"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/mono/references/products/aitable/aitable-field.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-field.md"
|
||||
],
|
||||
"agent_summary": "获取字段完整配置。",
|
||||
"agent_summary": "底层获取字段完整类型与 config。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
|
||||
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
|
||||
],
|
||||
"canonical_path": "aitable.field_get",
|
||||
"cli_name": "get",
|
||||
@@ -23572,14 +23576,14 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": "获取字段完整配置。"
|
||||
"value": "底层获取字段完整类型与 config。"
|
||||
}
|
||||
],
|
||||
"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": "底层获取字段完整类型与 config。"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
@@ -23611,7 +23615,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
|
||||
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -23620,7 +23624,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
|
||||
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -23848,7 +23852,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要字段类型/config(选项、公式等)详情时"
|
||||
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -23857,7 +23861,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要字段类型/config(选项、公式等)详情时"
|
||||
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -24201,7 +24205,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "获取字段详情",
|
||||
"use_when": [
|
||||
"需要字段类型/config(选项、公式等)详情时"
|
||||
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
|
||||
]
|
||||
},
|
||||
"aitable.field_list": {
|
||||
@@ -38119,16 +38123,18 @@
|
||||
"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/mono/references/products/aitable/aitable-record-query.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md"
|
||||
],
|
||||
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
|
||||
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"aliases": [
|
||||
"aitable record list"
|
||||
],
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
],
|
||||
"canonical_path": "aitable.query_records",
|
||||
"cli_name": "query",
|
||||
@@ -38149,14 +38155,14 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。"
|
||||
"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": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。"
|
||||
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
@@ -38188,7 +38194,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -38197,7 +38203,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -38447,7 +38453,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"查看、筛选、全文搜索或遍历记录时的主入口"
|
||||
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -38456,7 +38462,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、底层原始响应或不同执行语义时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -39699,7 +39705,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "获取行记录",
|
||||
"use_when": [
|
||||
"查看、筛选、全文搜索或遍历记录时的主入口"
|
||||
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
|
||||
]
|
||||
},
|
||||
"aitable.record_batch_update": {
|
||||
@@ -40397,13 +40403,15 @@
|
||||
"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/mono/references/products/aitable/aitable-record-create.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md"
|
||||
],
|
||||
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
|
||||
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
|
||||
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
|
||||
],
|
||||
"canonical_path": "aitable.record_create",
|
||||
"cli_name": "create",
|
||||
@@ -40424,14 +40432,14 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": "新增记录(cells 的 key 必须是 fieldId)。"
|
||||
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。"
|
||||
}
|
||||
],
|
||||
"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": "新增记录(cells 的 key 必须是 fieldId)。"
|
||||
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
@@ -40463,7 +40471,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
|
||||
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -40472,7 +40480,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
|
||||
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -40700,7 +40708,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要插入新行数据时"
|
||||
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -40709,7 +40717,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要插入新行数据时"
|
||||
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -41156,7 +41164,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "新增记录",
|
||||
"use_when": [
|
||||
"需要插入新行数据时"
|
||||
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
|
||||
]
|
||||
},
|
||||
"aitable.record_delete": {
|
||||
@@ -46549,13 +46557,15 @@
|
||||
"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/mono/references/products/aitable/aitable-attachment.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md"
|
||||
],
|
||||
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
|
||||
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
],
|
||||
"canonical_path": "aitable.record_update",
|
||||
"cli_name": "update",
|
||||
@@ -46576,14 +46586,14 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。"
|
||||
"value": "更新已有记录字段;先 query 拿 recordId,使用 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": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。"
|
||||
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
@@ -46615,7 +46625,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -46624,7 +46634,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -46852,7 +46862,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要修改已有记录若干字段时"
|
||||
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -46861,7 +46871,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要修改已有记录若干字段时"
|
||||
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -47308,7 +47318,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "更新记录",
|
||||
"use_when": [
|
||||
"需要修改已有记录若干字段时"
|
||||
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
|
||||
]
|
||||
},
|
||||
"aitable.record_upsert": {
|
||||
@@ -53447,7 +53457,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
|
||||
],
|
||||
"canonical_path": "aitable.shortcut_base_list",
|
||||
"cli_name": "+base-list",
|
||||
@@ -53502,7 +53512,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -53511,7 +53521,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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -53943,7 +53953,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
|
||||
],
|
||||
"canonical_path": "aitable.shortcut_base_search",
|
||||
"cli_name": "+base-search",
|
||||
@@ -53997,7 +54007,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -54006,7 +54016,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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -56196,7 +56206,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
|
||||
],
|
||||
"canonical_path": "aitable.shortcut_field_get",
|
||||
"cli_name": "+field-get",
|
||||
@@ -56250,7 +56260,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -56259,7 +56269,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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -60316,7 +60326,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
|
||||
],
|
||||
"canonical_path": "aitable.shortcut_record_query",
|
||||
"cli_name": "+record-query",
|
||||
@@ -60370,7 +60380,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -60379,7 +60389,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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -63485,7 +63495,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
|
||||
],
|
||||
"canonical_path": "aitable.shortcut_resolve_base",
|
||||
"cli_name": "+resolve-base",
|
||||
@@ -63539,7 +63549,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -63548,7 +63558,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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -63758,7 +63768,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
|
||||
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -63767,7 +63777,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 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
|
||||
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -63884,7 +63894,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)",
|
||||
"use_when": [
|
||||
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
|
||||
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
|
||||
]
|
||||
},
|
||||
"aitable.shortcut_resolve_table": {
|
||||
@@ -63900,7 +63910,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
|
||||
],
|
||||
"canonical_path": "aitable.shortcut_resolve_table",
|
||||
"cli_name": "+resolve-table",
|
||||
@@ -63954,7 +63964,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -63963,7 +63973,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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -64173,7 +64183,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
|
||||
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -64182,7 +64192,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)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
|
||||
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -64395,7 +64405,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)",
|
||||
"use_when": [
|
||||
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
|
||||
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
|
||||
]
|
||||
},
|
||||
"aitable.shortcut_role_list": {
|
||||
@@ -65656,7 +65666,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
|
||||
],
|
||||
"canonical_path": "aitable.shortcut_table_get",
|
||||
"cli_name": "+table-get",
|
||||
@@ -65711,7 +65721,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -65720,7 +65730,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": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -70522,13 +70532,15 @@
|
||||
"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/mono/references/products/aitable/aitable-record-create.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable.md"
|
||||
],
|
||||
"agent_summary": "获取数据表结构(字段+视图目录)。",
|
||||
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
|
||||
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
|
||||
],
|
||||
"canonical_path": "aitable.table_get",
|
||||
"cli_name": "get",
|
||||
@@ -70549,14 +70561,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": [
|
||||
@@ -70588,7 +70600,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
|
||||
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -70597,7 +70609,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
|
||||
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -70825,7 +70837,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
|
||||
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -70834,7 +70846,7 @@
|
||||
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
|
||||
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -71081,7 +71093,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "获取数据表",
|
||||
"use_when": [
|
||||
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
|
||||
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
|
||||
]
|
||||
},
|
||||
"aitable.table_list": {
|
||||
|
||||
+17805
-138
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -27242,8 +27242,10 @@
|
||||
"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 或目标根目录"
|
||||
],
|
||||
@@ -27267,7 +27269,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件"
|
||||
@@ -27275,7 +27277,7 @@
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件"
|
||||
},
|
||||
@@ -27299,12 +27301,14 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"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 或目标根目录"
|
||||
]
|
||||
@@ -27312,11 +27316,13 @@
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"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 或目标根目录"
|
||||
]
|
||||
@@ -27429,7 +27435,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"value": [
|
||||
@@ -27440,7 +27446,7 @@
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"value": [
|
||||
"dws drive upload --file ./report.pdf --format json",
|
||||
@@ -27528,7 +27534,7 @@
|
||||
},
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"value": true
|
||||
@@ -27580,23 +27586,25 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"value": [
|
||||
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
|
||||
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
|
||||
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
|
||||
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
|
||||
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/drive.json",
|
||||
"value": [
|
||||
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
|
||||
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
|
||||
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
|
||||
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
|
||||
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
|
||||
]
|
||||
}
|
||||
@@ -28319,7 +28327,8 @@
|
||||
"title": "上传本地文件到钉盘或文档空间",
|
||||
"use_when": [
|
||||
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
|
||||
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
|
||||
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
|
||||
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
|
||||
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -79,36 +79,6 @@
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,378 @@
|
||||
{
|
||||
"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"
|
||||
@@ -57,6 +429,10 @@
|
||||
"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"
|
||||
@@ -149,10 +525,18 @@
|
||||
"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"
|
||||
@@ -161,114 +545,62 @@
|
||||
"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.search_bots",
|
||||
"cli_path": "chat bot find"
|
||||
"canonical_path": "chat.transfer_group_owner",
|
||||
"cli_path": "chat group transfer-owner"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.search_my_robots",
|
||||
"cli_path": "chat bot search"
|
||||
"canonical_path": "chat.translate",
|
||||
"cli_path": "chat text translate"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.get_conv_categories_info",
|
||||
"cli_path": "chat category batch-info"
|
||||
"canonical_path": "chat.unread_message_conversation_list",
|
||||
"cli_path": "chat message list-unread-conversations"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.create_smart_conv_category",
|
||||
"cli_path": "chat category create-smart"
|
||||
"canonical_path": "chat.unset_pin_message",
|
||||
"cli_path": "chat message unset-pin-msg"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_user_define_conv_categories",
|
||||
"cli_path": "chat category list"
|
||||
"canonical_path": "chat.unset_top_message",
|
||||
"cli_path": "chat message unset-top-msg"
|
||||
},
|
||||
{
|
||||
"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_at_all_notification_off",
|
||||
"cli_path": "chat mute-at-all"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_conv_member_roles",
|
||||
"cli_path": "chat group set-admin"
|
||||
},
|
||||
{
|
||||
"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_custom_group_role",
|
||||
"cli_path": "chat group-role update"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
@@ -278,200 +610,16 @@
|
||||
"cli_path": "chat group update-settings"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.upgrade_group_to_external",
|
||||
"cli_path": "chat group upgrade-to-external"
|
||||
"canonical_path": "chat.update_notification_off",
|
||||
"cli_path": "chat mute"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.batch_query_group_chat_settings",
|
||||
"cli_path": "chat group user-settings query"
|
||||
"canonical_path": "chat.update_red_env_notification_off",
|
||||
"cli_path": "chat mute-red-envelope"
|
||||
},
|
||||
{
|
||||
"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_show_history_msg_option",
|
||||
"cli_path": "chat group set-history"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_streaming_card",
|
||||
@@ -482,40 +630,12 @@
|
||||
"cli_path": "chat message update-text-emotion"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_notification_off",
|
||||
"cli_path": "chat mute"
|
||||
"canonical_path": "chat.update_user_group_alias",
|
||||
"cli_path": "chat group update-alias"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"canonical_path": "chat.upgrade_group_to_external",
|
||||
"cli_path": "chat group upgrade-to-external"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -801,6 +801,396 @@
|
||||
"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": 845,
|
||||
"source_tools": 875,
|
||||
"matched_tools": 71
|
||||
},
|
||||
"tools": {
|
||||
|
||||
@@ -289,12 +289,12 @@
|
||||
]
|
||||
},
|
||||
"aitable.base_search": {
|
||||
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
|
||||
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
|
||||
"use_when": [
|
||||
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
|
||||
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
|
||||
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable base search --query \"项目\""
|
||||
@@ -304,7 +304,8 @@
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"cobra-help:dws aitable base search --help",
|
||||
"skills/mono/references/products/aitable.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable.md",
|
||||
"dws-schema-live:aitable.search_bases"
|
||||
]
|
||||
},
|
||||
@@ -710,12 +711,12 @@
|
||||
]
|
||||
},
|
||||
"aitable.field_get": {
|
||||
"agent_summary": "获取字段完整配置。",
|
||||
"agent_summary": "底层获取字段完整类型与 config。",
|
||||
"use_when": [
|
||||
"需要字段类型/config(选项、公式等)详情时"
|
||||
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
|
||||
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID>"
|
||||
@@ -725,7 +726,8 @@
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"cobra-help:dws aitable field get --help",
|
||||
"skills/mono/references/products/aitable.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-field.md",
|
||||
"dws-schema-live:aitable.get_fields"
|
||||
]
|
||||
},
|
||||
@@ -1110,12 +1112,12 @@
|
||||
]
|
||||
},
|
||||
"aitable.query_records": {
|
||||
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
|
||||
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
|
||||
"use_when": [
|
||||
"查看、筛选、全文搜索或遍历记录时的主入口"
|
||||
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID>"
|
||||
@@ -1125,7 +1127,8 @@
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"cobra-help:dws aitable record query --help",
|
||||
"skills/mono/references/products/aitable.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md",
|
||||
"dws-schema-live:aitable.query_records"
|
||||
]
|
||||
},
|
||||
@@ -1150,12 +1153,12 @@
|
||||
]
|
||||
},
|
||||
"aitable.record_create": {
|
||||
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
|
||||
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
|
||||
"use_when": [
|
||||
"需要插入新行数据时"
|
||||
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
|
||||
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> --records '[{\"cells\":{\"fldXXX\":\"值\"}}]'"
|
||||
@@ -1165,7 +1168,8 @@
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"cobra-help:dws aitable record create --help",
|
||||
"skills/mono/references/products/aitable.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md",
|
||||
"dws-schema-live:aitable.create_records"
|
||||
]
|
||||
},
|
||||
@@ -1310,12 +1314,12 @@
|
||||
]
|
||||
},
|
||||
"aitable.record_update": {
|
||||
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
|
||||
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
|
||||
"use_when": [
|
||||
"需要修改已有记录若干字段时"
|
||||
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> --records '[{\"recordId\":\"recXXX\",\"cells\":{\"fldYYY\":\"新值\"}}]'"
|
||||
@@ -1325,7 +1329,8 @@
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"cobra-help:dws aitable record update --help",
|
||||
"skills/mono/references/products/aitable.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md",
|
||||
"dws-schema-live:aitable.update_records"
|
||||
]
|
||||
},
|
||||
@@ -1550,12 +1555,12 @@
|
||||
]
|
||||
},
|
||||
"aitable.table_get": {
|
||||
"agent_summary": "获取数据表结构(字段+视图目录)。",
|
||||
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
|
||||
"use_when": [
|
||||
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
|
||||
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
|
||||
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable table get --base-id BASE_ID"
|
||||
@@ -1565,7 +1570,8 @@
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"cobra-help:dws aitable table get --help",
|
||||
"skills/mono/references/products/aitable.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable.md",
|
||||
"dws-schema-live:aitable.get_tables"
|
||||
]
|
||||
},
|
||||
@@ -2335,7 +2341,7 @@
|
||||
"当你不知道具体 baseId、想先浏览自己最近用过或可访问的 AI 表格清单以便定位目标时使用;支持游标分页,返回 Base 列表及其 baseId。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable +base-list",
|
||||
@@ -2355,7 +2361,7 @@
|
||||
"当你知道某个 AI 表格的名字或部分关键词、想直接定位到它并拿到 baseId 时使用;输入名称关键词,返回匹配的 Base 列表。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable +base-search --query \"项目管理\""
|
||||
@@ -2393,7 +2399,7 @@
|
||||
"当你已进入某个 Base、需要了解其中某些数据表有哪些字段(拿 fieldId)、有哪些视图(拿 viewId)以便读写数据时使用;批量返回表信息、字段目录和视图目录。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable +table-get --base-id BASE_ID",
|
||||
@@ -2413,7 +2419,7 @@
|
||||
"当你需要查看字段的完整类型配置(如单选选项、关联表设置、AI 配置)以便正确写入数据或改配置时使用;批量返回字段详情。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable +field-get --base-id B --table-id T"
|
||||
@@ -2432,7 +2438,7 @@
|
||||
"当你要读取表格里的行数据——按 recordId 精确取、按结构化条件筛选、按关键词全文搜索或分页遍历时使用;返回匹配记录及其单元格值。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable +record-query --base-id B --table-id T --query \"关键词\" --limit 50"
|
||||
@@ -2849,10 +2855,10 @@
|
||||
"aitable.shortcut_resolve_base": {
|
||||
"agent_summary": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)",
|
||||
"use_when": [
|
||||
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
|
||||
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable +resolve-base --name 项目管理"
|
||||
@@ -2868,10 +2874,10 @@
|
||||
"aitable.shortcut_resolve_table": {
|
||||
"agent_summary": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)",
|
||||
"use_when": [
|
||||
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
|
||||
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable +resolve-table --base B --name 任务"
|
||||
@@ -2887,18 +2893,19 @@
|
||||
},
|
||||
"products": {
|
||||
"aitable": {
|
||||
"agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
|
||||
"agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
|
||||
"use_when": [
|
||||
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
|
||||
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
|
||||
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工结合实时 MCP/Skill/Cobra 审阅产品级选择边界。",
|
||||
"review_reason": "人工结合多产品边界、根 Skill 的渐进加载顺序、真实 Cobra/Shortcut 路径与 MCP 语义审阅产品级选择边界。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"skills/mono/references/products/aitable.md",
|
||||
"skills/multi/dingtalk-aitable/SKILL.md",
|
||||
"skills/multi/dingtalk-aitable/references/aitable.md",
|
||||
"dws-schema-live:product-index"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
]
|
||||
},
|
||||
"chat.add_emoji_reaction": {
|
||||
"agent_summary": "给指定消息添加表情回应",
|
||||
"agent_summary": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应",
|
||||
"use_when": [
|
||||
"需要对已有消息添加一个 emoji reaction 时"
|
||||
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
|
||||
],
|
||||
"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"
|
||||
"只发送普通文本时使用 send 或 send-by-bot;转发现有审批、日历、待办等原生产品卡片时应先取得真实 openMessageId,再使用 message forward"
|
||||
],
|
||||
"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": [
|
||||
"已知源消息与源、目标会话 ID 时"
|
||||
"已知同一源会话中的真实 openMessageId 与源、目标会话 ID,需要保留原消息或原生产品卡片时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"合并转发多条消息时使用 chat message combine-forward"
|
||||
"合并转发多条消息时使用 chat message combine-forward;OA 实例 ID、日历事件 ID、待办任务 ID 等产品对象 ID 不能代替消息 ID"
|
||||
],
|
||||
"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 或用户标识并需要解析会话详情时"
|
||||
"已知群 ID 或单聊用户标识并需要解析会话详情时;--group、--user、--open-dingtalk-id 只能选择一个"
|
||||
],
|
||||
"avoid_when": [
|
||||
"按群名查找会话时使用 chat search"
|
||||
@@ -383,7 +383,7 @@
|
||||
"用户明确指定某个会话,并要读取消息或追溯引用回复中的原消息上下文时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"跨全部会话按时间查询时使用 chat message list-all"
|
||||
"跨全部会话按时间查询时使用 chat message list-all;关键词搜索或审计应使用 message search,不要拉最近消息后在本地筛选"
|
||||
],
|
||||
"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": "查询异步消息发送任务的状态",
|
||||
"agent_summary": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功",
|
||||
"use_when": [
|
||||
"发送命令返回 openTaskId 后需要确认投递结果时"
|
||||
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
|
||||
],
|
||||
"avoid_when": [
|
||||
"没有 openTaskId 或只需查消息内容时不要使用"
|
||||
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
|
||||
],
|
||||
"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 时"
|
||||
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
|
||||
],
|
||||
"avoid_when": [
|
||||
"移除文字表情时使用 chat message remove-text-emotion"
|
||||
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
|
||||
],
|
||||
"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"
|
||||
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
|
||||
],
|
||||
"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": "把指定消息设为会话置顶消息",
|
||||
"agent_summary": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin",
|
||||
"use_when": [
|
||||
"需要在会话中置顶一条已知消息时"
|
||||
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
|
||||
],
|
||||
"avoid_when": [
|
||||
"取消置顶使用 chat message unset-pin-msg"
|
||||
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
|
||||
],
|
||||
"examples": [
|
||||
"dws chat message set-pin-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"
|
||||
@@ -1316,12 +1316,12 @@
|
||||
]
|
||||
},
|
||||
"chat.unset_pin_message": {
|
||||
"agent_summary": "取消指定消息的会话置顶",
|
||||
"agent_summary": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin",
|
||||
"use_when": [
|
||||
"需要移除一条已知置顶消息时"
|
||||
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
|
||||
],
|
||||
"avoid_when": [
|
||||
"新增置顶使用 chat message set-pin-msg"
|
||||
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
|
||||
],
|
||||
"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": [
|
||||
"没有真实可用 mediaId 时先完成媒体上传"
|
||||
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 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 命令"
|
||||
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
|
||||
],
|
||||
"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": [
|
||||
"已有 bizId 并需要追加内容或结束流式输出时"
|
||||
"已有 message send-card 真实返回的 bizId,并需要追加内容或结束流式输出时;最后一次更新使用完成状态"
|
||||
],
|
||||
"avoid_when": [
|
||||
"创建新卡片时使用 chat message send-card"
|
||||
"创建新卡片时使用 chat message send-card;openMessageId、OA 实例 ID、日历事件 ID或待办任务 ID 不能代替 bizId"
|
||||
],
|
||||
"examples": [
|
||||
"dws chat message update-card --biz-id <bizId> --content \"处理完成\" --flow-status 2"
|
||||
@@ -1596,6 +1596,604 @@
|
||||
"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": [
|
||||
@@ -2247,12 +2845,12 @@
|
||||
]
|
||||
},
|
||||
"chat.shortcut_messages_query_send_status": {
|
||||
"agent_summary": "查询消息发送状态",
|
||||
"agent_summary": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功",
|
||||
"use_when": [
|
||||
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
|
||||
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
|
||||
],
|
||||
"examples": [
|
||||
"dws chat +messages-query-send-status --open-task-id <openTaskId>"
|
||||
@@ -2544,10 +3142,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"
|
||||
@@ -2563,10 +3161,10 @@
|
||||
"chat.batch_update_group_chat_settings": {
|
||||
"agent_summary": "批量更新当前用户自己的群会话设置(置顶/免打扰/群昵称/群备注)",
|
||||
"use_when": [
|
||||
"用户说 把这些群都设为免打扰/置顶"
|
||||
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
|
||||
],
|
||||
"avoid_when": [
|
||||
"单个群昵称优先 chat group update-nick"
|
||||
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
|
||||
],
|
||||
"examples": [
|
||||
"dws chat group user-settings set --items '[{\"openConversationId\":\"cid1\",\"top\":true,\"mute\":false}]' --format json"
|
||||
|
||||
@@ -82,22 +82,22 @@
|
||||
]
|
||||
},
|
||||
"doc.create_document": {
|
||||
"agent_summary": "创建一篇新的在线文档",
|
||||
"agent_summary": "在默认根目录、文档文件夹或知识库根创建带可选初始内容的 adoc",
|
||||
"use_when": [
|
||||
"用户要新建一篇文字在线文档(adoc),可空文档或带初始 Markdown 时",
|
||||
"创建到指定文件夹 --folder、知识库根 --workspace,或默认「我的文档」根目录时"
|
||||
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
|
||||
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
|
||||
],
|
||||
"avoid_when": [
|
||||
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type <type>(勿用 doc create)",
|
||||
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
|
||||
"导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert"
|
||||
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
|
||||
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc create --name \"项目周报\" --format json",
|
||||
"dws doc create --name \"Q1 总结\" --content \"# Q1 总结\" --folder <FOLDER_ID> --format json"
|
||||
"dws doc create --name \"Q1 总结\" --content-file ./q1.md --workspace <WORKSPACE_ID> --format json"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
|
||||
"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": [
|
||||
"用户要读取钉钉在线文字文档(adoc)正文(Markdown)时",
|
||||
"用户直接粘贴文档 URL 且无其他指令时(默认读内容)",
|
||||
"已由 drive info 确认 extension=adoc,用户要读取正文(Markdown)时",
|
||||
"用户提供已知 adoc nodeId/URL,且要读取内容或抽取指定章节时",
|
||||
"只需标题大纲、指定块区间/单块或特定 JSONML tags 时使用 --content-format jsonml 与 --scope"
|
||||
],
|
||||
"avoid_when": [
|
||||
"非 adoc(表格/多维表/普通文件)不要用本命令;先 doc info 再路由",
|
||||
"原始 alidocs URL 类型未知或目标不是 adoc 时先用 drive 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": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐 doc Skill 与 doc-info reference 的 extension 预检边界,并保留 Markdown/JSONML scope 的真实 Cobra 契约。",
|
||||
"source_refs": [
|
||||
"CommandRegistry:canonical_path=doc.get_document_content",
|
||||
"cobra-help:dws doc read",
|
||||
@@ -321,22 +321,23 @@
|
||||
]
|
||||
},
|
||||
"doc.get_document_info": {
|
||||
"agent_summary": "获取文档元信息(标题/类型/创建者/权限等)",
|
||||
"agent_summary": "在已确认是 ALIDOC 后读取文档专属元信息",
|
||||
"use_when": [
|
||||
"用户要查看文档/节点元信息(标题、类型、创建者、权限)时",
|
||||
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时"
|
||||
"drive info 已确认是 ALIDOC,用户还要标题、创建者、权限或 docUrl 等文档专属元信息时",
|
||||
"创建响应只有 nodeId、缺少 docUrl,需要补查文档链接时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"已确认是 adoc 且只要正文改用 dws doc read",
|
||||
"原始 alidocs URL 的类型探测、extension 路由或可靠 fileSize 使用 dws drive info;不要先猜是文档",
|
||||
"已确认是 adoc 且只要正文时改用 dws doc read",
|
||||
"只要目录列表改用 dws drive list / wiki node list",
|
||||
"需要可靠文件大小 fileSize 时改用 dws drive info;文档元信息接口可能不返回大小"
|
||||
"普通文件、电子表格或 AI 表格不使用本命令"
|
||||
],
|
||||
"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": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐 drive info 统一类型探测入口与 doc info 的文档专属补查职责;不改变 node 参数或接口绑定。",
|
||||
"source_refs": [
|
||||
"CommandRegistry:canonical_path=doc.get_document_info",
|
||||
"cobra-help:dws doc info",
|
||||
@@ -371,10 +372,11 @@
|
||||
"doc.insert_document_block": {
|
||||
"agent_summary": "向文档插入块元素",
|
||||
"use_when": [
|
||||
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
],
|
||||
"avoid_when": [
|
||||
"整篇追加 Markdown 优先 doc update --mode append",
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
@@ -383,7 +385,7 @@
|
||||
"dws doc block insert --node <DOC_ID> --heading \"二级标题\" --level 2 --format json"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
|
||||
"source_refs": [
|
||||
"CommandRegistry:canonical_path=doc.insert_document_block",
|
||||
"cobra-help:dws doc block insert",
|
||||
@@ -420,7 +422,7 @@
|
||||
"doc.list_document_blocks": {
|
||||
"agent_summary": "查询文档一级块元素列表",
|
||||
"use_when": [
|
||||
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
|
||||
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
|
||||
],
|
||||
"avoid_when": [
|
||||
"只要全文 Markdown 用 doc read",
|
||||
@@ -431,7 +433,7 @@
|
||||
"dws doc block list --node <DOC_ID> --start-index 0 --end-index 5 --format json"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
|
||||
"source_refs": [
|
||||
"CommandRegistry:canonical_path=doc.list_document_blocks",
|
||||
"cobra-help:dws doc block list",
|
||||
@@ -719,17 +721,18 @@
|
||||
"doc.update_comment": {
|
||||
"agent_summary": "更新指定文档评论的文字内容和可选 @用户/@群。",
|
||||
"use_when": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
],
|
||||
"avoid_when": [
|
||||
"删除评论用 delete;回复用 reply"
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
],
|
||||
"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": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.update_comment",
|
||||
"cobra-help:dws doc comment update --help",
|
||||
@@ -739,23 +742,24 @@
|
||||
]
|
||||
},
|
||||
"doc.update_document": {
|
||||
"agent_summary": "更新文档内容(追加 / 覆盖;覆盖需 --yes)",
|
||||
"agent_summary": "用原生自动分片管道追加或整篇覆盖 adoc 内容",
|
||||
"use_when": [
|
||||
"用户要向已有 adoc 追加内容时用 --mode append(更安全)",
|
||||
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
|
||||
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
|
||||
],
|
||||
"avoid_when": [
|
||||
"目标不是 adoc 或只要改单个块时改用 doc block update",
|
||||
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
|
||||
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
|
||||
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
|
||||
"创建新文档用 doc create,不要用 update 冒充创建"
|
||||
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
|
||||
],
|
||||
"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": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐 Runtime 的 10000 字符自动分片、append/overwrite 动态门禁与根 Skill 的写后回读流程;不改变参数和安全事实。",
|
||||
"source_refs": [
|
||||
"CommandRegistry:canonical_path=doc.update_document",
|
||||
"cobra-help:dws doc update",
|
||||
@@ -905,14 +909,14 @@
|
||||
"当你只记得文档的标题或主题词、需要先定位到某篇钉钉文档拿到它的 nodeId/URL 以便后续阅读或编辑时使用;可按关键词、扩展名、创建/访问时间、创建者等条件过滤,不传关键词则返回最近访问的文档,返回匹配的文档列表。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只有一个关键词且只要紧凑标题/URL/type/token 投影时优先 +find-doc;目标已给 nodeId/URL 时不要再搜索"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc +search --query \"会议纪要\"",
|
||||
"dws doc +search --extensions pdf,docx"
|
||||
],
|
||||
"reviewed": true,
|
||||
"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.",
|
||||
"review_reason": "人工区分 +search 的丰富过滤/最近访问能力与 +find-doc 的单关键词紧凑投影,避免两个同源搜索 Shortcut 互相争抢。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.shortcut_search",
|
||||
"cobra-help:dws doc +search",
|
||||
@@ -925,14 +929,14 @@
|
||||
"当你已知某个文档文件夹或知识库的 ID、想浏览它下面直接包含的文档与子文件夹(不递归深层)以便逐层导航时使用;输入 folder 或 workspace,返回该层级的子节点列表。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"全局或钉盘目录浏览优先 drive list,知识库节点树优先 wiki node list;本 Shortcut 只列已知 doc folder/workspace 的直接子节点"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc +list --folder DOC_FOLDER_NODE_ID",
|
||||
"dws doc +list --workspace WS_ID --limit 20"
|
||||
],
|
||||
"reviewed": true,
|
||||
"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.",
|
||||
"review_reason": "人工对齐 doc/drive/wiki 产品边界:保留已知文档文件夹/知识库的直接子节点投影,不替代常规目录管理。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.shortcut_list",
|
||||
"cobra-help:dws doc +list",
|
||||
@@ -945,13 +949,13 @@
|
||||
"当你想保留原件、在另一个文件夹或知识库里生成一份文档/文件副本(例如以某篇文档为模板另存)时使用;输入源 node 与目标 folder/workspace,会实际创建一个副本。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"常规文件管理优先 drive copy;要搬走原件用 move;复制后必须从真实返回取副本 nodeId,禁止继续编辑源文档"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc +copy --node DOC_ID --folder TARGET_FOLDER_NODE_ID"
|
||||
],
|
||||
"reviewed": true,
|
||||
"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.",
|
||||
"review_reason": "人工对齐保形复制、真实副本 ID 与 drive 文件管理边界;保留 Runtime user_required 门禁。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.shortcut_copy",
|
||||
"cobra-help:dws doc +copy",
|
||||
@@ -964,13 +968,13 @@
|
||||
"当你要整理文档归属、把某篇文档/文件从当前位置挪到另一个文件夹或知识库(原位置不再保留)时使用;输入 node 与目标 folder/workspace,会实际改变文件的存放位置。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"常规文件管理优先 drive move;要保留原位置副本用 copy;目标位置不明确或未确认时不要移动"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc +move --node DOC_ID --folder TARGET_FOLDER_NODE_ID"
|
||||
],
|
||||
"reviewed": true,
|
||||
"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.",
|
||||
"review_reason": "人工对齐 move 的原位置消失语义、目标位置确认与 drive 文件管理边界;保留 Runtime user_required 门禁。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.shortcut_move",
|
||||
"cobra-help:dws doc +move",
|
||||
@@ -1041,13 +1045,13 @@
|
||||
"当你想把在线文档导出成 docx/markdown/pdf 文件(例如离线保存或外发)时使用;这是异步任务的第一步,输入 node 与 export-format 提交导出,返回 jobId,随后用 +export-get 轮询结果。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"用户要直接拿到本地 docx/markdown/pdf 文件时使用一体化 dws doc export;不要默认手工编排 submit/get 轮询"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc +export-submit --node DOC_ID --export-format markdown"
|
||||
],
|
||||
"reviewed": true,
|
||||
"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.",
|
||||
"review_reason": "人工对齐 atomic doc export 的一体化提交/轮询/下载路径,将 +export-submit 限定为明确异步控制场景。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.shortcut_export_submit",
|
||||
"cobra-help:dws doc +export-submit",
|
||||
@@ -1117,13 +1121,13 @@
|
||||
"当文档被误改、你想把它整体恢复到某个历史版本时使用;先用 +version-list 找到目标版本号,再输入 node 与 version,会实际把文档内容覆盖回该版本,属于高风险写操作,需谨慎确认。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"只查看历史用 +version-list,只保存当前快照用 +version-save;版本号未核实、用户未确认或只需改单块时不要回滚"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc +version-revert --node DOC_ID --version 3"
|
||||
],
|
||||
"reviewed": true,
|
||||
"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.",
|
||||
"review_reason": "人工对齐 version list → 明确确认 → revert → 回读的 ID 与高风险边界;不改变 typed_yes 安全门禁。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.shortcut_version_revert",
|
||||
"cobra-help:dws doc +version-revert",
|
||||
@@ -1174,14 +1178,14 @@
|
||||
"当你只想往一篇钉钉文档的最后面补一段文字、又不想动原有内容时使用;内部用文档更新的“追加(append)”模式,把你给的文本安全地拼到文档末尾,不需要你先去查文档块列表、算末尾位置或手工拼块结构。会真实写入文档内容。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"长、多行、表格或文件内容使用 doc update --mode append --content-file;指定位置或富结构使用 block insert;不要跳过 user_required 确认"
|
||||
],
|
||||
"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": "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.",
|
||||
"review_reason": "人工将 Shortcut 限定为短纯文本末尾追加,并对齐 user_required 与写后回读;长内容交给原生自动分片管道。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.shortcut_doc_append",
|
||||
"cobra-help:dws doc +doc-append",
|
||||
@@ -1194,14 +1198,14 @@
|
||||
"当你只记得云文档标题或内容里的某个关键词,想快速按关键词找到匹配的文档、拿到它的标题、URL、类型和 token 以便后续查看或编辑,却不想拿到一大坨原始字段时使用;内部调用云文档的 search_documents 工具,把 --query 作为搜索关键词(keyword),可选地用 --limit 限制返回条数(pageSize),再在本地把每条命中结果精简为「标题、URL、类型、token」四个字段后打印。这是纯只读操作,只做搜索与本地投影,不会创建、修改或删除任何文档;未命中时提示「没搜到文档」。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"需要最近访问、extension/时间/创建者等组合过滤时使用 +search;目标已给 nodeId/URL 时直接进入 info/read,不再搜索"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc +find-doc --query 季度汇报",
|
||||
"dws doc +find-doc --query 合同 --limit 10"
|
||||
],
|
||||
"reviewed": true,
|
||||
"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.",
|
||||
"review_reason": "人工将 +find-doc 定位为高频单关键词紧凑投影,并与 +search 的丰富过滤能力做互斥路由。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.shortcut_find_doc",
|
||||
"cobra-help:dws doc +find-doc",
|
||||
@@ -1214,13 +1218,13 @@
|
||||
"当你手上已经有一个文档链接、想直接私信发给某个人而不必先查 userId 时使用;内部先按姓名搜通讯录解析出唯一用户,再用 openDingTalkId 把链接拼成一条 Markdown 消息发出去,姓名匹配到多人时会列出候选让你区分。只发链接、不读取或改动文档本身,会真实发出消息。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"同名人员未消歧、缺少真实文档 URL 或用户未确认时不要发送;本命令不授予文档权限,授权应走 drive permission"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc +share-doc --to 张三 --url https://docs.dingtalk.com/xxx --note \"帮忙过一下\""
|
||||
],
|
||||
"reviewed": true,
|
||||
"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.",
|
||||
"review_reason": "人工对齐人员消歧、真实 docUrl、user_required 消息发送门禁与文档权限边界。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.shortcut_share_doc",
|
||||
"cobra-help:dws doc +share-doc",
|
||||
|
||||
@@ -910,12 +910,15 @@
|
||||
"agent_summary": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件",
|
||||
"use_when": [
|
||||
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
|
||||
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
|
||||
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
|
||||
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
|
||||
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --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 或目标根目录"
|
||||
],
|
||||
@@ -924,7 +927,7 @@
|
||||
"dws drive upload --file ./README.md --node <dentryUuid> --format json"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
|
||||
"source_refs": [
|
||||
"CommandRegistry:canonical_path=drive.upload",
|
||||
"cobra-help:dws drive upload",
|
||||
|
||||
@@ -207,6 +207,270 @@ 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)
|
||||
|
||||
+53
-14
@@ -42,23 +42,24 @@ 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
|
||||
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
|
||||
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:"-"`
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
@@ -169,6 +170,22 @@ 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) {
|
||||
@@ -309,7 +326,12 @@ 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
|
||||
}
|
||||
@@ -388,6 +410,23 @@ 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))),
|
||||
|
||||
@@ -192,6 +192,46 @@ 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()
|
||||
|
||||
|
||||
@@ -308,6 +308,13 @@ 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 {
|
||||
@@ -316,6 +323,19 @@ 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, "搜索内容不能为空"):
|
||||
@@ -326,6 +346,14 @@ 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."
|
||||
}
|
||||
|
||||
+259
-22
@@ -56,11 +56,20 @@ const maxConversationCategoryTitleRunes = 15
|
||||
func validatedConversationCategoryTitle(raw string) (string, error) {
|
||||
title := strings.TrimSpace(raw)
|
||||
if title == "" {
|
||||
return "", apperrors.NewValidation("--title 不能为空")
|
||||
return "", apperrors.NewValidation(
|
||||
"--title 不能为空",
|
||||
apperrors.WithReason("invalid_category_title"),
|
||||
apperrors.WithHint("请提供 1 到 15 个字符的分组标题,并保持用户指定原文。"),
|
||||
apperrors.WithActions("补充非空 --title", "运行当前命令 --help 查看示例"),
|
||||
)
|
||||
}
|
||||
if utf8.RuneCountInString(title) > maxConversationCategoryTitleRunes {
|
||||
return "", apperrors.NewValidation(fmt.Sprintf(
|
||||
"--title 最多 %d 个字符", maxConversationCategoryTitleRunes))
|
||||
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 个字符以内", "使用用户确认后的标题原文重试"),
|
||||
)
|
||||
}
|
||||
return title, nil
|
||||
}
|
||||
@@ -331,6 +340,25 @@ 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 {
|
||||
@@ -434,9 +462,12 @@ func guardGroupOwnerRemoval(ctx context.Context, openConversationID string, remo
|
||||
if err != nil || ownerOpenID == "" {
|
||||
return nil
|
||||
}
|
||||
ownerErr := fmt.Errorf(
|
||||
"refusing to remove the group owner: 被移除列表包含群主,移出群主将导致群无群主(孤儿群)\n hint: 先执行 dws chat group transfer-owner --group %s --user <newOwnerUserId> 转让群主后再移除",
|
||||
openConversationID,
|
||||
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)),
|
||||
)
|
||||
userIDs, openDingTalkIDs := splitChatIDValues(removeValues)
|
||||
for _, id := range openDingTalkIDs {
|
||||
@@ -1681,7 +1712,27 @@ func newChatCommand() *cobra.Command {
|
||||
if atAll && !strings.Contains(text, "<@all>") {
|
||||
text = "<@all> " + text
|
||||
}
|
||||
// 用户身份发消息要求 @ 占位符为 <@openDingTalkId>;模型若写成裸 @id 自动补全,已有 <@id> 不变
|
||||
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> 保持不变
|
||||
text = normalizeAtPlaceholders(text, atOpenIds, true)
|
||||
// 群聊统一走 openDingTalkId @ 人接口。
|
||||
contentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
|
||||
@@ -1991,7 +2042,12 @@ 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 fmt.Errorf("--sender-user-id or --sender-open-dingtalk-id is required")
|
||||
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`},
|
||||
)
|
||||
}
|
||||
startMs, err := parseISOTimeToMillis("start", mustGetFlag(cmd, "start"))
|
||||
if err != nil {
|
||||
@@ -2182,6 +2238,25 @@ 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".
|
||||
@@ -2219,6 +2294,8 @@ 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 != "" {
|
||||
@@ -2509,7 +2586,6 @@ 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
|
||||
@@ -2744,6 +2820,8 @@ 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", "", "")
|
||||
@@ -2976,6 +3054,7 @@ 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 {
|
||||
@@ -2983,6 +3062,14 @@ 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,
|
||||
})
|
||||
@@ -3058,6 +3145,16 @@ 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,
|
||||
@@ -3129,6 +3226,16 @@ 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))
|
||||
}
|
||||
@@ -3152,6 +3259,14 @@ 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"),
|
||||
@@ -3434,6 +3549,14 @@ 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":
|
||||
@@ -3546,7 +3669,12 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
|
||||
newOwner = newOwnerUserID
|
||||
}
|
||||
if newOwner == "" {
|
||||
return fmt.Errorf("flag --new-owner or --user is required")
|
||||
return chatGuidanceError(
|
||||
"缺少新群主标识",
|
||||
"--new-owner 接收新群主 openDingTalkId,--user 接收新群主 userId,二者必须选择一个",
|
||||
[]string{"先查询新群主的人员标识", "使用 --new-owner 或 --user 之一"},
|
||||
[]string{`dws chat group transfer-owner --group <openConversationId> --new-owner <openDingTalkId> --format json`},
|
||||
)
|
||||
}
|
||||
if !isOpenDingTalkID(newOwner) {
|
||||
return callMCPToolOnServer("im", "transfer_group_owner", map[string]any{
|
||||
@@ -3696,9 +3824,36 @@ 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": mustGetFlag(cmd, "setting-key"),
|
||||
"settingKey": settingKey,
|
||||
"status": status,
|
||||
})
|
||||
},
|
||||
@@ -3787,6 +3942,14 @@ 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"),
|
||||
@@ -3931,7 +4094,21 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
|
||||
if !off {
|
||||
muteTime, _ := cmd.Flags().GetInt64("mute-time")
|
||||
if muteTime <= 0 {
|
||||
return fmt.Errorf("--mute-time is required when muting (supported: 300000/3600000/86400000/604800000/2592000000)")
|
||||
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`},
|
||||
)
|
||||
}
|
||||
toolArgs["muteTime"] = muteTime
|
||||
}
|
||||
@@ -4098,6 +4275,14 @@ 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"),
|
||||
@@ -4392,9 +4577,28 @@ 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": parseCSVValues(mustGetFlag(cmd, "msg-ids")),
|
||||
"srcOpenMessageIds": msgIDs,
|
||||
"destOpenCid": mustGetFlag(cmd, "dest-conversation-id"),
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("uuid"); v != "" {
|
||||
@@ -4430,6 +4634,14 @@ 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"),
|
||||
@@ -4708,12 +4920,21 @@ 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": mustGetFlag(cmd, "status"),
|
||||
"status": status,
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("description"); v != "" {
|
||||
toolArgs["auditDescription"] = v
|
||||
@@ -4839,7 +5060,7 @@ status 可选值:
|
||||
chatClearMessagesCmd := &cobra.Command{
|
||||
Use: "clear-messages",
|
||||
Short: "清空当前用户指定会话的聊天记录",
|
||||
Long: `清空当前用户在指定会话中的聊天记录。仅清空当前用户视角的消息,不影响其他成员。
|
||||
Long: `清空当前用户在指定会话中的聊天记录。仅清空当前用户视角的消息,不影响其他成员。该操作不可逆;必须先获得用户确认,再追加 --yes 执行。
|
||||
|
||||
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
|
||||
- 群聊:dws chat search --query "群名"
|
||||
@@ -4851,6 +5072,14 @@ 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,
|
||||
})
|
||||
@@ -5193,6 +5422,14 @@ 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"),
|
||||
@@ -5254,7 +5491,12 @@ status 可选值:
|
||||
target, _ := cmd.Flags().GetString("target")
|
||||
receiver, _ := cmd.Flags().GetString("receiver")
|
||||
if target == "" && receiver == "" {
|
||||
return fmt.Errorf("--target or --receiver is required")
|
||||
return chatGuidanceError(
|
||||
"缺少群邀请链接的接收目标",
|
||||
"--target 表示接收分享的目标会话,--receiver 表示接收分享的单聊用户,二者必须选择一个",
|
||||
[]string{"分享到群或会话时使用 --target", "分享到个人时使用 --receiver openDingTalkId"},
|
||||
[]string{`dws chat group share-invite --source <源群ID> --target <目标会话ID> --format json`},
|
||||
)
|
||||
}
|
||||
if target != "" && receiver != "" {
|
||||
return fmt.Errorf("--target and --receiver are mutually exclusive")
|
||||
@@ -5500,10 +5742,5 @@ 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
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -114,6 +115,20 @@ 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)
|
||||
}
|
||||
@@ -392,7 +407,13 @@ func TestCrossPlatformCoverageGuardGroupOwnerRemovalCoverage(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
caller := &scriptedToolCaller{steps: tc.steps}
|
||||
installScriptedCaller(t, caller)
|
||||
_ = guardGroupOwnerRemoval(context.Background(), "group", tc.remove)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{}`}}}
|
||||
|
||||
@@ -63,8 +63,14 @@ 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>",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,40 @@ 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
|
||||
|
||||
@@ -1070,9 +1070,6 @@ func newDocCommand() *cobra.Command {
|
||||
})
|
||||
}
|
||||
if md != "" {
|
||||
if name, ok := toolArgs["name"].(string); ok && name != "" {
|
||||
md = stripDuplicateTitle(md, name)
|
||||
}
|
||||
toolArgs["markdown"] = md
|
||||
}
|
||||
if md != "" {
|
||||
@@ -3218,53 +3215,6 @@ 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,12 +295,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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"
|
||||
)
|
||||
@@ -669,15 +670,7 @@ 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 {
|
||||
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)
|
||||
return apperrors.SuggestBusinessHint(body)
|
||||
}
|
||||
|
||||
// confirmDelete is a convenience wrapper around cmdutil.ConfirmDelete that
|
||||
|
||||
@@ -62,13 +62,18 @@ 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"}},
|
||||
{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.ConstraintCustom,
|
||||
Flags: []string{"conversation-id", "conversation-ids"},
|
||||
Flags: []string{"conversation-id", "conversation-ids", "open-conversation-id", "chat-id", "chat-ids"},
|
||||
Description: "会话 ID 去重后必须为 1-10 个",
|
||||
},
|
||||
},
|
||||
@@ -85,6 +90,10 @@ 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{
|
||||
@@ -92,7 +101,7 @@ var ConversationSetTop = shortcut.Shortcut{
|
||||
arguments: map[string]any{
|
||||
"openConversationId": id,
|
||||
"cid": id,
|
||||
"top": !rt.Bool("off"),
|
||||
"top": top,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -102,8 +111,11 @@ var ConversationSetTop = shortcut.Shortcut{
|
||||
|
||||
func conversationSetTopIDs(rt *shortcut.RuntimeContext) []string {
|
||||
values := append([]string{}, rt.StrSlice("conversation-ids")...)
|
||||
if value := rt.Str("conversation-id"); value != "" {
|
||||
values = append(values, value)
|
||||
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)
|
||||
}
|
||||
}
|
||||
return uniqueShortcutStrings(values)
|
||||
}
|
||||
|
||||
@@ -69,15 +69,25 @@ var ChatMembersGet = shortcut.Shortcut{
|
||||
Intent: "当你已有若干成员的 openDingTalkId、需要批量获取他们在该群内的详情(群昵称、角色等)时使用;只读,需传群 openConversationId 和成员 openDingTalkId 列表。",
|
||||
Risk: shortcut.RiskRead,
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "id", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
|
||||
{Name: "users", Type: shortcut.FlagStringSlice, Desc: "成员 openDingTalkId 列表", Required: true},
|
||||
{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"}},
|
||||
},
|
||||
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": rt.Str("id"),
|
||||
"cid": rt.Str("id"),
|
||||
"memberOpenDingTalkIds": rt.StrSlice("users"),
|
||||
"openConversationId": conversationID,
|
||||
"cid": conversationID,
|
||||
"memberOpenDingTalkIds": rt.StrSliceFirst("users", "open-dingtalk-ids"),
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -122,14 +132,22 @@ var ChatInviteURL = shortcut.Shortcut{
|
||||
Intent: "当你想拿到一条群邀请链接分享给别人加群时使用;只读生成链接,需传群 openConversationId,可用 --expires-seconds 设置有效期(0 表示永久)。",
|
||||
Risk: shortcut.RiskRead,
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
|
||||
{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: "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": rt.Str("group"),
|
||||
"cid": rt.Str("group"),
|
||||
"openConversationId": conversationID,
|
||||
"cid": conversationID,
|
||||
}
|
||||
if rt.Changed("expires-seconds") {
|
||||
params["expiresSeconds"] = rt.Int("expires-seconds")
|
||||
@@ -552,11 +570,20 @@ var ChatBots = shortcut.Shortcut{
|
||||
Intent: "当你想查看某个群里已添加了哪些机器人时使用;需传群 openConversationId,只读返回群内机器人列表(含 openBotId,供后续移除)。",
|
||||
Risk: shortcut.RiskRead,
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
|
||||
{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"}},
|
||||
},
|
||||
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.Str("group")})
|
||||
data, err := rt.CallMCPData("bot", "list_group_bots", map[string]any{
|
||||
"openConversationId": rt.StrFirst("group", "id", "chat-id", "conversation-id", "open-conversation-id"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -147,14 +147,23 @@ var MessagesRecall = shortcut.Shortcut{
|
||||
Intent: "当你想撤回当前用户刚发出的某条消息时使用;会实际撤回消息,需传会话 openConversationId 和消息 openMessageId。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true},
|
||||
{Name: "msg-id", Type: shortcut.FlagString, Desc: "消息 openMessageId", Required: true},
|
||||
{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"}},
|
||||
},
|
||||
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.Str("conversation-id"),
|
||||
"openMessageId": rt.Str("msg-id"),
|
||||
"openConversationId": rt.StrFirst("conversation-id", "group", "chat-id", "open-conversation-id"),
|
||||
"openMessageId": rt.StrFirst("msg-id", "message-id", "open-message-id"),
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -529,26 +538,30 @@ 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", Required: true},
|
||||
{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: "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"},
|
||||
Flags: []string{"msg-ids", "message-id", "message-ids", "open-message-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 := uniqueShortcutStrings(rt.StrSlice("msg-ids"))
|
||||
ids := messageMgetIDs(rt)
|
||||
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 := uniqueShortcutStrings(rt.StrSlice("msg-ids"))
|
||||
ids := messageMgetIDs(rt)
|
||||
data, err := rt.CallMCPData("im", "list_messages_by_ids", map[string]any{"openMsgIds": ids})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -581,6 +594,10 @@ 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 {
|
||||
@@ -833,11 +850,15 @@ var MessagesQuerySendStatus = shortcut.Shortcut{
|
||||
Intent: "当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。",
|
||||
Risk: shortcut.RiskRead,
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "open-task-id", Type: shortcut.FlagString, Desc: "发送消息时返回的 openTaskId", Required: true},
|
||||
{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"}},
|
||||
},
|
||||
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.Str("open-task-id")})
|
||||
return rt.CallMCP("query_message_send_status", map[string]any{"openTaskId": rt.StrFirst("open-task-id", "task-id")})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,72 @@ 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"},
|
||||
@@ -190,7 +256,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]; got != want {
|
||||
if got := fake.args[key]; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s = %#v, want %#v", key, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,14 +119,24 @@ var ChatUpdate = shortcut.Shortcut{
|
||||
Intent: "当你只需要修改群名称时使用;这是 lark-cli +chat-update 的诚实子集,只接受群 openConversationId 和新名称。修改群 description、个人备注、群昵称或其他群设置时不要使用。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
|
||||
{Name: "name", Type: shortcut.FlagString, Desc: "新的群名称", Required: true},
|
||||
{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"}},
|
||||
},
|
||||
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.Str("group"),
|
||||
"group_name": rt.Str("name"),
|
||||
"openconversation_id": rt.StrFirst("group", "id", "chat-id", "conversation-id", "open-conversation-id"),
|
||||
"group_name": rt.StrFirst("name", "title", "new-title"),
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -391,6 +401,8 @@ 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,
|
||||
@@ -402,7 +414,7 @@ var FlagList = shortcut.Shortcut{
|
||||
if rt.Int("cursor") < 0 {
|
||||
return apperrors.NewValidation("--cursor 必须大于等于 0")
|
||||
}
|
||||
if size := rt.Int("size"); size < 1 || size > 100 {
|
||||
if size := rt.IntFirst("size", "limit", "max"); size < 1 || size > 100 {
|
||||
return apperrors.NewValidation("--size 必须在 1-100 之间")
|
||||
}
|
||||
return nil
|
||||
@@ -410,7 +422,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.Int("size")),
|
||||
"size": strconv.Itoa(rt.IntFirst("size", "limit", "max")),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,6 +96,17 @@ 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)
|
||||
@@ -358,9 +369,27 @@ 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 {
|
||||
lines = append(lines, " - "+constraintHelp(constraint))
|
||||
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
|
||||
}
|
||||
return long + "\n\n参数约束:\n" + strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package shortcut
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -137,6 +138,7 @@ func TestCrossPlatformCoverageSchemaConstraintCollapsesHiddenAliases(t *testing.
|
||||
cmd := mount(Shortcut{
|
||||
Service: "chat",
|
||||
Command: "+search",
|
||||
Intent: "搜索消息",
|
||||
Flags: []Flag{
|
||||
{Name: "query", Type: FlagString},
|
||||
{Name: "keyword", Type: FlagString, Hidden: true},
|
||||
@@ -165,6 +167,11 @@ 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) {
|
||||
@@ -179,6 +186,8 @@ 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,
|
||||
@@ -193,6 +202,12 @@ 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)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ var AtMe = shortcut.Shortcut{
|
||||
`dws chat +at-me`,
|
||||
`dws chat +at-me --days 3`,
|
||||
},
|
||||
Validate: chatshortcut.ValidateMessageResourceDownload,
|
||||
Validate: validateAtMe,
|
||||
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,6 +109,23 @@ 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,6 +22,22 @@ 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 {
|
||||
|
||||
@@ -17,6 +17,7 @@ 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"
|
||||
@@ -66,8 +67,10 @@ 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()...),
|
||||
@@ -79,7 +82,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: chatshortcut.ValidateMessageResourceDownload,
|
||||
Validate: validateChatMessages,
|
||||
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
|
||||
@@ -88,8 +91,8 @@ var ChatMessages = shortcut.Shortcut{
|
||||
params := map[string]any{}
|
||||
fallbackConversationID := ""
|
||||
|
||||
if rt.Changed("time") && rt.Str("time") != "" {
|
||||
params["time"] = rt.Str("time")
|
||||
if boundary := rt.StrFirst("time", "before"); boundary != "" {
|
||||
params["time"] = boundary
|
||||
} else {
|
||||
params["time"] = formatDingTalkMessageBoundary(time.Now())
|
||||
}
|
||||
@@ -99,7 +102,9 @@ 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("direction") {
|
||||
if rt.Changed("before") {
|
||||
params["forward"] = false
|
||||
} else if rt.Changed("direction") {
|
||||
switch strings.TrimSpace(strings.ToLower(rt.Str("direction"))) {
|
||||
case "newer":
|
||||
params["forward"] = true
|
||||
@@ -148,6 +153,45 @@ 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,6 +218,20 @@ 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 {
|
||||
@@ -246,3 +260,117 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,16 +40,21 @@ var DM = shortcut.Shortcut{
|
||||
"内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "to", Type: shortcut.FlagString, Desc: "收件人姓名/花名", Required: true},
|
||||
{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: "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.Str("to"))
|
||||
user, err := resolveOpenDingTalkUser(rt, rt.StrFirst("to", "name", "keyword"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ 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")
|
||||
|
||||
@@ -109,17 +115,21 @@ 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"}},
|
||||
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id", "id", "chat-id", "open-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.Str("conversation-id"))
|
||||
groupID := strings.TrimSpace(rt.StrFirst("conversation-id", "id", "chat-id", "open-conversation-id"))
|
||||
groupName := strings.TrimSpace(rt.Str("group"))
|
||||
if groupID == "" {
|
||||
resolved, err := resolveGroupName(rt, groupName)
|
||||
@@ -191,6 +201,25 @@ 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,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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"),
|
||||
)
|
||||
}
|
||||
@@ -45,9 +45,12 @@ 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 点前提交进度"`},
|
||||
Tips: []string{`dws chat +send-to-group --group 项目冲刺 --text "今天 5 点前提交进度"`},
|
||||
Validate: validateSendToGroup,
|
||||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||||
groupName := rt.Str("group")
|
||||
text := rt.Str("text")
|
||||
@@ -82,6 +85,35 @@ 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
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
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"
|
||||
@@ -63,7 +65,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: chatshortcut.ValidateMessageResourceDownload,
|
||||
Validate: validateThreadReplies,
|
||||
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
|
||||
@@ -105,6 +107,22 @@ 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.
|
||||
|
||||
@@ -59,6 +59,12 @@ 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.
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
|
||||
// NewShortcutCommand builds the `dws shortcut` management command tree:
|
||||
//
|
||||
// dws shortcut list [--service x] # list built-in shortcuts
|
||||
// dws shortcut list [--service x] [--compact] # 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,6 +51,7 @@ 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 {
|
||||
@@ -61,19 +62,37 @@ func newListCommand() *cobra.Command {
|
||||
}
|
||||
rows = append(rows, newShortcutListRow(s))
|
||||
}
|
||||
return output.WriteCommandPayload(cmd, map[string]any{
|
||||
payload := map[string]any{
|
||||
"catalog": "shortcut",
|
||||
"runtime_schema": true,
|
||||
"count": len(rows),
|
||||
"shortcuts": rows,
|
||||
}, output.FormatJSON)
|
||||
}
|
||||
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)
|
||||
},
|
||||
}
|
||||
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"`
|
||||
@@ -94,6 +113,16 @@ 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 == "" {
|
||||
|
||||
@@ -75,6 +75,57 @@ 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",
|
||||
|
||||
@@ -50,12 +50,17 @@ 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 Runtime Catalog/Schema. Add services
|
||||
# here only after verifying that the product skill has its own reviewed routing
|
||||
# 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
|
||||
# section and intent table; compacting a sparse skill without an alternative
|
||||
# route would make its shortcuts harder to discover.
|
||||
COMPACT_PRODUCT_SERVICES = {"chat"}
|
||||
COMPACT_PRODUCT_SERVICES = {"aitable", "chat", "doc"}
|
||||
|
||||
# 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 "")
|
||||
@@ -135,12 +140,24 @@ 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 发现(按需)
|
||||
|
||||
`{md_escape(service)}` 当前有 {len(rows)} 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
|
||||
{inventory}已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
|
||||
|
||||
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service {md_escape(service)} --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
|
||||
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service {md_escape(service)} --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
|
||||
{PRODUCT_END}"""
|
||||
|
||||
|
||||
|
||||
@@ -7,8 +7,12 @@ 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
|
||||
@@ -17,11 +21,25 @@ 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 && /^\| `dws chat \+/ { count++ }
|
||||
in_block && /^\|[[:space:]]*`/ { count++ }
|
||||
END { print count + 0 }
|
||||
' "$chat_skill"
|
||||
)"
|
||||
@@ -31,6 +49,34 @@ 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
|
||||
@@ -38,4 +84,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)"
|
||||
"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)"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
name: dws
|
||||
description: 管理钉钉产品能力(AI表格/AI搜问/日历/通讯录/群聊与机器人/待办/审批/考勤/日志/DING消息/开放平台文档/钉钉文档/钉钉云盘/原生Markdown文件/AI听记/邮箱/在线电子表格/知识库等)。当用户需要操作表格数据、管理日程会议、模糊找人/查谁负责某事项、查询通讯录、管理群聊、机器人发消息、创建待办、提交审批、查看考勤、提交日报周报(钉钉日志模版)、读写钉钉文档、上传下载云盘文件、读取或修改原生.md文件、查询听记纪要、收发邮件、读写在线电子表格(axls)、管理钉钉知识库,或订阅个人 IM 事件、实时监听群成员加入、群成员退出、群改名和群解散时使用。
|
||||
cli_version: ">=1.0.15"
|
||||
metadata:
|
||||
cli_version: ">=1.0.15"
|
||||
---
|
||||
|
||||
# 钉钉全产品 Skill
|
||||
@@ -21,15 +22,15 @@ cli_version: ">=1.0.15"
|
||||
- 危险操作必须先向用户确认,用户同意后才加 `--yes` 执行
|
||||
- 单次批量操作不超过 30 条记录
|
||||
- 所有命令必须**严格遵循**对应产品参考文档里面规定的参数格式(如:如果有参数值,则参数和参数值之间至少用一个空格隔开)
|
||||
- **脚本优先**:[scripts/](./scripts/) 下的 `python scripts/<name>.py` 已封装翻页/轮询/批量逻辑,遇到对应场景(如 AI 表格批量导入导出、AI 应用创建轮询、文档创建后写内容、钉盘目录树等)**优先调用脚本**而非手写多步命令。脚本均支持 `--dry-run` 预览、`--format json` 输出,失败时回退到手动步骤
|
||||
- **CLI 路径必须独立可用**:不要假定用户环境安装了 Python。优先使用 `dws` 原生命令或 Shortcut;[scripts/](./scripts/) 仅在对应运行时已确认可用且能明显简化翻页、轮询或批量操作时作为可选加速项。脚本不可用时直接执行同场景的原生命令流程,不得把缺少 Python 当成能力阻塞
|
||||
- **实时个人消息事件例外**:用户要监听消息、订阅事件、自动回复消息或事件驱动 Agent 时,必须走 `dws event consume ... --flatten` 长连接,不要写脚本轮询消息历史
|
||||
|
||||
## Shortcut 与原子命令的使用原则
|
||||
|
||||
`shortcut` 是对常用操作的高层封装,适合优先承担用户意图;产品参考文档和本 skill 负责判断意图、风险、跨产品流程和复杂参数,CLI 帮助负责声明当前版本真正可调用的命令。
|
||||
|
||||
- 先按产品参考、意图表和 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` 只作为轻量批量发现入口。
|
||||
- 先按产品参考、意图表和 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` 的完整契约。
|
||||
- 真正组装参数前用叶子帮助 `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 | **优先**:`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. 合并全部消息后总结 |
|
||||
| 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. 合并结果后总结或按用户要求保存 |
|
||||
| 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 | **多群批量优先**:`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>`(备选) |
|
||||
| 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>`(备选) |
|
||||
| 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,6 +2,13 @@
|
||||
|
||||
> 通用规范见 [_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 "..."` |
|
||||
|
||||
@@ -4,24 +4,26 @@
|
||||
|
||||
## Shortcut 优先路由
|
||||
|
||||
常见 Agent 意图优先使用公开 `+` Shortcut;下面的原子命令章节保留给需要特定原始返回结构、兼容参数或 Shortcut 未覆盖字段的场景。执行前用 `dws schema --cli-path "chat +<shortcut>" --format json` 读取最终参数、约束和确认语义。
|
||||
精确脚本 / 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。
|
||||
|
||||
| 意图 | 首选 |
|
||||
|---|---|
|
||||
| 以 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` 会自动规范化并补齐 @ 占位符。user 使用 `<@id>` / `<@all>`;bot/webhook 使用 `@id` / `@手机号` / `@all`。声明 `--at-*` / `--at-all` 即可,不要为统一 Shortcut 手工拼 `@10`。
|
||||
- `+messages-send` 自动规范化并补齐对应身份的 @ 占位符。声明 `--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。
|
||||
- 上述五个查询 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` 使用。
|
||||
|
||||
### group (群组管理)
|
||||
|
||||
@@ -1169,6 +1171,10 @@ 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:
|
||||
@@ -1539,10 +1545,12 @@ Flags:
|
||||
Usage:
|
||||
dws chat category delete [flags]
|
||||
Example:
|
||||
dws chat category delete --category-id <分组ID>
|
||||
# 先向用户确认,确认后执行
|
||||
dws chat category delete --category-id <分组ID> --yes
|
||||
# 分组ID 可通过 dws chat category list 获取
|
||||
Flags:
|
||||
--category-id int 会话分组 ID (必填)
|
||||
--yes 跳过运行时确认;只能在用户明确确认后传入
|
||||
```
|
||||
|
||||
#### 重命名会话分组
|
||||
@@ -1771,15 +1779,18 @@ Flags:
|
||||
Usage:
|
||||
dws chat clear-messages [flags]
|
||||
Example:
|
||||
dws chat clear-messages --conversation-id <openConversationId>
|
||||
dws chat clear-messages --id <openConversationId>
|
||||
# 先向用户说明目标会话和影响范围,确认后执行
|
||||
dws chat clear-messages --conversation-id <openConversationId> --yes
|
||||
dws chat clear-messages --id <openConversationId> --yes
|
||||
Flags:
|
||||
--conversation-id string 会话 openConversationId (必填,支持群聊/单聊)
|
||||
--id string --conversation-id 的别名
|
||||
--chat string --conversation-id 的别名
|
||||
--yes 跳过运行时确认;只能在用户明确确认后传入
|
||||
|
||||
注意:
|
||||
- 仅清空当前用户视角的消息,不影响其他成员
|
||||
- 仍属于高风险操作;未确认时不得传 --yes 或执行
|
||||
- openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取
|
||||
```
|
||||
|
||||
@@ -2201,7 +2212,7 @@ dws chat message send --group <openConversationId> --msg-type image --media-id "
|
||||
|
||||
群聊传 --group,单聊传 --receiver,二者互斥。
|
||||
|
||||
**注意:send-card 必须和 update-card 搭配使用。** 创建卡片时无需传入内容,后续通过 update-card 更新内容,最后一次更新必须将 --flow-status 设为 3(finish),否则卡片会一直处于"生成中"的加载状态。
|
||||
**注意:本节原子 send-card 必须和 update-card 搭配使用。** 创建卡片时无需传入内容,后续通过 update-card 更新内容,最后一次更新必须将 --flow-status 设为 3(finish),否则卡片会一直处于"生成中"的加载状态。若使用 `+messages-send-card --content ...`,Shortcut 会在创建后立即完成一次更新;不传 `--content` 时仍返回 `bizId` 供本节 update-card 使用。
|
||||
flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成(FINISH),4=执行中(EXECUTING),5=错误(ERROR)。
|
||||
```
|
||||
Usage:
|
||||
@@ -2320,12 +2331,15 @@ 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 取消
|
||||
|
||||
## 自动化脚本
|
||||
## 可选自动化脚本
|
||||
|
||||
| 脚本 | 场景 | 用法 |
|
||||
|------|------|------|
|
||||
| [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"` |
|
||||
这些脚本仅用于已确认安装 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"` |
|
||||
|
||||
## 相关产品
|
||||
|
||||
|
||||
@@ -665,8 +665,8 @@ Flags:
|
||||
- 知识库内 → `dws wiki node create --workspace <WS_ID> --type folder`(`doc folder create` / `doc file create --type folder` 已弃用)
|
||||
|
||||
用户说"上传文件/传文件/上传到文档/上传到知识库":
|
||||
- 上传 → `upload`(需本地文件路径)
|
||||
- 上传并转换 → `upload --convert`
|
||||
- 仅保留原始文件用于存储/下载 → `drive upload`(需本地文件路径)
|
||||
- 用户明确要求“在线编辑/大家直接在线改/转在线文档” → `doc import --file <本地路径>`;不得用普通 upload 的成功响应宣称可在线编辑
|
||||
|
||||
用户说"导入文件/导入为在线文档/导入 Word/导入 Excel/导入 xmind/导入 Markdown/把本地文件转在线文档":
|
||||
- 导入并转换为在线文档 → `doc import --file <本地路径>`
|
||||
@@ -742,8 +742,8 @@ Flags:
|
||||
关键区分: doc(文档编辑/阅读) vs aitable(数据表格操作) vs drive(钉盘文件管理)
|
||||
|
||||
用户说"上传文件/传文件/上传到文档/上传到知识库":
|
||||
- 上传 → `upload`(需本地文件路径)
|
||||
- 上传并转换 → `upload --convert`
|
||||
- 仅保留原始文件用于存储/下载 → `drive upload`(需本地文件路径)
|
||||
- 要转换为可在线编辑文档 → `doc import --file <本地路径>`,导入后验证在线类型与目标文件夹
|
||||
|
||||
用户说"下载文件/导出文件/下载到本地":
|
||||
- 下载 → `download`(需文件节点 ID 或 URL)
|
||||
@@ -1050,14 +1050,18 @@ 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`
|
||||
- `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;只有用户未指定块操作的纯文本追加才建议 `update --mode append`。用户点名 list/insert/update/append 时必须逐项真实调用,不得折叠进 create
|
||||
- `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` 参数
|
||||
- `doc upload vs drive upload`:用户提到"知识库/文档空间/workspace" → `doc upload`;提到"钉盘/网盘/我的文件" → `drive upload`;未明确目标时默认 `drive upload`
|
||||
- `upload` 支持上传任意类型文件 (PDF、Office、图片等) 到钉钉文档空间或知识库;`--convert` 可将 Office 文件转换为钉钉在线文档
|
||||
- `drive upload` / `doc upload` 是普通文件存储路径;用户要求 Word/Excel “在线编辑/直接在线改”时硬路由到 `doc import`,并验证导入后的在线类型和文件夹。只有用户明确同时要原文件与在线版时才分别 upload + import
|
||||
- 同一请求中新建、复制或导入返回的 `nodeId` 必须绑定后续“这篇/刚才那篇/上次那篇”;禁止搜索同名旧资源覆盖绑定
|
||||
- `--name` 只是文档外壳标题,不能替代用户显式要求的正文 H1;用户说“正文先起一级标题”时必须写入或插入真实 H1
|
||||
- 汇总只能保持用户事实强度:“验证 12 条”不等于“12 条全部通过”,“整理问题清单”不等于“输出根因分析”
|
||||
- 写操作响应为 `null`/空对象或回查未变化时,该步骤失败;必须报告部分完成,禁止用其他成功步骤把整体说成“全部完成”
|
||||
- `upload` 是三步自动完成的流程 (获取凭证 → OSS 上传 → 提交入库),无需手动分步操作
|
||||
- `download` 是两步自动完成的流程 (获取下载链接 → HTTP GET 下载),支持自动推断文件名;`--output` 可指定文件路径或目录
|
||||
- `media insert` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 插入附件块到文档),无需手动分步操作
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
# doc block(块级精细编辑:list / insert / update / delete)
|
||||
|
||||
> **前置条件(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 复制范例)
|
||||
> 本文件自包含简单 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)。
|
||||
|
||||
> **改写已有文档优先 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(查询块元素)
|
||||
@@ -173,7 +169,8 @@ 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`。
|
||||
- **简单内容追加**:建议用 [`./doc-update.md`](./doc-update.md) `--mode append`,不必走 block insert。
|
||||
- **有序列表块**:用户明确要求 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。
|
||||
- **JSONML validator**(写入端默认行为):
|
||||
- 裸字符串、缺 uuid 等结构错误会被 validator 抦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。
|
||||
- `--fix-jsonml` 开启 JSON 语法修复,推荐 agent 调用。
|
||||
@@ -241,6 +238,12 @@ 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,9 +1,6 @@
|
||||
# doc comment(文档评论:list / create / reply / update / delete / create-inline)
|
||||
|
||||
> **前置条件(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 文本)
|
||||
> 本文件自包含评论命令契约。仅在需要 mention 时查询真实 userId/openConversationId;仅在划词评论尚无 blockId 与 paragraph 文本时读取 [`doc-block.md`](./doc-block.md) 并执行 block list。
|
||||
|
||||
---
|
||||
|
||||
@@ -131,6 +128,7 @@ 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,11 +1,6 @@
|
||||
# doc create(创建文档)
|
||||
|
||||
> **前置条件(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` 时必读
|
||||
> 本文件自包含普通 Markdown 创建契约,不要递归预读 `doc.md`、style 或 update reference。仅当用户要求复杂版式并实际选择 JSONML 时,读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);需要文档骨架建议时才读取对应 style 章节。
|
||||
|
||||
## 创建路由前置判断(必看)
|
||||
|
||||
@@ -40,7 +35,7 @@ Flags:
|
||||
|
||||
## 关键说明
|
||||
|
||||
- **`--name` 是 H1**:正文从 `##` 开始;正文内不要再写 `#` 一级标题(除非确需且已说明动机)。
|
||||
- **标题优先级**:`--name` 是文档外壳标题,默认可视作 H1;但它不能替代用户显式要求的正文一级标题。用户说“正文写 `# ...`”“正文先起个一级标题”时,必须在初始内容中保留该 `#` H1;用户未要求正文 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 -`。
|
||||
@@ -54,6 +49,12 @@ 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,7 +1,6 @@
|
||||
# doc export(在线文档导出为 docx)
|
||||
|
||||
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
|
||||
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
|
||||
> 本文件自包含 export 契约。已有当前请求返回的 adoc nodeId 时直接导出;目标类型未知时只执行一次 info 检查,不要递归读取 `doc.md`。
|
||||
|
||||
> **路由前置判断**:用户说「下载/导出」时**必须**先用 [`./doc-info.md`](./doc-info.md) `info --node <ID> --format json` 查 `contentType`:
|
||||
> - `contentType` 为 `ALIDOC`(在线文档)→ **必须用 `export`**,禁止用 `download`
|
||||
@@ -45,6 +44,7 @@ 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,7 +1,6 @@
|
||||
# doc 文件操作(upload / download / copy / move / rename / delete + folder create)
|
||||
|
||||
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
|
||||
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
|
||||
> **按需使用**:本文件自包含弃用命令的兼容说明,不要求先读总路由;优先按下方提示改用 `drive` / `wiki`。
|
||||
|
||||
> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令:
|
||||
> - `doc download` → **`dws drive download`**(下载已有文件;在线文档导出 docx 仍走 `doc export`)
|
||||
@@ -32,6 +31,7 @@ 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,6 +6,8 @@
|
||||
|
||||
不要先读取文件内容再调用 `doc create` 或 `doc update`。`doc import` 会按文件格式走导入任务,保留更完整的原始结构。
|
||||
|
||||
> **在线编辑硬路由**:用户说“上传后在线编辑/大家直接在线改/转成钉钉文档”时必须使用 `doc import`。`drive upload` 只保留原始 `.docx/.xlsx/...` 普通文件,不能据此宣称已可在线编辑。若用户明确要同时保留原文件和在线版,才先 `drive upload`,再单独 `doc import`,并分别验证两个返回节点。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
@@ -38,6 +40,7 @@ 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,8 +1,6 @@
|
||||
# doc info(获取文档元信息 + URL 解析)
|
||||
|
||||
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
|
||||
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
|
||||
> 2. [`../../url-patterns.md`](../../url-patterns.md) — 仅当用户原始 `alidocs` URL 需要 probe 时
|
||||
> 本文件自包含已知 nodeId/URL 的 info 契约。只有原始 alidocs URL 类型仍不明确时,才读取 [`url-patterns.md`](../../url-patterns.md);不要递归读取 `doc.md`。
|
||||
>
|
||||
> **同任务常配合**:`dws drive search` / `dws wiki node search`(先定位 nodeId)/ [`doc-read.md`](./doc-read.md)(确认是 ALIDOC 后读正文)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# doc media(附件 / 图片:download / insert)
|
||||
|
||||
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
|
||||
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
|
||||
> 本文件自包含 media insert/download 契约。nodeId、文件路径或 resourceId 已知时直接执行;只在需要相对块定位且 blockId 未知时读取 [`doc-block.md`](./doc-block.md)。
|
||||
|
||||
> ⚠️ **图片插入硬规则**:
|
||||
> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws drive download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert`
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# doc permission(文档权限:add / update / list)
|
||||
|
||||
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
|
||||
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
|
||||
> **按需使用**:本文件自包含文档节点权限命令;不要求先读总路由。知识库整体成员权限改读 `dingtalk-wiki`。
|
||||
|
||||
> **关键区分**:
|
||||
> - "把**某篇文档**授权给某人" → `doc permission add`(节点级,包括「我的文档」下的文档都支持)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
# doc read(读取文档内容)
|
||||
|
||||
> **前置条件(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)(块级精修前先读结构)
|
||||
> 本文件自包含普通 read 契约。用户已给当前 adoc nodeId/URL 时直接读取;类型未知时才先执行 info。选择 JSONML 只为获取结构,不要求预读 cookbook;实际构造 JSONML 写入时再按需加载。
|
||||
|
||||
## 命令格式
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
# doc update(更新文档内容)
|
||||
|
||||
> **前置条件(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)
|
||||
> 本文件自包含普通 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)。
|
||||
|
||||
## 命令格式
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
|
||||
> 改写已有文档见 [doc-update-workflow.md](./doc-update-workflow.md)。排版规范见 [doc-style-guideline.md](./doc-style-guideline.md)。
|
||||
|
||||
## 前置必读
|
||||
## 按需使用
|
||||
|
||||
> **同时读取 [doc-style-guideline.md](./doc-style-guideline.md):**
|
||||
> - **§2.0 类型判断决策表** → 锁定文档类型(决策型 / 执行型 / 说明型 / 知识沉淀型)和骨架
|
||||
> - **§1 硬规则** → 全程生效(`--name` 已是 H1、不编造 URL、Markdown 草稿不写 callout 等)
|
||||
普通 Markdown 创建不需要先读本文件或 style guideline。只有用户要求设计文档骨架或复杂版式时,才查看下方对应章节;实际选择 JSONML 后再读取 cookbook。需要按文档类型选骨架时,按需读取 [doc-style-guideline.md](./doc-style-guideline.md) 的对应一节,不要通读。
|
||||
|
||||
### 关键词速查(用户意图 → 起稿路径)
|
||||
|
||||
@@ -45,7 +43,7 @@
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| 标题 | 用 `--name` 传入;正文不要再重复同名一级标题 |
|
||||
| 标题 | 用 `--name` 传入;用户显式要求正文 H1 时原样保留,未要求时正文默认从 H2 开始 |
|
||||
| 位置 | 默认创建到我的文档;指定目录时只接受文档文件夹 `nodeId` 或 alidocs 文件夹 URL |
|
||||
| 正文 | 多行、表格、代码块、特殊字符或长度 >= 2KB 时必须写入 UTF-8 临时 `.md` 文件 |
|
||||
| 格式 | 按 §JSONML 起稿判定 决定起稿路径:命中 JSONML 起稿条件时**直接用 JSONML 构造**(跳过 markdown);未命中时用 Markdown 起稿,创建后按 [doc-update-workflow.md](./doc-update-workflow.md) 精修 |
|
||||
@@ -226,7 +224,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
|
||||
|
||||
> **脚手架策略警示**:Markdown 无法表达分栏/callout/色彩表头,拉回的 JSONML 只有纯文本骨架。精修阶段不是“在现有结构上加色”,而是“参照 RFC/Spec 重组结构”。
|
||||
|
||||
> **MUST READ**:动手写 JSONML 前,必须先用 Read 工具读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md) — 其中 §决策型文档骨架范例 有可直接复制修改的完整模板。
|
||||
> **JSONML 条件加载**:仅在确定使用 JSONML 后读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md);其中“决策型文档骨架范例”可直接改写。
|
||||
> 节点类型和属性的权威定义见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。
|
||||
|
||||
### ⚠️ JSONML 降级约束
|
||||
@@ -274,7 +272,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
|
||||
```
|
||||
|
||||
- 根节点固定 `"root"`(不是 `"body"`)
|
||||
- `--name` 已是 H1,JSONML 从 `h2` 开始
|
||||
- 用户未要求正文 H1 时,JSONML 从 `h2` 开始;用户明确要求“正文一级标题/插入一级标题”时必须构造 `h1`
|
||||
- 表格结构是 `table → tr → tc`(无 `th`/`td`)
|
||||
- 分栏是 `table` + `"sr": true`,`tc` 建议设 `fill` 背景色
|
||||
- 有序列表:仅第一项设 `"start": 1`,后续项不设 `start`(系统自动递增)
|
||||
@@ -315,7 +313,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. **`--name` 是 H1**:正文从 `##` 开始;正文内不写 `#`(除非确需正文内再造一级 H1 并说明动机)
|
||||
1. **用户显式正文 H1 优先**:`--name` 是文档外壳标题;用户未要求正文一级标题时从 `##` 开始。用户明确说“正文写 `# ...` / 先起一级标题 / 插入一级标题”时必须保留或插入真实 H1,不得用 `--name` 代替
|
||||
2. **同类信息同表达**:风险、状态、行动项、证据,每类只用一种元素 + 一种视觉语义(见 §5)
|
||||
3. **Markdown 草稿阶段只用稳定元素**:标题、段落、列表、checklist、表格、代码块;callout / 分栏 / 附件 / 复杂嵌套留到创建后用 `doc block insert` / `doc media insert` 精修
|
||||
4. **引用块只用于原文**:用户原话、会议摘录、外部材料原文;不许包装作者自己的结论
|
||||
@@ -209,7 +209,7 @@
|
||||
|
||||
### 4.1 标题与段落
|
||||
|
||||
- 正文从 `##` 开始(H1 已被 `--name` 占用)
|
||||
- 用户未要求正文 H1 时从 `##` 开始;用户明确要求正文 H1 时按原文使用 `#` 或 heading level 1
|
||||
- 标题层级 ≤ 4 层(§7)
|
||||
- 单段过长先拆段,再考虑换元素
|
||||
|
||||
@@ -217,6 +217,7 @@
|
||||
|
||||
- 普通列表:并列要点
|
||||
- 有序列表:顺序步骤
|
||||
- 用户明确要求“有序列表块”时必须使用真实列表结构;JSONML 为带 `list.isOrdered=true` 的多个 `p` 节点,不能只写带数字前缀的普通段落或以整篇 Markdown 代替显式 block insert
|
||||
- checklist:待办状态(含 `- [ ]` / `- [x]`)
|
||||
|
||||
列表项里开始出现"负责人 / 截止时间 / 状态"这类字段时,改用表格。
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
用机器人向多个群批量发送相同消息(如日报提醒)
|
||||
|
||||
用法:
|
||||
python bot_broadcast.py \
|
||||
python3 scripts/bot_broadcast.py \
|
||||
--robot-code <ROBOT_CODE> \
|
||||
--chats "conv_id1,conv_id2,conv_id3" \
|
||||
--title "日报提醒" \
|
||||
--text "请大家今天下班前提交日报"
|
||||
|
||||
python bot_broadcast.py \
|
||||
python3 scripts/bot_broadcast.py \
|
||||
--robot-code <ROBOT_CODE> \
|
||||
--chats-file groups.txt \
|
||||
--title "周会通知" \
|
||||
--text "明天下午3点周会"
|
||||
|
||||
python bot_broadcast.py --dry-run ...
|
||||
python3 scripts/bot_broadcast.py --dry-run ...
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -40,14 +40,19 @@ def run_dws(
|
||||
if result.returncode != 0:
|
||||
print(f" ✗ 错误:{result.stderr.strip()}")
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
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
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f" ✗ 错误:{e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
def run(argv: Optional[List[str]] = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description='向多个群批量发送机器人消息'
|
||||
)
|
||||
@@ -64,7 +69,7 @@ def main():
|
||||
'--text', required=True, help='消息内容 Markdown'
|
||||
)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
chat_ids: List[str] = []
|
||||
if args.chats:
|
||||
@@ -74,13 +79,13 @@ def main():
|
||||
p = Path(args.chats_file)
|
||||
if not p.exists():
|
||||
print(f"错误:文件不存在: {p}")
|
||||
sys.exit(1)
|
||||
return 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')
|
||||
sys.exit(1)
|
||||
return 1
|
||||
|
||||
print(f"📢 批量发送消息到 {len(chat_ids)} 个群")
|
||||
print(f" 标题: {args.title}")
|
||||
@@ -105,8 +110,8 @@ def main():
|
||||
fail += 1
|
||||
|
||||
print(f"\n完成: 成功 {success}, 失败 {fail}")
|
||||
sys.exit(0 if fail == 0 else 1)
|
||||
return 0 if fail == 0 else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
sys.exit(run())
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
导出群聊消息到 JSON 文件(从指定时间点拉取)
|
||||
|
||||
用法:
|
||||
python chat_export_messages.py \
|
||||
python3 scripts/chat_export_messages.py \
|
||||
--group <openconversation_id> \
|
||||
--time "2026-03-10 00:00:00" \
|
||||
--output messages.json
|
||||
|
||||
python chat_export_messages.py \
|
||||
python3 scripts/chat_export_messages.py \
|
||||
--query "项目冲刺" \
|
||||
--time "2026-03-10 00:00:00" \
|
||||
--no-forward --limit 100
|
||||
@@ -16,11 +16,51 @@
|
||||
|
||||
import sys
|
||||
import json
|
||||
import datetime
|
||||
import subprocess
|
||||
import argparse
|
||||
from typing import List, Any, Optional
|
||||
|
||||
|
||||
class ScriptError(RuntimeError):
|
||||
"""可预期的脚本执行错误。"""
|
||||
|
||||
exit_code = 1
|
||||
|
||||
|
||||
class AmbiguousTargetError(ScriptError):
|
||||
"""搜索结果不唯一,需要调用方消歧。"""
|
||||
|
||||
exit_code = 2
|
||||
|
||||
|
||||
def normalize_boundary_time(value: Any) -> str:
|
||||
"""将消息时间边界规范为 CLI 接受的 yyyy-MM-dd HH:mm:ss 或原字符串。"""
|
||||
if value is None or isinstance(value, bool):
|
||||
return ''
|
||||
if isinstance(value, (int, float)):
|
||||
timestamp = float(value)
|
||||
elif isinstance(value, str):
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return ''
|
||||
try:
|
||||
timestamp = float(text)
|
||||
except ValueError:
|
||||
return text
|
||||
else:
|
||||
return str(value).strip()
|
||||
if timestamp > 10_000_000_000:
|
||||
timestamp /= 1000
|
||||
try:
|
||||
dt = datetime.datetime.fromtimestamp(
|
||||
timestamp, tz=datetime.timezone(datetime.timedelta(hours=8))
|
||||
)
|
||||
except (OSError, OverflowError, ValueError) as exc:
|
||||
raise ScriptError(f'无效的消息时间边界:{value}') from exc
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
@@ -32,14 +72,19 @@ def run_dws(
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=120
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f"错误:{e}", file=sys.stderr)
|
||||
return None
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
|
||||
raise ScriptError(f'执行 dws 失败:{exc}') from exc
|
||||
if result.returncode != 0:
|
||||
detail = result.stderr.strip() or f'退出码 {result.returncode}'
|
||||
raise ScriptError(f'dws 命令失败:{detail}')
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ScriptError(f'dws 返回的不是合法 JSON:{exc}') from exc
|
||||
if isinstance(data, dict) and data.get('success') is False:
|
||||
detail = data.get('errorMsg') or data.get('message') or '未知错误'
|
||||
raise ScriptError(f'dws 业务调用失败:{detail}')
|
||||
return data
|
||||
|
||||
|
||||
def search_group(
|
||||
@@ -51,14 +96,17 @@ def search_group(
|
||||
], dry_run=dry_run)
|
||||
if dry_run:
|
||||
return '<CONV_ID>'
|
||||
if not data:
|
||||
return None
|
||||
if isinstance(data, list):
|
||||
groups = data
|
||||
elif isinstance(data, dict):
|
||||
inner = data.get('result', data)
|
||||
if isinstance(inner, dict):
|
||||
groups = inner.get('items', inner.get('groups', []))
|
||||
groups = (
|
||||
inner.get('value')
|
||||
or inner.get('groups')
|
||||
or inner.get('items')
|
||||
or []
|
||||
)
|
||||
elif isinstance(inner, list):
|
||||
groups = inner
|
||||
else:
|
||||
@@ -66,16 +114,64 @@ def search_group(
|
||||
else:
|
||||
groups = []
|
||||
if not groups:
|
||||
print(f"未找到群聊: {query}")
|
||||
return None
|
||||
g = groups[0]
|
||||
raise ScriptError(f'未找到群聊:{query}')
|
||||
exact = [
|
||||
item for item in groups
|
||||
if isinstance(item, dict)
|
||||
and str(item.get('title') or item.get('name') or '').strip().casefold()
|
||||
== query.strip().casefold()
|
||||
]
|
||||
candidates = exact if exact else groups
|
||||
if len(candidates) != 1:
|
||||
rendered = []
|
||||
for item in candidates:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = item.get('title') or item.get('name') or '未知'
|
||||
conv_id = item.get('openConversationId') or item.get('id') or '无ID'
|
||||
rendered.append(f'{name} ({conv_id})')
|
||||
detail = ';'.join(rendered) or f'{len(candidates)} 个候选'
|
||||
raise AmbiguousTargetError(
|
||||
f'群名“{query}”匹配到多个候选,请指定 --group:{detail}'
|
||||
)
|
||||
g = candidates[0]
|
||||
name = g.get('title') or g.get('name', '未知')
|
||||
conv_id = g.get('openConversationId') or g.get('id')
|
||||
if not conv_id:
|
||||
raise ScriptError(f'群聊“{name}”缺少 openConversationId')
|
||||
print(f" 找到群聊: {name} ({conv_id})")
|
||||
return conv_id
|
||||
|
||||
|
||||
def main():
|
||||
def parse_message_page(data: Any) -> tuple[List[Any], bool]:
|
||||
"""提取消息页和 hasMore。"""
|
||||
if isinstance(data, list):
|
||||
return data, False
|
||||
if not isinstance(data, dict):
|
||||
return [], False
|
||||
inner = data.get('result', data)
|
||||
if isinstance(inner, dict):
|
||||
messages = inner.get('messages', [])
|
||||
return messages if isinstance(messages, list) else [], bool(
|
||||
inner.get('hasMore', False)
|
||||
)
|
||||
if isinstance(inner, list):
|
||||
return inner, False
|
||||
return [], False
|
||||
|
||||
|
||||
def message_identity(message: Any) -> Optional[str]:
|
||||
if not isinstance(message, dict):
|
||||
return None
|
||||
value = (
|
||||
message.get('openMessageId')
|
||||
or message.get('openMsgId')
|
||||
or message.get('msgId')
|
||||
)
|
||||
return str(value) if value else None
|
||||
|
||||
|
||||
def run(argv: Optional[List[str]] = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description='导出群聊消息到 JSON'
|
||||
)
|
||||
@@ -95,100 +191,115 @@ def main():
|
||||
)
|
||||
parser.add_argument('--output', default='', help='输出文件')
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
conv_id = args.group
|
||||
if not conv_id:
|
||||
if not args.query:
|
||||
print('错误:需要 --group 或 --query 参数')
|
||||
sys.exit(1)
|
||||
print(f'🔍 搜索群聊: {args.query}')
|
||||
conv_id = search_group(args.query, args.dry_run)
|
||||
if not conv_id and not args.dry_run:
|
||||
sys.exit(1)
|
||||
try:
|
||||
conv_id = args.group
|
||||
if not conv_id:
|
||||
if not args.query:
|
||||
raise ScriptError('需要 --group 或 --query 参数')
|
||||
print(f'🔍 搜索群聊: {args.query}')
|
||||
conv_id = search_group(args.query, args.dry_run)
|
||||
|
||||
print(f'📥 拉取消息 (起始: {args.time})...')
|
||||
all_messages: List[Any] = []
|
||||
current_time = args.time
|
||||
page = 0
|
||||
max_pages = 50
|
||||
remaining = args.limit if args.limit > 0 else float('inf')
|
||||
print(f'📥 拉取消息 (起始: {args.time})...')
|
||||
all_messages: List[Any] = []
|
||||
seen_ids = set()
|
||||
current_time = args.time
|
||||
direction = 'older' if args.no_forward else 'newer'
|
||||
page = 0
|
||||
max_pages = 50
|
||||
remaining = args.limit if args.limit > 0 else float('inf')
|
||||
has_more = False
|
||||
|
||||
while page < max_pages and remaining > 0:
|
||||
cmd_args = [
|
||||
'chat', 'message', 'list',
|
||||
'--group', conv_id or '<CONV_ID>',
|
||||
'--time', current_time,
|
||||
'--format', 'json',
|
||||
]
|
||||
if args.no_forward:
|
||||
cmd_args.append('--forward=false')
|
||||
page_limit = min(int(remaining), 200) if args.limit > 0 else 0
|
||||
if page_limit > 0:
|
||||
cmd_args.extend(['--limit', str(page_limit)])
|
||||
data = run_dws(cmd_args, dry_run=args.dry_run)
|
||||
while page < max_pages and remaining > 0:
|
||||
cmd_args = [
|
||||
'chat', 'message', 'list',
|
||||
'--group', conv_id or '<CONV_ID>',
|
||||
'--time', current_time,
|
||||
'--direction', direction,
|
||||
'--format', 'json',
|
||||
]
|
||||
page_limit = min(int(remaining), 200) if args.limit > 0 else 0
|
||||
if page_limit > 0:
|
||||
cmd_args.extend(['--limit', str(page_limit)])
|
||||
data = run_dws(cmd_args, dry_run=args.dry_run)
|
||||
|
||||
if args.dry_run:
|
||||
print('[dry-run] 翻页循环: hasMore → 继续用边界 createTime 作为 --time')
|
||||
return
|
||||
if args.dry_run:
|
||||
print('[dry-run] 翻页循环: hasMore → 使用末条消息 createTime')
|
||||
return 0
|
||||
|
||||
if not data:
|
||||
break
|
||||
page_msgs, has_more = parse_message_page(data)
|
||||
if not page_msgs:
|
||||
if has_more:
|
||||
raise ScriptError('服务端返回 hasMore=true,但本页没有消息')
|
||||
break
|
||||
|
||||
# 兼容两种结构: 顶层 messages / {result: {messages, hasMore}}
|
||||
if isinstance(data, list):
|
||||
page_msgs = data
|
||||
has_more = False
|
||||
for message in page_msgs:
|
||||
if not isinstance(message, dict):
|
||||
raise ScriptError('消息列表包含非对象条目,无法安全导出')
|
||||
identity = message_identity(message)
|
||||
if identity and identity in seen_ids:
|
||||
continue
|
||||
if identity:
|
||||
seen_ids.add(identity)
|
||||
all_messages.append(message)
|
||||
remaining -= 1
|
||||
if remaining <= 0:
|
||||
break
|
||||
page += 1
|
||||
|
||||
if not has_more or remaining <= 0:
|
||||
break
|
||||
|
||||
last_msg = page_msgs[-1]
|
||||
if not isinstance(last_msg, dict):
|
||||
raise ScriptError('末条消息不是对象,无法取得翻页时间边界')
|
||||
boundary_time = normalize_boundary_time(
|
||||
last_msg.get('createTime')
|
||||
or last_msg.get('createAt')
|
||||
or last_msg.get('time')
|
||||
)
|
||||
if not boundary_time:
|
||||
raise ScriptError('hasMore=true,但末条消息缺少 createTime')
|
||||
if boundary_time == current_time:
|
||||
raise ScriptError('分页边界没有推进,已停止以避免重复循环')
|
||||
current_time = boundary_time
|
||||
print(f" 翻页 {page}: 已累计 {len(all_messages)} 条, 继续...")
|
||||
|
||||
if has_more and page >= max_pages and remaining > 0:
|
||||
raise ScriptError(f'达到最大分页数 {max_pages},结果不完整')
|
||||
|
||||
if not all_messages:
|
||||
print('未拉取到消息')
|
||||
return 0
|
||||
|
||||
if args.output:
|
||||
with open(args.output, 'w', encoding='utf-8') as file:
|
||||
json.dump(all_messages, file, ensure_ascii=False, indent=2)
|
||||
print(f" ✓ 已导出 {len(all_messages)} 条消息到 {args.output}")
|
||||
else:
|
||||
container = data
|
||||
inner = data.get('result')
|
||||
if isinstance(inner, dict):
|
||||
container = inner
|
||||
page_msgs = container.get('messages')
|
||||
if page_msgs is None and isinstance(inner, list):
|
||||
page_msgs = inner
|
||||
if not isinstance(page_msgs, list):
|
||||
page_msgs = []
|
||||
has_more = bool(container.get('hasMore', False))
|
||||
|
||||
if not page_msgs:
|
||||
break
|
||||
|
||||
all_messages.extend(page_msgs)
|
||||
remaining -= len(page_msgs)
|
||||
page += 1
|
||||
|
||||
if not has_more:
|
||||
break
|
||||
|
||||
last_msg = page_msgs[-1]
|
||||
boundary_time = (last_msg.get('createTime')
|
||||
or last_msg.get('createAt')
|
||||
or last_msg.get('time', ''))
|
||||
if not boundary_time or boundary_time == current_time:
|
||||
break
|
||||
current_time = boundary_time
|
||||
print(f" 翻页 {page}: 已累计 {len(all_messages)} 条, 继续...")
|
||||
|
||||
if not all_messages:
|
||||
print('未拉取到消息')
|
||||
return
|
||||
|
||||
if args.output:
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_messages, f, ensure_ascii=False, indent=2)
|
||||
print(f" ✓ 已导出 {len(all_messages)} 条消息到 {args.output}")
|
||||
else:
|
||||
for m in all_messages:
|
||||
# 实际字段: sender/createTime/content (兼容旧字段名)
|
||||
sender = (m.get('sender') or m.get('senderNick')
|
||||
or m.get('senderOpenDingTalkId', '未知'))
|
||||
text = m.get('content') or m.get('text', '')
|
||||
time_str = (m.get('createTime') or m.get('createAt')
|
||||
or m.get('time', ''))
|
||||
print(f" [{time_str}] {sender}: {text[:80]}")
|
||||
print(f"\n合计: {len(all_messages)} 条消息 ({page} 页)")
|
||||
for message in all_messages:
|
||||
sender = (
|
||||
message.get('sender')
|
||||
or message.get('senderNick')
|
||||
or '未知'
|
||||
)
|
||||
text = message.get('content') or message.get('text', '')
|
||||
time_str = (
|
||||
message.get('createTime')
|
||||
or message.get('createAt')
|
||||
or message.get('time', '')
|
||||
)
|
||||
print(f" [{time_str}] {sender}: {text[:80]}")
|
||||
print(f"\n合计: {len(all_messages)} 条消息 ({page} 页)")
|
||||
return 0
|
||||
except ScriptError as exc:
|
||||
print(f'错误:{exc}', file=sys.stderr)
|
||||
return exc.exit_code
|
||||
except OSError as exc:
|
||||
print(f'错误:无法写入输出文件:{exc}', file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
sys.exit(run())
|
||||
|
||||
@@ -3,23 +3,63 @@
|
||||
查询与某人的单聊聊天记录
|
||||
|
||||
用法:
|
||||
python chat_history_with_user.py --name "张三" --time "2026-03-10 00:00:00"
|
||||
python chat_history_with_user.py --user <userId> --time "2026-03-10 00:00:00" --limit 50
|
||||
python chat_history_with_user.py --name "张三" --time "2026-03-01 00:00:00" --output history.json
|
||||
python3 scripts/chat_history_with_user.py --name "张三" --time "2026-03-10 00:00:00"
|
||||
python3 scripts/chat_history_with_user.py --user <userId> --time "2026-03-10 00:00:00" --limit 50
|
||||
python3 scripts/chat_history_with_user.py --name "张三" --time "2026-03-01 00:00:00" --output history.json
|
||||
|
||||
工作流:
|
||||
1. 通过 --name 搜索通讯录,获取 userId(或直接传 --user)
|
||||
2. 调用 chat message list --user <userId> 拉取单聊消息
|
||||
2. 调用 chat message list-direct --user <userId> 拉取单聊消息
|
||||
3. 输出到终端或导出为 JSON 文件
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import datetime
|
||||
import subprocess
|
||||
import argparse
|
||||
from typing import List, Any, Optional
|
||||
|
||||
|
||||
class ScriptError(RuntimeError):
|
||||
"""可预期的脚本执行错误。"""
|
||||
|
||||
exit_code = 1
|
||||
|
||||
|
||||
class AmbiguousTargetError(ScriptError):
|
||||
"""搜索结果不唯一,需要调用方消歧。"""
|
||||
|
||||
exit_code = 2
|
||||
|
||||
|
||||
def normalize_boundary_time(value: Any) -> str:
|
||||
"""将消息时间边界规范为 CLI 接受的 yyyy-MM-dd HH:mm:ss 或原字符串。"""
|
||||
if value is None or isinstance(value, bool):
|
||||
return ''
|
||||
if isinstance(value, (int, float)):
|
||||
timestamp = float(value)
|
||||
elif isinstance(value, str):
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return ''
|
||||
try:
|
||||
timestamp = float(text)
|
||||
except ValueError:
|
||||
return text
|
||||
else:
|
||||
return str(value).strip()
|
||||
if timestamp > 10_000_000_000:
|
||||
timestamp /= 1000
|
||||
try:
|
||||
dt = datetime.datetime.fromtimestamp(
|
||||
timestamp, tz=datetime.timezone(datetime.timedelta(hours=8))
|
||||
)
|
||||
except (OSError, OverflowError, ValueError) as exc:
|
||||
raise ScriptError(f'无效的消息时间边界:{value}') from exc
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
@@ -32,14 +72,19 @@ def run_dws(
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=120
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f"错误:{e}", file=sys.stderr)
|
||||
return None
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
|
||||
raise ScriptError(f'执行 dws 失败:{exc}') from exc
|
||||
if result.returncode != 0:
|
||||
detail = result.stderr.strip() or f'退出码 {result.returncode}'
|
||||
raise ScriptError(f'dws 命令失败:{detail}')
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ScriptError(f'dws 返回的不是合法 JSON:{exc}') from exc
|
||||
if isinstance(data, dict) and data.get('success') is False:
|
||||
detail = data.get('errorMsg') or data.get('message') or '未知错误'
|
||||
raise ScriptError(f'dws 业务调用失败:{detail}')
|
||||
return data
|
||||
|
||||
|
||||
def search_user(
|
||||
@@ -52,30 +97,80 @@ def search_user(
|
||||
], dry_run=dry_run)
|
||||
if dry_run:
|
||||
return '<USER_ID>'
|
||||
if not data:
|
||||
return None
|
||||
# 解析返回结构
|
||||
users = data
|
||||
if isinstance(data, dict):
|
||||
inner = data.get('result', data)
|
||||
if isinstance(inner, dict):
|
||||
users = (inner.get('users', [])
|
||||
or inner.get('list', []))
|
||||
users = (
|
||||
inner.get('value')
|
||||
or inner.get('users')
|
||||
or inner.get('list')
|
||||
or inner.get('items')
|
||||
or []
|
||||
)
|
||||
elif isinstance(inner, list):
|
||||
users = inner
|
||||
else:
|
||||
users = []
|
||||
if not users or not isinstance(users, list):
|
||||
print(f"未找到用户: {name}")
|
||||
return None
|
||||
u = users[0]
|
||||
raise ScriptError(f'未找到用户:{name}')
|
||||
exact = [
|
||||
item for item in users
|
||||
if isinstance(item, dict)
|
||||
and str(item.get('name') or item.get('nick') or '').strip().casefold()
|
||||
== name.strip().casefold()
|
||||
]
|
||||
candidates = exact if exact else users
|
||||
if len(candidates) != 1:
|
||||
rendered = []
|
||||
for item in candidates:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
user_name = item.get('name') or item.get('nick') or '未知'
|
||||
user_id = item.get('userId') or item.get('userid') or '无ID'
|
||||
rendered.append(f'{user_name} ({user_id})')
|
||||
detail = ';'.join(rendered) or f'{len(candidates)} 个候选'
|
||||
raise AmbiguousTargetError(
|
||||
f'姓名“{name}”匹配到多个候选,请指定 --user:{detail}'
|
||||
)
|
||||
u = candidates[0]
|
||||
user_name = u.get('name') or u.get('nick', '未知')
|
||||
user_id = u.get('userId') or u.get('userid', '')
|
||||
if not user_id:
|
||||
raise ScriptError(f'用户“{user_name}”缺少 userId')
|
||||
print(f" 找到用户: {user_name} ({user_id})")
|
||||
return user_id
|
||||
|
||||
|
||||
def main():
|
||||
def parse_message_page(data: Any) -> tuple[List[Any], bool]:
|
||||
"""提取消息页和 hasMore。"""
|
||||
if isinstance(data, list):
|
||||
return data, False
|
||||
if not isinstance(data, dict):
|
||||
return [], False
|
||||
inner = data.get('result', data)
|
||||
if isinstance(inner, dict):
|
||||
messages = inner.get('messages', [])
|
||||
return messages if isinstance(messages, list) else [], bool(
|
||||
inner.get('hasMore', False)
|
||||
)
|
||||
if isinstance(inner, list):
|
||||
return inner, False
|
||||
return [], False
|
||||
|
||||
|
||||
def message_identity(message: Any) -> Optional[str]:
|
||||
if not isinstance(message, dict):
|
||||
return None
|
||||
value = (
|
||||
message.get('openMessageId')
|
||||
or message.get('openMsgId')
|
||||
or message.get('msgId')
|
||||
)
|
||||
return str(value) if value else None
|
||||
|
||||
|
||||
def run(argv: Optional[List[str]] = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description='查询与某人的单聊聊天记录'
|
||||
)
|
||||
@@ -96,100 +191,113 @@ def main():
|
||||
)
|
||||
parser.add_argument('--output', default='', help='导出到 JSON 文件')
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# 1. 获取 userId
|
||||
user_id = args.user
|
||||
if not user_id:
|
||||
print(f'🔍 搜索用户: {args.name}')
|
||||
user_id = search_user(args.name, args.dry_run)
|
||||
if not user_id and not args.dry_run:
|
||||
sys.exit(1)
|
||||
try:
|
||||
user_id = args.user
|
||||
if not user_id:
|
||||
print(f'🔍 搜索用户: {args.name}')
|
||||
user_id = search_user(args.name, args.dry_run)
|
||||
|
||||
# 2. 拉取单聊消息(自动翻页)
|
||||
print(f'📥 拉取与 {user_id} 的聊天记录 (起始: {args.time})...')
|
||||
all_messages: List[Any] = []
|
||||
current_time = args.time
|
||||
page = 0
|
||||
max_pages = 50
|
||||
remaining = args.limit if args.limit > 0 else float('inf')
|
||||
print(f'📥 拉取与 {user_id} 的聊天记录 (起始: {args.time})...')
|
||||
all_messages: List[Any] = []
|
||||
seen_ids = set()
|
||||
current_time = args.time
|
||||
direction = 'older' if args.no_forward else 'newer'
|
||||
page = 0
|
||||
max_pages = 50
|
||||
remaining = args.limit if args.limit > 0 else float('inf')
|
||||
has_more = False
|
||||
|
||||
while page < max_pages and remaining > 0:
|
||||
cmd_args = [
|
||||
'chat', 'message', 'list',
|
||||
'--user', user_id or '<USER_ID>',
|
||||
'--time', current_time,
|
||||
'--format', 'json',
|
||||
]
|
||||
if args.no_forward:
|
||||
cmd_args.append('--forward=false')
|
||||
page_limit = min(int(remaining), 200) if args.limit > 0 else 0
|
||||
if page_limit > 0:
|
||||
cmd_args.extend(['--limit', str(page_limit)])
|
||||
data = run_dws(cmd_args, dry_run=args.dry_run)
|
||||
while page < max_pages and remaining > 0:
|
||||
cmd_args = [
|
||||
'chat', 'message', 'list-direct',
|
||||
'--user', user_id or '<USER_ID>',
|
||||
'--time', current_time,
|
||||
'--direction', direction,
|
||||
'--format', 'json',
|
||||
]
|
||||
page_limit = min(int(remaining), 200) if args.limit > 0 else 0
|
||||
if page_limit > 0:
|
||||
cmd_args.extend(['--limit', str(page_limit)])
|
||||
data = run_dws(cmd_args, dry_run=args.dry_run)
|
||||
|
||||
if args.dry_run:
|
||||
print('[dry-run] 翻页循环: hasMore → 继续用边界 createTime 作为 --time')
|
||||
return
|
||||
if args.dry_run:
|
||||
print('[dry-run] 翻页循环: hasMore → 使用末条消息 createTime')
|
||||
return 0
|
||||
|
||||
if not data:
|
||||
break
|
||||
page_msgs, has_more = parse_message_page(data)
|
||||
if not page_msgs:
|
||||
if has_more:
|
||||
raise ScriptError('服务端返回 hasMore=true,但本页没有消息')
|
||||
break
|
||||
|
||||
# 兼容两种结构: 顶层 messages / {result: {messages, hasMore}}
|
||||
if isinstance(data, list):
|
||||
page_msgs = data
|
||||
has_more = False
|
||||
for message in page_msgs:
|
||||
if not isinstance(message, dict):
|
||||
raise ScriptError('消息列表包含非对象条目,无法安全导出')
|
||||
identity = message_identity(message)
|
||||
if identity and identity in seen_ids:
|
||||
continue
|
||||
if identity:
|
||||
seen_ids.add(identity)
|
||||
all_messages.append(message)
|
||||
remaining -= 1
|
||||
if remaining <= 0:
|
||||
break
|
||||
page += 1
|
||||
|
||||
if not has_more or remaining <= 0:
|
||||
break
|
||||
|
||||
last_msg = page_msgs[-1]
|
||||
if not isinstance(last_msg, dict):
|
||||
raise ScriptError('末条消息不是对象,无法取得翻页时间边界')
|
||||
boundary_time = normalize_boundary_time(
|
||||
last_msg.get('createTime')
|
||||
or last_msg.get('createAt')
|
||||
or last_msg.get('time')
|
||||
)
|
||||
if not boundary_time:
|
||||
raise ScriptError('hasMore=true,但末条消息缺少 createTime')
|
||||
if boundary_time == current_time:
|
||||
raise ScriptError('分页边界没有推进,已停止以避免重复循环')
|
||||
current_time = boundary_time
|
||||
print(f" 翻页 {page}: 已累计 {len(all_messages)} 条, 继续...")
|
||||
|
||||
if has_more and page >= max_pages and remaining > 0:
|
||||
raise ScriptError(f'达到最大分页数 {max_pages},结果不完整')
|
||||
|
||||
if not all_messages:
|
||||
print('未拉取到消息')
|
||||
return 0
|
||||
|
||||
if args.output:
|
||||
with open(args.output, 'w', encoding='utf-8') as file:
|
||||
json.dump(all_messages, file, ensure_ascii=False, indent=2)
|
||||
print(f" ✓ 已导出 {len(all_messages)} 条消息到 {args.output}")
|
||||
else:
|
||||
container = data
|
||||
inner = data.get('result')
|
||||
if isinstance(inner, dict):
|
||||
container = inner
|
||||
page_msgs = container.get('messages')
|
||||
if page_msgs is None and isinstance(inner, list):
|
||||
page_msgs = inner
|
||||
if not isinstance(page_msgs, list):
|
||||
page_msgs = []
|
||||
has_more = bool(container.get('hasMore', False))
|
||||
|
||||
if not page_msgs:
|
||||
break
|
||||
|
||||
all_messages.extend(page_msgs)
|
||||
remaining -= len(page_msgs)
|
||||
page += 1
|
||||
|
||||
if not has_more:
|
||||
break
|
||||
|
||||
last_msg = page_msgs[-1]
|
||||
boundary_time = (last_msg.get('createTime')
|
||||
or last_msg.get('createAt')
|
||||
or last_msg.get('time', ''))
|
||||
if not boundary_time or boundary_time == current_time:
|
||||
break
|
||||
current_time = boundary_time
|
||||
print(f" 翻页 {page}: 已累计 {len(all_messages)} 条, 继续...")
|
||||
|
||||
if not all_messages:
|
||||
print('未拉取到消息')
|
||||
return
|
||||
|
||||
# 3. 输出结果
|
||||
if args.output:
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_messages, f, ensure_ascii=False, indent=2)
|
||||
print(f" ✓ 已导出 {len(all_messages)} 条消息到 {args.output}")
|
||||
else:
|
||||
for m in all_messages:
|
||||
# 实际字段: sender/createTime/content (兼容旧字段名)
|
||||
sender = (m.get('sender') or m.get('senderNick')
|
||||
or m.get('senderOpenDingTalkId', '未知'))
|
||||
text = m.get('content') or m.get('text', '')
|
||||
time_str = (m.get('createTime') or m.get('createAt')
|
||||
or m.get('time', ''))
|
||||
print(f" [{time_str}] {sender}: {text[:80]}")
|
||||
print(f"\n合计: {len(all_messages)} 条消息 ({page} 页)")
|
||||
for message in all_messages:
|
||||
sender = (
|
||||
message.get('sender')
|
||||
or message.get('senderNick')
|
||||
or '未知'
|
||||
)
|
||||
text = message.get('content') or message.get('text', '')
|
||||
time_str = (
|
||||
message.get('createTime')
|
||||
or message.get('createAt')
|
||||
or message.get('time', '')
|
||||
)
|
||||
print(f" [{time_str}] {sender}: {text[:80]}")
|
||||
print(f"\n合计: {len(all_messages)} 条消息 ({page} 页)")
|
||||
return 0
|
||||
except ScriptError as exc:
|
||||
print(f'错误:{exc}', file=sys.stderr)
|
||||
return exc.exit_code
|
||||
except OSError as exc:
|
||||
print(f'错误:无法写入输出文件:{exc}', file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
sys.exit(run())
|
||||
|
||||
@@ -1,181 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
在指定目录创建文档并写入 Markdown 内容(一键完成)
|
||||
"""用原生 dws 写入管道创建文档,并回读验证。"""
|
||||
|
||||
用法:
|
||||
python doc_create_and_write.py \
|
||||
--name "项目周报" \
|
||||
--content "# 本周总结\n\n## 完成事项\n- 任务A"
|
||||
from __future__ import annotations
|
||||
|
||||
python doc_create_and_write.py \
|
||||
--name "会议纪要" \
|
||||
--content-file notes.md
|
||||
|
||||
python doc_create_and_write.py \
|
||||
--name "知识库文档" --content "# 内容" --folder FOLDER_ID
|
||||
|
||||
python doc_create_and_write.py --name "test" --content "hello" --dry-run
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import subprocess
|
||||
import argparse
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import List, Any, Optional
|
||||
from typing import Any, Optional, Sequence
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
cmd = ['dws'] + args
|
||||
class ScriptError(RuntimeError):
|
||||
"""可预期的脚本执行错误。"""
|
||||
|
||||
|
||||
def decode_json_output(output: str) -> Any:
|
||||
"""解析 JSON;兼容长内容写入前置的进度行。"""
|
||||
text = output.strip()
|
||||
if not text:
|
||||
raise ScriptError("dws 未返回 JSON")
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
decoder = json.JSONDecoder()
|
||||
for offset, character in enumerate(text):
|
||||
if character not in "[{":
|
||||
continue
|
||||
try:
|
||||
value, end = decoder.raw_decode(text, offset)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if not text[end:].strip():
|
||||
return value
|
||||
raise ScriptError("dws 返回的不是合法 JSON")
|
||||
|
||||
|
||||
def run_dws(args: Sequence[str], dry_run: bool = False) -> Any:
|
||||
"""执行一条 dws 命令,并把命令/业务失败统一转成 ScriptError。"""
|
||||
command = ["dws", *args]
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return {'dry_run': True}
|
||||
print(f"[dry-run] {shlex.join(command)}")
|
||||
return {"dry_run": True}
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=60
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" ✗ 错误:{result.stderr.strip()}")
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f" ✗ 错误:{e}")
|
||||
return None
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
|
||||
raise ScriptError(f"执行 dws 失败:{exc}") from exc
|
||||
if result.returncode != 0:
|
||||
detail = result.stderr.strip() or result.stdout.strip()
|
||||
raise ScriptError(
|
||||
f"dws 命令失败:{detail or f'退出码 {result.returncode}'}"
|
||||
)
|
||||
data = decode_json_output(result.stdout)
|
||||
if data is None or data == {}:
|
||||
raise ScriptError("dws 返回空业务结果,无法确认操作成功")
|
||||
if isinstance(data, dict) and data.get("success") is False:
|
||||
detail = data.get("errorMsg") or data.get("message") or "未知错误"
|
||||
raise ScriptError(f"dws 业务调用失败:{detail}")
|
||||
return data
|
||||
|
||||
|
||||
def run_dws_with_retry(
|
||||
args: List[str],
|
||||
dry_run: bool = False,
|
||||
max_retries: int = 3,
|
||||
retry_delay: float = 1.0,
|
||||
) -> Optional[Any]:
|
||||
"""带重试机制的 dws 命令执行"""
|
||||
last_error = None
|
||||
for attempt in range(1, max_retries + 1):
|
||||
result = run_dws(args, dry_run=dry_run)
|
||||
if result is not None:
|
||||
return result
|
||||
if attempt < max_retries:
|
||||
print(f" ⚠️ 第 {attempt} 次尝试失败,{retry_delay}秒后重试...")
|
||||
time.sleep(retry_delay)
|
||||
retry_delay *= 1.5 # 指数退避
|
||||
return None
|
||||
def first_value(payload: Any, keys: Sequence[str]) -> str:
|
||||
"""从嵌套响应中提取第一个非空稳定字段。"""
|
||||
if isinstance(payload, dict):
|
||||
for key in keys:
|
||||
value = payload.get(key)
|
||||
if value is not None and str(value).strip():
|
||||
return str(value).strip()
|
||||
for value in payload.values():
|
||||
found = first_value(value, keys)
|
||||
if found:
|
||||
return found
|
||||
elif isinstance(payload, list):
|
||||
for value in payload:
|
||||
found = first_value(value, keys)
|
||||
if found:
|
||||
return found
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
def run(argv: Optional[Sequence[str]] = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description='创建文档并写入内容'
|
||||
description="使用 dws doc create 创建文档并回读验证"
|
||||
)
|
||||
parser.add_argument('--name', required=True, help='文档名称')
|
||||
parser.add_argument('--content', default='', help='Markdown 内容')
|
||||
parser.add_argument('--content-file', default='', help='内容文件')
|
||||
parser.add_argument('--folder', default='', help='目标文件夹 ID 或 URL')
|
||||
parser.add_argument('--workspace', default='', help='目标知识库 ID')
|
||||
parser.add_argument(
|
||||
'--mode', default='append', choices=['overwrite', 'append'],
|
||||
help='写入模式: overwrite=覆盖, append=追加 (默认 append)',
|
||||
parser.add_argument("--name", required=True, help="文档名称")
|
||||
content_group = parser.add_mutually_exclusive_group(required=True)
|
||||
content_group.add_argument("--content", help="Markdown 内容")
|
||||
content_group.add_argument("--content-file", help="UTF-8 Markdown 文件")
|
||||
location_group = parser.add_mutually_exclusive_group()
|
||||
location_group.add_argument(
|
||||
"--folder", default="", help="目标文档文件夹 ID 或 URL"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--max-retries', type=int, default=3,
|
||||
help='每块写入失败时的最大重试次数 (默认 3)',
|
||||
location_group.add_argument(
|
||||
"--workspace", default="", help="目标知识库 ID 或 URL"
|
||||
)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
content = args.content
|
||||
supplied_path: Optional[Path] = None
|
||||
temporary_path: Optional[Path] = None
|
||||
if args.content_file:
|
||||
p = Path(args.content_file)
|
||||
if not p.exists():
|
||||
print(f"错误:文件不存在: {p}")
|
||||
sys.exit(1)
|
||||
content = p.read_text(encoding='utf-8')
|
||||
if not content:
|
||||
print('错误:需要 --content 或 --content-file')
|
||||
sys.exit(1)
|
||||
chunk_size = 30000
|
||||
supplied_path = Path(args.content_file)
|
||||
if not supplied_path.is_file():
|
||||
raise ScriptError(f"内容文件不存在:{supplied_path}")
|
||||
elif not args.content or not args.content.strip():
|
||||
raise ScriptError("--content 不能为空")
|
||||
|
||||
create_args = ['doc', 'create', '--name', args.name, '--format', 'json']
|
||||
if args.folder:
|
||||
create_args.extend(['--folder', args.folder])
|
||||
if args.workspace:
|
||||
create_args.extend(['--workspace', args.workspace])
|
||||
try:
|
||||
if supplied_path is None and not args.dry_run:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", encoding="utf-8", suffix=".md", delete=False
|
||||
) as handle:
|
||||
handle.write(args.content)
|
||||
temporary_path = Path(handle.name)
|
||||
supplied_path = temporary_path
|
||||
|
||||
print(f'\n📝 创建文档: {args.name}')
|
||||
create_data = run_dws(create_args, dry_run=args.dry_run)
|
||||
content_path = str(supplied_path) if supplied_path else "<TEMP_CONTENT.md>"
|
||||
create_args = [
|
||||
"doc", "create",
|
||||
"--name", args.name,
|
||||
"--content-file", content_path,
|
||||
"--content-format", "markdown",
|
||||
"--format", "json",
|
||||
]
|
||||
if args.folder:
|
||||
create_args.extend(["--folder", args.folder])
|
||||
if args.workspace:
|
||||
create_args.extend(["--workspace", args.workspace])
|
||||
|
||||
node_id = None
|
||||
if not args.dry_run:
|
||||
if not create_data:
|
||||
sys.exit(1)
|
||||
node_id = (create_data.get('nodeId')
|
||||
or create_data.get('dentryUuid')
|
||||
or create_data.get('id', ''))
|
||||
print(f" ✓ 文档已创建 (ID: {node_id})")
|
||||
created = run_dws(create_args, dry_run=args.dry_run)
|
||||
node_id = "<NODE_ID>" if args.dry_run else first_value(
|
||||
created, ("nodeId", "dentryUuid")
|
||||
)
|
||||
if not node_id:
|
||||
raise ScriptError("文档创建响应缺少 nodeId,无法验证")
|
||||
|
||||
if len(content) <= chunk_size:
|
||||
mode_label = '追加' if args.mode == 'append' else '覆盖'
|
||||
print(f'\n✍️ 写入内容 (模式: {mode_label}, {len(content)} 字符)...')
|
||||
write_data = run_dws([
|
||||
'doc', 'update',
|
||||
'--node', node_id or '<NODE_ID>',
|
||||
'--content', content,
|
||||
'--mode', args.mode,
|
||||
'--format', 'json',
|
||||
], dry_run=args.dry_run)
|
||||
if write_data:
|
||||
print(f" ✓ 内容已写入 ({len(content)} 字符)")
|
||||
else:
|
||||
chunks = []
|
||||
pos = 0
|
||||
while pos < len(content):
|
||||
end = min(pos + chunk_size, len(content))
|
||||
if end < len(content):
|
||||
newline_pos = content.rfind('\n', pos, end)
|
||||
if newline_pos > pos:
|
||||
end = newline_pos + 1
|
||||
chunks.append(content[pos:end])
|
||||
pos = end
|
||||
info = run_dws(
|
||||
["doc", "info", "--node", node_id, "--format", "json"],
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
readback = run_dws(
|
||||
["doc", "read", "--node", node_id, "--format", "json"],
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
if args.dry_run:
|
||||
return 0
|
||||
if not first_value(readback, ("markdown", "jsonml", "content")):
|
||||
raise ScriptError("文档回读未返回正文,无法确认写入成功")
|
||||
|
||||
total_chunks = len(chunks)
|
||||
print(f'\n✍️ 内容较长 ({len(content)} 字符), 分 {total_chunks} 块写入...')
|
||||
|
||||
success_chunks = 0
|
||||
for idx, chunk in enumerate(chunks):
|
||||
chunk_mode = args.mode if idx == 0 else 'append'
|
||||
write_data = run_dws_with_retry(
|
||||
[
|
||||
'doc', 'update',
|
||||
'--node', node_id or '<NODE_ID>',
|
||||
'--content', chunk,
|
||||
'--mode', chunk_mode,
|
||||
'--format', 'json',
|
||||
],
|
||||
dry_run=args.dry_run,
|
||||
max_retries=args.max_retries,
|
||||
)
|
||||
if write_data:
|
||||
print(f" ✓ 块 {idx + 1}/{total_chunks} 已写入 ({len(chunk)} 字符)")
|
||||
success_chunks += 1
|
||||
elif not args.dry_run:
|
||||
# 写入失败,报告部分写入状态
|
||||
print(f"\n❌ 块 {idx + 1}/{total_chunks} 写入失败(已重试 {args.max_retries} 次)")
|
||||
print(f"\n⚠️ 文档处于部分写入状态:")
|
||||
print(f" - 文档 ID: {node_id}")
|
||||
print(f" - 已写入: {success_chunks}/{total_chunks} 块")
|
||||
print(f" - 失败位置: 第 {idx + 1} 块")
|
||||
if args.mode == 'overwrite':
|
||||
print(f" - 模式: 覆盖模式,文档可能包含不完整内容")
|
||||
print(f" - 建议: 手动检查文档内容,或删除后重新创建")
|
||||
else:
|
||||
print(f" - 模式: 追加模式,已写入内容已保存")
|
||||
print(f" - 建议: 可手动补充剩余内容,或重新运行脚本")
|
||||
sys.exit(1)
|
||||
print('\n✅ 完成!')
|
||||
summary = {
|
||||
"success": True,
|
||||
"nodeId": node_id,
|
||||
"docUrl": first_value(info, ("docUrl", "documentUrl", "url"))
|
||||
or first_value(created, ("docUrl", "documentUrl", "url")),
|
||||
"chunksWritten": first_value(created, ("chunksWritten",)),
|
||||
"verified": True,
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False))
|
||||
return 0
|
||||
finally:
|
||||
if temporary_path is not None:
|
||||
temporary_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def main() -> None:
|
||||
try:
|
||||
raise SystemExit(run())
|
||||
except ScriptError as exc:
|
||||
print(f"错误:{exc}", file=sys.stderr)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: dingtalk-aitable
|
||||
description: 钉钉 AI 表格(多维表)。Use when 用户说 AI表格/多维表/数据表/base/table/建表/查记录/写数据/字段/记录增删改查/筛选/排序/公式/模板搜索/批量导入CSV或JSON/导出/仪表盘/图表/上传附件到表格/按字段类型建表。不做电子表格单元格读写(走 dingtalk-misc)、文档编辑(走 dingtalk-doc);听记待办入表先用 dingtalk-minutes 提取,再由本 skill 写入。命令前缀:dws aitable。
|
||||
description: 钉钉 AI 表格(多维表)。Use when 用户说 AI表格/多维表/Base/数据表/字段/记录增删改查/筛选排序/公式与跨表引用/视图表单/仪表盘图表/高级权限/自动化工作流/模板/CSV或JSON批量导入/Excel导入导出/记录附件。不做电子表格单元格读写与工作表公式(走 dingtalk-sheet)、普通文档编辑(走 dingtalk-doc)或钉盘文件管理(走 dingtalk-drive);听记待办入表先用 dingtalk-minutes 提取,再由本 skill 写入。命令前缀:dws aitable。
|
||||
metadata:
|
||||
cli_version: ">=0.2.14"
|
||||
category: product
|
||||
@@ -15,146 +15,90 @@ metadata:
|
||||
|
||||
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dws-shared`](../dws-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
|
||||
|
||||
> 命令参考:[aitable.md](references/aitable.md);复杂命令按需加载 `references/aitable/*.md`;剧本:[06-data-analytics.md](references/06-data-analytics.md)。
|
||||
## 加载与路由顺序
|
||||
|
||||
1. 命中下方高频意图时直接使用精确骨架,不先查 Help 或产品级 Schema。
|
||||
2. 路由优先级固定为:精确 recipe / 可运行脚本 > 匹配的公开 Shortcut > 原子命令。命令已确定且参数清楚时直接执行。
|
||||
3. 参数、约束或安全语义不确定时只读 leaf Schema:`dws schema --cli-path "aitable <leaf>" --format json`;只有当前 Cobra flag 不确定时才读对应 `--help`。
|
||||
4. 复杂字段、筛选、视图、权限或工作流任务按“低频能力与 Reference”只加载相关文件。所有下表的 `references/aitable/...` 都是相对本 Skill 根目录的完整精确路径;不得省略中间的 `aitable/`。dashboard/chart、导入、导出、批量字段和附件统一只读 `references/aitable/aitable-script-recipes.md`,再运行 `scripts/aitable_ops.py`;不要预读整个 reference 目录或任何脚本源码。
|
||||
5. 现有骨架和 reference 都无法定位能力时,才用 Runtime Shortcut Catalog 做最后发现;不得猜 `cli_path` 或 flag。
|
||||
6. Schema、Help、reference 与实际返回冲突时采用更安全的解释并报告契约漂移;`confirmation=user_required` 时先确认,再添加 `--yes`。
|
||||
7. 用户已给足名称、字段、数据和目标时,直接按依赖链完成全部步骤;不要调用 todo 工具、分步汇报或追问已明确的信息。中间返回只用于提取下一步 ID 和判断失败,完成所有请求后再统一回读并答复。bundled script 参数明确时直接运行;只有参数不明确时执行统一入口的操作级 `--help`,只有契约失败、环境异常或用户要求修改脚本时才读取源码。
|
||||
8. 用户要求新建 Base 但未指定 Base 名时,根据业务目标生成简短描述性名称(例如仪表盘任务用“数据看板”)并继续;不要仅为可回退的容器名称追问。Base 只接受 Base flags,不得把 table `--fields` 传给 `base create`。
|
||||
|
||||
<!-- VISIBLE_SHORTCUTS_START -->
|
||||
## Shortcuts(无专用脚本/recipe 时优先)
|
||||
## Shortcut 发现(按需)
|
||||
|
||||
以下 shortcut 同时进入公开 catalog 与 Runtime Schema。先按本 skill 的意图表、脚本和 recipe 路由:存在精确覆盖该场景的专用脚本/recipe 时按其执行;否则用户意图命中时,shortcut 优先于手写原子命令。命令已选中时直接执行;只在参数或安全语义不确定时读取 leaf Schema(例如 `dws schema --cli-path "aitable +<shortcut>" --format json`),在当前 Cobra flags 不确定时读取 `dws aitable <shortcut> --help`。仅当现有路由和 reference 都无法定位低频能力时,才用 `dws shortcut list --service aitable --format json` 批量发现。
|
||||
`aitable` 当前有 29 条公开 shortcut。完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation 的子集可通过 leaf Schema 查询。高频产品根 Skill 不重复展开完整清单。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
|
||||
|
||||
| Shortcut | 风险 | 适用场景 |
|
||||
|---|---|---|
|
||||
| `dws aitable +base-get` | read | 获取指定 Base 的目录信息(tables / dashboards summary) |
|
||||
| `dws aitable +base-list` | read | 获取当前用户可访问的 AI 表格 Base 列表(最近访问,支持游标分页) |
|
||||
| `dws aitable +base-search` | read | 按名称关键词搜索 AI 表格 Base |
|
||||
| `dws aitable +chart-get` | read | 获取指定 chart 的详细信息 |
|
||||
| `dws aitable +chart-widgets-example` | read | 获取所有图表类型的 widget config 示例 |
|
||||
| `dws aitable +dashboard-config-example` | read | 获取 dashboard config 的结构示例 |
|
||||
| `dws aitable +dashboard-get` | read | 获取指定 dashboard 的详细信息(含 charts summary) |
|
||||
| `dws aitable +field-get` | read | 批量获取字段详情(含类型相关完整配置) |
|
||||
| `dws aitable +find-record` | read | 在指定多维表里按关键词查记录(只读) |
|
||||
| `dws aitable +form-field-list` | read | 列出表单视图当前可见的字段及其配置 |
|
||||
| `dws aitable +form-list` | read | 列出指定数据表下的所有表单视图 |
|
||||
| `dws aitable +form-share-get` | read | 读取视图当前的分享表单配置 |
|
||||
| `dws aitable +list-tables` | read | 列出某个多维表(base)里的所有数据表(只读,投影 tableId/tableName) |
|
||||
| `dws aitable +record-history-list` | read | 按 recordId 查询单条记录的变更历史 |
|
||||
| `dws aitable +record-query` | read | 查询表格记录(按 ID 取 / 条件筛选 / 关键词 / 分页) |
|
||||
| `dws aitable +record-query-empty` | read | 扫描并过滤出完全没填用户字段的空行 |
|
||||
| `dws aitable +record-share-links` | read | 批量(可 >20 条)获取多维表记录分享链接:去重+分片+合并 |
|
||||
| `dws aitable +record-share-url` | read | 按 recordId 批量获取记录分享链接,单次最多 20 条 |
|
||||
| `dws aitable +resolve-base` | read | 按名称搜索多维表 Base 并解析出唯一 baseId(只读) |
|
||||
| `dws aitable +resolve-table` | read | 在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读) |
|
||||
| `dws aitable +role-list` | read | 列出指定 Base 下的全部角色 |
|
||||
| `dws aitable +section-list-empty` | read | 列出指定 Base 下所有没有子节点的空文件夹 |
|
||||
| `dws aitable +section-list-nodes` | read | 列出指定 Base 当前版本下的全部 nsheet 节点 |
|
||||
| `dws aitable +table-get` | read | 批量获取指定数据表的表级信息、字段目录与视图目录 |
|
||||
| `dws aitable +template-search` | read | 按名称关键词搜索 AI 表格模板 |
|
||||
| `dws aitable +view-get` | read | 获取视图完整信息(列顺序、筛选、排序、分组等) |
|
||||
| `dws aitable +view-get-frozen-cols` | read | 获取视图当前冻结的左侧列数 |
|
||||
| `dws aitable +view-get-lock` | read | 获取视图锁定状态 |
|
||||
| `dws aitable +view-get-row-height` | read | 获取视图单元格行高(像素) |
|
||||
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service aitable --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
|
||||
<!-- VISIBLE_SHORTCUTS_END -->
|
||||
|
||||
## 意图表
|
||||
## 核心对象与 ID
|
||||
|
||||
| 用户说 | 命令 |
|
||||
|--------|------|
|
||||
| "搜表格 / 找一个 base" | `dws aitable base search --query "<名>"` |
|
||||
| "创建 AI 表格 / 多维表" | `dws aitable base create --name "<名称>" [--template-id <id>]` |
|
||||
| "查数据表 / 建数据表" | `dws aitable table get --base-id <baseId>` / `dws aitable table create --base-id <baseId> --name "<表名>" --fields '[...]'` |
|
||||
| "查字段 / 字段类型" | `dws aitable field get --base-id <id> --table-id <id>` |
|
||||
| "查记录 / 搜索记录" | `dws aitable record query --base-id <baseId> --table-id <tableId> [--filters '...']` |
|
||||
| "写记录 / 更新记录 / 删除记录" | `dws aitable record create/update/delete --base-id <baseId> --table-id <tableId> ...` |
|
||||
| "筛选 / 排序 / 公式 / 跨表引用" | 先读 `references/aitable/aitable-filter-sort.md` / `aitable-formula-guide.md` |
|
||||
| "批量导入 JSON / CSV" | `python scripts/import_records.py <baseId> <tableId> data.csv\|data.json` |
|
||||
| "批量加字段" | `python scripts/bulk_add_fields.py --base-id <id> --table-id <id> --fields fields.json` |
|
||||
| "导入 / 导出表格" | 先读 `references/aitable/aitable-export-import.md`;导出优先 `python scripts/aitable_export_via_task.py <baseId> --scope table --table-id <tableId>` |
|
||||
| "仪表盘 / 图表" | 先读 `references/aitable/aitable-dashboard-chart.md` |
|
||||
| "上传附件到记录" | 先读 `references/aitable/aitable-attachment.md`;可用 `python scripts/upload_attachment.py --base-id <id> --file <path>` |
|
||||
| 对象 | 标识与执行边界 |
|
||||
|---|---|
|
||||
| Base | `baseId` 标识一个 AI 表格文件;名称只用于搜索或消歧,不能当 ID |
|
||||
| Table | `tableId` 标识 Base 内的数据表;必须来自 `+resolve-table` / `+table-get` / 创建返回 |
|
||||
| Field | `fieldId` 标识列;写入、筛选、排序和字段变更优先使用真实 `fieldId` |
|
||||
| Record | `recordId` 标识行;更新、删除和分享前必须先查询得到真实 ID |
|
||||
| View / Dashboard / Chart | `viewId` / `dashboardId` / `chartId` 各自绑定当前 Base/Table,不跨对象复用 |
|
||||
| 异步任务 | `taskId` / `importId` 只用于对应导出或导入任务,不能替代业务对象 ID |
|
||||
|
||||
## 标准 SOP(必遵流程)
|
||||
所有下游 ID 都从当前链路的结构化返回中提取;同名多候选必须让用户消歧,不默认取第一项,也不复用未经本轮校验的旧 ID。
|
||||
|
||||
> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 flag/ID。每条命令必须带 `--format json`,执行后必须按"解析"步取真实字段,不得凭返回结构猜测。`baseId`/`tableId`/`fieldId`/`recordId` 一律先查后用,**禁止默认/编造**。
|
||||
## 核心意图与执行骨架
|
||||
|
||||
### SOP-1 定位 Base 与 Table(list / search → table get)
|
||||
| 用户意图 | 首选骨架 | 必须保留的执行边界 |
|
||||
|---|---|---|
|
||||
| 按名称找 Base | `dws aitable +resolve-base --name "<名称>" --format json` | 唯一命中才继续;多候选停止并消歧 |
|
||||
| 浏览最近访问 | `dws aitable +base-list --format json` | 只代表最近访问,不得宣称全量 |
|
||||
| 搜索模板 | `dws aitable +template-search --query "<关键词>" --format json` | 关键词参数是 `--query`,只返回真实候选,不擅自创建 Base |
|
||||
| 按名称找 Table | `dws aitable +resolve-table --base <baseId> --name "<表名>" --format json` | `baseId` 必须来自上一步真实返回 |
|
||||
| 取表、字段与视图目录 | `dws aitable +table-get --base-id <baseId> [--table-ids <tableId>] --format json` | `tables[].fields[]` 是字段目录;完整类型/config 再用 `+field-get` |
|
||||
| 取字段完整配置 | `dws aitable +field-get --base-id <baseId> --table-id <tableId> [--field-ids <ids>] --format json` | 写入前核对类型、只读性和 select options;按需展开以控制返回体 |
|
||||
| 查/搜/筛记录 | `dws aitable +record-query --base-id <baseId> --table-id <tableId> [--query <词>\|--filters '<JSON>'\|--record-ids <ids>] --format json` | ID 模式忽略 filter/sort;全量结论必须完整分页 |
|
||||
| 新增记录 | `dws aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":<值>}}]' --format json` | 单次最多 100;取 `data.newRecordIds[]` 后立即按 ID 回读 |
|
||||
| 更新记录 | `dws aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<id>","cells":{"<fieldId>":<值>}}]' --format json` | 先 query 拿 recordId;只传需改字段;取 `data.recordIds[]` 后回读 |
|
||||
| 删除记录 | 先 `dws aitable +record-query ...` 定位,再 `dws aitable record delete --base-id <baseId> --table-id <tableId> --record-ids <ids>` | 展示目标与影响,得到明确确认后才加 `--yes` |
|
||||
| 创建 Base / Table | `dws aitable base create --name "<名>" --format json` → `dws aitable base get --base-id <baseId> --format json`;`dws aitable table create --base-id <id> --name "<名>" --fields '[...]' --format json` → `dws aitable +table-get --base-id <id> --table-ids <tableId> --format json` | 使用创建返回的真实 ID 立即回读;创建字段时回读 `fields[]` 的名称、类型与 config;系统改名/加后缀时不得继续猜原名 |
|
||||
| 创建仪表盘 / 常用图表 | `python3 <本 Skill 绝对目录>/scripts/aitable_ops.py dashboard <baseId> "<仪表盘名>" [--chart-specs <workspace内JSON>]` | 唯一首选;完整参数与 ledger 契约只读 `references/aitable/aitable-script-recipes.md` |
|
||||
| 复制视图 | `dws aitable view duplicate --base-id <baseId> --table-id <tableId> --view-id <源viewId> --new-name "<新名称>" --format json` | 源 viewId 来自当前表的真实返回;不要复制数据表或创建仪表盘替代 |
|
||||
| 导入 / 导出 / 批量字段 / 附件 | 先读 `references/aitable/aitable-script-recipes.md`,再运行其中唯一的 `scripts/aitable_ops.py <operation> ...` | 不直接选择底层脚本;不读源码;保留统一入口返回的退出状态与 ledger |
|
||||
|
||||
**触发**:找/打开某张 AI 表格、不知 baseId 或 tableId。
|
||||
导出完成以统一 ledger 中的真实 `taskId`、`polledTimes`、`status=success`、`fileSize>0` 和 `savedPath` 为证据;不要自己重新轮询、读脚本源码,也不要只用 `ls` 替代异步任务证据。字段类型使用 Runtime camelCase,例如 `singleSelect` / `multipleSelect`;select 写值优先传选项名字符串或 `{id,name}`,不传 `{value:...}`。
|
||||
|
||||
1. **选源(必须)**:有名称/关键词 → `dws aitable base search --query "<名称>"`;列最近访问 → `dws aitable base list`。`base list` 仅返回最近访问,不是全部,**禁止**当作全量清单。
|
||||
2. **执行(必须)**:`dws aitable base search --query "<完整名>" --format json`(或 `dws aitable base list --format json`)。
|
||||
3. **解析(必须)**:从 JSON 取真实 `baseId`;**多候选必须输出让用户选,禁止默认取第一个**。
|
||||
4. **取 tableId(必须)**:`dws aitable table get --base-id <baseId> --format json` → 从 `data.tables[].tableId` 取目标表 ID,并记录 `views[]`。枚举模式不返回 `fields[]`;需要字段目录时必须继续执行 SOP-2 的 `field get`。若只核对某张表,可显式加 `--table-ids <tableId>` 控制返回体。
|
||||
5. **失败(必须)**:`base list` 为空或不命中 → 换 `base search --query` 关键词重试一次;仍无果**必须如实告知**,禁止臆造 baseId/tableId。
|
||||
## 记录读写不变量
|
||||
|
||||
**禁止**:跳过 `table get` 直接用字段名写记录、用模糊名匹配当 baseId、用旧会话里的 ID 不再校验。
|
||||
- `record create/update` 前必须获取目标字段的 `fieldId`、`type` 与 `config`;`filterUp`、`lookup` 等只读字段不可写。完整格式只在需要时读精确路径 `references/aitable/aitable-cell-value.md`。
|
||||
- 筛选和排序字段使用 `fieldId`;`--filters` 最外层是 `and|or + operands`,`--sort` 使用 `direction: asc|desc`。日期和跨表字段规则按需读精确路径 `references/aitable/aitable-filter-sort.md`。
|
||||
- `record query --all` 仍受 `--page-limit` 约束;分页中断或局部富化失败时保留已有结果,输出 completeness 与逐项失败 ledger,不把部分结果描述为全量。
|
||||
- 创建、更新、导入、批量建字段等写操作必须检查业务 `status`、逐项结果与返回 ID;普通写入按用户明确要求执行后回读,不能只凭退出码宣称成功。
|
||||
- 长 JSON 使用 `--records-file` / 任务文件;不得为绕过字段错误而静默丢列、改类型或删除失败项。
|
||||
|
||||
### SOP-2 拿字段定义(field get,写记录/改字段前置)
|
||||
## 低频能力与 Reference
|
||||
|
||||
**触发**:建/改/写记录、改字段名或 options、按字段类型拼写入参前。
|
||||
| 场景 | 按需读取 |
|
||||
|---|---|
|
||||
| 完整命令索引、对象 URL 与一级路由 | `references/aitable.md` |
|
||||
| 记录 query/create/update/delete | `references/aitable/aitable-record-query.md`、`references/aitable/aitable-record-create.md`、`references/aitable/aitable-record-update.md`、`references/aitable/aitable-record-delete.md` 中只读与当前动词一致的一份 |
|
||||
| 记录 upsert/history/share | `references/aitable/aitable-record-upsert.md`、`references/aitable/aitable-record-history.md`、`references/aitable/aitable-record-share.md` 中只读与当前动词一致的一份 |
|
||||
| 字段创建、字段 config、cellValue、公式与跨表引用 | `references/aitable/aitable-field.md`、`references/aitable/aitable-field-properties.md`、`references/aitable/aitable-cell-value.md`、`references/aitable/aitable-formula-guide.md` |
|
||||
| 筛选、排序、统计、全量分析 | `references/aitable/aitable-filter-sort.md`、`references/aitable/aitable-data-analysis-sop.md` |
|
||||
| dashboard/chart、导入导出、批量字段、附件脚本 | `references/aitable/aitable-script-recipes.md`(精确路径;只读这一份脚本契约) |
|
||||
| 视图、表单及高级 dashboard/chart 原子回退 | `references/aitable/aitable-view-config.md`、`references/aitable/aitable-view-extras.md`、`references/aitable/aitable-form.md`、`references/aitable/aitable-dashboard-chart.md` |
|
||||
| 高级权限、自动化工作流、导航节点 | `references/aitable/aitable-advperm.md`、`references/aitable/aitable-workflow.md`、`references/aitable.md` 的 section 路由 |
|
||||
|
||||
1. **前置(必须)**:先按 SOP-1 拿到 `baseId` + `tableId`。
|
||||
2. **执行(必须)**:`dws aitable field get --base-id <baseId> --table-id <tableId> --format json`(仅展开需要的字段时加 `--field-ids fld1,fld2`,单次最多 10 个)。
|
||||
3. **解析(必须)**:取每个目标字段的 `fieldId`、`type`、`config`(如 singleSelect/multipleSelect 的 `options[].id|name`);写入 cells 的 key **必须用 `fieldId`**,不是字段中文名;select 字段过滤/写入传**选项名称字面量**,不传 option ID。
|
||||
4. **衔接(必须)**:拿到字段定义 → 进入 SOP-3 写记录、或 `dws aitable field update --field-id <fieldId> --name <新名>|--config <JSON> --format json` 改字段。
|
||||
5. **失败(必须)**:字段不存在或类型不符 → 重新 `field get` 核对,**禁止**凭旧名称/旧类型继续写入。
|
||||
## 错误恢复
|
||||
|
||||
**禁止**:用字段中文名当 cells key、跳过 `field get` 直接 `record create/update`、对 select 字段传 option ID 当写入值。
|
||||
|
||||
### SOP-3 写/批量写记录(record create)
|
||||
|
||||
**触发**:新增记录、批量加数据、CSV/JSON 入表。
|
||||
|
||||
1. **前置(必须)**:SOP-1 取 `baseId`/`tableId` + SOP-2 取 `fieldId`/类型。
|
||||
2. **执行(必须)**:`dws aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":<值>}}]' --format json`;单次最多 100 条,超长用 `--records-file ./data.json`。
|
||||
3. **写入格式(必须)**:按 `record create --help` 类型表严格传值(text→字符串、number→数值、singleSelect→"选项名"、date→RFC3339、url→`{"text","link"}`、group→`{"cid"}` 等);`filterUp`/`lookup` 字段只读不可写。
|
||||
4. **解析与验证(必须)**:从返回 `data.newRecordIds[]` 取全部新记录 ID;不要读取不存在的标量 `recordId`。立即执行 `dws aitable record query --base-id <baseId> --table-id <tableId> --record-ids <id1,id2,...> --format json` 回读写入值。
|
||||
5. **失败(必须)**:类型/格式错误按返回报错修正后重试,**禁止**降级丢弃字段;不确定格式先 `field get` 复核 config。
|
||||
|
||||
**禁止**:编造 fieldId/recordId、跳过 `field get` 凭中文名写、把 URL 字符串直接塞给 url 字段。
|
||||
|
||||
### SOP-4 查/筛/排记录(record query)
|
||||
|
||||
**触发**:查记录、按条件筛选、排序、取关联记录、定位待改/待删的 recordId。
|
||||
|
||||
1. **前置(必须)**:SOP-1 拿 `baseId`/`tableId`。
|
||||
2. **执行(必须)**:`dws aitable record query --base-id <baseId> --table-id <tableId> --format json`;已知 ID 直取加 `--record-ids rec1,rec2`(忽略 filters/sort,单次≤100)。
|
||||
3. **筛选/排序(必须)**:`--filters` 最外层必须 `{"operator":"and|or","operands":[...]}`,select 字段值传**选项名字面量**;日期只能用 `date_eq/before/after/not_before/not_after`,范围用 `not_before`+`not_after` 组合,**禁止** `eq`/区间/相对时间。`--sort` 用 `[{"fieldId":"..","direction":"asc|desc"}]`(**必须用 `direction`**)。公式/引用/关联字段默认不返回,需显式 `--field-ids` 指定。
|
||||
4. **解析(必须)**:取真实 `recordId` 与字段值;分页用 `--cursor`,全表用 `--all --page-limit N`。
|
||||
5. **衔接(必须)**:拿到 recordId → SOP-5 更新、`record delete --record-ids --yes` 删除(删前确认)。
|
||||
|
||||
**禁止**:用字段名做 filter/sort key、对日期用 `eq`、漏掉 `direction` 用旧 `order` 字段、用本地过滤替代服务端 filter。
|
||||
|
||||
### SOP-5 更新记录(record update)
|
||||
|
||||
**触发**:改记录字段值、批量更新状态、单字段重命名需求之外的记录改动。
|
||||
|
||||
1. **前置(必须)**:SOP-1 拿 `baseId`/`tableId`;SOP-2 拿字段类型;SOP-4 拿目标 `recordId`。
|
||||
2. **执行(必须)**:`dws aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"recXXX","cells":{"<fieldId>":<新值>}}]' --format json`(每条必含 `recordId`+`cells`,单次≤100;超长用 `--records-file`);只传需改字段,未传保持原值。
|
||||
3. **解析与验证(必须)**:写入格式同 SOP-3;从返回 `data.recordIds[]` 取实际更新的记录 ID。更新响应不返回“受影响字段”,必须立即用 `record query --record-ids <id1,id2,...> --format json` 回读目标字段确认。
|
||||
4. **失败(必须)**:recordId 不存在或类型不符 → 回 SOP-4 重新定位,**禁止**编造 ID 强写。
|
||||
|
||||
**禁止**:省略 `recordId`、用字段中文名当 cells key、凭空猜测 recordId 直接 update。
|
||||
|
||||
## 危险操作
|
||||
|
||||
`base delete` / `table delete` / `field delete` / `record delete` 不可逆,必须先向用户确认再加 `--yes`。
|
||||
|
||||
## 高频硬约束
|
||||
|
||||
- 创建/改字段/写记录是多轮连续任务时,不能在"让我执行/先获取 ID"后停下;必须实际调用对应 `dws aitable` 命令并验证结果。
|
||||
- 字段重命名使用 `dws aitable field update --base-id <baseId> --table-id <tableId> --field-id <fieldId> --name "<新名称>" --format json`;先 `field get` 找真实 `fieldId`,不要猜字段名能直接更新。
|
||||
- 写记录前必须 `field get` 获取 `fieldId` 与类型;`record create/update` 的 `cells` key 用 `fieldId`,不是字段中文名。长 JSON 使用 `--records-file`。
|
||||
- 表或字段创建返回名称被系统自动加后缀时,后续必须使用返回的真实 `tableId`/`fieldId`,不要继续按原名称猜。
|
||||
- `record update/delete` 先 `record query/list` 定位 `recordId`;删除必须确认,普通新增/更新按用户明确要求可直接执行后读回验证。
|
||||
- `record query/create/update/delete`、`field create`、导入导出、图表和附件场景必须先读对应 `references/aitable/*.md`,不要凭旧单文件参数猜 flag。
|
||||
|
||||
## 字段类型规则
|
||||
|
||||
详见本 skill 的 [field-rules.md](references/field-rules.md)。
|
||||
- 路径或 flag 错误:按既定的 leaf Schema → leaf Help 顺序校正一次;仍失败则停止,不连续尝试猜测别名。
|
||||
- 命令非零、输出非 JSON、业务 `status != success`、必需 ID 缺失、批处理部分失败均视为失败;保留成功项与 ledger,禁止吞错。
|
||||
- 同名歧义、权限不足、资源不存在、字段类型漂移、分页无法推进或 Schema/Help 冲突时停止并报告。具体恢复动作按需读精确路径 `references/aitable/aitable-error-recovery.md`。
|
||||
- 每次重试都从最新实际输出重新提取下游 ID;删除和其他 `confirmation=user_required` 操作不得自动重试或静默确认。
|
||||
|
||||
## 跨产品协作
|
||||
|
||||
- 单元格 / 工作表 / 公式 → 切到 `dingtalk-misc`(`references/sheet.md`,命令前缀:`dws sheet`)
|
||||
## 局部意图
|
||||
|
||||
- [局部意图消歧](references/intent-guide.md)。
|
||||
- 电子表格工作表、单元格与公式 → `dingtalk-sheet`;结构化 Base/Table/Field/Record 才走本 skill。
|
||||
- 普通文档内容 → `dingtalk-doc`;钉盘普通文件与文件夹 → `dingtalk-drive`;记录附件上传仍走本 skill。
|
||||
- 用户直接提供类型不明的 alidocs URL 时,按 `dws-shared` 的 URL 预检导航确认 `extension=able` 后再执行。
|
||||
- 听记内容入表:先用 `dingtalk-minutes` 提取结构化结果,再按本 skill 的字段与记录规则写入。
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
|--------|-------------------|
|
||||
| read-aitable | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable field get --base-id <baseId> --table-id <tableId>` → 取 `fieldId`<br>3. `aitable record query --base-id <baseId> --table-id <tableId>` → 取记录(分页)<br> 需要筛选时 `--filters` 格式见 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md),根节点必须是 `{"operator":"and\|or","operands":[...]}`<br>4. 总结数据 |
|
||||
| generate-data-report | 1. 同 read-aitable 步骤 1-3<br>2. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行 → 补充背景<br>3. `doc create --name "<报告名>" --content "<分析报告>"` |
|
||||
| create-aitable-record | **批量导入优先**:`python scripts/import_records.py <baseId> <tableId> data.csv\|data.json [batch_size]`(自动分批创建)<br>单条/少量:1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable field get --base-id <baseId> --table-id <tableId>` → 取 `fieldId` 与类型<br>3. `aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":"值"}}]'` |
|
||||
| create-aitable-record | **批量导入优先**:`python3 <本 Skill 绝对目录>/scripts/aitable_ops.py import-records <baseId> <tableId> data.csv\|data.json [--batch-size N]`<br>单条/少量:1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable field get --base-id <baseId> --table-id <tableId>` → 取 `fieldId` 与类型<br>3. `aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":"值"}}]'` |
|
||||
| update-aitable-record | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable record query --base-id <baseId> --table-id <tableId>` → 取 `recordId`,**先展示让用户确认**<br>3. `aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<recordId>","cells":{...}}]'` |
|
||||
| search-aitable-template | 1. `aitable template search --query "<关键词>"` → 取 `templateId`<br>2. 用户选定<br>3. `aitable base create --name "<表格名>" --template-id <templateId>` → 取 `baseId` |
|
||||
|
||||
@@ -102,11 +102,11 @@ dws aitable record delete --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
|
||||
> **不要使用钉盘 (drive) 上传!** 钉盘 fileId 无法写入 attachment 字段。
|
||||
|
||||
使用 `upload_attachment.py` 脚本(内部自动完成 prepare + PUT to OSS),**2 步**完成:
|
||||
使用统一 `aitable_ops.py upload-attachment` 入口(内部委派 prepare + PUT to OSS),**2 步**完成:
|
||||
|
||||
```bash
|
||||
# 步骤 1: 一键上传文件
|
||||
python3 scripts/upload_attachment.py <BASE_ID> /path/to/report.pdf
|
||||
python3 <本 Skill 绝对目录>/scripts/aitable_ops.py upload-attachment <BASE_ID> /path/to/report.pdf
|
||||
# 输出: { "fileToken": "ft_xxx", "fileName": "report.pdf", "size": 204800 }
|
||||
|
||||
# 步骤 2: 在 record create/update 中使用 fileToken 写入附件字段
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> **渐进式文档**:本文件为路由层(索引 + 意图判断),各命令的详细参数、示例和踩坑说明在 [aitable/](./aitable/) 目录下按需加载。
|
||||
|
||||
已知高频意图优先使用根 Skill 的精确 Shortcut/脚本骨架;本文件只在需要完整一级命令索引、对象 URL 或低频分支导航时加载。参数与安全不确定时读 leaf Schema,Cobra flag 不确定时才读 leaf Help,不要把本文件当作参数事实源。
|
||||
|
||||
## 文档地址 (URI)
|
||||
|
||||
| 资源 | URI 格式 |
|
||||
@@ -35,7 +37,7 @@
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `table get` | 获取数据表/视图目录 | `--base-id` | 不传 `--table-ids` 枚举全部表,但不返回字段;字段目录使用 `field get` |
|
||||
| `table get` | 获取表级信息、字段目录与视图目录 | `--base-id` | 不传 `--table-ids` 枚举全部表并返回精简 `fields[]`/`views[]`;字段完整 config 使用 `field get` |
|
||||
| `table create` | 创建数据表 | `--base-id` `--name` `--fields` | fields 为 JSON 数组,至少 1 个 |
|
||||
| `table update` | 修改表名 / 备注 / 行命名规则 | `--base-id` `--table-id` + 三选一(`--name` / `--description` / `--record-name-key`) | `--record-name-key` 是固定枚举(如 task/project/event/customer/ji_lu 等),非字段 ID |
|
||||
| `table delete` | 删除表 | `--base-id` `--table-id` | 不可逆 |
|
||||
@@ -427,17 +429,18 @@ dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 300
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
# 1. 搜索/列出 Base — 提取 baseId
|
||||
dws aitable base search --query "项目" --format json
|
||||
# 1. 按名称解析唯一 Base — 提取 baseId;多候选必须消歧
|
||||
dws aitable +resolve-base --name "项目" --format json
|
||||
|
||||
# 2. 获取 Base 信息 — 提取 tableId
|
||||
dws aitable base get --base-id <BASE_ID> --format json
|
||||
# 2. 按名称解析唯一 Table — 提取 tableId
|
||||
dws aitable +resolve-table --base <BASE_ID> --name "任务" --format json
|
||||
|
||||
# 3. 获取字段目录 — 提取 fieldId
|
||||
dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID> --format json
|
||||
# 3. 获取字段目录;需要完整类型 config 时再调用 field get
|
||||
dws aitable +table-get --base-id <BASE_ID> --table-ids <TABLE_ID> --format json
|
||||
dws aitable +field-get --base-id <BASE_ID> --table-id <TABLE_ID> --format json
|
||||
|
||||
# 4. 查询记录
|
||||
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --format json
|
||||
dws aitable +record-query --base-id <BASE_ID> --table-id <TABLE_ID> --format json
|
||||
|
||||
# 5. 新增记录 (cells 用 fieldId 作 key)
|
||||
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
@@ -452,8 +455,8 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
| `base create` | `baseId` | 后续命令 + 文档 URI |
|
||||
| `base get` | `tables[].tableId` | --table-id,拼接指定数据表 URI |
|
||||
| `table create` | `tableId` | 后续命令 + 拼接指定数据表 URI |
|
||||
| `table get` | `tables[].tableId`、视图目录 | 定位数据表和视图;字段需继续调用 `field get` |
|
||||
| `field get` | `fields[].fieldId` | record 操作的 cells key, field update/delete |
|
||||
| `table get` | `tables[].tableId`、精简 `fields[]`、`views[]` | 定位数据表、字段目录和视图;完整字段 config 再用 `field get` |
|
||||
| `field get` | `fields[].fieldId/type/config` | record 操作的 cells key、类型校验、field update/delete |
|
||||
| `record query` | `recordId` | record update/delete;按 ID 反查字段值用 `record get` |
|
||||
| `template search` | `templateId` | base create --template-id,拼接模板预览 URI |
|
||||
|
||||
@@ -478,10 +481,7 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
|
||||
| 脚本 | 场景 |
|
||||
|------|------|
|
||||
| [bulk_add_fields.py](../scripts/bulk_add_fields.py) | 批量添加字段 |
|
||||
| [import_records.py](../scripts/import_records.py) | 从 JSON/CSV 批量导入记录 |
|
||||
| [aitable_export_via_task.py](../scripts/aitable_export_via_task.py) | 文件导出(export_data 轮询 + 下载) |
|
||||
| [upload_attachment.py](../scripts/upload_attachment.py) | 上传附件到 AI 表格记录 |
|
||||
| `scripts/aitable_ops.py` | dashboard/chart、导入、导出、批量字段与附件的唯一稳定入口;参数见 `references/aitable/aitable-script-recipes.md` |
|
||||
|
||||
## 相关产品
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Flags:
|
||||
|
||||
```bash
|
||||
# 步骤 1: 使用脚本一键上传(内部自动完成 prepare + PUT)
|
||||
python3 scripts/upload_attachment.py <BASE_ID> /path/to/report.pdf
|
||||
python3 <本 Skill 绝对目录>/scripts/aitable_ops.py upload-attachment <BASE_ID> /path/to/report.pdf
|
||||
# 输出: { "fileToken": "ft_xxx", "fileName": "report.pdf", "size": 204800 }
|
||||
|
||||
# 步骤 2: 在 record create/update 中使用 fileToken 写入
|
||||
@@ -32,6 +32,7 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
```
|
||||
|
||||
> `uploadUrl` 有时效性(`expiresAt`),脚本会自动在获取后立即上传。
|
||||
> 脚本只接受成功的业务响应和 HTTPS `uploadUrl`;它返回 `fileToken` 不等于记录已经写入,后续仍需执行 record create/update 并按 recordId 回读。
|
||||
|
||||
## 手动流程(不使用脚本)
|
||||
|
||||
|
||||
@@ -1,18 +1,43 @@
|
||||
# dashboard & chart — 仪表盘与图表
|
||||
|
||||
## 建议操作顺序
|
||||
## 创建首选流程
|
||||
|
||||
```bash
|
||||
# 1) 先看配置模板(JSONC)
|
||||
dws aitable dashboard config-example --format json
|
||||
dws aitable chart widgets-example --format json
|
||||
# 仅建仪表盘
|
||||
python3 <本 Skill 绝对目录>/scripts/aitable_ops.py dashboard <BASE_ID> "<仪表盘名>"
|
||||
|
||||
# 2) 先拿 dashboard,再拿 chart 详情
|
||||
dws aitable dashboard get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --format json
|
||||
dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID> --format json
|
||||
# 建仪表盘和常用图表
|
||||
python3 <本 Skill 绝对目录>/scripts/aitable_ops.py dashboard <BASE_ID> "<仪表盘名>" \
|
||||
--chart-specs <workspace内/charts.json>
|
||||
```
|
||||
|
||||
## 要点
|
||||
统一入口的 dashboard 操作是 `dashboard create → chart create(可选)→ dashboard get` 的唯一首选
|
||||
recipe,并输出 `dws-skill-script-ledger/v1`。不要在脚本前调用 config-example 或
|
||||
widgets-example,也不要在成功后重复创建或回读。
|
||||
|
||||
`charts.json` 是 1–6 项数组,每项参数:
|
||||
|
||||
| 参数 | 要求 |
|
||||
|---|---|
|
||||
| `name` | 必填,图表名 |
|
||||
| `chart_type` | 必填:`AREA`、`BAR`、`HISTOGRAM`、`LINE`、`PIE`、`STATISTICS` |
|
||||
| `table_id` | 必填,当前链路的真实 tableId |
|
||||
| `measure_type` | `record-count`(默认)或 `field` |
|
||||
| `measure_field_id` | `measure_type=field` 时必填 |
|
||||
| `dimension_field_id` | 分组、分类或时间维度需要时填写 |
|
||||
| `aggregation` | 可选:`sum`、`count`、`count_distinct`、`average`、`min`、`max` |
|
||||
| `view_id` | 可选;不用视图时省略 |
|
||||
|
||||
例如按状态统计记录数:
|
||||
|
||||
```json
|
||||
[{"name":"跟进状态记录数","chart_type":"HISTOGRAM","table_id":"<tableId>","measure_type":"record-count","dimension_field_id":"<状态fieldId>"}]
|
||||
```
|
||||
|
||||
脚本不支持的图表类型或完整高级配置才走下方原子命令;这种例外至多读取一次
|
||||
`chart widgets-example`,再按真实 tableId/fieldId 构造配置。
|
||||
|
||||
## 查询与管理要点
|
||||
|
||||
- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用
|
||||
- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错
|
||||
@@ -26,7 +51,7 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
||||
| `dashboard create` | 创建仪表盘 | `--base-id` + (`--config` 或 `--name`) | `--name` 简化版创建空看板;`--config` 传完整 JSON |
|
||||
| `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config` 或 `--name`) | `--name` 仅改名;`--config` 更新完整配置 |
|
||||
| `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` `--yes` | — |
|
||||
| `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 |
|
||||
| `dashboard config-example` | 查看仪表盘配置模板 | 无 | 仅脚本不支持的高级配置按需读取一次 |
|
||||
| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 把图表按行铺满网格,避免某行只占半幅、留下大片空白;返回 `{totalColumns, layout, alignedChartCount}` |
|
||||
|
||||
## chart 子命令
|
||||
@@ -34,11 +59,10 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
||||
| 命令 | 用途 | 必填参数 |
|
||||
|------|------|----------|
|
||||
| `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` |
|
||||
| `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` |
|
||||
| `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` |
|
||||
| `chart widgets-example` | 查看图表 widgets 配置模板 | 无 |
|
||||
| `chart widgets-example` | 查看图表 widgets 配置模板 | 无;返回很大 | 仅脚本不支持的高级图表按需读取一次 |
|
||||
|
||||
## 配置获取流程
|
||||
|
||||
创建图表前,必须先调用 `chart widgets-example` 查看配置模板,了解每种图表类型需要的字段结构,然后根据实际 tableId 和 fieldId 填充配置。
|
||||
原子 `chart create` 必须同时传 `--layout`。返回 chartId 后用 `chart get`,或最后
|
||||
一次 `dashboard get` 核对;回读成功即停止。脚本已自动完成这些动作,不再重复执行。
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- `status: "error"` 表示操作失败
|
||||
- `summary` 包含错误摘要信息
|
||||
- `trace_id` 用于问题追踪
|
||||
- 命令退出码为 0 但 `status != "success"`、必需 ID 缺失或逐项结果含失败,也属于业务失败
|
||||
|
||||
## 2. 常见错误与恢复动作
|
||||
|
||||
@@ -42,7 +43,7 @@
|
||||
|-------------------|------|---------|
|
||||
| `base not found` | base-id 错误或无权限 | 确认 base-id 正确;尝试 `base list` 或 `base search` 重新定位 |
|
||||
| `table not found` | table-id 错误 | 用 `table get --base-id <baseId>` 不带 table-ids 查看所有表 |
|
||||
| 表名重复 | 同 Base 下已存在同名表 | 系统会自动续号(如"原名 1"),无需额外处理 |
|
||||
| 表名重复 | 同 Base 下已存在同名表 | 使用创建返回的真实 `tableId/tableName`;禁止继续按原名猜测目标 |
|
||||
|
||||
### 2.4 视图操作错误
|
||||
|
||||
@@ -57,7 +58,7 @@
|
||||
|-------------------|------|---------|
|
||||
| filters 无效被忽略 | 根节点不是 and/or,或 operands 格式错误 | 确保 filters 根节点是 `{"operator":"and"/"or", "operands":[...]}` 结构 |
|
||||
| sort 无效 | fieldId 不存在 | 先 `field get` 确认字段 ID |
|
||||
| 筛选结果为空 | 条件过严或字段值不匹配 | 放宽条件验证;注意 singleSelect 筛选值用 option name 或 id |
|
||||
| 筛选结果为空 | 条件过严或字段值不匹配 | 放宽条件验证;singleSelect/multipleSelect 筛选值使用 option name 字面量 |
|
||||
|
||||
### 2.6 导入导出错误
|
||||
|
||||
@@ -65,6 +66,7 @@
|
||||
|-------------------|------|---------|
|
||||
| 导出任务超时 | 数据量大,异步任务未完成 | 用 `export data --task-id <taskId>` 轮询直到完成 |
|
||||
| 导入文件格式错误 | 不支持的文件格式或文件损坏 | 确认文件为 .xlsx 格式且未加密 |
|
||||
| 批处理只成功一部分 | 某批或某个字段返回失败 | 保留成功 ID,输出失败 batch/item ledger,以非零状态结束;不要整批重放 |
|
||||
|
||||
## 3. 重试策略
|
||||
|
||||
@@ -106,18 +108,6 @@ dws aitable record create \
|
||||
|
||||
`--verbose` 会输出请求/响应的详细信息,帮助定位问题。
|
||||
|
||||
### 4.2 使用 --dry-run 预览
|
||||
|
||||
```bash
|
||||
dws aitable record create \
|
||||
--base-id <baseId> \
|
||||
--table-id <tableId> \
|
||||
--records '[...]' \
|
||||
--dry-run --format json
|
||||
```
|
||||
|
||||
`--dry-run` 只预览不执行,适合在不确定参数是否正确时先验证。
|
||||
|
||||
## 5. 错误预防最佳实践
|
||||
|
||||
1. **写记录前先读字段结构** — `field get` 确认字段类型和 ID
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
`export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。
|
||||
|
||||
优先使用 `python3 <本 Skill 绝对目录>/scripts/aitable_ops.py export <baseId> --scope all|table|view [...]`。完整参数只读 `references/aitable/aitable-script-recipes.md`;统一入口检查业务状态、持续轮询、要求 HTTPS 下载地址,并在本地文件已存在时停止而不是静默覆盖。只有需要控制底层轮询参数时才走下面的原子命令。
|
||||
|
||||
> ⚠️ **`--format` 冲突警告**:`export data` 的 `--format` 是**导出格式**(excel/attachment 等),不是全局输出格式。**此命令禁止追加全局 `--format json`**,否则会覆盖导出格式导致 `INVALID_EXPORT_FORMAT` 错误。输出默认就是 JSON,无需额外指定。
|
||||
|
||||
```bash
|
||||
@@ -28,6 +30,8 @@ dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 300
|
||||
|
||||
> **无需手动解析 CSV/Excel 再逐条 record create**,效率极低且容易出错。
|
||||
|
||||
新建数据表导入优先使用 `python3 <本 Skill 绝对目录>/scripts/aitable_ops.py import-new <baseId> <file>`;追加到已有表使用 `python3 <本 Skill 绝对目录>/scripts/aitable_ops.py import-records <baseId> <tableId> <file> [--batch-size N]`。完整参数只读 `references/aitable/aitable-script-recipes.md`;二者语义不同,不要自动互换。
|
||||
|
||||
```bash
|
||||
# 第 1 步:申请上传凭证
|
||||
dws aitable import upload --base-id <BASE_ID> \
|
||||
|
||||
@@ -14,7 +14,7 @@ Flags:
|
||||
--table-id string Table ID (必填)
|
||||
```
|
||||
|
||||
返回字段的完整配置(含 options 等)。不要假设未指定 `--table-ids` 的 `table get` 枚举结果含字段;字段目录和配置以 `field get` 返回为准。
|
||||
返回字段的完整配置(含 options 等)。`table get` 会返回精简字段目录,但写入、筛选或修改前需要类型相关完整 `config` 时,以 `field get` 返回为准。
|
||||
|
||||
## field create — 创建字段
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
|
||||
创建成功以 `data.newRecordIds[]` 为 ID 来源;不要把整个 `data` 当作单个 recordId,也不要只以退出码作为写入成功证据。
|
||||
|
||||
批量追加本地 JSON/CSV 到已有表时使用 `python3 <本 Skill 绝对目录>/scripts/aitable_ops.py import-records <baseId> <tableId> <file> [--batch-size N]`。完整参数只读 `references/aitable/aitable-script-recipes.md`;统一入口保留逐批业务状态、`newRecordIds` 回读和部分失败 ledger。
|
||||
|
||||
## cells 写入格式
|
||||
|
||||
各字段类型的写入格式见 [aitable-cell-value.md](./aitable-cell-value.md)。
|
||||
|
||||
@@ -60,7 +60,7 @@ dws aitable record query --base-id X --table-id Y --all --cursor "上次返回
|
||||
{"operator":"and","operands":[{"operator":"eq","operands":["<fieldId>","<value>"]}]}
|
||||
```
|
||||
|
||||
> **singleSelect/multipleSelect 过滤**:filters 中可传 option id 或 option name,但建议优先用 **option id**(通过 `field get` 获取),更可靠。
|
||||
> **singleSelect/multipleSelect 过滤**:filters 中传选项名称字面量,不传 option id。选项名称先通过 `field get` 的 `config.options[]` 核对。
|
||||
|
||||
## 减少响应体积
|
||||
|
||||
|
||||
@@ -17,23 +17,17 @@ Flags:
|
||||
|
||||
只需传入需修改的字段,未传入的保持原值。每条记录必须含 recordId 和 cells。
|
||||
|
||||
## cells key:优先使用 fieldId,也支持唯一字段名
|
||||
## cells key:自动化与 Agent 路径使用 fieldId
|
||||
|
||||
`cells` 的 key 有两种写法:
|
||||
公开稳定的 Agent/自动化写法是 `fieldId`:它不受字段重命名或重名影响,并可通过 `field get` 获取。
|
||||
|
||||
- fieldId(推荐):不受字段重命名或重名影响,通过 `field get` 获取。
|
||||
- 当前表内唯一的字段名:按名称精确匹配;如果存在同名字段,必须改用 fieldId。
|
||||
|
||||
同一字段同时通过 fieldId 和字段名传入时,fieldId 对应的值优先。
|
||||
Runtime 仍兼容当前表内唯一字段名,但这属于便捷兼容路径:重名或重命名会改变解析结果,不能用于脚本、跨步骤调用或从旧上下文重放。字段名与 fieldId 同时出现时也不要依赖覆盖顺序。
|
||||
|
||||
```bash
|
||||
# 推荐:fieldId
|
||||
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
--records '[{"recordId":"recXXX","cells":{"fldStatusId":"已完成"}}]' --format json
|
||||
|
||||
# 便捷写法:当前表内唯一字段名
|
||||
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
--records '[{"recordId":"recXXX","cells":{"状态":"已完成"}}]' --format json
|
||||
```
|
||||
|
||||
## 推荐参数形式
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Bundled script recipes
|
||||
|
||||
Use this file only for dashboard/chart, import, export, bulk fields, or attachment workflows.
|
||||
The stable entry point is `scripts/aitable_ops.py`; do not read it or the delegated `.py` files before execution. Run `python3 <Skill绝对目录>/scripts/aitable_ops.py --help` or the operation-level `--help` only when an argument is unclear.
|
||||
|
||||
For a create-Base-then-script workflow, create the Base first and take `baseId` from its structured response. If the user did not specify the container name, choose a short descriptive name and continue instead of asking only for that name. Verify the Base with `dws aitable base get --base-id <baseId> --format json`, then pass that exact `baseId` to this entry point.
|
||||
|
||||
| Intent | Exact command |
|
||||
|---|---|
|
||||
| Create dashboard, optionally charts | `python3 <Skill绝对目录>/scripts/aitable_ops.py dashboard <baseId> "<dashboardName>" [--chart-specs <workspace JSON>]` |
|
||||
| Import CSV/XLS/XLSX as a new table | `python3 <Skill绝对目录>/scripts/aitable_ops.py import-new <baseId> <file>` |
|
||||
| Append JSON/CSV to an existing table | `python3 <Skill绝对目录>/scripts/aitable_ops.py import-records <baseId> <tableId> <file> [--batch-size 100]` |
|
||||
| Export Base/table/view | `python3 <Skill绝对目录>/scripts/aitable_ops.py export <baseId> --scope all\|table\|view [--table-id <id>] [--view-id <id>] [--output <path>]` |
|
||||
| Add up to 15 fields | `python3 <Skill绝对目录>/scripts/aitable_ops.py add-fields <baseId> <tableId> <fields.json>` |
|
||||
| Upload attachment | `python3 <Skill绝对目录>/scripts/aitable_ops.py upload-attachment <baseId> <file>` |
|
||||
|
||||
## Contracts
|
||||
|
||||
- `dashboard` creates the dashboard/charts, chains returned IDs, performs final dashboard/chart readback, and emits `dws-skill-script-ledger/v1`. Do not add a second guessed dashboard command after it. `--chart-specs` is a JSON array; each item accepts `name`, `chart_type`, `table_id`, `measure_type`, `measure_field_id`, `dimension_field_id`, `aggregation`, and `view_id`.
|
||||
- `import-new` runs prepare → HTTPS PUT → import task and checks each business status. It is not interchangeable with `import-records`.
|
||||
- `import-records` requires CSV headers to be field IDs; use JSON for typed boolean/array/object values. It checks batch results and reads back returned record IDs.
|
||||
- `export --scope table|view` requires `--table-id`; view also requires `--view-id`. The unified ledger exposes the real `taskId`, `polledTimes`, `savedPath`, and `fileSize`; treat success plus a non-empty saved file as the completion evidence, without reading source or substituting `ls` for task polling. Do not overwrite unless the user explicitly requests it.
|
||||
- `add-fields` accepts at most 15 items and reports partial failure. `upload-attachment` only returns `fileToken`; write that token to the attachment field and read the record back.
|
||||
- Preserve nonzero exit, partial-success ledger, timeout, and incomplete readback in the final answer. Do not replace a failed deterministic workflow with guessed atomic commands unless its reported error proves that the script contract is unavailable.
|
||||
@@ -91,11 +91,11 @@ dws aitable table create --base-id <BASE_ID> --name "产品图片" \
|
||||
|
||||
> **不要**使用钉盘 (drive) 上传来替代此流程!钉盘 fileId **无法**写入 attachment 字段。
|
||||
|
||||
附件字段写入使用 `upload_attachment.py` 脚本,**2 步**完成:
|
||||
附件字段写入使用统一 `aitable_ops.py upload-attachment` 入口,**2 步**完成:
|
||||
|
||||
```bash
|
||||
# 步骤 1: 一键上传文件(脚本内部自动完成 prepare + PUT to OSS)
|
||||
python3 scripts/upload_attachment.py <BASE_ID> /path/to/photo.png
|
||||
python3 <本 Skill 绝对目录>/scripts/aitable_ops.py upload-attachment <BASE_ID> /path/to/photo.png
|
||||
# 输出: { "fileToken": "ft_xxx", "fileName": "photo.png", "size": 1024 }
|
||||
|
||||
# 步骤 2: 在 record create/update 中使用 fileToken
|
||||
|
||||
@@ -26,7 +26,8 @@ from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
|
||||
# Runtime AI Table IDs are opaque and some table IDs are seven characters.
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{6,128}$")
|
||||
ALLOWED_FORMATS = {"excel", "attachment", "excel_and_attachment", "excel_with_inline_images"}
|
||||
|
||||
|
||||
@@ -54,15 +55,20 @@ def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
|
||||
|
||||
|
||||
def normalize_download_url(url: str) -> str:
|
||||
if url.startswith("http://") or url.startswith("https://"):
|
||||
return url
|
||||
return f"https://{url}"
|
||||
normalized = url if "://" in url else f"https://{url}"
|
||||
parsed = urlparse(normalized)
|
||||
if parsed.scheme != "https" or not parsed.hostname:
|
||||
raise ValueError("downloadUrl 必须是有效的 HTTPS URL")
|
||||
return normalized
|
||||
|
||||
|
||||
def download_file(url: str, output_path: Path) -> Tuple[bool, str]:
|
||||
req = Request(url, method="GET")
|
||||
try:
|
||||
with urlopen(req, timeout=180) as resp:
|
||||
redirected = urlparse(resp.geturl())
|
||||
if redirected.scheme != "https" or not redirected.hostname:
|
||||
return False, "download redirect is not HTTPS"
|
||||
if resp.status != 200:
|
||||
return False, f"download http status: {resp.status}"
|
||||
output_path.write_bytes(resp.read())
|
||||
@@ -79,6 +85,20 @@ def fail(msg: str, code: int = 1) -> None:
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def resolve_output_path(value: Optional[str], file_name: str, overwrite: bool) -> Path:
|
||||
root = Path.cwd().resolve()
|
||||
candidate = Path(value) if value else Path(file_name).name
|
||||
output_path = candidate.resolve() if candidate.is_absolute() else (root / candidate).resolve()
|
||||
try:
|
||||
output_path.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise ValueError("输出路径必须位于当前工作目录内") from exc
|
||||
if output_path.exists() and not overwrite:
|
||||
raise ValueError(f"输出文件已存在:{output_path};如需覆盖请显式传 --overwrite")
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return output_path
|
||||
|
||||
|
||||
def build_start_args(args: argparse.Namespace) -> list[str]:
|
||||
cmd = [
|
||||
"aitable",
|
||||
@@ -113,6 +133,7 @@ def main() -> None:
|
||||
parser.add_argument("--output", help="本地保存路径(不传则按 fileName 保存到当前目录)")
|
||||
parser.add_argument("--dws", default="dws", help="dws 可执行文件路径,默认 dws")
|
||||
parser.add_argument("--no-download", action="store_true", help="仅返回 downloadUrl,不下载文件")
|
||||
parser.add_argument("--overwrite", action="store_true", help="允许覆盖当前工作目录内的已有输出文件")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not validate_resource_id(args.base_id):
|
||||
@@ -121,6 +142,10 @@ def main() -> None:
|
||||
fail("scope=table/view 时必须传 --table-id")
|
||||
if args.scope == "view" and not args.view_id:
|
||||
fail("scope=view 时必须传 --view-id")
|
||||
if args.table_id and not validate_resource_id(args.table_id):
|
||||
fail("无效的 tableId 格式")
|
||||
if args.view_id and not validate_resource_id(args.view_id):
|
||||
fail("无效的 viewId 格式")
|
||||
|
||||
print("[1/2] start export task", file=sys.stderr)
|
||||
rc, out, err = run_dws(args.dws, build_start_args(args), timeout_sec=120)
|
||||
@@ -132,7 +157,7 @@ def main() -> None:
|
||||
|
||||
data = obj.get("data", {}) or {}
|
||||
status = obj.get("status")
|
||||
if status == "error":
|
||||
if status != "success":
|
||||
fail(f"export_data 返回失败: {json.dumps(obj, ensure_ascii=False)}")
|
||||
|
||||
download_url = data.get("downloadUrl")
|
||||
@@ -163,7 +188,7 @@ def main() -> None:
|
||||
obj2 = parse_json_output(out2)
|
||||
if not obj2:
|
||||
fail(f"export_data 轮询返回非 JSON: {out2[:300]}")
|
||||
if obj2.get("status") == "error":
|
||||
if obj2.get("status") != "success":
|
||||
fail(f"export_data 轮询返回失败: {json.dumps(obj2, ensure_ascii=False)}")
|
||||
d2 = obj2.get("data", {}) or {}
|
||||
download_url = d2.get("downloadUrl") or download_url
|
||||
@@ -194,8 +219,11 @@ def main() -> None:
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return
|
||||
|
||||
norm_url = normalize_download_url(download_url)
|
||||
output_path = Path(args.output).expanduser().resolve() if args.output else Path.cwd() / file_name
|
||||
try:
|
||||
norm_url = normalize_download_url(download_url)
|
||||
output_path = resolve_output_path(args.output, file_name, args.overwrite)
|
||||
except ValueError as exc:
|
||||
fail(str(exc))
|
||||
ok, dl_err = download_file(norm_url, output_path)
|
||||
if not ok:
|
||||
fail(f"downloadUrl 下载失败: {dl_err}")
|
||||
|
||||
@@ -22,6 +22,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
|
||||
@@ -52,6 +53,9 @@ def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
|
||||
|
||||
|
||||
def put_file(upload_url: str, file_path: Path) -> Tuple[bool, str]:
|
||||
parsed = urlparse(upload_url)
|
||||
if parsed.scheme != "https" or not parsed.hostname:
|
||||
return False, "uploadUrl must be a valid HTTPS URL"
|
||||
payload = file_path.read_bytes()
|
||||
req = Request(upload_url, data=payload, method="PUT")
|
||||
# 关键:清空 Content-Type,避免 SignatureDoesNotMatch。
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stable black-box entry point for bundled AI Table workflows."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
LEDGER_SCHEMA_VERSION = "dws-skill-script-ledger/v1"
|
||||
|
||||
|
||||
def command_for(args: argparse.Namespace) -> list[str]:
|
||||
if args.operation == "dashboard":
|
||||
command = [
|
||||
"create_dashboard_chart.py", args.base_id, args.dashboard_name,
|
||||
*(["--chart-specs", args.chart_specs] if args.chart_specs else []),
|
||||
]
|
||||
elif args.operation == "import-new":
|
||||
command = ["aitable_import_via_task.py", args.base_id, args.file]
|
||||
elif args.operation == "import-records":
|
||||
command = [
|
||||
"import_records.py", args.base_id, args.table_id, args.file,
|
||||
str(args.batch_size),
|
||||
]
|
||||
elif args.operation == "export":
|
||||
command = [
|
||||
"aitable_export_via_task.py", args.base_id, "--scope", args.scope,
|
||||
*(["--table-id", args.table_id] if args.table_id else []),
|
||||
*(["--view-id", args.view_id] if args.view_id else []),
|
||||
*(["--output", args.output] if args.output else []),
|
||||
*(["--export-format", args.export_format] if args.export_format else []),
|
||||
*(["--overwrite"] if args.overwrite else []),
|
||||
]
|
||||
elif args.operation == "add-fields":
|
||||
command = [
|
||||
"bulk_add_fields.py", args.base_id, args.table_id, args.fields_file,
|
||||
]
|
||||
else:
|
||||
command = ["upload_attachment.py", args.base_id, args.file]
|
||||
return [sys.executable, str(SCRIPT_DIR / command[0]), *command[1:]]
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
root = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Run a bundled AI Table workflow without reading implementation source. "
|
||||
"Each operation preserves the underlying script ledger and exit status."
|
||||
)
|
||||
)
|
||||
sub = root.add_subparsers(dest="operation", required=True)
|
||||
|
||||
dashboard = sub.add_parser("dashboard", help="create and read back a dashboard/chart")
|
||||
dashboard.add_argument("base_id")
|
||||
dashboard.add_argument("dashboard_name")
|
||||
dashboard.add_argument("--chart-specs", help="workspace JSON array of chart specs")
|
||||
|
||||
import_new = sub.add_parser("import-new", help="import CSV/XLS/XLSX as a new table")
|
||||
import_new.add_argument("base_id")
|
||||
import_new.add_argument("file")
|
||||
|
||||
import_records = sub.add_parser("import-records", help="append JSON/CSV records to an existing table")
|
||||
import_records.add_argument("base_id")
|
||||
import_records.add_argument("table_id")
|
||||
import_records.add_argument("file")
|
||||
import_records.add_argument("--batch-size", type=int, default=100)
|
||||
|
||||
export = sub.add_parser("export", help="export a Base, table, or view")
|
||||
export.add_argument("base_id")
|
||||
export.add_argument("--scope", choices=("all", "table", "view"), required=True)
|
||||
export.add_argument("--table-id")
|
||||
export.add_argument("--view-id")
|
||||
export.add_argument("--output")
|
||||
export.add_argument(
|
||||
"--export-format",
|
||||
choices=("attachment", "excel", "excel_and_attachment", "excel_with_inline_images"),
|
||||
)
|
||||
export.add_argument("--overwrite", action="store_true")
|
||||
|
||||
fields = sub.add_parser("add-fields", help="create up to 15 fields from JSON")
|
||||
fields.add_argument("base_id")
|
||||
fields.add_argument("table_id")
|
||||
fields.add_argument("fields_file")
|
||||
|
||||
attachment = sub.add_parser("upload-attachment", help="upload a file and return fileToken")
|
||||
attachment.add_argument("base_id")
|
||||
attachment.add_argument("file")
|
||||
return root
|
||||
|
||||
|
||||
def normalize_output(raw: str, args: argparse.Namespace | None = None) -> str:
|
||||
"""Make a delegated trusted ledger attributable to this stable entry point."""
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return raw
|
||||
if (
|
||||
isinstance(payload, dict)
|
||||
and payload.get("schema_version") == LEDGER_SCHEMA_VERSION
|
||||
and isinstance(payload.get("script"), str)
|
||||
):
|
||||
payload["implementation_script"] = payload["script"]
|
||||
payload["script"] = Path(__file__).name
|
||||
return json.dumps(payload, ensure_ascii=False)
|
||||
if args and args.operation == "export" and isinstance(payload, dict):
|
||||
status = str(payload.get("status") or "error")
|
||||
saved_path = str(payload.get("savedPath") or "")
|
||||
file_size = 0
|
||||
if saved_path:
|
||||
try:
|
||||
file_size = Path(saved_path).stat().st_size
|
||||
except OSError:
|
||||
file_size = 0
|
||||
task_id = str(payload.get("taskId") or "")
|
||||
ledger_status = "success" if status == "success" and task_id and (
|
||||
bool(payload.get("downloadUrl")) and (not saved_path or file_size > 0)
|
||||
) else status
|
||||
ledger = {
|
||||
"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"script": Path(__file__).name,
|
||||
"implementation_script": "aitable_export_via_task.py",
|
||||
"status": ledger_status,
|
||||
"result": payload,
|
||||
"ledger": [{
|
||||
"cli_path": "aitable export data",
|
||||
"status": ledger_status,
|
||||
"params": {
|
||||
"base-id": args.base_id,
|
||||
"scope": args.scope,
|
||||
**({"table-id": args.table_id} if args.table_id else {}),
|
||||
**({"view-id": args.view_id} if args.view_id else {}),
|
||||
},
|
||||
"output_ids": {
|
||||
"taskId": task_id,
|
||||
"polledTimes": int(payload.get("polledTimes") or 0),
|
||||
"savedPath": saved_path,
|
||||
"fileSize": file_size,
|
||||
},
|
||||
"error": "" if ledger_status == "success" else str(payload.get("summary") or "export incomplete"),
|
||||
}],
|
||||
}
|
||||
return json.dumps(ledger, ensure_ascii=False)
|
||||
return raw
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parser().parse_args()
|
||||
if args.operation == "export":
|
||||
if args.scope in {"table", "view"} and not args.table_id:
|
||||
parser().error("export --scope table|view requires --table-id")
|
||||
if args.scope == "view" and not args.view_id:
|
||||
parser().error("export --scope view requires --view-id")
|
||||
result = subprocess.run(command_for(args), check=False, capture_output=True, text=True)
|
||||
if result.stdout:
|
||||
print(normalize_output(result.stdout, args), end="" if result.stdout.endswith("\n") else "\n")
|
||||
if result.stderr:
|
||||
print(result.stderr, file=sys.stderr, end="" if result.stderr.endswith("\n") else "\n")
|
||||
return result.returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,273 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
批量添加字段到钉钉 AI 表格数据表(新版 schema)
|
||||
"""批量添加字段到钉钉 AI 表格数据表。
|
||||
|
||||
用法:
|
||||
python bulk_add_fields.py <baseId> <tableId> fields.json
|
||||
python3 bulk_add_fields.py <baseId> <tableId> fields.json
|
||||
|
||||
fields.json 格式:
|
||||
[
|
||||
{"fieldName": "字段 1", "type": "text"},
|
||||
{"fieldName": "字段 2", "type": "number", "config": {"formatter": "INT"}},
|
||||
{"fieldName": "字段 3", "type": "singleSelect", "config": {"options": [{"name": "高"}]}}
|
||||
]
|
||||
|
||||
兼容写法:
|
||||
- name 会自动映射为 fieldName
|
||||
- phone 会自动映射为 telephone
|
||||
脚本检查业务状态和逐项结果,并回读成功字段。部分成功会输出 ledger,但整体以非零
|
||||
状态结束。fields.json 单次最多 15 个字段;name 会映射为 fieldName,phone 会映射为
|
||||
telephone。
|
||||
"""
|
||||
|
||||
import sys
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Union, List, Dict, Any, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
JsonData = Union[List[Any], Dict[str, Any]]
|
||||
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024
|
||||
ALLOWED_FILE_EXTENSIONS = ['.json']
|
||||
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
|
||||
ALLOWED_FIELD_TYPES = {
|
||||
'text', 'number', 'singleSelect', 'multipleSelect', 'date', 'currency',
|
||||
'user', 'department', 'group', 'progress', 'rating', 'checkbox',
|
||||
'attachment', 'url', 'richText', 'telephone', 'email', 'idCard',
|
||||
'barcode', 'geolocation', 'address', 'primaryDoc', 'formula',
|
||||
'unidirectionalLink', 'bidirectionalLink', 'lookup', 'filterUp',
|
||||
'creator', 'lastModifier', 'createdTime', 'lastModifiedTime',
|
||||
}
|
||||
FIELD_TYPE_ALIASES = {
|
||||
'phone': 'telephone',
|
||||
"text", "number", "singleSelect", "multipleSelect", "date", "currency",
|
||||
"user", "department", "group", "progress", "rating", "checkbox",
|
||||
"attachment", "url", "richText", "telephone", "email", "idCard",
|
||||
"barcode", "geolocation", "address", "primaryDoc", "formula",
|
||||
"unidirectionalLink", "bidirectionalLink", "lookup", "filterUp",
|
||||
"creator", "lastModifier", "createdTime", "lastModifiedTime",
|
||||
}
|
||||
|
||||
|
||||
def resolve_safe_path(path: str, allowed_root: Optional[str] = None) -> Path:
|
||||
if allowed_root is None:
|
||||
allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
|
||||
|
||||
allowed_root = Path(allowed_root).resolve()
|
||||
target_path = (
|
||||
Path(path).resolve()
|
||||
if Path(path).is_absolute()
|
||||
else (Path.cwd() / path).resolve()
|
||||
)
|
||||
|
||||
root = Path(allowed_root or os.environ.get("OPENCLAW_WORKSPACE", os.getcwd())).resolve()
|
||||
candidate = Path(path)
|
||||
target = candidate.resolve() if candidate.is_absolute() else (Path.cwd() / candidate).resolve()
|
||||
try:
|
||||
target_path.relative_to(allowed_root)
|
||||
return target_path
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"路径超出允许范围:{path}\n"
|
||||
f"目标路径:{target_path}\n"
|
||||
f"允许根目录:{allowed_root}\n"
|
||||
f"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内"
|
||||
)
|
||||
target.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"路径超出允许范围:{path}(允许根目录:{root})") from exc
|
||||
return target
|
||||
|
||||
|
||||
def validate_resource_id(resource_id: str) -> bool:
|
||||
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
|
||||
return bool(resource_id and RESOURCE_ID_PATTERN.fullmatch(resource_id.strip()))
|
||||
|
||||
|
||||
def validate_file_extension(filename: str, allowed_extensions: list) -> bool:
|
||||
return any(filename.lower().endswith(ext) for ext in allowed_extensions)
|
||||
|
||||
|
||||
def safe_json_load(file_path: Path, max_size: int = MAX_FILE_SIZE) -> JsonData:
|
||||
file_size = file_path.stat().st_size
|
||||
if file_size > max_size:
|
||||
raise ValueError(
|
||||
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
|
||||
)
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
def safe_json_load(file_path: Path) -> JsonData:
|
||||
if file_path.stat().st_size > MAX_FILE_SIZE:
|
||||
raise ValueError(f"文件过大(限制 {MAX_FILE_SIZE:,} 字节)")
|
||||
with file_path.open("r", encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def normalize_field_config(field: Dict[str, Any]) -> Dict[str, Any]:
|
||||
normalized = dict(field)
|
||||
if 'fieldName' not in normalized and 'name' in normalized:
|
||||
normalized['fieldName'] = normalized.pop('name')
|
||||
normalized['type'] = FIELD_TYPE_ALIASES.get(
|
||||
normalized.get('type', 'text'), normalized.get('type', 'text')
|
||||
)
|
||||
if "fieldName" not in normalized and "name" in normalized:
|
||||
normalized["fieldName"] = normalized.pop("name")
|
||||
if normalized.get("type") == "phone":
|
||||
normalized["type"] = "telephone"
|
||||
return normalized
|
||||
|
||||
|
||||
def validate_field_config(field: Dict[str, Any]) -> Tuple[bool, str]:
|
||||
def validate_field_config(field: Any) -> Tuple[bool, str]:
|
||||
if not isinstance(field, dict):
|
||||
return False, '字段配置必须是对象'
|
||||
|
||||
field = normalize_field_config(field)
|
||||
|
||||
if 'fieldName' not in field:
|
||||
return False, '缺少必需字段:fieldName'
|
||||
if not isinstance(field['fieldName'], str) or not field['fieldName'].strip():
|
||||
return False, 'fieldName 必须是非空字符串'
|
||||
|
||||
field_type = field.get('type', 'text')
|
||||
return False, "字段配置必须是对象"
|
||||
normalized = normalize_field_config(field)
|
||||
name = normalized.get("fieldName")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
return False, "fieldName 必须是非空字符串"
|
||||
field_type = normalized.get("type", "text")
|
||||
if field_type not in ALLOWED_FIELD_TYPES:
|
||||
return False, f"不支持的字段类型:{field_type}"
|
||||
|
||||
config = field.get('config')
|
||||
config = normalized.get("config")
|
||||
if config is not None and not isinstance(config, dict):
|
||||
return False, 'config 必须是对象'
|
||||
|
||||
if field_type in {'singleSelect', 'multipleSelect'}:
|
||||
options = (config or {}).get('options')
|
||||
if not options or not isinstance(options, list):
|
||||
return False, (
|
||||
'singleSelect / multipleSelect 必须提供 config.options 数组'
|
||||
)
|
||||
|
||||
if field_type in {'unidirectionalLink', 'bidirectionalLink'}:
|
||||
linked_table_id = (config or {}).get('linkedTableId')
|
||||
if not linked_table_id or not validate_resource_id(linked_table_id):
|
||||
return False, (
|
||||
'关联字段必须提供合法的 config.linkedTableId(目标 Table ID)'
|
||||
)
|
||||
|
||||
if field_type == 'lookup':
|
||||
cfg = config or {}
|
||||
if not cfg.get('associateField'):
|
||||
return False, 'lookup 必须提供 config.associateField(本表关联字段的 fieldId)'
|
||||
if not cfg.get('valuesField'):
|
||||
return False, 'lookup 必须提供 config.valuesField(关联目标表中要取值的字段 fieldId)'
|
||||
if not cfg.get('aggregator'):
|
||||
return False, 'lookup 必须提供 config.aggregator(SUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE)'
|
||||
|
||||
if field_type == 'filterUp':
|
||||
cfg = config or {}
|
||||
if not cfg.get('targetSheet'):
|
||||
return False, 'filterUp 必须提供 config.targetSheet(目标 Table ID)'
|
||||
filters = cfg.get('filters')
|
||||
if not filters or not isinstance(filters, list):
|
||||
return False, 'filterUp 必须提供 config.filters(至少一条筛选规则)'
|
||||
if not cfg.get('valuesField'):
|
||||
return False, 'filterUp 必须提供 config.valuesField(目标表中要取值的字段 fieldId)'
|
||||
if not cfg.get('aggregator'):
|
||||
return False, 'filterUp 必须提供 config.aggregator(SUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE)'
|
||||
|
||||
return True, ''
|
||||
return False, "config 必须是对象"
|
||||
ai_config = normalized.get("aiConfig")
|
||||
if ai_config is not None and not isinstance(ai_config, dict):
|
||||
return False, "aiConfig 必须是对象"
|
||||
if field_type in {"singleSelect", "multipleSelect"}:
|
||||
options = (config or {}).get("options")
|
||||
if not isinstance(options, list) or not options:
|
||||
return False, "singleSelect / multipleSelect 必须提供 config.options 数组"
|
||||
if field_type in {"unidirectionalLink", "bidirectionalLink"}:
|
||||
linked_table_id = (config or {}).get("linkedTableId")
|
||||
if not validate_resource_id(str(linked_table_id or "")):
|
||||
return False, "关联字段必须提供合法的 config.linkedTableId"
|
||||
if field_type == "lookup":
|
||||
required = ("associateField", "valuesField", "aggregator")
|
||||
missing = [name for name in required if not (config or {}).get(name)]
|
||||
if missing:
|
||||
return False, f"lookup 缺少 config.{missing[0]}"
|
||||
if field_type == "filterUp":
|
||||
required = ("targetSheet", "filters", "valuesField", "aggregator")
|
||||
missing = [name for name in required if not (config or {}).get(name)]
|
||||
if missing:
|
||||
return False, f"filterUp 缺少 config.{missing[0]}"
|
||||
if not isinstance((config or {}).get("filters"), list):
|
||||
return False, "filterUp config.filters 必须是数组"
|
||||
return True, ""
|
||||
|
||||
|
||||
def build_fields_json(fields: List[Dict[str, Any]]) -> str:
|
||||
"""构建 --fields 参数的 JSON 字符串。"""
|
||||
payload_fields = []
|
||||
def build_fields_payload(fields: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
payload: List[Dict[str, Any]] = []
|
||||
for field in fields:
|
||||
normalized = normalize_field_config(field)
|
||||
item: Dict[str, Any] = {
|
||||
'fieldName': normalized['fieldName'].strip(),
|
||||
'type': normalized.get('type', 'text'),
|
||||
"fieldName": normalized["fieldName"].strip(),
|
||||
"type": normalized.get("type", "text"),
|
||||
}
|
||||
if 'config' in normalized and normalized['config'] is not None:
|
||||
item['config'] = normalized['config']
|
||||
payload_fields.append(item)
|
||||
return json.dumps(payload_fields, ensure_ascii=False)
|
||||
for key in ("config", "aiConfig"):
|
||||
if normalized.get(key) is not None:
|
||||
item[key] = normalized[key]
|
||||
payload.append(item)
|
||||
return payload
|
||||
|
||||
|
||||
def run_dws(args: List[str]) -> Optional[Dict[str, Any]]:
|
||||
if not args:
|
||||
print('错误:空命令')
|
||||
return None
|
||||
|
||||
cmd = ['dws'] + args
|
||||
def run_dws(dws_bin: str, args: List[str], timeout_sec: int = 120) -> Tuple[Optional[Dict[str, Any]], str]:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=60
|
||||
[dws_bin] + args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout_sec,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"错误:{result.stderr.strip()}")
|
||||
return None
|
||||
try:
|
||||
return json.loads(result.stdout)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"无法解析响应:{result.stdout[:200]}...")
|
||||
print(f"JSON 解析错误:{e}")
|
||||
return None
|
||||
except subprocess.TimeoutExpired:
|
||||
print('错误:命令执行超时(60 秒)')
|
||||
return None
|
||||
return None, f"dws 命令超时({timeout_sec} 秒)"
|
||||
except FileNotFoundError:
|
||||
print('错误:未找到 dws 命令,请确认已安装')
|
||||
return None
|
||||
return None, f"未找到 dws 命令:{dws_bin}"
|
||||
if result.returncode != 0:
|
||||
return None, (result.stderr or result.stdout).strip() or f"dws 退出码 {result.returncode}"
|
||||
try:
|
||||
payload = json.loads(result.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
return None, f"dws 返回非 JSON:{exc}"
|
||||
if not isinstance(payload, dict):
|
||||
return None, "dws 返回的 JSON 不是对象"
|
||||
if payload.get("status") != "success":
|
||||
detail = payload.get("summary") or payload.get("error") or payload
|
||||
return None, f"业务失败:{detail}"
|
||||
return payload, ""
|
||||
|
||||
|
||||
def normalize_result_item(index: int, item: Any) -> Dict[str, Any]:
|
||||
if not isinstance(item, dict):
|
||||
return {"index": index, "status": "failed", "error": "逐项结果不是对象"}
|
||||
field_id = item.get("fieldId") or (item.get("data") or {}).get("fieldId")
|
||||
succeeded = item.get("success") is True or item.get("status") == "success"
|
||||
if "success" not in item and "status" not in item:
|
||||
succeeded = bool(field_id)
|
||||
result: Dict[str, Any] = {
|
||||
"index": index,
|
||||
"status": "success" if succeeded and field_id else "failed",
|
||||
}
|
||||
if field_id:
|
||||
result["fieldId"] = str(field_id)
|
||||
if result["status"] != "success":
|
||||
result["error"] = item.get("reason") or item.get("error") or "字段创建未返回 fieldId"
|
||||
return result
|
||||
|
||||
|
||||
def bulk_add_fields(
|
||||
base_id: str, table_id: str, fields_file: str
|
||||
) -> bool:
|
||||
base_id: str,
|
||||
table_id: str,
|
||||
fields: List[Dict[str, Any]],
|
||||
dws_bin: str = "dws",
|
||||
) -> Dict[str, Any]:
|
||||
created, error = run_dws(
|
||||
dws_bin,
|
||||
[
|
||||
"aitable", "field", "create",
|
||||
"--base-id", base_id,
|
||||
"--table-id", table_id,
|
||||
"--fields", json.dumps(build_fields_payload(fields), ensure_ascii=False),
|
||||
"--format", "json",
|
||||
],
|
||||
)
|
||||
if created is None:
|
||||
return {
|
||||
"status": "failed",
|
||||
"complete": False,
|
||||
"requestedCount": len(fields),
|
||||
"verifiedCount": 0,
|
||||
"ledger": [{"status": "failed", "error": error}],
|
||||
}
|
||||
|
||||
data = created.get("data") if isinstance(created.get("data"), dict) else {}
|
||||
raw_results = data.get("results")
|
||||
if not isinstance(raw_results, list) or len(raw_results) != len(fields):
|
||||
return {
|
||||
"status": "failed",
|
||||
"complete": False,
|
||||
"requestedCount": len(fields),
|
||||
"verifiedCount": 0,
|
||||
"ledger": [{"status": "failed", "error": "响应缺少与输入数量一致的 data.results[]"}],
|
||||
}
|
||||
ledger = [normalize_result_item(index, item) for index, item in enumerate(raw_results)]
|
||||
field_ids = [item["fieldId"] for item in ledger if item["status"] == "success"]
|
||||
verified_ids: List[str] = []
|
||||
if field_ids:
|
||||
queried, query_error = run_dws(
|
||||
dws_bin,
|
||||
[
|
||||
"aitable", "field", "get",
|
||||
"--base-id", base_id,
|
||||
"--table-id", table_id,
|
||||
"--field-ids", ",".join(field_ids),
|
||||
"--format", "json",
|
||||
],
|
||||
)
|
||||
if queried is None:
|
||||
for item in ledger:
|
||||
if item["status"] == "success":
|
||||
item["status"] = "verify_failed"
|
||||
item["error"] = query_error
|
||||
else:
|
||||
query_data = queried.get("data") if isinstance(queried.get("data"), dict) else {}
|
||||
raw_fields = query_data.get("fields") or query_data.get("items") or []
|
||||
found = {
|
||||
str(item.get("fieldId"))
|
||||
for item in raw_fields
|
||||
if isinstance(item, dict) and item.get("fieldId")
|
||||
}
|
||||
verified_ids = [field_id for field_id in field_ids if field_id in found]
|
||||
for item in ledger:
|
||||
if item.get("fieldId") in field_ids and item.get("fieldId") not in found:
|
||||
item["status"] = "verify_failed"
|
||||
item["error"] = "回读未返回该字段"
|
||||
complete = len(verified_ids) == len(fields) and all(item["status"] == "success" for item in ledger)
|
||||
return {
|
||||
"status": "success" if complete else "partial",
|
||||
"complete": complete,
|
||||
"requestedCount": len(fields),
|
||||
"verifiedCount": len(verified_ids),
|
||||
"fieldIds": verified_ids,
|
||||
"ledger": ledger,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("base_id")
|
||||
parser.add_argument("table_id")
|
||||
parser.add_argument("fields_file")
|
||||
parser.add_argument("--dws", default="dws", help="dws 可执行文件路径")
|
||||
args = parser.parse_args()
|
||||
if not validate_resource_id(args.base_id):
|
||||
parser.error("无效的 baseId 格式")
|
||||
if not validate_resource_id(args.table_id):
|
||||
parser.error("无效的 tableId 格式")
|
||||
try:
|
||||
safe_path = resolve_safe_path(fields_file)
|
||||
except ValueError as e:
|
||||
print(f"路径验证失败:{e}")
|
||||
return False
|
||||
|
||||
if not validate_file_extension(fields_file, ALLOWED_FILE_EXTENSIONS):
|
||||
print(f"错误:只允许 {', '.join(ALLOWED_FILE_EXTENSIONS)} 文件")
|
||||
return False
|
||||
if not safe_path.exists():
|
||||
print(f"错误:文件不存在:{safe_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
fields = safe_json_load(safe_path)
|
||||
except ValueError as e:
|
||||
print(f"错误:{e}")
|
||||
return False
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"错误:JSON 格式无效:{e}")
|
||||
return False
|
||||
|
||||
if not isinstance(fields, list) or not fields:
|
||||
print('错误:fields.json 必须是非空 JSON 数组')
|
||||
return False
|
||||
if len(fields) > 15:
|
||||
print('错误:单次最多创建 15 个字段,请拆分后重试')
|
||||
return False
|
||||
|
||||
for i, field in enumerate(fields):
|
||||
valid, error = validate_field_config(field)
|
||||
if not valid:
|
||||
print(f"错误:字段 #{i+1} 配置无效:{error}")
|
||||
return False
|
||||
|
||||
fields_json = build_fields_json(fields)
|
||||
result = run_dws([
|
||||
'aitable', 'field', 'create',
|
||||
'--base-id', base_id,
|
||||
'--table-id', table_id,
|
||||
'--fields', fields_json,
|
||||
'--format', 'json',
|
||||
])
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
path = resolve_safe_path(args.fields_file)
|
||||
if path.suffix.lower() != ".json" or not path.exists() or not path.is_file():
|
||||
raise ValueError("fields_file 必须是工作区内存在的 .json 文件")
|
||||
fields = safe_json_load(path)
|
||||
if not isinstance(fields, list) or not fields:
|
||||
raise ValueError("fields.json 必须是非空 JSON 数组")
|
||||
if len(fields) > 15:
|
||||
raise ValueError("单次最多创建 15 个字段,请拆分后重试")
|
||||
for index, field in enumerate(fields, start=1):
|
||||
valid, error = validate_field_config(field)
|
||||
if not valid:
|
||||
raise ValueError(f"字段 #{index} 配置无效:{error}")
|
||||
result = bulk_add_fields(args.base_id, args.table_id, fields, args.dws)
|
||||
except (ValueError, OSError, json.JSONDecodeError) as exc:
|
||||
print(f"错误:{exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return True
|
||||
sys.exit(0 if result["complete"] else 2)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 4:
|
||||
print(__doc__)
|
||||
print('用法示例:')
|
||||
print(' python bulk_add_fields.py basexxx tablexxx fields.json')
|
||||
sys.exit(1)
|
||||
|
||||
base_id = sys.argv[1]
|
||||
table_id = sys.argv[2]
|
||||
fields_file = sys.argv[3]
|
||||
|
||||
if not validate_resource_id(base_id):
|
||||
print('错误:无效的 baseId 格式')
|
||||
sys.exit(1)
|
||||
if not validate_resource_id(table_id):
|
||||
print('错误:无效的 tableId 格式')
|
||||
sys.exit(1)
|
||||
|
||||
success = bulk_add_fields(base_id, table_id, fields_file)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create an AI Table dashboard and optional common charts deterministically.
|
||||
|
||||
Examples:
|
||||
python3 create_dashboard_chart.py BASE_ID "状态分析仪表盘"
|
||||
python3 create_dashboard_chart.py BASE_ID "状态分析仪表盘" --chart-specs charts.json
|
||||
|
||||
charts.json is a JSON array. Each item accepts:
|
||||
name, chart_type, table_id, measure_type, measure_field_id,
|
||||
dimension_field_id, aggregation, view_id.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
LEDGER_SCHEMA_VERSION = "dws-skill-script-ledger/v1"
|
||||
SCRIPT_NAME = "create_dashboard_chart.py"
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{3,128}$")
|
||||
SUPPORTED_CHART_TYPES = {"AREA", "BAR", "HISTOGRAM", "LINE", "PIE", "STATISTICS"}
|
||||
SUPPORTED_AGGREGATIONS = {"sum", "count", "count_distinct", "average", "avg", "min", "max"}
|
||||
MAX_CHARTS = 6
|
||||
|
||||
|
||||
def run_dws(dws_bin: str, args: list[str]) -> tuple[Optional[dict[str, Any]], str]:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[dws_bin, *args], capture_output=True, text=True, timeout=120
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, "dws command timeout after 120 seconds"
|
||||
except FileNotFoundError:
|
||||
return None, f"dws binary not found: {dws_bin}"
|
||||
if completed.returncode != 0:
|
||||
return None, (completed.stderr or completed.stdout).strip()[:800]
|
||||
try:
|
||||
payload = json.loads(completed.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
return None, f"dws returned non-JSON output: {exc}"
|
||||
if not isinstance(payload, dict) or payload.get("status") != "success":
|
||||
return None, f"dws business failure: {payload}"
|
||||
return payload, ""
|
||||
|
||||
|
||||
def safe_json_file(value: str) -> Any:
|
||||
root = Path(os.environ.get("OPENCLAW_WORKSPACE", os.getcwd())).resolve()
|
||||
source = Path(value).expanduser()
|
||||
source = source.resolve() if source.is_absolute() else (Path.cwd() / source).resolve()
|
||||
try:
|
||||
source.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"chart specs must be inside the workspace: {source}") from exc
|
||||
if not source.is_file() or source.stat().st_size > 1024 * 1024:
|
||||
raise ValueError("chart specs must be a readable JSON file no larger than 1 MiB")
|
||||
with source.open("r", encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def validate_specs(value: Any) -> list[dict[str, Any]]:
|
||||
if not isinstance(value, list) or not value or len(value) > MAX_CHARTS:
|
||||
raise ValueError(f"chart specs must contain 1-{MAX_CHARTS} items")
|
||||
specs: list[dict[str, Any]] = []
|
||||
for index, item in enumerate(value, start=1):
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError(f"chart spec #{index} must be an object")
|
||||
name = str(item.get("name") or "").strip()
|
||||
chart_type = str(item.get("chart_type") or "").strip().upper()
|
||||
table_id = str(item.get("table_id") or "").strip()
|
||||
measure_type = str(item.get("measure_type") or "record-count").strip()
|
||||
measure_field_id = str(item.get("measure_field_id") or "").strip()
|
||||
dimension_field_id = str(item.get("dimension_field_id") or "").strip()
|
||||
aggregation = str(item.get("aggregation") or "sum").strip().lower()
|
||||
view_id = str(item.get("view_id") or "").strip()
|
||||
if not name or len(name) > 80:
|
||||
raise ValueError(f"chart spec #{index} needs a 1-80 character name")
|
||||
if chart_type not in SUPPORTED_CHART_TYPES:
|
||||
raise ValueError(f"chart spec #{index} has unsupported chart_type: {chart_type}")
|
||||
if not RESOURCE_ID_PATTERN.fullmatch(table_id):
|
||||
raise ValueError(f"chart spec #{index} has invalid table_id")
|
||||
if measure_type not in {"record-count", "field"}:
|
||||
raise ValueError(f"chart spec #{index} has invalid measure_type")
|
||||
if measure_type == "field" and not RESOURCE_ID_PATTERN.fullmatch(measure_field_id):
|
||||
raise ValueError(f"chart spec #{index} needs measure_field_id")
|
||||
if aggregation not in SUPPORTED_AGGREGATIONS:
|
||||
raise ValueError(f"chart spec #{index} has unsupported aggregation")
|
||||
for label, resource_id in (
|
||||
("dimension_field_id", dimension_field_id), ("view_id", view_id)
|
||||
):
|
||||
if resource_id and not RESOURCE_ID_PATTERN.fullmatch(resource_id):
|
||||
raise ValueError(f"chart spec #{index} has invalid {label}")
|
||||
specs.append(
|
||||
{
|
||||
"name": name,
|
||||
"chart_type": chart_type,
|
||||
"table_id": table_id,
|
||||
"measure_type": measure_type,
|
||||
"measure_field_id": measure_field_id,
|
||||
"dimension_field_id": dimension_field_id,
|
||||
"aggregation": "average" if aggregation == "avg" else aggregation,
|
||||
"view_id": view_id,
|
||||
}
|
||||
)
|
||||
return specs
|
||||
|
||||
|
||||
def chart_config(spec: dict[str, Any]) -> dict[str, Any]:
|
||||
config: dict[str, Any] = {
|
||||
"chartType": spec["chart_type"],
|
||||
"name": spec["name"],
|
||||
"sheet": spec["table_id"],
|
||||
"view": spec["view_id"] or None,
|
||||
"measureType": spec["measure_type"],
|
||||
"measure": [],
|
||||
"filter": [],
|
||||
}
|
||||
if spec["measure_type"] == "field":
|
||||
config["measure"] = [
|
||||
{
|
||||
"value": spec["measure_field_id"],
|
||||
"externalValue": [{"type": "formula", "value": spec["aggregation"]}],
|
||||
}
|
||||
]
|
||||
if spec["dimension_field_id"]:
|
||||
config["dimension"] = [
|
||||
{"value": spec["dimension_field_id"], "externalValue": []}
|
||||
]
|
||||
if spec["chart_type"] in {"AREA", "BAR", "HISTOGRAM", "LINE", "PIE"}:
|
||||
config.update({"colors": "COLOR_PALETTE_1", "legend": "top", "label": True})
|
||||
if spec["chart_type"] in {"AREA", "BAR", "HISTOGRAM", "LINE"}:
|
||||
config.update({"xAxisShow": True, "yAxisShow": True})
|
||||
if spec["chart_type"] == "PIE":
|
||||
config.update({"innerRadius": 0, "outerRadius": 60})
|
||||
return config
|
||||
|
||||
|
||||
def layout(index: int, total: int) -> dict[str, int]:
|
||||
width = 12 if total == 1 else 6 if total in {2, 3, 4} else 4
|
||||
per_row = 12 // width
|
||||
return {"x": (index % per_row) * width, "y": (index // per_row) * 5, "w": width, "h": 5}
|
||||
|
||||
|
||||
def extract_id(payload: dict[str, Any], key: str) -> str:
|
||||
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
||||
value = data.get(key)
|
||||
return str(value or "").strip()
|
||||
|
||||
|
||||
def dashboard_chart_ids(payload: dict[str, Any]) -> set[str]:
|
||||
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
||||
charts = data.get("charts") if isinstance(data.get("charts"), list) else []
|
||||
return {
|
||||
str(item.get("chartId"))
|
||||
for item in charts
|
||||
if isinstance(item, dict) and item.get("chartId")
|
||||
}
|
||||
|
||||
|
||||
def ledger_step(
|
||||
cli_path: str, status: str, params: dict[str, Any], output_ids: Optional[dict[str, str]] = None,
|
||||
error: str = "",
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"cli_path": cli_path,
|
||||
"status": status,
|
||||
"params": params,
|
||||
"output_ids": output_ids or {},
|
||||
"error": error,
|
||||
}
|
||||
|
||||
|
||||
def emit(status: str, ledger: list[dict[str, Any]], **result: Any) -> None:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"script": SCRIPT_NAME,
|
||||
"status": status,
|
||||
"result": result,
|
||||
"ledger": ledger,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("base_id", help="Target AI Table baseId")
|
||||
parser.add_argument("dashboard_name", help="Dashboard name")
|
||||
parser.add_argument("--chart-specs", help="Workspace-local JSON chart spec file")
|
||||
parser.add_argument("--dws", default="dws", help="dws executable")
|
||||
args = parser.parse_args()
|
||||
|
||||
base_id = args.base_id.strip()
|
||||
dashboard_name = args.dashboard_name.strip()
|
||||
if not RESOURCE_ID_PATTERN.fullmatch(base_id) or not dashboard_name:
|
||||
parser.error("base_id and dashboard_name are required and must be valid")
|
||||
try:
|
||||
specs = validate_specs(safe_json_file(args.chart_specs)) if args.chart_specs else []
|
||||
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
parser.error(str(exc))
|
||||
|
||||
ledger: list[dict[str, Any]] = []
|
||||
dashboard_params = {"base-id": base_id, "name": dashboard_name, "format": "json"}
|
||||
dashboard, error = run_dws(
|
||||
args.dws,
|
||||
["aitable", "dashboard", "create", "--base-id", base_id, "--name", dashboard_name, "--format", "json"],
|
||||
)
|
||||
if not dashboard:
|
||||
ledger.append(ledger_step("aitable dashboard create", "failed", dashboard_params, error=error))
|
||||
emit("failed", ledger, error=error)
|
||||
return 1
|
||||
dashboard_id = extract_id(dashboard, "dashboardId")
|
||||
if not dashboard_id:
|
||||
error = "dashboard create returned no dashboardId"
|
||||
ledger.append(ledger_step("aitable dashboard create", "failed", dashboard_params, error=error))
|
||||
emit("failed", ledger, error=error)
|
||||
return 1
|
||||
ledger.append(
|
||||
ledger_step(
|
||||
"aitable dashboard create", "success", dashboard_params,
|
||||
{"dashboardId": dashboard_id},
|
||||
)
|
||||
)
|
||||
|
||||
chart_ids: list[str] = []
|
||||
for index, spec in enumerate(specs):
|
||||
config = chart_config(spec)
|
||||
chart_layout = layout(index, len(specs))
|
||||
params = {
|
||||
"base-id": base_id,
|
||||
"dashboard-id": dashboard_id,
|
||||
"config": config,
|
||||
"layout": chart_layout,
|
||||
"format": "json",
|
||||
}
|
||||
chart, error = run_dws(
|
||||
args.dws,
|
||||
[
|
||||
"aitable", "chart", "create", "--base-id", base_id,
|
||||
"--dashboard-id", dashboard_id,
|
||||
"--config", json.dumps(config, ensure_ascii=False, separators=(",", ":")),
|
||||
"--layout", json.dumps(chart_layout, separators=(",", ":")),
|
||||
"--format", "json",
|
||||
],
|
||||
)
|
||||
if not chart:
|
||||
ledger.append(ledger_step("aitable chart create", "failed", params, error=error))
|
||||
emit("failed", ledger, dashboardId=dashboard_id, chartIds=chart_ids, error=error)
|
||||
return 1
|
||||
chart_id = extract_id(chart, "chartId")
|
||||
if not chart_id:
|
||||
error = "chart create returned no chartId"
|
||||
ledger.append(ledger_step("aitable chart create", "failed", params, error=error))
|
||||
emit("failed", ledger, dashboardId=dashboard_id, chartIds=chart_ids, error=error)
|
||||
return 1
|
||||
chart_ids.append(chart_id)
|
||||
ledger.append(
|
||||
ledger_step("aitable chart create", "success", params, {"chartId": chart_id})
|
||||
)
|
||||
|
||||
get_params = {"base-id": base_id, "dashboard-id": dashboard_id, "format": "json"}
|
||||
verified, error = run_dws(
|
||||
args.dws,
|
||||
["aitable", "dashboard", "get", "--base-id", base_id, "--dashboard-id", dashboard_id, "--format", "json"],
|
||||
)
|
||||
if not verified:
|
||||
ledger.append(ledger_step("aitable dashboard get", "failed", get_params, error=error))
|
||||
emit("failed", ledger, dashboardId=dashboard_id, chartIds=chart_ids, error=error)
|
||||
return 1
|
||||
missing_chart_ids = set(chart_ids) - dashboard_chart_ids(verified)
|
||||
if missing_chart_ids:
|
||||
error = "dashboard verification missing chartIds: " + ",".join(sorted(missing_chart_ids))
|
||||
ledger.append(ledger_step("aitable dashboard get", "failed", get_params, error=error))
|
||||
emit("failed", ledger, dashboardId=dashboard_id, chartIds=chart_ids, error=error)
|
||||
return 1
|
||||
ledger.append(
|
||||
ledger_step("aitable dashboard get", "success", get_params, {"dashboardId": dashboard_id})
|
||||
)
|
||||
emit("success", ledger, dashboardId=dashboard_id, chartIds=chart_ids)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user