diff --git a/internal/output/lark_errors.go b/internal/output/lark_errors.go index 269d952fc..a2ac2590e 100644 --- a/internal/output/lark_errors.go +++ b/internal/output/lark_errors.go @@ -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", "" diff --git a/internal/output/lark_errors_test.go b/internal/output/lark_errors_test.go index 63c82002d..a9af905c5 100644 --- a/internal/output/lark_errors_test.go +++ b/internal/output/lark_errors_test.go @@ -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 { diff --git a/internal/output/ownership_recovery.go b/internal/output/ownership_recovery.go new file mode 100644 index 000000000..d927530ee --- /dev/null +++ b/internal/output/ownership_recovery.go @@ -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 ', then send the local file via 'im +send..'. For post or interactive, upload first and use the new image_key or file_key." +} diff --git a/internal/output/ownership_recovery_test.go b/internal/output/ownership_recovery_test.go new file mode 100644 index 000000000..43b09c9ff --- /dev/null +++ b/internal/output/ownership_recovery_test.go @@ -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 ", + "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 ", + "--file-key ", + "--type ", + "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") + } +}