feat(im): add recovery hint for cross-identity message resources (#652)

Change-Id: I8a43486333638271f0fbbcffca81a60c9f9d2060
This commit is contained in:
chenxingtong-bytedance
2026-04-28 20:16:39 +08:00
committed by GitHub
parent fc9f9c1f26
commit 0bbd0f2c7d
4 changed files with 92 additions and 0 deletions

View File

@@ -49,6 +49,9 @@ const (
// caller already holds the requested permission, or the target type does
// not accept apply operations).
LarkErrDrivePermApplyNotApplicable = 1063007
// IM resource ownership mismatch.
LarkErrOwnershipMismatch = 231205
)
// ClassifyLarkError maps a Lark API error code + message to (exitCode, errType, hint).
@@ -98,6 +101,9 @@ func ClassifyLarkError(code int, msg string) (int, string, string) {
case LarkErrDrivePermApplyNotApplicable:
return ExitAPI, "invalid_params",
"this document does not accept a permission-apply request (common causes: the document is configured to disallow access requests, the caller already holds the permission, or the target type does not support apply); contact the owner directly"
case LarkErrOwnershipMismatch:
return ExitAPI, "ownership_mismatch", buildOwnershipRecoveryHint()
}
return ExitAPI, "api_error", ""

View File

@@ -61,6 +61,13 @@ func TestClassifyLarkError_DriveCreateShortcutConstraints(t *testing.T) {
wantType: "invalid_params",
wantHint: "does not accept a permission-apply request",
},
{
name: "ownership mismatch",
code: LarkErrOwnershipMismatch,
wantExitCode: ExitAPI,
wantType: "ownership_mismatch",
wantHint: "messages-resources-download",
},
}
for _, tt := range tests {

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package output
func buildOwnershipRecoveryHint() string {
return "This resource belongs to another user — you can't send it directly. Download it with 'im +messages-resources-download --output <output_path>', then send the local file via 'im +send..'. For post or interactive, upload first and use the new image_key or file_key."
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package output
import (
"strings"
"testing"
)
func checkOwnershipRecoveryHint(t *testing.T, hint string) {
t.Helper()
for _, part := range []string{
"im +messages-resources-download",
"--output <output_path>",
"This resource belongs to another user",
"download",
"send",
"image_key",
"file_key",
} {
if !strings.Contains(hint, part) {
t.Fatalf("hint %q missing %q", hint, part)
}
}
if len(hint) > 360 {
t.Fatalf("hint is too long: %d bytes", len(hint))
}
for _, noisy := range []string{
"Step 1",
"Step 2",
"Step 3",
"--message-id <message_id>",
"--file-key <resource_key>",
"--type <image|file>",
"identity",
"do not keep retrying alternative download methods",
"POST /open-apis",
} {
if strings.Contains(hint, noisy) {
t.Fatalf("hint %q should not contain noisy phrase %q", hint, noisy)
}
}
}
func TestBuildOwnershipRecoveryHint(t *testing.T) {
checkOwnershipRecoveryHint(t, buildOwnershipRecoveryHint())
}
func TestErrAPI_OwnershipMismatch(t *testing.T) {
upstreamMessage := "Bot or User is NOT the owner of the uat resource."
err := ErrAPI(LarkErrOwnershipMismatch, upstreamMessage, map[string]any{"log_id": "test-log"})
if err.Code != ExitAPI {
t.Fatalf("exit code = %d, want %d", err.Code, ExitAPI)
}
if err.Detail == nil {
t.Fatal("expected detail")
}
if err.Detail.Type != "ownership_mismatch" {
t.Fatalf("type = %q, want %q", err.Detail.Type, "ownership_mismatch")
}
if got, want := err.Detail.Message, upstreamMessage; got != want {
t.Fatalf("message = %q, want %q", got, want)
}
checkOwnershipRecoveryHint(t, err.Detail.Hint)
if err.Detail.Detail == nil {
t.Fatal("expected upstream detail to be preserved")
}
}