Files
geo/server/internal/tenant/app/desktop_task_authorization_failure.go
T

476 lines
16 KiB
Go
Raw Normal View History

package app
import (
"context"
"encoding/json"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/zap"
"github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/shared/stream"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
type desktopAuthorizationFailureDisposition struct {
NonRetryable bool
Category string
Code string
Message string
}
type desktopAuthorizationBulkFailure struct {
Tasks []*repository.DesktopTask
PublishOutcomes []*desktopPublishSyncOutcome
MonitorTaskIDs []int64
ErrorPayload map[string]any
}
type queuedDesktopAuthorizationFailureCandidate struct {
DesktopID uuid.UUID
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,
task *repository.DesktopTask,
errorJSON []byte,
) (*desktopAuthorizationBulkFailure, error) {
result := &desktopAuthorizationBulkFailure{}
if tx == nil || task == nil || task.Status != "failed" {
return result, nil
}
errorPayload := unmarshalJSONObject(errorJSON)
disposition, ok := desktopAuthorizationFailureDispositionFromPayload(task, errorPayload)
if !ok || !disposition.NonRetryable {
return result, nil
}
result.ErrorPayload = ensureDesktopAuthorizationFailurePayload(errorPayload, disposition, task, nil)
candidates, err := loadQueuedDesktopAuthorizationFailureCandidates(ctx, tx, task)
if err != nil {
return nil, err
}
for _, candidate := range candidates {
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 := finishQueuedDesktopTaskForAuthorizationFailure(
ctx,
tx,
candidate.DesktopID,
desktopAuthorizationFollowupTaskStatus(task.Kind),
candidateErrorJSON,
)
if updateErr != nil {
return nil, updateErr
}
if updated == nil {
continue
}
result.Tasks = append(result.Tasks, updated)
if updated.Kind == "publish" {
publishOutcome, syncErr := syncDesktopPublishTaskState(ctx, tx, updated)
if syncErr != nil {
return nil, syncErr
}
if publishOutcome != nil {
result.PublishOutcomes = append(result.PublishOutcomes, publishOutcome)
}
}
if updated.Kind == "monitor" && updated.MonitorTaskID != nil {
result.MonitorTaskIDs = append(result.MonitorTaskIDs, *updated.MonitorTaskID)
}
}
return result, nil
}
func loadQueuedDesktopAuthorizationFailureCandidates(
ctx context.Context,
tx pgx.Tx,
task *repository.DesktopTask,
) ([]queuedDesktopAuthorizationFailureCandidate, error) {
rows, err := tx.Query(ctx, `
SELECT desktop_id, monitor_task_id
FROM desktop_tasks
WHERE tenant_id = $1
AND workspace_id = $2
AND kind = $3
AND status = 'queued'
AND target_client_id = $4
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
FOR UPDATE SKIP LOCKED
`, task.TenantID, task.WorkspaceID, task.Kind, task.TargetClientID, task.TargetAccountID, task.Platform, task.DesktopID)
if err != nil {
return nil, response.ErrInternal(50121, "desktop_task_authorization_failure_query_failed", "failed to select queued desktop tasks blocked by authorization failure")
}
defer rows.Close()
candidates := make([]queuedDesktopAuthorizationFailureCandidate, 0)
for rows.Next() {
var (
candidate queuedDesktopAuthorizationFailureCandidate
monitorTaskID pgtype.Int8
)
if scanErr := rows.Scan(&candidate.DesktopID, &monitorTaskID); scanErr != nil {
return nil, response.ErrInternal(50121, "desktop_task_authorization_failure_scan_failed", "failed to parse queued desktop tasks blocked by authorization failure")
}
if monitorTaskID.Valid {
value := monitorTaskID.Int64
candidate.MonitorTaskID = &value
}
candidates = append(candidates, candidate)
}
if err := rows.Err(); err != nil {
return nil, response.ErrInternal(50121, "desktop_task_authorization_failure_scan_failed", "failed to iterate queued desktop tasks blocked by authorization failure")
}
return candidates, nil
}
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 = $2,
result = NULL,
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)
if err != nil {
if err == pgx.ErrNoRows {
return nil, nil
}
return nil, response.ErrInternal(50123, "desktop_task_authorization_failure_update_failed", "failed to finish queued desktop task after authorization failure")
}
return updated, nil
}
func desktopAuthorizationFailureDispositionFromPayload(
task *repository.DesktopTask,
payload map[string]any,
) (desktopAuthorizationFailureDisposition, bool) {
if task == nil || len(payload) == 0 {
return desktopAuthorizationFailureDisposition{}, false
}
disposition := desktopAuthorizationFailureDisposition{
Category: desktopAuthorizationFailureString(payload["failure_category"]),
Code: desktopAuthorizationFailureString(payload["code"]),
Message: desktopAuthorizationFailureString(payload["message"]),
}
hasAuthorizationSignal := desktopAuthorizationFailureCategory(disposition.Category) ||
desktopAuthorizationFailureCode(disposition.Code) ||
desktopAuthorizationFailureMessage(disposition.Message)
if !hasAuthorizationSignal {
return desktopAuthorizationFailureDisposition{}, false
}
disposition.NonRetryable = true
if disposition.Category == "" {
disposition.Category = desktopAuthorizationFailureCategoryForTask(task)
}
if !desktopAuthorizationFailureCategoryMatchesTask(task, disposition.Category) {
return desktopAuthorizationFailureDisposition{}, false
}
return disposition, true
}
func ensureDesktopAuthorizationFailurePayload(
payload map[string]any,
disposition desktopAuthorizationFailureDisposition,
task *repository.DesktopTask,
monitorTaskID *int64,
) map[string]any {
result := make(map[string]any, len(payload)+6)
for key, value := range payload {
result[key] = value
}
if disposition.Code != "" {
result["code"] = disposition.Code
} else if _, exists := result["code"]; !exists {
result["code"] = "desktop_account_auth_expired"
}
if disposition.Message != "" {
result["message"] = disposition.Message
} else if _, exists := result["message"]; !exists {
result["message"] = "登录态已失效,任务未执行。"
}
result["retryable"] = false
result["non_retryable"] = true
if disposition.Category != "" {
result["failure_category"] = disposition.Category
} else if task != nil {
result["failure_category"] = desktopAuthorizationFailureCategoryForTask(task)
}
if task != nil && strings.TrimSpace(task.Platform) != "" {
result["platform"] = strings.TrimSpace(task.Platform)
}
if monitorTaskID != nil {
result["monitoring_task_id"] = *monitorTaskID
}
return result
}
func desktopAuthorizationFailureCategoryForTask(task *repository.DesktopTask) string {
if task != nil && task.Kind == "monitor" {
return "ai_platform_authorization"
}
return "media_account_authorization"
}
func desktopAuthorizationFailureCategoryMatchesTask(task *repository.DesktopTask, category string) bool {
category = strings.TrimSpace(category)
if task != nil && task.Kind == "monitor" {
return category == "ai_platform_authorization"
}
if task != nil && task.Kind == "publish" {
return category == "media_account_authorization"
}
return false
}
func desktopAuthorizationFailureCategory(category string) bool {
category = strings.TrimSpace(category)
return category == "ai_platform_authorization" || category == "media_account_authorization"
}
func desktopAuthorizationFailureCode(code string) bool {
normalized := strings.ToLower(strings.TrimSpace(code))
if normalized == "" {
return false
}
if normalized == "desktop_account_auth_expired" ||
normalized == "desktop_account_challenge_required" ||
normalized == "desktop_account_risk_control" {
return true
}
return strings.HasSuffix(normalized, "_login_required") ||
strings.HasSuffix(normalized, "_login_expired") ||
strings.HasSuffix(normalized, "_not_logged_in") ||
strings.HasSuffix(normalized, "_challenge_required")
}
func desktopAuthorizationFailureMessage(message string) bool {
normalized := strings.ToLower(strings.TrimSpace(message))
if normalized == "" {
return false
}
return strings.Contains(normalized, "login required") ||
strings.Contains(normalized, "login expired") ||
strings.Contains(normalized, "not logged in") ||
strings.Contains(normalized, "unauthorized") ||
strings.Contains(normalized, "登录态失效") ||
strings.Contains(normalized, "登录态已失效") ||
strings.Contains(normalized, "登录已失效") ||
strings.Contains(normalized, "请重新登录") ||
strings.Contains(normalized, "请先登录") ||
strings.Contains(normalized, "未登录")
}
func desktopAuthorizationFailureString(value any) string {
text, ok := value.(string)
if !ok {
return ""
}
return strings.TrimSpace(text)
}
func failMonitoringCollectTasksForDesktopAuthorizationFailure(
ctx context.Context,
pool *pgxpool.Pool,
monitorTaskIDs []int64,
errorPayload map[string]any,
) error {
if pool == nil || len(monitorTaskIDs) == 0 {
return nil
}
errorMessage := desktopAuthorizationFailureString(errorPayload["message"])
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,
})
if err != nil {
return response.ErrInternal(50124, "monitoring_authorization_failure_payload_failed", "failed to encode monitoring authorization failure payload")
}
if _, err := pool.Exec(ctx, `
UPDATE monitoring_collect_tasks
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 = $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
) || $4::jsonb,
updated_at = NOW()
WHERE id = ANY($1::bigint[])
AND callback_received_at IS NULL
AND status IN ('pending', 'leased', 'expired')
`, 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
}
func publishDesktopTaskLifecycleEvent(
ctx context.Context,
messaging *rabbitmq.Client,
logger *zap.Logger,
task *repository.DesktopTask,
eventType string,
) {
if task == nil {
return
}
title, businessDate, schedulerGroupKey, questionText := deriveDesktopTaskEventMetadataFromPayload(
task.Kind,
task.Payload,
)
event := DesktopTaskEvent{
Type: eventType,
TaskID: task.DesktopID.String(),
JobID: task.JobID.String(),
WorkspaceID: task.WorkspaceID,
TargetAccountID: task.TargetAccountID.String(),
TargetClientID: task.TargetClientID.String(),
Platform: task.Platform,
Title: title,
BusinessDate: businessDate,
SchedulerGroupKey: schedulerGroupKey,
QuestionText: questionText,
Status: task.Status,
Kind: task.Kind,
Priority: task.Priority,
Lane: task.Lane,
InterruptGeneration: task.InterruptGeneration,
UpdatedAt: task.UpdatedAt,
}
if err := publishDesktopTaskEvent(ctx, messaging, event); err != nil && logger != nil {
logger.Warn("desktop task event publish failed",
zap.String("task_id", task.DesktopID.String()),
zap.String("event_type", eventType),
zap.Error(err),
)
}
if err := publishDesktopDispatchEvent(ctx, messaging, logger, stream.DesktopDispatchEvent{
Type: event.Type,
TaskID: event.TaskID,
JobID: event.JobID,
WorkspaceID: event.WorkspaceID,
TargetAccountID: event.TargetAccountID,
TargetClientID: event.TargetClientID,
Platform: event.Platform,
Title: event.Title,
BusinessDate: event.BusinessDate,
SchedulerGroupKey: event.SchedulerGroupKey,
QuestionText: event.QuestionText,
Status: event.Status,
Kind: event.Kind,
Priority: event.Priority,
Lane: event.Lane,
InterruptGeneration: event.InterruptGeneration,
UpdatedAt: event.UpdatedAt,
}); err != nil && logger != nil {
logger.Warn("desktop dispatch mirror publish failed",
zap.String("task_id", task.DesktopID.String()),
zap.String("event_type", eventType),
zap.Error(err),
)
}
}