feat(desktop): add monitor scheduler, Playwright CDP layer, and account health
Move monitor-task scheduling authority onto the client with a durable file-backed queue that survives restart, drops stale cross-day tasks, enforces per-platform serialism, and adapts global concurrency from Electron process metrics. Publish tasks keep their existing FIFO. Add a hidden Playwright CDP manager that attaches to Electron Chromium on account session partitions, lets adapters opt into `executionMode: "playwright"`, and leaves the existing hidden WebContentsView path in place for current adapters. Introduce an account-health subsystem with silent probes, projected health/auth states, and IPC invalidation events so the renderer can show accurate auth/probe status and verification timestamps. Server-side, derive and forward title/business_date/scheduler_group_key/ question_text metadata on desktop task events so the local scheduler can defer same-question fan-out before leasing.
This commit is contained in:
@@ -11,7 +11,7 @@ import { computed, ref } from "vue";
|
||||
|
||||
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
||||
import { showClientActionError } from "../lib/client-errors";
|
||||
import { formatDateTime } from "../lib/formatters";
|
||||
import { formatDateTime, formatRelativeTime } from "../lib/formatters";
|
||||
import { desktopMonitoringMediaCatalog } from "../lib/media-catalog";
|
||||
import type { RuntimeAccount } from "../types";
|
||||
|
||||
@@ -52,15 +52,19 @@ function authLabel(account: AccountRow | null) {
|
||||
return "未绑定";
|
||||
}
|
||||
|
||||
switch (account.health) {
|
||||
case "live":
|
||||
switch (account.authState) {
|
||||
case "active":
|
||||
return "授权正常";
|
||||
case "captcha":
|
||||
return "待处理";
|
||||
case "challenge_required":
|
||||
return "需人工验证";
|
||||
case "expired":
|
||||
return "授权过期";
|
||||
case "expiring_soon":
|
||||
return account.probeState === "network_error" ? "最近校验失败" : "待重新校验";
|
||||
case "revoked":
|
||||
return "已停用";
|
||||
default:
|
||||
return "风险观察";
|
||||
return account.probeState === "probing" ? "校验中" : "待校验";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,18 +73,38 @@ function authColor(account: AccountRow | null) {
|
||||
return "default";
|
||||
}
|
||||
|
||||
switch (account.health) {
|
||||
case "live":
|
||||
switch (account.authState) {
|
||||
case "active":
|
||||
return "success";
|
||||
case "captcha":
|
||||
case "challenge_required":
|
||||
return "warning";
|
||||
case "expired":
|
||||
case "revoked":
|
||||
return "error";
|
||||
case "expiring_soon":
|
||||
return account.probeState === "network_error" ? "warning" : "default";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
|
||||
function verificationLabel(account: AccountRow): string {
|
||||
if (account.lastVerifiedAt) {
|
||||
return `${formatRelativeTime(account.lastVerifiedAt)}校验通过`;
|
||||
}
|
||||
if (account.probeState === "probing") {
|
||||
return "正在执行首轮校验";
|
||||
}
|
||||
return "尚未完成校验";
|
||||
}
|
||||
|
||||
function nextProbeLabel(account: AccountRow): string {
|
||||
if (!account.nextProbeAt) {
|
||||
return "未排程";
|
||||
}
|
||||
return formatRelativeTime(account.nextProbeAt);
|
||||
}
|
||||
|
||||
function sessionLabel(account: AccountRow): string {
|
||||
switch (account.sessionState) {
|
||||
case "hot":
|
||||
@@ -243,6 +267,14 @@ async function unbindPlatform(account: AccountRow) {
|
||||
<span class="info-label">最近同步</span>
|
||||
<span class="info-value">{{ formatDateTime(platform.account.lastSyncAt) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">校验状态</span>
|
||||
<span class="info-value">{{ verificationLabel(platform.account) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">下次巡检</span>
|
||||
<span class="info-value">{{ nextProbeLabel(platform.account) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
|
||||
Reference in New Issue
Block a user