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:
@@ -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(),
|
||||
};
|
||||
@@ -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(),
|
||||
};
|
||||
@@ -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(),
|
||||
};
|
||||
@@ -128,19 +128,15 @@ export function createDefaultPlatformStates(): StoredPlatformState[] {
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildPublishedUrl(platformId: string, externalArticleId: string): string {
|
||||
export function buildPublishedUrl(platformId: string, externalArticleId: string): string | null {
|
||||
switch (platformId) {
|
||||
case "toutiaohao":
|
||||
return `https://www.toutiao.com/article/${externalArticleId}/`;
|
||||
case "zhihu":
|
||||
return `https://zhuanlan.zhihu.com/p/${externalArticleId}`;
|
||||
case "bilibili":
|
||||
return `https://www.bilibili.com/read/cv${externalArticleId}/`;
|
||||
case "juejin":
|
||||
return `https://juejin.cn/post/${externalArticleId}`;
|
||||
case "jianshu":
|
||||
return `https://www.jianshu.com/p/${externalArticleId}`;
|
||||
default:
|
||||
return `https://example.com/${platformId}/${externalArticleId}`;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,30 @@ function normalizeText(value?: string | null): string | null {
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function logPublishRuntime(level: "info" | "warn" | "error", event: string, payload: Record<string, unknown>): void {
|
||||
const prefix = `[geo-publisher] ${event}`;
|
||||
if (level === "error") {
|
||||
console.error(prefix, payload);
|
||||
return;
|
||||
}
|
||||
if (level === "warn") {
|
||||
console.warn(prefix, payload);
|
||||
return;
|
||||
}
|
||||
console.info(prefix, payload);
|
||||
}
|
||||
|
||||
function normalizeUrl(value?: string | null, platformId?: string | null, externalArticleId?: string | null): string | null {
|
||||
const explicit = normalizeText(value);
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
if (!platformId || !externalArticleId) {
|
||||
return null;
|
||||
}
|
||||
return normalizeText(buildPublishedUrl(platformId, externalArticleId));
|
||||
}
|
||||
|
||||
async function postCallback<T>(
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
@@ -51,25 +75,78 @@ async function postCallback<T>(
|
||||
return body.data;
|
||||
}
|
||||
|
||||
function nextExternalArticleId(taskId: number): string {
|
||||
return `${Date.now()}${String(taskId).slice(-4)}`;
|
||||
}
|
||||
|
||||
function asFailedAdapterResult(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "publisher_adapter_failed";
|
||||
return {
|
||||
success: false,
|
||||
status: "failed" as const,
|
||||
externalArticleId: null,
|
||||
externalArticleUrl: null,
|
||||
externalManageUrl: null,
|
||||
message: error instanceof Error ? error.message : "publisher_adapter_failed",
|
||||
message,
|
||||
responsePayload: {
|
||||
mode: "platform-adapter",
|
||||
error: error instanceof Error ? error.message : "publisher_adapter_failed",
|
||||
error: message,
|
||||
stack: error instanceof Error ? error.stack ?? null : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function postPublishCallback(
|
||||
callbackBaseUrl: string,
|
||||
payload: PublisherPublishArticleRequest,
|
||||
task: PublisherPublishArticleRequest["tasks"][number],
|
||||
adapterResult: {
|
||||
success: boolean;
|
||||
status: PublisherPublishTaskResult["status"];
|
||||
externalArticleId?: string | null;
|
||||
externalArticleUrl?: string | null;
|
||||
externalManageUrl?: string | null;
|
||||
message?: string | null;
|
||||
responsePayload?: Record<string, unknown>;
|
||||
},
|
||||
) {
|
||||
const externalArticleId = normalizeText(adapterResult.externalArticleId);
|
||||
const externalArticleUrl = normalizeUrl(adapterResult.externalArticleUrl, task.platform_id, externalArticleId);
|
||||
const externalManageUrl = normalizeText(adapterResult.externalManageUrl);
|
||||
|
||||
await postCallback(callbackBaseUrl, "/api/callback/plugin/publish", {
|
||||
plugin_session_id: task.plugin_session_id,
|
||||
session_token: task.session_token,
|
||||
article_id: payload.article_id,
|
||||
publish_record_id: task.publish_record_id,
|
||||
platform_account_id: task.platform_account_id,
|
||||
platform_id: task.platform_id,
|
||||
status: adapterResult.status,
|
||||
external_article_id: externalArticleId,
|
||||
external_article_url: externalArticleUrl,
|
||||
external_manage_url: externalManageUrl,
|
||||
published_at: adapterResult.success ? new Date().toISOString() : null,
|
||||
error_message: adapterResult.success ? null : adapterResult.message,
|
||||
client_version: browser.runtime.getManifest().version,
|
||||
request_payload: {
|
||||
title: payload.title,
|
||||
cover_asset_url: normalizeText(payload.cover_asset_url),
|
||||
publish_type: payload.publish_type,
|
||||
},
|
||||
response_payload: adapterResult.responsePayload ?? {
|
||||
mode: "platform-adapter",
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
publish_record_id: task.publish_record_id,
|
||||
platform_account_id: task.platform_account_id,
|
||||
platform_id: task.platform_id,
|
||||
success: adapterResult.success,
|
||||
status: adapterResult.status,
|
||||
external_article_id: externalArticleId,
|
||||
external_article_url: externalArticleUrl,
|
||||
external_manage_url: externalManageUrl,
|
||||
message: adapterResult.message ?? null,
|
||||
} satisfies PublisherPublishTaskResult;
|
||||
}
|
||||
|
||||
export async function handlePublisherAction(
|
||||
action: PublisherAction,
|
||||
payload?: unknown,
|
||||
@@ -193,110 +270,55 @@ async function handlePublishArticle(payload: PublisherPublishArticleRequest): Pr
|
||||
}).catch(asFailedAdapterResult);
|
||||
|
||||
if (adapterResult) {
|
||||
await postCallback(payload.callback_base_url, "/api/callback/plugin/publish", {
|
||||
plugin_session_id: task.plugin_session_id,
|
||||
session_token: task.session_token,
|
||||
article_id: payload.article_id,
|
||||
publish_record_id: task.publish_record_id,
|
||||
platform_account_id: task.platform_account_id,
|
||||
platform_id: task.platform_id,
|
||||
status: adapterResult.status,
|
||||
external_article_id: normalizeText(adapterResult.externalArticleId),
|
||||
external_article_url: normalizeText(adapterResult.externalArticleUrl),
|
||||
external_manage_url: normalizeText(adapterResult.externalManageUrl),
|
||||
published_at: adapterResult.success ? new Date().toISOString() : null,
|
||||
error_message: adapterResult.success ? null : adapterResult.message,
|
||||
client_version: browser.runtime.getManifest().version,
|
||||
request_payload: {
|
||||
title: payload.title,
|
||||
cover_asset_url: normalizeText(payload.cover_asset_url),
|
||||
publish_type: payload.publish_type,
|
||||
},
|
||||
response_payload: adapterResult.responsePayload ?? {
|
||||
mode: "platform-adapter",
|
||||
},
|
||||
});
|
||||
|
||||
results.push({
|
||||
publish_record_id: task.publish_record_id,
|
||||
platform_account_id: task.platform_account_id,
|
||||
platform_id: task.platform_id,
|
||||
success: adapterResult.success,
|
||||
status: adapterResult.status,
|
||||
external_article_id: normalizeText(adapterResult.externalArticleId),
|
||||
external_article_url: normalizeText(adapterResult.externalArticleUrl),
|
||||
external_manage_url: normalizeText(adapterResult.externalManageUrl),
|
||||
message: adapterResult.message ?? null,
|
||||
});
|
||||
if (!adapterResult.success) {
|
||||
logPublishRuntime("error", "publish-task-failed", {
|
||||
article_id: payload.article_id,
|
||||
publish_record_id: task.publish_record_id,
|
||||
platform_account_id: task.platform_account_id,
|
||||
platform_id: task.platform_id,
|
||||
message: adapterResult.message ?? null,
|
||||
response_payload: adapterResult.responsePayload ?? null,
|
||||
});
|
||||
}
|
||||
results.push(await postPublishCallback(payload.callback_base_url, payload, task, adapterResult));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!platform?.connected) {
|
||||
await postCallback(payload.callback_base_url, "/api/callback/plugin/publish", {
|
||||
plugin_session_id: task.plugin_session_id,
|
||||
session_token: task.session_token,
|
||||
article_id: payload.article_id,
|
||||
publish_record_id: task.publish_record_id,
|
||||
platform_account_id: task.platform_account_id,
|
||||
platform_id: task.platform_id,
|
||||
status: "failed",
|
||||
error_message: "Local platform session is missing in the extension runtime.",
|
||||
client_version: browser.runtime.getManifest().version,
|
||||
request_payload: {
|
||||
title: payload.title,
|
||||
publish_type: payload.publish_type,
|
||||
},
|
||||
});
|
||||
const fallbackFailure = !platform?.connected
|
||||
? {
|
||||
success: false,
|
||||
status: "failed" as const,
|
||||
externalArticleId: null,
|
||||
externalArticleUrl: null,
|
||||
externalManageUrl: normalizeText(platform?.login_url),
|
||||
message: "Local platform session is missing in the extension runtime.",
|
||||
responsePayload: {
|
||||
mode: "missing-session",
|
||||
installation_key: state.installation_key,
|
||||
},
|
||||
}
|
||||
: {
|
||||
success: false,
|
||||
status: "failed" as const,
|
||||
externalArticleId: null,
|
||||
externalArticleUrl: null,
|
||||
externalManageUrl: normalizeText(platform.login_url),
|
||||
message: "No direct-publish adapter is available for this platform yet.",
|
||||
responsePayload: {
|
||||
mode: "missing-adapter",
|
||||
installation_key: state.installation_key,
|
||||
},
|
||||
};
|
||||
|
||||
results.push({
|
||||
publish_record_id: task.publish_record_id,
|
||||
platform_account_id: task.platform_account_id,
|
||||
platform_id: task.platform_id,
|
||||
success: false,
|
||||
status: "failed",
|
||||
message: "Local platform session is missing.",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const externalArticleId = nextExternalArticleId(task.publish_record_id);
|
||||
const externalArticleUrl = buildPublishedUrl(task.platform_id, externalArticleId);
|
||||
|
||||
await postCallback(payload.callback_base_url, "/api/callback/plugin/publish", {
|
||||
plugin_session_id: task.plugin_session_id,
|
||||
session_token: task.session_token,
|
||||
logPublishRuntime("warn", "publish-task-rejected", {
|
||||
article_id: payload.article_id,
|
||||
publish_record_id: task.publish_record_id,
|
||||
platform_account_id: task.platform_account_id,
|
||||
platform_id: task.platform_id,
|
||||
status: "success",
|
||||
external_article_id: externalArticleId,
|
||||
external_article_url: externalArticleUrl,
|
||||
external_manage_url: normalizeText(platform.login_url),
|
||||
published_at: new Date().toISOString(),
|
||||
client_version: browser.runtime.getManifest().version,
|
||||
request_payload: {
|
||||
title: payload.title,
|
||||
cover_asset_url: normalizeText(payload.cover_asset_url),
|
||||
publish_type: payload.publish_type,
|
||||
},
|
||||
response_payload: {
|
||||
mode: "mock-adapter",
|
||||
installation_key: state.installation_key,
|
||||
},
|
||||
});
|
||||
|
||||
results.push({
|
||||
publish_record_id: task.publish_record_id,
|
||||
platform_account_id: task.platform_account_id,
|
||||
platform_id: task.platform_id,
|
||||
success: true,
|
||||
status: "success",
|
||||
external_article_id: externalArticleId,
|
||||
external_article_url: externalArticleUrl,
|
||||
external_manage_url: normalizeText(platform.login_url),
|
||||
message: "Published through the mock adapter.",
|
||||
reason: fallbackFailure.message,
|
||||
response_payload: fallbackFailure.responsePayload,
|
||||
});
|
||||
results.push(await postPublishCallback(payload.callback_base_url, payload, task, fallbackFailure));
|
||||
}
|
||||
|
||||
const successCount = results.filter((item) => item.success).length;
|
||||
|
||||
Reference in New Issue
Block a user