feat(desktop): drop parked review flow, add publish management

SaaS 侧人工审核后才创建 publish job,desktop 不再做二次审核:移除
manual/waiting_user/parked/from_parked 状态机与 LeaseFromParked 查询,
desktop client 只执行发布并新增"发布管理"页(待发布队列 / 历史 / 再次
发送)。同时抽离 @geo/publisher-platforms 共享适配器包、新增 Redis-based
desktop_presence 与 publish_record_support,刷新 admin-web 发布弹窗与
媒体库;plan A / spec 文档同步口径。
This commit is contained in:
2026-04-20 09:52:48 +08:00
parent b16e9f0bd1
commit a617d39a4a
93 changed files with 5519 additions and 3594 deletions
+10 -285
View File
@@ -11,10 +11,7 @@ import { getSessionHandle, listSessionHandleSnapshots } from "./session-registry
import { getTransportSnapshot } from "./transport/api-client";
import { describeVaultBackend } from "./vault";
import { listHotViews } from "./view-pool";
function minutesAgo(now: number, minutes: number): number {
return now - minutes * 60_000;
}
import { normalizeAccountPlatformUid } from "../shared/account-identity";
function minutesAhead(now: number, minutes: number): number {
return now + minutes * 60_000;
@@ -30,11 +27,7 @@ function parseTimestamp(value: string | null | undefined): number | null {
}
export function createRuntimeSnapshot() {
const controller = getRuntimeControllerSnapshot();
if (!controller.session || controller.session.mode === "preview") {
return createPreviewRuntimeSnapshot();
}
return createLiveRuntimeSnapshot(controller);
return createLiveRuntimeSnapshot(getRuntimeControllerSnapshot());
}
function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeControllerSnapshot>) {
@@ -54,7 +47,7 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
|| now;
const clientOnline = controller.running && controller.lastHeartbeatStatus !== "failed";
const queueDepth = controller.tasks.filter((task) =>
["queued", "in_progress", "waiting_user"].includes(task.status),
["queued", "in_progress"].includes(task.status),
).length;
const clients = [
@@ -72,21 +65,21 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
] as const;
const accounts = controller.accounts.map((account) => {
const profile = controller.accountProfiles[account.id] ?? null;
const sessionHandle = getSessionHandle(account.id);
const isHot = hotViews.some((item) => item.accountId === account.id);
const accountQueueDepth = controller.tasks.filter((task) =>
task.accountId === account.id && ["queued", "in_progress", "waiting_user"].includes(task.status),
task.accountId === account.id && ["queued", "in_progress"].includes(task.status),
).length;
return {
id: account.id,
platform: account.platform,
displayName: profile?.displayName ?? account.display_name,
platformUid: profile?.platformUid ?? account.platform_uid,
avatarUrl: profile?.avatarUrl ?? null,
displayName: account.display_name,
platformUid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
avatarUrl: account.avatar_url ?? null,
health: account.health,
tags: account.tags,
syncVersion: account.sync_version,
clientId: account.client_id ?? primaryClientId,
partition: sessionHandle?.partition ?? `persist:acc-${account.id}`,
sessionState: isHot ? "hot" : sessionHandle ? "warm" : "cold",
@@ -95,7 +88,7 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
|| parseTimestamp(account.verified_at)
|| now,
queueDepth: accountQueueDepth,
online: clientOnline && account.client_id === primaryClientId,
online: clientOnline,
};
});
@@ -107,7 +100,6 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
}, {});
return {
previewMode: false,
generatedAt: now,
app: {
name: "GEO Rankly Desktop",
@@ -129,7 +121,7 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
accountsBound: accounts.length,
queuedTasks: tasks.filter((item) => item.status === "queued").length,
issuesOpen:
tasks.filter((item) => ["waiting_user", "unknown", "failed"].includes(item.status)).length
tasks.filter((item) => ["unknown", "failed"].includes(item.status)).length
+ accounts.filter((item) => item.health !== "live").length,
healthCounts,
},
@@ -163,270 +155,3 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
},
};
}
function createPreviewRuntimeSnapshot() {
const now = Date.now();
const sessions = listSessionHandleSnapshots();
const hotViews = listHotViews();
const leaseManager = getLeaseManagerSnapshot();
const rateLimiter = getRateLimiterSnapshot();
const clients = [
{
id: "cli-self-macbook",
label: "This device",
deviceName: "LiangXu MacBook Pro",
os: process.platform,
status: "online",
lastSeenAt: minutesAgo(now, 1),
queueDepth: 3,
heartbeat: "healthy",
channel: "stable",
},
{
id: "cli-studio-macmini",
label: "Remote node",
deviceName: "Studio Mac mini",
os: "darwin",
status: "offline",
lastSeenAt: minutesAgo(now, 38),
queueDepth: 1,
heartbeat: "stale",
channel: "stable",
},
] as const;
const accounts = [
{
id: "acc-doubao-core",
platform: "doubao",
displayName: "Doubao Monitor Core",
platformUid: "db-core-01",
avatarUrl: null,
health: "live",
tags: ["AI监控", "核心"],
clientId: clients[0].id,
partition: "persist:acc-doubao-core",
sessionState: hotViews.length > 0 ? "hot" : "warm",
lastSyncAt: minutesAgo(now, 3),
queueDepth: 1,
online: true,
},
{
id: "acc-toutiao-brand",
platform: "toutiaohao",
displayName: "Toutiao Brand Primary",
platformUid: "tt-brand-01",
avatarUrl: null,
health: "live",
tags: ["发布", "品牌"],
clientId: clients[0].id,
partition: "persist:acc-toutiao-brand",
sessionState: "warm",
lastSyncAt: minutesAgo(now, 8),
queueDepth: 2,
online: true,
},
{
id: "acc-toutiao-matrix",
platform: "toutiaohao",
displayName: "Toutiao Matrix B",
platformUid: "tt-matrix-b",
avatarUrl: null,
health: "captcha",
tags: ["发布", "待修复"],
clientId: clients[0].id,
partition: "persist:acc-toutiao-matrix",
sessionState: "cold",
lastSyncAt: minutesAgo(now, 42),
queueDepth: 1,
online: true,
},
{
id: "acc-qwen-research",
platform: "qwen",
displayName: "Qwen Research Backup",
platformUid: "qwen-research-02",
avatarUrl: null,
health: "risk",
tags: ["AI监控", "备用"],
clientId: clients[1].id,
partition: "persist:acc-qwen-research",
sessionState: "cold",
lastSyncAt: minutesAgo(now, 96),
queueDepth: 1,
online: false,
},
] as const;
const tasks = [
{
id: "task-monitor-doubao-001",
jobId: "job-monitor-daily-001",
title: "品牌提及采样 · Doubao",
kind: "monitor",
platform: "doubao",
accountId: accounts[0].id,
accountName: accounts[0].displayName,
clientId: accounts[0].clientId,
status: "in_progress",
mode: "auto",
routing: "rabbitmq-primary",
leaseExpiresAt: leaseManager.startedAt ? minutesAhead(leaseManager.startedAt, 10) : minutesAhead(now, 8),
updatedAt: minutesAgo(now, 2),
summary: "当前由本机执行,租约续期正常。",
},
{
id: "task-publish-toutiao-021",
jobId: "job-launch-q2-021",
title: "新品发布 · 头条号主号",
kind: "publish",
platform: "toutiaohao",
accountId: accounts[1].id,
accountName: accounts[1].displayName,
clientId: accounts[1].clientId,
status: "waiting_user",
mode: "manual",
routing: "rabbitmq-primary",
leaseExpiresAt: null,
updatedAt: minutesAgo(now, 6),
summary: "审核页已就绪,等待桌面端人工确认。",
},
{
id: "task-publish-toutiao-022",
jobId: "job-launch-q2-021",
title: "新品发布 · 矩阵 B",
kind: "publish",
platform: "toutiaohao",
accountId: accounts[2].id,
accountName: accounts[2].displayName,
clientId: accounts[2].clientId,
status: "queued",
mode: "auto",
routing: "rabbitmq-primary",
leaseExpiresAt: null,
updatedAt: minutesAgo(now, 1),
summary: "已入 MQ 队列,等待目标客户端拉取租约。",
},
{
id: "task-monitor-qwen-003",
jobId: "job-monitor-daily-003",
title: "竞品问答补采样 · Qwen",
kind: "monitor",
platform: "qwen",
accountId: accounts[3].id,
accountName: accounts[3].displayName,
clientId: accounts[3].clientId,
status: "unknown",
mode: "auto",
routing: "db-recovery",
leaseExpiresAt: null,
updatedAt: minutesAgo(now, 49),
summary: "原客户端离线,已切到 DB 补偿队列等待 reconcile。",
},
{
id: "task-publish-archive-007",
jobId: "job-content-rollout-007",
title: "专题文章回填 · 归档批次",
kind: "publish",
platform: "toutiaohao",
accountId: accounts[1].id,
accountName: accounts[1].displayName,
clientId: accounts[1].clientId,
status: "succeeded",
mode: "auto",
routing: "rabbitmq-primary",
leaseExpiresAt: null,
updatedAt: minutesAgo(now, 34),
summary: "发布完成,回执已写回服务端。",
},
] as const;
const activity = [
{
id: "evt-01",
severity: "info",
title: "MQ fanout ready",
detail: "desktop.task.event 已完成广播初始化,客户端优先走消息唤醒。",
at: minutesAgo(now, 2),
},
{
id: "evt-02",
severity: "warn",
title: "Captcha risk detected",
detail: "Toutiao Matrix B 在最近一次 resync 中被标记为 captcha。",
at: minutesAgo(now, 11),
},
{
id: "evt-03",
severity: "danger",
title: "Offline recovery armed",
detail: "Qwen Research Backup 目标客户端离线,任务已进入 DB fallback 观察态。",
at: minutesAgo(now, 49),
},
{
id: "evt-04",
severity: "success",
title: "Publish receipt committed",
detail: "归档批次回执已完成 result callbackWeb 端可见成功 URL。",
at: minutesAgo(now, 34),
},
] as const;
const healthCounts = accounts.reduce<Record<string, number>>((acc, item) => {
acc[item.health] = (acc[item.health] ?? 0) + 1;
return acc;
}, {});
return {
previewMode: true,
generatedAt: now,
app: {
name: "GEO Rankly Desktop",
version: app.getVersion(),
channel: process.env.NODE_ENV === "development" ? "dev" : "release",
platform: process.platform,
sessionCount: sessions.length,
hotViewCount: hotViews.length,
},
workspace: {
id: "ws-default",
name: "Default Workspace",
strategy: "rabbitmq-first / db-fallback",
nextSweepAt: minutesAhead(now, 5),
lastFullSyncAt: minutesAgo(now, 4),
},
summary: {
onlineClients: clients.filter((item) => item.status === "online").length,
accountsBound: accounts.length,
queuedTasks: tasks.filter((item) => item.status === "queued").length,
issuesOpen:
tasks.filter((item) => ["waiting_user", "unknown"].includes(item.status)).length
+ accounts.filter((item) => item.health !== "live").length,
healthCounts,
},
clients,
accounts,
tasks,
activity,
subsystems: {
transport: getTransportSnapshot(),
circuitBreaker: getCircuitBreakerState(),
keepAlive: getKeepAlivePlan(),
leaseManager: {
...leaseManager,
activeLeaseId: leaseManager.activeLeaseId ?? tasks[0].id,
},
rateLimiter: {
...rateLimiter,
refreshedAt: rateLimiter.refreshedAt || minutesAgo(now, 7),
bucketCount: rateLimiter.bucketCount || 6,
},
recovery: collectRecoverySnapshot(),
scheduler: getSchedulerState(),
sessions,
hotViews,
vault: describeVaultBackend(),
},
};
}