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
+184 -431
View File
@@ -17,7 +17,16 @@ import {
type MonitorAdapter,
type PublishAdapter,
} from "./adapters";
import { ensurePublishAccountSessionHandle, resolvePublishAccountProfile, type PublishAccountProfile } from "./account-binder";
import {
ensurePublishAccountSessionHandle,
inspectPublishAccountLocalSession,
type PublishAccountProfile,
} from "./account-binder";
import {
buildAccountIdentityKey,
normalizeAccountPlatformUid,
sameAccountPlatformUid,
} from "../shared/account-identity";
import { acquireHotView } from "./view-pool";
import { getLeaseManagerSnapshot, noteLeaseExtended, noteLeaseReleased, setActiveLease } from "./lease-manager";
import {
@@ -34,11 +43,11 @@ import {
setSchedulerPhase,
setSchedulerQueueDepth,
} from "./scheduler";
import { createSessionHandle, getSessionHandle } from "./session-registry";
import { openReviewWindow } from "./review-window";
import { clearSessionHandle, createSessionHandle, getSessionHandle } from "./session-registry";
import {
completeDesktopTask,
configureTransport,
deleteDesktopAccount,
extendDesktopTask,
getDesktopArticleContent,
heartbeatDesktopClient,
@@ -46,9 +55,11 @@ import {
listDesktopAccounts,
noteTransportHeartbeat,
noteTransportPull,
parkDesktopTask,
offlineDesktopClient,
revokeDesktopClient,
setTransportAuthState,
setTransportSseState,
upsertDesktopAccount,
} from "./transport/api-client";
import { SseClient, SseClientError } from "./transport/sse-client";
@@ -56,17 +67,14 @@ type RuntimeTone = "info" | "success" | "warn" | "danger";
type RuntimeTaskStatus =
| "queued"
| "in_progress"
| "waiting_user"
| "succeeded"
| "failed"
| "unknown"
| "aborted";
type RuntimeTaskMode = "auto" | "manual";
type RuntimeTaskRouting = "rabbitmq-primary" | "db-recovery";
const heartbeatIntervalMs = 25_000;
const pullIntervalMs = 60_000;
const accountsSyncIntervalMs = 90_000;
const leaseExtendIntervalMs = 60_000;
const maxActivityItems = 60;
@@ -80,7 +88,6 @@ interface RuntimeTaskRecord {
accountName: string;
clientId: string;
status: RuntimeTaskStatus;
mode: RuntimeTaskMode;
routing: RuntimeTaskRouting;
leaseExpiresAt: number | null;
updatedAt: number;
@@ -140,7 +147,6 @@ interface RuntimeState {
leaseInFlight: boolean;
heartbeatTimer: ReturnType<typeof setInterval> | null;
pullTimer: ReturnType<typeof setInterval> | null;
accountsTimer: ReturnType<typeof setInterval> | null;
sseClient: SseClient | null;
lastHeartbeatAt: number;
lastHeartbeatStatus: "idle" | "success" | "failed";
@@ -168,7 +174,6 @@ const state: RuntimeState = {
leaseInFlight: false,
heartbeatTimer: null,
pullTimer: null,
accountsTimer: null,
sseClient: null,
lastHeartbeatAt: 0,
lastHeartbeatStatus: "idle",
@@ -213,6 +218,33 @@ export function syncRuntimeSession(next: DesktopRuntimeSessionSyncRequest | null
}
}
export async function releaseRuntimeSession(options: { revoke?: boolean } = {}): Promise<void> {
const currentSession = state.session;
const shouldReleaseRemote = canRunLive(currentSession);
if (shouldReleaseRemote) {
try {
if (options.revoke) {
await revokeDesktopClient();
} else {
await offlineDesktopClient();
}
} catch (error) {
console.warn("[desktop-runtime] release runtime session failed", {
revoke: Boolean(options.revoke),
message: errorMessage(error),
});
}
}
stopRuntime();
clearRuntimeState();
configureTransport(null);
setTransportAuthState("pending");
state.session = null;
state.client = null;
}
export function getRuntimeControllerSnapshot(): RuntimeControllerSnapshot {
return {
session: state.session ? { ...state.session } : null,
@@ -238,103 +270,6 @@ export function getRuntimeControllerSnapshot(): RuntimeControllerSnapshot {
};
}
export async function openTaskReview(taskId: string): Promise<void> {
const task = state.tasks.get(taskId);
if (!task) {
throw new Error("desktop_task_not_found");
}
if (task.kind !== "publish") {
throw new Error("desktop_task_review_only_for_publish");
}
const reviewURL = resolveTaskReviewUrl(task);
if (!reviewURL) {
throw new Error("desktop_task_review_url_missing");
}
const sessionHandle = createSessionHandle(task.accountId || task.id);
await openReviewWindow({
taskId: task.id,
title: `${task.title} · 审核`,
url: reviewURL,
session: sessionHandle.session,
});
recordActivity("info", "Review Window Opened", `${task.title} 已打开审核页。`);
}
export async function resolveParkedTask(
taskId: string,
status: "succeeded" | "failed" | "unknown",
): Promise<void> {
const existing = state.tasks.get(taskId);
if (!existing) {
throw new Error("desktop_task_not_found");
}
if (existing.status !== "waiting_user") {
throw new Error("desktop_task_not_waiting_user");
}
if (!canRunLive(state.session)) {
throw new Error("desktop_runtime_not_authenticated");
}
if (state.currentTaskId && state.currentTaskId !== taskId) {
throw new Error("desktop_runtime_busy");
}
state.currentTaskId = taskId;
setSchedulerCurrentTask(taskId);
setSchedulerPhase("running");
try {
const leased = await leaseDesktopTask({ task_id: taskId }, { fromParked: true });
if (!leased.task || !leased.lease_token) {
throw new Error("desktop_task_resume_lease_missing");
}
setActiveLease({
taskId,
attemptId: leased.attempt_id ?? null,
leaseExpiresAt: leased.lease_expires_at ?? leased.task.lease_expires_at ?? null,
});
const resumed = upsertTaskFromInfo(leased.task, {
routing: existing.routing,
mode: existing.mode,
attemptId: leased.attempt_id ?? null,
leaseToken: leased.lease_token,
summary: "已从 parked 重新领取租约,准备回写人工审核结果。",
});
const resolutionPayload = buildManualResolutionPayload(resumed, status);
const completed = await completeDesktopTask(taskId, {
lease_token: leased.lease_token,
status,
payload: resolutionPayload.payload,
error: resolutionPayload.error,
});
noteLeaseReleased(status === "failed" ? "failed" : "completed");
upsertTaskFromInfo(completed, {
routing: existing.routing,
mode: existing.mode,
summary: resolutionPayload.summary,
attemptId: null,
leaseToken: null,
});
recordActivity(
status === "failed" ? "danger" : status === "unknown" ? "warn" : "success",
"Parked Task Resolved",
`${existing.title} 已按人工审核结果回写为 ${status}`,
);
} finally {
state.currentTaskId = null;
setSchedulerCurrentTask(null);
noteSchedulerTaskFinished();
processQueue();
}
}
function startRuntime(): void {
if (!canRunLive(state.session) || state.running) {
return;
@@ -349,7 +284,6 @@ function startRuntime(): void {
connectEventStream();
scheduleHeartbeatLoop();
schedulePullLoop();
scheduleAccountsSyncLoop();
void sendHeartbeat("startup");
void syncAccounts("startup");
@@ -372,10 +306,6 @@ function stopRuntime(): void {
clearInterval(state.pullTimer);
state.pullTimer = null;
}
if (state.accountsTimer) {
clearInterval(state.accountsTimer);
state.accountsTimer = null;
}
if (state.sseClient) {
state.sseClient.stop();
state.sseClient = null;
@@ -434,16 +364,6 @@ function schedulePullLoop(): void {
}, pullIntervalMs);
}
function scheduleAccountsSyncLoop(): void {
if (state.accountsTimer) {
clearInterval(state.accountsTimer);
}
state.accountsTimer = setInterval(() => {
void syncAccounts("timer");
}, accountsSyncIntervalMs);
}
function connectEventStream(): void {
if (!canRunLive(state.session)) {
return;
@@ -499,7 +419,6 @@ function connectEventStream(): void {
sseClient.on("task_available", taskEventHandler);
sseClient.on("task_leased", taskEventHandler);
sseClient.on("task_extended", taskEventHandler);
sseClient.on("task_parked", taskEventHandler);
sseClient.on("task_completed", taskEventHandler);
sseClient.on("task_canceled", taskEventHandler);
sseClient.on("task_reconciled", taskEventHandler);
@@ -541,7 +460,6 @@ function handleTaskEvent(event: DesktopTaskEventMessage): void {
accountName: existing?.accountName ?? "待同步账号",
clientId: event.target_client_id,
status: event.status,
mode: existing?.mode ?? "auto",
routing: existing?.routing ?? "rabbitmq-primary",
leaseExpiresAt: event.status === "in_progress" ? existing?.leaseExpiresAt ?? null : null,
updatedAt: parseTimestamp(event.updated_at) ?? Date.now(),
@@ -575,6 +493,7 @@ async function sendHeartbeat(source: "startup" | "timer"): Promise<void> {
cpu_arch: state.client?.cpu_arch ?? process.arch,
client_version: state.client?.client_version ?? "0.1.0-dev",
channel: state.client?.channel ?? "dev",
account_ids: state.accounts.filter((account) => account.health === "live").map((account) => account.id),
});
state.client = response.client;
@@ -609,52 +528,115 @@ async function sendHeartbeat(source: "startup" | "timer"): Promise<void> {
}
}
async function syncAccounts(source: "startup" | "timer" | "manual"): Promise<void> {
async function syncAccounts(source: "startup" | "manual"): Promise<void> {
if (!canRunLive(state.session)) {
return;
}
try {
const accounts = await listDesktopAccounts();
const currentClientId = state.client?.id ?? state.session?.desktop_client?.id ?? null;
state.lastAccountsSyncAt = Date.now();
state.lastError = null;
noteSchedulerAccountsSync();
setSchedulerError(null);
if (source === "startup") {
recordActivity("info", "Accounts Synced", `已同步 ${state.accounts.length} 个桌面账号。`);
}
state.accountProfiles.clear();
state.accounts = await Promise.all(accounts.map(async (account) => {
await ensurePublishAccountSessionHandle({
const syncedAccounts: Array<DesktopAccountInfo | null> = await Promise.all(accounts.map(async (account) => {
const normalizedPlatformUid = normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid;
const identity = {
id: account.id,
platform: account.platform,
platformUid: normalizedPlatformUid,
displayName: account.display_name,
} as const;
const local = await inspectPublishAccountLocalSession(identity);
if (local.availability === "missing") {
return null;
}
if (local.profile && !sameAccountPlatformUid(local.profile.platformUid, normalizedPlatformUid)) {
return {
...account,
platform_uid: normalizedPlatformUid,
health: "expired" as const,
};
}
let resolvedAccount: DesktopAccountInfo = {
...account,
platform_uid: normalizedPlatformUid,
health: local.availability === "expired" ? ("expired" as const) : ("live" as const),
};
const shouldRelinkClient = Boolean(currentClientId && resolvedAccount.client_id !== currentClientId);
if (shouldRelinkClient) {
try {
const updated = await upsertDesktopAccount({
platform: resolvedAccount.platform,
platform_uid: normalizedPlatformUid,
display_name: local.profile?.displayName ?? resolvedAccount.display_name,
avatar_url: local.profile?.avatarUrl ?? resolvedAccount.avatar_url,
account_fingerprint: resolvedAccount.account_fingerprint ?? normalizedPlatformUid,
health: resolvedAccount.health,
verified_at: new Date().toISOString(),
tags: resolvedAccount.tags,
});
resolvedAccount = {
...updated,
platform_uid: normalizeAccountPlatformUid(updated.platform_uid) || updated.platform_uid,
health: resolvedAccount.health,
};
} catch (error) {
console.warn("[desktop-runtime] account relink failed", {
accountId: resolvedAccount.id,
platform: resolvedAccount.platform,
platformUid: resolvedAccount.platform_uid,
message: errorMessage(error),
});
}
}
return resolvedAccount;
}));
const duplicateAccounts: DesktopAccountInfo[] = [];
const dedupedAccounts: DesktopAccountInfo[] = [];
const seenAccountKeys = new Set<string>();
for (const account of syncedAccounts) {
if (!account) {
continue;
}
const identityKey = buildAccountIdentityKey({
platform: account.platform,
platformUid: account.platform_uid,
displayName: account.display_name,
});
const profile = await resolvePublishAccountProfile({
id: account.id,
platform: account.platform,
platformUid: account.platform_uid,
displayName: account.display_name,
}).catch(() => null);
if (profile) {
state.accountProfiles.set(account.id, profile);
return {
...account,
display_name: profile.displayName,
platform_uid: profile.platformUid,
};
if (seenAccountKeys.has(identityKey)) {
duplicateAccounts.push(account);
continue;
}
return account;
}));
seenAccountKeys.add(identityKey);
dedupedAccounts.push(account);
}
state.accounts = dedupedAccounts;
if (duplicateAccounts.length > 0) {
void cleanupDuplicateDesktopAccounts(duplicateAccounts);
}
if (source === "startup") {
recordActivity("info", "Accounts Synced", `已同步 ${state.accounts.length} 个桌面账号。`);
}
refreshAccountNames();
void sendHeartbeat("timer");
} catch (error) {
const message = errorMessage(error);
state.lastError = message;
@@ -669,10 +651,46 @@ async function syncAccounts(source: "startup" | "timer" | "manual"): Promise<voi
}
}
async function cleanupDuplicateDesktopAccounts(accounts: DesktopAccountInfo[]): Promise<void> {
for (const account of accounts) {
try {
await deleteDesktopAccount(account.id, account.sync_version);
} catch (error) {
console.warn("[desktop-runtime] duplicate desktop account cleanup failed", {
accountId: account.id,
platform: account.platform,
platformUid: account.platform_uid,
message: errorMessage(error),
});
}
}
}
export async function refreshRuntimeAccounts(): Promise<void> {
await syncAccounts("manual");
}
export async function unbindRuntimeAccount(accountId: string, syncVersion: number): Promise<void> {
const matchedAccount = state.accounts.find((item) => item.id === accountId) ?? null;
await deleteDesktopAccount(accountId, syncVersion);
await clearSessionHandle(accountId);
state.accounts = state.accounts.filter((item) => item.id !== accountId);
state.accountProfiles.delete(accountId);
refreshAccountNames();
if (matchedAccount) {
recordActivity(
"info",
"Account Unbound",
`${matchedAccount.display_name} 已解绑,并清理了当前机器上的本地会话缓存。`,
);
}
await syncAccounts("manual");
}
function enqueueTask(taskId: string, routing: RuntimeTaskRouting): void {
if (!taskId || state.currentTaskId === taskId || state.queuedTaskIds.has(taskId)) {
return;
@@ -787,8 +805,6 @@ async function executeLeasedTask(
}
const task = leased.task;
const payload = normalizeJsonObject(task.payload);
const mode = resolveTaskMode(payload);
state.currentTaskId = task.id;
setSchedulerCurrentTask(task.id);
@@ -802,13 +818,9 @@ async function executeLeasedTask(
const taskRecord = upsertTaskFromInfo(task, {
routing,
mode,
attemptId: leased.attempt_id ?? null,
leaseToken: leased.lease_token,
summary:
mode === "manual" && task.kind === "publish"
? "已领取租约,准备切到 waiting_user parked 状态。"
: `已领取租约,开始执行 ${routing === "rabbitmq-primary" ? "MQ 唤醒" : "pull fallback"} 消费。`,
summary: `已领取租约,开始执行 ${routing === "rabbitmq-primary" ? "MQ 唤醒" : "pull fallback"} 消费。`,
});
recordActivity(
@@ -825,11 +837,6 @@ async function executeLeasedTask(
}, leaseExtendIntervalMs);
try {
if (mode === "manual" && task.kind === "publish") {
await prepareManualPublishReview(taskRecord, abortController.signal);
return;
}
const execution = await executeTaskAdapter(taskRecord, abortController.signal);
const completed = await completeDesktopTask(taskRecord.id, {
lease_token: leased.lease_token,
@@ -895,112 +902,6 @@ async function executeLeasedTask(
}
}
async function prepareManualPublishReview(task: RuntimeTaskRecord, signal: AbortSignal): Promise<void> {
const preparation = await executeTaskAdapter(task, signal);
if (preparation.status === "failed") {
const completed = await completeDesktopTask(task.id, {
lease_token: task.leaseToken as string,
status: "failed",
payload: preparation.payload,
error: preparation.error,
});
noteLeaseReleased("failed");
upsertTaskFromInfo(completed, {
routing: task.routing,
mode: task.mode,
summary: preparation.summary,
attemptId: null,
leaseToken: null,
});
recordActivity("danger", "Manual Review Prepare Failed", `${task.title} 无法进入人工审核态。`);
return;
}
const reviewUrl = preparation.reviewUrl ?? resolveTaskReviewUrl({
...task,
result: preparation.payload ?? task.result,
});
if (!reviewUrl) {
const completed = await completeDesktopTask(task.id, {
lease_token: task.leaseToken as string,
status: "unknown",
payload: preparation.payload,
error: preparation.error ?? {
code: "manual_review_url_missing",
message: "manual publish review url is not available for current platform",
},
});
noteLeaseReleased("completed");
upsertTaskFromInfo(completed, {
routing: task.routing,
mode: task.mode,
summary: "当前平台暂未提供可打开的审核页,任务回写为 unknown,等待后续对账。",
attemptId: null,
leaseToken: null,
});
recordActivity("warn", "Manual Review Unavailable", `${task.title} 暂无可用审核页。`);
return;
}
if (reviewUrl) {
try {
await openTaskReviewWindow(task, reviewUrl);
} catch (error) {
recordActivity("warn", "Review Window Open Failed", errorMessage(error));
}
}
await parkLeasedTask(task, {
summary:
preparation.summary || "任务已进入人工审核态,等待当前客户端操作员确认。",
payload: preparation.payload ?? null,
error: preparation.error ?? null,
});
}
async function parkLeasedTask(
task: RuntimeTaskRecord,
options: {
summary: string;
payload?: Record<string, JsonValue> | null;
error?: Record<string, JsonValue> | null;
},
): Promise<void> {
if (!task.leaseToken) {
return;
}
try {
if (options.payload) {
rememberTaskLocalResult(task.id, options.payload);
}
if (options.error) {
rememberTaskLocalError(task.id, options.error);
}
const parked = await parkDesktopTask(task.id, {
lease_token: task.leaseToken,
reason: "waiting_user",
});
noteLeaseReleased("parked");
upsertTaskFromInfo(parked, {
routing: task.routing,
summary: options.summary,
attemptId: null,
leaseToken: null,
});
recordActivity("warn", "Task Parked", `${task.title} 已进入人工审核态。`);
} catch (error) {
recordActivity("danger", "Task Park Failed", `${task.title} 进入人工审核态失败:${errorMessage(error)}`);
}
}
async function extendActiveLease(taskId: string, leaseToken: string): Promise<void> {
try {
const task = await extendDesktopTask(taskId, { lease_token: leaseToken });
@@ -1035,16 +936,18 @@ async function executeTaskAdapter(
return buildScaffoldResult(task, payload, "publish adapter is not implemented yet");
}
const viewHandle = acquireHotView(task.accountId || task.id);
const viewHandle =
adapter.executionMode === "session" || adapter.executionMode === "playwright"
? null
: acquireHotView(task.accountId || task.id);
const result = await adapter.publish(
{
taskId: task.id,
accountId: task.accountId || task.id,
session: sessionHandle.session,
view: viewHandle.view,
view: viewHandle?.view ?? null,
signal,
mode: task.mode,
phase: "initial",
article: await loadPublishArticle(task),
reportProgress(stage: string) {
@@ -1062,16 +965,18 @@ async function executeTaskAdapter(
return buildScaffoldResult(task, payload, "monitor adapter is not implemented yet");
}
const viewHandle = acquireHotView(task.accountId || task.id);
const viewHandle =
adapter.executionMode === "session" || adapter.executionMode === "playwright"
? null
: acquireHotView(task.accountId || task.id);
const result = await adapter.query(
{
taskId: task.id,
accountId: task.accountId || task.id,
session: sessionHandle.session,
view: viewHandle.view,
view: viewHandle?.view ?? null,
signal,
mode: task.mode,
phase: "initial",
reportProgress(stage: string) {
updateTaskProgress(task.id, `monitor adapter progress: ${stage}`);
@@ -1107,7 +1012,7 @@ function buildScaffoldResult(
async function loadPublishArticle(
task: RuntimeTaskRecord,
): Promise<DesktopArticleContent & { publish_type: "publish" | "draft" }> {
): Promise<DesktopArticleContent> {
const articleId = resolveArticleId(task.payload);
if (articleId === null) {
throw new Error("desktop_publish_article_id_missing");
@@ -1117,7 +1022,6 @@ async function loadPublishArticle(
return {
...article,
title: article.title?.trim() || task.title,
publish_type: task.mode === "manual" ? "draft" : "publish",
};
}
@@ -1147,109 +1051,6 @@ function toPositiveInt(value: JsonValue | null): number | null {
return null;
}
async function openTaskReviewWindow(task: RuntimeTaskRecord, reviewURL: string): Promise<void> {
const sessionHandle = createSessionHandle(task.accountId || task.id);
await openReviewWindow({
taskId: task.id,
title: `${task.title} · 审核`,
url: reviewURL,
session: sessionHandle.session,
});
}
function resolveTaskReviewUrl(task: RuntimeTaskRecord): string | null {
const candidate =
firstString(task.result?.external_manage_url) ??
firstString(task.result?.review_url) ??
firstString(task.payload.external_manage_url) ??
firstString(task.payload.review_url);
if (candidate) {
return candidate;
}
if (task.platform === "toutiaohao") {
return "https://mp.toutiao.com/profile_v4/graphic/publish";
}
return null;
}
function firstString(value: JsonValue | undefined): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function buildManualResolutionPayload(
task: RuntimeTaskRecord,
status: "succeeded" | "failed" | "unknown",
): {
payload: Record<string, JsonValue>;
error?: Record<string, JsonValue>;
summary: string;
} {
const payload: Record<string, JsonValue> = {
...(task.result ?? {}),
manual_resolution: status,
resolved_at: new Date().toISOString(),
resolved_by: "desktop_operator",
};
if (status === "succeeded") {
return {
payload,
summary: "人工审核已确认完成,任务回写为 succeeded。",
};
}
if (status === "unknown") {
return {
payload,
error: {
code: "manual_review_unconfirmed",
message: "operator could not confirm final publish result",
},
summary: "人工审核后仍无法确认最终状态,任务回写为 unknown。",
};
}
return {
payload,
error: {
code: "manual_review_rejected",
message: "operator marked parked task as failed",
},
summary: "人工审核已拒绝该任务,任务回写为 failed。",
};
}
function rememberTaskLocalResult(taskId: string, payload: Record<string, JsonValue>): void {
const existing = state.tasks.get(taskId);
if (!existing) {
return;
}
state.tasks.set(taskId, {
...existing,
result: {
...(existing.result ?? {}),
...payload,
},
});
}
function rememberTaskLocalError(taskId: string, error: Record<string, JsonValue>): void {
const existing = state.tasks.get(taskId);
if (!existing) {
return;
}
state.tasks.set(taskId, {
...existing,
error: {
...(existing.error ?? {}),
...error,
},
});
}
function updateTaskProgress(taskId: string, summary: string): void {
const existing = state.tasks.get(taskId);
if (!existing) {
@@ -1265,7 +1066,6 @@ function upsertTaskFromInfo(
task: DesktopTaskInfo,
options: {
routing?: RuntimeTaskRouting;
mode?: RuntimeTaskMode;
summary?: string;
attemptId?: string | null;
leaseToken?: string | null;
@@ -1273,7 +1073,6 @@ function upsertTaskFromInfo(
): RuntimeTaskRecord {
const existing = state.tasks.get(task.id);
const payload = normalizeJsonObject(task.payload);
const mode = options.mode ?? existing?.mode ?? resolveTaskMode(payload);
const record: RuntimeTaskRecord = {
id: task.id,
jobId: task.job_id,
@@ -1284,18 +1083,13 @@ function upsertTaskFromInfo(
accountName: resolveAccountName(task.target_account_id, existing?.accountName),
clientId: task.target_client_id,
status: task.status,
mode,
routing: options.routing ?? existing?.routing ?? "db-recovery",
leaseExpiresAt: parseTimestamp(task.lease_expires_at) ?? null,
updatedAt: parseTimestamp(task.updated_at) ?? Date.now(),
summary: options.summary ?? existing?.summary ?? summaryFromTaskStatus(task.status, mode),
summary: options.summary ?? existing?.summary ?? summaryFromTaskStatus(task.status),
payload,
error:
normalizeJsonObjectOrNull(task.error)
?? (task.status === "waiting_user" ? existing?.error ?? null : null),
result:
normalizeJsonObjectOrNull(task.result)
?? (task.status === "waiting_user" ? existing?.result ?? null : null),
error: normalizeJsonObjectOrNull(task.error),
result: normalizeJsonObjectOrNull(task.result),
attemptId:
task.status === "in_progress"
? options.attemptId ?? existing?.attemptId ?? task.active_attempt_id ?? null
@@ -1422,20 +1216,14 @@ function defaultTaskTitle(kind: "publish" | "monitor", platform?: string): strin
return `${kind === "publish" ? "发布任务" : "监控任务"}${platform ? ` · ${platform}` : ""}`;
}
function resolveTaskMode(payload: Record<string, JsonValue>): RuntimeTaskMode {
return payload.mode === "manual" ? "manual" : "auto";
}
function summaryFromTaskStatus(status: RuntimeTaskStatus, mode: RuntimeTaskMode): string {
function summaryFromTaskStatus(status: RuntimeTaskStatus): string {
switch (status) {
case "queued":
return "任务已排队,等待客户端领取租约。";
case "in_progress":
return "任务正在执行,结果回写需要携带有效 lease_token。";
case "waiting_user":
return "任务已 parked,等待原客户端上的人工确认。";
case "succeeded":
return mode === "manual" ? "任务已完成人工审核并成功提交。" : "任务执行成功。";
return "任务执行成功。";
case "failed":
return "任务执行失败。";
case "aborted":
@@ -1453,14 +1241,12 @@ function summaryFromEventType(type: DesktopTaskEventMessage["type"], status: Run
return "服务端已确认租约归属。";
case "task_extended":
return "任务租约已续期。";
case "task_parked":
return "任务已 parked 到 waiting_user。";
case "task_canceled":
return "任务已被取消。";
case "task_reconciled":
return "任务已完成人工对账并回写。";
default:
return summaryFromTaskStatus(status, "auto");
return summaryFromTaskStatus(status);
}
}
@@ -1492,39 +1278,6 @@ function normalizeJsonObjectOrNull(
return { ...value };
}
function normalizeUnknownResult(value: Record<string, unknown>): Record<string, JsonValue> {
const output: Record<string, JsonValue> = {};
for (const [key, item] of Object.entries(value)) {
output[key] = normalizeJsonValue(item);
}
return output;
}
function normalizeJsonValue(value: unknown): JsonValue {
if (
value === null
|| typeof value === "string"
|| typeof value === "number"
|| typeof value === "boolean"
) {
return value;
}
if (Array.isArray(value)) {
return value.map((item) => normalizeJsonValue(item));
}
if (typeof value === "object" && value) {
const output: Record<string, JsonValue> = {};
for (const [key, item] of Object.entries(value)) {
output[key] = normalizeJsonValue(item);
}
return output;
}
return String(value);
}
function toStructuredError(error: unknown): Record<string, JsonValue> {
if (error instanceof ApiClientError) {
return {