Compare commits

..
3 Commits
Author SHA1 Message Date
修雨 91f44a1efd docs: cut 1.0.39 changelog (#475 wukong-leak fix + #477 ai-tag opt-in) (#478) 2026-06-18 14:10:07 +08:00
修雨 1a7ba01e36 feat(chat): make AI-sent badge opt-in via --ai-tag (default off) (#477)
Previously every user-identity send/reply attached clawType=edition.ClawType()
unconditionally, so the IM server rendered an AI-sent badge ("通过AI发送" on the
open edition) under every message — surprising users by branding all their sends.

Make it opt-in: by default no clawType is attached (no badge). Passing --ai-tag
on 'chat message send' / 'chat message reply' attaches edition.ClawType() so the
badge shows (open=openClaw -> 通过AI发送, wukong overlay -> 悟空AI发送). Bot and
webhook sends remain untouched.

Follow-up to #475 (which fixed the hardcoded wukong leak, #474).
2026-06-18 12:22:19 +08:00
修雨 6310dcc39e feat(chat): tag user-identity message sends with edition clawType (#475)
Attach the clawType tool argument to every user-identity send path
(send_personal_message text/rich-media/reply and
send_direct_message_as_user) so the IM server can render the
"Send from AI" indicator on delivered messages. The value comes from a
new edition hook (Hooks.ClawTypeValue, exposed via edition.ClawType())
that falls back to DefaultOSSClawType ("openClaw"); overlays such as
wukong set their own identity to get their branded indicator.

Also fixes the reply command, which hardcoded clawType="wukong" and
made open-source replies carry the Wukong AI identity.

Bot sends intentionally stay untouched: they already render as bot
messages and must not carry the user-identity claw tag.
2026-06-18 10:53:52 +08:00
5 changed files with 184 additions and 1 deletions
+12
View File
@@ -6,6 +6,18 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
## [Unreleased]
## [1.0.39] - 2026-06-18
This release makes the AI-sent indicator opt-in. 1.0.38 unconditionally tagged every user-identity send/reply with the edition claw identity, so the IM server rendered a "Send from AI" badge under every message — and on the open edition a stale hardcoded value even leaked the Wukong-branded label (「悟空AI发送」) to external users. The badge is now off by default and shown only when the caller explicitly asks for it.
### Added
- **`--ai-tag` opt-in flag for `chat message send` / `chat message reply`** (#477; `internal/helpers/chat.go`) — by default no `clawType` tool argument is attached, so delivered messages carry no "Send from AI" badge. Passing `--ai-tag` attaches `edition.ClawType()` so the IM server renders the badge (open edition `openClaw` → 「通过AI发送」; the wukong overlay sets its own value → 「悟空AI发送」). Covers the text/Markdown, rich-media, and `--user`/`--open-dingtalk-id` direct send paths plus `reply`. Bot (`send-by-bot`) and webhook sends are intentionally untouched — they already render as bot messages. The badge is opt-in so dws does not brand every message a user sends.
### Fixed
- **`dws chat message reply` no longer leaks the Wukong AI label on the open edition** (#475, fixes #474; `internal/helpers/chat.go`, `pkg/edition/edition.go`) — the reply path hardcoded `clawType: "wukong"`, so open-source quoted replies were tagged 「悟空AI发送」 by the IM server, leaking Wukong branding to external users (reported by an external customer integrating via openclaw). The value now derives from the edition via the new `edition.ClawType()` accessor (open → `DefaultOSSClawType` = `openClaw`), and — together with #477 — is only attached when `--ai-tag` is passed. The earlier fix existed on a branch (PR #450) but was never merged to main; #475 cherry-picked it.
## [1.0.38] - 2026-06-16
This release adds client-side agent attribution for usage stats, fixes two commands that silently misbehaved (`dws sheet export` hanging, `dws upgrade --dry-run` actually upgrading), hardens the document write path against server-rejected characters, and makes the long-broken `--no-browser` login flag actually work.
+19 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
@@ -333,9 +334,19 @@ func newChatMessageSendCommand(runner executor.Runner) *cobra.Command {
cmd.Flags().String("file-type", "", "文件类型/扩展名 (msg-type=file)")
cmd.Flags().String("file-path", "", "文件展示路径 (msg-type=file)")
cmd.Flags().Int64("file-size", 0, "文件大小,单位字节 (msg-type=file)")
cmd.Flags().Bool("ai-tag", false, "标记为「通过AI发送」(默认不带;仅传 --ai-tag 时才在消息下方显示 AI 发送角标)")
return cmd
}
// attachAITag 仅在用户显式传入 --ai-tag 时,给发送参数加上 clawType,
// 由 IM 服务端据此渲染「通过AI发送」角标 (悟空版渲染「悟空AI发送」)。
// 默认不带:是否标记 AI 发送交由用户自行选择,不强加。
func attachAITag(cmd *cobra.Command, params map[string]any) {
if on, _ := cmd.Flags().GetBool("ai-tag"); on {
params["clawType"] = edition.ClawType()
}
}
// deriveTitleFromText 在未显式指定 --title 时,从正文截取一个标题
// (首行、最多 20 个字符),与 wukong 行为对齐 (send_personal_message 的
// content 内携带 title)。
@@ -442,6 +453,7 @@ func buildChatMessageSendInvocation(cmd *cobra.Command, args []string) (map[stri
return nil, "", apperrors.NewValidation("unsupported --msg-type: " + msgType + " (supported: image, file)")
}
params := map[string]any{"msgType": msgType, "content": contentJSON}
attachAITag(cmd, params)
if strings.TrimSpace(uuid) != "" {
params["uuid"] = uuid
}
@@ -481,6 +493,7 @@ func buildChatMessageSendInvocation(cmd *cobra.Command, args []string) (map[stri
"msgType": "markdown",
"content": marshalMessageContent(title, text),
}
attachAITag(cmd, params)
if atAll {
params["atAll"] = true
}
@@ -493,6 +506,7 @@ func buildChatMessageSendInvocation(cmd *cobra.Command, args []string) (map[stri
return params, "send_personal_message", nil
case hasUser:
params := map[string]any{"title": title, "text": text, "receiverUserId": user}
attachAITag(cmd, params)
return params, "send_direct_message_as_user", nil
default:
params := map[string]any{
@@ -500,6 +514,7 @@ func buildChatMessageSendInvocation(cmd *cobra.Command, args []string) (map[stri
"msgType": "markdown",
"content": marshalMessageContent(title, text),
}
attachAITag(cmd, params)
if strings.TrimSpace(uuid) != "" {
params["uuid"] = uuid
}
@@ -975,8 +990,10 @@ func newChatMessageReplyCommand(runner executor.Runner) *cobra.Command {
"openConversationId": convID,
"msgType": "reply",
"content": contentJSON,
"clawType": "wukong",
}
// clawType 仅在 --ai-tag 时携带;默认不带,回复不强加 AI 角标。
// edition 决定取值 (开源 openClaw / 悟空 wukong)。
attachAITag(cmd, params)
if uuid, _ := cmd.Flags().GetString("uuid"); strings.TrimSpace(uuid) != "" {
params["uuid"] = uuid
}
@@ -1000,6 +1017,7 @@ func newChatMessageReplyCommand(runner executor.Runner) *cobra.Command {
cmd.Flags().String("ref-sender", "", "被引用消息发送者 openDingTalkId (必填)")
cmd.Flags().String("text", "", "回复正文 (必填)")
cmd.Flags().String("uuid", "", "可选 uuid(幂等标识)")
cmd.Flags().Bool("ai-tag", false, "标记为「通过AI发送」(默认不带;仅传 --ai-tag 时才显示 AI 发送角标)")
return cmd
}
+100
View File
@@ -7,6 +7,8 @@ import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
type captureRunner struct {
@@ -308,6 +310,104 @@ func TestChatMessageSendRejectsAtMentionsOutsideGroup(t *testing.T) {
}
}
// TestChatMessageAITagControlsClawType guards the opt-in "Send from AI" indicator:
// by default NO user-identity send carries the clawType tool argument (so the IM
// server renders no AI badge). Only when --ai-tag is passed does each path attach
// the edition claw identity (open-source build pins it to edition.DefaultOSSClawType,
// "openClaw"); the wukong overlay would attach its own value. The label is opt-in so
// dws does not surprise users by branding every message they send.
func TestChatMessageAITagControlsClawType(t *testing.T) {
cases := []struct {
name string
make func(runner executor.Runner) *cobra.Command
args []string
}{
{
name: "group-markdown",
make: newChatMessageSendCommand,
args: []string{"--group", "cid-xyz", "--title", "t", "--text", "hello"},
},
{
name: "user-direct",
make: newChatMessageSendCommand,
args: []string{"--user", "034766", "--title", "t", "--text", "hi"},
},
{
name: "open-dingtalk-id-direct",
make: newChatMessageSendCommand,
args: []string{"--open-dingtalk-id", "OP123", "--title", "t", "--text", "hi"},
},
{
name: "group-rich-media-image",
make: newChatMessageSendCommand,
args: []string{"--group", "cid-xyz", "--msg-type", "image", "--media-id", "media-1"},
},
{
name: "reply",
make: newChatMessageReplyCommand,
args: []string{
"--conversation-id", "cid-xyz",
"--ref-msg-id", "msg-1",
"--ref-sender", "op-1",
"--text", "got it",
},
},
}
for _, tc := range cases {
// Default: no --ai-tag → must omit clawType entirely (no badge).
t.Run(tc.name+"/default-no-tag", func(t *testing.T) {
runner := &captureRunner{}
cmd := tc.make(runner)
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SetArgs(tc.args)
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String())
}
if v, ok := runner.last.Params["clawType"]; ok {
t.Fatalf("default send must omit clawType, got %#v", v)
}
})
// Opt-in: --ai-tag → attach the edition claw identity.
t.Run(tc.name+"/with-ai-tag", func(t *testing.T) {
runner := &captureRunner{}
cmd := tc.make(runner)
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SetArgs(append(append([]string{}, tc.args...), "--ai-tag"))
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String())
}
got, ok := runner.last.Params["clawType"]
if !ok {
t.Fatalf("--ai-tag send missing clawType; got %#v", runner.last.Params)
}
if got != edition.DefaultOSSClawType {
t.Fatalf("clawType = %#v, want %q", got, edition.DefaultOSSClawType)
}
})
}
}
// Robot sends are rendered as bot messages already; they must NOT carry the
// user-identity clawType argument.
func TestChatMessageSendByBotOmitsClawType(t *testing.T) {
runner := &captureRunner{}
cmd := newChatMessageSendByBotCommand(runner)
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SetArgs([]string{"--group", "cid-xyz", "--robot-code", "robot-001", "--title", "t", "--text", "x"})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String())
}
if _, ok := runner.last.Params["clawType"]; ok {
t.Fatalf("bot send must not carry clawType; got %#v", runner.last.Params)
}
}
func equalAny(a, b any) bool {
switch av := a.(type) {
case []any:
+17
View File
@@ -68,6 +68,12 @@ type Hooks struct {
Name string // "open" (default) / overlay identifier
ScenarioCode string // injected into x-dingtalk-scenario-code header
// ClawTypeValue is the claw identity carried in message-send tool
// arguments (parameter clawType) so the IM server can render the
// "Send from AI" indicator on delivered messages. Empty → falls back
// to DefaultOSSClawType; overlays set their own value (e.g. "wukong").
ClawTypeValue string
// --- runtime mode ---
IsEmbedded bool // true when running inside a host application
HideAuthLogin bool // true suppresses the "dws auth login" command
@@ -165,3 +171,14 @@ func Override(h *Hooks) {
defer mu.Unlock()
current = h
}
// ClawType returns the claw identity for the active edition, falling back
// to DefaultOSSClawType when the overlay does not set one. Message-send
// helpers attach this value as the clawType tool argument so the IM server
// can label delivered messages as sent via AI.
func ClawType() string {
if v := Get().ClawTypeValue; v != "" {
return v
}
return DefaultOSSClawType
}
+36
View File
@@ -0,0 +1,36 @@
// 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 edition
import "testing"
func TestClawTypeDefaultsToOSSValue(t *testing.T) {
prev := Get()
defer Override(prev)
Override(defaultHooks())
if got := ClawType(); got != DefaultOSSClawType {
t.Fatalf("ClawType() = %q, want %q", got, DefaultOSSClawType)
}
}
func TestClawTypeUsesOverlayValue(t *testing.T) {
prev := Get()
defer Override(prev)
Override(&Hooks{Name: "overlay", ClawTypeValue: "wukong"})
if got := ClawType(); got != "wukong" {
t.Fatalf("ClawType() = %q, want overlay value %q", got, "wukong")
}
}