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:
@@ -56,6 +56,7 @@ type AIPlatformStatusCard = {
|
|||||||
accent: string
|
accent: string
|
||||||
description: string
|
description: string
|
||||||
account: DesktopAccountInfo | null
|
account: DesktopAccountInfo | null
|
||||||
|
isClientOnline: boolean
|
||||||
sampleStatus: string
|
sampleStatus: string
|
||||||
lastSampledAt: string | null
|
lastSampledAt: string | null
|
||||||
actualSampleCount: number
|
actualSampleCount: number
|
||||||
@@ -327,6 +328,17 @@ const selectedPlatformAuthorized = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedPlatformOnlineAuthorized = computed(() => {
|
||||||
|
const onlinePlatformIDs =
|
||||||
|
dashboardQuery.data.value?.runtime.online_authorized_monitoring_platform_ids ?? []
|
||||||
|
if (selectedPlatformId.value === 'all') {
|
||||||
|
return onlinePlatformIDs.length > 0
|
||||||
|
}
|
||||||
|
return onlinePlatformIDs.some(
|
||||||
|
(item) => normalizeMonitoringPlatformId(item) === selectedPlatformId.value,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const collectNowDisabledReason = computed(() => {
|
const collectNowDisabledReason = computed(() => {
|
||||||
if (!selectedBrandId.value) {
|
if (!selectedBrandId.value) {
|
||||||
return t('tracking.collectBrandRequired')
|
return t('tracking.collectBrandRequired')
|
||||||
@@ -356,6 +368,9 @@ const collectNowDisabledReason = computed(() => {
|
|||||||
if (!currentUserClientOnline.value) {
|
if (!currentUserClientOnline.value) {
|
||||||
return t('tracking.collectClientOnlineRequired')
|
return t('tracking.collectClientOnlineRequired')
|
||||||
}
|
}
|
||||||
|
if (dashboardQuery.data.value && !selectedPlatformOnlineAuthorized.value) {
|
||||||
|
return t('tracking.collectClientOnlineRequired')
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -385,9 +400,19 @@ const aiPlatformStatusCards = computed<AIPlatformStatusCard[]>(() => {
|
|||||||
item,
|
item,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
const onlineAuthorizedPlatformIDs = new Set(
|
||||||
|
(dashboardQuery.data.value?.runtime.online_authorized_monitoring_platform_ids ?? [])
|
||||||
|
.map((item) => normalizeMonitoringPlatformId(item))
|
||||||
|
.filter((item): item is string => Boolean(item)),
|
||||||
|
)
|
||||||
|
|
||||||
return aiPlatformCatalog.map((platform) => {
|
return aiPlatformCatalog.map((platform) => {
|
||||||
const matchedAccounts = [...(accountGroups.get(platform.id) ?? [])].sort((left, right) => {
|
const matchedAccounts = [...(accountGroups.get(platform.id) ?? [])].sort((left, right) => {
|
||||||
|
const leftOnline = left.client_online === true ? 1 : 0
|
||||||
|
const rightOnline = right.client_online === true ? 1 : 0
|
||||||
|
if (leftOnline !== rightOnline) {
|
||||||
|
return rightOnline - leftOnline
|
||||||
|
}
|
||||||
const leftAt = Date.parse(resolveAccountCheckedAt(left) ?? '') || 0
|
const leftAt = Date.parse(resolveAccountCheckedAt(left) ?? '') || 0
|
||||||
const rightAt = Date.parse(resolveAccountCheckedAt(right) ?? '') || 0
|
const rightAt = Date.parse(resolveAccountCheckedAt(right) ?? '') || 0
|
||||||
return rightAt - leftAt
|
return rightAt - leftAt
|
||||||
@@ -401,6 +426,10 @@ const aiPlatformStatusCards = computed<AIPlatformStatusCard[]>(() => {
|
|||||||
accent: platform.accent,
|
accent: platform.accent,
|
||||||
description: platform.description,
|
description: platform.description,
|
||||||
account: matchedAccounts[0] ?? null,
|
account: matchedAccounts[0] ?? null,
|
||||||
|
isClientOnline:
|
||||||
|
onlineAuthorizedPlatformIDs.has(platform.id) ||
|
||||||
|
(!dashboardQuery.data.value &&
|
||||||
|
matchedAccounts.some((account) => account.client_online === true)),
|
||||||
sampleStatus: breakdown?.platform_sample_status ?? 'pending',
|
sampleStatus: breakdown?.platform_sample_status ?? 'pending',
|
||||||
lastSampledAt: breakdown?.last_sampled_at ?? null,
|
lastSampledAt: breakdown?.last_sampled_at ?? null,
|
||||||
actualSampleCount: breakdown?.actual_sample_count ?? 0,
|
actualSampleCount: breakdown?.actual_sample_count ?? 0,
|
||||||
@@ -671,12 +700,12 @@ function accountAuthColor(account: DesktopAccountInfo | null): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function desktopStatusLabel(account: DesktopAccountInfo | null): string {
|
function desktopStatusLabel(account: DesktopAccountInfo | null, isClientOnline: boolean): string {
|
||||||
if (!account?.client_id) {
|
if (!account?.client_id) {
|
||||||
return '未绑定桌面端'
|
return '未绑定桌面端'
|
||||||
}
|
}
|
||||||
|
|
||||||
return account.client_online ? '客户端在线' : '客户端离线'
|
return isClientOnline ? '客户端在线' : '客户端离线'
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCount(value: number | null | undefined): string {
|
function formatCount(value: number | null | undefined): string {
|
||||||
@@ -999,7 +1028,7 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
|||||||
</div>
|
</div>
|
||||||
<div class="ai-platform-status-card__row">
|
<div class="ai-platform-status-card__row">
|
||||||
<span>客户端</span>
|
<span>客户端</span>
|
||||||
<strong>{{ desktopStatusLabel(platform.account) }}</strong>
|
<strong>{{ desktopStatusLabel(platform.account, platform.isClientOnline) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-platform-status-card__row">
|
<div class="ai-platform-status-card__row">
|
||||||
<span>采样状态</span>
|
<span>采样状态</span>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { extractDeepseekIdentityFromPayload } from './account-identity'
|
||||||
|
|
||||||
|
describe('extractDeepseekIdentityFromPayload', () => {
|
||||||
|
it('returns the WeChat profile name + avatar for WECHAT-bound accounts', () => {
|
||||||
|
const wechatPayload = {
|
||||||
|
code: 0,
|
||||||
|
msg: '',
|
||||||
|
data: {
|
||||||
|
biz_code: 0,
|
||||||
|
biz_msg: '',
|
||||||
|
biz_data: {
|
||||||
|
id: '7a15e288-1560-49be-a519-3e3da071071c',
|
||||||
|
email: '',
|
||||||
|
mobile_number: '130******73',
|
||||||
|
area_code: '+86',
|
||||||
|
status: 0,
|
||||||
|
id_profile: {
|
||||||
|
provider: 'WECHAT',
|
||||||
|
id: 'e5a1861a-6c1b-48ae-9d2c-00e643e6b4bd',
|
||||||
|
name: '阿白',
|
||||||
|
picture: 'https://static.deepseek.com/user-avatar/LwJ3Jq-HLkgwCGbl0HS1lECW',
|
||||||
|
locale: 'zh_CN',
|
||||||
|
email: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(extractDeepseekIdentityFromPayload(wechatPayload)).toEqual({
|
||||||
|
displayName: '阿白',
|
||||||
|
avatarUrl: 'https://static.deepseek.com/user-avatar/LwJ3Jq-HLkgwCGbl0HS1lECW',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to mobile_number for phone-only logins (id_profile is null, email is empty string)', () => {
|
||||||
|
const phonePayload = {
|
||||||
|
code: 0,
|
||||||
|
msg: '',
|
||||||
|
data: {
|
||||||
|
biz_code: 0,
|
||||||
|
biz_msg: '',
|
||||||
|
biz_data: {
|
||||||
|
id: 'd782f68b-7358-4290-b891-38cb7a1e11fa',
|
||||||
|
email: '',
|
||||||
|
mobile_number: '177******08',
|
||||||
|
area_code: '+86',
|
||||||
|
status: 0,
|
||||||
|
id_profile: null,
|
||||||
|
id_profiles: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(extractDeepseekIdentityFromPayload(phonePayload)).toEqual({
|
||||||
|
displayName: '177******08',
|
||||||
|
avatarUrl: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prefers id_profile.name over mobile_number when both are present', () => {
|
||||||
|
const payload = {
|
||||||
|
data: {
|
||||||
|
biz_data: {
|
||||||
|
mobile_number: '130******73',
|
||||||
|
id_profile: {
|
||||||
|
provider: 'WECHAT',
|
||||||
|
name: '阿白',
|
||||||
|
picture: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(extractDeepseekIdentityFromPayload(payload).displayName).toBe('阿白')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses email as last-resort fallback when id_profile and mobile_number are empty', () => {
|
||||||
|
const payload = {
|
||||||
|
data: {
|
||||||
|
biz_data: {
|
||||||
|
id_profile: null,
|
||||||
|
mobile_number: '',
|
||||||
|
email: 'user@example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(extractDeepseekIdentityFromPayload(payload).displayName).toBe('user@example.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null displayName when nothing usable is present', () => {
|
||||||
|
expect(
|
||||||
|
extractDeepseekIdentityFromPayload({
|
||||||
|
data: { biz_data: { id_profile: null, email: '', mobile_number: '' } },
|
||||||
|
}),
|
||||||
|
).toEqual({ displayName: null, avatarUrl: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects non-record payloads without throwing', () => {
|
||||||
|
expect(extractDeepseekIdentityFromPayload(null)).toEqual({
|
||||||
|
displayName: null,
|
||||||
|
avatarUrl: null,
|
||||||
|
})
|
||||||
|
expect(extractDeepseekIdentityFromPayload('hello')).toEqual({
|
||||||
|
displayName: null,
|
||||||
|
avatarUrl: null,
|
||||||
|
})
|
||||||
|
expect(extractDeepseekIdentityFromPayload([])).toEqual({
|
||||||
|
displayName: null,
|
||||||
|
avatarUrl: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -14,18 +14,52 @@ function normalizeText(value: unknown): string | null {
|
|||||||
return trimmed || null
|
return trimmed || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickString(value: unknown): string | null {
|
||||||
|
return typeof value === 'string' && value.trim() ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure parser, exported for tests. Both 微信登录 and 手机号登录 must work:
|
||||||
|
// 微信: { biz_data: { id_profile: { provider: 'WECHAT', name: '阿白', picture: '...' }, ... } }
|
||||||
|
// 手机: { biz_data: { id_profile: null, mobile_number: '177******08', email: '', ... } }
|
||||||
|
// Empty-string fields (email = "") are common, so we use truthy checks rather than ??.
|
||||||
|
export function extractDeepseekIdentityFromPayload(
|
||||||
|
payload: unknown,
|
||||||
|
): DeepseekAccountIdentityHints {
|
||||||
|
if (!isRecord(payload)) {
|
||||||
|
return { displayName: null, avatarUrl: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = isRecord(payload.data) ? payload.data : null
|
||||||
|
const bizData = data && isRecord(data.biz_data) ? data.biz_data : null
|
||||||
|
if (!bizData) {
|
||||||
|
return { displayName: null, avatarUrl: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = isRecord(bizData.id_profile) ? bizData.id_profile : null
|
||||||
|
|
||||||
|
const displayName =
|
||||||
|
pickString(profile?.name) ??
|
||||||
|
pickString(bizData.mobile_number) ??
|
||||||
|
pickString(bizData.email)
|
||||||
|
const avatarUrl = pickString(profile?.picture)
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName: normalizeText(displayName),
|
||||||
|
avatarUrl: normalizeText(avatarUrl),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DeepSeek (chat.deepseek.com) 没有 Redux/Pinia/window state,登录态只有 localStorage
|
// DeepSeek (chat.deepseek.com) 没有 Redux/Pinia/window state,登录态只有 localStorage
|
||||||
// 里的 `userToken`(JSON 包了 value 字段)。账号信息要走 /api/v0/users/current,
|
// 里的 `userToken`(JSON 包了 value 字段)。账号信息要走 GET /api/v0/users/current,
|
||||||
// 用 Authorization: Bearer <token> 调。Cookie 不带 token,因此只能在页面上下文里
|
// 用 Authorization: Bearer <token> 调(cookie 不带 token)。
|
||||||
// 执行 fetch(自带 token)。
|
|
||||||
//
|
|
||||||
// 不同登录方式响应差异较大:
|
|
||||||
// 微信登录: id_profile = { provider: 'WECHAT', name: '阿白', picture: '...' }
|
|
||||||
// 手机登录: id_profile = null, mobile_number = '177******08'(其它字段是空串而非 null)
|
|
||||||
// 所以这里 fallback 必须用 truthy 判断而不是 ??,否则空串会击穿后续兜底。
|
|
||||||
//
|
//
|
||||||
// probe 通常在隐藏窗口、刚加载完就跑,React app 可能还没把 userToken 写入 localStorage,
|
// probe 通常在隐藏窗口、刚加载完就跑,React app 可能还没把 userToken 写入 localStorage,
|
||||||
// 所以要 poll 一下直到 token 出现,再调 API。
|
// 所以要 poll 一下直到 token 出现,再调 API。webContents 里的 IIFE 仅拉原始 JSON,
|
||||||
|
// 解析与字段挑选放到 extractDeepseekIdentityFromPayload 里以便单测。
|
||||||
export async function readDeepseekAccountIdentity(
|
export async function readDeepseekAccountIdentity(
|
||||||
webContents: WebContents | null | undefined,
|
webContents: WebContents | null | undefined,
|
||||||
): Promise<DeepseekAccountIdentityHints> {
|
): Promise<DeepseekAccountIdentityHints> {
|
||||||
@@ -33,7 +67,7 @@ export async function readDeepseekAccountIdentity(
|
|||||||
return { displayName: null, avatarUrl: null }
|
return { displayName: null, avatarUrl: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await webContents
|
const rawJson = await webContents
|
||||||
.executeJavaScript(
|
.executeJavaScript(
|
||||||
`(async () => {
|
`(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -64,17 +98,7 @@ export async function readDeepseekAccountIdentity(
|
|||||||
headers: { Authorization: 'Bearer ' + token },
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
});
|
});
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
const payload = await response.json().catch(() => null);
|
return await response.text();
|
||||||
const bizData = payload?.data?.biz_data ?? null;
|
|
||||||
const profile = bizData?.id_profile ?? null;
|
|
||||||
const pickString = (value) =>
|
|
||||||
typeof value === 'string' && value.trim() ? value : null;
|
|
||||||
const name =
|
|
||||||
pickString(profile?.name) ??
|
|
||||||
pickString(bizData?.mobile_number) ??
|
|
||||||
pickString(bizData?.email);
|
|
||||||
const picture = pickString(profile?.picture);
|
|
||||||
return { displayName: name, avatarUrl: picture };
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -83,13 +107,16 @@ export async function readDeepseekAccountIdentity(
|
|||||||
)
|
)
|
||||||
.catch(() => null)
|
.catch(() => null)
|
||||||
|
|
||||||
if (!result || typeof result !== 'object') {
|
if (typeof rawJson !== 'string' || !rawJson) {
|
||||||
return { displayName: null, avatarUrl: null }
|
return { displayName: null, avatarUrl: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
const typed = result as { displayName?: unknown; avatarUrl?: unknown }
|
let payload: unknown
|
||||||
return {
|
try {
|
||||||
displayName: normalizeText(typed.displayName),
|
payload = JSON.parse(rawJson)
|
||||||
avatarUrl: normalizeText(typed.avatarUrl),
|
} catch {
|
||||||
|
return { displayName: null, avatarUrl: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return extractDeepseekIdentityFromPayload(payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1352,6 +1352,7 @@ export interface MonitoringDashboardRuntime {
|
|||||||
platform_authorization_status: MonitoringPlatformAuthorizationStatus
|
platform_authorization_status: MonitoringPlatformAuthorizationStatus
|
||||||
authorized_monitoring_platform_ids: string[]
|
authorized_monitoring_platform_ids: string[]
|
||||||
authorized_monitoring_platform_count: number
|
authorized_monitoring_platform_count: number
|
||||||
|
online_authorized_monitoring_platform_ids?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MonitoringDashboardCompositeResponse {
|
export interface MonitoringDashboardCompositeResponse {
|
||||||
|
|||||||
@@ -46,11 +46,12 @@ type monitoringCollectRequestState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type monitoringCollectRequestScope struct {
|
type monitoringCollectRequestScope struct {
|
||||||
BrandID int64 `json:"brand_id"`
|
BrandID int64 `json:"brand_id"`
|
||||||
KeywordID *int64 `json:"keyword_id"`
|
KeywordID *int64 `json:"keyword_id"`
|
||||||
QuestionIDs []int64 `json:"question_ids"`
|
QuestionIDs []int64 `json:"question_ids"`
|
||||||
PlatformIDs []string `json:"platform_ids"`
|
PlatformIDs []string `json:"platform_ids"`
|
||||||
Preempt bool `json:"preempt"`
|
TargetClientIDs []string `json:"target_client_ids"`
|
||||||
|
Preempt bool `json:"preempt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type monitoringCollectRequestReconcileRow struct {
|
type monitoringCollectRequestReconcileRow struct {
|
||||||
@@ -553,16 +554,18 @@ func (s *MonitoringService) loadCollectNowRequestTaskStats(
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
targetClientID := ""
|
targetClientIDs := normalizeCollectNowScopeTargetClientIDs(scope.TargetClientIDs, request.TargetClientID)
|
||||||
if request.TargetClientID.Valid {
|
|
||||||
targetClientID = strings.TrimSpace(request.TargetClientID.String)
|
|
||||||
}
|
|
||||||
if targetClientID == "" {
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, businessDate := monitoringBusinessDayAndDateAt(request.CreatedAt)
|
_, businessDate := monitoringBusinessDayAndDateAt(request.CreatedAt)
|
||||||
if err := s.monitoringPool.QueryRow(ctx, `
|
queryArgs := []any{
|
||||||
|
request.TenantID,
|
||||||
|
request.BrandID,
|
||||||
|
monitoringCollectorType,
|
||||||
|
businessDate,
|
||||||
|
request.InterruptGeneration,
|
||||||
|
scope.QuestionIDs,
|
||||||
|
scope.PlatformIDs,
|
||||||
|
}
|
||||||
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total_count,
|
COUNT(*)::bigint AS total_count,
|
||||||
COUNT(*) FILTER (WHERE t.status IN ('completed', 'failed', 'skipped', 'expired'))::bigint AS terminal_count,
|
COUNT(*) FILTER (WHERE t.status IN ('completed', 'failed', 'skipped', 'expired'))::bigint AS terminal_count,
|
||||||
@@ -579,11 +582,15 @@ func (s *MonitoringService) loadCollectNowRequestTaskStats(
|
|||||||
AND t.collector_type = $3
|
AND t.collector_type = $3
|
||||||
AND t.trigger_source = 'manual'
|
AND t.trigger_source = 'manual'
|
||||||
AND t.business_date = $4::date
|
AND t.business_date = $4::date
|
||||||
AND t.target_client_id = $5::uuid
|
AND t.interrupt_generation = $5
|
||||||
AND t.interrupt_generation = $6
|
AND t.question_id = ANY($6::bigint[])
|
||||||
AND t.question_id = ANY($7::bigint[])
|
AND t.ai_platform_id = ANY($7::text[])
|
||||||
AND t.ai_platform_id = ANY($8::text[])
|
`
|
||||||
`, request.TenantID, request.BrandID, monitoringCollectorType, businessDate, targetClientID, request.InterruptGeneration, scope.QuestionIDs, scope.PlatformIDs).Scan(
|
if len(targetClientIDs) > 0 {
|
||||||
|
query += ` AND t.target_client_id = ANY($8::uuid[])`
|
||||||
|
queryArgs = append(queryArgs, targetClientIDs)
|
||||||
|
}
|
||||||
|
if err := s.monitoringPool.QueryRow(ctx, query, queryArgs...).Scan(
|
||||||
&stats.Total,
|
&stats.Total,
|
||||||
&stats.Terminal,
|
&stats.Terminal,
|
||||||
&stats.Completed,
|
&stats.Completed,
|
||||||
@@ -614,9 +621,8 @@ func (s *MonitoringService) loadCollectNowRequestTaskStats(
|
|||||||
FROM desktop_tasks d
|
FROM desktop_tasks d
|
||||||
WHERE d.kind = 'monitor'
|
WHERE d.kind = 'monitor'
|
||||||
AND d.status = 'in_progress'
|
AND d.status = 'in_progress'
|
||||||
AND d.target_client_id = $1::uuid
|
AND d.monitor_task_id = ANY($1::bigint[])
|
||||||
AND d.monitor_task_id = ANY($2::bigint[])
|
`, monitorTaskIDs).Scan(
|
||||||
`, targetClientID, monitorTaskIDs).Scan(
|
|
||||||
&stats.ActiveDesktopTaskCount,
|
&stats.ActiveDesktopTaskCount,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return stats, response.ErrInternal(50041, "query_failed", "failed to inspect collect-now desktop task progress")
|
return stats, response.ErrInternal(50041, "query_failed", "failed to inspect collect-now desktop task progress")
|
||||||
@@ -637,16 +643,18 @@ func (s *MonitoringService) loadCollectNowMonitorTaskIDs(
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
targetClientID := ""
|
targetClientIDs := normalizeCollectNowScopeTargetClientIDs(scope.TargetClientIDs, request.TargetClientID)
|
||||||
if request.TargetClientID.Valid {
|
|
||||||
targetClientID = strings.TrimSpace(request.TargetClientID.String)
|
|
||||||
}
|
|
||||||
if targetClientID == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, businessDate := monitoringBusinessDayAndDateAt(request.CreatedAt)
|
_, businessDate := monitoringBusinessDayAndDateAt(request.CreatedAt)
|
||||||
rows, err := s.monitoringPool.Query(ctx, `
|
queryArgs := []any{
|
||||||
|
request.TenantID,
|
||||||
|
request.BrandID,
|
||||||
|
monitoringCollectorType,
|
||||||
|
businessDate,
|
||||||
|
request.InterruptGeneration,
|
||||||
|
scope.QuestionIDs,
|
||||||
|
scope.PlatformIDs,
|
||||||
|
}
|
||||||
|
query := `
|
||||||
SELECT t.id
|
SELECT t.id
|
||||||
FROM monitoring_collect_tasks t
|
FROM monitoring_collect_tasks t
|
||||||
WHERE t.tenant_id = $1
|
WHERE t.tenant_id = $1
|
||||||
@@ -654,11 +662,15 @@ func (s *MonitoringService) loadCollectNowMonitorTaskIDs(
|
|||||||
AND t.collector_type = $3
|
AND t.collector_type = $3
|
||||||
AND t.trigger_source = 'manual'
|
AND t.trigger_source = 'manual'
|
||||||
AND t.business_date = $4::date
|
AND t.business_date = $4::date
|
||||||
AND t.target_client_id = $5::uuid
|
AND t.interrupt_generation = $5
|
||||||
AND t.interrupt_generation = $6
|
AND t.question_id = ANY($6::bigint[])
|
||||||
AND t.question_id = ANY($7::bigint[])
|
AND t.ai_platform_id = ANY($7::text[])
|
||||||
AND t.ai_platform_id = ANY($8::text[])
|
`
|
||||||
`, request.TenantID, request.BrandID, monitoringCollectorType, businessDate, targetClientID, request.InterruptGeneration, scope.QuestionIDs, scope.PlatformIDs)
|
if len(targetClientIDs) > 0 {
|
||||||
|
query += ` AND t.target_client_id = ANY($8::uuid[])`
|
||||||
|
queryArgs = append(queryArgs, targetClientIDs)
|
||||||
|
}
|
||||||
|
rows, err := s.monitoringPool.Query(ctx, query, queryArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load collect-now monitor task ids")
|
return nil, response.ErrInternal(50041, "query_failed", "failed to load collect-now monitor task ids")
|
||||||
}
|
}
|
||||||
@@ -689,6 +701,37 @@ func decodeCollectNowRequestScope(payload []byte) monitoringCollectRequestScope
|
|||||||
return scope
|
return scope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeCollectNowScopeTargetClientIDs(values []string, fallback sql.NullString) []uuid.UUID {
|
||||||
|
result := make([]uuid.UUID, 0, len(values)+1)
|
||||||
|
seen := make(map[string]struct{}, len(values)+1)
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsed, err := uuid.Parse(value)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
result = append(result, parsed)
|
||||||
|
}
|
||||||
|
if fallback.Valid {
|
||||||
|
value := strings.TrimSpace(fallback.String)
|
||||||
|
if value != "" {
|
||||||
|
if parsed, err := uuid.Parse(value); err == nil {
|
||||||
|
if _, exists := seen[value]; !exists {
|
||||||
|
result = append(result, parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeCollectNowStringList(items []string) []string {
|
func normalizeCollectNowStringList(items []string) []string {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -825,7 +868,7 @@ func (s *MonitoringService) enqueueCollectNowOutbox(
|
|||||||
tx pgx.Tx,
|
tx pgx.Tx,
|
||||||
requestID uuid.UUID,
|
requestID uuid.UUID,
|
||||||
workspaceID int64,
|
workspaceID int64,
|
||||||
targetClientID uuid.UUID,
|
targetClientIDs []uuid.UUID,
|
||||||
affectedTaskCount int64,
|
affectedTaskCount int64,
|
||||||
interruptGeneration int,
|
interruptGeneration int,
|
||||||
interruptTaskIDs []int64,
|
interruptTaskIDs []int64,
|
||||||
@@ -834,15 +877,22 @@ func (s *MonitoringService) enqueueCollectNowOutbox(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetClientIDs = uniqueUUIDs(targetClientIDs)
|
||||||
|
if len(targetClientIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if affectedTaskCount > 0 {
|
if affectedTaskCount > 0 {
|
||||||
if _, err := tx.Exec(ctx, `
|
for _, targetClientID := range targetClientIDs {
|
||||||
INSERT INTO monitoring_collect_dispatch_outbox (
|
if _, err := tx.Exec(ctx, `
|
||||||
request_id, workspace_id, target_client_id, event_kind,
|
INSERT INTO monitoring_collect_dispatch_outbox (
|
||||||
lane, priority, interrupt_generation, request_dispatched_count
|
request_id, workspace_id, target_client_id, event_kind,
|
||||||
)
|
lane, priority, interrupt_generation, request_dispatched_count
|
||||||
VALUES ($1, $2, $3, 'dispatch_high', 'high', 5000, $4, $5)
|
)
|
||||||
`, requestID, workspaceID, targetClientID, interruptGeneration, affectedTaskCount); err != nil {
|
VALUES ($1, $2, $3, 'dispatch_high', 'high', 5000, $4, $5)
|
||||||
return response.ErrInternal(50041, "outbox_insert_failed", "failed to enqueue collect-now dispatch signal")
|
`, requestID, workspaceID, targetClientID, interruptGeneration, affectedTaskCount); err != nil {
|
||||||
|
return response.ErrInternal(50041, "outbox_insert_failed", "failed to enqueue collect-now dispatch signal")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,14 +905,16 @@ func (s *MonitoringService) enqueueCollectNowOutbox(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seenTaskIDs[taskID] = struct{}{}
|
seenTaskIDs[taskID] = struct{}{}
|
||||||
if _, err := tx.Exec(ctx, `
|
for _, targetClientID := range targetClientIDs {
|
||||||
INSERT INTO monitoring_collect_dispatch_outbox (
|
if _, err := tx.Exec(ctx, `
|
||||||
request_id, workspace_id, target_client_id, event_kind, task_id,
|
INSERT INTO monitoring_collect_dispatch_outbox (
|
||||||
lane, priority, interrupt_generation, reason
|
request_id, workspace_id, target_client_id, event_kind, task_id,
|
||||||
)
|
lane, priority, interrupt_generation, reason
|
||||||
VALUES ($1, $2, $3, 'interrupt_requested', $4, 'high', 5000, $5, 'collect_now_preempt')
|
)
|
||||||
`, requestID, workspaceID, targetClientID, taskID, interruptGeneration); err != nil {
|
VALUES ($1, $2, $3, 'interrupt_requested', $4, 'high', 5000, $5, 'collect_now_preempt')
|
||||||
return response.ErrInternal(50041, "outbox_insert_failed", "failed to enqueue collect-now interrupt signal")
|
`, requestID, workspaceID, targetClientID, taskID, interruptGeneration); err != nil {
|
||||||
|
return response.ErrInternal(50041, "outbox_insert_failed", "failed to enqueue collect-now interrupt signal")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type monitorDesktopTaskSpec struct {
|
|||||||
Priority int
|
Priority int
|
||||||
Lane string
|
Lane string
|
||||||
InterruptGeneration int
|
InterruptGeneration int
|
||||||
|
RequestedByUserID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type monitorDesktopAccountCandidate struct {
|
type monitorDesktopAccountCandidate struct {
|
||||||
@@ -46,6 +47,7 @@ type monitorDesktopAccountCandidate struct {
|
|||||||
AccountID uuid.UUID
|
AccountID uuid.UUID
|
||||||
DBClientID *uuid.UUID
|
DBClientID *uuid.UUID
|
||||||
HealthRank int
|
HealthRank int
|
||||||
|
OnlineRank int
|
||||||
RecordOrder int
|
RecordOrder int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +109,7 @@ func (s *MonitoringService) loadMonitorDesktopTaskSpecs(
|
|||||||
questionIDs []int64,
|
questionIDs []int64,
|
||||||
platformIDs []string,
|
platformIDs []string,
|
||||||
targetClientID uuid.UUID,
|
targetClientID uuid.UUID,
|
||||||
|
requestedByUserID int64,
|
||||||
) ([]monitorDesktopTaskSpec, error) {
|
) ([]monitorDesktopTaskSpec, error) {
|
||||||
if q == nil || len(questionIDs) == 0 || len(platformIDs) == 0 {
|
if q == nil || len(questionIDs) == 0 || len(platformIDs) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -142,11 +145,10 @@ func (s *MonitoringService) loadMonitorDesktopTaskSpecs(
|
|||||||
AND t.business_date = $3::date
|
AND t.business_date = $3::date
|
||||||
AND t.status = 'pending'
|
AND t.status = 'pending'
|
||||||
AND COALESCE(t.execution_owner, 'legacy') = 'desktop_tasks'
|
AND COALESCE(t.execution_owner, 'legacy') = 'desktop_tasks'
|
||||||
AND t.target_client_id = $4
|
AND t.question_id = ANY($4)
|
||||||
AND t.question_id = ANY($5)
|
AND t.ai_platform_id = ANY($5)
|
||||||
AND t.ai_platform_id = ANY($6)
|
|
||||||
ORDER BY t.dispatch_priority DESC, t.id ASC
|
ORDER BY t.dispatch_priority DESC, t.id ASC
|
||||||
`, tenantID, monitoringCollectorType, businessDate.Format("2006-01-02"), targetClientID, questionIDs, platformIDs)
|
`, tenantID, monitoringCollectorType, businessDate.Format("2006-01-02"), questionIDs, platformIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load phase2 monitor desktop task specs")
|
return nil, response.ErrInternal(50041, "query_failed", "failed to load phase2 monitor desktop task specs")
|
||||||
}
|
}
|
||||||
@@ -177,6 +179,7 @@ func (s *MonitoringService) loadMonitorDesktopTaskSpecs(
|
|||||||
}
|
}
|
||||||
spec.WorkspaceID = workspaceID
|
spec.WorkspaceID = workspaceID
|
||||||
spec.TargetClientID = targetClientID
|
spec.TargetClientID = targetClientID
|
||||||
|
spec.RequestedByUserID = requestedByUserID
|
||||||
spec.BusinessDate = businessDay.Format("2006-01-02")
|
spec.BusinessDate = businessDay.Format("2006-01-02")
|
||||||
spec.QuestionHash = encodeQuestionHash(questionHash)
|
spec.QuestionHash = encodeQuestionHash(questionHash)
|
||||||
spec.SchedulerGroupKey = monitoringSchedulerGroupKey(spec.QuestionHash, spec.QuestionID, spec.QuestionText)
|
spec.SchedulerGroupKey = monitoringSchedulerGroupKey(spec.QuestionHash, spec.QuestionID, spec.QuestionText)
|
||||||
@@ -196,7 +199,7 @@ func (s *MonitoringService) dispatchMonitorDesktopTasks(
|
|||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
targets, err := s.loadMonitorDesktopTaskTargets(ctx, specs[0].TenantID, specs[0].WorkspaceID, specs)
|
targets, err := s.loadMonitorDesktopTaskTargets(ctx, specs[0].TenantID, specs[0].WorkspaceID, specs[0].RequestedByUserID, specs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -240,7 +243,7 @@ func (s *MonitoringService) dispatchMonitorDesktopTasks(
|
|||||||
|
|
||||||
func (s *MonitoringService) loadMonitorDesktopTaskTargets(
|
func (s *MonitoringService) loadMonitorDesktopTaskTargets(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tenantID, workspaceID int64,
|
tenantID, workspaceID, userID int64,
|
||||||
specs []monitorDesktopTaskSpec,
|
specs []monitorDesktopTaskSpec,
|
||||||
) (map[string]monitorDesktopTaskTarget, error) {
|
) (map[string]monitorDesktopTaskTarget, error) {
|
||||||
platformIDs := uniqueMonitorDesktopTaskPlatformIDs(specs)
|
platformIDs := uniqueMonitorDesktopTaskPlatformIDs(specs)
|
||||||
@@ -253,6 +256,20 @@ func (s *MonitoringService) loadMonitorDesktopTaskTargets(
|
|||||||
platform_id,
|
platform_id,
|
||||||
desktop_id,
|
desktop_id,
|
||||||
COALESCE(client_id::text, '') AS client_id,
|
COALESCE(client_id::text, '') AS client_id,
|
||||||
|
CASE
|
||||||
|
WHEN client_id IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM desktop_clients dc
|
||||||
|
WHERE dc.id = platform_accounts.client_id
|
||||||
|
AND dc.workspace_id = platform_accounts.workspace_id
|
||||||
|
AND dc.revoked_at IS NULL
|
||||||
|
AND dc.last_seen_at IS NOT NULL
|
||||||
|
AND dc.last_seen_at >= NOW() - $5::interval
|
||||||
|
)
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END AS online_rank,
|
||||||
CASE health
|
CASE health
|
||||||
WHEN 'live' THEN 0
|
WHEN 'live' THEN 0
|
||||||
WHEN 'risk' THEN 1
|
WHEN 'risk' THEN 1
|
||||||
@@ -264,7 +281,22 @@ func (s *MonitoringService) loadMonitorDesktopTaskTargets(
|
|||||||
AND workspace_id = $2
|
AND workspace_id = $2
|
||||||
AND deleted_at IS NULL
|
AND deleted_at IS NULL
|
||||||
AND platform_id = ANY($3::text[])
|
AND platform_id = ANY($3::text[])
|
||||||
|
AND ($4::bigint = 0 OR user_id = $4)
|
||||||
ORDER BY platform_id ASC,
|
ORDER BY platform_id ASC,
|
||||||
|
CASE
|
||||||
|
WHEN client_id IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM desktop_clients dc
|
||||||
|
WHERE dc.id = platform_accounts.client_id
|
||||||
|
AND dc.workspace_id = platform_accounts.workspace_id
|
||||||
|
AND dc.revoked_at IS NULL
|
||||||
|
AND dc.last_seen_at IS NOT NULL
|
||||||
|
AND dc.last_seen_at >= NOW() - $5::interval
|
||||||
|
)
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END ASC,
|
||||||
CASE health
|
CASE health
|
||||||
WHEN 'live' THEN 0
|
WHEN 'live' THEN 0
|
||||||
WHEN 'risk' THEN 1
|
WHEN 'risk' THEN 1
|
||||||
@@ -274,7 +306,7 @@ func (s *MonitoringService) loadMonitorDesktopTaskTargets(
|
|||||||
verified_at DESC NULLS LAST,
|
verified_at DESC NULLS LAST,
|
||||||
updated_at DESC,
|
updated_at DESC,
|
||||||
created_at DESC
|
created_at DESC
|
||||||
`, tenantID, workspaceID, platformIDs)
|
`, tenantID, workspaceID, platformIDs, userID, formatPgInterval(desktopClientPresenceTTL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, response.ErrInternal(50117, "desktop_account_lookup_failed", "failed to resolve monitor desktop accounts")
|
return nil, response.ErrInternal(50117, "desktop_account_lookup_failed", "failed to resolve monitor desktop accounts")
|
||||||
}
|
}
|
||||||
@@ -286,7 +318,7 @@ func (s *MonitoringService) loadMonitorDesktopTaskTargets(
|
|||||||
item monitorDesktopAccountCandidate
|
item monitorDesktopAccountCandidate
|
||||||
clientIDText string
|
clientIDText string
|
||||||
)
|
)
|
||||||
if scanErr := rows.Scan(&item.PlatformID, &item.AccountID, &clientIDText, &item.HealthRank); scanErr != nil {
|
if scanErr := rows.Scan(&item.PlatformID, &item.AccountID, &clientIDText, &item.OnlineRank, &item.HealthRank); scanErr != nil {
|
||||||
return nil, response.ErrInternal(50117, "desktop_account_lookup_failed", "failed to parse monitor desktop accounts")
|
return nil, response.ErrInternal(50117, "desktop_account_lookup_failed", "failed to parse monitor desktop accounts")
|
||||||
}
|
}
|
||||||
if parsedClientID, parseErr := uuid.Parse(strings.TrimSpace(clientIDText)); parseErr == nil {
|
if parsedClientID, parseErr := uuid.Parse(strings.TrimSpace(clientIDText)); parseErr == nil {
|
||||||
@@ -401,23 +433,28 @@ func selectMonitorDesktopTaskTargets(
|
|||||||
onlineClients map[uuid.UUID]bool,
|
onlineClients map[uuid.UUID]bool,
|
||||||
) map[string]monitorDesktopTaskTarget {
|
) map[string]monitorDesktopTaskTarget {
|
||||||
result := make(map[string]monitorDesktopTaskTarget)
|
result := make(map[string]monitorDesktopTaskTarget)
|
||||||
|
targetsByPlatform := make(map[string][]monitorDesktopTaskTarget)
|
||||||
for _, candidate := range candidates {
|
for _, candidate := range candidates {
|
||||||
platformID := normalizeMonitoringPlatformID(candidate.PlatformID)
|
platformID := normalizeMonitoringPlatformID(candidate.PlatformID)
|
||||||
if platformID == "" || candidate.AccountID == uuid.Nil {
|
if platformID == "" || candidate.AccountID == uuid.Nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, exists := result[platformID]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if clientID, ok := accountPresence[candidate.AccountID]; ok && clientID != uuid.Nil && onlineClients[clientID] {
|
if clientID, ok := accountPresence[candidate.AccountID]; ok && clientID != uuid.Nil && onlineClients[clientID] {
|
||||||
result[platformID] = monitorDesktopTaskTarget{AccountID: candidate.AccountID, ClientID: clientID}
|
targetsByPlatform[platformID] = append(targetsByPlatform[platformID], monitorDesktopTaskTarget{AccountID: candidate.AccountID, ClientID: clientID})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if candidate.DBClientID != nil && *candidate.DBClientID != uuid.Nil && onlineClients[*candidate.DBClientID] {
|
if candidate.DBClientID != nil && *candidate.DBClientID != uuid.Nil && onlineClients[*candidate.DBClientID] {
|
||||||
result[platformID] = monitorDesktopTaskTarget{AccountID: candidate.AccountID, ClientID: *candidate.DBClientID}
|
targetsByPlatform[platformID] = append(targetsByPlatform[platformID], monitorDesktopTaskTarget{AccountID: candidate.AccountID, ClientID: *candidate.DBClientID})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for platformID, targets := range targetsByPlatform {
|
||||||
|
if len(targets) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
index := randomIndex(len(targets))
|
||||||
|
result[platformID] = targets[index]
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,3 +76,31 @@ func TestSelectMonitorDesktopTaskTargetsSkipsOfflineClients(t *testing.T) {
|
|||||||
|
|
||||||
assert.Empty(t, targets)
|
assert.Empty(t, targets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelectMonitorDesktopTaskTargetsRandomizesAmongOnlineCandidates(t *testing.T) {
|
||||||
|
clientIDs := []uuid.UUID{
|
||||||
|
mustParseDailyMonitoringUUID(t, "00000000-0000-0000-0000-000000000301"),
|
||||||
|
mustParseDailyMonitoringUUID(t, "00000000-0000-0000-0000-000000000302"),
|
||||||
|
}
|
||||||
|
accountIDs := []uuid.UUID{
|
||||||
|
mustParseDailyMonitoringUUID(t, "00000000-0000-0000-0000-000000000401"),
|
||||||
|
mustParseDailyMonitoringUUID(t, "00000000-0000-0000-0000-000000000402"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 40; i++ {
|
||||||
|
targets := selectMonitorDesktopTaskTargets(
|
||||||
|
[]monitorDesktopAccountCandidate{
|
||||||
|
{PlatformID: "qwen", AccountID: accountIDs[0], DBClientID: &clientIDs[0]},
|
||||||
|
{PlatformID: "qwen", AccountID: accountIDs[1], DBClientID: &clientIDs[1]},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
map[uuid.UUID]bool{
|
||||||
|
clientIDs[0]: true,
|
||||||
|
clientIDs[1]: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
target := targets["qwen"]
|
||||||
|
assert.Contains(t, clientIDs, target.ClientID)
|
||||||
|
assert.Contains(t, accountIDs, target.AccountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package app
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
|
crand "crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -115,10 +117,11 @@ type MonitoringDashboardWindow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitoringDashboardRuntime struct {
|
type MonitoringDashboardRuntime struct {
|
||||||
CurrentUserClientOnline bool `json:"current_user_client_online"`
|
CurrentUserClientOnline bool `json:"current_user_client_online"`
|
||||||
PlatformAuthorizationStatus string `json:"platform_authorization_status"`
|
PlatformAuthorizationStatus string `json:"platform_authorization_status"`
|
||||||
AuthorizedMonitoringPlatformIDs []string `json:"authorized_monitoring_platform_ids"`
|
AuthorizedMonitoringPlatformIDs []string `json:"authorized_monitoring_platform_ids"`
|
||||||
AuthorizedMonitoringPlatformCount int `json:"authorized_monitoring_platform_count"`
|
AuthorizedMonitoringPlatformCount int `json:"authorized_monitoring_platform_count"`
|
||||||
|
OnlineAuthorizedMonitoringPlatformIDs []string `json:"online_authorized_monitoring_platform_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitoringOverview struct {
|
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) {
|
func (s *MonitoringService) loadDashboardRuntime(ctx context.Context, actor auth.Actor, workspaceID int64, quota monitoringQuotaConfig) (MonitoringDashboardRuntime, error) {
|
||||||
online := false
|
online, err := s.hasOnlineClientForUser(ctx, actor.TenantID, workspaceID, actor.UserID)
|
||||||
if quota.PrimaryClientID != nil {
|
if err != nil {
|
||||||
var err error
|
return MonitoringDashboardRuntime{}, err
|
||||||
online, err = s.isClientOnline(ctx, actor.TenantID, workspaceID, *quota.PrimaryClientID)
|
}
|
||||||
if err != nil {
|
onlinePlatformIDs, err := s.loadOnlineMonitoringPlatformIDsForUser(ctx, actor.TenantID, workspaceID, actor.UserID, quota.EnabledPlatforms)
|
||||||
return MonitoringDashboardRuntime{}, err
|
if err != nil {
|
||||||
}
|
return MonitoringDashboardRuntime{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return MonitoringDashboardRuntime{
|
return MonitoringDashboardRuntime{
|
||||||
CurrentUserClientOnline: online,
|
CurrentUserClientOnline: online,
|
||||||
PlatformAuthorizationStatus: monitoringPlatformAuthorizationStatus(quota),
|
PlatformAuthorizationStatus: monitoringPlatformAuthorizationStatus(quota),
|
||||||
AuthorizedMonitoringPlatformIDs: append([]string(nil), quota.EnabledPlatforms...),
|
AuthorizedMonitoringPlatformIDs: append([]string(nil), quota.EnabledPlatforms...),
|
||||||
AuthorizedMonitoringPlatformCount: len(quota.EnabledPlatforms),
|
AuthorizedMonitoringPlatformCount: len(quota.EnabledPlatforms),
|
||||||
|
OnlineAuthorizedMonitoringPlatformIDs: onlinePlatformIDs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,23 +684,7 @@ func (s *MonitoringService) CollectNow(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
clientID, err := s.resolveCollectNowClientID(ctx, actor.TenantID, workspaceID, actor.UserID, quota.PrimaryClientID, options.TargetClientID)
|
platforms := resolveMonitoringPlatforms(quota.EnabledPlatforms)
|
||||||
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, err = intersectMonitoringPlatforms(platforms, options.PlatformIDs)
|
platforms, err = intersectMonitoringPlatforms(platforms, options.PlatformIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -714,11 +702,38 @@ func (s *MonitoringService) CollectNow(
|
|||||||
}
|
}
|
||||||
questionIDs := configuredQuestionIDs(configuredQuestions)
|
questionIDs := configuredQuestionIDs(configuredQuestions)
|
||||||
platformIDs := monitoringPlatformIDs(platforms)
|
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"
|
executionOwner := "legacy"
|
||||||
if s.shouldUseDesktopTasksExecution(actor.TenantID) {
|
if s.shouldUseDesktopTasksExecution(actor.TenantID) {
|
||||||
executionOwner = "desktop_tasks"
|
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()
|
now := time.Now().UTC()
|
||||||
todayTime, _ := monitoringBusinessDayAndDateAt(now)
|
todayTime, _ := monitoringBusinessDayAndDateAt(now)
|
||||||
requestID := uuid.New()
|
requestID := uuid.New()
|
||||||
@@ -749,32 +764,30 @@ func (s *MonitoringService) CollectNow(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if existingRequest != nil {
|
if existingRequest != nil {
|
||||||
if strings.TrimSpace(existingRequest.TargetClientID) == clientID.String() {
|
base := buildCollectNowResponseFromState(quota.CollectionMode, existingRequest)
|
||||||
base := buildCollectNowResponseFromState(quota.CollectionMode, existingRequest)
|
executing, execErr := s.hasExecutingCollectNowTask(ctx, existingRequest.RequestID)
|
||||||
executing, execErr := s.hasExecutingCollectNowTask(ctx, existingRequest.RequestID)
|
if execErr != nil && s.logger != nil {
|
||||||
if execErr != nil && s.logger != nil {
|
s.logger.Warn("collect-now executing-state inspection failed",
|
||||||
s.logger.Warn("collect-now executing-state inspection failed",
|
zap.String("request_id", existingRequest.RequestID.String()),
|
||||||
zap.String("request_id", existingRequest.RequestID.String()),
|
zap.Error(execErr),
|
||||||
zap.Error(execErr),
|
)
|
||||||
)
|
}
|
||||||
}
|
if executing {
|
||||||
if executing {
|
if base == nil {
|
||||||
if base == nil {
|
base = &MonitoringCollectNowResponse{CollectionMode: quota.CollectionMode}
|
||||||
base = &MonitoringCollectNowResponse{CollectionMode: quota.CollectionMode}
|
|
||||||
}
|
|
||||||
base.Message = "任务正在执行中,请等待"
|
|
||||||
_ = tx.Rollback(ctx)
|
|
||||||
return base, nil
|
|
||||||
}
|
}
|
||||||
|
base.Message = "任务正在执行中,请等待"
|
||||||
|
_ = tx.Rollback(ctx)
|
||||||
|
return base, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tx.Exec(ctx, `
|
if _, err := tx.Exec(ctx, `
|
||||||
UPDATE monitoring_collect_requests
|
UPDATE monitoring_collect_requests
|
||||||
SET superseded_by_request_id = $2,
|
SET superseded_by_request_id = $2,
|
||||||
status = CASE
|
status = CASE
|
||||||
WHEN status IN ('accepted', 'dispatching') THEN 'superseded'
|
WHEN status IN ('accepted', 'dispatching') THEN 'superseded'
|
||||||
ELSE status
|
ELSE status
|
||||||
END,
|
END,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE request_id = $1
|
WHERE request_id = $1
|
||||||
AND superseded_by_request_id IS NULL
|
AND superseded_by_request_id IS NULL
|
||||||
@@ -791,7 +804,7 @@ func (s *MonitoringService) CollectNow(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
interruptGeneration, err := s.nextCollectNowInterruptGeneration(ctx, tx, *clientID, executionOwner)
|
interruptGeneration, err := s.nextCollectNowInterruptGeneration(ctx, tx, dispatchTargetClientIDs, executionOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -804,6 +817,7 @@ func (s *MonitoringService) CollectNow(
|
|||||||
configuredQuestions,
|
configuredQuestions,
|
||||||
platforms,
|
platforms,
|
||||||
todayTime,
|
todayTime,
|
||||||
|
targetClientIDsByPlatform,
|
||||||
*clientID,
|
*clientID,
|
||||||
interruptGeneration,
|
interruptGeneration,
|
||||||
executionOwner,
|
executionOwner,
|
||||||
@@ -823,6 +837,7 @@ func (s *MonitoringService) CollectNow(
|
|||||||
questionIDs,
|
questionIDs,
|
||||||
platformIDs,
|
platformIDs,
|
||||||
*clientID,
|
*clientID,
|
||||||
|
actor.UserID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -830,29 +845,33 @@ func (s *MonitoringService) CollectNow(
|
|||||||
}
|
}
|
||||||
abortedQueuedCount := int64(0)
|
abortedQueuedCount := int64(0)
|
||||||
if options.Preempt {
|
if options.Preempt {
|
||||||
abortedQueuedCount, err = s.deferQueuedNormalTasks(
|
for _, targetClientID := range dispatchTargetClientIDs {
|
||||||
ctx,
|
deferredCount, deferErr := s.deferQueuedNormalTasks(
|
||||||
tx,
|
ctx,
|
||||||
actor.TenantID,
|
tx,
|
||||||
todayTime,
|
actor.TenantID,
|
||||||
brand.ID,
|
todayTime,
|
||||||
configuredQuestionIDs(configuredQuestions),
|
brand.ID,
|
||||||
monitoringPlatformIDs(platforms),
|
configuredQuestionIDs(configuredQuestions),
|
||||||
*clientID,
|
monitoringPlatformIDs(platforms),
|
||||||
requestID,
|
targetClientID,
|
||||||
ttlExpiresAt,
|
requestID,
|
||||||
)
|
ttlExpiresAt,
|
||||||
if err != nil {
|
)
|
||||||
return nil, err
|
if deferErr != nil {
|
||||||
|
return nil, deferErr
|
||||||
|
}
|
||||||
|
abortedQueuedCount += deferredCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestScopeJSON, err := json.Marshal(map[string]any{
|
requestScopeJSON, err := json.Marshal(map[string]any{
|
||||||
"brand_id": brand.ID,
|
"brand_id": brand.ID,
|
||||||
"keyword_id": keywordID,
|
"keyword_id": keywordID,
|
||||||
"question_ids": questionIDs,
|
"question_ids": questionIDs,
|
||||||
"platform_ids": platformIDs,
|
"platform_ids": platformIDs,
|
||||||
"preempt": options.Preempt,
|
"target_client_ids": uuidStrings(dispatchTargetClientIDs),
|
||||||
|
"preempt": options.Preempt,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, response.ErrInternal(50041, "payload_encode_failed", "failed to encode collect-now request scope")
|
return nil, response.ErrInternal(50041, "payload_encode_failed", "failed to encode collect-now request scope")
|
||||||
@@ -886,14 +905,14 @@ func (s *MonitoringService) CollectNow(
|
|||||||
tx,
|
tx,
|
||||||
requestID,
|
requestID,
|
||||||
workspaceID,
|
workspaceID,
|
||||||
*clientID,
|
dispatchTargetClientIDs,
|
||||||
affectedTaskCount,
|
affectedTaskCount,
|
||||||
interruptGeneration,
|
interruptGeneration,
|
||||||
func() []int64 {
|
func() []int64 {
|
||||||
if !options.Preempt {
|
if !options.Preempt {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return interruptTaskIDs
|
return collectNowInterruptTaskIDs(interruptTaskIDs)
|
||||||
}(),
|
}(),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -937,12 +956,38 @@ func (s *MonitoringService) CollectNow(
|
|||||||
for _, spec := range phase2TaskSpecs {
|
for _, spec := range phase2TaskSpecs {
|
||||||
excludedMonitorTaskIDs = append(excludedMonitorTaskIDs, spec.MonitorTaskID)
|
excludedMonitorTaskIDs = append(excludedMonitorTaskIDs, spec.MonitorTaskID)
|
||||||
}
|
}
|
||||||
phase2DeferredTasks, err = s.deferQueuedPhase2MonitorTasks(ctx, *clientID, excludedMonitorTaskIDs)
|
phase2TargetClientIDs := desktopTaskClientIDs(phase2PublishedTasks)
|
||||||
if err != nil {
|
if len(phase2TargetClientIDs) == 0 {
|
||||||
return nil, err
|
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 {
|
if len(phase2DeferredTasks) > 0 {
|
||||||
abortedQueuedCount += int64(len(phase2DeferredTasks))
|
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, `
|
if _, updateErr := s.monitoringPool.Exec(ctx, `
|
||||||
UPDATE monitoring_collect_requests
|
UPDATE monitoring_collect_requests
|
||||||
SET aborted_queued_count = aborted_queued_count + $2,
|
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")
|
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 {
|
for _, task := range phase2PublishedTasks {
|
||||||
@@ -1032,13 +1063,27 @@ func (s *MonitoringService) resolveCollectNowClientID(
|
|||||||
tenantID, workspaceID, userID int64,
|
tenantID, workspaceID, userID int64,
|
||||||
defaultClientID *uuid.UUID,
|
defaultClientID *uuid.UUID,
|
||||||
preferredClientID *uuid.UUID,
|
preferredClientID *uuid.UUID,
|
||||||
|
platformIDs []string,
|
||||||
) (*uuid.UUID, error) {
|
) (*uuid.UUID, error) {
|
||||||
if preferredClientID != nil {
|
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 nil, err
|
||||||
}
|
}
|
||||||
return preferredClientID, nil
|
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
|
return defaultClientID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1050,25 +1095,106 @@ func (s *MonitoringService) hasOnlineClientForUser(ctx context.Context, tenantID
|
|||||||
return clientID != nil, nil
|
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(
|
func (s *MonitoringService) ensureClientBelongsToUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tenantID, workspaceID, userID int64,
|
tenantID, workspaceID, userID int64,
|
||||||
clientID uuid.UUID,
|
clientID uuid.UUID,
|
||||||
|
platformIDs []string,
|
||||||
) error {
|
) error {
|
||||||
var exists bool
|
var exists bool
|
||||||
if err := s.businessPool.QueryRow(ctx, `
|
if err := s.businessPool.QueryRow(ctx, `
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM desktop_clients
|
FROM desktop_clients dc
|
||||||
WHERE id = $1
|
WHERE dc.id = $1
|
||||||
AND tenant_id = $2
|
AND dc.tenant_id = $2
|
||||||
AND workspace_id = $3
|
AND dc.workspace_id = $3
|
||||||
AND user_id = $4
|
AND dc.user_id = $4
|
||||||
AND revoked_at IS NULL
|
AND dc.revoked_at IS NULL
|
||||||
AND last_seen_at IS NOT NULL
|
AND dc.last_seen_at IS NOT NULL
|
||||||
AND last_seen_at >= NOW() - $5::interval
|
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")
|
return response.ErrInternal(50041, "query_failed", "failed to validate target monitoring desktop client")
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -1080,25 +1206,29 @@ func (s *MonitoringService) ensureClientBelongsToUser(
|
|||||||
func (s *MonitoringService) nextCollectNowInterruptGeneration(
|
func (s *MonitoringService) nextCollectNowInterruptGeneration(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx pgx.Tx,
|
tx pgx.Tx,
|
||||||
clientID uuid.UUID,
|
clientIDs []uuid.UUID,
|
||||||
executionOwner string,
|
executionOwner string,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
var next int
|
var next int
|
||||||
|
clientIDs = uniqueUUIDs(clientIDs)
|
||||||
|
if len(clientIDs) == 0 {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
if strings.TrimSpace(executionOwner) == "desktop_tasks" && s.businessPool != nil {
|
if strings.TrimSpace(executionOwner) == "desktop_tasks" && s.businessPool != nil {
|
||||||
if err := s.businessPool.QueryRow(ctx, `
|
if err := s.businessPool.QueryRow(ctx, `
|
||||||
SELECT COALESCE(MAX(interrupt_generation), 0) + 1
|
SELECT COALESCE(MAX(interrupt_generation), 0) + 1
|
||||||
FROM desktop_tasks
|
FROM desktop_tasks
|
||||||
WHERE kind = 'monitor'
|
WHERE kind = 'monitor'
|
||||||
AND target_client_id = $1
|
AND target_client_id = ANY($1::uuid[])
|
||||||
`, clientID).Scan(&next); err != nil {
|
`, clientIDs).Scan(&next); err != nil {
|
||||||
return 0, response.ErrInternal(50041, "query_failed", "failed to allocate collect-now interrupt generation")
|
return 0, response.ErrInternal(50041, "query_failed", "failed to allocate collect-now interrupt generation")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := tx.QueryRow(ctx, `
|
if err := tx.QueryRow(ctx, `
|
||||||
SELECT COALESCE(MAX(interrupt_generation), 0) + 1
|
SELECT COALESCE(MAX(interrupt_generation), 0) + 1
|
||||||
FROM monitoring_collect_tasks
|
FROM monitoring_collect_tasks
|
||||||
WHERE target_client_id = $1
|
WHERE target_client_id = ANY($1::uuid[])
|
||||||
`, clientID).Scan(&next); err != nil {
|
`, clientIDs).Scan(&next); err != nil {
|
||||||
return 0, response.ErrInternal(50041, "query_failed", "failed to allocate collect-now interrupt generation")
|
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
|
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 {
|
func defaultMonitoringPlatformMetadata() []monitoringPlatformMetadata {
|
||||||
return append([]monitoringPlatformMetadata(nil), defaultMonitoringPlatforms...)
|
return append([]monitoringPlatformMetadata(nil), defaultMonitoringPlatforms...)
|
||||||
}
|
}
|
||||||
@@ -1402,6 +1559,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
|
|||||||
questions []monitoringConfiguredQuestion,
|
questions []monitoringConfiguredQuestion,
|
||||||
platforms []monitoringPlatformMetadata,
|
platforms []monitoringPlatformMetadata,
|
||||||
businessDate time.Time,
|
businessDate time.Time,
|
||||||
|
targetClientIDsByPlatform map[string]uuid.UUID,
|
||||||
targetClientID uuid.UUID,
|
targetClientID uuid.UUID,
|
||||||
interruptGeneration int,
|
interruptGeneration int,
|
||||||
executionOwner string,
|
executionOwner string,
|
||||||
@@ -1418,6 +1576,14 @@ func (s *MonitoringService) ensureCollectNowTasks(
|
|||||||
|
|
||||||
for _, question := range questions {
|
for _, question := range questions {
|
||||||
for _, platform := range platforms {
|
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, `
|
tag, err := tx.Exec(ctx, `
|
||||||
UPDATE monitoring_collect_tasks
|
UPDATE monitoring_collect_tasks
|
||||||
SET question_hash = $7,
|
SET question_hash = $7,
|
||||||
@@ -1454,7 +1620,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
|
|||||||
AND run_mode = $6
|
AND run_mode = $6
|
||||||
AND business_date = $8::date
|
AND business_date = $8::date
|
||||||
AND status IN ('pending', 'expired', 'failed', 'completed', 'skipped')
|
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 {
|
if err != nil {
|
||||||
return 0, 0, 0, 0, nil, response.ErrInternal(50041, "update_failed", "failed to refresh monitoring tasks")
|
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 status = 'leased'
|
||||||
AND COALESCE(execution_owner, 'legacy') = 'legacy'
|
AND COALESCE(execution_owner, 'legacy') = 'legacy'
|
||||||
RETURNING id
|
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 {
|
if err != nil {
|
||||||
return 0, 0, 0, 0, nil, response.ErrInternal(50041, "update_failed", "failed to promote leased monitoring tasks")
|
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
|
tenant_id, brand_id, question_id, ai_platform_id, collector_type, run_mode, business_date
|
||||||
)
|
)
|
||||||
DO NOTHING
|
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 {
|
if err != nil {
|
||||||
return 0, 0, 0, 0, nil, response.ErrInternal(50041, "insert_failed", "failed to create monitoring tasks")
|
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,
|
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 {
|
if err != nil {
|
||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
@@ -1608,7 +1780,7 @@ func (s *MonitoringService) loadQuota(ctx context.Context, actor auth.Actor, wor
|
|||||||
if clientID == nil {
|
if clientID == nil {
|
||||||
return cfg, 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 {
|
if err != nil {
|
||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
@@ -1616,6 +1788,20 @@ func (s *MonitoringService) loadQuota(ctx context.Context, actor auth.Actor, wor
|
|||||||
return cfg, nil
|
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
|
// findLatestRegisteredClientWithMonitoringAccountForUser returns the newest
|
||||||
// non-revoked desktop client that has at least one bound monitoring platform
|
// non-revoked desktop client that has at least one bound monitoring platform
|
||||||
// account. Platform account health is not authorization.
|
// account. Platform account health is not authorization.
|
||||||
@@ -1673,6 +1859,41 @@ func (s *MonitoringService) findLatestRegisteredClientForUser(ctx context.Contex
|
|||||||
return &clientID, nil
|
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) {
|
func (s *MonitoringService) loadMonitoringClientPlatformIDs(ctx context.Context, tenantID, workspaceID int64, clientID uuid.UUID) ([]string, error) {
|
||||||
rows, err := s.businessPool.Query(ctx, `
|
rows, err := s.businessPool.Query(ctx, `
|
||||||
SELECT DISTINCT platform_id
|
SELECT DISTINCT platform_id
|
||||||
@@ -1702,6 +1923,230 @@ func (s *MonitoringService) loadMonitoringClientPlatformIDs(ctx context.Context,
|
|||||||
return reconcileEnabledMonitoringPlatforms(platformIDs), nil
|
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 {
|
func newMonitoringDerivedMetrics() monitoringDerivedMetrics {
|
||||||
return monitoringDerivedMetrics{
|
return monitoringDerivedMetrics{
|
||||||
ByDate: make(map[string]monitoringDerivedRateStats),
|
ByDate: make(map[string]monitoringDerivedRateStats),
|
||||||
|
|||||||
@@ -132,6 +132,27 @@ func TestReconcileEnabledMonitoringPlatformsKeepsAuthorizedSubset(t *testing.T)
|
|||||||
assert.Empty(t, resolveMonitoringPlatforms(nil))
|
assert.Empty(t, resolveMonitoringPlatforms(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilterMonitoringPlatformsByIDSetKeepsCatalogOrder(t *testing.T) {
|
||||||
|
platforms := defaultMonitoringPlatformMetadata()
|
||||||
|
|
||||||
|
filtered := filterMonitoringPlatformsByIDSet(platforms, []string{"qwen", "yuanbao"})
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"yuanbao", "qwen"}, monitoringPlatformIDs(filtered))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectNowDispatchTargetClientIDsDeduplicatesByPlatformOrder(t *testing.T) {
|
||||||
|
sharedClientID := mustParseDailyMonitoringUUID(t, "00000000-0000-0000-0000-000000000501")
|
||||||
|
qwenClientID := mustParseDailyMonitoringUUID(t, "00000000-0000-0000-0000-000000000502")
|
||||||
|
|
||||||
|
targets := collectNowDispatchTargetClientIDs(map[string]uuid.UUID{
|
||||||
|
"yuanbao": sharedClientID,
|
||||||
|
"qwen": qwenClientID,
|
||||||
|
"doubao": sharedClientID,
|
||||||
|
}, &sharedClientID)
|
||||||
|
|
||||||
|
assert.Equal(t, []uuid.UUID{sharedClientID, qwenClientID}, targets)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMonitoringPlatformAuthorizationStatus(t *testing.T) {
|
func TestMonitoringPlatformAuthorizationStatus(t *testing.T) {
|
||||||
clientID := uuid.New()
|
clientID := uuid.New()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user