fix(chat): accept card update acknowledgement

This commit is contained in:
前津 committed 2026-08-11 23:44:53 +08:00
1 parent b1bfe6002d
commit eebd6b2a1c
7 files changed
+50 -24

No files matched your search

+4
View File
@@ -28,6 +28,10 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
### Fixed
- **Streaming-card update acknowledgement** — accepts the pre-production
`success: true` response from `update_streaming_card` as affirmative write
evidence while preserving explicit negative, conflicting, and bizId-drift
failures, so Agents do not repeat an update that the service already applied.
- **Machine-readable export and download receipts** — `dws doc export`,
`dws drive download`, and `dws drive download --version` now keep progress
logs on stderr under `--format json` and emit one JSON result on stdout after
+6 -4
View File
@@ -77,7 +77,7 @@ func TestCrossPlatformCoverageNativeMessageUpdateCardVerifiesWrite(t *testing.T)
}
})
t.Run("generic success is unverified", func(t *testing.T) {
t.Run("success acknowledgement is verified", func(t *testing.T) {
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"success":true,"errorCode":null}`}}}
err := runNativeCardUpdate(t, caller,
"message", "update-card",
@@ -85,9 +85,11 @@ func TestCrossPlatformCoverageNativeMessageUpdateCardVerifiesWrite(t *testing.T)
"--content", "完成",
"--flow-status", "3",
)
var typed *apperrors.Error
if !errors.As(err, &typed) || typed.Reason != "streaming_card_update_unverified" {
t.Fatalf("error = %#v, want streaming_card_update_unverified", err)
if err != nil {
t.Fatal(err)
}
if caller.calls != 1 || caller.tool != "update_streaming_card" {
t.Fatalf("call = count:%d tool:%q", caller.calls, caller.tool)
}
})
+25 -14
View File
@@ -1049,14 +1049,6 @@ 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": `{"success":true,"errorCode":null}`,
}},
wantError: "biz-unverified",
},
} {
t.Run(tc.name, func(t *testing.T) {
helpers.InitDeps(tc.fake)
@@ -1074,6 +1066,27 @@ func TestCrossPlatformCoverageMessagesSendCardDryRunAndFailureBoundaries(t *test
})
}
t.Run("success acknowledgement completes composite update", func(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"im/create_and_send_card": `{"bizId":"biz-acknowledged"}`,
"im/update_streaming_card": `{"success":true,"errorCode":null}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-send-card",
"--group", "cid",
"--content", "完成",
"--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 || fake.calls[1].tool != "update_streaming_card" {
t.Fatalf("calls = %#v", fake.calls)
}
})
for _, args := range [][]string{
{"--group", "cid", "--content", "x", "--flow-status", "6"},
{"--group", "cid", "--flow-status", "2"},
@@ -1091,7 +1104,7 @@ func TestCrossPlatformCoverageMessagesSendCardDryRunAndFailureBoundaries(t *test
}
}
func TestCrossPlatformCoverageMessagesUpdateCardRejectsFalseSuccess(t *testing.T) {
func TestCrossPlatformCoverageMessagesUpdateCardVerifiesSuccess(t *testing.T) {
t.Run("agent shortcut owns confirmation boundary", func(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"im/update_streaming_card": `{"result":{"bizId":"biz-confirm","updated":true}}`,
@@ -1115,7 +1128,7 @@ func TestCrossPlatformCoverageMessagesUpdateCardRejectsFalseSuccess(t *testing.T
}
})
t.Run("generic success is unverified", func(t *testing.T) {
t.Run("success acknowledgement is verified", func(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"im/update_streaming_card": `{"success":true,"errorCode":null}`,
}}
@@ -1128,10 +1141,8 @@ func TestCrossPlatformCoverageMessagesUpdateCardRejectsFalseSuccess(t *testing.T
"--flow-status", "3",
"--yes",
})
err := root.Execute()
var typed *apperrors.Error
if !errors.As(err, &typed) || typed.Reason != "streaming_card_update_unverified" {
t.Fatalf("error = %#v, want streaming_card_update_unverified", err)
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0].tool != "update_streaming_card" {
t.Fatalf("calls = %#v", fake.calls)
+9 -3
View File
@@ -59,9 +59,8 @@ func isCardBizIDPlaceholder(value string) bool {
}
// VerifyStreamingCardUpdate requires affirmative evidence that the requested
// write took effect. A transport-level {success:true,errorCode:null} only says
// that the RPC returned normally and is deliberately not accepted as proof of
// a card update.
// 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) {
requestedBizID = strings.TrimSpace(requestedBizID)
observation := cardUpdateObservation{bizIDs: map[string]struct{}{}}
@@ -132,6 +131,13 @@ func observeCardUpdateMap(value map[string]any, observation *cardUpdateObservati
}
}
}
if success, ok := value["success"].(bool); ok {
if success {
setPositiveCardUpdateEvidence(observation, "success=true")
} else {
setNegativeCardUpdateEvidence(observation, "success=false")
}
}
// Only documented response envelopes are traversed. This prevents an
// unrelated extension field containing "updated":true from proving the
@@ -62,7 +62,8 @@ func TestCrossPlatformCoverageVerifyStreamingCardUpdate(t *testing.T) {
{name: "matching id", response: map[string]any{"result": map[string]any{"bizId": "biz-1", "applied": true}}, wantProof: "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: "false success has no write proof", response: map[string]any{"success": true, "errorCode": nil}, wantErrIs: ErrCardUpdateUnverified},
{name: "success acknowledgement", response: map[string]any{"success": true, "errorCode": nil}, wantProof: "success=true"},
{name: "failed acknowledgement", response: map[string]any{"success": false}, wantErrIs: ErrCardUpdateNotApplied},
{name: "explicitly not updated", response: map[string]any{"result": map[string]any{"updated": false}}, wantErrIs: ErrCardUpdateNotApplied},
{name: "mismatched id", response: map[string]any{"result": map[string]any{"bizId": "biz-2", "updated": true}}, wantErrIs: ErrCardUpdateBizIDDrift},
{name: "unrelated extension ignored", response: map[string]any{"extension": map[string]any{"updated": true}}, wantErrIs: ErrCardUpdateUnverified},
+1
View File
@@ -2413,6 +2413,7 @@ Flags:
flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成(FINISH),4=执行中(EXECUTING),5=错误(ERROR)。
**最后一次更新必须将 --flow-status 设为 3(finish),否则卡片会一直处于"生成中"的加载状态。**
更新结果不确定时不要再次执行更新;保留返回结果并告知用户。
```
Usage:
dws chat message update-card [flags]
@@ -13,8 +13,9 @@ Runtime 会唯一解析为 openDingTalkId;已有 openDingTalkId 时传
或艾特占位符。
- `--dry-run` 仍执行只读 userId 解析,只输出两步计划,不执行写入。
创建成功但自动更新失败时,错误会保留真实 `bizId`。不要重复创建;使用该 `bizId` 继续
`+messages-update-card` 或人工处理。
自动更新结果不确定时,不要再次更新或重复创建;保留返回结果并告知用户。若结果中已经
包含 `openTaskId`,可以按用户需要查询一次投递状态;该查询只确认消息投递,不代表卡片
正文已经更新成功。
当前内容仅为 streaming text,不接受 Lark Card JSON、组件树或按钮 callback。