2026-05-15 23:50:50 +08:00
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
}
2026-07-14 22:00:41 +08:00
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"
}
2026-05-15 23:50:50 +08:00
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
2026-07-14 22:00:41 +08:00
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
}
2026-05-15 23:50:50 +08:00
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" )
}
2026-07-14 22:00:41 +08:00
updated , updateErr := finishQueuedDesktopTaskForAuthorizationFailure (
ctx ,
tx ,
candidate . DesktopID ,
desktopAuthorizationFollowupTaskStatus ( task . Kind ) ,
candidateErrorJSON ,
)
2026-05-15 23:50:50 +08:00
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
2026-07-14 22:00:41 +08:00
AND (kind = 'monitor' OR target_account_id = $5)
2026-05-15 23:50:50 +08:00
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
}
2026-07-14 22:00:41 +08:00
func finishQueuedDesktopTaskForAuthorizationFailure (
2026-05-15 23:50:50 +08:00
ctx context . Context ,
tx pgx . Tx ,
desktopID uuid . UUID ,
2026-07-14 22:00:41 +08:00
status string ,
2026-05-15 23:50:50 +08:00
errorJSON [ ] byte ,
) ( * repository . DesktopTask , error ) {
row := tx . QueryRow ( ctx , `
UPDATE desktop_tasks AS t
2026-07-14 22:00:41 +08:00
SET status = $2,
2026-05-15 23:50:50 +08:00
result = NULL,
2026-07-14 22:00:41 +08:00
error = $3,
2026-05-15 23:50:50 +08:00
active_attempt_id = NULL,
lease_token_hash = NULL,
lease_expires_at = NULL,
2026-07-14 22:00:41 +08:00
completed_at = NOW(),
2026-05-15 23:50:50 +08:00
updated_at = NOW()
WHERE desktop_id = $1
AND status = 'queued'
RETURNING ` + desktopTaskRepositoryReturningColumns ,
desktopID ,
2026-07-14 22:00:41 +08:00
status ,
2026-05-15 23:50:50 +08:00
errorJSON ,
)
updated , err := scanRepositoryDesktopTask ( row )
if err != nil {
if err == pgx . ErrNoRows {
return nil , nil
}
2026-07-14 22:00:41 +08:00
return nil , response . ErrInternal ( 50123 , "desktop_task_authorization_failure_update_failed" , "failed to finish queued desktop task after authorization failure" )
2026-05-15 23:50:50 +08:00
}
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" ] )
2026-07-14 22:00:41 +08:00
skipReason , cancellationMessage := authorizationFailureFollowupCancellation (
desktopAuthorizationFailureString ( errorPayload [ "code" ] ) ,
errorMessage ,
)
errorPayload [ "canceled" ] = true
errorPayload [ "cancellation_reason" ] = skipReason
errorPayload [ "source_authorization_failure_message" ] = errorMessage
errorPayload [ "message" ] = cancellationMessage
2026-05-15 23:50:50 +08:00
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
2026-07-14 22:00:41 +08:00
SET status = 'skipped',
2026-05-15 23:50:50 +08:00
lease_token_hash = NULL,
leased_to_executor = NULL,
leased_at = NULL,
lease_expires_at = NULL,
callback_received_at = NOW(),
completed_at = NOW(),
2026-07-14 22:00:41 +08:00
skip_reason = $2,
error_message = $3,
2026-05-15 23:50:50 +08:00
request_payload_json = (
CASE
WHEN request_payload_json IS NULL OR jsonb_typeof(request_payload_json) <> 'object'
THEN ' { }'::jsonb
ELSE request_payload_json
END
2026-07-14 22:00:41 +08:00
) || $4::jsonb,
2026-05-15 23:50:50 +08:00
updated_at = NOW()
WHERE id = ANY($1::bigint[])
AND callback_received_at IS NULL
AND status IN ('pending', 'leased', 'expired')
2026-07-14 22:00:41 +08:00
` , monitorTaskIDs , skipReason , cancellationMessage , requestPayloadJSON ) ; err != nil {
return response . ErrInternal ( 50125 , "monitoring_authorization_failure_update_failed" , "failed to skip monitoring tasks after authorization failure" )
2026-05-15 23:50:50 +08:00
}
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 ) ,
)
}
}