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
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,11 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="48" height="48">
<path
d="M0 0m170.666667 0l682.666666 0q170.666667 0 170.666667 170.666667l0 682.666666q0 170.666667-170.666667 170.666667l-682.666666 0q-170.666667 0-170.666667-170.666667l0-682.666666q0-170.666667 170.666667-170.666667Z"
fill="#FECC32"></path>
<path
d="M512 199.369697c-202.364121 0-366.390303 164.026182-366.390303 366.390303A365.164606 365.164606 0 0 0 250.414545 822.30303l99.669334-92.004848a230.089697 230.089697 0 0 1-68.949334-164.553697c0-127.503515 103.330909-230.865455 230.84994-230.865455s230.865455 103.361939 230.865454 230.865455c0 65.318788-27.10497 124.276364-70.68703 166.275879l99.126303 92.609939a365.288727 365.288727 0 0 0 107.116606-258.885818c0-202.364121-164.057212-366.405818-366.421333-366.405818z"
fill="#000000"></path>
<path
d="M279.489939 522.627879c37.701818-13.187879 137.75903-32.845576 248.754425-30.409697 110.995394 2.435879 150.900364 12.955152 210.788848 26.298182 30.937212 6.888727 77.746424 42.713212 80.32194 83.673212 2.435879 38.291394-66.746182 57.607758-95.232 57.607757 0 0-105.580606-27.415273-211.999031-27.492848-106.433939-0.077576-213.767758 27.182545-213.767757 27.182545l-69.725091-50.641454 50.858666-86.217697z"
fill="#000000"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,4 @@
<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#FFFFFF" rx="8" />
<text x="30" y="320" font-family="Source Han Sans SC" font-size="220" fill="#D95354" font-weight="bold">简书</text>
</svg>

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,8 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="128" height="128">
<path
d="M0 0m225.890154 0l572.219692 0q225.890154 0 225.890154 225.890154l0 572.219692q0 225.890154-225.890154 225.890154l-572.219692 0q-225.890154 0-225.890154-225.890154l0-572.219692q0-225.890154 225.890154-225.890154Z"
fill="#EE3731"></path>
<path
d="M280.725047 864.454516l-56.635994-328.614599a610.844676 610.844676 0 0 1-51.282037 67.491947l-8.104955-3.367813q23.438987-75.251485 39.278808-176.668281 15.469731-106.338983 15.790476-204.474321l137.401809 15.691785c-1.060923 9.782693-9.264568 16.098883-24.425892 18.627826q-16.54299 83.714193-43.806233 154.783669l32.074403 5.464985c-0.629152 7.056369-6.834315 12.644717-18.504463 16.962425l67.491946 391.147349c-0.209717 4.095655-7.574494 10.942306-22.007975 20.317901-16.629345 10.214464-33.308034 16.839062-49.801679 19.738094z m133.096438-25.696532l-81.18525-470.210753 111.026781-8.746443c0.222054 9.585312-6.106473 16.641681-19.158287 21.267797l66.44336 384.89284 225.421374-38.859373 34.220921-66.01159a177.901912 177.901912 0 0 1-35.05979 9.474286L698.827231 673.562471l-4.416398-25.474478-94.545472 16.320936 2.948378 16.740372c0.629152 3.996964-5.057887 9.572976-17.172143 16.740371-14.630862 8.52439-32.32113 14.519836-52.848747 18.109702l-16.740371 2.837351L447.721662 323.211295l78.545279 14.803571-9.165877-57.067765L356.75372 308.407725l-8.536726-17.788958 165.627284-28.644909-15.691785-91.17766 129.296855-11.26305c-0.320744 12.003229-7.895238 20.971725-23.056562 26.745118 0.629152 11.361741 1.468021 31.272543 2.738661 59.584372l28.102112-4.835833 37.79845-73.499729 36.540148 19.984821q41.178599 22.637127 65.283747 37.79845c-0.641488 7.475803-6.427217 12.212946-17.591577 14.100401l-149.614755 25.733541c0.629152 22.42741 1.233631 44.965846 1.998482 67.393256l26.745118-4.638452 36.219403-58.745504 92.028865 49.123183c-4.107991 8.104955-12.53369 14.322455-25.06738 18.84988l51.393063 297.749154c0.209717 1.36933-2.109509 4.107991-6.957678 8.104955A107.399906 107.399906 0 0 1 762.852675 656.291638q60.953703 33.073644 96.445264 54.440132c-0.530461 7.475803-6.427217 12.212946-17.591577 14.112737l-347.452142 60.003807 2.208199 12.743407c0.740179 4.428735-6.106473 11.16436-20.737336 20.108184s-30.643392 15.691785-45.903405 18.319419zM561.955883 444.35385l94.545472-16.283928-14.100401-81.814401-94.668835 16.283927z m16.851398 97.913284l94.545472-16.320937-13.56994-79.174431-94.656498 16.320937z m17.788958 103.291915l94.656499-16.320937-14.630863-84.553061-94.557808 16.320936z"
fill="#FFFFFF"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,5 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="128" height="128">
<path
d="M849.92 51.2H174.08c-67.8656 0-122.88 55.0144-122.88 122.88v675.84c0 67.8656 55.0144 122.88 122.88 122.88h675.84c67.8656 0 122.88-55.0144 122.88-122.88V174.08c0-67.8656-55.0144-122.88-122.88-122.88zM448.18432 230.94272c176.98304-53.95968 267.17696 110.98624 267.17696 110.98624-32.59392-17.78176-130.39104-37.53472-235.09504 16.7936s-126.4384 172.87168-126.4384 172.87168c-42.56256-45.4144-44.4928-118.6304-44.4928-118.6304 5.03296-137.41568 138.84928-182.02112 138.84928-182.02112zM393.50784 796.42112c-256.12288-49.6384-197.85216-273.38752-133.81632-371.95264 0 0-2.88256 138.13248 130.22208 214.4 0 0 15.82592 7.1936 10.79296 30.21312l-5.03808 29.49632s-6.656 20.1472 6.02624 22.30272c0 0 4.04992 0 13.39904-6.4768l48.92672-32.37376s10.07104-7.1936 23.01952-5.03808c12.94848 2.16064 95.68768 23.74656 177.70496-44.60032-0.00512 0-15.10912 213.67296-271.23712 164.02944z m256.8448-19.42016c16.54784-7.9104 97.1264-102.8864 58.98752-231.66464s-167.6288-157.55776-167.6288-157.55776c66.19136-28.0576 143.89248-7.19872 143.89248-7.19872 117.9904 34.5344 131.6608 146.77504 131.6608 146.77504 23.01952 200.71936-166.912 249.64608-166.912 249.64608z"
fill="#01CC7A"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { QuestionCircleOutlined, UserOutlined } from "@ant-design/icons-vue";
import { useQuery } from "@tanstack/vue-query";
import type { MediaPlatform, PlatformAccount } from "@geo/shared-types";
import type { MediaPlatform } from "@geo/shared-types";
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { articlesApi, mediaApi, resolveApiURL } from "@/lib/api";
import { articlesApi, mediaApi } from "@/lib/api";
import { getPublishStatusMeta } from "@/lib/display";
import { getPublishPlatformMeta, normalizePublishPlatformId } from "@/lib/publish-platforms";
@@ -49,13 +49,6 @@ const platformsQuery = useQuery({
staleTime: 5 * 60 * 1000,
});
const accountsQuery = useQuery({
queryKey: ["media", "platform-accounts", "publish-status"],
enabled: computed(() => popoverOpen.value && shouldLoadPlatforms.value),
queryFn: () => mediaApi.accounts(),
staleTime: 5 * 60 * 1000,
});
const publishRecordsQuery = useQuery({
queryKey: computed(() => ["articles", "publish-records", props.articleId, "publish-status"]),
enabled: computed(() => popoverOpen.value && shouldLoadPlatforms.value),
@@ -73,12 +66,6 @@ const platformMap = computed(() => {
);
});
const accountMap = computed(() => {
return new Map(
(accountsQuery.data.value ?? []).map((account: PlatformAccount) => [account.id, account]),
);
});
const platformEntries = computed<PublishPlatformEntry[]>(() => {
const seen = new Set<string>();
const entries: PublishPlatformEntry[] = [];
@@ -92,7 +79,6 @@ const platformEntries = computed<PublishPlatformEntry[]>(() => {
const normalizedId = normalizePublishPlatformId(record.platform_id);
const platform = platformMap.value.get(normalizedId);
const account = accountMap.value.get(record.platform_account_id);
const fallback = getPublishPlatformMeta(normalizedId);
entries.push({
@@ -100,8 +86,8 @@ const platformEntries = computed<PublishPlatformEntry[]>(() => {
name: record.platform_name || platform?.name || fallback.name,
shortName: platform?.short_name || fallback.shortName,
accent: platform?.accent_color || fallback.accent,
accountName: account?.nickname || record.platform_nickname || "--",
avatarUrl: resolveApiURL(account?.avatar_url),
accountName: record.platform_nickname || "--",
avatarUrl: "",
status: normalizePublishRecordStatus(record.status),
});
}
@@ -251,7 +237,7 @@ function getAccountInitial(accountName: string): string {
<template #content>
<div class="article-publish-status__popover">
<div
v-if="publishRecordsQuery.isPending.value || platformsQuery.isPending.value || accountsQuery.isPending.value"
v-if="publishRecordsQuery.isPending.value || platformsQuery.isPending.value"
class="article-publish-status__loading"
>
<a-spin size="small" />
@@ -8,7 +8,7 @@ import { useI18n } from "vue-i18n";
import PromptRuleModal from "@/components/PromptRuleModal.vue";
import PublishPlatformSelector from "@/components/PublishPlatformSelector.vue";
import { generateApi, mediaApi, promptRulesApi, schedulesApi } from "@/lib/api";
import { generateApi, mediaApi, promptRulesApi, schedulesApi, tenantAccountsApi } from "@/lib/api";
import { formatError } from "@/lib/errors";
import { IMAGE_UPLOAD_ACCEPT, useImageUploadProgress, validateImageUploadFile } from "@/lib/image-webp";
import {
@@ -55,9 +55,9 @@ const platformsQuery = useQuery({
});
const accountsQuery = useQuery({
queryKey: ["media", "platform-accounts", "task-drawer"],
queryKey: ["tenant", "desktop-accounts", "task-drawer"],
enabled: computed(() => props.open),
queryFn: () => mediaApi.accounts(),
queryFn: () => tenantAccountsApi.list(),
});
const allPlatforms = computed(() =>
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -152,7 +152,7 @@ const enUS = {
},
media: {
title: "Media Management",
description: "Manage media bindings, local extension state, and one-click publishing from a single control plane.",
description: "Review desktop-managed media bindings, authorization state, and sync results in one place.",
},
brands: {
title: "Brand Library",
+1 -1
View File
@@ -152,7 +152,7 @@ const zhCN = {
},
media: {
title: "媒体管理",
description: "统一管理媒体账号绑定、本地插件状态和一键发布执行链路。",
description: "统一查看桌面端媒体账号绑定、授权状态与同步结果。",
},
brands: {
title: "品牌词库",
+20 -1
View File
@@ -15,6 +15,8 @@ import type {
CompetitorRequest,
CreatePluginSessionRequest,
CreatePluginSessionResponse,
CreatePublishJobRequest,
CreatePublishJobResponse,
CreatePublishBatchRequest,
CreatePublishBatchResponse,
GenerateFromRuleRequest,
@@ -104,6 +106,7 @@ import type {
CreateKolPromptRequest,
UpdateKolPackageRequest,
UpdateKolPromptRequest,
DesktopAccountInfo,
UserInfo,
WorkspaceOverview,
} from "@geo/shared-types";
@@ -869,7 +872,7 @@ export const brandsApi = {
export const mediaApi = {
platforms() {
return removedMediaApi<MediaPlatform[]>("platforms");
return apiClient.get<MediaPlatform[]>("/api/tenant/media/platforms");
},
accounts() {
return removedMediaApi<PlatformAccount[]>("accounts");
@@ -888,6 +891,22 @@ export const mediaApi = {
},
};
export const tenantAccountsApi = {
list() {
return apiClient.get<DesktopAccountInfo[]>("/api/tenant/accounts");
},
requestDelete(id: string, options?: { undo?: boolean }) {
const query = options?.undo ? "?undo=true" : "";
return apiClient.post<DesktopAccountInfo>(`/api/tenant/accounts/${encodeURIComponent(id)}/request-delete${query}`);
},
};
export const publishJobsApi = {
create(payload: CreatePublishJobRequest) {
return apiClient.post<CreatePublishJobResponse, CreatePublishJobRequest>("/api/tenant/publish-jobs", payload);
},
};
function removedMediaApi<T>(action: string): Promise<T> {
return Promise.reject(new ApiClientError({
message: "desktop_media_api_removed",
+4
View File
@@ -50,6 +50,10 @@ const errorMessageMap: Record<string, string> = {
brand_not_found: "品牌不存在或已删除",
keyword_not_found: "关键词不存在或已删除",
publish_cover_required: "已选择百家号,请先上传封面图",
desktop_account_client_missing: "所选账号还没有绑定桌面客户端,暂时无法发布",
desktop_account_not_found: "所选桌面账号不存在,请刷新后重试",
desktop_publish_job_store_unavailable: "发布队列暂时不可用,请稍后重试",
desktop_media_api_removed: "旧版媒体接口已下线,请刷新页面后重试",
publisher_plugin_timeout: "浏览器插件响应超时,请刷新当前页面后重试",
publisher_plugin_empty_response: "浏览器插件未返回数据,请刷新当前页面后重试",
publisher_plugin_invalid_response: "浏览器插件返回了无效数据,请刷新当前页面后重试",
+7 -3
View File
@@ -1,4 +1,4 @@
import type { MediaPlatform, PlatformAccount } from "@geo/shared-types";
import type { DesktopAccountInfo, MediaPlatform, PlatformAccount } from "@geo/shared-types";
export interface PublishPlatformOption {
id: string;
@@ -53,10 +53,14 @@ export function normalizePublishPlatformIds(platformIds?: Array<string | null |
export function buildPublishPlatformOptions(
platforms: MediaPlatform[],
accounts?: PlatformAccount[] | null,
accounts?: Array<PlatformAccount | DesktopAccountInfo> | null,
): PublishPlatformOption[] {
const boundPlatformIds = new Set(
(accounts ?? []).map((account) => normalizePublishPlatformId(account.platform_id)).filter(Boolean),
(accounts ?? [])
.map((account) =>
normalizePublishPlatformId("platform_id" in account ? account.platform_id : account.platform),
)
.filter(Boolean),
);
return platforms.map((platform) => {
@@ -10,7 +10,7 @@ import ArticleEditorCanvas from "@/components/ArticleEditorCanvas.vue";
import CoverPickerModal from "@/components/CoverPickerModal.vue";
import PublishArticleModal from "@/components/PublishArticleModal.vue";
import PublishPlatformSelector from "@/components/PublishPlatformSelector.vue";
import { articlesApi, mediaApi, resolveApiURL } from "@/lib/api";
import { articlesApi, mediaApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
import {
coverUploadRequired,
deriveCoverFileName,
@@ -55,8 +55,8 @@ const platformsQuery = useQuery({
});
const accountsQuery = useQuery({
queryKey: ["media", "platform-accounts", "article-editor"],
queryFn: () => mediaApi.accounts(),
queryKey: ["tenant", "desktop-accounts", "article-editor"],
queryFn: () => tenantAccountsApi.list(),
});
const publishPlatformOptions = computed(() =>
File diff suppressed because it is too large Load Diff