Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/output/lark_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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", ""
Expand Down
7 changes: 7 additions & 0 deletions internal/output/lark_errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions internal/output/ownership_recovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package output

func buildOwnershipRecoveryHint() string {
Comment thread
chenxingtong-bytedance marked this conversation as resolved.
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."
}
71 changes: 71 additions & 0 deletions internal/output/ownership_recovery_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading