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
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed } from "vue";
import StatusBadge from "../components/StatusBadge.vue";
import SurfaceCard from "../components/SurfaceCard.vue";
@@ -7,61 +7,54 @@ import { useDesktopRuntime } from "../composables/useDesktopRuntime";
import { formatRelativeTime, titleCaseToken } from "../lib/formatters";
import { taskTone } from "../lib/runtime-ui";
const { snapshot, refresh } = useDesktopRuntime();
const actionPendingTaskId = ref<string | null>(null);
const actionError = ref<string | null>(null);
async function openReview(taskId: string) {
actionPendingTaskId.value = taskId;
actionError.value = null;
try {
await window.desktopBridge.app.openTaskReview(taskId);
} catch (error) {
actionError.value = error instanceof Error ? error.message : "打开审核页失败";
} finally {
actionPendingTaskId.value = null;
}
function translatePlatform(platform: string) {
const map: Record<string, string> = {
toutiaohao: "头条号",
bilibili: "哔哩哔哩",
xiaohongshu: "小红书",
douyin: "抖音",
kuaishou: "快手",
wechat_oa: "微信公众号",
baijiahao: "百家号",
zhihu: "知乎",
};
return map[platform?.toLowerCase()] || titleCaseToken(platform);
}
async function resolveParkedTask(taskId: string, status: "succeeded" | "failed") {
actionPendingTaskId.value = taskId;
actionError.value = null;
try {
await window.desktopBridge.app.resolveParkedTask(taskId, status);
await refresh();
} catch (error) {
actionError.value = error instanceof Error ? error.message : "回写审核结果失败";
} finally {
actionPendingTaskId.value = null;
}
function translateTaskStatus(status: string) {
const map: Record<string, string> = {
queued: "等待排队",
in_progress: "执行中",
succeeded: "成功",
failed: "失败",
unknown: "未知异常",
aborted: "已取消",
};
return map[status?.toLowerCase()] || status;
}
const { snapshot } = useDesktopRuntime();
const columns = computed(() => {
const tasks = snapshot.value?.tasks ?? [];
return [
{
key: "queued",
title: "Queued",
title: "等待派发 (Queued)",
description: "已经落库,等待 RabbitMQ 唤醒或客户端主动 lease。",
items: tasks.filter((item) => item.status === "queued"),
},
{
key: "in_progress",
title: "In Progress",
title: "正在执行 (In Progress)",
description: "当前被 client 持有活动租约,结果回写必须带 lease_token。",
items: tasks.filter((item) => item.status === "in_progress"),
},
{
key: "waiting_user",
title: "Waiting User",
description: "已释放 lease,不自动迁移,只允许原 target_client 恢复。",
items: tasks.filter((item) => item.status === "waiting_user"),
},
{
key: "exception",
title: "Fallback / Terminal",
title: "异常与终态 (Terminal)",
description: "unknown、failed、succeeded 等终态或补偿态。",
items: tasks.filter((item) => !["queued", "in_progress", "waiting_user"].includes(item.status)),
items: tasks.filter((item) => !["queued", "in_progress"].includes(item.status)),
},
];
});
@@ -74,7 +67,7 @@ const columns = computed(() => {
<p class="eyebrow">WORKFLOW CENTER</p>
<h2>任务调度中心</h2>
<p class="hero-summary">
基于 Lease-based 的可靠端云协议泳道全局监控本地客户端在 MQ 下发本地锁抢占及人工介入补偿各个流转阶段的阻塞情况
基于 lease 的可靠端云协议泳道这里更偏底层调度视角用来观察 MQ 下发租约占用与 fallback 消费的运行阶段
</p>
</div>
</header>
@@ -87,61 +80,29 @@ const columns = computed(() => {
:title="column.title"
:description="column.description"
>
<div class="lane-stack">
<div class="lane-stack" :class="{ 'scrollable-lane': column.items.length > 0 }">
<article v-for="task in column.items" :key="task.id" class="task-card">
<div class="task-head">
<div>
<div class="task-identity-copy">
<strong>{{ task.title }}</strong>
<p>{{ task.accountName }} · {{ titleCaseToken(task.platform) }}</p>
<p>{{ task.accountName }} · {{ translatePlatform(task.platform) }}</p>
</div>
<StatusBadge :tone="taskTone(task.status)" :label="task.status" />
<StatusBadge :tone="taskTone(task.status)" :label="translateTaskStatus(task.status)" class="shrink-badge" />
</div>
<div class="task-flags">
<StatusBadge tone="brand" :label="task.routing" subtle />
<StatusBadge tone="info" :label="task.kind" subtle />
<StatusBadge
:tone="task.mode === 'manual' ? 'warn' : 'success'"
:label="task.mode"
subtle
/>
</div>
<p class="task-summary">{{ task.summary }}</p>
<div v-if="task.status === 'waiting_user'" class="task-actions">
<button
type="button"
class="action-button secondary"
:disabled="actionPendingTaskId === task.id"
@click="openReview(task.id)"
>
打开审核页
</button>
<button
type="button"
class="action-button success"
:disabled="actionPendingTaskId === task.id"
@click="resolveParkedTask(task.id, 'succeeded')"
>
标记已发布
</button>
<button
type="button"
class="action-button danger"
:disabled="actionPendingTaskId === task.id"
@click="resolveParkedTask(task.id, 'failed')"
>
标记失败
</button>
</div>
<footer class="task-footer">
<span>
{{
task.leaseExpiresAt
? `lease ${formatRelativeTime(task.leaseExpiresAt)}`
: "lease released"
? `${formatRelativeTime(task.leaseExpiresAt)} 过期`
: "已释放"
}}
</span>
<small>{{ formatRelativeTime(task.updatedAt) }}</small>
@@ -149,7 +110,6 @@ const columns = computed(() => {
</article>
<p v-if="column.items.length === 0" class="empty-text">当前泳道为空</p>
</div>
<p v-if="actionError" class="error-text">{{ actionError }}</p>
</SurfaceCard>
</div>
</section>
@@ -157,12 +117,31 @@ const columns = computed(() => {
<style scoped>
.page,
.lane-grid,
.lane-stack {
.lane-grid {
display: grid;
gap: 18px;
}
.lane-stack {
display: flex;
flex-direction: column;
gap: 16px;
}
.scrollable-lane {
max-height: 480px;
overflow-y: auto;
padding-right: 6px;
}
.scrollable-lane::-webkit-scrollbar {
width: 6px;
}
.scrollable-lane::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 999px;
}
.hero {
display: block;
}
@@ -228,11 +207,22 @@ h2 {
gap: 16px;
}
.task-identity-copy {
flex: 1;
min-width: 0;
padding-right: 12px;
}
.task-head strong {
display: block;
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
word-wrap: break-word;
}
.shrink-badge {
flex-shrink: 0;
}
.task-head p,