feat(monitoring): scope authorization-failure cancellation to platform across accounts
When a monitor task's final account hits human verification or an authorization failure, cancel the remaining queued desktop/monitoring tasks for the whole platform instead of only the same account. Follow-up tasks are now marked skipped/aborted (not failed) with a platform_human_verification / platform_authorization_unavailable reason and a user-facing cancellation message, and emit task_canceled lifecycle events for monitor kinds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,39 @@ type queuedDesktopAuthorizationFailureCandidate struct {
|
||||
MonitorTaskID *int64
|
||||
}
|
||||
|
||||
const (
|
||||
monitoringPlatformHumanVerificationSkipReason = "platform_human_verification"
|
||||
monitoringPlatformAuthorizationSkipReason = "platform_authorization_unavailable"
|
||||
)
|
||||
|
||||
func authorizationFailureFollowupCancellation(code, message string) (string, string) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(code + " " + message))
|
||||
if strings.Contains(normalized, "challenge") ||
|
||||
strings.Contains(normalized, "captcha") ||
|
||||
strings.Contains(normalized, "risk_control") ||
|
||||
strings.Contains(normalized, "人机验证") ||
|
||||
strings.Contains(normalized, "人工验证") ||
|
||||
strings.Contains(normalized, "验证码") ||
|
||||
strings.Contains(normalized, "风控") {
|
||||
return monitoringPlatformHumanVerificationSkipReason, "该模型没有其他可用账号,因触发人机验证,后续采集任务已自动取消。"
|
||||
}
|
||||
return monitoringPlatformAuthorizationSkipReason, "该模型当前没有可用账号,后续采集任务已自动取消。"
|
||||
}
|
||||
|
||||
func desktopAuthorizationFollowupTaskStatus(kind string) string {
|
||||
if strings.TrimSpace(kind) == "monitor" {
|
||||
return "aborted"
|
||||
}
|
||||
return "failed"
|
||||
}
|
||||
|
||||
func desktopAuthorizationFollowupEventType(task *repository.DesktopTask) string {
|
||||
if task != nil && task.Kind == "monitor" && task.Status == "aborted" {
|
||||
return "task_canceled"
|
||||
}
|
||||
return "task_completed"
|
||||
}
|
||||
|
||||
func bulkFailQueuedDesktopTasksForAuthorizationFailure(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
@@ -62,13 +95,27 @@ func bulkFailQueuedDesktopTasksForAuthorizationFailure(
|
||||
payload := ensureDesktopAuthorizationFailurePayload(errorPayload, disposition, task, candidate.MonitorTaskID)
|
||||
payload["blocked_by_authorization_failure_task_id"] = task.DesktopID.String()
|
||||
payload["blocked_by_authorization_failure_platform"] = task.Platform
|
||||
if task.Kind == "monitor" {
|
||||
skipReason, cancellationMessage := authorizationFailureFollowupCancellation(disposition.Code, disposition.Message)
|
||||
payload["source_authorization_failure_code"] = payload["code"]
|
||||
payload["code"] = "desktop_monitor_platform_authorization_canceled"
|
||||
payload["message"] = cancellationMessage
|
||||
payload["canceled"] = true
|
||||
payload["cancellation_reason"] = skipReason
|
||||
}
|
||||
|
||||
candidateErrorJSON, marshalErr := json.Marshal(payload)
|
||||
if marshalErr != nil {
|
||||
return nil, response.ErrInternal(50122, "desktop_task_authorization_failure_payload_failed", "failed to encode non-retryable authorization failure")
|
||||
}
|
||||
|
||||
updated, updateErr := failQueuedDesktopTaskForAuthorizationFailure(ctx, tx, candidate.DesktopID, candidateErrorJSON)
|
||||
updated, updateErr := finishQueuedDesktopTaskForAuthorizationFailure(
|
||||
ctx,
|
||||
tx,
|
||||
candidate.DesktopID,
|
||||
desktopAuthorizationFollowupTaskStatus(task.Kind),
|
||||
candidateErrorJSON,
|
||||
)
|
||||
if updateErr != nil {
|
||||
return nil, updateErr
|
||||
}
|
||||
@@ -107,7 +154,7 @@ func loadQueuedDesktopAuthorizationFailureCandidates(
|
||||
AND kind = $3
|
||||
AND status = 'queued'
|
||||
AND target_client_id = $4
|
||||
AND target_account_id = $5
|
||||
AND (kind = 'monitor' OR target_account_id = $5)
|
||||
AND platform_id = $6
|
||||
AND desktop_id <> $7
|
||||
ORDER BY COALESCE(enqueued_at, created_at) ASC, created_at ASC, desktop_id ASC
|
||||
@@ -139,25 +186,28 @@ func loadQueuedDesktopAuthorizationFailureCandidates(
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
func failQueuedDesktopTaskForAuthorizationFailure(
|
||||
func finishQueuedDesktopTaskForAuthorizationFailure(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
desktopID uuid.UUID,
|
||||
status string,
|
||||
errorJSON []byte,
|
||||
) (*repository.DesktopTask, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
UPDATE desktop_tasks AS t
|
||||
SET status = 'failed',
|
||||
SET status = $2,
|
||||
result = NULL,
|
||||
error = $2,
|
||||
error = $3,
|
||||
active_attempt_id = NULL,
|
||||
lease_token_hash = NULL,
|
||||
lease_expires_at = NULL,
|
||||
completed_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE desktop_id = $1
|
||||
AND status = 'queued'
|
||||
RETURNING `+desktopTaskRepositoryReturningColumns,
|
||||
desktopID,
|
||||
status,
|
||||
errorJSON,
|
||||
)
|
||||
updated, err := scanRepositoryDesktopTask(row)
|
||||
@@ -165,7 +215,7 @@ func failQueuedDesktopTaskForAuthorizationFailure(
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, response.ErrInternal(50123, "desktop_task_authorization_failure_update_failed", "failed to fail queued desktop task after authorization failure")
|
||||
return nil, response.ErrInternal(50123, "desktop_task_authorization_failure_update_failed", "failed to finish queued desktop task after authorization failure")
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
@@ -311,9 +361,14 @@ func failMonitoringCollectTasksForDesktopAuthorizationFailure(
|
||||
}
|
||||
|
||||
errorMessage := desktopAuthorizationFailureString(errorPayload["message"])
|
||||
if errorMessage == "" {
|
||||
errorMessage = "登录态已失效,任务未执行。"
|
||||
}
|
||||
skipReason, cancellationMessage := authorizationFailureFollowupCancellation(
|
||||
desktopAuthorizationFailureString(errorPayload["code"]),
|
||||
errorMessage,
|
||||
)
|
||||
errorPayload["canceled"] = true
|
||||
errorPayload["cancellation_reason"] = skipReason
|
||||
errorPayload["source_authorization_failure_message"] = errorMessage
|
||||
errorPayload["message"] = cancellationMessage
|
||||
requestPayloadJSON, err := json.Marshal(map[string]any{
|
||||
"runtime_error": errorPayload,
|
||||
"blocked_by_authorization_failure": true,
|
||||
@@ -324,28 +379,28 @@ func failMonitoringCollectTasksForDesktopAuthorizationFailure(
|
||||
|
||||
if _, err := pool.Exec(ctx, `
|
||||
UPDATE monitoring_collect_tasks
|
||||
SET status = 'failed',
|
||||
SET status = 'skipped',
|
||||
lease_token_hash = NULL,
|
||||
leased_to_executor = NULL,
|
||||
leased_at = NULL,
|
||||
lease_expires_at = NULL,
|
||||
callback_received_at = NOW(),
|
||||
completed_at = NOW(),
|
||||
skip_reason = NULL,
|
||||
error_message = $2,
|
||||
skip_reason = $2,
|
||||
error_message = $3,
|
||||
request_payload_json = (
|
||||
CASE
|
||||
WHEN request_payload_json IS NULL OR jsonb_typeof(request_payload_json) <> 'object'
|
||||
THEN '{}'::jsonb
|
||||
ELSE request_payload_json
|
||||
END
|
||||
) || $3::jsonb,
|
||||
) || $4::jsonb,
|
||||
updated_at = NOW()
|
||||
WHERE id = ANY($1::bigint[])
|
||||
AND callback_received_at IS NULL
|
||||
AND status IN ('pending', 'leased', 'expired')
|
||||
`, monitorTaskIDs, errorMessage, requestPayloadJSON); err != nil {
|
||||
return response.ErrInternal(50125, "monitoring_authorization_failure_update_failed", "failed to fail monitoring tasks after authorization failure")
|
||||
`, monitorTaskIDs, skipReason, cancellationMessage, requestPayloadJSON); err != nil {
|
||||
return response.ErrInternal(50125, "monitoring_authorization_failure_update_failed", "failed to skip monitoring tasks after authorization failure")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user