Merge branch 'main' into feat/robot-message-image-file

This commit is contained in:
Anonymity-0
2026-08-06 09:54:07 +08:00
committed by GitHub
5 changed files with 229 additions and 28 deletions
+1
View File
@@ -73,6 +73,7 @@ and compatibility and CI reliability fixes.
### Changed
- **Chat reply mentions** — `dws chat message reply` can @ specified group members with `--at-open-dingtalk-ids` or @ everyone with `--at-all`, forwarding the existing `send_personal_message` mention fields and automatically adding missing current-user `<@id>` / `<@all>` placeholders.
- **Pinned MCP metadata retired** — deletes `internal/cli/schema_mcp_metadata.json` and removes its embed/loader/fallback role from Schema assembly. Catalog now assembles from Contract/ParamDecl/Interface + Cobra only; `make fetch-mcp-metadata` remains an optional diagnostic dump under `artifacts/` and refuses the retired pin path. Policy bans the pin from reappearing.
- **MCP service review retired** — deletes `schema_mcp_service_review.json` and removes its policy jq / outputguard / test disposition gate (`notify` → `out_of_surface`, snapshot hash pin). No replacement ledger.
- **Hints retired; ContractDecl is the leaf Schema source** (#830) — `schema_hints/`, Manual/Schema hint overlays, and `schema_agent_metadata/` delivery are removed. Selection, safety, parameters, and interface facts declare on ProductDecl / leaf `Contract` (`corecmd.ContractDecl` + `contract.ParamDecl` / `Safety`). Authoring renamed `SchemaDecl` → `ContractDecl`; nested fields reuse `contract.*` directly.
+71 -27
View File
@@ -404,6 +404,50 @@ func NormalizeMessageMentions(text string, ids []string, atAll, wrapAngle bool)
return text
}
// applyCurrentUserGroupMentions keeps the body placeholders and
// send_personal_message mention arguments aligned for send and reply.
func applyCurrentUserGroupMentions(params map[string]any, text, rawOpenIDs string, atAll bool) string {
var atOpenIDs []string
if rawOpenIDs != "" {
atOpenIDs = strings.Split(rawOpenIDs, ",")
}
if atAll && !strings.Contains(text, "<@all>") {
text = "<@all> " + text
}
text = normalizeAtPlaceholders(text, atOpenIDs, true)
if atAll {
params["atAll"] = true
}
if len(atOpenIDs) > 0 {
params["atOpenDingTalkIds"] = atOpenIDs
}
return text
}
func addMissingCurrentUserMentionPlaceholders(text, rawOpenIDs string) string {
if rawOpenIDs == "" {
return text
}
missing := make([]string, 0)
probeText := text
for _, id := range parseCSVValues(rawOpenIDs) {
placeholder := "<@" + id + ">"
if strings.Contains(probeText, placeholder) {
continue
}
missing = append(missing, placeholder)
probeText += placeholder
}
if len(missing) == 0 {
return text
}
prefix := strings.Join(missing, " ")
if strings.HasPrefix(text, "<@all> ") {
return "<@all> " + prefix + " " + strings.TrimPrefix(text, "<@all> ")
}
return prefix + " " + text
}
func containsMessageMention(text, placeholder string) bool {
if strings.HasPrefix(placeholder, "<") {
return strings.Contains(text, placeholder)
@@ -2094,29 +2138,15 @@ func newChatCommand() *cobra.Command {
if groupID != "" {
atAll, _ := cmd.Flags().GetBool("at-all")
atOpenIdsStr, _ := cmd.Flags().GetString("at-open-dingtalk-ids")
var atOpenIds []string
if atOpenIdsStr != "" {
atOpenIds = strings.Split(atOpenIdsStr, ",")
}
if atAll && !strings.Contains(text, "<@all>") {
text = "<@all> " + text
}
// 用户身份发消息要求 @ 占位符为 <@openDingTalkId>;模型若写成裸 @id 自动补全,已有 <@id> 不变
text = normalizeAtPlaceholders(text, atOpenIds, true)
// 群聊统一走 openDingTalkId @ 人接口。
contentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
newParams := map[string]any{
"openConversationId": groupID,
"msgType": "markdown",
"content": string(contentJSON),
"clawType": clawType,
}
if atAll {
newParams["atAll"] = true
}
if len(atOpenIds) > 0 {
newParams["atOpenDingTalkIds"] = atOpenIds
}
text = applyCurrentUserGroupMentions(newParams, text, atOpenIdsStr, atAll)
contentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
newParams["content"] = string(contentJSON)
if msgUuid != "" {
newParams["uuid"] = msgUuid
}
@@ -5508,13 +5538,14 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
chatMessageReplyCmd := &cobra.Command{
Use: "reply",
Short: "引用回复消息(支持单聊/群聊)",
Long: `以当前用户身份引用某条消息并回复。需要指定会话 ID、被引用消息 ID、原消息发送者 openDingTalkId,以及回复内容。
Long: `以当前用户身份引用某条消息并回复。需要指定会话 ID、被引用消息 ID、原消息发送者 openDingTalkId,以及回复内容。群聊回复可通过 --at-open-dingtalk-ids @指定成员,或通过 --at-all @所有人;正文中的裸 @openDingTalkId 会自动规范化为 <@openDingTalkId>,缺少对应成员或 <@all> 占位符时会自动补齐。
如何获取 openConversationId(如果上层已有则直接使用,不必再查):
- 群聊:dws chat search --query "群名"
- 单聊:dws chat conversation-info --open-dingtalk-id <openDingTalkId>
(人员信息可通过 dws contact user search --keyword "姓名" --format json 获取)`,
Example: ` dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text "收到,马上处理"`,
Example: ` dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text "收到,马上处理"
dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text "请看一下" --at-open-dingtalk-ids <mentionedOpenDingTalkId>`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "conversation-id", "ref-msg-id", "ref-sender", "text"); err != nil {
return err
@@ -5527,13 +5558,6 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
}
refSender = resolved
}
replyContent := map[string]string{
"referenceOpenMessageId": mustGetFlag(cmd, "ref-msg-id"),
"srcMsgSendOpenDingTalkId": refSender,
"replyMsgType": "text",
"content": mustGetFlag(cmd, "text"),
}
contentJSON, _ := marshalJSONRaw(replyContent)
clawType := ""
aiTag, _ := cmd.Flags().GetBool("ai-tag")
if aiTag {
@@ -5542,9 +5566,25 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "conversation-id"),
"msgType": "reply",
"content": string(contentJSON),
"clawType": clawType,
}
atAll, _ := cmd.Flags().GetBool("at-all")
atOpenIDs := mustGetFlag(cmd, "at-open-dingtalk-ids")
replyText := applyCurrentUserGroupMentions(
toolArgs,
mustGetFlag(cmd, "text"),
atOpenIDs,
atAll,
)
replyText = addMissingCurrentUserMentionPlaceholders(replyText, atOpenIDs)
replyContent := map[string]string{
"referenceOpenMessageId": mustGetFlag(cmd, "ref-msg-id"),
"srcMsgSendOpenDingTalkId": refSender,
"replyMsgType": "text",
"content": replyText,
}
contentJSON, _ := marshalJSONRaw(replyContent)
toolArgs["content"] = string(contentJSON)
if v, _ := cmd.Flags().GetString("uuid"); v != "" {
toolArgs["uuid"] = v
}
@@ -5578,6 +5618,8 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
},
Parameters: []contract.ParamDecl{
{Name: "ai-tag", Property: "clawType", InterfaceType: "string"},
{Name: "at-all", Property: "atAll", Required: boolPtr(false), InterfaceType: "boolean"},
{Name: "at-open-dingtalk-ids", Property: "atOpenDingTalkIds", Required: boolPtr(false), InterfaceType: "array"},
{Name: "conversation-id", Property: "openConversationId"},
},
},
@@ -5592,6 +5634,8 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
_ = chatMessageReplyCmd.MarkFlagRequired("text")
chatMessageReplyCmd.Flags().String("uuid", "", "幂等键(可选)")
chatMessageReplyCmd.Flags().Bool("ai-tag", true, "消息是否带 AI 发送角标(默认 true)")
chatMessageReplyCmd.Flags().Bool("at-all", false, "@所有人(仅群聊时生效;正文缺少 <@all> 时自动补齐)")
chatMessageReplyCmd.Flags().String("at-open-dingtalk-ids", "", "@指定成员的 openDingTalkId 列表,逗号分隔(仅群聊时生效;正文缺少对应 <@id> 时自动补齐,裸 @id 自动规范化)")
cli.AttachRuntimeSchema(chatMessageReplyCmd, "chat", "reply_personal_message", "hardcoded:chat")
// ── message forward: 转发单条消息 ────────────────────────
@@ -15,6 +15,7 @@ package helpers
import (
"context"
"encoding/json"
"io"
"os"
"reflect"
@@ -285,6 +286,150 @@ func TestChatSendAndReplyDisableAITagWithEmptyClawType(t *testing.T) {
}
}
func TestCrossPlatformCoverageChatCurrentUserSendAndReplyMentions(t *testing.T) {
tests := []struct {
name string
args []string
contentField string
wantContent string
wantAtAll bool
wantOpenIDs []string
}{
{
name: "send",
args: []string{
"message", "send", "--group", "cid",
"--text", "收到 @D-target 和 <@D-second>",
"--at-open-dingtalk-ids", "D-target,D-second",
"--at-all",
},
contentField: "text",
wantContent: "<@all> 收到 <@D-target> 和 <@D-second>",
wantAtAll: true,
wantOpenIDs: []string{"D-target", "D-second"},
},
{
name: "send keeps missing member placeholders unchanged",
args: []string{
"message", "send", "--group", "cid",
"--text", "DWS 发消息自测",
"--at-open-dingtalk-ids", "D-target",
},
contentField: "text",
wantContent: "DWS 发消息自测",
wantOpenIDs: []string{"D-target"},
},
{
name: "reply",
args: []string{
"message", "reply",
"--conversation-id", "cid",
"--ref-msg-id", "mid",
"--ref-sender", "D-sender",
"--text", "收到 @D-target 和 <@D-second>",
"--at-open-dingtalk-ids", "D-target,D-second",
"--at-all",
},
contentField: "content",
wantContent: "<@all> 收到 <@D-target> 和 <@D-second>",
wantAtAll: true,
wantOpenIDs: []string{"D-target", "D-second"},
},
{
name: "reply adds missing member placeholders",
args: []string{
"message", "reply",
"--conversation-id", "cid",
"--ref-msg-id", "mid",
"--ref-sender", "D-sender",
"--text", "DWS 回复艾特前津(非主用)自测",
"--at-open-dingtalk-ids", "D-target,D-second,D-target",
},
contentField: "content",
wantContent: "<@D-target> <@D-second> DWS 回复艾特前津(非主用)自测",
wantOpenIDs: []string{"D-target", "D-second", "D-target"},
},
{
name: "reply adds missing member placeholders after at-all",
args: []string{
"message", "reply",
"--conversation-id", "cid",
"--ref-msg-id", "mid",
"--ref-sender", "D-sender",
"--text", "请大家确认",
"--at-open-dingtalk-ids", "D-target",
"--at-all",
},
contentField: "content",
wantContent: "<@all> <@D-target> 请大家确认",
wantAtAll: true,
wantOpenIDs: []string{"D-target"},
},
{
name: "reply at-all preserves alliance word",
args: []string{
"message", "reply",
"--conversation-id", "cid",
"--ref-msg-id", "mid",
"--ref-sender", "D-sender",
"--text", "联系 @alliance",
"--at-all",
},
contentField: "content",
wantContent: "<@all> 联系 @alliance",
wantAtAll: true,
},
{
name: "reply without at flags preserves alliance word",
args: []string{
"message", "reply",
"--conversation-id", "cid",
"--ref-msg-id", "mid",
"--ref-sender", "D-sender",
"--text", "联系 @alliance",
},
contentField: "content",
wantContent: "联系 @alliance",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
caller := &chatChangedContractCaller{}
if err := executeChatChangedContract(t, caller, tc.args...); err != nil {
t.Fatal(err)
}
if len(caller.calls) != 1 || caller.calls[0].toolName != "send_personal_message" {
t.Fatalf("calls = %#v", caller.calls)
}
args := caller.calls[0].args
gotAtAll, hasAtAll := args["atAll"]
if tc.wantAtAll {
if !hasAtAll || gotAtAll != true {
t.Fatalf("atAll = %#v, present = %v; want true", gotAtAll, hasAtAll)
}
} else if hasAtAll {
t.Fatalf("atAll = %#v; want absent", gotAtAll)
}
gotOpenIDs, hasOpenIDs := args["atOpenDingTalkIds"]
if len(tc.wantOpenIDs) > 0 {
if !hasOpenIDs || !reflect.DeepEqual(gotOpenIDs, tc.wantOpenIDs) {
t.Fatalf("atOpenDingTalkIds = %#v, present = %v; want %#v", gotOpenIDs, hasOpenIDs, tc.wantOpenIDs)
}
} else if hasOpenIDs {
t.Fatalf("atOpenDingTalkIds = %#v; want absent", gotOpenIDs)
}
var content map[string]string
if err := json.Unmarshal([]byte(args["content"].(string)), &content); err != nil {
t.Fatal(err)
}
if got := content[tc.contentField]; got != tc.wantContent {
t.Fatalf("content[%q] = %q; want %q", tc.contentField, got, tc.wantContent)
}
})
}
}
func TestCrossPlatformCoverageChatSendFailsClosedWhenUserCannotResolve(t *testing.T) {
caller := &chatChangedContractCaller{}
err := executeChatChangedContract(t, caller, "message", "send", "--user", "123", "--text", "hello")
+4
View File
@@ -1251,8 +1251,11 @@ Usage:
dws chat message reply [flags]
Example:
dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text "收到,马上处理"
dws chat message reply --conversation-id <openConversationId> --ref-msg-id <openMessageId> --ref-sender <openDingTalkId> --text "请看一下" --at-open-dingtalk-ids <mentionedOpenDingTalkId>
# 被引用消息的 openMessageId、发送者 openDingTalkId 通过 dws chat message list 获取
Flags:
--at-all @所有人(仅群聊时生效;正文缺少 <@all> 时自动补齐)
--at-open-dingtalk-ids string @指定成员的 openDingTalkId 列表,逗号分隔(仅群聊时生效;正文缺少对应 <@id> 时自动补齐)
--conversation-id string 会话 openConversationId (必填,支持单聊/群聊)
--ref-msg-id string 被引用的消息 openMessageId (必填)
--ref-sender string 被引用消息的发送者 openDingTalkId (必填)
@@ -1262,6 +1265,7 @@ Flags:
注意:
- 以当前用户身份引用回复,语义同 chat message send;目前回复类型仅支持 text
- 群聊 @指定成员时,正文缺少对应 <@openDingTalkId> 会自动补齐,已有裸 @openDingTalkId 会规范化;--at-all 会自动补齐 <@all>
```
#### 转发单条消息 — 将一条消息从源会话转发到目标会话(源/目标均支持单聊/群聊)
@@ -180,11 +180,18 @@ dws chat message edit --group <openConversationId> --msg-id <openMessageId> --co
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `message reply` | 引用回复,单聊/群聊均可 | `--conversation-id` `--ref-msg-id` `--ref-sender` `--text` |
| `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
```
### 话题与卡片
话题完整读取流程: