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.
This commit is contained in:
2026-04-03 17:48:30 +08:00
parent 32d6a462cd
commit 134dd063c3
33 changed files with 2722 additions and 457 deletions
@@ -0,0 +1,210 @@
import type { StoredPlatformState } from "../platforms";
import {
connectedPlatform,
disconnectedPlatform,
fetchImageBlob,
fetchJson,
fetchText,
manageUrlResult,
normalizeArticleHtml,
uploadHtmlImages,
} from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type BaijiahaoAppInfoResponse = {
data?: {
user?: {
userid?: string | number;
name?: string;
uname?: string;
username?: string;
avatar?: string;
app_id?: string;
};
};
};
type BaijiahaoPublishResponse = {
errno?: number;
errmsg?: string;
ret?: {
id?: string | number;
};
};
type BaijiahaoUploadResponse = {
ret?: {
https_url?: string;
};
};
type BaijiahaoCutResponse = {
data?: {
new_url?: string;
};
};
async function fetchAppInfo(): Promise<NonNullable<BaijiahaoAppInfoResponse["data"]>["user"] | null> {
const response = await fetchJson<BaijiahaoAppInfoResponse>("https://baijiahao.baidu.com/builder/app/appinfo", {
headers: {
accept: "application/json, text/plain, */*",
},
}).catch(() => null);
return response?.data?.user ?? null;
}
async function detectBaijiahao(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const user = await fetchAppInfo();
const uid = user?.userid != null ? String(user.userid) : "";
const nickname = user?.name || user?.uname || user?.username || "";
if (!uid || !nickname) {
return disconnectedPlatform(platform, "未检测到百家号登录态");
}
return connectedPlatform(platform, uid, nickname, user?.avatar ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "百家号登录检测失败");
}
}
async function getPublishToken(): Promise<string> {
const html = await fetchText("https://baijiahao.baidu.com/builder/rc/edit?type=news", {
headers: {
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
},
});
return /window\.__BJH__INIT__AUTH__\s*=\s*(["'])(.*?)\1/.exec(html)?.[2] ?? "";
}
async function uploadImage(sourceUrl: string, appId: string, cropCover: boolean): Promise<string | null> {
const blob = await fetchImageBlob(sourceUrl);
if (!blob) {
return null;
}
const form = new FormData();
form.append("media", blob, "image.png");
form.append("org_file_name", "image.png");
form.append("type", "image");
form.append("app_id", appId);
form.append("is_waterlog", "0");
form.append("save_material", "1");
form.append("no_compress", "0");
form.append("article_type", "news");
const uploaded = await fetchJson<BaijiahaoUploadResponse>("https://baijiahao.baidu.com/materialui/picture/uploadProxy", {
method: "POST",
body: form,
}).catch(() => null);
const uploadedUrl = uploaded?.ret?.https_url ?? null;
if (!uploadedUrl) {
return null;
}
if (!cropCover) {
return uploadedUrl;
}
const cut = await fetchJson<BaijiahaoCutResponse>("https://baijiahao.baidu.com/materialui/picture/auto_cutting", {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
org_url: uploadedUrl,
type: "news",
cutting_type: "cover_image",
}),
}).catch(() => null);
return cut?.data?.new_url ?? uploadedUrl;
}
async function publishBaijiahao(context: AdapterContext): Promise<PlatformPublishResult> {
const user = await fetchAppInfo();
const appId = user?.app_id ?? "";
if (!user?.userid || !appId) {
throw new Error("baijiahao_not_logged_in");
}
const token = await getPublishToken();
if (!token) {
throw new Error("baijiahao_token_missing");
}
const content = normalizeArticleHtml(context.article);
const processed = await uploadHtmlImages(
content,
async (sourceUrl) => uploadImage(sourceUrl, appId, false),
["baijiahao.baidu.com", "bdstatic.com", "bcebos.com"],
);
const coverUrl = context.article.cover_asset_url?.trim()
? await uploadImage(context.article.cover_asset_url.trim(), appId, true)
: null;
const endpoint =
context.article.publish_type === "publish"
? "https://baijiahao.baidu.com/pcui/article/publish"
: "https://baijiahao.baidu.com/pcui/article/save";
const response = await fetchJson<BaijiahaoPublishResponse>(endpoint, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
Token: token,
},
body: new URLSearchParams({
type: "news",
title: context.article.title,
content: processed.html,
abstract_from: "1",
cover_layout: coverUrl ? "one" : "",
cover_images: coverUrl
? JSON.stringify([
{
src: coverUrl,
cropData: {},
machine_chooseimg: 0,
isLegal: 0,
cover_source_tag: "local",
},
])
: "",
activity_list: JSON.stringify([
{ id: "ttv", is_checked: 0 },
{ id: "ai_tts", is_checked: 0 },
{ id: "aigc_bjh_status", is_checked: 1 },
{ id: "reward", is_checked: 0 },
]),
}),
});
const articleId = response?.ret?.id != null ? String(response.ret.id) : "";
if (response.errno !== 0 || !articleId) {
throw new Error(response.errmsg || "baijiahao_publish_failed");
}
const manageUrl = `https://baijiahao.baidu.com/builder/rc/edit?type=news&article_id=${articleId}`;
if (context.article.publish_type === "draft") {
return manageUrlResult(true, "pending_review", articleId, manageUrl, "百家号草稿保存成功。");
}
return manageUrlResult(
true,
"success",
articleId,
manageUrl,
"百家号发布成功,公开链接待补查。",
null,
);
}
export const baijiahaoAdapter: PlatformAdapter = {
platformId: "baijiahao",
detect: detectBaijiahao,
publish: publishBaijiahao,
};
@@ -0,0 +1,48 @@
import type { StoredPlatformState } from "../platforms";
import { connectedPlatform, disconnectedPlatform, fetchJson, getCookieString, unsupportedPublishResult } from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type BilibiliNavResponse = {
code?: number;
data?: {
isLogin?: boolean;
mid?: string | number;
uname?: string;
face?: string;
};
};
async function detectBilibili(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const cookie = await getCookieString("bilibili.com");
const response = await fetchJson<BilibiliNavResponse>(
"https://api.bilibili.com/x/web-interface/nav?build=0&mobi_app=web",
{
headers: {
accept: "application/json, text/plain, */*",
...(cookie ? { cookie } : {}),
},
},
);
const user = response.data;
const uid = user?.mid != null ? String(user.mid) : "";
if (response.code !== 0 || !user?.isLogin || !uid || !user.uname) {
return disconnectedPlatform(platform, "未检测到 bilibili 登录态");
}
return connectedPlatform(platform, uid, user.uname, user.face ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "bilibili 登录检测失败");
}
}
async function publishBilibili(): Promise<PlatformPublishResult> {
return unsupportedPublishResult("bilibili");
}
export const bilibiliAdapter: PlatformAdapter = {
platformId: "bilibili",
detect: detectBilibili,
publish: (_context: AdapterContext) => publishBilibili(),
};
@@ -0,0 +1,279 @@
import type { PublisherPublishArticleRequest } from "@geo/shared-types";
import { marked } from "marked";
import { browser } from "wxt/browser";
import type { StoredPlatformState } from "../platforms";
import type { PlatformPublishResult } from "./types";
export function normalizeRemoteUrl(value?: string | null): string | null {
const trimmed = value?.trim();
if (!trimmed) {
return null;
}
if (/^(data|blob|chrome-extension):/i.test(trimmed)) {
return trimmed;
}
if (/^https?:\/\//i.test(trimmed)) {
return trimmed;
}
if (/^\/\//.test(trimmed)) {
return `https:${trimmed}`;
}
if (/^[a-z][a-z\d+\-.]*:/i.test(trimmed)) {
return trimmed;
}
return `https://${trimmed.replace(/^\/+/, "")}`;
}
export function connectedPlatform(
platform: StoredPlatformState,
platformUid: string,
nickname: string,
avatarUrl?: string | null,
): StoredPlatformState {
return {
...platform,
connected: true,
platform_uid: platformUid,
nickname,
avatar_url: normalizeRemoteUrl(avatarUrl),
message: null,
};
}
export function disconnectedPlatform(platform: StoredPlatformState, message: string): StoredPlatformState {
return {
...platform,
connected: false,
platform_uid: null,
nickname: null,
avatar_url: null,
message,
};
}
export function unsupportedPublishResult(platformName: string): PlatformPublishResult {
return {
success: false,
status: "failed",
externalArticleId: null,
externalArticleUrl: null,
externalManageUrl: null,
message: `${platformName} 暂未接入真实发布链路。`,
responsePayload: {
mode: "unsupported-adapter",
platform: platformName,
},
};
}
export async function getCookieValue(domain: string, name: string): Promise<string> {
const cookies = await browser.cookies.getAll({ domain });
return cookies.find((item) => item.name === name)?.value ?? "";
}
export async function getCookieString(domain: string): Promise<string> {
const cookies = await browser.cookies.getAll({ domain });
return cookies.map((item) => `${item.name}=${item.value}`).join("; ");
}
export async function fetchText(input: string, init?: RequestInit): Promise<string> {
const response = await fetch(input, {
...init,
credentials: init?.credentials ?? "include",
});
const text = await response.text();
if (!response.ok) {
throw new Error(text || `request_failed_${response.status}`);
}
return text;
}
export async function fetchJson<T>(input: string, init?: RequestInit): Promise<T> {
const text = await fetchText(input, init);
if (!text) {
return {} as T;
}
return JSON.parse(text) as T;
}
export function markdownToHtml(markdown: string): string {
const source = markdown.trim();
if (!source) {
return "";
}
return marked.parse(source, {
async: false,
gfm: true,
breaks: false,
}) as string;
}
export function normalizeArticleHtml(article: PublisherPublishArticleRequest): string {
const markdown = article.markdown_content?.trim();
const source = markdown ? markdownToHtml(markdown) : article.html_content?.trim() || "";
let next = source.trim();
next = next.replace(/<section\b/gi, "<div").replace(/<\/section>/gi, "</div>");
next = next.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, "");
next = next.replace(/\sstyle="[^"]*"/gi, "");
next = next.replace(/\sdata-[a-z-]+="[^"]*"/gi, "");
next = next.replace(/<figure[^>]*>\s*(<img[\s\S]*?>)\s*<\/figure>/gi, "$1");
return next;
}
export function extractImageSources(html: string): string[] {
const sources = new Set<string>();
for (const match of html.matchAll(/<img\b[^>]*src=(['"])(.*?)\1[^>]*>/gi)) {
const src = match[2]?.trim();
if (src) {
sources.add(src);
}
}
return [...sources];
}
export function extractMarkdownImageSources(markdown: string): string[] {
const sources = new Set<string>();
for (const match of markdown.matchAll(/!\[[^\]]*]\((?:<([^>\s]+)>|([^\s)]+))(?:\s+["'][^"']*["'])?\)/g)) {
const src = match[1] || match[2];
const normalized = src?.trim();
if (normalized) {
sources.add(normalized);
}
}
for (const source of extractImageSources(markdown)) {
sources.add(source);
}
return [...sources];
}
function shouldSkipSource(source: string, skipPatterns?: Array<string | RegExp>): boolean {
if (!skipPatterns?.length) {
return false;
}
return skipPatterns.some((pattern) =>
typeof pattern === "string" ? source.includes(pattern) : pattern.test(source),
);
}
export async function uploadHtmlImages(
html: string,
uploader: (sourceUrl: string) => Promise<string | null>,
skipPatterns?: Array<string | RegExp>,
): Promise<{ html: string; uploaded: Map<string, string> }> {
const sources = extractImageSources(html);
if (!sources.length) {
return {
html,
uploaded: new Map(),
};
}
const uploaded = new Map<string, string>();
for (const source of sources) {
if (shouldSkipSource(source, skipPatterns)) {
continue;
}
const target = await uploader(source);
if (target) {
uploaded.set(source, target);
}
}
let next = html;
for (const [from, to] of uploaded.entries()) {
next = next.split(from).join(to);
}
return { html: next, uploaded };
}
export async function uploadMarkdownImages(
markdown: string,
uploader: (sourceUrl: string) => Promise<string | null>,
skipPatterns?: Array<string | RegExp>,
): Promise<{ markdown: string; uploaded: Map<string, string> }> {
const sources = extractMarkdownImageSources(markdown);
if (!sources.length) {
return {
markdown,
uploaded: new Map(),
};
}
const uploaded = new Map<string, string>();
for (const source of sources) {
if (shouldSkipSource(source, skipPatterns)) {
continue;
}
const target = await uploader(source);
if (target) {
uploaded.set(source, target);
}
}
let next = markdown;
for (const [from, to] of uploaded.entries()) {
next = next.split(from).join(to);
}
return { markdown: next, uploaded };
}
export async function fetchImageBlob(sourceUrl: string): Promise<Blob | null> {
const normalizedUrl = normalizeRemoteUrl(sourceUrl);
if (!normalizedUrl) {
return null;
}
const response = await fetch(normalizedUrl).catch(() => null);
if (!response?.ok) {
return null;
}
const blob = await response.blob().catch(() => null);
if (!blob || !blob.type.startsWith("image/")) {
return null;
}
return blob;
}
export function randomHex(length: number): string {
const chars = "0123456789abcdef";
let result = "";
for (let index = 0; index < length; index += 1) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export function articleMarkdown(article: PublisherPublishArticleRequest): string {
return article.markdown_content || "";
}
export function manageUrlResult(
success: boolean,
status: PlatformPublishResult["status"],
externalArticleId: string | null,
externalManageUrl: string | null,
message: string,
externalArticleUrl?: string | null,
): PlatformPublishResult {
return {
success,
status,
externalArticleId,
externalArticleUrl: externalArticleUrl ?? null,
externalManageUrl,
message,
};
}
@@ -0,0 +1,38 @@
import type { StoredPlatformState } from "../platforms";
import { connectedPlatform, disconnectedPlatform, fetchJson, unsupportedPublishResult } from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type DongchediAccountInfoResponse = {
data?: {
user_id_str?: string;
name?: string;
avatar_url?: string;
};
};
async function detectDongchedi(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const response = await fetchJson<DongchediAccountInfoResponse>(
"https://mp.dcdapp.com/passport/account/info/v2/?aid=2302&account_sdk_source=web",
).catch(() => null);
const user = response?.data;
if (!user?.user_id_str || !user.name) {
return disconnectedPlatform(platform, "未检测到懂车帝登录态");
}
return connectedPlatform(platform, user.user_id_str, user.name, user.avatar_url ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "懂车帝登录检测失败");
}
}
async function publishDongchedi(): Promise<PlatformPublishResult> {
return unsupportedPublishResult("懂车帝");
}
export const dongchediAdapter: PlatformAdapter = {
platformId: "dongchedi",
detect: detectDongchedi,
publish: (_context: AdapterContext) => publishDongchedi(),
};
+29 -1
View File
@@ -1,9 +1,37 @@
import type { StoredPlatformState } from "../platforms";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
import { baijiahaoAdapter } from "./baijiahao";
import { bilibiliAdapter } from "./bilibili";
import { dongchediAdapter } from "./dongchedi";
import { jianshuAdapter } from "./jianshu";
import { juejinAdapter } from "./juejin";
import { qiehaoAdapter } from "./qiehao";
import { smzdmAdapter } from "./smzdm";
import { sohuhaoAdapter } from "./sohuhao";
import { toutiaohaoAdapter } from "./toutiaohao";
import { wangyihaoAdapter } from "./wangyihao";
import { weixinGzhAdapter } from "./weixin_gzh";
import { zhihuAdapter } from "./zhihu";
import { zolAdapter } from "./zol";
const adapters = new Map<string, PlatformAdapter>([[zhihuAdapter.platformId, zhihuAdapter]]);
const adapters = new Map<string, PlatformAdapter>(
[
zhihuAdapter,
toutiaohaoAdapter,
baijiahaoAdapter,
sohuhaoAdapter,
qiehaoAdapter,
jianshuAdapter,
bilibiliAdapter,
juejinAdapter,
wangyihaoAdapter,
smzdmAdapter,
weixinGzhAdapter,
zolAdapter,
dongchediAdapter,
].map((adapter) => [adapter.platformId, adapter]),
);
export function getPlatformAdapter(platformId: string): PlatformAdapter | undefined {
return adapters.get(platformId);
@@ -0,0 +1,222 @@
import type { StoredPlatformState } from "../platforms";
import {
articleMarkdown,
connectedPlatform,
disconnectedPlatform,
fetchImageBlob,
fetchJson,
manageUrlResult,
normalizeArticleHtml,
randomHex,
uploadHtmlImages,
uploadMarkdownImages,
} from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type JianshuUser = {
id?: string | number;
nickname?: string;
avatar?: string;
preferred_note_type?: string;
};
type JianshuNotebook = {
id?: string | number;
};
type JianshuNote = {
id?: string | number;
};
type JianshuUploadToken = {
token?: string;
key?: string;
};
type JianshuErrorResponse = {
error?: Array<{
message?: string;
}>;
};
const JIANSHU_JSON_HEADERS = {
Accept: "application/json",
"Content-Type": "application/json; charset=UTF-8",
};
async function fetchCurrentUser(): Promise<JianshuUser | null> {
return fetchJson<JianshuUser>("https://www.jianshu.com/author/current_user", {
headers: {
accept: "application/json",
},
}).catch(() => null);
}
async function detectJianshu(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const user = await fetchCurrentUser();
const uid = user?.id != null ? String(user.id) : "";
if (!uid || !user?.nickname) {
return disconnectedPlatform(platform, "未检测到简书登录态");
}
return connectedPlatform(platform, uid, user.nickname, user.avatar ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "简书登录检测失败");
}
}
async function uploadImage(noteId: string, sourceUrl: string): Promise<string | null> {
const blob = await fetchImageBlob(sourceUrl);
if (!blob) {
return null;
}
const uploadToken = await fetchJson<JianshuUploadToken>(
`https://www.jianshu.com/upload_images/token.json?filename=${noteId}.png`,
{
headers: {
accept: "application/json",
},
},
).catch(() => null);
if (!uploadToken?.token || !uploadToken.key) {
return null;
}
const form = new FormData();
form.append("token", uploadToken.token);
form.append("key", uploadToken.key);
form.append("file", blob, `${noteId}.png`);
form.append("x:protocol", "https");
const uploaded = await fetchJson<{ url?: string }>("https://upload.qiniup.com", {
method: "POST",
headers: {
accept: "application/json",
},
body: form,
}).catch(() => null);
return uploaded?.url ?? null;
}
async function publishJianshu(context: AdapterContext): Promise<PlatformPublishResult> {
const user = await fetchCurrentUser();
if (!user?.id || !user.nickname) {
throw new Error("jianshu_not_logged_in");
}
const notebooks = await fetchJson<JianshuNotebook[]>("https://www.jianshu.com/author/notebooks", {
headers: {
Accept: "application/json",
},
});
const notebookId = notebooks[0]?.id != null ? String(notebooks[0].id) : "";
if (!notebookId) {
throw new Error("jianshu_notebook_missing");
}
let noteId = "";
try {
const created = await fetchJson<JianshuNote>("https://www.jianshu.com/author/notes", {
method: "POST",
headers: JIANSHU_JSON_HEADERS,
body: JSON.stringify({
notebook_id: notebookId,
title: context.article.title,
at_bottom: true,
}),
});
noteId = created.id != null ? String(created.id) : "";
if (!noteId) {
throw new Error("jianshu_note_create_failed");
}
const preferredType = user.preferred_note_type || "plain";
const rawMarkdown = articleMarkdown(context.article).trim();
const useMarkdownMode = preferredType !== "plain" && Boolean(rawMarkdown);
const markdownProcessed = useMarkdownMode
? await uploadMarkdownImages(rawMarkdown, async (sourceUrl) => uploadImage(noteId, sourceUrl))
: null;
const htmlProcessed = useMarkdownMode
? null
: await uploadHtmlImages(normalizeArticleHtml(context.article), async (sourceUrl) => uploadImage(noteId, sourceUrl));
await fetchJson(`https://www.jianshu.com/author/notes/${noteId}`, {
method: "PUT",
headers: JIANSHU_JSON_HEADERS,
body: JSON.stringify({
id: noteId,
autosave_control: 1,
title: context.article.title,
content: useMarkdownMode ? (markdownProcessed?.markdown ?? rawMarkdown) : (htmlProcessed?.html ?? ""),
}),
});
if (context.article.cover_asset_url?.trim()) {
const coverUrl = await uploadImage(noteId || randomHex(12), context.article.cover_asset_url.trim());
if (coverUrl) {
await fetchJson(`https://www.jianshu.com/author/notes/${noteId}/update_cover_images`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=UTF-8",
Accept: "application/json",
},
body: JSON.stringify({
flow_type: 14,
abbr: "",
cover_image_urls: [coverUrl],
}),
}).catch(() => null);
}
}
const manageUrl = `https://www.jianshu.com/writer#/notebooks/${notebookId}/notes/${noteId}`;
if (context.article.publish_type === "draft") {
return manageUrlResult(true, "pending_review", noteId, manageUrl, "简书草稿保存成功。");
}
const publishResponse = await fetch(`https://www.jianshu.com/author/notes/${noteId}/publicize`, {
method: "POST",
credentials: "include",
headers: JIANSHU_JSON_HEADERS,
body: JSON.stringify({}),
});
const publishBody = (await publishResponse.json().catch(() => ({}))) as JianshuErrorResponse;
if (!publishResponse.ok || publishBody.error?.[0]?.message) {
throw new Error(publishBody.error?.[0]?.message || "jianshu_publish_failed");
}
noteId = "";
return manageUrlResult(
true,
"success",
created.id != null ? String(created.id) : "",
manageUrl,
"简书发布成功。",
`https://www.jianshu.com/p/${created.id}`,
);
} finally {
if (noteId) {
await fetch(`https://www.jianshu.com/author/notes/${noteId}/soft_destroy`, {
method: "POST",
credentials: "include",
headers: {
Accept: "application/json",
},
}).catch(() => null);
}
}
}
export const jianshuAdapter: PlatformAdapter = {
platformId: "jianshu",
detect: detectJianshu,
publish: publishJianshu,
};
@@ -0,0 +1,46 @@
import type { StoredPlatformState } from "../platforms";
import { connectedPlatform, disconnectedPlatform, fetchJson, unsupportedPublishResult } from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type JuejinProfileResponse = {
data?: {
bui_user?: {
user_id?: string;
screen_name?: string;
avatar_large?: string;
avatar_url?: string;
};
};
};
async function detectJuejin(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const response = await fetchJson<JuejinProfileResponse>("https://api.juejin.cn/user_api/v1/user/get_profile", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
},
body: "{}",
});
const user = response.data?.bui_user;
if (!user?.user_id || !user.screen_name) {
return disconnectedPlatform(platform, "未检测到掘金登录态");
}
return connectedPlatform(platform, user.user_id, user.screen_name, user.avatar_url ?? user.avatar_large ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "掘金登录检测失败");
}
}
async function publishJuejin(): Promise<PlatformPublishResult> {
return unsupportedPublishResult("掘金");
}
export const juejinAdapter: PlatformAdapter = {
platformId: "juejin",
detect: detectJuejin,
publish: (_context: AdapterContext) => publishJuejin(),
};
@@ -0,0 +1,242 @@
import type { StoredPlatformState } from "../platforms";
import {
connectedPlatform,
disconnectedPlatform,
fetchImageBlob,
fetchJson,
getCookieString,
manageUrlResult,
normalizeArticleHtml,
sleep,
uploadHtmlImages,
} from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
const QIEHAO_APP_ID = "LA6zXi1lWzAioIzdiAD6iM10aHarlHF6";
type QiehaoBasicInfoResponse = {
data?: {
cpInfo?: {
mediaId?: string | number;
mediaName?: string;
header?: string;
};
};
};
type QiehaoPublishResponse = {
response?: {
code?: number;
msg?: string;
};
data?: {
articleId?: string | number;
};
msg?: string;
};
type QiehaoArticleListResponse = {
data?: {
articles?: Array<{
article_id?: string | number;
url?: string;
}>;
};
};
type QiehaoUploadStepOneResponse = {
data?: {
url?: {
size?: Record<string, { imageUrl?: string }>;
};
};
};
type QiehaoUploadStepTwoResponse = {
data?: {
url?: string;
};
};
async function fetchBasicInfo() {
const cookie = await getCookieString("om.qq.com");
return fetchJson<QiehaoBasicInfoResponse>("https://om.qq.com/maccountsetting/basicinfo?relogin=1", {
headers: {
accept: "application/json, text/plain, */*",
...(cookie ? { cookie } : {}),
},
}).catch(() => null);
}
async function detectQiehao(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const cpInfo = (await fetchBasicInfo())?.data?.cpInfo;
const uid = cpInfo?.mediaId != null ? String(cpInfo.mediaId) : "";
if (!uid || !cpInfo?.mediaName) {
return disconnectedPlatform(platform, "未检测到企鹅号登录态");
}
return connectedPlatform(platform, uid, cpInfo.mediaName, cpInfo.header ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "企鹅号登录检测失败");
}
}
async function uploadImage(sourceUrl: string, includeCoverMeta: boolean): Promise<string | null> {
const blob = await fetchImageBlob(sourceUrl);
if (!blob) {
return null;
}
const form = new FormData();
form.append("file", blob, "image.png");
form.append("appid", QIEHAO_APP_ID);
form.append("isUpOrg", "1");
form.append("endpoint", "1");
form.append("isRetImgAttr", "1");
form.append("opCode", "151");
const first = await fetchJson<QiehaoUploadStepOneResponse>("https://image.om.qq.com/cpom_pimage/ArchacaleUploadViaFile", {
method: "POST",
body: form,
}).catch(() => null);
const imageUrl = first?.data?.url?.size?.["641"]?.imageUrl ?? null;
if (!imageUrl) {
return null;
}
if (!includeCoverMeta) {
return imageUrl;
}
const second = await fetchJson<QiehaoUploadStepTwoResponse>("https://image.om.qq.com/cpom_pimage/ListImageUploadViaUrl", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
auth: {
appid: QIEHAO_APP_ID,
endpoint: 1,
},
reqData: {
appid: QIEHAO_APP_ID,
isUpOrg: 1,
endpoint: 1,
isRetImgAttr: 1,
opCode: 151,
imageUrl,
},
relogin: 1,
}),
}).catch(() => null);
return second?.data?.url ?? imageUrl;
}
async function publishQiehao(context: AdapterContext): Promise<PlatformPublishResult> {
const cpInfo = (await fetchBasicInfo())?.data?.cpInfo;
const mediaId = cpInfo?.mediaId != null ? String(cpInfo.mediaId) : "";
if (!mediaId) {
throw new Error("qiehao_not_logged_in");
}
const cookie = await getCookieString("om.qq.com");
if (!cookie) {
throw new Error("qiehao_cookie_missing");
}
const content = normalizeArticleHtml(context.article);
const processed = await uploadHtmlImages(content, async (sourceUrl) => uploadImage(sourceUrl, false));
const coverImages = context.article.cover_asset_url?.trim()
? [await uploadImage(context.article.cover_asset_url.trim(), true)].filter(Boolean)
: [];
const uploadedImageList = Array.from(processed.uploaded.values());
const resourceMarkInfo = uploadedImageList.reduce<Record<string, Record<string, unknown>>>((result, imageUrl) => {
result[imageUrl] = {
image_url: imageUrl,
id: imageUrl,
resource_type: "image",
can_modify: 1,
aigc_user_statement: 2,
};
return result;
}, {});
const endpoint =
context.article.publish_type === "publish"
? "https://om.qq.com/marticlepublish/omPublish"
: "https://om.qq.com/marticlepublish/omSave";
const response = await fetchJson<QiehaoPublishResponse>(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
cookie,
},
body: JSON.stringify({
title: context.article.title,
content: processed.html,
imgurl_ext: JSON.stringify(coverImages),
cover_type: "1",
imgurlsrc: "custom",
orignal: 0,
user_original: 0,
mediaId,
needpub: 1,
type: 0,
relogin: 1,
self_declare: '{"id":7,"desc":"作者声明:该文章由AI辅助创作"}',
resource_aigc_mark_info: JSON.stringify(resourceMarkInfo),
}),
});
const articleId = response.data?.articleId != null ? String(response.data.articleId) : "";
if (response.response?.code !== 0 || !articleId) {
throw new Error(response.msg || response.response?.msg || "qiehao_publish_failed");
}
await sleep(1500);
const list = await fetchJson<QiehaoArticleListResponse>(
"https://om.qq.com/marticle/article/list?category=&search=&source=&startDate=&endDate=&num=20&ftype=&readChannel=qb&dstChannel=&isPartDst=0&refreshField=&relogin=1",
{
headers: {
accept: "application/json, text/plain, */*",
cookie,
},
},
).catch(() => null);
const article = list?.data?.articles?.find((item) => String(item.article_id) === articleId);
const articleUrl = article?.url?.trim() || null;
const externalArticleId = articleUrl ? articleUrl.split("/").filter(Boolean).pop() || articleId : articleId;
if (context.article.publish_type === "draft") {
return manageUrlResult(
true,
"pending_review",
externalArticleId,
"https://om.qq.com/article/manage",
"企鹅号草稿保存成功。",
articleUrl,
);
}
return manageUrlResult(
true,
"success",
externalArticleId,
"https://om.qq.com/article/manage",
"企鹅号发布成功。",
articleUrl,
);
}
export const qiehaoAdapter: PlatformAdapter = {
platformId: "qiehao",
detect: detectQiehao,
publish: publishQiehao,
};
@@ -0,0 +1,35 @@
import type { StoredPlatformState } from "../platforms";
import { connectedPlatform, disconnectedPlatform, fetchJson, unsupportedPublishResult } from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type SmzdmCurrentUser = {
smzdm_id?: string | number;
nickname?: string;
audit_nickname?: string;
avatar?: string;
};
async function detectSmzdm(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const user = await fetchJson<SmzdmCurrentUser>("https://zhiyou.smzdm.com/user/info/jsonp_get_current").catch(() => null);
const uid = user?.smzdm_id != null ? String(user.smzdm_id) : "";
const nickname = user?.nickname || user?.audit_nickname || "";
if (!uid || !nickname) {
return disconnectedPlatform(platform, "未检测到什么值得买登录态");
}
return connectedPlatform(platform, uid, nickname, user?.avatar ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "什么值得买登录检测失败");
}
}
async function publishSmzdm(): Promise<PlatformPublishResult> {
return unsupportedPublishResult("什么值得买");
}
export const smzdmAdapter: PlatformAdapter = {
platformId: "smzdm",
detect: detectSmzdm,
publish: (_context: AdapterContext) => publishSmzdm(),
};
@@ -0,0 +1,142 @@
import type { StoredPlatformState } from "../platforms";
import {
connectedPlatform,
disconnectedPlatform,
fetchImageBlob,
fetchJson,
getCookieValue,
manageUrlResult,
normalizeArticleHtml,
randomHex,
uploadHtmlImages,
} from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type SohuRegisterInfoResponse = {
data?: {
account?: {
id?: string | number;
nickName?: string;
avatar?: string;
};
};
};
type SohuPublishResponse = {
code?: number;
msg?: string;
data?: string | number;
};
type SohuUploadResponse = {
url?: string;
msg?: string;
};
function generateDeviceId(): string {
return randomHex(32);
}
async function fetchAccountInfo() {
const response = await fetchJson<SohuRegisterInfoResponse>("https://mp.sohu.com/mpbp/bp/account/register-info").catch(
() => null,
);
return response?.data?.account ?? null;
}
async function detectSohuhao(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const account = await fetchAccountInfo();
const uid = account?.id != null ? String(account.id) : "";
if (!uid || !account?.nickName) {
return disconnectedPlatform(platform, "未检测到搜狐号登录态");
}
return connectedPlatform(platform, uid, account.nickName, account.avatar ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "搜狐号登录检测失败");
}
}
async function uploadImage(sourceUrl: string, accountId: string, spCm: string): Promise<string | null> {
const blob = await fetchImageBlob(sourceUrl);
if (!blob) {
return null;
}
const form = new FormData();
form.append("file", blob, "image.png");
form.append("accountId", accountId);
const response = await fetchJson<SohuUploadResponse>(
`https://mp.sohu.com/commons/front/outerUpload/image/file?accountId=${accountId}`,
{
method: "POST",
headers: {
"Sp-Cm": spCm,
},
body: form,
},
).catch(() => null);
return response?.url ?? null;
}
async function publishSohuhao(context: AdapterContext): Promise<PlatformPublishResult> {
const account = await fetchAccountInfo();
const accountId = account?.id != null ? String(account.id) : "";
if (!accountId) {
throw new Error("sohuhao_not_logged_in");
}
const spCm = (await getCookieValue("sohu.com", "mp-cv")) || `100-${Date.now()}-${generateDeviceId()}`;
const content = normalizeArticleHtml(context.article);
const processed = await uploadHtmlImages(content, async (sourceUrl) => uploadImage(sourceUrl, accountId, spCm), [
"sohu.com",
]);
const coverUrl = context.article.cover_asset_url?.trim()
? await uploadImage(context.article.cover_asset_url.trim(), accountId, spCm)
: "";
const endpoint =
context.article.publish_type === "publish"
? "https://mp.sohu.com/mpbp/bp/news/v4/news/publish/v2"
: "https://mp.sohu.com/mpbp/bp/news/v4/news/draft/v2";
const response = await fetchJson<SohuPublishResponse>(`${endpoint}?accountId=${accountId}`, {
method: "POST",
headers: {
"content-type": "application/json",
"Sp-Cm": spCm,
},
body: JSON.stringify({
title: context.article.title,
content: processed.html,
brief: "",
channelId: 30,
categoryId: -1,
cover: coverUrl,
accountId,
infoResource: 2,
}),
});
const articleId = response.data != null ? String(response.data) : "";
if (response.code !== 2000000 || !articleId) {
throw new Error(response.msg || "sohuhao_publish_failed");
}
const manageUrl = `https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle?spm=smmp.articlelist.0.0&contentStatus=2&id=${articleId}`;
if (context.article.publish_type === "draft") {
return manageUrlResult(true, "pending_review", articleId, manageUrl, "搜狐号草稿保存成功。");
}
return manageUrlResult(true, "success", articleId, manageUrl, "搜狐号发布成功,公开链接待补查。");
}
export const sohuhaoAdapter: PlatformAdapter = {
platformId: "sohuhao",
detect: detectSohuhao,
publish: publishSohuhao,
};
@@ -0,0 +1,249 @@
import type { StoredPlatformState } from "../platforms";
import {
connectedPlatform,
disconnectedPlatform,
fetchImageBlob,
fetchJson,
manageUrlResult,
normalizeArticleHtml,
uploadHtmlImages,
} from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type ToutiaoMediaInfoResponse = {
data?: {
user?: {
id_str?: string;
screen_name?: string;
https_avatar_url?: string;
};
media?: {
has_third_party_ad_permission?: boolean;
has_toutiao_ad_permission?: boolean;
};
};
code?: number;
message?: string;
};
type ToutiaoUploadPictureResponse = {
url?: string;
web_uri?: string;
origin_web_uri?: string;
rigin_web_uri?: string;
width?: number;
height?: number;
};
type ToutiaoSpiceImageResponse = {
data?: {
image_url?: string;
image_uri?: string;
image_width?: number;
image_height?: number;
};
};
type ToutiaoPublishResponse = {
code?: number;
message?: string;
data?: {
pgc_id?: string | number;
};
};
async function fetchMediaInfo(): Promise<ToutiaoMediaInfoResponse["data"] | null> {
const response = await fetchJson<ToutiaoMediaInfoResponse>("https://mp.toutiao.com/mp/agw/media/get_media_info", {
headers: {
accept: "application/json, text/plain, */*",
},
}).catch(() => null);
return response?.data ?? null;
}
async function detectToutiaohao(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const data = await fetchMediaInfo();
const user = data?.user;
if (!user?.id_str || !user.screen_name) {
return disconnectedPlatform(platform, "未检测到头条号登录态");
}
return connectedPlatform(platform, user.id_str, user.screen_name, user.https_avatar_url ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "头条号登录检测失败");
}
}
async function uploadCover(sourceUrl: string, publishType: "publish" | "draft") {
const blob = await fetchImageBlob(sourceUrl);
if (!blob) {
return null;
}
if (publishType === "publish") {
const form = new FormData();
form.append("upfile", blob, "cover.png");
const uploaded = await fetchJson<ToutiaoUploadPictureResponse>(
"https://mp.toutiao.com/mp/agw/article_material/photo/upload_picture",
{
method: "POST",
body: form,
},
).catch(() => null);
if (!uploaded?.url || !uploaded.web_uri) {
return null;
}
return {
id: 0,
url: uploaded.url,
uri: uploaded.web_uri,
origin_uri: uploaded.origin_web_uri ?? uploaded.rigin_web_uri ?? "",
thumb_width: uploaded.width ?? 0,
thumb_height: uploaded.height ?? 0,
};
}
const form = new FormData();
form.append("image", blob, "cover.png");
const first = await fetchJson<ToutiaoSpiceImageResponse>("https://mp.toutiao.com/spice/image?device_platform=web", {
method: "POST",
body: form,
}).catch(() => null);
const imageUrl = first?.data?.image_url;
if (!imageUrl) {
return null;
}
const second = new FormData();
second.append("imageUrl", imageUrl);
const final = await fetchJson<ToutiaoSpiceImageResponse>(
"https://mp.toutiao.com/spice/image?device_platform=web&need_cover_url=1",
{
method: "POST",
body: second,
},
).catch(() => null);
if (!final?.data?.image_url || !first.data?.image_uri) {
return null;
}
return {
id: "",
url: final.data.image_url,
uri: first.data.image_uri,
ic_uri: "",
thumb_width: first.data.image_width ?? 0,
thumb_height: first.data.image_height ?? 0,
extra: {
from_content_uri: "",
from_content: "0",
},
};
}
async function uploadContentImage(sourceUrl: string): Promise<string | null> {
const blob = await fetchImageBlob(sourceUrl);
if (!blob) {
return null;
}
const form = new FormData();
form.append("image", blob, "image.png");
const uploaded = await fetchJson<ToutiaoSpiceImageResponse>("https://mp.toutiao.com/spice/image?device_platform=web", {
method: "POST",
body: form,
}).catch(() => null);
return uploaded?.data?.image_url ?? null;
}
async function publishToutiaohao(context: AdapterContext): Promise<PlatformPublishResult> {
const mediaInfo = await fetchMediaInfo();
const user = mediaInfo?.user;
if (!user?.id_str || !user.screen_name) {
throw new Error("toutiaohao_not_logged_in");
}
const content = normalizeArticleHtml(context.article);
const processed = await uploadHtmlImages(content, uploadContentImage);
const cover = context.article.cover_asset_url?.trim()
? await uploadCover(context.article.cover_asset_url.trim(), context.article.publish_type)
: null;
const hasAdPermission = Boolean(
mediaInfo?.media?.has_third_party_ad_permission || mediaInfo?.media?.has_toutiao_ad_permission,
);
const body = new URLSearchParams({
content: processed.html,
title: context.article.title,
mp_editor_stat: JSON.stringify({ code_block: 1 }),
is_refute_rumor: "0",
save: context.article.publish_type === "publish" ? "1" : "0",
timer_status: "0",
draft_form_data: JSON.stringify({ coverType: 1 }),
article_ad_type: hasAdPermission ? "3" : "2",
pgc_feed_covers: JSON.stringify(cover ? [cover] : []),
is_fans_article: "0",
govern_forward: "0",
praise: "0",
disable_praise: "0",
tree_plan_article: "0",
activity_tag: "0",
trends_writing_tag: "0",
claim_exclusive: "0",
info_source: JSON.stringify({
source_type: 3,
source_author_uid: "",
time_format: "",
position: {},
}),
});
const response = await fetchJson<ToutiaoPublishResponse>(
"https://mp.toutiao.com/mp/agw/article/publish?source=mp&type=article&aid=1231",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body,
},
);
const articleId = response.data?.pgc_id != null ? String(response.data.pgc_id) : "";
if (response.code !== 0 || !articleId) {
throw new Error(response.message || "toutiaohao_publish_failed");
}
if (context.article.publish_type === "draft") {
return manageUrlResult(
true,
"pending_review",
articleId,
"https://mp.toutiao.com/profile_v4/graphic/publish",
"头条号草稿保存成功。",
);
}
return manageUrlResult(
true,
"success",
articleId,
"https://mp.toutiao.com/profile_v4/index",
"头条号发布成功。",
`https://www.toutiao.com/article/${articleId}/`,
);
}
export const toutiaohaoAdapter: PlatformAdapter = {
platformId: "toutiaohao",
detect: detectToutiaohao,
publish: publishToutiaohao,
};
@@ -0,0 +1,38 @@
import type { StoredPlatformState } from "../platforms";
import { connectedPlatform, disconnectedPlatform, fetchJson, unsupportedPublishResult } from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type WangyiInfoResponse = {
data?: {
mediaInfo?: {
userId?: string | number;
tname?: string;
icon?: string;
};
};
};
async function detectWangyihao(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const response = await fetchJson<WangyiInfoResponse>("https://mp.163.com/wemedia/info.do").catch(() => null);
const media = response?.data?.mediaInfo;
const uid = media?.userId != null ? String(media.userId) : "";
if (!uid || !media?.tname) {
return disconnectedPlatform(platform, "未检测到网易号登录态");
}
return connectedPlatform(platform, uid, media.tname, media.icon ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "网易号登录检测失败");
}
}
async function publishWangyihao(): Promise<PlatformPublishResult> {
return unsupportedPublishResult("网易号");
}
export const wangyihaoAdapter: PlatformAdapter = {
platformId: "wangyihao",
detect: detectWangyihao,
publish: (_context: AdapterContext) => publishWangyihao(),
};
@@ -0,0 +1,34 @@
import type { StoredPlatformState } from "../platforms";
import { connectedPlatform, disconnectedPlatform, fetchText, unsupportedPublishResult } from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
async function detectWeixinGzh(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const html = await fetchText("https://mp.weixin.qq.com/");
const token = html.match(/data:\s*\{[\s\S]*?t:\s*["']([^"']+)["']/)?.[1] ?? "";
const userName = html.match(/user_name:\s*["']([^"']+)["']/)?.[1] ?? "";
const nickName = html.match(/nick_name:\s*["']([^"']+)["']/)?.[1] ?? "";
const avatarFromHead = html.match(/class="weui-desktop-account__thumb"[^>]*src="([^"]+)"/)?.[1] ?? "";
const avatarFromMeta = html.match(/head_img:\s*['"]([^'"]+)['"]/)?.[1] ?? "";
const avatarUrl = (avatarFromHead || avatarFromMeta || "").replace(/^http:\/\//, "https://");
if (!token || !userName || !nickName) {
return disconnectedPlatform(platform, "未检测到公众号登录态");
}
return connectedPlatform(platform, userName, nickName, avatarUrl || null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "公众号登录检测失败");
}
}
async function publishWeixinGzh(): Promise<PlatformPublishResult> {
return unsupportedPublishResult("微信公众号");
}
export const weixinGzhAdapter: PlatformAdapter = {
platformId: "weixin_gzh",
detect: detectWeixinGzh,
publish: (_context: AdapterContext) => publishWeixinGzh(),
};
+2 -1
View File
@@ -4,6 +4,7 @@ import { marked } from "marked";
import type { StoredPlatformState } from "../platforms";
import { normalizeRemoteUrl } from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type ZhihuMeResponse = {
@@ -232,7 +233,7 @@ async function detectZhihu(platform: StoredPlatformState): Promise<StoredPlatfor
connected: true,
platform_uid: uid,
nickname: me.name,
avatar_url: me.avatar_url ?? null,
avatar_url: normalizeRemoteUrl(me.avatar_url),
message: null,
};
} catch (error) {
@@ -0,0 +1,41 @@
import type { StoredPlatformState } from "../platforms";
import { connectedPlatform, disconnectedPlatform, fetchJson, unsupportedPublishResult } from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
type ZolUserInfoResponse = {
data?: {
userId?: string | number;
nickName?: string;
photo?: string;
};
};
async function detectZol(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const response = await fetchJson<ZolUserInfoResponse>("https://open-api.zol.com.cn/api/v1/creator.user.getinfo", {
headers: {
accept: "application/json, text/plain, */*",
},
}).catch(() => null);
const user = response?.data;
const uid = user?.userId != null ? String(user.userId) : "";
if (!uid || !user?.nickName) {
return disconnectedPlatform(platform, "未检测到中关村在线登录态");
}
return connectedPlatform(platform, uid, user.nickName, user.photo ?? null);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "中关村在线登录检测失败");
}
}
async function publishZol(): Promise<PlatformPublishResult> {
return unsupportedPublishResult("中关村在线");
}
export const zolAdapter: PlatformAdapter = {
platformId: "zol",
detect: detectZol,
publish: (_context: AdapterContext) => publishZol(),
};