diff --git a/internal/errclass/classify_test.go b/internal/errclass/classify_test.go index d4dff5809..2f8924e8d 100644 --- a/internal/errclass/classify_test.go +++ b/internal/errclass/classify_test.go @@ -113,6 +113,7 @@ func TestBuildAPIError_ExitCodeMatrix(t *testing.T) { {"230027 user_not_authorized", 230027, errs.CategoryAuthorization, errs.SubtypeUserUnauthorized, 3, "PermissionError"}, {"1470403 task_permission_denied", 1470403, errs.CategoryAuthorization, errs.SubtypePermissionDenied, 3, "PermissionError"}, {"1470400 task_invalid_params", 1470400, errs.CategoryAPI, errs.SubtypeInvalidParameters, 1, "APIError"}, + {"1062507 drive_parent_sibling_limit", 1062507, errs.CategoryAPI, errs.SubtypeQuotaExceeded, 1, "APIError"}, {"99991400 rate_limit", 99991400, errs.CategoryAPI, errs.SubtypeRateLimit, 1, "APIError"}, {"99991661 token_missing", 99991661, errs.CategoryAuthentication, errs.SubtypeTokenMissing, 3, "AuthenticationError"}, {"21000 challenge_required", 21000, errs.CategoryPolicy, errs.Subtype("challenge_required"), 6, "SecurityPolicyError"}, diff --git a/internal/errclass/codemeta_drive.go b/internal/errclass/codemeta_drive.go index 113949b24..2a7e40d3d 100644 --- a/internal/errclass/codemeta_drive.go +++ b/internal/errclass/codemeta_drive.go @@ -17,6 +17,7 @@ var driveCodeMeta = map[int]CodeMeta{ 1061043: {Category: errs.CategoryAPI, Subtype: errs.SubtypeQuotaExceeded}, // file size beyond limit 1061044: {Category: errs.CategoryAPI, Subtype: errs.SubtypeNotFound}, // parent folder does not exist (upload) 1061101: {Category: errs.CategoryAPI, Subtype: errs.SubtypeQuotaExceeded}, // file quota exceeded + 1062507: {Category: errs.CategoryAPI, Subtype: errs.SubtypeQuotaExceeded}, // parent folder child count limit exceeded 1062009: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // actual size inconsistent with declared size 1063001: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // secure label invalid parameter 1063002: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypePermissionDenied}, // secure label permission denied diff --git a/shortcuts/drive/drive_push.go b/shortcuts/drive/drive_push.go index bc5adfc4c..46050a924 100644 --- a/shortcuts/drive/drive_push.go +++ b/shortcuts/drive/drive_push.go @@ -623,6 +623,10 @@ func driveClassifyBatchFailure(err error) driveBatchFailureDecision { case problem.Subtype == errs.SubtypeRateLimit || problem.Code == 99991400: decision.Class = "rate_limited" decision.Terminal = true + case problem.Code == 1062507: + decision.Class = "parent_sibling_limit" + decision.Terminal = true + decision.Hint = "The destination parent folder has reached its child-count limit. Clean up that folder, choose another --folder-token, or split the upload across subfolders before retrying." case problem.Subtype == errs.SubtypeQuotaExceeded || problem.Code == 1061043: decision.Class = "file_size_limit" case problem.Code == 1062009: diff --git a/shortcuts/drive/drive_push_test.go b/shortcuts/drive/drive_push_test.go index 7bad05d0f..21c57e678 100644 --- a/shortcuts/drive/drive_push_test.go +++ b/shortcuts/drive/drive_push_test.go @@ -1334,6 +1334,75 @@ func TestDrivePushAbortsAfterCreateFolderMissingScope(t *testing.T) { } } +func TestDrivePushAbortsAfterCreateFolderParentSiblingLimit(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig()) + + tmpDir := t.TempDir() + withDriveWorkingDir(t, tmpDir) + if err := os.MkdirAll(filepath.Join("local", "a"), 0o755); err != nil { + t.Fatalf("MkdirAll a: %v", err) + } + if err := os.MkdirAll(filepath.Join("local", "b"), 0o755); err != nil { + t.Fatalf("MkdirAll b: %v", err) + } + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "folder_token=folder_root", + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{"files": []interface{}{}, "has_more": false}, + }, + }) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/files/create_folder", + Body: map[string]interface{}{ + "code": 1062507, + "msg": "parent node out of sibling num.", + }, + }) + + err := mountAndRunDrive(t, DrivePush, []string{ + "+push", + "--local-dir", "local", + "--folder-token", "folder_root", + "--as", "bot", + }, f, stdout) + if err == nil { + t.Fatalf("expected partial failure, got nil\nstdout: %s", stdout.String()) + } + var pfErr *output.PartialFailureError + if !errors.As(err, &pfErr) { + t.Fatalf("expected *output.PartialFailureError, got %T: %v", err, err) + } + summary, items := splitDrivePushStdout(t, stdout.Bytes()) + if got := summary["failed"]; got != float64(1) { + t.Fatalf("summary.failed = %v, want 1", got) + } + if got := summary["aborted"]; got != true { + t.Fatalf("summary.aborted = %v, want true", got) + } + if len(items) != 1 { + t.Fatalf("items len = %d, want 1; items=%#v", len(items), items) + } + item := items[0] + if item["rel_path"] != "a" || item["phase"] != "create_folder" || item["error_class"] != "parent_sibling_limit" { + t.Fatalf("unexpected failed item: %#v", item) + } + if item["code"] != float64(1062507) || item["subtype"] != "quota_exceeded" || item["retryable"] != false { + t.Fatalf("unexpected failure metadata: %#v", item) + } + if got, _ := item["hint"].(string); !strings.Contains(got, "--folder-token") || !strings.Contains(got, "child-count limit") { + t.Fatalf("hint should explain the destination folder child-count limit, got item=%#v", item) + } + for _, item := range items { + if item["rel_path"] == "b" { + t.Fatalf("parent sibling limit must abort before b, got items=%#v", items) + } + } +} + func TestDrivePushDetectsLocalFileChangedBeforeUpload(t *testing.T) { f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig()) diff --git a/skills/lark-drive/references/lark-drive-push.md b/skills/lark-drive/references/lark-drive-push.md index 0d7b881a7..c885146b8 100644 --- a/skills/lark-drive/references/lark-drive-push.md +++ b/skills/lark-drive/references/lark-drive-push.md @@ -143,6 +143,7 @@ lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \ | `permission_denied` | `1061004` / HTTP 403 | 当前身份无权操作目标资源 | 停止重试,检查目标文件夹权限、身份类型(user / bot)和资源可见性 | | `invalid_api_parameters` | `1061002` | API 参数被服务端拒绝 | 停止重试,检查 `--folder-token`、覆盖模式、`file_token`、文件名和上传参数;不要对同一参数组合批量重试 | | `parent_node_missing` | `1061044` | 上传 / 建目录使用的父文件夹不存在或当前身份不可见 | 停止重试,检查 `--folder-token` 是否仍存在、是否有权限、父目录是否在 push 过程中被删除;不要继续上传同一目录树 | +| `parent_sibling_limit` | `1062507` | 目标父文件夹单层子节点数量超过上限 | 停止重试,清理目标目录、换一个 `--folder-token`,或把上传内容拆到多个子目录 | | `rate_limited` | `99991400` | 触发频控 | 停止当前批次,退避后再重试 | | `server_error` | `1061001` / `2200` | Drive 服务端异常 | 停止当前批次,稍后重试;保留 `log_id` 便于排查 |