Compare commits

...
Author SHA1 Message Date
修雨 cf5b76de07 chore(release): prepare v1.0.55-beta.1 2026-07-23 13:35:30 +08:00
修雨 423e16ced0 Merge pull request #767 from DingTalk-Real-AI/codex/align-chat-file-upload
fix(chat): align local file sending with Wukong
2026-07-23 13:25:10 +08:00
修雨 c1a4bd6781 fix(chat): preserve interface while retiring discovery 2026-07-23 13:00:05 +08:00
修雨 02817bc043 Merge main and complete chat media retirement 2026-07-23 12:56:11 +08:00
修雨 9f76c1844a Merge pull request #697 from FloralTide/feat/mcp-url-get
feat(mcp): add URL resolution command
2026-07-23 11:59:34 +08:00
炳昱 857d9b8c1b test(mcp): cover URL command error paths 2026-07-23 11:31:12 +08:00
炳昱 a7fdcea086 test(cli): update public interface baseline 2026-07-23 10:19:14 +08:00
炳昱 9fc63b6405 fix(mcp): expose URL command in schema 2026-07-23 00:46:14 +08:00
炳昱 3233e1fe93 feat(mcp): add URL resolution command 2026-07-23 00:46:14 +08:00
修雨 f7e61feacf Merge remote-tracking branch 'origin/main' into codex/align-chat-file-upload
# Conflicts:
#	CHANGELOG.md
2026-07-23 00:45:11 +08:00
修雨 cdc3fbe328 test(chat): cover ID routing helpers 2026-07-23 00:38:50 +08:00
Dennis4477 b4ea1f168d fix(chat): render cards, forwards and encrypted messages
Normalize message projections across read shortcuts, preserve mixed user JSON, expand forwarded records, mask ciphertext, and accept media-download message ID aliases while retaining the Cobra/Schema required contract.
2026-07-23 00:18:46 +08:00
修雨 b03017997d fix(schema): preserve chat interface contract 2026-07-23 00:11:41 +08:00
修雨 902e084d8a fix(chat): align local file sending with wukong 2026-07-23 00:03:58 +08:00
44 changed files with 2869 additions and 1020 deletions
+4
View File
@@ -62,3 +62,7 @@ dwsbin
/docs/shortcut-comparison.html
/docs/shortcut-gsb-eval.*
/scripts/run_shortcut_real_read_matrix.py
# Local coverage artifacts
coverage-shortcut.txt
coverage-*.txt
+19
View File
@@ -6,6 +6,25 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
## [Unreleased]
## [1.0.55-beta.1] - 2026-07-23
This beta validates MCP Market URL resolution, the supported Wukong local-file
send path after retiring the legacy credential-based media upload command from
discovery, and reliable message-read rendering for rich content, forwarded
records, encrypted messages, and media-download ID aliases.
### Added
- **MCP URL resolution** — adds `dws mcp url get <mcpId>` for resolving a DingTalk MCP Market ID to the current user and organization scoped Streamable HTTP URL, while keeping the helper-only `mcp-meta` endpoint out of the public product command surface.
### Changed
- **Chat local-file sending** — hides the open-source-only `chat media upload` compatibility command from Help, Schema, and bundled Skills, and removes its legacy AppKey/AppSecret OAPI path. Historical argv still receives an actionable migration error. Send local images and files through `chat message send --msg-type file --file-path`; callers that already hold a mediaId may continue to use `--msg-type image --media-id`.
### Fixed
- **Message-read shortcut projection** (#706) — the message-list shortcuts (`chat +chat-messages` / `+messages-list` / `+messages-list-direct` / `+at-me` / `+search-msg` / `+thread-replies`) now render card and out-of-office rich-content JSON as readable text (without ever rewriting ordinary text that merely embeds a JSON fragment), expand a forwarded chat record's nested `forwardMessages` instead of collapsing to a "[卡片]" summary, and mark undecryptable encrypted card messages as `[加密消息]`; the speaker is read from the bare `sender` key, nested `{name:…}` sender objects yield their display name, and the literal string `"null"` is treated as absent. Shared projection helpers now live in `internal/shortcut/chatmsg`. `chat message download-media` also gains `--msg-id` / `--open-message-id` aliases for its `--message-id` flag so agents copying the `openMessageId`/`msgId` output field no longer hit "unknown flag".
## [1.0.54] - 2026-07-21
This release promotes the validated `v1.0.54-beta.2` baseline to stable. It restores the default transport envelope for personal event output with opt-in flattening, plus Schema CLI path and plugin overlay compatibility fixes.
+108
View File
@@ -0,0 +1,108 @@
// 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 (
"encoding/json"
"fmt"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
const (
mcpMetaServerID = "mcp-meta"
mcpMetaURLTool = "get_mcp_server_url"
)
func newMCPURLGroup(caller edition.ToolCaller) *cobra.Command {
group := &cobra.Command{
Use: "url",
Short: "管理 MCP 服务连接地址",
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Help()
},
}
group.AddCommand(newMCPURLGetCommand(caller))
return group
}
func newMCPURLGetCommand(caller edition.ToolCaller) *cobra.Command {
cmd := &cobra.Command{
Use: "get <mcpId>",
Short: "按 mcpId 获取 MCP 的 Streamable HTTP 服务地址",
Long: "输入 MCP 市场 mcpId,返回以当前用户和组织身份访问该 MCP 的 " +
"Streamable HTTP 服务地址。\n\n" +
"安全提示:返回的 mcpURL 和 mcpJSON 可能包含身份凭据,仅限个人使用," +
"请勿分享到群聊、文档、邮件、代码仓库或日志。",
Example: " dws mcp url get 2480\n" +
" dws mcp url get 2480 --format json",
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
if caller == nil {
return fmt.Errorf("MCP tool caller is not configured")
}
mcpID := strings.TrimSpace(args[0])
if mcpID == "" {
return fmt.Errorf("mcpId 不能为空")
}
result, err := caller.CallTool(cmd.Context(), mcpMetaServerID, mcpMetaURLTool, map[string]any{
"mcpId": mcpID,
})
if err != nil {
return fmt.Errorf("获取 MCP 服务地址: %w", err)
}
return writeMCPURLResult(cmd, result)
},
}
cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{
Name: "mcp_id",
Type: "string",
Description: "钉钉 MCP 市场中的 mcpId",
Required: true,
Index: 0,
})
return cmd
}
func writeMCPURLResult(cmd *cobra.Command, result *edition.ToolResult) error {
if result == nil {
return fmt.Errorf("MCP 元服务返回空结果")
}
// get_mcp_server_url returns one JSON document in its first non-empty text
// block. Other block types and trailing blocks are intentionally ignored.
for _, block := range result.Content {
if block.Type != "text" || strings.TrimSpace(block.Text) == "" {
continue
}
if err := apperrors.ClassifyMCPResponseText(block.Text); err != nil {
return err
}
var payload any
if err := json.Unmarshal([]byte(block.Text), &payload); err != nil {
return fmt.Errorf("MCP 元服务返回了无效 JSON: %w", err)
}
return output.WriteCommandPayload(cmd, payload, output.FormatJSON)
}
return fmt.Errorf("MCP 元服务返回空结果")
}
+194
View File
@@ -0,0 +1,194 @@
// 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 (
"bytes"
"context"
"encoding/json"
"errors"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
type mcpURLTestCaller struct {
productID string
toolName string
args map[string]any
result *edition.ToolResult
err error
}
func (c *mcpURLTestCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
c.productID = productID
c.toolName = toolName
c.args = args
return c.result, c.err
}
func (*mcpURLTestCaller) Format() string { return "json" }
func (*mcpURLTestCaller) DryRun() bool { return false }
func (*mcpURLTestCaller) Fields() string { return "" }
func (*mcpURLTestCaller) JQ() string { return "" }
func executeMCPURLCommand(t *testing.T, caller edition.ToolCaller, args ...string) (string, error) {
t.Helper()
root := &cobra.Command{Use: "mcp", SilenceErrors: true, SilenceUsage: true}
root.AddCommand(newMCPURLGroup(caller))
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs(args)
err := root.ExecuteContext(t.Context())
return out.String(), err
}
func TestMCPURLGetCallsMetaServerAndPreservesResponse(t *testing.T) {
const response = `{"result":{"mcpURL":"https://example.test/mcp?key=one&token=two","mcpJSON":{"transport":"streamable-http"},"name":"Example"}}`
caller := &mcpURLTestCaller{
result: &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: response}}},
}
out, err := executeMCPURLCommand(t, caller, "url", "get", " 10043 ")
if err != nil {
t.Fatalf("execute mcp url get: %v", err)
}
if caller.productID != mcpMetaServerID {
t.Fatalf("productID = %q, want %q", caller.productID, mcpMetaServerID)
}
if caller.toolName != mcpMetaURLTool {
t.Fatalf("toolName = %q, want %q", caller.toolName, mcpMetaURLTool)
}
if got := caller.args["mcpId"]; got != "10043" {
t.Fatalf("mcpId = %#v, want %q", got, "10043")
}
var payload map[string]any
if err := json.Unmarshal([]byte(out), &payload); err != nil {
t.Fatalf("output is not JSON: %v\n%s", err, out)
}
result, ok := payload["result"].(map[string]any)
if !ok {
t.Fatalf("output result = %#v", payload["result"])
}
if got := result["mcpURL"]; got != "https://example.test/mcp?key=one&token=two" {
t.Fatalf("result.mcpURL = %#v", got)
}
}
func TestMCPURLGetRejectsBlankID(t *testing.T) {
_, err := executeMCPURLCommand(t, &mcpURLTestCaller{}, "url", "get", " ")
if err == nil || !strings.Contains(err.Error(), "mcpId 不能为空") {
t.Fatalf("error = %v, want blank mcpId error", err)
}
}
func TestMCPURLGroupShowsHelp(t *testing.T) {
out, err := executeMCPURLCommand(t, nil, "url")
if err != nil {
t.Fatalf("execute mcp url: %v", err)
}
if !strings.Contains(out, "get") {
t.Fatalf("help output does not list get command:\n%s", out)
}
}
func TestMCPURLGetRejectsMissingCaller(t *testing.T) {
_, err := executeMCPURLCommand(t, nil, "url", "get", "10043")
if err == nil || !strings.Contains(err.Error(), "caller is not configured") {
t.Fatalf("error = %v, want missing caller error", err)
}
}
func TestMCPURLGetPropagatesCallError(t *testing.T) {
caller := &mcpURLTestCaller{err: errors.New("permission denied")}
_, err := executeMCPURLCommand(t, caller, "url", "get", "10043")
if err == nil || !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("error = %v, want call error", err)
}
}
func TestMCPURLGetRejectsInvalidJSON(t *testing.T) {
caller := &mcpURLTestCaller{
result: &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: "not-json"}}},
}
_, err := executeMCPURLCommand(t, caller, "url", "get", "10043")
if err == nil || !strings.Contains(err.Error(), "无效 JSON") {
t.Fatalf("error = %v, want invalid JSON error", err)
}
}
func TestMCPURLGetRejectsEmptyResults(t *testing.T) {
tests := []struct {
name string
result *edition.ToolResult
}{
{name: "nil result"},
{
name: "no usable text content",
result: &edition.ToolResult{Content: []edition.ContentBlock{
{Type: "image", Text: "ignored"},
{Type: "text", Text: " "},
}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
caller := &mcpURLTestCaller{result: tt.result}
_, err := executeMCPURLCommand(t, caller, "url", "get", "10043")
if err == nil || !strings.Contains(err.Error(), "返回空结果") {
t.Fatalf("error = %v, want empty result error", err)
}
})
}
}
func TestMCPURLGetClassifiesBusinessError(t *testing.T) {
caller := &mcpURLTestCaller{
result: &edition.ToolResult{Content: []edition.ContentBlock{{
Type: "text",
Text: `{"success":false,"errorMsg":"搜索内容不能为空"}`,
}}},
}
_, err := executeMCPURLCommand(t, caller, "url", "get", "10043")
if err == nil {
t.Fatal("expected classified business error")
}
var typed *apperrors.Error
if !errors.As(err, &typed) || typed.Reason != "business_error" {
t.Fatalf("error = %#v, want classified business error", err)
}
}
func TestRootRegistersMCPURLGet(t *testing.T) {
root := NewRootCommand(t.Context())
mcp, _, err := root.Find([]string{"mcp"})
if err != nil {
t.Fatalf("find mcp: %v", err)
}
if mcp.Hidden {
t.Fatal("mcp command must be public when it contains reviewed public helpers")
}
cmd, _, err := root.Find([]string{"mcp", "url", "get"})
if err != nil {
t.Fatalf("find mcp url get: %v", err)
}
if got := cmd.CommandPath(); got != "dws mcp url get" {
t.Fatalf("command path = %q, want %q", got, "dws mcp url get")
}
}
+6 -1
View File
@@ -385,11 +385,16 @@ func newRootCommandWithEngine(rootCtx context.Context, engine *pipeline.Engine,
schemaCmd := newSchemaCommand(loader)
mcpCmd := newMCPCommand(rootCtx, loader, runner, engine)
mcpCmd.Hidden = true
// The legacy dynamic MCP surface remains disabled, but reviewed static MCP
// helpers registered below are part of the public CLI and Schema surface.
mcpCmd.Hidden = false
mcpCmd.Short = "管理 MCP 服务连接信息"
mcpCmd.Long = "管理经过审核并纳入 Schema 的 MCP 服务连接辅助能力。"
// Wrap the caller so every MCP tool call's shape is recorded to the local
// usage log (privacy-preserving; see internal/shortcut/usage). Powers
// `dws shortcut stats` and future high-frequency shortcut distillation.
patCaller := newRecordingToolCaller(newToolCallerAdapter(runner, flags))
mcpCmd.AddCommand(newMCPURLGroup(patCaller))
utilityCommands := []*cobra.Command{
newAuthCommand(patCaller),
+118 -1
View File
@@ -15,11 +15,14 @@ package app
import (
"bytes"
stderrors "errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
@@ -75,7 +78,14 @@ func TestRootKeepsMainBranchChatCompatibilityCommands(t *testing.T) {
}
}
mediaUpload := mustFindCommand(t, root, "chat", "media", "upload")
mediaGroup := mustFindCommand(t, root, "chat", "media")
if mediaGroup.Deprecated == "" || mediaGroup.Hidden || !mediaGroup.Runnable() {
t.Fatalf("chat media compatibility contract: deprecated=%q hidden=%v runnable=%v", mediaGroup.Deprecated, mediaGroup.Hidden, mediaGroup.Runnable())
}
mediaUpload := mustFindCommand(t, mediaGroup, "upload")
if mediaUpload.Deprecated == "" || mediaUpload.Hidden || !mediaUpload.Runnable() {
t.Fatalf("chat media upload compatibility contract: deprecated=%q hidden=%v runnable=%v", mediaUpload.Deprecated, mediaUpload.Hidden, mediaUpload.Runnable())
}
for _, flag := range []string{"file", "type"} {
if mediaUpload.Flags().Lookup(flag) == nil {
t.Fatalf("chat media upload missing --%s", flag)
@@ -88,6 +98,113 @@ func TestRootKeepsMainBranchChatCompatibilityCommands(t *testing.T) {
mustFindCommand(t, root, "conference", "meeting", "reserve")
}
func TestChatHelpAndSchemaHideRetiredMediaUpload(t *testing.T) {
for _, args := range [][]string{
{"chat", "--help"},
{"chat", "media", "--help"},
} {
root := NewRootCommand()
var output bytes.Buffer
root.SetOut(&output)
root.SetErr(&output)
root.SetArgs(args)
if err := root.Execute(); err != nil {
t.Fatalf("dws %s: %v\n%s", strings.Join(args, " "), err, output.String())
}
for _, line := range strings.Split(output.String(), "\n") {
fields := strings.Fields(line)
if len(fields) > 0 && (fields[0] == "media" || fields[0] == "upload") {
t.Fatalf("dws %s exposes retired command in Help line %q:\n%s", strings.Join(args, " "), line, output.String())
}
}
}
root := NewRootCommand()
var output bytes.Buffer
root.SetOut(&output)
root.SetErr(&output)
root.SetArgs([]string{"schema", "--cli-path", "chat media upload", "--format", "json"})
err := root.Execute()
if err == nil {
t.Fatalf("retired chat media upload remains queryable from Schema:\n%s", output.String())
}
if !strings.Contains(err.Error(), "unknown runtime schema path") {
t.Fatalf("retired chat media upload Schema error = %v, want unknown path", err)
}
}
func TestRootChatMediaUploadWithoutAppCredentialsReturnsMigrationValidation(t *testing.T) {
for _, key := range []string{"DWS_CLIENT_ID", "DWS_CLIENT_SECRET"} {
value, existed := os.LookupEnv(key)
if err := os.Unsetenv(key); err != nil {
t.Fatalf("unset %s: %v", key, err)
}
t.Cleanup(func() {
if existed {
_ = os.Setenv(key, value)
return
}
_ = os.Unsetenv(key)
})
if _, exists := os.LookupEnv(key); exists {
t.Fatalf("%s is still set", key)
}
}
t.Setenv("DWS_CONFIG_DIR", filepath.Join(t.TempDir(), "config"))
filePath := filepath.Join(t.TempDir(), "image.png")
if err := os.WriteFile(filePath, []byte("image"), 0o600); err != nil {
t.Fatalf("write image fixture: %v", err)
}
commandArgs := []string{
"chat", "media", "upload",
"--file", filePath,
"--type", "image",
}
previousArgs := os.Args
os.Args = append([]string{"dws"}, commandArgs...)
t.Cleanup(func() { os.Args = previousArgs })
root := NewRootCommand()
var output bytes.Buffer
root.SetOut(&output)
root.SetErr(&output)
root.SetArgs(commandArgs)
err := root.Execute()
if err == nil {
t.Fatalf("chat media upload succeeded without app credentials:\n%s", output.String())
}
var typed *apperrors.Error
if !stderrors.As(err, &typed) {
t.Fatalf("chat media upload error type = %T, want *errors.Error: %v", err, err)
}
if typed.Category != apperrors.CategoryValidation {
t.Fatalf("chat media upload category = %q, want %q", typed.Category, apperrors.CategoryValidation)
}
if exitCode := apperrors.ExitCode(err); exitCode != 3 {
t.Fatalf("chat media upload exit code = %d, want 3", exitCode)
}
got := output.String() + "\n" + err.Error()
for _, want := range []string{"已下线", "chat message send --msg-type file --file-path"} {
if !strings.Contains(got, want) {
t.Fatalf("chat media upload migration output missing %q:\n%s", want, got)
}
}
for _, forbidden := range []string{
"DWS_CLIENT_ID",
"DWS_CLIENT_SECRET",
"缺少应用凭证",
"AppSecret",
"clientSecret",
} {
if strings.Contains(got, forbidden) {
t.Fatalf("chat media upload returned credential error %q:\n%s", forbidden, got)
}
}
}
func TestRootKeepsContactWukongCompatibilityCommands(t *testing.T) {
root := NewRootCommand()
label := mustFindCommand(t, root, "contact", "label")
+81 -12
View File
@@ -1,18 +1,18 @@
{
"version": 1,
"source_hash": "sha256:5df496973c41b3b4f7ae8c856ff0e9e99bcabd4455419b7d41bb23c44df6f1af",
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
"source_hash": "sha256:0977e2b3d4af77d348318fd0cd7ce36d325ba9ca4e7c9ed0d6daeb1be0a0c4ec",
"surface_hash": "sha256:e85148774f65d34b01d50ebdc702b785afdb8ea78663b6f2eeecc35bc3cb3fd5",
"coverage": {
"surface_products": 22,
"products_with_metadata": 22,
"surface_tools": 572,
"tools_with_metadata": 572,
"tools_with_agent_summary": 572,
"tools_with_use_when": 572,
"tools_with_avoid_when": 572,
"tools_with_examples": 572,
"tools_with_interface_mode": 572,
"unmatched_skill_tools": 120,
"surface_products": 23,
"products_with_metadata": 23,
"surface_tools": 573,
"tools_with_metadata": 573,
"tools_with_agent_summary": 573,
"tools_with_use_when": 573,
"tools_with_avoid_when": 573,
"tools_with_examples": 573,
"tools_with_interface_mode": 573,
"unmatched_skill_tools": 119,
"unreviewed_skill_tools": 7
},
"products": {
@@ -1089,6 +1089,74 @@
"查收、搜索、阅读、回复、发送或整理邮件"
]
},
"mcp": {
"agent_summary": "解析和管理当前身份可用的 MCP 服务连接信息",
"agent_summary_source": "dws-agent-selection/mcp",
"avoid_when": [
"查询普通钉钉业务数据时使用对应产品命令,不要使用 mcp"
],
"field_provenance": {
"agent_summary": {
"value": "解析和管理当前身份可用的 MCP 服务连接信息",
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"candidates": [
{
"value": "解析和管理当前身份可用的 MCP 服务连接信息",
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"selected": true
}
]
},
"avoid_when": {
"value": [
"查询普通钉钉业务数据时使用对应产品命令,不要使用 mcp"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"candidates": [
{
"value": [
"查询普通钉钉业务数据时使用对应产品命令,不要使用 mcp"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"selected": true
}
]
},
"use_when": {
"value": [
"需要把钉钉 MCP 市场中的服务连接到支持 Streamable HTTP 的 Agent 或客户端"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"candidates": [
{
"value": [
"需要把钉钉 MCP 市场中的服务连接到支持 Streamable HTTP 的 Agent 或客户端"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"selected": true
}
]
}
},
"source_refs": [
"CommandRegistry:product=mcp",
"cobra-help:dws mcp --help",
"internal/cli/schema_command_registry.json#mcp",
"internal/cli/schema_hints/selection/mcp.json"
],
"use_when": [
"需要把钉钉 MCP 市场中的服务连接到支持 Streamable HTTP 的 Agent 或客户端"
]
},
"minutes": {
"agent_summary": "查询和维护钉钉听记的转写、摘要、待办、权限、录音、标签、说话人总结及文件上传会话。",
"agent_summary_source": "dws-agent-selection/minutes",
@@ -1605,6 +1673,7 @@
"event",
"live",
"mail",
"mcp",
"minutes",
"oa",
"pat",
+267
View File
@@ -0,0 +1,267 @@
{
"product_id": "mcp",
"tools": {
"mcp url get": {
"agent_summary": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址",
"agent_summary_source": "dws-agent-selection/mcp",
"availability": "available",
"avoid_when": [
"只是查询 DWS 已公开命令或参数时使用 dws schema",
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws mcp url get 10043 --format json"
],
"field_provenance": {
"agent_summary": {
"value": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址",
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"candidates": [
{
"value": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址",
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output."
}
]
},
"avoid_when": {
"value": [
"只是查询 DWS 已公开命令或参数时使用 dws schema",
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"candidates": [
{
"value": [
"只是查询 DWS 已公开命令或参数时使用 dws schema",
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output."
}
]
},
"effect": {
"value": "read",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"candidates": [
{
"value": "read",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output."
}
]
},
"examples": {
"value": [
"dws mcp url get 10043 --format json"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"candidates": [
{
"value": [
"dws mcp url get 10043 --format json"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"candidates": [
{
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output."
}
]
},
"interface_mode": {
"value": "composite",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"candidates": [
{
"value": "composite",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output."
}
]
},
"interface_reason": {
"value": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"candidates": [
{
"value": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output."
}
]
},
"interface_ref": {
"value": null,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"candidates": [
{
"value": null,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "final interface mode composite forbids a direct MCP interface_ref"
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output."
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。"
}
]
},
"risk": {
"value": "medium",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"candidates": [
{
"value": "medium",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output."
}
]
},
"use_when": {
"value": [
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"candidates": [
{
"value": [
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
],
"source": "internal/cli/schema_hints/selection/mcp.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "composite",
"interface_reason": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata.",
"reviewed": true,
"risk": "medium",
"source_refs": [
"cobra-help:dws mcp url get --help",
"internal/app/mcp_url_command.go",
"internal/cli/schema_command_registry.json#mcp.url_get",
"internal/cli/schema_hints/metadata/mcp.json",
"internal/cli/schema_hints/selection/mcp.json",
"pkg/edition/default.go#openSupplementServers"
],
"use_when": [
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
]
}
}
}
+77 -118
View File
@@ -1,11 +1,11 @@
{
"version": 1,
"source_hash": "sha256:5df496973c41b3b4f7ae8c856ff0e9e99bcabd4455419b7d41bb23c44df6f1af",
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
"source_files": 150,
"hint_files": 46,
"hint_products": 39,
"hint_tools": 1296,
"source_hash": "sha256:0977e2b3d4af77d348318fd0cd7ce36d325ba9ca4e7c9ed0d6daeb1be0a0c4ec",
"surface_hash": "sha256:e85148774f65d34b01d50ebdc702b785afdb8ea78663b6f2eeecc35bc3cb3fd5",
"source_files": 152,
"hint_files": 48,
"hint_products": 40,
"hint_tools": 1298,
"interface_metadata": {
"source": "mcp-tools-list+cli-registry",
"revision": "4574f7022c32cf4c033e9b7b4156e2fec815fed8",
@@ -27,16 +27,16 @@
]
},
"coverage": {
"surface_products": 22,
"products_with_metadata": 22,
"surface_tools": 572,
"tools_with_metadata": 572,
"tools_with_agent_summary": 572,
"tools_with_use_when": 572,
"tools_with_avoid_when": 572,
"tools_with_examples": 572,
"tools_with_interface_mode": 572,
"unmatched_skill_tools": 120,
"surface_products": 23,
"products_with_metadata": 23,
"surface_tools": 573,
"tools_with_metadata": 573,
"tools_with_agent_summary": 573,
"tools_with_use_when": 573,
"tools_with_avoid_when": 573,
"tools_with_examples": 573,
"tools_with_interface_mode": 573,
"unmatched_skill_tools": 119,
"unreviewed_skill_tools": 7
},
"source_products": [
@@ -60,6 +60,7 @@
"generate-topic-report",
"live",
"mail",
"mcp",
"meeting-followup",
"minutes",
"oa",
@@ -1504,38 +1505,10 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "chat media upload",
"source": "skills/mono/references/products/chat.md",
"line": 610,
"candidates": [
"chat bot find",
"chat bot search",
"chat category create-smart"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "chat media upload",
"source": "skills/mono/references/products/chat.md",
"line": 611,
"candidates": [
"chat bot find",
"chat bot search",
"chat category create-smart"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "chat message list-emotion-replies",
"source": "skills/mono/references/products/chat.md",
"line": 1086,
"line": 1061,
"candidates": [
"chat message add-emoji",
"chat message add-favorite",
@@ -1549,7 +1522,7 @@
{
"tool_path": "chat category create",
"source": "skills/mono/references/products/chat.md",
"line": 1441,
"line": 1416,
"candidates": [
"chat group create",
"chat category create-smart",
@@ -1563,7 +1536,7 @@
{
"tool_path": "chat category delete",
"source": "skills/mono/references/products/chat.md",
"line": 1466,
"line": 1441,
"candidates": [
"chat category create-smart",
"chat category list",
@@ -1577,7 +1550,7 @@
{
"tool_path": "chat category rename",
"source": "skills/mono/references/products/chat.md",
"line": 1477,
"line": 1452,
"candidates": [
"chat group rename",
"chat category create-smart",
@@ -1591,7 +1564,7 @@
{
"tool_path": "chat category add-conv",
"source": "skills/mono/references/products/chat.md",
"line": 1489,
"line": 1464,
"candidates": [
"chat category create-smart",
"chat category list",
@@ -1605,7 +1578,7 @@
{
"tool_path": "chat category remove-conv",
"source": "skills/mono/references/products/chat.md",
"line": 1502,
"line": 1477,
"candidates": [
"chat category create-smart",
"chat category list",
@@ -1619,7 +1592,7 @@
{
"tool_path": "chat text translate",
"source": "skills/mono/references/products/chat.md",
"line": 1517,
"line": 1492,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1633,7 +1606,7 @@
{
"tool_path": "chat text translate",
"source": "skills/mono/references/products/chat.md",
"line": 1518,
"line": 1493,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1647,7 +1620,7 @@
{
"tool_path": "chat text translate",
"source": "skills/mono/references/products/chat.md",
"line": 1519,
"line": 1494,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1661,7 +1634,7 @@
{
"tool_path": "chat hide",
"source": "skills/mono/references/products/chat.md",
"line": 1556,
"line": 1531,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1675,7 +1648,7 @@
{
"tool_path": "chat hide",
"source": "skills/mono/references/products/chat.md",
"line": 1557,
"line": 1532,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1689,7 +1662,7 @@
{
"tool_path": "chat mute-at-all",
"source": "skills/mono/references/products/chat.md",
"line": 1579,
"line": 1554,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1703,7 +1676,7 @@
{
"tool_path": "chat mute-at-all",
"source": "skills/mono/references/products/chat.md",
"line": 1580,
"line": 1555,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1717,7 +1690,7 @@
{
"tool_path": "chat mute-red-envelope",
"source": "skills/mono/references/products/chat.md",
"line": 1602,
"line": 1577,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1731,7 +1704,7 @@
{
"tool_path": "chat mute-red-envelope",
"source": "skills/mono/references/products/chat.md",
"line": 1603,
"line": 1578,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1745,7 +1718,7 @@
{
"tool_path": "chat mark-unread",
"source": "skills/mono/references/products/chat.md",
"line": 1623,
"line": 1598,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1759,7 +1732,7 @@
{
"tool_path": "chat mark-unread",
"source": "skills/mono/references/products/chat.md",
"line": 1624,
"line": 1599,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1773,7 +1746,7 @@
{
"tool_path": "chat clear-red-point",
"source": "skills/mono/references/products/chat.md",
"line": 1642,
"line": 1617,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1787,7 +1760,7 @@
{
"tool_path": "chat clear-red-point",
"source": "skills/mono/references/products/chat.md",
"line": 1643,
"line": 1618,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1801,7 +1774,7 @@
{
"tool_path": "chat clear-all-red-point",
"source": "skills/mono/references/products/chat.md",
"line": 1659,
"line": 1634,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1815,7 +1788,7 @@
{
"tool_path": "chat clear-all-red-point",
"source": "skills/mono/references/products/chat.md",
"line": 1661,
"line": 1636,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1829,7 +1802,7 @@
{
"tool_path": "chat list-all-conversations",
"source": "skills/mono/references/products/chat.md",
"line": 1675,
"line": 1650,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1843,7 +1816,7 @@
{
"tool_path": "chat list-all-conversations",
"source": "skills/mono/references/products/chat.md",
"line": 1676,
"line": 1651,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1857,7 +1830,7 @@
{
"tool_path": "chat list-all-conversations",
"source": "skills/mono/references/products/chat.md",
"line": 1677,
"line": 1652,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1871,7 +1844,7 @@
{
"tool_path": "chat list-all-conversations",
"source": "skills/mono/references/products/chat.md",
"line": 1678,
"line": 1653,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1885,7 +1858,7 @@
{
"tool_path": "chat clear-messages",
"source": "skills/mono/references/products/chat.md",
"line": 1698,
"line": 1673,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1899,7 +1872,7 @@
{
"tool_path": "chat clear-messages",
"source": "skills/mono/references/products/chat.md",
"line": 1699,
"line": 1674,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1913,7 +1886,7 @@
{
"tool_path": "chat mark-read",
"source": "skills/mono/references/products/chat.md",
"line": 1717,
"line": 1692,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1927,7 +1900,7 @@
{
"tool_path": "chat mark-read",
"source": "skills/mono/references/products/chat.md",
"line": 1718,
"line": 1693,
"candidates": [
"chat bot find",
"chat bot search",
@@ -1941,7 +1914,7 @@
{
"tool_path": "chat group list-join-validations",
"source": "skills/mono/references/products/chat.md",
"line": 1761,
"line": 1736,
"candidates": [
"chat group bots",
"chat group create",
@@ -1955,7 +1928,7 @@
{
"tool_path": "chat group list-join-validations",
"source": "skills/mono/references/products/chat.md",
"line": 1762,
"line": 1737,
"candidates": [
"chat group bots",
"chat group create",
@@ -1969,7 +1942,7 @@
{
"tool_path": "chat group list-join-validations",
"source": "skills/mono/references/products/chat.md",
"line": 1763,
"line": 1738,
"candidates": [
"chat group bots",
"chat group create",
@@ -1983,7 +1956,7 @@
{
"tool_path": "chat group audit-join-validation",
"source": "skills/mono/references/products/chat.md",
"line": 1783,
"line": 1758,
"candidates": [
"chat group bots",
"chat group create",
@@ -1997,7 +1970,7 @@
{
"tool_path": "chat group audit-join-validation",
"source": "skills/mono/references/products/chat.md",
"line": 1784,
"line": 1759,
"candidates": [
"chat group bots",
"chat group create",
@@ -2011,7 +1984,7 @@
{
"tool_path": "chat group members",
"source": "skills/mono/references/products/chat.md",
"line": 1816,
"line": 1791,
"candidates": [
"chat group members add",
"chat group members add-bot",
@@ -2025,7 +1998,7 @@
{
"tool_path": "chat group update-alias",
"source": "skills/mono/references/products/chat.md",
"line": 1821,
"line": 1796,
"candidates": [
"chat group bots",
"chat group create",
@@ -2039,7 +2012,7 @@
{
"tool_path": "chat group update-nick",
"source": "skills/mono/references/products/chat.md",
"line": 1822,
"line": 1797,
"candidates": [
"chat group bots",
"chat group create",
@@ -2053,7 +2026,7 @@
{
"tool_path": "chat group members list-by-ids",
"source": "skills/mono/references/products/chat.md",
"line": 1823,
"line": 1798,
"candidates": [
"chat message list-by-ids",
"chat group members add",
@@ -2067,7 +2040,7 @@
{
"tool_path": "chat category create",
"source": "skills/mono/references/products/chat.md",
"line": 1847,
"line": 1822,
"candidates": [
"chat group create",
"chat category create-smart",
@@ -2081,7 +2054,7 @@
{
"tool_path": "chat category delete",
"source": "skills/mono/references/products/chat.md",
"line": 1849,
"line": 1824,
"candidates": [
"chat category create-smart",
"chat category list",
@@ -2095,7 +2068,7 @@
{
"tool_path": "chat category rename",
"source": "skills/mono/references/products/chat.md",
"line": 1850,
"line": 1825,
"candidates": [
"chat group rename",
"chat category create-smart",
@@ -2109,7 +2082,7 @@
{
"tool_path": "chat category add-conv",
"source": "skills/mono/references/products/chat.md",
"line": 1851,
"line": 1826,
"candidates": [
"chat category create-smart",
"chat category list",
@@ -2123,7 +2096,7 @@
{
"tool_path": "chat category remove-conv",
"source": "skills/mono/references/products/chat.md",
"line": 1852,
"line": 1827,
"candidates": [
"chat category create-smart",
"chat category list",
@@ -2137,7 +2110,7 @@
{
"tool_path": "chat group share-invite",
"source": "skills/mono/references/products/chat.md",
"line": 1863,
"line": 1838,
"candidates": [
"chat group bots",
"chat group create",
@@ -2151,7 +2124,7 @@
{
"tool_path": "chat group notice create/edit/get/list",
"source": "skills/mono/references/products/chat.md",
"line": 1864,
"line": 1839,
"candidates": [
"chat group bots",
"chat group create",
@@ -2165,7 +2138,7 @@
{
"tool_path": "chat message list-emotion-replies",
"source": "skills/mono/references/products/chat.md",
"line": 1866,
"line": 1841,
"candidates": [
"chat message add-emoji",
"chat message add-favorite",
@@ -2179,7 +2152,7 @@
{
"tool_path": "chat hide",
"source": "skills/mono/references/products/chat.md",
"line": 1873,
"line": 1848,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2193,7 +2166,7 @@
{
"tool_path": "chat mute-at-all",
"source": "skills/mono/references/products/chat.md",
"line": 1874,
"line": 1849,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2207,7 +2180,7 @@
{
"tool_path": "chat mute-at-all",
"source": "skills/mono/references/products/chat.md",
"line": 1875,
"line": 1850,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2221,7 +2194,7 @@
{
"tool_path": "chat mute-red-envelope",
"source": "skills/mono/references/products/chat.md",
"line": 1876,
"line": 1851,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2235,7 +2208,7 @@
{
"tool_path": "chat mute-red-envelope",
"source": "skills/mono/references/products/chat.md",
"line": 1877,
"line": 1852,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2249,7 +2222,7 @@
{
"tool_path": "chat mark-unread",
"source": "skills/mono/references/products/chat.md",
"line": 1878,
"line": 1853,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2263,7 +2236,7 @@
{
"tool_path": "chat mark-read",
"source": "skills/mono/references/products/chat.md",
"line": 1879,
"line": 1854,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2277,7 +2250,7 @@
{
"tool_path": "chat clear-red-point",
"source": "skills/mono/references/products/chat.md",
"line": 1880,
"line": 1855,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2291,7 +2264,7 @@
{
"tool_path": "chat clear-all-red-point",
"source": "skills/mono/references/products/chat.md",
"line": 1881,
"line": 1856,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2305,7 +2278,7 @@
{
"tool_path": "chat list-all-conversations",
"source": "skills/mono/references/products/chat.md",
"line": 1882,
"line": 1857,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2319,7 +2292,7 @@
{
"tool_path": "chat clear-messages",
"source": "skills/mono/references/products/chat.md",
"line": 1883,
"line": 1858,
"candidates": [
"chat bot find",
"chat bot search",
@@ -2333,7 +2306,7 @@
{
"tool_path": "chat group list-join-validations",
"source": "skills/mono/references/products/chat.md",
"line": 1885,
"line": 1860,
"candidates": [
"chat group bots",
"chat group create",
@@ -2347,7 +2320,7 @@
{
"tool_path": "chat group audit-join-validation",
"source": "skills/mono/references/products/chat.md",
"line": 1886,
"line": 1861,
"candidates": [
"chat group bots",
"chat group create",
@@ -2358,24 +2331,10 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "chat media upload",
"source": "skills/mono/references/products/chat.md",
"line": 1893,
"candidates": [
"chat bot find",
"chat bot search",
"chat category create-smart"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "chat text translate",
"source": "skills/mono/references/products/chat.md",
"line": 1905,
"line": 1881,
"candidates": [
"chat bot find",
"chat bot search",
+461 -25
View File
@@ -1,21 +1,21 @@
{
"version": 1,
"source_hash": "sha256:14398788381c822f208e8cae059d98cd60da6625023869c74015735650d67e6f",
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
"source_hash": "sha256:360114d03cc3e8dc6cef30c6e617cc40f88e5577c29dd5b5946fe1cde91eb9de",
"surface_hash": "sha256:e85148774f65d34b01d50ebdc702b785afdb8ea78663b6f2eeecc35bc3cb3fd5",
"catalog": {
"agent_metadata": {
"products_with_metadata": 22,
"products_with_metadata": 23,
"source": "embedded-skill-metadata",
"source_hash": "sha256:5df496973c41b3b4f7ae8c856ff0e9e99bcabd4455419b7d41bb23c44df6f1af",
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
"surface_products": 22,
"surface_tools": 572,
"tools_with_agent_summary": 572,
"tools_with_metadata": 572,
"unmatched_skill_tools": 120,
"source_hash": "sha256:0977e2b3d4af77d348318fd0cd7ce36d325ba9ca4e7c9ed0d6daeb1be0a0c4ec",
"surface_hash": "sha256:e85148774f65d34b01d50ebdc702b785afdb8ea78663b6f2eeecc35bc3cb3fd5",
"surface_products": 23,
"surface_tools": 573,
"tools_with_agent_summary": 573,
"tools_with_metadata": 573,
"unmatched_skill_tools": 119,
"version": 1
},
"count": 22,
"count": 23,
"interface_metadata": {
"coverage": {
"aliased_tools": 178,
@@ -7660,7 +7660,7 @@
"cli_name": "send",
"cli_path": "chat message send",
"confirmation": "not_required",
"description": "以当前用户身份发送消息。\n\n⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。\n\n目标选择(三选一,必填):\n --group 群聊 openconversation_id\n --user 单聊接收人 userId\n --open-dingtalk-id 单聊接收人 openDingTalkId\n\n纯文本 / Markdown 消息(默认):\n 无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。\n\n富媒体消息(通过 --msg-type 指定类型):\n image — 发送图片:--msg-type image --media-id(通过 dt_media_upload 上传获得)\n file/audio/video — 发送文件、音频、视频:传本地 --file-path,CLI 会上传后按 file 消息发送",
"description": "以当前用户身份发送消息。\n\n⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。\n\n目标选择(三选一,必填):\n --group 群聊 openconversation_id\n --user 单聊接收人 userId\n --open-dingtalk-id 单聊接收人 openDingTalkId\n\n纯文本 / Markdown 消息(默认):\n 无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。\n\n本地图片 / 文件消息:\n 统一使用 --msg-type file --file-path \u003c本地路径\u003e。CLI 会完成上传并按 file 消息发送;\n .png/.jpg 也会显示为可下载的文件附件,不会生成 mediaId 或渲染成内联图片。\n\n旧版内联图片消息:\n 仅当上游已经提供有效 mediaId 时,使用 --msg-type image --media-id。\n 当前 CLI 不提供本地文件到 mediaId 的上传能力。",
"effect": "write",
"group": "message",
"idempotency": "unknown",
@@ -13476,6 +13476,110 @@
"查收、搜索、阅读、回复、发送或整理邮件"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
"CommandRegistry:product=mcp",
"cobra-help:dws mcp --help",
"internal/cli/schema_command_registry.json#mcp",
"internal/cli/schema_hints/selection/mcp.json"
],
"agent_summary": "解析和管理当前身份可用的 MCP 服务连接信息",
"agent_summary_source": "dws-agent-selection/mcp",
"avoid_when": [
"查询普通钉钉业务数据时使用对应产品命令,不要使用 mcp"
],
"description": "管理 MCP 服务连接信息",
"field_provenance": {
"agent_summary": {
"candidates": [
{
"precedence": "reviewed_explicit",
"selected": true,
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": "解析和管理当前身份可用的 MCP 服务连接信息"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": "解析和管理当前身份可用的 MCP 服务连接信息"
},
"avoid_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"selected": true,
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"查询普通钉钉业务数据时使用对应产品命令,不要使用 mcp"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"查询普通钉钉业务数据时使用对应产品命令,不要使用 mcp"
]
},
"use_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"selected": true,
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"需要把钉钉 MCP 市场中的服务连接到支持 Streamable HTTP 的 Agent 或客户端"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"需要把钉钉 MCP 市场中的服务连接到支持 Streamable HTTP 的 Agent 或客户端"
]
}
},
"id": "mcp",
"name": "管理 MCP 服务连接信息",
"runtime": true,
"tool_count": 1,
"tools": [
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址",
"agent_summary_source": "dws-agent-selection/mcp",
"availability": "available",
"avoid_when": [
"只是查询 DWS 已公开命令或参数时使用 dws schema",
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
],
"canonical_path": "mcp.url_get",
"cli_name": "get",
"cli_path": "mcp url get",
"confirmation": "not_required",
"description": "输入 MCP 市场 mcpId,返回以当前用户和组织身份访问该 MCP 的 Streamable HTTP 服务地址。\n\n安全提示:返回的 mcpURL 和 mcpJSON 可能包含身份凭据,仅限个人使用,请勿分享到群聊、文档、邮件、代码仓库或日志。",
"effect": "read",
"group": "url",
"idempotency": "idempotent",
"interface_mode": "composite",
"interface_reason": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata.",
"name": "url_get",
"primary_cli_path": "mcp url get",
"reviewed": true,
"risk": "medium",
"title": "按 mcpId 获取 MCP 的 Streamable HTTP 服务地址",
"use_when": [
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
]
}
],
"use_when": [
"需要把钉钉 MCP 市场中的服务连接到支持 Streamable HTTP 的 Agent 或客户端"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
@@ -19153,7 +19257,7 @@
}
],
"source": "embedded-command-catalog",
"tool_count": 572
"tool_count": 573
},
"tools": {
"aisearch.enterprise_person_search": {
@@ -196483,7 +196587,7 @@
]
]
},
"description": "以当前用户身份发送消息。\n\n⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。\n\n目标选择(三选一,必填):\n --group 群聊 openconversation_id\n --user 单聊接收人 userId\n --open-dingtalk-id 单聊接收人 openDingTalkId\n\n纯文本 / Markdown 消息(默认):\n 无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。\n\n富媒体消息(通过 --msg-type 指定类型):\n image — 发送图片:--msg-type image --media-id(通过 dt_media_upload 上传获得)\n file/audio/video — 发送文件、音频、视频:传本地 --file-path,CLI 会上传后按 file 消息发送",
"description": "以当前用户身份发送消息。\n\n⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。\n\n目标选择(三选一,必填):\n --group 群聊 openconversation_id\n --user 单聊接收人 userId\n --open-dingtalk-id 单聊接收人 openDingTalkId\n\n纯文本 / Markdown 消息(默认):\n 无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。\n\n本地图片 / 文件消息:\n 统一使用 --msg-type file --file-path \u003c本地路径\u003e。CLI 会完成上传并按 file 消息发送;\n .png/.jpg 也会显示为可下载的文件附件,不会生成 mediaId 或渲染成内联图片。\n\n旧版内联图片消息:\n 仅当上游已经提供有效 mediaId 时,使用 --msg-type image --media-id。\n 当前 CLI 不提供本地文件到 mediaId 的上传能力。",
"display": "群聊 / 消息 / 机器人",
"effect": "write",
"effect_source": "agent-hint",
@@ -196600,7 +196704,7 @@
"precedence": "cobra_help",
"selected": true,
"source": "cobra_help",
"value": "以当前用户身份发送消息。\n\n⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。\n\n目标选择(三选一,必填):\n --group 群聊 openconversation_id\n --user 单聊接收人 userId\n --open-dingtalk-id 单聊接收人 openDingTalkId\n\n纯文本 / Markdown 消息(默认):\n 无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。\n\n富媒体消息(通过 --msg-type 指定类型):\n image — 发送图片:--msg-type image --media-id(通过 dt_media_upload 上传获得)\n file/audio/video — 发送文件、音频、视频:传本地 --file-path,CLI 会上传后按 file 消息发送"
"value": "以当前用户身份发送消息。\n\n⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。\n\n目标选择(三选一,必填):\n --group 群聊 openconversation_id\n --user 单聊接收人 userId\n --open-dingtalk-id 单聊接收人 openDingTalkId\n\n纯文本 / Markdown 消息(默认):\n 无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。\n\n本地图片 / 文件消息:\n 统一使用 --msg-type file --file-path \u003c本地路径\u003e。CLI 会完成上传并按 file 消息发送;\n .png/.jpg 也会显示为可下载的文件附件,不会生成 mediaId 或渲染成内联图片。\n\n旧版内联图片消息:\n 仅当上游已经提供有效 mediaId 时,使用 --msg-type image --media-id。\n 当前 CLI 不提供本地文件到 mediaId 的上传能力。"
},
{
"precedence": "mcp_metadata",
@@ -196612,7 +196716,7 @@
"precedence": "cobra_help",
"resolution": "highest_precedence",
"source": "cobra_help",
"value": "以当前用户身份发送消息。\n\n⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。\n\n目标选择(三选一,必填):\n --group 群聊 openconversation_id\n --user 单聊接收人 userId\n --open-dingtalk-id 单聊接收人 openDingTalkId\n\n纯文本 / Markdown 消息(默认):\n 无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。\n\n富媒体消息(通过 --msg-type 指定类型):\n image — 发送图片:--msg-type image --media-id(通过 dt_media_upload 上传获得)\n file/audio/video — 发送文件、音频、视频:传本地 --file-path,CLI 会上传后按 file 消息发送"
"value": "以当前用户身份发送消息。\n\n⚠️ 重要:该接口会真实发送消息到目标会话,不可用于测试或试探性调用。调用前必须确认消息内容和接收对象无误。\n\n目标选择(三选一,必填):\n --group 群聊 openconversation_id\n --user 单聊接收人 userId\n --open-dingtalk-id 单聊接收人 openDingTalkId\n\n纯文本 / Markdown 消息(默认):\n 无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。\n\n本地图片 / 文件消息:\n 统一使用 --msg-type file --file-path \u003c本地路径\u003e。CLI 会完成上传并按 file 消息发送;\n .png/.jpg 也会显示为可下载的文件附件,不会生成 mediaId 或渲染成内联图片。\n\n旧版内联图片消息:\n 仅当上游已经提供有效 mediaId 时,使用 --msg-type image --media-id。\n 当前 CLI 不提供本地文件到 mediaId 的上传能力。"
},
"effect": {
"candidates": [
@@ -197176,7 +197280,7 @@
"type": "string"
},
"file-path": {
"description": "本地文件路径(msgType=file 时可直接上传发送)",
"description": "本地文件路径(msgType=file/audio/video 时直接上传并按 file 消息发送)",
"field_provenance": {
"description": {
"candidates": [
@@ -197184,7 +197288,7 @@
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "本地文件路径(msgType=file 时可直接上传发送)"
"value": "本地文件路径(msgType=file/audio/video 时直接上传并按 file 消息发送)"
},
{
"precedence": "default",
@@ -197196,7 +197300,7 @@
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "本地文件路径(msgType=file 时可直接上传发送)"
"value": "本地文件路径(msgType=file/audio/video 时直接上传并按 file 消息发送)"
},
"format": {
"candidates": [
@@ -197398,7 +197502,7 @@
"type": "string"
},
"media-id": {
"description": "图片 mediaId(通过 dt_media_upload 上传后用 extract_media_id.py 提取,仅 msgType=image)",
"description": "上游已提供的图片 mediaId(仅旧版 msgType=image;CLI 不提供本地上传到 mediaId)",
"example": "@lADP_schema_smoke",
"field_provenance": {
"description": {
@@ -197407,7 +197511,7 @@
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "图片 mediaId(通过 dt_media_upload 上传后用 extract_media_id.py 提取,仅 msgType=image)"
"value": "上游已提供的图片 mediaId(仅旧版 msgType=image;CLI 不提供本地上传到 mediaId)"
},
{
"precedence": "default",
@@ -197419,7 +197523,7 @@
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "图片 mediaId(通过 dt_media_upload 上传后用 extract_media_id.py 提取,仅 msgType=image)"
"value": "上游已提供的图片 mediaId(仅旧版 msgType=image;CLI 不提供本地上传到 mediaId)"
},
"example": {
"candidates": [
@@ -197517,7 +197621,7 @@
"type": "string"
},
"msg-type": {
"description": "富媒体消息类型: image/file/audio/video(audio/video 是 file 别名;纯文本/Markdown 无需指定,直接传内容即可)",
"description": "富媒体消息类型: image/file/audio/video(本地图片/文件推荐 file --file-path;image 仅接受已有 mediaId)",
"enum": [
"image",
"file",
@@ -197531,7 +197635,7 @@
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "富媒体消息类型: image/file/audio/video(audio/video 是 file 别名;纯文本/Markdown 无需指定,直接传内容即可)"
"value": "富媒体消息类型: image/file/audio/video(本地图片/文件推荐 file --file-path;image 仅接受已有 mediaId)"
},
{
"precedence": "mcp_metadata",
@@ -197549,7 +197653,7 @@
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "富媒体消息类型: image/file/audio/video(audio/video 是 file 别名;纯文本/Markdown 无需指定,直接传内容即可)"
"value": "富媒体消息类型: image/file/audio/video(本地图片/文件推荐 file --file-path;image 仅接受已有 mediaId)"
},
"enum": {
"candidates": [
@@ -319775,6 +319879,338 @@
"已知 templateId 并需要修改名称、主题、正文或收件人时"
]
},
"mcp.url_get": {
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
"cobra-help:dws mcp url get --help",
"internal/app/mcp_url_command.go",
"internal/cli/schema_command_registry.json#mcp.url_get",
"internal/cli/schema_hints/metadata/mcp.json",
"internal/cli/schema_hints/selection/mcp.json",
"pkg/edition/default.go#openSupplementServers"
],
"agent_summary": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址",
"agent_summary_source": "dws-agent-selection/mcp",
"availability": "available",
"avoid_when": [
"只是查询 DWS 已公开命令或参数时使用 dws schema",
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
],
"canonical_path": "mcp.url_get",
"cli_name": "get",
"cli_path": "mcp url get",
"confirmation": "not_required",
"description": "输入 MCP 市场 mcpId,返回以当前用户和组织身份访问该 MCP 的 Streamable HTTP 服务地址。\n\n安全提示:返回的 mcpURL 和 mcpJSON 可能包含身份凭据,仅限个人使用,请勿分享到群聊、文档、邮件、代码仓库或日志。",
"display": "管理 MCP 服务连接信息",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws mcp url get 10043 --format json"
],
"field_provenance": {
"agent_summary": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"selected": true,
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址"
},
"availability": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "available"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "available"
},
"avoid_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"selected": true,
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"只是查询 DWS 已公开命令或参数时使用 dws schema",
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"只是查询 DWS 已公开命令或参数时使用 dws schema",
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
]
},
"canonical_path": {
"candidates": [
{
"precedence": "command_registry",
"selected": true,
"source": "reviewed_command_registry",
"source_ref": "mcp url get",
"value": "mcp.url_get"
}
],
"precedence": "command_registry",
"resolution": "registry_identity",
"source": "reviewed_command_registry",
"source_ref": "mcp url get",
"value": "mcp.url_get"
},
"confirmation": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "not_required"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "not_required"
},
"description": {
"candidates": [
{
"precedence": "cobra_help",
"selected": true,
"source": "cobra_help",
"value": "输入 MCP 市场 mcpId,返回以当前用户和组织身份访问该 MCP 的 Streamable HTTP 服务地址。\n\n安全提示:返回的 mcpURL 和 mcpJSON 可能包含身份凭据,仅限个人使用,请勿分享到群聊、文档、邮件、代码仓库或日志。"
}
],
"precedence": "cobra_help",
"resolution": "highest_precedence",
"source": "cobra_help",
"value": "输入 MCP 市场 mcpId,返回以当前用户和组织身份访问该 MCP 的 Streamable HTTP 服务地址。\n\n安全提示:返回的 mcpURL 和 mcpJSON 可能包含身份凭据,仅限个人使用,请勿分享到群聊、文档、邮件、代码仓库或日志。"
},
"effect": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "read"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "read"
},
"examples": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"selected": true,
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"dws mcp url get 10043 --format json"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"dws mcp url get 10043 --format json"
]
},
"idempotency": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "idempotent"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "idempotent"
},
"interface_mode": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "composite"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "composite"
},
"interface_reason": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata."
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata."
},
"interface_ref": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"selected": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": null
}
],
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": null
},
"reviewed": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": true
},
{
"precedence": "reviewed_explicit",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"selected": false,
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": true
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": true
},
"risk": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "medium"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
"source": "internal/cli/schema_hints/metadata/mcp.json",
"value": "medium"
},
"title": {
"candidates": [
{
"precedence": "cobra_help",
"selected": true,
"source": "cobra_help",
"value": "按 mcpId 获取 MCP 的 Streamable HTTP 服务地址"
}
],
"precedence": "cobra_help",
"resolution": "highest_precedence",
"source": "cobra_help",
"value": "按 mcpId 获取 MCP 的 Streamable HTTP 服务地址"
},
"use_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"selected": true,
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"source": "internal/cli/schema_hints/selection/mcp.json",
"value": [
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
]
}
},
"group": "url",
"has_parameters": false,
"idempotency": "idempotent",
"interface_mode": "composite",
"interface_reason": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata.",
"is_alias": false,
"name": "url_get",
"parameter_count": 0,
"parameters": {},
"path": "mcp.url_get",
"positionals": [
{
"description": "钉钉 MCP 市场中的 mcpId",
"index": 0,
"name": "mcp_id",
"required": true,
"type": "string"
}
],
"primary_cli_path": "mcp url get",
"product_id": "mcp",
"reviewed": true,
"risk": "medium",
"source": "reviewed_command_registry",
"title": "按 mcpId 获取 MCP 的 Streamable HTTP 服务地址",
"use_when": [
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
]
},
"minutes.add_member_permission": {
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
@@ -321,7 +321,6 @@
"chat list-all-conversations",
"chat mark-read",
"chat mark-unread",
"chat media upload",
"chat message list-emotion-replies",
"chat message set-top-msg",
"chat message unset-top-msg",
@@ -1700,6 +1700,15 @@
}
]
},
{
"id": "mcp",
"tools": [
{
"canonical_path": "mcp.url_get",
"cli_path": "mcp url get"
}
]
},
{
"id": "minutes",
"tools": [
+2 -4
View File
@@ -23,6 +23,7 @@
"event": "metadata/event.json",
"live": "metadata/live.json",
"mail": "metadata/mail.json",
"mcp": "metadata/mcp.json",
"minutes": "metadata/minutes.json",
"oa": "metadata/oa.json",
"pat": "metadata/pat.json",
@@ -47,6 +48,7 @@
"event": "selection/event.json",
"live": "selection/live.json",
"mail": "selection/mail.json",
"mcp": "selection/mcp.json",
"minutes": "selection/minutes.json",
"oa": "selection/oa.json",
"pat": "selection/pat.json",
@@ -434,10 +436,6 @@
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
},
"chat media upload": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
},
"chat message list-emotion-replies": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
@@ -0,0 +1,24 @@
{
"version": 1,
"source": {
"kind": "explicit",
"name": "dws-tool-metadata/mcp",
"repository": "DingTalk-Real-AI/dingtalk-workspace-cli",
"channel": "open-source",
"reviewed": true
},
"tools": {
"mcp.url_get": {
"effect": "read",
"risk": "medium",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata.",
"runtime_gate": "none",
"reviewed": true,
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output."
}
}
}
@@ -385,10 +385,6 @@
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
},
"chat media upload": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
},
"chat message list-emotion-replies": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
@@ -7,7 +7,7 @@
"channel": "open-source"
},
"coverage": {
"source_tools": 572,
"source_tools": 573,
"matched_tools": 71
},
"tools": {
@@ -0,0 +1,51 @@
{
"version": 1,
"source": {
"kind": "explicit",
"name": "dws-agent-selection/mcp",
"repository": "DingTalk-Real-AI/dingtalk-workspace-cli",
"channel": "open-source",
"reviewed": true
},
"tools": {
"mcp.url_get": {
"agent_summary": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址",
"use_when": [
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
],
"avoid_when": [
"只是查询 DWS 已公开命令或参数时使用 dws schema",
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
],
"examples": [
"dws mcp url get 10043 --format json"
],
"reviewed": true,
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
"source_refs": [
"internal/cli/schema_command_registry.json#mcp.url_get",
"cobra-help:dws mcp url get --help",
"internal/app/mcp_url_command.go",
"pkg/edition/default.go#openSupplementServers"
]
}
},
"products": {
"mcp": {
"agent_summary": "解析和管理当前身份可用的 MCP 服务连接信息",
"use_when": [
"需要把钉钉 MCP 市场中的服务连接到支持 Streamable HTTP 的 Agent 或客户端"
],
"avoid_when": [
"查询普通钉钉业务数据时使用对应产品命令,不要使用 mcp"
],
"reviewed": true,
"review_reason": "为公开 mcp 命令提供产品级 Agent 路由,并强调连接信息属于敏感凭据。",
"source_refs": [
"internal/cli/schema_command_registry.json#mcp",
"cobra-help:dws mcp --help",
"CommandRegistry:product=mcp"
]
}
}
}
+42 -10
View File
@@ -1433,19 +1433,25 @@ func newChatCommand() *cobra.Command {
纯文本 / Markdown 消息(默认):
无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。
富媒体消息(通过 --msg-type 指定类型):
image — 发送图片:--msg-type image --media-id(通过 dt_media_upload 上传获得)
file/audio/video — 发送文件、音频、视频:传本地 --file-path,CLI 会上传后按 file 消息发送`,
本地图片 / 文件消息:
统一使用 --msg-type file --file-path <本地路径>。CLI 会完成上传并按 file 消息发送;
.png/.jpg 也会显示为可下载的文件附件,不会生成 mediaId 或渲染成内联图片。
旧版内联图片消息:
仅当上游已经提供有效 mediaId 时,使用 --msg-type image --media-id。
当前 CLI 不提供本地文件到 mediaId 的上传能力。`,
Example: ` dws chat message send --group <openconversation_id> "hello"
dws chat message send --user <userId> "请查收"
dws chat message send --open-dingtalk-id <openDingTalkId> "请查收"
dws chat message send --group <openconversation_id> --title "周报提醒" "请大家本周五前提交周报"
# 发送图片
dws chat message send --group <openconversation_id> --msg-type image --media-id <mediaId>
# 发送本地文件/音频/视频(audio/video 是 file 的语义别名)
# 发送本地图片或文件(图片会作为可下载的 file 附件发送)
dws chat message send --group <openconversation_id> --msg-type file --file-path ./screenshot.png
dws chat message send --group <openconversation_id> --msg-type file --file-path ./report.pdf
# 发送本地音频/视频(audio/video 是 file 的语义别名)
dws chat message send --group <openconversation_id> --msg-type audio --file-path ./recording.mp3
dws chat message send --group <openconversation_id> --msg-type video --file-path ./demo.mp4
# 旧版内联图片:仅当上游已经持有有效 mediaId 时使用
dws chat message send --group <openconversation_id> --msg-type image --media-id <mediaId>
# 查询群 ID: dws chat search --query "群名"
# 查询用户 ID: dws contact user search --query "姓名"`,
Args: cobra.MaximumNArgs(1),
@@ -2400,13 +2406,13 @@ func newChatCommand() *cobra.Command {
_ = chatMessageSendCmd.Flags().MarkHidden("markdown")
chatMessageSendCmd.Flags().Bool("at-all", false, "@所有人(仅群聊时生效,可选),设置时,消息内容中一定要包含对应的占位符<@all>")
chatMessageSendCmd.Flags().String("at-open-dingtalk-ids", "", "@指定成员的 openDingTalkId 列表,逗号分隔(仅群聊时生效,可选),设置--at-open-dingtalk-ids openDingTalkId1,openDingTalkId2时,消息内容中一定要包含对应格式的占位符<@openDingTalkId1> <@openDingTalkId2>")
chatMessageSendCmd.Flags().String("media-id", "", "图片 mediaId(通过 dt_media_upload 上传后用 extract_media_id.py 提取,仅 msgType=image)")
chatMessageSendCmd.Flags().String("msg-type", "", "富媒体消息类型: image/file/audio/video(audio/video 是 file 别名;纯文本/Markdown 无需指定,直接传内容即可)")
chatMessageSendCmd.Flags().String("media-id", "", "上游已提供的图片 mediaId(仅旧版 msgType=image;CLI 不提供本地上传到 mediaId)")
chatMessageSendCmd.Flags().String("msg-type", "", "富媒体消息类型: image/file/audio/video(本地图片/文件推荐 file --file-path;image 仅接受已有 mediaId)")
chatMessageSendCmd.Flags().Int64("dentry-id", 0, "文件 dentryId(与 --space-id 成对传入时跳过自动上传)")
chatMessageSendCmd.Flags().Int64("space-id", 0, "空间 ID(与 --dentry-id 成对传入时跳过自动上传)")
chatMessageSendCmd.Flags().String("file-name", "", "文件名")
chatMessageSendCmd.Flags().String("file-type", "", "文件类型/扩展名")
chatMessageSendCmd.Flags().String("file-path", "", "本地文件路径(msgType=file 时可直接上传发送)")
chatMessageSendCmd.Flags().String("file-path", "", "本地文件路径(msgType=file/audio/video 时直接上传并按 file 消息发送)")
chatMessageSendCmd.Flags().Int64("file-size", 0, "文件大小,单位字节")
_ = chatMessageSendCmd.Flags().MarkHidden("dentry-id")
_ = chatMessageSendCmd.Flags().MarkHidden("space-id")
@@ -3143,6 +3149,25 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
# resource-id: 从 dws chat message list 返回的消息内容中获取 mediaId
# message-id: 从 dws chat message list 返回的 openMessageId
# open-conversation-id: 从 dws chat search 获取 openConversationId`,
PreRunE: func(cmd *cobra.Command, args []string) error {
// Cobra validates required flags after PreRunE. Copy a supplied alias
// into the canonical flag first so --message-id can remain a hard
// required fact in both the executable and Agent Schema contracts.
if cmd.Flags().Changed("message-id") {
return nil
}
alias := ""
switch {
case cmd.Flags().Changed("msg-id"):
alias = "msg-id"
case cmd.Flags().Changed("open-message-id"):
alias = "open-message-id"
default:
return nil
}
value, _ := cmd.Flags().GetString(alias) // registered string flags above
return cmd.Flags().Set("message-id", value)
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "type", "resource-id", "message-id", "open-conversation-id", "output"); err != nil {
return err
@@ -3226,6 +3251,13 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
_ = chatMessageDownloadMediaCmd.MarkFlagRequired("open-conversation-id")
chatMessageDownloadMediaCmd.Flags().String("message-id", "", "消息 openMessageId (必填)")
_ = chatMessageDownloadMediaCmd.MarkFlagRequired("message-id")
// Hidden aliases: agents routinely pass --msg-id / --open-message-id since
// the message-list output exposes the field as openMessageId/msgId. Accept
// them transparently instead of failing with "unknown flag".
chatMessageDownloadMediaCmd.Flags().String("msg-id", "", "--message-id 的别名")
_ = chatMessageDownloadMediaCmd.Flags().MarkHidden("msg-id")
chatMessageDownloadMediaCmd.Flags().String("open-message-id", "", "--message-id 的别名")
_ = chatMessageDownloadMediaCmd.Flags().MarkHidden("open-message-id")
chatMessageDownloadMediaCmd.Flags().String("output", "", "本地保存路径,文件或目录 (必填)")
_ = chatMessageDownloadMediaCmd.MarkFlagRequired("output")
@@ -3361,7 +3393,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
}
iconMediaID := strings.TrimSpace(mustGetFlag(cmd, "icon-media-id"))
if iconMediaID == "" {
return fmt.Errorf("invalid --icon-media-id: mediaId 不能为空\n hint: 先通过媒体上传命令(dt_media_upload)上传图片,使用返回的 mediaId")
return fmt.Errorf("invalid --icon-media-id: mediaId 不能为空\n hint: 请使用上游媒体上传能力返回的有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传命令")
}
return callMCPToolOnServer("im", "update_group_icon", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
@@ -222,6 +222,12 @@ func TestCrossPlatformCoverageChatWebhookReplyConversationAndDownloadEdges(t *te
tmp := t.TempDir()
base := []string{"message", "download-media", "--type=mediaId", "--resource-id=r", "--open-conversation-id=cid", "--message-id=mid"}
_ = runChatCoverageCommand(t, &productExampleCaller{dry: true}, append(base, "--output="+filepath.Join(tmp, "dry"))...)
for _, alias := range []string{"--msg-id=mid", "--open-message-id=mid"} {
aliasArgs := []string{"message", "download-media", "--type=mediaId", "--resource-id=r", "--open-conversation-id=cid", alias, "--output=" + filepath.Join(tmp, "alias-dry")}
_ = runChatCoverageCommand(t, &productExampleCaller{dry: true}, aliasArgs...)
}
missingMessageID := []string{"message", "download-media", "--type=mediaId", "--resource-id=r", "--open-conversation-id=cid", "--output=" + filepath.Join(tmp, "missing-id")}
_ = runChatCoverageCommand(t, &productExampleCaller{dry: true}, missingMessageID...)
for _, tc := range []struct {
step scriptedToolStep
out string
@@ -0,0 +1,262 @@
package helpers
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type chatFilePathCall struct {
server string
tool string
args map[string]any
}
type chatFilePathCaller struct {
sequence []string
calls []chatFilePathCall
}
func (c *chatFilePathCaller) CallTool(_ context.Context, server, tool string, args map[string]any) (*edition.ToolResult, error) {
c.sequence = append(c.sequence, tool)
copied := make(map[string]any, len(args))
for key, value := range args {
copied[key] = value
}
c.calls = append(c.calls, chatFilePathCall{server: server, tool: tool, args: copied})
switch tool {
case "init_conversation_file_upload":
return textToolResult(`{"resourceUrl":"https://upload.example/file","uploadKey":"upload-key","headers":{"x-upload":"yes"}}`), nil
case "commit_conversation_file_upload":
return textToolResult(`{"result":{"dentryId":123,"spaceId":456}}`), nil
case "send_personal_message":
return textToolResult(`{"success":true}`), nil
default:
return nil, fmt.Errorf("unexpected tool call %s/%s", server, tool)
}
}
func (*chatFilePathCaller) Format() string { return "json" }
func (*chatFilePathCaller) DryRun() bool { return false }
func (*chatFilePathCaller) Fields() string { return "" }
func (*chatFilePathCaller) JQ() string { return "" }
func TestChatMessageSendFilePathUsesWukongUploadSequence(t *testing.T) {
previousDeps, previousPut, previousArgs := deps, httpPutFile, os.Args
t.Cleanup(func() {
deps = previousDeps
httpPutFile = previousPut
os.Args = previousArgs
})
filePath := filepath.Join(t.TempDir(), "image.png")
payload := []byte("png payload")
if err := os.WriteFile(filePath, payload, 0o600); err != nil {
t.Fatal(err)
}
caller := &chatFilePathCaller{}
commandArgs := []string{
"message", "send",
"--group=cid",
"--msg-type=file",
"--file-path=" + filePath,
}
os.Args = append([]string{"dws", "chat"}, commandArgs...)
httpPutFile = func(_ context.Context, resourceURL string, headers map[string]string, localPath string, fileSize int64) error {
caller.sequence = append(caller.sequence, "HTTP PUT")
if resourceURL != "https://upload.example/file" {
t.Fatalf("resourceURL = %q", resourceURL)
}
if headers["x-upload"] != "yes" {
t.Fatalf("headers = %#v", headers)
}
if localPath != filePath || fileSize != int64(len(payload)) {
t.Fatalf("upload file = %q (%d), want %q (%d)", localPath, fileSize, filePath, len(payload))
}
return nil
}
err := runChatCoverageCommand(t, caller, commandArgs...)
if err != nil {
t.Fatalf("chat message send --file-path: %v", err)
}
wantSequence := []string{
"init_conversation_file_upload",
"HTTP PUT",
"commit_conversation_file_upload",
"send_personal_message",
}
if !reflect.DeepEqual(caller.sequence, wantSequence) {
t.Fatalf("call sequence = %#v, want %#v", caller.sequence, wantSequence)
}
if len(caller.calls) != 3 {
t.Fatalf("tool calls = %#v, want init, commit, send", caller.calls)
}
fileMD5 := fmt.Sprintf("%x", md5.Sum(payload))
wantInit := chatFilePathCall{
server: "im",
tool: "init_conversation_file_upload",
args: map[string]any{
"openConversationId": "cid",
"fileName": "image.png",
"fileSize": int64(len(payload)),
"md5": fileMD5,
},
}
if !reflect.DeepEqual(caller.calls[0], wantInit) {
t.Fatalf("init call = %#v, want %#v", caller.calls[0], wantInit)
}
wantCommit := chatFilePathCall{
server: "im",
tool: "commit_conversation_file_upload",
args: map[string]any{
"openConversationId": "cid",
"uploadKey": "upload-key",
"fileName": "image.png",
"fileSize": int64(len(payload)),
"md5": fileMD5,
},
}
if !reflect.DeepEqual(caller.calls[1], wantCommit) {
t.Fatalf("commit call = %#v, want %#v", caller.calls[1], wantCommit)
}
send := caller.calls[len(caller.calls)-1]
if send.server != "chat" || send.tool != "send_personal_message" || send.args["msgType"] != "file" {
t.Fatalf("send call = %#v", send)
}
if send.args["openConversationId"] != "cid" {
t.Fatalf("send target = %#v", send.args["openConversationId"])
}
content, ok := send.args["content"].(string)
if !ok {
t.Fatalf("send content = %#v", send.args["content"])
}
var parsed struct {
DentryID int64 `json:"dentryId"`
SpaceID int64 `json:"spaceId"`
FileName string `json:"fileName"`
FileType string `json:"fileType"`
FilePath string `json:"filePath"`
FileSize int64 `json:"fileSize"`
}
if err := json.Unmarshal([]byte(content), &parsed); err != nil {
t.Fatalf("decode send content %q: %v", content, err)
}
if parsed.DentryID != 123 || parsed.SpaceID != 456 ||
parsed.FileName != "image.png" || parsed.FileType != "png" ||
parsed.FilePath != "/image.png" || parsed.FileSize != int64(len(payload)) {
t.Fatalf("send content = %#v", parsed)
}
}
func TestChatMessageSendFilePathUsesOpenDingTalkIDTarget(t *testing.T) {
previousDeps, previousPut, previousArgs := deps, httpPutFile, os.Args
t.Cleanup(func() {
deps = previousDeps
httpPutFile = previousPut
os.Args = previousArgs
})
filePath := filepath.Join(t.TempDir(), "report.pdf")
payload := []byte("pdf payload")
if err := os.WriteFile(filePath, payload, 0o600); err != nil {
t.Fatal(err)
}
caller := &chatFilePathCaller{}
commandArgs := []string{
"message", "send",
"--open-dingtalk-id=D-target",
"--msg-type=file",
"--file-path=" + filePath,
}
os.Args = append([]string{"dws", "chat"}, commandArgs...)
httpPutFile = func(_ context.Context, resourceURL string, _ map[string]string, localPath string, fileSize int64) error {
caller.sequence = append(caller.sequence, "HTTP PUT")
if resourceURL != "https://upload.example/file" || localPath != filePath || fileSize != int64(len(payload)) {
t.Fatalf("upload = %q, %q (%d)", resourceURL, localPath, fileSize)
}
return nil
}
if err := runChatCoverageCommand(t, caller, commandArgs...); err != nil {
t.Fatalf("chat message send --open-dingtalk-id --file-path: %v", err)
}
if len(caller.calls) != 3 {
t.Fatalf("tool calls = %#v, want init, commit, send", caller.calls)
}
fileMD5 := fmt.Sprintf("%x", md5.Sum(payload))
wantInit := chatFilePathCall{
server: "im",
tool: "init_conversation_file_upload",
args: map[string]any{
"openDingTalkId": "D-target",
"fileName": "report.pdf",
"fileSize": int64(len(payload)),
"md5": fileMD5,
},
}
if !reflect.DeepEqual(caller.calls[0], wantInit) {
t.Fatalf("init direct target call = %#v, want %#v", caller.calls[0], wantInit)
}
wantCommit := chatFilePathCall{
server: "im",
tool: "commit_conversation_file_upload",
args: map[string]any{
"openDingTalkId": "D-target",
"uploadKey": "upload-key",
"fileName": "report.pdf",
"fileSize": int64(len(payload)),
"md5": fileMD5,
},
}
if !reflect.DeepEqual(caller.calls[1], wantCommit) {
t.Fatalf("commit direct target call = %#v, want %#v", caller.calls[1], wantCommit)
}
send := caller.calls[2]
if send.server != "chat" || send.tool != "send_personal_message" || send.args["msgType"] != "file" {
t.Fatalf("send direct target call = %#v", send)
}
if send.args["receiverOpenDingTalkId"] != "D-target" {
t.Fatalf("send direct target = %#v", send.args)
}
if _, ok := send.args["openDingTalkId"]; ok {
t.Fatalf("send direct target leaked upload target key: %#v", send.args)
}
}
func TestChatMessageSendFilePathRequiresFileMessageType(t *testing.T) {
previousDeps := deps
t.Cleanup(func() { deps = previousDeps })
filePath := filepath.Join(t.TempDir(), "image.png")
if err := os.WriteFile(filePath, []byte("png"), 0o600); err != nil {
t.Fatal(err)
}
caller := &chatFilePathCaller{}
err := runChatCoverageCommand(t, caller,
"message", "send",
"--group=cid",
"--file-path="+filePath,
)
if err == nil {
t.Fatal("bare --file-path succeeded without --msg-type=file")
}
if len(caller.calls) != 0 {
t.Fatalf("bare --file-path made remote calls: %#v", caller.calls)
}
}
+28 -170
View File
@@ -14,38 +14,23 @@
package helpers
import (
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
)
var (
chatMediaResolveAppToken = mediaResolveAppToken
chatMediaUploadFile = mediaUploadFile
mediaCreateFormFile = func(w *multipart.Writer, field, name string) (io.Writer, error) { return w.CreateFormFile(field, name) }
mediaOpenFile = os.Open
mediaCopyFile = io.Copy
)
const chatMediaUploadReplacement = "dws chat message send --msg-type file --file-path <本地路径>"
func newChatMediaGroup() *cobra.Command {
media := &cobra.Command{
Use: "media",
Short: "媒体文件管理",
Short: "已下线:媒体文件上传兼容入口",
Deprecated: "本地图片和文件请改用 " + chatMediaUploadReplacement,
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() },
RunE: func(*cobra.Command, []string) error {
return chatMediaUploadDownlineError()
},
}
media.AddCommand(newChatMediaUploadCommand())
return media
@@ -53,160 +38,33 @@ func newChatMediaGroup() *cobra.Command {
func newChatMediaUploadCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "upload",
Short: "上传图片获取 mediaId(用于 chat message send --msg-type image)",
Example: " dws chat media upload --file ./screenshot.png\n" +
" dws chat media upload --file ./photo.jpg --type image",
Use: "upload",
Short: "已下线:请通过 chat message send 直接发送本地文件",
Deprecated: "请改用 " + chatMediaUploadReplacement,
Long: `此命令仅为 1.x 命令行兼容保留,不再读取应用凭证或调用旧版媒体上传接口。
发送本地图片或文件时,请使用 chat message send --msg-type file --file-path。
该路径会把图片作为可下载的 file 消息发送,不会生成 mediaId,也不会渲染成内联 image 消息。
如果上游已经提供 mediaId,仍可使用 chat message send --msg-type image --media-id。`,
Example: " dws chat message send --group <openConversationId> --msg-type file --file-path ./screenshot.png\n" +
" dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file-path ./report.pdf",
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
filePath, _ := cmd.Flags().GetString("file")
filePath = strings.TrimSpace(filePath)
if filePath == "" {
return apperrors.NewValidation("--file is required")
}
fi, err := os.Stat(filePath)
if err != nil {
return apperrors.NewValidation("cannot read file: " + err.Error())
}
if fi.IsDir() {
return apperrors.NewValidation(filePath + " is a directory")
}
mediaType, _ := cmd.Flags().GetString("type")
mediaType = strings.TrimSpace(strings.ToLower(mediaType))
if mediaType == "" {
mediaType = "image"
}
ctx, cancel := context.WithTimeout(cmd.Context(), 2*time.Minute)
defer cancel()
token, err := chatMediaResolveAppToken(ctx)
if err != nil {
return err
}
mediaID, err := chatMediaUploadFile(ctx, token, filePath, mediaType)
if err != nil {
return err
}
return writeCommandPayload(cmd, map[string]any{
"success": true,
"mediaId": mediaID,
})
RunE: func(*cobra.Command, []string) error {
return chatMediaUploadDownlineError()
},
}
cmd.Flags().String("file", "", "本地文件路径 (必填)")
cmd.Flags().String("type", "image", "媒体类型: image/voice/video/file")
// Keep the historical flags so existing argv remains parseable while the
// 1.x compatibility command returns an actionable migration error.
cmd.Flags().String("file", "", "旧版兼容参数;本地文件请改用 chat message send --file-path")
cmd.Flags().String("type", "image", "旧版兼容参数;不再执行媒体上传")
return cmd
}
func mediaResolveAppToken(ctx context.Context) (string, error) {
return mediaResolveAppTokenWithRequest(ctx, http.NewRequestWithContext)
}
type mediaRequestFactory func(context.Context, string, string, io.Reader) (*http.Request, error)
func mediaResolveAppTokenWithRequest(ctx context.Context, newRequest mediaRequestFactory) (string, error) {
appKey := os.Getenv("DWS_CLIENT_ID")
appSecret := os.Getenv("DWS_CLIENT_SECRET")
if appKey == "" || appSecret == "" {
return "", apperrors.NewAuth(
"缺少应用凭证。chat media upload 需要 DWS_CLIENT_ID / DWS_CLIENT_SECRET 环境变量。\n" +
"请使用 dws auth login --client-id <APP_KEY> --client-secret <APP_SECRET> 登录。")
}
endpoint := url.URL{Scheme: "https", Host: "oapi.dingtalk.com", Path: "/gettoken"}
endpoint.RawQuery = url.Values{
"appkey": []string{appKey},
"appsecret": []string{appSecret},
}.Encode()
req, err := newRequest(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return "", apperrors.NewAuth("构造访问令牌请求失败: " + err.Error())
}
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
if err != nil {
return "", apperrors.NewAuth("获取访问令牌失败: " + err.Error())
}
defer resp.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if resp.StatusCode >= 400 {
return "", apperrors.NewAuth(fmt.Sprintf("获取访问令牌 HTTP %d: %s", resp.StatusCode, string(raw)))
}
var parsed struct {
AccessToken string `json:"access_token"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(raw, &parsed); err != nil {
return "", apperrors.NewAuth("gettoken 响应解析失败: " + string(raw))
}
if parsed.ErrCode != 0 || parsed.AccessToken == "" {
return "", apperrors.NewAuth(fmt.Sprintf("gettoken errcode=%d errmsg=%s", parsed.ErrCode, parsed.ErrMsg))
}
return parsed.AccessToken, nil
}
func mediaUploadFile(ctx context.Context, token, filePath, mediaType string) (string, error) {
return mediaUploadFileWithRequest(ctx, token, filePath, mediaType, http.NewRequestWithContext)
}
func mediaUploadFileWithRequest(ctx context.Context, token, filePath, mediaType string, newRequest mediaRequestFactory) (string, error) {
pr, pw := io.Pipe()
writer := multipart.NewWriter(pw)
endpoint := url.URL{Scheme: "https", Host: "oapi.dingtalk.com", Path: "/media/upload"}
endpoint.RawQuery = url.Values{
"access_token": []string{token},
"type": []string{mediaType},
}.Encode()
req, err := newRequest(ctx, http.MethodPost, endpoint.String(), pr)
if err != nil {
_ = pr.CloseWithError(err)
_ = pw.CloseWithError(err)
return "", apperrors.NewAPI("construct media upload request: " + err.Error())
}
req.Header.Set("Content-Type", writer.FormDataContentType())
go func() {
defer pw.Close()
defer writer.Close()
part, err := mediaCreateFormFile(writer, "media", filepath.Base(filePath))
if err != nil {
pw.CloseWithError(err)
return
}
f, err := mediaOpenFile(filePath)
if err != nil {
pw.CloseWithError(err)
return
}
defer f.Close()
if _, err := mediaCopyFile(part, f); err != nil {
pw.CloseWithError(err)
}
}()
resp, err := (&http.Client{Timeout: 2 * time.Minute}).Do(req)
if err != nil {
return "", apperrors.NewAPI("media upload failed: " + err.Error())
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if resp.StatusCode >= 400 {
return "", apperrors.NewAPI(fmt.Sprintf("media upload HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body))))
}
var parsed struct {
MediaID string `json:"media_id"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(body, &parsed); err != nil {
return "", apperrors.NewAPI("media upload 响应解析失败: " + string(body))
}
if parsed.ErrCode != 0 || strings.TrimSpace(parsed.MediaID) == "" {
return "", apperrors.NewAPI(fmt.Sprintf("media upload errcode=%d errmsg=%s body=%s", parsed.ErrCode, parsed.ErrMsg, string(body)))
}
return strings.TrimSpace(parsed.MediaID), nil
func chatMediaUploadDownlineError() error {
return apperrors.NewValidation(
"chat media upload 已下线,当前 CLI 不提供本地文件到 mediaId 的上传能力。" +
" 本地图片或文件请改用: " + chatMediaUploadReplacement +
";已有 mediaId 时可使用 dws chat message send --msg-type image --media-id <mediaId>。",
)
}
@@ -1,87 +1,36 @@
package helpers
import (
"bytes"
"context"
"errors"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCrossPlatformCoverageChatMediaUploadCommandRemainingCoverage(t *testing.T) {
file := filepath.Join(t.TempDir(), "image.png")
if err := os.WriteFile(file, []byte("image"), 0o600); err != nil {
t.Fatal(err)
}
if err := executeFilterCoverage(t, newChatMediaUploadCommand(), "--file", t.TempDir()); err == nil {
t.Fatal("directory upload returned nil")
func TestCrossPlatformCoverageChatMediaUploadIsDeprecatedCompatibilityStub(t *testing.T) {
group := newChatMediaGroup()
if group.Deprecated == "" || group.Hidden || !group.Runnable() {
t.Fatalf("media group compatibility contract: deprecated=%q hidden=%v runnable=%v", group.Deprecated, group.Hidden, group.Runnable())
}
origResolve, origUpload := chatMediaResolveAppToken, chatMediaUploadFile
t.Cleanup(func() {
chatMediaResolveAppToken = origResolve
chatMediaUploadFile = origUpload
})
chatMediaResolveAppToken = func(context.Context) (string, error) { return "", errors.New("token") }
if err := executeFilterCoverage(t, newChatMediaUploadCommand(), "--file", file); err == nil {
t.Fatal("token failure returned nil")
}
chatMediaResolveAppToken = func(context.Context) (string, error) { return "token", nil }
chatMediaUploadFile = func(context.Context, string, string, string) (string, error) { return "", errors.New("upload") }
if err := executeFilterCoverage(t, newChatMediaUploadCommand(), "--file", file); err == nil {
t.Fatal("upload failure returned nil")
}
var mediaType string
chatMediaUploadFile = func(_ context.Context, _, _, typ string) (string, error) {
mediaType = typ
return "media-id", nil
}
cmd := newChatMediaUploadCommand()
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"--file", file, "--type", " "})
if err := cmd.Execute(); err != nil || mediaType != "image" || !strings.Contains(out.String(), "media-id") {
t.Fatalf("success type=%q output=%q err=%v", mediaType, out.String(), err)
if cmd.Deprecated == "" || cmd.Hidden || !cmd.Runnable() {
t.Fatalf("media upload compatibility contract: deprecated=%q hidden=%v runnable=%v", cmd.Deprecated, cmd.Hidden, cmd.Runnable())
}
}
func TestCrossPlatformCoverageMediaUploadMultipartWriterRemainingFailures(t *testing.T) {
file := filepath.Join(t.TempDir(), "image.png")
if err := os.WriteFile(file, []byte("image"), 0o600); err != nil {
t.Fatal(err)
}
origTransport := http.DefaultTransport
origCreate, origOpen, origCopy := mediaCreateFormFile, mediaOpenFile, mediaCopyFile
t.Cleanup(func() {
http.DefaultTransport = origTransport
mediaCreateFormFile, mediaOpenFile, mediaCopyFile = origCreate, origOpen, origCopy
})
http.DefaultTransport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
_, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
for _, flag := range []string{"file", "type"} {
if cmd.Flags().Lookup(flag) == nil {
t.Fatalf("media upload lost historical --%s flag", flag)
}
return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"media_id":"id"}`)), Request: req}, nil
})
}
mediaCreateFormFile = func(*multipart.Writer, string, string) (io.Writer, error) { return nil, errors.New("part") }
if _, err := mediaUploadFile(context.Background(), "token", file, "image"); err == nil {
t.Fatal("form part failure returned nil")
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetArgs([]string{"--file", "/path/that/does/not/exist.png", "--type", "image"})
err := cmd.Execute()
if err == nil {
t.Fatal("deprecated media upload returned nil error")
}
mediaCreateFormFile = origCreate
mediaOpenFile = func(string) (*os.File, error) { return nil, errors.New("open") }
if _, err := mediaUploadFile(context.Background(), "token", file, "image"); err == nil {
t.Fatal("open failure returned nil")
}
mediaOpenFile = origOpen
mediaCopyFile = func(io.Writer, io.Reader) (int64, error) { return 0, errors.New("copy") }
if _, err := mediaUploadFile(context.Background(), "token", file, "image"); err == nil {
t.Fatal("copy failure returned nil")
for _, want := range []string{"已下线", "chat message send", "--msg-type file", "--file-path", "--media-id"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("media upload migration error missing %q: %v", want, err)
}
}
}
+1 -83
View File
@@ -3,14 +3,11 @@ package helpers
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
@@ -117,86 +114,7 @@ func TestCrossPlatformCoverageMailHTTPTransfersCoverage(t *testing.T) {
}
}
func TestCrossPlatformCoverageChatMediaHTTPAndDocVersionsCoverage(t *testing.T) {
previousTransport := http.DefaultTransport
mode := "success"
var requestQuery url.Values
http.DefaultTransport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
if mode == "transport-error" {
return nil, errors.New("transport")
}
if req.Body != nil {
if _, err := io.ReadAll(req.Body); err != nil {
return nil, err
}
}
requestQuery = req.URL.Query()
status := http.StatusOK
body := `{"access_token":"token","errcode":0}`
if strings.Contains(req.URL.Path, "media/upload") {
body = `{"media_id":"media-id","errcode":0}`
}
switch mode {
case "http-error":
status = http.StatusInternalServerError
body = `failure`
case "invalid-json":
body = `{`
case "api-error":
body = `{"errcode":1,"errmsg":"failed"}`
}
return &http.Response{StatusCode: status, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), Request: req}, nil
})
t.Cleanup(func() { http.DefaultTransport = previousTransport })
t.Setenv("DWS_CLIENT_ID", "")
t.Setenv("DWS_CLIENT_SECRET", "")
if _, err := mediaResolveAppToken(context.Background()); err == nil {
t.Fatal("missing media credentials succeeded")
}
t.Setenv("DWS_CLIENT_ID", "client")
t.Setenv("DWS_CLIENT_SECRET", "secret")
for _, current := range []string{"transport-error", "http-error", "invalid-json", "api-error", "success"} {
mode = current
_, _ = mediaResolveAppToken(context.Background())
}
t.Setenv("DWS_CLIENT_ID", "client&scope=chat")
t.Setenv("DWS_CLIENT_SECRET", "secret=with?reserved")
mode = "success"
if _, err := mediaResolveAppToken(context.Background()); err != nil {
t.Fatal(err)
}
if requestQuery.Get("appkey") != "client&scope=chat" || requestQuery.Get("appsecret") != "secret=with?reserved" {
t.Fatalf("token query was not encoded: %v", requestQuery)
}
file := filepath.Join(t.TempDir(), "image.png")
if err := os.WriteFile(file, []byte("image"), 0o600); err != nil {
t.Fatal(err)
}
for _, current := range []string{"transport-error", "http-error", "invalid-json", "api-error", "success"} {
mode = current
_, _ = mediaUploadFile(context.Background(), "token", file, "image")
}
mode = "success"
if _, err := mediaUploadFile(context.Background(), "token&scope=chat", file, "image&type=file"); err != nil {
t.Fatal(err)
}
if requestQuery.Get("access_token") != "token&scope=chat" || requestQuery.Get("type") != "image&type=file" {
t.Fatalf("upload query was not encoded: %v", requestQuery)
}
mode = "success"
_, _ = mediaUploadFile(context.Background(), "token", filepath.Join(t.TempDir(), "missing"), "image")
requestFailure := func(context.Context, string, string, io.Reader) (*http.Request, error) {
return nil, errors.New("request")
}
if _, err := mediaResolveAppTokenWithRequest(context.Background(), requestFailure); err == nil {
t.Fatal("token request construction failure returned nil")
}
if _, err := mediaUploadFileWithRequest(context.Background(), "token", file, "image", requestFailure); err == nil {
t.Fatal("upload request construction failure returned nil")
}
func TestCrossPlatformCoverageDocVersionsCoverage(t *testing.T) {
for _, value := range []any{float64(3), float64(3.5), "3", "bad", jsonNumber("3"), jsonNumber("bad"), true} {
_ = docVersionNumberMatches(value, 3)
}
+33 -17
View File
@@ -17,6 +17,7 @@ import (
"fmt"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
)
// MessagesSend sends a text/markdown message as the current user
@@ -242,6 +243,11 @@ var MessagesList = shortcut.Shortcut{
// clean output projection. Both the list container and the per-item field names are
// probed defensively across candidate keys, so an empty or unexpected shape
// yields an empty list rather than a crash or fabricated data.
//
// Text goes through the shared chatmsg projection so card/auto-reply JSON is
// rendered readable and encrypted ciphertext is marked, and forwarded chat
// records ("聊天记录") expand their nested messages under "forwarded" instead of
// collapsing to a "[卡片]" summary.
func listMessagesProject(data map[string]any) []map[string]any {
raw := listMessagesResolveList(data)
out := make([]map[string]any, 0, len(raw))
@@ -250,29 +256,39 @@ func listMessagesProject(data map[string]any) []map[string]any {
if !ok {
continue
}
row := map[string]any{}
if v, ok := listMessagesFirst(m, "openMessageId", "openMsgId", "messageId", "msgId"); ok {
row["messageId"] = v
}
if v, ok := listMessagesFirst(m, "senderOpenDingTalkId", "senderUserId", "senderId", "senderStaffId"); ok {
row["senderId"] = v
}
if v, ok := listMessagesFirst(m, "msgType", "messageType", "type"); ok {
row["msgType"] = v
}
if v, ok := listMessagesFirst(m, "createTime", "sendTime", "gmtCreate", "messageTime"); ok {
row["createTime"] = v
}
if v, ok := listMessagesFirst(m, "text", "content", "plainText"); ok {
row["text"] = v
}
if len(row) > 0 {
if row := listMessageProjectOne(m); len(row) > 0 {
out = append(out, row)
}
}
return out
}
// listMessageProjectOne projects a single message into the native
// {messageId, senderId, msgType, createTime, text(, forwarded)} shape, reused
// recursively for forwarded chat records.
func listMessageProjectOne(m map[string]any) map[string]any {
row := map[string]any{}
if v, ok := listMessagesFirst(m, "openMessageId", "openMsgId", "messageId", "msgId"); ok {
row["messageId"] = v
}
if v, ok := listMessagesFirst(m, "senderOpenDingTalkId", "senderUserId", "senderId", "senderStaffId"); ok {
row["senderId"] = v
}
if v, ok := listMessagesFirst(m, "msgType", "messageType", "type"); ok {
row["msgType"] = v
}
if v, ok := listMessagesFirst(m, "createTime", "sendTime", "gmtCreate", "messageTime"); ok {
row["createTime"] = v
}
if text := chatmsg.Text(m); text != nil {
row["text"] = text
}
if forwarded := chatmsg.Forwarded(m, listMessageProjectOne); len(forwarded) > 0 {
row["forwarded"] = forwarded
}
return row
}
// listMessagesResolveList locates the list payload, tolerating a bare top-level
// array container or nesting one level deeper under a common envelope key.
func listMessagesResolveList(data map[string]any) []any {
@@ -0,0 +1,56 @@
// 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 chat
import (
"strings"
"testing"
)
const testCipher = "SwzNkAraDE6lUHUNlVT3mjFdbxL6dWvmt77XtjACdpJx9VFibzTbW9KtDbkzGOYP||2||1||1"
func TestListMessageProjectOne(t *testing.T) {
// full field mapping + forwarded expansion; an encrypted body is marked (no
// cross-conversation recovery), not leaked as base64.
row := listMessageProjectOne(map[string]any{
"openMessageId": "mid",
"senderOpenDingTalkId": "DXYZ",
"msgType": "text",
"createTime": "2026-07-19 13:37:03",
"content": testCipher,
"forwardMessages": []any{
map[string]any{"openMessageId": "c1", "senderOpenDingTalkId": "DA", "content": "子消息", "createTime": "t"},
},
})
if row["messageId"] != "mid" || row["senderId"] != "DXYZ" || row["msgType"] != "text" {
t.Fatalf("field mapping = %#v", row)
}
if row["createTime"] != "2026-07-19 13:37:03" {
t.Errorf("createTime = %v", row["createTime"])
}
if s, _ := row["text"].(string); !strings.Contains(s, "加密消息") || strings.Contains(s, "||2||1||") {
t.Errorf("encrypted text = %v, want marker", row["text"])
}
fwd, ok := row["forwarded"].([]map[string]any)
if !ok || len(fwd) != 1 || fwd[0]["messageId"] != "c1" || fwd[0]["text"] != "子消息" {
t.Errorf("forwarded = %#v", row["forwarded"])
}
// a bare message with no recognizable fields → empty row (no keys)
row = listMessageProjectOne(map[string]any{"unrelated": 1})
if len(row) != 0 {
t.Errorf("empty message row = %#v, want no keys", row)
}
}
@@ -16,6 +16,7 @@ package chat
import (
"context"
"io"
"reflect"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
@@ -129,3 +130,48 @@ func TestCrossPlatformCoverageCompatibilityAliases(t *testing.T) {
})
}
}
func TestCrossPlatformCoverageChatIDHelpers(t *testing.T) {
t.Run("recognize open DingTalk IDs", func(t *testing.T) {
tests := []struct {
value string
want bool
}{
{value: "DingTalk-open-id", want: true},
{value: " dingtalk-open-id ", want: true},
{value: "user-id", want: false},
{value: " ", want: false},
}
for _, tc := range tests {
if got := isOpenID(tc.value); got != tc.want {
t.Errorf("isOpenID(%q) = %v, want %v", tc.value, got, tc.want)
}
}
})
t.Run("split mixed IDs", func(t *testing.T) {
userIDs, openIDs := splitIDs([]string{" user-1 ", "", "D-open-1", "d-open-2", "user-2"})
if want := []string{"user-1", "user-2"}; !reflect.DeepEqual(userIDs, want) {
t.Fatalf("user IDs = %#v, want %#v", userIDs, want)
}
if want := []string{"D-open-1", "d-open-2"}; !reflect.DeepEqual(openIDs, want) {
t.Fatalf("open IDs = %#v, want %#v", openIDs, want)
}
})
t.Run("parse numeric IDs", func(t *testing.T) {
got, err := toInt64Slice([]string{" 1 ", "", "-2"})
if err != nil {
t.Fatal(err)
}
if want := []int64{1, -2}; !reflect.DeepEqual(got, want) {
t.Fatalf("numeric IDs = %#v, want %#v", got, want)
}
if _, err := toInt64Slice([]string{"not-a-number"}); err == nil {
t.Fatal("invalid integer unexpectedly succeeded")
}
if _, err := toInt64Slice([]string{"", " "}); err == nil {
t.Fatal("empty integer list unexpectedly succeeded")
}
})
}
+292
View File
@@ -0,0 +1,292 @@
// 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 chatmsg holds the shared, read-only projection helpers for DingTalk
// message-list responses (list_individual_chat_message,
// list_conversation_message_v2, search_at_me_message, search_messages_by_keyword,
// list_topic_replies, …). Several shortcuts reshape those raw responses into a
// clean speaker/text/time list; centralising the fiddly bits here keeps them
// consistent and fixed in one place:
//
// - Sender: the display name lives under the bare "sender" key, forwarded
// entries carry the literal string "null", and some responses nest the
// speaker in a {name:…} object — all handled here.
// - Text: out-of-office auto-replies / cards arrive as raw rich-content JSON,
// and card/robot messages arrive as undecryptable ciphertext; CleanText
// renders the former to readable text and marks the latter, WITHOUT ever
// rewriting ordinary text that merely contains a JSON fragment.
// - Forwarded: a forwarded chat record ("聊天记录") hides its real per-message
// bodies in forwardMessages while the top-level content is a lossy summary.
package chatmsg
import (
"encoding/json"
"regexp"
"strings"
)
// Sender reads a message's speaker display name, tolerating common sender-name
// keys. The message-list responses carry the display name under the bare
// "sender" key (verified live), so it is probed first; the remaining aliases and
// the *Id fallbacks keep the projection resilient to other shapes. The literal
// string "null" (forwarded entries) and the empty string are treated as absent,
// and a nested {name:…} sender object yields its display name rather than the
// raw object.
func Sender(m map[string]any) any {
for _, key := range []string{"sender", "senderName", "senderNick", "nick", "senderStaffName", "userName", "name", "senderId", "senderStaffId", "senderOpenDingTalkId"} {
v, ok := m[key]
if !ok || v == nil {
continue
}
switch t := v.(type) {
case string:
if t == "" || t == "null" {
continue
}
return t
case map[string]any:
// Nested sender object: extract a display-name field; never return
// the raw map (it would surface a JSON object and block fallbacks).
if name := senderDisplayName(t); name != "" {
return name
}
continue
default:
// Scalar id (e.g. numeric) — usable as-is.
return v
}
}
return nil
}
// senderDisplayName extracts a human name from a nested sender object.
func senderDisplayName(m map[string]any) string {
for _, k := range []string{"name", "nick", "userName", "staffName", "displayName", "senderName"} {
if s, ok := m[k].(string); ok {
if s = strings.TrimSpace(s); s != "" && s != "null" {
return s
}
}
}
return ""
}
// Text reads a message's textual content (tolerating common text keys and one
// level of nesting) and runs it through CleanText.
func Text(m map[string]any) any {
for _, key := range []string{"text", "content", "msgContent", "message", "body", "plainText"} {
v, ok := m[key]
if !ok || v == nil {
continue
}
switch t := v.(type) {
case string:
if t != "" {
return CleanText(t)
}
case map[string]any:
for _, inner := range []string{"text", "content", "value"} {
if s, ok := t[inner].(string); ok && s != "" {
return CleanText(s)
}
}
}
}
return nil
}
// CreateTime reads a message's create/send time under whichever candidate key is
// present, returning the raw value.
func CreateTime(m map[string]any) any {
for _, key := range []string{"createTime", "sendTime", "gmtCreate", "createAt", "timestamp", "time"} {
if v, ok := m[key]; ok && v != nil {
return v
}
}
return nil
}
// Forwarded projects the nested messages of a forwarded chat record. The caller
// supplies its own per-message projection so each command keeps its own row
// shape; project is applied recursively, so multi-level forwards expand too.
func Forwarded(m map[string]any, project func(map[string]any) map[string]any) []map[string]any {
raw, ok := m["forwardMessages"].([]any)
if !ok || len(raw) == 0 {
return nil
}
out := make([]map[string]any, 0, len(raw))
for _, e := range raw {
if sub, ok := e.(map[string]any); ok {
out = append(out, project(sub))
}
}
return out
}
// CleanText makes a message body human-readable WITHOUT ever rewriting ordinary
// text. It only transforms a body that is a genuine DingTalk structured message:
//
// - Encrypted card/robot ciphertext (base64 + "||v||t||len" trailer) → a clear
// "[加密消息]" marker instead of the raw base64.
// - A rich-content card (out-of-office auto-reply, link/preview card, …) whose
// lines include at least one recognised rich-content block → the readable
// text extracted from those blocks, with the card's decorative JSON lines and
// "empty" placeholders dropped.
//
// Crucially, if NO line is a recognised rich-content block (e.g. ordinary text
// that merely embeds a `{"approved":false}` fragment), the original string is
// returned verbatim — a JSON line is never silently dropped.
func CleanText(s string) string {
if IsEncrypted(s) {
return "[加密消息,无法解码]"
}
// Fast path: no JSON delimiters at all — the overwhelming common case.
if !strings.ContainsAny(s, "{[") {
return s
}
lines := strings.Split(s, "\n")
isJSON := make([]bool, len(lines))
isDecoration := make([]bool, len(lines))
extracted := make([][]string, len(lines))
anyExtracted := false
for i, line := range lines {
t := strings.TrimSpace(line)
if !strings.HasPrefix(t, "{") && !strings.HasPrefix(t, "[") {
continue
}
var v any
if json.Unmarshal([]byte(t), &v) != nil {
continue
}
isJSON[i] = true
isDecoration[i] = isKnownRichDecoration(v)
if texts := richItemTexts(v); len(texts) > 0 {
extracted[i] = texts
anyExtracted = true
}
}
// No recognised rich-content block anywhere → treat the whole body as plain
// text (which may merely contain a JSON fragment) and return it untouched.
if !anyExtracted {
return s
}
out := make([]string, 0, len(lines))
for i, line := range lines {
if len(extracted[i]) > 0 {
out = append(out, extracted[i]...)
continue
}
// In card mode, drop only JSON shapes known to be card decoration.
// Unrecognised JSON may be user-authored message content and must remain
// verbatim even when another line contains a rich-content block.
if isJSON[i] && isDecoration[i] {
continue
}
if t := strings.TrimSpace(line); t == "" || t == "empty" {
continue
}
out = append(out, line)
}
// anyExtracted is true here, so out always holds at least one non-empty
// extracted text — the joined result is never empty.
return strings.TrimSpace(strings.Join(out, "\n"))
}
// isKnownRichDecoration recognises the two decoration records emitted alongside
// DingTalk rich-content bodies. Keep this deliberately narrow: an arbitrary JSON
// object in the same message is user content unless its shape is known here.
func isKnownRichDecoration(node any) bool {
m, ok := node.(map[string]any)
if !ok {
return false
}
_, hasPreviewURL := m["previewUrl"]
_, hasTitle := m["title"]
_, hasAutoLayout := m["autoLayout"]
_, hasEnableForward := m["enableForward"]
return (hasPreviewURL && hasTitle) || (hasAutoLayout && hasEnableForward)
}
// richItemTexts walks a decoded DingTalk rich-content blob and returns the
// readable text carried by its rich-content items (items[].data.text). It only
// harvests item bodies, so decorative fields (card titles, preview URLs, layout
// config) contribute nothing and are dropped. An empty result means "not a
// recognised rich-content block".
func richItemTexts(node any) []string {
var texts []string
var walk func(n any)
walk = func(n any) {
switch t := n.(type) {
case []any:
for _, e := range t {
walk(e)
}
case map[string]any:
if items, ok := t["items"].([]any); ok {
for _, it := range items {
mm, ok := it.(map[string]any)
if !ok {
continue
}
data, ok := mm["data"].(map[string]any)
if !ok {
continue
}
if s, ok := data["text"].(string); ok {
if s = strings.TrimSpace(s); s != "" {
texts = append(texts, s)
}
}
}
}
for _, e := range t {
walk(e)
}
}
}
walk(node)
return texts
}
// encryptedTrailerRE matches DingTalk's encrypted-message trailer
// "||<version>||<type>||<len>" (e.g. "||2||1||196") anchored at the end.
var encryptedTrailerRE = regexp.MustCompile(`\|\|\d+\|\|\d+\|\|\d+\s*$`)
// IsEncrypted reports whether a message body is a raw DingTalk encrypted-message
// ciphertext: a base64 blob (DingTalk wraps it across several lines) followed by
// the "||v||t||len" trailer. It is intentionally strict — both the trailer and a
// pure-base64 body are required — so ordinary text (CJK, punctuation, …) never
// trips it.
func IsEncrypted(s string) bool {
s = strings.TrimSpace(s)
if !encryptedTrailerRE.MatchString(s) {
return false
}
body := strings.TrimSpace(encryptedTrailerRE.ReplaceAllString(s, ""))
if len(body) < 32 {
return false
}
for _, r := range body {
switch {
case r >= 'A' && r <= 'Z', r >= 'a' && r <= 'z', r >= '0' && r <= '9',
r == '+', r == '/', r == '=', r == '\n', r == '\r', r == ' ', r == '\t':
default:
return false
}
}
return true
}
+176
View File
@@ -0,0 +1,176 @@
// 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 chatmsg
import (
"strings"
"testing"
)
func TestSender(t *testing.T) {
// The display name lives under the bare "sender" key.
if got := Sender(map[string]any{"sender": "念晨", "senderOpenDingTalkId": "D1"}); got != "念晨" {
t.Fatalf("sender = %v, want 念晨", got)
}
// Falls back to the open id when no display name is present.
if got := Sender(map[string]any{"senderOpenDingTalkId": "DXYZ"}); got != "DXYZ" {
t.Fatalf("sender fallback = %v, want DXYZ", got)
}
// forwardMessages entries carry the literal string "null" — treat as absent.
if got := Sender(map[string]any{"sender": "null"}); got != nil {
t.Fatalf("sender \"null\" = %v, want nil", got)
}
if got := Sender(map[string]any{"sender": "null", "senderName": "念晨"}); got != "念晨" {
t.Fatalf("sender \"null\" fallthrough = %v, want 念晨", got)
}
// A nested {name:…} sender object yields its display name, not the raw map.
if got := Sender(map[string]any{"sender": map[string]any{"name": "Alice"}}); got != "Alice" {
t.Fatalf("nested sender = %v, want Alice", got)
}
// A nested sender object with no usable name must not block the fallback.
if got := Sender(map[string]any{"sender": map[string]any{"foo": "bar"}, "senderName": "Bob"}); got != "Bob" {
t.Fatalf("nested-no-name fallthrough = %v, want Bob", got)
}
// A scalar numeric id is returned as-is.
if got := Sender(map[string]any{"senderId": float64(42)}); got != float64(42) {
t.Fatalf("numeric sender id = %v", got)
}
}
func TestCleanText(t *testing.T) {
// Out-of-office auto-reply: readable body lives in items[].data.text; the
// decorative preview/config JSON lines and "empty" placeholder are dropped.
autoReply := "* 仅你和对方可见\n" +
`[{"text":{"minSupportVersion":"1.1","translateMap":{},"version":"1.2","items":[{"fallbackKey":"","data":{"text":"你好,我在出差中,消息回复可能不及时。"},"style":{"size":15,"bold":0},"type":"text"}]},"type":"markdown"}]` + "\n" +
`{"previewUrl":"dingtalk://x","title":{"text":"自动回复","type":"text"}}` + "\n" +
"empty\n" +
`{"autoLayout":false,"enableForward":false}`
if got, want := CleanText(autoReply), "* 仅你和对方可见\n你好,我在出差中,消息回复可能不及时。"; got != want {
t.Fatalf("auto-reply cleaned = %q, want %q", got, want)
}
// P1 regression: ordinary text whose middle line is a JSON fragment (no
// rich-content block anywhere) must be returned VERBATIM, not rewritten.
mixed := "payload:\n{\"approved\":false}\nplease check"
if got := CleanText(mixed); got != mixed {
t.Fatalf("mixed text was rewritten: got %q, want %q", got, mixed)
}
// An ordinary JSON line must also survive when a different line contains a
// recognised rich-content block. Card mode is not permission to discard
// unrelated user-authored JSON.
richAndPlain := `[{"items":[{"data":{"text":"卡片正文"}}]}]` + "\n" +
`{"approved":false}`
if got, want := CleanText(richAndPlain), "卡片正文\n{\"approved\":false}"; got != want {
t.Fatalf("mixed rich/plain JSON was rewritten: got %q, want %q", got, want)
}
// Malformed items (non-map item, item whose "data" isn't a map) are skipped;
// only the well-formed item's text is extracted.
blob := `[{"items":["notmap",{"data":"notmap"},{"data":{"text":"有效正文"}}]}]`
if got := CleanText(blob); got != "有效正文" {
t.Fatalf("CleanText rich edge = %q, want 有效正文", got)
}
tests := map[string]string{
"上周五 7.1 KW": "上周五 7.1 KW",
"上周客户统计的[图片消息](mediaId=@lQ)": "上周客户统计的[图片消息](mediaId=@lQ)",
"[文件] 简历.pdf fileId: qnY 注意:如需下载使用dws drive download命令下载": "[文件] 简历.pdf fileId: qnY 注意:如需下载使用dws drive download命令下载",
"[讨论] 排期\n明天开会": "[讨论] 排期\n明天开会",
// a lone JSON object that isn't a rich-content block is left untouched
`{"autoLayout":false,"enableForward":false}`: `{"autoLayout":false,"enableForward":false}`,
}
for in, want := range tests {
if got := CleanText(in); got != want {
t.Errorf("CleanText(%q) = %q, want %q", in, got, want)
}
}
}
func TestIsEncryptedAndMarker(t *testing.T) {
cipher := "SwzNkAraDE6lUHUNlVT3mjFdbxL6dWvmt77XtjACdpJx9VFibzTbW9KtDbkzGOYP\n" +
"7oDptklFO+YzDltH+myErV6rkc8URHYykpeSDsMP6kznFa9E320NsIntfY771dx+\n" +
"||2||1||196"
if !IsEncrypted(cipher) {
t.Fatalf("ciphertext not detected: %q", cipher)
}
if got := CleanText(cipher); !strings.Contains(got, "加密消息") || strings.Contains(got, "||2||1||") {
t.Fatalf("encrypted cleaned = %q, want marker not ciphertext", got)
}
for _, s := range []string{
"上周五 7.1 KW",
"价格 100||2||1||3",
"[图片消息](mediaId=@lQLPJwDw3VmNDcfMos0DhLB3OHPQeTBlzgov2Oi1ly4A)",
"大哥,我看了一下我觉得有几个点可以关注一下",
strings.Repeat("好", 20) + "||2||1||1", // long CJK body + trailer, not base64
} {
if IsEncrypted(s) {
t.Errorf("false positive: %q flagged as encrypted", s)
}
}
}
func TestText(t *testing.T) {
if got := Text(map[string]any{"content": "你好"}); got != "你好" {
t.Errorf("Text string = %v", got)
}
if got := Text(map[string]any{"content": map[string]any{"text": "嵌套"}}); got != "嵌套" {
t.Errorf("Text nested = %v", got)
}
if got := Text(map[string]any{"plainText": "纯文本"}); got != "纯文本" {
t.Errorf("Text plainText = %v", got)
}
if got := Text(map[string]any{"foo": 1}); got != nil {
t.Errorf("Text none = %v, want nil", got)
}
}
func TestCreateTime(t *testing.T) {
if got := CreateTime(map[string]any{"sendTime": "2026-07-19 13:37:03"}); got != "2026-07-19 13:37:03" {
t.Errorf("CreateTime = %v", got)
}
if got := CreateTime(map[string]any{}); got != nil {
t.Errorf("CreateTime empty = %v, want nil", got)
}
}
func TestForwarded(t *testing.T) {
var project func(m map[string]any) map[string]any
project = func(m map[string]any) map[string]any {
row := map[string]any{"text": Text(m)}
if fwd := Forwarded(m, project); len(fwd) > 0 { // recurse
row["forwarded"] = fwd
}
return row
}
if got := Forwarded(map[string]any{"content": "x"}, project); got != nil {
t.Errorf("Forwarded none = %v", got)
}
fwd := Forwarded(map[string]any{
"forwardMessages": []any{
map[string]any{"content": "a"},
"not-a-map",
map[string]any{"content": "b", "forwardMessages": []any{
map[string]any{"content": "nested"},
}},
},
}, project)
if len(fwd) != 2 || fwd[0]["text"] != "a" || fwd[1]["text"] != "b" {
t.Fatalf("Forwarded = %#v", fwd)
}
nested, ok := fwd[1]["forwarded"].([]map[string]any)
if !ok || len(nested) != 1 || nested[0]["text"] != "nested" {
t.Errorf("nested forwarded = %#v", fwd[1]["forwarded"])
}
}
+41 -11
View File
@@ -19,6 +19,7 @@ import (
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
)
// AtMe: pull the messages that recently @-mentioned ME across chats in one step.
@@ -91,12 +92,7 @@ var AtMe = shortcut.Shortcut{
}
results := make([]map[string]any, 0, len(items))
for _, m := range items {
results = append(results, map[string]any{
"sender": atMeSender(m),
"time": atMeTime(m),
"text": atMeText(m),
"conversation": atMeConversation(m),
})
results = append(results, atMeProject(m))
}
return rt.Output(map[string]any{"messages": results})
},
@@ -194,28 +190,62 @@ func atMeToMaps(arr []any) []map[string]any {
return out
}
// atMeProject reshapes one @me message into {sender, time, text, conversation},
// running text through the shared chatmsg cleaning (card/auto-reply JSON →
// readable, ciphertext → marker) and recursively expanding any forwarded chat
// record under "forwarded".
func atMeProject(m map[string]any) map[string]any {
row := map[string]any{
"sender": atMeSender(m),
"time": atMeTime(m),
"text": atMeCleanText(m),
"conversation": atMeConversation(m),
}
if forwarded := chatmsg.Forwarded(m, atMeProject); len(forwarded) > 0 {
row["forwarded"] = forwarded
}
return row
}
// atMeCleanText runs atMeText's extraction through chatmsg.CleanText so
// card/auto-reply JSON and ciphertext render readable instead of leaking raw.
func atMeCleanText(m map[string]any) any {
if s, ok := atMeText(m).(string); ok {
return chatmsg.CleanText(s)
}
return atMeText(m)
}
// atMeSender reads a message's sender display name/id, tolerating the common
// sender keys the gateway may use (including a nested sender object).
// sender keys the gateway may use (including a nested sender object). The literal
// string "null" (carried by forwarded sub-messages) and the empty string are
// both treated as absent so they never surface as the speaker.
func atMeSender(m map[string]any) any {
norm := func(v any) string {
if s := atMeString(v); s != "" && s != "null" {
return s
}
return ""
}
for _, key := range []string{"senderName", "sender_name", "senderNick", "fromName", "senderStaffName"} {
if v := atMeString(m[key]); v != "" {
if v := norm(m[key]); v != "" {
return v
}
}
for _, key := range []string{"sender", "from", "senderUser"} {
if nested, ok := m[key].(map[string]any); ok {
for _, k2 := range []string{"name", "nick", "userName", "staffName", "displayName"} {
if v := atMeString(nested[k2]); v != "" {
if v := norm(nested[k2]); v != "" {
return v
}
}
}
if v := atMeString(m[key]); v != "" {
if v := norm(m[key]); v != "" {
return v
}
}
for _, key := range []string{"senderId", "sender_id", "senderUserId", "senderStaffId", "openDingTalkId"} {
if v := atMeString(m[key]); v != "" {
if v := norm(m[key]); v != "" {
return v
}
}
@@ -0,0 +1,131 @@
// 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 smart
import (
"strings"
"testing"
)
const testCipher = "SwzNkAraDE6lUHUNlVT3mjFdbxL6dWvmt77XtjACdpJx9VFibzTbW9KtDbkzGOYP||2||1||1"
func TestAtMeProject(t *testing.T) {
// nested sender object + plain text
row := atMeProject(map[string]any{
"sender": map[string]any{"name": "念晨"},
"createTime": "2026-07-19 13:37:03",
"content": "普通消息",
"conversationTitle": "群A",
"openConversationId": "cid1",
})
if row["sender"] != "念晨" || row["text"] != "普通消息" || row["conversation"] != "群A" {
t.Fatalf("atMeProject nested = %#v", row)
}
// encrypted content → marked (never leaked); id-only sender fallback; a
// forwarded sub-message whose sender is the literal "null" must be nulled.
row = atMeProject(map[string]any{
"senderId": "DXYZ",
"openMessageId": "m1",
"content": testCipher,
"forwardMessages": []any{
map[string]any{"sender": "null", "content": "子消息", "createTime": "t"},
},
})
if row["sender"] != "DXYZ" {
t.Errorf("atMeProject id-fallback sender = %v", row["sender"])
}
if s, _ := row["text"].(string); !strings.Contains(s, "加密消息") {
t.Errorf("atMeProject encrypted text = %v, want marker", row["text"])
}
fwd, ok := row["forwarded"].([]map[string]any)
if !ok || len(fwd) != 1 {
t.Fatalf("atMeProject forwarded = %#v", row["forwarded"])
}
if fwd[0]["sender"] != nil {
t.Errorf("forwarded sub sender = %v, want nil (literal \"null\")", fwd[0]["sender"])
}
// no sender / no text at all → nils, no forwarded key
row = atMeProject(map[string]any{"createTime": "t"})
if row["sender"] != nil || row["text"] != nil {
t.Errorf("atMeProject empty = %#v", row)
}
if _, has := row["forwarded"]; has {
t.Errorf("atMeProject plain unexpectedly has forwarded")
}
}
func TestSearchMsgProject(t *testing.T) {
// nested sender + plain text + messageId
row := searchMsgProject(map[string]any{
"sender": map[string]any{"nick": "千启"},
"createTime": "2026-07-19 13:37:03",
"content": "命中关键词的消息",
"msgId": "mid1",
})
if row["sender"] != "千启" || row["text"] != "命中关键词的消息" {
t.Fatalf("searchMsgProject = %#v", row)
}
// encrypted → marker; id-only sender; forwarded "null" sender nulled.
row = searchMsgProject(map[string]any{
"senderId": "DAAA",
"openMessageId": "m2",
"content": testCipher,
"forwardMessages": []any{
map[string]any{"sender": "null", "content": "转发子消息", "createTime": "t"},
},
})
if row["sender"] != "DAAA" {
t.Errorf("searchMsgProject sender = %v", row["sender"])
}
if s, _ := row["text"].(string); !strings.Contains(s, "加密消息") {
t.Errorf("searchMsgProject encrypted text = %v, want marker", row["text"])
}
fwd, ok := row["forwarded"].([]map[string]any)
if !ok || len(fwd) != 1 || fwd[0]["sender"] != nil {
t.Errorf("searchMsgProject forwarded = %#v", row["forwarded"])
}
// no sender / no text
row = searchMsgProject(map[string]any{"createTime": "t"})
if row["sender"] != nil || row["text"] != nil {
t.Errorf("searchMsgProject empty = %#v", row)
}
}
// TestSenderHelpers exercises the atMe/searchMsg sender key families directly:
// a senderName-family key (first probe loop), a flat string under "sender"
// (second loop), and the "null" sentinel normalisation.
func TestSenderHelpers(t *testing.T) {
cases := []struct {
fn func(map[string]any) any
name string
}{
{atMeSender, "atMeSender"},
{searchMsgSender, "searchMsgSender"},
}
for _, c := range cases {
if got := c.fn(map[string]any{"senderName": "张三"}); got != "张三" {
t.Errorf("%s senderName = %v, want 张三", c.name, got)
}
if got := c.fn(map[string]any{"sender": "李四"}); got != "李四" {
t.Errorf("%s flat sender = %v, want 李四", c.name, got)
}
if got := c.fn(map[string]any{"senderName": "null"}); got != nil {
t.Errorf("%s \"null\" = %v, want nil", c.name, got)
}
}
}
+14 -50
View File
@@ -17,6 +17,7 @@ import (
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
)
// ChatMessages: fetch the message list of one conversation (group OR single
@@ -111,11 +112,7 @@ var ChatMessages = shortcut.Shortcut{
items := chatMessageItems(data)
results := make([]map[string]any, 0, len(items))
for _, m := range items {
results = append(results, map[string]any{
"sender": chatMessageSender(m),
"text": chatMessageText(m),
"createTime": chatMessageCreateTime(m),
})
results = append(results, projectChatMessage(m))
}
return rt.Output(map[string]any{
@@ -156,53 +153,20 @@ func chatMessageItems(data map[string]any) []map[string]any {
return nil
}
// chatMessageSender reads a message's speaker display name, tolerating common
// sender-name keys.
func chatMessageSender(m map[string]any) any {
for _, key := range []string{"senderName", "senderNick", "nick", "senderStaffName", "userName", "name", "senderId", "senderStaffId"} {
if v, ok := m[key]; ok && v != nil {
if s, ok := v.(string); ok && s == "" {
continue
}
return v
}
// projectChatMessage reshapes one raw message into the clean
// {sender, text, createTime} projection, rendering card/auto-reply JSON and
// marking encrypted messages via chatmsg, and recursively expanding forwarded
// chat records under "forwarded".
func projectChatMessage(m map[string]any) map[string]any {
row := map[string]any{
"sender": chatmsg.Sender(m),
"text": chatmsg.Text(m),
"createTime": chatmsg.CreateTime(m),
}
return nil
}
// chatMessageText reads a message's textual content, tolerating common text
// keys and one level of nesting (e.g. {"content":{"text":"..."}}).
func chatMessageText(m map[string]any) any {
for _, key := range []string{"text", "content", "msgContent", "message", "body"} {
v, ok := m[key]
if !ok || v == nil {
continue
}
switch t := v.(type) {
case string:
if t != "" {
return t
}
case map[string]any:
for _, inner := range []string{"text", "content", "value"} {
if s, ok := t[inner].(string); ok && s != "" {
return s
}
}
}
if forwarded := chatmsg.Forwarded(m, projectChatMessage); len(forwarded) > 0 {
row["forwarded"] = forwarded
}
return nil
}
// chatMessageCreateTime reads a message's create/send time, returning the raw
// value under whichever candidate key is present.
func chatMessageCreateTime(m map[string]any) any {
for _, key := range []string{"createTime", "sendTime", "gmtCreate", "createAt", "timestamp", "time"} {
if v, ok := m[key]; ok && v != nil {
return v
}
}
return nil
return row
}
func init() {
@@ -0,0 +1,62 @@
// 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 smart
import "testing"
// TestProjectChatMessageExpandsForwarded guards that a forwarded chat record
// ("聊天记录") exposes its nested messages under "forwarded" instead of
// collapsing to the lossy top-level "[卡片]" summary, recursing through nested
// forwards, and that the string-"null" sender is nulled out. The per-field
// behaviour (sender/text/encryption) is covered in the chatmsg package tests.
func TestProjectChatMessageExpandsForwarded(t *testing.T) {
row := projectChatMessage(map[string]any{
"sender": "hugozhu",
"content": "hugozhu与opencode-agent的聊天记录\nopencode-agent:[卡片]",
"createTime": "2026-07-20 21:41:21",
"forwardMessages": []any{
map[string]any{"sender": "null", "content": "读下冬翔发给我的最近两条消息", "createTime": "2026-07-20 09:30:33"},
map[string]any{"sender": "冬翔", "content": "W29 工作总结", "createTime": "2026-07-19 23:35:40",
// nested forward inside a forward — must expand recursively.
"forwardMessages": []any{
map[string]any{"sender": "念晨", "content": "收到", "createTime": "2026-07-19 23:36:00"},
},
},
},
})
if row["sender"] != "hugozhu" {
t.Fatalf("top sender = %v, want hugozhu", row["sender"])
}
forwarded, ok := row["forwarded"].([]map[string]any)
if !ok || len(forwarded) != 2 {
t.Fatalf("forwarded = %#v, want 2 entries", row["forwarded"])
}
if forwarded[0]["sender"] != nil {
t.Errorf("forwarded[0].sender = %v, want nil (string \"null\")", forwarded[0]["sender"])
}
if forwarded[0]["text"] != "读下冬翔发给我的最近两条消息" {
t.Errorf("forwarded[0].text = %v", forwarded[0]["text"])
}
nested, ok := forwarded[1]["forwarded"].([]map[string]any)
if !ok || len(nested) != 1 || nested[0]["sender"] != "念晨" {
t.Errorf("nested forwarded = %#v, want 1 entry from 念晨", forwarded[1]["forwarded"])
}
// A plain message must not grow a "forwarded" key.
plain := projectChatMessage(map[string]any{"sender": "念晨", "content": "hi", "createTime": "t"})
if _, has := plain["forwarded"]; has {
t.Errorf("plain message unexpectedly has forwarded key: %#v", plain)
}
}
+40 -11
View File
@@ -19,6 +19,7 @@ import (
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
)
// SearchMsg: search messages inside a single group chat by keyword in one step.
@@ -108,12 +109,7 @@ var SearchMsg = shortcut.Shortcut{
}
results := make([]map[string]any, 0, len(items))
for _, m := range items {
results = append(results, map[string]any{
"sender": searchMsgSender(m),
"time": searchMsgTime(m),
"text": searchMsgText(m),
"messageId": searchMsgMessageID(m),
})
results = append(results, searchMsgProject(m))
}
return rt.Output(map[string]any{"messages": results})
},
@@ -152,28 +148,61 @@ func searchMsgToMaps(arr []any) []map[string]any {
return out
}
// searchMsgProject reshapes one matched message into {sender, time, text,
// messageId}, running text through the shared chatmsg cleaning (card/auto-reply
// JSON → readable, ciphertext → marker) and recursively expanding any forwarded
// chat record under "forwarded".
func searchMsgProject(m map[string]any) map[string]any {
row := map[string]any{
"sender": searchMsgSender(m),
"time": searchMsgTime(m),
"text": searchMsgCleanText(m),
"messageId": searchMsgMessageID(m),
}
if forwarded := chatmsg.Forwarded(m, searchMsgProject); len(forwarded) > 0 {
row["forwarded"] = forwarded
}
return row
}
// searchMsgCleanText runs searchMsgText's extraction through chatmsg.CleanText.
func searchMsgCleanText(m map[string]any) any {
if s, ok := searchMsgText(m).(string); ok {
return chatmsg.CleanText(s)
}
return searchMsgText(m)
}
// searchMsgSender reads a message's sender display name/id, tolerating the
// common sender keys the gateway may use (including a nested sender object).
// common sender keys the gateway may use (including a nested sender object). The
// literal string "null" (carried by forwarded sub-messages) and the empty string
// are both treated as absent so they never surface as the speaker.
func searchMsgSender(m map[string]any) any {
norm := func(v any) string {
if s := searchMsgString(v); s != "" && s != "null" {
return s
}
return ""
}
for _, key := range []string{"senderName", "sender_name", "senderNick", "fromName", "senderStaffName"} {
if v := searchMsgString(m[key]); v != "" {
if v := norm(m[key]); v != "" {
return v
}
}
for _, key := range []string{"sender", "from", "senderUser"} {
if nested, ok := m[key].(map[string]any); ok {
for _, k2 := range []string{"name", "nick", "userName", "staffName", "displayName"} {
if v := searchMsgString(nested[k2]); v != "" {
if v := norm(nested[k2]); v != "" {
return v
}
}
}
if v := searchMsgString(m[key]); v != "" {
if v := norm(m[key]); v != "" {
return v
}
}
for _, key := range []string{"senderId", "sender_id", "senderUserId", "senderStaffId", "openDingTalkId"} {
if v := searchMsgString(m[key]); v != "" {
if v := norm(m[key]); v != "" {
return v
}
}
+1 -54
View File
@@ -78,11 +78,7 @@ var ThreadReplies = shortcut.Shortcut{
items := threadReplyItems(data)
results := make([]map[string]any, 0, len(items))
for _, m := range items {
results = append(results, map[string]any{
"sender": threadReplySender(m),
"text": threadReplyText(m),
"createTime": threadReplyCreateTime(m),
})
results = append(results, projectChatMessage(m))
}
return rt.Output(map[string]any{
@@ -124,55 +120,6 @@ func threadReplyItems(data map[string]any) []map[string]any {
return nil
}
// threadReplySender reads a reply's speaker display name, tolerating common
// sender-name keys.
func threadReplySender(m map[string]any) any {
for _, key := range []string{"senderName", "senderNick", "nick", "senderStaffName", "userName", "name", "senderId", "senderStaffId"} {
if v, ok := m[key]; ok && v != nil {
if s, ok := v.(string); ok && s == "" {
continue
}
return v
}
}
return nil
}
// threadReplyText reads a reply's textual content, tolerating common text keys
// and one level of nesting (e.g. {"text":{"content":"..."}}).
func threadReplyText(m map[string]any) any {
for _, key := range []string{"text", "content", "msgContent", "message", "body"} {
v, ok := m[key]
if !ok || v == nil {
continue
}
switch t := v.(type) {
case string:
if t != "" {
return t
}
case map[string]any:
for _, inner := range []string{"content", "text", "value"} {
if s, ok := t[inner].(string); ok && s != "" {
return s
}
}
}
}
return nil
}
// threadReplyCreateTime reads a reply's create/send time, returning the raw
// value under whichever candidate key is present.
func threadReplyCreateTime(m map[string]any) any {
for _, key := range []string{"createTime", "sendTime", "gmtCreate", "createAt", "timestamp", "time"} {
if v, ok := m[key]; ok && v != nil {
return v
}
}
return nil
}
func init() {
shortcut.Register(ThreadReplies)
}
+18 -2
View File
@@ -39,8 +39,24 @@ func defaultHooks() *Hooks {
base["claw-type"] = DefaultOSSClawType
return base
},
StaticServers: openStaticServers,
VisibleProducts: openVisibleProducts,
StaticServers: openStaticServers,
SupplementServers: openSupplementServers,
VisibleProducts: openVisibleProducts,
}
}
// openSupplementServers returns helper-only MCP endpoints owned by the open
// CLI. They are callable by explicit server ID but are deliberately excluded
// from VisibleProducts, so no top-level product command is generated. They
// stay separate from syncdata.StaticServers because only explicitly wired CLI
// helpers may call them; they are not public MCP product surfaces.
func openSupplementServers() []ServerInfo {
return []ServerInfo{
{
ID: "mcp-meta",
Name: "MCP 元服务",
Endpoint: "https://mcp-gw.dingtalk.com/server/89833ea5debf30c260a07ffcb5127ffa3bf0c830cd76babadb293d9861485d44",
},
}
}
+20
View File
@@ -70,4 +70,24 @@ func TestOpenVisibleProductsExcludesCompatibilityOnlyCommands(t *testing.T) {
t.Fatal("conference must remain compatibility-only and not be added to StaticServers")
}
}
if byID["mcp-meta"] {
t.Fatal("mcp-meta is helper-only and must not appear in VisibleProducts")
}
}
func TestOpenSupplementServersIncludesMCPMeta(t *testing.T) {
servers := openSupplementServers()
for _, server := range servers {
if server.ID != "mcp-meta" {
continue
}
if server.Endpoint == "" {
t.Fatal("mcp-meta has empty endpoint")
}
if len(server.Prefixes) != 0 {
t.Fatal("mcp-meta must remain helper-only without command prefixes")
}
return
}
t.Fatal("openSupplementServers() missing mcp-meta")
}
+6 -6
View File
@@ -273,8 +273,8 @@ alidocs 链接表面长得一样(`https://alidocs.dingtalk.com/i/nodes/{id}`
- 已有 userId 时直接使用 `--user`;已有 openDingTalkId 时使用 `--open-dingtalk-id`
- 纯文本/Markdown 单聊传 `--user` 时直接走 userId 发送能力,不需要先手动查询 openDingTalkId
- 富媒体消息(image/file)单聊优先使用 `--open-dingtalk-id`;传 `--user` 时 CLI 会尝试解析为 openDingTalkId 后发送
- "发文件/语音/视频到群里" — `dws chat message send ... --msg-type file --file-path <本地路径>`,CLI 内部自动上传并发送(png/jpg/pdf/mp4/zip… 任意扩展名都走这条,但**都作为「文件」消息**发出,接收方看到的是可下载的文件条目)
- "发张图片/截图(要在聊天里内联渲染成图,不是文件)" — 走图片消息链路:先 `dt_media_upload` 拿 mediaId,再 `dws chat message send ... --msg-type image --media-id <mediaId>`。**注意**:用 `--msg-type file` 发 .png 只会显示为[文件](fileId),不会渲染成图片;要图片效果必须走 `--msg-type image`
- "发本地图片/文件/语音/视频到群里" — `dws chat message send ... --msg-type file --file-path <本地路径>`,CLI 内部自动上传并发送;png/jpg/pdf/mp4/zip 等任意扩展名都走这条,接收方看到的是可下载的文件附件。图片不会内联渲染,也不会生成 mediaId
- "用已有 mediaId 发内联图片" — 仅当上游已经提供有效 mediaId 时,使用 `dws chat message send ... --msg-type image --media-id <mediaId>`;DWS CLI 不能把本地图片转换成 mediaId
- "发图片+文字说明" — 不要硬塞进一条命令;先发图片/文件消息再补一条 `--text "..."` 即可
```bash
@@ -282,7 +282,7 @@ dws chat message send --group <openConversationId> --msg-type file --file-path .
dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file-path ./report.pdf --format json
```
> ❌ 反模式:调 `dt_media_upload` / `extract_media_id.py` / `drive upload` / `drive download` 等前置工具再 `--msg-type image --media-id`。这是**旧链路**,仅当上游已持有 mediaId 才用;新场景一律 `--file-path` 直发,避免长链路与"空白图"现象。
> ❌ 反模式:先把本地文件转换成 mediaId,或先经钉盘上传再拼装聊天消息。本地图片/文件一律用 `--msg-type file --file-path` 直发;`--msg-type image --media-id` 只接受上游已经提供的有效 mediaId。
> 富媒体消息单聊优先使用 `--open-dingtalk-id`;传 `--user` 时 CLI 会尝试解析为 openDingTalkId 后发送。
**用 `chat message send-by-bot` 的场景**:
@@ -453,7 +453,7 @@ dws chat message send --group <openConversationId> --msg-type file --file-path <
dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file-path <本地路径> --format json
```
支持任意扩展名(`.png/.jpg/.gif/.bmp/.webp/.pdf/.doc/.xls/.zip/.mp3/.wav/.mp4/.avi` …),CLI 自动识别并处理。**无需** `dt_media_upload` / `extract_media_id.py` / `drive upload` / `drive download` / `chat conversation-info` / `chat file upload` 等任何前置工具调用。
支持任意扩展名(`.png/.jpg/.gif/.bmp/.webp/.pdf/.doc/.xls/.zip/.mp3/.wav/.mp4/.avi` …),CLI 自动完成上传与发送,不需要任何前置工具调用。图片文件同样作为可下载的 file 附件发送,不会内联渲染,也不会生成 mediaId。
### 图片/文件 + 文字说明
@@ -464,9 +464,9 @@ dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file
dws chat message send --open-dingtalk-id <openDingTalkId> --text "这是本周数据汇总" --format json
```
### 旧链路(mediaId)— 仅兼容场景
### 上游 mediaId — 仅兼容场景
仅当上游已经通过 `dt_media_upload` 拿到 `@lQL...` 形式的 mediaId 时使用:
仅当上游已经提供有效的 `@lQL...` 形式 mediaId 时使用;DWS CLI 不提供本地文件到 mediaId 的转换能力:
```bash
dws chat message send --group <openConversationId> --msg-type image --media-id "@lQLPD4JNnliqBq3NBQDNA8Cw" --format json
+45 -89
View File
@@ -182,8 +182,8 @@ Flags:
--group string 群聊 openConversationId (必填)
--icon-media-id string 群头像 mediaId (必填)
```
> `--icon-media-id` 有本地格式校验:必须是 `@` 开头的媒体 ID(如 `dt_media_upload` 的返回值),非法格式会在本地直接报错。
> ⚠️ 本地格式校验只查前缀。格式合法但不真实存在的 mediaId 服务端仍会静默返回成功,头像并不会真正更新。务必用 `dt_media_upload` / `chat media upload` 上传真实图片拿到的 mediaId。
> `--icon-media-id` 有本地格式校验:必须是 `@` 开头的、由可信上游提供的媒体 ID,非法格式会在本地直接报错。
> ⚠️ 本地格式校验只查前缀。格式合法但不真实存在的 mediaId 服务端仍会静默返回成功,头像并不会真正更新。务必使用真实上游媒体上传能力拿到有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传命令。
#### 更新群设置 — 更新指定群聊的设置项
@@ -527,17 +527,17 @@ Flags:
--group 指定群聊 openConversationId 发群消息;--user 指定用户 userId 发单聊;--open-dingtalk-id 指定用户 openDingTalkId 发单聊。三者只能选其一,不能同时指定。纯文本/Markdown 单聊传 --user 时直接走 userId 发送能力,不需要先手动查询 openDingTalkId。推荐使用 --text flag 传递消息内容(也支持位置参数)。可选 --title 作为消息标题。
若用户只提供了数字群号而非 openConversationId,需先调用 `chat group get-by-group-id` 将群号转为 openConversationId,再传入 --group。
--群聊时可选 --at-all @所有人,或 --at-open-dingtalk-ids 指定成员(仅群聊时生效)。
--富媒体消息:通过 --msg-type 指定类型(image/file/audio/video),audio/video 是 file 的语义别名,发给服务端仍按 file 发送;必须根据文件扩展名判断 msgType 后再发送。
--本地图片、文件、音频或视频统一用 --msg-type file --file-path;图片会作为可下载的文件附件发送。--msg-type image --media-id 仅用于上游已经提供有效 mediaId 的场景。
```
Usage:
dws chat message send [flags] [<text>]
富媒体消息 msgType 决策(必须按此规则判断,不可跳过):
文件扩展名 → msgType → 发送参数
.jpg/.jpeg/.png/.gif/.bmp/.webp → image → dt_media_upload 上传 → `python scripts/extract_media_id.py <URL>` 提取 mediaId → --msg-type image --media-id
.mp3/.wav/.m4a/.aac/.flac → audio(file) → 本地 --file-path 或 conversation-info/drive 上传 → --msg-type audio
.mp4/.mov/.avi/.mkv/.webm → video(file) → 本地 --file-path 或 conversation-info/drive 上传 → --msg-type video
其他所有(.pdf/.doc/.xls/.zip 等) → file → 本地 --file-path 或 conversation-info/drive 上传 → --msg-type file
富媒体消息路由:
场景 → msgType → 发送参数
本地图片/文件/音频/视频 → file → --file-path <本地路径>
上游已提供有效 mediaId 的内联图片 → image → --media-id <mediaId>
注意:本地 .png/.jpg 也按 file 发送,接收方看到的是可下载附件;DWS CLI 不能把本地文件转换成 mediaId。
Example:
dws chat message send --group <openconversation_id> --text "hello"
@@ -549,15 +549,13 @@ Example:
dws chat message send --group <openconversation_id> --text "hello" --uuid "unique-id-123"
dws chat message send --group <openconversation_id> --at-all "<@all> 请大家注意"
dws chat message send --group <openconversation_id> --at-open-dingtalk-ids openDingTalkId1,openDingTalkId2 "<@openDingTalkId1> <@openDingTalkId2> 请查收"
# 发送图片
# 本地图片/文件/音频/视频统一作为 file 附件发送
dws chat message send --group <openconversation_id> --msg-type file --file-path ./screenshot.png
dws chat message send --group <openconversation_id> --msg-type file --file-path ./report.pdf
dws chat message send --group <openconversation_id> --msg-type file --file-path ./recording.mp3
dws chat message send --group <openconversation_id> --msg-type file --file-path ./demo.mp4
# 仅当上游已有有效 mediaId 时发送内联图片
dws chat message send --group <openconversation_id> --msg-type image --media-id <mediaId>
# 发送文件/音频/视频(audio/video 是 file 的语义别名)
# 先 dws chat conversation-info --group <id> 获取 spaceId(取 newCSpaceIdIM)
# 再 dws drive upload --file <文件> --space-id <spaceId> 上传
# 再 dws drive info --file-id <fileId> --space-id <spaceId> 获取 dentryId
dws chat message send --group <openconversation_id> --msg-type file --dentry-id <dentryId> --space-id 24557356340 --file-name "report.pdf" --file-type "pdf" --file-path "/report.pdf" --file-size 234724
dws chat message send --group <openconversation_id> --msg-type audio --file-path ./recording.mp3
dws chat message send --group <openconversation_id> --msg-type video --file-path ./demo.mp4
Flags:
--text string 消息内容(推荐使用,也可用位置参数)
--group string 群聊 openconversation_id(群聊时必填)
@@ -566,14 +564,14 @@ Flags:
--title string 消息标题(可选,默认「消息」)
--at-all @所有人(仅群聊时生效,可选,默认 false)
--at-open-dingtalk-ids string @指定成员的 openDingTalkId 列表,逗号分隔(仅群聊时生效,可选)
--media-id string 图片 mediaId(dt_media_upload 上传后用 `python scripts/extract_media_id.py <URL>` 提取,仅 msgType=image)
--msg-type string 消息类型: image/file/audio/video(audio/video 是 file 别名;image 用 mediaId,file/audio/video 用钉盘上传)
--dentry-id int64 钉盘文件 dentryId(msgType=file 时必填,通过 drive info 获取)
--space-id int64 钉盘空间 ID(msgType=file 时必填)
--file-name string 文件名(msgType=file 时必填)
--file-type string 文件类型/扩展名(msgType=file 时必填)
--file-path string 文件路径(msgType=file 时必填)
--file-size int64 文件大小,单位字节(msgType=file 时必填)
--media-id string 上游提供的有效图片 mediaId(仅 msgType=image)
--msg-type string 消息类型: image/file/audio/video(本地文件统一使用 file;image 仅配合已有 mediaId)
--dentry-id int64 已有钉盘文件 dentryId(兼容参数,非本地文件发送的默认路径)
--space-id int64 已有钉盘文件空间 ID(元数据兼容模式)
--file-name string 已有钉盘文件名(元数据兼容模式)
--file-type string 已有钉盘文件类型/扩展名(元数据兼容模式)
--file-path string 本地文件路径(本地图片/文件/音视频配合 msgType=file 使用)
--file-size int64 已有钉盘文件大小,单位字节(元数据兼容模式)
--uuid string 幂等 UUID,相同 uuid 在 24h 内不会重复发送(可选)
--ai-tag 消息是否带 AI 发送角标(可选,默认 true)
@@ -586,36 +584,13 @@ Flags:
- **换行符**:消息内容按 Markdown 渲染,换行有两层要求,缺一不可:
1. 必须使用**真实换行符**(Unicode `U+000A`),而非字面量字符串 `\n`(反斜杠 + 字母 n)。程序或大模型构造参数时,须确保已正确反转义;否则全部内容会渲染在同一行
2. Markdown 规范下**单个换行不产生换行效果**。需要换行时请使用:段落分隔(连续两个真实换行符 `\n\n`)、行尾两个空格 + 真实换行符(硬换行 `<br>`),或直接写 HTML 的 `<br>` 标签
- 富媒体消息类型与参数对应关系:
- image(图片):--msg-type image --media-id
- audio/video:file 的语义别名,发给服务端仍是 file;可直接传本地 --file-path,或先 conversation-info 获取 spaceId → drive upload --space-id 上传 → drive info 获取 dentryId → --msg-type audio 或 --msg-type video --dentry-id --space-id --file-name --file-type --file-path --file-size
- file(文档/压缩包等其他非图片文件):可直接传本地 --file-path,或先 conversation-info 获取 spaceId → drive upload --space-id 上传 → drive info 获取 dentryId → --msg-type file --dentry-id --space-id --file-name --file-type --file-path --file-size
- mediaId 通过 dt_media_upload 上传获得,必须用脚本提取:`python scripts/extract_media_id.py "<URL>"`(输出如 @lQLPxxx,直接用于 --media-id)。禁止手动从 URL 中截取或拼接 mediaId,手动解析会因 URL 格式不稳定导致尺寸后缀残留
- 本地图片、文档、压缩包、音频和视频统一使用 `--msg-type file --file-path <本地路径>`;图片会成为可下载的 file 附件,不会内联渲染,也不会生成 mediaId
- `--msg-type image --media-id` 仅接受上游已经提供的有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传或转换能力
- audio/video 仍是兼容的 file 语义别名,但本地文件的推荐路径保持为 `--msg-type file --file-path`
- dentryId/spaceId 等参数仅用于调用方已经持有钉盘文件元数据的兼容场景,不是发送本地文件的前置步骤
- --uuid 用于幂等发送,传入相同 uuid 在 24h 内不会重复投递消息(可选,群聊和单聊均支持)
- 富媒体消息的单聊优先使用 `--open-dingtalk-id`;传 `--user` 时 CLI 会尝试解析成 openDingTalkId 后发送
- 发送文件/媒体消息时,必须先根据文件扩展名判断 msgType:图片(.jpg/.png/.gif/.bmp/.webp)→image,音频(.mp3/.wav/.m4a/.aac/.flac)→audio,视频(.mp4/.mov/.avi/.mkv/.webm)→video,其他所有→file;不可跳过此判断步骤。audio/video 是 file 别名,不代表不支持音频/视频
- 20MB 降级:图片超过 20MB 时 dt_media_upload 会失败,必须降级走钉盘上传 + Markdown 嵌入方式发送(参见「发送图片+文字消息」章节)。音频/视频/文件走钉盘上传无 20MB 限制
- 发送文字 + 文件混合消息时的完整流程:除了将文件以 Markdown 链接内嵌到文字消息中发送一条 md 消息外,还必须额外逐个发送独立的文件消息(--msg-type file),确保接收方可以直接下载原始文件。即:先发一条包含文字和文件链接的 md 消息,再对每个涉及的文件各发一条 --msg-type file 的文件消息
```
### media (上传媒体获取 mediaId)
#### 上传图片/媒体获取 mediaId — 用于 chat message send --msg-type image 等
⚠️ 前置条件:本命令需要应用凭证。必须已通过 `dws auth login --client-id <APP_KEY> --client-secret <APP_SECRET>` 登录,或设置环境变量 `DWS_CLIENT_ID` / `DWS_CLIENT_SECRET`;否则报"缺少应用凭证"。这与其他 chat 命令走用户登录态不同。
```
Usage:
dws chat media upload [flags]
Example:
dws chat media upload --file ./screenshot.png
dws chat media upload --file ./photo.jpg --type image
Flags:
--file string 本地文件路径 (必填)
--type string 媒体类型: image/voice/video/file(默认 image)
注意:
- 返回的 mediaId 可直接用于 chat message send --msg-type image --media-id
- 发图片+文字时,agent 侧一般用独立的 dt_media_upload 工具;本命令是 dws 内置的等价上传入口
- 发送文字 + 文件时,先发送 `--msg-type file --file-path` 文件消息,再补一条文本或 Markdown 说明;这是两条独立消息
```
### file (会话文件上传,已下线)
@@ -1165,7 +1140,7 @@ Flags:
#### 获取会话基础信息 — 含会话关联的钉盘共享空间 ID
获取指定会话的基础信息,包含会话关联的钉盘共享空间 ID (newCSpaceIdIM)。发送文件消息前需先调用此命令获取 spaceId,再用 drive upload --space-id 上传文件到共享空间。
获取指定会话的基础信息,包含会话关联的钉盘共享空间 ID (newCSpaceIdIM)。该 ID 可用于独立的钉盘存储操作;发送本地图片或文件到聊天不需要先调用本命令,直接使用 `chat message send --msg-type file --file-path`。
```
Usage:
dws chat conversation-info [flags]
@@ -1181,8 +1156,8 @@ Flags:
注意:
- --group、--user、--open-dingtalk-id 互斥,必须且只能指定其一
- --group 的别名: --id, --chat, --conversation-id (均可替代 --group)
- 返回值中的 newCSpaceIdIM 为会话共享空间 ID,用于 drive upload --space-id 参数
- 上传到共享空间的文件对方才能打开,上传到个人空间的文件对方无法访问
- 返回值中的 newCSpaceIdIM 为会话共享空间 ID,可用于调用方明确需要的钉盘存储流程
- 该 ID 不是发送本地聊天附件的前置条件;本地附件直接走 `chat message send --msg-type file --file-path`
```
#### 引用回复消息 — 引用某条消息并回复文字(单聊/群聊均可)
@@ -1890,7 +1865,8 @@ Flags:
用户说"转发话题/转发话题消息" → `chat message forward-topic`
用户说"置顶消息/把消息置顶" → `chat message set-top-msg`
用户说"取消置顶消息/撤销消息置顶" → `chat message unset-top-msg`
用户说"上传图片拿mediaId/上传媒体" → `chat media upload`
用户说"发送/上传本地图片或媒体到聊天" → `chat message send --msg-type file --file-path <本地路径>`
用户明确只要 mediaId → DWS CLI 当前不提供本地上传入口;仅在上游已有有效 mediaId 时使用 `chat message send --msg-type image --media-id`
用户说"群机器人列表/群里有哪些机器人/查看群机器人" → `chat group bots`
用户说"从群里移除机器人/踢出机器人" → `chat group members remove-bot`
用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 botOpenDingTalkId 可发单聊)
@@ -1916,7 +1892,7 @@ Flags:
- `chat message list-topic-replies` — 拉取群话题的回复消息列表
- `chat message list-focused` — 拉取特别关注人的消息,cursor 分页
- `chat list-top-conversations` — 拉取置顶会话列表(用户询问"置顶会话"或"置顶消息"时路由到此),cursor 分页
- `chat message send` — 以当前用户身份发消息(群聊或单聊),text 为位置参数;支持 --msg-type 发送富媒体消息:image(图片)、file/audio/video(audio/video 是 file 别名),图片的 mediaId 通过 dt_media_upload 上传获得,其他文件可直接用 --file-path 或先获取会话共享空间再上传钉盘
- `chat message send` — 以当前用户身份发消息(群聊或单聊),text 为位置参数;本地图片/文件/音视频统一用 `--msg-type file --file-path`,其中图片显示为可下载附件而非内联图片;`--msg-type image --media-id` 只用于上游已经提供有效 mediaId 的场景,DWS CLI 不能从本地文件生成 mediaId
- `chat message search` — 按关键词搜索消息内容(跨所有会话,可选指定群)
- `chat search-common` — 搜索共同群,查询指定人共同所在的群聊(AND=所有人都在,OR=任一人在)
- `chat message send-by-bot` — 以**机器人**身份发消息(群聊或单聊),text 为 --text flag
@@ -2073,35 +2049,17 @@ dws chat message send-by-bot --robot-code <robot-code> --group <openconversation
```
### 发送图片+文字 / 文件+文字消息(跨产品: drive → chat)
### 发送图片/文件 + 文字说明(两条消息)
- **图片+文字**:图片**必须**通过 `dt_media_upload` 工具(非 dws 命令,是 agent 可调用的独立 tool)上传获取 mediaId,然后用 Markdown 嵌入方式发送。**禁止**使用钉盘上传图片。
- **文件+文字**:文件通过钉盘上传 + Markdown 嵌入方式发送。
纯发图片/文件(不带文字)的完整流程见 [intent-guide.md](../intent-guide.md) 对应章节。
本地图片和文件先用 `--msg-type file --file-path` 发送,再补一条文本消息说明;这是两条独立消息,不需要媒体上传或钉盘前置步骤。图片会显示为可下载的文件附件,不会内联渲染。
```bash
# === 图片+文字 ===
# Step 1: 调用 dt_media_upload 工具上传图片(这是一个独立的 tool,不是 dws 命令)
# dt_media_upload 会返回 mediaId(如 @lQLPxxx)
# 提取 mediaId 可使用脚本: python extract_media_id.py "<返回的URL>"
# Step 2: 用 Markdown 语法发送(mediaId 作为图片引用)
dws chat message send --group <openconversation_id> \
--text "![截图](mediaId) 这是本周的数据汇总" --format json
# === 文件+文字 ===
# Step 1: 上传文件到钉盘
dws drive upload --file "报告.pdf" --format json
# Step 2: 获取下载链接
dws drive download --file-id <dentryUuid> --format json
# Step 3: 用 Markdown 语法发送
dws chat message send --group <openconversation_id> \
--text "[报告.pdf](下载链接) 这是季度报告" --format json
dws chat message send --group <openconversation_id> --msg-type file --file-path ./screenshot.png --format json
dws chat message send --group <openconversation_id> --text "这是本周的数据汇总" --format json
```
如果调用方已经从上游取得有效 mediaId,可以先用 `--msg-type image --media-id` 发送内联图片,再补一条文本消息;DWS CLI 本身不能把本地图片转换成 mediaId。
### 发送图片 / 文件(统一一条命令)
**`dws chat message send --msg-type file --file-path <本地路径>`** 适用于所有发图片/文件场景,任意扩展名。CLI 内部完成上传与发送,无需任何前置工具调用。
@@ -2122,7 +2080,7 @@ dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file
dws chat message send --open-dingtalk-id <openDingTalkId> --text "这是本周数据汇总" --format json
```
**旧链路(仅当上游已经持有 `dt_media_upload` 返回的 mediaId 时才用)**:
**已有 mediaId(仅当上游已经提供有效 mediaId 时才用)**:
```bash
dws chat message send --group <openConversationId> --msg-type image --media-id "@lQLPD4JNnliqBq3NBQDNA8Cw" --format json
@@ -2182,7 +2140,7 @@ Flags:
| `chat message search` | `nextCursor` | 下次 message search 的 --cursor |
| `chat message search-advanced` | `nextCursor` | 下次 message search-advanced 的 --cursor |
| `chat search-common` | `openConversationId` | message send/list 等的 --group |
| `chat conversation-info` | `newCSpaceIdIM` | drive upload 的 --space-id(发送文件消息前获取共享空间) |
| `chat conversation-info` | `newCSpaceIdIM` | 独立钉盘存储流程的共享空间 ID;不是发送本地聊天附件的前置条件 |
| `chat file upload` | 无(已下线) | 不要调用;常规发图/发文件用 `chat message send --msg-type file --file-path` |
| `chat message list` | `openMsgId` | message read-status 的 --message-id |
| `chat group-role list` | `openRoleId` | group-role update/remove/set-user/remove-user 的 --role-id |
@@ -2190,7 +2148,6 @@ Flags:
| `chat category list` | `categoryId` | category list-conversations 的 --category-id |
| `chat group get-by-group-id` | `openConversationId` | 同 chat search,将群号转为 openConversationId |
| `chat message send-card` | `bizId` | update-card 的 --biz-id |
| `drive download` | 下载链接 | message send 的 Markdown 图片/链接语法 |
| `chat message list` | `openMessageId` | message reply 的 --ref-msg-id、message forward 的 --msg-id |
| `chat search` | `openConversationId` | set-top 的 --conversation-id、group-mute / group-mute-member 的 --group |
@@ -2207,7 +2164,7 @@ Flags:
- 如果不传 `--uuid`,每次调用都视为新消息,重试可能导致消息重复发送
- 此参数适用于 `chat message send`(群聊和单聊均支持)
- `--group` 为群聊会话 ID (openconversation_id),可从群搜索或群聊信息中获取
- `chat message send` 的 text 是位置参数(恰好 1 个),非 flag;群聊用 `--group`,单聊用 `--user`(userId)或 `--open-dingtalk-id`(openDingTalkId),三者互斥;纯文本/Markdown 单聊传 `--user` 时直接走 userId 发送能力;`--at-all`、`--at-open-dingtalk-ids` 仅在 `--group` 群聊时生效;富媒体消息通过 `--msg-type` 指定类型(image/file/audio/video),必须显式指定;发送文件/媒体消息时,必须先根据文件扩展名判断 msgType:图片→image,音频→audio,视频→video,其他→file,不可跳过此判断
- `chat message send` 的 text 是位置参数(恰好 1 个),非 flag;群聊用 `--group`,单聊用 `--user`(userId)或 `--open-dingtalk-id`(openDingTalkId),三者互斥;纯文本/Markdown 单聊传 `--user` 时直接走 userId 发送能力;`--at-all`、`--at-open-dingtalk-ids` 仅在 `--group` 群聊时生效;本地图片/文件/音视频统一用 `--msg-type file --file-path`,其中图片是可下载附件;`--msg-type image --media-id` 仅用于上游已经提供有效 mediaId 的内联图片
- `chat message list-all` 的四个参数(--start、--end、--limit、--cursor)每次请求都必须传递;翻页时用响应中的 nextCursor 值作为下次 --cursor
- `chat message list` 的 `--group`、`--user`、`--open-dingtalk-id` 三者互斥,必须且只能指定其一
- `chat message list-by-sender` 不需要指定单聊/群聊,返回结果自带会话类型标识;`--sender-user-id`(userId)与 `--sender-open-dingtalk-id`(openDingTalkId)二选一;时间用 `--start`/`--end`(ISO-8601),分页用 `--limit`/`--cursor`
@@ -2228,7 +2185,7 @@ Flags:
- `chat group transfer-owner` 转让群主,需传 --group(openConversationId);新群主 userId 用 `--user`,openDingTalkId 用 `--new-owner`
- `chat group invite-url` 获取群邀请链接,需传 --group(openConversationId),可选 --expires-seconds 指定有效期(秒,0=永久)
- `chat group quit` 退出群聊,需传 --group(openConversationId)
- `chat group update-icon` 更新群头像,需传 --group(openConversationId)和 --icon-media-id(mediaId)
- `chat group update-icon` 更新群头像,需传 --group(openConversationId)和由可信上游提供的有效 --icon-media-id(mediaId);DWS CLI 不能从本地图片生成该 ID
- `chat group update-settings` 更新群设置,需传 --group(openConversationId)、--setting-key(设置项 key)、--status(0=关闭 1=开启)
- `chat message send-card` 创建并推送流式卡片,群聊传 --group,单聊传 --receiver,二者互斥;不传 content,后续通过 update-card 更新内容
- `chat message update-card` 流式更新卡片内容,需传 --biz-id(创建卡片返回的业务 ID)、--content、--flow-status
@@ -2258,9 +2215,8 @@ Flags:
|------|------|------|
| [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"` |
| [extract_media_id.py](../../scripts/extract_media_id.py) | 从 dt_media_upload URL 提取 mediaId | `python extract_media_id.py "<URL>"`(输出如 @lQLPxxx,直接用于 --media-id) |
## 相关产品
- [contact](./contact.md) — 搜索同事/好友,获取 userId 用于 --user、send-by-bot --users、send-by-bot --at-user-ids、list-by-sender --sender-user-id;获取 openDingTalkId 用于 message send 的 --at-open-dingtalk-ids、--open-dingtalk-id、send-by-bot --open-dingtalk-ids、send-by-bot --at-open-dingtalk-ids、list-by-sender 的 --sender-open-dingtalk-id
- [drive](./drive.md) — 上传文件获取下载链接,用于 Markdown 图片/文件消息
- [drive](./drive.md) — 钉盘文件存储与下载;不是发送本地聊天图片/文件的前置步骤
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""
从 dt_media_upload 返回的 URL 中提取 mediaId。
用法:
python extract_media_id.py <url>
python extract_media_id.py "https://down.dingtalk.com/media/lQLPD4JNnliqBq3NBQDNA8Cw_960_1280.png"
# 输出: @lQLPD4JNnliqBq3NBQDNA8Cw
依赖: 无(纯 Python 标准库)
典型工作流(发送图片/语音/视频消息):
# 1. 用 dt_media_upload 上传文件,获得 URL
# 2. 用本脚本从 URL 提取 mediaId
python extract_media_id.py "<dt_media_upload 返回的 URL>"
# 输出: @lQLPxxx(直接用于 --media-id 或 --pic-url 参数)
"""
import argparse
import re
import sys
from urllib.parse import urlparse
def extract_media_id(url: str) -> str:
"""从 dt_media_upload 返回的 URL 中提取 mediaId。
支持的 URL 格式示例:
https://down.dingtalk.com/media/lQLPD4JNnliqBq3NBQDNA8Cw_960_1280.png
https://down.dingtalk.com/media/lQLPD4JNnliqBq3NBQDNA8Cw.png
https://down.dingtalk.com/media/lQLPD4JNnliqBq3NBQDNA8Cw
提取规则:
1. 取 URL 路径中 /media/ 之后的部分
2. 去除尾部的 _数字_数字.扩展名 后缀(如 _960_1280.png)
3. 如果没有尺寸后缀,去除尾部的 .扩展名
4. 加上 @ 前缀
返回: @lQLPD4JNnliqBq3NBQDNA8Cw
"""
url = url.strip()
# 提取路径部分
parsed = urlparse(url)
path = parsed.path # e.g. /media/lQLPxxx_960_1280.png
# 取 /media/ 之后的部分
media_prefix = "/media/"
idx = path.find(media_prefix)
if idx == -1:
print(f"错误:URL 中未找到 /media/ 路径: {url}", file=sys.stderr)
sys.exit(1)
raw = path[idx + len(media_prefix):] # e.g. lQLPxxx_960_1280.png
if not raw:
print(f"错误:/media/ 后无内容: {url}", file=sys.stderr)
sys.exit(1)
# 尝试去除 _数字_数字.扩展名 后缀(如 _960_1280.png)
match = re.match(r'^(.+?)(_\d+_\d+\.\w+)$', raw)
if match:
media_id = match.group(1)
else:
# 没有尺寸后缀,尝试去除 .扩展名
match2 = re.match(r'^(.+)\.\w+$', raw)
if match2:
media_id = match2.group(1)
else:
# 无后缀,直接使用
media_id = raw
return f"@{media_id}"
def main():
parser = argparse.ArgumentParser(
description="从 dt_media_upload 返回的 URL 中提取 mediaId"
)
parser.add_argument("url", help="dt_media_upload 返回的文件 URL")
args = parser.parse_args()
media_id = extract_media_id(args.url)
print(media_id)
if __name__ == "__main__":
main()
+1 -1
View File
@@ -96,6 +96,6 @@ metadata:
## 跨产品协作
- 收件人是人名 → 先用 `dingtalk-contact` 或 `dingtalk-aisearch` 拿 `openDingTalkId` / `userId`
- 要发图片/文件 → 先 `dt_media_upload` 上传 → `python scripts/extract_media_id.py "<URL>"` 提取 mediaId → 再用 `--media-id`
- 要发本地图片/文件 → 直接用 `dws chat message send --msg-type file --file-path <本地路径>`;图片会作为可下载的文件附件发送,不会内联渲染。只有上游已提供有效 mediaId 时才用 `--msg-type image --media-id`,DWS CLI 不能把本地文件转换成 mediaId
- 紧急升级(应用内/短信/电话)→ 切到 `dingtalk-ding`
- 发邮件 → 切到 `dingtalk-mail`
+44 -87
View File
@@ -184,8 +184,8 @@ Flags:
--icon-media-id string 群头像 mediaId (必填)
```
> `--icon-media-id` 必须是 `@` 开头的媒体 ID(如 dt_media_upload 返回值),非法格式会本地报错。
> ⚠️ 本地格式校验只查前缀。格式合法但不真实存在的 mediaId 服务端仍会静默返回成功,头像并不会真正更新。务必用 `dt_media_upload` / `chat media upload` 上传真实图片拿到的 mediaId。
> `--icon-media-id` 必须是 `@` 开头的、由可信上游提供的媒体 ID,非法格式会本地报错。
> ⚠️ 本地格式校验只查前缀。格式合法但不真实存在的 mediaId 服务端仍会静默返回成功,头像并不会真正更新。务必使用真实上游媒体上传能力拿到有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传命令。
#### 更新群设置 — 更新指定群聊的设置项
@@ -529,17 +529,17 @@ Flags:
--group 指定群聊 openConversationId 发群消息;--user 指定用户 userId 发单聊;--open-dingtalk-id 指定用户 openDingTalkId 发单聊。三者只能选其一,不能同时指定。纯文本/Markdown 单聊传 --user 时直接走 userId 发送能力,不需要先手动查询 openDingTalkId。推荐使用 --text flag 传递消息内容(也支持位置参数)。可选 --title 作为消息标题。
若用户只提供了数字群号而非 openConversationId,需先调用 `chat group get-by-group-id` 将群号转为 openConversationId,再传入 --group。
--群聊时可选 --at-all @所有人,或 --at-open-dingtalk-ids 指定成员(仅群聊时生效)。
--富媒体消息:通过 --msg-type 指定类型(image/file/audio/video),audio/video 是 file 的语义别名,发给服务端仍按 file 发送;必须根据文件扩展名判断 msgType 后再发送。
--本地图片、文件、音频或视频统一用 --msg-type file --file-path;图片会作为可下载的文件附件发送。--msg-type image --media-id 仅用于上游已经提供有效 mediaId 的场景。
```
Usage:
dws chat message send [flags] [<text>]
富媒体消息 msgType 决策(必须按此规则判断,不可跳过):
文件扩展名 → msgType → 发送参数
.jpg/.jpeg/.png/.gif/.bmp/.webp → image → dt_media_upload 上传 → `python scripts/extract_media_id.py <URL>` 提取 mediaId → --msg-type image --media-id
.mp3/.wav/.m4a/.aac/.flac → audio(file) → 本地 --file-path 或 conversation-info/drive 上传 → --msg-type audio
.mp4/.mov/.avi/.mkv/.webm → video(file) → 本地 --file-path 或 conversation-info/drive 上传 → --msg-type video
其他所有(.pdf/.doc/.xls/.zip 等) → file → 本地 --file-path 或 conversation-info/drive 上传 → --msg-type file
富媒体消息路由:
场景 → msgType → 发送参数
本地图片/文件/音频/视频 → file → --file-path <本地路径>
上游已提供有效 mediaId 的内联图片 → image → --media-id <mediaId>
注意:本地 .png/.jpg 也按 file 发送,接收方看到的是可下载附件;DWS CLI 不能把本地文件转换成 mediaId。
Example:
dws chat message send --group <openconversation_id> --text "hello"
@@ -551,15 +551,13 @@ Example:
dws chat message send --group <openconversation_id> --text "hello" --uuid "unique-id-123"
dws chat message send --group <openconversation_id> --at-all "<@all> 请大家注意"
dws chat message send --group <openconversation_id> --at-open-dingtalk-ids openDingTalkId1,openDingTalkId2 "<@openDingTalkId1> <@openDingTalkId2> 请查收"
# 发送图片
# 本地图片/文件/音频/视频统一作为 file 附件发送
dws chat message send --group <openconversation_id> --msg-type file --file-path ./screenshot.png
dws chat message send --group <openconversation_id> --msg-type file --file-path ./report.pdf
dws chat message send --group <openconversation_id> --msg-type file --file-path ./recording.mp3
dws chat message send --group <openconversation_id> --msg-type file --file-path ./demo.mp4
# 仅当上游已有有效 mediaId 时发送内联图片
dws chat message send --group <openconversation_id> --msg-type image --media-id <mediaId>
# 发送文件/音频/视频(audio/video 是 file 的语义别名)
# 先 dws chat conversation-info --group <id> 获取 spaceId(取 newCSpaceIdIM)
# 再 dws drive upload --file <文件> --space-id <spaceId> 上传
# 再 dws drive info --file-id <fileId> --space-id <spaceId> 获取 dentryId
dws chat message send --group <openconversation_id> --msg-type file --dentry-id <dentryId> --space-id 24557356340 --file-name "report.pdf" --file-type "pdf" --file-path "/report.pdf" --file-size 234724
dws chat message send --group <openconversation_id> --msg-type audio --file-path ./recording.mp3
dws chat message send --group <openconversation_id> --msg-type video --file-path ./demo.mp4
Flags:
--text string 消息内容(推荐使用,也可用位置参数)
--group string 群聊 openconversation_id(群聊时必填)
@@ -568,14 +566,14 @@ Flags:
--title string 消息标题(可选,默认「消息」)
--at-all @所有人(仅群聊时生效,可选,默认 false)
--at-open-dingtalk-ids string @指定成员的 openDingTalkId 列表,逗号分隔(仅群聊时生效,可选)
--media-id string 图片 mediaId(dt_media_upload 上传后用 `python scripts/extract_media_id.py <URL>` 提取,仅 msgType=image)
--msg-type string 消息类型: image/file/audio/video(audio/video 是 file 别名;image 用 mediaId,file/audio/video 用钉盘上传)
--dentry-id int64 钉盘文件 dentryId(msgType=file 时必填,通过 drive info 获取)
--space-id int64 钉盘空间 ID(msgType=file 时必填)
--file-name string 文件名(msgType=file 时必填)
--file-type string 文件类型/扩展名(msgType=file 时必填)
--file-path string 文件路径(msgType=file 时必填)
--file-size int64 文件大小,单位字节(msgType=file 时必填)
--media-id string 上游提供的有效图片 mediaId(仅 msgType=image)
--msg-type string 消息类型: image/file/audio/video(本地文件统一使用 file;image 仅配合已有 mediaId)
--dentry-id int64 已有钉盘文件 dentryId(兼容参数,非本地文件发送的默认路径)
--space-id int64 已有钉盘文件空间 ID(元数据兼容模式)
--file-name string 已有钉盘文件名(元数据兼容模式)
--file-type string 已有钉盘文件类型/扩展名(元数据兼容模式)
--file-path string 本地文件路径(本地图片/文件/音视频配合 msgType=file 使用)
--file-size int64 已有钉盘文件大小,单位字节(元数据兼容模式)
--uuid string 幂等 UUID,相同 uuid 在 24h 内不会重复发送(可选)
--ai-tag 消息是否带 AI 发送角标(可选,默认 true)
@@ -588,16 +586,13 @@ Flags:
- **换行符**:消息内容按 Markdown 渲染,换行有两层要求,缺一不可:
1. 必须使用**真实换行符**(Unicode `U+000A`),而非字面量字符串 `\n`(反斜杠 + 字母 n)。程序或大模型构造参数时,须确保已正确反转义;否则全部内容会渲染在同一行
2. Markdown 规范下**单个换行不产生换行效果**。需要换行时请使用:段落分隔(连续两个真实换行符 `\n\n`)、行尾两个空格 + 真实换行符(硬换行 `<br>`),或直接写 HTML 的 `<br>` 标签
- 富媒体消息类型与参数对应关系:
- image(图片):--msg-type image --media-id
- audio/video:file 的语义别名,发给服务端仍是 file;可直接传本地 --file-path,或先 conversation-info 获取 spaceId → drive upload --space-id 上传 → drive info 获取 dentryId → --msg-type audio 或 --msg-type video --dentry-id --space-id --file-name --file-type --file-path --file-size
- file(文档/压缩包等其他非图片文件):可直接传本地 --file-path,或先 conversation-info 获取 spaceId → drive upload --space-id 上传 → drive info 获取 dentryId → --msg-type file --dentry-id --space-id --file-name --file-type --file-path --file-size
- mediaId 通过 dt_media_upload 上传获得,必须用脚本提取:`python scripts/extract_media_id.py "<URL>"`(输出如 @lQLPxxx,直接用于 --media-id)。禁止手动从 URL 中截取或拼接 mediaId,手动解析会因 URL 格式不稳定导致尺寸后缀残留
- 本地图片、文档、压缩包、音频和视频统一使用 `--msg-type file --file-path <本地路径>`;图片会成为可下载的 file 附件,不会内联渲染,也不会生成 mediaId
- `--msg-type image --media-id` 仅接受上游已经提供的有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传或转换能力
- audio/video 仍是兼容的 file 语义别名,但本地文件的推荐路径保持为 `--msg-type file --file-path`
- dentryId/spaceId 等参数仅用于调用方已经持有钉盘文件元数据的兼容场景,不是发送本地文件的前置步骤
- --uuid 用于幂等发送,传入相同 uuid 在 24h 内不会重复投递消息(可选,群聊和单聊均支持)
- 富媒体消息的单聊优先使用 `--open-dingtalk-id`;传 `--user` 时 CLI 会尝试解析成 openDingTalkId 后发送
- 发送文件/媒体消息时,必须先根据文件扩展名判断 msgType:图片(.jpg/.png/.gif/.bmp/.webp)→image,音频(.mp3/.wav/.m4a/.aac/.flac)→audio,视频(.mp4/.mov/.avi/.mkv/.webm)→video,其他所有→file;不可跳过此判断步骤。audio/video 是 file 别名,不代表不支持音频/视频
- 20MB 降级:图片超过 20MB 时 dt_media_upload 会失败,必须降级走钉盘上传 + Markdown 嵌入方式发送(参见「发送图片+文字消息」章节)。音频/视频/文件走钉盘上传无 20MB 限制
- 发送文字 + 文件混合消息时的完整流程:除了将文件以 Markdown 链接内嵌到文字消息中发送一条 md 消息外,还必须额外逐个发送独立的文件消息(--msg-type file),确保接收方可以直接下载原始文件。即:先发一条包含文字和文件链接的 md 消息,再对每个涉及的文件各发一条 --msg-type file 的文件消息
- 发送文字 + 文件时,先发送 `--msg-type file --file-path` 文件消息,再补一条文本或 Markdown 说明;这是两条独立消息
```
#### 查询消息发送状态 — 查询以当前用户身份发送的消息的发送状态
@@ -1130,7 +1125,7 @@ Flags:
#### 获取会话基础信息 — 含会话关联的钉盘共享空间 ID
获取指定会话的基础信息,包含会话关联的钉盘共享空间 ID (newCSpaceIdIM)。发送文件消息前需先调用此命令获取 spaceId,再用 drive upload --space-id 上传文件到共享空间。
获取指定会话的基础信息,包含会话关联的钉盘共享空间 ID (newCSpaceIdIM)。该 ID 可用于独立的钉盘存储操作;发送本地图片或文件到聊天不需要先调用本命令,直接使用 `chat message send --msg-type file --file-path`。
```
Usage:
dws chat conversation-info [flags]
@@ -1146,8 +1141,8 @@ Flags:
注意:
- --group、--user、--open-dingtalk-id 互斥,必须且只能指定其一
- --group 的别名: --id, --chat, --conversation-id (均可替代 --group)
- 返回值中的 newCSpaceIdIM 为会话共享空间 ID,用于 drive upload --space-id 参数
- 上传到共享空间的文件对方才能打开,上传到个人空间的文件对方无法访问
- 返回值中的 newCSpaceIdIM 为会话共享空间 ID,可用于调用方明确需要的钉盘存储流程
- 该 ID 不是发送本地聊天附件的前置条件;本地附件直接走 `chat message send --msg-type file --file-path`
```
#### 引用回复消息 — 引用某条消息并回复文字(单聊/群聊均可)
@@ -1511,25 +1506,6 @@ Flags:
- 支持单聊和群聊,openConversationId 可通过 chat search(群聊)或 chat conversation-info(单聊)获取
```
### media (上传媒体获取 mediaId)
#### 上传图片/媒体获取 mediaId — 用于 chat message send --msg-type image 等
⚠️ 前置条件:本命令需要应用凭证。必须已通过 `dws auth login --client-id <APP_KEY> --client-secret <APP_SECRET>` 登录,或设置环境变量 `DWS_CLIENT_ID` / `DWS_CLIENT_SECRET`;否则报"缺少应用凭证"。这与其他 chat 命令走用户登录态不同。
```
Usage:
dws chat media upload [flags]
Example:
dws chat media upload --file ./screenshot.png
dws chat media upload --file ./photo.jpg --type image
Flags:
--file string 本地文件路径 (必填)
--type string 媒体类型: image/voice/video/file(默认 image)
注意:
- 返回的 mediaId 可直接用于 chat message send --msg-type image --media-id
```
### hide (隐藏会话)
#### 隐藏会话 — 在会话列表中隐藏指定会话(支持单聊/群聊),收到新消息时会重新出现
@@ -1855,7 +1831,8 @@ Flags:
用户说"转发话题/转发话题消息" → `chat message forward-topic`
用户说"置顶消息/把消息置顶" → `chat message set-top-msg`
用户说"取消置顶消息/撤销消息置顶" → `chat message unset-top-msg`
用户说"上传图片拿mediaId/上传媒体" → `chat media upload`
用户说"发送/上传本地图片或媒体到聊天" → `chat message send --msg-type file --file-path <本地路径>`
用户明确只要 mediaId → DWS CLI 当前不提供本地上传入口;仅在上游已有有效 mediaId 时使用 `chat message send --msg-type image --media-id`
用户说"群机器人列表/群里有哪些机器人/查看群机器人" → `chat group bots`
用户说"从群里移除机器人/踢出机器人" → `chat group members remove-bot`
用户说"搜索机器人/找机器人/查机器人/帮我找XXX机器人" → `chat bot find`(全部可用机器人,额外返回 botOpenDingTalkId 可发单聊)
@@ -1881,7 +1858,7 @@ Flags:
- `chat message list-topic-replies` — 拉取群话题的回复消息列表
- `chat message list-focused` — 拉取特别关注人的消息,cursor 分页
- `chat list-top-conversations` — 拉取置顶会话列表(用户询问"置顶会话"或"置顶消息"时路由到此),cursor 分页
- `chat message send` — 以当前用户身份发消息(群聊或单聊),text 为位置参数;支持 --msg-type 发送富媒体消息:image(图片)、file/audio/video(audio/video 是 file 别名),图片的 mediaId 通过 dt_media_upload 上传获得,其他文件可直接用 --file-path 或先获取会话共享空间再上传钉盘
- `chat message send` — 以当前用户身份发消息(群聊或单聊),text 为位置参数;本地图片/文件/音视频统一用 `--msg-type file --file-path`,其中图片显示为可下载附件而非内联图片;`--msg-type image --media-id` 只用于上游已经提供有效 mediaId 的场景,DWS CLI 不能从本地文件生成 mediaId
- `chat message search` — 按关键词搜索消息内容(跨所有会话,可选指定群)
- `chat search-common` — 搜索共同群,查询指定人共同所在的群聊(AND=所有人都在,OR=任一人在)
- `chat message send-by-bot` — 以**机器人**身份发消息(群聊或单聊),text 为 --text flag
@@ -2028,35 +2005,17 @@ dws chat message send-by-bot --robot-code <robot-code> --group <openconversation
```
### 发送图片+文字 / 文件+文字消息(跨产品: drive → chat)
### 发送图片/文件 + 文字说明(两条消息)
- **图片+文字**:图片**必须**通过 `dt_media_upload` 工具(非 dws 命令,是 agent 可调用的独立 tool)上传获取 mediaId,然后用 Markdown 嵌入方式发送。**禁止**使用钉盘上传图片。
- **文件+文字**:文件通过钉盘上传 + Markdown 嵌入方式发送。
纯发图片/文件(不带文字)的完整流程见 [intent-guide.md](./01-messaging.md) 对应章节。
本地图片和文件先用 `--msg-type file --file-path` 发送,再补一条文本消息说明;这是两条独立消息,不需要媒体上传或钉盘前置步骤。图片会显示为可下载的文件附件,不会内联渲染。
```bash
# === 图片+文字 ===
# Step 1: 调用 dt_media_upload 工具上传图片(这是一个独立的 tool,不是 dws 命令)
# dt_media_upload 会返回 mediaId(如 @lQLPxxx)
# 提取 mediaId 可使用脚本: python extract_media_id.py "<返回的URL>"
# Step 2: 用 Markdown 语法发送(mediaId 作为图片引用)
dws chat message send --group <openconversation_id> \
--text "![截图](mediaId) 这是本周的数据汇总" --format json
# === 文件+文字 ===
# Step 1: 上传文件到钉盘
dws drive upload --file "报告.pdf" --format json
# Step 2: 获取下载链接
dws drive download --file-id <dentryUuid> --format json
# Step 3: 用 Markdown 语法发送
dws chat message send --group <openconversation_id> \
--text "[报告.pdf](下载链接) 这是季度报告" --format json
dws chat message send --group <openconversation_id> --msg-type file --file-path ./screenshot.png --format json
dws chat message send --group <openconversation_id> --text "这是本周的数据汇总" --format json
```
如果调用方已经从上游取得有效 mediaId,可以先用 `--msg-type image --media-id` 发送内联图片,再补一条文本消息;DWS CLI 本身不能把本地图片转换成 mediaId。
#### 创建并推送流式卡片 — 向群聊或单聊发送流式卡片消息
群聊传 --group,单聊传 --receiver,二者互斥。
@@ -2111,14 +2070,13 @@ Flags:
| `chat message search` | `nextCursor` | 下次 message search 的 --cursor |
| `chat message search-advanced` | `nextCursor` | 下次 message search-advanced 的 --cursor |
| `chat search-common` | `openConversationId` | message send/list 等的 --group |
| `chat conversation-info` | `newCSpaceIdIM` | drive upload 的 --space-id(发送文件消息前获取共享空间) |
| `chat conversation-info` | `newCSpaceIdIM` | 独立钉盘存储流程的共享空间 ID;不是发送本地聊天附件的前置条件 |
| `chat message list` | `openMsgId` | message read-status 的 --message-id |
| `chat group-role list` | `openRoleId` | group-role update/remove/set-user/remove-user 的 --role-id |
| `chat message create-text-emotion` | `emotionId` | add-text-emotion 的 --emotion-id |
| `chat category list` | `categoryId` | category list-conversations 的 --category-id |
| `chat group get-by-group-id` | `openConversationId` | 同 chat search,将群号转为 openConversationId |
| `chat message send-card` | `bizId` | update-card 的 --biz-id |
| `drive download` | 下载链接 | message send 的 Markdown 图片/链接语法 |
| `chat message list` | `openMessageId` | message reply 的 --ref-msg-id、message forward 的 --msg-id |
| `chat search` | `openConversationId` | set-top 的 --conversation-id、group-mute / group-mute-member 的 --group |
@@ -2135,7 +2093,7 @@ Flags:
- 如果不传 `--uuid`,每次调用都视为新消息,重试可能导致消息重复发送
- 此参数适用于 `chat message send`(群聊和单聊均支持)
- `--group` 为群聊会话 ID (openconversation_id),可从群搜索或群聊信息中获取
- `chat message send` 的 text 是位置参数(恰好 1 个),非 flag;群聊用 `--group`,单聊用 `--user`(userId)或 `--open-dingtalk-id`(openDingTalkId),三者互斥;纯文本/Markdown 单聊传 `--user` 时直接走 userId 发送能力;`--at-all`、`--at-open-dingtalk-ids` 仅在 `--group` 群聊时生效;富媒体消息通过 `--msg-type` 指定类型(image/file/audio/video),必须显式指定;发送文件/媒体消息时,必须先根据文件扩展名判断 msgType:图片→image,音频→audio,视频→video,其他→file,不可跳过此判断
- `chat message send` 的 text 是位置参数(恰好 1 个),非 flag;群聊用 `--group`,单聊用 `--user`(userId)或 `--open-dingtalk-id`(openDingTalkId),三者互斥;纯文本/Markdown 单聊传 `--user` 时直接走 userId 发送能力;`--at-all`、`--at-open-dingtalk-ids` 仅在 `--group` 群聊时生效;本地图片/文件/音视频统一用 `--msg-type file --file-path`,其中图片是可下载附件;`--msg-type image --media-id` 仅用于上游已经提供有效 mediaId 的内联图片
- `chat message list-all` 的四个参数(--start、--end、--limit、--cursor)每次请求都必须传递;翻页时用响应中的 nextCursor 值作为下次 --cursor
- `chat message list` 的 `--group`、`--user`、`--open-dingtalk-id` 三者互斥,必须且只能指定其一
- `chat message list-by-sender` 不需要指定单聊/群聊,返回结果自带会话类型标识;`--sender-user-id`(userId)与 `--sender-open-dingtalk-id`(openDingTalkId)二选一;时间用 `--start`/`--end`(ISO-8601),分页用 `--limit`/`--cursor`
@@ -2156,7 +2114,7 @@ Flags:
- `chat group transfer-owner` 转让群主,需传 --group(openConversationId);新群主 userId 用 `--user`,openDingTalkId 用 `--new-owner`
- `chat group invite-url` 获取群邀请链接,需传 --group(openConversationId),可选 --expires-seconds 指定有效期(秒,0=永久)
- `chat group quit` 退出群聊,需传 --group(openConversationId)
- `chat group update-icon` 更新群头像,需传 --group(openConversationId)和 --icon-media-id(mediaId)
- `chat group update-icon` 更新群头像,需传 --group(openConversationId)和由可信上游提供的有效 --icon-media-id(mediaId);DWS CLI 不能从本地图片生成该 ID
- `chat group update-settings` 更新群设置,需传 --group(openConversationId)、--setting-key(设置项 key)、--status(0=关闭 1=开启)
- `chat message send-card` 创建并推送流式卡片,群聊传 --group,单聊传 --receiver,二者互斥;不传 content,后续通过 update-card 更新内容
- `chat message update-card` 流式更新卡片内容,需传 --biz-id(创建卡片返回的业务 ID)、--content、--flow-status
@@ -2183,9 +2141,8 @@ Flags:
|------|------|------|
| [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"` |
| [extract_media_id.py](../scripts/extract_media_id.py) | 从 dt_media_upload URL 提取 mediaId | `python extract_media_id.py "<URL>"`(输出如 @lQLPxxx,直接用于 --media-id) |
## 相关产品
- [contact](../../dingtalk-contact/references/contact.md) — 搜索同事/好友,获取 userId 用于 --user、send-by-bot --users、send-by-bot --at-user-ids、list-by-sender --sender-user-id;获取 openDingTalkId 用于 message send 的 --at-open-dingtalk-ids、--open-dingtalk-id、send-by-bot --open-dingtalk-ids、send-by-bot --at-open-dingtalk-ids、list-by-sender 的 --sender-open-dingtalk-id
- [drive](../../dingtalk-drive/references/drive.md) — 上传文件获取下载链接,用于 Markdown 图片/文件消息
- [drive](../../dingtalk-drive/references/drive.md) — 钉盘文件存储与下载;不是发送本地聊天图片/文件的前置步骤
@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""
从 dt_media_upload 返回的 URL 中提取 mediaId。
用法:
python extract_media_id.py <url>
python extract_media_id.py "https://down.dingtalk.com/media/lQLPD4JNnliqBq3NBQDNA8Cw_960_1280.png"
# 输出: @lQLPD4JNnliqBq3NBQDNA8Cw
依赖: 无(纯 Python 标准库)
典型工作流(发送图片/语音/视频消息):
# 1. 用 dt_media_upload 上传文件,获得 URL
# 2. 用本脚本从 URL 提取 mediaId
python extract_media_id.py "<dt_media_upload 返回的 URL>"
# 输出: @lQLPxxx(直接用于 --media-id 或 --pic-url 参数)
"""
import argparse
import re
import sys
from urllib.parse import urlparse
def extract_media_id(url: str) -> str:
"""从 dt_media_upload 返回的 URL 中提取 mediaId。
支持的 URL 格式示例:
https://down.dingtalk.com/media/lQLPD4JNnliqBq3NBQDNA8Cw_960_1280.png
https://down.dingtalk.com/media/lQLPD4JNnliqBq3NBQDNA8Cw.png
https://down.dingtalk.com/media/lQLPD4JNnliqBq3NBQDNA8Cw
提取规则:
1. 取 URL 路径中 /media/ 之后的部分
2. 去除尾部的 _数字_数字.扩展名 后缀(如 _960_1280.png)
3. 如果没有尺寸后缀,去除尾部的 .扩展名
4. 加上 @ 前缀
返回: @lQLPD4JNnliqBq3NBQDNA8Cw
"""
url = url.strip()
# 提取路径部分
parsed = urlparse(url)
path = parsed.path # e.g. /media/lQLPxxx_960_1280.png
# 取 /media/ 之后的部分
media_prefix = "/media/"
idx = path.find(media_prefix)
if idx == -1:
print(f"错误:URL 中未找到 /media/ 路径: {url}", file=sys.stderr)
sys.exit(1)
raw = path[idx + len(media_prefix):] # e.g. lQLPxxx_960_1280.png
if not raw:
print(f"错误:/media/ 后无内容: {url}", file=sys.stderr)
sys.exit(1)
# 尝试去除 _数字_数字.扩展名 后缀(如 _960_1280.png)
match = re.match(r'^(.+?)(_\d+_\d+\.\w+)$', raw)
if match:
media_id = match.group(1)
else:
# 没有尺寸后缀,尝试去除 .扩展名
match2 = re.match(r'^(.+)\.\w+$', raw)
if match2:
media_id = match2.group(1)
else:
# 无后缀,直接使用
media_id = raw
return f"@{media_id}"
def main():
parser = argparse.ArgumentParser(
description="从 dt_media_upload 返回的 URL 中提取 mediaId"
)
parser.add_argument("url", help="dt_media_upload 返回的文件 URL")
args = parser.parse_args()
media_id = extract_media_id(args.url)
print(media_id)
if __name__ == "__main__":
main()
+3 -2
View File
@@ -13,7 +13,7 @@ import (
// MCP architecture, products are discovered dynamically from MCP servers,
// and their availability depends on the test environment's fixture data.
func TestHiddenMCPHelpIsReachable(t *testing.T) {
func TestPublicMCPHelpShowsReviewedURLCommand(t *testing.T) {
cmd := app.NewRootCommand()
var out strings.Builder
cmd.SetOut(&out)
@@ -25,7 +25,8 @@ func TestHiddenMCPHelpIsReachable(t *testing.T) {
}
got := out.String()
if !strings.Contains(got, "canonical MCP command surface is disabled") {
if !strings.Contains(got, "管理经过审核并纳入 Schema 的 MCP 服务连接辅助能力") ||
!strings.Contains(got, "url") {
t.Fatalf("mcp help missing expected text:\n%s", got)
}
}
+59 -15
View File
@@ -1,7 +1,7 @@
[root]
runnable: true
hidden: false
commands: agoal, aisearch, aitable, api, attendance, audit, auth, calendar, chat, completion, config, contact, dev, devapp, devdoc, ding, doc, doctor, drive, event, help, live, mail, minutes, oa, pat, plugin, profile, recovery, report, schema, sheet, skill, todo, upgrade, version, wiki
commands: agoal, aisearch, aitable, api, attendance, audit, auth, calendar, chat, completion, config, contact, dev, devapp, devdoc, ding, doc, doctor, drive, event, help, live, mail, mcp, minutes, oa, pat, plugin, profile, recovery, report, schema, sheet, skill, todo, upgrade, version, wiki
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=persistent, --client-secret:string|required=false|hidden=false|no-opt=""|scope=persistent, --debug:bool|required=false|hidden=false|no-opt="true"|scope=persistent, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=persistent, --fields:string|required=false|hidden=false|no-opt=""|scope=persistent, -f/--format:string|required=false|hidden=false|no-opt=""|scope=persistent, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=persistent, --mock:bool|required=false|hidden=false|no-opt="true"|scope=persistent, -o/--output:string|required=false|hidden=true|no-opt=""|scope=persistent, --profile:string|required=false|hidden=false|no-opt=""|scope=persistent, --timeout:int|required=false|hidden=false|no-opt=""|scope=persistent, --token:string|required=false|hidden=true|no-opt=""|scope=persistent, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=persistent, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=persistent
[agoal]
@@ -1738,7 +1738,7 @@
[chat.+bot-find]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --keyword:string|required=false|hidden=true|no-opt=""|scope=local, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[chat.+bot-search]
runnable: true
@@ -1748,7 +1748,7 @@
[chat.+broadcast]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --ai-tag:bool|required=false|hidden=false|no-opt="true"|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[chat.+category-create]
runnable: true
@@ -1833,7 +1833,7 @@
[chat.+chat-search]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --exclude-muted:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --exclude-muted:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --keyword:string|required=false|hidden=true|no-opt=""|scope=local, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --size:int|required=false|hidden=true|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[chat.+chat-set-admin]
runnable: true
@@ -1878,7 +1878,7 @@
[chat.+dm]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --ai-tag:bool|required=false|hidden=false|no-opt="true"|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[chat.+group-members]
runnable: true
@@ -1888,7 +1888,7 @@
[chat.+messages-list-direct]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --forward:bool|required=false|hidden=false|no-opt="true"|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --open-dingtalk-id:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --time:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --forward:bool|required=false|hidden=false|no-opt="true"|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --open-dingtalk-id:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:int|required=false|hidden=true|no-opt=""|scope=local, --time:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[chat.+messages-list-pin]
runnable: true
@@ -1913,7 +1913,7 @@
[chat.+messages-read-status]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --conversation-id:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --message-id:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --conversation-id:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=true|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=true|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --message-id:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[chat.+messages-send-by-webhook]
runnable: true
@@ -1933,7 +1933,7 @@
[chat.+send-to-group]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --ai-tag:bool|required=false|hidden=false|no-opt="true"|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[chat.+unread-chats]
runnable: true
@@ -2317,7 +2317,7 @@
[chat.message.download-media]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --message-id:string|required=true|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --open-conversation-id:string|required=true|hidden=false|no-opt=""|scope=local, --output:string|required=true|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --resource-id:string|required=true|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --type:string|required=true|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --message-id:string|required=true|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --msg-id:string|required=false|hidden=true|no-opt=""|scope=local, --open-conversation-id:string|required=true|hidden=false|no-opt=""|scope=local, --open-message-id:string|required=false|hidden=true|no-opt=""|scope=local, --output:string|required=true|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --resource-id:string|required=true|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --type:string|required=true|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[chat.message.forward]
runnable: true
@@ -2539,7 +2539,7 @@
[contact]
runnable: true
hidden: false
commands: +by-mobile, +dept-members, +list-dept-members, +list-followings, +list-role-members, +list-roles, +list-sub-depts, +lookup, +me, +org, +resolve-dept, +search-mobile, +search-user, +team, dept, label, relation, user
commands: +by-mobile, +dept-members, +list-dept-members, +list-followings, +list-role-members, +list-roles, +list-sub-depts, +lookup, +me, +org, +resolve-dept, +search-mobile, +search-user, +team, account, dept, label, org, relation, user
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.+by-mobile]
@@ -2612,6 +2612,17 @@
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.account]
runnable: true
hidden: false
commands: create
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.account.create]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dept-ids:string|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --login-id:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --org-user-mobile:string|required=false|hidden=false|no-opt=""|scope=local, --org-user-name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --send-pwd-via-sms:bool|required=false|hidden=false|no-opt="true"|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.dept]
runnable: true
hidden: false
@@ -2660,6 +2671,17 @@
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --label-id:string|required=false|hidden=true|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --role-id:string|required=false|hidden=true|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.org]
runnable: true
hidden: false
commands: create
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.org.create]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --creator-username:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --org-name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.relation]
runnable: true
hidden: false
@@ -2674,7 +2696,7 @@
[contact.user]
runnable: true
hidden: false
commands: dismission, get, get-self, profile, search, search-mobile
commands: dismission, get, get-self, invite, profile, search, search-mobile
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.user.dismission]
@@ -2699,6 +2721,11 @@
aliases: current, me, self, whoami
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.user.invite]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --depts:string|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --org-user-mobile:string|required=false|hidden=false|no-opt=""|scope=local, --org-user-name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[contact.user.profile]
runnable: true
hidden: false
@@ -2792,7 +2819,7 @@
[dev.app.get]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --app-key:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[dev.app.list]
runnable: true
@@ -3232,7 +3259,7 @@
[doc.+share-doc]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --note:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --url:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --ai-tag:bool|required=false|hidden=false|no-opt="true"|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --note:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --url:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[doc.+template-list]
runnable: true
@@ -3637,7 +3664,7 @@
[event.consume]
runnable: true
hidden: false
flags: --as:string|required=false|hidden=true|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --compact:bool|required=false|hidden=false|no-opt="true"|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --debug-raw-events:bool|required=false|hidden=false|no-opt="true"|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=local, --duration:duration|required=false|hidden=false|no-opt=""|scope=local, --ephemeral:bool|required=false|hidden=false|no-opt="true"|scope=local, --event-types:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --filter:string|required=false|hidden=false|no-opt=""|scope=local, --filter-json:string|required=false|hidden=false|no-opt=""|scope=local, --force:bool|required=false|hidden=false|no-opt="true"|scope=local, --foreground:bool|required=false|hidden=false|no-opt="true"|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=local, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --max-events:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --output-dir:string|required=false|hidden=false|no-opt=""|scope=local, --personal-event-base-url:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --quiet:bool|required=false|hidden=false|no-opt="true"|scope=local, --route:stringArray|required=false|hidden=false|no-opt=""|scope=local, --rule:string|required=false|hidden=false|no-opt=""|scope=local, --stream-source-id:string|required=false|hidden=false|no-opt=""|scope=local, --stream-ticket-mode:string|required=false|hidden=false|no-opt=""|scope=local, --stream-ticket-url:string|required=false|hidden=false|no-opt=""|scope=local, --subscribe-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --ttl:duration|required=false|hidden=false|no-opt=""|scope=local, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --as:string|required=false|hidden=true|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --compact:bool|required=false|hidden=false|no-opt="true"|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --debug-raw-events:bool|required=false|hidden=false|no-opt="true"|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=local, --duration:duration|required=false|hidden=false|no-opt=""|scope=local, --ephemeral:bool|required=false|hidden=false|no-opt="true"|scope=local, --event-types:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --filter:string|required=false|hidden=false|no-opt=""|scope=local, --filter-json:string|required=false|hidden=false|no-opt=""|scope=local, --flatten:bool|required=false|hidden=false|no-opt="true"|scope=local, --force:bool|required=false|hidden=false|no-opt="true"|scope=local, --foreground:bool|required=false|hidden=false|no-opt="true"|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=local, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --max-events:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, --open-dingtalk-id:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --output-dir:string|required=false|hidden=false|no-opt=""|scope=local, --personal-event-base-url:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --quiet:bool|required=false|hidden=false|no-opt="true"|scope=local, --route:stringArray|required=false|hidden=false|no-opt=""|scope=local, --rule:string|required=false|hidden=false|no-opt=""|scope=local, --stream-source-id:string|required=false|hidden=false|no-opt=""|scope=local, --stream-ticket-mode:string|required=false|hidden=false|no-opt=""|scope=local, --stream-ticket-url:string|required=false|hidden=false|no-opt=""|scope=local, --subscribe-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --ttl:duration|required=false|hidden=false|no-opt=""|scope=local, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[event.list]
runnable: true
@@ -3647,7 +3674,7 @@
[event.schema]
runnable: true
hidden: false
flags: --as:string|required=false|hidden=true|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
flags: --as:string|required=false|hidden=true|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --flatten:bool|required=false|hidden=false|no-opt="true"|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[event.status]
runnable: true
@@ -4117,6 +4144,23 @@
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --employee-no:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --keyword:string|required=false|hidden=false|no-opt=""|scope=local, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:string|required=false|hidden=true|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[mcp]
runnable: true
hidden: false
commands: url
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[mcp.url]
runnable: true
hidden: false
commands: get
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[mcp.url.get]
runnable: true
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[minutes]
runnable: true
hidden: false