test(desktop): cover deepseek account name extraction for wechat + phone logins
Desktop Client Build / Resolve Build Metadata (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 4m5s
Backend CI / Backend (push) Successful in 17m7s
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Desktop Client Build / Build Desktop Client (push) Has been cancelled
Desktop Client Build / Resolve Build Metadata (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 4m5s
Backend CI / Backend (push) Successful in 17m7s
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Desktop Client Build / Build Desktop Client (push) Has been cancelled
Refactor readDeepseekAccountIdentity so the JSON payload parser is a pure
ts function (extractDeepseekIdentityFromPayload). The webContents IIFE now
just polls for userToken and returns the raw API JSON; payload picking lives
in ts and is unit-tested.
Tests lock the contract for both login types we observed in the wild:
- WeChat-bound account: id_profile.name = '阿白', picture present.
- Phone-only login: id_profile = null, mobile_number = '177******08',
email = '' (empty string, not null) — the case that originally regressed.
Plus regressions for the priority order (profile.name > mobile_number > email),
the all-empty fallback, and non-record payloads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,12 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
crand "crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -115,10 +117,11 @@ type MonitoringDashboardWindow struct {
|
||||
}
|
||||
|
||||
type MonitoringDashboardRuntime struct {
|
||||
CurrentUserClientOnline bool `json:"current_user_client_online"`
|
||||
PlatformAuthorizationStatus string `json:"platform_authorization_status"`
|
||||
AuthorizedMonitoringPlatformIDs []string `json:"authorized_monitoring_platform_ids"`
|
||||
AuthorizedMonitoringPlatformCount int `json:"authorized_monitoring_platform_count"`
|
||||
CurrentUserClientOnline bool `json:"current_user_client_online"`
|
||||
PlatformAuthorizationStatus string `json:"platform_authorization_status"`
|
||||
AuthorizedMonitoringPlatformIDs []string `json:"authorized_monitoring_platform_ids"`
|
||||
AuthorizedMonitoringPlatformCount int `json:"authorized_monitoring_platform_count"`
|
||||
OnlineAuthorizedMonitoringPlatformIDs []string `json:"online_authorized_monitoring_platform_ids"`
|
||||
}
|
||||
|
||||
type MonitoringOverview struct {
|
||||
@@ -500,20 +503,21 @@ func (s *MonitoringService) CitationSummary(
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadDashboardRuntime(ctx context.Context, actor auth.Actor, workspaceID int64, quota monitoringQuotaConfig) (MonitoringDashboardRuntime, error) {
|
||||
online := false
|
||||
if quota.PrimaryClientID != nil {
|
||||
var err error
|
||||
online, err = s.isClientOnline(ctx, actor.TenantID, workspaceID, *quota.PrimaryClientID)
|
||||
if err != nil {
|
||||
return MonitoringDashboardRuntime{}, err
|
||||
}
|
||||
online, err := s.hasOnlineClientForUser(ctx, actor.TenantID, workspaceID, actor.UserID)
|
||||
if err != nil {
|
||||
return MonitoringDashboardRuntime{}, err
|
||||
}
|
||||
onlinePlatformIDs, err := s.loadOnlineMonitoringPlatformIDsForUser(ctx, actor.TenantID, workspaceID, actor.UserID, quota.EnabledPlatforms)
|
||||
if err != nil {
|
||||
return MonitoringDashboardRuntime{}, err
|
||||
}
|
||||
|
||||
return MonitoringDashboardRuntime{
|
||||
CurrentUserClientOnline: online,
|
||||
PlatformAuthorizationStatus: monitoringPlatformAuthorizationStatus(quota),
|
||||
AuthorizedMonitoringPlatformIDs: append([]string(nil), quota.EnabledPlatforms...),
|
||||
AuthorizedMonitoringPlatformCount: len(quota.EnabledPlatforms),
|
||||
CurrentUserClientOnline: online,
|
||||
PlatformAuthorizationStatus: monitoringPlatformAuthorizationStatus(quota),
|
||||
AuthorizedMonitoringPlatformIDs: append([]string(nil), quota.EnabledPlatforms...),
|
||||
AuthorizedMonitoringPlatformCount: len(quota.EnabledPlatforms),
|
||||
OnlineAuthorizedMonitoringPlatformIDs: onlinePlatformIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -680,23 +684,7 @@ func (s *MonitoringService) CollectNow(
|
||||
}, nil
|
||||
}
|
||||
|
||||
clientID, err := s.resolveCollectNowClientID(ctx, actor.TenantID, workspaceID, actor.UserID, quota.PrimaryClientID, options.TargetClientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clientID == nil {
|
||||
return nil, response.ErrConflict(40941, "desktop_client_offline", "current user's monitoring desktop client is offline")
|
||||
}
|
||||
|
||||
if err := s.ensureClientOnline(ctx, actor.TenantID, workspaceID, *clientID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targetPlatformIDs, err := s.loadMonitoringClientPlatformIDs(ctx, actor.TenantID, workspaceID, *clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
platforms := resolveMonitoringPlatforms(targetPlatformIDs)
|
||||
platforms := resolveMonitoringPlatforms(quota.EnabledPlatforms)
|
||||
platforms, err = intersectMonitoringPlatforms(platforms, options.PlatformIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -714,11 +702,38 @@ func (s *MonitoringService) CollectNow(
|
||||
}
|
||||
questionIDs := configuredQuestionIDs(configuredQuestions)
|
||||
platformIDs := monitoringPlatformIDs(platforms)
|
||||
targetClientIDsByPlatform, err := s.loadRandomOnlineMonitoringClientIDsByPlatformForUser(ctx, actor.TenantID, workspaceID, actor.UserID, platformIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
platforms = filterMonitoringPlatformsByIDSet(platforms, platformIDsFromClientTargetMap(targetClientIDsByPlatform))
|
||||
if len(platforms) == 0 {
|
||||
return nil, response.ErrConflict(40941, "desktop_client_offline", "current user's monitoring desktop client is offline for selected platforms")
|
||||
}
|
||||
platformIDs = monitoringPlatformIDs(platforms)
|
||||
dispatchTargetClientIDs := collectNowDispatchTargetClientIDs(targetClientIDsByPlatform, nil)
|
||||
executionOwner := "legacy"
|
||||
if s.shouldUseDesktopTasksExecution(actor.TenantID) {
|
||||
executionOwner = "desktop_tasks"
|
||||
}
|
||||
|
||||
clientID, err := s.resolveCollectNowClientID(
|
||||
ctx,
|
||||
actor.TenantID,
|
||||
workspaceID,
|
||||
actor.UserID,
|
||||
quota.PrimaryClientID,
|
||||
options.TargetClientID,
|
||||
platformIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clientID == nil {
|
||||
return nil, response.ErrConflict(40941, "desktop_client_offline", "current user's monitoring desktop client is offline")
|
||||
}
|
||||
dispatchTargetClientIDs = collectNowDispatchTargetClientIDs(targetClientIDsByPlatform, clientID)
|
||||
|
||||
now := time.Now().UTC()
|
||||
todayTime, _ := monitoringBusinessDayAndDateAt(now)
|
||||
requestID := uuid.New()
|
||||
@@ -749,32 +764,30 @@ func (s *MonitoringService) CollectNow(
|
||||
return nil, err
|
||||
}
|
||||
if existingRequest != nil {
|
||||
if strings.TrimSpace(existingRequest.TargetClientID) == clientID.String() {
|
||||
base := buildCollectNowResponseFromState(quota.CollectionMode, existingRequest)
|
||||
executing, execErr := s.hasExecutingCollectNowTask(ctx, existingRequest.RequestID)
|
||||
if execErr != nil && s.logger != nil {
|
||||
s.logger.Warn("collect-now executing-state inspection failed",
|
||||
zap.String("request_id", existingRequest.RequestID.String()),
|
||||
zap.Error(execErr),
|
||||
)
|
||||
}
|
||||
if executing {
|
||||
if base == nil {
|
||||
base = &MonitoringCollectNowResponse{CollectionMode: quota.CollectionMode}
|
||||
}
|
||||
base.Message = "任务正在执行中,请等待"
|
||||
_ = tx.Rollback(ctx)
|
||||
return base, nil
|
||||
base := buildCollectNowResponseFromState(quota.CollectionMode, existingRequest)
|
||||
executing, execErr := s.hasExecutingCollectNowTask(ctx, existingRequest.RequestID)
|
||||
if execErr != nil && s.logger != nil {
|
||||
s.logger.Warn("collect-now executing-state inspection failed",
|
||||
zap.String("request_id", existingRequest.RequestID.String()),
|
||||
zap.Error(execErr),
|
||||
)
|
||||
}
|
||||
if executing {
|
||||
if base == nil {
|
||||
base = &MonitoringCollectNowResponse{CollectionMode: quota.CollectionMode}
|
||||
}
|
||||
base.Message = "任务正在执行中,请等待"
|
||||
_ = tx.Rollback(ctx)
|
||||
return base, nil
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE monitoring_collect_requests
|
||||
SET superseded_by_request_id = $2,
|
||||
status = CASE
|
||||
WHEN status IN ('accepted', 'dispatching') THEN 'superseded'
|
||||
ELSE status
|
||||
END,
|
||||
UPDATE monitoring_collect_requests
|
||||
SET superseded_by_request_id = $2,
|
||||
status = CASE
|
||||
WHEN status IN ('accepted', 'dispatching') THEN 'superseded'
|
||||
ELSE status
|
||||
END,
|
||||
updated_at = NOW()
|
||||
WHERE request_id = $1
|
||||
AND superseded_by_request_id IS NULL
|
||||
@@ -791,7 +804,7 @@ func (s *MonitoringService) CollectNow(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
interruptGeneration, err := s.nextCollectNowInterruptGeneration(ctx, tx, *clientID, executionOwner)
|
||||
interruptGeneration, err := s.nextCollectNowInterruptGeneration(ctx, tx, dispatchTargetClientIDs, executionOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -804,6 +817,7 @@ func (s *MonitoringService) CollectNow(
|
||||
configuredQuestions,
|
||||
platforms,
|
||||
todayTime,
|
||||
targetClientIDsByPlatform,
|
||||
*clientID,
|
||||
interruptGeneration,
|
||||
executionOwner,
|
||||
@@ -823,6 +837,7 @@ func (s *MonitoringService) CollectNow(
|
||||
questionIDs,
|
||||
platformIDs,
|
||||
*clientID,
|
||||
actor.UserID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -830,29 +845,33 @@ func (s *MonitoringService) CollectNow(
|
||||
}
|
||||
abortedQueuedCount := int64(0)
|
||||
if options.Preempt {
|
||||
abortedQueuedCount, err = s.deferQueuedNormalTasks(
|
||||
ctx,
|
||||
tx,
|
||||
actor.TenantID,
|
||||
todayTime,
|
||||
brand.ID,
|
||||
configuredQuestionIDs(configuredQuestions),
|
||||
monitoringPlatformIDs(platforms),
|
||||
*clientID,
|
||||
requestID,
|
||||
ttlExpiresAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for _, targetClientID := range dispatchTargetClientIDs {
|
||||
deferredCount, deferErr := s.deferQueuedNormalTasks(
|
||||
ctx,
|
||||
tx,
|
||||
actor.TenantID,
|
||||
todayTime,
|
||||
brand.ID,
|
||||
configuredQuestionIDs(configuredQuestions),
|
||||
monitoringPlatformIDs(platforms),
|
||||
targetClientID,
|
||||
requestID,
|
||||
ttlExpiresAt,
|
||||
)
|
||||
if deferErr != nil {
|
||||
return nil, deferErr
|
||||
}
|
||||
abortedQueuedCount += deferredCount
|
||||
}
|
||||
}
|
||||
|
||||
requestScopeJSON, err := json.Marshal(map[string]any{
|
||||
"brand_id": brand.ID,
|
||||
"keyword_id": keywordID,
|
||||
"question_ids": questionIDs,
|
||||
"platform_ids": platformIDs,
|
||||
"preempt": options.Preempt,
|
||||
"brand_id": brand.ID,
|
||||
"keyword_id": keywordID,
|
||||
"question_ids": questionIDs,
|
||||
"platform_ids": platformIDs,
|
||||
"target_client_ids": uuidStrings(dispatchTargetClientIDs),
|
||||
"preempt": options.Preempt,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "payload_encode_failed", "failed to encode collect-now request scope")
|
||||
@@ -886,14 +905,14 @@ func (s *MonitoringService) CollectNow(
|
||||
tx,
|
||||
requestID,
|
||||
workspaceID,
|
||||
*clientID,
|
||||
dispatchTargetClientIDs,
|
||||
affectedTaskCount,
|
||||
interruptGeneration,
|
||||
func() []int64 {
|
||||
if !options.Preempt {
|
||||
return nil
|
||||
}
|
||||
return interruptTaskIDs
|
||||
return collectNowInterruptTaskIDs(interruptTaskIDs)
|
||||
}(),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -937,12 +956,38 @@ func (s *MonitoringService) CollectNow(
|
||||
for _, spec := range phase2TaskSpecs {
|
||||
excludedMonitorTaskIDs = append(excludedMonitorTaskIDs, spec.MonitorTaskID)
|
||||
}
|
||||
phase2DeferredTasks, err = s.deferQueuedPhase2MonitorTasks(ctx, *clientID, excludedMonitorTaskIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
phase2TargetClientIDs := desktopTaskClientIDs(phase2PublishedTasks)
|
||||
if len(phase2TargetClientIDs) == 0 {
|
||||
phase2TargetClientIDs = []uuid.UUID{*clientID}
|
||||
}
|
||||
for _, phase2TargetClientID := range phase2TargetClientIDs {
|
||||
deferredTasks, deferErr := s.deferQueuedPhase2MonitorTasks(ctx, phase2TargetClientID, excludedMonitorTaskIDs)
|
||||
if deferErr != nil {
|
||||
return nil, deferErr
|
||||
}
|
||||
phase2DeferredTasks = append(phase2DeferredTasks, deferredTasks...)
|
||||
interruptTargets, interruptErr := s.requestPhase2MonitorInterrupts(ctx, phase2TargetClientID, excludedMonitorTaskIDs, interruptGeneration)
|
||||
if interruptErr != nil {
|
||||
return nil, interruptErr
|
||||
}
|
||||
phase2InterruptTargets = append(phase2InterruptTargets, interruptTargets...)
|
||||
}
|
||||
if len(phase2DeferredTasks) > 0 {
|
||||
abortedQueuedCount += int64(len(phase2DeferredTasks))
|
||||
}
|
||||
if len(phase2InterruptTargets) > 0 {
|
||||
// Keep collect-now request counters in sync after per-platform dispatch
|
||||
// fan-out chooses more than one desktop client.
|
||||
if _, updateErr := s.monitoringPool.Exec(ctx, `
|
||||
UPDATE monitoring_collect_requests
|
||||
SET aborted_queued_count = aborted_queued_count + $2,
|
||||
interrupt_requested_count = interrupt_requested_count + $3,
|
||||
updated_at = NOW()
|
||||
WHERE request_id = $1
|
||||
`, requestID, len(phase2DeferredTasks), len(phase2InterruptTargets)); updateErr != nil {
|
||||
return nil, response.ErrInternal(50041, "request_update_failed", "failed to persist phase2 preempt counts")
|
||||
}
|
||||
} else if len(phase2DeferredTasks) > 0 {
|
||||
if _, updateErr := s.monitoringPool.Exec(ctx, `
|
||||
UPDATE monitoring_collect_requests
|
||||
SET aborted_queued_count = aborted_queued_count + $2,
|
||||
@@ -952,20 +997,6 @@ func (s *MonitoringService) CollectNow(
|
||||
return nil, response.ErrInternal(50041, "request_update_failed", "failed to persist phase2 deferred queue count")
|
||||
}
|
||||
}
|
||||
phase2InterruptTargets, err = s.requestPhase2MonitorInterrupts(ctx, *clientID, excludedMonitorTaskIDs, interruptGeneration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(phase2InterruptTargets) > 0 {
|
||||
if _, updateErr := s.monitoringPool.Exec(ctx, `
|
||||
UPDATE monitoring_collect_requests
|
||||
SET interrupt_requested_count = interrupt_requested_count + $2,
|
||||
updated_at = NOW()
|
||||
WHERE request_id = $1
|
||||
`, requestID, len(phase2InterruptTargets)); updateErr != nil {
|
||||
return nil, response.ErrInternal(50041, "request_update_failed", "failed to persist phase2 interrupt request count")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, task := range phase2PublishedTasks {
|
||||
@@ -1032,13 +1063,27 @@ func (s *MonitoringService) resolveCollectNowClientID(
|
||||
tenantID, workspaceID, userID int64,
|
||||
defaultClientID *uuid.UUID,
|
||||
preferredClientID *uuid.UUID,
|
||||
platformIDs []string,
|
||||
) (*uuid.UUID, error) {
|
||||
if preferredClientID != nil {
|
||||
if err := s.ensureClientBelongsToUser(ctx, tenantID, workspaceID, userID, *preferredClientID); err != nil {
|
||||
if err := s.ensureClientBelongsToUser(ctx, tenantID, workspaceID, userID, *preferredClientID, platformIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preferredClientID, nil
|
||||
}
|
||||
clientID, err := s.findRandomOnlineClientForUserWithMonitoringPlatforms(ctx, tenantID, workspaceID, userID, platformIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clientID != nil {
|
||||
return clientID, nil
|
||||
}
|
||||
if defaultClientID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := s.ensureClientBelongsToUser(ctx, tenantID, workspaceID, userID, *defaultClientID, platformIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return defaultClientID, nil
|
||||
}
|
||||
|
||||
@@ -1050,25 +1095,106 @@ func (s *MonitoringService) hasOnlineClientForUser(ctx context.Context, tenantID
|
||||
return clientID != nil, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) findRandomOnlineClientForUser(
|
||||
ctx context.Context,
|
||||
tenantID, workspaceID, userID int64,
|
||||
) (*uuid.UUID, error) {
|
||||
candidates, err := s.loadOnlineClientIDsForUser(ctx, tenantID, workspaceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return randomUUIDFromSlice(candidates), nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) findRandomOnlineClientForUserWithMonitoringPlatforms(
|
||||
ctx context.Context,
|
||||
tenantID, workspaceID, userID int64,
|
||||
platformIDs []string,
|
||||
) (*uuid.UUID, error) {
|
||||
targets, err := s.loadRandomOnlineMonitoringClientIDsByPlatformForUser(ctx, tenantID, workspaceID, userID, platformIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return randomUUIDFromSlice(collectNowDispatchTargetClientIDs(targets, nil)), nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOnlineMonitoringPlatformIDsForUser(
|
||||
ctx context.Context,
|
||||
tenantID, workspaceID, userID int64,
|
||||
platformIDs []string,
|
||||
) ([]string, error) {
|
||||
onlineClientIDs, err := s.loadOnlineClientIDsForUser(ctx, tenantID, workspaceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(onlineClientIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.loadMonitoringPlatformIDsForClients(ctx, tenantID, workspaceID, userID, onlineClientIDs, platformIDs)
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadRandomOnlineMonitoringClientIDsByPlatformForUser(
|
||||
ctx context.Context,
|
||||
tenantID, workspaceID, userID int64,
|
||||
platformIDs []string,
|
||||
) (map[string]uuid.UUID, error) {
|
||||
platformIDs = reconcileEnabledMonitoringPlatforms(platformIDs)
|
||||
if len(platformIDs) == 0 {
|
||||
platformIDs = monitoringPlatformIDs(defaultMonitoringPlatforms)
|
||||
}
|
||||
onlineClientIDs, err := s.loadOnlineClientIDsForUser(ctx, tenantID, workspaceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(onlineClientIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
candidatesByPlatform, err := s.loadOnlineMonitoringClientIDCandidatesByPlatformForUser(ctx, tenantID, workspaceID, userID, onlineClientIDs, platformIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]uuid.UUID, len(candidatesByPlatform))
|
||||
for platformID, candidates := range candidatesByPlatform {
|
||||
if selected := randomUUIDFromSlice(candidates); selected != nil {
|
||||
result[platformID] = *selected
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) ensureClientBelongsToUser(
|
||||
ctx context.Context,
|
||||
tenantID, workspaceID, userID int64,
|
||||
clientID uuid.UUID,
|
||||
platformIDs []string,
|
||||
) error {
|
||||
var exists bool
|
||||
if err := s.businessPool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM desktop_clients
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND workspace_id = $3
|
||||
AND user_id = $4
|
||||
AND revoked_at IS NULL
|
||||
AND last_seen_at IS NOT NULL
|
||||
AND last_seen_at >= NOW() - $5::interval
|
||||
FROM desktop_clients dc
|
||||
WHERE dc.id = $1
|
||||
AND dc.tenant_id = $2
|
||||
AND dc.workspace_id = $3
|
||||
AND dc.user_id = $4
|
||||
AND dc.revoked_at IS NULL
|
||||
AND dc.last_seen_at IS NOT NULL
|
||||
AND dc.last_seen_at >= NOW() - $5::interval
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM platform_accounts pa
|
||||
WHERE pa.tenant_id = dc.tenant_id
|
||||
AND pa.workspace_id = dc.workspace_id
|
||||
AND pa.user_id = dc.user_id
|
||||
AND pa.client_id = dc.id
|
||||
AND pa.deleted_at IS NULL
|
||||
AND pa.platform_id = ANY($6::text[])
|
||||
)
|
||||
)
|
||||
`, clientID, tenantID, workspaceID, userID, formatPgInterval(desktopClientPresenceTTL)).Scan(&exists); err != nil {
|
||||
`, clientID, tenantID, workspaceID, userID, formatPgInterval(desktopClientPresenceTTL), reconcileEnabledMonitoringPlatforms(platformIDs)).Scan(&exists); err != nil {
|
||||
return response.ErrInternal(50041, "query_failed", "failed to validate target monitoring desktop client")
|
||||
}
|
||||
if !exists {
|
||||
@@ -1080,25 +1206,29 @@ func (s *MonitoringService) ensureClientBelongsToUser(
|
||||
func (s *MonitoringService) nextCollectNowInterruptGeneration(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
clientID uuid.UUID,
|
||||
clientIDs []uuid.UUID,
|
||||
executionOwner string,
|
||||
) (int, error) {
|
||||
var next int
|
||||
clientIDs = uniqueUUIDs(clientIDs)
|
||||
if len(clientIDs) == 0 {
|
||||
return 1, nil
|
||||
}
|
||||
if strings.TrimSpace(executionOwner) == "desktop_tasks" && s.businessPool != nil {
|
||||
if err := s.businessPool.QueryRow(ctx, `
|
||||
SELECT COALESCE(MAX(interrupt_generation), 0) + 1
|
||||
FROM desktop_tasks
|
||||
WHERE kind = 'monitor'
|
||||
AND target_client_id = $1
|
||||
`, clientID).Scan(&next); err != nil {
|
||||
AND target_client_id = ANY($1::uuid[])
|
||||
`, clientIDs).Scan(&next); err != nil {
|
||||
return 0, response.ErrInternal(50041, "query_failed", "failed to allocate collect-now interrupt generation")
|
||||
}
|
||||
} else {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT COALESCE(MAX(interrupt_generation), 0) + 1
|
||||
FROM monitoring_collect_tasks
|
||||
WHERE target_client_id = $1
|
||||
`, clientID).Scan(&next); err != nil {
|
||||
WHERE target_client_id = ANY($1::uuid[])
|
||||
`, clientIDs).Scan(&next); err != nil {
|
||||
return 0, response.ErrInternal(50041, "query_failed", "failed to allocate collect-now interrupt generation")
|
||||
}
|
||||
}
|
||||
@@ -1122,6 +1252,33 @@ func monitoringPlatformIDs(items []monitoringPlatformMetadata) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func filterMonitoringPlatformsByIDSet(items []monitoringPlatformMetadata, platformIDs []string) []monitoringPlatformMetadata {
|
||||
if len(items) == 0 || len(platformIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
enabled := make(map[string]struct{}, len(platformIDs))
|
||||
for _, platformID := range platformIDs {
|
||||
platformID = normalizeMonitoringPlatformID(platformID)
|
||||
if platformID == "" {
|
||||
continue
|
||||
}
|
||||
enabled[platformID] = struct{}{}
|
||||
}
|
||||
if len(enabled) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]monitoringPlatformMetadata, 0, len(items))
|
||||
for _, item := range items {
|
||||
platformID := normalizeMonitoringPlatformID(item.ID)
|
||||
if _, ok := enabled[platformID]; !ok {
|
||||
continue
|
||||
}
|
||||
item.ID = platformID
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func defaultMonitoringPlatformMetadata() []monitoringPlatformMetadata {
|
||||
return append([]monitoringPlatformMetadata(nil), defaultMonitoringPlatforms...)
|
||||
}
|
||||
@@ -1402,6 +1559,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
|
||||
questions []monitoringConfiguredQuestion,
|
||||
platforms []monitoringPlatformMetadata,
|
||||
businessDate time.Time,
|
||||
targetClientIDsByPlatform map[string]uuid.UUID,
|
||||
targetClientID uuid.UUID,
|
||||
interruptGeneration int,
|
||||
executionOwner string,
|
||||
@@ -1418,6 +1576,14 @@ func (s *MonitoringService) ensureCollectNowTasks(
|
||||
|
||||
for _, question := range questions {
|
||||
for _, platform := range platforms {
|
||||
platformID := normalizeMonitoringPlatformID(platform.ID)
|
||||
if platformID == "" {
|
||||
continue
|
||||
}
|
||||
platformTargetClientID, ok := targetClientIDsByPlatform[platformID]
|
||||
if !ok || platformTargetClientID == uuid.Nil {
|
||||
platformTargetClientID = targetClientID
|
||||
}
|
||||
tag, err := tx.Exec(ctx, `
|
||||
UPDATE monitoring_collect_tasks
|
||||
SET question_hash = $7,
|
||||
@@ -1454,7 +1620,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
|
||||
AND run_mode = $6
|
||||
AND business_date = $8::date
|
||||
AND status IN ('pending', 'expired', 'failed', 'completed', 'skipped')
|
||||
`, tenantID, brandID, question.ID, platform.ID, monitoringCollectorType, runMode, question.QuestionHash, dateText, targetClientID, interruptGeneration, executionOwner)
|
||||
`, tenantID, brandID, question.ID, platformID, monitoringCollectorType, runMode, question.QuestionHash, dateText, platformTargetClientID, interruptGeneration, executionOwner)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, nil, response.ErrInternal(50041, "update_failed", "failed to refresh monitoring tasks")
|
||||
}
|
||||
@@ -1487,7 +1653,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
|
||||
AND status = 'leased'
|
||||
AND COALESCE(execution_owner, 'legacy') = 'legacy'
|
||||
RETURNING id
|
||||
`, tenantID, brandID, question.ID, platform.ID, monitoringCollectorType, runMode, question.QuestionHash, dateText, targetClientID, interruptGeneration)
|
||||
`, tenantID, brandID, question.ID, platformID, monitoringCollectorType, runMode, question.QuestionHash, dateText, platformTargetClientID, interruptGeneration)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, nil, response.ErrInternal(50041, "update_failed", "failed to promote leased monitoring tasks")
|
||||
}
|
||||
@@ -1522,7 +1688,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
|
||||
tenant_id, brand_id, question_id, ai_platform_id, collector_type, run_mode, business_date
|
||||
)
|
||||
DO NOTHING
|
||||
`, tenantID, brandID, question.ID, question.QuestionHash, platform.ID, monitoringCollectorType, runMode, dateText, targetClientID, interruptGeneration, fmt.Sprintf("%d:%d:%s", tenantID, brandID, dateText), executionOwner)
|
||||
`, tenantID, brandID, question.ID, question.QuestionHash, platformID, monitoringCollectorType, runMode, dateText, platformTargetClientID, interruptGeneration, fmt.Sprintf("%d:%d:%s", tenantID, brandID, dateText), executionOwner)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, nil, response.ErrInternal(50041, "insert_failed", "failed to create monitoring tasks")
|
||||
}
|
||||
@@ -1594,7 +1760,13 @@ func (s *MonitoringService) loadQuota(ctx context.Context, actor auth.Actor, wor
|
||||
CollectionMode: monitoringCollectorType,
|
||||
}
|
||||
|
||||
clientID, err := s.findLatestRegisteredClientWithMonitoringAccountForUser(ctx, actor.TenantID, workspaceID, actor.UserID)
|
||||
clientID, err := s.findLatestOnlineClientWithMonitoringAccountForUser(ctx, actor.TenantID, workspaceID, actor.UserID)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if clientID == nil {
|
||||
clientID, err = s.findLatestRegisteredClientWithMonitoringAccountForUser(ctx, actor.TenantID, workspaceID, actor.UserID)
|
||||
}
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
@@ -1608,7 +1780,7 @@ func (s *MonitoringService) loadQuota(ctx context.Context, actor auth.Actor, wor
|
||||
if clientID == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
platforms, err := s.loadMonitoringClientPlatformIDs(ctx, actor.TenantID, workspaceID, *clientID)
|
||||
platforms, err := s.loadMonitoringPlatformIDsForUser(ctx, actor.TenantID, workspaceID, actor.UserID)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
@@ -1616,6 +1788,20 @@ func (s *MonitoringService) loadQuota(ctx context.Context, actor auth.Actor, wor
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// findLatestOnlineClientWithMonitoringAccountForUser returns an online client for
|
||||
// dashboard runtime defaults. Collect-now may still randomize among all online
|
||||
// clients that can execute the requested platforms.
|
||||
func (s *MonitoringService) findLatestOnlineClientWithMonitoringAccountForUser(ctx context.Context, tenantID, workspaceID, userID int64) (*uuid.UUID, error) {
|
||||
clientIDs, err := s.loadOnlineClientIDsForUser(ctx, tenantID, workspaceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(clientIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &clientIDs[0], nil
|
||||
}
|
||||
|
||||
// findLatestRegisteredClientWithMonitoringAccountForUser returns the newest
|
||||
// non-revoked desktop client that has at least one bound monitoring platform
|
||||
// account. Platform account health is not authorization.
|
||||
@@ -1673,6 +1859,41 @@ func (s *MonitoringService) findLatestRegisteredClientForUser(ctx context.Contex
|
||||
return &clientID, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadMonitoringPlatformIDsForUser(ctx context.Context, tenantID, workspaceID, userID int64) ([]string, error) {
|
||||
rows, err := s.businessPool.Query(ctx, `
|
||||
SELECT DISTINCT pa.platform_id
|
||||
FROM platform_accounts pa
|
||||
JOIN desktop_clients dc
|
||||
ON dc.id = pa.client_id
|
||||
AND dc.tenant_id = pa.tenant_id
|
||||
AND dc.workspace_id = pa.workspace_id
|
||||
WHERE pa.tenant_id = $1
|
||||
AND pa.workspace_id = $2
|
||||
AND pa.user_id = $3
|
||||
AND pa.deleted_at IS NULL
|
||||
AND pa.platform_id = ANY($4::text[])
|
||||
AND dc.user_id = $3
|
||||
AND dc.revoked_at IS NULL
|
||||
`, tenantID, workspaceID, userID, monitoringPlatformIDs(defaultMonitoringPlatforms))
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load monitoring desktop platform accounts")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
platformIDs := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var platformID string
|
||||
if scanErr := rows.Scan(&platformID); scanErr != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse monitoring desktop platform accounts")
|
||||
}
|
||||
platformIDs = append(platformIDs, platformID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to iterate monitoring desktop platform accounts")
|
||||
}
|
||||
return reconcileEnabledMonitoringPlatforms(platformIDs), nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadMonitoringClientPlatformIDs(ctx context.Context, tenantID, workspaceID int64, clientID uuid.UUID) ([]string, error) {
|
||||
rows, err := s.businessPool.Query(ctx, `
|
||||
SELECT DISTINCT platform_id
|
||||
@@ -1702,6 +1923,230 @@ func (s *MonitoringService) loadMonitoringClientPlatformIDs(ctx context.Context,
|
||||
return reconcileEnabledMonitoringPlatforms(platformIDs), nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadMonitoringPlatformIDsForClients(
|
||||
ctx context.Context,
|
||||
tenantID, workspaceID, userID int64,
|
||||
clientIDs []uuid.UUID,
|
||||
platformIDs []string,
|
||||
) ([]string, error) {
|
||||
clientIDs = uniqueUUIDs(clientIDs)
|
||||
if len(clientIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
platformIDs = reconcileEnabledMonitoringPlatforms(platformIDs)
|
||||
if len(platformIDs) == 0 {
|
||||
platformIDs = monitoringPlatformIDs(defaultMonitoringPlatforms)
|
||||
}
|
||||
|
||||
rows, err := s.businessPool.Query(ctx, `
|
||||
SELECT DISTINCT platform_id
|
||||
FROM platform_accounts
|
||||
WHERE tenant_id = $1
|
||||
AND workspace_id = $2
|
||||
AND user_id = $3
|
||||
AND client_id = ANY($4::uuid[])
|
||||
AND deleted_at IS NULL
|
||||
AND platform_id = ANY($5::text[])
|
||||
`, tenantID, workspaceID, userID, clientIDs, platformIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load online monitoring desktop platform accounts")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var platformID string
|
||||
if scanErr := rows.Scan(&platformID); scanErr != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse online monitoring desktop platform accounts")
|
||||
}
|
||||
result = append(result, platformID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to iterate online monitoring desktop platform accounts")
|
||||
}
|
||||
return reconcileEnabledMonitoringPlatforms(result), nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOnlineMonitoringClientIDCandidatesByPlatformForUser(
|
||||
ctx context.Context,
|
||||
tenantID, workspaceID, userID int64,
|
||||
clientIDs []uuid.UUID,
|
||||
platformIDs []string,
|
||||
) (map[string][]uuid.UUID, error) {
|
||||
clientIDs = uniqueUUIDs(clientIDs)
|
||||
if len(clientIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
platformIDs = reconcileEnabledMonitoringPlatforms(platformIDs)
|
||||
if len(platformIDs) == 0 {
|
||||
platformIDs = monitoringPlatformIDs(defaultMonitoringPlatforms)
|
||||
}
|
||||
|
||||
rows, err := s.businessPool.Query(ctx, `
|
||||
SELECT DISTINCT platform_id, client_id
|
||||
FROM platform_accounts
|
||||
WHERE tenant_id = $1
|
||||
AND workspace_id = $2
|
||||
AND user_id = $3
|
||||
AND client_id = ANY($4::uuid[])
|
||||
AND deleted_at IS NULL
|
||||
AND platform_id = ANY($5::text[])
|
||||
`, tenantID, workspaceID, userID, clientIDs, platformIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load online monitoring desktop platform targets")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[string][]uuid.UUID)
|
||||
for rows.Next() {
|
||||
var (
|
||||
platformID string
|
||||
clientID uuid.UUID
|
||||
)
|
||||
if scanErr := rows.Scan(&platformID, &clientID); scanErr != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse online monitoring desktop platform targets")
|
||||
}
|
||||
platformID = normalizeMonitoringPlatformID(platformID)
|
||||
if platformID == "" || clientID == uuid.Nil {
|
||||
continue
|
||||
}
|
||||
result[platformID] = append(result[platformID], clientID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to iterate online monitoring desktop platform targets")
|
||||
}
|
||||
for platformID, candidates := range result {
|
||||
result[platformID] = uniqueUUIDs(candidates)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOnlineClientIDsForUser(
|
||||
ctx context.Context,
|
||||
tenantID, workspaceID, userID int64,
|
||||
) ([]uuid.UUID, error) {
|
||||
rows, err := s.businessPool.Query(ctx, `
|
||||
SELECT dc.id
|
||||
FROM desktop_clients dc
|
||||
WHERE dc.tenant_id = $1
|
||||
AND dc.workspace_id = $2
|
||||
AND dc.user_id = $3
|
||||
AND dc.revoked_at IS NULL
|
||||
AND dc.last_seen_at IS NOT NULL
|
||||
AND dc.last_seen_at >= NOW() - $4::interval
|
||||
ORDER BY dc.last_seen_at DESC, dc.created_at DESC, dc.id DESC
|
||||
`, tenantID, workspaceID, userID, formatPgInterval(desktopClientPresenceTTL))
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to inspect online monitoring desktop clients")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
clientIDs := make([]uuid.UUID, 0)
|
||||
for rows.Next() {
|
||||
var clientID uuid.UUID
|
||||
if scanErr := rows.Scan(&clientID); scanErr != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse online monitoring desktop clients")
|
||||
}
|
||||
clientIDs = append(clientIDs, clientID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to iterate online monitoring desktop clients")
|
||||
}
|
||||
return clientIDs, nil
|
||||
}
|
||||
|
||||
func randomUUIDFromSlice(values []uuid.UUID) *uuid.UUID {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
selected := values[randomIndex(len(values))]
|
||||
return &selected
|
||||
}
|
||||
|
||||
func randomIndex(length int) int {
|
||||
if length <= 1 {
|
||||
return 0
|
||||
}
|
||||
if value, err := crand.Int(crand.Reader, big.NewInt(int64(length))); err == nil {
|
||||
return int(value.Int64())
|
||||
}
|
||||
return int(time.Now().UnixNano() % int64(length))
|
||||
}
|
||||
|
||||
func platformIDsFromClientTargetMap(targets map[string]uuid.UUID) []string {
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(targets))
|
||||
for _, platform := range defaultMonitoringPlatforms {
|
||||
if clientID, ok := targets[platform.ID]; ok && clientID != uuid.Nil {
|
||||
result = append(result, platform.ID)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func collectNowDispatchTargetClientIDs(targets map[string]uuid.UUID, fallback *uuid.UUID) []uuid.UUID {
|
||||
result := make([]uuid.UUID, 0, len(targets)+1)
|
||||
for _, platform := range defaultMonitoringPlatforms {
|
||||
if clientID, ok := targets[platform.ID]; ok && clientID != uuid.Nil {
|
||||
result = append(result, clientID)
|
||||
}
|
||||
}
|
||||
if fallback != nil && *fallback != uuid.Nil {
|
||||
result = append(result, *fallback)
|
||||
}
|
||||
return uniqueUUIDs(result)
|
||||
}
|
||||
|
||||
func uuidStrings(values []uuid.UUID) []string {
|
||||
values = uniqueUUIDs(values)
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value == uuid.Nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, value.String())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func collectNowInterruptTaskIDs(taskIDs []int64) []int64 {
|
||||
if len(taskIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[int64]struct{}, len(taskIDs))
|
||||
result := make([]int64, 0, len(taskIDs))
|
||||
for _, taskID := range taskIDs {
|
||||
if taskID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[taskID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[taskID] = struct{}{}
|
||||
result = append(result, taskID)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func desktopTaskClientIDs(tasks []*repository.DesktopTask) []uuid.UUID {
|
||||
if len(tasks) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]uuid.UUID, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
if task == nil || task.TargetClientID == uuid.Nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, task.TargetClientID)
|
||||
}
|
||||
return uniqueUUIDs(result)
|
||||
}
|
||||
|
||||
func newMonitoringDerivedMetrics() monitoringDerivedMetrics {
|
||||
return monitoringDerivedMetrics{
|
||||
ByDate: make(map[string]monitoringDerivedRateStats),
|
||||
|
||||
Reference in New Issue
Block a user