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:
2026-07-14 22:00:41 +08:00
parent b0bf9b4299
commit 1fab7b1e5f
4 changed files with 196 additions and 31 deletions
@@ -10,10 +10,12 @@ import (
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgconn"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/geo-platform/tenant-api/internal/shared/config"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
func TestMonitoringAnswerParseModelUsesConfig(t *testing.T) {
@@ -635,6 +637,109 @@ func TestMonitoringAuthorizationFailureTargetClientPrefersExplicitTargetClient(t
}
}
func TestMonitorAuthorizationFollowupCancellationIsPlatformScopedAcrossAccounts(t *testing.T) {
tx := &captureDailyMonitorTaskTx{}
task := &repository.DesktopTask{
DesktopID: uuid.New(),
TenantID: 77,
WorkspaceID: 88,
Kind: "monitor",
TargetClientID: uuid.New(),
TargetAccountID: uuid.New(),
Platform: "doubao",
}
rows, err := loadQueuedDesktopAuthorizationFailureCandidates(context.Background(), tx, task)
require.NoError(t, err)
assert.Empty(t, rows)
require.True(t, tx.queryCalled)
query := normalizeSQLWhitespace(tx.querySQL)
assert.Contains(t, query, "AND kind = $3")
assert.Contains(t, query, "AND target_client_id = $4")
assert.Contains(t, query, "AND (kind = 'monitor' OR target_account_id = $5)")
assert.Contains(t, query, "AND platform_id = $6")
assert.Equal(t, "aborted", desktopAuthorizationFollowupTaskStatus("monitor"))
assert.Equal(t, "failed", desktopAuthorizationFollowupTaskStatus("publish"))
assert.Equal(t, "task_canceled", desktopAuthorizationFollowupEventType(&repository.DesktopTask{
Kind: "monitor",
Status: "aborted",
}))
}
func TestFailRemainingMonitoringTasksAfterFinalAccountChallengeScopesToModelAndClient(t *testing.T) {
targetClientID := uuid.New().String()
completedAt := time.Date(2026, 7, 14, 9, 30, 0, 0, time.UTC)
message := "豆包账号触发人机验证,请完成验证码后再继续采集。"
tx := &captureDailyMonitorTaskTx{commandTag: pgconn.NewCommandTag("UPDATE 3")}
task := &monitoringCollectTask{
ID: 901,
TenantID: 77,
AIPlatformID: "doubao",
CollectorType: monitoringCollectorType,
BusinessDate: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC),
ExecutionOwner: "desktop_tasks",
TargetClientID: sql.NullString{String: targetClientID, Valid: true},
}
req := MonitoringTaskResultRequest{
Status: "failed",
ErrorMessage: &message,
RawResponseJSON: map[string]interface{}{
"runtime_error": map[string]interface{}{
"code": "doubao_challenge_required",
"message": message,
"retryable": false,
"non_retryable": true,
"failure_category": "ai_platform_authorization",
},
},
}
affected, err := (&MonitoringCallbackService{}).failRemainingMonitoringTasksForAuthorizationFailure(
context.Background(),
tx,
task,
req,
completedAt,
)
require.NoError(t, err)
assert.Equal(t, int64(3), affected)
require.True(t, tx.execCalled)
query := normalizeSQLWhitespace(tx.execSQL)
assert.Contains(t, query, "SET status = 'skipped'")
assert.Contains(t, query, "skip_reason = $8")
assert.Contains(t, query, "WHERE tenant_id = $1")
assert.Contains(t, query, "AND business_date = $3::date")
assert.Contains(t, query, "AND ai_platform_id = $4")
assert.Contains(t, query, "AND id <> $5")
assert.Contains(t, query, "target_client_id::text = $6 OR leased_to_executor = $6")
assert.Contains(t, query, "AND status IN ('pending', 'leased', 'expired')")
require.Len(t, tx.execArgs, 11)
assert.Equal(t, int64(77), tx.execArgs[0])
assert.Equal(t, monitoringCollectorType, tx.execArgs[1])
assert.Equal(t, "2026-07-14", tx.execArgs[2])
assert.Equal(t, "doubao", tx.execArgs[3])
assert.Equal(t, int64(901), tx.execArgs[4])
assert.Equal(t, targetClientID, tx.execArgs[5])
assert.Equal(t, completedAt, tx.execArgs[6])
assert.Equal(t, monitoringPlatformHumanVerificationSkipReason, tx.execArgs[7])
assert.Equal(t, "该模型没有其他可用账号,因触发人机验证,后续采集任务已自动取消。", tx.execArgs[8])
assert.Equal(t, "desktop_tasks", tx.execArgs[10])
payloadJSON, ok := tx.execArgs[9].([]byte)
require.True(t, ok)
var payload map[string]interface{}
require.NoError(t, json.Unmarshal(payloadJSON, &payload))
assert.Equal(t, true, payload["blocked_by_authorization_failure"])
runtimeError, ok := payload["runtime_error"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "doubao_challenge_required", runtimeError["code"])
assert.Equal(t, "doubao", runtimeError["blocked_by_authorization_failure_platform"])
assert.Equal(t, true, runtimeError["canceled"])
assert.Equal(t, monitoringPlatformHumanVerificationSkipReason, runtimeError["cancellation_reason"])
}
func TestBuildMonitoringRawPayloadQwenIncludesSearchResultsInCitationInputs(t *testing.T) {
searchTitle := "搜索网页结果"