Files
geo/apps/admin-web/src/lib/publisher-plugin.ts
T
root 134dd063c3 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.
2026-04-03 17:48:30 +08:00

189 lines
5.5 KiB
TypeScript

import type {
PublisherBindRequest,
PublisherBindResponse,
PublisherLocalPlatformState,
PublisherPluginPingResponse,
PublisherRegisterInstallationPayload,
PublisherRegisterInstallationResult,
PublisherPublishArticleRequest,
PublisherPublishResponse,
} from "@geo/shared-types";
type PluginAction = "ping" | "detectPlatforms" | "registerInstallation" | "bindAccount" | "publishArticle";
type PluginSuccessPayload<T> = {
source: "geo-extension";
type: "geo.publisher.response";
requestId: string;
success: true;
data: T;
};
type PluginErrorPayload = {
source: "geo-extension";
type: "geo.publisher.response";
requestId: string;
success: false;
error: string;
};
type PluginPayloadMap = {
ping: PublisherPluginPingResponse;
detectPlatforms: PublisherLocalPlatformState[];
registerInstallation: PublisherRegisterInstallationResult;
bindAccount: PublisherBindResponse;
publishArticle: PublisherPublishResponse;
};
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isPingPayload(value: unknown): value is PublisherPluginPingResponse {
return isPlainObject(value) && typeof value.installed === "boolean";
}
function isDetectPlatformsPayload(value: unknown): value is PublisherLocalPlatformState[] {
return Array.isArray(value);
}
function isRegisterInstallationPayload(value: unknown): value is PublisherRegisterInstallationResult {
return isPlainObject(value) && typeof value.success === "boolean" && typeof value.plugin_installation_id === "number";
}
function isBindPayload(value: unknown): value is PublisherBindResponse {
return isPlainObject(value) && typeof value.success === "boolean" && typeof value.platform_id === "string";
}
function isPublishPayload(value: unknown): value is PublisherPublishResponse {
return isPlainObject(value) && Array.isArray(value.results);
}
function isPayloadValid<T extends PluginAction>(action: T, value: unknown): value is PluginPayloadMap[T] {
switch (action) {
case "ping":
return isPingPayload(value);
case "detectPlatforms":
return isDetectPlatformsPayload(value);
case "registerInstallation":
return isRegisterInstallationPayload(value);
case "bindAccount":
return isBindPayload(value);
case "publishArticle":
return isPublishPayload(value);
default:
return false;
}
}
function nextRequestId(): string {
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
return crypto.randomUUID();
}
return `geo-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
window.setTimeout(resolve, ms);
});
}
async function requestPlugin<T extends PluginAction>(
action: T,
payload?: T extends "bindAccount"
? PublisherBindRequest
: T extends "registerInstallation"
? PublisherRegisterInstallationPayload
: T extends "publishArticle"
? PublisherPublishArticleRequest
: undefined,
timeoutMs = 4000,
): Promise<PluginPayloadMap[T]> {
if (typeof window === "undefined") {
throw new Error("plugin bridge is only available in browser contexts");
}
const requestId = nextRequestId();
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
cleanup();
reject(new Error("publisher_plugin_timeout"));
}, timeoutMs);
const handleMessage = (event: MessageEvent<PluginSuccessPayload<PluginPayloadMap[T]> | PluginErrorPayload>) => {
if (event.source !== window || !event.data || event.data.type !== "geo.publisher.response") {
return;
}
if (event.data.requestId !== requestId || event.data.source !== "geo-extension") {
return;
}
cleanup();
if (event.data.success) {
if (!isPayloadValid(action, event.data.data)) {
reject(new Error("publisher_plugin_invalid_response"));
return;
}
resolve(event.data.data);
return;
}
reject(new Error(event.data.error || "publisher_plugin_unknown_error"));
};
const cleanup = () => {
window.clearTimeout(timer);
window.removeEventListener("message", handleMessage);
};
window.addEventListener("message", handleMessage);
window.postMessage(
{
source: "geo-admin",
type: "geo.publisher.request",
requestId,
action,
payload,
},
window.location.origin,
);
});
}
export async function pingPublisherPlugin(): Promise<PublisherPluginPingResponse> {
try {
return await requestPlugin("ping", undefined, 1500);
} catch {
try {
await sleep(300);
return await requestPlugin("ping", undefined, 2000);
} catch {
return {
installed: false,
capabilities: [],
};
}
}
}
export async function detectPublisherPlatforms(): Promise<PublisherLocalPlatformState[]> {
return requestPlugin("detectPlatforms", undefined, 2500);
}
export async function registerPublisherInstallation(
payload: PublisherRegisterInstallationPayload,
): Promise<PublisherRegisterInstallationResult> {
return requestPlugin("registerInstallation", payload, 5000);
}
export async function bindPublisherPlatform(payload: PublisherBindRequest): Promise<PublisherBindResponse> {
return requestPlugin("bindAccount", payload, 10000);
}
export async function publishWithPublisherPlugin(
payload: PublisherPublishArticleRequest,
): Promise<PublisherPublishResponse> {
return requestPlugin("publishArticle", payload, 25000);
}