Compare commits

..
Author SHA1 Message Date
栩朝 b53b84616e fix(cli): match ambiguous from flag exactly 2026-08-14 19:32:54 +08:00
栩朝 15a2fea0dc fix(chat): split references and harden card updates
Split chat message and group references by task, update intent routing and context budget, distinguish accepted card updates from verified writes, and explain the ambiguous chat --from flag.
2026-08-14 18:38:31 +08:00
github-actions[bot] d8da9a2e9f Merge pull request #1011 from DingTalk-Real-AI/ci-coverage-speedup
ci: shard full-suite coverage and cache merge-base profile
2026-08-14 18:32:09 +08:00
23 changed files with 814 additions and 702 deletions
@@ -0,0 +1,6 @@
---
category: Fixed
---
- **Chat card update evidence** — distinguishes an accepted update request from an independently verified visible update, preserving the real `bizId` and warning callers not to repeat an unverified write.
- **Chat command guidance** — splits message and group references by task and explains that `--from` is ambiguous between sender and time-range intent.
+40
View File
@@ -82,6 +82,46 @@ func TestFlagErrorWithSuggestions_unknownFlagHintAndFlags(t *testing.T) {
}
}
func TestFlagErrorWithSuggestionsChatFromExplainsBothMeanings(t *testing.T) {
t.Parallel()
root := &cobra.Command{Use: "dws"}
chat := &cobra.Command{Use: "chat"}
search := &cobra.Command{Use: "+search-msg", Run: func(*cobra.Command, []string) {}}
search.Flags().String("sender", "", "sender target")
search.Flags().String("start", "", "start time")
root.AddCommand(chat)
chat.AddCommand(search)
orig := fmt.Errorf("unknown flag: --from")
err := flagErrorWithSuggestions(search, orig)
var ae *apperrors.Error
if !stderrors.As(err, &ae) {
t.Fatalf("want *apperrors.Error, got %T", err)
}
if ae.Reason != "ambiguous_flag" || !strings.Contains(ae.Hint, "--sender") || !strings.Contains(ae.Hint, "--start") {
t.Fatalf("structured error = reason %q hint %q", ae.Reason, ae.Hint)
}
if !strings.HasSuffix(ae.Message, "See 'dws chat +search-msg --help' for usage.") {
t.Fatalf("Message = %q", ae.Message)
}
for _, flag := range []string{"from-file", "from-user"} {
t.Run(flag, func(t *testing.T) {
err := flagErrorWithSuggestions(search, fmt.Errorf("unknown flag: --%s", flag))
var structured *apperrors.Error
if stderrors.As(err, &structured) && structured.Reason == "ambiguous_flag" {
t.Fatalf("--%s incorrectly used --from ambiguity handling: %#v", flag, structured)
}
if strings.Contains(err.Error(), "--from 在消息查询中含义不明确") {
t.Fatalf("--%s incorrectly received --from ambiguity hint: %v", flag, err)
}
if !strings.Contains(err.Error(), "unknown flag: --"+flag) {
t.Fatalf("error = %q, want original flag --%s", err, flag)
}
})
}
}
// TestFlagErrorWithSuggestions_fallbackTailHint 验证 fallback 路径(非 unknown flag 类错误,
// 如 missing required flag / ambiguous shorthand)也带尾部 See '<cmd> --help' for usage.
// 这是 wukong / docker / kubectl 的通用 UX——任何 flag 解析错误都给用户一条 help 入口。
+31 -7
View File
@@ -511,6 +511,22 @@ func flagErrorWithSuggestions(cmd *cobra.Command, err error) error {
// 无论哪种格式,子串 "--help' for usage." 都可被检索到。
tail := fmt.Sprintf("\nSee '%s --help' for usage.", cmd.CommandPath())
msgWithTail := errMsg + tail
if flag, ok := unknownFlagName(errMsg); ok && flag == "from" {
switch cmd.CommandPath() {
case "dws chat +search-msg", "dws chat +chat-messages":
return apperrors.NewValidation(
msgWithTail,
apperrors.WithHint("--from 在消息查询中含义不明确:按发送者过滤请使用 --sender <姓名|userId|openDingTalkId>;指定时间起点请使用 --start <RFC3339>"),
apperrors.WithReason("ambiguous_flag"),
apperrors.WithCause(err),
apperrors.WithActions(
"Use --sender <姓名|userId|openDingTalkId> to filter by sender",
"Use --start <RFC3339> together with --end <RFC3339> to set a time range",
),
apperrors.WithAvailableFlags(cmdutil.VisibleFlagNames(cmd)...),
)
}
}
if flag, protection, ok := reviewedFlagProtection(cmd, errMsg); ok {
hint := fmt.Sprintf("Parameter --%s is blocked from automatic normalization on %q; choose an explicit flag from --help.", flag, cmd.CommandPath())
reason := "blocked_flag"
@@ -579,15 +595,10 @@ func reviewedFlagProtection(cmd *cobra.Command, errMsg string) (string, pipeline
if cmd == nil {
return "", "", false
}
const prefix = "unknown flag: --"
idx := strings.Index(errMsg, prefix)
if idx < 0 {
flag, ok := unknownFlagName(errMsg)
if !ok {
return "", "", false
}
flag := strings.TrimSpace(errMsg[idx+len(prefix):])
if i := strings.IndexAny(flag, " =\n\t"); i >= 0 {
flag = flag[:i]
}
entry, ok := cli.LookupParamAlias(cmd.CommandPath())
if !ok {
return "", "", false
@@ -602,6 +613,19 @@ func reviewedFlagProtection(cmd *cobra.Command, errMsg string) (string, pipeline
return "", "", false
}
func unknownFlagName(errMsg string) (string, bool) {
const prefix = "unknown flag: --"
idx := strings.Index(errMsg, prefix)
if idx < 0 {
return "", false
}
flag := strings.TrimSpace(errMsg[idx+len(prefix):])
if i := strings.IndexAny(flag, " =\n\t"); i >= 0 {
flag = flag[:i]
}
return flag, flag != ""
}
func printExecutionError(root *cobra.Command, stdout, stderr io.Writer, err error) error {
var raw apperrors.RawStderrError
if stderrors.As(err, &raw) {
+10 -3
View File
@@ -1644,13 +1644,20 @@ var MessagesSendCard = shortcut.Shortcut{
if err != nil {
return fmt.Errorf("卡片已创建(bizId=%s),但自动更新失败: %w", bizID, err)
}
if _, err := chatmsg.VerifyStreamingCardUpdate(bizID, updated); err != nil {
verification, err := chatmsg.VerifyStreamingCardUpdate(bizID, updated)
if err != nil {
return fmt.Errorf("卡片已创建(bizId=%s),但自动更新结果不可信: %w", bizID, cardUpdateVerificationError(bizID, err))
}
payload := chatmsg.ProjectStreamingCardReceipt(created, bizID)
payload["bizId"] = bizID
payload["flowStatus"] = status
payload["updated"] = updated
payload["updateAccepted"] = verification.Accepted
payload["updateVerified"] = verification.Verified
payload["updateVerificationEvidence"] = verification.Evidence
if verification.Accepted && !verification.Verified {
payload["updateWarning"] = "服务端已接受卡片更新请求,但未返回可独立证明可见内容已更新的字段;不要重复执行相同更新"
}
return rt.Output(payload)
},
}
@@ -1793,11 +1800,11 @@ var MessagesUpdateCard = shortcut.Shortcut{
if err != nil {
return err
}
proof, err := chatmsg.VerifyStreamingCardUpdate(bizID, updated)
verification, err := chatmsg.VerifyStreamingCardUpdate(bizID, updated)
if err != nil {
return cardUpdateVerificationError(bizID, err)
}
return rt.Output(chatmsg.ProjectStreamingCardUpdate(updated, bizID, proof))
return rt.Output(chatmsg.ProjectStreamingCardUpdate(updated, bizID, verification))
},
}
+27 -1
View File
@@ -1049,6 +1049,14 @@ func TestCrossPlatformCoverageMessagesSendCardDryRunAndFailureBoundaries(t *test
},
wantError: "biz-preserved",
},
{
name: "unverified update preserves id",
fake: &larkAlignmentCaller{responses: map[string]string{
"im/create_and_send_card": `{"bizId":"biz-unverified"}`,
"im/update_streaming_card": `{"result":{"updated":false}}`,
}},
wantError: "biz-unverified",
},
} {
t.Run(tc.name, func(t *testing.T) {
helpers.InitDeps(tc.fake)
@@ -1073,6 +1081,8 @@ func TestCrossPlatformCoverageMessagesSendCardDryRunAndFailureBoundaries(t *test
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+messages-send-card",
"--group", "cid",
@@ -1085,6 +1095,13 @@ func TestCrossPlatformCoverageMessagesSendCardDryRunAndFailureBoundaries(t *test
if len(fake.calls) != 2 || fake.calls[1].tool != "update_streaming_card" {
t.Fatalf("calls = %#v", fake.calls)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["updateAccepted"] != true || payload["updateVerified"] != false || payload["updateWarning"] == "" {
t.Fatalf("card payload = %#v", payload)
}
})
for _, args := range [][]string{
@@ -1157,12 +1174,14 @@ func TestCrossPlatformCoverageMessagesUpdateCardVerifiesSuccess(t *testing.T) {
}
})
t.Run("success acknowledgement is verified", func(t *testing.T) {
t.Run("success acknowledgement is accepted but unverified", func(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"im/update_streaming_card": `{"success":true,"errorCode":null}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+messages-update-card",
"--biz-id", "中文乱串",
@@ -1176,6 +1195,13 @@ func TestCrossPlatformCoverageMessagesUpdateCardVerifiesSuccess(t *testing.T) {
if len(fake.calls) != 1 || fake.calls[0].tool != "update_streaming_card" {
t.Fatalf("calls = %#v", fake.calls)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["accepted"] != true || payload["verified"] != false || payload["warning"] == "" {
t.Fatalf("payload = %#v", payload)
}
})
t.Run("explicit update evidence succeeds", func(t *testing.T) {
+7 -3
View File
@@ -55,11 +55,15 @@ func ProjectStreamingCardReceipt(created map[string]any, bizID string) map[strin
// ProjectStreamingCardUpdate preserves the lower response while making the
// verified target explicit for downstream consumers.
func ProjectStreamingCardUpdate(updated map[string]any, bizID, proof string) map[string]any {
func ProjectStreamingCardUpdate(updated map[string]any, bizID string, verification CardUpdateVerification) map[string]any {
payload := cloneSendStatusMap(updated)
payload["contractVersion"] = StreamingCardContractVersion
payload["cardRef"] = map[string]any{"bizId": strings.TrimSpace(bizID)}
payload["verified"] = true
payload["verificationEvidence"] = proof
payload["accepted"] = verification.Accepted
payload["verified"] = verification.Verified
payload["verificationEvidence"] = verification.Evidence
if verification.Accepted && !verification.Verified {
payload["warning"] = "服务端已接受卡片更新请求,但未返回可独立证明可见内容已更新的字段;不要重复执行相同更新"
}
return payload
}
+8 -1
View File
@@ -32,7 +32,7 @@ func TestCrossPlatformCoverageProjectStreamingCardReceipt(t *testing.T) {
}
func TestCrossPlatformCoverageProjectStreamingCardUpdate(t *testing.T) {
payload := ProjectStreamingCardUpdate(map[string]any{"result": map[string]any{"updated": true}}, "biz-1", "updated=true")
payload := ProjectStreamingCardUpdate(map[string]any{"result": map[string]any{"updated": true}}, "biz-1", CardUpdateVerification{Accepted: true, Verified: true, Evidence: "updated=true"})
if payload["contractVersion"] != StreamingCardContractVersion || payload["verified"] != true || payload["verificationEvidence"] != "updated=true" {
t.Fatalf("payload = %#v", payload)
}
@@ -40,3 +40,10 @@ func TestCrossPlatformCoverageProjectStreamingCardUpdate(t *testing.T) {
t.Fatal("lower response was not preserved")
}
}
func TestCrossPlatformCoverageProjectAcceptedUnverifiedStreamingCardUpdate(t *testing.T) {
payload := ProjectStreamingCardUpdate(map[string]any{"success": true}, "biz-1", CardUpdateVerification{Accepted: true, Verified: false, Evidence: "success=true"})
if payload["accepted"] != true || payload["verified"] != false || payload["warning"] == "" {
t.Fatalf("payload = %#v", payload)
}
}
+31 -18
View File
@@ -27,6 +27,16 @@ var (
ErrCardUpdateBizIDDrift = errors.New("streaming card update returned a different bizId")
)
// CardUpdateVerification distinguishes an accepted write from an independently
// verified write. Some server versions return success=true without an updated
// flag or affected count; that is sufficient to report acceptance, but not to
// claim that the card's visible content was observed after the write.
type CardUpdateVerification struct {
Accepted bool
Verified bool
Evidence string
}
// NormalizeCardBizID performs only format-independent checks. bizId is an
// opaque server-issued identifier; a stricter character or prefix contract
// must not be invented by the CLI without an authoritative API declaration.
@@ -58,33 +68,40 @@ func isCardBizIDPlaceholder(value string) bool {
}
}
// VerifyStreamingCardUpdate requires affirmative evidence that the requested
// write took effect. update_streaming_card may acknowledge an applied write
// with success=true without returning an updated flag or affected count.
func VerifyStreamingCardUpdate(requestedBizID string, response map[string]any) (string, error) {
// VerifyStreamingCardUpdate separates server acceptance from affirmative
// evidence that the requested write took effect.
func VerifyStreamingCardUpdate(requestedBizID string, response map[string]any) (CardUpdateVerification, error) {
requestedBizID = strings.TrimSpace(requestedBizID)
observation := cardUpdateObservation{bizIDs: map[string]struct{}{}}
observeCardUpdate(response, &observation)
for responseBizID := range observation.bizIDs {
if requestedBizID != "" && responseBizID != requestedBizID {
return "", fmt.Errorf("%w: requested %q, response %q", ErrCardUpdateBizIDDrift, requestedBizID, responseBizID)
return CardUpdateVerification{}, fmt.Errorf("%w: requested %q, response %q", ErrCardUpdateBizIDDrift, requestedBizID, responseBizID)
}
}
if observation.positiveEvidence != "" && observation.negativeEvidence != "" {
return "", fmt.Errorf("%w: conflicting evidence %s and %s", ErrCardUpdateUnverified, observation.positiveEvidence, observation.negativeEvidence)
if observation.negativeEvidence != "" && (observation.positiveEvidence != "" || observation.acceptedEvidence != "") {
positive := observation.positiveEvidence
if positive == "" {
positive = observation.acceptedEvidence
}
return CardUpdateVerification{}, fmt.Errorf("%w: conflicting evidence %s and %s", ErrCardUpdateUnverified, positive, observation.negativeEvidence)
}
if observation.positiveEvidence != "" {
return observation.positiveEvidence, nil
return CardUpdateVerification{Accepted: true, Verified: true, Evidence: observation.positiveEvidence}, nil
}
if observation.negativeEvidence != "" {
return "", fmt.Errorf("%w: %s", ErrCardUpdateNotApplied, observation.negativeEvidence)
return CardUpdateVerification{}, fmt.Errorf("%w: %s", ErrCardUpdateNotApplied, observation.negativeEvidence)
}
return "", ErrCardUpdateUnverified
if observation.acceptedEvidence != "" {
return CardUpdateVerification{Accepted: true, Verified: false, Evidence: observation.acceptedEvidence}, nil
}
return CardUpdateVerification{}, ErrCardUpdateUnverified
}
type cardUpdateObservation struct {
bizIDs map[string]struct{}
acceptedEvidence string
positiveEvidence string
negativeEvidence string
}
@@ -107,6 +124,9 @@ func observeCardUpdate(value any, observation *cardUpdateObservation) {
}
func observeCardUpdateMap(value map[string]any, observation *cardUpdateObservation) {
if accepted, ok := value["success"].(bool); ok && accepted && observation.acceptedEvidence == "" {
observation.acceptedEvidence = "success=true"
}
for _, key := range []string{"bizId", "bizID", "biz_id"} {
if candidate, ok := value[key].(string); ok && strings.TrimSpace(candidate) != "" {
observation.bizIDs[strings.TrimSpace(candidate)] = struct{}{}
@@ -137,14 +157,7 @@ func observeCardUpdateMap(value map[string]any, observation *cardUpdateObservati
setNegativeCardUpdateEvidence(observation, "errorCode=non-empty")
}
if success, ok := value["success"].(bool); ok {
if success {
// Record success=true only when the same response envelope explicitly
// includes its business-error field. A non-empty code is already
// negative evidence above, so the two signals reject the conflict.
if hasErrorCode {
setPositiveCardUpdateEvidence(observation, "success=true")
}
} else {
if !success {
setNegativeCardUpdateEvidence(observation, "success=false")
}
}
+11 -11
View File
@@ -52,19 +52,19 @@ func TestCrossPlatformCoverageVerifyStreamingCardUpdate(t *testing.T) {
for _, test := range []struct {
name string
response map[string]any
wantProof string
want CardUpdateVerification
wantErrIs error
}{
{name: "updated", response: map[string]any{"result": map[string]any{"updated": true}}, wantProof: "updated=true"},
{name: "affected", response: map[string]any{"data": map[string]any{"affectedCount": float64(1)}}, wantProof: "affectedCount=1"},
{name: "boolean result", response: map[string]any{"result": true}, wantProof: "result=true"},
{name: "updated", response: map[string]any{"result": map[string]any{"updated": true}}, want: CardUpdateVerification{Accepted: true, Verified: true, Evidence: "updated=true"}},
{name: "affected", response: map[string]any{"data": map[string]any{"affectedCount": float64(1)}}, want: CardUpdateVerification{Accepted: true, Verified: true, Evidence: "affectedCount=1"}},
{name: "boolean result", response: map[string]any{"result": true}, want: CardUpdateVerification{Accepted: true, Verified: true, Evidence: "result=true"}},
{name: "boolean false result", response: map[string]any{"result": false}, wantErrIs: ErrCardUpdateNotApplied},
{name: "matching id", response: map[string]any{"result": map[string]any{"bizId": "biz-1", "applied": true}}, wantProof: "applied=true"},
{name: "matching id", response: map[string]any{"result": map[string]any{"bizId": "biz-1", "applied": true}}, want: CardUpdateVerification{Accepted: true, Verified: true, Evidence: "applied=true"}},
{name: "conflicting evidence", response: map[string]any{"updated": true, "applied": false}, wantErrIs: ErrCardUpdateUnverified},
{name: "zero affected", response: map[string]any{"affectedCount": 0}, wantErrIs: ErrCardUpdateNotApplied},
{name: "success acknowledgement", response: map[string]any{"success": true, "errorCode": nil}, wantProof: "success=true"},
{name: "success acknowledgement with empty error code", response: map[string]any{"success": true, "errorCode": " "}, wantProof: "success=true"},
{name: "success without explicit error code", response: map[string]any{"success": true}, wantErrIs: ErrCardUpdateUnverified},
{name: "success acknowledgement", response: map[string]any{"success": true, "errorCode": nil}, want: CardUpdateVerification{Accepted: true, Verified: false, Evidence: "success=true"}},
{name: "success acknowledgement with empty error code", response: map[string]any{"success": true, "errorCode": " "}, want: CardUpdateVerification{Accepted: true, Verified: false, Evidence: "success=true"}},
{name: "success without explicit error code", response: map[string]any{"success": true}, want: CardUpdateVerification{Accepted: true, Verified: false, Evidence: "success=true"}},
{name: "success conflicts with error code", response: map[string]any{"success": true, "errorCode": "InternalError"}, wantErrIs: ErrCardUpdateUnverified},
{name: "success conflicts with numeric error code", response: map[string]any{"success": true, "errorCode": float64(500)}, wantErrIs: ErrCardUpdateUnverified},
{name: "error code without success", response: map[string]any{"errorCode": "InternalError"}, wantErrIs: ErrCardUpdateNotApplied},
@@ -74,15 +74,15 @@ func TestCrossPlatformCoverageVerifyStreamingCardUpdate(t *testing.T) {
{name: "unrelated extension ignored", response: map[string]any{"extension": map[string]any{"updated": true}}, wantErrIs: ErrCardUpdateUnverified},
} {
t.Run(test.name, func(t *testing.T) {
proof, err := VerifyStreamingCardUpdate("biz-1", test.response)
got, err := VerifyStreamingCardUpdate("biz-1", test.response)
if test.wantErrIs != nil {
if !errors.Is(err, test.wantErrIs) {
t.Fatalf("VerifyStreamingCardUpdate error = %v, want errors.Is(_, %v)", err, test.wantErrIs)
}
return
}
if err != nil || proof != test.wantProof {
t.Fatalf("VerifyStreamingCardUpdate = %q, %v; want %q", proof, err, test.wantProof)
if err != nil || got != test.want {
t.Fatalf("VerifyStreamingCardUpdate = %#v, %v; want %#v", got, err, test.want)
}
})
}
+1 -1
View File
@@ -12,7 +12,7 @@ event_skill="skills/multi/dingtalk-event/SKILL.md"
mono_skill="skills/mono/SKILL.md"
runtime_contract="skills/multi/dingtalk-shared/references/runtime-contract.md"
chat_target_bytes=10000
chat_max_overage_percent=5
chat_max_overage_percent=10
chat_max_bytes=$((chat_target_bytes * (100 + chat_max_overage_percent) / 100))
doc_max_bytes=10000
event_max_bytes=10000
@@ -25,8 +25,7 @@
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md"
"skills/multi/dingtalk-chat/references/01-messaging.md"
]
},
{
@@ -37,8 +36,7 @@
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md"
"skills/multi/dingtalk-chat/references/01-messaging.md"
]
},
{
@@ -53,7 +51,7 @@
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md",
"skills/multi/dingtalk-chat/references/chat/message-media.md",
"skills/multi/dingtalk-chat/references/chat/chat-bot.md"
]
},
@@ -72,7 +70,7 @@
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md",
"skills/multi/dingtalk-chat/references/chat/message-query.md",
"skills/multi/dingtalk-chat/references/chat/chat-conversation.md"
]
},
@@ -91,7 +89,7 @@
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md"
"skills/multi/dingtalk-chat/references/chat/message-query.md"
]
},
{
@@ -103,7 +101,7 @@
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-group.md"
"skills/multi/dingtalk-chat/references/chat/group-admin.md"
]
},
{
@@ -115,7 +113,7 @@
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md"
"skills/multi/dingtalk-chat/references/chat/message-actions.md"
]
},
{
+9 -4
View File
@@ -72,6 +72,8 @@ metadata:
- `+dm`:姓名目标的简单文本/Markdown,参数空间最小。
- `+send-to-group`:群名或稳定 ID 目标的简单文本/Markdown,避免暴露无关身份矩阵。
- Markdown 中的公网图片必须写成 `![图片标题](https://example.com/image.png)` 才会内联展示;
省略开头的 `!` 时只会显示为链接。
- `+messages-send`:文件、Bot、Webhook、复杂 @ 或幂等控制。user 已知 ID 可直接传,也可用 `--user-query` / `--chat-query` 运行同一只读解析链;Bot 多群使用 `--groups/--groups-file`,返回 `im.batch-write.v1`;bot/webhook 只使用下层真实支持的文本/Markdown 能力。
- 文件直接传 `+messages-send --file <相对路径>`;不要先独立上传并提取 mediaId。
- Webhook 使用 `+messages-send --as webhook --webhook-token <token>`;不要退回原子 Webhook 命令。
@@ -92,10 +94,13 @@ metadata:
| 场景 | Reference |
|---|---|
| 复杂发送、跨会话转发、共同群或组合流程 | [01-messaging.md](references/01-messaging.md) |
| 编辑/撤回/引用/转发/卡片/reaction/Pin/Top/Favorite | [chat-message.md](references/chat/chat-message.md) |
| 建群、成员、管理员、群公告、群设置 | [chat-group.md](references/chat/chat-group.md) |
| Bot 搜索、入群、群发和撤回 | [chat-bot.md](references/chat/chat-bot.md) |
| 需要跨步骤传递真实结果的消息/群组合流程 | [01-messaging.md](references/01-messaging.md) |
| 消息读取与查询 | [message-query](references/chat/message-query.md) |
| 编辑、撤回、回复、转发、Pin、Top、Favorite 或 reaction 写入 | [message-actions](references/chat/message-actions.md) |
| 位置、联系人名片、底层媒体与资源下载 | [message-media](references/chat/message-media.md) |
| 群列表、群搜索、共同群、成员与群内机器人读取 | [group-discovery](references/chat/group-discovery.md) |
| 建群、成员或已知机器人增删、管理员、公告与群设置 | [group-admin](references/chat/group-admin.md) |
| 搜索未知机器人、机器人消息发送/撤回与 Webhook | [chat-bot.md](references/chat/chat-bot.md) |
| 会话置顶、分类、红点、免打扰和隐藏 | [chat-conversation.md](references/chat/chat-conversation.md) |
| 低频意图之间仍需消歧 | [intent-guide.md](references/intent-guide.md) |
| 表情名称与 ID | [chat-emoji-list.md](references/chat-emoji-list.md) |
@@ -13,9 +13,11 @@ Runtime 会唯一解析为 openDingTalkId;已有 openDingTalkId 时传
或艾特占位符。
- `--dry-run` 仍执行只读 userId 解析,只输出两步计划,不执行写入。
自动更新结果不确定时,不要再次更新或重复创建;保留返回结果并告知用户。若结果中已经
包含 `openTaskId`,可以按用户需要查询一次投递状态;该查询只确认消息投递,不代表卡片
正文已经更新成功。
创建成功后保留真实 `bizId`。自动更新返回 `verified=true` 时已有明确生效证据;返回
`accepted=true, verified=false` 时仅表示服务端已接受请求但未提供独立更新证据,应如实说明,
不要重复创建或重复执行相同更新。只有错误明确标记 `retryable=true` 时,才使用原 `bizId`
重试;明确未应用或 `bizId` 不一致时停止并保留真实错误。若结果中已经包含 `openTaskId`,
可以按用户需要查询一次投递状态;该查询只确认消息投递,不代表卡片正文已经更新成功。
当前内容仅为 streaming text,不接受 Lark Card JSON、组件树或按钮 callback。
@@ -7,3 +7,7 @@
更新是写操作,confirmation 以精确 leaf Schema 与 Runtime gate 为准。失败后保留原
`bizId` 和状态,不创建新卡片来掩盖更新失败。
结果中 `verified=true` 表示已有明确更新证据;`accepted=true, verified=false` 仅表示服务端
接受了请求但未提供独立生效证据,应如实说明且不得重复执行相同更新。只有错误明确标记
`retryable=true` 时才重试;明确未应用或 `bizId` 不一致时停止。
@@ -19,9 +19,12 @@
| 用户终点 | 返回入口 |
|---|---|
| 姓名/群名简单发送、文件、Bot、Webhook、复杂 @ | 根 Skill Golden Route |
| 单会话消息、跨会话搜索、资源下载 | [消息任务级流程](01-messaging.md) |
| 引用、转发、卡片、reaction、Pin/Top/Favorite | [chat-message](chat/chat-message.md) |
| 基础建群、成员、公告、管理员和群设置 | [chat-group](chat/chat-group.md) |
| 消息读取、条件搜索、@我、Favorite/reaction 查询和批量详情 | [message-query](chat/message-query.md) |
| 编辑、撤回、引用、转发、reaction/Pin/Top/Favorite 写入 | [message-actions](chat/message-actions.md) |
| 位置、名片、资源下载和特殊媒体 fallback | [message-media](chat/message-media.md) |
| 群列表、群搜索、成员读取、Bot 列表和邀请链接 | [group-discovery](chat/group-discovery.md) |
| 建群、改群、成员写入、管理员、禁言、公告和群设置 | [group-admin](chat/group-admin.md) |
| 跨步骤消息/群组合流程 | [消息任务级流程](01-messaging.md) |
| Bot 搜索、进群和撤回 | [chat-bot](chat/chat-bot.md) |
| 会话置顶、状态和分组 | [chat-conversation](chat/chat-conversation.md) |
| 相邻低频意图仍需消歧 | [intent-guide](intent-guide.md) |
@@ -1,223 +0,0 @@
# chat-group:群聊、成员、设置与群身份
> 返回入口:[chat.md](../chat.md)
## 适用场景
用于群搜索、建群、成员增删、机器人进群、群设置、群主转让、群邀请分享、群公告、入群审批、群身份和群禁言。
<!-- dws-intent: chat.create.group -->基础建群默认使用 `dws chat +chat-create`:已知成员 ID 传
`--users`,姓名/花名传 `--member-query`;群主默认当前用户,也可传
`--owner-open-dingtalk-id` 或 `--owner-query`。全部自然身份唯一解析并去重后才执行一次创建。
## 必读约束
- 群聊目标统一使用 `openConversationId`。只有数字群号时,先用 `chat group get-by-group-id` 转换。
- 群搜索唯一推荐 `dws chat +chat-search --query <群名>`;`chat search`、`chat group search`、
`+chat-group-search` 和 `+search-group` 仅是兼容拼法,不应被写成并列默认路线。
- 群成员操作中,`--users` 常为逗号分隔列表;具体要求以命令 `--help` 为准。
- 解散群、踢人、转让群主、禁言、管理员设置都是高影响操作,执行前必须确认目标群和用户。
- 发布或修改群公告会触达群成员;公告正文是 Markdown,定时公告 `--run-at` 建议带时区。
## 命令明细
### 搜索与基础信息
| 命令 | 用途 | 示例与要点 |
|------|------|------------|
| `+chat-search` | 按关键词搜索群聊 | 默认一页;要求全部候选时用 `dws chat +chat-search --query "项目冲刺" --page-all`;可用 `--page-size/--page-token` 或兼容的 `--limit/--cursor`,并检查完整性 ledger;多候选必须消歧 |
| `chat search-common` | 搜索共同群 | `dws chat search-common --nicks "风雷,山乔" --match-mode AND --limit 20 --cursor 0` |
| `chat group get-by-group-id` | 数字群号转 openConversationId | `dws chat group get-by-group-id --group-id 12345678` |
| `+chat-bots` | 查看群内所有机器人 | `dws chat +chat-bots --group <群名或openConversationId>`;内部唯一解析自然群名 |
`search-common` 中 `--match-mode AND` 表示所有人都在群里,`OR` 表示任一人在群里。
### 群创建与基础操作
#### `dws chat group create`(底层 fallback)
只有需要 `+chat-create` 尚未发布的底层字段时才评估原子创建命令,并先读取精确 leaf
Schema。`+chat-create` 已支持 `--thread` 和显式群主;普通内部/外部群创建不得回流到手工
`aisearch → group create` 链路。
```bash
dws chat group create --name "Q1 项目冲刺群" --users userId1,userId2,userId3
dws chat group create --name "外部合作群" --users userId1,userId2 --type EXTERNAL
dws chat group create --name "话题圈" --users userId1,userId2 --thread
```
关键 flags:
| Flag | 说明 |
|------|------|
| `--name` | 群名称,必填 |
| `--users` | 成员 userId 或 openDingTalkId,逗号分隔,必填 |
| `--type` | `INTERNAL` / `EXTERNAL` / `NORMAL`,默认 `INTERNAL` |
| `--thread` | 开启话题模式,创建话题圈 |
创建成功后提取 `openConversationId`,用于发消息、成员管理、群设置。
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `group rename` | 更新群名称 | `--id` `--name`;只知群名时先用 `+chat-search --query <群名>` 唯一解析 ID,不猜 `+chat-rename` |
| `group quit` | 当前用户退出群聊 | `--group` |
| `group dismiss` | 解散群聊,不可逆 | `--group` |
### 成员与机器人
| 命令 | 用途 | 示例 |
|------|------|------|
| `+chat-members-list` | 全量查看群成员并分桶用户/机器人 | `--group <群名或openConversationId>`;显式 ID 也可用 `--conversation-id`,检查 buckets/complete/failures |
| `group members add` | 添加群成员 | `dws chat group members add --id <openConversationId> --users userId1,userId2` |
| `group members remove` | 移除群成员 | `dws chat group members remove --id <openConversationId> --users userId1,userId2` |
| `group members list-by-ids` | 按 openDingTalkId 批量查成员详情 | `dws chat group members list-by-ids --id <openConversationId> --users openDingTalkId1,openDingTalkId2` |
| `group members add-bot` | 将自定义机器人加入群 | `dws chat group members add-bot --id <openConversationId> --robot-code <robot-code>` |
| `group members remove-bot` | 从群内移除机器人 | `dws chat group members remove-bot --id <openConversationId> --bot-id <openBotId>` |
机器人发群消息如果报“机器人不存在”,先 `group members add-bot` 再重发。
### 群设置与权限
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `group transfer-owner` | 转让群主 | `--group` + `--user`(userId) 或 `--new-owner`(openDingTalkId) |
| `group upgrade-to-external` | 将普通群升级为外部群(不可逆,需先确认) | `--group` `--yes`;可选 `--extension`(拓展字段) |
| `+chat-invite-url` | 获取群邀请链接 | `--group <群名或openConversationId>`;可选 `--expires-seconds` |
| `group share-invite` | 将指定群的邀请链接分享到另一个会话或单聊用户 | `--source` + `--target` / `--receiver` 二选一 |
| `group update-icon` | 更新群头像 | `--group` `--icon-media-id` |
| `group update-settings` | 更新管理员级别的群功能开关 | `--group` `--setting-key` `--status` |
| `group user-settings query` | 批量查询当前用户自己的群会话设置 | `--groups` |
| `group user-settings set` | 批量更新当前用户自己的群会话设置 | `--items` |
| `group update-nick` | 设置或清除当前用户群昵称 | `--group`,可选 `--nick`;不传则清除群昵称 |
| `group update-alias` | 设置当前用户群备注 | `--group` `--alias-title` |
| `group set-history` | 设置新成员可查看历史消息范围 | `--group` `--option` |
| `group get-mute-config` | 查询群用户禁言配置 | `--group` |
| `group-mute` | 全员禁言/解除全员禁言 | `--group`,默认禁言,`--off` 解除 |
| `group-mute-member` | 指定成员禁言/解除禁言 | `--group` `--user`/`--users`;禁言需 `--mute-time` |
| `group set-admin` | 设置/取消管理员 | `--group` `--user`/`--users`;`--off` 取消 |
`update-settings` 是管理员级别的群功能开关操作,常用 settingKey:`authority`、`joinValidation`、`onlyAdminCanAtAll`、`searchable`、`addFriendForbidden`、`onlyAdminCanDING`、`onlyAdminCanPinMsg`、`onlyAdminCanSendFile`、`groupEmailDisabled`、`groupLiveAuthority`、`groupBillAuthority`。
`group user-settings` 是当前登录用户自己的群会话设置批量入口(置顶、免打扰、群昵称、群备注等),不是管理员级别的群功能开关;单个群昵称/群备注仍优先使用 `group update-nick` / `group update-alias`,管理员级别的群功能开关继续使用 `group update-settings`。
`group user-settings set --items` 传 JSON 数组,每个元素表示一个群会话的当前用户设置。字段含义:
| 字段 | 含义 | 值说明 |
|------|------|--------|
| `openConversationId` | 群会话 ID | 必填,来自 `chat search` / `group list-all` 等真实返回 |
| `top` | 当前用户是否置顶该群会话 | `true`=置顶,`false`=取消置顶 |
| `mute` | 当前用户是否开启该群会话免打扰 | `true`=开启免打扰,`false`=关闭免打扰 |
| `groupNick` | 当前用户在该群里的群昵称 | 字符串;空字符串表示清空昵称 |
| `groupAlias` | 当前用户给该群设置的备注 | 字符串;空字符串表示清空备注 |
只传本次要改的字段;不要补用户没要求修改的字段。批量设置多个群时,`items` 放多个对象。
`group-mute-member --mute-time` 单位毫秒,常用值:`300000`、`3600000`、`86400000`、`604800000`、`2592000000`。
`group share-invite` 的 `--source` 是被分享群的 `openConversationId`;`--target` 是接收分享消息的会话,`--receiver` 是接收分享消息的单聊用户 `openDingTalkId`,二者必须二选一。
```bash
dws chat group share-invite --source <sourceOpenConversationId> --target <targetOpenConversationId>
dws chat group share-invite --source <sourceOpenConversationId> --receiver <receiverOpenDingTalkId>
dws chat group share-invite --source <sourceOpenConversationId> --target <targetOpenConversationId> --expires-seconds 86400 --uuid <uuid>
dws chat group user-settings query --groups <openConversationId1>,<openConversationId2>
dws chat group user-settings set --items '[{"openConversationId":"cid1","top":true,"mute":false}]'
```
### 群公告
群公告正文使用 Markdown。支持标题、加粗、斜体、删除线、行内代码、链接、代码块、列表、表格、引用、分割线、图片、段落和换行;下划线、字体色、背景色、字号属于编辑器专属能力,无法通过 Markdown 表达。
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `group notice create` | 发布群公告,支持置顶、DING 和定时发布 | `--group` `--content` |
| `group notice edit` | 整体替换指定群公告内容 | `--group` `--notice-id` `--content` |
| `group notice get` | 查询单条群公告详情 | `--group` `--notice-id` |
| `group notice list` | 分页拉取群公告列表 | `--group` |
```bash
dws chat group notice create --group <openConversationId> --content "今晚 22 点系统维护,请提前保存工作内容"
dws chat group notice create --group <openConversationId> --content "# 重要通知\n\n请大家查收" --sticky --send-ding
dws chat group notice create --group <openConversationId> --content "明早九点例会" --run-at "2026-07-03T09:00:00+08:00"
dws chat group notice list --group <openConversationId> --limit 20 --cursor <nextPageCursor>
dws chat group notice get --group <openConversationId> --notice-id <dataId>
dws chat group notice edit --group <openConversationId> --notice-id <dataId> --content "更新后的公告内容"
```
注意事项:
- `notice edit` 会整体替换原公告正文,必须传完整的新内容。
- `notice list --scheduled` 查询尚未到发布时间的定时公告;默认查询已发布公告。
- `hasMore=true` 时,用返回的 `nextPageCursor` 继续翻页。
- `notice get` 返回正文摘要、置顶状态、发布者、已读人数/应收人数、点赞/评论数、是否可编辑、是否已读、是否定时公告等信息。
### 群列表与入群审批
| 命令 | 用途 | 示例与要点 |
|------|------|------------|
| `group list-my-groups` | 拉取我创建/管理的群 | 可选 `--role OWNER/ADMIN`、`--limit`、`--exclude-muted` |
| `group list-all` | 分页拉取我加入的所有群 | `--limit` 默认 100,最大 200;翻页用 `nextCursor` |
| `group list-join-validations` | 拉取入群验证记录 | 包括自己被拒绝的记录以及作为审批者的记录 |
| `group audit-join-validation` | 审批入群验证 | `--group` `--record-id` `--applicant` `--inviter` `--status` |
审批动作 `--status`:`AuditApprove`、`AuditDelete`、`AuditIgnore`、`AuditRefuse`、`AuditBlock`。
```bash
dws chat group list-join-validations --limit 20
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditApprove
```
### 群身份
`chat group-role` 管理群内自定义身份标签。
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `group-role list` | 查看群身份列表 | `--group` |
| `group-role add` | 新增群身份 | `--group` `--name` |
| `group-role update` | 修改群身份名称 | `--group` `--role-id` `--name` |
| `group-role remove` | 删除群身份 | `--group` `--role-id` |
| `group-role set-user` | 覆盖用户全部群身份,空 `--role-ids` 表示清除 | `--group` `--user` `--role-ids` |
| `group-role remove-user` | 移除用户指定群身份 | `--group` `--user` `--role-ids` |
| `group-role query-user` | 查询用户当前群身份 | `--group` `--user` |
`openRoleId` 来自 `group-role list` 返回。
## 常见工作流
### 搜索群并发消息
```bash
dws chat +send-to-group --group "项目冲刺" --text "请大家看一下最新进展" --format json
```
### 建群并拉人
```bash
dws chat +chat-create --name "Q1 项目冲刺群" --member-query "张三,李四" --format json
dws chat +chat-create --name "合作群" --member-query "张三,李四" --owner-query "王五" --format json
dws chat group members add --id <openConversationId> --users userId3,userId4 --format json
```
### 分享群邀请并发布公告
```bash
dws chat group share-invite --source <sourceOpenConversationId> --target <targetOpenConversationId> --format json
dws chat group notice create --group <openConversationId> --content "# 项目公告\n\n请大家关注最新安排" --send-ding --format json
```
### 设置管理员并禁言成员
```bash
dws chat group set-admin --group <openConversationId> --users userId1,userId2 --format json
dws chat group-mute-member --group <openConversationId> --users userId3 --mute-time 3600000 --format json
```
## 常见错误与回退
- 只有数字群号:先 `group get-by-group-id`,不要直接当 `--group`。
- 找不到群:使用 `+chat-search --query` 扩大关键词;零命中或多候选时停止,不臆测 openConversationId。
- 入群审批缺参数:从 `group list-join-validations` 提取 `record-id`、`applicant`、`inviter`。
- 机器人进群失败:确认当前用户有群管理权限。
- 分享群邀请目标不明确:`--target` 和 `--receiver` 只能二选一,先确认是发到群/会话还是发给个人。
- 修改公告前没有完整新正文:先向用户确认完整公告内容,不要只传增量片段。
@@ -1,410 +0,0 @@
# chat-message:消息、文件、搜索与卡片
> 返回入口:[chat.md](../chat.md)
## 适用场景
用于当前用户发消息、拉消息、搜索消息、撤回、已读状态、文件/图片/音频/视频/位置/名片发送、话题回复、转发、Pin/Top、表情回应、文本翻译和流式卡片。
## 默认入口
| 用户终点 | 默认入口 |
|---|---|
| <!-- dws-intent: chat.send.dm -->按姓名发送简单文本/Markdown | `dws chat +dm` |
| <!-- dws-intent: chat.send.group -->按群名发送简单文本/Markdown | `dws chat +send-to-group` |
| <!-- dws-intent: chat.send.advanced -->文件、Bot、Webhook、复杂 @、已知 ID 或幂等发送 | `dws chat +messages-send` |
| <!-- dws-intent: chat.read.conversation -->读取或导出指定群聊/单聊,可附带发送者姓名 | `dws chat +chat-messages`;姓名用非必填 `--sender-query` |
| <!-- dws-intent: chat.search.filtered -->直接按发送者、关键词、@对象或消息类型搜索,可限定单个或跨多个会话 | `dws chat +search-msg` |
| <!-- dws-intent: chat.reply.quote -->引用回复已有消息 | `dws chat +messages-reply` |
| 查看指定群内 @我的消息 | `dws chat +at-me --group <群名> --page-all` |
| 撤回当前用户消息 | `dws chat +messages-recall --msg-id <openMessageId>` |
以下原子命令只用于 Shortcut 未暴露的底层字段、原始响应或精确分页控制。不得把它们重新写成
上述高频任务的默认路径;写入原子命令若与 Golden Shortcut 的 confirmation 不一致,停止并
报告交付漂移,不以文档确认代替 Runtime gate。
## 必读约束
- 发消息前必须核对接收对象、消息内容、@ 对象、附件路径和消息类型;不明确时先问用户。
- `--group`、`--user`、`--open-dingtalk-id` 通常互斥,群聊用 `--group`,单聊用 `--user` 或 `--open-dingtalk-id`。
- 发送本地文件、音频、视频默认用 `dws chat +messages-send --as user --file <相对路径>`;
`audio` / `video` 的具体类型以 leaf Schema 为准。只有 Shortcut 未暴露的位置、名片等底层
类型才进入本文件的原子 fallback。
- 发送位置消息前必须确认纬度、经度、地址名称;地图缩略图需先通过旧媒体上传链路拿到 mediaId。
- 分享联系人名片前必须确认联系人 `openDingTalkId`,不要把 userId 直接当 `--contact-id`。
- 消息内容按 Markdown 渲染,换行必须是真实换行符;需要换行效果时用空行、行尾两个空格或 `<br>`。
- 图文混排 Markdown 中,公网图片 URL 需要写成 `![图片标题](https://example.com/image.png)` 才会以内联图片展示;省略开头的 `!` 时会按链接/URL 展示,不会渲染为图片。
- 建议发送时带 `--uuid`,失败重试复用同一个值。
- Bot/Webhook 只支持文本/Markdown;Bot 多群使用 `+messages-send --groups/--groups-file` 的逐项
ledger。不要把 user 文件/图片能力外推到 Bot。
- `+at-me` 和 `+messages-list-direct` 要求全量时使用 `--page-all`,并检查 `complete`、
`hasMore`、`stopReason` 和 `failures`;`+messages-list-direct` 的续页时间来自下层毫秒
`nextCursor`,不得用只有秒精度的消息展示时间手工拼接。
## 原子 fallback 命令明细
### 发送消息的底层 fallback
#### `dws chat message send`(非默认入口)
以当前用户身份发送群聊或单聊消息。
```bash
# 文本/Markdown
dws chat message send --group <openConversationId> --text "hello"
dws chat message send --user <userId> --text "请查收"
dws chat message send --open-dingtalk-id <openDingTalkId> --text "请查收"
dws chat message send --group <openConversationId> --title "周报提醒" --text "请大家本周五前提交周报" --uuid <uuid>
dws chat message send --group <openConversationId> --text $'这是图文说明\n\n![这个是展示图片标题](https://down.dingtalk.com/media/lQLPM5jiBEiBNjswMLAKd_CTzm8eowpEWPT_7-cA_48_48.png)'
# @ 群成员
dws chat message send --group <openConversationId> --at-all "<@all> 请大家注意"
dws chat message send --group <openConversationId> --at-open-dingtalk-ids odt1,odt2 "<@odt1> <@odt2> 请查收"
# 图片/文件/音频/视频,一条命令直发
dws chat message send --group <openConversationId> --msg-type file --file-path ./screenshot.png
dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file-path ./report.pdf
dws chat message send --group <openConversationId> --msg-type audio --file-path ./voice.mp3
dws chat message send --group <openConversationId> --msg-type video --file-path ./demo.mp4
# 位置/联系人名片
dws chat message send --group <openConversationId> --msg-type location --latitude <纬度> --longitude <经度> --location-name <地址名称> --map-thumbnail-url "@mediaId"
dws chat message send --group <openConversationId> --msg-type profile --contact-id <openDingTalkId>
```
关键 flags:
| Flag | 说明 |
|------|------|
| `--group` | 群聊 openConversationId;别名 `--id` / `--chat` / `--conversation-id` |
| `--user` | 单聊接收人 userId |
| `--open-dingtalk-id` | 单聊接收人 openDingTalkId |
| `--text` | 消息内容,推荐使用;也支持位置参数 |
| `--title` | 消息标题,未传时使用安全标题 |
| `--at-all` | 群聊 @所有人,正文需含 `<@all>` |
| `--at-open-dingtalk-ids` | 群聊 @指定 openDingTalkId,正文需含 `<@id>` |
| `--msg-type` | `file` / `audio` / `video` / `image` / `location` / `profile`;本地音视频用 `audio` / `video`,底层按 `file` 发送 |
| `--file-path` | 本地文件路径,`msg-type=file/audio/video` 时自动上传并发送 |
| `--media-id` | 旧图片链路 mediaId |
| `--latitude` / `--longitude` / `--location-name` | 位置消息参数 |
| `--map-thumbnail-url` | 位置消息缩略图 mediaId,形如 `@mediaId` |
| `--contact-id` | 联系人名片 openDingTalkId |
| `--uuid` | 幂等 UUID,24h 内相同值不重复投递 |
当前没有经过验证的 Thread writer。`openConvThreadId` 只用于 `+thread-replies` 读取或
`+messages-forward-topic` 转发;不要把它作为普通 `--group` 猜测写入。引用回复使用
`+messages-reply`,但这不等于 Thread 内新增回复。
读取话题回复可直接传话题主消息 `--message-id`,CLI 会先通过只读消息详情解析出
`conversationId/threadId`;也可显式传 `--group` 加 `--thread-id/--topic-id`。前一种模式如果同时传
`--group`,会校验它与消息解析出的会话一致;解析失败只会报错,不会错误转去查询通讯录。
默认只读一页;完整读取必须显式加 `--page-all`。可用
`--limit/--page-size` 控制每页条数、用 `--page-limit` 限制最大页数;自动续页使用下层返回的
毫秒级 `nextCursor` 无损生成下一次 `startTime`,不能使用只有秒级精度的回复 `createTime`。
输出默认 `--order desc`(兼容 `--sort`)。由于下层的 `newer/older` 表示读取方向而不是结果排序,
`asc` 只允许与 `--page-all` 一起使用:完整拉取后对整体结果升序排列,避免把单个“最新页”的本地反转
伪装为全局升序。结果中的 `orderScope=complete_result` 表示完整结果排序;读取被页数上限或错误截断时为
`fetched_pages`,并仍须结合完整性 ledger 判断。
必须检查 `complete`、`hasMore`、`stopReason` 和
`failures`,`complete=false` 时不得声称已经拿到全部回复。
```bash
dws chat +thread-replies --message-id <rootOpenMessageId> --page-all --order asc
dws chat +thread-replies --group <openConversationId> --thread-id <openConvThreadId> --page-all --page-limit 50
```
### 拉取消息的底层 fallback
默认使用 `dws chat +chat-messages`。群聊的 `--group` 可传群名或 openConversationId;
也可用 `--chat-query` 显式按群名解析、用 `--conversation-id` 显式传稳定 ID。全量读取加 `--page-all`,必要时用 `--page-limit`、
`--max-results` 控制边界,用 `--output <相对.json>` 原子导出。只有需要原始响应或显式手工
continuation 时才使用下表;原子 `message list` 不代表自动全量分页。
可附带非必填的 `--sender-query <姓名>` 做读取后筛选。未传姓名时正常返回全部;姓名未解析出稳定 ID 时保留全部并记录失败;唯一解析出 userId/openDingTalkId 后按消息 `senderId` 筛选,覆盖最终 `messages/count` 并返回 `resolvedFilters`。该调用已经完成读取、解析与筛选,不要补跑 `+search-msg`。
时间范围参数同样公开但非必填:`--start`(包含)、`--end`(不包含)、`--order asc|desc`,
兼容别名为 `--start-time/--end-time/--sort`。范围固定为 `[start,end)`;仅开始时间表示到本次执行当前时间,
仅结束时间只支持 `desc`,`asc` 必须提供开始时间。旧 `--time/--direction` 保持兼容但不能和范围模式混用。
```bash
dws chat +chat-messages --group "项目群" --sender-query "测试用户甲" --page-all --format json
dws chat +chat-messages --group "项目群" --start "2026-08-01T00:00:00+08:00" --end "2026-08-02T00:00:00+08:00" --order asc --page-all --format json
```
当会话已经确定、任务只需要完整读取结果中可由消息字段判断的子集时,不新增按条件专用的
Shortcut 参数。在同一次 `+chat-messages` 调用中使用全局 `--jq`,让 Runtime 完成读取后、
在 stdout 前筛选;表达式必须保留根信封、用筛选结果覆盖 `messages` 并同步重算 `count`,
不得丢失 `complete`、`hasMore`、`failures` 等完整性 ledger。不要先输出全量 JSON,再由
Agent 或另一条命令二次处理。
```bash
# 例:读取完整会话后,只返回存在 reaction 的消息
dws chat +chat-messages --group "项目群" --page-all --format json \
--jq '. as $root | [.messages[] | select((.reactions // []) | length > 0)] as $matched | $root | .messages = $matched | .count = ($matched | length)'
```
发送者姓名不是普通结果字段条件:仍用 `--sender-query <姓名>` 先解析稳定身份,再按
`senderId` 筛选,不能用 `--jq` 对展示名做字符串匹配。
| 命令 | 用途 | 示例与要点 |
|------|------|------------|
| `message list` | 拉取指定群聊或单聊消息 | `dws chat message list --group <cid> --time "2025-03-01 00:00:00" --direction older`;目标三选一,`--direction newer/older` 优先于旧 `--forward` |
| `message list-all` | 时间范围内全部会话消息 | `dws chat message list-all --start <ISO> --end <ISO> --limit 100 --cursor 0`;默认一页,完整遍历加 `--page-all`,保留并合并 `result.conversationMessagesList` |
| `message list-by-sender` | 查指定发送者消息 | `--sender-user-id` 与 `--sender-open-dingtalk-id` 二选一,跨单聊+群聊;完整遍历加 `--page-all`,同一会话跨页合并 messages |
| `message list-mentions` | 查 @ 我的消息 | 可传 `--group` 限定群,不传查全部;完整遍历加 `--page-all`,同一会话跨页合并 messages |
| `message list-focused` | 查特别关注人消息 | 零参数可用,可加 `--limit` / `--cursor`;完整遍历加 `--page-all`,cursor 为 int64 |
| `message list-unread-conversations` | 未读会话列表 | 可选 `--count` |
| `message list-topic-replies` | 拉取话题回复 | `--group <openConversationId> --topic-id <openConvThreadId>` |
| `message list-by-ids` | 按消息 ID 批量查询 | `--msg-ids msgId1,msgId2`,最多 50 条 |
Typed `chat message` 自动翻页只由 `--page-all` 触发;只传 `--page-limit`、`--max-items` 或 `--page-delay` 仍保持默认单页 fallback。`conversationMessagesList` 结构会保留并按 `openConversationId` 合并 messages。分页元数据输出到顶层 `paging`,包含 `truncated`、`hasMore`、`lastCursor`、`pages`、`total`;非第一页失败时会输出 partial 结果和 `failedPage` / `failedCursor` / `pagesFetched` / `itemsFetched`。
`message list` 注意事项:
- `--group`、`--user`、`--open-dingtalk-id` 互斥且必须指定一个。
- `--time` 格式为 `yyyy-MM-dd HH:mm:ss`。
- `hasMore=true` 时,用结果中的边界 `createTime` 作为下次 `--time`。
- 返回 `openConvThreadId` 表示话题消息,完整内容需再拉 `list-topic-replies`。
### 搜索消息的底层 fallback
直接按发送者、关键词、@对象或消息类型检索时优先使用 `dws chat +search-msg`;搜索范围可以是单个、多个或全部会话。若已选择 `+chat-messages` 读取指定会话,可由其非必填 `--sender-query` 在同一次调用完成姓名解析和筛选。
- 搜索内容使用公开参数 `--query`。
- 已知稳定会话 ID 使用 `--group` / `--groups`;已知稳定发送者 ID 使用 `--senders`。
- 只有群名时使用非必填参数 `--chat-query`,由 CLI 唯一解析会话。
- 只有发送者姓名时使用非必填参数 `--sender-query`,由 CLI 唯一解析人员。
- 不要把群名传给只接受稳定 ID 的会话参数,也不要把姓名传给 `--senders`。零命中或多候选时停止,不选择第一项。
- 不传会话过滤时搜索全部会话;`--page-all` 只翻完当前时间范围内的游标页,默认时间范围是最近 7 天。
- 精确范围使用成对的 `--start/--end`(兼容 `--start-time/--end-time`);`--order`(兼容 `--sort`)稳定排列本次实际取得的结果。未 `--page-all` 或 `complete=false` 时不能称为完整范围的全局排序。
需要 Shortcut 未暴露的原始过滤字段或响应时,才评估 `message search-advanced`。它是原子 `message search` 的严格超集,但不是 Agent 高频默认入口。
```bash
# 单群 + 发送者姓名
dws chat +search-msg --chat-query "项目群" --sender-query "测试用户甲" --page-all --format json
# 单群 + 关键词
dws chat +search-msg --chat-query "项目群" --query "发布计划" --page-all --format json
# 跨全部会话 + 发送者姓名
dws chat +search-msg --sender-query "测试用户甲" --page-all --format json
dws chat +search-msg --query "发布计划" --start-time "2026-08-01T00:00:00+08:00" --end-time "2026-08-02T00:00:00+08:00" --sort asc --page-all --format json
```
```bash
dws chat message search-advanced --query "周报" --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00"
dws chat message search-advanced --user <userId> --start <ISO> --end <ISO>
dws chat message search-advanced --at-me --start <ISO> --end <ISO>
dws chat message search-advanced --conversation-ids <cid1>,<cid2> --query "合同" --limit 50 --cursor 0
dws chat message search-advanced --query "周报" --start <ISO> --end <ISO> --page-all --max-items 200
dws chat message search-advanced --message-type file --search-conv-type group_chat --query "附件"
dws chat message search-advanced --only-robot-messages --query "通知"
```
| 参数 | 说明 |
|------|------|
| `--query` | 搜索关键词,可选 |
| `--user` / `--users` | 发送者 userId |
| `--sender-ids` | 发送者 openDingTalkId |
| `--at-me` / `--at-ids` | @ 我 / @ 指定 openDingTalkId |
| `--conversation-ids` | 多个群聊或单聊 openConversationId;别名 `--groups` |
| `--message-type` | 按消息类型过滤,例如 `file` |
| `--search-conv-type` | 按会话类型过滤,例如 `group_chat` |
| `--only-robot-messages` | 只搜索机器人消息 |
| `--start` / `--end` | ISO-8601 时间范围 |
| `--cursor` / `--limit` | 分页,翻页用 `nextCursor` |
| `--page-all` / `--page-limit` / `--max-items` / `--page-delay` | 自动翻页;`--page-all` 是唯一触发开关 |
仅简单关键词搜索时可用:
```bash
dws chat message search --query "changefree" --start <ISO> --end <ISO> --limit 50 --cursor 0
dws chat message search --query "codereview" --group <openConversationId> --start <ISO> --end <ISO>
dws chat message search --query "发布计划" --start <ISO> --end <ISO> --page-all --page-delay 0
```
### 消息状态与撤回
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `message query-send-status` | 查询当前用户发消息任务状态 | `--open-task-id`,来自 `message send` 返回 |
| `+messages-recall` | 撤回当前用户消息 | `--msg-id`;可选 `--conversation-id`,省略时 CLI 从消息详情补齐;兼容单值 `--message-ids` |
| `message edit` | 编辑已发送消息内容 | `--conversation-id` `--msg-id`,并在 `--text` / `--content` 中二选一;可选 `--title` `--at-all` `--at-open-dingtalk-ids` |
| `message read-status` | 查消息已读/未读状态 | `--group` `--message-id`;可选目标用户 |
刚由 `message send` 发出的消息会返回 `openTaskId`。先用 `message query-send-status` 查询,成功结果中的 `openMessageId` 和 `openConversationId` 可直接传给 `message edit` 或 `message recall`,无需再按消息内容从列表反查 ID。
```bash
# 1. 发送后保留 openTaskId
dws chat message send --group <openConversationId> --text "原始内容"
# 2. 查询得到 openMessageId 和 openConversationId
dws chat message query-send-status --open-task-id <openTaskId>
# 3. 编辑消息
dws chat message edit --conversation-id <openConversationId> --msg-id <openMessageId> --text "更新后的内容"
# 发送后撤回使用同一 ID 链
dws chat message send --group <openConversationId> --text "待撤回的内容"
dws chat message query-send-status --open-task-id <openTaskId>
dws chat message recall --conversation-id <openConversationId> --msg-id <openMessageId>
```
`+messages-recall` 与 `recall-by-bot` 不同:前者使用 `openMessageId`,缺少会话 ID 时先只读查询消息详情;后者撤回机器人消息,需要 `robot-code + processQueryKey`。不要把 `processQueryKey` 当 `openMessageId`。
编辑消息使用 `message edit`。推荐传 `--text`,CLI 会生成 markdown content JSON:`{"title":"标题","text":"正文"}`;可选 `--title`,不传时会从正文自动生成标题。高级场景可直接传 `--content`,此时必须是完整 markdown content JSON,且不能同时传 `--text`。
@ 规则:`--at-all` 会传 `atAll=true`,正文应包含 `<@all>`,未包含时 CLI 会自动补到开头;`--at-open-dingtalk-ids` 会传 `atOpenDingTalkIds`,正文需包含对应 `<@openDingTalkId>` 占位符,裸 `@openDingTalkId` 会自动补成尖括号格式。
```bash
dws chat message edit --conversation-id <openConversationId> --msg-id <openMessageId> --text "更新后的内容"
dws chat message edit --group <openConversationId> --msg-id <openMessageId> --title "标题" --text "更新后的内容"
dws chat message edit --group <openConversationId> --msg-id <openMessageId> --text "<@all> 请查看" --at-all
dws chat message edit --group <openConversationId> --msg-id <openMessageId> --text "<@openDingTalkId1> 请查看" --at-open-dingtalk-ids <openDingTalkId1>
dws chat message edit --group <openConversationId> --msg-id <openMessageId> --content '{"title":"标题","text":"更新后的内容"}'
```
### 回复与转发的底层 fallback
引用回复默认使用 `dws chat +messages-reply`。以下原子 reply 只保留底层字段 fallback;转发
仍按各自精确 Shortcut/leaf Schema 选择。
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `message reply` | 引用回复,单聊/群聊均可;群聊可 @指定成员或 @所有人 | `--conversation-id` `--ref-msg-id` `--ref-sender` `--text`;可选 `--at-open-dingtalk-ids` `--at-all` |
| `message forward` | 转发单条消息,源/目标均支持单聊/群聊 | `--src-conversation-id` `--msg-id` `--dest-conversation-id` |
| `message combine-forward` | 多条消息合并为一条转发 | `--src-conversation-id` `--msg-ids` `--dest-conversation-id`,可选 `--uuid` |
| `message forward-topic` | 转发话题消息 | `--src-msg-id` `--src-conversation-id` `--src-thread-id` `--dest-conversation-id` |
群聊引用回复使用 `--at-open-dingtalk-ids` 传 `atOpenDingTalkIds`;正文缺少对应 `<@openDingTalkId>` 时自动补齐,已有裸 `@openDingTalkId` 会规范化。`--at-all` 会传 `atAll=true`,正文缺少 `<@all>` 时自动补齐。
```bash
dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <senderOpenDingTalkId> --text "请看一下" --at-open-dingtalk-ids <mentionedOpenDingTalkId>
dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <senderOpenDingTalkId> --text "请大家确认" --at-all
```
### 话题与卡片
话题完整读取流程:
1. `dws chat message list --group <openConversationId> --time ...` 获取话题主消息。
2. 如果返回 `openConvThreadId`,执行 `dws chat message list-topic-replies --group <openConversationId> --topic-id <openConvThreadId>`。
流式卡片优先使用公开 Shortcut;创建可选在同一次调用中写入内容:
```bash
dws chat +messages-send-card --group <openConversationId> --at-open-dingtalk-ids <mentionedOpenDingTalkId> --content "开始处理" --flow-status 1
dws chat +messages-update-card --biz-id <bizId> --content "更新的卡片内容" --flow-status 2
dws chat +messages-update-card --biz-id <bizId> --content "最终内容" --flow-status 3
```
`flow-status`:1=处理中,2=输入中,3=完成,4=执行中,5=错误,Runtime 拒绝范围外值。
群聊还可传 `--at-all`;两种艾特参数只随创建请求发送。send-card 同时带正文时,
Runtime 会把创建响应的 `atTag` 自动放在正文前;不要自行写 ID 或占位符。
当前只支持 streaming text;不支持 Card JSON 组件或 action callback。精确边界见
[card references](../card/schema.md)。
### Pin / Top / Favorite
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `message set-pin-msg` / `unset-pin-msg` | 钉住/取消钉住消息 | `--open-conversation-id` `--msg-id` |
| `message list-pin-msg` | 拉取钉住消息列表 | `--open-conversation-id`,可选 `--cursor` `--size` |
| `message set-top-msg` / `unset-top-msg` | 置顶/取消置顶会话内某条消息 | `--open-conversation-id` `--msg-id` |
| `message add-favorite` | 收藏消息 | `--open-message-id` `--open-conversation-id` |
| `message remove-favorite` | 取消收藏消息 | `--open-message-id` `--open-conversation-id` |
| `+flag-list` | 查询收藏消息列表的默认 Shortcut | 默认一页;要求全部时加 `--page-all`;`--page-size/--size` 范围为 1-30,可用 `--page-token` 或兼容的 `--cursor` 续页,检查 `complete` / `failures` |
| `message list-favorites` | typed 收藏消息列表 | 默认一页;完整遍历加 `--page-all`,数字 cursor,聚合 `result.items`;可选 `--cursor` `--size` |
`+flag-list` 查询钉钉 message favorite,底层使用数字 cursor;它与消息 Pin、消息 Top 和会话置顶属于不同对象层级。
`message list-favorites --page-all` 仍遵守 `--size` 1-30;`--page-limit` 只控制最多请求页数,`--max-items` 可精确截断输出。
消息置顶 `set-top-msg` 与会话置顶 `chat set-top` 不同:前者置顶会话内消息,后者置顶整个会话。
### 表情回应
优先查 [chat-emoji-list.md](../chat-emoji-list.md) 中的默认表情名称。
| 命令 | 场景 | 必填参数 |
|------|------|----------|
| `message add-emoji` / `remove-emoji` | 默认表情命中时使用 | `--conversation-id` `--msg-id` `--emoji` |
| `message create-text-emotion` | 默认表情没有合适项时先创建 | `--emotion-name` `--text`,可选 `--background-id` |
| `message add-text-emotion` / `remove-text-emotion` | 添加/移除文字表情 | `--conversation-id` `--msg-id` `--emotion-id` `--emotion-name` `--text` `--background-id` |
| `message update-text-emotion` | 用新的文字表情替换消息上的原回应 | `--conversation-id` `--msg-id` `--old-emotion-id` `--emotion-id` `--emotion-name` `--text` `--background-id` |
| `message list-emotion-replies` | 批量查询消息的表情回复和文字回复 | `--msg-ids` |
```bash
dws chat message list-emotion-replies --msg-ids msgId1,msgId2,msgId3
```
消息 ID 可通过 `dws chat message list` 获取;该命令用于一次性查看多条消息上的 emoji 回应和文字表情回应。
### 文本工具
#### `dws chat text translate`
将指定文本翻译成目标语言。用户只说“翻译这段文字”时使用;不要误走 `message send`。
```bash
dws chat text translate --query "你好世界" --to en_US
dws chat text translate --query "Hello World" --to zh_CN
dws chat text translate --query "Bonjour" --to ja_JP
```
关键 flags:
| Flag | 说明 |
|------|------|
| `--query` | 待翻译文本,必填 |
| `--to` | 目标语言代码,必填,默认 `en_US` |
### 文件与媒体
#### `dws chat message download-media`
```bash
dws chat message download-media --type mediaId --resource-id <mediaId> --message-id <openMessageId> --open-conversation-id <openConversationId> --output ./downloads/
```
`resource-id` 来自消息内容中的 mediaId,`message-id` 来自 `openMessageId`,会话 ID 来自 `chat search` 或 `conversation-info`。
公开 `+messages-resource-download` 使用工作目录内安全相对路径、默认不覆盖、整文件临时落盘后
原子发布。当前没有 Range/断点续传;失败时保留 ledger 或错误,显式重试整个文件,不拼接残片。
## 常见工作流
### 群聊发文字与文件
```bash
dws chat +send-to-group --group "项目冲刺" --text "请大家本周五前提交周报" --format json
dws chat +messages-send --as user --chat-query "项目冲刺" --file ./report.pdf --uuid <uuid> --format json
dws chat +messages-send --as user --chat-query "项目冲刺" --msg-type audio --file ./voice.mp3 --uuid <uuid> --format json
dws chat +messages-send --as user --chat-query "项目冲刺" --msg-type video --file ./demo.mp4 --uuid <uuid> --format json
# 仅 Shortcut 尚未覆盖的位置/名片底层类型才使用原子 fallback
dws chat message send --group <openConversationId> --msg-type location --latitude <纬度> --longitude <经度> --location-name <地址名称> --map-thumbnail-url "@mediaId" --format json
dws chat message send --group <openConversationId> --msg-type profile --contact-id <openDingTalkId> --format json
```
### 查消息并撤回
```bash
dws chat +chat-messages --group <openConversationId> --direction older --format json
dws chat +messages-recall --msg-id <openMessageId> --format json
```
### 多维度搜索
```bash
dws chat message search-advanced --query "合同" --conversation-ids <cid1>,<cid2> --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00" --limit 50 --cursor 0 --format json
dws chat message search-advanced --message-type file --search-conv-type group_chat --query "附件" --format json
```
## 常见错误与回退
- 发送目标不唯一:保留 resolver 返回的候选并让用户消歧;不要退回手工搜索后选择第一项。
- `unknown flag`:立即执行对应命令 `--help`,不要猜参数。
- 文件/音视频发送失败:确认本地路径可读;新链路使用 `--msg-type file|audio|video --file-path`。
- 位置消息参数不完整:先确认经纬度、地址名称和缩略图 mediaId。
- 名片发送失败:确认 `--contact-id` 是 openDingTalkId,不是 userId。
- 话题回复缺失:检查是否只拉了主消息,需继续用 `list-topic-replies`。
- `search-advanced` 无条件:至少提供 query、sender、@、conversation、时间等任一有效过滤条件。
@@ -0,0 +1,144 @@
# group-admin:群创建、成员写入与管理
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
用于建群、修改群资料、成员增删、邀请卡片分享、群主和管理员、禁言、公告、群设置、
入群审批、群身份、退出、解散和升级外部群。只读群发现、成员读取和邀请链接使用
[group-discovery.md](group-discovery.md)。
## 安全与目标
- 群目标统一使用当前 profile 下真实 `openConversationId`;支持自然群名的 Shortcut 由 CLI
唯一解析,多候选时停止。
- 解散群、踢人、转让群主、禁言、管理员和外部群升级都是高影响操作;以最终 Runtime gate
和精确 leaf Schema 为准确认对象、动作与影响。
- 所有自然成员和群主必须先完成唯一解析并按稳定 ID 去重,再开始任何写入;不得边解析边
产生部分副作用。
- 群公告会触达成员;`notice edit` 是整体替换,必须有完整新正文。
## 建群与基础资料
<!-- dws-intent: chat.create.group -->基础建群使用 `dws chat +chat-create`。已知成员 ID 传 `--users`,
姓名/花名传 `--member-query`;群主默认当前用户,也可传 `--owner-open-dingtalk-id` 或
`--owner-query`。任一自然身份未唯一解析时,创建前整体停止。
```bash
dws chat +chat-create --name "项目冲刺群" --member-query "测试用户甲,测试用户乙" --format json
dws chat +chat-create --name "合作群" --member-query "测试用户甲" \
--owner-query "测试用户乙" --type EXTERNAL --format json
```
修改群名称优先使用接受群名或稳定 ID 的 `+chat-update`:
```bash
dws chat +chat-update --group <群名或openConversationId> --name "新群名" --format json
```
群头像和管理员级群开关使用 `+chat-update-icon`、`+chat-update-settings`;只有 Shortcut
尚未发布真实必需字段时才评估原子 `group rename/update-icon/update-settings`。
原子 `chat group create` 只用于 `+chat-create` 未发布的真实底层字段,并先读取精确 leaf
Schema。普通内部/外部群、话题群和显式群主已经由 `+chat-create` 覆盖,不回流到手工
`aisearch → group create` 链路。
## 成员与机器人写入
| 动作 | 入口与关键参数 |
|---|---|
| 添加成员 | `group members add --id <cid> --users <userIds>` |
| 移除成员 | `group members remove --id <cid> --users <userIds>` |
| 添加已知机器人 | `+chat-add-bot` 或精确原子 `group members add-bot` |
| 查看群内机器人 | `+chat-bots --group <群名或cid>` |
| 移除群内机器人 | `+chat-remove-bot` 或精确原子 `group members remove-bot` |
普通成员增删的 `--users` 只接受组织 `userId`,必须来自真实人员解析结果;不得把
`+chat-members-list` / `+chat-members-get` 返回的 `openDingTalkId` 直接传入。添加已知机器人
使用 `robotCode`;移除机器人使用当前群 `+chat-bots` 返回的真实 `openBotId`,两者不能互换。
缺少 `openBotId` 时在同一流程中先执行 `+chat-bots`,不必额外读取群发现 reference。只有需要
搜索未知机器人、区分 `bot search` / `bot find`、机器人发送或撤回、Webhook 时,才读取
[chat-bot.md](chat-bot.md)。
## 邀请卡片、群主、管理员与禁言
邀请链接只读走 `+chat-invite-url`。实际分享邀请卡片使用 `group share-invite`:`--source`
是被分享群,接收端在 `--target` 会话和 `--receiver` 单聊用户之间二选一。
```bash
dws chat group share-invite --source <sourceCid> --target <targetCid> --format json
dws chat group share-invite --source <sourceCid> --receiver <openDingTalkId> --format json
```
| 动作 | 入口与关键参数 |
|---|---|
| 转让群主 | `+chat-transfer-owner --group <cid> --new-owner <稳定ID>` |
| 设置/取消管理员 | `group set-admin --group <cid> --users <ids> [--off]` |
| 全员禁言/解除 | `group-mute --group <cid> [--off]` |
| 成员禁言/解除 | `+chat-mute-member` 或 `group-mute-member` |
| 查询禁言配置 | `group get-mute-config --group <cid>` |
原子 `group-mute-member --mute-time` 单位为毫秒。不要用展示名称代替稳定用户 ID,也不要
在未确认影响时执行转让、踢人或禁言。
## 群设置与当前用户偏好
管理员级群开关使用 `+chat-update-settings` 或原子 `group update-settings`。常见 settingKey
包括 `authority`、`joinValidation`、`onlyAdminCanAtAll`、`searchable`、
`addFriendForbidden`、`onlyAdminCanDING`、`onlyAdminCanPinMsg` 和
`onlyAdminCanSendFile`、`groupEmailDisabled`、`groupLiveAuthority`、
`groupBillAuthority`;只修改用户明确要求的字段。
新成员历史消息可见范围使用 `group set-history --group <cid> --option <值>`;`option` 只取
精确 leaf Schema 发布值,不按自然语言猜枚举。
当前登录用户自己的置顶、免打扰、群昵称和群备注使用 `group user-settings query/set`,
不是管理员群开关。单个群昵称/备注优先 `group update-nick/update-alias`。
```bash
dws chat group user-settings query --groups <cid1>,<cid2> --format json
dws chat group user-settings set \
--items '[{"openConversationId":"cid1","top":true,"mute":false}]' --format json
```
批量设置只传本次要改的字段;空字符串清除昵称或备注,不补用户未要求的值。
## 群公告
| 动作 | 原子入口 |
|---|---|
| 发布公告 | `group notice create --group <cid> --content <完整Markdown>` |
| 修改公告 | `group notice edit --group <cid> --notice-id <id> --content <完整Markdown>` |
| 查询公告 | `group notice get/list` |
定时公告 `--run-at` 使用带时区时间;`notice list --scheduled` 查询待发布公告。分页时沿真实
`nextPageCursor` 继续。修改前必须取得完整替换正文,不把增量片段当整篇公告。
## 入群审批与群身份
先用 `group list-join-validations` 取得真实 `record-id/applicant/inviter`,再执行
`group audit-join-validation` 或 `+chat-audit-join`。审批状态只使用精确 leaf Schema 发布值。
群身份使用 `group-role` / `+chat-role-*`:
- `list/add/update/remove` 管理身份定义;
- `set-user/remove-user/query-user` 管理成员身份;
- `openRoleId` 必须来自真实身份列表。
覆盖或清除成员身份前确认用户、群和完整角色集合,不能用展示名称猜 `openRoleId`。
## 退出、解散与外部群升级
- 当前用户退出群:`+chat-quit` 或精确原子 `group quit`。
- 解散群:`group dismiss`,不可逆。
- 普通群升级外部群:`group upgrade-to-external`,不可逆。
这些动作必须以最终 Runtime gate 为准,不把示例中的确认参数当固定事实。
## 完成与错误
- 创建或更新后保留真实 `openConversationId` 和任务结果;只对查询结果真实返回的字段执行读回验证。
- 写接口成功但现有查询未返回目标设置时,报告真实写入回执和不可独立读回的边界;不用群名、
成员数等其他字段代替验证,也不猜未发布的读回命令。
- 任一自然目标零命中或多候选时,在写入前整体停止。
- 逐项写入保留 succeeded/failed/unknown ledger,不用重试抹掉失败项。
- 分享邀请时 `--target` 与 `--receiver` 只能二选一;接收对象不明确时先确认。
- 机器人进群失败时确认机器人身份和当前用户管理权限,不连续切换同义原子命令。
@@ -0,0 +1,121 @@
# group-discovery:群发现、列表与成员读取
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
用于只读的群列表、群搜索、共同群、群成员、群机器人和邀请链接。建群、改群、成员增删、
邀请卡片分享、公告、禁言和其他群管理写操作读取 [group-admin.md](group-admin.md)。
## 入口选择
| 用户终点 | 唯一推荐入口 |
|---|---|
| 我加入的全部群 | `dws chat +my-groups --page-all` |
| 我创建或管理的群 | `dws chat +chat-list-mine` |
| 只看群主群或管理员群 | `+chat-list-mine --role OWNER|ADMIN` |
| 按关键词搜索群 | `dws chat +chat-search --query <关键词>` |
| 查看指定群全部成员 | `dws chat +chat-members-list --group <群名或ID>` |
| 已知成员 openDingTalkId 批量查群内详情 | `dws chat +chat-members-get --id <cid> --users <ids>` |
| 获取群邀请链接 | `dws chat +chat-invite-url --group <群名或ID>` |
| 查看群机器人 | `dws chat +chat-bots --group <群名或ID>` |
“全部群”与“全部会话”不同:`+my-groups` 只列当前用户加入的群;
`+conversation-list --page-all` 可能同时包含单聊和群聊,不能替代群成员关系。
## 群列表、分页与角色
`+my-groups` 返回当前用户加入的群,包括作为群主、管理员和普通成员加入的群:
- 要求完整列表时使用 `--page-all`;Runtime 沿真实 `nextCursor` 读取后续页,并按
`openConversationId` 合并去重,读完后再应用可选 `--type` 本地过滤。
- `--limit` 是每页数量,不是最终结果上限;`--cursor` 只用于从已有 `nextCursor` 手工续读。
- `--page-limit` 只与 `--page-all` 一起使用,用于限制最多读取页数。达到上限后仍有下一页时,
结果不完整。
- 只有 `complete=true` 且 `hasMore=false` 才能声称已经读取全部;否则保留 `nextCursor`、
`stopReason` 和 `failures` 并说明结果不完整。
```bash
dws chat +my-groups --page-all --page-limit 50 --format json
```
`+chat-list-mine` 只返回当前用户作为群主或管理员的群。只要群集合时不传 `--role`,
一次取得 OWNER 和 ADMIN。要求逐项标明身份时,直接分别查询 `--role OWNER` 和
`--role ADMIN`,不先执行无角色查询或读取 Help;按 `openConversationId` 合并去重后,
再应用一次全局数量上限,不得把两个分支直接拼接。
```bash
dws chat +chat-list-mine --limit 20 --format json
dws chat +chat-list-mine --role OWNER --format json
dws chat +chat-list-mine --role ADMIN --exclude-muted --format json
```
`+my-groups` 不提供当前用户角色。用户明确要求普通成员群时,使用
`chat group list-all --limit 200`;返回 `hasMore=true` 时,必须把真实 `nextCursor`
传给下一次调用并继续读取,直到 `hasMore=false`,不得把继续翻页交给用户。读完后按
`openConversationId` 去重,仅筛选真实返回的 `myRole=普通成员`;不得给 `+my-groups`
编造 `--role MEMBER`,也不得用“全部群减去 OWNER/ADMIN 群”推断。
## 群搜索与稳定 ID
群搜索默认使用 `+chat-search`。要求全部候选时加 `--page-all`;可用
`--page-size/--page-token` 或兼容 `--limit/--cursor`。零命中或多候选时停止并展示候选,
不要选择第一项。
```bash
dws chat +chat-search --query "项目冲刺" --page-all --format json
```
只有数字群号时,使用 `chat group get-by-group-id --group-id <数字>` 转换为
`openConversationId`。需要搜索共同群时使用原子 `chat search-common`;`AND` 表示所有人
都在群里,`OR` 表示任一人在群里。自然人员必须先解析为当前 profile 的真实身份。
```bash
dws chat search-common --nicks "测试用户甲,测试用户乙" --match-mode AND --limit 20 --cursor 0
```
## 群成员
`+chat-members-list` 接受群名或 `openConversationId`,唯一解析后全量读取,并把用户与机器人
分桶。结果必须检查 `buckets/complete/failures`。
```bash
dws chat +chat-members-list --group "项目群" --format json
dws chat +chat-members-list --conversation-id <openConversationId> --format json
```
先检查 `+chat-members-list` 的稳定结果。只有结果未包含用户要求的群昵称、角色或其他群内字段时,
才使用其中的真实 `openDingTalkId` 批量调用:
```bash
dws chat +chat-members-get --id <openConversationId> \
--users <openDingTalkId1>,<openDingTalkId2> --format json
```
不要为了群内详情默认切换到企业通讯录;只有用户明确要求部门、岗位、直属主管等企业资料时,
才把真实 userId 交给 `dingtalk-contact`。
## 邀请链接与机器人
`+chat-invite-url` 是只读获取链接,可选 `--expires-seconds`;`group share-invite` 会实际把
邀请卡片发送给另一个会话或用户,属于 [group-admin.md](group-admin.md)。
`+chat-bots` 返回稳定 `bots[]` 和 `openBotId`,供后续移除。搜索可用机器人、机器人发送和
撤回读取 [chat-bot.md](chat-bot.md)。
## 原子 fallback
| 原子命令 | 仅用于 |
|---|---|
| `chat search` / `search-common` | Shortcut 未发布的搜索字段或共同群 |
| `chat group get-by-group-id` | 数字群号转换 |
| `chat group members` | 需要原始成员分页响应 |
| `chat group members list-by-ids` | 需要原始批量成员详情 |
| `chat group list-all` / `list-my-groups` | 需要 Shortcut 未投影的真实底层字段 |
使用原子 fallback 前读取精确 leaf Schema;不得把 fallback 写成与 Shortcut 并列的默认路线。
## 完成与错误
- 分页完成只以真实 `complete/hasMore/nextCursor/failures` 判断,不看过滤后的 `count` 猜测。
- 所有稳定 ID 必须来自同一 profile 的真实返回。
- 找不到群或出现多候选时停止,不臆测 `openConversationId`。
- 任务从只读发现转为写操作时,使用 [group-admin.md](group-admin.md) 的目标和安全规则。
@@ -0,0 +1,132 @@
# message-actions:消息编辑、撤回、回复与对象操作
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
用于对真实消息执行编辑、撤回、引用回复、转发、Pin、Top、Favorite 和表情回应写操作,
并包含必要的紧邻验证。需要跨多个阶段传递真实结果的组合流程由
[01-messaging.md](../01-messaging.md) 说明;本文件不重复完整工作流。
## 入口选择
| 用户终点 | 推荐入口 |
|---|---|
| 撤回当前用户消息 | `dws chat +messages-recall --msg-id <openMessageId>` |
| <!-- dws-intent: chat.reply.quote -->引用回复 | `dws chat +messages-reply` |
| 编辑已发送消息 | `dws chat message edit` |
| 单条/合并/话题转发 | `+messages-forward` / `+messages-combine-forward` / `+messages-forward-topic` |
| Pin / Unpin | `+messages-set-pin` / `+messages-unset-pin` |
| 消息 Top / 取消 Top | `+messages-set-top` / `+messages-unset-top` |
| Favorite / 取消 Favorite | `+flag-create` / `+flag-cancel` |
| 默认 emoji 回应 | `+messages-add-emoji` / `+messages-remove-emoji` |
所有写操作以最终 Runtime gate 和精确 leaf Schema 为准。确认对象、消息和影响后再执行;
不要因为文档示例自行制造或省略 confirmation。
## 稳定 ID 规则
- `openTaskId` 是发送任务 ID,不是消息 ID。
- 撤回、编辑、回复、转发、Pin、Top 和 reaction 使用真实查询结果中的 `messageId`。
- 同时保留消息的 `conversationId`、thread、发送者和引用上下文。
- 子消息使用自己的 `messageId`;只在缺会话 ID 时继承父消息 `conversationId`。
- Bot 撤回使用 `processQueryKey`,不使用本文件的 `openMessageId` 路线。
刚由用户身份发送的消息如果只得到 `openTaskId`,先查询发送状态:
```text
+messages-send 或 message send
→ openTaskId
→ message query-send-status
→ openMessageId + openConversationId
→ 编辑或撤回
```
## 撤回与编辑
`+messages-recall` 可只传 `--msg-id`;省略会话 ID 时 CLI 会通过只读消息详情补齐。
兼容单值 `--message-ids`,但不要把 `processQueryKey` 当消息 ID。
```bash
dws chat +messages-recall --msg-id <openMessageId> --format json
dws chat +messages-recall --conversation-id <openConversationId> --msg-id <openMessageId> --format json
```
编辑使用 `message edit --conversation-id <cid> --msg-id <id>`,并在 `--text` 与 `--content`
中二选一。`--text` 由 CLI 生成 Markdown content;`--content` 必须是完整 content JSON。
```bash
dws chat message edit --conversation-id <openConversationId> --msg-id <openMessageId> --text "更新后的内容"
dws chat message edit --conversation-id <openConversationId> --msg-id <openMessageId> --content '{"title":"标题","text":"更新后的内容"}'
```
群聊 @所有人使用 `--at-all`;指定人员使用 `--at-open-dingtalk-ids`。正文中的占位符以
Runtime 规范化结果为准,不把裸展示名当稳定身份。
## 引用回复与转发
引用回复默认使用 `+messages-reply`。`--conversation-id` 和消息 ID 来自真实查询;
`--ref-sender` 可省略时让 CLI 只读补齐,不手工猜发送者身份。
```bash
dws chat +messages-reply --conversation-id <openConversationId> \
--message-id <openMessageId> --text "收到" --format json
```
| 动作 | 入口 | 关键上下文 |
|---|---|---|
| 单条转发 | `+messages-forward` | 源消息 ID、源会话、目标会话 |
| 合并转发 | `+messages-combine-forward` | 多个真实消息 ID、源/目标会话 |
| 话题转发 | `+messages-forward-topic` | 源消息、源会话、源 thread、目标会话 |
只有 Shortcut 尚未发布真实必需字段时,才评估原子 `message reply`、`forward`、
`combine-forward` 或 `forward-topic`,并先读取精确 leaf Schema。不要复制正文伪装原生转发。
## Pin、Top 与 Favorite
| 对象 | 写入入口 | 说明 |
|---|---|---|
| 消息 Pin | `+messages-set-pin` / `+messages-unset-pin` | 作用于一条消息 |
| 消息 Top | `+messages-set-top` / `+messages-unset-top` | 作用于会话内一条消息 |
| Favorite | `+flag-create` / `+flag-cancel` | 当前用户收藏 |
| 会话 Top | `+conversation-set-top` | 作用于整个会话,不属于本文件 |
用户要求确认 Pin 已生效时,使用 `+messages-list-pin` 检查真实结果中的 `messageId`;取消 Pin
后仅在用户要求确认取消结果时再次查询。典型短链为:`+messages-set-pin` →
`+messages-list-pin` → `+messages-unset-pin`。
需要原子 fallback 时,消息 Pin 对应 `message set-pin-msg/unset-pin-msg`,消息 Top 对应
`message set-top-msg/unset-top-msg`,Favorite 对应 `message add-favorite/remove-favorite`。
四种对象不能互换,即使用户都使用“收藏、钉住、置顶”等自然语言。
## 表情回应
优先在 [chat-emoji-list.md](../chat-emoji-list.md) 按表情名称查默认 emoji,不必全文理解表格。
| 场景 | 入口 |
|---|---|
| 添加/移除默认 emoji | `+messages-add-emoji` / `+messages-remove-emoji` |
| 默认表情无合适项时创建文字表情 | `+messages-create-text-emotion` |
| 添加/移除文字表情 | `+messages-add-text-emotion` / `+messages-remove-text-emotion` |
| 替换文字表情 | `message update-text-emotion` |
reaction 查询属于 [message-query.md](message-query.md),不要为了查看回应执行写命令。
## 流式卡片与文本工具
流式卡片使用根 Skill 直接链接的 [card/create.md](../card/create.md)、
[card/update.md](../card/update.md) 和 [card/schema.md](../card/schema.md);本文件不复制卡片参数。
纯文本翻译使用:
```bash
dws chat text translate --query "你好世界" --to en_US
```
用户只要求翻译文本时不要误走消息发送。
## 完成与错误
- 写操作检查任务级结果、投递状态和失败项,不只看退出码。
- 投递状态 unknown 时保留幂等键,不自动换目标重发。
- `unknown flag` 时读取精确 leaf Help,最多修正一次。
- 目标消息不存在、会话不匹配或发送者上下文缺失时停止,不猜 ID。
- 已知稳定消息 ID 的单一动作及其紧邻验证均在本文件完成。
@@ -0,0 +1,76 @@
# message-media:特殊消息与资源下载
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
只用于位置、联系人名片、底层 mediaId/fileId 和消息资源下载。普通文本、Markdown、文件、
图片、音频和视频发送继续按根 Skill 使用 `+dm`、`+send-to-group` 或 `+messages-send --file`,
不读取本文件。
## 默认边界
- 用户身份普通文件/音视频:`dws chat +messages-send --as user --file <相对路径>`。
- 已知资源引用单独下载:`dws chat +messages-resource-download`。
- 从消息中定位并下载资源:在定位消息的 `+chat-messages`、`+search-msg` 或
`+messages-mget` 同一次调用中加 `--download-resources`。
- 只有 Shortcut 尚未发布的位置、联系人名片或真实底层媒体字段,才使用原子 fallback。
<!-- dws-intent: chat.send.advanced -->`dws chat +messages-send` 的 user 文件能力不能外推给 Bot/Webhook;
机器人富媒体边界读取 [chat-bot.md](chat-bot.md),不得静默改成当前用户身份。
## 位置与联系人名片
位置消息必须确认纬度、经度、地址名称和地图缩略图 mediaId:
```bash
dws chat message send --group <openConversationId> --msg-type location \
--latitude <纬度> --longitude <经度> --location-name <地址名称> \
--map-thumbnail-url "@mediaId"
```
联系人名片的 `--contact-id` 必须是联系人 `openDingTalkId`,不能把 userId 直接代入:
```bash
dws chat message send --group <openConversationId> \
--msg-type profile --contact-id <openDingTalkId>
```
用户要求真实发送结果时,保留发送返回的 `openTaskId`,再执行:
```bash
dws chat message query-send-status --open-task-id <openTaskId> --format json
```
检查真实 `sendStatus`、`openMessageId` 和 `openConversationId`。
原子 `message send` 只在 Shortcut 缺少真实必需字段时使用。群聊目标用 `--group`;单聊目标
用 `--user` 或 `--open-dingtalk-id`,三者通常互斥。发送前核对接收对象、消息类型和资源来源。
## 资源下载
公开 `+messages-resource-download` 使用工作目录内安全相对路径,默认不覆盖;完整文件先写入
临时落盘再原子发布。覆盖必须由用户显式传 `--overwrite`,读取和下载不需要 `--yes`。
任务要求从某条消息中定位并下载资源时,优先在限定会话、消息或时间范围的查询中加
`--download-resources --output-dir <目录>`,并检查下载 ledger。`+messages-resource-download`
只用于已经持有完整、真实且属于当前组织/profile 的独立资源引用、无需再定位消息的场景。
若 `fileId` 返回 `RESOURCE_NOT_FOUND`,不得把同一个 ID 改称 `mediaId` 重试,也不得原样
重复调用;应回到消息查询并使用 `--download-resources`。
底层 fallback:
```bash
dws chat message download-media --type mediaId --resource-id <mediaId> \
--message-id <openMessageId> --open-conversation-id <openConversationId> \
--output ./downloads/
```
`resource-id`、`message-id` 和会话 ID 必须来自同一 profile 下的真实消息查询结果。
当前没有 Range/断点续传;失败时保留 ledger 或错误,显式重试整个文件,不拼接残片。
## 完成与错误
- 查询并下载时同时检查消息完整性和每项下载 ledger;单项失败不抹掉已取得消息。
- 文件/音视频发送失败先确认工作目录内相对路径可读,不恢复独立上传再提取 mediaId 的旧默认链路。
- 位置参数不完整时先向用户确认,不猜经纬度或缩略图。
- 名片发送失败时确认 `--contact-id` 是 openDingTalkId。
- 下载目标存在时默认停止;只有用户明确允许覆盖时才传 `--overwrite`。
@@ -0,0 +1,132 @@
# message-query:消息读取、搜索与查询
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
用于浏览或导出指定会话、按条件搜索消息、按消息 ID 读取详情、查看 @我、话题回复、
Favorite、Pin 和 reaction。只读任务优先使用 Shortcut;只有 Shortcut 未发布所需底层字段、
原始响应或手工 continuation 时才读取精确原子 leaf Schema。
## 入口选择
| 用户终点 | 唯一推荐入口 |
|---|---|
| <!-- dws-intent: chat.read.conversation -->浏览或导出一个指定群聊/单聊 | `dws chat +chat-messages` |
| <!-- dws-intent: chat.search.filtered -->发送者、关键词、@对象或消息类型是主要条件 | `dws chat +search-msg` |
| 已知消息 IDs 读取详情 | `dws chat +messages-mget` |
| 查看 @我的消息 | `dws chat +at-me` |
| 查看 Favorite | `dws chat +flag-list` |
| 已知话题主消息或 thread/topic ID 读取回复 | `dws chat +thread-replies` |
`+chat-messages` 是指定会话的粗粒度读取;`+search-msg` 是目标条件明确的单/跨会话检索。
不要先读完整会话再补跑搜索,也不要把群名或姓名直接填入只接受稳定 ID 的参数。
## 指定会话读取
群聊 `--group` 可传群名或 `openConversationId`;也可用 `--chat-query` 显式解析群名、
用 `--conversation-id` 显式传稳定 ID。单聊使用 `--user` 或 `--open-dingtalk-id`。
```bash
dws chat +chat-messages --group <群名或openConversationId> --format json
dws chat +chat-messages --group <openConversationId> --page-all --page-limit 50 --format json
```
可附带非必填的 `--sender-query <姓名>`:未传时返回全部消息;未解析出稳定 ID 时保留
全部消息并记录失败;唯一解析出 userId/openDingTalkId 后,按消息 `senderId` 筛选同一次
读取结果,覆盖最终 `messages/count` 并返回 `resolvedFilters`。不得用展示名字符串比较否定
已经成功解析的稳定发送者身份。
```bash
dws chat +chat-messages --group "项目群" --sender-query "测试用户甲" --page-all --format json
```
时间范围使用公开可选的 `--start`、`--end`、`--order asc|desc`,兼容别名为
`--start-time/--end-time/--sort`。范围为 `[start,end)`;仅开始时间表示到本次执行当前时间;
仅结束时间只支持 `desc`,`asc` 必须提供开始时间。旧 `--time/--direction` 只用于兼容的
单边界模式,不能与范围模式混用。
```bash
dws chat +chat-messages --group <openConversationId> \
--start "2026-08-01T00:00:00+08:00" --end "2026-08-02T00:00:00+08:00" \
--order asc --page-all --format json
```
完整读取后只需消息字段可判断的子集时,在同一次调用中使用全局 `--jq`,保留根信封并
同步改写 `messages/count`;不得丢失 `complete`、`hasMore`、`failures` 等 ledger。
发送者姓名仍使用 `--sender-query` 解析稳定身份,不用 `--jq` 比较展示名。
```bash
dws chat +chat-messages --group "项目群" --page-all --format json \
--jq '. as $root | [.messages[] | select((.reactions // []) | length > 0)] as $matched | $root | .messages = $matched | .count = ($matched | length)'
```
要求导出时用 `--output <工作目录内相对.json>` 原子写入;需要资源时在读取命令上加
`--download-resources`,不要让 Agent 先输出全量 JSON 再手工遍历资源引用。
## 多维度搜索
- 关键词使用公开 `--query`。
- 已知稳定会话 ID 使用 `--group` / `--groups`;稳定发送者 ID 使用 `--senders`。
- 只有群名时使用 `--chat-query`,由 CLI 唯一解析会话。
- 只有发送者姓名时使用 `--sender-query`,由 CLI 唯一解析人员。
- 不传会话过滤时搜索全部会话;默认时间范围为最近 7 天。
- `--page-all` 只翻完当前时间范围内的游标页;精确范围使用成对的 `--start/--end`。
- `--order` 只稳定排列已经取得的结果;未全量或 `complete=false` 时不得称为完整范围全局排序。
```bash
dws chat +search-msg --chat-query "项目群" --sender-query "测试用户甲" --page-all --format json
dws chat +search-msg --chat-query "项目群" --query "发布计划" --page-all --format json
dws chat +search-msg --sender-query "测试用户甲" --page-all --format json
```
需要 Shortcut 未发布的原始过滤字段或响应时,才评估 `message search-advanced`。它支持
发送者、@对象、多个会话、消息类型、会话类型、机器人消息和时间范围,但不是默认入口。
至少提供一种真实过滤条件,完整遍历只有 `--page-all` 会触发。
## 其他查询
### 已知消息、@我与话题回复
- `+messages-mget --msg-ids <id...>`:最多 50 条;结果可直接用于回复、转发、撤回和资源下载。
- `+at-me [--group <群名或ID>] --page-all`:群内或跨全部会话查看 @我的消息。
- `+thread-replies --message-id <rootMessageId>`:自动只读解析 conversation/thread。
- `+thread-replies --group <cid> --thread-id <threadId>`:显式稳定上下文。
话题回复默认 `desc`;`asc` 必须与 `--page-all` 一起使用。自动续页使用下层毫秒级
`nextCursor`,不得使用只有秒精度的展示时间手工拼 continuation。检查 `complete`、
`hasMore`、`stopReason` 和 `failures`。
### Favorite、Pin 与 reaction 查询
| 任务 | 入口 |
|---|---|
| Favorite 列表 | `+flag-list`;要求全部时加 `--page-all`,页大小 1–30 |
| 消息 Pin 列表 | `message list-pin-msg --open-conversation-id <cid>` |
| 批量 reaction/文字回应 | `message list-emotion-replies --msg-ids <id...>` |
| 已读/未读状态 | `message read-status --group <cid> --message-id <id>` |
Favorite、消息 Pin、消息 Top 和会话 Top 是不同对象。写入或取消这些状态读取
[message-actions.md](message-actions.md),这里只负责查询。
## 原子 fallback
| 原子命令 | 仅用于 |
|---|---|
| `message list` | 指定会话原始响应或显式手工 continuation |
| `message list-all` | 时间范围内全部会话的原始分页响应 |
| `message list-by-sender` | 已有稳定发送者 ID 且需要底层原始响应 |
| `message list-mentions` / `list-focused` | @我或特别关注的原始列表 |
| `message search` / `search-advanced` | Shortcut 未发布的真实过滤字段 |
| `message list-topic-replies` | 已知 conversation/thread 的原始话题回复 |
| `message list-by-ids` | 已知消息 ID 的原始详情响应 |
Typed `chat message` 自动翻页只由 `--page-all` 触发;只传 `--page-limit`、`--max-items`
或 `--page-delay` 仍是单页。非第一页失败时保留 partial 结果、失败页和 continuation,不能
把 partial result 表述成完整成功。
## 完成与错误
- 查询必须检查 `complete`、`hasMore`、`stopReason`、`failures` 和下载 ledger。
- 发送者/群名零命中或多候选时停止,不选择第一项。
- `unknown flag` 时读取精确 leaf Help,修正后最多重试一次。
- 子消息优先使用自己的 `messageId`;只在缺会话 ID 时继承父消息的 `conversationId`。
- 查到真实消息后需要写操作时,使用 [message-actions.md](message-actions.md) 中的稳定 ID 规则。
@@ -22,12 +22,12 @@
| 用户终点 | 对象 | Reference |
|---|---|---|
| 收藏或取消收藏 | 当前用户的 Favorite | [chat-message.md](chat/chat-message.md) |
| Pin/Unpin 一条消息 | 消息 Pin | [chat-message.md](chat/chat-message.md) |
| 置顶/取消置顶一条消息 | 消息 Top | [chat-message.md](chat/chat-message.md) |
| 收藏或取消收藏 | 当前用户的 Favorite | [message-actions.md](chat/message-actions.md) |
| Pin/Unpin 一条消息 | 消息 Pin | [message-actions.md](chat/message-actions.md) |
| 置顶/取消置顶一条消息 | 消息 Top | [message-actions.md](chat/message-actions.md) |
| 置顶/取消置顶整个会话 | 会话 Top | [chat-conversation.md](chat/chat-conversation.md) |
| 查看置顶会话 | 会话列表 | `+conversation-list-top` |
| 标记消息已读 | 消息读取状态 | [chat-message.md](chat/chat-message.md) |
| 标记消息已读 | 消息读取状态 | [message-actions.md](chat/message-actions.md) |
| 清红点、标记会话未读 | 会话状态 | [chat-conversation.md](chat/chat-conversation.md) |
Favorite、消息 Pin、消息 Top 和会话 Top 不能互换,即使用户都说“收藏/钉住/置顶”。
@@ -37,7 +37,8 @@ Favorite、消息 Pin、消息 Top 和会话 Top 不能互换,即使用户都
| 用户终点 | 选择 |
|---|---|
| 已有成员 IDs 创建群 | `+chat-create` |
| 加人、踢人、管理员、群公告、群设置 | [chat-group.md](chat/chat-group.md) |
| 查群、查看成员、邀请链接 | [group-discovery.md](chat/group-discovery.md) |
| 加人、踢人、管理员、群公告、群设置 | [group-admin.md](chat/group-admin.md) |
| 找可用机器人并取得单聊 ID | `chat bot find`,不是只查自己创建机器人的 `bot search` |
| 已知 robotCode 发送 | `+messages-send --as bot` |
| 机器人入群、移除、批量群发或撤回 | [chat-bot.md](chat/chat-bot.md) |