From 134dd063c3f888bab094f0f89fad04f711c58788 Mon Sep 17 00:00:00 2001 From: liangxu Date: Fri, 3 Apr 2026 17:48:30 +0800 Subject: [PATCH] feat: Add platform adapters for various content publishing platforms - Implemented Dongchedi adapter for user detection and publishing. - Implemented Jianshu adapter for user detection and publishing. - Implemented Juejin adapter for user detection and publishing. - Implemented Qiehao adapter for user detection and publishing. - Implemented Smzdm adapter for user detection and publishing. - Implemented Sohuhao adapter for user detection and publishing. - Implemented Toutiaohao adapter for user detection and publishing. - Implemented Wangyihao adapter for user detection and publishing. - Implemented Weixin Gzh adapter for user detection and publishing. - Implemented Zol adapter for user detection and publishing. - Added documentation for manual testing of media publishing. --- .../src/components/PublishArticleModal.vue | 427 +++++++++++++----- apps/admin-web/src/i18n/messages/en-US.ts | 11 + apps/admin-web/src/i18n/messages/zh-CN.ts | 11 + apps/admin-web/src/lib/errors.ts | 6 +- apps/admin-web/src/lib/publisher-plugin.ts | 64 ++- apps/admin-web/src/lib/publisher-runtime.ts | 19 +- apps/admin-web/src/main.ts | 2 + apps/admin-web/src/views/MediaView.vue | 28 +- apps/admin-web/src/views/WorkspaceView.vue | 2 +- .../entrypoints/background.ts | 28 +- apps/browser-extension/entrypoints/content.ts | 29 +- .../entrypoints/popup/App.vue | 274 +++-------- apps/browser-extension/public/rules.json | 299 ++++++++++++ .../src/adapters/baijiahao.ts | 210 +++++++++ .../src/adapters/bilibili.ts | 48 ++ apps/browser-extension/src/adapters/common.ts | 279 ++++++++++++ .../src/adapters/dongchedi.ts | 38 ++ apps/browser-extension/src/adapters/index.ts | 30 +- .../browser-extension/src/adapters/jianshu.ts | 222 +++++++++ apps/browser-extension/src/adapters/juejin.ts | 46 ++ apps/browser-extension/src/adapters/qiehao.ts | 242 ++++++++++ apps/browser-extension/src/adapters/smzdm.ts | 35 ++ .../browser-extension/src/adapters/sohuhao.ts | 142 ++++++ .../src/adapters/toutiaohao.ts | 249 ++++++++++ .../src/adapters/wangyihao.ts | 38 ++ .../src/adapters/weixin_gzh.ts | 34 ++ apps/browser-extension/src/adapters/zhihu.ts | 3 +- apps/browser-extension/src/adapters/zol.ts | 41 ++ apps/browser-extension/src/platforms.ts | 8 +- apps/browser-extension/src/runtime.ts | 224 ++++----- server/internal/tenant/app/media_service.go | 24 +- .../tenant/transport/media_handler.go | 39 +- 媒体发布手工自测.md | 27 ++ 33 files changed, 2722 insertions(+), 457 deletions(-) create mode 100644 apps/browser-extension/src/adapters/baijiahao.ts create mode 100644 apps/browser-extension/src/adapters/bilibili.ts create mode 100644 apps/browser-extension/src/adapters/common.ts create mode 100644 apps/browser-extension/src/adapters/dongchedi.ts create mode 100644 apps/browser-extension/src/adapters/jianshu.ts create mode 100644 apps/browser-extension/src/adapters/juejin.ts create mode 100644 apps/browser-extension/src/adapters/qiehao.ts create mode 100644 apps/browser-extension/src/adapters/smzdm.ts create mode 100644 apps/browser-extension/src/adapters/sohuhao.ts create mode 100644 apps/browser-extension/src/adapters/toutiaohao.ts create mode 100644 apps/browser-extension/src/adapters/wangyihao.ts create mode 100644 apps/browser-extension/src/adapters/weixin_gzh.ts create mode 100644 apps/browser-extension/src/adapters/zol.ts create mode 100644 媒体发布手工自测.md diff --git a/apps/admin-web/src/components/PublishArticleModal.vue b/apps/admin-web/src/components/PublishArticleModal.vue index 271ca61..485838f 100644 --- a/apps/admin-web/src/components/PublishArticleModal.vue +++ b/apps/admin-web/src/components/PublishArticleModal.vue @@ -1,9 +1,15 @@ @@ -596,7 +598,7 @@ function toneClass(tone: CardTone): string { top: 4px; right: -8px; background: #f5f5f5; - color: #bfbfbf; + color: f3f#737373; font-size: 10px; padding: 2px 6px; border-radius: 4px; @@ -716,12 +718,12 @@ function toneClass(tone: CardTone): string { .media-card__identity-text p { margin: 2px 0 0; - color: #bfbfbf; + color: #737373; font-size: 11px; } .more-icon { - color: #bfbfbf; + color: #737373; font-size: 18px; cursor: pointer; padding: 0 4px; @@ -750,7 +752,7 @@ function toneClass(tone: CardTone): string { .media-card__account-unbound { font-size: 13px; - color: #bfbfbf; + color: #737373; } .media-card__footer { diff --git a/apps/admin-web/src/views/WorkspaceView.vue b/apps/admin-web/src/views/WorkspaceView.vue index d6a91f3..3f275f9 100644 --- a/apps/admin-web/src/views/WorkspaceView.vue +++ b/apps/admin-web/src/views/WorkspaceView.vue @@ -501,7 +501,7 @@ function refreshDashboard(): void { align-items: center; gap: 6px; } -.muted { color: #bfbfbf; font-size: 11px; } +.muted { color: #737373; font-size: 11px; } .muted-dot { color: #d9d9d9; font-size: 10px; } /* Status Badges */ diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index 2bf7de2..0b1cc03 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -4,6 +4,16 @@ import { defineBackground } from "wxt/utils/define-background"; import { handlePublisherAction } from "../src/runtime"; import { getExtensionState } from "../src/storage"; +type BackgroundSuccessResponse = { + ok: true; + data: Awaited>; +}; + +type BackgroundErrorResponse = { + ok: false; + error: string; +}; + export default defineBackground(() => { void getExtensionState(); @@ -21,11 +31,25 @@ export default defineBackground(() => { await getExtensionState(); }); - browser.runtime.onMessage.addListener((message) => { + chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (!message || message.type !== "geo.publisher.request") { return undefined; } - return handlePublisherAction(message.action, message.payload); + void handlePublisherAction(message.action, message.payload) + .then((data) => { + sendResponse({ + ok: true, + data, + } satisfies BackgroundSuccessResponse); + }) + .catch((error: unknown) => { + sendResponse({ + ok: false, + error: error instanceof Error ? error.message : "publisher_plugin_unknown_error", + } satisfies BackgroundErrorResponse); + }); + + return true; }); }); diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts index 10a5b6c..5b153ac 100644 --- a/apps/browser-extension/entrypoints/content.ts +++ b/apps/browser-extension/entrypoints/content.ts @@ -9,6 +9,20 @@ type PublisherRequestMessage = { payload?: unknown; }; +type BackgroundSuccessResponse = { + ok: true; + data: unknown; +}; + +type BackgroundErrorResponse = { + ok: false; + error?: string; +}; + +function isBackgroundResponse(value: unknown): value is BackgroundSuccessResponse | BackgroundErrorResponse { + return typeof value === "object" && value !== null && "ok" in value; +} + export default defineContentScript({ matches: ["http://*/*", "https://*/*"], main() { @@ -23,12 +37,25 @@ export default defineContentScript({ const { requestId, action, payload } = event.data; try { - const data = await browser.runtime.sendMessage({ + const response = await browser.runtime.sendMessage({ type: "geo.publisher.request", action, payload, }); + const data = isBackgroundResponse(response) + ? (() => { + if (!response.ok) { + throw new Error(response.error || "publisher_plugin_unknown_error"); + } + return response.data; + })() + : response; + + if (typeof data === "undefined") { + throw new Error("publisher_plugin_empty_response"); + } + window.postMessage( { source: "geo-extension", diff --git a/apps/browser-extension/entrypoints/popup/App.vue b/apps/browser-extension/entrypoints/popup/App.vue index be0b90f..ce4763b 100644 --- a/apps/browser-extension/entrypoints/popup/App.vue +++ b/apps/browser-extension/entrypoints/popup/App.vue @@ -2,14 +2,22 @@ import { computed, onMounted, ref } from "vue"; import { browser } from "wxt/browser"; -import { getExtensionState, resetPlatformStates, updatePlatformState } from "../../src/storage"; +import { normalizeRemoteUrl } from "../../src/adapters/common"; +import { getExtensionState } from "../../src/storage"; import type { StoredPlatformState } from "../../src/platforms"; +type BackgroundSuccessResponse = { + ok: true; + data: unknown; +}; + +type BackgroundErrorResponse = { + ok: false; + error?: string; +}; + const loading = ref(true); const refreshing = ref(false); -const resetting = ref(false); -const savingId = ref(null); -const expandedId = ref(null); const installationKey = ref(""); const installationId = ref(null); @@ -33,6 +41,10 @@ function clonePlatforms(items: StoredPlatformState[]): StoredPlatformState[] { return items.map((platform) => ({ ...platform })); } +function isBackgroundResponse(value: unknown): value is BackgroundSuccessResponse | BackgroundErrorResponse { + return typeof value === "object" && value !== null && "ok" in value; +} + async function loadState(): Promise { loading.value = true; try { @@ -64,8 +76,9 @@ function uidFor(platform: StoredPlatformState): string { } function avatarFor(platform: StoredPlatformState): string | null { - if (platform.avatar_url?.trim()) { - return platform.avatar_url.trim(); + const normalizedAvatar = normalizeRemoteUrl(platform.avatar_url); + if (normalizedAvatar) { + return normalizedAvatar; } if (platform.connected) { return `https://api.dicebear.com/9.x/initials/svg?seed=${encodeURIComponent(titleFor(platform))}`; @@ -73,13 +86,6 @@ function avatarFor(platform: StoredPlatformState): string | null { return null; } -function statusLabel(platform: StoredPlatformState): string { - if (refreshing.value || loading.value) { - return "检测中..."; - } - return platform.connected ? "已登录" : "未登录"; -} - function statusTone(platform: StoredPlatformState): "success" | "danger" | "pending" { if (refreshing.value || loading.value) { return "pending"; @@ -87,36 +93,18 @@ function statusTone(platform: StoredPlatformState): "success" | "danger" | "pend return platform.connected ? "success" : "danger"; } -function fillDemo(platform: StoredPlatformState): void { - platform.connected = true; - platform.nickname = platform.nickname?.trim() || `${platform.platform_name} Demo`; - platform.platform_uid = (platform.platform_uid != null ? String(platform.platform_uid).trim() : "") || `${platform.platform_id}-demo`; - platform.avatar_url = - platform.avatar_url?.trim() || - `https://api.dicebear.com/9.x/initials/svg?seed=${encodeURIComponent(platform.nickname || platform.platform_name)}`; - platform.message = null; -} - -function toggleExpanded(platformId: string): void { - expandedId.value = expandedId.value === platformId ? null : platformId; -} - -async function savePlatform(platform: StoredPlatformState): Promise { - savingId.value = platform.platform_id; - try { - const state = await updatePlatformState(platform.platform_id, { - connected: platform.connected, - nickname: platform.nickname?.trim() || null, - platform_uid: platform.platform_uid != null ? String(platform.platform_uid).trim() || null : null, - avatar_url: platform.avatar_url?.trim() || null, - message: platform.connected ? null : "Sign in required", - }); - installationId.value = state.plugin_installation_id; - apiBaseUrl.value = state.api_base_url; - platforms.value = clonePlatforms(state.platforms); - } finally { - savingId.value = null; +function loginButtonLabel(platform: StoredPlatformState): string { + if (refreshing.value || loading.value) { + return "检测中..."; } + return "未登录"; +} + +async function openLoginPage(platform: StoredPlatformState): Promise { + if (!platform.login_url || refreshing.value || loading.value) { + return; + } + await browser.tabs.create({ url: platform.login_url }); } async function simulateDetect(): Promise { @@ -126,10 +114,13 @@ async function simulateDetect(): Promise { async function refreshDetectedPlatforms(): Promise { refreshing.value = true; try { - await browser.runtime.sendMessage({ + const response = await browser.runtime.sendMessage({ type: "geo.publisher.request", action: "detectPlatforms", }); + if (isBackgroundResponse(response) && !response.ok) { + throw new Error(response.error || "publisher_plugin_unknown_error"); + } const state = await getExtensionState(); installationId.value = state.plugin_installation_id; apiBaseUrl.value = state.api_base_url; @@ -138,17 +129,6 @@ async function refreshDetectedPlatforms(): Promise { refreshing.value = false; } } - -async function handleReset(): Promise { - resetting.value = true; - try { - const state = await resetPlatformStates(); - platforms.value = clonePlatforms(state.platforms); - expandedId.value = null; - } finally { - resetting.value = false; - } -}